@drmhse/sso-sdk 0.2.7 → 0.2.9

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/dist/index.mjs CHANGED
@@ -49,6 +49,12 @@ var HttpClient = class {
49
49
  timeout: 3e4
50
50
  };
51
51
  }
52
+ /**
53
+ * Allow injecting session manager after construction to avoid circular dep
54
+ */
55
+ setSessionManager(manager) {
56
+ this.sessionManager = manager;
57
+ }
52
58
  /**
53
59
  * Build query string from params object
54
60
  */
@@ -82,6 +88,12 @@ var HttpClient = class {
82
88
  ...this.defaults.headers.common,
83
89
  ...options.headers
84
90
  };
91
+ if (this.sessionManager) {
92
+ const token = await this.sessionManager.getToken();
93
+ if (token) {
94
+ headers["Authorization"] = `Bearer ${token}`;
95
+ }
96
+ }
85
97
  const controller = new AbortController();
86
98
  const timeoutId = setTimeout(() => controller.abort(), timeout);
87
99
  try {
@@ -92,6 +104,17 @@ var HttpClient = class {
92
104
  signal: controller.signal
93
105
  });
94
106
  clearTimeout(timeoutId);
107
+ if (response.status === 401 && this.sessionManager && !options._retry && !path.includes("/auth/login")) {
108
+ try {
109
+ const newToken = await this.sessionManager.refreshSession();
110
+ return this.request(path, {
111
+ ...options,
112
+ _retry: true,
113
+ headers: { ...options.headers, Authorization: `Bearer ${newToken}` }
114
+ });
115
+ } catch (refreshError) {
116
+ }
117
+ }
95
118
  let data;
96
119
  const contentType = response.headers.get("content-type");
97
120
  if (contentType?.includes("application/json")) {
@@ -160,6 +183,16 @@ var HttpClient = class {
160
183
  headers: config?.headers
161
184
  });
162
185
  }
186
+ /**
187
+ * PUT request
188
+ */
189
+ async put(path, data, config) {
190
+ return this.request(path, {
191
+ method: "PUT",
192
+ body: data,
193
+ headers: config?.headers
194
+ });
195
+ }
163
196
  /**
164
197
  * PATCH request
165
198
  */
@@ -184,6 +217,125 @@ function createHttpAgent(baseURL) {
184
217
  return new HttpClient(baseURL);
185
218
  }
186
219
 
220
+ // src/session.ts
221
+ var SessionManager = class {
222
+ constructor(storage, refreshHandler, config = { storageKeyPrefix: "sso_" }) {
223
+ this.storage = storage;
224
+ this.refreshHandler = refreshHandler;
225
+ this.config = config;
226
+ this.accessToken = null;
227
+ this.refreshToken = null;
228
+ this.refreshPromise = null;
229
+ this.listeners = [];
230
+ }
231
+ /**
232
+ * Initialize session from storage
233
+ */
234
+ async loadSession() {
235
+ this.accessToken = await this.storage.getItem(`${this.config.storageKeyPrefix}access_token`);
236
+ this.refreshToken = await this.storage.getItem(`${this.config.storageKeyPrefix}refresh_token`);
237
+ }
238
+ /**
239
+ * Set the session data (used after login/register)
240
+ */
241
+ async setSession(tokens) {
242
+ this.accessToken = tokens.access_token;
243
+ await this.storage.setItem(`${this.config.storageKeyPrefix}access_token`, tokens.access_token);
244
+ if (tokens.refresh_token) {
245
+ this.refreshToken = tokens.refresh_token;
246
+ await this.storage.setItem(`${this.config.storageKeyPrefix}refresh_token`, tokens.refresh_token);
247
+ }
248
+ this.notifyListeners(true);
249
+ }
250
+ /**
251
+ * Clear session (logout)
252
+ */
253
+ async clearSession() {
254
+ this.accessToken = null;
255
+ this.refreshToken = null;
256
+ await this.storage.removeItem(`${this.config.storageKeyPrefix}access_token`);
257
+ await this.storage.removeItem(`${this.config.storageKeyPrefix}refresh_token`);
258
+ this.notifyListeners(false);
259
+ }
260
+ /**
261
+ * Get the current access token, refreshing it if necessary/possible
262
+ */
263
+ async getToken() {
264
+ return this.accessToken;
265
+ }
266
+ /**
267
+ * Handle logic for when a 401 occurs
268
+ */
269
+ async refreshSession() {
270
+ if (!this.refreshToken) {
271
+ throw new Error("No refresh token available");
272
+ }
273
+ if (this.refreshPromise) {
274
+ return this.refreshPromise;
275
+ }
276
+ this.refreshPromise = (async () => {
277
+ try {
278
+ const tokens = await this.refreshHandler(this.refreshToken);
279
+ await this.setSession(tokens);
280
+ return tokens.access_token;
281
+ } catch (err) {
282
+ await this.clearSession();
283
+ throw err;
284
+ } finally {
285
+ this.refreshPromise = null;
286
+ }
287
+ })();
288
+ return this.refreshPromise;
289
+ }
290
+ isAuthenticated() {
291
+ return !!this.accessToken;
292
+ }
293
+ /**
294
+ * Subscribe to auth state changes (useful for UI updates)
295
+ */
296
+ subscribe(listener) {
297
+ this.listeners.push(listener);
298
+ return () => {
299
+ this.listeners = this.listeners.filter((l) => l !== listener);
300
+ };
301
+ }
302
+ notifyListeners(isAuth) {
303
+ this.listeners.forEach((l) => l(isAuth));
304
+ }
305
+ };
306
+
307
+ // src/storage.ts
308
+ var MemoryStorage = class {
309
+ constructor() {
310
+ this.store = /* @__PURE__ */ new Map();
311
+ }
312
+ getItem(key) {
313
+ return this.store.get(key) || null;
314
+ }
315
+ setItem(key, value) {
316
+ this.store.set(key, value);
317
+ }
318
+ removeItem(key) {
319
+ this.store.delete(key);
320
+ }
321
+ };
322
+ var BrowserStorage = class {
323
+ getItem(key) {
324
+ return typeof window !== "undefined" ? window.localStorage.getItem(key) : null;
325
+ }
326
+ setItem(key, value) {
327
+ if (typeof window !== "undefined") window.localStorage.setItem(key, value);
328
+ }
329
+ removeItem(key) {
330
+ if (typeof window !== "undefined") window.localStorage.removeItem(key);
331
+ }
332
+ };
333
+ function resolveStorage(userStorage) {
334
+ if (userStorage) return userStorage;
335
+ if (typeof window !== "undefined" && window.localStorage) return new BrowserStorage();
336
+ return new MemoryStorage();
337
+ }
338
+
187
339
  // src/modules/analytics.ts
