@tokenoftrust/storefront-runner 2.0.17 → 2.0.19
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.
- package/apps/storefront/env.d.ts +21 -0
- package/apps/storefront/migrations-apps/0012_test_destinations.sql +29 -0
- package/apps/storefront/src/components/blog/ReactionBar.astro +1 -1
- package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +365 -49
- package/apps/storefront/src/components/islands/checkoutVerificationFlow.ts +251 -0
- package/apps/storefront/src/config/compliance/rulesets.ts +18 -7
- package/apps/storefront/src/config/devTenantSeed.ts +24 -1
- package/apps/storefront/src/config/evidenceTenantRoster.ts +25 -0
- package/apps/storefront/src/config/publicationSpaceConfig.ts +25 -0
- package/apps/storefront/src/config/resolver.ts +5 -0
- package/apps/storefront/src/config/tenantAnalyticsFiles.ts +43 -0
- package/apps/storefront/src/config/tenants.ts +11 -0
- package/apps/storefront/src/layouts/Layout.astro +5 -1
- package/apps/storefront/src/lib/activity/killSwitch.ts +1 -1
- package/apps/storefront/src/lib/admin/consentSettingsClient.ts +113 -0
- package/apps/storefront/src/lib/apps/consentConfigRead.ts +24 -0
- package/apps/storefront/src/lib/apps/gtmMigrationDraft.ts +103 -0
- package/apps/storefront/src/lib/auth/brokerAssertion.ts +18 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +11 -1
- package/apps/storefront/src/lib/auth/internalContentAuthority.ts +153 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +25 -0
- package/apps/storefront/src/lib/auth/session.ts +3 -0
- package/apps/storefront/src/lib/checkoutCommerce.ts +1 -1
- package/apps/storefront/src/lib/compliance/enforcement.ts +25 -3
- package/apps/storefront/src/lib/d1/schema-apps.ts +44 -0
- package/apps/storefront/src/lib/edgeCache.ts +17 -1
- package/apps/storefront/src/lib/env.ts +13 -111
- package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +198 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +1 -1
- package/apps/storefront/src/lib/publication/contentProvider.ts +159 -0
- package/apps/storefront/src/lib/publication/runtimeSource.ts +97 -0
- package/apps/storefront/src/lib/rawMarketingHtml.ts +9 -1
- package/apps/storefront/src/lib/runtimeEnv.ts +124 -0
- package/apps/storefront/src/lib/verification/totVerificationClient.ts +485 -0
- package/apps/storefront/src/middleware/candidatePin.ts +35 -0
- package/apps/storefront/src/middleware/controlPlane.ts +291 -0
- package/apps/storefront/src/middleware/index.ts +200 -793
- package/apps/storefront/src/middleware/publicRender.ts +43 -0
- package/apps/storefront/src/middleware/renderChain.ts +730 -0
- package/apps/storefront/src/middleware/routeBranch.ts +192 -0
- package/apps/storefront/src/middleware/staticBundle.ts +109 -0
- package/apps/storefront/src/pages/[...slug].astro +1 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +1 -0
- package/apps/storefront/src/pages/api/auth/step-up-verify.ts +1 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +1 -0
- package/apps/storefront/src/pages/api/compliance/preflight.ts +179 -2
- package/apps/storefront/src/pages/index.astro +1 -0
- package/apps/storefront/vitest.config.ts +5 -0
- package/package.json +1 -1
- package/packages/public-runtime/package.json +1 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/deliver-test-event-request.schema.json +122 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-test-destination-delivery-request.schema.json +79 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/register-test-destination-request.schema.json +93 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/test-destination-delivery.schema.json +106 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/test-destination.schema.json +77 -0
- package/packages/public-runtime/schemas/publication-space/v1/manifest.schema.json +195 -0
- package/packages/public-runtime/schemas/publication-space/v1/normalized-mount.schema.json +80 -0
- package/packages/public-runtime/schemas/publication-space/v1/ref.schema.json +20 -0
- package/packages/public-runtime/schemas/publication-version/v1/pointer.schema.json +28 -0
- package/packages/public-runtime/schemas/publication-version/v1/transition-request.schema.json +44 -0
- package/packages/public-runtime/schemas/publication-version/v1/transition.schema.json +47 -0
- package/packages/public-runtime/schemas/publication-version/v1/version.schema.json +58 -0
- package/packages/public-runtime/src/activity/catalog.ts +1 -1
- package/packages/public-runtime/src/candidate-index.ts +6 -0
- package/packages/public-runtime/src/customization-reconcile.ts +8 -0
- package/packages/public-runtime/src/html-cache-policy.ts +57 -0
- package/packages/public-runtime/src/index.ts +9 -0
- package/packages/public-runtime/src/integration/consent-broker.ts +115 -0
- package/packages/public-runtime/src/integration/interim-bridge.ts +111 -27
- package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +65 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +81 -0
- package/packages/public-runtime/src/integration/render-phases.ts +8 -3
- package/packages/public-runtime/src/publication-artifact.ts +220 -0
- package/packages/public-runtime/src/publication-reader.ts +187 -0
- package/packages/public-runtime/src/publication-routing.ts +246 -0
- package/packages/public-runtime/src/publication-space.ts +853 -0
- package/packages/public-runtime/src/publication-version.ts +602 -0
- package/packages/public-runtime/src/route-tier.ts +257 -0
- package/packages/public-runtime/src/static-bundle.ts +18 -5
- package/packages/public-runtime/src/tenant-assets.ts +85 -17
- package/packages/public-runtime/src/tenant.ts +70 -0
- package/packages/public-runtime/tests/fixtures/publication-spaces/internal.json +32 -0
- package/packages/public-runtime/tests/fixtures/publication-spaces/mounted-public.json +38 -0
- package/packages/public-runtime/tests/fixtures/publication-spaces/normalized-mounts.json +39 -0
- package/packages/public-runtime/tests/fixtures/publication-spaces/ref.json +5 -0
- package/packages/public-runtime/tests/fixtures/publication-spaces/root-public.json +31 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/articles-v1.json +20 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/articles-v2.json +20 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/internal-v1.json +20 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/live-pointer.json +11 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/live-transition-request.json +14 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/live-transition.json +17 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/root-v1.json +20 -0
- package/packages/public-runtime/tests/fixtures/publication-versions/root-v2.json +20 -0
- package/scripts/dev/ai-edit.mjs +16 -3
- package/scripts/dev/port-check.mjs +18 -3
- package/scripts/dev/publish.mjs +26 -3
- package/scripts/tenant/validate.mjs +14 -1
|
@@ -140,6 +140,33 @@ export function defaultConsentSnapshot(): ConsentSnapshot {
|
|
|
140
140
|
return makeSnapshot([ESSENTIAL]);
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The MATERIALIZE-ONLY request header carrying a tenant's resolved consent snapshot
|
|
145
|
+
* (ADR 0013). The go-live dispatch resolves the tenant's REAL snapshot from D1 (the
|
|
146
|
+
* Worker holds that binding) and the bake pipeline sets this header on each render, so
|
|
147
|
+
* the static render inlines the real snapshot WITHOUT a consent D1 read on the
|
|
148
|
+
* public/materialize path. Never present on a real public request.
|
|
149
|
+
*/
|
|
150
|
+
export const CONSENT_SNAPSHOT_INPUT_HEADER = "x-tot-consent-snapshot";
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The HMAC-SHA256 signature (hex) over the {@link CONSENT_SNAPSHOT_INPUT_HEADER}
|
|
154
|
+
* value, carried alongside it. The go-live Worker (which holds both the tenant's real
|
|
155
|
+
* consent D1 and the signing secret) signs the snapshot; the materialize render
|
|
156
|
+
* VERIFIES it before honoring the input header. Absent/invalid ⇒ the snapshot input is
|
|
157
|
+
* rejected and consent falls back to the fail-closed default. Together with the
|
|
158
|
+
* materialize-mode binding gate this makes the input header un-forgeable: the deployed
|
|
159
|
+
* Worker never reads it at all, and even in materialize mode a snapshot without a valid
|
|
160
|
+
* signature from the go-live secret is refused.
|
|
161
|
+
*/
|
|
162
|
+
export const CONSENT_SNAPSHOT_SIG_HEADER = "x-tot-consent-snapshot-sig";
|
|
163
|
+
|
|
164
|
+
/** The Worker secret binding whose value signs/verifies the baked consent snapshot. */
|
|
165
|
+
export const CONSENT_SNAPSHOT_HMAC_SECRET_BINDING = "CONSENT_SNAPSHOT_HMAC_SECRET";
|
|
166
|
+
|
|
167
|
+
/** The Worker binding that marks a MATERIALIZE-mode render — absent on the deployed Worker. */
|
|
168
|
+
export const MATERIALIZE_MODE_BINDING = "TOT_MATERIALIZE_MODE";
|
|
169
|
+
|
|
143
170
|
/**
|
|
144
171
|
* Whether a purpose is currently eligible. `essential` is always eligible; an
|
|
145
172
|
* unknown or unapproved-and-ungranted purpose is not — so this both permits
|
|
@@ -208,6 +235,94 @@ export function parseConsentSnapshot(raw: unknown): ConsentSnapshot | null {
|
|
|
208
235
|
return canonical;
|
|
209
236
|
}
|
|
210
237
|
|
|
238
|
+
// ── Baked-snapshot signature (materialize bake wire) ────────────────────────────
|
|
239
|
+
// The go-live Worker holds the tenant's real consent D1 AND the signing secret; it
|
|
240
|
+
// signs the resolved snapshot, and the materialize render verifies before honoring the
|
|
241
|
+
// input header. Web Crypto only (crypto.subtle) so it runs unchanged on workerd, and
|
|
242
|
+
// verification is constant-time. The signature is TENANT-BOUND — it covers a
|
|
243
|
+
// length-prefixed framing of `tenantId` and `serializedSnapshot`, so neither field's
|
|
244
|
+
// content can shift a byte across the field boundary: a signed snapshot for one tenant
|
|
245
|
+
// can never be replayed as another tenant's baked input, or as a different snapshot for
|
|
246
|
+
// the same tenant.
|
|
247
|
+
|
|
248
|
+
const consentSigTextEncoder = new TextEncoder();
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The exact string the consent snapshot signature covers: each field prefixed by its
|
|
252
|
+
* own UTF-8 byte length, so no value in either field can be interpreted as a field
|
|
253
|
+
* separator.
|
|
254
|
+
*/
|
|
255
|
+
function consentSignaturePayload(tenantId: string, serializedSnapshot: string): string {
|
|
256
|
+
const tenantBytes = consentSigTextEncoder.encode(tenantId).length;
|
|
257
|
+
const snapshotBytes = consentSigTextEncoder.encode(serializedSnapshot).length;
|
|
258
|
+
return `${tenantBytes}:${tenantId}${snapshotBytes}:${serializedSnapshot}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function consentHmacKey(secret: string): Promise<CryptoKey> {
|
|
262
|
+
return crypto.subtle.importKey(
|
|
263
|
+
"raw",
|
|
264
|
+
consentSigTextEncoder.encode(secret) as unknown as BufferSource,
|
|
265
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
266
|
+
false,
|
|
267
|
+
["sign", "verify"],
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function consentSigHexToBytes(hex: string): Uint8Array | null {
|
|
272
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || /[^0-9a-fA-F]/.test(hex)) return null;
|
|
273
|
+
const out = new Uint8Array(hex.length / 2);
|
|
274
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
275
|
+
return out;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Sign a tenant's serialized consent snapshot: hex HMAC-SHA256 over the length-prefixed
|
|
280
|
+
* {@link consentSignaturePayload}. The go-live Worker calls this; the materialize render
|
|
281
|
+
* verifies with the same secret via {@link verifyConsentSnapshotSig}.
|
|
282
|
+
*/
|
|
283
|
+
export async function signConsentSnapshot(
|
|
284
|
+
secret: string,
|
|
285
|
+
tenantId: string,
|
|
286
|
+
serializedSnapshot: string,
|
|
287
|
+
): Promise<string> {
|
|
288
|
+
const key = await consentHmacKey(secret);
|
|
289
|
+
const sig = await crypto.subtle.sign(
|
|
290
|
+
"HMAC",
|
|
291
|
+
key,
|
|
292
|
+
consentSigTextEncoder.encode(
|
|
293
|
+
consentSignaturePayload(tenantId, serializedSnapshot),
|
|
294
|
+
) as unknown as BufferSource,
|
|
295
|
+
);
|
|
296
|
+
let hex = "";
|
|
297
|
+
for (const b of new Uint8Array(sig)) hex += b.toString(16).padStart(2, "0");
|
|
298
|
+
return hex;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Verify a consent snapshot signature (constant-time via `crypto.subtle.verify`). A
|
|
303
|
+
* malformed / short / absent signature returns false rather than throwing — fail-closed,
|
|
304
|
+
* so a caller can never smuggle in an unsigned or forged snapshot.
|
|
305
|
+
*/
|
|
306
|
+
export async function verifyConsentSnapshotSig(
|
|
307
|
+
secret: string,
|
|
308
|
+
tenantId: string,
|
|
309
|
+
serializedSnapshot: string,
|
|
310
|
+
signatureHex: string | null | undefined,
|
|
311
|
+
): Promise<boolean> {
|
|
312
|
+
if (!signatureHex) return false;
|
|
313
|
+
const sig = consentSigHexToBytes(signatureHex.trim());
|
|
314
|
+
if (!sig) return false;
|
|
315
|
+
const key = await consentHmacKey(secret);
|
|
316
|
+
return crypto.subtle.verify(
|
|
317
|
+
"HMAC",
|
|
318
|
+
key,
|
|
319
|
+
sig as unknown as BufferSource,
|
|
320
|
+
consentSigTextEncoder.encode(
|
|
321
|
+
consentSignaturePayload(tenantId, serializedSnapshot),
|
|
322
|
+
) as unknown as BufferSource,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
211
326
|
/** Listener invoked with the new snapshot whenever eligibility changes. */
|
|
212
327
|
export type ConsentListener = (snapshot: ConsentSnapshot) => void;
|
|
213
328
|
|
|
@@ -12,13 +12,15 @@
|
|
|
12
12
|
* with it (the plan carries no vendor runtime, and `planRevision` moves), so a
|
|
13
13
|
* rollback leaves no stale effect.
|
|
14
14
|
*
|
|
15
|
-
* It is deliberately narrow (ADR 0016/0023 Q6):
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
15
|
+
* It is deliberately narrow (ADR 0016/0023 Q6): on a regulated tenant the bridge
|
|
16
|
+
* enables only the declarations the surface × tier matrix permits on the regulated
|
|
17
|
+
* surface without a certification — reviewed platform-adapter/sandbox/server tiers —
|
|
18
|
+
* and REFUSES full parent authority (`elevated-parent`) until a capability-specific
|
|
19
|
+
* certification clears every clause. The regulated shared-runtime floor is
|
|
20
|
+
* non-waivable, and this bridge never opens an escape hatch around it. Promote-time
|
|
21
|
+
* enforcement still runs through the full fail-closed gate
|
|
22
|
+
* (`verifyIntegrationPromotable`); this module is the render-time tenant-eligibility
|
|
23
|
+
* decision that precedes it.
|
|
22
24
|
*/
|
|
23
25
|
import type { BrowserEnvironment } from "./environment.js";
|
|
24
26
|
import type { EffectivePlan } from "./plan.js";
|
|
@@ -34,6 +36,13 @@ import {
|
|
|
34
36
|
isRegulatedParentCertified,
|
|
35
37
|
type RegulatedParentCertification,
|
|
36
38
|
} from "./regulated-parent-cert.js";
|
|
39
|
+
import {
|
|
40
|
+
allowedTiersForCell,
|
|
41
|
+
resolveCellAuthority,
|
|
42
|
+
type CellAuthority,
|
|
43
|
+
type TenantTier,
|
|
44
|
+
} from "../integration-capabilities.js";
|
|
45
|
+
import type { ExecutionTier } from "./phases.js";
|
|
37
46
|
|
|
38
47
|
/**
|
|
39
48
|
* Whether an integration's declared consent dependency permits loading its entry
|
|
@@ -57,6 +66,11 @@ export interface InterimBridgeRefusal {
|
|
|
57
66
|
surface: AssuranceSurface;
|
|
58
67
|
/** An operator-facing explanation of the refusal and what unblocks it. */
|
|
59
68
|
message: string;
|
|
69
|
+
/**
|
|
70
|
+
* The registered feedback item tracking this gap, so a consumer can route the reader
|
|
71
|
+
* to live work without parsing the message. See {@link REGULATED_REFUSAL_FEEDBACK_REF}.
|
|
72
|
+
*/
|
|
73
|
+
feedbackRef: string;
|
|
60
74
|
}
|
|
61
75
|
|
|
62
76
|
/**
|
|
@@ -80,10 +94,11 @@ export interface InterimBridgeInput {
|
|
|
80
94
|
* G2: the regulated elevated-parent certification artifact for a regulated tenant.
|
|
81
95
|
* The bridge NEVER accepts a raw "certified" boolean — it runs
|
|
82
96
|
* {@link decideRegulatedParentCert} itself (via {@link isRegulatedParentCertified}),
|
|
83
|
-
* so `certifiedImmutable` has a SINGLE ORIGIN: a regulated tenant
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
97
|
+
* so `certifiedImmutable` has a SINGLE ORIGIN: a regulated tenant's full-parent
|
|
98
|
+
* authority (`elevated-parent`) declaration is enabled ONLY when the cert clears
|
|
99
|
+
* every clause (pinned, undrifted from `servedDigest`, every server-derived
|
|
100
|
+
* obligation proven). Absent/failed ⇒ the decision fails closed and that declaration
|
|
101
|
+
* stays refused. This is the WS2 forward seam, not a per-tenant bypass.
|
|
87
102
|
*/
|
|
88
103
|
regulatedCertification?: RegulatedParentCertification | null;
|
|
89
104
|
/**
|
|
@@ -101,31 +116,71 @@ export interface InterimBridgeInput {
|
|
|
101
116
|
* set), so an eligible tenant's plan is exactly the kernel plan every mount serves —
|
|
102
117
|
* the bridge adds only the tenant-eligibility gate, not a second plan shape.
|
|
103
118
|
*/
|
|
119
|
+
/**
|
|
120
|
+
* Feedback tripwire for the regulated refusal below.
|
|
121
|
+
*
|
|
122
|
+
* A tripwire is three coupled parts: a registered feedback item describing an accepted
|
|
123
|
+
* gap, a reference to it from the message emitted where execution trips over that gap,
|
|
124
|
+
* and a workstream that owns closing it. Carrying the reference in the refusal itself is
|
|
125
|
+
* what routes a reader to live work instead of a fresh diagnosis.
|
|
126
|
+
*
|
|
127
|
+
* This gate refuses a regulated tenant whose declaration requests full parent
|
|
128
|
+
* authority (`elevated-parent`) without a {@link RegulatedParentCertification} that
|
|
129
|
+
* clears every clause — an absent or failed one fails closed. Owned by the
|
|
130
|
+
* `tot-regulated-integration-certification` workstream.
|
|
131
|
+
*
|
|
132
|
+
* See docs/playbooks/feedback-tripwire.md for the generalized recipe — this is the
|
|
133
|
+
* reference implementation, not a one-off.
|
|
134
|
+
*/
|
|
135
|
+
export const REGULATED_REFUSAL_FEEDBACK_REF = "fb-1788612281954-3q8f1n" as const;
|
|
136
|
+
|
|
104
137
|
export function resolveInterimBridgePlan(input: InterimBridgeInput): InterimBridgeResult {
|
|
105
138
|
const surface = resolveObligationSurface(input.tenant);
|
|
106
139
|
|
|
107
140
|
// The certification is the SINGLE ORIGIN of `certifiedImmutable`: derive it from the
|
|
108
|
-
// decision (never a raw external flag), so a regulated tenant
|
|
109
|
-
// cert actually clears every clause. Absent/failed
|
|
141
|
+
// decision (never a raw external flag), so a regulated tenant's full-parent authority
|
|
142
|
+
// declaration is enabled only when the cert actually clears every clause. Absent/failed
|
|
143
|
+
// ⇒ false ⇒ that declaration is refused.
|
|
110
144
|
const regulatedCertified = isRegulatedParentCertified(input.regulatedCertification, {
|
|
111
145
|
tenant: input.tenant,
|
|
112
146
|
servedDigest: input.servedDigest,
|
|
113
147
|
});
|
|
114
148
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
149
|
+
// Drive the render-time refusal off the same surface × tier matrix the policy layer
|
|
150
|
+
// enforces (SURFACE_TENANT_TIER_MATRIX): a regulated tenant is refused only for a
|
|
151
|
+
// declaration the matrix does not permit on the regulated surface — full parent
|
|
152
|
+
// authority (`elevated-parent`) without a certification that clears every clause, never
|
|
153
|
+
// a reviewed platform-adapter/sandbox/server tier the cell already allows. A cert whose
|
|
154
|
+
// codeForm is `mutable-container`, or that has drifted/is incomplete, never clears the
|
|
155
|
+
// decision, so mutable parent code on a regulated surface stays refused.
|
|
156
|
+
// `verifyIntegrationPromotable` remains the authoritative fail-closed backstop at
|
|
157
|
+
// promote time; this only stops the bridge PRE-refusing a class the policy permits.
|
|
158
|
+
if (isRegulated(input.tenant)) {
|
|
159
|
+
const cell = resolveCellAuthority(regulatedTenantTier(input.tenant), "regulated");
|
|
160
|
+
const refused =
|
|
161
|
+
!cell ||
|
|
162
|
+
input.declarations.some((decl) =>
|
|
163
|
+
isRegulatedDeclarationRefused(decl.manifest.browser.executionTier, cell, regulatedCertified),
|
|
164
|
+
);
|
|
165
|
+
if (refused) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
reason: "regulated-refused",
|
|
169
|
+
refusal: {
|
|
170
|
+
code: "regulated-tenant-not-certified",
|
|
171
|
+
surface,
|
|
172
|
+
message:
|
|
173
|
+
`This integration needs full-page access to run on your storefront. Because your ` +
|
|
174
|
+
`store is regulated, that level of access is only available once a compliance ` +
|
|
175
|
+
`certification confirms it can't reach age-verification or customer data — a ` +
|
|
176
|
+
`safeguard, not a limitation of your store. Standard analytics and platform ` +
|
|
177
|
+
`integrations, including Google Analytics 4 (GA4), work normally on a regulated ` +
|
|
178
|
+
`storefront and can be added today. To proceed, choose a standard, already-supported ` +
|
|
179
|
+
`integration, or contact support to pursue certification for this one.`,
|
|
180
|
+
feedbackRef: REGULATED_REFUSAL_FEEDBACK_REF,
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
129
184
|
}
|
|
130
185
|
|
|
131
186
|
const result = resolveEffectivePlan({
|
|
@@ -136,3 +191,32 @@ export function resolveInterimBridgePlan(input: InterimBridgeInput): InterimBrid
|
|
|
136
191
|
if (!result.ok) return { ok: false, reason: "invalid-plan", violations: result.violations };
|
|
137
192
|
return { ok: true, plan: result.value };
|
|
138
193
|
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* The regulated tenant tier for the matrix lookup. Both regulated tiers carry the
|
|
197
|
+
* identical `regulated` cell (`maxTier: "server"`, `elevatedParent: "cert-required"`),
|
|
198
|
+
* so the split only tracks the tenant's site type; an unset site type resolves to
|
|
199
|
+
* `regulated-commerce`, mirroring {@link resolveObligationSurface}'s commerce floor.
|
|
200
|
+
*/
|
|
201
|
+
function regulatedTenantTier(tenant: ObligationTenant): TenantTier {
|
|
202
|
+
return tenant.siteType === "marketing" ? "regulated-marketing" : "regulated-commerce";
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Whether one declaration is refused on the regulated surface, per the cell's authority.
|
|
207
|
+
* Full parent authority (`elevated-parent`) is gated by the cell's elevated-parent
|
|
208
|
+
* disposition — `cert-required` enables it only behind a certification that clears every
|
|
209
|
+
* clause, and any other disposition (`prohibited`/`acknowledge`) never enables it here
|
|
210
|
+
* (fail closed). Every other tier is permitted when it is within the cell's allowed
|
|
211
|
+
* non-elevated tiers (`platform-adapter` ≤ `sandbox` ≤ `server`).
|
|
212
|
+
*/
|
|
213
|
+
function isRegulatedDeclarationRefused(
|
|
214
|
+
tier: ExecutionTier,
|
|
215
|
+
cell: CellAuthority,
|
|
216
|
+
regulatedCertified: boolean,
|
|
217
|
+
): boolean {
|
|
218
|
+
if (tier === "elevated-parent") {
|
|
219
|
+
return cell.elevatedParent === "cert-required" ? !regulatedCertified : true;
|
|
220
|
+
}
|
|
221
|
+
return !allowedTiersForCell(cell).includes(tier);
|
|
222
|
+
}
|
|
@@ -286,6 +286,32 @@ const SET_INTEGRATION_INSTALL_STATUS_BODY_SCHEMA = {
|
|
|
286
286
|
const DELETE_INTEGRATION_INSTALL_BODY_SCHEMA = {
|
|
287
287
|
type: "object", additionalProperties: false, required: ["installId"], properties: { installId: PUBLIC_IDENTIFIER },
|
|
288
288
|
} as const;
|
|
289
|
+
const TEST_DESTINATION_ENVIRONMENT = { type: "string", enum: ["local", "candidate", "staged"] } as const;
|
|
290
|
+
const APPROVED_MARKETING_ACTION_NAME = {
|
|
291
|
+
type: "string",
|
|
292
|
+
enum: [
|
|
293
|
+
"user_clicked_contact_sales_mktg",
|
|
294
|
+
"user_clicked_create_account_mktg",
|
|
295
|
+
"user_clicked_login_mktg",
|
|
296
|
+
"user_clicked_contact_support_consumer_mktg",
|
|
297
|
+
"user_clicked_contact_support_partner_mktg",
|
|
298
|
+
],
|
|
299
|
+
} as const;
|
|
300
|
+
const REGISTER_TEST_DESTINATION_BODY_SCHEMA = {
|
|
301
|
+
type: "object", additionalProperties: false,
|
|
302
|
+
required: ["destinationId", "environment"],
|
|
303
|
+
properties: { destinationId: PUBLIC_IDENTIFIER, environment: TEST_DESTINATION_ENVIRONMENT },
|
|
304
|
+
} as const;
|
|
305
|
+
const DELIVER_TEST_EVENT_BODY_SCHEMA = {
|
|
306
|
+
type: "object", additionalProperties: false,
|
|
307
|
+
required: ["destinationId", "eventId", "actionName"],
|
|
308
|
+
properties: { destinationId: PUBLIC_IDENTIFIER, eventId: PUBLIC_IDENTIFIER, actionName: APPROVED_MARKETING_ACTION_NAME },
|
|
309
|
+
} as const;
|
|
310
|
+
const TEST_DESTINATION_DELIVERY_QUERY_SCHEMA = {
|
|
311
|
+
type: "object", additionalProperties: false,
|
|
312
|
+
required: ["destinationId", "eventId"],
|
|
313
|
+
properties: { destinationId: PUBLIC_IDENTIFIER, eventId: PUBLIC_IDENTIFIER },
|
|
314
|
+
} as const;
|
|
289
315
|
|
|
290
316
|
export const CREATE_MIGRATION_DRAFT_REQUEST_V1_JSON_SCHEMA = mutationOperationSchema("CreateMigrationDraftRequest", "create-migration-draft-request.schema.json", CREATE_MIGRATION_DRAFT_BODY_SCHEMA);
|
|
291
317
|
export const UPDATE_MIGRATION_DRAFT_REQUEST_V1_JSON_SCHEMA = mutationOperationSchema("UpdateMigrationDraftRequest", "update-migration-draft-request.schema.json", UPDATE_MIGRATION_DRAFT_BODY_SCHEMA);
|
|
@@ -294,6 +320,40 @@ export const CREATE_INTEGRATION_INSTALL_REQUEST_V1_JSON_SCHEMA = mutationOperati
|
|
|
294
320
|
export const REVISE_INTEGRATION_INSTALL_REQUEST_V1_JSON_SCHEMA = mutationOperationSchema("ReviseIntegrationInstallRequest", "revise-integration-install-request.schema.json", REVISE_INTEGRATION_INSTALL_BODY_SCHEMA);
|
|
295
321
|
export const SET_INTEGRATION_INSTALL_STATUS_REQUEST_V1_JSON_SCHEMA = mutationOperationSchema("SetIntegrationInstallStatusRequest", "set-integration-install-status-request.schema.json", SET_INTEGRATION_INSTALL_STATUS_BODY_SCHEMA);
|
|
296
322
|
export const DELETE_INTEGRATION_INSTALL_REQUEST_V1_JSON_SCHEMA = mutationOperationSchema("DeleteIntegrationInstallRequest", "delete-integration-install-request.schema.json", DELETE_INTEGRATION_INSTALL_BODY_SCHEMA);
|
|
323
|
+
export const REGISTER_TEST_DESTINATION_REQUEST_V1_JSON_SCHEMA = mutationOperationSchema("RegisterTestDestinationRequest", "register-test-destination-request.schema.json", REGISTER_TEST_DESTINATION_BODY_SCHEMA);
|
|
324
|
+
export const DELIVER_TEST_EVENT_REQUEST_V1_JSON_SCHEMA = mutationOperationSchema("DeliverTestEventRequest", "deliver-test-event-request.schema.json", DELIVER_TEST_EVENT_BODY_SCHEMA);
|
|
325
|
+
export const GET_TEST_DESTINATION_DELIVERY_REQUEST_V1_JSON_SCHEMA = readOperationSchema("GetTestDestinationDeliveryRequest", "get-test-destination-delivery-request.schema.json", TEST_DESTINATION_DELIVERY_QUERY_SCHEMA);
|
|
326
|
+
|
|
327
|
+
export const TEST_DESTINATION_V1_JSON_SCHEMA = {
|
|
328
|
+
$schema: DRAFT,
|
|
329
|
+
$id: `${BASE}/test-destination.schema.json`,
|
|
330
|
+
title: "TestDestinationV1",
|
|
331
|
+
type: "object",
|
|
332
|
+
additionalProperties: false,
|
|
333
|
+
required: ["schemaVersion", "destinationId", "environment", "createdAt"],
|
|
334
|
+
properties: {
|
|
335
|
+
schemaVersion: { type: "integer", const: 1 },
|
|
336
|
+
destinationId: PUBLIC_IDENTIFIER,
|
|
337
|
+
environment: TEST_DESTINATION_ENVIRONMENT,
|
|
338
|
+
createdAt: { ...SAFE_CONFIG_STRING, format: "date-time" },
|
|
339
|
+
},
|
|
340
|
+
} as const;
|
|
341
|
+
|
|
342
|
+
export const TEST_DESTINATION_DELIVERY_V1_JSON_SCHEMA = {
|
|
343
|
+
$schema: DRAFT,
|
|
344
|
+
$id: `${BASE}/test-destination-delivery.schema.json`,
|
|
345
|
+
title: "TestDestinationDeliveryV1",
|
|
346
|
+
type: "object",
|
|
347
|
+
additionalProperties: false,
|
|
348
|
+
required: ["schemaVersion", "destinationId", "eventId", "actionName", "deliveredAt"],
|
|
349
|
+
properties: {
|
|
350
|
+
schemaVersion: { type: "integer", const: 1 },
|
|
351
|
+
destinationId: PUBLIC_IDENTIFIER,
|
|
352
|
+
eventId: PUBLIC_IDENTIFIER,
|
|
353
|
+
actionName: APPROVED_MARKETING_ACTION_NAME,
|
|
354
|
+
deliveredAt: { ...SAFE_CONFIG_STRING, format: "date-time" },
|
|
355
|
+
},
|
|
356
|
+
} as const;
|
|
297
357
|
|
|
298
358
|
export const READ_SUCCESS_ENVELOPE_V1_JSON_SCHEMA = {
|
|
299
359
|
$schema: DRAFT,
|
|
@@ -376,6 +436,11 @@ export const MANAGED_ANALYTICS_API_V1_JSON_SCHEMAS = Object.freeze({
|
|
|
376
436
|
reviseIntegrationInstallRequest: REVISE_INTEGRATION_INSTALL_REQUEST_V1_JSON_SCHEMA,
|
|
377
437
|
setIntegrationInstallStatusRequest: SET_INTEGRATION_INSTALL_STATUS_REQUEST_V1_JSON_SCHEMA,
|
|
378
438
|
deleteIntegrationInstallRequest: DELETE_INTEGRATION_INSTALL_REQUEST_V1_JSON_SCHEMA,
|
|
439
|
+
registerTestDestinationRequest: REGISTER_TEST_DESTINATION_REQUEST_V1_JSON_SCHEMA,
|
|
440
|
+
deliverTestEventRequest: DELIVER_TEST_EVENT_REQUEST_V1_JSON_SCHEMA,
|
|
441
|
+
getTestDestinationDeliveryRequest: GET_TEST_DESTINATION_DELIVERY_REQUEST_V1_JSON_SCHEMA,
|
|
442
|
+
testDestination: TEST_DESTINATION_V1_JSON_SCHEMA,
|
|
443
|
+
testDestinationDelivery: TEST_DESTINATION_DELIVERY_V1_JSON_SCHEMA,
|
|
379
444
|
readSuccessEnvelope: READ_SUCCESS_ENVELOPE_V1_JSON_SCHEMA,
|
|
380
445
|
mutationSuccessEnvelope: MUTATION_SUCCESS_ENVELOPE_V1_JSON_SCHEMA,
|
|
381
446
|
errorEnvelope: API_ERROR_ENVELOPE_V1_JSON_SCHEMA,
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type { ContractResult } from "./result.js";
|
|
11
11
|
import { Violations, isPlainObject } from "./result.js";
|
|
12
12
|
import { isForbiddenKey } from "./secrets.js";
|
|
13
|
+
import { APPROVED_MARKETING_ACTION_NAMES } from "./events.js";
|
|
13
14
|
import {
|
|
14
15
|
MANAGED_ANALYTICS_PII_VALUE_PATTERN,
|
|
15
16
|
MANAGED_ANALYTICS_SECRET_VALUE_PATTERN,
|
|
@@ -112,6 +113,37 @@ export interface MigrationDraftV1 {
|
|
|
112
113
|
updatedAt: string;
|
|
113
114
|
}
|
|
114
115
|
|
|
116
|
+
/**
|
|
117
|
+
* A test destination is intentionally never `live`: it exists so a developer can
|
|
118
|
+
* prove exactly-once delivery for a new marketing action without touching a real
|
|
119
|
+
* vendor property. Real first-class adapters (GA4/Meta/Amplitude) fire directly to
|
|
120
|
+
* their vendor with no server-side readback; this destination is Storefront-owned,
|
|
121
|
+
* so a delivered event can be read back.
|
|
122
|
+
*/
|
|
123
|
+
export const TEST_DESTINATION_ENVIRONMENTS = ["local", "candidate", "staged"] as const;
|
|
124
|
+
export type TestDestinationEnvironment = (typeof TEST_DESTINATION_ENVIRONMENTS)[number];
|
|
125
|
+
|
|
126
|
+
export interface TestDestinationV1 {
|
|
127
|
+
schemaVersion: typeof MANAGED_ANALYTICS_SCHEMA_VERSION;
|
|
128
|
+
destinationId: OpaquePublicId;
|
|
129
|
+
environment: TestDestinationEnvironment;
|
|
130
|
+
createdAt: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* One delivered event, read back by (destinationId, eventId). `actionName` is
|
|
135
|
+
* restricted to the program-approved v1 marketing action allowlist — the same
|
|
136
|
+
* set a tenant's action registry may enable — so this proves delivery for a real
|
|
137
|
+
* action, never an arbitrary caller-invented event shape.
|
|
138
|
+
*/
|
|
139
|
+
export interface TestDestinationDeliveryV1 {
|
|
140
|
+
schemaVersion: typeof MANAGED_ANALYTICS_SCHEMA_VERSION;
|
|
141
|
+
destinationId: OpaquePublicId;
|
|
142
|
+
eventId: OpaquePublicId;
|
|
143
|
+
actionName: OpaquePublicId;
|
|
144
|
+
deliveredAt: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
115
147
|
export interface PageRequest {
|
|
116
148
|
cursor?: OpaquePublicId;
|
|
117
149
|
limit?: number;
|
|
@@ -261,6 +293,25 @@ export interface DeleteIntegrationInstallBody { installId: OpaquePublicId }
|
|
|
261
293
|
export type DeleteIntegrationInstallRequest = MutationRequest<DeleteIntegrationInstallBody>;
|
|
262
294
|
export type DeleteIntegrationInstallResponse = MutationResult<{ deleted: true }>;
|
|
263
295
|
|
|
296
|
+
export interface RegisterTestDestinationBody {
|
|
297
|
+
destinationId: OpaquePublicId;
|
|
298
|
+
environment: TestDestinationEnvironment;
|
|
299
|
+
}
|
|
300
|
+
export type RegisterTestDestinationRequest = MutationRequest<RegisterTestDestinationBody>;
|
|
301
|
+
export type RegisterTestDestinationResponse = MutationResult<{ destination: TestDestinationV1 }>;
|
|
302
|
+
|
|
303
|
+
export interface DeliverTestEventBody {
|
|
304
|
+
destinationId: OpaquePublicId;
|
|
305
|
+
eventId: OpaquePublicId;
|
|
306
|
+
actionName: OpaquePublicId;
|
|
307
|
+
}
|
|
308
|
+
export type DeliverTestEventRequest = MutationRequest<DeliverTestEventBody>;
|
|
309
|
+
/** `duplicate: true` when this eventId had already been delivered — the exactly-once proof. */
|
|
310
|
+
export type DeliverTestEventResponse = MutationResult<{ delivery: TestDestinationDeliveryV1; duplicate: boolean }>;
|
|
311
|
+
|
|
312
|
+
export type GetTestDestinationDeliveryRequest = ReadRequest<{ destinationId: OpaquePublicId; eventId: OpaquePublicId }>;
|
|
313
|
+
export type GetTestDestinationDeliveryResponse = ApiResult<TestDestinationDeliveryV1>;
|
|
314
|
+
|
|
264
315
|
/** The only backend seam WS2 needs; tests can mock this interface without HTTP. */
|
|
265
316
|
export interface ManagedAnalyticsApiV1Client {
|
|
266
317
|
listAdapterDefinitions(request: ListAdapterDefinitionsRequest): Promise<ListAdapterDefinitionsResponse>;
|
|
@@ -557,6 +608,26 @@ export function validateIntegrationRevisionV1(input: unknown): ContractResult<In
|
|
|
557
608
|
return v.ok ? prepared : { ok: false, violations: v.items };
|
|
558
609
|
}
|
|
559
610
|
|
|
611
|
+
export function validateTestDestinationV1(input: unknown): ContractResult<TestDestinationV1> {
|
|
612
|
+
const prepared = snapshotManagedAnalyticsDeclarativeInput<TestDestinationV1>(input); if (!prepared.ok) return prepared;
|
|
613
|
+
const v = new Violations(); const obj = object(prepared.value, v, ""); if (!obj) return { ok: false, violations: v.items };
|
|
614
|
+
rejectKeys(obj, ["schemaVersion", "destinationId", "environment", "createdAt"], v, "");
|
|
615
|
+
literal(obj, "schemaVersion", 1, v, ""); opaqueId(obj, "destinationId", v, ""); enumeration(obj, "environment", TEST_DESTINATION_ENVIRONMENTS, v, ""); timestamp(obj, "createdAt", v, "");
|
|
616
|
+
const safe = validateManagedAnalyticsSafePayload(obj); if (!safe.ok) v.items.push(...safe.violations);
|
|
617
|
+
return v.ok ? prepared : { ok: false, violations: v.items };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export function validateTestDestinationDeliveryV1(input: unknown): ContractResult<TestDestinationDeliveryV1> {
|
|
621
|
+
const prepared = snapshotManagedAnalyticsDeclarativeInput<TestDestinationDeliveryV1>(input); if (!prepared.ok) return prepared;
|
|
622
|
+
const v = new Violations(); const obj = object(prepared.value, v, ""); if (!obj) return { ok: false, violations: v.items };
|
|
623
|
+
rejectKeys(obj, ["schemaVersion", "destinationId", "eventId", "actionName", "deliveredAt"], v, "");
|
|
624
|
+
literal(obj, "schemaVersion", 1, v, ""); opaqueId(obj, "destinationId", v, ""); opaqueId(obj, "eventId", v, "");
|
|
625
|
+
enumeration(obj, "actionName", APPROVED_MARKETING_ACTION_NAMES, v, "");
|
|
626
|
+
timestamp(obj, "deliveredAt", v, "");
|
|
627
|
+
const safe = validateManagedAnalyticsSafePayload(obj); if (!safe.ok) v.items.push(...safe.violations);
|
|
628
|
+
return v.ok ? prepared : { ok: false, violations: v.items };
|
|
629
|
+
}
|
|
630
|
+
|
|
560
631
|
export function validateMigrationDraftV1(input: unknown): ContractResult<MigrationDraftV1> {
|
|
561
632
|
const prepared = snapshotManagedAnalyticsDeclarativeInput<MigrationDraftV1>(input); if (!prepared.ok) return prepared;
|
|
562
633
|
const v = new Violations(); const obj = object(prepared.value, v, ""); if (!obj) return { ok: false, violations: v.items };
|
|
@@ -649,6 +720,13 @@ const validateSetIntegrationInstallStatusBody: ObjectShapeValidator = (obj, v, p
|
|
|
649
720
|
rejectKeys(obj, ["installId", "status"], v, path); opaqueId(obj, "installId", v, path); enumeration(obj, "status", INTEGRATION_INSTALL_STATUSES, v, path);
|
|
650
721
|
};
|
|
651
722
|
const validateDeleteIntegrationInstallBody: ObjectShapeValidator = (obj, v, path) => { rejectKeys(obj, ["installId"], v, path); opaqueId(obj, "installId", v, path); };
|
|
723
|
+
const validateRegisterTestDestinationBody: ObjectShapeValidator = (obj, v, path) => {
|
|
724
|
+
rejectKeys(obj, ["destinationId", "environment"], v, path); opaqueId(obj, "destinationId", v, path); enumeration(obj, "environment", TEST_DESTINATION_ENVIRONMENTS, v, path);
|
|
725
|
+
};
|
|
726
|
+
const validateDeliverTestEventBody: ObjectShapeValidator = (obj, v, path) => {
|
|
727
|
+
rejectKeys(obj, ["destinationId", "eventId", "actionName"], v, path); opaqueId(obj, "destinationId", v, path); opaqueId(obj, "eventId", v, path); enumeration(obj, "actionName", APPROVED_MARKETING_ACTION_NAMES, v, path);
|
|
728
|
+
};
|
|
729
|
+
const validateTestDestinationDeliveryQuery: ObjectShapeValidator = (obj, v, path) => { rejectKeys(obj, ["destinationId", "eventId"], v, path); opaqueId(obj, "destinationId", v, path); opaqueId(obj, "eventId", v, path); };
|
|
652
730
|
|
|
653
731
|
export function validateCreateMigrationDraftRequest(input: unknown): ContractResult<CreateMigrationDraftRequest> { return validateMutationOperation(input, validateCreateMigrationDraftBody); }
|
|
654
732
|
export function validateUpdateMigrationDraftRequest(input: unknown): ContractResult<UpdateMigrationDraftRequest> { return validateMutationOperation(input, validateUpdateMigrationDraftBody); }
|
|
@@ -657,6 +735,9 @@ export function validateCreateIntegrationInstallRequest(input: unknown): Contrac
|
|
|
657
735
|
export function validateReviseIntegrationInstallRequest(input: unknown): ContractResult<ReviseIntegrationInstallRequest> { return validateMutationOperation(input, validateReviseIntegrationInstallBody); }
|
|
658
736
|
export function validateSetIntegrationInstallStatusRequest(input: unknown): ContractResult<SetIntegrationInstallStatusRequest> { return validateMutationOperation(input, validateSetIntegrationInstallStatusBody); }
|
|
659
737
|
export function validateDeleteIntegrationInstallRequest(input: unknown): ContractResult<DeleteIntegrationInstallRequest> { return validateMutationOperation(input, validateDeleteIntegrationInstallBody); }
|
|
738
|
+
export function validateRegisterTestDestinationRequest(input: unknown): ContractResult<RegisterTestDestinationRequest> { return validateMutationOperation(input, validateRegisterTestDestinationBody); }
|
|
739
|
+
export function validateDeliverTestEventRequest(input: unknown): ContractResult<DeliverTestEventRequest> { return validateMutationOperation(input, validateDeliverTestEventBody); }
|
|
740
|
+
export function validateGetTestDestinationDeliveryRequest(input: unknown): ContractResult<GetTestDestinationDeliveryRequest> { return validateReadOperation(input, validateTestDestinationDeliveryQuery); }
|
|
660
741
|
|
|
661
742
|
/** Validate the closed, public-safe error shape before it crosses the API boundary. */
|
|
662
743
|
export function validateApiErrorEnvelope(input: unknown): ContractResult<ApiErrorEnvelope> {
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
renderBrowserEnvironmentScript,
|
|
37
37
|
serializeForInlineScript,
|
|
38
38
|
} from "./environment.js";
|
|
39
|
-
import { defaultConsentSnapshot } from "./consent-broker.js";
|
|
39
|
+
import { defaultConsentSnapshot, type ConsentSnapshot } from "./consent-broker.js";
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* The sentinel a renderer stamps in place of a live CSP nonce. It is a valid
|
|
@@ -138,11 +138,16 @@ export function phaseBoundary(phase: ReservedPhase): string {
|
|
|
138
138
|
* splice regions both document mounts consume. `nonce` is the value stamped onto
|
|
139
139
|
* the emitted inline scripts — pass {@link INTEGRATION_NONCE_PLACEHOLDER} so the
|
|
140
140
|
* renderer can stamp a fresh nonce per response; an empty string omits the
|
|
141
|
-
* attribute (for tests / mounts that stamp uniformly downstream).
|
|
141
|
+
* attribute (for tests / mounts that stamp uniformly downstream). `consentSnapshot`
|
|
142
|
+
* is the tenant's actual initial consent state — the snapshot the ext-policy-03
|
|
143
|
+
* broker computes from its {@link TenantConsentConfig}, seeded before any CMP/banner
|
|
144
|
+
* interaction; it defaults to {@link defaultConsentSnapshot} (essential-only, fail
|
|
145
|
+
* closed) for callers with no tenant consent config to resolve.
|
|
142
146
|
*/
|
|
143
147
|
export function renderIntegrationPhases(
|
|
144
148
|
plan: EffectivePlan,
|
|
145
149
|
nonce: string = INTEGRATION_NONCE_PLACEHOLDER,
|
|
150
|
+
consentSnapshot: ConsentSnapshot = defaultConsentSnapshot(),
|
|
146
151
|
): IntegrationPhaseMarkup {
|
|
147
152
|
assertRenderablePlan(plan);
|
|
148
153
|
|
|
@@ -150,7 +155,7 @@ export function renderIntegrationPhases(
|
|
|
150
155
|
phaseBoundary("platform.environment") +
|
|
151
156
|
renderBrowserEnvironmentScript(plan.environment, nonce) +
|
|
152
157
|
phaseBoundary("consent.defaults") +
|
|
153
|
-
inlineFrozenGlobal(INTEGRATION_CONSENT_GLOBAL,
|
|
158
|
+
inlineFrozenGlobal(INTEGRATION_CONSENT_GLOBAL, consentSnapshot, nonce) +
|
|
154
159
|
phaseBoundary("head.early") +
|
|
155
160
|
inlineFrozenGlobal(INTEGRATION_PLAN_GLOBAL, bootDescriptor(plan), nonce);
|
|
156
161
|
|