@aventara/access 0.0.0-stage → 0.1.0-rc.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.
Files changed (240) hide show
  1. package/CHANGELOG.md +831 -0
  2. package/LICENSE +91 -0
  3. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  4. package/README.md +2439 -2
  5. package/dist/access/access-options.d.ts +66 -0
  6. package/dist/access/access-options.js +5 -0
  7. package/dist/access/access.error.d.ts +35 -0
  8. package/dist/access/access.error.js +11 -0
  9. package/dist/access/access.factory.d.ts +273 -0
  10. package/dist/access/access.factory.js +440 -0
  11. package/dist/access/better-auth.hardening.d.ts +120 -0
  12. package/dist/access/better-auth.hardening.js +420 -0
  13. package/dist/access/better-auth.loader.d.ts +67 -0
  14. package/dist/access/better-auth.loader.js +65 -0
  15. package/dist/access/better-auth.range.d.ts +9 -0
  16. package/dist/access/better-auth.range.js +28 -0
  17. package/dist/access/better-auth.require.d.ts +90 -0
  18. package/dist/access/better-auth.require.js +6 -0
  19. package/dist/access/better-auth.require.node.d.ts +7 -0
  20. package/dist/access/better-auth.require.node.js +52 -0
  21. package/dist/access/claims.mapper.d.ts +79 -0
  22. package/dist/access/claims.mapper.js +152 -0
  23. package/dist/access/key-set.cache.d.ts +10 -0
  24. package/dist/access/key-set.cache.js +63 -0
  25. package/dist/access/policies-block.checker.d.ts +64 -0
  26. package/dist/access/policies-block.checker.js +138 -0
  27. package/dist/access/principal.subject.d.ts +12 -0
  28. package/dist/access/principal.subject.js +60 -0
  29. package/dist/access/profile.policy.d.ts +74 -0
  30. package/dist/access/profile.policy.js +110 -0
  31. package/dist/access/request.state.d.ts +89 -0
  32. package/dist/access/request.state.js +196 -0
  33. package/dist/access/session.hooks.d.ts +114 -0
  34. package/dist/access/session.hooks.js +258 -0
  35. package/dist/access/token.verifier.d.ts +35 -0
  36. package/dist/access/token.verifier.js +69 -0
  37. package/dist/access-schema.d.ts +48 -0
  38. package/dist/access-schema.js +5 -0
  39. package/dist/access-warning.emitter.d.ts +2 -0
  40. package/dist/access-warning.emitter.js +20 -0
  41. package/dist/access.builder.d.ts +299 -0
  42. package/dist/access.builder.js +66 -0
  43. package/dist/access.engine.d.ts +142 -0
  44. package/dist/access.engine.js +470 -0
  45. package/dist/audit/audit-event.d.ts +135 -0
  46. package/dist/audit/audit-event.js +8 -0
  47. package/dist/audit/audit.log.d.ts +122 -0
  48. package/dist/audit/audit.log.js +345 -0
  49. package/dist/audit/audit.schema.d.ts +143 -0
  50. package/dist/audit/audit.schema.js +50 -0
  51. package/dist/auth/index.d.ts +12 -0
  52. package/dist/auth/index.js +2 -0
  53. package/dist/aventara-where.translator.d.ts +19 -0
  54. package/dist/aventara-where.translator.js +87 -0
  55. package/dist/beacon/beacon.check.d.ts +59 -0
  56. package/dist/beacon/beacon.check.js +128 -0
  57. package/dist/beacon/beacon.schema.d.ts +41 -0
  58. package/dist/beacon/beacon.schema.js +48 -0
  59. package/dist/beacon/beacon.store.d.ts +29 -0
  60. package/dist/beacon/beacon.store.js +78 -0
  61. package/dist/better-auth/access.client-plugin.d.ts +75 -0
  62. package/dist/better-auth/access.client-plugin.js +133 -0
  63. package/dist/better-auth/access.plugin.d.ts +164 -0
  64. package/dist/better-auth/access.plugin.js +348 -0
  65. package/dist/better-auth/access.provider.d.ts +78 -0
  66. package/dist/better-auth/access.provider.js +100 -0
  67. package/dist/better-auth/index.d.ts +7 -0
  68. package/dist/better-auth/index.js +4 -0
  69. package/dist/better-auth/organization.bridge.d.ts +66 -0
  70. package/dist/better-auth/organization.bridge.js +144 -0
  71. package/dist/canonical-json.writer.d.ts +12 -0
  72. package/dist/canonical-json.writer.js +30 -0
  73. package/dist/cli/avaccess.bin.d.ts +2 -0
  74. package/dist/cli/avaccess.bin.js +26 -0
  75. package/dist/cli/avaccess.program.d.ts +15 -0
  76. package/dist/cli/avaccess.program.js +166 -0
  77. package/dist/cli/beacon-pins.file.d.ts +14 -0
  78. package/dist/cli/beacon-pins.file.js +62 -0
  79. package/dist/cli/beacon.loader.d.ts +20 -0
  80. package/dist/cli/beacon.loader.js +71 -0
  81. package/dist/cli/cli.context.d.ts +49 -0
  82. package/dist/cli/cli.context.js +66 -0
  83. package/dist/cli/diff.command.d.ts +2 -0
  84. package/dist/cli/diff.command.js +66 -0
  85. package/dist/cli/generate.command.d.ts +2 -0
  86. package/dist/cli/generate.command.js +82 -0
  87. package/dist/cli/keys.command.d.ts +2 -0
  88. package/dist/cli/keys.command.js +96 -0
  89. package/dist/cli/lock.file.d.ts +31 -0
  90. package/dist/cli/lock.file.js +181 -0
  91. package/dist/cli/module.loader.d.ts +13 -0
  92. package/dist/cli/module.loader.js +63 -0
  93. package/dist/cli/policies.diff.d.ts +16 -0
  94. package/dist/cli/policies.diff.js +83 -0
  95. package/dist/cli/pull.command.d.ts +2 -0
  96. package/dist/cli/pull.command.js +234 -0
  97. package/dist/cli/push.command.d.ts +2 -0
  98. package/dist/cli/push.command.js +59 -0
  99. package/dist/cli/registry.client.d.ts +63 -0
  100. package/dist/cli/registry.client.js +287 -0
  101. package/dist/cli/schema.command.d.ts +2 -0
  102. package/dist/cli/schema.command.js +60 -0
  103. package/dist/cli/serve.command.d.ts +6 -0
  104. package/dist/cli/serve.command.js +200 -0
  105. package/dist/cli/status.command.d.ts +2 -0
  106. package/dist/cli/status.command.js +31 -0
  107. package/dist/cli/translator.flag.d.ts +6 -0
  108. package/dist/cli/translator.flag.js +24 -0
  109. package/dist/cli/verify.command.d.ts +2 -0
  110. package/dist/cli/verify.command.js +40 -0
  111. package/dist/client/access-client.factory.d.ts +92 -0
  112. package/dist/client/access-client.factory.js +269 -0
  113. package/dist/client/access-verbs.schema.d.ts +66 -0
  114. package/dist/client/access-verbs.schema.js +159 -0
  115. package/dist/client/verb-rate.limiter.d.ts +116 -0
  116. package/dist/client/verb-rate.limiter.js +358 -0
  117. package/dist/foreign/foreign-facts.d.ts +30 -0
  118. package/dist/foreign/foreign-facts.js +75 -0
  119. package/dist/foreign/foreign.merger.d.ts +10 -0
  120. package/dist/foreign/foreign.merger.js +52 -0
  121. package/dist/foreign-references.check.d.ts +12 -0
  122. package/dist/foreign-references.check.js +134 -0
  123. package/dist/generator/client.emitter.d.ts +28 -0
  124. package/dist/generator/client.emitter.js +411 -0
  125. package/dist/generator/copied-modules.reader.d.ts +14 -0
  126. package/dist/generator/copied-modules.reader.js +80 -0
  127. package/dist/generator/namespace.facts.d.ts +28 -0
  128. package/dist/generator/namespace.facts.js +32 -0
  129. package/dist/generator/orm.translator.d.ts +48 -0
  130. package/dist/generator/orm.translator.js +29 -0
  131. package/dist/generator/prisma7/prisma-schema.reader.d.ts +9 -0
  132. package/dist/generator/prisma7/prisma-schema.reader.js +394 -0
  133. package/dist/generator/prisma7/prisma-where.translator.d.ts +72 -0
  134. package/dist/generator/prisma7/prisma-where.translator.js +136 -0
  135. package/dist/generator/prisma7/prisma7.translator.d.ts +2 -0
  136. package/dist/generator/prisma7/prisma7.translator.js +87 -0
  137. package/dist/generator/schema.emitter.d.ts +14 -0
  138. package/dist/generator/schema.emitter.js +23 -0
  139. package/dist/generator/tsconfig.reader.d.ts +44 -0
  140. package/dist/generator/tsconfig.reader.js +110 -0
  141. package/dist/generator/typescript-literal.writer.d.ts +5 -0
  142. package/dist/generator/typescript-literal.writer.js +42 -0
  143. package/dist/guardrails/guardrails-contract.d.ts +45 -0
  144. package/dist/guardrails/guardrails-contract.js +12 -0
  145. package/dist/guardrails/guardrails.builder.d.ts +48 -0
  146. package/dist/guardrails/guardrails.builder.js +32 -0
  147. package/dist/guardrails/guardrails.compiler.d.ts +10 -0
  148. package/dist/guardrails/guardrails.compiler.js +254 -0
  149. package/dist/guardrails/guardrails.composer.d.ts +7 -0
  150. package/dist/guardrails/guardrails.composer.js +95 -0
  151. package/dist/guardrails/guardrails.hash.d.ts +10 -0
  152. package/dist/guardrails/guardrails.hash.js +38 -0
  153. package/dist/index.d.ts +16 -0
  154. package/dist/index.js +6 -0
  155. package/dist/kiosk/kiosk-verbs.schema.d.ts +19 -0
  156. package/dist/kiosk/kiosk-verbs.schema.js +61 -0
  157. package/dist/kiosk/kiosk.plugin.d.ts +217 -0
  158. package/dist/kiosk/kiosk.plugin.js +877 -0
  159. package/dist/kiosk/kiosk.schema.d.ts +176 -0
  160. package/dist/kiosk/kiosk.schema.js +101 -0
  161. package/dist/kiosk/kiosk.store.d.ts +171 -0
  162. package/dist/kiosk/kiosk.store.js +478 -0
  163. package/dist/kiosk/lan-range.policy.d.ts +31 -0
  164. package/dist/kiosk/lan-range.policy.js +113 -0
  165. package/dist/kiosk/pin.policy.d.ts +9 -0
  166. package/dist/kiosk/pin.policy.js +143 -0
  167. package/dist/kiosk/session-use.marker.d.ts +4 -0
  168. package/dist/kiosk/session-use.marker.js +16 -0
  169. package/dist/lock/lock-contract.d.ts +38 -0
  170. package/dist/lock/lock-contract.js +67 -0
  171. package/dist/lock/lock.verifier.d.ts +27 -0
  172. package/dist/lock/lock.verifier.js +62 -0
  173. package/dist/mcp/agent-scope.gate.d.ts +11 -0
  174. package/dist/mcp/agent-scope.gate.js +75 -0
  175. package/dist/mcp/agent.grant.d.ts +36 -0
  176. package/dist/mcp/agent.grant.js +92 -0
  177. package/dist/mcp/mcp-options.d.ts +60 -0
  178. package/dist/mcp/mcp-options.js +72 -0
  179. package/dist/oauth/access-token.verifier.d.ts +66 -0
  180. package/dist/oauth/access-token.verifier.js +201 -0
  181. package/dist/oauth/oauth-provider.hardening.d.ts +62 -0
  182. package/dist/oauth/oauth-provider.hardening.js +261 -0
  183. package/dist/passwords/argon2id.hasher.d.ts +97 -0
  184. package/dist/passwords/argon2id.hasher.js +256 -0
  185. package/dist/passwords/node-rs-argon2.adapter.d.ts +25 -0
  186. package/dist/passwords/node-rs-argon2.adapter.js +42 -0
  187. package/dist/passwords/passwords.options.d.ts +79 -0
  188. package/dist/passwords/passwords.options.js +212 -0
  189. package/dist/policies-contract.d.ts +192 -0
  190. package/dist/policies-contract.js +8 -0
  191. package/dist/policies.compiler.d.ts +36 -0
  192. package/dist/policies.compiler.js +1358 -0
  193. package/dist/policies.decider.d.ts +103 -0
  194. package/dist/policies.decider.js +382 -0
  195. package/dist/policies.hash.d.ts +10 -0
  196. package/dist/policies.hash.js +53 -0
  197. package/dist/predicate.evaluator.d.ts +62 -0
  198. package/dist/predicate.evaluator.js +378 -0
  199. package/dist/random-secret.generator.d.ts +2 -0
  200. package/dist/random-secret.generator.js +15 -0
  201. package/dist/reference-reads.check.d.ts +12 -0
  202. package/dist/reference-reads.check.js +148 -0
  203. package/dist/registry/access.registry.d.ts +173 -0
  204. package/dist/registry/access.registry.js +865 -0
  205. package/dist/registry/beacon.ledger.d.ts +30 -0
  206. package/dist/registry/beacon.ledger.js +93 -0
  207. package/dist/registry/ownership.check.d.ts +54 -0
  208. package/dist/registry/ownership.check.js +215 -0
  209. package/dist/registry/registry.endpoints.d.ts +50 -0
  210. package/dist/registry/registry.endpoints.js +193 -0
  211. package/dist/registry/registry.key.d.ts +7 -0
  212. package/dist/registry/registry.key.js +13 -0
  213. package/dist/registry/registry.options.d.ts +13 -0
  214. package/dist/registry/registry.options.js +28 -0
  215. package/dist/registry/registry.refusal.d.ts +6 -0
  216. package/dist/registry/registry.refusal.js +11 -0
  217. package/dist/registry/registry.schema.d.ts +238 -0
  218. package/dist/registry/registry.schema.js +127 -0
  219. package/dist/resources/auth-tables.catalog.d.ts +91 -0
  220. package/dist/resources/auth-tables.catalog.js +102 -0
  221. package/dist/resources/auth-tables.guard.d.ts +95 -0
  222. package/dist/resources/auth-tables.guard.js +405 -0
  223. package/dist/scope.resolver.d.ts +138 -0
  224. package/dist/scope.resolver.js +512 -0
  225. package/dist/sha256.digest.d.ts +2 -0
  226. package/dist/sha256.digest.js +81 -0
  227. package/dist/standalone/access.server.d.ts +41 -0
  228. package/dist/standalone/access.server.js +301 -0
  229. package/dist/standalone/index.d.ts +1 -0
  230. package/dist/standalone/index.js +1 -0
  231. package/package.json +101 -3
  232. package/src/aventara-where.translator.ts +116 -0
  233. package/src/beacon/beacon.check.ts +204 -0
  234. package/src/canonical-json.writer.ts +51 -0
  235. package/src/generator/prisma7/prisma-where.translator.ts +241 -0
  236. package/src/policies-contract.ts +184 -0
  237. package/src/policies.decider.ts +650 -0
  238. package/src/predicate.evaluator.ts +574 -0
  239. package/src/scope.resolver.ts +791 -0
  240. package/src/sha256.digest.ts +85 -0
