astroidjs 0.12.1 → 0.14.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.
Files changed (136) hide show
  1. package/README.md +42 -42
  2. package/bin/astroid.mjs +35 -29
  3. package/dist/analytics/index.d.ts +3 -3
  4. package/dist/analytics/index.js +11 -11
  5. package/dist/astro/csp.d.ts +5 -5
  6. package/dist/astro/csp.js +6 -6
  7. package/dist/astro/index.js +1 -1
  8. package/dist/auth/index.d.ts +2 -2
  9. package/dist/auth/index.js +5 -5
  10. package/dist/commerce/adapters.d.ts +6 -6
  11. package/dist/commerce/adapters.js +9 -9
  12. package/dist/commerce/checkout-scaffold.d.ts +5 -5
  13. package/dist/commerce/checkout-scaffold.js +27 -27
  14. package/dist/commerce/checkout.d.ts +12 -12
  15. package/dist/commerce/checkout.js +7 -7
  16. package/dist/commerce/loader.d.ts +2 -2
  17. package/dist/commerce/loader.js +3 -3
  18. package/dist/commerce/mirror.d.ts +4 -4
  19. package/dist/commerce/mirror.js +12 -12
  20. package/dist/commerce/roles.d.ts +10 -10
  21. package/dist/commerce/roles.js +13 -13
  22. package/dist/commerce/secrets.d.ts +9 -9
  23. package/dist/commerce/secrets.js +9 -9
  24. package/dist/commerce/sync.d.ts +7 -7
  25. package/dist/commerce/sync.js +5 -5
  26. package/dist/components/sections.d.ts +9 -9
  27. package/dist/components/sections.js +12 -12
  28. package/dist/config.d.ts +89 -62
  29. package/dist/config.js +18 -18
  30. package/dist/email/inquiry.d.ts +2 -2
  31. package/dist/email/inquiry.js +1 -1
  32. package/dist/email/send.d.ts +4 -4
  33. package/dist/email/send.js +7 -7
  34. package/dist/email/templates.js +3 -3
  35. package/dist/email/theme.d.ts +1 -1
  36. package/dist/email/theme.js +4 -4
  37. package/dist/errors.d.ts +1 -1
  38. package/dist/errors.js +1 -1
  39. package/dist/index.js +1 -1
  40. package/dist/map/pmtiles.d.ts +5 -5
  41. package/dist/map/pmtiles.js +5 -5
  42. package/dist/map/scaffold.d.ts +2 -2
  43. package/dist/map/scaffold.js +8 -8
  44. package/dist/map/style.d.ts +4 -4
  45. package/dist/map/style.js +1 -1
  46. package/dist/portal/config.d.ts +3 -3
  47. package/dist/portal/config.js +11 -10
  48. package/dist/portal/guard.d.ts +4 -4
  49. package/dist/portal/guard.js +4 -4
  50. package/dist/portal/nav.js +2 -2
  51. package/dist/portal/scaffold.d.ts +4 -4
  52. package/dist/portal/scaffold.js +13 -13
  53. package/dist/portal/session.d.ts +11 -5
  54. package/dist/portal/session.js +10 -5
  55. package/dist/portfolio/scaffold.d.ts +1 -1
  56. package/dist/portfolio/scaffold.js +8 -8
  57. package/dist/project/actions.d.ts +1 -1
  58. package/dist/project/actions.js +18 -12
  59. package/dist/project/generate.d.ts +4 -4
  60. package/dist/project/generate.js +26 -26
  61. package/dist/project/index.d.ts +1 -0
  62. package/dist/project/index.js +2 -1
  63. package/dist/project/scaffold.d.ts +2 -2
  64. package/dist/project/scaffold.js +69 -13
  65. package/dist/project/seed.d.ts +22 -0
  66. package/dist/project/seed.js +214 -0
  67. package/dist/pwa/generate.d.ts +11 -11
  68. package/dist/pwa/generate.js +19 -19
  69. package/dist/queues/consumer.d.ts +3 -3
  70. package/dist/queues/consumer.js +2 -2
  71. package/dist/queues/messages.d.ts +4 -4
  72. package/dist/queues/messages.js +2 -2
  73. package/dist/queues/scaffold.d.ts +4 -4
  74. package/dist/queues/scaffold.js +17 -17
  75. package/dist/queues/webhook.d.ts +5 -5
  76. package/dist/queues/webhook.js +3 -3
  77. package/dist/realtime/scaffold.d.ts +4 -4
  78. package/dist/realtime/scaffold.js +12 -12
  79. package/dist/schema/collections.d.ts +42 -11
  80. package/dist/schema/collections.js +43 -42
  81. package/dist/schema/framework.d.ts +1 -1
  82. package/dist/schema/framework.js +2 -2
  83. package/dist/schema/generate.js +6 -6
  84. package/dist/schema/index.js +1 -1
  85. package/dist/secrets.d.ts +6 -6
  86. package/dist/secrets.js +6 -6
  87. package/dist/security/csp-origins.d.ts +1 -1
  88. package/dist/security/csp-origins.js +3 -3
  89. package/dist/security/rate-rules.d.ts +2 -2
  90. package/dist/security/rate-rules.js +8 -8
  91. package/dist/seo/resolve.d.ts +5 -5
  92. package/dist/seo/resolve.js +2 -2
  93. package/dist/seo/routes.d.ts +5 -5
  94. package/dist/seo/routes.js +3 -3
  95. package/dist/seo/structured-data.d.ts +6 -6
  96. package/dist/seo/structured-data.js +7 -7
  97. package/dist/status.d.ts +5 -5
  98. package/dist/status.js +7 -7
  99. package/dist/tenancy/index.d.ts +3 -3
  100. package/dist/tenancy/index.js +11 -11
  101. package/dist/worker/generate.d.ts +2 -2
  102. package/dist/worker/generate.js +91 -57
  103. package/dist/worker/index.js +1 -1
  104. package/dist/worker/routes.js +11 -11
  105. package/dist/workflow/advance.d.ts +3 -3
  106. package/dist/workflow/advance.js +6 -6
  107. package/dist/workflow/config.d.ts +4 -4
  108. package/dist/workflow/config.js +4 -4
  109. package/dist/workflow/generate.d.ts +2 -2
  110. package/dist/workflow/generate.js +11 -11
  111. package/package.json +3 -4
  112. package/src/components/Collection.tsx +5 -5
  113. package/src/components/Editable.astro +9 -9
  114. package/src/components/JustifiedGallery.astro +8 -8
  115. package/src/components/MediaSlot.astro +12 -12
  116. package/src/components/PortalShell.astro +4 -4
  117. package/src/components/RegisterSW.astro +3 -3
  118. package/src/components/Section.astro +8 -8
  119. package/src/components/Sections.astro +6 -6
  120. package/src/components/Seo.astro +3 -3
  121. package/src/components/StageBar.astro +3 -3
  122. package/src/components/StructuredData.astro +2 -2
  123. package/src/components/justify.ts +9 -9
  124. package/src/components/media-meta.ts +10 -10
  125. package/src/components/sections/AboutIntro.astro +1 -1
  126. package/src/components/sections/Contact.astro +1 -1
  127. package/src/components/sections/Cta.astro +1 -1
  128. package/src/components/sections/Faq.astro +1 -1
  129. package/src/components/sections/FeatureGrid.astro +2 -2
  130. package/src/components/sections/Hero.astro +1 -1
  131. package/src/components/sections/PricingTiers.astro +1 -1
  132. package/src/components/sections/ProductGrid.astro +1 -1
  133. package/src/components/sections/SplitImage.astro +1 -1
  134. package/src/components/sections/Steps.astro +1 -1
  135. package/src/components/sections/Testimonial.astro +1 -1
  136. package/src/components/sections.ts +17 -17
