@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,103 @@
1
+ import { type AccessWhere } from "./aventara-where.translator.js";
2
+ import type { AccessAction, AccessOrigin, AccessPolicies } from "./policies-contract.js";
3
+ import { type AccessExists, type AccessLookup, type AccessRow } from "./predicate.evaluator.js";
4
+ import { type FoldedDeny, type FoldedGrant } from "./scope.resolver.js";
5
+ /** What every question states. */
6
+ export type AccessQuestion = {
7
+ /** What the host derived for the request: an object, or `null` (anonymous). */
8
+ readonly principal: unknown;
9
+ readonly origin: AccessOrigin;
10
+ readonly resource: string;
11
+ };
12
+ /** `can`: without a row, admission; with one, the row a create inserts or an update leaves. */
13
+ export type AccessCanQuestion = AccessQuestion & {
14
+ readonly action: AccessAction;
15
+ readonly row?: AccessRow;
16
+ /**
17
+ * On an update with a `row` only: the row as it was before the update. One update
18
+ * grant must then cover both rows, its `where` this one and its `where` and
19
+ * `after` the row the update leaves. Without it, any update grant that covers the
20
+ * row the update leaves lets it in. Read nowhere else.
21
+ */
22
+ readonly before?: AccessRow;
23
+ /**
24
+ * On an update with a `row` only: what `grantsOf` answered about the row before the
25
+ * update, asked before the write. The grant that covers `row` (its `where` and
26
+ * `after`) must then be one of these. With `before` too, each such grant's `where`
27
+ * is checked on `before` in memory, so a list from another row or principal fails
28
+ * on a scalar condition; a relation condition on it is not asked again: it was
29
+ * decided before anything the update wrote. Read nowhere else.
30
+ */
31
+ readonly beforeGrants?: readonly string[];
32
+ readonly exists?: AccessExists;
33
+ };
34
+ /**
35
+ * `grantsOf`: the update grants that cover `row`, the row an update will change, as it
36
+ * is now. Ask it before the write, and hand its answer back to `can` as `beforeGrants`.
37
+ */
38
+ export type AccessGrantsQuestion = AccessQuestion & {
39
+ readonly action: "update";
40
+ readonly row: AccessRow;
41
+ readonly exists?: AccessExists;
42
+ };
43
+ /** `scopeOf`: the rows the principal may read, update or delete. */
44
+ export type AccessScopeQuestion = AccessQuestion & {
45
+ readonly action: "read" | "update" | "delete";
46
+ };
47
+ /** `fieldsOf`: reading, or writing by one action. */
48
+ export type AccessFieldsQuestion = AccessQuestion & ({
49
+ readonly access: "read";
50
+ } | {
51
+ readonly access: "write";
52
+ readonly action?: "create" | "update";
53
+ });
54
+ /** `fieldsOf`'s answer: fields usable on every reachable row, and fields usable on some rows. */
55
+ export type AccessFields = {
56
+ readonly always: "all" | readonly string[];
57
+ readonly perRow: {
58
+ readonly [field: string]: AccessWhere;
59
+ };
60
+ };
61
+ /** How one rule took part in a decision. */
62
+ export type AccessRuleOutcome = "matched" | "not matched" | "unknown" | "not asked";
63
+ /** How each rule took part in one `can` decision, and the `exists` lookups it made. */
64
+ export type DecisionTrace = {
65
+ readonly grants: Map<FoldedGrant, AccessRuleOutcome>;
66
+ readonly denies: Map<FoldedDeny, AccessRuleOutcome>;
67
+ readonly lookups: AccessLookup[];
68
+ };
69
+ /** A trace with nothing in it yet. */
70
+ export declare const newTrace: () => DecisionTrace;
71
+ /**
72
+ * The Resources where the row an update leaves can fail what allowed the update:
73
+ * an update grant whose condition reads the row (a `where`, an `after`, a role held
74
+ * on a row), or an update deny with a `where`. Core asks `can` about each row such
75
+ * an update changed, and refuses these updates where it has no interactive
76
+ * transaction to roll one back in.
77
+ */
78
+ export declare const afterChecksOf: (policies: AccessPolicies) => Record<string, "update"[]>;
79
+ /**
80
+ * What is wrong with a `fieldsOf` question the types rule out but a caller can still
81
+ * send, or `undefined`: answered as asked, it would read as a read, or as no field.
82
+ */
83
+ export declare const fieldsQuestionProblem: (question: AccessFieldsQuestion) => string | undefined;
84
+ /**
85
+ * What is wrong with a `grantsOf` question the types rule out but a caller can still
86
+ * send, or `undefined`: it asks about an update, and nothing else.
87
+ */
88
+ export declare const grantsQuestionProblem: (question: AccessGrantsQuestion) => string | undefined;
89
+ /** The questions a policies file answers. */
90
+ export type PoliciesDecider = {
91
+ /** `can`, recording each rule's part in the answer in `trace`. */
92
+ can(question: AccessCanQuestion, trace: DecisionTrace): boolean | Promise<boolean>;
93
+ /**
94
+ * The keys of the update grants whose `where`, and their subject's row condition,
95
+ * hold on `row` as it is now: opaque and stable for this file, never rule data.
96
+ * None when an update deny is decided on `row` in memory.
97
+ */
98
+ grantsOf(question: AccessGrantsQuestion): readonly string[] | Promise<readonly string[]>;
99
+ scopeOf(question: AccessScopeQuestion): boolean | AccessWhere;
100
+ fieldsOf(question: AccessFieldsQuestion): AccessFields;
101
+ };
102
+ /** The decisions of one policies file, already checked. */
103
+ export declare const createDecider: (file: AccessPolicies) => PoliciesDecider;
@@ -0,0 +1,382 @@
1
+ // The decisions a policies file makes: `can`, `grantsOf`, `scopeOf` and `fieldsOf`,
2
+ // answered from the file alone. Default deny, any grant allows, any deny wins, order
3
+ // never matters. The engine (`access.engine.ts`) runs this with its lock, guardrails,
4
+ // decision observer and `explain` around it; `avaccess generate` copies this module,
5
+ // with the modules it imports, into a service's generated client.
6
+ //
7
+ // Like those modules, it imports nothing but them: no Node.js, no package.
8
+ import { translateAnswer, translateWhere, } from "./aventara-where.translator.js";
9
+ import { canonicalJson } from "./canonical-json.writer.js";
10
+ import { decideOnRow, evaluateOnRow, } from "./predicate.evaluator.js";
11
+ import { FALSE, foldDenies, foldGrants, nullSafe, orOf, ownOf, principalFactsOf, rulesFor, scopeCondition, TRUE, } from "./scope.resolver.js";
12
+ import { sha256Hex } from "./sha256.digest.js";
13
+ /** A trace with nothing in it yet. */
14
+ export const newTrace = () => ({
15
+ grants: new Map(),
16
+ denies: new Map(),
17
+ lookups: [],
18
+ });
19
+ /**
20
+ * The Resources where the row an update leaves can fail what allowed the update:
21
+ * an update grant whose condition reads the row (a `where`, an `after`, a role held
22
+ * on a row), or an update deny with a `where`. Core asks `can` about each row such
23
+ * an update changed, and refuses these updates where it has no interactive
24
+ * transaction to roll one back in.
25
+ */
26
+ export const afterChecksOf = (policies) => {
27
+ const afterChecks = {};
28
+ const reads = (grant) => grant.where !== undefined ||
29
+ grant.after !== undefined ||
30
+ grant.in !== undefined ||
31
+ grant.subjects.some((subject) => {
32
+ const role = ownOf(policies.roles, subject);
33
+ return role !== undefined && "on" in role;
34
+ });
35
+ for (const grant of policies.grants) {
36
+ if (grant.resource !== "*" &&
37
+ grant.actions.includes("update") &&
38
+ reads(grant)) {
39
+ afterChecks[grant.resource] = ["update"];
40
+ }
41
+ }
42
+ for (const deny of policies.denies) {
43
+ if (deny.resource !== "*" &&
44
+ deny.where !== undefined &&
45
+ (deny.actions === "*" || deny.actions.includes("update"))) {
46
+ afterChecks[deny.resource] = ["update"];
47
+ }
48
+ }
49
+ return afterChecks;
50
+ };
51
+ /**
52
+ * The fields each grant's `write.except` locks. A relation and its reference fields
53
+ * are locked together: writing the relation (`$connect`, `$set`) writes those
54
+ * fields, and writing a reference field moves the relation, so naming either one
55
+ * locks both.
56
+ */
57
+ const lockedFieldsOf = (policies) => {
58
+ const locks = new Map();
59
+ for (const grant of policies.grants) {
60
+ if (grant.write === undefined) {
61
+ continue;
62
+ }
63
+ const named = new Set(grant.write.except);
64
+ const locked = new Set(named);
65
+ const relations = ownOf(policies.schema, grant.resource)?.relations ?? {};
66
+ for (const [relation, fact] of Object.entries(relations)) {
67
+ const references = fact.referenceFields ?? [];
68
+ if (named.has(relation) || references.some((field) => named.has(field))) {
69
+ locked.add(relation);
70
+ for (const field of references) {
71
+ locked.add(field);
72
+ }
73
+ }
74
+ }
75
+ locks.set(grant, locked);
76
+ }
77
+ return locks;
78
+ };
79
+ /**
80
+ * What is wrong with a `fieldsOf` question the types rule out but a caller can still
81
+ * send, or `undefined`: answered as asked, it would read as a read, or as no field.
82
+ */
83
+ export const fieldsQuestionProblem = (question) => {
84
+ const { access, action } = question;
85
+ const fits = access === "read"
86
+ ? action === undefined
87
+ : access === "write" &&
88
+ (action === undefined || action === "create" || action === "update");
89
+ return fits
90
+ ? undefined
91
+ : `fieldsOf takes \`access: "read"\`, or \`access: "write"\` with an optional \`action\` ("create" or "update"); it was asked ${JSON.stringify({ access, action })}`;
92
+ };
93
+ const outcomeOf = (truth) => truth === "unknown" ? "unknown" : truth ? "matched" : "not matched";
94
+ const isDecided = (node) => node.k === "true" || node.k === "false";
95
+ /**
96
+ * What is wrong with a `grantsOf` question the types rule out but a caller can still
97
+ * send, or `undefined`: it asks about an update, and nothing else.
98
+ */
99
+ export const grantsQuestionProblem = (question) => {
100
+ const { action } = question;
101
+ return action === "update"
102
+ ? undefined
103
+ : `grantsOf takes \`action: "update"\`; it was asked ${JSON.stringify({ action })}`;
104
+ };
105
+ /** The decisions of one policies file, already checked. */
106
+ export const createDecider = (file) => {
107
+ const locks = lockedFieldsOf(file);
108
+ // A grant's key: a short hash of the file's grants, in their order, then its place
109
+ // in the file and its subject's place in the grant. It names a grant for one
110
+ // subject without carrying any of its data, a list from another file names no grant
111
+ // of this one, and it is the same for the folded grant `grantsOf` reads (its
112
+ // `where`) and the one `can` reads (its `where` and `after`).
113
+ const mark = sha256Hex(canonicalJson(file.grants)).slice(0, 8);
114
+ const places = new Map(file.grants.map((grant, index) => [grant, index]));
115
+ const keyOf = (grant) => `${mark}:${places.get(grant.grant) ?? -1}.${grant.grant.subjects.indexOf(grant.subject)}`;
116
+ const can = (question, trace) => {
117
+ const facts = principalFactsOf(question.principal, question.origin);
118
+ const { grants, denies } = rulesFor(file, question.resource, question.action);
119
+ const { row } = question;
120
+ if (row === undefined) {
121
+ // Admission: could this principal ever do this? Row conditions are the scope's.
122
+ const folded = foldGrants(file, grants, facts);
123
+ const foldedDenies = foldDenies(file, denies, facts);
124
+ for (const grant of folded) {
125
+ trace.grants.set(grant, grant.condition.k === "false" ? "not matched" : "matched");
126
+ }
127
+ for (const deny of foldedDenies) {
128
+ trace.denies.set(deny, deny.condition.k === "true" ? "matched" : "not matched");
129
+ }
130
+ return (folded.some((grant) => grant.condition.k !== "false") &&
131
+ !foldedDenies.some((deny) => deny.condition.k === "true"));
132
+ }
133
+ if (typeof row !== "object" || row === null) {
134
+ return false;
135
+ }
136
+ // The row a create inserts, or the row an update leaves (core's after check, or
137
+ // any caller's): an update's row must still meet the `where` and the `after` of
138
+ // one update grant, and no update deny may match it.
139
+ const isUpdate = question.action === "update";
140
+ const kind = question.action === "create" ? "new" : "existing";
141
+ // With the row before the update: the grant that covers the row
142
+ // the update leaves must be one whose `where` covered the row before it, so no
143
+ // update moves a row from the rows one grant reaches to another's. With
144
+ // `beforeGrants`, what covered it was decided before the write (`grantsOf`):
145
+ // `before` is still held to each listed grant's `where` in memory, since the list
146
+ // is the caller's to hand back, but a relation condition on it is not asked again.
147
+ const beforeGrants = isUpdate ? question.beforeGrants : undefined;
148
+ if (beforeGrants !== undefined &&
149
+ (!Array.isArray(beforeGrants) ||
150
+ beforeGrants.some((key) => typeof key !== "string"))) {
151
+ return false;
152
+ }
153
+ const coveredBefore = beforeGrants === undefined ? undefined : new Set(beforeGrants);
154
+ const before = isUpdate ? question.before : undefined;
155
+ if (before !== undefined &&
156
+ (typeof before !== "object" || before === null)) {
157
+ return false;
158
+ }
159
+ const folded = foldGrants(file, grants, facts, isUpdate);
160
+ const reached = before === undefined ? undefined : foldGrants(file, grants, facts);
161
+ const foldedDenies = foldDenies(file, denies, facts);
162
+ const grantResiduals = [];
163
+ let granted = false;
164
+ for (const [index, grant] of folded.entries()) {
165
+ if (coveredBefore !== undefined && !coveredBefore.has(keyOf(grant))) {
166
+ // It did not cover the row before the update.
167
+ trace.grants.set(grant, "not matched");
168
+ continue;
169
+ }
170
+ const residual = evaluateOnRow(file, question.resource, grant.condition, row);
171
+ // Folded alike, the two lists pair grant for grant; a missing pair covers nothing.
172
+ const evaluated = reached === undefined || before === undefined
173
+ ? undefined
174
+ : evaluateOnRow(file, question.resource, reached?.[index]?.condition ?? FALSE, before);
175
+ // Listed in `beforeGrants`: what is left of its `where` on `before` is a relation
176
+ // condition `grantsOf` decided before the write; only a decided false, or a
177
+ // value of the wrong type, refuses.
178
+ const prior = coveredBefore !== undefined &&
179
+ evaluated?.k !== "false" &&
180
+ evaluated?.k !== "unknown"
181
+ ? undefined
182
+ : evaluated;
183
+ if (residual.k === "unknown" || prior?.k === "unknown") {
184
+ // A value of the wrong type: no grant.
185
+ trace.grants.set(grant, "unknown");
186
+ }
187
+ else if (residual.k === "false" || prior?.k === "false") {
188
+ trace.grants.set(grant, "not matched");
189
+ }
190
+ else if (residual.k === "true" &&
191
+ (prior === undefined || prior.k === "true")) {
192
+ trace.grants.set(grant, "matched");
193
+ granted = true;
194
+ }
195
+ else {
196
+ trace.grants.set(grant, "not asked");
197
+ grantResiduals.push({
198
+ grant,
199
+ residual,
200
+ prior: prior === undefined || prior.k === "true" ? undefined : prior,
201
+ });
202
+ }
203
+ }
204
+ const denyResiduals = [];
205
+ for (const deny of foldedDenies) {
206
+ const residual = evaluateOnRow(file, question.resource, deny.condition, row);
207
+ if (residual.k === "unknown") {
208
+ // A value of the wrong type: the deny counts (fail closed).
209
+ trace.denies.set(deny, "unknown");
210
+ return false;
211
+ }
212
+ if (isDecided(residual)) {
213
+ trace.denies.set(deny, residual.k === "true" ? "matched" : "not matched");
214
+ if (residual.k === "true") {
215
+ return false;
216
+ }
217
+ }
218
+ else {
219
+ trace.denies.set(deny, "not asked");
220
+ denyResiduals.push({ deny, residual });
221
+ }
222
+ }
223
+ if (!granted && grantResiduals.length === 0) {
224
+ return false;
225
+ }
226
+ if (denyResiduals.length === 0 && granted) {
227
+ return true;
228
+ }
229
+ const ask = (condition, on = row) => decideOnRow({
230
+ policies: file,
231
+ resource: question.resource,
232
+ condition,
233
+ row: on,
234
+ kind,
235
+ exists: question.exists,
236
+ lookups: trace.lookups,
237
+ });
238
+ return (async () => {
239
+ for (const { deny, residual } of denyResiduals) {
240
+ const truth = await ask(residual);
241
+ trace.denies.set(deny, outcomeOf(truth));
242
+ // A deny that cannot be decided counts: fail closed.
243
+ if (truth !== false) {
244
+ return false;
245
+ }
246
+ }
247
+ if (granted) {
248
+ return true;
249
+ }
250
+ for (const { grant, residual, prior } of grantResiduals) {
251
+ let truth = await ask(residual);
252
+ if (truth === true && prior !== undefined && before !== undefined) {
253
+ truth = await ask(prior, before);
254
+ }
255
+ trace.grants.set(grant, outcomeOf(truth));
256
+ if (truth === true) {
257
+ return true;
258
+ }
259
+ }
260
+ return false;
261
+ })();
262
+ };
263
+ const fieldsOf = (question) => {
264
+ const problem = fieldsQuestionProblem(question);
265
+ if (problem !== undefined) {
266
+ throw new Error(`Aventara Access: ${problem}`);
267
+ }
268
+ const facts = principalFactsOf(question.principal, question.origin);
269
+ const governed = ownOf(file.govern, question.resource) ?? [];
270
+ const actions = question.access === "read"
271
+ ? ["read"]
272
+ : question.action !== undefined
273
+ ? [question.action]
274
+ : ["create", "update"];
275
+ const grants = [
276
+ ...new Set(actions.flatMap((action) => rulesFor(file, question.resource, action).grants)),
277
+ ];
278
+ const live = foldGrants(file, grants, facts).filter((grant) => grant.condition.k !== "false");
279
+ if (live.length === 0) {
280
+ return { always: [], perRow: {} };
281
+ }
282
+ const excludes = (grant, field) => question.access === "write" &&
283
+ (locks.get(grant.grant)?.has(field) ?? false);
284
+ const names = (grant, field) => grant.grant.fields?.includes(`${question.resource}.${field}`) ?? false;
285
+ const answer = (condition) => condition.k === "true"
286
+ ? {}
287
+ : translateWhere(nullSafe(file, question.resource, condition));
288
+ const perRow = {};
289
+ for (const field of governed) {
290
+ const condition = orOf(live
291
+ .filter((grant) => names(grant, field) && !excludes(grant, field))
292
+ .map((grant) => grant.condition));
293
+ if (condition.k !== "false") {
294
+ perRow[field] = answer(condition);
295
+ }
296
+ }
297
+ const excluded = new Set(live.flatMap((grant) => question.access === "write" ? [...(locks.get(grant.grant) ?? [])] : []));
298
+ if (excluded.size === 0) {
299
+ return { always: "all", perRow };
300
+ }
301
+ const always = [];
302
+ for (const field of ownOf(file.schema, question.resource)?.fields ?? []) {
303
+ if (governed.includes(field)) {
304
+ continue;
305
+ }
306
+ if (!excluded.has(field)) {
307
+ always.push(field);
308
+ continue;
309
+ }
310
+ const condition = orOf(live
311
+ .filter((grant) => !excludes(grant, field))
312
+ .map((grant) => grant.condition));
313
+ if (condition.k === "true") {
314
+ always.push(field);
315
+ }
316
+ else if (condition.k !== "false") {
317
+ perRow[field] = answer(condition);
318
+ }
319
+ }
320
+ return { always, perRow };
321
+ };
322
+ const scopeOf = (question) => {
323
+ const facts = principalFactsOf(question.principal, question.origin);
324
+ const { grants, denies } = rulesFor(file, question.resource, question.action);
325
+ return translateAnswer(nullSafe(file, question.resource, scopeCondition(foldGrants(file, grants, facts), foldDenies(file, denies, facts))));
326
+ };
327
+ const grantsOf = (question) => {
328
+ const problem = grantsQuestionProblem(question);
329
+ if (problem !== undefined) {
330
+ throw new Error(`Aventara Access: ${problem}`);
331
+ }
332
+ const { row } = question;
333
+ if (typeof row !== "object" || row === null) {
334
+ // No row, no grant: a `can` handed this answer refuses.
335
+ return [];
336
+ }
337
+ const facts = principalFactsOf(question.principal, question.origin);
338
+ const { grants, denies } = rulesFor(file, question.resource, "update");
339
+ // An update deny decided on the row as it is (or on a value of the wrong type,
340
+ // failing closed): no grant lets the update in. A deny on a relation is the row
341
+ // after's to answer.
342
+ if (foldDenies(file, denies, facts).some((deny) => {
343
+ const truth = evaluateOnRow(file, question.resource, deny.condition, row).k;
344
+ return truth === "true" || truth === "unknown";
345
+ })) {
346
+ return [];
347
+ }
348
+ // Each grant's `where` and its subject's row condition, without its `after`: the
349
+ // condition `scopeOf` reads, asked about the row as it is. The answer lists the
350
+ // keys in the file's order.
351
+ const decided = foldGrants(file, grants, facts).map((grant) => ({
352
+ key: keyOf(grant),
353
+ residual: evaluateOnRow(file, question.resource, grant.condition, row),
354
+ }));
355
+ const covering = () => decided.flatMap(({ key, residual }) => residual.k === "true" ? [key] : []);
356
+ if (decided.every(({ residual }) => isDecided(residual) || residual.k === "unknown")) {
357
+ return covering();
358
+ }
359
+ return (async () => {
360
+ const lookups = [];
361
+ for (const entry of decided) {
362
+ const { residual } = entry;
363
+ if (isDecided(residual) || residual.k === "unknown") {
364
+ continue;
365
+ }
366
+ const truth = await decideOnRow({
367
+ policies: file,
368
+ resource: question.resource,
369
+ condition: residual,
370
+ row,
371
+ kind: "existing",
372
+ exists: question.exists,
373
+ lookups,
374
+ });
375
+ // A grant that cannot be decided does not cover the row.
376
+ entry.residual = truth === true ? TRUE : FALSE;
377
+ }
378
+ return covering();
379
+ })();
380
+ };
381
+ return { can, grantsOf, scopeOf, fieldsOf };
382
+ };
@@ -0,0 +1,10 @@
1
+ import type { AccessPolicies } from "./policies-contract.js";
2
+ /** The canonical text of a `@1` file (see the header). The file is not checked here. */
3
+ export declare const canonicalPolicies: (policies: AccessPolicies) => string;
4
+ /**
5
+ * The canonical hash of a policies file: `sha256:` and the SHA-256 of its canonical
6
+ * byte form, in lowercase hex. The same rules give the same hash however the file's
7
+ * members and rules are ordered; any changed name or literal gives a different one.
8
+ * The file is checked first, so a file the engine would refuse has no hash.
9
+ */
10
+ export declare const policiesHash: (policies: AccessPolicies) => string;
@@ -0,0 +1,53 @@
1
+ // The canonical byte form of a `policies@1` file and its hash. The
2
+ // registry keys revisions by this hash, `avAccess.lock.json` pins it, and the build
3
+ // and the CLI both call this code to check a pulled file, so one function decides
4
+ // what "the same file" means.
5
+ //
6
+ // The canonical form is the file's JSON text, written by `canonical-json.writer.ts`
7
+ // (members sorted by key, no whitespace) and encoded as UTF-8, with:
8
+ // - the lists whose order the format gives no meaning sorted: `grants` and `denies`
9
+ // by `id`; a rule's `subjects`, `actions`, `fields` and `write.except`; each
10
+ // `govern` list; each schema entry's `fields` and `nullable`.
11
+ // Every other list keeps its order: identifiers (the first is the primary one),
12
+ // relation reference pairs, principal and claim paths, a condition's arguments and
13
+ // an `in` list's values.
14
+ import { byId, canonicalJson, sortedNames as sorted, } from "./canonical-json.writer.js";
15
+ import { checkPolicies } from "./policies.compiler.js";
16
+ import { sha256Hex } from "./sha256.digest.js";
17
+ const canonicalGrant = (grant) => ({
18
+ ...grant,
19
+ subjects: sorted(grant.subjects),
20
+ actions: sorted(grant.actions),
21
+ ...(grant.fields !== undefined ? { fields: sorted(grant.fields) } : {}),
22
+ ...(grant.write !== undefined
23
+ ? { write: { except: sorted(grant.write.except) } }
24
+ : {}),
25
+ });
26
+ const canonicalDeny = (deny) => ({
27
+ ...deny,
28
+ actions: deny.actions === "*" ? "*" : sorted(deny.actions),
29
+ });
30
+ const canonicalFacts = (facts) => ({
31
+ ...facts,
32
+ fields: sorted(facts.fields),
33
+ ...(facts.nullable !== undefined ? { nullable: sorted(facts.nullable) } : {}),
34
+ });
35
+ const mapValues = (record, map) => Object.fromEntries(Object.entries(record).map(([key, value]) => [key, map(value)]));
36
+ /** The canonical text of a `@1` file (see the header). The file is not checked here. */
37
+ export const canonicalPolicies = (policies) => canonicalJson({
38
+ ...policies,
39
+ schema: mapValues(policies.schema, canonicalFacts),
40
+ govern: mapValues(policies.govern, (fields) => sorted(fields)),
41
+ grants: [...policies.grants].map(canonicalGrant).sort(byId),
42
+ denies: [...policies.denies].map(canonicalDeny).sort(byId),
43
+ });
44
+ /**
45
+ * The canonical hash of a policies file: `sha256:` and the SHA-256 of its canonical
46
+ * byte form, in lowercase hex. The same rules give the same hash however the file's
47
+ * members and rules are ordered; any changed name or literal gives a different one.
48
+ * The file is checked first, so a file the engine would refuse has no hash.
49
+ */
50
+ export const policiesHash = (policies) => {
51
+ checkPolicies(policies);
52
+ return `sha256:${sha256Hex(canonicalPolicies(policies))}`;
53
+ };
@@ -0,0 +1,62 @@
1
+ import { type AccessWhere } from "./aventara-where.translator.js";
2
+ import type { AccessPolicies } from "./policies-contract.js";
3
+ import { type Resolved } from "./scope.resolver.js";
4
+ /**
5
+ * A row as core hands it to `can`: its scalar fields. A relation member on it is
6
+ * ignored; never put relation data a client sent on it.
7
+ */
8
+ export type AccessRow = {
9
+ readonly [field: string]: unknown;
10
+ };
11
+ /** Core's `exists`: whether a row of `resource` meets `where`, unscoped. */
12
+ export type AccessExists = (resource: string, where: AccessWhere) => Promise<boolean>;
13
+ /** One `exists` question the engine asked, and its answer. */
14
+ export type AccessLookup = {
15
+ readonly resource: string;
16
+ readonly where: AccessWhere;
17
+ readonly answer: boolean;
18
+ };
19
+ /** A three-valued answer: decided, or not decidable from what the engine has. */
20
+ export type AccessTruth = boolean | "unknown";
21
+ /** Whether the row is about to be created, or already exists (an after check). */
22
+ export type AccessRowKind = "new" | "existing";
23
+ /**
24
+ * A decimal's value as `{ sign, digits, point }`: `0.<digits> × 10^point`, with no
25
+ * leading or trailing zero in `digits` (zero is `{ sign: "", digits: "", point: 0 }`).
26
+ * `undefined` for text that is no decimal.
27
+ */
28
+ export declare const decimalParts: (text: string) => {
29
+ readonly sign: "" | "-";
30
+ readonly digits: string;
31
+ readonly point: number;
32
+ } | undefined;
33
+ /** An instant's epoch milliseconds, from a valid `Date` or an ISO-8601 string that reads back as written. */
34
+ export declare const instantOf: (value: unknown) => number | undefined;
35
+ /** A condition a row decided as neither true nor false: a value of the wrong type. */
36
+ export type Unknown = {
37
+ readonly k: "unknown";
38
+ };
39
+ /** What evaluating on a row gives: `TRUE`, `FALSE`, `UNKNOWN`, or the residual. */
40
+ export type Evaluated = Resolved | Unknown;
41
+ /**
42
+ * Evaluates what the row carries. The answer is `TRUE`, `FALSE`, `UNKNOWN` (a value
43
+ * of another type than its field's), or the residual condition: the part the row
44
+ * could not decide, with the decided parts folded away. A relation member on the row
45
+ * is ignored: every relation step stays whole in the residual, for `exists`. Each comparison is made by its
46
+ * field's type (`types`): a decimal by its digits, a datetime by its instant, a
47
+ * bigint exactly.
48
+ */
49
+ export declare const evaluateOnRow: (policies: AccessPolicies, resource: string, node: Resolved, row: AccessRow) => Evaluated;
50
+ /**
51
+ * Decides a folded condition on a row: in memory first, then through `exists` for
52
+ * what the row does not carry. `"unknown"` when neither can decide.
53
+ */
54
+ export declare const decideOnRow: (args: {
55
+ readonly policies: AccessPolicies;
56
+ readonly resource: string;
57
+ readonly condition: Resolved;
58
+ readonly row: AccessRow;
59
+ readonly kind: AccessRowKind;
60
+ readonly exists: AccessExists | undefined;
61
+ readonly lookups: AccessLookup[];
62
+ }) => Promise<AccessTruth>;