@better-auth/oauth-provider 1.7.9 → 1.8.0-beta.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
@@ -1,11 +1,12 @@
1
- import { T as oauthClientMetadataSchema, b as ResourceUriSchema, f as extractRepeatedResourceFromForm, i as getOAuthProviderApi, v as resolveResourcePolicy } from "./introspect-7WCMPe5Z.mjs";
2
- import { D as validateClientScopes, N as extendOAuthProvider, O as verifyOAuthQueryParams, T as toResourceList, a as getClient, f as normalizeClientAuthenticationParameters, s as getOAuthProviderPlugin, w as toAudienceClaim } from "./utils-DKU5wblX.mjs";
3
- import { d as metadataResponse, f as oauthAuthorizationServerMetadata, h as oidcServerMetadata, i as oauthProvider, m as oauthProviderOpenIdConfigMetadata, n as DEFAULT_OAUTH_SCOPES, p as oauthProviderAuthServerMetadata, r as getOAuthProviderState, s as consumeClientAssertion, t as getIssuer, u as authServerMetadata } from "./authorize-D8r6mvZx.mjs";
1
+ import { E as oauthClientMetadataSchema, r as getOAuthProviderApi, v as resolveResourcePolicy, x as ResourceUriSchema } from "./introspect-CEo9Q06v.mjs";
2
+ import { C as parseScopes, H as extendOAuthProvider, I as validateClientScopes, L as verifyOAuthQueryParams, N as toResourceList, P as tokenError, i as consentCovers, l as findConsent, p as getOAuthProviderPlugin, u as getClient, v as normalizeClientAuthenticationParameters, w as readFormParams, y as normalizeTimestampValue } from "./utils-BMNe8Sg2.mjs";
3
+ import { c as consumeClientAssertion, d as authServerMetadata, f as metadataResponse, g as oidcServerMetadata, h as oauthProviderOpenIdConfigMetadata, i as oauthProvider, m as oauthProviderAuthServerMetadata, n as DEFAULT_OAUTH_SCOPES, p as oauthAuthorizationServerMetadata, r as getOAuthProviderState, t as getIssuer } from "./authorize-92EtLsBi.mjs";
4
4
  import { t as createResourceServerChallenge } from "./resource-challenge-Damwi0Or.mjs";
5
- import { APIError } from "better-auth/api";
5
+ import { appendQueryParams } from "@better-auth/core/utils/url";
6
+ import { isAPIError } from "better-auth/api";
6
7
  import * as z from "zod";
7
8
  import { BetterAuthError } from "@better-auth/core/error";
8
- import { deviceAuthorization, redeemDeviceCode } from "better-auth/plugins/device-authorization";
9
+ import { assertDeviceCodePending, buildVerificationUris, decideDeviceCode, deviceAuthorization, findDeviceCodeByUserCode, redeemDeviceCode } from "better-auth/plugins/device-authorization";
9
10
  //#region src/device-code.ts
