Skip to content

feat(release-service): add publisher authority and admission - #2744

Open
ascorbic wants to merge 12 commits into
feat/drs-review-01-foundationfrom
feat/drs-review-02-authority-admission
Open

feat(release-service): add publisher authority and admission#2744
ascorbic wants to merge 12 commits into
feat/drs-review-01-foundationfrom
feat/drs-review-02-authority-admission

Conversation

@ascorbic

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds publisher identity and confidential OAuth custody, encrypted retained delegation, application sessions, serialized refresh, GitHub Actions OIDC verification, publisher-owned workload policy, intent admission, idempotent package/version reservations, and generation-bound publication operations.

This is PR 2 of 7. It keeps the five custody, compatibility, log-redaction, and admission commits separate inside the PR. The stack merges as one unit and this branch is not deployable by itself.

Discussion: #1590

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (not applicable in this layer)
  • I have added and reviewed the user-facing changeset (not applicable; this layer changes a private application)
  • New features link to an approved Discussion: RFC: Attested Automated Publishing #1590

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: OpenAI Codex (GPT-5)

Screenshots / test output

OAuth custody/callback, publisher Durable Object, session, OIDC claim-substitution, workload-policy, intent-state, reservation, replay, and publication-operation tests pass in workerd.

@changeset-bot

changeset-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: d1e52bb

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet size/XL labels Aug 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 6,708 lines across 27 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 27, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs d1e52bb Aug 28 2026, 11:11 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 27, 2026

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://feat-drs-review-02-authority-admission.try.emdashcms.com, https://feat-drs-review-02-authority-admission-emdash-playground.emdash-cms.workers.dev (commit d1e52bb)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://dd60b755.try.emdashcms.com, https://dd60b755-emdash-playground.emdash-cms.workers.dev d1e52bb 2026-08-28T23:16:27.377Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://7361bd04.try.emdashcms.com, https://7361bd04-emdash-playground.emdash-cms.workers.dev 993e54c 2026-08-28T21:34:35.889Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://087e9b25.try.emdashcms.com, https://087e9b25-emdash-playground.emdash-cms.workers.dev 1e96322 2026-08-28T15:24:53.541Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://69ca2aa8.try.emdashcms.com, https://69ca2aa8-emdash-playground.emdash-cms.workers.dev 35c901a 2026-08-28T11:20:55.572Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://1aa2194e.try.emdashcms.com, https://1aa2194e-emdash-playground.emdash-cms.workers.dev 2137dcc 2026-08-28T10:26:34.015Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://6a13df53.try.emdashcms.com, https://6a13df53-emdash-playground.emdash-cms.workers.dev ac2a17f 2026-08-27T15:15:56.398Z Visit the dashboard ↗

@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@ascorbic
ascorbic force-pushed the feat/drs-review-02-authority-admission branch from ac2a17f to 2137dcc Compare August 28, 2026 10:15
@pkg-pr-new

pkg-pr-new Bot commented Aug 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/admin@2744

@emdash-cms/auth

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth@2744

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth-atproto@2744

@emdash-cms/blocks

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/blocks@2744

@emdash-cms/cloudflare

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/cloudflare@2744

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/contentful-to-portable-text@2744

emdash

npm i https://pkg.pr.new/emdash-cms/emdash@2744

create-emdash

npm i https://pkg.pr.new/emdash-cms/emdash/create-emdash@2744

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/gutenberg-to-portable-text@2744

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-cli@2744

@emdash-cms/plugin-types

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-types@2744

@emdash-cms/registry-client

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-client@2744

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-lexicons@2744

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-moderation@2744

@emdash-cms/registry-verification

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-verification@2744

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/sandbox-workerd@2744

@emdash-cms/x402

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/x402@2744

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-ai-moderation@2744

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-atproto@2744

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-audit-log@2744

@emdash-cms/plugin-color

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-color@2744

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-embeds@2744

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-field-kit@2744

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-forms@2744

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-webhook-notifier@2744

commit: d1e52bb

@ascorbic
ascorbic force-pushed the feat/drs-review-02-authority-admission branch from 2137dcc to 35c901a Compare August 28, 2026 11:15
@ascorbic

Copy link
Copy Markdown
Collaborator Author

🔍 Adversarial review — stack #2766, layer 3 of 8

Automated deep review of this layer's diff (feat/drs-review-01-foundation...feat/drs-review-02-authority-admission). Findings ranked most-severe first; confidence is CONFIRMED (full code path read) or PLAUSIBLE (strong suspicion, path partially read). Nothing was auto-fixed.

