@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 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
- const set = this.store(tokens);
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
- const { payload } = decodeJwt(set.idToken);
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
- 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<{
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.request("GET", `/users/lookup?email=${encodeURIComponent(email)}`);
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.0",
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",