@louise-toolkit/astro 0.5.0 → 0.6.0

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
@@ -43,9 +43,19 @@ Pass `apiGate: true` to deny the editor API by default: every request under
43
43
  `/api/louise` must come from a signed-in editor before any route runs, with
44
44
  writes and WebSocket upgrades origin-checked. This is the same gate as
45
45
  `composeWorker({ gate })` (ADR 0012), for routes mounted as Astro API routes.
46
+
47
+ An error a page, an endpoint, or the middleware throws is reported as an
48
+ incident, then re-thrown, so Astro still renders its error page. Astro catches
49
+ the error outside every middleware, so `composeWorker` never sees a throw; this
50
+ is how it still reaches the sinks you gave `composeWorker`'s `onIncident`
51
+ (ADR 0022). Pass `reportErrors: false` to turn it off.
46
52
  Middleware runs before Astro knows which route file answers, so a public route
47
53
  is declared by path: the toolkit's form, vitals, and status routes are exempt
48
54
  at their default paths, and `apiGate: { isPublic: (path) => … }` adds your own.
55
+ An MCP endpoint that takes agent tokens is declared the same way:
56
+ `apiGate: { takesBearer: (path) => path === LOUISE_MCP_PATH }` lets a request
57
+ with `Authorization: Bearer` through to that path, and only that path, for the
58
+ route to verify the token itself. It's off unless you set it.
49
59
 
50
60
  **Actions**: the editor write path as Astro Actions, so a save is a typed call
51
61
  rather than a hand-rolled endpoint. `louiseSaveAction`, `louiseSaveDraftAction`,
@@ -26,6 +26,18 @@ export interface LouiseMiddlewareApiGate {
26
26
  * way `publicRoute` does for `composeWorker`.
27
27
  */
28
28
  isPublic?: (pathname: string) => boolean;
29
+ /**
30
+ * Paths under the prefix whose route checks a bearer token itself—in
31
+ * practice, an `mcpRoute` given `resolveAgent`, at `LOUISE_MCP_PATH`. A
32
+ * request that carries `Authorization: Bearer` skips the gate on these paths
33
+ * and nowhere else; one without a bearer token is gated as usual.
34
+ *
35
+ * Off unless you set it: the gate can't see whether the route at a path
36
+ * checks tokens, and a route that relies on the gate alone would be open to
37
+ * any request with a bearer header. `composeWorker` needs none of this; it
38
+ * reads `bearerRoute`'s mark instead.
39
+ */
40
+ takesBearer?: (pathname: string) => boolean;
29
41
  }
