astroidjs 0.12.1 → 0.14.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 (136) hide show
  1. package/README.md +42 -42
  2. package/bin/astroid.mjs +35 -29
  3. package/dist/analytics/index.d.ts +3 -3
  4. package/dist/analytics/index.js +11 -11
  5. package/dist/astro/csp.d.ts +5 -5
  6. package/dist/astro/csp.js +6 -6
  7. package/dist/astro/index.js +1 -1
  8. package/dist/auth/index.d.ts +2 -2
  9. package/dist/auth/index.js +5 -5
  10. package/dist/commerce/adapters.d.ts +6 -6
  11. package/dist/commerce/adapters.js +9 -9
  12. package/dist/commerce/checkout-scaffold.d.ts +5 -5
  13. package/dist/commerce/checkout-scaffold.js +27 -27
  14. package/dist/commerce/checkout.d.ts +12 -12
  15. package/dist/commerce/checkout.js +7 -7
  16. package/dist/commerce/loader.d.ts +2 -2
  17. package/dist/commerce/loader.js +3 -3
  18. package/dist/commerce/mirror.d.ts +4 -4
  19. package/dist/commerce/mirror.js +12 -12
  20. package/dist/commerce/roles.d.ts +10 -10
  21. package/dist/commerce/roles.js +13 -13
  22. package/dist/commerce/secrets.d.ts +9 -9
  23. package/dist/commerce/secrets.js +9 -9
  24. package/dist/commerce/sync.d.ts +7 -7
  25. package/dist/commerce/sync.js +5 -5
  26. package/dist/components/sections.d.ts +9 -9
  27. package/dist/components/sections.js +12 -12
  28. package/dist/config.d.ts +89 -62
  29. package/dist/config.js +18 -18
  30. package/dist/email/inquiry.d.ts +2 -2
  31. package/dist/email/inquiry.js +1 -1
  32. package/dist/email/send.d.ts +4 -4
  33. package/dist/email/send.js +7 -7
  34. package/dist/email/templates.js +3 -3
  35. package/dist/email/theme.d.ts +1 -1
  36. package/dist/email/theme.js +4 -4
  37. package/dist/errors.d.ts +1 -1
  38. package/dist/errors.js +1 -1
  39. package/dist/index.js +1 -1
  40. package/dist/map/pmtiles.d.ts +5 -5
  41. package/dist/map/pmtiles.js +5 -5
  42. package/dist/map/scaffold.d.ts +2 -2
  43. package/dist/map/scaffold.js +8 -8
  44. package/dist/map/style.d.ts +4 -4
  45. package/dist/map/style.js +1 -1
  46. package/dist/portal/config.d.ts +3 -3
  47. package/dist/portal/config.js +11 -10
  48. package/dist/portal/guard.d.ts +4 -4
  49. package/dist/portal/guard.js +4 -4
  50. package/dist/portal/nav.js +2 -2
  51. package/dist/portal/scaffold.d.ts +4 -4
  52. package/dist/portal/scaffold.js +13 -13
  53. package/dist/portal/session.d.ts +11 -5
  54. package/dist/portal/session.js +10 -5
  55. package/dist/portfolio/scaffold.d.ts +1 -1
  56. package/dist/portfolio/scaffold.js +8 -8
  57. package/dist/project/actions.d.ts +1 -1
  58. package/dist/project/actions.js +18 -12
  59. package/dist/project/generate.d.ts +4 -4
  60. package/dist/project/generate.js +26 -26
  61. package/dist/project/index.d.ts +1 -0
  62. package/dist/project/index.js +2 -1
  63. package/dist/project/scaffold.d.ts +2 -2
  64. package/dist/project/scaffold.js +69 -13
  65. package/dist/project/seed.d.ts +22 -0
  66. package/dist/project/seed.js +214 -0
  67. package/dist/pwa/generate.d.ts +11 -11
  68. package/dist/pwa/generate.js +19 -19
  69. package/dist/queues/consumer.d.ts +3 -3
  70. package/dist/queues/consumer.js +2 -2
  71. package/dist/queues/messages.d.ts +4 -4
  72. package/dist/queues/messages.js +2 -2
  73. package/dist/queues/scaffold.d.ts +4 -4
  74. package/dist/queues/scaffold.js +17 -17
  75. package/dist/queues/webhook.d.ts +5 -5
  76. package/dist/queues/webhook.js +3 -3
  77. package/dist/realtime/scaffold.d.ts +4 -4
  78. package/dist/realtime/scaffold.js +12 -12
  79. package/dist/schema/collections.d.ts +42 -11
  80. package/dist/schema/collections.js +43 -42
  81. package/dist/schema/framework.d.ts +1 -1
  82. package/dist/schema/framework.js +2 -2
  83. package/dist/schema/generate.js +6 -6
  84. package/dist/schema/index.js +1 -1
  85. package/dist/secrets.d.ts +6 -6
  86. package/dist/secrets.js +6 -6
  87. package/dist/security/csp-origins.d.ts +1 -1
  88. package/dist/security/csp-origins.js +3 -3
  89. package/dist/security/rate-rules.d.ts +2 -2
  90. package/dist/security/rate-rules.js +8 -8
  91. package/dist/seo/resolve.d.ts +5 -5
  92. package/dist/seo/resolve.js +2 -2
  93. package/dist/seo/routes.d.ts +5 -5
  94. package/dist/seo/routes.js +3 -3
  95. package/dist/seo/structured-data.d.ts +6 -6
  96. package/dist/seo/structured-data.js +7 -7
  97. package/dist/status.d.ts +5 -5
  98. package/dist/status.js +7 -7
  99. package/dist/tenancy/index.d.ts +3 -3
  100. package/dist/tenancy/index.js +11 -11
  101. package/dist/worker/generate.d.ts +2 -2
  102. package/dist/worker/generate.js +91 -57
  103. package/dist/worker/index.js +1 -1
  104. package/dist/worker/routes.js +11 -11
  105. package/dist/workflow/advance.d.ts +3 -3
  106. package/dist/workflow/advance.js +6 -6
  107. package/dist/workflow/config.d.ts +4 -4
  108. package/dist/workflow/config.js +4 -4
  109. package/dist/workflow/generate.d.ts +2 -2
  110. package/dist/workflow/generate.js +11 -11
  111. package/package.json +3 -4
  112. package/src/components/Collection.tsx +5 -5
  113. package/src/components/Editable.astro +9 -9
  114. package/src/components/JustifiedGallery.astro +8 -8
  115. package/src/components/MediaSlot.astro +12 -12
  116. package/src/components/PortalShell.astro +4 -4
  117. package/src/components/RegisterSW.astro +3 -3
  118. package/src/components/Section.astro +8 -8
  119. package/src/components/Sections.astro +6 -6
  120. package/src/components/Seo.astro +3 -3
  121. package/src/components/StageBar.astro +3 -3
  122. package/src/components/StructuredData.astro +2 -2
  123. package/src/components/justify.ts +9 -9
  124. package/src/components/media-meta.ts +10 -10
  125. package/src/components/sections/AboutIntro.astro +1 -1
  126. package/src/components/sections/Contact.astro +1 -1
  127. package/src/components/sections/Cta.astro +1 -1
  128. package/src/components/sections/Faq.astro +1 -1
  129. package/src/components/sections/FeatureGrid.astro +2 -2
  130. package/src/components/sections/Hero.astro +1 -1
  131. package/src/components/sections/PricingTiers.astro +1 -1
  132. package/src/components/sections/ProductGrid.astro +1 -1
  133. package/src/components/sections/SplitImage.astro +1 -1
  134. package/src/components/sections/Steps.astro +1 -1
  135. package/src/components/sections/Testimonial.astro +1 -1
  136. package/src/components/sections.ts +17 -17
