@gradial/aci 0.1.42 → 0.1.44

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 (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +4 -4
  3. package/dist/audience-data-models.d.ts +286 -0
  4. package/dist/audience-data-models.js +46 -0
  5. package/dist/collect/aci.js +1 -0
  6. package/dist/collect/aci.js.sha256 +1 -0
  7. package/dist/collect/batcher.d.ts +2 -0
  8. package/dist/collect/batcher.js +18 -6
  9. package/dist/collect/consent/envelope.d.ts +9 -3
  10. package/dist/collect/consent/envelope.js +58 -40
  11. package/dist/collect/index.js +9 -2
  12. package/dist/collect/linkclicks.js +4 -2
  13. package/dist/collect/transport.d.ts +2 -0
  14. package/dist/collect/transport.js +6 -3
  15. package/dist/consent/portable-envelope.d.ts +9 -0
  16. package/dist/consent/portable-envelope.js +110 -0
  17. package/dist/consent-banner/index.js +2 -1
  18. package/dist/content/assets.d.ts +1 -0
  19. package/dist/content/assets.js +1 -0
  20. package/dist/next/collect-route.d.ts +2 -2
  21. package/dist/next/collect-route.js +90 -19
  22. package/dist/next/envelope-digest.d.ts +21 -0
  23. package/dist/next/envelope-digest.js +58 -0
  24. package/dist/next/middleware.d.ts +16 -2
  25. package/dist/next/middleware.js +190 -10
  26. package/dist/next/personalization-host.d.ts +16 -7
  27. package/dist/next/personalization-host.js +14 -33
  28. package/dist/next/server.d.ts +1 -0
  29. package/dist/next/server.js +3 -0
  30. package/dist/next/vercel-collect-sdk-route.d.ts +27 -0
  31. package/dist/next/vercel-collect-sdk-route.js +205 -0
  32. package/dist/next/vercel-personalization.d.ts +3 -2
  33. package/dist/next/vercel-personalization.js +3 -2
  34. package/dist/next/vercel-s3-personalization.d.ts +25 -4
  35. package/dist/next/vercel-s3-personalization.js +368 -14
  36. package/dist/providers/s3.d.ts +19 -4
  37. package/dist/providers/s3.js +92 -22
  38. package/dist/testing/conformance/checks.js +75 -4
  39. package/package.json +27 -16
  40. package/src/cli/validate-content.mjs +0 -0
  41. package/templates/nextjs/starter-content/src/middleware.ts +31 -1
  42. package/templates/nextjs/template/src/app/collect/route.ts +2 -2
  43. package/templates/nextjs/template/src/middleware.ts +31 -1
  44. package/templates/nextjs/template/src/middleware.ts.tmpl +31 -1
@@ -1,4 +1,6 @@
1
1
  import crypto from 'node:crypto';
2
+ import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
3
+ import { awsCredentialsProvider } from '@vercel/oidc-aws-credentials-provider';
2
4
  import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError } from '../content/provider.js';
3
5
  import { normalizeRoute } from '../content/routes.js';
4
6
  // ---------------------------------------------------------------------------
