@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,96 @@
1
+ // `avaccess keys issue|rotate|revoke`: the operator's side of the registry, run as the
2
+ // operator, never over HTTP. `--server <module>` names the Access server's own module
3
+ // (the one that calls `createAccess({ …, registry: { enabled: true } })` and exports
4
+ // the instance, as its default export or as `access`; or, for a Better Auth app that
5
+ // is the hub, the one that exports `accessOf(auth)` with
6
+ // `aventaraAccess({ registry: { enabled: true } })` in its plugins); the CLI loads it
7
+ // with the server's own configuration and database, and calls `access.registry` in
8
+ // this process. Whoever can run it holds the server's database credentials already, so
9
+ // no key or route of its own opens the operator's API.
10
+ //
11
+ // A key is printed once, by the command that issues it, and never again: the registry
12
+ // keeps only its hash, and no other command prints one.
13
+ //
14
+ // avaccess keys issue <namespace> --server <module> a new namespace, its push and pull keys
15
+ // avaccess keys rotate <namespace> <push|pull> --server <m> a new key; the previous one is refused
16
+ // avaccess keys revoke <namespace> <push|pull> --server <m> the key is refused from now on
17
+ import { isRecord } from "../policies.compiler.js";
18
+ import { CliFailure, codeOf, EXIT_ERROR, EXIT_FAILED, EXIT_OK, messageOf, stringFlag, } from "./cli.context.js";
19
+ import { loadProjectModule, pickExport } from "./module.loader.js";
20
+ const USAGE = [
21
+ "avaccess keys issue <namespace> --server <module>",
22
+ "avaccess keys rotate <namespace> <push|pull> --server <module>",
23
+ "avaccess keys revoke <namespace> <push|pull> --server <module>",
24
+ ].join("\n ");
25
+ /** `createAccess`'s instance (it has the handler), or `accessOf(auth)` (it has the request state). */
26
+ const isAccessInstance = (value) => isRecord(value) &&
27
+ (typeof value.handler === "function" || typeof value.state === "function");
28
+ /** Better Auth's own instance with the Access plugin in its plugins: its operator API is `accessOf(auth).registry`. */
29
+ const isPluginApp = (instance) => isRecord(instance.options) &&
30
+ Array.isArray(instance.options.plugins) &&
31
+ instance.options.plugins.some((plugin) => isRecord(plugin) && plugin.id === "aventara-access") &&
32
+ typeof instance.api === "object";
33
+ /** The operator's API of the Access server `--server` names. */
34
+ const operatorOf = async (context) => {
35
+ const server = stringFlag(context, "server");
36
+ if (server === undefined) {
37
+ throw new CliFailure(EXIT_ERROR, "no server: pass --server <module>, the Access server's module that exports its `createAccess({ …, registry: { enabled: true } })` instance, or `accessOf(auth)` for a Better Auth app with `aventaraAccess({ registry: { enabled: true } })`");
38
+ }
39
+ const exports = await loadProjectModule(context.cwd, server);
40
+ const instance = pickExport(exports, server, "access", isAccessInstance, "Access instance");
41
+ const registry = instance.registry;
42
+ if (!isRecord(registry) || typeof registry.createNamespace !== "function") {
43
+ if (isPluginApp(instance)) {
44
+ throw new CliFailure(EXIT_ERROR, `${server} exports Better Auth's own instance: export \`accessOf(auth)\` (from @aventara/access/better-auth) as \`access\`, built with \`aventaraAccess({ registry: { enabled: true } })\`; its \`registry\` is the operator's API`);
45
+ }
46
+ throw new CliFailure(EXIT_ERROR, `the Access instance in ${server} has the registry off: build it with \`registry: { enabled: true }\` (on \`createAccess\`, or on \`aventaraAccess()\` for \`accessOf(auth)\`)`);
47
+ }
48
+ return registry;
49
+ };
50
+ const kindOf = (kind) => {
51
+ if (kind !== "push" && kind !== "pull") {
52
+ throw new CliFailure(EXIT_ERROR, `a key's kind is "push" or "pull"; it was ${JSON.stringify(kind ?? "")}\nusage: ${USAGE}`);
53
+ }
54
+ return kind;
55
+ };
56
+ /** Runs one operator call, its refusal as the CLI's. */
57
+ const asOperator = async (run) => {
58
+ try {
59
+ return await run();
60
+ }
61
+ catch (error) {
62
+ throw new CliFailure(EXIT_FAILED, messageOf(error), codeOf(error));
63
+ }
64
+ };
65
+ export const keysCommand = async (context) => {
66
+ const [action, namespace, kind, ...rest] = context.positionals;
67
+ const arity = action === "issue" ? 2 : 3;
68
+ if ((action !== "issue" && action !== "rotate" && action !== "revoke") ||
69
+ namespace === undefined ||
70
+ context.positionals.length !== arity ||
71
+ rest.length > 0) {
72
+ throw new CliFailure(EXIT_ERROR, `usage: ${USAGE}`);
73
+ }
74
+ if (action !== "issue") {
75
+ kindOf(kind);
76
+ }
77
+ const operator = await operatorOf(context);
78
+ if (action === "issue") {
79
+ const keys = await asOperator(() => operator.createNamespace(namespace));
80
+ context.out(`namespace "${keys.namespace}" created. Its keys, shown this once (the registry keeps only their hashes):`);
81
+ context.out(` push key: ${keys.push}`);
82
+ context.out(` pull key: ${keys.pull}`);
83
+ context.out("Give each to the service as AVACCESS_KEY, kept as a secret: the push key where it pushes (its CI), the pull key where it only pulls.");
84
+ return EXIT_OK;
85
+ }
86
+ const checked = kindOf(kind);
87
+ if (action === "rotate") {
88
+ const key = await asOperator(() => operator.rotateKey(namespace, checked));
89
+ context.out(`the ${checked} key of namespace "${namespace}", shown this once: ${key}`);
90
+ context.out(`The previous ${checked} key is refused from now on: give this one to the service as AVACCESS_KEY.`);
91
+ return EXIT_OK;
92
+ }
93
+ await asOperator(() => operator.revokeKey(namespace, checked));
94
+ context.out(`the ${checked} key of namespace "${namespace}" is revoked: it is refused from now on. \`avaccess keys rotate ${namespace} ${checked}\` issues a new one.`);
95
+ return EXIT_OK;
96
+ };
@@ -0,0 +1,31 @@
1
+ import { type AccessForeignFacts } from "../foreign/foreign-facts.js";
2
+ import { type AccessLock, type AccessLockPin } from "../lock/lock-contract.js";
3
+ /** The project's lock, checked; a missing or malformed one is refused, naming the fix. */
4
+ export declare const readLock: (cwd: string) => Promise<AccessLock>;
5
+ /** The project's lock, or `undefined` when it has none yet. */
6
+ export declare const readLockIfAny: (cwd: string) => Promise<AccessLock | undefined>;
7
+ /** A pinned file, parsed. A missing or unparsable one fails the check (`ACCESS_LOCK_MISMATCH`). */
8
+ export declare const readPinned: (cwd: string, pin: AccessLockPin) => Promise<unknown>;
9
+ /**
10
+ * The foreign facts the lock pins, held to their pin and to the lock's namespace;
11
+ * `undefined` when it pins none. A changed file fails the check
12
+ * (`ACCESS_LOCK_MISMATCH`).
13
+ */
14
+ export declare const readForeignPinned: (cwd: string, lock: AccessLock) => Promise<AccessForeignFacts | undefined>;
15
+ /** JSON text as the CLI writes it: two-space indent, a final newline. */
16
+ export declare const jsonText: (value: unknown) => string;
17
+ /**
18
+ * Writes a file under the project in one step: a temporary file beside it, created
19
+ * new (never one that is there already, nor a link), then a rename over the file.
20
+ * A path whose directory leaves the project through a symbolic link is refused.
21
+ *
22
+ * The temporary file is created in the directory's canonical path, read once its
23
+ * symbolic links are resolved and held inside the project, so a link on the way
24
+ * that is swapped after the check cannot move it out. It is written only once that
25
+ * path still names the file just created (a directory on the way replaced by a link
26
+ * since is refused, the file removed), and renamed within that same directory. A
27
+ * window stays where a real directory on the way is renamed away and replaced by a
28
+ * link between that last check and the rename: Node.js opens no file relative to a
29
+ * directory it holds open, so no path-based write closes it entirely.
30
+ */
31
+ export declare const writeProjectFile: (cwd: string, path: string, text: string) => Promise<void>;
@@ -0,0 +1,181 @@
1
+ // `avAccess.lock.json` and the pulled files on disk. `avaccess pull` writes them,
2
+ // `verify`, `status` and `diff` read them. A pulled file is written in its canonical
3
+ // member order, indented for review; its hash is over the parsed file, so a
4
+ // formatter that re-indents it changes nothing, and an edit of a name or a literal
5
+ // changes the hash.
6
+ import { mkdir, open, readFile, realpath, rename, stat, unlink, } from "node:fs/promises";
7
+ import { basename, dirname, join, resolve, sep } from "node:path";
8
+ import { checkForeignFacts, foreignFactsHash, } from "../foreign/foreign-facts.js";
9
+ import { checkLock, LOCK_FILE, } from "../lock/lock-contract.js";
10
+ import { CliFailure, EXIT_ERROR, EXIT_FAILED, messageOf, } from "./cli.context.js";
11
+ const isMissing = (error) => error?.code === "ENOENT";
12
+ /** The project's lock, checked; a missing or malformed one is refused, naming the fix. */
13
+ export const readLock = async (cwd) => {
14
+ let text;
15
+ try {
16
+ text = await readFile(resolve(cwd, LOCK_FILE), "utf8");
17
+ }
18
+ catch (error) {
19
+ throw new CliFailure(EXIT_ERROR, isMissing(error)
20
+ ? `no ${LOCK_FILE} in ${cwd}: run \`avaccess pull --namespace <namespace>\` first`
21
+ : `${LOCK_FILE} could not be read (${messageOf(error)})`);
22
+ }
23
+ let parsed;
24
+ try {
25
+ parsed = JSON.parse(text);
26
+ }
27
+ catch (error) {
28
+ throw new CliFailure(EXIT_ERROR, `${LOCK_FILE} is not JSON (${messageOf(error)}): pull again with \`avaccess pull\``);
29
+ }
30
+ try {
31
+ return checkLock(parsed, LOCK_FILE);
32
+ }
33
+ catch (error) {
34
+ throw new CliFailure(EXIT_ERROR, messageOf(error));
35
+ }
36
+ };
37
+ /** The project's lock, or `undefined` when it has none yet. */
38
+ export const readLockIfAny = async (cwd) => {
39
+ try {
40
+ await readFile(resolve(cwd, LOCK_FILE), "utf8");
41
+ }
42
+ catch (error) {
43
+ if (isMissing(error)) {
44
+ return undefined;
45
+ }
46
+ }
47
+ return readLock(cwd);
48
+ };
49
+ /** A pinned file, parsed. A missing or unparsable one fails the check (`ACCESS_LOCK_MISMATCH`). */
50
+ export const readPinned = async (cwd, pin) => {
51
+ let text;
52
+ try {
53
+ text = await readFile(resolve(cwd, pin.path), "utf8");
54
+ }
55
+ catch (error) {
56
+ throw new CliFailure(isMissing(error) ? EXIT_FAILED : EXIT_ERROR, isMissing(error)
57
+ ? `${pin.path} is pinned in ${LOCK_FILE} under ${pin.hash}, and is missing: pull it again with \`avaccess pull\``
58
+ : `${pin.path} could not be read (${messageOf(error)})`, isMissing(error) ? "ACCESS_LOCK_MISMATCH" : undefined);
59
+ }
60
+ try {
61
+ return JSON.parse(text);
62
+ }
63
+ catch (error) {
64
+ throw new CliFailure(EXIT_FAILED, `${pin.path} is pinned in ${LOCK_FILE} under ${pin.hash}, and is no longer JSON (${messageOf(error)}): it was changed after it was pulled. Pull it again with \`avaccess pull\`; a pulled file is never edited by hand`, "ACCESS_LOCK_MISMATCH");
65
+ }
66
+ };
67
+ /**
68
+ * The foreign facts the lock pins, held to their pin and to the lock's namespace;
69
+ * `undefined` when it pins none. A changed file fails the check
70
+ * (`ACCESS_LOCK_MISMATCH`).
71
+ */
72
+ export const readForeignPinned = async (cwd, lock) => {
73
+ const pin = lock.foreign;
74
+ if (pin === undefined) {
75
+ return undefined;
76
+ }
77
+ let facts;
78
+ let hash;
79
+ try {
80
+ facts = checkForeignFacts(await readPinned(cwd, pin));
81
+ hash = foreignFactsHash(facts);
82
+ }
83
+ catch (error) {
84
+ if (error instanceof CliFailure) {
85
+ throw error;
86
+ }
87
+ throw new CliFailure(EXIT_FAILED, `${pin.path} is pinned in ${LOCK_FILE} under ${pin.hash}, and is no longer a foreign facts file (${messageOf(error)}): it was changed after it was pulled. Pull it again with \`avaccess pull\`; a pulled file is never edited by hand`, "ACCESS_LOCK_MISMATCH");
88
+ }
89
+ if (hash !== pin.hash) {
90
+ throw new CliFailure(EXIT_FAILED, `${pin.path} hashes to ${hash}, and ${LOCK_FILE} pins ${pin.hash}: it was changed after it was pulled. Pull it again with \`avaccess pull\`; a pulled file is never edited by hand`, "ACCESS_LOCK_MISMATCH");
91
+ }
92
+ if (facts.namespace !== lock.namespace) {
93
+ throw new CliFailure(EXIT_FAILED, `${pin.path} holds what namespace "${facts.namespace}" reads, and ${LOCK_FILE} pins namespace "${lock.namespace}": pull this project's own namespace again with \`avaccess pull\``, "ACCESS_LOCK_MISMATCH");
94
+ }
95
+ return facts;
96
+ };
97
+ /** JSON text as the CLI writes it: two-space indent, a final newline. */
98
+ export const jsonText = (value) => `${JSON.stringify(value, null, 2)}\n`;
99
+ /** Whether `real`, a canonical path, is `root` or under it. */
100
+ const within = (root, real) => real === root || real.startsWith(`${root}${sep}`);
101
+ /** The refusal of a path that leaves the project through a symbolic link. */
102
+ const leaves = (path, real) => new CliFailure(EXIT_ERROR, `${path} leaves the project through a symbolic link (its directory resolves to ${real}); avaccess writes only inside the project. Nothing was written there`);
103
+ /**
104
+ * Refuses `path` when its directory, as far as it exists, resolves outside the
105
+ * project: a directory on the way that is a symbolic link out of it.
106
+ */
107
+ const holdInside = async (cwd, path) => {
108
+ const root = await realpath(cwd);
109
+ let directory = dirname(resolve(cwd, path));
110
+ for (;;) {
111
+ let real;
112
+ try {
113
+ real = await realpath(directory);
114
+ }
115
+ catch (error) {
116
+ const parent = dirname(directory);
117
+ if (isMissing(error) && parent !== directory) {
118
+ directory = parent;
119
+ continue;
120
+ }
121
+ throw error;
122
+ }
123
+ if (!within(root, real)) {
124
+ throw leaves(path, real);
125
+ }
126
+ return;
127
+ }
128
+ };
129
+ /**
130
+ * Writes a file under the project in one step: a temporary file beside it, created
131
+ * new (never one that is there already, nor a link), then a rename over the file.
132
+ * A path whose directory leaves the project through a symbolic link is refused.
133
+ *
134
+ * The temporary file is created in the directory's canonical path, read once its
135
+ * symbolic links are resolved and held inside the project, so a link on the way
136
+ * that is swapped after the check cannot move it out. It is written only once that
137
+ * path still names the file just created (a directory on the way replaced by a link
138
+ * since is refused, the file removed), and renamed within that same directory. A
139
+ * window stays where a real directory on the way is renamed away and replaced by a
140
+ * link between that last check and the rename: Node.js opens no file relative to a
141
+ * directory it holds open, so no path-based write closes it entirely.
142
+ */
143
+ export const writeProjectFile = async (cwd, path, text) => {
144
+ const target = resolve(cwd, path);
145
+ await holdInside(cwd, path);
146
+ await mkdir(dirname(target), { recursive: true });
147
+ const root = await realpath(cwd);
148
+ const directory = await realpath(dirname(target));
149
+ if (!within(root, directory)) {
150
+ throw leaves(path, directory);
151
+ }
152
+ const temporary = join(directory, `${basename(target)}.${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}.tmp`);
153
+ const handle = await open(temporary, "wx");
154
+ let kept = false;
155
+ try {
156
+ const created = await handle.stat();
157
+ let named;
158
+ let real;
159
+ try {
160
+ named = await stat(temporary);
161
+ real = await realpath(temporary);
162
+ }
163
+ catch {
164
+ throw leaves(path, directory);
165
+ }
166
+ if (real !== temporary ||
167
+ created.dev !== named.dev ||
168
+ created.ino !== named.ino) {
169
+ throw leaves(path, dirname(real));
170
+ }
171
+ await handle.writeFile(text);
172
+ kept = true;
173
+ }
174
+ finally {
175
+ await handle.close();
176
+ if (!kept) {
177
+ await unlink(temporary).catch(() => undefined);
178
+ }
179
+ }
180
+ await rename(temporary, join(directory, basename(target)));
181
+ };
@@ -0,0 +1,13 @@
1
+ /** A module's exports, by name (`default` included). */
2
+ type ModuleExports = {
3
+ readonly [name: string]: unknown;
4
+ };
5
+ /** The exports of the project's module at `path` (a `.json` file is `{ default: <its value> }`). */
6
+ export declare const loadProjectModule: (cwd: string, path: string) => Promise<ModuleExports>;
7
+ /**
8
+ * The one export of `exports` that `accepts`: `default`, else the export named
9
+ * `preferred`, else the only export that fits. Names what it found when that is
10
+ * not one.
11
+ */
12
+ export declare const pickExport: <T>(exports: ModuleExports, path: string, preferred: string, accepts: (value: unknown) => value is T, what: string) => T;
13
+ export {};
@@ -0,0 +1,63 @@
1
+ // The project's own modules, as `avaccess` reads them: a `.json` file is parsed; a
2
+ // TypeScript or JavaScript module (`.ts`, `.mts`, `.cts`, `.js`, `.mjs`, `.cjs`) is
3
+ // loaded with jiti, the loader `prisma.config.ts` and the framework's configs are
4
+ // read with, so a project's sources load as they are, with no build step. jiti keeps
5
+ // no cache on disk here: the CLI writes nothing but what a command says it writes.
6
+ import { readFile } from "node:fs/promises";
7
+ import { resolve } from "node:path";
8
+ import { CliFailure, EXIT_ERROR, messageOf } from "./cli.context.js";
9
+ const MODULE = /\.(?:[cm]?ts|[cm]?js)$/;
10
+ /** The exports of the project's module at `path` (a `.json` file is `{ default: <its value> }`). */
11
+ export const loadProjectModule = async (cwd, path) => {
12
+ const file = resolve(cwd, path);
13
+ if (file.endsWith(".json")) {
14
+ let text;
15
+ try {
16
+ text = await readFile(file, "utf8");
17
+ }
18
+ catch (error) {
19
+ throw new CliFailure(EXIT_ERROR, `${path} could not be read (${messageOf(error)})`);
20
+ }
21
+ try {
22
+ return { default: JSON.parse(text) };
23
+ }
24
+ catch (error) {
25
+ throw new CliFailure(EXIT_ERROR, `${path} is not JSON (${messageOf(error)})`);
26
+ }
27
+ }
28
+ if (!MODULE.test(file)) {
29
+ throw new CliFailure(EXIT_ERROR, `${path} is neither a .json file nor a module (.ts, .mts, .cts, .js, .mjs, .cjs)`);
30
+ }
31
+ const { createJiti } = await import("jiti");
32
+ const jiti = createJiti(import.meta.url, {
33
+ fsCache: false,
34
+ moduleCache: false,
35
+ interopDefault: false,
36
+ });
37
+ try {
38
+ return (await jiti.import(file));
39
+ }
40
+ catch (error) {
41
+ throw new CliFailure(EXIT_ERROR, `${path} could not be loaded: ${messageOf(error)}`);
42
+ }
43
+ };
44
+ /**
45
+ * The one export of `exports` that `accepts`: `default`, else the export named
46
+ * `preferred`, else the only export that fits. Names what it found when that is
47
+ * not one.
48
+ */
49
+ export const pickExport = (exports, path, preferred, accepts, what) => {
50
+ for (const name of ["default", preferred]) {
51
+ const value = exports[name];
52
+ if (accepts(value)) {
53
+ return value;
54
+ }
55
+ }
56
+ const fitting = Object.entries(exports).filter(([, value]) => accepts(value));
57
+ if (fitting.length === 1) {
58
+ return fitting[0][1];
59
+ }
60
+ throw new CliFailure(EXIT_ERROR, fitting.length === 0
61
+ ? `${path} exports no ${what}: export it as the default export or as \`${preferred}\``
62
+ : `${path} exports ${fitting.length} ${what}s (${fitting.map(([name]) => name).join(", ")}): export the one to use as the default export or as \`${preferred}\``);
63
+ };
@@ -0,0 +1,16 @@
1
+ import type { AccessPolicies } from "../policies-contract.js";
2
+ /** One part of a policies file a change names. */
3
+ type PoliciesPart = "namespace" | "claim" | "schema" | "role" | "govern" | "grant" | "deny";
4
+ /** One named change between two revisions. */
5
+ type PoliciesChange = {
6
+ readonly part: PoliciesPart;
7
+ readonly name: string;
8
+ readonly change: "added" | "removed" | "changed";
9
+ /** For a changed entry: its members that differ. */
10
+ readonly members?: readonly string[];
11
+ };
12
+ /** The named changes from `before` to `after`, in the order of the file's parts. */
13
+ export declare const diffPolicies: (before: AccessPolicies, after: AccessPolicies) => readonly PoliciesChange[];
14
+ /** A change as one line: `grant "Note/read/player#1": changed (where)`. */
15
+ export declare const changeLine: (change: PoliciesChange) => string;
16
+ export {};
@@ -0,0 +1,83 @@
1
+ // What changed between two revisions of a namespace's policies file, by name: each
2
+ // grant and deny by its `id`, each role by its name, each governed Resource, each
3
+ // schema entry and each claim. Both files are compared in their canonical form
4
+ // (`policies.hash.ts`), so a reordering is no change, and a changed entry lists the
5
+ // members that differ.
6
+ import { canonicalJson } from "../canonical-json.writer.js";
7
+ import { canonicalPolicies } from "../policies.hash.js";
8
+ /** A file's members in canonical form. */
9
+ const canonicalOf = (file) => JSON.parse(canonicalPolicies(file));
10
+ const text = (value) => value === undefined ? "" : canonicalJson(value);
11
+ /** The members of two entries that differ (none when either is not an object). */
12
+ const differingMembers = (before, after) => {
13
+ if (typeof before !== "object" ||
14
+ typeof after !== "object" ||
15
+ before === null ||
16
+ after === null ||
17
+ Array.isArray(before) ||
18
+ Array.isArray(after)) {
19
+ return [];
20
+ }
21
+ const a = before;
22
+ const b = after;
23
+ return [...new Set([...Object.keys(a), ...Object.keys(b)])]
24
+ .filter((key) => text(a[key]) !== text(b[key]))
25
+ .sort();
26
+ };
27
+ const compareEntries = (part, before, after) => {
28
+ const changes = [];
29
+ for (const name of [...new Set([...before.keys(), ...after.keys()])].sort()) {
30
+ const a = before.get(name);
31
+ const b = after.get(name);
32
+ if (a === undefined) {
33
+ changes.push({ part, name, change: "added" });
34
+ }
35
+ else if (b === undefined) {
36
+ changes.push({ part, name, change: "removed" });
37
+ }
38
+ else if (text(a) !== text(b)) {
39
+ const members = differingMembers(a, b);
40
+ changes.push({
41
+ part,
42
+ name,
43
+ change: "changed",
44
+ ...(members.length > 0 ? { members } : {}),
45
+ });
46
+ }
47
+ }
48
+ return changes;
49
+ };
50
+ const byKey = (record) => new Map(Object.entries((record ?? {})).filter(([, value]) => value !== undefined));
51
+ const byId = (rules) => new Map((rules ?? []).map((rule) => [
52
+ String(rule.id),
53
+ rule,
54
+ ]));
55
+ /** The named changes from `before` to `after`, in the order of the file's parts. */
56
+ export const diffPolicies = (before, after) => {
57
+ const a = canonicalOf(before);
58
+ const b = canonicalOf(after);
59
+ const namespace = text(a.namespace) === text(b.namespace)
60
+ ? []
61
+ : [
62
+ {
63
+ part: "namespace",
64
+ name: String(b.namespace ?? a.namespace),
65
+ change: a.namespace === undefined
66
+ ? "added"
67
+ : b.namespace === undefined
68
+ ? "removed"
69
+ : "changed",
70
+ },
71
+ ];
72
+ return [
73
+ ...namespace,
74
+ ...compareEntries("claim", byKey(a.claims), byKey(b.claims)),
75
+ ...compareEntries("schema", byKey(a.schema), byKey(b.schema)),
76
+ ...compareEntries("role", byKey(a.roles), byKey(b.roles)),
77
+ ...compareEntries("govern", byKey(a.govern), byKey(b.govern)),
78
+ ...compareEntries("grant", byId(a.grants), byId(b.grants)),
79
+ ...compareEntries("deny", byId(a.denies), byId(b.denies)),
80
+ ];
81
+ };
82
+ /** A change as one line: `grant "Note/read/player#1": changed (where)`. */
83
+ export const changeLine = (change) => `${change.part} ${JSON.stringify(change.name)}: ${change.change}${change.members === undefined ? "" : ` (${change.members.join(", ")})`}`;
@@ -0,0 +1,2 @@
1
+ import { type CliContext, type ExitCode } from "./cli.context.js";
2
+ export declare const pullCommand: (context: CliContext) => Promise<ExitCode>;