@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.
- package/README.md +13 -20
- package/dist/astro/assets.d.ts +6 -6
- package/dist/astro/assets.js +6 -6
- package/dist/astro/index.d.ts +0 -2
- package/dist/astro/index.js +0 -4
- package/dist/cli/emit-error-pages.mjs +0 -1
- package/dist/cli/verify-next-cdn-routes.mjs +3 -4
- package/dist/collect/aci.js +1 -1
- package/dist/collect/aci.js.sha256 +1 -1
- package/dist/collect/webvitals.d.ts +0 -3
- package/dist/collect/webvitals.js +11 -21
- package/dist/content/cache.d.ts +1 -9
- package/dist/content/cache.js +2 -37
- package/dist/content/error-pages.d.ts +1 -2
- package/dist/content/index.d.ts +0 -1
- package/dist/content/index.js +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/internal/domain/edge/identity.d.ts +14 -0
- package/dist/internal/domain/edge/identity.js +214 -0
- package/dist/internal/domain/edge/index.d.ts +4 -0
- package/dist/internal/domain/edge/index.js +6 -0
- package/dist/internal/domain/edge/personalization-core.d.ts +24 -0
- package/dist/{edge → internal/domain/edge}/personalization-core.js +165 -64
- package/dist/internal/domain/edge/record/codec.d.ts +71 -0
- package/dist/internal/domain/edge/record/codec.js +401 -0
- package/dist/internal/domain/edge/resolve.d.ts +2 -0
- package/dist/internal/domain/edge/resolve.js +76 -0
- package/dist/{next/personalization-host.d.ts → internal/domain/edge/types.d.ts} +110 -61
- package/dist/internal/domain/edge/types.js +7 -0
- package/dist/internal/platform/collectionstore/errors.d.ts +5 -0
- package/dist/internal/platform/collectionstore/errors.js +7 -0
- package/dist/next/config.d.ts +0 -8
- package/dist/next/config.js +3 -78
- package/dist/next/error-pages.d.ts +2 -3
- package/dist/next/error-pages.js +5 -5
- package/dist/next/error-seat.d.ts +2 -3
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +1 -2
- package/dist/next/page.d.ts +3 -13
- package/dist/next/page.js +39 -51
- package/dist/next/server.d.ts +12 -32
- package/dist/next/server.js +66 -295
- package/dist/providers/object.d.ts +24 -0
- package/dist/providers/object.js +105 -0
- package/dist/providers/vercel-object-reader.d.ts +20 -0
- package/dist/providers/vercel-object-reader.js +45 -0
- package/dist/testing/conformance/checks.d.ts +1 -1
- package/dist/testing/conformance/checks.js +14 -144
- package/dist/testing/conformance/index.d.ts +1 -1
- package/dist/testing/conformance/index.js +1 -1
- package/dist/testing/conformance/preset.js +1 -1
- package/dist/testing/conformance/types.d.ts +1 -1
- package/dist/types/config.d.ts +0 -4
- package/dist/types/index.d.ts +0 -2
- package/dist/types/index.js +0 -2
- package/dist/types/page.d.ts +0 -10
- package/package.json +11 -35
- package/src/cli/emit-error-pages.mjs +0 -1
- package/src/cli/verify-next-cdn-routes.mjs +8 -11
- package/src/types/config.ts +0 -5
- package/src/types/index.ts +0 -2
- package/src/types/page.ts +0 -11
- package/templates/astro/template/.aci.yaml.tmpl +0 -11
- package/templates/common/.agents/skills/aci/SKILL.md +8 -9
- package/templates/common/.agents/skills/authoring/SKILL.md +3 -4
- package/templates/common/.agents/skills/authoring/references/block-registry.md +13 -36
- package/templates/common/.agents/skills/authoring/references/contracts.md +10 -17
- package/templates/common/.agents/skills/authoring/references/repo-structure.md +2 -2
- package/templates/common/.agents/skills/authoring/references/sdk-server-functions.md +45 -95
- package/templates/common/.agents/skills/migration/references/canon-migration.md +17 -37
- package/templates/nextjs/template/.aci.yaml.tmpl +0 -11
- package/templates/nextjs/template/README.md.tmpl +0 -1
- package/templates/nextjs/template/src/app/[[...slug]]/page.tsx +1 -1
- package/dist/content/assets.d.ts +0 -18
- package/dist/content/assets.js +0 -66
- package/dist/edge/personalization-core.d.ts +0 -11
- package/dist/internal/render-proof.d.ts +0 -1
- package/dist/internal/render-proof.js +0 -197
- package/dist/next/asset-route.d.ts +0 -10
- package/dist/next/asset-route.js +0 -45
- package/dist/next/collect-route.d.ts +0 -19
- package/dist/next/collect-route.js +0 -422
- package/dist/next/edge-config.d.ts +0 -1
- package/dist/next/edge-config.js +0 -92
- package/dist/next/envelope-digest.d.ts +0 -21
- package/dist/next/envelope-digest.js +0 -58
- package/dist/next/middleware.d.ts +0 -40
- package/dist/next/middleware.js +0 -462
- package/dist/next/personalization-host.js +0 -257
- package/dist/next/vercel-collect-sdk-route.d.ts +0 -27
- package/dist/next/vercel-collect-sdk-route.js +0 -181
- package/dist/next/vercel-edge-projection-v1.d.ts +0 -24
- package/dist/next/vercel-edge-projection-v1.js +0 -880
- package/dist/next/vercel-personalization.d.ts +0 -4
- package/dist/next/vercel-personalization.js +0 -4
- package/dist/next/vercel-s3-personalization.d.ts +0 -79
- package/dist/next/vercel-s3-personalization.js +0 -570
- package/dist/providers/s3.d.ts +0 -47
- package/dist/providers/s3.js +0 -434
- package/dist/types/render-mode.d.ts +0 -2
- package/dist/types/render-mode.js +0 -1
- package/dist/types/renderer.d.ts +0 -71
- package/dist/types/renderer.js +0 -1
- package/src/types/render-mode.ts +0 -10
- package/src/types/renderer.ts +0 -83
- package/templates/nextjs/starter-content/src/middleware.ts +0 -41
- package/templates/nextjs/template/src/app/gradial/assets/[...path]/route.ts +0 -1
- package/templates/nextjs/template/src/middleware.ts +0 -41
- package/templates/nextjs/template/src/middleware.ts.tmpl +0 -40
package/dist/next/server.js
CHANGED
|
@@ -1,79 +1,77 @@
|
|
|
1
1
|
import { headers } from 'next/headers.js';
|
|
2
2
|
import { FragmentNotFoundError, LayoutNotFoundError, PageNotFoundError, } from '../content/provider.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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,
|
|
28
|
-
const
|
|
29
|
-
if (
|
|
30
|
-
|
|
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
|
|
36
|
+
const provider = await resolveProvider(context);
|
|
35
37
|
return await provider.getFragment(name);
|
|
36
38
|
}
|
|
37
|
-
export async function getLayout(name,
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
|
|
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
|
|
44
|
+
const provider = await resolveProvider(context);
|
|
45
45
|
return await provider.getLayout(name);
|
|
46
46
|
}
|
|
47
|
-
export async function getRoutes(
|
|
48
|
-
const provider = await
|
|
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 = '/',
|
|
53
|
-
const
|
|
54
|
-
if (
|
|
55
|
-
return
|
|
52
|
+
export async function getRenderInput(route = '/', context = {}) {
|
|
53
|
+
const compilerInput = await getCompilerRenderInput();
|
|
54
|
+
if (compilerInput) {
|
|
55
|
+
return compilerInput;
|
|
56
56
|
}
|
|
57
|
-
const provider = await
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
67
|
+
page,
|
|
70
68
|
};
|
|
71
|
-
if (selection.receipt) {
|
|
72
|
-
attachRenderProofReceipt(input, selection.receipt);
|
|
73
|
-
}
|
|
74
|
-
return input;
|
|
75
69
|
}
|
|
76
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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[];
|