@tokenoftrust/storefront-runner 2.0.28 → 2.0.30
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 +8 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +2 -0
- package/apps/storefront/src/lib/integration/browserEnvironment.ts +97 -9
- package/apps/storefront/src/lib/integration/renderIntegrationState.ts +44 -0
- package/apps/storefront/src/middleware/candidatePin.ts +6 -1
- package/apps/storefront/src/middleware/renderChain.ts +40 -30
- package/package.json +1 -1
- package/packages/public-runtime/src/customization-preview.ts +3 -0
- package/packages/public-runtime/src/integration/contact-sales-action-source.ts +75 -0
- package/packages/public-runtime/src/integration/index.ts +1 -0
- package/packages/public-runtime/src/integration/lifecycle-boot.ts +15 -2
package/apps/storefront/env.d.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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.
|
|
3
|
+
"version": "2.0.30",
|
|
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
|
) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform-owned browser binding for the two reviewed contact-sales CTAs.
|
|
3
|
+
*
|
|
4
|
+
* This is deliberately not a general interaction adapter. Both DOM identifiers and
|
|
5
|
+
* the emitted semantic name are fixed below; the source reads no labels, URLs,
|
|
6
|
+
* selectors supplied by a caller, event detail, or arbitrary DOM data.
|
|
7
|
+
*/
|
|
8
|
+
import type { PlatformManagedActionActivation, PlatformManagedActionSource } from "./managed-analytics-bridge.js";
|
|
9
|
+
|
|
10
|
+
/** The only reviewed CTA identities that may raise this marketing action. */
|
|
11
|
+
export const CONTACT_SALES_ACTION_SOURCES = [
|
|
12
|
+
{ ctaId: "cta-book-demo", logicalId: "contact-sales-book-demo" },
|
|
13
|
+
{ ctaId: "contact-sales", logicalId: "contact-sales-lower" },
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
const CONTACT_SALES_ACTION_NAME = "user_clicked_contact_sales_mktg" as const;
|
|
17
|
+
const CORRELATION_TTL_MS = 60 * 60 * 1000;
|
|
18
|
+
|
|
19
|
+
export interface ContactSalesActionElement {
|
|
20
|
+
addEventListener(type: "click", listener: () => void): void;
|
|
21
|
+
removeEventListener(type: "click", listener: () => void): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The narrow DOM seam used only to find the two fixed platform CTA identities. */
|
|
25
|
+
export interface ContactSalesActionDocument {
|
|
26
|
+
querySelectorAll(selector: string): ArrayLike<ContactSalesActionElement>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ContactSalesActionSourceOptions {
|
|
30
|
+
document?: ContactSalesActionDocument;
|
|
31
|
+
planRevision: string;
|
|
32
|
+
now?: () => number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Bind literal `data-cta-id` values to a typed source for the managed-analytics
|
|
37
|
+
* bridge. A keyboard activation of an anchor/button produces its native `click`, so
|
|
38
|
+
* it follows this same semantic path without a separate key listener.
|
|
39
|
+
*/
|
|
40
|
+
export function createContactSalesActionSource(
|
|
41
|
+
options: ContactSalesActionSourceOptions,
|
|
42
|
+
): PlatformManagedActionSource {
|
|
43
|
+
const document = options.document;
|
|
44
|
+
const now = options.now ?? Date.now;
|
|
45
|
+
let sequence = 0;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
subscribe(listener) {
|
|
49
|
+
if (!document || typeof document.querySelectorAll !== "function") return () => {};
|
|
50
|
+
const removals: Array<() => void> = [];
|
|
51
|
+
for (const source of CONTACT_SALES_ACTION_SOURCES) {
|
|
52
|
+
const nodes = document.querySelectorAll(`[data-cta-id="${source.ctaId}"]`);
|
|
53
|
+
for (const node of Array.from(nodes)) {
|
|
54
|
+
const emit = (): void => {
|
|
55
|
+
sequence += 1;
|
|
56
|
+
const at = now();
|
|
57
|
+
const activation: PlatformManagedActionActivation = {
|
|
58
|
+
logicalId: `${source.logicalId}-${options.planRevision}-${sequence}`,
|
|
59
|
+
name: CONTACT_SALES_ACTION_NAME,
|
|
60
|
+
correlation: {
|
|
61
|
+
id: `contact-sales-${options.planRevision}`,
|
|
62
|
+
expiresAt: new Date(at + CORRELATION_TTL_MS).toISOString(),
|
|
63
|
+
},
|
|
64
|
+
sequence,
|
|
65
|
+
};
|
|
66
|
+
listener(activation);
|
|
67
|
+
};
|
|
68
|
+
node.addEventListener("click", emit);
|
|
69
|
+
removals.push(() => node.removeEventListener("click", emit));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return () => removals.splice(0).forEach((remove) => remove());
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -76,6 +76,7 @@ export * from "./events-catalog.js";
|
|
|
76
76
|
export * from "./commerce-broker.js";
|
|
77
77
|
export * from "./managed-analytics.js";
|
|
78
78
|
export * from "./managed-analytics-bridge.js";
|
|
79
|
+
export * from "./contact-sales-action-source.js";
|
|
79
80
|
export * from "./g2-action-boundary.js";
|
|
80
81
|
export * from "./correlation-token.js";
|
|
81
82
|
export * from "./order-conversion.js";
|
|
@@ -45,6 +45,10 @@ import { validateBrowserEnvironment, BROWSER_ENVIRONMENT_GLOBAL, type BrowserEnv
|
|
|
45
45
|
import { makePlatformAdapterRuntimeLoader } from "./platform-adapter-runtime.js";
|
|
46
46
|
import { ManagedAnalyticsEventBrokerBridge } from "./managed-analytics-bridge.js";
|
|
47
47
|
import { minimizePageAttribution, type ManagedPageViewInput } from "./managed-analytics.js";
|
|
48
|
+
import {
|
|
49
|
+
createContactSalesActionSource,
|
|
50
|
+
type ContactSalesActionDocument,
|
|
51
|
+
} from "./contact-sales-action-source.js";
|
|
48
52
|
|
|
49
53
|
/** The window property holding the single per-document lifecycle (idempotent boot guard). */
|
|
50
54
|
export const LIFECYCLE_SINGLETON_GLOBAL = "__TOT_INTEGRATION_LIFECYCLE__";
|
|
@@ -68,7 +72,7 @@ export interface LifecycleWindow {
|
|
|
68
72
|
* analytics data allowlist accepts no other document state — no title, no DOM, no
|
|
69
73
|
* globals — so this is deliberately the narrowest slice that supports attribution.
|
|
70
74
|
*/
|
|
71
|
-
document?: { referrer?: string };
|
|
75
|
+
document?: { referrer?: string; querySelectorAll?: ContactSalesActionDocument["querySelectorAll"] };
|
|
72
76
|
[key: string]: unknown;
|
|
73
77
|
}
|
|
74
78
|
|
|
@@ -337,7 +341,16 @@ export function bootIntegrationLifecycle(
|
|
|
337
341
|
commerceBroker,
|
|
338
342
|
managedAnalytics: {
|
|
339
343
|
bridge: options.managedAnalyticsBridge ??
|
|
340
|
-
new ManagedAnalyticsEventBrokerBridge({
|
|
344
|
+
new ManagedAnalyticsEventBrokerBridge({
|
|
345
|
+
broker: commerceBroker,
|
|
346
|
+
actionSource: createContactSalesActionSource({
|
|
347
|
+
document: typeof win.document?.querySelectorAll === "function"
|
|
348
|
+
? win.document as ContactSalesActionDocument
|
|
349
|
+
: undefined,
|
|
350
|
+
planRevision: descriptor.planRevision,
|
|
351
|
+
now: options.now ?? Date.now,
|
|
352
|
+
}),
|
|
353
|
+
}),
|
|
341
354
|
pageView: () => documentPageView(win, descriptor, options.now ?? Date.now),
|
|
342
355
|
},
|
|
343
356
|
});
|