astroidjs 0.12.1 → 0.13.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 (133) hide show
  1. package/README.md +42 -42
  2. package/bin/astroid.mjs +25 -25
  3. package/dist/analytics/index.d.ts +3 -3
  4. package/dist/analytics/index.js +9 -9
  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 +9 -9
  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 +9 -9
  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 +62 -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 +4 -4
  44. package/dist/map/style.d.ts +4 -4
  45. package/dist/map/style.js +1 -1
  46. package/dist/portal/config.d.ts +2 -2
  47. package/dist/portal/config.js +3 -3
  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 +6 -6
  53. package/dist/portal/session.d.ts +2 -2
  54. package/dist/portal/session.js +5 -5
  55. package/dist/portfolio/scaffold.d.ts +1 -1
  56. package/dist/portfolio/scaffold.js +4 -4
  57. package/dist/project/actions.d.ts +1 -1
  58. package/dist/project/actions.js +6 -6
  59. package/dist/project/generate.d.ts +4 -4
  60. package/dist/project/generate.js +15 -15
  61. package/dist/project/index.js +1 -1
  62. package/dist/project/scaffold.d.ts +2 -2
  63. package/dist/project/scaffold.js +11 -11
  64. package/dist/pwa/generate.d.ts +11 -11
  65. package/dist/pwa/generate.js +12 -12
  66. package/dist/queues/consumer.d.ts +3 -3
  67. package/dist/queues/consumer.js +2 -2
  68. package/dist/queues/messages.d.ts +4 -4
  69. package/dist/queues/messages.js +2 -2
  70. package/dist/queues/scaffold.d.ts +4 -4
  71. package/dist/queues/scaffold.js +8 -8
  72. package/dist/queues/webhook.d.ts +5 -5
  73. package/dist/queues/webhook.js +3 -3
  74. package/dist/realtime/scaffold.d.ts +4 -4
  75. package/dist/realtime/scaffold.js +8 -8
  76. package/dist/schema/collections.d.ts +6 -6
  77. package/dist/schema/collections.js +23 -23
  78. package/dist/schema/framework.d.ts +1 -1
  79. package/dist/schema/framework.js +2 -2
  80. package/dist/schema/generate.js +2 -2
  81. package/dist/schema/index.js +1 -1
  82. package/dist/secrets.d.ts +6 -6
  83. package/dist/secrets.js +6 -6
  84. package/dist/security/csp-origins.d.ts +1 -1
  85. package/dist/security/csp-origins.js +3 -3
  86. package/dist/security/rate-rules.d.ts +2 -2
  87. package/dist/security/rate-rules.js +8 -8
  88. package/dist/seo/resolve.d.ts +5 -5
  89. package/dist/seo/resolve.js +2 -2
  90. package/dist/seo/routes.d.ts +5 -5
  91. package/dist/seo/routes.js +3 -3
  92. package/dist/seo/structured-data.d.ts +6 -6
  93. package/dist/seo/structured-data.js +7 -7
  94. package/dist/status.d.ts +5 -5
  95. package/dist/status.js +7 -7
  96. package/dist/tenancy/index.d.ts +3 -3
  97. package/dist/tenancy/index.js +6 -6
  98. package/dist/worker/generate.d.ts +2 -2
  99. package/dist/worker/generate.js +19 -19
  100. package/dist/worker/index.js +1 -1
  101. package/dist/worker/routes.js +1 -1
  102. package/dist/workflow/advance.d.ts +3 -3
  103. package/dist/workflow/advance.js +6 -6
  104. package/dist/workflow/config.d.ts +4 -4
  105. package/dist/workflow/config.js +4 -4
  106. package/dist/workflow/generate.d.ts +2 -2
  107. package/dist/workflow/generate.js +4 -4
  108. package/package.json +3 -4
  109. package/src/components/Collection.tsx +5 -5
  110. package/src/components/Editable.astro +9 -9
  111. package/src/components/JustifiedGallery.astro +8 -8
  112. package/src/components/MediaSlot.astro +12 -12
  113. package/src/components/PortalShell.astro +4 -4
  114. package/src/components/RegisterSW.astro +3 -3
  115. package/src/components/Section.astro +8 -8
  116. package/src/components/Sections.astro +6 -6
  117. package/src/components/Seo.astro +3 -3
  118. package/src/components/StageBar.astro +3 -3
  119. package/src/components/StructuredData.astro +2 -2
  120. package/src/components/justify.ts +9 -9
  121. package/src/components/media-meta.ts +10 -10
  122. package/src/components/sections/AboutIntro.astro +1 -1
  123. package/src/components/sections/Contact.astro +1 -1
  124. package/src/components/sections/Cta.astro +1 -1
  125. package/src/components/sections/Faq.astro +1 -1
  126. package/src/components/sections/FeatureGrid.astro +2 -2
  127. package/src/components/sections/Hero.astro +1 -1
  128. package/src/components/sections/PricingTiers.astro +1 -1
  129. package/src/components/sections/ProductGrid.astro +1 -1
  130. package/src/components/sections/SplitImage.astro +1 -1
  131. package/src/components/sections/Steps.astro +1 -1
  132. package/src/components/sections/Testimonial.astro +1 -1
  133. package/src/components/sections.ts +17 -17
package/README.md CHANGED
@@ -1,16 +1,16 @@
1
1
  # astroidjs
2
2
 
