@aventara/access 0.0.0-stage → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/CHANGELOG.md +877 -0
  2. package/LICENSE +91 -0
  3. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  4. package/README.md +2536 -2
  5. package/dist/access/access-options.d.ts +74 -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 +275 -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 +22 -0
  28. package/dist/access/principal.subject.js +89 -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 +266 -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 +4 -0
  40. package/dist/access-warning.emitter.js +24 -0
  41. package/dist/access.builder.d.ts +340 -0
  42. package/dist/access.builder.js +75 -0
  43. package/dist/access.engine.d.ts +155 -0
  44. package/dist/access.engine.js +477 -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 +80 -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 +101 -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 +6 -0
  122. package/dist/foreign-references.check.js +131 -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 +73 -0
  134. package/dist/generator/prisma7/prisma-where.translator.js +138 -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 +13 -0
  174. package/dist/mcp/agent-scope.gate.js +75 -0
  175. package/dist/mcp/agent.grant.d.ts +40 -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 +219 -0
  190. package/dist/policies-contract.js +8 -0
  191. package/dist/policies.compiler.d.ts +38 -0
  192. package/dist/policies.compiler.js +1433 -0
  193. package/dist/policies.decider.d.ts +103 -0
  194. package/dist/policies.decider.js +390 -0
  195. package/dist/policies.hash.d.ts +10 -0
  196. package/dist/policies.hash.js +54 -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/read-governed-unique.check.d.ts +3 -0
  202. package/dist/read-governed-unique.check.js +21 -0
  203. package/dist/reference-reads.check.d.ts +16 -0
  204. package/dist/reference-reads.check.js +158 -0
  205. package/dist/registry/access.registry.d.ts +173 -0
  206. package/dist/registry/access.registry.js +865 -0
  207. package/dist/registry/beacon.ledger.d.ts +30 -0
  208. package/dist/registry/beacon.ledger.js +93 -0
  209. package/dist/registry/ownership.check.d.ts +54 -0
  210. package/dist/registry/ownership.check.js +221 -0
  211. package/dist/registry/registry.endpoints.d.ts +50 -0
  212. package/dist/registry/registry.endpoints.js +193 -0
  213. package/dist/registry/registry.key.d.ts +7 -0
  214. package/dist/registry/registry.key.js +13 -0
  215. package/dist/registry/registry.options.d.ts +13 -0
  216. package/dist/registry/registry.options.js +28 -0
  217. package/dist/registry/registry.refusal.d.ts +6 -0
  218. package/dist/registry/registry.refusal.js +11 -0
  219. package/dist/registry/registry.schema.d.ts +238 -0
  220. package/dist/registry/registry.schema.js +127 -0
  221. package/dist/resources/auth-tables.catalog.d.ts +96 -0
  222. package/dist/resources/auth-tables.catalog.js +110 -0
  223. package/dist/resources/auth-tables.guard.d.ts +106 -0
  224. package/dist/resources/auth-tables.guard.js +462 -0
  225. package/dist/scope.resolver.d.ts +144 -0
  226. package/dist/scope.resolver.js +529 -0
  227. package/dist/sha256.digest.d.ts +2 -0
  228. package/dist/sha256.digest.js +81 -0
  229. package/dist/standalone/access.server.d.ts +41 -0
  230. package/dist/standalone/access.server.js +301 -0
  231. package/dist/standalone/index.d.ts +1 -0
  232. package/dist/standalone/index.js +1 -0
  233. package/package.json +102 -3
  234. package/skill/SKILL.md +170 -0
  235. package/skill/reference/callers.md +99 -0
  236. package/skill/reference/errors.md +89 -0
  237. package/skill/reference/recipes.md +281 -0
  238. package/skill/reference/rules.md +153 -0
  239. package/skill/reference/setup.md +334 -0
  240. package/src/aventara-where.translator.ts +116 -0
  241. package/src/beacon/beacon.check.ts +204 -0
  242. package/src/canonical-json.writer.ts +51 -0
  243. package/src/generator/prisma7/prisma-where.translator.ts +243 -0
  244. package/src/policies-contract.ts +215 -0
  245. package/src/policies.decider.ts +662 -0
  246. package/src/predicate.evaluator.ts +574 -0
  247. package/src/scope.resolver.ts +819 -0
  248. package/src/sha256.digest.ts +85 -0
