@gradial/aci 0.1.41 → 0.1.43

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 (134) hide show
  1. package/CHANGELOG.md +15 -11
  2. package/README.md +4 -4
  3. package/dist/astro/index.d.ts +1 -1
  4. package/dist/astro/index.js +1 -1
  5. package/dist/astro/page.d.ts +6 -0
  6. package/dist/astro/page.js +3 -2
  7. package/dist/audience-data-models.d.ts +286 -0
  8. package/dist/audience-data-models.js +46 -0
  9. package/dist/cli/compile-registry.d.mts +6 -0
  10. package/dist/cli/compile-registry.mjs +774 -0
  11. package/dist/cli/materialize-template.d.mts +19 -0
  12. package/dist/cli/materialize-template.mjs +128 -0
  13. package/dist/cli/validate-content.d.mts +32 -0
  14. package/dist/cli/validate-content.mjs +464 -0
  15. package/dist/cli/verify-next-cdn-routes.d.mts +2 -0
  16. package/dist/cli/verify-next-cdn-routes.mjs +47 -0
  17. package/dist/collect/aci.js +1 -0
  18. package/dist/collect/aci.js.sha256 +1 -0
  19. package/dist/collect/assignments.d.ts +65 -0
  20. package/dist/collect/assignments.js +138 -0
  21. package/dist/collect/batcher.d.ts +23 -0
  22. package/dist/collect/batcher.js +28 -6
  23. package/dist/collect/consent/envelope.d.ts +9 -3
  24. package/dist/collect/consent/envelope.js +58 -40
  25. package/dist/collect/declarative.d.ts +1 -1
  26. package/dist/collect/declarative.js +7 -1
  27. package/dist/collect/index.js +39 -6
  28. package/dist/collect/linkclicks.js +4 -2
  29. package/dist/collect/pageviews.d.ts +15 -3
  30. package/dist/collect/pageviews.js +28 -13
  31. package/dist/collect/transport.d.ts +4 -0
  32. package/dist/collect/transport.js +37 -2
  33. package/dist/consent/portable-envelope.d.ts +9 -0
  34. package/dist/consent/portable-envelope.js +110 -0
  35. package/dist/consent-banner/index.d.ts +33 -0
  36. package/dist/consent-banner/index.js +100 -0
  37. package/dist/content/assets.d.ts +1 -0
  38. package/dist/content/assets.js +1 -0
  39. package/dist/content/personalization.d.ts +35 -0
  40. package/dist/content/personalization.js +57 -0
  41. package/dist/content/resolve-slots.js +1 -0
  42. package/dist/content/types.d.ts +9 -1
  43. package/dist/edge/personalization-core.d.ts +11 -0
  44. package/dist/edge/personalization-core.js +265 -0
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +1 -0
  47. package/dist/next/assignments-script.d.ts +14 -0
  48. package/dist/next/assignments-script.js +17 -0
  49. package/dist/next/collect-route.d.ts +19 -0
  50. package/dist/next/collect-route.js +422 -0
  51. package/dist/next/envelope-digest.d.ts +21 -0
  52. package/dist/next/envelope-digest.js +58 -0
  53. package/dist/next/index.d.ts +1 -0
  54. package/dist/next/index.js +1 -0
  55. package/dist/next/middleware.d.ts +31 -3
  56. package/dist/next/middleware.js +254 -30
  57. package/dist/next/page.js +2 -1
  58. package/dist/next/personalization-host.d.ts +127 -0
  59. package/dist/next/personalization-host.js +257 -0
  60. package/dist/next/server.d.ts +1 -0
  61. package/dist/next/server.js +9 -6
  62. package/dist/next/vercel-collect-sdk-route.d.ts +27 -0
  63. package/dist/next/vercel-collect-sdk-route.js +205 -0
  64. package/dist/next/vercel-personalization.d.ts +4 -0
  65. package/dist/next/vercel-personalization.js +4 -0
  66. package/dist/next/vercel-s3-personalization.d.ts +79 -0
  67. package/dist/next/vercel-s3-personalization.js +574 -0
  68. package/dist/providers/s3.d.ts +20 -5
  69. package/dist/providers/s3.js +93 -23
  70. package/dist/react/VideoPlayer.d.ts +6 -6
  71. package/dist/react/index.d.ts +6 -6
  72. package/dist/testing/conformance/checks.js +75 -4
  73. package/dist/types/block-ref.d.ts +2 -0
  74. package/dist/types/block-ref.js +1 -0
  75. package/dist/types/page.d.ts +1 -1
  76. package/package.json +44 -15
  77. package/src/astro/ConsentBanner.astro +62 -0
  78. package/src/cli/compile-registry.mjs +24 -10
  79. package/src/cli/validate-content.mjs +52 -13
  80. package/src/cli/verify-next-cdn-routes.mjs +5 -4
  81. package/src/consent-banner/styles.css +110 -0
  82. package/src/types/block-ref.test.ts +45 -0
  83. package/src/types/block-ref.ts +2 -0
  84. package/src/types/page.ts +1 -1
  85. package/templates/astro/starter-content/src/cms/contracts/components/index.ts +2 -0
  86. package/templates/astro/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
  87. package/templates/astro/starter-content/src/cms/registry.ts +3 -0
  88. package/templates/astro/starter-content/src/components/sections/PersonalizationChoice.astro +41 -0
  89. package/templates/astro/starter-content/src/fixtures/atomicBlocks.ts +7 -6
  90. package/templates/astro/starter-content/src/fixtures/composition.ts +3 -2
  91. package/templates/astro/starter-content/src/fixtures/sections.ts +2 -1
  92. package/templates/astro/starter-content/src/render/RenderPage.astro +3 -2
  93. package/templates/astro/template/src/layouts/SiteLayout.astro +3 -1
  94. package/templates/astro/template/src/pages/[...slug].astro +1 -0
  95. package/templates/common/.agents/skills/authoring/SKILL.md +11 -3
  96. package/templates/common/.agents/skills/authoring/references/block-slots.md +3 -0
  97. package/templates/common/.content/audiences/preference-blue/_index.json +11 -0
  98. package/templates/common/.content/audiences/preference-red/_index.json +11 -0
  99. package/templates/common/.content/overlays/audience/preference-blue/pages/home/_index.json +13 -0
  100. package/templates/common/.content/overlays/audience/preference-red/pages/home/_index.json +13 -0
  101. package/templates/common/.content/pages/home/_index.json +124 -4
  102. package/templates/common/.content/segments/preference-blue/_index.json +20 -0
  103. package/templates/common/.content/segments/preference-red/_index.json +20 -0
  104. package/templates/common/contracts/components/index.ts +21 -1
  105. package/templates/common/contracts/components/personalizationChoice.contract.ts +17 -0
  106. package/templates/common/starter-content/.content/pages/about/_index.json +3 -0
  107. package/templates/common/starter-content/.content/pages/home/_index.json +9 -0
  108. package/templates/nextjs/starter-content/src/cms/contracts/components/index.ts +3 -1
  109. package/templates/nextjs/starter-content/src/cms/contracts/components/personalizationChoice.contract.ts +17 -0
  110. package/templates/nextjs/starter-content/src/cms/registry.ts +3 -0
  111. package/templates/nextjs/starter-content/src/components/sections/PersonalizationChoice.tsx +53 -0
  112. package/templates/nextjs/starter-content/src/middleware.ts +41 -0
  113. package/templates/nextjs/template/src/app/collect/route.ts +18 -0
  114. package/templates/nextjs/template/src/middleware.ts +36 -2
  115. package/templates/nextjs/template/src/middleware.ts.tmpl +31 -1
  116. package/templates/astro/template/.aci.yaml +0 -28
  117. package/templates/common/.content/.gradial-dam/blobs/sha256/7f/bf/7fbfc7fe230ff12688c496f660119e41d9725c4504d82e310c15665c73c09292 +0 -0
  118. package/templates/common/.content/.gradial-dam/blobs/sha256/a0/f5/a0f506689de9b15a7055f70103a3f02cd3ba105771ae2de6754df2ede1210d3a +0 -0
  119. package/templates/common/.content/.gradial-dam/blobs/sha256/dd/b7/ddb7e9642be08317bcd4b2bd26b58ba8d0d34dd7239e03e684a4f3634fa3845b +0 -0
  120. package/templates/common/.content/.gradial-dam/blobs/sha256/fb/32/fb3260e8286ea8231ea1c77b9c0be92d6edd9026565731eb14cfdcfd6b1b75b3 +0 -0
  121. package/templates/common/.content/config/robots.txt +0 -2
  122. package/templates/common/.content/config/site.json +0 -197
  123. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/asset.json +0 -9
  124. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/editorial.json +0 -14
  125. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/governance.json +0 -23
  126. package/templates/common/.content/dam/folders/seed/ast_site_about_hero/versions/v0001.json +0 -37
  127. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/asset.json +0 -9
  128. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/editorial.json +0 -14
  129. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/governance.json +0 -23
  130. package/templates/common/.content/dam/folders/seed/ast_site_home_hero/versions/v0001.json +0 -37
  131. package/templates/common/.content/fragments/navbar/_index.json +0 -25
  132. package/templates/common/.content/fragments/site-footer/_index.json +0 -62
  133. package/templates/common/.content/pages/about/_index.json +0 -51
  134. package/templates/nextjs/template/.aci.yaml +0 -26