3
- **Astroid** — an opinionated meta-framework over
3
+ **Astroid**—an opinionated meta-framework over
4
4
  [Louise Toolkit](https://github.com/bowenlabs/louise-toolkit/tree/main/packages/louise)
5
5
  and Astro for building editable, multi-editor sites on Cloudflare Workers.
6
6
 
7
- > **Status: pre-1.0, experimental.** The API will change between minor versions —
7
+ > **Status: pre-1.0, experimental.** The API will change between minor versions—
8
8
  > pin an exact version if you depend on it. Astroid lives in the same workspace as
9
9
  > Louise so its opinions co-evolve with the toolkit.
10
10
 
11
11
  ## What it is
12
12
 
13
- Louise is the unopinionated toolkit — primitives you assemble by hand. Astroid is
13
+ Louise is the unopinionated toolkit—primitives you assemble by hand. Astroid is
14
14
  the opinionated preset on top: a theme system, a section library, and a single
15
15
  config that generates the Louise wiring (worker routes, middleware, schema,
16
16
  theme) a site would otherwise hand-write per repo.
@@ -29,12 +29,12 @@ exports. This keeps the toolkit neutral while Astroid holds the opinions.
29
29
 
30
30
  ## Configure
31
31
 
32
- The whole shape of a project — its brand + theme + editable home, its commerce
33
- backend and optional modules — collapses into one typed config. **One brand per
32
+ The whole shape of a project—its brand + theme + editable home, its commerce
33
+ backend and optional modules—collapses into one typed config. **One brand per
34
34
  project:** every site Astroid targets serves a single brand from a single deploy,
35
35
  so the config describes one brand, not an array. What actually multiplexes is
36
36
  _editors_ (Louise's org plugin) and _audiences_ (a gated portal beside the public
37
- site) — both options on the one brand. The vocabulary is drawn from the real
37
+ site)—both options on the one brand. The vocabulary is drawn from the real
38
38
  sites Astroid targets: a storefront (coracle.coffee), a wholesale front
39
39
  (ghostfire.coffee), an artist portfolio (themidwestartist.com), and a plain
40
40
  marketing baseline (louise-web).