@@ -0,0 +1,440 @@
1
+ // `createAccess`: Better Auth with Access's concerns beside it. Better Auth's own
2
+ // options sit in the `betterAuth` block, typed as Better Auth types them and passed
3
+ // to `betterAuth()` as they are; Access adds its blocks beside it (`policies`,
4
+ // `principal`, `profile`, `registry`, `audit`, `resources`, `passwords`) and installs two plugins of its own:
5
+ // Better Auth's `jwt()`, whose tokens carry `sub` and the declared claims under one
6
+ // fixed issuer and audience, and the Access plugin, which holds the service
7
+ // profile's refusals, with `registry: { enabled: true }` the registry's tables and
8
+ // routes, and with `audit` the hooks that log sessions and tokens (and, for
9
+ // `audit: true`, the audit table). Agents' and services' OAuth tokens are Better Auth's
10
+ // own: its OAuth provider (`@better-auth/oauth-provider`), which the app adds to
11
+ // `betterAuth.plugins` itself, issues them, and Access issues none. With `mcp`, Access
12
+ // reads the provider's tokens for the MCP endpoint: `access.mcp` is the framework's
13
+ // `mcp.authorization`, and an agent's principal is held to its token's audience and
14
+ // scopes. With `audience`, the service opts in to tokens addressed to it alone (RFC
15
+ // 8707). Either way Access checks the provider and requires PKCE of every
16
+ // authorization request. Over Better Auth's tables the engine runs
17
+ // built-in rules beside the app's (`resources`): a table no key opens is closed, an
18
+ // opened one takes only the writes Better Auth's hooks do not guard, and a secret
19
+ // column is never readable. Passwords are hashed with argon2id (`passwords`) unless
20
+ // the app set Better Auth's `emailAndPassword.password` hook itself. The instance mounts nothing: the app mounts
21
+ // `access.handler` the way it would mount Better Auth's. It satisfies the
22
+ // framework's access provider by shape (`policy`, `state`) without importing it.
23
+ //
24
+ // Building it reads no network and opens no port: Better Auth itself is built on
25
+ // the first request, `access.api` read or `state` call.
26
+ import { auditDestinationOf, createAuditLog, } from "../audit/audit.log.js";
27
+ import { auditSchema, kioskAuditSchema } from "../audit/audit.schema.js";
28
+ import { adapterBeaconStore, beaconSchema, } from "../beacon/beacon.schema.js";
29
+ import { accessClient, meEndpoint, } from "../client/access-client.factory.js";
30
+ import { verbRateLimiter, } from "../client/verb-rate.limiter.js";
31
+ import { kioskPlugin, } from "../kiosk/kiosk.plugin.js";
32
+ import { kioskSchema } from "../kiosk/kiosk.schema.js";
33
+ import { PIN_LENGTH } from "../kiosk/pin.policy.js";
34
+ import { authorizationServerOf, checkMcpOptions, } from "../mcp/mcp-options.js";
35
+ import { createAccessTokenVerifier, mcpAuthorizationOf, userInfoAudience, } from "../oauth/access-token.verifier.js";
36
+ import { checkOAuthProvider, checkResourceUrl, oauthUseOf, pkceGuard, } from "../oauth/oauth-provider.hardening.js";
37
+ import { passwordsOf, pinHasherOf, } from "../passwords/passwords.options.js";
38
+ import { isRecord } from "../policies.compiler.js";
39
+ import { POLICIES_FORMAT } from "../policies-contract.js";
40
+ import { createRegistry, } from "../registry/access.registry.js";
41
+ import { registryEndpoints, registryOnRequest, } from "../registry/registry.endpoints.js";
42
+ import { registryOptionsOf, } from "../registry/registry.options.js";
43
+ import { registrySchema } from "../registry/registry.schema.js";
44
+ import { checkResourcesOptions, } from "../resources/auth-tables.guard.js";
45
+ import { AccessError } from "./access.error.js";
46
+ import { baseURLOf, cookieCacheOn, crossSiteSessionCookie, refuseInstalledPlugins, refuseKioskOptions, refuseUnverifiedInvitations, } from "./better-auth.hardening.js";
47
+ import { loadBetterAuth } from "./better-auth.loader.js";
48
+ import { issuedClaims, } from "./claims.mapper.js";
49
+ import { betterAuthTablesFor, checkPoliciesBlock, guardedPolicy, } from "./policies-block.checker.js";
50
+ import { checkProfile } from "./profile.policy.js";
51
+ import { meOf, requestIdentity, requestState, sessionStillLive, verbMeOf, } from "./request.state.js";
52
+ import { sessionCookies, sessionWatch, signInIdentifier, withoutSessionCookies, } from "./session.hooks.js";
53
+ import { createTokenVerifier } from "./token.verifier.js";
54
+ /**
55
+ * The plugins `createAccess` installs itself, by id, as a sentence names each.
56
+ */
57
+ const INSTALLED_PLUGINS = {
58
+ "aventara-access": "aventaraAccess()",
59
+ jwt: "jwt()",
60
+ };
61
+ /** The shortest and longest a token may live, in seconds, and the default. */
62
+ /**
63
+ * The `policies` block of a registry-only server, which has no rules of its own: no
64
+ * Resource, no claim beside `sub`, and so every policy question is denied.
65
+ */
66
+ const REGISTRY_ONLY_POLICIES = {
67
+ rules: {
68
+ format: POLICIES_FORMAT,
69
+ claims: {},
70
+ schema: {},
71
+ roles: {},
72
+ govern: {},
73
+ grants: [],
74
+ denies: [],
75
+ },
76
+ claims: {},
77
+ };
78
+ const TOP_LEVEL = new Set([
79
+ "betterAuth",
80
+ "policies",
81
+ "principal",
82
+ "profile",
83
+ "registry",
84
+ "audit",
85
+ "resources",
86
+ "passwords",
87
+ "mcp",
88
+ "audience",
89
+ ]);
90
+ const refuse = (message) => {
91
+ throw new AccessError("ACCESS_OPTIONS_INVALID", message);
92
+ };
93
+ /**
94
+ * Builds an Access instance: Better Auth (from the `betterAuth` block, with `jwt()`
95
+ * and the Access plugin installed), the policy engine (from `policies.rules`) and
96
+ * the request state the framework's `access` option reads. It mounts nothing and
97
+ * opens no connection; `better-auth` is loaded here, and must be the exact version
98
+ * Access is built against.
99
+ */
100
+ export function createAccess(options) {
101
+ if (!isRecord(options)) {
102
+ return refuse("createAccess takes one object: `{ betterAuth, policies, principal?, profile?, registry?, audit?, resources?, passwords?, mcp?, audience? }`.");
103
+ }
104
+ for (const key of Object.keys(options)) {
105
+ if (!TOP_LEVEL.has(key)) {
106
+ refuse(`createAccess does not take \`${key}\`; it takes \`betterAuth\`, \`policies\`, \`principal\`, \`profile\`, \`registry\`, \`audit\`, \`resources\`, \`passwords\`, \`mcp\` and \`audience\`.`);
107
+ }
108
+ }
109
+ const { betterAuth: block, policies, principal, profile, registry, audit, resources, passwords, mcp, audience, } = options;
110
+ if (block === undefined) {
111
+ return refuse("the `betterAuth` option is missing: pass Better Auth's options there, as `betterAuth({...})` takes them (`{ database, ... }`).");
112
+ }
113
+ if (!isRecord(block)) {
114
+ return refuse("`betterAuth` holds Better Auth's options, as `betterAuth({...})` takes them.");
115
+ }
116
+ const { kind, kiosk: kioskSettings } = checkProfile(profile);
117
+ const plugins = refuseInstalledPlugins(block.plugins, {
118
+ by: "createAccess",
119
+ at: "betterAuth.plugins",
120
+ installs: INSTALLED_PLUGINS,
121
+ me: true,
122
+ kiosk: kind === "kiosk",
123
+ registry: isRecord(registry) && registry.enabled === true,
124
+ });
125
+ if (policies === undefined &&
126
+ !(isRecord(registry) && registry.enabled === true)) {
127
+ return refuse("the `policies` option is missing: pass `{ rules, claims }`, the rules (`definePolicies(model)…`) and the claims they read. Only a registry-only server (`registry: { enabled: true }`) may leave it out.");
128
+ }
129
+ const checked = checkPoliciesBlock(policies ?? REGISTRY_ONLY_POLICIES, principal);
130
+ const { claims, claimsOf, tokenTtl } = checked;
131
+ const auditTo = auditDestinationOf(audit);
132
+ // The audit log, when on: events reach the table through Better Auth's adapter
133
+ // (once Better Auth is built), or the app's sink.
134
+ const auditLog = auditTo === undefined
135
+ ? undefined
136
+ : createAuditLog(auditTo, async () => (await instance().$context));
137
+ const registrySettings = registryOptionsOf(registry);
138
+ const registryOn = registrySettings !== undefined;
139
+ const opened = checkResourcesOptions(resources);
140
+ // The rules read the organization plugin's memberships when `members` opens
141
+ // them: a membership an unverified account took by invitation is refused.
142
+ if (opened.members) {
143
+ refuseUnverifiedInvitations(plugins, "`resources: { members: true }`");
144
+ }
145
+ const modules = loadBetterAuth();
146
+ // Better Auth's `emailAndPassword` with the argon2id hook in it, unless the app
147
+ // set its own `password` hook (then the block passes through as given), and with
148
+ // that hook the one that rehashes a password at the profile at sign-in.
149
+ const { emailAndPassword, rehash } = passwordsOf(passwords, block, modules, async () => await instance().$context);
150
+ // Every token's issuer and audience, fixed now: the app's base URL.
151
+ const writtenBase = baseURLOf(block, modules, "betterAuth.baseURL");
152
+ const issuer = new URL(writtenBase).origin;
153
+ // With `mcp` or `audience`: the resources whose tokens Better Auth's OAuth provider
154
+ // issues and Access reads, the provider checked, and with `mcp` the issuer as the
155
+ // authorization server the framework names.
156
+ const checkedMcp = checkMcpOptions(mcp, "mcp", issuer);
157
+ const ownAudience = audience === undefined
158
+ ? undefined
159
+ : checkResourceUrl(audience, "audience", issuer);
160
+ let server;
161
+ const oauth = oauthUseOf(checkedMcp, ownAudience, userInfoAudience(block, modules));
162
+ if (oauth !== undefined) {
163
+ if (kind === "service") {
164
+ refuse(`\`${oauth.by}\` reads the tokens Better Auth's OAuth provider issues after its user signs in and consents in a browser, and the \`service\` profile hands no caller a session: use the \`web\` profile.`);
165
+ }
166
+ if (checkedMcp !== undefined) {
167
+ server = authorizationServerOf(issuer, "betterAuth.baseURL");
168
+ }
169
+ checkOAuthProvider(plugins, oauth, { at: "betterAuth.plugins" });
170
+ }
171
+ // A session cookie a browser sends from any site is read only from the origins
172
+ // Better Auth trusts (`state`, below), so there must be some.
173
+ const crossSite = crossSiteSessionCookie(block, "betterAuth.trustedOrigins");
174
+ // With Better Auth's cookie cache on, a session read from its cookie may have
175
+ // been revoked since: a token is issued only for a session the store still has.
176
+ const cookieCache = cookieCacheOn(block);
177
+ // On a till network's plain http: base URL (`profile.insecureLan`), the ranges it
178
+ // is served on: Better Auth's logger warns of them when it starts.
179
+ const servedOnLan = kioskSettings === undefined
180
+ ? undefined
181
+ : refuseKioskOptions(block, plugins, cookieCache, writtenBase, "betterAuth.baseURL", kioskSettings.insecureLan);
182
+ // Better Auth's database adapter, once Better Auth is built: the registry stores
183
+ // its tables through it, whichever adapter the app gave Better Auth.
184
+ const registryStore = registryOn
185
+ ? createRegistry(async () => (await instance().$context).adapter, registrySettings)
186
+ : undefined;
187
+ // The Access server (the registry on) whose rules name a namespace is one of the
188
+ // org's namespaces: it keeps its own beacon, through Better Auth's adapter.
189
+ const beaconStore = registryOn && typeof checked.file.namespace === "string"
190
+ ? adapterBeaconStore(async () => (await instance().$context).adapter)
191
+ : undefined;
192
+ const schema = {
193
+ ...(registryStore === undefined ? {} : registrySchema),
194
+ ...(beaconStore === undefined ? {} : beaconSchema),
195
+ ...(auditTo?.kind === "table"
196
+ ? kioskSettings === undefined
197
+ ? auditSchema
198
+ : kioskAuditSchema
199
+ : {}),
200
+ ...(kioskSettings === undefined ? {} : kioskSchema),
201
+ };
202
+ // The requests `access.client`'s calls build, each standing for the caller's: the
203
+ // Access plugin runs Better Auth's router origin check on them (a direct API call
204
+ // skips the router).
205
+ const clientCalls = new WeakSet();
206
+ const claimsFor = (source) => issuedClaims(source, claims, claimsOf);
207
+ // The kiosk profile's routes and its hook: PINs hashed with argon2id at the
208
+ // `passwords` profile, whatever hashes the app's passwords.
209
+ const kiosk = kioskSettings === undefined
210
+ ? undefined
211
+ : kioskPlugin({
212
+ settings: kioskSettings,
213
+ endpoint: modules.createAuthEndpoint,
214
+ serverOnly: modules.createAuthEndpoint.serverOnly,
215
+ middleware: modules.createAuthMiddleware,
216
+ refusal: modules.APIError,
217
+ getSessionFromCtx: modules.getSessionFromCtx,
218
+ cookies: modules.cookies,
219
+ oauthState: modules.getOAuthState,
220
+ pins: pinHasherOf(passwords, modules, async () => await instance().$context, PIN_LENGTH.max),
221
+ claimsFor: claimsFor,
222
+ auditLog,
223
+ secureDevice: issuer.startsWith("https:"),
224
+ });
225
+ const watch = sessionWatch({
226
+ kind,
227
+ auditLog,
228
+ modules,
229
+ plugins,
230
+ ...(kiosk === undefined
231
+ ? {}
232
+ : {
233
+ sessionCreating: kiosk.sessionCreating,
234
+ sessionCreated: kiosk.sessionCreated,
235
+ }),
236
+ });
237
+ const NO_HOOKS = [];
238
+ const watchedBefore = watch.hooks === undefined ? NO_HOOKS : watch.hooks.before;
239
+ const watchedAfter = watch.hooks === undefined ? NO_HOOKS : watch.hooks.after;
240
+ const accessPlugin = {
241
+ id: "aventara-access",
242
+ ...(Object.keys(schema).length === 0 ? {} : { schema }),
243
+ // A push's key is checked before Better Auth reads its body, on any mount.
244
+ ...(registryStore === undefined
245
+ ? {}
246
+ : { onRequest: registryOnRequest(registryStore) }),
247
+ endpoints: {
248
+ ...(registryStore === undefined
249
+ ? {}
250
+ : registryEndpoints(registryStore, modules.createAuthEndpoint)),
251
+ ...meEndpoint(modules.createAuthEndpoint,
252
+ // An agent's access token opens nothing over HTTP but the MCP endpoint: the
253
+ // route answers it `null`. `access.client.verbs.me` below answers it, for the
254
+ // MCP `me` tool, marked for `agentOf`.
255
+ async (headers, request) => meOf(await identityOf(headers, request))),
256
+ ...(kiosk === undefined ? {} : kiosk.endpoints),
257
+ },
258
+ ...(servedOnLan === undefined
259
+ ? watch.init === undefined
260
+ ? {}
261
+ : { init: watch.init }
262
+ : {
263
+ init: (context) => {
264
+ context.logger.warn(`Aventara Access: the kiosk profile is served on ${issuer}, plain http:, under \`profile.insecureLan\` (${servedOnLan.map((range) => range.cidr).join(", ")}). Device credentials, PIN sessions and PINs cross that network in the clear: keep it to the tills alone (a router of their own, WPA3, no guest devices), and run nothing else on the server's address, since cookies are not kept apart by port.`);
265
+ return watch.init?.(context);
266
+ },
267
+ }),
268
+ hooks: {
269
+ before: [
270
+ {
271
+ matcher: (context) => typeof context?.request === "object" &&
272
+ context.request !== null &&
273
+ clientCalls.has(context.request),
274
+ handler: modules.originCheckMiddleware,
275
+ },
276
+ ...(oauth === undefined ? [] : [pkceGuard(modules)]),
277
+ ...watchedBefore,
278
+ ...(kiosk === undefined ? [] : kiosk.hooks.before),
279
+ ],
280
+ after: [
281
+ ...(rehash === undefined ? [] : [rehash]),
282
+ ...watchedAfter,
283
+ ...(kiosk === undefined ? [] : kiosk.hooks.after),
284
+ ],
285
+ },
286
+ };
287
+ const effective = {
288
+ ...block,
289
+ ...(emailAndPassword === undefined ? {} : { emailAndPassword }),
290
+ plugins: [
291
+ ...plugins,
292
+ modules.jwt({
293
+ // A PIN session gets no token (below), so `/get-session` hands out none
294
+ // beside any session on the kiosk profile; `GET /token` still does for a
295
+ // session opened with full credentials.
296
+ ...(kind === "kiosk" ? { disableSettingJwtHeader: true } : {}),
297
+ jwt: {
298
+ issuer,
299
+ audience: issuer,
300
+ expirationTime: `${tokenTtl}s`,
301
+ definePayload: async (source) => {
302
+ // A session on a kiosk device is bound to it: a token would carry
303
+ // it anywhere, past the device's lock.
304
+ if (typeof source.session.kioskDeviceId === "string") {
305
+ throw new modules.APIError("FORBIDDEN", {
306
+ code: "KIOSK_SESSION_NOT_ALLOWED",
307
+ message: "A session bound to a kiosk device is not issued a token.",
308
+ });
309
+ }
310
+ if (cookieCache) {
311
+ await sessionStillLive(() => instance().$context, source.session, () => new modules.APIError("UNAUTHORIZED", {
312
+ message: "The session has ended.",
313
+ }));
314
+ }
315
+ return claimsFor(source);
316
+ },
317
+ },
318
+ }),
319
+ accessPlugin,
320
+ ],
321
+ };
322
+ // Better Auth's tables as it is built (its plugins' fields included): a declared
323
+ // claim the default mapping reads must come from a field the user cannot set, and
324
+ // the rules' schema names each table (on the Prisma adapter, the models it reads;
325
+ // on any other, the app's `authTables`).
326
+ const facts = betterAuthTablesFor({
327
+ options: effective,
328
+ modules,
329
+ checked,
330
+ resources: opened,
331
+ });
332
+ // The engine the framework asks, with Access's built-in rules over Better Auth's
333
+ // tables. Built on the first read of `access.policy`, which is the framework's
334
+ // construction: a `resources` key whose tables are missing refuses the app's
335
+ // start, while a tool that reads only `access.options` (`npx auth generate`,
336
+ // before the schema has the tables) loads the configuration.
337
+ let guarded;
338
+ const policyOf = () => {
339
+ guarded ??= guardedPolicy({
340
+ checked,
341
+ auditLog,
342
+ resources: opened,
343
+ tables: facts.tables,
344
+ naming: facts.naming,
345
+ });
346
+ return guarded;
347
+ };
348
+ let auth;
349
+ const instance = () => {
350
+ auth ??= modules.betterAuth(effective);
351
+ return auth;
352
+ };
353
+ const keys = async () => (await instance().api.getJwks());
354
+ const tokensFor = (resource) => createAccessTokenVerifier({
355
+ keys,
356
+ issuer,
357
+ resource,
358
+ userInfo: userInfoAudience(block, modules),
359
+ context: async () => await instance().$context,
360
+ claimsFor,
361
+ });
362
+ const agents = checkedMcp === undefined ? undefined : tokensFor(checkedMcp.resource);
363
+ const ownTokens = ownAudience === undefined ? undefined : tokensFor(ownAudience);
364
+ const identityOf = requestIdentity({
365
+ auth: instance,
366
+ verifier: createTokenVerifier({
367
+ claims,
368
+ keys,
369
+ issuer,
370
+ tokenTtl,
371
+ }),
372
+ ...(agents === undefined ? {} : { agents }),
373
+ ...(ownTokens === undefined ? {} : { audience: ownTokens }),
374
+ kind,
375
+ crossSite,
376
+ claimsFor,
377
+ });
378
+ const state = requestState(identityOf, checked.principal);
379
+ const client = accessClient({
380
+ block,
381
+ kind,
382
+ claims,
383
+ baseOf: modules.getBaseURL,
384
+ isDynamicBaseURL: modules.isDynamicBaseURLConfig,
385
+ // Read untyped: Better Auth's API type is the app's, and `call` names its endpoints itself.
386
+ api: () => instance().api,
387
+ identityOf: async (headers, request) => verbMeOf(await identityOf(headers, request)),
388
+ calls: clientCalls,
389
+ refusal: modules.APIError,
390
+ identify: signInIdentifier(plugins),
391
+ context: () => instance()
392
+ .$context,
393
+ limit: verbRateLimiter({
394
+ context: async () => (await instance().$context),
395
+ ipOf: modules.getIP,
396
+ refusal: modules.APIError,
397
+ auditLog,
398
+ }),
399
+ });
400
+ const access = {
401
+ client,
402
+ // On the service profile, the handler's own last step: no session cookie
403
+ // leaves, whatever plugin or route set it.
404
+ handler: kind === "service"
405
+ ? async (request) => withoutSessionCookies(await instance().handler(request), sessionCookies((await instance().$context)))
406
+ : (request) => instance().handler(request),
407
+ options: effective,
408
+ $Infer: Object.freeze({}),
409
+ ...(registryStore === undefined
410
+ ? {}
411
+ : { registry: Object.freeze({ ...registryStore.operator }) }),
412
+ ...(kiosk === undefined
413
+ ? {}
414
+ : {
415
+ kiosk: Object.freeze(kiosk.operator(async () => (await instance().$context))),
416
+ }),
417
+ ...(beaconStore === undefined
418
+ ? {}
419
+ : { beacon: Object.freeze(beaconStore) }),
420
+ ...(agents === undefined || server === undefined
421
+ ? {}
422
+ : { mcp: mcpAuthorizationOf(agents, server) }),
423
+ state,
424
+ };
425
+ // `api` is read on use: reading it builds Better Auth. Not enumerable, so a walk
426
+ // over the instance (a configuration check, a log line) builds nothing.
427
+ Object.defineProperty(access, "api", {
428
+ get: () => instance().api,
429
+ enumerable: false,
430
+ });
431
+ // `policy` is built on first read, with the built-in rules over Better Auth's
432
+ // tables; not enumerable either, so a tool that loads the configuration and walks
433
+ // it (`npx auth generate`, which merges what the module exports) runs no startup
434
+ // check. The framework reads it by name.
435
+ Object.defineProperty(access, "policy", {
436
+ get: policyOf,
437
+ enumerable: false,
438
+ });
439
+ return access;
440
+ }
@@ -0,0 +1,120 @@
1
+ import { type LanRange } from "../kiosk/lan-range.policy.js";
2
+ import type { BetterAuthModules } from "./better-auth.loader.js";
3
+ /** Better Auth's options, as the checks read them: any object. */
4
+ export type BetterAuthBlock = {
5
+ readonly [key: string]: unknown;
6
+ };
7
+ /** The id of the Access plugin: `aventaraAccess()`, or the one `createAccess` installs. */
8
+ export declare const ACCESS_PLUGIN_ID = "aventara-access";
9
+ /** The id of Better Auth's `jwt()` plugin. */
10
+ export declare const JWT_PLUGIN_ID = "jwt";
11
+ /** A plugin id as Better Auth's checks could take it: no case, no surrounding space. */
12
+ export declare const normalisedId: (id: unknown) => string | undefined;
13
+ /** How a refusal names the one that installs the plugins it guards. */
14
+ type PluginInstaller = {
15
+ /** Who installs them, as the sentence names it: `createAccess`, `aventaraAccess()`. */
16
+ readonly by: string;
17
+ /** Where the app put the plugin, as the sentence names it: `betterAuth.plugins`, `plugins`. */
18
+ readonly at: string;
19
+ /** The plugins it installs, by id, as a sentence names each. */
20
+ readonly installs: {
21
+ readonly [id: string]: string;
22
+ };
23
+ /** The one plugin of the list that is the installer's own (`aventaraAccess()` in its own list), skipped. */
24
+ readonly own?: unknown;
25
+ /** The installer serves Access's own `me` route (`createAccess`): a plugin defining it is refused too. */
26
+ readonly me?: boolean;
27
+ /** The installer serves the kiosk profile's routes: a plugin defining one (`/kiosk/…`) is refused too. */
28
+ readonly kiosk?: boolean;
29
+ /**
30
+ * The installer serves the registry (`registry: { enabled: true }`): a plugin
31
+ * defining one of its routes, or a middleware whose path reaches them, is refused
32
+ * too. Other plugins' `onRequest` and `hooks` run in the app's own process, inside
33
+ * its trust boundary, and are not checked.
34
+ */
35
+ readonly registry?: boolean;
36
+ };
37
+ /**
38
+ * The plugins of a Better Auth configuration, refusing one the installer puts in
39
+ * itself (under any case or spacing of its id) and one that defines `jwt()`'s
40
+ * endpoints. `ACCESS_PLUGIN_DUPLICATE`, with a sentence naming the plugin.
41
+ */
42
+ export declare const refuseInstalledPlugins: (plugins: unknown, installer: PluginInstaller) => readonly unknown[];
43
+ /**
44
+ * The issuer and audience of every token: the origin of Better Auth's base URL,
45
+ * fixed when Access starts, as Better Auth resolves it (`baseURL`, else its
46
+ * environment variables, `BETTER_AUTH_URL` first), or a dynamic `baseURL`'s
47
+ * `fallback`. Better Auth's own default follows the request's host when the base URL
48
+ * is dynamic or unset; a fixed one keeps a token from one host or app from passing
49
+ * as another's.
50
+ */
51
+ export declare const issuerOf: (block: BetterAuthBlock, modules: Pick<BetterAuthModules, "getBaseURL" | "isDynamicBaseURLConfig">, baseURLAt: string) => string;
52
+ /** Better Auth's base URL as {@link issuerOf} resolves it, as it was written (before the URL parser reads it). */
53
+ export declare const baseURLOf: (block: BetterAuthBlock, modules: Pick<BetterAuthModules, "getBaseURL" | "isDynamicBaseURLConfig">, baseURLAt: string) => string;
54
+ /**
55
+ * Whether the session cookie is `sameSite: "none"`: what Better Auth sets it with,
56
+ * `advanced.cookies.session_token.attributes` over `advanced.defaultCookieAttributes`.
57
+ * Refuses it when Better Auth trusts no origin: a session cookie a browser sends
58
+ * from any site is read only from the origins Better Auth trusts, so there must be
59
+ * some.
60
+ */
61
+ export declare const crossSiteSessionCookie: (block: BetterAuthBlock, trustedOriginsAt: string) => boolean;
62
+ /** Whether Better Auth's cookie cache is on: a session read from its cookie may have been revoked since. */
63
+ export declare const cookieCacheOn: (block: BetterAuthBlock) => boolean;
64
+ /**
65
+ * The kiosk profile's refusals of Better Auth's options, each a way around the
66
+ * device binding of a session:
67
+ *
68
+ * - the cookie cache: a session the device's lock ended would still be answered
69
+ * from its cookie;
70
+ * - `secondaryStorage` without `session.storeSessionInDatabase`: a device's sessions
71
+ * must be found in the database to be ended;
72
+ * - `bearer()`: it hands every session's token to page code (`set-auth-token`) and
73
+ * reads it back from `Authorization`, where no device credential goes with it;
74
+ * - `multiSession()`: it keeps every session the browser signed into and switches
75
+ * back to one by its token, so a cashier would restore the manager before them;
76
+ * - `crossSubDomainCookies` and `SameSite=None`: the device's credential and the
77
+ * session would be sent to other hosts, or from other sites;
78
+ * - a plain http: base URL but on a loopback host, or (`profile.insecureLan`) on an
79
+ * IPv4 address inside one of the till network's ranges: the device's credential
80
+ * and the sessions would cross the network in the clear. On such a LAN address no
81
+ * cookie may be `Secure` (or named `__Secure-` or `__Host-`): a browser drops it
82
+ * over http, and the till would silently hold no session.
83
+ *
84
+ * Answers the ranges an http: base URL is served on under `profile.insecureLan`,
85
+ * for the startup warning; `undefined` when it is https: or loopback.
86
+ */
87
+ export declare const refuseKioskOptions: (block: BetterAuthBlock, plugins: readonly unknown[], cookieCache: boolean,
88
+ /** The app's base URL as written (`baseURLOf`), and where the app set it. */
89
+ written: string, baseURLAt: string,
90
+ /** `profile.insecureLan`, read. */
91
+ insecureLan: readonly LanRange[] | undefined) => readonly LanRange[] | undefined;
92
+ /**
93
+ * Better Auth's Prisma adapter's naming, when `database` is that adapter: read from
94
+ * the adapter the factory builds (its id, and `usePlural`). Building it touches no
95
+ * database, and Better Auth builds its own from the factory again when it starts,
96
+ * with its own options, so this one leaves nothing behind.
97
+ */
98
+ export declare const prismaAdapterOf: (database: unknown, options: unknown) => {
99
+ readonly usePlural: boolean;
100
+ } | undefined;
101
+ /** The organization plugin among `plugins`, with its options; `undefined` when it is not installed. */
102
+ export declare const organizationPluginOf: (plugins: readonly unknown[]) => {
103
+ readonly options: BetterAuthBlock;
104
+ } | undefined;
105
+ /**
106
+ * Refuses an organization plugin whose invitations a session with an unverified
107
+ * email may accept, when Access trusts its memberships (`reads` says how).
108
+ *
109
+ * Better Auth's advisory GHSA-fmh4-wcc4-5jm3: from better-auth 1.6.14 on, with
110
+ * Better Auth's own invitation ids, `acceptInvitation` takes a session whose email
111
+ * matches the invitation's and is not verified, unless the organization plugin sets
112
+ * `requireEmailVerificationOnInvitation: true`. Anyone who signs up with the invited
113
+ * address first and learns the invitation's id (a forwarded email, an admin screen,
114
+ * an invitation list) joins at the invited role. Better Auth ships no fix (the
115
+ * advisory lists `>= 1.6.14` with none; the GitHub advisory database's entry still
116
+ * says `< 1.6.11` only), so Access refuses to trust a membership the plugin let in
117
+ * that way. Fail closed: only `true`, given explicitly, passes.
118
+ */
119
+ export declare const refuseUnverifiedInvitations: (plugins: readonly unknown[], reads: string) => void;
120
+ export {};