feat: signed cross-instance write path (#10) #65

Merged
claude-bot merged 2 commits from feat/v7-signed-write-path into main 2026-06-30 01:41:04 +00:00
Contributor

Closes #10. Third slice of v7 — invited remote editors can now create/update entries in a host project over the signed AP inbox.

Flow

A Create/Update of a Note whose context is a project's AP URL (${appUrl}/ap/projects/:id) is routed as an entry write, ahead of the existing comment path:

  1. Signature verified against the actor's cached key (existing verifyInboxSignature).
  2. Anti-spoofing — new parseSignatureKeyId() exposes the verified signer, and the inbox requires signer === activity.actor. A valid signature from actor A cannot write as actor B → 401.
  3. Authorization — the actor must be an accepted remote editor on the target project (project_collaborators). Verified-but-uninvited → 403. No open-inbox path.
  4. Applyservices/remoteContribution.ts writes the entry with user_id NULL + attribution snapshotted from the remote actor, records a remote_contributions row (idempotent on ap_activity_id), and embeds in the background.

Updates target ${appUrl}/ap/entries/:id and apply last-write-wins for now; the optimistic-lock conflict gate + review queue are #11.

Design notes

  • Rather than refactoring the session-bound HTTP entry route, remote writes get a dedicated service — the remote path genuinely differs (no user, actor attribution, remote_contributions instead of audit_log, conflict handling to come), so a separate function is lower-risk than over-generalizing.
  • Audit lives in remote_contributions (there's no user FK for a remote author).

Verification (dev server)

remoteWritePath.test.ts5 tests that forge real HTTP signatures with a generated RSA key:

  • signed Create by invited editor → 202, entry with user_id NULL / source='federation' / actor attribution + remote_contributions row;
  • idempotent on activity id (no dup entry);
  • verified non-editor → 403;
  • actor spoofing (sign as A, claim actor B) → 401, no entry;
  • signed Update edits the target entry.

Full API suite 195 passed (was 190), no regressions to the existing Follow/comment/actor inbox handling.

🤖 Generated with Claude Code

Closes #10. Third slice of v7 — invited remote editors can now create/update entries in a host project over the signed AP inbox. ## Flow A `Create`/`Update` of a `Note` whose `context` is a project's AP URL (`${appUrl}/ap/projects/:id`) is routed as an **entry write**, ahead of the existing comment path: 1. **Signature** verified against the actor's cached key (existing `verifyInboxSignature`). 2. **Anti-spoofing** — new `parseSignatureKeyId()` exposes the verified signer, and the inbox requires `signer === activity.actor`. A valid signature from actor A cannot write as actor B → `401`. 3. **Authorization** — the actor must be an *accepted remote editor* on the target project (`project_collaborators`). Verified-but-uninvited → `403`. No open-inbox path. 4. **Apply** — `services/remoteContribution.ts` writes the entry with `user_id NULL` + attribution snapshotted from the remote actor, records a `remote_contributions` row (idempotent on `ap_activity_id`), and embeds in the background. Updates target `${appUrl}/ap/entries/:id` and apply **last-write-wins** for now; the optimistic-lock conflict gate + review queue are #11. ## Design notes - Rather than refactoring the session-bound HTTP entry route, remote writes get a dedicated service — the remote path genuinely differs (no user, actor attribution, `remote_contributions` instead of `audit_log`, conflict handling to come), so a separate function is lower-risk than over-generalizing. - Audit lives in `remote_contributions` (there's no user FK for a remote author). ## Verification (dev server) `remoteWritePath.test.ts` — **5 tests** that forge real HTTP signatures with a generated RSA key: - signed Create by invited editor → `202`, entry with `user_id NULL` / `source='federation'` / actor attribution + `remote_contributions` row; - idempotent on activity id (no dup entry); - verified non-editor → `403`; - **actor spoofing** (sign as A, claim actor B) → `401`, no entry; - signed Update edits the target entry. Full API suite **195 passed** (was 190), no regressions to the existing Follow/comment/actor inbox handling. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Accept entry writes from invited remote editors over the signed AP inbox.

- New services/remoteContribution.ts: applyRemoteCreate / applyRemoteUpdate
  write a remote-authored entry (user_id NULL, attribution from the actor),
  record it in remote_contributions, and embed in the background. Idempotent
  on ap_activity_id. Updates apply LWW for now (#11 adds the conflict gate).
- Inbox (activitypub.ts): a Create/Update of a Note addressed to a project's
  AP context (${appUrl}/ap/projects/:id) is routed as an entry write, ahead of
  the comment path. Authorization requires the actor to be an accepted remote
  editor on that project — no open inbox.
- Security: parseSignatureKeyId() exposes the verified signer so the inbox can
  bind it to activity.actor (a valid signature from actor A cannot write as
  actor B → 401). Unauthorized-but-verified actor → 403.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
docs(p10): changelog for signed write path (#10)
All checks were successful
CI / App — typecheck, test & build (push) Successful in 1m51s
CI / API — typecheck & tests (push) Successful in 2m42s
CI / App — typecheck, test & build (pull_request) Successful in 1m39s
CI / API — typecheck & tests (pull_request) Successful in 2m36s
8671e19cd5
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-06-30 01:38:30 +00:00
claude-bot deleted branch feat/v7-signed-write-path 2026-06-30 01:41:04 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
rbrooks/TeaLeaves!65
No description provided.