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/index.js CHANGED
@@ -22,6 +22,8 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AuthMiClient: () => AuthMiClient,
24
24
  AuthMiError: () => AuthMiError,
25
+ CookieTokenStorage: () => CookieTokenStorage,
26
+ IntrospectCache: () => IntrospectCache,
25
27
  LocalStorageTokenStorage: () => LocalStorageTokenStorage,
26
28
  MemoryTokenStorage: () => MemoryTokenStorage,
27
29
  ScopeError: () => ScopeError,
@@ -29,6 +31,57 @@ __export(index_exports, {
29
31
  });
30
32
  module.exports = __toCommonJS(index_exports);
31
33
 
34
+ // src/config.ts
35
+ function resolveConfig(provider) {
36
+ const raw = typeof provider === "function" ? provider() : provider;
37
+ return {
38
+ baseUrl: raw.baseUrl,
39
+ apiKey: raw.apiKey ?? "",
40
+ serviceId: raw.serviceId ?? "",
41
+ clientId: raw.clientId ?? "",
42
+ clientSecret: raw.clientSecret ?? "",
43
+ defaultTokenExpiry: raw.defaultTokenExpiry ?? 24
44
+ };
45
+ }
46
+
47
+ // src/storage/localStorage.ts
48
+ var STORAGE_KEY = "authmi_token";
49
+ var LocalStorageTokenStorage = class {
50
+ canAccess() {
51
+ if (typeof window === "undefined") return false;
52
+ try {
53
+ const probe = "__authmi_storage_test__";
54
+ localStorage.setItem(probe, "1");
55
+ localStorage.removeItem(probe);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+ getToken() {
62
+ if (!this.canAccess()) return null;
63
+ try {
64
+ return localStorage.getItem(STORAGE_KEY);
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+ setToken(token) {
70
+ if (!this.canAccess()) return;
71
+ try {
72
+ localStorage.setItem(STORAGE_KEY, token);
73
+ } catch {
74
+ }
75
+ }
76
+ removeToken() {
77
+ if (!this.canAccess()) return;
78
+ try {
79
+ localStorage.removeItem(STORAGE_KEY);
80
+ } catch {
81
+ }
82
+ }
83
+ };
84
+
32
85
  // src/types.ts
33
86
  var AuthMiError = class extends Error {
34
87
  constructor(message, code, statusCode) {
@@ -46,82 +99,38 @@ var ScopeError = class extends AuthMiError {
46
99
  this.name = "ScopeError";
47
100
  }
48
101
  };
49
- var _LocalStorageTokenStorage = class _LocalStorageTokenStorage {
50
- getToken() {
51
- if (typeof window === "undefined") return null;
52
- return localStorage.getItem(_LocalStorageTokenStorage.KEY);
53
- }
54
- setToken(token) {
55
- if (typeof window === "undefined") return;
56
- localStorage.setItem(_LocalStorageTokenStorage.KEY, token);
57
- }
58
- removeToken() {
59
- if (typeof window === "undefined") return;
60
- localStorage.removeItem(_LocalStorageTokenStorage.KEY);
61
- }
62
- };
63
- _LocalStorageTokenStorage.KEY = "authmi_token";
64
- var LocalStorageTokenStorage = _LocalStorageTokenStorage;
65
- var MemoryTokenStorage = class {
66
- constructor() {
67
- this.token = null;
68
- }
69
- getToken() {
70
- return this.token;
71
- }
72
- setToken(token) {
73
- this.token = token;
74
- }
75
- removeToken() {
76
- this.token = null;
77
- }
78
- };
79
102
 
80
103
  // src/client.ts
81
104
  var AuthMiClient = class {
82
- constructor(config, tokenStorage) {
83
- this.config = {
84
- apiKey: "",
85
- serviceId: "",
86
- clientId: "",
87
- clientSecret: "",
88
- defaultTokenExpiry: 24,
89
- ...config
90
- };
91
- this.tokenStorage = tokenStorage || new LocalStorageTokenStorage();
92
- }
93
- /** Build Basic auth header from clientId:clientSecret */
94
- basicAuthHeader() {
95
- if (this.config.clientId && this.config.clientSecret) {
96
- const encoded = btoa(`${this.config.clientId}:${this.config.clientSecret}`);
97
- return `Basic ${encoded}`;
98
- }
99
- return null;
100
- }
101
- // ============ HTTP Utilities ============
102
- async request(endpoint, options = {}, requireAuth = false) {
103
- const url = `${this.config.baseUrl}${endpoint}`;
105
+ constructor(config, options) {
106
+ this.configProvider = config;
107
+ this.storage = options?.storage ?? new LocalStorageTokenStorage();
108
+ this.cache = options?.cache || null;
109
+ }
110
+ /** Resolve current config (supports lazy evaluation) */
111
+ get config() {
112
+ return resolveConfig(this.configProvider);
113
+ }
114
+ // ============ HTTP plumbing ============
115
+ async request(endpoint, options = {}, requireUserToken = false) {
116
+ const cfg = this.config;
117
+ const url = `${cfg.baseUrl}${endpoint}`;
104
118
  const headers = {
105
119
  "Content-Type": "application/json",
106
120
  ...options.headers || {}
107
121
  };
108
- const basic = this.basicAuthHeader();
109
- if (basic && !requireAuth && !headers["Authorization"]) {
110
- headers["Authorization"] = basic;
111
- } else if (this.config.apiKey && !requireAuth && !headers["Authorization"]) {
112
- headers["Authorization"] = `Bearer ${this.config.apiKey}`;
113
- }
114
- if (requireAuth) {
115
- const token = this.getAccessToken();
116
- if (token && !headers["Authorization"]) {
117
- headers["Authorization"] = `Bearer ${token}`;
122
+ if (!headers["Authorization"]) {
123
+ if (requireUserToken) {
124
+ const token = await this.storage.getToken();
125
+ if (token) headers["Authorization"] = `Bearer ${token}`;
126
+ } else if (cfg.clientId && cfg.clientSecret) {
127
+ headers["Authorization"] = `Basic ${btoa(`${cfg.clientId}:${cfg.clientSecret}`)}`;
128
+ } else if (cfg.apiKey) {
129
+ headers["Authorization"] = `Bearer ${cfg.apiKey}`;
118
130
  }
119
131
  }
120
132
  try {
121
- const response = await fetch(url, {
122
- ...options,
123
- headers
124
- });
133
+ const response = await fetch(url, { ...options, headers });
125
134
  const data = await response.json();
126
135
  if (!response.ok) {
127
136
  if (response.status === 403 && data.required && data.provided) {
@@ -139,9 +148,7 @@ var AuthMiClient = class {
139
148
  }
140
149
  return data;
141
150
  } catch (error) {
142
- if (error instanceof AuthMiError) {
143
- throw error;
144
- }
151
+ if (error instanceof AuthMiError) throw error;
145
152
  throw new AuthMiError(
146
153
  error instanceof Error ? error.message : "Network error",
147
154
  "NETWORK_ERROR"
@@ -149,384 +156,497 @@ var AuthMiClient = class {
149
156
  }
150
157
  }
151
158
  // ============ Configuration ============
152
- /**
153
- * Update the client's service configuration
154
- */
155
159
  configure(config) {
156
- this.config = { ...this.config, ...config };
160
+ const current = resolveConfig(this.configProvider);
161
+ this.configProvider = { ...current, ...config };
157
162
  }
158
- /**
159
- * Get current configuration (without sensitive data)
160
- */
161
163
  getConfig() {
162
- const { apiKey: _, clientSecret: __, ...rest } = this.config;
164
+ const { apiKey: _, clientSecret: __, ...rest } = resolveConfig(this.configProvider);
163
165
  return rest;
164
166
  }
165
167
  // ============ Token Management ============
166
- /**
167
- * Get the stored access token
168
- */
169
- getAccessToken() {
170
- return this.tokenStorage.getToken();
168
+ async getAccessToken() {
169
+ return this.storage.getToken();
171
170
  }
172
- /**
173
- * Store an access token
174
- */
175
171
  setAccessToken(token) {
176
- this.tokenStorage.setToken(token);
172
+ this.storage.setToken(token);
177
173
  }
178
- /**
179
- * Remove the stored access token (logout)
180
- */
181
174
  clearAccessToken() {
182
- this.tokenStorage.removeToken();
175
+ this.storage.removeToken();
183
176
  }
184
- /**
185
- * Check if user is logged in
186
- */
187
- isLoggedIn() {
188
- return !!this.getAccessToken();
177
+ async isLoggedIn() {
178
+ const token = await this.storage.getToken();
179
+ return !!token;
189
180
  }
190
- // ============ Scope Validation ============
191
- /**
192
- * Introspect a token or API key and validate required scopes
193
- */
194
- async introspect(request) {
195
- return this.request("/api/v1/introspect", {
181
+ // ============ Auth Endpoints ============
182
+ async signup(credentials) {
183
+ const response = await this.request("/api/v1/auth/signup", {
196
184
  method: "POST",
197
- body: JSON.stringify(request)
185
+ body: JSON.stringify(credentials)
198
186
  });
187
+ return this.mapAuthToken(response);
199
188
  }
200
- /**
201
- * Validate the current token with optional scope checking
202
- */
203
- async validateWithScopes(requiredScopes) {
204
- const token = this.getAccessToken();
205
- if (!token) {
206
- return { valid: false, error: "No token provided" };
207
- }
208
- if (!this.config.apiKey && !this.config.clientSecret) {
209
- return { valid: false, error: "Service credentials not configured" };
210
- }
211
- if (!this.config.serviceId) {
212
- return { valid: false, error: "Service ID not configured in AuthMiConfig" };
213
- }
214
- try {
215
- const result = await this.introspect({
216
- serviceId: this.config.serviceId,
217
- token,
218
- requiredScopes
219
- });
220
- return {
221
- valid: result.valid,
222
- type: result.type === "bearer" ? "token" : "api_key",
223
- scopes: result.scopes,
224
- userId: result.userId,
225
- email: result.email,
226
- error: result.error
227
- };
228
- } catch (error) {
229
- if (error instanceof ScopeError) {
230
- return {
231
- valid: false,
232
- error: error.message,
233
- scopes: error.providedScopes
234
- };
235
- }
236
- if (error instanceof AuthMiError && error.statusCode === 401) {
237
- return { valid: false, error: "Invalid or expired token" };
238
- }
239
- throw error;
240
- }
241
- }
242
- /**
243
- * Check if the current user has all the required scopes
244
- * Throws ScopeError if scopes are insufficient
245
- */
246
- async requireScopes(...requiredScopes) {
247
- const result = await this.validateWithScopes(requiredScopes);
248
- if (!result.valid) {
249
- throw new AuthMiError(
250
- result.error || "Authentication required",
251
- "UNAUTHORIZED",
252
- 401
253
- );
254
- }
255
- const hasScopes = requiredScopes.every(
256
- (scope) => result.scopes?.includes(scope)
257
- );
258
- if (!hasScopes) {
259
- throw new ScopeError(
260
- `Required scopes: ${requiredScopes.join(", ")}`,
261
- requiredScopes,
262
- result.scopes || []
263
- );
264
- }
265
- return result;
266
- }
267
- /**
268
- * Execute a function only if the user has the required scopes
269
- */
270
- async withScope(scopes, fn) {
271
- await this.requireScopes(...scopes);
272
- return fn();
273
- }
274
- // ============ OAuth ============
275
- /**
276
- * Initiate OAuth login flow and return the authorization URL
277
- */
278
- async initiateOAuth(provider, redirectUri) {
279
- if (!this.config.apiKey && !this.config.clientSecret) {
280
- throw new AuthMiError(
281
- "API key or client credentials required for OAuth. Configure the client first.",
282
- "MISSING_CREDENTIALS"
283
- );
284
- }
285
- if (!this.config.serviceId) {
286
- throw new AuthMiError(
287
- "serviceId required for OAuth. Configure the client first.",
288
- "MISSING_SERVICE_ID"
289
- );
290
- }
291
- const response = await this.request(
292
- "/api/v1/auth/oauth/initiate",
293
- {
294
- method: "POST",
295
- body: JSON.stringify({ provider, redirectUri, client_id: this.config.serviceId })
296
- }
297
- );
298
- return response.url;
299
- }
300
- /**
301
- * Handle OAuth callback URL and store the access token
302
- * Returns null if no OAuth params are present in the URL
303
- */
304
- handleOAuthCallback(url) {
305
- const parsedUrl = typeof url === "string" ? new URL(url) : url;
306
- const token = parsedUrl.searchParams.get("token");
307
- const userId = parsedUrl.searchParams.get("user_id");
308
- const email = parsedUrl.searchParams.get("email");
309
- if (!token || !userId || !email) {
310
- return null;
311
- }
312
- this.setAccessToken(token);
313
- return { token, userId, email };
314
- }
315
- /**
316
- * Check if the current URL contains OAuth callback params
317
- */
318
- isOAuthCallback(url) {
319
- const parsedUrl = typeof url === "string" ? new URL(url) : url;
320
- return parsedUrl.searchParams.has("token") && parsedUrl.searchParams.has("user_id") && parsedUrl.searchParams.has("email");
321
- }
322
- // ============ Authentication ============
323
- /**
324
- * Login a user and store the access token
325
- */
326
189
  async login(credentials) {
327
- if (!this.config.apiKey && !this.config.clientSecret) {
328
- throw new AuthMiError(
329
- "API key or client credentials required for login. Configure the client first.",
330
- "MISSING_CREDENTIALS"
331
- );
332
- }
190
+ const cfg = this.config;
333
191
  const response = await this.request("/api/v1/auth/login", {
334
192
  method: "POST",
335
193
  body: JSON.stringify({
336
194
  email: credentials.email,
337
195
  password: credentials.password,
338
- client_id: this.config.serviceId,
339
- expires_in_hours: credentials.expiresInHours || this.config.defaultTokenExpiry,
196
+ client_id: cfg.serviceId,
197
+ expires_in_hours: credentials.expiresInHours || cfg.defaultTokenExpiry,
340
198
  scopes: credentials.scopes
341
199
  })
342
200
  });
343
- const token = {
344
- accessToken: response.access_token,
345
- tokenType: response.token_type,
346
- expiresIn: response.expires_in,
347
- expiresAt: response.expires_at,
348
- userId: response.user_id,
349
- email: response.email,
350
- scopes: response.scopes
351
- };
201
+ const token = this.mapAuthToken(response);
352
202
  this.setAccessToken(token.accessToken);
353
203
  return token;
354
204
  }
355
- /**
356
- * Logout the current user
357
- */
358
205
  async logout() {
359
- const token = this.getAccessToken();
206
+ const token = await this.storage.getToken();
360
207
  if (token) {
361
208
  try {
362
- await this.request("/api/v1/auth/logout", {
363
- method: "POST"
364
- }, true);
365
- } catch (error) {
209
+ await this.request("/api/v1/auth/logout", { method: "POST" }, true);
210
+ } catch {
366
211
  }
367
212
  }
368
213
  this.clearAccessToken();
369
214
  }
370
- /**
371
- * Validate the current token or an API key
372
- * @deprecated Use validateWithScopes instead
373
- */
374
- async validate(_credential) {
375
- return this.validateWithScopes();
376
- }
377
- /**
378
- * Refresh the current token
379
- */
380
215
  async refreshToken() {
381
- const response = await this.request("/api/v1/auth/refresh", {
382
- method: "POST"
383
- }, true);
216
+ const response = await this.request("/api/v1/auth/refresh", { method: "POST" }, true);
384
217
  const token = {
385
218
  accessToken: response.access_token,
386
- tokenType: response.token_type,
219
+ tokenType: "Bearer",
387
220
  expiresIn: response.expires_in,
388
221
  expiresAt: response.expires_at,
389
222
  userId: "",
390
- // Refresh doesn't return user info
391
223
  email: ""
392
224
  };
393
225
  this.setAccessToken(token.accessToken);
394
226
  return token;
395
227
  }
396
- // ============ User Management ============
397
- /**
398
- * Create a new user (requires admin API key)
399
- */
400
- async createUser(request) {
401
- const response = await this.request("/api/v1/users/create", {
228
+ async getMe() {
229
+ const response = await this.request("/api/v1/auth/me");
230
+ return { userId: response.user_id, email: response.email, name: response.name, createdAt: response.created_at };
231
+ }
232
+ async forgotPassword(request) {
233
+ return this.request("/api/v1/auth/forgot-password", {
402
234
  method: "POST",
403
- body: JSON.stringify(request)
235
+ body: JSON.stringify({ email: request.email, client_id: this.config.serviceId })
236
+ });
237
+ }
238
+ async resetPassword(request) {
239
+ return this.request("/api/v1/auth/reset-password", {
240
+ method: "POST",
241
+ body: JSON.stringify({ token: request.token, new_password: request.newPassword })
242
+ });
243
+ }
244
+ async verifyEmail(request) {
245
+ return this.request("/api/v1/auth/verify-email", {
246
+ method: "POST",
247
+ body: JSON.stringify({ token: request.token })
248
+ });
249
+ }
250
+ async resendVerification(request) {
251
+ return this.request("/api/v1/auth/resend-verification", {
252
+ method: "POST",
253
+ body: JSON.stringify({ email: request.email, client_id: this.config.serviceId })
404
254
  });
255
+ }
256
+ async deactivateAccount(password) {
257
+ await this.request("/api/v1/auth/deactivate", {
258
+ method: "POST",
259
+ body: JSON.stringify({ password })
260
+ });
261
+ }
262
+ mapAuthToken(raw) {
405
263
  return {
406
- userId: response.user_id,
407
- email: response.email,
408
- createdAt: response.created_at
264
+ accessToken: raw.access_token,
265
+ tokenType: raw.token_type,
266
+ expiresIn: raw.expires_in,
267
+ expiresAt: raw.expires_at,
268
+ userId: raw.user_id,
269
+ email: raw.email,
270
+ emailVerified: raw.email_verified,
271
+ scopes: raw.scopes
409
272
  };
410
273
  }
411
- /**
412
- * List all users for the service
413
- */
274
+ // ============ OAuth ============
275
+ async initiateOAuth(provider, redirectUri) {
276
+ const cfg = this.config;
277
+ if (!cfg.clientId && !cfg.clientSecret && !cfg.apiKey) {
278
+ throw new AuthMiError("Credentials required for OAuth", "MISSING_CREDENTIALS");
279
+ }
280
+ const response = await this.request("/api/v1/auth/oauth/initiate", {
281
+ method: "POST",
282
+ body: JSON.stringify({ provider, redirectUri, client_id: cfg.serviceId || cfg.clientId })
283
+ });
284
+ return response.url;
285
+ }
286
+ handleOAuthCallback(url) {
287
+ const parsed = typeof url === "string" ? new URL(url) : url;
288
+ const token = parsed.searchParams.get("token");
289
+ const userId = parsed.searchParams.get("user_id");
290
+ const email = parsed.searchParams.get("email");
291
+ if (!token || !userId || !email) return null;
292
+ this.setAccessToken(token);
293
+ return { token, userId, email };
294
+ }
295
+ isOAuthCallback(url) {
296
+ const parsed = typeof url === "string" ? new URL(url) : url;
297
+ return !!(parsed.searchParams.has("token") && parsed.searchParams.has("user_id") && parsed.searchParams.has("email"));
298
+ }
299
+ // ============ SAML ============
300
+ async initiateSaml(request) {
301
+ return this.request("/api/v1/auth/saml/initiate", {
302
+ method: "POST",
303
+ body: JSON.stringify({ email: request.email, redirect_uri: request.redirectUri })
304
+ });
305
+ }
306
+ async parseSamlMetadata(xml) {
307
+ return this.request("/api/v1/auth/saml/parse-metadata", {
308
+ method: "POST",
309
+ body: JSON.stringify({ metadata_xml: xml })
310
+ });
311
+ }
312
+ async listSamlConnections() {
313
+ const response = await this.request("/api/v1/auth/saml/connections");
314
+ return response.connections;
315
+ }
316
+ async createSamlConnection(request) {
317
+ return this.request("/api/v1/auth/saml/connections", {
318
+ method: "POST",
319
+ body: JSON.stringify(request)
320
+ });
321
+ }
322
+ async deleteSamlConnection(id) {
323
+ await this.request(`/api/v1/auth/saml/connections/${encodeURIComponent(id)}`, {
324
+ method: "DELETE"
325
+ });
326
+ }
327
+ // ============ 2FA ============
328
+ async setup2fa() {
329
+ return this.request("/api/v1/auth/2fa/setup", { method: "POST" });
330
+ }
331
+ async enable2fa(code) {
332
+ return this.request("/api/v1/auth/2fa/enable", {
333
+ method: "POST",
334
+ body: JSON.stringify({ code })
335
+ });
336
+ }
337
+ async disable2fa(password) {
338
+ await this.request("/api/v1/auth/2fa/disable", {
339
+ method: "POST",
340
+ body: JSON.stringify({ password })
341
+ });
342
+ }
343
+ async regenerateBackupCodes() {
344
+ return this.request("/api/v1/auth/2fa/backup-codes", { method: "POST" });
345
+ }
346
+ async challenge2fa(request) {
347
+ return this.request("/api/v1/auth/2fa/challenge", {
348
+ method: "POST",
349
+ body: JSON.stringify(request)
350
+ });
351
+ }
352
+ // ============ Users ============
353
+ async createUser(request) {
354
+ const response = await this.request(
355
+ "/api/v1/users/create",
356
+ {
357
+ method: "POST",
358
+ body: JSON.stringify(request)
359
+ }
360
+ );
361
+ return { userId: response.user_id, email: response.email, name: response.name, createdAt: response.created_at };
362
+ }
414
363
  async listUsers() {
415
364
  const response = await this.request("/api/v1/users/list");
416
365
  return response.users.map((u) => ({
417
366
  userId: u.user_id,
418
367
  email: u.email,
368
+ name: u.name,
419
369
  createdAt: u.created_at
420
370
  }));
421
371
  }
422
- /**
423
- * Delete a user
424
- */
372
+ async getUser(userId) {
373
+ const response = await this.request(
374
+ `/api/v1/users/${encodeURIComponent(userId)}`
375
+ );
376
+ return { userId: response.user_id, email: response.email, name: response.name, createdAt: response.created_at };
377
+ }
425
378
  async deleteUser(userId) {
426
379
  await this.request("/api/v1/users/delete", {
427
380
  method: "POST",
428
381
  body: JSON.stringify({ user_id: userId })
429
382
  });
430
383
  }
431
- /**
432
- * Update user password
433
- */
384
+ async deleteUserById(userId) {
385
+ await this.request(`/api/v1/users/${encodeURIComponent(userId)}`, { method: "DELETE" });
386
+ }
434
387
  async updatePassword(userId, oldPassword, newPassword) {
435
388
  await this.request("/api/v1/users/password", {
436
389
  method: "POST",
437
- body: JSON.stringify({
438
- user_id: userId,
439
- old_password: oldPassword,
440
- new_password: newPassword
441
- })
390
+ body: JSON.stringify({ user_id: userId, old_password: oldPassword, new_password: newPassword })
442
391
  });
443
392
  }
444
- // ============ Scope Management ============
445
- /**
446
- * List custom scopes for the service
447
- */
448
- async listScopes() {
449
- const response = await this.request("/api/v1/scopes/list");
450
- return response.scopes;
393
+ // ============ Roles ============
394
+ async createRole(request) {
395
+ const response = await this.request(
396
+ "/api/v1/users/roles/create",
397
+ { method: "POST", body: JSON.stringify(request) }
398
+ );
399
+ return { roleId: response.role_id, name: response.name, description: response.description, createdAt: response.created_at };
400
+ }
401
+ async listRoles() {
402
+ const response = await this.request("/api/v1/users/roles/list");
403
+ return response.roles.map((r) => ({
404
+ roleId: r.role_id,
405
+ name: r.name,
406
+ description: r.description,
407
+ createdAt: r.created_at
408
+ }));
451
409
  }
452
- /**
453
- * Create a custom scope
454
- */
455
- async createScope(request) {
456
- const response = await this.request("/api/v1/scopes/create", {
410
+ async deleteRole(roleId) {
411
+ await this.request(`/api/v1/users/roles/${encodeURIComponent(roleId)}`, { method: "DELETE" });
412
+ }
413
+ async assignRole(userId, roleId) {
414
+ await this.request("/api/v1/users/roles/assign", {
457
415
  method: "POST",
458
- body: JSON.stringify(request)
416
+ body: JSON.stringify({ user_id: userId, role_id: roleId })
459
417
  });
460
- return {
461
- scopeId: response.scope_id,
462
- name: response.name,
463
- key: response.key,
464
- description: response.description,
465
- createdAt: response.created_at
466
- };
467
418
  }
468
- /**
469
- * Delete a custom scope
470
- */
471
- async deleteScope(scopeId) {
472
- await this.request("/api/v1/scopes/delete", {
419
+ async unassignRole(userId, roleId) {
420
+ await this.request("/api/v1/users/roles/unassign", {
473
421
  method: "POST",
474
- body: JSON.stringify({ scope_id: scopeId })
422
+ body: JSON.stringify({ user_id: userId, role_id: roleId })
475
423
  });
476
424
  }
477
- // ============ API Key Management ============
478
- /**
479
- * List all API keys for the service
480
- */
425
+ async getUserRoles(userId) {
426
+ const response = await this.request(
427
+ `/api/v1/users/${encodeURIComponent(userId)}/roles`
428
+ );
429
+ return response.roles.map((r) => ({
430
+ roleId: r.role_id,
431
+ name: r.name,
432
+ description: r.description,
433
+ createdAt: r.created_at
434
+ }));
435
+ }
436
+ // ============ API Keys ============
481
437
  async listApiKeys() {
482
438
  const response = await this.request("/api/v1/keys/list");
483
439
  return response.keys.map((k) => ({
484
440
  keyId: k.keyId,
485
441
  name: k.name,
486
442
  scope: k.scope,
487
- scopes: [],
443
+ scopes: k.scopes,
488
444
  keyType: k.keyType,
445
+ tpsLimit: k.tpsLimit,
489
446
  revoked: k.revoked,
490
- createdAt: k.createdAt,
491
- tpsLimit: k.tpsLimit
447
+ createdAt: k.createdAt
492
448
  }));
493
449
  }
494
- /**
495
- * Create a new API key with optional custom scopes
496
- */
497
450
  async createApiKey(request) {
498
- const response = await this.request("/api/v1/keys/create", {
451
+ const response = await this.request(
452
+ "/api/v1/keys/create",
453
+ {
454
+ method: "POST",
455
+ body: JSON.stringify({
456
+ user_id: request.userId,
457
+ name: request.name,
458
+ scope: request.scope || "service",
459
+ scopes: request.scopes,
460
+ key_type: request.keyType || "service",
461
+ tps_limit: request.tpsLimit || 100
462
+ })
463
+ }
464
+ );
465
+ return { keyId: response.key_id, apiKey: response.api_key, name: response.name, scope: response.scope, scopes: response.scopes };
466
+ }
467
+ async revokeApiKey(keyId) {
468
+ await this.request("/api/v1/keys/revoke", {
469
+ method: "POST",
470
+ body: JSON.stringify({ key_id: keyId })
471
+ });
472
+ }
473
+ // ============ Scopes ============
474
+ async listScopes() {
475
+ const response = await this.request("/api/v1/scopes/list");
476
+ return response.scopes;
477
+ }
478
+ async createScope(request) {
479
+ const response = await this.request(
480
+ "/api/v1/scopes/create",
481
+ { method: "POST", body: JSON.stringify(request) }
482
+ );
483
+ return { scopeId: response.scope_id, name: response.name, key: response.key, description: response.description, createdAt: response.created_at };
484
+ }
485
+ async deleteScope(scopeId) {
486
+ await this.request("/api/v1/scopes/delete", {
487
+ method: "POST",
488
+ body: JSON.stringify({ scope_id: scopeId })
489
+ });
490
+ }
491
+ // ============ Introspection & Validation ============
492
+ async introspect(request) {
493
+ if (this.cache && request.token) {
494
+ const cached = this.cache.get(request.token);
495
+ if (cached) return cached;
496
+ }
497
+ const result = await this.request("/api/v1/introspect", {
499
498
  method: "POST",
500
499
  body: JSON.stringify({
501
- user_id: request.userId,
502
- name: request.name,
503
- scope: request.scope || "service",
504
- scopes: request.scopes,
505
- key_type: request.keyType || "service",
506
- tps_limit: request.tpsLimit || 100
500
+ serviceId: request.serviceId,
501
+ token: request.token,
502
+ apiKey: request.apiKey,
503
+ requiredScopes: request.requiredScopes
507
504
  })
508
505
  });
509
- return {
510
- keyId: response.key_id,
511
- apiKey: response.api_key,
512
- name: response.name,
513
- scope: response.scope,
514
- scopes: response.scopes
515
- };
506
+ if (this.cache && request.token) {
507
+ this.cache.set(request.token, result);
508
+ }
509
+ return result;
516
510
  }
517
- /**
518
- * Revoke an API key
519
- */
520
- async revokeApiKey(keyId) {
521
- await this.request("/api/v1/keys/revoke", {
511
+ async validate() {
512
+ const cfg = this.config;
513
+ const token = await this.storage.getToken();
514
+ if (!token) return { valid: false, error: "No token stored" };
515
+ if (!cfg.serviceId) return { valid: false, error: "Service ID not configured" };
516
+ if (!cfg.apiKey && !cfg.clientSecret) return { valid: false, error: "Service credentials not configured" };
517
+ try {
518
+ const result = await this.introspect({ serviceId: cfg.serviceId, token });
519
+ return {
520
+ valid: result.valid,
521
+ type: result.type === "bearer" ? "token" : "api_key",
522
+ scopes: result.scopes,
523
+ userId: result.userId,
524
+ email: result.email,
525
+ error: result.error
526
+ };
527
+ } catch (error) {
528
+ if (error instanceof ScopeError) {
529
+ return { valid: false, error: error.message, scopes: error.providedScopes };
530
+ }
531
+ if (error instanceof AuthMiError && error.statusCode === 401) {
532
+ return { valid: false, error: "Invalid or expired token" };
533
+ }
534
+ throw error;
535
+ }
536
+ }
537
+ async validateWithScopes(requiredScopes) {
538
+ const cfg = this.config;
539
+ const token = await this.storage.getToken();
540
+ if (!token) return { valid: false, error: "No token provided" };
541
+ if (!cfg.apiKey && !cfg.clientSecret) return { valid: false, error: "Service credentials not configured" };
542
+ if (!cfg.serviceId) return { valid: false, error: "Service ID not configured" };
543
+ try {
544
+ const result = await this.introspect({ serviceId: cfg.serviceId, token, requiredScopes });
545
+ return {
546
+ valid: result.valid,
547
+ type: result.type === "bearer" ? "token" : "api_key",
548
+ scopes: result.scopes,
549
+ userId: result.userId,
550
+ email: result.email,
551
+ error: result.error
552
+ };
553
+ } catch (error) {
554
+ if (error instanceof ScopeError) {
555
+ return { valid: false, error: error.message, scopes: error.providedScopes };
556
+ }
557
+ if (error instanceof AuthMiError && error.statusCode === 401) {
558
+ return { valid: false, error: "Invalid or expired token" };
559
+ }
560
+ throw error;
561
+ }
562
+ }
563
+ async requireScopes(...requiredScopes) {
564
+ const result = await this.validateWithScopes(requiredScopes);
565
+ if (!result.valid) {
566
+ throw new AuthMiError(result.error || "Authentication required", "UNAUTHORIZED", 401);
567
+ }
568
+ const hasScopes = requiredScopes.every((s) => result.scopes?.includes(s));
569
+ if (!hasScopes) {
570
+ throw new ScopeError(`Required: ${requiredScopes.join(", ")}`, requiredScopes, result.scopes || []);
571
+ }
572
+ return result;
573
+ }
574
+ async withScope(scopes, fn) {
575
+ await this.requireScopes(...scopes);
576
+ return fn();
577
+ }
578
+ // ============ Webhooks ============
579
+ async listWebhooks() {
580
+ const response = await this.request("/api/v1/webhooks/list");
581
+ return response.webhooks;
582
+ }
583
+ async registerWebhook(request) {
584
+ const response = await this.request(
585
+ "/api/v1/webhooks/register",
586
+ { method: "POST", body: JSON.stringify(request) }
587
+ );
588
+ return { webhookId: response.webhook_id, url: response.url, events: response.events, active: response.active, createdAt: response.created_at };
589
+ }
590
+ async deleteWebhook(webhookId) {
591
+ await this.request("/api/v1/webhooks/delete", {
522
592
  method: "POST",
523
- body: JSON.stringify({ key_id: keyId })
593
+ body: JSON.stringify({ webhook_id: webhookId })
524
594
  });
525
595
  }
526
- // ============ Service Management ============
527
- /**
528
- * Onboard a new service (requires master admin key)
529
- */
596
+ // ============ Billing ============
597
+ async listPlans() {
598
+ const response = await this.request("/api/v1/billing/plans");
599
+ return response.plans;
600
+ }
601
+ async getSubscription() {
602
+ return this.request("/api/v1/billing/subscription");
603
+ }
604
+ async createCheckout(priceId) {
605
+ return this.request("/api/v1/billing/checkout", {
606
+ method: "POST",
607
+ body: JSON.stringify({ price_id: priceId })
608
+ });
609
+ }
610
+ async createPortalSession() {
611
+ return this.request("/api/v1/billing/portal", { method: "POST" });
612
+ }
613
+ // ============ Audit ============
614
+ async getAuditLogs(query) {
615
+ const params = new URLSearchParams();
616
+ if (query?.limit) params.set("limit", String(query.limit));
617
+ if (query?.offset) params.set("offset", String(query.offset));
618
+ if (query?.actorId) params.set("actor_id", query.actorId);
619
+ if (query?.action) params.set("action", query.action);
620
+ if (query?.resourceType) params.set("resource_type", query.resourceType);
621
+ if (query?.startDate) params.set("start_date", query.startDate);
622
+ if (query?.endDate) params.set("end_date", query.endDate);
623
+ const qs = params.toString();
624
+ const response = await this.request(
625
+ `/api/v1/audit-logs${qs ? `?${qs}` : ""}`
626
+ );
627
+ return response.logs;
628
+ }
629
+ // ============ Services ============
630
+ async getService(serviceId) {
631
+ return this.request(`/api/v1/services/${encodeURIComponent(serviceId)}`);
632
+ }
633
+ async updateService(serviceId, data) {
634
+ return this.request(`/api/v1/services/${encodeURIComponent(serviceId)}`, {
635
+ method: "POST",
636
+ body: JSON.stringify(data)
637
+ });
638
+ }
639
+ async rotateClientSecret(serviceId) {
640
+ return this.request(
641
+ `/api/v1/services/${encodeURIComponent(serviceId)}/rotate-secret`,
642
+ { method: "POST" }
643
+ );
644
+ }
645
+ // ============ JWKS ============
646
+ async getJwks(serviceId) {
647
+ return this.request(`/.well-known/jwks.json?service_id=${encodeURIComponent(serviceId)}`);
648
+ }
649
+ // ============ Onboarding (admin only) ============
530
650
  async onboardService(request, masterAdminKey) {
531
651
  const response = await this.request("/api/v1/services/onboard", {
532
652
  method: "POST",
@@ -547,9 +667,6 @@ var AuthMiClient = class {
547
667
  createdAt: response.created_at
548
668
  };
549
669
  }
550
- /**
551
- * Delete a service (requires master admin key)
552
- */
553
670
  async deleteService(serviceId, masterAdminKey) {
554
671
  await this.request("/api/v1/services/deboard", {
555
672
  method: "POST",
@@ -559,49 +676,127 @@ var AuthMiClient = class {
559
676
  body: JSON.stringify({ service_id: serviceId })
560
677
  });
561
678
  }
562
- // ============ Webhook Management ============
563
- /**
564
- * List webhooks
565
- */
566
- async listWebhooks() {
567
- const response = await this.request(
568
- "/api/v1/webhooks/list"
679
+ };
680
+
681
+ // src/storage/cookie.ts
682
+ var CookieTokenStorage = class _CookieTokenStorage {
683
+ /**
684
+ * @param cookieStr The raw `Cookie` header value (server-side) or `document.cookie` (browser).
685
+ * Pass an empty string in environments where cookies are managed via Set-Cookie.
686
+ */
687
+ constructor(cookieStr = "", options = {}) {
688
+ this.cookieStr = cookieStr;
689
+ this.options = {
690
+ name: "authmi_token",
691
+ httpOnly: true,
692
+ secure: typeof process !== "undefined" && process.env?.NODE_ENV === "production",
693
+ sameSite: "lax",
694
+ domain: "",
695
+ path: "/",
696
+ maxAge: 86400,
697
+ ...options
698
+ };
699
+ }
700
+ /** Update the raw cookie string (e.g. per-request in server middleware) */
701
+ setCookieString(cookieStr) {
702
+ this.cookieStr = cookieStr;
703
+ }
704
+ /** Escape special regex characters in a string */
705
+ static escapeRegex(s) {
706
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
707
+ }
708
+ getToken() {
709
+ const name = _CookieTokenStorage.escapeRegex(this.options.name);
710
+ const match = this.cookieStr.match(
711
+ new RegExp(`(?:^|;\\s*)${name}=([^;]*)`)
569
712
  );
570
- return response.webhooks;
713
+ return match ? decodeURIComponent(match[1]) : null;
571
714
  }
572
- /**
573
- * Register a webhook
574
- */
575
- async registerWebhook(request) {
576
- const response = await this.request("/api/v1/webhooks/register", {
577
- method: "POST",
578
- body: JSON.stringify(request)
579
- });
580
- return {
581
- webhookId: response.webhook_id,
582
- url: response.url,
583
- events: response.events,
584
- active: response.active,
585
- createdAt: response.created_at
586
- };
715
+ setToken(token) {
716
+ const { name, httpOnly, secure, sameSite, domain, path, maxAge } = this.options;
717
+ let cookie = `${name}=${encodeURIComponent(token)}; path=${path}; max-age=${maxAge}`;
718
+ if (httpOnly) cookie += "; HttpOnly";
719
+ if (secure) cookie += "; Secure";
720
+ if (sameSite) cookie += `; SameSite=${sameSite}`;
721
+ if (domain) cookie += `; domain=${domain}`;
722
+ this.lastSetCookie = cookie;
723
+ this.cookieStr = `${name}=${encodeURIComponent(token)}`;
724
+ }
725
+ /** The last Set-Cookie header generated by setToken() or removeToken() */
726
+ getSetCookieHeader() {
727
+ return this.lastSetCookie;
587
728
  }
588
- /**
589
- * Delete a webhook
590
- */
591
- async deleteWebhook(webhookId) {
592
- await this.request("/api/v1/webhooks/delete", {
593
- method: "POST",
594
- body: JSON.stringify({ webhook_id: webhookId })
729
+ removeToken() {
730
+ const { name, domain, path } = this.options;
731
+ let cookie = `${name}=; path=${path}; max-age=0`;
732
+ if (domain) cookie += `; domain=${domain}`;
733
+ this.lastSetCookie = cookie;
734
+ this.cookieStr = "";
735
+ }
736
+ };
737
+
738
+ // src/storage/memory.ts
739
+ var MemoryTokenStorage = class {
740
+ constructor() {
741
+ this.token = null;
742
+ }
743
+ getToken() {
744
+ return this.token;
745
+ }
746
+ setToken(token) {
747
+ this.token = token;
748
+ }
749
+ removeToken() {
750
+ this.token = null;
751
+ }
752
+ };
753
+
754
+ // src/cache.ts
755
+ var IntrospectCache = class {
756
+ constructor(ttlMs = 6e4) {
757
+ this.store = /* @__PURE__ */ new Map();
758
+ this.defaultTtlMs = ttlMs;
759
+ }
760
+ /** Build a cache key from the token (simple hash) */
761
+ key(token) {
762
+ let hash = 0;
763
+ for (let i = 0; i < token.length; i++) {
764
+ hash = hash * 31 + token.charCodeAt(i) | 0;
765
+ }
766
+ return `tok:${hash.toString(36)}`;
767
+ }
768
+ get(token) {
769
+ const entry = this.store.get(this.key(token));
770
+ if (!entry) return void 0;
771
+ if (Date.now() > entry.expiresAt) {
772
+ this.store.delete(this.key(token));
773
+ return void 0;
774
+ }
775
+ return entry.result;
776
+ }
777
+ set(token, result, ttlMs) {
778
+ if (!result.valid) return;
779
+ this.store.set(this.key(token), {
780
+ result,
781
+ expiresAt: Date.now() + (ttlMs ?? this.defaultTtlMs)
595
782
  });
596
783
  }
784
+ invalidate(token) {
785
+ this.store.delete(this.key(token));
786
+ }
787
+ clear() {
788
+ this.store.clear();
789
+ }
597
790
  };
598
791
 
599
792
  // src/index.ts
600
- var VERSION = "1.0.0";
793
+ var VERSION = "1.1.0";
601
794
  // Annotate the CommonJS export names for ESM import in node:
602
795
  0 && (module.exports = {
603
796
  AuthMiClient,
604
797
  AuthMiError,
798
+ CookieTokenStorage,
799
+ IntrospectCache,
605
800
  LocalStorageTokenStorage,
606
801
  MemoryTokenStorage,
607
802
  ScopeError,