@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.
Files changed (123) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +4 -4
  3. package/dist/astro/preview.js +2 -17
  4. package/dist/audience-data-models.d.ts +289 -16
  5. package/dist/audience-data-models.js +99 -2
  6. package/dist/cli/compile-registry.d.mts +5 -1
  7. package/dist/cli/compile-registry.mjs +25 -10
  8. package/dist/cli/validate-content.d.mts +31 -1
  9. package/dist/cli/validate-content.mjs +50 -12
  10. package/dist/collect/aci.js +1 -1
  11. package/dist/collect/aci.js.sha256 +1 -1
  12. package/dist/collect/api.d.ts +3 -7
  13. package/dist/collect/api.js +2 -8
  14. package/dist/collect/batcher.d.ts +6 -5
  15. package/dist/collect/batcher.js +6 -5
  16. package/dist/collect/index.js +1 -1
  17. package/dist/content/provider.d.ts +1 -0
  18. package/dist/content/resolve-slots.d.ts +9 -2
  19. package/dist/content/resolve-slots.js +5 -3
  20. package/dist/content/types.d.ts +2 -0
  21. package/dist/edge/expression-capabilities.v1.d.ts +106 -0
  22. package/dist/edge/expression-capabilities.v1.js +165 -0
  23. package/dist/internal/domain/edge/compose.d.ts +24 -0
  24. package/dist/internal/domain/edge/compose.js +89 -0
  25. package/dist/internal/domain/edge/identity.js +1 -1
  26. package/dist/internal/domain/edge/index.d.ts +2 -0
  27. package/dist/internal/domain/edge/index.js +2 -0
  28. package/dist/internal/domain/edge/mimic_token.d.ts +13 -0
  29. package/dist/internal/domain/edge/mimic_token.js +58 -0
  30. package/dist/internal/domain/edge/personalization-core.d.ts +69 -5
  31. package/dist/internal/domain/edge/personalization-core.js +693 -58
  32. package/dist/internal/domain/edge/record/codec.d.ts +5 -2
  33. package/dist/internal/domain/edge/record/codec.js +85 -12
  34. package/dist/internal/domain/edge/resolve.js +38 -18
  35. package/dist/internal/domain/edge/testdata/flat_only_validator_v1.d.ts +10 -0
  36. package/dist/internal/domain/edge/testdata/flat_only_validator_v1.js +253 -0
  37. package/dist/internal/domain/edge/types.d.ts +52 -9
  38. package/dist/internal/domain/edge/types.js +3 -3
  39. package/dist/mimic/core.d.ts +2 -0
  40. package/dist/mimic/core.js +31 -0
  41. package/dist/next/error-seat-payload.js +1 -1
  42. package/dist/next/hosted-page-target.d.ts +7 -0
  43. package/dist/next/hosted-page-target.js +39 -0
  44. package/dist/next/index.d.ts +0 -1
  45. package/dist/next/index.js +0 -1
  46. package/dist/next/page.d.ts +29 -6
  47. package/dist/next/page.js +47 -25
  48. package/dist/next/server.js +28 -1
  49. package/dist/next/vercel-edge-projection-v1.d.ts +0 -9
  50. package/dist/next/vercel-edge-projection-v1.js +9 -539
  51. package/dist/preview/core.d.ts +1 -0
  52. package/dist/preview/core.js +27 -0
  53. package/dist/providers/s3.js +4 -3
  54. package/dist/react/VideoPlayer.d.ts +1 -1
  55. package/dist/react/index.d.ts +1 -1
  56. package/dist/types/block-ref.d.ts +2 -0
  57. package/dist/types/block-ref.js +1 -0
  58. package/package.json +16 -7
  59. package/src/cli/compile-registry.mjs +24 -10
  60. package/src/cli/validate-content.mjs +52 -13
  61. package/src/types/block-ref.test.ts +45 -0
  62. package/src/types/block-ref.ts +2 -0
  63. package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
  64. package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
  65. package/templates/astro/starter-content/src/fixtures/sections.ts +2 -1
  66. package/templates/astro/template/package.json.tmpl +1 -1
  67. package/templates/astro/template/src/layouts/SiteLayout.astro +2 -2
  68. package/templates/common/.agents/skills/aci/SKILL.md +6 -0
  69. package/templates/common/.agents/skills/authoring/SKILL.md +11 -3
  70. package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
  71. package/templates/common/.agents/skills/dam-delivery/SKILL.md +228 -0
  72. package/templates/common/.agents/skills/dam-delivery/references/parameter-contract.md +107 -0
  73. package/templates/common/.agents/skills/dam-delivery/references/permalink-grammar.md +131 -0
  74. package/templates/common/.content/pages/{home/_index.json → _index.json} +10 -0
  75. package/templates/common/.content/schemas/site.schema.json +1 -1
  76. package/templates/common/.nvmrc +1 -1
  77. package/templates/common/starter-content/.content/.rules-baseline.json +8 -0
  78. package/templates/common/starter-content/.content/config/site.json +1 -1
  79. package/templates/common/starter-content/.content/content-rules/no-public-resource-paths.json +6 -0
  80. package/templates/common/starter-content/.content/fragments/navbar/_index.json +23 -20
  81. package/templates/common/starter-content/.content/fragments/site-footer/_index.json +59 -56
  82. package/templates/common/starter-content/.content/pages/404/_index.json +1 -0
  83. package/templates/common/starter-content/.content/pages/about/_index.json +3 -0
  84. package/templates/common/starter-content/.content/pages/error/_index.json +1 -0
  85. package/templates/common/starter-content/.content/pages/home/_index.json +9 -0
  86. package/templates/common/starter-content/.content/patterns/components/button.json +55 -0
  87. package/templates/common/starter-content/.content/patterns/components/container.json +62 -0
  88. package/templates/common/starter-content/.content/patterns/components/feature_card.json +30 -0
  89. package/templates/common/starter-content/.content/patterns/components/feature_grid.json +57 -0
  90. package/templates/common/starter-content/.content/patterns/components/home_hero.json +142 -0
  91. package/templates/common/starter-content/.content/patterns/components/personalization_choice.json +42 -0
  92. package/templates/common/starter-content/.content/patterns/components/site-footer.json +203 -0
  93. package/templates/common/starter-content/.content/patterns/components/site-navigation.json +115 -0
  94. package/templates/common/starter-content/.content/patterns/components/stats_bar.json +45 -0
  95. package/templates/common/starter-content/.content/patterns/layouts/default.json +14 -0
  96. package/templates/common/starter-content/.content/schemas/site.schema.json +1 -1
  97. package/templates/nextjs/template/.aci.yaml +10 -0
  98. package/templates/nextjs/template/package.json.tmpl +4 -3
  99. package/templates/nextjs/template/src/app/_components/c15t-bridge.ts +30 -0
  100. package/templates/nextjs/template/src/app/_components/c15t-consent-ui.tsx +37 -0
  101. package/templates/nextjs/template/src/app/_components/consent-manager.css +63 -0
  102. package/templates/nextjs/template/src/app/_components/consent-manager.tsx +76 -0
  103. package/templates/nextjs/template/src/app/layout.tsx +2 -0
  104. package/dist/edge/personalization-core.d.ts +0 -11
  105. package/dist/edge/personalization-core.js +0 -269
  106. package/dist/next/collect-route.d.ts +0 -19
  107. package/dist/next/collect-route.js +0 -459
  108. package/dist/next/envelope-digest.d.ts +0 -21
  109. package/dist/next/envelope-digest.js +0 -58
  110. package/dist/next/middleware.d.ts +0 -40
  111. package/dist/next/middleware.js +0 -475
  112. package/dist/next/personalization-host.d.ts +0 -134
  113. package/dist/next/personalization-host.js +0 -270
  114. package/dist/next/root-layout.d.ts +0 -4
  115. package/dist/next/root-layout.js +0 -5
  116. package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
  117. package/dist/next/vercel-collect-sdk-route.js +0 -181
  118. package/dist/next/vercel-personalization.d.ts +0 -4
  119. package/dist/next/vercel-personalization.js +0 -4
  120. package/dist/next/vercel-s3-personalization.d.ts +0 -79
  121. package/dist/next/vercel-s3-personalization.js +0 -570
  122. /package/templates/common/.content/overlays/audience/preference-blue/pages/{home/_index.json → _index.json} +0 -0
  123. /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/identity.ts imports from here as well.
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 ──
@@ -1,4 +1,6 @@
1
1
  export * from './types.js';
2
2
  export * from './personalization-core.js';
3
3
  export * from './identity.js';
4
+ export * from './mimic_token.js';
5
+ export * from './compose.js';
4
6
  export { resolvePersonalizationRequest, } from './resolve.js';
@@ -3,4 +3,6 @@
3
3
  export * from './types.js';
4
4
  export * from './personalization-core.js';
5
5
  export * from './identity.js';
6
+ export * from './mimic_token.js';
7
+ export * from './compose.js';
6
8
  export { resolvePersonalizationRequest, } from './resolve.js';
@@ -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, PersonalizationVariant, PersonalizationRoute, PersonalizationExperiment, PersonalizationIdentity, PersonalizationDecision, ClassificationData, Classification, EdgeRouteEntry } from './types.js';
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
- * No rule evaluation — just set membership and priority comparison.
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;