@meistrari/auth-core 1.35.0 → 1.37.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.mjs CHANGED
@@ -8,7 +8,20 @@ import { defaultStatements } from 'better-auth/plugins/organization/access';
8
8
  import z, { z as z$1 } from 'zod';
9
9
  export { APIError } from 'better-auth';
10
10
 
11
- const version = "1.35.0";
11
+ function isTokenExpired(token, expiresInMs) {
12
+ const payload = JSON.parse(atob(token.split(".")[1] ?? ""));
13
+ if (!payload.exp)
14
+ return false;
15
+ const nowInSeconds = Date.now() / 1e3;
16
+ const expirationTime = payload.exp;
17
+ if (expiresInMs !== void 0) {
18
+ const futureTimeInSeconds = nowInSeconds + expiresInMs / 1e3;
19
+ return futureTimeInSeconds >= expirationTime;
20
+ }
21
+ return nowInSeconds > expirationTime;
22
+ }
23
+
24
+ const version = "1.37.0";
12
25
 
13
26
  function parsedEnumValues(schema) {
14
27
  return Object.fromEntries(
@@ -187,15 +200,17 @@ const JWTPayload = z$1.object({
187
200
  });
188
201
 
189
202
  const DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
203
+ const CLIENT_CREDENTIALS_GRANT = "client_credentials";
190
204
  function normalizeOrganizationsResponse(response) {
191
205
  if (Array.isArray(response)) {
192
206
  return {
193
207
  data: response
194
208
  };
195
209
  }
210
+ const candidate = response;
196
211
  return {
197
- ...response,
198
- data: Array.isArray(response.data) ? response.data : []
212
+ ...candidate,
213
+ data: Array.isArray(candidate.data) ? candidate.data : []
199
214
  };
200
215
  }
201
216
  function applicationsPluginClient() {
@@ -273,6 +288,17 @@ function applicationsPluginClient() {
273
288
  }
274
289
  });
275
290
  },