188
340
  var AnalyticsModule = class {
189
341
  constructor(http) {
@@ -289,8 +441,9 @@ var AnalyticsModule = class {
289
441
 
290
442
  // src/modules/auth.ts
291
443
  var AuthModule = class {
292
- constructor(http) {
444
+ constructor(http, session) {
293
445
  this.http = http;
446
+ this.session = session;
294
447
  /**
295
448
  * Device Flow: Request a device code for CLI/device authentication.
296
449
  *
@@ -337,6 +490,9 @@ var AuthModule = class {
337
490
  /**
338
491
  * Exchange a device code for a JWT token.
339
492
  * This should be polled by the device/CLI after displaying the user code.
493
+ * Note: This returns a TokenResponse (not RefreshTokenResponse) and typically
494
+ * only includes access_token. For device flows that need persistence,
495
+ * manually call sso.session.setSession() if needed.
340
496
  *
341
497
  * @param payload Token request payload
342
498
  * @returns Token response with JWT
@@ -352,7 +508,7 @@ var AuthModule = class {
352
508
  * client_id: 'service-client-id'
353
509
  * });
354
510
  * clearInterval(interval);
355
- * sso.setAuthToken(token.access_token);
511
+ * // Session is automatically configured
356
512
  * } catch (error) {
357
513
  * if (error.errorCode !== 'authorization_pending') {
358
514
  * clearInterval(interval);
@@ -374,17 +530,29 @@ var AuthModule = class {
374
530
  * should redirect the user's browser to this URL.
375
531
  *
376
532
  * @param provider The OAuth provider to use
377
- * @param params Login parameters (org, service, redirect_uri)
533
+ * @param params Login parameters (org, service, redirect_uri, connection_id)
378
534
  * @returns The full URL to redirect the user to
379
535
  *
380
536
  * @example
381
537
  * ```typescript
538
+ * // Standard OAuth login
382
539
  * const url = sso.auth.getLoginUrl('github', {
383
540
  * org: 'acme-corp',
384
541
  * service: 'main-app',
385
542
  * redirect_uri: 'https://app.acme.com/callback'
386
543
  * });
387
544
  * window.location.href = url;
545
+ *
546
+ * // Enterprise IdP login (after HRD lookup)
547
+ * const hrd = await sso.auth.lookupEmail('user@enterprise.com');
548
+ * if (hrd.connection_id) {
549
+ * const url = sso.auth.getLoginUrl('github', {
550
+ * org: 'acme-corp',
551
+ * service: 'main-app',
552
+ * connection_id: hrd.connection_id
553
+ * });
554
+ * window.location.href = url;
555
+ * }
388
556
  * ```
389
557
  */
390
558
  getLoginUrl(provider, params) {
@@ -399,6 +567,9 @@ var AuthModule = class {
399
567
  if (params.user_code) {
400
568
  searchParams.append("user_code", params.user_code);
401
569
  }
570
+ if (params.connection_id) {
571
+ searchParams.append("connection_id", params.connection_id);
572
+ }
402
573
  return `${baseURL}/auth/${provider}?${searchParams.toString()}`;
403
574
  }
404
575
  /**
@@ -431,19 +602,20 @@ var AuthModule = class {
431
602
  }
432
603
  /**
433
604
  * Logout the current user by revoking their JWT.
434
- * After calling this, you should clear the token from storage
435
- * and call `sso.setAuthToken(null)`.
605
+ * Automatically clears the session and tokens from storage.
436
606
  *
437
607
  * @example
438
608
  * ```typescript
439
609
  * await sso.auth.logout();
440
- * sso.setAuthToken(null);
441
- * localStorage.removeItem('sso_access_token');
442
- * localStorage.removeItem('sso_refresh_token');
610
+ * // Session is automatically cleared - no need for manual cleanup
443
611
  * ```
444
612
  */
445
613
  async logout() {
446
- await this.http.post("/api/auth/logout");
614
+ try {
615
+ await this.http.post("/api/auth/logout");
616
+ } finally {
617
+ await this.session.clearSession();
618
+ }
447
619
  }
448
620
  /**
449
621
  * Refresh an expired JWT access token using a refresh token.
@@ -518,8 +690,7 @@ var AuthModule = class {
518
690
  }
519
691
  /**
520
692
  * Login with email and password.
521
- * Returns access token and refresh token on successful authentication.
522
- * The user's email must be verified before login.
693
+ * Automatically persists the session and configures the client.
523
694
  *
524
695
  * @param payload Login credentials (email and password)
525
696
  * @returns Access token, refresh token, and expiration info
@@ -530,13 +701,15 @@ var AuthModule = class {
530
701
  * email: 'user@example.com',
531
702
  * password: 'SecurePassword123!'
532
703
  * });
533
- * sso.setAuthToken(tokens.access_token);
534
- * localStorage.setItem('sso_access_token', tokens.access_token);
535
- * localStorage.setItem('sso_refresh_token', tokens.refresh_token);
704
+ * // Session is automatically saved - no need for manual token management
536
705
  * ```
537
706
  */
538
707
  async login(payload) {
539
708
  const response = await this.http.post("/api/auth/login", payload);
709
+ await this.session.setSession({
710
+ access_token: response.data.access_token,
711
+ refresh_token: response.data.refresh_token
712
+ });
540
713
  return response.data;
541
714
  }
542
715
  /**
@@ -544,6 +717,7 @@ var AuthModule = class {
544
717
  * This method should be called after login when the user has MFA enabled.
545
718
  * The login will return a pre-auth token with a short expiration (5 minutes).
546
719
  * Exchange the pre-auth token and TOTP code for a full session.
720
+ * Automatically persists the session after successful MFA verification.
547
721
  *
548
722
  * @param preauthToken The pre-authentication token received from login
549
723
  * @param code The TOTP code from the user's authenticator app or a backup code
@@ -562,9 +736,7 @@ var AuthModule = class {
562
736
  * // User needs to provide MFA code
563
737
  * const mfaCode = prompt('Enter your 6-digit code from authenticator app');
564
738
  * const tokens = await sso.auth.verifyMfa(loginResponse.access_token, mfaCode);
565
- * sso.setAuthToken(tokens.access_token);
566
- * localStorage.setItem('sso_access_token', tokens.access_token);
567
- * localStorage.setItem('sso_refresh_token', tokens.refresh_token);
739
+ * // Session is automatically saved - no need for manual token management
568
740
  * }
569
741
  * ```
570
742
  */
@@ -574,6 +746,10 @@ var AuthModule = class {
574
746
  code,
575
747
  ...deviceCodeId && { device_code_id: deviceCodeId }
576
748
  });
749
+ await this.session.setSession({
750
+ access_token: response.data.access_token,
751
+ refresh_token: response.data.refresh_token
752
+ });
577
753
  return response.data;
578
754
  }
579
755
  /**
@@ -616,6 +792,52 @@ var AuthModule = class {
616
792
  const response = await this.http.post("/api/auth/reset-password", payload);
617
793
  return response.data;
618
794
  }
795
+ // ============================================================================
796
+ // HOME REALM DISCOVERY (HRD)
797
+ // ============================================================================
798
+ /**
799
+ * Lookup an email address to determine which authentication method to use.
800
+ * This implements Home Realm Discovery (HRD), allowing users to simply enter
801
+ * their email address and be automatically routed to the correct identity provider.
802
+ *
803
+ * The system will:
804
+ * 1. Extract the domain from the email address
805
+ * 2. Check if the domain is verified and mapped to an enterprise IdP
806
+ * 3. Return routing information (connection_id) if found
807
+ * 4. Otherwise, indicate to use default authentication (password or OAuth)
808
+ *
809
+ * @param email The user's email address
810
+ * @returns HRD response with routing information
811
+ *
812
+ * @example
813
+ * ```typescript
814
+ * // Lookup email to determine authentication flow
815
+ * const result = await sso.auth.lookupEmail('john@acmecorp.com');
816
+ *
817
+ * if (result.auth_method === 'upstream' && result.connection_id) {
818
+ * // Route to enterprise IdP
819
+ * console.log(`Redirecting to ${result.provider_name}`);
820
+ * const url = sso.auth.getLoginUrl('github', {
821
+ * org: 'acme-corp',
822
+ * service: 'main-app',
823
+ * connection_id: result.connection_id
824
+ * });
825
+ * window.location.href = url;
826
+ * } else if (result.auth_method === 'password') {
827
+ * // Show password login form
828
+ * showPasswordForm();
829
+ * } else {
830
+ * // Show default OAuth provider buttons (GitHub, Google, Microsoft)
831
+ * showOAuthButtons();
832
+ * }
833
+ * ```
834
+ */
835
+ async lookupEmail(email) {
836
+ const response = await this.http.post("/api/auth/lookup-email", {
837
+ email
838
+ });
839
+ return response.data;
840
+ }
619
841
  };
620
842
 
621
843
  // src/modules/user.ts
@@ -752,11 +974,127 @@ var MfaModule = class {
752
974
  return response.data;
753
975
  }
754
976
  };
977
+ var DevicesModule = class {
978
+ constructor(http) {
979
+ this.http = http;
980
+ }
981
+ /**
982
+ * List all devices associated with the authenticated user.
983
+ *
984
+ * @param options Optional query parameters for pagination
985
+ * @returns Array of user devices
986
+ *
987
+ * @example
988
+ * ```typescript
989
+ * const { devices, total } = await sso.user.devices.list();
990
+ * console.log(devices); // Array of trusted devices
991
+ * ```
992
+ */
993
+ async list(options) {
994
+ const params = new URLSearchParams();
995
+ if (options?.page) params.append("page", options.page.toString());
996
+ if (options?.limit) params.append("limit", options.limit.toString());
997
+ if (options?.sort_by) params.append("sort_by", options.sort_by);
998
+ if (options?.sort_order) params.append("sort_order", options.sort_order);
999
+ const query = params.toString();
1000
+ const url = `/api/user/devices${query ? `?${query}` : ""}`;
1001
+ const response = await this.http.get(url);
1002
+ return response.data;
1003
+ }
1004
+ /**
1005
+ * Get details for a specific device.
1006
+ *
1007
+ * @param deviceId The device ID to retrieve
1008
+ * @returns Device details
1009
+ *
1010
+ * @example
1011
+ * ```typescript
1012
+ * const device = await sso.user.devices.get('device-123');
1013
+ * console.log(device.device_name, device.is_trusted);
1014
+ * ```
1015
+ */
1016
+ async get(deviceId) {
1017
+ const response = await this.http.get(`/api/user/devices/${deviceId}`);
1018
+ return response.data;
1019
+ }
1020
+ /**
1021
+ * Revoke access for a specific device.
1022
+ * This will remove the device's trust and require re-authentication.
1023
+ *
1024
+ * @param deviceId The device ID to revoke
1025
+ * @param reason Optional reason for revocation
1026
+ * @returns Confirmation message
1027
+ *
1028
+ * @example
1029
+ * ```typescript
1030
+ * const result = await sso.user.devices.revoke('device-123', 'Device lost');
1031
+ * console.log(result.message);
1032
+ * ```
1033
+ */
1034
+ async revoke(deviceId, reason) {
1035
+ const payload = reason ? { reason } : {};
1036
+ const response = await this.http.post(`/api/user/devices/${deviceId}/revoke`, payload);
1037
+ return response.data;
1038
+ }
1039
+ /**
1040
+ * Revoke all devices except the current one.
1041
+ * This is useful when you suspect account compromise or want to force re-authentication on all devices.
1042
+ *
1043
+ * @returns Confirmation message
1044
+ *
1045
+ * @example
1046
+ * ```typescript
1047
+ * const result = await sso.user.devices.revokeAll();
1048
+ * console.log(result.message); // "All other devices have been revoked"
1049
+ * ```
1050
+ */
1051
+ async revokeAll() {
1052
+ const response = await this.http.post("/api/user/devices/revoke-all", {});
1053
+ return response.data;
1054
+ }
1055
+ /**
1056
+ * Update the name of a device.
1057
+ *
1058
+ * @param deviceId The device ID to update
1059
+ * @param deviceName New device name
1060
+ * @returns Updated device information
1061
+ *
1062
+ * @example
1063
+ * ```typescript
1064
+ * const device = await sso.user.devices.updateName('device-123', 'My Laptop');
1065
+ * console.log(device.device_name); // "My Laptop"
1066
+ * ```
1067
+ */
1068
+ async updateName(deviceId, deviceName) {
1069
+ const response = await this.http.patch(`/api/user/devices/${deviceId}`, {
1070
+ device_name: deviceName
1071
+ });
1072
+ return response.data;
1073
+ }
1074
+ /**
1075
+ * Mark a device as trusted manually.
1076
+ * This is useful for devices that you want to explicitly trust regardless of risk assessment.
1077
+ *
1078
+ * @param deviceId The device ID to trust
1079
+ * @returns Updated device information
1080
+ *
1081
+ * @example
1082
+ * ```typescript
1083
+ * const device = await sso.user.devices.trust('device-123');
1084
+ * console.log(device.is_trusted); // true
1085
+ * ```
1086
+ */
1087
+ async trust(deviceId) {
1088
+ const response = await this.http.post(`/api/user/devices/${deviceId}/trust`, {});
1089
+ return response.data;
1090
+ }
1091
+ };
755
1092
  var UserModule = class {
756
1093
  constructor(http) {
757
1094
  this.http = http;
758
1095
  this.identities = new IdentitiesModule(http);
759
1096
  this.mfa = new MfaModule(http);
1097
+ this.devices = new DevicesModule(http);
760
1098
  }
761
1099
  /**
762
1100
  * Get the profile of the currently authenticated user.
@@ -1318,129 +1656,345 @@ var OrganizationsModule = class {
1318
1656
  return response.data;
1319
1657
  }
1320
1658
  };
1321
- this.auditLogs = new AuditLogsModule(http);
1322
- this.webhooks = new WebhooksModule(http);
1323
- }
1324
- /**
1325
- * Create a new organization (public endpoint).
1326
- * The organization will be created with 'pending' status and requires
1327
- * platform owner approval before becoming active.
1328
- *
1329
- * @param payload Organization creation payload
1330
- * @returns Created organization with owner and membership details
1331
- *
1332
- * @example
1333
- * ```typescript
1334
- * const result = await sso.organizations.createPublic({
1335
- * slug: 'acme-corp',
1336
- * name: 'Acme Corporation',
1337
- * owner_email: 'founder@acme.com'
1338
- * });
1339
- * ```
1340
- */
1341
- async createPublic(payload) {
1342
- const response = await this.http.post("/api/organizations", payload);
1343
- return response.data;
1344
- }
1345
- /**
1346
- * List all organizations the authenticated user is a member of.
1347
- *
1348
- * @param params Optional query parameters for filtering and pagination
1349
- * @returns Array of organization responses
1350
- *
1351
- * @example
1352
- * ```typescript
1353
- * const orgs = await sso.organizations.list({
1354
- * status: 'active',
1355
- * page: 1,
1356
- * limit: 20
1357
- * });
1358
- * ```
1359
- */
1360
- async list(params) {
1361
- const response = await this.http.get("/api/organizations", { params });
1362
- return response.data;
1363
- }
1364
- /**
1365
- * Get detailed information for a specific organization.
1366
- *
1367
- * @param orgSlug Organization slug
1368
- * @returns Organization details
1369
- *
1370
- * @example
1371
- * ```typescript
1372
- * const org = await sso.organizations.get('acme-corp');
1373
- * console.log(org.organization.name, org.membership_count);
1374
- * ```
1375
- */
1376
- async get(orgSlug) {
1377
- const response = await this.http.get(`/api/organizations/${orgSlug}`);
1378
- return response.data;
1379
- }
1380
- /**
1381
- * Update organization details.
1382
- * Requires 'owner' or 'admin' role.
1383
- *
1384
- * @param orgSlug Organization slug
1385
- * @param payload Update payload
1386
- * @returns Updated organization details
1387
- *
1388
- * @example
1389
- * ```typescript
1390
- * const updated = await sso.organizations.update('acme-corp', {
1391
- * name: 'Acme Corporation Inc.',
1392
- * max_services: 20
1393
- * });
1394
- * ```
1395
- */
1396
- async update(orgSlug, payload) {
1397
- const response = await this.http.patch(
1398
- `/api/organizations/${orgSlug}`,
1399
- payload
1400
- );
1401
- return response.data;
1402
- }
1403
- /**
1404
- * Delete an organization and all its associated data.
1405
- * This is a destructive operation that cannot be undone.
1406
- * Requires 'owner' role.
1407
- *
1408
- * All related data will be cascaded deleted including:
1409
- * - Members and invitations
1410
- * - Services and plans
1411
- * - Subscriptions
1412
- * - OAuth credentials
1413
- * - Audit logs
1414
- *
1415
- * @param orgSlug Organization slug
1416
- *
1417
- * @example
1418
- * ```typescript
1419
- * await sso.organizations.delete('acme-corp');
1420
- * ```
1421
- */
1422
- async delete(orgSlug) {
1423
- await this.http.delete(`/api/organizations/${orgSlug}`);
1424
- }
1425
- // ============================================================================
1426
- // SMTP MANAGEMENT
1427
- // ============================================================================
1428
- /**
1429
- * Configure SMTP settings for an organization.
1430
- * Only owners and admins can configure SMTP.
1431
- * The organization will use these settings for sending transactional emails
1432
- * (registration, password reset, etc.).
1433
- *
1434
- * @param orgSlug Organization slug
1435
- * @param config SMTP configuration
1436
- * @returns Success message
1437
- *
1438
- * @example
1439
- * ```typescript
1440
- * await sso.organizations.setSmtp('acme-corp', {
1441
- * host: 'smtp.gmail.com',
1442
- * port: 587,
1443
- * username: 'notifications@acme.com',
1659
+ // ============================================================================
1660
+ // RISK SETTINGS
1661
+ // ============================================================================
1662
+ /**
1663
+ * Risk settings management methods
1664
+ */
1665
+ this.riskSettings = {
1666
+ /**
1667
+ * Get risk settings for an organization.
1668
+ * Requires 'owner' or 'admin' role.
1669
+ *
1670
+ * @param orgSlug Organization slug
1671
+ * @returns Risk settings configuration
1672
+ *
1673
+ * @example
1674
+ * ```typescript
1675
+ * const settings = await sso.organizations.riskSettings.get('acme-corp');
1676
+ * console.log('Enforcement mode:', settings.enforcement_mode);
1677
+ * console.log('Low threshold:', settings.low_threshold);
1678
+ * ```
1679
+ */
1680
+ get: async (orgSlug) => {
1681
+ const response = await this.http.get(
1682
+ `/api/organizations/${orgSlug}/risk-settings`
1683
+ );
1684
+ return response.data;
1685
+ },
1686
+ /**
1687
+ * Update risk settings for an organization.
1688
+ * Requires 'owner' or 'admin' role.
1689
+ *
1690
+ * @param orgSlug Organization slug
1691
+ * @param payload Risk settings update payload
1692
+ * @returns Updated risk settings
1693
+ *
1694
+ * @example
1695
+ * ```typescript
1696
+ * const result = await sso.organizations.riskSettings.update('acme-corp', {
1697
+ * enforcement_mode: 'challenge',
1698
+ * low_threshold: 30,
1699
+ * medium_threshold: 70,
1700
+ * new_device_score: 20,
1701
+ * impossible_travel_score: 50
1702
+ * });
1703
+ * console.log(result.message);
1704
+ * ```
1705
+ */
1706
+ update: async (orgSlug, payload) => {
1707
+ const response = await this.http.put(
1708
+ `/api/organizations/${orgSlug}/risk-settings`,
1709
+ payload
1710
+ );
1711
+ return response.data;
1712
+ },
1713
+ /**
1714
+ * Reset risk settings to default values.
1715
+ * Requires 'owner' or 'admin' role.
1716
+ *
1717
+ * @param orgSlug Organization slug
1718
+ * @returns Reset confirmation with default values
1719
+ *
1720
+ * @example
1721
+ * ```typescript
1722
+ * const result = await sso.organizations.riskSettings.reset('acme-corp');
1723
+ * console.log('Risk settings reset to defaults');
1724
+ * ```
1725
+ */
1726
+ reset: async (orgSlug) => {
1727
+ const response = await this.http.post(
1728
+ `/api/organizations/${orgSlug}/risk-settings/reset`
1729
+ );
1730
+ return response.data;
1731
+ }
1732
+ };
1733
+ // ============================================================================
1734
+ // SIEM CONFIGURATIONS
1735
+ // ============================================================================
1736
+ /**
1737
+ * SIEM (Security Information and Event Management) configuration methods
1738
+ */
1739
+ this.siem = {
1740
+ /**
1741
+ * Create a new SIEM configuration.
1742
+ * Requires 'owner' or 'admin' role.
1743
+ *
1744
+ * @param orgSlug Organization slug
1745
+ * @param payload SIEM configuration payload
1746
+ * @returns Created SIEM configuration
1747
+ *
1748
+ * @example
1749
+ * ```typescript
1750
+ * const config = await sso.organizations.siem.create('acme-corp', {
1751
+ * name: 'Datadog Integration',
1752
+ * provider_type: 'Datadog',
1753
+ * endpoint_url: 'https://http-intake.logs.datadoghq.com/v1/input',
1754
+ * api_key: 'dd-api-key',
1755
+ * batch_size: 100
1756
+ * });
1757
+ * ```
1758
+ */
1759
+ create: async (orgSlug, payload) => {
1760
+ const response = await this.http.post(
1761
+ `/api/organizations/${orgSlug}/siem-configs`,
1762
+ payload
1763
+ );
1764
+ return response.data;
1765
+ },
1766
+ /**
1767
+ * List all SIEM configurations for an organization.
1768
+ * Requires 'owner' or 'admin' role.
1769
+ *
1770
+ * @param orgSlug Organization slug
1771
+ * @returns List of SIEM configurations
1772
+ *
1773
+ * @example
1774
+ * ```typescript
1775
+ * const result = await sso.organizations.siem.list('acme-corp');
1776
+ * console.log(`Total SIEM configs: ${result.total}`);
1777
+ * result.siem_configs.forEach(config => {
1778
+ * console.log(config.name, config.provider_type, config.enabled);
1779
+ * });
1780
+ * ```
1781
+ */
1782
+ list: async (orgSlug) => {
1783
+ const response = await this.http.get(
1784
+ `/api/organizations/${orgSlug}/siem-configs`
1785
+ );
1786
+ return response.data;
1787
+ },
1788
+ /**
1789
+ * Get a specific SIEM configuration.
1790
+ * Requires 'owner' or 'admin' role.
1791
+ *
1792
+ * @param orgSlug Organization slug
1793
+ * @param configId SIEM configuration ID
1794
+ * @returns SIEM configuration
1795
+ *
1796
+ * @example
1797
+ * ```typescript
1798
+ * const config = await sso.organizations.siem.get('acme-corp', 'config-id');
1799
+ * console.log(config.name, config.endpoint_url);
1800
+ * ```
1801
+ */
1802
+ get: async (orgSlug, configId) => {
1803
+ const response = await this.http.get(
1804
+ `/api/organizations/${orgSlug}/siem-configs/${configId}`
1805
+ );
1806
+ return response.data;
1807
+ },
1808
+ /**
1809
+ * Update a SIEM configuration.
1810
+ * Requires 'owner' or 'admin' role.
1811
+ *
1812
+ * @param orgSlug Organization slug
1813
+ * @param configId SIEM configuration ID
1814
+ * @param payload Update payload
1815
+ * @returns Updated SIEM configuration
1816
+ *
1817
+ * @example
1818
+ * ```typescript
1819
+ * const updated = await sso.organizations.siem.update('acme-corp', 'config-id', {
1820
+ * enabled: false,
1821
+ * batch_size: 200
1822
+ * });
1823
+ * ```
1824
+ */
1825
+ update: async (orgSlug, configId, payload) => {
1826
+ const response = await this.http.patch(
1827
+ `/api/organizations/${orgSlug}/siem-configs/${configId}`,
1828
+ payload
1829
+ );
1830
+ return response.data;
1831
+ },
1832
+ /**
1833
+ * Delete a SIEM configuration.
1834
+ * Requires 'owner' or 'admin' role.
1835
+ *
1836
+ * @param orgSlug Organization slug
1837
+ * @param configId SIEM configuration ID
1838
+ *
1839
+ * @example
1840
+ * ```typescript
1841
+ * await sso.organizations.siem.delete('acme-corp', 'config-id');
1842
+ * console.log('SIEM configuration deleted');
1843
+ * ```
1844
+ */
1845
+ delete: async (orgSlug, configId) => {
1846
+ await this.http.delete(`/api/organizations/${orgSlug}/siem-configs/${configId}`);
1847
+ },
1848
+ /**
1849
+ * Test connection to a SIEM endpoint.
1850
+ * Sends a test event to verify the configuration.
1851
+ * Requires 'owner' or 'admin' role.
1852
+ *
1853
+ * @param orgSlug Organization slug
1854
+ * @param configId SIEM configuration ID
1855
+ * @returns Test result
1856
+ *
1857
+ * @example
1858
+ * ```typescript
1859
+ * const result = await sso.organizations.siem.test('acme-corp', 'config-id');
1860
+ * if (result.success) {
1861
+ * console.log('Connection successful:', result.message);
1862
+ * } else {
1863
+ * console.error('Connection failed:', result.message);
1864
+ * }
1865
+ * ```
1866
+ */
1867
+ test: async (orgSlug, configId) => {
1868
+ const response = await this.http.post(
1869
+ `/api/organizations/${orgSlug}/siem-configs/${configId}/test`
1870
+ );
1871
+ return response.data;
1872
+ }
1873
+ };
1874
+ this.auditLogs = new AuditLogsModule(http);
1875
+ this.webhooks = new WebhooksModule(http);
1876
+ }
1877
+ /**
1878
+ * Create a new organization (requires authentication).
1879
+ * The authenticated user becomes the organization owner.
1880
+ * Returns JWT tokens with organization context, eliminating the need to re-authenticate.
1881
+ *
1882
+ * @param payload Organization creation payload
1883
+ * @returns Created organization with owner, membership, and JWT tokens
1884
+ *
1885
+ * @example
1886
+ * ```typescript
1887
+ * const result = await sso.organizations.create({
1888
+ * slug: 'acme-corp',
1889
+ * name: 'Acme Corporation'
1890
+ * });
1891
+ * // Store the new tokens with org context
1892
+ * authStore.setTokens(result.access_token, result.refresh_token);
1893
+ * ```
1894
+ */
1895
+ async create(payload) {
1896
+ const response = await this.http.post("/api/organizations", payload);
1897
+ return response.data;
1898
+ }
1899
+ /**
1900
+ * List all organizations the authenticated user is a member of.
1901
+ *
1902
+ * @param params Optional query parameters for filtering and pagination
1903
+ * @returns Array of organization responses
1904
+ *
1905
+ * @example
1906
+ * ```typescript
1907
+ * const orgs = await sso.organizations.list({
1908
+ * status: 'active',
1909
+ * page: 1,
1910
+ * limit: 20
1911
+ * });
1912
+ * ```
1913
+ */
1914
+ async list(params) {
1915
+ const response = await this.http.get("/api/organizations", { params });
1916
+ return response.data;
1917
+ }
1918
+ /**
1919
+ * Get detailed information for a specific organization.
1920
+ *
1921
+ * @param orgSlug Organization slug
1922
+ * @returns Organization details
1923
+ *
1924
+ * @example
1925
+ * ```typescript
1926
+ * const org = await sso.organizations.get('acme-corp');
1927
+ * console.log(org.organization.name, org.membership_count);
1928
+ * ```
1929
+ */
1930
+ async get(orgSlug) {
1931
+ const response = await this.http.get(`/api/organizations/${orgSlug}`);
1932
+ return response.data;
1933
+ }
1934
+ /**
1935
+ * Update organization details.
1936
+ * Requires 'owner' or 'admin' role.
1937
+ *
1938
+ * @param orgSlug Organization slug
1939
+ * @param payload Update payload
1940
+ * @returns Updated organization details
1941
+ *
1942
+ * @example
1943
+ * ```typescript
1944
+ * const updated = await sso.organizations.update('acme-corp', {
1945
+ * name: 'Acme Corporation Inc.',
1946
+ * max_services: 20
1947
+ * });
1948
+ * ```
1949
+ */
1950
+ async update(orgSlug, payload) {
1951
+ const response = await this.http.patch(
1952
+ `/api/organizations/${orgSlug}`,
1953
+ payload
1954
+ );
1955
+ return response.data;
1956
+ }
1957
+ /**
1958
+ * Delete an organization and all its associated data.
1959
+ * This is a destructive operation that cannot be undone.
1960
+ * Requires 'owner' role.
1961
+ *
1962
+ * All related data will be cascaded deleted including:
1963
+ * - Members and invitations
1964
+ * - Services and plans
1965
+ * - Subscriptions
1966
+ * - OAuth credentials
1967
+ * - Audit logs
1968
+ *
1969
+ * @param orgSlug Organization slug
1970
+ *
1971
+ * @example
1972
+ * ```typescript
1973
+ * await sso.organizations.delete('acme-corp');
1974
+ * ```
1975
+ */
1976
+ async delete(orgSlug) {
1977
+ await this.http.delete(`/api/organizations/${orgSlug}`);
1978
+ }
1979
+ // ============================================================================
1980
+ // SMTP MANAGEMENT
1981
+ // ============================================================================
1982
+ /**
1983
+ * Configure SMTP settings for an organization.
1984
+ * Only owners and admins can configure SMTP.
1985
+ * The organization will use these settings for sending transactional emails
1986
+ * (registration, password reset, etc.).
1987
+ *
1988
+ * @param orgSlug Organization slug
1989
+ * @param config SMTP configuration
1990
+ * @returns Success message
1991
+ *
1992
+ * @example
1993
+ * ```typescript
1994
+ * await sso.organizations.setSmtp('acme-corp', {
1995
+ * host: 'smtp.gmail.com',
1996
+ * port: 587,
1997
+ * username: 'notifications@acme.com',
1444
1998
  * password: 'your-app-password',
1445
1999
  * from_email: 'notifications@acme.com',
1446
2000
  * from_name: 'Acme Corp'
@@ -2053,19 +2607,54 @@ var ServicesModule = class {
2053
2607
  return `${baseURL}/saml/${orgSlug}/${serviceSlug}/sso`;
2054
2608
  }
2055
2609
  };
2056
- }
2057
- /**
2058
- * Create a new service for an organization.
2059
- * Requires 'owner' or 'admin' role.
2060
- *
2061
- * @param orgSlug Organization slug
2062
- * @param payload Service creation payload
2063
- * @returns Created service with details
2064
- *
2065
- * @example
2066
- * ```typescript
2067
- * const result = await sso.services.create('acme-corp', {
2068
- * slug: 'main-app',
2610
+ /**
2611
+ * Stripe billing and checkout methods
2612
+ */
2613
+ this.billing = {
2614
+ /**
2615
+ * Create a Stripe checkout session for the authenticated user to subscribe to a plan.
2616
+ * Requires organization membership.
2617
+ *
2618
+ * IMPORTANT: The plan must have a `stripe_price_id` configured to be available for purchase.
2619
+ *
2620
+ * @param orgSlug Organization slug
2621
+ * @param serviceSlug Service slug
2622
+ * @param payload Checkout payload containing plan_id and redirect URLs
2623
+ * @returns Checkout session with URL to redirect user to
2624
+ *
2625
+ * @example
2626
+ * ```typescript
2627
+ * const checkout = await sso.services.billing.createCheckout('acme-corp', 'main-app', {
2628
+ * plan_id: 'plan_pro',
2629
+ * success_url: 'https://app.acme.com/billing/success?session_id={CHECKOUT_SESSION_ID}',
2630
+ * cancel_url: 'https://app.acme.com/billing/cancel'
2631
+ * });
2632
+ *
2633
+ * // Redirect user to Stripe checkout
2634
+ * window.location.href = checkout.checkout_url;
2635
+ * ```
2636
+ */
2637
+ createCheckout: async (orgSlug, serviceSlug, payload) => {
2638
+ const response = await this.http.post(
2639
+ `/api/organizations/${orgSlug}/services/${serviceSlug}/checkout`,
2640
+ payload
2641
+ );
2642
+ return response.data;
2643
+ }
2644
+ };
2645
+ }
2646
+ /**
2647
+ * Create a new service for an organization.
2648
+ * Requires 'owner' or 'admin' role.
2649
+ *
2650
+ * @param orgSlug Organization slug
2651
+ * @param payload Service creation payload
2652
+ * @returns Created service with details
2653
+ *
2654
+ * @example
2655
+ * ```typescript
2656
+ * const result = await sso.services.create('acme-corp', {
2657
+ * slug: 'main-app',
2069
2658
  * name: 'Main Application',
2070
2659
  * service_type: 'web',
2071
2660
  * github_scopes: ['user:email', 'read:org'],
@@ -2669,6 +3258,41 @@ var PlatformModule = class {
2669
3258
  const response = await this.http.get("/api/platform/audit-log", { params });
2670
3259
  return response.data;
2671
3260
  }
3261
+ /**
3262
+ * Impersonate a user (Platform Owner or Org Admin only).
3263
+ * Returns a short-lived JWT (15 minutes) that allows acting as the target user.
3264
+ *
3265
+ * Security:
3266
+ * - Platform Owners can impersonate any user
3267
+ * - Organization Admins can only impersonate users within their organization
3268
+ * - All impersonation actions are logged to the platform audit log with HIGH severity
3269
+ * - Tokens contain RFC 8693 actor claim for full audit trail
3270
+ *
3271
+ * @param payload Impersonation details (user_id and reason)
3272
+ * @returns Impersonation token and user context
3273
+ *
3274
+ * @example
3275
+ * ```typescript
3276
+ * const result = await sso.platform.impersonateUser({
3277
+ * user_id: 'user-uuid-123',
3278
+ * reason: 'Investigating support ticket #456'
3279
+ * });
3280
+ *
3281
+ * // Use the returned token to create a new client acting as the user
3282
+ * const userClient = new SsoClient({
3283
+ * baseURL: 'https://sso.example.com',
3284
+ * token: result.token
3285
+ * });
3286
+ *
3287
+ * // Now all requests with userClient are made as the target user
3288
+ * const profile = await userClient.user.getProfile();
3289
+ * console.log('Acting as:', result.target_user.email);
3290
+ * ```
3291
+ */
3292
+ async impersonateUser(payload) {
3293
+ const response = await this.http.post("/api/platform/impersonate", payload);
3294
+ return response.data;
3295
+ }
2672
3296
  };
2673
3297
 
2674
3298
  // src/modules/serviceApi.ts
@@ -2753,24 +3377,640 @@ var ServiceApiModule = class {
2753
3377
  }
2754
3378
  };
