@tokenoftrust/storefront-runner 2.0.17 → 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.
Files changed (86) hide show
  1. package/apps/storefront/env.d.ts +18 -0
  2. package/apps/storefront/migrations-apps/0012_test_destinations.sql +29 -0
  3. package/apps/storefront/src/components/blog/ReactionBar.astro +1 -1
  4. package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +365 -49
  5. package/apps/storefront/src/components/islands/checkoutVerificationFlow.ts +251 -0
  6. package/apps/storefront/src/config/compliance/rulesets.ts +18 -7
  7. package/apps/storefront/src/config/devTenantSeed.ts +24 -1
  8. package/apps/storefront/src/config/evidenceTenantRoster.ts +25 -0
  9. package/apps/storefront/src/config/publicationSpaceConfig.ts +25 -0
  10. package/apps/storefront/src/config/resolver.ts +5 -0
  11. package/apps/storefront/src/config/tenantAnalyticsFiles.ts +43 -0
  12. package/apps/storefront/src/config/tenants.ts +11 -0
  13. package/apps/storefront/src/layouts/Layout.astro +5 -1
  14. package/apps/storefront/src/lib/activity/killSwitch.ts +1 -1
  15. package/apps/storefront/src/lib/admin/consentSettingsClient.ts +113 -0
  16. package/apps/storefront/src/lib/apps/consentConfigRead.ts +24 -0
  17. package/apps/storefront/src/lib/apps/gtmMigrationDraft.ts +103 -0
  18. package/apps/storefront/src/lib/auth/loginGate.ts +25 -0
  19. package/apps/storefront/src/lib/checkoutCommerce.ts +1 -1
  20. package/apps/storefront/src/lib/compliance/enforcement.ts +25 -3
  21. package/apps/storefront/src/lib/d1/schema-apps.ts +44 -0
  22. package/apps/storefront/src/lib/env.ts +13 -111
  23. package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +198 -0
  24. package/apps/storefront/src/lib/monitoring/manifest.ts +1 -1
  25. package/apps/storefront/src/lib/rawMarketingHtml.ts +9 -1
  26. package/apps/storefront/src/lib/runtimeEnv.ts +124 -0
  27. package/apps/storefront/src/lib/verification/totVerificationClient.ts +485 -0
  28. package/apps/storefront/src/middleware/candidatePin.ts +35 -0
  29. package/apps/storefront/src/middleware/controlPlane.ts +270 -0
  30. package/apps/storefront/src/middleware/index.ts +100 -796
  31. package/apps/storefront/src/middleware/publicRender.ts +43 -0
  32. package/apps/storefront/src/middleware/renderChain.ts +685 -0
  33. package/apps/storefront/src/middleware/routeBranch.ts +160 -0
  34. package/apps/storefront/src/middleware/staticBundle.ts +109 -0
  35. package/apps/storefront/src/pages/[...slug].astro +1 -0
  36. package/apps/storefront/src/pages/api/compliance/preflight.ts +179 -2
  37. package/apps/storefront/src/pages/index.astro +1 -0
  38. package/apps/storefront/vitest.config.ts +5 -0
  39. package/package.json +1 -1
  40. package/packages/public-runtime/package.json +1 -0
  41. package/packages/public-runtime/schemas/managed-analytics/v1/deliver-test-event-request.schema.json +122 -0
  42. package/packages/public-runtime/schemas/managed-analytics/v1/get-test-destination-delivery-request.schema.json +79 -0
  43. package/packages/public-runtime/schemas/managed-analytics/v1/register-test-destination-request.schema.json +93 -0
  44. package/packages/public-runtime/schemas/managed-analytics/v1/test-destination-delivery.schema.json +106 -0
  45. package/packages/public-runtime/schemas/managed-analytics/v1/test-destination.schema.json +77 -0
  46. package/packages/public-runtime/schemas/publication-space/v1/manifest.schema.json +195 -0
  47. package/packages/public-runtime/schemas/publication-space/v1/normalized-mount.schema.json +80 -0
  48. package/packages/public-runtime/schemas/publication-space/v1/ref.schema.json +20 -0
  49. package/packages/public-runtime/schemas/publication-version/v1/pointer.schema.json +28 -0
  50. package/packages/public-runtime/schemas/publication-version/v1/transition-request.schema.json +44 -0
  51. package/packages/public-runtime/schemas/publication-version/v1/transition.schema.json +47 -0
  52. package/packages/public-runtime/schemas/publication-version/v1/version.schema.json +58 -0
  53. package/packages/public-runtime/src/activity/catalog.ts +1 -1
  54. package/packages/public-runtime/src/candidate-index.ts +6 -0
  55. package/packages/public-runtime/src/customization-reconcile.ts +8 -0
  56. package/packages/public-runtime/src/html-cache-policy.ts +57 -0
  57. package/packages/public-runtime/src/index.ts +7 -0
  58. package/packages/public-runtime/src/integration/consent-broker.ts +115 -0
  59. package/packages/public-runtime/src/integration/interim-bridge.ts +111 -27
  60. package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +65 -0
  61. package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +81 -0
  62. package/packages/public-runtime/src/integration/render-phases.ts +8 -3
  63. package/packages/public-runtime/src/publication-artifact.ts +203 -0
  64. package/packages/public-runtime/src/publication-space.ts +853 -0
  65. package/packages/public-runtime/src/publication-version.ts +587 -0
  66. package/packages/public-runtime/src/route-tier.ts +257 -0
  67. package/packages/public-runtime/src/static-bundle.ts +18 -5
  68. package/packages/public-runtime/src/tenant-assets.ts +85 -17
  69. package/packages/public-runtime/src/tenant.ts +70 -0
  70. package/packages/public-runtime/tests/fixtures/publication-spaces/internal.json +32 -0
  71. package/packages/public-runtime/tests/fixtures/publication-spaces/mounted-public.json +38 -0
  72. package/packages/public-runtime/tests/fixtures/publication-spaces/normalized-mounts.json +39 -0
  73. package/packages/public-runtime/tests/fixtures/publication-spaces/ref.json +5 -0
  74. package/packages/public-runtime/tests/fixtures/publication-spaces/root-public.json +31 -0
  75. package/packages/public-runtime/tests/fixtures/publication-versions/articles-v1.json +20 -0
  76. package/packages/public-runtime/tests/fixtures/publication-versions/articles-v2.json +20 -0
  77. package/packages/public-runtime/tests/fixtures/publication-versions/internal-v1.json +20 -0
  78. package/packages/public-runtime/tests/fixtures/publication-versions/live-pointer.json +11 -0
  79. package/packages/public-runtime/tests/fixtures/publication-versions/live-transition-request.json +14 -0
  80. package/packages/public-runtime/tests/fixtures/publication-versions/live-transition.json +17 -0
  81. package/packages/public-runtime/tests/fixtures/publication-versions/root-v1.json +20 -0
  82. package/packages/public-runtime/tests/fixtures/publication-versions/root-v2.json +20 -0
  83. package/scripts/dev/ai-edit.mjs +16 -3
  84. package/scripts/dev/port-check.mjs +18 -3
  85. package/scripts/dev/publish.mjs +26 -3
  86. package/scripts/tenant/validate.mjs +14 -1
