@9in-io/sdk 0.1.0 → 0.1.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 +8 -0
- package/dist/index.js +10 -3
- package/dist/server.d.ts +85 -9
- package/dist/server.js +26 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -90,10 +90,18 @@ import { ManagementClient } from "@9in-io/sdk/server";
|
|
|
90
90
|
const nine = new ManagementClient({ issuer, clientId, clientSecret }); // app with management scopes
|
|
91
91
|
await nine.createUser({ email: "jane@acme.com", password: "…", emailVerified: true });
|
|
92
92
|
await nine.blockUser(userId);
|
|
93
|
+
const { user } = await nine.findUser("phone", "+447700900000"); // or "email", "username"
|
|
94
|
+
|
|
95
|
+
// Sessions: list them, end one (its sid), or sign the user out everywhere
|
|
96
|
+
const { sessions } = await nine.listUserSessions(userId);
|
|
97
|
+
await nine.revokeSession(sessions[0].sid);
|
|
93
98
|
await nine.revokeUserSessions(userId);
|
|
94
99
|
|
|
95
100
|
// Single-use sign-in link (magic links, moving a signed-in user from your app to 9in)
|
|
96
101
|
const { url } = await nine.createLoginLink(userId, { returnTo: "https://app.acme.com/callback", ttlSeconds: 600 });
|
|
102
|
+
|
|
103
|
+
// Bulk import (500 per call): old ids, password hashes (bcrypt, argon2, PBKDF2, Firebase scrypt), social identities
|
|
104
|
+
const { results } = await nine.importUsers([{ id: "auth0|42", email: "jane@acme.com", passwordHash: "$2b$10$…" }], { onConflict: "skip", source: "auth0" });
|
|
97
105
|
```
|
|
98
106
|
|
|
99
107
|
## Keep users in your own database (adapter)
|
package/dist/index.js
CHANGED
|
@@ -107,17 +107,24 @@ export class NineAuth {
|
|
|
107
107
|
throw new NineError("invalid_issuer", `unexpected issuer ${iss}`);
|
|
108
108
|
const config = await discover(this.issuer, this.fetcher);
|
|
109
109
|
const tokens = await postForm(config.token_endpoint, { grant_type: "authorization_code", code, redirect_uri: this.options.redirectUri, client_id: this.options.clientId, code_verifier: pending.verifier }, {}, this.fetcher);
|
|
110
|
-
|
|
111
|
-
if (set.idToken) {
|
|
110
|
+
if (typeof tokens.id_token === "string") {
|
|
112
111
|
// The ID token came straight from the token endpoint over TLS (OIDC Core 3.1.3.7):
|
|
113
112
|
// its claims are checked; the signature check is for servers (verifyAccessToken).
|
|
114
|
-
|
|
113
|
+
// Checked before anything is stored, so a refused sign-in leaves no tokens behind.
|
|
114
|
+
let payload;
|
|
115
|
+
try {
|
|
116
|
+
payload = decodeJwt(tokens.id_token).payload;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw new NineError("invalid_id_token", "malformed ID token");
|
|
120
|
+
}
|
|
115
121
|
const aud = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
|
|
116
122
|
if (!aud.includes(this.options.clientId))
|
|
117
123
|
throw new NineError("invalid_id_token", "audience mismatch");
|
|
118
124
|
if (payload.nonce !== pending.nonce)
|
|
119
125
|
throw new NineError("invalid_id_token", "nonce mismatch");
|
|
120
126
|
}
|
|
127
|
+
this.store(tokens);
|
|
121
128
|
if (typeof window !== "undefined" && window.history && url === window.location.href) {
|
|
122
129
|
for (const p of ["code", "state", "iss", "session_state"])
|
|
123
130
|
current.searchParams.delete(p);
|
package/dist/server.d.ts
CHANGED
|
@@ -108,6 +108,68 @@ export interface ManagedUser {
|
|
|
108
108
|
lastLoginAt: number | null;
|
|
109
109
|
[field: string]: unknown;
|
|
110
110
|
}
|
|
111
|
+
/** An active sign-in session (`sid` is the `sid` claim of its tokens). Times are ms since epoch. */
|
|
112
|
+
export interface ManagedSession {
|
|
113
|
+
sid: string;
|
|
114
|
+
userId: string;
|
|
115
|
+
authTime: number;
|
|
116
|
+
amr: string[];
|
|
117
|
+
ip: string;
|
|
118
|
+
userAgent: string;
|
|
119
|
+
createdAt: number;
|
|
120
|
+
lastSeenAt: number;
|
|
121
|
+
expiresAt: number;
|
|
122
|
+
revokedAt: number | null;
|
|
123
|
+
}
|
|
124
|
+
/** One record for `importUsers`: the fields of `createUser` plus what an import may carry over. */
|
|
125
|
+
export interface ImportUser {
|
|
126
|
+
/** Keep the id from your old system (letters, digits and | _ . : @ + -, max 128). */
|
|
127
|
+
id?: string;
|
|
128
|
+
email?: string | null;
|
|
129
|
+
emailVerified?: boolean;
|
|
130
|
+
/** E.164, e.g. +447700900000. */
|
|
131
|
+
phone?: string | null;
|
|
132
|
+
phoneVerified?: boolean;
|
|
133
|
+
username?: string | null;
|
|
134
|
+
name?: string | null;
|
|
135
|
+
given_name?: string | null;
|
|
136
|
+
family_name?: string | null;
|
|
137
|
+
picture?: string | null;
|
|
138
|
+
locale?: string | null;
|
|
139
|
+
zoneinfo?: string | null;
|
|
140
|
+
birthdate?: string | null;
|
|
141
|
+
metadata?: Record<string, unknown>;
|
|
142
|
+
blocked?: boolean;
|
|
143
|
+
/** A plain password (at most 50 such records per call), or `passwordHash` from your old system. */
|
|
144
|
+
password?: string;
|
|
145
|
+
/** bcrypt, argon2, PBKDF2 or Firebase scrypt hash: users keep their password. */
|
|
146
|
+
passwordHash?: string;
|
|
147
|
+
/** Linked sign-ins, e.g. { connection: "social:google", subject: "1069…" }. */
|
|
148
|
+
identities?: {
|
|
149
|
+
connection: string;
|
|
150
|
+
subject: string;
|
|
151
|
+
}[];
|
|
152
|
+
/** ms since epoch */
|
|
153
|
+
createdAt?: number;
|
|
154
|
+
lastLoginAt?: number;
|
|
155
|
+
}
|
|
156
|
+
/** Fields of a new user (`createUser`, `updateUser`). */
|
|
157
|
+
export type NewUser = Omit<ImportUser, "id" | "identities" | "createdAt" | "lastLoginAt">;
|
|
158
|
+
export type ImportResult = {
|
|
159
|
+
index: number;
|
|
160
|
+
ok: boolean;
|
|
161
|
+
} & ({
|
|
162
|
+
status: "created" | "updated";
|
|
163
|
+
id: string;
|
|
164
|
+
} | {
|
|
165
|
+
status: "skipped";
|
|
166
|
+
id: string;
|
|
167
|
+
reason: string;
|
|
168
|
+
} | {
|
|
169
|
+
status: "failed";
|
|
170
|
+
error: string;
|
|
171
|
+
details?: unknown;
|
|
172
|
+
});
|
|
111
173
|
export declare class ManagementClient {
|
|
112
174
|
private readonly options;
|
|
113
175
|
/** One token per scope: each call asks only for what it needs, so an app granted some scopes works. */
|
|
@@ -131,17 +193,24 @@ export declare class ManagementClient {
|
|
|
131
193
|
findUserByEmail(email: string): Promise<{
|
|
132
194
|
user: ManagedUser;
|
|
133
195
|
}>;
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
name?: string;
|
|
140
|
-
emailVerified?: boolean;
|
|
141
|
-
metadata?: Record<string, unknown>;
|
|
142
|
-
}): Promise<{
|
|
196
|
+
/** Looks a user up by email, phone (E.164) or username; throws `not_found` (404) when unknown. */
|
|
197
|
+
findUser(type: "email" | "phone" | "username", value: string): Promise<{
|
|
198
|
+
user: ManagedUser;
|
|
199
|
+
}>;
|
|
200
|
+
createUser(input: NewUser): Promise<{
|
|
143
201
|
user: ManagedUser;
|
|
144
202
|
}>;
|
|
203
|
+
/**
|
|
204
|
+
* Bulk import, up to 500 users per call. Records may keep their old id and carry password
|
|
205
|
+
* hashes (bcrypt, argon2, PBKDF2, Firebase scrypt), social identities and original dates.
|
|
206
|
+
* Results are per record, in order; one bad record does not stop the others.
|
|
207
|
+
*/
|
|
208
|
+
importUsers(users: ImportUser[], options?: {
|
|
209
|
+
onConflict?: "skip" | "update";
|
|
210
|
+
source?: string;
|
|
211
|
+
}): Promise<{
|
|
212
|
+
results: ImportResult[];
|
|
213
|
+
}>;
|
|
145
214
|
updateUser(id: string, input: Record<string, unknown>): Promise<{
|
|
146
215
|
user: ManagedUser;
|
|
147
216
|
}>;
|
|
@@ -161,9 +230,16 @@ export declare class ManagementClient {
|
|
|
161
230
|
url: string;
|
|
162
231
|
expires_at: number;
|
|
163
232
|
}>;
|
|
233
|
+
/** The user's active sessions, most recently used first. */
|
|
234
|
+
listUserSessions(id: string): Promise<{
|
|
235
|
+
sessions: ManagedSession[];
|
|
236
|
+
}>;
|
|
237
|
+
/** Signs the user out everywhere; returns how many sessions ended. */
|
|
164
238
|
revokeUserSessions(id: string): Promise<{
|
|
165
239
|
revoked: number;
|
|
166
240
|
}>;
|
|
241
|
+
/** Ends one session (its `sid`) and every token issued under it. */
|
|
242
|
+
revokeSession(sid: string): Promise<void>;
|
|
167
243
|
}
|
|
168
244
|
export interface AdapterIdentifier {
|
|
169
245
|
type: "email" | "username" | "phone";
|
package/dist/server.js
CHANGED
|
@@ -293,11 +293,27 @@ export class ManagementClient {
|
|
|
293
293
|
return this.request("GET", `/users/${encodeURIComponent(id)}`);
|
|
294
294
|
}
|
|
295
295
|
findUserByEmail(email) {
|
|
296
|
-
return this.
|
|
296
|
+
return this.findUser("email", email);
|
|
297
|
+
}
|
|
298
|
+
/** Looks a user up by email, phone (E.164) or username; throws `not_found` (404) when unknown. */
|
|
299
|
+
findUser(type, value) {
|
|
300
|
+
return this.request("GET", `/users/lookup?${type}=${encodeURIComponent(value)}`);
|
|
297
301
|
}
|
|
298
302
|
createUser(input) {
|
|
299
303
|
return this.request("POST", "/users", input);
|
|
300
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Bulk import, up to 500 users per call. Records may keep their old id and carry password
|
|
307
|
+
* hashes (bcrypt, argon2, PBKDF2, Firebase scrypt), social identities and original dates.
|
|
308
|
+
* Results are per record, in order; one bad record does not stop the others.
|
|
309
|
+
*/
|
|
310
|
+
importUsers(users, options = {}) {
|
|
311
|
+
return this.request("POST", "/users/import", {
|
|
312
|
+
users,
|
|
313
|
+
...(options.onConflict ? { on_conflict: options.onConflict } : {}),
|
|
314
|
+
...(options.source ? { source: options.source } : {}),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
301
317
|
updateUser(id, input) {
|
|
302
318
|
return this.request("PATCH", `/users/${encodeURIComponent(id)}`, input);
|
|
303
319
|
}
|
|
@@ -318,9 +334,18 @@ export class ManagementClient {
|
|
|
318
334
|
...(options.ttlSeconds ? { ttl_seconds: options.ttlSeconds } : {}),
|
|
319
335
|
});
|
|
320
336
|
}
|
|
337
|
+
/** The user's active sessions, most recently used first. */
|
|
338
|
+
listUserSessions(id) {
|
|
339
|
+
return this.request("GET", `/users/${encodeURIComponent(id)}/sessions`);
|
|
340
|
+
}
|
|
341
|
+
/** Signs the user out everywhere; returns how many sessions ended. */
|
|
321
342
|
revokeUserSessions(id) {
|
|
322
343
|
return this.request("DELETE", `/users/${encodeURIComponent(id)}/sessions`);
|
|
323
344
|
}
|
|
345
|
+
/** Ends one session (its `sid`) and every token issued under it. */
|
|
346
|
+
revokeSession(sid) {
|
|
347
|
+
return this.request("DELETE", `/sessions/${encodeURIComponent(sid)}`);
|
|
348
|
+
}
|
|
324
349
|
}
|
|
325
350
|
const SIGNATURE_HEADER = "9in-signature";
|
|
326
351
|
async function hmac(secret, payload) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@9in-io/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Official 9in SDK: browser sign-in with PKCE, token verification, Management API client and adapter handler. Zero dependencies; runs in browsers, Node 18+, Cloudflare Workers, Deno and Bun.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|