astroidjs 0.1.1 → 0.2.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/README.md +240 -5
- package/bin/astroid.mjs +185 -9
- package/dist/analytics/index.d.ts +37 -0
- package/dist/analytics/index.js +108 -0
- package/dist/astro/csp.d.ts +64 -0
- package/dist/astro/csp.js +173 -0
- package/dist/astro/index.d.ts +1 -0
- package/dist/astro/index.js +7 -0
- package/dist/commerce/adapters.d.ts +60 -0
- package/dist/commerce/adapters.js +90 -0
- package/dist/commerce/checkout-scaffold.d.ts +42 -0
- package/dist/commerce/checkout-scaffold.js +306 -0
- package/dist/commerce/checkout.d.ts +72 -0
- package/dist/commerce/checkout.js +124 -0
- package/dist/commerce/index.d.ts +8 -0
- package/dist/commerce/index.js +9 -0
- package/dist/commerce/loader.d.ts +71 -0
- package/dist/commerce/loader.js +90 -0
- package/dist/commerce/mirror.d.ts +67 -0
- package/dist/commerce/mirror.js +203 -0
- package/dist/commerce/roles.d.ts +38 -0
- package/dist/commerce/roles.js +93 -0
- package/dist/commerce/secrets.d.ts +74 -0
- package/dist/commerce/secrets.js +129 -0
- package/dist/commerce/sync.d.ts +86 -0
- package/dist/commerce/sync.js +154 -0
- package/dist/components/sections.d.ts +577 -0
- package/dist/components/sections.js +425 -0
- package/dist/config.d.ts +174 -12
- package/dist/config.js +43 -1
- package/dist/email/index.d.ts +4 -0
- package/dist/email/index.js +5 -0
- package/dist/email/inquiry.d.ts +33 -0
- package/dist/email/inquiry.js +63 -0
- package/dist/email/send.d.ts +120 -0
- package/dist/email/send.js +196 -0
- package/dist/email/templates.d.ts +24 -0
- package/dist/email/templates.js +184 -0
- package/dist/email/theme.d.ts +24 -0
- package/dist/email/theme.js +150 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/map/index.d.ts +3 -0
- package/dist/map/index.js +4 -0
- package/dist/map/pmtiles.d.ts +92 -0
- package/dist/map/pmtiles.js +130 -0
- package/dist/map/scaffold.d.ts +29 -0
- package/dist/map/scaffold.js +212 -0
- package/dist/map/style.d.ts +58 -0
- package/dist/map/style.js +154 -0
- package/dist/portal/config.d.ts +26 -0
- package/dist/portal/config.js +50 -0
- package/dist/portal/guard.d.ts +48 -0
- package/dist/portal/guard.js +64 -0
- package/dist/portal/index.d.ts +5 -0
- package/dist/portal/index.js +6 -0
- package/dist/portal/nav.d.ts +26 -0
- package/dist/portal/nav.js +35 -0
- package/dist/portal/scaffold.d.ts +28 -0
- package/dist/portal/scaffold.js +140 -0
- package/dist/portal/session.d.ts +36 -0
- package/dist/portal/session.js +86 -0
- package/dist/portfolio/index.d.ts +1 -0
- package/dist/portfolio/index.js +4 -0
- package/dist/portfolio/scaffold.d.ts +9 -0
- package/dist/portfolio/scaffold.js +93 -0
- package/dist/project/actions.d.ts +3 -0
- package/dist/project/actions.js +106 -0
- package/dist/project/generate.d.ts +15 -0
- package/dist/project/generate.js +144 -2
- package/dist/project/index.d.ts +2 -0
- package/dist/project/index.js +2 -0
- package/dist/project/scaffold.d.ts +29 -0
- package/dist/project/scaffold.js +140 -0
- package/dist/pwa/generate.d.ts +49 -0
- package/dist/pwa/generate.js +218 -0
- package/dist/pwa/index.d.ts +1 -0
- package/dist/pwa/index.js +2 -0
- package/dist/queues/consumer.d.ts +29 -0
- package/dist/queues/consumer.js +37 -0
- package/dist/queues/index.d.ts +4 -0
- package/dist/queues/index.js +5 -0
- package/dist/queues/messages.d.ts +60 -0
- package/dist/queues/messages.js +71 -0
- package/dist/queues/scaffold.d.ts +44 -0
- package/dist/queues/scaffold.js +204 -0
- package/dist/queues/webhook.d.ts +60 -0
- package/dist/queues/webhook.js +81 -0
- package/dist/realtime/index.d.ts +1 -0
- package/dist/realtime/index.js +4 -0
- package/dist/realtime/scaffold.d.ts +30 -0
- package/dist/realtime/scaffold.js +159 -0
- package/dist/schema/collections.d.ts +42 -8
- package/dist/schema/collections.js +102 -8
- package/dist/schema/generate.js +10 -1
- package/dist/secrets.d.ts +54 -0
- package/dist/secrets.js +80 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.js +2 -0
- package/dist/security/rate-rules.d.ts +21 -0
- package/dist/security/rate-rules.js +107 -0
- package/dist/seo/index.d.ts +3 -0
- package/dist/seo/index.js +4 -0
- package/dist/seo/resolve.d.ts +68 -0
- package/dist/seo/resolve.js +73 -0
- package/dist/seo/routes.d.ts +44 -0
- package/dist/seo/routes.js +104 -0
- package/dist/seo/structured-data.d.ts +51 -0
- package/dist/seo/structured-data.js +105 -0
- package/dist/status.d.ts +51 -0
- package/dist/status.js +113 -0
- package/dist/worker/generate.d.ts +18 -10
- package/dist/worker/generate.js +325 -37
- package/dist/worker/routes.d.ts +1 -1
- package/dist/worker/routes.js +42 -0
- package/dist/workflow/advance.d.ts +102 -0
- package/dist/workflow/advance.js +145 -0
- package/dist/workflow/config.d.ts +60 -0
- package/dist/workflow/config.js +73 -0
- package/dist/workflow/generate.d.ts +22 -0
- package/dist/workflow/generate.js +138 -0
- package/dist/workflow/index.d.ts +3 -0
- package/dist/workflow/index.js +4 -0
- package/package.json +21 -5
- package/src/components/Editable.astro +33 -9
- package/src/components/JustifiedGallery.astro +254 -0
- package/src/components/MediaSlot.astro +178 -0
- package/src/components/PortalShell.astro +80 -0
- package/src/components/RegisterSW.astro +45 -0
- package/src/components/Section.astro +101 -35
- package/src/components/Sections.astro +64 -0
- package/src/components/Seo.astro +57 -0
- package/src/components/StageBar.astro +137 -0
- package/src/components/StructuredData.astro +33 -0
- package/src/components/justify.ts +170 -0
- package/src/components/media-meta.ts +174 -0
- package/src/components/sections/AboutIntro.astro +46 -0
- package/src/components/sections/Banner.astro +31 -0
- package/src/components/sections/Contact.astro +22 -9
- package/src/components/sections/Cta.astro +33 -10
- package/src/components/sections/Faq.astro +50 -0
- package/src/components/sections/FeatureGrid.astro +40 -11
- package/src/components/sections/Gallery.astro +46 -0
- package/src/components/sections/Hero.astro +40 -12
- package/src/components/sections/LocationHours.astro +59 -0
- package/src/components/sections/Media.astro +44 -0
- package/src/components/sections/PricingTiers.astro +79 -0
- package/src/components/sections/ProductGrid.astro +73 -0
- package/src/components/sections/SplitImage.astro +61 -0
- package/src/components/sections/Steps.astro +58 -0
- package/src/components/sections/Testimonial.astro +51 -0
- package/src/components/sections.ts +452 -67
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// Real-visitor Core Web Vitals (#106 CWV) — the field-data half of the health
|
|
4
|
+
// co-pilot.
|
|
5
|
+
//
|
|
6
|
+
// `HealthSummary` has carried an optional `cwv` field since the health module
|
|
7
|
+
// landed, and the Health panel renders a "not measured yet" badge when it's
|
|
8
|
+
// absent. Astroid never populated it, so that badge was permanently accurate and
|
|
9
|
+
// permanently useless — the same dead-UI shape as the missing overview route.
|
|
10
|
+
//
|
|
11
|
+
// The loop has three parts and needs all three to close:
|
|
12
|
+
//
|
|
13
|
+
// 1. INGEST — a beacon in the page POSTs LCP/CLS/INP to `/api/louise/vitals`,
|
|
14
|
+
// which writes a data point to an Analytics Engine dataset.
|
|
15
|
+
// 2. STORE — the `analytics_engine_datasets` binding. Free, and the route
|
|
16
|
+
// degrades to accept-and-drop without it.
|
|
17
|
+
// 3. READ BACK — the daily health scan queries the p75 over the last day via
|
|
18
|
+
// the Analytics Engine SQL API and folds it into the summary.
|
|
19
|
+
//
|
|
20
|
+
// Step 3 is the one with credentials: the SQL API is account-scoped and needs an
|
|
21
|
+
// API token, which no binding provides. So it follows the dormant-until-
|
|
22
|
+
// provisioned convention — collection runs regardless, and the badge stays "not
|
|
23
|
+
// measured yet" until the pair is real, rather than the scan erroring.
|
|
24
|
+
/** The Analytics Engine dataset binding name. */
|
|
25
|
+
export const ASTROID_VITALS_BINDING = "VITALS";
|
|
26
|
+
/** The dataset name, derived from the project key so two Astroid sites on one
|
|
27
|
+
* account don't write into the same table. */
|
|
28
|
+
export function astroidVitalsDataset(config) {
|
|
29
|
+
return `${config.key.replace(/[^a-z0-9_]/gi, "_")}_web_vitals`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Credentials the CWV READ-BACK needs — not the collection.
|
|
33
|
+
*
|
|
34
|
+
* The Analytics Engine SQL API is account-scoped and has no binding, so a token
|
|
35
|
+
* is unavoidable. Both are required together: an account id without a token (or
|
|
36
|
+
* vice versa) can't query, so partial provisioning reads as dormant exactly like
|
|
37
|
+
* every other Astroid module.
|
|
38
|
+
*/
|
|
39
|
+
export const ASTROID_VITALS_SECRET_NAMES = ["CF_ACCOUNT_ID", "CF_API_TOKEN"];
|
|
40
|
+
/**
|
|
41
|
+
* The public beacon, as a static `public/vitals.js`.
|
|
42
|
+
*
|
|
43
|
+
* A FILE rather than an inline script, and that is a CSP decision, not a style
|
|
44
|
+
* one: Astro hashes the scripts it processes into `script-src`, and an
|
|
45
|
+
* `is:inline` script carrying generated content cannot be hashed — it would be
|
|
46
|
+
* blocked. Served from `public/`, it is same-origin and covered by
|
|
47
|
+
* `script-src 'self'` with no policy change at all.
|
|
48
|
+
*/
|
|
49
|
+
export function generateAstroidVitalsBeacon(config, beacon) {
|
|
50
|
+
void config;
|
|
51
|
+
return {
|
|
52
|
+
path: "public/vitals.js",
|
|
53
|
+
contents: [
|
|
54
|
+
"// Real-visitor Core Web Vitals beacon — generated by astroidjs.",
|
|
55
|
+
"//",
|
|
56
|
+
"// A static file, not an inline script: Astro hashes processed scripts into",
|
|
57
|
+
"// script-src, and an is:inline script with generated content can't be",
|
|
58
|
+
"// hashed, so it would be CSP-blocked. From public/ it is same-origin and",
|
|
59
|
+
"// already covered by script-src 'self'.",
|
|
60
|
+
"//",
|
|
61
|
+
"// It reports once per page, on visibilitychange → hidden, via sendBeacon —",
|
|
62
|
+
"// so it costs nothing on the critical path and never blocks unload.",
|
|
63
|
+
beacon,
|
|
64
|
+
"",
|
|
65
|
+
].join("\n"),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The health scan's CWV read-back, as source for the generated worker.
|
|
70
|
+
*
|
|
71
|
+
* Returns `undefined` — so the badge stays "not measured yet" — when the API
|
|
72
|
+
* credentials aren't set, the query fails, or there is no field data yet. Never
|
|
73
|
+
* throws: a failed vitals query must not take down the rest of the health scan,
|
|
74
|
+
* which is mostly cheap COUNTs that have nothing to do with it.
|
|
75
|
+
*/
|
|
76
|
+
export function generateAstroidCwvQuery(config) {
|
|
77
|
+
const dataset = astroidVitalsDataset(config);
|
|
78
|
+
return [
|
|
79
|
+
"// Real-visitor CWV over the last day, from the Analytics Engine SQL API.",
|
|
80
|
+
"//",
|
|
81
|
+
"// The SQL API is account-scoped and has no binding, so it needs a token.",
|
|
82
|
+
"// Unprovisioned (or a failed query, or no field data yet) → undefined, and",
|
|
83
|
+
"// the Health panel shows 'not measured yet' rather than the scan failing.",
|
|
84
|
+
"async function queryCwv(env: CloudflareEnv) {",
|
|
85
|
+
" const accountId = await readModuleSecret(env.CF_ACCOUNT_ID);",
|
|
86
|
+
" const token = await readModuleSecret(env.CF_API_TOKEN);",
|
|
87
|
+
" if (!accountId || !token) return undefined;",
|
|
88
|
+
" try {",
|
|
89
|
+
" const res = await fetch(",
|
|
90
|
+
" `https://api.cloudflare.com/client/v4/accounts/${accountId}/analytics_engine/sql`,",
|
|
91
|
+
" {",
|
|
92
|
+
' method: "POST",',
|
|
93
|
+
" headers: { authorization: `Bearer ${token}` },",
|
|
94
|
+
` body: cwvSqlQuery(${JSON.stringify(dataset)}, 24),`,
|
|
95
|
+
" },",
|
|
96
|
+
" );",
|
|
97
|
+
" if (!res.ok) return undefined;",
|
|
98
|
+
" const body = (await res.json()) as {",
|
|
99
|
+
" data?: { metric?: unknown; p75?: unknown; samples?: unknown }[];",
|
|
100
|
+
" };",
|
|
101
|
+
" const summary = summarizeCwv(parseCwvRows(body.data ?? []));",
|
|
102
|
+
" return summary.sampleSize > 0 ? summary : undefined;",
|
|
103
|
+
" } catch {",
|
|
104
|
+
" return undefined;",
|
|
105
|
+
" }",
|
|
106
|
+
"}",
|
|
107
|
+
];
|
|
108
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AstroidConfig, CspOrigins } from "../config.js";
|
|
2
|
+
/** A `sha256-…` CSP hash, in the shape Astro's `security.csp.hashes` takes. */
|
|
3
|
+
export type CspHash = `${"sha256" | "sha384" | "sha512"}-${string}`;
|
|
4
|
+
/**
|
|
5
|
+
* The directives Astro lets a config own. `script-src` and `style-src` are
|
|
6
|
+
* absent by design — Astro owns the first (it hashes what it processes) and the
|
|
7
|
+
* middleware owns the second.
|
|
8
|
+
*
|
|
9
|
+
* Mirrored from Astro's own union rather than imported, so `astroidjs/astro`
|
|
10
|
+
* needs no `astro` dependency. Two things fall out of matching it exactly: the
|
|
11
|
+
* returned config is assignable to `defineConfig`'s `security` without a cast,
|
|
12
|
+
* and a typo like `"img-srcs 'self'"` fails to compile *here* rather than
|
|
13
|
+
* silently producing a directive no browser enforces.
|
|
14
|
+
*/
|
|
15
|
+
type CspDirectiveName = "base-uri" | "child-src" | "connect-src" | "default-src" | "fenced-frame-src" | "font-src" | "form-action" | "frame-ancestors" | "frame-src" | "img-src" | "manifest-src" | "media-src" | "object-src" | "referrer" | "report-to" | "report-uri" | "require-trusted-types-for" | "sandbox" | "trusted-types" | "upgrade-insecure-requests" | "worker-src";
|
|
16
|
+
/** One rendered directive line, e.g. `"default-src 'self'"`. */
|
|
17
|
+
export type CspDirective = `${CspDirectiveName}${string}`;
|
|
18
|
+
/** The `security` block for `astro.config.mjs`, structurally typed so Astroid
|
|
19
|
+
* doesn't take a hard dependency on Astro's types. */
|
|
20
|
+
export interface AstroidSecurityConfig {
|
|
21
|
+
csp: {
|
|
22
|
+
algorithm: "SHA-256";
|
|
23
|
+
scriptDirective: {
|
|
24
|
+
resources: string[];
|
|
25
|
+
hashes: CspHash[];
|
|
26
|
+
};
|
|
27
|
+
directives: CspDirective[];
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Hash of Solid's inline hydration bootstrap.
|
|
32
|
+
*
|
|
33
|
+
* `@astrojs/solid-js` injects this script on every page with an island, but
|
|
34
|
+
* Astro's CSP tracker only hashes scripts it processed itself — so without this
|
|
35
|
+
* the bootstrap is blocked under `script-src 'self'` and every island silently
|
|
36
|
+
* fails to hydrate. Computed from `generateHydrationScript()` (the same call the
|
|
37
|
+
* renderer makes), so a solid-js upgrade that changes the bootstrap updates the
|
|
38
|
+
* hash on the next build.
|
|
39
|
+
*/
|
|
40
|
+
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
|
+
/**
|
|
48
|
+
* The `security` block for `astro.config.mjs` — Astro's half of the split.
|
|
49
|
+
*
|
|
50
|
+
* `style-src` is deliberately absent: the generated middleware rewrites it per
|
|
51
|
+
* response, and declaring it here would be the hash-vs-`'unsafe-inline'`
|
|
52
|
+
* conflict described at the top of this file.
|
|
53
|
+
*/
|
|
54
|
+
export declare function astroidSecurity(config: AstroidConfig): AstroidSecurityConfig;
|
|
55
|
+
/**
|
|
56
|
+
* Vite build options the CSP depends on. `assetsInlineLimit: 0` stops Vite from
|
|
57
|
+
* inlining small assets as `data:` URLs — an inlined script would be inline, and
|
|
58
|
+
* therefore unhashed, and therefore blocked by `script-src 'self'`. Spread this
|
|
59
|
+
* into `vite.build` rather than remembering why the number is zero.
|
|
60
|
+
*/
|
|
61
|
+
export declare const ASTROID_VITE_BUILD: {
|
|
62
|
+
readonly assetsInlineLimit: 0;
|
|
63
|
+
};
|
|
64
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// Content-Security-Policy composition, owned by Astroid rather than re-derived
|
|
4
|
+
// per site.
|
|
5
|
+
//
|
|
6
|
+
// The split is the non-obvious part, and all three consuming sites arrived at it
|
|
7
|
+
// the hard way:
|
|
8
|
+
//
|
|
9
|
+
// - **Astro owns `script-src`.** Its `security.csp` hashes every script it
|
|
10
|
+
// processes, so the policy can be `'self'` with no `'unsafe-inline'`. What it
|
|
11
|
+
// does NOT hash is Solid's hydration bootstrap, which `@astrojs/solid-js`
|
|
12
|
+
// injects on every page carrying an island — Astro only tracks its own inline
|
|
13
|
+
// scripts. So we compute that hash from the very function the renderer calls,
|
|
14
|
+
// which means it follows solid-js upgrades instead of going stale as a
|
|
15
|
+
// copy-pasted literal.
|
|
16
|
+
// - **The middleware owns `style-src`.** Louise's data-driven `style=""`
|
|
17
|
+
// carriers and the editor's runtime-injected `<style>` need
|
|
18
|
+
// `'unsafe-inline'`, and per spec a single hash in `style-src` VOIDS
|
|
19
|
+
// `'unsafe-inline'` — so the two cannot coexist in one directive. The
|
|
20
|
+
// generated middleware rewrites that one directive after the fact
|
|
21
|
+
// (`cspStyleSrc`), leaving Astro's script hashes verbatim.
|
|
22
|
+
//
|
|
23
|
+
// Modules contribute origins; this module owns the composition seam.
|
|
24
|
+
//
|
|
25
|
+
// This subpath is build-time only (Node, `node:crypto`, `solid-js/web`) and is
|
|
26
|
+
// deliberately NOT re-exported from the `astroidjs` entry the generated Worker
|
|
27
|
+
// imports.
|
|
28
|
+
import { createHash } from "node:crypto";
|
|
29
|
+
import { generateHydrationScript } from "solid-js/web";
|
|
30
|
+
import { astroidCommerceProviders } from "../commerce/roles.js";
|
|
31
|
+
/**
|
|
32
|
+
* Hash of Solid's inline hydration bootstrap.
|
|
33
|
+
*
|
|
34
|
+
* `@astrojs/solid-js` injects this script on every page with an island, but
|
|
35
|
+
* Astro's CSP tracker only hashes scripts it processed itself — so without this
|
|
36
|
+
* the bootstrap is blocked under `script-src 'self'` and every island silently
|
|
37
|
+
* fails to hydrate. Computed from `generateHydrationScript()` (the same call the
|
|
38
|
+
* renderer makes), so a solid-js upgrade that changes the bootstrap updates the
|
|
39
|
+
* hash on the next build.
|
|
40
|
+
*/
|
|
41
|
+
export function solidHydrationHash() {
|
|
42
|
+
const inner = generateHydrationScript().match(/<script[^>]*>([\s\S]*?)<\/script>/)?.[1] ?? "";
|
|
43
|
+
return `sha256-${createHash("sha256").update(inner).digest("base64")}`;
|
|
44
|
+
}
|
|
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
|
+
/**
|
|
120
|
+
* Render one directive. The name is a literal from the union above, so the
|
|
121
|
+
* concatenation is a valid `CspDirective` by construction — which is what the
|
|
122
|
+
* assertion is standing in for (TS widens template concatenation to `string`).
|
|
123
|
+
*/
|
|
124
|
+
function directive(name, ...sources) {
|
|
125
|
+
const list = sources.flat().filter(Boolean).join(" ");
|
|
126
|
+
return (list ? `${name} ${list}` : name);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The `security` block for `astro.config.mjs` — Astro's half of the split.
|
|
130
|
+
*
|
|
131
|
+
* `style-src` is deliberately absent: the generated middleware rewrites it per
|
|
132
|
+
* response, and declaring it here would be the hash-vs-`'unsafe-inline'`
|
|
133
|
+
* conflict described at the top of this file.
|
|
134
|
+
*/
|
|
135
|
+
export function astroidSecurity(config) {
|
|
136
|
+
const origins = astroidCspOrigins(config);
|
|
137
|
+
return {
|
|
138
|
+
csp: {
|
|
139
|
+
algorithm: "SHA-256",
|
|
140
|
+
scriptDirective: {
|
|
141
|
+
resources: ["'self'", ...origins.script],
|
|
142
|
+
hashes: [solidHydrationHash()],
|
|
143
|
+
},
|
|
144
|
+
directives: [
|
|
145
|
+
directive("default-src", "'self'"),
|
|
146
|
+
// Editor-authored content may embed any https image (the sanitizer
|
|
147
|
+
// already gates that), and provider catalogs serve images from hosts
|
|
148
|
+
// that aren't ours to pin. `data:`/`blob:` cover canvas + object URLs.
|
|
149
|
+
directive("img-src", "'self' https: data: blob:", origins.img),
|
|
150
|
+
// Louise base64-inlines its brand font, hence `data:`.
|
|
151
|
+
directive("font-src", "'self' data:", origins.font),
|
|
152
|
+
directive("connect-src", "'self'", origins.connect),
|
|
153
|
+
// A bare `frame-src` with no source list is invalid CSP, so an empty
|
|
154
|
+
// origin set has to become an explicit `'none'`.
|
|
155
|
+
directive("frame-src", origins.frame.length ? origins.frame : "'none'"),
|
|
156
|
+
// Clickjacking + injection floor. `frame-ancestors 'none'` is the one
|
|
157
|
+
// that can't be set from a meta tag, so it has to live here.
|
|
158
|
+
directive("frame-ancestors", "'none'"),
|
|
159
|
+
directive("base-uri", "'self'"),
|
|
160
|
+
directive("form-action", "'self'"),
|
|
161
|
+
directive("object-src", "'none'"),
|
|
162
|
+
directive("worker-src", "'self'", origins.worker),
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Vite build options the CSP depends on. `assetsInlineLimit: 0` stops Vite from
|
|
169
|
+
* inlining small assets as `data:` URLs — an inlined script would be inline, and
|
|
170
|
+
* therefore unhashed, and therefore blocked by `script-src 'self'`. Spread this
|
|
171
|
+
* into `vite.build` rather than remembering why the number is zero.
|
|
172
|
+
*/
|
|
173
|
+
export const ASTROID_VITE_BUILD = { assetsInlineLimit: 0 };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ASTROID_VITE_BUILD, astroidCspOrigins, astroidSecurity, type AstroidSecurityConfig, type CspHash, solidHydrationHash, } from "./csp.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// `astroidjs/astro` — the BUILD-TIME surface, imported from `astro.config.mjs`.
|
|
4
|
+
// Kept off the main entry on purpose: it reaches for `node:crypto` and
|
|
5
|
+
// `solid-js/web`, neither of which belongs in the Worker bundle the generated
|
|
6
|
+
// `worker.ts` produces.
|
|
7
|
+
export { ASTROID_VITE_BUILD, astroidCspOrigins, astroidSecurity, solidHydrationHash, } from "./csp.js";
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { CatalogItem } from "./sync.js";
|
|
2
|
+
/** The subset of `SquareCatalogItem` the mirror reads. */
|
|
3
|
+
export interface SquareItemLike {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
imageUrl?: string | null;
|
|
7
|
+
variations?: {
|
|
8
|
+
id: string;
|
|
9
|
+
name: string;
|
|
10
|
+
sku?: string | null;
|
|
11
|
+
priceCents: number;
|
|
12
|
+
currency?: string;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
/** The subset of `FwProduct` the mirror reads. */
|
|
16
|
+
export interface FourthwallProductLike {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
slug?: string;
|
|
20
|
+
images?: {
|
|
21
|
+
url?: string;
|
|
22
|
+
}[];
|
|
23
|
+
variants?: {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
sku?: string;
|
|
27
|
+
unitPrice?: {
|
|
28
|
+
value?: number;
|
|
29
|
+
currency?: string;
|
|
30
|
+
} | null;
|
|
31
|
+
stock?: unknown;
|
|
32
|
+
attributes?: unknown;
|
|
33
|
+
}[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Square item → `CatalogItem`.
|
|
37
|
+
*
|
|
38
|
+
* `price` is the LOWEST variation price. A Square item is a family ("Bag of
|
|
39
|
+
* beans" with 12oz and 2lb variations), so a single headline number has to mean
|
|
40
|
+
* "from" — taking the first variation's price instead would change with Square's
|
|
41
|
+
* ordering and quietly misprice the card.
|
|
42
|
+
*/
|
|
43
|
+
export declare function squareToCatalogItem(item: SquareItemLike): CatalogItem;
|
|
44
|
+
/**
|
|
45
|
+
* Fourthwall product → `CatalogItem`. Same "lowest variant wins" rule as Square,
|
|
46
|
+
* for the same reason.
|
|
47
|
+
*
|
|
48
|
+
* Fourthwall already prices in major units, so there's no conversion — mirroring
|
|
49
|
+
* `lowestPrice` in `louise-toolkit/commerce/fourthwall`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function fourthwallToCatalogItem(product: FourthwallProductLike): CatalogItem;
|
|
52
|
+
/**
|
|
53
|
+
* The normalizer for a storefront provider.
|
|
54
|
+
*
|
|
55
|
+
* Stripe is absent on purpose, not by omission: its client has no catalog API,
|
|
56
|
+
* so it can only hold the invoicing role and never reaches a catalog sync. The
|
|
57
|
+
* config validation in `assertCommerceRoles` makes that unreachable anyway; this
|
|
58
|
+
* returns null rather than throwing so a caller can degrade.
|
|
59
|
+
*/
|
|
60
|
+
export declare function catalogNormalizer(provider: string): ((item: never) => CatalogItem) | null;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// Provider → `CatalogItem` normalizers.
|
|
4
|
+
//
|
|
5
|
+
// This is the file the whole module exists for. themidwestartist.com's loader
|
|
6
|
+
// says it outright: coracle runs the same helper over Square, "only the
|
|
7
|
+
// content/repo reads differ — issue: repo drift." Two sites, one intent, two
|
|
8
|
+
// hand-written translations that drifted apart. The translation is mechanical,
|
|
9
|
+
// so it belongs here once.
|
|
10
|
+
//
|
|
11
|
+
// Each provider's client already returns a normalized-for-that-provider shape
|
|
12
|
+
// (`SquareCatalogItem`, `FwProduct`); these functions take that last step to the
|
|
13
|
+
// shape the mirror stores. Deliberately pure — they take the provider's objects,
|
|
14
|
+
// not credentials or an `env`, so they're trivially testable and the caller
|
|
15
|
+
// keeps control of how the fetch happens (cached, paged, rate-limited).
|
|
16
|
+
/** Minor units → major. Square prices in cents; the mirror stores dollars, since
|
|
17
|
+
* that's what a template renders and what an owner types into an overlay. */
|
|
18
|
+
const toMajor = (cents) => Math.round(cents) / 100;
|
|
19
|
+
/**
|
|
20
|
+
* Square item → `CatalogItem`.
|
|
21
|
+
*
|
|
22
|
+
* `price` is the LOWEST variation price. A Square item is a family ("Bag of
|
|
23
|
+
* beans" with 12oz and 2lb variations), so a single headline number has to mean
|
|
24
|
+
* "from" — taking the first variation's price instead would change with Square's
|
|
25
|
+
* ordering and quietly misprice the card.
|
|
26
|
+
*/
|
|
27
|
+
export function squareToCatalogItem(item) {
|
|
28
|
+
const prices = (item.variations ?? []).map((v) => v.priceCents).filter((c) => Number.isFinite(c));
|
|
29
|
+
return {
|
|
30
|
+
externalId: item.id,
|
|
31
|
+
name: item.name,
|
|
32
|
+
price: prices.length ? toMajor(Math.min(...prices)) : 0,
|
|
33
|
+
images: item.imageUrl ? [item.imageUrl] : [],
|
|
34
|
+
variants: (item.variations ?? []).map((v) => ({
|
|
35
|
+
id: v.id,
|
|
36
|
+
name: v.name,
|
|
37
|
+
sku: v.sku ?? null,
|
|
38
|
+
price: toMajor(v.priceCents),
|
|
39
|
+
currency: v.currency ?? "USD",
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Fourthwall product → `CatalogItem`. Same "lowest variant wins" rule as Square,
|
|
45
|
+
* for the same reason.
|
|
46
|
+
*
|
|
47
|
+
* Fourthwall already prices in major units, so there's no conversion — mirroring
|
|
48
|
+
* `lowestPrice` in `louise-toolkit/commerce/fourthwall`.
|
|
49
|
+
*/
|
|
50
|
+
export function fourthwallToCatalogItem(product) {
|
|
51
|
+
const prices = (product.variants ?? [])
|
|
52
|
+
.map((v) => v.unitPrice?.value)
|
|
53
|
+
.filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
54
|
+
return {
|
|
55
|
+
externalId: product.id,
|
|
56
|
+
name: product.name,
|
|
57
|
+
price: prices.length ? Math.min(...prices) : 0,
|
|
58
|
+
images: (product.images ?? [])
|
|
59
|
+
.map((i) => i.url)
|
|
60
|
+
.filter((u) => typeof u === "string" && u.length > 0),
|
|
61
|
+
variants: (product.variants ?? []).map((v) => ({
|
|
62
|
+
id: v.id,
|
|
63
|
+
name: v.name,
|
|
64
|
+
sku: v.sku ?? null,
|
|
65
|
+
price: v.unitPrice?.value ?? 0,
|
|
66
|
+
currency: v.unitPrice?.currency ?? "USD",
|
|
67
|
+
attributes: v.attributes ?? null,
|
|
68
|
+
stock: v.stock ?? null,
|
|
69
|
+
})),
|
|
70
|
+
externalSlug: product.slug,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The normalizer for a storefront provider.
|
|
75
|
+
*
|
|
76
|
+
* Stripe is absent on purpose, not by omission: its client has no catalog API,
|
|
77
|
+
* so it can only hold the invoicing role and never reaches a catalog sync. The
|
|
78
|
+
* config validation in `assertCommerceRoles` makes that unreachable anyway; this
|
|
79
|
+
* returns null rather than throwing so a caller can degrade.
|
|
80
|
+
*/
|
|
81
|
+
export function catalogNormalizer(provider) {
|
|
82
|
+
switch (provider) {
|
|
83
|
+
case "square":
|
|
84
|
+
return squareToCatalogItem;
|
|
85
|
+
case "fourthwall":
|
|
86
|
+
return fourthwallToCatalogItem;
|
|
87
|
+
default:
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AstroidConfig } from "../config.js";
|
|
2
|
+
/** Does this project take card payments in-page? Square storefront only. */
|
|
3
|
+
export declare function usesCardCheckout(config: AstroidConfig): boolean;
|
|
4
|
+
/**
|
|
5
|
+
* `src/pages/api/checkout.ts` — the server-authoritative payment route.
|
|
6
|
+
*
|
|
7
|
+
* Scaffold-once: a real store adds shipping, tax, an order record, a receipt
|
|
8
|
+
* email. What Astroid fixes is the sequence, because every step of it is a place
|
|
9
|
+
* where getting it wrong costs money rather than throwing an error.
|
|
10
|
+
*
|
|
11
|
+
* Returns null unless this project takes card payments.
|
|
12
|
+
*/
|
|
13
|
+
export declare function generateAstroidCheckoutRoute(config: AstroidConfig): string | null;
|
|
14
|
+
/**
|
|
15
|
+
* `src/components/SquareCard.astro` — the card input.
|
|
16
|
+
*
|
|
17
|
+
* Square's Web Payments SDK renders the field in an iframe from their CDN and
|
|
18
|
+
* hands back a single-use token, so the raw card number never touches the Worker
|
|
19
|
+
* and the site stays out of PCI scope. That is the entire reason this is a
|
|
20
|
+
* component and not an `<input>`.
|
|
21
|
+
*
|
|
22
|
+
* Returns null unless this project takes card payments.
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateAstroidSquareCard(config: AstroidConfig): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* The wrangler `vars` the card input needs, or `[]`.
|
|
27
|
+
*
|
|
28
|
+
* PUBLIC values, so they are vars rather than entries in the secret roster: the
|
|
29
|
+
* application id is shipped to the browser by design, and the environment is a
|
|
30
|
+
* choice, not a credential. Putting them in `credentials` would also fold them
|
|
31
|
+
* into the dormancy gate, which is about whether the module can safely CALL
|
|
32
|
+
* Square — a different question from whether the card field can render.
|
|
33
|
+
*/
|
|
34
|
+
export declare function astroidCheckoutVars(config: AstroidConfig): {
|
|
35
|
+
name: string;
|
|
36
|
+
value: string;
|
|
37
|
+
}[];
|
|
38
|
+
/**
|
|
39
|
+
* The `CloudflareEnv` members the card input adds, as a block `create-astroid`
|
|
40
|
+
* substitutes into `src/env.d.ts`. Empty without card checkout.
|
|
41
|
+
*/
|
|
42
|
+
export declare function generateAstroidCheckoutEnv(config: AstroidConfig): string;
|