@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,133 @@
1
+ // `aventaraAccessClient()`: the client plugin of `aventaraAccess()`, for Better
2
+ // Auth's `createAuthClient({ plugins: [...] })`. It adds `authClient.access.can`,
3
+ // which asks the framework, not Better Auth: one `POST {api}/_access/can` for the
4
+ // whole list of questions, answered by the same engine that decides the real calls
5
+ // (`accessOf(auth).policy`, with the row looked up within the caller's scope), one
6
+ // boolean per question. So the two paths have one evaluator. The answer only shapes
7
+ // the UI: the real call is still checked.
8
+ //
9
+ // It sends Better Auth's own fetch, so the session cookie (or the bearer token the
10
+ // app gives Better Auth's client) goes with it, and answers as Better Auth's client
11
+ // actions do, `{ data, error }`. It names no Node.js API and loads nothing from
12
+ // `better-auth` at runtime: it runs in the browser.
13
+ /** The framework protocol version the questions are asked under. */
14
+ const PROTOCOL_VERSION = "1";
15
+ /** A protocol envelope's code, when a body is one. */
16
+ const codeOf = (body) => {
17
+ const code = body?.code;
18
+ return typeof code === "string" ? code : undefined;
19
+ };
20
+ const failure = (status, statusText, body) => {
21
+ const code = codeOf(body);
22
+ const message = body?.message;
23
+ return {
24
+ data: null,
25
+ error: {
26
+ status,
27
+ statusText,
28
+ ...(code === undefined ? {} : { code }),
29
+ ...(typeof message === "string" ? { message } : {}),
30
+ },
31
+ };
32
+ };
33
+ /** Whether every answer is a boolean. */
34
+ const allBooleans = (answers) => {
35
+ for (const answer of answers) {
36
+ if (typeof answer !== "boolean") {
37
+ return false;
38
+ }
39
+ }
40
+ return true;
41
+ };
42
+ /** A Better Auth fetch error as `can` reports it. */
43
+ const fetchFailure = (error) => {
44
+ const { status, statusText } = (error ?? {});
45
+ return failure(typeof status === "number" ? status : 0, typeof statusText === "string" ? statusText : "", error);
46
+ };
47
+ /**
48
+ * The client plugin: `createAuthClient({ plugins: [aventaraAccessClient({ api: "/api" })] })`
49
+ * gives `authClient.access.can([{ resource, action, where? }, ...])`.
50
+ */
51
+ export const aventaraAccessClient = (options) => {
52
+ const { api, contractHash: given } = options ?? {};
53
+ if (typeof api !== "string" || api === "") {
54
+ throw new TypeError('Aventara Access: aventaraAccessClient takes `{ api }`, the framework\'s API path or URL ("/api").');
55
+ }
56
+ return {
57
+ id: "aventara-access",
58
+ $InferServerPlugin: {},
59
+ pathMethods: { "/jwks": "GET" },
60
+ getActions: ($fetch, _$store, clientOptions) => {
61
+ const base = clientOptions?.baseURL ??
62
+ globalThis
63
+ .location?.href;
64
+ /** `{api}/<route>`, absolute, so Better Auth's fetch does not read it against its own base path. */
65
+ const route = (path) => new URL(`${api.replace(/\/+$/, "")}/${path}`, base).href;
66
+ let discovered;
67
+ /** The ClientContract's hash: the one given, else `GET {api}/_contract`'s. */
68
+ const hashOf = async () => {
69
+ if (given !== undefined) {
70
+ return given;
71
+ }
72
+ if (discovered !== undefined) {
73
+ return discovered;
74
+ }
75
+ const { data, error } = await $fetch(route("_contract"), {
76
+ method: "GET",
77
+ });
78
+ const hash = data?.protocol?.hash;
79
+ if (error !== null && error !== undefined) {
80
+ return fetchFailure(error);
81
+ }
82
+ if (typeof hash !== "string") {
83
+ return failure(500, "The framework's /_contract answer carries no protocol hash");
84
+ }
85
+ discovered = hash;
86
+ return hash;
87
+ };
88
+ const ask = async (questions, hash) => $fetch(route("_access/can"), {
89
+ method: "POST",
90
+ body: { questions },
91
+ headers: {
92
+ "aventara-protocol-version": PROTOCOL_VERSION,
93
+ "aventara-contract-hash": hash,
94
+ },
95
+ });
96
+ const can = async (questions) => {
97
+ if (questions.length === 0) {
98
+ return { data: [], error: null };
99
+ }
100
+ let hash = await hashOf();
101
+ if (typeof hash !== "string") {
102
+ return hash;
103
+ }
104
+ let answer = await ask(questions, hash);
105
+ // A hash read from `_contract` is stale once the framework redeploys: read it
106
+ // again, once.
107
+ if (given === undefined &&
108
+ codeOf(answer.error) === "A2005" &&
109
+ answer.error.status === 409) {
110
+ discovered = undefined;
111
+ hash = await hashOf();
112
+ if (typeof hash !== "string") {
113
+ return hash;
114
+ }
115
+ answer = await ask(questions, hash);
116
+ }
117
+ if (answer.error !== null && answer.error !== undefined) {
118
+ return fetchFailure(answer.error);
119
+ }
120
+ const answers = answer.data
121
+ ?.data;
122
+ if (codeOf(answer.data) !== "A1000" ||
123
+ !Array.isArray(answers) ||
124
+ answers.length !== questions.length ||
125
+ !allBooleans(answers)) {
126
+ return failure(500, "The framework's /_access/can answer is not one boolean per question", answer.data);
127
+ }
128
+ return { data: answers, error: null };
129
+ };
130
+ return { access: { can } };
131
+ },
132
+ };
133
+ };
@@ -0,0 +1,164 @@
1
+ import type { jwt } from "better-auth/plugins/jwt";
2
+ import type { AccessPoliciesOptions } from "../access/access-options.js";
3
+ import { type BetterAuthBlock } from "../access/better-auth.hardening.js";
4
+ import { type BetterAuthModules } from "../access/better-auth.loader.js";
5
+ import { type AccessClaimsDeclaration, type AccessClaimsOf, type AccessClaimsSource, type AccessIdentity } from "../access/claims.mapper.js";
6
+ import { type BetterAuthTablesFacts, type CheckedPolicies } from "../access/policies-block.checker.js";
7
+ import { type AuditLog } from "../audit/audit.log.js";
8
+ import type { AccessAuditOptions } from "../audit/audit-event.js";
9
+ import { type AccessMcpOptions } from "../mcp/mcp-options.js";
10
+ import { type AccessPasswordsOptions } from "../passwords/passwords.options.js";
11
+ import { type RegistrySettings } from "../registry/access.registry.js";
12
+ import { type AccessRegistryOptions } from "../registry/registry.options.js";
13
+ import { type AccessResourcesOptions, type ResourcesChoice } from "../resources/auth-tables.guard.js";
14
+ import { type AccessOrganizationBridge, type CheckedBridge } from "./organization.bridge.js";
15
+ type GovernedShape = {
16
+ readonly [resource: string]: readonly string[];
17
+ };
18
+ /**
19
+ * The principal (`me`) an instance hands the engine: the rules' principal type when
20
+ * the rules are the builder, else what `principal` returns (by default the identity).
21
+ */
22
+ type AccessPrincipalOf<R, M> = unknown extends R ? M : R;
23
+ /**
24
+ * The request's principal (`me`) from a verified identity: the app maps `sub` to its
25
+ * own profile, and `null` leaves the caller anonymous. With the builder as the rules,
26
+ * it returns their principal type.
27
+ */
28
+ type AccessPrincipalFunction<C extends AccessClaimsDeclaration, R, M> = (identity: AccessIdentity<C>) => (unknown extends R ? M : NoInfer<R>) | null | Promise<(unknown extends R ? M : NoInfer<R>) | null>;
29
+ /**
30
+ * The `principal` option: optional while the identity (`{ sub, claims }`) is itself
31
+ * the rules' principal type, and required when the rules are typed with another.
32
+ */
33
+ type AccessPrincipalOption<C extends AccessClaimsDeclaration, R, M> = [
34
+ AccessIdentity<C>
35
+ ] extends [R] ? {
36
+ /** The request's principal (`me`) from the verified identity; without it, `me` is the identity, `{ sub, claims }`. */
37
+ readonly principal?: AccessPrincipalFunction<C, R, M>;
38
+ } : {
39
+ /** The request's principal (`me`) from the verified identity, typed as the rules' principal. */
40
+ readonly principal: AccessPrincipalFunction<C, R, M>;
41
+ };
42
+ /** `aventaraAccess()`'s options. */
43
+ export type AventaraAccessOptions<C extends AccessClaimsDeclaration, R, Gov extends GovernedShape, M, E extends boolean = false> = {
44
+ /** The rules and the claims they read, as `createAccess` takes them. */
45
+ readonly policies: AccessPoliciesOptions<C, R, Gov>;
46
+ /**
47
+ * Which Resource of the rules' schema is each of Better Auth's tables, and which
48
+ * of them are Resources of the rules, as `createAccess` takes it. Every other
49
+ * table of Better Auth's is closed, and no grant opens a secret column.
50
+ */
51
+ readonly resources?: AccessResourcesOptions;
52
+ /**
53
+ * The organization bridge: a member's role in the session's active organization
54
+ * (Better Auth's `organization()` plugin) fills one claim of the token, and the
55
+ * organization another. Each grant on the role must be held to the organization.
56
+ */
57
+ readonly organization?: AccessOrganizationBridge<C>;
58
+ /**
59
+ * The audit log, as `createAccess` takes it: `true` writes the `accessAudit` table
60
+ * (Better Auth's `generate`/`migrate` create it), `{ sink }` hands each event to the
61
+ * app instead. Off by default.
62
+ */
63
+ readonly audit?: AccessAuditOptions;
64
+ /**
65
+ * How passwords are hashed, as `createAccess` takes it: argon2id at
66
+ * `"owasp-minimum"` (the default) or `"rfc9106-low-memory"`, and with
67
+ * `legacyScrypt: true` Better Auth's scrypt hashes accepted. A hash at another
68
+ * profile, or scrypt, is rehashed at the profile at its account's next sign-in.
69
+ * An app that sets Better Auth's `emailAndPassword.password` hashes its
70
+ * own way, and may not set this too.
71
+ */
72
+ readonly passwords?: AccessPasswordsOptions;
73
+ /**
74
+ * The policy registry, as `createAccess` takes it: `{ enabled: true }` makes the
75
+ * app the hub other services push their files to and pull them from, its tables
76
+ * this plugin's (Better Auth's `generate`/`migrate` create them) and its routes
77
+ * this plugin's endpoints under `/access/v1/namespaces`; `accessOf(auth).registry`
78
+ * is the operator's API. Off by default: no tables, no routes.
79
+ */
80
+ readonly registry?: AccessRegistryOptions<E>;
81
+ /**
82
+ * Access as the authorization layer of an MCP endpoint, as `createAccess`
83
+ * takes it: `{ resource }`, the endpoint's URL. Better Auth's OAuth provider
84
+ * (`@better-auth/oauth-provider`) goes in `plugins` after this plugin, with the
85
+ * endpoint among its `resources`; `accessOf(auth).mcp` is the framework's
86
+ * `mcp.authorization`.
87
+ */
88
+ readonly mcp?: AccessMcpOptions;
89
+ /**
90
+ * The service's own audience, an opt-in, as `createAccess` takes it (RFC 8707): with
91
+ * it, `accessOf(auth).state` takes a bearer token only when it is the OAuth
92
+ * provider's access token addressed to this audience alone, and refuses the
93
+ * one-org token `GET /token` issues. Off by default.
94
+ */
95
+ readonly audience?: string;
96
+ } & AccessPrincipalOption<C, R, M>;
97
+ /** The `jwt()` plugin `aventaraAccess()` carries inside it. */
98
+ type JwtPlugin = ReturnType<typeof jwt>;
99
+ /** What `aventaraAccess()` returns: a Better Auth plugin, with `jwt()`'s endpoints, hooks and table. */
100
+ export type AventaraAccessPlugin<C extends AccessClaimsDeclaration = AccessClaimsDeclaration, Gov extends GovernedShape = GovernedShape, M = unknown, E extends boolean = boolean, P extends boolean = boolean> = {
101
+ readonly id: "aventara-access";
102
+ readonly version: string;
103
+ readonly init: (context: unknown) => void;
104
+ readonly endpoints: JwtPlugin["endpoints"];
105
+ readonly hooks: JwtPlugin["hooks"];
106
+ readonly schema: JwtPlugin["schema"];
107
+ /** Types only, as Better Auth's plugins carry theirs: `typeof auth.$Infer.AccessMe` is the principal. */
108
+ readonly $Infer: {
109
+ readonly AccessMe: M;
110
+ readonly AccessClaims: AccessClaimsOf<C>;
111
+ readonly AccessGoverned: Gov;
112
+ /** Whether the plugin serves the registry (`registry: { enabled: true }`): `accessOf(auth).registry` is typed present exactly then. */
113
+ readonly AccessRegistryEnabled: E;
114
+ /** Whether `mcp` is given: `accessOf(auth).mcp` is there exactly then. */
115
+ readonly AccessMcp: P;
116
+ };
117
+ };
118
+ /** What Better Auth's options at `init` (or `auth.options`) fixed: the facts the request state and the engine read. */
119
+ type PluginStart = {
120
+ readonly issuer: string;
121
+ readonly crossSite: boolean;
122
+ readonly facts: BetterAuthTablesFacts;
123
+ /** With `mcp`: the MCP endpoint, and the authorization server `accessOf(auth).mcp` names (the issuer). */
124
+ readonly mcp?: {
125
+ readonly resource: string;
126
+ readonly server: string;
127
+ };
128
+ /** With `audience`: the service's own audience. */
129
+ readonly audience?: string;
130
+ /** The provider's UserInfo URL, which a token asked with `openid` names beside its resource. */
131
+ readonly userInfo: string | undefined;
132
+ };
133
+ /** What `aventaraAccess()` was built with, for `accessOf`. */
134
+ type PluginKernel = {
135
+ readonly checked: CheckedPolicies;
136
+ readonly resources: ResourcesChoice;
137
+ readonly bridge: CheckedBridge | undefined;
138
+ readonly modules: BetterAuthModules;
139
+ /** The audit log, when `audit` is on: the decisions of `accessOf(auth).policy` go to it. */
140
+ readonly auditLog: AuditLog | undefined;
141
+ /** The registry's settings when the plugin serves it, for `accessOf(auth).registry`; `undefined` when it is off. */
142
+ readonly registry: RegistrySettings | undefined;
143
+ /** Better Auth's options, checked as `init` checks them. */
144
+ start(options: BetterAuthBlock, own: unknown): PluginStart;
145
+ /** A session's claims, as its token is issued with them; `context` is Better Auth's. */
146
+ claimsFor(context: () => Promise<unknown>, source: AccessClaimsSource): Promise<{
147
+ readonly [claim: string]: string | number | boolean;
148
+ }>;
149
+ };
150
+ /**
151
+ * The Access plugin for Better Auth: `betterAuth({ plugins: [aventaraAccess({ policies })] })`.
152
+ * It carries Better Auth's `jwt()` (whose tokens carry `sub` and the declared claims,
153
+ * under the app's base URL as issuer and audience), so `jwt()` is not added beside
154
+ * it. `accessOf(auth)` is the framework's `access` option. `better-auth` is loaded
155
+ * here, and must be in Access's range, `~1.7.12` (1.7.12 or a later 1.7 patch).
156
+ */
157
+ export declare function aventaraAccess<const C extends AccessClaimsDeclaration, R = unknown, Gov extends GovernedShape = Record<never, never>, M = AccessIdentity<C>, const E extends boolean = false, X extends AccessMcpOptions | undefined = undefined>(options: AventaraAccessOptions<C, R, Gov, M, E> & {
158
+ readonly mcp?: X;
159
+ }): AventaraAccessPlugin<C, Gov, AccessPrincipalOf<R, M>, E, [
160
+ X
161
+ ] extends [undefined] ? false : [X] extends [AccessMcpOptions] ? true : boolean>;
162
+ /** What `aventaraAccess()` was built with, when `plugin` is one; `undefined` for any other plugin. */
163
+ export declare const pluginKernelOf: (plugin: unknown) => PluginKernel | undefined;
164
+ export {};
@@ -0,0 +1,348 @@
1
+ // `aventaraAccess()`: Access as a Better Auth plugin, for an app that builds Better
2
+ // Auth itself (`betterAuth({ plugins: [aventaraAccess({ policies })] })`). It is the
3
+ // other way in beside `createAccess`, and runs the same code: the `policies` block
4
+ // and `principal` checked when it is called; Better Auth's options checked when
5
+ // Better Auth initialises the plugin, as `createAccess` checks its `betterAuth` block
6
+ // (the fixed issuer, the session cookie's site, the claims' sources, which Resource
7
+ // is which of Better Auth's tables, no second `jwt()`); and tokens issued by Better
8
+ // Auth's own `jwt()`, which the plugin carries inside it, with `sub` and the declared
9
+ // claims only. `accessOf(auth)` then gives the framework its `access` option.
10
+ //
11
+ // Passwords are hashed with argon2id as under `createAccess` (the `passwords` block,
12
+ // or the app's own `emailAndPassword.password`, which passes through): the plugin
13
+ // puts the hook in Better Auth's context when Better Auth initialises it.
14
+ //
15
+ // The audit log is here as under `createAccess` (`audit: true` or `{ sink }`): sign-ins,
16
+ // sign-outs, refused sign-ins and tokens from Better Auth's hooks, and the decisions
17
+ // of `accessOf(auth).policy`.
18
+ //
19
+ // The registry is here too, as under `createAccess` (`registry: { enabled: true }`),
20
+ // which makes the app the hub other services push to and pull from: its tables are
21
+ // this plugin's schema, so Better Auth's `migrate`/`generate` create them through the
22
+ // app's own adapter, and its routes are this plugin's endpoints, so Better Auth serves
23
+ // them wherever the app mounts it. A service reaches them only with its namespace's
24
+ // key; the operator's API (namespaces, keys, the org's guardrails) is
25
+ // `accessOf(auth).registry`, in the app's own process, and no route opens it. What
26
+ // else `createAccess` adds beside Better Auth (profiles, its `handler`, the standalone
27
+ // host) is not here: the app owns its Better Auth instance and mounts it as it
28
+ // already does.
29
+ //
30
+ // `jwt()` is carried inside this plugin, under the Access plugin's id. A Better Auth
31
+ // plugin that looks `jwt()` up by its id (`ctx.getPlugin("jwt")`, as the OAuth
32
+ // provider does for its issuer and keys) finds the carried one: the plugin answers
33
+ // for that id in Better Auth's context (`getPlugin`, `hasPlugin`) when Better Auth
34
+ // starts it, so a plugin that reads it as it starts comes after this one in
35
+ // `plugins`.
36
+ //
37
+ // With `mcp` or `audience`, Access reads the tokens Better Auth's OAuth provider (in
38
+ // `plugins` after this plugin) issues, as under `createAccess`: PKCE is required of
39
+ // every authorization request, `accessOf(auth).mcp` is the framework's
40
+ // `mcp.authorization`, and with `audience` the service takes only bearer tokens
41
+ // addressed to it. Access issues no token and serves no OAuth route.
42
+ import { AccessError } from "../access/access.error.js";
43
+ import { ACCESS_PLUGIN_ID, cookieCacheOn, crossSiteSessionCookie, issuerOf, JWT_PLUGIN_ID, normalisedId, refuseInstalledPlugins, refuseUnverifiedInvitations, } from "../access/better-auth.hardening.js";
44
+ import { loadBetterAuth, } from "../access/better-auth.loader.js";
45
+ import { issuedClaims, } from "../access/claims.mapper.js";
46
+ import { betterAuthTablesFor, checkPoliciesBlock, } from "../access/policies-block.checker.js";
47
+ import { sessionStillLive } from "../access/request.state.js";
48
+ import { sessionWatch } from "../access/session.hooks.js";
49
+ import { auditDestinationOf, createAuditLog, } from "../audit/audit.log.js";
50
+ import { auditSchema } from "../audit/audit.schema.js";
51
+ import { authorizationServerOf, checkMcpOptions, } from "../mcp/mcp-options.js";
52
+ import { userInfoAudience } from "../oauth/access-token.verifier.js";
53
+ import { checkOAuthProvider, checkResourceUrl, oauthUseOf, pkceGuard, } from "../oauth/oauth-provider.hardening.js";
54
+ import { checkPasswordsOptions, passwordHookOf, passwordRehashHook, } from "../passwords/passwords.options.js";
55
+ import { isRecord } from "../policies.compiler.js";
56
+ import { createRegistry, } from "../registry/access.registry.js";
57
+ import { registryEndpoints, registryOnRequest, } from "../registry/registry.endpoints.js";
58
+ import { registryOptionsOf, } from "../registry/registry.options.js";
59
+ import { registrySchema } from "../registry/registry.schema.js";
60
+ import { checkResourcesOptions, } from "../resources/auth-tables.guard.js";
61
+ import { bridgedClaims, checkBridge, checkBridgePlugins, } from "./organization.bridge.js";
62
+ /**
63
+ * What each plugin `aventaraAccess()` built was built with, by plugin: a map this
64
+ * module alone holds, so no object but one it returned passes for the plugin.
65
+ */
66
+ const KERNELS = new WeakMap();
67
+ const TOP_LEVEL = new Set([
68
+ "policies",
69
+ "principal",
70
+ "resources",
71
+ "organization",
72
+ "audit",
73
+ "passwords",
74
+ "registry",
75
+ "mcp",
76
+ "audience",
77
+ ]);
78
+ /** The plugins `aventaraAccess()` carries itself, by id: a second one in `plugins` would answer for its routes. */
79
+ const CARRIED = { [JWT_PLUGIN_ID]: "jwt()" };
80
+ /**
81
+ * The Access plugin for Better Auth: `betterAuth({ plugins: [aventaraAccess({ policies })] })`.
82
+ * It carries Better Auth's `jwt()` (whose tokens carry `sub` and the declared claims,
83
+ * under the app's base URL as issuer and audience), so `jwt()` is not added beside
84
+ * it. `accessOf(auth)` is the framework's `access` option. `better-auth` is loaded
85
+ * here, and must be in Access's range, `~1.7.12` (1.7.12 or a later 1.7 patch).
86
+ */
87
+ export function aventaraAccess(options) {
88
+ if (!isRecord(options)) {
89
+ throw new AccessError("ACCESS_OPTIONS_INVALID", "aventaraAccess takes one object: `{ policies, principal?, resources?, organization?, audit?, passwords?, registry?, mcp?, audience? }`.");
90
+ }
91
+ for (const key of Object.keys(options)) {
92
+ if (!TOP_LEVEL.has(key)) {
93
+ throw new AccessError("ACCESS_OPTIONS_INVALID", `aventaraAccess does not take \`${key}\`; it takes \`policies\`, \`principal\`, \`resources\`, \`organization\`, \`audit\`, \`passwords\`, \`registry\`, \`mcp\` and \`audience\`. Better Auth's own options go to betterAuth({...}) beside it.`);
94
+ }
95
+ }
96
+ const given = options;
97
+ if (given.policies === undefined) {
98
+ throw new AccessError("ACCESS_OPTIONS_INVALID", "the `policies` option is missing: pass `{ rules, claims }`, the rules (`definePolicies(model)…`) and the claims they read.");
99
+ }
100
+ const checked = checkPoliciesBlock(given.policies, given.principal);
101
+ const resources = checkResourcesOptions(given.resources);
102
+ const bridge = checkBridge(given.organization, checked.claims);
103
+ const passwords = checkPasswordsOptions(given.passwords);
104
+ const registrySettings = registryOptionsOf(given.registry);
105
+ // The block's shape now; its resource against the issuer when Better Auth starts.
106
+ const mcp = checkMcpOptions(given.mcp, "mcp", "");
107
+ const oauthOn = oauthUseOf(mcp, given.audience === undefined
108
+ ? undefined
109
+ : checkResourceUrl(given.audience, "audience", ""), undefined);
110
+ const modules = loadBetterAuth();
111
+ // The bridge owns its two claims: neither the user's field of the same name nor
112
+ // `claimsOf` fills them.
113
+ const bridged = bridge === undefined ? [] : [bridge.claim, bridge.organizationClaim];
114
+ const ownClaims = {
115
+ ...checked.claims,
116
+ };
117
+ for (const claim of bridged) {
118
+ delete ownClaims[claim];
119
+ }
120
+ const claimsOf = checked.claimsOf === undefined || bridge === undefined
121
+ ? checked.claimsOf
122
+ : async (source) => {
123
+ const answer = await checked.claimsOf(source);
124
+ if (!isRecord(answer)) {
125
+ return answer;
126
+ }
127
+ const kept = { ...answer };
128
+ for (const claim of bridged) {
129
+ delete kept[claim];
130
+ }
131
+ return kept;
132
+ };
133
+ const claimsFor = async (context, source) => {
134
+ const claims = await issuedClaims(source, bridge === undefined ? checked.claims : ownClaims, claimsOf);
135
+ if (bridge === undefined) {
136
+ return claims;
137
+ }
138
+ const fromBridge = await bridgedClaims(bridge, (await context()), source);
139
+ return fromBridge === undefined ? claims : { ...claims, ...fromBridge };
140
+ };
141
+ const start = (block, own) => {
142
+ const plugins = refuseInstalledPlugins(block.plugins, {
143
+ by: "aventaraAccess()",
144
+ at: "plugins",
145
+ installs: CARRIED,
146
+ own,
147
+ registry: registrySettings !== undefined,
148
+ });
149
+ for (const plugin of plugins) {
150
+ const id = plugin?.id;
151
+ if (plugin !== own && normalisedId(id) === ACCESS_PLUGIN_ID) {
152
+ throw new AccessError("ACCESS_PLUGIN_DUPLICATE", `plugins holds a second Access plugin (plugin id "${String(id)}") beside aventaraAccess(): Better Auth runs one. Remove it.`);
153
+ }
154
+ }
155
+ if (resources.members) {
156
+ refuseUnverifiedInvitations(plugins, "`resources: { members: true }`");
157
+ }
158
+ if (bridge !== undefined) {
159
+ checkBridgePlugins(plugins, bridge);
160
+ }
161
+ const issuer = issuerOf(block, modules, "baseURL");
162
+ const checkedMcp = checkMcpOptions(given.mcp, "mcp", issuer);
163
+ const audience = given.audience === undefined
164
+ ? undefined
165
+ : checkResourceUrl(given.audience, "audience", issuer);
166
+ const userInfo = userInfoAudience(block, modules);
167
+ const use = oauthUseOf(checkedMcp, audience, userInfo);
168
+ if (use !== undefined) {
169
+ checkOAuthProvider(plugins, use, { at: "plugins", own });
170
+ }
171
+ return {
172
+ ...(checkedMcp === undefined
173
+ ? {}
174
+ : {
175
+ mcp: {
176
+ resource: checkedMcp.resource,
177
+ server: authorizationServerOf(issuer, "baseURL"),
178
+ },
179
+ }),
180
+ ...(audience === undefined ? {} : { audience }),
181
+ userInfo,
182
+ issuer,
183
+ crossSite: crossSiteSessionCookie(block, "trustedOrigins"),
184
+ facts: betterAuthTablesFor({
185
+ options: block,
186
+ modules,
187
+ checked,
188
+ resources,
189
+ bridged,
190
+ }),
191
+ };
192
+ };
193
+ // Fixed when Better Auth initialises the plugin, from its options: the tokens'
194
+ // issuer and audience, and the context the claims, the session check and the
195
+ // audit log read. `jwt()` hands its payload function the session alone, never
196
+ // Better Auth's context, so these are the plugin's, not each context's: a plugin
197
+ // started again (Better Auth's migrations build another context from the same
198
+ // options) must find every one of them as it was, or it is refused.
199
+ let first;
200
+ const contextOf = async () => first?.context;
201
+ const auditTo = auditDestinationOf(given.audit);
202
+ const auditLog = auditTo === undefined
203
+ ? undefined
204
+ : createAuditLog(auditTo, async () => (await contextOf()));
205
+ const watch = sessionWatch({
206
+ kind: "web",
207
+ auditLog,
208
+ modules,
209
+ plugins: () => first?.plugins ?? [],
210
+ });
211
+ const carried = modules.jwt({
212
+ jwt: {
213
+ get issuer() {
214
+ return first?.issuer;
215
+ },
216
+ get audience() {
217
+ return first?.issuer;
218
+ },
219
+ expirationTime: `${checked.tokenTtl}s`,
220
+ definePayload: async (source) => {
221
+ if (first?.cookieCache === true) {
222
+ await sessionStillLive(contextOf, source.session, () => new modules.APIError("UNAUTHORIZED", {
223
+ message: "The session has ended.",
224
+ }));
225
+ }
226
+ return claimsFor(contextOf, source);
227
+ },
228
+ },
229
+ });
230
+ // The registry's routes store through the adapter of the Better Auth that started
231
+ // the plugin: every instance it is in has the same database (`init` refuses one
232
+ // that does not), and no route runs before Better Auth has started it.
233
+ const registryStore = registrySettings === undefined
234
+ ? undefined
235
+ : createRegistry(async () => {
236
+ const context = first?.context;
237
+ if (context?.adapter === undefined) {
238
+ throw new AccessError("ACCESS_OPTIONS_INVALID", "the registry was asked before Better Auth started aventaraAccess(): build Better Auth with it in its plugins first.");
239
+ }
240
+ return context.adapter;
241
+ }, registrySettings);
242
+ const registryRoutes = registryStore === undefined
243
+ ? undefined
244
+ : registryEndpoints(registryStore, modules.createAuthEndpoint);
245
+ const carriedHooks = carried.hooks;
246
+ const plugin = {
247
+ id: ACCESS_PLUGIN_ID,
248
+ version: carried.version,
249
+ init(ctx) {
250
+ const { options: block } = ctx;
251
+ const started = start(block, plugin);
252
+ const now = {
253
+ issuer: started.issuer,
254
+ crossSite: started.crossSite,
255
+ cookieCache: cookieCacheOn(block),
256
+ database: block.database,
257
+ secondaryStorage: block.secondaryStorage,
258
+ };
259
+ if (first === undefined) {
260
+ first = {
261
+ ...now,
262
+ context: ctx,
263
+ plugins: Array.isArray(block.plugins) ? block.plugins : [],
264
+ };
265
+ }
266
+ else if (now.issuer !== first.issuer ||
267
+ now.crossSite !== first.crossSite ||
268
+ now.cookieCache !== first.cookieCache ||
269
+ now.database !== first.database ||
270
+ now.secondaryStorage !== first.secondaryStorage) {
271
+ throw new AccessError("ACCESS_PLUGIN_DUPLICATE", "one aventaraAccess() plugin is in two Better Auth instances whose options differ (database, secondary storage, base URL, session cookie or cookie cache): call aventaraAccess() for each betterAuth({...}).");
272
+ }
273
+ // Better Auth's password hook, argon2id unless the app set its own: put in
274
+ // Better Auth's context, which Better Auth built from its options before
275
+ // initialising its plugins.
276
+ const hook = passwordHookOf(passwords, block.emailAndPassword, modules, contextOf, "emailAndPassword");
277
+ const watched = watch.init?.();
278
+ // The carried `jwt()`, found by its own id in Better Auth's context, as a plugin
279
+ // beside it would be: the OAuth provider reads its issuer and keys there.
280
+ const { getPlugin, hasPlugin } = ctx;
281
+ const jwtLookup = {
282
+ getPlugin: (id) => id === JWT_PLUGIN_ID ? carried : getPlugin(id),
283
+ hasPlugin: (id) => id === JWT_PLUGIN_ID || hasPlugin(id),
284
+ };
285
+ const { password } = ctx;
286
+ return {
287
+ ...watched,
288
+ context: {
289
+ ...watched?.context,
290
+ ...jwtLookup,
291
+ ...(hook === undefined
292
+ ? {}
293
+ : {
294
+ password: {
295
+ ...password,
296
+ hash: hook.hash,
297
+ verify: hook.verify,
298
+ },
299
+ }),
300
+ },
301
+ };
302
+ },
303
+ endpoints: registryRoutes === undefined
304
+ ? carried.endpoints
305
+ : { ...carried.endpoints, ...registryRoutes },
306
+ // A push's key is checked before Better Auth reads its body, on any mount.
307
+ ...(registryStore === undefined
308
+ ? {}
309
+ : { onRequest: registryOnRequest(registryStore) }),
310
+ hooks: {
311
+ before: [
312
+ ...(carriedHooks.before ?? []),
313
+ ...(oauthOn === undefined ? [] : [pkceGuard(modules)]),
314
+ ...(watch.hooks?.before ?? []),
315
+ ],
316
+ after: [
317
+ ...(carriedHooks.after ?? []),
318
+ ...(watch.hooks?.after ?? []),
319
+ // Inert unless Better Auth's password hook is Access's (the app's own passes through).
320
+ passwordRehashHook(modules),
321
+ ],
322
+ },
323
+ schema: {
324
+ ...carried.schema,
325
+ ...(registrySettings === undefined ? {} : registrySchema),
326
+ ...(auditTo?.kind === "table" ? auditSchema : {}),
327
+ },
328
+ $Infer: {},
329
+ };
330
+ const kernel = {
331
+ checked,
332
+ resources,
333
+ bridge,
334
+ modules,
335
+ auditLog,
336
+ registry: registrySettings,
337
+ start,
338
+ claimsFor,
339
+ };
340
+ KERNELS.set(plugin, kernel);
341
+ // The declared return type states it; a cast restating it costs the type-checker
342
+ // the whole plugin type again.
343
+ return plugin;
344
+ }
345
+ /** What `aventaraAccess()` was built with, when `plugin` is one; `undefined` for any other plugin. */
346
+ export const pluginKernelOf = (plugin) => typeof plugin === "object" && plugin !== null
347
+ ? KERNELS.get(plugin)
348
+ : undefined;