@gradial/aci 0.1.52-rc.41 → 0.1.52-rc.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.
- package/CHANGELOG.md +18 -0
- package/README.md +4 -4
- package/dist/next/server.d.ts +2 -4
- package/dist/next/server.js +2 -31
- package/package.json +7 -7
- package/dist/next/edge-config.d.ts +0 -1
- package/dist/next/edge-config.js +0 -92
- package/dist/next/vercel-edge-projection-v1.d.ts +0 -15
- package/dist/next/vercel-edge-projection-v1.js +0 -350
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.52
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
|
|
7
|
+
- Prerelease versions are no longer committed. Canary builds
|
|
8
|
+
(`@gradial/aci@canary`) publish automatically from every SDK change on
|
|
9
|
+
`main`; release candidates (`@gradial/aci@rc`) and stable releases publish
|
|
10
|
+
on demand. All are prereleases or releases of the next stable version.
|
|
11
|
+
|
|
12
|
+
### Removed
|
|
13
|
+
|
|
14
|
+
- `@gradial/aci/next/server` no longer reads Vercel Edge Config.
|
|
15
|
+
`resolveReleaseId()` resolves an explicit release, then the verified routed
|
|
16
|
+
release, then `ACI_RELEASE_ID`, and otherwise throws; it never falls back to
|
|
17
|
+
an Edge Config active-release pointer. `FetchConfig` loses `edgeConfig`,
|
|
18
|
+
`deploymentId`, and `siteId`, which only that pointer read used. The
|
|
19
|
+
`EDGE_CONFIG` and `VERCEL_DEPLOYMENT_ID` environment variables are ignored.
|
|
20
|
+
|
|
3
21
|
## 0.1.52-rc.41
|
|
4
22
|
|
|
5
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Agentic Content Infrastructure SDK
|
|
2
2
|
|
|
3
|
-
`@gradial/aci` `0.1.52-rc.
|
|
3
|
+
`@gradial/aci` `0.1.52-rc.43` is a pre-1.0 public-surface reset for the ACI
|
|
4
4
|
client SDK. Breaking changes from the old `0.1.x` accidental surface are
|
|
5
5
|
intentional: the root import is framework-neutral, framework runtime behavior is
|
|
6
6
|
behind explicit subpaths, and the first required customer path is React/Next.
|
|
@@ -8,7 +8,7 @@ behind explicit subpaths, and the first required customer path is React/Next.
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
npm install @gradial/aci@0.1.52-rc.
|
|
11
|
+
npm install @gradial/aci@0.1.52-rc.43 zod@^4
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Root Import
|
|
@@ -137,13 +137,13 @@ host bundles select and serve immutable release artifacts.
|
|
|
137
137
|
The first RC scaffold path is Next:
|
|
138
138
|
|
|
139
139
|
```bash
|
|
140
|
-
npx @gradial/aci@0.1.52-rc.
|
|
140
|
+
npx @gradial/aci@0.1.52-rc.43 init my-site --template next-starter
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
List available templates:
|
|
144
144
|
|
|
145
145
|
```bash
|
|
146
|
-
npx @gradial/aci@0.1.52-rc.
|
|
146
|
+
npx @gradial/aci@0.1.52-rc.43 init --list
|
|
147
147
|
```
|
|
148
148
|
|
|
149
149
|
## Personalization: The One-Tag Install
|
package/dist/next/server.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { type ReleaseObjectReader } from '../providers/object.js';
|
|
|
3
3
|
import type { RenderInput } from '../content/types.js';
|
|
4
4
|
export { FragmentNotFoundError, PageNotFoundError, LayoutNotFoundError };
|
|
5
5
|
export interface FetchConfig {
|
|
6
|
-
siteId?: string;
|
|
7
6
|
bucket?: string;
|
|
8
7
|
keyPrefix?: string;
|
|
9
8
|
region?: string;
|
|
@@ -13,8 +12,6 @@ export interface FetchConfig {
|
|
|
13
12
|
roleArn?: string;
|
|
14
13
|
endpoint?: string;
|
|
15
14
|
releaseId?: string;
|
|
16
|
-
edgeConfig?: string;
|
|
17
|
-
deploymentId?: string;
|
|
18
15
|
}
|
|
19
16
|
export interface ServerRenderInput<TPage, TSite extends SiteConfig = SiteConfig> {
|
|
20
17
|
route: string;
|
|
@@ -60,6 +57,7 @@ export declare function getDamAssetResponse(assetPath: string, config?: FetchCon
|
|
|
60
57
|
* router dispatched this request to (release-context.ts — the same release,
|
|
61
58
|
* for code such as the not-found seat that receives no route params), else a
|
|
62
59
|
* process pin. A routed release outranks the pin because the catch-all of the
|
|
63
|
-
* same request renders the routed release regardless of the pin.
|
|
60
|
+
* same request renders the routed release regardless of the pin. There is no
|
|
61
|
+
* further source: without one of the three the read fails.
|
|
64
62
|
*/
|
|
65
63
|
export declare function resolveReleaseId(config?: FetchConfig): Promise<string>;
|
package/dist/next/server.js
CHANGED
|
@@ -7,9 +7,7 @@ import { S3ContentProvider } from '../providers/s3.js';
|
|
|
7
7
|
import { VercelReleaseObjectReader } from '../providers/vercel-object-reader.js';
|
|
8
8
|
import { GRADIAL_RENDER_INPUT_HEADER, getPendingRenderInput } from '../runtime/page.js';
|
|
9
9
|
import { attachRenderProofReceipt, selectVerifiedPage, } from '../internal/render-proof.js';
|
|
10
|
-
import { getUncachedEdgeConfigValue } from './edge-config.js';
|
|
11
10
|
import { routedRelease } from './release-context.js';
|
|
12
|
-
import { activeReleaseV1Key, clampDeploymentCompatibleReleaseV1, decodeActiveReleaseV1, decodeDeploymentCompatibilityV1, deploymentCompatibilityKey, } from './vercel-edge-projection-v1.js';
|
|
13
11
|
export { FragmentNotFoundError, PageNotFoundError, LayoutNotFoundError };
|
|
14
12
|
const releaseObjectReaderSymbol = Symbol.for('@gradial/aci.release-object-reader');
|
|
15
13
|
const objectProviders = new WeakMap();
|
|
@@ -299,7 +297,8 @@ async function safeResponseText(res) {
|
|
|
299
297
|
* router dispatched this request to (release-context.ts — the same release,
|
|
300
298
|
* for code such as the not-found seat that receives no route params), else a
|
|
301
299
|
* process pin. A routed release outranks the pin because the catch-all of the
|
|
302
|
-
* same request renders the routed release regardless of the pin.
|
|
300
|
+
* same request renders the routed release regardless of the pin. There is no
|
|
301
|
+
* further source: without one of the three the read fails.
|
|
303
302
|
*/
|
|
304
303
|
export async function resolveReleaseId(config = {}) {
|
|
305
304
|
if (config.releaseId)
|
|
@@ -309,9 +308,6 @@ export async function resolveReleaseId(config = {}) {
|
|
|
309
308
|
return routed.releaseId;
|
|
310
309
|
if (process.env.ACI_RELEASE_ID)
|
|
311
310
|
return process.env.ACI_RELEASE_ID;
|
|
312
|
-
const activeReleaseId = await activeReleaseFromEdgeConfig(config);
|
|
313
|
-
if (activeReleaseId)
|
|
314
|
-
return activeReleaseId;
|
|
315
311
|
throw new Error('ACI release ID is required. Pass releaseId from the release-scoped route or set ACI_RELEASE_ID.'
|
|
316
312
|
+ (routed.rejected ? ` The request carried a release context that was not used: ${routed.rejected}.` : ''));
|
|
317
313
|
}
|
|
@@ -400,34 +396,9 @@ function sameS3ProviderConfig(left, right) {
|
|
|
400
396
|
&& left.roleArn === right.roleArn
|
|
401
397
|
&& left.endpoint === right.endpoint;
|
|
402
398
|
}
|
|
403
|
-
async function activeReleaseFromEdgeConfig(config) {
|
|
404
|
-
const edgeConfig = config.edgeConfig || process.env.EDGE_CONFIG || '';
|
|
405
|
-
const siteId = config.siteId || process.env.ACI_SITE_ID || '';
|
|
406
|
-
if (!edgeConfig || !siteId)
|
|
407
|
-
return '';
|
|
408
|
-
const active = await activeReleasePointer(edgeConfig, siteId);
|
|
409
|
-
if (active == null)
|
|
410
|
-
return '';
|
|
411
|
-
const deploymentId = config.deploymentId || process.env.VERCEL_DEPLOYMENT_ID || '';
|
|
412
|
-
if (!deploymentId)
|
|
413
|
-
return active.releaseId;
|
|
414
|
-
const compatibilityValue = await getEdgeConfigValue(edgeConfig, deploymentCompatibilityKey(siteId, deploymentId));
|
|
415
|
-
if (compatibilityValue == null)
|
|
416
|
-
return active.releaseId;
|
|
417
|
-
return clampDeploymentCompatibleReleaseV1(active, decodeDeploymentCompatibilityV1(compatibilityValue));
|
|
418
|
-
}
|
|
419
|
-
async function getEdgeConfigValue(edgeConfig, key) {
|
|
420
|
-
return getUncachedEdgeConfigValue(edgeConfig, key);
|
|
421
|
-
}
|
|
422
399
|
function releaseKey(config, releaseId) {
|
|
423
400
|
return joinKey(config.keyPrefix || process.env.ACI_S3_KEY_PREFIX || '', 'releases', releaseId);
|
|
424
401
|
}
|
|
425
|
-
async function activeReleasePointer(edgeConfig, siteId) {
|
|
426
|
-
const value = await getEdgeConfigValue(edgeConfig, activeReleaseV1Key(siteId));
|
|
427
|
-
if (value == null)
|
|
428
|
-
return null;
|
|
429
|
-
return decodeActiveReleaseV1(value);
|
|
430
|
-
}
|
|
431
402
|
class InjectedReaderProvider {
|
|
432
403
|
releaseId;
|
|
433
404
|
reader;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradial/aci",
|
|
3
|
-
"version": "0.1.52-rc.
|
|
3
|
+
"version": "0.1.52-rc.43",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/gradial/bare-metal-go.git",
|
|
@@ -160,11 +160,11 @@
|
|
|
160
160
|
"zod": "^4.0.0"
|
|
161
161
|
},
|
|
162
162
|
"optionalDependencies": {
|
|
163
|
-
"@gradial/aci-cli-darwin-arm64": "0.1.52-rc.
|
|
164
|
-
"@gradial/aci-cli-darwin-x64": "0.1.52-rc.
|
|
165
|
-
"@gradial/aci-cli-linux-arm64": "0.1.52-rc.
|
|
166
|
-
"@gradial/aci-cli-linux-x64": "0.1.52-rc.
|
|
167
|
-
"@gradial/aci-cli-win32-arm64": "0.1.52-rc.
|
|
168
|
-
"@gradial/aci-cli-win32-x64": "0.1.52-rc.
|
|
163
|
+
"@gradial/aci-cli-darwin-arm64": "0.1.52-rc.43",
|
|
164
|
+
"@gradial/aci-cli-darwin-x64": "0.1.52-rc.43",
|
|
165
|
+
"@gradial/aci-cli-linux-arm64": "0.1.52-rc.43",
|
|
166
|
+
"@gradial/aci-cli-linux-x64": "0.1.52-rc.43",
|
|
167
|
+
"@gradial/aci-cli-win32-arm64": "0.1.52-rc.43",
|
|
168
|
+
"@gradial/aci-cli-win32-x64": "0.1.52-rc.43"
|
|
169
169
|
}
|
|
170
170
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function getUncachedEdgeConfigValue(edgeConfig: string, key: string): Promise<unknown>;
|
package/dist/next/edge-config.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
export async function getUncachedEdgeConfigValue(edgeConfig, key) {
|
|
2
|
-
const connection = parseEdgeConfigConnection(edgeConfig);
|
|
3
|
-
if (!connection) {
|
|
4
|
-
throw new Error('Invalid Edge Config connection string');
|
|
5
|
-
}
|
|
6
|
-
const headers = new Headers({
|
|
7
|
-
authorization: `Bearer ${connection.token}`,
|
|
8
|
-
'x-edge-config-min-updated-at': `${Number.MAX_SAFE_INTEGER}`,
|
|
9
|
-
});
|
|
10
|
-
if (process.env.VERCEL_ENV) {
|
|
11
|
-
headers.set('x-edge-config-vercel-env', process.env.VERCEL_ENV);
|
|
12
|
-
}
|
|
13
|
-
const url = `${connection.baseUrl}/item/${encodeURIComponent(key)}?version=${encodeURIComponent(connection.version)}`;
|
|
14
|
-
const response = await fetch(url, {
|
|
15
|
-
headers,
|
|
16
|
-
cache: 'no-store',
|
|
17
|
-
});
|
|
18
|
-
if (response.ok) {
|
|
19
|
-
return response.json();
|
|
20
|
-
}
|
|
21
|
-
if (response.status === 404 && response.headers.has('x-edge-config-digest')) {
|
|
22
|
-
return undefined;
|
|
23
|
-
}
|
|
24
|
-
const body = await response.text().catch(() => '');
|
|
25
|
-
throw new Error(`Edge Config read ${key} returned ${response.status}: ${body.slice(0, 500)}`);
|
|
26
|
-
}
|
|
27
|
-
function parseEdgeConfigConnection(value) {
|
|
28
|
-
const text = value.trim();
|
|
29
|
-
if (!text)
|
|
30
|
-
return null;
|
|
31
|
-
return parseEdgeConfigQueryConnection(text)
|
|
32
|
-
|| parseVercelEdgeConfigURL(text)
|
|
33
|
-
|| parseExternalEdgeConfigURL(text);
|
|
34
|
-
}
|
|
35
|
-
function parseEdgeConfigQueryConnection(value) {
|
|
36
|
-
if (!value.startsWith('edge-config:'))
|
|
37
|
-
return null;
|
|
38
|
-
const params = new URLSearchParams(value.slice('edge-config:'.length));
|
|
39
|
-
const id = params.get('id') || '';
|
|
40
|
-
const token = params.get('token') || '';
|
|
41
|
-
if (!id || !token)
|
|
42
|
-
return null;
|
|
43
|
-
return {
|
|
44
|
-
baseUrl: `https://edge-config.vercel.com/${id}`,
|
|
45
|
-
token,
|
|
46
|
-
version: params.get('version') || '1',
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function parseVercelEdgeConfigURL(value) {
|
|
50
|
-
let url;
|
|
51
|
-
try {
|
|
52
|
-
url = new URL(value);
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
55
|
-
return null;
|
|
56
|
-
}
|
|
57
|
-
if (url.protocol !== 'https:' || url.host !== 'edge-config.vercel.com')
|
|
58
|
-
return null;
|
|
59
|
-
const id = url.pathname.split('/').filter(Boolean)[0] || '';
|
|
60
|
-
const token = url.searchParams.get('token') || '';
|
|
61
|
-
if (!id || !id.startsWith('ecfg') || !token)
|
|
62
|
-
return null;
|
|
63
|
-
return {
|
|
64
|
-
baseUrl: `https://edge-config.vercel.com/${id}`,
|
|
65
|
-
token,
|
|
66
|
-
version: url.searchParams.get('version') || '1',
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
function parseExternalEdgeConfigURL(value) {
|
|
70
|
-
let url;
|
|
71
|
-
try {
|
|
72
|
-
url = new URL(value);
|
|
73
|
-
}
|
|
74
|
-
catch {
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
let id = url.searchParams.get('id') || '';
|
|
78
|
-
const token = url.searchParams.get('token') || '';
|
|
79
|
-
const version = url.searchParams.get('version') || '1';
|
|
80
|
-
if (!id || url.pathname.startsWith('/ecfg_')) {
|
|
81
|
-
id = url.pathname.split('/').filter(Boolean)[0] || '';
|
|
82
|
-
}
|
|
83
|
-
if (!id || !token)
|
|
84
|
-
return null;
|
|
85
|
-
url.search = '';
|
|
86
|
-
const base = url.toString().replace(/\/+$/, '');
|
|
87
|
-
return {
|
|
88
|
-
baseUrl: base,
|
|
89
|
-
token,
|
|
90
|
-
version,
|
|
91
|
-
};
|
|
92
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export interface ActiveReleasePointerV1 {
|
|
2
|
-
releaseId: string;
|
|
3
|
-
sequence: bigint;
|
|
4
|
-
}
|
|
5
|
-
export interface DeploymentCompatibilityV1 {
|
|
6
|
-
minReleaseId?: string;
|
|
7
|
-
minSequence?: bigint;
|
|
8
|
-
maxReleaseId?: string;
|
|
9
|
-
maxSequence?: bigint;
|
|
10
|
-
}
|
|
11
|
-
export declare function activeReleaseV1Key(siteId: string): string;
|
|
12
|
-
export declare function deploymentCompatibilityKey(siteId: string, deploymentId: string): string;
|
|
13
|
-
export declare function decodeActiveReleaseV1(value: unknown): ActiveReleasePointerV1;
|
|
14
|
-
export declare function decodeDeploymentCompatibilityV1(value: unknown): DeploymentCompatibilityV1;
|
|
15
|
-
export declare function clampDeploymentCompatibleReleaseV1(active: ActiveReleasePointerV1, compatibility: DeploymentCompatibilityV1): string;
|
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
// The v1 Vercel Edge Config release-state contract read by the Next SSR
|
|
2
|
-
// server (next/server.ts): the active-release pointer and the per-deployment
|
|
3
|
-
// compatibility clamp. Values are canonical Go-encoded JSON strings; decoding
|
|
4
|
-
// is strict — byte-for-byte canonical form, exact fields, bounded size.
|
|
5
|
-
//
|
|
6
|
-
// The v1 edge-route projection decoder that used to live here served the
|
|
7
|
-
// retired SDK Next personalization-host lane (ADR 0041); the platform-owned
|
|
8
|
-
// edge router (internal/domain/hosting) is the only route-projection consumer
|
|
9
|
-
// now, and it reads route partitions, not Edge Config.
|
|
10
|
-
const ACTIVE_RELEASE_V1_PREFIX = 'activeReleaseV1_';
|
|
11
|
-
const MAX_EDGE_CONFIG_VALUE_BYTES = 65_536;
|
|
12
|
-
const MAX_JSON_DEPTH = 64;
|
|
13
|
-
const POSITIVE_DECIMAL = /^[1-9][0-9]*$/;
|
|
14
|
-
const MAX_INT64 = 9223372036854775807n;
|
|
15
|
-
export function activeReleaseV1Key(siteId) {
|
|
16
|
-
return `${ACTIVE_RELEASE_V1_PREFIX}${edgeKeySegment(siteId)}`;
|
|
17
|
-
}
|
|
18
|
-
export function deploymentCompatibilityKey(siteId, deploymentId) {
|
|
19
|
-
return `deploymentCompatibility_${edgeKeySegment(siteId)}_${edgeKeySegment(deploymentId)}`;
|
|
20
|
-
}
|
|
21
|
-
export function decodeActiveReleaseV1(value) {
|
|
22
|
-
const pointer = parseCanonicalJSONStringRecord(value, 'v1 active release pointer');
|
|
23
|
-
const orderedKeys = ['releaseId', 'sequence'];
|
|
24
|
-
assertExactKeys(pointer, orderedKeys, [], 'v1 active release pointer');
|
|
25
|
-
if (Object.keys(pointer).some((key, index) => key !== orderedKeys[index])) {
|
|
26
|
-
throw new Error('v1 active release pointer fields are not canonical');
|
|
27
|
-
}
|
|
28
|
-
return {
|
|
29
|
-
releaseId: requiredIdentity(pointer.releaseId, 'v1 active release ID'),
|
|
30
|
-
sequence: requiredPositiveDecimalBigInt(pointer.sequence, 'v1 active release sequence'),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
export function decodeDeploymentCompatibilityV1(value) {
|
|
34
|
-
const compatibility = parseCanonicalJSONStringRecord(value, 'v1 deployment compatibility');
|
|
35
|
-
const orderedKeys = [
|
|
36
|
-
'minReleaseId',
|
|
37
|
-
'minSequence',
|
|
38
|
-
'maxReleaseId',
|
|
39
|
-
'maxSequence',
|
|
40
|
-
];
|
|
41
|
-
assertExactKeys(compatibility, [], orderedKeys, 'v1 deployment compatibility');
|
|
42
|
-
const actualKeys = Object.keys(compatibility);
|
|
43
|
-
const expectedKeys = orderedKeys.filter((key) => (Object.prototype.hasOwnProperty.call(compatibility, key)));
|
|
44
|
-
if (actualKeys.some((key, index) => key !== expectedKeys[index])) {
|
|
45
|
-
throw new Error('v1 deployment compatibility fields are not canonical');
|
|
46
|
-
}
|
|
47
|
-
const hasMinRelease = Object.prototype.hasOwnProperty.call(compatibility, 'minReleaseId');
|
|
48
|
-
const hasMinSequence = Object.prototype.hasOwnProperty.call(compatibility, 'minSequence');
|
|
49
|
-
const hasMaxRelease = Object.prototype.hasOwnProperty.call(compatibility, 'maxReleaseId');
|
|
50
|
-
const hasMaxSequence = Object.prototype.hasOwnProperty.call(compatibility, 'maxSequence');
|
|
51
|
-
if (hasMinRelease !== hasMinSequence || hasMaxRelease !== hasMaxSequence) {
|
|
52
|
-
throw new Error('v1 deployment compatibility release IDs and sequences must be paired');
|
|
53
|
-
}
|
|
54
|
-
return {
|
|
55
|
-
...(hasMinRelease
|
|
56
|
-
? {
|
|
57
|
-
minReleaseId: requiredIdentity(compatibility.minReleaseId, 'v1 deployment compatibility minimum release ID'),
|
|
58
|
-
minSequence: requiredPositiveDecimalBigInt(compatibility.minSequence, 'v1 deployment compatibility minimum sequence'),
|
|
59
|
-
}
|
|
60
|
-
: {}),
|
|
61
|
-
...(hasMaxRelease
|
|
62
|
-
? {
|
|
63
|
-
maxReleaseId: requiredIdentity(compatibility.maxReleaseId, 'v1 deployment compatibility maximum release ID'),
|
|
64
|
-
maxSequence: requiredPositiveDecimalBigInt(compatibility.maxSequence, 'v1 deployment compatibility maximum sequence'),
|
|
65
|
-
}
|
|
66
|
-
: {}),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
export function clampDeploymentCompatibleReleaseV1(active, compatibility) {
|
|
70
|
-
if (compatibility.minSequence !== undefined
|
|
71
|
-
&& active.sequence < compatibility.minSequence
|
|
72
|
-
&& compatibility.minReleaseId) {
|
|
73
|
-
return compatibility.minReleaseId;
|
|
74
|
-
}
|
|
75
|
-
if (compatibility.maxSequence !== undefined
|
|
76
|
-
&& active.sequence > compatibility.maxSequence
|
|
77
|
-
&& compatibility.maxReleaseId) {
|
|
78
|
-
return compatibility.maxReleaseId;
|
|
79
|
-
}
|
|
80
|
-
return active.releaseId;
|
|
81
|
-
}
|
|
82
|
-
function parseCanonicalJSONStringRecord(value, label) {
|
|
83
|
-
if (typeof value !== 'string') {
|
|
84
|
-
throw new Error(`${label} must be a canonical JSON string`);
|
|
85
|
-
}
|
|
86
|
-
if (Buffer.byteLength(value, 'utf8') > MAX_EDGE_CONFIG_VALUE_BYTES) {
|
|
87
|
-
throw new Error(`${label} exceeds the Edge Config value bound`);
|
|
88
|
-
}
|
|
89
|
-
assertCanonicalJSONSyntax(value, label);
|
|
90
|
-
let parsed;
|
|
91
|
-
try {
|
|
92
|
-
parsed = JSON.parse(value);
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
throw new Error(`${label} is malformed JSON`, { cause: error });
|
|
96
|
-
}
|
|
97
|
-
const record = requiredRecord(parsed, label);
|
|
98
|
-
if (canonicalRecordJSONInInputOrder(record) !== value) {
|
|
99
|
-
throw new Error(`${label} is not canonical JSON`);
|
|
100
|
-
}
|
|
101
|
-
return record;
|
|
102
|
-
}
|
|
103
|
-
function canonicalSortedMap(record, encode) {
|
|
104
|
-
const entries = Object.entries(record)
|
|
105
|
-
.sort(([left], [right]) => Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8')))
|
|
106
|
-
.map(([key, item]) => [key, encode(item)]);
|
|
107
|
-
return canonicalObject(entries);
|
|
108
|
-
}
|
|
109
|
-
function canonicalUnknownNode(value) {
|
|
110
|
-
if (value === null
|
|
111
|
-
|| typeof value === 'boolean'
|
|
112
|
-
|| typeof value === 'number'
|
|
113
|
-
|| typeof value === 'string') {
|
|
114
|
-
return value;
|
|
115
|
-
}
|
|
116
|
-
if (Array.isArray(value)) {
|
|
117
|
-
return value.map(canonicalUnknownNode);
|
|
118
|
-
}
|
|
119
|
-
if (value && typeof value === 'object') {
|
|
120
|
-
return canonicalSortedMap(value, canonicalUnknownNode);
|
|
121
|
-
}
|
|
122
|
-
throw new Error('v1 edge runtime payload contains an unsupported JSON value');
|
|
123
|
-
}
|
|
124
|
-
function canonicalObject(entries) {
|
|
125
|
-
return { entries };
|
|
126
|
-
}
|
|
127
|
-
function canonicalRecordJSONInInputOrder(record) {
|
|
128
|
-
const entries = Object.entries(record).map(([key, item]) => [
|
|
129
|
-
key,
|
|
130
|
-
canonicalUnknownNode(item),
|
|
131
|
-
]);
|
|
132
|
-
return encodeCanonicalJSONNode(canonicalObject(entries));
|
|
133
|
-
}
|
|
134
|
-
function encodeCanonicalJSONNode(value) {
|
|
135
|
-
if (value === null)
|
|
136
|
-
return 'null';
|
|
137
|
-
if (typeof value === 'string')
|
|
138
|
-
return encodeGoJSONString(value);
|
|
139
|
-
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
140
|
-
return JSON.stringify(value);
|
|
141
|
-
}
|
|
142
|
-
if (Array.isArray(value)) {
|
|
143
|
-
return `[${value.map(encodeCanonicalJSONNode).join(',')}]`;
|
|
144
|
-
}
|
|
145
|
-
return `{${value.entries.map(([key, item]) => (`${encodeGoJSONString(key)}:${encodeCanonicalJSONNode(item)}`)).join(',')}}`;
|
|
146
|
-
}
|
|
147
|
-
function encodeGoJSONString(value) {
|
|
148
|
-
const normalized = Array.from(value, (character) => {
|
|
149
|
-
const code = character.charCodeAt(0);
|
|
150
|
-
return character.length === 1 && code >= 0xd800 && code <= 0xdfff
|
|
151
|
-
? '\ufffd'
|
|
152
|
-
: character;
|
|
153
|
-
}).join('');
|
|
154
|
-
return JSON.stringify(normalized).replace(/[<>&\u2028\u2029]/gu, (character) => `\\u${character.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
155
|
-
}
|
|
156
|
-
function assertExactKeys(record, required, optional, label) {
|
|
157
|
-
const allowed = new Set([...required, ...optional]);
|
|
158
|
-
for (const key of Object.keys(record)) {
|
|
159
|
-
if (!allowed.has(key)) {
|
|
160
|
-
throw new Error(`${label} contains unsupported field ${JSON.stringify(key)}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
for (const key of required) {
|
|
164
|
-
if (!Object.prototype.hasOwnProperty.call(record, key)) {
|
|
165
|
-
throw new Error(`${label} is missing field ${JSON.stringify(key)}`);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
function requiredRecord(value, label) {
|
|
170
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
171
|
-
throw new Error(`${label} must be an object`);
|
|
172
|
-
}
|
|
173
|
-
return value;
|
|
174
|
-
}
|
|
175
|
-
function requiredString(value, label) {
|
|
176
|
-
if (typeof value !== 'string' || value.length === 0) {
|
|
177
|
-
throw new Error(`${label} must be a non-empty string`);
|
|
178
|
-
}
|
|
179
|
-
return value;
|
|
180
|
-
}
|
|
181
|
-
function requiredIdentity(value, label) {
|
|
182
|
-
const result = requiredString(value, label);
|
|
183
|
-
if (result === '.'
|
|
184
|
-
|| result === '..'
|
|
185
|
-
|| result.includes('/')
|
|
186
|
-
|| result.includes('\\')) {
|
|
187
|
-
throw new Error(`${label} must be one path segment`);
|
|
188
|
-
}
|
|
189
|
-
return result;
|
|
190
|
-
}
|
|
191
|
-
function requiredPositiveDecimalBigInt(value, label) {
|
|
192
|
-
if (typeof value !== 'string' || !POSITIVE_DECIMAL.test(value)) {
|
|
193
|
-
throw new Error(`${label} must be a canonical positive decimal string`);
|
|
194
|
-
}
|
|
195
|
-
const result = BigInt(value);
|
|
196
|
-
if (result > MAX_INT64) {
|
|
197
|
-
throw new Error(`${label} exceeds the signed 64-bit sequence bound`);
|
|
198
|
-
}
|
|
199
|
-
return result;
|
|
200
|
-
}
|
|
201
|
-
function edgeKeySegment(value) {
|
|
202
|
-
return value.replace(/[^A-Za-z0-9_-]/gu, '_');
|
|
203
|
-
}
|
|
204
|
-
function assertCanonicalJSONSyntax(value, label) {
|
|
205
|
-
let index = 0;
|
|
206
|
-
function parseValue(depth) {
|
|
207
|
-
if (depth > MAX_JSON_DEPTH) {
|
|
208
|
-
throw new Error(`${label} exceeds the JSON nesting bound`);
|
|
209
|
-
}
|
|
210
|
-
const token = value[index];
|
|
211
|
-
if (token === '{') {
|
|
212
|
-
if (depth >= MAX_JSON_DEPTH) {
|
|
213
|
-
throw new Error(`${label} exceeds the JSON nesting bound`);
|
|
214
|
-
}
|
|
215
|
-
parseObject(depth + 1);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
if (token === '[') {
|
|
219
|
-
if (depth >= MAX_JSON_DEPTH) {
|
|
220
|
-
throw new Error(`${label} exceeds the JSON nesting bound`);
|
|
221
|
-
}
|
|
222
|
-
parseArray(depth + 1);
|
|
223
|
-
return;
|
|
224
|
-
}
|
|
225
|
-
if (token === '"') {
|
|
226
|
-
parseString();
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
if (token === 't') {
|
|
230
|
-
parseLiteral('true');
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
if (token === 'f') {
|
|
234
|
-
parseLiteral('false');
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
if (token === 'n') {
|
|
238
|
-
parseLiteral('null');
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
const match = /^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/
|
|
242
|
-
.exec(value.slice(index));
|
|
243
|
-
if (!match)
|
|
244
|
-
throw new Error(`${label} contains invalid JSON at byte ${index}`);
|
|
245
|
-
index += match[0].length;
|
|
246
|
-
}
|
|
247
|
-
function parseObject(depth) {
|
|
248
|
-
index += 1;
|
|
249
|
-
const keys = new Set();
|
|
250
|
-
if (value[index] === '}') {
|
|
251
|
-
index += 1;
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
for (;;) {
|
|
255
|
-
if (value[index] !== '"') {
|
|
256
|
-
throw new Error(`${label} contains invalid object key at byte ${index}`);
|
|
257
|
-
}
|
|
258
|
-
const key = parseString();
|
|
259
|
-
if (keys.has(key)) {
|
|
260
|
-
throw new Error(`${label} contains duplicate object key ${JSON.stringify(key)}`);
|
|
261
|
-
}
|
|
262
|
-
keys.add(key);
|
|
263
|
-
if (value[index] !== ':') {
|
|
264
|
-
throw new Error(`${label} contains invalid object separator at byte ${index}`);
|
|
265
|
-
}
|
|
266
|
-
index += 1;
|
|
267
|
-
parseValue(depth);
|
|
268
|
-
if (value[index] === '}') {
|
|
269
|
-
index += 1;
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
if (value[index] !== ',') {
|
|
273
|
-
throw new Error(`${label} contains invalid object delimiter at byte ${index}`);
|
|
274
|
-
}
|
|
275
|
-
index += 1;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
function parseArray(depth) {
|
|
279
|
-
index += 1;
|
|
280
|
-
if (value[index] === ']') {
|
|
281
|
-
index += 1;
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
for (;;) {
|
|
285
|
-
parseValue(depth);
|
|
286
|
-
if (value[index] === ']') {
|
|
287
|
-
index += 1;
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
if (value[index] !== ',') {
|
|
291
|
-
throw new Error(`${label} contains invalid array delimiter at byte ${index}`);
|
|
292
|
-
}
|
|
293
|
-
index += 1;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
function parseString() {
|
|
297
|
-
const start = index;
|
|
298
|
-
index += 1;
|
|
299
|
-
while (index < value.length) {
|
|
300
|
-
const code = value.charCodeAt(index);
|
|
301
|
-
if (code === 0x22) {
|
|
302
|
-
index += 1;
|
|
303
|
-
try {
|
|
304
|
-
return JSON.parse(value.slice(start, index));
|
|
305
|
-
}
|
|
306
|
-
catch (error) {
|
|
307
|
-
throw new Error(`${label} contains an invalid JSON string`, {
|
|
308
|
-
cause: error,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
if (code < 0x20) {
|
|
313
|
-
throw new Error(`${label} contains an unescaped control character`);
|
|
314
|
-
}
|
|
315
|
-
if (code === 0x5c) {
|
|
316
|
-
index += 1;
|
|
317
|
-
const escaped = value[index];
|
|
318
|
-
if (escaped === 'u') {
|
|
319
|
-
const digits = value.slice(index + 1, index + 5);
|
|
320
|
-
if (!/^[0-9a-fA-F]{4}$/.test(digits)) {
|
|
321
|
-
throw new Error(`${label} contains an invalid Unicode escape`);
|
|
322
|
-
}
|
|
323
|
-
index += 5;
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
if (!escaped || !'"\\/bfnrt'.includes(escaped)) {
|
|
327
|
-
throw new Error(`${label} contains an invalid string escape`);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
index += 1;
|
|
331
|
-
}
|
|
332
|
-
throw new Error(`${label} contains an unterminated JSON string`);
|
|
333
|
-
}
|
|
334
|
-
function parseLiteral(literal) {
|
|
335
|
-
if (!value.startsWith(literal, index)) {
|
|
336
|
-
throw new Error(`${label} contains invalid JSON at byte ${index}`);
|
|
337
|
-
}
|
|
338
|
-
index += literal.length;
|
|
339
|
-
}
|
|
340
|
-
if (value.length === 0)
|
|
341
|
-
throw new Error(`${label} is empty`);
|
|
342
|
-
parseValue(0);
|
|
343
|
-
if (index !== value.length) {
|
|
344
|
-
const remaining = value.slice(index);
|
|
345
|
-
if (/^\s/u.test(remaining)) {
|
|
346
|
-
throw new Error(`${label} contains noncanonical whitespace`);
|
|
347
|
-
}
|
|
348
|
-
throw new Error(`${label} contains trailing JSON at byte ${index}`);
|
|
349
|
-
}
|
|
350
|
-
}
|