@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.js CHANGED
@@ -20,10 +20,19 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ AuthMethod: () => AuthMethod,
23
24
  AuthModule: () => AuthModule,
25
+ BrowserStorage: () => BrowserStorage,
24
26
  InvitationsModule: () => InvitationsModule,
27
+ MagicLinks: () => MagicLinks,
28
+ MemoryStorage: () => MemoryStorage,
25
29
  OrganizationsModule: () => OrganizationsModule,
30
+ PasskeysModule: () => PasskeysModule,
31
+ PermissionsModule: () => PermissionsModule,
26
32
  PlatformModule: () => PlatformModule,
33
+ RiskAction: () => RiskAction,
34
+ RiskEventOutcome: () => RiskEventOutcome,
35
+ RiskFactorType: () => RiskFactorType,
27
36
  ServiceApiModule: () => ServiceApiModule,
28
37
  ServicesModule: () => ServicesModule,
29
38
  SsoApiError: () => SsoApiError,
@@ -83,6 +92,12 @@ var HttpClient = class {
83
92
  timeout: 3e4
84
93
  };
85
94
  }
95
+ /**
96
+ * Allow injecting session manager after construction to avoid circular dep
97
+ */
98
+ setSessionManager(manager) {
99
+ this.sessionManager = manager;
100
+ }
86
101
  /**
87
102
  * Build query string from params object
88
103
  */
@@ -116,6 +131,12 @@ var HttpClient = class {
116
131
  ...this.defaults.headers.common,
117
132
  ...options.headers
118
133
  };
134
+ if (this.sessionManager) {
135
+ const token = await this.sessionManager.getToken();
136
+ if (token) {
137
+ headers["Authorization"] = `Bearer ${token}`;
138
+ }
139
+ }
119
140
  const controller = new AbortController();
120
141
  const timeoutId = setTimeout(() => controller.abort(), timeout);
121
142
  try {
@@ -126,6 +147,17 @@ var HttpClient = class {
126
147
  signal: controller.signal
127
148
  });
128
149
  clearTimeout(timeoutId);
150
+ if (response.status === 401 && this.sessionManager && !options._retry && !path.includes("/auth/login")) {
151
+ try {
152
+ const newToken = await this.sessionManager.refreshSession();
153
+ return this.request(path, {
154
+ ...options,
155
+ _retry: true,
156
+ headers: { ...options.headers, Authorization: `Bearer ${newToken}` }
157
+ });
158
+ } catch (refreshError) {
159
+ }
160
+ }
129
161
  let data;
130
162
  const contentType = response.headers.get("content-type");
131
163
  if (contentType?.includes("application/json")) {
@@ -194,6 +226,16 @@ var HttpClient = class {
194
226
  headers: config?.headers
195
227
  });
196
228
  }
229
+ /**
230
+ * PUT request
231
+ */
232
+ async put(path, data, config) {
233
+ return this.request(path, {
234
+ method: "PUT",
235
+ body: data,
236
+ headers: config?.headers
237
+ });
238
+ }
197
239
  /**
198
240
  * PATCH request
199
241
  */
@@ -218,6 +260,125 @@ function createHttpAgent(baseURL) {
218
260
  return new HttpClient(baseURL);
219
261
  }
220
262
 
263
+ // src/session.ts
264
+ var SessionManager = class {
265
+ constructor(storage, refreshHandler, config = { storageKeyPrefix: "sso_" }) {
266
+ this.storage = storage;
267
+ this.refreshHandler = refreshHandler;
268
+ this.config = config;
269
+ this.accessToken = null;
270
+ this.refreshToken = null;
271
+ this.refreshPromise = null;
272
+ this.listeners = [];
273
+ }
274
+ /**
275
+ * Initialize session from storage
276
+ */
277
+ async loadSession() {
278
+ this.accessToken = await this.storage.getItem(`${this.config.storageKeyPrefix}access_token`);
279
+ this.refreshToken = await this.storage.getItem(`${this.config.storageKeyPrefix}refresh_token`);
280
+ }
281
+ /**
282
+ * Set the session data (used after login/register)
283
+ */
284
+ async setSession(tokens) {
285
+ this.accessToken = tokens.access_token;
286
+ await this.storage.setItem(`${this.config.storageKeyPrefix}access_token`, tokens.access_token);
287
+ if (tokens.refresh_token) {
288
+ this.refreshToken = tokens.refresh_token;
289
+ await this.storage.setItem(`${this.config.storageKeyPrefix}refresh_token`, tokens.refresh_token);
290
+ }
291
+ this.notifyListeners(true);
292
+ }
293
+ /**
294
+ * Clear session (logout)
295
+ */
296
+ async clearSession() {
297
+ this.accessToken = null;
298
+ this.refreshToken = null;
299
+ await this.storage.removeItem(`${this.config.storageKeyPrefix}access_token`);
300
+ await this.storage.removeItem(`${this.config.storageKeyPrefix}refresh_token`);
301
+ this.notifyListeners(false);
302
+ }
303
+ /**
304
+ * Get the current access token, refreshing it if necessary/possible
305
+ */
306
+ async getToken() {
307
+ return this.accessToken;
308
+ }
309
+ /**
310
+ * Handle logic for when a 401 occurs
311
+ */
312
+ async refreshSession() {
313
+ if (!this.refreshToken) {
314
+ throw new Error("No refresh token available");
315
+ }
316
+ if (this.refreshPromise) {
317
+ return this.refreshPromise;
318
+ }
319
+ this.refreshPromise = (async () => {
320
+ try {
321
+ const tokens = await this.refreshHandler(this.refreshToken);
322
+ await this.setSession(tokens);
323
+ return tokens.access_token;
324
+ } catch (err) {
325
+ await this.clearSession();
326
+ throw err;
327
+ } finally {
328
+ this.refreshPromise = null;
329
+ }
330
+ })();
331
+ return this.refreshPromise;
332
+ }
333
+ isAuthenticated() {
334
+ return !!this.accessToken;
335
+ }
336
+ /**
337
+ * Subscribe to auth state changes (useful for UI updates)
338
+ */
339
+ subscribe(listener) {
340
+ this.listeners.push(listener);
341
+ return () => {
342
+ this.listeners = this.listeners.filter((l) => l !== listener);
343
+ };
344
+ }
345
+ notifyListeners(isAuth) {
346
+ this.listeners.forEach((l) => l(isAuth));
347
+ }
348
+ };
349
+
350
+ // src/storage.ts
351
+ var MemoryStorage = class {
352
+ constructor() {
353
+ this.store = /* @__PURE__ */ new Map();
354
+ }
355
+ getItem(key) {
356
+ return this.store.get(key) || null;
357
+ }
358
+ setItem(key, value) {
359
+ this.store.set(key, value);
360
+ }
361
+ removeItem(key) {
362
+ this.store.delete(key);
363
+ }
364
+ };
365
+ var BrowserStorage = class {
366
+ getItem(key) {
367
+ return typeof window !== "undefined" ? window.localStorage.getItem(key) : null;
368
+ }
369
+ setItem(key, value) {
370
+ if (typeof window !== "undefined") window.localStorage.setItem(key, value);
371
+ }
372
+ removeItem(key) {
373
+ if (typeof window !== "undefined") window.localStorage.removeItem(key);
374
+ }
375
+ };
376
+ function resolveStorage(userStorage) {
377
+ if (userStorage) return userStorage;
378
+ if (typeof window !== "undefined" && window.localStorage) return new BrowserStorage();
379
+ return new MemoryStorage();
380
+ }
381
+
221
382
  // src/modules/analytics.ts