291
+ exchangeClientCredentialsForToken: async (credentials) => {
292
+ return await $fetch("/applications/token", {
293
+ method: "POST",
294
+ body: {
295
+ grantType: CLIENT_CREDENTIALS_GRANT,
296
+ clientId: credentials.clientId,
297
+ clientSecret: credentials.clientSecret,
298
+ organizationId: credentials.organizationId
299
+ }
300
+ });
301
+ },
276
302
  completeAuthorizationFlow: async (code, codeVerifier) => {
277
303
  return await $fetch("/applications/token", {
278
304
  method: "POST",
@@ -408,12 +434,23 @@ function createAPIClient(apiUrl, fetchOptions = {}) {
408
434
  });
409
435
  }
410
436
 
411
- async function readErrorMessage(response) {
437
+ class ApplicationRouteError extends Error {
438
+ code;
439
+ details;
440
+ status;
441
+ constructor(response, body) {
442
+ super(body?.error ?? body?.message ?? response.statusText);
443
+ this.name = "ApplicationRouteError";
444
+ this.status = response.status;
445
+ this.code = body?.code;
446
+ this.details = body?.details;
447
+ }
448
+ }
449
+ async function readErrorBody(response) {
412
450
  try {
413
- const body = await response.json();
414
- return body.error ?? body.message ?? response.statusText;
451
+ return await response.json();
415
452
  } catch {
416
- return response.statusText;
453
+ return null;
417
454
  }
418
455
  }
419
456
  class AdminService {
@@ -459,7 +496,7 @@ class AdminService {
459
496
  ...request.body !== void 0 ? { body: JSON.stringify(request.body) } : {}
460
497
  });
461
498
  if (!response.ok)
462
- throw new Error(await readErrorMessage(response));
499
+ throw new ApplicationRouteError(response, await readErrorBody(response));
463
500
  return await response.json();
464
501
  }
465
502
  /**
@@ -546,6 +583,53 @@ class AdminService {
546
583
  body: options
547
584
  });
548
585
  }
586
+ /**
587
+ * Lists client secrets for an application.
588
+ *
589
+ * @param applicationId - ID of the application to inspect
590
+ * @returns Secret metadata without plaintext values
591
+ */
592
+ async listApplicationSecrets(applicationId) {
593
+ return await this.requestApplicationRoute(`applications/${encodeURIComponent(applicationId)}/secrets`);
594
+ }
595
+ /**
596
+ * Creates a client secret for an application.
597
+ *
598
+ * @param applicationId - ID of the application to update
599
+ * @param options - Optional secret display name
600
+ * @returns The created secret, including the plaintext value shown only once
601
+ */
602
+ async createApplicationSecret(applicationId, options = {}) {
603
+ return await this.requestApplicationRoute(`applications/${encodeURIComponent(applicationId)}/secrets`, {
604
+ method: "POST",
605
+ body: options
606
+ });
607
+ }
608
+ /**
609
+ * Deletes one client secret for an application.
610
+ *
611
+ * @param applicationId - ID of the application to update
612
+ * @param secretId - ID of the secret to delete
613
+ * @returns The deleted secret metadata
614
+ */
615
+ async deleteApplicationSecret(applicationId, secretId) {
616
+ return await this.requestApplicationRoute(
617
+ `applications/${encodeURIComponent(applicationId)}/secrets/${encodeURIComponent(secretId)}`,
618
+ { method: "DELETE" }
619
+ );
620
+ }
621
+ /**
622
+ * Retires an application's service-account identity.
623
+ *
624
+ * @param applicationId - ID of the application to update
625
+ * @returns The updated application
626
+ */
627
+ async retireApplicationServiceAccount(applicationId) {
628
+ return await this.requestApplicationRoute(
629
+ `applications/${encodeURIComponent(applicationId)}/service-account`,
630
+ { method: "DELETE" }
631
+ );
632
+ }
549
633
  /**
550
634
  * Lists all organizations, optionally with member and application previews
551
635
  * and totals when a preview size is requested.
@@ -1359,6 +1443,22 @@ class ApplicationService {
1359
1443
  throwDeviceGrantError(error);
1360
1444
  }
1361
1445
  }
1446
+ /**
1447
+ * Exchanges application client credentials for a service-account access token.
1448
+ *
1449
+ * The returned token is scoped to the requested organization and cannot be
1450
+ * refreshed. Exchange the credentials again when a new token is needed.
1451
+ *
1452
+ * @param credentials - Application credentials and target organization
1453
+ * @returns A short-lived access token with service-account and organization details
1454
+ */
1455
+ async exchangeClientCredentialsForToken(credentials) {
1456
+ const response = await this.client.applications.exchangeClientCredentialsForToken(credentials);
1457
+ if (!response.data) {
1458
+ throw new Error("No data returned from the API", { cause: response.error });
1459
+ }
1460
+ return response.data;
1461
+ }
1362
1462
  /**
1363
1463
  * Completes an authorization flow for a specific application.
1364
1464
  *
@@ -1472,14 +1572,10 @@ class OrganizationService {
1472
1572
  });
1473
1573
  return organization;
1474
1574
  }
1475
- /**
1476
- * Lists all organizations accessible to the current user.
1477
- *
1478
- * @returns An array of organizations
1479
- */
1480
- async listOrganizations() {
1481
- const organizations = await this.client.organization.list();
1482
- return organizations;
1575
+ async listOrganizations(options = {}) {
1576
+ return await this.client.organization.list({
1577
+ query: options.include?.length ? { include: options.include.join(",") } : void 0
1578
+ });
1483
1579
  }
1484
1580
  /**
1485
1581
  * Sets the active organization for the current user session.
@@ -2162,18 +2258,128 @@ class AuthClient {
2162
2258
  }
2163
2259
  }
2164
2260
 
2165
- function isTokenExpired(token, expiresInMs) {
2166
- const payload = JSON.parse(atob(token.split(".")[1] ?? ""));
2167
- if (!payload.exp)
2168
- return false;
2169
- const nowInSeconds = Date.now() / 1e3;
2170
- const expirationTime = payload.exp;
2171
- if (expiresInMs !== void 0) {
2172
- const futureTimeInSeconds = nowInSeconds + expiresInMs / 1e3;
2173
- return futureTimeInSeconds >= expirationTime;
2261
+ const TOKEN_REFRESH_WINDOW_MS = 6e4;
2262
+ const DEFAULT_INITIAL_BACKOFF_MS = 100;
2263
+ const DEFAULT_MAX_ATTEMPTS = 3;
2264
+ const DEFAULT_MAX_ORGANIZATIONS = 300;
2265
+ function validateMaxAttempts(maxAttempts) {
2266
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
2267
+ throw new RangeError("maxAttempts must be a positive integer");
2268
+ }
2269
+ }
2270
+ function validateInitialBackoff(initialBackoffMs) {
2271
+ if (!Number.isFinite(initialBackoffMs) || initialBackoffMs < 0) {
2272
+ throw new RangeError("initialBackoffMs must be a non-negative finite number");
2273
+ }
2274
+ }
2275
+ function getErrorStatus(error) {
2276
+ const candidates = [error];
2277
+ const visited = /* @__PURE__ */ new Set();
2278
+ while (candidates.length > 0) {
2279
+ const candidate = candidates.shift();
2280
+ if (!candidate || typeof candidate !== "object" || visited.has(candidate))
2281
+ continue;
2282
+ visited.add(candidate);
2283
+ const errorCandidate = candidate;
2284
+ if (typeof errorCandidate.status === "number")
2285
+ return errorCandidate.status;
2286
+ candidates.push(errorCandidate.cause, errorCandidate.error);
2287
+ }
2288
+ return void 0;
2289
+ }
2290
+ function isTransientError(error) {
2291
+ const status = getErrorStatus(error);
2292
+ return status === void 0 || status === 429 || status >= 500 && status < 600;
2293
+ }
2294
+ class ApplicationTokenStorage {
2295
+ auth;
2296
+ clientId;
2297
+ clientSecret;
2298
+ initialBackoffMs;
2299
+ inFlight = /* @__PURE__ */ new Map();
2300
+ maxAttempts;
2301
+ maxOrganizations;
2302
+ tokens = /* @__PURE__ */ new Map();
2303
+ /** Creates an application token cache for one client-credentials identity. */
2304
+ constructor({
2305
+ authApiUrl,
2306
+ clientId,
2307
+ clientSecret,
2308
+ initialBackoffMs = DEFAULT_INITIAL_BACKOFF_MS,
2309
+ maxAttempts = DEFAULT_MAX_ATTEMPTS,
2310
+ maxOrganizations = DEFAULT_MAX_ORGANIZATIONS
2311
+ }) {
2312
+ if (!Number.isInteger(maxOrganizations) || maxOrganizations < 1) {
2313
+ throw new RangeError("maxOrganizations must be a positive integer");
2314
+ }
2315
+ validateMaxAttempts(maxAttempts);
2316
+ validateInitialBackoff(initialBackoffMs);
2317
+ this.auth = new AuthClient(authApiUrl);
2318
+ this.clientId = clientId;
2319
+ this.clientSecret = clientSecret;
2320
+ this.initialBackoffMs = initialBackoffMs;
2321
+ this.maxAttempts = maxAttempts;
2322
+ this.maxOrganizations = maxOrganizations;
2323
+ }
2324
+ /** Returns a fresh access token scoped to the requested organization. */
2325
+ async getToken(organizationId, {
2326
+ initialBackoffMs = this.initialBackoffMs,
2327
+ maxAttempts = this.maxAttempts
2328
+ } = {}) {
2329
+ validateMaxAttempts(maxAttempts);
2330
+ validateInitialBackoff(initialBackoffMs);
2331
+ const cachedToken = this.tokens.get(organizationId);
2332
+ if (cachedToken && !isTokenExpired(cachedToken, TOKEN_REFRESH_WINDOW_MS)) {
2333
+ this.tokens.delete(organizationId);
2334
+ this.tokens.set(organizationId, cachedToken);
2335
+ return cachedToken;
2336
+ }
2337
+ const pendingExchange = this.inFlight.get(organizationId);
2338
+ if (pendingExchange) {
2339
+ return await pendingExchange;
2340
+ }
2341
+ const exchange = this.exchangeTokenWithRetry(organizationId, maxAttempts, initialBackoffMs).then((token) => {
2342
+ this.tokens.delete(organizationId);
2343
+ this.tokens.set(organizationId, token);
2344
+ if (this.tokens.size > this.maxOrganizations) {
2345
+ const leastRecentlyUsedOrganization = this.tokens.keys().next().value;
2346
+ if (leastRecentlyUsedOrganization !== void 0) {
2347
+ this.tokens.delete(leastRecentlyUsedOrganization);
2348
+ }
2349
+ }
2350
+ return token;
2351
+ }).finally(() => this.inFlight.delete(organizationId));
2352
+ this.inFlight.set(organizationId, exchange);
2353
+ return await exchange;
2354
+ }
2355
+ async exchangeTokenWithRetry(organizationId, maxAttempts, initialBackoffMs) {
2356
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
2357
+ try {
2358
+ return await this.exchangeToken(organizationId);
2359
+ } catch (error) {
2360
+ if (attempt === maxAttempts || !isTransientError(error))
2361
+ throw error;
2362
+ const exponentialDelay = initialBackoffMs * 2 ** (attempt - 1);
2363
+ await this.wait(Math.random() * exponentialDelay);
2364
+ }
2365
+ }
2366
+ throw new Error("Client credentials exchange exhausted without a result");
2367
+ }
2368
+ /** Performs the client-credentials exchange for one organization. */
2369
+ async exchangeToken(organizationId) {
2370
+ const { accessToken } = await this.auth.application.exchangeClientCredentialsForToken({
2371
+ clientId: this.clientId,
2372
+ clientSecret: this.clientSecret,
2373
+ organizationId
2374
+ });
2375
+ return accessToken;
2376
+ }
2377
+ /** Waits before the next exchange attempt. */
2378
+ async wait(delayMs) {
2379
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
2174
2380
  }
2175
- return nowInSeconds > expirationTime;
2176
2381
  }
