@louise-toolkit/astro 0.1.2 → 0.2.1
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 +8 -0
- package/dist/middleware.d.ts +27 -0
- package/dist/middleware.js +31 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -39,6 +39,14 @@ import { createLouiseMiddleware } from "@louise-toolkit/astro";
|
|
|
39
39
|
export const onRequest = createLouiseMiddleware({/* ... */});
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
Pass `apiGate: true` to deny the editor API by default: every request under
|
|
43
|
+
`/api/louise` must come from a signed-in editor before any route runs, with
|
|
44
|
+
writes and WebSocket upgrades origin-checked. This is the same gate as
|
|
45
|
+
`composeWorker({ gate })` (ADR 0012), for routes mounted as Astro API routes.
|
|
46
|
+
Middleware runs before Astro knows which route file answers, so a public route
|
|
47
|
+
is declared by path: the toolkit's form and vitals routes are exempt at their
|
|
48
|
+
default paths, and `apiGate: { isPublic: (path) => … }` adds your own.
|
|
49
|
+
|
|
42
50
|
**Actions** — the editor write path as Astro Actions, so a save is a typed call
|
|
43
51
|
rather than a hand-rolled endpoint. `louiseSaveAction`, `louiseSaveDraftAction`
|
|
44
52
|
and `louiseSettingsAction` wrap the same primitives the framework exposes, which
|
package/dist/middleware.d.ts
CHANGED
|
@@ -13,6 +13,20 @@ export interface LouiseMiddlewareRateLimit {
|
|
|
13
13
|
*/
|
|
14
14
|
kv: RateLimitBackend | (() => RateLimitBackend | undefined);
|
|
15
15
|
}
|
|
16
|
+
export interface LouiseMiddlewareApiGate {
|
|
17
|
+
/** Path the gate protects. Default `/api/louise`, on a segment boundary. */
|
|
18
|
+
prefix?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Paths under the prefix an anonymous request may still reach, on top of the
|
|
21
|
+
* toolkit's own public routes at their default mounts (forms and vitals — see
|
|
22
|
+
* `isLouisePublicPath`).
|
|
23
|
+
*
|
|
24
|
+
* A path, not a mark on the route, because middleware runs before it knows
|
|
25
|
+
* which route file will answer: a route can't declare itself public here the
|
|
26
|
+
* way `publicRoute` does for `composeWorker`.
|
|
27
|
+
*/
|
|
28
|
+
isPublic?: (pathname: string) => boolean;
|
|
29
|
+
}
|
|
16
30
|
export interface LouiseMiddlewareConfig<TEditor = unknown> {
|
|
17
31
|
/**
|
|
18
32
|
* Resolve the editor session for a request — the site wraps its own auth,
|
|
@@ -86,6 +100,19 @@ export interface LouiseMiddlewareConfig<TEditor = unknown> {
|
|
|
86
100
|
* rendered from it stay correct.
|
|
87
101
|
*/
|
|
88
102
|
rewrite?: (context: APIContext) => string | undefined | Promise<string | undefined>;
|
|
103
|
+
/**
|
|
104
|
+
* Deny-by-default gate for the editor API (ADR 0012), for routes mounted as
|
|
105
|
+
* framework API routes (`runEditorRoute`) rather than `composeWorker` routes.
|
|
106
|
+
* `true` — or an object to change the prefix or add public paths — and every
|
|
107
|
+
* request under `/api/louise` must resolve to an editor, with writes and
|
|
108
|
+
* WebSocket upgrades origin-checked, before any route runs. Gated responses
|
|
109
|
+
* get `Cache-Control: no-store` unless the route set its own. Omit to skip.
|
|
110
|
+
*
|
|
111
|
+
* Behind `composeWorker({ gate })` this is a second check on requests the
|
|
112
|
+
* worker already let through, and costs nothing extra: the editor is
|
|
113
|
+
* resolved on every request regardless.
|
|
114
|
+
*/
|
|
115
|
+
apiGate?: boolean | LouiseMiddlewareApiGate;
|
|
89
116
|
/** Edit-mode cookie name. Default {@link LOUISE_EDIT_COOKIE} (`"louise_edit"`).
|
|
90
117
|
* Change it and the `withEdgeCache` bypass predicate must be told too, or an
|
|
91
118
|
* editor gets served the cached public page. */
|
package/dist/middleware.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
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 { LOUISE_EDIT_COOKIE } from "louise-toolkit/worker";
|
|
22
|
+
import { isLouisePublicPath, LOUISE_API_PREFIX, LOUISE_EDIT_COOKIE, louiseApiGate, 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
|
|
@@ -31,6 +31,8 @@ export function createLouiseMiddleware(config) {
|
|
|
31
31
|
// The default comes from the same constant `isEditRequest` reads, so the
|
|
32
32
|
// cookie this sets and the predicate that looks for it cannot drift apart.
|
|
33
33
|
const editCookie = config.editCookie ?? LOUISE_EDIT_COOKIE;
|
|
34
|
+
const apiGate = config.apiGate === true ? {} : config.apiGate || undefined;
|
|
35
|
+
const apiPrefix = apiGate?.prefix ?? LOUISE_API_PREFIX;
|
|
34
36
|
return async (context, next) => {
|
|
35
37
|
// Rate-limit the public, unauthenticated POST surfaces before any other
|
|
36
38
|
// work. Keyed by client IP via a KV counter; `rateLimit` fails open on a KV
|
|
@@ -90,6 +92,23 @@ export function createLouiseMiddleware(config) {
|
|
|
90
92
|
// SESSION_SECRET) → public rendering. Auth degrading is fine; what it
|
|
91
93
|
// must NOT do is cancel anything else.
|
|
92
94
|
}
|
|
95
|
+
// The API gate, before extend: it needs only the editor, and a refused
|
|
96
|
+
// request shouldn't pay for the site's extra work. A `resolveEditor` that
|
|
97
|
+
// threw left `locals.editor` null above, so where pages degrade to public
|
|
98
|
+
// the API fails closed — refused, not served anonymously.
|
|
99
|
+
const pathname = context.url.pathname;
|
|
100
|
+
const gatedApi = apiGate !== undefined &&
|
|
101
|
+
underPrefix(pathname, apiPrefix) &&
|
|
102
|
+
!isLouisePublicPath(pathname) &&
|
|
103
|
+
!apiGate.isPublic?.(pathname);
|
|
104
|
+
if (gatedApi) {
|
|
105
|
+
const denied = await louiseApiGate(context.request, undefined, {
|
|
106
|
+
resolveEditor: () => locals.editor,
|
|
107
|
+
prefix: apiPrefix,
|
|
108
|
+
});
|
|
109
|
+
if (denied)
|
|
110
|
+
return finish(context, denied);
|
|
111
|
+
}
|
|
93
112
|
// extend gets its OWN catch, deliberately separate from auth's. When these
|
|
94
113
|
// shared one, `resolveEditor` throwing (a sentinel SESSION_SECRET — the
|
|
95
114
|
// dormant-until-provisioned state every module is supposed to survive)
|
|
@@ -118,6 +137,16 @@ export function createLouiseMiddleware(config) {
|
|
|
118
137
|
// for host dispatch is another tenant's page.
|
|
119
138
|
const rewrite = await config.rewrite?.(context);
|
|
120
139
|
const response = rewrite === undefined ? await next() : await next(rewrite);
|
|
140
|
+
// An editor's JSON must not land in a shared cache. A route that chose its
|
|
141
|
+
// own policy keeps it.
|
|
142
|
+
if (gatedApi && !response.headers.has("cache-control")) {
|
|
143
|
+
response.headers.set("Cache-Control", "no-store");
|
|
144
|
+
}
|
|
145
|
+
return finish(context, response);
|
|
146
|
+
};
|
|
147
|
+
/** The response-side work every answer gets, a gate refusal included. */
|
|
148
|
+
function finish(context, response) {
|
|
149
|
+
const locals = context.locals;
|
|
121
150
|
// content freshness: cached HTML would hide editor edits. Edit-mode pages are
|
|
122
151
|
// per-editor and must be live (`no-store`); public HTML `no-cache` so edits
|
|
123
152
|
// appear without a manual purge. Only HTML — hashed `/_astro/*` assets keep
|
|
@@ -139,5 +168,5 @@ export function createLouiseMiddleware(config) {
|
|
|
139
168
|
response.headers.set("X-Robots-Tag", "noindex");
|
|
140
169
|
}
|
|
141
170
|
return response;
|
|
142
|
-
}
|
|
171
|
+
}
|
|
143
172
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@louise-toolkit/astro",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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.
|
|
38
|
+
"louise-toolkit": "0.30.1"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@cloudflare/workers-types": "^5.20260829.1",
|