naro-js 0.1.0 → 0.2.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 +83 -1
- package/dist/auth/admin.d.ts +23 -0
- package/dist/auth/client.d.ts +24 -5
- package/dist/auth/flows.d.ts +1 -1
- package/dist/client.d.ts +4 -1
- package/dist/errors.d.ts +15 -5
- package/dist/http.d.ts +5 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +137 -12
- package/dist/types.d.ts +86 -4
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -71,12 +71,94 @@ await naro.from("posts").delete().eq("id", 10);
|
|
|
71
71
|
`auth: { storage }` with your own `getItem` / `setItem` / `removeItem`.
|
|
72
72
|
- Types: `createClient<Database>()` with the `Database` that
|
|
73
73
|
`naro gen types` writes — see [Types](#types).
|
|
74
|
+
- Sign-in is naro Auth, the project's own (like Supabase Auth). Email and
|
|
75
|
+
password sign-up and sign-in need no setup: they work as soon as the
|
|
76
|
+
backend is on. Resend is needed only to send confirmation and password
|
|
77
|
+
reset emails.
|
|
78
|
+
- Admin accounts, closed sign-ups, sign-in by username and the password
|
|
79
|
+
length — see [Admin accounts and closed sign-ups](#admin-accounts-and-closed-sign-ups).
|
|
74
80
|
- Email and social sign-in — see [Sign-in flows](#sign-in-flows).
|
|
75
81
|
|
|
82
|
+
## Admin accounts and closed sign-ups
|
|
83
|
+
|
|
84
|
+
An app that only its admins sign in to: close sign-ups, then create the
|
|
85
|
+
accounts yourself. With sign-ups closed, `authenticated` in a policy means
|
|
86
|
+
exactly the users you created.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
naro backend auth signups off
|
|
90
|
+
echo "$ADMIN_PASSWORD" | naro backend users create --username admin --password-stdin
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
// In the app: sign in by username, or by email.
|
|
95
|
+
await naro.auth.signIn({ username: "admin", password });
|
|
96
|
+
await naro.auth.signIn({ email, password });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The same from your own server, with supabase-js's `auth.admin` names:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// Server only: the service key skips every policy.
|
|
103
|
+
const admin = createClient({
|
|
104
|
+
url: "https://my-app.naro.sh",
|
|
105
|
+
serviceKey: process.env.NARO_SERVICE_KEY,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const { data, error } = await admin.auth.admin.createUser({
|
|
109
|
+
username: "admin", // and/or email
|
|
110
|
+
password,
|
|
111
|
+
// emailConfirm: false — only with an email; users are confirmed otherwise
|
|
112
|
+
});
|
|
113
|
+
const {
|
|
114
|
+
data: { users, total },
|
|
115
|
+
} = await admin.auth.admin.listUsers({ page: 1, perPage: 50 });
|
|
116
|
+
await admin.auth.admin.deleteUser(data.user.id);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
- `createUser` takes an email, a username or both. The user is confirmed
|
|
120
|
+
and signs in at once — while sign-ups are closed, and while sign-in waits
|
|
121
|
+
for a confirmed email — unless `emailConfirm: false` (which needs an
|
|
122
|
+
email). A user with only a username answers `email: null`, here, in
|
|
123
|
+
`signIn` and in `getUser()`, and signs in with its username. Answers
|
|
124
|
+
carry `id`, `email`, `username`, `emailVerified` and `createdAt`, never a
|
|
125
|
+
password.
|
|
126
|
+
- `listUsers` is newest first, 50 a page unless `perPage` says (at most
|
|
127
|
+
1000), with the `total`. `deleteUser(id)` removes the user and its
|
|
128
|
+
sign-in methods and ends its sessions; an unknown id is
|
|
129
|
+
`404 USER_NOT_FOUND` ("No user has this id.").
|
|
130
|
+
- `auth.admin` works only on a client made with `serviceKey`. On an anon
|
|
131
|
+
client each call answers `403 PERMISSION_DENIED` ("Only the service key
|
|
132
|
+
can manage users.") without a request.
|
|
133
|
+
- A username is 3–30 letters, digits, `_` and `.`, and is matched in any
|
|
134
|
+
case (stored lowercased). `signUp({ email, password, username })` sets one
|
|
135
|
+
at sign-up.
|
|
136
|
+
- Closed sign-ups: every `signUp` answers `403 SIGNUPS_DISABLED` ("Sign-ups
|
|
137
|
+
are closed for this project."), and so does a first Google or GitHub
|
|
138
|
+
sign-in, in `getSessionFromUrl()`. Users that exist keep signing in, with
|
|
139
|
+
a password or a provider. `naro backend auth signups on` opens them again.
|
|
140
|
+
- While sign-ups are open, a sign-up with a username someone has answers
|
|
141
|
+
`409 USERNAME_TAKEN` ("That username is already taken.") — so anyone can
|
|
142
|
+
learn which usernames exist, as fast as sign-up's per-address rate limit
|
|
143
|
+
lets them. With sign-ups closed, every sign-up answers
|
|
144
|
+
`403 SIGNUPS_DISABLED` first. `createUser` answers `USERNAME_TAKEN` too,
|
|
145
|
+
and `422 USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL` for an email in use.
|
|
146
|
+
- Passwords take at least 6 characters unless
|
|
147
|
+
`naro backend auth password --min-length <n>` (6–128) says more. A
|
|
148
|
+
shorter one, on sign-up, `createUser` or a reset, answers
|
|
149
|
+
`400 PASSWORD_TOO_SHORT` naming it: "Use at least 10 characters." Sign-in
|
|
150
|
+
never checks the length, so raising it locks nobody out.
|
|
151
|
+
- Creating and deleting users are writes: past the project's daily write
|
|
152
|
+
limit they answer `429 WRITE_QUOTA_EXCEEDED`, like sign-up.
|
|
153
|
+
- A backend turned on before these existed gets them with its next upload:
|
|
154
|
+
run `naro backend enable` once. Until then `auth.admin` and
|
|
155
|
+
`signIn({ username })` answer `404 NOT_FOUND`.
|
|
156
|
+
|
|
76
157
|
## Sign-in flows
|
|
77
158
|
|
|
78
159
|
Google and GitHub sign-in, email confirmation and password reset, once the
|
|
79
|
-
project has set them up (`naro backend auth`).
|
|
160
|
+
project has set them up (`naro backend auth`). Email and password sign-up
|
|
161
|
+
and sign-in need none of it. The names follow supabase-js.
|
|
80
162
|
|
|
81
163
|
```ts
|
|
82
164
|
// Sends the browser to the project's /_naro/v1/auth/authorize/github, then
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type HttpConfig } from "../http.js";
|
|
2
|
+
import type { AdminUserAttributes, AdminUserListResponse, AdminUserResponse, PageParams } from "../types.js";
|
|
3
|
+
export declare class AuthAdminApi {
|
|
4
|
+
#private;
|
|
5
|
+
constructor(http: HttpConfig, serviceKey: boolean);
|
|
6
|
+
/**
|
|
7
|
+
* Creates a user with a password: an email, a username or both. It is
|
|
8
|
+
* made while sign-ups are closed too, and is confirmed unless
|
|
9
|
+
* `emailConfirm: false`. A username-only user answers `email: null` and
|
|
10
|
+
* signs in with `signIn({ username, password })`. Refusals come as the
|
|
11
|
+
* worker says them: USERNAME_TAKEN (409),
|
|
12
|
+
* USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL (422), PASSWORD_TOO_SHORT ("Use at
|
|
13
|
+
* least {n} characters."), VALIDATION_ERROR, WRITE_QUOTA_EXCEEDED (429).
|
|
14
|
+
*/
|
|
15
|
+
createUser(attributes: AdminUserAttributes): Promise<AdminUserResponse>;
|
|
16
|
+
/** The project's users, newest first: page 1 and 50 a page unless given. */
|
|
17
|
+
listUsers(params?: PageParams): Promise<AdminUserListResponse>;
|
|
18
|
+
/**
|
|
19
|
+
* Deletes a user and its sign-in methods, and ends its sessions. Answers
|
|
20
|
+
* the user it was; USER_NOT_FOUND (404) for an id that is no user.
|
|
21
|
+
*/
|
|
22
|
+
deleteUser(id: string): Promise<AdminUserResponse>;
|
|
23
|
+
}
|
package/dist/auth/client.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type NaroError } from "../errors.js";
|
|
2
2
|
import { type HttpConfig } from "../http.js";
|
|
3
3
|
import type { AuthActionResponse, EmailRedirectOptions, OAuthResponse, SessionResponse, SignInCredentials, SignInWithOAuthOptions, SignOutResponse, SignUpCredentials, UrlSessionResponse, UserAttributes, UserResponse } from "../types.js";
|
|
4
|
+
import { AuthAdminApi } from "./admin.js";
|
|
4
5
|
import type { FlowVerifiers } from "./flows.js";
|
|
5
6
|
import type { RecoveryToken } from "./recovery.js";
|
|
6
|
-
/**
|
|
7
|
+
/** Sign-up needs a `name`; the email's local part stands in. */
|
|
7
8
|
export declare function defaultName(email: string): string;
|
|
8
9
|
export type AuthFlowStores = {
|
|
9
10
|
/** The PKCE verifiers of the provider sign-ins this browser began. */
|
|
@@ -24,24 +25,42 @@ export type AuthFlowStores = {
|
|
|
24
25
|
storage: object;
|
|
25
26
|
key: string;
|
|
26
27
|
};
|
|
28
|
+
/** Made with the service key: `admin` sends its requests. */
|
|
29
|
+
serviceKey: boolean;
|
|
27
30
|
};
|
|
28
31
|
/** How long the code exchange a sign-in redirect starts may take. */
|
|
29
32
|
export declare const EXCHANGE_TIMEOUT_MS = 15000;
|
|
30
33
|
/**
|
|
31
|
-
* `naro.auth` —
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
34
|
+
* `naro.auth` — naro Auth, the project's own sign-in (like Supabase Auth):
|
|
35
|
+
* email or username and password, email confirmation, password reset and
|
|
36
|
+
* provider sign-in through the worker's `/_naro/v1/auth/*` routes (spec
|
|
37
|
+
* §8.1, §13; P2-6 §13.3; 2026-09-26 §2.3). Email and password work with no
|
|
38
|
+
* setup; email is needed only to send confirmation and reset links. Every
|
|
39
|
+
* method resolves; none throws.
|
|
35
40
|
*/
|
|
36
41
|
export declare class AuthClient {
|
|
37
42
|
#private;
|
|
43
|
+
/**
|
|
44
|
+
* The service key's user management — createUser, listUsers, deleteUser,
|
|
45
|
+
* as in supabase-js (spec 2026-09-26 §2.2). Only on a client made with
|
|
46
|
+
* `serviceKey`, on a server.
|
|
47
|
+
*/
|
|
48
|
+
readonly admin: AuthAdminApi;
|
|
38
49
|
constructor(http: HttpConfig, flow: AuthFlowStores);
|
|
39
50
|
/**
|
|
40
51
|
* Creates the account and signs in; the token is stored for later calls.
|
|
41
52
|
* When the project requires a confirmed email there is no session yet —
|
|
42
53
|
* `user` comes back, and after the confirmation link the user signs in.
|
|
54
|
+
* While the project's sign-ups are closed every sign-up answers 403
|
|
55
|
+
* SIGNUPS_DISABLED; while they are open, a taken `username` answers 409
|
|
56
|
+
* USERNAME_TAKEN.
|
|
43
57
|
*/
|
|
44
58
|
signUp(credentials: SignUpCredentials): Promise<UserResponse>;
|
|
59
|
+
/**
|
|
60
|
+
* Signs in with an email and password, or a username and password (spec
|
|
61
|
+
* 2026-09-26 §2.3); the token is stored for later calls. A wrong password
|
|
62
|
+
* answers 401 INVALID_EMAIL_OR_PASSWORD, or INVALID_USERNAME_OR_PASSWORD.
|
|
63
|
+
*/
|
|
45
64
|
signIn(credentials: SignInCredentials): Promise<UserResponse>;
|
|
46
65
|
/**
|
|
47
66
|
* Starts a Google or GitHub sign-in: sends the browser, with a top-level
|
package/dist/auth/flows.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TokenStore } from "./storage.js";
|
|
2
|
-
/** A flow's life: the worker's flow row and
|
|
2
|
+
/** A flow's life: the worker's flow row and its sign-in state. */
|
|
3
3
|
export declare const FLOW_TTL_MS: number;
|
|
4
4
|
/** More in flight than anyone clicks; the oldest go first. */
|
|
5
5
|
export declare const MAX_FLOWS = 10;
|
package/dist/client.d.ts
CHANGED
|
@@ -46,7 +46,10 @@ export declare const CONFIG_MESSAGES: {
|
|
|
46
46
|
};
|
|
47
47
|
export declare class NaroClient<Database extends DatabaseShape<Database>> {
|
|
48
48
|
#private;
|
|
49
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Sign-up, sign-in, sign-out and the current user (spec §13); with the
|
|
51
|
+
* service key, `auth.admin` manages users.
|
|
52
|
+
*/
|
|
50
53
|
readonly auth: AuthClient;
|
|
51
54
|
constructor(options: ClientOptions);
|
|
52
55
|
/** Start a query on `table`: `.select()`, `.insert()`, `.update()` or `.delete()`. */
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import type { AuthErrorCode } from "./types.js";
|
|
1
2
|
/**
|
|
2
3
|
* Every failure the SDK hands back (spec §15). The Naro worker's errors
|
|
3
4
|
* (spec §14) arrive as they are; the SDK makes its own only for what never
|
|
4
5
|
* got a readable answer from the worker, and for queries it can tell are
|
|
5
6
|
* malformed before sending them. The messages are a contract — spec §15's
|
|
6
7
|
* "(P1-4 구현 결정)" block quotes them.
|
|
8
|
+
*
|
|
9
|
+
* `code` can be any string. The sign-in and user codes an app branches on
|
|
10
|
+
* (`AuthErrorCode`) are spelled out, so an editor completes them.
|
|
7
11
|
*/
|
|
8
12
|
export type NaroError = {
|
|
9
|
-
code: string;
|
|
13
|
+
code: AuthErrorCode | (string & {});
|
|
10
14
|
message: string;
|
|
11
15
|
details?: unknown;
|
|
12
16
|
};
|
|
@@ -37,15 +41,21 @@ export declare function codeVerifierMissing(): NaroError;
|
|
|
37
41
|
export declare function passwordRecoveryRequired(): NaroError;
|
|
38
42
|
/** `signInWithOAuth` with a provider the worker does not know. */
|
|
39
43
|
export declare function invalidProvider(): NaroError;
|
|
44
|
+
/**
|
|
45
|
+
* `naro.auth.admin` on a client made with the anon key (spec 2026-09-26
|
|
46
|
+
* admin users §2.2): the worker's own refusal, word for word, without the
|
|
47
|
+
* request — the answer would be the same.
|
|
48
|
+
*/
|
|
49
|
+
export declare function serviceKeyRequired(): NaroError;
|
|
40
50
|
/** No `crypto.subtle`: an http page that is not localhost. */
|
|
41
51
|
export declare function cryptoUnavailable(): NaroError;
|
|
42
52
|
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
43
53
|
/**
|
|
44
54
|
* The error a response carries, or null when it carries none. Every error
|
|
45
55
|
* under `/_naro/v1` comes as `{ data: null, error: { code, message,
|
|
46
|
-
* details? } }` — the
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
56
|
+
* details? } }` — the sign-in routes' too (spec §14, P1-2 리뷰 반영). Their
|
|
57
|
+
* success bodies are not wrapped, so a 2xx without an `error` is not an
|
|
58
|
+
* error; a failing status without the envelope is not a Naro answer at all
|
|
59
|
+
* (a proxy's HTML page, the app's own 404).
|
|
50
60
|
*/
|
|
51
61
|
export declare function errorFromBody(status: number, body: unknown): NaroError | null;
|
package/dist/http.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type HttpConfig = {
|
|
|
15
15
|
tokens: TokenStore;
|
|
16
16
|
};
|
|
17
17
|
export type CallOptions = {
|
|
18
|
-
method: "GET" | "POST";
|
|
18
|
+
method: "GET" | "POST" | "DELETE";
|
|
19
19
|
/** Below `/_naro/v1`, e.g. `/data/query`. */
|
|
20
20
|
path: string;
|
|
21
21
|
/** Sent as JSON. */
|
|
@@ -54,8 +54,9 @@ export type CallResult = {
|
|
|
54
54
|
*/
|
|
55
55
|
export declare function call(config: HttpConfig, options: CallOptions): Promise<CallResult>;
|
|
56
56
|
/**
|
|
57
|
-
* The worker's two 429s. `RATE_LIMITED` is
|
|
58
|
-
* `WRITE_QUOTA_EXCEEDED` (PR C) is the project over its daily
|
|
59
|
-
* limit — Data API writes
|
|
57
|
+
* The worker's two 429s. `RATE_LIMITED` is the sign-in routes' per-address
|
|
58
|
+
* limiter; `WRITE_QUOTA_EXCEEDED` (PR C) is the project over its daily
|
|
59
|
+
* database write limit — Data API writes, sign-up and creating or deleting
|
|
60
|
+
* users wait for 00:00 UTC, reads still work.
|
|
60
61
|
*/
|
|
61
62
|
export declare const RETRY_AFTER_CODES: ReadonlySet<string>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export { createClient, NaroClient, type ClientOptions } from "./client.js";
|
|
2
|
+
export type { AuthAdminApi } from "./auth/admin.js";
|
|
2
3
|
export type { AuthClient } from "./auth/client.js";
|
|
3
4
|
export type { AuthStorage } from "./auth/storage.js";
|
|
4
5
|
export type { NaroError } from "./errors.js";
|
|
5
6
|
export type { FetchLike } from "./http.js";
|
|
6
7
|
export type { ArrayValues, FilterGroup, FilterGroupCallback, InsertBuilder, MatchQuery, MutationBuilder, RawFilterValue, SelectBuilder, TableBuilder, } from "./query/builder.js";
|
|
7
8
|
export { quoteFilterValue, type FilterOperatorName } from "./query/filters.js";
|
|
8
|
-
export type { AuthActionResponse, AuthFlowType, AuthResponse, DatabaseShape, EmailRedirectOptions, OAuthProvider, OAuthResponse, SignInWithOAuthOptions, UrlSessionResponse, UserAttributes, GenericTable, GenericView, NaroResponse, OrderOptions, Session, SessionResponse, SignInCredentials, SignOutResponse, SignUpCredentials, UntypedDatabase, User, UserResponse, } from "./types.js";
|
|
9
|
+
export type { AdminUser, AdminUserAttributes, AdminUserListResponse, AdminUserResponse, AuthActionResponse, AuthErrorCode, AuthFlowType, AuthResponse, DatabaseShape, EmailRedirectOptions, OAuthProvider, OAuthResponse, SignInWithOAuthOptions, UrlSessionResponse, UserAttributes, GenericTable, GenericView, NaroResponse, OrderOptions, PageParams, Session, SessionResponse, SignInCredentials, SignOutResponse, SignUpCredentials, UntypedDatabase, User, UserResponse, } from "./types.js";
|
|
9
10
|
export type { Filter, FilterNode, FilterOperator, Order, Query, Row, Value, } from "./wire.js";
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,12 @@ function invalidProvider() {
|
|
|
49
49
|
message: "provider must be google or github."
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
+
function serviceKeyRequired() {
|
|
53
|
+
return {
|
|
54
|
+
code: "PERMISSION_DENIED",
|
|
55
|
+
message: "Only the service key can manage users."
|
|
56
|
+
};
|
|
57
|
+
}
|
|
52
58
|
function cryptoUnavailable() {
|
|
53
59
|
return {
|
|
54
60
|
code: "CRYPTO_UNAVAILABLE",
|
|
@@ -128,9 +134,10 @@ async function call(config, options) {
|
|
|
128
134
|
const init = {
|
|
129
135
|
method: options.method,
|
|
130
136
|
headers,
|
|
131
|
-
// Identity is the bearer token alone.
|
|
132
|
-
// cookie (spec §13); a request that carried it would let any site
|
|
133
|
-
// as the user. Workers accept the field (measured on workerd
|
|
137
|
+
// Identity is the bearer token alone. The sign-in routes also set a
|
|
138
|
+
// session cookie (spec §13); a request that carried it would let any site
|
|
139
|
+
// act as the user. Workers accept the field (measured on workerd
|
|
140
|
+
// 1.20260815).
|
|
134
141
|
credentials: "omit"
|
|
135
142
|
};
|
|
136
143
|
if (payload !== void 0) {
|
|
@@ -196,6 +203,90 @@ function isKnownServer(scope = globalThis) {
|
|
|
196
203
|
return hasServerSignal(g) && !hasDeviceSignal(g);
|
|
197
204
|
}
|
|
198
205
|
|
|
206
|
+
// src/auth/admin.ts
|
|
207
|
+
var USERS_PATH = "/auth/admin/users";
|
|
208
|
+
function dataOf(result) {
|
|
209
|
+
if (result.error !== null) return null;
|
|
210
|
+
const { body } = result.answer;
|
|
211
|
+
return isRecord(body) && isRecord(body.data) ? body.data : null;
|
|
212
|
+
}
|
|
213
|
+
function errorOf(result) {
|
|
214
|
+
if (result.error !== null) return result.error;
|
|
215
|
+
return invalidResponse(result.answer.status);
|
|
216
|
+
}
|
|
217
|
+
function userOf(value) {
|
|
218
|
+
return isRecord(value) && typeof value.id === "string" ? value : null;
|
|
219
|
+
}
|
|
220
|
+
var AuthAdminApi = class {
|
|
221
|
+
#http;
|
|
222
|
+
#serviceKey;
|
|
223
|
+
constructor(http, serviceKey) {
|
|
224
|
+
this.#http = http;
|
|
225
|
+
this.#serviceKey = serviceKey;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Creates a user with a password: an email, a username or both. It is
|
|
229
|
+
* made while sign-ups are closed too, and is confirmed unless
|
|
230
|
+
* `emailConfirm: false`. A username-only user answers `email: null` and
|
|
231
|
+
* signs in with `signIn({ username, password })`. Refusals come as the
|
|
232
|
+
* worker says them: USERNAME_TAKEN (409),
|
|
233
|
+
* USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL (422), PASSWORD_TOO_SHORT ("Use at
|
|
234
|
+
* least {n} characters."), VALIDATION_ERROR, WRITE_QUOTA_EXCEEDED (429).
|
|
235
|
+
*/
|
|
236
|
+
async createUser(attributes) {
|
|
237
|
+
if (!this.#serviceKey)
|
|
238
|
+
return { data: { user: null }, error: serviceKeyRequired() };
|
|
239
|
+
const result = await call(this.#http, {
|
|
240
|
+
method: "POST",
|
|
241
|
+
path: USERS_PATH,
|
|
242
|
+
body: attributes,
|
|
243
|
+
withToken: false
|
|
244
|
+
});
|
|
245
|
+
const user = userOf(dataOf(result)?.user);
|
|
246
|
+
if (user === null) return { data: { user: null }, error: errorOf(result) };
|
|
247
|
+
return { data: { user }, error: null };
|
|
248
|
+
}
|
|
249
|
+
/** The project's users, newest first: page 1 and 50 a page unless given. */
|
|
250
|
+
async listUsers(params = {}) {
|
|
251
|
+
const none = { users: [], total: 0 };
|
|
252
|
+
if (!this.#serviceKey) return { data: none, error: serviceKeyRequired() };
|
|
253
|
+
const query = new URLSearchParams();
|
|
254
|
+
if (params.page !== void 0) query.set("page", String(params.page));
|
|
255
|
+
if (params.perPage !== void 0)
|
|
256
|
+
query.set("perPage", String(params.perPage));
|
|
257
|
+
const search = query.toString();
|
|
258
|
+
const result = await call(this.#http, {
|
|
259
|
+
method: "GET",
|
|
260
|
+
path: search === "" ? USERS_PATH : `${USERS_PATH}?${search}`,
|
|
261
|
+
withToken: false
|
|
262
|
+
});
|
|
263
|
+
const data = dataOf(result);
|
|
264
|
+
const users = Array.isArray(data?.users) ? data.users.map(userOf) : null;
|
|
265
|
+
if (!data || users === null || users.includes(null) || typeof data.total !== "number")
|
|
266
|
+
return { data: none, error: errorOf(result) };
|
|
267
|
+
return {
|
|
268
|
+
data: { users, total: data.total },
|
|
269
|
+
error: null
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Deletes a user and its sign-in methods, and ends its sessions. Answers
|
|
274
|
+
* the user it was; USER_NOT_FOUND (404) for an id that is no user.
|
|
275
|
+
*/
|
|
276
|
+
async deleteUser(id) {
|
|
277
|
+
if (!this.#serviceKey)
|
|
278
|
+
return { data: { user: null }, error: serviceKeyRequired() };
|
|
279
|
+
const result = await call(this.#http, {
|
|
280
|
+
method: "DELETE",
|
|
281
|
+
path: `${USERS_PATH}/${encodeURIComponent(String(id))}`,
|
|
282
|
+
withToken: false
|
|
283
|
+
});
|
|
284
|
+
const user = userOf(dataOf(result)?.user);
|
|
285
|
+
if (user === null) return { data: { user: null }, error: errorOf(result) };
|
|
286
|
+
return { data: { user }, error: null };
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
199
290
|
// src/auth/flow-errors.ts
|
|
200
291
|
var CODE = /^[A-Z0-9_]{1,64}$/;
|
|
201
292
|
var INVALID_CONFIRMATION = "The confirmation link is not valid. Ask for a new one.";
|
|
@@ -207,6 +298,9 @@ var MESSAGES = {
|
|
|
207
298
|
OAUTH_ACCOUNT_NOT_LINKED: "An account with this email already exists and is not linked to this provider. Sign in the way you signed up.",
|
|
208
299
|
PROVIDER_NOT_CONFIGURED: "Signing in with this provider is not set up for this project.",
|
|
209
300
|
RATE_LIMITED: "Too many sign-in attempts. Wait a few seconds and try again.",
|
|
301
|
+
// A first Google or GitHub sign-in while the project's sign-ups are
|
|
302
|
+
// closed (spec 2026-09-26 §2.1): the worker's own words.
|
|
303
|
+
SIGNUPS_DISABLED: "Sign-ups are closed for this project.",
|
|
210
304
|
INTERNAL_ERROR: "Something went wrong. Try again.",
|
|
211
305
|
PASSWORD_RESET_REQUIRED: "Your email is confirmed. Someone else also signed up with it before it was confirmed, so its password was cleared: choose a new one with a password reset.",
|
|
212
306
|
TOKEN_EXPIRED: "The confirmation link has expired. Ask for a new one.",
|
|
@@ -326,6 +420,12 @@ function share(shared, pending) {
|
|
|
326
420
|
});
|
|
327
421
|
}
|
|
328
422
|
var AuthClient = class {
|
|
423
|
+
/**
|
|
424
|
+
* The service key's user management — createUser, listUsers, deleteUser,
|
|
425
|
+
* as in supabase-js (spec 2026-09-26 §2.2). Only on a client made with
|
|
426
|
+
* `serviceKey`, on a server.
|
|
427
|
+
*/
|
|
428
|
+
admin;
|
|
329
429
|
#http;
|
|
330
430
|
#flow;
|
|
331
431
|
/** The last `getSessionFromUrl()`, for callers after detection ran it. */
|
|
@@ -333,6 +433,7 @@ var AuthClient = class {
|
|
|
333
433
|
constructor(http, flow) {
|
|
334
434
|
this.#http = http;
|
|
335
435
|
this.#flow = flow;
|
|
436
|
+
this.admin = new AuthAdminApi(http, flow.serviceKey);
|
|
336
437
|
if (flow.detectSessionInUrl) {
|
|
337
438
|
if (naroFragment() !== null) void this.getSessionFromUrl();
|
|
338
439
|
else this.#fromUrl = sharedInFlight(flow.shared);
|
|
@@ -342,18 +443,37 @@ var AuthClient = class {
|
|
|
342
443
|
* Creates the account and signs in; the token is stored for later calls.
|
|
343
444
|
* When the project requires a confirmed email there is no session yet —
|
|
344
445
|
* `user` comes back, and after the confirmation link the user signs in.
|
|
446
|
+
* While the project's sign-ups are closed every sign-up answers 403
|
|
447
|
+
* SIGNUPS_DISABLED; while they are open, a taken `username` answers 409
|
|
448
|
+
* USERNAME_TAKEN.
|
|
345
449
|
*/
|
|
346
450
|
async signUp(credentials) {
|
|
347
|
-
const { email, password } = credentials;
|
|
348
|
-
const name = credentials.name ?? defaultName(email);
|
|
451
|
+
const { email, password, username } = credentials;
|
|
452
|
+
const name = credentials.name ?? username ?? defaultName(email);
|
|
349
453
|
const body = { email, password, name };
|
|
454
|
+
if (username !== void 0) body.username = username;
|
|
350
455
|
if (credentials.redirectTo !== void 0)
|
|
351
456
|
body.callbackURL = credentials.redirectTo;
|
|
352
457
|
return this.#signIn("/auth/sign-up/email", body, false);
|
|
353
458
|
}
|
|
459
|
+
/**
|
|
460
|
+
* Signs in with an email and password, or a username and password (spec
|
|
461
|
+
* 2026-09-26 §2.3); the token is stored for later calls. A wrong password
|
|
462
|
+
* answers 401 INVALID_EMAIL_OR_PASSWORD, or INVALID_USERNAME_OR_PASSWORD.
|
|
463
|
+
*/
|
|
354
464
|
async signIn(credentials) {
|
|
355
|
-
const {
|
|
356
|
-
|
|
465
|
+
const { password } = credentials;
|
|
466
|
+
if (credentials.username !== void 0)
|
|
467
|
+
return this.#signIn(
|
|
468
|
+
"/auth/sign-in/username",
|
|
469
|
+
{ username: credentials.username, password },
|
|
470
|
+
true
|
|
471
|
+
);
|
|
472
|
+
return this.#signIn(
|
|
473
|
+
"/auth/sign-in/email",
|
|
474
|
+
{ email: credentials.email, password },
|
|
475
|
+
true
|
|
476
|
+
);
|
|
357
477
|
}
|
|
358
478
|
/**
|
|
359
479
|
* Starts a Google or GitHub sign-in: sends the browser, with a top-level
|
|
@@ -1668,9 +1788,10 @@ function apiKeyOf(options) {
|
|
|
1668
1788
|
if (hasAnon && hasService) throw new Error(CONFIG_MESSAGES.bothKeys);
|
|
1669
1789
|
const key = hasService ? serviceKey : hasAnon ? anonKey : void 0;
|
|
1670
1790
|
if (key === void 0) throw new Error(CONFIG_MESSAGES.key);
|
|
1671
|
-
|
|
1791
|
+
const service = hasService || key.startsWith(SERVICE_KEY_PREFIX);
|
|
1792
|
+
if (service && !isKnownServer())
|
|
1672
1793
|
throw new Error(CONFIG_MESSAGES.serviceKeyOffServer);
|
|
1673
|
-
return key;
|
|
1794
|
+
return { key, service };
|
|
1674
1795
|
}
|
|
1675
1796
|
function queryRunner(http, auth) {
|
|
1676
1797
|
return async (query) => {
|
|
@@ -1689,14 +1810,17 @@ function queryRunner(http, auth) {
|
|
|
1689
1810
|
};
|
|
1690
1811
|
}
|
|
1691
1812
|
var NaroClient = class {
|
|
1692
|
-
/**
|
|
1813
|
+
/**
|
|
1814
|
+
* Sign-up, sign-in, sign-out and the current user (spec §13); with the
|
|
1815
|
+
* service key, `auth.admin` manages users.
|
|
1816
|
+
*/
|
|
1693
1817
|
auth;
|
|
1694
1818
|
#run;
|
|
1695
1819
|
// naro.storage — Phase 3 (spec §18): R2 storage lands here, beside `auth`
|
|
1696
1820
|
// and `from()`. Deliberately absent in Phase 1: not implemented, not typed.
|
|
1697
1821
|
constructor(options) {
|
|
1698
1822
|
const baseUrl = baseUrlOf(options.url);
|
|
1699
|
-
const apiKey = apiKeyOf(options);
|
|
1823
|
+
const { key: apiKey, service } = apiKeyOf(options);
|
|
1700
1824
|
if (options.fetch === void 0 && typeof globalThis.fetch !== "function")
|
|
1701
1825
|
throw new Error(CONFIG_MESSAGES.fetch);
|
|
1702
1826
|
const storage = options.auth?.storage ?? defaultStorage();
|
|
@@ -1714,7 +1838,8 @@ var NaroClient = class {
|
|
|
1714
1838
|
new TokenStore(storage, recoveryKey(baseUrl))
|
|
1715
1839
|
),
|
|
1716
1840
|
detectSessionInUrl: options.auth?.detectSessionInUrl ?? true,
|
|
1717
|
-
shared: { storage, key: storageKey(baseUrl) }
|
|
1841
|
+
shared: { storage, key: storageKey(baseUrl) },
|
|
1842
|
+
serviceKey: service
|
|
1718
1843
|
});
|
|
1719
1844
|
this.#run = queryRunner(http, this.auth);
|
|
1720
1845
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -81,10 +81,19 @@ export type OrderOptions = {
|
|
|
81
81
|
/** Where NULLs go. Omitted, SQLite's default: first ascending, last descending. */
|
|
82
82
|
nullsFirst?: boolean;
|
|
83
83
|
};
|
|
84
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* The signed-in user, as naro Auth answers it (spec §13): dates are ISO
|
|
86
|
+
* strings. `email` is null for a user created with only a username
|
|
87
|
+
* (`auth.admin.createUser({ username, password })`).
|
|
88
|
+
*/
|
|
85
89
|
export type User = {
|
|
86
90
|
id: string;
|
|
87
|
-
email: string;
|
|
91
|
+
email: string | null;
|
|
92
|
+
/**
|
|
93
|
+
* Lowercased; null for a user without one. Absent from a backend whose
|
|
94
|
+
* Naro worker predates usernames (`naro backend enable` updates it).
|
|
95
|
+
*/
|
|
96
|
+
username?: string | null;
|
|
88
97
|
name: string;
|
|
89
98
|
emailVerified: boolean;
|
|
90
99
|
image?: string | null;
|
|
@@ -92,7 +101,7 @@ export type User = {
|
|
|
92
101
|
updatedAt: string;
|
|
93
102
|
[field: string]: unknown;
|
|
94
103
|
};
|
|
95
|
-
/**
|
|
104
|
+
/** The session, as naro Auth answers it. */
|
|
96
105
|
export type Session = {
|
|
97
106
|
id: string;
|
|
98
107
|
userId: string;
|
|
@@ -124,7 +133,16 @@ export type SignOutResponse = {
|
|
|
124
133
|
export type SignUpCredentials = {
|
|
125
134
|
email: string;
|
|
126
135
|
password: string;
|
|
127
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* A name to sign in by as well as the email (spec 2026-09-26 §2.3): 3–30
|
|
138
|
+
* letters, digits, `_` and `.`, stored lowercased. One that is taken
|
|
139
|
+
* answers 409 USERNAME_TAKEN.
|
|
140
|
+
*/
|
|
141
|
+
username?: string;
|
|
142
|
+
/**
|
|
143
|
+
* Every user has a display name; omitted, the username is sent, or else
|
|
144
|
+
* the email's local part.
|
|
145
|
+
*/
|
|
128
146
|
name?: string;
|
|
129
147
|
/**
|
|
130
148
|
* Where the confirmation link sends the browser back to, when the project
|
|
@@ -133,8 +151,17 @@ export type SignUpCredentials = {
|
|
|
133
151
|
*/
|
|
134
152
|
redirectTo?: string;
|
|
135
153
|
};
|
|
154
|
+
/**
|
|
155
|
+
* An email and its password, or a username and its password (spec
|
|
156
|
+
* 2026-09-26 §2.3). A username is matched in any case.
|
|
157
|
+
*/
|
|
136
158
|
export type SignInCredentials = {
|
|
137
159
|
email: string;
|
|
160
|
+
username?: undefined;
|
|
161
|
+
password: string;
|
|
162
|
+
} | {
|
|
163
|
+
username: string;
|
|
164
|
+
email?: undefined;
|
|
138
165
|
password: string;
|
|
139
166
|
};
|
|
140
167
|
export type OAuthProvider = "google" | "github";
|
|
@@ -183,4 +210,59 @@ export type AuthActionResponse = {
|
|
|
183
210
|
data: null;
|
|
184
211
|
error: NaroError | null;
|
|
185
212
|
};
|
|
213
|
+
/**
|
|
214
|
+
* A user as `naro.auth.admin` answers it — never a password or a hash.
|
|
215
|
+
* `email` is null for a user created with only a username.
|
|
216
|
+
*/
|
|
217
|
+
export type AdminUser = {
|
|
218
|
+
id: string;
|
|
219
|
+
email: string | null;
|
|
220
|
+
/** Lowercased; null for a user without one. */
|
|
221
|
+
username: string | null;
|
|
222
|
+
emailVerified: boolean;
|
|
223
|
+
/** ISO 8601. */
|
|
224
|
+
createdAt: string;
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* `auth.admin.createUser()`: an email, a username or both, and a password
|
|
228
|
+
* of at least the project's minimum length (6 unless
|
|
229
|
+
* `naro backend auth password --min-length` changed it). The user is
|
|
230
|
+
* confirmed — signs in at once, even where sign-in waits for a confirmed
|
|
231
|
+
* email — unless `emailConfirm: false`, which needs an email. (Supabase's
|
|
232
|
+
* `email_confirm` is false by default; here confirmed is the default.)
|
|
233
|
+
*/
|
|
234
|
+
export type AdminUserAttributes = {
|
|
235
|
+
email?: string;
|
|
236
|
+
username?: string;
|
|
237
|
+
password: string;
|
|
238
|
+
emailConfirm?: boolean;
|
|
239
|
+
};
|
|
240
|
+
/** `auth.admin.listUsers()`: page 1 and 50 a page unless given; at most 1000. */
|
|
241
|
+
export type PageParams = {
|
|
242
|
+
page?: number;
|
|
243
|
+
perPage?: number;
|
|
244
|
+
};
|
|
245
|
+
export type AdminUserResponse = AuthResponse<{
|
|
246
|
+
user: AdminUser | null;
|
|
247
|
+
}>;
|
|
248
|
+
/** A page of users, newest first, and how many there are in all. */
|
|
249
|
+
export type AdminUserListResponse = AuthResponse<{
|
|
250
|
+
users: AdminUser[];
|
|
251
|
+
total: number;
|
|
252
|
+
}>;
|
|
253
|
+
/**
|
|
254
|
+
* The codes sign-up, sign-in and `auth.admin` answer that an app branches
|
|
255
|
+
* on. `NaroError.code` can be any other string too.
|
|
256
|
+
*/
|
|
257
|
+
export type AuthErrorCode =
|
|
258
|
+
/** Sign-ups are closed: only `auth.admin.createUser` makes users. */
|
|
259
|
+
"SIGNUPS_DISABLED"
|
|
260
|
+
/** Sign-up or `createUser` with a username someone has, in any case. */
|
|
261
|
+
| "USERNAME_TAKEN" | "USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL"
|
|
262
|
+
/** "Use at least {n} characters." — the project's minimum. */
|
|
263
|
+
| "PASSWORD_TOO_SHORT" | "PASSWORD_TOO_LONG" | "USERNAME_TOO_SHORT" | "USERNAME_TOO_LONG" | "INVALID_USERNAME" | "INVALID_EMAIL" | "INVALID_EMAIL_OR_PASSWORD" | "INVALID_USERNAME_OR_PASSWORD" | "EMAIL_NOT_VERIFIED" | "INVALID_SESSION"
|
|
264
|
+
/** `auth.admin` without the service key. */
|
|
265
|
+
| "PERMISSION_DENIED"
|
|
266
|
+
/** `auth.admin.deleteUser` with an id that is no user. */
|
|
267
|
+
| "USER_NOT_FOUND" | "RATE_LIMITED" | "WRITE_QUOTA_EXCEEDED";
|
|
186
268
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naro-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Auth and data for your Naro project, from the browser or the server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"vitest": "3.2.7",
|
|
26
26
|
"@next-deploy/backend-worker": "0.1.0",
|
|
27
27
|
"@next-deploy/config": "0.1.0",
|
|
28
|
-
"@next-deploy/
|
|
29
|
-
"@next-deploy/
|
|
28
|
+
"@next-deploy/data-core": "0.1.0",
|
|
29
|
+
"@next-deploy/sdk": "0.1.0"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
32
|
"build": "tsup && pnpm build:types",
|