@lunora/client 1.0.0-alpha.22 → 1.0.0-alpha.24

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 (29) hide show
  1. package/dist/auth/index.d.mts +1 -1
  2. package/dist/auth/index.d.ts +1 -1
  3. package/dist/index.d.mts +66 -4
  4. package/dist/index.d.ts +66 -4
  5. package/dist/index.mjs +8 -7
  6. package/dist/packem_shared/{LunoraClient-Clb118SU.mjs → LunoraClient-D3h4P7hg.mjs} +143 -247
  7. package/dist/packem_shared/{OfflineQueue-B4HUF7rt.mjs → OfflineQueue-BgarnAub.mjs} +1 -1
  8. package/dist/packem_shared/{SubscriptionRegistry-D4jfIzZu.mjs → SubscriptionRegistry-CxS_Inha.mjs} +2 -2
  9. package/dist/packem_shared/{TabCoordinator-BwRR8H06.mjs → TabCoordinator-D_5oNTTt.mjs} +48 -12
  10. package/dist/packem_shared/{createClientQuery-CQ51bWAE.mjs → createClientQuery-dJZg1ohm.mjs} +15 -6
  11. package/dist/packem_shared/createLocalStore-BtqUmOQA.mjs +2 -0
  12. package/dist/packem_shared/{createServerClient-Dxemst5C.mjs → createServerClient-DzeC2J3A.mjs} +1 -1
  13. package/dist/packem_shared/{createSnapshotPrecondition-CBwnVz6r.mjs → createSnapshotPrecondition-CxQ1T4ZP.mjs} +3 -3
  14. package/dist/packem_shared/httpStream-DIdL8NEw.mjs +168 -0
  15. package/dist/packem_shared/{local-store-DtcIW4c0.mjs → local-store-DIq-UWfD.mjs} +1 -1
  16. package/dist/packem_shared/{lunora-client.d-pw-9sLl0.d.mts → lunora-client.d-B8bdwHLr.d.mts} +132 -18
  17. package/dist/packem_shared/{lunora-client.d-pw-9sLl0.d.ts → lunora-client.d-B8bdwHLr.d.ts} +132 -18
  18. package/dist/packem_shared/{offline-queue-CF4_Co5k.mjs → offline-queue-N-1JvYb4.mjs} +14 -2
  19. package/dist/packem_shared/{preload.d-BkQr-3Vh.d.ts → preload.d-BPy9qajK.d.ts} +1 -1
  20. package/dist/packem_shared/{preload.d-6ME5ubgq.d.mts → preload.d-Ccrgw2z0.d.mts} +1 -1
  21. package/dist/packem_shared/wire-key-Djie6aaR.mjs +266 -0
  22. package/dist/query/index.d.mts +2 -2
  23. package/dist/query/index.d.ts +2 -2
  24. package/dist/ssr/index.d.mts +3 -3
  25. package/dist/ssr/index.d.ts +3 -3
  26. package/dist/ssr/index.mjs +1 -1
  27. package/package.json +2 -2
  28. package/dist/packem_shared/createLocalStore-BDbbkoXw.mjs +0 -2
  29. package/dist/packem_shared/stable-key-wv6eP48B.mjs +0 -40
