astroidjs 0.1.2 → 0.3.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 (157) 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/auth/index.d.ts +27 -0
  10. package/dist/auth/index.js +59 -0
  11. package/dist/commerce/adapters.d.ts +60 -0
  12. package/dist/commerce/adapters.js +90 -0
  13. package/dist/commerce/checkout-scaffold.d.ts +42 -0
  14. package/dist/commerce/checkout-scaffold.js +306 -0
  15. package/dist/commerce/checkout.d.ts +72 -0
  16. package/dist/commerce/checkout.js +124 -0
  17. package/dist/commerce/index.d.ts +8 -0
  18. package/dist/commerce/index.js +9 -0
  19. package/dist/commerce/loader.d.ts +71 -0
  20. package/dist/commerce/loader.js +90 -0
  21. package/dist/commerce/mirror.d.ts +69 -0
  22. package/dist/commerce/mirror.js +214 -0
  23. package/dist/commerce/roles.d.ts +38 -0
  24. package/dist/commerce/roles.js +93 -0
  25. package/dist/commerce/secrets.d.ts +74 -0
  26. package/dist/commerce/secrets.js +129 -0
  27. package/dist/commerce/sync.d.ts +86 -0
  28. package/dist/commerce/sync.js +154 -0
  29. package/dist/components/sections.d.ts +577 -0
  30. package/dist/components/sections.js +425 -0
  31. package/dist/config.d.ts +239 -12
  32. package/dist/config.js +49 -1
  33. package/dist/email/index.d.ts +4 -0
  34. package/dist/email/index.js +5 -0
  35. package/dist/email/inquiry.d.ts +33 -0
  36. package/dist/email/inquiry.js +63 -0
  37. package/dist/email/send.d.ts +120 -0
  38. package/dist/email/send.js +196 -0
  39. package/dist/email/templates.d.ts +24 -0
  40. package/dist/email/templates.js +184 -0
  41. package/dist/email/theme.d.ts +24 -0
  42. package/dist/email/theme.js +150 -0
  43. package/dist/errors.d.ts +14 -0
  44. package/dist/errors.js +17 -0
  45. package/dist/index.d.ts +15 -0
  46. package/dist/index.js +15 -0
  47. package/dist/map/index.d.ts +3 -0
  48. package/dist/map/index.js +4 -0
  49. package/dist/map/pmtiles.d.ts +92 -0
  50. package/dist/map/pmtiles.js +130 -0
  51. package/dist/map/scaffold.d.ts +29 -0
  52. package/dist/map/scaffold.js +212 -0
  53. package/dist/map/style.d.ts +58 -0
  54. package/dist/map/style.js +154 -0
  55. package/dist/portal/config.d.ts +26 -0
  56. package/dist/portal/config.js +56 -0
  57. package/dist/portal/guard.d.ts +54 -0
  58. package/dist/portal/guard.js +64 -0
  59. package/dist/portal/index.d.ts +5 -0
  60. package/dist/portal/index.js +6 -0
  61. package/dist/portal/nav.d.ts +26 -0
  62. package/dist/portal/nav.js +35 -0
  63. package/dist/portal/scaffold.d.ts +28 -0
  64. package/dist/portal/scaffold.js +140 -0
  65. package/dist/portal/session.d.ts +36 -0
  66. package/dist/portal/session.js +86 -0
  67. package/dist/portfolio/index.d.ts +1 -0
  68. package/dist/portfolio/index.js +4 -0
  69. package/dist/portfolio/scaffold.d.ts +9 -0
  70. package/dist/portfolio/scaffold.js +93 -0
  71. package/dist/project/actions.d.ts +3 -0
  72. package/dist/project/actions.js +121 -0
  73. package/dist/project/generate.d.ts +15 -0
  74. package/dist/project/generate.js +144 -2
  75. package/dist/project/index.d.ts +2 -0
  76. package/dist/project/index.js +2 -0
  77. package/dist/project/scaffold.d.ts +29 -0
  78. package/dist/project/scaffold.js +166 -0
  79. package/dist/pwa/generate.d.ts +49 -0
  80. package/dist/pwa/generate.js +218 -0
  81. package/dist/pwa/index.d.ts +1 -0
  82. package/dist/pwa/index.js +2 -0
  83. package/dist/queues/consumer.d.ts +29 -0
  84. package/dist/queues/consumer.js +37 -0
  85. package/dist/queues/index.d.ts +4 -0
  86. package/dist/queues/index.js +5 -0
  87. package/dist/queues/messages.d.ts +60 -0
  88. package/dist/queues/messages.js +71 -0
  89. package/dist/queues/scaffold.d.ts +44 -0
  90. package/dist/queues/scaffold.js +204 -0
  91. package/dist/queues/webhook.d.ts +60 -0
  92. package/dist/queues/webhook.js +81 -0
  93. package/dist/realtime/index.d.ts +1 -0
  94. package/dist/realtime/index.js +4 -0
  95. package/dist/realtime/scaffold.d.ts +30 -0
  96. package/dist/realtime/scaffold.js +159 -0
  97. package/dist/schema/collections.d.ts +41 -7
  98. package/dist/schema/collections.js +110 -12
  99. package/dist/schema/framework.js +5 -0
  100. package/dist/schema/generate.js +17 -1
  101. package/dist/secrets.d.ts +54 -0
  102. package/dist/secrets.js +80 -0
  103. package/dist/security/index.d.ts +1 -0
  104. package/dist/security/index.js +2 -0
  105. package/dist/security/rate-rules.d.ts +21 -0
  106. package/dist/security/rate-rules.js +110 -0
  107. package/dist/seo/index.d.ts +3 -0
  108. package/dist/seo/index.js +4 -0
  109. package/dist/seo/resolve.d.ts +68 -0
  110. package/dist/seo/resolve.js +73 -0
  111. package/dist/seo/routes.d.ts +44 -0
  112. package/dist/seo/routes.js +104 -0
  113. package/dist/seo/structured-data.d.ts +51 -0
  114. package/dist/seo/structured-data.js +105 -0
  115. package/dist/status.d.ts +51 -0
  116. package/dist/status.js +113 -0
  117. package/dist/worker/generate.d.ts +18 -10
  118. package/dist/worker/generate.js +353 -42
  119. package/dist/worker/routes.d.ts +1 -1
  120. package/dist/worker/routes.js +42 -0
  121. package/dist/workflow/advance.d.ts +102 -0
  122. package/dist/workflow/advance.js +145 -0
  123. package/dist/workflow/config.d.ts +60 -0
  124. package/dist/workflow/config.js +73 -0
  125. package/dist/workflow/generate.d.ts +22 -0
  126. package/dist/workflow/generate.js +138 -0
  127. package/dist/workflow/index.d.ts +3 -0
  128. package/dist/workflow/index.js +4 -0
  129. package/package.json +21 -4
  130. package/src/components/Editable.astro +33 -9
  131. package/src/components/JustifiedGallery.astro +254 -0
  132. package/src/components/MediaSlot.astro +178 -0
  133. package/src/components/PortalShell.astro +80 -0
  134. package/src/components/RegisterSW.astro +45 -0
  135. package/src/components/Section.astro +101 -35
  136. package/src/components/Sections.astro +64 -0
  137. package/src/components/Seo.astro +57 -0
  138. package/src/components/StageBar.astro +137 -0
  139. package/src/components/StructuredData.astro +33 -0
  140. package/src/components/justify.ts +170 -0
  141. package/src/components/media-meta.ts +174 -0
  142. package/src/components/sections/AboutIntro.astro +46 -0
  143. package/src/components/sections/Banner.astro +31 -0
  144. package/src/components/sections/Contact.astro +22 -9
  145. package/src/components/sections/Cta.astro +33 -10
  146. package/src/components/sections/Faq.astro +50 -0
  147. package/src/components/sections/FeatureGrid.astro +40 -11
  148. package/src/components/sections/Gallery.astro +46 -0
  149. package/src/components/sections/Hero.astro +40 -12
  150. package/src/components/sections/LocationHours.astro +59 -0
  151. package/src/components/sections/Media.astro +44 -0
  152. package/src/components/sections/PricingTiers.astro +79 -0
  153. package/src/components/sections/ProductGrid.astro +73 -0
  154. package/src/components/sections/SplitImage.astro +61 -0
  155. package/src/components/sections/Steps.astro +58 -0
  156. package/src/components/sections/Testimonial.astro +51 -0
  157. package/src/components/sections.ts +452 -67