222
383
  var AnalyticsModule = class {
223
384
  constructor(http) {
@@ -323,8 +484,9 @@ var AnalyticsModule = class {
323
484
 
324
485
  // src/modules/auth.ts
325
486
  var AuthModule = class {
326
- constructor(http) {
487
+ constructor(http, session) {
327
488
  this.http = http;
489
+ this.session = session;
328
490
  /**
329
491
  * Device Flow: Request a device code for CLI/device authentication.
330
492
  *
@@ -371,6 +533,9 @@ var AuthModule = class {
371
533
  /**
372
534
  * Exchange a device code for a JWT token.
373
535
  * This should be polled by the device/CLI after displaying the user code.
536
+ * Note: This returns a TokenResponse (not RefreshTokenResponse) and typically
537
+ * only includes access_token. For device flows that need persistence,
538
+ * manually call sso.session.setSession() if needed.
374
539
  *
375
540
  * @param payload Token request payload
376
541
  * @returns Token response with JWT
@@ -386,7 +551,7 @@ var AuthModule = class {
386
551
  * client_id: 'service-client-id'
387
552
  * });
388
553
  * clearInterval(interval);
389
- * sso.setAuthToken(token.access_token);
554
+ * // Session is automatically configured
390
555
  * } catch (error) {
391
556
  * if (error.errorCode !== 'authorization_pending') {
392
557
  * clearInterval(interval);
@@ -408,17 +573,29 @@ var AuthModule = class {
408
573
  * should redirect the user's browser to this URL.
409
574
  *
410
575
  * @param provider The OAuth provider to use
411
- * @param params Login parameters (org, service, redirect_uri)
576
+ * @param params Login parameters (org, service, redirect_uri, connection_id)
412
577
  * @returns The full URL to redirect the user to
413
578
  *
414
579
  * @example
415
580
  * ```typescript
581
+ * // Standard OAuth login
416
582
  * const url = sso.auth.getLoginUrl('github', {
417
583
  * org: 'acme-corp',
418
584
  * service: 'main-app',
419
585
  * redirect_uri: 'https://app.acme.com/callback'
420
586
  * });
421
587
  * window.location.href = url;
588
+ *
589
+ * // Enterprise IdP login (after HRD lookup)
590
+ * const hrd = await sso.auth.lookupEmail('user@enterprise.com');
591
+ * if (hrd.connection_id) {
592
+ * const url = sso.auth.getLoginUrl('github', {
593
+ * org: 'acme-corp',
594
+ * service: 'main-app',
595
+ * connection_id: hrd.connection_id
596
+ * });
597
+ * window.location.href = url;
598
+ * }
422
599
  * ```
423
600
  */
424
601
  getLoginUrl(provider, params) {
@@ -433,6 +610,9 @@ var AuthModule = class {
433
610
  if (params.user_code) {
434
611
  searchParams.append("user_code", params.user_code);
435
612
  }
613
+ if (params.connection_id) {
614
+ searchParams.append("connection_id", params.connection_id);
615
+ }
436
616
  return `${baseURL}/auth/${provider}?${searchParams.toString()}`;
437
617
  }
438
618
  /**
@@ -465,19 +645,20 @@ var AuthModule = class {
465
645
  }
466
646
  /**
467
647
  * Logout the current user by revoking their JWT.
468
- * After calling this, you should clear the token from storage
469
- * and call `sso.setAuthToken(null)`.
648
+ * Automatically clears the session and tokens from storage.
470
649
  *
471
650
  * @example
472
651
  * ```typescript
473
652
  * await sso.auth.logout();
474
- * sso.setAuthToken(null);
475
- * localStorage.removeItem('sso_access_token');
476
- * localStorage.removeItem('sso_refresh_token');
653
+ * // Session is automatically cleared - no need for manual cleanup
477
654
  * ```
478
655
  */
479
656
  async logout() {
480
- await this.http.post("/api/auth/logout");
657
+ try {
658
+ await this.http.post("/api/auth/logout");
659
+ } finally {
660
+ await this.session.clearSession();
661
+ }
481
662
  }
482
663
  /**
483
664
  * Refresh an expired JWT access token using a refresh token.
@@ -552,8 +733,7 @@ var AuthModule = class {
552
733
  }
553
734
  /**
554
735
  * Login with email and password.
555
- * Returns access token and refresh token on successful authentication.
556
- * The user's email must be verified before login.
736
+ * Automatically persists the session and configures the client.
557
737
  *
558
738
  * @param payload Login credentials (email and password)
559
739
  * @returns Access token, refresh token, and expiration info
@@ -564,13 +744,15 @@ var AuthModule = class {
564
744
  * email: 'user@example.com',
565
745
  * password: 'SecurePassword123!'
566
746
  * });
567
- * sso.setAuthToken(tokens.access_token);
568
- * localStorage.setItem('sso_access_token', tokens.access_token);
569
- * localStorage.setItem('sso_refresh_token', tokens.refresh_token);
747
+ * // Session is automatically saved - no need for manual token management
570
748
  * ```
571
749
  */
572
750
  async login(payload) {
573
751
  const response = await this.http.post("/api/auth/login", payload);
752
+ await this.session.setSession({
753
+ access_token: response.data.access_token,
754
+ refresh_token: response.data.refresh_token
755
+ });
574
756
  return response.data;
575
757
  }
576
758
  /**
@@ -578,6 +760,7 @@ var AuthModule = class {
578
760
  * This method should be called after login when the user has MFA enabled.
579
761
  * The login will return a pre-auth token with a short expiration (5 minutes).
580
762
  * Exchange the pre-auth token and TOTP code for a full session.
763
+ * Automatically persists the session after successful MFA verification.
581
764
  *
582
765
  * @param preauthToken The pre-authentication token received from login
583
766
  * @param code The TOTP code from the user's authenticator app or a backup code
@@ -596,9 +779,7 @@ var AuthModule = class {
596
779
  * // User needs to provide MFA code
597
780
  * const mfaCode = prompt('Enter your 6-digit code from authenticator app');
598
781
  * const tokens = await sso.auth.verifyMfa(loginResponse.access_token, mfaCode);
599
- * sso.setAuthToken(tokens.access_token);
600
- * localStorage.setItem('sso_access_token', tokens.access_token);
601
- * localStorage.setItem('sso_refresh_token', tokens.refresh_token);
782
+ * // Session is automatically saved - no need for manual token management
602
783
  * }
603
784
  * ```
604
785
  */
@@ -608,6 +789,10 @@ var AuthModule = class {
608
789
  code,
609
790
  ...deviceCodeId && { device_code_id: deviceCodeId }
610
791
  });
792
+ await this.session.setSession({
793
+ access_token: response.data.access_token,
794
+ refresh_token: response.data.refresh_token
795
+ });
611
796
  return response.data;
612
797
  }
613
798
  /**
@@ -650,6 +835,52 @@ var AuthModule = class {
650
835
  const response = await this.http.post("/api/auth/reset-password", payload);
651
836
  return response.data;
652
837
  }
838
+ // ============================================================================
839
+ // HOME REALM DISCOVERY (HRD)
840
+ // ============================================================================
841
+ /**
842
+ * Lookup an email address to determine which authentication method to use.
843
+ * This implements Home Realm Discovery (HRD), allowing users to simply enter
844
+ * their email address and be automatically routed to the correct identity provider.
845
+ *
846
+ * The system will:
847
+ * 1. Extract the domain from the email address
848
+ * 2. Check if the domain is verified and mapped to an enterprise IdP
849
+ * 3. Return routing information (connection_id) if found
850
+ * 4. Otherwise, indicate to use default authentication (password or OAuth)
851
+ *
852
+ * @param email The user's email address
853
+ * @returns HRD response with routing information
854
+ *
855
+ * @example
856
+ * ```typescript
857
+ * // Lookup email to determine authentication flow
858
+ * const result = await sso.auth.lookupEmail('john@acmecorp.com');
859
+ *
860
+ * if (result.auth_method === 'upstream' && result.connection_id) {
861
+ * // Route to enterprise IdP
862
+ * console.log(`Redirecting to ${result.provider_name}`);
863
+ * const url = sso.auth.getLoginUrl('github', {
864
+ * org: 'acme-corp',
865
+ * service: 'main-app',
866
+ * connection_id: result.connection_id
867
+ * });
868
+ * window.location.href = url;
869
+ * } else if (result.auth_method === 'password') {
870
+ * // Show password login form
871
+ * showPasswordForm();
872
+ * } else {
873
+ * // Show default OAuth provider buttons (GitHub, Google, Microsoft)
874
+ * showOAuthButtons();
875
+ * }
876
+ * ```
877
+ */
878
+ async lookupEmail(email) {
879
+ const response = await this.http.post("/api/auth/lookup-email", {
880
+ email
881
+ });
882
+ return response.data;
883
+ }
653
884
  };
654
885
 
655
886
  // src/modules/user.ts
@@ -786,11 +1017,127 @@ var MfaModule = class {
786
1017
  return response.data;
787
1018
  }
788
1019
  };
1020
+ var DevicesModule = class {
1021
+ constructor(http) {
1022
+ this.http = http;
1023
+ }
1024
+ /**
1025
+ * List all devices associated with the authenticated user.
1026
+ *
1027
+ * @param options Optional query parameters for pagination
1028
+ * @returns Array of user devices
1029
+ *
1030
+ * @example
1031
+ * ```typescript
1032
+ * const { devices, total } = await sso.user.devices.list();
1033
+ * console.log(devices); // Array of trusted devices
1034
+ * ```
1035
+ */
1036
+ async list(options) {
1037
+ const params = new URLSearchParams();
1038
+ if (options?.page) params.append("page", options.page.toString());
1039
+ if (options?.limit) params.append("limit", options.limit.toString());
1040
+ if (options?.sort_by) params.append("sort_by", options.sort_by);
1041
+ if (options?.sort_order) params.append("sort_order", options.sort_order);
1042
+ const query = params.toString();
1043
+ const url = `/api/user/devices${query ? `?${query}` : ""}`;
1044
+ const response = await this.http.get(url);
1045
+ return response.data;
1046
+ }
1047
+ /**
1048
+ * Get details for a specific device.
1049
+ *
1050
+ * @param deviceId The device ID to retrieve
1051
+ * @returns Device details
1052
+ *
1053
+ * @example
1054
+ * ```typescript
1055
+ * const device = await sso.user.devices.get('device-123');
1056
+ * console.log(device.device_name, device.is_trusted);
1057
+ * ```
1058
+ */
1059
+ async get(deviceId) {
1060
+ const response = await this.http.get(`/api/user/devices/${deviceId}`);
1061
+ return response.data;
1062
+ }
1063
+ /**
1064
+ * Revoke access for a specific device.
1065
+ * This will remove the device's trust and require re-authentication.
1066
+ *
1067
+ * @param deviceId The device ID to revoke
1068
+ * @param reason Optional reason for revocation
1069
+ * @returns Confirmation message
1070
+ *
1071
+ * @example
1072
+ * ```typescript
1073
+ * const result = await sso.user.devices.revoke('device-123', 'Device lost');
1074
+ * console.log(result.message);
1075
+ * ```
1076
+ */
1077
+ async revoke(deviceId, reason) {
1078
+ const payload = reason ? { reason } : {};
1079
+ const response = await this.http.post(`/api/user/devices/${deviceId}/revoke`, payload);
1080
+ return response.data;
1081
+ }
1082
+ /**
1083
+ * Revoke all devices except the current one.
1084
+ * This is useful when you suspect account compromise or want to force re-authentication on all devices.
1085
+ *
1086
+ * @returns Confirmation message
1087
+ *
1088
+ * @example
1089
+ * ```typescript
1090
+ * const result = await sso.user.devices.revokeAll();
1091
+ * console.log(result.message); // "All other devices have been revoked"
1092
+ * ```
1093
+ */
1094
+ async revokeAll() {
1095
+ const response = await this.http.post("/api/user/devices/revoke-all", {});
1096
+ return response.data;
1097
+ }
1098
+ /**
1099
+ * Update the name of a device.
1100
+ *
1101
+ * @param deviceId The device ID to update
1102
+ * @param deviceName New device name
1103
+ * @returns Updated device information
1104
+ *
1105
+ * @example
1106
+ * ```typescript
1107
+ * const device = await sso.user.devices.updateName('device-123', 'My Laptop');
1108
+ * console.log(device.device_name); // "My Laptop"
1109
+ * ```
1110
+ */
1111
+ async updateName(deviceId, deviceName) {
1112
+ const response = await this.http.patch(`/api/user/devices/${deviceId}`, {
1113
+ device_name: deviceName
1114
+ });
1115
+ return response.data;
1116
+ }
1117
+ /**
1118
+ * Mark a device as trusted manually.
1119
+ * This is useful for devices that you want to explicitly trust regardless of risk assessment.
1120
+ *
1121
+ * @param deviceId The device ID to trust
1122
+ * @returns Updated device information
1123
+ *
1124
+ * @example
1125
+ * ```typescript
1126
+ * const device = await sso.user.devices.trust('device-123');
1127
+ * console.log(device.is_trusted); // true
1128
+ * ```
1129
+ */
1130
+ async trust(deviceId) {
1131
+ const response = await this.http.post(`/api/user/devices/${deviceId}/trust`, {});
1132
+ return response.data;
1133
+ }
1134
+ };
789
1135
  var UserModule = class {
790
1136
  constructor(http) {
791
1137
  this.http = http;
792
1138
  this.identities = new IdentitiesModule(http);
793
1139
  this.mfa = new MfaModule(http);
1140
+ this.devices = new DevicesModule(http);
794
1141
  }
795
1142
  /**
796
1143
  * Get the profile of the currently authenticated user.
@@ -1352,129 +1699,345 @@ var OrganizationsModule = class {
1352
1699
  return response.data;
1353
1700
  }
1354
1701
  };
1355
- this.auditLogs = new AuditLogsModule(http);
1356
- this.webhooks = new WebhooksModule(http);
1357
- }
1358
- /**
1359
- * Create a new organization (public endpoint).
1360
- * The organization will be created with 'pending' status and requires
1361
- * platform owner approval before becoming active.
1362
- *
1363
- * @param payload Organization creation payload
1364
- * @returns Created organization with owner and membership details
1365
- *
1366
- * @example
1367
- * ```typescript
1368
- * const result = await sso.organizations.createPublic({
1369
- * slug: 'acme-corp',
1370
- * name: 'Acme Corporation',
1371
- * owner_email: 'founder@acme.com'
1372
- * });
1373
- * ```
1374
- */
1375
- async createPublic(payload) {
1376
- const response = await this.http.post("/api/organizations", payload);
1377
- return response.data;
1378
- }
1379
- /**
1380
- * List all organizations the authenticated user is a member of.
1381
- *
1382
- * @param params Optional query parameters for filtering and pagination
1383
- * @returns Array of organization responses
1384
- *
1385
- * @example
1386
- * ```typescript
1387
- * const orgs = await sso.organizations.list({
1388
- * status: 'active',
1389
- * page: 1,
1390
- * limit: 20
1391
- * });
1392
- * ```
1393
- */
1394
- async list(params) {
1395
- const response = await this.http.get("/api/organizations", { params });
1396
- return response.data;
1397
- }
1398
- /**
1399
- * Get detailed information for a specific organization.
1400
- *
1401
- * @param orgSlug Organization slug
1402
- * @returns Organization details
1403
- *
1404
- * @example
1405
- * ```typescript
1406
- * const org = await sso.organizations.get('acme-corp');
1407
- * console.log(org.organization.name, org.membership_count);
1408
- * ```
1409
- */
1410
- async get(orgSlug) {
1411
- const response = await this.http.get(`/api/organizations/${orgSlug}`);
1412
- return response.data;
1413
- }
1414
- /**
1415
- * Update organization details.
1416
- * Requires 'owner' or 'admin' role.
1417
- *
1418
- * @param orgSlug Organization slug
1419
- * @param payload Update payload
1420
- * @returns Updated organization details
1421
- *
1422
- * @example
1423
- * ```typescript
1424
- * const updated = await sso.organizations.update('acme-corp', {
1425
- * name: 'Acme Corporation Inc.',
1426
- * max_services: 20
1427
- * });
1428
- * ```
1429
- */
1430
- async update(orgSlug, payload) {
1431
- const response = await this.http.patch(
1432
- `/api/organizations/${orgSlug}`,
1433
- payload
1434
- );
1435
- return response.data;
1436
- }
1437
- /**
1438
- * Delete an organization and all its associated data.
1439
- * This is a destructive operation that cannot be undone.
1440
- * Requires 'owner' role.
1441
- *
1442
- * All related data will be cascaded deleted including:
1443
- * - Members and invitations
1444
- * - Services and plans
1445
- * - Subscriptions
1446
- * - OAuth credentials
1447
- * - Audit logs
1448
- *
1449
- * @param orgSlug Organization slug
1450
- *
1451
- * @example
1452
- * ```typescript
1453
- * await sso.organizations.delete('acme-corp');
1454
- * ```
1455
- */
1456
- async delete(orgSlug) {
1457
- await this.http.delete(`/api/organizations/${orgSlug}`);
1458
- }
1459
- // ============================================================================
1460
- // SMTP MANAGEMENT
1461
- // ============================================================================
1462
- /**
1463
- * Configure SMTP settings for an organization.
1464
- * Only owners and admins can configure SMTP.
1465
- * The organization will use these settings for sending transactional emails
1466
- * (registration, password reset, etc.).
1467
- *
1468
- * @param orgSlug Organization slug
1469
- * @param config SMTP configuration
1470
- * @returns Success message
1471
- *
1472
- * @example
1473
- * ```typescript
1474
- * await sso.organizations.setSmtp('acme-corp', {
1475
- * host: 'smtp.gmail.com',
1476
- * port: 587,
1477
- * username: 'notifications@acme.com',
1702
+ // ============================================================================
1703
+ // RISK SETTINGS
1704
+ // ============================================================================
1705
+ /**
1706
+ * Risk settings management methods
1707
+ */
1708
+ this.riskSettings = {
1709
+ /**
1710
+ * Get risk settings for an organization.
1711
+ * Requires 'owner' or 'admin' role.
1712
+ *
1713
+ * @param orgSlug Organization slug
1714
+ * @returns Risk settings configuration
1715
+ *
1716
+ * @example
1717
+ * ```typescript
1718
+ * const settings = await sso.organizations.riskSettings.get('acme-corp');
1719
+ * console.log('Enforcement mode:', settings.enforcement_mode);
1720
+ * console.log('Low threshold:', settings.low_threshold);
1721
+ * ```
1722
+ */
1723
+ get: async (orgSlug) => {
1724
+ const response = await this.http.get(
1725
+ `/api/organizations/${orgSlug}/risk-settings`
1726
+ );
1727
+ return response.data;
1728
+ },
1729
+ /**
1730
+ * Update risk settings for an organization.
1731
+ * Requires 'owner' or 'admin' role.
1732
+ *
1733
+ * @param orgSlug Organization slug
1734
+ * @param payload Risk settings update payload
1735
+ * @returns Updated risk settings
1736
+ *
1737
+ * @example
1738
+ * ```typescript
1739
+ * const result = await sso.organizations.riskSettings.update('acme-corp', {
1740
+ * enforcement_mode: 'challenge',
1741
+ * low_threshold: 30,
1742
+ * medium_threshold: 70,
1743
+ * new_device_score: 20,
1744
+ * impossible_travel_score: 50
1745
+ * });
1746
+ * console.log(result.message);
1747
+ * ```
1748
+ */
1749
+ update: async (orgSlug, payload) => {
1750
+ const response = await this.http.put(
1751
+ `/api/organizations/${orgSlug}/risk-settings`,
1752
+ payload
1753
+ );
1754
+ return response.data;
1755
+ },
1756
+ /**
1757
+ * Reset risk settings to default values.
1758
+ * Requires 'owner' or 'admin' role.
1759
+ *
1760
+ * @param orgSlug Organization slug
1761
+ * @returns Reset confirmation with default values
1762
+ *
1763
+ * @example
1764
+ * ```typescript
1765
+ * const result = await sso.organizations.riskSettings.reset('acme-corp');
1766
+ * console.log('Risk settings reset to defaults');
1767
+ * ```
1768
+ */
1769
+ reset: async (orgSlug) => {
1770
+ const response = await this.http.post(
1771
+ `/api/organizations/${orgSlug}/risk-settings/reset`
1772
+ );
1773
+ return response.data;
1774
+ }
1775
+ };
1776
+ // ============================================================================
1777
+ // SIEM CONFIGURATIONS
1778
+ // ============================================================================
1779
+ /**
1780
+ * SIEM (Security Information and Event Management) configuration methods
1781
+ */
1782
+ this.siem = {
1783
+ /**
1784
+ * Create a new SIEM configuration.
1785
+ * Requires 'owner' or 'admin' role.
1786
+ *
1787
+ * @param orgSlug Organization slug
1788
+ * @param payload SIEM configuration payload
1789
+ * @returns Created SIEM configuration
1790
+ *
1791
+ * @example
1792
+ * ```typescript
1793
+ * const config = await sso.organizations.siem.create('acme-corp', {
1794
+ * name: 'Datadog Integration',
1795
+ * provider_type: 'Datadog',
1796
+ * endpoint_url: 'https://http-intake.logs.datadoghq.com/v1/input',
1797
+ * api_key: 'dd-api-key',
1798
+ * batch_size: 100
1799
+ * });
1800
+ * ```
1801
+ */
1802
+ create: async (orgSlug, payload) => {
1803
+ const response = await this.http.post(
1804
+ `/api/organizations/${orgSlug}/siem-configs`,
1805
+ payload
1806
+ );
1807
+ return response.data;
1808
+ },
1809
+ /**
1810
+ * List all SIEM configurations for an organization.
1811
+ * Requires 'owner' or 'admin' role.
1812
+ *
1813
+ * @param orgSlug Organization slug
1814
+ * @returns List of SIEM configurations
1815
+ *
1816
+ * @example
1817
+ * ```typescript
1818
+ * const result = await sso.organizations.siem.list('acme-corp');
1819
+ * console.log(`Total SIEM configs: ${result.total}`);
1820
+ * result.siem_configs.forEach(config => {
1821
+ * console.log(config.name, config.provider_type, config.enabled);
1822
+ * });
1823
+ * ```
1824
+ */
1825
+ list: async (orgSlug) => {
1826
+ const response = await this.http.get(
1827
+ `/api/organizations/${orgSlug}/siem-configs`
1828
+ );
1829
+ return response.data;
1830
+ },
1831
+ /**
1832
+ * Get a specific SIEM configuration.
1833
+ * Requires 'owner' or 'admin' role.
1834
+ *
1835
+ * @param orgSlug Organization slug
1836
+ * @param configId SIEM configuration ID
1837
+ * @returns SIEM configuration
1838
+ *
1839
+ * @example
1840
+ * ```typescript
1841
+ * const config = await sso.organizations.siem.get('acme-corp', 'config-id');
1842
+ * console.log(config.name, config.endpoint_url);
1843
+ * ```
1844
+ */
1845
+ get: async (orgSlug, configId) => {
1846
+ const response = await this.http.get(
1847
+ `/api/organizations/${orgSlug}/siem-configs/${configId}`
1848
+ );
1849
+ return response.data;
1850
+ },
1851
+ /**
1852
+ * Update a SIEM configuration.
1853
+ * Requires 'owner' or 'admin' role.
1854
+ *
1855
+ * @param orgSlug Organization slug
1856
+ * @param configId SIEM configuration ID
1857
+ * @param payload Update payload
1858
+ * @returns Updated SIEM configuration
1859
+ *
1860
+ * @example
1861
+ * ```typescript
1862
+ * const updated = await sso.organizations.siem.update('acme-corp', 'config-id', {
1863
+ * enabled: false,
1864
+ * batch_size: 200
1865
+ * });
1866
+ * ```
1867
+ */
1868
+ update: async (orgSlug, configId, payload) => {
1869
+ const response = await this.http.patch(
1870
+ `/api/organizations/${orgSlug}/siem-configs/${configId}`,
1871
+ payload
1872
+ );
1873
+ return response.data;
1874
+ },
1875
+ /**
1876
+ * Delete a SIEM configuration.
1877
+ * Requires 'owner' or 'admin' role.
1878
+ *
1879
+ * @param orgSlug Organization slug
1880
+ * @param configId SIEM configuration ID
1881
+ *
1882
+ * @example
1883
+ * ```typescript
1884
+ * await sso.organizations.siem.delete('acme-corp', 'config-id');
1885
+ * console.log('SIEM configuration deleted');
1886
+ * ```
1887
+ */
1888
+ delete: async (orgSlug, configId) => {
1889
+ await this.http.delete(`/api/organizations/${orgSlug}/siem-configs/${configId}`);
1890
+ },
1891
+ /**
1892
+ * Test connection to a SIEM endpoint.
1893
+ * Sends a test event to verify the configuration.
1894
+ * Requires 'owner' or 'admin' role.
1895
+ *
1896
+ * @param orgSlug Organization slug
1897
+ * @param configId SIEM configuration ID
1898
+ * @returns Test result
1899
+ *
1900
+ * @example
1901
+ * ```typescript
1902
+ * const result = await sso.organizations.siem.test('acme-corp', 'config-id');
1903
+ * if (result.success) {
1904
+ * console.log('Connection successful:', result.message);
1905
+ * } else {
1906
+ * console.error('Connection failed:', result.message);
1907
+ * }
1908
+ * ```
1909
+ */
1910
+ test: async (orgSlug, configId) => {
1911
+ const response = await this.http.post(
1912
+ `/api/organizations/${orgSlug}/siem-configs/${configId}/test`
1913
+ );
1914
+ return response.data;
1915
+ }
1916
+ };
1917
+ this.auditLogs = new AuditLogsModule(http);
1918
+ this.webhooks = new WebhooksModule(http);
1919
+ }
1920
+ /**
1921
+ * Create a new organization (requires authentication).
1922
+ * The authenticated user becomes the organization owner.
1923
+ * Returns JWT tokens with organization context, eliminating the need to re-authenticate.
1924
+ *
1925
+ * @param payload Organization creation payload
1926
+ * @returns Created organization with owner, membership, and JWT tokens
1927
+ *
1928
+ * @example
1929
+ * ```typescript
1930
+ * const result = await sso.organizations.create({
1931
+ * slug: 'acme-corp',
1932
+ * name: 'Acme Corporation'
1933
+ * });
1934
+ * // Store the new tokens with org context
1935
+ * authStore.setTokens(result.access_token, result.refresh_token);
1936
+ * ```
1937
+ */
1938
+ async create(payload) {
1939
+ const response = await this.http.post("/api/organizations", payload);
1940
+ return response.data;
1941
+ }
1942
+ /**
1943
+ * List all organizations the authenticated user is a member of.
1944
+ *
1945
+ * @param params Optional query parameters for filtering and pagination
1946
+ * @returns Array of organization responses
1947
+ *
1948
+ * @example
1949
+ * ```typescript
1950
+ * const orgs = await sso.organizations.list({
1951
+ * status: 'active',
1952
+ * page: 1,
1953
+ * limit: 20
1954
+ * });
1955
+ * ```
1956
+ */
1957
+ async list(params) {
1958
+ const response = await this.http.get("/api/organizations", { params });
1959
+ return response.data;
1960
+ }
1961
+ /**
1962
+ * Get detailed information for a specific organization.
1963
+ *
1964
+ * @param orgSlug Organization slug
1965
+ * @returns Organization details
1966
+ *
1967
+ * @example
1968
+ * ```typescript
1969
+ * const org = await sso.organizations.get('acme-corp');
1970
+ * console.log(org.organization.name, org.membership_count);
1971
+ * ```
1972
+ */
1973
+ async get(orgSlug) {
1974
+ const response = await this.http.get(`/api/organizations/${orgSlug}`);
1975
+ return response.data;
1976
+ }
1977
+ /**
1978
+ * Update organization details.
1979
+ * Requires 'owner' or 'admin' role.
1980
+ *
1981
+ * @param orgSlug Organization slug
1982
+ * @param payload Update payload
1983
+ * @returns Updated organization details
1984
+ *
1985
+ * @example
1986
+ * ```typescript
1987
+ * const updated = await sso.organizations.update('acme-corp', {
1988
+ * name: 'Acme Corporation Inc.',
1989
+ * max_services: 20
1990
+ * });
1991
+ * ```
1992
+ */
1993
+ async update(orgSlug, payload) {
1994
+ const response = await this.http.patch(
1995
+ `/api/organizations/${orgSlug}`,
1996
+ payload
1997
+ );
1998
+ return response.data;
1999
+ }
2000
+ /**
2001
+ * Delete an organization and all its associated data.
2002
+ * This is a destructive operation that cannot be undone.
2003
+ * Requires 'owner' role.
2004
+ *
2005
+ * All related data will be cascaded deleted including:
2006
+ * - Members and invitations
2007
+ * - Services and plans
2008
+ * - Subscriptions
2009
+ * - OAuth credentials
2010
+ * - Audit logs
2011
+ *
2012
+ * @param orgSlug Organization slug
2013
+ *
2014
+ * @example
2015
+ * ```typescript
2016
+ * await sso.organizations.delete('acme-corp');
2017
+ * ```
2018
+ */
2019
+ async delete(orgSlug) {
2020
+ await this.http.delete(`/api/organizations/${orgSlug}`);
2021
+ }
2022
+ // ============================================================================
2023
+ // SMTP MANAGEMENT
2024
+ // ============================================================================
2025
+ /**
2026
+ * Configure SMTP settings for an organization.
2027
+ * Only owners and admins can configure SMTP.
2028
+ * The organization will use these settings for sending transactional emails
2029
+ * (registration, password reset, etc.).
2030
+ *
2031
+ * @param orgSlug Organization slug
2032
+ * @param config SMTP configuration
2033
+ * @returns Success message
2034
+ *
2035
+ * @example
2036
+ * ```typescript
2037
+ * await sso.organizations.setSmtp('acme-corp', {
2038
+ * host: 'smtp.gmail.com',
2039
+ * port: 587,
2040
+ * username: 'notifications@acme.com',
1478
2041
  * password: 'your-app-password',
1479
2042
  * from_email: 'notifications@acme.com',
1480
2043
  * from_name: 'Acme Corp'
@@ -2087,19 +2650,54 @@ var ServicesModule = class {
2087
2650
  return `${baseURL}/saml/${orgSlug}/${serviceSlug}/sso`;
2088
2651
  }
2089
2652
  };
2090
- }
2091
- /**
2092
- * Create a new service for an organization.
2093
- * Requires 'owner' or 'admin' role.
2094
- *
2095
- * @param orgSlug Organization slug
2096
- * @param payload Service creation payload
2097
- * @returns Created service with details
2098
- *
2099
- * @example
2100
- * ```typescript
2101
- * const result = await sso.services.create('acme-corp', {
2102
- * slug: 'main-app',
2653
+ /**
2654
+ * Stripe billing and checkout methods
2655
+ */
2656
+ this.billing = {
2657
+ /**
2658
+ * Create a Stripe checkout session for the authenticated user to subscribe to a plan.
2659
+ * Requires organization membership.
2660
+ *
2661
+ * IMPORTANT: The plan must have a `stripe_price_id` configured to be available for purchase.
2662
+ *
2663
+ * @param orgSlug Organization slug
2664
+ * @param serviceSlug Service slug
2665
+ * @param payload Checkout payload containing plan_id and redirect URLs
2666
+ * @returns Checkout session with URL to redirect user to
2667
+ *
2668
+ * @example
2669
+ * ```typescript
2670
+ * const checkout = await sso.services.billing.createCheckout('acme-corp', 'main-app', {
2671
+ * plan_id: 'plan_pro',
2672
+ * success_url: 'https://app.acme.com/billing/success?session_id={CHECKOUT_SESSION_ID}',
2673
+ * cancel_url: 'https://app.acme.com/billing/cancel'
2674
+ * });
2675
+ *
2676
+ * // Redirect user to Stripe checkout
2677
+ * window.location.href = checkout.checkout_url;
2678
+ * ```
2679
+ */
2680
+ createCheckout: async (orgSlug, serviceSlug, payload) => {
2681
+ const response = await this.http.post(
2682
+ `/api/organizations/${orgSlug}/services/${serviceSlug}/checkout`,
2683
+ payload
2684
+ );
2685
+ return response.data;
2686
+ }
2687
+ };
2688
+ }
2689
+ /**
2690
+ * Create a new service for an organization.
2691
+ * Requires 'owner' or 'admin' role.
2692
+ *
2693
+ * @param orgSlug Organization slug
2694
+ * @param payload Service creation payload
2695
+ * @returns Created service with details
2696
+ *
2697
+ * @example
2698
+ * ```typescript
2699
+ * const result = await sso.services.create('acme-corp', {
2700
+ * slug: 'main-app',
2103
2701
  * name: 'Main Application',
2104
2702
  * service_type: 'web',
2105
2703
  * github_scopes: ['user:email', 'read:org'],
@@ -2703,6 +3301,41 @@ var PlatformModule = class {
2703
3301
  const response = await this.http.get("/api/platform/audit-log", { params });
2704
3302
  return response.data;
2705
3303
  }
3304
+ /**
3305
+ * Impersonate a user (Platform Owner or Org Admin only).
3306
+ * Returns a short-lived JWT (15 minutes) that allows acting as the target user.
3307
+ *
3308
+ * Security:
3309
+ * - Platform Owners can impersonate any user
3310
+ * - Organization Admins can only impersonate users within their organization
3311
+ * - All impersonation actions are logged to the platform audit log with HIGH severity
3312
+ * - Tokens contain RFC 8693 actor claim for full audit trail
3313
+ *
3314
+ * @param payload Impersonation details (user_id and reason)
3315
+ * @returns Impersonation token and user context
3316
+ *
3317
+ * @example
3318
+ * ```typescript
3319
+ * const result = await sso.platform.impersonateUser({
3320
+ * user_id: 'user-uuid-123',
3321
+ * reason: 'Investigating support ticket #456'
3322
+ * });
3323
+ *
3324
+ * // Use the returned token to create a new client acting as the user
3325
+ * const userClient = new SsoClient({
3326
+ * baseURL: 'https://sso.example.com',
3327
+ * token: result.token
3328
+ * });
3329
+ *
3330
+ * // Now all requests with userClient are made as the target user
3331
+ * const profile = await userClient.user.getProfile();
3332
+ * console.log('Acting as:', result.target_user.email);
3333
+ * ```
3334
+ */
3335
+ async impersonateUser(payload) {
3336
+ const response = await this.http.post("/api/platform/impersonate", payload);
3337
+ return response.data;
3338
+ }
2706
3339
  };
2707
3340
 
2708
3341
  // src/modules/serviceApi.ts
@@ -2787,24 +3420,640 @@ var ServiceApiModule = class {
2787
3420
  }
2788
3421
  };
