astroidjs 0.19.0 → 0.20.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 +54 -2
- package/dist/config.js +84 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/project/generate.js +96 -52
- package/dist/project/scaffold.js +27 -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 +48 -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 +142 -10
- package/package.json +3 -3
- package/src/components/Credit.astro +70 -0
package/dist/project/scaffold.js
CHANGED
|
@@ -31,6 +31,7 @@ import { cwvBeaconScript } from "louise-toolkit/analytics";
|
|
|
31
31
|
import { generateMapEmbedComponent, generateMapTileRoute } from "../map/scaffold.js";
|
|
32
32
|
import { generateAstroidGalleryPage } from "../portfolio/scaffold.js";
|
|
33
33
|
import { astroidPortal } from "../portal/config.js";
|
|
34
|
+
import { astroidHasEditor } from "../shape.js";
|
|
34
35
|
import { generateAstroidPortalAuth, generateAstroidPortalAuthRoute } from "../portal/scaffold.js";
|
|
35
36
|
import { generateAstroidTenancy } from "../tenancy/index.js";
|
|
36
37
|
import { generateAstroidEditSession } from "../realtime/scaffold.js";
|
|
@@ -154,6 +155,10 @@ function generateAstroidSettingsHooks() {
|
|
|
154
155
|
*/
|
|
155
156
|
export function generateAstroidScaffoldFiles(config) {
|
|
156
157
|
const files = [];
|
|
158
|
+
// An app with no editor gets none of the editor's files: the content
|
|
159
|
+
// migrations, the CWV beacon its Health panel reads, the Actions surface
|
|
160
|
+
// over pages and settings, and the gallery page over the media library.
|
|
161
|
+
const editor = astroidHasEditor(config);
|
|
157
162
|
// --- commerce: the catalog table's migration ------------------------------
|
|
158
163
|
// Numbered 0003 so it lands after the template's 0000_content and the auth
|
|
159
164
|
// pair (0001, 0002) that `create-astroid` writes. Without it `--commerce`
|
|
@@ -170,21 +175,25 @@ export function generateAstroidScaffoldFiles(config) {
|
|
|
170
175
|
// filename. The CLI moves each into the site's `migrations_dir` and past
|
|
171
176
|
// the site's own numbers (see migrations.ts), so a site that already has its
|
|
172
177
|
// own 0004 gets the next free number instead of a second 0004.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
if (editor) {
|
|
179
|
+
files.push({
|
|
180
|
+
path: "migrations/0004_page_redirects.sql",
|
|
181
|
+
contents: ASTROID_PAGE_REDIRECTS_MIGRATION,
|
|
182
|
+
migration: true,
|
|
183
|
+
}, {
|
|
184
|
+
path: "migrations/0005_media_alt_undecided.sql",
|
|
185
|
+
contents: ASTROID_MEDIA_ALT_MIGRATION,
|
|
186
|
+
migration: true,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
182
189
|
// --- the CWV beacon -------------------------------------------------------
|
|
183
190
|
// A static file under public/, so it is same-origin and covered by
|
|
184
191
|
// `script-src 'self'`—an inline script carrying generated content could not
|
|
185
192
|
// be hashed into the CSP and would be blocked.
|
|
186
|
-
|
|
187
|
-
|
|
193
|
+
if (editor) {
|
|
194
|
+
const beacon = generateAstroidVitalsBeacon(config, cwvBeaconScript());
|
|
195
|
+
files.push({ path: beacon.path, contents: beacon.contents });
|
|
196
|
+
}
|
|
188
197
|
// --- site-owned schema tables --------------------------------------------
|
|
189
198
|
// Always: the generated src/schema.ts re-exports `./schema.site.js`, so the
|
|
190
199
|
// file must exist even when empty. A project declares tables Astroid doesn't
|
|
@@ -218,10 +227,12 @@ export function generateAstroidScaffoldFiles(config) {
|
|
|
218
227
|
files.push({ path: "src/settings-hooks.ts", contents: generateAstroidSettingsHooks() });
|
|
219
228
|
}
|
|
220
229
|
// --- the typed Astro Actions surface --------------------------------------
|
|
221
|
-
//
|
|
222
|
-
// Astro-native half of ADR 0001 unbuilt. Scaffold-once because it
|
|
223
|
-
// be added to.
|
|
224
|
-
|
|
230
|
+
// Every project with an editor: it has editable pages, and the routes alone
|
|
231
|
+
// leave the Astro-native half of ADR 0001 unbuilt. Scaffold-once because it
|
|
232
|
+
// is meant to be added to.
|
|
233
|
+
if (editor) {
|
|
234
|
+
files.push({ path: "src/actions/index.ts", contents: generateAstroidActions(config) });
|
|
235
|
+
}
|
|
225
236
|
// --- commerce: the server-authoritative payment seam ----------------------
|
|
226
237
|
// Scaffold-once: a real store adds shipping, tax, an order row, a receipt.
|
|
227
238
|
// What's fixed is the sequence that keeps a charge correct.
|
|
@@ -243,7 +254,7 @@ export function generateAstroidScaffoldFiles(config) {
|
|
|
243
254
|
}
|
|
244
255
|
// --- portfolio: the gallery page -----------------------------------------
|
|
245
256
|
// "Which assets appear, in what order" is the first thing a portfolio changes.
|
|
246
|
-
const gallery = generateAstroidGalleryPage(config);
|
|
257
|
+
const gallery = editor ? generateAstroidGalleryPage(config) : null;
|
|
247
258
|
if (gallery)
|
|
248
259
|
files.push({ path: "src/pages/work.astro", contents: gallery });
|
|
249
260
|
// --- 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,51 @@
|
|
|
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 schema of an app with no editor (`editor: false`): no `pages`, no
|
|
25
|
+
* versions, and no framework tables, because nothing here edits them. What's
|
|
26
|
+
* left is what the config itself implies, the commerce catalog, and the site's
|
|
27
|
+
* own tables.
|
|
28
|
+
*
|
|
29
|
+
* A framework table the app only READS, such as `siteSettings` in a database
|
|
30
|
+
* another app owns, is imported straight from `louise-toolkit/db` where it's
|
|
31
|
+
* queried. Re-exporting it here would put it in front of drizzle-kit, which
|
|
32
|
+
* would then write migrations for a table this app doesn't own.
|
|
33
|
+
*/
|
|
34
|
+
function generateAppSchema(config) {
|
|
35
|
+
const catalog = generateCatalogTable(config);
|
|
36
|
+
return [
|
|
37
|
+
"// Generated by astroidjs—do not hand-edit.",
|
|
38
|
+
"// Source: your defineAstroid config. This app has no editor (`editor: false`),",
|
|
39
|
+
"// so it owns no content tables. To read one that another app owns, such as",
|
|
40
|
+
'// `siteSettings`, import it from "louise-toolkit/db" where you query it.',
|
|
41
|
+
...(catalog
|
|
42
|
+
? [
|
|
43
|
+
`import { ${catalogBuilders(catalog).join(", ")} } from "drizzle-orm/sqlite-core";`,
|
|
44
|
+
// The catalog's JSON columns are typed with it; the editor schema
|
|
45
|
+
// imports it for `pages.sections` anyway.
|
|
46
|
+
...(catalog.includes("JsonValue")
|
|
47
|
+
? ['import type { JsonValue } from "louise-toolkit/content";']
|
|
48
|
+
: []),
|
|
49
|
+
"",
|
|
50
|
+
catalog,
|
|
51
|
+
]
|
|
52
|
+
: [""]),
|
|
53
|
+
"// Site-owned tables, declared in src/schema.site.ts and re-exported here so",
|
|
54
|
+
"// drizzle-kit and the worker see them. Empty until the app adds one.",
|
|
55
|
+
'export * from "./schema.site.js";',
|
|
56
|
+
"",
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
15
59
|
/**
|
|
16
60
|
* Generate the TypeScript source of a site's Drizzle schema from an Astroid
|
|
17
61
|
* config. The output is a drop-in replacement for the hand-written `schema.ts`:
|
|
@@ -19,6 +63,8 @@ import { astroidFrameworkTables } from "./framework.js";
|
|
|
19
63
|
* table, and the re-exported framework tables the config selects.
|
|
20
64
|
*/
|
|
21
65
|
export function generateAstroidSchema(config) {
|
|
66
|
+
if (!astroidHasEditor(config))
|
|
67
|
+
return generateAppSchema(config);
|
|
22
68
|
const framework = astroidFrameworkTables(config); // sorted
|
|
23
69
|
// pagesColumns is spread into `pages`; the framework tables are re-exported.
|
|
24
70
|
// One import brings them all in (matching the site) so nothing is unused.
|
|
@@ -27,13 +73,11 @@ export function generateAstroidSchema(config) {
|
|
|
27
73
|
// Only import the column builders the emitted source actually uses—an
|
|
28
74
|
// unused import is a lint error in the project we're generating into, and a
|
|
29
75
|
// 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");
|
|
76
|
+
const drizzleImports = catalogBuilders(catalog);
|
|
33
77
|
return [
|
|
34
78
|
"// Generated by astroidjs—do not hand-edit.",
|
|
35
79
|
"// Source: your defineAstroid config.",
|
|
36
|
-
`import { ${drizzleImports.
|
|
80
|
+
`import { ${drizzleImports.join(", ")} } from "drizzle-orm/sqlite-core";`,
|
|
37
81
|
'import { collectionVersionsTable, type JsonValue } from "louise-toolkit/content";',
|
|
38
82
|
`import { ${dbImports.join(", ")} } from "louise-toolkit/db";`,
|
|
39
83
|
"",
|
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
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// The contact form is also absent by design: it's a worker route with its own
|
|
17
17
|
// per-form limiter, and worker routes are matched before Astro's middleware ever
|
|
18
18
|
// runs, so a rule here would never fire.
|
|
19
|
+
import { astroidHasEditor } from "../shape.js";
|
|
19
20
|
/**
|
|
20
21
|
* Base path the customer/portal Better Auth instance mounts at—its own
|
|
21
22
|
* handler, separate from the editor's `/api/auth`. Fixed by Astroid so the rate
|
|
@@ -24,14 +25,21 @@
|
|
|
24
25
|
export const ASTROID_PORTAL_BASE_PATH = "/api/portal-auth";
|
|
25
26
|
/** Path the commerce module's checkout POSTs to. */
|
|
26
27
|
export const ASTROID_CHECKOUT_PATH = "/api/checkout";
|
|
28
|
+
/**
|
|
29
|
+
* Where an app with no editor (`editor: false`) serves its versioned JSON API.
|
|
30
|
+
* A web client and a later native client call the same routes, so a new
|
|
31
|
+
* version is a new prefix rather than a change to the one clients already use.
|
|
32
|
+
*/
|
|
33
|
+
export const ASTROID_API_PREFIX = "/api/v1";
|
|
27
34
|
/** Ten minutes. Every default budget uses this window—long enough that a
|
|
28
35
|
* burst can't wait it out, short enough that a false positive self-heals. */
|
|
29
36
|
const WINDOW = 600;
|
|
30
37
|
const exact = (path) => (p) => p === path;
|
|
31
38
|
/**
|
|
32
39
|
* The rule set for a project, derived from its config: the editor sign-in
|
|
33
|
-
* surface
|
|
34
|
-
* checkout when commerce is configured
|
|
40
|
+
* surface unless the project has no editor, the portal's credential surfaces
|
|
41
|
+
* when a portal is enabled, checkout when commerce is configured, and the
|
|
42
|
+
* versioned API for an app with no editor.
|
|
35
43
|
*
|
|
36
44
|
* Rules are matched first-wins, and `security.rateRules` from the config are
|
|
37
45
|
* placed FIRST—so a site tightens or loosens any default by declaring its own
|
|
@@ -39,26 +47,29 @@ const exact = (path) => (p) => p === path;
|
|
|
39
47
|
*/
|
|
40
48
|
export function astroidRateRules(config) {
|
|
41
49
|
const rules = [...(config.security?.rateRules ?? [])];
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
// The editor's sign-in, which an app with no editor doesn't serve.
|
|
51
|
+
if (astroidHasEditor(config)) {
|
|
52
|
+
// Magic-link sign-in is the email-bombing target: without a cap, anyone who
|
|
53
|
+
// knows an editor's address can trigger unbounded sign-in mail (their inbox,
|
|
54
|
+
// your Email + Worker spend). Tightest budget in the set.
|
|
55
|
+
rules.push({
|
|
56
|
+
name: "magic-link",
|
|
57
|
+
method: "POST",
|
|
58
|
+
match: exact("/api/auth/sign-in/magic-link"),
|
|
59
|
+
limit: 5,
|
|
60
|
+
windowSec: WINDOW,
|
|
61
|
+
});
|
|
62
|
+
// Everything else Better Auth serves (passkey challenges, sign-out, callbacks)
|
|
63
|
+
// behind a looser catch-all. Ordered after the specific rule above, which
|
|
64
|
+
// matters: `matchRateRule` takes the first match.
|
|
65
|
+
rules.push({
|
|
66
|
+
name: "auth",
|
|
67
|
+
method: "POST",
|
|
68
|
+
match: (p) => p.startsWith("/api/auth/"),
|
|
69
|
+
limit: 30,
|
|
70
|
+
windowSec: WINDOW,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
62
73
|
if (config.portal?.enabled) {
|
|
63
74
|
// The portal's mount is configurable (a site may already ship a second
|
|
64
75
|
// instance at its own path), so the credential-surface rules must track the
|
|
@@ -106,5 +117,18 @@ export function astroidRateRules(config) {
|
|
|
106
117
|
windowSec: WINDOW,
|
|
107
118
|
});
|
|
108
119
|
}
|
|
120
|
+
if (!astroidHasEditor(config)) {
|
|
121
|
+
// An app's own JSON API: quotes, orders, account changes. Looser than
|
|
122
|
+
// checkout because a client posts here as a cart changes, not once per
|
|
123
|
+
// purchase, so this is abuse control rather than flow control. After the
|
|
124
|
+
// specific rules, so a tighter budget for one route wins.
|
|
125
|
+
rules.push({
|
|
126
|
+
name: "api",
|
|
127
|
+
method: "POST",
|
|
128
|
+
match: (p) => p.startsWith(`${ASTROID_API_PREFIX}/`),
|
|
129
|
+
limit: 120,
|
|
130
|
+
windowSec: WINDOW,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
109
133
|
return rules;
|
|
110
134
|
}
|
package/dist/seo/routes.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// rules are: which routes exist is a function of which modules are enabled, and
|
|
14
14
|
// a hand-maintained list drifts the moment someone turns a portal on.
|
|
15
15
|
import { ASTROID_PORTAL_BASE_PATH } from "../security/rate-rules.js";
|
|
16
|
+
import { astroidHasEditor } from "../shape.js";
|
|
16
17
|
/**
|
|
17
18
|
* Paths that must never be indexed, derived from the config: the editor and its
|
|
18
19
|
* API always, plus the portal's account + auth surfaces and checkout when those
|
|
@@ -25,8 +26,8 @@ export function astroidNoindexPaths(config) {
|
|
|
25
26
|
const paths = [
|
|
26
27
|
// Every worker route (editor CRUD, media, forms) and Better Auth.
|
|
27
28
|
"/api/",
|
|
28
|
-
// The editor entry point.
|
|
29
|
-
"/louise",
|
|
29
|
+
// The editor entry point, when there is an editor.
|
|
30
|
+
...(astroidHasEditor(config) ? ["/louise"] : []),
|
|
30
31
|
];
|
|
31
32
|
if (config.portal?.enabled) {
|
|
32
33
|
paths.push(ASTROID_PORTAL_BASE_PATH, "/account", "/login", "/register", "/reset-password");
|
package/dist/shape.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AstroidConfig } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Whether the project has a Louise editor: its routes, its sign-in, and the
|
|
4
|
+
* content tables it edits. True unless the config sets `editor: false`.
|
|
5
|
+
*/
|
|
6
|
+
export declare function astroidHasEditor(config: AstroidConfig): boolean;
|
package/dist/shape.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// The project's shape: a Louise-edited site, or an app with no pages to edit.
|
|
4
|
+
//
|
|
5
|
+
// Its own module, and dependency-free, because nearly every generator asks the
|
|
6
|
+
// question and `config.ts` imports some of them at runtime. A type-only import
|
|
7
|
+
// of the config keeps that from becoming a cycle.
|
|
8
|
+
/**
|
|
9
|
+
* Whether the project has a Louise editor: its routes, its sign-in, and the
|
|
10
|
+
* content tables it edits. True unless the config sets `editor: false`.
|
|
11
|
+
*/
|
|
12
|
+
export function astroidHasEditor(config) {
|
|
13
|
+
return config.editor !== false;
|
|
14
|
+
}
|
package/dist/status.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { commerceSecretNames, resolveCommerceStatus } from "./commerce/secrets.js";
|
|
20
20
|
import { ASTROID_VITALS_SECRET_NAMES } from "./analytics/index.js";
|
|
21
21
|
import { EMAIL_SECRET_NAMES, resolveMailerStatus } from "./email/send.js";
|
|
22
|
+
import { astroidHasEditor } from "./shape.js";
|
|
22
23
|
/**
|
|
23
24
|
* Secrets every Astroid site has, independent of which modules are on.
|
|
24
25
|
*
|
|
@@ -40,17 +41,25 @@ export const ASTROID_CORE_SECRET_NAMES = [
|
|
|
40
41
|
* and a marketing site shouldn't be told to provision a Square token.
|
|
41
42
|
*/
|
|
42
43
|
export function astroidSecretNames(config) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
// An app with no editor signs nobody in unless it has a portal, which needs
|
|
45
|
+
// the session secret and mail, not the editor's Turnstile pair.
|
|
46
|
+
const editor = astroidHasEditor(config);
|
|
47
|
+
const portal = Boolean(config.portal?.enabled);
|
|
48
|
+
const groups = {};
|
|
49
|
+
if (editor)
|
|
50
|
+
groups.core = [...ASTROID_CORE_SECRET_NAMES];
|
|
51
|
+
else if (portal)
|
|
52
|
+
groups.core = ["SESSION_SECRET"];
|
|
53
|
+
if (editor || portal)
|
|
54
|
+
groups.email = [...EMAIL_SECRET_NAMES];
|
|
47
55
|
const commerce = commerceSecretNames(config.commerce);
|
|
48
56
|
if (commerce.length > 0)
|
|
49
57
|
groups.commerce = commerce;
|
|
50
58
|
// The CWV read-back's API credentials. Collection needs none of this—only
|
|
51
59
|
// querying the p75 back out does, because the Analytics Engine SQL API is
|
|
52
|
-
// account-scoped and has no binding.
|
|
53
|
-
|
|
60
|
+
// account-scoped and has no binding. The editor's Health panel reads it.
|
|
61
|
+
if (editor)
|
|
62
|
+
groups.vitals = [...ASTROID_VITALS_SECRET_NAMES];
|
|
54
63
|
return groups;
|
|
55
64
|
}
|
|
56
65
|
/**
|
|
@@ -68,8 +77,11 @@ export async function astroidModuleStatus(config, env) {
|
|
|
68
77
|
resolveCommerceStatus(config.commerce, secretEnv),
|
|
69
78
|
resolveMailerStatus(env),
|
|
70
79
|
]);
|
|
71
|
-
const reports = [
|
|
72
|
-
|
|
80
|
+
const reports = [];
|
|
81
|
+
// Mail is for signing in: the editor's magic link, or a portal's password
|
|
82
|
+
// reset. An app with neither sends none, so there's nothing to report.
|
|
83
|
+
if (astroidHasEditor(config) || config.portal?.enabled) {
|
|
84
|
+
reports.push({
|
|
73
85
|
module: "email",
|
|
74
86
|
enabled: true,
|
|
75
87
|
configured: mailer.configured,
|
|
@@ -79,8 +91,8 @@ export async function astroidModuleStatus(config, env) {
|
|
|
79
91
|
: // Naming the log is the useful part: the magic link IS in the console,
|
|
80
92
|
// and someone who doesn't know that concludes sign-in is broken.
|
|
81
93
|
"dormant — messages are logged to the console (the magic link is in the log), not sent",
|
|
82
|
-
}
|
|
83
|
-
|
|
94
|
+
});
|
|
95
|
+
}
|
|
84
96
|
if (commerce.enabled) {
|
|
85
97
|
reports.push({
|
|
86
98
|
module: "commerce",
|