astroidjs 0.19.0 → 0.21.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/bin/astroid.mjs +32 -8
- package/dist/commerce/secrets.d.ts +7 -0
- package/dist/commerce/secrets.js +12 -3
- package/dist/config.d.ts +74 -2
- package/dist/config.js +84 -1
- package/dist/incidents/index.d.ts +2 -0
- package/dist/incidents/index.js +12 -0
- package/dist/incidents/names.d.ts +17 -0
- package/dist/incidents/names.js +55 -0
- package/dist/incidents/sentry.d.ts +20 -0
- package/dist/incidents/sentry.js +118 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/pages/index.d.ts +19 -0
- package/dist/pages/index.js +34 -0
- package/dist/project/generate.js +109 -52
- package/dist/project/scaffold.js +41 -16
- package/dist/queues/index.d.ts +1 -1
- package/dist/queues/index.js +1 -1
- package/dist/queues/messages.d.ts +6 -0
- package/dist/queues/messages.js +17 -2
- package/dist/queues/scaffold.js +5 -1
- package/dist/schema/generate.js +67 -4
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/rate-rules.d.ts +9 -2
- package/dist/security/rate-rules.js +46 -22
- package/dist/seo/routes.js +3 -2
- package/dist/shape.d.ts +6 -0
- package/dist/shape.js +14 -0
- package/dist/status.js +22 -10
- package/dist/worker/generate.js +232 -18
- package/package.json +10 -3
- package/src/components/Credit.astro +70 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// `astroidjs/pages`—the editor's work-in-progress on a page, for a page route
|
|
4
|
+
// in edit mode. A subpath of its own because it reads through louise-toolkit's
|
|
5
|
+
// editor and Drizzle, which the main entry (loaded by astroid.config.ts and the
|
|
6
|
+
// CLI) stays free of.
|
|
7
|
+
//
|
|
8
|
+
// The generated worker saves pages through the DRAFTS buffer, keyed by the pages
|
|
9
|
+
// collection's slug, and flushes to `pages_versions` in D1 behind it. A page
|
|
10
|
+
// route has to read the same way, buffer first, or an editor who reloads before
|
|
11
|
+
// the flush sees an older page than the one they just saved. Every site that
|
|
12
|
+
// read D1 alone hit that, and each wrapped louise-toolkit's `resumeDraft` in the
|
|
13
|
+
// same few lines to fix it. This is those lines, with the slug and the versions
|
|
14
|
+
// table derived from the config rather than restated.
|
|
15
|
+
import { collectionVersionsTable } from "louise-toolkit/content";
|
|
16
|
+
import { resumeDraft } from "louise-toolkit/editor";
|
|
17
|
+
import { astroidPagesCollection } from "../schema/collections.js";
|
|
18
|
+
/**
|
|
19
|
+
* The editor's work-in-progress snapshot of the page with id `pageId`, or
|
|
20
|
+
* `null` when there's none (render the live row). The buffer comes first, then
|
|
21
|
+
* the newest pending draft in D1, the same order a save builds on.
|
|
22
|
+
*
|
|
23
|
+
* Returns the whole snapshot: which fields a page renders from it (`sections`,
|
|
24
|
+
* `body`, `title`) is the site's call. Call it only in edit mode; a visitor
|
|
25
|
+
* sees the live row.
|
|
26
|
+
*/
|
|
27
|
+
export function astroidPageDraft(config, env, pageId) {
|
|
28
|
+
const collection = astroidPagesCollection(config);
|
|
29
|
+
return resumeDraft(env.DB, {
|
|
30
|
+
versionsTable: collectionVersionsTable(collection),
|
|
31
|
+
collection: collection.slug,
|
|
32
|
+
bufferKv: env.DRAFTS,
|
|
33
|
+
}, { id: pageId });
|
|
34
|
+
}
|
package/dist/project/generate.js
CHANGED
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
import { ASTROID_VITALS_BINDING, astroidVitalsDataset } from "../analytics/index.js";
|
|
18
18
|
import { astroidCheckoutVars } from "../commerce/checkout-scaffold.js";
|
|
19
19
|
import { astroidCommerceProviders } from "../commerce/roles.js";
|
|
20
|
-
import { COMMERCE_PROVIDER_SECRETS, COMMERCE_PROVIDER_SETUP, commerceSecretNames, } from "../commerce/secrets.js";
|
|
20
|
+
import { COMMERCE_PROVIDER_SECRETS, COMMERCE_PROVIDER_SETUP, commerceProviderWebhookSecrets, commerceSecretNames, } from "../commerce/secrets.js";
|
|
21
|
+
import { ASTROID_INCIDENT_EVENTS_BINDING, ASTROID_VERSION_METADATA_BINDING, astroidIncidentEventsDataset, } from "../incidents/names.js";
|
|
21
22
|
import { ASTROID_QUEUE_BINDING, ASTROID_QUEUE_RETRY_DELAY, astroidCrons, astroidQueueNames, astroidUsesQueues, } from "../queues/messages.js";
|
|
22
23
|
import { ASTROID_EDIT_SESSION_CLASS, ASTROID_REALTIME_BINDING, ASTROID_REALTIME_MIGRATION_TAG, usesRealtime, } from "../realtime/scaffold.js";
|
|
23
24
|
import { ASTROID_SECRET_PLACEHOLDER } from "../secrets.js";
|
|
25
|
+
import { astroidHasEditor } from "../shape.js";
|
|
24
26
|
import { tenancyZone } from "../tenancy/index.js";
|
|
25
27
|
import { generateAstroidSchema } from "../schema/generate.js";
|
|
26
28
|
import { ASTROID_AI_GATEWAY_VAR } from "../worker/gateway.js";
|
|
@@ -93,7 +95,10 @@ export function generateAstroidSecretsEnv(config) {
|
|
|
93
95
|
for (const provider of providers) {
|
|
94
96
|
const spec = COMMERCE_PROVIDER_SECRETS[provider];
|
|
95
97
|
lines.push("#", `# ${provider}: ${COMMERCE_PROVIDER_SETUP[provider]}`);
|
|
96
|
-
for (const name of [
|
|
98
|
+
for (const name of [
|
|
99
|
+
...spec.credentials,
|
|
100
|
+
...commerceProviderWebhookSecrets(provider, config.commerce),
|
|
101
|
+
]) {
|
|
97
102
|
lines.push(`${name}=${ASTROID_SECRET_PLACEHOLDER}`);
|
|
98
103
|
}
|
|
99
104
|
}
|
|
@@ -116,6 +121,12 @@ const COMPATIBILITY_DATE = "2026-06-20";
|
|
|
116
121
|
*/
|
|
117
122
|
export function generateAstroidWrangler(config) {
|
|
118
123
|
const key = config.key;
|
|
124
|
+
// An app with no editor binds none of what the editor uses: the media bucket
|
|
125
|
+
// and Images, the draft buffer, Workers AI, and the CWV dataset. It sends mail
|
|
126
|
+
// only for a portal's password resets.
|
|
127
|
+
const editor = astroidHasEditor(config);
|
|
128
|
+
const mail = editor || Boolean(config.portal?.enabled);
|
|
129
|
+
const crons = astroidCrons(config);
|
|
119
130
|
const mediaBase = config.deploy?.mediaBase ?? "/media";
|
|
120
131
|
const hosts = config.hosts ?? [];
|
|
121
132
|
const primaryHost = hosts[0];
|
|
@@ -184,7 +195,10 @@ export function generateAstroidWrangler(config) {
|
|
|
184
195
|
// Daily: the site-health scan (broken links, missing alt text, SEO gaps).
|
|
185
196
|
// Hourly (commerce only): the catalog re-sync safety net, so a missed or DLQ'd
|
|
186
197
|
// webhook can only leave the site stale until the next tick.
|
|
187
|
-
|
|
198
|
+
// None at all for an app with nothing scheduled: a trigger with no handler
|
|
199
|
+
// is an invocation that fails every time it fires.
|
|
200
|
+
if (crons.length > 0)
|
|
201
|
+
p(` "triggers": { "crons": ${JSON.stringify(crons)} },`);
|
|
188
202
|
if (astroidUsesQueues(config)) {
|
|
189
203
|
const { queue, dlq } = astroidQueueNames(config);
|
|
190
204
|
p(" // Provider webhooks are verified at the edge, then enqueued here so the");
|
|
@@ -205,44 +219,74 @@ export function generateAstroidWrangler(config) {
|
|
|
205
219
|
p(` "retry_delay": ${config.queues?.retryDelay ?? ASTROID_QUEUE_RETRY_DELAY},`);
|
|
206
220
|
p(` "dead_letter_queue": ${JSON.stringify(dlq)},`);
|
|
207
221
|
p(" },");
|
|
222
|
+
p(" // The dead-letter queue's own consumer: the worker keeps each message");
|
|
223
|
+
p(" // in the `dead_letters` table and counts it as an incident, so a");
|
|
224
|
+
p(" // failed event is never lost unseen. No retries of its own.");
|
|
225
|
+
p(` { "queue": ${JSON.stringify(dlq)}, "max_batch_size": 10, "max_retries": 0 },`);
|
|
208
226
|
p(" ],");
|
|
209
227
|
p(" },");
|
|
210
228
|
}
|
|
211
|
-
|
|
212
|
-
|
|
229
|
+
if (editor) {
|
|
230
|
+
p(" // D1 holds pages / site_settings / media / inquiries (schema in src/schema.ts,");
|
|
231
|
+
p(" // migrations in ./migrations). Create it: `wrangler d1 create <name>`.");
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
p(" // D1: this app's tables (src/schema.ts), or the database of the app that");
|
|
235
|
+
p(" // owns the schema, bound by its id. Create one: `wrangler d1 create <name>`.");
|
|
236
|
+
}
|
|
237
|
+
p(" // The deployed version's ID, which each incident records as its release.");
|
|
238
|
+
p(` "version_metadata": { "binding": ${JSON.stringify(ASTROID_VERSION_METADATA_BINDING)} },`);
|
|
239
|
+
if (!editor) {
|
|
240
|
+
// The editor shape lists this dataset beside the Core Web Vitals one below.
|
|
241
|
+
p(" // Analytics Engine: incident counts over time (louise-toolkit ADR 0022).");
|
|
242
|
+
p(` "analytics_engine_datasets": [{ "binding": ${JSON.stringify(ASTROID_INCIDENT_EVENTS_BINDING)}, "dataset": ${JSON.stringify(astroidIncidentEventsDataset(config))} }],`);
|
|
243
|
+
}
|
|
213
244
|
p(' "d1_databases": [');
|
|
214
245
|
p(" {");
|
|
215
246
|
p(' "binding": "DB",');
|
|
216
247
|
p(` "database_name": ${JSON.stringify(key)},`);
|
|
217
248
|
p(' "database_id": "<run: wrangler d1 create ' + key + '>",');
|
|
218
|
-
|
|
249
|
+
// Left out when another app migrates this database (`deploy.migrations:
|
|
250
|
+
// false`), which `astroid doctor` would otherwise report as a contradiction.
|
|
251
|
+
if (config.deploy?.migrations !== false)
|
|
252
|
+
p(' "migrations_dir": "migrations",');
|
|
219
253
|
p(" },");
|
|
220
254
|
p(" ],");
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
255
|
+
if (editor) {
|
|
256
|
+
p(" // R2 bucket for uploaded media, streamed back through the Worker at MEDIA_URL");
|
|
257
|
+
p(" // (no public bucket). Create it: `wrangler r2 bucket create <name>-media`.");
|
|
258
|
+
p(` "r2_buckets": [{ "binding": "MEDIA", "bucket_name": ${JSON.stringify(`${key}-media`)} }],`);
|
|
259
|
+
p(" // Cloudflare Images: the media route reads upload dimensions + backs server-");
|
|
260
|
+
p(" // side re-encode. Also @astrojs/cloudflare's production image service.");
|
|
261
|
+
p(' "images": { "binding": "IMAGES" },');
|
|
262
|
+
p(" // Analytics Engine: real-visitor Core Web Vitals. Free, and the ingest");
|
|
263
|
+
p(" // route accepts-and-drops without it, so it costs nothing unused. Reading");
|
|
264
|
+
p(" // the p75 back out needs CF_ACCOUNT_ID + CF_API_TOKEN (see .env.example)—");
|
|
265
|
+
p(" // until those are real the Health badge reads 'not measured yet'. The");
|
|
266
|
+
p(" // second dataset counts incidents over time (louise-toolkit ADR 0022).");
|
|
267
|
+
p(` "analytics_engine_datasets": [{ "binding": ${JSON.stringify(ASTROID_VITALS_BINDING)}, "dataset": ${JSON.stringify(astroidVitalsDataset(config))} }, { "binding": ${JSON.stringify(ASTROID_INCIDENT_EVENTS_BINDING)}, "dataset": ${JSON.stringify(astroidIncidentEventsDataset(config))} }],`);
|
|
268
|
+
p(" // Workers AI. Powers the editor's rewrite + SEO-suggest buttons and alt-text");
|
|
269
|
+
p(" // generation on upload—all of which SHIP IN THE EDITOR DRAWER already and,");
|
|
270
|
+
p(" // without this binding, were permanently invisible: their routes answer 503");
|
|
271
|
+
p(" // and the client hides the button. No account setup beyond the binding, and");
|
|
272
|
+
p(" // every call is editor-gated, so a visitor can never spend your AI budget.");
|
|
273
|
+
p(' "ai": { "binding": "AI" },');
|
|
274
|
+
p(" // KV: RL = the security rate limiter (it also holds the daily site-health");
|
|
275
|
+
p(" // summary under its own key—one small singleton blob, not worth a binding");
|
|
276
|
+
p(" // someone has to remember to provision); DRAFTS = the autosave write-buffer.");
|
|
277
|
+
p(" // Named for the project, so two sites in one account don't collide.");
|
|
278
|
+
p(" // `astroid provision` creates each and fills in its id.");
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
p(" // KV: RL = the security rate limiter. Named for the project, so two apps in");
|
|
282
|
+
p(" // one account don't collide. `astroid provision` creates it and fills in");
|
|
283
|
+
p(" // its id.");
|
|
284
|
+
}
|
|
243
285
|
p(' "kv_namespaces": [');
|
|
244
286
|
p(` { "binding": "RL", "id": "<run: wrangler kv namespace create ${key}-rl>" },`);
|
|
245
|
-
|
|
287
|
+
if (editor) {
|
|
288
|
+
p(` { "binding": "DRAFTS", "id": "<run: wrangler kv namespace create ${key}-drafts>" },`);
|
|
289
|
+
}
|
|
246
290
|
p(" ],");
|
|
247
291
|
// Email Sending. NOT optional decoration: `src/env.d.ts` declares EMAIL as a
|
|
248
292
|
// required member, and Better Auth's magic-link path console-logs the link in
|
|
@@ -251,31 +295,44 @@ export function generateAstroidWrangler(config) {
|
|
|
251
295
|
// sign-in was impossible on every DEPLOYED site, while every local build
|
|
252
296
|
// and every CI scaffold passed. Nothing in this repo runs a deployed scaffold,
|
|
253
297
|
// which is why it survived.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
298
|
+
if (editor) {
|
|
299
|
+
p(" // Cloudflare Email Sending—magic-link sign-in + inquiry notifications.");
|
|
300
|
+
p(" // Sign-in DEPENDS on this: in production the magic link is emailed, not logged.");
|
|
301
|
+
}
|
|
302
|
+
else if (mail) {
|
|
303
|
+
p(" // Cloudflare Email Sending—the portal's password-reset mail, which is");
|
|
304
|
+
p(" // logged in dev and emailed in production.");
|
|
305
|
+
}
|
|
306
|
+
if (mail) {
|
|
307
|
+
p(" // Enable Email Sending for your zone, then verify the address in MAIL_FROM.");
|
|
308
|
+
p(' "send_email": [{ "name": "EMAIL" }],');
|
|
309
|
+
}
|
|
310
|
+
if (editor) {
|
|
311
|
+
p(" // Public base for media URLs; same-origin keeps media self-contained. Read off");
|
|
312
|
+
p(" // the runtime env by the framework-agnostic media route, so it stays a `var`.");
|
|
313
|
+
}
|
|
260
314
|
p(' "vars": {');
|
|
261
|
-
|
|
315
|
+
if (editor)
|
|
316
|
+
p(` "MEDIA_URL": ${JSON.stringify(mediaBase)},`);
|
|
262
317
|
p(` "SITE_URL": ${JSON.stringify(primaryHost ? `https://${primaryHost}` : `https://${key}.workers.dev`)},`);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
318
|
+
if (editor) {
|
|
319
|
+
p(" // The editor allowlist / owner. Wire this into your auth seam (src/auth.ts).");
|
|
320
|
+
p(' "OWNER_EMAIL": "",');
|
|
321
|
+
p(" // AI Gateway for the editor's AI assists: request logs, latency and error");
|
|
322
|
+
p(" // rates, and caching. Empty calls Workers AI directly. Create a gateway,");
|
|
323
|
+
p(" // put its id here, and first say on the privacy page that its log holds");
|
|
324
|
+
p(" // the text editors send to the assists.");
|
|
325
|
+
p(` "${ASTROID_AI_GATEWAY_VAR}": "",`);
|
|
326
|
+
p(" // Edge caching for published pages (ADR 0004). OFF by default, and the");
|
|
327
|
+
p(" // default is the safe state: with it off every render is `no-store` and");
|
|
328
|
+
p(" // the Worker cache layer stores nothing.");
|
|
329
|
+
p(" //");
|
|
330
|
+
p(" // Turn it on for a PREVIEW deploy first and walk the activation runbook");
|
|
331
|
+
p(" // (docs/adr/0004-edge-caching.md). `caches.default` is not cleared by");
|
|
332
|
+
p(" // Cloudflare Dev Mode or Purge Everything, so a bad prod flip is hard to");
|
|
333
|
+
p(" // undo—this feature was reverted twice for exactly that.");
|
|
334
|
+
p(' "ASTROID_EDGE_CACHE": "false",');
|
|
335
|
+
}
|
|
279
336
|
for (const v of astroidCheckoutVars(config)) {
|
|
280
337
|
// Public, not secret—the app id ships to the browser to mount the card
|
|
281
338
|
// field, and the environment is a choice. Keeping them out of the secret
|
package/dist/project/scaffold.js
CHANGED
|
@@ -26,11 +26,13 @@
|
|
|
26
26
|
import { generateAstroidCheckoutRoute, generateAstroidSquareCard, } from "../commerce/checkout-scaffold.js";
|
|
27
27
|
import { generateCatalogMigrationSql } from "../commerce/mirror.js";
|
|
28
28
|
import { generateAstroidVitalsBeacon } from "../analytics/index.js";
|
|
29
|
+
import { ASTROID_INCIDENTS_MIGRATION } from "../incidents/names.js";
|
|
29
30
|
import { generateAstroidActions } from "./actions.js";
|
|
30
31
|
import { cwvBeaconScript } from "louise-toolkit/analytics";
|
|
31
32
|
import { generateMapEmbedComponent, generateMapTileRoute } from "../map/scaffold.js";
|
|
32
33
|
import { generateAstroidGalleryPage } from "../portfolio/scaffold.js";
|
|
33
34
|
import { astroidPortal } from "../portal/config.js";
|
|
35
|
+
import { astroidHasEditor } from "../shape.js";
|
|
34
36
|
import { generateAstroidPortalAuth, generateAstroidPortalAuthRoute } from "../portal/scaffold.js";
|
|
35
37
|
import { generateAstroidTenancy } from "../tenancy/index.js";
|
|
36
38
|
import { generateAstroidEditSession } from "../realtime/scaffold.js";
|
|
@@ -154,6 +156,10 @@ function generateAstroidSettingsHooks() {
|
|
|
154
156
|
*/
|
|
155
157
|
export function generateAstroidScaffoldFiles(config) {
|
|
156
158
|
const files = [];
|
|
159
|
+
// An app with no editor gets none of the editor's files: the content
|
|
160
|
+
// migrations, the CWV beacon its Health panel reads, the Actions surface
|
|
161
|
+
// over pages and settings, and the gallery page over the media library.
|
|
162
|
+
const editor = astroidHasEditor(config);
|
|
157
163
|
// --- commerce: the catalog table's migration ------------------------------
|
|
158
164
|
// Numbered 0003 so it lands after the template's 0000_content and the auth
|
|
159
165
|
// pair (0001, 0002) that `create-astroid` writes. Without it `--commerce`
|
|
@@ -170,21 +176,38 @@ export function generateAstroidScaffoldFiles(config) {
|
|
|
170
176
|
// filename. The CLI moves each into the site's `migrations_dir` and past
|
|
171
177
|
// the site's own numbers (see migrations.ts), so a site that already has its
|
|
172
178
|
// own 0004 gets the next free number instead of a second 0004.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
if (editor) {
|
|
180
|
+
files.push({
|
|
181
|
+
path: "migrations/0004_page_redirects.sql",
|
|
182
|
+
contents: ASTROID_PAGE_REDIRECTS_MIGRATION,
|
|
183
|
+
migration: true,
|
|
184
|
+
}, {
|
|
185
|
+
path: "migrations/0005_media_alt_undecided.sql",
|
|
186
|
+
contents: ASTROID_MEDIA_ALT_MIGRATION,
|
|
187
|
+
migration: true,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
// --- incident capture's tables -------------------------------------------
|
|
191
|
+
// For every shape: the generated worker counts failures into `incidents`
|
|
192
|
+
// and keeps dead letters in `dead_letters` (louise-toolkit ADR 0022). Written
|
|
193
|
+
// into an existing site by `astroid generate`, like the two above, and
|
|
194
|
+
// renumbered past the site's own migrations. Not when another app migrates
|
|
195
|
+
// this database (`deploy.migrations: false`); that app owns the tables.
|
|
196
|
+
if (config.deploy?.migrations !== false) {
|
|
197
|
+
files.push({
|
|
198
|
+
path: "migrations/0006_incidents.sql",
|
|
199
|
+
contents: ASTROID_INCIDENTS_MIGRATION,
|
|
200
|
+
migration: true,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
182
203
|
// --- the CWV beacon -------------------------------------------------------
|
|
183
204
|
// A static file under public/, so it is same-origin and covered by
|
|
184
205
|
// `script-src 'self'`—an inline script carrying generated content could not
|
|
185
206
|
// be hashed into the CSP and would be blocked.
|
|
186
|
-
|
|
187
|
-
|
|
207
|
+
if (editor) {
|
|
208
|
+
const beacon = generateAstroidVitalsBeacon(config, cwvBeaconScript());
|
|
209
|
+
files.push({ path: beacon.path, contents: beacon.contents });
|
|
210
|
+
}
|
|
188
211
|
// --- site-owned schema tables --------------------------------------------
|
|
189
212
|
// Always: the generated src/schema.ts re-exports `./schema.site.js`, so the
|
|
190
213
|
// file must exist even when empty. A project declares tables Astroid doesn't
|
|
@@ -218,10 +241,12 @@ export function generateAstroidScaffoldFiles(config) {
|
|
|
218
241
|
files.push({ path: "src/settings-hooks.ts", contents: generateAstroidSettingsHooks() });
|
|
219
242
|
}
|
|
220
243
|
// --- the typed Astro Actions surface --------------------------------------
|
|
221
|
-
//
|
|
222
|
-
// Astro-native half of ADR 0001 unbuilt. Scaffold-once because it
|
|
223
|
-
// be added to.
|
|
224
|
-
|
|
244
|
+
// Every project with an editor: it has editable pages, and the routes alone
|
|
245
|
+
// leave the Astro-native half of ADR 0001 unbuilt. Scaffold-once because it
|
|
246
|
+
// is meant to be added to.
|
|
247
|
+
if (editor) {
|
|
248
|
+
files.push({ path: "src/actions/index.ts", contents: generateAstroidActions(config) });
|
|
249
|
+
}
|
|
225
250
|
// --- commerce: the server-authoritative payment seam ----------------------
|
|
226
251
|
// Scaffold-once: a real store adds shipping, tax, an order row, a receipt.
|
|
227
252
|
// What's fixed is the sequence that keeps a charge correct.
|
|
@@ -243,7 +268,7 @@ export function generateAstroidScaffoldFiles(config) {
|
|
|
243
268
|
}
|
|
244
269
|
// --- portfolio: the gallery page -----------------------------------------
|
|
245
270
|
// "Which assets appear, in what order" is the first thing a portfolio changes.
|
|
246
|
-
const gallery = generateAstroidGalleryPage(config);
|
|
271
|
+
const gallery = editor ? generateAstroidGalleryPage(config) : null;
|
|
247
272
|
if (gallery)
|
|
248
273
|
files.push({ path: "src/pages/work.astro", contents: gallery });
|
|
249
274
|
// --- pwa: the service worker, manifest, and its headers -------------------
|
package/dist/queues/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { astroidQueueHandler, type QueueHandlerOptions } from "./consumer.js";
|
|
2
|
-
export { affectsCatalog, ASTROID_DEFAULT_CRON, ASTROID_HEALTH_CRON, ASTROID_QUEUE_BINDING, ASTROID_QUEUE_RETRY_DELAY, type AstroidQueueMessage, astroidCron, astroidCrons, astroidQueueNames, astroidUsesQueues, type CatalogRefreshMessage, type WebhookMessage, } from "./messages.js";
|
|
2
|
+
export { affectsCatalog, ASTROID_DEFAULT_CRON, ASTROID_HEALTH_CRON, ASTROID_QUEUE_BINDING, ASTROID_QUEUE_RETRY_DELAY, type AstroidQueueMessage, astroidCommercePipeline, astroidCron, astroidCrons, astroidQueueNames, astroidUsesQueues, type CatalogRefreshMessage, type WebhookMessage, } from "./messages.js";
|
|
3
3
|
export { generateAstroidEnvBindings, generateAstroidQueueSeam, generateAstroidWebhookRoute, generateAstroidWebhookRoutes, } from "./scaffold.js";
|
|
4
4
|
export { astroidQueue, handleWebhook, type QueueProducer, type WebhookRouteOptions, type WebhookVerifyInput, } from "./webhook.js";
|
package/dist/queues/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
2
|
export { astroidQueueHandler } from "./consumer.js";
|
|
3
|
-
export { affectsCatalog, ASTROID_DEFAULT_CRON, ASTROID_HEALTH_CRON, ASTROID_QUEUE_BINDING, ASTROID_QUEUE_RETRY_DELAY, astroidCron, astroidCrons, astroidQueueNames, astroidUsesQueues, } from "./messages.js";
|
|
3
|
+
export { affectsCatalog, ASTROID_DEFAULT_CRON, ASTROID_HEALTH_CRON, ASTROID_QUEUE_BINDING, ASTROID_QUEUE_RETRY_DELAY, astroidCommercePipeline, astroidCron, astroidCrons, astroidQueueNames, astroidUsesQueues, } from "./messages.js";
|
|
4
4
|
export { generateAstroidEnvBindings, generateAstroidQueueSeam, generateAstroidWebhookRoute, generateAstroidWebhookRoutes, } from "./scaffold.js";
|
|
5
5
|
export { astroidQueue, handleWebhook, } from "./webhook.js";
|
|
@@ -7,6 +7,12 @@ import type { AstroidConfig } from "../config.js";
|
|
|
7
7
|
* provider's delivery timeout is shorter than your catalog sync.
|
|
8
8
|
*/
|
|
9
9
|
export declare function astroidUsesQueues(config: AstroidConfig): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Whether this project runs the commerce pipeline: the webhook receivers and the
|
|
12
|
+
* catalog re-sync. On whenever commerce is configured, unless the config sets
|
|
13
|
+
* `commerce.pipeline: false` because another project runs it.
|
|
14
|
+
*/
|
|
15
|
+
export declare function astroidCommercePipeline(config: AstroidConfig): boolean;
|
|
10
16
|
/** Hourly. Frequent enough that stale data has a bounded lifetime, rare enough
|
|
11
17
|
* to be free. */
|
|
12
18
|
export declare const ASTROID_DEFAULT_CRON = "0 * * * *";
|
package/dist/queues/messages.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
2
|
//
|
|
3
3
|
// What flows through the project's queue, and when it matters.
|
|
4
|
+
import { astroidHasEditor } from "../shape.js";
|
|
4
5
|
/**
|
|
5
6
|
* Whether this project runs a queue consumer + cron.
|
|
6
7
|
*
|
|
@@ -9,7 +10,15 @@
|
|
|
9
10
|
* provider's delivery timeout is shorter than your catalog sync.
|
|
10
11
|
*/
|
|
11
12
|
export function astroidUsesQueues(config) {
|
|
12
|
-
return config.queues?.enabled ??
|
|
13
|
+
return config.queues?.enabled ?? astroidCommercePipeline(config);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Whether this project runs the commerce pipeline: the webhook receivers and the
|
|
17
|
+
* catalog re-sync. On whenever commerce is configured, unless the config sets
|
|
18
|
+
* `commerce.pipeline: false` because another project runs it.
|
|
19
|
+
*/
|
|
20
|
+
export function astroidCommercePipeline(config) {
|
|
21
|
+
return Boolean(config.commerce) && config.commerce?.pipeline !== false;
|
|
13
22
|
}
|
|
14
23
|
/** Hourly. Frequent enough that stale data has a bounded lifetime, rare enough
|
|
15
24
|
* to be free. */
|
|
@@ -18,6 +27,10 @@ export const ASTROID_DEFAULT_CRON = "0 * * * *";
|
|
|
18
27
|
export function astroidCron(config) {
|
|
19
28
|
if (!astroidUsesQueues(config))
|
|
20
29
|
return null;
|
|
30
|
+
// The re-sync is part of the pipeline, so a project that leaves the pipeline
|
|
31
|
+
// to another one leaves this to it too, even when its queue runs for crons.
|
|
32
|
+
if (config.commerce && !astroidCommercePipeline(config))
|
|
33
|
+
return null;
|
|
21
34
|
const cron = config.queues?.cron;
|
|
22
35
|
if (cron === false)
|
|
23
36
|
return null;
|
|
@@ -40,7 +53,9 @@ export const ASTROID_HEALTH_CRON = "17 4 * * *";
|
|
|
40
53
|
* agree exactly, and a mismatch is a job that silently never runs.
|
|
41
54
|
*/
|
|
42
55
|
export function astroidCrons(config) {
|
|
43
|
-
|
|
56
|
+
// The health scan reports to the editor's Health panel, over the editor's
|
|
57
|
+
// pages and media, so an app with no editor has nothing to scan or show.
|
|
58
|
+
const crons = astroidHasEditor(config) ? [ASTROID_HEALTH_CRON] : [];
|
|
44
59
|
const catalog = astroidCron(config);
|
|
45
60
|
if (catalog)
|
|
46
61
|
crons.push(catalog);
|
package/dist/queues/scaffold.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { astroidCatalogMirror } from "../commerce/mirror.js";
|
|
12
12
|
import { astroidCommerceProviders, astroidCommerceRoles } from "../commerce/roles.js";
|
|
13
13
|
import { COMMERCE_PROVIDER_SECRETS } from "../commerce/secrets.js";
|
|
14
|
-
import { ASTROID_QUEUE_BINDING } from "./messages.js";
|
|
14
|
+
import { ASTROID_QUEUE_BINDING, astroidCommercePipeline } from "./messages.js";
|
|
15
15
|
/**
|
|
16
16
|
* Per-provider webhook facts: the header, the verifier, and how it's called.
|
|
17
17
|
*
|
|
@@ -244,6 +244,10 @@ export function generateAstroidWebhookRoute(config, forProvider) {
|
|
|
244
244
|
* two roles still has one endpoint and one secret.
|
|
245
245
|
*/
|
|
246
246
|
export function generateAstroidWebhookRoutes(config) {
|
|
247
|
+
// A project that leaves the pipeline to another one receives no webhooks: the
|
|
248
|
+
// provider delivers each event to one endpoint, and it's the other project's.
|
|
249
|
+
if (!astroidCommercePipeline(config))
|
|
250
|
+
return [];
|
|
247
251
|
return astroidCommerceProviders(config.commerce).flatMap((provider) => {
|
|
248
252
|
const contents = generateAstroidWebhookRoute(config, provider);
|
|
249
253
|
return contents ? [{ path: `src/pages/api/webhooks/${provider}.ts`, contents }] : [];
|
package/dist/schema/generate.js
CHANGED
|
@@ -11,7 +11,69 @@
|
|
|
11
11
|
// result to disk and formats it. Import ordering matches the site's schema.ts so
|
|
12
12
|
// a formatter never re-flags the generated file.
|
|
13
13
|
import { generateCatalogTable } from "../commerce/mirror.js";
|
|
14
|
+
import { astroidHasEditor } from "../shape.js";
|
|
14
15
|
import { astroidFrameworkTables } from "./framework.js";
|
|
16
|
+
/** The Drizzle column builders a catalog table's source uses. */
|
|
17
|
+
function catalogBuilders(catalog) {
|
|
18
|
+
const builders = ["integer", "sqliteTable", "text"];
|
|
19
|
+
if (catalog?.includes("real("))
|
|
20
|
+
builders.push("real");
|
|
21
|
+
return builders.sort();
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The incident tables every shape re-exports (louise-toolkit ADR 0022): the
|
|
25
|
+
* worker counts failures into `incidents` and keeps dead-lettered messages in
|
|
26
|
+
* `dead_letters`, so an app owns both, even when it owns nothing else. Not when
|
|
27
|
+
* another app migrates its database (`deploy.migrations: false`): that app owns
|
|
28
|
+
* them, and drizzle-kit here must not write migrations for them.
|
|
29
|
+
*/
|
|
30
|
+
function incidentTables(config) {
|
|
31
|
+
return config.deploy?.migrations === false ? [] : INCIDENT_TABLES;
|
|
32
|
+
}
|
|
33
|
+
const INCIDENT_TABLES = [
|
|
34
|
+
"// Incident capture's tables (louise-toolkit ADR 0022): the generated worker",
|
|
35
|
+
"// counts failures into `incidents` and keeps dead-lettered queue messages in",
|
|
36
|
+
"// `dead_letters`.",
|
|
37
|
+
'export { deadLetters, incidents } from "louise-toolkit/incidents";',
|
|
38
|
+
"",
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* The schema of an app with no editor (`editor: false`): no `pages`, no
|
|
42
|
+
* versions, and no framework tables, because nothing here edits them. What's
|
|
43
|
+
* left is what the config itself implies, the commerce catalog, and the site's
|
|
44
|
+
* own tables.
|
|
45
|
+
*
|
|
46
|
+
* A framework table the app only READS, such as `siteSettings` in a database
|
|
47
|
+
* another app owns, is imported straight from `louise-toolkit/db` where it's
|
|
48
|
+
* queried. Re-exporting it here would put it in front of drizzle-kit, which
|
|
49
|
+
* would then write migrations for a table this app doesn't own.
|
|
50
|
+
*/
|
|
51
|
+
function generateAppSchema(config) {
|
|
52
|
+
const catalog = generateCatalogTable(config);
|
|
53
|
+
return [
|
|
54
|
+
"// Generated by astroidjs—do not hand-edit.",
|
|
55
|
+
"// Source: your defineAstroid config. This app has no editor (`editor: false`),",
|
|
56
|
+
"// so it owns no content tables. To read one that another app owns, such as",
|
|
57
|
+
'// `siteSettings`, import it from "louise-toolkit/db" where you query it.',
|
|
58
|
+
...(catalog
|
|
59
|
+
? [
|
|
60
|
+
`import { ${catalogBuilders(catalog).join(", ")} } from "drizzle-orm/sqlite-core";`,
|
|
61
|
+
// The catalog's JSON columns are typed with it; the editor schema
|
|
62
|
+
// imports it for `pages.sections` anyway.
|
|
63
|
+
...(catalog.includes("JsonValue")
|
|
64
|
+
? ['import type { JsonValue } from "louise-toolkit/content";']
|
|
65
|
+
: []),
|
|
66
|
+
"",
|
|
67
|
+
catalog,
|
|
68
|
+
]
|
|
69
|
+
: [""]),
|
|
70
|
+
...incidentTables(config),
|
|
71
|
+
"// Site-owned tables, declared in src/schema.site.ts and re-exported here so",
|
|
72
|
+
"// drizzle-kit and the worker see them. Empty until the app adds one.",
|
|
73
|
+
'export * from "./schema.site.js";',
|
|
74
|
+
"",
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
15
77
|
/**
|
|
16
78
|
* Generate the TypeScript source of a site's Drizzle schema from an Astroid
|
|
17
79
|
* config. The output is a drop-in replacement for the hand-written `schema.ts`:
|
|
@@ -19,6 +81,8 @@ import { astroidFrameworkTables } from "./framework.js";
|
|
|
19
81
|
* table, and the re-exported framework tables the config selects.
|
|
20
82
|
*/
|
|
21
83
|
export function generateAstroidSchema(config) {
|
|
84
|
+
if (!astroidHasEditor(config))
|
|
85
|
+
return generateAppSchema(config);
|
|
22
86
|
const framework = astroidFrameworkTables(config); // sorted
|
|
23
87
|
// pagesColumns is spread into `pages`; the framework tables are re-exported.
|
|
24
88
|
// One import brings them all in (matching the site) so nothing is unused.
|
|
@@ -27,13 +91,11 @@ export function generateAstroidSchema(config) {
|
|
|
27
91
|
// Only import the column builders the emitted source actually uses—an
|
|
28
92
|
// unused import is a lint error in the project we're generating into, and a
|
|
29
93
|
// missing one (`real`, from the catalog's price/sortOrder) is a type error.
|
|
30
|
-
const drizzleImports =
|
|
31
|
-
if (catalog?.includes("real("))
|
|
32
|
-
drizzleImports.push("real");
|
|
94
|
+
const drizzleImports = catalogBuilders(catalog);
|
|
33
95
|
return [
|
|
34
96
|
"// Generated by astroidjs—do not hand-edit.",
|
|
35
97
|
"// Source: your defineAstroid config.",
|
|
36
|
-
`import { ${drizzleImports.
|
|
98
|
+
`import { ${drizzleImports.join(", ")} } from "drizzle-orm/sqlite-core";`,
|
|
37
99
|
'import { collectionVersionsTable, type JsonValue } from "louise-toolkit/content";',
|
|
38
100
|
`import { ${dbImports.join(", ")} } from "louise-toolkit/db";`,
|
|
39
101
|
"",
|
|
@@ -53,6 +115,7 @@ export function generateAstroidSchema(config) {
|
|
|
53
115
|
...(catalog ? [catalog] : []),
|
|
54
116
|
`export { ${framework.join(", ")} };`,
|
|
55
117
|
"",
|
|
118
|
+
...incidentTables(config),
|
|
56
119
|
"// Site-owned tables (the ones Astroid doesn't manage): a project declares",
|
|
57
120
|
"// its own Drizzle tables in src/schema.site.ts and they're re-exported here",
|
|
58
121
|
"// so drizzle-kit sees them and the generated worker can import them. The file",
|
package/dist/security/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { ASTROID_CHECKOUT_PATH, ASTROID_PORTAL_BASE_PATH, astroidRateRules, type RateRule, } from "./rate-rules.js";
|
|
1
|
+
export { ASTROID_API_PREFIX, ASTROID_CHECKOUT_PATH, ASTROID_PORTAL_BASE_PATH, astroidRateRules, type RateRule, } from "./rate-rules.js";
|
package/dist/security/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
-
export { ASTROID_CHECKOUT_PATH, ASTROID_PORTAL_BASE_PATH, astroidRateRules, } from "./rate-rules.js";
|
|
2
|
+
export { ASTROID_API_PREFIX, ASTROID_CHECKOUT_PATH, ASTROID_PORTAL_BASE_PATH, astroidRateRules, } from "./rate-rules.js";
|
|
@@ -9,10 +9,17 @@ export type { RateRule };
|
|
|
9
9
|
export declare const ASTROID_PORTAL_BASE_PATH = "/api/portal-auth";
|
|
10
10
|
/** Path the commerce module's checkout POSTs to. */
|
|
11
11
|
export declare const ASTROID_CHECKOUT_PATH = "/api/checkout";
|
|
12
|
+
/**
|
|
13
|
+
* Where an app with no editor (`editor: false`) serves its versioned JSON API.
|
|
14
|
+
* A web client and a later native client call the same routes, so a new
|
|
15
|
+
* version is a new prefix rather than a change to the one clients already use.
|
|
16
|
+
*/
|
|
17
|
+
export declare const ASTROID_API_PREFIX = "/api/v1";
|
|
12
18
|
/**
|
|
13
19
|
* The rule set for a project, derived from its config: the editor sign-in
|
|
14
|
-
* surface
|
|
15
|
-
* checkout when commerce is configured
|
|
20
|
+
* surface unless the project has no editor, the portal's credential surfaces
|
|
21
|
+
* when a portal is enabled, checkout when commerce is configured, and the
|
|
22
|
+
* versioned API for an app with no editor.
|
|
16
23
|
*
|
|
17
24
|
* Rules are matched first-wins, and `security.rateRules` from the config are
|
|
18
25
|
* placed FIRST—so a site tightens or loosens any default by declaring its own
|