@@ -5,8 +5,8 @@
5
5
  //
6
6
  // This file used to define a parallel universe: a `SectionProps` union
7
7
  // discriminated on `kind`, with `colorway`/`align` as component props. Louise's
8
- // actual model — the one the on-canvas editor and the write-time validator both
9
- // read — is different in every particular, and it is the one that wins:
8
+ // actual model—the one the on-canvas editor and the write-time validator both
9
+ // read—is different in every particular, and it is the one that wins:
10
10
  //
11
11
  // • a section is a stored `SectionItem`: `{ _type, blocks?, _layout?,
12
12
  // _settings?, ...fields }`. The discriminant is `_type`, not `kind`.
@@ -16,7 +16,7 @@
16
16
  // or validated-but-uneditable.
17
17
  // • presentation choices are `_settings` / `_layout` **tokens**. Louise stores
18
18
  // the token; the site maps it to CSS. That's why COLORWAY_CLASS below stays
19
- // — it is exactly the site-owned half of that contract — while `colorway`
19
+ //: it is exactly the site-owned half of that contract—while `colorway`
20
20
  // stops being a prop and becomes a stored setting.
21
21
  //
22
22
  // ADR 0005 §2 names this file's job outright: "<Section> reads `_layout` /
@@ -26,7 +26,7 @@
26
26
  //
27
27
  // Self-contained on purpose: this module ships as SOURCE (the `.astro` files
28
28
  // beside it import it directly), so it must not reach back into astroid's built
29
- // `src/*` — only siblings and external packages. The `louise-toolkit/content`
29
+ // `src/*`—only siblings and external packages. The `louise-toolkit/content`
30
30
  // import is TYPE-ONLY, so it erases at build and never drags the validator (or
31
31
  // drizzle, which that entry pulls in) into a page bundle.
32
32
  /**
@@ -73,7 +73,7 @@ const tokenOptions = (map) => Object.keys(map).map((value) => ({ value, label: l
73
73
  *
74
74
  * These are closed token sets, declared as `select` (#272) so the inspector
75
75
  * renders a picker and an unknown token is rejected on write. They used to be
76
- * `text` with the valid values stuffed into `placeholder` — which meant a typo
76
+ * `text` with the valid values stuffed into `placeholder`—which meant a typo
77
77
  * wasn't a validation error at all, just a silent fallback to the default
78
78
  * inside `colorwayClass` at render time.
79
79
  */
@@ -83,7 +83,7 @@ export const SECTION_SETTINGS = {
83
83
  label: "Colorway",
84
84
  inline: false,
85
85
  options: tokenOptions(COLORWAY_CLASS),
86
- // An opaque hint — the schema layer doesn't know what a swatch looks like;
86
+ // An opaque hint—the schema layer doesn't know what a swatch looks like;
87
87
  // a renderer that doesn't support it just shows a normal picker.
88
88
  display: "swatch",
89
89
  },