@@ -0,0 +1,168 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { createStream } from './DEFAULT_MAX_BUFFER-7hFnzNk9.mjs';
3
+
4
+ const SSE_FIELD_SPACE_RE = /^ /u;
5
+ const parameterToString = (value) => typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
6
+ const parseSseFrame = (raw) => {
7
+ let event = "";
8
+ const dataLines = [];
9
+ for (const line of raw.split("\n")) {
10
+ if (line.startsWith("event:")) {
11
+ event = line.slice("event:".length).replace(SSE_FIELD_SPACE_RE, "");
12
+ } else if (line.startsWith("data:")) {
13
+ dataLines.push(line.slice("data:".length).replace(SSE_FIELD_SPACE_RE, ""));
14
+ }
15
+ }
16
+ return { data: dataLines.join("\n"), event };
17
+ };
18
+ const buildHttpStreamUrl = (route, args, baseUrl) => {
19
+ const parameters = args.params ?? {};
20
+ const path = route.path.split("/").map((segment) => {
21
+ if (!segment.startsWith(":")) {
22
+ return segment;
23
+ }
24
+ const name = segment.slice(1);
25
+ const value = parameters[name];
26
+ if (value === void 0) {
27
+ throw new LunoraError("HTTP_STREAM_MISSING_PARAM", `httpStream: missing path param ":${name}" for route ${route.path}`);
28
+ }
29
+ return encodeURIComponent(parameterToString(value));
30
+ }).join("/");
31
+ const search = new URLSearchParams();
32
+ for (const [key, value] of Object.entries(args.searchParams ?? {})) {
33
+ if (value !== void 0) {
34
+ search.set(key, parameterToString(value));
35
+ }
36
+ }
37
+ const query = search.toString();
38
+ const trimmedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
39
+ return `${trimmedBase}${path}${query === "" ? "" : `?${query}`}`;
40
+ };
41
+ const handleSseFrame = (frame, handle) => {
42
+ if (frame.event === "complete") {
43
+ handle.complete();
44
+ return true;
45
+ }
46
+ if (frame.event === "error") {
47
+ let payload = {};
48
+ try {
49
+ payload = JSON.parse(frame.data);
50
+ } catch {
51
+ }
52
+ const message = typeof payload.message === "string" ? payload.message : "stream error";
53
+ const code = typeof payload.code === "string" ? payload.code : "HTTP_STREAM_ERROR";
54
+ handle.fail(new LunoraError(code, message));
55
+ return true;
56
+ }
57
+ if ((frame.event === "" || frame.event === "message") && frame.data !== "") {
58
+ try {
59
+ handle.push(JSON.parse(frame.data));
60
+ } catch {
61
+ handle.fail(new LunoraError("HTTP_STREAM_BAD_CHUNK", "httpStream: malformed SSE chunk (invalid JSON)"));
62
+ return true;
63
+ }
64
+ }
65
+ return false;
66
+ };
67
+ const pumpSseBody = async (body, handle) => {
68
+ const reader = body.getReader();
69
+ const decoder = new TextDecoder();
70
+ let buffer = "";
71
+ const drainFrames = () => {
72
+ let separatorIndex = buffer.indexOf("\n\n");
73
+ while (separatorIndex !== -1) {
74
+ const frame = parseSseFrame(buffer.slice(0, separatorIndex));
75
+ buffer = buffer.slice(separatorIndex + 2);
76
+ if (handleSseFrame(frame, handle)) {
77
+ return true;
78
+ }
79
+ separatorIndex = buffer.indexOf("\n\n");
80
+ }
81
+ return false;
82
+ };
83
+ for (; ; ) {
84
+ const { done, value } = await reader.read();
85
+ if (done) {
86
+ break;
87
+ }
88
+ buffer += decoder.decode(value, { stream: true }).replaceAll("\r\n", "\n");
89
+ if (drainFrames()) {
90
+ await reader.cancel().catch(() => {
91
+ });
92
+ return;
93
+ }
94
+ }
95
+ buffer += decoder.decode().replaceAll("\r\n", "\n");
96
+ if (!drainFrames()) {
97
+ handle.fail(new LunoraError("HTTP_STREAM_INTERRUPTED", "httpStream: stream ended without a complete frame"));
98
+ }
99
+ };
100
+ const httpStream = (route, args, options = {}) => {
101
+ const fetchImpl = options.fetch ?? (typeof fetch === "function" ? fetch.bind(globalThis) : void 0);
102
+ if (!fetchImpl) {
103
+ throw new LunoraError("INTERNAL", "httpStream: no `fetch` implementation available");
104
+ }
105
+ const url = buildHttpStreamUrl(route, args ?? {}, options.baseUrl ?? "");
106
+ const ac = new AbortController();
107
+ let onExternalAbort;
108
+ if (options.signal) {
109
+ if (options.signal.aborted) {
110
+ ac.abort();
111
+ } else {
112
+ onExternalAbort = () => {
113
+ ac.abort();
114
+ };
115
+ options.signal.addEventListener("abort", onExternalAbort, { once: true });
116
+ }
117
+ }
118
+ const detachExternalAbort = () => {
119
+ if (onExternalAbort) {
120
+ options.signal?.removeEventListener("abort", onExternalAbort);
121
+ }
122
+ };
123
+ const { handle, iterable } = createStream({
124
+ maxBuffer: options.maxBuffer,
125
+ onCancel: () => {
126
+ ac.abort();
127
+ }
128
+ });
129
+ (async () => {
130
+ try {
131
+ const response = await fetchImpl(url, {
132
+ headers: { accept: "text/event-stream", ...options.headers },
133
+ method: route.method,
134
+ signal: ac.signal
135
+ });
136
+ if (!response.ok) {
137
+ await response.body?.cancel().catch(() => {
138
+ });
139
+ handle.fail(
140
+ new LunoraError("HTTP_STREAM_STATUS", `httpStream: request failed (status ${response.status.toString()})`, {
141
+ status: response.status
142
+ })
143
+ );
144
+ return;
145
+ }
146
+ if (!response.body) {
147
+ handle.fail(new LunoraError("HTTP_STREAM_NO_BODY", "httpStream: response has no body"));
148
+ return;
149
+ }
150
+ await pumpSseBody(response.body, handle);
151
+ } finally {
152
+ detachExternalAbort();
153
+ }
154
+ })().catch((error) => {
155
+ if (ac.signal.aborted) {
156
+ handle.complete();
157
+ return;
158
+ }
159
+ if (error instanceof Error && "code" in error) {
160
+ handle.fail(error);
161
+ return;
162
+ }
163
+ handle.fail(new LunoraError("HTTP_STREAM_TRANSPORT", error instanceof Error ? error.message : String(error), { cause: error }));
164
+ });
165
+ return iterable;
166
+ };
167
+
168
+ export { httpStream };
@@ -1,4 +1,4 @@
1
- import { SubscriptionRegistry } from './SubscriptionRegistry-D4jfIzZu.mjs';
1
+ import { SubscriptionRegistry } from './SubscriptionRegistry-CxS_Inha.mjs';
2
2
 
