@kici-dev/engine 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/audit/access-log-policy.js +1 -0
  2. package/dist/audit/retention-policy.js +2 -0
  3. package/dist/billing/subscription-status.d.ts +38 -0
  4. package/dist/billing/subscription-status.js +63 -0
  5. package/dist/context/host-match.d.ts +25 -2
  6. package/dist/context/host-match.js +29 -7
  7. package/dist/index.d.ts +6 -1
  8. package/dist/index.js +10 -5
  9. package/dist/labels/compile.d.ts +3 -1
  10. package/dist/labels/compile.js +10 -5
  11. package/dist/labels-canonical.d.ts +36 -0
  12. package/dist/labels-canonical.js +21 -0
  13. package/dist/labels-match.d.ts +38 -7
  14. package/dist/labels-match.js +45 -9
  15. package/dist/metrics/catalog-policy.js +6 -1
  16. package/dist/metrics/metric-catalog.generated.d.ts +97 -2
  17. package/dist/metrics/metric-catalog.generated.js +116 -2
  18. package/dist/protocol/dashboard-write-operations.d.ts +17 -0
  19. package/dist/protocol/dashboard-write-operations.js +20 -3
  20. package/dist/protocol/messages/access-log.d.ts +5 -0
  21. package/dist/protocol/messages/access-log.js +1 -0
  22. package/dist/protocol/messages/config-paths.d.ts +20 -0
  23. package/dist/protocol/messages/config-paths.js +27 -0
  24. package/dist/protocol/messages/dashboard.d.ts +134 -0
  25. package/dist/protocol/messages/dashboard.js +68 -1
  26. package/dist/protocol/messages/event-log.d.ts +6 -0
  27. package/dist/protocol/messages/event-log.js +6 -0
  28. package/dist/protocol/messages/orchestrator-agent.d.ts +32 -0
  29. package/dist/protocol/messages/orchestrator-agent.js +52 -5
  30. package/dist/protocol/messages/peer.d.ts +3 -0
  31. package/dist/protocol/messages/peer.js +6 -1
  32. package/dist/protocol/messages/platform-orchestrator.d.ts +36 -0
  33. package/dist/protocol/messages/source-registration.d.ts +5 -0
  34. package/dist/protocol/messages/source-registration.js +8 -0
  35. package/dist/provenance/id-token-event-claims.d.ts +142 -0
  36. package/dist/provenance/id-token-event-claims.js +113 -0
  37. package/dist/provenance/statement-hash.d.ts +14 -5
  38. package/dist/provenance/statement-hash.js +14 -5
  39. package/dist/provenance/verify.d.ts +21 -0
  40. package/dist/provenance/verify.js +26 -16
  41. package/dist/regex-flags.d.ts +32 -0
  42. package/dist/regex-flags.js +43 -0
  43. package/dist/trigger/compiled-matchers.d.ts +9 -2
  44. package/dist/trigger/compiled-matchers.js +14 -5
  45. package/dist/trigger/decision-trace.d.ts +1 -1
  46. package/dist/trigger/decision-trace.js +1 -1
  47. package/dist/trigger/text-match.d.ts +8 -3
  48. package/dist/trigger/text-match.js +11 -5
  49. package/dist/trigger/trigger-event-type.d.ts +16 -0
  50. package/dist/trigger/trigger-event-type.js +25 -1
  51. package/dist/trigger/types.d.ts +55 -4
  52. package/dist/trigger/types.js +5 -1
  53. package/dist/ws/close-codes.d.ts +8 -0
  54. package/dist/ws/close-codes.js +9 -1
  55. package/package.json +1 -1
  56. package/sbom.spdx.json +5 -5
@@ -1,4 +1,5 @@
1
1
  import "../../rolldown-runtime-ClRpJifh.js";
2
+ import { provenanceContextSchema } from "../../provenance/id-token-event-claims.js";
2
3
  import { ExecutionJobStatus, ExecutionStepStatus, StepConcurrencyKind } from "./execution-status.js";
3
4
  import { dsseEnvelopeSchema } from "../../provenance/dsse.js";
4
5
  import { approvalTimeoutSecondsSchema, approverClauseSchema } from "../../approval/types.js";