@@ -14,7 +14,7 @@ export declare const ASTROID_DEFAULT_CRON = "0 * * * *";
14
14
  export declare function astroidCron(config: AstroidConfig): string | null;
15
15
  /**
16
16
  * Daily, at an off-peak-ish minute. The health scan crawls the site's own pages,
17
- * so it is deliberately NOT on the hourly catalog cron — hourly would be a
17
+ * so it is deliberately NOT on the hourly catalog cron—hourly would be a
18
18
  * self-inflicted crawl 24× a day to recompute counts that move slowly.
19
19
  */
20
20
  export declare const ASTROID_HEALTH_CRON = "17 4 * * *";
@@ -31,7 +31,7 @@ export declare const ASTROID_HEALTH_CRON = "17 4 * * *";
31
31
  export declare function astroidCrons(config: AstroidConfig): string[];
32
32
  /** Binding name for the project's queue producer. */
33
33
  export declare const ASTROID_QUEUE_BINDING = "COMMERCE_QUEUE";
34
- /** Queue names derived from the project key — the main queue and its DLQ. */
34
+ /** Queue names derived from the project key—the main queue and its DLQ. */
35
35
  export declare function astroidQueueNames(config: AstroidConfig): {
36
36
  queue: string;
37
37
  dlq: string;
@@ -44,9 +44,9 @@ export declare function astroidQueueNames(config: AstroidConfig): {
44
44
  */
45
45
  export interface WebhookMessage {
46
46
  kind: "webhook";
47
- /** Which integration sent it — `"square"`, `"stripe"`, `"fourthwall"`. */
47
+ /** Which integration sent it—`"square"`, `"stripe"`, `"fourthwall"`. */
48
48
  provider: string;
49
- /** The provider's event type, e.g. `"catalog.version.updated"`. */
49
+ /** The provider's event type, for example, `"catalog.version.updated"`. */
50
50
  type: string;
51
51
  payload: unknown;
52
52
  }
@@ -25,7 +25,7 @@ export function astroidCron(config) {
25
25
  }
26
26
  /**
27
27
  * Daily, at an off-peak-ish minute. The health scan crawls the site's own pages,
28
- * so it is deliberately NOT on the hourly catalog cron — hourly would be a
28
+ * so it is deliberately NOT on the hourly catalog cron—hourly would be a
29
29
  * self-inflicted crawl 24× a day to recompute counts that move slowly.
30
30
  */
31
31
  export const ASTROID_HEALTH_CRON = "17 4 * * *";
@@ -52,7 +52,7 @@ export function astroidCrons(config) {
52
52
  }
53
53
  /** Binding name for the project's queue producer. */
54
54
  export const ASTROID_QUEUE_BINDING = "COMMERCE_QUEUE";
55
- /** Queue names derived from the project key — the main queue and its DLQ. */
55
+ /** Queue names derived from the project key—the main queue and its DLQ. */
56
56
  export function astroidQueueNames(config) {
57
57
  return { queue: `${config.key}-commerce`, dlq: `${config.key}-commerce-dlq` };
58
58
  }
@@ -11,7 +11,7 @@ import type { AstroidConfig, CommerceProvider } from "../config.js";
11
11
  */
12
12
  export declare function generateAstroidEnvBindings(config: AstroidConfig): string;
13
13
  /**
14
- * `src/queue.ts` — the consumer seam the generated worker imports.
14
+ * `src/queue.ts`—the consumer seam the generated worker imports.
15
15
  *
16
16
  * `astroidQueueHandler` already owns the dispatch every site wrote (periodic
17
17
  * refresh, catalog-affecting webhook, no-op for everything else); what's left
@@ -20,14 +20,14 @@ export declare function generateAstroidEnvBindings(config: AstroidConfig): strin
20
20
  */
21
21
  export declare function generateAstroidQueueSeam(config: AstroidConfig): string;
22
22
  /**
23
- * The provider webhook receiver — `src/pages/api/webhooks/<provider>.ts`.
23
+ * The provider webhook receiver—`src/pages/api/webhooks/<provider>.ts`.
24
24
  *
25
25
  * Thin on purpose: `handleWebhook` owns the ordering (verify the raw body before
26
26
  * parsing) and the status-code contract (which codes ask the provider to retry
27
27
  * and which tell it to stop). What's here is the provider's own header and
28
28
  * verifier, plus the secret read.
29
29
  *
30
- * Returns null when the project has no commerce provider — nothing to receive.
30
+ * Returns null when the project has no commerce provider—nothing to receive.
31
31
  */
32
32
  export declare function generateAstroidWebhookRoute(config: AstroidConfig, forProvider?: CommerceProvider): string | null;
33
33
  /**
@@ -35,7 +35,7 @@ export declare function generateAstroidWebhookRoute(config: AstroidConfig, forPr
35
35
  *
36
36
  * Plural because roles are: a site running Stripe for invoicing beside
37
37
  * Fourthwall for the storefront receives from both, each with its own signing
38
- * secret and header. One route per provider, not per role — a provider filling
38
+ * secret and header. One route per provider, not per role—a provider filling
39
39
  * two roles still has one endpoint and one secret.
40
40
  */
41
41
  export declare function generateAstroidWebhookRoutes(config: AstroidConfig): {
@@ -3,7 +3,7 @@
3
3
  // The two SCAFFOLD-ONCE files the queue pipeline needs: the consumer seam
4
4
  // (`src/queue.ts`) and the provider webhook receiver.
5
5
  //
6
- // Deliberately not part of the regenerated trio. Both exist to be edited — the
6
+ // Deliberately not part of the regenerated trio. Both exist to be edited—the
7
7
  // consumer is where a project says what a catalog refresh actually does, and the
8
8
  // webhook route is where it narrows which events it cares about. Regenerating
9
9
  // over them would erase exactly the work they're for. The same boundary
@@ -15,7 +15,7 @@ import { ASTROID_QUEUE_BINDING } from "./messages.js";
15
15
  /**
16
16
  * Per-provider webhook facts: the header, the verifier, and how it's called.
17
17
  *
18
- * The signing-secret NAME is deliberately not here — it's one field of a
18
+ * The signing-secret NAME is deliberately not here—it's one field of a
19
19
  * provider's secret set, which `commerce/secrets.ts` owns so the wrangler
20
20
  * generator, the status report, and this scaffold all read the same list.
21
21
  */
@@ -36,7 +36,7 @@ const PROVIDERS = {
36
36
  // Stripe's header carries a timestamp; the verifier rejects replays outside
37
37
  // its tolerance, so it needs the current time.
38
38
  call: 'verifyStripeSignature(raw, headers.get(HEADER) ?? "", secret, Math.floor(Date.now() / 1000))',
39
- note: "Stripe's signature is timestamped — the verifier rejects replays outside a 5-minute tolerance.",
39
+ note: "Stripe's signature is timestamped—the verifier rejects replays outside a 5-minute tolerance.",
40
40
  },
41
41
  fourthwall: {
42
42
  module: "louise-toolkit/commerce/fourthwall",
@@ -61,12 +61,12 @@ export function generateAstroidEnvBindings(config) {
61
61
  if (providers.length === 0)
62
62
  return "";
63
63
  return [
64
- " /** Queue producer — verified webhooks + the cron re-sync (src/queue.ts). */",
64
+ " /** Queue producer—verified webhooks + the cron re-sync (src/queue.ts). */",
65
65
  ' COMMERCE_QUEUE: Queue<import("astroidjs").AstroidQueueMessage>;',
66
66
  // One secret SET per provider: a site running Stripe for invoicing and
67
67
  // Fourthwall for the storefront talks to both, credentialed and signed
68
68
  // independently. Every one is optional, because every one is allowed to be
69
- // absent — that's what leaves the module dormant rather than broken.
69
+ // absent—that's what leaves the module dormant rather than broken.
70
70
  ...providers.flatMap((provider) => [
71
71
  ` /** ${provider} API credentials. Absent or still holding the`,
72
72
  " * DUMMY_REPLACE_ME sentinel reads as unconfigured, which leaves commerce",
@@ -80,7 +80,7 @@ export function generateAstroidEnvBindings(config) {
80
80
  ].join("\n");
81
81
  }
82
82
  /**
83
- * `src/queue.ts` — the consumer seam the generated worker imports.
83
+ * `src/queue.ts`—the consumer seam the generated worker imports.
84
84
  *
85
85
  * `astroidQueueHandler` already owns the dispatch every site wrote (periodic
86
86
  * refresh, catalog-affecting webhook, no-op for everything else); what's left
@@ -88,7 +88,7 @@ export function generateAstroidEnvBindings(config) {
88
88
  * a generated constant.
89
89
  */
90
90
  export function generateAstroidQueueSeam(config) {
91
- // The STOREFRONT provider — it's the one with a catalog to re-sync. An
91
+ // The STOREFRONT provider—it's the one with a catalog to re-sync. An
92
92
  // invoicing-only provider has nothing for this hook to do.
93
93
  const provider = astroidCommerceRoles(config.commerce).storefront;
94
94
  const table = astroidCatalogMirror(config).table;
@@ -97,15 +97,15 @@ export function generateAstroidQueueSeam(config) {
97
97
  // in rather than leaving it to be discovered on a busy day (#294).
98
98
  const multiProvider = astroidCommerceProviders(config.commerce).length > 1;
99
99
  return [
100
- "// The queue consumer — what each message actually does.",
100
+ "// The queue consumer—what each message actually does.",
101
101
  "//",
102
102
  "// Scaffolded once; yours to edit. `astroidQueueHandler` owns the dispatch",
103
103
  "// (a periodic refresh and any catalog-affecting webhook trigger a re-sync;",
104
104
  "// everything else acks as a no-op), so what's left here is what a refresh",
105
105
  "// MEANS for this project.",
106
106
  "//",
107
- "// Throwing marks the message for retry. That's usually right — a failed",
108
- "// refresh means the site is serving stale data — and Cloudflare routes it to",
107
+ "// Throwing marks the message for retry. That's usually right—a failed",
108
+ "// refresh means the site is serving stale data—and Cloudflare routes it to",
109
109
  "// the DLQ once it exceeds max_retries (wrangler.jsonc).",
110
110
  'import { astroidQueueHandler, type AstroidQueueMessage } from "astroidjs";',
111
111
  "",
@@ -118,7 +118,7 @@ export function generateAstroidQueueSeam(config) {
118
118
  ? [
119
119
  ` // This project runs more than one commerce provider, and the catalog is`,
120
120
  ` // ${provider}'s. Scoping the refresh keeps the OTHER provider's webhooks from`,
121
- ` // triggering it — Square alone emits an inventory event on every sale, which`,
121
+ ` // triggering it—Square alone emits an inventory event on every sale, which`,
122
122
  ` // unscoped would re-sync ${provider} once per transaction.`,
123
123
  ` catalogProvider: ${JSON.stringify(provider)},`,
124
124
  ]
@@ -143,7 +143,7 @@ export function generateAstroidQueueSeam(config) {
143
143
  " // takes `retry: { attempts: 3 }`, backing off on 429/5xx inside every",
144
144
  " // verb. It is off by default because a checkout route has a customer",
145
145
  " // watching a spinner, and there a fast failure beats a slow one. Here",
146
- " // the opposite holds — a catalog push that gives up halfway is worse.",
146
+ " // the opposite holds—a catalog push that gives up halfway is worse.",
147
147
  ]
148
148
  : [
149
149
  " // Nobody is watching this run, so ask for backoff on 429/5xx wherever",
@@ -158,7 +158,7 @@ export function generateAstroidQueueSeam(config) {
158
158
  " // letting that escape is correct: an uncaught throw here marks the",
159
159
  " // message for retry. Swallow it and the queue acks, the cron acks too,",
160
160
  " // and the site serves a frozen catalog with nothing in `wrangler tail`.",
161
- " // Partial failures don't throw — that's what `r.failed` above is for.",
161
+ " // Partial failures don't throw—that's what `r.failed` above is for.",
162
162
  " void env;",
163
163
  " },",
164
164
  " })(message);",
@@ -169,14 +169,14 @@ export function generateAstroidQueueSeam(config) {
169
169
  .join("\n");
170
170
  }
171
171
  /**
172
- * The provider webhook receiver — `src/pages/api/webhooks/<provider>.ts`.
172
+ * The provider webhook receiver—`src/pages/api/webhooks/<provider>.ts`.
173
173
  *
174
174
  * Thin on purpose: `handleWebhook` owns the ordering (verify the raw body before
175
175
  * parsing) and the status-code contract (which codes ask the provider to retry
176
176
  * and which tell it to stop). What's here is the provider's own header and
177
177
  * verifier, plus the secret read.
178
178
  *
179
- * Returns null when the project has no commerce provider — nothing to receive.
179
+ * Returns null when the project has no commerce provider—nothing to receive.
180
180
  */
181
181
  export function generateAstroidWebhookRoute(config, forProvider) {
182
182
  const provider = forProvider ?? astroidCommerceProviders(config.commerce)[0];
@@ -194,7 +194,7 @@ export function generateAstroidWebhookRoute(config, forProvider) {
194
194
  `// ${p.note}`,
195
195
  "//",
196
196
  "// Unprovisioned (the secret is absent or still the placeholder) answers 503,",
197
- "// which keeps the provider retrying — so events delivered before you set the",
197
+ "// which keeps the provider retrying—so events delivered before you set the",
198
198
  "// secret land afterwards instead of being lost.",
199
199
  'import type { APIRoute } from "astro";',
200
200
  'import { handleWebhook, readModuleSecret } from "astroidjs";',
@@ -222,7 +222,7 @@ export function generateAstroidWebhookRoute(config, forProvider) {
222
222
  *
223
223
  * Plural because roles are: a site running Stripe for invoicing beside
224
224
  * Fourthwall for the storefront receives from both, each with its own signing
225
- * secret and header. One route per provider, not per role — a provider filling
225
+ * secret and header. One route per provider, not per role—a provider filling
226
226
  * two roles still has one endpoint and one secret.
227
227
  */
228
228
  export function generateAstroidWebhookRoutes(config) {
@@ -1,6 +1,6 @@
1
1
  import type { AstroidQueueMessage } from "./messages.js";
2
2
  /**
3
- * The queue producer surface used here — structural, so a real `Queue<T>`
3
+ * The queue producer surface used here—structural, so a real `Queue<T>`
4
4
  * binding satisfies it without astroid depending on the Workers types.
5
5
  *
6
6
  * `Promise<unknown>` rather than `Promise<void>`: Cloudflare's `Queue.send`
@@ -15,18 +15,18 @@ export interface WebhookVerifyInput {
15
15
  raw: string;
16
16
  headers: Headers;
17
17
  url: URL;
18
- /** The signing secret — already checked to be real by the caller. */
18
+ /** The signing secret—already checked to be real by the caller. */
19
19
  secret: string;
20
20
  }
21
21
  export interface WebhookRouteOptions {
22
- /** Which integration this endpoint serves — carried into the message. */
22
+ /** Which integration this endpoint serves—carried into the message. */
23
23
  provider: string;
24
24
  /**
25
25
  * The signing secret, or `null` when unprovisioned. Read it with
26
26
  * `readModuleSecret` so a placeholder counts as absent.
27
27
  */
28
28
  secret: string | null;
29
- /** Signature check over the raw body — e.g. `verifySquareSignature`. */
29
+ /** Signature check over the raw body—for example, `verifySquareSignature`. */
30
30
  verify: (input: WebhookVerifyInput) => boolean | Promise<boolean>;
31
31
  /** The queue binding, or null/undefined when Queues aren't provisioned. */
32
32
  queue?: QueueProducer | null;
@@ -38,7 +38,7 @@ export interface WebhookRouteOptions {
38
38
  eventType?: (payload: unknown) => string;
39
39
  /**
40
40
  * Decide whether an event is worth queueing at all. Returning false acks the
41
- * delivery without enqueuing — the provider is satisfied and the consumer
41
+ * delivery without enqueuing—the provider is satisfied and the consumer
42
42
  * isn't woken for an event nothing acts on.
43
43
  */
44
44
  accept?: (type: string, payload: unknown) => boolean;
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // All three sites wrote this route the same way, and the ordering is the part
6
6
  // worth encoding. **Verify the HMAC over the raw body before parsing anything.**
7
- // Not for style — parsing first means an unauthenticated caller can reach the
7
+ // Not for style—parsing first means an unauthenticated caller can reach the
8
8
  // JSON parser and everything downstream of it, and re-serializing a parsed body
9
9
  // to check the signature is how signature checks quietly stop checking anything.
10
10
  // So the raw text is read once, verified, and only then parsed.
@@ -58,7 +58,7 @@ export async function handleWebhook(request, url, options) {
58
58
  payload = JSON.parse(raw);
59
59
  }
60
60
  catch {
61
- // Also terminal — a body that isn't JSON now won't become JSON later.
61
+ // Also terminal—a body that isn't JSON now won't become JSON later.
62
62
  return text("Invalid JSON", 400);
63
63
  }
64
64
  const type = (options.eventType ?? defaultEventType)(payload);
@@ -76,6 +76,6 @@ export async function handleWebhook(request, url, options) {
76
76
  return text("Queue unavailable", 503);
77
77
  }
78
78
  // 202, not 200: the work hasn't happened yet, it's been accepted. That's the
79
- // entire point of enqueuing — the response returns before the consumer runs.
79
+ // entire point of enqueuing—the response returns before the consumer runs.
80
80
  return text("Accepted", 202);
81
81
  }
@@ -10,12 +10,12 @@ export declare const ASTROID_REALTIME_MIGRATION_TAG = "v1";
10
10
  /** Is the realtime module switched on for this project? */
11
11
  export declare function usesRealtime(config: AstroidConfig): boolean;
12
12
  /**
13
- * `src/edit-session.ts` — the site-owned Durable Object subclass.
13
+ * `src/edit-session.ts`—the site-owned Durable Object subclass.
14
14
  *
15
15
  * Scaffold-once: `persist` is where a project decides what a flush means, and
16
16
  * the lock/field sets are tuning. What Astroid fixes is the delegation shape,
17
17
  * because getting it wrong fails in ways that look like anything but a bug in
18
- * this file — a missing `webSocketClose` leaks presence forever, a non-lazy
18
+ * this file—a missing `webSocketClose` leaks presence forever, a non-lazy
19
19
  * session breaks after the first hibernation wake.
20
20
  *
21
21
  * Returns null when the project has no realtime module.
@@ -23,8 +23,8 @@ export declare function usesRealtime(config: AstroidConfig): boolean;
23
23
  export declare function generateAstroidEditSession(config: AstroidConfig): string | null;
24
24
  /**
25
25
  * The `CloudflareEnv` member the realtime module adds, as a block
26
- * `create-astroid` substitutes into `src/env.d.ts`. Empty without the module —
27
- * a project that types a binding its wrangler.jsonc never creates is making a
26
+ * `create-astroid` substitutes into `src/env.d.ts`. Empty without the module—a
27
+ * project that types a binding its wrangler.jsonc never creates is making a
28
28
  * promise it doesn't keep.
29
29
  */
30
30
  export declare function generateAstroidRealtimeEnv(config: AstroidConfig): string;
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // The package description has claimed "multi-editor sites" since 0.1.0, and this
7
7
  // is the half that makes it true for two people on the SAME page. (The other
8
- // axis — multi-EDITOR, i.e. an org of accounts — was always real.) Without it
8
+ // axis—multi-EDITOR, that is, an org of accounts—was always real.) Without it
9
9
  // two editors on one page clobber each other; the server-side draft merge
10
10
  // narrows the window but there is no live channel, no presence, and no signal
11
11
  // that someone else is in the same field.
@@ -13,15 +13,15 @@
13
13
  // What Astroid generates and what it deliberately does NOT:
14
14
  //
15
15
  // - The DO SUBCLASS is scaffold-once (`src/edit-session.ts`), because it must
16
- // import `cloudflare:workers` — a runtime-only specifier the toolkit can't
17
- // carry — and because its `persist` is the seam a project tunes. Louise
16
+ // import `cloudflare:workers`—a runtime-only specifier the toolkit can't
17
+ // carry—and because its `persist` is the seam a project tunes. Louise
18
18
  // ships the session LOGIC it delegates to; this is the boilerplate around it.
19
19
  // - The wrangler `durable_objects` binding + `migrations` block, which is the
20
20
  // part nobody gets right from memory: a DO class needs a migration tag, and
21
21
  // a SQLite-backed one needs `new_sqlite_classes` rather than `new_classes`.
22
22
  // - The `realtimeRoute` upgrade endpoint, in the generated worker.
23
23
  //
24
- // Persistence goes through `applySaveDraft` — the SAME path the fetch auto-save
24
+ // Persistence goes through `applySaveDraft`—the SAME path the fetch auto-save
25
25
  // uses. One write path, per the ADR: the DO is a new front end to it, not a
26
26
  // parallel store, so drafts, version history, publish, and read-your-writes all
27
27
  // stay intact.
@@ -38,12 +38,12 @@ export function usesRealtime(config) {
38
38
  return (config.modules ?? []).includes("realtime");
39
39
  }
40
40
  /**
41
- * `src/edit-session.ts` — the site-owned Durable Object subclass.
41
+ * `src/edit-session.ts`—the site-owned Durable Object subclass.
42
42
  *
43
43
  * Scaffold-once: `persist` is where a project decides what a flush means, and
44
44
  * the lock/field sets are tuning. What Astroid fixes is the delegation shape,
45
45
  * because getting it wrong fails in ways that look like anything but a bug in
46
- * this file — a missing `webSocketClose` leaks presence forever, a non-lazy
46
+ * this file—a missing `webSocketClose` leaks presence forever, a non-lazy
47
47
  * session breaks after the first hibernation wake.
48
48
  *
49
49
  * Returns null when the project has no realtime module.
@@ -54,7 +54,7 @@ export function generateAstroidEditSession(config) {
54
54
  return [
55
55
  "// The per-page live editing session Durable Object (ADR 0002 / #71).",
56
56
  "//",
57
- "// Scaffolded once and yours to edit — `persist` in particular. What should NOT",
57
+ "// Scaffolded once and yours to edit—`persist` in particular. What should NOT",
58
58
  "// change is the delegation: every handler forwards to the session object, and",
59
59
  "// the session is built LAZILY. A Durable Object is re-instantiated after a",
60
60
  "// hibernation wake, so a session captured in a field initializer would be",
@@ -91,7 +91,7 @@ export function generateAstroidEditSession(config) {
91
91
  " // Guard the collection: only `pages` is realtime, and a stray target",
92
92
  " // must never write into the wrong table.",
93
93
  ' if (target.slug !== "pages") return;',
94
- " // The SAME merge-over-pending-draft path the fetch auto-save uses —",
94
+ " // The SAME merge-over-pending-draft path the fetch auto-save uses—",
95
95
  " // one write path, so drafts/history/publish semantics are identical.",
96
96
  " //",
97
97
  " // No `bufferKv` here on purpose: the DO's alarm IS the coalescer for",
@@ -105,8 +105,8 @@ export function generateAstroidEditSession(config) {
105
105
  " snapshot,",
106
106
  " );",
107
107
  " // A THROW (D1 down) propagates so the alarm keeps the snapshot dirty",
108
- " // and retries. An `ok: false` is terminal — a deleted row, an invalid",
109
- " // draft — which a retry can't fix, so let the alarm clear it, but say",
108
+ " // and retries. An `ok: false` is terminal—a deleted row, an invalid",
109
+ " // draft—which a retry can't fix, so let the alarm clear it, but say",
110
110
  " // so rather than dropping it silently.",
111
111
  " if (!result.ok) {",
112
112
  " console.warn(",
@@ -143,8 +143,8 @@ export function generateAstroidEditSession(config) {
143
143
  }
144
144
  /**
145
145
  * The `CloudflareEnv` member the realtime module adds, as a block
146
- * `create-astroid` substitutes into `src/env.d.ts`. Empty without the module —
147
- * a project that types a binding its wrangler.jsonc never creates is making a
146
+ * `create-astroid` substitutes into `src/env.d.ts`. Empty without the module—a
147
+ * project that types a binding its wrangler.jsonc never creates is making a
148
148
  * promise it doesn't keep.
149
149
  */
150
150
  export function generateAstroidRealtimeEnv(config) {
@@ -2,18 +2,18 @@ import { type CollectionConfig, type ContentConfig } from "louise-toolkit/conten
2
2
  import type { AstroidConfig } from "../config.js";
3
3
  /**
4
4
  * Return a copy of a `pages` write payload with its `sections` rich-text fields
5
- * sanitized against the project media base — a no-op when the write carries no
5
+ * sanitized against the project media base—a no-op when the write carries no
6
6
  * `sections`. Pure; leaves every other field (and a partial PATCH's absent ones)
7
7
  * untouched.
8
8
  *
9
9
  * Exported because two write paths need it: the collection's `beforeChange` hook
10
- * below, AND the raw `pagesRoute` (which does not run collection hooks — see
10
+ * below, AND the raw `pagesRoute` (which does not run collection hooks—see
11
11
  * {@link astroidPagesWriteHooks}).
12
12
  */
13
13
  export declare function sanitizeAstroidPageSections(config: AstroidConfig, data: Record<string, unknown>): Record<string, unknown>;
14
14
  /**
15
15
  * Validate the (already-sanitized) `sections` of a `pages` write against the
16
- * catalog, throwing `LouiseValidationError` — an unknown `_type`, a field of the
16
+ * catalog, throwing `LouiseValidationError`—an unknown `_type`, a field of the
17
17
  * wrong shape, or a setting outside its declared options is rejected with a 422
18
18
  * carrying the per-field violations. A no-op when the write carries no
19
19
  * `sections`, so a partial PATCH of other fields isn't spuriously validated.
@@ -23,7 +23,7 @@ export declare function assertAstroidPageSections(config: AstroidConfig, data: R
23
23
  * The write-time hooks the raw `pagesRoute` (louise-toolkit/editor) needs to
24
24
  * enforce the same section contract as the draft path.
25
25
  *
26
- * `pagesRoute` writes straight to the table and — unlike `versionsRoute` — takes
26
+ * `pagesRoute` writes straight to the table and—unlike `versionsRoute`—takes
27
27
  * no collection config, so it never runs the `beforeChange` hook below. Left
28
28
  * bare (as it was), a direct `POST` / `PATCH /api/louise/pages/:id` persists an
29
29
  * unknown section `_type`, a setting outside its options, or unsanitized section
@@ -37,18 +37,49 @@ export declare function assertAstroidPageSections(config: AstroidConfig, data: R
37
37
  *
38
38
  * pagesRoute({ table: pages, resolveEditor, fields, ...astroidPagesWriteHooks(config) })
39
39
  */
40
- export declare function astroidPagesWriteHooks(config: AstroidConfig): {
40
+ /** The write context `pagesRoute` passes to a transform or validator. */
41
+ export interface AstroidPagesWriteContext {
42
+ operation: "create" | "update";
43
+ }
44
+ /**
45
+ * A site's own `pagesRoute` hooks, exported as `pagesHooks` from the
46
+ * scaffold-once `src/pages-hooks.ts` when `pages.hooks` is on.
47
+ */
48
+ export interface AstroidPagesHooks {
49
+ /**
50
+ * Clean a page write before Astroid's section sanitize and validate run, for
51
+ * example to normalize the slug, clamp a title, or fill a new page's defaults.
52
+ * It gets only the allowlisted fields of the write.
53
+ */
54
+ transform?: (data: Record<string, unknown>, ctx: AstroidPagesWriteContext) => Record<string, unknown> | Promise<Record<string, unknown>>;
55
+ /**
56
+ * Reject a write, after both transforms and before Astroid's own section
57
+ * validation. Throw a `LouiseValidationError` for a 422 with per-field
58
+ * violations, for example when a required field is empty.
59
+ */
60
+ validate?: (data: Record<string, unknown>, ctx: AstroidPagesWriteContext) => void | Promise<void>;
61
+ /** Slugs to refuse on top of {@link ASTROID_RESERVED_SLUGS}, such as a path
62
+ * a site's own file route serves. */
63
+ reservedSlugs?: Iterable<string>;
64
+ }
65
+ /**
66
+ * Slugs no page may take, on any site. Each one is a path Astro, Cloudflare,
67
+ * or Astroid serves before the catch-all page route, so a page saved under it
68
+ * would be unreachable, and nothing would say why. `pagesRoute` refuses them
69
+ * with a 422 instead.
70
+ */
71
+ export declare const ASTROID_RESERVED_SLUGS: readonly string[];
72
+ export declare function astroidPagesWriteHooks(config: AstroidConfig, site?: AstroidPagesHooks): {
41
73
  sanitize: (html: string) => string;
42
- transform: (data: Record<string, unknown>) => Record<string, unknown>;
43
- validate: (data: Record<string, unknown>, ctx: {
44
- operation: "create" | "update";
45
- }) => Promise<void>;
74
+ transform: (data: Record<string, unknown>, ctx: AstroidPagesWriteContext) => Promise<Record<string, unknown>>;
75
+ validate: (data: Record<string, unknown>, ctx: AstroidPagesWriteContext) => Promise<void>;
76
+ reservedSlugs: string[];
46
77
  };
47
78
  export declare function astroidPagesCollection(config: AstroidConfig): CollectionConfig;
48
79
  /**
49
80
  * The Louise `ContentConfig` for an Astroid project. Today: the `pages`
50
- * collection. Archetype- and module-specific collections (e.g. a portfolio
51
- * `gallery`) layer in here as they land — this is the single place that maps
81
+ * collection. Archetype- and module-specific collections (for example, a portfolio
82
+ * `gallery`) layer in here as they land—this is the single place that maps
52
83
  * brand config down to Louise content.
53
84
  */
54
85
  export declare function astroidContentConfig(config: AstroidConfig): ContentConfig;