3
3
  const foldOptimistic = (base, layers) => {
4
4
  let value = base;
@@ -117,6 +117,55 @@ interface FunctionReference<Kind extends FunctionKind = FunctionKind, Args = unk
117
117
  type ArgsOf<F> = F extends FunctionReference<infer _K, infer A, infer _R> ? A : never;
118
118
  /** Extract the return type from a {@link FunctionReference}. */
119
119
  type ReturnOf<F> = F extends FunctionReference<infer _K, infer _A, infer R> ? R : never;
120
+ /**
121
+ * Typed reference to an HTTP-SSE stream route (`httpRoute.&lt;verb>(path).stream()`)
122
+ * emitted by `@lunora/codegen` as `httpStreams.&lt;namespace>.&lt;name>`.
123
+ *
124
+ * Distinct from {@link FunctionReference}: this is the **HTTP-SSE route stream**
125
+ * (opened with `fetch` + `ReadableStream` against the route's own URL), not the
126
+ * WS procedure stream (`kind: "stream"`). At runtime it carries the HTTP verb
127
+ * and the route path; the phantom marker carries the chunk / searchParams /
128
+ * params types so `httpStream` (and the framework hooks over it) infer the
129
+ * chunk type end-to-end.
130
+ * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
131
+ */
132
+ interface HttpStreamRef<Chunk = unknown, SearchParams = unknown, Params = unknown> {
133
+ /**
134
+ * Phantom marker carrying the `Chunk`/`SearchParams`/`Params` type
135
+ * parameters for inference. Never present at runtime; declared in a
136
+ * covariant (output) position so a concrete reference stays assignable to
137
+ * a widened one.
138
+ */
139
+ readonly __lunoraHttpStream?: {
140
+ chunk: Chunk;
141
+ params: Params;
142
+ searchParams: SearchParams;
143
+ };
144
+ /** HTTP verb the route binds to (uppercased), e.g. `"GET"`. */
145
+ readonly method: string;
146
+ /** The route path as declared, e.g. `/api/tokens/:id` — `:name` segments are filled from `params`. */
147
+ readonly path: string;
148
+ }
149
+ /**
150
+ * The call-side args of an HTTP-SSE stream route: `:name` path params plus URL query params.
151
+ * @experimental Part of the HTTP-SSE stream surface.
152
+ */
153
+ interface HttpStreamCallArgs<SearchParams = unknown, Params = unknown> {
154
+ /** Values for the route path's `:name` segments. */
155
+ params?: Params;
156
+ /** URL query params, appended to the request URL (undefined entries are skipped). */
157
+ searchParams?: SearchParams;
158
+ }
159
+ /**
160
+ * Extract the chunk type from a {@link HttpStreamRef}.
161
+ * @experimental Part of the HTTP-SSE stream surface.
162
+ */
163
+ type HttpStreamChunkOf<R> = R extends HttpStreamRef<infer Chunk, infer _S, infer _P> ? Chunk : never;
164
+ /**
165
+ * Extract the call-side args type from a {@link HttpStreamRef}.
166
+ * @experimental Part of the HTTP-SSE stream surface.
167
+ */
168
+ type HttpStreamArgsOf<R> = R extends HttpStreamRef<infer _C, infer S, infer P> ? HttpStreamCallArgs<S, P> : never;
120
169
  type Unsubscribe = () => void;
121
170
  /**
122
171
  * Serializable result of `preloadQuery`. Produced on the server during SSR,
@@ -309,6 +358,15 @@ interface QueryCacheAdapter {
309
358
  /** Remove one cached query by key. */
310
359
  remove: (key: string) => Promise<void>;
311
360
  }
361
+ /**
362
+ * Resolves the WS `?token=` credential fresh at every (re)connect — the channel
363
+ * for short-lived tokens (e.g. the ephemeral admin sub-token the worker mints
364
+ * at `POST /_lunora/admin/ws-token`) instead of a static secret in the URL.
365
+ * May return the token synchronously or as a Promise; returning `undefined`
366
+ * connects without a token. A thrown error / rejected Promise fails that
367
+ * connect attempt, and the client retries with its normal reconnect backoff.
368
+ */
369
+ type WsTokenProvider = () => Promise<string | undefined> | string | undefined;
312
370
  interface LunoraClientOptions {
313
371
  /**
314
372
  * Base path the worker mounts better-auth at, used by the client's
@@ -421,15 +479,21 @@ interface LunoraClientOptions {
421
479
  url: string;
422
480
  WebSocket?: typeof WebSocket;
423
481
  /**
424
- * Token appended to the WebSocket URL as `?token=…`. The server matches it
425
- * against `LUNORA_WS_BEARER` (to clear the upgrade gate) and/or
482
+ * Credential appended to the WebSocket URL as `?token=…`. The server matches
483
+ * it against `LUNORA_WS_BEARER` (to clear the upgrade gate) and/or
426
484
  * `LUNORA_ADMIN_TOKEN` (to authorize `__lunora_admin__:*` subscriptions —
427
- * what the studio sets it to). Browsers can't set headers on the
428
- * `WebSocket` constructor, so the query parameter is the only channel; it
429
- * ends up in server logs and history, so prefer a short-lived rotating
430
- * token in production.
485
+ * what the studio supplies). Browsers can't set headers on the `WebSocket`
486
+ * constructor, so the query parameter is the only channel; it ends up in
487
+ * server logs and history, so prefer a short-lived rotating token in
488
+ * production over a static secret.
489
+ *
490
+ * Pass a {@link WsTokenProvider} function to resolve the token fresh at
491
+ * every (re)connect — the channel for short-lived credentials such as the
492
+ * ephemeral admin sub-token minted by `POST /_lunora/admin/ws-token`: the
493
+ * provider re-mints on each reconnect, including the one following a `4001`
494
+ * token-expired drop, so a static master token never has to ride the URL.
431
495
  */
432
- wsToken?: string;
496
+ wsToken?: string | WsTokenProvider;
433
497
  wsUrl?: string;
434
498
  }
435
499
  /** Wire envelope sent on `POST /_lunora/rpc`. */