@@ -0,0 +1,20 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://tokenoftrust.com/schemas/publication-space/v1/ref.schema.json",
4
+ "title": "PublicationSpaceRefV1",
5
+ "type": "object",
6
+ "patternProperties": { "^\\$(?:schema|comment|id)$": {} },
7
+ "additionalProperties": false,
8
+ "required": ["version", "tenant", "spaceId"],
9
+ "properties": {
10
+ "version": { "const": 1 },
11
+ "tenant": {
12
+ "type": "string",
13
+ "pattern": "^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$"
14
+ },
15
+ "spaceId": {
16
+ "type": "string",
17
+ "pattern": "^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$"
18
+ }
19
+ }
20
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://tokenoftrust.com/schemas/publication-version/v1/pointer.schema.json",
4
+ "title": "PublicationChannelPointerV1",
5
+ "type": "object",
6
+ "patternProperties": { "^\\$(?:schema|comment|id)$": {} },
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "tenant", "spaceId", "channel", "revision", "versionId", "sourceSha", "artifactDigest", "updatedAt"],
9
+ "properties": {
10
+ "schemaVersion": { "const": 1 },
11
+ "tenant": { "$ref": "#/$defs/identifier" },
12
+ "spaceId": { "$ref": "#/$defs/identifier" },
13
+ "channel": { "enum": ["preview", "live"] },
14
+ "revision": { "type": "integer", "minimum": 1 },
15
+ "versionId": { "$ref": "#/$defs/versionId" },
16
+ "sourceSha": { "$ref": "#/$defs/sha40" },
17
+ "artifactDigest": { "$ref": "#/$defs/sha256" },
18
+ "updatedAt": { "$ref": "#/$defs/instant" }
19
+ },
20
+ "$defs": {
21
+ "identifier": { "type": "string", "pattern": "^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$" },
22
+ "opaqueId": { "type": "string", "pattern": "^[a-z0-9](?:[a-z0-9._:-]{6,126}[a-z0-9])$" },
23
+ "versionId": { "type": "string", "pattern": "^pv_[a-z0-9](?:[a-z0-9._:-]{4,122}[a-z0-9])$" },
24
+ "sha40": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
25
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
26
+ "instant": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$" }
27
+ }
28
+ }
@@ -0,0 +1,44 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://tokenoftrust.com/schemas/publication-version/v1/transition-request.schema.json",
4
+ "title": "PublicationTransitionRequestV1",
5
+ "type": "object",
6
+ "patternProperties": { "^\\$(?:schema|comment|id)$": {} },
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "schemaVersion",
10
+ "transitionId",
11
+ "operation",
12
+ "tenant",
13
+ "spaceId",
14
+ "channel",
15
+ "targetVersionId",
16
+ "expectedSourceSha",
17
+ "expectedArtifactDigest",
18
+ "expectedRevision",
19
+ "principal",
20
+ "occurredAt"
21
+ ],
22
+ "properties": {
23
+ "schemaVersion": { "const": 1 },
24
+ "transitionId": { "$ref": "#/$defs/opaqueId" },
25
+ "operation": { "enum": ["activate", "rollback"] },
26
+ "tenant": { "$ref": "#/$defs/identifier" },
27
+ "spaceId": { "$ref": "#/$defs/identifier" },
28
+ "channel": { "enum": ["preview", "live"] },
29
+ "targetVersionId": { "$ref": "#/$defs/versionId" },
30
+ "expectedSourceSha": { "$ref": "#/$defs/sha40" },
31
+ "expectedArtifactDigest": { "$ref": "#/$defs/sha256" },
32
+ "expectedRevision": { "type": "integer", "minimum": 0 },
33
+ "principal": { "$ref": "#/$defs/opaqueId" },
34
+ "occurredAt": { "$ref": "#/$defs/instant" }
35
+ },
36
+ "$defs": {
37
+ "identifier": { "type": "string", "pattern": "^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$" },
38
+ "opaqueId": { "type": "string", "pattern": "^[a-z0-9](?:[a-z0-9._:-]{6,126}[a-z0-9])$" },
39
+ "versionId": { "type": "string", "pattern": "^pv_[a-z0-9](?:[a-z0-9._:-]{4,122}[a-z0-9])$" },
40
+ "sha40": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
41
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
42
+ "instant": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$" }
43
+ }
44
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://tokenoftrust.com/schemas/publication-version/v1/transition.schema.json",
4
+ "title": "PublicationPointerTransitionV1",
5
+ "type": "object",
6
+ "patternProperties": { "^\\$(?:schema|comment|id)$": {} },
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "transitionId", "operation", "tenant", "spaceId", "channel", "previous", "next", "principal", "occurredAt"],
9
+ "properties": {
10
+ "schemaVersion": { "const": 1 },
11
+ "transitionId": { "$ref": "#/$defs/opaqueId" },
12
+ "operation": { "enum": ["activate", "rollback"] },
13
+ "tenant": { "$ref": "#/$defs/identifier" },
14
+ "spaceId": { "$ref": "#/$defs/identifier" },
15
+ "channel": { "enum": ["preview", "live"] },
16
+ "previous": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/snapshot" }] },
17
+ "next": { "$ref": "#/$defs/snapshot" },
18
+ "principal": { "$ref": "#/$defs/opaqueId" },
19
+ "occurredAt": { "$ref": "#/$defs/instant" }
20
+ },
21
+ "allOf": [
22
+ {
23
+ "if": { "properties": { "operation": { "const": "rollback" } }, "required": ["operation"] },
24
+ "then": { "properties": { "previous": { "$ref": "#/$defs/snapshot" } } }
25
+ }
26
+ ],
27
+ "$defs": {
28
+ "identifier": { "type": "string", "pattern": "^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$" },
29
+ "opaqueId": { "type": "string", "pattern": "^[a-z0-9](?:[a-z0-9._:-]{6,126}[a-z0-9])$" },
30
+ "versionId": { "type": "string", "pattern": "^pv_[a-z0-9](?:[a-z0-9._:-]{4,122}[a-z0-9])$" },
31
+ "sha40": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
32
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
33
+ "instant": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$" },
34
+ "snapshot": {
35
+ "type": "object",
36
+ "patternProperties": { "^\\$(?:schema|comment|id)$": {} },
37
+ "additionalProperties": false,
38
+ "required": ["revision", "versionId", "sourceSha", "artifactDigest"],
39
+ "properties": {
40
+ "revision": { "type": "integer", "minimum": 1 },
41
+ "versionId": { "$ref": "#/$defs/versionId" },
42
+ "sourceSha": { "$ref": "#/$defs/sha40" },
43
+ "artifactDigest": { "$ref": "#/$defs/sha256" }
44
+ }
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://tokenoftrust.com/schemas/publication-version/v1/version.schema.json",
4
+ "title": "PublicationVersionV1",
5
+ "type": "object",
6
+ "patternProperties": { "^\\$(?:schema|comment|id)$": {} },
7
+ "additionalProperties": false,
8
+ "required": ["schemaVersion", "versionId", "tenant", "spaceId", "source", "renderer", "artifactDigest", "createdAt", "metadata"],
9
+ "properties": {
10
+ "schemaVersion": { "const": 1 },
11
+ "versionId": { "$ref": "#/$defs/versionId" },
12
+ "tenant": { "$ref": "#/$defs/identifier" },
13
+ "spaceId": { "$ref": "#/$defs/identifier" },
14
+ "source": {
15
+ "type": "object",
16
+ "patternProperties": { "^\\$(?:schema|comment|id)$": {} },
17
+ "additionalProperties": false,
18
+ "required": ["forge", "repo", "commitSha"],
19
+ "properties": {
20
+ "forge": { "enum": ["gitea", "github", "fixture"] },
21
+ "repo": {
22
+ "type": "string",
23
+ "minLength": 1,
24
+ "maxLength": 256,
25
+ "pattern": "^(?!.*\\.\\.)(?!.*//)[A-Za-z0-9](?:[A-Za-z0-9._/-]{0,254}[A-Za-z0-9])?$"
26
+ },
27
+ "commitSha": { "$ref": "#/$defs/sha40" }
28
+ }
29
+ },
30
+ "renderer": {
31
+ "type": "object",
32
+ "patternProperties": { "^\\$(?:schema|comment|id)$": {} },
33
+ "additionalProperties": false,
34
+ "required": ["contractVersion", "themeContractVersion"],
35
+ "properties": {
36
+ "contractVersion": { "type": "string", "minLength": 1 },
37
+ "themeContractVersion": { "type": "string", "minLength": 1 }
38
+ }
39
+ },
40
+ "artifactDigest": { "$ref": "#/$defs/sha256" },
41
+ "createdAt": { "$ref": "#/$defs/instant" },
42
+ "metadata": {
43
+ "type": "object",
44
+ "patternProperties": { "^\\$(?:schema|comment|id)$": {} },
45
+ "additionalProperties": false,
46
+ "required": ["buildId"],
47
+ "properties": { "buildId": { "$ref": "#/$defs/opaqueId" } }
48
+ }
49
+ },
50
+ "$defs": {
51
+ "identifier": { "type": "string", "pattern": "^[a-z0-9](?:[a-z0-9._-]{0,126}[a-z0-9])?$" },
52
+ "opaqueId": { "type": "string", "pattern": "^[a-z0-9](?:[a-z0-9._:-]{6,126}[a-z0-9])$" },
53
+ "versionId": { "type": "string", "pattern": "^pv_[a-z0-9](?:[a-z0-9._:-]{4,122}[a-z0-9])$" },
54
+ "sha40": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
55
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
56
+ "instant": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$" }
57
+ }
58
+ }
@@ -98,7 +98,7 @@ export const ACTION_CATALOG = {
98
98
  },
99
99
  "cli.command.result": {
100
100
  actors: ["dev", "agent"], source: "cli", sinks: HIGH_VOLUME,
101
- argsAllow: ["command", "subcommand", "cliVersion", "exitCode", "durationMs"], renderedAllow: [],
101
+ argsAllow: ["command", "subcommand", "cliVersion", "exitCode", "durationMs", "diagnosticId"], renderedAllow: [],
102
102
  desc: "A `tot` CLI command finished (exit code + duration).",
103
103
  },
104
104
  "cli.obstacle.reported": {
@@ -122,6 +122,12 @@ export interface ReviewEnvironment {
122
122
  * surfaced in the PR review console. Absent/empty ⇒ validated clean.
123
123
  */
124
124
  findings?: PreviewStateFinding[];
125
+ /**
126
+ * Artifact paths this candidate's reconcile DROPPED from the version it published.
127
+ * Non-empty on a `ready` candidate means the preview renders the tenant's content
128
+ * MINUS these files; `lastError` names them in prose. Absent/empty ⇒ published clean.
129
+ */
130
+ droppedArtifacts?: string[];
125
131
  /** Auditable safety-block override applied to this candidate (who/why), if any. */
126
132
  overriddenBy?: PreviewStateOverride | null;
127
133
  /**
@@ -76,6 +76,14 @@ export interface PreviewState {
76
76
  * Absent/empty ⇒ the tenant's files validated clean. Written by `reconcilePreview`.
77
77
  */
78
78
  findings?: PreviewStateFinding[];
79
+ /**
80
+ * Artifact paths the last reconcile DROPPED from the version it published. A
81
+ * non-empty list on a SUCCESSFUL reconcile means the site is serving the tenant's
82
+ * content MINUS these files — they exist in the repo and are absent from the live
83
+ * version. Absent/empty ⇒ everything mapped was published. `lastError` names the
84
+ * same paths in prose. Written by `reconcilePreview`.
85
+ */
86
+ droppedArtifacts?: string[];
79
87
  /**
80
88
  * The auditable safety-block override applied to this reconcile (who/why), when a
81
89
  * blocking-safety finding was materialized past. Absent/null ⇒ no override.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Route-document HTML cache policy (ADR 0013 — `serving.htmlCachePolicy`).
3
+ *
4
+ * The browser/shared TTLs a tenant's ROUTE DOCUMENTS (HTML) are served with are a
5
+ * declared property of the TENANT, read by BOTH public-serving substrates (the
6
+ * Cloudflare Worker+R2 pointer plane and the AWS S3/CloudFront plane), so moving
7
+ * substrate never silently changes repeat-visit caching. This module is the ONE
8
+ * place the policy is turned into a `Cache-Control` string, so both planes emit a
9
+ * byte-identical header from one formatter.
10
+ */
11
+
12
+ /** The declared route-document cache TTLs (all in SECONDS). Absent fields fall to {@link DEFAULT_HTML_CACHE_POLICY}. */
13
+ export interface HtmlCachePolicy {
14
+ /** `max-age` — how long a browser may reuse a route document without revalidating. */
15
+ browserMaxAge?: number;
16
+ /** `s-maxage` — how long the shared CDN cache may reuse it. */
17
+ sharedMaxAge?: number;
18
+ /** `stale-while-revalidate` — grace window a stale document may still serve while a fresh one is fetched. */
19
+ staleWhileRevalidate?: number;
20
+ }
21
+
22
+ /**
23
+ * The route-document default when a tenant declares no `serving.htmlCachePolicy`.
24
+ * A 5-minute browser+shared TTL with a 1-day SWR: fresh-biased because the pointer
25
+ * flip republishes at a STABLE url and every publish busts the edge (digest-scoped
26
+ * keys self-invalidate), so a short browser ceiling is safe and the ADR's contract.
27
+ */
28
+ export const DEFAULT_HTML_CACHE_POLICY: Required<HtmlCachePolicy> = {
29
+ browserMaxAge: 300,
30
+ sharedMaxAge: 300,
31
+ staleWhileRevalidate: 86400,
32
+ };
33
+
34
+ /**
35
+ * Render a {@link HtmlCachePolicy} to a `Cache-Control` header value. Each field
36
+ * falls back to {@link DEFAULT_HTML_CACHE_POLICY} INDEPENDENTLY, so a partial policy
37
+ * is completed rather than rejected, and `null`/`undefined` yields the full default.
38
+ * This is the single source of the route-document Cache-Control string for both planes.
39
+ */
40
+ export function formatHtmlCacheControl(policy?: HtmlCachePolicy | null): string {
41
+ const browserMaxAge = policy?.browserMaxAge ?? DEFAULT_HTML_CACHE_POLICY.browserMaxAge;
42
+ const sharedMaxAge = policy?.sharedMaxAge ?? DEFAULT_HTML_CACHE_POLICY.sharedMaxAge;
43
+ const staleWhileRevalidate =
44
+ policy?.staleWhileRevalidate ?? DEFAULT_HTML_CACHE_POLICY.staleWhileRevalidate;
45
+ return `public, max-age=${browserMaxAge}, s-maxage=${sharedMaxAge}, stale-while-revalidate=${staleWhileRevalidate}`;
46
+ }
47
+
48
+ /**
49
+ * The internal, MATERIALIZE-ONLY response header the render emits (gated on the
50
+ * materialize probe request header) so the publish/materialize plane can capture the
51
+ * tenant's resolved route-document Cache-Control into the bundle manifest — the same
52
+ * capture pattern as CSP. It is NEVER emitted on a real public response.
53
+ */
54
+ export const HTML_CACHE_POLICY_CAPTURE_HEADER = "x-tot-html-cache-policy";
55
+
56
+ /** The request header the materialize probe sends to ask the render to emit {@link HTML_CACHE_POLICY_CAPTURE_HEADER}. */
57
+ export const HTML_CACHE_POLICY_CAPTURE_PROBE = "x-tot-materialize-capture";
@@ -15,6 +15,12 @@
15
15
  // preview / script types the renderer needs. Read/apply only.
16
16
  export * from "./product.js";
17
17
  export * from "./tenant.js";
18
+ export * from "./publication-space.js";
19
+ export * from "./publication-version.js";
20
+ export * from "./publication-artifact.js";
21
+ // The ONE route-tier authority: route -> mount -> publication space ->
22
+ // {visibility, authorization} -> an ADR 0013 tier. No consumer derives its own.
23
+ export * from "./route-tier.js";
18
24
  export * from "./checkout.js";
19
25
  export * from "./membership.js";
20
26
  export * from "./checkout-orders.js";
@@ -51,6 +57,7 @@ export * from "./review-trust-proof.js";
51
57
  export * from "./compliance/index.js";
52
58
  export * from "./tenant-assets.js";
53
59
  export * from "./static-bundle.js";
60
+ export * from "./html-cache-policy.js";
54
61
  export * from "./binary-path.js";
55
62
  export * from "./hot-standby-readiness.js";
56
63
  export * from "./hash.js";
@@ -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): the bridge is enabled ONLY on
16
- * eligible unregulated-marketing tenants. A regulated tenant is REFUSED with an
17
- * actionable explanation until a capability-specific certification exists — the
18
- * regulated shared-runtime floor is non-waivable, and this bridge never opens an
19
- * escape hatch around it. Promote-time enforcement still runs through the full
20
- * fail-closed gate (`verifyIntegrationPromotable`); this module is the render-time
21
- * tenant-eligibility decision that precedes it.
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 is enabled ONLY
84
- * when the cert clears every clause (pinned, undrifted from `servedDigest`, every
85
- * server-derived obligation proven). Absent/failed ⇒ the decision fails closed and
86
- * the tenant stays refused. This is the WS2 forward seam, not a per-tenant bypass.
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 is enabled only when the
109
- // cert actually clears every clause. Absent/failed ⇒ false ⇒ refused.
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
- if (isRegulated(input.tenant) && !regulatedCertified) {
116
- return {
117
- ok: false,
118
- reason: "regulated-refused",
119
- refusal: {
120
- code: "regulated-tenant-not-certified",
121
- surface,
122
- message:
123
- `The interim marketing bridge is not enabled for regulated tenants (resolved surface ` +
124
- `"${surface}"). A regulated parent tier requires an immutable, version-pinned, ` +
125
- `capability-specific certification — a mere acknowledgement cannot waive the ` +
126
- `non-waivable regulated shared-runtime floor. Complete WS2 certification to enable it.`,
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
+ }