@@ -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();
@@ -34,7 +38,7 @@ export class S3ContentProvider {
34
38
  }
35
39
  /**
36
40
  * getPageByRef serves the exact payload the edge selected (the
37
- * X-Baremetal-Function-Ref contract): base page or variant. Only refs the
41
+ * X-Aci-Content-Ref contract): base page or variant. Only refs the
38
42
  * release manifest declares are served — the header names published
39
43
  * content, it cannot address arbitrary keys.
40
44
  */
@@ -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) {
@@ -16,12 +16,16 @@ export declare function VideoPlayer({ video, ...attrs }: VideoPlayerProps): impo
16
16
  title: string;
17
17
  rel?: string | undefined | undefined;
18
18
  content?: string | undefined | undefined;
19
- dir?: string | undefined | undefined;
20
- hidden?: boolean | undefined | undefined;
21
19
  slot?: string | undefined | undefined;
22
20
  style?: import("react").CSSProperties | undefined;
21
+ dir?: string | undefined | undefined;
22
+ hidden?: boolean | undefined | undefined;
23
+ dangerouslySetInnerHTML?: {
24
+ __html: string | TrustedHTML;
25
+ } | undefined | undefined;
23
26
  children?: import("react").ReactNode;
24
27
  className?: string | undefined | undefined;
28
+ lang?: string | undefined | undefined;
25
29
  crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
26
30
  defaultChecked?: boolean | undefined | undefined;
27
31
  defaultValue?: string | number | readonly string[] | undefined;
@@ -34,7 +38,6 @@ export declare function VideoPlayer({ video, ...attrs }: VideoPlayerProps): impo
34
38
  contextMenu?: string | undefined | undefined;
35
39
  draggable?: (boolean | "true" | "false") | undefined;
36
40
  enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
37
- lang?: string | undefined | undefined;
38
41
  nonce?: string | undefined | undefined;
39
42
  spellCheck?: (boolean | "true" | "false") | undefined;
40
43
  tabIndex?: number | undefined | undefined;
@@ -121,9 +124,6 @@ export declare function VideoPlayer({ video, ...attrs }: VideoPlayerProps): impo
121
124
  "aria-valuemin"?: number | undefined | undefined;
122
125
  "aria-valuenow"?: number | undefined | undefined;
123
126
  "aria-valuetext"?: string | undefined | undefined;
124
- dangerouslySetInnerHTML?: {
125
- __html: string | TrustedHTML;
126
- } | undefined | undefined;
127
127
  onCopy?: import("react").ClipboardEventHandler<HTMLVideoElement> | undefined;
128
128
  onCopyCapture?: import("react").ClipboardEventHandler<HTMLVideoElement> | undefined;
129
129
  onCut?: import("react").ClipboardEventHandler<HTMLVideoElement> | undefined;
@@ -25,12 +25,16 @@ export declare function Image({ image, value, ...attrs }: ImageProps): import("r
25
25
  rel?: string | undefined | undefined;
26
26
  content?: string | undefined | undefined;
27
27
  referrerPolicy?: import("react").HTMLAttributeReferrerPolicy | undefined;
28
- dir?: string | undefined | undefined;
29
- hidden?: boolean | undefined | undefined;
30
28
  slot?: string | undefined | undefined;
31
29
  style?: import("react").CSSProperties | undefined;
30
+ dir?: string | undefined | undefined;
31
+ hidden?: boolean | undefined | undefined;
32
+ dangerouslySetInnerHTML?: {
33
+ __html: string | TrustedHTML;
34
+ } | undefined | undefined;
32
35
  children?: ReactNode;
33
36
  className?: string | undefined | undefined;
37
+ lang?: string | undefined | undefined;
34
38
  crossOrigin?: "" | "anonymous" | "use-credentials" | undefined;
35
39
  fetchPriority?: "high" | "low" | "auto" | undefined | undefined;
36
40
  useMap?: string | undefined | undefined;
@@ -45,7 +49,6 @@ export declare function Image({ image, value, ...attrs }: ImageProps): import("r
45
49
  contextMenu?: string | undefined | undefined;
46
50
  draggable?: (boolean | "true" | "false") | undefined;
47
51
  enterKeyHint?: "enter" | "done" | "go" | "next" | "previous" | "search" | "send" | undefined | undefined;
48
- lang?: string | undefined | undefined;
49
52
  nonce?: string | undefined | undefined;
50
53
  spellCheck?: (boolean | "true" | "false") | undefined;
51
54
  tabIndex?: number | undefined | undefined;
@@ -132,9 +135,6 @@ export declare function Image({ image, value, ...attrs }: ImageProps): import("r
132
135
  "aria-valuemin"?: number | undefined | undefined;
133
136
  "aria-valuenow"?: number | undefined | undefined;
134
137
  "aria-valuetext"?: string | undefined | undefined;
135
- dangerouslySetInnerHTML?: {
136
- __html: string | TrustedHTML;
137
- } | undefined | undefined;
138
138
  onCopy?: import("react").ClipboardEventHandler<HTMLImageElement> | undefined;
139
139
  onCopyCapture?: import("react").ClipboardEventHandler<HTMLImageElement> | undefined;
140
140
  onCut?: import("react").ClipboardEventHandler<HTMLImageElement> | undefined;
@@ -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
  // ---------------------------------------------------------------------------
@@ -5,6 +5,7 @@ export interface BlockRefMeta {
5
5
  multiple: boolean;
6
6
  }
7
7
  declare const blockRefBase: z.ZodObject<{
8
+ $type: z.ZodLiteral<"component">;
8
9
  id: z.ZodString;
9
10
  component: z.ZodString;
10
11
  props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -12,6 +13,7 @@ declare const blockRefBase: z.ZodObject<{
12
13
  type InferContractProps<T> = T extends ComponentContract<string, infer Schema> ? Schema extends z.ZodType<infer P> ? P : Record<string, unknown> : Record<string, unknown>;
13
14
  type InferContractId<T> = T extends ComponentContract<infer Id, unknown> ? Id : never;
14
15
  export type BlockRefEntry<T extends ComponentContract> = {
16
+ $type: 'component';
15
17
  id: string;
16
18
  component: InferContractId<T>;
17
19
  props: InferContractProps<T>;
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  const BLOCK_REF_KEY = '_gradialBlockRef';
3
3
  const blockRefBase = z.object({
4
+ $type: z.literal('component'),
4
5
  id: z.string().min(1),
5
6
  component: z.string().min(1),
6
7
  props: z.record(z.string(), z.unknown()),
@@ -30,7 +30,7 @@ export interface BlockNode {
30
30
  component: string;
31
31
  props: Record<string, unknown>;
32
32
  renderHints: BlockRenderHints;
33
- contentRef?: string;
33
+ contentPath?: string;
34
34
  }
35
35
  export interface BlockRenderHints {
36
36
  canStatic: boolean;
package/package.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
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",
@@ -9,6 +14,8 @@
9
14
  "bin/aci.js",
10
15
  "src/cli/*.mjs",
11
16
  "src/types/*.ts",
17
+ "src/astro/*.astro",
18
+ "src/consent-banner/*.css",
12
19
  "templates/common",
13
20
  "templates/nextjs",
14
21
  "templates/astro",
@@ -65,6 +72,16 @@
65
72
  "import": "./dist/next/middleware.js",
66
73
  "default": "./dist/next/middleware.js"
67
74
  },
75
+ "./next/vercel-personalization": {
76
+ "types": "./dist/next/vercel-personalization.d.ts",
77
+ "import": "./dist/next/vercel-personalization.js",
78
+ "default": "./dist/next/vercel-personalization.js"
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
+ },
68
85
  "./next/asset-route": {
69
86
  "types": "./dist/next/asset-route.d.ts",
70
87
  "import": "./dist/next/asset-route.js",
@@ -84,12 +101,30 @@
84
101
  "import": "./dist/astro/index.js",
85
102
  "default": "./dist/astro/index.js"
86
103
  },
104
+ "./astro/ConsentBanner": {
105
+ "import": "./src/astro/ConsentBanner.astro"
106
+ },
107
+ "./consent-banner": {
108
+ "types": "./dist/consent-banner/index.d.ts",
109
+ "import": "./dist/consent-banner/index.js"
110
+ },
87
111
  "./testing": {
88
112
  "types": "./dist/testing/conformance/index.d.ts",
89
113
  "import": "./dist/testing/conformance/index.js"
90
114
  }
91
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
+ },
92
125
  "dependencies": {
126
+ "@aws-sdk/client-s3": "^3.883.0",
127
+ "@vercel/oidc-aws-credentials-provider": "^3.3.0",
93
128
  "markdown-it": "^14.1.0",
94
129
  "sanitize-html": "^2.13.1",
95
130
  "ws": "^8.18.0"
@@ -121,6 +156,7 @@
121
156
  "@types/react": "^19.0.0",
122
157
  "@types/sanitize-html": "^2.16.0",
123
158
  "@types/ws": "^8.18.0",
159
+ "esbuild": "0.28.1",
124
160
  "jsdom": "^29.1.1",
125
161
  "next": "^15.5.19",
126
162
  "tsx": "^4.20.0",
@@ -128,18 +164,11 @@
128
164
  "zod": "^4.0.0"
129
165
  },
130
166
  "optionalDependencies": {
131
- "@gradial/aci-cli-darwin-arm64": "0.1.41",
132
- "@gradial/aci-cli-darwin-x64": "0.1.41",
133
- "@gradial/aci-cli-linux-arm64": "0.1.41",
134
- "@gradial/aci-cli-linux-x64": "0.1.41",
135
- "@gradial/aci-cli-win32-arm64": "0.1.41",
136
- "@gradial/aci-cli-win32-x64": "0.1.41"
137
- },
138
- "scripts": {
139
- "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
140
- "test": "node --import tsx --test \"src/**/*.test.ts\"",
141
- "build:cli": "cd ../.. && ./scripts/build-cli.sh",
142
- "generate-starters": "cd ../.. && tsx scripts/generate-starters.ts",
143
- "compile-registry": "tsx src/cli/compile-registry.mjs"
167
+ "@gradial/aci-cli-darwin-arm64": "0.1.43",
168
+ "@gradial/aci-cli-darwin-x64": "0.1.43",
169
+ "@gradial/aci-cli-linux-arm64": "0.1.43",
170
+ "@gradial/aci-cli-linux-x64": "0.1.43",
171
+ "@gradial/aci-cli-win32-arm64": "0.1.43",
172
+ "@gradial/aci-cli-win32-x64": "0.1.43"
144
173
  }
145
- }
174
+ }
@@ -0,0 +1,62 @@
1
+ ---
2
+ // ConsentBanner.astro — Astro wrapper around the SDK's first-party consent
3
+ // banner. Import this into your layout and pass site-specific text/styles.
4
+ //
5
+ // Usage:
6
+ // import ConsentBanner from '@gradial/aci/astro/ConsentBanner.astro';
7
+ // <ConsentBanner
8
+ // heading="Cookie Preferences"
9
+ // message="We use cookies to personalize your experience."
10
+ // acceptLabel="Accept all"
11
+ // rejectLabel="Reject"
12
+ // privacyPolicyHref="/privacy"
13
+ // privacyPolicyLabel="Privacy Policy"
14
+ // />
15
+
16
+ export interface Props {
17
+ heading?: string;
18
+ message?: string;
19
+ acceptLabel?: string;
20
+ rejectLabel?: string;
21
+ privacyPolicyLabel?: string;
22
+ privacyPolicyHref?: string;
23
+ source?: string;
24
+ }
25
+
26
+ const {
27
+ heading,
28
+ message,
29
+ acceptLabel,
30
+ rejectLabel,
31
+ privacyPolicyLabel,
32
+ privacyPolicyHref,
33
+ source,
34
+ } = Astro.props as Props;
35
+
36
+ const opts = JSON.stringify({
37
+ heading,
38
+ message,
39
+ acceptLabel,
40
+ rejectLabel,
41
+ privacyPolicyLabel,
42
+ privacyPolicyHref,
43
+ source,
44
+ });
45
+ ---
46
+
47
+ <div
48
+ id="aci-consent-banner-mount"
49
+ data-aci-consent-banner-mount
50
+ data-opts={opts}
51
+ >
52
+ </div>
53
+
54
+ <script>
55
+ import { mountConsentBanner } from '../consent-banner/index.js';
56
+
57
+ const mount = document.getElementById('aci-consent-banner-mount');
58
+ if (mount) {
59
+ const opts = JSON.parse(mount.getAttribute('data-opts') || '{}');
60
+ mountConsentBanner(document, opts);
61
+ }
62
+ </script>
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
+ import { realpathSync } from 'node:fs';
2
3
  import { mkdir, writeFile } from 'node:fs/promises';
3
4
  import path from 'node:path';
4
- import { pathToFileURL } from 'node:url';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
6
  import { z } from 'zod';
6
7
  import { ImageSchema } from '../types/image.ts';
7
8
  import { VideoSchema } from '../types/video.ts';
@@ -19,14 +20,15 @@ try {
19
20
  if (!globalThis.React) globalThis.React = React;
20
21
  } catch { /* react not available — component files without JSX will still work */ }
21
22
 
22
- const args = parseArgs(process.argv.slice(2));
23
-
24
- try {
25
- const result = await compileRegistry(args);
26
- console.log(JSON.stringify({ success: true, ...result, errors: [] }, null, 2));
27
- } catch (error) {
28
- console.error(JSON.stringify({ success: false, errors: [String(error?.message ?? error)] }, null, 2));
29
- process.exitCode = 1;
23
+ if (isMainModule()) {
24
+ const args = parseArgs(process.argv.slice(2));
25
+ try {
26
+ const result = await compileRegistry(args);
27
+ console.log(JSON.stringify({ success: true, ...result, errors: [] }, null, 2));
28
+ } catch (error) {
29
+ console.error(JSON.stringify({ success: false, errors: [String(error?.message ?? error)] }, null, 2));
30
+ process.exitCode = 1;
31
+ }
30
32
  }
31
33
 
32
34
  async function compileRegistry(options) {
@@ -430,9 +432,10 @@ async function generatePageSchema(schemaDir, componentEntries, outDir) {
430
432
  const defs = {
431
433
  baseBlock: {
432
434
  type: 'object',
433
- required: ['id', 'component', 'props'],
435
+ required: ['$type', 'id', 'component', 'props'],
434
436
  additionalProperties: true,
435
437
  properties: {
438
+ $type: { const: 'component' },
436
439
  id: { type: 'string', minLength: 1 },
437
440
  component: { type: 'string' },
438
441
  props: { type: 'object' },
@@ -741,3 +744,14 @@ function parseArgs(argv) {
741
744
  }
742
745
  return out;
743
746
  }
747
+
748
+ function isMainModule() {
749
+ if (!process.argv[1]) return false;
750
+ try {
751
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(path.resolve(process.argv[1]));
752
+ } catch {
753
+ return false;
754
+ }
755
+ }
756
+
757
+ export { compileRegistry, generatePageSchema };