What the PR actually does + verdict

At this layer the PR adds: (a) a PublisherDurableObject (one per publisher DID, SQLite-backed) holding OAuth transaction state, a single encrypted delegation row with CAS state-versioning, hashed publisher app sessions with epoch-based mass revocation, workload policies, release intents with reservations/idempotency, and lease-based publication operations; (b) OAuth custody stores bridging @atcute/oauth-node-client to the DO, with layer-01 JWE envelope encryption context-bound to {purpose, table, primaryKey, ownerDid} so ciphertext cannot be replayed across publishers, identity-flow sessions kept request-local in memory, delegation-flow refresh serialized through a DO lease; (c) three HTTP routes (identity authorize, delegation authorize, callback) with __Host- cookies, double-submit CSRF, state-to-cookie binding; (d) GitHub Actions OIDC verification and policy evaluation — which are not wired to any route at this layer (used only by tests). Overall quality is high: real-DO tests (no mocked security boundary), fail-closed validation everywhere, hashed-at-rest tokens, genuine refresh serialization (verified against the library: SessionGetter.get wraps every read in requestLockbeginDelegationRefresh lease with token+generation+version binding, all writes in transactionSync). The substantive defects are in state-machine/lifecycle coherence, not in crypto or authentication.

Findings

  • [high] apps/release-service/src/publisher-do/intent-state.ts:345-364,467-469 + publication-operation.ts:140-232,234-307 — expired-but-non-terminal intents lose their version reservation while the publication path ignores intent expiry, so two intents can drive the same package+version concurrently. create() deletes a reservation whose intent has expires_at <= now and is not terminal/published — including intents in publishing/reconciling. But beginPublicationOperation/completePublicationOperation never check intent expiry (only state === 'publishing' + generation), so the first intent can still complete its PDS write while a second intent, admitted after the steal, publishes the same version. Additionally, an expired intent in publishing/reconciling is permanently wedged in transition(): every toState !== 'expired' is blocked by the expiry guard (line 467) and 'expired' is absent from ALLOWED_TRANSITIONS['publishing'|'reconciling'] (lines 39-40) — so post-recovery reconciliation of an expired intent can never record a final state. Wedge + reservation release: CONFIRMED; end-to-end double publish depends on later layers' reconciliation: PLAUSIBLE.
  • [medium] apps/release-service/src/publisher-do/publisher-do.ts:1110-1128 — expired-row GC is alarm-starved: #scheduleNextAlarm sets an alarm only from publication_operations deadlines and deletes it otherwise, yet alarm() is the only place oauth_states and intent_idempotency expired rows are purged. A publisher DO with no publication activity accumulates rows forever, and putOAuthState is reachable unauthenticated via POST /v1/publisher/session/authorize (the Origin/x-emdash-request check at src/oauth/routes.ts:103-110 is trivially satisfiable by non-browser clients), so anyone can grow a victim DID's DO storage without bound. CONFIRMED.
  • [medium] apps/release-service/src/oauth/custody.ts:41-42,567-599 — abandoned refresh lease blocks all delegation use with no possible wait-out: waiters poll for REFRESH_LOCK_TIMEOUT_MS (30s) but a crashed holder's lease lasts REFRESH_LEASE_MS (60s), so every waiter is guaranteed OAUTH_REFRESH_LOCK_TIMEOUT during the tail ~30s. Compounding it, the library acquires this lock on every session read (verified in @atcute/oauth-node-client session-getter.js:133), not just on refresh, so each restore costs a lease-acquire write + generation bump and all reads for a publisher serialize. CONFIRMED mechanics; self-heals after lease expiry.
  • [medium] apps/release-service/src/publisher-do/workload-policy.ts:156-166 vs src/workload/policy.ts:29workflow_ref matching is case-inconsistent: put() lowercases repository and compares repo names case-insensitively, but stores workflowRef verbatim, and evaluateWorkloadPolicy exact-compares it against GitHub's canonical-case claim. A publisher who registers myorg/myrepo/.github/workflows/release.yml@... for a repo canonically cased MyOrg/MyRepo gets WORKLOAD_WORKFLOW_MISMATCH on every admission. Fail-closed (availability, not security). CONFIRMED inconsistency; whether it bites depends on how later layers source the value.
  • [low] apps/release-service/src/workload/policy.ts:17-43 — policy cannot constrain job_workflow_ref, event_name, or actor: a pinned caller workflow invoking an arbitrary reusable workflow, or triggered via pull_request_target, passes evaluation. The workflow_ref-belongs-to-repo check in github-oidc.ts:80 prevents cross-repo caller spoofing, but the reusable-workflow dimension is normalized, digested, and then ignored. Design gap; publisher controls the pinned workflow's content. PLAUSIBLE.
  • [low] No OIDC jti replay ledger anywhere at this layer: within the 10-minute maxTokenAge window a captured token can be presented repeatedly; digestWorkloadIdempotencyIdentity deliberately excludes jti, so replays with different package/version/idempotency keys mint distinct intents. Depends on whether a later layer records tokenId; judged as-is: PLAUSIBLE.
  • [low] apps/release-service/src/oauth/custody.ts:457-458,486-495,546-557 — the #preserveNextDelete protocol guards a call sequence the real library never makes: CachedGetter.getStored (verified) swallows store.get errors and skips deleteStored when storedValue is undefined, so the "delete after failed get" it defends against cannot occur via the library; meanwhile the sticky flag will silently no-op the next legitimate delete() (i.e., a revocation) on the same store instance after any failed get. Fragile cross-call state; currently unreachable harm: CONFIRMED behavior.
  • [low] apps/release-service/src/oauth/routes.ts:289-292 — identity-flow tokens (scope atproto, and a refresh_token when the PDS issues one) are abandoned in request memory without upstream revocation after the app session is minted; every login leaves a live grant on the user's PDS. CONFIRMED.
  • [low] apps/release-service/src/oauth/custody.ts:526-529 — with an active delegation, a second delegation authorization always fails CAS at the end of the OAuth dance (the library then revokes the just-issued tokens upstream — verified in oauth-client.js callback + onStoreError), and no revoke route exists at this layer, so consent rotation is a dead-end returning a generic OAUTH_CALLBACK_INVALID. Safe but a guaranteed UX trap. CONFIRMED.