package/dist/config.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ import type { SectionCatalog } from "louise-toolkit/content";
2
+ import type { RateRule } from "louise-toolkit/security";
3
+ import type { CatalogMirrorConfig } from "./commerce/mirror.js";
4
+ import type { astroidSectionCatalog } from "./components/sections.js";
5
+ import type { PortalRoute } from "./portal/guard.js";
6
+ import type { PwaConfig } from "./pwa/generate.js";
1
7
  /**
2
8
  * The starting shape the front-end takes. Not a fork — each archetype is a preset
3
9
  * of defaults (which sections/modules are on, nav shape) that the site then tunes.
@@ -7,17 +13,55 @@
7
13
  */
8
14
  export type Archetype = "marketing" | "storefront" | "wholesale" | "portfolio";
9
15
  /**
10
- * The section vocabulary — the editable home page is an ordered list of these, top
11
- * to bottom. Each maps to a themeable component in the Astroid section library.
12
- * Drawn from real usage across the target sites (annotated below).
16
+ * The section vocabulary — the editable home page is an ordered list of these,
17
+ * top to bottom.
18
+ *
19
+ * DERIVED from the section catalog, not hand-written (#277). It used to be its
20
+ * own union, and the two drifted in both directions: this named four kinds with
21
+ * no catalog entry and no component (`marquee`, `featured`, `story`, `visit`),
22
+ * while omitting eight that were real and renderable. A scaffold's config then
23
+ * listed sections that could never render, and nothing type-checked the gap.
24
+ *
25
+ * A type-only import, so the derivation adds no runtime dependency: `config.ts`
26
+ * is loaded by the `create-astroid` CLI, and this keeps its import graph
27
+ * exactly as it was.
13
28
  */
