@gradial/aci 0.1.48 → 0.1.51-ssr.2

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 (109) hide show
  1. package/README.md +13 -20
  2. package/dist/astro/assets.d.ts +6 -6
  3. package/dist/astro/assets.js +6 -6
  4. package/dist/astro/index.d.ts +0 -2
  5. package/dist/astro/index.js +0 -4
  6. package/dist/cli/emit-error-pages.mjs +0 -1
  7. package/dist/cli/verify-next-cdn-routes.mjs +3 -4
  8. package/dist/collect/aci.js +1 -1
  9. package/dist/collect/aci.js.sha256 +1 -1
  10. package/dist/collect/webvitals.d.ts +0 -3
  11. package/dist/collect/webvitals.js +11 -21
  12. package/dist/content/cache.d.ts +1 -9
  13. package/dist/content/cache.js +2 -37
  14. package/dist/content/error-pages.d.ts +1 -2
  15. package/dist/content/index.d.ts +0 -1
  16. package/dist/content/index.js +0 -1
  17. package/dist/index.d.ts +0 -1
  18. package/dist/internal/domain/edge/identity.d.ts +14 -0
  19. package/dist/internal/domain/edge/identity.js +214 -0
  20. package/dist/internal/domain/edge/index.d.ts +4 -0
  21. package/dist/internal/domain/edge/index.js +6 -0
  22. package/dist/internal/domain/edge/personalization-core.d.ts +24 -0
  23. package/dist/{edge → internal/domain/edge}/personalization-core.js +165 -64
  24. package/dist/internal/domain/edge/record/codec.d.ts +71 -0
  25. package/dist/internal/domain/edge/record/codec.js +401 -0
  26. package/dist/internal/domain/edge/resolve.d.ts +2 -0
  27. package/dist/internal/domain/edge/resolve.js +76 -0
  28. package/dist/{next/personalization-host.d.ts → internal/domain/edge/types.d.ts} +110 -61
  29. package/dist/internal/domain/edge/types.js +7 -0
  30. package/dist/internal/platform/collectionstore/errors.d.ts +5 -0
  31. package/dist/internal/platform/collectionstore/errors.js +7 -0
  32. package/dist/next/config.d.ts +0 -8
  33. package/dist/next/config.js +3 -78
  34. package/dist/next/error-pages.d.ts +2 -3
  35. package/dist/next/error-pages.js +5 -5
  36. package/dist/next/error-seat.d.ts +2 -3
  37. package/dist/next/index.d.ts +2 -2
  38. package/dist/next/index.js +1 -2
  39. package/dist/next/page.d.ts +3 -13
  40. package/dist/next/page.js +39 -51
  41. package/dist/next/server.d.ts +12 -32
  42. package/dist/next/server.js +66 -295
  43. package/dist/providers/object.d.ts +24 -0
  44. package/dist/providers/object.js +105 -0
  45. package/dist/providers/vercel-object-reader.d.ts +20 -0
  46. package/dist/providers/vercel-object-reader.js +45 -0
  47. package/dist/testing/conformance/checks.d.ts +1 -1
  48. package/dist/testing/conformance/checks.js +14 -144
  49. package/dist/testing/conformance/index.d.ts +1 -1
  50. package/dist/testing/conformance/index.js +1 -1
  51. package/dist/testing/conformance/preset.js +1 -1
  52. package/dist/testing/conformance/types.d.ts +1 -1
  53. package/dist/types/config.d.ts +0 -4
  54. package/dist/types/index.d.ts +0 -2
  55. package/dist/types/index.js +0 -2
  56. package/dist/types/page.d.ts +0 -10
  57. package/package.json +11 -35
  58. package/src/cli/emit-error-pages.mjs +0 -1
  59. package/src/cli/verify-next-cdn-routes.mjs +8 -11
  60. package/src/types/config.ts +0 -5
  61. package/src/types/index.ts +0 -2
  62. package/src/types/page.ts +0 -11
  63. package/templates/astro/template/.aci.yaml.tmpl +0 -11
  64. package/templates/common/.agents/skills/aci/SKILL.md +8 -9
  65. package/templates/common/.agents/skills/authoring/SKILL.md +3 -4
  66. package/templates/common/.agents/skills/authoring/references/block-registry.md +13 -36
  67. package/templates/common/.agents/skills/authoring/references/contracts.md +10 -17
  68. package/templates/common/.agents/skills/authoring/references/repo-structure.md +2 -2
  69. package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +45 -95
  70. package/templates/common/.agents/skills/migration/references/canon-migration.md +17 -37
  71. package/templates/nextjs/template/.aci.yaml.tmpl +0 -11
  72. package/templates/nextjs/template/README.md.tmpl +0 -1
  73. package/templates/nextjs/template/src/app/[[...slug]]/page.tsx +1 -1
  74. package/dist/content/assets.d.ts +0 -18
  75. package/dist/content/assets.js +0 -66
  76. package/dist/edge/personalization-core.d.ts +0 -11
  77. package/dist/internal/render-proof.d.ts +0 -1
  78. package/dist/internal/render-proof.js +0 -197
  79. package/dist/next/asset-route.d.ts +0 -10
  80. package/dist/next/asset-route.js +0 -45
  81. package/dist/next/collect-route.d.ts +0 -19
  82. package/dist/next/collect-route.js +0 -422
  83. package/dist/next/edge-config.d.ts +0 -1
  84. package/dist/next/edge-config.js +0 -92
  85. package/dist/next/envelope-digest.d.ts +0 -21
  86. package/dist/next/envelope-digest.js +0 -58
  87. package/dist/next/middleware.d.ts +0 -40
  88. package/dist/next/middleware.js +0 -462
  89. package/dist/next/personalization-host.js +0 -257
  90. package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
  91. package/dist/next/vercel-collect-sdk-route.js +0 -181
  92. package/dist/next/vercel-edge-projection-v1.d.ts +0 -24
  93. package/dist/next/vercel-edge-projection-v1.js +0 -880
  94. package/dist/next/vercel-personalization.d.ts +0 -4
  95. package/dist/next/vercel-personalization.js +0 -4
  96. package/dist/next/vercel-s3-personalization.d.ts +0 -79
  97. package/dist/next/vercel-s3-personalization.js +0 -570
  98. package/dist/providers/s3.d.ts +0 -47
  99. package/dist/providers/s3.js +0 -434
  100. package/dist/types/render-mode.d.ts +0 -2
  101. package/dist/types/render-mode.js +0 -1
  102. package/dist/types/renderer.d.ts +0 -71
  103. package/dist/types/renderer.js +0 -1
  104. package/src/types/render-mode.ts +0 -10
  105. package/src/types/renderer.ts +0 -83
  106. package/templates/nextjs/starter-content/src/middleware.ts +0 -41
  107. package/templates/nextjs/template/src/app/gradial/assets/[...path]/route.ts +0 -1
  108. package/templates/nextjs/template/src/middleware.ts +0 -41
  109. package/templates/nextjs/template/src/middleware.ts.tmpl +0 -40
