authmi 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.js CHANGED
@@ -23,9 +23,7 @@ __export(react_exports, {
23
23
  AuthMiClient: () => AuthMiClient,
24
24
  AuthMiError: () => AuthMiError,
25
25
  AuthMiProvider: () => AuthMiProvider,
26
- LocalStorageTokenStorage: () => LocalStorageTokenStorage,
27
26
  LoginForm: () => LoginForm,
28
- MemoryTokenStorage: () => MemoryTokenStorage,
29
27
  Protected: () => Protected,
30
28
  ScopeError: () => ScopeError,
31
29
  useAuthActions: () => useAuthActions,
@@ -37,6 +35,57 @@ __export(react_exports, {
37
35
  module.exports = __toCommonJS(react_exports);
38
36
  var import_react = require("react");
39
37
 
38
+ // src/config.ts
39
+ function resolveConfig(provider) {
40
+ const raw = typeof provider === "function" ? provider() : provider;
41
+ return {
42
+ baseUrl: raw.baseUrl,
43
+ apiKey: raw.apiKey ?? "",
44
+ serviceId: raw.serviceId ?? "",
45
+ clientId: raw.clientId ?? "",
46
+ clientSecret: raw.clientSecret ?? "",
47
+ defaultTokenExpiry: raw.defaultTokenExpiry ?? 24
48
+ };
49
+ }
50
+
51
+ // src/storage/localStorage.ts
52
+ var STORAGE_KEY = "authmi_token";
53
+ var LocalStorageTokenStorage = class {
54
+ canAccess() {
55
+ if (typeof window === "undefined") return false;
56
+ try {
57
+ const probe = "__authmi_storage_test__";
58
+ localStorage.setItem(probe, "1");
59
+ localStorage.removeItem(probe);
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+ getToken() {
66
+ if (!this.canAccess()) return null;
67
+ try {
68
+ return localStorage.getItem(STORAGE_KEY);
69
+ } catch {
70
+ return null;
71
+ }
72
+ }
73
+ setToken(token) {
74
+ if (!this.canAccess()) return;
75
+ try {
76
+ localStorage.setItem(STORAGE_KEY, token);
77
+ } catch {
78
+ }
79
+ }
80
+ removeToken() {
81
+ if (!this.canAccess()) return;
82
+ try {
83
+ localStorage.removeItem(STORAGE_KEY);
84
+ } catch {
85
+ }
86
+ }
87
+ };
88
+
40
89
  // src/types.ts
41
90
  var AuthMiError = class extends Error {
42
91
  constructor(message, code, statusCode) {
@@ -54,82 +103,38 @@ var ScopeError = class extends AuthMiError {
54
103
  this.name = "ScopeError";
55
104
  }
56
105
  };
57
- var _LocalStorageTokenStorage = class _LocalStorageTokenStorage {
58
- getToken() {
59
- if (typeof window === "undefined") return null;
60
- return localStorage.getItem(_LocalStorageTokenStorage.KEY);
61
- }
62
- setToken(token) {
63
- if (typeof window === "undefined") return;
64
- localStorage.setItem(_LocalStorageTokenStorage.KEY, token);
65
- }
66
- removeToken() {
67
- if (typeof window === "undefined") return;
68
- localStorage.removeItem(_LocalStorageTokenStorage.KEY);
69
- }
70
- };
71
- _LocalStorageTokenStorage.KEY = "authmi_token";
72
- var LocalStorageTokenStorage = _LocalStorageTokenStorage;
73
- var MemoryTokenStorage = class {
74
- constructor() {
75
- this.token = null;
76
- }
77
- getToken() {
78
- return this.token;
79
- }
80
- setToken(token) {
81
- this.token = token;
82
- }
83
- removeToken() {
84
- this.token = null;
85
- }
86
- };
87
106
 
88
107
  // src/client.ts
89
108
  var AuthMiClient = class {
90
- constructor(config, tokenStorage) {
91
- this.config = {
92
- apiKey: "",
93
- serviceId: "",
94
- clientId: "",
95
- clientSecret: "",
96
- defaultTokenExpiry: 24,
97
- ...config
98
- };
99
- this.tokenStorage = tokenStorage || new LocalStorageTokenStorage();
100
- }
101
- /** Build Basic auth header from clientId:clientSecret */
102
- basicAuthHeader() {
103
- if (this.config.clientId && this.config.clientSecret) {
104
- const encoded = btoa(`${this.config.clientId}:${this.config.clientSecret}`);
105
- return `Basic ${encoded}`;
106
- }
107
- return null;
108
- }
109
- // ============ HTTP Utilities ============
110
- async request(endpoint, options = {}, requireAuth = false) {
111
- const url = `${this.config.baseUrl}${endpoint}`;
109
+ constructor(config, options) {
110
+ this.configProvider = config;
111
+ this.storage = options?.storage ?? new LocalStorageTokenStorage();
112
+ this.cache = options?.cache || null;
113
+ }
114
+ /** Resolve current config (supports lazy evaluation) */
115
+ get config() {
116
+ return resolveConfig(this.configProvider);
117
+ }
118
+ // ============ HTTP plumbing ============
119
+ async request(endpoint, options = {}, requireUserToken = false) {
120
+ const cfg = this.config;
121
+ const url = `${cfg.baseUrl}${endpoint}`;
112
122
  const headers = {
113
123
  "Content-Type": "application/json",
114
124
  ...options.headers || {}
115
125
  };
116
- const basic = this.basicAuthHeader();
117
- if (basic && !requireAuth && !headers["Authorization"]) {
118
- headers["Authorization"] = basic;
119
- } else if (this.config.apiKey && !requireAuth && !headers["Authorization"]) {
120
- headers["Authorization"] = `Bearer ${this.config.apiKey}`;
121
- }
122
- if (requireAuth) {
123
- const token = this.getAccessToken();
124
- if (token && !headers["Authorization"]) {
125
- headers["Authorization"] = `Bearer ${token}`;
126
+ if (!headers["Authorization"]) {
127
+ if (requireUserToken) {
128
+ const token = await this.storage.getToken();
129
+ if (token) headers["Authorization"] = `Bearer ${token}`;
130
+ } else if (cfg.clientId && cfg.clientSecret) {
131
+ headers["Authorization"] = `Basic ${btoa(`${cfg.clientId}:${cfg.clientSecret}`)}`;
132
+ } else if (cfg.apiKey) {
133
+ headers["Authorization"] = `Bearer ${cfg.apiKey}`;
126
134
  }
127
135
  }
128
136
  try {
129
- const response = await fetch(url, {
130
- ...options,
131
- headers
132
- });
137
+ const response = await fetch(url, { ...options, headers });
133
138
  const data = await response.json();
134
139
  if (!response.ok) {
135
140
  if (response.status === 403 && data.required && data.provided) {
@@ -147,9 +152,7 @@ var AuthMiClient = class {
147
152
  }
148
153
  return data;
149
154
  } catch (error) {
150
- if (error instanceof AuthMiError) {
151
- throw error;
152
- }
155
+ if (error instanceof AuthMiError) throw error;
153
156
  throw new AuthMiError(
154
157
  error instanceof Error ? error.message : "Network error",
155
158
  "NETWORK_ERROR"
@@ -157,384 +160,497 @@ var AuthMiClient = class {
157
160
  }
158
161
  }
159
162
  // ============ Configuration ============
160
- /**
161
- * Update the client's service configuration
162
- */
163
163
  configure(config) {
164
- this.config = { ...this.config, ...config };
164
+ const current = resolveConfig(this.configProvider);
165
+ this.configProvider = { ...current, ...config };
165
166
  }
166
- /**
167
- * Get current configuration (without sensitive data)
168
- */
169
167
  getConfig() {
170
- const { apiKey: _, clientSecret: __, ...rest } = this.config;
168
+ const { apiKey: _, clientSecret: __, ...rest } = resolveConfig(this.configProvider);
171
169
  return rest;
172
170
  }
173
171
  // ============ Token Management ============
174
- /**
175
- * Get the stored access token
176
- */
177
- getAccessToken() {
178
- return this.tokenStorage.getToken();
179
- }
180
- /**
181
- * Store an access token
182
- */
172
+ async getAccessToken() {
173
+ return this.storage.getToken();
174
+ }
183
175
  setAccessToken(token) {
184
- this.tokenStorage.setToken(token);
176
+ this.storage.setToken(token);
185
177
  }
186
- /**
187
- * Remove the stored access token (logout)
188
- */
189
178
  clearAccessToken() {
190
- this.tokenStorage.removeToken();
191
- }
192
- /**
193
- * Check if user is logged in
194
- */
195
- isLoggedIn() {
196
- return !!this.getAccessToken();
197
- }
198
- // ============ Scope Validation ============
199
- /**
200
- * Introspect a token or API key and validate required scopes
201
- */
202
- async introspect(request) {
203
- return this.request("/api/v1/introspect", {
204
- method: "POST",
205
- body: JSON.stringify(request)
206
- });
207
- }
208
- /**
209
- * Validate the current token with optional scope checking
210
- */
211
- async validateWithScopes(requiredScopes) {
212
- const token = this.getAccessToken();
213
- if (!token) {
214
- return { valid: false, error: "No token provided" };
215
- }
216
- if (!this.config.apiKey && !this.config.clientSecret) {
217
- return { valid: false, error: "Service credentials not configured" };
218
- }
219
- if (!this.config.serviceId) {
220
- return { valid: false, error: "Service ID not configured in AuthMiConfig" };
221
- }
222
- try {
223
- const result = await this.introspect({
224
- serviceId: this.config.serviceId,
225
- token,
226
- requiredScopes
227
- });
228
- return {
229
- valid: result.valid,
230
- type: result.type === "bearer" ? "token" : "api_key",
231
- scopes: result.scopes,
232
- userId: result.userId,
233
- email: result.email,
234
- error: result.error
235
- };
236
- } catch (error) {
237
- if (error instanceof ScopeError) {
238
- return {
239
- valid: false,
240
- error: error.message,
241
- scopes: error.providedScopes
242
- };
243
- }
244
- if (error instanceof AuthMiError && error.statusCode === 401) {
245
- return { valid: false, error: "Invalid or expired token" };
246
- }
247
- throw error;
248
- }
249
- }
250
- /**
251
- * Check if the current user has all the required scopes
252
- * Throws ScopeError if scopes are insufficient
253
- */
254
- async requireScopes(...requiredScopes) {
255
- const result = await this.validateWithScopes(requiredScopes);
256
- if (!result.valid) {
257
- throw new AuthMiError(
258
- result.error || "Authentication required",
259
- "UNAUTHORIZED",
260
- 401
261
- );
262
- }
263
- const hasScopes = requiredScopes.every(
264
- (scope) => result.scopes?.includes(scope)
265
- );
266
- if (!hasScopes) {
267
- throw new ScopeError(
268
- `Required scopes: ${requiredScopes.join(", ")}`,
269
- requiredScopes,
270
- result.scopes || []
271
- );
272
- }
273
- return result;
274
- }
275
- /**
276
- * Execute a function only if the user has the required scopes
277
- */
278
- async withScope(scopes, fn) {
279
- await this.requireScopes(...scopes);
280
- return fn();
281
- }
282
- // ============ OAuth ============
283
- /**
284
- * Initiate OAuth login flow and return the authorization URL
285
- */
286
- async initiateOAuth(provider, redirectUri) {
287
- if (!this.config.apiKey && !this.config.clientSecret) {
288
- throw new AuthMiError(
289
- "API key or client credentials required for OAuth. Configure the client first.",
290
- "MISSING_CREDENTIALS"
291
- );
292
- }
293
- if (!this.config.serviceId) {
294
- throw new AuthMiError(
295
- "serviceId required for OAuth. Configure the client first.",
296
- "MISSING_SERVICE_ID"
297
- );
298
- }
299
- const response = await this.request(
300
- "/api/v1/auth/oauth/initiate",
301
- {
302
- method: "POST",
303
- body: JSON.stringify({ provider, redirectUri, client_id: this.config.serviceId })
304
- }
305
- );
306
- return response.url;
179
+ this.storage.removeToken();
307
180
  }
308
- /**
309
- * Handle OAuth callback URL and store the access token
310
- * Returns null if no OAuth params are present in the URL
311
- */
312
- handleOAuthCallback(url) {
313
- const parsedUrl = typeof url === "string" ? new URL(url) : url;
314
- const token = parsedUrl.searchParams.get("token");
315
- const userId = parsedUrl.searchParams.get("user_id");
316
- const email = parsedUrl.searchParams.get("email");
317
- if (!token || !userId || !email) {
318
- return null;
319
- }
320
- this.setAccessToken(token);
321
- return { token, userId, email };
181
+ async isLoggedIn() {
182
+ const token = await this.storage.getToken();
183
+ return !!token;
322
184
  }
323
- /**
324
- * Check if the current URL contains OAuth callback params
325
- */
326
- isOAuthCallback(url) {
327
- const parsedUrl = typeof url === "string" ? new URL(url) : url;
328
- return parsedUrl.searchParams.has("token") && parsedUrl.searchParams.has("user_id") && parsedUrl.searchParams.has("email");
185
+ // ============ Auth Endpoints ============
186
+ async signup(credentials) {
187
+ const response = await this.request("/api/v1/auth/signup", {
188
+ method: "POST",
189
+ body: JSON.stringify(credentials)
190
+ });
191
+ return this.mapAuthToken(response);
329
192
  }
330
- // ============ Authentication ============
331
- /**
332
- * Login a user and store the access token
333
- */
334
193
  async login(credentials) {
335
- if (!this.config.apiKey && !this.config.clientSecret) {
336
- throw new AuthMiError(
337
- "API key or client credentials required for login. Configure the client first.",
338
- "MISSING_CREDENTIALS"
339
- );
340
- }
194
+ const cfg = this.config;
341
195
  const response = await this.request("/api/v1/auth/login", {
342
196
  method: "POST",
343
197
  body: JSON.stringify({
344
198
  email: credentials.email,
345
199
  password: credentials.password,
346
- client_id: this.config.serviceId,
347
- expires_in_hours: credentials.expiresInHours || this.config.defaultTokenExpiry,
200
+ client_id: cfg.serviceId,
201
+ expires_in_hours: credentials.expiresInHours || cfg.defaultTokenExpiry,
348
202
  scopes: credentials.scopes
349
203
  })
350
204
  });
351
- const token = {
352
- accessToken: response.access_token,
353
- tokenType: response.token_type,
354
- expiresIn: response.expires_in,
355
- expiresAt: response.expires_at,
356
- userId: response.user_id,
357
- email: response.email,
358
- scopes: response.scopes
359
- };
205
+ const token = this.mapAuthToken(response);
360
206
  this.setAccessToken(token.accessToken);
361
207
  return token;
362
208
  }
363
- /**
364
- * Logout the current user
365
- */
366
209
  async logout() {
367
- const token = this.getAccessToken();
210
+ const token = await this.storage.getToken();
368
211
  if (token) {
369
212
  try {
370
- await this.request("/api/v1/auth/logout", {
371
- method: "POST"
372
- }, true);
373
- } catch (error) {
213
+ await this.request("/api/v1/auth/logout", { method: "POST" }, true);
214
+ } catch {
374
215
  }
375
216
  }
376
217
  this.clearAccessToken();
377
218
  }
378
- /**
379
- * Validate the current token or an API key
380
- * @deprecated Use validateWithScopes instead
381
- */
382
- async validate(_credential) {
383
- return this.validateWithScopes();
384
- }
385
- /**
386
- * Refresh the current token
387
- */
388
219
  async refreshToken() {
389
- const response = await this.request("/api/v1/auth/refresh", {
390
- method: "POST"
391
- }, true);
220
+ const response = await this.request("/api/v1/auth/refresh", { method: "POST" }, true);
392
221
  const token = {
393
222
  accessToken: response.access_token,
394
- tokenType: response.token_type,
223
+ tokenType: "Bearer",
395
224
  expiresIn: response.expires_in,
396
225
  expiresAt: response.expires_at,
397
226
  userId: "",
398
- // Refresh doesn't return user info
399
227
  email: ""
400
228
  };
401
229
  this.setAccessToken(token.accessToken);
402
230
  return token;
403
231
  }
404
- // ============ User Management ============
405
- /**
406
- * Create a new user (requires admin API key)
407
- */
408
- async createUser(request) {
409
- const response = await this.request("/api/v1/users/create", {
232
+ async getMe() {
233
+ const response = await this.request("/api/v1/auth/me");
234
+ return { userId: response.user_id, email: response.email, name: response.name, createdAt: response.created_at };
235
+ }
236
+ async forgotPassword(request) {
237
+ return this.request("/api/v1/auth/forgot-password", {
410
238
  method: "POST",
411
- body: JSON.stringify(request)
239
+ body: JSON.stringify({ email: request.email, client_id: this.config.serviceId })
240
+ });
241
+ }
242
+ async resetPassword(request) {
243
+ return this.request("/api/v1/auth/reset-password", {
244
+ method: "POST",
245
+ body: JSON.stringify({ token: request.token, new_password: request.newPassword })
412
246
  });
247
+ }
248
+ async verifyEmail(request) {
249
+ return this.request("/api/v1/auth/verify-email", {
250
+ method: "POST",
251
+ body: JSON.stringify({ token: request.token })
252
+ });
253
+ }
254
+ async resendVerification(request) {
255
+ return this.request("/api/v1/auth/resend-verification", {
256
+ method: "POST",
257
+ body: JSON.stringify({ email: request.email, client_id: this.config.serviceId })
258
+ });
259
+ }
260
+ async deactivateAccount(password) {
261
+ await this.request("/api/v1/auth/deactivate", {
262
+ method: "POST",
263
+ body: JSON.stringify({ password })
264
+ });
265
+ }
266
+ mapAuthToken(raw) {
413
267
  return {
414
- userId: response.user_id,
415
- email: response.email,
416
- createdAt: response.created_at
268
+ accessToken: raw.access_token,
269
+ tokenType: raw.token_type,
270
+ expiresIn: raw.expires_in,
271
+ expiresAt: raw.expires_at,
272
+ userId: raw.user_id,
273
+ email: raw.email,
274
+ emailVerified: raw.email_verified,
275
+ scopes: raw.scopes
417
276
  };
418
277
  }
419
- /**
420
- * List all users for the service
421
- */
278
+ // ============ OAuth ============
279
+ async initiateOAuth(provider, redirectUri) {
280
+ const cfg = this.config;
281
+ if (!cfg.clientId && !cfg.clientSecret && !cfg.apiKey) {
282
+ throw new AuthMiError("Credentials required for OAuth", "MISSING_CREDENTIALS");
283
+ }
284
+ const response = await this.request("/api/v1/auth/oauth/initiate", {
285
+ method: "POST",
286
+ body: JSON.stringify({ provider, redirectUri, client_id: cfg.serviceId || cfg.clientId })
287
+ });
288
+ return response.url;
289
+ }
290
+ handleOAuthCallback(url) {
291
+ const parsed = typeof url === "string" ? new URL(url) : url;
292
+ const token = parsed.searchParams.get("token");
293
+ const userId = parsed.searchParams.get("user_id");
294
+ const email = parsed.searchParams.get("email");
295
+ if (!token || !userId || !email) return null;
296
+ this.setAccessToken(token);
297
+ return { token, userId, email };
298
+ }
299
+ isOAuthCallback(url) {
300
+ const parsed = typeof url === "string" ? new URL(url) : url;
301
+ return !!(parsed.searchParams.has("token") && parsed.searchParams.has("user_id") && parsed.searchParams.has("email"));
302
+ }
303
+ // ============ SAML ============
304
+ async initiateSaml(request) {
305
+ return this.request("/api/v1/auth/saml/initiate", {
306
+ method: "POST",
307
+ body: JSON.stringify({ email: request.email, redirect_uri: request.redirectUri })
308
+ });
309
+ }
310
+ async parseSamlMetadata(xml) {
311
+ return this.request("/api/v1/auth/saml/parse-metadata", {
312
+ method: "POST",
313
+ body: JSON.stringify({ metadata_xml: xml })
314
+ });
315
+ }
316
+ async listSamlConnections() {
317
+ const response = await this.request("/api/v1/auth/saml/connections");
318
+ return response.connections;
319
+ }
320
+ async createSamlConnection(request) {
321
+ return this.request("/api/v1/auth/saml/connections", {
322
+ method: "POST",
323
+ body: JSON.stringify(request)
324
+ });
325
+ }
326
+ async deleteSamlConnection(id) {
327
+ await this.request(`/api/v1/auth/saml/connections/${encodeURIComponent(id)}`, {
328
+ method: "DELETE"
329
+ });
330
+ }
331
+ // ============ 2FA ============
332
+ async setup2fa() {
333
+ return this.request("/api/v1/auth/2fa/setup", { method: "POST" });
334
+ }
335
+ async enable2fa(code) {
336
+ return this.request("/api/v1/auth/2fa/enable", {
337
+ method: "POST",
338
+ body: JSON.stringify({ code })
339
+ });
340
+ }
341
+ async disable2fa(password) {
342
+ await this.request("/api/v1/auth/2fa/disable", {
343
+ method: "POST",
344
+ body: JSON.stringify({ password })
345
+ });
346
+ }
347
+ async regenerateBackupCodes() {
348
+ return this.request("/api/v1/auth/2fa/backup-codes", { method: "POST" });
349
+ }
350
+ async challenge2fa(request) {
351
+ return this.request("/api/v1/auth/2fa/challenge", {
352
+ method: "POST",
353
+ body: JSON.stringify(request)
354
+ });
355
+ }
356
+ // ============ Users ============
357
+ async createUser(request) {
358
+ const response = await this.request(
359
+ "/api/v1/users/create",
360
+ {
361
+ method: "POST",
362
+ body: JSON.stringify(request)
363
+ }
364
+ );
365
+ return { userId: response.user_id, email: response.email, name: response.name, createdAt: response.created_at };
366
+ }
422
367
  async listUsers() {
423
368
  const response = await this.request("/api/v1/users/list");
424
369
  return response.users.map((u) => ({
425
370
  userId: u.user_id,
426
371
  email: u.email,
372
+ name: u.name,
427
373
  createdAt: u.created_at
428
374
  }));
429
375
  }
430
- /**
431
- * Delete a user
432
- */
376
+ async getUser(userId) {
377
+ const response = await this.request(
378
+ `/api/v1/users/${encodeURIComponent(userId)}`
379
+ );
380
+ return { userId: response.user_id, email: response.email, name: response.name, createdAt: response.created_at };
381
+ }
433
382
  async deleteUser(userId) {
434
383
  await this.request("/api/v1/users/delete", {
435
384
  method: "POST",
436
385
  body: JSON.stringify({ user_id: userId })
437
386
  });
438
387
  }
439
- /**
440
- * Update user password
441
- */
388
+ async deleteUserById(userId) {
389
+ await this.request(`/api/v1/users/${encodeURIComponent(userId)}`, { method: "DELETE" });
390
+ }
442
391
  async updatePassword(userId, oldPassword, newPassword) {
443
392
  await this.request("/api/v1/users/password", {
444
393
  method: "POST",
445
- body: JSON.stringify({
446
- user_id: userId,
447
- old_password: oldPassword,
448
- new_password: newPassword
449
- })
394
+ body: JSON.stringify({ user_id: userId, old_password: oldPassword, new_password: newPassword })
450
395
  });
451
396
  }
452
- // ============ Scope Management ============
453
- /**
454
- * List custom scopes for the service
455
- */
456
- async listScopes() {
457
- const response = await this.request("/api/v1/scopes/list");
458
- return response.scopes;
397
+ // ============ Roles ============
398
+ async createRole(request) {
399
+ const response = await this.request(
400
+ "/api/v1/users/roles/create",
401
+ { method: "POST", body: JSON.stringify(request) }
402
+ );
403
+ return { roleId: response.role_id, name: response.name, description: response.description, createdAt: response.created_at };
404
+ }
405
+ async listRoles() {
406
+ const response = await this.request("/api/v1/users/roles/list");
407
+ return response.roles.map((r) => ({
408
+ roleId: r.role_id,
409
+ name: r.name,
410
+ description: r.description,
411
+ createdAt: r.created_at
412
+ }));
459
413
  }
460
- /**
461
- * Create a custom scope
462
- */
463
- async createScope(request) {
464
- const response = await this.request("/api/v1/scopes/create", {
414
+ async deleteRole(roleId) {
415
+ await this.request(`/api/v1/users/roles/${encodeURIComponent(roleId)}`, { method: "DELETE" });
416
+ }
417
+ async assignRole(userId, roleId) {
418
+ await this.request("/api/v1/users/roles/assign", {
465
419
  method: "POST",
466
- body: JSON.stringify(request)
420
+ body: JSON.stringify({ user_id: userId, role_id: roleId })
467
421
  });
468
- return {
469
- scopeId: response.scope_id,
470
- name: response.name,
471
- key: response.key,
472
- description: response.description,
473
- createdAt: response.created_at
474
- };
475
422
  }
476
- /**
477
- * Delete a custom scope
478
- */
479
- async deleteScope(scopeId) {
480
- await this.request("/api/v1/scopes/delete", {
423
+ async unassignRole(userId, roleId) {
424
+ await this.request("/api/v1/users/roles/unassign", {
481
425
  method: "POST",
482
- body: JSON.stringify({ scope_id: scopeId })
426
+ body: JSON.stringify({ user_id: userId, role_id: roleId })
483
427
  });
484
428
  }
485
- // ============ API Key Management ============
486
- /**
487
- * List all API keys for the service
488
- */
429
+ async getUserRoles(userId) {
430
+ const response = await this.request(
431
+ `/api/v1/users/${encodeURIComponent(userId)}/roles`
432
+ );
433
+ return response.roles.map((r) => ({
434
+ roleId: r.role_id,
435
+ name: r.name,
436
+ description: r.description,
437
+ createdAt: r.created_at
438
+ }));
439
+ }
440
+ // ============ API Keys ============
489
441
  async listApiKeys() {
490
442
  const response = await this.request("/api/v1/keys/list");
491
443
  return response.keys.map((k) => ({
492
444
  keyId: k.keyId,
493
445
  name: k.name,
494
446
  scope: k.scope,
495
- scopes: [],
447
+ scopes: k.scopes,
496
448
  keyType: k.keyType,
449
+ tpsLimit: k.tpsLimit,
497
450
  revoked: k.revoked,
498
- createdAt: k.createdAt,
499
- tpsLimit: k.tpsLimit
451
+ createdAt: k.createdAt
500
452
  }));
501
453
  }
502
- /**
503
- * Create a new API key with optional custom scopes
504
- */
505
454
  async createApiKey(request) {
506
- const response = await this.request("/api/v1/keys/create", {
455
+ const response = await this.request(
456
+ "/api/v1/keys/create",
457
+ {
458
+ method: "POST",
459
+ body: JSON.stringify({
460
+ user_id: request.userId,
461
+ name: request.name,
462
+ scope: request.scope || "service",
463
+ scopes: request.scopes,
464
+ key_type: request.keyType || "service",
465
+ tps_limit: request.tpsLimit || 100
466
+ })
467
+ }
468
+ );
469
+ return { keyId: response.key_id, apiKey: response.api_key, name: response.name, scope: response.scope, scopes: response.scopes };
470
+ }
471
+ async revokeApiKey(keyId) {
472
+ await this.request("/api/v1/keys/revoke", {
473
+ method: "POST",
474
+ body: JSON.stringify({ key_id: keyId })
475
+ });
476
+ }
477
+ // ============ Scopes ============
478
+ async listScopes() {
479
+ const response = await this.request("/api/v1/scopes/list");
480
+ return response.scopes;
481
+ }
482
+ async createScope(request) {
483
+ const response = await this.request(
484
+ "/api/v1/scopes/create",
485
+ { method: "POST", body: JSON.stringify(request) }
486
+ );
487
+ return { scopeId: response.scope_id, name: response.name, key: response.key, description: response.description, createdAt: response.created_at };
488
+ }
489
+ async deleteScope(scopeId) {
490
+ await this.request("/api/v1/scopes/delete", {
491
+ method: "POST",
492
+ body: JSON.stringify({ scope_id: scopeId })
493
+ });
494
+ }
495
+ // ============ Introspection & Validation ============
496
+ async introspect(request) {
497
+ if (this.cache && request.token) {
498
+ const cached = this.cache.get(request.token);
499
+ if (cached) return cached;
500
+ }
501
+ const result = await this.request("/api/v1/introspect", {
507
502
  method: "POST",
508
503
  body: JSON.stringify({
509
- user_id: request.userId,
510
- name: request.name,
511
- scope: request.scope || "service",
512
- scopes: request.scopes,
513
- key_type: request.keyType || "service",
514
- tps_limit: request.tpsLimit || 100
504
+ serviceId: request.serviceId,
505
+ token: request.token,
506
+ apiKey: request.apiKey,
507
+ requiredScopes: request.requiredScopes
515
508
  })
516
509
  });
517
- return {
518
- keyId: response.key_id,
519
- apiKey: response.api_key,
520
- name: response.name,
521
- scope: response.scope,
522
- scopes: response.scopes
523
- };
510
+ if (this.cache && request.token) {
511
+ this.cache.set(request.token, result);
512
+ }
513
+ return result;
524
514
  }
525
- /**
526
- * Revoke an API key
527
- */
528
- async revokeApiKey(keyId) {
529
- await this.request("/api/v1/keys/revoke", {
515
+ async validate() {
516
+ const cfg = this.config;
517
+ const token = await this.storage.getToken();
518
+ if (!token) return { valid: false, error: "No token stored" };
519
+ if (!cfg.serviceId) return { valid: false, error: "Service ID not configured" };
520
+ if (!cfg.apiKey && !cfg.clientSecret) return { valid: false, error: "Service credentials not configured" };
521
+ try {
522
+ const result = await this.introspect({ serviceId: cfg.serviceId, token });
523
+ return {
524
+ valid: result.valid,
525
+ type: result.type === "bearer" ? "token" : "api_key",
526
+ scopes: result.scopes,
527
+ userId: result.userId,
528
+ email: result.email,
529
+ error: result.error
530
+ };
531
+ } catch (error) {
532
+ if (error instanceof ScopeError) {
533
+ return { valid: false, error: error.message, scopes: error.providedScopes };
534
+ }
535
+ if (error instanceof AuthMiError && error.statusCode === 401) {
536
+ return { valid: false, error: "Invalid or expired token" };
537
+ }
538
+ throw error;
539
+ }
540
+ }
541
+ async validateWithScopes(requiredScopes) {
542
+ const cfg = this.config;
543
+ const token = await this.storage.getToken();
544
+ if (!token) return { valid: false, error: "No token provided" };
545
+ if (!cfg.apiKey && !cfg.clientSecret) return { valid: false, error: "Service credentials not configured" };
546
+ if (!cfg.serviceId) return { valid: false, error: "Service ID not configured" };
547
+ try {
548
+ const result = await this.introspect({ serviceId: cfg.serviceId, token, requiredScopes });
549
+ return {
550
+ valid: result.valid,
551
+ type: result.type === "bearer" ? "token" : "api_key",
552
+ scopes: result.scopes,
553
+ userId: result.userId,
554
+ email: result.email,
555
+ error: result.error
556
+ };
557
+ } catch (error) {
558
+ if (error instanceof ScopeError) {
559
+ return { valid: false, error: error.message, scopes: error.providedScopes };
560
+ }
561
+ if (error instanceof AuthMiError && error.statusCode === 401) {
562
+ return { valid: false, error: "Invalid or expired token" };
563
+ }
564
+ throw error;
565
+ }
566
+ }
567
+ async requireScopes(...requiredScopes) {
568
+ const result = await this.validateWithScopes(requiredScopes);
569
+ if (!result.valid) {
570
+ throw new AuthMiError(result.error || "Authentication required", "UNAUTHORIZED", 401);
571
+ }
572
+ const hasScopes = requiredScopes.every((s) => result.scopes?.includes(s));
573
+ if (!hasScopes) {
574
+ throw new ScopeError(`Required: ${requiredScopes.join(", ")}`, requiredScopes, result.scopes || []);
575
+ }
576
+ return result;
577
+ }
578
+ async withScope(scopes, fn) {
579
+ await this.requireScopes(...scopes);
580
+ return fn();
581
+ }
582
+ // ============ Webhooks ============
583
+ async listWebhooks() {
584
+ const response = await this.request("/api/v1/webhooks/list");
585
+ return response.webhooks;
586
+ }
587
+ async registerWebhook(request) {
588
+ const response = await this.request(
589
+ "/api/v1/webhooks/register",
590
+ { method: "POST", body: JSON.stringify(request) }
591
+ );
592
+ return { webhookId: response.webhook_id, url: response.url, events: response.events, active: response.active, createdAt: response.created_at };
593
+ }
594
+ async deleteWebhook(webhookId) {
595
+ await this.request("/api/v1/webhooks/delete", {
530
596
  method: "POST",
531
- body: JSON.stringify({ key_id: keyId })
597
+ body: JSON.stringify({ webhook_id: webhookId })
598
+ });
599
+ }
600
+ // ============ Billing ============
601
+ async listPlans() {
602
+ const response = await this.request("/api/v1/billing/plans");
603
+ return response.plans;
604
+ }
605
+ async getSubscription() {
606
+ return this.request("/api/v1/billing/subscription");
607
+ }
608
+ async createCheckout(priceId) {
609
+ return this.request("/api/v1/billing/checkout", {
610
+ method: "POST",
611
+ body: JSON.stringify({ price_id: priceId })
532
612
  });
533
613
  }
534
- // ============ Service Management ============
535
- /**
536
- * Onboard a new service (requires master admin key)
537
- */
614
+ async createPortalSession() {
615
+ return this.request("/api/v1/billing/portal", { method: "POST" });
616
+ }
617
+ // ============ Audit ============
618
+ async getAuditLogs(query) {
619
+ const params = new URLSearchParams();
620
+ if (query?.limit) params.set("limit", String(query.limit));
621
+ if (query?.offset) params.set("offset", String(query.offset));
622
+ if (query?.actorId) params.set("actor_id", query.actorId);
623
+ if (query?.action) params.set("action", query.action);
624
+ if (query?.resourceType) params.set("resource_type", query.resourceType);
625
+ if (query?.startDate) params.set("start_date", query.startDate);
626
+ if (query?.endDate) params.set("end_date", query.endDate);
627
+ const qs = params.toString();
628
+ const response = await this.request(
629
+ `/api/v1/audit-logs${qs ? `?${qs}` : ""}`
630
+ );
631
+ return response.logs;
632
+ }
633
+ // ============ Services ============
634
+ async getService(serviceId) {
635
+ return this.request(`/api/v1/services/${encodeURIComponent(serviceId)}`);
636
+ }
637
+ async updateService(serviceId, data) {
638
+ return this.request(`/api/v1/services/${encodeURIComponent(serviceId)}`, {
639
+ method: "POST",
640
+ body: JSON.stringify(data)
641
+ });
642
+ }
643
+ async rotateClientSecret(serviceId) {
644
+ return this.request(
645
+ `/api/v1/services/${encodeURIComponent(serviceId)}/rotate-secret`,
646
+ { method: "POST" }
647
+ );
648
+ }
649
+ // ============ JWKS ============
650
+ async getJwks(serviceId) {
651
+ return this.request(`/.well-known/jwks.json?service_id=${encodeURIComponent(serviceId)}`);
652
+ }
653
+ // ============ Onboarding (admin only) ============
538
654
  async onboardService(request, masterAdminKey) {
539
655
  const response = await this.request("/api/v1/services/onboard", {
540
656
  method: "POST",
@@ -555,9 +671,6 @@ var AuthMiClient = class {
555
671
  createdAt: response.created_at
556
672
  };
557
673
  }
558
- /**
559
- * Delete a service (requires master admin key)
560
- */
561
674
  async deleteService(serviceId, masterAdminKey) {
562
675
  await this.request("/api/v1/services/deboard", {
563
676
  method: "POST",
@@ -567,41 +680,6 @@ var AuthMiClient = class {
567
680
  body: JSON.stringify({ service_id: serviceId })
568
681
  });
569
682
  }
570
- // ============ Webhook Management ============
571
- /**
572
- * List webhooks
573
- */
574
- async listWebhooks() {
575
- const response = await this.request(
576
- "/api/v1/webhooks/list"
577
- );
578
- return response.webhooks;
579
- }
580
- /**
581
- * Register a webhook
582
- */
583
- async registerWebhook(request) {
584
- const response = await this.request("/api/v1/webhooks/register", {
585
- method: "POST",
586
- body: JSON.stringify(request)
587
- });
588
- return {
589
- webhookId: response.webhook_id,
590
- url: response.url,
591
- events: response.events,
592
- active: response.active,
593
- createdAt: response.created_at
594
- };
595
- }
596
- /**
597
- * Delete a webhook
598
- */
599
- async deleteWebhook(webhookId) {
600
- await this.request("/api/v1/webhooks/delete", {
601
- method: "POST",
602
- body: JSON.stringify({ webhook_id: webhookId })
603
- });
604
- }
605
683
  };
606
684
 
607
685
  // src/react.tsx
@@ -609,27 +687,30 @@ var import_jsx_runtime = require("react/jsx-runtime");
609
687
  var AuthMiContext = (0, import_react.createContext)(null);
610
688
  function AuthMiProvider({
611
689
  config,
612
- tokenStorage,
690
+ options,
613
691
  children,
614
692
  validateOnMount = true,
615
693
  validationInterval = 5 * 60 * 1e3
616
694
  }) {
617
- const [client] = (0, import_react.useState)(() => new AuthMiClient(config, tokenStorage));
695
+ const [client] = (0, import_react.useState)(() => new AuthMiClient(config, options));
618
696
  const [isAuthenticated, setIsAuthenticated] = (0, import_react.useState)(false);
619
697
  const [user, setUser] = (0, import_react.useState)(null);
620
698
  const [isLoading, setIsLoading] = (0, import_react.useState)(true);
621
699
  (0, import_react.useEffect)(() => {
622
700
  const checkAuth = async () => {
623
- const token = client.getAccessToken();
701
+ const token = await client.getAccessToken();
624
702
  if (token && validateOnMount) {
625
703
  try {
626
704
  const result = await client.validate();
627
705
  if (result.valid) {
628
706
  setIsAuthenticated(true);
707
+ if (result.userId && result.email) {
708
+ setUser({ userId: result.userId, email: result.email, createdAt: "" });
709
+ }
629
710
  } else {
630
711
  client.clearAccessToken();
631
712
  }
632
- } catch (error) {
713
+ } catch {
633
714
  client.clearAccessToken();
634
715
  }
635
716
  }
@@ -646,30 +727,34 @@ function AuthMiProvider({
646
727
  setIsAuthenticated(false);
647
728
  setUser(null);
648
729
  }
649
- } catch (error) {
730
+ } catch {
650
731
  setIsAuthenticated(false);
651
732
  setUser(null);
652
733
  }
653
734
  }, validationInterval);
654
735
  return () => clearInterval(interval);
655
736
  }, [client, isAuthenticated, validationInterval]);
656
- const login = (0, import_react.useCallback)(
657
- async (email, password) => {
658
- setIsLoading(true);
659
- try {
660
- const token = await client.login({ email, password });
661
- setIsAuthenticated(true);
662
- setUser({
663
- userId: token.userId,
664
- email: token.email,
665
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
666
- });
667
- } finally {
668
- setIsLoading(false);
669
- }
670
- },
671
- [client]
672
- );
737
+ const login = (0, import_react.useCallback)(async (email, password) => {
738
+ setIsLoading(true);
739
+ try {
740
+ const token = await client.login({ email, password });
741
+ setIsAuthenticated(true);
742
+ setUser({ userId: token.userId, email: token.email, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
743
+ } finally {
744
+ setIsLoading(false);
745
+ }
746
+ }, [client]);
747
+ const signup = (0, import_react.useCallback)(async (email, password, name) => {
748
+ setIsLoading(true);
749
+ try {
750
+ const cfg = client["config"];
751
+ const token = await client.signup({ email, password, name, client_id: cfg.serviceId || cfg.clientId });
752
+ setIsAuthenticated(true);
753
+ setUser({ userId: token.userId, email: token.email, createdAt: (/* @__PURE__ */ new Date()).toISOString() });
754
+ } finally {
755
+ setIsLoading(false);
756
+ }
757
+ }, [client]);
673
758
  const logout = (0, import_react.useCallback)(async () => {
674
759
  setIsLoading(true);
675
760
  try {
@@ -694,17 +779,26 @@ function AuthMiProvider({
694
779
  user,
695
780
  isLoading,
696
781
  login,
782
+ signup,
697
783
  logout,
698
- validate
784
+ validate,
785
+ setup2fa: () => client.setup2fa(),
786
+ enable2fa: (code) => client.enable2fa(code),
787
+ disable2fa: (password) => client.disable2fa(password),
788
+ regenerateBackupCodes: () => client.regenerateBackupCodes(),
789
+ challenge2fa: (request) => client.challenge2fa(request),
790
+ forgotPassword: (email) => client.forgotPassword({ email }),
791
+ initiateOAuth: (provider, redirectUri) => client.initiateOAuth(provider, redirectUri),
792
+ handleOAuthCallback: (url) => client.handleOAuthCallback(url),
793
+ isOAuthCallback: (url) => client.isOAuthCallback(url),
794
+ getMe: () => client.getMe()
699
795
  };
700
796
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AuthMiContext.Provider, { value, children });
701
797
  }
702
798
  function useAuthMi() {
703
- const context = (0, import_react.useContext)(AuthMiContext);
704
- if (!context) {
705
- throw new Error("useAuthMi must be used within AuthMiProvider");
706
- }
707
- return context;
799
+ const ctx = (0, import_react.useContext)(AuthMiContext);
800
+ if (!ctx) throw new Error("useAuthMi must be used within AuthMiProvider");
801
+ return ctx;
708
802
  }
709
803
  function useAuthMiClient() {
710
804
  return useAuthMi().client;
@@ -716,17 +810,37 @@ function useUser() {
716
810
  return useAuthMi().user;
717
811
  }
718
812
  function useAuthActions() {
719
- const { login, logout, isLoading } = useAuthMi();
720
- return { login, logout, isLoading };
813
+ const {
814
+ login,
815
+ signup,
816
+ logout,
817
+ isLoading,
818
+ forgotPassword,
819
+ setup2fa,
820
+ enable2fa,
821
+ disable2fa,
822
+ regenerateBackupCodes,
823
+ challenge2fa,
824
+ initiateOAuth
825
+ } = useAuthMi();
826
+ return {
827
+ login,
828
+ signup,
829
+ logout,
830
+ isLoading,
831
+ forgotPassword,
832
+ setup2fa,
833
+ enable2fa,
834
+ disable2fa,
835
+ regenerateBackupCodes,
836
+ challenge2fa,
837
+ initiateOAuth
838
+ };
721
839
  }
722
840
  function Protected({ children, fallback = null }) {
723
841
  const { isAuthenticated, isLoading } = useAuthMi();
724
- if (isLoading) {
725
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: fallback });
726
- }
727
- if (!isAuthenticated) {
728
- return null;
729
- }
842
+ if (isLoading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: fallback });
843
+ if (!isAuthenticated) return null;
730
844
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
731
845
  }
732
846
  function LoginForm({ onSuccess, onError, className }) {
@@ -741,9 +855,9 @@ function LoginForm({ onSuccess, onError, className }) {
741
855
  await login(email, password);
742
856
  onSuccess?.();
743
857
  } catch (err) {
744
- const message = err instanceof Error ? err.message : "Login failed";
745
- setError(message);
746
- onError?.(err instanceof Error ? err : new Error(message));
858
+ const msg = err instanceof Error ? err.message : "Login failed";
859
+ setError(msg);
860
+ onError?.(err instanceof Error ? err : new Error(msg));
747
861
  }
748
862
  };
749
863
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { onSubmit: handleSubmit, className, children: [
@@ -781,14 +895,7 @@ function LoginForm({ onSuccess, onError, className }) {
781
895
  {
782
896
  type: "submit",
783
897
  disabled: isLoading,
784
- style: {
785
- padding: "0.5rem 1rem",
786
- background: isLoading ? "#ccc" : "#007bff",
787
- color: "white",
788
- border: "none",
789
- borderRadius: "4px",
790
- cursor: isLoading ? "not-allowed" : "pointer"
791
- },
898
+ style: { padding: "0.5rem 1rem", background: isLoading ? "#ccc" : "#007bff", color: "white", border: "none", borderRadius: "4px", cursor: isLoading ? "not-allowed" : "pointer" },
792
899
  children: isLoading ? "Logging in..." : "Login"
793
900
  }
794
901
  )
@@ -799,9 +906,7 @@ function LoginForm({ onSuccess, onError, className }) {
799
906
  AuthMiClient,
800
907
  AuthMiError,
801
908
  AuthMiProvider,
802
- LocalStorageTokenStorage,
803
909
  LoginForm,
804
- MemoryTokenStorage,
805
910
  Protected,
806
911
  ScopeError,
807
912
  useAuthActions,