naro-js 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -0
- package/dist/client.d.ts +21 -2
- package/dist/errors.d.ts +6 -0
- package/dist/functions-caller.d.ts +13 -0
- package/dist/functions.d.ts +65 -0
- package/dist/functions.js +74 -0
- package/dist/http.d.ts +73 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +596 -51
- package/dist/invoke/body.d.ts +19 -0
- package/dist/invoke/client.d.ts +52 -0
- package/dist/invoke/errors.d.ts +38 -0
- package/package.json +15 -3
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
|
|
|
@@ -880,3 +881,135 @@ const naro = createClient({ url, anonKey, realtime: { WebSocket } });
|
|
|
880
881
|
|
|
881
882
|
The whole guide, with the policies, the close codes and the limits:
|
|
882
883
|
https://naro.sh/docs/realtime
|
|
884
|
+
|
|
885
|
+
## Functions
|
|
886
|
+
|
|
887
|
+
Server code your app calls by name, written the way Supabase Edge Functions
|
|
888
|
+
are (naro-js 0.7.0). `naro functions new hello` writes one and
|
|
889
|
+
`naro functions deploy hello` puts it live; the folders, imports, secrets and
|
|
890
|
+
limits are in https://naro.sh/docs/functions.
|
|
891
|
+
|
|
892
|
+
```ts
|
|
893
|
+
// naro/functions/hello/index.ts
|
|
894
|
+
import { withNaro } from "naro-js/functions";
|
|
895
|
+
|
|
896
|
+
Deno.serve(
|
|
897
|
+
withNaro(async (req, { user, naro }) => {
|
|
898
|
+
const { name } = await req.json();
|
|
899
|
+
// naro acts as the caller: the tables' policies apply.
|
|
900
|
+
const { data } = await naro
|
|
901
|
+
.from("profiles")
|
|
902
|
+
.select("*")
|
|
903
|
+
.eq("id", user?.id ?? "");
|
|
904
|
+
return Response.json({ message: `Hello ${name}!`, profile: data });
|
|
905
|
+
}),
|
|
906
|
+
);
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
```ts
|
|
910
|
+
// your app
|
|
911
|
+
import { createClient, FunctionsHttpError } from "naro-js";
|
|
912
|
+
|
|
913
|
+
const naro = createClient({ url, anonKey });
|
|
914
|
+
const { data, error } = await naro.functions.invoke("hello", {
|
|
915
|
+
body: { name: "Naro" },
|
|
916
|
+
});
|
|
917
|
+
if (error instanceof FunctionsHttpError)
|
|
918
|
+
console.log(await error.context.json());
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
- `invoke(name, { body, headers, method, timeout, signal })` calls
|
|
922
|
+
`<url>/_naro/v1/functions/<name>` (`POST` by default) with the anon key
|
|
923
|
+
and, when someone is signed in, their access token — renewed as a query's
|
|
924
|
+
is. `name` may carry a path and a query (`"hello/items?limit=5"`); only
|
|
925
|
+
the name is encoded. A name of `.` or `..`, or a path whose `..` would
|
|
926
|
+
take the URL out of `/functions/` once it is normalised, is refused
|
|
927
|
+
before anything is sent. `region` is taken and ignored.
|
|
928
|
+
- An object or array goes as JSON, a string as text, a Blob or bytes as
|
|
929
|
+
`application/octet-stream`; `FormData`, `URLSearchParams` and a
|
|
930
|
+
`ReadableStream` go as they are, and your own `content-type` wins; a
|
|
931
|
+
`GET` or `HEAD` call takes no body. The
|
|
932
|
+
answer is read by its content type: JSON, a Blob for
|
|
933
|
+
`application/octet-stream` and `application/pdf`, the `Response` itself
|
|
934
|
+
for `text/event-stream`, a `FormData` for `multipart/form-data`, text
|
|
935
|
+
otherwise, `null` for 204 and `HEAD`. `timeout` (ms) is the whole call's:
|
|
936
|
+
the wait for a sign-in the page URL carried, a token renewal and the one
|
|
937
|
+
retry of a call Naro refused for an expired token count in it, up to the
|
|
938
|
+
final answer's headers. Your `signal` stops the call at any of these
|
|
939
|
+
waits; the sign-in and the renewal go on for the rest of the page. The
|
|
940
|
+
body is not timed — pass a `signal` to stop a body that is slow to
|
|
941
|
+
arrive.
|
|
942
|
+
- It resolves to `{ data, error }` and never throws, with `response` beside
|
|
943
|
+
them when one came. `error` is one of supabase-js's classes, and has
|
|
944
|
+
`code`, `message` and `details` too: `FunctionsHttpError` — your function
|
|
945
|
+
answered 4xx or 5xx (`FUNCTION_HTTP_ERROR`; `error.context` is the unread
|
|
946
|
+
`Response`, and a function that threw answers
|
|
947
|
+
`500 FUNCTION_ERROR` in it); `FunctionsRelayError` — Naro answered for it
|
|
948
|
+
(`x-relay-error: true`): `FUNCTION_NOT_FOUND`, `INVALID_API_KEY`,
|
|
949
|
+
`INVALID_SESSION`, `FUNCTIONS_QUOTA_EXCEEDED` with `details.retryAfter`,
|
|
950
|
+
…, and `RELAY_ERROR` for a `HEAD` call; `FunctionsFetchError` — everything
|
|
951
|
+
else, `error.context` holding the cause:
|
|
952
|
+
- no answer came: `NETWORK_ERROR` (fetch threw, `details.cause`),
|
|
953
|
+
`TIMEOUT` (the call did not finish within `timeout` — a sign-in
|
|
954
|
+
exchange or a token renewal that outlasts it included, and Naro's
|
|
955
|
+
refusal of the first token whose body was still being read then, which
|
|
956
|
+
is dropped — a refusal of the renewed token is the call's answer, so it
|
|
957
|
+
comes back as `INVALID_SESSION` even past `timeout`), `ABORTED` (your
|
|
958
|
+
`signal`);
|
|
959
|
+
- nothing was sent: `INVALID_BODY` (a body JSON cannot hold),
|
|
960
|
+
`INVALID_OPTIONS` (a name, `timeout`, `headers` or `signal` it cannot
|
|
961
|
+
use, or a body with `GET` or `HEAD`), `ACCESS_TOKEN_UNAVAILABLE` (the
|
|
962
|
+
`accessToken` function of a client made with one failed), or the error
|
|
963
|
+
renewing the access token before the call answered — `INVALID_SESSION`
|
|
964
|
+
when the session is gone, `NETWORK_ERROR` when Naro could not be
|
|
965
|
+
reached;
|
|
966
|
+
- the function answered 2xx but its body could not be read as its
|
|
967
|
+
content type says: `INVALID_RESPONSE`, with `response` beside it.
|
|
968
|
+
- Only Naro's own `INVALID_SESSION` renews the access token and tries once
|
|
969
|
+
more; your function answering `{ "code": "INVALID_SESSION" }` signs nobody
|
|
970
|
+
out. A `HEAD` answer has no body, so Naro's refusal of a `HEAD` call is
|
|
971
|
+
`RELAY_ERROR` with the status alone, and a `HEAD` call is never renewed:
|
|
972
|
+
the `INVALID_SESSION` that would say so is not there to read.
|
|
973
|
+
- Inside a function, `withNaro(handler)` hands the handler `user` (`id`,
|
|
974
|
+
`email` — `null` until it is confirmed — and `emailVerified`, or `null`),
|
|
975
|
+
`role` (`anon`, `authenticated`, `service`, or `null` for a function
|
|
976
|
+
deployed with `--no-verify-jwt`), `traceId`, `naro` and `naroAdmin`. Naro
|
|
977
|
+
checked the caller before your code runs; `withNaro` does not check again.
|
|
978
|
+
`naro` sends `NARO_ANON_KEY` and the caller's token, so the tables'
|
|
979
|
+
policies apply; `naroAdmin` sends `NARO_SERVICE_KEY` and skips them, and
|
|
980
|
+
is made the first time you read it. Outside a Naro function `withNaro`
|
|
981
|
+
refuses with `withNaro can only run inside a Naro function.`
|
|
982
|
+
- `role` says how the caller came in; `naro` always sends the anon key — use
|
|
983
|
+
`naroAdmin` for service access.
|
|
984
|
+
- Inside a Naro function, `withNaro` finds the caller for the request it was
|
|
985
|
+
handed and for one your code rebuilt from it (`req.clone()`,
|
|
986
|
+
`new Request(req, …)`). `naro` sends the token of the request you hand
|
|
987
|
+
it, so a request you rebuild must copy `authorization` (or start from
|
|
988
|
+
`req.headers`): `new Request(req, { headers: { "x-extra": "1" } })` has no
|
|
989
|
+
`authorization`, and `naro` goes as anon while `context.user` still names
|
|
990
|
+
the caller. The caller is found per call, not per request: work one call
|
|
991
|
+
started and your module state keeps (a memoised promise, say) answers with
|
|
992
|
+
that call's caller when another call awaits it. A rebuilt request whose
|
|
993
|
+
`x-naro-trace-id` is not this call's is refused rather than run as this
|
|
994
|
+
call's caller. The trace id belongs to a chain of calls, not to one call
|
|
995
|
+
(a function calling another passes it on), so two calls sharing one are
|
|
996
|
+
not told apart.
|
|
997
|
+
- With verify off (`--no-verify-jwt`), Naro checked nobody: `user` is `null`
|
|
998
|
+
even when `naro` carries the caller's token (the Data API judges that
|
|
999
|
+
token on each query), and `naro.auth.*` is refused on that client. An
|
|
1000
|
+
`Authorization` that is not a bearer token — a webhook's `Basic …`, or
|
|
1001
|
+
anything but printable ASCII without spaces — gives `naro` no token: it
|
|
1002
|
+
goes as anon.
|
|
1003
|
+
- `naro` is `createClient({ url, anonKey, accessToken })`: `accessToken` is
|
|
1004
|
+
a function that returns the token to send, or `null`, read for every
|
|
1005
|
+
call. Such a client never stores, renews or clears a token; `naro.auth`
|
|
1006
|
+
and `naro.channel()` throw on it (Realtime signs in with a session —
|
|
1007
|
+
broadcast from a function with `naroAdmin`), and it cannot take a
|
|
1008
|
+
`serviceKey`.
|
|
1009
|
+
- `naro-js/functions` declares the globals Naro provides — `Deno.serve`,
|
|
1010
|
+
`Deno.env` and `EdgeRuntime.waitUntil` — so a function type-checks. Leave
|
|
1011
|
+
`naro/functions` out of your app's `tsconfig.json`
|
|
1012
|
+
(`"exclude": ["naro/functions"]`): a project that also loads Deno's or
|
|
1013
|
+
Next.js's own declarations can see them twice.
|
|
1014
|
+
- A function's request has no Cloudflare `request.cf`; the caller's address
|
|
1015
|
+
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,
|
|
92
|
-
*
|
|
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.
|
|
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";
|