Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/dpop-eddsa-es512.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@dwk/dpop": minor
---

Widen the DPoP proof algorithm allow-list with `EdDSA` (Ed25519, RFC 8037 OKP keys — including the OKP RFC 7638 thumbprint) and `ES512` (P-521 + SHA-512). The spec had marked both "not implemented yet — widen on demand"; Ed25519 in particular is where fediverse client signing is converging. Ed448 stays rejected (`crv_mismatch`) — the Workers runtime has no Web Crypto support for it. Symmetric algorithms and `none` remain excluded.
6 changes: 6 additions & 0 deletions .changeset/mastodon-follow-notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@dwk/activitypub": minor
"@dwk/mastodon-api": minor
---

Implement `follow` notifications (the deferred phase-2 gap): `@dwk/activitypub`'s `#onFollow` now stores a _new_ follower's `Follow` (or FEP-1b12 `Group` membership `Join`) in the actor's inbox — a re-Follow from a still-recorded follower is not a fresh notification — and the `__client/notifications` classifier surfaces those rows; `@dwk/mastodon-api`'s `notificationEntity` maps them to Mastodon's `type: "follow"` (account attached, `status: null`), so clients like Tusky and Pixelfed now see new-follower notifications. Storing via the existing inbox path also queues the follower's actor-profile fetch, so the notification renders with a real display name and avatar once hydrated.
5 changes: 5 additions & 0 deletions .changeset/webmention-deleted-source-resend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@dwk/webmention": minor
---

