@tokenoftrust/storefront-runner 2.2.16 → 2.2.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.
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* passes — the client pass included — runs with `build.ssr === true`, so a Vite plugin
|
|
22
22
|
* cannot tell them apart, while `config.build.client` says it outright.
|
|
23
23
|
*/
|
|
24
|
-
import {
|
|
24
|
+
import { createHash } from "node:crypto";
|
|
25
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
25
26
|
import { createRequire } from "node:module";
|
|
26
27
|
import { dirname } from "node:path";
|
|
27
28
|
import { fileURLToPath } from "node:url";
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
LIFECYCLE_HOST_ASSET_PATH,
|
|
30
31
|
LIFECYCLE_HOST_COMPATIBILITY_ASSET_PATHS,
|
|
31
32
|
} from "@tot/public-runtime/integration/lifecycle-host-asset";
|
|
33
|
+
import { decideLifecycleHostLock, formatLifecycleHostLockReport } from "../../../scripts/lib/lifecycle-host-lock.mjs";
|
|
32
34
|
|
|
33
35
|
const HOST_ASSET_PATHS = Object.freeze([
|
|
34
36
|
...LIFECYCLE_HOST_COMPATIBILITY_ASSET_PATHS,
|
|
@@ -42,6 +44,11 @@ const HOST_ENTRY = fileURLToPath(
|
|
|
42
44
|
),
|
|
43
45
|
);
|
|
44
46
|
|
|
47
|
+
const LOCK_URL = new URL(
|
|
48
|
+
"../../../packages/public-runtime/src/integration/lifecycle-host-asset.lock.json",
|
|
49
|
+
import.meta.url,
|
|
50
|
+
);
|
|
51
|
+
|
|
45
52
|
/**
|
|
46
53
|
* Bundle the host to a single self-contained ES module.
|
|
47
54
|
*
|
|
@@ -124,6 +131,30 @@ export function lifecycleHostAsset() {
|
|
|
124
131
|
if (!clientDir) throw new Error("the lifecycle host has no resolved client output dir");
|
|
125
132
|
const outputDir = clientDir;
|
|
126
133
|
const code = await bundleLifecycleHost();
|
|
134
|
+
const hash = createHash("sha256").update(code, "utf8").digest("hex");
|
|
135
|
+
|
|
136
|
+
// Every HOST_ASSET_PATH is served under an `immutable, max-age=1yr` Cache-Control
|
|
137
|
+
// (scripts/publish/lib/s3-publish.mjs PLATFORM_ASSET_PREFIX) — once published, a
|
|
138
|
+
// path's bytes may never change, or visitors who already cached it are stuck on
|
|
139
|
+
// stale tracking/consent behavior for up to a year with nothing to notice it. This
|
|
140
|
+
// is the guard for that: refuse the build rather than silently rewrite a path that
|
|
141
|
+
// already shipped different bytes.
|
|
142
|
+
const lock = JSON.parse(await readFile(LOCK_URL, "utf8"));
|
|
143
|
+
const decision = decideLifecycleHostLock(
|
|
144
|
+
lock,
|
|
145
|
+
HOST_ASSET_PATHS.map((assetPath) => ({ assetPath, hash })),
|
|
146
|
+
);
|
|
147
|
+
if (!decision.ok) {
|
|
148
|
+
throw new Error(formatLifecycleHostLockReport(decision));
|
|
149
|
+
}
|
|
150
|
+
if (JSON.stringify(decision.nextLock) !== JSON.stringify(lock)) {
|
|
151
|
+
await writeFile(LOCK_URL, `${JSON.stringify(decision.nextLock, null, 2)}\n`, "utf8");
|
|
152
|
+
logger.warn(
|
|
153
|
+
"lifecycle host lock recorded a new path — commit " +
|
|
154
|
+
"packages/public-runtime/src/integration/lifecycle-host-asset.lock.json",
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
127
158
|
await Promise.all(HOST_ASSET_PATHS.map(async (assetPath) => {
|
|
128
159
|
const target = fileURLToPath(new URL(assetPath, outputDir));
|
|
129
160
|
await mkdir(dirname(target), { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.18",
|
|
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",
|
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
* payload to one exact collection endpoint. The fixed device value groups events
|
|
7
7
|
* as non-identifying storefront traffic; it is never derived from a browser,
|
|
8
8
|
* person, account, persisted client value, network address, or device signal.
|
|
9
|
+
*
|
|
10
|
+
* Channel ATTRIBUTION (UTM tokens + the referrer's origin) is allowed through as
|
|
11
|
+
* plain `event_properties`: it is a minimized, campaign-scoped label already
|
|
12
|
+
* validated upstream (`events.ts` `validatePageAttribution`), not a visitor or
|
|
13
|
+
* device signal, and this adapter re-screens each value for PII before sending
|
|
14
|
+
* it regardless. A persistent/session identity is a different thing entirely —
|
|
15
|
+
* it stays out, on purpose: `device_id` remains the fixed, non-identifying value
|
|
16
|
+
* below and there is still no `session_id`, `user_id`, or any persisted client
|
|
17
|
+
* value anywhere in this file.
|
|
9
18
|
*/
|
|
10
19
|
import type { CommerceEventBroker, CommerceEventSubscription } from "./commerce-broker.js";
|
|
11
20
|
import {
|
|
@@ -47,13 +56,23 @@ export const AMPLITUDE_HTTP_EVENT_TYPES = Object.freeze([
|
|
|
47
56
|
"storefront.action.activated.v1",
|
|
48
57
|
] as const);
|
|
49
58
|
|
|
59
|
+
/** The flat channel-attribution keys this adapter will forward, mirroring `events.ts` UTM_FIELDS. */
|
|
60
|
+
export type AmplitudeAttributionProperties = Partial<{
|
|
61
|
+
utm_source: string;
|
|
62
|
+
utm_medium: string;
|
|
63
|
+
utm_campaign: string;
|
|
64
|
+
utm_term: string;
|
|
65
|
+
utm_content: string;
|
|
66
|
+
referrer_origin: string;
|
|
67
|
+
}>;
|
|
68
|
+
|
|
50
69
|
export interface AmplitudeHttpEvent {
|
|
51
70
|
event_type: "page_view" | (typeof APPROVED_MARKETING_ACTION_NAMES)[number];
|
|
52
71
|
device_id: typeof ANONYMOUS_DEVICE_ID;
|
|
53
72
|
insert_id: string;
|
|
54
73
|
/** Occurrence time, epoch milliseconds. */
|
|
55
74
|
time: number;
|
|
56
|
-
event_properties: GovernedEventProperties & { page_path?: string };
|
|
75
|
+
event_properties: GovernedEventProperties & { page_path?: string } & AmplitudeAttributionProperties;
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
export const AMPLITUDE_DEDUPE_CAPACITY = 512 as const;
|
|
@@ -104,6 +123,44 @@ class BoundedInsertIds {
|
|
|
104
123
|
}
|
|
105
124
|
}
|
|
106
125
|
|
|
126
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
127
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Flatten the already-validated `data.attribution` object (`events.ts`
|
|
132
|
+
* `validatePageAttribution` — a minimized referrer origin plus up to five UTM
|
|
133
|
+
* tokens) into Amplitude's flat `event_properties`. Re-screened for PII here
|
|
134
|
+
* rather than trusted from upstream validation alone, matching `isSafePagePath`'s
|
|
135
|
+
* own double-check: a field that resembles PII is dropped individually rather
|
|
136
|
+
* than failing the whole page-view event.
|
|
137
|
+
*/
|
|
138
|
+
function extractAttributionProperties(data: Record<string, unknown>): AmplitudeAttributionProperties {
|
|
139
|
+
const attribution = data["attribution"];
|
|
140
|
+
if (!isPlainObject(attribution)) return {};
|
|
141
|
+
const out: AmplitudeAttributionProperties = {};
|
|
142
|
+
|
|
143
|
+
const utm = attribution["utm"];
|
|
144
|
+
if (isPlainObject(utm)) {
|
|
145
|
+
for (const key of ["source", "medium", "campaign", "term", "content"] as const) {
|
|
146
|
+
const value = utm[key];
|
|
147
|
+
if (typeof value === "string" && value.length > 0 && !resemblesManagedAnalyticsPii(value)) {
|
|
148
|
+
out[`utm_${key}`] = value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const referrer = attribution["referrer"];
|
|
154
|
+
if (isPlainObject(referrer)) {
|
|
155
|
+
const origin = referrer["origin"];
|
|
156
|
+
if (typeof origin === "string" && origin.length > 0 && !resemblesManagedAnalyticsPii(origin)) {
|
|
157
|
+
out.referrer_origin = origin;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
|
|
107
164
|
function isSafePagePath(value: unknown): value is string {
|
|
108
165
|
return typeof value === "string" && value.startsWith("/") && !value.startsWith("//") &&
|
|
109
166
|
!value.includes("\\") && !value.includes("?") && !value.includes("#") &&
|
|
@@ -166,7 +223,12 @@ export function mapCommerceEventToAmplitude(
|
|
|
166
223
|
}
|
|
167
224
|
const path = event.data["path"];
|
|
168
225
|
if (!isSafePagePath(path)) return null;
|
|
169
|
-
|
|
226
|
+
const attribution = extractAttributionProperties(event.data);
|
|
227
|
+
return {
|
|
228
|
+
...base,
|
|
229
|
+
event_type: "page_view",
|
|
230
|
+
event_properties: { ...governed, page_path: path, ...attribution },
|
|
231
|
+
};
|
|
170
232
|
}
|
|
171
233
|
|
|
172
234
|
export interface RegisterAmplitudeHttpDestinationInput {
|