diff --git a/.changeset/delegated-release-client.md b/.changeset/delegated-release-client.md index 93788ea9c7..f5f52c4a54 100644 --- a/.changeset/delegated-release-client.md +++ b/.changeset/delegated-release-client.md @@ -3,8 +3,14 @@ "@emdash-cms/plugin-cli": minor --- -Adds typed clients for the experimental delegated release service. `ReleaseServiceClient` submits, polls, and cancels GitHub OpenID Connect release intents, and manages publisher workload policies and retained delegation through a publisher session. `ReleaseServiceOperatorClient` exposes the Cloudflare Access status, sharded publisher and approver inventory, pause, suspension, revocation, cancellation, reconciliation, resumable encryption-key rotation, Workflow-backed encrypted R2 archive, and fail-safe publisher restore and abort operations. +Adds typed clients for the experimental delegated release service. `ReleaseServiceClient` submits, polls, and cancels GitHub OpenID Connect release intents; manages publisher workload policies and retained delegation; and lets publishers check whether profile-listed approvers have an active passkey and inspect publisher-scoped audit events through a publisher session. `ReleaseServiceOperatorClient` exposes the Cloudflare Access status and sanitized audit, sharded publisher and approver inventory, pause, suspension, revocation, cancellation, reconciliation, resumable encryption-key rotation, Workflow-backed fleet verification, audited key retirement, encrypted R2 archive, and fail-safe publisher restore and abort operations. + +`ReleaseServiceClient` can also create, inspect, claim, and confirm short-lived GitHub workflow pairings. A pairing records GitHub's signed repository, workflow, ref, and environment as a proposal. The publisher must confirm those details in the browser before the service creates a workload policy. Both clients validate response envelopes and return stable `ReleaseServiceError` codes with retry metadata. Mutation helpers require idempotency keys, and workload polling requests a fresh token from the configured provider for each call. -The plugin CLI adds `emdash-plugin release submit`, `release status`, and `release cancel` for GitHub Actions jobs. The commands request audience-bound OIDC tokens from the runner, support JSON output, and use the GitHub run identity as the default idempotency key. +The plugin CLI adds `emdash-plugin release connect`, `release dry-run`, `release submit`, `release status`, and `release cancel` for GitHub Actions jobs. `release connect` supplies the workflow's signed GitHub identity to a short-lived pairing created in the account dashboard. Dry-run verifies workload admission without creating an intent, consuming rate budget, or reserving a version. The commands request audience-bound OIDC tokens from the runner, support JSON output, and use the GitHub run identity as the default idempotency key where a mutation occurs. + +Delegated submissions use a URL-source release record: each package or listing-image artifact supplies a checksum-bound HTTPS URL and no blob. The service stages and uploads those bytes through the publisher's delegation, then creates a blob-only release record. Submit and dry-run reject mixed or blob-backed source inputs before requesting GitHub OIDC. + +Interactive `release delegate`, `revoke`, `workload`, `enrol`, `approve`, and `reject` commands print validated browser handoffs. Publisher application sessions, OAuth credentials, and passkey assertions remain at the release-service origin instead of entering the terminal process. diff --git a/.changeset/swift-verifiers-match.md b/.changeset/swift-verifiers-match.md index 4c9fb0a459..5c33420e08 100644 --- a/.changeset/swift-verifiers-match.md +++ b/.changeset/swift-verifiers-match.md @@ -7,3 +7,7 @@ Adds optional artifact digest candidates to `GitHubProvenanceVerifier`, allowing Existing callers can continue passing only `artifactDigest`. Successful results return the candidate that matched the signed subject. Fixes `@emdash-cms/registry-verification` when it is rebundled into an Astro Cloudflare application, preventing requests from failing during Worker startup. + +Adds `@emdash-cms/registry-verification/records` for Worker callers that supply an explicit `ProvenanceVerifier`. The runtime-neutral entry does not load the Node-oriented default Sigstore verifier, while the package root keeps the existing default-verifier behavior. + +Fixes `@emdash-cms/registry-verification` when it is rebundled into an Astro Cloudflare application, preventing requests from failing during Worker startup. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d822b9335e..b0aa7e537b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,13 @@ jobs: - run: pnpm run --filter emdash-demo --filter @emdash-cms/demo-cloudflare typecheck - run: pnpm typecheck:templates - run: node scripts/typecheck-public-source.mjs + - run: pnpm run --filter @emdash-cms/release-service --filter @emdash-cms/release-verifier --filter @emdash-cms/release-action typecheck + - run: pnpm run --filter @emdash-cms/release-service --filter @emdash-cms/release-verifier --filter @emdash-cms/release-action build + - run: git diff --exit-code -- apps/release-action/dist/index.js + - run: pnpm --dir apps/release-service exec wrangler types --check + - run: pnpm --dir apps/release-verifier exec wrangler types --check + - run: pnpm --dir apps/release-service exec wrangler deploy --dry-run + - run: pnpm --dir apps/release-verifier exec wrangler deploy --dry-run lint: name: Lint @@ -129,6 +136,7 @@ jobs: # Render tests use the Astro Vite plugin (vitest.repro.config.ts); # they can't run under the plain-node config in test:unit. - run: pnpm --filter emdash exec vitest run --config vitest.repro.config.ts + - run: pnpm run --filter @emdash-cms/release-service --filter @emdash-cms/release-verifier --filter @emdash-cms/release-action test test-smoke: name: Smoke Tests @@ -191,7 +199,7 @@ jobs: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - - run: pnpm run --filter @emdash-cms/admin... build + - run: pnpm run --filter @emdash-cms/admin... --filter @emdash-cms/release-service... build - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 id: playwright-cache with: @@ -199,6 +207,7 @@ jobs: key: playwright-${{ hashFiles('pnpm-lock.yaml') }} - run: pnpm exec playwright install --with-deps chromium if: steps.playwright-cache.outputs.cache-hit != 'true' + - run: pnpm --filter @emdash-cms/release-service test:browser - run: pnpm run --filter @emdash-cms/admin test test-e2e-rollup: diff --git a/apps/release-action/README.md b/apps/release-action/README.md index 8fe9ddedca..fc6aa8747b 100644 --- a/apps/release-action/README.md +++ b/apps/release-action/README.md @@ -1,6 +1,6 @@ # EmDash delegated release Action -This experimental Action submits a package release record to an EmDash delegated release service. It requests a GitHub OpenID Connect (OIDC) token for each service call, so the workflow does not store a release-service secret. +This experimental Action submits a URL-source package release record to an EmDash delegated release service. It requests a GitHub OpenID Connect (OIDC) token for each service call, so the workflow does not store a release-service secret. ## Workflow setup @@ -36,19 +36,19 @@ jobs: Replace the example service URL, publisher DID, build command, and exact commit with values for your publisher. Pin the Action to an exact commit while the delegated release protocol remains experimental. -The release record must conform to `com.emdashcms.experimental.package.release`. The service validates its package, version, artifact, declared access, and provenance before publication. +The source release record must conform to `com.emdashcms.experimental.package.release`. Every package or listing-image artifact must have a checksum-bound HTTPS `url` and must not contain `blob`. The service validates and uploads those bytes to the publisher's PDS, then creates a release record whose artifact descriptors contain blobs and no source URLs. Provenance remains checksum-bound to its HTTPS source. ## Inputs -| Input | Required | Default | Purpose | -| ----------------------- | -------- | -------------- | ---------------------------------------------------------------------------------------------- | -| `service-url` | Yes | — | HTTPS origin of the delegated release service. | -| `publisher-did` | Yes | — | DID that owns the package profile and release records. | -| `release-file` | Yes | — | JSON file containing the package release record. The path must stay inside `GITHUB_WORKSPACE`. | -| `idempotency-key` | No | Current run ID | Stable key used to replay the same submission. | -| `poll-interval-seconds` | No | `5` | Delay between intent status requests. | -| `timeout-minutes` | No | `30` | Maximum polling time. | -| `wait-for-approval` | No | `false` | Continue polling when the intent reaches `awaiting_approval`. | +| Input | Required | Default | Purpose | +| ----------------------- | -------- | -------------- | --------------------------------------------------------------------------------------------------------- | +| `service-url` | Yes | — | HTTPS origin of the delegated release service. | +| `publisher-did` | Yes | — | DID that owns the package profile and release records. | +| `release-file` | Yes | — | JSON file containing the URL-source package release record. The path must stay inside `GITHUB_WORKSPACE`. | +| `idempotency-key` | No | Current run ID | Stable key used to replay the same submission. | +| `poll-interval-seconds` | No | `5` | Delay between intent status requests. | +| `timeout-minutes` | No | `30` | Maximum polling time. | +| `wait-for-approval` | No | `false` | Continue polling when the intent reaches `awaiting_approval`. | The default idempotency key is stable across attempts of one GitHub run. Set `idempotency-key` when separate runs or jobs must replay the same submission identity. diff --git a/apps/release-action/action.yml b/apps/release-action/action.yml index 1b01697138..9bd1de0c35 100644 --- a/apps/release-action/action.yml +++ b/apps/release-action/action.yml @@ -9,7 +9,7 @@ inputs: description: Publisher DID that owns the package. required: true release-file: - description: Path to the package release record JSON file. + description: Path to a URL-source package release JSON file. The service publishes PDS blobs. required: true idempotency-key: description: Stable key for replaying this submission. Defaults to the GitHub run. diff --git a/apps/release-action/dist/index.js b/apps/release-action/dist/index.js index e1b27ca4aa..17fa0b00ae 100644 --- a/apps/release-action/dist/index.js +++ b/apps/release-action/dist/index.js @@ -149,6 +149,34 @@ const isResourceUri = /* @__NO_SIDE_EFFECTS__ */ (input) => { return /* @__PURE__ */ isActorIdentifier(input.substring(5, repoEnd)) && (collection === void 0 || /* @__PURE__ */ isNsid(collection)) && (rkey === void 0 || /* @__PURE__ */ isRecordKey(rkey)); }; +//#endregion +//#region ../../node_modules/.pnpm/@atcute+lexicons@2.0.0/node_modules/@atcute/lexicons/dist/syntax/cid.js +const DASL_CID_RE = /^baf[ky]rei[a-z2-7]{52}$/; +const isCid = /* @__NO_SIDE_EFFECTS__ */ (input) => { + return typeof input === "string" && input.length === 59 && DASL_CID_RE.test(input); +}; + +//#endregion +//#region ../../node_modules/.pnpm/@atcute+lexicons@2.0.0/node_modules/@atcute/lexicons/dist/syntax/datetime.js +const DATE_TIME_RE = /^((?!0{3})\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]))T((?:[01]\d|2[0-3]):(?:[0-5]\d):(?:[0-5]\d))(\.\d+)?(Z|(?!-00:00)[+-](?:[01]\d|2[0-3]):(?:[0-5]\d))$/; +const isDatetime = /* @__NO_SIDE_EFFECTS__ */ (input) => { + return typeof input === "string" && input.length >= 20 && input.length <= 64 && DATE_TIME_RE.test(input); +}; + +//#endregion +//#region ../../node_modules/.pnpm/@atcute+lexicons@2.0.0/node_modules/@atcute/lexicons/dist/syntax/language.js +const LANGUAGE_CODE_RE = /^((?(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?([A-Za-z]{2,3}(-(?[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-(?