@tokenoftrust/storefront-runner 2.0.16 → 2.0.18
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 +18 -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/apps/routeParams.ts +45 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +25 -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/env.ts +13 -111
- package/apps/storefront/src/lib/fixtures/regulated-commerce-truth/index.ts +540 -0
- package/apps/storefront/src/lib/fixtures/ws3-readonly-clone-preflight/index.ts +495 -0
- 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/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 +270 -0
- package/apps/storefront/src/middleware/index.ts +100 -796
- package/apps/storefront/src/middleware/publicRender.ts +43 -0
- package/apps/storefront/src/middleware/renderChain.ts +685 -0
- package/apps/storefront/src/middleware/routeBranch.ts +160 -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/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 +7 -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 +203 -0
- package/packages/public-runtime/src/publication-space.ts +853 -0
- package/packages/public-runtime/src/publication-version.ts +587 -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/conformance/fake-destination.ts +402 -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
|
@@ -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
|
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { PublicationSpaceManifestV1 } from "./publication-space.js";
|
|
2
|
+
import { sha256Hex } from "./hash.js";
|
|
3
|
+
|
|
4
|
+
/** Renderer-safe description of one immutable publication artifact file. */
|
|
5
|
+
export interface PublicationArtifactFileV1 {
|
|
6
|
+
lane: "content" | "public-asset" | "authorized-asset";
|
|
7
|
+
path: string;
|
|
8
|
+
encoding: "utf8" | "base64";
|
|
9
|
+
content: string;
|
|
10
|
+
byteSize: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Renderer-safe marker for one complete tenant+space+version artifact set. */
|
|
14
|
+
export interface PublicationArtifactBundleV1 {
|
|
15
|
+
tenant: string;
|
|
16
|
+
spaceId: string;
|
|
17
|
+
versionId: string;
|
|
18
|
+
sourceSha: string;
|
|
19
|
+
artifactDigest: string;
|
|
20
|
+
sourceLock: string;
|
|
21
|
+
visibility: PublicationSpaceManifestV1["visibility"];
|
|
22
|
+
assetNamespace: string;
|
|
23
|
+
assetAccess: PublicationSpaceManifestV1["assets"]["access"];
|
|
24
|
+
files: PublicationArtifactFileV1[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface PublicationArtifactBundleExpectationV1 {
|
|
28
|
+
tenant: string;
|
|
29
|
+
spaceId: string;
|
|
30
|
+
versionId: string;
|
|
31
|
+
sourceSha: string;
|
|
32
|
+
artifactDigest: string;
|
|
33
|
+
forge: string;
|
|
34
|
+
repo: string;
|
|
35
|
+
visibility?: PublicationArtifactBundleV1["visibility"];
|
|
36
|
+
assetNamespace?: string;
|
|
37
|
+
assetAccess?: PublicationArtifactBundleV1["assetAccess"];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Locale-independent canonical order for immutable publication files. */
|
|
41
|
+
export function comparePublicationArtifactFilesV1(
|
|
42
|
+
left: PublicationArtifactFileV1,
|
|
43
|
+
right: PublicationArtifactFileV1,
|
|
44
|
+
): number {
|
|
45
|
+
const leftIdentity = `${left.lane}\0${left.path}`;
|
|
46
|
+
const rightIdentity = `${right.lane}\0${right.path}`;
|
|
47
|
+
return leftIdentity < rightIdentity ? -1 : leftIdentity > rightIdentity ? 1 : 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function safeArtifactPath(path: string): boolean {
|
|
51
|
+
return !!path && !path.startsWith("/") && !path.includes("\\") && !path.includes("\0") &&
|
|
52
|
+
path.split("/").every((part) => !!part && part !== "." && part !== "..");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function hasExactKeys(value: object, keys: readonly string[]): boolean {
|
|
56
|
+
const actual = Object.keys(value).sort();
|
|
57
|
+
const expected = [...keys].sort();
|
|
58
|
+
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function decodePublicationArtifactFileV1(file: PublicationArtifactFileV1): Uint8Array | null {
|
|
62
|
+
try {
|
|
63
|
+
if (file.encoding === "utf8") return new TextEncoder().encode(file.content);
|
|
64
|
+
if (file.encoding !== "base64" ||
|
|
65
|
+
!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(file.content)) return null;
|
|
66
|
+
const binary = atob(file.content);
|
|
67
|
+
if (btoa(binary) !== file.content) return null;
|
|
68
|
+
return Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function validPublicationArtifactFileV1(value: unknown): value is PublicationArtifactFileV1 {
|
|
75
|
+
if (!value || typeof value !== "object" || !hasExactKeys(value,
|
|
76
|
+
["lane", "path", "encoding", "content", "byteSize"])) return false;
|
|
77
|
+
const file = value as PublicationArtifactFileV1;
|
|
78
|
+
return ["content", "public-asset", "authorized-asset"].includes(file.lane) &&
|
|
79
|
+
["utf8", "base64"].includes(file.encoding) && typeof file.path === "string" &&
|
|
80
|
+
safeArtifactPath(file.path) && typeof file.content === "string" &&
|
|
81
|
+
Number.isSafeInteger(file.byteSize) && file.byteSize >= 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Decode one canonical portable artifact file while enforcing its declared byte length. */
|
|
85
|
+
export function publicationArtifactFileBytesV1(file: PublicationArtifactFileV1): Uint8Array | null {
|
|
86
|
+
if (!validPublicationArtifactFileV1(file)) return null;
|
|
87
|
+
try {
|
|
88
|
+
const decoded = decodePublicationArtifactFileV1(file);
|
|
89
|
+
if (!decoded) return null;
|
|
90
|
+
return decoded.byteLength === file.byteSize ? decoded : null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Canonical marker admission shared by the writer-side activation gate and the
|
|
98
|
+
* renderer. It validates shape, identity, source provenance/lock, and recomputes
|
|
99
|
+
* the complete file-list digest; a marker cannot define its own admission facts.
|
|
100
|
+
*/
|
|
101
|
+
export async function validatePublicationArtifactBundleV1(
|
|
102
|
+
value: unknown,
|
|
103
|
+
expected: PublicationArtifactBundleExpectationV1,
|
|
104
|
+
): Promise<{ ok: true; value: PublicationArtifactBundleV1 } | { ok: false }> {
|
|
105
|
+
if (!value || typeof value !== "object") return { ok: false };
|
|
106
|
+
const bundle = value as PublicationArtifactBundleV1;
|
|
107
|
+
if (!hasExactKeys(bundle, ["tenant", "spaceId", "versionId", "sourceSha", "artifactDigest",
|
|
108
|
+
"sourceLock", "visibility", "assetNamespace", "assetAccess", "files"]) ||
|
|
109
|
+
bundle.tenant !== expected.tenant || bundle.spaceId !== expected.spaceId ||
|
|
110
|
+
bundle.versionId !== expected.versionId || bundle.sourceSha !== expected.sourceSha ||
|
|
111
|
+
!/^[0-9a-f]{40}$/.test(bundle.sourceSha) || bundle.artifactDigest !== expected.artifactDigest ||
|
|
112
|
+
!/^[0-9a-f]{64}$/.test(bundle.artifactDigest) || typeof bundle.sourceLock !== "string" ||
|
|
113
|
+
!/^[0-9a-f]{64}$/.test(bundle.sourceLock) ||
|
|
114
|
+
!["public", "internal"].includes(bundle.visibility) || typeof bundle.assetNamespace !== "string" ||
|
|
115
|
+
!bundle.assetNamespace ||
|
|
116
|
+
!["public", "authorized"].includes(bundle.assetAccess) || !Array.isArray(bundle.files) ||
|
|
117
|
+
(expected.visibility !== undefined && bundle.visibility !== expected.visibility) ||
|
|
118
|
+
(expected.assetNamespace !== undefined && bundle.assetNamespace !== expected.assetNamespace) ||
|
|
119
|
+
(expected.assetAccess !== undefined && bundle.assetAccess !== expected.assetAccess)) return { ok: false };
|
|
120
|
+
const paths = new Set<string>();
|
|
121
|
+
for (const file of bundle.files) {
|
|
122
|
+
if (!validPublicationArtifactFileV1(file) || !publicationArtifactFileBytesV1(file)) {
|
|
123
|
+
return { ok: false };
|
|
124
|
+
}
|
|
125
|
+
if ((file.lane === "public-asset" && bundle.assetAccess !== "public") ||
|
|
126
|
+
(file.lane === "authorized-asset" && bundle.assetAccess !== "authorized")) return { ok: false };
|
|
127
|
+
if (paths.has(file.path)) return { ok: false };
|
|
128
|
+
paths.add(file.path);
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const digest = await publicationArtifactDigestV1(bundle.files);
|
|
132
|
+
if (digest !== expected.artifactDigest) return { ok: false };
|
|
133
|
+
const sourceLock = await publicationArtifactSourceLockV1({
|
|
134
|
+
tenant: expected.tenant,
|
|
135
|
+
spaceId: expected.spaceId,
|
|
136
|
+
forge: expected.forge,
|
|
137
|
+
repo: expected.repo,
|
|
138
|
+
sourceSha: expected.sourceSha,
|
|
139
|
+
versionId: expected.versionId,
|
|
140
|
+
artifactDigest: digest,
|
|
141
|
+
});
|
|
142
|
+
return sourceLock === bundle.sourceLock ? { ok: true, value: bundle } : { ok: false };
|
|
143
|
+
} catch {
|
|
144
|
+
return { ok: false };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Canonical content digest shared by the trusted writer and renderer readback. */
|
|
149
|
+
export async function publicationArtifactDigestV1(
|
|
150
|
+
files: readonly PublicationArtifactFileV1[],
|
|
151
|
+
): Promise<string> {
|
|
152
|
+
if (!Array.isArray(files)) throw new Error("publication artifact file list is malformed");
|
|
153
|
+
const ordered = [...files].sort(comparePublicationArtifactFilesV1);
|
|
154
|
+
const paths = new Set<string>();
|
|
155
|
+
const rows = await Promise.all(ordered.map(async (file) => {
|
|
156
|
+
if (!validPublicationArtifactFileV1(file) || paths.has(file.path)) {
|
|
157
|
+
throw new Error("publication artifact file is malformed or duplicated");
|
|
158
|
+
}
|
|
159
|
+
paths.add(file.path);
|
|
160
|
+
const bytes = publicationArtifactFileBytesV1(file);
|
|
161
|
+
if (!bytes) throw new Error("publication artifact file is malformed");
|
|
162
|
+
return `${file.lane}\0${file.path}\0${file.encoding}\0${file.byteSize}\0${await sha256Hex(bytes)}`;
|
|
163
|
+
}));
|
|
164
|
+
return sha256Hex(rows.join("\n"));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Canonical source lock binds one immutable artifact to its configured source. */
|
|
168
|
+
export function publicationArtifactSourceLockV1(input: {
|
|
169
|
+
tenant: string;
|
|
170
|
+
spaceId: string;
|
|
171
|
+
forge: string;
|
|
172
|
+
repo: string;
|
|
173
|
+
sourceSha: string;
|
|
174
|
+
versionId: string;
|
|
175
|
+
artifactDigest: string;
|
|
176
|
+
}): Promise<string> {
|
|
177
|
+
return sha256Hex([
|
|
178
|
+
input.tenant, input.spaceId, input.forge, input.repo,
|
|
179
|
+
input.sourceSha, input.versionId, input.artifactDigest,
|
|
180
|
+
].join("\0"));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Physical immutable namespace. Logical artifact paths are always relative. */
|
|
184
|
+
export function publicationArtifactKeyV1(
|
|
185
|
+
tenant: string,
|
|
186
|
+
spaceId: string,
|
|
187
|
+
versionId: string,
|
|
188
|
+
lane: PublicationArtifactFileV1["lane"],
|
|
189
|
+
relativePath: string,
|
|
190
|
+
): string {
|
|
191
|
+
const identifier = /^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$/;
|
|
192
|
+
const version = /^pv_[a-z0-9](?:[a-z0-9._:-]{4,122}[a-z0-9])$/;
|
|
193
|
+
if (!identifier.test(tenant) || !identifier.test(spaceId) || !version.test(versionId)) {
|
|
194
|
+
throw new Error("publication artifact identity is unsafe");
|
|
195
|
+
}
|
|
196
|
+
if (!["content", "public-asset", "authorized-asset"].includes(lane) ||
|
|
197
|
+
!relativePath || relativePath.startsWith("/") || relativePath.includes("\\") ||
|
|
198
|
+
relativePath.includes("\0") ||
|
|
199
|
+
relativePath.split("/").some((part) => !part || part === "." || part === "..")) {
|
|
200
|
+
throw new Error("publication artifact path is unsafe");
|
|
201
|
+
}
|
|
202
|
+
return `publications/${tenant}/${spaceId}/${versionId}/${lane}/${relativePath}`;
|
|
203
|
+
}
|