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.
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 +42 -8
  96. package/dist/schema/collections.js +102 -8
  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 -5
  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,218 @@
1
+ // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
+ //
3
+ // The PWA scaffold — a scoped service worker, a manifest, and the headers they
4
+ // need.
5
+ //
6
+ // The scoping is the whole design, not a detail. A Louise site is CMS-edited:
7
+ // an editor signs in, flips edit mode on, and edits the live page in place. A
8
+ // service worker that cached HTML across the whole origin would serve that
9
+ // editor a stale copy of the page they are trying to change — and the bug would
10
+ // present as "my edits don't save", which is about as far from the cause as a
11
+ // report can get.
12
+ //
13
+ // So the generated worker is scoped, and inside its scope it still refuses to
14
+ // touch anything dynamic:
15
+ //
16
+ // • `/api/*` never cached — checkout, auth, and every Louise write
17
+ // • editor routes never cached — the studio must always be live
18
+ // • edit-mode URLs never cached — `?louise` marks a request as an editing
19
+ // session, and caching one poisons it for everyone
20
+ //
21
+ // Everything else is the ordinary split: navigations network-first with a
22
+ // cached offline shell, hashed assets cache-first because their names change
23
+ // when their content does.
24
+ /** True when this project switched the PWA on. */
25
+ export const usesPwa = (config) => (config.modules ?? []).includes("pwa");
26
+ /** Resolved PWA settings — config over derivation over default. */
27
+ export function resolvePwa(config) {
28
+ const pwa = config.pwa ?? {};
29
+ // Normalized to a leading slash and no trailing one (except root), because
30
+ // scope comparison is a string prefix test and "/order/" vs "/order" would
31
+ // silently exclude the scope root itself.
32
+ const raw = pwa.scope ?? "/";
33
+ const scope = raw === "/" ? "/" : `/${raw.replace(/^\/+|\/+$/g, "")}`;
34
+ return {
35
+ scope,
36
+ shortName: pwa.shortName ?? config.theme.name,
37
+ description: pwa.description ?? `${config.theme.name} — installable app.`,
38
+ display: pwa.display ?? "standalone",
39
+ orientation: pwa.orientation ?? "any",
40
+ backgroundColor: pwa.backgroundColor ?? "#ffffff",
41
+ themeColor: pwa.themeColor ?? config.theme.colors.brand,
42
+ shell: [scope, "/manifest.webmanifest", ...(pwa.shell ?? [])],
43
+ };
44
+ }
45
+ /**
46
+ * `public/manifest.webmanifest`.
47
+ *
48
+ * Icons are declared but NOT generated — a brand's icon is not something a
49
+ * scaffold can invent, and emitting placeholders would produce an installable
50
+ * app with a grey square for a face. The generated README step says to add them.
51
+ */
52
+ export function generateWebManifest(config) {
53
+ if (!usesPwa(config))
54
+ return null;
55
+ const pwa = resolvePwa(config);
56
+ return `${JSON.stringify({
57
+ name: config.theme.name,
58
+ short_name: pwa.shortName,
59
+ description: pwa.description,
60
+ id: pwa.scope,
61
+ start_url: pwa.scope,
62
+ scope: pwa.scope,
63
+ display: pwa.display,
64
+ orientation: pwa.orientation,
65
+ background_color: pwa.backgroundColor,
66
+ theme_color: pwa.themeColor,
67
+ icons: [
68
+ { src: "/icons/icon-192.png", sizes: "192x192", type: "image/png", purpose: "any" },
69
+ { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png", purpose: "any" },
70
+ // A maskable icon is a separate asset, not a flag on the same file: the
71
+ // platform crops it to its own shape, so the artwork needs padding the
72
+ // `any` icon shouldn't have.
73
+ { src: "/icons/maskable-192.png", sizes: "192x192", type: "image/png", purpose: "maskable" },
74
+ { src: "/icons/maskable-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
75
+ ],
76
+ }, null, 2)}\n`;
77
+ }
78
+ /** `public/sw.js`. Plain JS — a service worker is not bundled. */
79
+ export function generateServiceWorker(config) {
80
+ if (!usesPwa(config))
81
+ return null;
82
+ const pwa = resolvePwa(config);
83
+ // Cache name carries the project key so two Astroid apps on one origin (a
84
+ // preview deploy, say) can't read each other's entries.
85
+ const cacheName = `${config.key}-pwa-v1`;
86
+ return [
87
+ `// Service worker for the ${config.theme.name} PWA, scoped to ${pwa.scope}.`,
88
+ "//",
89
+ "// Generated by Astroid. Safe to edit — bump CACHE to invalidate everything",
90
+ "// on the next visit.",
91
+ "//",
92
+ "// What it deliberately never caches, and why:",
93
+ "// /api/* checkout, auth, and every Louise write — a cached POST",
94
+ "// response or a stale session is worse than being offline",
95
+ "// editor routes the studio must always be live",
96
+ "// ?louise URLs an edit-mode request; caching one would serve an editor a",
97
+ "// stale copy of the page they're editing, and the bug would",
98
+ "// present as 'my changes don't save'",
99
+ `const CACHE = ${JSON.stringify(cacheName)};`,
100
+ `const SCOPE = ${JSON.stringify(pwa.scope)};`,
101
+ `const SHELL = ${JSON.stringify([...new Set(pwa.shell)])};`,
102
+ "",
103
+ "self.addEventListener('install', (event) => {",
104
+ " event.waitUntil(",
105
+ " caches",
106
+ " .open(CACHE)",
107
+ " // Best-effort: one 404 or redirect in the shell must not fail the",
108
+ " // whole install and leave the app without a worker.",
109
+ " .then((c) => Promise.allSettled(SHELL.map((u) => c.add(u))))",
110
+ " .then(() => self.skipWaiting()),",
111
+ " );",
112
+ "});",
113
+ "",
114
+ "self.addEventListener('activate', (event) => {",
115
+ " event.waitUntil(",
116
+ " caches",
117
+ " .keys()",
118
+ " .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))",
119
+ " .then(() => self.clients.claim()),",
120
+ " );",
121
+ "});",
122
+ "",
123
+ "/** Requests that must always hit the network. */",
124
+ "function isDynamic(url) {",
125
+ " return (",
126
+ " url.pathname.startsWith('/api/') ||",
127
+ " url.pathname.startsWith('/login') ||",
128
+ " url.pathname.startsWith('/admin') ||",
129
+ " // Louise's edit mode. `has()` rather than a value check: `?louise` with",
130
+ " // no value is the usual form.",
131
+ " url.searchParams.has('louise')",
132
+ " );",
133
+ "}",
134
+ "",
135
+ "/** Hashed build output — the filename changes when the content does, so it",
136
+ " * can be cached forever without a staleness risk. */",
137
+ "function isImmutable(url) {",
138
+ " return url.pathname.startsWith('/_astro/') || url.pathname.startsWith('/icons/');",
139
+ "}",
140
+ "",
141
+ "self.addEventListener('fetch', (event) => {",
142
+ " const req = event.request;",
143
+ " // Only GET: a cached POST response would be a correctness bug, not a",
144
+ " // speedup.",
145
+ " if (req.method !== 'GET') return;",
146
+ "",
147
+ " const url = new URL(req.url);",
148
+ " // Cross-origin requests belong to whoever serves them.",
149
+ " if (url.origin !== self.location.origin) return;",
150
+ " if (isDynamic(url)) return;",
151
+ " // Outside the scope this worker has no business intercepting — the rest of",
152
+ " // the site is CMS-edited and must stay live.",
153
+ " if (SCOPE !== '/' && !url.pathname.startsWith(SCOPE)) return;",
154
+ "",
155
+ " if (req.mode === 'navigate') {",
156
+ " // Network-first: a page is only worth serving from cache when the network",
157
+ " // failed, since the content behind it changes.",
158
+ " event.respondWith(",
159
+ " fetch(req)",
160
+ " .then((res) => {",
161
+ " const copy = res.clone();",
162
+ " caches",
163
+ " .open(CACHE)",
164
+ " .then((c) => c.put(req, copy))",
165
+ " .catch(() => {});",
166
+ " return res;",
167
+ " })",
168
+ " .catch(() => caches.match(req).then((r) => r || caches.match(SCOPE))),",
169
+ " );",
170
+ " return;",
171
+ " }",
172
+ "",
173
+ " if (isImmutable(url)) {",
174
+ " event.respondWith(",
175
+ " caches.match(req).then(",
176
+ " (cached) =>",
177
+ " cached ||",
178
+ " fetch(req).then((res) => {",
179
+ " const copy = res.clone();",
180
+ " caches",
181
+ " .open(CACHE)",
182
+ " .then((c) => c.put(req, copy))",
183
+ " .catch(() => {});",
184
+ " return res;",
185
+ " }),",
186
+ " ),",
187
+ " );",
188
+ " }",
189
+ " // Everything else falls through to the browser's own handling.",
190
+ "});",
191
+ "",
192
+ ].join("\n");
193
+ }
194
+ /**
195
+ * The `public/_headers` block the PWA needs.
196
+ *
197
+ * `Service-Worker-Allowed` is emitted ONLY when the scope is broader than the
198
+ * script's own location — which, with `sw.js` at the root, never is. Emitting it
199
+ * unconditionally (as the reference does) is harmless but misleading: it implies
200
+ * a requirement that isn't there, and someone later moving the script will trust
201
+ * a header that no longer says what they need.
202
+ */
203
+ export function generatePwaHeaders(config) {
204
+ if (!usesPwa(config))
205
+ return null;
206
+ return [
207
+ "",
208
+ "# The service worker must revalidate on every load, or a bad worker sticks",
209
+ "# around until its cache entry expires — and it controls every page in scope.",
210
+ "/sw.js",
211
+ " Cache-Control: no-cache",
212
+ "",
213
+ "/manifest.webmanifest",
214
+ " Content-Type: application/manifest+json",
215
+ " Cache-Control: public, max-age=3600",
216
+ "",
217
+ ].join("\n");
218
+ }
@@ -0,0 +1 @@
1
+ export { generatePwaHeaders, generateServiceWorker, generateWebManifest, type PwaConfig, resolvePwa, usesPwa, } from "./generate.js";
@@ -0,0 +1,2 @@
1
+ // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
+ export { generatePwaHeaders, generateServiceWorker, generateWebManifest, resolvePwa, usesPwa, } from "./generate.js";
@@ -0,0 +1,29 @@
1
+ import { type AstroidQueueMessage } from "./messages.js";
2
+ export interface QueueHandlerOptions {
3
+ /**
4
+ * Re-sync whatever the provider owns — the catalog mirror, a cache. Called
5
+ * for a periodic refresh and for webhooks that touched the catalog.
6
+ *
7
+ * Throwing marks the message for retry, which is usually right: a failed
8
+ * refresh means the site is serving stale data.
9
+ */
10
+ refreshCatalog?: () => void | Promise<void>;
11
+ /**
12
+ * Anything else this project queues. Runs for every message, after the
13
+ * catalog dispatch above, so a project can add its own kinds without
14
+ * reimplementing the refresh logic.
15
+ */
16
+ onMessage?: (message: AstroidQueueMessage) => void | Promise<void>;
17
+ }
18
+ /**
19
+ * Build the per-message handler to hand to `processBatch`.
20
+ *
21
+ * ```ts
22
+ * async queue(batch, env) {
23
+ * await processBatch(batch, astroidQueueHandler({
24
+ * refreshCatalog: () => refreshCatalog(env),
25
+ * }));
26
+ * }
27
+ * ```
28
+ */
29
+ export declare function astroidQueueHandler(options?: QueueHandlerOptions): (message: AstroidQueueMessage) => Promise<void>;
@@ -0,0 +1,37 @@
1
+ // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
+ //
3
+ // The consumer side.
4
+ //
5
+ // `processBatch` (louise-toolkit/queues) already owns per-message ack/retry, and
6
+ // Cloudflare Queues owns redelivery and DLQ routing. What every site then wrote
7
+ // on top was the same dispatch: a periodic refresh runs the re-sync, a webhook
8
+ // runs it only if the event actually touched the catalog, and everything else
9
+ // acks as a no-op.
10
+ //
11
+ // That last part matters more than it looks. Order, payment, and subscription
12
+ // events are read live from the provider, so there is nothing local to update —
13
+ // but they still arrive, in volume. A consumer that treats every event as
14
+ // actionable turns a busy sales day into a catalog-refresh storm.
15
+ import { affectsCatalog } from "./messages.js";
16
+ /**
17
+ * Build the per-message handler to hand to `processBatch`.
18
+ *
19
+ * ```ts
20
+ * async queue(batch, env) {
21
+ * await processBatch(batch, astroidQueueHandler({
22
+ * refreshCatalog: () => refreshCatalog(env),
23
+ * }));
24
+ * }
25
+ * ```
26
+ */
27
+ export function astroidQueueHandler(options = {}) {
28
+ return async (message) => {
29
+ if (message.kind === "catalog_refresh") {
30
+ await options.refreshCatalog?.();
31
+ }
32
+ else if (message.kind === "webhook" && affectsCatalog(message.provider, message.type)) {
33
+ await options.refreshCatalog?.();
34
+ }
35
+ await options.onMessage?.(message);
36
+ };
37
+ }
@@ -0,0 +1,4 @@
1
+ export { astroidQueueHandler, type QueueHandlerOptions } from "./consumer.js";
2
+ export { affectsCatalog, ASTROID_DEFAULT_CRON, ASTROID_HEALTH_CRON, ASTROID_QUEUE_BINDING, type AstroidQueueMessage, astroidCron, astroidCrons, astroidQueueNames, astroidUsesQueues, type CatalogRefreshMessage, type WebhookMessage, } from "./messages.js";
3
+ export { generateAstroidEnvBindings, generateAstroidQueueSeam, generateAstroidWebhookRoute, generateAstroidWebhookRoutes, } from "./scaffold.js";
4
+ export { handleWebhook, type QueueProducer, type WebhookRouteOptions, type WebhookVerifyInput, } from "./webhook.js";
@@ -0,0 +1,5 @@
1
+ // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
+ export { astroidQueueHandler } from "./consumer.js";
3
+ export { affectsCatalog, ASTROID_DEFAULT_CRON, ASTROID_HEALTH_CRON, ASTROID_QUEUE_BINDING, astroidCron, astroidCrons, astroidQueueNames, astroidUsesQueues, } from "./messages.js";
4
+ export { generateAstroidEnvBindings, generateAstroidQueueSeam, generateAstroidWebhookRoute, generateAstroidWebhookRoutes, } from "./scaffold.js";
5
+ export { handleWebhook, } from "./webhook.js";
@@ -0,0 +1,60 @@
1
+ import type { AstroidConfig } from "../config.js";
2
+ /**
3
+ * Whether this project runs a queue consumer + cron.
4
+ *
5
+ * On by default whenever commerce is configured: a commerce provider means
6
+ * webhooks, and a webhook processed inline is a webhook you drop the moment the
7
+ * provider's delivery timeout is shorter than your catalog sync.
8
+ */
9
+ export declare function astroidUsesQueues(config: AstroidConfig): boolean;
10
+ /** Hourly. Frequent enough that stale data has a bounded lifetime, rare enough
11
+ * to be free. */
12
+ export declare const ASTROID_DEFAULT_CRON = "0 * * * *";
13
+ /** The cron expression for the safety-net re-sync, or null when disabled. */
14
+ export declare function astroidCron(config: AstroidConfig): string | null;
15
+ /**
16
+ * Daily, at an off-peak-ish minute. The health scan crawls the site's own pages,
17
+ * so it is deliberately NOT on the hourly catalog cron — hourly would be a
18
+ * self-inflicted crawl 24× a day to recompute counts that move slowly.
19
+ */
20
+ export declare const ASTROID_HEALTH_CRON = "17 4 * * *";
21
+ /**
22
+ * Every cron expression the project needs, in the order they're declared in
23
+ * `wrangler.jsonc`.
24
+ *
25
+ * Cloudflare fires ONE `scheduled` handler for all of them and identifies which
26
+ * by `controller.cron`, so the generated handler dispatches on that string. That
27
+ * is why this list is derived in one place rather than assembled at each call
28
+ * site: the wrangler `triggers.crons` array and the handler's dispatch have to
29
+ * agree exactly, and a mismatch is a job that silently never runs.
30
+ */
31
+ export declare function astroidCrons(config: AstroidConfig): string[];
32
+ /** Binding name for the project's queue producer. */
33
+ export declare const ASTROID_QUEUE_BINDING = "COMMERCE_QUEUE";
34
+ /** Queue names derived from the project key — the main queue and its DLQ. */
35
+ export declare function astroidQueueNames(config: AstroidConfig): {
36
+ queue: string;
37
+ dlq: string;
38
+ };
39
+ /**
40
+ * A provider webhook, thinned to what a consumer needs. The raw `payload` is
41
+ * carried through deliberately: the consumer's needs change faster than the
42
+ * webhook contract, and a message that only kept the fields today's consumer
43
+ * reads can't be replayed from the DLQ once that changes.
44
+ */
45
+ export interface WebhookMessage {
46
+ kind: "webhook";
47
+ /** Which integration sent it — `"square"`, `"stripe"`, `"fourthwall"`. */
48
+ provider: string;
49
+ /** The provider's event type, e.g. `"catalog.version.updated"`. */
50
+ type: string;
51
+ payload: unknown;
52
+ }
53
+ /** The periodic (or manually triggered) full re-sync. */
54
+ export interface CatalogRefreshMessage {
55
+ kind: "catalog_refresh";
56
+ }
57
+ /** Everything the project's queue carries. Match on `kind`. */
58
+ export type AstroidQueueMessage = WebhookMessage | CatalogRefreshMessage;
59
+ /** Whether a provider event should trigger a catalog re-sync. */
60
+ export declare function affectsCatalog(provider: string, type: string): boolean;
@@ -0,0 +1,71 @@
1
+ // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
+ //
3
+ // What flows through the project's queue, and when it matters.
4
+ /**
5
+ * Whether this project runs a queue consumer + cron.
6
+ *
7
+ * On by default whenever commerce is configured: a commerce provider means
8
+ * webhooks, and a webhook processed inline is a webhook you drop the moment the
9
+ * provider's delivery timeout is shorter than your catalog sync.
10
+ */
11
+ export function astroidUsesQueues(config) {
12
+ return config.queues?.enabled ?? Boolean(config.commerce);
13
+ }
14
+ /** Hourly. Frequent enough that stale data has a bounded lifetime, rare enough
15
+ * to be free. */
16
+ export const ASTROID_DEFAULT_CRON = "0 * * * *";
17
+ /** The cron expression for the safety-net re-sync, or null when disabled. */
18
+ export function astroidCron(config) {
19
+ if (!astroidUsesQueues(config))
20
+ return null;
21
+ const cron = config.queues?.cron;
22
+ if (cron === false)
23
+ return null;
24
+ return cron ?? ASTROID_DEFAULT_CRON;
25
+ }
26
+ /**
27
+ * Daily, at an off-peak-ish minute. The health scan crawls the site's own pages,
28
+ * so it is deliberately NOT on the hourly catalog cron — hourly would be a
29
+ * self-inflicted crawl 24× a day to recompute counts that move slowly.
30
+ */
31
+ export const ASTROID_HEALTH_CRON = "17 4 * * *";
32
+ /**
33
+ * Every cron expression the project needs, in the order they're declared in
34
+ * `wrangler.jsonc`.
35
+ *
36
+ * Cloudflare fires ONE `scheduled` handler for all of them and identifies which
37
+ * by `controller.cron`, so the generated handler dispatches on that string. That
38
+ * is why this list is derived in one place rather than assembled at each call
39
+ * site: the wrangler `triggers.crons` array and the handler's dispatch have to
40
+ * agree exactly, and a mismatch is a job that silently never runs.
41
+ */
42
+ export function astroidCrons(config) {
43
+ const crons = [ASTROID_HEALTH_CRON];
44
+ const catalog = astroidCron(config);
45
+ if (catalog)
46
+ crons.push(catalog);
47
+ return crons;
48
+ }
49
+ /** Binding name for the project's queue producer. */
50
+ export const ASTROID_QUEUE_BINDING = "COMMERCE_QUEUE";
51
+ /** Queue names derived from the project key — the main queue and its DLQ. */
52
+ export function astroidQueueNames(config) {
53
+ return { queue: `${config.key}-commerce`, dlq: `${config.key}-commerce-dlq` };
54
+ }
55
+ /**
56
+ * Event-type prefixes that invalidate a cached catalog, per provider.
57
+ *
58
+ * Prefix matching rather than an exhaustive list on purpose: providers add event
59
+ * types, and the failure mode of matching one too many is a redundant refresh,
60
+ * while missing one is a storefront serving a price that no longer exists.
61
+ */
62
+ const CATALOG_EVENT_PREFIXES = {
63
+ square: ["catalog.", "inventory.", "item.", "item_variation."],
64
+ stripe: ["product.", "price.", "plan."],
65
+ fourthwall: ["product.", "variant.", "collection."],
66
+ };
67
+ /** Whether a provider event should trigger a catalog re-sync. */
68
+ export function affectsCatalog(provider, type) {
69
+ const prefixes = CATALOG_EVENT_PREFIXES[provider] ?? [];
70
+ return prefixes.some((prefix) => type.startsWith(prefix));
71
+ }
@@ -0,0 +1,44 @@
1
+ import type { AstroidConfig, CommerceProvider } from "../config.js";
2
+ /**
3
+ * The extra `CloudflareEnv` members the queue pipeline introduces, as a block
4
+ * `create-astroid` substitutes into the scaffolded `src/env.d.ts`.
5
+ *
6
+ * Substituted rather than always present because a declaration is a promise: a
7
+ * marketing site that types `COMMERCE_QUEUE` is claiming a binding its
8
+ * `wrangler.jsonc` never creates, and the first `env.COMMERCE_QUEUE.send()`
9
+ * someone writes against that type fails at runtime with the type system's
10
+ * blessing. Empty string when the project runs no consumer.
11
+ */
12
+ export declare function generateAstroidEnvBindings(config: AstroidConfig): string;
13
+ /**
14
+ * `src/queue.ts` — the consumer seam the generated worker imports.
15
+ *
16
+ * `astroidQueueHandler` already owns the dispatch every site wrote (periodic
17
+ * refresh, catalog-affecting webhook, no-op for everything else); what's left
18
+ * for the project is what "refresh" means, which is why this is a file and not
19
+ * a generated constant.
20
+ */
21
+ export declare function generateAstroidQueueSeam(config: AstroidConfig): string;
22
+ /**
23
+ * The provider webhook receiver — `src/pages/api/webhooks/<provider>.ts`.
24
+ *
25
+ * Thin on purpose: `handleWebhook` owns the ordering (verify the raw body before
26
+ * parsing) and the status-code contract (which codes ask the provider to retry
27
+ * and which tell it to stop). What's here is the provider's own header and
28
+ * verifier, plus the secret read.
29
+ *
30
+ * Returns null when the project has no commerce provider — nothing to receive.
31
+ */
32
+ export declare function generateAstroidWebhookRoute(config: AstroidConfig, forProvider?: CommerceProvider): string | null;
33
+ /**
34
+ * Every webhook receiver this project needs, as `{ path, contents }`.
35
+ *
36
+ * Plural because roles are: a site running Stripe for invoicing beside
37
+ * Fourthwall for the storefront receives from both, each with its own signing
38
+ * secret and header. One route per provider, not per role — a provider filling
39
+ * two roles still has one endpoint and one secret.
40
+ */
41
+ export declare function generateAstroidWebhookRoutes(config: AstroidConfig): {
42
+ path: string;
43
+ contents: string;
44
+ }[];