2382
+
2177
2383
  async function validateToken(token, apiUrl) {
2178
2384
  if (isTokenExpired(token)) {
2179
2385
  return false;
@@ -2195,4 +2401,4 @@ function extractTokenPayload(token) {
2195
2401
  return payload;
2196
2402
  }
2197
2403
 
2198
- export { AAL, AALBrand, AALs, ACR, ACRBrand, ACRs, AMR, AMRBrand, AMRs, ApiKeyMetadata, ApplicationConfig, ApplicationError, ApplicationSessionAssuranceConfig, ApplicationSessionAssuranceError, AuthClient, AuthMethod, AuthMethodBrand, AuthMethods, AuthorizationFlowError, DEFAULT_APPLICATION_CONFIG, DEFAULT_JWT_ASSURANCE, DEFAULT_SESSION_ASSURANCE_CONFIG, DeviceAccessDeniedError, DeviceAuthorizationPendingError, DeviceAuthorizationSlowDownError, DeviceCodeExpiredError, DeviceTransientServerError, EmailOTPService, EmailRequired, InvalidCallbackURL, InvalidSocialProvider, JWTPayload, JWTPayloadAssurance, JWTPayloadUser, JWTPayloadWorkspace, OAuthStepUpProvider, RefreshTokenExpiredError, Roles, SENSITIVE_ACTION_ASSURANCE_POLICY, STEP_UP_MAX_AGE_SECONDS, SessionAssurancePolicy, SessionAssuranceRequiredError, SessionAssuranceRequiredPayload, SessionAssuranceService, StepUpOption, UserNotLoggedInError, ac, assuranceFields, canStepUpWithAmr, extractTokenPayload, getAalRank, invitationAdditionalFields, isAssuranceSufficientForPolicy, isSessionAssuranceSufficient, isTokenExpired, memberAdditionalFields, mergeStepUpAssurance, organizationAdditionalFields, parseSessionAssuranceRequiredPayload, rolesAccessControl, sessionAssurancePluginClient, userAdditionalFields, validateToken };
2404
+ export { AAL, AALBrand, AALs, ACR, ACRBrand, ACRs, AMR, AMRBrand, AMRs, ApiKeyMetadata, ApplicationConfig, ApplicationError, ApplicationSessionAssuranceConfig, ApplicationSessionAssuranceError, ApplicationTokenStorage, AuthClient, AuthMethod, AuthMethodBrand, AuthMethods, AuthorizationFlowError, DEFAULT_APPLICATION_CONFIG, DEFAULT_JWT_ASSURANCE, DEFAULT_SESSION_ASSURANCE_CONFIG, DeviceAccessDeniedError, DeviceAuthorizationPendingError, DeviceAuthorizationSlowDownError, DeviceCodeExpiredError, DeviceTransientServerError, EmailOTPService, EmailRequired, InvalidCallbackURL, InvalidSocialProvider, JWTPayload, JWTPayloadAssurance, JWTPayloadUser, JWTPayloadWorkspace, OAuthStepUpProvider, RefreshTokenExpiredError, Roles, SENSITIVE_ACTION_ASSURANCE_POLICY, STEP_UP_MAX_AGE_SECONDS, SessionAssurancePolicy, SessionAssuranceRequiredError, SessionAssuranceRequiredPayload, SessionAssuranceService, StepUpOption, UserNotLoggedInError, ac, assuranceFields, canStepUpWithAmr, extractTokenPayload, getAalRank, invitationAdditionalFields, isAssuranceSufficientForPolicy, isSessionAssuranceSufficient, isTokenExpired, memberAdditionalFields, mergeStepUpAssurance, organizationAdditionalFields, parseSessionAssuranceRequiredPayload, rolesAccessControl, sessionAssurancePluginClient, userAdditionalFields, validateToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/auth-core",
3
- "version": "1.35.0",
3
+ "version": "1.37.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {