@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,196 @@
1
+ // A request's state, `{ me }`: who the request's bearer token or session cookie says
2
+ // the caller is, verified, and mapped to the app's principal. `createAccess`'s
3
+ // `state` and `accessOf(auth).state` are this one function over their own Better
4
+ // Auth instance, so both ways in read a request alike: the bearer token against the
5
+ // key set, never a cookie beside an `Authorization` header, and a session cookie only
6
+ // from a site Better Auth trusts.
7
+ //
8
+ // A bearer token may also be an access token Better Auth's OAuth provider issued for
9
+ // one resource (`typ: "at+jwt"`), read by the verifier of that resource alone
10
+ // (`oauth/access-token.verifier.ts`), never as a session token:
11
+ //
12
+ // - with `mcp`, one for the MCP endpoint: an agent's, whose principal carries what the
13
+ // token grants (`mcp/agent.grant.ts`), which the policy holds it to before the rules
14
+ // (`mcp/agent-scope.gate.ts`), and which `agentOf(me)` reads;
15
+ // - with `audience` (the service's opt-in), one addressed to this service, and carrying
16
+ // one of Access's scopes (`aventara:read`, `aventara:write`): the user's, held to
17
+ // those scopes under every origin. One with neither is nobody's. The service then
18
+ // takes no other bearer token: the one-org token `GET /token` issues (its audience
19
+ // the issuer) is refused.
20
+ import { decodeProtectedHeader } from "jose";
21
+ import { mayChange, SESSION_USE } from "../kiosk/session-use.marker.js";
22
+ import { hasAventaraScope, withGrant, } from "../mcp/agent.grant.js";
23
+ import { carrySubject } from "./principal.subject.js";
24
+ import { readsSessionCookie, } from "./profile.policy.js";
25
+ import { isAccessTokenType } from "./token.verifier.js";
26
+ const bearerOf = (headers) => /^Bearer[ \t]+([^ \t]+)[ \t]*$/i.exec(headers.get("authorization") ?? "")?.[1];
27
+ /**
28
+ * Whether a request may be read by its session cookie, where the browser could have
29
+ * sent it from another site: its `Origin` is one Better Auth trusts, or, without
30
+ * one, the browser says it is not cross-site (`Sec-Fetch-Site` `same-origin` or
31
+ * `none`). A request with neither header is no browser's, and passes.
32
+ */
33
+ const fromTrustedSite = (headers, trusted) => {
34
+ const origin = headers.get("origin");
35
+ if (origin !== null) {
36
+ return origin !== "null" && trusted(origin);
37
+ }
38
+ const site = headers.get("sec-fetch-site");
39
+ return site === null || site === "same-origin" || site === "none";
40
+ };
41
+ /** The methods that change nothing: a cookie on them is read whatever site sent it (with `sameSite: "lax"`, a browser sends it from another site only on a top-level `GET`). */
42
+ const SAFE_METHODS = new Set(["GET", "HEAD", "OPTIONS"]);
43
+ /** The method the client sent, from the host's request (Express, Fastify or a Web `Request`); `undefined` when there is none. */
44
+ const methodOf = (request) => {
45
+ const { method, originalMethod } = (request ?? {});
46
+ // Express's method-override keeps the method the client sent here.
47
+ const sent = typeof originalMethod === "string" ? originalMethod : method;
48
+ return typeof sent === "string" ? sent.toUpperCase() : undefined;
49
+ };
50
+ const headersOf = (given) => {
51
+ const headers = new Headers();
52
+ given.forEach((value, name) => {
53
+ headers.append(name, value);
54
+ });
55
+ return headers;
56
+ };
57
+ /** Whether a JWT's header names an OAuth access token; `false` for anything that is not a JWT. */
58
+ const isAccessToken = (token) => {
59
+ try {
60
+ return isAccessTokenType(decodeProtectedHeader(token).typ);
61
+ }
62
+ catch {
63
+ return false;
64
+ }
65
+ };
66
+ /**
67
+ * Who Access's own `GET /me` route answers a request as: the identity of a session
68
+ * token, a session cookie or a token addressed to the service; `null` for nobody, and
69
+ * for an agent's access token, which opens nothing over HTTP but the MCP endpoint.
70
+ */
71
+ export const meOf = (caller) => caller === null || caller.grant?.via === "mcp" ? null : caller.identity;
72
+ /**
73
+ * Who `access.client.verbs.me` answers a request as: the identity `{ sub, claims }`
74
+ * of any credential the state reads, an agent's included (the framework's MCP `me`
75
+ * tool calls it in-process with the agent's headers). A token's identity is a copy
76
+ * carrying what it grants, so `agentOf` tells an agent's apart.
77
+ */
78
+ export const verbMeOf = (caller) => caller === null
79
+ ? null
80
+ : caller.grant === undefined
81
+ ? caller.identity
82
+ : withGrant(caller.identity, caller.grant);
83
+ /**
84
+ * A request's identity over one Better Auth instance (`auth`, built on first use):
85
+ * from a bearer token the verifier accepts (an agent's access token, the agent
86
+ * verifier, when `mcp` is on), or (profile `web`) from the session Better Auth's
87
+ * store holds for the request's cookie, with the claims `claimsFor` gives that
88
+ * session.
89
+ */
90
+ export const requestIdentity = (input) => {
91
+ const { auth, verifier, agents, audience, kind, crossSite, claimsFor } = input;
92
+ const identityOf = async (headers, request) => {
93
+ // An Authorization header names the credential: a bearer token, or nothing
94
+ // Access reads. The session cookie beside it is not a fallback.
95
+ if (headers.has("authorization")) {
96
+ const token = bearerOf(headers);
97
+ if (token === undefined) {
98
+ return null;
99
+ }
100
+ // An access token is read by the verifiers of the resources this instance
101
+ // reads tokens for, never by the session verifier (which refuses one too).
102
+ if (isAccessToken(token)) {
103
+ const agent = await agents?.verify(token);
104
+ if (agent !== undefined && agent !== null) {
105
+ return {
106
+ identity: agent.identity,
107
+ grant: { ...agent.grant, via: "mcp" },
108
+ };
109
+ }
110
+ const own = await audience?.verify(token);
111
+ // Scopes narrow, never widen: a token addressed to the service with none of
112
+ // Access's scopes grants nothing, and is nobody's.
113
+ return own === undefined ||
114
+ own === null ||
115
+ !hasAventaraScope(own.grant.scopes)
116
+ ? null
117
+ : {
118
+ identity: own.identity,
119
+ grant: { ...own.grant, via: "audience" },
120
+ };
121
+ }
122
+ // A service that opts in to its own audience takes no one-org token.
123
+ if (audience !== undefined) {
124
+ return null;
125
+ }
126
+ const identity = await verifier.verify(token);
127
+ return identity === null ? null : { identity };
128
+ }
129
+ if (!readsSessionCookie(kind) || !headers.has("cookie")) {
130
+ return null;
131
+ }
132
+ // A cookie counts only from a site Better Auth trusts: on every request that
133
+ // may change something (any method but GET, HEAD and OPTIONS, and a request
134
+ // whose method the host did not pass), and with `sameSite: "none"`, on all.
135
+ const method = methodOf(request);
136
+ if (crossSite || method === undefined || !SAFE_METHODS.has(method)) {
137
+ const context = (await auth().$context);
138
+ if (!fromTrustedSite(headers, (origin) => context.isTrustedOrigin(origin))) {
139
+ return null;
140
+ }
141
+ }
142
+ // The store, never the cookie cache: a revoked session is gone at once.
143
+ const session = await auth().api.getSession({
144
+ headers: headersOf(headers),
145
+ query: { disableCookieCache: true },
146
+ // A write is the till's use of its session (the kiosk's idle timer); a read is not.
147
+ ...(mayChange(method) ? { [SESSION_USE]: true } : {}),
148
+ });
149
+ if (session === null) {
150
+ return null;
151
+ }
152
+ return {
153
+ identity: { sub: session.user.id, claims: await claimsFor(session) },
154
+ };
155
+ };
156
+ return identityOf;
157
+ };
158
+ /**
159
+ * The request state over a request's identity (`requestIdentity`): `me` is the
160
+ * identity, or what the app's `principal` maps it to, with the `sub` carried on it
161
+ * for the audit, and, for an OAuth access token, a copy of it carrying what the
162
+ * token grants. The identity `principal` is given for such a token carries the grant
163
+ * too, so `agentOf(identity)` tells the app an agent's request while it maps it.
164
+ */
165
+ export const requestState = (identityOf, principal) => {
166
+ return async ({ headers, request }) => {
167
+ const caller = await identityOf(headers, request);
168
+ if (caller === null) {
169
+ return { me: null };
170
+ }
171
+ const { grant } = caller;
172
+ const identity = grant === undefined
173
+ ? caller.identity
174
+ : withGrant(caller.identity, grant);
175
+ const mapped = principal === undefined ? identity : await principal(identity);
176
+ const me = grant === undefined ? mapped : withGrant(mapped, grant);
177
+ // The `sub` travels with `me`: whichever instance the host asks for the
178
+ // policy (under Next, another bundle's) reads it for the audit's `userId`.
179
+ carrySubject(me, identity.sub);
180
+ return { me: me ?? null };
181
+ };
182
+ };
183
+ /**
184
+ * Refuses a token for a session the store no longer has (revoked, signed out,
185
+ * expired): with Better Auth's cookie cache on, a session read from its cookie may
186
+ * have been revoked since, so a token is issued only for one the store still has.
187
+ */
188
+ export const sessionStillLive = async (store, session, refuse) => {
189
+ const { token } = session;
190
+ const found = typeof token === "string"
191
+ ? await (await store()).internalAdapter.findSession(token)
192
+ : null;
193
+ if (found === null || found.session.expiresAt.getTime() <= Date.now()) {
194
+ throw refuse();
195
+ }
196
+ };
@@ -0,0 +1,114 @@
1
+ import { type AuditLog, type SignInIdentifier } from "../audit/audit.log.js";
2
+ import type { BetterAuthModules } from "./better-auth.loader.js";
3
+ import { type AccessProfileKind } from "./profile.policy.js";
4
+ /** A session row, as Better Auth's database hooks hand it over. */
5
+ type HookSession = {
6
+ readonly userId?: unknown;
7
+ } | null | undefined;
8
+ /** The endpoint context Better Auth hands a hook's matcher and its database hooks. */
9
+ type HookContext = {
10
+ readonly request?: unknown;
11
+ readonly path?: unknown;
12
+ } | null | undefined;
13
+ /** Where Better Auth's context names its session cookies. */
14
+ type CookieContext = {
15
+ readonly authCookies?: {
16
+ readonly sessionToken?: {
17
+ readonly name?: unknown;
18
+ };
19
+ readonly sessionData?: {
20
+ readonly name?: unknown;
21
+ };
22
+ };
23
+ };
24
+ /** The part of a response's headers the service profile's cookie strip reads and changes. */
25
+ type ResponseCookies = {
26
+ getSetCookie(): string[];
27
+ delete(name: string): void;
28
+ append(name: string, value: string): void;
29
+ };
30
+ /** The session cookies' bare names: the token's, and the cookie cache's, which carries the session too. */
31
+ export declare const sessionCookies: (context: CookieContext) => readonly string[];
32
+ /**
33
+ * The service profile's last word on what `access.handler` answers, after Better
34
+ * Auth's router and every plugin's `onResponse`: no session cookie (`names`,
35
+ * through `sessionCookies`), whatever set it. The headers are changed in place; a
36
+ * response whose headers cannot change is answered with a bare 404 instead.
37
+ */
38
+ export declare const withoutSessionCookies: (response: {
39
+ readonly headers: ResponseCookies;
40
+ }, names: readonly string[]) => {
41
+ readonly headers: ResponseCookies;
42
+ };
43
+ /**
44
+ * The identifier a sign-in names, read as Better Auth 1.7.12 to 1.7.14 reads it on that route
45
+ * and nowhere else (any other field of the body is the caller's to fill): an email
46
+ * lower-cased, as `/sign-in/email` and `/sign-in/email-otp` look it up; a username
47
+ * through the `username()` plugin's `usernameNormalization` (lower-cased by default,
48
+ * as given with `false`); a phone number as given, as `/sign-in/phone-number` looks
49
+ * it up.
50
+ */
51
+ export declare const signInIdentifier: (plugins: readonly unknown[] | (() => readonly unknown[])) => SignInIdentifier;
52
+ /**
53
+ * The Access plugin's members for sessions (`init` with its database hooks, and
54
+ * `hooks`), for the profile and the audit log; none when neither needs them.
55
+ */
56
+ export declare const sessionWatch: (input: {
57
+ readonly kind: AccessProfileKind;
58
+ readonly auditLog: AuditLog | undefined;
59
+ readonly modules: Pick<BetterAuthModules, "createAuthMiddleware" | "APIError">;
60
+ /** The app's Better Auth plugins (or, for the Access plugin, a read of them once Better Auth starts it): how they read a sign-in's identifier. */
61
+ readonly plugins: readonly unknown[] | (() => readonly unknown[]);
62
+ /** The kiosk profile's own step before a session is created (`kioskPlugin`'s `sessionCreating`): it binds one on an enrolled device. */
63
+ readonly sessionCreating?: (session: {
64
+ readonly kioskDeviceId?: unknown;
65
+ } | null | undefined, context: HookContext) => Promise<false | {
66
+ readonly data: object;
67
+ } | undefined>;
68
+ /** The kiosk profile's own step once a session is created (`kioskPlugin`'s `sessionCreated`). */
69
+ readonly sessionCreated?: (session: HookSession, context: HookContext) => Promise<void>;
70
+ }) => {
71
+ init?: never;
72
+ hooks?: never;
73
+ } | {
74
+ init: () => {
75
+ options: {
76
+ databaseHooks: {
77
+ session: {
78
+ delete?: {
79
+ after: (session: {
80
+ readonly userId?: unknown;
81
+ readonly impersonatedBy?: unknown;
82
+ } | null | undefined, context: {
83
+ readonly request?: unknown;
84
+ readonly path?: unknown;
85
+ readonly body?: unknown;
86
+ readonly context?: {
87
+ readonly secret?: unknown;
88
+ } | undefined;
89
+ } | null | undefined) => Promise<void>;
90
+ };
91
+ create: {
92
+ after?: (session: HookSession, context: HookContext) => Promise<void>;
93
+ before?: (session: {
94
+ readonly kioskDeviceId?: unknown;
95
+ } | null | undefined, context: HookContext) => Promise<false | {
96
+ readonly data: object;
97
+ } | undefined>;
98
+ };
99
+ };
100
+ };
101
+ };
102
+ };
103
+ hooks: {
104
+ before: {
105
+ matcher: (ctx: HookContext) => boolean;
106
+ handler: unknown;
107
+ }[];
108
+ after: {
109
+ matcher: (ctx: HookContext) => boolean;
110
+ handler: unknown;
111
+ }[];
112
+ };
113
+ };
114
+ export {};
@@ -0,0 +1,258 @@
1
+ // The Better Auth hooks the Access plugin installs over sessions: the service
2
+ // profile's refusals, and the audit log's events from Better Auth's request path.
3
+ //
4
+ // - Service profile: no request through `access.handler` gets a session. A route
5
+ // that starts one (`startsSession`, by Better Auth's own endpoint path, `ctx.path`)
6
+ // is refused with a 404 before it runs. Any other route (a plugin's the list does
7
+ // not know) cannot create one either: Better Auth's `session.create` before-hook
8
+ // refuses the row, so no session exists even for a moment, and once the endpoint
9
+ // has answered, the answer is replaced by a bare 404, its cookies and headers
10
+ // dropped. So is an answer that sets the session cookie of a session that already
11
+ // existed. And `access.handler` itself strips any session cookie from what Better
12
+ // Auth answers (`withoutSessionCookies`), after every plugin's `onResponse`, which
13
+ // also covers a route that returns its own `Response` (Better Auth runs no after
14
+ // hook on one). A route that puts a session token in a JSON body is
15
+ // not covered: no built-in plugin does. The server's own calls to Better Auth's
16
+ // API with no request sign users in as before.
17
+ // - Audit: sign-ins and sign-outs from Better Auth's session hooks, refused sign-ins
18
+ // and issued tokens from its endpoint hooks (`audit.log.ts` says what each event
19
+ // holds).
20
+ import { endpointAudit, sessionAuditHooks, } from "../audit/audit.log.js";
21
+ import { KIOSK_SWITCH_PATH } from "../kiosk/kiosk-verbs.schema.js";
22
+ import { servesRoute, startsSession, } from "./profile.policy.js";
23
+ /** Whether the call came through `access.handler`: a request, not the server's own API call. */
24
+ const fromOutside = (context) => typeof context?.request === "object" && context.request !== null;
25
+ /** A cookie name as a browser could match it to Better Auth's: no case, no `__Secure-`/`__Host-` prefix. */
26
+ const bareCookie = (name) => name
27
+ .trim()
28
+ .toLowerCase()
29
+ .replace(/^__(?:secure|host)-/, "");
30
+ /** The session cookies' bare names: the token's, and the cookie cache's, which carries the session too. */
31
+ export const sessionCookies = (context) => {
32
+ const names = [];
33
+ for (const name of [
34
+ context.authCookies?.sessionToken?.name,
35
+ context.authCookies?.sessionData?.name,
36
+ ]) {
37
+ if (typeof name === "string") {
38
+ names.push(bareCookie(name));
39
+ }
40
+ }
41
+ return names;
42
+ };
43
+ /** The bare name a `Set-Cookie` line sets, and whether it sets a value (not one that deletes it). */
44
+ const cookieSet = (line) => {
45
+ const [pair = ""] = line.split(";");
46
+ const equals = pair.indexOf("=");
47
+ return {
48
+ name: bareCookie(equals < 0 ? pair : pair.slice(0, equals)),
49
+ value: equals >= 0 && pair.slice(equals + 1).trim() !== "",
50
+ };
51
+ };
52
+ /**
53
+ * Whether a cookie's bare name is a session cookie's: one of `names`, or the
54
+ * `multiSession()` plugin's `<session_token>_multi-<token>`.
55
+ */
56
+ const isSessionCookie = (name, names) => {
57
+ for (const session of names) {
58
+ if (name === session || name.startsWith(`${session}_multi-`)) {
59
+ return true;
60
+ }
61
+ }
62
+ return false;
63
+ };
64
+ /** Whether response headers set one of `names` to a value. */
65
+ const setsCookie = (headers, names) => {
66
+ for (const line of headers?.getSetCookie() ?? []) {
67
+ const set = cookieSet(line);
68
+ if (set.value && isSessionCookie(set.name, names)) {
69
+ return true;
70
+ }
71
+ }
72
+ return false;
73
+ };
74
+ /**
75
+ * The service profile's last word on what `access.handler` answers, after Better
76
+ * Auth's router and every plugin's `onResponse`: no session cookie (`names`,
77
+ * through `sessionCookies`), whatever set it. The headers are changed in place; a
78
+ * response whose headers cannot change is answered with a bare 404 instead.
79
+ */
80
+ export const withoutSessionCookies = (response, names) => {
81
+ const lines = response.headers.getSetCookie();
82
+ const kept = [];
83
+ for (const line of lines) {
84
+ if (!isSessionCookie(cookieSet(line).name, names)) {
85
+ kept.push(line);
86
+ }
87
+ }
88
+ if (kept.length === lines.length) {
89
+ return response;
90
+ }
91
+ try {
92
+ response.headers.delete("set-cookie");
93
+ for (const line of kept) {
94
+ response.headers.append("set-cookie", line);
95
+ }
96
+ return response;
97
+ }
98
+ catch {
99
+ return new Response(null, { status: 404 });
100
+ }
101
+ };
102
+ /** The sign-in routes that name their user, by the body field each reads it from. */
103
+ const IDENTIFIED = {
104
+ "/sign-in/email": "email",
105
+ "/sign-in/email-otp": "email",
106
+ "/sign-in/username": "username",
107
+ "/sign-in/phone-number": "phoneNumber",
108
+ };
109
+ /**
110
+ * The identifier a sign-in names, read as Better Auth 1.7.12 to 1.7.14 reads it on that route
111
+ * and nowhere else (any other field of the body is the caller's to fill): an email
112
+ * lower-cased, as `/sign-in/email` and `/sign-in/email-otp` look it up; a username
113
+ * through the `username()` plugin's `usernameNormalization` (lower-cased by default,
114
+ * as given with `false`); a phone number as given, as `/sign-in/phone-number` looks
115
+ * it up.
116
+ */
117
+ export const signInIdentifier = (plugins) => {
118
+ let normalization;
119
+ let read = false;
120
+ /** The `username()` plugin's normalization, read once: the plugins known now, or (the Access plugin) when Better Auth starts it. */
121
+ const normalizationOf = () => {
122
+ if (!read) {
123
+ read = true;
124
+ for (const plugin of typeof plugins === "function"
125
+ ? plugins()
126
+ : plugins) {
127
+ const { id, options } = (plugin ?? {});
128
+ if (id === "username") {
129
+ normalization = options?.usernameNormalization;
130
+ }
131
+ }
132
+ }
133
+ return normalization;
134
+ };
135
+ if (typeof plugins !== "function") {
136
+ normalizationOf();
137
+ }
138
+ return (path, body) => {
139
+ const field = typeof path === "string" ? IDENTIFIED[path] : undefined;
140
+ const value = field === undefined
141
+ ? undefined
142
+ : body?.[field];
143
+ if (field === undefined || typeof value !== "string") {
144
+ return undefined;
145
+ }
146
+ if (field !== "username") {
147
+ return `${field}:${field === "email" ? value.toLowerCase() : value}`;
148
+ }
149
+ try {
150
+ const normalized = normalizationOf() === false
151
+ ? value
152
+ : typeof normalizationOf() === "function"
153
+ ? normalizationOf()(value)
154
+ : value.toLowerCase();
155
+ return typeof normalized === "string"
156
+ ? `username:${normalized}`
157
+ : undefined;
158
+ }
159
+ catch {
160
+ return undefined;
161
+ }
162
+ };
163
+ };
164
+ /**
165
+ * The Access plugin's members for sessions (`init` with its database hooks, and
166
+ * `hooks`), for the profile and the audit log; none when neither needs them.
167
+ */
168
+ export const sessionWatch = (input) => {
169
+ const { kind, auditLog, modules, sessionCreating, sessionCreated } = input;
170
+ const service = kind === "service";
171
+ if (!service &&
172
+ auditLog === undefined &&
173
+ sessionCreated === undefined &&
174
+ sessionCreating === undefined) {
175
+ return {};
176
+ }
177
+ const identify = signInIdentifier(input.plugins);
178
+ /** The requests through the handler whose session the service profile refused. */
179
+ const refused = new WeakSet();
180
+ const audit = auditLog === undefined ? undefined : sessionAuditHooks(auditLog, identify);
181
+ const created = audit === undefined && sessionCreated === undefined
182
+ ? undefined
183
+ : async (session, context) => {
184
+ await audit?.session.create.after(session, context);
185
+ await sessionCreated?.(session, context);
186
+ };
187
+ const databaseHooks = {
188
+ session: {
189
+ create: {
190
+ ...(service
191
+ ? {
192
+ before: async (_session, context) => {
193
+ if (!fromOutside(context)) {
194
+ return undefined;
195
+ }
196
+ refused.add(context.request);
197
+ return false;
198
+ },
199
+ }
200
+ : sessionCreating === undefined
201
+ ? {}
202
+ : { before: sessionCreating }),
203
+ ...(created === undefined ? {} : { after: created }),
204
+ },
205
+ ...(audit === undefined ? {} : { delete: audit.session.delete }),
206
+ },
207
+ };
208
+ const notFound = () => new modules.APIError("NOT_FOUND");
209
+ const before = service
210
+ ? [
211
+ {
212
+ matcher: (ctx) => fromOutside(ctx) && !servesRoute(kind, ctx.path),
213
+ handler: modules.createAuthMiddleware(async () => {
214
+ throw notFound();
215
+ }),
216
+ },
217
+ ]
218
+ : [];
219
+ const after = [
220
+ ...(auditLog === undefined
221
+ ? []
222
+ : [
223
+ {
224
+ // The kiosk's PIN switch logs its own refusals, with their device.
225
+ matcher: (ctx) => (startsSession(ctx?.path) && ctx?.path !== KIOSK_SWITCH_PATH) ||
226
+ ctx?.path === "/token",
227
+ handler: modules.createAuthMiddleware(endpointAudit(auditLog, startsSession, identify)),
228
+ },
229
+ ]),
230
+ ...(service
231
+ ? [
232
+ {
233
+ matcher: fromOutside,
234
+ handler: modules.createAuthMiddleware(async ({ request, context }) => {
235
+ const headers = context.responseHeaders;
236
+ const handsOutSession = (typeof request === "object" &&
237
+ request !== null &&
238
+ refused.has(request)) ||
239
+ setsCookie(headers, sessionCookies(context));
240
+ if (!handsOutSession) {
241
+ return undefined;
242
+ }
243
+ for (const name of [...(headers?.keys() ?? [])]) {
244
+ headers?.delete(name);
245
+ }
246
+ // A Response, not an error: Better Auth keeps the status an
247
+ // endpoint refused with over an after hook's error.
248
+ return new Response(null, { status: 404 });
249
+ }),
250
+ },
251
+ ]
252
+ : []),
253
+ ];
254
+ return {
255
+ init: () => ({ options: { databaseHooks } }),
256
+ hooks: { before, after },
257
+ };
258
+ };
@@ -0,0 +1,35 @@
1
+ import { type JSONWebKeySet } from "jose";
2
+ import { type AccessClaimsDeclaration } from "./claims.mapper.js";
3
+ /** Where the key set comes from, and what a token must state. */
4
+ type TokenVerifierSource = {
5
+ /** The published key set: `{ keys: [...] }`. */
6
+ readonly keys: () => Promise<JSONWebKeySet>;
7
+ readonly claims: AccessClaimsDeclaration;
8
+ /** The `iss` and the `aud` every token carries: the instance's fixed issuer. */
9
+ readonly issuer: string;
10
+ /** How long a token lives, in seconds: one issued longer ago is refused. */
11
+ readonly tokenTtl: number;
12
+ /** Milliseconds since the epoch; a test passes its own clock. */
13
+ readonly now?: () => number;
14
+ };
15
+ export type TokenVerifier = {
16
+ /** The identity a token proves, or `null` for a token that is not valid, current and Access's own. */
17
+ verify(token: string): Promise<{
18
+ readonly sub: string;
19
+ readonly claims: {
20
+ readonly [claim: string]: string | number | boolean;
21
+ };
22
+ } | null>;
23
+ };
24
+ /** The clock skew allowed between the issuer and the verifier, in seconds. */
25
+ export declare const CLOCK_TOLERANCE_S = 5;
26
+ /**
27
+ * The algorithms a token may be signed with: those Better Auth's `jwt()` signs with
28
+ * (its `keyPairConfig.alg`), all asymmetric. A token signed otherwise is refused
29
+ * before its key is looked up, whatever key the set holds.
30
+ */
31
+ export declare const JWT_ALGORITHMS: readonly ["EdDSA", "ES256", "ES512", "PS256", "RS256"];
32
+ /** Whether a JWT header's `typ` names an OAuth access token (RFC 9068, section 2.1: `at+jwt`, or `application/at+jwt`). */
33
+ export declare const isAccessTokenType: (typ: unknown) => boolean;
34
+ export declare const createTokenVerifier: (source: TokenVerifierSource) => TokenVerifier;
35
+ export {};
@@ -0,0 +1,69 @@
1
+ // Verifies the JWTs Better Auth's `jwt()` plugin signs, against the key set it
2
+ // publishes (`key-set.cache.ts`: read once and kept, read again when stale or when a
3
+ // token names a key the set does not hold, the last good set kept while the key
4
+ // store is down).
5
+ //
6
+ // A token is Access's own only when it carries the instance's issuer and audience
7
+ // (the one string `createAccess` fixed, never a list holding it), `sub`, `exp` and
8
+ // `iat`, and is no older than `tokenTtl`, nor issued in the future (a few seconds of
9
+ // clock skew aside). An access token Better Auth's OAuth provider issued (`typ:
10
+ // "at+jwt"`, RFC 9068) is never one, whatever it carries: it is for one resource
11
+ // alone (`oauth/access-token.verifier.ts`).
12
+ import { errors, jwtVerify } from "jose";
13
+ import { acceptedClaims, } from "./claims.mapper.js";
14
+ import { createKeySetCache } from "./key-set.cache.js";
15
+ /** The clock skew allowed between the issuer and the verifier, in seconds. */
16
+ export const CLOCK_TOLERANCE_S = 5;
17
+ /**
18
+ * The algorithms a token may be signed with: those Better Auth's `jwt()` signs with
19
+ * (its `keyPairConfig.alg`), all asymmetric. A token signed otherwise is refused
20
+ * before its key is looked up, whatever key the set holds.
21
+ */
22
+ export const JWT_ALGORITHMS = Object.freeze([
23
+ "EdDSA",
24
+ "ES256",
25
+ "ES512",
26
+ "PS256",
27
+ "RS256",
28
+ ]);
29
+ /** Whether a JWT header's `typ` names an OAuth access token (RFC 9068, section 2.1: `at+jwt`, or `application/at+jwt`). */
30
+ export const isAccessTokenType = (typ) => typeof typ === "string" && /^(?:application\/)?at\+jwt$/i.test(typ.trim());
31
+ export const createTokenVerifier = (source) => {
32
+ const now = source.now ?? (() => Date.now());
33
+ const keySet = createKeySetCache(source.keys, now);
34
+ return {
35
+ async verify(token) {
36
+ let verified;
37
+ try {
38
+ verified = await keySet.withKeys((keys) => jwtVerify(token, keys, {
39
+ requiredClaims: ["sub", "exp", "iat"],
40
+ issuer: source.issuer,
41
+ algorithms: [...JWT_ALGORITHMS],
42
+ audience: source.issuer,
43
+ maxTokenAge: source.tokenTtl,
44
+ clockTolerance: CLOCK_TOLERANCE_S,
45
+ currentDate: new Date(now()),
46
+ }));
47
+ }
48
+ catch (error) {
49
+ if (error instanceof errors.JOSEError) {
50
+ return null;
51
+ }
52
+ throw error;
53
+ }
54
+ const { payload, protectedHeader } = verified;
55
+ if (isAccessTokenType(protectedHeader.typ)) {
56
+ return null;
57
+ }
58
+ const { sub, aud } = payload;
59
+ const claims = acceptedClaims(payload, source.claims);
60
+ if (typeof sub !== "string" ||
61
+ sub === "" ||
62
+ aud !== source.issuer ||
63
+ claims === undefined) {
64
+ return null;
65
+ }
66
+ return { sub, claims };
67
+ },
68
+ };
69
+ };