astroidjs 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +240 -5
- package/bin/astroid.mjs +185 -9
- package/dist/analytics/index.d.ts +37 -0
- package/dist/analytics/index.js +108 -0
- package/dist/astro/csp.d.ts +64 -0
- package/dist/astro/csp.js +173 -0
- package/dist/astro/index.d.ts +1 -0
- package/dist/astro/index.js +7 -0
- package/dist/commerce/adapters.d.ts +60 -0
- package/dist/commerce/adapters.js +90 -0
- package/dist/commerce/checkout-scaffold.d.ts +42 -0
- package/dist/commerce/checkout-scaffold.js +306 -0
- package/dist/commerce/checkout.d.ts +72 -0
- package/dist/commerce/checkout.js +124 -0
- package/dist/commerce/index.d.ts +8 -0
- package/dist/commerce/index.js +9 -0
- package/dist/commerce/loader.d.ts +71 -0
- package/dist/commerce/loader.js +90 -0
- package/dist/commerce/mirror.d.ts +67 -0
- package/dist/commerce/mirror.js +203 -0
- package/dist/commerce/roles.d.ts +38 -0
- package/dist/commerce/roles.js +93 -0
- package/dist/commerce/secrets.d.ts +74 -0
- package/dist/commerce/secrets.js +129 -0
- package/dist/commerce/sync.d.ts +86 -0
- package/dist/commerce/sync.js +154 -0
- package/dist/components/sections.d.ts +577 -0
- package/dist/components/sections.js +425 -0
- package/dist/config.d.ts +174 -12
- package/dist/config.js +43 -1
- package/dist/email/index.d.ts +4 -0
- package/dist/email/index.js +5 -0
- package/dist/email/inquiry.d.ts +33 -0
- package/dist/email/inquiry.js +63 -0
- package/dist/email/send.d.ts +120 -0
- package/dist/email/send.js +196 -0
- package/dist/email/templates.d.ts +24 -0
- package/dist/email/templates.js +184 -0
- package/dist/email/theme.d.ts +24 -0
- package/dist/email/theme.js +150 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.js +17 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/map/index.d.ts +3 -0
- package/dist/map/index.js +4 -0
- package/dist/map/pmtiles.d.ts +92 -0
- package/dist/map/pmtiles.js +130 -0
- package/dist/map/scaffold.d.ts +29 -0
- package/dist/map/scaffold.js +212 -0
- package/dist/map/style.d.ts +58 -0
- package/dist/map/style.js +154 -0
- package/dist/portal/config.d.ts +26 -0
- package/dist/portal/config.js +50 -0
- package/dist/portal/guard.d.ts +48 -0
- package/dist/portal/guard.js +64 -0
- package/dist/portal/index.d.ts +5 -0
- package/dist/portal/index.js +6 -0
- package/dist/portal/nav.d.ts +26 -0
- package/dist/portal/nav.js +35 -0
- package/dist/portal/scaffold.d.ts +28 -0
- package/dist/portal/scaffold.js +140 -0
- package/dist/portal/session.d.ts +36 -0
- package/dist/portal/session.js +86 -0
- package/dist/portfolio/index.d.ts +1 -0
- package/dist/portfolio/index.js +4 -0
- package/dist/portfolio/scaffold.d.ts +9 -0
- package/dist/portfolio/scaffold.js +93 -0
- package/dist/project/actions.d.ts +3 -0
- package/dist/project/actions.js +106 -0
- package/dist/project/generate.d.ts +15 -0
- package/dist/project/generate.js +144 -2
- package/dist/project/index.d.ts +2 -0
- package/dist/project/index.js +2 -0
- package/dist/project/scaffold.d.ts +29 -0
- package/dist/project/scaffold.js +140 -0
- package/dist/pwa/generate.d.ts +49 -0
- package/dist/pwa/generate.js +218 -0
- package/dist/pwa/index.d.ts +1 -0
- package/dist/pwa/index.js +2 -0
- package/dist/queues/consumer.d.ts +29 -0
- package/dist/queues/consumer.js +37 -0
- package/dist/queues/index.d.ts +4 -0
- package/dist/queues/index.js +5 -0
- package/dist/queues/messages.d.ts +60 -0
- package/dist/queues/messages.js +71 -0
- package/dist/queues/scaffold.d.ts +44 -0
- package/dist/queues/scaffold.js +204 -0
- package/dist/queues/webhook.d.ts +60 -0
- package/dist/queues/webhook.js +81 -0
- package/dist/realtime/index.d.ts +1 -0
- package/dist/realtime/index.js +4 -0
- package/dist/realtime/scaffold.d.ts +30 -0
- package/dist/realtime/scaffold.js +159 -0
- package/dist/schema/collections.d.ts +42 -8
- package/dist/schema/collections.js +102 -8
- package/dist/schema/generate.js +10 -1
- package/dist/secrets.d.ts +54 -0
- package/dist/secrets.js +80 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.js +2 -0
- package/dist/security/rate-rules.d.ts +21 -0
- package/dist/security/rate-rules.js +107 -0
- package/dist/seo/index.d.ts +3 -0
- package/dist/seo/index.js +4 -0
- package/dist/seo/resolve.d.ts +68 -0
- package/dist/seo/resolve.js +73 -0
- package/dist/seo/routes.d.ts +44 -0
- package/dist/seo/routes.js +104 -0
- package/dist/seo/structured-data.d.ts +51 -0
- package/dist/seo/structured-data.js +105 -0
- package/dist/status.d.ts +51 -0
- package/dist/status.js +113 -0
- package/dist/worker/generate.d.ts +18 -10
- package/dist/worker/generate.js +325 -37
- package/dist/worker/routes.d.ts +1 -1
- package/dist/worker/routes.js +42 -0
- package/dist/workflow/advance.d.ts +102 -0
- package/dist/workflow/advance.js +145 -0
- package/dist/workflow/config.d.ts +60 -0
- package/dist/workflow/config.js +73 -0
- package/dist/workflow/generate.d.ts +22 -0
- package/dist/workflow/generate.js +138 -0
- package/dist/workflow/index.d.ts +3 -0
- package/dist/workflow/index.js +4 -0
- package/package.json +21 -5
- package/src/components/Editable.astro +33 -9
- package/src/components/JustifiedGallery.astro +254 -0
- package/src/components/MediaSlot.astro +178 -0
- package/src/components/PortalShell.astro +80 -0
- package/src/components/RegisterSW.astro +45 -0
- package/src/components/Section.astro +101 -35
- package/src/components/Sections.astro +64 -0
- package/src/components/Seo.astro +57 -0
- package/src/components/StageBar.astro +137 -0
- package/src/components/StructuredData.astro +33 -0
- package/src/components/justify.ts +170 -0
- package/src/components/media-meta.ts +174 -0
- package/src/components/sections/AboutIntro.astro +46 -0
- package/src/components/sections/Banner.astro +31 -0
- package/src/components/sections/Contact.astro +22 -9
- package/src/components/sections/Cta.astro +33 -10
- package/src/components/sections/Faq.astro +50 -0
- package/src/components/sections/FeatureGrid.astro +40 -11
- package/src/components/sections/Gallery.astro +46 -0
- package/src/components/sections/Hero.astro +40 -12
- package/src/components/sections/LocationHours.astro +59 -0
- package/src/components/sections/Media.astro +44 -0
- package/src/components/sections/PricingTiers.astro +79 -0
- package/src/components/sections/ProductGrid.astro +73 -0
- package/src/components/sections/SplitImage.astro +61 -0
- package/src/components/sections/Steps.astro +58 -0
- package/src/components/sections/Testimonial.astro +51 -0
- package/src/components/sections.ts +452 -67
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// `robots.txt` + `sitemap.xml` builders.
|
|
4
|
+
//
|
|
5
|
+
// Both are ORIGIN-AWARE rather than built against a configured `site` URL, and
|
|
6
|
+
// that's deliberate: a project serves from several hosts (`*.workers.dev`, a
|
|
7
|
+
// preview subdomain, the custom domain), and a sitemap that advertises the
|
|
8
|
+
// canonical host from a preview deploy invites the preview's content to be
|
|
9
|
+
// indexed under the real domain. Deriving the origin from the request means each
|
|
10
|
+
// host describes only itself.
|
|
11
|
+
//
|
|
12
|
+
// The disallow list is derived from the config for the same reason the rate
|
|
13
|
+
// rules are: which routes exist is a function of which modules are enabled, and
|
|
14
|
+
// a hand-maintained list drifts the moment someone turns a portal on.
|
|
15
|
+
import { ASTROID_PORTAL_BASE_PATH } from "../security/rate-rules.js";
|
|
16
|
+
/**
|
|
17
|
+
* Paths that must never be indexed, derived from the config: the editor and its
|
|
18
|
+
* API always, plus the portal's account + auth surfaces and checkout when those
|
|
19
|
+
* are enabled.
|
|
20
|
+
*
|
|
21
|
+
* These are *prefixes* — `robots.txt` matches by prefix, so `/api/` covers every
|
|
22
|
+
* endpoint beneath it.
|
|
23
|
+
*/
|
|
24
|
+
export function astroidNoindexPaths(config) {
|
|
25
|
+
const paths = [
|
|
26
|
+
// Every worker route (editor CRUD, media, forms) and Better Auth.
|
|
27
|
+
"/api/",
|
|
28
|
+
// The editor entry point.
|
|
29
|
+
"/louise",
|
|
30
|
+
];
|
|
31
|
+
if (config.portal?.enabled) {
|
|
32
|
+
paths.push(ASTROID_PORTAL_BASE_PATH, "/account", "/login", "/register", "/reset-password");
|
|
33
|
+
}
|
|
34
|
+
if (config.commerce) {
|
|
35
|
+
// The checkout PAGE, not `ASTROID_CHECKOUT_PATH` — that's the POST endpoint,
|
|
36
|
+
// already covered by the `/api/` prefix. What a crawler would actually reach
|
|
37
|
+
// is the UI route.
|
|
38
|
+
paths.push("/checkout", "/cart");
|
|
39
|
+
}
|
|
40
|
+
return [...new Set(paths)].sort();
|
|
41
|
+
}
|
|
42
|
+
/** Render `robots.txt`, pointing at the sitemap on the SAME origin. */
|
|
43
|
+
export function astroidRobotsTxt(config, options) {
|
|
44
|
+
const origin = options.origin.replace(/\/$/, "");
|
|
45
|
+
if (options.disableIndexing) {
|
|
46
|
+
return ["User-agent: *", "Disallow: /", ""].join("\n");
|
|
47
|
+
}
|
|
48
|
+
const disallow = options.disallow ?? astroidNoindexPaths(config);
|
|
49
|
+
return [
|
|
50
|
+
"User-agent: *",
|
|
51
|
+
"Allow: /",
|
|
52
|
+
...disallow.map((path) => `Disallow: ${path}`),
|
|
53
|
+
"",
|
|
54
|
+
`Sitemap: ${origin}/sitemap.xml`,
|
|
55
|
+
"",
|
|
56
|
+
].join("\n");
|
|
57
|
+
}
|
|
58
|
+
const XML_ESCAPES = {
|
|
59
|
+
"&": "&",
|
|
60
|
+
"<": "<",
|
|
61
|
+
">": ">",
|
|
62
|
+
'"': """,
|
|
63
|
+
"'": "'",
|
|
64
|
+
};
|
|
65
|
+
const escapeXml = (value) => value.replace(/[&<>"']/g, (c) => XML_ESCAPES[c] ?? c);
|
|
66
|
+
/**
|
|
67
|
+
* Render `sitemap.xml` from a set of paths.
|
|
68
|
+
*
|
|
69
|
+
* Entries are de-duplicated and sorted (a stable document diffs cleanly and
|
|
70
|
+
* caches predictably), excluded paths are dropped by prefix, and every `loc` is
|
|
71
|
+
* XML-escaped — a slug containing `&` would otherwise produce a malformed
|
|
72
|
+
* document that search engines reject wholesale.
|
|
73
|
+
*/
|
|
74
|
+
export function astroidSitemapXml(config, entries, options) {
|
|
75
|
+
const origin = options.origin.replace(/\/$/, "");
|
|
76
|
+
const exclude = options.exclude ?? astroidNoindexPaths(config);
|
|
77
|
+
const seen = new Map();
|
|
78
|
+
for (const raw of entries) {
|
|
79
|
+
const entry = typeof raw === "string" ? { path: raw } : raw;
|
|
80
|
+
const path = entry.path.startsWith("/") ? entry.path : `/${entry.path}`;
|
|
81
|
+
if (exclude.some((prefix) => path.startsWith(prefix)))
|
|
82
|
+
continue;
|
|
83
|
+
if (!seen.has(path))
|
|
84
|
+
seen.set(path, { ...entry, path });
|
|
85
|
+
}
|
|
86
|
+
const urls = [...seen.values()]
|
|
87
|
+
.sort((a, b) => a.path.localeCompare(b.path))
|
|
88
|
+
.map((entry) => {
|
|
89
|
+
const lastmod = entry.lastmod instanceof Date
|
|
90
|
+
? entry.lastmod.toISOString()
|
|
91
|
+
: typeof entry.lastmod === "string"
|
|
92
|
+
? entry.lastmod
|
|
93
|
+
: undefined;
|
|
94
|
+
const loc = `<loc>${escapeXml(`${origin}${entry.path}`)}</loc>`;
|
|
95
|
+
return ` <url>${loc}${lastmod ? `<lastmod>${escapeXml(lastmod)}</lastmod>` : ""}</url>`;
|
|
96
|
+
});
|
|
97
|
+
return [
|
|
98
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
99
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
100
|
+
...urls,
|
|
101
|
+
"</urlset>",
|
|
102
|
+
"",
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Archetype, AstroidConfig } from "../config.js";
|
|
2
|
+
import type { AstroidSeoSettings } from "./resolve.js";
|
|
3
|
+
/** JSON-LD is a plain JSON tree; this is as much typing as it deserves. */
|
|
4
|
+
export type JsonLdNode = Record<string, unknown>;
|
|
5
|
+
/**
|
|
6
|
+
* The schema.org `@type` each archetype describes its owner with. These are
|
|
7
|
+
* intentionally the broad parent types: pick a subtype (`CafeOrCoffeeShop`,
|
|
8
|
+
* `ArtGallery`, `HomeAndConstructionBusiness`) via `seo.businessType` when you
|
|
9
|
+
* know one — a more specific type is strictly better for rich results.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ARCHETYPE_BUSINESS_TYPE: Record<Archetype, string>;
|
|
12
|
+
export interface StructuredDataInput {
|
|
13
|
+
config: AstroidConfig;
|
|
14
|
+
settings: AstroidSeoSettings & {
|
|
15
|
+
logoUrl?: string | null;
|
|
16
|
+
contactEmail?: string | null;
|
|
17
|
+
contactPhone?: string | null;
|
|
18
|
+
contactAddress?: string | null;
|
|
19
|
+
/** Profile URLs, as stored in `site_settings.social_links`. */
|
|
20
|
+
socialLinks?: unknown;
|
|
21
|
+
};
|
|
22
|
+
/** Absolute origin serving this page (the canonical host). */
|
|
23
|
+
siteUrl: string;
|
|
24
|
+
/**
|
|
25
|
+
* An extra node for the thing this page is *about* — a Product, a
|
|
26
|
+
* VisualArtwork, an Article. Joined into the same `@graph` so crawlers see
|
|
27
|
+
* one connected description rather than three unrelated blobs.
|
|
28
|
+
*/
|
|
29
|
+
entity?: JsonLdNode;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Build the JSON-LD `@graph` for a page: the business node, a `WebSite` node,
|
|
33
|
+
* and the page's own entity when there is one.
|
|
34
|
+
*
|
|
35
|
+
* The business gets a stable `@id` (`<origin>/#business`) so other nodes — a
|
|
36
|
+
* product's `seller`, a future `Article` author — can reference it by id
|
|
37
|
+
* instead of restating it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function astroidStructuredData(input: StructuredDataInput): JsonLdNode;
|
|
40
|
+
/**
|
|
41
|
+
* Serialize JSON-LD for injection into a `<script type="application/ld+json">`.
|
|
42
|
+
*
|
|
43
|
+
* `application/ld+json` is data, not executable script, so `script-src` doesn't
|
|
44
|
+
* govern it and no CSP hash is needed. But `JSON.stringify` does **not** escape
|
|
45
|
+
* `<`, so any value folded into the graph that contains a literal `</script>` —
|
|
46
|
+
* a product description, an artist statement, anything editor-authored — would
|
|
47
|
+
* close the tag early and inject markup straight into `<head>`. Escaping the
|
|
48
|
+
* HTML-significant characters as `\uXXXX` keeps the payload valid JSON while
|
|
49
|
+
* making it impossible to break out of the element.
|
|
50
|
+
*/
|
|
51
|
+
export declare function escapeJsonLd(value: unknown): string;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// JSON-LD structured data — the `@graph` that describes the business to rich
|
|
4
|
+
// results and AI answer surfaces.
|
|
5
|
+
//
|
|
6
|
+
// Everything here is generic except the business `@type`, which is the one thing
|
|
7
|
+
// that genuinely differs per site — so it comes from the archetype, with a
|
|
8
|
+
// config escape hatch for the many cases where schema.org has a more specific
|
|
9
|
+
// subtype (a coffee shop is a `CafeOrCoffeeShop`, not a bare `Store`).
|
|
10
|
+
/**
|
|
11
|
+
* The schema.org `@type` each archetype describes its owner with. These are
|
|
12
|
+
* intentionally the broad parent types: pick a subtype (`CafeOrCoffeeShop`,
|
|
13
|
+
* `ArtGallery`, `HomeAndConstructionBusiness`) via `seo.businessType` when you
|
|
14
|
+
* know one — a more specific type is strictly better for rich results.
|
|
15
|
+
*/
|
|
16
|
+
export const ARCHETYPE_BUSINESS_TYPE = {
|
|
17
|
+
marketing: "Organization",
|
|
18
|
+
storefront: "Store",
|
|
19
|
+
wholesale: "Organization",
|
|
20
|
+
// A portfolio site is a person's body of work far more often than a company's.
|
|
21
|
+
portfolio: "Person",
|
|
22
|
+
};
|
|
23
|
+
/** Absolute URL, or undefined when the value is empty or unparseable. */
|
|
24
|
+
function absolute(value, base) {
|
|
25
|
+
const raw = value?.trim();
|
|
26
|
+
if (!raw)
|
|
27
|
+
return undefined;
|
|
28
|
+
try {
|
|
29
|
+
return new URL(raw, base).toString();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Pull profile URLs out of `socialLinks`, which the editor stores as either a
|
|
36
|
+
* `{ instagram: "…" }` map or a plain array. */
|
|
37
|
+
function sameAs(links) {
|
|
38
|
+
const values = Array.isArray(links)
|
|
39
|
+
? links
|
|
40
|
+
: links && typeof links === "object"
|
|
41
|
+
? Object.values(links)
|
|
42
|
+
: [];
|
|
43
|
+
return values
|
|
44
|
+
.filter((v) => typeof v === "string")
|
|
45
|
+
.map((v) => v.trim())
|
|
46
|
+
.filter((v) => /^https?:\/\//.test(v));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build the JSON-LD `@graph` for a page: the business node, a `WebSite` node,
|
|
50
|
+
* and the page's own entity when there is one.
|
|
51
|
+
*
|
|
52
|
+
* The business gets a stable `@id` (`<origin>/#business`) so other nodes — a
|
|
53
|
+
* product's `seller`, a future `Article` author — can reference it by id
|
|
54
|
+
* instead of restating it.
|
|
55
|
+
*/
|
|
56
|
+
export function astroidStructuredData(input) {
|
|
57
|
+
const { config, settings, siteUrl, entity } = input;
|
|
58
|
+
const origin = siteUrl.replace(/\/$/, "");
|
|
59
|
+
const businessId = `${origin}/#business`;
|
|
60
|
+
const name = settings.siteName?.trim() || config.theme.name;
|
|
61
|
+
const profiles = sameAs(settings.socialLinks);
|
|
62
|
+
const image = absolute(settings.defaultOgImageUrl, siteUrl) ?? absolute(settings.logoUrl, siteUrl);
|
|
63
|
+
const business = {
|
|
64
|
+
"@type": config.seo?.businessType ?? ARCHETYPE_BUSINESS_TYPE[config.archetype],
|
|
65
|
+
"@id": businessId,
|
|
66
|
+
name,
|
|
67
|
+
url: origin,
|
|
68
|
+
...(settings.tagline?.trim() ? { description: settings.tagline.trim() } : {}),
|
|
69
|
+
...(image ? { image, logo: image } : {}),
|
|
70
|
+
...(settings.contactEmail?.trim() ? { email: settings.contactEmail.trim() } : {}),
|
|
71
|
+
...(settings.contactPhone?.trim() ? { telephone: settings.contactPhone.trim() } : {}),
|
|
72
|
+
...(settings.contactAddress?.trim()
|
|
73
|
+
? { address: { "@type": "PostalAddress", streetAddress: settings.contactAddress.trim() } }
|
|
74
|
+
: {}),
|
|
75
|
+
...(profiles.length ? { sameAs: profiles } : {}),
|
|
76
|
+
};
|
|
77
|
+
const website = {
|
|
78
|
+
"@type": "WebSite",
|
|
79
|
+
"@id": `${origin}/#website`,
|
|
80
|
+
url: origin,
|
|
81
|
+
name,
|
|
82
|
+
publisher: { "@id": businessId },
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
"@context": "https://schema.org",
|
|
86
|
+
"@graph": entity ? [business, website, entity] : [business, website],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Serialize JSON-LD for injection into a `<script type="application/ld+json">`.
|
|
91
|
+
*
|
|
92
|
+
* `application/ld+json` is data, not executable script, so `script-src` doesn't
|
|
93
|
+
* govern it and no CSP hash is needed. But `JSON.stringify` does **not** escape
|
|
94
|
+
* `<`, so any value folded into the graph that contains a literal `</script>` —
|
|
95
|
+
* a product description, an artist statement, anything editor-authored — would
|
|
96
|
+
* close the tag early and inject markup straight into `<head>`. Escaping the
|
|
97
|
+
* HTML-significant characters as `\uXXXX` keeps the payload valid JSON while
|
|
98
|
+
* making it impossible to break out of the element.
|
|
99
|
+
*/
|
|
100
|
+
export function escapeJsonLd(value) {
|
|
101
|
+
return JSON.stringify(value)
|
|
102
|
+
.replace(/</g, "\\u003c")
|
|
103
|
+
.replace(/>/g, "\\u003e")
|
|
104
|
+
.replace(/&/g, "\\u0026");
|
|
105
|
+
}
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { AstroidConfig } from "./config.js";
|
|
2
|
+
import { type MailerEnv } from "./email/send.js";
|
|
3
|
+
import type { SecretSource } from "./secrets.js";
|
|
4
|
+
/**
|
|
5
|
+
* Secrets every Astroid site has, independent of which modules are on.
|
|
6
|
+
*
|
|
7
|
+
* `SESSION_SECRET` is here but is NOT a dormancy gate — it fails closed off
|
|
8
|
+
* localhost (see `getSessionSecret`), because an unsigned session isn't a
|
|
9
|
+
* feature to switch off. It's listed so the scaffold seeds and types it.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ASTROID_CORE_SECRET_NAMES: readonly ["SESSION_SECRET", "TURNSTILE_SECRET", "TURNSTILE_SITE_KEY"];
|
|
12
|
+
/** One module's line in the report. */
|
|
13
|
+
export interface AstroidModuleReport {
|
|
14
|
+
/** Module name, as a developer would say it: `"commerce"`, `"email"`. */
|
|
15
|
+
module: string;
|
|
16
|
+
/** Whether the project switched this module on at all. */
|
|
17
|
+
enabled: boolean;
|
|
18
|
+
/** Whether it has everything it needs to run live. */
|
|
19
|
+
configured: boolean;
|
|
20
|
+
/** Unprovisioned secret/binding names, in declaration order. */
|
|
21
|
+
missing: string[];
|
|
22
|
+
/** What the module does in this state — the sentence a banner prints. */
|
|
23
|
+
detail: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Every secret name this config implies, grouped by module.
|
|
27
|
+
*
|
|
28
|
+
* The scaffold uses this twice: to seed `.dev.vars`/`.env.example` with the
|
|
29
|
+
* placeholder sentinel, and to type the matching `CloudflareEnv` members. A
|
|
30
|
+
* module that isn't enabled contributes nothing — a declaration is a promise,
|
|
31
|
+
* and a marketing site shouldn't be told to provision a Square token.
|
|
32
|
+
*/
|
|
33
|
+
export declare function astroidSecretNames(config: AstroidConfig): Record<string, string[]>;
|
|
34
|
+
/** The env shape the status report reads. Structural; a real `env` fits. */
|
|
35
|
+
export type AstroidStatusEnv = MailerEnv & Record<string, SecretSource | unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve every enabled module's gate.
|
|
38
|
+
*
|
|
39
|
+
* ```ts
|
|
40
|
+
* // src/pages/api/_astroid/status.ts, or a dev-only banner
|
|
41
|
+
* const report = await astroidModuleStatus(config, env);
|
|
42
|
+
* console.info(describeAstroidStatus(report));
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function astroidModuleStatus(config: AstroidConfig, env: AstroidStatusEnv): Promise<AstroidModuleReport[]>;
|
|
46
|
+
/**
|
|
47
|
+
* The report as a printable block. One line per module, missing names spelled
|
|
48
|
+
* out — "commerce is off" sends someone reading source; "commerce is dormant —
|
|
49
|
+
* set SQUARE_ACCESS_TOKEN" does not.
|
|
50
|
+
*/
|
|
51
|
+
export declare function describeAstroidStatus(reports: AstroidModuleReport[]): string;
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// Copyright (c) 2026 BowenLabs. Astroid is MIT licensed.
|
|
2
|
+
//
|
|
3
|
+
// The module status report — "what is actually switched on right now".
|
|
4
|
+
//
|
|
5
|
+
// `secrets.ts` gives one module its gate. This composes every enabled module's
|
|
6
|
+
// gate into one answer, which is what the dormant-until-provisioned convention
|
|
7
|
+
// needs to be usable rather than merely available: a fresh scaffold boots with
|
|
8
|
+
// nothing provisioned, and the failure mode that convention exists to avoid is
|
|
9
|
+
// not a crash — it's a developer wondering for twenty minutes why the contact
|
|
10
|
+
// form "works" but no mail arrives.
|
|
11
|
+
//
|
|
12
|
+
// So the deal is: dormant is fine, dormant AND SILENT is not. Two consumers of
|
|
13
|
+
// this, both cheap:
|
|
14
|
+
//
|
|
15
|
+
// • `astroid doctor` / a dev-server banner prints `describeAstroidStatus`.
|
|
16
|
+
// • `astroidSecretNames` drives what the scaffold seeds and types, so the
|
|
17
|
+
// list a developer is told to fill is generated from the same declaration
|
|
18
|
+
// the runtime gate reads. They cannot drift.
|
|
19
|
+
import { commerceSecretNames, resolveCommerceStatus } from "./commerce/secrets.js";
|
|
20
|
+
import { ASTROID_VITALS_SECRET_NAMES } from "./analytics/index.js";
|
|
21
|
+
import { EMAIL_SECRET_NAMES, resolveMailerStatus } from "./email/send.js";
|
|
22
|
+
/**
|
|
23
|
+
* Secrets every Astroid site has, independent of which modules are on.
|
|
24
|
+
*
|
|
25
|
+
* `SESSION_SECRET` is here but is NOT a dormancy gate — it fails closed off
|
|
26
|
+
* localhost (see `getSessionSecret`), because an unsigned session isn't a
|
|
27
|
+
* feature to switch off. It's listed so the scaffold seeds and types it.
|
|
28
|
+
*/
|
|
29
|
+
export const ASTROID_CORE_SECRET_NAMES = [
|
|
30
|
+
"SESSION_SECRET",
|
|
31
|
+
"TURNSTILE_SECRET",
|
|
32
|
+
"TURNSTILE_SITE_KEY",
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Every secret name this config implies, grouped by module.
|
|
36
|
+
*
|
|
37
|
+
* The scaffold uses this twice: to seed `.dev.vars`/`.env.example` with the
|
|
38
|
+
* placeholder sentinel, and to type the matching `CloudflareEnv` members. A
|
|
39
|
+
* module that isn't enabled contributes nothing — a declaration is a promise,
|
|
40
|
+
* and a marketing site shouldn't be told to provision a Square token.
|
|
41
|
+
*/
|
|
42
|
+
export function astroidSecretNames(config) {
|
|
43
|
+
const groups = {
|
|
44
|
+
core: [...ASTROID_CORE_SECRET_NAMES],
|
|
45
|
+
email: [...EMAIL_SECRET_NAMES],
|
|
46
|
+
};
|
|
47
|
+
const commerce = commerceSecretNames(config.commerce);
|
|
48
|
+
if (commerce.length > 0)
|
|
49
|
+
groups.commerce = commerce;
|
|
50
|
+
// The CWV read-back's API credentials. Collection needs none of this — only
|
|
51
|
+
// querying the p75 back out does, because the Analytics Engine SQL API is
|
|
52
|
+
// account-scoped and has no binding.
|
|
53
|
+
groups.vitals = [...ASTROID_VITALS_SECRET_NAMES];
|
|
54
|
+
return groups;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Resolve every enabled module's gate.
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* // src/pages/api/_astroid/status.ts, or a dev-only banner
|
|
61
|
+
* const report = await astroidModuleStatus(config, env);
|
|
62
|
+
* console.info(describeAstroidStatus(report));
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export async function astroidModuleStatus(config, env) {
|
|
66
|
+
const secretEnv = env;
|
|
67
|
+
const [commerce, mailer] = await Promise.all([
|
|
68
|
+
resolveCommerceStatus(config.commerce, secretEnv),
|
|
69
|
+
resolveMailerStatus(env),
|
|
70
|
+
]);
|
|
71
|
+
const reports = [
|
|
72
|
+
{
|
|
73
|
+
module: "email",
|
|
74
|
+
enabled: true,
|
|
75
|
+
configured: mailer.configured,
|
|
76
|
+
missing: mailer.missing,
|
|
77
|
+
detail: mailer.configured
|
|
78
|
+
? "sending through the EMAIL binding"
|
|
79
|
+
: // Naming the log is the useful part: the magic link IS in the console,
|
|
80
|
+
// and someone who doesn't know that concludes sign-in is broken.
|
|
81
|
+
"dormant — messages are logged to the console (the magic link is in the log), not sent",
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
if (commerce.enabled) {
|
|
85
|
+
reports.push({
|
|
86
|
+
module: "commerce",
|
|
87
|
+
enabled: true,
|
|
88
|
+
configured: commerce.configured,
|
|
89
|
+
missing: commerce.missing,
|
|
90
|
+
detail: commerce.configured
|
|
91
|
+
? `live via ${commerce.providers.map((p) => p.provider).join(" + ")}`
|
|
92
|
+
: "dormant — the D1 catalog mirror still serves, but nothing syncs from the provider and checkout is simulated",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return reports;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The report as a printable block. One line per module, missing names spelled
|
|
99
|
+
* out — "commerce is off" sends someone reading source; "commerce is dormant —
|
|
100
|
+
* set SQUARE_ACCESS_TOKEN" does not.
|
|
101
|
+
*/
|
|
102
|
+
export function describeAstroidStatus(reports) {
|
|
103
|
+
if (reports.length === 0)
|
|
104
|
+
return "[astroid] no optional modules enabled";
|
|
105
|
+
return [
|
|
106
|
+
"[astroid] module status",
|
|
107
|
+
...reports.map((r) => {
|
|
108
|
+
const head = ` ${r.module}: ${r.configured ? "configured" : "dormant (simulated)"}`;
|
|
109
|
+
const why = r.missing.length > 0 ? ` — unprovisioned: ${r.missing.join(", ")}` : "";
|
|
110
|
+
return `${head}${why}\n ${r.detail}`;
|
|
111
|
+
}),
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { AstroidConfig } from "../config.js";
|
|
2
|
+
export declare const ASTROID_SETTINGS_COLUMNS: string[];
|
|
3
|
+
export declare const ASTROID_SETTINGS_IMAGE_KEYS: string[];
|
|
2
4
|
/**
|
|
3
5
|
* Generate the Worker entrypoint (`worker.ts`) from an Astroid config: the editor
|
|
4
6
|
* routes in collision-free order, an R2 media-asset route, and the `composeWorker`
|
|
@@ -10,15 +12,21 @@ export declare function generateAstroidWorker(config: AstroidConfig): string;
|
|
|
10
12
|
* Generate the Astro middleware (`middleware.ts`) from an Astroid config: the
|
|
11
13
|
* shared Louise flow (rate-limit the unauthenticated POST surface → resolve editor
|
|
12
14
|
* session + sticky `?louise` edit mode → content-freshness + security headers) via
|
|
13
|
-
* `createLouiseMiddleware`.
|
|
14
|
-
* sign-in) against the provisioned `RL` KV; auth is the same seam as the worker.
|
|
15
|
+
* `createLouiseMiddleware`.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
-
* `
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
17
|
+
* The rate rules are NOT emitted as literals here — the file calls
|
|
18
|
+
* `astroidRateRules(astroidConfig)`, so the set stays real data in the package
|
|
19
|
+
* (testable, and a `match` predicate survives, which a serialized literal could
|
|
20
|
+
* not). Enabling a portal or commerce in the config adds that surface's rules
|
|
21
|
+
* with no regeneration of this file at all.
|
|
22
|
+
*
|
|
23
|
+
* CSP: `astro.config.mjs` enables `security.csp` (via `astroidSecurity`), so
|
|
24
|
+
* Astro emits a hash-based `content-security-policy` response header on every SSR
|
|
25
|
+
* page and owns `script-src`. The `cspStyleSrc` below tells
|
|
26
|
+
* `createLouiseMiddleware` to rewrite that header's `style-src` to
|
|
27
|
+
* `'self' 'unsafe-inline'` — a hash-based `style-src` would, per spec, void the
|
|
28
|
+
* `'unsafe-inline'` that Louise's data-driven `style=""` carriers and the
|
|
29
|
+
* editor's runtime-injected `<style>` require. Script hashes are left verbatim,
|
|
30
|
+
* and the inlined `data:` brand font is auto-allowed.
|
|
23
31
|
*/
|
|
24
|
-
export declare function generateAstroidMiddleware(
|
|
32
|
+
export declare function generateAstroidMiddleware(config: AstroidConfig): string;
|