@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,155 @@
1
+ import type { AccessWhere } from "./aventara-where.translator.js";
2
+ import { type AccessBeaconPins, type AccessBeaconReader } from "./beacon/beacon.check.js";
3
+ import type { AccessGuardrails, AccessGuardrailsJson } from "./guardrails/guardrails-contract.js";
4
+ import type { AccessLock } from "./lock/lock-contract.js";
5
+ import { type AccessCanQuestion, type AccessFields, type AccessFieldsQuestion, type AccessGrantsQuestion, type AccessRuleOutcome, type AccessScopeQuestion } from "./policies.decider.js";
6
+ import type { AccessPolicies, AccessPoliciesJson } from "./policies-contract.js";
7
+ import type { AccessLookup } from "./predicate.evaluator.js";
8
+ import { type AuthTablesGuard } from "./resources/auth-tables.guard.js";
9
+ /** The `govern` block, by Resource: the list form (each field for both) or a mode per field. */
10
+ type GovernedShape = {
11
+ readonly [resource: string]: readonly string[] | {
12
+ readonly [field: string]: "read" | "write" | "both";
13
+ };
14
+ };
15
+ /** One Resource's fields governed for reading: its list, or its fields not `"write"`. */
16
+ type ReadGoverned<G> = G extends readonly string[] ? G[number] : {
17
+ [F in keyof G]: G[F] extends "write" ? never : F;
18
+ }[keyof G] & string;
19
+ /** `explain`'s answer: the decision, each rule's part in it, and the `exists` lookups. */
20
+ export type AccessExplanation = {
21
+ readonly allowed: boolean;
22
+ readonly grants: readonly {
23
+ readonly id: string;
24
+ readonly subject: string;
25
+ readonly outcome: AccessRuleOutcome;
26
+ }[];
27
+ readonly denies: readonly {
28
+ readonly id: string;
29
+ readonly outcome: AccessRuleOutcome;
30
+ }[];
31
+ readonly lookups: readonly AccessLookup[];
32
+ };
33
+ /** The engine `access.engine()` and `createAccessEngine(policies)` return. */
34
+ export interface AccessEngine<Governed extends GovernedShape = GovernedShape> {
35
+ /**
36
+ * The fields governed for reading (`"read"`, `"both"` or the list form), by
37
+ * Resource: out of every default projection. A `"write"` field is read as any other
38
+ * field, so it is not here.
39
+ */
40
+ readonly governed: {
41
+ readonly [R in keyof Governed as [ReadGoverned<Governed[R]>] extends [never] ? never : R]: Governed[R] extends readonly string[] ? Governed[R] : readonly ReadGoverned<Governed[R]>[];
42
+ };
43
+ /** Every relation path and operator each rule filters on, by rule id. */
44
+ readonly paths: {
45
+ readonly [rule: string]: readonly (readonly string[])[];
46
+ };
47
+ /**
48
+ * The Resources where the row an update leaves is checked: an update grant there
49
+ * reads the row (`where`, `after`, a role held on a row), or an update deny has a
50
+ * `where`.
51
+ */
52
+ readonly afterChecks: {
53
+ readonly [resource: string]: readonly "update"[];
54
+ };
55
+ /**
56
+ * The Resources the rules read as foreign: read in conditions, never granted on,
57
+ * so no caller reads a row of one. Aventara (`@aventara/core` `1.0.0-rc.10` or
58
+ * later) then holds no written reference to one to the caller's read scope: the
59
+ * write grant decides which row may be linked (its `where`, its `after`).
60
+ */
61
+ readonly unheld: readonly string[];
62
+ can(question: AccessCanQuestion): boolean | Promise<boolean>;
63
+ /**
64
+ * The update grants that cover `row`, the row an update in `afterChecks` will
65
+ * change, as it is before the write: each grant's `where` and its subject's row
66
+ * condition, relation conditions asked through `exists`. Core asks it before the
67
+ * write and hands the answer back to `can` as `beforeGrants` (`@aventara/core`
68
+ * `1.0.0-rc.10` or later). The keys are opaque: a grant's place in the file, no
69
+ * rule data.
70
+ */
71
+ grantsOf(question: AccessGrantsQuestion): readonly string[] | Promise<readonly string[]>;
72
+ scopeOf(question: AccessScopeQuestion): boolean | AccessWhere;
73
+ fieldsOf(question: AccessFieldsQuestion): AccessFields;
74
+ /**
75
+ * Called by the framework once it is built on this engine (`@aventara/core`
76
+ * `1.0.0-rc.10` or later): `held` lists the Resources a written reference is
77
+ * held to the caller's read scope of. The rules are checked against them, and a
78
+ * role that may write a reference to one no read grant reaches for it is warned
79
+ * about (ACCESS_REFERENCE_UNREADABLE), and so is a role whose create or update
80
+ * grant may write a reference to a foreign Resource with no condition through
81
+ * that relation (ACCESS_FOREIGN_REFERENCE_UNCHECKED), each once per process and
82
+ * namespace. It changes nothing the engine answers.
83
+ */
84
+ hosted(host: {
85
+ readonly held: readonly string[];
86
+ }): void;
87
+ /** Development only: why `can` answers as it does. Throws when `NODE_ENV` is `production`. */
88
+ explain(question: AccessCanQuestion): Promise<AccessExplanation>;
89
+ /**
90
+ * The startup check that the service reaches the database of each namespace
91
+ * whose Resources the rules read as foreign: its row in `_aventara_beacon`, read
92
+ * through `reader` (the service's own connection), hashes to its pin in `pins`,
93
+ * the pins of `environment` (`avAccess.beacons.<environment>.json`). Await it
94
+ * before the service takes requests: an engine whose rules read another
95
+ * namespace answers no question until it has passed, and none once a check has
96
+ * failed, for good (`ACCESS_BEACON_MISMATCH`); calls run one after another. An
97
+ * engine that reads no other namespace passes at once.
98
+ */
99
+ checkBeacons(reader: AccessBeaconReader, pins: AccessBeaconPins, environment: string): Promise<void>;
100
+ }
101
+ /** What an engine is built with beside its policies file. */
102
+ export type AccessEngineOptions = {
103
+ /**
104
+ * The org's guardrails, as pulled from the registry: deny-only rules on top of the
105
+ * file's own. They only narrow; the governed fields, the paths and the types are
106
+ * the file's.
107
+ */
108
+ readonly guardrails?: AccessGuardrails | AccessGuardrailsJson;
109
+ /**
110
+ * The project's `avAccess.lock.json`, as `avaccess pull` wrote it. With it, the
111
+ * engine refuses to start (`ACCESS_LOCK_MISMATCH`) unless what it runs from is
112
+ * what was pulled: the guardrails hash to the lock's pin (and are given exactly
113
+ * when it pins some), and a policies file handed to `createAccessEngine` hashes
114
+ * to its own. Typed rules (`rules.engine()`) are the project's own source: only
115
+ * their namespace is held to the lock's, unless `requireLock` is set.
116
+ */
117
+ readonly lock?: AccessLock;
118
+ /**
119
+ * `true`: refuse to build without `lock` (`ACCESS_LOCK_MISMATCH`), so a deploy that
120
+ * lost its `avAccess.lock.json` fails at startup instead of running unpinned files;
121
+ * typed rules are then held to the lock's policies hash too. Recommended in
122
+ * production.
123
+ */
124
+ readonly requireLock?: boolean;
125
+ };
126
+ /** Where an engine's policies file comes from: a file handed over, or the project's typed rules. */
127
+ export type PoliciesSource = "file" | "rules";
128
+ /**
129
+ * What decided one `can` question: the grant that allowed it, or the deny that
130
+ * refused it; `null` when it was refused because no grant matched.
131
+ */
132
+ export type DecidingRule = {
133
+ readonly kind: "grant" | "deny";
134
+ readonly id: string;
135
+ } | null;
136
+ /**
137
+ * Told of every `can` answer, after it is known and before `can` returns it: the
138
+ * audit log's hook into the engine. `can` waits for what it returns.
139
+ */
140
+ export type DecisionObserver = (question: AccessCanQuestion, allowed: boolean, rule: DecidingRule) => void | Promise<void>;
141
+ /**
142
+ * Builds the engine from a policies file and, optionally, the org's guardrails. Both
143
+ * are checked first; a bad one throws, naming the rule. With `lock`, both are held
144
+ * to the hashes `avAccess.lock.json` pins (`ACCESS_LOCK_MISMATCH`).
145
+ */
146
+ export declare const createAccessEngine: <Governed extends GovernedShape = GovernedShape>(policies: AccessPolicies<Governed> | AccessPoliciesJson<Governed>, options?: AccessEngineOptions) => AccessEngine<Governed>;
147
+ /**
148
+ * The engine, from a policies file that came from `source`: with a `lock`, a file
149
+ * handed over is held to the lock's pin, and typed rules (the project's own
150
+ * source) to its namespace only. `createAccess` adds its built-in rules over Better
151
+ * Auth's tables (`guard`): their denies join the file's, and `fieldsOf` never lists a
152
+ * secret column or a field a write may not set.
153
+ */
154
+ export declare const engineOf: <Governed extends GovernedShape = GovernedShape>(policies: AccessPolicies<Governed>, options: AccessEngineOptions | undefined, source: PoliciesSource, observe?: DecisionObserver, guard?: AuthTablesGuard) => AccessEngine<Governed>;
155
+ export {};
@@ -0,0 +1,477 @@
1
+ // The engine: a plain object that answers core's `AvPolicyEngine` questions by shape
2
+ // (`can`, `grantsOf`, `scopeOf`, `fieldsOf`, the startup facts `governed`,
3
+ // `paths`, `afterChecks`, `unheld` (the Resources the rules read as foreign), and
4
+ // `hosted`, where the framework says which Resources it holds a written reference
5
+ // to and the rules are checked against them, reference-reads.check.ts and
6
+ // foreign-references.check.ts), run from a `@1` policies file alone. Default deny, any grant
7
+ // allows, any deny wins, order never matters. Neither package imports the other.
8
+ // The decisions themselves are `policies.decider.ts`'s; the engine adds the lock,
9
+ // the guardrails, the decision observer, `explain`, the startup facts and, when it
10
+ // runs from a lock and its rules read other namespaces' Resources as foreign, the
11
+ // startup check that the service reaches their database (`checkBeacons`): until it
12
+ // has passed, the engine decides nothing.
13
+ import { AccessError } from "./access/access.error.js";
14
+ import { operatorOf } from "./aventara-where.translator.js";
15
+ import { beaconProblem, } from "./beacon/beacon.check.js";
16
+ import { warnForeignReferences } from "./foreign-references.check.js";
17
+ import { composeGuardrails } from "./guardrails/guardrails.composer.js";
18
+ import { foreignOwnersOf, verifyLocked } from "./lock/lock.verifier.js";
19
+ import { checkLock } from "./lock/lock-contract.js";
20
+ import { checkPolicies, fail, isRecord } from "./policies.compiler.js";
21
+ import { afterChecksOf, createDecider, fieldsQuestionProblem, grantsQuestionProblem, newTrace, } from "./policies.decider.js";
22
+ import { warnReadGovernedUnique } from "./read-governed-unique.check.js";
23
+ import { warnReferenceReads } from "./reference-reads.check.js";
24
+ import { guardedFields, withBuiltInRules, } from "./resources/auth-tables.guard.js";
25
+ import { governedFor, nullSafe, ownOf, } from "./scope.resolver.js";
26
+ const ENGINE_OPTIONS = new Set(["guardrails", "lock", "requireLock"]);
27
+ /**
28
+ * The engine's own copy of a file handed to it: what is checked and hashed is what
29
+ * runs, whatever the caller does to its object afterwards.
30
+ */
31
+ const copyOf = (what, file) => {
32
+ try {
33
+ return structuredClone(file);
34
+ }
35
+ catch {
36
+ return fail(what, "is JSON data: it holds a value that cannot be copied");
37
+ }
38
+ };
39
+ /** Freezes a copied file all the way down, once it is checked. */
40
+ const deepFreeze = (value) => {
41
+ if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
42
+ Object.freeze(value);
43
+ for (const member of Object.values(value)) {
44
+ deepFreeze(member);
45
+ }
46
+ }
47
+ return value;
48
+ };
49
+ // ---------------------------------------------------------------------------
50
+ // Startup facts
51
+ // ---------------------------------------------------------------------------
52
+ const pathCollector = (policies) => {
53
+ const collect = (predicate, prefix, into, roles = new Set()) => {
54
+ switch (predicate.op) {
55
+ case "and":
56
+ case "or":
57
+ for (const arg of predicate.args) {
58
+ collect(arg, prefix, into, roles);
59
+ }
60
+ return;
61
+ case "not":
62
+ collect(predicate.arg, prefix, into, roles);
63
+ return;
64
+ case "cmp":
65
+ into.push([...prefix, predicate.field, operatorOf(predicate.cmp)]);
66
+ return;
67
+ case "rel":
68
+ collect(predicate.where, [...prefix, predicate.relation], into, roles);
69
+ return;
70
+ case "some":
71
+ case "every":
72
+ case "none":
73
+ collect(predicate.where, [...prefix, predicate.relation, predicate.op], into, roles);
74
+ return;
75
+ case "role": {
76
+ const role = ownOf(policies.roles, predicate.role);
77
+ if (role !== undefined && "on" in role && !roles.has(predicate.role)) {
78
+ collect(role.where, prefix, into, new Set([...roles, predicate.role]));
79
+ }
80
+ return;
81
+ }
82
+ default:
83
+ return;
84
+ }
85
+ };
86
+ return collect;
87
+ };
88
+ /**
89
+ * A rule's condition as the null-safe rewrite reads it: roles followed, a `me` value
90
+ * stood in for by a string (never `null`: a missing one folds the comparison away).
91
+ * Nothing is folded, so every comparison stays to be counted.
92
+ */
93
+ const resolvedOf = (policies, predicate, roles = new Set()) => {
94
+ switch (predicate.op) {
95
+ case "true":
96
+ case "false":
97
+ return { k: predicate.op };
98
+ case "and":
99
+ case "or":
100
+ return {
101
+ k: predicate.op,
102
+ args: predicate.args.map((arg) => resolvedOf(policies, arg, roles)),
103
+ };
104
+ case "not":
105
+ return { k: "not", arg: resolvedOf(policies, predicate.arg, roles) };
106
+ case "cmp": {
107
+ const literal = (operand) => operand !== null && typeof operand === "object"
108
+ ? ""
109
+ : operand;
110
+ return {
111
+ k: "cmp",
112
+ field: predicate.field,
113
+ cmp: predicate.cmp,
114
+ value: Array.isArray(predicate.value)
115
+ ? predicate.value.map(literal)
116
+ : literal(predicate.value),
117
+ };
118
+ }
119
+ case "rel":
120
+ case "some":
121
+ case "every":
122
+ case "none":
123
+ return {
124
+ k: predicate.op,
125
+ relation: predicate.relation,
126
+ target: predicate.target,
127
+ where: resolvedOf(policies, predicate.where, roles),
128
+ };
129
+ case "role": {
130
+ const role = ownOf(policies.roles, predicate.role);
131
+ return role !== undefined && "on" in role && !roles.has(predicate.role)
132
+ ? resolvedOf(policies, role.where, new Set([...roles, predicate.role]))
133
+ : { k: "false" };
134
+ }
135
+ }
136
+ };
137
+ /** The filter paths a condition the null-safe rewrite wrote uses. */
138
+ const resolvedPaths = (node, prefix, into) => {
139
+ switch (node.k) {
140
+ case "true":
141
+ case "false":
142
+ return;
143
+ case "cmp":
144
+ into.push([...prefix, node.field, operatorOf(node.cmp)]);
145
+ return;
146
+ case "and":
147
+ case "all":
148
+ case "or":
149
+ for (const arg of node.args) {
150
+ resolvedPaths(arg, prefix, into);
151
+ }
152
+ return;
153
+ case "not":
154
+ resolvedPaths(node.arg, prefix, into);
155
+ return;
156
+ case "rel":
157
+ resolvedPaths(node.where, [...prefix, node.relation], into);
158
+ return;
159
+ case "some":
160
+ case "every":
161
+ case "none":
162
+ resolvedPaths(node.where, [...prefix, node.relation, node.k], into);
163
+ }
164
+ };
165
+ /**
166
+ * The paths the null-safe rewrite adds to a condition on `resource`, read as it is
167
+ * (a grant, an `exists` question) and, for a deny, negated too (its scope): the
168
+ * `IS NULL` guards (`equals`), a reference's `IS NULL`, `not` for a `notIn` left
169
+ * empty, and the `none` and `some` an `every` becomes.
170
+ */
171
+ const nullSafePaths = (policies, resource, predicate, negated, into) => {
172
+ const node = resolvedOf(policies, predicate);
173
+ resolvedPaths(nullSafe(policies, resource, node), [resource], into);
174
+ if (negated) {
175
+ resolvedPaths(nullSafe(policies, resource, { k: "not", arg: node }), [resource], into);
176
+ }
177
+ };
178
+ /** The to-one steps at a created row's own level, roles followed: each is one `exists` on its target. */
179
+ const createSteps = (policies, predicate, roles = new Set()) => {
180
+ switch (predicate.op) {
181
+ case "and":
182
+ case "or":
183
+ return predicate.args.flatMap((arg) => createSteps(policies, arg, roles));
184
+ case "not":
185
+ return createSteps(policies, predicate.arg, roles);
186
+ case "rel":
187
+ return [predicate];
188
+ case "role": {
189
+ const role = ownOf(policies.roles, predicate.role);
190
+ return role !== undefined && "on" in role && !roles.has(predicate.role)
191
+ ? createSteps(policies, role.where, new Set([...roles, predicate.role]))
192
+ : [];
193
+ }
194
+ default:
195
+ return [];
196
+ }
197
+ };
198
+ const subjectPredicate = (policies, grant, subject) => {
199
+ const role = ownOf(policies.roles, subject);
200
+ if (role === undefined || !("on" in role)) {
201
+ return undefined;
202
+ }
203
+ const held = { op: "role", role: subject };
204
+ return grant.in === undefined
205
+ ? held
206
+ : { op: "rel", relation: grant.in, target: role.on, where: held };
207
+ };
208
+ const pathsOf = (policies) => {
209
+ const collect = pathCollector(policies);
210
+ const paths = {};
211
+ const existsPaths = (resource, predicate, into) => {
212
+ for (const step of createSteps(policies, predicate)) {
213
+ const relation = ownOf(ownOf(policies.schema, resource)?.relations, step.relation);
214
+ for (const reference of relation?.references ?? []) {
215
+ into.push([step.target, reference, "equals"]);
216
+ }
217
+ collect(step.where, [step.target], into);
218
+ nullSafePaths(policies, step.target, step.where, false, into);
219
+ }
220
+ };
221
+ const keep = (rule, found) => {
222
+ const unique = new Map(found.map((path) => [JSON.stringify(path), path]));
223
+ if (unique.size > 0) {
224
+ paths[rule] = [...unique.values()];
225
+ }
226
+ };
227
+ for (const grant of policies.grants) {
228
+ if (grant.resource === "*") {
229
+ continue;
230
+ }
231
+ const found = [];
232
+ const parts = [
233
+ grant.where,
234
+ ...grant.subjects.map((subject) => subjectPredicate(policies, grant, subject)),
235
+ ].filter((part) => part !== undefined);
236
+ for (const part of parts) {
237
+ collect(part, [grant.resource], found);
238
+ nullSafePaths(policies, grant.resource, part, false, found);
239
+ }
240
+ if (grant.after !== undefined) {
241
+ collect(grant.after, [grant.resource], found);
242
+ nullSafePaths(policies, grant.resource, grant.after, false, found);
243
+ const identifier = ownOf(policies.schema, grant.resource)?.identifiers[0] ?? [];
244
+ for (const field of identifier) {
245
+ found.push([grant.resource, field, "equals"]);
246
+ }
247
+ }
248
+ if (grant.actions.includes("create")) {
249
+ for (const part of parts) {
250
+ existsPaths(grant.resource, part, found);
251
+ }
252
+ }
253
+ keep(grant.id, found);
254
+ }
255
+ for (const deny of policies.denies) {
256
+ if (deny.where === undefined) {
257
+ continue;
258
+ }
259
+ const found = [];
260
+ collect(deny.where, [deny.resource], found);
261
+ // A scope (read, update, delete) negates the deny; a create only asks it as it is.
262
+ const scoped = deny.actions === "*" ||
263
+ deny.actions.some((action) => action !== "create");
264
+ nullSafePaths(policies, deny.resource, deny.where, scoped, found);
265
+ if (deny.actions === "*" || deny.actions.includes("create")) {
266
+ existsPaths(deny.resource, deny.where, found);
267
+ }
268
+ keep(deny.id, found);
269
+ }
270
+ return paths;
271
+ };
272
+ // ---------------------------------------------------------------------------
273
+ // Decisions
274
+ // ---------------------------------------------------------------------------
275
+ /** The rule a trace shows deciding: the first grant matched, or the first deny that counted (fail closed). */
276
+ const decidingRule = (trace, allowed) => {
277
+ if (allowed) {
278
+ for (const [grant, outcome] of trace.grants) {
279
+ if (outcome === "matched") {
280
+ return { kind: "grant", id: grant.grant.id };
281
+ }
282
+ }
283
+ return null;
284
+ }
285
+ for (const [deny, outcome] of trace.denies) {
286
+ if (outcome === "matched" || outcome === "unknown") {
287
+ return { kind: "deny", id: deny.deny.id };
288
+ }
289
+ }
290
+ return null;
291
+ };
292
+ /** Refuses a `fieldsOf` question the types rule out but a caller can still send (the decider refuses it too). */
293
+ const checkFieldsQuestion = (question) => {
294
+ const problem = fieldsQuestionProblem(question);
295
+ if (problem !== undefined) {
296
+ throw new AccessError("ACCESS_QUESTION_INVALID", problem);
297
+ }
298
+ };
299
+ /**
300
+ * Builds the engine from a policies file and, optionally, the org's guardrails. Both
301
+ * are checked first; a bad one throws, naming the rule. With `lock`, both are held
302
+ * to the hashes `avAccess.lock.json` pins (`ACCESS_LOCK_MISMATCH`).
303
+ */
304
+ export const createAccessEngine = (policies, options) => engineOf(policies, options, "file");
305
+ /**
306
+ * The engine, from a policies file that came from `source`: with a `lock`, a file
307
+ * handed over is held to the lock's pin, and typed rules (the project's own
308
+ * source) to its namespace only. `createAccess` adds its built-in rules over Better
309
+ * Auth's tables (`guard`): their denies join the file's, and `fieldsOf` never lists a
310
+ * secret column or a field a write may not set.
311
+ */
312
+ export const engineOf = (policies, options, source, observe, guard) => {
313
+ // The engine's own copy, checked, held to the lock, then frozen: nothing the
314
+ // caller does to its objects afterwards reaches what the engine runs from.
315
+ const own = copyOf("policies", policies);
316
+ checkPolicies(own);
317
+ warnReadGovernedUnique(own);
318
+ if (options !== undefined &&
319
+ (!isRecord(options) ||
320
+ Object.keys(options).some((key) => !ENGINE_OPTIONS.has(key)) ||
321
+ (options.requireLock !== undefined &&
322
+ typeof options.requireLock !== "boolean"))) {
323
+ fail("engine", "the engine's options are `{ guardrails?, lock?, requireLock?: boolean }`");
324
+ }
325
+ // A guardrails file imported as JSON is typed by its outline only; it is checked
326
+ // here (`composeGuardrails`), as a typed one is.
327
+ const guardrails = options?.guardrails === undefined
328
+ ? undefined
329
+ : copyOf("guardrails", options.guardrails);
330
+ if (options?.lock === undefined) {
331
+ if (options?.requireLock === true) {
332
+ throw new AccessError("ACCESS_LOCK_MISMATCH", "`requireLock` is set and no `lock` was given: pass the project's avAccess.lock.json (`avaccess pull` writes it), so the engine runs only from the files it pins");
333
+ }
334
+ }
335
+ else {
336
+ verifyLocked(checkLock(options.lock, "engine"), {
337
+ namespace: own.namespace,
338
+ // Typed rules are the project's own source; with `requireLock` they are held to
339
+ // the lock's pin too, so a lock that pins other rules is refused.
340
+ ...(source === "file" || options.requireLock === true
341
+ ? { policies: own }
342
+ : {}),
343
+ guardrails,
344
+ });
345
+ }
346
+ // The namespaces the rules read as foreign, from the file's own markers: with a
347
+ // lock or without, they are read only once the beacons have passed.
348
+ const owners = foreignOwnersOf(own);
349
+ const guarded = guardrails === undefined ? own : composeGuardrails(own, guardrails);
350
+ const file = deepFreeze(guard === undefined ? guarded : withBuiltInRules(guarded, guard));
351
+ if (file !== own) {
352
+ checkPolicies(file, true);
353
+ }
354
+ deepFreeze(own);
355
+ const decider = createDecider(file);
356
+ // The beacons' state: `undefined` once they have passed (or when the rules read
357
+ // no other namespace), otherwise why the engine answers nothing yet.
358
+ let refusal = owners.length === 0
359
+ ? undefined
360
+ : `the rules read ${owners.map((owner) => `namespace "${owner}"'s`).join(" and ")} Resources as foreign, and the beacons were not checked: \`await access.policy.checkBeacons(beacon, beaconPins, environment)\` (\`engine.checkBeacons\` on an engine of your own) when the service starts, before it takes requests, with this environment's avAccess.beacons.<environment>.json`;
361
+ // A failed check latches: no later check opens the engine again.
362
+ let failed;
363
+ // The checks, one after another.
364
+ let checking = Promise.resolve();
365
+ const open = () => {
366
+ if (refusal !== undefined) {
367
+ throw new AccessError("ACCESS_BEACON_MISMATCH", refusal);
368
+ }
369
+ };
370
+ const fieldsOf = (question) => {
371
+ open();
372
+ checkFieldsQuestion(question);
373
+ return guard === undefined
374
+ ? decider.fieldsOf(question)
375
+ : guardedFields(guard, question, decider.fieldsOf(question), {
376
+ fields: ownOf(file.schema, question.resource)?.fields ?? [],
377
+ // What `"all"` stands for: every field reads do not govern.
378
+ governed: governedFor(file, question.resource, "read"),
379
+ });
380
+ };
381
+ // The seam's `governed`: the fields reads leave out. The built-in rules govern
382
+ // for writing only, so the file's own reads are the same.
383
+ const governed = deepFreeze(Object.fromEntries(Object.keys(file.govern).flatMap((resource) => {
384
+ const fields = governedFor(file, resource, "read");
385
+ return fields.length === 0 ? [] : [[resource, [...fields]]];
386
+ })));
387
+ return {
388
+ governed: governed,
389
+ paths: deepFreeze(pathsOf(file)),
390
+ afterChecks: deepFreeze(afterChecksOf(file)),
391
+ unheld: Object.freeze(Object.entries(own.schema)
392
+ .filter(([, facts]) => facts.foreign !== undefined)
393
+ .map(([resource]) => resource)),
394
+ can: observe === undefined
395
+ ? (question) => {
396
+ open();
397
+ return decider.can(question, newTrace());
398
+ }
399
+ : (question) => {
400
+ open();
401
+ const trace = newTrace();
402
+ const report = async (allowed) => {
403
+ await observe(question, allowed, decidingRule(trace, allowed));
404
+ return allowed;
405
+ };
406
+ const answer = decider.can(question, trace);
407
+ return typeof answer === "boolean"
408
+ ? report(answer)
409
+ : answer.then(report);
410
+ },
411
+ grantsOf: (question) => {
412
+ open();
413
+ const problem = grantsQuestionProblem(question);
414
+ if (problem !== undefined) {
415
+ throw new AccessError("ACCESS_QUESTION_INVALID", problem);
416
+ }
417
+ return decider.grantsOf(question);
418
+ },
419
+ scopeOf: (question) => {
420
+ open();
421
+ return decider.scopeOf(question);
422
+ },
423
+ fieldsOf,
424
+ hosted: (host) => {
425
+ const held = isRecord(host) ? host.held : undefined;
426
+ if (!Array.isArray(held) ||
427
+ !held.every((resource) => typeof resource === "string")) {
428
+ throw new AccessError("ACCESS_QUESTION_INVALID", "`hosted` takes `{ held }`, the list of Resource names the framework holds a written reference to");
429
+ }
430
+ const served = new Set(held);
431
+ warnReferenceReads(own, served, guard?.closed);
432
+ warnForeignReferences(own, served);
433
+ },
434
+ explain: async (question) => {
435
+ open();
436
+ if (globalThis.process
437
+ ?.env?.NODE_ENV === "production") {
438
+ throw new Error("Aventara Access: explain() is for development only");
439
+ }
440
+ const trace = newTrace();
441
+ const allowed = await decider.can(question, trace);
442
+ return {
443
+ allowed,
444
+ grants: [...trace.grants].map(([grant, outcome]) => ({
445
+ id: grant.grant.id,
446
+ subject: grant.subject,
447
+ outcome,
448
+ })),
449
+ denies: [...trace.denies].map(([deny, outcome]) => ({
450
+ id: deny.deny.id,
451
+ outcome,
452
+ })),
453
+ lookups: trace.lookups,
454
+ };
455
+ },
456
+ checkBeacons: (reader, pins, environment) => {
457
+ const run = async () => {
458
+ if (owners.length === 0) {
459
+ return;
460
+ }
461
+ if (failed !== undefined) {
462
+ throw new AccessError("ACCESS_BEACON_MISMATCH", `a beacon check failed before, and the engine stays shut: ${failed}`);
463
+ }
464
+ const problem = await beaconProblem(own.namespace ?? "", owners, pins, environment, reader);
465
+ if (problem !== undefined) {
466
+ failed = problem;
467
+ refusal = problem;
468
+ throw new AccessError("ACCESS_BEACON_MISMATCH", problem);
469
+ }
470
+ refusal = undefined;
471
+ };
472
+ const result = checking.then(run);
473
+ checking = result.catch(() => undefined);
474
+ return result;
475
+ },
476
+ };
477
+ };