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.
- package/README.md +240 -5
- package/bin/astroid.mjs +185 -9
- package/dist/analytics/index.d.ts +37 -0
- package/dist/analytics/index.js +108 -0
- package/dist/astro/csp.d.ts +64 -0
- package/dist/astro/csp.js +173 -0
- package/dist/astro/index.d.ts +1 -0
- package/dist/astro/index.js +7 -0
- package/dist/auth/index.d.ts +27 -0
- package/dist/auth/index.js +59 -0
- package/dist/commerce/adapters.d.ts +60 -0
- package/dist/commerce/adapters.js +90 -0
- package/dist/commerce/checkout-scaffold.d.ts +42 -0
- package/dist/commerce/checkout-scaffold.js +306 -0
- package/dist/commerce/checkout.d.ts +72 -0
- package/dist/commerce/checkout.js +124 -0
- package/dist/commerce/index.d.ts +8 -0
- package/dist/commerce/index.js +9 -0
- package/dist/commerce/loader.d.ts +71 -0
- package/dist/commerce/loader.js +90 -0
- package/dist/commerce/mirror.d.ts +69 -0
- package/dist/commerce/mirror.js +214 -0
- package/dist/commerce/roles.d.ts +38 -0
- package/dist/commerce/roles.js +93 -0
- package/dist/commerce/secrets.d.ts +74 -0
- package/dist/commerce/secrets.js +129 -0
- package/dist/commerce/sync.d.ts +86 -0
- package/dist/commerce/sync.js +154 -0
- package/dist/components/sections.d.ts +577 -0
- package/dist/components/sections.js +425 -0
- package/dist/config.d.ts +239 -12
- package/dist/config.js +49 -1
- package/dist/email/index.d.ts +4 -0
- package/dist/email/index.js +5 -0
- package/dist/email/inquiry.d.ts +33 -0
- package/dist/email/inquiry.js +63 -0
- package/dist/email/send.d.ts +120 -0
- package/dist/email/send.js +196 -0
- package/dist/email/templates.d.ts +24 -0
- package/dist/email/templates.js +184 -0
- package/dist/email/theme.d.ts +24 -0
- package/dist/email/theme.js +150 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/map/index.d.ts +3 -0
- package/dist/map/index.js +4 -0
- package/dist/map/pmtiles.d.ts +92 -0
- package/dist/map/pmtiles.js +130 -0
- package/dist/map/scaffold.d.ts +29 -0
- package/dist/map/scaffold.js +212 -0
- package/dist/map/style.d.ts +58 -0
- package/dist/map/style.js +154 -0
- package/dist/portal/config.d.ts +26 -0
- package/dist/portal/config.js +56 -0
- package/dist/portal/guard.d.ts +54 -0
- package/dist/portal/guard.js +64 -0
- package/dist/portal/index.d.ts +5 -0
- package/dist/portal/index.js +6 -0
- package/dist/portal/nav.d.ts +26 -0
- package/dist/portal/nav.js +35 -0
- package/dist/portal/scaffold.d.ts +28 -0
- package/dist/portal/scaffold.js +140 -0
- package/dist/portal/session.d.ts +36 -0
- package/dist/portal/session.js +86 -0
- package/dist/portfolio/index.d.ts +1 -0
- package/dist/portfolio/index.js +4 -0
- package/dist/portfolio/scaffold.d.ts +9 -0
- package/dist/portfolio/scaffold.js +93 -0
- package/dist/project/actions.d.ts +3 -0
- package/dist/project/actions.js +121 -0
- package/dist/project/generate.d.ts +15 -0
- package/dist/project/generate.js +144 -2
- package/dist/project/index.d.ts +2 -0
- package/dist/project/index.js +2 -0
- package/dist/project/scaffold.d.ts +29 -0
- package/dist/project/scaffold.js +166 -0
- package/dist/pwa/generate.d.ts +49 -0
- package/dist/pwa/generate.js +218 -0
- package/dist/pwa/index.d.ts +1 -0
- package/dist/pwa/index.js +2 -0
- package/dist/queues/consumer.d.ts +29 -0
- package/dist/queues/consumer.js +37 -0
- package/dist/queues/index.d.ts +4 -0
- package/dist/queues/index.js +5 -0
- package/dist/queues/messages.d.ts +60 -0
- package/dist/queues/messages.js +71 -0
- package/dist/queues/scaffold.d.ts +44 -0
- package/dist/queues/scaffold.js +204 -0
- package/dist/queues/webhook.d.ts +60 -0
- package/dist/queues/webhook.js +81 -0
- package/dist/realtime/index.d.ts +1 -0
- package/dist/realtime/index.js +4 -0
- package/dist/realtime/scaffold.d.ts +30 -0
- package/dist/realtime/scaffold.js +159 -0
- package/dist/schema/collections.d.ts +41 -7
- package/dist/schema/collections.js +110 -12
- package/dist/schema/framework.js +5 -0
- package/dist/schema/generate.js +17 -1
- package/dist/secrets.d.ts +54 -0
- package/dist/secrets.js +80 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.js +2 -0
- package/dist/security/rate-rules.d.ts +21 -0
- package/dist/security/rate-rules.js +110 -0
- package/dist/seo/index.d.ts +3 -0
- package/dist/seo/index.js +4 -0
- package/dist/seo/resolve.d.ts +68 -0
- package/dist/seo/resolve.js +73 -0
- package/dist/seo/routes.d.ts +44 -0
- package/dist/seo/routes.js +104 -0
- package/dist/seo/structured-data.d.ts +51 -0
- package/dist/seo/structured-data.js +105 -0
- package/dist/status.d.ts +51 -0
- package/dist/status.js +113 -0
- package/dist/worker/generate.d.ts +18 -10
- package/dist/worker/generate.js +353 -42
- package/dist/worker/routes.d.ts +1 -1
- package/dist/worker/routes.js +42 -0
- package/dist/workflow/advance.d.ts +102 -0
- package/dist/workflow/advance.js +145 -0
- package/dist/workflow/config.d.ts +60 -0
- package/dist/workflow/config.js +73 -0
- package/dist/workflow/generate.d.ts +22 -0
- package/dist/workflow/generate.js +138 -0
- package/dist/workflow/index.d.ts +3 -0
- package/dist/workflow/index.js +4 -0
- package/package.json +21 -4
- package/src/components/Editable.astro +33 -9
- package/src/components/JustifiedGallery.astro +254 -0
- package/src/components/MediaSlot.astro +178 -0
- package/src/components/PortalShell.astro +80 -0
- package/src/components/RegisterSW.astro +45 -0
- package/src/components/Section.astro +101 -35
- package/src/components/Sections.astro +64 -0
- package/src/components/Seo.astro +57 -0
- package/src/components/StageBar.astro +137 -0
- package/src/components/StructuredData.astro +33 -0
- package/src/components/justify.ts +170 -0
- package/src/components/media-meta.ts +174 -0
- package/src/components/sections/AboutIntro.astro +46 -0
- package/src/components/sections/Banner.astro +31 -0
- package/src/components/sections/Contact.astro +22 -9
- package/src/components/sections/Cta.astro +33 -10
- package/src/components/sections/Faq.astro +50 -0
- package/src/components/sections/FeatureGrid.astro +40 -11
- package/src/components/sections/Gallery.astro +46 -0
- package/src/components/sections/Hero.astro +40 -12
- package/src/components/sections/LocationHours.astro +59 -0
- package/src/components/sections/Media.astro +44 -0
- package/src/components/sections/PricingTiers.astro +79 -0
- package/src/components/sections/ProductGrid.astro +73 -0
- package/src/components/sections/SplitImage.astro +61 -0
- package/src/components/sections/Steps.astro +58 -0
- package/src/components/sections/Testimonial.astro +51 -0
- package/src/components/sections.ts +452 -67
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,
|
|
11
|
-
* to bottom.
|
|
12
|
-
*
|
|
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 =
|
|
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.
|
|
18
|
-
*
|
|
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 = "
|
|
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
|
-
/**
|
|
44
|
-
*
|
|
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. `
|
|
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
|
-
|
|
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", "
|
|
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", "
|
|
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[]>;
|