@@ -1,79 +1,77 @@
1
1
  import { headers } from 'next/headers.js';
2
2
  import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError, } from '../content/provider.js';
3
- import { getCachedManifest, getCachedSiteConfig } from '../content/cache.js';
4
- import { S3ContentProvider } from '../providers/s3.js';
3
+ import { getCachedSiteConfig } from '../content/cache.js';
4
+ import { ObjectContentProvider } from '../providers/object.js';
5
+ import { VercelReleaseObjectReader } from '../providers/vercel-object-reader.js';
5
6
  import { GRADIAL_RENDER_INPUT_HEADER, getPendingRenderInput } from '../runtime/page.js';
6
- import { attachRenderProofReceipt, selectVerifiedPage, } from '../internal/render-proof.js';
7
- import { getUncachedEdgeConfigValue } from './edge-config.js';
8
- import { activeReleaseV1Key, clampDeploymentCompatibleReleaseV1, decodeActiveReleaseV1, decodeDeploymentCompatibilityV1, deploymentCompatibilityKey, } from './vercel-edge-projection-v1.js';
9
7
  export { FragmentNotFoundError, PageNotFoundError, LayoutNotFoundError };
10
- const MAX_S3_PROVIDER_CACHE_ENTRIES = 8;
11
- const s3ProviderCache = [];
12
- const s3ProviderReleaseIds = new WeakMap();
13
- export async function getPage(route = '', config = {}) {
14
- const runtimeInput = await getPageRuntimeRenderInput();
15
- if (runtimeInput)
16
- return runtimeInput.page;
17
- const provider = await resolveProviderWithFallback(config);
18
- return (await selectRenderPage(provider, route)).page;
19
- }
20
- export async function getSiteConfig(config = {}) {
21
- const runtimeInput = await getPageRuntimeRenderInput();
22
- if (runtimeInput)
23
- return runtimeInput.siteConfig;
24
- const provider = await resolveProviderWithFallback(config);
8
+ const releaseObjectReaderSymbol = Symbol.for('@gradial/aci.release-object-reader');
9
+ let defaultReleaseObjectReader;
10
+ export function setReleaseObjectReader(reader) {
11
+ const scope = globalThis;
12
+ if (reader)
13
+ scope[releaseObjectReaderSymbol] = reader;
14
+ else
15
+ delete scope[releaseObjectReaderSymbol];
16
+ }
17
+ export async function getPage(route = '', context = {}) {
18
+ const compilerInput = await getCompilerRenderInput();
19
+ if (compilerInput)
20
+ return compilerInput.page;
21
+ const provider = await resolveProvider(context);
22
+ return await provider.getPage(route);
23
+ }
24
+ export async function getSiteConfig(context = {}) {
25
+ const compilerInput = await getCompilerRenderInput();
26
+ if (compilerInput)
27
+ return compilerInput.siteConfig;
28
+ const provider = await resolveProvider(context);
25
29
  return await provider.getSiteConfig();
26
30
  }
27
- export async function getFragment(name, config = {}) {
28
- const runtimeInput = await getPageRuntimeRenderInput();
29
- if (runtimeInput) {
30
- const fragments = runtimeInput.fragments || {};
31
- if (name in fragments)
32
- return fragments[name];
31
+ export async function getFragment(name, context = {}) {
32
+ const compilerInput = await getCompilerRenderInput();
33
+ if (compilerInput && name in (compilerInput.fragments || {})) {
34
+ return compilerInput.fragments[name];
33
35
  }
34
- const provider = await resolveProviderWithFallback(config);
36
+ const provider = await resolveProvider(context);
35
37
  return await provider.getFragment(name);
36
38
  }
37
- export async function getLayout(name, config = {}) {
38
- const runtimeInput = await getPageRuntimeRenderInput();
39
- if (runtimeInput) {
40
- const layouts = runtimeInput.layouts || {};
41
- if (name in layouts)
42
- return layouts[name];
39
+ export async function getLayout(name, context = {}) {
40
+ const compilerInput = await getCompilerRenderInput();
41
+ if (compilerInput && name in (compilerInput.layouts || {})) {
42
+ return compilerInput.layouts[name];
43
43
  }
44
- const provider = await resolveProviderWithFallback(config);
44
+ const provider = await resolveProvider(context);
45
45
  return await provider.getLayout(name);
46
46
  }
47
- export async function getRoutes(config = {}) {
48
- const provider = await resolveProviderWithFallback(config);
47
+ export async function getRoutes(context = {}) {
48
+ const provider = await resolveProvider(context);
49
49
  const routes = await provider.listRoutes();
50
50
  return routes.map((route) => route.path).sort();
51
51
  }
52
- export async function getRenderInput(route = '/', config = {}) {
53
- const runtimeInput = await getPageRuntimeRenderInput();
54
- if (runtimeInput) {
55
- return runtimeInput;
52
+ export async function getRenderInput(route = '/', context = {}) {
53
+ const compilerInput = await getCompilerRenderInput();
54
+ if (compilerInput) {
55
+ return compilerInput;
56
56
  }
57
- const provider = await resolveProviderWithFallback(config);
58
- // Cache shared resources (manifest, siteConfig) but not per-page content
59
- // Page-level caching is handled by CDN
60
- const [selection, siteConfig] = await Promise.all([
61
- selectRenderPage(provider, route),
57
+ const provider = await resolveProvider(context);
58
+ const [page, siteConfig] = await Promise.all([
59
+ provider.getPage(route),
62
60
  getCachedSiteConfig(provider),
63
61
  ]);
64
- const input = {
62
+ return {
65
63
  route,
66
64
  domain: siteConfig.domain || 'www.aci.local',
67
65
  locale: siteConfig.defaultLocale || 'en-us',
68
66
  siteConfig,
69
- page: selection.page,
67
+ page,
70
68
  };
71
- if (selection.receipt) {
72
- attachRenderProofReceipt(input, selection.receipt);
73
- }
74
- return input;
75
69
  }
76
- export async function getPageRuntimeRenderInput() {
70
+ // getCompilerRenderInput is the offline page-compiler seam used by the Next
71
+ // capsule harness. The header is only an opaque lookup key into this process's
72
+ // pending-input map; viewer requests cannot supply release or content
73
+ // authority through it, and hosted CMS routes never enter Next.
74
+ async function getCompilerRenderInput() {
77
75
  try {
78
76
  const current = await headers();
79
77
  return getPendingRenderInput(current.get(GRADIAL_RENDER_INPUT_HEADER));
@@ -82,47 +80,6 @@ export async function getPageRuntimeRenderInput() {
82
80
  return null;
83
81
  }
84
82
  }
85
- // The personalization edge proxies framework-origin requests with
86
- // X-Aci-Content-Ref naming the exact page payload its decision
87
- // selected (base or variant). When present, the SSR render must use that
88
- // payload — otherwise every visitor gets the base page and edge decisioning
89
- // is cosmetic. The S3 provider only serves refs the release manifest
90
- // declares.
91
- const ACI_CONTENT_PATH_HEADER = 'x-aci-content-path';
92
- const ACI_VARIANT_KEY_HEADER = 'x-aci-variant-key';
93
- async function selectRenderPage(provider, route) {
94
- if (!(provider instanceof S3ContentProvider)) {
95
- return { page: await provider.getPage(route) };
96
- }
97
- const releaseId = s3ProviderReleaseIds.get(provider);
98
- if (!releaseId) {
99
- throw new Error('ACI S3 page integrity error: provider release is unavailable');
100
- }
101
- let contentRef = '';
102
- let contentRefPresent = false;
103
- let variantKey = '';
104
- let variantKeyPresent = false;
105
- try {
106
- const current = await headers();
107
- const selectedContentRef = current.get(ACI_CONTENT_PATH_HEADER);
108
- contentRefPresent = selectedContentRef !== null;
109
- contentRef = selectedContentRef ?? '';
110
- const selectedVariantKey = current.get(ACI_VARIANT_KEY_HEADER);
111
- variantKeyPresent = selectedVariantKey !== null;
112
- variantKey = selectedVariantKey ?? '';
113
- }
114
- catch {
115
- // `headers()` has no request context in static generation and unit tests.
116
- }
117
- return await provider[selectVerifiedPage]({
118
- releaseId,
119
- route,
120
- contentRef,
121
- contentRefPresent,
122
- variantKey,
123
- variantKeyPresent,
124
- });
125
- }
126
83
  export { isPreviewMode } from './preview-mode.js';
127
84
  export async function routeFromNextParams(params) {
128
85
  const resolved = await params;
@@ -141,207 +98,21 @@ export async function generateStaticParams() {
141
98
  : entry.path.replace(/^\/|\/$/g, '').split('/'),
142
99
  }));
143
100
  }
144
- export class ReleaseAssetNotFoundError extends Error {
145
- constructor(releaseId, assetPath, cause) {
146
- super(`ACI release asset not found: ${releaseId}/${assetPath}`);
147
- this.name = 'ReleaseAssetNotFoundError';
148
- this.cause = cause;
149
- }
150
- }
151
- export async function getReleaseAssetResponse(releaseId, assetPath, config = {}) {
152
- const clean = cleanAssetPath(assetPath);
153
- if (!releaseId || !clean) {
154
- throw new ReleaseAssetNotFoundError(releaseId, assetPath);
155
- }
156
- const provider = new S3ContentProvider({
157
- bucket: config.bucket,
158
- keyPrefix: releaseKey(config, releaseId),
159
- region: config.region,
160
- accessKeyId: config.accessKeyId,
161
- secretAccessKey: config.secretAccessKey,
162
- sessionToken: config.sessionToken,
163
- roleArn: config.roleArn,
164
- endpoint: config.endpoint,
165
- });
166
- const key = joinKey(provider.config.keyPrefix, 'assets', clean);
167
- const res = await provider.fetchRaw(key);
168
- if (!res.ok || !res.body) {
169
- throw new ReleaseAssetNotFoundError(releaseId, clean, await safeResponseText(res));
170
- }
171
- const headers = new Headers();
172
- copyHeader(res.headers, headers, 'content-type');
173
- copyHeader(res.headers, headers, 'content-length');
174
- copyHeader(res.headers, headers, 'etag');
175
- copyHeader(res.headers, headers, 'last-modified');
176
- headers.set('cache-control', 'public, max-age=300, must-revalidate');
177
- return new Response(res.body, { status: 200, headers });
178
- }
179
- // DAM assets are globally shared and content-addressed: the Go hosting
180
- // adapter stages them at the bucket ROOT under `.gradial-dam/...`
181
- // (prefixedKey(damAssetKey(path)) — see internal/domain/hosting/vercel), not
182
- // under the per-release `releases/{id}/assets/...` prefix. The tenant key
183
- // prefix (ACI_S3_KEY_PREFIX) still applies.
184
- const DAM_ASSET_KEY_PREFIX = '.gradial-dam';
185
- export function isDamAssetPath(assetPath) {
186
- const clean = cleanAssetPath(assetPath);
187
- return clean === DAM_ASSET_KEY_PREFIX || clean.startsWith(`${DAM_ASSET_KEY_PREFIX}/`);
188
- }
189
- export class DamAssetNotFoundError extends Error {
190
- constructor(assetPath, cause) {
191
- super(`ACI DAM asset not found: ${assetPath}`);
192
- this.name = 'DamAssetNotFoundError';
193
- this.cause = cause;
194
- }
195
- }
196
- export async function getDamAssetResponse(assetPath, config = {}) {
197
- const clean = cleanAssetPath(assetPath);
198
- if (!clean.startsWith(`${DAM_ASSET_KEY_PREFIX}/`)) {
199
- throw new DamAssetNotFoundError(assetPath);
200
- }
201
- const provider = new S3ContentProvider({
202
- bucket: config.bucket,
203
- keyPrefix: joinKey(config.keyPrefix || process.env.ACI_S3_KEY_PREFIX || ''),
204
- region: config.region,
205
- accessKeyId: config.accessKeyId,
206
- secretAccessKey: config.secretAccessKey,
207
- sessionToken: config.sessionToken,
208
- roleArn: config.roleArn,
209
- endpoint: config.endpoint,
210
- });
211
- const key = joinKey(provider.config.keyPrefix, clean);
212
- const res = await provider.fetchRaw(key);
213
- if (!res.ok || !res.body) {
214
- throw new DamAssetNotFoundError(clean, await safeResponseText(res));
215
- }
216
- const headers = new Headers();
217
- copyHeader(res.headers, headers, 'content-type');
218
- copyHeader(res.headers, headers, 'content-length');
219
- copyHeader(res.headers, headers, 'etag');
220
- copyHeader(res.headers, headers, 'last-modified');
221
- // DAM keys are content-addressed, so the bytes behind a key never change.
222
- headers.set('cache-control', 'public, max-age=31536000, immutable');
223
- headers.set('cdn-cache-control', 'max-age=31536000');
224
- headers.set('vercel-cdn-cache-control', 'max-age=31536000');
225
- return new Response(res.body, { status: 200, headers });
226
- }
227
- function cleanAssetPath(value) {
228
- const clean = value.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/');
229
- if (!clean || clean.split('/').some((s) => s === '..' || s === '.'))
230
- return '';
231
- return clean;
232
- }
233
- function copyHeader(from, to, name) {
234
- const value = from.get(name);
235
- if (value)
236
- to.set(name, value);
237
- }
238
- async function safeResponseText(res) {
239
- try {
240
- return await res.text();
241
- }
242
- catch {
243
- return '';
244
- }
245
- }
246
- export async function resolveReleaseId(config = {}) {
247
- if (config.releaseId)
248
- return config.releaseId;
249
- if (process.env.ACI_RELEASE_ID)
250
- return process.env.ACI_RELEASE_ID;
251
- const activeReleaseId = await activeReleaseFromEdgeConfig(config);
252
- if (activeReleaseId)
253
- return activeReleaseId;
254
- throw new Error('ACI release ID is required. Pass releaseId from the release-scoped route or set ACI_RELEASE_ID.');
255
- }
256
- async function resolveS3Provider(config) {
257
- const releaseId = await resolveReleaseId(config);
258
- if (releaseId.length > 128
259
- || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(releaseId)) {
260
- throw new Error(`ACI release ID is invalid: ${releaseId}`);
261
- }
262
- const candidate = new S3ContentProvider({
263
- bucket: config.bucket,
264
- keyPrefix: releaseKey(config, releaseId),
265
- region: config.region,
266
- accessKeyId: config.accessKeyId,
267
- secretAccessKey: config.secretAccessKey,
268
- sessionToken: config.sessionToken,
269
- roleArn: config.roleArn,
270
- endpoint: config.endpoint,
101
+ async function resolveLocalProvider() {
102
+ const { FileContentProvider } = await import('../providers/file.js');
103
+ return new FileContentProvider();
104
+ }
105
+ async function resolveProvider(context) {
106
+ if (!context.releaseId && !context.pageRef)
107
+ return await resolveLocalProvider();
108
+ if (!context.releaseId || !context.pageRef) {
109
+ throw new Error('ACI hosted content requires both releaseId and pageRef');
110
+ }
111
+ const reader = globalThis[releaseObjectReaderSymbol]
112
+ ?? (defaultReleaseObjectReader ??= new VercelReleaseObjectReader());
113
+ return new ObjectContentProvider({
114
+ releaseId: context.releaseId,
115
+ pageRef: context.pageRef,
116
+ reader,
271
117
  });
272
- s3ProviderReleaseIds.set(candidate, releaseId);
273
- const cachedIndex = s3ProviderCache.findIndex((entry) => sameS3ProviderConfig(entry.config, candidate.config));
274
- if (cachedIndex >= 0) {
275
- const [cached] = s3ProviderCache.splice(cachedIndex, 1);
276
- s3ProviderCache.push(cached);
277
- return await cached.provider;
278
- }
279
- const provider = getCachedManifest(candidate).then(() => candidate);
280
- const entry = { config: candidate.config, provider };
281
- s3ProviderCache.push(entry);
282
- if (s3ProviderCache.length > MAX_S3_PROVIDER_CACHE_ENTRIES) {
283
- s3ProviderCache.shift();
284
- }
285
- try {
286
- return await provider;
287
- }
288
- catch (error) {
289
- const rejectedIndex = s3ProviderCache.findIndex((cached) => cached.provider === provider);
290
- if (rejectedIndex >= 0) {
291
- s3ProviderCache.splice(rejectedIndex, 1);
292
- }
293
- throw error;
294
- }
295
- }
296
- async function resolveProviderWithFallback(config) {
297
- if (process.env.ACI_CONTENT_PROVIDER === 'file') {
298
- const { FileContentProvider } = await import('../providers/file.js');
299
- return new FileContentProvider();
300
- }
301
- return await resolveS3Provider(config);
302
- }
303
- function sameS3ProviderConfig(left, right) {
304
- return left.bucket === right.bucket
305
- && left.keyPrefix === right.keyPrefix
306
- && left.region === right.region
307
- && left.accessKeyId === right.accessKeyId
308
- && left.secretAccessKey === right.secretAccessKey
309
- && left.sessionToken === right.sessionToken
310
- && left.roleArn === right.roleArn
311
- && left.endpoint === right.endpoint;
312
- }
313
- async function activeReleaseFromEdgeConfig(config) {
314
- const edgeConfig = config.edgeConfig || process.env.EDGE_CONFIG || '';
315
- const siteId = config.siteId || process.env.ACI_SITE_ID || '';
316
- if (!edgeConfig || !siteId)
317
- return '';
318
- const active = await activeReleasePointer(edgeConfig, siteId);
319
- if (active == null)
320
- return '';
321
- const deploymentId = config.deploymentId || process.env.VERCEL_DEPLOYMENT_ID || '';
322
- if (!deploymentId)
323
- return active.releaseId;
324
- const compatibilityValue = await getEdgeConfigValue(edgeConfig, deploymentCompatibilityKey(siteId, deploymentId));
325
- if (compatibilityValue == null)
326
- return active.releaseId;
327
- return clampDeploymentCompatibleReleaseV1(active, decodeDeploymentCompatibilityV1(compatibilityValue));
328
- }
329
- async function getEdgeConfigValue(edgeConfig, key) {
330
- return getUncachedEdgeConfigValue(edgeConfig, key);
331
- }
332
- function releaseKey(config, releaseId) {
333
- return joinKey(config.keyPrefix || process.env.ACI_S3_KEY_PREFIX || '', 'releases', releaseId);
334
- }
335
- async function activeReleasePointer(edgeConfig, siteId) {
336
- const value = await getEdgeConfigValue(edgeConfig, activeReleaseV1Key(siteId));
337
- if (value == null)
338
- return null;
339
- return decodeActiveReleaseV1(value);
340
- }
341
- function joinKey(...parts) {
342
- return parts
343
- .flatMap((part) => part.split('/'))
344
- .map((part) => part.trim())
345
- .filter(Boolean)
346
- .join('/');
347
118
  }
@@ -0,0 +1,24 @@
1
+ import { type CompiledManifest, type ContentProvider, type RouteEntry } from '../content/provider.js';
2
+ export interface ReleaseObjectReader {
3
+ readReleaseObject(releaseId: string, path: string): Promise<Uint8Array>;
4
+ }
5
+ export interface ObjectContentProviderOptions {
6
+ releaseId: string;
7
+ pageRef: string;
8
+ reader: ReleaseObjectReader;
9
+ }
10
+ /** Reads one immutable compiled release through a host-injected object reader. */
11
+ export declare class ObjectContentProvider implements ContentProvider {
12
+ #private;
13
+ readonly releaseId: string;
14
+ readonly pageRef: string;
15
+ readonly reader: ReleaseObjectReader;
16
+ constructor(options: ObjectContentProviderOptions);
17
+ listRoutes(): Promise<RouteEntry[]>;
18
+ getSiteConfig<T = unknown>(): Promise<T>;
19
+ getPage<T = unknown>(route: string): Promise<T>;
20
+ getFragment<T = unknown>(id: string): Promise<T>;
21
+ getLayout<T = unknown>(name: string): Promise<T>;
22
+ manifest(): Promise<CompiledManifest>;
23
+ private readJSON;
24
+ }
@@ -0,0 +1,105 @@
1
+ import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError, } from '../content/provider.js';
2
+ import { normalizeRoute } from '../content/routes.js';
3
+ /** Reads one immutable compiled release through a host-injected object reader. */
4
+ export class ObjectContentProvider {
5
+ releaseId;
6
+ pageRef;
7
+ reader;
8
+ #manifest;
9
+ constructor(options) {
10
+ if (!portableReleaseId(options.releaseId))
11
+ throw new Error('ACI release ID is invalid');
12
+ if (!cleanObjectPath(options.pageRef))
13
+ throw new Error('ACI page ref is invalid');
14
+ this.releaseId = options.releaseId;
15
+ this.pageRef = options.pageRef;
16
+ this.reader = options.reader;
17
+ }
18
+ async listRoutes() {
19
+ return Object.values((await this.manifest()).routes)
20
+ .sort((left, right) => left.path.localeCompare(right.path));
21
+ }
22
+ async getSiteConfig() {
23
+ const manifest = await this.manifest();
24
+ return await this.readJSON(manifest.siteConfigRef);
25
+ }
26
+ async getPage(route) {
27
+ const normalized = normalizeRoute(route);
28
+ const entries = Object.values((await this.manifest()).routes)
29
+ .filter((entry) => normalizeRoute(entry.path) === normalized);
30
+ if (entries.length !== 1)
31
+ throw new PageNotFoundError(route);
32
+ const entry = entries[0];
33
+ const candidates = [
34
+ { ref: entry.payloadRef, digest: entry.digest },
35
+ ...Object.values(entry.variants ?? {}).map((variant) => ({
36
+ ref: variant.payloadRef,
37
+ digest: variant.digest,
38
+ })),
39
+ ];
40
+ const exact = candidates.find((candidate) => candidate.ref === this.pageRef);
41
+ if (!exact)
42
+ throw new PageNotFoundError(route, new Error(`selected page ref is not declared: ${this.pageRef}`));
43
+ try {
44
+ return await this.readJSON(exact.ref, exact.digest);
45
+ }
46
+ catch (error) {
47
+ throw new PageNotFoundError(route, error);
48
+ }
49
+ }
50
+ async getFragment(id) {
51
+ const entry = (await this.manifest()).fragments[id];
52
+ if (!entry)
53
+ throw new FragmentNotFoundError(id);
54
+ try {
55
+ return await this.readJSON(entry.payloadRef, entry.digest);
56
+ }
57
+ catch (error) {
58
+ throw new FragmentNotFoundError(id, error);
59
+ }
60
+ }
61
+ async getLayout(name) {
62
+ const entry = (await this.manifest()).layouts?.[name];
63
+ if (!entry)
64
+ throw new LayoutNotFoundError(name);
65
+ try {
66
+ return await this.readJSON(`layouts/${name}/index.json`);
67
+ }
68
+ catch (error) {
69
+ throw new LayoutNotFoundError(name, error);
70
+ }
71
+ }
72
+ async manifest() {
73
+ this.#manifest ??= this.readJSON('manifest.json');
74
+ return await this.#manifest;
75
+ }
76
+ async readJSON(path, digest) {
77
+ const clean = cleanObjectPath(path);
78
+ if (!clean || clean !== path)
79
+ throw new Error(`ACI release object path is invalid: ${path}`);
80
+ const bytes = await this.reader.readReleaseObject(this.releaseId, clean);
81
+ if (digest)
82
+ await verifyDigest(bytes, digest, clean);
83
+ return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
84
+ }
85
+ }
86
+ async function verifyDigest(bytes, expected, path) {
87
+ const match = /^sha256:([0-9a-f]{64})$/.exec(expected);
88
+ if (!match)
89
+ throw new Error(`ACI manifest digest is invalid for ${path}`);
90
+ const input = Uint8Array.from(bytes).buffer;
91
+ const actual = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', input)))
92
+ .map((value) => value.toString(16).padStart(2, '0'))
93
+ .join('');
94
+ if (actual !== match[1])
95
+ throw new Error(`ACI release object digest mismatch for ${path}`);
96
+ }
97
+ function portableReleaseId(value) {
98
+ return value.length > 0 && value.length <= 128 && /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value);
99
+ }
100
+ function cleanObjectPath(value) {
101
+ const clean = value.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/');
102
+ if (!clean || clean.split('/').some((part) => part === '.' || part === '..'))
103
+ return '';
104
+ return clean;
105
+ }
@@ -0,0 +1,20 @@
1
+ import type { ReleaseObjectReader } from './object.js';
2
+ export interface VercelReleaseObjectReaderConfig {
3
+ bucket?: string;
4
+ keyPrefix?: string;
5
+ region?: string;
6
+ roleArn?: string;
7
+ endpoint?: string;
8
+ }
9
+ export interface S3CommandClient {
10
+ send(command: {
11
+ input: unknown;
12
+ }): Promise<unknown>;
13
+ }
14
+ export declare class VercelReleaseObjectReader implements ReleaseObjectReader {
15
+ readonly bucket: string;
16
+ readonly keyPrefix: string;
17
+ readonly client: S3CommandClient;
18
+ constructor(config?: VercelReleaseObjectReaderConfig, client?: S3CommandClient);
19
+ readReleaseObject(releaseId: string, path: string): Promise<Uint8Array>;
20
+ }
@@ -0,0 +1,45 @@
1
+ import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
2
+ import { awsCredentialsProvider } from '@vercel/oidc-aws-credentials-provider';
3
+ export class VercelReleaseObjectReader {
4
+ bucket;
5
+ keyPrefix;
6
+ client;
7
+ constructor(config = {}, client) {
8
+ this.bucket = value(config.bucket, process.env.ACI_S3_BUCKET || process.env.HOSTING_BUCKET);
9
+ this.keyPrefix = value(config.keyPrefix, process.env.ACI_S3_KEY_PREFIX || process.env.HOSTING_KEY_PREFIX);
10
+ const region = value(config.region, process.env.AWS_REGION) || 'us-east-1';
11
+ const roleArn = value(config.roleArn, process.env.AWS_ROLE_ARN);
12
+ const endpoint = value(config.endpoint, process.env.ACI_S3_ENDPOINT);
13
+ if (!this.bucket)
14
+ throw new Error('ACI release object reader requires an S3 bucket');
15
+ if (!roleArn && !client)
16
+ throw new Error('ACI release object reader requires AWS_ROLE_ARN');
17
+ this.client = client ?? new S3Client({
18
+ region,
19
+ ...(endpoint ? { endpoint, forcePathStyle: true } : {}),
20
+ credentials: awsCredentialsProvider({ roleArn, clientConfig: { region } }),
21
+ });
22
+ }
23
+ async readReleaseObject(releaseId, path) {
24
+ const key = joinKey(this.keyPrefix, 'releases', releaseId, path);
25
+ try {
26
+ const output = await this.client.send(new GetObjectCommand({
27
+ Bucket: this.bucket,
28
+ Key: key,
29
+ }));
30
+ if (!output.Body?.transformToByteArray) {
31
+ throw new Error('S3 returned an unsupported object body');
32
+ }
33
+ return await output.Body.transformToByteArray();
34
+ }
35
+ catch (error) {
36
+ throw new Error(`ACI release object read failed for ${releaseId}/${path}`, { cause: error });
37
+ }
38
+ }
39
+ }
40
+ function value(configured, environment) {
41
+ return (configured === undefined ? environment : configured)?.trim() || '';
42
+ }
43
+ function joinKey(...parts) {
44
+ return parts.flatMap((part) => part.split('/')).map((part) => part.trim()).filter(Boolean).join('/');
45
+ }
@@ -5,7 +5,7 @@ export declare function checkPageEntry(ctx: ConformanceContext): ConformanceChec
5
5
  export declare function checkMiddleware(ctx: ConformanceContext): ConformanceCheckResult[];
6
6
  export declare function checkNoFileContentProvider(ctx: ConformanceContext): ConformanceCheckResult[];
7
7
  export declare function checkNoCustomContent(ctx: ConformanceContext): ConformanceCheckResult[];
8
- export declare function checkNoLegacyAssetRoute(ctx: ConformanceContext): ConformanceCheckResult[];
8
+ export declare function checkNoRequestTimeAssetRoute(ctx: ConformanceContext): ConformanceCheckResult[];
9
9
  export declare function checkBuildSucceeds(ctx: ConformanceContext): ConformanceCheckResult[];
10
10
  export declare function checkContentCompile(ctx: ConformanceContext): ConformanceCheckResult[];
11
11
  export declare function checkSdkVersion(ctx: ConformanceContext): ConformanceCheckResult[];