astroidjs 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -16
- package/bin/astroid.mjs +35 -12
- package/dist/auth/index.js +2 -2
- package/dist/commerce/checkout-scaffold.d.ts +10 -1
- package/dist/commerce/checkout-scaffold.js +45 -26
- package/dist/commerce/index.d.ts +1 -1
- package/dist/commerce/index.js +1 -1
- package/dist/commerce/roles.d.ts +26 -4
- package/dist/commerce/roles.js +41 -6
- package/dist/commerce/secrets.d.ts +36 -1
- package/dist/commerce/secrets.js +53 -5
- package/dist/config.d.ts +47 -1
- package/dist/config.js +1 -1
- package/dist/email/send.js +6 -1
- package/dist/map/pmtiles.js +1 -1
- package/dist/map/scaffold.js +6 -6
- package/dist/portal/scaffold.js +1 -1
- package/dist/project/generate.js +2 -2
- package/dist/project/scaffold.js +1 -1
- package/dist/pwa/generate.js +12 -2
- package/dist/queues/scaffold.js +1 -1
- package/dist/schema/collections.js +15 -1
- package/dist/worker/generate.js +6 -9
- package/dist/workflow/generate.js +3 -3
- package/package.json +16 -15
- package/src/components/sections.ts +1 -6
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ The whole shape of a project — its brand + theme + editable home, its commerce
|
|
|
33
33
|
backend and optional modules — collapses into one typed config. **One brand per
|
|
34
34
|
project:** every site Astroid targets serves a single brand from a single deploy,
|
|
35
35
|
so the config describes one brand, not an array. What actually multiplexes is
|
|
36
|
-
|
|
36
|
+
_editors_ (Louise's org plugin) and _audiences_ (a gated portal beside the public
|
|
37
37
|
site) — both options on the one brand. The vocabulary is drawn from the real
|
|
38
38
|
sites Astroid targets: a storefront (coracle.coffee), a wholesale front
|
|
39
39
|
(ghostfire.coffee), an artist portfolio (themidwestartist.com), and a plain
|
|
@@ -131,16 +131,16 @@ wait on the work.
|
|
|
131
131
|
Status codes are the only backpressure signal a provider gives you, so each one
|
|
132
132
|
is picked for what it tells the sender to do:
|
|
133
133
|
|
|
134
|
-
| Situation
|
|
135
|
-
|
|
136
|
-
| Secret unprovisioned
|
|
137
|
-
| Bad / missing signature | 401
|
|
138
|
-
| Body isn't JSON
|
|
139
|
-
| Enqueue failed
|
|
140
|
-
| Enqueued
|
|
134
|
+
| Situation | Code | Why |
|
|
135
|
+
| ----------------------- | ---- | --------------------------------------------------------------------------------------------- |
|
|
136
|
+
| Secret unprovisioned | 503 | Dormant is temporary — keep retrying so events delivered before you set the secret still land |
|
|
137
|
+
| Bad / missing signature | 401 | Terminal. It won't verify on retry either, and retrying turns a misconfiguration into a flood |
|
|
138
|
+
| Body isn't JSON | 400 | Terminal for the same reason |
|
|
139
|
+
| Enqueue failed | 503 | The signature checked out, so the event is real — ask for redelivery |
|
|
140
|
+
| Enqueued | 202 | Accepted, not done. That's the point of a queue |
|
|
141
141
|
|
|
142
142
|
On the consumer side `astroidQueueHandler` owns the dispatch every site wrote: a
|
|
143
|
-
periodic refresh re-syncs, a webhook re-syncs
|
|
143
|
+
periodic refresh re-syncs, a webhook re-syncs _only_ if it touched the catalog,
|
|
144
144
|
and everything else acks as a no-op. That last part matters — order and payment
|
|
145
145
|
events arrive in volume and have nothing local to update, so treating them as
|
|
146
146
|
actionable turns a busy sales day into a refresh storm.
|
|
@@ -195,7 +195,7 @@ settings is a site-wide kill switch that beats any page asking to be indexed —
|
|
|
195
195
|
useful for staging.
|
|
196
196
|
|
|
197
197
|
`<StructuredData>` emits a schema.org `@graph`: the business, the `WebSite`, and
|
|
198
|
-
optionally the entity the page is
|
|
198
|
+
optionally the entity the page is _about_ (a Product, a VisualArtwork). The
|
|
199
199
|
business `@type` comes from the archetype (`storefront` → `Store`, `portfolio` →
|
|
200
200
|
`Person`); set `seo.businessType` to a narrower subtype whenever you know one.
|
|
201
201
|
The payload is escaped with `escapeJsonLd`, not `JSON.stringify` — `stringify`
|
|
@@ -219,7 +219,7 @@ email-bombing target, so the tightest budget in the set), the portal's
|
|
|
219
219
|
credential surfaces when `portal.enabled`, checkout when `commerce` is set. The
|
|
220
220
|
session-gated editor API stays out on purpose — a limiter that can lock the
|
|
221
221
|
owner out of their own studio is worse than the abuse it stops. Add or override
|
|
222
|
-
via `security.rateRules`, which is matched
|
|
222
|
+
via `security.rateRules`, which is matched _before_ the defaults, so you replace
|
|
223
223
|
one budget rather than the whole set.
|
|
224
224
|
|
|
225
225
|
**The CSP is composed, and it's split for a reason.** `astroidSecurity(config)`
|
|
@@ -229,7 +229,7 @@ Astroid adds the one hash Astro can't produce itself: Solid's hydration
|
|
|
229
229
|
bootstrap, injected by `@astrojs/solid-js` on every page with an island.
|
|
230
230
|
Computing it from `generateHydrationScript()` means it tracks solid-js upgrades
|
|
231
231
|
instead of going stale as a copy-pasted literal. Meanwhile the generated
|
|
232
|
-
middleware rewrites
|
|
232
|
+
middleware rewrites _only_ `style-src`, because Louise's data-driven `style=""`
|
|
233
233
|
carriers need `'unsafe-inline'` and a single hash in that directive would void
|
|
234
234
|
it per spec — the two cannot share one directive.
|
|
235
235
|
|
|
@@ -240,8 +240,8 @@ import astroidConfig from "./astroid.config.ts";
|
|
|
240
240
|
|
|
241
241
|
export default defineConfig({
|
|
242
242
|
security: astroidSecurity(astroidConfig),
|
|
243
|
-
vite: { build: { ...ASTROID_VITE_BUILD } },
|
|
244
|
-
});
|
|
243
|
+
vite: { build: { ...ASTROID_VITE_BUILD } }, // assetsInlineLimit: 0 — an
|
|
244
|
+
}); // inlined asset can't be hashed
|
|
245
245
|
```
|
|
246
246
|
|
|
247
247
|
Enabled modules contribute their own origins (a commerce provider's SDK hosts,
|
|
@@ -249,8 +249,8 @@ the captcha frame); `security.cspOrigins` adds anything Astroid can't see.
|
|
|
249
249
|
|
|
250
250
|
## Modules are dormant, not broken
|
|
251
251
|
|
|
252
|
-
Astroid's optional modules are opt-in at the
|
|
253
|
-
|
|
252
|
+
Astroid's optional modules are opt-in at the _config_ level, never at the
|
|
253
|
+
_account_ level: switching commerce on must not require a Square account before
|
|
254
254
|
`pnpm dev` will boot. So a module whose secrets aren't provisioned is **dormant**
|
|
255
255
|
— it renders, it serves, it says out loud that it's simulated, and it never calls
|
|
256
256
|
upstream with a dummy credential. A fresh clone runs with zero external accounts.
|
package/bin/astroid.mjs
CHANGED
|
@@ -215,7 +215,7 @@ async function cmdDoctor(cwd, flags) {
|
|
|
215
215
|
else
|
|
216
216
|
err(
|
|
217
217
|
"wrangler.jsonc has no `send_email` binding, but production sign-in emails the " +
|
|
218
|
-
|
|
218
|
+
'magic link (it is only console-logged in dev). Add: "send_email": [{ "name": "EMAIL" }]',
|
|
219
219
|
);
|
|
220
220
|
|
|
221
221
|
if (hasBinding("DB")) ok("wrangler: D1 `DB` binding present");
|
|
@@ -328,7 +328,9 @@ async function cmdAstro(cwd, subcommand, flags, rest) {
|
|
|
328
328
|
await cmdGenerate(cwd, flags, { quiet: true });
|
|
329
329
|
const astroBin = resolveBin(cwd, "astro", "astro");
|
|
330
330
|
if (!astroBin) {
|
|
331
|
-
fail(
|
|
331
|
+
fail(
|
|
332
|
+
"Could not find `astro` in this project. Run inside an Astroid project (with astro installed).",
|
|
333
|
+
);
|
|
332
334
|
}
|
|
333
335
|
const child = spawn(process.execPath, [astroBin, subcommand, ...rest], { stdio: "inherit", cwd });
|
|
334
336
|
child.on("exit", (code) => process.exit(code ?? 0));
|
|
@@ -409,7 +411,9 @@ function provisionPlan(facts) {
|
|
|
409
411
|
/** Look up a just-created resource's id by name via a `… list` JSON command. */
|
|
410
412
|
function lookupId(wranglerBin, cwd, args, pick) {
|
|
411
413
|
try {
|
|
412
|
-
const rows = JSON.parse(
|
|
414
|
+
const rows = JSON.parse(
|
|
415
|
+
execFileSync(process.execPath, [wranglerBin, ...args], { cwd, encoding: "utf8" }),
|
|
416
|
+
);
|
|
413
417
|
return pick(Array.isArray(rows) ? rows : []) ?? null;
|
|
414
418
|
} catch {
|
|
415
419
|
return null;
|
|
@@ -453,7 +457,9 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
453
457
|
out(" Secrets: wrangler secret put SESSION_SECRET (prompted)");
|
|
454
458
|
out(" Deploy: wrangler deploy\n");
|
|
455
459
|
if (!facts.hasAccount) {
|
|
456
|
-
out(
|
|
460
|
+
out(
|
|
461
|
+
" ! No account_id set — uncomment it in wrangler.jsonc or export CLOUDFLARE_ACCOUNT_ID.\n",
|
|
462
|
+
);
|
|
457
463
|
}
|
|
458
464
|
|
|
459
465
|
if (dryRun) {
|
|
@@ -464,10 +470,14 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
464
470
|
// Gate the irreversible work behind a clear yes.
|
|
465
471
|
if (!assumeYes) {
|
|
466
472
|
if (!process.stdin.isTTY) {
|
|
467
|
-
fail(
|
|
473
|
+
fail(
|
|
474
|
+
"Refusing to provision + deploy non-interactively. Re-run with --yes (or --dry-run to preview).",
|
|
475
|
+
);
|
|
468
476
|
}
|
|
469
477
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
470
|
-
const answer = (await rl.question("Provision the above, then migrate + deploy? [y/N] "))
|
|
478
|
+
const answer = (await rl.question("Provision the above, then migrate + deploy? [y/N] "))
|
|
479
|
+
.trim()
|
|
480
|
+
.toLowerCase();
|
|
471
481
|
rl.close();
|
|
472
482
|
if (answer !== "y" && answer !== "yes") {
|
|
473
483
|
out("Aborted.");
|
|
@@ -477,7 +487,8 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
477
487
|
|
|
478
488
|
const wranglerBin = resolveBin(cwd, "wrangler", "wrangler");
|
|
479
489
|
if (!wranglerBin) fail("Could not find `wrangler` in this project (add it as a devDependency).");
|
|
480
|
-
const runInherit = (args) =>
|
|
490
|
+
const runInherit = (args) =>
|
|
491
|
+
spawnSync(process.execPath, [wranglerBin, ...args], { cwd, stdio: "inherit" });
|
|
481
492
|
|
|
482
493
|
// 1) Provision, patching discovered ids back into wrangler.jsonc.
|
|
483
494
|
for (const s of plan) {
|
|
@@ -490,17 +501,28 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
490
501
|
}
|
|
491
502
|
|
|
492
503
|
if (s.kind === "d1") {
|
|
493
|
-
const id = lookupId(
|
|
504
|
+
const id = lookupId(
|
|
505
|
+
wranglerBin,
|
|
506
|
+
cwd,
|
|
507
|
+
["d1", "list", "--json"],
|
|
508
|
+
(rows) => rows.find((r) => r.name === s.name)?.uuid,
|
|
509
|
+
);
|
|
494
510
|
if (id) {
|
|
495
|
-
wrangler = wrangler.replace(
|
|
511
|
+
wrangler = wrangler.replace(
|
|
512
|
+
/"database_id":\s*"<[^"]*>"/,
|
|
513
|
+
`"database_id": ${JSON.stringify(id)}`,
|
|
514
|
+
);
|
|
496
515
|
writeFileSync(wranglerPath, wrangler);
|
|
497
516
|
out(` ↳ database_id = ${id}`);
|
|
498
517
|
} else {
|
|
499
518
|
out(" ↳ couldn't auto-detect the id — fill database_id in wrangler.jsonc by hand.");
|
|
500
519
|
}
|
|
501
520
|
} else if (s.kind === "kv") {
|
|
502
|
-
const id = lookupId(
|
|
503
|
-
|
|
521
|
+
const id = lookupId(
|
|
522
|
+
wranglerBin,
|
|
523
|
+
cwd,
|
|
524
|
+
["kv", "namespace", "list"],
|
|
525
|
+
(rows) => rows.find((r) => typeof r.title === "string" && r.title.endsWith(s.name))?.id,
|
|
504
526
|
);
|
|
505
527
|
if (id) {
|
|
506
528
|
wrangler = patchKvId(wrangler, s.name, id);
|
|
@@ -514,7 +536,8 @@ async function cmdDeploy(cwd, flags, rest) {
|
|
|
514
536
|
|
|
515
537
|
// 2) Migrations.
|
|
516
538
|
out(`\n▸ wrangler d1 migrations apply DB ${remoteArgs.join(" ")}`.trimEnd());
|
|
517
|
-
if (runInherit(["d1", "migrations", "apply", "DB", ...remoteArgs]).status !== 0)
|
|
539
|
+
if (runInherit(["d1", "migrations", "apply", "DB", ...remoteArgs]).status !== 0)
|
|
540
|
+
fail("Migrations failed.");
|
|
518
541
|
|
|
519
542
|
// 3) Secrets (interactive; wrangler prompts for the value).
|
|
520
543
|
out("\n▸ wrangler secret put SESSION_SECRET");
|
package/dist/auth/index.js
CHANGED
|
@@ -50,10 +50,10 @@ export function assertAuthIsolation(config) {
|
|
|
50
50
|
return;
|
|
51
51
|
if (!portal.cookiePrefix || portal.cookiePrefix === ASTROID_EDITOR_COOKIE_PREFIX) {
|
|
52
52
|
throw new AstroidConfigError(`portal.cookiePrefix must be set and distinct from the editor's (${JSON.stringify(ASTROID_EDITOR_COOKIE_PREFIX)}). Two instances sharing a cookie prefix silently sign you out of one when ` +
|
|
53
|
-
|
|
53
|
+
'you sign into the other. Pick a project-specific prefix (e.g. "acme_shop").');
|
|
54
54
|
}
|
|
55
55
|
if (portal.tablePrefix === ASTROID_EDITOR_TABLE_PREFIX) {
|
|
56
56
|
throw new AstroidConfigError(`portal.tablePrefix must differ from the editor's (${JSON.stringify(ASTROID_EDITOR_TABLE_PREFIX)}) — a shared table prefix merges the two instances into one user table. ` +
|
|
57
|
-
|
|
57
|
+
"Leave it unset (the unprefixed `user`/`session` tables) or use a distinct prefix.");
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -23,13 +23,22 @@ export declare function generateAstroidCheckoutRoute(config: AstroidConfig): str
|
|
|
23
23
|
*/
|
|
24
24
|
export declare function generateAstroidSquareCard(config: AstroidConfig): string | null;
|
|
25
25
|
/**
|
|
26
|
-
* The wrangler `vars`
|
|
26
|
+
* The wrangler `vars` Square needs, or `[]`.
|
|
27
27
|
*
|
|
28
28
|
* PUBLIC values, so they are vars rather than entries in the secret roster: the
|
|
29
29
|
* application id is shipped to the browser by design, and the environment is a
|
|
30
30
|
* choice, not a credential. Putting them in `credentials` would also fold them
|
|
31
31
|
* into the dormancy gate, which is about whether the module can safely CALL
|
|
32
32
|
* Square — a different question from whether the card field can render.
|
|
33
|
+
*
|
|
34
|
+
* The two vars are gated SEPARATELY, and that split is load-bearing.
|
|
35
|
+
* `SQUARE_ENVIRONMENT` selects the API HOST for every Square call, so it belongs
|
|
36
|
+
* to any project that talks to Square at all; `SQUARE_APP_ID` only mounts the
|
|
37
|
+
* browser card field. Gating both on card checkout — as this did until
|
|
38
|
+
* `invoicing: "square"` became expressible — left a site that runs Square for
|
|
39
|
+
* invoicing alone with no `SQUARE_ENVIRONMENT`, and `SquareConfig.environment`
|
|
40
|
+
* defaults to "sandbox". Every production invoice would have been created
|
|
41
|
+
* against the sandbox: no error, no warning, just money that never arrives.
|
|
33
42
|
*/
|
|
34
43
|
export declare function astroidCheckoutVars(config: AstroidConfig): {
|
|
35
44
|
name: string;
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// redirects to its own hosted checkout (no card token to charge) and Stripe has
|
|
21
21
|
// no catalog API, so it fills the `invoicing` role rather than `storefront`.
|
|
22
22
|
import { astroidCatalogMirror } from "./mirror.js";
|
|
23
|
-
import { astroidCommerceRoles } from "./roles.js";
|
|
23
|
+
import { astroidCommerceProviders, astroidCommerceRoles } from "./roles.js";
|
|
24
24
|
/** Does this project take card payments in-page? Square storefront only. */
|
|
25
25
|
export function usesCardCheckout(config) {
|
|
26
26
|
return astroidCommerceRoles(config.commerce).storefront === "square";
|
|
@@ -65,7 +65,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
65
65
|
" resolveCommerceStatus,",
|
|
66
66
|
" type SecretSource,",
|
|
67
67
|
" verifyCheckout,",
|
|
68
|
-
|
|
68
|
+
'} from "astroidjs";',
|
|
69
69
|
'import { createPayment } from "louise-toolkit/commerce/square";',
|
|
70
70
|
'import { isSameOrigin } from "louise-toolkit/security";',
|
|
71
71
|
'import astroidConfig from "../../../astroid.config.js";',
|
|
@@ -106,7 +106,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
106
106
|
" // an Origin/Referer matching the host; a non-browser caller (a stripped",
|
|
107
107
|
" // header) is refused. If you deliberately serve checkout from another origin,",
|
|
108
108
|
" // this is the line to relax — it's yours.",
|
|
109
|
-
|
|
109
|
+
' if (!isSameOrigin(request)) return json({ error: "Forbidden" }, 403);',
|
|
110
110
|
"",
|
|
111
111
|
" const body = (await request.json().catch(() => null)) as {",
|
|
112
112
|
" lines?: unknown;",
|
|
@@ -115,15 +115,15 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
115
115
|
" verificationToken?: unknown;",
|
|
116
116
|
" email?: unknown;",
|
|
117
117
|
" } | null;",
|
|
118
|
-
|
|
118
|
+
' if (!body) return json({ error: "Invalid JSON" }, 400);',
|
|
119
119
|
"",
|
|
120
120
|
" // The cart id is the identity half of the idempotency key. It must be high",
|
|
121
121
|
" // entropy and client-generated ONCE per cart (a v4 uuid in localStorage):",
|
|
122
122
|
" // regenerate it per request and a double-click charges twice; make it",
|
|
123
123
|
" // guessable and someone else's identical cart can dedupe into your charge.",
|
|
124
|
-
|
|
124
|
+
' const cartId = typeof body.cartId === "string" ? body.cartId : "";',
|
|
125
125
|
" if (!/^[0-9a-f-]{36}$/i.test(cartId)) {",
|
|
126
|
-
|
|
126
|
+
' return json({ error: "A uuid `cartId` is required" }, 400);',
|
|
127
127
|
" }",
|
|
128
128
|
"",
|
|
129
129
|
" // 1 + 2: re-price and refuse on mismatch.",
|
|
@@ -153,12 +153,12 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
153
153
|
" });",
|
|
154
154
|
" }",
|
|
155
155
|
"",
|
|
156
|
-
|
|
156
|
+
' const sourceId = typeof body.sourceId === "string" ? body.sourceId : "";',
|
|
157
157
|
' if (!sourceId) return json({ error: "A card token (`sourceId`) is required" }, 400);',
|
|
158
158
|
"",
|
|
159
159
|
" // Both are guaranteed real by the dormancy gate above; the narrowing here",
|
|
160
160
|
" // is for the type system, which can't know that. `environment` is a UNION",
|
|
161
|
-
|
|
161
|
+
' // ("sandbox" | "production"), not a free string — an unrecognised value',
|
|
162
162
|
" // would otherwise silently select the sandbox host in production.",
|
|
163
163
|
' const environment = env.SQUARE_ENVIRONMENT === "production" ? "production" : "sandbox";',
|
|
164
164
|
" const payment = await createPayment(",
|
|
@@ -172,7 +172,7 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
172
172
|
' amountMoney: { amount: check.subtotalCents, currency: "USD" },',
|
|
173
173
|
' locationId: env.SQUARE_LOCATION_ID ?? "",',
|
|
174
174
|
" idempotencyKey,",
|
|
175
|
-
|
|
175
|
+
' ...(typeof body.verificationToken === "string"',
|
|
176
176
|
" ? { verificationToken: body.verificationToken }",
|
|
177
177
|
" : {}),",
|
|
178
178
|
' ...(typeof body.email === "string" ? { buyerEmailAddress: body.email } : {}),',
|
|
@@ -270,37 +270,56 @@ export function generateAstroidSquareCard(config) {
|
|
|
270
270
|
"",
|
|
271
271
|
].join("\n");
|
|
272
272
|
}
|
|
273
|
+
/** Does this project talk to Square in ANY role — storefront, invoicing, or
|
|
274
|
+
* otherwise? Distinct from {@link usesCardCheckout}, which asks the narrower
|
|
275
|
+
* question of whether the in-page card field renders. */
|
|
276
|
+
function usesSquare(config) {
|
|
277
|
+
return astroidCommerceProviders(config.commerce).includes("square");
|
|
278
|
+
}
|
|
273
279
|
/**
|
|
274
|
-
* The wrangler `vars`
|
|
280
|
+
* The wrangler `vars` Square needs, or `[]`.
|
|
275
281
|
*
|
|
276
282
|
* PUBLIC values, so they are vars rather than entries in the secret roster: the
|
|
277
283
|
* application id is shipped to the browser by design, and the environment is a
|
|
278
284
|
* choice, not a credential. Putting them in `credentials` would also fold them
|
|
279
285
|
* into the dormancy gate, which is about whether the module can safely CALL
|
|
280
286
|
* Square — a different question from whether the card field can render.
|
|
287
|
+
*
|
|
288
|
+
* The two vars are gated SEPARATELY, and that split is load-bearing.
|
|
289
|
+
* `SQUARE_ENVIRONMENT` selects the API HOST for every Square call, so it belongs
|
|
290
|
+
* to any project that talks to Square at all; `SQUARE_APP_ID` only mounts the
|
|
291
|
+
* browser card field. Gating both on card checkout — as this did until
|
|
292
|
+
* `invoicing: "square"` became expressible — left a site that runs Square for
|
|
293
|
+
* invoicing alone with no `SQUARE_ENVIRONMENT`, and `SquareConfig.environment`
|
|
294
|
+
* defaults to "sandbox". Every production invoice would have been created
|
|
295
|
+
* against the sandbox: no error, no warning, just money that never arrives.
|
|
281
296
|
*/
|
|
282
297
|
export function astroidCheckoutVars(config) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
298
|
+
const vars = [];
|
|
299
|
+
// Browser-only, so card checkout is genuinely the right gate here.
|
|
300
|
+
if (usesCardCheckout(config)) {
|
|
286
301
|
// Public app id from the Square dashboard (Developer → Credentials).
|
|
287
|
-
{ name: "SQUARE_APP_ID", value: "" }
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
302
|
+
vars.push({ name: "SQUARE_APP_ID", value: "" });
|
|
303
|
+
}
|
|
304
|
+
if (usesSquare(config)) {
|
|
305
|
+
// "sandbox" until you have tested a real payment end to end.
|
|
306
|
+
vars.push({ name: "SQUARE_ENVIRONMENT", value: "sandbox" });
|
|
307
|
+
}
|
|
308
|
+
return vars;
|
|
291
309
|
}
|
|
292
310
|
/**
|
|
293
311
|
* The `CloudflareEnv` members the card input adds, as a block `create-astroid`
|
|
294
312
|
* substitutes into `src/env.d.ts`. Empty without card checkout.
|
|
295
313
|
*/
|
|
296
314
|
export function generateAstroidCheckoutEnv(config) {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
" * Web Payments card field. Not a secret; see wrangler.jsonc `vars`. */",
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
" SQUARE_ENVIRONMENT: string;"
|
|
305
|
-
|
|
315
|
+
// Mirrors the gating in `astroidCheckoutVars` — the app id is card-checkout
|
|
316
|
+
// only, the environment belongs to any project that calls Square at all.
|
|
317
|
+
const lines = [];
|
|
318
|
+
if (usesCardCheckout(config)) {
|
|
319
|
+
lines.push(" /** Square's PUBLIC application id — shipped to the browser to mount the", " * Web Payments card field. Not a secret; see wrangler.jsonc `vars`. */", " SQUARE_APP_ID: string;");
|
|
320
|
+
}
|
|
321
|
+
if (usesSquare(config)) {
|
|
322
|
+
lines.push(' /** Square API environment: "sandbox" or "production". Selects the API', " * host for EVERY Square call, so it is required for invoicing too. */", " SQUARE_ENVIRONMENT: string;");
|
|
323
|
+
}
|
|
324
|
+
return lines.join("\n");
|
|
306
325
|
}
|
package/dist/commerce/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { catalogNormalizer, type FourthwallProductLike, fourthwallToCatalogItem,
|
|
|
2
2
|
export { type CheckoutVerification, checkoutIdempotencyKey, type ClientLine, type PriceLookup, verifyCheckout, type VerifiedLine, } from "./checkout.js";
|
|
3
3
|
export { astroidCatalogLoaderConfig, type CatalogDatabase, type CatalogProduct, type CatalogReadOptions, readCatalog, readCatalogItem, } from "./loader.js";
|
|
4
4
|
export { astroidCatalogMirror, BUILT_IN_OWNED, type CatalogMirrorConfig, generateCatalogMigrationSql, generateCatalogTable, type OwnedColumn, PULLED_COLUMNS, } from "./mirror.js";
|
|
5
|
-
export { COMMERCE_PROVIDER_SECRETS, type CommerceStatus, commerceSecretNames, type ProviderStatus, resolveCommerceStatus, } from "./secrets.js";
|
|
5
|
+
export { COMMERCE_PROVIDER_SECRETS, type CommerceStatus, commerceSecretNames, type ProviderStatus, providerConfigured, resolveCommerceStatus, roleConfigured, } from "./secrets.js";
|
|
6
6
|
export { assertCommerceRoles, astroidCommerceProviders, astroidCommerceRoles, type CommerceRole, hasStorefront, PROVIDER_ROLES, type ResolvedCommerceRoles, } from "./roles.js";
|
|
7
7
|
export { astroidCatalogSync, astroidCatalogUpsert, type CatalogItem, type CatalogSyncOptions, type CatalogSyncResult, defaultSlug, type SyncDatabase, } from "./sync.js";
|
|
8
8
|
export { astroidCheckoutVars, generateAstroidCheckoutEnv, generateAstroidCheckoutRoute, generateAstroidSquareCard, usesCardCheckout, } from "./checkout-scaffold.js";
|
package/dist/commerce/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { catalogNormalizer, fourthwallToCatalogItem, squareToCatalogItem, } from
|
|
|
3
3
|
export { checkoutIdempotencyKey, verifyCheckout, } from "./checkout.js";
|
|
4
4
|
export { astroidCatalogLoaderConfig, readCatalog, readCatalogItem, } from "./loader.js";
|
|
5
5
|
export { astroidCatalogMirror, BUILT_IN_OWNED, generateCatalogMigrationSql, generateCatalogTable, PULLED_COLUMNS, } from "./mirror.js";
|
|
6
|
-
export { COMMERCE_PROVIDER_SECRETS, commerceSecretNames, resolveCommerceStatus, } from "./secrets.js";
|
|
6
|
+
export { COMMERCE_PROVIDER_SECRETS, commerceSecretNames, providerConfigured, resolveCommerceStatus, roleConfigured, } from "./secrets.js";
|
|
7
7
|
export { assertCommerceRoles, astroidCommerceProviders, astroidCommerceRoles, hasStorefront, PROVIDER_ROLES, } from "./roles.js";
|
|
8
8
|
export { astroidCatalogSync, astroidCatalogUpsert, defaultSlug, } from "./sync.js";
|
|
9
9
|
export { astroidCheckoutVars, generateAstroidCheckoutEnv, generateAstroidCheckoutRoute, generateAstroidSquareCard, usesCardCheckout, } from "./checkout-scaffold.js";
|
package/dist/commerce/roles.d.ts
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
import type { CommerceConfig, CommerceProvider } from "../config.js";
|
|
2
2
|
/** What a provider is being used FOR. */
|
|
3
|
-
export type CommerceRole = "storefront" | "invoicing";
|
|
3
|
+
export type CommerceRole = "storefront" | "invoicing" | "pos";
|
|
4
4
|
/**
|
|
5
5
|
* Which roles each provider can serve, derived from the surface its
|
|
6
6
|
* `louise-toolkit/commerce/*` client actually exposes — not from what the
|
|
7
7
|
* vendor's full API could theoretically do.
|
|
8
8
|
*
|
|
9
|
-
* square catalog + orders + payments,
|
|
9
|
+
* square catalog + orders + payments, `createInvoice`/`publishInvoice`,
|
|
10
|
+
* AND locations + per-location price overrides + inventory counts
|
|
10
11
|
* stripe invoices + payment intents; NO catalog
|
|
11
|
-
* fourthwall catalog + cart; NO invoicing
|
|
12
|
+
* fourthwall catalog + cart; NO invoicing, and NO locations or inventory —
|
|
13
|
+
* its Platform API is create-only for products, so it cannot
|
|
14
|
+
* model stock held at a place
|
|
15
|
+
*
|
|
16
|
+
* Square alone can serve `pos`, and that is a fact about the clients rather than
|
|
17
|
+
* a preference: `pos` needs `listLocations`, `location_overrides` and
|
|
18
|
+
* `batchChangeInventory`, none of which the other two clients expose.
|
|
12
19
|
*/
|
|
13
20
|
export declare const PROVIDER_ROLES: Record<CommerceProvider, readonly CommerceRole[]>;
|
|
14
|
-
/** The providers filling each role.
|
|
21
|
+
/** The providers filling each role. Any may be absent. */
|
|
15
22
|
export interface ResolvedCommerceRoles {
|
|
16
23
|
storefront?: CommerceProvider;
|
|
17
24
|
invoicing?: CommerceProvider;
|
|
25
|
+
pos?: CommerceProvider;
|
|
18
26
|
}
|
|
19
27
|
/**
|
|
20
28
|
* Resolve a `commerce` block into role assignments.
|
|
@@ -27,6 +35,20 @@ export interface ResolvedCommerceRoles {
|
|
|
27
35
|
export declare function astroidCommerceRoles(commerce: CommerceConfig | undefined): ResolvedCommerceRoles;
|
|
28
36
|
/** Every distinct provider this project talks to, in a stable order. */
|
|
29
37
|
export declare function astroidCommerceProviders(commerce: CommerceConfig | undefined): CommerceProvider[];
|
|
38
|
+
/**
|
|
39
|
+
* True when this project sells in person — the switch for locations,
|
|
40
|
+
* per-location pricing and inventory.
|
|
41
|
+
*/
|
|
42
|
+
export declare const hasPos: (commerce: CommerceConfig | undefined) => boolean;
|
|
43
|
+
/**
|
|
44
|
+
* True when Square is used with more than one Location, i.e. the location id
|
|
45
|
+
* comes from the request rather than the environment.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately independent of which ROLE Square fills: a project could run
|
|
48
|
+
* multi-location invoicing without a `pos` storefront, and the consequence —
|
|
49
|
+
* no ambient `SQUARE_LOCATION_ID` — is the same either way.
|
|
50
|
+
*/
|
|
51
|
+
export declare const hasMultiLocation: (commerce: CommerceConfig | undefined) => boolean;
|
|
30
52
|
/** True when this project sells anything at all. */
|
|
31
53
|
export declare const hasStorefront: (commerce: CommerceConfig | undefined) => boolean;
|
|
32
54
|
/**
|
package/dist/commerce/roles.js
CHANGED
|
@@ -21,12 +21,19 @@ import { AstroidConfigError } from "../errors.js";
|
|
|
21
21
|
* `louise-toolkit/commerce/*` client actually exposes — not from what the
|
|
22
22
|
* vendor's full API could theoretically do.
|
|
23
23
|
*
|
|
24
|
-
* square catalog + orders + payments,
|
|
24
|
+
* square catalog + orders + payments, `createInvoice`/`publishInvoice`,
|
|
25
|
+
* AND locations + per-location price overrides + inventory counts
|
|
25
26
|
* stripe invoices + payment intents; NO catalog
|
|
26
|
-
* fourthwall catalog + cart; NO invoicing
|
|
27
|
+
* fourthwall catalog + cart; NO invoicing, and NO locations or inventory —
|
|
28
|
+
* its Platform API is create-only for products, so it cannot
|
|
29
|
+
* model stock held at a place
|
|
30
|
+
*
|
|
31
|
+
* Square alone can serve `pos`, and that is a fact about the clients rather than
|
|
32
|
+
* a preference: `pos` needs `listLocations`, `location_overrides` and
|
|
33
|
+
* `batchChangeInventory`, none of which the other two clients expose.
|
|
27
34
|
*/
|
|
28
35
|
export const PROVIDER_ROLES = {
|
|
29
|
-
square: ["storefront", "invoicing"],
|
|
36
|
+
square: ["storefront", "invoicing", "pos"],
|
|
30
37
|
stripe: ["invoicing"],
|
|
31
38
|
fourthwall: ["storefront"],
|
|
32
39
|
};
|
|
@@ -52,13 +59,29 @@ export function astroidCommerceRoles(commerce) {
|
|
|
52
59
|
roles.storefront = commerce.storefront;
|
|
53
60
|
if (commerce.invoicing)
|
|
54
61
|
roles.invoicing = commerce.invoicing;
|
|
62
|
+
if (commerce.pos)
|
|
63
|
+
roles.pos = commerce.pos;
|
|
55
64
|
return roles;
|
|
56
65
|
}
|
|
57
66
|
/** Every distinct provider this project talks to, in a stable order. */
|
|
58
67
|
export function astroidCommerceProviders(commerce) {
|
|
59
|
-
const { storefront, invoicing } = astroidCommerceRoles(commerce);
|
|
60
|
-
return [...new Set([storefront, invoicing].filter((p) => !!p))];
|
|
68
|
+
const { storefront, invoicing, pos } = astroidCommerceRoles(commerce);
|
|
69
|
+
return [...new Set([storefront, invoicing, pos].filter((p) => !!p))];
|
|
61
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* True when this project sells in person — the switch for locations,
|
|
73
|
+
* per-location pricing and inventory.
|
|
74
|
+
*/
|
|
75
|
+
export const hasPos = (commerce) => Boolean(astroidCommerceRoles(commerce).pos);
|
|
76
|
+
/**
|
|
77
|
+
* True when Square is used with more than one Location, i.e. the location id
|
|
78
|
+
* comes from the request rather than the environment.
|
|
79
|
+
*
|
|
80
|
+
* Deliberately independent of which ROLE Square fills: a project could run
|
|
81
|
+
* multi-location invoicing without a `pos` storefront, and the consequence —
|
|
82
|
+
* no ambient `SQUARE_LOCATION_ID` — is the same either way.
|
|
83
|
+
*/
|
|
84
|
+
export const hasMultiLocation = (commerce) => commerce?.square?.locations === "multi";
|
|
62
85
|
/** True when this project sells anything at all. */
|
|
63
86
|
export const hasStorefront = (commerce) => Boolean(astroidCommerceRoles(commerce).storefront);
|
|
64
87
|
/**
|
|
@@ -81,7 +104,12 @@ export function assertCommerceRoles(commerce) {
|
|
|
81
104
|
known(provider);
|
|
82
105
|
if (!PROVIDER_ROLES[provider].includes(role)) {
|
|
83
106
|
const able = Object.keys(PROVIDER_ROLES).filter((p) => PROVIDER_ROLES[p].includes(role));
|
|
84
|
-
|
|
107
|
+
const missing = {
|
|
108
|
+
storefront: "catalog",
|
|
109
|
+
invoicing: "invoicing",
|
|
110
|
+
pos: "locations/inventory",
|
|
111
|
+
}[role];
|
|
112
|
+
throw new AstroidConfigError(`commerce: ${provider} can't serve the "${role}" role — its louise-toolkit client has no ${missing} API. Providers that can: ${able.join(", ")}.`);
|
|
85
113
|
}
|
|
86
114
|
};
|
|
87
115
|
// The shorthand assigns itself to a role it can serve, so it only has to be a
|
|
@@ -90,4 +118,11 @@ export function assertCommerceRoles(commerce) {
|
|
|
90
118
|
known(commerce.provider);
|
|
91
119
|
check("storefront", commerce.storefront);
|
|
92
120
|
check("invoicing", commerce.invoicing);
|
|
121
|
+
check("pos", commerce.pos);
|
|
122
|
+
// `square.locations` only means something if Square is actually in play.
|
|
123
|
+
// Silently ignoring it would let a typo'd config look like it had opted into
|
|
124
|
+
// multi-location when nothing reads the setting.
|
|
125
|
+
if (commerce.square && !astroidCommerceProviders(commerce).includes("square")) {
|
|
126
|
+
throw new AstroidConfigError('commerce.square is set but no role is assigned to Square. Assign it to "storefront", "invoicing" or "pos", or drop the square block.');
|
|
127
|
+
}
|
|
93
128
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CommerceConfig, CommerceProvider } from "../config.js";
|
|
2
2
|
import { type ModuleSecrets, type SecretSource } from "../secrets.js";
|
|
3
|
+
import { type CommerceRole } from "./roles.js";
|
|
3
4
|
/**
|
|
4
5
|
* Per-provider secret names, split by what they gate.
|
|
5
6
|
*
|
|
@@ -25,6 +26,21 @@ export declare const COMMERCE_PROVIDER_SECRETS: Record<CommerceProvider, {
|
|
|
25
26
|
* do, only that something is missing.
|
|
26
27
|
*/
|
|
27
28
|
export declare const COMMERCE_PROVIDER_SETUP: Record<CommerceProvider, string>;
|
|
29
|
+
/**
|
|
30
|
+
* The credentials a provider needs **for this project's configuration**.
|
|
31
|
+
*
|
|
32
|
+
* Only Square varies, and only on one name. `SQUARE_LOCATION_ID` is required
|
|
33
|
+
* for a single-location project because Square's orders and payments endpoints
|
|
34
|
+
* refuse a request without one. Under `square.locations: "multi"` the location
|
|
35
|
+
* is a property of the *request* — which merchant's storefront is this? — so an
|
|
36
|
+
* ambient id is not just unnecessary, it is dangerous: any code path that
|
|
37
|
+
* defaulted to it would ring one merchant's sale against another merchant's
|
|
38
|
+
* books, and the sale would look perfectly successful while doing it.
|
|
39
|
+
*
|
|
40
|
+
* So it is dropped from the gate entirely rather than left optional. A name that
|
|
41
|
+
* is present-but-ignored is the kind of thing someone later "fixes" by using it.
|
|
42
|
+
*/
|
|
43
|
+
export declare function commerceProviderCredentials(provider: CommerceProvider, commerce: CommerceConfig | undefined): readonly string[];
|
|
28
44
|
/**
|
|
29
45
|
* Every secret name this project's commerce configuration needs, deduplicated
|
|
30
46
|
* and in a stable order.
|
|
@@ -38,7 +54,7 @@ export declare function commerceSecretNames(commerce: CommerceConfig | undefined
|
|
|
38
54
|
export interface ProviderStatus {
|
|
39
55
|
provider: CommerceProvider;
|
|
40
56
|
/** Which role(s) this provider fills for the project. */
|
|
41
|
-
roles:
|
|
57
|
+
roles: CommerceRole[];
|
|
42
58
|
/** API credentials — false means no live call can be made. */
|
|
43
59
|
credentials: ModuleSecrets<string>;
|
|
44
60
|
/** Webhook signing secret — false means the receiver answers 503. */
|
|
@@ -72,3 +88,22 @@ export interface CommerceStatus {
|
|
|
72
88
|
* reason the mirror exists as a separate layer from the provider client.
|
|
73
89
|
*/
|
|
74
90
|
export declare function resolveCommerceStatus(commerce: CommerceConfig | undefined, env: Record<string, SecretSource>): Promise<CommerceStatus>;
|
|
91
|
+
/**
|
|
92
|
+
* Is THIS provider live?
|
|
93
|
+
*
|
|
94
|
+
* `CommerceStatus.configured` is an all-or-nothing aggregate (`every`), which is
|
|
95
|
+
* the right answer for "is the whole module ready" and the wrong one for gating
|
|
96
|
+
* a single call site. A two-provider project — Fourthwall for the storefront,
|
|
97
|
+
* Square for invoicing — reads `configured: false` the moment either half is
|
|
98
|
+
* unprovisioned, so gating the working Fourthwall checkout on the aggregate
|
|
99
|
+
* would silently simulate it because SQUARE's secrets are still placeholders.
|
|
100
|
+
*
|
|
101
|
+
* Gate each surface on the provider it actually calls.
|
|
102
|
+
*/
|
|
103
|
+
export declare function providerConfigured(status: CommerceStatus, provider: CommerceProvider): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Is the provider filling this ROLE live? The role-shaped question, for code
|
|
106
|
+
* that cares about the capability rather than the vendor — "can I take a
|
|
107
|
+
* checkout?" rather than "is Square up?".
|
|
108
|
+
*/
|
|
109
|
+
export declare function roleConfigured(status: CommerceStatus, role: CommerceRole): boolean;
|
package/dist/commerce/secrets.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// `queues/scaffold.ts`, because it is the same fact: a provider's secret set.
|
|
21
21
|
// The scaffold imports it from here so the two can't drift.
|
|
22
22
|
import { resolveModuleSecrets } from "../secrets.js";
|
|
23
|
-
import { astroidCommerceProviders, astroidCommerceRoles } from "./roles.js";
|
|
23
|
+
import { astroidCommerceProviders, astroidCommerceRoles, hasMultiLocation, } from "./roles.js";
|
|
24
24
|
/**
|
|
25
25
|
* Per-provider secret names, split by what they gate.
|
|
26
26
|
*
|
|
@@ -38,6 +38,8 @@ export const COMMERCE_PROVIDER_SECRETS = {
|
|
|
38
38
|
// payments endpoints refuse a request without one, so a token on its own
|
|
39
39
|
// leaves checkout broken rather than dormant. Requiring both is what makes
|
|
40
40
|
// "configured" mean "can actually take money".
|
|
41
|
+
//
|
|
42
|
+
// UNLESS the project is multi-location — see `commerceProviderCredentials`.
|
|
41
43
|
square: {
|
|
42
44
|
credentials: ["SQUARE_ACCESS_TOKEN", "SQUARE_LOCATION_ID"],
|
|
43
45
|
webhook: "SQUARE_WEBHOOK_SECRET",
|
|
@@ -67,6 +69,27 @@ export const COMMERCE_PROVIDER_SETUP = {
|
|
|
67
69
|
stripe: "dashboard.stripe.com → Developers → API keys (secret key). Webhook secret: Developers → Webhooks → your endpoint → Signing secret.",
|
|
68
70
|
fourthwall: "Fourthwall dashboard → Settings → For developers → Storefront token. Webhook secret: the same page → Webhooks.",
|
|
69
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* The credentials a provider needs **for this project's configuration**.
|
|
74
|
+
*
|
|
75
|
+
* Only Square varies, and only on one name. `SQUARE_LOCATION_ID` is required
|
|
76
|
+
* for a single-location project because Square's orders and payments endpoints
|
|
77
|
+
* refuse a request without one. Under `square.locations: "multi"` the location
|
|
78
|
+
* is a property of the *request* — which merchant's storefront is this? — so an
|
|
79
|
+
* ambient id is not just unnecessary, it is dangerous: any code path that
|
|
80
|
+
* defaulted to it would ring one merchant's sale against another merchant's
|
|
81
|
+
* books, and the sale would look perfectly successful while doing it.
|
|
82
|
+
*
|
|
83
|
+
* So it is dropped from the gate entirely rather than left optional. A name that
|
|
84
|
+
* is present-but-ignored is the kind of thing someone later "fixes" by using it.
|
|
85
|
+
*/
|
|
86
|
+
export function commerceProviderCredentials(provider, commerce) {
|
|
87
|
+
const spec = COMMERCE_PROVIDER_SECRETS[provider];
|
|
88
|
+
if (provider === "square" && hasMultiLocation(commerce)) {
|
|
89
|
+
return spec.credentials.filter((name) => name !== "SQUARE_LOCATION_ID");
|
|
90
|
+
}
|
|
91
|
+
return spec.credentials;
|
|
92
|
+
}
|
|
70
93
|
/**
|
|
71
94
|
* Every secret name this project's commerce configuration needs, deduplicated
|
|
72
95
|
* and in a stable order.
|
|
@@ -77,17 +100,19 @@ export const COMMERCE_PROVIDER_SETUP = {
|
|
|
77
100
|
*/
|
|
78
101
|
export function commerceSecretNames(commerce) {
|
|
79
102
|
const names = astroidCommerceProviders(commerce).flatMap((provider) => [
|
|
80
|
-
...
|
|
103
|
+
...commerceProviderCredentials(provider, commerce),
|
|
81
104
|
COMMERCE_PROVIDER_SECRETS[provider].webhook,
|
|
82
105
|
]);
|
|
83
106
|
return [...new Set(names)];
|
|
84
107
|
}
|
|
85
108
|
/** Read one provider's secrets off an env-shaped record. */
|
|
86
|
-
async function resolveProvider(provider, roles, env) {
|
|
109
|
+
async function resolveProvider(provider, roles, env, commerce) {
|
|
87
110
|
const spec = COMMERCE_PROVIDER_SECRETS[provider];
|
|
88
111
|
const pick = (names) => Object.fromEntries(names.map((n) => [n, env[n]]));
|
|
89
112
|
const [credentials, webhook] = await Promise.all([
|
|
90
|
-
|
|
113
|
+
// Config-aware: a multi-location project must not be held dormant waiting
|
|
114
|
+
// for a SQUARE_LOCATION_ID it will never legitimately have.
|
|
115
|
+
resolveModuleSecrets(pick(commerceProviderCredentials(provider, commerce))),
|
|
91
116
|
resolveModuleSecrets(pick([spec.webhook])),
|
|
92
117
|
]);
|
|
93
118
|
return {
|
|
@@ -119,7 +144,7 @@ export async function resolveCommerceStatus(commerce, env) {
|
|
|
119
144
|
if (providers.length === 0) {
|
|
120
145
|
return { configured: false, enabled: false, providers: [], missing: [] };
|
|
121
146
|
}
|
|
122
|
-
const resolved = await Promise.all(providers.map((provider) => resolveProvider(provider, ["storefront", "invoicing"].filter((r) => roles[r] === provider), env)));
|
|
147
|
+
const resolved = await Promise.all(providers.map((provider) => resolveProvider(provider, ["storefront", "invoicing", "pos"].filter((r) => roles[r] === provider), env, commerce)));
|
|
123
148
|
return {
|
|
124
149
|
configured: resolved.every((p) => p.configured),
|
|
125
150
|
enabled: true,
|
|
@@ -127,3 +152,26 @@ export async function resolveCommerceStatus(commerce, env) {
|
|
|
127
152
|
missing: resolved.flatMap((p) => [...p.credentials.missing, ...p.webhook.missing]),
|
|
128
153
|
};
|
|
129
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Is THIS provider live?
|
|
157
|
+
*
|
|
158
|
+
* `CommerceStatus.configured` is an all-or-nothing aggregate (`every`), which is
|
|
159
|
+
* the right answer for "is the whole module ready" and the wrong one for gating
|
|
160
|
+
* a single call site. A two-provider project — Fourthwall for the storefront,
|
|
161
|
+
* Square for invoicing — reads `configured: false` the moment either half is
|
|
162
|
+
* unprovisioned, so gating the working Fourthwall checkout on the aggregate
|
|
163
|
+
* would silently simulate it because SQUARE's secrets are still placeholders.
|
|
164
|
+
*
|
|
165
|
+
* Gate each surface on the provider it actually calls.
|
|
166
|
+
*/
|
|
167
|
+
export function providerConfigured(status, provider) {
|
|
168
|
+
return status.providers.some((p) => p.provider === provider && p.configured);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Is the provider filling this ROLE live? The role-shaped question, for code
|
|
172
|
+
* that cares about the capability rather than the vendor — "can I take a
|
|
173
|
+
* checkout?" rather than "is Square up?".
|
|
174
|
+
*/
|
|
175
|
+
export function roleConfigured(status, role) {
|
|
176
|
+
return status.providers.some((p) => p.roles.includes(role) && p.configured);
|
|
177
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SectionCatalog } from "louise-toolkit/content";
|
|
1
|
+
import type { BlockCatalog, SectionCatalog } from "louise-toolkit/content";
|
|
2
2
|
import type { RateRule } from "louise-toolkit/security";
|
|
3
3
|
import type { CatalogMirrorConfig } from "./commerce/mirror.js";
|
|
4
4
|
import type { astroidSectionCatalog } from "./components/sections.js";
|
|
@@ -156,8 +156,40 @@ export interface CommerceConfig {
|
|
|
156
156
|
* Fourthwall as the storefront, because neither can do the other's job.
|
|
157
157
|
*/
|
|
158
158
|
invoicing?: CommerceProvider;
|
|
159
|
+
/**
|
|
160
|
+
* In-person selling: physical inventory held at real places, sold at a
|
|
161
|
+
* counter or a market stall rather than through the site's own cart.
|
|
162
|
+
*
|
|
163
|
+
* Separate from `storefront` because they are genuinely different jobs and a
|
|
164
|
+
* site commonly runs both. themidwestartist.com sells print-on-demand merch
|
|
165
|
+
* through Fourthwall (`storefront`) while originals and self-stocked prints
|
|
166
|
+
* live in Square (`pos`) across several shops and galleries — one catalog per
|
|
167
|
+
* rail, neither able to do the other's job.
|
|
168
|
+
*
|
|
169
|
+
* What `pos` turns on that `storefront` does not: locations, per-location
|
|
170
|
+
* pricing, and per-location inventory.
|
|
171
|
+
*/
|
|
172
|
+
pos?: CommerceProvider;
|
|
159
173
|
/** The catalog mirror's shape — its mode, table name, and owned columns. */
|
|
160
174
|
catalog?: CatalogMirrorConfig;
|
|
175
|
+
/** Square-specific options. Only meaningful when Square fills some role. */
|
|
176
|
+
square?: SquareCommerceConfig;
|
|
177
|
+
}
|
|
178
|
+
export interface SquareCommerceConfig {
|
|
179
|
+
/**
|
|
180
|
+
* How many Square Locations this project sells at.
|
|
181
|
+
*
|
|
182
|
+
* `"single"` (the default) is the ordinary case: one location, its id supplied
|
|
183
|
+
* once as `SQUARE_LOCATION_ID`, and every order placed against it.
|
|
184
|
+
*
|
|
185
|
+
* `"multi"` is the multi-merchant model — each merchant is a Location, and the
|
|
186
|
+
* id comes from the *request* (which merchant's storefront is this?) rather
|
|
187
|
+
* than from the environment. Setting it stops Astroid requiring
|
|
188
|
+
* `SQUARE_LOCATION_ID`: a single ambient location id is not merely unnecessary
|
|
189
|
+
* there, it is a hazard, because anything defaulting to it would silently ring
|
|
190
|
+
* one merchant's sale against another merchant's books.
|
|
191
|
+
*/
|
|
192
|
+
locations?: "single" | "multi";
|
|
161
193
|
}
|
|
162
194
|
export interface QueuesConfig {
|
|
163
195
|
/**
|
|
@@ -278,6 +310,20 @@ export interface AstroidConfig {
|
|
|
278
310
|
* write paths agree. Omit to use Astroid's built-in catalog.
|
|
279
311
|
*/
|
|
280
312
|
sectionCatalog?: SectionCatalog;
|
|
313
|
+
/**
|
|
314
|
+
* The site's catalog of BLOCK types (ADR 0005) — the block-level analogue of
|
|
315
|
+
* {@link sectionCatalog}, and required for any section whose def declares a
|
|
316
|
+
* `blocks` policy.
|
|
317
|
+
*
|
|
318
|
+
* Without it the server has no field shape to check a block against, so
|
|
319
|
+
* `validateSections` rejects every block `_type` as unknown and a block-bearing
|
|
320
|
+
* write 422s — the on-canvas block toolbar appears to work and then nothing
|
|
321
|
+
* saves. It also gates block rich-text **sanitization**: block fields are only
|
|
322
|
+
* scrubbed when their def is resolvable here.
|
|
323
|
+
*
|
|
324
|
+
* Omit for a sections-only site (no section declares `blocks`).
|
|
325
|
+
*/
|
|
326
|
+
blockCatalog?: BlockCatalog;
|
|
281
327
|
/** Optional capabilities switched on for this site. */
|
|
282
328
|
modules?: ModuleKind[];
|
|
283
329
|
/** Gated account/portal area (order tracking, client galleries). */
|
package/dist/config.js
CHANGED
|
@@ -93,7 +93,7 @@ export function defineAstroid(config) {
|
|
|
93
93
|
if (config.portal?.gated) {
|
|
94
94
|
throw new AstroidConfigError("`portal.gated` is not implemented — it is accepted but wires no guard, so the site " +
|
|
95
95
|
"would be fully public while appearing gated. Remove it, and gate the whole site by " +
|
|
96
|
-
|
|
96
|
+
'listing the prefixes you mean in `portal.routes` (e.g. `[{ prefix: "/" }]` with your ' +
|
|
97
97
|
"login and auth paths ahead of it).");
|
|
98
98
|
}
|
|
99
99
|
return config;
|
package/dist/email/send.js
CHANGED
|
@@ -95,7 +95,12 @@ function describe(mail, reason, includeBody) {
|
|
|
95
95
|
" binding to deliver it, or pass `devLog: true` if this really is local.)",
|
|
96
96
|
].join("\n");
|
|
97
97
|
}
|
|
98
|
-
return [
|
|
98
|
+
return [
|
|
99
|
+
...head,
|
|
100
|
+
" ---",
|
|
101
|
+
...mail.content.text.split("\n").map((line) => ` ${line}`),
|
|
102
|
+
" ---",
|
|
103
|
+
].join("\n");
|
|
99
104
|
}
|
|
100
105
|
/**
|
|
101
106
|
* Best-effort "are we in development?".
|
package/dist/map/pmtiles.js
CHANGED
|
@@ -112,7 +112,7 @@ export async function servePmtiles(request, options) {
|
|
|
112
112
|
// client's view of the archive.
|
|
113
113
|
const got = object.range ?? {};
|
|
114
114
|
const start = got.offset ?? (parsed.kind === "suffix" ? object.size - parsed.suffix : parsed.offset);
|
|
115
|
-
const length = got.length ??
|
|
115
|
+
const length = got.length ?? got.suffix ?? object.size - start;
|
|
116
116
|
return new Response(head ? null : (object.body ?? null), {
|
|
117
117
|
status: 206,
|
|
118
118
|
headers: headers({
|
package/dist/map/scaffold.js
CHANGED
|
@@ -116,7 +116,7 @@ export function generateMapEmbedComponent(config) {
|
|
|
116
116
|
"",
|
|
117
117
|
"{",
|
|
118
118
|
" show ? (",
|
|
119
|
-
|
|
119
|
+
" <div",
|
|
120
120
|
' class="astroid-map"',
|
|
121
121
|
" data-astroid-map",
|
|
122
122
|
" data-lat={nlat}",
|
|
@@ -159,12 +159,12 @@ export function generateMapEmbedComponent(config) {
|
|
|
159
159
|
" })));",
|
|
160
160
|
"",
|
|
161
161
|
" const init = async (el: HTMLElement) => {",
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
" const lat = Number(el.dataset.lat);",
|
|
163
|
+
" const lng = Number(el.dataset.lng);",
|
|
164
164
|
" if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;",
|
|
165
165
|
"",
|
|
166
166
|
" const { maplibregl, Protocol } = await load();",
|
|
167
|
-
|
|
167
|
+
" // Register the pmtiles:// protocol once so MapLibre range-reads the",
|
|
168
168
|
" // archive instead of requesting tile URLs.",
|
|
169
169
|
' maplibregl.addProtocol("pmtiles", new Protocol().tile);',
|
|
170
170
|
"",
|
|
@@ -172,7 +172,7 @@ export function generateMapEmbedComponent(config) {
|
|
|
172
172
|
" container: el,",
|
|
173
173
|
' style: JSON.parse(el.dataset.style ?? "{}"),',
|
|
174
174
|
" center: [lng, lat],",
|
|
175
|
-
|
|
175
|
+
" zoom: Number(el.dataset.zoom ?? 15),",
|
|
176
176
|
" // A map that swallows page scroll is a trap on mobile; cooperative",
|
|
177
177
|
" // gestures require an explicit modifier/two fingers to pan.",
|
|
178
178
|
" cooperativeGestures: true,",
|
|
@@ -182,7 +182,7 @@ export function generateMapEmbedComponent(config) {
|
|
|
182
182
|
" });",
|
|
183
183
|
' map.addControl(new maplibregl.NavigationControl({ showCompass: false }), "top-right");',
|
|
184
184
|
"",
|
|
185
|
-
|
|
185
|
+
' const pin = document.createElement("div");',
|
|
186
186
|
" pin.style.cssText =",
|
|
187
187
|
` "width:18px;height:18px;border-radius:999px;background:${brand};" +`,
|
|
188
188
|
' "border:3px solid #fff;box-shadow:0 0 0 6px rgba(0,0,0,0.12);";',
|
package/dist/portal/scaffold.js
CHANGED
|
@@ -58,7 +58,7 @@ export function generateAstroidPortalAuth(config) {
|
|
|
58
58
|
" // only thing that applies the DUMMY_REPLACE_ME sentinel check. Built by",
|
|
59
59
|
" // hand, a fresh deploy with a real EMAIL binding but a placeholder",
|
|
60
60
|
" // MAIL_FROM read as configured and called the Email API with an envelope",
|
|
61
|
-
|
|
61
|
+
' // sender of literally "DUMMY_REPLACE_ME" — rejected upstream, swallowed',
|
|
62
62
|
" // here, and reported to the user as a reset email that was sent.",
|
|
63
63
|
" const mailer = await resolveMailer(env);",
|
|
64
64
|
" await sendTransactional(mailer, [",
|
package/dist/project/generate.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// them (fills real binding ids, secrets, account). `astroid generate` must
|
|
15
15
|
// NEVER clobber them, or it would wipe provisioned ids — so they live in a
|
|
16
16
|
// separate function the regenerate path doesn't call.
|
|
17
|
-
import { ASTROID_VITALS_BINDING, astroidVitalsDataset
|
|
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
20
|
import { COMMERCE_PROVIDER_SECRETS, COMMERCE_PROVIDER_SETUP, commerceSecretNames, } from "../commerce/secrets.js";
|
|
@@ -130,7 +130,7 @@ export function generateAstroidWrangler(config) {
|
|
|
130
130
|
p(" // `new_classes`) because the session keeps its authoritative state in");
|
|
131
131
|
p(" // `ctx.storage`, which is the SQLite-backed store — and the storage");
|
|
132
132
|
p(" // backend cannot be changed after the class is first deployed.");
|
|
133
|
-
p(
|
|
133
|
+
p(' "migrations": [');
|
|
134
134
|
p(` { "tag": ${JSON.stringify(ASTROID_REALTIME_MIGRATION_TAG)}, "new_sqlite_classes": [${JSON.stringify(ASTROID_EDIT_SESSION_CLASS)}] }`);
|
|
135
135
|
p(" ],");
|
|
136
136
|
}
|
package/dist/project/scaffold.js
CHANGED
|
@@ -71,7 +71,7 @@ export function generateAstroidScaffoldFiles(config) {
|
|
|
71
71
|
"// here; the generated src/schema.ts re-exports everything from this file, so",
|
|
72
72
|
"// drizzle-kit sees them and the worker can import them. Empty by default.",
|
|
73
73
|
"//",
|
|
74
|
-
|
|
74
|
+
'// e.g. export const redirects = sqliteTable("redirects", { … });',
|
|
75
75
|
"export {};",
|
|
76
76
|
"",
|
|
77
77
|
].join("\n"),
|
package/dist/pwa/generate.js
CHANGED
|
@@ -70,8 +70,18 @@ export function generateWebManifest(config) {
|
|
|
70
70
|
// A maskable icon is a separate asset, not a flag on the same file: the
|
|
71
71
|
// platform crops it to its own shape, so the artwork needs padding the
|
|
72
72
|
// `any` icon shouldn't have.
|
|
73
|
-
{
|
|
74
|
-
|
|
73
|
+
{
|
|
74
|
+
src: "/icons/maskable-192.png",
|
|
75
|
+
sizes: "192x192",
|
|
76
|
+
type: "image/png",
|
|
77
|
+
purpose: "maskable",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
src: "/icons/maskable-512.png",
|
|
81
|
+
sizes: "512x512",
|
|
82
|
+
type: "image/png",
|
|
83
|
+
purpose: "maskable",
|
|
84
|
+
},
|
|
75
85
|
],
|
|
76
86
|
}, null, 2)}\n`;
|
|
77
87
|
}
|
package/dist/queues/scaffold.js
CHANGED
|
@@ -120,7 +120,7 @@ export function generateAstroidQueueSeam(config) {
|
|
|
120
120
|
" //",
|
|
121
121
|
` // const items = (await listCatalog(token)).map(${provider ?? "provider"}ToCatalogItem);`,
|
|
122
122
|
` // const r = await astroidCatalogSync(items, { db: env.DB, table: ${JSON.stringify(table)} });`,
|
|
123
|
-
|
|
123
|
+
' // if (r.failed > 0) console.warn("[catalog] skipped", r.failed, r.errors);',
|
|
124
124
|
" //",
|
|
125
125
|
" // The sync is idempotent (keyed on the provider's id) and never",
|
|
126
126
|
" // writes an owner-edited column, so it's safe to run on every event.",
|
|
@@ -47,6 +47,19 @@ function pageMediaBase(config) {
|
|
|
47
47
|
function resolveSectionCatalog(config) {
|
|
48
48
|
return config.sectionCatalog ?? astroidSectionCatalog;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The block catalog a `pages` write is validated + sanitized against (ADR 0005).
|
|
52
|
+
* Defaults to empty, which is correct for a sections-only site: with no section
|
|
53
|
+
* declaring a `blocks` policy, no block is ever reached.
|
|
54
|
+
*
|
|
55
|
+
* It is NOT optional once a site does declare one. Unlike the section catalog
|
|
56
|
+
* there's no built-in fallback to borrow — block types are wholly site-defined —
|
|
57
|
+
* so an unset catalog means every block `_type` reads as unknown and the write
|
|
58
|
+
* 422s, and block rich text goes unsanitized because its def can't be resolved.
|
|
59
|
+
*/
|
|
60
|
+
function resolveBlockCatalog(config) {
|
|
61
|
+
return config.blockCatalog ?? {};
|
|
62
|
+
}
|
|
50
63
|
/**
|
|
51
64
|
* Return a copy of a `pages` write payload with its `sections` rich-text fields
|
|
52
65
|
* sanitized against the project media base — a no-op when the write carries no
|
|
@@ -61,7 +74,7 @@ export function sanitizeAstroidPageSections(config, data) {
|
|
|
61
74
|
if (data.sections === undefined)
|
|
62
75
|
return data;
|
|
63
76
|
const mediaBase = pageMediaBase(config);
|
|
64
|
-
const sections = sanitizeSectionsRichText(data.sections, resolveSectionCatalog(config), (html) => sanitizeRichHtml(html, { mediaBase }));
|
|
77
|
+
const sections = sanitizeSectionsRichText(data.sections, resolveSectionCatalog(config), (html) => sanitizeRichHtml(html, { mediaBase }), resolveBlockCatalog(config));
|
|
65
78
|
return { ...data, sections };
|
|
66
79
|
}
|
|
67
80
|
/**
|
|
@@ -77,6 +90,7 @@ export async function assertAstroidPageSections(config, data, operation = "updat
|
|
|
77
90
|
await assertValidSections(resolveSectionCatalog(config), data.sections, {
|
|
78
91
|
operation,
|
|
79
92
|
mediaBase: pageMediaBase(config),
|
|
93
|
+
blockCatalog: resolveBlockCatalog(config),
|
|
80
94
|
});
|
|
81
95
|
}
|
|
82
96
|
/**
|
package/dist/worker/generate.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// the pages routes is wired here — versionsRoute runs it through the collection's
|
|
12
12
|
// beforeChange hook, and pagesRoute (which takes no collection config) through
|
|
13
13
|
// the `astroidPagesWriteHooks` spread, so both write paths enforce one contract.
|
|
14
|
-
import { ASTROID_VITALS_BINDING, generateAstroidCwvQuery
|
|
14
|
+
import { ASTROID_VITALS_BINDING, generateAstroidCwvQuery } from "../analytics/index.js";
|
|
15
15
|
import { astroidEditorTable } from "../auth/index.js";
|
|
16
16
|
import { astroidPortal } from "../portal/config.js";
|
|
17
17
|
import { ASTROID_HEALTH_CRON, astroidCron, astroidUsesQueues } from "../queues/messages.js";
|
|
@@ -194,10 +194,7 @@ export function generateAstroidWorker(config) {
|
|
|
194
194
|
p();
|
|
195
195
|
p("// Editable site_settings columns the Settings panel may write, and which of");
|
|
196
196
|
p("// them resolve to a media-library asset.");
|
|
197
|
-
const settingsImageKeys = [
|
|
198
|
-
...ASTROID_SETTINGS_IMAGE_KEYS,
|
|
199
|
-
...(config.settings?.imageKeys ?? []),
|
|
200
|
-
];
|
|
197
|
+
const settingsImageKeys = [...ASTROID_SETTINGS_IMAGE_KEYS, ...(config.settings?.imageKeys ?? [])];
|
|
201
198
|
const settingsCustomKeys = config.settings?.customKeys ?? [];
|
|
202
199
|
// A custom-heavy site can override (or empty) the editable base columns.
|
|
203
200
|
const settingsColumns = config.settings?.columns ?? ASTROID_SETTINGS_COLUMNS;
|
|
@@ -245,11 +242,11 @@ export function generateAstroidWorker(config) {
|
|
|
245
242
|
p("async function runHealthScan(env: CloudflareEnv) {");
|
|
246
243
|
p(" const origin = env.SITE_URL ?? MEDIA_BASE;");
|
|
247
244
|
p(" const [brokenLinks, missingAlt, seoGaps] = await Promise.all([");
|
|
248
|
-
p(
|
|
245
|
+
p(' checkLinks({ base: origin, paths: ["/"] }).catch(() => []),');
|
|
249
246
|
p(" countRows(env, \"SELECT COUNT(*) AS n FROM media WHERE alt IS NULL OR alt = ''\"),");
|
|
250
247
|
p(" countRows(");
|
|
251
248
|
p(" env,");
|
|
252
|
-
p(
|
|
249
|
+
p(" \"SELECT COUNT(*) AS n FROM pages WHERE status = 'published'\" +");
|
|
253
250
|
p(" \" AND (seo_title IS NULL OR seo_title = '' OR seo_description IS NULL OR seo_description = '')\",");
|
|
254
251
|
p(" ),");
|
|
255
252
|
p(" ]);");
|
|
@@ -292,7 +289,7 @@ export function generateAstroidWorker(config) {
|
|
|
292
289
|
p(" const row = await env.DB.prepare(");
|
|
293
290
|
p(' "SELECT" +');
|
|
294
291
|
p(" \" (SELECT COUNT(*) FROM pages WHERE status = 'draft') AS drafts,\" +");
|
|
295
|
-
p(
|
|
292
|
+
p(" \" (SELECT COUNT(DISTINCT parent_id) FROM pages_versions WHERE status = 'draft') AS unpublished,\" +");
|
|
296
293
|
p(' " (SELECT MAX(updated_at) FROM pages) AS last_edited",');
|
|
297
294
|
p(" ).first<{ drafts: number; unpublished: number; last_edited: number | null }>();");
|
|
298
295
|
p(" if (!row) return undefined;");
|
|
@@ -352,7 +349,7 @@ export function generateAstroidWorker(config) {
|
|
|
352
349
|
p(" // Wrapped UNCONDITIONALLY, and that is safe: `withEdgeCache` only stores a");
|
|
353
350
|
p(" // response that carries a cacheable Cloudflare-CDN-Cache-Control directive,");
|
|
354
351
|
p(" // and a page emits one only via `Astro.cache.set(...)` — which the scaffold");
|
|
355
|
-
p(
|
|
352
|
+
p(' // gates on ASTROID_EDGE_CACHE being "true" AND the request not being in edit');
|
|
356
353
|
p(" // mode. With the var off (the default) every render is `no-store`, so this");
|
|
357
354
|
p(" // layer stores nothing and is a transparent pass-through.");
|
|
358
355
|
p(" //");
|
|
@@ -41,12 +41,12 @@ export function generateWorkflowSchema(config) {
|
|
|
41
41
|
" {",
|
|
42
42
|
' id: integer("id").primaryKey({ autoIncrement: true }),',
|
|
43
43
|
` ${camel(fk)}: text(${JSON.stringify(fk)}).notNull(),`,
|
|
44
|
-
|
|
44
|
+
" /** The stage this row completes (0-based). */",
|
|
45
45
|
' stage: integer("stage").notNull(),',
|
|
46
46
|
" /** Recorded values for the stage, [{ k, v }] JSON. */",
|
|
47
47
|
' specs: text("specs", { mode: "json" }).$type<{ k: string; v: string }[]>(),',
|
|
48
48
|
' initials: text("initials").notNull(),',
|
|
49
|
-
|
|
49
|
+
" /** Account id when the pipeline runs behind a portal. */",
|
|
50
50
|
' actorId: text("actor_id"),',
|
|
51
51
|
' signedAt: integer("signed_at", { mode: "timestamp" })',
|
|
52
52
|
" .notNull()",
|
|
@@ -110,7 +110,7 @@ export function generateWorkflowRoute(config) {
|
|
|
110
110
|
" specs?: { k: string; v: string }[];",
|
|
111
111
|
" } | null;",
|
|
112
112
|
"",
|
|
113
|
-
|
|
113
|
+
' if (!body?.id || typeof body.stage !== "number") {',
|
|
114
114
|
' return Response.json({ ok: false, error: "Bad request" }, { status: 400 });',
|
|
115
115
|
" }",
|
|
116
116
|
"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astroidjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Astroid — an opinionated meta-framework over Louise Toolkit and Astro for building editable, multi-editor sites on Cloudflare Workers.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -17,10 +17,15 @@
|
|
|
17
17
|
"url": "git+https://github.com/bowenlabs/louise-toolkit.git",
|
|
18
18
|
"directory": "packages/astroid"
|
|
19
19
|
},
|
|
20
|
-
"type": "module",
|
|
21
20
|
"bin": {
|
|
22
21
|
"astroid": "./bin/astroid.mjs"
|
|
23
22
|
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist",
|
|
25
|
+
"bin",
|
|
26
|
+
"src/components"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
24
29
|
"exports": {
|
|
25
30
|
".": {
|
|
26
31
|
"types": "./dist/index.d.ts",
|
|
@@ -51,16 +56,18 @@
|
|
|
51
56
|
},
|
|
52
57
|
"./package.json": "./package.json"
|
|
53
58
|
},
|
|
54
|
-
"files": [
|
|
55
|
-
"dist",
|
|
56
|
-
"bin",
|
|
57
|
-
"src/components"
|
|
58
|
-
],
|
|
59
59
|
"publishConfig": {
|
|
60
60
|
"access": "public"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"louise-toolkit": "0.
|
|
63
|
+
"louise-toolkit": "0.20.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/node": "^24.13.3",
|
|
67
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
68
|
+
"solid-js": "^1.9.14",
|
|
69
|
+
"typescript": "^5.8.0",
|
|
70
|
+
"vitest": "^4.1.10"
|
|
64
71
|
},
|
|
65
72
|
"peerDependencies": {
|
|
66
73
|
"solid-js": "^1.9.0"
|
|
@@ -70,18 +77,12 @@
|
|
|
70
77
|
"optional": true
|
|
71
78
|
}
|
|
72
79
|
},
|
|
73
|
-
"devDependencies": {
|
|
74
|
-
"@types/node": "^24.13.3",
|
|
75
|
-
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
76
|
-
"solid-js": "^1.9.14",
|
|
77
|
-
"typescript": "^5.8.0",
|
|
78
|
-
"vitest": "^4.1.10"
|
|
79
|
-
},
|
|
80
80
|
"engines": {
|
|
81
81
|
"node": ">=24.0.0"
|
|
82
82
|
},
|
|
83
83
|
"scripts": {
|
|
84
84
|
"build": "tsgo -p tsconfig.build.json",
|
|
85
|
+
"check": "vp check",
|
|
85
86
|
"test": "vp test",
|
|
86
87
|
"typecheck": "tsgo --noEmit"
|
|
87
88
|
}
|
|
@@ -30,12 +30,7 @@
|
|
|
30
30
|
// import is TYPE-ONLY, so it erases at build and never drags the validator (or
|
|
31
31
|
// drizzle, which that entry pulls in) into a page bundle.
|
|
32
32
|
|
|
33
|
-
import type {
|
|
34
|
-
SectionCatalog,
|
|
35
|
-
SectionDef,
|
|
36
|
-
SectionField,
|
|
37
|
-
SectionItem,
|
|
38
|
-
} from "louise-toolkit/content";
|
|
33
|
+
import type { SectionCatalog, SectionDef, SectionField, SectionItem } from "louise-toolkit/content";
|
|
39
34
|
|
|
40
35
|
export type { SectionCatalog, SectionDef, SectionField, SectionItem };
|
|
41
36
|
|