PR description vs code

  • "GitHub Actions OIDC verification", "workload policy", and "intent admission" ship as unwired code: no HTTP route calls verifyGitHubActionsToken, evaluateWorkloadPolicy, or createIntent — they exist only as DO RPC methods and exported functions exercised by tests. The only live HTTP surface added is the two authorize routes and the callback.
  • "Serialized refresh" is accurate and stronger than claimed (lease + generation + hashed token + CAS on state version, all inside transactionSync).
  • "Idempotent package/version reservations" are per-publisher-DO only; nothing at this layer prevents two different publishers reserving the same package name (presumably a later product-layer concern, but the wording overstates this layer).

Test-coverage gaps

  • No test of intent expiry while in publishing/reconciling (would have caught the high finding); publisher-intent-state.test.ts:145 only covers expiry in received.
  • No test that expired oauth_states/intent_idempotency rows are ever purged absent publication activity (would have caught the alarm starvation).
  • No custody-level test of an abandoned refresh lease vs the 30s waiter timeout (DO-level lease takeover is tested; the custody polling mismatch is not).
  • No case-variant workflow_ref test between policy storage and evaluation.
  • OIDC tests omit alg-confusion (e.g., an HS256/ES256 token), aud-as-array, and any reusable-workflow policy scenario — the job_workflow_ref tests only cover claim shape, not that policy ignores it.
  • The real OAuthClient.restore()/refresh path (requestLock through the library against a fake PDS) is never exercised — refresh serialization is only tested by calling custody.requestLock directly with the lock name hard-coded in the test.

No repo-convention violations found; tests run against real DO storage and real envelope encryption, which is exactly right for this code.

~ 🤖 Fable

@ascorbic
ascorbic force-pushed the feat/drs-review-02-authority-admission branch from 35c901a to 1e96322 Compare August 28, 2026 15:11
@ascorbic
ascorbic force-pushed the feat/drs-review-02-authority-admission branch from 1e96322 to 36d5b60 Compare August 28, 2026 21:15
Comment thread apps/release-service/test/publication-materialization.test.ts Dismissed
Comment thread apps/release-service/test/publication-materialization.test.ts Dismissed
@ascorbic
ascorbic force-pushed the feat/drs-review-02-authority-admission branch from 36d5b60 to 993e54c Compare August 28, 2026 21:19
@github-actions github-actions Bot added the review/needs-rereview Author pushed changes since the last review label Aug 28, 2026
@github-actions github-actions Bot removed the review/needs-review No maintainer or bot review yet label Aug 28, 2026
@ascorbic
ascorbic force-pushed the feat/drs-review-02-authority-admission branch from 993e54c to d1e52bb Compare August 28, 2026 23:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants