@gradial/aci 0.1.37 → 0.1.39
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/dist/next/middleware.d.ts +1 -9
- package/dist/next/middleware.js +9 -5
- package/dist/testing/conformance/checks.js +26 -67
- package/dist/testing/conformance/preset.js +0 -6
- package/dist/testing/conformance/types.d.ts +20 -38
- package/package.json +7 -7
- package/templates/nextjs/template/src/middleware.ts.tmpl +1 -3
- package/templates/nextjs/template/tests/conformance.test.ts.tmpl +1 -0
|
@@ -1,19 +1,11 @@
|
|
|
1
1
|
import { type NextRequest } from 'next/server.js';
|
|
2
2
|
export interface GradialMiddlewareConfig {
|
|
3
|
-
/**
|
|
4
|
-
* Site identity for this deployment. Env-injected identity is valid —
|
|
5
|
-
* the platform sets ACI_SITE_ID per deployment, so passing a bare
|
|
6
|
-
* `process.env.ACI_SITE_ID` (which types as `string | undefined`) is
|
|
7
|
-
* supported. A missing value 500s cleanly at runtime
|
|
8
|
-
* ("ACI site id is not configured").
|
|
9
|
-
*/
|
|
10
|
-
siteId: string | undefined;
|
|
11
3
|
edgeConfig?: string;
|
|
12
4
|
previewSignKey?: string;
|
|
13
5
|
deploymentId?: string;
|
|
14
6
|
deploymentProtectionBypass?: string;
|
|
15
7
|
}
|
|
16
|
-
export declare function createGradialMiddleware(config
|
|
8
|
+
export declare function createGradialMiddleware(config?: GradialMiddlewareConfig): (request: NextRequest) => Promise<Response>;
|
|
17
9
|
export declare const aciMiddlewareMatcher: string[];
|
|
18
10
|
export declare const config: {
|
|
19
11
|
matcher: string[];
|
package/dist/next/middleware.js
CHANGED
|
@@ -10,7 +10,7 @@ const PASSTHROUGH_ROUTES = (typeof __ACI_PASSTHROUGH_ROUTES__ !== 'undefined' ?
|
|
|
10
10
|
const ASSET_PREFIX = typeof __ACI_ASSET_PREFIX__ !== 'undefined'
|
|
11
11
|
? __ACI_ASSET_PREFIX__
|
|
12
12
|
: '/.gradial-dam';
|
|
13
|
-
export function createGradialMiddleware(config) {
|
|
13
|
+
export function createGradialMiddleware(config = {}) {
|
|
14
14
|
return async function middleware(request) {
|
|
15
15
|
const url = request.nextUrl;
|
|
16
16
|
// ?leave_preview=1 → clear cookies, redirect to clean URL
|
|
@@ -185,8 +185,8 @@ async function activeReleaseFromEdgeConfig(config) {
|
|
|
185
185
|
return active.releaseId;
|
|
186
186
|
return clampRelease(active.releaseId, active.sequence || 0, compatibility);
|
|
187
187
|
}
|
|
188
|
-
function siteIdForConfig(
|
|
189
|
-
return
|
|
188
|
+
function siteIdForConfig(_config) {
|
|
189
|
+
return process.env.ACI_SITE_ID || '';
|
|
190
190
|
}
|
|
191
191
|
async function getEdgeConfigJSON(edgeConfig, key) {
|
|
192
192
|
const value = await getEdgeConfigValue(edgeConfig, key);
|
|
@@ -244,7 +244,11 @@ function edgeKeySegment(value) {
|
|
|
244
244
|
return value.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
245
245
|
}
|
|
246
246
|
function isAssetPath(pathname) {
|
|
247
|
-
|
|
247
|
+
if (pathname === ASSET_PREFIX || pathname.startsWith(`${ASSET_PREFIX}/`))
|
|
248
|
+
return true;
|
|
249
|
+
if (pathname.startsWith('/gradial/assets/'))
|
|
250
|
+
return true;
|
|
251
|
+
return false;
|
|
248
252
|
}
|
|
249
253
|
function isPassthroughRoute(pathname) {
|
|
250
254
|
return PASSTHROUGH_ROUTES.some((route) => routeMatches(pathname, route));
|
|
@@ -272,7 +276,7 @@ function normalizeRoutePath(value) {
|
|
|
272
276
|
return pathname.replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
|
|
273
277
|
}
|
|
274
278
|
export const aciMiddlewareMatcher = [
|
|
275
|
-
'/((?!_next(?:/|$)|api(?:/|$)|favicon\\.ico$|\\.gradial-dam(?:/|$)
|
|
279
|
+
'/((?!_next(?:/|$)|api(?:/|$)|favicon\\.ico$|\\.gradial-dam(?:/|$)).*)',
|
|
276
280
|
];
|
|
277
281
|
export const config = {
|
|
278
282
|
matcher: aciMiddlewareMatcher,
|
|
@@ -89,17 +89,25 @@ function walkFiles(dir, predicate) {
|
|
|
89
89
|
// ---------------------------------------------------------------------------
|
|
90
90
|
export function checkConfig(ctx) {
|
|
91
91
|
const results = [];
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
// site and drifts silently from the deployed identity.
|
|
96
|
-
if (ctx.siteIdMode === 'env') {
|
|
92
|
+
// Validate .aci.yaml siteId: when ctx.siteId is provided, verify it matches;
|
|
93
|
+
// when not provided, verify .aci.yaml does not pin a siteId at all.
|
|
94
|
+
{
|
|
97
95
|
const aciYaml = readTextFile(join(ctx.rootDir, '.aci.yaml'));
|
|
98
96
|
if (aciYaml !== null) {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
const siteIdMatch = aciYaml.match(/^siteId\s*:\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
98
|
+
if (ctx.siteId) {
|
|
99
|
+
const declared = siteIdMatch ? siteIdMatch[1].trim() : null;
|
|
100
|
+
results.push(declared === ctx.siteId
|
|
101
|
+
? ok('config', '.aci.yaml siteId matches expected value')
|
|
102
|
+
: fail('config', '.aci.yaml siteId matches expected value', declared
|
|
103
|
+
? `.aci.yaml siteId is '${declared}' but expected '${ctx.siteId}'`
|
|
104
|
+
: `.aci.yaml is missing siteId (expected '${ctx.siteId}')`));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
results.push(!siteIdMatch
|
|
108
|
+
? ok('config', '.aci.yaml does not pin a siteId')
|
|
109
|
+
: fail('config', '.aci.yaml does not pin a siteId', '.aci.yaml declares siteId but no expected value was provided — either pass siteId to the preset or remove it from .aci.yaml'));
|
|
110
|
+
}
|
|
103
111
|
}
|
|
104
112
|
}
|
|
105
113
|
if (ctx.framework === 'next') {
|
|
@@ -255,71 +263,22 @@ export function checkMiddleware(ctx) {
|
|
|
255
263
|
results.push(usesSDKMiddleware
|
|
256
264
|
? ok('middleware', 'middleware uses createGradialMiddleware', rel)
|
|
257
265
|
: fail('middleware', 'middleware uses createGradialMiddleware', rel + ' should use createGradialMiddleware from @gradial/aci/next/middleware'));
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
// (a) a string literal exactly equal to ctx.siteId (the tenant's
|
|
263
|
-
// canonical site id) — this is what the template stamps;
|
|
264
|
-
// (b) process.env.ACI_SITE_ID || '<literal>' (or ?? instead of ||)
|
|
265
|
-
// where the literal default is exactly ctx.siteId — the
|
|
266
|
-
// multi-tenant/dev-mirror form: the deployment env supplies the
|
|
267
|
-
// per-tenant identity, the default keeps local dev correct.
|
|
268
|
-
//
|
|
269
|
-
// 'env' (tenant-agnostic repos deployed to multiple sites): the ONLY
|
|
270
|
-
// accepted form is exactly `siteId: process.env.ACI_SITE_ID`. The
|
|
271
|
-
// platform owns tenant identity — terraform sets ACI_SITE_ID on every
|
|
272
|
-
// deployment — and the runtime 500s cleanly if it is missing, so any
|
|
273
|
-
// literal, fallback (|| / ??), other env var name, or computed
|
|
274
|
-
// expression fails.
|
|
275
|
-
//
|
|
276
|
-
// Anything else (placeholder literal, wrong/missing default, other env
|
|
277
|
-
// var names, computed expressions) fails: a wrong siteId means the edge
|
|
278
|
-
// release-pointer key never matches and every live route 503s.
|
|
279
|
-
const siteIdMode = ctx.siteIdMode ?? 'literal';
|
|
280
|
-
if (siteIdMode === 'env') {
|
|
281
|
-
const label = 'middleware configures canonical siteId';
|
|
282
|
-
// Narrow the search to the createGradialMiddleware call when present so
|
|
283
|
-
// an unrelated siteId reference elsewhere in the file can't satisfy us.
|
|
284
|
-
const callIdx = middlewareFile.content.indexOf('createGradialMiddleware(');
|
|
285
|
-
const region = callIdx >= 0 ? middlewareFile.content.slice(callIdx) : middlewareFile.content;
|
|
286
|
-
// Exactly `siteId: process.env.ACI_SITE_ID` followed by the end of the
|
|
287
|
-
// property (`,` or `}`) — same object-property shape the literal-mode
|
|
288
|
-
// regexes target, but with nothing allowed after the env access.
|
|
289
|
-
const bareEnv = /siteId\s*:\s*process\.env\.ACI_SITE_ID\s*(?=[,}])/.test(region);
|
|
290
|
-
results.push(bareEnv
|
|
291
|
-
? ok('middleware', label, `${rel} (env-only siteId)`)
|
|
292
|
-
: fail('middleware', label, `${rel}: middleware must read siteId only from process.env.ACI_SITE_ID (no literal or fallback) — the platform owns tenant identity and injects ACI_SITE_ID per deployment`));
|
|
293
|
-
}
|
|
294
|
-
else if (ctx.siteId) {
|
|
295
|
-
const label = 'middleware configures canonical siteId';
|
|
296
|
-
const expected = ctx.siteId;
|
|
297
|
-
const acceptedForms = `accepted forms: siteId: '${expected}' or ` +
|
|
298
|
-
`siteId: process.env.ACI_SITE_ID || '${expected}' (?? also accepted)`;
|
|
299
|
-
// Narrow the search to the createGradialMiddleware call when present so
|
|
300
|
-
// an unrelated siteId reference elsewhere in the file can't satisfy us.
|
|
266
|
+
// Middleware must NOT pass a siteId — the SDK reads process.env.ACI_SITE_ID
|
|
267
|
+
// internally. Any siteId arg is stale config that should be removed.
|
|
268
|
+
{
|
|
269
|
+
const label = 'middleware does not pass siteId';
|
|
301
270
|
const callIdx = middlewareFile.content.indexOf('createGradialMiddleware(');
|
|
302
271
|
const region = callIdx >= 0 ? middlewareFile.content.slice(callIdx) : middlewareFile.content;
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const configured = envMatch ? envMatch[2] : literalMatch ? literalMatch[2] : null;
|
|
308
|
-
if (configured === expected) {
|
|
309
|
-
results.push(ok('middleware', label, `${rel} (${envMatch ? 'env with canonical default' : 'canonical literal'})`));
|
|
310
|
-
}
|
|
311
|
-
else if (configured !== null) {
|
|
312
|
-
results.push(fail('middleware', label, `${rel}: siteId ${envMatch ? 'default ' : ''}is '${configured}' but the canonical site id is '${expected}'; ${acceptedForms}`));
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
results.push(fail('middleware', label, `${rel}: siteId passed to createGradialMiddleware is not an accepted form (placeholder/computed expressions, other env vars, and process.env.ACI_SITE_ID without a literal default all fail); ${acceptedForms}`));
|
|
316
|
-
}
|
|
272
|
+
const passesSiteId = /siteId\s*:/.test(region);
|
|
273
|
+
results.push(!passesSiteId
|
|
274
|
+
? ok('middleware', label, rel)
|
|
275
|
+
: fail('middleware', label, `${rel}: remove siteId from createGradialMiddleware() — the SDK reads process.env.ACI_SITE_ID internally; the platform injects it per deployment`));
|
|
317
276
|
}
|
|
318
277
|
// Check that the middleware matcher is inlined (not imported) and matches
|
|
319
278
|
// the SDK's canonical value. Next.js requires config.matcher to be
|
|
320
279
|
// statically analyzable, so importing the matcher from the SDK breaks the
|
|
321
280
|
// build. Instead the template inlines the value and this check detects drift.
|
|
322
|
-
const canonicalMatcher = "['/((?!_next(?:/|$)|api(?:/|$)|favicon\\\\.ico$|\\\\.gradial-dam(?:/|$)
|
|
281
|
+
const canonicalMatcher = "['/((?!_next(?:/|$)|api(?:/|$)|favicon\\\\.ico$|\\\\.gradial-dam(?:/|$)).*)']";
|
|
323
282
|
// Check for actual import/usage of aciMiddlewareMatcher (not just a mention
|
|
324
283
|
// in a comment). Look for `matcher: aciMiddlewareMatcher` or
|
|
325
284
|
// `import { ... aciMiddlewareMatcher ... }`.
|
|
@@ -56,11 +56,8 @@ function resolveChecks(input) {
|
|
|
56
56
|
export function runConformanceChecks(options) {
|
|
57
57
|
const ctx = {
|
|
58
58
|
framework: options.framework,
|
|
59
|
-
// 'env' mode carries no site identity in the repo; checks never compare
|
|
60
|
-
// against a value, so the context uses the empty string.
|
|
61
59
|
siteId: options.siteId ?? '',
|
|
62
60
|
rootDir: options.rootDir || process.cwd(),
|
|
63
|
-
siteIdMode: options.siteIdMode || 'literal',
|
|
64
61
|
};
|
|
65
62
|
const checks = resolveChecks(options.checks);
|
|
66
63
|
const allResults = checks.flatMap((name) => {
|
|
@@ -85,11 +82,8 @@ export function conformancePreset(options) {
|
|
|
85
82
|
test('ACI conformance', async (t) => {
|
|
86
83
|
const ctx = {
|
|
87
84
|
framework: options.framework,
|
|
88
|
-
// 'env' mode carries no site identity in the repo; checks never compare
|
|
89
|
-
// against a value, so the context uses the empty string.
|
|
90
85
|
siteId: options.siteId ?? '',
|
|
91
86
|
rootDir: options.rootDir || process.cwd(),
|
|
92
|
-
siteIdMode: options.siteIdMode || 'literal',
|
|
93
87
|
};
|
|
94
88
|
for (const checkName of checks) {
|
|
95
89
|
await t.test(checkName, () => {
|
|
@@ -8,10 +8,27 @@
|
|
|
8
8
|
export type ConformanceFramework = 'next' | 'astro';
|
|
9
9
|
/** Individual check names that can be enabled/disabled. */
|
|
10
10
|
export type ConformanceCheckName = 'config' | 'registry' | 'pageEntry' | 'middleware' | 'noFileContentProvider' | 'noCustomContent' | 'noLegacyAssetRoute' | 'buildSucceeds' | 'contentCompile' | 'sdkVersion';
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Options for the conformance preset.
|
|
13
|
+
*
|
|
14
|
+
* Middleware must always read siteId from `process.env.ACI_SITE_ID` — the
|
|
15
|
+
* platform owns tenant identity and injects it per deployment. Literals,
|
|
16
|
+
* fallbacks (`||`/`??`), and other env var names all fail. A missing env
|
|
17
|
+
* produces a clean 500 at runtime.
|
|
18
|
+
*
|
|
19
|
+
* `siteId` is optional — when provided, .aci.yaml's siteId is validated
|
|
20
|
+
* against it. When omitted, the check verifies .aci.yaml does not pin a
|
|
21
|
+
* siteId at all.
|
|
22
|
+
*/
|
|
23
|
+
export interface ConformancePresetOptions {
|
|
13
24
|
/** Framework the site uses. */
|
|
14
25
|
framework: ConformanceFramework;
|
|
26
|
+
/**
|
|
27
|
+
* Expected canonical siteId in .aci.yaml. When provided, the config check
|
|
28
|
+
* verifies .aci.yaml declares this exact value. When omitted, the config
|
|
29
|
+
* check verifies .aci.yaml does NOT declare a siteId.
|
|
30
|
+
*/
|
|
31
|
+
siteId?: string;
|
|
15
32
|
/** Root directory of the site project. Defaults to process.cwd(). */
|
|
16
33
|
rootDir?: string;
|
|
17
34
|
/**
|
|
@@ -20,36 +37,6 @@ export interface ConformancePresetCommonOptions {
|
|
|
20
37
|
*/
|
|
21
38
|
checks?: ConformanceCheckName[] | Partial<Record<ConformanceCheckName, boolean>>;
|
|
22
39
|
}
|
|
23
|
-
/**
|
|
24
|
-
* Options for the conformance preset.
|
|
25
|
-
*
|
|
26
|
-
* The siteId requirement is keyed off `siteIdMode` (defaults to 'literal'):
|
|
27
|
-
*
|
|
28
|
-
* - 'literal': `siteId` is REQUIRED — it is the tenant's canonical site id,
|
|
29
|
-
* and the middleware must pass it as a string literal (or
|
|
30
|
-
* `process.env.ACI_SITE_ID || '<canonical>'` / `??` with the canonical
|
|
31
|
-
* literal as the default). This is what the template stamps for
|
|
32
|
-
* single-tenant repos.
|
|
33
|
-
* - 'env': `siteId` is FORBIDDEN — tenant-agnostic repos deployed to
|
|
34
|
-
* multiple sites have no site identity in the repo at all. The middleware
|
|
35
|
-
* must read siteId ONLY from the environment — exactly
|
|
36
|
-
* `siteId: process.env.ACI_SITE_ID`, with no literal default, no
|
|
37
|
-
* `||`/`??` fallback, no other env var names, and no computed
|
|
38
|
-
* expressions — and `.aci.yaml` must not pin a siteId. The platform owns
|
|
39
|
-
* tenant identity and injects ACI_SITE_ID per deployment; a missing env
|
|
40
|
-
* produces a clean 500 at runtime.
|
|
41
|
-
*/
|
|
42
|
-
export type ConformancePresetOptions = ConformancePresetCommonOptions & ({
|
|
43
|
-
/** See ConformancePresetOptions: 'literal' expects a canonical siteId in the repo. */
|
|
44
|
-
siteIdMode?: 'literal';
|
|
45
|
-
/** Expected canonical siteId value (verified in config and middleware). */
|
|
46
|
-
siteId: string;
|
|
47
|
-
} | {
|
|
48
|
-
/** See ConformancePresetOptions: 'env' means the repo carries no site identity. */
|
|
49
|
-
siteIdMode: 'env';
|
|
50
|
-
/** Forbidden in env mode — the repo must not carry a site identity. */
|
|
51
|
-
siteId?: never;
|
|
52
|
-
});
|
|
53
40
|
/** Result of a single conformance check. */
|
|
54
41
|
export interface ConformanceCheckResult {
|
|
55
42
|
name: ConformanceCheckName;
|
|
@@ -68,12 +55,7 @@ export type ConformanceCheckFn = (ctx: ConformanceContext) => ConformanceCheckRe
|
|
|
68
55
|
/** Context passed to each check function. */
|
|
69
56
|
export interface ConformanceContext {
|
|
70
57
|
framework: ConformanceFramework;
|
|
71
|
-
/**
|
|
72
|
-
* Canonical siteId in 'literal' mode; '' in 'env' mode (no site identity
|
|
73
|
-
* in the repo — env-mode checks never compare against a value).
|
|
74
|
-
*/
|
|
58
|
+
/** Canonical siteId from options, or '' when not provided. */
|
|
75
59
|
siteId: string;
|
|
76
60
|
rootDir: string;
|
|
77
|
-
/** See ConformancePresetOptions.siteIdMode. Defaults to 'literal'. */
|
|
78
|
-
siteIdMode?: 'literal' | 'env';
|
|
79
61
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gradial/aci",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -127,12 +127,12 @@
|
|
|
127
127
|
"zod": "^4.0.0"
|
|
128
128
|
},
|
|
129
129
|
"optionalDependencies": {
|
|
130
|
-
"@gradial/aci-cli-darwin-arm64": "0.1.
|
|
131
|
-
"@gradial/aci-cli-darwin-x64": "0.1.
|
|
132
|
-
"@gradial/aci-cli-linux-arm64": "0.1.
|
|
133
|
-
"@gradial/aci-cli-linux-x64": "0.1.
|
|
134
|
-
"@gradial/aci-cli-win32-arm64": "0.1.
|
|
135
|
-
"@gradial/aci-cli-win32-x64": "0.1.
|
|
130
|
+
"@gradial/aci-cli-darwin-arm64": "0.1.39",
|
|
131
|
+
"@gradial/aci-cli-darwin-x64": "0.1.39",
|
|
132
|
+
"@gradial/aci-cli-linux-arm64": "0.1.39",
|
|
133
|
+
"@gradial/aci-cli-linux-x64": "0.1.39",
|
|
134
|
+
"@gradial/aci-cli-win32-arm64": "0.1.39",
|
|
135
|
+
"@gradial/aci-cli-win32-x64": "0.1.39"
|
|
136
136
|
},
|
|
137
137
|
"scripts": {
|
|
138
138
|
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { createGradialMiddleware } from '@gradial/aci/next/middleware';
|
|
2
2
|
|
|
3
|
-
export default createGradialMiddleware(
|
|
4
|
-
siteId: '{{siteName}}',
|
|
5
|
-
});
|
|
3
|
+
export default createGradialMiddleware();
|
|
6
4
|
|
|
7
5
|
// The matcher value below is inlined for Next.js static analysis compatibility.
|
|
8
6
|
// Source of truth: aciMiddlewareMatcher from '@gradial/aci/next/middleware'.
|