Implement the Webmention §3.1.5 deleted-source re-send (the spec's last known gap, a SHOULD): `SendOptions` gains an opt-in `sentLog` that records every accepted notification (`createD1SentLog` — D1-backed, own `webmentions_sent` table, strongly consistent), and the new `resendForDeletedSource(source, options)` re-sends to every recorded target once the source serves `410 Gone`, so receivers re-verify and drop the mention. Accepted (or endpoint-less) re-sends clear their log row; failed ones keep it for a later retry. Two new observability events: `webmention.send.sent_log_write_failed` and `webmention.resend.completed`.
81 changes: 54 additions & 27 deletions catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,15 @@
"path": "/micropub",
"match": "exact",
"methods": ["GET", "POST"],
"handler": "createMicropub"
"handler": "createMicropub",
"specificationURL": "https://micropub.spec.indieweb.org/"
},
{
"path": "/media",
"match": "exact",
"methods": ["POST"],
"handler": "createMicropub"
"handler": "createMicropub",
"specificationURL": "https://micropub.spec.indieweb.org/#media-endpoint"
},
{
"path": "/media/",
Expand Down Expand Up @@ -142,7 +144,8 @@
"path": "/microsub",
"match": "exact",
"methods": ["GET", "POST"],
"handler": "createMicrosub"
"handler": "createMicrosub",
"specificationURL": "https://indieweb.org/Microsub-spec"
}
]
},
Expand Down Expand Up @@ -172,7 +175,8 @@
"path": "/webmention",
"match": "exact",
"methods": ["POST"],
"handler": "createWebmention"
"handler": "createWebmention",
"specificationURL": "https://www.w3.org/TR/webmention/"
}
]
},
Expand Down Expand Up @@ -201,7 +205,8 @@
"path": "/websub",
"match": "exact",
"methods": ["POST"],
"handler": "createWebSub"
"handler": "createWebSub",
"specificationURL": "https://www.w3.org/TR/websub/"
}
]
},
Expand All @@ -228,28 +233,32 @@
"match": "prefix",
"methods": ["GET", "POST"],
"head": true,
"handler": "createActivityPub"
"handler": "createActivityPub",
"specificationURL": "https://www.w3.org/TR/activitypub/"
},
{
"path": "/inbox",
"match": "exact",
"methods": ["POST"],
"handler": "createActivityPub"
"handler": "createActivityPub",
"specificationURL": "https://www.w3.org/TR/activitypub/"
},
{
"path": "/.well-known/nodeinfo",
"match": "exact",
"methods": ["GET"],
"head": true,
"handler": "createActivityPub",
"authorityBound": true
"authorityBound": true,
"specificationURL": "https://nodeinfo.diaspora.software/protocol.html"
},
{
"path": "/nodeinfo/",
"match": "prefix",
"methods": ["GET"],
"head": true,
"handler": "createActivityPub"
"handler": "createActivityPub",
"specificationURL": "https://nodeinfo.diaspora.software/protocol.html"
}
]
},
Expand Down Expand Up @@ -295,20 +304,23 @@
"match": "exact",
"methods": ["GET"],
"handler": "createMastodonApi",
"authorityBinding": true
"authorityBinding": true,
"specificationURL": "https://docs.joinmastodon.org/spec/oauth/"
},
{
"path": "/oauth/token",
"match": "exact",
"methods": ["POST"],
"handler": "createMastodonApi",
"authorityBinding": true
"authorityBinding": true,
"specificationURL": "https://docs.joinmastodon.org/spec/oauth/"
},
{
"path": "/oauth/revoke",
"match": "exact",
"methods": ["POST"],
"handler": "createMastodonApi"
"handler": "createMastodonApi",
"specificationURL": "https://docs.joinmastodon.org/spec/oauth/"
}
]
},
Expand All @@ -329,7 +341,8 @@
"methods": ["GET"],
"head": true,
"handler": "createWebfinger",
"authorityBound": true
"authorityBound": true,
"specificationURL": "https://www.rfc-editor.org/rfc/rfc7033"
}
]
},
Expand All @@ -350,15 +363,17 @@
"methods": ["GET"],
"head": true,
"handler": "createHostMeta",
"authorityBound": true
"authorityBound": true,
"specificationURL": "https://www.rfc-editor.org/rfc/rfc6415"
},
{
"path": "/.well-known/host-meta.json",
"match": "exact",
"methods": ["GET"],
"head": true,
"handler": "createHostMeta",
"authorityBound": true
"authorityBound": true,
"specificationURL": "https://www.rfc-editor.org/rfc/rfc6415"
}
]
},
Expand All @@ -384,25 +399,29 @@
"path": "/webauthn/register/options",
"match": "exact",
"methods": ["POST"],
"handler": "createWebAuthn"
"handler": "createWebAuthn",
"specificationURL": "https://www.w3.org/TR/webauthn-3/"
},
{
"path": "/webauthn/register/verify",
"match": "exact",
"methods": ["POST"],
"handler": "createWebAuthn"
"handler": "createWebAuthn",
"specificationURL": "https://www.w3.org/TR/webauthn-3/"
},
{
"path": "/webauthn/authenticate/options",
"match": "exact",
"methods": ["POST"],
"handler": "createWebAuthn"
"handler": "createWebAuthn",
"specificationURL": "https://www.w3.org/TR/webauthn-3/"
},
{
"path": "/webauthn/authenticate/verify",
"match": "exact",
"methods": ["POST"],
"handler": "createWebAuthn"
"handler": "createWebAuthn",
"specificationURL": "https://www.w3.org/TR/webauthn-3/"
}
]
},
Expand Down Expand Up @@ -459,14 +478,16 @@
"match": "exact",
"methods": ["GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS"],
"head": true,
"handler": "createSolidPod"
"handler": "createSolidPod",
"specificationURL": "https://solidproject.org/TR/protocol"
},
{
"path": "/pod/",
"match": "prefix",
"methods": ["GET", "PUT", "POST", "PATCH", "DELETE", "OPTIONS"],
"head": true,
"handler": "createSolidPod"
"handler": "createSolidPod",
"specificationURL": "https://solidproject.org/TR/protocol"
}
],
"triggers": [
Expand Down Expand Up @@ -523,7 +544,8 @@
"OPTIONS"
],
"head": true,
"handler": "createSolidPodWebdav"
"handler": "createSolidPodWebdav",
"specificationURL": "https://www.rfc-editor.org/rfc/rfc4918"
},
{
"path": "/dav/",
Expand All @@ -542,7 +564,8 @@
"OPTIONS"
],
"head": true,
"handler": "createSolidPodWebdav"
"handler": "createSolidPodWebdav",
"specificationURL": "https://www.rfc-editor.org/rfc/rfc4918"
},
{
"path": "/dav-credentials",
Expand Down Expand Up @@ -584,7 +607,8 @@
"match": "prefix",
"methods": ["GET", "PUT", "DELETE", "OPTIONS"],
"head": true,
"handler": "createRemoteStorage"
"handler": "createRemoteStorage",
"specificationURL": "https://datatracker.ietf.org/doc/html/draft-dejong-remotestorage"
}
],
"triggers": [
Expand Down Expand Up @@ -623,23 +647,26 @@
"path": "/xrpc/",
"match": "prefix",
"methods": ["GET", "POST"],
"handler": "createAtprotoPds"
"handler": "createAtprotoPds",
"specificationURL": "https://atproto.com/specs/xrpc"
},
{
"path": "/.well-known/atproto-did",
"match": "exact",
"methods": ["GET"],
"head": true,
"handler": "createAtprotoPds",
"authorityBound": true
"authorityBound": true,
"specificationURL": "https://atproto.com/specs/handle"
},
{
"path": "/.well-known/did.json",
"match": "exact",
"methods": ["GET"],
"head": true,
"handler": "createAtprotoPds",
"authorityBound": true
"authorityBound": true,
"specificationURL": "https://w3c-ccg.github.io/did-method-web/"
}
]
}
Expand Down
62 changes: 62 additions & 0 deletions packages/activitypub/src/mastodon-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,68 @@ describe("buildMastodonBackend", () => {
);
});

