authmi 1.0.0 → 1.2.0

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