@@ -71,14 +71,14 @@ export default defineAstroid({
71
71
 
72
72
  `media.maxUploadBytes` is checked at generate time, not in production: a value
73
73
  above Cloudflare's **100 MB request-body limit** is rejected outright, because
74
- the edge drops an oversized body before the Worker runs — the media route never
74
+ the edge drops an oversized body before the Worker runs—the media route never
75
75
  gets to answer with its own `413`, so the editor would see an opaque failure.
76
76
  Zero, negatives, and non-integers are rejected too (that last one catches the
77
77
  megabytes-not-bytes slip).
78
78
 
79
79
  ## Commerce
80
80
 
81
- **Providers fill roles.** Not "the commerce provider" — the toolkit's clients
81
+ **Providers fill roles.** Not "the commerce provider"—the toolkit's clients
82
82
  make that impossible: `commerce/stripe` has no catalog API, `commerce/fourthwall`
83
83
  has no invoicing, Square does both. So `storefront` and `invoicing` are assigned
84
84
  independently, and a provider put in a role it can't serve fails at config load
@@ -93,7 +93,7 @@ commerce: { provider: "square" } // shorthand → s
93
93
  of truth; D1 holds the owner's edits. `mode: "mirror"` keeps the catalog fields
94
94
  in D1 (fast reads, briefly stale); `mode: "overlay"` keeps only the owner's
95
95
  columns (never stale, one provider round-trip per read). The sync **never writes
96
- an owned column** — a sync that does silently reverts the owner's work, and
96
+ an owned column**—a sync that does silently reverts the owner's work, and
97
97
  they find out days later. `slug` is owned for that reason: it's the public URL,
98
98
  so a provider rename must not break links.
99
99
 
@@ -106,14 +106,14 @@ commerce: {
106
106
 
107
107
  Everything else follows from that table. `astroidCatalogLoaderConfig` reads it
108
108
  for the Live Content Collection, and the adapters normalize before the row is
109
- written — so one loader definition serves a Square site and a Fourthwall site,
109
+ written—so one loader definition serves a Square site and a Fourthwall site,
110
110
  which is the drift the module exists to kill.
111
111
 
112
112
  **Checkout is server-authoritative.** `verifyCheckout` treats the client's price
113
113
  as a staleness check, never an input to the charge: re-price server-side, refuse
114
114
  on mismatch. `checkoutIdempotencyKey` derives a stable key from the verified cart
115
- **and a required `identity`**, so a double-clicked Pay button charges once —
116
- while two customers buying the same thing stay two charges.
115
+ **and a required `identity`**, so a double-clicked Pay button charges once—while
116
+ two customers buying the same thing stay two charges.
117
117
 
118
118
  ```ts
119
119
  const key = await checkoutIdempotencyKey(check, "order", cartId);
@@ -122,7 +122,7 @@ const key = await checkoutIdempotencyKey(check, "order", cartId);
122
122
  Pass something stable across a retry of this attempt and distinct between buyers
123
123
  (a cart id, checkout-session id, or portal user id). It is required, and empty is
124
124
  refused, because a key derived from cart contents alone collides between
125
- customers: providers scope idempotency keys per account for ~24h, so the second
125
+ customers: providers scope idempotency keys per account for about 24 hours, so the second
126
126
  buyer's charge is deduped into the first buyer's order and never happens.
127
127
 
128
128
  ## The webhook pipeline
@@ -133,7 +133,7 @@ gains a webhook receiver plus a consumer seam. `--commerce <provider>` on
133
133
  `pnpm create astroid` sets it all up.
134
134
 
135
135
  The receiver's ordering is the part worth knowing. `handleWebhook` verifies the
136
- HMAC over the **raw body before anything parses it** — parse first and an
136
+ HMAC over the **raw body before anything parses it**—parse first and an
137
137
  unauthenticated caller reaches the JSON parser and everything downstream, and
138
138
  re-serializing a parsed body to check a signature is how signature checks quietly
139
139
  stop checking anything. It then enqueues and returns, so the response doesn't
@@ -144,15 +144,15 @@ is picked for what it tells the sender to do:
144
144
 
145
145
  | Situation | Code | Why |
146
146
  | ----------------------- | ---- | --------------------------------------------------------------------------------------------- |
147
- | Secret unprovisioned | 503 | Dormant is temporary — keep retrying so events delivered before you set the secret still land |
147
+ | Secret unprovisioned | 503 | Dormant is temporary—keep retrying so events delivered before you set the secret still land |
148
148
  | Bad / missing signature | 401 | Terminal. It won't verify on retry either, and retrying turns a misconfiguration into a flood |
149
149
  | Body isn't JSON | 400 | Terminal for the same reason |
150
- | Enqueue failed | 503 | The signature checked out, so the event is real — ask for redelivery |
150
+ | Enqueue failed | 503 | The signature checked out, so the event is real—ask for redelivery |
151
151
  | Enqueued | 202 | Accepted, not done. That's the point of a queue |
152
152
 
153
153
  On the consumer side `astroidQueueHandler` owns the dispatch every site wrote: a
154
154
  periodic refresh re-syncs, a webhook re-syncs _only_ if it touched the catalog,
155
- and everything else acks as a no-op. That last part matters — order and payment
155
+ and everything else acks as a no-op. That last part matters—order and payment
156
156
  events arrive in volume and have nothing local to update, so treating them as
157
157
  actionable turns a busy sales day into a refresh storm.
158
158
 
@@ -162,8 +162,8 @@ the same retry and DLQ path as everything else. Retries and DLQ routing live in
162
162
 
163
163
  ## Transactional email
164
164
 
165
- Four templates — sign-in link, password reset, and the inquiry pair (notify the
166
- owner, confirm to the sender) — over the toolkit's email shell. Each renders HTML
165
+ Four templates—sign-in link, password reset, and the inquiry pair (notify the
166
+ owner, confirm to the sender)—over the toolkit's email shell. Each renders HTML
167
167
  **and** plaintext from one definition: a message with no text/plain part scores
168
168
  worse with spam filters, and for a sign-in link the plaintext body is what a
169
169
  terminal client shows and what the dev log prints.
@@ -172,12 +172,12 @@ terminal client shows and what the dev log prints.
172
172
  Neutrals stay fixed (they're typography choices, not brand ones); what varies is
173
173
  the accent and the five-cell masthead band, built as a ramp so it reads as
174
174
  designed whether you configured one brand colour or three. The accent is
175
- **contrast-corrected** — a brand yellow used verbatim as 11px uppercase text on a
175
+ **contrast-corrected**—a brand yellow used verbatim as 11px uppercase text on a
176
176
  near-white card is unreadable, and mail clients have no dark-mode escape hatch.
177
177
  Pass overrides for any slot you want to own.
178
178
 
179
179
  Delivery is best-effort and never throws. Mail here is always the notification of
180
- something already durable — the inquiry row is inserted, the account exists — so
180
+ something already durable—the inquiry row is inserted, the account exists—so
181
181
  a failure must not fail the request that caused it, and messages send
182
182
  independently so the owner's copy still arrives when a visitor typos their
183
183
  address. With no `EMAIL` binding the mailer is **dormant**: it logs the rendered
@@ -194,7 +194,7 @@ formRoute({
194
194
 
195
195
  ## SEO
196
196
 
197
- A settings-driven head, structured data, and the two crawler files — first-party,
197
+ A settings-driven head, structured data, and the two crawler files—first-party,
198
198
  no `astro-seo` dependency.
199
199
 
200
200
  `<Seo>` resolves three levels (page override → the page's own default →
@@ -202,20 +202,20 @@ no `astro-seo` dependency.
202
202
  clearing a field in the editor falls back instead of publishing a blank `<meta>`.
203
203
  The title template applies only when a page supplies its own title, so the home
204
204
  page reads `Acme Coffee`, not `Acme Coffee | Acme Coffee`. `disableIndexing` in
205
- settings is a site-wide kill switch that beats any page asking to be indexed —
206
- useful for staging.
205
+ settings is a site-wide kill switch that beats any page asking to be indexed—useful
206
+ for staging.
207
207
 
208
208
  `<StructuredData>` emits a schema.org `@graph`: the business, the `WebSite`, and
209
209
  optionally the entity the page is _about_ (a Product, a VisualArtwork). The
210
210
  business `@type` comes from the archetype (`storefront` → `Store`, `portfolio` →
211
211
  `Person`); set `seo.businessType` to a narrower subtype whenever you know one.
212
- The payload is escaped with `escapeJsonLd`, not `JSON.stringify` — `stringify`
212
+ The payload is escaped with `escapeJsonLd`, not `JSON.stringify`—`stringify`
213
213
  doesn't escape `<`, so an editor-authored value containing `</script>` would
214
214
  close the tag early and inject markup into `<head>`.
215
215
 
216
216
  `robots.txt` and `sitemap.xml` derive their disallow list from the same config
217
217
  (`astroidNoindexPaths`), so the two files can't disagree about what's crawlable.
218
- Both are **origin-aware** — built from the serving origin rather than a
218
+ Both are **origin-aware**—built from the serving origin rather than a
219
219
  configured domain, because a preview deploy advertising the production host
220
220
  invites its content to be indexed under the real domain.
221
221
 
@@ -228,21 +228,21 @@ framework.
228
228
  calls `astroidRateRules(config)`: the editor magic-link always (the
229
229
  email-bombing target, so the tightest budget in the set), the portal's
230
230
  credential surfaces when `portal.enabled`, checkout when `commerce` is set. The
231
- session-gated editor API stays out on purpose — a limiter that can lock the
231
+ session-gated editor API stays out on purpose—a limiter that can lock the
232
232
  owner out of their own studio is worse than the abuse it stops. Add or override
233
233
  via `security.rateRules`, which is matched _before_ the defaults, so you replace
234
234
  one budget rather than the whole set.
235
235
 
236
236
  **The CSP is composed, and it's split for a reason.** `astroidSecurity(config)`
237
- gives `astro.config.mjs` its `security` block. Astro owns `script-src` — it
238
- hashes every script it processes, so the policy needs no `'unsafe-inline'` — and
237
+ gives `astro.config.mjs` its `security` block. Astro owns `script-src`—it
238
+ hashes every script it processes, so the policy needs no `'unsafe-inline'`—and
239
239
  Astroid adds the one hash Astro can't produce itself: Solid's hydration
240
240
  bootstrap, injected by `@astrojs/solid-js` on every page with an island.
241
241
  Computing it from `generateHydrationScript()` means it tracks solid-js upgrades
242
242
  instead of going stale as a copy-pasted literal. Meanwhile the generated
243
243
  middleware rewrites _only_ `style-src`, because Louise's data-driven `style=""`
244
244
  carriers need `'unsafe-inline'` and a single hash in that directive would void
245
- it per spec — the two cannot share one directive.
245
+ it per spec—the two cannot share one directive.
246
246
 
247
247
  ```js
248
248
  // astro.config.mjs
@@ -262,14 +262,14 @@ the captcha frame); `security.cspOrigins` adds anything Astroid can't see.
262
262
 
263
263
  Astroid's optional modules are opt-in at the _config_ level, never at the
264
264
  _account_ level: switching commerce on must not require a Square account before
265
- `pnpm dev` will boot. So a module whose secrets aren't provisioned is **dormant**
266
- — it renders, it serves, it says out loud that it's simulated, and it never calls
265
+ `pnpm dev` will boot. So a module whose secrets aren't provisioned is **dormant**—it
266
+ renders, it serves, it says out loud that it's simulated, and it never calls
267
267
  upstream with a dummy credential. A fresh clone runs with zero external accounts.
268
268
 
269
269
  `create-astroid` seeds every module secret with one loud sentinel,
270
270
  `DUMMY_REPLACE_ME`, so a scaffold has a complete and valid binding set and no
271
- real credentials. Reading a secret back that still holds the sentinel — or that
272
- is absent, empty, or bound to an unprovisioned store — yields `null`:
271
+ real credentials. Reading a secret back that still holds the sentinel—or that
272
+ is absent, empty, or bound to an unprovisioned store—yields `null`:
273
273
 
274
274
  ```ts
275
275
  import { resolveModuleSecrets, describeModuleStatus } from "astroidjs";
@@ -292,15 +292,15 @@ exists to prevent.
292
292
 
293
293
  The scaffold ships one worked example: Turnstile captcha on the **editor
294
294
  sign-in**, seeded with the sentinel secret plus Cloudflare's always-passing test
295
- site key, enforcing only once **both** halves are real — so provisioning one of
295
+ site key, enforcing only once **both** halves are real—so provisioning one of
296
296
  them can't lock you out of your own sign-in.
297
297
 
298
298
  Both halves matter, and the second one is the reason this is worth spelling out.
299
299
  `getLouiseAuth` registers Better Auth's captcha plugin on `/sign-in/magic-link`
300
300
  as soon as the pair is real, and that plugin rejects any request without an
301
301
  `x-captcha-response` header. So the login page renders the widget under exactly
302
- the same condition the server arms the check — `turnstileSiteKey` returns null
303
- for the test key, the same test `activeCaptchaSecret` applies — and forwards the
302
+ the same condition the server arms the check—`turnstileSiteKey` returns null
303
+ for the test key, the same test `activeCaptchaSecret` applies—and forwards the
304
304
  token in that header. A gate that turns on server-side while the page keeps
305
305
  posting without a token is not a half-configured integration; it is a locked
306
306
  door with the owner outside.
@@ -328,22 +328,22 @@ astroid deploy provision bindings + migrate + secrets + deploy (--dry-run /
328
328
  `deploy` is plan-first: it prints exactly what it will run and refuses to
329
329
  provision non-interactively without `--yes` (use `--dry-run` to preview).
330
330
 
331
- The generated trio carries a "do not hand-edit" banner — `generate` (and
331
+ The generated trio carries a "do not hand-edit" banner—`generate` (and
332
332
  `dev`/`build`) rewrite them on every run, and `doctor` diffs them against your
333
333
  config to catch drift. Your `wrangler.jsonc` is scaffolded once and then yours to
334
334
  edit (real binding ids, secrets); `generate` never touches it.
335
335
 
336
336
  New projects come from the `create-astroid` scaffold (`pnpm create astroid`), which
337
- writes the floor — config, the generated trio, `wrangler.jsonc`, and the baseline
338
- Astro app — in one step.
337
+ writes the floor—config, the generated trio, `wrangler.jsonc`, and the baseline
338
+ Astro app—in one step.
339
339
 
340
340
  ## Roadmap
341
341
 
342
- 1. ✅ **Config surface** (`defineAstroid`) — single brand per project.
342
+ 1. ✅ **Config surface** (`defineAstroid`)—single brand per project.
343
343
  2. ✅ Config → generated Drizzle schema.
344
344
  3. ✅ Config → generated `worker.ts` + middleware (no hand-wired route ordering).
345
345
  4. ✅ `<Section>` / `<Editable>` / `<Collection>` component primitives.
346
- 5. ✅ **CLI** — `astroid generate / doctor / dev / build / deploy`; `create-astroid`
346
+ 5. ✅ **CLI**—`astroid generate / doctor / dev / build / deploy`; `create-astroid`
347
347
  scaffold (`pnpm create astroid`).
348
348
 
349
349
  ## License
package/bin/astroid.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
3
3
  //
4
- // The `astroid` CLI — the meta-framework's project commands:
4
+ // The `astroid` CLI—the meta-framework's project commands:
5
5
  //
6
6
  // astroid generate [--config <path>] [--cwd <dir>] regenerate schema/worker/middleware from the config
7
7
  // astroid doctor [--config <path>] [--cwd <dir>] validate config + bindings + generated-file freshness
@@ -11,7 +11,7 @@
11
11
  //
12
12
  // It loads the project's `astroid.config.ts` with Node's native TypeScript
13
13
  // stripping (the config only imports the built `astroidjs`, so it resolves), and
14
- // consumes this package's own built generators from ../dist — the same version the
14
+ // consumes this package's own built generators from ../dist—the same version the
15
15
  // CLI ships in, no dependency on node_modules layout (mirrors the louise bin).
16
16
 
17
17
  import { execFileSync, spawn, spawnSync } from "node:child_process";
@@ -89,7 +89,7 @@ async function cmdGenerate(cwd, flags, { quiet = false } = {}) {
89
89
 
90
90
  // Scaffold-once files for whatever modules the config switched on.
91
91
  //
92
- // Written only when ABSENT — each is a seam the project owns, so overwriting
92
+ // Written only when ABSENT—each is a seam the project owns, so overwriting
93
93
  // one would destroy the edit it exists to hold. But a MISSING one is not a
94
94
  // choice the user made: the trio above emits static imports of `./queue.js`
95
95
  // and `./portal-auth.js`, so a config that gained a module after scaffold
@@ -138,7 +138,7 @@ async function cmdDoctor(cwd, flags) {
138
138
 
139
139
  ok(`config loads and validates (${rel(cwd, configPath)})`);
140
140
 
141
- // 1. Generated trio freshness — regenerate in memory, diff against disk.
141
+ // 1. Generated trio freshness—regenerate in memory, diff against disk.
142
142
  for (const file of generateAstroidProject(config)) {
143
143
  const abs = join(cwd, file.path);
144
144
  if (!existsSync(abs)) {
@@ -146,7 +146,7 @@ async function cmdDoctor(cwd, flags) {
146
146
  } else if (readFileSync(abs, "utf8") !== file.contents) {
147
147
  // An ERROR, not a warning. These three carry a "do not hand-edit" banner
148
148
  // and are a pure function of the config, so "stale" means the project on
149
- // disk is not the project the config describes — which is the single
149
+ // disk is not the project the config describes—which is the single
150
150
  // condition doctor exists to catch. As a warning it printed "healthy" and
151
151
  // exited 0, so `pnpm doctor` could not gate CI on it.
152
152
  err(`${file.path} is stale (out of sync with your config) — run \`astroid generate\`.`);
@@ -159,14 +159,14 @@ async function cmdDoctor(cwd, flags) {
159
159
  // emits static imports of `./queue.js` / `./portal-auth.js`, so a config
160
160
  // that names a module whose seam was never written produces a project that
161
161
  // cannot resolve its own imports. This is precisely the state that used to
162
- // report "healthy, 2 warning(s)" and exit 0.
162
+ // report "healthy" with two warnings and exit 0.
163
163
  for (const file of generateAstroidScaffoldFiles(config)) {
164
- if (file.apply === "append-once") continue; // accumulated, not owned — see below
164
+ if (file.apply === "append-once") continue; // accumulated, not owned—see below
165
165
  if (existsSync(join(cwd, file.path))) ok(`${file.path} present`);
166
166
  else err(`${file.path} is missing (required by your config) — run \`astroid generate\`.`);
167
167
  }
168
168
 
169
- // 2. wrangler.jsonc bindings — presence checks + placeholder detection. Read as
169
+ // 2. wrangler.jsonc bindings—presence checks + placeholder detection. Read as
170
170
  // text (JSONC with comments/trailing commas) rather than parse, to stay robust.
171
171
  const wranglerPath = join(cwd, "wrangler.jsonc");
172
172
  if (!existsSync(wranglerPath)) {
@@ -209,7 +209,7 @@ async function cmdDoctor(cwd, flags) {
209
209
  // Email Sending declares its binding as `"name"`, not `"binding"`, so the
210
210
  // regex above cannot see it. Checked separately because sign-in depends on
211
211
  // it: the magic link is console-logged in dev and EMAILED in production, so
212
- // a missing binding is a site nobody can sign in to — and it fails only once
212
+ // a missing binding is a site nobody can sign in to—and it fails only once
213
213
  // deployed, which is the one place nothing in this repo exercises.
214
214
  if (/"send_email"\s*:/.test(w)) ok("wrangler: Email Sending `EMAIL` binding present");
215
215
  else
@@ -228,8 +228,8 @@ async function cmdDoctor(cwd, flags) {
228
228
  // Cron triggers. The generated `scheduled` handler dispatches on a fixed set
229
229
  // of cron strings (`astroidCrons`); every one of them MUST be declared here
230
230
  // or Cloudflare never fires it and that job silently never runs. Because
231
- // wrangler.jsonc is scaffold-once, a config that gains a cron (e.g. the daily
232
- // health scan alongside the hourly catalog sync) can't update it — the exact
231
+ // wrangler.jsonc is scaffold-once, a config that gains a cron (for example, the daily
232
+ // health scan alongside the hourly catalog sync) can't update it—the exact
233
233
  // drift that shipped a dead daily scan while the handler dispatched on it and
234
234
  // doctor reported healthy. Parse the array from the JSONC text rather than
235
235
  // JSON.parse (comments/trailing commas), matching the binding checks above.
@@ -268,14 +268,14 @@ async function cmdDoctor(cwd, flags) {
268
268
  if (existsSync(join(cwd, "migrations"))) ok("migrations/ directory present");
269
269
  else warn("no migrations/ directory — create your D1 schema migrations there.");
270
270
 
271
- // 4. Local secret provisioning — which modules will run dormant under
271
+ // 4. Local secret provisioning—which modules will run dormant under
272
272
  // `astroid dev`, and what to set to wake them.
273
273
  //
274
274
  // Scoped deliberately to SECRETS, read from .dev.vars. Doctor is a static
275
275
  // CLI: it cannot see runtime bindings (EMAIL, the queue), so claiming
276
276
  // "email is dormant" here would be reporting its own blindness as the
277
277
  // project's state. What it CAN say for certain is whether a value is still
278
- // the placeholder — and that is the half developers actually forget.
278
+ // the placeholder—and that is the half developers actually forget.
279
279
  //
280
280
  // A dormant module is never an error. It is the documented default, and a
281
281
  // fresh scaffold is expected to report every module dormant.
@@ -295,7 +295,7 @@ async function cmdDoctor(cwd, flags) {
295
295
  const value = (devVars?.[name] ?? "").trim();
296
296
  return value === "" || value === ASTROID_SECRET_PLACEHOLDER;
297
297
  });
298
- // `core` is not a module — its secrets each gate their own thing (sessions
298
+ // `core` is not a module—its secrets each gate their own thing (sessions
299
299
  // fail closed off localhost, Turnstile needs a matched pair), so a blanket
300
300
  // "dormant" line would be wrong. Report it as provisioning, not dormancy.
301
301
  if (unset.length === 0) ok(`${moduleName}: all secrets provisioned in .dev.vars`);
@@ -337,7 +337,7 @@ async function cmdAstro(cwd, subcommand, flags, rest) {
337
337
  }
338
338
 
339
339
  /** Resolve a project-local CLI bin (astro, wrangler) to an absolute path via the
340
- * project's own dependency resolution — so we run the version it ships. */
340
+ * project's own dependency resolution—so we run the version it ships. */
341
341
  function resolveBin(cwd, pkgName, binName) {
342
342
  try {
343
343
  const require = createRequire(join(cwd, "package.json"));
@@ -353,14 +353,14 @@ function resolveBin(cwd, pkgName, binName) {
353
353
  // --- deploy ----------------------------------------------------------------
354
354
  // `astroid deploy` orchestrates the one-time platform bring-up: provision the
355
355
  // bindings that still hold placeholder ids (D1/R2/KV), apply migrations, prompt
356
- // for secrets, and deploy — all by shelling out to the project's own `wrangler`.
356
+ // for secrets, and deploy—all by shelling out to the project's own `wrangler`.
357
357
  // It's plan-first: it prints exactly what it will run, and only proceeds past the
358
358
  // irreversible steps on an interactive `y` (or `--yes`). `--dry-run` prints the
359
359
  // plan and stops; `--local` targets the local D1 for migrations.
360
360
 
361
361
  const isPlaceholder = (v) => !v || /^<.*>$/.test(v);
362
362
 
363
- /** Pull the deploy-relevant bits out of wrangler.jsonc by regex — robust against
363
+ /** Pull the deploy-relevant bits out of wrangler.jsonc by regex—robust against
364
364
  * its JSONC comments + trailing commas (a strict JSON.parse would throw). */
365
365
  function readWranglerFacts(text) {
366
366
  return {
@@ -373,8 +373,8 @@ function readWranglerFacts(text) {
373
373
  id: m[2],
374
374
  })),
375
375
  // Queue names, from the producer + the consumer's dead_letter_queue. Unlike
376
- // D1/R2/KV these are referenced BY NAME, so there's no id to patch back —
377
- // creating them is the whole job.
376
+ // D1/R2/KV these are referenced BY NAME, so there's no id to patch back—creating
377
+ // them is the whole job.
378
378
  queues: [
379
379
  ...new Set([
380
380
  ...[...text.matchAll(/"queue":\s*"([^"]+)"/g)].map((m) => m[1]),
@@ -400,7 +400,7 @@ function provisionPlan(facts) {
400
400
  steps.push({ kind: "kv", name: binding, args: ["kv", "namespace", "create", binding] });
401
401
  }
402
402
  }
403
- // Queues carry no id, so there's no placeholder to test — creating one that
403
+ // Queues carry no id, so there's no placeholder to test—creating one that
404
404
  // already exists just errors, which the runner tolerates (same as R2).
405
405
  for (const queue of facts.queues) {
406
406
  steps.push({ kind: "queue", name: queue, args: ["queues", "create", queue] });
@@ -437,9 +437,9 @@ async function cmdDeploy(cwd, flags, rest) {
437
437
  const wranglerPath = join(cwd, "wrangler.jsonc");
438
438
  if (!existsSync(wranglerPath)) fail("wrangler.jsonc not found — run inside an Astroid project.");
439
439
 
440
- // Regenerate so the shipped worker/schema always match the config — but NOT
440
+ // Regenerate so the shipped worker/schema always match the config—but NOT
441
441
  // on a dry run. This used to sit above the `--dry-run` guard, so a command
442
- // that ends by printing "(dry run — nothing executed)" had already rewritten
442
+ // that ends by printing "(dry run—nothing executed)" had already rewritten
443
443
  // three files and silently discarded any local edits to them. Probing the
444
444
  // plan on a working branch is exactly when someone has local edits.
445
445
  if (!dryRun) await cmdGenerate(cwd, flags, { quiet: true });
@@ -494,8 +494,8 @@ async function cmdDeploy(cwd, flags, rest) {
494
494
  for (const s of plan) {
495
495
  out(`\n▸ wrangler ${s.args.join(" ")}`);
496
496
  const res = runInherit(s.args);
497
- // R2 bucket + queue creates are idempotent-ish (an existing one errors) —
498
- // tolerate those so a re-run of `astroid deploy` isn't a hard stop.
497
+ // R2 bucket + queue creates are idempotent-ish (an existing one errors)—tolerate
498
+ // those so a re-run of `astroid deploy` isn't a hard stop.
499
499
  if (res.status !== 0 && s.kind !== "r2" && s.kind !== "queue") {
500
500
  fail(`Provisioning failed at: wrangler ${s.args.join(" ")}`);
501
501
  }
@@ -551,7 +551,7 @@ async function cmdDeploy(cwd, flags, rest) {
551
551
 
552
552
  // --- helpers ---------------------------------------------------------------
553
553
  /**
554
- * Minimal KEY=VALUE reader for .dev.vars. Not a dotenv implementation — doctor
554
+ * Minimal KEY=VALUE reader for .dev.vars. Not a dotenv implementation—doctor
555
555
  * only needs to know whether a value is absent, empty, or the placeholder, so
556
556
  * expansion, multiline values, and `export` prefixes are out of scope. Quotes
557
557
  * are stripped because wrangler accepts them and a quoted placeholder must
@@ -5,7 +5,7 @@ export declare const ASTROID_VITALS_BINDING = "VITALS";
5
5
  * account don't write into the same table. */
6
6
  export declare function astroidVitalsDataset(config: AstroidConfig): string;
7
7
  /**
8
- * Credentials the CWV READ-BACK needs — not the collection.
8
+ * Credentials the CWV READ-BACK needs—not the collection.
9
9
  *
10
10
  * The Analytics Engine SQL API is account-scoped and has no binding, so a token
11
11
  * is unavoidable. Both are required together: an account id without a token (or
@@ -18,7 +18,7 @@ export declare const ASTROID_VITALS_SECRET_NAMES: readonly ["CF_ACCOUNT_ID", "CF
18
18
  *
19
19
  * A FILE rather than an inline script, and that is a CSP decision, not a style
20
20
  * one: Astro hashes the scripts it processes into `script-src`, and an
21
- * `is:inline` script carrying generated content cannot be hashed — it would be
21
+ * `is:inline` script carrying generated content cannot be hashed—it would be
22
22
  * blocked. Served from `public/`, it is same-origin and covered by
23
23
  * `script-src 'self'` with no policy change at all.
24
24
  */
@@ -29,7 +29,7 @@ export declare function generateAstroidVitalsBeacon(config: AstroidConfig, beaco
29
29
  /**
30
30
  * The health scan's CWV read-back, as source for the generated worker.
31
31
  *
32
- * Returns `undefined` — so the badge stays "not measured yet" — when the API
32
+ * Returns `undefined`—so the badge stays "not measured yet"—when the API
33
33
  * credentials aren't set, the query fails, or there is no field data yet. Never
34
34
  * throws: a failed vitals query must not take down the rest of the health scan,
35
35
  * which is mostly cheap COUNTs that have nothing to do with it.
@@ -1,25 +1,25 @@
1
1
  // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
2
  //
3
- // Real-visitor Core Web Vitals (#106 CWV) — the field-data half of the health
3
+ // Real-visitor Core Web Vitals (#106 CWV)—the field-data half of the health
4
4
  // co-pilot.
5
5
  //
6
6
  // `HealthSummary` has carried an optional `cwv` field since the health module
7
7
  // landed, and the Health panel renders a "not measured yet" badge when it's
8
8
  // absent. Astroid never populated it, so that badge was permanently accurate and
9
- // permanently useless — the same dead-UI shape as the missing overview route.
9
+ // permanently useless—the same dead-UI shape as the missing overview route.
10
10
  //
11
11
  // The loop has three parts and needs all three to close:
12
12
  //
13
- // 1. INGEST — a beacon in the page POSTs LCP/CLS/INP to `/api/louise/vitals`,
13
+ // 1. INGEST—a beacon in the page POSTs LCP/CLS/INP to `/api/louise/vitals`,
14
14
  // which writes a data point to an Analytics Engine dataset.
15
- // 2. STORE — the `analytics_engine_datasets` binding. Free, and the route
15
+ // 2. STORE—the `analytics_engine_datasets` binding. Free, and the route
16
16
  // degrades to accept-and-drop without it.
17
- // 3. READ BACK — the daily health scan queries the p75 over the last day via
17
+ // 3. READ BACK—the daily health scan queries the p75 over the last day via
18
18
  // the Analytics Engine SQL API and folds it into the summary.
19
19
  //
20
20
  // Step 3 is the one with credentials: the SQL API is account-scoped and needs an
21
21
  // API token, which no binding provides. So it follows the dormant-until-
22
- // provisioned convention — collection runs regardless, and the badge stays "not
22
+ // provisioned convention—collection runs regardless, and the badge stays "not
23
23
  // measured yet" until the pair is real, rather than the scan erroring.
24
24
  /** The Analytics Engine dataset binding name. */
25
25
  export const ASTROID_VITALS_BINDING = "VITALS";
@@ -29,7 +29,7 @@ export function astroidVitalsDataset(config) {
29
29
  return `${config.key.replace(/[^a-z0-9_]/gi, "_")}_web_vitals`;
30
30
  }
31
31
  /**
32
- * Credentials the CWV READ-BACK needs — not the collection.
32
+ * Credentials the CWV READ-BACK needs—not the collection.
33
33
  *
34
34
  * The Analytics Engine SQL API is account-scoped and has no binding, so a token
35
35
  * is unavoidable. Both are required together: an account id without a token (or
@@ -42,7 +42,7 @@ export const ASTROID_VITALS_SECRET_NAMES = ["CF_ACCOUNT_ID", "CF_API_TOKEN"];
42
42
  *
43
43
  * A FILE rather than an inline script, and that is a CSP decision, not a style
44
44
  * one: Astro hashes the scripts it processes into `script-src`, and an
45
- * `is:inline` script carrying generated content cannot be hashed — it would be
45
+ * `is:inline` script carrying generated content cannot be hashed—it would be
46
46
  * blocked. Served from `public/`, it is same-origin and covered by
47
47
  * `script-src 'self'` with no policy change at all.
48
48
  */
@@ -68,7 +68,7 @@ export function generateAstroidVitalsBeacon(config, beacon) {
68
68
  /**
69
69
  * The health scan's CWV read-back, as source for the generated worker.
70
70
  *
71
- * Returns `undefined` — so the badge stays "not measured yet" — when the API
71
+ * Returns `undefined`—so the badge stays "not measured yet"—when the API
72
72
  * credentials aren't set, the query fails, or there is no field data yet. Never
73
73
  * throws: a failed vitals query must not take down the rest of the health scan,
74
74
  * which is mostly cheap COUNTs that have nothing to do with it.
@@ -4,7 +4,7 @@ export { astroidCspOrigins } from "../security/csp-origins.js";
4
4
  export type CspHash = `${"sha256" | "sha384" | "sha512"}-${string}`;
5
5
  /**
6
6
  * The directives Astro lets a config own. `script-src` and `style-src` are
7
- * absent by design — Astro owns the first (it hashes what it processes) and the
7
+ * absent by design—Astro owns the first (it hashes what it processes) and the
8
8
  * middleware owns the second.
9
9
  *
10
10
  * Mirrored from Astro's own union rather than imported, so `astroidjs/astro`
@@ -14,7 +14,7 @@ export type CspHash = `${"sha256" | "sha384" | "sha512"}-${string}`;
14
14
  * silently producing a directive no browser enforces.
15
15
  */
16
16
  type CspDirectiveName = "base-uri" | "child-src" | "connect-src" | "default-src" | "fenced-frame-src" | "font-src" | "form-action" | "frame-ancestors" | "frame-src" | "img-src" | "manifest-src" | "media-src" | "object-src" | "referrer" | "report-to" | "report-uri" | "require-trusted-types-for" | "sandbox" | "trusted-types" | "upgrade-insecure-requests" | "worker-src";
17
- /** One rendered directive line, e.g. `"default-src 'self'"`. */
17
+ /** One rendered directive line, for example, `"default-src 'self'"`. */
18
18
  export type CspDirective = `${CspDirectiveName}${string}`;
19
19
  /** The `security` block for `astro.config.mjs`, structurally typed so Astroid
20
20
  * doesn't take a hard dependency on Astro's types. */
@@ -32,7 +32,7 @@ export interface AstroidSecurityConfig {
32
32
  * Hash of Solid's inline hydration bootstrap.
33
33
  *
34
34
  * `@astrojs/solid-js` injects this script on every page with an island, but
35
- * Astro's CSP tracker only hashes scripts it processed itself — so without this
35
+ * Astro's CSP tracker only hashes scripts it processed itself—so without this
36
36
  * the bootstrap is blocked under `script-src 'self'` and every island silently
37
37
  * fails to hydrate. Computed from `generateHydrationScript()` (the same call the
38
38
  * renderer makes), so a solid-js upgrade that changes the bootstrap updates the
@@ -40,7 +40,7 @@ export interface AstroidSecurityConfig {
40
40
  */
41
41
  export declare function solidHydrationHash(): CspHash;
42
42
  /**
43
- * The `security` block for `astro.config.mjs` — Astro's half of the split.
43
+ * The `security` block for `astro.config.mjs`—Astro's half of the split.
44
44
  *
45
45
  * `style-src` is deliberately absent: the generated middleware rewrites it per
46
46
  * response, and declaring it here would be the hash-vs-`'unsafe-inline'`
@@ -49,7 +49,7 @@ export declare function solidHydrationHash(): CspHash;
49
49
  export declare function astroidSecurity(config: AstroidConfig): AstroidSecurityConfig;
50
50
  /**
51
51
  * Vite build options the CSP depends on. `assetsInlineLimit: 0` stops Vite from
52
- * inlining small assets as `data:` URLs — an inlined script would be inline, and
52
+ * inlining small assets as `data:` URLs—an inlined script would be inline, and
53
53
  * therefore unhashed, and therefore blocked by `script-src 'self'`. Spread this
54
54
  * into `vite.build` rather than remembering why the number is zero.
55
55
  */
package/dist/astro/csp.js CHANGED
@@ -9,14 +9,14 @@
9
9
  // - **Astro owns `script-src`.** Its `security.csp` hashes every script it
10
10
  // processes, so the policy can be `'self'` with no `'unsafe-inline'`. What it
11
11
  // does NOT hash is Solid's hydration bootstrap, which `@astrojs/solid-js`
12
- // injects on every page carrying an island — Astro only tracks its own inline
12
+ // injects on every page carrying an island—Astro only tracks its own inline
13
13
  // scripts. So we compute that hash from the very function the renderer calls,
14
14
  // which means it follows solid-js upgrades instead of going stale as a
15
15
  // copy-pasted literal.
16
16
  // - **The middleware owns `style-src`.** Louise's data-driven `style=""`
17
17
  // carriers and the editor's runtime-injected `<style>` need
18
18
  // `'unsafe-inline'`, and per spec a single hash in `style-src` VOIDS
19
- // `'unsafe-inline'` — so the two cannot coexist in one directive. The
19
+ // `'unsafe-inline'`—so the two cannot coexist in one directive. The
20
20
  // generated middleware rewrites that one directive after the fact
21
21
  // (`cspStyleSrc`), leaving Astro's script hashes verbatim.
22
22
  //
@@ -33,7 +33,7 @@ export { astroidCspOrigins } from "../security/csp-origins.js";
33
33
  * Hash of Solid's inline hydration bootstrap.
34
34
  *
35
35
  * `@astrojs/solid-js` injects this script on every page with an island, but
36
- * Astro's CSP tracker only hashes scripts it processed itself — so without this
36
+ * Astro's CSP tracker only hashes scripts it processed itself—so without this
37
37
  * the bootstrap is blocked under `script-src 'self'` and every island silently
38
38
  * fails to hydrate. Computed from `generateHydrationScript()` (the same call the
39
39
  * renderer makes), so a solid-js upgrade that changes the bootstrap updates the
@@ -45,7 +45,7 @@ export function solidHydrationHash() {
45
45
  }
46
46
  /**
47
47
  * Render one directive. The name is a literal from the union above, so the
48
- * concatenation is a valid `CspDirective` by construction — which is what the
48
+ * concatenation is a valid `CspDirective` by construction—which is what the
49
49
  * assertion is standing in for (TS widens template concatenation to `string`).
50
50
  */
51
51
  function directive(name, ...sources) {
@@ -53,7 +53,7 @@ function directive(name, ...sources) {
53
53
  return (list ? `${name} ${list}` : name);
54
54
  }
55
55
  /**
56
- * The `security` block for `astro.config.mjs` — Astro's half of the split.
56
+ * The `security` block for `astro.config.mjs`—Astro's half of the split.
57
57
  *
58
58
  * `style-src` is deliberately absent: the generated middleware rewrites it per
59
59
  * response, and declaring it here would be the hash-vs-`'unsafe-inline'`
@@ -93,7 +93,7 @@ export function astroidSecurity(config) {
93
93
  }
94
94
  /**
95
95
  * Vite build options the CSP depends on. `assetsInlineLimit: 0` stops Vite from
96
- * inlining small assets as `data:` URLs — an inlined script would be inline, and
96
+ * inlining small assets as `data:` URLs—an inlined script would be inline, and
97
97
  * therefore unhashed, and therefore blocked by `script-src 'self'`. Spread this
98
98
  * into `vite.build` rather than remembering why the number is zero.
99
99
  */