astroidjs 0.10.0 → 0.12.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/dist/astro/csp.d.ts +2 -8
- package/dist/astro/csp.js +2 -75
- package/dist/commerce/checkout-scaffold.js +10 -2
- package/dist/commerce/checkout.d.ts +17 -0
- package/dist/commerce/checkout.js +44 -39
- package/dist/commerce/index.d.ts +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/project/generate.js +7 -1
- package/dist/security/csp-origins.d.ts +16 -0
- package/dist/security/csp-origins.js +76 -0
- package/dist/worker/generate.js +16 -2
- package/package.json +2 -2
package/dist/astro/csp.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { AstroidConfig
|
|
1
|
+
import type { AstroidConfig } from "../config.js";
|
|
2
|
+
export { astroidCspOrigins } from "../security/csp-origins.js";
|
|
2
3
|
/** A `sha256-…` CSP hash, in the shape Astro's `security.csp.hashes` takes. */
|
|
3
4
|
export type CspHash = `${"sha256" | "sha384" | "sha512"}-${string}`;
|
|
4
5
|
/**
|
|
@@ -38,12 +39,6 @@ export interface AstroidSecurityConfig {
|
|
|
38
39
|
* hash on the next build.
|
|
39
40
|
*/
|
|
40
41
|
export declare function solidHydrationHash(): CspHash;
|
|
41
|
-
/**
|
|
42
|
-
* Every origin the project's enabled modules need, merged with the config's own
|
|
43
|
-
* `security.cspOrigins`. Exported so a site (or `astroid doctor`) can inspect
|
|
44
|
-
* what a config implies without rebuilding the whole policy.
|
|
45
|
-
*/
|
|
46
|
-
export declare function astroidCspOrigins(config: AstroidConfig): Required<CspOrigins>;
|
|
47
42
|
/**
|
|
48
43
|
* The `security` block for `astro.config.mjs` — Astro's half of the split.
|
|
49
44
|
*
|
|
@@ -61,4 +56,3 @@ export declare function astroidSecurity(config: AstroidConfig): AstroidSecurityC
|
|
|
61
56
|
export declare const ASTROID_VITE_BUILD: {
|
|
62
57
|
readonly assetsInlineLimit: 0;
|
|
63
58
|
};
|
|
64
|
-
export {};
|
package/dist/astro/csp.js
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
// imports.
|
|
28
28
|
import { createHash } from "node:crypto";
|
|
29
29
|
import { generateHydrationScript } from "solid-js/web";
|
|
30
|
-
import {
|
|
30
|
+
import { astroidCspOrigins } from "../security/csp-origins.js";
|
|
31
|
+
export { astroidCspOrigins } from "../security/csp-origins.js";
|
|
31
32
|
/**
|
|
32
33
|
* Hash of Solid's inline hydration bootstrap.
|
|
33
34
|
*
|
|
@@ -42,80 +43,6 @@ export function solidHydrationHash() {
|
|
|
42
43
|
const inner = generateHydrationScript().match(/<script[^>]*>([\s\S]*?)<\/script>/)?.[1] ?? "";
|
|
43
44
|
return `sha256-${createHash("sha256").update(inner).digest("base64")}`;
|
|
44
45
|
}
|
|
45
|
-
/** Origins a commerce provider's client-side SDK needs. Server-only providers
|
|
46
|
-
* (Fourthwall's storefront API) contribute nothing but their image host. */
|
|
47
|
-
const COMMERCE_ORIGINS = {
|
|
48
|
-
// Square Web Payments renders the card form in an iframe from the squarecdn
|
|
49
|
-
// hosts, tokenizes against pci-connect, and pulls its own fonts. Both the
|
|
50
|
-
// sandbox and production hosts are listed so ONE build serves either
|
|
51
|
-
// environment — which environment you're in is a runtime secret, not a
|
|
52
|
-
// build-time one.
|
|
53
|
-
square: {
|
|
54
|
-
script: ["https://sandbox.web.squarecdn.com", "https://web.squarecdn.com"],
|
|
55
|
-
frame: [
|
|
56
|
-
"https://sandbox.web.squarecdn.com",
|
|
57
|
-
"https://web.squarecdn.com",
|
|
58
|
-
"https://connect.squareupsandbox.com",
|
|
59
|
-
"https://connect.squareup.com",
|
|
60
|
-
],
|
|
61
|
-
connect: [
|
|
62
|
-
"https://pci-connect.squareupsandbox.com",
|
|
63
|
-
"https://pci-connect.squareup.com",
|
|
64
|
-
"https://sandbox.web.squarecdn.com",
|
|
65
|
-
"https://web.squarecdn.com",
|
|
66
|
-
],
|
|
67
|
-
font: [
|
|
68
|
-
"https://square-fonts-production-f.squarecdn.com",
|
|
69
|
-
"https://d1g145x70srn7h.cloudfront.net",
|
|
70
|
-
],
|
|
71
|
-
},
|
|
72
|
-
// Stripe.js and its Elements/Checkout iframes.
|
|
73
|
-
stripe: {
|
|
74
|
-
script: ["https://js.stripe.com"],
|
|
75
|
-
frame: ["https://js.stripe.com", "https://hooks.stripe.com"],
|
|
76
|
-
connect: ["https://api.stripe.com"],
|
|
77
|
-
},
|
|
78
|
-
// Fourthwall is read server-side; nothing runs in the browser.
|
|
79
|
-
fourthwall: {},
|
|
80
|
-
};
|
|
81
|
-
// Turnstile. Always allowed, not gated on the captcha being configured: the
|
|
82
|
-
// scaffold ships the widget dormant (see the dormant-until-provisioned
|
|
83
|
-
// convention) and it must not need a rebuild to switch on — CSP is baked at
|
|
84
|
-
// build time, the secret is a runtime value.
|
|
85
|
-
const TURNSTILE = {
|
|
86
|
-
script: ["https://challenges.cloudflare.com"],
|
|
87
|
-
frame: ["https://challenges.cloudflare.com"],
|
|
88
|
-
connect: ["https://challenges.cloudflare.com"],
|
|
89
|
-
};
|
|
90
|
-
// The map module. MapLibre spins its tile-decoding workers up from blob: URLs,
|
|
91
|
-
// so `worker-src blob:` is not optional — without it the map renders an empty
|
|
92
|
-
// canvas and the console fills with worker-construction errors.
|
|
93
|
-
//
|
|
94
|
-
// Nothing else is needed, and that's the whole argument for the self-hosted
|
|
95
|
-
// basemap: the PMTiles archive is served same-origin, so `connect-src` stays
|
|
96
|
-
// `'self'` with no tile host and no API key to allow.
|
|
97
|
-
const MAP = { worker: ["blob:"] };
|
|
98
|
-
const DIRECTIVE_KEYS = ["script", "frame", "connect", "font", "img", "worker"];
|
|
99
|
-
/** Merge origin lists, de-duplicated, order preserved. */
|
|
100
|
-
function mergeOrigins(...sets) {
|
|
101
|
-
const out = {};
|
|
102
|
-
for (const key of DIRECTIVE_KEYS) {
|
|
103
|
-
out[key] = [...new Set(sets.flatMap((set) => set[key] ?? []))];
|
|
104
|
-
}
|
|
105
|
-
return out;
|
|
106
|
-
}
|
|
107
|
-
/**
|
|
108
|
-
* Every origin the project's enabled modules need, merged with the config's own
|
|
109
|
-
* `security.cspOrigins`. Exported so a site (or `astroid doctor`) can inspect
|
|
110
|
-
* what a config implies without rebuilding the whole policy.
|
|
111
|
-
*/
|
|
112
|
-
export function astroidCspOrigins(config) {
|
|
113
|
-
return mergeOrigins(TURNSTILE, ...((config.modules ?? []).includes("map") ? [MAP] : []),
|
|
114
|
-
// EVERY provider in play, not "the" provider: a site can run Stripe for
|
|
115
|
-
// invoicing beside Fourthwall for the storefront, and a policy that allowed
|
|
116
|
-
// only one of them blocks the other's SDK at runtime.
|
|
117
|
-
...astroidCommerceProviders(config.commerce).map((p) => COMMERCE_ORIGINS[p] ?? {}), config.security?.cspOrigins ?? {});
|
|
118
|
-
}
|
|
119
46
|
/**
|
|
120
47
|
* Render one directive. The name is a literal from the union above, so the
|
|
121
48
|
* concatenation is a valid `CspDirective` by construction — which is what the
|
|
@@ -231,7 +231,11 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
231
231
|
" const check = await verifyCheckout(body.lines, serverPrices, {",
|
|
232
232
|
" scope: { locationId },",
|
|
233
233
|
" });",
|
|
234
|
-
"
|
|
234
|
+
" // Every stale line, with its live price — feed `issues` to `repairCart`",
|
|
235
|
+
" // (louise-toolkit/commerce) to fix the whole cart in one step.",
|
|
236
|
+
" if (!check.ok) {",
|
|
237
|
+
" return json({ error: check.message, reason: check.reason, issues: check.issues }, 409);",
|
|
238
|
+
" }",
|
|
235
239
|
"",
|
|
236
240
|
" // 3: stable per cart, distinct per customer.",
|
|
237
241
|
' const idempotencyKey = await checkoutIdempotencyKey(check, "order", cartId);',
|
|
@@ -239,7 +243,11 @@ export function generateAstroidCheckoutRoute(config) {
|
|
|
239
243
|
: [
|
|
240
244
|
" // 1 + 2: re-price and refuse on mismatch.",
|
|
241
245
|
" const check = await verifyCheckout(body.lines, serverPrices);",
|
|
242
|
-
"
|
|
246
|
+
" // Every stale line, with its live price — feed `issues` to `repairCart`",
|
|
247
|
+
" // (louise-toolkit/commerce) to fix the whole cart in one step.",
|
|
248
|
+
" if (!check.ok) {",
|
|
249
|
+
" return json({ error: check.message, reason: check.reason, issues: check.issues }, 409);",
|
|
250
|
+
" }",
|
|
243
251
|
"",
|
|
244
252
|
" // 3: stable per cart, distinct per customer.",
|
|
245
253
|
' const idempotencyKey = await checkoutIdempotencyKey(check, "order", cartId);',
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CartIssue } from "louise-toolkit/commerce";
|
|
1
2
|
/** One line as the CLIENT sent it. Every field is untrusted. */
|
|
2
3
|
export interface ClientLine {
|
|
3
4
|
/** Provider id of the variant/item being bought. */
|
|
@@ -32,14 +33,30 @@ export interface VerifiedLine {
|
|
|
32
33
|
* them and guessing would put the wrong sentence on the screen.
|
|
33
34
|
*/
|
|
34
35
|
export type CheckoutRefusal = "empty" | "unavailable" | "out-of-stock" | "price-changed" | "invalid";
|
|
36
|
+
/**
|
|
37
|
+
* One way the cart disagrees with the live catalog — the toolkit's
|
|
38
|
+
* `CartIssue`, less the add-on case (checkout lines carry no add-ons yet).
|
|
39
|
+
* Hand the list to `repairCart` from `louise-toolkit/commerce` to fix the cart
|
|
40
|
+
* in one step.
|
|
41
|
+
*/
|
|
42
|
+
export type CheckoutIssue = Exclude<CartIssue, {
|
|
43
|
+
kind: "modifier-unavailable";
|
|
44
|
+
}>;
|
|
35
45
|
export type CheckoutVerification = {
|
|
36
46
|
ok: true;
|
|
37
47
|
lines: VerifiedLine[];
|
|
38
48
|
subtotalCents: number;
|
|
39
49
|
} | {
|
|
40
50
|
ok: false;
|
|
51
|
+
/** The first problem, in cart order. */
|
|
41
52
|
reason: CheckoutRefusal;
|
|
42
53
|
message: string;
|
|
54
|
+
/**
|
|
55
|
+
* Every problem, in cart order, with what the catalog says now — empty
|
|
56
|
+
* for `"empty"` and `"invalid"`, which are about the request, not the
|
|
57
|
+
* catalog.
|
|
58
|
+
*/
|
|
59
|
+
issues: CheckoutIssue[];
|
|
43
60
|
};
|
|
44
61
|
/** Look up current prices, in minor units, keyed by variant id. Anything the
|
|
45
62
|
* map omits is treated as no longer purchasable. */
|
|
@@ -12,8 +12,36 @@
|
|
|
12
12
|
//
|
|
13
13
|
// The failure this prevents is not exotic. Accept `unitPrice` from the request
|
|
14
14
|
// body and anyone can buy anything for a penny.
|
|
15
|
+
//
|
|
16
|
+
// The comparison itself is the toolkit's `cartIssues`, which reports EVERY
|
|
17
|
+
// stale line rather than the first: a refusal that names one problem at a time
|
|
18
|
+
// is how a customer ends up fixing a line, retrying, and being refused over the
|
|
19
|
+
// next. What this adds is the opinion — policing the untrusted body, and the
|
|
20
|
+
// sentence a customer sees.
|
|
21
|
+
import { cartIssues } from "louise-toolkit/commerce";
|
|
15
22
|
import { AstroidUsageError } from "../errors.js";
|
|
16
23
|
const MAX_QUANTITY = 999;
|
|
24
|
+
/** What a customer reads, by issue kind: one line affected, or several. */
|
|
25
|
+
const ISSUE_MESSAGES = {
|
|
26
|
+
"out-of-stock": ["An item in your cart just sold out.", "Some items in your cart just sold out."],
|
|
27
|
+
unavailable: [
|
|
28
|
+
"An item in your cart is no longer available.",
|
|
29
|
+
"Some items in your cart are no longer available.",
|
|
30
|
+
],
|
|
31
|
+
"price-changed": [
|
|
32
|
+
"Prices changed — please review your cart.",
|
|
33
|
+
"Prices changed — please review your cart.",
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
/** One sentence for the whole refusal. Mixed kinds get a sentence that covers them all. */
|
|
37
|
+
function issueMessage(issues) {
|
|
38
|
+
const kinds = new Set(issues.map((i) => i.kind));
|
|
39
|
+
const [kind] = kinds;
|
|
40
|
+
if (kinds.size > 1 || !kind)
|
|
41
|
+
return "Your cart changed since you filled it — please review it.";
|
|
42
|
+
const [one, many] = ISSUE_MESSAGES[kind];
|
|
43
|
+
return issues.length > 1 ? many : one;
|
|
44
|
+
}
|
|
17
45
|
/** Accept either lookup shape without making every caller branch. */
|
|
18
46
|
function normalizeLookup(result) {
|
|
19
47
|
if (result instanceof Map)
|
|
@@ -42,7 +70,7 @@ export async function verifyCheckout(lines,
|
|
|
42
70
|
// and a union of call signatures would reject the two-argument call below.
|
|
43
71
|
lookup, options = {}) {
|
|
44
72
|
if (!Array.isArray(lines) || lines.length === 0) {
|
|
45
|
-
return { ok: false, reason: "empty", message: "Your cart is empty." };
|
|
73
|
+
return { ok: false, reason: "empty", message: "Your cart is empty.", issues: [] };
|
|
46
74
|
}
|
|
47
75
|
const parsed = [];
|
|
48
76
|
for (const raw of lines) {
|
|
@@ -57,48 +85,25 @@ lookup, options = {}) {
|
|
|
57
85
|
l.quantity > MAX_QUANTITY ||
|
|
58
86
|
typeof l.unitPriceCents !== "number" ||
|
|
59
87
|
!Number.isFinite(l.unitPriceCents)) {
|
|
60
|
-
return { ok: false, reason: "invalid", message: "That cart isn't valid." };
|
|
88
|
+
return { ok: false, reason: "invalid", message: "That cart isn't valid.", issues: [] };
|
|
61
89
|
}
|
|
62
90
|
parsed.push({ variantId: l.variantId, quantity: l.quantity, unitPriceCents: l.unitPriceCents });
|
|
63
91
|
}
|
|
64
92
|
const { prices, outOfStock } = normalizeLookup(await lookup([...new Set(parsed.map((l) => l.variantId))], options.scope));
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (serverPrice === undefined) {
|
|
80
|
-
return {
|
|
81
|
-
ok: false,
|
|
82
|
-
reason: "unavailable",
|
|
83
|
-
message: "An item in your cart is no longer available.",
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
if (serverPrice !== line.unitPriceCents) {
|
|
87
|
-
return {
|
|
88
|
-
ok: false,
|
|
89
|
-
reason: "price-changed",
|
|
90
|
-
message: "Prices changed — please review your cart.",
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
const lineSubtotal = serverPrice * line.quantity;
|
|
94
|
-
verified.push({
|
|
95
|
-
variantId: line.variantId,
|
|
96
|
-
quantity: line.quantity,
|
|
97
|
-
unitPriceCents: serverPrice,
|
|
98
|
-
subtotalCents: lineSubtotal,
|
|
99
|
-
});
|
|
100
|
-
subtotalCents += lineSubtotal;
|
|
101
|
-
}
|
|
93
|
+
// No `liveModifierIds`, so the add-on check is skipped and every issue is a
|
|
94
|
+
// variant one — which is what makes the narrowing to CheckoutIssue true.
|
|
95
|
+
const issues = cartIssues(parsed, { prices, outOfStock });
|
|
96
|
+
const [first] = issues;
|
|
97
|
+
if (first)
|
|
98
|
+
return { ok: false, reason: first.kind, message: issueMessage(issues), issues };
|
|
99
|
+
// Every line is now known to be priced, and at the price the customer saw.
|
|
100
|
+
const verified = parsed.map((line) => ({
|
|
101
|
+
variantId: line.variantId,
|
|
102
|
+
quantity: line.quantity,
|
|
103
|
+
unitPriceCents: line.unitPriceCents,
|
|
104
|
+
subtotalCents: line.unitPriceCents * line.quantity,
|
|
105
|
+
}));
|
|
106
|
+
const subtotalCents = verified.reduce((sum, l) => sum + l.subtotalCents, 0);
|
|
102
107
|
return { ok: true, lines: verified, subtotalCents };
|
|
103
108
|
}
|
|
104
109
|
/**
|
package/dist/commerce/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { catalogNormalizer, type FourthwallProductLike, fourthwallToCatalogItem, type SquareAdapterOptions, squareItemSoldAt, type SquareItemLike, type SquareLocationOverrideLike, type SquarePresenceLike, squareToCatalogItem, } from "./adapters.js";
|
|
2
|
-
export { type CheckoutRefusal, type CheckoutScope, type CheckoutVerification, checkoutIdempotencyKey, type ClientLine, type PriceLookup, type ScopedPriceLookup, type ScopedPrices, verifyCheckout, type VerifiedLine, type VerifyCheckoutOptions, } from "./checkout.js";
|
|
2
|
+
export { type CheckoutIssue, type CheckoutRefusal, type CheckoutScope, type CheckoutVerification, checkoutIdempotencyKey, type ClientLine, type PriceLookup, type ScopedPriceLookup, type ScopedPrices, verifyCheckout, type VerifiedLine, type VerifyCheckoutOptions, } 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
5
|
export { COMMERCE_PROVIDER_SECRETS, type CommerceStatus, commerceSecretNames, type ProviderStatus, providerConfigured, resolveCommerceStatus, roleConfigured, } from "./secrets.js";
|
package/dist/config.d.ts
CHANGED
|
@@ -378,6 +378,8 @@ export interface SecurityConfig {
|
|
|
378
378
|
/** Per-directive origin lists contributed to the CSP. */
|
|
379
379
|
export interface CspOrigins {
|
|
380
380
|
script?: string[];
|
|
381
|
+
/** Rendered by the generated middleware (it owns `style-src`), not by Astro. */
|
|
382
|
+
style?: string[];
|
|
381
383
|
frame?: string[];
|
|
382
384
|
connect?: string[];
|
|
383
385
|
font?: string[];
|
package/dist/project/generate.js
CHANGED
|
@@ -103,7 +103,13 @@ export function generateAstroidWrangler(config) {
|
|
|
103
103
|
p(" // Pin your account so deploys don't prompt (or set CLOUDFLARE_ACCOUNT_ID).");
|
|
104
104
|
p(' // "account_id": "<your-cloudflare-account-id>",');
|
|
105
105
|
p(` "compatibility_date": ${JSON.stringify(COMPATIBILITY_DATE)},`);
|
|
106
|
-
|
|
106
|
+
// global_fetch_strictly_public (ADR 0012): a fetch to the site's own zone goes
|
|
107
|
+
// in through Cloudflare's front door, past the WAF, like any Internet request.
|
|
108
|
+
p(" // global_fetch_strictly_public: a fetch to this zone goes through Cloudflare's");
|
|
109
|
+
p(" // front door like any Internet request, past the WAF and bot rules (ADR 0012).");
|
|
110
|
+
p(" // If the daily health scan starts reporting broken links that aren't, a zone");
|
|
111
|
+
p(" // rule is challenging its self-crawl — allow it rather than drop the flag.");
|
|
112
|
+
p(' "compatibility_flags": ["nodejs_compat", "global_fetch_strictly_public"],');
|
|
107
113
|
p(" // @astrojs/cloudflare builds this entry and wires the static assets under dist/.");
|
|
108
114
|
p(' "main": "src/worker.ts",');
|
|
109
115
|
const tenancy = config.tenancy;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { AstroidConfig, CspOrigins } from "../config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Every origin the project's enabled modules need, merged with the config's own
|
|
4
|
+
* `security.cspOrigins`. Exported so a site (or `astroid doctor`) can inspect
|
|
5
|
+
* what a config implies without rebuilding the whole policy.
|
|
6
|
+
*/
|
|
7
|
+
export declare function astroidCspOrigins(config: AstroidConfig): Required<CspOrigins>;
|
|
8
|
+
/**
|
|
9
|
+
* The `style-src` value the generated middleware rewrites every response to.
|
|
10
|
+
*
|
|
11
|
+
* `'unsafe-inline'` is required (Louise's data-driven `style=""` carriers and
|
|
12
|
+
* the editor's runtime `<style>`), which is why Astro can't own this directive:
|
|
13
|
+
* per spec a single hash here voids `'unsafe-inline'`. Module and config
|
|
14
|
+
* `style` origins ride along, e.g. the Square SDK's own stylesheet.
|
|
15
|
+
*/
|
|
16
|
+
export declare function astroidCspStyleSrc(config: AstroidConfig): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// The CSP origins a config implies, as plain data.
|
|
4
|
+
//
|
|
5
|
+
// Split out of `astro/csp.ts` (build-time only: `node:crypto`, `solid-js/web`)
|
|
6
|
+
// so the worker generator can read it too: Astro's `security.csp` renders every
|
|
7
|
+
// directive except `style-src`, and the generated middleware renders that one.
|
|
8
|
+
// Both have to see the same origin set, or a module's stylesheet host lands in
|
|
9
|
+
// one half of the policy and not the other.
|
|
10
|
+
import { squareWebPaymentsCsp } from "louise-toolkit/commerce/square-web";
|
|
11
|
+
import { turnstileCsp } from "louise-toolkit/forms/turnstile";
|
|
12
|
+
import { astroidCommerceProviders } from "../commerce/roles.js";
|
|
13
|
+
/** Origins a commerce provider's client-side SDK needs. Server-only providers
|
|
14
|
+
* (Fourthwall's storefront API) contribute nothing but their image host. */
|
|
15
|
+
const COMMERCE_ORIGINS = {
|
|
16
|
+
// Square Web Payments, from the toolkit that mounts it, so a host Square adds
|
|
17
|
+
// arrives with an upgrade instead of as a console violation on every site. A
|
|
18
|
+
// hand copy lived here and had already missed two (Square's Sentry ingest,
|
|
19
|
+
// and Cash Sans's font host). Both environments by default, so ONE build
|
|
20
|
+
// serves either: which environment you're in is a runtime secret, not a
|
|
21
|
+
// build-time one.
|
|
22
|
+
square: squareWebPaymentsCsp(),
|
|
23
|
+
// Stripe.js and its Elements/Checkout iframes.
|
|
24
|
+
stripe: {
|
|
25
|
+
script: ["https://js.stripe.com"],
|
|
26
|
+
frame: ["https://js.stripe.com", "https://hooks.stripe.com"],
|
|
27
|
+
connect: ["https://api.stripe.com"],
|
|
28
|
+
},
|
|
29
|
+
// Fourthwall is read server-side; nothing runs in the browser.
|
|
30
|
+
fourthwall: {},
|
|
31
|
+
};
|
|
32
|
+
// Turnstile. Always allowed, not gated on the captcha being configured: the
|
|
33
|
+
// scaffold ships the widget dormant (see the dormant-until-provisioned
|
|
34
|
+
// convention) and it must not need a rebuild to switch on — CSP is baked at
|
|
35
|
+
// build time, the secret is a runtime value.
|
|
36
|
+
const TURNSTILE = turnstileCsp();
|
|
37
|
+
// The map module. MapLibre spins its tile-decoding workers up from blob: URLs,
|
|
38
|
+
// so `worker-src blob:` is not optional — without it the map renders an empty
|
|
39
|
+
// canvas and the console fills with worker-construction errors.
|
|
40
|
+
//
|
|
41
|
+
// Nothing else is needed, and that's the whole argument for the self-hosted
|
|
42
|
+
// basemap: the PMTiles archive is served same-origin, so `connect-src` stays
|
|
43
|
+
// `'self'` with no tile host and no API key to allow.
|
|
44
|
+
const MAP = { worker: ["blob:"] };
|
|
45
|
+
const DIRECTIVE_KEYS = ["script", "style", "frame", "connect", "font", "img", "worker"];
|
|
46
|
+
/** Merge origin lists, de-duplicated, order preserved. */
|
|
47
|
+
function mergeOrigins(...sets) {
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const key of DIRECTIVE_KEYS) {
|
|
50
|
+
out[key] = [...new Set(sets.flatMap((set) => set[key] ?? []))];
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Every origin the project's enabled modules need, merged with the config's own
|
|
56
|
+
* `security.cspOrigins`. Exported so a site (or `astroid doctor`) can inspect
|
|
57
|
+
* what a config implies without rebuilding the whole policy.
|
|
58
|
+
*/
|
|
59
|
+
export function astroidCspOrigins(config) {
|
|
60
|
+
return mergeOrigins(TURNSTILE, ...((config.modules ?? []).includes("map") ? [MAP] : []),
|
|
61
|
+
// EVERY provider in play, not "the" provider: a site can run Stripe for
|
|
62
|
+
// invoicing beside Fourthwall for the storefront, and a policy that allowed
|
|
63
|
+
// only one of them blocks the other's SDK at runtime.
|
|
64
|
+
...astroidCommerceProviders(config.commerce).map((p) => COMMERCE_ORIGINS[p] ?? {}), config.security?.cspOrigins ?? {});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The `style-src` value the generated middleware rewrites every response to.
|
|
68
|
+
*
|
|
69
|
+
* `'unsafe-inline'` is required (Louise's data-driven `style=""` carriers and
|
|
70
|
+
* the editor's runtime `<style>`), which is why Astro can't own this directive:
|
|
71
|
+
* per spec a single hash here voids `'unsafe-inline'`. Module and config
|
|
72
|
+
* `style` origins ride along, e.g. the Square SDK's own stylesheet.
|
|
73
|
+
*/
|
|
74
|
+
export function astroidCspStyleSrc(config) {
|
|
75
|
+
return ["'self'", "'unsafe-inline'", ...astroidCspOrigins(config).style].join(" ");
|
|
76
|
+
}
|
package/dist/worker/generate.js
CHANGED
|
@@ -17,6 +17,7 @@ import { astroidPortal } from "../portal/config.js";
|
|
|
17
17
|
import { ASTROID_HEALTH_CRON, astroidCron, astroidUsesQueues } from "../queues/messages.js";
|
|
18
18
|
import { ASTROID_EDIT_SESSION_CLASS, ASTROID_REALTIME_BINDING, usesRealtime, } from "../realtime/scaffold.js";
|
|
19
19
|
import { capturesInquiries } from "../schema/framework.js";
|
|
20
|
+
import { astroidCspStyleSrc } from "../security/csp-origins.js";
|
|
20
21
|
import { ASTROID_REWRITE_EXCLUDE, ASTROID_TENANT_PREFIX } from "../tenancy/index.js";
|
|
21
22
|
import { astroidEditorRoutePlan } from "./routes.js";
|
|
22
23
|
// Astroid's default editable site_settings surface — the columns the Settings
|
|
@@ -369,6 +370,13 @@ export function generateAstroidWorker(config) {
|
|
|
369
370
|
? "export default composeWorker<CloudflareEnv, AstroidQueueMessage>({"
|
|
370
371
|
: "export default composeWorker<CloudflareEnv>({");
|
|
371
372
|
p(" routes: [...editorRoutes, mediaAssetRoute],");
|
|
373
|
+
p(" // Deny-by-default editor API (ADR 0012). Under /api/louise a request must");
|
|
374
|
+
p(" // resolve to an editor unless it's headed for a public route — the contact");
|
|
375
|
+
p(" // form and the vitals beacon mark themselves. Every route above still checks");
|
|
376
|
+
p(" // for itself; this is what catches one that forgets. It also gives route");
|
|
377
|
+
p(" // responses the security headers the middleware never sees, since these");
|
|
378
|
+
p(" // routes answer before Astro runs.");
|
|
379
|
+
p(" gate: { resolveEditor },");
|
|
372
380
|
p(" // The SSR fallback, wrapped in the cookie-aware Worker cache (ADR 0004).");
|
|
373
381
|
p(" //");
|
|
374
382
|
p(" // Wrapped UNCONDITIONALLY, and that is safe: `withEdgeCache` only stores a");
|
|
@@ -470,8 +478,10 @@ export function generateAstroidWorker(config) {
|
|
|
470
478
|
export function generateAstroidMiddleware(config) {
|
|
471
479
|
// Louise's brand font is bundled + base64-inlined (no Google Fonts host to
|
|
472
480
|
// allow); createLouiseMiddleware auto-allows `data:` fonts in the CSP, so the
|
|
473
|
-
// inlined @font-face needs no manual `font-src` entry.
|
|
474
|
-
|
|
481
|
+
// inlined @font-face needs no manual `font-src` entry. Module + config `style`
|
|
482
|
+
// origins (the Square SDK's stylesheet) are baked in here; `astroid build`
|
|
483
|
+
// regenerates this file, so a config change lands on the next deploy.
|
|
484
|
+
const cspStyleSrc = astroidCspStyleSrc(config);
|
|
475
485
|
const portal = astroidPortal(config);
|
|
476
486
|
const tenancy = config.tenancy;
|
|
477
487
|
const rewritePrefix = tenancy?.rewritePrefix ?? ASTROID_TENANT_PREFIX;
|
|
@@ -538,6 +548,10 @@ export function generateAstroidMiddleware(config) {
|
|
|
538
548
|
"export const onRequest = createLouiseMiddleware({",
|
|
539
549
|
" resolveEditor: (request) => resolveEditor(request),",
|
|
540
550
|
" rateLimit: { rules: RATE_RULES, kv: () => env.RL },",
|
|
551
|
+
" // The same deny-by-default gate for /api/louise routes that reach Astro —",
|
|
552
|
+
" // anything the worker's routes didn't answer. A second check behind the",
|
|
553
|
+
" // worker's gate, and free: the editor is resolved here on every request.",
|
|
554
|
+
" apiGate: true,",
|
|
541
555
|
// `extend` runs once and may need to populate BOTH — a tenanted site with a
|
|
542
556
|
// portal resolves a tenant and a customer on the same request.
|
|
543
557
|
...(portal || tenancy
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "astroidjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"access": "public"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"louise-toolkit": "^0.
|
|
63
|
+
"louise-toolkit": "^0.30.1"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
66
|
"@types/node": "^24.13.3",
|