@@ -7,8 +9,10 @@ import { normalizeRoute } from '../content/routes.js';
7
9
  export class S3ContentProvider {
8
10
  config;
9
11
  #manifest;
10
- constructor(config = {}) {
12
+ #client;
13
+ constructor(config = {}, client) {
11
14
  this.config = resolveS3Config(config);
15
+ this.#client = client || oidcS3Client(this.config);
12
16
  }
13
17
  async listRoutes() {
14
18
  const manifest = await this.manifest();
@@ -83,24 +87,18 @@ export class S3ContentProvider {
83
87
  return await this.#manifest;
84
88
  }
85
89
  async fetchRaw(key) {
86
- return await getS3Raw(key, this.config);
90
+ return await getS3Raw(key, this.config, this.#client);
87
91
  }
88
92
  async getJSON(key) {
89
- return await getS3JSON(joinKey(this.config.keyPrefix, key), this.config);
93
+ return await getS3JSON(joinKey(this.config.keyPrefix, key), this.config, this.#client);
90
94
  }
91
95
  }
92
96
  // ---------------------------------------------------------------------------
93
97
  // S3 fetch with AWS SigV4 signing
94
98
  // ---------------------------------------------------------------------------
95
- export async function getS3JSON(key, config) {
99
+ export async function getS3JSON(key, config, client) {
96
100
  const startTime = Date.now();
97
- const url = s3ObjectURL(config, key);
98
- const signedHeaders = signedS3Headers('GET', url, config);
99
- const res = await fetch(url, {
100
- method: 'GET',
101
- headers: signedHeaders,
102
- cache: 'no-store'
103
- });
101
+ const res = await getS3Raw(key, config, client);
104
102
  const duration = Date.now() - startTime;
105
103
  console.log(`[ACI S3] GET ${key} - ${res.status} in ${duration}ms`);
106
104
  if (!res.ok) {
@@ -108,7 +106,9 @@ export async function getS3JSON(key, config) {
108
106
  }
109
107
  return (await res.json());
110
108
  }
111
- export async function getS3Raw(key, config) {
109
+ export async function getS3Raw(key, config, client) {
110
+ if (client)
111
+ return await getS3RawWithClient(key, config, client);
112
112
  const url = s3ObjectURL(config, key);
113
113
  const signedHeaders = signedS3Headers('GET', url, config);
114
114
  return await fetch(url, {
@@ -122,20 +122,90 @@ export async function getS3Raw(key, config) {
122
122
  // ---------------------------------------------------------------------------
123
123
  function resolveS3Config(config) {
124
124
  const resolved = {
125
- bucket: config.bucket || process.env.ACI_S3_BUCKET || '',
126
- keyPrefix: config.keyPrefix || process.env.ACI_S3_KEY_PREFIX || '',
127
- region: config.region || process.env.AWS_REGION || 'us-east-1',
128
- accessKeyId: config.accessKeyId || process.env.AWS_ACCESS_KEY_ID || '',
129
- secretAccessKey: config.secretAccessKey || process.env.AWS_SECRET_ACCESS_KEY || '',
130
- sessionToken: config.sessionToken || process.env.AWS_SESSION_TOKEN || '',
131
- endpoint: trimSlash(config.endpoint || process.env.ACI_S3_ENDPOINT || '')
125
+ bucket: configuredValue(config.bucket, process.env.ACI_S3_BUCKET),
126
+ keyPrefix: configuredValue(config.keyPrefix, process.env.ACI_S3_KEY_PREFIX),
127
+ region: configuredValue(config.region, process.env.AWS_REGION) || 'us-east-1',
128
+ accessKeyId: configuredValue(config.accessKeyId, process.env.AWS_ACCESS_KEY_ID),
129
+ secretAccessKey: configuredValue(config.secretAccessKey, process.env.AWS_SECRET_ACCESS_KEY),
130
+ sessionToken: configuredValue(config.sessionToken, process.env.AWS_SESSION_TOKEN),
131
+ roleArn: configuredValue(config.roleArn, process.env.AWS_ROLE_ARN),
132
+ endpoint: trimSlash(configuredValue(config.endpoint, process.env.ACI_S3_ENDPOINT))
132
133
  };
133
- for (const key of ['bucket', 'accessKeyId', 'secretAccessKey']) {
134
- if (!resolved[key])
135
- throw new Error(`S3 config missing ${key}`);
134
+ if (!resolved.bucket)
135
+ throw new Error('S3 config missing bucket');
136
+ if (!resolved.roleArn) {
137
+ if (resolved.accessKeyId && !resolved.secretAccessKey) {
138
+ throw new Error('S3 config missing secretAccessKey');
139
+ }
140
+ if (!resolved.accessKeyId && resolved.secretAccessKey) {
141
+ throw new Error('S3 config missing accessKeyId');
142
+ }
143
+ if (!resolved.accessKeyId) {
144
+ throw new Error('S3 config missing credential mode: set AWS_ROLE_ARN or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY');
145
+ }
136
146
  }
137
147
  return resolved;
138
148
  }
149
+ function oidcS3Client(config) {
150
+ if (!config.roleArn)
151
+ return undefined;
152
+ return new S3Client({
153
+ region: config.region,
154
+ ...(config.endpoint ? {
155
+ endpoint: config.endpoint,
156
+ forcePathStyle: true,
157
+ } : {}),
158
+ credentials: awsCredentialsProvider({
159
+ roleArn: config.roleArn,
160
+ clientConfig: { region: config.region },
161
+ }),
162
+ });
163
+ }
164
+ async function getS3RawWithClient(key, config, client) {
165
+ try {
166
+ const output = await client.send(new GetObjectCommand({
167
+ Bucket: config.bucket,
168
+ Key: key,
169
+ }));
170
+ if (!output.Body?.transformToWebStream) {
171
+ throw new Error(`S3 GET ${key} returned an unsupported object body`);
172
+ }
173
+ const headers = new Headers();
174
+ if (output.ContentType)
175
+ headers.set('content-type', output.ContentType);
176
+ if (output.ContentLength !== undefined) {
177
+ headers.set('content-length', String(output.ContentLength));
178
+ }
179
+ if (output.ETag)
180
+ headers.set('etag', output.ETag);
181
+ if (output.LastModified) {
182
+ headers.set('last-modified', output.LastModified.toUTCString());
183
+ }
184
+ return new Response(output.Body.transformToWebStream(), {
185
+ status: 200,
186
+ headers,
187
+ });
188
+ }
189
+ catch (error) {
190
+ const status = s3ErrorStatus(error);
191
+ if (status) {
192
+ return new Response(error instanceof Error ? error.message : String(error), { status });
193
+ }
194
+ throw new Error(`S3 GET ${key} failed`, { cause: error });
195
+ }
196
+ }
197
+ function s3ErrorStatus(error) {
198
+ if (!error || typeof error !== 'object')
199
+ return undefined;
200
+ const status = error
201
+ .$metadata?.httpStatusCode;
202
+ return typeof status === 'number' && status >= 400 && status <= 599
203
+ ? status
204
+ : undefined;
205
+ }
206
+ function configuredValue(configured, environment) {
207
+ return (configured === undefined ? environment : configured)?.trim() || '';
208
+ }
139
209
  function s3ObjectURL(config, key) {
140
210
  const encodedKey = encodeKey(key);
141
211
  if (config.endpoint) {
@@ -278,7 +278,37 @@ export function checkMiddleware(ctx) {
278
278
  // the SDK's canonical value. Next.js requires config.matcher to be
279
279
  // statically analyzable, so importing the matcher from the SDK breaks the
280
280
  // build. Instead the template inlines the value and this check detects drift.
281
- const canonicalMatcher = "['/((?!_next(?:/|$)|api(?:/|$)|favicon\\\\.ico$|\\\\.gradial-dam(?:/|$)).*)']";
281
+ const canonicalMatcher = String.raw `[
282
+ '/((?!_next(?:/|$)|api(?:/|$)|collect(?:/|$)|gradial/assets(?:/|$)|favicon\\.ico$).*)',
283
+ {
284
+ source: '/:path*',
285
+ has: [{ type: 'header', key: 'x-gradial-framework-origin-auth' }],
286
+ },
287
+ {
288
+ source: '/:path*',
289
+ has: [{ type: 'header', key: 'x-baremetal-site-id' }],
290
+ },
291
+ {
292
+ source: '/:path*',
293
+ has: [{ type: 'header', key: 'x-baremetal-release-id' }],
294
+ },
295
+ {
296
+ source: '/:path*',
297
+ has: [{ type: 'header', key: 'x-aci-content-ref' }],
298
+ },
299
+ {
300
+ source: '/:path*',
301
+ has: [{ type: 'header', key: 'x-baremetal-content-base-url' }],
302
+ },
303
+ {
304
+ source: '/:path*',
305
+ has: [{ type: 'header', key: 'x-aci-content-path' }],
306
+ },
307
+ {
308
+ source: '/:path*',
309
+ has: [{ type: 'query', key: '__aci_content_path' }],
310
+ },
311
+ ]`;
282
312
  // Check for actual import/usage of aciMiddlewareMatcher (not just a mention
283
313
  // in a comment). Look for `matcher: aciMiddlewareMatcher` or
284
314
  // `import { ... aciMiddlewareMatcher ... }`.
@@ -289,9 +319,9 @@ export function checkMiddleware(ctx) {
289
319
  }
290
320
  else {
291
321
  // Verify the inlined value matches the SDK canonical
292
- const matcherMatch = middlewareFile.content.match(/matcher\s*:\s*(\[.*?\])/s);
293
- if (matcherMatch) {
294
- const normalizedInlined = matcherMatch[1].replace(/\s+/g, '');
322
+ const matcherLiteral = extractArrayPropertyLiteral(middlewareFile.content, 'matcher');
323
+ if (matcherLiteral) {
324
+ const normalizedInlined = matcherLiteral.replace(/\s+/g, '');
295
325
  const normalizedCanonical = canonicalMatcher.replace(/\s+/g, '');
296
326
  results.push(normalizedInlined === normalizedCanonical
297
327
  ? ok('middleware', 'middleware matcher matches SDK canonical', rel)
@@ -303,6 +333,47 @@ export function checkMiddleware(ctx) {
303
333
  }
304
334
  return results;
305
335
  }
336
+ function extractArrayPropertyLiteral(content, propertyName) {
337
+ const property = new RegExp(`\\b${propertyName}\\s*:`).exec(content);
338
+ if (!property)
339
+ return null;
340
+ const start = content.indexOf('[', property.index + property[0].length);
341
+ if (start < 0)
342
+ return null;
343
+ let depth = 0;
344
+ let quote = '';
345
+ let escaped = false;
346
+ for (let index = start; index < content.length; index += 1) {
347
+ const character = content[index];
348
+ if (quote) {
349
+ if (escaped) {
350
+ escaped = false;
351
+ }
352
+ else if (character === '\\') {
353
+ escaped = true;
354
+ }
355
+ else if (character === quote) {
356
+ quote = '';
357
+ }
358
+ continue;
359
+ }
360
+ if (character === "'" || character === '"' || character === '`') {
361
+ quote = character;
362
+ continue;
363
+ }
364
+ if (character === '[') {
365
+ depth += 1;
366
+ continue;
367
+ }
368
+ if (character === ']') {
369
+ depth -= 1;
370
+ if (depth === 0) {
371
+ return content.slice(start, index + 1);
372
+ }
373
+ }
374
+ }
375
+ return null;
376
+ }
306
377
  // ---------------------------------------------------------------------------
307
378
  // Check: noFileContentProvider — no direct FileContentProvider imports in app code
308
379
  // ---------------------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.42",
3
+ "version": "0.1.44",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/gradial/bare-metal-go.git",
7
+ "directory": "packages/sdk"
8
+ },
4
9
  "type": "module",
5
10
  "main": "./dist/index.js",
6
11
  "types": "./dist/index.d.ts",
@@ -72,6 +77,11 @@
72
77
  "import": "./dist/next/vercel-personalization.js",
73
78
  "default": "./dist/next/vercel-personalization.js"
74
79
  },
80
+ "./next/vercel-collect-sdk-route": {
81
+ "types": "./dist/next/vercel-collect-sdk-route.d.ts",
82
+ "import": "./dist/next/vercel-collect-sdk-route.js",
83
+ "default": "./dist/next/vercel-collect-sdk-route.js"
84
+ },
75
85
  "./next/asset-route": {
76
86
  "types": "./dist/next/asset-route.d.ts",
77
87
  "import": "./dist/next/asset-route.js",
@@ -103,6 +113,15 @@
103
113
  "import": "./dist/testing/conformance/index.js"
104
114
  }
105
115
  },
116
+ "scripts": {
117
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json && node scripts/build-collect.mjs",
118
+ "test": "node --import tsx --test \"src/**/*.test.ts\"",
119
+ "build:collect": "node scripts/build-collect.mjs",
120
+ "build:cli": "cd ../.. && ./scripts/build-cli.sh",
121
+ "generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
122
+ "compile-registry": "tsx src/cli/compile-registry.mjs",
123
+ "prepack": "npm run generate-starters && npm run build && node scripts/ensure-bin-modes.mjs"
124
+ },
106
125
  "dependencies": {
107
126
  "@aws-sdk/client-s3": "^3.883.0",
108
127
  "@vercel/oidc-aws-credentials-provider": "^3.3.0",
@@ -145,19 +164,11 @@
145
164
  "zod": "^4.0.0"
146
165
  },
147
166
  "optionalDependencies": {
148
- "@gradial/aci-cli-darwin-arm64": "0.1.41",
149
- "@gradial/aci-cli-darwin-x64": "0.1.41",
150
- "@gradial/aci-cli-linux-arm64": "0.1.41",
151
- "@gradial/aci-cli-linux-x64": "0.1.41",
152
- "@gradial/aci-cli-win32-arm64": "0.1.41",
153
- "@gradial/aci-cli-win32-x64": "0.1.41"
154
- },
155
- "scripts": {
156
- "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
157
- "test": "node --import tsx --test \"src/**/*.test.ts\"",
158
- "build:collect": "node scripts/build-collect.mjs",
159
- "build:cli": "cd ../.. && ./scripts/build-cli.sh",
160
- "generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
161
- "compile-registry": "tsx src/cli/compile-registry.mjs"
167
+ "@gradial/aci-cli-darwin-arm64": "0.1.44",
168
+ "@gradial/aci-cli-darwin-x64": "0.1.44",
169
+ "@gradial/aci-cli-linux-arm64": "0.1.44",
170
+ "@gradial/aci-cli-linux-x64": "0.1.44",
171
+ "@gradial/aci-cli-win32-arm64": "0.1.44",
172
+ "@gradial/aci-cli-win32-x64": "0.1.44"
162
173
  }
163
- }
174
+ }
File without changes
@@ -7,5 +7,35 @@ export default createGradialMiddleware({
7
7
 
8
8
  export const config = {
9
9
  runtime: 'nodejs',
10
- matcher: ['/((?!_next(?:/|$)|api(?:/|$)|collect(?:/|$)|gradial/assets(?:/|$)|favicon\\.ico$).*)'],
10
+ matcher: [
11
+ '/((?!_next(?:/|$)|api(?:/|$)|collect(?:/|$)|gradial/assets(?:/|$)|favicon\\.ico$).*)',
12
+ {
13
+ source: '/:path*',
14
+ has: [{ type: 'header', key: 'x-gradial-framework-origin-auth' }],
15
+ },
16
+ {
17
+ source: '/:path*',
18
+ has: [{ type: 'header', key: 'x-baremetal-site-id' }],
19
+ },
20
+ {
21
+ source: '/:path*',
22
+ has: [{ type: 'header', key: 'x-baremetal-release-id' }],
23
+ },
24
+ {
25
+ source: '/:path*',
26
+ has: [{ type: 'header', key: 'x-aci-content-ref' }],
27
+ },
28
+ {
29
+ source: '/:path*',
30
+ has: [{ type: 'header', key: 'x-baremetal-content-base-url' }],
31
+ },
32
+ {
33
+ source: '/:path*',
34
+ has: [{ type: 'header', key: 'x-aci-content-path' }],
35
+ },
36
+ {
37
+ source: '/:path*',
38
+ has: [{ type: 'query', key: '__aci_content_path' }],
39
+ },
40
+ ],
11
41
  };
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createPersonalizationCollectRoute,
3
- createVercelPersonalizationFromEnv,
3
+ createVercelCollectionFromEnv,
4
4
  } from '@gradial/aci/next/vercel-personalization';
5
5
 
6
6
  export const runtime = 'nodejs';
7
7
 
8
- const personalization = createVercelPersonalizationFromEnv();
8
+ const personalization = createVercelCollectionFromEnv();
9
9
  const unavailable = async () => new Response('personalization host is not configured', {
10
10
  status: 503,
11
11
  headers: { 'Cache-Control': 'private, no-store' },
@@ -7,5 +7,35 @@ export default createGradialMiddleware({
7
7
 
8
8
  export const config = {
9
9
  runtime: 'nodejs',
10
- matcher: ['/((?!_next(?:/|$)|api(?:/|$)|collect(?:/|$)|gradial/assets(?:/|$)|favicon\\.ico$).*)'],
10
+ matcher: [
11
+ '/((?!_next(?:/|$)|api(?:/|$)|collect(?:/|$)|gradial/assets(?:/|$)|favicon\\.ico$).*)',
12
+ {
13
+ source: '/:path*',
14
+ has: [{ type: 'header', key: 'x-gradial-framework-origin-auth' }],
15
+ },
16
+ {
17
+ source: '/:path*',
18
+ has: [{ type: 'header', key: 'x-baremetal-site-id' }],
19
+ },
20
+ {
21
+ source: '/:path*',
22
+ has: [{ type: 'header', key: 'x-baremetal-release-id' }],
23
+ },
24
+ {
25
+ source: '/:path*',
26
+ has: [{ type: 'header', key: 'x-aci-content-ref' }],
27
+ },
28
+ {
29
+ source: '/:path*',
30
+ has: [{ type: 'header', key: 'x-baremetal-content-base-url' }],
31
+ },
32
+ {
33
+ source: '/:path*',
34
+ has: [{ type: 'header', key: 'x-aci-content-path' }],
35
+ },
36
+ {
37
+ source: '/:path*',
38
+ has: [{ type: 'query', key: '__aci_content_path' }],
39
+ },
40
+ ],
11
41
  };
@@ -6,5 +6,35 @@ export default createGradialMiddleware();
6
6
  // Source of truth: aciMiddlewareMatcher from '@gradial/aci/next/middleware'.
7
7
  // The conformance check verifies this stays in sync with the SDK export.
8
8
  export const config = {
9
- matcher: ['/((?!_next(?:/|$)|api(?:/|$)|favicon\\.ico$|\\.gradial-dam(?:/|$)).*)'],
9
+ matcher: [
10
+ '/((?!_next(?:/|$)|api(?:/|$)|collect(?:/|$)|gradial/assets(?:/|$)|favicon\\.ico$).*)',
11
+ {
12
+ source: '/:path*',
13
+ has: [{ type: 'header', key: 'x-gradial-framework-origin-auth' }],
14
+ },
15
+ {
16
+ source: '/:path*',
17
+ has: [{ type: 'header', key: 'x-baremetal-site-id' }],
18
+ },
19
+ {
20
+ source: '/:path*',
21
+ has: [{ type: 'header', key: 'x-baremetal-release-id' }],
22
+ },
23
+ {
24
+ source: '/:path*',
25
+ has: [{ type: 'header', key: 'x-aci-content-ref' }],
26
+ },
27
+ {
28
+ source: '/:path*',
29
+ has: [{ type: 'header', key: 'x-baremetal-content-base-url' }],
30
+ },
31
+ {
32
+ source: '/:path*',
33
+ has: [{ type: 'header', key: 'x-aci-content-path' }],
34
+ },
35
+ {
36
+ source: '/:path*',
37
+ has: [{ type: 'query', key: '__aci_content_path' }],
38
+ },
39
+ ],
10
40
  };