@better-auth/oauth-provider 1.7.10 → 1.8.0-beta.1

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 { E as oauthClientMetadataSchema, i as getOAuthProviderApi, p as extractRepeatedResourceFromForm, x as ResourceUriSchema, y as resolveResourcePolicy } from "./introspect-BVm_zFQC.mjs";
2
- import { L as extendOAuthProvider, M as verifyOAuthQueryParams, O as toAudienceClaim, g as normalizeClientAuthenticationParameters, j as validateClientScopes, k as toResourceList, s as getClient, u as getOAuthProviderPlugin } from "./utils-DfMNRh3J.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-dkOG8GqZ.mjs";
1
+ import { E as oauthClientMetadataSchema, r as getOAuthProviderApi, v as resolveResourcePolicy, x as ResourceUriSchema } from "./introspect-DdmCPEHN.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-D-CUrvd1.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-Dg976Ug0.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
@@ -53,7 +44,7 @@ async function exchangeOAuthDeviceCode(input) {
53
44
  const deviceCode = body?.device_code;
54
45
  if (!deviceCode) tokenError("BAD_REQUEST", "invalid_request", "device_code is required");
55
46
  const authentication = await provider.authenticateClient({ requireCredentials: false });
56
- const { authorizationContext: { client, confirmation, scopes }, claimedDeviceCode, redemptionContext: resources, user } = await redeemDeviceCode({
47
+ const { authorizationContext: { client, scopes }, claimedDeviceCode, redemptionContext: { resources, confirmation }, user } = await redeemDeviceCode({
57
48
  ctx,
58
49
  deviceCode,
59
50
  authorizeRedemption: async (record) => {
@@ -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,11 +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,
97
100
  originalResources: claimedDeviceCode.resources ?? void 0,
98
101
  confirmation
99
102
  });
100
103
  }
101
- 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) {
102
167
  return {
103
168
  requestSchemaFields: oauthDeviceRequestFields,
104
169
  requestErrorCodes: ["invalid_target"],
@@ -134,14 +199,30 @@ function buildOAuthDeviceGrant() {
134
199
  oauthClientId: {
135
200
  type: "string",
136
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
137
218
  }
138
219
  },
139
220
  grants: { [DEVICE_CODE_GRANT_TYPE]: exchangeOAuthDeviceCode },
140
221
  metadata: (metadataInput) => ({ device_authorization_endpoint: `${metadataInput.ctx.context.baseURL}${DEVICE_AUTHORIZATION_PATH}` }),
141
222
  authorizeRequest: async ({ ctx, request }) => {
142
223
  const { detected: hasClientAuthentication } = await normalizeClientAuthenticationParameters(ctx.request, ctx.body ?? {});
143
- const formResources = ctx.request ? await extractRepeatedResourceFromForm(ctx.request) : void 0;
144
- if (formResources) {
224
+ const formResources = ctx.request ? (await readFormParams(ctx.request))?.getAll("resource").filter(Boolean) : void 0;
225
+ if (formResources?.length) {
145
226
  const parsedResource = deviceCodeResourceSchema.safeParse(formResources.length === 1 ? formResources[0] : formResources);
146
227
  if (!parsedResource.success) tokenError("BAD_REQUEST", "invalid_target", "Invalid resource indicator");
147
228
  request.resource = parsedResource.data;
@@ -173,26 +254,12 @@ function buildOAuthDeviceGrant() {
173
254
  }
174
255
  };
175
256
  },
176
- assertSessionRedemption: ({ deviceCode }) => {
257
+ assertStandaloneUse: ({ deviceCode, use }) => {
177
258
  if (typeof deviceCode.oauthClientId !== "string") return;
178
- throw new APIError("BAD_REQUEST", {
179
- error: "invalid_grant",
180
- error_description: "This device code must be exchanged at the OAuth token endpoint (/oauth2/token)."
181
- });
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))}.`);
182
261
  },
183
- getVerificationContext: (deviceCode) => {
184
- if (typeof deviceCode.oauthClientId !== "string") return;
185
- const resources = deviceCode.resources;
186
- if (!Array.isArray(resources) || !resources.every((resource) => typeof resource === "string")) return;
187
- return { resource: toAudienceClaim(resources) };
188
- },
189
- verificationOpenAPIProperties: { resource: {
190
- oneOf: [{ type: "string" }, {
191
- type: "array",
192
- items: { type: "string" }
193
- }],
194
- description: "The requested resource indicators, returned only to the authenticated user who owns this request"
195
- } }
262
+ interactionRequests: { user_code: deviceApprovalRequest(verificationUri) }
196
263
  };
197
264
  }
198
265
  /**
@@ -215,7 +282,7 @@ function buildOAuthDeviceGrant() {
215
282
  * ```
216
283
  */
217
284
  function oauthDeviceAuthorization(options = {}) {
218
- const grant = buildOAuthDeviceGrant();
285
+ const grant = buildOAuthDeviceGrant(options.verificationUri);
219
286
  return {
220
287
  ...deviceAuthorization({
221
288
  ...options,
@@ -229,4 +296,93 @@ function oauthDeviceAuthorization(options = {}) {
229
296
  };
230
297
  }
231
298
  //#endregion
232
- 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,33 +1,120 @@
1
- import { H as Scope, V as SchemaClient, j as OAuthOptions, o as OAuthClient } from "./oauth-DXiqbRzK.mjs";
2
- import { n as PublicClientJwksValidationResult, r as validatePublicClientJwks } from "./client-jwks-DY2QBrsl.mjs";
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-CzT1PvAY.mjs";
2
+ import { a as validatePublicClientJwks, i as PublicClientJwksValidationResult, p as createOAuthEndpoint } from "./oauth-DfH-ghKj.mjs";
3
+ import { b as getExtensionClientMetadataFields, i as validateClientScopes, n as parseClientMetadata, r as toResourceList } from "./index-DVydGHlT.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
 
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[];
11
+ //#endregion
5
12
  //#region src/client-metadata.d.ts
6
13
  declare function isForbiddenCimdClientMetadataField(field: string): boolean;
7
14
  //#endregion
8
- //#region src/register.d.ts
9
- interface OAuthClientRegistrationResult {
10
- client: SchemaClient<Scope[]>;
11
- clientSecret?: string;
12
- resources: string[];
13
- created: boolean;
14
- }
15
- interface RegisterClientMetadataDocumentInput {
16
- metadata: OAuthClient;
17
- clientId: string;
18
- clientDiscoveryId: string;
19
- 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[];
20
88
  }
21
89
  /**
22
- * First-party persistence seam for a validated Client ID Metadata Document.
90
+ * Resolves the resource policy for a request.
91
+ *
92
+ * Validation steps (RFC 8707 §3 + per-resource policy):
23
93
  *
24
- * This operation owns both initial creation and atomic replacement. It never
25
- * generates a client secret, preserves operator-controlled flags on
26
- * replacement, and ensures server-default resource links in the same
27
- * transaction.
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.
28
111
  *
29
112
  * @internal
30
113
  */
31
- 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>;
32
119
  //#endregion
33
- 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-dkOG8GqZ.mjs";
2
- export { isForbiddenCimdClientMetadataField, registerClientMetadataDocument, validatePublicClientJwks };
1
+ import { A as getAcrValuesSupported, v as resolveResourcePolicy } from "./introspect-DdmCPEHN.mjs";
2
+ import { I as validateClientScopes, N as toResourceList, W as getExtensionClientMetadataFields, et as isForbiddenCimdClientMetadataField, x as parseClientMetadata } from "./utils-D-CUrvd1.mjs";
3
+ import { a as assertPublicRoutableHttpsUri, l as validatePublicClientJwks, o as registerClientMetadataDocument, u as createOAuthEndpoint } from "./authorize-Dg976Ug0.mjs";
4
+ export { assertPublicRoutableHttpsUri, createOAuthEndpoint, getAcrValuesSupported, getExtensionClientMetadataFields, isForbiddenCimdClientMetadataField, parseClientMetadata, registerClientMetadataDocument, resolveResourcePolicy, toResourceList, validateClientScopes, validatePublicClientJwks };