Skip to content

feat(release-service): add publication product flow - #2747

Open
ascorbic wants to merge 27 commits into
feat/drs-review-04-verificationfrom
feat/drs-review-05-publication-product
Open

feat(release-service): add publication product flow#2747
ascorbic wants to merge 27 commits into
feat/drs-review-04-verificationfrom
feat/drs-review-05-publication-product

Conversation

@ascorbic

Copy link
Copy Markdown
Collaborator

What does this PR do?

Adds Cloudflare Access authentication and role-specific service control, final reverification, create-only publication, ambiguous-write reconciliation, public/operator APIs, the typed client, official GitHub Action, CLI flows, and publisher/approver/operator web surfaces.

This is PR 5 of 7. Access control and publication/product flow remain separate commits inside the PR so reviewers can evaluate the authority boundary before the consumers. 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 use Lingui and logical RTL-safe layout
  • I have added and reviewed the user-facing changeset for registry-client and plugin-cli
  • 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

Access, service-control, publication/reconciliation Workflows, API clients, plugin CLI, Action, and UI suites pass as part of the complete stack.

@changeset-bot

changeset-bot Bot commented Aug 27, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6c3d97f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@emdash-cms/registry-client Minor
@emdash-cms/plugin-cli Minor
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/release-action Patch
@emdash-cms/release-service Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 13,882 lines across 100 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 Preview to Cloudflare 🚀

Preview URL: https://feat-drs-review-05-publication-product.try.emdashcms.com, https://feat-drs-review-05-publication-product-emdash-playground.emdash-cms.workers.dev (commit 6c3d97f)

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://a30337ea.try.emdashcms.com, https://a30337ea-emdash-playground.emdash-cms.workers.dev 6c3d97f 2026-08-29T07:33:23.194Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://9846d07a.try.emdashcms.com, https://9846d07a-emdash-playground.emdash-cms.workers.dev 2002046 2026-08-29T07:19:01.896Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

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

View logs ↗
https://46cda155.try.emdashcms.com, https://46cda155-emdash-playground.emdash-cms.workers.dev e5c78b8 2026-08-28T22:47:48.957Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://caf5a012.try.emdashcms.com, https://caf5a012-emdash-playground.emdash-cms.workers.dev a83f0b7 2026-08-28T22:20:31.923Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://5d411ff2.try.emdashcms.com, https://5d411ff2-emdash-playground.emdash-cms.workers.dev d29e944 2026-08-28T22:07:42.749Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://06157c4d.try.emdashcms.com, https://06157c4d-emdash-playground.emdash-cms.workers.dev 2735894 2026-08-28T21:39:45.270Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://2cd51ace.try.emdashcms.com, https://2cd51ace-emdash-playground.emdash-cms.workers.dev cd21ca4 2026-08-28T15:16:47.833Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://2c049255.try.emdashcms.com, https://2c049255-emdash-playground.emdash-cms.workers.dev 091e80b 2026-08-28T13:42:47.834Z Visit the dashboard ↗
  • Build: In progress 🔵

View logs ↗
c599d04 2026-08-28T13:35:37.635Z View logs ↗

View all previews: View all previews ↗

@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label Aug 27, 2026
@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 2002046 Aug 29 2026, 07:14 AM

@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.

Comment thread apps/release-action/dist/index.js Dismissed
@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@2747

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-moderation

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

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 6c3d97f

@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review query-count changed PR diff modifies query-count snapshot files and removed review/needs-review No maintainer or bot review yet labels Aug 28, 2026
@ascorbic
ascorbic force-pushed the feat/drs-review-05-publication-product branch from 0ef3e42 to 3b22d6e Compare August 28, 2026 11:15
@github-actions github-actions Bot added query-count changed PR diff modifies query-count snapshot files and removed query-count changed PR diff modifies query-count snapshot files labels Aug 28, 2026
@ascorbic
ascorbic force-pushed the feat/drs-review-05-publication-product branch from ee82ccf to 6e2faf2 Compare August 28, 2026 13:25
@github-actions github-actions Bot removed the query-count changed PR diff modifies query-count snapshot files label Aug 28, 2026
@ascorbic
ascorbic force-pushed the feat/drs-review-05-publication-product branch from 6e2faf2 to c599d04 Compare August 28, 2026 13:31
@ascorbic

Copy link
Copy Markdown
Collaborator Author

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

Automated deep review of this layer's diff (feat/drs-review-04-verification...feat/drs-review-05-publication-product). 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

