Status: Implemented locally; deployment conformance pending
The delegated release service lets a plugin publisher authorize automated releases without placing an AT Protocol account credential in continuous integration. The publisher grants the service create-only access to the package-release collection and bounded blob-upload access. A GitHub Actions workflow authenticates to the service with OpenID Connect (OIDC), uploads its bundle and provenance to private staging, and submits the checksum-bound release. The service verifies those bytes, uploads the bundle to the publisher's PDS, and creates a blob-only release record. The workflow receives either the published release or an intent waiting for passkey approval.
The publisher task guide is Automated plugin releases. Contributor setup and component boundaries are documented in the release-service README.
The service is a delegated writer, not a registry or trust authority. It cannot edit package profiles, overwrite releases, serve public or long-lived artifacts, moderate listings, or make an invalid release installable. EmDash installers independently verify the publisher's records, artifact, manifest, provenance, and signed package policy.
Canonical service state is sharded across SQLite-backed Durable Objects. A PublisherDurableObject owns each publisher's delegations, workload policies, release intents, reservations, and audit history. An ApproverDurableObject owns each approver's passkeys and challenges. A small ServiceControlDurableObject owns global pause and key-version state. Cloudflare Workflows orchestrate long-running verification and approval waits but do not replace the Durable Objects as the source of truth.
- Publish an immutable sandboxed-plugin release from GitHub Actions without a stored AT Protocol credential in the runner.
- Restrict the service's durable AT Protocol grant to creating records in the active release collection.
- Bind every automated release to its source repository, workflow, commit, artifact, and provenance.
- Require a user-verified passkey decision when signed package policy requires confirmation or declared access expands.
- Preserve publisher ownership of package policy and approver membership in the publisher's PDS.
- Let an EmDash installer reject invalid service output without consulting or trusting the service.
- Provide a hosted service and the same Workers-based application for self-hosting.
- Support operator incident response through a Cloudflare Access-protected admin surface without giving operators publisher authority.
- Native-plugin distribution.
- Public or long-lived artifact or provenance hosting.
- Plugin-listing or metadata moderation.
- Code-quality or malware assessment beyond canonical bundle, manifest, and provenance verification.
- Editing package profiles from the release service.
- Non-GitHub workload issuers in the first release.
- Quorum approval or signed approval receipts verifiable by installers.
- Generic user-defined OIDC claim expressions.
- Billing, organization management, or a general-purpose CI platform.
- Making the aggregator authoritative for publisher records, policy, or provenance.
The service has three external authentication mechanisms. Credentials from one mechanism never authorize another.
| Actor | Authentication | Authority |
|---|---|---|
| GitHub Actions workflow | GitHub Actions OIDC | Request publisher approval for its identity, then submit release intents |
| Atmosphere account holder | AT Protocol OAuth plus an enrolled passkey when needed | Establish or revoke delegation, confirm a GitHub workflow, and approve or reject an exact release |
| Service operator | Cloudflare Access | Observe, pause, suspend, revoke, retry, and recover the service |
Cloudflare Access protects /admin/* and the operator API. The Worker verifies the Cf-Access-Jwt-Assertion signature, team issuer, role-specific audience, time claims, token type, and human identity. Access-injected identity headers and the browser cookie are not sufficient by themselves. Operator mutations retain CSRF and idempotency protection.
Deployments use separate Access policy and audience boundaries for three service roles:
viewerreads health, publisher state, intent state, and sanitized audit data;reviewercancels unpublished intents and triggers bounded reconciliation; andadminchanges service mode, suspends publishers, revokes retained authority, and operates key or recovery controls.
Role names and audience bindings are stable application contracts. Each deployment assigns its operator groups to the appropriate Access policies; Access evaluates membership before forwarding the request. The Worker does not authorize from optional group claims because Access may trim custom claims to fit its cookie limit. No Access role can establish publisher delegation, authorize workload policy, enrol an approver, approve an intent, or publish a release.
One identity-only OAuth flow proves control of an Atmosphere account and issues short-lived publisher and approver application sessions. The separate cookies and Durable Objects preserve the internal authority boundaries without requiring two user logins. Application sessions cannot write to the PDS. The separately authorized release delegation is encrypted and stored in the publisher's Durable Object.
GitHub Actions OIDC tokens are audience-bound to the service. The service persists normalized claims and token identifiers, not the raw token.
The implementation must preserve these invariants:
- CI never receives or stores an AT Protocol refresh token, DPoP key, app password, or delegated release session.
- The delegated grant is the exact scope returned by
getDelegatedReleasePermission()from@emdash-cms/registry-lexicons: create-only release-record authority plus gzip-package and image-blob uploads. - The service never requests or stores package-profile write scope.
- The delegated path exposes no update or delete operation for a release record.
- A package version maps to one deterministic record key and one reservation in the publisher shard.
- Publisher records are read directly from the DID-resolved PDS. Aggregator output is never an authority input.
- Supplied HTTPS artifact and provenance bytes pass the shared SSRF-safe fetcher and their signed checksums. Publication replaces every artifact URL with a checksum-bound PDS blob; the final artifact descriptors contain no source URL.
- Record, bundle, manifest, provenance, workload, policy, and access checks run after submission and again immediately before publication.
- Human approval cannot override failed verification. It authorizes only a valid release that policy requires a human to confirm.
- Approval is bound to the intent, workload claims, profile CID, baseline release CID, artifact and provenance checksums, declared-access diff, approver DID, and decision.
- Passkey approval requires WebAuthn user verification, not only user presence.
- A listed but unenrolled DID cannot approve. Enrolment and approval are separate ceremonies.
- An Access operator can remove or suspend authority but cannot create publisher authority, edit signed policy, enrol an approver, or approve a release.
- Durable Object state and a PDS write are never treated as one transaction. Ambiguous writes are reconciled by reading the deterministic key.
- OAuth sessions, DPoP keys, emails, and future webhook secrets are application-encrypted before persistence.
- Workflows, projections, and notification queues are recoverable coordination or read models, not canonical authorization state.
The active release collection and OAuth scope come from @emdash-cms/registry-lexicons:
const permission = getDelegatedReleasePermission();
// permission.collection identifies the release NSID.
// permission.scope grants only create access to that collection.The service does not hard-code the experimental or future stable NSID. A namespace migration requires a new publisher grant because AT Protocol repository scope is collection-specific.
The first release requests this granular permission directly rather than wrapping it in an AT Protocol permission set. Permission sets are useful when an application needs many permissions under one Lexicon namespace; this service needs one create-only repository permission. Direct scope keeps the effective authority visible in client metadata, stored delegation state, audit output, and conformance evidence. A future permission-set alias is acceptable only if every supported PDS resolves it to the identical collection and create action and the full authorization, refresh, and revocation matrix is repeated.
The initial support matrix contains:
- Bluesky PDS software hosted by npmX (
npmx.social); and - Cirrus.
Disposable accounts are available for both implementations. Credentials and recovery material remain outside the repository. Neither implementation is advertised as supported until its conformance run passes.
The supported-PDS matrix must prove that each claimed implementation:
- accepts a create in the release collection;
- rejects an update to the created release;
- rejects deletion through the delegated path;
- rejects profile writes and writes to unrelated collections;
- supports refresh and explicit revocation as claimed by the service; and
- stops accepting refreshed authority after revocation and client-key removal according to the authorization server's documented behavior.
There is no broad-scope fallback.
The package profile extension supplies the publisher-controlled policy:
interface PackageProfileExtension {
repository: string;
releasePolicy?: {
requireProvenance?: boolean;
confirmation?: "escalation-only" | "always";
approvers?: string[];
};
}The service normalizes omitted values to the protocol defaults. It validates the repository as a canonical HTTPS source URL and every approver as a DID. The release service can display this policy but cannot change it.
emdash-plugin release setup creates a missing package profile or adds the extension to an existing valid profile through the publisher's local CLI session. It asks for confirmation in an interactive terminal and preserves existing package metadata. The service checks the signed extension before creating a workflow connection request and again before accepting that workflow's artifact uploads. A missing profile, missing extension, or repository mismatch returns PACKAGE_PROFILE_REQUIRED with the local setup command.
Setup writes one repository workflow. With Changesets, the workflow is called after the existing Changesets publish job and receives its official published-package JSON. It maps package names to plugin manifests, verifies the reported versions, and emits one matrix entry per matching plugin. Without Changesets, <slug>@<version> tags select a package directly. Manual selection remains available in every generated workflow. Trigger selection does not change the service's workload identity, repository connection, provenance, or package-profile checks.
The delegated path always requires supported provenance, even when requireProvenance is absent. The profile field communicates the publisher's requirement to every installer and non-delegated publisher. A supplied unsupported predicate is present-but-unverifiable and fails delegated publication.
The release extension identifies the provenance document and the source it must describe:
interface ReleaseProvenance {
predicateType: string;
url: string;
checksum: string;
sourceRepository: string;
builderId: string;
}The first release understands SLSA provenance v1 in a Sigstore bundle. The provenance checksum covers the fetched provenance document. Its subject digest covers the package artifact. The source repository and builder identity must agree with the signed package profile, registered workload policy, GitHub OIDC claims, and provenance statement.
The publisher authorizes a service-local workload policy after proving the publisher DID:
interface GitHubWorkloadPolicy {
publisherDid: string;
packageSlug: string;
repository: string;
repositoryId: string;
repositoryOwnerId: string;
workflowRef: string;
allowedRefs?: string[];
allowedEnvironments?: string[];
repositoryConnection: boolean;
active: boolean;
stateVersion: number;
}Repository and owner IDs preserve the authorization boundary across GitHub renames or name reuse. This policy narrows who may submit an intent. It cannot weaken the signed package policy or change publisher records.
Ref restrictions are exact by default. Trigger refs may use a trailing * under refs/heads/ or refs/tags/; workflow repository and file paths remain exact. A reusable repository connection stores the workflow source as refs/*, but the separately checked trigger refs still limit which tags and branches can publish. Confirming another ref scope merges it with the marked connection. Existing unmarked workload policies remain package-scoped.
GitHub Actions Publisher or approver browser
| OIDC | AT Protocol OAuth
| | WebAuthn when approving
v v
+---------------------------------------------------------------------+
| Release-service Worker |
| Public API, publisher UI, approver UI, Access-protected admin |
+--------------+--------------------+--------------------+-------------+
| | |
v v v
PublisherDurableObject ApproverDurableObject ServiceControlDO
publisher DID shard approver DID shard service shard
|
| intent ID
v
ReleaseIntentWorkflow -------> isolated verifier Worker
| |
| v
| artifact/provenance hosts
v
private R2 staging -----> publisher PDS blobs and release record
Operational projections:
OAuth callbacks -> 256 IdentityDirectoryDurableObject shards
DO snapshots and audit exports -> R2
The stateless Worker owns HTTP concerns:
- route and method dispatch;
- request parsing and size limits;
- authentication-realm selection;
- Access JWT, application session, CSRF, OIDC, and idempotency checks;
- deterministic routing to Durable Objects;
- static publisher, approver, and operator assets;
- public health responses that expose no tenant data; and
- stable API error envelopes and request IDs.
Business state transitions live in Durable Object RPC methods, not route handlers.
The Worker routes with getByName(canonicalPublisherDid). The DID is the coordination atom because one AT Protocol session and PDS serialize authority for all packages owned by that publisher.
The object owns:
- publisher status and tenancy;
- encrypted delegated OAuth session and key version;
- workload policies;
- release intents and immutable transition history;
- package/version reservations;
- immutable publication materialization plans, staged-object bindings, blob receipts, and final record JSON;
- OIDC and request idempotency;
- refresh and publication operation tokens;
- publisher-scoped audit events; and
- publisher-session revocation epochs.
SQLite constraints enforce uniqueness for package/version reservations, workload-policy identity, intent idempotency, and transition sequence numbers. RPC methods perform synchronous SQLite transactions for related state changes. The object never holds blockConcurrencyWhile() across an external request.
Slow external work uses a persistent operation-token protocol:
Workflow -> beginPublication(intentId)
PublisherDO -> transition to publishing; return generation-bound token
Workflow -> refresh session and call the PDS
Workflow -> completePublication(token, result)
Only the current token can complete the operation. The object schedules its one alarm for the earliest refresh, publication, expiry, or cleanup deadline. The alarm marks abandoned operations for reconciliation and schedules the next pending deadline.
The Worker routes with getByName(canonicalApproverDid). The object owns:
- identity-proof transactions;
- enrolled WebAuthn credentials;
- credential names, counters, transports, creation, last use, and revocation;
- approval challenges bound to an approval digest;
- challenge expiry and single-use state; and
- approver-scoped audit events.
The object verifies a passkey decision and returns a receipt containing the approver DID, intent ID, digest, decision, credential ID, and verification time. The publisher object stores the receipt against the intent. Cross-object calls are replay-safe but are not treated as one transaction. Final verification checks that the approver DID is still authorized by the current profile.
One low-traffic object owns:
- service mode:
active,admission-paused, orpublication-paused; - active and readable encryption-key versions;
- hosted-service publisher allowlist or suspension defaults;
- operator mutation idempotency; and
- global operational audit events.
Intent admission checks the service mode. Every publication obtains a fresh publication permit immediately before the PDS write. A cached permit cannot authorize publication.
One Workflow instance orchestrates one intent. The instance ID is derived from the immutable intent ID. Step names are deterministic. Each step either reads or commits its result through the publisher object before proceeding.
The Workflow owns:
- durable retry timing;
- isolated verification steps;
- waiting for approval, rejection, cancellation, or expiry;
- OIDC-authorized workflow uploads, guarded external-source staging, PDS blob upload, and durable materialization;
- publication and ambiguous-write reconciliation; and
- non-critical completion fan-out.
The Workflow does not own the authoritative intent state. If Workflow state is lost after its retention period, the publisher object and audit history remain sufficient to determine the release state and start recovery.
The verifier is a separate Worker reached through a service binding. It has no publisher OAuth binding, operator authentication secret, or Durable Object namespace. It:
- fetches artifacts and provenance through the shared guarded fetcher;
- enforces redirect, DNS, protocol, time, and size policy;
- validates checksums and canonical bundle structure;
- reconciles the bundled manifest with the proposed record;
- verifies supported Sigstore/SLSA provenance; and
- returns a bounded, serializable report without artifact bytes or secrets.
The service stages workflow-produced artifacts privately. It promotes verified provenance to immutable service storage before publishing its stable URL.
Cross-publisher inventory uses 256 IdentityDirectoryDurableObject shards selected by a digest of the canonical DID. Successful publisher and approver OAuth callbacks register only the DID, actor kind, and timestamps.
The directory is a non-authoritative projection. It excludes OAuth material, approval details, OIDC claims, emails, WebAuthn credentials, package policy, and release state. All operator reads that affect a decision query the authoritative publisher or approver object, and every mutation routes directly to that object.
The directory can be deleted and rebuilt as identities authenticate again without changing authorization or release outcomes.
The private PUBLICATION_STAGING bucket holds checksum-verified workflow uploads and publication artifacts while the Workflow uploads them to the publisher's PDS. Object keys bind the publisher, stable workload identity, package, version, and artifact slot. The Workflow deletes transient objects after all blob receipts and the canonical final record are committed in the publisher object. Seven-day lifecycle rules remove abandoned workload/ and publication/ objects after interrupted Workflows. Staging objects are never public, authoritative, or referenced by the published record.
The private PROVENANCE_STORE bucket holds provenance that passed initial and final verification. Objects use checksum-addressed, create-only keys. GET /v1/provenance/{checksum} streams only promoted objects with immutable caching; the transient staging bucket has no public read route.
Encrypted publisher snapshots and append-only audit exports may be written to the separate OPERATIONS_ARCHIVE bucket. Snapshot production must be bounded and resumable. A restore never revives expired or revoked authority automatically. If an OAuth session cannot be restored safely, the publisher reauthorizes delegation.
The following schemas describe required data and constraints. Exact SQL belongs with the implementation.
| Table | Required properties |
|---|---|
publisher |
One row; DID, status, creation time, suspension reason code, session epoch |
delegations |
Encrypted session envelope, exact scope, PDS, client key ID, expiry, refresh metadata, encryption-key version, revocation state |
workload_policies |
Package slug, repository, workflow reference, ref/environment restrictions, active state, authorizing publisher identity |
intents |
ULID, package/version, state, Workflow ID, normalized OIDC claims, record inputs, verification summaries, approval digest, result URI/CID, error code |
intent_transitions |
Intent ID, monotonic sequence, from/to state, actor realm and identity, reason code, timestamp |
release_reservations |
Unique package/version, intent ID, reservation state |
idempotency_keys |
Realm, key, request digest, intent/result reference, expiry |
publication_materializations |
Intent-bound source digest, status, canonical blob-only record JSON and digest, timestamps |
publication_materialization_slots |
Artifact slot, source URL digest, checksum, R2 key, media metadata, immutable PDS blob receipt |
operations |
Kind, generation, attempt key, token hash, intent ID, phase, materialization digest, start/deadline, completion state |
audit_events |
Monotonic sequence, event type, actor realm, actor identity, subject, public-safe payload, timestamp |
deadlines |
Kind, subject ID, scheduled time, generation |
workflow_connection_invitations |
Legacy one-time SHA-256 token hash, package slug, expiry, creation time |
workflow_connection_requests |
ULID, mutation key, connection key, package slug, normalized OIDC claims, state, ref scope, expected policy version, expiry and timestamps |
Sensitive values are encrypted individually with associated data binding the publisher DID, table, row identity, and key version. The database never stores an encryption master key.
| Table | Required properties |
|---|---|
approver |
One row; DID, status, session epoch |
credentials |
Credential ID, public key, counter, transports, name, creation, last use, revocation |
identity_transactions |
OAuth state hash, PKCE state, expiry, completion |
approval_challenges |
Challenge hash, intent ID, publisher DID, approval digest, expiry, consumed time |
decisions |
Idempotency key, intent ID, digest, decision, credential ID, verification time |
audit_events |
Monotonic sequence, event type, subject, public-safe payload, timestamp |
deadlines |
Challenge or session cleanup deadline and generation |
| Table | Required properties |
|---|---|
service_state |
Current mode, epoch, reason code, Access operator identity, changed time |
encryption_keys |
Key version, status, activation and retirement metadata; never key material |
publisher_controls |
Publisher DID, allow/suspend state, reason code, operator identity, timestamp |
operator_idempotency |
Mutation key, request digest, result, expiry |
audit_events |
Monotonic sequence, operator identity, action, subject, public-safe payload, timestamp |
The canonical state machine is:
received -> verifying -> verified
| |
| +-> ready -> publishing -> reconciling -> published
|
+-> awaiting_approval -> ready
Non-success terminals:
invalid, rejected, cancelled, expired, failed, conflict
Allowed transitions are explicit. Compare-and-set transition methods take the expected state and generation. Unknown or repeated transitions return the stored result when idempotent and reject when the requested payload conflicts.
failed represents an exhausted service or dependency failure after valid input. invalid represents a permanent input, identity, policy, bundle, or provenance failure. Neither is manually approvable. conflict means the deterministic release key contains a different record.
- The publisher signs in once with the Atmosphere account that owns the plugin.
- The service resolves and verifies the publisher DID and PDS.
- Hosted-service admission policy permits or rejects the publisher.
- The publisher authorizes the exact delegated release scope for create-only release records and gzip-package and image-blob uploads.
- The service verifies the returned grant and stores the encrypted session in the publisher object.
- The permanent release Action runs in the intended GitHub Actions workflow. Its audience-bound OIDC token supplies the immutable repository and owner IDs, workflow file, ref, and environment.
- Before storing a connection request, the service fetches the initiating package profile and requires its canonical repository to match the OIDC repository. Unrelated GitHub principals cannot consume publisher onboarding capacity.
- When no matching repository workflow scope exists, the publisher object stores a short-lived workflow connection request and returns a browser approval URL. The Action writes that URL to the job summary and waits.
- The browser displays the human-readable repository, workflow file, branch or tag, environment, and initiating package. For a tag-triggered release, the publisher chooses the current tag or all package version tags. Confirmation creates or extends the package workload policy and marks it as a reusable repository connection; the OIDC request cannot create authority by itself. The publisher can reject a request without granting authority.
- A later package whose signed profile names the same repository reuses the confirmed repository, workflow, ref, and environment scope. The publisher object derives an unmarked package policy so the existing intent and revocation guards remain package-scoped. Unmarked policies created before this model are never used to derive another package's authority.
- Profile-listed approvers use the same Atmosphere login and enrol passkeys before approving a release.
- A dry-run submission verifies OIDC identity, request shape, service admission, and workload policy without reserving, rate-limiting, starting verification, or publishing a version. It does not fetch or validate the artifact or provenance document.
The publisher can revoke the delegation from the service or directly through the authorization server. The service treats refresh failure after revocation as terminal authority loss, not as a retry loop.
- GitHub Actions builds and bundles the plugin.
- GitHub Actions creates SLSA provenance for the exact bundle.
- The Action requests or verifies its repository workflow connection with an OIDC token whose audience is the release service. The service validates the signed package-to-repository binding before creating a pending request. A matching marked repository workflow scope derives a policy for a new package without another browser decision. An unmatched tag or branch scope requires confirmation and is merged into the connection without removing existing scopes.
- After browser confirmation, the Action uploads the bundle and raw Sigstore file to private staging with fresh OIDC tokens. Each object is checksum-bound to the publisher, workload, package, version, and slot.
- The Action submits the package, version, staged source references, checksums, and idempotency key. Artifact descriptors contain internal HTTPS references and no PDS blobs.
- The Worker verifies request shape, OIDC signature and claims, and routes to the publisher object.
- The publisher object verifies workload-policy admission, reserves package/version, creates the intent, and records normalized claims.
- The object creates the Workflow instance using the intent ID.
- The API returns the intent resource. Publication is always asynchronous even when it completes quickly.
The idempotency identity includes publisher DID, package, version, GitHub repository, workflow, run ID, and run attempt. Repeating the same request returns the existing intent. Reusing the identity with a different request digest returns IDEMPOTENCY_CONFLICT.
The Workflow performs these checks in order:
- Resolve the publisher DID and authoritative PDS.
- Fetch the current package profile and record its CID.
- Validate package identity, profile extension, canonical repository, policy, and approver DIDs.
- Confirm the proposed release key is absent.
- Select the highest-semver existing release as the access baseline, excluding the proposed key. Record its CID, or the empty baseline for the first release.
- Ask the verifier Worker to fetch and validate the artifact.
- Confirm bundle package, version, manifest, and declared access match the proposed release.
- Ask the verifier to fetch and validate provenance.
- Match the artifact digest, source repository, builder, workflow, commit, and relevant GitHub claims.
- Compute the canonical declared-access diff against the baseline.
- Resolve
confirmation: alwaysor escalation-only policy. - Persist a bounded verification report and its input CIDs/checksums.
A missing or invalid supported provenance document is invalid. Approval cannot change that result.
If approval is required, the publisher object stores an approval digest and transitions to awaiting_approval. The Workflow waits for a decision event with a bounded expiry.
An approver:
- signs in with an Atmosphere account, using the same identity flow as the account dashboard;
- opens the release and reviews its plugin, repository, workflow, GitHub account, and permission changes, with protocol evidence available under technical details;
- requests a challenge from the approver object;
- signs the approval digest using an active, user-verified credential; and
- submits
approveorrejectwith an idempotency key.
The service accepts the receipt only if the current profile still lists the approver DID. A rejection is terminal. A cancellation or expiry invalidates outstanding challenges. A new profile CID, access baseline, artifact checksum, provenance checksum, or workload claim set creates a new digest and requires a new approval.
Before publication, the Workflow repeats every authoritative read and verification step. It then:
- reads workflow-uploaded artifacts from private R2 or fetches external URL sources through the guarded verifier path, then writes verified bytes to deterministic publication objects;
- obtains a replay-stable, generation-bound publication operation from the publisher object;
- refreshes the encrypted AT Protocol session if required and uploads each staged package or image as a PDS blob;
- validates every returned blob against the source checksum, byte size, and media type, then persists immutable receipts and canonical blob-only record JSON in the publisher object;
- promotes verified provenance to its immutable store and deletes transient R2 objects after durable materialization;
- obtains and consumes a fresh publication permit, rechecks delegation, and moves the operation to
creating; - creates the deterministic release record without an update path;
- completes the operation with the confirmed URI and CID; or
- enters reconciliation only when the
createRecordresult is ambiguous.
Reconciliation reads the deterministic release key directly from the PDS:
- an exact proof-verified semantic match completes publication with the authoritative CID;
- confirmed absence permits an idempotent create retry;
- a different record at the key transitions to
conflict; and - a transient inability to establish presence or absence remains
reconcilingwith bounded retries and operator visibility.
The service never reports published from an HTTP status alone when the response may be ambiguous.
- CI may cancel its own intent before
publishingusing a fresh matching OIDC identity. - The publisher may cancel an intent before
publishingthrough its application session. - An approver may reject but cannot cancel on behalf of the publisher.
- An Access operator may cancel or quarantine an intent for incident response but cannot transition it to
readyorpublished. - Publisher revocation blocks new intents and refresh, and publication checks revocation immediately before the write.
- Service pause is checked at admission and again before the write.
- A publisher suspension blocks all publisher shards without deleting audit or encrypted state.
Revocation does not remove an already published record. Record removal or yanking follows the registry protocol and publisher authority model outside this service.
The public service API is versioned under /v1.
Health endpoints are outside the versioned API. GET /health is configuration-independent process liveness. GET /ready loads required configuration and checks the service-control shard, but returns no tenant or operational state.
| Method and path | Purpose |
|---|---|
POST /v1/release-intents |
Submit or replay an OIDC-authenticated intent |
POST /v1/release-intents/dry-run |
Check OIDC and admission policy without creating an intent |
POST /v1/workflow-connections |
Request or reuse a repository workflow connection using OIDC |
GET /v1/release-intents/{id} |
Read status using matching workload identity or publisher session |
POST /v1/release-intents/{id}/cancel |
Cancel before publication |
| Method and path | Purpose |
|---|---|
POST /v1/publisher/session/authorize |
Start identity-only publisher authorization |
GET /v1/publisher |
Read publisher and delegation state |
POST /v1/publisher/delegation/authorize |
Start exact-scope delegation authorization |
DELETE /v1/publisher/delegation |
Revoke retained authority |
GET /v1/publisher/workloads |
List package workload policies |
POST /v1/publisher/workloads |
Create or replace an authorized policy |
POST /v1/publisher/workflow-connection-invitations |
Create a legacy package-bound connection invitation |
GET /v1/publisher/workflow-connections |
List pending workflow connection requests |
POST /v1/publisher/workflow-connections/{requestId}/confirm |
Confirm a request and create its workload policy |
DELETE /v1/publisher/workflow-connections/{requestId} |
Reject and clear a pending connection request |
DELETE /v1/publisher/workloads/{packageSlug} |
Disable a policy |
GET /v1/publisher/workloads/{packageSlug}/approvers |
Read approval readiness for DIDs in the signed profile |
GET /v1/publisher/intents |
List publisher intents with cursor pagination |
GET /v1/publisher/audit |
List publisher-scoped audit events with cursor pagination |
POST /v1/workflow-connections is the GitHub OIDC-authenticated endpoint used by the permanent Action and release submit. It checks the signed package profile before recording an unmatched workflow request. A publisher confirmation marks the resulting policy as a repository workflow scope. Later package profiles that name the same repository reuse that scope and receive their own unmarked package policy. Existing unmarked policies stay package-scoped. The optional invitation field and publisher invitation endpoint remain available for workflows generated before repository connections.
| Method and path | Purpose |
|---|---|
POST /v1/approver/session/authorize |
Start identity-only authorization |
GET /v1/approver/credentials |
List active and revoked passkeys |
POST /v1/approver/credentials/options |
Begin enrolment |
POST /v1/approver/credentials |
Finish enrolment |
DELETE /v1/approver/credentials/{id} |
Revoke one credential |
GET /v1/approvals/{intentId} |
Read approval-safe intent details |
POST /v1/approvals/{intentId}/options |
Create a digest-bound challenge |
POST /v1/approvals/{intentId} |
Approve or reject |
The typed operator client uses these canonical paths:
| Method and path | Minimum role | Purpose |
|---|---|---|
GET /admin/api/status |
viewer | Read service mode and component health |
GET /admin/api/directory |
viewer | List publisher or approver directory projections |
GET /admin/api/audit |
viewer | Query global or projected operational audit data |
GET /admin/api/publishers/{publisherDid} |
viewer | Read authoritative publisher state |
POST /admin/api/intents/{intentId}/cancel |
reviewer | Stop an unpublished intent |
POST /admin/api/intents/{intentId}/reconcile |
reviewer | Trigger bounded reconciliation |
POST /admin/api/pause |
admin | Change admission or publication mode |
POST /admin/api/publishers/{publisherDid}/suspend |
admin | Suspend or restore publisher admission |
POST /admin/api/publishers/{publisherDid}/revoke |
admin | Revoke retained service authority |
POST /admin/api/publishers/{publisherDid}/encryption/rotate |
admin | Re-encrypt one page of publisher state |
POST /admin/api/approvers/{approverDid}/encryption/rotate |
admin | Re-encrypt one page of approver state |
POST /admin/api/publishers/{publisherDid}/archive |
admin | Produce one bounded publisher archive page |
POST /admin/api/publishers/{publisherDid}/archive/start |
admin | Start a resumable publisher archive Workflow |
POST /admin/api/publishers/{publisherDid}/restore/prepare |
admin | Validate an archive before restore |
POST /admin/api/publishers/{publisherDid}/restore |
admin | Restore one validated archive page |
GET /admin/api/encryption/keys |
viewer | Read encryption-key lifecycle state |
POST /admin/api/encryption/keys/activate |
admin | Activate a configured encryption-key version |
POST /admin/api/encryption/verify |
admin | Start fleet verification for a retiring key |
POST /admin/api/encryption/keys/{version}/retire |
admin | Retire a verified inactive key |
State-changing requests require content-type validation, CSRF where cookies are used, and idempotency keys. API errors expose stable codes and public-safe messages.
At minimum, the API and Workflow use these error classes:
| Code | Meaning | Retry behavior |
|---|---|---|
AUTH_INVALID |
Authentication or session proof failed | Permanent for request |
ACCESS_DENIED |
Verified actor lacks required role or ownership | Permanent |
PUBLISHER_SUSPENDED |
Hosted service or operator blocked the publisher | Retry after state change |
SERVICE_PAUSED |
Admission or publication is paused | Retry after state change |
DELEGATION_REQUIRED |
No usable exact-scope session exists | Publisher must reauthorize |
WORKLOAD_NOT_ALLOWED |
OIDC claims do not match active policy | Permanent until policy changes |
IDEMPOTENCY_CONFLICT |
Same key was used with a different request | Permanent |
VERSION_RESERVED |
Package/version belongs to another intent | Permanent or return owner intent |
RELEASE_EXISTS |
Proposed deterministic key already exists | Permanent unless exact replay |
PACKAGE_PROFILE_REQUIRED |
Signed profile is missing, incomplete, or linked elsewhere | Run local profile setup |
PROFILE_CHANGED |
Authoritative policy changed after verification or approval | Reverify and possibly reapprove |
BASELINE_CHANGED |
Access baseline changed | Reverify and possibly reapprove |
ARTIFACT_INVALID |
Fetch, checksum, bundle, or manifest failed | Permanent for supplied input |
PROVENANCE_INVALID |
Provenance or workload binding failed | Permanent for supplied input |
APPROVAL_REQUIRED |
Valid release awaits human decision | Not an error state |
APPROVAL_INVALID |
DID, credential, challenge, digest, or UV failed | Permanent for attempt |
DELEGATION_REVOKED |
Session was revoked or cannot refresh | Publisher must reauthorize |
PDS_TRANSIENT |
PDS result is retryable | Workflow retry |
PDS_AMBIGUOUS |
Create outcome is unknown | Reconciliation |
RELEASE_CONFLICT |
Deterministic key contains different data | Terminal conflict |
INTERNAL_ERROR |
Public-safe catch-all | Operator-visible correlation ID |
Provider payloads, tokens, secrets, raw assertions, private evidence, and stack traces never enter public errors or persistent generic error strings.
Envelope encryption uses compact JSON Web Encryption (JWE) through jose. A versioned 256-bit master key from Secrets Store or an equivalent Worker secret binding acts as an A256GCMKW key-encryption key. Each value receives a fresh content-encryption key, and A256GCM encrypts the payload. Master keys are read inside request or RPC scope and are never logged or persisted.
Each compact JWE includes:
- the key-management and content-encryption algorithms;
- the master-key version and initial profile version;
- a wrapped content-encryption key;
- key-wrap and content-encryption nonces and authentication tags;
- ciphertext; and
- a critical SHA-256 context digest.
The protected header is authenticated as JWE associated data. Its context digest binds the deployment, publisher DID, object class, table, row identity, field purpose, and master-key version. Swapping ciphertext between publishers or fields fails authentication.
Routine rotation introduces a new active version, retains old versions for decryption, re-encrypts in bounded publisher-shard batches, verifies completion through audit/export data, and only then retires the old version. Emergency rotation pauses publication first. Missing key material fails closed and requires operator recovery; it never silently discards or recreates delegation.
Every security-relevant state change appends an immutable audit event in the authoritative shard before returning success. Events name the actor realm (oidc, publisher, approver, access, or system), stable actor identity, action, subject, reason code, and timestamp.
Audit payloads may include normalized repository, workflow, package, version, public record identifiers, and digest prefixes. They exclude raw tokens, assertions, OAuth sessions, DPoP keys, email bodies, WebAuthn public-key material, and artifact contents.
Access operator views use the least detailed representation required for operations. Publisher and approver views cannot read another publisher's private intents or credentials.
Publishing and listing moderation remain independent:
- The delegated service creates a valid release record.
- The aggregator observes the record.
- Metadata labellers assess the exact visible profile and release CIDs.
- Default admin discovery requires the configured positive moderation labels.
The delegated service neither issues moderation labels nor bypasses the aggregator's visibility policy. A successfully published release may remain absent from default discovery while moderation is pending.
The Access admin exposes service mode, publisher lookup, stuck operations, reconciliation, authority revocation, suspension, encryption-key status, Workflow status, and sanitized audit events.
Required runbooks cover:
- compromised service encryption key;
- compromised Access operator identity;
- publisher-requested revocation;
- authorization-server outage;
- PDS write ambiguity;
- Workflow loss or prolonged retry;
- verifier egress failure;
- Durable Object schema migration failure;
- lost or corrupt publisher shard;
- passkey compromise or counter anomaly; and
- hosted-service rollback.
Recovery prefers removal of authority. If encrypted session state cannot be proved correct, revoke or disable it and require publisher reauthorization.
- The first deployment uses one complete initial Durable Object schema and one initial encryption profile. Implementation PR boundaries do not create schema versions or legacy ciphertext formats.
- Protocol additions remain optional while the registry is experimental.
- Interactive publisher-owned CLI publication continues to work.
- Existing releases without provenance remain installable unless signed package policy requires provenance.
- The delegated service itself always requires provenance for its releases.
- A stable release-collection namespace requires a fresh delegation grant.
- Old application versions tolerate the optional profile and release extension fields.
- After the first deployment, a rolling service update must tolerate shards at the previous application schema version until their forward migration completes.
The service is complete only when all of the following hold:
- A publisher authorizes the exact delegated release grant on the Bluesky PDS implementation at npmX and on Cirrus.
- The retained session creates a new release and cannot update it, delete it, edit a profile, or write another collection.
- Revocation prevents new publication and eventual refresh; emergency service revocation removes retained authority.
- Cloudflare Access operators cannot reach publisher, approver, or CI-authorized actions.
- A configured GitHub workflow can submit an intent; a wrong repository, workflow, audience, ref, environment, run identity, or expired token cannot.
- Replaying the same request returns the same intent; changing the payload under the same idempotency identity fails.
- Artifact, bundle, manifest, declared access, provenance, source, builder, and commit substitution tests fail closed.
- The service reads authoritative profile and release records directly from the publisher PDS.
- An ordinary non-escalating release publishes automatically under
escalation-only. confirmation: alwaysand declared-access expansion wait for approval.- An unlisted, unenrolled, revoked, cloned, non-user-verified, or digest-mismatched credential cannot approve.
- Profile, baseline, artifact, provenance, or workload changes invalidate prior approval.
- Approval never converts invalid verification into a publishable release.
- Concurrent submissions cannot claim the same publisher/package/version.
- OAuth refresh and PDS publication serialize within a publisher shard without holding a Durable Object-wide external-I/O lock.
- Every package and listing image is uploaded as a checksum-bound PDS blob, and the created release record contains blob references with no artifact source URLs.
- Blob receipts and the canonical final record are committed before a publication permit is issued or
createRecordbegins. - Duplicate Workflow execution cannot create a second semantic release or duplicate a terminal transition.
- A timeout before, during, or after the PDS write converges to published, retryable absence, or conflict through reconciliation.
- Service pause and publisher suspension are enforced immediately before the write.
- A clean EmDash site independently accepts a valid delegated release.
- It rejects absent, malformed, mismatched, unsupported, or invalid required provenance even if the service marked the intent published.
- It rejects a bundle whose manifest differs from the signed release's declared access.
- Listing moderation remains required independently for default admin discovery.
- After the first deployment, Durable Object migrations preserve existing delegation, intent, credential, and audit data.
- Successful materialization removes its R2 staging objects, and the staging bucket lifecycle removes objects abandoned for seven days.
- Encryption-key rotation completes without losing authority or leaving values unreadable.
- Encrypted snapshots and audit exports restore into a fresh deployment without automatically reviving expired or revoked authority.
- Hosted and self-hosted deployments pass the same protocol and service conformance suite.
- External security review has no unresolved critical or high findings before production launch.