@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,144 @@
1
+ // The organization bridge (`aventaraAccess({ organization })`): a member's role in
2
+ // Better Auth's organization plugin becomes two claims of the token, the role's
3
+ // mapped value and the organization it holds in, so the rules read it as an
4
+ // organization's role, never a global one. Both are read when the token is issued,
5
+ // from the membership Better Auth's store holds for the session's active
6
+ // organization: removing the member, or changing their role, changes the next token.
7
+ //
8
+ // An organization role is a fact about one organization. The rules' grammar has no
9
+ // role held "in the active organization", so the bridge hands the rules the
10
+ // organization beside the role, and each grant on the role must be held to it
11
+ // (`where: { organizationId: me.org }`, with `principal` copying the claim onto
12
+ // `me`). A rule on the role alone grants across every organization: the README says
13
+ // so, and the bridge's own refusals keep the role from being self-made.
14
+ //
15
+ // The bridge trusts the organization plugin's memberships, so it refuses an
16
+ // organization plugin
17
+ // - whose invitations a user with an unverified email may accept (GHSA-fmh4-wcc4-5jm3);
18
+ // - that lets any user create an organization, and so make themselves its owner,
19
+ // unless the app chose that (`allowUserToCreateOrganization` `false`, or a function);
20
+ // - with dynamic access control, mapping a role that is not one of its static roles
21
+ // (an organization's admins name dynamic roles).
22
+ import { AccessError } from "../access/access.error.js";
23
+ import { organizationPluginOf, refuseUnverifiedInvitations, } from "../access/better-auth.hardening.js";
24
+ import { isRecord } from "../policies.compiler.js";
25
+ const refuse = (message) => {
26
+ throw new AccessError("ACCESS_OPTIONS_INVALID", message);
27
+ };
28
+ const SHAPE = '`organization` is `{ claim, organizationClaim, roles }`: the claim a member\'s role fills and the claim its organization fills (both declared as "string" in `policies.claims`), and each Better Auth role, in order, with its value: `[["owner", "admin"], ["admin", "admin"]]`';
29
+ /** A role name Better Auth stores as one: not empty, no comma, no whitespace. */
30
+ const ROLE_NAME = /^[^\s,]+$/;
31
+ /** One of the two claims, checked: declared as a string. */
32
+ const stringClaim = (member, claim, claims) => {
33
+ if (typeof claim !== "string" || claims[claim] !== "string") {
34
+ return refuse(`${SHAPE}; \`organization.${member}\` names ${typeof claim === "string" ? (claims[claim] === undefined ? `the claim "${claim}", which \`policies.claims\` does not declare` : `\`${claim}\`, which \`policies.claims\` declares as ${claims[claim]}`) : JSON.stringify(claim)}.`);
35
+ }
36
+ return claim;
37
+ };
38
+ /** The `organization` option, checked against the declared claims; `undefined` when it is not given. */
39
+ export const checkBridge = (value, claims) => {
40
+ if (value === undefined) {
41
+ return undefined;
42
+ }
43
+ if (!isRecord(value)) {
44
+ return refuse(`${SHAPE}.`);
45
+ }
46
+ for (const key of Object.keys(value)) {
47
+ if (key !== "claim" && key !== "organizationClaim" && key !== "roles") {
48
+ refuse(`${SHAPE}; it does not take \`${key}\`.`);
49
+ }
50
+ }
51
+ const claim = stringClaim("claim", value.claim, claims);
52
+ const organizationClaim = stringClaim("organizationClaim", value.organizationClaim, claims);
53
+ if (claim === organizationClaim) {
54
+ refuse(`${SHAPE}; \`claim\` and \`organizationClaim\` are both "${claim}": they are two claims.`);
55
+ }
56
+ const { roles } = value;
57
+ if (!Array.isArray(roles) || roles.length === 0) {
58
+ return refuse(`${SHAPE}; \`organization.roles\` is ${Array.isArray(roles) ? "empty" : JSON.stringify(roles)}.`);
59
+ }
60
+ const entries = [];
61
+ const seen = [];
62
+ for (const entry of roles) {
63
+ const [role, mapped] = Array.isArray(entry) ? entry : [];
64
+ if (!Array.isArray(entry) ||
65
+ entry.length !== 2 ||
66
+ typeof role !== "string" ||
67
+ !ROLE_NAME.test(role) ||
68
+ seen.includes(role) ||
69
+ typeof mapped !== "string" ||
70
+ mapped === "") {
71
+ return refuse(`${SHAPE}; \`organization.roles\` holds ${JSON.stringify(entry)}: each entry is a Better Auth role (one name, no comma or whitespace, listed once) and a value.`);
72
+ }
73
+ seen.push(role);
74
+ entries.push([role, mapped]);
75
+ }
76
+ return { claim, organizationClaim, roles: entries };
77
+ };
78
+ /** Better Auth's organization roles when the app names none of its own. */
79
+ const DEFAULT_ROLES = ["owner", "admin", "member"];
80
+ /**
81
+ * Refuses a bridge over Better Auth without its organization plugin, and over one
82
+ * that lets a member be made otherwise than the app decided: by an unverified email
83
+ * accepting an invitation, by any user creating an organization, or (with dynamic
84
+ * access control) under a role an organization's admins name.
85
+ */
86
+ export const checkBridgePlugins = (plugins, bridge) => {
87
+ const organization = organizationPluginOf(plugins);
88
+ if (organization === undefined) {
89
+ throw new AccessError("ACCESS_RESOURCE_MISSING", "`aventaraAccess({ organization })` reads Better Auth's organization memberships, and Better Auth has no organization plugin: add organization() to plugins.");
90
+ }
91
+ refuseUnverifiedInvitations(plugins, "`aventaraAccess({ organization })`");
92
+ const { allowUserToCreateOrganization, dynamicAccessControl, roles } = organization.options;
93
+ if (allowUserToCreateOrganization !== false &&
94
+ typeof allowUserToCreateOrganization !== "function") {
95
+ refuse("`aventaraAccess({ organization })` maps an organization's roles into claims, and the organization plugin lets any user create an organization, and so be its owner. Decide who may: pass `organization({ allowUserToCreateOrganization: false })`, or a function of the user.");
96
+ }
97
+ if (isRecord(dynamicAccessControl) && dynamicAccessControl.enabled === true) {
98
+ const statics = isRecord(roles) ? Object.keys(roles) : DEFAULT_ROLES;
99
+ for (const [role] of bridge.roles) {
100
+ if (!statics.includes(role)) {
101
+ refuse(`\`aventaraAccess({ organization })\` maps the role "${role}", and with dynamic access control an organization's admins name roles themselves: map only the organization plugin's static roles (${statics.join(", ")}).`);
102
+ }
103
+ }
104
+ }
105
+ };
106
+ /**
107
+ * The bridged claims for a session: the mapped value of the member's role in the
108
+ * session's active organization, and that organization's id, read from Better Auth's
109
+ * store; `undefined` when there is no active organization, no membership, or no role
110
+ * the bridge maps. The member's roles are read as Better Auth reads them, split on
111
+ * `,` and taken as they are: a stored role with whitespace in it (`member, owner`) is
112
+ * one Better Auth does not grant, and gives nothing here.
113
+ */
114
+ export const bridgedClaims = async (bridge, context, source) => {
115
+ const organizationId = source.session.activeOrganizationId;
116
+ if (typeof organizationId !== "string" || organizationId === "") {
117
+ return undefined;
118
+ }
119
+ const member = await context.adapter.findOne({
120
+ model: "member",
121
+ where: [
122
+ { field: "userId", value: source.user.id },
123
+ { field: "organizationId", value: organizationId },
124
+ ],
125
+ });
126
+ if (typeof member?.role !== "string") {
127
+ return undefined;
128
+ }
129
+ const held = member.role.split(",");
130
+ for (const role of held) {
131
+ if (!ROLE_NAME.test(role)) {
132
+ return undefined;
133
+ }
134
+ }
135
+ for (const [role, value] of bridge.roles) {
136
+ if (held.includes(role)) {
137
+ return {
138
+ [bridge.claim]: value,
139
+ [bridge.organizationClaim]: organizationId,
140
+ };
141
+ }
142
+ }
143
+ return undefined;
144
+ };
@@ -0,0 +1,12 @@
1
+ /** A JSON value, as the canonical writer reads it. */
2
+ export type CanonicalJson = string | number | boolean | null | readonly CanonicalJson[] | {
3
+ readonly [key: string]: CanonicalJson | undefined;
4
+ };
5
+ /** JSON text with every object's members sorted by key and no whitespace. */
6
+ export declare const canonicalJson: (value: CanonicalJson) => string;
7
+ /** A copy of a list of names, sorted: the order of a list the format calls a set. */
8
+ export declare const sortedNames: <T extends string>(list: readonly T[]) => T[];
9
+ /** Orders rules by their `id`. */
10
+ export declare const byId: <T extends {
11
+ readonly id: string;
12
+ }>(a: T, b: T) => number;
@@ -0,0 +1,30 @@
1
+ // The canonical JSON text both Access file formats hash (`policies.hash.ts`,
2
+ // `guardrails/guardrails.hash.ts`): every object's members sorted by key (UTF-16
3
+ // code units), whitespace removed, members whose value is `undefined` left out, and
4
+ // strings and numbers written as `JSON.stringify` writes them (RFC 8785's rules for
5
+ // the values these files hold). A number JSON cannot carry (NaN, Infinity) throws. Which lists a format treats as sets is the format's
6
+ // own decision; this writer keeps every list in the order it is given.
7
+ /** JSON text with every object's members sorted by key and no whitespace. */
8
+ export const canonicalJson = (value) => {
9
+ if (typeof value === "number" && !Number.isFinite(value)) {
10
+ // `JSON.stringify` writes NaN and Infinity as `null`: two files that decide
11
+ // differently would share one hash.
12
+ throw new Error(`Aventara Access: a file holds the number ${value}, and JSON carries finite numbers only`);
13
+ }
14
+ if (value === null || typeof value !== "object") {
15
+ return JSON.stringify(value);
16
+ }
17
+ if (Array.isArray(value)) {
18
+ return `[${value.map(canonicalJson).join(",")}]`;
19
+ }
20
+ const record = value;
21
+ const members = Object.keys(record)
22
+ .filter((key) => record[key] !== undefined)
23
+ .sort()
24
+ .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`);
25
+ return `{${members.join(",")}}`;
26
+ };
27
+ /** A copy of a list of names, sorted: the order of a list the format calls a set. */
28
+ export const sortedNames = (list) => [...list].sort();
29
+ /** Orders rules by their `id`. */
30
+ export const byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ // The `avaccess` bin. It runs the program in the working directory, waits for its
3
+ // lines to reach the terminal or the pipe, and exits with its code: a command that
4
+ // loaded the Access server's module (`keys`) may leave a database pool open, and
5
+ // the bin does not wait for it. A command that runs until stopped (`serve`) stops on
6
+ // SIGTERM or SIGINT; the bin listens for them only once such a command asks.
7
+ import process from "node:process";
8
+ import { runAvaccess } from "./avaccess.program.js";
9
+ const flushed = () => new Promise((resolve) => {
10
+ process.stdout.write("", () => {
11
+ process.stderr.write("", () => resolve());
12
+ });
13
+ });
14
+ const code = await runAvaccess(process.argv.slice(2), process.cwd(), process.env, {
15
+ out: (line) => {
16
+ process.stdout.write(`${line}\n`);
17
+ },
18
+ err: (line) => {
19
+ process.stderr.write(`${line}\n`);
20
+ },
21
+ }, () => new Promise((resolve) => {
22
+ process.once("SIGTERM", () => resolve("SIGTERM"));
23
+ process.once("SIGINT", () => resolve("SIGINT"));
24
+ }));
25
+ await flushed();
26
+ process.exit(code);
@@ -0,0 +1,15 @@
1
+ import { type ExitCode } from "./cli.context.js";
2
+ /** What the program prints and where. */
3
+ type ProgramOutput = {
4
+ out(line: string): void;
5
+ err(line: string): void;
6
+ };
7
+ /**
8
+ * Runs `avaccess` with `args` (the command line after the bin's name) in `cwd`, and
9
+ * answers its exit code. `untilStopped` resolves when a command that runs until
10
+ * stopped (`serve`) should stop; the bin resolves it on SIGTERM or SIGINT.
11
+ */
12
+ export declare const runAvaccess: (args: readonly string[], cwd: string, env: {
13
+ readonly [name: string]: string | undefined;
14
+ }, printed: ProgramOutput, untilStopped?: () => Promise<string>) => Promise<ExitCode>;
15
+ export {};
@@ -0,0 +1,166 @@
1
+ // The `avaccess` program: the command line read, the command run, its refusal printed
2
+ // and its exit code returned. The bin (`avaccess.bin.ts`) only calls it.
3
+ import { parseArgs } from "node:util";
4
+ import { CLIENT_TRANSLATORS, ORM_TRANSLATORS, } from "../generator/orm.translator.js";
5
+ import { ENV_VARIABLE } from "./beacon-pins.file.js";
6
+ import { CliFailure, EXIT_ERROR, EXIT_OK, messageOf, versionOf, } from "./cli.context.js";
7
+ import { diffCommand } from "./diff.command.js";
8
+ import { generateCommand } from "./generate.command.js";
9
+ import { keysCommand } from "./keys.command.js";
10
+ import { pullCommand } from "./pull.command.js";
11
+ import { pushCommand } from "./push.command.js";
12
+ import { KEY_VARIABLE, REGISTRY_URL_VARIABLE } from "./registry.client.js";
13
+ import { schemaCommand } from "./schema.command.js";
14
+ import { MIGRATE_VARIABLE, PORT_VARIABLE, serveCommand, } from "./serve.command.js";
15
+ import { statusCommand } from "./status.command.js";
16
+ import { verifyCommand } from "./verify.command.js";
17
+ const REGISTRY = { type: "string" };
18
+ const NAMESPACE = { type: "string" };
19
+ const ORM = { type: "string" };
20
+ const BEACON = { type: "string" };
21
+ const ENV = { type: "string" };
22
+ /** Each command, what it runs, the flags it takes and its line in the help. */
23
+ const COMMANDS = {
24
+ push: {
25
+ run: pushCommand,
26
+ flags: { registry: REGISTRY, beacon: BEACON },
27
+ help: "push <rules> [--beacon <module>] push the namespace's policies file (a .json file, or the module holding the rules); --beacon sends the namespace's beacon and those of the namespaces it reads as foreign",
28
+ },
29
+ pull: {
30
+ run: pullCommand,
31
+ flags: {
32
+ registry: REGISTRY,
33
+ namespace: NAMESPACE,
34
+ out: { type: "string" },
35
+ foreign: { type: "string", multiple: true },
36
+ beacon: BEACON,
37
+ env: ENV,
38
+ },
39
+ help: "pull [--namespace <ns>] [--out <dir>] [--foreign <ns>:<Resource>,…] [--beacon <module>] [--env <env>] pull the namespace's file and the org's guardrails, and the foreign facts --foreign names; pin them in avAccess.lock.json, and the beacons of the namespaces read as foreign (checked through --beacon) in avAccess.beacons.<env>.json",
40
+ },
41
+ status: {
42
+ run: statusCommand,
43
+ flags: { registry: REGISTRY },
44
+ help: "status is avAccess.lock.json behind the registry? (exit 1 when it is)",
45
+ },
46
+ diff: {
47
+ run: diffCommand,
48
+ flags: { registry: REGISTRY, namespace: NAMESPACE },
49
+ help: "diff [<from> [<to>]] [--namespace <ns>] the rules that changed between two revisions (lock, head, a hash); default lock → head",
50
+ },
51
+ verify: {
52
+ run: verifyCommand,
53
+ flags: { env: ENV },
54
+ help: "verify [--env <env>] the pulled files against avAccess.lock.json, and each avAccess.beacons.<env>.json against the rules, offline (exit 1 on a changed file)",
55
+ },
56
+ generate: {
57
+ run: generateCommand,
58
+ flags: { orm: ORM, out: { type: "string" }, tsconfig: { type: "string" } },
59
+ help: "generate [--orm <orm>] [--out <file>] [--tsconfig <file>] the client of a service not on Aventara, from the pulled files: --out and <name>.internal.ts beside it (default orm: none; default out: src/generated/avAccess.ts)",
60
+ },
61
+ schema: {
62
+ run: schemaCommand,
63
+ flags: { orm: ORM, schema: { type: "string" }, out: { type: "string" } },
64
+ help: "schema --orm <orm> [--schema <path>] [--out <file>] the AccessSchema of the ORM's schema, to author rules with definePolicies (default out: src/generated/avAccess.schema.ts)",
65
+ },
66
+ serve: {
67
+ run: serveCommand,
68
+ flags: {
69
+ config: { type: "string" },
70
+ port: { type: "string" },
71
+ hostname: { type: "string" },
72
+ "max-body-bytes": { type: "string" },
73
+ migrate: { type: "boolean" },
74
+ },
75
+ help: "serve [--config <file>] [--port <port>] [--hostname <host>] [--max-body-bytes <bytes>] [--migrate] the standalone host, serving access.config.ts's instance on 127.0.0.1 unless --hostname says (--migrate or AVACCESS_MIGRATE=true runs Better Auth's migration first)",
76
+ },
77
+ keys: {
78
+ run: keysCommand,
79
+ flags: { server: { type: "string" } },
80
+ help: "keys issue <ns> | rotate <ns> <push|pull> | revoke <ns> <push|pull> --server <module> the operator's keys",
81
+ },
82
+ };
83
+ const isCommand = (name) => Object.hasOwn(COMMANDS, name);
84
+ const HELP = [
85
+ "avaccess: the Aventara Access registry, from a project",
86
+ "",
87
+ "usage: avaccess <command> [arguments] [flags]",
88
+ "",
89
+ ...Object.values(COMMANDS).map((command) => ` ${command.help}`),
90
+ "",
91
+ `The registry: --registry <url> or ${REGISTRY_URL_VARIABLE} (the Access server's URL).`,
92
+ `The key: ${KEY_VARIABLE}, the namespace's push or pull key (never a flag).`,
93
+ `The environment (beacon pins): --env <env> or ${ENV_VARIABLE}.`,
94
+ `serve: the port from --port or ${PORT_VARIABLE} (default 3000); the migration on start only with --migrate or ${MIGRATE_VARIABLE}=true.`,
95
+ `The translators (--orm): for generate ${Object.keys(CLIENT_TRANSLATORS).sort().join(", ")} (default none); for schema, required, ${Object.keys(ORM_TRANSLATORS).sort().join(", ")}.`,
96
+ "Exit codes: 0 done; 1 the check failed or the registry refused; 2 the command could not run.",
97
+ ].join("\n");
98
+ /** The control characters but the line feed (U+0000 to U+001F, U+007F to U+009F): what a terminal may act on. */
99
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: the characters it removes
100
+ const TERMINAL_CONTROL = /[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/g;
101
+ /** A line as the program prints it: whatever text it carries, no terminal control. */
102
+ const plain = (line) => line.replace(TERMINAL_CONTROL, " ");
103
+ /** Never resolves: a program run without a way to be stopped serves until its process ends. */
104
+ const NEVER_STOPPED = () => new Promise(() => { });
105
+ /**
106
+ * Runs `avaccess` with `args` (the command line after the bin's name) in `cwd`, and
107
+ * answers its exit code. `untilStopped` resolves when a command that runs until
108
+ * stopped (`serve`) should stop; the bin resolves it on SIGTERM or SIGINT.
109
+ */
110
+ export const runAvaccess = async (args, cwd, env, printed, untilStopped = NEVER_STOPPED) => {
111
+ const output = {
112
+ out: (line) => printed.out(plain(line)),
113
+ err: (line) => printed.err(plain(line)),
114
+ };
115
+ const [name, ...rest] = args;
116
+ if (name === undefined ||
117
+ name === "help" ||
118
+ name === "--help" ||
119
+ name === "-h") {
120
+ output.out(HELP);
121
+ return name === undefined ? EXIT_ERROR : EXIT_OK;
122
+ }
123
+ if (name === "--version" || name === "-v") {
124
+ output.out(await versionOf());
125
+ return EXIT_OK;
126
+ }
127
+ if (!isCommand(name)) {
128
+ output.err(`avaccess: no command ${JSON.stringify(name)}\n\n${HELP}`);
129
+ return EXIT_ERROR;
130
+ }
131
+ const command = COMMANDS[name];
132
+ try {
133
+ let parsed;
134
+ try {
135
+ parsed = parseArgs({
136
+ args: rest,
137
+ options: { ...command.flags, help: { type: "boolean", short: "h" } },
138
+ allowPositionals: true,
139
+ strict: true,
140
+ });
141
+ }
142
+ catch (error) {
143
+ throw new CliFailure(EXIT_ERROR, `${messageOf(error)}\nusage: avaccess ${command.help}`);
144
+ }
145
+ if (parsed.values.help === true) {
146
+ output.out(`usage: avaccess ${command.help}`);
147
+ return EXIT_OK;
148
+ }
149
+ return await command.run({
150
+ cwd,
151
+ env,
152
+ flags: parsed.values,
153
+ positionals: parsed.positionals,
154
+ out: output.out,
155
+ untilStopped,
156
+ });
157
+ }
158
+ catch (error) {
159
+ if (error instanceof CliFailure) {
160
+ output.err(`avaccess ${name}: ${error.code === undefined ? "" : `${error.code}: `}${error.message}`);
161
+ return error.exitCode;
162
+ }
163
+ output.err(`avaccess ${name}: ${messageOf(error)}`);
164
+ return EXIT_ERROR;
165
+ }
166
+ };
@@ -0,0 +1,14 @@
1
+ import { type CliContext } from "./cli.context.js";
2
+ /** The environment variable that names the environment, when `--env` does not. */
3
+ export declare const ENV_VARIABLE = "AVACCESS_ENV";
4
+ /** The pins file of `environment`, at the project's root. */
5
+ export declare const beaconPinsFile: (environment: string) => string;
6
+ /** The environment `--env` or `AVACCESS_ENV` selects; `undefined` when neither does. */
7
+ export declare const environmentOf: (context: CliContext) => string | undefined;
8
+ /**
9
+ * Checks every `avAccess.beacons.<environment>.json` in the project against the
10
+ * lock's namespace and the `owners` its pulled file reads, and that `selected`'s is
11
+ * there when one is selected. Answers the environments checked. A file that fails
12
+ * the check fails it (`ACCESS_LOCK_MISMATCH`).
13
+ */
14
+ export declare const verifyBeaconPins: (cwd: string, namespace: string, owners: readonly string[], selected: string | undefined) => Promise<readonly string[]>;
@@ -0,0 +1,62 @@
1
+ // `avAccess.beacons.<environment>.json`: one environment's beacon pins. A beacon is
2
+ // its database's own, so a staging and a production database have different ones;
3
+ // the pins live beside `avAccess.lock.json` in one file per environment, and the lock
4
+ // itself (the policy and foreign facts hashes) holds against any registry.
5
+ //
6
+ // The environment is `--env <name>`, else `AVACCESS_ENV`, as the registry's URL is
7
+ // `--registry` else `AVACCESS_REGISTRY_URL`. `avaccess pull` writes the selected
8
+ // environment's file; `avaccess verify` checks every one in the project, and the
9
+ // selected one must be there. At startup the app hands its environment's pins to
10
+ // `checkBeacons`, read from this file or from its own environment.
11
+ import { readdir, readFile } from "node:fs/promises";
12
+ import { resolve } from "node:path";
13
+ import { beaconPinsProblem, } from "../beacon/beacon.check.js";
14
+ import { CliFailure, EXIT_ERROR, EXIT_FAILED, messageOf, stringFlag, } from "./cli.context.js";
15
+ /** The environment variable that names the environment, when `--env` does not. */
16
+ export const ENV_VARIABLE = "AVACCESS_ENV";
17
+ const ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
18
+ const FILE = /^avAccess\.beacons\.([a-z][a-z0-9_-]{0,62})\.json$/;
19
+ /** The pins file of `environment`, at the project's root. */
20
+ export const beaconPinsFile = (environment) => `avAccess.beacons.${environment}.json`;
21
+ /** The environment `--env` or `AVACCESS_ENV` selects; `undefined` when neither does. */
22
+ export const environmentOf = (context) => {
23
+ const given = stringFlag(context, "env") ?? context.env[ENV_VARIABLE];
24
+ if (given === undefined || given === "") {
25
+ return undefined;
26
+ }
27
+ if (!ENVIRONMENT.test(given)) {
28
+ throw new CliFailure(EXIT_ERROR, `the environment ${JSON.stringify(given)} is not an environment name (a lowercase letter, then lowercase letters, digits, "_" or "-"; such as "production")`);
29
+ }
30
+ return given;
31
+ };
32
+ /**
33
+ * Checks every `avAccess.beacons.<environment>.json` in the project against the
34
+ * lock's namespace and the `owners` its pulled file reads, and that `selected`'s is
35
+ * there when one is selected. Answers the environments checked. A file that fails
36
+ * the check fails it (`ACCESS_LOCK_MISMATCH`).
37
+ */
38
+ export const verifyBeaconPins = async (cwd, namespace, owners, selected) => {
39
+ const environments = (await readdir(cwd, { withFileTypes: true }))
40
+ .filter((entry) => entry.isFile())
41
+ .map((entry) => FILE.exec(entry.name)?.[1])
42
+ .filter((name) => name !== undefined)
43
+ .sort();
44
+ if (selected !== undefined && !environments.includes(selected)) {
45
+ throw new CliFailure(EXIT_FAILED, `no ${beaconPinsFile(selected)}: the rules ${owners.length === 0 ? "need no beacon pins" : `read ${owners.map((owner) => `"${owner}"`).join(", ")} as foreign`}, and environment "${selected}" has no pins. Pull them with \`avaccess pull --env ${selected} --beacon <module>\``, "ACCESS_LOCK_MISMATCH");
46
+ }
47
+ for (const environment of environments) {
48
+ const path = beaconPinsFile(environment);
49
+ let pins;
50
+ try {
51
+ pins = JSON.parse(await readFile(resolve(cwd, path), "utf8"));
52
+ }
53
+ catch (error) {
54
+ throw new CliFailure(EXIT_FAILED, `${path} is no longer JSON (${messageOf(error)}): pull it again with \`avaccess pull --env ${environment} --beacon <module>\``, "ACCESS_LOCK_MISMATCH");
55
+ }
56
+ const problem = beaconPinsProblem(namespace, owners, pins, environment);
57
+ if (problem !== undefined) {
58
+ throw new CliFailure(EXIT_FAILED, `${path}: ${problem}`, "ACCESS_LOCK_MISMATCH");
59
+ }
60
+ }
61
+ return environments;
62
+ };
@@ -0,0 +1,20 @@
1
+ import { type AccessBeaconStore } from "../beacon/beacon.store.js";
2
+ import { type CliContext } from "./cli.context.js";
3
+ /** The store `--beacon` names; `undefined` when the flag is not given. */
4
+ export declare const beaconStoreOf: (context: CliContext) => Promise<AccessBeaconStore | undefined>;
5
+ /**
6
+ * The namespace's own beacon, its row written the first time; a row the registry
7
+ * does not hold (`registered`) is refused, never adopted (`ensureBeacon`).
8
+ */
9
+ export declare const ownBeacon: (store: AccessBeaconStore, namespace: string, registered: string | null) => Promise<string>;
10
+ /**
11
+ * `owner`'s beacon as the store's connection reads it. No row, or a row that holds
12
+ * no beacon, is refused here: the service does not reach `owner`'s database, and the
13
+ * registry would refuse the push for it.
14
+ */
15
+ export declare const readOwnerBeacon: (store: AccessBeaconStore, owner: string) => Promise<string>;
16
+ /**
17
+ * Holds `owner`'s beacon, as the store reads it, to the hash `owner` pushed (`null`:
18
+ * none): the pull's check that this service reaches `owner`'s database.
19
+ */
20
+ export declare const holdOwnerBeacon: (store: AccessBeaconStore, owner: string, pushed: string | null) => Promise<void>;
@@ -0,0 +1,71 @@
1
+ // `--beacon <module>`: the project's beacon store, for `avaccess push` and `avaccess
2
+ // pull`. The module (TypeScript or JavaScript, loaded as `push <rules>` loads the
3
+ // rules) exports an `AccessBeaconStore` over the service's own data layer, as its
4
+ // default export or as `beacon`: `read(namespace)` reads a namespace's row in
5
+ // `_aventara_beacon`, `create(namespace, value)` writes one. The CLI uses it to write
6
+ // and read the namespace's own beacon, and to read the beacons of the namespaces its
7
+ // rules read as foreign, through the connection the service itself runs on. No line
8
+ // it prints quotes a beacon.
9
+ import { beaconHashOf } from "../beacon/beacon.check.js";
10
+ import { BEACON_TABLE, ensureBeacon, isBeacon, } from "../beacon/beacon.store.js";
11
+ import { isRecord } from "../policies.compiler.js";
12
+ import { CliFailure, codeOf, EXIT_ERROR, EXIT_FAILED, messageOf, stringFlag, } from "./cli.context.js";
13
+ import { loadProjectModule, pickExport } from "./module.loader.js";
14
+ const isStore = (value) => isRecord(value) &&
15
+ typeof value.read === "function" &&
16
+ typeof value.create === "function";
17
+ /** The store `--beacon` names; `undefined` when the flag is not given. */
18
+ export const beaconStoreOf = async (context) => {
19
+ const path = stringFlag(context, "beacon");
20
+ if (path === undefined) {
21
+ return undefined;
22
+ }
23
+ const exports = await loadProjectModule(context.cwd, path);
24
+ return pickExport(exports, path, "beacon", isStore, "beacon store (`{ read(namespace), create(namespace, value) }`)");
25
+ };
26
+ /**
27
+ * The namespace's own beacon, its row written the first time; a row the registry
28
+ * does not hold (`registered`) is refused, never adopted (`ensureBeacon`).
29
+ */
30
+ export const ownBeacon = async (store, namespace, registered) => {
31
+ try {
32
+ return (await ensureBeacon(store, namespace, registered)).value;
33
+ }
34
+ catch (error) {
35
+ throw new CliFailure(codeOf(error) === undefined ? EXIT_ERROR : EXIT_FAILED, `namespace "${namespace}"'s own beacon could not be read or written through the --beacon store (${messageOf(error)})`, codeOf(error));
36
+ }
37
+ };
38
+ /**
39
+ * `owner`'s beacon as the store's connection reads it. No row, or a row that holds
40
+ * no beacon, is refused here: the service does not reach `owner`'s database, and the
41
+ * registry would refuse the push for it.
42
+ */
43
+ export const readOwnerBeacon = async (store, owner) => {
44
+ let value;
45
+ try {
46
+ value = await store.read(owner);
47
+ }
48
+ catch (error) {
49
+ throw new CliFailure(EXIT_ERROR, `reading namespace "${owner}"'s row in ${BEACON_TABLE} through the --beacon store failed (${messageOf(error)})`);
50
+ }
51
+ if (value === null || value === undefined) {
52
+ throw new CliFailure(EXIT_FAILED, `the rules read namespace "${owner}"'s Resources as foreign, and the --beacon store's connection reads no row of "${owner}" in ${BEACON_TABLE}: this service does not reach "${owner}"'s database. A foreign Resource is read only within one database; across databases, carry the fact in a JWT claim instead (\`policies.claims\`)`, "ACCESS_FOREIGN_CROSS_DATABASE");
53
+ }
54
+ if (!isBeacon(value)) {
55
+ throw new CliFailure(EXIT_FAILED, `namespace "${owner}"'s row in ${BEACON_TABLE} holds no beacon (avb_ and 43 base64url characters): it was changed outside the service that writes it`, "ACCESS_BEACON_MISMATCH");
56
+ }
57
+ return value;
58
+ };
59
+ /**
60
+ * Holds `owner`'s beacon, as the store reads it, to the hash `owner` pushed (`null`:
61
+ * none): the pull's check that this service reaches `owner`'s database.
62
+ */
63
+ export const holdOwnerBeacon = async (store, owner, pushed) => {
64
+ if (pushed === null) {
65
+ throw new CliFailure(EXIT_FAILED, `the rules read namespace "${owner}"'s Resources as foreign, and "${owner}" has pushed no beacon, so nothing shows the two services share a database: "${owner}"'s service pushes its file with \`avaccess push --beacon <module>\` first. Nothing was written`, "ACCESS_FOREIGN_CROSS_DATABASE");
66
+ }
67
+ const read = await readOwnerBeacon(store, owner);
68
+ if ((await beaconHashOf(owner, read)) !== pushed) {
69
+ throw new CliFailure(EXIT_FAILED, `namespace "${owner}"'s beacon, as the --beacon store's connection reads it, is not the one "${owner}" pushed: this service and "${owner}" are on different databases. A foreign Resource is read only within one database; across databases, carry the fact in a JWT claim instead (\`policies.claims\`). Nothing was written`, "ACCESS_FOREIGN_CROSS_DATABASE");
70
+ }
71
+ };
@@ -0,0 +1,49 @@
1
+ /** The command did what it says. */
2
+ export declare const EXIT_OK = 0;
3
+ /** It ran, and the answer is no. */
4
+ export declare const EXIT_FAILED = 1;
5
+ /** It could not run. */
6
+ export declare const EXIT_ERROR = 2;
7
+ export type ExitCode = typeof EXIT_OK | typeof EXIT_FAILED | typeof EXIT_ERROR;
8
+ /** A command's refusal: the exit code, the message, and the error code when Access or the registry named one. */
9
+ export declare class CliFailure extends Error {
10
+ readonly exitCode: typeof EXIT_FAILED | typeof EXIT_ERROR;
11
+ readonly code: string | undefined;
12
+ constructor(exitCode: typeof EXIT_FAILED | typeof EXIT_ERROR, message: string, code?: string);
13
+ }
14
+ /** The flags a command reads, parsed. */
15
+ export type CliFlags = {
16
+ readonly [name: string]: string | boolean | readonly (string | boolean)[] | undefined;
17
+ };
18
+ export type CliContext = {
19
+ /** The project's directory: the lock and the pulled files are read and written under it. */
20
+ readonly cwd: string;
21
+ readonly env: {
22
+ readonly [name: string]: string | undefined;
23
+ };
24
+ readonly flags: CliFlags;
25
+ readonly positionals: readonly string[];
26
+ /** Writes one line to standard output. */
27
+ out(line: string): void;
28
+ /**
29
+ * Resolves, with the signal's name, once the process is told to stop (SIGTERM or
30
+ * SIGINT). Only a command that runs until stopped (`serve`) calls it; the bin
31
+ * listens for the signals from that call on.
32
+ */
33
+ untilStopped(): Promise<string>;
34
+ };
35
+ /** A string flag's value, or `undefined`. */
36
+ export declare const stringFlag: (context: CliContext, name: string) => string | undefined;
37
+ /** The command's positional arguments, refused when there are more or fewer than it takes. */
38
+ export declare const positionals: (context: CliContext, usage: string, min: number, max: number) => readonly string[];
39
+ /**
40
+ * A file path inside the project, `/`-separated and relative to it, from a flag's
41
+ * value; refused when it leaves the project or does not end in `extension`.
42
+ */
43
+ export declare const projectFilePath: (value: string, flag: string, example: string, extension: string) => string;
44
+ /** The version of `@aventara/access` this bin is, from its own manifest. */
45
+ export declare const versionOf: () => Promise<string>;
46
+ /** An error's message without the package's prefix: the CLI names itself. */
47
+ export declare const messageOf: (error: unknown) => string;
48
+ /** The `code` an Access error carries (read by shape: a module the CLI loads may hold its own copy of Access). */
49
+ export declare const codeOf: (error: unknown) => string | undefined;