Verified against code: the layer adds (a) Cloudflare Access authentication for operator surfaces — full JWT verification (RS256, per-role audience, issuer = team domain, exp/nbf/iat, type: "app", subject/email shape checks) with a route-table invariant that fails closed if an /admin/api route lacks a role, plus Origin + x-emdash-request + idempotency-key gating on all operator mutations; (b) a global ServiceControl DO (service mode, per-publisher suspension, single-use token-hashed publication permits bound to a mode epoch, operator idempotency, audit log); (c) the publication product: a final reverification step that recomputes the approval-evidence digest against a fresh PDS snapshot, artifact materialization with checksum/CID-multihash binding, create-only createRecord, a leased publication operation with ambiguous-outcome completion, and reconciliation that adopts an authoritative record only when it is content-identical (canonical JSON, blob refs normalized against approved checksums); (d) public/publisher/operator API routes, a typed registry-client release-service client, a Node GitHub Action (random-delimiter GITHUB_OUTPUT, masked OIDC token, commit-pinning docs), CLI release submit/status/cancel, and a Kumo-based publisher/approver/operator SPA. The security core (Access boundary, permit/lease/CAS machinery, reconciliation identity test, digest-bound approvals, userVerification: "required" server-side) is genuinely well built and well tested. The quality problems are concentrated in failure-path liveness and in the published client's response validation.

Findings

  • [high] packages/registry-client/src/release-service/index.ts (parseIntent, updatedAt > expiresAt || check) — the client rejects every intent in the expired state as an invalid response. The DO's transition() allows the expired transition only when expiresAt <= now and then writes updated_at = now (intent-state.ts, UPDATE intents ... updated_at = ?), so every expired intent has updatedAt > expiresAt; parseIntent throws CLIENT_RESPONSE_INVALID on exactly that. Failure scenario: one approval times out → the publisher dashboard (PublisherPage.refresh() calls listPublisherIntents({limit:100}) inside Promise.all) throws and the whole page shows a permanent error; emdash-plugin release status and Action polling on that intent also fail with a bogus "invalid response". Same invariant is violated by recoverExpired/completePublicationOperation, which update updated_at past expiry without the transition guard. CONFIRMED.
  • [high] apps/release-service/src/publishing/workflow.ts (final-verification-${attempt} step) + src/verification/pds.tsPublisherSnapshotError thrown during final reverification is never mapped to a terminal intent state; the workflow instance errors and the intent is stranded. readPublisherVerificationSnapshot throws RELEASE_EXISTS if the proposed release record exists and PROFILE_INVALID if the package profile record is missing; the step has no catch, so the step retries and then the whole run errors, leaving the intent in ready (or reconciling) instead of invalid/conflict. Deterministic trigger: publisher deletes or replaces their package-profile record (or manually creates the release record) after approval but before publication — exactly the drift final reverification exists to handle. Race trigger: ambiguous write → reconcile reads absent → reconcile-absence returns intent to ready → the late write lands → attempt N+1's snapshot throws RELEASE_EXISTS. Operator POST /admin/api/intents/{id}/reconcile restarts the instance into the identical crash (recovery for ready goes straight to publishVerifiedIntent), so there is no recovery except cancelling an intent whose record may already be live. The first-verification steps have the same unhandled throw, but that is inherited from layer 04; final-verification is new here. CONFIRMED code path.
  • [medium] apps/release-service/src/publishing/workflow.ts (publication-attempt-${attempt} re-entry) — a single transient DO error between transition(→publishing) and beginPublicationOperation permanently wedges the intent in publishing. On step retry the code sees state === "publishing" and returns failed INTENT_NOT_READY instead of calling begin (which is specifically designed to run against a publishing intent). Because no operation row was ever created, recoverExpired never fires (it scans publication_operations, publication-operation.ts); publishing is not in CANCELLABLE_STATES (operator or workload cancel → 409) and restartReleaseIntentWorkflow requires ready/reconciling → 409. Result: intent shows "Publishing" forever and the version reservation blocks resubmission until the 24 h intent expiry. CONFIRMED path; trigger is one unluckily-timed RPC failure.
  • [medium] apps/release-service/src/workload/policy.ts (digestWorkloadIdempotencyIdentity includes run.id + run.attempt) — a GitHub re-run can neither replay, read, nor cancel the previous attempt's intent. Replay lookup and authorizeIntent both key on this digest, so on re-run: custom "stable" idempotency-key still misses (digest differs) → new intent → VERSION_RESERVED 409 while the prior intent lives (up to 24 h); and cancel/status of the old intent → 403 ACCESS_DENIED. The only recovery is a human cancelling via the publisher web UI. action.yml's idempotency-key description ("Stable key for replaying this submission") overpromises; the README's "one run attempt" caveat contradicts it. CONFIRMED.
  • [medium] apps/release-service/src/publishing/workflow.ts (PUBLICATION_TTL_MS = 30_000) — the 30 s permit + 30 s operation lease must cover artifact fetch (up to the bundle max) plus serial blob uploads (package, icon, banner, every screenshot) plus createRecord. Any publication slower than 30 s ends with complete(published) rejected on the expired lease → forced through the ambiguous/reconciliation detour (extra direct-PDS reads, PDS_AMBIGUOUS audit noise) on every slow publish; it self-heals only because reconciliation then adopts the record. The permit is also consumed before materialization, so pause/suspension enforcement can lag the actual write by however long materialization takes. MAX_LEASE_MS allows 5 min; 30 s looks under-provisioned. CONFIRMED behavior, severity operational.
  • [low] apps/release-service/src/routes.ts + control-do/routes.ts + operator/routes.tstwo parallel operator surfaces with divergent semantics. /admin/api/publishers/{did}/suspend writes control-DO state and suspends the publisher DO (deleting sessions); /admin/api/admin/publisher-control writes only control-DO state, leaving publisher sessions alive. Same for /admin/api/pause vs /admin/api/admin/service-mode. Enforcement gates are control-DO based so this is not an escalation, but an admin using the role-prefixed route gets weaker suspension than the other route, silently. Also the suspend route commits the control-DO write before the publisher-DO write; a failure in between leaves the halves inconsistent until a client retry replays it. CONFIRMED.
  • [low] apps/release-service/wrangler.jsoncrun_worker_first lists "/admin/*" but not bare /admin, while /publisher* deliberately uses the no-slash glob. If production glob semantics match the usual "prefix under /admin/" reading, the bare /admin SPA shell is served by asset infra without the Worker's Access check (the test SELF.fetch(".../admin") expecting 401 may not reflect edge behavior). Impact is minimal — it's the same public shell served at /publisher, all data comes from authenticated /admin/api — but the defense-in-depth gate is inconsistent. PLAUSIBLE (depends on production glob semantics).
  • [low] apps/release-service/src/workflows/release-intent.ts — if waitForEvent wakes before the intent deadline while still awaiting_approval, run() throws a plain Error and the instance ends errored; a later approval still transitions the intent to ready (decision route) but sendEvent fails silently, so nothing publishes until an operator manually reconciles. The approver sees success; the release silently stalls. PLAUSIBLE (timing edge).
  • [note] No rate limiting anywhere, including unauthenticated-until-verified POST /v1/release-intents (JWKS fetch + JWT verify per request) — presumably deferred to the operations layer; worth confirming it actually lands there. (It does — layer 06 adds per-publisher limits consumed in the submit-intent route — but nothing covers the pre-verification JWT/JWKS work.)