@@ -124,7 +124,7 @@ export function itemField(row, key) {
124
124
  *
125
125
  * The precedence is the whole reason `<Sections>` does its media lookup: a
126
126
  * per-usage `alt` on the section wins, because the same photo means something
127
- * different in a hero than in a thumbnail strip — but when there isn't one, the
127
+ * different in a hero than in a thumbnail strip—but when there isn't one, the
128
128
  * alt an editor typed once in the media library is used. That's what makes
129
129
  * fixing alt text a single edit that propagates everywhere the asset appears,
130
130
  * instead of a hunt through every page that embeds it.
@@ -139,7 +139,7 @@ export function mediaAlt(mediaMeta, src, override) {
139
139
  return (src ? mediaMeta?.[src]?.alt : undefined) ?? "";
140
140
  }
141
141
  /** Caption for an image, same precedence as {@link mediaAlt}. Undefined when
142
- * there is none — a missing caption renders nothing, unlike a missing alt. */
142
+ * there is none—a missing caption renders nothing, unlike a missing alt. */
143
143
  export function mediaCaption(mediaMeta, src, override) {
144
144
  if (override !== undefined && override !== "")
145
145
  return override;
@@ -154,7 +154,7 @@ export function mediaCaption(mediaMeta, src, override) {
154
154
  * `satisfies` rather than a `: SectionCatalog` annotation, and it matters:
155
155
  * `SectionCatalog` is `Record<string, SectionDef>`, so annotating would widen
156
156
  * `keyof typeof` to `string` and throw away the literal keys. Those keys are
157
- * the project's whole section vocabulary — `SectionKind` is derived from them
157
+ * the project's whole section vocabulary—`SectionKind` is derived from them
158
158
  * (config.ts), `isRenderableSection` narrows to them, and `<Section>` indexes
159
159
  * its component map with them. Annotate this and all three silently degrade to
160
160
  * "any string", which is how the dispatcher lost its type safety once already.
@@ -167,7 +167,7 @@ export const astroidSectionCatalog = {
167
167
  heading: { type: "text", label: "Heading", validation: (r) => r.required().max(120) },
168
168
  subheading: { type: "textarea", label: "Subheading" },
169
169
  // A link URL is something you can't point at on the page, so it is not
170
- // inline — it belongs in the inspector, which is what `inline: false` says.
170
+ // inline—it belongs in the inspector, which is what `inline: false` says.
171
171
  ctaLabel: { type: "text", label: "Button label" },
172
172
  ctaHref: { type: "text", label: "Button link", inline: false },
173
173
  },
@@ -312,7 +312,7 @@ export const astroidSectionCatalog = {
312
312
  name: { type: "text", label: "Name", validation: (r) => r.required() },
313
313
  price: { type: "text", label: "Price" },
314
314
  period: { type: "text", label: "Period (e.g. /mo)" },
315
- // A list of strings isn't expressible — array items are objects — so
315
+ // A list of strings isn't expressible—array items are objects—so
316
316
  // each feature is a one-field row. That also leaves room to add an
317
317
  // `included` flag later without a data migration.
318
318
  features: {
@@ -368,7 +368,7 @@ export const astroidSectionCatalog = {
368
368
  heading: { type: "text", label: "Heading" },
369
369
  // Deliberately hand-authored rows rather than a live catalog read. A
370
370
  // section is stored content, and the commerce mirror is a separate
371
- // concern with its own loader — a site that wants the live catalog renders
371
+ // concern with its own loader—a site that wants the live catalog renders
372
372
  // `readCatalog` in its own page, not through the page-builder.
373
373
  items: {
374
374
  type: "array",
package/dist/config.d.ts CHANGED
@@ -5,7 +5,7 @@ import type { astroidSectionCatalog } from "./components/sections.js";
5
5
  import type { PortalRoute } from "./portal/guard.js";
6
6
  import type { PwaConfig } from "./pwa/generate.js";
7
7
  /**
8
- * The starting shape the front-end takes. Not a fork — each archetype is a preset
8
+ * The starting shape the front-end takes. Not a fork—each archetype is a preset
9
9
  * of defaults (which sections/modules are on, nav shape) that the site then tunes.
10
10
  * `marketing` = the lean brochure floor (louise-web, no commerce); `storefront` =
11
11
  * DTC shop (coracle); `wholesale` = B2B/private-label (ghostfire); `portfolio` =
@@ -13,7 +13,7 @@ import type { PwaConfig } from "./pwa/generate.js";
13
13
  */
14
14
  export type Archetype = "marketing" | "storefront" | "wholesale" | "portfolio";
15
15
  /**
16
- * The section vocabulary — the editable home page is an ordered list of these,
16
+ * The section vocabulary—the editable home page is an ordered list of these,
17
17
  * top to bottom.
18
18
  *
19
19
  * DERIVED from the section catalog, not hand-written (#277). It used to be its
@@ -30,42 +30,42 @@ export type SectionKind = keyof typeof astroidSectionCatalog;
30
30
  /**
31
31
  * Each archetype's default home-page sections.
32
32
  *
33
- * Lives here, in TypeScript, rather than in `create-astroid`'s plain JS — the
33
+ * Lives here, in TypeScript, rather than in `create-astroid`'s plain JS—the
34
34
  * other half of #277. As a JS object literal it could name a section that
35
35
  * didn't exist and nothing would say so; typed against {@link SectionKind}
36
36
  * (itself derived from the catalog) a stale name is a compile error, and CI
37
37
  * type-checks this package.
38
38
  *
39
- * The four kinds this used to name — `marquee`, `featured`, `story`, `visit` —
40
- * had no catalog entry or component and could never render. Each is replaced by
39
+ * The four kinds this used to name—`marquee`, `featured`, `story`, `visit`—had
40
+ * no catalog entry or component and could never render. Each is replaced by
41
41
  * the real section that does its job: a marquee is a `banner`, curated picks
42
42
  * are a `productGrid`, a brand-origin block is `aboutIntro`, and "visit" is
43
43
  * exactly `locationHours`.
44
44
  */
45
45
  export declare const ASTROID_ARCHETYPE_SECTIONS: Record<Archetype, SectionKind[]>;
46
46
  /**
47
- * Optional capabilities the site switches on. Pluggable, not core — a portfolio
47
+ * Optional capabilities the site switches on. Pluggable, not core—a portfolio
48
48
  * site runs none of the commerce ones.
49
49
  *
50
50
  * **Every value here is read by something.** The union used to also name
51
51
  * `orderTracking`, `subscriptions`, `giftCards`, and `privateLabel`, none of
52
52
  * which had a single consumer anywhere in the package: setting one type-checked,
53
- * passed validation, and did nothing at all — no scaffold, no CSP origin, no
53
+ * passed validation, and did nothing at all—no scaffold, no CSP origin, no
54
54
  * rate rule, no table. A config surface that accepts a setting it ignores is
55
55
  * worse than a smaller one, because the only way to discover the truth is to
56
56
  * deploy and notice the absence.
57
57
  *
58
58
  * They are removed rather than left as TODOs. `orderTracking` in particular has
59
- * a real implementation waiting — `src/workflow/` is the ghostfire order tracker,
60
- * generalized — but it is reached through `defineWorkflow`, not this flag, and
59
+ * a real implementation waiting—`src/workflow/` is the ghostfire order tracker,
60
+ * generalized—but it is reached through `defineWorkflow`, not this flag, and
61
61
  * pretending otherwise is what made the flag misleading. Re-add each one in the
62
62
  * change that wires it.
63
63
  */
64
64
  export type ModuleKind = "map" | "pwa" | "realtime" | "wholesaleInquiry";
65
- /** Commerce backend — mirrors Louise's provider set (louise-toolkit/commerce). */
65
+ /** Commerce backend—mirrors Louise's provider set (louise-toolkit/commerce). */
66
66
  export type CommerceProvider = "stripe" | "square" | "fourthwall";
67
67
  export interface Theme {
68
- /** Display name — the brand, used in nav, `<title>`, OG cards. */
68
+ /** Display name—the brand, used in nav, `<title>`, OG cards. */
69
69
  name: string;
70
70
  /** Path to the primary logo (media-library asset or a `/brand/*` file). */
71
71
  logo?: string;
@@ -85,21 +85,21 @@ export interface Theme {
85
85
  export interface Portal {
86
86
  enabled: boolean;
87
87
  /**
88
- * @deprecated NOT IMPLEMENTED — `defineAstroid` throws if this is set.
88
+ * @deprecated NOT IMPLEMENTED—`defineAstroid` throws if this is set.
89
89
  *
90
90
  * It was meant to require a session for the whole site (a pre-launch client
91
91
  * gallery), not just the account area, but nothing ever read it: the guard
92
92
  * table is built from {@link Portal.routes} and `portalGuard` allows any
93
93
  * unmatched path. Until it's wired, gate the site by naming the prefixes in
94
- * `routes` — that is the mechanism this would have been sugar for.
94
+ * `routes`—that is the mechanism this would have been sugar for.
95
95
  */
96
96
  gated?: boolean;
97
- /** Modules exposed inside the account area (e.g. `wholesaleInquiry`, which
97
+ /** Modules exposed inside the account area (for example, `wholesaleInquiry`, which
98
98
  * adds the inquiries table even on an archetype that wouldn't have one). */
99
99
  features?: ModuleKind[];
100
100
  /**
101
101
  * Roles a portal account can hold, first being the default for a new account.
102
- * Default `["customer"]`. These are the portal's OWN roles — entirely separate
102
+ * Default `["customer"]`. These are the portal's OWN roles—entirely separate
103
103
  * from the editor's `admin`, because the two auth instances don't share a
104
104
  * user table.
105
105
  */
@@ -111,7 +111,7 @@ export interface Portal {
111
111
  */
112
112
  routes?: PortalRoute[];
113
113
  /**
114
- * Where a portal user lands, per role — used to bounce someone who reached an
114
+ * Where a portal user lands, per role—used to bounce someone who reached an
115
115
  * area they don't belong in. Default `/portal` for everyone.
116
116
  */
117
117
  home?: Record<string, string>;
@@ -121,14 +121,14 @@ export interface Portal {
121
121
  */
122
122
  signUp?: boolean;
123
123
  /**
124
- * Where the portal's Better Auth instance mounts — its own handler, separate
124
+ * Where the portal's Better Auth instance mounts—its own handler, separate
125
125
  * from the editor's `/api/auth`. Default `/api/portal-auth`. Override when a
126
- * site already ships a second instance at a different path (e.g. a shop
126
+ * site already ships a second instance at a different path (for example, a shop
127
127
  * account at `/api/shop-auth`) whose live cookies must not change.
128
128
  */
129
129
  basePath?: string;
130
130
  /**
131
- * Cookie prefix for the portal instance — MUST differ from the editor's
131
+ * Cookie prefix for the portal instance—MUST differ from the editor's
132
132
  * (Better Auth's default), or signing into one instance signs you out of the
133
133
  * other. Default `"portal"`. `defineAstroid` rejects a colliding value.
134
134
  */
@@ -144,14 +144,14 @@ export interface Portal {
144
144
  export interface CommerceConfig {
145
145
  /**
146
146
  * Shorthand for a single-provider site. Assigns the provider to a role it can
147
- * actually serve — `square`/`fourthwall` become the storefront, `stripe`
147
+ * actually serve—`square`/`fourthwall` become the storefront, `stripe`
148
148
  * becomes invoicing (its client has no catalog API).
149
149
  */
150
150
  provider?: CommerceProvider;
151
151
  /** Catalog, cart, checkout. Needs a provider with a catalog API. */
152
152
  storefront?: CommerceProvider;
153
153
  /**
154
- * Invoices for work that isn't a catalog item — commissions, originals.
154
+ * Invoices for work that isn't a catalog item—commissions, originals.
155
155
  * Independent of `storefront`: themidwestartist.com runs Stripe here and
156
156
  * Fourthwall as the storefront, because neither can do the other's job.
157
157
  */
@@ -163,14 +163,14 @@ export interface CommerceConfig {
163
163
  * Separate from `storefront` because they are genuinely different jobs and a
164
164
  * site commonly runs both. themidwestartist.com sells print-on-demand merch
165
165
  * through Fourthwall (`storefront`) while originals and self-stocked prints
166
- * live in Square (`pos`) across several shops and galleries — one catalog per
166
+ * live in Square (`pos`) across several shops and galleries—one catalog per
167
167
  * rail, neither able to do the other's job.
168
168
  *
169
169
  * What `pos` turns on that `storefront` does not: locations, per-location
170
170
  * pricing, and per-location inventory.
171
171
  */
172
172
  pos?: CommerceProvider;
173
- /** The catalog mirror's shape — its mode, table name, and owned columns. */
173
+ /** The catalog mirror's shape—its mode, table name, and owned columns. */
174
174
  catalog?: CatalogMirrorConfig;
175
175
  /** Square-specific options. Only meaningful when Square fills some role. */
176
176
  square?: SquareCommerceConfig;
@@ -182,7 +182,7 @@ export interface SquareCommerceConfig {
182
182
  * `"single"` (the default) is the ordinary case: one location, its id supplied
183
183
  * once as `SQUARE_LOCATION_ID`, and every order placed against it.
184
184
  *
185
- * `"multi"` is the multi-merchant model — each merchant is a Location, and the
185
+ * `"multi"` is the multi-merchant model—each merchant is a Location, and the
186
186
  * id comes from the *request* (which merchant's storefront is this?) rather
187
187
  * than from the environment. Setting it stops Astroid requiring
188
188
  * `SQUARE_LOCATION_ID`: a single ambient location id is not merely unnecessary
@@ -199,8 +199,8 @@ export interface QueuesConfig {
199
199
  */
200
200
  enabled?: boolean;
201
201
  /**
202
- * Cron for the safety-net re-sync, or `false` for none. Webhooks get missed —
203
- * a provider outage, a deploy mid-delivery, a DLQ'd message — and without a
202
+ * Cron for the safety-net re-sync, or `false` for none. Webhooks get missed—a
203
+ * provider outage, a deploy mid-delivery, a DLQ'd message—and without a
204
204
  * periodic re-sync the site serves stale data until someone notices. Default
205
205
  * hourly.
206
206
  */
@@ -218,13 +218,13 @@ export interface QueuesConfig {
218
218
  *
219
219
  * Declaring it here rather than by hand is what keeps `wrangler.jsonc` and the
220
220
  * generated `scheduled` dispatch in agreement. Adding a cron to `triggers.crons`
221
- * alone produces a trigger Cloudflare fires and the dispatch never matches —
222
- * unreachable code that costs an invocation and does nothing, with no error
221
+ * alone produces a trigger Cloudflare fires and the dispatch never matches—unreachable
222
+ * code that costs an invocation and does nothing, with no error
223
223
  * anywhere. Adding it in the dashboard instead drifts from the config that is
224
224
  * supposed to describe the deploy.
225
225
  */
226
226
  /**
227
- * Serve `*.example.com` from this one Worker — scoped views of **this brand's**
227
+ * Serve `*.example.com` from this one Worker—scoped views of **this brand's**
228
228
  * data, narrowed by host. A per-merchant storefront, a per-client gallery.
229
229
  *
230
230
  * Not multi-brand: see the note at the top of this file. Same theme, same
@@ -232,26 +232,26 @@ export interface QueuesConfig {
232
232
  *
233
233
  * Astroid provides only the plumbing that cannot live in a site: the wildcard
234
234
  * Worker route (which `hosts` cannot express) and the one middleware file Astro
235
- * permits. **Everything that decides anything stays yours** — what a label maps
235
+ * permits. **Everything that decides anything stays yours**—what a label maps
236
236
  * to, whether the lookup is cached, and what an unknown host should do. Those
237
237
  * live in the scaffolded `src/tenancy.ts`, which is yours to edit.
238
238
  */
239
239
  export interface TenancyConfig {
240
240
  /**
241
- * The wildcard host, e.g. `"*.example.com"`. Emitted as a **zone route**
242
- * (`{ pattern, zone_name }`), never `custom_domain: true` — a wildcard cannot
241
+ * The wildcard host, for example, `"*.example.com"`. Emitted as a **zone route**
242
+ * (`{ pattern, zone_name }`), never `custom_domain: true`—a wildcard cannot
243
243
  * be a custom domain, which is exactly why `hosts` cannot express this.
244
244
  */
245
245
  hostPattern: string;
246
246
  /**
247
247
  * The Cloudflare zone the pattern belongs to. Defaults to `hostPattern` minus
248
248
  * its leading `*.`, which is right whenever the wildcard sits directly under
249
- * the zone apex. Set it explicitly for a deeper pattern — `*.shop.example.com`
249
+ * the zone apex. Set it explicitly for a deeper pattern—`*.shop.example.com`
250
250
  * is served by the `example.com` zone, not a `shop.example.com` one.
251
251
  */
252
252
  zone?: string;
253
253
  /**
254
- * Labels that are **not** tenants — `www`, `admin`, `studio`, `api`. These
254
+ * Labels that are **not** tenants—`www`, `admin`, `studio`, `api`. These
255
255
  * skip the tenant lookup entirely and render the ordinary site.
256
256
  *
257
257
  * Declared here rather than in the seam because the generated middleware needs
@@ -264,27 +264,27 @@ export interface TenancyConfig {
264
264
  * Internal path prefix a tenant request is rewritten to. Default `"/t"`, so
265
265
  * `acme.example.com/prints` renders `/t/acme/prints`.
266
266
  *
267
- * The visitor's URL never changes — this is an internal rewrite, so links
267
+ * The visitor's URL never changes—this is an internal rewrite, so links
268
268
  * built from `Astro.url` stay public and correct.
269
269
  */
270
270
  rewritePrefix?: string;
271
271
  /**
272
272
  * First-party apps on their own labels, mapped to the internal path prefix
273
- * each serves from — `{ studio: "/studio" }` serves
273
+ * each serves from—`{ studio: "/studio" }` serves
274
274
  * `studio.example.com/<path>` from `src/pages/studio/<path>`.
275
275
  *
276
276
  * This is the missing half of {@link PwaConfig.emitDir}'s subdomain story:
277
- * an app label is not a tenant (there is no lookup — the studio exists
277
+ * an app label is not a tenant (there is no lookup—the studio exists
278
278
  * whether or not any tenant does) and not merely reserved (a reserved label
279
279
  * renders the ordinary site, which turns the admin host into a second copy
280
280
  * of the marketing homepage). It is a static rewrite, decided at config time.
281
281
  *
282
282
  * An app label is implicitly reserved: `tenantLabel` never offers it to
283
- * `resolveTenant`, and listing it in `reserved` too is refused — one list
283
+ * `resolveTenant`, and listing it in `reserved` too is refused—one list
284
284
  * per fact, or the two drift.
285
285
  *
286
286
  * Same internal-rewrite semantics as tenants: the visitor's URL never
287
- * changes, and the path form stays reachable on the apex — which is what
287
+ * changes, and the path form stays reachable on the apex—which is what
288
288
  * makes local dev work, since `wrangler dev` cannot serve subdomains.
289
289
  */
290
290
  apps?: Record<string, string>;
@@ -292,7 +292,7 @@ export interface TenancyConfig {
292
292
  * What a syntactically-valid tenant host whose label resolves to NOTHING
293
293
  * (`resolveTenant` returned `null`) should get.
294
294
  *
295
- * `"fallthrough"` (the default) renders the ordinary site — which means a
295
+ * `"fallthrough"` (the default) renders the ordinary site—which means a
296
296
  * stranger who points a CNAME at your zone gets your homepage. `"404"` emits
297
297
  * a guard that refuses the request instead, which is the right answer the
298
298
  * moment tenant hosts are commercial surfaces: an unknown storefront must be
@@ -302,7 +302,7 @@ export interface TenancyConfig {
302
302
  * Either way the decision stays visible in config rather than buried in the
303
303
  * seam: `resolveTenant` decides *what exists*; this decides what not-existing
304
304
  * means. Reserved labels, app labels, the apex, and off-pattern hosts are
305
- * never affected — they aren't tenant candidates at all.
305
+ * never affected—they aren't tenant candidates at all.
306
306
  */
307
307
  unknown?: "fallthrough" | "404";
308
308
  /**
@@ -311,13 +311,13 @@ export interface TenancyConfig {
311
311
  * The rewrite exists to choose which PAGE renders for a host. An API route
312
312
  * is not a page: its address is absolute, chosen by the client that calls
313
313
  * it, and it reads the host from `locals.tenant` rather than from its own
314
- * path. Rewriting it moves it somewhere no route matches — and on an app
314
+ * path. Rewriting it moves it somewhere no route matches—and on an app
315
315
  * host with a catch-all page, somewhere much worse than a 404: the page
316
316
  * catch-all answers, so `fetch("/api/…")` gets HTML (or a redirect to a
317
317
  * sign-in) instead of JSON, and every data load on that host silently fails
318
318
  * while the same code works on the apex.
319
319
  *
320
- * That is not hypothetical — it is why this default exists (found on
320
+ * That is not hypothetical—it is why this default exists (found on
321
321
  * themidwestartist.com's studio, where the whole admin app loaded and then
322
322
  * fetched nothing).
323
323
  *
@@ -328,7 +328,7 @@ export interface TenancyConfig {
328
328
  rewriteExclude?: string[];
329
329
  }
330
330
  export interface AstroidCron {
331
- /** Standard 5-field cron, UTC — e.g. `"*&#47;15 * * * *"`. */
331
+ /** Standard 5-field cron, UTC—for example, `"*&#47;15 * * * *"`. */
332
332
  expression: string;
333
333
  /**
334
334
  * The queue message this trigger sends. **Enqueued, never run inline**, so the
@@ -350,14 +350,14 @@ export interface SeoConfig {
350
350
  /**
351
351
  * schema.org `@type` for the business node in the JSON-LD graph. Defaults to
352
352
  * the archetype's broad type (see `ARCHETYPE_BUSINESS_TYPE`); set a more
353
- * specific subtype whenever you know one — `"CafeOrCoffeeShop"`,
354
- * `"ArtGallery"`, `"HomeAndConstructionBusiness"` — since a narrower type is
353
+ * specific subtype whenever you know one—`"CafeOrCoffeeShop"`,
354
+ * `"ArtGallery"`, `"HomeAndConstructionBusiness"`—since a narrower type is
355
355
  * strictly better for rich results.
356
356
  */
357
357
  businessType?: string;
358
358
  /** `@handle` for Twitter/X card attribution. */
359
359
  twitterHandle?: string;
360
- /** Open Graph locale, e.g. `"en_US"`. */
360
+ /** Open Graph locale, for example, `"en_US"`. */
361
361
  locale?: string;
362
362
  }
363
363
  export interface SecurityConfig {
@@ -391,8 +391,8 @@ export interface SettingsConfig {
391
391
  * Override the editable base `site_settings` columns. Defaults to Astroid's
392
392
  * standard set (`ASTROID_SETTINGS_COLUMNS`). A **custom-heavy** site whose
393
393
  * settings shape doesn't align with the base column names keeps everything in
394
- * `custom` by passing `[]` — otherwise a key that happens to match a base
395
- * column name (e.g. `contactEmail`) would route to that column instead of
394
+ * `custom` by passing `[]`—otherwise a key that happens to match a base
395
+ * column name (for example, `contactEmail`) would route to that column instead of
396
396
  * `custom`, where the site's render reads it.
397
397
  */
398
398
  columns?: string[];
@@ -405,9 +405,22 @@ export interface SettingsConfig {
405
405
  * hours table, ui strings, shop/order config) lists their top-level keys here.
406
406
  */
407
407
  customKeys?: string[];
408
- /** Extra media-library image keys beyond the base logo/favicon/OG defaults —
409
- * settings values validated as media-library URLs on write. */
408
+ /** Extra media-library image keys beyond the base logo/favicon/OG defaults—*
409
+ settings values validated as media-library URLs on write. */
410
410
  imageKeys?: string[];
411
+ /**
412
+ * Take per-key sanitizers and a GET transform from the scaffold-once
413
+ * `src/settings-hooks.ts`. The generated `settingsRoute` and the scaffolded
414
+ * settings Action both spread them in, so the two write paths clean a value
415
+ * the same way.
416
+ *
417
+ * The allowlist alone decides only which keys are written, not what's in
418
+ * them. Turn this on when a site's settings need clamping or normalizing
419
+ * before they're stored, such as a length limit, an email address, or a
420
+ * nested config object. The hooks live in a module rather than in this config
421
+ * because a sanitizer usually imports runtime code, which the CLI can't load.
422
+ */
423
+ hooks?: boolean;
411
424
  }
412
425
  /** Media-library upload policy. */
413
426
  export interface MediaConfig {
@@ -415,7 +428,7 @@ export interface MediaConfig {
415
428
  * Largest accepted upload, in bytes. Default 10 MB (louise-toolkit's
416
429
  * `DEFAULT_MAX_BYTES`).
417
430
  *
418
- * Raise it when the masters ARE the product — a photographer's or painter's
431
+ * Raise it when the masters ARE the product—a photographer's or painter's
419
432
  * portfolio uploads 40 MB camera files and only ever serves Cloudflare-
420
433
  * resized derivatives, so the master's size costs storage, not page weight.
421
434
  *
@@ -426,23 +439,35 @@ export interface MediaConfig {
426
439
  */
427
440
  maxUploadBytes?: number;
428
441
  }
442
+ export interface PagesConfig {
443
+ /**
444
+ * Take a transform and extra reserved slugs for the `pages` route from the
445
+ * scaffold-once `src/pages-hooks.ts`. The generated worker passes them to
446
+ * `astroidPagesWriteHooks`, so the site's transform runs before Astroid's own
447
+ * section sanitize and validate.
448
+ *
449
+ * Turn this on when a site cleans a page write itself: normalizing the slug,
450
+ * clamping a title, or filling a new page's defaults.
451
+ */
452
+ hooks?: boolean;
453
+ }
429
454
  export interface DeployConfig {
430
455
  platform: "cloudflare";
431
- /** Media base for R2 + `cf-image` resizing — matches Louise's media route
456
+ /** Media base for R2 + `cf-image` resizing—matches Louise's media route
432
457
  * (`media.<brand>/cdn-cgi/image`). Default `"/media"`. */
433
458
  mediaBase?: string;
434
459
  }
435
460
  export interface AstroidConfig {
436
461
  /**
437
- * Stable project slug — the worker/D1/R2 base name and default subdomain (e.g.
462
+ * Stable project slug—the worker/D1/R2 base name and default subdomain (for example,
438
463
  * `"coracle"`). Required and non-empty; it drives the generated binding names.
439
464
  */
440
465
  key: string;
441
- /** Hostname(s) this site serves (prod + preview), for custom-domain routes. */
466
+ /** Hostnames this site serves (prod + preview), for custom-domain routes. */
442
467
  hosts?: string[];
443
468
  /**
444
469
  * Serve a wildcard host from this Worker, mapping each subdomain to an
445
- * internal path prefix. See {@link TenancyConfig} — and note it is an
470
+ * internal path prefix. See {@link TenancyConfig}—and note it is an
446
471
  * *audiences* axis, not multi-brand.
447
472
  */
448
473
  tenancy?: TenancyConfig;
@@ -455,8 +480,8 @@ export interface AstroidConfig {
455
480
  /**
456
481
  * A site-provided section catalog that REPLACES the built-in one for
457
482
  * SERVER-side validation + sanitization of `pages.sections` (the generated
458
- * pages route + versions route). A site with bespoke section designs — its own
459
- * `.astro` components and field defs (coracle's 13 sections) — registers them
483
+ * pages route + versions route). A site with bespoke section designs—its own
484
+ * `.astro` components and field defs (coracle's 13 sections)—registers them
460
485
  * here so writes to its custom `_type`s validate instead of 422-ing against the
461
486
  * built-in vocabulary. The on-canvas editor already uses the site's catalog
462
487
  * (its `mountSections` call passes it); this closes the server half so both
@@ -464,13 +489,13 @@ export interface AstroidConfig {
464
489
  */
465
490
  sectionCatalog?: SectionCatalog;
466
491
  /**
467
- * The site's catalog of BLOCK types (ADR 0005) — the block-level analogue of
492
+ * The site's catalog of BLOCK types (ADR 0005)—the block-level analogue of
468
493
  * {@link sectionCatalog}, and required for any section whose def declares a
469
494
  * `blocks` policy.
470
495
  *
471
496
  * Without it the server has no field shape to check a block against, so
472
497
  * `validateSections` rejects every block `_type` as unknown and a block-bearing
473
- * write 422s — the on-canvas block toolbar appears to work and then nothing
498
+ * write returns 422—the on-canvas block toolbar appears to work and then nothing
474
499
  * saves. It also gates block rich-text **sanitization**: block fields are only
475
500
  * scrubbed when their def is resolvable here.
476
501
  *
@@ -499,11 +524,13 @@ export interface AstroidConfig {
499
524
  seo?: SeoConfig;
500
525
  /** Additions to the rate-limit rules + CSP origins Astroid derives. */
501
526
  security?: SecurityConfig;
502
- /** Site-specific editable settings — extra `custom` keys + image keys on top
527
+ /** Site-specific editable settings—extra `custom` keys + image keys on top
503
528
  * of Astroid's base `site_settings` columns. */
504
529
  settings?: SettingsConfig;
505
- /** Media-library upload policy (e.g. a larger `maxUploadBytes`). */
530
+ /** Media-library upload policy (for example, a larger `maxUploadBytes`). */
506
531
  media?: MediaConfig;
532
+ /** The editable `pages` collection's site-owned write hooks. */
533
+ pages?: PagesConfig;
507
534
  /**
508
535
  * Force the contact form + `inquiries` table on or off. Omit to detect from
509
536
  * the config (a `contact` section, or a wholesale-inquiry module). Set `true`
package/dist/config.js CHANGED
@@ -1,28 +1,28 @@
1
1
  // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
2
  //
3
- // `defineAstroid` — the Astroid project configuration surface.
3
+ // `defineAstroid`—the Astroid project configuration surface.
4
4
  //
5
5
  // Astroid is the opinionated layer over Louise Toolkit + Astro. A site's whole
6
- // shape — its brand + theme + editable home, its commerce backend, its optional
7
- // modules — collapses into ONE typed config here. Astroid consumes it to generate
6
+ // shape—its brand + theme + editable home, its commerce backend, its optional
7
+ // modules—collapses into ONE typed config here. Astroid consumes it to generate
8
8
  // the Louise wiring (worker routes, middleware, Drizzle schema, theme tokens) a
9
9
  // site would otherwise hand-write per repo.
10
10
  //
11
11
  // ONE brand per project. Every site Astroid targets (coracle.coffee,
12
12
  // ghostfire.coffee, themidwestartist.com, louise-web) serves a single brand from a
13
13
  // single deploy. The axis that genuinely multiplexes is *editors* (Louise's org
14
- // plugin, #100) and *audiences* — a gated portal alongside the public site, or a
15
- // per-merchant storefront on its own subdomain (`tenancy`) — not brands. So all
14
+ // plugin, #100) and *audiences*—a gated portal alongside the public site, or a
15
+ // per-merchant storefront on its own subdomain (`tenancy`)—not brands. So all
16
16
  // of them live here as options on the one brand, not as a `brands[]` array.
17
17
  //
18
18
  // `tenancy` is worth being precise about, because "serves many hosts" sounds like
19
19
  // the thing this paragraph rules out and isn't. It serves scoped VIEWS OF ONE
20
- // BRAND'S DATA — the same theme, the same catalog, the same editors — narrowed by
20
+ // BRAND'S DATA—the same theme, the same catalog, the same editors—narrowed by
21
21
  // host. It is the portal's axis, one step further out. A second brand still means
22
22
  // a second project.
23
23
  //
24
24
  // The vocabulary below is not invented: `Archetype`, `SectionKind`, and
25
- // `ModuleKind` are extracted from the real sites Astroid targets — a storefront
25
+ // `ModuleKind` are extracted from the real sites Astroid targets—a storefront
26
26
  // (coracle), a wholesale front (ghostfire), an artist portfolio (megbowen), and a
27
27
  // plain marketing baseline (louise-web).
28
28
  import { assertAuthIsolation } from "./auth/index.js";
@@ -37,14 +37,14 @@ import { ASTROID_HEALTH_CRON, astroidCron, astroidUsesQueues } from "./queues/me
37
37
  /**
38
38
  * Each archetype's default home-page sections.
39
39
  *
40
- * Lives here, in TypeScript, rather than in `create-astroid`'s plain JS — the
40
+ * Lives here, in TypeScript, rather than in `create-astroid`'s plain JS—the
41
41
  * other half of #277. As a JS object literal it could name a section that
42
42
  * didn't exist and nothing would say so; typed against {@link SectionKind}
43
43
  * (itself derived from the catalog) a stale name is a compile error, and CI
44
44
  * type-checks this package.
45
45
  *
46
- * The four kinds this used to name — `marquee`, `featured`, `story`, `visit` —
47
- * had no catalog entry or component and could never render. Each is replaced by
46
+ * The four kinds this used to name—`marquee`, `featured`, `story`, `visit`—had
47
+ * no catalog entry or component and could never render. Each is replaced by
48
48
  * the real section that does its job: a marquee is a `banner`, curated picks
49
49
  * are a `productGrid`, a brand-origin block is `aboutIntro`, and "visit" is
50
50
  * exactly `locationHours`.
@@ -79,8 +79,8 @@ export const ASTROID_ARCHETYPE_SECTIONS = {
79
79
  * A duplicate expression is the sharper one: the generated dispatch matches on
80
80
  * `controller.cron` in order, so a custom trigger colliding with a derived one
81
81
  * (or another custom one) never reaches its own branch. Cloudflare fires it, the
82
- * first branch handles it, and the config reads as though both are live —
83
- * exactly the unreachable-trigger failure `config.crons` exists to prevent.
82
+ * first branch handles it, and the config reads as though both are live—exactly
83
+ * the unreachable-trigger failure `config.crons` exists to prevent.
84
84
  */
85
85
  function assertCrons(config) {
86
86
  const crons = config.crons ?? [];
@@ -114,7 +114,7 @@ function assertCrons(config) {
114
114
  *
115
115
  * All three are cheap to state and expensive to discover: two surface as a
116
116
  * wrangler deploy error naming a zone or a pattern rather than the config that
117
- * produced it, and the third never surfaces at all — it just serves the wrong
117
+ * produced it, and the third never surfaces at all—it just serves the wrong
118
118
  * page.
119
119
  */
120
120
  function assertTenancy(config) {
@@ -134,7 +134,7 @@ function assertTenancy(config) {
134
134
  throw new AstroidConfigError(`\`tenancy.hostPattern\` "${pattern}" has no domain after the wildcard`);
135
135
  }
136
136
  // A wildcard does NOT match its own apex, so the apex needs its own route.
137
- // Without one it 404s — and the symptom is "the marketing site is down"
137
+ // Without one it returns 404—and the symptom is "the marketing site is down"
138
138
  // immediately after enabling a feature that reads like it only adds hosts.
139
139
  if (!(config.hosts ?? []).some((host) => host.toLowerCase() === apex.toLowerCase())) {
140
140
  throw new AstroidConfigError(`\`tenancy.hostPattern\` is "${pattern}", but "${apex}" is not in \`hosts\`. ` +
@@ -161,7 +161,7 @@ function assertTenancy(config) {
161
161
  }
162
162
  }
163
163
  /** Cloudflare rejects a request body over 100 MB at the edge, before any Worker
164
- * handler runs — so an upload limit above it can never be honoured, and the
164
+ * handler runs—so an upload limit above it can never be honoured, and the
165
165
  * failure arrives as an opaque edge error rather than the route's own 413. */
166
166
  const WORKERS_MAX_REQUEST_BODY_BYTES = 100 * 1024 * 1024;
167
167
  function assertMediaConfig(media) {
@@ -191,15 +191,15 @@ export function defineAstroid(config) {
191
191
  // Fourthwall, a storefront over Stripe) fails here rather than at runtime on
192
192
  // the first invoice, as a missing function.
193
193
  assertCommerceRoles(config.commerce);
194
- // A media limit above the platform's own body cap is unhonourable — reject it
194
+ // A media limit above the platform's own body cap is unhonourable—reject it
195
195
  // here rather than let an editor watch a 120 MB upload die at the edge.
196
196
  assertMediaConfig(config.media);
197
197
  // A portal is a SECOND Better Auth instance beside the editor's. Reject any
198
198
  // isolation that would collide with the editor on the same origin (a shared
199
199
  // cookie prefix silently cross-signs-out; a shared table prefix merges the two
200
- // user tables) — the intermittent-prod failure the fixed defaults prevent.
200
+ // user tables)—the intermittent-prod failure the fixed defaults prevent.
201
201
  assertAuthIsolation(config);
202
- // `portal.gated` is declared and resolved but read by NOTHING — the guard
202
+ // `portal.gated` is declared and resolved but read by NOTHING—the guard
203
203
  // table is built from `portal.routes` alone, and `portalGuard` allows any
204
204
  // unmatched path. So a site that set it believed the whole site sat behind a
205
205
  // login (a pre-launch client gallery) while every page outside /portal was