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.
- package/README.md +42 -42
- package/bin/astroid.mjs +35 -29
- package/dist/analytics/index.d.ts +3 -3
- package/dist/analytics/index.js +11 -11
- package/dist/astro/csp.d.ts +5 -5
- package/dist/astro/csp.js +6 -6
- package/dist/astro/index.js +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.js +5 -5
- package/dist/commerce/adapters.d.ts +6 -6
- package/dist/commerce/adapters.js +9 -9
- package/dist/commerce/checkout-scaffold.d.ts +5 -5
- package/dist/commerce/checkout-scaffold.js +27 -27
- package/dist/commerce/checkout.d.ts +12 -12
- package/dist/commerce/checkout.js +7 -7
- package/dist/commerce/loader.d.ts +2 -2
- package/dist/commerce/loader.js +3 -3
- package/dist/commerce/mirror.d.ts +4 -4
- package/dist/commerce/mirror.js +12 -12
- package/dist/commerce/roles.d.ts +10 -10
- package/dist/commerce/roles.js +13 -13
- package/dist/commerce/secrets.d.ts +9 -9
- package/dist/commerce/secrets.js +9 -9
- package/dist/commerce/sync.d.ts +7 -7
- package/dist/commerce/sync.js +5 -5
- package/dist/components/sections.d.ts +9 -9
- package/dist/components/sections.js +12 -12
- package/dist/config.d.ts +89 -62
- package/dist/config.js +18 -18
- package/dist/email/inquiry.d.ts +2 -2
- package/dist/email/inquiry.js +1 -1
- package/dist/email/send.d.ts +4 -4
- package/dist/email/send.js +7 -7
- package/dist/email/templates.js +3 -3
- package/dist/email/theme.d.ts +1 -1
- package/dist/email/theme.js +4 -4
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.js +1 -1
- package/dist/map/pmtiles.d.ts +5 -5
- package/dist/map/pmtiles.js +5 -5
- package/dist/map/scaffold.d.ts +2 -2
- package/dist/map/scaffold.js +8 -8
- package/dist/map/style.d.ts +4 -4
- package/dist/map/style.js +1 -1
- package/dist/portal/config.d.ts +3 -3
- package/dist/portal/config.js +11 -10
- package/dist/portal/guard.d.ts +4 -4
- package/dist/portal/guard.js +4 -4
- package/dist/portal/nav.js +2 -2
- package/dist/portal/scaffold.d.ts +4 -4
- package/dist/portal/scaffold.js +13 -13
- package/dist/portal/session.d.ts +11 -5
- package/dist/portal/session.js +10 -5
- package/dist/portfolio/scaffold.d.ts +1 -1
- package/dist/portfolio/scaffold.js +8 -8
- package/dist/project/actions.d.ts +1 -1
- package/dist/project/actions.js +18 -12
- package/dist/project/generate.d.ts +4 -4
- package/dist/project/generate.js +26 -26
- package/dist/project/index.d.ts +1 -0
- package/dist/project/index.js +2 -1
- package/dist/project/scaffold.d.ts +2 -2
- package/dist/project/scaffold.js +69 -13
- package/dist/project/seed.d.ts +22 -0
- package/dist/project/seed.js +214 -0
- package/dist/pwa/generate.d.ts +11 -11
- package/dist/pwa/generate.js +19 -19
- package/dist/queues/consumer.d.ts +3 -3
- package/dist/queues/consumer.js +2 -2
- package/dist/queues/messages.d.ts +4 -4
- package/dist/queues/messages.js +2 -2
- package/dist/queues/scaffold.d.ts +4 -4
- package/dist/queues/scaffold.js +17 -17
- package/dist/queues/webhook.d.ts +5 -5
- package/dist/queues/webhook.js +3 -3
- package/dist/realtime/scaffold.d.ts +4 -4
- package/dist/realtime/scaffold.js +12 -12
- package/dist/schema/collections.d.ts +42 -11
- package/dist/schema/collections.js +43 -42
- package/dist/schema/framework.d.ts +1 -1
- package/dist/schema/framework.js +2 -2
- package/dist/schema/generate.js +6 -6
- package/dist/schema/index.js +1 -1
- package/dist/secrets.d.ts +6 -6
- package/dist/secrets.js +6 -6
- package/dist/security/csp-origins.d.ts +1 -1
- package/dist/security/csp-origins.js +3 -3
- package/dist/security/rate-rules.d.ts +2 -2
- package/dist/security/rate-rules.js +8 -8
- package/dist/seo/resolve.d.ts +5 -5
- package/dist/seo/resolve.js +2 -2
- package/dist/seo/routes.d.ts +5 -5
- package/dist/seo/routes.js +3 -3
- package/dist/seo/structured-data.d.ts +6 -6
- package/dist/seo/structured-data.js +7 -7
- package/dist/status.d.ts +5 -5
- package/dist/status.js +7 -7
- package/dist/tenancy/index.d.ts +3 -3
- package/dist/tenancy/index.js +11 -11
- package/dist/worker/generate.d.ts +2 -2
- package/dist/worker/generate.js +91 -57
- package/dist/worker/index.js +1 -1
- package/dist/worker/routes.js +11 -11
- package/dist/workflow/advance.d.ts +3 -3
- package/dist/workflow/advance.js +6 -6
- package/dist/workflow/config.d.ts +4 -4
- package/dist/workflow/config.js +4 -4
- package/dist/workflow/generate.d.ts +2 -2
- package/dist/workflow/generate.js +11 -11
- package/package.json +3 -4
- package/src/components/Collection.tsx +5 -5
- package/src/components/Editable.astro +9 -9
- package/src/components/JustifiedGallery.astro +8 -8
- package/src/components/MediaSlot.astro +12 -12
- package/src/components/PortalShell.astro +4 -4
- package/src/components/RegisterSW.astro +3 -3
- package/src/components/Section.astro +8 -8
- package/src/components/Sections.astro +6 -6
- package/src/components/Seo.astro +3 -3
- package/src/components/StageBar.astro +3 -3
- package/src/components/StructuredData.astro +2 -2
- package/src/components/justify.ts +9 -9
- package/src/components/media-meta.ts +10 -10
- package/src/components/sections/AboutIntro.astro +1 -1
- package/src/components/sections/Contact.astro +1 -1
- package/src/components/sections/Cta.astro +1 -1
- package/src/components/sections/Faq.astro +1 -1
- package/src/components/sections/FeatureGrid.astro +2 -2
- package/src/components/sections/Hero.astro +1 -1
- package/src/components/sections/PricingTiers.astro +1 -1
- package/src/components/sections/ProductGrid.astro +1 -1
- package/src/components/sections/SplitImage.astro +1 -1
- package/src/components/sections/Steps.astro +1 -1
- package/src/components/sections/Testimonial.astro +1 -1
- package/src/components/sections.ts +17 -17
package/README.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# astroidjs
|
|
2
2
|
|
|
3
|
-
**Astroid
|
|
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
|
|
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
|
|
33
|
-
backend and optional modules
|
|
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)
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
166
|
-
owner, confirm to the sender)
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
238
|
-
hashes every script it processes, so the policy needs no `'unsafe-inline'
|
|
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
|
|
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
|
-
|
|
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
|
|
272
|
-
is absent, empty, or bound to an unprovisioned store
|
|
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
|
|
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
|
|
303
|
-
for the test key, the same test `activeCaptchaSecret` applies
|
|
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
|
|
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
|
|
338
|
-
Astro app
|
|
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`)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
232
|
-
// health scan alongside the hourly catalog sync) can't update it
|
|
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.
|
|
@@ -264,18 +264,24 @@ async function cmdDoctor(cwd, flags) {
|
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
// 3. migrations directory
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
267
|
+
// 3. migrations directory: the D1 `migrations_dir` wrangler.jsonc declares,
|
|
268
|
+
// or `migrations/`, the generated default, when it names none. A site that
|
|
269
|
+
// keeps its migrations under another name (drizzle/) isn't missing them.
|
|
270
|
+
const wranglerText = existsSync(wranglerPath) ? readFileSync(wranglerPath, "utf8") : "";
|
|
271
|
+
const migrationsDir =
|
|
272
|
+
wranglerText.match(/"migrations_dir"\s*:\s*"([^"]+)"/)?.[1]?.replace(/\/+$/, "") ??
|
|
273
|
+
"migrations";
|
|
274
|
+
if (existsSync(join(cwd, migrationsDir))) ok(`${migrationsDir}/ directory present`);
|
|
275
|
+
else warn(`no ${migrationsDir}/ directory — create your D1 schema migrations there.`);
|
|
276
|
+
|
|
277
|
+
// 4. Local secret provisioning—which modules will run dormant under
|
|
272
278
|
// `astroid dev`, and what to set to wake them.
|
|
273
279
|
//
|
|
274
280
|
// Scoped deliberately to SECRETS, read from .dev.vars. Doctor is a static
|
|
275
281
|
// CLI: it cannot see runtime bindings (EMAIL, the queue), so claiming
|
|
276
282
|
// "email is dormant" here would be reporting its own blindness as the
|
|
277
283
|
// project's state. What it CAN say for certain is whether a value is still
|
|
278
|
-
// the placeholder
|
|
284
|
+
// the placeholder—and that is the half developers actually forget.
|
|
279
285
|
//
|
|
280
286
|
// A dormant module is never an error. It is the documented default, and a
|
|
281
287
|
// fresh scaffold is expected to report every module dormant.
|
|
@@ -295,7 +301,7 @@ async function cmdDoctor(cwd, flags) {
|
|
|
295
301
|
const value = (devVars?.[name] ?? "").trim();
|
|
296
302
|
return value === "" || value === ASTROID_SECRET_PLACEHOLDER;
|
|
297
303
|
});
|
|
298
|
-
// `core` is not a module
|
|
304
|
+
// `core` is not a module—its secrets each gate their own thing (sessions
|
|
299
305
|
// fail closed off localhost, Turnstile needs a matched pair), so a blanket
|
|
300
306
|
// "dormant" line would be wrong. Report it as provisioning, not dormancy.
|
|
301
307
|
if (unset.length === 0) ok(`${moduleName}: all secrets provisioned in .dev.vars`);
|
|
@@ -337,7 +343,7 @@ async function cmdAstro(cwd, subcommand, flags, rest) {
|
|
|
337
343
|
}
|
|
338
344
|
|
|
339
345
|
/** Resolve a project-local CLI bin (astro, wrangler) to an absolute path via the
|
|
340
|
-
* project's own dependency resolution
|
|
346
|
+
* project's own dependency resolution—so we run the version it ships. */
|
|
341
347
|
function resolveBin(cwd, pkgName, binName) {
|
|
342
348
|
try {
|
|
343
349
|
const require = createRequire(join(cwd, "package.json"));
|
|
@@ -353,14 +359,14 @@ function resolveBin(cwd, pkgName, binName) {
|
|
|
353
359
|
// --- deploy ----------------------------------------------------------------
|
|
354
360
|
// `astroid deploy` orchestrates the one-time platform bring-up: provision the
|
|
355
361
|
// bindings that still hold placeholder ids (D1/R2/KV), apply migrations, prompt
|
|
356
|
-
// for secrets, and deploy
|
|
362
|
+
// for secrets, and deploy—all by shelling out to the project's own `wrangler`.
|
|
357
363
|
// It's plan-first: it prints exactly what it will run, and only proceeds past the
|
|
358
364
|
// irreversible steps on an interactive `y` (or `--yes`). `--dry-run` prints the
|
|
359
365
|
// plan and stops; `--local` targets the local D1 for migrations.
|
|
360
366
|
|
|
361
367
|
const isPlaceholder = (v) => !v || /^<.*>$/.test(v);
|
|
362
368
|
|
|
363
|
-
/** Pull the deploy-relevant bits out of wrangler.jsonc by regex
|
|
369
|
+
/** Pull the deploy-relevant bits out of wrangler.jsonc by regex—robust against
|
|
364
370
|
* its JSONC comments + trailing commas (a strict JSON.parse would throw). */
|
|
365
371
|
function readWranglerFacts(text) {
|
|
366
372
|
return {
|
|
@@ -373,8 +379,8 @@ function readWranglerFacts(text) {
|
|
|
373
379
|
id: m[2],
|
|
374
380
|
})),
|
|
375
381
|
// 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
|
-
//
|
|
382
|
+
// D1/R2/KV these are referenced BY NAME, so there's no id to patch back—creating
|
|
383
|
+
// them is the whole job.
|
|
378
384
|
queues: [
|
|
379
385
|
...new Set([
|
|
380
386
|
...[...text.matchAll(/"queue":\s*"([^"]+)"/g)].map((m) => m[1]),
|
|
@@ -400,7 +406,7 @@ function provisionPlan(facts) {
|
|
|
400
406
|
steps.push({ kind: "kv", name: binding, args: ["kv", "namespace", "create", binding] });
|
|
401
407
|
}
|
|
402
408
|
}
|
|
403
|
-
// Queues carry no id, so there's no placeholder to test
|
|
409
|
+
// Queues carry no id, so there's no placeholder to test—creating one that
|
|
404
410
|
// already exists just errors, which the runner tolerates (same as R2).
|
|
405
411
|
for (const queue of facts.queues) {
|
|
406
412
|
steps.push({ kind: "queue", name: queue, args: ["queues", "create", queue] });
|
|
@@ -437,9 +443,9 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
437
443
|
const wranglerPath = join(cwd, "wrangler.jsonc");
|
|
438
444
|
if (!existsSync(wranglerPath)) fail("wrangler.jsonc not found — run inside an Astroid project.");
|
|
439
445
|
|
|
440
|
-
// Regenerate so the shipped worker/schema always match the config
|
|
446
|
+
// Regenerate so the shipped worker/schema always match the config—but NOT
|
|
441
447
|
// on a dry run. This used to sit above the `--dry-run` guard, so a command
|
|
442
|
-
// that ends by printing "(dry run
|
|
448
|
+
// that ends by printing "(dry run—nothing executed)" had already rewritten
|
|
443
449
|
// three files and silently discarded any local edits to them. Probing the
|
|
444
450
|
// plan on a working branch is exactly when someone has local edits.
|
|
445
451
|
if (!dryRun) await cmdGenerate(cwd, flags, { quiet: true });
|
|
@@ -494,8 +500,8 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
494
500
|
for (const s of plan) {
|
|
495
501
|
out(`\n▸ wrangler ${s.args.join(" ")}`);
|
|
496
502
|
const res = runInherit(s.args);
|
|
497
|
-
// R2 bucket + queue creates are idempotent-ish (an existing one errors)
|
|
498
|
-
//
|
|
503
|
+
// R2 bucket + queue creates are idempotent-ish (an existing one errors)—tolerate
|
|
504
|
+
// those so a re-run of `astroid deploy` isn't a hard stop.
|
|
499
505
|
if (res.status !== 0 && s.kind !== "r2" && s.kind !== "queue") {
|
|
500
506
|
fail(`Provisioning failed at: wrangler ${s.args.join(" ")}`);
|
|
501
507
|
}
|
|
@@ -551,7 +557,7 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
551
557
|
|
|
552
558
|
// --- helpers ---------------------------------------------------------------
|
|
553
559
|
/**
|
|
554
|
-
* Minimal KEY=VALUE reader for .dev.vars. Not a dotenv implementation
|
|
560
|
+
* Minimal KEY=VALUE reader for .dev.vars. Not a dotenv implementation—doctor
|
|
555
561
|
* only needs to know whether a value is absent, empty, or the placeholder, so
|
|
556
562
|
* expansion, multiline values, and `export` prefixes are out of scope. Quotes
|
|
557
563
|
* 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
|
|
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
|
|
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
|
|
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.
|
package/dist/analytics/index.js
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
2
|
//
|
|
3
|
-
// Real-visitor Core Web Vitals (#106 CWV)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
*/
|
|
@@ -51,14 +51,14 @@ export function generateAstroidVitalsBeacon(config, beacon) {
|
|
|
51
51
|
return {
|
|
52
52
|
path: "public/vitals.js",
|
|
53
53
|
contents: [
|
|
54
|
-
"// Real-visitor Core Web Vitals beacon
|
|
54
|
+
"// Real-visitor Core Web Vitals beacon—generated by astroidjs.",
|
|
55
55
|
"//",
|
|
56
56
|
"// A static file, not an inline script: Astro hashes processed scripts into",
|
|
57
57
|
"// script-src, and an is:inline script with generated content can't be",
|
|
58
58
|
"// hashed, so it would be CSP-blocked. From public/ it is same-origin and",
|
|
59
59
|
"// already covered by script-src 'self'.",
|
|
60
60
|
"//",
|
|
61
|
-
"// It reports once per page, on visibilitychange → hidden, via sendBeacon
|
|
61
|
+
"// It reports once per page, on visibilitychange → hidden, via sendBeacon—",
|
|
62
62
|
"// so it costs nothing on the critical path and never blocks unload.",
|
|
63
63
|
beacon,
|
|
64
64
|
"",
|
|
@@ -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
|
|
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.
|
package/dist/astro/csp.d.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
*/
|