PR description vs code

  • "Final reverification": holds for content/evidence drift (digest mismatch → invalid), but snapshot-read failures — including the two most natural drift cases, record-already-exists and profile-gone — crash the workflow instead of producing a terminal state (finding 2).
  • "Ambiguous-write reconciliation": correct and well-tested for the mainline ambiguous path; the wedged-publishing edge has no reconciliation or operator escape (finding 3).
  • Changeset says the clients "validate response envelopes" — they do, but the validation itself is wrong for expired intents (finding 1), and ReleaseServiceOperatorClient mutations can only work from a browser behind Access (the client never sets cf-access-jwt-assertion and never sets Origin, which validateAccessMutation requires) — the changeset/readme don't say the operator client is browser-only.
  • action.yml's idempotency-key description implies cross-run replay that the digest scoping makes impossible (finding 4).

Test-coverage gaps

  • No client test parses an intent in the expired state (would have caught finding 1 immediately; App.test.tsx and release-service.test.ts fixtures are all pre-expiry).
  • No workflow test for final-verification against a PDS where the release record already exists or the profile record is missing post-approval (finding 2), nor for operator reconcile of such an intent.
  • No test re-enters publication-attempt with the intent already in publishing (Workflow step-retry semantics; finding 3).
  • No cross-attempt re-run scenario for the Action/CLI (submit attempt 1 → re-run attempt 2: expected behavior is undefined-by-test; finding 4).
  • Access boundary, permit single-use/epoch-staleness, reconciliation absence/exact/conflict, checksum-bound blob normalization, and Action OIDC handling are all well covered — credit where due.

~ 🤖 Fable

@github-actions github-actions Bot removed the query-count changed PR diff modifies query-count snapshot files label Aug 28, 2026
@ascorbic
ascorbic force-pushed the feat/drs-review-05-publication-product branch from e49b7ef to 2002046 Compare August 29, 2026 07:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla: signed overlap review/needs-rereview Author pushed changes since the last review size/XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants