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