@c9up/aurora 0.1.19 → 0.1.21

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.
@@ -33,16 +33,29 @@ export interface AuroraManagerConfig {
33
33
  * URL prefix the asset routes mount under. The aurora runtime is served
34
34
  * from `<assetsPrefix>/aurora/*` and the app's pages from
35
35
  * `<assetsPrefix>/pages/*`, and the SSR importmap + page URLs derive from
36
- * it. Default `/_assets` (the leading underscore namespaces framework
36
+ * it. Default `/__assets` (the leading underscore namespaces framework
37
37
  * assets away from app routes, Next.js `/_next` style). Set e.g. `/assets`
38
38
  * for an underscore-free scheme. An explicit `pages.urlPrefix` still wins.
39
39
  */
40
40
  assetsPrefix?: string;
41
+ /**
42
+ * App-level importmap overrides, configured ONCE here (the AdonisJS model:
43
+ * config lives in `config/aurora.ts`, controllers stay thin and never hand-write
44
+ * importmaps). Merged over aurora's auto defaults (`@c9up/aurora`,
45
+ * `@c9up/aurora/rpc`, `@c9up/comet`) on every `render()`. Use to point a bare
46
+ * specifier at a curated browser entry, e.g.
47
+ * `{ '@c9up/aurora': '/__assets/pages/browser/aurora.js' }`. A per-call
48
+ * `render(..., { importmap })` still wins over this.
49
+ *
50
+ * (Importmaps are aurora's no-bundler particularity — AdonisJS bundles via Vite
51
+ * and has no importmap; we keep the config-driven shape to stay Adonis-idiomatic.)
52
+ */
53
+ importmap?: Record<string, string>;
41
54
  }
42
55
  export declare class AuroraManager {
43
56
  readonly pages: Pages;
44
57
  readonly auroraDistRoot: string;
45
- /** Resolved asset prefix (default `/_assets`). */
58
+ /** Resolved asset prefix (default `/__assets`). */
46
59
  readonly assetsPrefix: string;
47
60
  /** Mount path for the aurora runtime — `<assetsPrefix>/aurora`. */
48
61
  readonly auroraAssetPath: string;
@@ -52,22 +65,25 @@ export declare class AuroraManager {
52
65
  readonly cometAssetPath: string;
53
66
  /** Resolved `@c9up/comet` dist dir, or `null` when comet isn't installed. */
54
67
  readonly cometDistRoot: string | null;
68
+ /** App-level importmap overrides from `config/aurora.ts`, merged on render. */
69
+ readonly importmap: Record<string, string>;
55
70
  constructor(config: AuroraManagerConfig);
56
71
  /**
57
- * SSR + hydrate + ship the document. The importmap default points
58
- * `@c9up/aurora` at this manager's `assetsPrefix`; a caller's
59
- * `options.importmap` still overrides (e.g. to remap to an app-curated
60
- * browser entry).
72
+ * SSR + hydrate + ship the document. The importmap layers, last wins:
73
+ * aurora's auto defaults (`@c9up/aurora`, `@c9up/aurora/rpc`, `@c9up/comet`)
74
+ * < the config-level `importmap` (from `config/aurora.ts`) < a per-call
75
+ * `options.importmap`. So a thin controller calls `render(ctx, name, props)`
76
+ * with no importmap, and any curation lives once in config.
61
77
  */
62
78
  render(ctx: RenderHttpContext, name: string, props: unknown, options?: RenderPageOptions): Promise<void>;
63
79
  /**
64
80
  * Handler for aurora's pre-built ESM runtime. Mount on
65
- * `GET /_assets/aurora/*`.
81
+ * `GET /__assets/aurora/*`.
66
82
  */
67
83
  auroraAssetsHandler(): (ctx: AssetsHttpContext) => Promise<void>;
68
84
  /**
69
85
  * Handler for the app's pages directory. Mount on
70
- * `GET /_assets/pages/*`.
86
+ * `GET /__assets/pages/*`.
71
87
  */
72
88
  pageAssetsHandler(): (ctx: AssetsHttpContext) => Promise<void>;
73
89
  /**
@@ -38,7 +38,7 @@ function normalizePrefix(prefix) {
38
38
  export class AuroraManager {
39
39
  pages;
40
40
  auroraDistRoot;
41
- /** Resolved asset prefix (default `/_assets`). */
41
+ /** Resolved asset prefix (default `/__assets`). */
42
42
  assetsPrefix;
43
43
  /** Mount path for the aurora runtime — `<assetsPrefix>/aurora`. */
44
44
  auroraAssetPath;
@@ -48,12 +48,15 @@ export class AuroraManager {
48
48
  cometAssetPath;
49
49
  /** Resolved `@c9up/comet` dist dir, or `null` when comet isn't installed. */
50
50
  cometDistRoot;
51
+ /** App-level importmap overrides from `config/aurora.ts`, merged on render. */
52
+ importmap;
51
53
  constructor(config) {
52
- this.assetsPrefix = normalizePrefix(config.assetsPrefix ?? "/_assets");
54
+ this.assetsPrefix = normalizePrefix(config.assetsPrefix ?? "/__assets");
53
55
  this.auroraAssetPath = `${this.assetsPrefix}/aurora`;
54
56
  this.pageAssetPath = `${this.assetsPrefix}/pages`;
55
57
  this.cometAssetPath = `${this.assetsPrefix}/comet`;
56
58
  this.cometDistRoot = config.cometDistRoot ?? resolveCometDist();
59
+ this.importmap = config.importmap ?? {};
57
60
  // Pages serve their compiled JS from the same prefix unless the app
58
61
  // pins an explicit urlPrefix.
59
62
  this.pages = new Pages({
@@ -63,10 +66,11 @@ export class AuroraManager {
63
66
  this.auroraDistRoot = config.auroraDistRoot ?? DEFAULT_AURORA_DIST;
64
67
  }
65
68
  /**
66
- * SSR + hydrate + ship the document. The importmap default points
67
- * `@c9up/aurora` at this manager's `assetsPrefix`; a caller's
68
- * `options.importmap` still overrides (e.g. to remap to an app-curated
69
- * browser entry).
69
+ * SSR + hydrate + ship the document. The importmap layers, last wins:
70
+ * aurora's auto defaults (`@c9up/aurora`, `@c9up/aurora/rpc`, `@c9up/comet`)
71
+ * < the config-level `importmap` (from `config/aurora.ts`) < a per-call
72
+ * `options.importmap`. So a thin controller calls `render(ctx, name, props)`
73
+ * with no importmap, and any curation lives once in config.
70
74
  */
71
75
  render(ctx, name, props, options) {
72
76
  return renderPage(ctx, this.pages, name, props, {
@@ -84,20 +88,23 @@ export class AuroraManager {
84
88
  ...(this.cometDistRoot
85
89
  ? { "@c9up/comet": `${this.cometAssetPath}/index.js` }
86
90
  : {}),
91
+ // Config-level overrides (config/aurora.ts) — Adonis-style config-driven,
92
+ // so controllers never hand-write an importmap. A per-call override wins.
93
+ ...this.importmap,
87
94
  ...options?.importmap,
88
95
  },
89
96
  });
90
97
  }
91
98
  /**
92
99
  * Handler for aurora's pre-built ESM runtime. Mount on
93
- * `GET /_assets/aurora/*`.
100
+ * `GET /__assets/aurora/*`.
94
101
  */
95
102
  auroraAssetsHandler() {
96
103
  return serveAssets({ root: this.auroraDistRoot });
97
104
  }
98
105
  /**
99
106
  * Handler for the app's pages directory. Mount on
100
- * `GET /_assets/pages/*`.
107
+ * `GET /__assets/pages/*`.
101
108
  */
102
109
  pageAssetsHandler() {
103
110
  return serveAssets({ root: this.pages.root });
@@ -2,8 +2,8 @@
2
2
  * AuroraProvider — registers the AuroraManager singleton and auto-mounts
3
3
  * the two asset routes the browser needs:
4
4
  *
5
- * GET /_assets/aurora/* → packages/@c9up/aurora/dist/*
6
- * GET /_assets/pages/* → resources/pages/*
5
+ * GET /__assets/aurora/* → packages/@c9up/aurora/dist/*
6
+ * GET /__assets/pages/* → resources/pages/*
7
7
  *
8
8
  * Config (in `config/aurora.ts`):
9
9
  *
@@ -2,8 +2,8 @@
2
2
  * AuroraProvider — registers the AuroraManager singleton and auto-mounts
3
3
  * the two asset routes the browser needs:
4
4
  *
5
- * GET /_assets/aurora/* → packages/@c9up/aurora/dist/*
6
- * GET /_assets/pages/* → resources/pages/*
5
+ * GET /__assets/aurora/* → packages/@c9up/aurora/dist/*
6
+ * GET /__assets/pages/* → resources/pages/*
7
7
  *
8
8
  * Config (in `config/aurora.ts`):
9
9
  *
@@ -64,7 +64,7 @@ export default class AuroraProvider {
64
64
  const router = this.app.container.resolve("router");
65
65
  const manager = this.app.container.resolve(AuroraManager);
66
66
  // Mount paths derive from the configured `assetsPrefix` (default
67
- // `/_assets`) — set `config.aurora.assetsPrefix` to change the scheme.
67
+ // `/__assets`) — set `config.aurora.assetsPrefix` to change the scheme.
68
68
  router.get(`${manager.auroraAssetPath}/*`, adaptHandler(manager.auroraAssetsHandler()));
69
69
  router.get(`${manager.pageAssetPath}/*`, adaptHandler(manager.pageAssetsHandler()));
70
70
  // Serve @c9up/comet's runtime so the RPC client's bare `import
package/dist/Pages.d.ts CHANGED
@@ -25,7 +25,7 @@ export interface PagesConfig {
25
25
  root: string;
26
26
  /**
27
27
  * URL prefix the browser uses to fetch a page's compiled JS.
28
- * Defaults to `/_assets/pages`. A name `"Foo"` maps to
28
+ * Defaults to `/__assets/pages`. A name `"Foo"` maps to
29
29
  * `${urlPrefix}/Foo.js`.
30
30
  */
31
31
  urlPrefix?: string;
package/dist/Pages.js CHANGED
@@ -29,7 +29,7 @@ export class Pages {
29
29
  registry = new Map();
30
30
  constructor(config) {
31
31
  this.root = config.root;
32
- this.urlPrefix = (config.urlPrefix ?? "/_assets/pages").replace(/\/$/, "");
32
+ this.urlPrefix = (config.urlPrefix ?? "/__assets/pages").replace(/\/$/, "");
33
33
  this.extension = config.extension ?? ".js";
34
34
  }
35
35
  /**
package/dist/live.d.ts CHANGED
@@ -53,3 +53,8 @@ export interface LiveSession {
53
53
  * every session then reads the same signal and patches on its change.
54
54
  */
55
55
  export declare function mountLiveSession(factory: () => LiveComponentDefinition): LiveSession;
56
+ export { connectPatches, type LiveStore, liveStore, type RelayBroadcaster, } from "./liveBroadcast.js";
57
+ export { buildLiveTransport, type LiveClientOptions, type LiveClientTransport, type LiveHttpPoster, liveClient, type RelaySubscribeClient, } from "./liveClient.js";
58
+ export { createLiveRegistry, type LiveRegistry, type LiveSessionHandle, } from "./liveRegistry.js";
59
+ export { createLiveRouter, type LiveMount, type LiveRouter, } from "./liveRouter.js";
60
+ export { DEFAULT_LIVE_EVENT_PATH, type LiveHttpContext, type LiveHttpRouter, type WireLiveEventsOptions, wireLiveEvents, } from "./liveServer.js";
package/dist/live.js CHANGED
@@ -94,3 +94,15 @@ export function mountLiveSession(factory) {
94
94
  },
95
95
  };
96
96
  }
97
+ // ─── `@c9up/aurora/live` barrel ──────────────────────────────────────
98
+ //
99
+ // The Live stack (broadcast / client / registry / router / server) is
100
+ // re-exported here so the whole niche surface lives behind ONE subpath,
101
+ // `@c9up/aurora/live` — keeping the main `.` barrel lean (it no longer
102
+ // eager-pulls these into every browser graph), consistent with how `./ssr`,
103
+ // `./relay`, `./rpc` and `./hydrate` are already subpath-gated.
104
+ export { connectPatches, liveStore, } from "./liveBroadcast.js";
105
+ export { buildLiveTransport, liveClient, } from "./liveClient.js";
106
+ export { createLiveRegistry, } from "./liveRegistry.js";
107
+ export { createLiveRouter, } from "./liveRouter.js";
108
+ export { DEFAULT_LIVE_EVENT_PATH, wireLiveEvents, } from "./liveServer.js";
@@ -61,7 +61,7 @@ export interface LiveHttpPoster {
61
61
  /**
62
62
  * Build a {@link LiveClientTransport} from a relay client (SSE down) + an HTTP
63
63
  * client (events up). `path` must match the server's `wireLiveEvents` route
64
- * (default `/_live/event`). Keeps `liveClient` itself transport-agnostic.
64
+ * (default `/__live/event`). Keeps `liveClient` itself transport-agnostic.
65
65
  *
66
66
  * @example
67
67
  * import { relay } from '@c9up/aurora/relay'
@@ -58,7 +58,7 @@ export function liveClient(opts) {
58
58
  /**
59
59
  * Build a {@link LiveClientTransport} from a relay client (SSE down) + an HTTP
60
60
  * client (events up). `path` must match the server's `wireLiveEvents` route
61
- * (default `/_live/event`). Keeps `liveClient` itself transport-agnostic.
61
+ * (default `/__live/event`). Keeps `liveClient` itself transport-agnostic.
62
62
  *
63
63
  * @example
64
64
  * import { relay } from '@c9up/aurora/relay'
@@ -67,7 +67,7 @@ export function liveClient(opts) {
67
67
  * liveClient({ container, factory, mount, transport })
68
68
  */
69
69
  export function buildLiveTransport(relayClient, http, options = {}) {
70
- const path = options.path ?? "/_live/event";
70
+ const path = options.path ?? "/__live/event";
71
71
  return {
72
72
  subscribe: (channel, handler) => relayClient.subscribe(channel, handler),
73
73
  post: (id, event, payload) => {
@@ -29,7 +29,7 @@ export interface WireLiveEventsOptions {
29
29
  path?: string;
30
30
  }
31
31
  /** Default inbound-event route — keep the client transport's `path` in sync. */
32
- export declare const DEFAULT_LIVE_EVENT_PATH = "/_live/event";
32
+ export declare const DEFAULT_LIVE_EVENT_PATH = "/__live/event";
33
33
  /**
34
34
  * Register the inbound live-event route on the host router. Call once at boot
35
35
  * (e.g. from a provider that resolved the router + relay from the container).
@@ -18,7 +18,7 @@ function isLiveEventBody(value) {
18
18
  return typeof value.id === "string" && typeof value.event === "string";
19
19
  }
20
20
  /** Default inbound-event route — keep the client transport's `path` in sync. */
21
- export const DEFAULT_LIVE_EVENT_PATH = "/_live/event";
21
+ export const DEFAULT_LIVE_EVENT_PATH = "/__live/event";
22
22
  /**
23
23
  * Register the inbound live-event route on the host router. Call once at boot
24
24
  * (e.g. from a provider that resolved the router + relay from the container).
@@ -0,0 +1,32 @@
1
+ /**
2
+ * `auroraContext` — binds `ctx.aurora.render(name, props)` onto the request
3
+ * context, the AdonisJS ctx-service idiom (the `ctx.view` / `ctx.inertia`
4
+ * analog). Register it globally in your kernel; a thin controller then does:
5
+ *
6
+ * async show({ aurora }: HttpContext) {
7
+ * return aurora.render('Dashboard', { user, stats })
8
+ * }
9
+ *
10
+ * Agnostic: it resolves the AuroraManager from the request container
11
+ * (`ctx.containerResolver.make('aurora')`) — never imports `@c9up/ream` — and is
12
+ * a no-op when no manager is registered. The module-level `aurora.render(ctx, …)`
13
+ * service still works; this is the ctx-bound sugar.
14
+ */
15
+ import type { RenderHttpContext, RenderPageOptions } from "./server/renderPage.js";
16
+ /** The `ctx.aurora` surface — ctx-bound render (no explicit ctx argument). */
17
+ export interface AuroraRequestRenderer {
18
+ render(name: string, props?: unknown, options?: RenderPageOptions): Promise<void>;
19
+ }
20
+ /** Request context the middleware needs: render target + optional resolver/slot. */
21
+ interface AuroraMiddlewareContext extends RenderHttpContext {
22
+ containerResolver?: {
23
+ make(token: unknown): unknown;
24
+ };
25
+ aurora?: AuroraRequestRenderer;
26
+ }
27
+ /**
28
+ * Middleware: attach `ctx.aurora` for the request. No-op (passes through) when
29
+ * the AuroraManager isn't registered, so it's safe to mount unconditionally.
30
+ */
31
+ export declare function auroraContext(ctx: AuroraMiddlewareContext, next: () => Promise<void>): Promise<void>;
32
+ export {};
@@ -0,0 +1,43 @@
1
+ /**
2
+ * `auroraContext` — binds `ctx.aurora.render(name, props)` onto the request
3
+ * context, the AdonisJS ctx-service idiom (the `ctx.view` / `ctx.inertia`
4
+ * analog). Register it globally in your kernel; a thin controller then does:
5
+ *
6
+ * async show({ aurora }: HttpContext) {
7
+ * return aurora.render('Dashboard', { user, stats })
8
+ * }
9
+ *
10
+ * Agnostic: it resolves the AuroraManager from the request container
11
+ * (`ctx.containerResolver.make('aurora')`) — never imports `@c9up/ream` — and is
12
+ * a no-op when no manager is registered. The module-level `aurora.render(ctx, …)`
13
+ * service still works; this is the ctx-bound sugar.
14
+ */
15
+ /** Structural check that a resolved value is render-capable (an AuroraManager). */
16
+ function isManager(value) {
17
+ return (typeof value === "object" &&
18
+ value !== null &&
19
+ "render" in value &&
20
+ typeof value.render === "function");
21
+ }
22
+ function resolveManager(resolver) {
23
+ try {
24
+ const resolved = resolver?.make("aurora");
25
+ return isManager(resolved) ? resolved : undefined;
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
31
+ /**
32
+ * Middleware: attach `ctx.aurora` for the request. No-op (passes through) when
33
+ * the AuroraManager isn't registered, so it's safe to mount unconditionally.
34
+ */
35
+ export function auroraContext(ctx, next) {
36
+ const manager = resolveManager(ctx.containerResolver);
37
+ if (manager) {
38
+ ctx.aurora = {
39
+ render: (name, props, options) => manager.render(ctx, name, props, options),
40
+ };
41
+ }
42
+ return next();
43
+ }
package/dist/rpc.d.ts CHANGED
@@ -8,9 +8,23 @@ export interface RpcClientOptions {
8
8
  http?: HttpClient;
9
9
  /** Default headers — only used when no `http` client is supplied. */
10
10
  headers?: Record<string, string>;
11
+ /**
12
+ * Auto CSRF: read the `XSRF-TOKEN` cookie and echo it as `X-XSRF-TOKEN` on
13
+ * every call (Axios/Angular convention), so RPC POSTs pass blackhole's
14
+ * signed double-submit check when the route is cookie/session-authed. No-op
15
+ * outside the browser and when the cookie is absent. Default `true`.
16
+ * `/rpc` under a bearer (JWT) guard is CSRF-exempt, so the missing-cookie
17
+ * no-op is exactly right there too.
18
+ */
19
+ xsrf?: boolean;
20
+ /** Cookie to read the CSRF token from. Default `XSRF-TOKEN`. */
21
+ xsrfCookieName?: string;
22
+ /** Header to echo the CSRF token in. Default `X-XSRF-TOKEN`. */
23
+ xsrfHeaderName?: string;
11
24
  }
12
25
  /**
13
26
  * Create a JSON-RPC client bound to aurora's HttpClient transport. Inherits the
14
- * supplied (or a fresh) HttpClient's base URL, auth headers, and timeouts.
27
+ * supplied (or a fresh) HttpClient's base URL, auth headers, and timeouts, and
28
+ * (by default) auto-attaches the `X-XSRF-TOKEN` CSRF header from the cookie.
15
29
  */
16
30
  export declare function createRpcClient(options?: RpcClientOptions): RpcClient;
package/dist/rpc.js CHANGED
@@ -16,14 +16,43 @@
16
16
  import { createRpcClient as createCometRpcClient } from "@c9up/comet";
17
17
  import { HttpClient } from "./http.js";
18
18
  export { isRpcError, RpcError, } from "@c9up/comet";
19
+ /**
20
+ * Read a cookie's raw value from `document.cookie`. Returns `undefined`
21
+ * server-side (no `document`) or when the cookie is absent. The value is sent
22
+ * verbatim — double-submit compares it byte-for-byte against the cookie, so it
23
+ * must not be decoded.
24
+ */
25
+ function readCookie(name) {
26
+ if (typeof document === "undefined")
27
+ return undefined;
28
+ const prefix = `${name}=`;
29
+ for (const part of document.cookie.split(";")) {
30
+ const trimmed = part.trimStart();
31
+ if (trimmed.startsWith(prefix))
32
+ return trimmed.slice(prefix.length);
33
+ }
34
+ return undefined;
35
+ }
19
36
  /**
20
37
  * Create a JSON-RPC client bound to aurora's HttpClient transport. Inherits the
21
- * supplied (or a fresh) HttpClient's base URL, auth headers, and timeouts.
38
+ * supplied (or a fresh) HttpClient's base URL, auth headers, and timeouts, and
39
+ * (by default) auto-attaches the `X-XSRF-TOKEN` CSRF header from the cookie.
22
40
  */
23
41
  export function createRpcClient(options = {}) {
24
42
  const http = options.http ?? new HttpClient({ headers: options.headers });
43
+ const xsrfEnabled = options.xsrf ?? true;
44
+ const cookieName = options.xsrfCookieName ?? "XSRF-TOKEN";
45
+ const headerName = options.xsrfHeaderName ?? "X-XSRF-TOKEN";
25
46
  return createCometRpcClient({
26
47
  url: options.url,
27
- transport: (url, body, { signal }) => http.post(url, body, { signal }),
48
+ transport: (url, body, { signal }) => {
49
+ let headers;
50
+ if (xsrfEnabled) {
51
+ const token = readCookie(cookieName);
52
+ if (token !== undefined)
53
+ headers = { [headerName]: token };
54
+ }
55
+ return http.post(url, body, { signal, headers });
56
+ },
28
57
  });
29
58
  }
@@ -8,7 +8,7 @@
8
8
  * <head>
9
9
  * ...
10
10
  * <script type="importmap">
11
- * { "imports": { "@c9up/aurora": "/_assets/aurora/index.js" } }
11
+ * { "imports": { "@c9up/aurora": "/__assets/aurora/index.js" } }
12
12
  * </script>
13
13
  * </head>
14
14
  * <body>
@@ -16,7 +16,7 @@
16
16
  * <script id="aurora-page-data" type="application/json">{…}</script>
17
17
  * <script type="module">
18
18
  * import { hydrate } from '@c9up/aurora'
19
- * import Page from '/_assets/pages/ProjectPage.js'
19
+ * import Page from '/__assets/pages/ProjectPage.js'
20
20
  * const data = JSON.parse(document.getElementById('aurora-page-data').textContent)
21
21
  * hydrate(document.getElementById('aurora-root'), () => Page(data.props))
22
22
  * </script>
@@ -39,7 +39,7 @@ export interface RenderHttpContext {
39
39
  export interface RenderPageOptions {
40
40
  /**
41
41
  * Importmap entries injected into `<head>`. Defaults to mapping
42
- * `@c9up/aurora` to `/_assets/aurora/index.js`. Override to point
42
+ * `@c9up/aurora` to `/__assets/aurora/index.js`. Override to point
43
43
  * at a different mount or to add app-side aliases.
44
44
  */
45
45
  importmap?: Record<string, string>;
@@ -8,7 +8,7 @@
8
8
  * <head>
9
9
  * ...
10
10
  * <script type="importmap">
11
- * { "imports": { "@c9up/aurora": "/_assets/aurora/index.js" } }
11
+ * { "imports": { "@c9up/aurora": "/__assets/aurora/index.js" } }
12
12
  * </script>
13
13
  * </head>
14
14
  * <body>
@@ -16,7 +16,7 @@
16
16
  * <script id="aurora-page-data" type="application/json">{…}</script>
17
17
  * <script type="module">
18
18
  * import { hydrate } from '@c9up/aurora'
19
- * import Page from '/_assets/pages/ProjectPage.js'
19
+ * import Page from '/__assets/pages/ProjectPage.js'
20
20
  * const data = JSON.parse(document.getElementById('aurora-page-data').textContent)
21
21
  * hydrate(document.getElementById('aurora-root'), () => Page(data.props))
22
22
  * </script>
@@ -58,7 +58,7 @@ export async function renderPage(ctx, pages, name, props, options = {}) {
58
58
  const tree = await factory(props);
59
59
  const body = renderToString(tree);
60
60
  const importmap = {
61
- "@c9up/aurora": "/_assets/aurora/index.js",
61
+ "@c9up/aurora": "/__assets/aurora/index.js",
62
62
  ...options.importmap,
63
63
  };
64
64
  const rootId = options.rootId ?? "aurora-root";
@@ -2,8 +2,8 @@
2
2
  * `serveAssets` — generic static-file handler exposed by aurora so an
3
3
  * app can mount the runtime + the pages dist with a couple of routes:
4
4
  *
5
- * router.get('/_assets/aurora/*', serveAssets({ root: auroraDistPath }))
6
- * router.get('/_assets/pages/*', serveAssets({ root: pagesPath }))
5
+ * router.get('/__assets/aurora/*', serveAssets({ root: auroraDistPath }))
6
+ * router.get('/__assets/pages/*', serveAssets({ root: pagesPath }))
7
7
  *
8
8
  * The handler is framework-agnostic: it reads `ctx.request.param('*')`
9
9
  * and writes to `ctx.response`. Any context that satisfies
@@ -2,8 +2,8 @@
2
2
  * `serveAssets` — generic static-file handler exposed by aurora so an
3
3
  * app can mount the runtime + the pages dist with a couple of routes:
4
4
  *
5
- * router.get('/_assets/aurora/*', serveAssets({ root: auroraDistPath }))
6
- * router.get('/_assets/pages/*', serveAssets({ root: pagesPath }))
5
+ * router.get('/__assets/aurora/*', serveAssets({ root: auroraDistPath }))
6
+ * router.get('/__assets/pages/*', serveAssets({ root: pagesPath }))
7
7
  *
8
8
  * The handler is framework-agnostic: it reads `ctx.request.param('*')`
9
9
  * and writes to `ctx.response`. Any context that satisfies
package/dist/server.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { AuroraManager, type AuroraManagerConfig } from "./AuroraManager.js";
2
+ export { type AuroraRequestRenderer, auroraContext, } from "./middleware.js";
2
3
  export { type PageFactory, Pages, type PagesConfig } from "./Pages.js";
3
4
  export { type RenderHttpContext, type RenderPageOptions, type RenderResponse, renderPage, } from "./server/renderPage.js";
4
5
  export { type AssetsHttpContext, type AssetsRequest, type AssetsResponse, packageAssetDir, type ServeAssetsOptions, serveAssets, } from "./server/serveAssets.js";
package/dist/server.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // Node built-ins through the import graph. Server code imports from
6
6
  // `@c9up/aurora/server`; the client `.` entry stays node-free.
7
7
  export { AuroraManager } from "./AuroraManager.js";
8
+ export { auroraContext, } from "./middleware.js";
8
9
  export { Pages } from "./Pages.js";
9
10
  export { renderPage, } from "./server/renderPage.js";
10
11
  export { packageAssetDir, serveAssets, } from "./server/serveAssets.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@c9up/aurora",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "Aurora — reactive UI runtime for the Ream framework. Tagged-template DOM, signal-based state, isomorphic SSR + hydration, zero build step.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -45,11 +45,24 @@ export interface AuroraManagerConfig {
45
45
  * URL prefix the asset routes mount under. The aurora runtime is served
46
46
  * from `<assetsPrefix>/aurora/*` and the app's pages from
47
47
  * `<assetsPrefix>/pages/*`, and the SSR importmap + page URLs derive from
48
- * it. Default `/_assets` (the leading underscore namespaces framework
48
+ * it. Default `/__assets` (the leading underscore namespaces framework
49
49
  * assets away from app routes, Next.js `/_next` style). Set e.g. `/assets`
50
50
  * for an underscore-free scheme. An explicit `pages.urlPrefix` still wins.
51
51
  */
52
52
  assetsPrefix?: string;
53
+ /**
54
+ * App-level importmap overrides, configured ONCE here (the AdonisJS model:
55
+ * config lives in `config/aurora.ts`, controllers stay thin and never hand-write
56
+ * importmaps). Merged over aurora's auto defaults (`@c9up/aurora`,
57
+ * `@c9up/aurora/rpc`, `@c9up/comet`) on every `render()`. Use to point a bare
58
+ * specifier at a curated browser entry, e.g.
59
+ * `{ '@c9up/aurora': '/__assets/pages/browser/aurora.js' }`. A per-call
60
+ * `render(..., { importmap })` still wins over this.
61
+ *
62
+ * (Importmaps are aurora's no-bundler particularity — AdonisJS bundles via Vite
63
+ * and has no importmap; we keep the config-driven shape to stay Adonis-idiomatic.)
64
+ */
65
+ importmap?: Record<string, string>;
53
66
  }
54
67
 
55
68
  const DEFAULT_AURORA_DIST = resolvePath(
@@ -79,7 +92,7 @@ function normalizePrefix(prefix: string): string {
79
92
  export class AuroraManager {
80
93
  readonly pages: Pages;
81
94
  readonly auroraDistRoot: string;
82
- /** Resolved asset prefix (default `/_assets`). */
95
+ /** Resolved asset prefix (default `/__assets`). */
83
96
  readonly assetsPrefix: string;
84
97
  /** Mount path for the aurora runtime — `<assetsPrefix>/aurora`. */
85
98
  readonly auroraAssetPath: string;
@@ -89,13 +102,16 @@ export class AuroraManager {
89
102
  readonly cometAssetPath: string;
90
103
  /** Resolved `@c9up/comet` dist dir, or `null` when comet isn't installed. */
91
104
  readonly cometDistRoot: string | null;
105
+ /** App-level importmap overrides from `config/aurora.ts`, merged on render. */
106
+ readonly importmap: Record<string, string>;
92
107
 
93
108
  constructor(config: AuroraManagerConfig) {
94
- this.assetsPrefix = normalizePrefix(config.assetsPrefix ?? "/_assets");
109
+ this.assetsPrefix = normalizePrefix(config.assetsPrefix ?? "/__assets");
95
110
  this.auroraAssetPath = `${this.assetsPrefix}/aurora`;
96
111
  this.pageAssetPath = `${this.assetsPrefix}/pages`;
97
112
  this.cometAssetPath = `${this.assetsPrefix}/comet`;
98
113
  this.cometDistRoot = config.cometDistRoot ?? resolveCometDist();
114
+ this.importmap = config.importmap ?? {};
99
115
  // Pages serve their compiled JS from the same prefix unless the app
100
116
  // pins an explicit urlPrefix.
101
117
  this.pages = new Pages({
@@ -106,10 +122,11 @@ export class AuroraManager {
106
122
  }
107
123
 
108
124
  /**
109
- * SSR + hydrate + ship the document. The importmap default points
110
- * `@c9up/aurora` at this manager's `assetsPrefix`; a caller's
111
- * `options.importmap` still overrides (e.g. to remap to an app-curated
112
- * browser entry).
125
+ * SSR + hydrate + ship the document. The importmap layers, last wins:
126
+ * aurora's auto defaults (`@c9up/aurora`, `@c9up/aurora/rpc`, `@c9up/comet`)
127
+ * < the config-level `importmap` (from `config/aurora.ts`) < a per-call
128
+ * `options.importmap`. So a thin controller calls `render(ctx, name, props)`
129
+ * with no importmap, and any curation lives once in config.
113
130
  */
114
131
  render(
115
132
  ctx: RenderHttpContext,
@@ -132,6 +149,9 @@ export class AuroraManager {
132
149
  ...(this.cometDistRoot
133
150
  ? { "@c9up/comet": `${this.cometAssetPath}/index.js` }
134
151
  : {}),
152
+ // Config-level overrides (config/aurora.ts) — Adonis-style config-driven,
153
+ // so controllers never hand-write an importmap. A per-call override wins.
154
+ ...this.importmap,
135
155
  ...options?.importmap,
136
156
  },
137
157
  });
@@ -139,7 +159,7 @@ export class AuroraManager {
139
159
 
140
160
  /**
141
161
  * Handler for aurora's pre-built ESM runtime. Mount on
142
- * `GET /_assets/aurora/*`.
162
+ * `GET /__assets/aurora/*`.
143
163
  */
144
164
  auroraAssetsHandler(): (ctx: AssetsHttpContext) => Promise<void> {
145
165
  return serveAssets({ root: this.auroraDistRoot });
@@ -147,7 +167,7 @@ export class AuroraManager {
147
167
 
148
168
  /**
149
169
  * Handler for the app's pages directory. Mount on
150
- * `GET /_assets/pages/*`.
170
+ * `GET /__assets/pages/*`.
151
171
  */
152
172
  pageAssetsHandler(): (ctx: AssetsHttpContext) => Promise<void> {
153
173
  return serveAssets({ root: this.pages.root });
@@ -2,8 +2,8 @@
2
2
  * AuroraProvider — registers the AuroraManager singleton and auto-mounts
3
3
  * the two asset routes the browser needs:
4
4
  *
5
- * GET /_assets/aurora/* → packages/@c9up/aurora/dist/*
6
- * GET /_assets/pages/* → resources/pages/*
5
+ * GET /__assets/aurora/* → packages/@c9up/aurora/dist/*
6
+ * GET /__assets/pages/* → resources/pages/*
7
7
  *
8
8
  * Config (in `config/aurora.ts`):
9
9
  *
@@ -92,7 +92,7 @@ export default class AuroraProvider {
92
92
  const router = this.app.container.resolve<ReamRouter>("router");
93
93
  const manager = this.app.container.resolve<AuroraManager>(AuroraManager);
94
94
  // Mount paths derive from the configured `assetsPrefix` (default
95
- // `/_assets`) — set `config.aurora.assetsPrefix` to change the scheme.
95
+ // `/__assets`) — set `config.aurora.assetsPrefix` to change the scheme.
96
96
  router.get(
97
97
  `${manager.auroraAssetPath}/*`,
98
98
  adaptHandler(manager.auroraAssetsHandler()),
package/src/Pages.ts CHANGED
@@ -33,7 +33,7 @@ export interface PagesConfig {
33
33
 
34
34
  /**
35
35
  * URL prefix the browser uses to fetch a page's compiled JS.
36
- * Defaults to `/_assets/pages`. A name `"Foo"` maps to
36
+ * Defaults to `/__assets/pages`. A name `"Foo"` maps to
37
37
  * `${urlPrefix}/Foo.js`.
38
38
  */
39
39
  urlPrefix?: string;
@@ -63,7 +63,7 @@ export class Pages {
63
63
 
64
64
  constructor(config: PagesConfig) {
65
65
  this.root = config.root;
66
- this.urlPrefix = (config.urlPrefix ?? "/_assets/pages").replace(/\/$/, "");
66
+ this.urlPrefix = (config.urlPrefix ?? "/__assets/pages").replace(/\/$/, "");
67
67
  this.extension = config.extension ?? ".js";
68
68
  }
69
69
 
package/src/live.ts CHANGED
@@ -130,3 +130,42 @@ export function mountLiveSession(
130
130
  },
131
131
  };
132
132
  }
133
+
134
+ // ─── `@c9up/aurora/live` barrel ──────────────────────────────────────
135
+ //
136
+ // The Live stack (broadcast / client / registry / router / server) is
137
+ // re-exported here so the whole niche surface lives behind ONE subpath,
138
+ // `@c9up/aurora/live` — keeping the main `.` barrel lean (it no longer
139
+ // eager-pulls these into every browser graph), consistent with how `./ssr`,
140
+ // `./relay`, `./rpc` and `./hydrate` are already subpath-gated.
141
+ export {
142
+ connectPatches,
143
+ type LiveStore,
144
+ liveStore,
145
+ type RelayBroadcaster,
146
+ } from "./liveBroadcast.js";
147
+ export {
148
+ buildLiveTransport,
149
+ type LiveClientOptions,
150
+ type LiveClientTransport,
151
+ type LiveHttpPoster,
152
+ liveClient,
153
+ type RelaySubscribeClient,
154
+ } from "./liveClient.js";
155
+ export {
156
+ createLiveRegistry,
157
+ type LiveRegistry,
158
+ type LiveSessionHandle,
159
+ } from "./liveRegistry.js";
160
+ export {
161
+ createLiveRouter,
162
+ type LiveMount,
163
+ type LiveRouter,
164
+ } from "./liveRouter.js";
165
+ export {
166
+ DEFAULT_LIVE_EVENT_PATH,
167
+ type LiveHttpContext,
168
+ type LiveHttpRouter,
169
+ type WireLiveEventsOptions,
170
+ wireLiveEvents,
171
+ } from "./liveServer.js";
package/src/liveClient.ts CHANGED
@@ -91,7 +91,7 @@ export interface LiveHttpPoster {
91
91
  /**
92
92
  * Build a {@link LiveClientTransport} from a relay client (SSE down) + an HTTP
93
93
  * client (events up). `path` must match the server's `wireLiveEvents` route
94
- * (default `/_live/event`). Keeps `liveClient` itself transport-agnostic.
94
+ * (default `/__live/event`). Keeps `liveClient` itself transport-agnostic.
95
95
  *
96
96
  * @example
97
97
  * import { relay } from '@c9up/aurora/relay'
@@ -104,7 +104,7 @@ export function buildLiveTransport(
104
104
  http: LiveHttpPoster,
105
105
  options: { path?: string } = {},
106
106
  ): LiveClientTransport {
107
- const path = options.path ?? "/_live/event";
107
+ const path = options.path ?? "/__live/event";
108
108
  return {
109
109
  subscribe: (channel, handler) =>
110
110
  relayClient.subscribe<SlotPatch[]>(channel, handler),
package/src/liveServer.ts CHANGED
@@ -45,7 +45,7 @@ function isLiveEventBody(value: unknown): value is LiveEventBody {
45
45
  }
46
46
 
47
47
  /** Default inbound-event route — keep the client transport's `path` in sync. */
48
- export const DEFAULT_LIVE_EVENT_PATH = "/_live/event";
48
+ export const DEFAULT_LIVE_EVENT_PATH = "/__live/event";
49
49
 
50
50
  /**
51
51
  * Register the inbound live-event route on the host router. Call once at boot
@@ -0,0 +1,74 @@
1
+ /**
2
+ * `auroraContext` — binds `ctx.aurora.render(name, props)` onto the request
3
+ * context, the AdonisJS ctx-service idiom (the `ctx.view` / `ctx.inertia`
4
+ * analog). Register it globally in your kernel; a thin controller then does:
5
+ *
6
+ * async show({ aurora }: HttpContext) {
7
+ * return aurora.render('Dashboard', { user, stats })
8
+ * }
9
+ *
10
+ * Agnostic: it resolves the AuroraManager from the request container
11
+ * (`ctx.containerResolver.make('aurora')`) — never imports `@c9up/ream` — and is
12
+ * a no-op when no manager is registered. The module-level `aurora.render(ctx, …)`
13
+ * service still works; this is the ctx-bound sugar.
14
+ */
15
+
16
+ import type { AuroraManager } from "./AuroraManager.js";
17
+ import type {
18
+ RenderHttpContext,
19
+ RenderPageOptions,
20
+ } from "./server/renderPage.js";
21
+
22
+ /** The `ctx.aurora` surface — ctx-bound render (no explicit ctx argument). */
23
+ export interface AuroraRequestRenderer {
24
+ render(
25
+ name: string,
26
+ props?: unknown,
27
+ options?: RenderPageOptions,
28
+ ): Promise<void>;
29
+ }
30
+
31
+ /** Request context the middleware needs: render target + optional resolver/slot. */
32
+ interface AuroraMiddlewareContext extends RenderHttpContext {
33
+ containerResolver?: { make(token: unknown): unknown };
34
+ aurora?: AuroraRequestRenderer;
35
+ }
36
+
37
+ /** Structural check that a resolved value is render-capable (an AuroraManager). */
38
+ function isManager(value: unknown): value is AuroraManager {
39
+ return (
40
+ typeof value === "object" &&
41
+ value !== null &&
42
+ "render" in value &&
43
+ typeof value.render === "function"
44
+ );
45
+ }
46
+
47
+ function resolveManager(
48
+ resolver: { make(token: unknown): unknown } | undefined,
49
+ ): AuroraManager | undefined {
50
+ try {
51
+ const resolved = resolver?.make("aurora");
52
+ return isManager(resolved) ? resolved : undefined;
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Middleware: attach `ctx.aurora` for the request. No-op (passes through) when
60
+ * the AuroraManager isn't registered, so it's safe to mount unconditionally.
61
+ */
62
+ export function auroraContext(
63
+ ctx: AuroraMiddlewareContext,
64
+ next: () => Promise<void>,
65
+ ): Promise<void> {
66
+ const manager = resolveManager(ctx.containerResolver);
67
+ if (manager) {
68
+ ctx.aurora = {
69
+ render: (name, props, options) =>
70
+ manager.render(ctx, name, props, options),
71
+ };
72
+ }
73
+ return next();
74
+ }
package/src/rpc.ts CHANGED
@@ -34,17 +34,56 @@ export interface RpcClientOptions {
34
34
  http?: HttpClient;
35
35
  /** Default headers — only used when no `http` client is supplied. */
36
36
  headers?: Record<string, string>;
37
+ /**
38
+ * Auto CSRF: read the `XSRF-TOKEN` cookie and echo it as `X-XSRF-TOKEN` on
39
+ * every call (Axios/Angular convention), so RPC POSTs pass blackhole's
40
+ * signed double-submit check when the route is cookie/session-authed. No-op
41
+ * outside the browser and when the cookie is absent. Default `true`.
42
+ * `/rpc` under a bearer (JWT) guard is CSRF-exempt, so the missing-cookie
43
+ * no-op is exactly right there too.
44
+ */
45
+ xsrf?: boolean;
46
+ /** Cookie to read the CSRF token from. Default `XSRF-TOKEN`. */
47
+ xsrfCookieName?: string;
48
+ /** Header to echo the CSRF token in. Default `X-XSRF-TOKEN`. */
49
+ xsrfHeaderName?: string;
50
+ }
51
+
52
+ /**
53
+ * Read a cookie's raw value from `document.cookie`. Returns `undefined`
54
+ * server-side (no `document`) or when the cookie is absent. The value is sent
55
+ * verbatim — double-submit compares it byte-for-byte against the cookie, so it
56
+ * must not be decoded.
57
+ */
58
+ function readCookie(name: string): string | undefined {
59
+ if (typeof document === "undefined") return undefined;
60
+ const prefix = `${name}=`;
61
+ for (const part of document.cookie.split(";")) {
62
+ const trimmed = part.trimStart();
63
+ if (trimmed.startsWith(prefix)) return trimmed.slice(prefix.length);
64
+ }
65
+ return undefined;
37
66
  }
38
67
 
39
68
  /**
40
69
  * Create a JSON-RPC client bound to aurora's HttpClient transport. Inherits the
41
- * supplied (or a fresh) HttpClient's base URL, auth headers, and timeouts.
70
+ * supplied (or a fresh) HttpClient's base URL, auth headers, and timeouts, and
71
+ * (by default) auto-attaches the `X-XSRF-TOKEN` CSRF header from the cookie.
42
72
  */
43
73
  export function createRpcClient(options: RpcClientOptions = {}): RpcClient {
44
74
  const http = options.http ?? new HttpClient({ headers: options.headers });
75
+ const xsrfEnabled = options.xsrf ?? true;
76
+ const cookieName = options.xsrfCookieName ?? "XSRF-TOKEN";
77
+ const headerName = options.xsrfHeaderName ?? "X-XSRF-TOKEN";
45
78
  return createCometRpcClient({
46
79
  url: options.url,
47
- transport: (url, body, { signal }) =>
48
- http.post<unknown>(url, body, { signal }),
80
+ transport: (url, body, { signal }) => {
81
+ let headers: Record<string, string> | undefined;
82
+ if (xsrfEnabled) {
83
+ const token = readCookie(cookieName);
84
+ if (token !== undefined) headers = { [headerName]: token };
85
+ }
86
+ return http.post<unknown>(url, body, { signal, headers });
87
+ },
49
88
  });
50
89
  }
@@ -8,7 +8,7 @@
8
8
  * <head>
9
9
  * ...
10
10
  * <script type="importmap">
11
- * { "imports": { "@c9up/aurora": "/_assets/aurora/index.js" } }
11
+ * { "imports": { "@c9up/aurora": "/__assets/aurora/index.js" } }
12
12
  * </script>
13
13
  * </head>
14
14
  * <body>
@@ -16,7 +16,7 @@
16
16
  * <script id="aurora-page-data" type="application/json">{…}</script>
17
17
  * <script type="module">
18
18
  * import { hydrate } from '@c9up/aurora'
19
- * import Page from '/_assets/pages/ProjectPage.js'
19
+ * import Page from '/__assets/pages/ProjectPage.js'
20
20
  * const data = JSON.parse(document.getElementById('aurora-page-data').textContent)
21
21
  * hydrate(document.getElementById('aurora-root'), () => Page(data.props))
22
22
  * </script>
@@ -45,7 +45,7 @@ export interface RenderHttpContext {
45
45
  export interface RenderPageOptions {
46
46
  /**
47
47
  * Importmap entries injected into `<head>`. Defaults to mapping
48
- * `@c9up/aurora` to `/_assets/aurora/index.js`. Override to point
48
+ * `@c9up/aurora` to `/__assets/aurora/index.js`. Override to point
49
49
  * at a different mount or to add app-side aliases.
50
50
  */
51
51
  importmap?: Record<string, string>;
@@ -139,7 +139,7 @@ export async function renderPage<P>(
139
139
  const body = renderToString(tree);
140
140
 
141
141
  const importmap = {
142
- "@c9up/aurora": "/_assets/aurora/index.js",
142
+ "@c9up/aurora": "/__assets/aurora/index.js",
143
143
  ...options.importmap,
144
144
  };
145
145
  const rootId = options.rootId ?? "aurora-root";
@@ -2,8 +2,8 @@
2
2
  * `serveAssets` — generic static-file handler exposed by aurora so an
3
3
  * app can mount the runtime + the pages dist with a couple of routes:
4
4
  *
5
- * router.get('/_assets/aurora/*', serveAssets({ root: auroraDistPath }))
6
- * router.get('/_assets/pages/*', serveAssets({ root: pagesPath }))
5
+ * router.get('/__assets/aurora/*', serveAssets({ root: auroraDistPath }))
6
+ * router.get('/__assets/pages/*', serveAssets({ root: pagesPath }))
7
7
  *
8
8
  * The handler is framework-agnostic: it reads `ctx.request.param('*')`
9
9
  * and writes to `ctx.response`. Any context that satisfies
package/src/server.ts CHANGED
@@ -6,6 +6,10 @@
6
6
  // `@c9up/aurora/server`; the client `.` entry stays node-free.
7
7
 
8
8
  export { AuroraManager, type AuroraManagerConfig } from "./AuroraManager.js";
9
+ export {
10
+ type AuroraRequestRenderer,
11
+ auroraContext,
12
+ } from "./middleware.js";
9
13
  export { type PageFactory, Pages, type PagesConfig } from "./Pages.js";
10
14
  export {
11
15
  type RenderHttpContext,