@louise-toolkit/astro 0.2.1 → 0.2.2

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,21 +1,21 @@
1
1
  # @louise-toolkit/astro
2
2
 
3
- **Astro adapter for [Louise Toolkit](https://github.com/bowenlabs/louise-toolkit)** —
3
+ **Astro adapter for [Louise Toolkit](https://github.com/bowenlabs/louise-toolkit)**:
4
4
  middleware, Actions, content-layer loaders, and the forms schema bridge.
5
5
 
6
- > **Status: pre-1.0, experimental.** The API changes between minor versions —
6
+ > **Status: pre-1.0, experimental.** The API changes between minor versions, so
7
7
  > pin an exact version if you depend on it.
8
8
 
9
9
  ## Why this is a separate package
10
10
 
11
11
  `louise-toolkit` is framework-agnostic, and that claim is enforced rather than
12
12
  merely stated: CI fails if the string "astro" appears anywhere in the library's
13
- source, in code or in prose. Everything that has to import Astro's types lives
13
+ source, whether that's code or prose. Everything that has to import Astro's types lives
14
14
  here instead.
15
15
 
16
- So the dependency runs one way — this package depends on `louise-toolkit`, never
17
- the reverse — and `astro` is an **optional peer**, pulled in only by sites that
18
- actually import this adapter.
16
+ So the dependency runs one way (this package depends on `louise-toolkit`, never
17
+ the reverse), and `astro` is an **optional peer**, pulled in only by sites that
18
+ import this adapter.
19
19
 
20
20
  ## Install
21
21
 
@@ -29,7 +29,7 @@ top of both, or scaffold one with `pnpm create astroid`.
29
29
 
30
30
  ## What it gives you
31
31
 
32
- **Middleware** — one factory that mounts Louise's editor routes, session handling
32
+ **Middleware**: one factory that mounts Louise's editor routes, session handling,
33
33
  and optional rate limiting into an Astro site.
34
34
 
35
35
  ```ts
@@ -47,20 +47,20 @@ Middleware runs before Astro knows which route file answers, so a public route
47
47
  is declared by path: the toolkit's form and vitals routes are exempt at their
48
48
  default paths, and `apiGate: { isPublic: (path) => … }` adds your own.
49
49
 
50
- **Actions** — the editor write path as Astro Actions, so a save is a typed call
51
- rather than a hand-rolled endpoint. `louiseSaveAction`, `louiseSaveDraftAction`
50
+ **Actions**: the editor write path as Astro Actions, so a save is a typed call
51
+ rather than a hand-rolled endpoint. `louiseSaveAction`, `louiseSaveDraftAction`,
52
52
  and `louiseSettingsAction` wrap the same primitives the framework exposes, which
53
53
  is why an agent writing over MCP and a human clicking in the editor take the
54
54
  identical code path.
55
55
 
56
- **Content-layer loaders** — `louiseLoader` feeds Louise-managed rows into Astro's
56
+ **Content-layer loaders**: `louiseLoader` feeds Louise-managed rows into Astro's
57
57
  content layer, and `collectionToAstroSchema` derives the Zod schema from the same
58
58
  `CollectionConfig` that drives codegen and the editor. One definition, not two
59
59
  that drift.
60
60
 
61
- **Catalog loader** — `defineCatalogLoader`, for commerce catalogs.
61
+ **Catalog loader**: `defineCatalogLoader`, for commerce catalogs.
62
62
 
63
- **Forms bridge** — `formToAstroSchema`, deriving an Astro-shaped schema from a
63
+ **Forms bridge**: `formToAstroSchema`, which derives an Astro-shaped schema from a
64
64
  Louise `FormConfig`.
65
65
 
66
66
  ## Full exports
package/dist/actions.d.ts CHANGED
@@ -5,8 +5,8 @@ import type { EditorRouteEnv } from "louise-toolkit/editor";
5
5
  import { type SaveDraftDeps } from "louise-toolkit/editor";
6
6
  /** The subset of Astro's `ActionError` codes the editor handlers emit. */
7
7
  type ActionErrorCode = "BAD_REQUEST" | "UNAUTHORIZED" | "FORBIDDEN" | "NOT_FOUND" | "INTERNAL_SERVER_ERROR";
8
- /** The shape of Astro's `ActionError` constructor the handlers depend on —
9
- * injected (see file header) so the toolkit needn't import `astro:actions`. */
8
+ /** The shape of Astro's `ActionError` constructor the handlers depend on—injected
9
+ * (see file header) so the toolkit needn't import `astro:actions`. */
10
10
  export interface ActionErrorCtor {
11
11
  new (opts: {
12
12
  code: ActionErrorCode;
@@ -15,7 +15,7 @@ export interface ActionErrorCtor {
15
15
  }
16
16
  /** The slice of an Astro `ActionAPIContext` the editor handlers read: the
17
17
  * middleware-resolved `locals.editor`, plus `cookies` for the D1 bookmark. The
18
- * Worker `env` is NOT read off the context — Astro v6+ removed
18
+ * Worker `env` is NOT read off the context—Astro v6+ removed
19
19
  * `Astro.locals.runtime.env`, so it's supplied by the injected
20
20
  * {@link EditorActionDeps.getEnv}. A real context (which carries much more)
21
21
  * structurally satisfies this. */
@@ -40,7 +40,7 @@ export interface EditorActionDeps<Env extends EditorRouteEnv = EditorRouteEnv> {
40
40
  * (set by `createLouiseMiddleware`). A falsy result answers 401. */
41
41
  getEditor?: (ctx: EditorActionContext) => unknown;
42
42
  /**
43
- * Resolve the Worker `env` (the D1 binding) for the Action. **Required** — Astro
43
+ * Resolve the Worker `env` (the D1 binding) for the Action. **Required**—Astro
44
44
  * v6+ removed `Astro.locals.runtime.env`, so there is no context field to default
45
45
  * to; inject the bindings explicitly, typically by closing over the Cloudflare
46
46
  * `env` (the `ctx` argument is available for per-request selection but usually
@@ -53,7 +53,7 @@ export interface EditorActionDeps<Env extends EditorRouteEnv = EditorRouteEnv> {
53
53
  */
54
54
  getEnv: (ctx: EditorActionContext) => Env;
55
55
  }
56
- /** The validated `save` input — the inline field-save body, same keys the raw
56
+ /** The validated `save` input—the inline field-save body, same keys the raw
57
57
  * route's `SAVE_BODY` uses. `value` stays `unknown`; its non-empty-string check
58
58
  * needs the collection config and so lives in `applyFieldSave`. */
59
59
  export interface SaveActionInput {
@@ -63,7 +63,7 @@ export interface SaveActionInput {
63
63
  value: unknown;
64
64
  }
65
65
  export interface LouiseSaveActionConfig<Env extends EditorRouteEnv = EditorRouteEnv> extends EditorActionDeps<Env> {
66
- /** Editable collections keyed by the client's `collection` slug — the same
66
+ /** Editable collections keyed by the client's `collection` slug—the same
67
67
  * shape the raw `saveRoute` takes. */
68
68
  collections: Record<string, SaveCollectionConfig>;
69
69
  /** Rich-HTML sanitizer; defaults to louise-toolkit/security's `sanitizeRichHtml`. */
@@ -83,8 +83,8 @@ export interface LouiseSaveDraftActionConfig<Env extends EditorRouteEnv = Editor
83
83
  /**
84
84
  * Build the `{ input, handler }` config for the editor `save` Action (the inline
85
85
  * field-save). The site drops the result into `defineAction` (see file header).
86
- * The `input` schema is validated by Astro *before* the handler runs — replacing
87
- * the raw route's manual `request.json()` + `standardValidate` — and the handler
86
+ * The `input` schema is validated by Astro *before* the handler runs—replacing
87
+ * the raw route's manual `request.json()` + `standardValidate`—and the handler
88
88
  * shares the raw route's store path via {@link applyFieldSave}, so a field is
89
89
  * validated once and written in exactly one place.
90
90
  */
@@ -119,8 +119,8 @@ export declare function louiseSettingsAction<Env extends EditorRouteEnv = Editor
119
119
  * versioned-page draft save). Mirrors {@link louiseSaveAction}, but the input
120
120
  * bundles the row `id` with the changed `data` (an Action call has no URL to carry
121
121
  * the id). The handler shares the raw `versionsRoute` store path via
122
- * {@link applySaveDraft} — the concurrent-surface merge base and the #70 KV
123
- * write-buffer — and returns that path's JSON body (a created `version`, or
122
+ * {@link applySaveDraft}—the concurrent-surface merge base and the #70 KV
123
+ * write-buffer—and returns that path's JSON body (a created `version`, or
124
124
  * `{ buffered: true }` when a write is coalesced into the buffer).
125
125
  */
126
126
  export declare function louiseSaveDraftAction<Env extends EditorRouteEnv = EditorRouteEnv>(config: LouiseSaveDraftActionConfig<Env>): {
package/dist/actions.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) 2026 BowenLabs. Louise Toolkit is MIT licensed.
2
2
  //
3
- // `louise-toolkit/astro` — the editor mutations as Astro Actions (#72): typed,
3
+ // `louise-toolkit/astro`—the editor mutations as Astro Actions (#72): typed,
4
4
  // Zod-validated server functions so a site calls `actions.louise.save(...)` /
5
5
  // `settings(...)` and gets end-to-end types + automatic input validation, instead
6
6
  // of hand-building a `fetch("/api/louise/*")` JSON body and re-parsing it.
@@ -19,14 +19,14 @@
19
19
  //
20
20
  // The Worker `env` (the D1 binding) is injected via `getEnv`, not read off the
21
21
  // context: Astro v6+ removed `Astro.locals.runtime.env`, so a site hands in its
22
- // bindings — typically `() => env` from `cloudflare:workers` — the same way the
22
+ // bindings—typically `() => env` from `cloudflare:workers`—the same way the
23
23
  // core primitives take their bindings by injection (the library never reaches for
24
24
  // `cloudflare:workers` itself).
25
25
  //
26
26
  // Why a factory that returns `{ input, handler }` instead of a ready `defineAction`:
27
27
  // `defineAction`/`ActionError` live in Astro's VIRTUAL `astro:actions` module,
28
- // which only resolves inside an Astro app — a library can't import it (this subpath
29
- // imports only real `astro/*` subpaths, e.g. `astro/zod`). So the adapter ships the
28
+ // which only resolves inside an Astro app—a library can't import it (this subpath
29
+ // imports only real `astro/*` subpaths, for example `astro/zod`). So the adapter ships the
30
30
  // ingredients and the SITE assembles `defineAction`, and it takes the `ActionError`
31
31
  // class by injection so the handler can still throw framework-correct 400/401/404.
32
32
  //
@@ -53,8 +53,8 @@ function statusToCode(status) {
53
53
  return "BAD_REQUEST";
54
54
  }
55
55
  /** Resolve an editor Action's deps, filling the default `locals.editor` reader.
56
- * `getEnv` has no default — Astro v6+ removed `locals.runtime.env`, so a safe one
57
- * can't exist — and a missing one is a wiring error thrown here, at
56
+ * `getEnv` has no default—Astro v6+ removed `locals.runtime.env`, so a safe one
57
+ * can't exist—and a missing one is a wiring error thrown here, at
58
58
  * action-construction time, rather than a per-request 500 on an `undefined` env. */
59
59
  function resolveDeps(deps) {
60
60
  if (typeof deps.getEnv !== "function") {
@@ -68,8 +68,8 @@ function resolveDeps(deps) {
68
68
  getEnv: deps.getEnv,
69
69
  };
70
70
  }
71
- /** Require the (middleware-resolved) editor session — a missing one is a 401 —
72
- * and return it. CSRF/same-origin is Astro's default for Action POSTs, so only
71
+ /** Require the (middleware-resolved) editor session—a missing one is a 401—and
72
+ * return it. CSRF/same-origin is Astro's default for Action POSTs, so only
73
73
  * auth is ported. */
74
74
  function requireEditor(resolved, ctx) {
75
75
  const editor = resolved.getEditor(ctx);
@@ -78,7 +78,7 @@ function requireEditor(resolved, ctx) {
78
78
  }
79
79
  return editor;
80
80
  }
81
- /** Throw the injected `ActionError` for an `apply*` failure — `never`, so a
81
+ /** Throw the injected `ActionError` for an `apply*` failure—`never`, so a
82
82
  * `if (!result.ok) throwActionError(...)` narrows the result to its ok branch. */
83
83
  function throwActionError(ActionError, status, error) {
84
84
  throw new ActionError({ code: statusToCode(status), message: error });
@@ -86,8 +86,8 @@ function throwActionError(ActionError, status, error) {
86
86
  /**
87
87
  * Build the `{ input, handler }` config for the editor `save` Action (the inline
88
88
  * field-save). The site drops the result into `defineAction` (see file header).
89
- * The `input` schema is validated by Astro *before* the handler runs — replacing
90
- * the raw route's manual `request.json()` + `standardValidate` — and the handler
89
+ * The `input` schema is validated by Astro *before* the handler runs—replacing
90
+ * the raw route's manual `request.json()` + `standardValidate`—and the handler
91
91
  * shares the raw route's store path via {@link applyFieldSave}, so a field is
92
92
  * validated once and written in exactly one place.
93
93
  */
@@ -138,8 +138,8 @@ export function louiseSettingsAction(config) {
138
138
  * versioned-page draft save). Mirrors {@link louiseSaveAction}, but the input
139
139
  * bundles the row `id` with the changed `data` (an Action call has no URL to carry
140
140
  * the id). The handler shares the raw `versionsRoute` store path via
141
- * {@link applySaveDraft} — the concurrent-surface merge base and the #70 KV
142
- * write-buffer — and returns that path's JSON body (a created `version`, or
141
+ * {@link applySaveDraft}—the concurrent-surface merge base and the #70 KV
142
+ * write-buffer—and returns that path's JSON body (a created `version`, or
143
143
  * `{ buffered: true }` when a write is coalesced into the buffer).
144
144
  */
145
145
  export function louiseSaveDraftAction(config) {
package/dist/catalog.d.ts CHANGED
@@ -2,7 +2,7 @@ import type { LiveLoader } from "astro/loaders";
2
2
  type AnyRecord = Record<string, any>;
3
3
  /**
4
4
  * What a site provides to build a catalog live loader. `Data` is the entry shape
5
- * (e.g. a display product); `Filter` is the collection query shape.
5
+ * (for example, a display product); `Filter` is the collection query shape.
6
6
  */
7
7
  export interface CatalogLoaderConfig<Data extends AnyRecord, Filter extends AnyRecord = Record<string, never>> {
8
8
  /** Loader name (Astro convention: the npm package or a stable id). Also the
@@ -10,7 +10,7 @@ export interface CatalogLoaderConfig<Data extends AnyRecord, Filter extends AnyR
10
10
  name: string;
11
11
  /**
12
12
  * Load the (cached) catalog for a collection query, already narrowed to what
13
- * the query asks for (the site owns its own filtering — category trees, etc.).
13
+ * the query asks for (the site owns its own filtering—category trees, etc.).
14
14
  * `fetchedAt` (epoch ms) becomes the `cacheHint.lastModified` so the hint
15
15
  * reflects the snapshot's age, not the render time.
16
16
  */
@@ -21,7 +21,7 @@ export interface CatalogLoaderConfig<Data extends AnyRecord, Filter extends AnyR
21
21
  /** Resolve a single item by its entry id (slug). `null` → not found (Astro
22
22
  * raises `LiveEntryNotFoundError`, which the page can turn into a redirect). */
23
23
  loadItem: (id: string) => Promise<Data | null>;
24
- /** The entry id (slug) for an item — keys the collection and resolves
24
+ /** The entry id (slug) for an item—keys the collection and resolves
25
25
  * `getLiveEntry("catalog", slug)`. */
26
26
  idOf: (item: Data) => string;
27
27
  /** `cacheHint` tag for tag-based purges. Default: `name`. */
package/dist/catalog.js CHANGED
@@ -1,14 +1,14 @@
1
1
  // Copyright (c) 2026 BowenLabs. Louise Toolkit is MIT licensed.
2
2
  //
3
- // `louise-toolkit/astro` — `defineCatalogLoader`: the shared plumbing for a commerce
3
+ // `louise-toolkit/astro`—`defineCatalogLoader`: the shared plumbing for a commerce
4
4
  // catalog served as an Astro Live Content Collection. Live collections fetch at
5
5
  // request time, so a price/stock edit shows on the next render with no rebuild.
6
6
  //
7
- // Every catalog loader repeats the same boilerplate — map items to keyed entries,
7
+ // Every catalog loader repeats the same boilerplate—map items to keyed entries,
8
8
  // stamp a `cacheHint` (a tag for future webhook-driven purges, plus the snapshot
9
9
  // age as `lastModified`), and wrap a read failure as a loader error instead of a
10
10
  // 500. That lives here ONCE. A site injects only what's domain-specific: how to
11
- // read its (cached) catalog, how to resolve one item, and each item's slug — so
11
+ // read its (cached) catalog, how to resolve one item, and each item's slug—so
12
12
  // a Fourthwall site and a Square site share one loader definition and only their
13
13
  // `lib/<provider>` reads differ.
14
14
  //
@@ -1,22 +1,22 @@
1
1
  import type { Loader } from "astro/loaders";
2
2
  import { z } from "astro/zod";
3
3
  import type { CollectionConfig } from "louise-toolkit/content";
4
- /** A published row as read from D1 — a document in the collection's field shape. */
4
+ /** A published row as read from D1—a document in the collection's field shape. */
5
5
  export type LouiseRow = Record<string, unknown>;
6
6
  /**
7
7
  * Build an Astro (Zod) schema from a collection's `defineCollection` fields.
8
- * Unknown/bookkeeping columns (`id`, `status`, timestamps) are dropped — the
8
+ * Unknown/bookkeeping columns (`id`, `status`, timestamps) are dropped—the
9
9
  * schema captures exactly the declared fields, so `getCollection` entry data
10
10
  * matches the editor's model.
11
11
  */
12
12
  export declare function collectionToAstroSchema(collection: CollectionConfig): z.ZodType;
13
13
  export interface LouiseLoaderConfig {
14
- /** The collection definition (from `defineCollection`) — drives the schema. */
14
+ /** The collection definition (from `defineCollection`)—drives the schema. */
15
15
  collection: CollectionConfig;
16
16
  /**
17
17
  * Read the published rows to expose, each a document in the collection's field
18
18
  * shape. The site owns D1 access (the loader runs at build time, off any
19
- * binding) — typically the D1 REST API, or a cached snapshot. Only published
19
+ * binding)—typically the D1 REST API, or a cached snapshot. Only published
20
20
  * rows should be returned; drafts never reach `getCollection`.
21
21
  */
22
22
  read: () => Promise<LouiseRow[]>;
@@ -46,7 +46,7 @@ export interface LouiseLoaderConfig {
46
46
  * };
47
47
  * ```
48
48
  *
49
- * then read with `getCollection("pages")` / `getEntry("pages", slug)` — typed
49
+ * then read with `getCollection("pages")` / `getEntry("pages", slug)`—typed
50
50
  * from the collection's own fields, no hand-written schema.
51
51
  */
52
52
  export declare function louiseLoader(config: LouiseLoaderConfig): Loader;
@@ -1,17 +1,17 @@
1
1
  // Copyright (c) 2026 BowenLabs. Louise Toolkit is MIT licensed.
2
2
  //
3
- // `louise-toolkit/astro` — `louiseLoader`: an Astro Content Layer loader that
3
+ // `louise-toolkit/astro`—`louiseLoader`: an Astro Content Layer loader that
4
4
  // exposes a Louise D1 collection through the native `getCollection()` /
5
5
  // `getEntry()` pipeline, with a Zod schema derived from the collection's own
6
6
  // `defineCollection` fields (so entry data is typed the same way the editor
7
7
  // models it).
8
8
  //
9
9
  // Content Layer loaders run at BUILD time (in Node, during `astro build`), where
10
- // Cloudflare bindings don't exist — so, like `defineCatalogLoader`, the *read*
10
+ // Cloudflare bindings don't exist—so, like `defineCatalogLoader`, the *read*
11
11
  // is injected: a site supplies `read()` (typically the D1 REST API at build, or
12
- // a snapshot) and this owns the rest — schema mapping, store population, content
12
+ // a snapshot) and this owns the rest—schema mapping, store population, content
13
13
  // digests for incremental builds, and fail-safe error handling. The result is a
14
- // build-time snapshot of published content; rebuild on publish (e.g. a webhook)
14
+ // build-time snapshot of published content; rebuild on publish (for example, a webhook)
15
15
  // to refresh it. For request-time freshness, read D1 directly in an SSR page (or
16
16
  // use the Live-collection `defineCatalogLoader`).
17
17
  //
@@ -40,7 +40,7 @@ function fieldToZod(field) {
40
40
  case "relationship":
41
41
  return z.number();
42
42
  case "checkbox":
43
- // D1 stores booleans as 0/1 — accept either and normalize to boolean.
43
+ // D1 stores booleans as 0/1—accept either and normalize to boolean.
44
44
  return z.union([z.boolean(), z.number().transform((n) => n !== 0)]);
45
45
  case "date":
46
46
  // D1 stores dates as integer epochs; a REST/JSON read may hand back a
@@ -48,10 +48,10 @@ function fieldToZod(field) {
48
48
  return z.coerce.date();
49
49
  case "group":
50
50
  // A group flattens to real columns in D1, but the Local API re-nests it on
51
- // read — so mirror the config's nested object shape.
51
+ // read—so mirror the config's nested object shape.
52
52
  return z.object(mapFields(field.fields));
53
53
  // JSON-backed columns (rich text, builder arrays, freeform json) pass through
54
- // untouched — their inner shape is the site's concern, not the loader's.
54
+ // untouched—their inner shape is the site's concern, not the loader's.
55
55
  case "richText":
56
56
  case "array":
57
57
  case "json":
@@ -64,7 +64,7 @@ function fieldToZod(field) {
64
64
  function mapFields(fields) {
65
65
  const shape = {};
66
66
  for (const [key, field] of Object.entries(fields)) {
67
- // A hasMany relationship lives in a join table — no column on this row.
67
+ // A hasMany relationship lives in a join table—no column on this row.
68
68
  if (field.type === "relationship" && field.hasMany)
69
69
  continue;
70
70
  const base = fieldToZod(field);
@@ -75,7 +75,7 @@ function mapFields(fields) {
75
75
  }
76
76
  /**
77
77
  * Build an Astro (Zod) schema from a collection's `defineCollection` fields.
78
- * Unknown/bookkeeping columns (`id`, `status`, timestamps) are dropped — the
78
+ * Unknown/bookkeeping columns (`id`, `status`, timestamps) are dropped—the
79
79
  * schema captures exactly the declared fields, so `getCollection` entry data
80
80
  * matches the editor's model.
81
81
  */
@@ -103,7 +103,7 @@ function errorMessage(error) {
103
103
  * };
104
104
  * ```
105
105
  *
106
- * then read with `getCollection("pages")` / `getEntry("pages", slug)` — typed
106
+ * then read with `getCollection("pages")` / `getEntry("pages", slug)`—typed
107
107
  * from the collection's own fields, no hand-written schema.
108
108
  */
109
109
  export function louiseLoader(config) {
@@ -1,7 +1,7 @@
1
1
  import { z } from "astro/zod";
2
2
  import type { FormConfig } from "louise-toolkit/forms";
3
3
  /**
4
- * Build a Zod schema from a `defineForm` definition's fields — the form is the
4
+ * Build a Zod schema from a `defineForm` definition's fields—the form is the
5
5
  * single source of truth for its Astro Action `input`, so the handler receives a
6
6
  * typed, validated value and the client infers the same shape. Field-level
7
7
  * `validation`/`schema` extras still run in the shared `validateSubmission` pass;
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) 2026 BowenLabs. Louise Toolkit is MIT licensed.
2
2
  //
3
- // `louise-toolkit/astro` — `formToAstroSchema`: the forms counterpart to
3
+ // `louise-toolkit/astro`—`formToAstroSchema`: the forms counterpart to
4
4
  // `collectionToAstroSchema`. It maps a `defineForm` definition to a Zod schema
5
5
  // so a form can drop straight into an Astro Action's `input`:
6
6
  //
@@ -36,14 +36,14 @@ function formFieldToZod(field) {
36
36
  return required ? z.coerce.date() : z.coerce.date().optional();
37
37
  case "checkbox": {
38
38
  // A checkbox may arrive as a real boolean (JSON action), or "on"/"true"/
39
- // "1"/1 (form-encoded) — normalize any of them to a boolean.
39
+ // "1"/1 (form-encoded)—normalize any of them to a boolean.
40
40
  const bool = z
41
41
  .union([z.boolean(), z.number(), z.string()])
42
42
  .transform((v) => v === true || v === 1 || v === "1" || v === "true" || v === "on");
43
43
  return required ? bool : bool.optional();
44
44
  }
45
45
  case "select": {
46
- // Options double as the allowlist — a value outside them is rejected.
46
+ // Options double as the allowlist—a value outside them is rejected.
47
47
  const select = field.options && field.options.length > 0
48
48
  ? z.enum([...field.options])
49
49
  : z.string();
@@ -57,7 +57,7 @@ function formFieldToZod(field) {
57
57
  }
58
58
  }
59
59
  /**
60
- * Build a Zod schema from a `defineForm` definition's fields — the form is the
60
+ * Build a Zod schema from a `defineForm` definition's fields—the form is the
61
61
  * single source of truth for its Astro Action `input`, so the handler receives a
62
62
  * typed, validated value and the client infers the same shape. Field-level
63
63
  * `validation`/`schema` extras still run in the shared `validateSubmission` pass;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) 2026 BowenLabs. Louise Toolkit is MIT licensed.
2
2
  //
3
- // `@louise-toolkit/astro` — optional Astro glue for Louise sites. Framework-specific
3
+ // `@louise-toolkit/astro`—optional Astro glue for Louise sites. Framework-specific
4
4
  // helpers that import Astro's types live here (never in the framework-agnostic
5
5
  // core), so `astro` is an OPTIONAL peer, pulled in only by sites that import
6
6
  // this subpath. First inhabitant: the shared middleware factory.
@@ -1,15 +1,15 @@
1
1
  import type { APIContext, MiddlewareHandler } from "astro";
2
2
  import { type RateLimitBackend, type RateRule } from "louise-toolkit/security";
3
3
  export interface LouiseMiddlewareRateLimit {
4
- /** The site's rate-limit rules — the public POST surfaces worth protecting. */
4
+ /** The site's rate-limit rules—the public POST surfaces worth protecting. */
5
5
  rules: RateRule[];
6
6
  /**
7
- * Rate-limit backend — a KV counter or Cloudflare's native Rate Limiting
7
+ * Rate-limit backend—a KV counter or Cloudflare's native Rate Limiting
8
8
  * binding, or a getter that yields one. A getter is resolved per request, so a
9
9
  * `cloudflare:workers` `env` binding is read in request scope rather than at
10
- * module-eval — the same reason editor Actions take `getEnv: () => env`. A
11
- * getter that yields a falsy backend (e.g. the KV namespace isn't provisioned
12
- * yet) simply skips rate-limiting — fail open, consistent with {@link rateLimit}.
10
+ * module-eval—the same reason editor Actions take `getEnv: () => env`. A
11
+ * getter that yields a falsy backend (for example, the KV namespace isn't provisioned
12
+ * yet) simply skips rate-limiting—fail open, consistent with {@link rateLimit}.
13
13
  */
14
14
  kv: RateLimitBackend | (() => RateLimitBackend | undefined);
15
15
  }
@@ -18,7 +18,7 @@ export interface LouiseMiddlewareApiGate {
18
18
  prefix?: string;
19
19
  /**
20
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
21
+ * toolkit's own public routes at their default mounts (forms and vitals—see
22
22
  * `isLouisePublicPath`).
23
23
  *
24
24
  * A path, not a mark on the route, because middleware runs before it knows
@@ -29,17 +29,17 @@ export interface LouiseMiddlewareApiGate {
29
29
  }
30
30
  export interface LouiseMiddlewareConfig<TEditor = unknown> {
31
31
  /**
32
- * Resolve the editor session for a request — the site wraps its own auth,
33
- * e.g. `resolveEditorSession(await getLouiseAuth(env, origin), request)`. A
32
+ * Resolve the editor session for a request—the site wraps its own auth,
33
+ * for example, `resolveEditorSession(await getLouiseAuth(env, origin), request)`. A
34
34
  * truthy result is written to `locals.editor` and unlocks edit mode; `null`
35
- * renders the public page. A thrown error (e.g. missing bindings under plain
35
+ * renders the public page. A thrown error (for example, missing bindings under plain
36
36
  * `astro preview`) degrades to public rendering.
37
37
  */
38
38
  resolveEditor: (request: Request) => TEditor | null | Promise<TEditor | null>;
39
39
  /** Rate-limit the public POST surfaces before any other work. Omit to skip. */
40
40
  rateLimit?: LouiseMiddlewareRateLimit;
41
41
  /**
42
- * `style-src` replacement for the response CSP header — the site's allow-list.
42
+ * `style-src` replacement for the response CSP header—the site's allow-list.
43
43
  * Astro's `security.csp` hashes inline island styles, which voids the
44
44
  * `'unsafe-inline'` the data-driven `style=""` carriers need; this rewrites
45
45
  * ONLY `style-src` (script hashes stay verbatim). No-op without a CSP header
@@ -50,13 +50,13 @@ export interface LouiseMiddlewareConfig<TEditor = unknown> {
50
50
  * response. Default `true`. */
51
51
  securityHeaders?: boolean;
52
52
  /**
53
- * Hosts to keep out of search indexes — sent as `X-Robots-Tag: noindex`.
54
- * E.g. `(host) => isNoindexHost(host, { prefixes: ["preview."] })`. Set here
53
+ * Hosts to keep out of search indexes—sent as `X-Robots-Tag: noindex`.
54
+ * For example, `(host) => isNoindexHost(host, { prefixes: ["preview."] })`. Set here
55
55
  * rather than in a page, because a streamed page's headers are already gone.
56
56
  */
57
57
  noindex?: (hostname: string) => boolean;
58
58
  /**
59
- * Extra per-request work after editor resolution, before `next()` — e.g.
59
+ * Extra per-request work after editor resolution, before `next()`—for example,
60
60
  * resolve a second session (a shop customer) onto `locals`. Runs inside the
61
61
  * same try/catch, so a throw degrades to public rendering.
62
62
  */
@@ -69,12 +69,12 @@ export interface LouiseMiddlewareConfig<TEditor = unknown> {
69
69
  * Deliberately separate from `extend`: sessions must be resolved before
70
70
  * anything can be authorized against them, and collapsing the two would make
71
71
  * that ordering a convention rather than a guarantee. It runs OUTSIDE the
72
- * `extend` try/catch, because a guard that throws must fail closed — a
72
+ * `extend` try/catch, because a guard that throws must fail closed—a
73
73
  * swallowed error there would serve the protected page.
74
74
  */
75
75
  guard?: (context: APIContext) => Response | undefined | Promise<Response | undefined>;
76
76
  /**
77
- * Rewrite the request internally before the page runs — return the path to
77
+ * Rewrite the request internally before the page runs—return the path to
78
78
  * render, or `undefined` to render the requested one. Runs **after**
79
79
  * {@link guard}, so policy is still expressed against the URL the visitor
80
80
  * actually asked for rather than an internal one.
@@ -83,7 +83,7 @@ export interface LouiseMiddlewareConfig<TEditor = unknown> {
83
83
  * `void` and `guard` returns only a `Response`. Astro permits exactly one
84
84
  * middleware file, and in a generated one there is nowhere else to put it.
85
85
  *
86
- * The motivating case is host dispatch — serving `*.example.com` from one
86
+ * The motivating case is host dispatch—serving `*.example.com` from one
87
87
  * Worker by mapping a subdomain onto an internal path prefix. The middleware
88
88
  * stays policy-free: what a host means, and whether an unknown one is a 404,
89
89
  * belong to the site.
@@ -95,7 +95,7 @@ export interface LouiseMiddlewareConfig<TEditor = unknown> {
95
95
  * },
96
96
  * ```
97
97
  *
98
- * The visitor's URL is unchanged — this is an internal rewrite, not a
98
+ * The visitor's URL is unchanged—this is an internal rewrite, not a
99
99
  * redirect, so `context.url` still reads as the public address and links
100
100
  * rendered from it stay correct.
101
101
  */
@@ -103,7 +103,7 @@ export interface LouiseMiddlewareConfig<TEditor = unknown> {
103
103
  /**
104
104
  * Deny-by-default gate for the editor API (ADR 0012), for routes mounted as
105
105
  * framework API routes (`runEditorRoute`) rather than `composeWorker` routes.
106
- * `true` — or an object to change the prefix or add public paths — and every
106
+ * `true`—or an object to change the prefix or add public paths—and every
107
107
  * request under `/api/louise` must resolve to an editor, with writes and
108
108
  * WebSocket upgrades origin-checked, before any route runs. Gated responses
109
109
  * get `Cache-Control: no-store` unless the route set its own. Omit to skip.
@@ -1,6 +1,6 @@
1
1
  // Copyright (c) 2026 BowenLabs. Louise Toolkit is MIT licensed.
2
2
  //
3
- // louise-toolkit/astro — the shared Louise Astro middleware, as a factory. Every
3
+ // louise-toolkit/astro—the shared Louise Astro middleware, as a factory. Every
4
4
  // Louise site's `middleware.ts` runs the same flow; only the auth wiring, rate
5
5
  // rules, and CSP allow-list vary. `createLouiseMiddleware` owns the flow and
6
6
  // takes those as config, so a site's middleware collapses to:
@@ -12,11 +12,11 @@
12
12
  // cspStyleSrc: "'self' 'unsafe-inline'",
13
13
  // });
14
14
  //
15
- // (The brand font is bundled + base64-inlined — no Google Fonts host to allow.
15
+ // (The brand font is bundled + base64-inlined—no Google Fonts host to allow.
16
16
  // The middleware auto-allows `data:` fonts in the response CSP, so a strict
17
17
  // `font-src` needs no manual change for the inlined @font-face.)
18
18
  //
19
- // This subpath is the ONE place Louise touches Astro's types — `astro` is an
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
22
  import { isLouisePublicPath, LOUISE_API_PREFIX, LOUISE_EDIT_COOKIE, louiseApiGate, underPrefix, } from "louise-toolkit/worker";
@@ -43,7 +43,7 @@ export function createLouiseMiddleware(config) {
43
43
  // Resolve the backend only for a matched surface, and per request: a
44
44
  // getter defers the `env` binding read to request scope (never
45
45
  // module-eval). A falsy backend (binding not yet provisioned) skips
46
- // limiting — fail open, like `rateLimit` itself.
46
+ // limiting—fail open, like `rateLimit` itself.
47
47
  const backend = typeof config.rateLimit.kv === "function" ? config.rateLimit.kv() : config.rateLimit.kv;
48
48
  if (backend) {
49
49
  const ip = context.request.headers.get("cf-connecting-ip") ?? "unknown";
@@ -65,13 +65,13 @@ export function createLouiseMiddleware(config) {
65
65
  if (editor) {
66
66
  locals.editor = editor;
67
67
  // Edit mode is sticky: ?louise enters (sets a cookie), ?louise=off
68
- // exits. The cookie alone never grants anything — the session above is
68
+ // exits. The cookie alone never grants anything—the session above is
69
69
  // always re-checked, so a stale cookie without a session renders public.
70
70
  const param = context.url.searchParams.get("louise");
71
71
  if (context.url.searchParams.has("louise") && param !== "off") {
72
72
  // `secure` only over https, so plain-http localhost dev still round-trips
73
- // the toggle. The cookie grants nothing on its own — the session above is
74
- // re-verified every request — so this is hygiene, not a control.
73
+ // the toggle. The cookie grants nothing on its own—the session above is
74
+ // re-verified every request—so this is hygiene, not a control.
75
75
  context.cookies.set(editCookie, "1", {
76
76
  path: "/",
77
77
  sameSite: "lax",
@@ -88,14 +88,14 @@ export function createLouiseMiddleware(config) {
88
88
  }
89
89
  }
90
90
  catch {
91
- // Missing bindings (e.g. plain `astro preview`, an unprovisioned
91
+ // Missing bindings (for example, plain `astro preview`, an unprovisioned
92
92
  // SESSION_SECRET) → public rendering. Auth degrading is fine; what it
93
93
  // must NOT do is cancel anything else.
94
94
  }
95
95
  // The API gate, before extend: it needs only the editor, and a refused
96
96
  // request shouldn't pay for the site's extra work. A `resolveEditor` that
97
97
  // threw left `locals.editor` null above, so where pages degrade to public
98
- // the API fails closed — refused, not served anonymously.
98
+ // the API fails closed—refused, not served anonymously.
99
99
  const pathname = context.url.pathname;
100
100
  const gatedApi = apiGate !== undefined &&
101
101
  underPrefix(pathname, apiPrefix) &&
@@ -110,7 +110,7 @@ export function createLouiseMiddleware(config) {
110
110
  return finish(context, denied);
111
111
  }
112
112
  // extend gets its OWN catch, deliberately separate from auth's. When these
113
- // shared one, `resolveEditor` throwing (a sentinel SESSION_SECRET — the
113
+ // shared one, `resolveEditor` throwing (a sentinel SESSION_SECRET—the
114
114
  // dormant-until-provisioned state every module is supposed to survive)
115
115
  // silently skipped extend too, and everything extend feeds died with it:
116
116
  // `locals.tenant` never set, so host dispatch quietly served the ordinary
@@ -149,7 +149,7 @@ export function createLouiseMiddleware(config) {
149
149
  const locals = context.locals;
150
150
  // content freshness: cached HTML would hide editor edits. Edit-mode pages are
151
151
  // per-editor and must be live (`no-store`); public HTML `no-cache` so edits
152
- // appear without a manual purge. Only HTML — hashed `/_astro/*` assets keep
152
+ // appear without a manual purge. Only HTML—hashed `/_astro/*` assets keep
153
153
  // their immutable caching (set via `_headers`).
154
154
  if ((response.headers.get("content-type") ?? "").includes("text/html")) {
155
155
  response.headers.set("Cache-Control", locals.editMode ? "no-store" : "no-cache");
@@ -157,7 +157,7 @@ export function createLouiseMiddleware(config) {
157
157
  if (config.cspStyleSrc)
158
158
  rewriteCspStyleSrc(response, config.cspStyleSrc);
159
159
  // Louise's bundled brand font is an inlined `data:` @font-face (loaded on
160
- // every edit surface), so guarantee the CSP permits data: fonts — no-op
160
+ // every edit surface), so guarantee the CSP permits data: fonts—no-op
161
161
  // without a CSP header or when already allowed. Saves consumers a font-src edit.
162
162
  allowCspDataFonts(response);
163
163
  const noindex = config.noindex?.(context.url.hostname) ?? false;
package/dist/resume.d.ts CHANGED
@@ -8,8 +8,8 @@ export interface ResumeReadSession {
8
8
  }
9
9
  /**
10
10
  * Open a D1 session anchored at the editor's persisted bookmark, for the
11
- * edit-mode resume read. On a database without read replication — or a
12
- * runtime without the Sessions API — `client` is the raw binding and nothing
11
+ * edit-mode resume read. On a database without read replication—or a
12
+ * runtime without the Sessions API—`client` is the raw binding and nothing
13
13
  * changes, so this is safe to wire before replication is on.
14
14
  *
15
15
  * Edit mode only: a view-mode render should stay session-free and cookie-free,
package/dist/resume.js CHANGED
@@ -7,8 +7,8 @@
7
7
  import { D1_BOOKMARK_COOKIE, D1_BOOKMARK_MAX_AGE, d1Bookmark, openD1Session, } from "louise-toolkit/db";
8
8
  /**
9
9
  * Open a D1 session anchored at the editor's persisted bookmark, for the
10
- * edit-mode resume read. On a database without read replication — or a
11
- * runtime without the Sessions API — `client` is the raw binding and nothing
10
+ * edit-mode resume read. On a database without read replication—or a
11
+ * runtime without the Sessions API—`client` is the raw binding and nothing
12
12
  * changes, so this is safe to wire before replication is on.
13
13
  *
14
14
  * Edit mode only: a view-mode render should stay session-free and cookie-free,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@louise-toolkit/astro",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
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,11 +35,12 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "louise-toolkit": "0.30.1"
38
+ "louise-toolkit": "0.31.0"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@cloudflare/workers-types": "^5.20260829.1",
42
42
  "@typescript/native-preview": "7.0.0-dev.20260707.2",
43
+ "@vitest/coverage-v8": "4.1.11",
43
44
  "astro": "^7.2.9",
44
45
  "drizzle-orm": "^0.45.2",
45
46
  "typescript": "^5.9.3",
@@ -59,7 +60,7 @@
59
60
  "scripts": {
60
61
  "build": "tsgo -p tsconfig.build.json",
61
62
  "check": "vp check",
62
- "test": "vp test",
63
+ "test": "vp test --coverage",
63
64
  "typecheck": "tsgo --noEmit"
64
65
  }
65
66
  }