@tokenoftrust/storefront-runner 2.0.28 → 2.0.29

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.
@@ -10,6 +10,7 @@ import type {
10
10
  BrowserEnvironment,
11
11
  CompiledIntegrationCsp,
12
12
  EffectivePlan,
13
+ IntegrationDeclarationInput,
13
14
  ConsentSnapshot,
14
15
  ResolvedRouteTierV1,
15
16
  PublicationRoutePlanV1,
@@ -170,6 +171,13 @@ declare global {
170
171
  * wired; carries no secret and no nonce.
171
172
  */
172
173
  integrationPlan: EffectivePlan;
174
+ /**
175
+ * The exact declaration set used to resolve this request's integration
176
+ * plan. Exact candidate/staged renderers reuse it with their server-owned
177
+ * stage and immutable revision; public browse requests never receive
178
+ * request-time managed declarations.
179
+ */
180
+ integrationDeclarations: readonly IntegrationDeclarationInput[];
173
181
  /**
174
182
  * The tenant's actual initial consent snapshot — computed once by the
175
183
  * middleware from its saved {@link TenantConsentConfig} (ext-admin-03) via
@@ -210,12 +210,14 @@ export function isStaticBundleUploadPath(pathname: string): boolean {
210
210
  * /preview/change/<changeId> the per-change review dashboard
211
211
  * /<tenant>/preview/pr/<n> the friendly PR-number alias
212
212
  * /<tenant>/preview/rev/<sha>/... the immutable per-revision renderer
213
+ * /<tenant>/preview/staged/<sha>/... the immutable combined-release renderer
213
214
  */
214
215
  export function isCandidatePreviewPath(pathname: string): boolean {
215
216
  if (pathname.startsWith("/preview/render/")) return true;
216
217
  if (pathname === "/preview/change" || pathname.startsWith("/preview/change/")) return true;
217
218
  // /<tenant>/preview/pr and /<tenant>/preview/pr/<n> — the PR-number alias.
218
219
  if (/^\/[^/]+\/preview\/pr(?:\/|$)/.test(pathname)) return true;
220
+ if (/^\/[^/]+\/preview\/staged(?:\/|$)/.test(pathname)) return true;
219
221
  // /<tenant>/preview/rev/<sha> and /<tenant>/preview/rev/<sha>/<path...> — the
220
222
  // immutable per-revision alias.
221
223
  return /^\/[^/]+\/preview\/rev(?:\/|$)/.test(pathname);
@@ -12,12 +12,18 @@
12
12
  */
13
13
  import {
14
14
  resolveBrowserEnvironment,
15
- resolveEnvironmentOnlyPlan,
16
15
  type BrowserEnvironment,
17
16
  type EffectivePlan,
17
+ type IntegrationDeclarationInput,
18
+ type IntegrationEnvironment,
18
19
  type IntegrationSiteSurface,
20
+ type ObligationTenant,
19
21
  } from "@tot/public-runtime";
20
22
  import { isAdminPath } from "@/lib/auth/loginGate";
23
+ import {
24
+ resolveIntegrationRenderState,
25
+ type IntegrationRenderState,
26
+ } from "@/lib/integration/renderIntegrationState";
21
27
 
22
28
  /**
23
29
  * The non-secret render revision: the commit SHA this build was cut from (baked at
@@ -109,11 +115,28 @@ export function candidateBrowserEnvironment(input: {
109
115
  canonicalHost: string;
110
116
  revision: string;
111
117
  siteSurface?: IntegrationSiteSurface;
118
+ }): BrowserEnvironment {
119
+ return previewBrowserEnvironment({ ...input, stage: "candidate" });
120
+ }
121
+
122
+ /**
123
+ * The renderer-owned environment for an exact preview artifact. A per-change
124
+ * render is `candidate`; the combined release preview is `staged`. The stage is
125
+ * supplied only by the server-owned route that selected the artifact, never by
126
+ * query string or tenant content.
127
+ */
128
+ export function previewBrowserEnvironment(input: {
129
+ canonicalHost: string;
130
+ revision: string;
131
+ stage: Extract<IntegrationEnvironment, "candidate" | "staged">;
132
+ siteSurface?: IntegrationSiteSurface;
112
133
  }): BrowserEnvironment {
113
134
  return resolveBrowserEnvironment({
114
- dev: import.meta.env.DEV,
135
+ // This is an explicit server-owned preview route, even while exercised by
136
+ // Astro dev/Vitest. Do not let the process mode erase its signed stage.
137
+ dev: false,
115
138
  localHost: false,
116
- candidate: true,
139
+ candidate: input.stage === "candidate",
117
140
  previewHost: true,
118
141
  customizationEnv: "test",
119
142
  siteSurface: input.siteSurface ?? "marketing",
@@ -123,15 +146,80 @@ export function candidateBrowserEnvironment(input: {
123
146
  }
124
147
 
125
148
  /**
126
- * The resolved effective plan for a candidate render — the `candidate`-stage
127
- * environment wrapped in an environment-only plan so `rawHtmlResponse` (the raw
128
- * mount of the fixed-phase engine) renders the same renderer-owned phases a live
129
- * page gets. No integration declarations are materialized on the candidate hot
130
- * path, so this is environment-only until that wiring lands.
149
+ * Resolve an exact preview artifact through the same declaration and regulated-
150
+ * tenant bridge as a normal page. Invalid declarations fail closed to an
151
+ * environment-only plan; a preview can never fall back to the live plan.
131
152
  */
153
+ export function previewIntegrationPlan(
154
+ input: {
155
+ canonicalHost: string;
156
+ revision: string;
157
+ stage: Extract<IntegrationEnvironment, "candidate" | "staged">;
158
+ siteSurface?: IntegrationSiteSurface;
159
+ },
160
+ tenantPublicId: string,
161
+ tenant: ObligationTenant,
162
+ declarations: readonly IntegrationDeclarationInput[],
163
+ ): EffectivePlan {
164
+ return previewIntegrationState(input, tenantPublicId, tenant, declarations).plan;
165
+ }
166
+
167
+ /** Resolve an exact preview plan and CSP atomically. */
168
+ export function previewIntegrationState(
169
+ input: {
170
+ canonicalHost: string;
171
+ revision: string;
172
+ stage: Extract<IntegrationEnvironment, "candidate" | "staged">;
173
+ siteSurface?: IntegrationSiteSurface;
174
+ },
175
+ tenantPublicId: string,
176
+ tenant: ObligationTenant,
177
+ declarations: readonly IntegrationDeclarationInput[],
178
+ ): IntegrationRenderState {
179
+ return resolveIntegrationRenderState({
180
+ tenantPublicId,
181
+ tenant,
182
+ environment: previewBrowserEnvironment(input),
183
+ declarations,
184
+ });
185
+ }
186
+
187
+ /** Resolve a per-change preview against the candidate destination set. */
132
188
  export function candidateIntegrationPlan(
133
189
  input: { canonicalHost: string; revision: string; siteSurface?: IntegrationSiteSurface },
134
190
  tenantPublicId: string,
191
+ tenant: ObligationTenant,
192
+ declarations: readonly IntegrationDeclarationInput[],
193
+ ): EffectivePlan {
194
+ return previewIntegrationPlan({ ...input, stage: "candidate" }, tenantPublicId, tenant, declarations);
195
+ }
196
+
197
+ /** Resolve a combined-release preview against the staged destination set. */
198
+ export function stagedIntegrationPlan(
199
+ input: { canonicalHost: string; revision: string; siteSurface?: IntegrationSiteSurface },
200
+ tenantPublicId: string,
201
+ tenant: ObligationTenant,
202
+ declarations: readonly IntegrationDeclarationInput[],
135
203
  ): EffectivePlan {
136
- return resolveEnvironmentOnlyPlan(candidateBrowserEnvironment(input), tenantPublicId);
204
+ return previewIntegrationPlan({ ...input, stage: "staged" }, tenantPublicId, tenant, declarations);
205
+ }
206
+
207
+ /** Resolve a per-change preview plan and CSP atomically. */
208
+ export function candidateIntegrationState(
209
+ input: { canonicalHost: string; revision: string; siteSurface?: IntegrationSiteSurface },
210
+ tenantPublicId: string,
211
+ tenant: ObligationTenant,
212
+ declarations: readonly IntegrationDeclarationInput[],
213
+ ): IntegrationRenderState {
214
+ return previewIntegrationState({ ...input, stage: "candidate" }, tenantPublicId, tenant, declarations);
215
+ }
216
+
217
+ /** Resolve a combined-release preview plan and CSP atomically. */
218
+ export function stagedIntegrationState(
219
+ input: { canonicalHost: string; revision: string; siteSurface?: IntegrationSiteSurface },
220
+ tenantPublicId: string,
221
+ tenant: ObligationTenant,
222
+ declarations: readonly IntegrationDeclarationInput[],
223
+ ): IntegrationRenderState {
224
+ return previewIntegrationState({ ...input, stage: "staged" }, tenantPublicId, tenant, declarations);
137
225
  }
@@ -0,0 +1,44 @@
1
+ import {
2
+ collectDeclaredCspProfiles,
3
+ compileIntegrationCspFromPlan,
4
+ resolveEnvironmentOnlyPlan,
5
+ resolveInterimBridgePlan,
6
+ type BrowserEnvironment,
7
+ type CompiledIntegrationCsp,
8
+ type EffectivePlan,
9
+ type IntegrationDeclarationInput,
10
+ type ObligationTenant,
11
+ } from "@tot/public-runtime";
12
+
13
+ export interface IntegrationRenderState {
14
+ plan: EffectivePlan;
15
+ compiledCsp?: CompiledIntegrationCsp;
16
+ }
17
+
18
+ /**
19
+ * Resolve the plan and CSP as one fail-closed unit. A document either receives
20
+ * the reviewed plan with the CSP compiled from that exact plan, or an
21
+ * environment-only plan with no integration origins. Partial state never serves.
22
+ */
23
+ export function resolveIntegrationRenderState(input: {
24
+ tenantPublicId: string;
25
+ tenant: ObligationTenant;
26
+ environment: BrowserEnvironment;
27
+ declarations: readonly IntegrationDeclarationInput[];
28
+ }): IntegrationRenderState {
29
+ const fallback = (): IntegrationRenderState => ({
30
+ plan: resolveEnvironmentOnlyPlan(input.environment, input.tenantPublicId),
31
+ });
32
+ if (input.declarations.length === 0) return fallback();
33
+
34
+ const bridge = resolveInterimBridgePlan(input);
35
+ if (!bridge.ok) return fallback();
36
+
37
+ const compiled = compileIntegrationCspFromPlan(
38
+ bridge.plan,
39
+ collectDeclaredCspProfiles(input.declarations),
40
+ );
41
+ if (!compiled.ok) return fallback();
42
+
43
+ return { plan: bridge.plan, compiledCsp: compiled.value };
44
+ }
@@ -14,6 +14,7 @@ import { CANDIDATE_CONTENT_PIN_PARAM, verifyContentPinToken } from "@tot/public-
14
14
  export interface CandidateContentPin {
15
15
  versionId: string;
16
16
  basePath: string;
17
+ integrationStage: "candidate" | "staged";
17
18
  }
18
19
 
19
20
  /**
@@ -31,5 +32,9 @@ export async function resolveCandidateContentPin(
31
32
  if (!secret) return null;
32
33
  const verified = await verifyContentPinToken(secret, param, Math.floor(Date.now() / 1000));
33
34
  if (!verified.ok || verified.claims.tenantId !== tenantId) return null;
34
- return { versionId: verified.claims.versionId, basePath: verified.claims.basePath };
35
+ return {
36
+ versionId: verified.claims.versionId,
37
+ basePath: verified.claims.basePath,
38
+ integrationStage: verified.claims.integrationStage,
39
+ };
35
40
  }
@@ -49,9 +49,6 @@ import {
49
49
  resolveServableChannelVersion,
50
50
  channelFromEnv,
51
51
  resolveEnvironmentOnlyPlan,
52
- resolveInterimBridgePlan,
53
- compileIntegrationCspFromPlan,
54
- collectDeclaredCspProfiles,
55
52
  INTEGRATION_NONCE_PLACEHOLDER,
56
53
  defaultConsentSnapshot,
57
54
  exactPublicationTimestamp,
@@ -82,8 +79,14 @@ import {
82
79
  } from "@tot/public-runtime";
83
80
  import { loadTenantIntegrationDeclarations } from "@/lib/integration/tenantDeclarations";
84
81
  import { resolveTenantCommerce } from "@/lib/checkoutCommerce";
85
- import { requestBrowserEnvironment, deriveSiteSurface, cspHeaderName } from "@/lib/integration/browserEnvironment";
82
+ import {
83
+ requestBrowserEnvironment,
84
+ previewBrowserEnvironment,
85
+ deriveSiteSurface,
86
+ cspHeaderName,
87
+ } from "@/lib/integration/browserEnvironment";
86
88
  import { freshNonce, stampResponseNonce } from "@/lib/integration/nonceStamp";
89
+ import { resolveIntegrationRenderState } from "@/lib/integration/renderIntegrationState";
87
90
  import { recordActivity } from "@/lib/activity/recordActivity";
88
91
  import { middlewareRewriteTargetV1 } from "./routeBranch.js";
89
92
  import {
@@ -271,32 +274,32 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
271
274
  ...loadTenantIntegrationDeclarations(tenant),
272
275
  ...managedIntegrationDeclarations,
273
276
  ];
277
+ locals.integrationDeclarations = integrationDeclarations;
274
278
  if (integrationDeclarations.length > 0) {
275
279
  const declFeatures = resolveTenantFeatures(tenant);
276
- resolvedBrowserEnvironment = requestBrowserEnvironment({
277
- host,
278
- isPreviewHost,
279
- customizationEnv: customization.env,
280
- canonicalHost: tenant.canonicalDomain || host,
281
- siteSurface: deriveSiteSurface({ catalog: declFeatures.catalog, pathname: renderPath }),
282
- });
283
- const bridge = resolveInterimBridgePlan({
280
+ const siteSurface = deriveSiteSurface({ catalog: declFeatures.catalog, pathname: renderPath });
281
+ resolvedBrowserEnvironment = candidatePin
282
+ ? previewBrowserEnvironment({
283
+ stage: candidatePin.integrationStage,
284
+ canonicalHost: tenant.canonicalDomain || host,
285
+ revision: candidatePin.versionId,
286
+ siteSurface,
287
+ })
288
+ : requestBrowserEnvironment({
289
+ host,
290
+ isPreviewHost,
291
+ customizationEnv: customization.env,
292
+ canonicalHost: tenant.canonicalDomain || host,
293
+ siteSurface,
294
+ });
295
+ const state = resolveIntegrationRenderState({
284
296
  tenantPublicId: tenant.tenant_id,
285
297
  tenant,
286
298
  environment: resolvedBrowserEnvironment,
287
299
  declarations: integrationDeclarations,
288
300
  });
289
- if (bridge.ok) {
290
- // A resolved plan serves; its compiled CSP additions + `planRevision` reach the
291
- // cache key and the header. A refusal (regulated) or an invalid set leaves the
292
- // plan unset here → the environment-only fallback below fills it (nothing loads).
293
- locals.integrationPlan = bridge.plan;
294
- const compiled = compileIntegrationCspFromPlan(
295
- bridge.plan,
296
- collectDeclaredCspProfiles(integrationDeclarations),
297
- );
298
- if (compiled.ok) locals.integrationCsp = compiled.value;
299
- }
301
+ locals.integrationPlan = state.plan;
302
+ if (state.compiledCsp) locals.integrationCsp = state.compiledCsp;
300
303
  }
301
304
 
302
305
  // ---- Consent snapshot: the tenant's ACTUAL initial consent state, not a
@@ -586,13 +589,20 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
586
589
  // never diverge; an undeclared tenant computes it here for the first time.
587
590
  locals.browserEnvironment =
588
591
  resolvedBrowserEnvironment ??
589
- requestBrowserEnvironment({
590
- host,
591
- isPreviewHost,
592
- customizationEnv: customization.env,
593
- canonicalHost: tenant.canonicalDomain || host,
594
- siteSurface: deriveSiteSurface({ catalog: locals.features.catalog, pathname: renderPath }),
595
- });
592
+ (candidatePin
593
+ ? previewBrowserEnvironment({
594
+ stage: candidatePin.integrationStage,
595
+ canonicalHost: tenant.canonicalDomain || host,
596
+ revision: candidatePin.versionId,
597
+ siteSurface: deriveSiteSurface({ catalog: locals.features.catalog, pathname: renderPath }),
598
+ })
599
+ : requestBrowserEnvironment({
600
+ host,
601
+ isPreviewHost,
602
+ customizationEnv: customization.env,
603
+ canonicalHost: tenant.canonicalDomain || host,
604
+ siteSurface: deriveSiteSurface({ catalog: locals.features.catalog, pathname: renderPath }),
605
+ }));
596
606
  // Resolved effective plan (ext-kernel-03) for this request — the fixed-phase
597
607
  // render engine (ext-kernel-04) consumes it in BOTH document mounts (Layout.astro
598
608
  // + rawHtmlResponse). A declared tenant's plan is already resolved above (through
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.0.28",
3
+ "version": "2.0.29",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -150,6 +150,8 @@ export const CANDIDATE_CONTENT_PIN_PARAM = "__tot_candidate_pin";
150
150
  export interface ContentPinClaims {
151
151
  tenantId: string;
152
152
  versionId: string;
153
+ /** Analytics stage selected by the server-owned preview route. */
154
+ integrationStage: "candidate" | "staged";
153
155
  /** basePath the pinned render's own generated links must stay under. */
154
156
  basePath: string;
155
157
  /** Expiry, epoch seconds. */
@@ -221,6 +223,7 @@ export async function verifyContentPinToken(
221
223
  !claims ||
222
224
  typeof claims.tenantId !== "string" ||
223
225
  typeof claims.versionId !== "string" ||
226
+ (claims.integrationStage !== "candidate" && claims.integrationStage !== "staged") ||
224
227
  typeof claims.basePath !== "string" ||
225
228
  typeof claims.exp !== "number"
226
229
  ) {