@adcp/sdk 16.1.0 → 16.3.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 (126) hide show
  1. package/AGENTS.md +2 -0
  2. package/dist/lib/core/AgentClient.js +17 -1
  3. package/dist/lib/core/AgentClient.mjs +17 -1
  4. package/dist/lib/core/SingleAgentClient.js +13 -0
  5. package/dist/lib/core/SingleAgentClient.mjs +13 -0
  6. package/dist/lib/core/TaskExecutor.js +88 -13
  7. package/dist/lib/core/TaskExecutor.mjs +89 -14
  8. package/dist/lib/errors/index.d.mts +34 -0
  9. package/dist/lib/errors/index.d.ts +34 -0
  10. package/dist/lib/errors/index.js +11 -0
  11. package/dist/lib/errors/index.mjs +10 -0
  12. package/dist/lib/governance/authorization.d.mts +66 -0
  13. package/dist/lib/governance/authorization.d.ts +66 -0
  14. package/dist/lib/governance/authorization.js +181 -37
  15. package/dist/lib/governance/authorization.mjs +183 -36
  16. package/dist/lib/governance/index.d.mts +2 -2
  17. package/dist/lib/governance/index.d.ts +2 -2
  18. package/dist/lib/governance/index.js +4 -2
  19. package/dist/lib/governance/index.mjs +3 -1
  20. package/dist/lib/identity-3.3/index.d.mts +1 -0
  21. package/dist/lib/identity-3.3/index.d.ts +1 -0
  22. package/dist/lib/identity-3.3/index.js +3 -1
  23. package/dist/lib/identity-3.3/index.mjs +1 -0
  24. package/dist/lib/identity-3.3/jws-artifacts.d.mts +147 -0
  25. package/dist/lib/identity-3.3/jws-artifacts.d.ts +147 -0
  26. package/dist/lib/identity-3.3/jws-artifacts.js +258 -0
  27. package/dist/lib/identity-3.3/jws-artifacts.mjs +234 -0
  28. package/dist/lib/identity-3.3/revocation.d.mts +7 -0
  29. package/dist/lib/identity-3.3/revocation.d.ts +7 -0
  30. package/dist/lib/identity-3.3/revocation.js +13 -0
  31. package/dist/lib/identity-3.3/revocation.mjs +13 -0
  32. package/dist/lib/index.d.mts +3 -3
  33. package/dist/lib/index.d.ts +3 -3
  34. package/dist/lib/index.js +4 -0
  35. package/dist/lib/index.mjs +4 -0
  36. package/dist/lib/media-buy/compatibility.d.mts +1 -0
  37. package/dist/lib/media-buy/compatibility.d.ts +1 -0
  38. package/dist/lib/media-buy/compatibility.js +33 -10
  39. package/dist/lib/media-buy/compatibility.mjs +32 -14
  40. package/dist/lib/negotiation/buyer.js +11 -5
  41. package/dist/lib/negotiation/buyer.mjs +11 -5
  42. package/dist/lib/negotiation/types.d.mts +4 -2
  43. package/dist/lib/negotiation/types.d.ts +4 -2
  44. package/dist/lib/negotiation/verification.d.mts +15 -6
  45. package/dist/lib/negotiation/verification.d.ts +15 -6
  46. package/dist/lib/negotiation/verification.js +146 -27
  47. package/dist/lib/negotiation/verification.mjs +146 -28
  48. package/dist/lib/negotiation/version.d.mts +6 -0
  49. package/dist/lib/negotiation/version.d.ts +6 -0
  50. package/dist/lib/negotiation/version.js +60 -0
  51. package/dist/lib/negotiation/version.mjs +33 -0
  52. package/dist/lib/registry/types.generated.d.mts +24 -1
  53. package/dist/lib/registry/types.generated.d.ts +24 -1
  54. package/dist/lib/server/create-adcp-server.js +1 -1
  55. package/dist/lib/server/create-adcp-server.mjs +1 -1
  56. package/dist/lib/server/error-arm-tools.d.mts +10 -10
  57. package/dist/lib/server/error-arm-tools.d.ts +10 -10
  58. package/dist/lib/server/error-arm-tools.js +22 -58
  59. package/dist/lib/server/error-arm-tools.mjs +30 -65
  60. package/dist/lib/server/index.d.mts +2 -2
  61. package/dist/lib/server/index.d.ts +2 -2
  62. package/dist/lib/server/index.js +2 -0
  63. package/dist/lib/server/index.mjs +2 -0
  64. package/dist/lib/signing/web-bot-auth.js +5 -0
  65. package/dist/lib/signing/web-bot-auth.mjs +4 -0
  66. package/dist/lib/testing/compliance/comply.js +17 -0
  67. package/dist/lib/testing/compliance/comply.mjs +17 -0
  68. package/dist/lib/testing/index.d.mts +3 -0
  69. package/dist/lib/testing/index.d.ts +3 -0
  70. package/dist/lib/testing/index.js +44 -2
  71. package/dist/lib/testing/index.mjs +42 -0
  72. package/dist/lib/testing/storyboard/collection-selection.d.mts +259 -0
  73. package/dist/lib/testing/storyboard/collection-selection.d.ts +259 -0
  74. package/dist/lib/testing/storyboard/collection-selection.js +384 -0
  75. package/dist/lib/testing/storyboard/collection-selection.mjs +348 -0
  76. package/dist/lib/testing/storyboard/context.js +12 -6
  77. package/dist/lib/testing/storyboard/context.mjs +12 -6
  78. package/dist/lib/testing/storyboard/fixture-resolution.d.mts +39 -1
  79. package/dist/lib/testing/storyboard/fixture-resolution.d.ts +39 -1
  80. package/dist/lib/testing/storyboard/fixture-resolution.js +216 -12
  81. package/dist/lib/testing/storyboard/fixture-resolution.mjs +218 -12
  82. package/dist/lib/testing/storyboard/index.d.mts +9 -2
  83. package/dist/lib/testing/storyboard/index.d.ts +9 -2
  84. package/dist/lib/testing/storyboard/index.js +51 -2
  85. package/dist/lib/testing/storyboard/index.mjs +49 -1
  86. package/dist/lib/testing/storyboard/prepared-fixtures.d.mts +471 -0
  87. package/dist/lib/testing/storyboard/prepared-fixtures.d.ts +471 -0
  88. package/dist/lib/testing/storyboard/prepared-fixtures.js +434 -0
  89. package/dist/lib/testing/storyboard/prepared-fixtures.mjs +389 -0
  90. package/dist/lib/testing/storyboard/protected-context.d.mts +127 -0
  91. package/dist/lib/testing/storyboard/protected-context.d.ts +127 -0
  92. package/dist/lib/testing/storyboard/protected-context.js +301 -0
  93. package/dist/lib/testing/storyboard/protected-context.mjs +267 -0
  94. package/dist/lib/testing/storyboard/rejection-hints.js +13 -4
  95. package/dist/lib/testing/storyboard/rejection-hints.mjs +13 -4
  96. package/dist/lib/testing/storyboard/runner.d.mts +24 -0
  97. package/dist/lib/testing/storyboard/runner.d.ts +24 -0
  98. package/dist/lib/testing/storyboard/runner.js +1061 -105
  99. package/dist/lib/testing/storyboard/runner.mjs +1075 -106
  100. package/dist/lib/testing/storyboard/seeding.d.mts +15 -1
  101. package/dist/lib/testing/storyboard/seeding.d.ts +15 -1
  102. package/dist/lib/testing/storyboard/seeding.js +272 -12
  103. package/dist/lib/testing/storyboard/seeding.mjs +274 -13
  104. package/dist/lib/testing/storyboard/types.d.mts +207 -10
  105. package/dist/lib/testing/storyboard/types.d.ts +207 -10
  106. package/dist/lib/testing/storyboard/types.js +1 -1
  107. package/dist/lib/testing/storyboard/types.mjs +1 -1
  108. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  109. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  110. package/dist/lib/testing/storyboard/validations.js +95 -0
  111. package/dist/lib/testing/storyboard/validations.mjs +95 -0
  112. package/dist/lib/validation/bundled-schema-store.js +5 -0
  113. package/dist/lib/validation/bundled-schema-store.mjs +4 -0
  114. package/dist/lib/validation/schema-loader.js +19 -2
  115. package/dist/lib/validation/schema-loader.mjs +22 -3
  116. package/dist/lib/version.d.mts +3 -3
  117. package/dist/lib/version.d.ts +3 -3
  118. package/dist/lib/version.js +3 -3
  119. package/dist/lib/version.mjs +3 -3
  120. package/docs/TYPE-SUMMARY.md +2 -2
  121. package/docs/guides/COLLECTION-FIXTURE-FOUNDATION.md +850 -0
  122. package/docs/guides/PROPOSAL-TERMS-VERIFICATION.md +66 -0
  123. package/docs/llms.txt +4 -3
  124. package/docs/migration-14.x-rc-worksheet.md +4 -4
  125. package/docs/migration-agent-identity-3.3.md +51 -6
  126. package/package.json +6 -4
