@better-auth/oauth-provider 1.7.0-rc.5 → 1.7.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.
@@ -1,1557 +0,0 @@
1
- import { i as validatePublicClientJwks, r as isPrivateHostname } from "./client-assertion-Cy8ZcH7P.mjs";
2
- import { I as getSupportedAuthMethods, L as getSupportedGrantTypes, S as storeClientSecret, h as parseClientMetadata, m as parseBearerToken, z as isExtensionTokenEndpointAuthMethod } from "./utils-jZEA4KQb.mjs";
3
- import { isLoopbackIP } from "@better-auth/core/utils/host";
4
- import { APIError, NO_STORE_HEADERS, getSessionFromCtx } from "better-auth/api";
5
- import { generateRandomString } from "better-auth/crypto";
6
- import { APIError as APIError$1 } from "better-call";
7
- import { logger } from "@better-auth/core/env";
8
- import { getCurrentAdapter, runWithTransaction } from "@better-auth/core/context";
9
- import { toExpJWT } from "better-auth/plugins";
10
- import { isReverseDomainPrivateUseRedirectUri } from "@better-auth/core/utils/redirect-uri";
11
- //#region src/resources.ts
12
- /**
13
- * Source-of-truth list of asymmetric JWS algorithms supported by the JWT
14
- * plugin. Mirrors the {@link JWSAlgorithms} literal-union type from
15
- * `packages/better-auth/src/plugins/jwt/types.ts`. Kept as a runtime const
16
- * so the admin-CRUD zod schema AND the seed-config validator can reject
17
- * bad values up-front rather than surfacing opaque jose errors at issuance.
18
- *
19
- * MUST stay in sync with `JWSAlgorithms`. The type-level guard immediately
20
- * below fails the typecheck if the two drift.
21
- */
22
- const JWS_ALGORITHMS = [
23
- "EdDSA",
24
- "ES256",
25
- "ES512",
26
- "PS256",
27
- "RS256"
28
- ];
29
- const JWS_ALGORITHM_SET = new Set(JWS_ALGORITHMS);
30
- /**
31
- * Upper bound on how many entries are accepted in a JWT's `aud` claim during
32
- * introspection / revocation validation. Without a cap, a hostile or replayed
33
- * token can supply hundreds of fake resource identifiers and amplify load on
34
- * the resource table (one DB lookup per unique unrecognized entry). Real-world
35
- * deployments use single-digit resource lists; 64 is a generous ceiling that
36
- * stays well clear of any legitimate use and bounds the per-request DB fan-out.
37
- *
38
- * RFC 7519 §4.1.3 does not specify a maximum, so this is a defensive limit,
39
- * not a spec one. Tokens that exceed it are treated as invalid (introspect →
40
- * `active: false`, revoke → no-op) rather than triggering the lookup path.
41
- *
42
- * @internal
43
- */
44
- const MAX_AUD_VALUES = 64;
45
- /**
46
- * Validates a resource `identifier` per RFC 8707 §2 (absolute URI, no
47
- * fragment), honoring {@link OAuthOptions.identifierValidator} when provided.
48
- *
49
- * Returns a structured result so callers can decide whether to throw
50
- * (admin CRUD / DCR) or warn-and-skip (config seed path).
51
- *
52
- * @internal
53
- */
54
- async function checkIdentifier(opts, identifier) {
55
- const customValidator = opts.identifierValidator;
56
- if (customValidator) {
57
- if (!await customValidator(identifier)) return {
58
- ok: false,
59
- reason: `resource identifier ${identifier} failed validation`
60
- };
61
- return { ok: true };
62
- }
63
- let url;
64
- try {
65
- url = new URL(identifier);
66
- } catch {
67
- return {
68
- ok: false,
69
- reason: `resource identifier ${identifier} must be an absolute URI (RFC 8707 §2)`
70
- };
71
- }
72
- if (url.hash) return {
73
- ok: false,
74
- reason: `resource identifier ${identifier} must not contain a URI fragment (RFC 8707 §2)`
75
- };
76
- return { ok: true };
77
- }
78
- /**
79
- * Variant of {@link checkIdentifier} that throws `invalid_target` on failure.
80
- * Used by admin CRUD endpoints where validation failure is a client error.
81
- *
82
- * @internal
83
- */
84
- async function assertIdentifierValid(opts, identifier) {
85
- const result = await checkIdentifier(opts, identifier);
86
- if (!result.ok) throw new APIError("BAD_REQUEST", {
87
- error: "invalid_target",
88
- error_description: result.reason
89
- });
90
- }
91
- /**
92
- * The OIDC userinfo endpoint's resource identifier. Always accepted as an
93
- * implicit `aud` value when `openid` is in scope — not looked up against
94
- * `oauthResource` rows.
95
- */
96
- const userInfoResource = (baseURL) => `${baseURL}/oauth2/userinfo`;
97
- /**
98
- * Re-parses a request's raw `application/x-www-form-urlencoded` body to
99
- * recover every value of the repeated `resource` parameter (RFC 8707 §2).
100
- *
101
- * Workaround for better-call's form-body parser (better-call ≤1.3.5), which
102
- * collapses repeated form keys with last-write-wins. A client sending
103
- * `resource=https://a&resource=https://b` would otherwise arrive in the
104
- * handler as `{ resource: "https://b" }`, silently narrowing the issued
105
- * token's `aud` to a single resource.
106
- *
107
- * Returns the full ordered list when the body is form-encoded and contains
108
- * any `resource` entries; `undefined` otherwise. Caller MUST enable
109
- * `cloneRequest: true` on the endpoint — the body stream is read here a
110
- * second time, and that only works when better-call cloned the request
111
- * before its own parse.
112
- *
113
- * Note: the URL-encoded body path is the only one affected. The query-string
114
- * path (e.g. `/oauth2/authorize?resource=…&resource=…`) is parsed by the
115
- * router itself, which DOES array-promote duplicate keys correctly.
116
- *
117
- * @internal
118
- */
119
- async function extractRepeatedResourceFromForm(request) {
120
- if (!(request.headers.get("content-type")?.toLowerCase() ?? "").includes("application/x-www-form-urlencoded")) return;
121
- let text;
122
- try {
123
- text = await request.clone().text();
124
- } catch {
125
- return;
126
- }
127
- if (!text) return void 0;
128
- const values = new URLSearchParams(text).getAll("resource").filter((value) => value.length > 0);
129
- return values.length > 0 ? values : void 0;
130
- }
131
- /**
132
- * Normalizes a `resource` parameter into an array. Accepts the RFC 8707
133
- * forms: a single string, a string[], or undefined.
134
- *
135
- * @internal
136
- */
137
- function normalizeResourceParam(resource) {
138
- if (resource === void 0 || resource === null) return void 0;
139
- if (typeof resource === "string") return [resource];
140
- if (Array.isArray(resource)) {
141
- const result = resource.filter((r) => typeof r === "string" && r.length > 0);
142
- return result.length > 0 ? result : void 0;
143
- }
144
- }
145
- /**
146
- * Validates a JWT `aud` claim against the OAuth resource model.
147
- *
148
- * Every non-implicit audience value must resolve to an `oauthResource` row.
149
- * Disabled rows still pass because disabling blocks new issuance; deleting the
150
- * row is the operation that invalidates already-issued tokens.
151
- *
152
- * @internal
153
- */
154
- async function isAudienceClaimAllowed(ctx, opts, audienceClaim, implicitAudiences = []) {
155
- if (audienceClaim === void 0) return true;
156
- const audienceValues = Array.isArray(audienceClaim) ? audienceClaim : [audienceClaim];
157
- if (audienceValues.length > MAX_AUD_VALUES) return false;
158
- const implicitAudienceSet = new Set(implicitAudiences);
159
- const resourcesToLookup = /* @__PURE__ */ new Set();
160
- for (const audienceValue of audienceValues) {
161
- if (implicitAudienceSet.has(audienceValue)) continue;
162
- resourcesToLookup.add(audienceValue);
163
- }
164
- if (resourcesToLookup.size === 0) return true;
165
- return (await Promise.all(Array.from(resourcesToLookup, (resource) => getResource(ctx, opts, resource)))).every(Boolean);
166
- }
167
- /**
168
- * Resolves the resource policy for a request.
169
- *
170
- * Validation steps (RFC 8707 §3 + per-resource policy):
171
- *
172
- * 1. Resolve `resource` parameter to a list of identifiers.
173
- * 2. For each identifier, look up the {@link OAuthResource} row.
174
- * - Missing → `invalid_target`.
175
- * - `disabled` → `invalid_target` (no new issuance).
176
- * 3. If {@link OAuthOptions.enforcePerClientResources} resolves to true,
177
- * confirm the client is linked to every requested resource via
178
- * `oauthClientResource`. Unlinked → `invalid_target`.
179
- * 4. Intersect `requestedScopes` with each resource's `allowedScopes`.
180
- * Empty intersection → `invalid_scope`.
181
- * 5. Pick the minimum `accessTokenTtl` across requested resources.
182
- * 6. Pick signing config — only honored for single-resource requests
183
- * (a JWT can only have one signature).
184
- * 7. Merge per-resource `customClaims` (returned raw; reserved-claim
185
- * stripping is owned by `resolveAccessTokenClaims`).
186
- *
187
- * When no `resource` param is present, returns an empty policy: no `aud`
188
- * claim and no resource-specific overrides.
189
- *
190
- * @internal
191
- */
192
- async function resolveResourcePolicy(ctx, opts, params) {
193
- const requestedResources = normalizeResourceParam(params.resource);
194
- const includesOpenid = params.requestedScopes.includes("openid");
195
- const baseURL = ctx.context.baseURL ?? "";
196
- const userInfoResourceIdentifier = userInfoResource(baseURL);
197
- if (!requestedResources) return {
198
- audienceClaim: void 0,
199
- accessTokenTtl: null,
200
- refreshTokenTtl: null,
201
- signingAlgorithm: null,
202
- signingKeyId: null,
203
- rawCustomClaims: {},
204
- dpopBoundAccessTokensRequired: false,
205
- effectiveScopes: [...params.requestedScopes]
206
- };
207
- const uniqueRequestedResources = [...new Set(requestedResources)];
208
- const resolved = [];
209
- for (const identifier of uniqueRequestedResources) {
210
- if (identifier === userInfoResourceIdentifier) continue;
211
- const row = await getResource(ctx, opts, identifier);
212
- if (row) {
213
- if (row.disabled) throw new APIError("BAD_REQUEST", {
214
- error: "invalid_target",
215
- error_description: `requested resource ${identifier} is disabled`
216
- });
217
- resolved.push(row);
218
- continue;
219
- }
220
- throw new APIError("BAD_REQUEST", {
221
- error: "invalid_target",
222
- error_description: `requested resource ${identifier} is not configured`
223
- });
224
- }
225
- const { value: enforcePerClient } = resolveEnforcePerClientResources(opts);
226
- if (enforcePerClient && resolved.length > 0) await assertClientLinkedToResources(ctx, opts, params.clientId, resolved);
227
- let effectiveScopes = [...params.requestedScopes];
228
- for (const row of resolved) {
229
- if (row.allowedScopes === null || row.allowedScopes === void 0) continue;
230
- const allowed = new Set(row.allowedScopes);
231
- const intersection = effectiveScopes.filter((s) => allowed.has(s));
232
- if (intersection.length === 0) throw new APIError("BAD_REQUEST", {
233
- error: "invalid_scope",
234
- error_description: `none of the requested scopes are allowed for resource ${row.identifier}`
235
- });
236
- effectiveScopes = intersection;
237
- }
238
- let accessTokenTtl = null;
239
- let refreshTokenTtl = null;
240
- for (const row of resolved) {
241
- if (row.accessTokenTtl != null) accessTokenTtl = accessTokenTtl === null ? row.accessTokenTtl : Math.min(accessTokenTtl, row.accessTokenTtl);
242
- if (row.refreshTokenTtl != null) refreshTokenTtl = refreshTokenTtl === null ? row.refreshTokenTtl : Math.min(refreshTokenTtl, row.refreshTokenTtl);
243
- }
244
- const uniqueSigningAlgs = /* @__PURE__ */ new Set();
245
- const uniqueSigningKids = /* @__PURE__ */ new Set();
246
- for (const row of resolved) {
247
- if (row.signingAlgorithm) uniqueSigningAlgs.add(row.signingAlgorithm);
248
- if (row.signingKeyId) uniqueSigningKids.add(row.signingKeyId);
249
- }
250
- if (uniqueSigningAlgs.size > 1) throw new APIError("BAD_REQUEST", {
251
- error: "invalid_request",
252
- error_description: "multi-resource request has conflicting signingAlgorithm pins; a single JWS signature cannot satisfy multiple algorithms"
253
- });
254
- if (uniqueSigningKids.size > 1) throw new APIError("BAD_REQUEST", {
255
- error: "invalid_request",
256
- error_description: "multi-resource request has conflicting signingKeyId pins; a single JWS signature cannot satisfy multiple key ids"
257
- });
258
- const signingAlgorithm = uniqueSigningAlgs.values().next().value ?? null;
259
- const signingKeyId = uniqueSigningKids.values().next().value ?? null;
260
- const mergedClaims = {};
261
- for (const row of resolved) if (row.customClaims && typeof row.customClaims === "object") Object.assign(mergedClaims, row.customClaims);
262
- const dpopBoundAccessTokensRequired = resolved.some((row) => row.dpopBoundAccessTokensRequired === true);
263
- const audienceIdentifiers = includesOpenid ? [...uniqueRequestedResources, userInfoResourceIdentifier] : uniqueRequestedResources;
264
- const audClaim = [...new Set(audienceIdentifiers)];
265
- return {
266
- audienceClaim: audClaim.length === 1 ? audClaim[0] : audClaim,
267
- accessTokenTtl,
268
- refreshTokenTtl,
269
- signingAlgorithm,
270
- signingKeyId,
271
- rawCustomClaims: mergedClaims,
272
- dpopBoundAccessTokensRequired,
273
- effectiveScopes
274
- };
275
- }
276
- /**
277
- * Throws `invalid_target` if the client isn't linked to every resource via
278
- * the `oauthClientResource` join table. Issues one `findMany` per call.
279
- *
280
- * @internal
281
- */
282
- async function assertClientLinkedToResources(ctx, opts, clientId, resources) {
283
- if (resources.length === 0) return;
284
- const modelName = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
285
- const links = await ctx.context.adapter.findMany({
286
- model: modelName,
287
- where: [{
288
- field: "clientId",
289
- value: clientId
290
- }]
291
- });
292
- const linkedSet = new Set(links?.map((l) => l.resourceId) ?? []);
293
- const unlinked = resources.filter((resource) => !linkedSet.has(resource.identifier));
294
- if (unlinked.length > 0) throw new APIError("BAD_REQUEST", {
295
- error: "invalid_target",
296
- error_description: `client ${clientId} is not linked to resource(s) ${unlinked.map((resource) => resource.identifier).join(", ")}`
297
- });
298
- }
299
- /**
300
- * Returns `true` when `clientId` is linked, via the `oauthClientResource` join
301
- * table, to at least one of `resourceIdentifiers`. Authorizes cross-client token
302
- * introspection (RFC 7662 §2.1/§4): a resource server may introspect a token
303
- * whose audience it serves, even when a different client issued the token.
304
- *
305
- * @internal
306
- */
307
- async function isClientLinkedToAnyResource(ctx, opts, clientId, resourceIdentifiers) {
308
- if (resourceIdentifiers.length === 0) return false;
309
- const modelName = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
310
- return ((await ctx.context.adapter.findMany({
311
- model: modelName,
312
- where: [{
313
- field: "clientId",
314
- value: clientId
315
- }, {
316
- field: "resourceId",
317
- operator: "in",
318
- value: resourceIdentifiers
319
- }],
320
- limit: 1
321
- }))?.length ?? 0) > 0;
322
- }
323
- /**
324
- * Merges `customClaims` from the existing rows of `resourceIdentifiers` in
325
- * order (later resources win on collision). Missing rows are skipped; disabled
326
- * rows are included, since an already-issued token keeps its claims until it
327
- * expires. Returned raw; `resolveAccessTokenClaims` strips reserved names.
328
- *
329
- * Unlike {@link resolveResourcePolicy}, this applies none of the new-issuance
330
- * gates (disabled, scope allowlist, client linkage), so it is the right way to
331
- * re-derive claims for an already-issued token at introspection.
332
- *
333
- * @internal
334
- */
335
- async function getResourceCustomClaims(ctx, opts, resourceIdentifiers) {
336
- if (resourceIdentifiers.length === 0) return {};
337
- const rows = await ctx.context.adapter.findMany({
338
- model: opts.schema?.oauthResource?.modelName ?? "oauthResource",
339
- where: [{
340
- field: "identifier",
341
- operator: "in",
342
- value: resourceIdentifiers
343
- }]
344
- });
345
- const rowByIdentifier = new Map(rows.map((row) => [row.identifier, row]));
346
- const merged = {};
347
- for (const identifier of resourceIdentifiers) {
348
- const row = rowByIdentifier.get(identifier);
349
- if (row?.customClaims && typeof row.customClaims === "object") Object.assign(merged, row.customClaims);
350
- }
351
- return merged;
352
- }
353
- /**
354
- * Resolves the effective {@link OAuthOptions.enforcePerClientResources} value.
355
- *
356
- * - Explicit `true | false` always wins (`source: "explicit"`).
357
- * - Otherwise resolve to `true` (`source: "default"`). RFC 8707 §3 per-client
358
- * validation is the secure default.
359
- *
360
- * Deterministic and pure — safe to call on every validation pass.
361
- */
362
- function resolveEnforcePerClientResources(opts) {
363
- if (opts.enforcePerClientResources !== void 0) return {
364
- value: opts.enforcePerClientResources,
365
- source: "explicit"
366
- };
367
- return {
368
- value: true,
369
- source: "default"
370
- };
371
- }
372
- /**
373
- * In-process cache of {@link OAuthResource} rows, keyed by `identifier`.
374
- *
375
- * Opt-in via {@link OAuthOptions.cachedResources} (same membership-Set pattern
376
- * as `cachedTrustedClients`). Resources outside the set are looked up from the
377
- * DB on every request — the safe default for deployments that edit rows
378
- * through external tooling.
379
- *
380
- * Module-scoped so admin CRUD handlers can invalidate from anywhere via
381
- * {@link invalidateResourceCache}.
382
- */
383
- const resourceCache = /* @__PURE__ */ new Map();
384
- /**
385
- * Removes an entry from the resource cache. Called by admin CRUD handlers
386
- * after every write. Pass no argument to clear the entire cache.
387
- *
388
- * @internal
389
- */
390
- function invalidateResourceCache(identifier) {
391
- if (identifier === void 0) {
392
- resourceCache.clear();
393
- return;
394
- }
395
- resourceCache.delete(identifier);
396
- }
397
- /**
398
- * Looks up an {@link OAuthResource} by `identifier`, consulting the in-process
399
- * cache when the identifier is in {@link OAuthOptions.cachedResources}.
400
- *
401
- * Triggers the lazy seed via {@link seedResourcesOnce} on first call — this
402
- * is the safety net for deployments where migrations run after plugin init
403
- * (so seeding at init couldn't see the table yet). The seed is idempotent
404
- * and coalesced, so the cost is paid only once per process.
405
- *
406
- * Returns a defensive copy so callers can't mutate cached state.
407
- *
408
- * @internal
409
- */
410
- async function getResource(ctx, opts, identifier) {
411
- await seedResourcesOnce(ctx.context, opts);
412
- if (opts.cachedResources?.has(identifier)) {
413
- const cached = resourceCache.get(identifier);
414
- if (cached) return Object.assign({}, cached);
415
- }
416
- const dbResource = await ctx.context.adapter.findOne({
417
- model: opts.schema?.oauthResource?.modelName ?? "oauthResource",
418
- where: [{
419
- field: "identifier",
420
- value: identifier
421
- }]
422
- });
423
- if (dbResource && opts.cachedResources?.has(identifier)) resourceCache.set(identifier, Object.assign({}, dbResource));
424
- return dbResource;
425
- }
426
- /**
427
- * Default `name` value when an input doesn't provide one. Mirrors what most
428
- * admin UIs would show.
429
- */
430
- function defaultName(input) {
431
- return input.name ?? input.identifier;
432
- }
433
- /**
434
- * Coerces seed-config `resources` entries — string or object form — into a
435
- * normalized list of {@link OAuthResourceInput}.
436
- *
437
- * @internal
438
- */
439
- function collectResourceInputs(opts) {
440
- const inputs = [];
441
- if (opts.resources?.length) for (const entry of opts.resources) inputs.push(typeof entry === "string" ? { identifier: entry } : entry);
442
- return inputs;
443
- }
444
- /**
445
- * Builds the row payload sent to the adapter for a seed insert. All policy
446
- * columns default to `null` (= inherit plugin default at issuance time)
447
- * when the input doesn't specify a value.
448
- */
449
- function buildSeedRow(input, now) {
450
- return {
451
- identifier: input.identifier,
452
- name: defaultName(input),
453
- accessTokenTtl: input.accessTokenTtl ?? null,
454
- refreshTokenTtl: input.refreshTokenTtl ?? null,
455
- signingAlgorithm: input.signingAlgorithm ?? null,
456
- signingKeyId: input.signingKeyId ?? null,
457
- allowedScopes: input.allowedScopes ?? null,
458
- customClaims: input.customClaims ?? null,
459
- dpopBoundAccessTokensRequired: input.dpopBoundAccessTokensRequired ?? false,
460
- disabled: input.disabled ?? false,
461
- policyVersion: 1,
462
- metadata: input.metadata ?? null,
463
- createdAt: now,
464
- updatedAt: now
465
- };
466
- }
467
- /**
468
- * Builds the partial update payload when re-seeding an existing row.
469
- *
470
- * - `overwrite` mode replaces every policy column with the input value
471
- * (omitted fields fall back to `null` to clear stale state).
472
- * - `merge` mode updates only fields present in the input — admin edits to
473
- * other fields are preserved.
474
- */
475
- function buildSeedUpdate(input, mode, now) {
476
- if (mode === "overwrite") {
477
- const { identifier: _identifier, createdAt: _createdAt, ...rest } = buildSeedRow(input, now);
478
- return rest;
479
- }
480
- const update = { updatedAt: now };
481
- if (input.name !== void 0) update.name = input.name;
482
- if (input.accessTokenTtl !== void 0) update.accessTokenTtl = input.accessTokenTtl;
483
- if (input.refreshTokenTtl !== void 0) update.refreshTokenTtl = input.refreshTokenTtl;
484
- if (input.signingAlgorithm !== void 0) update.signingAlgorithm = input.signingAlgorithm;
485
- if (input.signingKeyId !== void 0) update.signingKeyId = input.signingKeyId;
486
- if (input.allowedScopes !== void 0) update.allowedScopes = input.allowedScopes;
487
- if (input.customClaims !== void 0) update.customClaims = input.customClaims;
488
- if (input.dpopBoundAccessTokensRequired !== void 0) update.dpopBoundAccessTokensRequired = input.dpopBoundAccessTokensRequired;
489
- if (input.disabled !== void 0) update.disabled = input.disabled;
490
- if (input.metadata !== void 0) update.metadata = input.metadata;
491
- return update;
492
- }
493
- /**
494
- * Pattern matched against adapter errors to detect the "table not yet
495
- * created" case — i.e. migrations haven't been run. When matched, the
496
- * caller treats the seed as deferred (will retry on first resource access).
497
- *
498
- * Covers SQLite ("no such table"), Postgres ("relation X does not exist"),
499
- * and MySQL ("Table X does not exist" / contracted form).
500
- */
501
- const MISSING_TABLE_PATTERN = /no such table|relation.*does not exist|table.*does(?: not|n[''']?t) exist/i;
502
- /**
503
- * Per-adapter state for the lazy-seed path. A module-level boolean would let
504
- * one Better Auth instance suppress seeding for every later instance in the same
505
- * process. Endpoint `AuthContext` objects are not guaranteed to be stable across
506
- * requests, so keying by the adapter keeps one seed state per backing store.
507
- */
508
- let seedStates = /* @__PURE__ */ new WeakMap();
509
- function getSeedState(ctx) {
510
- const key = ctx.adapter;
511
- const existing = seedStates.get(key);
512
- if (existing) return existing;
513
- const created = {
514
- completed: false,
515
- promise: null
516
- };
517
- seedStates.set(key, created);
518
- return created;
519
- }
520
- /**
521
- * Seeds `oauthResource` rows from plugin config.
522
- *
523
- * Behavior is controlled by {@link OAuthOptions.resourceSeedMode}:
524
- *
525
- * - `"insertOnly"` (default, safe): inserts rows whose `identifier` is not
526
- * already present. Existing rows are untouched — admin edits via CRUD
527
- * are never reverted on restart.
528
- * - `"merge"`: inserts missing rows; updates only specified fields for
529
- * existing rows. Useful when config holds the "preferred defaults" but
530
- * admins customize per-row.
531
- * - `"overwrite"`: inserts missing rows; replaces every policy column on
532
- * existing rows with the config value (omitted fields → null). Use only
533
- * when config is the single source of truth.
534
- *
535
- * Race-safety: the `identifier` column carries a UNIQUE constraint, so two
536
- * processes booting simultaneously can each attempt the insert — one wins,
537
- * the other catches the constraint error and treats it as a no-op.
538
- *
539
- * Migration ordering: at plugin `init` time, tables may not exist yet
540
- * (Better Auth's test harness, and many deployment setups, run migrations
541
- * after auth construction). Seeding tolerates "no such table" errors and
542
- * defers — the lazy {@link seedResourcesOnce} path picks up the work on
543
- * the first resource access.
544
- *
545
- * Idempotent: safe to call multiple times.
546
- *
547
- * @internal
548
- */
549
- async function seedResources(ctx, opts) {
550
- const inputs = collectResourceInputs(opts);
551
- if (inputs.length === 0) return;
552
- const mode = opts.resourceSeedMode ?? "insertOnly";
553
- const modelName = opts.schema?.oauthResource?.modelName ?? "oauthResource";
554
- for (const rawInput of inputs) {
555
- const check = await checkIdentifier(opts, rawInput.identifier);
556
- if (!check.ok) {
557
- logger.warn(`oauth-provider: skipping resource seed for ${rawInput.identifier} — ${check.reason}`);
558
- continue;
559
- }
560
- let input = rawInput;
561
- if (input.signingAlgorithm != null && !JWS_ALGORITHM_SET.has(input.signingAlgorithm)) {
562
- logger.warn(`oauth-provider: dropping unsupported signingAlgorithm "${input.signingAlgorithm}" for resource ${input.identifier} — must be one of ${JWS_ALGORITHMS.join(", ")}. Continuing without an algorithm override.`);
563
- const { signingAlgorithm: _, ...rest } = input;
564
- input = rest;
565
- }
566
- let existing;
567
- try {
568
- existing = await ctx.adapter.findOne({
569
- model: modelName,
570
- where: [{
571
- field: "identifier",
572
- value: input.identifier
573
- }]
574
- });
575
- } catch (err) {
576
- const message = err instanceof Error ? err.message : String(err);
577
- if (MISSING_TABLE_PATTERN.test(message)) {
578
- logger.debug("oauth-provider: oauthResource table not yet created; deferring resource seed to first access.");
579
- return;
580
- }
581
- throw err;
582
- }
583
- const now = /* @__PURE__ */ new Date();
584
- if (!existing) {
585
- try {
586
- await ctx.adapter.create({
587
- model: modelName,
588
- data: buildSeedRow(input, now)
589
- });
590
- } catch (err) {
591
- const message = err instanceof Error ? err.message : String(err);
592
- if (/unique|duplicate|UNIQUE/i.test(message)) {
593
- logger.debug(`oauth-provider: resource ${input.identifier} already inserted by a concurrent process — skipping.`);
594
- continue;
595
- }
596
- if (MISSING_TABLE_PATTERN.test(message)) {
597
- logger.debug("oauth-provider: oauthResource table not yet created; deferring resource seed to first access.");
598
- return;
599
- }
600
- throw err;
601
- }
602
- continue;
603
- }
604
- if (mode === "insertOnly") continue;
605
- await ctx.adapter.update({
606
- model: modelName,
607
- where: [{
608
- field: "identifier",
609
- value: input.identifier
610
- }],
611
- update: buildSeedUpdate(input, mode, now)
612
- });
613
- }
614
- }
615
- /**
616
- * Idempotent, coalesced wrapper around {@link seedResources} for the
617
- * lazy-seed path. Safe to call from every resource lookup — the first call
618
- * runs the seed, concurrent calls await the same promise, and subsequent
619
- * calls become no-ops.
620
- *
621
- * The endpoint context shape (`{ context: AuthContext }`) is what
622
- * `getResource` receives, so this is a convenience overload that unwraps
623
- * for callers.
624
- *
625
- * @internal
626
- */
627
- async function seedResourcesOnce(ctx, opts) {
628
- const state = getSeedState(ctx);
629
- if (state.completed === true) return;
630
- if (state.promise !== null) return state.promise;
631
- state.promise = seedResources(ctx, opts).then(() => {
632
- state.completed = true;
633
- }).catch((err) => {
634
- state.promise = null;
635
- throw err;
636
- });
637
- return state.promise;
638
- }
639
- /**
640
- * Logs the resolved value of {@link OAuthOptions.enforcePerClientResources}
641
- * so deployment admins see which default applied at init. Separated from
642
- * {@link resolveEnforcePerClientResources} so the latter stays pure and
643
- * cheap to call from validation flow.
644
- *
645
- * @internal
646
- */
647
- function logEnforcePerClientResourcesResolution(opts) {
648
- const resolved = resolveEnforcePerClientResources(opts);
649
- logger.info(`oauth-provider: enforcePerClientResources resolved to ${resolved.value} (${resolved.source})`);
650
- }
651
- //#endregion
652
- //#region src/oauthClient/client-credentials.ts
653
- const USER_DELEGATED_SCOPES = /* @__PURE__ */ new Set([
654
- "openid",
655
- "profile",
656
- "email",
657
- "offline_access"
658
- ]);
659
- function isUserDelegatedScope(scope) {
660
- return USER_DELEGATED_SCOPES.has(scope);
661
- }
662
- function normalizeClientCredentialsScopes(scopes) {
663
- return [...new Set(scopes.map((scope) => scope.trim()).filter(Boolean))];
664
- }
665
- function validateClientCredentialsScopes(scopes, grantTypes, tokenEndpointAuthMethod, opts) {
666
- if (scopes.length === 0) return;
667
- if (!grantTypes.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
668
- error: "invalid_client_metadata",
669
- error_description: "client_credentials_scopes requires the client_credentials grant"
670
- });
671
- if (tokenEndpointAuthMethod === "none") throw new APIError("BAD_REQUEST", {
672
- error: "invalid_client_metadata",
673
- error_description: "public clients cannot be assigned client_credentials scopes"
674
- });
675
- const providerScopes = new Set(opts.scopes ?? []);
676
- const invalidScopes = scopes.filter((scope) => !providerScopes.has(scope) || isUserDelegatedScope(scope));
677
- if (invalidScopes.length > 0) throw new APIError("BAD_REQUEST", {
678
- error: "invalid_scope",
679
- error_description: `The following client_credentials scopes are invalid: ${invalidScopes.join(", ")}`
680
- });
681
- }
682
- //#endregion
683
- //#region src/client-metadata.ts
684
- const OAUTH_CLIENT_RECORD_FIELD_NAMES = Object.freeze(Object.keys({
685
- clientId: true,
686
- clientSecret: true,
687
- clientDiscoveryId: true,
688
- disabled: true,
689
- scopes: true,
690
- clientCredentialsScopes: true,
691
- userId: true,
692
- createdAt: true,
693
- updatedAt: true,
694
- expiresAt: true,
695
- name: true,
696
- uri: true,
697
- icon: true,
698
- contacts: true,
699
- tos: true,
700
- policy: true,
701
- softwareId: true,
702
- softwareVersion: true,
703
- softwareStatement: true,
704
- redirectUris: true,
705
- postLogoutRedirectUris: true,
706
- backchannelLogoutUri: true,
707
- backchannelLogoutSessionRequired: true,
708
- tokenEndpointAuthMethod: true,
709
- grantTypes: true,
710
- responseTypes: true,
711
- applicationType: true,
712
- jwks: true,
713
- jwksUri: true,
714
- requirePKCE: true,
715
- dpopBoundAccessTokens: true,
716
- skipConsent: true,
717
- enableEndSession: true,
718
- subjectType: true,
719
- referenceId: true,
720
- metadata: true
721
- }));
722
- /**
723
- * Canonical wire fields emitted by schemaToOAuth. They are mapped explicitly
724
- * and therefore must never also survive inside the opaque metadata envelope.
725
- */
726
- const OAUTH_CLIENT_WIRE_FIELD_NAMES = [
727
- "client_id",
728
- "client_secret",
729
- "client_secret_expires_at",
730
- "scope",
731
- "user_id",
732
- "client_id_issued_at",
733
- "client_name",
734
- "client_uri",
735
- "logo_uri",
736
- "contacts",
737
- "tos_uri",
738
- "policy_uri",
739
- "jwks",
740
- "jwks_uri",
741
- "software_id",
742
- "software_version",
743
- "software_statement",
744
- "redirect_uris",
745
- "post_logout_redirect_uris",
746
- "backchannel_logout_uri",
747
- "backchannel_logout_session_required",
748
- "token_endpoint_auth_method",
749
- "grant_types",
750
- "response_types",
751
- "application_type",
752
- "disabled",
753
- "skip_consent",
754
- "enable_end_session",
755
- "require_pkce",
756
- "dpop_bound_access_tokens",
757
- "subject_type",
758
- "reference_id"
759
- ];
760
- const OPAQUE_METADATA_RESERVED_FIELDS = /* @__PURE__ */ new Set([
761
- ...OAUTH_CLIENT_RECORD_FIELD_NAMES,
762
- ...OAUTH_CLIENT_WIRE_FIELD_NAMES,
763
- "public",
764
- "type",
765
- "resources",
766
- "client_credentials_scopes"
767
- ]);
768
- const CIMD_FORBIDDEN_SERVER_FIELD_NAMES = /* @__PURE__ */ new Set([
769
- "disabled",
770
- "client_secret",
771
- "client_secret_expires_at",
772
- "client_id_issued_at",
773
- "skip_consent",
774
- "enable_end_session",
775
- "require_pkce",
776
- "reference_id",
777
- "user_id",
778
- "resources",
779
- "clientSecret",
780
- "clientDiscoveryId",
781
- "skipConsent",
782
- "enableEndSession",
783
- "requirePKCE",
784
- "referenceId",
785
- "userId",
786
- "clientId",
787
- "applicationType",
788
- "tokenEndpointAuthMethod",
789
- "redirectUris",
790
- "postLogoutRedirectUris",
791
- "grantTypes",
792
- "responseTypes",
793
- "scopes",
794
- "expiresAt",
795
- "createdAt",
796
- "updatedAt",
797
- "softwareId",
798
- "softwareVersion",
799
- "softwareStatement",
800
- "backchannelLogoutUri",
801
- "backchannelLogoutSessionRequired",
802
- "jwksUri",
803
- "dpopBoundAccessTokens",
804
- "subjectType"
805
- ]);
806
- function isForbiddenCimdClientMetadataField(field) {
807
- return CIMD_FORBIDDEN_SERVER_FIELD_NAMES.has(field);
808
- }
809
- function stripReservedOAuthClientMetadataExtensions(metadata) {
810
- if (!metadata) return void 0;
811
- return Object.fromEntries(Object.entries(metadata).filter(([field]) => !OPAQUE_METADATA_RESERVED_FIELDS.has(field)));
812
- }
813
- //#endregion
814
- //#region src/oauthClient/privileges.ts
815
- /**
816
- * Authorizes a client action against the configured `clientPrivileges` hook.
817
- *
818
- * This is the single authorization helper for every OAuth client mutation. The
819
- * create path enforces it at the shared creation chokepoint so that no
820
- * registration route can reach client persistence without it.
821
- *
822
- * @throws APIError UNAUTHORIZED when there is no session or the hook denies the action.
823
- * @throws APIError BAD_REQUEST when the request carries no headers.
824
- */
825
- async function assertClientPrivileges(ctx, session, opts, action) {
826
- if (!session) throw new APIError("UNAUTHORIZED");
827
- if (!ctx.headers) throw new APIError("BAD_REQUEST");
828
- if (action === "configure-client-credentials-scopes" && !opts.clientPrivileges) throw new APIError("UNAUTHORIZED");
829
- if (opts.clientPrivileges && !await opts.clientPrivileges({
830
- headers: ctx.headers,
831
- action,
832
- session: session.session,
833
- user: session.user
834
- })) throw new APIError("UNAUTHORIZED");
835
- }
836
- //#endregion
837
- //#region src/utils/initial-access-token.ts
838
- /**
839
- * Builds an RFC 6750 §3 Bearer challenge for the registration endpoint. The
840
- * `error` code maps to the status per RFC 6750 §3.1 (`invalid_request` → 400,
841
- * `invalid_token` → 401) and is echoed in both the `WWW-Authenticate` challenge
842
- * and the JSON body.
843
- *
844
- * @see https://datatracker.ietf.org/doc/html/rfc6750#section-3
845
- */
846
- function registrationBearerError(status, error, errorDescription) {
847
- return new APIError$1(status, {
848
- error,
849
- error_description: errorDescription
850
- }, {
851
- "WWW-Authenticate": `Bearer error="${error}"`,
852
- ...NO_STORE_HEADERS
853
- });
854
- }
855
- /**
856
- * Resolves an RFC 7591 initial access token from the request and authorizes it
857
- * against the deployment-supplied validator.
858
- *
859
- * Returns the authorization (optionally carrying a `referenceId`) when a valid
860
- * token is present, or `undefined` when no Bearer credentials were sent so the
861
- * caller can fall back to session or open registration. Throws an RFC 6750
862
- * Bearer challenge when the header is malformed, no validator is configured, or
863
- * the token is rejected.
864
- *
865
- * @see https://datatracker.ietf.org/doc/html/rfc7591#section-3
866
- */
867
- async function authorizeInitialAccessToken(ctx, opts, clientMetadata) {
868
- const headers = ctx.headers;
869
- let initialAccessToken;
870
- try {
871
- initialAccessToken = parseBearerToken(headers?.get("authorization"));
872
- } catch {
873
- throw registrationBearerError("BAD_REQUEST", "invalid_request", "Malformed initial access token Authorization header");
874
- }
875
- if (!initialAccessToken || !headers) return;
876
- if (!opts.validateInitialAccessToken) throw registrationBearerError("UNAUTHORIZED", "invalid_token", "Invalid initial access token");
877
- let authorization;
878
- try {
879
- authorization = await opts.validateInitialAccessToken({
880
- initialAccessToken,
881
- headers,
882
- clientMetadata
883
- });
884
- } catch (error) {
885
- if (error instanceof APIError$1) throw error;
886
- throw new APIError$1("INTERNAL_SERVER_ERROR", {
887
- error: "server_error",
888
- error_description: "Initial access token validation failed"
889
- });
890
- }
891
- if (!authorization) throw registrationBearerError("UNAUTHORIZED", "invalid_token", "Invalid initial access token");
892
- return authorization;
893
- }
894
- //#endregion
895
- //#region src/register.ts
896
- const DEFAULT_REGISTRATION_GRANT_TYPES = ["authorization_code"];
897
- function resolveClientRegistrationScopes(opts) {
898
- return [.../* @__PURE__ */ new Set([...opts.clientRegistrationDefaultScopes ?? opts.scopes ?? [], ...opts.clientRegistrationAllowedScopes ?? []])];
899
- }
900
- function resolveRegistrationGrantTypes(client) {
901
- const grantTypes = client.grant_types ?? [...DEFAULT_REGISTRATION_GRANT_TYPES];
902
- if (grantTypes.length > 0) return grantTypes;
903
- throw new APIError("BAD_REQUEST", {
904
- error: "invalid_client_metadata",
905
- error_description: "grant_types must contain at least one grant type"
906
- });
907
- }
908
- function resolveRegistrationResponseTypes(client, grantTypes) {
909
- if (client.response_types) return client.response_types;
910
- return grantTypes.includes("authorization_code") ? ["code"] : void 0;
911
- }
912
- function applyOAuthClientRegistrationDefaults(client, defaultApplicationType = "web") {
913
- const grantTypes = resolveRegistrationGrantTypes(client);
914
- return {
915
- ...client,
916
- token_endpoint_auth_method: client.token_endpoint_auth_method ?? "client_secret_basic",
917
- application_type: client.application_type ?? (defaultApplicationType === null ? void 0 : defaultApplicationType),
918
- grant_types: grantTypes,
919
- response_types: resolveRegistrationResponseTypes(client, grantTypes)
920
- };
921
- }
922
- const FORBIDDEN_NATIVE_REDIRECT_SCHEMES = /* @__PURE__ */ new Set([
923
- "file:",
924
- "ftp:",
925
- "mailto:",
926
- "javascript:",
927
- "data:",
928
- "vbscript:"
929
- ]);
930
- function invalidRedirectUri(description) {
931
- throw new APIError("BAD_REQUEST", {
932
- error: "invalid_redirect_uri",
933
- error_description: description
934
- });
935
- }
936
- function getRawHttpHostname(redirectUri) {
937
- const authority = /^http:\/\/([^/?#]*)/i.exec(redirectUri)?.[1];
938
- if (!authority) return null;
939
- const hostAndPort = authority.slice(authority.lastIndexOf("@") + 1);
940
- if (hostAndPort.startsWith("[")) {
941
- const bracketEnd = hostAndPort.indexOf("]");
942
- return bracketEnd < 0 ? null : hostAndPort.slice(0, bracketEnd + 1).toLowerCase();
943
- }
944
- return (hostAndPort.split(":")[0] ?? "").toLowerCase();
945
- }
946
- async function resolveClientRegistrationResources(ctx, opts, requestedResources) {
947
- const defaultResources = opts.clientRegistrationDefaultResources ?? [];
948
- const allowedResources = /* @__PURE__ */ new Set([...defaultResources, ...opts.clientRegistrationAllowedResources ?? []]);
949
- for (const identifier of requestedResources) if (!allowedResources.has(identifier)) throw new APIError("BAD_REQUEST", {
950
- error: "invalid_target",
951
- error_description: `requested resource ${identifier} is not allowed for client registration`
952
- });
953
- const resources = [.../* @__PURE__ */ new Set([...defaultResources, ...requestedResources])];
954
- for (const identifier of resources) {
955
- const row = await getResource(ctx, opts, identifier);
956
- if (!row) throw new APIError("BAD_REQUEST", {
957
- error: "invalid_target",
958
- error_description: `requested resource ${identifier} does not exist`
959
- });
960
- if (row.disabled) throw new APIError("BAD_REQUEST", {
961
- error: "invalid_target",
962
- error_description: `requested resource ${identifier} is disabled`
963
- });
964
- }
965
- return resources;
966
- }
967
- function validateClientRedirectUri(redirectUri, applicationType) {
968
- let url;
969
- try {
970
- url = new URL(redirectUri);
971
- } catch {
972
- invalidRedirectUri(`redirect URI must be an absolute URI: ${redirectUri}`);
973
- }
974
- if (redirectUri.includes("#") || url.username.length > 0 || url.password.length > 0) invalidRedirectUri(`redirect URI must not include credentials or a fragment: ${redirectUri}`);
975
- const isHttp = url.protocol === "http:";
976
- const isHttps = url.protocol === "https:";
977
- if (/^localhost\.+$/i.test(url.hostname)) invalidRedirectUri(`redirect URI localhost must not include trailing dots: ${redirectUri}`);
978
- const isRedirectLoopback = isLoopbackIP(url.hostname) || url.hostname === "localhost";
979
- const rawHttpHostname = getRawHttpHostname(redirectUri);
980
- const isAllowedNativeHttpLoopback = rawHttpHostname === "localhost" || rawHttpHostname === "127.0.0.1" || rawHttpHostname === "[::1]";
981
- if (applicationType === "web") {
982
- if (!isHttps || isRedirectLoopback) invalidRedirectUri(`web clients require https redirect URIs on non-loopback hosts: ${redirectUri}`);
983
- return;
984
- }
985
- if (isHttps) {
986
- if (isRedirectLoopback) invalidRedirectUri(`native clients must not use https loopback redirect URIs: ${redirectUri}`);
987
- return;
988
- }
989
- if (isHttp) {
990
- if (!isAllowedNativeHttpLoopback) invalidRedirectUri(`native clients may use http only on the exact loopback hosts localhost, 127.0.0.1, or [::1]: ${redirectUri}`);
991
- return;
992
- }
993
- if (FORBIDDEN_NATIVE_REDIRECT_SCHEMES.has(url.protocol) || !isReverseDomainPrivateUseRedirectUri(url)) invalidRedirectUri(`native private-use redirect URI schemes must be well-formed reverse-domain names, omit the naming authority, and must not use a reserved scheme: ${redirectUri}`);
994
- }
995
- function assertValidRegistrationJwks(jwks) {
996
- const result = validatePublicClientJwks(jwks);
997
- if (!result.valid) throw new APIError("BAD_REQUEST", {
998
- error: "invalid_client_metadata",
999
- error_description: result.error
1000
- });
1001
- }
1002
- async function registerEndpoint(ctx, opts) {
1003
- const body = ctx.body;
1004
- if (!opts.allowDynamicClientRegistration) throw new APIError("FORBIDDEN", {
1005
- error: "access_denied",
1006
- error_description: "Client registration is disabled"
1007
- });
1008
- const session = await getSessionFromCtx(ctx);
1009
- const tokenAuthorization = session ? void 0 : await authorizeInitialAccessToken(ctx, opts, body);
1010
- const isTokenAuthorized = Boolean(tokenAuthorization);
1011
- if (!(session || isTokenAuthorized || opts.allowUnauthenticatedClientRegistration)) throw new APIError("UNAUTHORIZED", { error_description: "Authentication required for client registration" }, {
1012
- "WWW-Authenticate": "Bearer",
1013
- ...NO_STORE_HEADERS
1014
- });
1015
- if (!session && !isTokenAuthorized) {
1016
- if (body.grant_types?.includes("client_credentials")) throw new APIError("BAD_REQUEST", {
1017
- error: "invalid_client_metadata",
1018
- error_description: "client_credentials grant requires authenticated registration"
1019
- });
1020
- }
1021
- const requestedResources = Array.isArray(body.resources) ? body.resources.filter((resource) => typeof resource === "string" && resource.length > 0) : [];
1022
- if (session) await assertClientPrivileges(ctx, session, opts, "create");
1023
- const referenceId = tokenAuthorization?.referenceId ?? (session && opts.clientReference ? await opts.clientReference({
1024
- user: session.user,
1025
- session: session.session
1026
- }) : void 0);
1027
- const response = await createOAuthClientRegistration(ctx, opts, {
1028
- metadata: body,
1029
- registrationSource: "dynamic",
1030
- userId: referenceId ? void 0 : session?.session.userId,
1031
- referenceId,
1032
- requestedResources
1033
- });
1034
- ctx.setStatus(201);
1035
- return ctx.json(response);
1036
- }
1037
- async function checkOAuthClient(client, opts, settings) {
1038
- const isClientMetadataDocument = settings?.registrationSource === "clientMetadataDocument";
1039
- const clientWithDefaults = applyOAuthClientRegistrationDefaults(client, isClientMetadataDocument ? null : "web");
1040
- const tokenEndpointAuthMethod = clientWithDefaults.token_endpoint_auth_method ?? "client_secret_basic";
1041
- if (!new Set(getSupportedAuthMethods(opts, { includeNone: true })).has(tokenEndpointAuthMethod)) throw new APIError("BAD_REQUEST", {
1042
- error: "invalid_client_metadata",
1043
- error_description: `unsupported token_endpoint_auth_method ${tokenEndpointAuthMethod}`
1044
- });
1045
- if (clientWithDefaults.dpop_bound_access_tokens !== void 0 && typeof clientWithDefaults.dpop_bound_access_tokens !== "boolean") throw new APIError("BAD_REQUEST", {
1046
- error: "invalid_client_metadata",
1047
- error_description: "dpop_bound_access_tokens must be a boolean"
1048
- });
1049
- const grantTypes = clientWithDefaults.grant_types ?? [];
1050
- const responseTypes = clientWithDefaults.response_types;
1051
- const applicationType = clientWithDefaults.application_type;
1052
- if (applicationType !== void 0 && applicationType !== "web" && applicationType !== "native") throw new APIError("BAD_REQUEST", {
1053
- error: "invalid_client_metadata",
1054
- error_description: "application_type must be web or native"
1055
- });
1056
- if (grantTypes.includes("authorization_code") && (!clientWithDefaults.redirect_uris || clientWithDefaults.redirect_uris.length === 0)) throw new APIError("BAD_REQUEST", {
1057
- error: "invalid_redirect_uri",
1058
- error_description: "Redirect URIs are required for authorization_code and implicit grant types"
1059
- });
1060
- for (const uri of clientWithDefaults.redirect_uris ?? []) validateClientRedirectUri(uri, applicationType ?? (isClientMetadataDocument ? "native" : "web"));
1061
- const supportedGrantTypes = new Set(getSupportedGrantTypes(opts));
1062
- for (const grantType of grantTypes) if (!supportedGrantTypes.has(grantType)) throw new APIError("BAD_REQUEST", {
1063
- error: "invalid_client_metadata",
1064
- error_description: `unsupported grant_type ${grantType}`
1065
- });
1066
- if (grantTypes.includes("authorization_code") && !responseTypes?.includes("code")) throw new APIError("BAD_REQUEST", {
1067
- error: "invalid_client_metadata",
1068
- error_description: "When 'authorization_code' grant type is used, 'code' response type must be included"
1069
- });
1070
- if (!grantTypes.includes("authorization_code") && responseTypes?.includes("code")) throw new APIError("BAD_REQUEST", {
1071
- error: "invalid_client_metadata",
1072
- error_description: "When 'code' response type is used, 'authorization_code' grant type must be included"
1073
- });
1074
- if (clientWithDefaults.subject_type !== void 0) {
1075
- if (clientWithDefaults.subject_type !== "public" && clientWithDefaults.subject_type !== "pairwise") throw new APIError("BAD_REQUEST", {
1076
- error: "invalid_client_metadata",
1077
- error_description: `subject_type must be "public" or "pairwise"`
1078
- });
1079
- if (clientWithDefaults.subject_type === "pairwise" && !opts.pairwiseSecret) throw new APIError("BAD_REQUEST", {
1080
- error: "invalid_client_metadata",
1081
- error_description: "pairwise subject_type requires server pairwiseSecret configuration"
1082
- });
1083
- if (clientWithDefaults.subject_type === "pairwise" && clientWithDefaults.redirect_uris && clientWithDefaults.redirect_uris.length > 1) {
1084
- if (new Set(clientWithDefaults.redirect_uris.map((uri) => new URL(uri).host)).size > 1) throw new APIError("BAD_REQUEST", {
1085
- error: "invalid_client_metadata",
1086
- error_description: "pairwise clients with redirect_uris on different hosts require a sector_identifier_uri, which is not yet supported. All redirect_uris must share the same host."
1087
- });
1088
- }
1089
- }
1090
- const requestedScopes = (clientWithDefaults?.scope)?.split(" ").filter((v) => v.length);
1091
- const validatesRegistrationMetadata = settings?.registrationSource === "dynamic" || settings?.registrationSource === "clientMetadataDocument";
1092
- const allowedScopes = validatesRegistrationMetadata ? resolveClientRegistrationScopes(opts) : opts.scopes;
1093
- if (allowedScopes) {
1094
- const validScopes = new Set(allowedScopes);
1095
- for (const requestedScope of requestedScopes ?? []) if (!validScopes?.has(requestedScope)) throw new APIError("BAD_REQUEST", {
1096
- error: "invalid_scope",
1097
- error_description: `cannot request scope ${requestedScope}`
1098
- });
1099
- }
1100
- if (validatesRegistrationMetadata && clientWithDefaults.require_pkce === false) throw new APIError("BAD_REQUEST", {
1101
- error: "invalid_client_metadata",
1102
- error_description: `pkce is required for registered clients.`
1103
- });
1104
- if (clientWithDefaults.jwks || clientWithDefaults.jwks_uri) {
1105
- if (clientWithDefaults.jwks && clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
1106
- error: "invalid_client_metadata",
1107
- error_description: "jwks and jwks_uri are mutually exclusive"
1108
- });
1109
- if (clientWithDefaults.jwks_uri) try {
1110
- const uri = new URL(clientWithDefaults.jwks_uri);
1111
- if (uri.protocol !== "https:") throw new APIError("BAD_REQUEST", {
1112
- error: "invalid_client_metadata",
1113
- error_description: "jwks_uri must use HTTPS"
1114
- });
1115
- if (uri.username || uri.password) throw new APIError("BAD_REQUEST", {
1116
- error: "invalid_client_metadata",
1117
- error_description: "jwks_uri must not contain credentials"
1118
- });
1119
- if (clientWithDefaults.jwks_uri.includes("#")) throw new APIError("BAD_REQUEST", {
1120
- error: "invalid_client_metadata",
1121
- error_description: "jwks_uri must not include a fragment component"
1122
- });
1123
- if (isPrivateHostname(uri.hostname)) throw new APIError("BAD_REQUEST", {
1124
- error: "invalid_client_metadata",
1125
- error_description: "jwks_uri must not point to a private or reserved address"
1126
- });
1127
- if (settings?.ctx && !settings.ctx.context.isTrustedOrigin(uri.href)) {
1128
- const clientId = typeof clientWithDefaults.client_id === "string" ? clientWithDefaults.client_id : void 0;
1129
- if (!(isClientMetadataDocument && clientId !== void 0 && URL.canParse(clientId) && new URL(clientId).origin === uri.origin)) throw new APIError("BAD_REQUEST", {
1130
- error: "invalid_client_metadata",
1131
- error_description: "jwks_uri must belong to a trusted origin or the Client ID Metadata Document origin"
1132
- });
1133
- }
1134
- } catch (e) {
1135
- if (e instanceof APIError) throw e;
1136
- throw new APIError("BAD_REQUEST", {
1137
- error: "invalid_client_metadata",
1138
- error_description: "jwks_uri must be a valid URL"
1139
- });
1140
- }
1141
- if (clientWithDefaults.jwks) assertValidRegistrationJwks(clientWithDefaults.jwks);
1142
- }
1143
- if (tokenEndpointAuthMethod === "private_key_jwt" && !clientWithDefaults.jwks && !clientWithDefaults.jwks_uri) throw new APIError("BAD_REQUEST", {
1144
- error: "invalid_client_metadata",
1145
- error_description: "private_key_jwt requires either jwks or jwks_uri"
1146
- });
1147
- if (clientWithDefaults.backchannel_logout_uri !== void 0) {
1148
- if (opts.disableJwtPlugin) throw new APIError("BAD_REQUEST", {
1149
- error: "invalid_client_metadata",
1150
- error_description: "backchannel_logout_uri requires the jwt plugin (disableJwtPlugin must be false)"
1151
- });
1152
- let url;
1153
- try {
1154
- url = new URL(clientWithDefaults.backchannel_logout_uri);
1155
- } catch {
1156
- throw new APIError("BAD_REQUEST", {
1157
- error: "invalid_client_metadata",
1158
- error_description: "backchannel_logout_uri must be an absolute URL"
1159
- });
1160
- }
1161
- if (clientWithDefaults.backchannel_logout_uri.includes("#")) throw new APIError("BAD_REQUEST", {
1162
- error: "invalid_client_metadata",
1163
- error_description: "backchannel_logout_uri must not include a fragment component"
1164
- });
1165
- if (url.protocol !== "https:") throw new APIError("BAD_REQUEST", {
1166
- error: "invalid_client_metadata",
1167
- error_description: "backchannel_logout_uri must use https"
1168
- });
1169
- if (url.username || url.password) throw new APIError("BAD_REQUEST", {
1170
- error: "invalid_client_metadata",
1171
- error_description: "backchannel_logout_uri must not contain credentials"
1172
- });
1173
- if (isPrivateHostname(url.hostname)) throw new APIError("BAD_REQUEST", {
1174
- error: "invalid_client_metadata",
1175
- error_description: "backchannel_logout_uri must not point to a private or reserved address"
1176
- });
1177
- }
1178
- }
1179
- const CLIENT_REGISTRATION_COLLISION = Symbol("client-registration-collision");
1180
- function isClientRegistrationCollision(error) {
1181
- if (!error || typeof error !== "object") return false;
1182
- const candidate = error;
1183
- return candidate[CLIENT_REGISTRATION_COLLISION] === true && (candidate.kind === "oauth-client-row-unique" || candidate.kind === "oauth-client-resource-link-unique") && "cause" in candidate;
1184
- }
1185
- /**
1186
- * Creates and persists one canonical OAuth client registration.
1187
- *
1188
- * Authorization and ownership resolution belong to endpoint adapters. This
1189
- * operation owns metadata normalization, validation, credentials, persistence,
1190
- * and resource links so canonical creation paths share one transactional
1191
- * boundary.
1192
- */
1193
- async function persistOAuthClientRegistration(ctx, opts, input) {
1194
- const body = applyOAuthClientRegistrationDefaults(input.registrationSource === "dynamic" && !input.metadata.scope ? {
1195
- ...input.metadata,
1196
- scope: (opts.clientRegistrationDefaultScopes ?? opts.scopes)?.join(" ")
1197
- } : input.metadata, input.registrationSource === "clientMetadataDocument" ? null : "web");
1198
- const isPublic = body.token_endpoint_auth_method === "none";
1199
- const isPrivateKeyJwt = body.token_endpoint_auth_method === "private_key_jwt";
1200
- const isExtensionAuthMethod = isExtensionTokenEndpointAuthMethod(opts, body.token_endpoint_auth_method);
1201
- await checkOAuthClient(body, opts, {
1202
- registrationSource: input.registrationSource,
1203
- ctx
1204
- });
1205
- const clientId = input.clientId ?? opts.generateClientId?.() ?? generateRandomString(32, "a-z", "A-Z");
1206
- const clientSecret = isPublic || isPrivateKeyJwt || isExtensionAuthMethod ? void 0 : opts.generateClientSecret?.() || generateRandomString(32, "a-z", "A-Z");
1207
- const storedClientSecret = clientSecret ? await storeClientSecret(ctx, opts, clientSecret) : void 0;
1208
- const isPKCEOptionalForRegisteredClient = input.registrationSource === "dynamic" && !isPublic && opts.clientRegistrationRequirePKCE === false;
1209
- const requirePKCE = body.require_pkce ?? (isPKCEOptionalForRegisteredClient ? false : void 0);
1210
- const iat = Math.floor(Date.now() / 1e3);
1211
- const { resources: _requestedResources, ...persistableBody } = input.registrationSource === "dynamic" || input.registrationSource === "clientMetadataDocument" ? {
1212
- ...body,
1213
- scope: resolveClientRegistrationScopes(opts).join(" ")
1214
- } : body;
1215
- const owner = input.registrationSource === "clientMetadataDocument" ? void 0 : {
1216
- userId: input.userId,
1217
- referenceId: input.referenceId
1218
- };
1219
- const schema = oauthToSchema({
1220
- ...persistableBody,
1221
- redirect_uris: body.redirect_uris ?? [],
1222
- disabled: void 0,
1223
- client_secret_expires_at: storedClientSecret ? input.registrationSource === "dynamic" && opts.clientRegistrationClientSecretExpiration ? toExpJWT(opts.clientRegistrationClientSecretExpiration, iat) : 0 : void 0,
1224
- client_id: clientId,
1225
- client_secret: storedClientSecret,
1226
- client_id_issued_at: iat,
1227
- require_pkce: requirePKCE,
1228
- user_id: owner?.referenceId ? void 0 : owner?.userId,
1229
- reference_id: owner?.referenceId
1230
- });
1231
- schema.clientCredentialsScopes = input.registrationSource === "clientMetadataDocument" ? input.existingClient?.clientCredentialsScopes ?? [] : input.registrationSource === "managed" ? input.clientCredentialsScopes ?? [] : [];
1232
- schema.clientDiscoveryId = input.registrationSource === "clientMetadataDocument" ? input.clientDiscoveryId : null;
1233
- if (input.registrationSource === "clientMetadataDocument" && body.application_type === void 0) schema.applicationType = null;
1234
- const resources = await resolveClientRegistrationResources(ctx, opts, input.registrationSource === "dynamic" ? input.requestedResources ?? [] : []);
1235
- const clientModel = opts.schema?.oauthClient?.modelName ?? "oauthClient";
1236
- const clientResourceModel = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
1237
- const existingClient = input.registrationSource === "clientMetadataDocument" ? input.existingClient : void 0;
1238
- const clientDiscoveryId = input.registrationSource === "clientMetadataDocument" ? input.clientDiscoveryId : null;
1239
- const persistenceResult = await runWithTransaction(ctx.context.adapter, async () => {
1240
- const adapter = await getCurrentAdapter(ctx.context.adapter);
1241
- let storedClient;
1242
- let created = false;
1243
- if (existingClient) {
1244
- const updatedClient = await adapter.update({
1245
- model: clientModel,
1246
- where: [{
1247
- field: "clientId",
1248
- value: clientId
1249
- }, {
1250
- field: "clientDiscoveryId",
1251
- value: clientDiscoveryId
1252
- }],
1253
- update: {
1254
- ...schema,
1255
- disabled: existingClient.disabled,
1256
- skipConsent: existingClient.skipConsent,
1257
- enableEndSession: existingClient.enableEndSession,
1258
- createdAt: existingClient.createdAt,
1259
- updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
1260
- }
1261
- });
1262
- if (!updatedClient) throw new APIError("BAD_REQUEST", {
1263
- error: "invalid_client",
1264
- error_description: "client no longer exists"
1265
- });
1266
- storedClient = updatedClient;
1267
- } else {
1268
- try {
1269
- storedClient = await adapter.create({
1270
- model: clientModel,
1271
- data: {
1272
- ...schema,
1273
- createdAt: /* @__PURE__ */ new Date(iat * 1e3),
1274
- updatedAt: /* @__PURE__ */ new Date(iat * 1e3)
1275
- }
1276
- });
1277
- } catch (error) {
1278
- if (input.registrationSource === "clientMetadataDocument" && isUniqueConstraintError(error)) throw {
1279
- [CLIENT_REGISTRATION_COLLISION]: true,
1280
- kind: "oauth-client-row-unique",
1281
- clientId,
1282
- cause: error
1283
- };
1284
- throw error;
1285
- }
1286
- created = true;
1287
- }
1288
- const linkedResources = resources.length === 0 ? [] : await adapter.findMany({
1289
- model: clientResourceModel,
1290
- where: [{
1291
- field: "clientId",
1292
- value: clientId
1293
- }]
1294
- });
1295
- const linkedResourceIds = new Set(linkedResources.map((link) => link.resourceId));
1296
- const now = /* @__PURE__ */ new Date();
1297
- for (const resourceId of resources) {
1298
- if (linkedResourceIds.has(resourceId)) continue;
1299
- try {
1300
- await adapter.create({
1301
- model: clientResourceModel,
1302
- data: {
1303
- clientId,
1304
- resourceId,
1305
- createdAt: now
1306
- }
1307
- });
1308
- } catch (error) {
1309
- if (input.registrationSource === "clientMetadataDocument" && isUniqueConstraintError(error)) throw {
1310
- [CLIENT_REGISTRATION_COLLISION]: true,
1311
- kind: "oauth-client-resource-link-unique",
1312
- clientId,
1313
- resourceId,
1314
- cause: error
1315
- };
1316
- throw error;
1317
- }
1318
- }
1319
- return {
1320
- client: storedClient,
1321
- created
1322
- };
1323
- });
1324
- return {
1325
- client: persistenceResult.client,
1326
- clientSecret,
1327
- resources,
1328
- created: persistenceResult.created
1329
- };
1330
- }
1331
- function createOAuthClientRegistrationResponse(result, opts) {
1332
- const responseBody = schemaToOAuth({
1333
- ...result.client,
1334
- clientSecret: result.clientSecret ? (opts.prefix?.clientSecret ?? "") + result.clientSecret : void 0
1335
- });
1336
- if (result.resources.length > 0) responseBody.resources = result.resources;
1337
- return responseBody;
1338
- }
1339
- function createOAuthClientAdministrativeResponse(result, opts) {
1340
- return {
1341
- ...createOAuthClientRegistrationResponse(result, opts),
1342
- client_credentials_scopes: [...result.client.clientCredentialsScopes ?? []]
1343
- };
1344
- }
1345
- /**
1346
- * Creates and persists one canonical OAuth client registration.
1347
- */
1348
- async function createOAuthClientRegistration(ctx, opts, input) {
1349
- return createOAuthClientRegistrationResponse(await persistOAuthClientRegistration(ctx, opts, input), opts);
1350
- }
1351
- async function createOAuthClientAdministrativeRegistration(ctx, opts, input) {
1352
- return createOAuthClientAdministrativeResponse(await persistOAuthClientRegistration(ctx, opts, input), opts);
1353
- }
1354
- function assertClientDiscoveryOwnership(client, clientDiscoveryId) {
1355
- if (!client || client.clientDiscoveryId === clientDiscoveryId) return;
1356
- throw new APIError("BAD_REQUEST", {
1357
- error: "invalid_client",
1358
- error_description: "client_id is already owned by a different registration source"
1359
- });
1360
- }
1361
- /**
1362
- * First-party persistence seam for a validated Client ID Metadata Document.
1363
- *
1364
- * This operation owns both initial creation and atomic replacement. It never
1365
- * generates a client secret, preserves operator-controlled flags on
1366
- * replacement, and ensures server-default resource links in the same
1367
- * transaction.
1368
- *
1369
- * @internal
1370
- */
1371
- async function registerClientMetadataDocument(ctx, opts, input) {
1372
- assertClientDiscoveryOwnership(input.existingClient, input.clientDiscoveryId);
1373
- if (input.metadata.backchannel_logout_uri !== void 0 || input.metadata.backchannel_logout_session_required !== void 0) throw new APIError("BAD_REQUEST", {
1374
- error: "invalid_client_metadata",
1375
- error_description: "Client ID Metadata Documents cannot register a back-channel logout target"
1376
- });
1377
- if ((input.metadata.token_endpoint_auth_method ?? "none") !== "none" && input.metadata.token_endpoint_auth_method !== "private_key_jwt") throw new APIError("BAD_REQUEST", {
1378
- error: "invalid_client_metadata",
1379
- error_description: "Client ID Metadata Document clients cannot use a shared secret"
1380
- });
1381
- const registrationInput = {
1382
- ...input,
1383
- metadata: {
1384
- ...input.metadata,
1385
- token_endpoint_auth_method: input.metadata.token_endpoint_auth_method ?? "none",
1386
- client_secret: void 0,
1387
- client_secret_expires_at: void 0
1388
- },
1389
- registrationSource: "clientMetadataDocument"
1390
- };
1391
- try {
1392
- return await persistOAuthClientRegistration(ctx, opts, registrationInput);
1393
- } catch (error) {
1394
- if (!isClientRegistrationCollision(error)) throw error;
1395
- const clientModel = opts.schema?.oauthClient?.modelName ?? "oauthClient";
1396
- const clientResourceModel = opts.schema?.oauthClientResource?.modelName ?? "oauthClientResource";
1397
- const currentClient = await ctx.context.adapter.findOne({
1398
- model: clientModel,
1399
- where: [{
1400
- field: "clientId",
1401
- value: input.clientId
1402
- }]
1403
- });
1404
- if (error.clientId !== input.clientId || !currentClient || currentClient.clientDiscoveryId !== input.clientDiscoveryId) throw error.cause;
1405
- if (error.kind === "oauth-client-resource-link-unique") {
1406
- if (!await ctx.context.adapter.findOne({
1407
- model: clientResourceModel,
1408
- where: [{
1409
- field: "clientId",
1410
- value: error.clientId
1411
- }, {
1412
- field: "resourceId",
1413
- value: error.resourceId
1414
- }]
1415
- })) throw error.cause;
1416
- }
1417
- try {
1418
- return await persistOAuthClientRegistration(ctx, opts, {
1419
- ...registrationInput,
1420
- existingClient: currentClient
1421
- });
1422
- } catch (retryError) {
1423
- if (isClientRegistrationCollision(retryError)) throw retryError.cause;
1424
- throw retryError;
1425
- }
1426
- }
1427
- }
1428
- function isUniqueConstraintError(error) {
1429
- if (!error || typeof error !== "object") return false;
1430
- const candidate = error;
1431
- const code = String(candidate.code ?? candidate.errno ?? "");
1432
- if ([
1433
- "1062",
1434
- "11000",
1435
- "23505",
1436
- "ER_DUP_ENTRY",
1437
- "P2002",
1438
- "SQLITE_CONSTRAINT_UNIQUE"
1439
- ].includes(code)) return true;
1440
- if (candidate.name === "MongoServerError" && candidate.code === 11e3) return true;
1441
- return typeof candidate.message === "string" && /(?:duplicate|unique constraint|unique key)/i.test(candidate.message);
1442
- }
1443
- async function createOAuthClientEndpoint(ctx, opts, settings) {
1444
- const session = await getSessionFromCtx(ctx);
1445
- await assertClientPrivileges(ctx, session, opts, "create");
1446
- if (!session) throw new APIError("UNAUTHORIZED");
1447
- const referenceId = opts.clientReference ? await opts.clientReference({
1448
- user: session.user,
1449
- session: session.session
1450
- }) : void 0;
1451
- const { client_credentials_scopes: rawClientCredentialsScopes, ...metadata } = ctx.body;
1452
- const clientCredentialsScopes = settings?.admin ? normalizeClientCredentialsScopes(rawClientCredentialsScopes ?? []) : [];
1453
- validateClientCredentialsScopes(clientCredentialsScopes, resolveRegistrationGrantTypes(metadata), metadata.token_endpoint_auth_method, opts);
1454
- if (clientCredentialsScopes.length > 0) await assertClientPrivileges(ctx, session, opts, "configure-client-credentials-scopes");
1455
- const registrationInput = {
1456
- metadata,
1457
- registrationSource: "managed",
1458
- userId: referenceId ? void 0 : session.session.userId,
1459
- referenceId,
1460
- clientCredentialsScopes
1461
- };
1462
- const responseBody = settings?.admin ? await createOAuthClientAdministrativeRegistration(ctx, opts, registrationInput) : await createOAuthClientRegistration(ctx, opts, registrationInput);
1463
- ctx.setStatus(201);
1464
- return ctx.json(responseBody);
1465
- }
1466
- function oauthToSchema(input) {
1467
- const { client_id: clientId, client_secret: clientSecret, client_secret_expires_at: _expiresAt, scope: _scope, user_id: userId, client_id_issued_at: _createdAt, client_name: name, client_uri: uri, logo_uri: icon, contacts, tos_uri: tos, policy_uri: policy, jwks: inputJwks, jwks_uri: jwksUri, software_id: softwareId, software_version: softwareVersion, software_statement: softwareStatement, redirect_uris: redirectUris, post_logout_redirect_uris: postLogoutRedirectUris, backchannel_logout_uri: backchannelLogoutUri, backchannel_logout_session_required: backchannelLogoutSessionRequired, token_endpoint_auth_method: tokenEndpointAuthMethod, grant_types: grantTypes, response_types: responseTypes, application_type: applicationType, disabled, skip_consent: skipConsent, enable_end_session: enableEndSession, require_pkce: requirePKCE, dpop_bound_access_tokens: dpopBoundAccessTokens, subject_type: subjectType, reference_id: referenceId, metadata: inputMetadata } = input;
1468
- const expiresAt = _expiresAt ? /* @__PURE__ */ new Date(Number(_expiresAt) * 1e3) : void 0;
1469
- const createdAt = _createdAt ? /* @__PURE__ */ new Date(_createdAt * 1e3) : void 0;
1470
- const scopes = _scope?.split(" ");
1471
- const metadataObj = stripReservedOAuthClientMetadataExtensions(inputMetadata ?? {});
1472
- const metadata = metadataObj && Object.keys(metadataObj).length ? JSON.stringify(metadataObj) : void 0;
1473
- return {
1474
- clientId,
1475
- clientSecret,
1476
- disabled,
1477
- scopes,
1478
- userId,
1479
- createdAt,
1480
- expiresAt,
1481
- name,
1482
- uri,
1483
- icon,
1484
- contacts,
1485
- tos,
1486
- policy,
1487
- softwareId,
1488
- softwareVersion,
1489
- softwareStatement,
1490
- redirectUris,
1491
- postLogoutRedirectUris,
1492
- backchannelLogoutUri,
1493
- backchannelLogoutSessionRequired,
1494
- tokenEndpointAuthMethod,
1495
- grantTypes,
1496
- responseTypes,
1497
- jwks: inputJwks ? JSON.stringify(inputJwks) : void 0,
1498
- jwksUri,
1499
- applicationType,
1500
- skipConsent,
1501
- enableEndSession,
1502
- requirePKCE,
1503
- dpopBoundAccessTokens,
1504
- subjectType,
1505
- referenceId,
1506
- metadata
1507
- };
1508
- }
1509
- /**
1510
- * Converts a Database Schema to an OAuth 2.0 Dynamic Client Schema
1511
- * @param input
1512
- * @param cleaned - default true, determines if the output has only Oauth 2.0 compatible data
1513
- * @returns
1514
- */
1515
- function schemaToOAuth(input) {
1516
- const { clientId, clientSecret, disabled, scopes, userId, createdAt, updatedAt: _updatedAt, expiresAt, name, uri, icon, contacts, tos, policy, softwareId, softwareVersion, softwareStatement, redirectUris, postLogoutRedirectUris, backchannelLogoutUri, backchannelLogoutSessionRequired, tokenEndpointAuthMethod, grantTypes, responseTypes, applicationType, jwks, jwksUri, skipConsent, enableEndSession, requirePKCE, dpopBoundAccessTokens, subjectType, referenceId, metadata } = input;
1517
- const _expiresAt = expiresAt ? Math.round(new Date(expiresAt).getTime() / 1e3) : void 0;
1518
- const _createdAt = createdAt ? Math.round(new Date(createdAt).getTime() / 1e3) : void 0;
1519
- const _scopes = scopes?.join(" ");
1520
- return {
1521
- ...stripReservedOAuthClientMetadataExtensions(parseClientMetadata(metadata)),
1522
- client_id: clientId,
1523
- client_secret: clientSecret ?? void 0,
1524
- client_secret_expires_at: clientSecret ? _expiresAt ?? 0 : void 0,
1525
- scope: _scopes ?? void 0,
1526
- user_id: userId ?? void 0,
1527
- client_id_issued_at: _createdAt ?? void 0,
1528
- client_name: name ?? void 0,
1529
- client_uri: uri ?? void 0,
1530
- logo_uri: icon ?? void 0,
1531
- contacts: contacts ?? void 0,
1532
- tos_uri: tos ?? void 0,
1533
- policy_uri: policy ?? void 0,
1534
- jwks: jwks ? JSON.parse(jwks) : void 0,
1535
- jwks_uri: jwksUri ?? void 0,
1536
- software_id: softwareId ?? void 0,
1537
- software_version: softwareVersion ?? void 0,
1538
- software_statement: softwareStatement ?? void 0,
1539
- redirect_uris: redirectUris ?? [],
1540
- post_logout_redirect_uris: postLogoutRedirectUris ?? void 0,
1541
- backchannel_logout_uri: backchannelLogoutUri ?? void 0,
1542
- backchannel_logout_session_required: backchannelLogoutSessionRequired ?? void 0,
1543
- token_endpoint_auth_method: tokenEndpointAuthMethod ?? void 0,
1544
- grant_types: grantTypes ?? void 0,
1545
- response_types: responseTypes ?? void 0,
1546
- application_type: applicationType ?? void 0,
1547
- disabled: disabled ?? void 0,
1548
- skip_consent: skipConsent ?? void 0,
1549
- enable_end_session: enableEndSession ?? void 0,
1550
- require_pkce: requirePKCE ?? void 0,
1551
- dpop_bound_access_tokens: dpopBoundAccessTokens ?? void 0,
1552
- subject_type: subjectType ?? void 0,
1553
- reference_id: referenceId ?? void 0
1554
- };
1555
- }
1556
- //#endregion
1557
- export { isAudienceClaimAllowed as _, registerEndpoint as a, resolveResourcePolicy as b, isForbiddenCimdClientMetadataField as c, validateClientCredentialsScopes as d, JWS_ALGORITHMS as f, invalidateResourceCache as g, getResourceCustomClaims as h, registerClientMetadataDocument as i, isUserDelegatedScope as l, extractRepeatedResourceFromForm as m, createOAuthClientEndpoint as n, schemaToOAuth as o, assertIdentifierValid as p, oauthToSchema as r, assertClientPrivileges as s, checkOAuthClient as t, normalizeClientCredentialsScopes as u, isClientLinkedToAnyResource as v, seedResources as x, logEnforcePerClientResourcesResolution as y };