astroidjs 0.8.0 → 0.8.2

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/dist/config.d.ts CHANGED
@@ -305,6 +305,27 @@ export interface TenancyConfig {
305
305
  * never affected — they aren't tenant candidates at all.
306
306
  */
307
307
  unknown?: "fallthrough" | "404";
308
+ /**
309
+ * Path prefixes the host rewrite must leave alone. Default `["/api"]`.
310
+ *
311
+ * The rewrite exists to choose which PAGE renders for a host. An API route
312
+ * is not a page: its address is absolute, chosen by the client that calls
313
+ * it, and it reads the host from `locals.tenant` rather than from its own
314
+ * path. Rewriting it moves it somewhere no route matches — and on an app
315
+ * host with a catch-all page, somewhere much worse than a 404: the page
316
+ * catch-all answers, so `fetch("/api/…")` gets HTML (or a redirect to a
317
+ * sign-in) instead of JSON, and every data load on that host silently fails
318
+ * while the same code works on the apex.
319
+ *
320
+ * That is not hypothetical — it is why this default exists (found on
321
+ * themidwestartist.com's studio, where the whole admin app loaded and then
322
+ * fetched nothing).
323
+ *
324
+ * Set `[]` to rewrite everything, or add prefixes for other host-agnostic
325
+ * surfaces (`/_actions`, `/webhooks`). Matching is prefix-based on a path
326
+ * segment, so `/api` matches `/api/x` and `/api` but never `/apiary`.
327
+ */
328
+ rewriteExclude?: string[];
308
329
  }