@@ -103,8 +104,19 @@ const jobDispatchSchema = z.object({
103
104
  concurrencyWaitTimeoutMs: z.coerce.number().optional(),
104
105
  /** URL or file:// path to a pre-packed `.kici/` source tarball. If present, agent extracts it into workDir instead of cloning the repo. */
105
106
  sourceTarUrl: z.string().optional(),
106
- /** SHA-256 hash of the source tarball bytes for integrity verification on download. */
107
+ /**
108
+ * @deprecated Use `sourceTarDigest`. Despite its name this carries the
109
+ * workflow `contentHash`, not a hash of the tarball bytes, so an agent
110
+ * could not verify a restored tarball against it. Kept on the wire for
111
+ * older agents; removed at v1.0.0.
112
+ */
107
113
  sourceTarHash: z.string().optional(),
114
+ /**
115
+ * SHA-256 of the source tarball's own bytes, for integrity verification
116
+ * before extraction. The sibling of `depsHash`, which has always carried
117
+ * the dependency tarball's real digest.
118
+ */
119
+ sourceTarDigest: z.string().optional(),
108
120
  /** URL or file:// path to pre-built dependency tarball. If present, agent extracts to .kici/node_modules/ instead of running install. */
109
121
  depsUrl: z.string().optional(),
110
122
  /** SHA-256 hash of the dependency tarball for integrity verification. */
@@ -113,6 +125,22 @@ const jobDispatchSchema = z.object({
113
125
  requestId: z.string().optional(),
114
126
  /** Base64-encoded X25519 public key for the workflow run (for encrypting secret outputs). */
115
127
  runPublicKey: z.string().optional(),
128
+ /**
129
+ * The orchestrator's own view of the build, for a provenance statement the
130
+ * agent has to freeze before its identity token exists (the deferred path).
131
+ *
132
+ * Every field is what `buildIdTokenClaims` derives from the run row, so a
133
+ * frozen statement built from this is field-for-field what a live mint
134
+ * would have produced — and the server can therefore cross-check it. The
135
+ * agent's local guess is NOT: the job's checkout `ref` is a pull request's
136
+ * HEAD branch where the claim is the BASE branch, and `workflowRef` here is
137
+ * the `<name>@<sha>` claim rather than a global workflow's clone ref.
138
+ *
139
+ * Additive and optional: an older orchestrator omits it and the agent falls
140
+ * back to its local guess. That fallback statement fails the capture
141
+ * cross-check, so the defer is dropped rather than stored unchecked.
142
+ */
143
+ provenanceContext: provenanceContextSchema.optional(),
116
144
  /** Plain outputs from upstream jobs (keyed by job name, then by step name). Populated for downstream jobs with `needs` dependencies. */
117
145
  upstreamJobOutputs: z.record(z.string(), z.record(z.string(), z.unknown())).optional(),
118
146
  /** Terminal status of each upstream job (keyed by job name; per-child for fan-out). Powers `ctx.needs.<job>.status`. */
@@ -545,7 +573,18 @@ const cacheUploadRequestSchema = z.object({
545
573
  * and hashed it by the time it asks. Optional so an older agent that omits it
546
574
  * still gets a usable (lockfile-keyed) URL during a mixed-version rollout.
547
575
  */
548
- depsHash: z.string().optional()
576
+ depsHash: z.string().optional(),
577
+ /**
578
+ * SHA-256 of the source tarball about to be uploaded. Source uploads only.
579
+ *
580
+ * The source tarball is stored under its own content hash, so the
581
+ * orchestrator needs it to sign the upload URL — the agent has already packed
582
+ * and hashed it by the time it asks. Optional so an older agent that omits it
583
+ * still gets a usable URL during a mixed-version rollout.
584
+ */
585
+ sourceTarDigest: z.string().optional(),
586
+ /** In-repo `workspace:` sibling closure digest; part of the dep pointer key. */
587
+ siblingsDigest: z.string().optional()
549
588
  });
550
589
  /** Orchestrator -> Agent: return the pre-signed upload URL. */
551
590
  const cacheUploadResponseSchema = z.object({
@@ -564,7 +603,11 @@ const cacheUploadCompleteSchema = z.object({
564
603
  platform: z.string(),
565
604
  arch: z.string(),
566
605
  /** SHA-256 hash of the dependency tarball for integrity verification. Only present for deps uploads. */
567
- depsHash: z.string().optional()
606
+ depsHash: z.string().optional(),
607
+ /** SHA-256 of the source tarball's own bytes. Only present for source uploads. */
608
+ sourceTarDigest: z.string().optional(),
609
+ /** In-repo `workspace:` sibling closure digest; part of the dep pointer key. */
610
+ siblingsDigest: z.string().optional()
568
611
  });
569
612
  /** Agent -> Orchestrator: request a user-cache restore (presigned download). */
570
613
  const cacheUserRestoreRequestSchema = z.object({
@@ -659,8 +702,12 @@ const provenanceUploadDeferSchema = z.object({
659
702
  subjectName: z.string(),
660
703
  /** Primary subject digest (lowercase hex). */
661
704
  subjectDigest: z.string(),
662
- /** Requested token audience for the later mint. */
663
- audience: z.string(),
705
+ /**
706
+ * Requested token audience for the later mint. Bounded to match the LIVE
707
+ * mint's `oidcTokenRequestParamsSchema` — the two are the same
708
+ * agent-supplied value and had no reason to differ.
709
+ */
710
+ audience: z.string().min(1).max(255),
664
711
  /** Bundle media type. */
665
712
  mediaType: z.string(),
666
713
  /** SHA-256 of the frozen DSSE statement payload — the later-mint binding. */
@@ -173,6 +173,7 @@ export declare const jobRerouteSchema: z.ZodObject<{
173
173
  providerContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
174
174
  sourceTarUrl: z.ZodOptional<z.ZodString>;
175
175
  sourceTarHash: z.ZodOptional<z.ZodString>;
176
+ sourceTarDigest: z.ZodOptional<z.ZodString>;
176
177
  depsUrl: z.ZodOptional<z.ZodString>;
177
178
  depsHash: z.ZodOptional<z.ZodString>;
178
179
  cloneToken: z.ZodOptional<z.ZodString>;
@@ -598,6 +599,7 @@ export declare const peerToPeerMessageSchema: z.ZodDiscriminatedUnion<[z.ZodObje
598
599
  providerContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
599
600
  sourceTarUrl: z.ZodOptional<z.ZodString>;
600
601
  sourceTarHash: z.ZodOptional<z.ZodString>;
602
+ sourceTarDigest: z.ZodOptional<z.ZodString>;
601
603
  depsUrl: z.ZodOptional<z.ZodString>;
602
604
  depsHash: z.ZodOptional<z.ZodString>;
603
605
  cloneToken: z.ZodOptional<z.ZodString>;
@@ -914,6 +916,7 @@ export declare const peerFromPeerMessageSchema: z.ZodDiscriminatedUnion<[z.ZodOb
914
916
  providerContext: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
915
917
  sourceTarUrl: z.ZodOptional<z.ZodString>;
916
918
  sourceTarHash: z.ZodOptional<z.ZodString>;
919
+ sourceTarDigest: z.ZodOptional<z.ZodString>;
917
920
  depsUrl: z.ZodOptional<z.ZodString>;
918
921
  depsHash: z.ZodOptional<z.ZodString>;
919
922
  cloneToken: z.ZodOptional<z.ZodString>;
@@ -210,8 +210,13 @@ const jobRerouteSchema = z.object({
210
210
  providerContext: z.record(z.string(), z.unknown()).optional(),
211
211
  /** Pre-signed source tarball download URL (cache hit). */
212
212
  sourceTarUrl: z.string().optional(),
213
- /** SHA-256 hash of the source tarball bytes for integrity verification. */
213
+ /**
214
+ * @deprecated Use `sourceTarDigest` — this carries the workflow
215
+ * `contentHash`, not a hash of the tarball bytes.
216
+ */
214
217
  sourceTarHash: z.string().optional(),
218
+ /** SHA-256 of the source tarball's own bytes, for integrity verification. */
219
+ sourceTarDigest: z.string().optional(),
215
220
  /** Pre-signed dependency tarball download URL (cache hit). */
216
221
  depsUrl: z.string().optional(),
217
222
  /** Dependency tarball hash for cache keying. */
@@ -1110,6 +1110,35 @@ export declare const platformToOrchestratorMessageSchema: z.ZodDiscriminatedUnio
1110
1110
  }, z.core.$strip>], "type">;
1111
1111
  limit: z.ZodOptional<z.ZodNumber>;
1112
1112
  cursor: z.ZodOptional<z.ZodString>;
1113
+ }, z.core.$strip>, z.ZodObject<{
1114
+ type: z.ZodLiteral<"dashboard.admin-tokens.list">;
1115
+ requestId: z.ZodString;
1116
+ actor: z.ZodDiscriminatedUnion<[z.ZodObject<{
1117
+ type: z.ZodLiteral<"user">;
1118
+ sub: z.ZodString;
1119
+ agent: z.ZodOptional<z.ZodObject<{
1120
+ patId: z.ZodString;
1121
+ label: z.ZodString;
1122
+ }, z.core.$strip>>;
1123
+ }, z.core.$strip>, z.ZodObject<{
1124
+ type: z.ZodLiteral<"api_key">;
1125
+ keyId: z.ZodString;
1126
+ ownerSub: z.ZodString;
1127
+ agent: z.ZodOptional<z.ZodObject<{
1128
+ label: z.ZodString;
1129
+ }, z.core.$strip>>;
1130
+ }, z.core.$strip>, z.ZodObject<{
1131
+ type: z.ZodLiteral<"service_account">;
1132
+ id: z.ZodString;
1133
+ }, z.core.$strip>, z.ZodObject<{
1134
+ type: z.ZodLiteral<"platform_operator">;
1135
+ sub: z.ZodString;
1136
+ reason: z.ZodString;
1137
+ sessionId: z.ZodOptional<z.ZodString>;
1138
+ }, z.core.$strip>, z.ZodObject<{
1139
+ type: z.ZodLiteral<"system">;
1140
+ component: z.ZodString;
1141
+ }, z.core.$strip>], "type">;
1113
1142
  }, z.core.$strip>, z.ZodObject<{
1114
1143
  type: z.ZodLiteral<"run.rerun.request">;
1115
1144
  requestId: z.ZodString;
@@ -2616,6 +2645,7 @@ export declare const platformToOrchestratorMessageSchema: z.ZodDiscriminatedUnio
2616
2645
  lockfile_missing: "lockfile_missing";
2617
2646
  processed: "processed";
2618
2647
  received: "received";
2648
+ shed: "shed";
2619
2649
  }>>;
2620
2650
  fromTimestamp: z.ZodOptional<z.ZodString>;
2621
2651
  toTimestamp: z.ZodOptional<z.ZodString>;
@@ -2952,6 +2982,7 @@ export declare const platformToOrchestratorMessageSchema: z.ZodDiscriminatedUnio
2952
2982
  actorId: z.ZodOptional<z.ZodString>;
2953
2983
  action: z.ZodOptional<z.ZodEnum<{
2954
2984
  "access_log.list.read": "access_log.list.read";
2985
+ "admin_tokens.list.read": "admin_tokens.list.read";
2955
2986
  archive_chunk: "archive_chunk";
2956
2987
  "artifacts.read": "artifacts.read";
2957
2988
  "attestation.retry": "attestation.retry";
@@ -3628,6 +3659,11 @@ export declare const orchestratorToPlatformMessageSchema: z.ZodDiscriminatedUnio
3628
3659
  adminInvocation: z.ZodOptional<z.ZodString>;
3629
3660
  adminPath: z.ZodOptional<z.ZodString>;
3630
3661
  }, z.core.$strip>>;
3662
+ configPaths: z.ZodOptional<z.ZodObject<{
3663
+ envFile: z.ZodOptional<z.ZodString>;
3664
+ scalerConfig: z.ZodOptional<z.ZodString>;
3665
+ composeFile: z.ZodOptional<z.ZodString>;
3666
+ }, z.core.$strip>>;
3631
3667
  s3LogAccess: z.ZodOptional<z.ZodBoolean>;
3632
3668
  queueTimeoutMs: z.ZodOptional<z.ZodNumber>;
3633
3669
  }, z.core.$strip>, z.ZodObject<{
@@ -126,6 +126,11 @@ export declare const sourceRegistrationSchema: z.ZodObject<{
126
126
  adminInvocation: z.ZodOptional<z.ZodString>;
127
127
  adminPath: z.ZodOptional<z.ZodString>;
128
128
  }, z.core.$strip>>;
129
+ configPaths: z.ZodOptional<z.ZodObject<{
130
+ envFile: z.ZodOptional<z.ZodString>;
131
+ scalerConfig: z.ZodOptional<z.ZodString>;
132
+ composeFile: z.ZodOptional<z.ZodString>;
133
+ }, z.core.$strip>>;
129
134
  s3LogAccess: z.ZodOptional<z.ZodBoolean>;
130
135
  queueTimeoutMs: z.ZodOptional<z.ZodNumber>;
131
136
  }, z.core.$strip>;
@@ -1,5 +1,6 @@
1
1
  import "../../rolldown-runtime-ClRpJifh.js";
2
2
  import { DeploymentIdentitySchema } from "./deployment-identity.js";
3
+ import { ConfigPathsSchema } from "./config-paths.js";
3
4
  import { z } from "zod";
4
5
  //#region src/protocol/messages/source-registration.ts
5
6
  /**
@@ -150,6 +151,13 @@ const sourceRegistrationSchema = z.object({
150
151
  * publish it, in which case the dashboard treats the shape as `unknown`.
151
152
  */
152
153
  deployment: DeploymentIdentitySchema.optional(),
154
+ /**
155
+ * Where this orchestrator's own config files live on its host, so the
156
+ * dashboard can point an operator straight at them. Optional: an
157
+ * orchestrator that predates the field omits it, and each member is omitted
158
+ * independently when that path is not knowable.
159
+ */
160
+ configPaths: ConfigPathsSchema.optional(),
153
161
  /** Whether this orchestrator has S3 log storage configured. Used for multi-orch pool validation. */
154
162
  s3LogAccess: z.boolean().optional(),
155
163
  /** Queue timeout in ms. Platform uses this (with margin) for safety-net GC of stale queued jobs. */
@@ -0,0 +1,142 @@
1
+ /**
2
+ * The event-context half of an OIDC ID token's claims, shared by every minter.
3
+ *
4
+ * A cloud trust policy pins `sub` and, ideally, the claims below. Those values
5
+ * are what tells a fork pull request from a trusted push, so the two minters —
6
+ * the customer's orchestrator and the hosted Platform — must derive them
7
+ * identically. Any drift between them is a hole: a bundle minted by one would
8
+ * satisfy a policy written against the other. So the derivation lives here,
9
+ * once, and each minter spreads the result into its own claim object.
10
+ */
11
+ import { z } from 'zod';
12
+ /** The run columns the event claims read. Every one may be absent. */
13
+ export interface EventClaimSource {
14
+ repo_identifier: string | null;
15
+ ref: string | null;
16
+ workflow_name: string | null;
17
+ /** Normalized event that started the run (`push`, `pull_request:opened`, …). */
18
+ trigger_event?: string | null;
19
+ /**
20
+ * The event the SUBJECT is derived from, when that is not `trigger_event`.
21
+ *
22
+ * A re-run records `trigger_event: 'rerun'`, which carries no pull-request
23
+ * dimension, so a re-run of a pull request would present the branch-shaped
24
+ * subject a push to the same base branch presents. This column carries the
25
+ * original run's event forward for that one purpose. NULL means "use
26
+ * `trigger_event`", which is what every row written before the column
27
+ * existed says — so a legacy row keeps the subject it already mints.
28
+ *
29
+ * Read ONLY by `buildIdTokenSubject`. It is deliberately absent from
30
+ * `buildEventClaims`: `event_name` reports what actually started THIS run,
31
+ * and a re-run is a re-run.
32
+ */
33
+ subject_trigger_event?: string | null;
34
+ /** Pull-request HEAD branch. */
35
+ head_ref?: string | null;
36
+ /** `owner/repo` of the pull-request HEAD. */
37
+ head_repository?: string | null;
38
+ /** True for a fork pull request. NULL means the run did not resolve one. */
39
+ is_fork?: boolean | null;
40
+ /** Resolved trust tier for the triggering actor. */
41
+ trust_tier?: string | null;
42
+ /** Provider login of the triggering actor. */
43
+ trigger_actor_username?: string | null;
44
+ }
45
+ /**
46
+ * The event-context claims. Every value is a STRING and every one is ALWAYS
47
+ * PRESENT.
48
+ *
49
+ * Strings because an AWS IAM `StringEquals` condition on an OIDC claim is
50
+ * string-typed and a three-valued `is_fork` has to be expressible. Always
51
+ * present because an ABSENT claim makes a `StringEquals` condition fail — which
52
+ * silently removes the constraint for a policy author who wrote it expecting
53
+ * enforcement. An explicit `''` / `'unresolved'` makes that policy fail closed
54
+ * instead, and makes the unknown state visible in a decoded token.
55
+ */
56
+ export interface EventClaims {
57
+ /** Normalized event type, or `'unknown'`. */
58
+ event_name: string;
59
+ /** The BASE branch — the same value as `ref`, named the way GitHub names it. */
60
+ base_ref: string;
61
+ /** The pull-request HEAD branch, or `''`. */
62
+ head_ref: string;
63
+ /** `owner/repo` of the pull-request HEAD, or `''`. */
64
+ head_repository: string;
65
+ /** `'true'` / `'false'`, or `'unresolved'` when the run resolved no answer. */
66
+ is_fork: string;
67
+ /** The run's trust tier, or `'unresolved'`. */
68
+ trust_tier: string;
69
+ /** Provider login of the triggering actor, or `''`. */
70
+ actor: string;
71
+ }
72
+ /** The sentinel for a value the run did not resolve. Never a plausible default. */
73
+ export declare const UNRESOLVED_CLAIM = "unresolved";
74
+ /**
75
+ * Build the event-context claims from a run row.
76
+ *
77
+ * A NULL never becomes a plausible default. `head_repository` does not fall
78
+ * back to `repository`, and `is_fork` does not fall back to `'false'`: both
79
+ * would fail OPEN on a lost write, which is the failure these claims exist to
80
+ * remove.
81
+ */
82
+ export declare function buildEventClaims(run: EventClaimSource): EventClaims;
83
+ /**
84
+ * Build the token subject.
85
+ *
86
+ * Two shapes, mirroring GitHub Actions' own claim vocabulary — which is what
87
+ * every customer's existing trust policy is written against:
88
+ *
89
+ * - pull-request family: `repo:<owner/repo>:pull_request`, with NO ref
90
+ * segment. GitHub omits the ref there for exactly this reason.
91
+ * - everything else: `repo:<owner/repo>:ref:<ref>:workflow:<name>`, unchanged.
92
+ *
93
+ * A pull request's `ref` is its BASE branch, so the branch-shaped subject made
94
+ * a fork PR against `main` byte-identical to a trusted push to `main`. An
95
+ * external contributor whose PR ran the same workflow minted the exact string a
96
+ * policy pinned, and assumed the customer's cloud role. The two shapes cannot
97
+ * collide: no branch name produces the literal segment `pull_request` in the
98
+ * position a branch subject puts `ref`.
99
+ *
100
+ * The event tested is `subject_trigger_event ?? trigger_event`. A re-run writes
101
+ * `trigger_event: 'rerun'` and carries the original run's event in
102
+ * `subject_trigger_event`, so re-running a pull request keeps the
103
+ * pull-request subject instead of decaying to the colliding branch shape.
104
+ * `trigger_event` itself is left alone because two other readers depend on it:
105
+ * the dashboard's trigger-type filter and the git credential relay's
106
+ * `triggerTypeFilters`, both of which must keep seeing `'rerun'`.
107
+ *
108
+ * `legacyPullRequestSubject` restores the old, colliding form for one release —
109
+ * see `KICI_OIDC_LEGACY_PR_SUB`. It is deprecated on arrival.
110
+ *
111
+ * @deprecated `legacyPullRequestSubject` is removed at v1.0.0.
112
+ */
113
+ export declare function buildIdTokenSubject(run: Pick<EventClaimSource, 'repo_identifier' | 'ref' | 'workflow_name' | 'trigger_event' | 'subject_trigger_event'>, opts?: {
114
+ legacyPullRequestSubject?: boolean;
115
+ }): string;
116
+ /**
117
+ * The orchestrator's own view of a build, sent with the job so the agent can
118
+ * freeze a provenance statement that matches what a live mint would say.
119
+ *
120
+ * Every field mirrors a claim `buildIdTokenClaims` derives, so a statement
121
+ * built from this passes `crossCheckBuildContext` against the token that is
122
+ * later minted for the same (run, job) — which is what lets the orchestrator
123
+ * refuse a deferred statement it has not checked against its own run row.
124
+ *
125
+ * `.passthrough()` so an older agent tolerates a field a newer orchestrator
126
+ * adds.
127
+ */
128
+ export declare const provenanceContextSchema: z.ZodObject<{
129
+ repository: z.ZodNullable<z.ZodString>;
130
+ ref: z.ZodNullable<z.ZodString>;
131
+ sha: z.ZodNullable<z.ZodString>;
132
+ workflowRef: z.ZodNullable<z.ZodString>;
133
+ runId: z.ZodString;
134
+ jobId: z.ZodString;
135
+ orgId: z.ZodString;
136
+ sourceOrigin: z.ZodString;
137
+ provider: z.ZodNullable<z.ZodString>;
138
+ issuer: z.ZodString;
139
+ orchestratorId: z.ZodString;
140
+ }, z.core.$loose>;
141
+ export type ProvenanceContext = z.infer<typeof provenanceContextSchema>;
142
+ //# sourceMappingURL=id-token-event-claims.d.ts.map
@@ -0,0 +1,113 @@
1
+ import "../rolldown-runtime-ClRpJifh.js";
2
+ import { isPullRequestFamilyTriggerEvent } from "../trigger/trigger-event-type.js";
3
+ import { z } from "zod";
4
+ //#region src/provenance/id-token-event-claims.ts
5
+ /**
6
+ * The event-context half of an OIDC ID token's claims, shared by every minter.
7
+ *
8
+ * A cloud trust policy pins `sub` and, ideally, the claims below. Those values
9
+ * are what tells a fork pull request from a trusted push, so the two minters —
10
+ * the customer's orchestrator and the hosted Platform — must derive them
11
+ * identically. Any drift between them is a hole: a bundle minted by one would
12
+ * satisfy a policy written against the other. So the derivation lives here,
13
+ * once, and each minter spreads the result into its own claim object.
14
+ */
15
+ /** The sentinel for a value the run did not resolve. Never a plausible default. */
16
+ const UNRESOLVED_CLAIM = "unresolved";
17
+ /**
18
+ * Build the event-context claims from a run row.
19
+ *
20
+ * A NULL never becomes a plausible default. `head_repository` does not fall
21
+ * back to `repository`, and `is_fork` does not fall back to `'false'`: both
22
+ * would fail OPEN on a lost write, which is the failure these claims exist to
23
+ * remove.
24
+ */
25
+ function buildEventClaims(run) {
26
+ return {
27
+ event_name: run.trigger_event ?? "unknown",
28
+ base_ref: run.ref ?? "",
29
+ head_ref: run.head_ref ?? "",
30
+ head_repository: run.head_repository ?? "",
31
+ is_fork: run.is_fork == null ? UNRESOLVED_CLAIM : String(run.is_fork),
32
+ trust_tier: run.trust_tier ?? "unresolved",
33
+ actor: run.trigger_actor_username ?? ""
34
+ };
35
+ }
36
+ /**
37
+ * Build the token subject.
38
+ *
39
+ * Two shapes, mirroring GitHub Actions' own claim vocabulary — which is what
40
+ * every customer's existing trust policy is written against:
41
+ *
42
+ * - pull-request family: `repo:<owner/repo>:pull_request`, with NO ref
43
+ * segment. GitHub omits the ref there for exactly this reason.
44
+ * - everything else: `repo:<owner/repo>:ref:<ref>:workflow:<name>`, unchanged.
45
+ *
46
+ * A pull request's `ref` is its BASE branch, so the branch-shaped subject made
47
+ * a fork PR against `main` byte-identical to a trusted push to `main`. An
48
+ * external contributor whose PR ran the same workflow minted the exact string a
49
+ * policy pinned, and assumed the customer's cloud role. The two shapes cannot
50
+ * collide: no branch name produces the literal segment `pull_request` in the
51
+ * position a branch subject puts `ref`.
52
+ *
53
+ * The event tested is `subject_trigger_event ?? trigger_event`. A re-run writes
54
+ * `trigger_event: 'rerun'` and carries the original run's event in
55
+ * `subject_trigger_event`, so re-running a pull request keeps the
56
+ * pull-request subject instead of decaying to the colliding branch shape.
57
+ * `trigger_event` itself is left alone because two other readers depend on it:
58
+ * the dashboard's trigger-type filter and the git credential relay's
59
+ * `triggerTypeFilters`, both of which must keep seeing `'rerun'`.
60
+ *
61
+ * `legacyPullRequestSubject` restores the old, colliding form for one release —
62
+ * see `KICI_OIDC_LEGACY_PR_SUB`. It is deprecated on arrival.
63
+ *
64
+ * @deprecated `legacyPullRequestSubject` is removed at v1.0.0.
65
+ */
66
+ function buildIdTokenSubject(run, opts) {
67
+ const repository = run.repo_identifier ?? "unknown";
68
+ const subjectEvent = run.subject_trigger_event ?? run.trigger_event;
69
+ if (!opts?.legacyPullRequestSubject && isPullRequestFamilyTriggerEvent(subjectEvent)) return `repo:${repository}:pull_request`;
70
+ return `repo:${repository}:ref:${run.ref ?? "unknown"}:workflow:${run.workflow_name ?? "unknown"}`;
71
+ }
72
+ /**
73
+ * The orchestrator's own view of a build, sent with the job so the agent can
74
+ * freeze a provenance statement that matches what a live mint would say.
75
+ *
76
+ * Every field mirrors a claim `buildIdTokenClaims` derives, so a statement
77
+ * built from this passes `crossCheckBuildContext` against the token that is
78
+ * later minted for the same (run, job) — which is what lets the orchestrator
79
+ * refuse a deferred statement it has not checked against its own run row.
80
+ *
81
+ * `.passthrough()` so an older agent tolerates a field a newer orchestrator
82
+ * adds.
83
+ */
84
+ const provenanceContextSchema = z.object({
85
+ /** `owner/repo`, the token's `repository` claim. */
86
+ repository: z.string().nullable(),
87
+ /** The branch the run PRESENTS — a pull request's BASE branch. */
88
+ ref: z.string().nullable(),
89
+ /** The run's commit SHA, the token's `sha` claim. */
90
+ sha: z.string().nullable(),
91
+ /**
92
+ * The token's `workflow_ref` claim (`<workflow_name>@<sha>`) — NOT the git
93
+ * ref used to clone a global workflow's repository. The statement's
94
+ * `workflow.path` is compared against this.
95
+ */
96
+ workflowRef: z.string().nullable(),
97
+ runId: z.string(),
98
+ jobId: z.string(),
99
+ /** The customer's public org id, resolved server-side from the routing key. */
100
+ orgId: z.string(),
101
+ /** `triggered` or `run-remote`, derived from the run's local-working-tree flag. */
102
+ sourceOrigin: z.string(),
103
+ /** Informational source provider (github / gitlab / …). */
104
+ provider: z.string().nullable(),
105
+ /** The orchestrator's provenance issuer, for the statement's `builder.id`. */
106
+ issuer: z.string(),
107
+ /** This orchestrator's instance id, also for `builder.id`. */
108
+ orchestratorId: z.string()
109
+ }).passthrough();
110
+ //#endregion
111
+ export { UNRESOLVED_CLAIM, buildEventClaims, buildIdTokenSubject, provenanceContextSchema };
112
+
113
+ //# sourceMappingURL=id-token-event-claims.js.map
@@ -1,9 +1,18 @@
1
1
  /**
2
- * Lowercase-hex SHA-256 of the DSSE statement payload bytes. This is the binding
3
- * a deferred OIDC mint commits to (truth-contract property 2): the later token
4
- * carries this hash as a claim so the Platform identity cannot be re-bound to a
5
- * different frozen statement at retry time. Browser-safe: `crypto.subtle` only,
6
- * so the verifier (dashboard + CLI) can recompute it.
2
+ * Lowercase-hex SHA-256 of the DSSE statement payload bytes.
3
+ *
4
+ * One of the two bindings a deferred OIDC mint commits to: the later token
5
+ * carries this hash as a claim, so the identity cannot be re-bound to a
6
+ * different frozen statement at retry time.
7
+ *
8
+ * It is a binding, not a substitute for the build-context cross-check. The
9
+ * verifier requires BOTH for a non-live origin — the hash proves the statement
10
+ * has not been swapped, and the cross-check proves the statement agrees with
11
+ * the run the token names. A hash-only rule verified a bundle whose statement
12
+ * claimed a release SHA the build never touched.
13
+ *
14
+ * Browser-safe: `crypto.subtle` only, so the verifier (dashboard + CLI) can
15
+ * recompute it.
7
16
  */
8
17
  export declare function computeStatementHash(payload: Uint8Array): Promise<string>;
9
18
  //# sourceMappingURL=statement-hash.d.ts.map
@@ -1,11 +1,20 @@
1
1
  import "../rolldown-runtime-ClRpJifh.js";
2
2
  //#region src/provenance/statement-hash.ts
3
3
  /**
4
- * Lowercase-hex SHA-256 of the DSSE statement payload bytes. This is the binding
5
- * a deferred OIDC mint commits to (truth-contract property 2): the later token
6
- * carries this hash as a claim so the Platform identity cannot be re-bound to a
7
- * different frozen statement at retry time. Browser-safe: `crypto.subtle` only,
8
- * so the verifier (dashboard + CLI) can recompute it.
4
+ * Lowercase-hex SHA-256 of the DSSE statement payload bytes.
5
+ *
6
+ * One of the two bindings a deferred OIDC mint commits to: the later token
7
+ * carries this hash as a claim, so the identity cannot be re-bound to a
8
+ * different frozen statement at retry time.
9
+ *
10
+ * It is a binding, not a substitute for the build-context cross-check. The
11
+ * verifier requires BOTH for a non-live origin — the hash proves the statement
12
+ * has not been swapped, and the cross-check proves the statement agrees with
13
+ * the run the token names. A hash-only rule verified a bundle whose statement
14
+ * claimed a release SHA the build never touched.
15
+ *
16
+ * Browser-safe: `crypto.subtle` only, so the verifier (dashboard + CLI) can
17
+ * recompute it.
9
18
  */
10
19
  async function computeStatementHash(payload) {
11
20
  const digest = await crypto.subtle.digest("SHA-256", payload);
@@ -75,4 +75,25 @@ export interface VerifyKiciBundleOptions {
75
75
  * verification is `verified:false` with populated `failures`).
76
76
  */
77
77
  export declare function verifyKiciBundle(opts: VerifyKiciBundleOptions): Promise<VerifyResult>;
78
+ /**
79
+ * The statement's build context must equal the (server-truth) JWT claims.
80
+ *
81
+ * Only the **claims** side is normalized (`?? ''`): the producer coerces a
82
+ * null/absent `repository` / `ref` / `workflow_ref` claim into the
83
+ * schema-required empty string and omits `commit` entirely when `sha` is
84
+ * falsy, so a null/absent claim is compared against that coerced `''` /
85
+ * omitted-commit. The **statement** side stays strict — a non-empty
86
+ * attacker-controlled statement field never matches a null claim
87
+ * (`'evil/repo' === (null ?? '')` → `'evil/repo' === ''` → false), and an
88
+ * empty statement field never matches a non-null claim — so this relaxation
89
+ * is forgery-safe in exactly one direction: empty/absent statement ⇔
90
+ * null/absent claim, and nothing else. `runId` / `jobId` are always minted,
91
+ * so they stay strict.
92
+ */
93
+ export declare function crossCheckBuildContext(statement: KiciProvenanceStatement, claims: Record<string, unknown>): boolean;
94
+ /** At least one subject must carry a digest matching the supplied artifact digest. */
95
+ export declare function checkSubjectDigest(statement: KiciProvenanceStatement, expected: {
96
+ alg: string;
97
+ hex: string;
98
+ }): boolean;
78
99
  //# sourceMappingURL=verify.d.ts.map
@@ -106,18 +106,17 @@ async function verifyKiciBundle(opts) {
106
106
  checks.dsse = "fail";
107
107
  failures.push("dsse_signature_invalid");
108
108
  }
109
- const attestationOrigin = resolveAttestationOrigin(claims, statement);
109
+ const attestationOrigin = resolveAttestationOrigin(claims);
110
110
  if (statement && claims && statementBytes) {
111
- if (attestationOrigin === AttestationOrigin.enum.live) {
112
- const ok = crossCheckBuildContext(statement, claims);
113
- checks.buildContext = ok ? "pass" : "fail";
114
- if (!ok) failures.push("build_context_mismatch");
115
- } else {
111
+ const contextOk = crossCheckBuildContext(statement, claims);
112
+ if (!contextOk) failures.push("build_context_mismatch");
113
+ let hashOk = true;
114
+ if (attestationOrigin !== AttestationOrigin.enum.live) {
116
115
  const actual = await computeStatementHash(statementBytes);
117
- const ok = typeof claims.statement_hash === "string" && claims.statement_hash === actual;
118
- checks.buildContext = ok ? "pass" : "fail";
119
- if (!ok) failures.push("statement_hash_mismatch");
116
+ hashOk = typeof claims.statement_hash === "string" && claims.statement_hash === actual;
117
+ if (!hashOk) failures.push("statement_hash_mismatch");
120
118
  }
119
+ checks.buildContext = contextOk && hashOk ? "pass" : "fail";
121
120
  } else {
122
121
  checks.buildContext = "fail";
123
122
  failures.push("build_context_uncheckable");
@@ -137,13 +136,24 @@ async function verifyKiciBundle(opts) {
137
136
  attestationOrigin
138
137
  };
139
138
  }
140
- /** Derive the mint-timing origin from the token claim, then the frozen statement. */
141
- function resolveAttestationOrigin(claims, statement) {
139
+ /**
140
+ * Derive the mint-timing origin from the SIGNED token claim.
141
+ *
142
+ * There is deliberately no fallback to the frozen statement's
143
+ * `internalParameters.attestationOrigin`. That field is unsigned and
144
+ * producer-written (the agent sets it itself), so honouring it let a producer
145
+ * choose which verification branch ran by writing one field — a per-bundle
146
+ * opt-out of the check.
147
+ *
148
+ * A token carrying no claim is treated as `live`, which is now the STRICTER
149
+ * reading: `live` requires the field-by-field cross-check and nothing else can
150
+ * satisfy it. Both minters have emitted the claim unconditionally since the
151
+ * deferral feature shipped, so a token without one predates it and is live in
152
+ * fact as well as by default.
153
+ */
154
+ function resolveAttestationOrigin(claims) {
142
155
  const fromClaim = AttestationOrigin.safeParse(claims?.attestation_origin);
143
- if (fromClaim.success) return fromClaim.data;
144
- const internal = statement?.predicate.buildDefinition.internalParameters;
145
- const fromStatement = AttestationOrigin.safeParse(internal?.attestationOrigin);
146
- return fromStatement.success ? fromStatement.data : AttestationOrigin.enum.live;
156
+ return fromClaim.success ? fromClaim.data : AttestationOrigin.enum.live;
147
157
  }
148
158
  /**
149
159
  * The statement's build context must equal the (server-truth) JWT claims.
@@ -171,6 +181,6 @@ function checkSubjectDigest(statement, expected) {
171
181
  return statement.subject.some((s) => s.digest[expected.alg] === expected.hex);
172
182
  }
173
183
  //#endregion
174
- export { verifyKiciBundle };
184
+ export { checkSubjectDigest, crossCheckBuildContext, verifyKiciBundle };
175
185
 
176
186
  //# sourceMappingURL=verify.js.map