@gradial/aci 0.1.25 → 0.1.27

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Agentic Content Infrastructure SDK
2
2
 
3
- `@gradial/aci` `0.1.21` is a pre-1.0 public-surface reset for the ACI
3
+ `@gradial/aci` `0.1.27` 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.21 zod@^4
11
+ npm install @gradial/aci@0.1.27 zod@^4
12
12
  ```
13
13
 
14
14
  ## Root Import
@@ -44,10 +44,27 @@ Next integration helpers live under explicit Next subpaths:
44
44
 
45
45
  ```ts
46
46
  import { createPage, withAci } from '@gradial/aci/next';
47
- import { createMiddleware } from '@gradial/aci/next/middleware';
47
+ import { createGradialMiddleware } from '@gradial/aci/next/middleware';
48
48
  import { DevRefresh } from '@gradial/aci/next/dev-refresh';
49
49
  ```
50
50
 
51
+ Use `ACI_SITE_ID` for the deployed site identity. Passing `siteId` to
52
+ `createGradialMiddleware()` is still supported, but the default template relies
53
+ on the environment so server and middleware resolution behave the same way.
54
+
55
+ Middleware release-scopes document routes by default, including files such as
56
+ `/robots.txt` and `/sitemap.xml`. Next internals, API routes, `favicon.ico`,
57
+ public files discovered from `public/`, and `/gradial/assets/*` are left to
58
+ Next. App-owned routes can opt out through `next.config`:
59
+
60
+ ```js
61
+ import { withAci } from '@gradial/aci/next/config';
62
+
63
+ export default withAci(nextConfig, {
64
+ passthroughRoutes: ['/healthz', '/checkout/:path*'],
65
+ });
66
+ ```
67
+
51
68
  Astro public subpath is `@gradial/aci/astro`.
52
69
 
53
70
  ## Server-Side Content Providers
@@ -68,13 +85,13 @@ go through an app-owned HTTP endpoint backed server-side by a `ContentProvider`.
68
85
  The first RC scaffold path is Next:
69
86
 
70
87
  ```bash
71
- npx @gradial/aci@0.1.21 init my-site --template next-starter
88
+ npx @gradial/aci@0.1.27 init my-site --template next-starter
72
89
  ```
73
90
 
74
91
  List available templates:
75
92
 
76
93
  ```bash
77
- npx @gradial/aci@0.1.21 init --list
94
+ npx @gradial/aci@0.1.27 init --list
78
95
  ```
79
96
 
80
97
  ## Verify Locally
Binary file
@@ -2,5 +2,13 @@ import type { NextConfig } from 'next';
2
2
  export interface WithAciOptions {
3
3
  /** Asset rewrite prefix. Defaults to '/.gradial-dam' */
4
4
  assetPrefix?: string;
5
+ /**
6
+ * Customer-owned routes that ACI middleware should leave to Next.
7
+ *
8
+ * Use exact paths such as '/healthz' or simple Next-style catch-alls such as
9
+ * '/checkout/:path*'. CMS document routes such as '/robots.txt' and
10
+ * '/sitemap.xml' should be omitted so they stay release-scoped.
11
+ */
12
+ passthroughRoutes?: string[];
5
13
  }
6
14
  export declare function withAci(nextConfig?: NextConfig, options?: WithAciOptions): NextConfig;
@@ -1,8 +1,11 @@
1
1
  import { readdirSync, existsSync } from 'fs';
2
2
  import { join, relative } from 'path';
3
+ import { createRequire } from 'module';
3
4
  import { createContentWatchWebpackPlugin } from './content-watch.js';