@@ -0,0 +1,389 @@
1
+ import { createHash, createHmac, randomBytes, randomUUID } from "node:crypto";
2
+ import { readdirSync, statSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { getActiveSchemaArtifactLocation } from "../../validation/schema-loader.mjs";
5
+ import { ADCP_VERSION } from "../../version.mjs";
6
+ const PREPARED_FIXTURES_CONTRACT_VERSION = "1.0.0";
7
+ const REFERENCE_REFUSAL_DETAIL = "prepared fixture state was refused by the configured authorizer";
8
+ const trustedStates = /* @__PURE__ */ new WeakSet();
9
+ const registries = /* @__PURE__ */ new WeakMap();
10
+ function freezeDeepLocal(value) {
11
+ if (value === null || typeof value !== "object") return value;
12
+ if (Object.isFrozen(value)) return value;
13
+ Object.freeze(value);
14
+ for (const nested of Object.values(value)) freezeDeepLocal(nested);
15
+ return value;
16
+ }
17
+ const CONSTRUCTION_TOKEN = /* @__PURE__ */ Symbol("adcp.preparedStoryboardFixtures");
18
+ class PreparedStoryboardFixtures {
19
+ #binding;
20
+ #resolutions;
21
+ #coverageGaps;
22
+ #captures;
23
+ #captureProvenance;
24
+ #witnesses;
25
+ #disposition;
26
+ #phase;
27
+ /**
28
+ * @internal Constructed only by `prepareStoryboardFixtures`. The token is
29
+ * module-private, so an external `new` call cannot satisfy it.
30
+ */
31
+ constructor(init, token) {
32
+ if (token !== CONSTRUCTION_TOKEN) {
33
+ throw new Error(
34
+ "PreparedStoryboardFixtures cannot be constructed directly; call prepareStoryboardFixtures() to produce prepared state"
35
+ );
36
+ }
37
+ this.#binding = freezeDeepLocal(structuredClone(init.binding));
38
+ if (init.bindings) registries.set(this, init.bindings);
39
+ this.#resolutions = Object.freeze(structuredClone(init.resolutions));
40
+ this.#coverageGaps = Object.freeze(structuredClone(init.coverageGaps));
41
+ this.#captures = Object.freeze(structuredClone(init.captures));
42
+ this.#captureProvenance = Object.freeze(structuredClone(init.captureProvenance));
43
+ this.#witnesses = Object.freeze(structuredClone(init.witnesses));
44
+ this.#disposition = freezeDeepLocal(structuredClone(init.disposition));
45
+ this.#phase = init.phase === void 0 ? void 0 : structuredClone(init.phase);
46
+ trustedStates.add(this);
47
+ Object.freeze(this);
48
+ }
49
+ /** Nonsecret binding identity. Detached copy. */
50
+ get binding() {
51
+ return structuredClone(this.#binding);
52
+ }
53
+ get disposition() {
54
+ return structuredClone(this.#disposition);
55
+ }
56
+ /** Machine-readable per-handle resolution evidence. Detached copy. */
57
+ get resolutions() {
58
+ return structuredClone(this.#resolutions);
59
+ }
60
+ /** Coverage gaps observed during preparation. Detached copy. */
61
+ get coverageGaps() {
62
+ return structuredClone(this.#coverageGaps);
63
+ }
64
+ /** Pinned seller identities per handle. Detached copy. */
65
+ get selections() {
66
+ return this.#resolutions.filter((record) => record.seller_ids !== void 0).map((record) => ({
67
+ handle: record.handle,
68
+ fixture_type: record.fixture_type,
69
+ ...record.product_handle && { product_handle: record.product_handle },
70
+ seller_ids: structuredClone(record.seller_ids)
71
+ }));
72
+ }
73
+ /** Reserved-namespace captures. Detached copy. */
74
+ get captures() {
75
+ return structuredClone(this.#captures);
76
+ }
77
+ /** Capture provenance. Detached copy. */
78
+ get captureProvenance() {
79
+ return structuredClone(this.#captureProvenance);
80
+ }
81
+ /** Retained collection witnesses per handle. Detached copy. */
82
+ get witnesses() {
83
+ return structuredClone(this.#witnesses);
84
+ }
85
+ /**
86
+ * Detached copy of the synthetic pre-flight phase, for attachment to a
87
+ * full run's report. A live reference would let a report consumer mutate
88
+ * the preparation's own evidence — including a failed or no-match
89
+ * disposition's raw failure detail.
90
+ */
91
+ get phase() {
92
+ return this.#phase === void 0 ? void 0 : structuredClone(this.#phase);
93
+ }
94
+ }
95
+ Object.freeze(PreparedStoryboardFixtures.prototype);
96
+ function readPreparedBindingRegistry(state) {
97
+ return registries.get(state);
98
+ }
99
+ function preparedBindingRegistryFor(state) {
100
+ return readPreparedBindingRegistry(state);
101
+ }
102
+ function createPreparedStoryboardFixtures(init) {
103
+ return new PreparedStoryboardFixtures(init, CONSTRUCTION_TOKEN);
104
+ }
105
+ function withPreparedFixtures(state, authorizer) {
106
+ return { prepared_fixtures: state, prepared_fixtures_authorizer: authorizer };
107
+ }
108
+ function isTrustedPreparedFixtures(value) {
109
+ return value instanceof PreparedStoryboardFixtures && trustedStates.has(value);
110
+ }
111
+ const CREDENTIAL_FINGERPRINT_KEY = randomBytes(32);
112
+ function fingerprintCredential(secret) {
113
+ return createHmac("sha256", CREDENTIAL_FINGERPRINT_KEY).update(secret).digest("hex").slice(0, 32);
114
+ }
115
+ function redactRouteCredentials(raw) {
116
+ let url;
117
+ try {
118
+ url = new URL(raw);
119
+ } catch {
120
+ return raw;
121
+ }
122
+ url.username = "";
123
+ url.password = "";
124
+ for (const key of [...url.searchParams.keys()]) {
125
+ if (/(token|secret|key|password|credential|signature|auth)/i.test(key)) {
126
+ url.searchParams.set(key, "[redacted]");
127
+ }
128
+ }
129
+ url.hash = "";
130
+ return url.toString();
131
+ }
132
+ function canonicalize(value) {
133
+ if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
134
+ if (Array.isArray(value)) return `[${value.map(canonicalize).join(",")}]`;
135
+ const entries = Object.entries(value).filter(([, nested]) => nested !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
136
+ return `{${entries.map(([key, nested]) => `${JSON.stringify(key)}:${canonicalize(nested)}`).join(",")}}`;
137
+ }
138
+ function digestStoryboard(storyboard) {
139
+ return createHash("sha256").update(canonicalize(storyboard)).digest("hex");
140
+ }
141
+ const schemaDigestCache = /* @__PURE__ */ new Map();
142
+ function digestSchemaRoot(root) {
143
+ const cached = schemaDigestCache.get(root);
144
+ if (cached !== void 0) return cached;
145
+ const lines = [`root\0${root}`];
146
+ const walk = (dir, rel) => {
147
+ let entries;
148
+ try {
149
+ entries = readdirSync(dir, { withFileTypes: true });
150
+ } catch {
151
+ lines.push(`${rel || "."}\0unreadable-dir`);
152
+ return;
153
+ }
154
+ for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
155
+ const absolute = path.join(dir, entry.name);
156
+ const relative = rel ? `${rel}/${entry.name}` : entry.name;
157
+ if (entry.isDirectory()) {
158
+ walk(absolute, relative);
159
+ continue;
160
+ }
161
+ try {
162
+ const stat = statSync(absolute);
163
+ lines.push(`${relative}\0${stat.size}\0${stat.mtimeMs}`);
164
+ } catch {
165
+ lines.push(`${relative}\0unreadable`);
166
+ }
167
+ }
168
+ };
169
+ walk(root, "");
170
+ const digest = createHash("sha256").update(lines.join("\n")).digest("hex");
171
+ schemaDigestCache.set(root, digest);
172
+ return digest;
173
+ }
174
+ function resolveSchemaArtifact(adcpVersion = ADCP_VERSION) {
175
+ const location = getActiveSchemaArtifactLocation(adcpVersion);
176
+ return {
177
+ adcp_version: adcpVersion,
178
+ bundle_key: location.bundle_key,
179
+ external: location.external,
180
+ digest: digestSchemaRoot(location.root)
181
+ };
182
+ }
183
+ function buildPreparedBinding(input) {
184
+ return {
185
+ storyboard_id: input.storyboard.id,
186
+ storyboard_digest: digestStoryboard(input.storyboard),
187
+ agent_urls: [...input.agentUrls].sort().map(redactRouteCredentials),
188
+ // The route the GRAPH was pinned against, positionally — preparation
189
+ // contacts exactly one agent, `agentUrls[0]`, and nothing else in the
190
+ // binding records which one that was. `agent_urls` is sorted and
191
+ // `agent_route_fingerprint` sorts by key, both deliberately
192
+ // order-insensitive, so two runs whose `agents` maps differ only in
193
+ // insertion order produced byte-identical bindings while preparation
194
+ // had actually read seller B's catalog for one and seller A's for the
195
+ // other. Reuse across them sent B's pinned product_id to A.
196
+ fixture_agent_url: redactRouteCredentials(input.agentUrls[0] ?? ""),
197
+ // Covers the specialism→URL PAIRING, not just the URL set. Sorting the
198
+ // set alone meant swapping two specialisms' routes left the fingerprint
199
+ // unchanged while every per-specialism credential went to a different
200
+ // seller.
201
+ agent_route_fingerprint: fingerprintCredential(
202
+ JSON.stringify([
203
+ [...input.agentUrls].sort(),
204
+ input.agentRouteMap ? [...input.agentRouteMap].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0) : null,
205
+ // The RAW positional first url — the one preparation contacts.
206
+ // `fixture_agent_url` beside it is redacted for the authorizer to
207
+ // read, so two routes differing only in userinfo or a credential
208
+ // query parameter render identically there and would have
209
+ // collapsed into one binding.
210
+ input.agentUrls[0] ?? null
211
+ ])
212
+ ),
213
+ schema_artifact: resolveSchemaArtifact(input.adcpVersion ?? ADCP_VERSION),
214
+ contract_version: PREPARED_FIXTURES_CONTRACT_VERSION,
215
+ account_scope: input.accountScope,
216
+ brand_scope: input.brandScope,
217
+ ...input.requestAccountScope !== void 0 && { request_account_scope: input.requestAccountScope },
218
+ sandbox: input.sandbox,
219
+ ...input.principal !== void 0 && { principal: input.principal },
220
+ credential_fingerprint: fingerprintCredential(input.credential),
221
+ ...input.stepCredential !== void 0 && {
222
+ step_credential_fingerprint: fingerprintCredential(input.stepCredential)
223
+ },
224
+ ...input.stepAuthKind !== void 0 && { step_auth_kind: input.stepAuthKind },
225
+ // Independent of the credential: a routed step whose credential happens
226
+ // to equal the run's still dispatches to one specific seller.
227
+ ...input.stepRoute !== void 0 && { step_route: input.stepRoute },
228
+ run_id: input.runId ?? randomUUID()
229
+ };
230
+ }
231
+ const BINDING_FIELDS = [
232
+ "storyboard_id",
233
+ "storyboard_digest",
234
+ "agent_urls",
235
+ "agent_route_fingerprint",
236
+ "fixture_agent_url",
237
+ "schema_artifact",
238
+ "contract_version",
239
+ "account_scope",
240
+ "brand_scope",
241
+ "sandbox",
242
+ "principal",
243
+ "credential_fingerprint"
244
+ ];
245
+ function diffPreparedBinding(prepared, current) {
246
+ const mismatches = [];
247
+ for (const field of BINDING_FIELDS) {
248
+ if (canonicalize(prepared[field]) !== canonicalize(current[field])) mismatches.push(field);
249
+ }
250
+ return mismatches;
251
+ }
252
+ function isReference(value) {
253
+ return typeof value === "object" && value !== null && typeof value.ref === "string" && typeof value.resolve === "function";
254
+ }
255
+ async function resolvePreparedFixtures(input) {
256
+ const supplied = input.supplied;
257
+ if (supplied === void 0) return void 0;
258
+ const authorizer = input.authorizer;
259
+ if (!authorizer) {
260
+ return {
261
+ ok: false,
262
+ refusal: {
263
+ code: "authorizer_required",
264
+ detail: isReference(supplied) ? "prepared_fixtures was supplied as an opaque reference, but no prepared_fixtures_authorizer was configured. An opaque reference is never authorization: supply an authorizer, because the SDK has no session and no grant store and cannot decide whether this reference is still yours." : "prepared_fixtures reuse requires prepared_fixtures_authorizer. Trusted object identity proves the state came from prepareStoryboardFixtures in this process; it does not establish a current grant, and binding coherence cannot observe revocation. Supply an authorizer that re-checks live grant state on every lookup. If this run genuinely has no grant model (single-tenant CLI, local dev), either drop prepared_fixtures and let the runner re-resolve, or pass prepared_fixtures_authorizer: () => ({ authorized: true }) as an explicit, reviewable decision. See docs/guides/COLLECTION-FIXTURE-FOUNDATION.md section 4."
265
+ }
266
+ };
267
+ }
268
+ let state = supplied;
269
+ let reference;
270
+ if (isReference(supplied)) {
271
+ reference = supplied.ref;
272
+ try {
273
+ state = await supplied.resolve(supplied.ref);
274
+ } catch {
275
+ state = void 0;
276
+ }
277
+ }
278
+ const refuseReference = async () => {
279
+ try {
280
+ await authorizer({
281
+ // Cloned, and cloned SEPARATELY, exactly as the authorized path
282
+ // does: handing the same object twice would let an authorizer
283
+ // detect `binding === current` by reference.
284
+ //
285
+ // That closes the object-IDENTITY distinction and nothing more.
286
+ // The VALUES still differ by path — a resolving reference carries
287
+ // the stored preparation binding, this one carries a second copy
288
+ // of `current` — so an authorizer that deep-compares them can
289
+ // still tell a missing reference from a present one. That is not
290
+ // the oracle this branch exists to close: the authorizer IS the
291
+ // host, it owns the store it would be learning about, and it
292
+ // cannot be given a fabricated stored binding without being lied
293
+ // to. What is closed is the oracle available to the CALLER, who
294
+ // sees only the refusal: identical text, identical machine code,
295
+ // identical validation shape, same authorizer call count.
296
+ binding: structuredClone(input.current),
297
+ current: structuredClone(input.current),
298
+ ...input.stepId !== void 0 && { step_id: input.stepId },
299
+ ...input.gate !== void 0 && { gate: input.gate },
300
+ ...reference !== void 0 && { reference }
301
+ });
302
+ } catch {
303
+ }
304
+ return { ok: false, refusal: { code: "authorizer_denied", detail: REFERENCE_REFUSAL_DETAIL } };
305
+ };
306
+ if (!isTrustedPreparedFixtures(state)) {
307
+ if (reference !== void 0) return refuseReference();
308
+ return {
309
+ ok: false,
310
+ refusal: {
311
+ code: "untrusted_state",
312
+ detail: "prepared fixture state was not produced by prepareStoryboardFixtures in this process; a rehydrated, cloned or caller-constructed look-alike carries no preparation authority"
313
+ }
314
+ };
315
+ }
316
+ const binding = state.binding;
317
+ if (binding.contract_version !== PREPARED_FIXTURES_CONTRACT_VERSION) {
318
+ if (reference !== void 0) return refuseReference();
319
+ return {
320
+ ok: false,
321
+ refusal: {
322
+ code: "contract_version_mismatch",
323
+ detail: `prepared state declares contract version ${binding.contract_version}; this runner implements ${PREPARED_FIXTURES_CONTRACT_VERSION}`
324
+ }
325
+ };
326
+ }
327
+ let authorized = false;
328
+ let decisionDetail;
329
+ try {
330
+ const decision = await authorizer({
331
+ binding: structuredClone(binding),
332
+ current: structuredClone(input.current),
333
+ ...input.stepId !== void 0 && { step_id: input.stepId },
334
+ ...input.gate !== void 0 && { gate: input.gate },
335
+ ...reference !== void 0 && { reference }
336
+ });
337
+ authorized = decision?.authorized === true;
338
+ const detail = decision?.detail;
339
+ decisionDetail = typeof detail === "string" ? detail : void 0;
340
+ } catch {
341
+ return { ok: false, refusal: { code: "authorizer_denied", detail: REFERENCE_REFUSAL_DETAIL } };
342
+ }
343
+ if (!authorized) {
344
+ if (reference !== void 0) {
345
+ return { ok: false, refusal: { code: "authorizer_denied", detail: REFERENCE_REFUSAL_DETAIL } };
346
+ }
347
+ return {
348
+ ok: false,
349
+ refusal: {
350
+ code: "authorizer_denied",
351
+ // `detail` is optional, so an absent one must not render as the
352
+ // string "undefined" — a difference in text is a difference a
353
+ // caller can measure.
354
+ detail: decisionDetail !== void 0 && decisionDetail.length > 0 ? `prepared fixture state was refused by the configured authorizer: ${decisionDetail}` : REFERENCE_REFUSAL_DETAIL
355
+ }
356
+ };
357
+ }
358
+ const mismatches = diffPreparedBinding(binding, input.current);
359
+ if (mismatches.length > 0) {
360
+ if (reference !== void 0) {
361
+ return { ok: false, refusal: { code: "authorizer_denied", detail: REFERENCE_REFUSAL_DETAIL } };
362
+ }
363
+ return {
364
+ ok: false,
365
+ refusal: {
366
+ code: "binding_mismatch",
367
+ // Field names only. The values are another principal's identity when
368
+ // the caller guessed a reference, and `credential_fingerprint` must
369
+ // never reach operator-visible text at all.
370
+ detail: `prepared fixture state does not match the current run scope; differing binding field(s): ${mismatches.join(", ")}`
371
+ }
372
+ };
373
+ }
374
+ return { ok: true, state };
375
+ }
376
+ export {
377
+ PREPARED_FIXTURES_CONTRACT_VERSION,
378
+ PreparedStoryboardFixtures,
379
+ buildPreparedBinding,
380
+ createPreparedStoryboardFixtures,
381
+ diffPreparedBinding,
382
+ digestStoryboard,
383
+ fingerprintCredential,
384
+ isTrustedPreparedFixtures,
385
+ preparedBindingRegistryFor,
386
+ resolvePreparedFixtures,
387
+ resolveSchemaArtifact,
388
+ withPreparedFixtures
389
+ };
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Protected storyboard-context namespace and the single atomic write funnel
3
+ * that guards it.
4
+ *
5
+ * Fixture preparation publishes the selected product, its scoped pricing and
6
+ * its collection witnesses into storyboard context so later steps can build
7
+ * requests from the entity that was actually pinned. Those values decide what
8
+ * gets purchased, so nothing downstream may replace, delete or shadow them:
9
+ * not caller-supplied `options.context`, not storyboard defaults, not a
10
+ * convention extractor, not an authored `context_outputs` entry, not a
11
+ * generator alias, not a task-completion capture, not creative-prefix
12
+ * cleanup, and not a seller response.
13
+ *
14
+ * Three properties, each load-bearing:
15
+ *
16
+ * 1. **Reserved flat namespace.** Keys under `__adcp_fixture_` are writable
17
+ * only with a runner-owned capability. The prefix is flat so existing
18
+ * whole-key `$context.<key>` substitution keeps working unchanged; no
19
+ * nested-token syntax is introduced.
20
+ * 2. **Atomic whole-patch validation.** A patch touching a protected key
21
+ * without the capability is rejected before *any* map changes, so a
22
+ * partially-applied write can never leave context and provenance
23
+ * disagreeing. Values, deletions, provenance entries and
24
+ * unavailable-output markers all commit through the same call —
25
+ * protecting values alone would let a caller suppress a prepared capture
26
+ * by marking it unavailable, or rewrite its provenance. Prototype-
27
+ * sensitive keys are refused outright for every writer, and writes use
28
+ * `Object.defineProperty` so no inherited setter is ever invoked.
29
+ * 3. **Detached immutable snapshots.** Committed values are deep-cloned on
30
+ * ingestion and deep-frozen, and every read for request building or
31
+ * diagnostics hands back a fresh clone. Guarding map assignment alone is
32
+ * not enough: `injectContext`/`deepReplace` hand out `context[key]` by
33
+ * reference today, so a mutated request object can otherwise reach back
34
+ * into the snapshot it was built from.
35
+ *
36
+ * The capability is a runtime object identity held in a module-private
37
+ * `WeakSet`. It is deliberately not a string, a `source_kind`, a provenance
38
+ * entry or a boolean: all of those are reachable from caller JSON. A
39
+ * structurally-cloned or JSON-rehydrated copy of a capability is not the
40
+ * capability, which is what keeps "I have a frozen object that looks right"
41
+ * from being authority.
42
+ */
43
+ import type { ContextProvenanceEntry, StoryboardContext } from './types.mjs';
44
+ /** Reserved flat prefix for preparation-produced context keys. */
45
+ export declare const PROTECTED_CONTEXT_PREFIX = "__adcp_fixture_";
46
+ /** True for any key inside the reserved namespace, well-formed or not. */
47
+ export declare function isProtectedContextKey(key: string): boolean;
48
+ export type ProtectedContextViolationCode = 'unauthorized_write' | 'unauthorized_delete' | 'unauthorized_metadata' | 'already_committed' | 'reserved_key' | 'uncloneable_value' | 'invalid_key';
49
+ /** Thrown by the funnel; never partially applied. */
50
+ export declare class ProtectedContextViolation extends Error {
51
+ readonly code: ProtectedContextViolationCode;
52
+ readonly keys: readonly string[];
53
+ constructor(code: ProtectedContextViolationCode, keys: readonly string[], detail: string);
54
+ }
55
+ /**
56
+ * Runner-owned write capability. Opaque by construction: the only way to
57
+ * obtain one that the funnel accepts is to call `createProtectedContextWriter`
58
+ * inside this process.
59
+ */
60
+ export interface ProtectedContextWriter {
61
+ readonly kind: 'adcp.storyboard.protected-context-writer';
62
+ }
63
+ export declare function createProtectedContextWriter(): ProtectedContextWriter;
64
+ /** Register a context object as protected under `writer`. Idempotent. */
65
+ export declare function registerProtectedContext(context: StoryboardContext, writer: ProtectedContextWriter): void;
66
+ /**
67
+ * Carry protection onto a clone. Call wherever the runner shallow-copies or
68
+ * adopts a context object, alongside `forwardAliasCache`.
69
+ */
70
+ export declare function forwardProtectedContext(from: StoryboardContext, to: StoryboardContext): void;
71
+ /** Keys already committed on this context, for evidence and diagnostics. */
72
+ export declare function listProtectedContextKeys(context: StoryboardContext): string[];
73
+ export interface ProtectedContextSinks {
74
+ /** Per-key provenance the runner reports back to callers. */
75
+ provenance?: Map<string, ContextProvenanceEntry>;
76
+ /** Keys a skipped/ungraded step marked as unavailable, with their reason. */
77
+ unavailable?: Map<string, string>;
78
+ }
79
+ export interface ProtectedContextPatch {
80
+ /** Values to write. */
81
+ set?: Readonly<Record<string, unknown>>;
82
+ /** Keys to remove. */
83
+ unset?: readonly string[];
84
+ /** Provenance entries to write. */
85
+ setProvenance?: Readonly<Record<string, ContextProvenanceEntry>>;
86
+ /** Provenance entries to remove. */
87
+ unsetProvenance?: readonly string[];
88
+ /** Keys to mark unavailable (reason keyed by the same key). */
89
+ markUnavailable?: Readonly<Record<string, string>> | readonly string[];
90
+ /** Keys whose unavailable marker should be cleared. */
91
+ clearUnavailable?: readonly string[];
92
+ }
93
+ export interface ApplyProtectedContextOptions {
94
+ writer?: ProtectedContextWriter;
95
+ sinks?: ProtectedContextSinks;
96
+ /**
97
+ * Where this patch came from, used verbatim in the violation message so an
98
+ * operator can see which funnel attempted the write.
99
+ */
100
+ origin: string;
101
+ }
102
+ /**
103
+ * The sole commit point for storyboard-context values, deletions, provenance
104
+ * and unavailable-output metadata.
105
+ *
106
+ * Validates the whole patch first. A forbidden write, delete or metadata
107
+ * change throws `ProtectedContextViolation` and leaves every map exactly as
108
+ * it was — including the ordinary, unprotected keys in the same patch, so a
109
+ * rejected batch never half-lands.
110
+ */
111
+ export declare function applyProtectedContextPatch(context: StoryboardContext, patch: ProtectedContextPatch, options: ApplyProtectedContextOptions): void;
112
+ /** Authoring-time guard for a declaration-produced reserved key. */
113
+ export declare function assertWellFormedProtectedKey(key: string, location: string): void;
114
+ /**
115
+ * Deep-freeze in place. Applied to the canonical snapshot after cloning, so
116
+ * the value the runner holds cannot be mutated even by code that reached it
117
+ * through an ordinary context read.
118
+ */
119
+ export declare function freezeDeep<T>(value: T): T;
120
+ /**
121
+ * Detached view of a protected value for request building or diagnostics.
122
+ *
123
+ * Request builders legitimately mutate the objects they are handed, so a
124
+ * frozen snapshot cannot be passed through directly; a clone is both safe to
125
+ * mutate and unable to write back.
126
+ */
127
+ export declare function detachProtectedValue(value: unknown): unknown;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Protected storyboard-context namespace and the single atomic write funnel
3
+ * that guards it.
4
+ *
5
+ * Fixture preparation publishes the selected product, its scoped pricing and
6
+ * its collection witnesses into storyboard context so later steps can build
7
+ * requests from the entity that was actually pinned. Those values decide what
8
+ * gets purchased, so nothing downstream may replace, delete or shadow them:
9
+ * not caller-supplied `options.context`, not storyboard defaults, not a
10
+ * convention extractor, not an authored `context_outputs` entry, not a
11
+ * generator alias, not a task-completion capture, not creative-prefix
12
+ * cleanup, and not a seller response.
13
+ *
14
+ * Three properties, each load-bearing:
15
+ *
16
+ * 1. **Reserved flat namespace.** Keys under `__adcp_fixture_` are writable
17
+ * only with a runner-owned capability. The prefix is flat so existing
18
+ * whole-key `$context.<key>` substitution keeps working unchanged; no
19
+ * nested-token syntax is introduced.
20
+ * 2. **Atomic whole-patch validation.** A patch touching a protected key
21
+ * without the capability is rejected before *any* map changes, so a
22
+ * partially-applied write can never leave context and provenance
23
+ * disagreeing. Values, deletions, provenance entries and
24
+ * unavailable-output markers all commit through the same call —
25
+ * protecting values alone would let a caller suppress a prepared capture
26
+ * by marking it unavailable, or rewrite its provenance. Prototype-
27
+ * sensitive keys are refused outright for every writer, and writes use
28
+ * `Object.defineProperty` so no inherited setter is ever invoked.
29
+ * 3. **Detached immutable snapshots.** Committed values are deep-cloned on
30
+ * ingestion and deep-frozen, and every read for request building or
31
+ * diagnostics hands back a fresh clone. Guarding map assignment alone is
32
+ * not enough: `injectContext`/`deepReplace` hand out `context[key]` by
33
+ * reference today, so a mutated request object can otherwise reach back
34
+ * into the snapshot it was built from.
35
+ *
36
+ * The capability is a runtime object identity held in a module-private
37
+ * `WeakSet`. It is deliberately not a string, a `source_kind`, a provenance
38
+ * entry or a boolean: all of those are reachable from caller JSON. A
39
+ * structurally-cloned or JSON-rehydrated copy of a capability is not the
40
+ * capability, which is what keeps "I have a frozen object that looks right"
41
+ * from being authority.
42
+ */
43
+ import type { ContextProvenanceEntry, StoryboardContext } from './types';
44
+ /** Reserved flat prefix for preparation-produced context keys. */
45
+ export declare const PROTECTED_CONTEXT_PREFIX = "__adcp_fixture_";
46
+ /** True for any key inside the reserved namespace, well-formed or not. */
47
+ export declare function isProtectedContextKey(key: string): boolean;
48
+ export type ProtectedContextViolationCode = 'unauthorized_write' | 'unauthorized_delete' | 'unauthorized_metadata' | 'already_committed' | 'reserved_key' | 'uncloneable_value' | 'invalid_key';
49
+ /** Thrown by the funnel; never partially applied. */
50
+ export declare class ProtectedContextViolation extends Error {
51
+ readonly code: ProtectedContextViolationCode;
52
+ readonly keys: readonly string[];
53
+ constructor(code: ProtectedContextViolationCode, keys: readonly string[], detail: string);
54
+ }
55
+ /**
56
+ * Runner-owned write capability. Opaque by construction: the only way to
57
+ * obtain one that the funnel accepts is to call `createProtectedContextWriter`
58
+ * inside this process.
59
+ */
60
+ export interface ProtectedContextWriter {
61
+ readonly kind: 'adcp.storyboard.protected-context-writer';
62
+ }
63
+ export declare function createProtectedContextWriter(): ProtectedContextWriter;
64
+ /** Register a context object as protected under `writer`. Idempotent. */
65
+ export declare function registerProtectedContext(context: StoryboardContext, writer: ProtectedContextWriter): void;
66
+ /**
67
+ * Carry protection onto a clone. Call wherever the runner shallow-copies or
68
+ * adopts a context object, alongside `forwardAliasCache`.
69
+ */
70
+ export declare function forwardProtectedContext(from: StoryboardContext, to: StoryboardContext): void;
71
+ /** Keys already committed on this context, for evidence and diagnostics. */
72
+ export declare function listProtectedContextKeys(context: StoryboardContext): string[];
73
+ export interface ProtectedContextSinks {
74
+ /** Per-key provenance the runner reports back to callers. */
75
+ provenance?: Map<string, ContextProvenanceEntry>;
76
+ /** Keys a skipped/ungraded step marked as unavailable, with their reason. */
77
+ unavailable?: Map<string, string>;
78
+ }
79
+ export interface ProtectedContextPatch {
80
+ /** Values to write. */
81
+ set?: Readonly<Record<string, unknown>>;
82
+ /** Keys to remove. */
83
+ unset?: readonly string[];
84
+ /** Provenance entries to write. */
85
+ setProvenance?: Readonly<Record<string, ContextProvenanceEntry>>;
86
+ /** Provenance entries to remove. */
87
+ unsetProvenance?: readonly string[];
88
+ /** Keys to mark unavailable (reason keyed by the same key). */
89
+ markUnavailable?: Readonly<Record<string, string>> | readonly string[];
90
+ /** Keys whose unavailable marker should be cleared. */
91
+ clearUnavailable?: readonly string[];
92
+ }
93
+ export interface ApplyProtectedContextOptions {
94
+ writer?: ProtectedContextWriter;
95
+ sinks?: ProtectedContextSinks;
96
+ /**
97
+ * Where this patch came from, used verbatim in the violation message so an
98
+ * operator can see which funnel attempted the write.
99
+ */
100
+ origin: string;
101
+ }
102
+ /**
103
+ * The sole commit point for storyboard-context values, deletions, provenance
104
+ * and unavailable-output metadata.
105
+ *
106
+ * Validates the whole patch first. A forbidden write, delete or metadata
107
+ * change throws `ProtectedContextViolation` and leaves every map exactly as
108
+ * it was — including the ordinary, unprotected keys in the same patch, so a
109
+ * rejected batch never half-lands.
110
+ */
111
+ export declare function applyProtectedContextPatch(context: StoryboardContext, patch: ProtectedContextPatch, options: ApplyProtectedContextOptions): void;
112
+ /** Authoring-time guard for a declaration-produced reserved key. */
113
+ export declare function assertWellFormedProtectedKey(key: string, location: string): void;
114
+ /**
115
+ * Deep-freeze in place. Applied to the canonical snapshot after cloning, so
116
+ * the value the runner holds cannot be mutated even by code that reached it
117
+ * through an ordinary context read.
118
+ */
119
+ export declare function freezeDeep<T>(value: T): T;
120
+ /**
121
+ * Detached view of a protected value for request building or diagnostics.
122
+ *
123
+ * Request builders legitimately mutate the objects they are handed, so a
124
+ * frozen snapshot cannot be passed through directly; a clone is both safe to
125
+ * mutate and unable to write back.
126
+ */
127
+ export declare function detachProtectedValue(value: unknown): unknown;