10
11
  /**
11
12
  * RFC 8628 device authorization grant type. A registered OAuth client polls the
@@ -29,16 +30,6 @@ const oauthDeviceRequestFields = {
29
30
  client_assertion_type: z.string().meta({ description: "OAuth client assertion type" }).optional(),
30
31
  resource: deviceCodeResourceSchema.meta({ description: "RFC 8707 resource indicator(s) to bind to this authorization request" }).optional()
31
32
  };
32
- function tokenError(status, error, errorDescription) {
33
- throw new APIError(status, {
34
- error,
35
- error_description: errorDescription
36
- });
37
- }
38
- function parseScopes(scope) {
39
- const normalized = scope?.trim();
40
- return normalized ? normalized.split(/\s+/) : [];
41
- }
42
33
  /**
43
34
  * Exchanges an approved RFC 8628 device code for an OAuth token set. Unlike the
44
35
  * device-authorization plugin's `/device/token` (which mints a first-party
@@ -52,14 +43,14 @@ async function exchangeOAuthDeviceCode(input) {
52
43
  const body = ctx.body;
53
44
  const deviceCode = body?.device_code;
54
45
  if (!deviceCode) tokenError("BAD_REQUEST", "invalid_request", "device_code is required");
55
- const { authorizationContext: { client, confirmation, scopes }, redemptionContext: resources, user } = await redeemDeviceCode({
46
+ const authentication = await provider.authenticateClient({ requireCredentials: false });
47
+ const { authorizationContext: { client, scopes }, claimedDeviceCode, redemptionContext: { resources, confirmation }, user } = await redeemDeviceCode({
56
48
  ctx,
57
49
  deviceCode,
58
50
  authorizeRedemption: async (record) => {
59
51
  if (!record.oauthClientId) tokenError("BAD_REQUEST", "invalid_grant", "invalid device code");
60
52
  if (body?.client_id && record.oauthClientId !== body.client_id) tokenError("BAD_REQUEST", "invalid_grant", "Client ID mismatch");
61
53
  const scopes = parseScopes(record.scope);
62
- const authentication = await provider.authenticateClient({ requireCredentials: false });
63
54
  if (record.oauthClientId !== authentication.client.clientId) tokenError("BAD_REQUEST", "invalid_grant", "Client ID mismatch");
64
55
  validateClientScopes(authentication.client, scopes);
65
56
  return {
@@ -86,7 +77,15 @@ async function exchangeOAuthDeviceCode(input) {
86
77
  clientId: authorization.client.clientId,
87
78
  requestedScopes: authorization.scopes
88
79
  });
89
- return resources;
80
+ return {
81
+ resources,
82
+ confirmation: await provider.resolveConfirmation({
83
+ client: authorization.client,
84
+ scopes: authorization.scopes,
85
+ resources,
86
+ confirmation: authorization.confirmation
87
+ })
88
+ };
90
89
  }
91
90
  });
92
91
  return provider.issueTokens({
@@ -94,10 +93,77 @@ async function exchangeOAuthDeviceCode(input) {
94
93
  scopes,
95
94
  user,
96
95
  resources,
96
+ referenceId: claimedDeviceCode.referenceId ?? void 0,
97
+ authTime: normalizeTimestampValue(claimedDeviceCode.authTime),
98
+ acr: claimedDeviceCode.acr ?? void 0,
99
+ amr: claimedDeviceCode.amr ?? void 0,
100
+ originalResources: claimedDeviceCode.resources ?? void 0,
97
101
  confirmation
98
102
  });
99
103
  }
100
- function buildOAuthDeviceGrant() {
104
+ /**
105
+ * Lets the user approve an OAuth device code through `/oauth2/authorize`, so
106
+ * device approvals run the same sign-in, account selection, post-login, and
107
+ * consent pages as authorization requests. The user is bound to the code only
108
+ * at the final approval, after any account switch.
109
+ */
110
+ function deviceApprovalRequest(verificationUri) {
111
+ async function findOAuthDeviceCode(ctx, userCode) {
112
+ const deviceCode = await findDeviceCodeByUserCode(ctx, userCode);
113
+ if (!deviceCode?.oauthClientId) tokenError("BAD_REQUEST", "invalid_request", "Invalid user code");
114
+ return {
115
+ ...deviceCode,
116
+ oauthClientId: deviceCode.oauthClientId
117
+ };
118
+ }
119
+ const verificationPage = (ctx, userCode) => buildVerificationUris(verificationUri, ctx.context.baseURL, userCode).verificationUriComplete;
120
+ const completionURL = (ctx, userCode, status) => appendQueryParams(verificationPage(ctx, userCode), new URLSearchParams({ status }));
121
+ return {
122
+ load: async ({ ctx, opts, value, signedQuery }) => {
123
+ const deviceCode = await findOAuthDeviceCode(ctx, value);
124
+ assertDeviceCodePending(deviceCode);
125
+ const client = await getClient(ctx, opts, deviceCode.oauthClientId);
126
+ if (!client) tokenError("BAD_REQUEST", "invalid_client", "client_id is required");
127
+ const scopes = parseScopes(signedQuery?.scope ?? deviceCode.scope);
128
+ return {
129
+ query: {
130
+ user_code: deviceCode.userCode,
131
+ client_id: client.clientId,
132
+ scope: scopes.join(" "),
133
+ resource: deviceCode.resources ?? []
134
+ },
135
+ client,
136
+ requireConsent: true,
137
+ approve: async (approval) => {
138
+ await decideDeviceCode(ctx, {
139
+ deviceCode,
140
+ userId: approval.userId,
141
+ decision: "approved",
142
+ fields: {
143
+ scope: approval.query.scope,
144
+ referenceId: approval.referenceId ?? null,
145
+ authTime: new Date(approval.authTime),
146
+ acr: approval.acr ?? null,
147
+ amr: approval.amr ?? null
148
+ }
149
+ });
150
+ return completionURL(ctx, deviceCode.userCode, "approved");
151
+ }
152
+ };
153
+ },
154
+ deny: async ({ ctx, value, userId }) => {
155
+ const deviceCode = await findOAuthDeviceCode(ctx, value);
156
+ await decideDeviceCode(ctx, {
157
+ deviceCode,
158
+ userId,
159
+ decision: "denied"
160
+ });
161
+ return completionURL(ctx, deviceCode.userCode, "denied");
162
+ },
163
+ errorPage: ({ ctx, value }) => verificationPage(ctx, value)
164
+ };
165
+ }
166
+ function buildOAuthDeviceGrant(verificationUri) {
101
167
  return {
102
168
  requestSchemaFields: oauthDeviceRequestFields,
103
169
  requestErrorCodes: ["invalid_target"],
@@ -133,14 +199,30 @@ function buildOAuthDeviceGrant() {
133
199
  oauthClientId: {
134
200
  type: "string",
135
201
  required: false
202
+ },
203
+ referenceId: {
204
+ type: "string",
205
+ required: false
206
+ },
207
+ authTime: {
208
+ type: "date",
209
+ required: false
210
+ },
211
+ acr: {
212
+ type: "string",
213
+ required: false
214
+ },
215
+ amr: {
216
+ type: "string[]",
217
+ required: false
136
218
  }
137
219
  },
138
220
  grants: { [DEVICE_CODE_GRANT_TYPE]: exchangeOAuthDeviceCode },
139
221
  metadata: (metadataInput) => ({ device_authorization_endpoint: `${metadataInput.ctx.context.baseURL}${DEVICE_AUTHORIZATION_PATH}` }),
140
222
  authorizeRequest: async ({ ctx, request }) => {
141
223
  const { detected: hasClientAuthentication } = await normalizeClientAuthenticationParameters(ctx.request, ctx.body ?? {});
142
- const formResources = ctx.request ? await extractRepeatedResourceFromForm(ctx.request) : void 0;
143
- if (formResources) {
224
+ const formResources = ctx.request ? (await readFormParams(ctx.request))?.getAll("resource").filter(Boolean) : void 0;
225
+ if (formResources?.length) {
144
226
  const parsedResource = deviceCodeResourceSchema.safeParse(formResources.length === 1 ? formResources[0] : formResources);
145
227
  if (!parsedResource.success) tokenError("BAD_REQUEST", "invalid_target", "Invalid resource indicator");
146
228
  request.resource = parsedResource.data;
@@ -172,26 +254,12 @@ function buildOAuthDeviceGrant() {
172
254
  }
173
255
  };
174
256
  },
175
- assertSessionRedemption: ({ deviceCode }) => {
257
+ assertStandaloneUse: ({ deviceCode, use }) => {
176
258
  if (typeof deviceCode.oauthClientId !== "string") return;
177
- throw new APIError("BAD_REQUEST", {
178
- error: "invalid_grant",
179
- error_description: "This device code must be exchanged at the OAuth token endpoint (/oauth2/token)."
180
- });
259
+ if (use === "redeem") tokenError("BAD_REQUEST", "invalid_grant", "This device code must be exchanged at the OAuth token endpoint (/oauth2/token).");
260
+ tokenError("BAD_REQUEST", "invalid_request", `Approve this device code through the OAuth provider: send the user to /oauth2/authorize?user_code=${encodeURIComponent(String(deviceCode.userCode))}.`);
181
261
  },
182
- getVerificationContext: (deviceCode) => {
183
- if (typeof deviceCode.oauthClientId !== "string") return;
184
- const resources = deviceCode.resources;
185
- if (!Array.isArray(resources) || !resources.every((resource) => typeof resource === "string")) return;
186
- return { resource: toAudienceClaim(resources) };
187
- },
188
- verificationOpenAPIProperties: { resource: {
189
- oneOf: [{ type: "string" }, {
190
- type: "array",
191
- items: { type: "string" }
192
- }],
193
- description: "The requested resource indicators, returned only to the authenticated user who owns this request"
194
- } }
262
+ interactionRequests: { user_code: deviceApprovalRequest(verificationUri) }
195
263
  };
196
264
  }
197
265
  /**
@@ -214,7 +282,7 @@ function buildOAuthDeviceGrant() {
214
282
  * ```
215
283
  */
216
284
  function oauthDeviceAuthorization(options = {}) {
217
- const grant = buildOAuthDeviceGrant();
285
+ const grant = buildOAuthDeviceGrant(options.verificationUri);
218
286
  return {
219
287
  ...deviceAuthorization({
220
288
  ...options,
@@ -228,4 +296,93 @@ function oauthDeviceAuthorization(options = {}) {
228
296
  };
229
297
  }
230
298
  //#endregion
231
- export { DEFAULT_OAUTH_SCOPES, DEVICE_CODE_GRANT_TYPE, ResourceUriSchema, authServerMetadata, consumeClientAssertion, createResourceServerChallenge, extendOAuthProvider, getIssuer, getOAuthProviderApi, getOAuthProviderState, metadataResponse, oauthAuthorizationServerMetadata, oauthClientMetadataSchema, oauthDeviceAuthorization, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oidcServerMetadata, verifyOAuthQueryParams };
299
+ //#region src/token-exchange.ts
300
+ /** RFC 8693 §2.1 token exchange grant type. */
301
+ const TOKEN_EXCHANGE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:token-exchange";
302
+ /** RFC 8693 §3 token type identifier for an OAuth 2.0 access token. */
303
+ const ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
304
+ const NON_EXCHANGEABLE_SCOPES = new Set(["openid", "offline_access"]);
305
+ function deny(description) {
306
+ tokenError("BAD_REQUEST", "invalid_request", description);
307
+ }
308
+ async function exchangeToken({ ctx, opts, provider }, options) {
309
+ const body = ctx.body ?? {};
310
+ const subjectToken = body.subject_token;
311
+ if (typeof subjectToken !== "string" || !subjectToken) deny("subject_token is required");
312
+ if (body.subject_token_type !== "urn:ietf:params:oauth:token-type:access_token") deny(`subject_token_type must be ${ACCESS_TOKEN_TYPE}`);
313
+ if (body.actor_token !== void 0 || body.actor_token_type !== void 0) deny("actor_token is not supported");
314
+ if (body.requested_token_type !== void 0 && body.requested_token_type !== "urn:ietf:params:oauth:token-type:access_token") deny("requested_token_type is not supported");
315
+ const targets = [body.resource, body.audience].flat().filter((target) => target != null);
316
+ if (targets.length === 0) deny("resource or audience is required");
317
+ const { client, confirmation } = await provider.authenticateClient();
318
+ const userInfo = `${ctx.context.baseURL}/oauth2/userinfo`;
319
+ if (targets.some((target) => typeof target !== "string" || !target || target === userInfo)) tokenError("BAD_REQUEST", "invalid_target", "each resource or audience must be a registered resource");
320
+ const resources = [...new Set(targets)];
321
+ const subject = await Promise.resolve(provider.validateAccessToken(subjectToken, client.clientId)).catch((error) => {
322
+ if (isAPIError(error)) return;
323
+ throw error;
324
+ });
325
+ if (!subject?.active || !subject.sub || !subject.exp) deny("subject_token is invalid");
326
+ const user = await ctx.context.internalAdapter.findUserById(subject.sub);
327
+ if (!user) deny("subject_token does not represent a user");
328
+ const exchangeable = parseScopes(subject.scope).filter((scope) => !NON_EXCHANGEABLE_SCOPES.has(scope));
329
+ const scopes = typeof body.scope === "string" && body.scope.trim() ? [...new Set(parseScopes(body.scope))] : exchangeable.filter((scope) => !client.scopes || client.scopes.includes(scope));
330
+ const widened = scopes.find((scope) => !exchangeable.includes(scope));
331
+ if (widened) tokenError("BAD_REQUEST", "invalid_scope", `scope ${widened} is not available through token exchange`);
332
+ validateClientScopes(client, scopes);
333
+ const { effectiveScopes } = await resolveResourcePolicy(ctx, opts, {
334
+ resource: resources,
335
+ clientId: client.clientId,
336
+ requestedScopes: scopes
337
+ });
338
+ const referenceId = typeof subject.reference_id === "string" && subject.reference_id || void 0;
339
+ if (!client.skipConsent) {
340
+ const consent = await findConsent(ctx, client.clientId, user.id, referenceId);
341
+ if (!consent || !consentCovers(consent, effectiveScopes, resources)) deny("the user has not consented to this client for this exchange");
342
+ }
343
+ if (options.allowExchange && !await options.allowExchange({
344
+ client,
345
+ user,
346
+ subjectToken: subject,
347
+ resources,
348
+ scopes: effectiveScopes,
349
+ referenceId
350
+ })) deny("token exchange denied by policy");
351
+ return provider.issueTokens({
352
+ client,
353
+ user,
354
+ scopes: effectiveScopes,
355
+ resources,
356
+ referenceId,
357
+ sessionId: typeof subject.sid === "string" ? subject.sid : void 0,
358
+ maxExpiresAt: /* @__PURE__ */ new Date(subject.exp * 1e3),
359
+ confirmation,
360
+ tokenResponse: { issued_token_type: ACCESS_TOKEN_TYPE }
361
+ });
362
+ }
363
+ /**
364
+ * RFC 8693 token exchange for the OAuth Provider. A confidential client
365
+ * registered for the token-exchange grant trades a user's access token, issued
366
+ * by this server to it or for a resource it serves, for an access token to
367
+ * another registered resource. The issued token represents the same user
368
+ * (impersonation), carries the requesting client as `client_id`, and expires
369
+ * no later than the subject token.
370
+ *
371
+ * @example
372
+ * ```ts
373
+ * const auth = betterAuth({
374
+ * plugins: [jwt(), oauthProvider({ ... }), oauthTokenExchange()],
375
+ * });
376
+ * ```
377
+ */
378
+ function oauthTokenExchange(options = {}) {
379
+ const extension = { grants: { [TOKEN_EXCHANGE_GRANT_TYPE]: (input) => exchangeToken(input, options) } };
380
+ return {
381
+ id: "oauth-token-exchange",
382
+ init(ctx) {
383
+ extendOAuthProvider(ctx, extension);
384
+ }
385
+ };
386
+ }
387
+ //#endregion
388
+ export { ACCESS_TOKEN_TYPE, DEFAULT_OAUTH_SCOPES, DEVICE_CODE_GRANT_TYPE, ResourceUriSchema, TOKEN_EXCHANGE_GRANT_TYPE, authServerMetadata, consumeClientAssertion, createResourceServerChallenge, extendOAuthProvider, getIssuer, getOAuthProviderApi, getOAuthProviderState, metadataResponse, oauthAuthorizationServerMetadata, oauthClientMetadataSchema, oauthDeviceAuthorization, oauthProvider, oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata, oauthTokenExchange, oidcServerMetadata, verifyOAuthQueryParams };
@@ -1,52 +1,120 @@
1
- import { H as Scope, V as SchemaClient, j as OAuthOptions, o as OAuthClient } from "./oauth-DAMtT2vg.mjs";
2
- import { JSONWebKeySet } from "jose";
3
- import { GenericEndpointContext } from "@better-auth/core";
1
+ import { J as Scope, L as OAuthOptions, at as OAuthClientRegistrationResult, ct as registerClientMetadataDocument, ot as RegisterClientMetadataDocumentInput, st as assertPublicRoutableHttpsUri } from "./oauth-BlEuA8oD.mjs";
2
+ import { a as validatePublicClientJwks, i as PublicClientJwksValidationResult, p as createOAuthEndpoint } from "./oauth-mdkaIwmm.mjs";
3
+ import { b as getExtensionClientMetadataFields, i as validateClientScopes, n as parseClientMetadata, r as toResourceList } from "./index-sGnh4IfY.mjs";
4
+ import { JWSAlgorithms } from "better-auth/plugins";
5
+ import { Session, User } from "better-auth/types";
6
+ import { AuthContext, GenericEndpointContext } from "@better-auth/core";
4
7
 
5
- //#region src/client-jwks.d.ts
6
- type PublicClientJwksValidationResult = {
7
- valid: true;
8
- jwks: JSONWebKeySet;
9
- } | {
10
- valid: false;
11
- error: "jwks must be an RFC 7517 JWK Set object with a non-empty keys array" | "jwks must contain only public asymmetric keys" | "jwks keys must be supported public JWKs with required key parameters" | "jwks key alg must be supported for private_key_jwt and compatible with its key type and signing curve";
12
- };
13
- /**
14
- * Validates an OAuth client's public asymmetric JWK set.
15
- *
16
- * This boundary accepts only the RFC 7517 `{ keys: [...] }` representation.
17
- * It performs no I/O and returns the validated set for downstream JOSE
18
- * verification.
19
- *
20
- * @internal
21
- */
22
- declare function validatePublicClientJwks(input: unknown): PublicClientJwksValidationResult;
8
+ //#region src/authentication-context.d.ts
9
+ /** The ACR values the provider may assert, advertised in discovery. */
10
+ declare function getAcrValuesSupported(opts: OAuthOptions<Scope[]>): string[];
23
11
  //#endregion
24
12
  //#region src/client-metadata.d.ts
25
13
  declare function isForbiddenCimdClientMetadataField(field: string): boolean;
26
14
  //#endregion
27
- //#region src/register.d.ts
28
- interface OAuthClientRegistrationResult {
29
- client: SchemaClient<Scope[]>;
30
- clientSecret?: string;
31
- resources: string[];
32
- created: boolean;
33
- }
34
- interface RegisterClientMetadataDocumentInput {
35
- metadata: OAuthClient;
36
- clientId: string;
37
- clientDiscoveryId: string;
38
- existingClient?: SchemaClient<Scope[]>;
15
+ //#region src/resources.d.ts
16
+ /**
17
+ * Resolved resource policy for a single `/oauth2/token` (or `/oauth2/authorize`)
18
+ * request. Computed by {@link resolveResourcePolicy} and consumed by the
19
+ * token-issuance pipeline.
20
+ */
21
+ interface ResolvedResourcePolicy {
22
+ /**
23
+ * The `aud` claim value:
24
+ * - `undefined` when the request omitted `resource` (legacy behavior)
25
+ * - a single string when exactly one resource was requested
26
+ * - an array when multiple were requested
27
+ *
28
+ * Includes the implicit `/oauth2/userinfo` audience when `openid` is in
29
+ * scope — the OIDC userinfo endpoint expects to find itself in `aud`.
30
+ */
31
+ audienceClaim: string | string[] | undefined;
32
+ /**
33
+ * Effective access-token TTL in seconds. `null` means "no resource-specific
34
+ * override — caller should use the plugin default."
35
+ *
36
+ * When multiple resources are requested, this is the **minimum** TTL
37
+ * across all of them (OAuth 2.1 §1.5 bounded-blast-radius rule).
38
+ */
39
+ accessTokenTtl: number | null;
40
+ /**
41
+ * Effective refresh-token TTL in seconds. `null` means "no resource-specific
42
+ * override — caller should use {@link OAuthOptions.refreshTokenExpiresIn}
43
+ * (or its built-in default)."
44
+ *
45
+ * When multiple resources are requested, this is the **minimum** TTL
46
+ * across all of them (same bounded-blast-radius rule as access tokens).
47
+ */
48
+ refreshTokenTtl: number | null;
49
+ /**
50
+ * Effective signing algorithm and key id for the issued token.
51
+ *
52
+ * The JWS Protected Header (RFC 7515 §4.1) carries exactly one `alg` and
53
+ * at most one `kid`, so a single signed token can satisfy a multi-resource
54
+ * request only when every resource's pins are mutually compatible:
55
+ * the set of unique non-null `signingAlgorithm` values across requested
56
+ * resources must be ≤ 1, and the same for `signingKeyId`. When the
57
+ * compatibility check passes, the single unique pin (if any) is honored;
58
+ * `null` means "no resource pinned this, fall back to the JWT plugin
59
+ * default." Conflicts raise `invalid_request`.
60
+ */
61
+ signingAlgorithm: JWSAlgorithms | null;
62
+ signingKeyId: string | null;
63
+ /**
64
+ * Merged custom claims across requested resources, returned RAW: reserved
65
+ * RFC 9068 names are NOT stripped here. Feed this into
66
+ * `resolveAccessTokenClaims`, which owns the single strip; do not place it on
67
+ * a token directly or a resource could override an AS-owned claim.
68
+ */
69
+ rawCustomClaims: Record<string, unknown>;
70
+ /**
71
+ * True when at least one requested resource requires DPoP-bound access
72
+ * tokens.
73
+ */
74
+ dpopBoundAccessTokensRequired: boolean;
75
+ /**
76
+ * The intersection of the caller's `requestedScopes` with each requested
77
+ * resource's `allowedScopes`. When no requested resource defines an
78
+ * allowlist, equals `requestedScopes` unchanged.
79
+ *
80
+ * - A resource with `allowedScopes: null` is treated as "no restriction"
81
+ * for that resource (other resources' allowlists still apply).
82
+ * - A resource whose allowlist excludes every requested scope causes
83
+ * {@link resolveResourcePolicy} to throw `invalid_scope`.
84
+ * - Otherwise this is the filtered scope set the token endpoint should
85
+ * mint with (RFC 6749 §3.3 narrow-but-don't-widen).
86
+ */
87
+ effectiveScopes: string[];
39
88
  }
40
89
  /**
41
- * First-party persistence seam for a validated Client ID Metadata Document.
90
+ * Resolves the resource policy for a request.
42
91
  *
43
- * This operation owns both initial creation and atomic replacement. It never
44
- * generates a client secret, preserves operator-controlled flags on
45
- * replacement, and ensures server-default resource links in the same
46
- * transaction.
92
+ * Validation steps (RFC 8707 §3 + per-resource policy):
93
+ *
94
+ * 1. Resolve `resource` parameter to a list of identifiers.
95
+ * 2. For each identifier, look up the {@link OAuthResource} row.
96
+ * - Missing → `invalid_target`.
97
+ * - `disabled` → `invalid_target` (no new issuance).
98
+ * 3. If {@link OAuthOptions.enforcePerClientResources} resolves to true,
99
+ * confirm the client is linked to every requested resource via
100
+ * `oauthClientResource`. Unlinked → `invalid_target`.
101
+ * 4. Intersect `requestedScopes` with each resource's `allowedScopes`.
102
+ * Empty intersection → `invalid_scope`.
103
+ * 5. Pick the minimum `accessTokenTtl` across requested resources.
104
+ * 6. Pick signing config — only honored for single-resource requests
105
+ * (a JWT can only have one signature).
106
+ * 7. Merge per-resource `customClaims` (returned raw; reserved-claim
107
+ * stripping is owned by `resolveAccessTokenClaims`).
108
+ *
109
+ * When no `resource` param is present, returns an empty policy: no `aud`
110
+ * claim and no resource-specific overrides.
47
111
  *
48
112
  * @internal
49
113
  */
50
- declare function registerClientMetadataDocument(ctx: GenericEndpointContext, opts: OAuthOptions<Scope[]>, input: RegisterClientMetadataDocumentInput): Promise<OAuthClientRegistrationResult>;
114
+ declare function resolveResourcePolicy(ctx: GenericEndpointContext, opts: OAuthOptions<Scope[]>, params: {
115
+ resource: unknown;
116
+ clientId: string;
117
+ requestedScopes: string[];
118
+ }): Promise<ResolvedResourcePolicy>;
51
119
  //#endregion
52
- export { type OAuthClientRegistrationResult, type PublicClientJwksValidationResult, type RegisterClientMetadataDocumentInput, isForbiddenCimdClientMetadataField, registerClientMetadataDocument, validatePublicClientJwks };
120
+ export { type OAuthClientRegistrationResult, type PublicClientJwksValidationResult, type RegisterClientMetadataDocumentInput, assertPublicRoutableHttpsUri, createOAuthEndpoint, getAcrValuesSupported, getExtensionClientMetadataFields, isForbiddenCimdClientMetadataField, parseClientMetadata, registerClientMetadataDocument, resolveResourcePolicy, toResourceList, validateClientScopes, validatePublicClientJwks };
package/dist/internal.mjs CHANGED
@@ -1,2 +1,4 @@
1
- import { a as registerClientMetadataDocument, c as isForbiddenCimdClientMetadataField, l as validatePublicClientJwks } from "./authorize-D8r6mvZx.mjs";
2
- export { isForbiddenCimdClientMetadataField, registerClientMetadataDocument, validatePublicClientJwks };
1
+ import { A as getAcrValuesSupported, v as resolveResourcePolicy } from "./introspect-CEo9Q06v.mjs";
2
+ import { I as validateClientScopes, N as toResourceList, W as getExtensionClientMetadataFields, et as isForbiddenCimdClientMetadataField, x as parseClientMetadata } from "./utils-BMNe8Sg2.mjs";
3
+ import { a as assertPublicRoutableHttpsUri, l as validatePublicClientJwks, o as registerClientMetadataDocument, u as createOAuthEndpoint } from "./authorize-92EtLsBi.mjs";
4
+ export { assertPublicRoutableHttpsUri, createOAuthEndpoint, getAcrValuesSupported, getExtensionClientMetadataFields, isForbiddenCimdClientMetadataField, parseClientMetadata, registerClientMetadataDocument, resolveResourcePolicy, toResourceList, validateClientScopes, validatePublicClientJwks };