@@ -0,0 +1,72 @@
1
+ // The `mcp` block: Access as the authorization layer of one MCP endpoint
2
+ // (`createAccess({ mcp: { resource } })`, `aventaraAccess({ mcp: { resource } })`).
3
+ // Better Auth authenticates: its own OAuth provider (`@better-auth/oauth-provider`,
4
+ // which the app installs in Better Auth's plugins as it would for plain Better Auth,
5
+ // its options passed through untouched) is the authorization server and issues the
6
+ // tokens. Access issues none: it names the MCP endpoint they are for, refuses a
7
+ // provider configured short of what an agent's authorization needs
8
+ // (`oauth/oauth-provider.hardening.ts`), verifies the tokens, maps their scopes to the
9
+ // rules under the `agent` origin, and gives the framework `mcp.authorization`:
10
+ // `access.mcp`, `{ servers, verify }`.
11
+ //
12
+ // The authorization server's issuer is Access's fixed issuer, the origin of Better
13
+ // Auth's base URL, which `jwt()` signs every token with:
14
+ // one string, which the provider's metadata, its `iss` in each authorization
15
+ // response (RFC 9207) and the tokens' `iss` all carry, and which the endpoint's
16
+ // protected-resource metadata advertises. It is https, or, for a server on this
17
+ // machine, `http://127.0.0.1` or `http://[::1]` (the framework's rule for MCP authorization servers): a name
18
+ // such as `localhost` is resolved, and may resolve elsewhere.
19
+ import { AccessError } from "../access/access.error.js";
20
+ import { checkResourceUrl } from "../oauth/oauth-provider.hardening.js";
21
+ import { isRecord } from "../policies.compiler.js";
22
+ const refuse = (message) => {
23
+ throw new AccessError("ACCESS_OPTIONS_INVALID", message);
24
+ };
25
+ /** The hosts an http authorization server may name, as the WHATWG URL parser writes them (the framework's rule). */
26
+ const LOOPBACK_SERVER_HOSTS = new Set([
27
+ "127.0.0.1",
28
+ "[::1]",
29
+ ]);
30
+ /**
31
+ * The `mcp` block, checked: `{ resource, openRegistration? }`, the MCP endpoint's URL
32
+ * as its protected-resource metadata writes it (`checkResourceUrl`), and whether open
33
+ * registration is taken. `undefined` when the block is absent.
34
+ */
35
+ export const checkMcpOptions = (value, at, issuer) => {
36
+ if (value === undefined) {
37
+ return undefined;
38
+ }
39
+ if (!isRecord(value) ||
40
+ Object.keys(value).some((key) => key !== "resource" && key !== "openRegistration") ||
41
+ typeof value.resource !== "string" ||
42
+ (value.openRegistration !== undefined &&
43
+ typeof value.openRegistration !== "boolean")) {
44
+ return refuse(`\`${at}\` is \`{ resource, openRegistration? }\`: the MCP endpoint's URL, as its protected-resource metadata names it (\`"https://app.example.com/mcp"\`), and \`true\` to take the OAuth provider's open client registration.`);
45
+ }
46
+ return {
47
+ resource: checkResourceUrl(value.resource, `${at}.resource`, issuer),
48
+ openRegistration: value.openRegistration === true,
49
+ };
50
+ };
51
+ /**
52
+ * The authorization server `access.mcp.servers` names: the issuer, when it is https,
53
+ * or http on `127.0.0.1` or `[::1]` (the framework's rule). `localhost` is
54
+ * refused with the address to write instead.
55
+ */
56
+ export const authorizationServerOf = (issuer, baseURLAt) => {
57
+ const url = new URL(issuer);
58
+ if (url.port === "0") {
59
+ return refuse(`\`mcp\` advertises Access's issuer (${issuer}, from \`${baseURLAt}\`) as the MCP endpoint's OAuth authorization server, and it names port 0, which no server listens on.`);
60
+ }
61
+ if (url.protocol === "https:") {
62
+ return issuer;
63
+ }
64
+ if (url.protocol === "http:" && LOOPBACK_SERVER_HOSTS.has(url.hostname)) {
65
+ return issuer;
66
+ }
67
+ if (url.protocol === "http:" && url.hostname === "localhost") {
68
+ url.hostname = "127.0.0.1";
69
+ return refuse(`\`mcp\` advertises Access's issuer (${issuer}, from \`${baseURLAt}\`) as the MCP endpoint's OAuth authorization server, and an http one must be written as an address of this machine, not a name that is resolved (and may resolve elsewhere): set \`${baseURLAt}\` to ${url.origin}.`);
70
+ }
71
+ return refuse(`\`mcp\` advertises Access's issuer (${issuer}, from \`${baseURLAt}\`) as the MCP endpoint's OAuth authorization server, which must be https (or, on this machine, \`http://127.0.0.1\` or \`http://[::1]\`): set \`${baseURLAt}\` to its https URL.`);
72
+ };
@@ -0,0 +1,66 @@
1
+ import { type JSONWebKeySet } from "jose";
2
+ import type { AccessClaimsSource } from "../access/claims.mapper.js";
3
+ type ClaimsMap = {
4
+ readonly [claim: string]: string | number | boolean;
5
+ };
6
+ type AccessTokenVerifierSource = {
7
+ /** The published key set: `{ keys: [...] }`. */
8
+ readonly keys: () => Promise<JSONWebKeySet>;
9
+ /** The tokens' `iss`: the instance's fixed issuer. */
10
+ readonly issuer: string;
11
+ /** The resource's URL: the audience every token must name. */
12
+ readonly resource: string;
13
+ /** The provider's UserInfo URL, which a token asked with `openid` names beside the resource; `undefined` when Better Auth's base URL follows the request. */
14
+ readonly userInfo: string | undefined;
15
+ /** Better Auth's context, once Better Auth is built. */
16
+ readonly context: () => Promise<unknown>;
17
+ /** A user's claims, as a token `GET /token` issued now would carry them. */
18
+ readonly claimsFor: (source: AccessClaimsSource) => Promise<ClaimsMap>;
19
+ /** Milliseconds since the epoch; a test passes its own clock. */
20
+ readonly now?: () => number;
21
+ };
22
+ /** What an access token grants: the client it was issued to, and its scopes. */
23
+ export type TokenGrant = {
24
+ readonly clientId: string;
25
+ readonly scopes: ReadonlySet<string>;
26
+ };
27
+ /** Who an access token acts for, and what it grants. */
28
+ export type TokenCaller = {
29
+ readonly identity: {
30
+ readonly sub: string;
31
+ readonly claims: ClaimsMap;
32
+ };
33
+ readonly grant: TokenGrant;
34
+ };
35
+ export type AccessTokenVerifier = {
36
+ /** The caller an access token for the resource proves, or `null` for any other token. */
37
+ verify(token: string): Promise<TokenCaller | null>;
38
+ };
39
+ export declare const createAccessTokenVerifier: (source: AccessTokenVerifierSource) => AccessTokenVerifier;
40
+ /**
41
+ * The provider's UserInfo URL, which a token asked with `openid` names as its second
42
+ * audience: Better Auth's base URL (with its base path) and `/oauth2/userinfo`.
43
+ * `undefined` when the base URL follows the request (a dynamic `baseURL`): such a
44
+ * token then names an audience Access cannot know, and is refused.
45
+ */
46
+ export declare const userInfoAudience: (block: {
47
+ readonly baseURL?: unknown;
48
+ readonly basePath?: unknown;
49
+ }, modules: {
50
+ readonly getBaseURL: (baseURL: string | undefined, basePath: string | undefined) => string | undefined;
51
+ readonly isDynamicBaseURLConfig: (baseURL: unknown) => boolean;
52
+ }) => string | undefined;
53
+ /**
54
+ * `access.mcp` over the MCP endpoint's verifier: the framework's `mcp.authorization`, frozen,
55
+ * with exactly the two members the framework takes.
56
+ */
57
+ export declare const mcpAuthorizationOf: (agents: AccessTokenVerifier, server: string) => {
58
+ readonly servers: readonly [string];
59
+ verify(token: string): Promise<{
60
+ readonly sub: string;
61
+ readonly claims: ClaimsMap;
62
+ readonly clientId: string;
63
+ readonly scopes: readonly string[];
64
+ } | null>;
65
+ };
66
+ export {};
@@ -0,0 +1,201 @@
1
+ // Verifies the access tokens Better Auth's OAuth provider (`@better-auth/oauth-provider`)
2
+ // issues for one resource (RFC 8707), and finds who each acts for. Access issues no
3
+ // token itself: it reads the provider's. Two resources use it: the MCP endpoint
4
+ // (`mcp`, whose callers are agents) and the service itself when it opts in to its
5
+ // own audience (`audience`).
6
+ //
7
+ // A token passes when it is a JWT access token (`typ: "at+jwt"`, RFC 9068) signed
8
+ // with the keys `jwt()` publishes, by an algorithm `jwt()` signs with
9
+ // (`JWT_ALGORITHMS`), with the instance's issuer, current, issued for the one
10
+ // resource alone (`aud` the resource; with `openid` beside it, the provider's
11
+ // UserInfo endpoint too, and nothing else), to a client (`azp`, `client_id`) for a
12
+ // user (`sub`, never the client itself: a client-credentials token acts for no one),
13
+ // in the session the user consented in (`sid`), with its scopes, and not bound to a
14
+ // DPoP key (`cnf`): this endpoint reads a bearer token and no proof, and a bound
15
+ // token presented without one is refused (RFC 9449 section 7.1).
16
+ //
17
+ // Then, on every request, what the provider's own introspection checks, and more: the
18
+ // client exists and is not disabled; the resource is still one of the provider's (an
19
+ // `oauthResource` row: a deleted one takes no token, as the provider's introspection
20
+ // refuses it); the session the token was issued in has not ended, and is not bound to
21
+ // a kiosk device (`kioskDeviceId`: such a session is issued no token, whatever route
22
+ // minted one, since a token would act for the till's user off the device and past its
23
+ // lock and idle timeout); the user exists;
24
+ // and, unless the client is a trusted one that skips consent, the user's consent to
25
+ // that client still covers the token's scopes and its resource, so revoking consent
26
+ // stops the agent at once. A consent stored under a reference (the provider's
27
+ // `postLogin.consentReferenceId`, which Access refuses) never counts: a JWT access
28
+ // token does not say which reference it was consented under. The claims are the
29
+ // user's now, as a token `GET /token` issued now would carry them: a role taken away
30
+ // or a ban takes effect on the agent's next request, not when its token expires.
31
+ //
32
+ // A token that fails any of this is `null`. The token is read and never sent on:
33
+ // nothing here calls another service with it.
34
+ import { errors, jwtVerify } from "jose";
35
+ import { createKeySetCache } from "../access/key-set.cache.js";
36
+ import { CLOCK_TOLERANCE_S, JWT_ALGORITHMS } from "../access/token.verifier.js";
37
+ const isNonEmpty = (value) => typeof value === "string" && value !== "";
38
+ /** Whether `aud` names the resource alone (with `openid`, the UserInfo endpoint beside it). */
39
+ const forResource = (aud, resource, userInfo) => {
40
+ if (aud === resource) {
41
+ return true;
42
+ }
43
+ return (Array.isArray(aud) &&
44
+ aud.length === 2 &&
45
+ userInfo !== undefined &&
46
+ userInfo !== resource &&
47
+ aud.includes(resource) &&
48
+ aud.includes(userInfo));
49
+ };
50
+ const scopesOf = (scope) => typeof scope === "string"
51
+ ? scope.split(" ").filter((value) => value !== "")
52
+ : undefined;
53
+ const timeOf = (value) => value instanceof Date
54
+ ? value.getTime()
55
+ : typeof value === "string" || typeof value === "number"
56
+ ? new Date(value).getTime()
57
+ : Number.NaN;
58
+ export const createAccessTokenVerifier = (source) => {
59
+ const now = source.now ?? (() => Date.now());
60
+ const keySet = createKeySetCache(source.keys, now);
61
+ /** Who the token's claims name, when the store still says they may act: the user, and the session it was issued in. */
62
+ const live = async (claims) => {
63
+ const { adapter, internalAdapter } = (await source.context());
64
+ const [client, resource, user, session, consents] = await Promise.all([
65
+ adapter.findOne({
66
+ model: "oauthClient",
67
+ where: [{ field: "clientId", value: claims.clientId }],
68
+ }),
69
+ adapter.findOne({
70
+ model: "oauthResource",
71
+ where: [{ field: "identifier", value: source.resource }],
72
+ }),
73
+ internalAdapter.findUserById(claims.sub),
74
+ adapter.findOne({
75
+ model: "session",
76
+ where: [{ field: "id", value: claims.sid }],
77
+ }),
78
+ adapter.findMany({
79
+ model: "oauthConsent",
80
+ where: [
81
+ { field: "clientId", value: claims.clientId },
82
+ { field: "userId", value: claims.sub },
83
+ ],
84
+ }),
85
+ ]);
86
+ if (client === null ||
87
+ client.disabled === true ||
88
+ resource === null ||
89
+ user === null) {
90
+ return null;
91
+ }
92
+ if (session === null ||
93
+ !(timeOf(session.expiresAt) > now()) ||
94
+ (typeof session.kioskDeviceId === "string" &&
95
+ session.kioskDeviceId !== "")) {
96
+ return null;
97
+ }
98
+ if (client.skipConsent !== true &&
99
+ !consents.some((consent) => (consent.referenceId === undefined ||
100
+ consent.referenceId === null ||
101
+ consent.referenceId === "") &&
102
+ Array.isArray(consent.resources) &&
103
+ consent.resources.includes(source.resource) &&
104
+ Array.isArray(consent.scopes) &&
105
+ claims.scopes.every((scope) => consent.scopes.includes(scope)))) {
106
+ return null;
107
+ }
108
+ return { user, session };
109
+ };
110
+ return {
111
+ async verify(token) {
112
+ let payload;
113
+ try {
114
+ ({ payload } = await keySet.withKeys((keys) => jwtVerify(token, keys, {
115
+ typ: "at+jwt",
116
+ issuer: source.issuer,
117
+ algorithms: [...JWT_ALGORITHMS],
118
+ requiredClaims: [
119
+ "sub",
120
+ "aud",
121
+ "exp",
122
+ "iat",
123
+ "azp",
124
+ "client_id",
125
+ "scope",
126
+ "sid",
127
+ ],
128
+ clockTolerance: CLOCK_TOLERANCE_S,
129
+ currentDate: new Date(now()),
130
+ })));
131
+ }
132
+ catch (error) {
133
+ if (error instanceof errors.JOSEError) {
134
+ return null;
135
+ }
136
+ throw error;
137
+ }
138
+ const { sub, aud, azp, client_id: clientId, sid, cnf } = payload;
139
+ const scopes = scopesOf(payload.scope);
140
+ if (!isNonEmpty(sub) ||
141
+ !isNonEmpty(azp) ||
142
+ clientId !== azp ||
143
+ sub === azp ||
144
+ cnf !== undefined ||
145
+ scopes === undefined ||
146
+ !isNonEmpty(sid) ||
147
+ !forResource(aud, source.resource, source.userInfo)) {
148
+ return null;
149
+ }
150
+ const found = await live({
151
+ sub,
152
+ clientId: azp,
153
+ sid,
154
+ scopes,
155
+ });
156
+ if (found === null) {
157
+ return null;
158
+ }
159
+ return {
160
+ identity: { sub, claims: await source.claimsFor(found) },
161
+ grant: { clientId: azp, scopes: new Set(scopes) },
162
+ };
163
+ },
164
+ };
165
+ };
166
+ /**
167
+ * The provider's UserInfo URL, which a token asked with `openid` names as its second
168
+ * audience: Better Auth's base URL (with its base path) and `/oauth2/userinfo`.
169
+ * `undefined` when the base URL follows the request (a dynamic `baseURL`): such a
170
+ * token then names an audience Access cannot know, and is refused.
171
+ */
172
+ export const userInfoAudience = (block, modules) => {
173
+ if (modules.isDynamicBaseURLConfig(block.baseURL)) {
174
+ return undefined;
175
+ }
176
+ const base = modules.getBaseURL(typeof block.baseURL === "string" ? block.baseURL : undefined, typeof block.basePath === "string" ? block.basePath : undefined);
177
+ return base === undefined
178
+ ? undefined
179
+ : `${base.replace(/\/+$/, "")}/oauth2/userinfo`;
180
+ };
181
+ /**
182
+ * `access.mcp` over the MCP endpoint's verifier: the framework's `mcp.authorization`, frozen,
183
+ * with exactly the two members the framework takes.
184
+ */
185
+ export const mcpAuthorizationOf = (agents, server) => Object.freeze({
186
+ servers: Object.freeze([server]),
187
+ async verify(token) {
188
+ if (typeof token !== "string") {
189
+ return null;
190
+ }
191
+ const caller = await agents.verify(token);
192
+ return caller === null
193
+ ? null
194
+ : Object.freeze({
195
+ sub: caller.identity.sub,
196
+ claims: caller.identity.claims,
197
+ clientId: caller.grant.clientId,
198
+ scopes: Object.freeze([...caller.grant.scopes]),
199
+ });
200
+ },
201
+ });
@@ -0,0 +1,62 @@
1
+ /** How a refusal names where the plugins are. */
2
+ type ProviderPlace = {
3
+ /** Where the app lists Better Auth's plugins: `betterAuth.plugins`, `plugins`. */
4
+ readonly at: string;
5
+ /** The Access plugin itself, in a list it is part of (`aventaraAccess()`): it must come before the provider. */
6
+ readonly own?: unknown;
7
+ };
8
+ /** What Access reads the provider's tokens for. */
9
+ type ProviderUse = {
10
+ /** The resources whose tokens Access reads: the MCP endpoint, the service's own audience. */
11
+ readonly resources: readonly string[];
12
+ /** What reads them, as a refusal names it: `mcp`, `audience`, or both. */
13
+ readonly by: string;
14
+ /** With `mcp`: the MCP endpoint, whose agents read and write with Access's scopes. */
15
+ readonly mcp: string | undefined;
16
+ /** With `audience`: the service's own audience. */
17
+ readonly audience: string | undefined;
18
+ /** `mcp.openRegistration`: the provider's open client registration is taken. */
19
+ readonly openRegistration: boolean;
20
+ };
21
+ /**
22
+ * The OAuth provider among `plugins`, checked: present once, a version in range,
23
+ * and the options the tokens Access reads need.
24
+ */
25
+ export declare const checkOAuthProvider: (plugins: readonly unknown[], use: ProviderUse, place: ProviderPlace) => void;
26
+ /**
27
+ * A `before` hook (Better Auth's plugin hook shape) that refuses an authorization
28
+ * request without PKCE: a `code_challenge` made with the SHA-256 method (`code_challenge_method`).
29
+ * It reads the request's parameters as the provider does (a `POST`'s body, else the
30
+ * query; a sign-in resuming the request carries the signed query it began with). A
31
+ * request by reference (`request_uri`) is refused whatever else it carries: the
32
+ * provider would read its parameters from the reference, not from the request.
33
+ */
34
+ export declare const pkceGuard: (modules: {
35
+ readonly createAuthMiddleware: (handler: (context: never) => Promise<unknown>) => unknown;
36
+ readonly APIError: new (status: string, body?: {
37
+ readonly [key: string]: unknown;
38
+ }) => Error;
39
+ }) => {
40
+ matcher: (context: {
41
+ readonly path?: unknown;
42
+ } | undefined) => boolean;
43
+ handler: unknown;
44
+ };
45
+ /**
46
+ * A resource Access reads the provider's tokens for, checked: an absolute https URL
47
+ * (or http on this machine) with no credentials, query or fragment, written as a
48
+ * protected resource's metadata writes it (the origin, then the path with no trailing
49
+ * slash), and not the issuer itself, the audience of the tokens `GET /token` issues.
50
+ */
51
+ export declare const checkResourceUrl: (resource: unknown, at: string, issuer: string) => string;
52
+ /**
53
+ * What Access reads Better Auth's OAuth provider's tokens for, when it reads any.
54
+ * `userInfo` is the provider's UserInfo URL, when Access knows it: no resource Access
55
+ * reads tokens for may be it, since a token asked with `openid` names it beside its
56
+ * resource.
57
+ */
58
+ export declare const oauthUseOf: (mcp: {
59
+ readonly resource: string;
60
+ readonly openRegistration: boolean;
61
+ } | undefined, audience: string | undefined, userInfo: string | undefined) => ProviderUse | undefined;
62
+ export {};
@@ -0,0 +1,261 @@
1
+ // What Access holds Better Auth's OAuth provider (`@better-auth/oauth-provider`) to
2
+ // when it reads the provider's tokens (`mcp`, `audience`), and the one hook it adds
3
+ // to it. The provider is the app's, in Better Auth's plugins with its own options,
4
+ // passed through untouched, as plain Better Auth takes it: Access issues no token and
5
+ // serves no OAuth route. It refuses to start on options that would let a token's
6
+ // authorization fall short of what the MCP authorization specification and OAuth 2.1
7
+ // ask, rather than change them quietly (fail closed, as the GHSA-fmh4 guard does):
8
+ //
9
+ // - the exact provider version Access is built and tested against;
10
+ // - JWT access tokens (`disableJwtPlugin` off): `access.mcp.verify` reads the
11
+ // audience, client and scopes from the token, signed with the keys `jwt()` publishes;
12
+ // - each resource Access reads tokens for (the MCP endpoint, the service's own
13
+ // `audience`) among the provider's `resources`, so a token can name it as its
14
+ // audience (RFC 8707);
15
+ // - `aventara:read` among its `scopes`, the scope a token reads with;
16
+ // - refresh tokens rotated strictly (`refreshTokenReuseInterval: 0`, the provider's
17
+ // own default): a spent refresh token is never answered again, however soon;
18
+ // - exact redirect URI matching (no `validateRedirectUri`), which the provider does
19
+ // by default (RFC 9700, section 2.1; a native client's loopback redirect may use another
20
+ // port, RFC 8252, section 7.3);
21
+ // - no authorization request by reference (`requestUriResolver`): its parameters
22
+ // come from the resolver, never the request the PKCE hook reads;
23
+ // - no consent under a reference (`postLogin.consentReferenceId`): a JWT access token
24
+ // does not carry the reference, so Access could not tell which consent it was
25
+ // given under;
26
+ // - with `audience`, each client linked to the resources it may ask for
27
+ // (`enforcePerClientResources`, the provider's default): a client registered for
28
+ // one resource gets no token for another;
29
+ // - with `mcp` and `audience`, the service's audience never linked to a client as it
30
+ // registers (`clientRegistrationDefaultResources`,
31
+ // `clientRegistrationAllowedResources`): an agent's client that registers itself is
32
+ // for the MCP endpoint alone, and a token it refreshed to the service would pass
33
+ // for the service's own;
34
+ // - no open dynamic client registration (`allowUnauthenticatedClientRegistration`),
35
+ // unless `mcp.openRegistration` takes it: such a client is whoever it says it is.
36
+ // Client ID Metadata Documents (`@better-auth/cimd`) register a client as the domain
37
+ // its document is served from; with the plugin there, a version in Access's range.
38
+ //
39
+ // And, on every authorization request, PKCE with its SHA-256 method (`pkceGuard`): the provider
40
+ // asks it of public clients and most confidential ones, and lets a confidential
41
+ // OpenID request with a `nonce` (or a client created with `require_pkce: false`) go
42
+ // without; while Access reads its tokens, no request does, and none by reference
43
+ // (`request_uri`). The hook refuses a request before the provider runs, as the
44
+ // `service` profile's hook refuses a session-starting route; it issues nothing.
45
+ import { AccessError } from "../access/access.error.js";
46
+ import { normalisedId } from "../access/better-auth.hardening.js";
47
+ import { BETTER_AUTH_RANGE, BETTER_AUTH_RANGE_SAID, inBetterAuthRange, } from "../access/better-auth.range.js";
48
+ import { MCP_READ_SCOPE, MCP_WRITE_SCOPE } from "../mcp/agent.grant.js";
49
+ /** The OAuth provider's plugin id. */
50
+ const OAUTH_PROVIDER_ID = "oauth-provider";
51
+ /** The Client ID Metadata Document plugin's id. */
52
+ const CIMD_ID = "cimd";
53
+ const refuse = (message) => {
54
+ throw new AccessError("ACCESS_OPTIONS_INVALID", message);
55
+ };
56
+ const listOf = (value) => Array.isArray(value) ? value : [];
57
+ /**
58
+ * The OAuth provider among `plugins`, checked: present once, a version in range,
59
+ * and the options the tokens Access reads need.
60
+ */
61
+ export const checkOAuthProvider = (plugins, use, place) => {
62
+ const { at } = place;
63
+ const listed = use.resources.map((resource) => `"${resource}"`).join(", ");
64
+ const found = plugins.filter((plugin) => normalisedId(plugin?.id) ===
65
+ OAUTH_PROVIDER_ID);
66
+ // An agent's client registers itself for the MCP endpoint alone; a client the
67
+ // service's audience takes is linked to it one by one.
68
+ const install = `\`oauthProvider({ loginPage, consentPage, scopes: ["${MCP_READ_SCOPE}", "${MCP_WRITE_SCOPE}", "offline_access"], resources: [${listed}]${use.mcp === undefined ? "" : `, clientRegistrationDefaultResources: ["${use.mcp}"]`} })\` from @better-auth/oauth-provider${use.audience === undefined ? "" : `, and link each client the service takes to "${use.audience}" (\`auth.api.adminLinkClientResource\`)`}`;
69
+ if (found.length === 0) {
70
+ refuse(`\`${use.by}\` reads the access tokens Better Auth's OAuth provider issues, and ${at} has none: add ${install}.`);
71
+ }
72
+ if (found.length > 1) {
73
+ refuse(`${at} holds ${found.length} OAuth providers (plugin id "${OAUTH_PROVIDER_ID}"); Better Auth runs one. Keep one.`);
74
+ }
75
+ const provider = found[0];
76
+ if (!inBetterAuthRange(provider.version)) {
77
+ throw new AccessError("ACCESS_BETTER_AUTH_VERSION", `\`${use.by}\` runs on @better-auth/oauth-provider ${BETTER_AUTH_RANGE_SAID}, and ${at} holds ${typeof provider.version === "string" ? provider.version : "a version Access cannot read"}: npm install @better-auth/oauth-provider@${BETTER_AUTH_RANGE}`);
78
+ }
79
+ if (place.own !== undefined) {
80
+ const own = plugins.indexOf(place.own);
81
+ if (own > plugins.indexOf(provider)) {
82
+ refuse(`${at} lists the OAuth provider before aventaraAccess(): Better Auth starts its plugins in order, and the provider reads, as it starts, the jwt() the Access plugin carries. Put aventaraAccess() first.`);
83
+ }
84
+ }
85
+ const options = provider.options ?? {};
86
+ if (options.disableJwtPlugin === true) {
87
+ refuse(`\`${use.by}\` reads a token's audience, client and scopes from the token, signed with the keys jwt() publishes, and the OAuth provider's \`disableJwtPlugin: true\` issues opaque tokens: remove it.`);
88
+ }
89
+ const resources = Array.isArray(options.resources) ? options.resources : [];
90
+ for (const resource of use.resources) {
91
+ if (!resources.some((entry) => entry === resource ||
92
+ entry?.identifier ===
93
+ resource)) {
94
+ refuse(`the OAuth provider issues a token for ${resource} only when its \`resources\` list it (RFC 8707): add "${resource}" to them.`);
95
+ }
96
+ }
97
+ const scopes = listOf(options.scopes);
98
+ if (!scopes.includes(MCP_READ_SCOPE)) {
99
+ refuse(use.mcp !== undefined
100
+ ? `an agent reads through the MCP endpoint with the scope "${MCP_READ_SCOPE}" (and writes with "${MCP_WRITE_SCOPE}"), and the OAuth provider's \`scopes\` have neither: add them.`
101
+ : `a token addressed to the service reads with the scope "${MCP_READ_SCOPE}" (and writes with "${MCP_WRITE_SCOPE}"), and the OAuth provider's \`scopes\` have neither: add them.`);
102
+ }
103
+ if (options.refreshTokenReuseInterval !== 0) {
104
+ refuse(`the OAuth provider answers a spent refresh token again for ${String(options.refreshTokenReuseInterval)} seconds (\`refreshTokenReuseInterval\`): refresh tokens rotate strictly while Access reads the provider's tokens, so set it to 0.`);
105
+ }
106
+ if (options.validateRedirectUri !== undefined) {
107
+ refuse("the OAuth provider's `validateRedirectUri` accepts redirect URIs the client did not register: while Access reads the provider's tokens, an authorization code goes only to a redirect URI that matches a registered one exactly. Remove it.");
108
+ }
109
+ if (options.requestUriResolver !== undefined) {
110
+ refuse("the OAuth provider's `requestUriResolver` takes an authorization request by reference (`request_uri`), whose parameters Access cannot hold to PKCE: while Access reads the provider's tokens, every authorization request carries its own. Remove it.");
111
+ }
112
+ if (options.postLogin !== undefined &&
113
+ options.postLogin !== null &&
114
+ options.postLogin.consentReferenceId !== undefined) {
115
+ refuse("the OAuth provider's `postLogin.consentReferenceId` stores each consent under a reference, and a JWT access token does not say which: Access could not tell whether the consent a token was issued under still stands. Remove it.");
116
+ }
117
+ if (use.audience !== undefined) {
118
+ if (options.enforcePerClientResources === false) {
119
+ refuse(`the OAuth provider's \`enforcePerClientResources: false\` lets any client ask for a token addressed to the service (${use.audience}): with \`audience\`, a client gets one only when it is linked to it. Remove it.`);
120
+ }
121
+ if (use.mcp !== undefined) {
122
+ for (const option of [
123
+ "clientRegistrationDefaultResources",
124
+ "clientRegistrationAllowedResources",
125
+ ]) {
126
+ if (listOf(options[option]).includes(use.audience)) {
127
+ refuse(`the OAuth provider's \`${option}\` links the service's audience (${use.audience}) to a client as it registers: an agent's client that registers itself would be issued tokens that pass for the service's own. List only the MCP endpoint there ("${use.mcp}"), and link each client the service takes to its audience yourself (\`auth.api.adminLinkClientResource\`).`);
128
+ }
129
+ }
130
+ }
131
+ }
132
+ if (options.allowUnauthenticatedClientRegistration === true &&
133
+ !use.openRegistration) {
134
+ refuse(use.mcp !== undefined
135
+ ? "the OAuth provider's `allowUnauthenticatedClientRegistration: true` registers any client that asks, as whoever it says it is: its name, logo and redirect URI. Prefer Client ID Metadata Documents (`cimd()` from @better-auth/cimd), whose client is the domain its document is served from; to take open registration anyway, set `mcp.openRegistration: true`."
136
+ : "the OAuth provider's `allowUnauthenticatedClientRegistration: true` registers any client that asks, as whoever it says it is, and `audience` takes no such client: register clients signed in, with an initial access token, or by Client ID Metadata Documents (`cimd()` from @better-auth/cimd). Remove it.");
137
+ }
138
+ const cimd = plugins.find((plugin) => normalisedId(plugin?.id) ===
139
+ CIMD_ID);
140
+ if (cimd !== undefined && !inBetterAuthRange(cimd.version)) {
141
+ throw new AccessError("ACCESS_BETTER_AUTH_VERSION", `\`${use.by}\` runs on @better-auth/cimd ${BETTER_AUTH_RANGE_SAID}, and ${at} holds ${typeof cimd.version === "string" ? cimd.version : "a version Access cannot read"}: npm install @better-auth/cimd@${BETTER_AUTH_RANGE}`);
142
+ }
143
+ };
144
+ /** The OAuth provider's authorization endpoint, as Better Auth's hooks see its path. */
145
+ const AUTHORIZE_PATH = "/oauth2/authorize";
146
+ /**
147
+ * A `before` hook (Better Auth's plugin hook shape) that refuses an authorization
148
+ * request without PKCE: a `code_challenge` made with the SHA-256 method (`code_challenge_method`).
149
+ * It reads the request's parameters as the provider does (a `POST`'s body, else the
150
+ * query; a sign-in resuming the request carries the signed query it began with). A
151
+ * request by reference (`request_uri`) is refused whatever else it carries: the
152
+ * provider would read its parameters from the reference, not from the request.
153
+ */
154
+ export const pkceGuard = (modules) => ({
155
+ matcher: (context) => context?.path === AUTHORIZE_PATH,
156
+ handler: modules.createAuthMiddleware(async (given) => {
157
+ const context = given;
158
+ const settings = context.authorizeSettings ?? { isAuthorize: true };
159
+ const source = context.method === "POST" && settings.isAuthorize === true
160
+ ? context.body
161
+ : context.query;
162
+ const params = (typeof source === "object" && source !== null ? source : {});
163
+ const query = (typeof context.query === "object" && context.query !== null
164
+ ? context.query
165
+ : {});
166
+ if (params.request_uri !== undefined || query.request_uri !== undefined) {
167
+ throw new modules.APIError("BAD_REQUEST", {
168
+ error: "invalid_request",
169
+ error_description: "This authorization server takes no request by reference: send the authorization request's parameters, with code_challenge and code_challenge_method=S256.",
170
+ });
171
+ }
172
+ const { code_challenge: challenge, code_challenge_method: method } = params;
173
+ if (typeof challenge !== "string" ||
174
+ challenge === "" ||
175
+ method !== "S256") {
176
+ throw new modules.APIError("BAD_REQUEST", {
177
+ error: "invalid_request",
178
+ error_description: "This authorization server requires PKCE: send code_challenge with code_challenge_method=S256.",
179
+ });
180
+ }
181
+ }),
182
+ });
183
+ /** The hosts a resource served over http may name: this machine. */
184
+ const LOOPBACK_RESOURCE_HOSTS = new Set([
185
+ "localhost",
186
+ "127.0.0.1",
187
+ "[::1]",
188
+ ]);
189
+ /**
190
+ * A resource Access reads the provider's tokens for, checked: an absolute https URL
191
+ * (or http on this machine) with no credentials, query or fragment, written as a
192
+ * protected resource's metadata writes it (the origin, then the path with no trailing
193
+ * slash), and not the issuer itself, the audience of the tokens `GET /token` issues.
194
+ */
195
+ export const checkResourceUrl = (resource, at, issuer) => {
196
+ if (typeof resource !== "string") {
197
+ return refuse(`\`${at}\` is an absolute URL (\`"https://app.example.com/mcp"\`); it was ${JSON.stringify(resource)}.`);
198
+ }
199
+ let url;
200
+ try {
201
+ url = new URL(resource);
202
+ }
203
+ catch {
204
+ return refuse(`\`${at}\` is an absolute URL (\`"https://app.example.com/mcp"\`); it was ${JSON.stringify(resource)}.`);
205
+ }
206
+ if (!(url.protocol === "https:" ||
207
+ (url.protocol === "http:" && LOOPBACK_RESOURCE_HOSTS.has(url.hostname)))) {
208
+ return refuse(`\`${at}\` must be an https URL (or http on this machine: \`localhost\`, \`127.0.0.1\`, \`[::1]\`); it was ${JSON.stringify(resource)}.`);
209
+ }
210
+ if (url.username !== "" ||
211
+ url.password !== "" ||
212
+ resource.includes("?") ||
213
+ resource.includes("#") ||
214
+ url.pathname.includes("//")) {
215
+ return refuse(`\`${at}\` names one resource alone: no credentials, query, fragment or empty path segment; it was ${JSON.stringify(resource)}.`);
216
+ }
217
+ const canonical = `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
218
+ if (resource !== canonical) {
219
+ return refuse(`\`${at}\` must be written as the resource's metadata writes it, which a client sends back verbatim as its resource indicator: ${canonical}`);
220
+ }
221
+ if (canonical === issuer) {
222
+ return refuse(`\`${at}\` is Access's issuer (${issuer}), the audience of the tokens \`GET /token\` issues: a token for one must never pass for the other. Name the resource by its own path (\`${issuer}/mcp\`, \`${issuer}/api\`).`);
223
+ }
224
+ return canonical;
225
+ };
226
+ /**
227
+ * What Access reads Better Auth's OAuth provider's tokens for, when it reads any.
228
+ * `userInfo` is the provider's UserInfo URL, when Access knows it: no resource Access
229
+ * reads tokens for may be it, since a token asked with `openid` names it beside its
230
+ * resource.
231
+ */
232
+ export const oauthUseOf = (mcp, audience, userInfo) => {
233
+ if (mcp === undefined && audience === undefined) {
234
+ return undefined;
235
+ }
236
+ if (mcp !== undefined && mcp.resource === audience) {
237
+ throw new AccessError("ACCESS_OPTIONS_INVALID", "`audience` is the MCP endpoint (`mcp.resource`): an agent's token would pass for the service's own. Give the service its own URL.");
238
+ }
239
+ for (const [at, resource] of [
240
+ ["mcp.resource", mcp?.resource],
241
+ ["audience", audience],
242
+ ]) {
243
+ if (resource !== undefined && resource === userInfo) {
244
+ throw new AccessError("ACCESS_OPTIONS_INVALID", `\`${at}\` is the OAuth provider's UserInfo endpoint (${userInfo}), which every token asked with \`openid\` names beside its own resource: such a token would pass for it. Name the resource by its own path.`);
245
+ }
246
+ }
247
+ return {
248
+ resources: [
249
+ ...(mcp === undefined ? [] : [mcp.resource]),
250
+ ...(audience === undefined ? [] : [audience]),
251
+ ],
252
+ by: mcp !== undefined && audience !== undefined
253
+ ? "mcp` and `audience"
254
+ : mcp !== undefined
255
+ ? "mcp"
256
+ : "audience",
257
+ mcp: mcp?.resource,
258
+ audience,
259
+ openRegistration: mcp?.openRegistration === true,
260
+ };
261
+ };