astroidjs 0.8.1 → 0.9.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 CHANGED
@@ -61,10 +61,21 @@ export default defineAstroid({
61
61
  theme: { name: "Meg Bowen Studio", colors: { brand: "#2b2b2b" } },
62
62
  sections: ["hero", "gallery", "aboutIntro", "contact"],
63
63
  portal: { enabled: true },
64
+ // The masters ARE the product here: 40 MB camera files upload once and only
65
+ // resized derivatives are ever served, so the size costs storage, not page
66
+ // weight. Omit to keep the 10 MB default.
67
+ media: { maxUploadBytes: 40 * 1024 * 1024 },
64
68
  deploy: { platform: "cloudflare" },
65
69
  });
66
70
  ```
67
71
 
72
+ `media.maxUploadBytes` is checked at generate time, not in production: a value
73
+ above Cloudflare's **100 MB request-body limit** is rejected outright, because
74
+ the edge drops an oversized body before the Worker runs — the media route never
75
+ gets to answer with its own `413`, so the editor would see an opaque failure.
76
+ Zero, negatives, and non-integers are rejected too (that last one catches the
77
+ megabytes-not-bytes slip).
78
+
68
79
  ## Commerce
69
80
 
70
81
  **Providers fill roles.** Not "the commerce provider" — the toolkit's clients
package/dist/config.d.ts CHANGED
@@ -407,6 +407,23 @@ export interface SettingsConfig {
407
407
  * settings values validated as media-library URLs on write. */
408
408
  imageKeys?: string[];
409
409
  }
410
+ /** Media-library upload policy. */
411
+ export interface MediaConfig {
412
+ /**
413
+ * Largest accepted upload, in bytes. Default 10 MB (louise-toolkit's
414
+ * `DEFAULT_MAX_BYTES`).
415
+ *
416
+ * Raise it when the masters ARE the product — a photographer's or painter's
417
+ * portfolio uploads 40 MB camera files and only ever serves Cloudflare-
418
+ * resized derivatives, so the master's size costs storage, not page weight.
419
+ *
420
+ * Bounded by the platform, not by this setting: a Cloudflare Worker rejects
421
+ * a request body over 100 MB before any handler runs, so a value above that
422
+ * would fail at the edge with no error you can catch. Validated at generate
423
+ * time rather than surfacing as a mystery upload failure in production.
424
+ */
425
+ maxUploadBytes?: number;
426
+ }
410
427
  export interface DeployConfig {
411
428
  platform: "cloudflare";
412
429
  /** Media base for R2 + `cf-image` resizing — matches Louise's media route
@@ -483,6 +500,8 @@ export interface AstroidConfig {
483
500
  /** Site-specific editable settings — extra `custom` keys + image keys on top
484
501
  * of Astroid's base `site_settings` columns. */
485
502
  settings?: SettingsConfig;
503
+ /** Media-library upload policy (e.g. a larger `maxUploadBytes`). */
504
+ media?: MediaConfig;
486
505
  /**
487
506
  * Force the contact form + `inquiries` table on or off. Omit to detect from
488
507
  * the config (a `contact` section, or a wholesale-inquiry module). Set `true`
package/dist/config.js CHANGED
@@ -160,6 +160,23 @@ function assertTenancy(config) {
160
160
  }
161
161
  }
162
162
  }
163
+ /** Cloudflare rejects a request body over 100 MB at the edge, before any Worker
164
+ * handler runs — so an upload limit above it can never be honoured, and the
165
+ * failure arrives as an opaque edge error rather than the route's own 413. */
166
+ const WORKERS_MAX_REQUEST_BODY_BYTES = 100 * 1024 * 1024;
167
+ function assertMediaConfig(media) {
168
+ const max = media?.maxUploadBytes;
169
+ if (max === undefined)
170
+ return;
171
+ if (!Number.isInteger(max) || max <= 0) {
172
+ throw new AstroidConfigError(`\`media.maxUploadBytes\` must be a positive integer number of BYTES; got ${max}`);
173
+ }
174
+ if (max > WORKERS_MAX_REQUEST_BODY_BYTES) {
175
+ throw new AstroidConfigError(`\`media.maxUploadBytes\` (${max}) exceeds Cloudflare's 100 MB request-body limit. ` +
176
+ "A body that large is rejected at the edge before the Worker runs, so the upload " +
177
+ "would fail with an error the media route never sees.");
178
+ }
179
+ }
163
180
  export function defineAstroid(config) {
164
181
  if (!config.key || config.key.trim().length === 0) {
165
182
  throw new AstroidConfigError("Astroid config requires a non-empty `key` (it names the generated worker/D1/R2 bindings)");
@@ -174,6 +191,9 @@ export function defineAstroid(config) {
174
191
  // Fourthwall, a storefront over Stripe) fails here rather than at runtime on
175
192
  // the first invoice, as a missing function.
176
193
  assertCommerceRoles(config.commerce);
194
+ // A media limit above the platform's own body cap is unhonourable — reject it
195
+ // here rather than let an editor watch a 120 MB upload die at the edge.
196
+ assertMediaConfig(config.media);
177
197
  // A portal is a SECOND Better Auth instance beside the editor's. Reject any
178
198
  // isolation that would collide with the editor on the same origin (a shared
179
199
  // cookie prefix silently cross-signs-out; a shared table prefix merges the two
@@ -123,11 +123,18 @@ export function generateAstroidWorker(config) {
123
123
  return "aiRoute({ resolveEditor, ai: aiRunner })";
124
124
  case "seoFix":
125
125
  return "seoFixRoute({ table: pages, resolveEditor, ai: aiRunner })";
126
- case "media":
126
+ case "media": {
127
127
  // `altText` fills a new upload's alt from the image itself. Best-effort
128
128
  // by contract — a model error or a missing binding never fails the
129
129
  // upload — so it costs nothing on a project that doesn't want it.
130
- return "mediaRoute({ table: media, resolveEditor, referenceSources: MEDIA_REFERENCE_SOURCES, altText: aiRunner })";
130
+ //
131
+ // `maxBytes` is emitted only when the site raised it: omitted, the
132
+ // route keeps louise-toolkit's DEFAULT_MAX_BYTES, so the generated
133
+ // line stays identical for every project that doesn't care.
134
+ const maxBytes = config.media?.maxUploadBytes;
135
+ const maxArg = maxBytes ? `, maxBytes: ${maxBytes}` : "";
136
+ return `mediaRoute({ table: media, resolveEditor, referenceSources: MEDIA_REFERENCE_SOURCES, altText: aiRunner${maxArg} })`;
137
+ }
131
138
  case "editors":
132
139
  // The editor instance's user table is `louise_`-prefixed (the editor
133
140
  // convention — the unprefixed `user` table is left for a second/portal
@@ -606,11 +613,14 @@ export function generateAstroidMiddleware(config) {
606
613
  " // map, checked before the tenant — an app exists whether or not any",
607
614
  " // tenant does, and needs no lookup.",
608
615
  " const app = appPrefix(context.url.hostname, TENANCY);",
609
- " if (app) return `${app}${context.url.pathname}`;",
616
+ " // `search` is preserved: the rewrite chooses which page renders, not",
617
+ " // what was asked of it. Dropping it silently loses filters, pagination,",
618
+ " // campaign tags — and every typed search param a routed island reads.",
619
+ " if (app) return `${app}${context.url.pathname}${context.url.search}`;",
610
620
  ]
611
621
  : []),
612
622
  " const tenant = context.locals.tenant;",
613
- ` return tenant ? \`${rewritePrefix}/\${tenant.slug}\${context.url.pathname}\` : undefined;`,
623
+ ` return tenant ? \`${rewritePrefix}/\${tenant.slug}\${context.url.pathname}\${context.url.search}\` : undefined;`,
614
624
  " },",
615
625
  ]
616
626
  : []),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astroidjs",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Astroid — an opinionated meta-framework over Louise Toolkit and Astro for building editable, multi-editor sites on Cloudflare Workers.",
5
5
  "keywords": [
6
6
  "astro",