naro-js 0.6.1 → 0.7.1

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
@@ -100,6 +100,7 @@ await naro.from("posts").delete().eq("id", 10);
100
100
  - Email and social sign-in — see [Sign-in flows](#sign-in-flows).
101
101
  - Files — see [Storage](#storage).
102
102
  - Live changes, broadcast and presence — see [Realtime](#realtime).
103
+ - Server code your app calls by name — see [Functions](#functions).
103
104
 
104
105
  ## Policies
105
106
 
@@ -773,7 +774,10 @@ await naro.removeChannel(room);
773
774
  4003, 4004 and 4410 stop it — call `subscribe()` again; it connects after
774
775
  the backoff, counted from the stop, so a `subscribe()` in the
775
776
  `CHANNEL_ERROR` callback does not spin. 4029 comes back after 5 seconds at
776
- least (60 to 70 for `IP_RATE_LIMITED`); 4503 after 0 to 10 seconds, every
777
+ least (60 to 70 for `IP_RATE_LIMITED`; 900 to 910 for
778
+ `REALTIME_QUOTA_EXCEEDED` — the project used its realtime messages for the
779
+ month (an operator's cap): tried again every 15 minutes, naro-js 0.7.1);
780
+ 4503 after 0 to 10 seconds, every
777
781
  time; anything else after 1, 2, 5 and then 10 seconds, each times 0.5 to
778
782
  1.5 — after a 4008 the steps start over only once the connection outlived
779
783
  a heartbeat. Removing the last channel and subscribing again keeps the wait
@@ -880,3 +884,135 @@ const naro = createClient({ url, anonKey, realtime: { WebSocket } });
880
884
 
881
885
  The whole guide, with the policies, the close codes and the limits:
882
886
  https://naro.sh/docs/realtime
887
+
888
+ ## Functions
889
+
890
+ Server code your app calls by name, written the way Supabase Edge Functions
891
+ are (naro-js 0.7.0). `naro functions new hello` writes one and
892
+ `naro functions deploy hello` puts it live; the folders, imports, secrets and
893
+ limits are in https://naro.sh/docs/functions.
894
+
895
+ ```ts
896
+ // naro/functions/hello/index.ts
897
+ import { withNaro } from "naro-js/functions";
898
+
899
+ Deno.serve(
900
+ withNaro(async (req, { user, naro }) => {
901
+ const { name } = await req.json();
902
+ // naro acts as the caller: the tables' policies apply.
903
+ const { data } = await naro
904
+ .from("profiles")
905
+ .select("*")
906
+ .eq("id", user?.id ?? "");
907
+ return Response.json({ message: `Hello ${name}!`, profile: data });
908
+ }),
909
+ );
910
+ ```
911
+
912
+ ```ts
913
+ // your app
914
+ import { createClient, FunctionsHttpError } from "naro-js";
915
+
916
+ const naro = createClient({ url, anonKey });
917
+ const { data, error } = await naro.functions.invoke("hello", {
918
+ body: { name: "Naro" },
919
+ });
920
+ if (error instanceof FunctionsHttpError)
921
+ console.log(await error.context.json());
922
+ ```
923
+
924
+ - `invoke(name, { body, headers, method, timeout, signal })` calls
925
+ `<url>/_naro/v1/functions/<name>` (`POST` by default) with the anon key
926
+ and, when someone is signed in, their access token — renewed as a query's
927
+ is. `name` may carry a path and a query (`"hello/items?limit=5"`); only
928
+ the name is encoded. A name of `.` or `..`, or a path whose `..` would
929
+ take the URL out of `/functions/` once it is normalised, is refused
930
+ before anything is sent. `region` is taken and ignored.
931
+ - An object or array goes as JSON, a string as text, a Blob or bytes as
932
+ `application/octet-stream`; `FormData`, `URLSearchParams` and a
933
+ `ReadableStream` go as they are, and your own `content-type` wins; a
934
+ `GET` or `HEAD` call takes no body. The
935
+ answer is read by its content type: JSON, a Blob for
936
+ `application/octet-stream` and `application/pdf`, the `Response` itself
937
+ for `text/event-stream`, a `FormData` for `multipart/form-data`, text
938
+ otherwise, `null` for 204 and `HEAD`. `timeout` (ms) is the whole call's:
939
+ the wait for a sign-in the page URL carried, a token renewal and the one
940
+ retry of a call Naro refused for an expired token count in it, up to the
941
+ final answer's headers. Your `signal` stops the call at any of these
942
+ waits; the sign-in and the renewal go on for the rest of the page. The
943
+ body is not timed — pass a `signal` to stop a body that is slow to
944
+ arrive.
945
+ - It resolves to `{ data, error }` and never throws, with `response` beside
946
+ them when one came. `error` is one of supabase-js's classes, and has
947
+ `code`, `message` and `details` too: `FunctionsHttpError` — your function
948
+ answered 4xx or 5xx (`FUNCTION_HTTP_ERROR`; `error.context` is the unread
949
+ `Response`, and a function that threw answers
950
+ `500 FUNCTION_ERROR` in it); `FunctionsRelayError` — Naro answered for it
951
+ (`x-relay-error: true`): `FUNCTION_NOT_FOUND`, `INVALID_API_KEY`,
952
+ `INVALID_SESSION`, `FUNCTIONS_QUOTA_EXCEEDED` with `details.retryAfter`,
953
+ …, and `RELAY_ERROR` for a `HEAD` call; `FunctionsFetchError` — everything
954
+ else, `error.context` holding the cause:
955
+ - no answer came: `NETWORK_ERROR` (fetch threw, `details.cause`),
956
+ `TIMEOUT` (the call did not finish within `timeout` — a sign-in
957
+ exchange or a token renewal that outlasts it included, and Naro's
958
+ refusal of the first token whose body was still being read then, which
959
+ is dropped — a refusal of the renewed token is the call's answer, so it
960
+ comes back as `INVALID_SESSION` even past `timeout`), `ABORTED` (your
961
+ `signal`);
962
+ - nothing was sent: `INVALID_BODY` (a body JSON cannot hold),
963
+ `INVALID_OPTIONS` (a name, `timeout`, `headers` or `signal` it cannot
964
+ use, or a body with `GET` or `HEAD`), `ACCESS_TOKEN_UNAVAILABLE` (the
965
+ `accessToken` function of a client made with one failed), or the error
966
+ renewing the access token before the call answered — `INVALID_SESSION`
967
+ when the session is gone, `NETWORK_ERROR` when Naro could not be
968
+ reached;
969
+ - the function answered 2xx but its body could not be read as its
970
+ content type says: `INVALID_RESPONSE`, with `response` beside it.
971
+ - Only Naro's own `INVALID_SESSION` renews the access token and tries once
972
+ more; your function answering `{ "code": "INVALID_SESSION" }` signs nobody
973
+ out. A `HEAD` answer has no body, so Naro's refusal of a `HEAD` call is
974
+ `RELAY_ERROR` with the status alone, and a `HEAD` call is never renewed:
975
+ the `INVALID_SESSION` that would say so is not there to read.
976
+ - Inside a function, `withNaro(handler)` hands the handler `user` (`id`,
977
+ `email` — `null` until it is confirmed — and `emailVerified`, or `null`),
978
+ `role` (`anon`, `authenticated`, `service`, or `null` for a function
979
+ deployed with `--no-verify-jwt`), `traceId`, `naro` and `naroAdmin`. Naro
980
+ checked the caller before your code runs; `withNaro` does not check again.
981
+ `naro` sends `NARO_ANON_KEY` and the caller's token, so the tables'
982
+ policies apply; `naroAdmin` sends `NARO_SERVICE_KEY` and skips them, and
983
+ is made the first time you read it. Outside a Naro function `withNaro`
984
+ refuses with `withNaro can only run inside a Naro function.`
985
+ - `role` says how the caller came in; `naro` always sends the anon key — use
986
+ `naroAdmin` for service access.
987
+ - Inside a Naro function, `withNaro` finds the caller for the request it was
988
+ handed and for one your code rebuilt from it (`req.clone()`,
989
+ `new Request(req, …)`). `naro` sends the token of the request you hand
990
+ it, so a request you rebuild must copy `authorization` (or start from
991
+ `req.headers`): `new Request(req, { headers: { "x-extra": "1" } })` has no
992
+ `authorization`, and `naro` goes as anon while `context.user` still names
993
+ the caller. The caller is found per call, not per request: work one call
994
+ started and your module state keeps (a memoised promise, say) answers with
995
+ that call's caller when another call awaits it. A rebuilt request whose
996
+ `x-naro-trace-id` is not this call's is refused rather than run as this
997
+ call's caller. The trace id belongs to a chain of calls, not to one call
998
+ (a function calling another passes it on), so two calls sharing one are
999
+ not told apart.
1000
+ - With verify off (`--no-verify-jwt`), Naro checked nobody: `user` is `null`
1001
+ even when `naro` carries the caller's token (the Data API judges that
1002
+ token on each query), and `naro.auth.*` is refused on that client. An
1003
+ `Authorization` that is not a bearer token — a webhook's `Basic …`, or
1004
+ anything but printable ASCII without spaces — gives `naro` no token: it
1005
+ goes as anon.
1006
+ - `naro` is `createClient({ url, anonKey, accessToken })`: `accessToken` is
1007
+ a function that returns the token to send, or `null`, read for every
1008
+ call. Such a client never stores, renews or clears a token; `naro.auth`
1009
+ and `naro.channel()` throw on it (Realtime signs in with a session —
1010
+ broadcast from a function with `naroAdmin`), and it cannot take a
1011
+ `serviceKey`.
1012
+ - `naro-js/functions` declares the globals Naro provides — `Deno.serve`,
1013
+ `Deno.env` and `EdgeRuntime.waitUntil` — so a function type-checks. Leave
1014
+ `naro/functions` out of your app's `tsconfig.json`
1015
+ (`"exclude": ["naro/functions"]`): a project that also loads Deno's or
1016
+ Next.js's own declarations can see them twice.
1017
+ - A function's request has no Cloudflare `request.cf`; the caller's address
1018
+ is in `cf-connecting-ip`.
package/dist/client.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AuthClient } from "./auth/client.js";
2
2
  import { type AuthStorage } from "./auth/storage.js";
3
3
  import { type FetchLike } from "./http.js";
4
+ import { FunctionsClient } from "./invoke/client.js";
4
5
  import { TableBuilder } from "./query/builder.js";
5
6
  import type { RealtimeChannel } from "./realtime/channel.js";
6
7
  import type { RealtimeChannelOptions, RealtimeOptions, RealtimeResult } from "./realtime/types.js";
@@ -32,9 +33,17 @@ type CommonOptions = {
32
33
  */
33
34
  realtime?: RealtimeOptions;
34
35
  };
36
+ /**
37
+ * The token a client sends, from your code (design F20) — a Naro function's
38
+ * caller, or a token your own auth gives — read for every call; null (or
39
+ * undefined, or "") sends none. The client then stores, renews and clears
40
+ * nothing, and `auth` and `channel()` are not available.
41
+ */
42
+ export type AccessToken = () => string | null | undefined | Promise<string | null | undefined>;
35
43
  export type ClientOptions = CommonOptions & ({
36
44
  anonKey: string;
37
45
  serviceKey?: undefined;
46
+ accessToken?: AccessToken;
38
47
  }
39
48
  /**
40
49
  * Server only: skips every policy (spec §7). Refused unless the runtime
@@ -43,6 +52,7 @@ export type ClientOptions = CommonOptions & ({
43
52
  | {
44
53
  serviceKey: string;
45
54
  anonKey?: undefined;
55
+ accessToken?: undefined;
46
56
  });
47
57
  /** User-facing; quoted in spec §15's "(P1-4 구현 결정)". */
48
58
  export declare const CONFIG_MESSAGES: {
@@ -53,6 +63,8 @@ export declare const CONFIG_MESSAGES: {
53
63
  readonly serviceKeyOffServer: "A Naro service key (naro_sk_…) skips every policy, so createClient accepts it only on a server (Node, Deno, Bun, Cloudflare Workers, an edge runtime), never in a browser, a web worker or a React Native app. Use the anon key (naro_pk_…) here and keep the service key on your server.";
54
64
  readonly fetch: "This runtime has no fetch. Pass one to createClient({ fetch }).";
55
65
  readonly realtime: "createClient's realtime options take a WebSocket class, and heartbeatIntervalMs and timeoutMs in whole milliseconds from 1 to 2147483647.";
66
+ readonly accessTokenWithServiceKey: "Pass createClient an accessToken or a serviceKey, not both: the service key skips every policy, so the user's token would not count.";
67
+ readonly accessToken: "createClient's accessToken is a function that returns the user's access token, or null for none.";
56
68
  };
57
69
  export declare class NaroClient<Database extends DatabaseShape<Database>> {
58
70
  #private;
@@ -67,6 +79,11 @@ export declare class NaroClient<Database extends DatabaseShape<Database>> {
67
79
  * them, as supabase-js does.
68
80
  */
69
81
  readonly storage: StorageClient;
82
+ /**
83
+ * The project's Naro Functions (design §9): `functions.invoke(name, {
84
+ * body })`, as in supabase-js.
85
+ */
86
+ readonly functions: FunctionsClient;
70
87
  constructor(options: ClientOptions);
71
88
  /** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
72
89
  from<Table extends TableName<Database>>(table: Table): TableBuilder<RowOf<TablesOf<Database>[Table]>, InsertOf<TablesOf<Database>[Table]>, UpdateOf<TablesOf<Database>[Table]>>;
@@ -88,8 +105,10 @@ export declare class NaroClient<Database extends DatabaseShape<Database>> {
88
105
  }
89
106
  /**
90
107
  * The one entry point (spec §15). Throws only for a configuration mistake —
91
- * no url or not an origin, no key, or a service key off a server; every call
92
- * on the client resolves to `{ data, error }` instead.
108
+ * no url or not an origin, no key, a service key off a server, or an
109
+ * accessToken beside a service key or not a function. On a client made with
110
+ * accessToken, `naro.auth` and `naro.channel()` throw when used, not here.
111
+ * Every other call on the client resolves to `{ data, error }` instead.
93
112
  */
94
113
  export declare function createClient<Database extends DatabaseShape<Database> = UntypedDatabase>(options: ClientOptions): NaroClient<Database>;
95
114
  export {};
package/dist/errors.d.ts CHANGED
@@ -92,6 +92,12 @@ export declare function invalidChannel(reason: string): NaroError;
92
92
  export declare function invalidChannelConfig(path: string, reason: string): NaroError;
93
93
  /** `createClient`'s refusal of a service key off a server, for the call that reaches the socket late. */
94
94
  export declare function serviceKeyOffServer(): NaroError;
95
+ /** The accessToken function threw, rejected, or gave what is not a string: nothing is sent. */
96
+ export declare function accessTokenUnavailable(cause: unknown): NaroError;
97
+ /** `naro.auth.<name>` on such a client. */
98
+ export declare function accessTokenAuthMessage(name: string): string;
99
+ /** `naro.channel()` on such a client: the hub takes a session token, which it has none of. */
100
+ export declare const ACCESS_TOKEN_CHANNEL_MESSAGE = "naro.channel() is not available on a client made with accessToken: Realtime signs in with a session, not an access token. Broadcast from a function with naroAdmin.";
95
101
  /**
96
102
  * The hub's NOT_JOINED, raised before sending. A name that passed
97
103
  * channelNameProblem has at most 128 code points, which the hub quotes
@@ -0,0 +1,13 @@
1
+ export declare const WITH_NARO_MESSAGES: {
2
+ readonly outside: "withNaro can only run inside a Naro function.";
3
+ };
4
+ /**
5
+ * The caller's token for `naro`: the Authorization value with `Bearer `
6
+ * (any case) taken off, as the gate reads it (Naro worker `stripBearer`) —
7
+ * none when it is empty or one of the project's keys, and none when it is
8
+ * not printable ASCII without spaces. That last rule turns away another
9
+ * scheme (a webhook's `Basic …` has a space after the scheme) and anything
10
+ * `createClient`'s `accessToken` check would refuse — which would end every
11
+ * `naro` call in ACCESS_TOKEN_UNAVAILABLE instead of going as anon.
12
+ */
13
+ export declare function callerToken(request: Request): string | null;
@@ -0,0 +1,65 @@
1
+ import { type DatabaseShape, type NaroClient, type UntypedDatabase } from "./index.js";
2
+ export type NaroFunctionRole = "anon" | "authenticated" | "service";
3
+ /** The caller as the access token says (design F7): the email only once confirmed. */
4
+ export type NaroFunctionUser = {
5
+ id: string;
6
+ email: string | null;
7
+ emailVerified: boolean;
8
+ };
9
+ export type NaroFunctionContext<Database extends DatabaseShape<Database> = UntypedDatabase> = {
10
+ /** The signed-in caller, or null. */
11
+ user: NaroFunctionUser | null;
12
+ /** null when the function was deployed with --no-verify-jwt. */
13
+ role: NaroFunctionRole | null;
14
+ traceId: string;
15
+ /** The anon key and the caller's token: the tables' policies apply. */
16
+ naro: NaroClient<Database>;
17
+ /** The service key: skips every policy. Made the first time it is read. */
18
+ readonly naroAdmin: NaroClient<Database>;
19
+ };
20
+ export type NaroFunctionHandler<Database extends DatabaseShape<Database> = UntypedDatabase> = (request: Request, context: NaroFunctionContext<Database>) => Response | Promise<Response>;
21
+ /**
22
+ * Wraps a function's handler: `Deno.serve(withNaro(async (req, { user,
23
+ * naro }) => …))`, or `export default { fetch: withNaro(…) }`. Outside a
24
+ * Naro function — no context for the request — it refuses before the
25
+ * handler runs.
26
+ */
27
+ export declare function withNaro<Database extends DatabaseShape<Database> = UntypedDatabase>(handler: NaroFunctionHandler<Database>): (request: Request, ...rest: unknown[]) => Promise<Response>;
28
+ declare global {
29
+ namespace Deno {
30
+ interface ServeHandlerInfo {
31
+ remoteAddr: {
32
+ transport: "tcp";
33
+ hostname: string;
34
+ port: number;
35
+ };
36
+ completed: Promise<void>;
37
+ }
38
+ type ServeHandler = (request: Request, info: ServeHandlerInfo) => Response | Promise<Response>;
39
+ interface ServeOptions {
40
+ port?: number;
41
+ hostname?: string;
42
+ signal?: AbortSignal;
43
+ onListen?: (address: unknown) => void;
44
+ onError?: (error: unknown) => Response | Promise<Response>;
45
+ }
46
+ interface HttpServer {
47
+ finished: Promise<void>;
48
+ shutdown(): Promise<void>;
49
+ }
50
+ function serve(handler: ServeHandler): HttpServer;
51
+ function serve(options: ServeOptions, handler: ServeHandler): HttpServer;
52
+ function serve(options: ServeOptions & {
53
+ handler: ServeHandler;
54
+ }): HttpServer;
55
+ interface Env {
56
+ get(name: string): string | undefined;
57
+ has(name: string): boolean;
58
+ toObject(): Record<string, string>;
59
+ }
60
+ const env: Env;
61
+ }
62
+ var EdgeRuntime: {
63
+ waitUntil<T>(promise: Promise<T>): Promise<T>;
64
+ };
65
+ }
@@ -0,0 +1,74 @@
1
+ // src/functions-caller.ts
2
+ var WITH_NARO_MESSAGES = {
3
+ outside: "withNaro can only run inside a Naro function."
4
+ };
5
+ var KEY_PREFIXES = ["naro_pk_", "naro_sk_"];
6
+ function callerToken(request) {
7
+ const authorization = request.headers.get("authorization");
8
+ if (authorization === null) return null;
9
+ const token = authorization.replace(/^Bearer\s+/i, "");
10
+ if (token === "" || /^Bearer$/i.test(token)) return null;
11
+ if (!/^[\x21-\x7E]+$/.test(token)) return null;
12
+ return KEY_PREFIXES.some((prefix) => token.startsWith(prefix)) ? null : token;
13
+ }
14
+
15
+ // src/functions.ts
16
+ import {
17
+ createClient
18
+ } from "./index.js";
19
+ var CONTEXTS = Symbol.for("naro.functions.context");
20
+ var RUNTIME = Symbol.for("naro.functions.runtime");
21
+ function isCall(value) {
22
+ return typeof value === "object" && value !== null && typeof value.traceId === "string";
23
+ }
24
+ function callOf(request) {
25
+ const globals = globalThis;
26
+ const contexts = globals[CONTEXTS];
27
+ const mapped = contexts instanceof WeakMap ? contexts.get(request) : void 0;
28
+ if (mapped !== void 0) return mapped;
29
+ const runtime = globals[RUNTIME];
30
+ const store = runtime?.requests?.getStore?.();
31
+ if (isCall(store?.call)) {
32
+ const traceId = request.headers.get("x-naro-trace-id");
33
+ if (traceId === null || traceId === store.call.traceId) return store.call;
34
+ }
35
+ throw new Error(WITH_NARO_MESSAGES.outside);
36
+ }
37
+ function binding(name) {
38
+ const env = globalThis.Deno?.env;
39
+ const value = typeof env?.get === "function" ? env.get(name) : void 0;
40
+ if (typeof value !== "string" || value === "")
41
+ throw new Error(WITH_NARO_MESSAGES.outside);
42
+ return value;
43
+ }
44
+ function withNaro(handler) {
45
+ return async (request) => {
46
+ const call = callOf(request);
47
+ const url = binding("NARO_URL");
48
+ const anonKey = binding("NARO_ANON_KEY");
49
+ const token = callerToken(request);
50
+ const naro = createClient({
51
+ url,
52
+ anonKey,
53
+ accessToken: () => token
54
+ });
55
+ let admin;
56
+ const context = {
57
+ user: call.user === null ? null : { ...call.user },
58
+ role: call.role,
59
+ traceId: call.traceId,
60
+ naro,
61
+ get naroAdmin() {
62
+ admin ??= createClient({
63
+ url,
64
+ serviceKey: binding("NARO_SERVICE_KEY")
65
+ });
66
+ return admin;
67
+ }
68
+ };
69
+ return handler(request, context);
70
+ };
71
+ }
72
+ export {
73
+ withNaro
74
+ };
package/dist/http.d.ts CHANGED
@@ -7,6 +7,10 @@ import { type NaroError } from "./errors.js";
7
7
  * unbound `window.fetch` works.
8
8
  */
9
9
  export type FetchLike = (input: string, init: RequestInit) => Promise<Response>;
10
+ /** Where a client made with `accessToken` reads its token: a string, or null for none. */
11
+ export type AccessTokenSource = {
12
+ get(): Promise<string | null>;
13
+ };
10
14
  export type HttpConfig = {
11
15
  /** The project URL without a trailing slash. */
12
16
  baseUrl: string;
@@ -25,6 +29,13 @@ export type HttpConfig = {
25
29
  * its requests, so the access JWT is never renewed for them.
26
30
  */
27
31
  service?: boolean;
32
+ /**
33
+ * `createClient({ accessToken })` (design F20): the bearer of every data,
34
+ * storage and function request comes from the app's function, read for
35
+ * each one. Nothing is stored, renewed or cleared; `accessJwt` is then
36
+ * absent.
37
+ */
38
+ accessToken?: AccessTokenSource;
28
39
  };
29
40
  /**
30
41
  * A request that carries the access JWT renews it this long before its
@@ -54,7 +65,9 @@ export type CallOptions = {
54
65
  withToken: boolean;
55
66
  /**
56
67
  * Give up after this long: the request is aborted (where the runtime has
57
- * AbortController) and the call answers TIMEOUT. None by default.
68
+ * AbortController) and the call answers TIMEOUT. It is the whole call's —
69
+ * a token renewal and the one retry count in it (known-issues #339).
70
+ * None by default.
58
71
  */
59
72
  timeoutMs?: number;
60
73
  };
@@ -120,6 +133,57 @@ export type SendResult = {
120
133
  * envelope, or INVALID_RESPONSE — and every case resolves.
121
134
  */
122
135
  export declare function send(config: HttpConfig, options: SendOptions): Promise<SendResult>;
136
+ export type UnreadOptions = {
137
+ method: string;
138
+ /** Below `/_naro/v1`, e.g. `/functions/hello`. */
139
+ path: string;
140
+ /**
141
+ * The caller's headers. `apikey` is always the client's; an
142
+ * `authorization` of the caller's goes as it is, and the client's token
143
+ * is then neither sent nor renewed nor cleared for it (plan P3).
144
+ */
145
+ headers: Headers;
146
+ body?: BodyInit | undefined;
147
+ /**
148
+ * How long the whole call waits — renewals and the one retry included —
149
+ * up to the final answer's headers; the body is never timed.
150
+ */
151
+ timeoutMs?: number | undefined;
152
+ /** The caller's: calls the request off, and the body of an answer that came. */
153
+ signal?: AbortSignal | undefined;
154
+ /**
155
+ * Waited for before anything else, within the same deadline and signal —
156
+ * `auth.ready()`, a sign-in the page URL carried. Stopping the wait does
157
+ * not stop what it waits for: the exchange goes on, as a renewal does.
158
+ */
159
+ ready?: (() => Promise<unknown>) | undefined;
160
+ };
161
+ export type UnreadResult = {
162
+ response: Response;
163
+ } | {
164
+ failed: "network" | "timeout" | "aborted";
165
+ cause: unknown;
166
+ }
167
+ /** Not sent: the token it needed could not be had (a renewal's error). */
168
+ | {
169
+ refused: NaroError;
170
+ };
171
+ /**
172
+ * The third primitive (design F20), for a function's answer, which is the
173
+ * function's own — any status, any body: the response comes back unread.
174
+ * Identity and cookies are as `call`'s: `apikey`, the access JWT (renewed
175
+ * before it goes when about to expire), `credentials: "omit"`. A stored JWT
176
+ * is cleared only on Naro's own `401 INVALID_SESSION` — `x-relay-error:
177
+ * true` — never on a function's answer that happens to say the same; then
178
+ * it is renewed and the request sent once more, unless its body is a
179
+ * stream (read once) or the session changed meanwhile (`retryBearer`).
180
+ * The caller's `signal` answers aborted at once, a renewal in flight
181
+ * included (`untilStopped`; `send` stops the same way with `untilAborted`);
182
+ * `timeoutMs` is the whole call's
183
+ * (known-issues #339): the wait for `ready`, the renewals, both sends and
184
+ * the reading of a refusal share it, each send ending at its headers.
185
+ */
186
+ export declare function fetchUnread(config: HttpConfig, options: UnreadOptions): Promise<UnreadResult>;
123
187
  /**
124
188
  * The worker's 429s that say how long to wait. `RATE_LIMITED` is the
125
189
  * sign-in routes' per-address limiter; `WRITE_QUOTA_EXCEEDED` (PR C) is the
@@ -127,3 +191,11 @@ export declare function send(config: HttpConfig, options: SendOptions): Promise<
127
191
  * and creating or deleting users wait for 00:00 UTC, reads still work.
128
192
  */
129
193
  export declare const RETRY_AFTER_CODES: ReadonlySet<string>;
194
+ /**
195
+ * The sign-in limiter says in `X-Retry-After` how many seconds to wait, and
196
+ * the worker keeps the header on its 429 — and sends the same header, the
197
+ * seconds to 00:00 UTC, with WRITE_QUOTA_EXCEEDED. It becomes `details.retryAfter`
198
+ * (seconds), beside any details the worker sent. The worker lists it in
199
+ * Access-Control-Expose-Headers, so a page on another origin reads it too.
200
+ */
201
+ export declare function withRetryAfter(error: NaroError, headers: Headers): NaroError;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- export { createClient, NaroClient, type ClientOptions } from "./client.js";
1
+ export { createClient, NaroClient, type AccessToken, type ClientOptions, } from "./client.js";
2
+ export { FunctionsError, FunctionsFetchError, FunctionsHttpError, FunctionsRelayError, } from "./invoke/errors.js";
3
+ export type { FunctionInvokeOptions, FunctionsClient, FunctionsResponse, } from "./invoke/client.js";
2
4
  export type { AuthAdminApi } from "./auth/admin.js";
3
5
  export type { AuthClient } from "./auth/client.js";
4
6
  export type { AuthStorage } from "./auth/storage.js";