@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,66 @@
1
+ // What every `avaccess` command runs with: the project's directory, the environment,
2
+ // the parsed flags, and where its lines go. A command answers with an exit code, or
3
+ // throws a `CliFailure` that says what to change; the program prints it.
4
+ //
5
+ // Exit codes: 0 the command did what it says (a check passed); 1 it ran and the
6
+ // answer is no (a lock is behind, a file was changed after pull, the registry
7
+ // refused); 2 it could not run (a usage error, missing configuration, an unreadable
8
+ // file, an unreachable registry).
9
+ import { readFile } from "node:fs/promises";
10
+ /** The command did what it says. */
11
+ export const EXIT_OK = 0;
12
+ /** It ran, and the answer is no. */
13
+ export const EXIT_FAILED = 1;
14
+ /** It could not run. */
15
+ export const EXIT_ERROR = 2;
16
+ /** A command's refusal: the exit code, the message, and the error code when Access or the registry named one. */
17
+ export class CliFailure extends Error {
18
+ exitCode;
19
+ code;
20
+ constructor(exitCode, message, code) {
21
+ super(message);
22
+ this.exitCode = exitCode;
23
+ this.code = code;
24
+ }
25
+ }
26
+ /** A string flag's value, or `undefined`. */
27
+ export const stringFlag = (context, name) => {
28
+ const value = context.flags[name];
29
+ return typeof value === "string" && value !== "" ? value : undefined;
30
+ };
31
+ /** The command's positional arguments, refused when there are more or fewer than it takes. */
32
+ export const positionals = (context, usage, min, max) => {
33
+ if (context.positionals.length < min || context.positionals.length > max) {
34
+ throw new CliFailure(EXIT_ERROR, `usage: ${usage}`);
35
+ }
36
+ return context.positionals;
37
+ };
38
+ /**
39
+ * A file path inside the project, `/`-separated and relative to it, from a flag's
40
+ * value; refused when it leaves the project or does not end in `extension`.
41
+ */
42
+ export const projectFilePath = (value, flag, example, extension) => {
43
+ const path = value.replace(/\\/g, "/").replace(/^\.\/+/, "");
44
+ if (path === "" ||
45
+ path.startsWith("/") ||
46
+ /^[A-Za-z]:/.test(path) ||
47
+ path.split("/").includes("..") ||
48
+ !path.endsWith(extension)) {
49
+ throw new CliFailure(EXIT_ERROR, `${flag} is a ${extension} file inside the project, such as ${JSON.stringify(example)}; it was ${JSON.stringify(value)}`);
50
+ }
51
+ return path;
52
+ };
53
+ /** The version of `@aventara/access` this bin is, from its own manifest. */
54
+ export const versionOf = async () => {
55
+ const manifest = JSON.parse(await readFile(decodeURIComponent(new URL("../../package.json", import.meta.url).pathname), "utf8"));
56
+ return manifest.version;
57
+ };
58
+ /** An error's message without the package's prefix: the CLI names itself. */
59
+ export const messageOf = (error) => String(error instanceof Error ? error.message : error).replace(/^Aventara Access: /, "");
60
+ /** The `code` an Access error carries (read by shape: a module the CLI loads may hold its own copy of Access). */
61
+ export const codeOf = (error) => {
62
+ const code = error?.code;
63
+ return typeof code === "string" && code.startsWith("ACCESS_")
64
+ ? code
65
+ : undefined;
66
+ };
@@ -0,0 +1,2 @@
1
+ import { type CliContext, type ExitCode } from "./cli.context.js";
2
+ export declare const diffCommand: (context: CliContext) => Promise<ExitCode>;
@@ -0,0 +1,66 @@
1
+ // `avaccess diff [<from> [<to>]] [--namespace <namespace>]`: the rules that changed
2
+ // between two revisions of the namespace's policies file, by name. A revision is
3
+ // `lock` (the one `avAccess.lock.json` pins), `head` (the registry's current), its
4
+ // hash, or the start of its hash; `from` defaults to `lock` and `to` to `head`, so a
5
+ // bare `avaccess diff` says what a pull would bring. Both files come from the
6
+ // registry and are hashed again before they are compared.
7
+ import { LOCK_FILE } from "../lock/lock-contract.js";
8
+ import { CliFailure, EXIT_ERROR, EXIT_FAILED, EXIT_OK, positionals, stringFlag, } from "./cli.context.js";
9
+ import { readLockIfAny } from "./lock.file.js";
10
+ import { changeLine, diffPolicies } from "./policies.diff.js";
11
+ import { registryClient } from "./registry.client.js";
12
+ const USAGE = "avaccess diff [<from> [<to>]] [--namespace <namespace>]";
13
+ /** The shortest start of a hash `diff` takes. */
14
+ const MIN_PREFIX = 7;
15
+ /** A revision named on the command line, as the registry's full hash. */
16
+ const resolveRevision = async (reference, namespace, lock, registry) => {
17
+ if (reference === "lock") {
18
+ if (lock === undefined || lock.namespace !== namespace) {
19
+ throw new CliFailure(EXIT_ERROR, `"lock" names the revision ${LOCK_FILE} pins, and the project has no lock for namespace "${namespace}"`);
20
+ }
21
+ return lock.policies.hash;
22
+ }
23
+ if (reference === "head") {
24
+ const head = await registry.headHash(namespace);
25
+ if (head === null) {
26
+ throw new CliFailure(EXIT_FAILED, `namespace "${namespace}" has no file pushed yet`);
27
+ }
28
+ return head;
29
+ }
30
+ const hex = reference.replace(/^sha256:/, "").toLowerCase();
31
+ if (!/^[0-9a-f]+$/.test(hex) || hex.length < MIN_PREFIX || hex.length > 64) {
32
+ throw new CliFailure(EXIT_ERROR, `a revision is "lock", "head", a hash (sha256:<hex>) or its first ${MIN_PREFIX} or more hex digits; it was ${JSON.stringify(reference)}`);
33
+ }
34
+ if (hex.length === 64) {
35
+ return `sha256:${hex}`;
36
+ }
37
+ const matches = (await registry.revisions(namespace)).filter((entry) => entry.hash.startsWith(`sha256:${hex}`));
38
+ if (matches.length !== 1) {
39
+ throw new CliFailure(EXIT_FAILED, matches.length === 0
40
+ ? `namespace "${namespace}" has no revision starting ${reference}`
41
+ : `${reference} starts ${matches.length} revisions of namespace "${namespace}": give more of the hash`);
42
+ }
43
+ return matches[0].hash;
44
+ };
45
+ export const diffCommand = async (context) => {
46
+ const [from = "lock", to = "head"] = positionals(context, USAGE, 0, 2);
47
+ const lock = await readLockIfAny(context.cwd);
48
+ const namespace = stringFlag(context, "namespace") ?? lock?.namespace;
49
+ if (namespace === undefined) {
50
+ throw new CliFailure(EXIT_ERROR, `no namespace: pass --namespace <namespace> (the project has no ${LOCK_FILE} to read it from)`);
51
+ }
52
+ const registry = registryClient(context);
53
+ const fromHash = await resolveRevision(from, namespace, lock, registry);
54
+ const toHash = await resolveRevision(to, namespace, lock, registry);
55
+ const before = await registry.revision(namespace, fromHash);
56
+ const after = toHash === fromHash ? before : await registry.revision(namespace, toHash);
57
+ const changes = diffPolicies(before.file, after.file);
58
+ context.out(`namespace "${namespace}": ${fromHash} → ${toHash}`);
59
+ if (changes.length === 0) {
60
+ context.out(" no rule changed");
61
+ }
62
+ for (const change of changes) {
63
+ context.out(` ${changeLine(change)}`);
64
+ }
65
+ return EXIT_OK;
66
+ };
@@ -0,0 +1,2 @@
1
+ import { type CliContext, type ExitCode } from "./cli.context.js";
2
+ export declare const generateCommand: (context: CliContext) => Promise<ExitCode>;
@@ -0,0 +1,82 @@
1
+ // `avaccess generate --orm <orm> [--out <file>]`: the generated client of a service
2
+ // that does not run on Aventara, written from the pulled files `avAccess.lock.json`
3
+ // pins, with one ORM's translator or none (`--orm none`, the default). Two TypeScript files: `--out` (default
4
+ // `src/generated/avAccess.ts`), which the service imports, and
5
+ // `<name>.internal.ts` beside it, the copied decisions it imports, named with the
6
+ // extension the project's tsconfig asks for (`tsconfig.reader.ts`). It runs the
7
+ // offline check `avaccess verify` runs first: a pulled file edited by hand is refused
8
+ // (`ACCESS_LOCK_MISMATCH`) and nothing is written. No network.
9
+ import { relative, resolve } from "node:path";
10
+ import { emitClient } from "../generator/client.emitter.js";
11
+ import { importExtensionOf, TsconfigRefusal, } from "../generator/tsconfig.reader.js";
12
+ import { composeGuardrails } from "../guardrails/guardrails.composer.js";
13
+ import { canonicalGuardrails } from "../guardrails/guardrails.hash.js";
14
+ import { verifyLocked } from "../lock/lock.verifier.js";
15
+ import { canonicalPolicies } from "../policies.hash.js";
16
+ import { CliFailure, codeOf, EXIT_ERROR, EXIT_FAILED, EXIT_OK, messageOf, positionals, projectFilePath, stringFlag, versionOf, } from "./cli.context.js";
17
+ import { readLock, readPinned, writeProjectFile } from "./lock.file.js";
18
+ import { clientTranslatorOf } from "./translator.flag.js";
19
+ /** Where the generated client goes when `--out` does not say. */
20
+ const DEFAULT_CLIENT = "src/generated/avAccess.ts";
21
+ /** The internal file beside a client file: `avAccess.ts` → `avAccess.internal.ts`. */
22
+ const internalFileOf = (client) => client.replace(/\.ts$/, ".internal.ts");
23
+ export const generateCommand = async (context) => {
24
+ positionals(context, "avaccess generate [--orm <orm>] [--out <file>] [--tsconfig <file>]", 0, 0);
25
+ const translator = clientTranslatorOf(context);
26
+ const out = projectFilePath(stringFlag(context, "out") ?? DEFAULT_CLIENT, "--out", DEFAULT_CLIENT, ".ts");
27
+ const lock = await readLock(context.cwd);
28
+ const policies = await readPinned(context.cwd, lock.policies);
29
+ const guardrails = lock.guardrails === null
30
+ ? undefined
31
+ : await readPinned(context.cwd, lock.guardrails);
32
+ let guardrailDenies = [];
33
+ try {
34
+ verifyLocked(lock, {
35
+ namespace: policies
36
+ ?.namespace,
37
+ policies,
38
+ guardrails,
39
+ });
40
+ if (guardrails !== undefined) {
41
+ // Composed from the canonical forms, so a pulled file's member order (which
42
+ // its hash ignores) never reaches the client's text.
43
+ const file = JSON.parse(canonicalPolicies(policies));
44
+ guardrailDenies = composeGuardrails(file, JSON.parse(canonicalGuardrails(guardrails))).denies.slice(file.denies.length);
45
+ }
46
+ }
47
+ catch (error) {
48
+ throw new CliFailure(EXIT_FAILED, messageOf(error), codeOf(error));
49
+ }
50
+ const internal = internalFileOf(out);
51
+ // Relative to the project, with forward slashes, as `--out` is: the banner's bytes
52
+ // do not depend on how the path was typed.
53
+ const named = stringFlag(context, "tsconfig");
54
+ const tsconfig = named === undefined
55
+ ? undefined
56
+ : relative(context.cwd, resolve(context.cwd, named)).replace(/\\/g, "/");
57
+ let extension;
58
+ try {
59
+ extension = await importExtensionOf(context.cwd, resolve(context.cwd, out), tsconfig);
60
+ }
61
+ catch (error) {
62
+ throw new CliFailure(EXIT_ERROR, error instanceof TsconfigRefusal
63
+ ? error.message
64
+ : `the project's tsconfig.json could not be read to name ${internal} (${messageOf(error)}). Nothing was written.`);
65
+ }
66
+ const internalName = (internal.split("/").pop() ?? internal).replace(/\.ts$/, "");
67
+ const files = await emitClient({
68
+ policies: policies,
69
+ guardrailDenies,
70
+ lock,
71
+ translator,
72
+ command: `avaccess generate --orm ${translator.orm} --out ${out}${tsconfig === undefined ? "" : ` --tsconfig ${tsconfig}`}`,
73
+ version: await versionOf(),
74
+ internalSpecifier: `./${internalName}${extension}`,
75
+ });
76
+ await writeProjectFile(context.cwd, internal, files.internal);
77
+ await writeProjectFile(context.cwd, out, files.client);
78
+ context.out(`wrote ${out} and ${internal}: namespace "${lock.namespace}" from ${lock.policies.path}${lock.guardrails === null ? "" : ` and ${lock.guardrails.path}`}, ${translator.title === undefined
79
+ ? "with no ORM translator (--orm none)"
80
+ : `with the ${translator.title} translator`}`);
81
+ return EXIT_OK;
82
+ };
@@ -0,0 +1,2 @@
1
+ import { type CliContext, type ExitCode } from "./cli.context.js";
2
+ export declare const keysCommand: (context: CliContext) => Promise<ExitCode>;
@@ -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 {};