2789
3422
 
3423
+ // src/modules/permissions.ts
3424
+ var PermissionsModule = class {
3425
+ constructor(http) {
3426
+ this.http = http;
3427
+ }
3428
+ /**
3429
+ * Check if user has a specific permission.
3430
+ * Fetches from user profile API (which uses cached permissions).
3431
+ *
3432
+ * @param permission Permission in format "namespace:object_id#relation"
3433
+ * @returns true if the permission is present
3434
+ *
3435
+ * @example
3436
+ * ```typescript
3437
+ * const hasAccess = await sso.permissions.hasPermission('organization:acme#owner');
3438
+ * ```
3439
+ */
3440
+ async hasPermission(permission) {
3441
+ const response = await this.http.get("/api/user");
3442
+ return response.data.permissions.includes(permission);
3443
+ }
3444
+ /**
3445
+ * Get all user permissions.
3446
+ * Fetches from user profile API (which uses cached permissions).
3447
+ *
3448
+ * @returns Array of permission strings
3449
+ *
3450
+ * @example
3451
+ * ```typescript
3452
+ * const permissions = await sso.permissions.listPermissions();
3453
+ * // ["organization:acme#owner", "service:api#admin"]
3454
+ * ```
3455
+ */
3456
+ async listPermissions() {
3457
+ const response = await this.http.get("/api/user");
3458
+ return response.data.permissions;
3459
+ }
3460
+ /**
3461
+ * Check if user has access to a feature.
3462
+ *
3463
+ * @param feature Feature name to check
3464
+ * @returns true if the feature is available
3465
+ *
3466
+ * @example
3467
+ * ```typescript
3468
+ * const canExport = await sso.permissions.hasFeature('advanced-export');
3469
+ * ```
3470
+ */
3471
+ async hasFeature(feature) {
3472
+ const response = await this.http.get("/api/user");
3473
+ return response.data.features?.includes(feature) ?? false;
3474
+ }
3475
+ /**
3476
+ * Get current plan name.
3477
+ *
3478
+ * @returns Current plan name or null if not in org/service context
3479
+ *
3480
+ * @example
3481
+ * ```typescript
3482
+ * const plan = await sso.permissions.getPlan();
3483
+ * console.log(plan); // "pro", "enterprise", etc.
3484
+ * ```
3485
+ */
3486
+ async getPlan() {
3487
+ const response = await this.http.get("/api/user");
3488
+ return response.data.plan;
3489
+ }
3490
+ /**
3491
+ * Check if user has a specific permission on a resource.
3492
+ *
3493
+ * @param namespace The permission namespace (e.g., "organization", "service")
3494
+ * @param objectId The object ID (e.g., organization slug, service slug)
3495
+ * @param relation The relation type (e.g., "owner", "admin", "member")
3496
+ * @returns true if the user has the permission
3497
+ *
3498
+ * @example
3499
+ * ```typescript
3500
+ * const isOwner = await sso.permissions.can('organization', 'acme-corp', 'owner');
3501
+ * ```
3502
+ */
3503
+ async can(namespace, objectId, relation) {
3504
+ return this.hasPermission(`${namespace}:${objectId}#${relation}`);
3505
+ }
3506
+ /**
3507
+ * Check if user is a member of an organization.
3508
+ *
3509
+ * @param orgId The organization ID or slug
3510
+ * @returns true if the user is a member
3511
+ *
3512
+ * @example
3513
+ * ```typescript
3514
+ * if (await sso.permissions.isOrgMember('acme-corp')) {
3515
+ * // User is a member
3516
+ * }
3517
+ * ```
3518
+ */
3519
+ async isOrgMember(orgId) {
3520
+ return this.hasPermission(`organization:${orgId}#member`);
3521
+ }
3522
+ /**
3523
+ * Check if user is an admin of an organization.
3524
+ *
3525
+ * @param orgId The organization ID or slug
3526
+ * @returns true if the user is an admin
3527
+ *
3528
+ * @example
3529
+ * ```typescript
3530
+ * if (await sso.permissions.isOrgAdmin('acme-corp')) {
3531
+ * // User is an admin
3532
+ * }
3533
+ * ```
3534
+ */
3535
+ async isOrgAdmin(orgId) {
3536
+ return this.hasPermission(`organization:${orgId}#admin`);
3537
+ }
3538
+ /**
3539
+ * Check if user is an owner of an organization.
3540
+ *
3541
+ * @param orgId The organization ID or slug
3542
+ * @returns true if the user is an owner
3543
+ *
3544
+ * @example
3545
+ * ```typescript
3546
+ * if (await sso.permissions.isOrgOwner('acme-corp')) {
3547
+ * // User is an owner
3548
+ * }
3549
+ * ```
3550
+ */
3551
+ async isOrgOwner(orgId) {
3552
+ return this.hasPermission(`organization:${orgId}#owner`);
3553
+ }
3554
+ /**
3555
+ * Check if user has access to a service.
3556
+ *
3557
+ * @param serviceId The service ID or slug
3558
+ * @returns true if the user has access
3559
+ *
3560
+ * @example
3561
+ * ```typescript
3562
+ * if (await sso.permissions.hasServiceAccess('api-service')) {
3563
+ * // User has access to the service
3564
+ * }
3565
+ * ```
3566
+ */
3567
+ async hasServiceAccess(serviceId) {
3568
+ return this.hasPermission(`service:${serviceId}#member`);
3569
+ }
3570
+ /**
3571
+ * Filter permissions by namespace.
3572
+ *
3573
+ * @param namespace The namespace to filter by (e.g., "organization", "service")
3574
+ * @returns Array of permissions matching the namespace
3575
+ *
3576
+ * @example
3577
+ * ```typescript
3578
+ * const orgPermissions = await sso.permissions.getPermissionsByNamespace('organization');
3579
+ * ```
3580
+ */
3581
+ async getPermissionsByNamespace(namespace) {
3582
+ const allPermissions = await this.listPermissions();
3583
+ return allPermissions.filter((p) => p.startsWith(`${namespace}:`));
3584
+ }
3585
+ // ============================================================================
3586
+ // DEPRECATED METHODS - Token-based permission checking (legacy)
3587
+ // ============================================================================
3588
+ /**
3589
+ * @deprecated Use `hasPermission()` instead (without token parameter)
3590
+ * Decode a JWT token to extract claims (including permissions)
3591
+ * Note: This does NOT verify the signature - it only decodes the payload
3592
+ *
3593
+ * @param token The JWT access token
3594
+ * @returns The decoded JWT claims
3595
+ * @throws Error if the token is malformed
3596
+ */
3597
+ decodeToken(token) {
3598
+ try {
3599
+ const parts = token.split(".");
3600
+ if (parts.length !== 3) {
3601
+ throw new Error("Invalid JWT format");
3602
+ }
3603
+ const payload = parts[1];
3604
+ const decoded = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
3605
+ return JSON.parse(decoded);
3606
+ } catch (error) {
3607
+ throw new Error(`Failed to decode JWT: ${error instanceof Error ? error.message : "Unknown error"}`);
3608
+ }
3609
+ }
3610
+ /**
3611
+ * @deprecated JWT tokens no longer contain permissions. Use `hasPermission(permission)` instead.
3612
+ * Check if a JWT token contains a specific permission
3613
+ *
3614
+ * @param token The JWT access token (ignored)
3615
+ * @param permission Permission in format "namespace:object_id#relation"
3616
+ * @returns true if the permission is present in the token
3617
+ */
3618
+ hasPermissionFromToken(token, permission) {
3619
+ const claims = this.decodeToken(token);
3620
+ return claims.permissions?.includes(permission) ?? false;
3621
+ }
3622
+ /**
3623
+ * @deprecated JWT tokens no longer contain permissions. Use `can(namespace, objectId, relation)` instead.
3624
+ * Check if a user has a specific permission on a resource
3625
+ *
3626
+ * @param token The JWT access token (ignored)
3627
+ * @param namespace The permission namespace (e.g., "organization", "service")
3628
+ * @param objectId The object ID (e.g., organization slug, service slug)
3629
+ * @param relation The relation type (e.g., "owner", "admin", "member")
3630
+ * @returns true if the user has the permission
3631
+ */
3632
+ canFromToken(token, namespace, objectId, relation) {
3633
+ return this.hasPermissionFromToken(token, `${namespace}:${objectId}#${relation}`);
3634
+ }
3635
+ /**
3636
+ * @deprecated JWT tokens no longer contain permissions. Use `isOrgMember(orgId)` instead.
3637
+ * Check if user is a member of an organization
3638
+ *
3639
+ * @param token The JWT access token (ignored)
3640
+ * @param orgId The organization ID or slug
3641
+ * @returns true if the user is a member
3642
+ */
3643
+ isOrgMemberFromToken(token, orgId) {
3644
+ return this.hasPermissionFromToken(token, `organization:${orgId}#member`);
3645
+ }
3646
+ /**
3647
+ * @deprecated JWT tokens no longer contain permissions. Use `isOrgAdmin(orgId)` instead.
3648
+ * Check if user is an admin of an organization
3649
+ *
3650
+ * @param token The JWT access token (ignored)
3651
+ * @param orgId The organization ID or slug
3652
+ * @returns true if the user is an admin
3653
+ */
3654
+ isOrgAdminFromToken(token, orgId) {
3655
+ return this.hasPermissionFromToken(token, `organization:${orgId}#admin`);
3656
+ }
3657
+ /**
3658
+ * @deprecated JWT tokens no longer contain permissions. Use `isOrgOwner(orgId)` instead.
3659
+ * Check if user is an owner of an organization
3660
+ *
3661
+ * @param token The JWT access token (ignored)
3662
+ * @param orgId The organization ID or slug
3663
+ * @returns true if the user is an owner
3664
+ */
3665
+ isOrgOwnerFromToken(token, orgId) {
3666
+ return this.hasPermissionFromToken(token, `organization:${orgId}#owner`);
3667
+ }
3668
+ /**
3669
+ * @deprecated JWT tokens no longer contain permissions. Use `hasServiceAccess(serviceId)` instead.
3670
+ * Check if user has access to a service
3671
+ *
3672
+ * @param token The JWT access token (ignored)
3673
+ * @param serviceId The service ID or slug
3674
+ * @returns true if the user has access
3675
+ */
3676
+ hasServiceAccessFromToken(token, serviceId) {
3677
+ return this.hasPermissionFromToken(token, `service:${serviceId}#member`);
3678
+ }
3679
+ /**
3680
+ * @deprecated JWT tokens no longer contain permissions. Use `listPermissions()` instead.
3681
+ * Get all permissions from a JWT token
3682
+ *
3683
+ * @param token The JWT access token
3684
+ * @returns Array of permission strings, or empty array if none
3685
+ */
3686
+ getAllPermissionsFromToken(token) {
3687
+ const claims = this.decodeToken(token);
3688
+ return claims.permissions ?? [];
3689
+ }
3690
+ /**
3691
+ * Parse a permission string into its components
3692
+ *
3693
+ * @param permission Permission string in format "namespace:object_id#relation"
3694
+ * @returns Parsed permission components or null if invalid format
3695
+ *
3696
+ * @example
3697
+ * ```typescript
3698
+ * const parsed = sso.permissions.parsePermission('organization:acme#owner');
3699
+ * // { namespace: 'organization', objectId: 'acme', relation: 'owner' }
3700
+ * ```
3701
+ */
3702
+ parsePermission(permission) {
3703
+ const match = permission.match(/^([^:]+):([^#]+)#(.+)$/);
3704
+ if (!match) {
3705
+ return null;
3706
+ }
3707
+ return {
3708
+ namespace: match[1],
3709
+ objectId: match[2],
3710
+ relation: match[3]
3711
+ };
3712
+ }
3713
+ /**
3714
+ * @deprecated JWT tokens no longer contain permissions. Use `getPermissionsByNamespace(namespace)` instead.
3715
+ * Filter permissions by namespace
3716
+ *
3717
+ * @param token The JWT access token (ignored)
3718
+ * @param namespace The namespace to filter by (e.g., "organization", "service")
3719
+ * @returns Array of permissions matching the namespace
3720
+ */
3721
+ getPermissionsByNamespaceFromToken(token, namespace) {
3722
+ const allPermissions = this.getAllPermissionsFromToken(token);
3723
+ return allPermissions.filter((p) => p.startsWith(`${namespace}:`));
3724
+ }
3725
+ };
3726
+
3727
+ // src/modules/passkeys.ts
3728
+ var PasskeysModule = class {
3729
+ constructor(http) {
3730
+ this.http = http;
3731
+ }
3732
+ /**
3733
+ * Check if WebAuthn is supported in the current browser
3734
+ */
3735
+ isSupported() {
3736
+ return typeof window !== "undefined" && window.PublicKeyCredential !== void 0 && typeof window.PublicKeyCredential === "function";
3737
+ }
3738
+ /**
3739
+ * Check if platform authenticator (like Touch ID, Face ID, Windows Hello) is available
3740
+ */
3741
+ async isPlatformAuthenticatorAvailable() {
3742
+ if (!this.isSupported()) {
3743
+ return false;
3744
+ }
3745
+ return PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
3746
+ }
3747
+ /**
3748
+ * Register a new passkey for the authenticated user
3749
+ *
3750
+ * This method requires an authenticated session (JWT token must be set).
3751
+ * It starts the WebAuthn registration ceremony, prompts the user to create
3752
+ * a passkey using their device's authenticator (e.g., Touch ID, Face ID,
3753
+ * Windows Hello, or hardware security key), and stores the credential.
3754
+ *
3755
+ * @param displayName Optional display name for the passkey
3756
+ * @returns Promise resolving to the registered passkey ID
3757
+ * @throws {Error} If WebAuthn is not supported or registration fails
3758
+ *
3759
+ * @example
3760
+ * ```typescript
3761
+ * try {
3762
+ * const passkeyId = await sso.passkeys.register('My MacBook Pro');
3763
+ * console.log('Passkey registered:', passkeyId);
3764
+ * } catch (error) {
3765
+ * console.error('Passkey registration failed:', error);
3766
+ * }
3767
+ * ```
3768
+ */
3769
+ async register(displayName) {
3770
+ if (!this.isSupported()) {
3771
+ throw new Error("WebAuthn is not supported in this browser");
3772
+ }
3773
+ const startResponse = await this.http.post(
3774
+ "/auth/passkeys/register/start",
3775
+ { name: displayName }
3776
+ );
3777
+ const startData = startResponse.data;
3778
+ const createOptions = {
3779
+ publicKey: {
3780
+ ...startData.options,
3781
+ challenge: this.base64UrlToUint8Array(startData.options.challenge),
3782
+ user: {
3783
+ ...startData.options.user,
3784
+ id: this.base64UrlToUint8Array(startData.options.user.id)
3785
+ },
3786
+ excludeCredentials: startData.options.excludeCredentials?.map((cred) => ({
3787
+ ...cred,
3788
+ id: this.base64UrlToUint8Array(cred.id)
3789
+ }))
3790
+ }
3791
+ };
3792
+ const credential = await navigator.credentials.create(createOptions);
3793
+ if (!credential || !(credential instanceof PublicKeyCredential)) {
3794
+ throw new Error("Failed to create passkey");
3795
+ }
3796
+ if (!(credential.response instanceof AuthenticatorAttestationResponse)) {
3797
+ throw new Error("Invalid credential response type");
3798
+ }
3799
+ const credentialJSON = {
3800
+ id: credential.id,
3801
+ rawId: this.uint8ArrayToBase64Url(new Uint8Array(credential.rawId)),
3802
+ response: {
3803
+ clientDataJSON: this.uint8ArrayToBase64Url(new Uint8Array(credential.response.clientDataJSON)),
3804
+ attestationObject: this.uint8ArrayToBase64Url(new Uint8Array(credential.response.attestationObject)),
3805
+ transports: credential.response.getTransports?.()
3806
+ },
3807
+ authenticatorAttachment: credential.authenticatorAttachment,
3808
+ clientExtensionResults: credential.getClientExtensionResults(),
3809
+ type: credential.type
3810
+ };
3811
+ const finishResponse = await this.http.post(
3812
+ "/auth/passkeys/register/finish",
3813
+ {
3814
+ challenge_id: startData.challenge_id,
3815
+ credential: credentialJSON
3816
+ }
3817
+ );
3818
+ return finishResponse.data.passkey_id;
3819
+ }
3820
+ /**
3821
+ * Authenticate with a passkey and obtain a JWT token
3822
+ *
3823
+ * This method prompts the user to authenticate using their passkey.
3824
+ * Upon successful authentication, a JWT token is returned which can
3825
+ * be used to make authenticated API requests.
3826
+ *
3827
+ * @param email User's email address
3828
+ * @returns Promise resolving to authentication response with JWT token
3829
+ * @throws {Error} If WebAuthn is not supported or authentication fails
3830
+ *
3831
+ * @example
3832
+ * ```typescript
3833
+ * try {
3834
+ * const { token, user_id } = await sso.passkeys.login('user@example.com');
3835
+ * sso.setToken(token);
3836
+ * console.log('Logged in as:', user_id);
3837
+ * } catch (error) {
3838
+ * console.error('Passkey login failed:', error);
3839
+ * }
3840
+ * ```
3841
+ */
3842
+ async login(email) {
3843
+ if (!this.isSupported()) {
3844
+ throw new Error("WebAuthn is not supported in this browser");
3845
+ }
3846
+ const startRequest = { email };
3847
+ const startResponse = await this.http.post(
3848
+ "/auth/passkeys/authenticate/start",
3849
+ startRequest
3850
+ );
3851
+ const startData = startResponse.data;
3852
+ const getOptions = {
3853
+ publicKey: {
3854
+ ...startData.options,
3855
+ challenge: this.base64UrlToUint8Array(startData.options.challenge),
3856
+ allowCredentials: startData.options.allowCredentials?.map((cred) => ({
3857
+ ...cred,
3858
+ id: this.base64UrlToUint8Array(cred.id)
3859
+ }))
3860
+ }
3861
+ };
3862
+ const credential = await navigator.credentials.get(getOptions);
3863
+ if (!credential || !(credential instanceof PublicKeyCredential)) {
3864
+ throw new Error("Failed to get passkey");
3865
+ }
3866
+ if (!(credential.response instanceof AuthenticatorAssertionResponse)) {
3867
+ throw new Error("Invalid credential response type");
3868
+ }
3869
+ const credentialJSON = {
3870
+ id: credential.id,
3871
+ rawId: this.uint8ArrayToBase64Url(new Uint8Array(credential.rawId)),
3872
+ response: {
3873
+ clientDataJSON: this.uint8ArrayToBase64Url(new Uint8Array(credential.response.clientDataJSON)),
3874
+ authenticatorData: this.uint8ArrayToBase64Url(new Uint8Array(credential.response.authenticatorData)),
3875
+ signature: this.uint8ArrayToBase64Url(new Uint8Array(credential.response.signature)),
3876
+ userHandle: credential.response.userHandle ? this.uint8ArrayToBase64Url(new Uint8Array(credential.response.userHandle)) : void 0
3877
+ },
3878
+ authenticatorAttachment: credential.authenticatorAttachment,
3879
+ clientExtensionResults: credential.getClientExtensionResults(),
3880
+ type: credential.type
3881
+ };
3882
+ const finishResponse = await this.http.post(
3883
+ "/auth/passkeys/authenticate/finish",
3884
+ {
3885
+ challenge_id: startData.challenge_id,
3886
+ credential: credentialJSON
3887
+ }
3888
+ );
3889
+ return finishResponse.data;
3890
+ }
3891
+ /**
3892
+ * Convert Base64URL string to Uint8Array
3893
+ */
3894
+ base64UrlToUint8Array(base64url) {
3895
+ const padding = "=".repeat((4 - base64url.length % 4) % 4);
3896
+ const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/") + padding;
3897
+ const rawData = atob(base64);
3898
+ const outputArray = new Uint8Array(rawData.length);
3899
+ for (let i = 0; i < rawData.length; ++i) {
3900
+ outputArray[i] = rawData.charCodeAt(i);
3901
+ }
3902
+ return outputArray;
3903
+ }
3904
+ /**
3905
+ * Convert Uint8Array to Base64URL string
3906
+ */
3907
+ uint8ArrayToBase64Url(array) {
3908
+ let binary = "";
3909
+ for (let i = 0; i < array.byteLength; i++) {
3910
+ binary += String.fromCharCode(array[i]);
3911
+ }
3912
+ const base64 = btoa(binary);
3913
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
3914
+ }
3915
+ };
3916
+
3917
+ // src/modules/magic.ts
3918
+ var MagicLinks = class {
3919
+ constructor(http) {
3920
+ this.http = http;
3921
+ }
3922
+ /**
3923
+ * Request a magic link to be sent to the user's email
3924
+ *
3925
+ * @param data Magic link request data
3926
+ * @returns Promise resolving to magic link response
3927
+ */
3928
+ async request(data) {
3929
+ const response = await this.http.post("/api/auth/magic-link/request", data);
3930
+ return response.data;
3931
+ }
3932
+ /**
3933
+ * Verify a magic link token and complete authentication
3934
+ * Note: This is typically handled by redirecting to the magic link URL
3935
+ * The backend will handle verification and either redirect or return tokens
3936
+ *
3937
+ * @param token The magic link token to verify
3938
+ * @param redirectUri Optional where to redirect after success
3939
+ * @returns URL to redirect to for verification
3940
+ */
3941
+ getVerificationUrl(token, redirectUri) {
3942
+ const params = new URLSearchParams({ token });
3943
+ if (redirectUri) {
3944
+ params.append("redirect_uri", redirectUri);
3945
+ }
3946
+ return `/api/auth/magic-link/verify?${params.toString()}`;
3947
+ }
3948
+ /**
3949
+ * Verify a magic link token via API call
3950
+ * This is an alternative to redirect-based verification
3951
+ *
3952
+ * @param token The magic link token
3953
+ * @param redirectUri Optional redirect URI
3954
+ * @returns Promise resolving to authentication response
3955
+ */
3956
+ async verify(token, redirectUri) {
3957
+ const params = new URLSearchParams({ token });
3958
+ if (redirectUri) {
3959
+ params.append("redirect_uri", redirectUri);
3960
+ }
3961
+ return this.http.get(`/api/auth/magic-link/verify?${params.toString()}`);
3962
+ }
3963
+ /**
3964
+ * Construct the complete magic link URL that would be sent via email
3965
+ *
3966
+ * @param token The magic link token
3967
+ * @param redirectUri Optional redirect URI
3968
+ * @returns Complete magic link URL
3969
+ */
3970
+ constructMagicLink(token, redirectUri) {
3971
+ return this.getVerificationUrl(token, redirectUri);
3972
+ }
3973
+ };
3974
+
3975
+ // src/modules/privacy.ts
3976
+ var PrivacyModule = class {
3977
+ constructor(http) {
3978
+ this.http = http;
3979
+ }
3980
+ /**
3981
+ * Export all user data (GDPR Right to Access).
3982
+ * Users can export their own data, or organization owners can export their members' data.
3983
+ *
3984
+ * @param userId User ID to export data for
3985
+ * @returns Complete user data export including memberships, login events, identities, MFA events, and passkeys
3986
+ *
3987
+ * @example
3988
+ * ```typescript
3989
+ * const userData = await sso.privacy.exportData('user-id');
3990
+ * console.log(`Exported ${userData.login_events_count} login events`);
3991
+ * console.log(`User has ${userData.memberships.length} organization memberships`);
3992
+ * ```
3993
+ */
3994
+ async exportData(userId) {
3995
+ const response = await this.http.get(`/api/privacy/export/${userId}`);
3996
+ return response.data;
3997
+ }
3998
+ /**
3999
+ * Anonymize user data (GDPR Right to be Forgotten).
4000
+ * Requires organization owner permission for all organizations the user is a member of.
4001
+ * Platform owners cannot be anonymized.
4002
+ *
4003
+ * This operation:
4004
+ * - Soft-deletes the user account
4005
+ * - Hard-deletes PII from identities and passkeys tables
4006
+ * - Preserves audit logs for compliance
4007
+ *
4008
+ * @param userId User ID to anonymize
4009
+ * @returns Anonymization confirmation response
4010
+ *
4011
+ * @example
4012
+ * ```typescript
4013
+ * const result = await sso.privacy.forgetUser('user-id');
4014
+ * console.log(result.message);
4015
+ * // "User data has been anonymized. PII has been removed while preserving audit logs."
4016
+ * ```
4017
+ */
4018
+ async forgetUser(userId) {
4019
+ const response = await this.http.delete(`/api/privacy/forget/${userId}`);
4020
+ return response.data;
4021
+ }
4022
+ };
4023
+
2790
4024
  // src/client.ts
2791
4025
  var SsoClient = class {
2792
4026
  constructor(options) {
2793
4027
  this.http = createHttpAgent(options.baseURL);
2794
- if (options.token) {
2795
- this.setAuthToken(options.token);
2796
- }
2797
- if (options.apiKey) {
2798
- this.setApiKey(options.apiKey);
2799
- }
4028
+ this.session = new SessionManager(
4029
+ resolveStorage(options.storage),
4030
+ async (refreshToken) => {
4031
+ const res = await this.http.post("/api/auth/refresh", { refresh_token: refreshToken });
4032
+ return res.data;
4033
+ },
4034
+ { storageKeyPrefix: options.storagePrefix || "sso_" }
4035
+ );
4036
+ this.http.setSessionManager(this.session);
2800
4037
  this.analytics = new AnalyticsModule(this.http);
2801
- this.auth = new AuthModule(this.http);
4038
+ this.auth = new AuthModule(this.http, this.session);
2802
4039
  this.user = new UserModule(this.http);
2803
4040
  this.organizations = new OrganizationsModule(this.http);
2804
4041
  this.services = new ServicesModule(this.http);
2805
4042
  this.invitations = new InvitationsModule(this.http);
2806
4043
  this.platform = new PlatformModule(this.http);
2807
4044
  this.serviceApi = new ServiceApiModule(this.http);
4045
+ this.permissions = new PermissionsModule(this.http);
4046
+ this.passkeys = new PasskeysModule(this.http);
4047
+ this.magicLinks = new MagicLinks(this.http);
4048
+ this.privacy = new PrivacyModule(this.http);
4049
+ if (options.apiKey) {
4050
+ this.setApiKey(options.apiKey);
4051
+ }
4052
+ if (options.token) {
4053
+ this.session.setSession({ access_token: options.token });
4054
+ } else {
4055
+ this.session.loadSession().catch(console.error);
4056
+ }
2808
4057
  }
2809
4058
  /**
2810
4059
  * Sets the JWT for all subsequent authenticated requests.
@@ -2856,13 +4105,96 @@ var SsoClient = class {
2856
4105
  getBaseURL() {
2857
4106
  return this.http.defaults.baseURL || "";
2858
4107
  }
4108
+ /**
4109
+ * Check if the user is currently authenticated
4110
+ */
4111
+ isAuthenticated() {
4112
+ return this.session.isAuthenticated();
4113
+ }
4114
+ /**
4115
+ * Subscribe to authentication state changes.
4116
+ * Useful for updating UI when login/logout/expiration occurs.
4117
+ *
4118
+ * @param listener Callback function that receives the authentication state
4119
+ * @returns Unsubscribe function
4120
+ *
4121
+ * @example
4122
+ * ```typescript
4123
+ * const unsubscribe = sso.onAuthStateChange((isAuth) => {
4124
+ * console.log(isAuth ? 'User is logged in' : 'User is logged out');
4125
+ * });
4126
+ *
4127
+ * // Later, to stop listening
4128
+ * unsubscribe();
4129
+ * ```
4130
+ */
4131
+ onAuthStateChange(listener) {
4132
+ return this.session.subscribe(listener);
4133
+ }
4134
+ /**
4135
+ * Manually retrieve the current access token
4136
+ *
4137
+ * @returns The current access token, or null if not authenticated
4138
+ */
4139
+ async getToken() {
4140
+ return this.session.getToken();
4141
+ }
2859
4142
  };
4143
+
4144
+ // src/types/risk.ts
4145
+ var RiskAction = /* @__PURE__ */ ((RiskAction2) => {
4146
+ RiskAction2["ALLOW"] = "allow";
4147
+ RiskAction2["LOG_ONLY"] = "log_only";
4148
+ RiskAction2["CHALLENGE_MFA"] = "challenge_mfa";
4149
+ RiskAction2["BLOCK"] = "block";
4150
+ return RiskAction2;
4151
+ })(RiskAction || {});
4152
+ var RiskFactorType = /* @__PURE__ */ ((RiskFactorType2) => {
4153
+ RiskFactorType2["NEW_IP"] = "new_ip";
4154
+ RiskFactorType2["HIGH_RISK_LOCATION"] = "high_risk_location";
4155
+ RiskFactorType2["IMPOSSIBLE_TRAVEL"] = "impossible_travel";
4156
+ RiskFactorType2["NEW_DEVICE"] = "new_device";
4157
+ RiskFactorType2["FAILED_ATTEMPTS"] = "failed_attempts";
4158
+ RiskFactorType2["UNUSUAL_TIME"] = "unusual_time";
4159
+ RiskFactorType2["SUSPICIOUS_USER_AGENT"] = "suspicious_user_agent";
4160
+ RiskFactorType2["ANONYMOUS_NETWORK"] = "anonymous_network";
4161
+ RiskFactorType2["NEW_ACCOUNT"] = "new_account";
4162
+ RiskFactorType2["SUSPICIOUS_HISTORY"] = "suspicious_history";
4163
+ RiskFactorType2["HIGH_VELOCITY"] = "high_velocity";
4164
+ RiskFactorType2["CUSTOM_RULE"] = "custom_rule";
4165
+ return RiskFactorType2;
4166
+ })(RiskFactorType || {});
4167
+ var AuthMethod = /* @__PURE__ */ ((AuthMethod2) => {
4168
+ AuthMethod2["PASSWORD"] = "password";
4169
+ AuthMethod2["OAUTH"] = "oauth";
4170
+ AuthMethod2["PASSKEY"] = "passkey";
4171
+ AuthMethod2["MAGIC_LINK"] = "magic_link";
4172
+ AuthMethod2["MFA"] = "mfa";
4173
+ AuthMethod2["SAML"] = "saml";
4174
+ return AuthMethod2;
4175
+ })(AuthMethod || {});
4176
+ var RiskEventOutcome = /* @__PURE__ */ ((RiskEventOutcome2) => {
4177
+ RiskEventOutcome2["ALLOWED"] = "allowed";
4178
+ RiskEventOutcome2["BLOCKED"] = "blocked";
4179
+ RiskEventOutcome2["CHALLENGED"] = "challenged";
4180
+ RiskEventOutcome2["LOGGED"] = "logged";
4181
+ return RiskEventOutcome2;
4182
+ })(RiskEventOutcome || {});
2860
4183
  // Annotate the CommonJS export names for ESM import in node:
2861
4184
  0 && (module.exports = {
4185
+ AuthMethod,
2862
4186
  AuthModule,
4187
+ BrowserStorage,
2863
4188
  InvitationsModule,
4189
+ MagicLinks,
4190
+ MemoryStorage,
2864
4191
  OrganizationsModule,
4192
+ PasskeysModule,
4193
+ PermissionsModule,
2865
4194
  PlatformModule,
4195
+ RiskAction,
4196
+ RiskEventOutcome,
4197
+ RiskFactorType,
2866
4198
  ServiceApiModule,
2867
4199
  ServicesModule,
2868
4200
  SsoApiError,