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/bin/astroid.mjs
CHANGED
|
@@ -172,6 +172,7 @@ async function cmdDoctor(cwd, flags) {
|
|
|
172
172
|
generateAstroidScaffoldFiles,
|
|
173
173
|
astroidUsesQueues,
|
|
174
174
|
astroidCrons,
|
|
175
|
+
astroidHasEditor,
|
|
175
176
|
checkWranglerPreviews,
|
|
176
177
|
astroidRunsMigrations,
|
|
177
178
|
migrationsOwnershipError,
|
|
@@ -235,9 +236,15 @@ async function cmdDoctor(cwd, flags) {
|
|
|
235
236
|
//
|
|
236
237
|
// So check every binding the GENERATED code actually dereferences, not just
|
|
237
238
|
// the three the baseline happens to have.
|
|
239
|
+
// An app with no editor (`editor: false`) uses none of the editor's
|
|
240
|
+
// bindings, so it isn't held to them: no draft buffer, no media bucket, and
|
|
241
|
+
// no mail unless its portal sends password resets.
|
|
242
|
+
const editor = astroidHasEditor(config);
|
|
238
243
|
const requiredBindings = [
|
|
239
244
|
{ name: "RL", what: "KV namespace", why: "the rate limiter in src/middleware.ts" },
|
|
240
|
-
|
|
245
|
+
...(editor
|
|
246
|
+
? [{ name: "DRAFTS", what: "KV namespace", why: "the autosave draft buffer" }]
|
|
247
|
+
: []),
|
|
241
248
|
...(astroidUsesQueues(config)
|
|
242
249
|
? [
|
|
243
250
|
{
|
|
@@ -263,17 +270,24 @@ async function cmdDoctor(cwd, flags) {
|
|
|
263
270
|
// it: the magic link is console-logged in dev and EMAILED in production, so
|
|
264
271
|
// a missing binding is a site nobody can sign in to—and it fails only once
|
|
265
272
|
// deployed, which is the one place nothing in this repo exercises.
|
|
266
|
-
|
|
267
|
-
|
|
273
|
+
// An app with no editor and no portal signs nobody in, so it sends no mail.
|
|
274
|
+
const sendsMail = editor || Boolean(config.portal?.enabled);
|
|
275
|
+
if (sendsMail && /"send_email"\s*:/.test(w))
|
|
276
|
+
ok("wrangler: Email Sending `EMAIL` binding present");
|
|
277
|
+
else if (sendsMail)
|
|
268
278
|
err(
|
|
269
|
-
|
|
270
|
-
|
|
279
|
+
editor
|
|
280
|
+
? "wrangler.jsonc has no `send_email` binding, but production sign-in emails the " +
|
|
281
|
+
'magic link (it is only console-logged in dev). Add: "send_email": [{ "name": "EMAIL" }]'
|
|
282
|
+
: "wrangler.jsonc has no `send_email` binding, but the portal emails password resets " +
|
|
283
|
+
'in production (they are only console-logged in dev). Add: "send_email": [{ "name": "EMAIL" }]',
|
|
271
284
|
);
|
|
272
285
|
|
|
273
286
|
if (hasBinding("DB")) ok("wrangler: D1 `DB` binding present");
|
|
274
287
|
else err("wrangler.jsonc has no D1 `DB` binding.");
|
|
275
|
-
|
|
276
|
-
|
|
288
|
+
// An app with no editor has no media library, so no bucket to hold it.
|
|
289
|
+
if (editor && hasBinding("MEDIA")) ok("wrangler: R2 `MEDIA` binding present");
|
|
290
|
+
else if (editor) err("wrangler.jsonc has no R2 `MEDIA` binding.");
|
|
277
291
|
if (/"main"\s*:\s*"src\/worker\.ts"/.test(w)) ok("wrangler: `main` → src/worker.ts");
|
|
278
292
|
else warn("wrangler.jsonc `main` does not point at src/worker.ts.");
|
|
279
293
|
|
|
@@ -287,7 +301,17 @@ async function cmdDoctor(cwd, flags) {
|
|
|
287
301
|
// JSON.parse (comments/trailing commas), matching the binding checks above.
|
|
288
302
|
const expectedCrons = astroidCrons(config);
|
|
289
303
|
const cronsMatch = w.match(/"crons"\s*:\s*\[([^\]]*)\]/);
|
|
290
|
-
|
|
304
|
+
const declaredAny = cronsMatch && /"[^"]+"/.test(cronsMatch[1]);
|
|
305
|
+
if (expectedCrons.length === 0) {
|
|
306
|
+
// Nothing scheduled, so the generated worker has no `scheduled` handler,
|
|
307
|
+
// and a declared trigger would fail every time it fired.
|
|
308
|
+
if (declaredAny)
|
|
309
|
+
warn(
|
|
310
|
+
"wrangler.jsonc declares `triggers.crons`, but nothing in your config is scheduled, " +
|
|
311
|
+
"so the generated worker has no `scheduled` handler for them. Remove the triggers.",
|
|
312
|
+
);
|
|
313
|
+
else ok("wrangler: no crons, and nothing is scheduled");
|
|
314
|
+
} else if (!cronsMatch) {
|
|
291
315
|
err(
|
|
292
316
|
"wrangler.jsonc has no `triggers.crons`, but the generated `scheduled` handler " +
|
|
293
317
|
`dispatches on ${expectedCrons.map((c) => `"${c}"`).join(", ")}. ` +
|
|
@@ -50,6 +50,13 @@ export declare function commerceProviderCredentials(provider: CommerceProvider,
|
|
|
50
50
|
* `.dev.vars` would be a bug rather than a redundancy.
|
|
51
51
|
*/
|
|
52
52
|
export declare function commerceSecretNames(commerce: CommerceConfig | undefined): string[];
|
|
53
|
+
/**
|
|
54
|
+
* The webhook signing secret a provider needs, or none when the project runs no
|
|
55
|
+
* pipeline (`commerce.pipeline: false`). With no receiver there is nothing to
|
|
56
|
+
* verify, so requiring the secret would hold checkout dormant for a value no
|
|
57
|
+
* code reads.
|
|
58
|
+
*/
|
|
59
|
+
export declare function commerceProviderWebhookSecrets(provider: CommerceProvider, commerce: CommerceConfig | undefined): readonly string[];
|
|
53
60
|
/** One provider's resolved gate. */
|
|
54
61
|
export interface ProviderStatus {
|
|
55
62
|
provider: CommerceProvider;
|
package/dist/commerce/secrets.js
CHANGED
|
@@ -101,19 +101,28 @@ export function commerceProviderCredentials(provider, commerce) {
|
|
|
101
101
|
export function commerceSecretNames(commerce) {
|
|
102
102
|
const names = astroidCommerceProviders(commerce).flatMap((provider) => [
|
|
103
103
|
...commerceProviderCredentials(provider, commerce),
|
|
104
|
-
|
|
104
|
+
...commerceProviderWebhookSecrets(provider, commerce),
|
|
105
105
|
]);
|
|
106
106
|
return [...new Set(names)];
|
|
107
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* The webhook signing secret a provider needs, or none when the project runs no
|
|
110
|
+
* pipeline (`commerce.pipeline: false`). With no receiver there is nothing to
|
|
111
|
+
* verify, so requiring the secret would hold checkout dormant for a value no
|
|
112
|
+
* code reads.
|
|
113
|
+
*/
|
|
114
|
+
export function commerceProviderWebhookSecrets(provider, commerce) {
|
|
115
|
+
return commerce?.pipeline === false ? [] : [COMMERCE_PROVIDER_SECRETS[provider].webhook];
|
|
116
|
+
}
|
|
108
117
|
/** Read one provider's secrets off an env-shaped record. */
|
|
109
118
|
async function resolveProvider(provider, roles, env, commerce) {
|
|
110
|
-
const spec = COMMERCE_PROVIDER_SECRETS[provider];
|
|
111
119
|
const pick = (names) => Object.fromEntries(names.map((n) => [n, env[n]]));
|
|
112
120
|
const [credentials, webhook] = await Promise.all([
|
|
113
121
|
// Config-aware: a multi-location project must not be held dormant waiting
|
|
114
122
|
// for a SQUARE_LOCATION_ID it will never legitimately have.
|
|
115
123
|
resolveModuleSecrets(pick(commerceProviderCredentials(provider, commerce))),
|
|
116
|
-
|
|
124
|
+
// Empty without a pipeline, which resolves as configured: nothing to verify.
|
|
125
|
+
resolveModuleSecrets(pick(commerceProviderWebhookSecrets(provider, commerce))),
|
|
117
126
|
]);
|
|
118
127
|
return {
|
|
119
128
|
provider,
|
package/dist/config.d.ts
CHANGED
|
@@ -175,6 +175,18 @@ export interface CommerceConfig {
|
|
|
175
175
|
catalog?: CatalogMirrorConfig;
|
|
176
176
|
/** Square-specific options. Only meaningful when Square fills some role. */
|
|
177
177
|
square?: SquareCommerceConfig;
|
|
178
|
+
/**
|
|
179
|
+
* Whether this project runs the commerce pipeline: the webhook receivers, the
|
|
180
|
+
* queue consumer that processes them, and the hourly catalog re-sync. Default
|
|
181
|
+
* `true`.
|
|
182
|
+
*
|
|
183
|
+
* Set `false` for a project that only takes payments while another project,
|
|
184
|
+
* or another Worker in the same repository, runs the pipeline against the
|
|
185
|
+
* same account. It keeps what a checkout needs, the provider's CSP origins,
|
|
186
|
+
* the checkout rate rule, and the checkout route, and drops the rest, along
|
|
187
|
+
* with the webhook signing secret nothing would verify.
|
|
188
|
+
*/
|
|
189
|
+
pipeline?: boolean;
|
|
178
190
|
}
|
|
179
191
|
export interface SquareCommerceConfig {
|
|
180
192
|
/**
|
|
@@ -195,8 +207,9 @@ export interface SquareCommerceConfig {
|
|
|
195
207
|
export interface QueuesConfig {
|
|
196
208
|
/**
|
|
197
209
|
* Force the queue consumer + cron on or off. Defaults to on whenever
|
|
198
|
-
* `commerce` is configured: a commerce provider means
|
|
199
|
-
* you process inline is a webhook you drop when the
|
|
210
|
+
* `commerce` is configured with its pipeline: a commerce provider means
|
|
211
|
+
* webhooks, and a webhook you process inline is a webhook you drop when the
|
|
212
|
+
* provider times out.
|
|
200
213
|
*/
|
|
201
214
|
enabled?: boolean;
|
|
202
215
|
/**
|
|
@@ -497,6 +510,28 @@ export interface DeployConfig {
|
|
|
497
510
|
*/
|
|
498
511
|
migrations?: boolean;
|
|
499
512
|
}
|
|
513
|
+
/**
|
|
514
|
+
* A small "Site by …" line the footer renders for the agency that built the
|
|
515
|
+
* site. A site fact, so it has no default: omit it and nothing renders.
|
|
516
|
+
*/
|
|
517
|
+
export interface CreditConfig {
|
|
518
|
+
/** Who built the site, for example `"Example Organization"`. */
|
|
519
|
+
name: string;
|
|
520
|
+
/** Where the credit links, as an absolute `http` or `https` URL. */
|
|
521
|
+
href: string;
|
|
522
|
+
/**
|
|
523
|
+
* An optional mark shown before the name: a root-relative path (for example
|
|
524
|
+
* `"/credit-mark.svg"`) or an `https:` URL. It's drawn as a mask filled with
|
|
525
|
+
* the text color, so one single-color SVG works on every theme. Only its
|
|
526
|
+
* shape counts; its own fill is ignored.
|
|
527
|
+
*/
|
|
528
|
+
logo?: string;
|
|
529
|
+
/** The link's `rel`, for example `"noopener"` or `"noopener nofollow"`. The
|
|
530
|
+
* site decides; omitted, the link carries none. */
|
|
531
|
+
rel?: string;
|
|
532
|
+
/** The words before the name. Default `"Site by"`. */
|
|
533
|
+
label?: string;
|
|
534
|
+
}
|
|
500
535
|
export interface AstroidConfig {
|
|
501
536
|
/**
|
|
502
537
|
* Stable project slug—the worker/D1/R2 base name and default subdomain (for example,
|
|
@@ -513,6 +548,21 @@ export interface AstroidConfig {
|
|
|
513
548
|
tenancy?: TenancyConfig;
|
|
514
549
|
/** Starting shape; sets section/module/nav defaults the site can override. */
|
|
515
550
|
archetype: Archetype;
|
|
551
|
+
/**
|
|
552
|
+
* Whether the project has a Louise editor. Default `true`.
|
|
553
|
+
*
|
|
554
|
+
* Set `false` for an app with no pages to edit, such as an order app whose
|
|
555
|
+
* menu comes from a commerce provider and whose few settings another site's
|
|
556
|
+
* editor owns. The generated worker and middleware then carry no editor
|
|
557
|
+
* routes and no sign-in, the schema carries no content tables, and
|
|
558
|
+
* `wrangler.jsonc` binds no draft buffer, media bucket, or AI. What stays is
|
|
559
|
+
* the rate limiter, the CSP, the security headers, the public status route,
|
|
560
|
+
* and the `portal`, `pwa`, `commerce`, and `tenancy` modules.
|
|
561
|
+
*
|
|
562
|
+
* `defineAstroid` refuses every option that configures the editor
|
|
563
|
+
* alongside it, such as `sections` or `media`, rather than ignore it.
|
|
564
|
+
*/
|
|
565
|
+
editor?: boolean;
|
|
516
566
|
/** The single brand's theme (display name + color tokens + font). */
|
|
517
567
|
theme: Theme;
|
|
518
568
|
/** The editable home page, top to bottom. Omit to take the archetype default. */
|
|
@@ -583,6 +633,8 @@ export interface AstroidConfig {
|
|
|
583
633
|
inquiries?: boolean;
|
|
584
634
|
/** Installable-app settings. Only read when `modules` includes `"pwa"`. */
|
|
585
635
|
pwa?: PwaConfig;
|
|
636
|
+
/** The agency credit `<Credit>` renders in the footer. Omit for none. */
|
|
637
|
+
credit?: CreditConfig;
|
|
586
638
|
deploy?: DeployConfig;
|
|
587
639
|
}
|
|
588
640
|
export declare function defineAstroid(config: AstroidConfig): AstroidConfig;
|
package/dist/config.js
CHANGED
|
@@ -34,6 +34,7 @@ import { AstroidConfigError } from "./errors.js";
|
|
|
34
34
|
// type-only, so the cycle erases at build and nothing circular exists at runtime.
|
|
35
35
|
// It is also dependency-free, so `create-astroid`'s graph is unchanged.
|
|
36
36
|
import { ASTROID_HEALTH_CRON, astroidCron, astroidUsesQueues } from "./queues/messages.js";
|
|
37
|
+
import { astroidHasEditor } from "./shape.js";
|
|
37
38
|
/**
|
|
38
39
|
* Each archetype's default home-page sections.
|
|
39
40
|
*
|
|
@@ -88,6 +89,13 @@ export const ASTROID_SCAFFOLD_ROUTE_SLUGS = ["contact", "login", "work"];
|
|
|
88
89
|
* the unreachable-trigger failure `config.crons` exists to prevent.
|
|
89
90
|
*/
|
|
90
91
|
function assertCrons(config) {
|
|
92
|
+
// `queues.cron` schedules the catalog re-sync, which belongs to the pipeline.
|
|
93
|
+
// Without one it would be accepted and never scheduled.
|
|
94
|
+
if (config.commerce?.pipeline === false && typeof config.queues?.cron === "string") {
|
|
95
|
+
throw new AstroidConfigError("`queues.cron` schedules the catalog re-sync, which `commerce.pipeline: false` " +
|
|
96
|
+
"leaves to the project that runs the pipeline. Remove `queues.cron`, or use " +
|
|
97
|
+
"`crons` for a job of this project's own.");
|
|
98
|
+
}
|
|
91
99
|
const crons = config.crons ?? [];
|
|
92
100
|
if (crons.length === 0)
|
|
93
101
|
return;
|
|
@@ -96,7 +104,7 @@ function assertCrons(config) {
|
|
|
96
104
|
"without it the generated handler would `send` to a binding this project never creates. " +
|
|
97
105
|
"Set `queues: { enabled: true }`, or drop the crons.");
|
|
98
106
|
}
|
|
99
|
-
const seen = new Map([[ASTROID_HEALTH_CRON, "the daily health scan"]]);
|
|
107
|
+
const seen = new Map(astroidHasEditor(config) ? [[ASTROID_HEALTH_CRON, "the daily health scan"]] : []);
|
|
100
108
|
const catalog = astroidCron(config);
|
|
101
109
|
if (catalog)
|
|
102
110
|
seen.set(catalog, "the catalog re-sync (`queues.cron`)");
|
|
@@ -196,6 +204,79 @@ function assertMediaConfig(media) {
|
|
|
196
204
|
"would fail with an error the media route never sees.");
|
|
197
205
|
}
|
|
198
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* A credit that would render as a broken link or an empty mark.
|
|
209
|
+
*
|
|
210
|
+
* The logo is limited to a root-relative path or `https:` because it lands in a
|
|
211
|
+
* CSS `url()`: a `data:` or `javascript:` value there is at best unrenderable,
|
|
212
|
+
* and a relative one resolves against each page's path rather than the site.
|
|
213
|
+
*/
|
|
214
|
+
function assertCredit(credit) {
|
|
215
|
+
if (!credit)
|
|
216
|
+
return;
|
|
217
|
+
if (!credit.name?.trim()) {
|
|
218
|
+
throw new AstroidConfigError("`credit.name` is required: the name the footer credits");
|
|
219
|
+
}
|
|
220
|
+
let href;
|
|
221
|
+
try {
|
|
222
|
+
href = new URL(credit.href);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Reported below with the value that failed.
|
|
226
|
+
}
|
|
227
|
+
if (!href || (href.protocol !== "https:" && href.protocol !== "http:")) {
|
|
228
|
+
throw new AstroidConfigError(`\`credit.href\` must be an absolute http or https URL, such as "https://example.com", ` +
|
|
229
|
+
`but it's "${credit.href}"`);
|
|
230
|
+
}
|
|
231
|
+
const logo = credit.logo;
|
|
232
|
+
if (logo !== undefined &&
|
|
233
|
+
!(logo.startsWith("/") && !logo.startsWith("//")) &&
|
|
234
|
+
!logo.startsWith("https://")) {
|
|
235
|
+
throw new AstroidConfigError(`\`credit.logo\` must be a root-relative path such as "/credit-mark.svg" or an ` +
|
|
236
|
+
`https URL, but it's "${logo}"`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The options an app with no editor can't honor. Each configures the editor, a
|
|
241
|
+
* table it edits, or a surface only an editor reviews, so accepting one would
|
|
242
|
+
* be accepting a setting nothing reads.
|
|
243
|
+
*/
|
|
244
|
+
const EDITOR_ONLY_OPTIONS = [
|
|
245
|
+
["sections", "the editable home page"],
|
|
246
|
+
["sectionCatalog", "the page editor's sections"],
|
|
247
|
+
["blockCatalog", "the page editor's blocks"],
|
|
248
|
+
["media", "the media library"],
|
|
249
|
+
["pages", "the editable pages"],
|
|
250
|
+
["settings", "the Settings panel"],
|
|
251
|
+
];
|
|
252
|
+
/** Modules that only work with an editor, and why. */
|
|
253
|
+
const EDITOR_ONLY_MODULES = {
|
|
254
|
+
realtime: "it syncs editors editing one page",
|
|
255
|
+
wholesaleInquiry: "its inquiries are reviewed in the editor",
|
|
256
|
+
};
|
|
257
|
+
function assertEditorFree(config) {
|
|
258
|
+
if (astroidHasEditor(config))
|
|
259
|
+
return;
|
|
260
|
+
const without = "An app with `editor: false` has no editor";
|
|
261
|
+
for (const [key, what] of EDITOR_ONLY_OPTIONS) {
|
|
262
|
+
if (config[key] !== undefined) {
|
|
263
|
+
throw new AstroidConfigError(`${without}, so \`${key}\` (${what}) would do nothing. Remove it, or drop \`editor: false\`.`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (config.inquiries === true) {
|
|
267
|
+
throw new AstroidConfigError(`${without} to review inquiries in, so \`inquiries: true\` would collect messages ` +
|
|
268
|
+
"nobody reads. Remove it, or drop `editor: false`.");
|
|
269
|
+
}
|
|
270
|
+
for (const module of [...(config.modules ?? []), ...(config.portal?.features ?? [])]) {
|
|
271
|
+
const why = EDITOR_ONLY_MODULES[module];
|
|
272
|
+
if (why) {
|
|
273
|
+
throw new AstroidConfigError(`${without}, so the \`${module}\` module can't work: ${why}. Remove it, or drop \`editor: false\`.`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (config.deploy?.mediaBase !== undefined) {
|
|
277
|
+
throw new AstroidConfigError(`${without} and no media library, so \`deploy.mediaBase\` would do nothing. Remove it.`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
199
280
|
export function defineAstroid(config) {
|
|
200
281
|
if (!config.key || config.key.trim().length === 0) {
|
|
201
282
|
throw new AstroidConfigError("Astroid config requires a non-empty `key` (it names the generated worker/D1/R2 bindings)");
|
|
@@ -231,6 +312,8 @@ export function defineAstroid(config) {
|
|
|
231
312
|
assertCrons(config);
|
|
232
313
|
assertTenancy(config);
|
|
233
314
|
assertAllowSlugs(config);
|
|
315
|
+
assertEditorFree(config);
|
|
316
|
+
assertCredit(config.credit);
|
|
234
317
|
if (config.portal?.gated) {
|
|
235
318
|
throw new AstroidConfigError("`portal.gated` is not implemented: it is accepted but wires no guard, so the site " +
|
|
236
319
|
"would be fully public while appearing gated. Remove it, and gate the whole site by " +
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export * from "./queues/index.js";
|
|
|
15
15
|
export * from "./schema/index.js";
|
|
16
16
|
export * from "./security/index.js";
|
|
17
17
|
export * from "./seo/index.js";
|
|
18
|
+
export * from "./shape.js";
|
|
18
19
|
export * from "./status.js";
|
|
19
20
|
export * from "./tenancy/index.js";
|
|
20
21
|
export * from "./worker/index.js";
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export * from "./queues/index.js";
|
|
|
20
20
|
export * from "./schema/index.js";
|
|
21
21
|
export * from "./security/index.js";
|
|
22
22
|
export * from "./seo/index.js";
|
|
23
|
+
export * from "./shape.js";
|
|
23
24
|
export * from "./status.js";
|
|
24
25
|
export * from "./tenancy/index.js";
|
|
25
26
|
export * from "./worker/index.js";
|
package/dist/project/generate.js
CHANGED
|
@@ -17,10 +17,11 @@
|
|
|
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
21
|
import { ASTROID_QUEUE_BINDING, ASTROID_QUEUE_RETRY_DELAY, astroidCrons, astroidQueueNames, astroidUsesQueues, } from "../queues/messages.js";
|
|
22
22
|
import { ASTROID_EDIT_SESSION_CLASS, ASTROID_REALTIME_BINDING, ASTROID_REALTIME_MIGRATION_TAG, usesRealtime, } from "../realtime/scaffold.js";
|
|
23
23
|
import { ASTROID_SECRET_PLACEHOLDER } from "../secrets.js";
|
|
24
|
+
import { astroidHasEditor } from "../shape.js";
|
|
24
25
|
import { tenancyZone } from "../tenancy/index.js";
|
|
25
26
|
import { generateAstroidSchema } from "../schema/generate.js";
|
|
26
27
|
import { ASTROID_AI_GATEWAY_VAR } from "../worker/gateway.js";
|
|
@@ -93,7 +94,10 @@ export function generateAstroidSecretsEnv(config) {
|
|
|
93
94
|
for (const provider of providers) {
|
|
94
95
|
const spec = COMMERCE_PROVIDER_SECRETS[provider];
|
|
95
96
|
lines.push("#", `# ${provider}: ${COMMERCE_PROVIDER_SETUP[provider]}`);
|
|
96
|
-
for (const name of [
|
|
97
|
+
for (const name of [
|
|
98
|
+
...spec.credentials,
|
|
99
|
+
...commerceProviderWebhookSecrets(provider, config.commerce),
|
|
100
|
+
]) {
|
|
97
101
|
lines.push(`${name}=${ASTROID_SECRET_PLACEHOLDER}`);
|
|
98
102
|
}
|
|
99
103
|
}
|
|
@@ -116,6 +120,12 @@ const COMPATIBILITY_DATE = "2026-06-20";
|
|
|
116
120
|
*/
|
|
117
121
|
export function generateAstroidWrangler(config) {
|
|
118
122
|
const key = config.key;
|
|
123
|
+
// An app with no editor binds none of what the editor uses: the media bucket
|
|
124
|
+
// and Images, the draft buffer, Workers AI, and the CWV dataset. It sends mail
|
|
125
|
+
// only for a portal's password resets.
|
|
126
|
+
const editor = astroidHasEditor(config);
|
|
127
|
+
const mail = editor || Boolean(config.portal?.enabled);
|
|
128
|
+
const crons = astroidCrons(config);
|
|
119
129
|
const mediaBase = config.deploy?.mediaBase ?? "/media";
|
|
120
130
|
const hosts = config.hosts ?? [];
|
|
121
131
|
const primaryHost = hosts[0];
|
|
@@ -184,7 +194,10 @@ export function generateAstroidWrangler(config) {
|
|
|
184
194
|
// Daily: the site-health scan (broken links, missing alt text, SEO gaps).
|
|
185
195
|
// Hourly (commerce only): the catalog re-sync safety net, so a missed or DLQ'd
|
|
186
196
|
// webhook can only leave the site stale until the next tick.
|
|
187
|
-
|
|
197
|
+
// None at all for an app with nothing scheduled: a trigger with no handler
|
|
198
|
+
// is an invocation that fails every time it fires.
|
|
199
|
+
if (crons.length > 0)
|
|
200
|
+
p(` "triggers": { "crons": ${JSON.stringify(crons)} },`);
|
|
188
201
|
if (astroidUsesQueues(config)) {
|
|
189
202
|
const { queue, dlq } = astroidQueueNames(config);
|
|
190
203
|
p(" // Provider webhooks are verified at the edge, then enqueued here so the");
|
|
@@ -208,41 +221,59 @@ export function generateAstroidWrangler(config) {
|
|
|
208
221
|
p(" ],");
|
|
209
222
|
p(" },");
|
|
210
223
|
}
|
|
211
|
-
|
|
212
|
-
|
|
224
|
+
if (editor) {
|
|
225
|
+
p(" // D1 holds pages / site_settings / media / inquiries (schema in src/schema.ts,");
|
|
226
|
+
p(" // migrations in ./migrations). Create it: `wrangler d1 create <name>`.");
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
p(" // D1: this app's tables (src/schema.ts), or the database of the app that");
|
|
230
|
+
p(" // owns the schema, bound by its id. Create one: `wrangler d1 create <name>`.");
|
|
231
|
+
}
|
|
213
232
|
p(' "d1_databases": [');
|
|
214
233
|
p(" {");
|
|
215
234
|
p(' "binding": "DB",');
|
|
216
235
|
p(` "database_name": ${JSON.stringify(key)},`);
|
|
217
236
|
p(' "database_id": "<run: wrangler d1 create ' + key + '>",');
|
|
218
|
-
|
|
237
|
+
// Left out when another app migrates this database (`deploy.migrations:
|
|
238
|
+
// false`), which `astroid doctor` would otherwise report as a contradiction.
|
|
239
|
+
if (config.deploy?.migrations !== false)
|
|
240
|
+
p(' "migrations_dir": "migrations",');
|
|
219
241
|
p(" },");
|
|
220
242
|
p(" ],");
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
if (editor) {
|
|
244
|
+
p(" // R2 bucket for uploaded media, streamed back through the Worker at MEDIA_URL");
|
|
245
|
+
p(" // (no public bucket). Create it: `wrangler r2 bucket create <name>-media`.");
|
|
246
|
+
p(` "r2_buckets": [{ "binding": "MEDIA", "bucket_name": ${JSON.stringify(`${key}-media`)} }],`);
|
|
247
|
+
p(" // Cloudflare Images: the media route reads upload dimensions + backs server-");
|
|
248
|
+
p(" // side re-encode. Also @astrojs/cloudflare's production image service.");
|
|
249
|
+
p(' "images": { "binding": "IMAGES" },');
|
|
250
|
+
p(" // Analytics Engine: real-visitor Core Web Vitals. Free, and the ingest");
|
|
251
|
+
p(" // route accepts-and-drops without it, so it costs nothing unused. Reading");
|
|
252
|
+
p(" // the p75 back out needs CF_ACCOUNT_ID + CF_API_TOKEN (see .env.example)—");
|
|
253
|
+
p(" // until those are real the Health badge reads 'not measured yet'.");
|
|
254
|
+
p(` "analytics_engine_datasets": [{ "binding": ${JSON.stringify(ASTROID_VITALS_BINDING)}, "dataset": ${JSON.stringify(astroidVitalsDataset(config))} }],`);
|
|
255
|
+
p(" // Workers AI. Powers the editor's rewrite + SEO-suggest buttons and alt-text");
|
|
256
|
+
p(" // generation on upload—all of which SHIP IN THE EDITOR DRAWER already and,");
|
|
257
|
+
p(" // without this binding, were permanently invisible: their routes answer 503");
|
|
258
|
+
p(" // and the client hides the button. No account setup beyond the binding, and");
|
|
259
|
+
p(" // every call is editor-gated, so a visitor can never spend your AI budget.");
|
|
260
|
+
p(' "ai": { "binding": "AI" },');
|
|
261
|
+
p(" // KV: RL = the security rate limiter (it also holds the daily site-health");
|
|
262
|
+
p(" // summary under its own key—one small singleton blob, not worth a binding");
|
|
263
|
+
p(" // someone has to remember to provision); DRAFTS = the autosave write-buffer.");
|
|
264
|
+
p(" // Named for the project, so two sites in one account don't collide.");
|
|
265
|
+
p(" // `astroid provision` creates each and fills in its id.");
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
p(" // KV: RL = the security rate limiter. Named for the project, so two apps in");
|
|
269
|
+
p(" // one account don't collide. `astroid provision` creates it and fills in");
|
|
270
|
+
p(" // its id.");
|
|
271
|
+
}
|
|
243
272
|
p(' "kv_namespaces": [');
|
|
244
273
|
p(` { "binding": "RL", "id": "<run: wrangler kv namespace create ${key}-rl>" },`);
|
|
245
|
-
|
|
274
|
+
if (editor) {
|
|
275
|
+
p(` { "binding": "DRAFTS", "id": "<run: wrangler kv namespace create ${key}-drafts>" },`);
|
|
276
|
+
}
|
|
246
277
|
p(" ],");
|
|
247
278
|
// Email Sending. NOT optional decoration: `src/env.d.ts` declares EMAIL as a
|
|
248
279
|
// required member, and Better Auth's magic-link path console-logs the link in
|
|
@@ -251,31 +282,44 @@ export function generateAstroidWrangler(config) {
|
|
|
251
282
|
// sign-in was impossible on every DEPLOYED site, while every local build
|
|
252
283
|
// and every CI scaffold passed. Nothing in this repo runs a deployed scaffold,
|
|
253
284
|
// which is why it survived.
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
285
|
+
if (editor) {
|
|
286
|
+
p(" // Cloudflare Email Sending—magic-link sign-in + inquiry notifications.");
|
|
287
|
+
p(" // Sign-in DEPENDS on this: in production the magic link is emailed, not logged.");
|
|
288
|
+
}
|
|
289
|
+
else if (mail) {
|
|
290
|
+
p(" // Cloudflare Email Sending—the portal's password-reset mail, which is");
|
|
291
|
+
p(" // logged in dev and emailed in production.");
|
|
292
|
+
}
|
|
293
|
+
if (mail) {
|
|
294
|
+
p(" // Enable Email Sending for your zone, then verify the address in MAIL_FROM.");
|
|
295
|
+
p(' "send_email": [{ "name": "EMAIL" }],');
|
|
296
|
+
}
|
|
297
|
+
if (editor) {
|
|
298
|
+
p(" // Public base for media URLs; same-origin keeps media self-contained. Read off");
|
|
299
|
+
p(" // the runtime env by the framework-agnostic media route, so it stays a `var`.");
|
|
300
|
+
}
|
|
260
301
|
p(' "vars": {');
|
|
261
|
-
|
|
302
|
+
if (editor)
|
|
303
|
+
p(` "MEDIA_URL": ${JSON.stringify(mediaBase)},`);
|
|
262
304
|
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
|
-
|
|
305
|
+
if (editor) {
|
|
306
|
+
p(" // The editor allowlist / owner. Wire this into your auth seam (src/auth.ts).");
|
|
307
|
+
p(' "OWNER_EMAIL": "",');
|
|
308
|
+
p(" // AI Gateway for the editor's AI assists: request logs, latency and error");
|
|
309
|
+
p(" // rates, and caching. Empty calls Workers AI directly. Create a gateway,");
|
|
310
|
+
p(" // put its id here, and first say on the privacy page that its log holds");
|
|
311
|
+
p(" // the text editors send to the assists.");
|
|
312
|
+
p(` "${ASTROID_AI_GATEWAY_VAR}": "",`);
|
|
313
|
+
p(" // Edge caching for published pages (ADR 0004). OFF by default, and the");
|
|
314
|
+
p(" // default is the safe state: with it off every render is `no-store` and");
|
|
315
|
+
p(" // the Worker cache layer stores nothing.");
|
|
316
|
+
p(" //");
|
|
317
|
+
p(" // Turn it on for a PREVIEW deploy first and walk the activation runbook");
|
|
318
|
+
p(" // (docs/adr/0004-edge-caching.md). `caches.default` is not cleared by");
|
|
319
|
+
p(" // Cloudflare Dev Mode or Purge Everything, so a bad prod flip is hard to");
|
|
320
|
+
p(" // undo—this feature was reverted twice for exactly that.");
|
|
321
|
+
p(' "ASTROID_EDGE_CACHE": "false",');
|
|
322
|
+
}
|
|
279
323
|
for (const v of astroidCheckoutVars(config)) {
|
|
280
324
|
// Public, not secret—the app id ships to the browser to mount the card
|
|
281
325
|
// field, and the environment is a choice. Keeping them out of the secret
|