14
- export type SectionKind = "hero" | "marquee" | "featureGrid" | "featured" | "productGrid" | "gallery" | "story" | "visit" | "cta" | "testimonial" | "contact";
29
+ export type SectionKind = keyof typeof astroidSectionCatalog;
30
+ /**
31
+ * Each archetype's default home-page sections.
32
+ *
33
+ * Lives here, in TypeScript, rather than in `create-astroid`'s plain JS — the
34
+ * other half of #277. As a JS object literal it could name a section that
35
+ * didn't exist and nothing would say so; typed against {@link SectionKind}
36
+ * (itself derived from the catalog) a stale name is a compile error, and CI
37
+ * type-checks this package.
38
+ *
39
+ * The four kinds this used to name — `marquee`, `featured`, `story`, `visit` —
40
+ * had no catalog entry or component and could never render. Each is replaced by
41
+ * the real section that does its job: a marquee is a `banner`, curated picks
42
+ * are a `productGrid`, a brand-origin block is `aboutIntro`, and "visit" is
43
+ * exactly `locationHours`.
44
+ */
45
+ export declare const ASTROID_ARCHETYPE_SECTIONS: Record<Archetype, SectionKind[]>;
15
46
  /**
16
47
  * Optional capabilities the site switches on. Pluggable, not core — a portfolio
17
- * site runs none of the commerce ones. `orderTracking` is shared across both
18
- * coffee brands, so it's first-class but still opt-in.
48
+ * site runs none of the commerce ones.
49
+ *
50
+ * **Every value here is read by something.** The union used to also name
51
+ * `orderTracking`, `subscriptions`, `giftCards`, and `privateLabel`, none of
52
+ * which had a single consumer anywhere in the package: setting one type-checked,
53
+ * passed validation, and did nothing at all — no scaffold, no CSP origin, no
54
+ * rate rule, no table. A config surface that accepts a setting it ignores is
55
+ * worse than a smaller one, because the only way to discover the truth is to
56
+ * deploy and notice the absence.
57
+ *
58
+ * They are removed rather than left as TODOs. `orderTracking` in particular has
59
+ * a real implementation waiting — `src/workflow/` is the ghostfire order tracker,
60
+ * generalized — but it is reached through `defineWorkflow`, not this flag, and
61
+ * pretending otherwise is what made the flag misleading. Re-add each one in the
62
+ * change that wires it.
19
63
  */
20
- export type ModuleKind = "orderTracking" | "subscriptions" | "giftCards" | "wholesaleInquiry" | "privateLabel";
64
+ export type ModuleKind = "map" | "pwa" | "realtime" | "wholesaleInquiry";
21
65
  /** Commerce backend — mirrors Louise's provider set (louise-toolkit/commerce). */
22
66
  export type CommerceProvider = "stripe" | "square" | "fourthwall";
