@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,135 @@
1
+ import type { AccessAction, AccessOrigin } from "../policies-contract.js";
2
+ /**
3
+ * One audit event.
4
+ *
5
+ * - `sign-in`: a session began (a sign-in, or a sign-up that signs the user in).
6
+ * `origin` is `client` when it came through Access's routes, `application` when the
7
+ * server called Better Auth's API itself. `impersonatedBy` is the admin's user id
8
+ * when the session is an impersonation (Better Auth's `admin()` plugin).
9
+ * - `sign-in-failed`: a route that starts a session refused the caller, or the rate
10
+ * limit refused a server call of `signIn`, `signUp` or `resetPassword`
11
+ * (`access.client`, reason `rate-limited`). `route` is
12
+ * the Better Auth endpoint (`/sign-in/email`), `reason` the code it refused with
13
+ * (`INVALID_EMAIL_OR_PASSWORD`), `target` an HMAC-SHA-256 of the identifier the
14
+ * request named (its email, username or phone number), keyed by Better Auth's
15
+ * secret: the same for every attempt on one identifier, and no way back to it. The
16
+ * identifier itself, and anything else the caller sent, is never recorded.
17
+ * - `sign-out`: a session was ended through `/sign-out`.
18
+ * - `token`: a JWT was issued for a session by `GET /token`, with `impersonatedBy` as
19
+ * for `sign-in`.
20
+ * - `kiosk-switch` (the kiosk profile): a PIN switched the device to `userId`, ending
21
+ * the session of `previousUserId` on it (`null` when the device was locked).
22
+ * `deviceId` is the enrolled device's id. A failed PIN is a `sign-in-failed` on
23
+ * `/kiosk/switch-user`, with its `deviceId`.
24
+ * - `kiosk-lock`: a session on a device ended without a switch: `lock` (asked),
25
+ * `idle` (unused for the profile's `idleTimeout`), or `device-mismatch` (it was
26
+ * brought by a request without its device's credential, so it is ended for both).
27
+ * `userId` is the session's user.
28
+ * - `kiosk-device`: a device `enrolled` or `revoked` (by `userId`, `null` when the
29
+ * app's server did it), or revoked for `token-reuse`: its replaced credential was
30
+ * brought again, so a copy of it is in use somewhere (`userId` is `null`); or a
31
+ * request refused `stale-in-grace` (409): it brought the credential the device's
32
+ * last rotation retired, inside the grace, and nothing changed (`userId` is
33
+ * `null`).
34
+ * - `kiosk-pin`: `userId`'s PIN was set.
35
+ * - `decision`: the engine answered a `can` question. A request's admission (may
36
+ * this caller ever do `action` on `resource`?) is one event, allowed or refused,
37
+ * however many rows the request then reads or writes; a row question (a row a
38
+ * create inserts, or an update leaves) adds an event only when the rules refuse
39
+ * that row. `scopeOf` and `fieldsOf` shape an answer and are not decisions.
40
+ * `reason` names the rule that decided: `grant:<id>`, `deny:<id>`, or `no grant`.
41
+ * `userId` is the `sub` of the token or session the principal came from (through
42
+ * `access.state`); `null` for an anonymous caller or a principal Access did not derive.
43
+ *
44
+ * Anonymous refusals (a `decision` refused with `userId: null`, and every
45
+ * `sign-in-failed`) are logged once a minute for each origin and reason (a refused
46
+ * sign-in, for each route and target too): the first is logged, the repeats in the
47
+ * minute are counted, and when the minute closes the count is logged as a summary
48
+ * event, the first one's fields with `suppressed` (the count), `windowStart` and
49
+ * `windowEnd`. The summary is logged on the next event of any kind after the minute,
50
+ * or by a timer at its end; a sign-in on a target logs its target's count first. A
51
+ * flood of anonymous requests makes two events a minute, not one per request.
52
+ */
53
+ export type AccessAuditEvent = {
54
+ readonly at: Date;
55
+ readonly kind: "sign-in";
56
+ readonly userId: string;
57
+ readonly origin: AccessOrigin;
58
+ readonly impersonatedBy?: string;
59
+ } | {
60
+ readonly at: Date;
61
+ readonly kind: "sign-in-failed";
62
+ readonly userId: null;
63
+ readonly route: string;
64
+ readonly reason: string;
65
+ readonly target?: string;
66
+ /** The kiosk profile's device, for a failed PIN. */
67
+ readonly deviceId?: string;
68
+ readonly origin: AccessOrigin;
69
+ readonly suppressed?: number;
70
+ readonly windowStart?: Date;
71
+ readonly windowEnd?: Date;
72
+ } | {
73
+ readonly at: Date;
74
+ readonly kind: "sign-out";
75
+ readonly userId: string;
76
+ readonly origin: AccessOrigin;
77
+ readonly impersonatedBy?: string;
78
+ } | {
79
+ readonly at: Date;
80
+ readonly kind: "token";
81
+ readonly userId: string;
82
+ readonly origin: AccessOrigin;
83
+ readonly impersonatedBy?: string;
84
+ } | {
85
+ readonly at: Date;
86
+ readonly kind: "kiosk-switch";
87
+ readonly userId: string;
88
+ readonly previousUserId: string | null;
89
+ readonly deviceId: string;
90
+ readonly origin: AccessOrigin;
91
+ } | {
92
+ readonly at: Date;
93
+ readonly kind: "kiosk-lock";
94
+ readonly userId: string;
95
+ readonly deviceId: string;
96
+ readonly reason: "lock" | "idle" | "device-mismatch";
97
+ readonly origin: AccessOrigin;
98
+ } | {
99
+ readonly at: Date;
100
+ readonly kind: "kiosk-device";
101
+ readonly userId: string | null;
102
+ readonly deviceId: string;
103
+ readonly reason: "enrolled" | "revoked" | "token-reuse" | "stale-in-grace";
104
+ readonly origin: AccessOrigin;
105
+ } | {
106
+ readonly at: Date;
107
+ readonly kind: "kiosk-pin";
108
+ readonly userId: string;
109
+ readonly origin: AccessOrigin;
110
+ } | {
111
+ readonly at: Date;
112
+ readonly kind: "decision";
113
+ readonly userId: string | null;
114
+ readonly resource: string;
115
+ readonly action: AccessAction;
116
+ readonly allowed: boolean;
117
+ readonly reason: string;
118
+ readonly origin: AccessOrigin;
119
+ readonly suppressed?: number;
120
+ readonly windowStart?: Date;
121
+ readonly windowEnd?: Date;
122
+ };
123
+ /**
124
+ * The `audit` block: `true` writes each event to the `accessAudit` table, through
125
+ * Better Auth's database adapter (Better Auth's own `generate`/`migrate` create it);
126
+ * `{ sink }` hands each event to the app instead, and the table is not declared.
127
+ * `false`, the default, logs nothing.
128
+ *
129
+ * The request waits for the write or the sink. A sink that throws (or a write that
130
+ * fails) does not fail the request: Better Auth's logger warns, and the request goes on.
131
+ */
132
+ export type AccessAuditOptions = boolean | {
133
+ /** Receives each event; the request waits for it, so queue slow work. */
134
+ readonly sink: (event: AccessAuditEvent) => void | Promise<void>;
135
+ };
@@ -0,0 +1,8 @@
1
+ // The audit log's event and the `audit` option. One shape for both destinations: a
2
+ // row of the `accessAudit` table (`audit: true`) or the argument of the app's sink
3
+ // (`audit: { sink }`).
4
+ //
5
+ // An event holds who, what and the answer, never a credential: no token, password,
6
+ // cookie, key or header reaches it, because it is built field by field from the
7
+ // session's user id and the engine's question, never copied from a request.
8
+ export {};
@@ -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
+ };