Skip to content

feat(activitypub): emit the actor document's profile-page url - #513

Closed
davidwkeith wants to merge 2 commits into
mainfrom
feat/activitypub-actor-url
Closed

davidwkeith wants to merge 2 commits into
mainfrom
feat/activitypub-actor-url

Conversation

@davidwkeith

@davidwkeith davidwkeith commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Summary

The actor document never carried a top-level url, so every platform's "open original profile" link (Mastodon, Misskey, Pixelfed, Friendica) fell back to the actor id — the AS2 JSON document itself, not a web page. Surfaced by Anglesite's Fediverse interop conformance pass (Anglesite/Anglesite#1771).

  • buildActorDocument gains an ActorDocumentOptions.url seam and emits it as the actor url (AS2 §4.1's human-readable representation, distinct from id), mirroring how the FEP-2c59 webfinger back-link is threaded.
  • resolveConfig resolves ResolvedConfig.url to actor.url when set, else the normalized baseUrl root (https://example.com/) — v1 serves one actor per baseUrl, so the site's home page is the profile. ActorProfile.url is the new optional override for a profile that lives elsewhere.
  • The handler passes the resolved value to the actor route; every served actor document now links its profile page with no consumer change.

Additive and backward-compatible: existing configs get the baseUrl-root default automatically.

Packages affected

@dwk/activitypub

Checklist

  • Read the relevant spec(s) under spec/packages/ and updated them if
    behaviour changed — spec/packages/activitypub.md ▸ "Actor & collections" gains the url bullet
  • Added/updated colocated tests (src/*.test.ts) — as2.test.ts (emit/omit), config.test.ts (default, trailing-slash baseUrl, actor.url override), index.test.ts (served document carries url)
  • Ran the local CI gate: pnpm lint && pnpm format:check && pnpm typecheck && pnpm build && pnpm test — lint, format, typecheck, build clean; pnpm test --project @dwk/activitypub 376 passed (16 files). The first CI run failed on mastodon-api.test.tsrespondToFollowRequest(actor, 'authorize') … ("expected [] to have a length of 1"), which also failed once locally: the real DO alarm can resolve and drain the queued Accept through the test's 202 fetch stub before the assertion reads the delivery row. The second commit hardens that test and its 'reject' sibling to count the activity wherever the alarm left it — a test-only change, unrelated to the actor document.
  • Added a changeset (pnpm changeset) if this touches a publishable
    package — .changeset/actor-profile-url.md, minor (new optional field + new emitted property)
  • Updated catalog.json / conformance/status.json if this adds a new
    mountable worker or changes conformance status — not applicable: no new worker, conformance status unchanged

🤖 Generated with Claude Code

The actor document never carried a top-level `url`, so every platform's
"open original profile" link fell back to the actor `id` — the AS2 JSON
document, not a web page. Resolve `ResolvedConfig.url` to `actor.url` or
the `baseUrl` root (one actor per `baseUrl`, so the home page is the
profile) and emit it via a new `ActorDocumentOptions.url` seam, threaded
the same way as the FEP-2c59 `webfinger` back-link.

Surfaced by Anglesite/Anglesite#1771.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Copy link
Copy Markdown
Owner Author

Reviewed the diff against CONTRIBUTING.md and the code itself.

CONTRIBUTING.md conformance — all good:

  • PR title feat(activitypub): emit the actor document's profile-page url is correct Conventional Commits style (lowercase type, (scope), uncapitalized subject).
  • PR body uses the template verbatim (Summary / Packages affected / Checklist headings unchanged).
  • Spec updated in the same PR (spec/packages/activitypub.md).
  • Colocated tests added in all three touched surfaces (as2.test.ts, config.test.ts, index.test.ts).
  • Changeset present and correctly scoped (minor, additive field).
  • catalog.json/conformance/status.json checklist item correctly left unchecked with a one-line "not applicable" reason rather than deleted.

Code review — no bugs found, a couple of minor observations:

  • resolveConfig's url = config.actor.url ?? \${baseUrl}/`correctly relies onnormalizeBaseUrlhaving already stripped any trailing slash, so it can't double up — verified againstnormalizeBaseUrl` (config.ts:288) and the trailing-slash test case.
  • ResolvedConfig.url is a new required (non-optional) field, but since the type is only ever constructed by resolveConfig itself (no other manual ResolvedConfig literals in the package), this can't cause a compile break elsewhere.
  • Nit (non-blocking): config.actor.url uses ??, so an explicit empty string would flow through as doc.url = "" instead of falling back to the baseUrl root. Given every other optional string field in ActorProfile (summary, icon, etc.) has the same characteristic, this is consistent with existing conventions rather than a new issue — just flagging in case stricter validation is ever desired.
  • Out of scope but worth a follow-up issue: mastodon-api.ts's Account JSON mapping doesn't appear to surface resolved.url anywhere, so Mastodon-API clients reading accounts through that surface won't get the same profile-page fix this PR gives raw AS2 consumers. Not a blocker for this PR, which is explicitly scoped to the actor document.

Nothing here blocks merge — checklist and implementation both look correct and minimal.


Generated by Claude Code

…ependent

`respondToFollowRequest` goes through the real external DO stub, which arms a
genuinely-due alarm the test environment fires on its own schedule. When that
alarm won the race it resolved the pending Accept *and* drained it through
the test's 202 fetch stub in the same pass, deleting the `delivery` row the
'authorize' test counted (CI: "expected [] to have a length of 1"); the
'reject' test had the same exposure via `pending_accept` → `delivery`.
Count the activity wherever the alarm left it: still queued, or already
POSTed through the stub.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@davidwkeith

Copy link
Copy Markdown
Owner Author

Superseded by #514 — identical diff as one commit authored under the account's verified email, so the required_signatures / unattributed-changes rules can evaluate it. This branch's two commits were authored under me@dwk.io, which GitHub could not attribute (verified=false, reason=no_user).

@davidwkeith davidwkeith closed this Sep 3, 2026
auto-merge was automatically disabled September 3, 2026 15:58

Pull request was closed

@davidwkeith
davidwkeith deleted the feat/activitypub-actor-url branch September 3, 2026 15:58
davidwkeith added a commit that referenced this pull request Sep 3, 2026
The actor document never carried a top-level `url`, so every platform's "open original profile" link fell back to the actor `id` — the AS2 JSON document, not a web page. Resolve `ResolvedConfig.url` to `actor.url` (validated as an absolute URL at startup) or the `baseUrl` root and emit it via a new `ActorDocumentOptions.url` seam, threaded like the FEP-2c59 `webfinger` back-link. Also hardens the two follow-request delivery tests against the real DO alarm draining the queue first.

Surfaced by Anglesite/Anglesite#1771. Supersedes #513.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
davidwkeith added a commit to Anglesite/Anglesite that referenced this pull request Sep 3, 2026
* feat(#1771): thread AP_ICON into the ActivityPub actor document

The served actor had no `icon`, so Mastodon, Misskey, Pixelfed, and
Friendica all rendered their placeholder avatar. Thread a site-owned
image through as a new `AP_ICON` wrangler var, next to AP_DISPLAY_NAME/
AP_USERNAME: `DeployCoordinator.resolveActivityPubIcon` reads a
hand-editable `.site-config` `AP_ICON` override, else defaults to
`/apple-touch-icon.png` whenever `public/` ships one (the template always
does, and Settings ▸ Website Icon overwrites that exact file in place).
`worker.ts` resolves a root-relative value against the serving origin at
request time and drops anything non-http(s) or unparseable, matching
`resolvePreferredUsername`'s posture for hand-edited config.

Icon half of #1771 only — the `url` half is the package seam in
davidwkeith/workers#513 and goes live with the @dwk/activitypub bump.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(#1788): require a non-empty apple-touch-icon for the AP_ICON default

Review observation on #1788: the default only checked that the file
existed, so a 0-byte apple-touch-icon.png (an interrupted icon install)
would advertise an avatar every peer fetches and fails to render. Require
a non-empty file; a corrupt-but-non-empty file stays undetected since this
portable module has no image decoder.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(#1771): consume @dwk/activitypub 1.0.0-beta.5 for the actor url

The package (davidwkeith/workers#514, released as 1.0.0-beta.5) now emits
the actor document's `url`, defaulting to the baseUrl root, so every
platform's "open original profile" lands on the site home page instead of
the AS2 JSON. Pin it, regenerate the website-template attributions
manifest CI diffs, and assert the served document's `url` in
worker.test.ts. workers-version.json's pin map is brought back in line
with package.json (it had drifted since #1073; nothing reads it but docs).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant