@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,122 @@
1
+ import type { DecisionObserver } from "../access.engine.js";
2
+ import type { AccessAuditEvent } from "./audit-event.js";
3
+ /** Where events go: Better Auth's adapter into the table, or the app's function. */
4
+ type AuditDestination = {
5
+ readonly kind: "table";
6
+ } | {
7
+ readonly kind: "sink";
8
+ readonly sink: (event: AccessAuditEvent) => void | Promise<void>;
9
+ };
10
+ /** The part of Better Auth's context the log uses: its adapter and its logger. */
11
+ export type AuditContext = {
12
+ readonly adapter: {
13
+ create(input: {
14
+ model: string;
15
+ data: {
16
+ readonly [field: string]: unknown;
17
+ };
18
+ }): Promise<unknown>;
19
+ };
20
+ readonly logger: {
21
+ warn(message: string): void;
22
+ };
23
+ };
24
+ /** The `audit` option, checked: where events go, or `undefined` when the log is off. */
25
+ export declare const auditDestinationOf: (audit: unknown) => AuditDestination | undefined;
26
+ export type AuditLog = {
27
+ /** Delivers one event; never throws (a failure is logged as a warning). */
28
+ record(event: AccessAuditEvent): Promise<void>;
29
+ /**
30
+ * Closes the repeat windows of refused sign-ins for one sign-in target now,
31
+ * logging their counts: a success for that target follows its failures in the log.
32
+ */
33
+ flushTarget(target: string): Promise<void>;
34
+ };
35
+ /**
36
+ * How many repeat windows stay open at once. Past it, a refusal that would open
37
+ * another closes the oldest window early (its count logged, `windowEnd` now), and
38
+ * opens its own: every target keeps its own row.
39
+ */
40
+ export declare const AUDIT_OPEN_WINDOWS_MAX = 10000;
41
+ /**
42
+ * The log: each event goes to its destination; a failure warns through Better
43
+ * Auth's logger. An anonymous refusal is logged once per window; its repeats are
44
+ * counted, and when the window closes (at its end, or on the next event of any kind
45
+ * after it, or when its sign-in target signs in) the count is logged as a summary:
46
+ * the first event's fields with `suppressed`, `windowStart` and `windowEnd`.
47
+ *
48
+ * The windows are kept in the order they close (each opens a minute after the
49
+ * clock, never before the one opened last), so finding the expired ones stops at
50
+ * the first that is not; at most `AUDIT_OPEN_WINDOWS_MAX` stay open.
51
+ */
52
+ export declare const createAuditLog: (destination: AuditDestination, context: () => Promise<AuditContext>) => AuditLog;
53
+ /**
54
+ * The engine's decision observer: a request's admission is one event; a row
55
+ * question adds one only when the rules refuse that row.
56
+ */
57
+ export declare const decisionObserver: (log: AuditLog, userIdOf: (principal: unknown) => string | null) => DecisionObserver;
58
+ /** A session row, as Better Auth's database hooks hand it over. */
59
+ type SessionRow = {
60
+ readonly userId?: unknown;
61
+ readonly impersonatedBy?: unknown;
62
+ } | null | undefined;
63
+ /** The endpoint context Better Auth's database hooks receive: the request, the endpoint's path and body, and the auth context. */
64
+ type HookContext = {
65
+ readonly request?: unknown;
66
+ readonly path?: unknown;
67
+ readonly body?: unknown;
68
+ readonly context?: {
69
+ readonly secret?: unknown;
70
+ } | undefined;
71
+ } | null | undefined;
72
+ /**
73
+ * The identifier a sign-in request names its user by, as the route that takes it
74
+ * reads it (`email:<address>`, `username:<name>`), normalized as that route
75
+ * normalizes it; `undefined` for a route that names none this way.
76
+ */
77
+ export type SignInIdentifier = (path: unknown, body: unknown) => string | undefined;
78
+ /**
79
+ * A sign-in's target: an HMAC-SHA-256, keyed by Better Auth's secret, of the
80
+ * identifier the request named (`SignInIdentifier`), as 64 hex digits. It ties a
81
+ * refused sign-in to the later ones, and to the success, for the same identifier
82
+ * without storing the identifier: without the secret it cannot be reversed or
83
+ * checked against a guess. `undefined` when the request named none.
84
+ */
85
+ export declare const signInTarget: (identifier: string | undefined, secret: unknown) => Promise<string | undefined>;
86
+ /**
87
+ * Better Auth's database hooks that log sign-ins and sign-outs: a session row
88
+ * created (and committed) is a sign-in; one deleted by `/sign-out` is a sign-out.
89
+ * Through Access's routes it comes from a client; a call to Better Auth's API with
90
+ * no request is the application's own. A sign-in first logs the refused sign-ins
91
+ * counted for its target, so a guessed password shows after its failures.
92
+ */
93
+ export declare const sessionAuditHooks: (log: AuditLog, identify: SignInIdentifier) => {
94
+ session: {
95
+ create: {
96
+ after: (session: SessionRow, context: HookContext) => Promise<void>;
97
+ };
98
+ delete: {
99
+ after: (session: SessionRow, context: HookContext) => Promise<void>;
100
+ };
101
+ };
102
+ };
103
+ /** What an endpoint hook reads of Better Auth's context, once the endpoint has answered. */
104
+ type AuditEndpointContext = {
105
+ readonly path?: unknown;
106
+ readonly request?: unknown;
107
+ readonly body?: unknown;
108
+ readonly context: {
109
+ /** The endpoint's answer: its body, or the `APIError` it refused with. */
110
+ readonly returned?: unknown;
111
+ readonly session?: unknown;
112
+ readonly secret?: unknown;
113
+ };
114
+ };
115
+ /**
116
+ * Better Auth's endpoint hook (after the endpoint answers) that logs a refused
117
+ * sign-in on a route that starts a session (`startsSession`), with its target, and
118
+ * a token issued by `GET /token`. (`/get-session`'s `set-auth-jwt` header is not
119
+ * logged: a page asks for its session on every load.)
120
+ */
121
+ export declare const endpointAudit: (log: AuditLog, startsSession: (path: unknown) => boolean, identify: SignInIdentifier) => (ctx: AuditEndpointContext) => Promise<void>;
122
+ export {};
@@ -0,0 +1,345 @@
1
+ // The audit log: where events go (the `accessAudit` table or the app's sink), the
2
+ // places they come from (Better Auth's session hooks for sign-ins and sign-outs, its
3
+ // endpoint hooks for refused sign-ins and issued tokens, the engine's decision
4
+ // observer for `can` answers), the window that keeps anonymous refusals from
5
+ // flooding it, and the rule that a failing destination never fails the request.
6
+ import { AccessError } from "../access/access.error.js";
7
+ import { KIOSK_SWITCH_PATH } from "../kiosk/kiosk-verbs.schema.js";
8
+ import { isRecord } from "../policies.compiler.js";
9
+ import { AUDIT_MODEL } from "./audit.schema.js";
10
+ /** The `audit` option, checked: where events go, or `undefined` when the log is off. */
11
+ export const auditDestinationOf = (audit) => {
12
+ if (audit === undefined || audit === false) {
13
+ return undefined;
14
+ }
15
+ if (audit === true) {
16
+ return { kind: "table" };
17
+ }
18
+ if (isRecord(audit) &&
19
+ typeof audit.sink === "function" &&
20
+ Object.keys(audit).length === 1) {
21
+ return {
22
+ kind: "sink",
23
+ sink: audit.sink,
24
+ };
25
+ }
26
+ throw new AccessError("ACCESS_OPTIONS_INVALID", "`audit` is `true` (the accessAudit table), `{ sink: (event) => … }` (the app's own destination) or `false`, the default.");
27
+ };
28
+ /** The event as the table stores it: its fields, absent ones left out. */
29
+ const rowOf = (event) => Object.fromEntries(Object.entries(event).filter(([, value]) => value !== undefined));
30
+ /** How long an anonymous refusal is logged once for its origin and reason: a minute. */
31
+ const AUDIT_REPEAT_WINDOW_MS = 60_000;
32
+ /**
33
+ * The window an event shares with its repeats: an anonymous refused decision by
34
+ * origin and reason; a refused sign-in by origin, route, reason and target (the
35
+ * keyed hash of the identifier it named). Every other event is logged each time.
36
+ */
37
+ const repeatKeyOf = (event) => event.kind === "decision" && event.userId === null && !event.allowed
38
+ ? `decision\u0000${event.origin}\u0000${event.reason}`
39
+ : event.kind === "sign-in-failed"
40
+ ? `sign-in-failed\u0000${event.origin}\u0000${event.route}\u0000${event.reason}\u0000${event.target ?? ""}`
41
+ : undefined;
42
+ /**
43
+ * How many repeat windows stay open at once. Past it, a refusal that would open
44
+ * another closes the oldest window early (its count logged, `windowEnd` now), and
45
+ * opens its own: every target keeps its own row.
46
+ */
47
+ export const AUDIT_OPEN_WINDOWS_MAX = 10_000;
48
+ /** A sign-in target's windows, by key: what a sign-in on that target closes. */
49
+ const targetOf = (event) => event.kind === "sign-in-failed" ? event.target : undefined;
50
+ /**
51
+ * The log: each event goes to its destination; a failure warns through Better
52
+ * Auth's logger. An anonymous refusal is logged once per window; its repeats are
53
+ * counted, and when the window closes (at its end, or on the next event of any kind
54
+ * after it, or when its sign-in target signs in) the count is logged as a summary:
55
+ * the first event's fields with `suppressed`, `windowStart` and `windowEnd`.
56
+ *
57
+ * The windows are kept in the order they close (each opens a minute after the
58
+ * clock, never before the one opened last), so finding the expired ones stops at
59
+ * the first that is not; at most `AUDIT_OPEN_WINDOWS_MAX` stay open.
60
+ */
61
+ export const createAuditLog = (destination, context) => {
62
+ const windows = new Map();
63
+ const byTarget = new Map();
64
+ let lastUntil = Number.NEGATIVE_INFINITY;
65
+ let timer;
66
+ /** Logs a closed window's count, when it has one. */
67
+ const summarize = async (window, end) => {
68
+ if (window.suppressed > 0) {
69
+ await deliver({
70
+ ...window.first,
71
+ at: new Date(),
72
+ suppressed: window.suppressed,
73
+ windowStart: new Date(window.start),
74
+ windowEnd: new Date(end),
75
+ });
76
+ }
77
+ };
78
+ /** Closes one window, logging its count when it has one. */
79
+ const close = async (key, window, end) => {
80
+ windows.delete(key);
81
+ const target = targetOf(window.first);
82
+ if (target !== undefined) {
83
+ const keys = byTarget.get(target) ?? [];
84
+ const left = keys.length - 1;
85
+ const at = keys.indexOf(key);
86
+ if (at >= 0) {
87
+ keys[at] = keys[left] ?? key;
88
+ keys.length = left;
89
+ }
90
+ if (keys.length === 0) {
91
+ byTarget.delete(target);
92
+ }
93
+ }
94
+ await summarize(window, end);
95
+ };
96
+ /** Closes every window whose minute is over: the oldest first, stopping at the first still open. */
97
+ const closeExpired = async (now) => {
98
+ for (const [key, window] of windows) {
99
+ if (window.until > now) {
100
+ break;
101
+ }
102
+ await close(key, window, window.until);
103
+ }
104
+ };
105
+ /** Logs the counts when their minute ends, even with no event after it: one timer, at the earliest end. */
106
+ const schedule = (now) => {
107
+ let next;
108
+ for (const window of windows.values()) {
109
+ next = window.until;
110
+ break;
111
+ }
112
+ if (timer !== undefined || next === undefined) {
113
+ return;
114
+ }
115
+ timer = setTimeout(() => {
116
+ timer = undefined;
117
+ void closeExpired(Date.now()).then(() => schedule(Date.now()));
118
+ }, Math.max(0, next - now));
119
+ timer.unref?.();
120
+ };
121
+ return {
122
+ record: async (event) => {
123
+ const now = Date.now();
124
+ await closeExpired(now);
125
+ const key = repeatKeyOf(event);
126
+ if (key !== undefined) {
127
+ const window = windows.get(key);
128
+ if (window !== undefined) {
129
+ window.suppressed += 1;
130
+ schedule(now);
131
+ return;
132
+ }
133
+ lastUntil = Math.max(now + AUDIT_REPEAT_WINDOW_MS, lastUntil);
134
+ for (const [oldest, open] of windows) {
135
+ if (windows.size < AUDIT_OPEN_WINDOWS_MAX) {
136
+ break;
137
+ }
138
+ await close(oldest, open, now);
139
+ }
140
+ windows.set(key, {
141
+ first: event,
142
+ start: now,
143
+ until: lastUntil,
144
+ suppressed: 0,
145
+ });
146
+ const target = targetOf(event);
147
+ if (target !== undefined) {
148
+ const keys = byTarget.get(target);
149
+ if (keys === undefined) {
150
+ byTarget.set(target, [key]);
151
+ }
152
+ else {
153
+ keys.push(key);
154
+ }
155
+ }
156
+ }
157
+ await deliver(event);
158
+ },
159
+ flushTarget: async (target) => {
160
+ const now = Date.now();
161
+ await closeExpired(now);
162
+ for (const key of (byTarget.get(target) ?? []).slice()) {
163
+ const window = windows.get(key);
164
+ if (window !== undefined) {
165
+ await close(key, window, now);
166
+ }
167
+ }
168
+ },
169
+ };
170
+ async function deliver(event) {
171
+ const frozen = Object.freeze({ ...event });
172
+ try {
173
+ if (destination.kind === "sink") {
174
+ await destination.sink(frozen);
175
+ }
176
+ else {
177
+ await (await context()).adapter.create({
178
+ model: AUDIT_MODEL,
179
+ data: rowOf(frozen),
180
+ });
181
+ }
182
+ }
183
+ catch (error) {
184
+ const why = error instanceof Error ? error.message : String(error);
185
+ const what = destination.kind === "sink"
186
+ ? `the audit sink threw on a ${event.kind} event`
187
+ : `could not write a ${event.kind} event to the ${AUDIT_MODEL} table`;
188
+ try {
189
+ (await context()).logger.warn(`Aventara Access: ${what} (${why}); the request goes on.`);
190
+ }
191
+ catch {
192
+ // Better Auth itself is unavailable: nothing left to tell.
193
+ }
194
+ }
195
+ }
196
+ };
197
+ /**
198
+ * The engine's decision observer: a request's admission is one event; a row
199
+ * question adds one only when the rules refuse that row.
200
+ */
201
+ export const decisionObserver = (log, userIdOf) => (question, allowed, rule) => {
202
+ if (question.row !== undefined && allowed) {
203
+ return;
204
+ }
205
+ return log.record({
206
+ at: new Date(),
207
+ kind: "decision",
208
+ userId: userIdOf(question.principal),
209
+ resource: question.resource,
210
+ action: question.action,
211
+ allowed,
212
+ reason: rule === null ? "no grant" : `${rule.kind}:${rule.id}`,
213
+ origin: question.origin,
214
+ });
215
+ };
216
+ const originOf = (context) => (context?.request ? "client" : "application");
217
+ const impersonation = (by) => typeof by === "string" && by !== "" ? { impersonatedBy: by } : {};
218
+ /** The HMAC keys, by Better Auth secret: imported once each. */
219
+ const targetKeys = new Map();
220
+ const encoder = new TextEncoder();
221
+ /**
222
+ * A sign-in's target: an HMAC-SHA-256, keyed by Better Auth's secret, of the
223
+ * identifier the request named (`SignInIdentifier`), as 64 hex digits. It ties a
224
+ * refused sign-in to the later ones, and to the success, for the same identifier
225
+ * without storing the identifier: without the secret it cannot be reversed or
226
+ * checked against a guess. `undefined` when the request named none.
227
+ */
228
+ export const signInTarget = async (identifier, secret) => {
229
+ const subtle = globalThis.crypto?.subtle;
230
+ if (identifier === undefined ||
231
+ typeof secret !== "string" ||
232
+ secret === "" ||
233
+ subtle === undefined) {
234
+ return undefined;
235
+ }
236
+ let key = targetKeys.get(secret);
237
+ if (key === undefined) {
238
+ key = subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
239
+ targetKeys.set(secret, key);
240
+ }
241
+ const mac = await subtle.sign("HMAC", await key, encoder.encode(`aventara-access/sign-in-target:${identifier}`));
242
+ let hex = "";
243
+ for (const byte of new Uint8Array(mac)) {
244
+ hex += byte.toString(16).padStart(2, "0");
245
+ }
246
+ return hex;
247
+ };
248
+ /**
249
+ * Better Auth's database hooks that log sign-ins and sign-outs: a session row
250
+ * created (and committed) is a sign-in; one deleted by `/sign-out` is a sign-out.
251
+ * Through Access's routes it comes from a client; a call to Better Auth's API with
252
+ * no request is the application's own. A sign-in first logs the refused sign-ins
253
+ * counted for its target, so a guessed password shows after its failures.
254
+ */
255
+ export const sessionAuditHooks = (log, identify) => ({
256
+ session: {
257
+ create: {
258
+ after: async (session, context) => {
259
+ // The kiosk's PIN switch logs its own event (`kiosk-switch`).
260
+ if (typeof session?.userId !== "string" ||
261
+ context?.path === KIOSK_SWITCH_PATH) {
262
+ return;
263
+ }
264
+ const target = await signInTarget(identify(context?.path, context?.body), context?.context?.secret);
265
+ if (target !== undefined) {
266
+ await log.flushTarget(target);
267
+ }
268
+ await log.record({
269
+ at: new Date(),
270
+ kind: "sign-in",
271
+ userId: session.userId,
272
+ origin: originOf(context),
273
+ ...impersonation(session.impersonatedBy),
274
+ });
275
+ },
276
+ },
277
+ delete: {
278
+ after: async (session, context) => {
279
+ if (typeof session?.userId !== "string" ||
280
+ context?.path !== "/sign-out") {
281
+ return;
282
+ }
283
+ await log.record({
284
+ at: new Date(),
285
+ kind: "sign-out",
286
+ userId: session.userId,
287
+ origin: originOf(context),
288
+ ...impersonation(session.impersonatedBy),
289
+ });
290
+ },
291
+ },
292
+ },
293
+ });
294
+ /** The session a request ran as, as Better Auth's context holds it: its user and its impersonator. */
295
+ const sessionOf = (value) => {
296
+ const { session, user } = (value ?? {});
297
+ return typeof user?.id === "string"
298
+ ? { userId: user.id, ...impersonation(session?.impersonatedBy) }
299
+ : undefined;
300
+ };
301
+ /** The refusal an endpoint answered with: Better Auth's `APIError`, a 4xx or 5xx. */
302
+ const refusalOf = (returned) => {
303
+ if (!(returned instanceof Error)) {
304
+ return undefined;
305
+ }
306
+ const { statusCode, status, body } = returned;
307
+ if (typeof statusCode !== "number" || statusCode < 400) {
308
+ return undefined;
309
+ }
310
+ return typeof body?.code === "string" && body.code !== ""
311
+ ? body.code
312
+ : typeof status === "string"
313
+ ? status
314
+ : String(statusCode);
315
+ };
316
+ /**
317
+ * Better Auth's endpoint hook (after the endpoint answers) that logs a refused
318
+ * sign-in on a route that starts a session (`startsSession`), with its target, and
319
+ * a token issued by `GET /token`. (`/get-session`'s `set-auth-jwt` header is not
320
+ * logged: a page asks for its session on every load.)
321
+ */
322
+ export const endpointAudit = (log, startsSession, identify) => async (ctx) => {
323
+ const { path, context } = ctx;
324
+ const origin = originOf(ctx);
325
+ const refused = refusalOf(context.returned);
326
+ if (refused !== undefined) {
327
+ if (startsSession(path) && typeof path === "string") {
328
+ const target = await signInTarget(identify(path, ctx.body), context.secret);
329
+ await log.record({
330
+ at: new Date(),
331
+ kind: "sign-in-failed",
332
+ userId: null,
333
+ route: path,
334
+ reason: refused,
335
+ origin,
336
+ ...(target === undefined ? {} : { target }),
337
+ });
338
+ }
339
+ return;
340
+ }
341
+ const session = sessionOf(context.session);
342
+ if (path === "/token" && session !== undefined) {
343
+ await log.record({ at: new Date(), kind: "token", origin, ...session });
344
+ }
345
+ };
@@ -0,0 +1,143 @@
1
+ /** The model the audit log declares, by Better Auth model name. */
2
+ export declare const AUDIT_MODEL = "accessAudit";
3
+ /** The audit table, in Better Auth's plugin schema form. */
4
+ export declare const auditSchema: {
5
+ readonly accessAudit: {
6
+ readonly fields: {
7
+ readonly at: {
8
+ readonly type: "date";
9
+ readonly required: true;
10
+ readonly index: true;
11
+ };
12
+ readonly kind: {
13
+ readonly type: "string";
14
+ readonly required: true;
15
+ };
16
+ readonly userId: {
17
+ readonly type: "string";
18
+ readonly required: false;
19
+ readonly index: true;
20
+ };
21
+ readonly resource: {
22
+ readonly type: "string";
23
+ readonly required: false;
24
+ };
25
+ readonly action: {
26
+ readonly type: "string";
27
+ readonly required: false;
28
+ };
29
+ readonly allowed: {
30
+ readonly type: "boolean";
31
+ readonly required: false;
32
+ };
33
+ readonly reason: {
34
+ readonly type: "string";
35
+ readonly required: false;
36
+ };
37
+ readonly route: {
38
+ readonly type: "string";
39
+ readonly required: false;
40
+ };
41
+ readonly target: {
42
+ readonly type: "string";
43
+ readonly required: false;
44
+ readonly index: true;
45
+ };
46
+ readonly impersonatedBy: {
47
+ readonly type: "string";
48
+ readonly required: false;
49
+ };
50
+ readonly suppressed: {
51
+ readonly type: "number";
52
+ readonly required: false;
53
+ };
54
+ readonly windowStart: {
55
+ readonly type: "date";
56
+ readonly required: false;
57
+ };
58
+ readonly windowEnd: {
59
+ readonly type: "date";
60
+ readonly required: false;
61
+ };
62
+ readonly origin: {
63
+ readonly type: "string";
64
+ readonly required: true;
65
+ };
66
+ };
67
+ };
68
+ };
69
+ /** The audit table with the kiosk profile's columns. */
70
+ export declare const kioskAuditSchema: {
71
+ readonly accessAudit: {
72
+ readonly fields: {
73
+ readonly deviceId: {
74
+ readonly type: "string";
75
+ readonly required: false;
76
+ readonly index: true;
77
+ };
78
+ readonly previousUserId: {
79
+ readonly type: "string";
80
+ readonly required: false;
81
+ };
82
+ readonly at: {
83
+ readonly type: "date";
84
+ readonly required: true;
85
+ readonly index: true;
86
+ };
87
+ readonly kind: {
88
+ readonly type: "string";
89
+ readonly required: true;
90
+ };
91
+ readonly userId: {
92
+ readonly type: "string";
93
+ readonly required: false;
94
+ readonly index: true;
95
+ };
96
+ readonly resource: {
97
+ readonly type: "string";
98
+ readonly required: false;
99
+ };
100
+ readonly action: {
101
+ readonly type: "string";
102
+ readonly required: false;
103
+ };
104
+ readonly allowed: {
105
+ readonly type: "boolean";
106
+ readonly required: false;
107
+ };
108
+ readonly reason: {
109
+ readonly type: "string";
110
+ readonly required: false;
111
+ };
112
+ readonly route: {
113
+ readonly type: "string";
114
+ readonly required: false;
115
+ };
116
+ readonly target: {
117
+ readonly type: "string";
118
+ readonly required: false;
119
+ readonly index: true;
120
+ };
121
+ readonly impersonatedBy: {
122
+ readonly type: "string";
123
+ readonly required: false;
124
+ };
125
+ readonly suppressed: {
126
+ readonly type: "number";
127
+ readonly required: false;
128
+ };
129
+ readonly windowStart: {
130
+ readonly type: "date";
131
+ readonly required: false;
132
+ };
133
+ readonly windowEnd: {
134
+ readonly type: "date";
135
+ readonly required: false;
136
+ };
137
+ readonly origin: {
138
+ readonly type: "string";
139
+ readonly required: true;
140
+ };
141
+ };
142
+ };
143
+ };
@@ -0,0 +1,50 @@
1
+ // The audit table, declared through Better Auth's plugin schema: the Access plugin
2
+ // carries it when `audit: true`, so Better Auth's own migration (or its CLI's
3
+ // `generate` for an ORM's schema) creates it beside Better Auth's tables, and events
4
+ // are written through whatever database adapter the app gave Better Auth. With
5
+ // `audit: { sink }` the plugin declares no table.
6
+ //
7
+ // The name follows the registry's tables (and Better Auth's own plugins, `twoFactor`,
8
+ // `jwks`): a camelCase model, which Better Auth's Prisma generator turns into
9
+ // `AccessAudit`.
10
+ //
11
+ // `userId` is a plain column, not a reference to Better Auth's `user`: a row outlives
12
+ // the user it names, and deleting a user never touches the log.
13
+ /** The model the audit log declares, by Better Auth model name. */
14
+ export const AUDIT_MODEL = "accessAudit";
15
+ const optionalString = { type: "string", required: false };
16
+ /** The audit table, in Better Auth's plugin schema form. */
17
+ export const auditSchema = {
18
+ [AUDIT_MODEL]: {
19
+ fields: {
20
+ at: { type: "date", required: true, index: true },
21
+ kind: { type: "string", required: true },
22
+ userId: { type: "string", required: false, index: true },
23
+ resource: optionalString,
24
+ action: optionalString,
25
+ allowed: { type: "boolean", required: false },
26
+ reason: optionalString,
27
+ route: optionalString,
28
+ target: { type: "string", required: false, index: true },
29
+ impersonatedBy: optionalString,
30
+ suppressed: { type: "number", required: false },
31
+ windowStart: { type: "date", required: false },
32
+ windowEnd: { type: "date", required: false },
33
+ origin: { type: "string", required: true },
34
+ },
35
+ },
36
+ };
37
+ /**
38
+ * The audit table's columns for the kiosk profile's events, added to it only on that
39
+ * profile: the device, and the user a switch replaced.
40
+ */
41
+ const kioskAuditFields = {
42
+ deviceId: { type: "string", required: false, index: true },
43
+ previousUserId: optionalString,
44
+ };
45
+ /** The audit table with the kiosk profile's columns. */
46
+ export const kioskAuditSchema = {
47
+ [AUDIT_MODEL]: {
48
+ fields: { ...auditSchema[AUDIT_MODEL].fields, ...kioskAuditFields },
49
+ },
50
+ };
@@ -0,0 +1,12 @@
1
+ export { type Access, type AccessBetterAuthOptions, type AccessOptions, type AccessPoliciesOptions, type AccessRegistryOptions, type AccessRules, createAccess, } from "../access/access.factory.js";
2
+ export type { AccessClaimsAnswer, AccessClaimsSource, AccessIdentity, } from "../access/claims.mapper.js";
3
+ export type { AccessKioskProfile, AccessProfileKind, AccessProfileOptions, } from "../access/profile.policy.js";
4
+ export type { AccessAuditEvent, AccessAuditOptions, } from "../audit/audit-event.js";
5
+ export type { AccessKiosk, AccessKioskDevice, } from "../kiosk/kiosk.plugin.js";
6
+ export { type AccessAgent, agentOf } from "../mcp/agent.grant.js";
7
+ export type { AccessAgentIdentity, AccessMcpAuthorization, AccessMcpOptions, } from "../mcp/mcp-options.js";
8
+ export type { AccessArgon2idProfile } from "../passwords/argon2id.hasher.js";
9
+ export type { AccessPasswordsOptions } from "../passwords/passwords.options.js";
10
+ export type { AccessNamespaceKeys, AccessPushResult, AccessRegistry, } from "../registry/access.registry.js";
11
+ export type { AccessKeyKind } from "../registry/registry.key.js";
12
+ export type { AccessResourcesOptions } from "../resources/auth-tables.guard.js";
@@ -0,0 +1,2 @@
1
+ export { createAccess, } from "../access/access.factory.js";
2
+ export { agentOf } from "../mcp/agent.grant.js";