5
+ const require = createRequire(import.meta.url);
4
6
  export function withAci(nextConfig = {}, options = {}) {
5
7
  const publicPaths = collectPublicPaths(join(process.cwd(), 'public'));
8
+ const passthroughRoutes = normalizePassthroughRoutes(options.passthroughRoutes ?? []);
6
9
  const assetPrefix = options.assetPrefix || '/.gradial-dam';
7
10
  const userRewrites = nextConfig.rewrites;
8
11
  const userHeaders = nextConfig.headers;
@@ -60,6 +63,7 @@ export function withAci(nextConfig = {}, options = {}) {
60
63
  const webpack = require('webpack');
61
64
  config.plugins.push(new webpack.DefinePlugin({
62
65
  '__ACI_PUBLIC_PATHS__': JSON.stringify(publicPaths),
66
+ '__ACI_PASSTHROUGH_ROUTES__': JSON.stringify(passthroughRoutes),
63
67
  }));
64
68
  return typeof userWebpack === 'function'
65
69
  ? userWebpack(config, context)
@@ -67,6 +71,25 @@ export function withAci(nextConfig = {}, options = {}) {
67
71
  },
68
72
  };
69
73
  }
74
+ function normalizePassthroughRoutes(routes) {
75
+ const out = [];
76
+ const seen = new Set();
77
+ for (const route of routes) {
78
+ const clean = normalizeRoutePattern(route);
79
+ if (!clean || seen.has(clean))
80
+ continue;
81
+ seen.add(clean);
82
+ out.push(clean);
83
+ }
84
+ return out;
85
+ }
86
+ function normalizeRoutePattern(route) {
87
+ const trimmed = route.trim();
88
+ if (!trimmed)
89
+ return '';
90
+ const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
91
+ return withSlash.replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
92
+ }
70
93
  function collectPublicPaths(publicDir) {
71
94
  if (!existsSync(publicDir))
72
95
  return [];
@@ -3,6 +3,7 @@ import { getUncachedEdgeConfigValue } from './edge-config.js';
3
3
  import { PREVIEW_TOKEN_COOKIE, PREVIEW_RELEASE_COOKIE, extractPreviewToken, resolvePreviewContext, createPreviewCookies, previewSignKey, } from '../preview/core.js';
4
4
  const RELEASE_SCOPED_PREFIX = '/__r';
5
5
  const PUBLIC_PATHS = new Set(typeof __ACI_PUBLIC_PATHS__ !== 'undefined' ? __ACI_PUBLIC_PATHS__ : []);
6
+ const PASSTHROUGH_ROUTES = (typeof __ACI_PASSTHROUGH_ROUTES__ !== 'undefined' ? __ACI_PASSTHROUGH_ROUTES__ : []);
6
7
  export function createGradialMiddleware(config = {}) {
7
8
  return async function middleware(request) {
8
9
  const url = request.nextUrl;
@@ -17,6 +18,9 @@ export function createGradialMiddleware(config = {}) {
17
18
  if (PUBLIC_PATHS.has(url.pathname)) {
18
19
  return NextResponse.next();
19
20
  }
21
+ if (isPassthroughRoute(url.pathname)) {
22
+ return NextResponse.next();
23
+ }
20
24
  // Block direct access to /__r/... — only middleware rewrites may use it.
21
25
  if (isReleaseScopedPath(url.pathname, RELEASE_SCOPED_PREFIX)) {
22
26
  return new Response('not found', {
@@ -32,6 +36,12 @@ export function createGradialMiddleware(config = {}) {
32
36
  if (process.env.ACI_CONTENT_PROVIDER === 'file') {
33
37
  return NextResponse.next();
34
38
  }
39
+ if (!siteIdForConfig(config)) {
40
+ return new Response('ACI site id is not configured', {
41
+ status: 500,
42
+ headers: { 'Cache-Control': 'private, no-store' },
43
+ });
44
+ }
35
45
  return new Response('no active release', {
36
46
  status: 503,
37
47
  headers: { 'Cache-Control': 'private, no-store' },
@@ -123,7 +133,7 @@ async function resolvePreviewFromRequest(request, config) {
123
133
  console.warn('[aci/mw] GRADIAL_PREVIEW_SIGN_KEY not set — preview tokens will be ignored');
124
134
  return null;
125
135
  }
126
- return await resolvePreviewContext({ token, siteId: config.siteId || '', signKey });
136
+ return await resolvePreviewContext({ token, siteId: siteIdForConfig(config), signKey });
127
137
  }
128
138
  function cookieMap(cookies) {
129
139
  const map = new Map();
@@ -134,19 +144,23 @@ function cookieMap(cookies) {
134
144
  }
135
145
  async function activeReleaseFromEdgeConfig(config) {
136
146
  const edgeConfig = config.edgeConfig || process.env.EDGE_CONFIG || '';
137
- if (!edgeConfig || !config.siteId)
147
+ const siteId = siteIdForConfig(config);
148
+ if (!edgeConfig || !siteId)
138
149
  return '';
139
- const active = await activeReleasePointer(edgeConfig, config.siteId);
150
+ const active = await activeReleasePointer(edgeConfig, siteId);
140
151
  if (!active.releaseId)
141
152
  return '';
142
153
  const deploymentId = config.deploymentId || process.env.VERCEL_DEPLOYMENT_ID || '';
143
154
  if (!deploymentId)
144
155
  return active.releaseId;
145
- const compatibility = await getEdgeConfigJSON(edgeConfig, deploymentCompatibilityKey(config.siteId, deploymentId));
156
+ const compatibility = await getEdgeConfigJSON(edgeConfig, deploymentCompatibilityKey(siteId, deploymentId));
146
157
  if (!compatibility)
147
158
  return active.releaseId;
148
159
  return clampRelease(active.releaseId, active.sequence || 0, compatibility);
149
160
  }
161
+ function siteIdForConfig(config) {
162
+ return config.siteId || process.env.ACI_SITE_ID || '';
163
+ }
150
164
  async function getEdgeConfigJSON(edgeConfig, key) {
151
165
  const value = await getEdgeConfigValue(edgeConfig, key);
152
166
  if (!value)
@@ -202,6 +216,31 @@ function clampRelease(activeReleaseId, activeSequence, compatibility) {
202
216
  function edgeKeySegment(value) {
203
217
  return value.replace(/[^A-Za-z0-9_-]/g, '_');
204
218
  }
219
+ function isPassthroughRoute(pathname) {
220
+ return PASSTHROUGH_ROUTES.some((route) => routeMatches(pathname, route));
221
+ }
222
+ function routeMatches(pathname, route) {
223
+ const cleanPath = normalizeRoutePath(pathname);
224
+ const cleanRoute = normalizeRoutePath(route);
225
+ if (!cleanRoute)
226
+ return false;
227
+ if (cleanRoute.endsWith('/:path*')) {
228
+ const prefix = cleanRoute.slice(0, -'/:path*'.length) || '/';
229
+ return cleanPath === prefix || cleanPath.startsWith(`${prefix}/`);
230
+ }
231
+ if (cleanRoute.endsWith('/*')) {
232
+ const prefix = cleanRoute.slice(0, -'/*'.length) || '/';
233
+ return cleanPath === prefix || cleanPath.startsWith(`${prefix}/`);
234
+ }
235
+ return cleanPath === cleanRoute;
236
+ }
237
+ function normalizeRoutePath(value) {
238
+ const trimmed = value.trim();
239
+ if (!trimmed)
240
+ return '';
241
+ const pathname = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
242
+ return pathname.replace(/\/{2,}/g, '/').replace(/\/$/, '') || '/';
243
+ }
205
244
  export const config = {
206
- matcher: ['/((?!_next/).*)'],
245
+ matcher: ['/((?!_next(?:/|$)|api(?:/|$)|gradial/assets(?:/|$)|favicon\\.ico$).*)'],
207
246
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gradial/aci",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -1,4 +1,7 @@
1
1
  import { createGradialMiddleware } from '@gradial/aci/next/middleware';
2
2
 
3
3
  export default createGradialMiddleware();
4
- export { config } from '@gradial/aci/next/middleware';
4
+
5
+ export const config = {
6
+ matcher: ['/((?!_next(?:/|$)|api(?:/|$)|gradial/assets(?:/|$)|favicon\\.ico$).*)'],
7
+ };