2755
3379
 
3380
+ // src/modules/permissions.ts
3381
+ var PermissionsModule = class {
3382
+ constructor(http) {
3383
+ this.http = http;
3384
+ }
3385
+ /**
3386
+ * Check if user has a specific permission.
3387
+ * Fetches from user profile API (which uses cached permissions).
3388
+ *
3389
+ * @param permission Permission in format "namespace:object_id#relation"
3390
+ * @returns true if the permission is present
3391
+ *
3392
+ * @example
3393
+ * ```typescript
3394
+ * const hasAccess = await sso.permissions.hasPermission('organization:acme#owner');
3395
+ * ```
3396
+ */
3397
+ async hasPermission(permission) {
3398
+ const response = await this.http.get("/api/user");
3399
+ return response.data.permissions.includes(permission);
3400
+ }
3401
+ /**
3402
+ * Get all user permissions.
3403
+ * Fetches from user profile API (which uses cached permissions).
3404
+ *
3405
+ * @returns Array of permission strings
3406
+ *
3407
+ * @example
3408
+ * ```typescript
3409
+ * const permissions = await sso.permissions.listPermissions();
3410
+ * // ["organization:acme#owner", "service:api#admin"]
3411
+ * ```
3412
+ */
3413
+ async listPermissions() {
3414
+ const response = await this.http.get("/api/user");
3415
+ return response.data.permissions;
3416
+ }
3417
+ /**
3418
+ * Check if user has access to a feature.
3419
+ *
3420
+ * @param feature Feature name to check
3421
+ * @returns true if the feature is available
3422
+ *
3423
+ * @example
3424
+ * ```typescript
3425
+ * const canExport = await sso.permissions.hasFeature('advanced-export');
3426
+ * ```
3427
+ */
3428
+ async hasFeature(feature) {
3429
+ const response = await this.http.get("/api/user");
3430
+ return response.data.features?.includes(feature) ?? false;
3431
+ }
3432
+ /**
3433
+ * Get current plan name.
3434
+ *
3435
+ * @returns Current plan name or null if not in org/service context
3436
+ *
3437
+ * @example
3438
+ * ```typescript
3439
+ * const plan = await sso.permissions.getPlan();
3440
+ * console.log(plan); // "pro", "enterprise", etc.
3441
+ * ```
3442
+ */
3443
+ async getPlan() {
3444
+ const response = await this.http.get("/api/user");
3445
+ return response.data.plan;
3446
+ }
3447
+ /**
3448
+ * Check if user has a specific permission on a resource.
3449
+ *
3450
+ * @param namespace The permission namespace (e.g., "organization", "service")
3451
+ * @param objectId The object ID (e.g., organization slug, service slug)
3452
+ * @param relation The relation type (e.g., "owner", "admin", "member")
3453
+ * @returns true if the user has the permission
3454
+ *
3455
+ * @example
3456
+ * ```typescript
3457
+ * const isOwner = await sso.permissions.can('organization', 'acme-corp', 'owner');
3458
+ * ```
3459
+ */
3460
+ async can(namespace, objectId, relation) {
3461
+ return this.hasPermission(`${namespace}:${objectId}#${relation}`);
3462
+ }
3463
+ /**
3464
+ * Check if user is a member of an organization.
3465
+ *
3466
+ * @param orgId The organization ID or slug
3467
+ * @returns true if the user is a member
3468
+ *
3469
+ * @example
3470
+ * ```typescript
3471
+ * if (await sso.permissions.isOrgMember('acme-corp')) {
3472
+ * // User is a member
3473
+ * }
3474
+ * ```
3475
+ */
3476
+ async isOrgMember(orgId) {
3477
+ return this.hasPermission(`organization:${orgId}#member`);
3478
+ }
3479
+ /**
3480
+ * Check if user is an admin of an organization.
3481
+ *
3482
+ * @param orgId The organization ID or slug
3483
+ * @returns true if the user is an admin
3484
+ *
3485
+ * @example
3486
+ * ```typescript
3487
+ * if (await sso.permissions.isOrgAdmin('acme-corp')) {
3488
+ * // User is an admin
3489
+ * }
3490
+ * ```
3491
+ */
3492
+ async isOrgAdmin(orgId) {
3493
+ return this.hasPermission(`organization:${orgId}#admin`);
3494
+ }
3495
+ /**
3496
+ * Check if user is an owner of an organization.
3497
+ *
3498
+ * @param orgId The organization ID or slug
3499
+ * @returns true if the user is an owner
3500
+ *
3501
+ * @example
3502
+ * ```typescript
3503
+ * if (await sso.permissions.isOrgOwner('acme-corp')) {
3504
+ * // User is an owner
3505
+ * }
3506
+ * ```
3507
+ */
3508
+ async isOrgOwner(orgId) {
3509
+ return this.hasPermission(`organization:${orgId}#owner`);
3510
+ }
3511
+ /**
3512
+ * Check if user has access to a service.
3513
+ *
3514
+ * @param serviceId The service ID or slug
3515
+ * @returns true if the user has access
3516
+ *
3517
+ * @example
3518
+ * ```typescript
3519
+ * if (await sso.permissions.hasServiceAccess('api-service')) {
3520
+ * // User has access to the service
3521
+ * }
3522
+ * ```
3523
+ */
3524
+ async hasServiceAccess(serviceId) {
3525
+ return this.hasPermission(`service:${serviceId}#member`);
3526
+ }
3527
+ /**
3528
+ * Filter permissions by namespace.
3529
+ *
3530
+ * @param namespace The namespace to filter by (e.g., "organization", "service")
3531
+ * @returns Array of permissions matching the namespace
3532
+ *
3533
+ * @example
3534
+ * ```typescript
3535
+ * const orgPermissions = await sso.permissions.getPermissionsByNamespace('organization');
3536
+ * ```
3537
+ */
3538
+ async getPermissionsByNamespace(namespace) {
3539
+ const allPermissions = await this.listPermissions();
3540
+ return allPermissions.filter((p) => p.startsWith(`${namespace}:`));
3541
+ }
3542
+ // ============================================================================
3543
+ // DEPRECATED METHODS - Token-based permission checking (legacy)
3544
+ // ============================================================================
3545
+ /**
3546
+ * @deprecated Use `hasPermission()` instead (without token parameter)
3547
+ * Decode a JWT token to extract claims (including permissions)
3548
+ * Note: This does NOT verify the signature - it only decodes the payload
3549
+ *
3550
+ * @param token The JWT access token
3551
+ * @returns The decoded JWT claims
3552
+ * @throws Error if the token is malformed
3553
+ */
3554
+ decodeToken(token) {
3555
+ try {
3556
+ const parts = token.split(".");
3557
+ if (parts.length !== 3) {
3558
+ throw new Error("Invalid JWT format");
3559
+ }
3560
+ const payload = parts[1];
3561
+ const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
3562
+ return JSON.parse(decoded);
3563
+ } catch (error) {
3564
+ throw new Error(`Failed to decode JWT: ${error instanceof Error ? error.message : "Unknown error"}`);
3565
+ }
3566
+ }
3567
+ /**
3568
+ * @deprecated JWT tokens no longer contain permissions. Use `hasPermission(permission)` instead.
3569
+ * Check if a JWT token contains a specific permission
3570
+ *
3571
+ * @param token The JWT access token (ignored)
3572
+ * @param permission Permission in format "namespace:object_id#relation"
3573
+ * @returns true if the permission is present in the token
3574
+ */
3575
+ hasPermissionFromToken(token, permission) {
3576
+ const claims = this.decodeToken(token);
3577
+ return claims.permissions?.includes(permission) ?? false;
3578
+ }
3579
+ /**
3580
+ * @deprecated JWT tokens no longer contain permissions. Use `can(namespace, objectId, relation)` instead.
3581
+ * Check if a user has a specific permission on a resource
3582
+ *
3583
+ * @param token The JWT access token (ignored)
3584
+ * @param namespace The permission namespace (e.g., "organization", "service")
3585
+ * @param objectId The object ID (e.g., organization slug, service slug)
3586
+ * @param relation The relation type (e.g., "owner", "admin", "member")
3587
+ * @returns true if the user has the permission
3588
+ */
3589
+ canFromToken(token, namespace, objectId, relation) {
3590
+ return this.hasPermissionFromToken(token, `${namespace}:${objectId}#${relation}`);
3591
+ }
3592
+ /**
3593
+ * @deprecated JWT tokens no longer contain permissions. Use `isOrgMember(orgId)` instead.
3594
+ * Check if user is a member of an organization
3595
+ *
3596
+ * @param token The JWT access token (ignored)
3597
+ * @param orgId The organization ID or slug
3598
+ * @returns true if the user is a member
3599
+ */
3600
+ isOrgMemberFromToken(token, orgId) {
3601
+ return this.hasPermissionFromToken(token, `organization:${orgId}#member`);
3602
+ }
3603
+ /**
3604
+ * @deprecated JWT tokens no longer contain permissions. Use `isOrgAdmin(orgId)` instead.
3605
+ * Check if user is an admin of an organization
3606
+ *
3607
+ * @param token The JWT access token (ignored)
3608
+ * @param orgId The organization ID or slug
3609
+ * @returns true if the user is an admin
3610
+ */
3611
+ isOrgAdminFromToken(token, orgId) {
3612
+ return this.hasPermissionFromToken(token, `organization:${orgId}#admin`);
3613
+ }
3614
+ /**
3615
+ * @deprecated JWT tokens no longer contain permissions. Use `isOrgOwner(orgId)` instead.
3616
+ * Check if user is an owner of an organization
3617
+ *
3618
+ * @param token The JWT access token (ignored)
3619
+ * @param orgId The organization ID or slug
3620
+ * @returns true if the user is an owner
3621
+ */
3622
+ isOrgOwnerFromToken(token, orgId) {
3623
+ return this.hasPermissionFromToken(token, `organization:${orgId}#owner`);
3624
+ }
3625
+ /**
3626
+ * @deprecated JWT tokens no longer contain permissions. Use `hasServiceAccess(serviceId)` instead.
3627
+ * Check if user has access to a service
3628
+ *
3629
+ * @param token The JWT access token (ignored)
3630
+ * @param serviceId The service ID or slug
3631
+ * @returns true if the user has access
3632
+ */
3633
+ hasServiceAccessFromToken(token, serviceId) {
3634
+ return this.hasPermissionFromToken(token, `service:${serviceId}#member`);
3635
+ }
3636
+ /**
3637
+ * @deprecated JWT tokens no longer contain permissions. Use `listPermissions()` instead.
3638
+ * Get all permissions from a JWT token
3639
+ *
3640
+ * @param token The JWT access token
3641
+ * @returns Array of permission strings, or empty array if none
3642
+ */
3643
+ getAllPermissionsFromToken(token) {
3644
+ const claims = this.decodeToken(token);
3645
+ return claims.permissions ?? [];
3646
+ }
3647
+ /**
3648
+ * Parse a permission string into its components
3649
+ *
3650
+ * @param permission Permission string in format "namespace:object_id#relation"
3651
+ * @returns Parsed permission components or null if invalid format
3652
+ *
3653
+ * @example
3654
+ * ```typescript
3655
+ * const parsed = sso.permissions.parsePermission('organization:acme#owner');
3656
+ * // { namespace: 'organization', objectId: 'acme', relation: 'owner' }
3657
+ * ```
3658
+ */
3659
+ parsePermission(permission) {
3660
+ const match = permission.match(/^([^:]+):([^#]+)#(.+)$/);
3661
+ if (!match) {
3662
+ return null;
3663
+ }
3664
+ return {
3665
+ namespace: match[1],
3666
+ objectId: match[2],
3667
+ relation: match[3]
3668
+ };
3669
+ }
3670
+ /**
3671
+ * @deprecated JWT tokens no longer contain permissions. Use `getPermissionsByNamespace(namespace)` instead.
3672
+ * Filter permissions by namespace
3673
+ *
3674
+ * @param token The JWT access token (ignored)
3675
+ * @param namespace The namespace to filter by (e.g., "organization", "service")
3676
+ * @returns Array of permissions matching the namespace
3677
+ */
3678
+ getPermissionsByNamespaceFromToken(token, namespace) {
3679
+ const allPermissions = this.getAllPermissionsFromToken(token);
3680
+ return allPermissions.filter((p) => p.startsWith(`${namespace}:`));
3681
+ }
3682
+ };
3683
+
3684
+ // src/modules/passkeys.ts
3685
+ var PasskeysModule = class {
3686
+ constructor(http) {
3687
+ this.http = http;
3688
+ }
3689
+ /**
3690
+ * Check if WebAuthn is supported in the current browser
3691
+ */
3692
+ isSupported() {
3693
+ return typeof window !== "undefined" && window.PublicKeyCredential !== void 0 && typeof window.PublicKeyCredential === "function";
3694
+ }
3695
+ /**
3696
+ * Check if platform authenticator (like Touch ID, Face ID, Windows Hello) is available
3697
+ */
3698
+ async isPlatformAuthenticatorAvailable() {
3699
+ if (!this.isSupported()) {
3700
+ return false;
3701
+ }
3702
+ return PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
3703
+ }
3704
+ /**
3705
+ * Register a new passkey for the authenticated user
3706
+ *
3707
+ * This method requires an authenticated session (JWT token must be set).
3708
+ * It starts the WebAuthn registration ceremony, prompts the user to create
3709
+ * a passkey using their device's authenticator (e.g., Touch ID, Face ID,
3710
+ * Windows Hello, or hardware security key), and stores the credential.
3711
+ *
3712
+ * @param displayName Optional display name for the passkey
3713
+ * @returns Promise resolving to the registered passkey ID
3714
+ * @throws {Error} If WebAuthn is not supported or registration fails
3715
+ *
3716
+ * @example
3717
+ * ```typescript
3718
+ * try {
3719
+ * const passkeyId = await sso.passkeys.register('My MacBook Pro');
3720
+ * console.log('Passkey registered:', passkeyId);
3721
+ * } catch (error) {
3722
+ * console.error('Passkey registration failed:', error);
3723
+ * }
3724
+ * ```
3725
+ */
3726
+ async register(displayName) {
3727
+ if (!this.isSupported()) {
3728
+ throw new Error("WebAuthn is not supported in this browser");
3729
+ }
3730
+ const startResponse = await this.http.post(
3731
+ "/auth/passkeys/register/start",
3732
+ { name: displayName }
3733
+ );
3734
+ const startData = startResponse.data;
3735
+ const createOptions = {
3736
+ publicKey: {
3737
+ ...startData.options,
3738
+ challenge: this.base64UrlToUint8Array(startData.options.challenge),
3739
+ user: {
3740
+ ...startData.options.user,
3741
+ id: this.base64UrlToUint8Array(startData.options.user.id)
3742
+ },
3743
+ excludeCredentials: startData.options.excludeCredentials?.map((cred) => ({
3744
+ ...cred,
3745
+ id: this.base64UrlToUint8Array(cred.id)
3746
+ }))
3747
+ }
3748
+ };
3749
+ const credential = await navigator.credentials.create(createOptions);
3750
+ if (!credential || !(credential instanceof PublicKeyCredential)) {
3751
+ throw new Error("Failed to create passkey");
3752
+ }
3753
+ if (!(credential.response instanceof AuthenticatorAttestationResponse)) {
3754
+ throw new Error("Invalid credential response type");
3755
+ }
3756
+ const credentialJSON = {
3757
+ id: credential.id,
3758
+ rawId: this.uint8ArrayToBase64Url(new Uint8Array(credential.rawId)),
3759
+ response: {
3760
+ clientDataJSON: this.uint8ArrayToBase64Url(new Uint8Array(credential.response.clientDataJSON)),
3761
+ attestationObject: this.uint8ArrayToBase64Url(new Uint8Array(credential.response.attestationObject)),
3762
+ transports: credential.response.getTransports?.()
3763
+ },
3764
+ authenticatorAttachment: credential.authenticatorAttachment,
3765
+ clientExtensionResults: credential.getClientExtensionResults(),
3766
+ type: credential.type
3767
+ };
3768
+ const finishResponse = await this.http.post(
3769
+ "/auth/passkeys/register/finish",
3770
+ {
3771
+ challenge_id: startData.challenge_id,
3772
+ credential: credentialJSON
3773
+ }
3774
+ );
3775
+ return finishResponse.data.passkey_id;
3776
+ }
3777
+ /**
3778
+ * Authenticate with a passkey and obtain a JWT token
3779
+ *
3780
+ * This method prompts the user to authenticate using their passkey.
3781
+ * Upon successful authentication, a JWT token is returned which can
3782
+ * be used to make authenticated API requests.
3783
+ *
3784
+ * @param email User's email address
3785
+ * @returns Promise resolving to authentication response with JWT token
3786
+ * @throws {Error} If WebAuthn is not supported or authentication fails
3787
+ *
3788
+ * @example
3789
+ * ```typescript
3790
+ * try {
3791
+ * const { token, user_id } = await sso.passkeys.login('user@example.com');
3792
+ * sso.setToken(token);
3793
+ * console.log('Logged in as:', user_id);
3794
+ * } catch (error) {
3795
+ * console.error('Passkey login failed:', error);
3796
+ * }
3797
+ * ```
3798
+ */
3799
+ async login(email) {
3800
+ if (!this.isSupported()) {
3801
+ throw new Error("WebAuthn is not supported in this browser");
3802
+ }
3803
+ const startRequest = { email };
3804
+ const startResponse = await this.http.post(
3805
+ "/auth/passkeys/authenticate/start",
3806
+ startRequest
3807
+ );
3808
+ const startData = startResponse.data;
3809
+ const getOptions = {
3810
+ publicKey: {
3811
+ ...startData.options,
3812
+ challenge: this.base64UrlToUint8Array(startData.options.challenge),
3813
+ allowCredentials: startData.options.allowCredentials?.map((cred) => ({
3814
+ ...cred,
3815
+ id: this.base64UrlToUint8Array(cred.id)
3816
+ }))
3817
+ }
3818
+ };
3819
+ const credential = await navigator.credentials.get(getOptions);
3820
+ if (!credential || !(credential instanceof PublicKeyCredential)) {
3821
+ throw new Error("Failed to get passkey");
3822
+ }
3823
+ if (!(credential.response instanceof AuthenticatorAssertionResponse)) {
3824
+ throw new Error("Invalid credential response type");
3825
+ }
3826
+ const credentialJSON = {
3827
+ id: credential.id,
3828
+ rawId: this.uint8ArrayToBase64Url(new Uint8Array(credential.rawId)),
3829
+ response: {
3830
+ clientDataJSON: this.uint8ArrayToBase64Url(new Uint8Array(credential.response.clientDataJSON)),
3831
+ authenticatorData: this.uint8ArrayToBase64Url(new Uint8Array(credential.response.authenticatorData)),
3832
+ signature: this.uint8ArrayToBase64Url(new Uint8Array(credential.response.signature)),
3833
+ userHandle: credential.response.userHandle ? this.uint8ArrayToBase64Url(new Uint8Array(credential.response.userHandle)) : void 0
3834
+ },
3835
+ authenticatorAttachment: credential.authenticatorAttachment,
3836
+ clientExtensionResults: credential.getClientExtensionResults(),
3837
+ type: credential.type
3838
+ };
3839
+ const finishResponse = await this.http.post(
3840
+ "/auth/passkeys/authenticate/finish",
3841
+ {
3842
+ challenge_id: startData.challenge_id,
3843
+ credential: credentialJSON
3844
+ }
3845
+ );
3846
+ return finishResponse.data;
3847
+ }
3848
+ /**
3849
+ * Convert Base64URL string to Uint8Array
3850
+ */
3851
+ base64UrlToUint8Array(base64url) {
3852
+ const padding = "=".repeat((4 - base64url.length % 4) % 4);
3853
+ const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/") + padding;
3854
+ const rawData = atob(base64);
3855
+ const outputArray = new Uint8Array(rawData.length);
3856
+ for (let i = 0; i < rawData.length; ++i) {
3857
+ outputArray[i] = rawData.charCodeAt(i);
3858
+ }
3859
+ return outputArray;
3860
+ }
3861
+ /**
3862
+ * Convert Uint8Array to Base64URL string
3863
+ */
3864
+ uint8ArrayToBase64Url(array) {
3865
+ let binary = "";
3866
+ for (let i = 0; i < array.byteLength; i++) {
3867
+ binary += String.fromCharCode(array[i]);
3868
+ }
3869
+ const base64 = btoa(binary);
3870
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
3871
+ }
3872
+ };
3873
+
3874
+ // src/modules/magic.ts
3875
+ var MagicLinks = class {
3876
+ constructor(http) {
3877
+ this.http = http;
3878
+ }
3879
+ /**
3880
+ * Request a magic link to be sent to the user's email
3881
+ *
3882
+ * @param data Magic link request data
3883
+ * @returns Promise resolving to magic link response
3884
+ */
3885
+ async request(data) {
3886
+ const response = await this.http.post("/api/auth/magic-link/request", data);
3887
+ return response.data;
3888
+ }
3889
+ /**
3890
+ * Verify a magic link token and complete authentication
3891
+ * Note: This is typically handled by redirecting to the magic link URL
3892
+ * The backend will handle verification and either redirect or return tokens
3893
+ *
3894
+ * @param token The magic link token to verify
3895
+ * @param redirectUri Optional where to redirect after success
3896
+ * @returns URL to redirect to for verification
3897
+ */
3898
+ getVerificationUrl(token, redirectUri) {
3899
+ const params = new URLSearchParams({ token });
3900
+ if (redirectUri) {
3901
+ params.append("redirect_uri", redirectUri);
3902
+ }
3903
+ return `/api/auth/magic-link/verify?${params.toString()}`;
3904
+ }
3905
+ /**
3906
+ * Verify a magic link token via API call
3907
+ * This is an alternative to redirect-based verification
3908
+ *
3909
+ * @param token The magic link token
3910
+ * @param redirectUri Optional redirect URI
3911
+ * @returns Promise resolving to authentication response
3912
+ */
3913
+ async verify(token, redirectUri) {
3914
+ const params = new URLSearchParams({ token });
3915
+ if (redirectUri) {
3916
+ params.append("redirect_uri", redirectUri);
3917
+ }
3918
+ return this.http.get(`/api/auth/magic-link/verify?${params.toString()}`);
3919
+ }
3920
+ /**
3921
+ * Construct the complete magic link URL that would be sent via email
3922
+ *
3923
+ * @param token The magic link token
3924
+ * @param redirectUri Optional redirect URI
3925
+ * @returns Complete magic link URL
3926
+ */
3927
+ constructMagicLink(token, redirectUri) {
3928
+ return this.getVerificationUrl(token, redirectUri);
3929
+ }
3930
+ };
3931
+
3932
+ // src/modules/privacy.ts
3933
+ var PrivacyModule = class {
3934
+ constructor(http) {
3935
+ this.http = http;
3936
+ }
3937
+ /**
3938
+ * Export all user data (GDPR Right to Access).
3939
+ * Users can export their own data, or organization owners can export their members' data.
3940
+ *
3941
+ * @param userId User ID to export data for
3942
+ * @returns Complete user data export including memberships, login events, identities, MFA events, and passkeys
3943
+ *
3944
+ * @example
3945
+ * ```typescript
3946
+ * const userData = await sso.privacy.exportData('user-id');
3947
+ * console.log(`Exported ${userData.login_events_count} login events`);
3948
+ * console.log(`User has ${userData.memberships.length} organization memberships`);
3949
+ * ```
3950
+ */
3951
+ async exportData(userId) {
3952
+ const response = await this.http.get(`/api/privacy/export/${userId}`);
3953
+ return response.data;
3954
+ }
3955
+ /**
3956
+ * Anonymize user data (GDPR Right to be Forgotten).
3957
+ * Requires organization owner permission for all organizations the user is a member of.
3958
+ * Platform owners cannot be anonymized.
3959
+ *
3960
+ * This operation:
3961
+ * - Soft-deletes the user account
3962
+ * - Hard-deletes PII from identities and passkeys tables
3963
+ * - Preserves audit logs for compliance
3964
+ *
3965
+ * @param userId User ID to anonymize
3966
+ * @returns Anonymization confirmation response
3967
+ *
3968
+ * @example
3969
+ * ```typescript
3970
+ * const result = await sso.privacy.forgetUser('user-id');
3971
+ * console.log(result.message);
3972
+ * // "User data has been anonymized. PII has been removed while preserving audit logs."
3973
+ * ```
3974
+ */
3975
+ async forgetUser(userId) {
3976
+ const response = await this.http.delete(`/api/privacy/forget/${userId}`);
3977
+ return response.data;
3978
+ }
3979
+ };
3980
+
2756
3981
  // src/client.ts
2757
3982
  var SsoClient = class {
2758
3983
  constructor(options) {
2759
3984
  this.http = createHttpAgent(options.baseURL);
2760
- if (options.token) {
2761
- this.setAuthToken(options.token);
2762
- }
2763
- if (options.apiKey) {
2764
- this.setApiKey(options.apiKey);
2765
- }
3985
+ this.session = new SessionManager(
3986
+ resolveStorage(options.storage),
3987
+ async (refreshToken) => {
3988
+ const res = await this.http.post("/api/auth/refresh", { refresh_token: refreshToken });
3989
+ return res.data;
3990
+ },
3991
+ { storageKeyPrefix: options.storagePrefix || "sso_" }
3992
+ );
3993
+ this.http.setSessionManager(this.session);
2766
3994
  this.analytics = new AnalyticsModule(this.http);
2767
- this.auth = new AuthModule(this.http);
3995
+ this.auth = new AuthModule(this.http, this.session);
2768
3996
  this.user = new UserModule(this.http);
2769
3997
  this.organizations = new OrganizationsModule(this.http);
2770
3998
  this.services = new ServicesModule(this.http);
2771
3999
  this.invitations = new InvitationsModule(this.http);
2772
4000
  this.platform = new PlatformModule(this.http);
2773
4001
  this.serviceApi = new ServiceApiModule(this.http);
4002
+ this.permissions = new PermissionsModule(this.http);
4003
+ this.passkeys = new PasskeysModule(this.http);
4004
+ this.magicLinks = new MagicLinks(this.http);
4005
+ this.privacy = new PrivacyModule(this.http);
4006
+ if (options.apiKey) {
4007
+ this.setApiKey(options.apiKey);
4008
+ }
4009
+ if (options.token) {
4010
+ this.session.setSession({ access_token: options.token });
4011
+ } else {
4012
+ this.session.loadSession().catch(console.error);
4013
+ }
2774
4014
  }
2775
4015
  /**
2776
4016
  * Sets the JWT for all subsequent authenticated requests.
@@ -2822,12 +4062,95 @@ var SsoClient = class {
2822
4062
  getBaseURL() {
2823
4063
  return this.http.defaults.baseURL || "";
2824
4064
  }
4065
+ /**
4066
+ * Check if the user is currently authenticated
4067
+ */
4068
+ isAuthenticated() {
4069
+ return this.session.isAuthenticated();
4070
+ }
4071
+ /**
4072
+ * Subscribe to authentication state changes.
4073
+ * Useful for updating UI when login/logout/expiration occurs.
4074
+ *
4075
+ * @param listener Callback function that receives the authentication state
4076
+ * @returns Unsubscribe function
4077
+ *
4078
+ * @example
4079
+ * ```typescript
4080
+ * const unsubscribe = sso.onAuthStateChange((isAuth) => {
4081
+ * console.log(isAuth ? 'User is logged in' : 'User is logged out');
4082
+ * });
4083
+ *
4084
+ * // Later, to stop listening
4085
+ * unsubscribe();
4086
+ * ```
4087
+ */
4088
+ onAuthStateChange(listener) {
4089
+ return this.session.subscribe(listener);
4090
+ }
4091
+ /**
4092
+ * Manually retrieve the current access token
4093
+ *
4094
+ * @returns The current access token, or null if not authenticated
4095
+ */
4096
+ async getToken() {
4097
+ return this.session.getToken();
4098
+ }
2825
4099
  };
4100
+
4101
+ // src/types/risk.ts
4102
+ var RiskAction = /* @__PURE__ */ ((RiskAction2) => {
4103
+ RiskAction2["ALLOW"] = "allow";
4104
+ RiskAction2["LOG_ONLY"] = "log_only";
4105
+ RiskAction2["CHALLENGE_MFA"] = "challenge_mfa";
4106
+ RiskAction2["BLOCK"] = "block";
4107
+ return RiskAction2;
4108
+ })(RiskAction || {});
4109
+ var RiskFactorType = /* @__PURE__ */ ((RiskFactorType2) => {
4110
+ RiskFactorType2["NEW_IP"] = "new_ip";
4111
+ RiskFactorType2["HIGH_RISK_LOCATION"] = "high_risk_location";
4112
+ RiskFactorType2["IMPOSSIBLE_TRAVEL"] = "impossible_travel";
4113
+ RiskFactorType2["NEW_DEVICE"] = "new_device";
4114
+ RiskFactorType2["FAILED_ATTEMPTS"] = "failed_attempts";
4115
+ RiskFactorType2["UNUSUAL_TIME"] = "unusual_time";
4116
+ RiskFactorType2["SUSPICIOUS_USER_AGENT"] = "suspicious_user_agent";
4117
+ RiskFactorType2["ANONYMOUS_NETWORK"] = "anonymous_network";
4118
+ RiskFactorType2["NEW_ACCOUNT"] = "new_account";
4119
+ RiskFactorType2["SUSPICIOUS_HISTORY"] = "suspicious_history";
4120
+ RiskFactorType2["HIGH_VELOCITY"] = "high_velocity";
4121
+ RiskFactorType2["CUSTOM_RULE"] = "custom_rule";
4122
+ return RiskFactorType2;
4123
+ })(RiskFactorType || {});
4124
+ var AuthMethod = /* @__PURE__ */ ((AuthMethod2) => {
4125
+ AuthMethod2["PASSWORD"] = "password";
4126
+ AuthMethod2["OAUTH"] = "oauth";
4127
+ AuthMethod2["PASSKEY"] = "passkey";
4128
+ AuthMethod2["MAGIC_LINK"] = "magic_link";
4129
+ AuthMethod2["MFA"] = "mfa";
4130
+ AuthMethod2["SAML"] = "saml";
4131
+ return AuthMethod2;
4132
+ })(AuthMethod || {});
4133
+ var RiskEventOutcome = /* @__PURE__ */ ((RiskEventOutcome2) => {
4134
+ RiskEventOutcome2["ALLOWED"] = "allowed";
4135
+ RiskEventOutcome2["BLOCKED"] = "blocked";
4136
+ RiskEventOutcome2["CHALLENGED"] = "challenged";
4137
+ RiskEventOutcome2["LOGGED"] = "logged";
4138
+ return RiskEventOutcome2;
4139
+ })(RiskEventOutcome || {});
2826
4140
  export {
4141
+ AuthMethod,
2827
4142
  AuthModule,
4143
+ BrowserStorage,
2828
4144
  InvitationsModule,
4145
+ MagicLinks,
4146
+ MemoryStorage,
2829
4147
  OrganizationsModule,
4148
+ PasskeysModule,
4149
+ PermissionsModule,
2830
4150
  PlatformModule,
4151
+ RiskAction,
4152
+ RiskEventOutcome,
4153
+ RiskFactorType,
2831
4154
  ServiceApiModule,
2832
4155
  ServicesModule,
2833
4156
  SsoApiError,