@gradial/aci 0.1.50 → 0.1.51-ssr.2
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/CHANGELOG.md +0 -21
- package/README.md +13 -20
- package/dist/astro/assets.d.ts +6 -6
- package/dist/astro/assets.js +6 -6
- package/dist/astro/index.d.ts +0 -2
- package/dist/astro/index.js +0 -4
- package/dist/cli/emit-error-pages.mjs +0 -1
- package/dist/cli/verify-next-cdn-routes.mjs +3 -4
- package/dist/collect/aci.js +1 -1
- package/dist/collect/aci.js.sha256 +1 -1
- package/dist/collect/batcher.d.ts +0 -6
- package/dist/collect/index.js +0 -9
- package/dist/collect/webvitals.d.ts +0 -3
- package/dist/collect/webvitals.js +11 -21
- package/dist/content/cache.d.ts +1 -9
- package/dist/content/cache.js +2 -37
- package/dist/content/error-pages.d.ts +1 -2
- package/dist/content/index.d.ts +0 -1
- package/dist/content/index.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/internal/domain/edge/identity.d.ts +14 -0
- package/dist/internal/domain/edge/identity.js +214 -0
- package/dist/internal/domain/edge/index.d.ts +4 -0
- package/dist/internal/domain/edge/index.js +6 -0
- package/dist/internal/domain/edge/personalization-core.d.ts +24 -0
- package/dist/{edge → internal/domain/edge}/personalization-core.js +165 -64
- package/dist/internal/domain/edge/record/codec.d.ts +71 -0
- package/dist/internal/domain/edge/record/codec.js +401 -0
- package/dist/internal/domain/edge/resolve.d.ts +2 -0
- package/dist/internal/domain/edge/resolve.js +76 -0
- package/dist/{next/personalization-host.d.ts → internal/domain/edge/types.d.ts} +110 -63
- package/dist/internal/domain/edge/types.js +7 -0
- package/dist/internal/platform/collectionstore/errors.d.ts +5 -0
- package/dist/internal/platform/collectionstore/errors.js +7 -0
- package/dist/next/config.d.ts +0 -8
- package/dist/next/config.js +3 -78
- package/dist/next/error-pages.d.ts +2 -3
- package/dist/next/error-pages.js +5 -5
- package/dist/next/error-seat.d.ts +2 -3
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +1 -2
- package/dist/next/page.d.ts +3 -13
- package/dist/next/page.js +39 -51
- package/dist/next/server.d.ts +12 -32
- package/dist/next/server.js +66 -328
- package/dist/providers/object.d.ts +24 -0
- package/dist/providers/object.js +105 -0
- package/dist/providers/vercel-object-reader.d.ts +20 -0
- package/dist/providers/vercel-object-reader.js +45 -0
- package/dist/testing/conformance/checks.d.ts +1 -1
- package/dist/testing/conformance/checks.js +14 -152
- package/dist/testing/conformance/index.d.ts +1 -1
- package/dist/testing/conformance/index.js +1 -1
- package/dist/testing/conformance/preset.js +1 -1
- package/dist/testing/conformance/types.d.ts +1 -1
- package/dist/types/config.d.ts +0 -4
- package/dist/types/index.d.ts +0 -2
- package/dist/types/index.js +0 -2
- package/dist/types/page.d.ts +0 -10
- package/package.json +11 -35
- package/src/cli/emit-error-pages.mjs +0 -1
- package/src/cli/verify-next-cdn-routes.mjs +8 -11
- package/src/types/config.ts +0 -5
- package/src/types/index.ts +0 -2
- package/src/types/page.ts +0 -11
- package/templates/astro/template/.aci.yaml.tmpl +0 -11
- package/templates/common/.agents/skills/aci/SKILL.md +8 -9
- package/templates/common/.agents/skills/authoring/SKILL.md +3 -4
- package/templates/common/.agents/skills/authoring/references/block-registry.md +13 -36
- package/templates/common/.agents/skills/authoring/references/contracts.md +10 -17
- package/templates/common/.agents/skills/authoring/references/repo-structure.md +2 -2
- package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +45 -95
- package/templates/common/.agents/skills/migration/references/canon-migration.md +17 -37
- package/templates/nextjs/template/.aci.yaml.tmpl +0 -11
- package/templates/nextjs/template/README.md.tmpl +0 -1
- package/templates/nextjs/template/src/app/[[...slug]]/page.tsx +1 -1
- package/dist/collect/deviceclass.d.ts +0 -12
- package/dist/collect/deviceclass.js +0 -27
- package/dist/content/assets.d.ts +0 -18
- package/dist/content/assets.js +0 -66
- package/dist/edge/personalization-core.d.ts +0 -11
- package/dist/internal/render-proof.d.ts +0 -1
- package/dist/internal/render-proof.js +0 -197
- package/dist/next/asset-route.d.ts +0 -10
- package/dist/next/asset-route.js +0 -45
- package/dist/next/collect-route.d.ts +0 -19
- package/dist/next/collect-route.js +0 -459
- package/dist/next/edge-config.d.ts +0 -1
- package/dist/next/edge-config.js +0 -92
- package/dist/next/envelope-digest.d.ts +0 -21
- package/dist/next/envelope-digest.js +0 -58
- package/dist/next/middleware.d.ts +0 -40
- package/dist/next/middleware.js +0 -475
- package/dist/next/personalization-host.js +0 -270
- package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
- package/dist/next/vercel-collect-sdk-route.js +0 -181
- package/dist/next/vercel-edge-projection-v1.d.ts +0 -24
- package/dist/next/vercel-edge-projection-v1.js +0 -880
- package/dist/next/vercel-personalization.d.ts +0 -4
- package/dist/next/vercel-personalization.js +0 -4
- package/dist/next/vercel-s3-personalization.d.ts +0 -79
- package/dist/next/vercel-s3-personalization.js +0 -570
- package/dist/providers/s3.d.ts +0 -47
- package/dist/providers/s3.js +0 -434
- package/dist/types/render-mode.d.ts +0 -2
- package/dist/types/render-mode.js +0 -1
- package/dist/types/renderer.d.ts +0 -71
- package/dist/types/renderer.js +0 -1
- package/src/types/render-mode.ts +0 -10
- package/src/types/renderer.ts +0 -83
- package/templates/nextjs/starter-content/src/middleware.ts +0 -49
- package/templates/nextjs/template/src/app/gradial/assets/[...path]/route.ts +0 -1
- package/templates/nextjs/template/src/middleware.ts +0 -49
- package/templates/nextjs/template/src/middleware.ts.tmpl +0 -40
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
// Compact JSON codec for edge-decisioning route records and classification data.
|
|
2
|
+
//
|
|
3
|
+
// project(): canonical → compact (short field names via alias table, omit defaults)
|
|
4
|
+
// expand(): compact → canonical (reverse, never throws — returns INVALID sentinel on failure)
|
|
5
|
+
//
|
|
6
|
+
// Laws (property-tested):
|
|
7
|
+
// expand(project(r)) ≡ normalize(r)
|
|
8
|
+
// expand() never throws — malformed input → INVALID
|
|
9
|
+
// Unknown field code on expand = invalid data (→ base + alarm)
|
|
10
|
+
//
|
|
11
|
+
// The alias table is the single source of truth (schema/alias-table.v1.json).
|
|
12
|
+
// These codecs are hand-written from it; cross-language round-trip tests
|
|
13
|
+
// enforce drift between the Go and TS implementations.
|
|
14
|
+
//
|
|
15
|
+
// Failure is represented by the exported INVALID sentinel (a unique object).
|
|
16
|
+
// Expand functions return the value directly on success, or INVALID on failure.
|
|
17
|
+
// Callers check `result === INVALID`. This avoids the {ok,value,reason}
|
|
18
|
+
// wrapper in the byte-constrained runtime.
|
|
19
|
+
// ── INVALID sentinel ─────────────────────────────────────────────────
|
|
20
|
+
/**
|
|
21
|
+
* Unique sentinel object returned by expand functions on failure.
|
|
22
|
+
* Callers check `result === INVALID`. Never throws.
|
|
23
|
+
*/
|
|
24
|
+
export const INVALID = Symbol();
|
|
25
|
+
const te = new TextEncoder();
|
|
26
|
+
// ── Alias table (hand-mirrored from schema/alias-table.v1.json) ──────
|
|
27
|
+
const FIELD_ALIASES = {
|
|
28
|
+
schema: 's', path: 'p', dispatch: 'd', variants: 'v', experiments: 'x',
|
|
29
|
+
priority: 'pr', kind: 'k', rule: 'r', all: 'a', any: 'n', field: 'f',
|
|
30
|
+
operator: 'o', value: 'w', audiences: 'au', segments: 'sg',
|
|
31
|
+
classification: 'c', arms: 'ar', id: 'id', status: 'st',
|
|
32
|
+
allocationVersion: 'av', salt: 'sl', allocationBasisPoints: 'ab',
|
|
33
|
+
variantKey: 'vk', variantValues: 'vv', artifactPath: 'ap',
|
|
34
|
+
displayName: 'dn', primaryGoalId: 'pg', goalIds: 'gl', role: 'ro',
|
|
35
|
+
};
|
|
36
|
+
const DISPATCH_KIND_TO_COMPACT = {
|
|
37
|
+
'static-direct': 's',
|
|
38
|
+
};
|
|
39
|
+
const DISPATCH_KIND_FROM_COMPACT = {
|
|
40
|
+
s: 'static-direct',
|
|
41
|
+
};
|
|
42
|
+
const OPERATOR_TO_COMPACT = {
|
|
43
|
+
equals: 'eq', contains: 'ct', startsWith: 'sw', exists: 'ex',
|
|
44
|
+
};
|
|
45
|
+
const OPERATOR_FROM_COMPACT = {
|
|
46
|
+
eq: 'equals', ct: 'contains', sw: 'startsWith', ex: 'exists',
|
|
47
|
+
};
|
|
48
|
+
const EXPERIMENT_STATUS_TO_COMPACT = {
|
|
49
|
+
running: 'r', paused: 'p', completed: 'd',
|
|
50
|
+
};
|
|
51
|
+
const EXPERIMENT_STATUS_FROM_COMPACT = {
|
|
52
|
+
r: 'running', p: 'paused', d: 'completed',
|
|
53
|
+
};
|
|
54
|
+
const EXPERIMENT_KIND_TO_COMPACT = {
|
|
55
|
+
ab: 'a', abn: 'n',
|
|
56
|
+
};
|
|
57
|
+
const EXPERIMENT_KIND_FROM_COMPACT = {
|
|
58
|
+
a: 'ab', n: 'abn',
|
|
59
|
+
};
|
|
60
|
+
// Compact type guard — reduces minified bytes vs repeated typeof+null+isArray checks
|
|
61
|
+
function isObj(v) {
|
|
62
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
63
|
+
}
|
|
64
|
+
function projectDispatch(dispatch) {
|
|
65
|
+
if (dispatch.kind !== 'static-direct' || !dispatch.artifactPath) {
|
|
66
|
+
throw new Error('edge dispatch must name one static artifact');
|
|
67
|
+
}
|
|
68
|
+
return { k: DISPATCH_KIND_TO_COMPACT[dispatch.kind], ap: dispatch.artifactPath };
|
|
69
|
+
}
|
|
70
|
+
function expandDispatch(raw) {
|
|
71
|
+
if (raw == null)
|
|
72
|
+
return undefined;
|
|
73
|
+
if (!isObj(raw))
|
|
74
|
+
return INVALID;
|
|
75
|
+
const kind = DISPATCH_KIND_FROM_COMPACT[raw.k];
|
|
76
|
+
const artifactPath = raw.ap;
|
|
77
|
+
if (kind !== 'static-direct' || typeof artifactPath !== 'string' || artifactPath.length === 0)
|
|
78
|
+
return INVALID;
|
|
79
|
+
return { kind, artifactPath };
|
|
80
|
+
}
|
|
81
|
+
function projectCondition(cond) {
|
|
82
|
+
if (!cond || !cond.field || !cond.operator)
|
|
83
|
+
return null;
|
|
84
|
+
const compactOp = OPERATOR_TO_COMPACT[cond.operator];
|
|
85
|
+
if (!compactOp)
|
|
86
|
+
return null;
|
|
87
|
+
const result = { f: cond.field, o: compactOp };
|
|
88
|
+
if (cond.operator !== 'exists' && cond.value !== undefined)
|
|
89
|
+
result.w = cond.value;
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
function expandCondition(raw) {
|
|
93
|
+
if (!isObj(raw))
|
|
94
|
+
return INVALID;
|
|
95
|
+
const f = raw.f;
|
|
96
|
+
const co = raw.o;
|
|
97
|
+
if (typeof f !== 'string')
|
|
98
|
+
return INVALID;
|
|
99
|
+
const op = OPERATOR_FROM_COMPACT[co];
|
|
100
|
+
if (!op)
|
|
101
|
+
return INVALID;
|
|
102
|
+
const c = { field: f, operator: op };
|
|
103
|
+
if (op !== 'exists' && raw.w !== undefined)
|
|
104
|
+
c.value = raw.w;
|
|
105
|
+
return c;
|
|
106
|
+
}
|
|
107
|
+
function projectRule(rule) {
|
|
108
|
+
if (!rule)
|
|
109
|
+
return undefined;
|
|
110
|
+
const result = {};
|
|
111
|
+
if (rule.all && rule.all.length > 0)
|
|
112
|
+
result.a = rule.all.map(projectCondition).filter((c) => c !== null);
|
|
113
|
+
if (rule.any && rule.any.length > 0)
|
|
114
|
+
result.n = rule.any.map(projectCondition).filter((c) => c !== null);
|
|
115
|
+
if (!result.a && !result.n)
|
|
116
|
+
return undefined;
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
function expandRule(raw) {
|
|
120
|
+
if (raw == null)
|
|
121
|
+
return undefined;
|
|
122
|
+
if (!isObj(raw))
|
|
123
|
+
return INVALID;
|
|
124
|
+
const rule = {};
|
|
125
|
+
if (Array.isArray(raw.a))
|
|
126
|
+
for (const c of raw.a) {
|
|
127
|
+
const r = expandCondition(c);
|
|
128
|
+
if (r === INVALID)
|
|
129
|
+
return INVALID;
|
|
130
|
+
if (r)
|
|
131
|
+
(rule.all = rule.all || []).push(r);
|
|
132
|
+
}
|
|
133
|
+
if (Array.isArray(raw.n))
|
|
134
|
+
for (const c of raw.n) {
|
|
135
|
+
const r = expandCondition(c);
|
|
136
|
+
if (r === INVALID)
|
|
137
|
+
return INVALID;
|
|
138
|
+
if (r)
|
|
139
|
+
(rule.any = rule.any || []).push(r);
|
|
140
|
+
}
|
|
141
|
+
if (!rule.all && !rule.any)
|
|
142
|
+
return undefined;
|
|
143
|
+
return rule;
|
|
144
|
+
}
|
|
145
|
+
function projectVariant(variant) {
|
|
146
|
+
const result = {};
|
|
147
|
+
if (variant.priority)
|
|
148
|
+
result.pr = variant.priority;
|
|
149
|
+
if (variant.rule)
|
|
150
|
+
result.r = projectRule(variant.rule);
|
|
151
|
+
result.d = projectDispatch(variant.dispatch);
|
|
152
|
+
if (variant.variantValues && Object.keys(variant.variantValues).length > 0)
|
|
153
|
+
result.vv = variant.variantValues;
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
function expandVariant(raw) {
|
|
157
|
+
if (!isObj(raw))
|
|
158
|
+
return INVALID;
|
|
159
|
+
const obj = raw;
|
|
160
|
+
if (obj.d === undefined)
|
|
161
|
+
return INVALID;
|
|
162
|
+
const dispatchResult = expandDispatch(obj.d);
|
|
163
|
+
if (dispatchResult === INVALID || !dispatchResult)
|
|
164
|
+
return INVALID;
|
|
165
|
+
const variant = { variantKey: '', dispatch: dispatchResult };
|
|
166
|
+
if (typeof obj.pr === 'number')
|
|
167
|
+
variant.priority = obj.pr;
|
|
168
|
+
if (obj.r !== undefined) {
|
|
169
|
+
const ruleResult = expandRule(obj.r);
|
|
170
|
+
if (ruleResult === INVALID)
|
|
171
|
+
return INVALID;
|
|
172
|
+
if (ruleResult)
|
|
173
|
+
variant.rule = ruleResult;
|
|
174
|
+
}
|
|
175
|
+
if (isObj(obj.vv))
|
|
176
|
+
variant.variantValues = obj.vv;
|
|
177
|
+
return variant;
|
|
178
|
+
}
|
|
179
|
+
export function projectRouteRecord(entry) {
|
|
180
|
+
const result = { p: entry.pattern, d: projectDispatch(entry.dispatch) };
|
|
181
|
+
result.s = 1;
|
|
182
|
+
if (entry.variants && Object.keys(entry.variants).length > 0) {
|
|
183
|
+
result.v = {};
|
|
184
|
+
for (const [key, variant] of Object.entries(entry.variants))
|
|
185
|
+
result.v[key] = projectVariant(variant);
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
export function expandRouteRecord(raw) {
|
|
190
|
+
if (!isObj(raw))
|
|
191
|
+
return INVALID;
|
|
192
|
+
const obj = raw;
|
|
193
|
+
if (obj.s !== undefined && obj.s !== 1)
|
|
194
|
+
return INVALID;
|
|
195
|
+
const path = obj.p;
|
|
196
|
+
if (typeof path !== 'string')
|
|
197
|
+
return INVALID;
|
|
198
|
+
if (obj.d === undefined)
|
|
199
|
+
return INVALID;
|
|
200
|
+
const dispatchResult = expandDispatch(obj.d);
|
|
201
|
+
if (dispatchResult === INVALID || !dispatchResult)
|
|
202
|
+
return INVALID;
|
|
203
|
+
const entry = { pattern: path, priority: 0, dispatch: dispatchResult };
|
|
204
|
+
if (obj.v !== undefined) {
|
|
205
|
+
if (!isObj(obj.v))
|
|
206
|
+
return INVALID;
|
|
207
|
+
entry.variants = {};
|
|
208
|
+
for (const [key, variantRaw] of Object.entries(obj.v)) {
|
|
209
|
+
const variantResult = expandVariant(variantRaw);
|
|
210
|
+
if (variantResult === INVALID)
|
|
211
|
+
return INVALID;
|
|
212
|
+
const variant = variantResult;
|
|
213
|
+
variant.variantKey = key;
|
|
214
|
+
if (!variant.variantValues)
|
|
215
|
+
variant.variantValues = variantValuesFromKey(key);
|
|
216
|
+
entry.variants[key] = variant;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return entry;
|
|
220
|
+
}
|
|
221
|
+
export function projectClassificationData(data) {
|
|
222
|
+
const result = {};
|
|
223
|
+
if (data.audiences && Object.keys(data.audiences).length > 0) {
|
|
224
|
+
result.au = {};
|
|
225
|
+
for (const [key, def] of Object.entries(data.audiences)) {
|
|
226
|
+
const entry = {};
|
|
227
|
+
const rule = projectRule(def.rule);
|
|
228
|
+
if (rule)
|
|
229
|
+
entry.r = rule;
|
|
230
|
+
result.au[key] = entry;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (data.segments && Object.keys(data.segments).length > 0) {
|
|
234
|
+
result.sg = {};
|
|
235
|
+
for (const [key, def] of Object.entries(data.segments)) {
|
|
236
|
+
const entry = {};
|
|
237
|
+
const rule = projectRule(def.rule);
|
|
238
|
+
if (rule)
|
|
239
|
+
entry.r = rule;
|
|
240
|
+
result.sg[key] = entry;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (data.experiments && Object.keys(data.experiments).length > 0) {
|
|
244
|
+
result.x = {};
|
|
245
|
+
for (const [key, exp] of Object.entries(data.experiments))
|
|
246
|
+
result.x[key] = projectExperiment(exp);
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
function projectExperiment(exp) {
|
|
251
|
+
const result = { id: exp.id };
|
|
252
|
+
if (exp.status) {
|
|
253
|
+
const c = EXPERIMENT_STATUS_TO_COMPACT[exp.status];
|
|
254
|
+
if (c)
|
|
255
|
+
result.st = c;
|
|
256
|
+
}
|
|
257
|
+
if (exp.kind) {
|
|
258
|
+
const c = EXPERIMENT_KIND_TO_COMPACT[exp.kind];
|
|
259
|
+
if (c)
|
|
260
|
+
result.k = c;
|
|
261
|
+
}
|
|
262
|
+
if (exp.allocationVersion)
|
|
263
|
+
result.av = exp.allocationVersion;
|
|
264
|
+
if (exp.salt)
|
|
265
|
+
result.sl = exp.salt;
|
|
266
|
+
if (exp.priority)
|
|
267
|
+
result.pr = exp.priority;
|
|
268
|
+
if (exp.arms && exp.arms.length > 0) {
|
|
269
|
+
result.ar = exp.arms.map((arm) => {
|
|
270
|
+
const ca = { id: arm.id };
|
|
271
|
+
if (arm.allocationBasisPoints)
|
|
272
|
+
ca.ab = arm.allocationBasisPoints;
|
|
273
|
+
if (arm.variantKey)
|
|
274
|
+
ca.vk = arm.variantKey;
|
|
275
|
+
return ca;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
export function expandClassificationData(raw) {
|
|
281
|
+
if (!isObj(raw))
|
|
282
|
+
return INVALID;
|
|
283
|
+
const obj = raw;
|
|
284
|
+
const data = { audiences: {}, segments: {}, experiments: {} };
|
|
285
|
+
if (obj.au !== undefined) {
|
|
286
|
+
if (!isObj(obj.au))
|
|
287
|
+
return INVALID;
|
|
288
|
+
for (const [key, defRaw] of Object.entries(obj.au)) {
|
|
289
|
+
if (!isObj(defRaw))
|
|
290
|
+
return INVALID;
|
|
291
|
+
const ruleResult = expandRule(defRaw.r);
|
|
292
|
+
if (ruleResult === INVALID)
|
|
293
|
+
return INVALID;
|
|
294
|
+
data.audiences[key] = { rule: ruleResult || { all: [] } };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (obj.sg !== undefined) {
|
|
298
|
+
if (!isObj(obj.sg))
|
|
299
|
+
return INVALID;
|
|
300
|
+
for (const [key, defRaw] of Object.entries(obj.sg)) {
|
|
301
|
+
if (!isObj(defRaw))
|
|
302
|
+
return INVALID;
|
|
303
|
+
const ruleResult = expandRule(defRaw.r);
|
|
304
|
+
if (ruleResult === INVALID)
|
|
305
|
+
return INVALID;
|
|
306
|
+
data.segments[key] = { rule: ruleResult || { all: [] } };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (obj.x !== undefined) {
|
|
310
|
+
if (!isObj(obj.x))
|
|
311
|
+
return INVALID;
|
|
312
|
+
for (const [key, expRaw] of Object.entries(obj.x)) {
|
|
313
|
+
const expResult = expandExperiment(expRaw);
|
|
314
|
+
if (expResult === INVALID)
|
|
315
|
+
return INVALID;
|
|
316
|
+
data.experiments[key] = expResult;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return data;
|
|
320
|
+
}
|
|
321
|
+
function expandExperiment(raw) {
|
|
322
|
+
if (!isObj(raw))
|
|
323
|
+
return INVALID;
|
|
324
|
+
const obj = raw;
|
|
325
|
+
const id = obj.id;
|
|
326
|
+
if (typeof id !== 'string')
|
|
327
|
+
return INVALID;
|
|
328
|
+
const exp = { id, status: 'running', kind: 'ab', arms: [] };
|
|
329
|
+
if (typeof obj.st === 'string') {
|
|
330
|
+
const expanded = EXPERIMENT_STATUS_FROM_COMPACT[obj.st];
|
|
331
|
+
if (!expanded)
|
|
332
|
+
return INVALID;
|
|
333
|
+
exp.status = expanded;
|
|
334
|
+
}
|
|
335
|
+
if (typeof obj.k === 'string') {
|
|
336
|
+
const expanded = EXPERIMENT_KIND_FROM_COMPACT[obj.k];
|
|
337
|
+
if (!expanded)
|
|
338
|
+
return INVALID;
|
|
339
|
+
exp.kind = expanded;
|
|
340
|
+
}
|
|
341
|
+
if (typeof obj.av === 'string')
|
|
342
|
+
exp.allocationVersion = obj.av;
|
|
343
|
+
if (typeof obj.sl === 'string')
|
|
344
|
+
exp.salt = obj.sl;
|
|
345
|
+
if (typeof obj.pr === 'number')
|
|
346
|
+
exp.priority = obj.pr;
|
|
347
|
+
if (Array.isArray(obj.ar)) {
|
|
348
|
+
exp.arms = [];
|
|
349
|
+
for (const armRaw of obj.ar) {
|
|
350
|
+
if (!isObj(armRaw))
|
|
351
|
+
return INVALID;
|
|
352
|
+
const armObj = armRaw;
|
|
353
|
+
if (typeof armObj.id !== 'string')
|
|
354
|
+
return INVALID;
|
|
355
|
+
exp.arms.push({
|
|
356
|
+
id: armObj.id,
|
|
357
|
+
allocationBasisPoints: typeof armObj.ab === 'number' ? armObj.ab : 0,
|
|
358
|
+
variantKey: typeof armObj.vk === 'string' ? armObj.vk : '',
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return exp;
|
|
363
|
+
}
|
|
364
|
+
// ── Marker (tri-state: record | marker | absent) ─────────────────────
|
|
365
|
+
export const MARKER = '{"s":1,"e":1}';
|
|
366
|
+
export function isMarker(raw) {
|
|
367
|
+
return raw === MARKER;
|
|
368
|
+
}
|
|
369
|
+
// ── Serialization helpers ────────────────────────────────────────────
|
|
370
|
+
export function canonicalSerialize(value) {
|
|
371
|
+
return JSON.stringify(sortKeys(value));
|
|
372
|
+
}
|
|
373
|
+
function sortKeys(value) {
|
|
374
|
+
if (value === null || typeof value !== 'object')
|
|
375
|
+
return value;
|
|
376
|
+
if (Array.isArray(value))
|
|
377
|
+
return value.map(sortKeys);
|
|
378
|
+
const sorted = {};
|
|
379
|
+
for (const key of Object.keys(value).sort())
|
|
380
|
+
sorted[key] = sortKeys(value[key]);
|
|
381
|
+
return sorted;
|
|
382
|
+
}
|
|
383
|
+
// ── VariantKey parsing ───────────────────────────────────────────────
|
|
384
|
+
export function variantValuesFromKey(key) {
|
|
385
|
+
const values = {};
|
|
386
|
+
for (const part of String(key || '').replace(/&/g, ';').split(';')) {
|
|
387
|
+
const sep = part.indexOf('=');
|
|
388
|
+
if (sep > 0)
|
|
389
|
+
values[part.slice(0, sep).trim()] = part.slice(sep + 1).trim();
|
|
390
|
+
}
|
|
391
|
+
return values;
|
|
392
|
+
}
|
|
393
|
+
// ── Path hashing (FNV-1a-32, hex 16 chars) ───────────────────────────
|
|
394
|
+
export function pathHash16(path) {
|
|
395
|
+
let hash = 0x811c9dc5;
|
|
396
|
+
for (const byte of te.encode(path)) {
|
|
397
|
+
hash ^= byte;
|
|
398
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
399
|
+
}
|
|
400
|
+
return hash.toString(16).padStart(8, '0') + '00000000';
|
|
401
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// resolvePersonalizationRequest — the edge decisioning entry point.
|
|
2
|
+
//
|
|
3
|
+
// Reads cookies for classification but never writes them (R25: consent-gated
|
|
4
|
+
// identity model). Identity minting happens in the collect/consent endpoint,
|
|
5
|
+
// not here. Public personalized responses may be private while providers cache
|
|
6
|
+
// the selected immutable artifact by release and exact artifact path.
|
|
7
|
+
import { resolvePersonalizationDecision, } from './personalization-core.js';
|
|
8
|
+
import { consentSignals, verifyIdentityCookie, identityHashHex, verifyClaims, parseCookieHeader, } from './identity.js';
|
|
9
|
+
export async function resolvePersonalizationRequest(request, config) {
|
|
10
|
+
const cookies = parseCookieHeader(request.headers.get('cookie'));
|
|
11
|
+
const consent = consentSignals(request, cookies);
|
|
12
|
+
const now = (config.now ?? (() => new Date()))();
|
|
13
|
+
// Consent-gated identity model (R25): static artifact selection NEVER mints
|
|
14
|
+
// identity. It reads cookies for classification but never writes them.
|
|
15
|
+
// Identity (aci_id) is minted by the collect/consent endpoint after
|
|
16
|
+
// explicit consent. This keeps all content responses cacheable and is
|
|
17
|
+
// GDPR-compliant (no non-essential cookie before consent).
|
|
18
|
+
//
|
|
19
|
+
// GPC/DNT deny fully: no read, no identity, no experiment bucketing.
|
|
20
|
+
const rawIdentity = consent.privacyDenied
|
|
21
|
+
? null
|
|
22
|
+
: await verifyIdentityCookie(cookies.get('aci_id') || '', config.identityKeys);
|
|
23
|
+
// The identity hash feeds experiment bucketing for identified visitors.
|
|
24
|
+
// Claims are a signed cookie written by /collect; the EdgeRouter
|
|
25
|
+
// never reads the customer object store to refresh or revoke them.
|
|
26
|
+
let signedClaims = cookies.get('aci_claims') || '';
|
|
27
|
+
let identityHash = '';
|
|
28
|
+
if (rawIdentity) {
|
|
29
|
+
identityHash = await identityHashHex('aci_id', rawIdentity);
|
|
30
|
+
}
|
|
31
|
+
if (rawIdentity && consent.allowsIdentityTier && signedClaims) {
|
|
32
|
+
if (!await verifyClaims(signedClaims, config.claimsKeys || {}, now)) {
|
|
33
|
+
signedClaims = '';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const matchedRoute = config.route;
|
|
37
|
+
let variantKey = '';
|
|
38
|
+
if (matchedRoute) {
|
|
39
|
+
const claims = consent.allowsIdentityTier
|
|
40
|
+
? await verifyClaims(signedClaims, config.claimsKeys || {}, now)
|
|
41
|
+
: null;
|
|
42
|
+
const decision = resolvePersonalizationDecision(toPersonalizationRequestContext(request), matchedRoute, claims, config.experiments, identityHash
|
|
43
|
+
? { usable: true, siteId: config.siteId, namespace: 'aci_id', identityHash }
|
|
44
|
+
: undefined);
|
|
45
|
+
if (decision.variant) {
|
|
46
|
+
variantKey = decision.variant.variantKey;
|
|
47
|
+
}
|
|
48
|
+
const experiment = decision.experiment
|
|
49
|
+
? { id: decision.experiment.experimentId, arm: decision.experiment.armId, allocationVersion: decision.experiment.allocationVersion }
|
|
50
|
+
: undefined;
|
|
51
|
+
return { variantKey, experiment };
|
|
52
|
+
}
|
|
53
|
+
return { variantKey };
|
|
54
|
+
}
|
|
55
|
+
function toPersonalizationRequestContext(request) {
|
|
56
|
+
const url = new URL(request.url);
|
|
57
|
+
const cookies = parseCookieHeader(request.headers.get('cookie'));
|
|
58
|
+
const query = {};
|
|
59
|
+
url.searchParams.forEach((value, key) => {
|
|
60
|
+
(query[key] ||= []).push(value);
|
|
61
|
+
});
|
|
62
|
+
const headers = Object.fromEntries(request.headers.entries());
|
|
63
|
+
return {
|
|
64
|
+
path: url.pathname,
|
|
65
|
+
query,
|
|
66
|
+
headers,
|
|
67
|
+
cookies: Object.fromEntries(cookies.entries()),
|
|
68
|
+
signals: {
|
|
69
|
+
geo: {
|
|
70
|
+
country: headers['x-vercel-ip-country'] || headers['cloudfront-viewer-country'] || '',
|
|
71
|
+
region: headers['x-vercel-ip-country-region'] || headers['cloudfront-viewer-country-region'] || '',
|
|
72
|
+
city: headers['x-vercel-ip-city'] || headers['cloudfront-viewer-city'] || '',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import { type PersonalizationExperiment } from '../edge/personalization-core.js';
|
|
2
|
-
import { type PortableConsentSnapshot } from '../consent/portable-envelope.js';
|
|
3
|
-
export declare const CONTENT_PATH_HEADER = "x-aci-content-path";
|
|
4
|
-
export declare const CONTENT_PATH_CACHE_PARAM = "__aci_content_path";
|
|
5
|
-
export declare const ROUTED_PATH_HEADER = "x-aci-routed-path";
|
|
6
|
-
export declare const VARIANT_KEY_HEADER = "x-aci-variant-key";
|
|
7
1
|
export interface EdgeDispatch {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
/** The only deterministic CMS delivery mode: an immutable HTML object. */
|
|
3
|
+
kind: 'static-direct';
|
|
4
|
+
/** Release-relative immutable HTML artifact selected for this route. */
|
|
5
|
+
artifactPath: string;
|
|
11
6
|
}
|
|
12
7
|
export interface EdgeRuleCondition {
|
|
13
8
|
field: string;
|
|
@@ -45,31 +40,77 @@ export interface EdgeRouteManifest {
|
|
|
45
40
|
notFoundPage?: string;
|
|
46
41
|
errorPage?: string;
|
|
47
42
|
}
|
|
48
|
-
export interface
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
43
|
+
export interface PersonalizationRequestContext {
|
|
44
|
+
path: string;
|
|
45
|
+
query: Record<string, string[]>;
|
|
46
|
+
headers: Record<string, string>;
|
|
47
|
+
cookies: Record<string, string>;
|
|
48
|
+
signals?: {
|
|
49
|
+
geo?: Record<string, string>;
|
|
50
|
+
};
|
|
51
|
+
identityHash?: string;
|
|
52
|
+
identityNamespace?: string;
|
|
53
|
+
siteId?: string;
|
|
54
|
+
}
|
|
55
|
+
export interface PersonalizationClaims {
|
|
56
|
+
aud?: string[];
|
|
57
|
+
seg?: string[];
|
|
58
|
+
iat: number;
|
|
59
|
+
exp: number;
|
|
60
|
+
}
|
|
61
|
+
export interface PersonalizationVariant {
|
|
62
|
+
variantKey: string;
|
|
63
|
+
variantValues?: Record<string, string>;
|
|
64
|
+
priority?: number;
|
|
65
|
+
rule?: EdgeRuleExpression;
|
|
66
|
+
dispatch: EdgeDispatch;
|
|
56
67
|
}
|
|
57
|
-
export
|
|
58
|
-
|
|
59
|
-
constructor();
|
|
68
|
+
export interface PersonalizationRoute {
|
|
69
|
+
variants?: Record<string, PersonalizationVariant>;
|
|
60
70
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
export interface PersonalizationExperiment {
|
|
72
|
+
id: string;
|
|
73
|
+
status: string;
|
|
74
|
+
kind: string;
|
|
75
|
+
priority?: number;
|
|
76
|
+
allocationVersion?: string;
|
|
77
|
+
salt?: string;
|
|
78
|
+
arms: Array<{
|
|
79
|
+
id: string;
|
|
80
|
+
allocationBasisPoints?: number;
|
|
81
|
+
variantKey: string;
|
|
82
|
+
}>;
|
|
64
83
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
export interface PersonalizationIdentity {
|
|
85
|
+
usable: boolean;
|
|
86
|
+
siteId: string;
|
|
87
|
+
namespace: string;
|
|
88
|
+
identityHash: string;
|
|
70
89
|
}
|
|
71
|
-
|
|
72
|
-
|
|
90
|
+
export interface PersonalizationExperimentDecision {
|
|
91
|
+
experimentId: string;
|
|
92
|
+
armId: string;
|
|
93
|
+
allocationVersion: string;
|
|
94
|
+
variantKey: string;
|
|
95
|
+
bucket: number;
|
|
96
|
+
}
|
|
97
|
+
export interface PersonalizationDecision {
|
|
98
|
+
variant: PersonalizationVariant | null;
|
|
99
|
+
experiment: PersonalizationExperimentDecision | null;
|
|
100
|
+
}
|
|
101
|
+
export interface ClassificationData {
|
|
102
|
+
audiences: Record<string, {
|
|
103
|
+
rule: EdgeRuleExpression;
|
|
104
|
+
}>;
|
|
105
|
+
segments: Record<string, {
|
|
106
|
+
rule: EdgeRuleExpression;
|
|
107
|
+
}>;
|
|
108
|
+
experiments: Record<string, PersonalizationExperiment>;
|
|
109
|
+
}
|
|
110
|
+
export interface Classification {
|
|
111
|
+
matchedAudiences: string[];
|
|
112
|
+
matchedSegments: string[];
|
|
113
|
+
experimentArms: Record<string, PersonalizationExperimentDecision>;
|
|
73
114
|
}
|
|
74
115
|
export interface SigningKey {
|
|
75
116
|
kid: string;
|
|
@@ -79,16 +120,6 @@ export interface IdentitySigningKeys {
|
|
|
79
120
|
current: SigningKey;
|
|
80
121
|
previous?: SigningKey;
|
|
81
122
|
}
|
|
82
|
-
export interface PersonalizationHostConfig {
|
|
83
|
-
siteId: string;
|
|
84
|
-
store: PersonalizationRouteStore;
|
|
85
|
-
identityKeys: IdentitySigningKeys;
|
|
86
|
-
claimsKeys?: Record<string, string>;
|
|
87
|
-
identityCookieTTLSeconds?: number;
|
|
88
|
-
secureCookies?: boolean;
|
|
89
|
-
now?: () => Date;
|
|
90
|
-
randomBytes?: (length: number) => Uint8Array;
|
|
91
|
-
}
|
|
92
123
|
export interface CookieMutation {
|
|
93
124
|
name: string;
|
|
94
125
|
value: string;
|
|
@@ -101,34 +132,50 @@ export interface CookieMutation {
|
|
|
101
132
|
expires?: Date;
|
|
102
133
|
};
|
|
103
134
|
}
|
|
135
|
+
export interface ConsentSnapshot {
|
|
136
|
+
version: number;
|
|
137
|
+
purposes: string[];
|
|
138
|
+
grantedAt: string;
|
|
139
|
+
source?: string;
|
|
140
|
+
}
|
|
141
|
+
export interface ConsentSignals {
|
|
142
|
+
hasConsent: boolean;
|
|
143
|
+
consentInvalid: boolean;
|
|
144
|
+
snapshot: ConsentSnapshot | null;
|
|
145
|
+
allowsIdentityTier: boolean;
|
|
146
|
+
privacyDenied: boolean;
|
|
147
|
+
}
|
|
148
|
+
export interface PersonalizationEnvelopeWrite {
|
|
149
|
+
key: string;
|
|
150
|
+
envelopeId: string;
|
|
151
|
+
siteId: string;
|
|
152
|
+
identityNamespace: string;
|
|
153
|
+
identityHash: string;
|
|
154
|
+
records: readonly unknown[];
|
|
155
|
+
digest: string;
|
|
156
|
+
}
|
|
157
|
+
export interface PersonalizationCollectionStore {
|
|
158
|
+
readClaims(siteId: string, namespace: string, identityHash: string, now?: Date): Promise<string | null>;
|
|
159
|
+
appendEnvelope(write: PersonalizationEnvelopeWrite): Promise<void>;
|
|
160
|
+
putSuppression(key: string, record: unknown): Promise<void>;
|
|
161
|
+
}
|
|
162
|
+
export interface PersonalizationHostConfig {
|
|
163
|
+
siteId: string;
|
|
164
|
+
route: EdgeRouteEntry | null;
|
|
165
|
+
experiments?: Record<string, PersonalizationExperiment>;
|
|
166
|
+
identityKeys: IdentitySigningKeys;
|
|
167
|
+
claimsKeys?: Record<string, string>;
|
|
168
|
+
identityCookieTTLSeconds?: number;
|
|
169
|
+
secureCookies?: boolean;
|
|
170
|
+
now?: () => Date;
|
|
171
|
+
randomBytes?: (length: number) => Uint8Array;
|
|
172
|
+
}
|
|
104
173
|
export interface PersonalizationResolution {
|
|
105
|
-
requestHeaders: Headers;
|
|
106
|
-
contentPath: string;
|
|
107
174
|
variantKey: string;
|
|
108
175
|
experiment?: {
|
|
109
176
|
id: string;
|
|
110
177
|
arm: string;
|
|
111
178
|
allocationVersion: string;
|
|
112
179
|
};
|
|
113
|
-
cookies: CookieMutation[];
|
|
114
180
|
}
|
|
115
|
-
export
|
|
116
|
-
interface ClaimsPayload {
|
|
117
|
-
aud?: string[];
|
|
118
|
-
seg?: string[];
|
|
119
|
-
iat: number;
|
|
120
|
-
exp: number;
|
|
121
|
-
}
|
|
122
|
-
export declare function resolvePersonalizationRequest(request: Request, releaseId: string, config: PersonalizationHostConfig): Promise<PersonalizationResolution>;
|
|
123
|
-
export declare function consentSignals(request: Request, cookies: Map<string, string>): {
|
|
124
|
-
hasConsent: boolean;
|
|
125
|
-
snapshot: ConsentSnapshot | null;
|
|
126
|
-
allowsIdentityTier: boolean;
|
|
127
|
-
privacyDenied: boolean;
|
|
128
|
-
};
|
|
129
|
-
export declare function parseCookieHeader(value: string | null): Map<string, string>;
|
|
130
|
-
export declare function verifyIdentityCookie(value: string, keys: IdentitySigningKeys): Promise<string | null>;
|
|
131
|
-
export declare function sha256Hex(value: string): Promise<string>;
|
|
132
|
-
export declare function identityHashHex(namespace: string, value: string): Promise<string>;
|
|
133
|
-
export declare function verifyClaims(value: string, keys: Record<string, string>, now: Date): Promise<ClaimsPayload | null>;
|
|
134
|
-
export {};
|
|
181
|
+
export { IdentityErasureFencedError } from '../../platform/collectionstore/errors.js';
|