it("notifications() surfaces a new follower's Follow once — a re-Follow is not a fresh notification", async () => {
// manuallyApprovesFollowers avoids an outbound actor fetch in this DO.
const config = resolveConfig({
baseUrl: "https://owner.example",
actor: {
username: `owner-${crypto.randomUUID().slice(0, 8)}`,
manuallyApprovesFollowers: true,
},
publicKeyPem: "PUBLIC-PEM",
});
const follow = (id: string) => ({
"@context": "https://www.w3.org/ns/activitystreams",
id: `https://remote.example/activities/${id}`,
type: "Follow",
actor: "https://remote.example/users/frank",
object: config.iris.id,
});
await seedActivity(config, follow("follow-1"));
// A distinct re-Follow (fresh id, so not caught by activity dedup) from
// the same, still-recorded follower is not a new notification.
await seedActivity(config, follow("follow-2"));
const backend = buildMastodonBackend({ config, actor: testEnv.ACTOR });

const page = await backend.notifications({ limit: 10 });
expect(page.entries).toHaveLength(1);
expect(page.entries[0]?.activity["type"]).toBe("Follow");

// After an unfollow, following again is a genuinely new follower — and a
// fresh notification alongside the historical one.
await seedActivity(config, {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://remote.example/activities/unfollow-1",
type: "Undo",
actor: "https://remote.example/users/frank",
object: follow("follow-1"),
});
await seedActivity(config, follow("follow-3"));
const after = await backend.notifications({ limit: 10 });
expect(after.entries).toHaveLength(2);
});

it("notifications() excludes a misaddressed Follow (never recorded)", async () => {
const config = resolveConfig({
baseUrl: "https://owner.example",
actor: {
username: `owner-${crypto.randomUUID().slice(0, 8)}`,
manuallyApprovesFollowers: true,
},
publicKeyPem: "PUBLIC-PEM",
});
await seedActivity(config, {
"@context": "https://www.w3.org/ns/activitystreams",
id: "https://remote.example/activities/misaddressed",
type: "Follow",
actor: "https://remote.example/users/frank",
object: "https://someone-else.example/users/other",
});
const backend = buildMastodonBackend({ config, actor: testEnv.ACTOR });
const page = await backend.notifications({ limit: 10 });
expect(page.entries).toHaveLength(0);
});

it("caps the inbox scan instead of exhausting a plain-post-dominated table", async () => {
const config = freshConfig();
const timestamp = Date.now();
Expand Down
Loading
Loading