23
67
  export interface Theme {
@@ -40,14 +84,168 @@ export interface Theme {
40
84
  }
41
85
  export interface Portal {
42
86
  enabled: boolean;
43
- /** Require a session to view the whole site (Meg Bowen's gated preview), not
44
- * just the account area. Default `false`. */
87
+ /**
88
+ * @deprecated NOT IMPLEMENTED — `defineAstroid` throws if this is set.
89
+ *
90
+ * It was meant to require a session for the whole site (a pre-launch client
91
+ * gallery), not just the account area, but nothing ever read it: the guard
92
+ * table is built from {@link Portal.routes} and `portalGuard` allows any
93
+ * unmatched path. Until it's wired, gate the site by naming the prefixes in
94
+ * `routes` — that is the mechanism this would have been sugar for.
95
+ */
45
96
  gated?: boolean;
46
- /** Modules exposed inside the account area (e.g. `orderTracking`). */
97
+ /** Modules exposed inside the account area (e.g. `wholesaleInquiry`, which
98
+ * adds the inquiries table even on an archetype that wouldn't have one). */
47
99
  features?: ModuleKind[];
100
+ /**
101
+ * Roles a portal account can hold, first being the default for a new account.
102
+ * Default `["customer"]`. These are the portal's OWN roles — entirely separate
103
+ * from the editor's `admin`, because the two auth instances don't share a
104
+ * user table.
105
+ */
106
+ roles?: string[];
107
+ /**
108
+ * Route guard table: everything under `prefix` needs one of `roles`. Matched
109
+ * in order, first match wins. Defaults to `/portal` + `/api/portal` for any
110
+ * signed-in portal user.
111
+ */
112
+ routes?: PortalRoute[];
113
+ /**
114
+ * Where a portal user lands, per role — used to bounce someone who reached an
115
+ * area they don't belong in. Default `/portal` for everyone.
116
+ */
117
+ home?: Record<string, string>;
118
+ /**
119
+ * Allow public sign-up. Default `false`: both consuming sites provision portal
120
+ * accounts by hand, and a portal is usually for people you already know.
121
+ */
122
+ signUp?: boolean;
123
+ /**
124
+ * Where the portal's Better Auth instance mounts — its own handler, separate
125
+ * from the editor's `/api/auth`. Default `/api/portal-auth`. Override when a
126
+ * site already ships a second instance at a different path (e.g. a shop
127
+ * account at `/api/shop-auth`) whose live cookies must not change.
128
+ */
129
+ basePath?: string;
130
+ /**
131
+ * Cookie prefix for the portal instance — MUST differ from the editor's
132
+ * (Better Auth's default), or signing into one instance signs you out of the
133
+ * other. Default `"portal"`. `defineAstroid` rejects a colliding value.
134
+ */
135
+ cookiePrefix?: string;
136
+ /**
137
+ * Table-name prefix for the portal's Better Auth tables. Default `"portal_"`
138
+ * (`portal_user`, …); set `""` to take the unprefixed `user`/`session` tables
139
+ * (the editor owns `louise_*`, so they don't collide). MUST differ from the
140
+ * editor's `louise_` prefix.
141
+ */
142
+ tablePrefix?: string;
48
143
  }
49
144
  export interface CommerceConfig {
50
- provider: CommerceProvider;
145
+ /**
146
+ * Shorthand for a single-provider site. Assigns the provider to a role it can
147
+ * actually serve — `square`/`fourthwall` become the storefront, `stripe`
148
+ * becomes invoicing (its client has no catalog API).
149
+ */
150
+ provider?: CommerceProvider;
151
+ /** Catalog, cart, checkout. Needs a provider with a catalog API. */
152
+ storefront?: CommerceProvider;
153
+ /**
154
+ * Invoices for work that isn't a catalog item — commissions, originals.
155
+ * Independent of `storefront`: themidwestartist.com runs Stripe here and
156
+ * Fourthwall as the storefront, because neither can do the other's job.
157
+ */
158
+ invoicing?: CommerceProvider;
159
+ /** The catalog mirror's shape — its mode, table name, and owned columns. */
160
+ catalog?: CatalogMirrorConfig;
161
+ }
162
+ export interface QueuesConfig {
163
+ /**
164
+ * Force the queue consumer + cron on or off. Defaults to on whenever
165
+ * `commerce` is configured: a commerce provider means webhooks, and a webhook
166
+ * you process inline is a webhook you drop when the provider times out.
167
+ */
168
+ enabled?: boolean;
169
+ /**
170
+ * Cron for the safety-net re-sync, or `false` for none. Webhooks get missed —
171
+ * a provider outage, a deploy mid-delivery, a DLQ'd message — and without a
172
+ * periodic re-sync the site serves stale data until someone notices. Default
173
+ * hourly.
174
+ */
175
+ cron?: string | false;
176
+ /** Deliveries before Cloudflare routes a message to the DLQ. Default 5. */
177
+ maxRetries?: number;
178
+ /** Messages per consumer invocation. Default 10. */
179
+ maxBatchSize?: number;
180
+ /** Seconds the consumer waits to fill a batch. Default 30. */
181
+ maxBatchTimeout?: number;
182
+ }
183
+ export interface SeoConfig {
184
+ /**
185
+ * `<title>` template, `%s` standing in for the page title. Applied only when
186
+ * a page supplies its own title, so the home page reads "Acme Coffee" and not
187
+ * "Acme Coffee | Acme Coffee". Default `"%s | <site name>"`.
188
+ */
189
+ titleTemplate?: string;
190
+ /**
191
+ * schema.org `@type` for the business node in the JSON-LD graph. Defaults to
192
+ * the archetype's broad type (see `ARCHETYPE_BUSINESS_TYPE`); set a more
193
+ * specific subtype whenever you know one — `"CafeOrCoffeeShop"`,
194
+ * `"ArtGallery"`, `"HomeAndConstructionBusiness"` — since a narrower type is
195
+ * strictly better for rich results.
196
+ */
197
+ businessType?: string;
198
+ /** `@handle` for Twitter/X card attribution. */
199
+ twitterHandle?: string;
200
+ /** Open Graph locale, e.g. `"en_US"`. */
201
+ locale?: string;
202
+ }
203
+ export interface SecurityConfig {
204
+ /**
205
+ * Extra rate-limit rules for surfaces Astroid doesn't know about, and the seam
206
+ * for overriding a default budget. These are matched BEFORE the derived
207
+ * defaults (first match wins), so declaring a rule for a path Astroid already
208
+ * covers replaces that one rule rather than the whole set.
209
+ */
210
+ rateRules?: RateRule[];
211
+ /**
212
+ * Extra origins to allow in the generated Content-Security-Policy, merged with
213
+ * the ones Astroid derives from the enabled modules. Add a host here when you
214
+ * pull in a third party Astroid can't see (a chat widget, a video embed).
215
+ */
216
+ cspOrigins?: CspOrigins;
217
+ }
218
+ /** Per-directive origin lists contributed to the CSP. */
219
+ export interface CspOrigins {
220
+ script?: string[];
221
+ frame?: string[];
222
+ connect?: string[];
223
+ font?: string[];
224
+ img?: string[];
225
+ worker?: string[];
226
+ }
227
+ export interface SettingsConfig {
228
+ /**
229
+ * Override the editable base `site_settings` columns. Defaults to Astroid's
230
+ * standard set (`ASTROID_SETTINGS_COLUMNS`). A **custom-heavy** site whose
231
+ * settings shape doesn't align with the base column names keeps everything in
232
+ * `custom` by passing `[]` — otherwise a key that happens to match a base
233
+ * column name (e.g. `contactEmail`) would route to that column instead of
234
+ * `custom`, where the site's render reads it.
235
+ */
236
+ columns?: string[];
237
+ /**
238
+ * Site-specific settings keys stored in the `site_settings.custom` JSON column,
239
+ * on top of (or, with `columns: []`, instead of) Astroid's base columns. The
240
+ * generated `settingsRoute` + Action accept these; the Settings panel writes
241
+ * them through the `settingsExtension` groups a site supplies to
242
+ * `mountSettings`. A site with a rich settings shape (coracle's footer columns,
243
+ * hours table, ui strings, shop/order config) lists their top-level keys here.
244
+ */
245
+ customKeys?: string[];
246
+ /** Extra media-library image keys beyond the base logo/favicon/OG defaults —
247
+ * settings values validated as media-library URLs on write. */
248
+ imageKeys?: string[];
51
249
  }
52
250
  export interface DeployConfig {
53
251
  platform: "cloudflare";
@@ -69,12 +267,41 @@ export interface AstroidConfig {
69
267
  theme: Theme;
70
268
  /** The editable home page, top to bottom. Omit to take the archetype default. */
71
269
  sections?: SectionKind[];
270
+ /**
271
+ * A site-provided section catalog that REPLACES the built-in one for
272
+ * SERVER-side validation + sanitization of `pages.sections` (the generated
273
+ * pages route + versions route). A site with bespoke section designs — its own
274
+ * `.astro` components and field defs (coracle's 13 sections) — registers them
275
+ * here so writes to its custom `_type`s validate instead of 422-ing against the
276
+ * built-in vocabulary. The on-canvas editor already uses the site's catalog
277
+ * (its `mountSections` call passes it); this closes the server half so both
278
+ * write paths agree. Omit to use Astroid's built-in catalog.
279
+ */
280
+ sectionCatalog?: SectionCatalog;
72
281
  /** Optional capabilities switched on for this site. */
73
282
  modules?: ModuleKind[];
74
283
  /** Gated account/portal area (order tracking, client galleries). */
75
284
  portal?: Portal;
76
285
  /** Commerce backend. */
77
286
  commerce?: CommerceConfig;
287
+ /** Queue consumer + cron safety net. Defaults on when `commerce` is set. */
288
+ queues?: QueuesConfig;
289
+ /** Title template, structured-data type, and social-card attribution. */
290
+ seo?: SeoConfig;
291
+ /** Additions to the rate-limit rules + CSP origins Astroid derives. */
292
+ security?: SecurityConfig;
293
+ /** Site-specific editable settings — extra `custom` keys + image keys on top
294
+ * of Astroid's base `site_settings` columns. */
295
+ settings?: SettingsConfig;
296
+ /**
297
+ * Force the contact form + `inquiries` table on or off. Omit to detect from
298
+ * the config (a `contact` section, or a wholesale-inquiry module). Set `true`
299
+ * when a bespoke section captures inquiries under a name Astroid can't see
300
+ * (coracle's custom `contactForm`); set `false` to suppress it entirely.
301
+ */
302
+ inquiries?: boolean;
303
+ /** Installable-app settings. Only read when `modules` includes `"pwa"`. */
304
+ pwa?: PwaConfig;
78
305
  deploy?: DeployConfig;
79
306
  }
80
307
  /**
@@ -89,7 +316,7 @@ export interface AstroidConfig {
89
316
  * key: "coracle",
90
317
  * archetype: "storefront",
91
318
  * theme: { name: "Coracle Coffee", colors: { brand: "#1f6f78" } },
92
- * sections: ["hero", "marquee", "featured", "productGrid", "visit"],
319
+ * sections: ["hero", "banner", "productGrid", "locationHours", "contact"],
93
320
  * commerce: { provider: "square" },
94
321
  * deploy: { platform: "cloudflare" },
95
322
  * });
package/dist/config.js CHANGED
@@ -19,7 +19,30 @@
19
19
  // `ModuleKind` are extracted from the real sites Astroid targets — a storefront
20
20
  // (coracle), a wholesale front (ghostfire), an artist portfolio (megbowen), and a
21
21
  // plain marketing baseline (louise-web).
22
+ import { assertAuthIsolation } from "./auth/index.js";
23
+ import { assertCommerceRoles } from "./commerce/roles.js";
22
24
  import { AstroidConfigError } from "./errors.js";
25
+ /**
26
+ * Each archetype's default home-page sections.
27
+ *
28
+ * Lives here, in TypeScript, rather than in `create-astroid`'s plain JS — the
29
+ * other half of #277. As a JS object literal it could name a section that
30
+ * didn't exist and nothing would say so; typed against {@link SectionKind}
31
+ * (itself derived from the catalog) a stale name is a compile error, and CI
32
+ * type-checks this package.
33
+ *
34
+ * The four kinds this used to name — `marquee`, `featured`, `story`, `visit` —
35
+ * had no catalog entry or component and could never render. Each is replaced by
36
+ * the real section that does its job: a marquee is a `banner`, curated picks
37
+ * are a `productGrid`, a brand-origin block is `aboutIntro`, and "visit" is
38
+ * exactly `locationHours`.
39
+ */
40
+ export const ASTROID_ARCHETYPE_SECTIONS = {
41
+ marketing: ["hero", "featureGrid", "cta", "contact"],
42
+ storefront: ["hero", "banner", "productGrid", "locationHours", "contact"],
43
+ wholesale: ["hero", "featureGrid", "aboutIntro", "contact"],
44
+ portfolio: ["hero", "gallery", "aboutIntro", "contact"],
45
+ };
23
46
  /**
24
47
  * Define an Astroid project. An identity function in the shape of Astro's
25
48
  * `defineConfig`: it returns the config verbatim with full type-checking +
@@ -32,7 +55,7 @@ import { AstroidConfigError } from "./errors.js";
32
55
  * key: "coracle",
33
56
  * archetype: "storefront",
34
57
  * theme: { name: "Coracle Coffee", colors: { brand: "#1f6f78" } },
35
- * sections: ["hero", "marquee", "featured", "productGrid", "visit"],
58
+ * sections: ["hero", "banner", "productGrid", "locationHours", "contact"],
36
59
  * commerce: { provider: "square" },
37
60
  * deploy: { platform: "cloudflare" },
38
61
  * });
@@ -48,5 +71,30 @@ export function defineAstroid(config) {
48
71
  if (!config.theme.colors || !config.theme.colors.brand) {
49
72
  throw new AstroidConfigError("Astroid config requires `theme.colors.brand` (the primary brand color)");
50
73
  }
74
+ // A provider assigned to a role its client can't serve (invoicing over
75
+ // Fourthwall, a storefront over Stripe) fails here rather than at runtime on
76
+ // the first invoice, as a missing function.
77
+ assertCommerceRoles(config.commerce);
78
+ // A portal is a SECOND Better Auth instance beside the editor's. Reject any
79
+ // isolation that would collide with the editor on the same origin (a shared
80
+ // cookie prefix silently cross-signs-out; a shared table prefix merges the two
81
+ // user tables) — the intermittent-prod failure the fixed defaults prevent.
82
+ assertAuthIsolation(config);
83
+ // `portal.gated` is declared and resolved but read by NOTHING — the guard
84
+ // table is built from `portal.routes` alone, and `portalGuard` allows any
85
+ // unmatched path. So a site that set it believed the whole site sat behind a
86
+ // login (a pre-launch client gallery) while every page outside /portal was
87
+ // public, and it type-checked.
88
+ //
89
+ // Refusing the flag is the only safe state until it's implemented. A security
90
+ // control that silently does nothing is strictly worse than one that isn't
91
+ // offered: the first gives false confidence, the second sends you looking for
92
+ // an answer. Fail loudly, at config load, naming the workaround.
93
+ if (config.portal?.gated) {
94
+ throw new AstroidConfigError("`portal.gated` is not implemented — it is accepted but wires no guard, so the site " +
95
+ "would be fully public while appearing gated. Remove it, and gate the whole site by " +
96
+ "listing the prefixes you mean in `portal.routes` (e.g. `[{ prefix: \"/\" }]` with your " +
97
+ "login and auth paths ahead of it).");
98
+ }
51
99
  return config;
52
100
  }
@@ -0,0 +1,4 @@
1
+ export { astroidMailTheme, type MailThemeOverrides } from "./theme.js";
2
+ export { type AstroidMailEnv, sendInquiryMail } from "./inquiry.js";
3
+ export { createMailer, type DeliveryResult, EMAIL_SECRET_NAMES, type EmailSender, type MailerEnv, type MailerOptions, type MailerStatus, type OutgoingMail, resolveMailer, resolveMailerStatus, sendTransactional, } from "./send.js";
4
+ export { type InquiryDetails, inquiryConfirmationEmail, inquiryNotificationEmail, magicLinkEmail, type MailContent, type MailTheme, passwordResetEmail, } from "./templates.js";
@@ -0,0 +1,5 @@
1
+ // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
+ export { astroidMailTheme } from "./theme.js";
3
+ export { sendInquiryMail } from "./inquiry.js";
4
+ export { createMailer, EMAIL_SECRET_NAMES, resolveMailer, resolveMailerStatus, sendTransactional, } from "./send.js";
5
+ export { inquiryConfirmationEmail, inquiryNotificationEmail, magicLinkEmail, passwordResetEmail, } from "./templates.js";
@@ -0,0 +1,33 @@
1
+ import type { AstroidConfig } from "../config.js";
2
+ import type { SecretSource } from "../secrets.js";
3
+ import type { EmailSender } from "./send.js";
4
+ import { type DeliveryResult } from "./send.js";
5
+ import { type MailThemeOverrides } from "./theme.js";
6
+ /** The bindings the inquiry hook reads. All optional — an unprovisioned mail
7
+ * setup logs instead of sending, per the dormant-until-provisioned convention. */
8
+ export interface AstroidMailEnv {
9
+ /** Cloudflare Email Sending binding. */
10
+ EMAIL?: EmailSender;
11
+ /**
12
+ * Envelope sender; its domain must be onboarded for Email Sending. A
13
+ * `SecretSource` rather than a plain string so a Secrets Store binding works
14
+ * here too — and so the placeholder sentinel reads as unconfigured.
15
+ */
16
+ MAIL_FROM?: SecretSource;
17
+ /** Where owner notifications go. Also the first editor's address. */
18
+ OWNER_EMAIL?: string;
19
+ }
20
+ /**
21
+ * Send the notify + confirm pair for one contact-form submission.
22
+ *
23
+ * Wire it into the generated worker's form route:
24
+ *
25
+ * ```ts
26
+ * formRoute({ form: contactForm, onSubmit: (values, env) => sendInquiryMail(config, env, values) })
27
+ * ```
28
+ *
29
+ * Each half is skipped when its recipient is unknown rather than failing the
30
+ * batch: no `OWNER_EMAIL` means no notification to send, and a submission whose
31
+ * email didn't validate still deserves to reach the owner.
32
+ */
33
+ export declare function sendInquiryMail(config: AstroidConfig, env: AstroidMailEnv, values: Record<string, unknown>, overrides?: MailThemeOverrides): Promise<DeliveryResult[]>;
@@ -0,0 +1,63 @@
1
+ // Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
2
+ //
3
+ // The inquiry pair, wired to the contact form.
4
+ //
5
+ // `formRoute` fires `onSubmit` AFTER the row is inserted and off the response
6
+ // path (`waitUntil`), which is exactly the store-and-forward shape this wants:
7
+ // the submission is already durable, so mail is a notification of something that
8
+ // already happened and can fail without the visitor ever knowing.
9
+ //
10
+ // Two messages, not one — every site converged on the pair. The owner needs the
11
+ // message; the visitor needs to know it arrived, because a contact form with no
12
+ // acknowledgement is indistinguishable from one that's broken.
13
+ import { resolveMailer, sendTransactional } from "./send.js";
14
+ import { inquiryConfirmationEmail, inquiryNotificationEmail } from "./templates.js";
15
+ import { astroidMailTheme } from "./theme.js";
16
+ /** Trimmed string, or undefined for anything else. */
17
+ const str = (v) => {
18
+ const s = typeof v === "string" ? v.trim() : "";
19
+ return s || undefined;
20
+ };
21
+ /**
22
+ * Send the notify + confirm pair for one contact-form submission.
23
+ *
24
+ * Wire it into the generated worker's form route:
25
+ *
26
+ * ```ts
27
+ * formRoute({ form: contactForm, onSubmit: (values, env) => sendInquiryMail(config, env, values) })
28
+ * ```
29
+ *
30
+ * Each half is skipped when its recipient is unknown rather than failing the
31
+ * batch: no `OWNER_EMAIL` means no notification to send, and a submission whose
32
+ * email didn't validate still deserves to reach the owner.
33
+ */
34
+ export async function sendInquiryMail(config, env, values, overrides) {
35
+ const theme = astroidMailTheme(config, overrides);
36
+ const details = {
37
+ name: [str(values.firstName), str(values.lastName)].filter(Boolean).join(" "),
38
+ email: str(values.email) ?? "",
39
+ regarding: str(values.regarding),
40
+ message: str(values.message) ?? "",
41
+ };
42
+ const owner = str(env.OWNER_EMAIL);
43
+ const mails = [
44
+ ...(owner
45
+ ? [
46
+ {
47
+ to: owner,
48
+ content: inquiryNotificationEmail(theme, details),
49
+ // So the owner can answer by hitting reply, rather than copying an
50
+ // address out of the body.
51
+ ...(details.email ? { replyTo: details.email } : {}),
52
+ },
53
+ ]
54
+ : []),
55
+ ...(details.email
56
+ ? [{ to: details.email, content: inquiryConfirmationEmail(theme, details) }]
57
+ : []),
58
+ ];
59
+ // One resolver decides dormancy for the whole module: no binding, no sender
60
+ // address, or a sender still holding the placeholder sentinel all come back
61
+ // as `logOnly`, so nothing hands the Email API a dummy envelope.
62
+ return sendTransactional(await resolveMailer(env), mails);
63
+ }
@@ -0,0 +1,120 @@
1
+ import type { EmailSender, MailContent } from "louise-toolkit/email";
2
+ import { type ModuleSecrets, type SecretSource } from "../secrets.js";
3
+ export type { EmailSender };
4
+ /**
5
+ * The secrets the mailer needs. Just the envelope sender: the EMAIL binding is a
6
+ * binding, not a secret, so it's gated by presence rather than by value.
7
+ *
8
+ * Named here so `astroid doctor` and the scaffold's `.dev.vars` seed read the
9
+ * same list the runtime gate does.
10
+ */
11
+ export declare const EMAIL_SECRET_NAMES: readonly ["MAIL_FROM"];
12
+ /**
13
+ * The mailer's resolved gate. Deliberately NOT `ModuleSecrets<"MAIL_FROM">`:
14
+ * `missing` here can name `EMAIL`, which is a binding rather than a secret, so
15
+ * the key type is widened to plain strings.
16
+ */
17
+ export interface MailerStatus {
18
+ /** True only when a binding AND a real sender address are both present. */
19
+ configured: boolean;
20
+ values: ModuleSecrets<"MAIL_FROM">["values"];
21
+ /** What's unprovisioned — secret names and/or `"EMAIL"`. */
22
+ missing: string[];
23
+ /** Whether an Email Sending binding is present at all. */
24
+ hasBinding: boolean;
25
+ }
26
+ /** The env members the mailer reads. Structural, so a project's env fits. */
27
+ export interface MailerEnv {
28
+ EMAIL?: EmailSender | null;
29
+ MAIL_FROM?: SecretSource;
30
+ }
31
+ /**
32
+ * Resolve the email module's dormancy.
33
+ *
34
+ * Both halves are required, and for the same reason: a binding with no sender
35
+ * address can't build an envelope, and a sender address with no binding has
36
+ * nothing to send through. Either one missing means log-and-continue, which
37
+ * under `wrangler dev` (no EMAIL binding at all) is the normal case — and the
38
+ * reason the magic-link flow is still workable locally.
39
+ */
40
+ export declare function resolveMailerStatus(env: MailerEnv): Promise<MailerStatus>;
41
+ /**
42
+ * Build `MailerOptions` from an env, with dormancy already decided.
43
+ *
44
+ * The point of routing through {@link resolveMailerStatus} rather than checking
45
+ * `!env.EMAIL` inline is that the placeholder sentinel counts: a scaffold seeds
46
+ * `MAIL_FROM=DUMMY_REPLACE_ME`, and handing that to the Email API as an
47
+ * envelope sender is exactly the "called upstream with a dummy credential"
48
+ * failure the convention exists to prevent.
49
+ */
50
+ export declare function resolveMailer(env: MailerEnv, overrides?: Partial<Omit<MailerOptions, "binding" | "from">>): Promise<MailerOptions & {
51
+ status: MailerStatus;
52
+ }>;
53
+ /** One message queued for delivery. */
54
+ export interface OutgoingMail {
55
+ to: string;
56
+ content: MailContent;
57
+ /** Reply-To — for an inquiry notification, the visitor's own address, so the
58
+ * owner can just hit reply. */
59
+ replyTo?: string;
60
+ }
61
+ /** What happened to one message. Never an exception. */
62
+ export interface DeliveryResult {
63
+ to: string;
64
+ subject: string;
65
+ delivered: boolean;
66
+ messageId?: string;
67
+ /** Why it wasn't delivered — `"not-configured"`, `"log-only"`, or the error. */
68
+ reason?: string;
69
+ }
70
+ export interface MailerOptions {
71
+ /**
72
+ * The Cloudflare Email Sending binding. Absent or null → the mailer is
73
+ * dormant: messages are logged, and every result comes back
74
+ * `delivered: false, reason: "not-configured"`.
75
+ */
76
+ binding?: EmailSender | null;
77
+ /** Envelope sender. Its domain must be onboarded for Email Sending. */
78
+ from: string | {
79
+ email: string;
80
+ name?: string;
81
+ };
82
+ /** Log instead of sending even when a binding exists (a dry run). */
83
+ logOnly?: boolean;
84
+ /** Sink for the dev log. Defaults to `console.info`; pass one in a test. */
85
+ log?: (message: string) => void;
86
+ /**
87
+ * Print the message BODY when a send is skipped.
88
+ *
89
+ * The body carries single-use sign-in and password-reset links, so it is only
90
+ * printed where we can tell we're in development. Set this explicitly when the
91
+ * detection can't (a local `wrangler dev` against a real account, a test). Do
92
+ * not set it on a deployed Worker: the log is `wrangler tail` and Logpush.
93
+ */
94
+ devLog?: boolean;
95
+ }
96
+ /**
97
+ * Send a batch of transactional messages, best effort.
98
+ *
99
+ * Delivery runs concurrently and independently: an inquiry sends a notification
100
+ * to the owner and a confirmation to the visitor, and the owner's copy must
101
+ * still arrive when the visitor typo'd their address. Callers get a result per
102
+ * message and decide whether to care.
103
+ *
104
+ * ```ts
105
+ * const results = await sendTransactional(
106
+ * { binding: env.EMAIL, from: env.MAIL_FROM },
107
+ * [
108
+ * { to: owner, content: inquiryNotificationEmail(theme, i), replyTo: i.email },
109
+ * { to: i.email, content: inquiryConfirmationEmail(theme, i) },
110
+ * ],
111
+ * );
112
+ * ```
113
+ */
114
+ export declare function sendTransactional(options: MailerOptions, mails: OutgoingMail[]): Promise<DeliveryResult[]>;
115
+ /**
116
+ * Bind a mailer's options once so call sites read as `mailer([...])`. Useful
117
+ * where the binding and sender are resolved per request but the sends are
118
+ * scattered (an inquiry hook, an auth callback).
119
+ */
120
+ export declare function createMailer(options: MailerOptions): (mails: OutgoingMail[]) => Promise<DeliveryResult[]>;