@9in-io/sdk 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/CHANGELOG.md +24 -0
- package/README.md +22 -4
- package/dist/index.js +10 -3
- package/dist/server.d.ts +147 -26
- package/dist/server.js +34 -6
- package/package.json +4 -2
- package/src/index.ts +325 -0
- package/src/server.ts +843 -0
- package/src/util.ts +121 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
Breaking: 9in's wire formats are now snake_case throughout, with ISO 8601 UTC times
|
|
6
|
+
(see "API conventions" in the docs). Needs the 9in release of 2026-11-10.
|
|
7
|
+
|
|
8
|
+
- `WebhookEvent`: `type` → `event_type`, `createdAt` → `created_at`; `data.userId` is gone, use
|
|
9
|
+
`data.user.id`. For `user.deleted`, `data.user` is the user as it was before the deletion.
|
|
10
|
+
`token.revoked` data has `token_type` and `client_id`.
|
|
11
|
+
- `ManagedUser`: `created_at`, `updated_at`, `last_login_at` (ISO strings), `login_count`,
|
|
12
|
+
and every profile field.
|
|
13
|
+
- `ManagedSession`: `user_id`, `auth_time`, `user_agent`, `created_at`, `last_seen_at`,
|
|
14
|
+
`expires_at` (ISO strings); `revokedAt` removed.
|
|
15
|
+
- `getUser` returns `ManagedUserDetail` (`user`, `identities`, `sessions`, `has_password`,
|
|
16
|
+
`mfa`); `listUsers` returns `next_cursor`.
|
|
17
|
+
- `NewUser` / `ImportUser`: `email_verified`, `phone_number`, `phone_number_verified`,
|
|
18
|
+
`password_hash`, and ISO `created_at` / `last_login_at`. The server refuses the old names.
|
|
19
|
+
- `findUser("phone_number", …)` replaces `findUser("phone", …)`.
|
|
20
|
+
- `createLoginLink` returns `expires_at` as an ISO string.
|
|
21
|
+
- `AdapterEvent`: `event_type`, `created_at`, and `data.user_id` / `data.expires_at`.
|
|
22
|
+
- `TokenVerifier.handleEvent` reads the new webhook shape.
|
|
23
|
+
- The package ships `src/` too, so the `@9in/source` export condition resolves (0.1.x listed
|
|
24
|
+
it without shipping the files, which broke bundlers configured with that condition).
|
package/README.md
CHANGED
|
@@ -88,14 +88,30 @@ webhook events, or a short `cacheTtl`.
|
|
|
88
88
|
import { ManagementClient } from "@9in-io/sdk/server";
|
|
89
89
|
|
|
90
90
|
const nine = new ManagementClient({ issuer, clientId, clientSecret }); // app with management scopes
|
|
91
|
-
await nine.createUser({ email: "jane@acme.com", password: "…",
|
|
91
|
+
await nine.createUser({ email: "jane@acme.com", password: "…", email_verified: true });
|
|
92
|
+
await nine.updateUser(userId, { phone_number: "+447700900000", phone_number_verified: true });
|
|
92
93
|
await nine.blockUser(userId);
|
|
94
|
+
const { user } = await nine.findUser("phone_number", "+447700900000"); // or "email", "username"
|
|
95
|
+
|
|
96
|
+
// Sessions: list them, end one (its sid), or sign the user out everywhere
|
|
97
|
+
const { sessions } = await nine.listUserSessions(userId);
|
|
98
|
+
await nine.revokeSession(sessions[0].sid);
|
|
93
99
|
await nine.revokeUserSessions(userId);
|
|
94
100
|
|
|
95
101
|
// Single-use sign-in link (magic links, moving a signed-in user from your app to 9in)
|
|
96
102
|
const { url } = await nine.createLoginLink(userId, { returnTo: "https://app.acme.com/callback", ttlSeconds: 600 });
|
|
103
|
+
|
|
104
|
+
// Bulk import (500 per call): old ids, password hashes (bcrypt, argon2, PBKDF2, Firebase scrypt), social identities
|
|
105
|
+
const { results } = await nine.importUsers(
|
|
106
|
+
[{ id: "auth0|42", email: "jane@acme.com", password_hash: "$2b$10$…", created_at: "2021-03-04T05:06:07Z" }],
|
|
107
|
+
{ onConflict: "skip", source: "auth0" },
|
|
108
|
+
);
|
|
97
109
|
```
|
|
98
110
|
|
|
111
|
+
Users, sessions and requests use snake_case field names, the same going in and coming out
|
|
112
|
+
(`email_verified`, `phone_number`, `password_hash`), and times are ISO 8601 UTC strings
|
|
113
|
+
(`created_at`, `last_login_at`). Method options stay camelCase (`returnTo`, `onConflict`).
|
|
114
|
+
|
|
99
115
|
## Keep users in your own database (adapter)
|
|
100
116
|
|
|
101
117
|
```ts
|
|
@@ -115,7 +131,8 @@ export default {
|
|
|
115
131
|
return user ? toAdapterUser(user) : null;
|
|
116
132
|
},
|
|
117
133
|
async onEvent(event) {
|
|
118
|
-
|
|
134
|
+
// { id, event_type, created_at, data }; data.user_id is your own user id
|
|
135
|
+
if (event.event_type === "otp.requested") await sendCode(event.data); // user_id, channel, to, code, purpose, expires_at
|
|
119
136
|
},
|
|
120
137
|
}),
|
|
121
138
|
};
|
|
@@ -136,8 +153,9 @@ import { verifyWebhook } from "@9in-io/sdk/server";
|
|
|
136
153
|
export default {
|
|
137
154
|
async fetch(request: Request, env: { NINE_WEBHOOK_SECRET: string }) {
|
|
138
155
|
const event = await verifyWebhook(env.NINE_WEBHOOK_SECRET, request);
|
|
139
|
-
// event.id is the same on every
|
|
140
|
-
|
|
156
|
+
// { id, event_type, created_at, tenant, environment, data }. event.id is the same on every
|
|
157
|
+
// retry and redelivery: ignore ones you have seen. data.user is the user the event is about.
|
|
158
|
+
if (event.event_type === "user.created") await addToCrm(event.data.user);
|
|
141
159
|
return new Response(null, { status: 204 });
|
|
142
160
|
},
|
|
143
161
|
};
|
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
|
@@ -69,7 +69,7 @@ export interface TokenVerifier {
|
|
|
69
69
|
* user.sessions_revoked, user.updated and user.deleted).
|
|
70
70
|
*/
|
|
71
71
|
handleEvent(event: {
|
|
72
|
-
|
|
72
|
+
event_type: string;
|
|
73
73
|
data: Record<string, unknown>;
|
|
74
74
|
}): number;
|
|
75
75
|
/** Forgets every cached answer. */
|
|
@@ -94,20 +94,113 @@ export interface ManagementOptions {
|
|
|
94
94
|
allowInsecureIssuer?: boolean;
|
|
95
95
|
fetch?: typeof fetch;
|
|
96
96
|
}
|
|
97
|
+
/** A user as the Management API and webhooks send it. Times are ISO 8601 UTC strings. */
|
|
97
98
|
export interface ManagedUser {
|
|
98
99
|
id: string;
|
|
99
100
|
email: string | null;
|
|
100
101
|
email_verified: boolean;
|
|
101
102
|
phone_number: string | null;
|
|
103
|
+
phone_number_verified: boolean;
|
|
102
104
|
username: string | null;
|
|
105
|
+
preferred_username: string | null;
|
|
103
106
|
name: string | null;
|
|
107
|
+
given_name: string | null;
|
|
108
|
+
middle_name: string | null;
|
|
109
|
+
family_name: string | null;
|
|
110
|
+
nickname: string | null;
|
|
111
|
+
gender: string | null;
|
|
112
|
+
website: string | null;
|
|
104
113
|
picture: string | null;
|
|
114
|
+
locale: string | null;
|
|
115
|
+
zoneinfo: string | null;
|
|
116
|
+
birthdate: string | null;
|
|
105
117
|
blocked: boolean;
|
|
106
118
|
metadata: Record<string, unknown>;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
119
|
+
created_at: string;
|
|
120
|
+
updated_at: string;
|
|
121
|
+
last_login_at: string | null;
|
|
122
|
+
login_count: number;
|
|
110
123
|
}
|
|
124
|
+
/** An active sign-in session (`sid` is the `sid` claim of its tokens). Times are ISO 8601 UTC strings. */
|
|
125
|
+
export interface ManagedSession {
|
|
126
|
+
sid: string;
|
|
127
|
+
user_id: string;
|
|
128
|
+
auth_time: string;
|
|
129
|
+
amr: string[];
|
|
130
|
+
ip: string;
|
|
131
|
+
user_agent: string;
|
|
132
|
+
created_at: string;
|
|
133
|
+
last_seen_at: string;
|
|
134
|
+
expires_at: string;
|
|
135
|
+
}
|
|
136
|
+
/** A linked sign-in of a user (`social:google`, `enterprise:<id>`, `adapter`, …). */
|
|
137
|
+
export interface ManagedIdentity {
|
|
138
|
+
connection: string;
|
|
139
|
+
subject: string;
|
|
140
|
+
profile: Record<string, unknown>;
|
|
141
|
+
created_at: string;
|
|
142
|
+
}
|
|
143
|
+
/** `getUser`: the user with their identities, active sessions and two-factor state. */
|
|
144
|
+
export interface ManagedUserDetail {
|
|
145
|
+
user: ManagedUser;
|
|
146
|
+
identities: ManagedIdentity[];
|
|
147
|
+
sessions: ManagedSession[];
|
|
148
|
+
has_password: boolean;
|
|
149
|
+
mfa: {
|
|
150
|
+
enabled: boolean;
|
|
151
|
+
enabled_at?: string;
|
|
152
|
+
recovery_codes_left?: number;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** One record for `importUsers`: the fields of `createUser` plus what an import may carry over. */
|
|
156
|
+
export interface ImportUser {
|
|
157
|
+
/** Keep the id from your old system (letters, digits and | _ . : @ + -, max 128). */
|
|
158
|
+
id?: string;
|
|
159
|
+
email?: string | null;
|
|
160
|
+
email_verified?: boolean;
|
|
161
|
+
/** E.164, e.g. +447700900000. */
|
|
162
|
+
phone_number?: string | null;
|
|
163
|
+
phone_number_verified?: boolean;
|
|
164
|
+
username?: string | null;
|
|
165
|
+
name?: string | null;
|
|
166
|
+
given_name?: string | null;
|
|
167
|
+
family_name?: string | null;
|
|
168
|
+
picture?: string | null;
|
|
169
|
+
locale?: string | null;
|
|
170
|
+
zoneinfo?: string | null;
|
|
171
|
+
birthdate?: string | null;
|
|
172
|
+
metadata?: Record<string, unknown>;
|
|
173
|
+
blocked?: boolean;
|
|
174
|
+
/** A plain password (at most 50 such records per call), or `password_hash` from your old system. */
|
|
175
|
+
password?: string;
|
|
176
|
+
/** bcrypt, argon2, PBKDF2 or Firebase scrypt hash: users keep their password. */
|
|
177
|
+
password_hash?: string;
|
|
178
|
+
/** Linked sign-ins, e.g. { connection: "social:google", subject: "1069…" }. */
|
|
179
|
+
identities?: {
|
|
180
|
+
connection: string;
|
|
181
|
+
subject: string;
|
|
182
|
+
}[];
|
|
183
|
+
/** Original dates, ISO 8601 (`Z` or an offset). */
|
|
184
|
+
created_at?: string;
|
|
185
|
+
last_login_at?: string;
|
|
186
|
+
}
|
|
187
|
+
/** Fields of a new user (`createUser`); `updateUser` takes any of them. */
|
|
188
|
+
export type NewUser = Omit<ImportUser, "id" | "identities" | "created_at" | "last_login_at">;
|
|
189
|
+
export type ImportResult = {
|
|
190
|
+
index: number;
|
|
191
|
+
ok: boolean;
|
|
192
|
+
} & ({
|
|
193
|
+
status: "created" | "updated";
|
|
194
|
+
id: string;
|
|
195
|
+
} | {
|
|
196
|
+
status: "skipped";
|
|
197
|
+
id: string;
|
|
198
|
+
reason: string;
|
|
199
|
+
} | {
|
|
200
|
+
status: "failed";
|
|
201
|
+
error: string;
|
|
202
|
+
details?: unknown;
|
|
203
|
+
});
|
|
111
204
|
export declare class ManagementClient {
|
|
112
205
|
private readonly options;
|
|
113
206
|
/** One token per scope: each call asks only for what it needs, so an app granted some scopes works. */
|
|
@@ -123,26 +216,34 @@ export declare class ManagementClient {
|
|
|
123
216
|
limit?: number;
|
|
124
217
|
}): Promise<{
|
|
125
218
|
users: ManagedUser[];
|
|
126
|
-
|
|
219
|
+
next_cursor: string | null;
|
|
220
|
+
total: number;
|
|
127
221
|
}>;
|
|
128
|
-
|
|
222
|
+
/** The user with their identities, active sessions and two-factor state. */
|
|
223
|
+
getUser(id: string): Promise<ManagedUserDetail>;
|
|
224
|
+
findUserByEmail(email: string): Promise<{
|
|
129
225
|
user: ManagedUser;
|
|
130
226
|
}>;
|
|
131
|
-
|
|
227
|
+
/** Looks a user up by email, phone number (E.164) or username; throws `not_found` (404) when unknown. */
|
|
228
|
+
findUser(type: "email" | "phone_number" | "username", value: string): Promise<{
|
|
132
229
|
user: ManagedUser;
|
|
133
230
|
}>;
|
|
134
|
-
createUser(input: {
|
|
135
|
-
email?: string;
|
|
136
|
-
phone?: string;
|
|
137
|
-
username?: string;
|
|
138
|
-
password?: string;
|
|
139
|
-
name?: string;
|
|
140
|
-
emailVerified?: boolean;
|
|
141
|
-
metadata?: Record<string, unknown>;
|
|
142
|
-
}): Promise<{
|
|
231
|
+
createUser(input: NewUser): Promise<{
|
|
143
232
|
user: ManagedUser;
|
|
144
233
|
}>;
|
|
145
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Bulk import, up to 500 users per call. Records may keep their old id and carry password
|
|
236
|
+
* hashes (bcrypt, argon2, PBKDF2, Firebase scrypt), social identities and original dates.
|
|
237
|
+
* Results are per record, in order; one bad record does not stop the others.
|
|
238
|
+
*/
|
|
239
|
+
importUsers(users: ImportUser[], options?: {
|
|
240
|
+
onConflict?: "skip" | "update";
|
|
241
|
+
source?: string;
|
|
242
|
+
}): Promise<{
|
|
243
|
+
results: ImportResult[];
|
|
244
|
+
}>;
|
|
245
|
+
/** Changes only the fields you send; `metadata` is replaced as a whole. */
|
|
246
|
+
updateUser(id: string, input: Partial<NewUser>): Promise<{
|
|
146
247
|
user: ManagedUser;
|
|
147
248
|
}>;
|
|
148
249
|
deleteUser(id: string): Promise<void>;
|
|
@@ -159,11 +260,18 @@ export declare class ManagementClient {
|
|
|
159
260
|
ttlSeconds?: number;
|
|
160
261
|
}): Promise<{
|
|
161
262
|
url: string;
|
|
162
|
-
expires_at:
|
|
263
|
+
expires_at: string;
|
|
163
264
|
}>;
|
|
265
|
+
/** The user's active sessions, most recently used first. */
|
|
266
|
+
listUserSessions(id: string): Promise<{
|
|
267
|
+
sessions: ManagedSession[];
|
|
268
|
+
}>;
|
|
269
|
+
/** Signs the user out everywhere; returns how many sessions ended. */
|
|
164
270
|
revokeUserSessions(id: string): Promise<{
|
|
165
271
|
revoked: number;
|
|
166
272
|
}>;
|
|
273
|
+
/** Ends one session (its `sid`) and every token issued under it. */
|
|
274
|
+
revokeSession(sid: string): Promise<void>;
|
|
167
275
|
}
|
|
168
276
|
export interface AdapterIdentifier {
|
|
169
277
|
type: "email" | "username" | "phone";
|
|
@@ -186,11 +294,20 @@ export interface AdapterUser {
|
|
|
186
294
|
blocked?: boolean | null;
|
|
187
295
|
metadata?: Record<string, unknown> | null;
|
|
188
296
|
}
|
|
297
|
+
/**
|
|
298
|
+
* What 9in POSTs to `…/event`. `data.user_id` is your own user id. `otp.requested` carries
|
|
299
|
+
* `user_id, channel, to, code, purpose, expires_at`; `user.updated` and
|
|
300
|
+
* `user.password_changed` carry `user_id, fields`.
|
|
301
|
+
*/
|
|
189
302
|
export interface AdapterEvent {
|
|
190
303
|
id: string;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
304
|
+
event_type: "otp.requested" | "user.created" | "user.updated" | "user.password_changed" | "session.revoked";
|
|
305
|
+
/** ISO 8601 UTC. */
|
|
306
|
+
created_at: string;
|
|
307
|
+
data: {
|
|
308
|
+
user_id?: string;
|
|
309
|
+
[field: string]: unknown;
|
|
310
|
+
};
|
|
194
311
|
}
|
|
195
312
|
export interface AdapterHandlers {
|
|
196
313
|
/** Shared secret from the console (Connections → User database). Several allow rotation. */
|
|
@@ -215,14 +332,18 @@ export type WebhookEventType = "user.created" | "user.updated" | "user.deleted"
|
|
|
215
332
|
export interface WebhookEvent<Data extends Record<string, unknown> = Record<string, unknown>> {
|
|
216
333
|
/** Event id: the same on every retry and redelivery, so you can ignore duplicates. */
|
|
217
334
|
id: string;
|
|
218
|
-
type
|
|
219
|
-
|
|
335
|
+
/** The event name, also in the `9in-event-type` header. */
|
|
336
|
+
event_type: WebhookEventType;
|
|
337
|
+
/** ISO 8601 UTC. */
|
|
338
|
+
created_at: string;
|
|
220
339
|
/** Tenant id (tnt_…). */
|
|
221
340
|
tenant: string;
|
|
222
341
|
environment: "production" | "sandbox";
|
|
223
|
-
/**
|
|
342
|
+
/**
|
|
343
|
+
* Event details, plus the `user` the event is about (null when there is none; for
|
|
344
|
+
* `user.deleted`, the user as it was just before the deletion).
|
|
345
|
+
*/
|
|
224
346
|
data: Data & {
|
|
225
|
-
userId?: string | null;
|
|
226
347
|
user?: ManagedUser | null;
|
|
227
348
|
};
|
|
228
349
|
}
|
|
@@ -238,7 +359,7 @@ export interface VerifyWebhookOptions {
|
|
|
238
359
|
* the body, so pass `request.clone()` if you need it afterwards.
|
|
239
360
|
*
|
|
240
361
|
* const event = await verifyWebhook(env.NINE_WEBHOOK_SECRET, request);
|
|
241
|
-
* if (event.
|
|
362
|
+
* if (event.event_type === "user.created") …
|
|
242
363
|
*/
|
|
243
364
|
export declare function verifyWebhook(secret: string | string[], request: Request, options?: VerifyWebhookOptions): Promise<WebhookEvent>;
|
|
244
365
|
/**
|
package/dist/server.js
CHANGED
|
@@ -221,13 +221,14 @@ export function createTokenVerifier(options) {
|
|
|
221
221
|
},
|
|
222
222
|
handleEvent(event) {
|
|
223
223
|
const data = event.data ?? {};
|
|
224
|
+
const userId = data.user?.id;
|
|
224
225
|
let match = null;
|
|
225
|
-
if (event.
|
|
226
|
+
if (event.event_type === "token.revoked" && data.token_type === "access_token" && data.jti)
|
|
226
227
|
match = (c) => c.jti === data.jti;
|
|
227
|
-
else if (event.
|
|
228
|
+
else if (event.event_type === "session.revoked" && data.sid)
|
|
228
229
|
match = (c) => c.sid === data.sid;
|
|
229
|
-
else if (["user.sessions_revoked", "user.deleted", "user.updated", "user.password_changed"].includes(event.
|
|
230
|
-
match = (c) => c.sub ===
|
|
230
|
+
else if (["user.sessions_revoked", "user.deleted", "user.updated", "user.password_changed"].includes(event.event_type) && typeof userId === "string") {
|
|
231
|
+
match = (c) => c.sub === userId;
|
|
231
232
|
}
|
|
232
233
|
if (!match)
|
|
233
234
|
return 0;
|
|
@@ -289,15 +290,33 @@ export class ManagementClient {
|
|
|
289
290
|
params.set("limit", String(options.limit));
|
|
290
291
|
return this.request("GET", `/users?${params}`);
|
|
291
292
|
}
|
|
293
|
+
/** The user with their identities, active sessions and two-factor state. */
|
|
292
294
|
getUser(id) {
|
|
293
295
|
return this.request("GET", `/users/${encodeURIComponent(id)}`);
|
|
294
296
|
}
|
|
295
297
|
findUserByEmail(email) {
|
|
296
|
-
return this.
|
|
298
|
+
return this.findUser("email", email);
|
|
299
|
+
}
|
|
300
|
+
/** Looks a user up by email, phone number (E.164) or username; throws `not_found` (404) when unknown. */
|
|
301
|
+
findUser(type, value) {
|
|
302
|
+
return this.request("GET", `/users/lookup?${type}=${encodeURIComponent(value)}`);
|
|
297
303
|
}
|
|
298
304
|
createUser(input) {
|
|
299
305
|
return this.request("POST", "/users", input);
|
|
300
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* Bulk import, up to 500 users per call. Records may keep their old id and carry password
|
|
309
|
+
* hashes (bcrypt, argon2, PBKDF2, Firebase scrypt), social identities and original dates.
|
|
310
|
+
* Results are per record, in order; one bad record does not stop the others.
|
|
311
|
+
*/
|
|
312
|
+
importUsers(users, options = {}) {
|
|
313
|
+
return this.request("POST", "/users/import", {
|
|
314
|
+
users,
|
|
315
|
+
...(options.onConflict ? { on_conflict: options.onConflict } : {}),
|
|
316
|
+
...(options.source ? { source: options.source } : {}),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
/** Changes only the fields you send; `metadata` is replaced as a whole. */
|
|
301
320
|
updateUser(id, input) {
|
|
302
321
|
return this.request("PATCH", `/users/${encodeURIComponent(id)}`, input);
|
|
303
322
|
}
|
|
@@ -318,9 +337,18 @@ export class ManagementClient {
|
|
|
318
337
|
...(options.ttlSeconds ? { ttl_seconds: options.ttlSeconds } : {}),
|
|
319
338
|
});
|
|
320
339
|
}
|
|
340
|
+
/** The user's active sessions, most recently used first. */
|
|
341
|
+
listUserSessions(id) {
|
|
342
|
+
return this.request("GET", `/users/${encodeURIComponent(id)}/sessions`);
|
|
343
|
+
}
|
|
344
|
+
/** Signs the user out everywhere; returns how many sessions ended. */
|
|
321
345
|
revokeUserSessions(id) {
|
|
322
346
|
return this.request("DELETE", `/users/${encodeURIComponent(id)}/sessions`);
|
|
323
347
|
}
|
|
348
|
+
/** Ends one session (its `sid`) and every token issued under it. */
|
|
349
|
+
revokeSession(sid) {
|
|
350
|
+
return this.request("DELETE", `/sessions/${encodeURIComponent(sid)}`);
|
|
351
|
+
}
|
|
324
352
|
}
|
|
325
353
|
const SIGNATURE_HEADER = "9in-signature";
|
|
326
354
|
async function hmac(secret, payload) {
|
|
@@ -361,7 +389,7 @@ export async function signPayload(secret, method, path, body) {
|
|
|
361
389
|
* the body, so pass `request.clone()` if you need it afterwards.
|
|
362
390
|
*
|
|
363
391
|
* const event = await verifyWebhook(env.NINE_WEBHOOK_SECRET, request);
|
|
364
|
-
* if (event.
|
|
392
|
+
* if (event.event_type === "user.created") …
|
|
365
393
|
*/
|
|
366
394
|
export async function verifyWebhook(secret, request, options = {}) {
|
|
367
395
|
const body = await request.text();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@9in-io/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"dist",
|
|
28
|
-
"
|
|
28
|
+
"src",
|
|
29
|
+
"README.md",
|
|
30
|
+
"CHANGELOG.md"
|
|
29
31
|
],
|
|
30
32
|
"keywords": [
|
|
31
33
|
"oauth",
|