309
330
  export interface AstroidCron {
310
331
  /** Standard 5-field cron, UTC — e.g. `"*/15 * * * *"`. */
@@ -1,6 +1,19 @@
1
1
  import type { AstroidConfig, TenancyConfig } from "../config.js";
2
2
  /** Default internal prefix a tenant request is rewritten to. */
3
3
  export declare const ASTROID_TENANT_PREFIX = "/t";
4
+ /** Path prefixes the host rewrite leaves alone unless a site says otherwise.
5
+ * See {@link TenancyConfig.rewriteExclude} for why `/api` is the default. */
6
+ export declare const ASTROID_REWRITE_EXCLUDE: string[];
7
+ /**
8
+ * Whether a path is host-agnostic and must render from its own address rather
9
+ * than the rewritten one.
10
+ *
11
+ * Segment-aware on purpose: a prefix of `/api` must match `/api` and `/api/x`
12
+ * but never `/apiary`, or a site could lose a page to a coincidental name.
13
+ *
14
+ * Exported and pure so a site can unit-test its own exclusion list.
15
+ */
16
+ export declare function isRewriteExcluded(pathname: string, exclude: string[]): boolean;
4
17
  /**
5
18
  * The Cloudflare zone a wildcard pattern belongs to.
6
19
  *
@@ -10,6 +10,21 @@
10
10
  // `src/tenancy.ts`.
11
11
  /** Default internal prefix a tenant request is rewritten to. */
12
12
  export const ASTROID_TENANT_PREFIX = "/t";
13
+ /** Path prefixes the host rewrite leaves alone unless a site says otherwise.
14
+ * See {@link TenancyConfig.rewriteExclude} for why `/api` is the default. */
15
+ export const ASTROID_REWRITE_EXCLUDE = ["/api"];
16
+ /**
17
+ * Whether a path is host-agnostic and must render from its own address rather
18
+ * than the rewritten one.
19
+ *
20
+ * Segment-aware on purpose: a prefix of `/api` must match `/api` and `/api/x`
21
+ * but never `/apiary`, or a site could lose a page to a coincidental name.
22
+ *
23
+ * Exported and pure so a site can unit-test its own exclusion list.
24
+ */
25
+ export function isRewriteExcluded(pathname, exclude) {
26
+ return exclude.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix.replace(/\/$/, "")}/`));
27
+ }
13
28
  /**
14
29
  * The Cloudflare zone a wildcard pattern belongs to.
15
30
  *
@@ -17,7 +17,7 @@ import { astroidPortal } from "../portal/config.js";
17
17
  import { ASTROID_HEALTH_CRON, astroidCron, astroidUsesQueues } from "../queues/messages.js";
18
18
  import { ASTROID_EDIT_SESSION_CLASS, ASTROID_REALTIME_BINDING, usesRealtime, } from "../realtime/scaffold.js";
19
19
  import { capturesInquiries } from "../schema/framework.js";
20
- import { ASTROID_TENANT_PREFIX } from "../tenancy/index.js";
20
+ import { ASTROID_REWRITE_EXCLUDE, ASTROID_TENANT_PREFIX } from "../tenancy/index.js";
21
21
  import { astroidEditorRoutePlan } from "./routes.js";
22
22
  // Astroid's default editable site_settings surface — the columns the Settings
23
23
  // panel may write, and which of them hold a media-library image URL.
@@ -462,6 +462,7 @@ export function generateAstroidMiddleware(config) {
462
462
  const portal = astroidPortal(config);
463
463
  const tenancy = config.tenancy;
464
464
  const rewritePrefix = tenancy?.rewritePrefix ?? ASTROID_TENANT_PREFIX;
465
+ const rewriteExclude = tenancy?.rewriteExclude ?? ASTROID_REWRITE_EXCLUDE;
465
466
  return [
466
467
  "// Generated by astroidjs — do not hand-edit.",
467
468
  "// The shared Louise middleware: rate-limit the unauthenticated POST surfaces,",
@@ -479,7 +480,7 @@ export function generateAstroidMiddleware(config) {
479
480
  ...(portal ? ["astroidPortalGuardConfig"] : []),
480
481
  "astroidRateRules",
481
482
  ...(portal ? ["guardResponse", "portalGuard", "resolvePortalSession"] : []),
482
- ...(tenancy ? ["tenantLabel"] : []),
483
+ ...(tenancy ? ["isRewriteExcluded", "tenantLabel"] : []),
483
484
  ].join(", ")} } from "astroidjs";`,
484
485
  'import astroidConfig from "../astroid.config.js";',
485
486
  ...(tenancy
@@ -591,17 +592,28 @@ export function generateAstroidMiddleware(config) {
591
592
  " // falls through to the ordinary site below — or 404s first, when the",
592
593
  ' // config sets `tenancy.unknown: "404"` (see the guard above).',
593
594
  " rewrite: (context) => {",
595
+ " // Host-agnostic paths render from their own address. An API route is",
596
+ " // addressed absolutely by whatever calls it and reads the host from",
597
+ " // `locals.tenant`, so rewriting it moves it where no route matches —",
598
+ " // and on an app host with a catch-all page, the PAGE answers, so",
599
+ " // fetch() gets HTML instead of JSON and every data load quietly fails.",
600
+ ` if (isRewriteExcluded(context.url.pathname, ${JSON.stringify(rewriteExclude)})) {`,
601
+ " return undefined;",
602
+ " }",
594
603
  ...(Object.keys(tenancy.apps ?? {}).length
595
604
  ? [
596
605
  " // First-party app hosts (config `tenancy.apps`): a static label→prefix",
597
606
  " // map, checked before the tenant — an app exists whether or not any",
598
607
  " // tenant does, and needs no lookup.",
599
608
  " const app = appPrefix(context.url.hostname, TENANCY);",
600
- " if (app) return `${app}${context.url.pathname}`;",
609
+ " // `search` is preserved: the rewrite chooses which page renders, not",
610
+ " // what was asked of it. Dropping it silently loses filters, pagination,",
611
+ " // campaign tags — and every typed search param a routed island reads.",
612
+ " if (app) return `${app}${context.url.pathname}${context.url.search}`;",
601
613
  ]
602
614
  : []),
603
615
  " const tenant = context.locals.tenant;",
604
- ` return tenant ? \`${rewritePrefix}/\${tenant.slug}\${context.url.pathname}\` : undefined;`,
616
+ ` return tenant ? \`${rewritePrefix}/\${tenant.slug}\${context.url.pathname}\${context.url.search}\` : undefined;`,
605
617
  " },",
606
618
  ]
607
619
  : []),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "astroidjs",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
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",