@gradial/aci 0.1.52-rc.2 → 0.1.52-rc.21
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 +7 -0
- package/README.md +4 -4
- package/dist/astro/preview.js +2 -17
- package/dist/audience-data-models.d.ts +289 -16
- package/dist/audience-data-models.js +99 -2
- package/dist/cli/compile-registry.d.mts +5 -1
- package/dist/cli/compile-registry.mjs +25 -10
- package/dist/cli/validate-content.d.mts +31 -1
- package/dist/cli/validate-content.mjs +50 -12
- package/dist/collect/aci.js +1 -1
- package/dist/collect/aci.js.sha256 +1 -1
- package/dist/collect/api.d.ts +3 -7
- package/dist/collect/api.js +2 -8
- package/dist/collect/batcher.d.ts +6 -5
- package/dist/collect/batcher.js +6 -5
- package/dist/collect/index.js +1 -1
- package/dist/content/provider.d.ts +1 -0
- package/dist/content/resolve-slots.d.ts +9 -2
- package/dist/content/resolve-slots.js +5 -3
- package/dist/content/types.d.ts +2 -0
- package/dist/edge/expression-capabilities.v1.d.ts +106 -0
- package/dist/edge/expression-capabilities.v1.js +165 -0
- package/dist/internal/domain/edge/compose.d.ts +24 -0
- package/dist/internal/domain/edge/compose.js +89 -0
- package/dist/internal/domain/edge/identity.js +1 -1
- package/dist/internal/domain/edge/index.d.ts +2 -0
- package/dist/internal/domain/edge/index.js +2 -0
- package/dist/internal/domain/edge/mimic_token.d.ts +13 -0
- package/dist/internal/domain/edge/mimic_token.js +58 -0
- package/dist/internal/domain/edge/personalization-core.d.ts +69 -5
- package/dist/internal/domain/edge/personalization-core.js +693 -58
- package/dist/internal/domain/edge/record/codec.d.ts +5 -2
- package/dist/internal/domain/edge/record/codec.js +85 -12
- package/dist/internal/domain/edge/resolve.js +38 -18
- package/dist/internal/domain/edge/testdata/flat_only_validator_v1.d.ts +10 -0
- package/dist/internal/domain/edge/testdata/flat_only_validator_v1.js +253 -0
- package/dist/internal/domain/edge/types.d.ts +52 -9
- package/dist/internal/domain/edge/types.js +3 -3
- package/dist/mimic/core.d.ts +2 -0
- package/dist/mimic/core.js +31 -0
- package/dist/next/error-seat-payload.js +1 -1
- package/dist/next/hosted-page-target.d.ts +7 -0
- package/dist/next/hosted-page-target.js +39 -0
- package/dist/next/index.d.ts +0 -1
- package/dist/next/index.js +0 -1
- package/dist/next/page.d.ts +29 -6
- package/dist/next/page.js +47 -25
- package/dist/next/server.js +28 -1
- package/dist/next/vercel-edge-projection-v1.d.ts +0 -9
- package/dist/next/vercel-edge-projection-v1.js +9 -539
- package/dist/preview/core.d.ts +1 -0
- package/dist/preview/core.js +27 -0
- package/dist/providers/s3.js +4 -3
- package/dist/react/VideoPlayer.d.ts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/types/block-ref.d.ts +2 -0
- package/dist/types/block-ref.js +1 -0
- package/package.json +16 -7
- package/src/cli/compile-registry.mjs +24 -10
- package/src/cli/validate-content.mjs +52 -13
- package/src/types/block-ref.test.ts +45 -0
- package/src/types/block-ref.ts +2 -0
- package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
- package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
- package/templates/astro/starter-content/src/fixtures/sections.ts +2 -1
- package/templates/astro/template/package.json.tmpl +1 -1
- package/templates/astro/template/src/layouts/SiteLayout.astro +2 -2
- package/templates/common/.agents/skills/aci/SKILL.md +6 -0
- package/templates/common/.agents/skills/authoring/SKILL.md +11 -3
- package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
- package/templates/common/.agents/skills/dam-delivery/SKILL.md +228 -0
- package/templates/common/.agents/skills/dam-delivery/references/parameter-contract.md +107 -0
- package/templates/common/.agents/skills/dam-delivery/references/permalink-grammar.md +131 -0
- package/templates/common/.content/pages/{home/_index.json → _index.json} +10 -0
- package/templates/common/.content/schemas/site.schema.json +1 -1
- package/templates/common/.nvmrc +1 -1
- package/templates/common/starter-content/.content/.rules-baseline.json +8 -0
- package/templates/common/starter-content/.content/config/site.json +1 -1
- package/templates/common/starter-content/.content/content-rules/no-public-resource-paths.json +6 -0
- package/templates/common/starter-content/.content/fragments/navbar/_index.json +23 -20
- package/templates/common/starter-content/.content/fragments/site-footer/_index.json +59 -56
- package/templates/common/starter-content/.content/pages/404/_index.json +1 -0
- package/templates/common/starter-content/.content/pages/about/_index.json +3 -0
- package/templates/common/starter-content/.content/pages/error/_index.json +1 -0
- package/templates/common/starter-content/.content/pages/home/_index.json +9 -0
- package/templates/common/starter-content/.content/patterns/components/button.json +55 -0
- package/templates/common/starter-content/.content/patterns/components/container.json +62 -0
- package/templates/common/starter-content/.content/patterns/components/feature_card.json +30 -0
- package/templates/common/starter-content/.content/patterns/components/feature_grid.json +57 -0
- package/templates/common/starter-content/.content/patterns/components/home_hero.json +142 -0
- package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +42 -0
- package/templates/common/starter-content/.content/patterns/components/site-footer.json +203 -0
- package/templates/common/starter-content/.content/patterns/components/site-navigation.json +115 -0
- package/templates/common/starter-content/.content/patterns/components/stats_bar.json +45 -0
- package/templates/common/starter-content/.content/patterns/layouts/default.json +14 -0
- package/templates/common/starter-content/.content/schemas/site.schema.json +1 -1
- package/templates/nextjs/template/.aci.yaml +10 -0
- package/templates/nextjs/template/package.json.tmpl +4 -3
- package/templates/nextjs/template/src/app/_components/c15t-bridge.ts +30 -0
- package/templates/nextjs/template/src/app/_components/c15t-consent-ui.tsx +37 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.css +63 -0
- package/templates/nextjs/template/src/app/_components/consent-manager.tsx +76 -0
- package/templates/nextjs/template/src/app/layout.tsx +2 -0
- package/dist/edge/personalization-core.d.ts +0 -11
- package/dist/edge/personalization-core.js +0 -269
- package/dist/next/collect-route.d.ts +0 -19
- package/dist/next/collect-route.js +0 -459
- 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.d.ts +0 -134
- package/dist/next/personalization-host.js +0 -270
- package/dist/next/root-layout.d.ts +0 -4
- package/dist/next/root-layout.js +0 -5
- 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-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/templates/common/.content/overlays/audience/preference-blue/pages/{home/_index.json → _index.json} +0 -0
- /package/templates/common/.content/overlays/audience/preference-red/pages/{home/_index.json → _index.json} +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export declare const expressionCapabilities: {
|
|
2
|
+
readonly schemaVersion: 1;
|
|
3
|
+
readonly contract: "personalization-expression";
|
|
4
|
+
readonly maxConditions: 64;
|
|
5
|
+
readonly maxDepth: 5;
|
|
6
|
+
readonly maxInValues: 32;
|
|
7
|
+
readonly grammars: {
|
|
8
|
+
readonly number: "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$";
|
|
9
|
+
readonly timestamp: "^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$";
|
|
10
|
+
};
|
|
11
|
+
readonly operators: readonly [{
|
|
12
|
+
readonly name: "equals";
|
|
13
|
+
readonly operand: "scalar";
|
|
14
|
+
readonly compact: "eq";
|
|
15
|
+
}, {
|
|
16
|
+
readonly name: "contains";
|
|
17
|
+
readonly operand: "scalar";
|
|
18
|
+
readonly compact: "ct";
|
|
19
|
+
}, {
|
|
20
|
+
readonly name: "startsWith";
|
|
21
|
+
readonly operand: "scalar";
|
|
22
|
+
readonly compact: "sw";
|
|
23
|
+
}, {
|
|
24
|
+
readonly name: "exists";
|
|
25
|
+
readonly operand: "optionalBoolean";
|
|
26
|
+
readonly compact: "ex";
|
|
27
|
+
}, {
|
|
28
|
+
readonly name: "greaterThan";
|
|
29
|
+
readonly operand: "number";
|
|
30
|
+
readonly compact: "gt";
|
|
31
|
+
}, {
|
|
32
|
+
readonly name: "greaterThanOrEqual";
|
|
33
|
+
readonly operand: "number";
|
|
34
|
+
readonly compact: "ge";
|
|
35
|
+
}, {
|
|
36
|
+
readonly name: "lessThan";
|
|
37
|
+
readonly operand: "number";
|
|
38
|
+
readonly compact: "lt";
|
|
39
|
+
}, {
|
|
40
|
+
readonly name: "lessThanOrEqual";
|
|
41
|
+
readonly operand: "number";
|
|
42
|
+
readonly compact: "le";
|
|
43
|
+
}, {
|
|
44
|
+
readonly name: "before";
|
|
45
|
+
readonly operand: "timestamp";
|
|
46
|
+
readonly compact: "bf";
|
|
47
|
+
}, {
|
|
48
|
+
readonly name: "after";
|
|
49
|
+
readonly operand: "timestamp";
|
|
50
|
+
readonly compact: "af";
|
|
51
|
+
}, {
|
|
52
|
+
readonly name: "between";
|
|
53
|
+
readonly operand: "range";
|
|
54
|
+
readonly compact: "bt";
|
|
55
|
+
readonly range: {
|
|
56
|
+
readonly memberTypes: readonly ["number", "timestamp"];
|
|
57
|
+
readonly homogeneous: true;
|
|
58
|
+
readonly inclusive: true;
|
|
59
|
+
};
|
|
60
|
+
}, {
|
|
61
|
+
readonly name: "in";
|
|
62
|
+
readonly operand: "scalarList";
|
|
63
|
+
readonly compact: "in";
|
|
64
|
+
readonly maxValues: 32;
|
|
65
|
+
}];
|
|
66
|
+
readonly scopes: readonly [{
|
|
67
|
+
readonly scope: "request";
|
|
68
|
+
readonly allowsEmpty: false;
|
|
69
|
+
readonly fields: readonly [{
|
|
70
|
+
readonly field: "referrer";
|
|
71
|
+
}, {
|
|
72
|
+
readonly field: "query.*";
|
|
73
|
+
}, {
|
|
74
|
+
readonly field: "header.*";
|
|
75
|
+
}, {
|
|
76
|
+
readonly field: "cookie.*";
|
|
77
|
+
}, {
|
|
78
|
+
readonly field: "geo.*";
|
|
79
|
+
}, {
|
|
80
|
+
readonly field: "utm.*";
|
|
81
|
+
}, {
|
|
82
|
+
readonly field: "device.class";
|
|
83
|
+
readonly operators: readonly ["equals", "in", "exists"];
|
|
84
|
+
readonly values: readonly ["desktop", "mobile", "tablet", "unknown"];
|
|
85
|
+
}, {
|
|
86
|
+
readonly field: "time.now";
|
|
87
|
+
readonly operators: readonly ["before", "after", "between", "exists"];
|
|
88
|
+
readonly instant: true;
|
|
89
|
+
}, {
|
|
90
|
+
readonly field: "audience.memberships";
|
|
91
|
+
readonly operators: readonly ["equals", "in", "exists"];
|
|
92
|
+
}];
|
|
93
|
+
}, {
|
|
94
|
+
readonly scope: "event";
|
|
95
|
+
readonly allowsEmpty: true;
|
|
96
|
+
readonly fields: readonly [{
|
|
97
|
+
readonly field: "referrer";
|
|
98
|
+
}, {
|
|
99
|
+
readonly field: "page.path";
|
|
100
|
+
}, {
|
|
101
|
+
readonly field: "properties.*";
|
|
102
|
+
}, {
|
|
103
|
+
readonly field: "utm.*";
|
|
104
|
+
}];
|
|
105
|
+
}];
|
|
106
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// GENERATED by scripts/generate-expression-capabilities.go — DO NOT EDIT.
|
|
2
|
+
// Regenerate: make expression-capabilities
|
|
3
|
+
//
|
|
4
|
+
// The published capability discovery schema for the personalization
|
|
5
|
+
// expression contract (ADR 0038), generated from the Go vocabulary tables
|
|
6
|
+
// and drift-gated in CI against the canonical JSON artifact at
|
|
7
|
+
// internal/domain/personalization/expression/schema/.
|
|
8
|
+
export const expressionCapabilities = {
|
|
9
|
+
"schemaVersion": 1,
|
|
10
|
+
"contract": "personalization-expression",
|
|
11
|
+
"maxConditions": 64,
|
|
12
|
+
"maxDepth": 5,
|
|
13
|
+
"maxInValues": 32,
|
|
14
|
+
"grammars": {
|
|
15
|
+
"number": "^-?(0|[1-9][0-9]*)(\\.[0-9]+)?([eE][+-]?[0-9]+)?$",
|
|
16
|
+
"timestamp": "^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d{1,9})?(Z|[+-]\\d{2}:\\d{2})$"
|
|
17
|
+
},
|
|
18
|
+
"operators": [
|
|
19
|
+
{
|
|
20
|
+
"name": "equals",
|
|
21
|
+
"operand": "scalar",
|
|
22
|
+
"compact": "eq"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "contains",
|
|
26
|
+
"operand": "scalar",
|
|
27
|
+
"compact": "ct"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"name": "startsWith",
|
|
31
|
+
"operand": "scalar",
|
|
32
|
+
"compact": "sw"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"name": "exists",
|
|
36
|
+
"operand": "optionalBoolean",
|
|
37
|
+
"compact": "ex"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "greaterThan",
|
|
41
|
+
"operand": "number",
|
|
42
|
+
"compact": "gt"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "greaterThanOrEqual",
|
|
46
|
+
"operand": "number",
|
|
47
|
+
"compact": "ge"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "lessThan",
|
|
51
|
+
"operand": "number",
|
|
52
|
+
"compact": "lt"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "lessThanOrEqual",
|
|
56
|
+
"operand": "number",
|
|
57
|
+
"compact": "le"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"name": "before",
|
|
61
|
+
"operand": "timestamp",
|
|
62
|
+
"compact": "bf"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "after",
|
|
66
|
+
"operand": "timestamp",
|
|
67
|
+
"compact": "af"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "between",
|
|
71
|
+
"operand": "range",
|
|
72
|
+
"compact": "bt",
|
|
73
|
+
"range": {
|
|
74
|
+
"memberTypes": [
|
|
75
|
+
"number",
|
|
76
|
+
"timestamp"
|
|
77
|
+
],
|
|
78
|
+
"homogeneous": true,
|
|
79
|
+
"inclusive": true
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "in",
|
|
84
|
+
"operand": "scalarList",
|
|
85
|
+
"compact": "in",
|
|
86
|
+
"maxValues": 32
|
|
87
|
+
}
|
|
88
|
+
],
|
|
89
|
+
"scopes": [
|
|
90
|
+
{
|
|
91
|
+
"scope": "request",
|
|
92
|
+
"allowsEmpty": false,
|
|
93
|
+
"fields": [
|
|
94
|
+
{
|
|
95
|
+
"field": "referrer"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"field": "query.*"
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
"field": "header.*"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"field": "cookie.*"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"field": "geo.*"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"field": "utm.*"
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"field": "device.class",
|
|
114
|
+
"operators": [
|
|
115
|
+
"equals",
|
|
116
|
+
"in",
|
|
117
|
+
"exists"
|
|
118
|
+
],
|
|
119
|
+
"values": [
|
|
120
|
+
"desktop",
|
|
121
|
+
"mobile",
|
|
122
|
+
"tablet",
|
|
123
|
+
"unknown"
|
|
124
|
+
]
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"field": "time.now",
|
|
128
|
+
"operators": [
|
|
129
|
+
"before",
|
|
130
|
+
"after",
|
|
131
|
+
"between",
|
|
132
|
+
"exists"
|
|
133
|
+
],
|
|
134
|
+
"instant": true
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"field": "audience.memberships",
|
|
138
|
+
"operators": [
|
|
139
|
+
"equals",
|
|
140
|
+
"in",
|
|
141
|
+
"exists"
|
|
142
|
+
]
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"scope": "event",
|
|
148
|
+
"allowsEmpty": true,
|
|
149
|
+
"fields": [
|
|
150
|
+
{
|
|
151
|
+
"field": "referrer"
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"field": "page.path"
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"field": "properties.*"
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"field": "utm.*"
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
]
|
|
165
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface ObjectReader {
|
|
2
|
+
readObject(path: string): Promise<Uint8Array>;
|
|
3
|
+
}
|
|
4
|
+
export interface ComposeObjectRef {
|
|
5
|
+
path: string;
|
|
6
|
+
digest: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ComposeSlot {
|
|
9
|
+
name: string;
|
|
10
|
+
required: boolean;
|
|
11
|
+
placeholder: string;
|
|
12
|
+
parts: ComposeObjectRef[];
|
|
13
|
+
}
|
|
14
|
+
export interface ComposeEnvelope {
|
|
15
|
+
schema: 'gradial.compose-envelope.v1';
|
|
16
|
+
algorithm: 'aci-html-compose-v1';
|
|
17
|
+
shell: ComposeObjectRef;
|
|
18
|
+
slots: ComposeSlot[];
|
|
19
|
+
}
|
|
20
|
+
export declare function composeEnvelope(reader: ObjectReader, envelopePath: string, envelopeDigest: string): Promise<{
|
|
21
|
+
html: string;
|
|
22
|
+
contentType: string;
|
|
23
|
+
}>;
|
|
24
|
+
export declare function validateEnvelope(envelope: unknown): ComposeEnvelope;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const decoder = new TextDecoder('utf-8', { fatal: true });
|
|
2
|
+
export async function composeEnvelope(reader, envelopePath, envelopeDigest) {
|
|
3
|
+
const envelopeBytes = await reader.readObject(envelopePath);
|
|
4
|
+
await requireDigest(envelopeBytes, envelopeDigest, 'compose envelope');
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = JSON.parse(decoder.decode(envelopeBytes));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
throw new Error('compose envelope is malformed');
|
|
11
|
+
}
|
|
12
|
+
const envelope = validateEnvelope(parsed);
|
|
13
|
+
const shellBytes = await reader.readObject(envelope.shell.path);
|
|
14
|
+
await requireDigest(shellBytes, envelope.shell.digest, 'compose shell');
|
|
15
|
+
let html = decoder.decode(shellBytes);
|
|
16
|
+
for (const slot of envelope.slots) {
|
|
17
|
+
if (occurrences(html, slot.placeholder) !== 1) {
|
|
18
|
+
throw new Error(`compose slot ${JSON.stringify(slot.name)} placeholder must occur exactly once`);
|
|
19
|
+
}
|
|
20
|
+
if (slot.required && slot.parts.length === 0) {
|
|
21
|
+
throw new Error(`compose slot ${JSON.stringify(slot.name)} is required`);
|
|
22
|
+
}
|
|
23
|
+
const parts = [];
|
|
24
|
+
for (const part of slot.parts) {
|
|
25
|
+
const bytes = await reader.readObject(part.path);
|
|
26
|
+
await requireDigest(bytes, part.digest, `compose slot ${slot.name}`);
|
|
27
|
+
parts.push(decoder.decode(bytes));
|
|
28
|
+
}
|
|
29
|
+
html = html.replace(slot.placeholder, parts.join(''));
|
|
30
|
+
}
|
|
31
|
+
if (html.includes('<!--ACI_SLOT:')) {
|
|
32
|
+
throw new Error('compose shell has unresolved slots');
|
|
33
|
+
}
|
|
34
|
+
return { html, contentType: 'text/html; charset=utf-8' };
|
|
35
|
+
}
|
|
36
|
+
export function validateEnvelope(envelope) {
|
|
37
|
+
if (!envelope || typeof envelope !== 'object') {
|
|
38
|
+
throw new Error('compose envelope is malformed');
|
|
39
|
+
}
|
|
40
|
+
const obj = envelope;
|
|
41
|
+
if (obj.schema !== 'gradial.compose-envelope.v1'
|
|
42
|
+
|| obj.algorithm !== 'aci-html-compose-v1'
|
|
43
|
+
|| !Array.isArray(obj.slots)) {
|
|
44
|
+
throw new Error('compose envelope is malformed');
|
|
45
|
+
}
|
|
46
|
+
const env = envelope;
|
|
47
|
+
validateRef(env.shell);
|
|
48
|
+
const names = new Set();
|
|
49
|
+
for (const slot of env.slots) {
|
|
50
|
+
if (!slot || !/^[A-Za-z][A-Za-z0-9_-]*$/.test(slot.name) || names.has(slot.name)) {
|
|
51
|
+
throw new Error('compose slot is malformed');
|
|
52
|
+
}
|
|
53
|
+
names.add(slot.name);
|
|
54
|
+
if (slot.placeholder !== `<!--ACI_SLOT:${slot.name}-->` || !Array.isArray(slot.parts)) {
|
|
55
|
+
throw new Error(`compose slot ${JSON.stringify(slot.name)} is malformed`);
|
|
56
|
+
}
|
|
57
|
+
for (const part of slot.parts)
|
|
58
|
+
validateRef(part);
|
|
59
|
+
}
|
|
60
|
+
return env;
|
|
61
|
+
}
|
|
62
|
+
function validateRef(ref) {
|
|
63
|
+
const path = String(ref?.path || '');
|
|
64
|
+
if (!path || path.includes('\\') || path.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
65
|
+
throw new Error('compose object path is invalid');
|
|
66
|
+
}
|
|
67
|
+
if (!/^[0-9a-f]{64}$/.test(String(ref.digest || '').toLowerCase())) {
|
|
68
|
+
throw new Error('compose object digest is invalid');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function requireDigest(bytes, want, label) {
|
|
72
|
+
const ab = new ArrayBuffer(bytes.byteLength);
|
|
73
|
+
new Uint8Array(ab).set(bytes);
|
|
74
|
+
const actual = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', ab)), (byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
75
|
+
if (actual !== want.toLowerCase())
|
|
76
|
+
throw new Error(`${label} digest mismatch`);
|
|
77
|
+
}
|
|
78
|
+
function occurrences(value, needle) {
|
|
79
|
+
let count = 0;
|
|
80
|
+
let offset = 0;
|
|
81
|
+
while (true) {
|
|
82
|
+
offset = value.indexOf(needle, offset);
|
|
83
|
+
if (offset < 0)
|
|
84
|
+
break;
|
|
85
|
+
count += 1;
|
|
86
|
+
offset += needle.length;
|
|
87
|
+
}
|
|
88
|
+
return count;
|
|
89
|
+
}
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// - Cookie mutation helpers
|
|
9
9
|
//
|
|
10
10
|
// Shared by every static edge adapter; this logic has no SDK host wrapper.
|
|
11
|
-
// internal/domain/audience/runtime/
|
|
11
|
+
// internal/domain/audience/runtime/collect.ts imports from here as well.
|
|
12
12
|
const encoder = new TextEncoder();
|
|
13
13
|
const decoder = new TextDecoder();
|
|
14
14
|
// ── Constants ──
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface MimicClaims {
|
|
2
|
+
scope: string;
|
|
3
|
+
siteId: string;
|
|
4
|
+
aud?: string[];
|
|
5
|
+
seg?: string[];
|
|
6
|
+
sub?: string;
|
|
7
|
+
jti?: string;
|
|
8
|
+
iat: string;
|
|
9
|
+
expiresAt: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const MIMIC_MAX_MEMBERSHIPS = 32;
|
|
12
|
+
export declare const MIMIC_MAX_TOKEN_BYTES = 2048;
|
|
13
|
+
export declare function validateMimicToken(token: string, signingKey: string, siteId: string): Promise<MimicClaims | null>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Mimic token validation for the edge runtime (specs/features/audience-mimic).
|
|
2
|
+
//
|
|
3
|
+
// Same HS256 envelope and signing key as preview; the scope claim makes the
|
|
4
|
+
// tokens non-interchangeable (D-6). This validator is the twin of
|
|
5
|
+
// internal/domain/edge/routing/mimic.go, pinned by the shared vector file
|
|
6
|
+
// (internal/domain/edge/routing/testdata/mimic_token_vectors.json).
|
|
7
|
+
export const MIMIC_MAX_MEMBERSHIPS = 32;
|
|
8
|
+
export const MIMIC_MAX_TOKEN_BYTES = 2048;
|
|
9
|
+
export async function validateMimicToken(token, signingKey, siteId) {
|
|
10
|
+
if (!token || token.length > MIMIC_MAX_TOKEN_BYTES || !signingKey || !siteId)
|
|
11
|
+
return null;
|
|
12
|
+
const parts = token.split('.');
|
|
13
|
+
if (parts.length !== 3)
|
|
14
|
+
return null;
|
|
15
|
+
const signingInput = parts[0] + '.' + parts[1];
|
|
16
|
+
let signatureBytes;
|
|
17
|
+
try {
|
|
18
|
+
signatureBytes = base64URLDecode(parts[2]);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(signingKey), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
24
|
+
const valid = await crypto.subtle.verify('HMAC', key, signatureBytes, new TextEncoder().encode(signingInput));
|
|
25
|
+
if (!valid)
|
|
26
|
+
return null;
|
|
27
|
+
let claims;
|
|
28
|
+
try {
|
|
29
|
+
claims = JSON.parse(new TextDecoder().decode(base64URLDecode(parts[1])));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (claims.scope !== 'mimic')
|
|
35
|
+
return null;
|
|
36
|
+
if (typeof claims.siteId !== 'string' || claims.siteId !== siteId)
|
|
37
|
+
return null;
|
|
38
|
+
const aud = Array.isArray(claims.aud) ? claims.aud : [];
|
|
39
|
+
const seg = Array.isArray(claims.seg) ? claims.seg : [];
|
|
40
|
+
if (aud.length + seg.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
if (aud.length > MIMIC_MAX_MEMBERSHIPS || seg.length > MIMIC_MAX_MEMBERSHIPS)
|
|
43
|
+
return null;
|
|
44
|
+
const expiresAt = new Date(claims.expiresAt);
|
|
45
|
+
if (Number.isNaN(expiresAt.getTime()) || expiresAt.getTime() <= Date.now())
|
|
46
|
+
return null;
|
|
47
|
+
return { ...claims, aud, seg };
|
|
48
|
+
}
|
|
49
|
+
function base64URLDecode(value) {
|
|
50
|
+
const padded = value.replace(/-/g, '+').replace(/_/g, '/')
|
|
51
|
+
+ '='.repeat((4 - value.length % 4) % 4);
|
|
52
|
+
const binary = atob(padded);
|
|
53
|
+
const bytes = new Uint8Array(binary.length);
|
|
54
|
+
for (let index = 0; index < binary.length; index += 1) {
|
|
55
|
+
bytes[index] = binary.charCodeAt(index);
|
|
56
|
+
}
|
|
57
|
+
return bytes;
|
|
58
|
+
}
|
|
@@ -1,6 +1,51 @@
|
|
|
1
|
-
import type { PersonalizationRequestContext, PersonalizationClaims,
|
|
2
|
-
export declare function selectPersonalizationVariant(request: PersonalizationRequestContext, route: PersonalizationRoute | null, claims: PersonalizationClaims | null, experiments?: Record<string, PersonalizationExperiment>, identity?: PersonalizationIdentity): PersonalizationVariant | null;
|
|
1
|
+
import type { PersonalizationRequestContext, PersonalizationClaims, PersonalizationRoute, PersonalizationExperiment, PersonalizationIdentity, PersonalizationDecision, ClassificationData, Classification, EdgeRouteEntry } from './types.js';
|
|
3
2
|
export declare function resolvePersonalizationDecision(request: PersonalizationRequestContext, route: PersonalizationRoute | null, claims: PersonalizationClaims | null, experiments?: Record<string, PersonalizationExperiment>, identity?: PersonalizationIdentity): PersonalizationDecision;
|
|
3
|
+
export type ExpressionFactStatus = 'present' | 'absent' | 'unavailable';
|
|
4
|
+
export interface ExpressionFact {
|
|
5
|
+
status: ExpressionFactStatus;
|
|
6
|
+
values?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface ExpressionDiagnostic {
|
|
9
|
+
code: string;
|
|
10
|
+
path: string;
|
|
11
|
+
scope: 'request';
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ExpressionLeafTrace {
|
|
15
|
+
path: string;
|
|
16
|
+
group: 'all' | 'any';
|
|
17
|
+
index: number;
|
|
18
|
+
field: string;
|
|
19
|
+
operator: string;
|
|
20
|
+
status: ExpressionFactStatus;
|
|
21
|
+
matched: boolean;
|
|
22
|
+
reason: 'matched' | 'not_matched' | 'absent' | 'unavailable';
|
|
23
|
+
}
|
|
24
|
+
export interface ExpressionGroupTrace {
|
|
25
|
+
path: string;
|
|
26
|
+
group: 'all' | 'any';
|
|
27
|
+
total: number;
|
|
28
|
+
matched: number;
|
|
29
|
+
passed: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface RequestExpressionResult {
|
|
32
|
+
qualified: boolean;
|
|
33
|
+
diagnostics: ExpressionDiagnostic[];
|
|
34
|
+
trace: {
|
|
35
|
+
scope: 'request';
|
|
36
|
+
empty: boolean;
|
|
37
|
+
groups: ExpressionGroupTrace[];
|
|
38
|
+
leaves: ExpressionLeafTrace[];
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Evaluate one request expression and produce the same redacted trace used for qualification. */
|
|
42
|
+
export declare function evaluateRequestExpression(rule: {
|
|
43
|
+
all?: unknown[];
|
|
44
|
+
any?: unknown[];
|
|
45
|
+
}, resolve: (field: string) => ExpressionFact): RequestExpressionResult;
|
|
46
|
+
export declare const EXPRESSION_OPERATORS: string[];
|
|
47
|
+
export declare const EXPRESSION_NUMBER_GRAMMAR: RegExp;
|
|
48
|
+
export declare const EXPRESSION_TIMESTAMP_GRAMMAR: RegExp;
|
|
4
49
|
/**
|
|
5
50
|
* Phase 1: Classify the visitor against all release-level rules.
|
|
6
51
|
*
|
|
@@ -8,17 +53,36 @@ export declare function resolvePersonalizationDecision(request: PersonalizationR
|
|
|
8
53
|
* @param data Release-level classification data (audiences, segments, experiments)
|
|
9
54
|
* @param identity Visitor identity (for experiment bucketing). Pass undefined
|
|
10
55
|
* if no identity or privacy denied.
|
|
56
|
+
* @param claims The verified claims the caller already holds (eligibility
|
|
57
|
+
* gate input, ASE-F4). Undefined/null means "no claims" —
|
|
58
|
+
* gated experiments are skipped.
|
|
11
59
|
* @returns Classification — matched audiences, segments, and experiment arm assignments
|
|
12
60
|
*/
|
|
13
|
-
export declare function classify(ctx: PersonalizationRequestContext, data: ClassificationData, identity?: PersonalizationIdentity): Classification;
|
|
61
|
+
export declare function classify(ctx: PersonalizationRequestContext, data: ClassificationData, identity?: PersonalizationIdentity, claims?: PersonalizationClaims | null): Classification;
|
|
14
62
|
/**
|
|
15
63
|
* Phase 2: Decide the winning variant for a specific route.
|
|
16
64
|
*
|
|
17
65
|
* Uses the classification from Phase 1 for cheap set-membership lookups.
|
|
18
|
-
*
|
|
66
|
+
* Audience/segment rules were already evaluated in Phase 1; the one rule
|
|
67
|
+
* evaluated here is an experiment arm variant's own rule, because the arm
|
|
68
|
+
* assignment is candidacy, not entitlement (ADR 0040).
|
|
19
69
|
*
|
|
20
70
|
* @param record The route entry (path, dispatch, variants)
|
|
21
71
|
* @param classification The visitor's classification from Phase 1
|
|
72
|
+
* @param ctx The request context (signal facts + rejection sink)
|
|
73
|
+
* @param claims The verified claims the caller already holds — the
|
|
74
|
+
* same object passed to classify() — so an arm variant's
|
|
75
|
+
* audience.memberships rule resolves identically to the
|
|
76
|
+
* resolver path and fails closed without it (ADR 0039).
|
|
22
77
|
* @returns The decision — winning variant + experiment assignment, or nulls
|
|
23
78
|
*/
|
|
24
|
-
export declare function decide(record: EdgeRouteEntry, classification: Classification, ctx: PersonalizationRequestContext): PersonalizationDecision;
|
|
79
|
+
export declare function decide(record: EdgeRouteEntry, classification: Classification, ctx: PersonalizationRequestContext, claims?: PersonalizationClaims | null): PersonalizationDecision;
|
|
80
|
+
export declare const DEVICE_CLASSES: string[];
|
|
81
|
+
/**
|
|
82
|
+
* Classifies the request's device class from its own headers. Returns '' when
|
|
83
|
+
* the request carried neither User-Agent nor Sec-CH-UA-Mobile, which the fact
|
|
84
|
+
* resolver reports as ABSENT — the same status the missing header itself
|
|
85
|
+
* resolves to, and deliberately distinct from the 'unknown' class, which means
|
|
86
|
+
* classified and unrecognized.
|
|
87
|
+
*/
|
|
88
|
+
export declare function deviceClassFromHeaders(headers: Record<string, string>): string;
|