30
42
  export interface LouiseMiddlewareConfig<TEditor = unknown> {
31
43
  /**
@@ -132,6 +144,18 @@ export interface LouiseMiddlewareConfig<TEditor = unknown> {
132
144
  location: string;
133
145
  status: number;
134
146
  } | null>;
147
+ /**
148
+ * Report an error a page, an endpoint, or this middleware throws as an
149
+ * incident (ADR 0022), then re-throw it, so Astro still renders its error
150
+ * page. Astro catches the error outside every middleware and answers with
151
+ * its 500, so `composeWorker` never sees a throw; this is how the error still
152
+ * reaches `composeWorker`'s `onIncident` sinks. Without `onIncident` it does
153
+ * nothing. Default `true`.
154
+ *
155
+ * An error a streamed page throws after its first bytes are sent happens
156
+ * outside every middleware, so no middleware can report it.
157
+ */
158
+ reportErrors?: boolean;
135
159
  /** Edit-mode cookie name. Default {@link LOUISE_EDIT_COOKIE} (`"louise_edit"`).
136
160
  * Change it and the `withEdgeCache` bypass predicate must be told too, or an
137
161
  * editor gets served the cached public page. */
@@ -140,7 +164,8 @@ export interface LouiseMiddlewareConfig<TEditor = unknown> {
140
164
  /**
141
165
  * Build the shared Louise Astro middleware: rate-limit → resolve the editor
142
166
  * session + sticky `?louise` edit mode → `next()` → content-freshness cache headers
143
- * + CSP `style-src` rewrite + transport security headers. Sites supply the bits
167
+ * + CSP `style-src` rewrite + transport security headers. A thrown error is
168
+ * reported as an incident and re-thrown (see {@link LouiseMiddlewareConfig.reportErrors}). Sites supply the bits
144
169
  * that vary via {@link LouiseMiddlewareConfig} and export the result as
145
170
  * `onRequest`.
146
171
  */
@@ -19,11 +19,12 @@
19
19
  // This subpath is the ONE place Louise touches Astro's types—`astro` is an
20
20
  // optional peer, pulled in only by sites that import `louise-toolkit/astro`.
21
21
  import { allowCspDataFonts, louiseSecurityHeaders, matchRateRule, rateLimit, rewriteCspStyleSrc, } from "louise-toolkit/security";
22
- import { isLouisePublicPath, LOUISE_API_PREFIX, LOUISE_EDIT_COOKIE, louiseApiGate, underPrefix, } from "louise-toolkit/worker";
22
+ import { hasBearerCredential, isLouisePublicPath, LOUISE_API_PREFIX, LOUISE_EDIT_COOKIE, louiseApiGate, reportIncident, underPrefix, } from "louise-toolkit/worker";
23
23
  /**
24
24
  * Build the shared Louise Astro middleware: rate-limit → resolve the editor
25
25
  * session + sticky `?louise` edit mode → `next()` → content-freshness cache headers
26
- * + CSP `style-src` rewrite + transport security headers. Sites supply the bits
26
+ * + CSP `style-src` rewrite + transport security headers. A thrown error is
27
+ * reported as an incident and re-thrown (see {@link LouiseMiddlewareConfig.reportErrors}). Sites supply the bits
27
28
  * that vary via {@link LouiseMiddlewareConfig} and export the result as
28
29
  * `onRequest`.
29
30
  */
@@ -33,7 +34,7 @@ export function createLouiseMiddleware(config) {
33
34
  const editCookie = config.editCookie ?? LOUISE_EDIT_COOKIE;
34
35
  const apiGate = config.apiGate === true ? {} : config.apiGate || undefined;
35
36
  const apiPrefix = apiGate?.prefix ?? LOUISE_API_PREFIX;
36
- return async (context, next) => {
37
+ const handle = async (context, next) => {
37
38
  // Rate-limit the public, unauthenticated POST surfaces before any other
38
39
  // work. Keyed by client IP via a KV counter; `rateLimit` fails open on a KV
39
40
  // error so a limiter outage never takes down sign-in or the contact form.
@@ -100,7 +101,8 @@ export function createLouiseMiddleware(config) {
100
101
  const gatedApi = apiGate !== undefined &&
101
102
  underPrefix(pathname, apiPrefix) &&
102
103
  !isLouisePublicPath(pathname) &&
103
- !apiGate.isPublic?.(pathname);
104
+ !apiGate.isPublic?.(pathname) &&
105
+ !(apiGate.takesBearer?.(pathname) && hasBearerCredential(context.request));
104
106
  if (gatedApi) {
105
107
  const denied = await louiseApiGate(context.request, undefined, {
106
108
  resolveEditor: () => locals.editor,
@@ -159,6 +161,18 @@ export function createLouiseMiddleware(config) {
159
161
  }
160
162
  return finish(context, response);
161
163
  };
164
+ if (config.reportErrors === false)
165
+ return handle;
166
+ const reporting = async (context, next) => {
167
+ try {
168
+ return await handle(context, next);
169
+ }
170
+ catch (err) {
171
+ reportIncident({ kind: "fetch", cause: err, request: context.request });
172
+ throw err;
173
+ }
174
+ };
175
+ return reporting;
162
176
  /** The response-side work every answer gets, a gate refusal included. */
163
177
  function finish(context, response) {
164
178
  const locals = context.locals;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@louise-toolkit/astro",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Astro adapter for louise-toolkit: middleware, Actions, content-layer loaders, and the forms schema bridge.",
5
5
  "keywords": [
6
6
  "astro",
@@ -35,7 +35,7 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "louise-toolkit": "0.36.0"
38
+ "louise-toolkit": "0.37.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@cloudflare/workers-types": "^5.20260829.1",