@@ -997,9 +1061,10 @@ interface SubscriptionState {
997
1061
  acked: boolean;
998
1062
  readonly args: Record<string, unknown>;
999
1063
  /**
1000
- * Stable-stringified `args`, computed once at subscribe time. Cached so the
1001
- * optimistic-update fan-out can compare against a mutation's args key without
1002
- * re-serializing every subscription's args on every mutation.
1064
+ * Stable wire-key of `args` (`stableWireKey`), computed once at subscribe
1065
+ * time. Cached so the optimistic-update fan-out can compare against a
1066
+ * mutation's args key without re-serializing every subscription's args on
1067
+ * every mutation.
1003
1068
  */
1004
1069
  readonly argsKey: string;
1005
1070
  readonly callbacks: Set<SubscriptionCallback>;
@@ -1068,11 +1133,13 @@ interface SubscriptionState {
1068
1133
  }
1069
1134
  /**
1070
1135
  * Active subscription registry. The client keys subscriptions by
1071
- * `(functionPath, stableStringify(args), shardKey)` so duplicate calls share a
1136
+ * `(functionPath, stableWireKey(args), shardKey)` so duplicate calls share a
1072
1137
  * single server-side registration. Args are stably encoded (keys sorted at every
1073
1138
  * depth) so two structurally-equal arg records constructed with a different key
1074
1139
  * order (`{ a, b }` vs `{ b, a }`) collapse to the same key instead of leaking a
1075
- * duplicate subscription.
1140
+ * duplicate subscription. Encoding the args' **wire form** keeps the key
1141
+ * byte-identical for pure-JSON args while giving wire-typed args (`bigint`,
1142
+ * `Date`, bytes, …) distinct stable tokens instead of a throw.
1076
1143
  */
1077
1144
  declare class SubscriptionRegistry {
1078
1145
  static key(functionPath: string, args: Record<string, unknown>, shardKey?: string): string;
@@ -1539,10 +1606,12 @@ declare class LunoraClient {
1539
1606
  * Replace the token appended to WS upgrade URLs as `?token=…` and close
1540
1607
  * every open shard socket so the reconnect picks up the new value. Call
1541
1608
  * this whenever the user's WS credential changes (rotating the admin token
1542
- * in the studio, switching workspaces, etc.). Bearer tokens for HTTP
1543
- * RPC are independentsee {@link setAuthToken}.
1609
+ * in the studio, switching workspaces, etc.). Accepts a static string or a
1610
+ * {@link WsTokenProvider} resolved fresh at every (re)connect the channel
1611
+ * for short-lived credentials like the minted ephemeral admin sub-token.
1612
+ * Bearer tokens for HTTP RPC are independent — see {@link setAuthToken}.
1544
1613
  */
1545
- setWsToken(token: string | undefined): void;
1614
+ setWsToken(token: string | undefined | WsTokenProvider): void;
1546
1615
  /**
1547
1616
  * Register (or clear, with `undefined`) the app context sent in the `connect`
1548
1617
  * envelope for a shard's socket, overriding the client-wide
@@ -1655,6 +1724,18 @@ declare class LunoraClient {
1655
1724
  */
1656
1725
  onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
1657
1726
  /**
1727
+ * The `WebSocket` implementation this client was constructed with (an
1728
+ * explicit `options.WebSocket`, or the ambient global on platforms that have
1729
+ * one) — `undefined` if neither is available. This is the seam a feature
1730
+ * that opens its OWN socket outside the client's multiplexed connection
1731
+ * (e.g. a voice-agent hook) should default to, instead of reaching for
1732
+ * `globalThis.WebSocket` directly: on React Native the client wraps this
1733
+ * constructor to inject the auth-headers factory's credential onto the
1734
+ * upgrade request (`createLunoraClient`'s `withAuthWebSocket`), which a raw
1735
+ * `new globalThis.WebSocket(url)` would silently bypass.
1736
+ */
1737
+ getWebSocketImpl(): typeof WebSocket | undefined;
1738
+ /**
1658
1739
  * Read the current value for a {@link ClientQueryRef}. Returns
1659
1740
  * `ref.defaultValue` when no value has been explicitly set.
1660
1741
  */
@@ -1869,8 +1950,10 @@ declare class LunoraClient {
1869
1950
  * Subscribe to the live scheduled-jobs list over the SchedulerDO's admin
1870
1951
  * WebSocket. `onJobs` fires with the full list on connect and on every
1871
1952
  * change (schedule / cancel / alarm-fire). Reconnects with the client's
1872
- * configured backoff. Requires `wsToken` to be set to the admin token (the
1873
- * browser can't send an `Authorization` header on a WS). Returns an
1953
+ * configured backoff. Requires `wsToken` to be set to an admin credential
1954
+ * (the browser can't send an `Authorization` header on a WS) the master
1955
+ * token, or preferably a {@link WsTokenProvider} minting the ephemeral
1956
+ * sub-token so the master credential stays out of the URL. Returns an
1874
1957
  * unsubscribe function that closes the socket and stops reconnecting.
1875
1958
  */
1876
1959
  subscribeScheduledJobs(onJobs: (jobs: ScheduleRecord[]) => void): Unsubscribe;
@@ -2370,6 +2453,25 @@ declare class LunoraClient {
2370
2453
  maxBuffer?: number;
2371
2454
  shardKey?: string;
2372
2455
  }): StreamIterable<ReturnOf<F>>;
2456
+ /**
2457
+ * Open a typed **HTTP-SSE route stream** (`httpRoute.&lt;verb>(path).stream()`).
2458
+ * Distinct from {@link LunoraClient.stream}, which consumes the WS procedure
2459
+ * stream (`kind: "stream"`): this one opens the route's own URL with `fetch`
2460
+ * and parses the Server-Sent Events framing the route pump writes (`data:`
2461
+ * chunks, a final `event: complete`, an `event: error` on throw).
2462
+ *
2463
+ * The reference comes from the generated `httpStreams.*` registry, so the
2464
+ * yielded chunk type is the route handler's yielded type. Cancelling the
2465
+ * returned iterable (or aborting `options.signal`) aborts the fetch, which
2466
+ * the server handler observes via its `signal`. The client's bearer token
2467
+ * (when set) rides as an `authorization` header.
2468
+ * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
2469
+ */
2470
+ httpStream<Ref extends HttpStreamRef>(route: Ref, args?: HttpStreamArgsOf<Ref>, options?: {
2471
+ headers?: Record<string, string>;
2472
+ maxBuffer?: number;
2473
+ signal?: AbortSignal;
2474
+ }): StreamIterable<HttpStreamChunkOf<Ref>>;
2373
2475
  close(): void;
2374
2476
  /**
2375
2477
  * Persist a mutation that can't go out on the wire right now (offline, or
@@ -2515,6 +2617,18 @@ declare class LunoraClient {
2515
2617
  */
2516
2618
  private resendShapeSubscriptions;
2517
2619
  private ensureSocket;
2620
+ /**
2621
+ * Resolve the {@link WsTokenProvider} and open the shard socket with the
2622
+ * minted token. The connection is already in the `connecting` state, so the
2623
+ * async gap is race-guarded: a client `close()`, a `setWsToken` bounce, or a
2624
+ * competing connect that landed first all abandon this attempt. A provider
2625
+ * failure fails the attempt through {@link handleDisconnect}, which arms the
2626
+ * normal reconnect backoff — a broken mint endpoint degrades to retries, not
2627
+ * a silent tokenless socket the admin gate would reject.
2628
+ */
2629
+ private openSocketWithProvidedToken;
2630
+ /** Construct the shard socket and wire its lifecycle handlers. The connection must already be in the `connecting` state. */
2631
+ private openSocket;
2518
2632
  private handleDisconnect;
2519
2633
  /**
2520
2634
  * Begin the keepalive heartbeat on an open connection. Each tick sends a
@@ -2719,4 +2833,4 @@ declare class LunoraClient {
2719
2833
  */
2720
2834
  private settleReplayBatchSlots;
2721
2835
  }
2722
- export { StorageObject as $, ArgsOf as A, BookmarkStorage as B, CONFLICT_ERROR_CODE as C, DEFAULT_MAX_BUFFER as D, RowOp as E, FunctionReference as F, GlobalFacetResult as G, RpcEnvelope as H, RpcResponseBody as I, ScheduleRecord as J, SchedulerPoolStatus as K, LunoraClient as L, MutationCallOptions as M, SchedulerStatus as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, ServerMessage as T, User as U, ServerPokeEndMessage as V, ServerPokePartMessage as W, ServerPokeStartMessage as X, ShardTrafficEntry as Y, ShardTrafficResult as Z, StorageListPage as _, Unsubscribe as a, StreamHandle as a0, StreamIterable as a1, SubscriptionCallback as a2, SubscriptionRegistry as a3, SubscriptionState as a4, SyncWatermark as a5, WorkflowInstanceAction as a6, WorkflowInstanceDetail as a7, WorkflowInstancePage as a8, WorkflowInstanceStatus as a9, WorkflowInstanceSummary as aa, WorkflowStepDetail as ab, createClientQuery as ac, createLocalStore as ad, createStream as ae, getErrorCode as af, getRetryAfterMs as ag, isConflictError as ah, isForbiddenError as ai, isRateLimitedError as aj, isUnauthorizedError as ak, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, BatchSlot as e, CachedQuery as f, ClientMessage as g, ClientQueryRef as h, ClientShapeSubscribeMessage as i, ClientShapeUnsubscribeMessage as j, ConnectionStatus as k, FunctionArgumentDescriptor as l, FunctionDescriptor as m, GlobalFacetValue as n, GlobalFilterClause as o, GlobalTableInfo as p, GlobalTablePage as q, LunoraClientError as r, LunoraClientOptions as s, LunoraErrorCode as t, MutationSettledEvent as u, OptimisticLocalStore as v, OptimisticUpdate as w, OutboxMutation as x, OutboxSink as y, PersistedMutation as z };
2836
+ export { ServerPokePartMessage as $, ArgsOf as A, BookmarkStorage as B, CONFLICT_ERROR_CODE as C, DEFAULT_MAX_BUFFER as D, OptimisticUpdate as E, FunctionReference as F, GlobalFacetResult as G, HttpStreamRef as H, OutboxMutation as I, OutboxSink as J, PersistedMutation as K, LunoraClient as L, MutationCallOptions as M, RowOp as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, RpcEnvelope as T, User as U, RpcResponseBody as V, ScheduleRecord as W, SchedulerPoolStatus as X, SchedulerStatus as Y, ServerMessage as Z, ServerPokeEndMessage as _, Unsubscribe as a, ServerPokeStartMessage as a0, ShardTrafficEntry as a1, ShardTrafficResult as a2, StorageListPage as a3, StorageObject as a4, StreamHandle as a5, SubscriptionCallback as a6, SubscriptionRegistry as a7, SubscriptionState as a8, SyncWatermark as a9, WorkflowInstanceAction as aa, WorkflowInstanceDetail as ab, WorkflowInstancePage as ac, WorkflowInstanceStatus as ad, WorkflowInstanceSummary as ae, WorkflowStepDetail as af, WsTokenProvider as ag, createClientQuery as ah, createLocalStore as ai, createStream as aj, getErrorCode as ak, getRetryAfterMs as al, isConflictError as am, isForbiddenError as an, isRateLimitedError as ao, isUnauthorizedError as ap, SubscriptionErrorCallback as b, PersistenceAdapter as c, HttpStreamArgsOf as d, HttpStreamChunkOf as e, StreamIterable as f, ReconnectOptions as g, BatchSlot as h, CachedQuery as i, ClientMessage as j, ClientQueryRef as k, ClientShapeSubscribeMessage as l, ClientShapeUnsubscribeMessage as m, ConnectionStatus as n, FunctionArgumentDescriptor as o, FunctionDescriptor as p, GlobalFacetValue as q, GlobalFilterClause as r, GlobalTableInfo as s, GlobalTablePage as t, HttpStreamCallArgs as u, LunoraClientError as v, LunoraClientOptions as w, LunoraErrorCode as x, MutationSettledEvent as y, OptimisticLocalStore as z };
@@ -117,6 +117,55 @@ interface FunctionReference<Kind extends FunctionKind = FunctionKind, Args = unk
117
117
  type ArgsOf<F> = F extends FunctionReference<infer _K, infer A, infer _R> ? A : never;
118
118
  /** Extract the return type from a {@link FunctionReference}. */
119
119
  type ReturnOf<F> = F extends FunctionReference<infer _K, infer _A, infer R> ? R : never;
120
+ /**
121
+ * Typed reference to an HTTP-SSE stream route (`httpRoute.&lt;verb>(path).stream()`)
122
+ * emitted by `@lunora/codegen` as `httpStreams.&lt;namespace>.&lt;name>`.
123
+ *
124
+ * Distinct from {@link FunctionReference}: this is the **HTTP-SSE route stream**
125
+ * (opened with `fetch` + `ReadableStream` against the route's own URL), not the
126
+ * WS procedure stream (`kind: "stream"`). At runtime it carries the HTTP verb
127
+ * and the route path; the phantom marker carries the chunk / searchParams /
128
+ * params types so `httpStream` (and the framework hooks over it) infer the
129
+ * chunk type end-to-end.
130
+ * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
131
+ */
132
+ interface HttpStreamRef<Chunk = unknown, SearchParams = unknown, Params = unknown> {
133
+ /**
134
+ * Phantom marker carrying the `Chunk`/`SearchParams`/`Params` type
135
+ * parameters for inference. Never present at runtime; declared in a
136
+ * covariant (output) position so a concrete reference stays assignable to
137
+ * a widened one.
138
+ */
139
+ readonly __lunoraHttpStream?: {
140
+ chunk: Chunk;
141
+ params: Params;
142
+ searchParams: SearchParams;
143
+ };
144
+ /** HTTP verb the route binds to (uppercased), e.g. `"GET"`. */
145
+ readonly method: string;
146
+ /** The route path as declared, e.g. `/api/tokens/:id` — `:name` segments are filled from `params`. */
147
+ readonly path: string;
148
+ }
149
+ /**
150
+ * The call-side args of an HTTP-SSE stream route: `:name` path params plus URL query params.
151
+ * @experimental Part of the HTTP-SSE stream surface.
152
+ */
153
+ interface HttpStreamCallArgs<SearchParams = unknown, Params = unknown> {
154
+ /** Values for the route path's `:name` segments. */
155
+ params?: Params;
156
+ /** URL query params, appended to the request URL (undefined entries are skipped). */
157
+ searchParams?: SearchParams;
158
+ }
159
+ /**
160
+ * Extract the chunk type from a {@link HttpStreamRef}.
161
+ * @experimental Part of the HTTP-SSE stream surface.
162
+ */
163
+ type HttpStreamChunkOf<R> = R extends HttpStreamRef<infer Chunk, infer _S, infer _P> ? Chunk : never;
164
+ /**
165
+ * Extract the call-side args type from a {@link HttpStreamRef}.
166
+ * @experimental Part of the HTTP-SSE stream surface.
167
+ */
168
+ type HttpStreamArgsOf<R> = R extends HttpStreamRef<infer _C, infer S, infer P> ? HttpStreamCallArgs<S, P> : never;
120
169
  type Unsubscribe = () => void;
121
170
  /**
122
171
  * Serializable result of `preloadQuery`. Produced on the server during SSR,
@@ -309,6 +358,15 @@ interface QueryCacheAdapter {
309
358
  /** Remove one cached query by key. */
310
359
  remove: (key: string) => Promise<void>;
311
360
  }
361
+ /**
362
+ * Resolves the WS `?token=` credential fresh at every (re)connect — the channel
363
+ * for short-lived tokens (e.g. the ephemeral admin sub-token the worker mints
364
+ * at `POST /_lunora/admin/ws-token`) instead of a static secret in the URL.
365
+ * May return the token synchronously or as a Promise; returning `undefined`
366
+ * connects without a token. A thrown error / rejected Promise fails that
367
+ * connect attempt, and the client retries with its normal reconnect backoff.
368
+ */
369
+ type WsTokenProvider = () => Promise<string | undefined> | string | undefined;
312
370
  interface LunoraClientOptions {
313
371
  /**
314
372
  * Base path the worker mounts better-auth at, used by the client's
@@ -421,15 +479,21 @@ interface LunoraClientOptions {
421
479
  url: string;
422
480
  WebSocket?: typeof WebSocket;
423
481
  /**
424
- * Token appended to the WebSocket URL as `?token=…`. The server matches it
425
- * against `LUNORA_WS_BEARER` (to clear the upgrade gate) and/or
482
+ * Credential appended to the WebSocket URL as `?token=…`. The server matches
483
+ * it against `LUNORA_WS_BEARER` (to clear the upgrade gate) and/or
426
484
  * `LUNORA_ADMIN_TOKEN` (to authorize `__lunora_admin__:*` subscriptions —
427
- * what the studio sets it to). Browsers can't set headers on the
428
- * `WebSocket` constructor, so the query parameter is the only channel; it
429
- * ends up in server logs and history, so prefer a short-lived rotating
430
- * token in production.
485
+ * what the studio supplies). Browsers can't set headers on the `WebSocket`
486
+ * constructor, so the query parameter is the only channel; it ends up in
487
+ * server logs and history, so prefer a short-lived rotating token in
488
+ * production over a static secret.
489
+ *
490
+ * Pass a {@link WsTokenProvider} function to resolve the token fresh at
491
+ * every (re)connect — the channel for short-lived credentials such as the
492
+ * ephemeral admin sub-token minted by `POST /_lunora/admin/ws-token`: the
493
+ * provider re-mints on each reconnect, including the one following a `4001`
494
+ * token-expired drop, so a static master token never has to ride the URL.
431
495
  */
432
- wsToken?: string;
496
+ wsToken?: string | WsTokenProvider;
433
497
  wsUrl?: string;
434
498
  }
435
499
  /** Wire envelope sent on `POST /_lunora/rpc`. */
@@ -997,9 +1061,10 @@ interface SubscriptionState {
997
1061
  acked: boolean;
998
1062
  readonly args: Record<string, unknown>;
999
1063
  /**
1000
- * Stable-stringified `args`, computed once at subscribe time. Cached so the
1001
- * optimistic-update fan-out can compare against a mutation's args key without
1002
- * re-serializing every subscription's args on every mutation.
1064
+ * Stable wire-key of `args` (`stableWireKey`), computed once at subscribe
1065
+ * time. Cached so the optimistic-update fan-out can compare against a
1066
+ * mutation's args key without re-serializing every subscription's args on
1067
+ * every mutation.
1003
1068
  */
1004
1069
  readonly argsKey: string;
1005
1070
  readonly callbacks: Set<SubscriptionCallback>;
@@ -1068,11 +1133,13 @@ interface SubscriptionState {
1068
1133
  }
1069
1134
  /**
1070
1135
  * Active subscription registry. The client keys subscriptions by
1071
- * `(functionPath, stableStringify(args), shardKey)` so duplicate calls share a
1136
+ * `(functionPath, stableWireKey(args), shardKey)` so duplicate calls share a
1072
1137
  * single server-side registration. Args are stably encoded (keys sorted at every
1073
1138
  * depth) so two structurally-equal arg records constructed with a different key
1074
1139
  * order (`{ a, b }` vs `{ b, a }`) collapse to the same key instead of leaking a
1075
- * duplicate subscription.
1140
+ * duplicate subscription. Encoding the args' **wire form** keeps the key
1141
+ * byte-identical for pure-JSON args while giving wire-typed args (`bigint`,
1142
+ * `Date`, bytes, …) distinct stable tokens instead of a throw.
1076
1143
  */
1077
1144
  declare class SubscriptionRegistry {
1078
1145
  static key(functionPath: string, args: Record<string, unknown>, shardKey?: string): string;
@@ -1539,10 +1606,12 @@ declare class LunoraClient {
1539
1606
  * Replace the token appended to WS upgrade URLs as `?token=…` and close
1540
1607
  * every open shard socket so the reconnect picks up the new value. Call
1541
1608
  * this whenever the user's WS credential changes (rotating the admin token
1542
- * in the studio, switching workspaces, etc.). Bearer tokens for HTTP
1543
- * RPC are independentsee {@link setAuthToken}.
1609
+ * in the studio, switching workspaces, etc.). Accepts a static string or a
1610
+ * {@link WsTokenProvider} resolved fresh at every (re)connect the channel
1611
+ * for short-lived credentials like the minted ephemeral admin sub-token.
1612
+ * Bearer tokens for HTTP RPC are independent — see {@link setAuthToken}.
1544
1613
  */
1545
- setWsToken(token: string | undefined): void;
1614
+ setWsToken(token: string | undefined | WsTokenProvider): void;
1546
1615
  /**
1547
1616
  * Register (or clear, with `undefined`) the app context sent in the `connect`
1548
1617
  * envelope for a shard's socket, overriding the client-wide
@@ -1655,6 +1724,18 @@ declare class LunoraClient {
1655
1724
  */
1656
1725
  onMutationSettled(listener: (event: MutationSettledEvent) => void): Unsubscribe;
1657
1726
  /**
1727
+ * The `WebSocket` implementation this client was constructed with (an
1728
+ * explicit `options.WebSocket`, or the ambient global on platforms that have
1729
+ * one) — `undefined` if neither is available. This is the seam a feature
1730
+ * that opens its OWN socket outside the client's multiplexed connection
1731
+ * (e.g. a voice-agent hook) should default to, instead of reaching for
1732
+ * `globalThis.WebSocket` directly: on React Native the client wraps this
1733
+ * constructor to inject the auth-headers factory's credential onto the
1734
+ * upgrade request (`createLunoraClient`'s `withAuthWebSocket`), which a raw
1735
+ * `new globalThis.WebSocket(url)` would silently bypass.
1736
+ */
1737
+ getWebSocketImpl(): typeof WebSocket | undefined;
1738
+ /**
1658
1739
  * Read the current value for a {@link ClientQueryRef}. Returns
1659
1740
  * `ref.defaultValue` when no value has been explicitly set.
1660
1741
  */
@@ -1869,8 +1950,10 @@ declare class LunoraClient {
1869
1950
  * Subscribe to the live scheduled-jobs list over the SchedulerDO's admin
1870
1951
  * WebSocket. `onJobs` fires with the full list on connect and on every
1871
1952
  * change (schedule / cancel / alarm-fire). Reconnects with the client's
1872
- * configured backoff. Requires `wsToken` to be set to the admin token (the
1873
- * browser can't send an `Authorization` header on a WS). Returns an
1953
+ * configured backoff. Requires `wsToken` to be set to an admin credential
1954
+ * (the browser can't send an `Authorization` header on a WS) the master
1955
+ * token, or preferably a {@link WsTokenProvider} minting the ephemeral
1956
+ * sub-token so the master credential stays out of the URL. Returns an
1874
1957
  * unsubscribe function that closes the socket and stops reconnecting.
1875
1958
  */
1876
1959
  subscribeScheduledJobs(onJobs: (jobs: ScheduleRecord[]) => void): Unsubscribe;
@@ -2370,6 +2453,25 @@ declare class LunoraClient {
2370
2453
  maxBuffer?: number;
2371
2454
  shardKey?: string;
2372
2455
  }): StreamIterable<ReturnOf<F>>;
2456
+ /**
2457
+ * Open a typed **HTTP-SSE route stream** (`httpRoute.&lt;verb>(path).stream()`).
2458
+ * Distinct from {@link LunoraClient.stream}, which consumes the WS procedure
2459
+ * stream (`kind: "stream"`): this one opens the route's own URL with `fetch`
2460
+ * and parses the Server-Sent Events framing the route pump writes (`data:`
2461
+ * chunks, a final `event: complete`, an `event: error` on throw).
2462
+ *
2463
+ * The reference comes from the generated `httpStreams.*` registry, so the
2464
+ * yielded chunk type is the route handler's yielded type. Cancelling the
2465
+ * returned iterable (or aborting `options.signal`) aborts the fetch, which
2466
+ * the server handler observes via its `signal`. The client's bearer token
2467
+ * (when set) rides as an `authorization` header.
2468
+ * @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
2469
+ */
2470
+ httpStream<Ref extends HttpStreamRef>(route: Ref, args?: HttpStreamArgsOf<Ref>, options?: {
2471
+ headers?: Record<string, string>;
2472
+ maxBuffer?: number;
2473
+ signal?: AbortSignal;
2474
+ }): StreamIterable<HttpStreamChunkOf<Ref>>;
2373
2475
  close(): void;
2374
2476
  /**
2375
2477
  * Persist a mutation that can't go out on the wire right now (offline, or
@@ -2515,6 +2617,18 @@ declare class LunoraClient {
2515
2617
  */
2516
2618
  private resendShapeSubscriptions;
2517
2619
  private ensureSocket;
2620
+ /**
2621
+ * Resolve the {@link WsTokenProvider} and open the shard socket with the
2622
+ * minted token. The connection is already in the `connecting` state, so the
2623
+ * async gap is race-guarded: a client `close()`, a `setWsToken` bounce, or a
2624
+ * competing connect that landed first all abandon this attempt. A provider
2625
+ * failure fails the attempt through {@link handleDisconnect}, which arms the
2626
+ * normal reconnect backoff — a broken mint endpoint degrades to retries, not
2627
+ * a silent tokenless socket the admin gate would reject.
2628
+ */
2629
+ private openSocketWithProvidedToken;
2630
+ /** Construct the shard socket and wire its lifecycle handlers. The connection must already be in the `connecting` state. */
2631
+ private openSocket;
2518
2632
  private handleDisconnect;
2519
2633
  /**
2520
2634
  * Begin the keepalive heartbeat on an open connection. Each tick sends a
@@ -2719,4 +2833,4 @@ declare class LunoraClient {
2719
2833
  */
2720
2834
  private settleReplayBatchSlots;
2721
2835
  }
2722
- export { StorageObject as $, ArgsOf as A, BookmarkStorage as B, CONFLICT_ERROR_CODE as C, DEFAULT_MAX_BUFFER as D, RowOp as E, FunctionReference as F, GlobalFacetResult as G, RpcEnvelope as H, RpcResponseBody as I, ScheduleRecord as J, SchedulerPoolStatus as K, LunoraClient as L, MutationCallOptions as M, SchedulerStatus as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, ServerMessage as T, User as U, ServerPokeEndMessage as V, ServerPokePartMessage as W, ServerPokeStartMessage as X, ShardTrafficEntry as Y, ShardTrafficResult as Z, StorageListPage as _, Unsubscribe as a, StreamHandle as a0, StreamIterable as a1, SubscriptionCallback as a2, SubscriptionRegistry as a3, SubscriptionState as a4, SyncWatermark as a5, WorkflowInstanceAction as a6, WorkflowInstanceDetail as a7, WorkflowInstancePage as a8, WorkflowInstanceStatus as a9, WorkflowInstanceSummary as aa, WorkflowStepDetail as ab, createClientQuery as ac, createLocalStore as ad, createStream as ae, getErrorCode as af, getRetryAfterMs as ag, isConflictError as ah, isForbiddenError as ai, isRateLimitedError as aj, isUnauthorizedError as ak, SubscriptionErrorCallback as b, PersistenceAdapter as c, ReconnectOptions as d, BatchSlot as e, CachedQuery as f, ClientMessage as g, ClientQueryRef as h, ClientShapeSubscribeMessage as i, ClientShapeUnsubscribeMessage as j, ConnectionStatus as k, FunctionArgumentDescriptor as l, FunctionDescriptor as m, GlobalFacetValue as n, GlobalFilterClause as o, GlobalTableInfo as p, GlobalTablePage as q, LunoraClientError as r, LunoraClientOptions as s, LunoraErrorCode as t, MutationSettledEvent as u, OptimisticLocalStore as v, OptimisticUpdate as w, OutboxMutation as x, OutboxSink as y, PersistedMutation as z };
2836
+ export { ServerPokePartMessage as $, ArgsOf as A, BookmarkStorage as B, CONFLICT_ERROR_CODE as C, DEFAULT_MAX_BUFFER as D, OptimisticUpdate as E, FunctionReference as F, GlobalFacetResult as G, HttpStreamRef as H, OutboxMutation as I, OutboxSink as J, PersistedMutation as K, LunoraClient as L, MutationCallOptions as M, RowOp as N, OfflineQueueOptions as O, Preloaded as P, QueryCacheAdapter as Q, ReturnOf as R, SubscriptionError as S, RpcEnvelope as T, User as U, RpcResponseBody as V, ScheduleRecord as W, SchedulerPoolStatus as X, SchedulerStatus as Y, ServerMessage as Z, ServerPokeEndMessage as _, Unsubscribe as a, ServerPokeStartMessage as a0, ShardTrafficEntry as a1, ShardTrafficResult as a2, StorageListPage as a3, StorageObject as a4, StreamHandle as a5, SubscriptionCallback as a6, SubscriptionRegistry as a7, SubscriptionState as a8, SyncWatermark as a9, WorkflowInstanceAction as aa, WorkflowInstanceDetail as ab, WorkflowInstancePage as ac, WorkflowInstanceStatus as ad, WorkflowInstanceSummary as ae, WorkflowStepDetail as af, WsTokenProvider as ag, createClientQuery as ah, createLocalStore as ai, createStream as aj, getErrorCode as ak, getRetryAfterMs as al, isConflictError as am, isForbiddenError as an, isRateLimitedError as ao, isUnauthorizedError as ap, SubscriptionErrorCallback as b, PersistenceAdapter as c, HttpStreamArgsOf as d, HttpStreamChunkOf as e, StreamIterable as f, ReconnectOptions as g, BatchSlot as h, CachedQuery as i, ClientMessage as j, ClientQueryRef as k, ClientShapeSubscribeMessage as l, ClientShapeUnsubscribeMessage as m, ConnectionStatus as n, FunctionArgumentDescriptor as o, FunctionDescriptor as p, GlobalFacetValue as q, GlobalFilterClause as r, GlobalTableInfo as s, GlobalTablePage as t, HttpStreamCallArgs as u, LunoraClientError as v, LunoraClientOptions as w, LunoraErrorCode as x, MutationSettledEvent as y, OptimisticLocalStore as z };