astroidjs 0.1.2 → 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.
Files changed (154) hide show
  1. package/README.md +240 -5
  2. package/bin/astroid.mjs +185 -9
  3. package/dist/analytics/index.d.ts +37 -0
  4. package/dist/analytics/index.js +108 -0
  5. package/dist/astro/csp.d.ts +64 -0
  6. package/dist/astro/csp.js +173 -0
  7. package/dist/astro/index.d.ts +1 -0
  8. package/dist/astro/index.js +7 -0
  9. package/dist/commerce/adapters.d.ts +60 -0
  10. package/dist/commerce/adapters.js +90 -0
  11. package/dist/commerce/checkout-scaffold.d.ts +42 -0
  12. package/dist/commerce/checkout-scaffold.js +306 -0
  13. package/dist/commerce/checkout.d.ts +72 -0
  14. package/dist/commerce/checkout.js +124 -0
  15. package/dist/commerce/index.d.ts +8 -0
  16. package/dist/commerce/index.js +9 -0
  17. package/dist/commerce/loader.d.ts +71 -0
  18. package/dist/commerce/loader.js +90 -0
  19. package/dist/commerce/mirror.d.ts +67 -0
  20. package/dist/commerce/mirror.js +203 -0
  21. package/dist/commerce/roles.d.ts +38 -0
  22. package/dist/commerce/roles.js +93 -0
  23. package/dist/commerce/secrets.d.ts +74 -0
  24. package/dist/commerce/secrets.js +129 -0
  25. package/dist/commerce/sync.d.ts +86 -0
  26. package/dist/commerce/sync.js +154 -0
  27. package/dist/components/sections.d.ts +577 -0
  28. package/dist/components/sections.js +425 -0
  29. package/dist/config.d.ts +174 -12
  30. package/dist/config.js +43 -1
  31. package/dist/email/index.d.ts +4 -0
  32. package/dist/email/index.js +5 -0
  33. package/dist/email/inquiry.d.ts +33 -0
  34. package/dist/email/inquiry.js +63 -0
  35. package/dist/email/send.d.ts +120 -0
  36. package/dist/email/send.js +196 -0
  37. package/dist/email/templates.d.ts +24 -0
  38. package/dist/email/templates.js +184 -0
  39. package/dist/email/theme.d.ts +24 -0
  40. package/dist/email/theme.js +150 -0
  41. package/dist/errors.d.ts +14 -0
  42. package/dist/errors.js +17 -0
  43. package/dist/index.d.ts +14 -0
  44. package/dist/index.js +14 -0
  45. package/dist/map/index.d.ts +3 -0
  46. package/dist/map/index.js +4 -0
  47. package/dist/map/pmtiles.d.ts +92 -0
  48. package/dist/map/pmtiles.js +130 -0
  49. package/dist/map/scaffold.d.ts +29 -0
  50. package/dist/map/scaffold.js +212 -0
  51. package/dist/map/style.d.ts +58 -0
  52. package/dist/map/style.js +154 -0
  53. package/dist/portal/config.d.ts +26 -0
  54. package/dist/portal/config.js +50 -0
  55. package/dist/portal/guard.d.ts +48 -0
  56. package/dist/portal/guard.js +64 -0
  57. package/dist/portal/index.d.ts +5 -0
  58. package/dist/portal/index.js +6 -0
  59. package/dist/portal/nav.d.ts +26 -0
  60. package/dist/portal/nav.js +35 -0
  61. package/dist/portal/scaffold.d.ts +28 -0
  62. package/dist/portal/scaffold.js +140 -0
  63. package/dist/portal/session.d.ts +36 -0
  64. package/dist/portal/session.js +86 -0
  65. package/dist/portfolio/index.d.ts +1 -0
  66. package/dist/portfolio/index.js +4 -0
  67. package/dist/portfolio/scaffold.d.ts +9 -0
  68. package/dist/portfolio/scaffold.js +93 -0
  69. package/dist/project/actions.d.ts +3 -0
  70. package/dist/project/actions.js +106 -0
  71. package/dist/project/generate.d.ts +15 -0
  72. package/dist/project/generate.js +144 -2
  73. package/dist/project/index.d.ts +2 -0
  74. package/dist/project/index.js +2 -0
  75. package/dist/project/scaffold.d.ts +29 -0
  76. package/dist/project/scaffold.js +140 -0
  77. package/dist/pwa/generate.d.ts +49 -0
  78. package/dist/pwa/generate.js +218 -0
  79. package/dist/pwa/index.d.ts +1 -0
  80. package/dist/pwa/index.js +2 -0
  81. package/dist/queues/consumer.d.ts +29 -0
  82. package/dist/queues/consumer.js +37 -0
  83. package/dist/queues/index.d.ts +4 -0
  84. package/dist/queues/index.js +5 -0
  85. package/dist/queues/messages.d.ts +60 -0
  86. package/dist/queues/messages.js +71 -0
  87. package/dist/queues/scaffold.d.ts +44 -0
  88. package/dist/queues/scaffold.js +204 -0
  89. package/dist/queues/webhook.d.ts +60 -0
  90. package/dist/queues/webhook.js +81 -0
  91. package/dist/realtime/index.d.ts +1 -0
  92. package/dist/realtime/index.js +4 -0
  93. package/dist/realtime/scaffold.d.ts +30 -0
  94. package/dist/realtime/scaffold.js +159 -0
  95. package/dist/schema/collections.d.ts +41 -7
  96. package/dist/schema/collections.js +101 -12
  97. package/dist/schema/generate.js +10 -1
  98. package/dist/secrets.d.ts +54 -0
  99. package/dist/secrets.js +80 -0
  100. package/dist/security/index.d.ts +1 -0
  101. package/dist/security/index.js +2 -0
  102. package/dist/security/rate-rules.d.ts +21 -0
  103. package/dist/security/rate-rules.js +107 -0
  104. package/dist/seo/index.d.ts +3 -0
  105. package/dist/seo/index.js +4 -0
  106. package/dist/seo/resolve.d.ts +68 -0
  107. package/dist/seo/resolve.js +73 -0
  108. package/dist/seo/routes.d.ts +44 -0
  109. package/dist/seo/routes.js +104 -0
  110. package/dist/seo/structured-data.d.ts +51 -0
  111. package/dist/seo/structured-data.js +105 -0
  112. package/dist/status.d.ts +51 -0
  113. package/dist/status.js +113 -0
  114. package/dist/worker/generate.d.ts +18 -10
  115. package/dist/worker/generate.js +325 -37
  116. package/dist/worker/routes.d.ts +1 -1
  117. package/dist/worker/routes.js +42 -0
  118. package/dist/workflow/advance.d.ts +102 -0
  119. package/dist/workflow/advance.js +145 -0
  120. package/dist/workflow/config.d.ts +60 -0
  121. package/dist/workflow/config.js +73 -0
  122. package/dist/workflow/generate.d.ts +22 -0
  123. package/dist/workflow/generate.js +138 -0
  124. package/dist/workflow/index.d.ts +3 -0
  125. package/dist/workflow/index.js +4 -0
  126. package/package.json +21 -4
  127. package/src/components/Editable.astro +33 -9
  128. package/src/components/JustifiedGallery.astro +254 -0
  129. package/src/components/MediaSlot.astro +178 -0
  130. package/src/components/PortalShell.astro +80 -0
  131. package/src/components/RegisterSW.astro +45 -0
  132. package/src/components/Section.astro +101 -35
  133. package/src/components/Sections.astro +64 -0
  134. package/src/components/Seo.astro +57 -0
  135. package/src/components/StageBar.astro +137 -0
  136. package/src/components/StructuredData.astro +33 -0
  137. package/src/components/justify.ts +170 -0
  138. package/src/components/media-meta.ts +174 -0
  139. package/src/components/sections/AboutIntro.astro +46 -0
  140. package/src/components/sections/Banner.astro +31 -0
  141. package/src/components/sections/Contact.astro +22 -9
  142. package/src/components/sections/Cta.astro +33 -10
  143. package/src/components/sections/Faq.astro +50 -0
  144. package/src/components/sections/FeatureGrid.astro +40 -11
  145. package/src/components/sections/Gallery.astro +46 -0
  146. package/src/components/sections/Hero.astro +40 -12
  147. package/src/components/sections/LocationHours.astro +59 -0
  148. package/src/components/sections/Media.astro +44 -0
  149. package/src/components/sections/PricingTiers.astro +79 -0
  150. package/src/components/sections/ProductGrid.astro +73 -0
  151. package/src/components/sections/SplitImage.astro +61 -0
  152. package/src/components/sections/Steps.astro +58 -0
  153. package/src/components/sections/Testimonial.astro +51 -0
  154. 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;