@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
@@ -1,270 +0,0 @@
1
- import { resolvePersonalizationDecision, } from '../edge/personalization-core.js';
2
- import { parsePortableConsentEnvelope, portableConsentAllowsPersonalization, trimPortableConsentWhitespace, } from '../consent/portable-envelope.js';
3
- const DEFAULT_IDENTITY_TTL_SECONDS = 90 * 24 * 60 * 60;
4
- const DEFAULT_CLAIMS_TTL_SECONDS = 24 * 60 * 60;
5
- const textEncoder = new TextEncoder();
6
- export const CONTENT_PATH_HEADER = 'x-aci-content-path';
7
- export const CONTENT_PATH_CACHE_PARAM = '__aci_content_path';
8
- // Names the public route the edge decision was made FOR. The server render
9
- // applies the content-path selector only to lookups of this route; content
10
- // reads for any other route fall back to that route's base payload instead of
11
- // inheriting a selector that names a different route's content.
12
- export const ROUTED_PATH_HEADER = 'x-aci-routed-path';
13
- // Names the variant the edge decision selected. Like the content path and
14
- // routed path, it is a reserved dispatch signal: only the middleware may set
15
- // it, and a viewer-supplied value must never reach the SSR render (a bare
16
- // variant key without a matching ref is an integrity error there).
17
- export const VARIANT_KEY_HEADER = 'x-aci-variant-key';
18
- export class IdentityErasureFencedError extends Error {
19
- name = 'IdentityErasureFencedError';
20
- constructor() {
21
- super('identity collection is fenced by erasure');
22
- }
23
- }
24
- export async function resolvePersonalizationRequest(request, releaseId, config) {
25
- const requestHeaders = new Headers(request.headers);
26
- const cookies = parseCookieHeader(request.headers.get('cookie'));
27
- const consent = consentSignals(request, cookies);
28
- const mutations = [];
29
- const now = (config.now ?? (() => new Date()))();
30
- const secure = config.secureCookies ?? new URL(request.url).protocol === 'https:';
31
- // Universal minting (spec anonymous-experiments REQ-1 / ADR 0016):
32
- // identity existence is privacy-signal gated, never consent gated.
33
- // GPC/DNT deny FULLY — no mint, no resolve, even with a valid cookie or a
34
- // stored granting envelope (D-2). An unverifiable cookie self-heals by
35
- // re-minting (REQ-4). aci_m is issue-only (D-11): a resolve-path refresh
36
- // would put a Set-Cookie on every returning visitor's response, defeating
37
- // the any-Set-Cookie => private, no-store cache rule.
38
- let rawIdentity = consent.privacyDenied
39
- ? null
40
- : await verifyIdentityCookie(cookies.get('aci_id') || '', config.identityKeys);
41
- if (!consent.privacyDenied && !rawIdentity) {
42
- rawIdentity = randomIdentity(config.randomBytes);
43
- const signed = await signIdentity(rawIdentity, config.identityKeys.current);
44
- const maxAge = config.identityCookieTTLSeconds ?? DEFAULT_IDENTITY_TTL_SECONDS;
45
- mutations.push(cookieMutation('aci_id', signed, maxAge, now, secure, true));
46
- mutations.push(cookieMutation('aci_m', '1', maxAge, now, secure, false));
47
- }
48
- // The identity hash feeds experiment bucketing for EVERY identified
49
- // visitor. Claims are a signed cookie written by /collect; the EdgeRouter
50
- // never reads the customer object store to refresh or revoke them.
51
- let signedClaims = cookies.get('aci_claims') || '';
52
- let identityHash = '';
53
- if (rawIdentity) {
54
- identityHash = await identityHashHex('aci_id', rawIdentity);
55
- }
56
- if (rawIdentity && consent.allowsIdentityTier && signedClaims) {
57
- if (!await verifyClaims(signedClaims, config.claimsKeys || {}, now)) {
58
- signedClaims = '';
59
- }
60
- }
61
- if ((!consent.allowsIdentityTier || !rawIdentity || !signedClaims) && cookies.has('aci_claims')) {
62
- mutations.push(expireCookie('aci_claims', secure));
63
- }
64
- const manifest = await config.store.readEdgeManifest(releaseId);
65
- const route = manifest ? matchRoute(new URL(request.url).pathname, manifest) : null;
66
- let contentPath = route?.dispatch.contentPath || '';
67
- let variantKey = '';
68
- if (route) {
69
- const claims = consent.allowsIdentityTier
70
- ? await verifyClaims(signedClaims, config.claimsKeys || {}, now)
71
- : null;
72
- const decision = resolvePersonalizationDecision(toPersonalizationRequestContext(request), route, claims, manifest?.experiments, identityHash
73
- ? { usable: true, siteId: config.siteId, namespace: 'aci_id', identityHash }
74
- : undefined);
75
- if (decision.variant) {
76
- variantKey = decision.variant.variantKey;
77
- contentPath = decision.variant.dispatch.contentPath || contentPath;
78
- }
79
- if (decision.experiment) {
80
- requestHeaders.set('X-ACI-Experiment-ID', decision.experiment.experimentId);
81
- requestHeaders.set('X-ACI-Experiment-Arm', decision.experiment.armId);
82
- requestHeaders.set('X-ACI-Experiment-Allocation-Version', decision.experiment.allocationVersion);
83
- }
84
- const experiment = decision.experiment
85
- ? { id: decision.experiment.experimentId, arm: decision.experiment.armId, allocationVersion: decision.experiment.allocationVersion }
86
- : undefined;
87
- if (contentPath) {
88
- requestHeaders.set(CONTENT_PATH_HEADER, contentPath);
89
- requestHeaders.set(ROUTED_PATH_HEADER, normalizePath(new URL(request.url).pathname));
90
- }
91
- if (variantKey)
92
- requestHeaders.set(VARIANT_KEY_HEADER, variantKey);
93
- return { requestHeaders, contentPath, variantKey, experiment, cookies: mutations };
94
- }
95
- if (contentPath) {
96
- requestHeaders.set(CONTENT_PATH_HEADER, contentPath);
97
- requestHeaders.set(ROUTED_PATH_HEADER, normalizePath(new URL(request.url).pathname));
98
- }
99
- if (variantKey) {
100
- requestHeaders.set(VARIANT_KEY_HEADER, variantKey);
101
- }
102
- return { requestHeaders, contentPath, variantKey, cookies: mutations };
103
- }
104
- export function consentSignals(request, cookies) {
105
- const privacyDenied = request.headers.get('sec-gpc')?.trim() === '1'
106
- || request.headers.get('dnt')?.trim() === '1'
107
- || ['deny', 'opt-out', 'off'].includes((request.headers.get('x-gradial-privacy-override') || '').trim().toLowerCase());
108
- const headerConsent = request.headers.get('x-aci-consent');
109
- const hasHeaderConsent = trimPortableConsentWhitespace(headerConsent || '') !== '';
110
- const raw = hasHeaderConsent
111
- ? headerConsent
112
- : cookies.get('aci_consent') || '';
113
- const snapshot = parsePortableConsentEnvelope(raw, !hasHeaderConsent);
114
- const allows = portableConsentAllowsPersonalization(snapshot);
115
- return {
116
- hasConsent: snapshot !== null,
117
- snapshot,
118
- allowsIdentityTier: !privacyDenied && allows,
119
- privacyDenied,
120
- };
121
- }
122
- export function parseCookieHeader(value) {
123
- const cookies = new Map();
124
- for (const part of (value || '').split(';')) {
125
- const separator = part.indexOf('=');
126
- if (separator < 0)
127
- continue;
128
- const name = part.slice(0, separator).trim();
129
- if (name)
130
- cookies.set(name, part.slice(separator + 1).trim());
131
- }
132
- return cookies;
133
- }
134
- export async function verifyIdentityCookie(value, keys) {
135
- const parts = value.trim().split('.');
136
- if (parts.length !== 3 || parts.some((part) => part === ''))
137
- return null;
138
- const [rawID, kid, signature] = parts;
139
- const key = [keys.current, keys.previous].find((candidate) => candidate?.kid === kid);
140
- if (!key)
141
- return null;
142
- return await verifyHMAC(`${rawID}.${kid}`, signature, key.secret) ? rawID : null;
143
- }
144
- export async function sha256Hex(value) {
145
- const digest = await crypto.subtle.digest('SHA-256', textEncoder.encode(value));
146
- return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
147
- }
148
- export async function identityHashHex(namespace, value) {
149
- return sha256Hex(`${namespace.trim().toLowerCase()}\x00${value.trim()}`);
150
- }
151
- function matchRoute(pathname, manifest) {
152
- const normalized = normalizePath(pathname);
153
- if (manifest.routes[normalized])
154
- return manifest.routes[normalized];
155
- const candidates = Object.values(manifest.routes)
156
- .filter((route) => routePatternMatches(route.pattern, normalized))
157
- .sort((left, right) => right.priority - left.priority || left.pattern.localeCompare(right.pattern));
158
- return candidates[0] || null;
159
- }
160
- function normalizePath(value) {
161
- let path = value.trim() || '/';
162
- if (!path.startsWith('/'))
163
- path = `/${path}`;
164
- if (path.length > 1)
165
- path = path.replace(/\/+$/, '');
166
- return path;
167
- }
168
- function routePatternMatches(pattern, pathname) {
169
- pattern = normalizePath(pattern);
170
- if (pattern === pathname)
171
- return true;
172
- const expression = pattern
173
- .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
174
- .replace(/\\\[\\\[\\\.\\\.\\\.[^\]]+\\\]\\\]/g, '.*')
175
- .replace(/\\\[\\\.\\\.\\\.[^\]]+\\\]/g, '.+')
176
- .replace(/\\\[[^\]]+\\\]/g, '[^/]+')
177
- .replace(/:[^/]+/g, '[^/]+')
178
- .replace(/\\\*/g, '.*');
179
- try {
180
- return new RegExp(`^${expression}$`).test(pathname);
181
- }
182
- catch {
183
- return false;
184
- }
185
- }
186
- function toPersonalizationRequestContext(request) {
187
- const url = new URL(request.url);
188
- const cookies = parseCookieHeader(request.headers.get('cookie'));
189
- const query = {};
190
- url.searchParams.forEach((value, key) => {
191
- (query[key] ||= []).push(value);
192
- });
193
- const headers = Object.fromEntries(request.headers.entries());
194
- return {
195
- path: url.pathname,
196
- query,
197
- headers,
198
- cookies: Object.fromEntries(cookies.entries()),
199
- signals: {
200
- geo: {
201
- country: headers['x-vercel-ip-country'] || '',
202
- region: headers['x-vercel-ip-country-region'] || '',
203
- city: headers['x-vercel-ip-city'] || '',
204
- },
205
- },
206
- };
207
- }
208
- export async function verifyClaims(value, keys, now) {
209
- const parts = value.trim().split('.');
210
- if (parts.length !== 4 || parts[0] !== 'v1')
211
- return null;
212
- const [, kid, body, signature] = parts;
213
- const secret = keys[kid];
214
- if (!secret || !await verifyHMAC(`v1.${kid}.${body}`, signature, secret))
215
- return null;
216
- try {
217
- const payload = JSON.parse(new TextDecoder().decode(base64URLDecode(body)));
218
- if (!Number.isFinite(payload.exp) || payload.exp <= Math.floor(now.getTime() / 1000))
219
- return null;
220
- return payload;
221
- }
222
- catch {
223
- return null;
224
- }
225
- }
226
- async function signIdentity(rawID, key) {
227
- const signature = await hmac(`${rawID}.${key.kid}`, key.secret);
228
- return `${rawID}.${key.kid}.${signature}`;
229
- }
230
- function randomIdentity(random) {
231
- const bytes = random ? random(16) : crypto.getRandomValues(new Uint8Array(16));
232
- return base64URLEncode(bytes);
233
- }
234
- async function verifyHMAC(value, signature, secret) {
235
- try {
236
- const key = await crypto.subtle.importKey('raw', textEncoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
237
- return await crypto.subtle.verify('HMAC', key, base64URLDecode(signature), textEncoder.encode(value));
238
- }
239
- catch {
240
- return false;
241
- }
242
- }
243
- async function hmac(value, secret) {
244
- const key = await crypto.subtle.importKey('raw', textEncoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
245
- return base64URLEncode(new Uint8Array(await crypto.subtle.sign('HMAC', key, textEncoder.encode(value))));
246
- }
247
- function base64URLEncode(bytes) {
248
- let binary = '';
249
- for (const byte of bytes)
250
- binary += String.fromCharCode(byte);
251
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
252
- }
253
- function base64URLDecode(value) {
254
- const padded = value.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - value.length % 4) % 4);
255
- const binary = atob(padded);
256
- const bytes = new Uint8Array(binary.length);
257
- for (let index = 0; index < binary.length; index += 1)
258
- bytes[index] = binary.charCodeAt(index);
259
- return bytes;
260
- }
261
- function cookieMutation(name, value, maxAge, now, secure, httpOnly) {
262
- return {
263
- name,
264
- value,
265
- options: { path: '/', httpOnly, secure, sameSite: 'lax', maxAge, expires: new Date(now.getTime() + maxAge * 1000) },
266
- };
267
- }
268
- function expireCookie(name, secure) {
269
- return { name, value: '', options: { path: '/', httpOnly: true, secure, sameSite: 'lax', maxAge: 0, expires: new Date(0) } };
270
- }
@@ -1,4 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- export declare function RootLayout({ children }: {
3
- children: ReactNode;
4
- }): import("react").JSX.Element;
@@ -1,5 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { DevRefresh } from './dev-refresh.js';
3
- export function RootLayout({ children }) {
4
- return (_jsx("html", { lang: "en", children: _jsxs("body", { children: [children, _jsx(DevRefresh, {})] }) }));
5
- }
@@ -1,27 +0,0 @@
1
- export interface VercelCollectSDKRouteConfig {
2
- siteId: string;
3
- edgeConfig: string;
4
- region: string;
5
- roleArn: string;
6
- hostingBucket: string;
7
- hostingKeyPrefix?: string;
8
- }
9
- export interface VercelCollectSDKRouteDependencies {
10
- s3Client?: VercelCollectSDKS3Client;
11
- readEdgeConfig?: (connection: string, key: string) => Promise<unknown>;
12
- onError?: (error: unknown) => void;
13
- }
14
- export interface VercelCollectSDKS3Client {
15
- send(command: {
16
- input: unknown;
17
- }): Promise<unknown>;
18
- }
19
- export type VercelCollectSDKRoute = (request: Request) => Promise<Response>;
20
- /**
21
- * Creates the Node-only stable collect-SDK route used by ACI's injected
22
- * Vercel Function. The provider pointer selects the immutable release on every
23
- * uncached invocation, so an old code deployment remains correct while a
24
- * canonical activation converges through Edge Config and deployment promotion.
25
- */
26
- export declare function createVercelCollectSDKRoute(input: VercelCollectSDKRouteConfig, dependencies?: VercelCollectSDKRouteDependencies): VercelCollectSDKRoute;
27
- export declare function createVercelCollectSDKRouteFromEnv(env?: Record<string, string | undefined>, dependencies?: VercelCollectSDKRouteDependencies): VercelCollectSDKRoute;
@@ -1,181 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
3
- import { awsCredentialsProvider } from '@vercel/oidc-aws-credentials-provider';
4
- import { getUncachedEdgeConfigValue } from './edge-config.js';
5
- import { activeReleaseV1Key, decodeActiveReleaseV1, decodeEdgeRouteProjectionV1, edgeRoutesV1Key, } from './vercel-edge-projection-v1.js';
6
- const SHA256_HEX = /^[0-9a-f]{64}$/;
7
- /**
8
- * Creates the Node-only stable collect-SDK route used by ACI's injected
9
- * Vercel Function. The provider pointer selects the immutable release on every
10
- * uncached invocation, so an old code deployment remains correct while a
11
- * canonical activation converges through Edge Config and deployment promotion.
12
- */
13
- export function createVercelCollectSDKRoute(input, dependencies = {}) {
14
- const config = validateConfig(input);
15
- const readEdgeConfig = dependencies.readEdgeConfig || getUncachedEdgeConfigValue;
16
- const client = dependencies.s3Client || new S3Client({
17
- region: config.region,
18
- credentials: awsCredentialsProvider({
19
- roleArn: config.roleArn,
20
- clientConfig: { region: config.region },
21
- }),
22
- });
23
- const onError = dependencies.onError || ((error) => {
24
- console.error('[ACI collect SDK] route failed', error);
25
- });
26
- return async (request) => {
27
- const method = request.method.toUpperCase();
28
- if (method !== 'GET' && method !== 'HEAD') {
29
- return new Response('method not allowed', {
30
- status: 405,
31
- headers: {
32
- Allow: 'GET, HEAD',
33
- 'Cache-Control': 'private, no-store',
34
- },
35
- });
36
- }
37
- try {
38
- const active = decodeActiveReleaseV1(await readEdgeConfig(config.edgeConfig, activeReleaseV1Key(config.siteId)));
39
- const projection = decodeEdgeRouteProjectionV1(await readEdgeConfig(config.edgeConfig, edgeRoutesV1Key(config.siteId, active.releaseId)), {
40
- siteId: config.siteId,
41
- releaseId: active.releaseId,
42
- });
43
- if (!Object.prototype.hasOwnProperty.call(projection, 'collectSdk')) {
44
- return new Response(method === 'HEAD' ? null : 'collect SDK is not enabled for this release', {
45
- status: 404,
46
- headers: { 'Cache-Control': 'no-store' },
47
- });
48
- }
49
- const entry = validateCollectSDKEntry(projection.collectSdk);
50
- const body = await readSDKObject(client, config.hostingBucket, joinKey(config.hostingKeyPrefix, 'releases', active.releaseId, entry.path));
51
- const actualHash = createHash('sha256').update(body).digest('hex');
52
- if (actualHash !== entry.hash) {
53
- throw new Error(`collect SDK digest mismatch: got ${actualHash}, want ${entry.hash}`);
54
- }
55
- const etag = `"${entry.hash}"`;
56
- const headers = new Headers({
57
- 'Content-Type': 'text/javascript; charset=utf-8',
58
- 'Cache-Control': 'public, max-age=300, must-revalidate',
59
- ETag: etag,
60
- });
61
- if (ifNoneMatchSatisfied(request.headers.get('if-none-match'), etag)) {
62
- return new Response(null, { status: 304, headers });
63
- }
64
- headers.set('Content-Length', String(body.byteLength));
65
- const responseBody = body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength);
66
- return new Response(method === 'HEAD' ? null : responseBody, { status: 200, headers });
67
- }
68
- catch (error) {
69
- onError(error);
70
- return new Response(method === 'HEAD' ? null : 'collect SDK is temporarily unavailable', {
71
- status: 503,
72
- headers: { 'Cache-Control': 'private, no-store' },
73
- });
74
- }
75
- };
76
- }
77
- export function createVercelCollectSDKRouteFromEnv(env = process.env, dependencies = {}) {
78
- try {
79
- return createVercelCollectSDKRoute({
80
- siteId: requiredEnv(env, 'ACI_SITE_ID'),
81
- edgeConfig: requiredEnv(env, 'EDGE_CONFIG'),
82
- region: env.AWS_REGION || env.AWS_DEFAULT_REGION || '',
83
- roleArn: requiredEnv(env, 'AWS_ROLE_ARN'),
84
- hostingBucket: requiredEnv(env, 'HOSTING_BUCKET'),
85
- hostingKeyPrefix: env.HOSTING_KEY_PREFIX || env.GRADIAL_S3_KEY_PREFIX,
86
- }, dependencies);
87
- }
88
- catch (error) {
89
- (dependencies.onError || ((value) => {
90
- console.error('[ACI collect SDK] configuration failed', value);
91
- }))(error);
92
- return async (request) => new Response(request.method.toUpperCase() === 'HEAD' ? null : 'collect SDK is temporarily unavailable', { status: 503, headers: { 'Cache-Control': 'private, no-store' } });
93
- }
94
- }
95
- function validateConfig(input) {
96
- const siteId = requiredPathSegment(input.siteId, 'ACI site ID');
97
- const edgeConfig = input.edgeConfig.trim();
98
- const region = input.region.trim();
99
- const roleArn = input.roleArn.trim();
100
- const hostingBucket = input.hostingBucket.trim();
101
- if (!edgeConfig)
102
- throw new Error('Edge Config connection is required');
103
- if (!region)
104
- throw new Error('AWS region is required');
105
- if (!roleArn)
106
- throw new Error('AWS role ARN is required');
107
- if (!hostingBucket)
108
- throw new Error('hosting bucket is required');
109
- return {
110
- siteId,
111
- edgeConfig,
112
- region,
113
- roleArn,
114
- hostingBucket,
115
- hostingKeyPrefix: cleanKey(input.hostingKeyPrefix || ''),
116
- };
117
- }
118
- function parseRecord(value, label) {
119
- const parsed = typeof value === 'string' ? JSON.parse(value) : value;
120
- if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
121
- throw new Error(`${label} is unavailable or malformed`);
122
- }
123
- return parsed;
124
- }
125
- function validateCollectSDKEntry(value) {
126
- const entry = parseRecord(value, 'collect SDK metadata');
127
- if (typeof entry.hash !== 'string' || typeof entry.path !== 'string') {
128
- throw new Error('collect SDK metadata must contain string hash and path fields');
129
- }
130
- const hash = entry.hash.trim().toLowerCase();
131
- const path = cleanKey(entry.path);
132
- if (!SHA256_HEX.test(hash))
133
- throw new Error('collect SDK hash must be lowercase SHA-256');
134
- const expectedPath = `_aci/aci.${hash}.js`;
135
- if (path !== expectedPath || path !== entry.path) {
136
- throw new Error(`collect SDK path ${JSON.stringify(entry.path)} must equal ${JSON.stringify(expectedPath)}`);
137
- }
138
- return { hash, path };
139
- }
140
- async function readSDKObject(client, bucket, key) {
141
- const output = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
142
- if (output.Body?.transformToByteArray)
143
- return await output.Body.transformToByteArray();
144
- if (output.Body?.transformToString) {
145
- return new TextEncoder().encode(await output.Body.transformToString());
146
- }
147
- throw new Error(`collect SDK object ${key} returned an unsupported body`);
148
- }
149
- function ifNoneMatchSatisfied(value, etag) {
150
- const header = (value || '').trim();
151
- if (!header)
152
- return false;
153
- if (header === '*')
154
- return true;
155
- return header.split(',').some((candidate) => {
156
- const normalized = candidate.trim().replace(/^W\//, '');
157
- return normalized === etag;
158
- });
159
- }
160
- function requiredPathSegment(value, label) {
161
- const clean = value.trim();
162
- if (!clean || clean === '.' || clean === '..' || clean.includes('/') || clean.includes('\\')) {
163
- throw new Error(`${label} is required and must be one path segment`);
164
- }
165
- return clean;
166
- }
167
- function cleanKey(value) {
168
- const segments = value.replaceAll('\\', '/').split('/').filter((segment) => segment && segment !== '.');
169
- if (segments.some((segment) => segment === '..'))
170
- throw new Error('S3 key traversal is not allowed');
171
- return segments.join('/');
172
- }
173
- function joinKey(...parts) {
174
- return parts.map((part) => cleanKey(part || '')).filter(Boolean).join('/');
175
- }
176
- function requiredEnv(env, key) {
177
- const value = env[key]?.trim() || '';
178
- if (!value)
179
- throw new Error(`${key} is required for the Vercel collect SDK route`);
180
- return value;
181
- }
@@ -1,4 +0,0 @@
1
- export { createPersonalizationCollectRoute, type PersonalizationCollectRouteConfig, } from './collect-route.js';
2
- export { computeEnvelopeDigest, type EnvelopeDigestInput, } from './envelope-digest.js';
3
- export { CONTENT_PATH_CACHE_PARAM, CONTENT_PATH_HEADER, IdentityErasureFencedError, resolvePersonalizationRequest, type EdgeRouteManifest, type PersonalizationBlobStore, type PersonalizationCollectionStore, type PersonalizationEnvelopeWrite, type PersonalizationHostConfig, type PersonalizationRouteStore, type PersonalizationResolution, } from './personalization-host.js';
4
- export { createVercelPersonalizationFromEnv, createVercelCollectionFromEnv, VercelEdgeConfigRouteStore, createVercelS3PersonalizationStore, EnvelopeIDCollisionError, VercelS3PersonalizationStore, type VercelS3PersonalizationConfig, } from './vercel-s3-personalization.js';
@@ -1,4 +0,0 @@
1
- export { createPersonalizationCollectRoute, } from './collect-route.js';
2
- export { computeEnvelopeDigest, } from './envelope-digest.js';
3
- export { CONTENT_PATH_CACHE_PARAM, CONTENT_PATH_HEADER, IdentityErasureFencedError, resolvePersonalizationRequest, } from './personalization-host.js';
4
- export { createVercelPersonalizationFromEnv, createVercelCollectionFromEnv, VercelEdgeConfigRouteStore, createVercelS3PersonalizationStore, EnvelopeIDCollisionError, VercelS3PersonalizationStore, } from './vercel-s3-personalization.js';
@@ -1,79 +0,0 @@
1
- import type { EdgeRouteManifest, PersonalizationCollectionStore, PersonalizationHostConfig, PersonalizationEnvelopeWrite, PersonalizationRouteStore } from './personalization-host.js';
2
- export interface VercelS3PersonalizationConfig {
3
- region: string;
4
- roleArn?: string;
5
- hostingBucket: string;
6
- eventsBucket: string;
7
- hostingKeyPrefix?: string;
8
- claimsKeyPrefix?: string;
9
- endpoint?: string;
10
- forcePathStyle?: boolean;
11
- collectionWriteTimeoutMs?: number;
12
- }
13
- export interface S3CommandClient {
14
- send(command: {
15
- input: unknown;
16
- }, options?: {
17
- abortSignal?: AbortSignal;
18
- }): Promise<any>;
19
- }
20
- export declare class EnvelopeIDCollisionError extends Error {
21
- readonly key: string;
22
- readonly existingDigest: string;
23
- readonly incomingDigest: string;
24
- readonly name = "EnvelopeIDCollisionError";
25
- constructor(key: string, existingDigest: string, incomingDigest: string);
26
- }
27
- /**
28
- * Node-only Vercel collection adapter for audience lake objects and signed
29
- * claims. The EdgeRouter never receives this adapter or AWS credentials.
30
- * Production construction exchanges the Vercel workload OIDC token for
31
- * scoped AWS credentials; no long-lived AWS keys are read.
32
- */
33
- export declare class VercelS3PersonalizationStore implements PersonalizationCollectionStore {
34
- private readonly config;
35
- private readonly client;
36
- constructor(config: VercelS3PersonalizationConfig, client?: S3CommandClient);
37
- readClaims(siteId: string, namespace: string, identityHash: string): Promise<string | null>;
38
- appendEnvelope(write: PersonalizationEnvelopeWrite): Promise<void>;
39
- putSuppression(key: string, record: unknown): Promise<void>;
40
- private acquireCollectionGate;
41
- private releaseCollectionGate;
42
- private settleResumedEnvelope;
43
- private collectionGenerationSettled;
44
- private putCollectionGate;
45
- private readCollectionGate;
46
- private sendWithWriteDeadline;
47
- private writeTimeoutMs;
48
- private readObject;
49
- }
50
- /**
51
- * Edge Config is the provider projection for immutable release routing state.
52
- * This tiny store is safe in Next middleware: it only fetches the route
53
- * projection and never reads S3, claims, or event data.
54
- */
55
- export declare class VercelEdgeConfigRouteStore implements PersonalizationRouteStore {
56
- private readonly edgeConfig;
57
- private readonly siteId;
58
- constructor(edgeConfig: string, siteId: string);
59
- readEdgeManifest(releaseId: string): Promise<EdgeRouteManifest | null>;
60
- }
61
- export declare function createVercelS3PersonalizationStore(config: VercelS3PersonalizationConfig & {
62
- roleArn: string;
63
- }): VercelS3PersonalizationStore;
64
- /**
65
- * Builds the EdgeRouter configuration used by Next middleware. It deliberately
66
- * has no AWS/S3 configuration: its only remote dependency is the provider's
67
- * Edge Config projection.
68
- */
69
- export declare function createVercelPersonalizationFromEnv(env?: Record<string, string | undefined>): PersonalizationHostConfig | undefined;
70
- /** Builds the Node.js `/collect` configuration. This is the only Vercel
71
- * application runtime that receives data-plane storage configuration, and it
72
- * fails closed unless the exact-project AWS workload role is configured. */
73
- export declare function createVercelCollectionFromEnv(env?: Record<string, string | undefined>): {
74
- siteId: string;
75
- store: PersonalizationCollectionStore;
76
- identityKeys: PersonalizationHostConfig['identityKeys'];
77
- claimsKeys: Record<string, string>;
78
- secureCookies: boolean;
79
- } | undefined;