@gradial/aci 0.1.38 → 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.
@@ -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: GradialMiddlewareConfig): (request: NextRequest) => Promise<Response>;
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[];
@@ -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(config) {
189
- return config.siteId ?? '';
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);
@@ -89,17 +89,25 @@ function walkFiles(dir, predicate) {
89
89
  // ---------------------------------------------------------------------------
90
90
  export function checkConfig(ctx) {
91
91
  const results = [];
92
- // Env-only site identity: in siteIdMode 'env' the platform owns tenant
93
- // identity (terraform injects ACI_SITE_ID per deployment), so .aci.yaml
94
- // must not carry a siteId at all — a leftover label pins the repo to one
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 pinsSiteId = /^siteId\s*:/m.test(aciYaml);
100
- results.push(!pinsSiteId
101
- ? ok('config', '.aci.yaml does not pin a siteId (env-only identity)')
102
- : fail('config', '.aci.yaml does not pin a siteId (env-only identity)', '.aci.yaml declares siteId but this repo uses siteIdMode "env": the platform owns site identity (ACI_SITE_ID is injected per deployment), so the manifest must not pin a site — delete the siteId line'));
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,65 +263,16 @@ 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
- // Check that the siteId passed to createGradialMiddleware is canonical.
259
- // Accepted forms depend on ctx.siteIdMode (default 'literal'):
260
- //
261
- // 'literal':
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
- // Form (b): process.env.ACI_SITE_ID || '<default>' (or ??).
304
- const envMatch = region.match(/siteId\s*:\s*process\.env\.ACI_SITE_ID\s*(?:\|\||\?\?)\s*(['"`])((?:(?!\1).)*)\1/);
305
- // Form (a): bare string literal (single/double quotes or backticks).
306
- const literalMatch = region.match(/siteId\s*:\s*(['"`])((?:(?!\1).)*)\1/);
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
@@ -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
- /** Options common to both siteId modes. */
12
- export interface ConformancePresetCommonOptions {
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.38",
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.38",
131
- "@gradial/aci-cli-darwin-x64": "0.1.38",
132
- "@gradial/aci-cli-linux-arm64": "0.1.38",
133
- "@gradial/aci-cli-linux-x64": "0.1.38",
134
- "@gradial/aci-cli-win32-arm64": "0.1.38",
135
- "@gradial/aci-cli-win32-x64": "0.1.38"
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'.
@@ -10,3 +10,4 @@ conformancePreset({
10
10
  framework: 'next',
11
11
  siteId: '{{siteName}}',
12
12
  });
13
+