@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,78 @@
1
+ // A service's own beacon: one row in `_aventara_beacon` with a random value, written
2
+ // once by the service through its own data layer and never changed. The row is
3
+ // keyed by the namespace, so a table holds one row per namespace: in a deployment
4
+ // with a database schema per service each schema's table holds its own service's
5
+ // row, and in one with a single schema (or a database with none) one table holds
6
+ // every namespace's. `avaccess push --beacon <module>` writes the row the first time
7
+ // and sends its value with the file; the registry keeps only its hash, bound to the
8
+ // namespace.
9
+ //
10
+ // In a shared schema every service with write access to the table is a co-tenant
11
+ // of every row: one that overwrites or deletes another namespace's row cannot make
12
+ // a reader pass (the registry holds the owner's hash, bound to the owner), but it
13
+ // makes the readers refuse at startup until the owner's service writes and pushes
14
+ // its beacon again. The check fails closed; give each service's database role write
15
+ // access to its own row only where the database can.
16
+ //
17
+ // Access writes no SQL and names no ORM: the app hands in an `AccessBeaconStore`
18
+ // over its own data layer (the generated client's `prismaBeacon` builds one over a
19
+ // Prisma client).
20
+ import { AccessError } from "../access/access.error.js";
21
+ import { randomSecret } from "../random-secret.generator.js";
22
+ import { BEACON, beaconHashOf } from "./beacon.check.js";
23
+ /** The beacon table's name. */
24
+ export const BEACON_TABLE = "_aventara_beacon";
25
+ /** A new beacon: 256 random bits from the runtime's CSPRNG, written `avb_…`. */
26
+ export const newBeacon = () => randomSecret("avb_");
27
+ /** Whether `value` is a beacon. */
28
+ export const isBeacon = (value) => typeof value === "string" && BEACON.test(value);
29
+ /**
30
+ * The namespace's own beacon, for its push: a new row written now when the
31
+ * namespace has none, or the row it has when its hash is the one the registry holds
32
+ * for the namespace (`registered`, `null` when it holds none). A row this service
33
+ * did not write and the registry does not know is never adopted: another service
34
+ * with write access to the table (a co-tenant of a shared schema) could have planted
35
+ * it, so it is refused (`ACCESS_BEACON_MISMATCH`) until an operator verifies it or
36
+ * deletes it, and the next push writes a new one. A row whose value is no beacon is
37
+ * refused too, never overwritten.
38
+ */
39
+ export const ensureBeacon = async (store, namespace, registered) => {
40
+ const found = await store.read(namespace);
41
+ if (found !== null) {
42
+ return { value: await known(found, namespace, registered), created: false };
43
+ }
44
+ const value = newBeacon();
45
+ try {
46
+ await store.create(namespace, value);
47
+ }
48
+ catch (error) {
49
+ // Another write took the namespace's row first: it is held to the registry as
50
+ // any row found is. Anything else is the store's own failure, reported as it was.
51
+ const written = await store.read(namespace);
52
+ if (written === null) {
53
+ throw error;
54
+ }
55
+ return {
56
+ value: await known(written, namespace, registered),
57
+ created: false,
58
+ };
59
+ }
60
+ const written = await store.read(namespace);
61
+ if (written !== value) {
62
+ throw new AccessError("ACCESS_BEACON_MISMATCH", `the beacon store wrote namespace "${namespace}"'s row in ${BEACON_TABLE}, and reading it back found ${written === null ? "none" : "another value"}: check that the store's read and create use the same table`);
63
+ }
64
+ return { value, created: true };
65
+ };
66
+ /** A row found for the namespace, taken only when the registry holds its hash. */
67
+ const known = async (value, namespace, registered) => {
68
+ if (!isBeacon(value)) {
69
+ throw new AccessError("ACCESS_BEACON_MISMATCH", `namespace "${namespace}"'s row in ${BEACON_TABLE} holds no beacon (avb_ and 43 base64url characters): it was changed outside Access. Delete the row; the next push writes a new one`);
70
+ }
71
+ if (registered === null) {
72
+ throw new AccessError("ACCESS_BEACON_MISMATCH", `namespace "${namespace}"'s row in ${BEACON_TABLE} is there, and the registry holds no beacon of "${namespace}": this push did not write the row, and another service with write access to the table may have. An operator verifies that "${namespace}"'s own service wrote it, or deletes it; the next push then writes a new one`);
73
+ }
74
+ if ((await beaconHashOf(namespace, value)) !== registered) {
75
+ throw new AccessError("ACCESS_BEACON_MISMATCH", `namespace "${namespace}"'s row in ${BEACON_TABLE} is not the beacon the registry holds for "${namespace}": it was changed outside "${namespace}"'s service, or this database's row is an old one. An operator verifies it or deletes it; the next push then writes a new one`);
76
+ }
77
+ return value;
78
+ };
@@ -0,0 +1,75 @@
1
+ import type { AventaraAccessPlugin } from "./access.plugin.js";
2
+ /** One question to the policy: may the signed-in user run `action` on `resource`, or on the one row `where` names. */
3
+ export type AccessQuestion<R extends string = string> = {
4
+ readonly resource: R;
5
+ readonly action: "read" | "create" | "update" | "delete";
6
+ /** One row, by its unique where (`{ id: "c1" }`). Omitted: may the user run `action` on `resource` at all. */
7
+ readonly where?: {
8
+ readonly [field: string]: unknown;
9
+ };
10
+ };
11
+ /** `aventaraAccessClient()`'s options. */
12
+ export type AventaraAccessClientOptions = {
13
+ /**
14
+ * The framework's API path or URL (its `api` option), where `_access/can` is
15
+ * served: `"/api"`, or `"https://api.example.com/api"`. A path is read against the
16
+ * auth client's `baseURL`, else the page's address.
17
+ */
18
+ readonly api: string;
19
+ /**
20
+ * The ClientContract's hash the questions are asked under (the generated client's).
21
+ * Without it, the client reads it from `GET {api}/_contract` once, and again when
22
+ * the framework answers that it changed.
23
+ */
24
+ readonly contractHash?: string;
25
+ };
26
+ /** Why `can` has no answers: the framework's (or the network's) refusal, as Better Auth's client reports one. */
27
+ export type AccessCanError = {
28
+ readonly status: number;
29
+ readonly statusText: string;
30
+ /** The framework's code (`A2005`: the client is stale), when it answered with one. */
31
+ readonly code?: string;
32
+ readonly message?: string;
33
+ };
34
+ /** `authClient.access.can`'s answer: one boolean per question, in order, or the error. */
35
+ export type AccessCanResult = {
36
+ readonly data: readonly boolean[];
37
+ readonly error: null;
38
+ } | {
39
+ readonly data: null;
40
+ readonly error: AccessCanError;
41
+ };
42
+ /** Better Auth's client fetch, as the plugin calls it. */
43
+ type ClientFetch = (url: string, options?: {
44
+ readonly method?: string;
45
+ readonly body?: unknown;
46
+ readonly headers?: {
47
+ readonly [name: string]: string;
48
+ };
49
+ }) => Promise<{
50
+ readonly data: unknown;
51
+ readonly error: unknown;
52
+ }>;
53
+ /** What `aventaraAccessClient()` returns: a Better Auth client plugin. */
54
+ export type AventaraAccessClientPlugin = {
55
+ readonly id: "aventara-access";
56
+ /** Types only: the server plugin, whose `jwt()` routes the client infers (`authClient.token()`). */
57
+ readonly $InferServerPlugin: AventaraAccessPlugin;
58
+ readonly pathMethods: {
59
+ readonly "/jwks": "GET";
60
+ };
61
+ readonly getActions: ($fetch: ClientFetch, $store: unknown, options: {
62
+ readonly baseURL?: string | undefined;
63
+ } | undefined) => {
64
+ readonly access: {
65
+ /** Asks the framework, in one request, whether the signed-in user may run each question. */
66
+ can(questions: readonly AccessQuestion[]): Promise<AccessCanResult>;
67
+ };
68
+ };
69
+ };
70
+ /**
71
+ * The client plugin: `createAuthClient({ plugins: [aventaraAccessClient({ api: "/api" })] })`
72
+ * gives `authClient.access.can([{ resource, action, where? }, ...])`.
73
+ */
74
+ export declare const aventaraAccessClient: (options: AventaraAccessClientOptions) => AventaraAccessClientPlugin;
75
+ export {};
@@ -0,0 +1,133 @@
1
+ // `aventaraAccessClient()`: the client plugin of `aventaraAccess()`, for Better
2
+ // Auth's `createAuthClient({ plugins: [...] })`. It adds `authClient.access.can`,
3
+ // which asks the framework, not Better Auth: one `POST {api}/_access/can` for the
4
+ // whole list of questions, answered by the same engine that decides the real calls
5
+ // (`accessOf(auth).policy`, with the row looked up within the caller's scope), one
6
+ // boolean per question. So the two paths have one evaluator. The answer only shapes
7
+ // the UI: the real call is still checked.
8
+ //
9
+ // It sends Better Auth's own fetch, so the session cookie (or the bearer token the
10
+ // app gives Better Auth's client) goes with it, and answers as Better Auth's client
11
+ // actions do, `{ data, error }`. It names no Node.js API and loads nothing from
12
+ // `better-auth` at runtime: it runs in the browser.
13
+ /** The framework protocol version the questions are asked under. */
14
+ const PROTOCOL_VERSION = "1";
15
+ /** A protocol envelope's code, when a body is one. */
16
+ const codeOf = (body) => {
17
+ const code = body?.code;
18
+ return typeof code === "string" ? code : undefined;
19
+ };
20
+ const failure = (status, statusText, body) => {
21
+ const code = codeOf(body);
22
+ const message = body?.message;
23
+ return {
24
+ data: null,
25
+ error: {
26
+ status,
27
+ statusText,
28
+ ...(code === undefined ? {} : { code }),
29
+ ...(typeof message === "string" ? { message } : {}),
30
+ },
31
+ };
32
+ };
33
+ /** Whether every answer is a boolean. */
34
+ const allBooleans = (answers) => {
35
+ for (const answer of answers) {
36
+ if (typeof answer !== "boolean") {
37
+ return false;
38
+ }
39
+ }
40
+ return true;
41
+ };
42
+ /** A Better Auth fetch error as `can` reports it. */
43
+ const fetchFailure = (error) => {
44
+ const { status, statusText } = (error ?? {});
45
+ return failure(typeof status === "number" ? status : 0, typeof statusText === "string" ? statusText : "", error);
46
+ };
47
+ /**
48
+ * The client plugin: `createAuthClient({ plugins: [aventaraAccessClient({ api: "/api" })] })`
49
+ * gives `authClient.access.can([{ resource, action, where? }, ...])`.
50
+ */
51
+ export const aventaraAccessClient = (options) => {
52
+ const { api, contractHash: given } = options ?? {};
53
+ if (typeof api !== "string" || api === "") {
54
+ throw new TypeError('Aventara Access: aventaraAccessClient takes `{ api }`, the framework\'s API path or URL ("/api").');
55
+ }
56
+ return {
57
+ id: "aventara-access",
58
+ $InferServerPlugin: {},
59
+ pathMethods: { "/jwks": "GET" },
60
+ getActions: ($fetch, _$store, clientOptions) => {
61
+ const base = clientOptions?.baseURL ??
62
+ globalThis
63
+ .location?.href;
64
+ /** `{api}/<route>`, absolute, so Better Auth's fetch does not read it against its own base path. */
65
+ const route = (path) => new URL(`${api.replace(/\/+$/, "")}/${path}`, base).href;
66
+ let discovered;
67
+ /** The ClientContract's hash: the one given, else `GET {api}/_contract`'s. */
68
+ const hashOf = async () => {
69
+ if (given !== undefined) {
70
+ return given;
71
+ }
72
+ if (discovered !== undefined) {
73
+ return discovered;
74
+ }
75
+ const { data, error } = await $fetch(route("_contract"), {
76
+ method: "GET",
77
+ });
78
+ const hash = data?.protocol?.hash;
79
+ if (error !== null && error !== undefined) {
80
+ return fetchFailure(error);
81
+ }
82
+ if (typeof hash !== "string") {
83
+ return failure(500, "The framework's /_contract answer carries no protocol hash");
84
+ }
85
+ discovered = hash;
86
+ return hash;
87
+ };
88
+ const ask = async (questions, hash) => $fetch(route("_access/can"), {
89
+ method: "POST",
90
+ body: { questions },
91
+ headers: {
92
+ "aventara-protocol-version": PROTOCOL_VERSION,
93
+ "aventara-contract-hash": hash,
94
+ },
95
+ });
96
+ const can = async (questions) => {
97
+ if (questions.length === 0) {
98
+ return { data: [], error: null };
99
+ }
100
+ let hash = await hashOf();
101
+ if (typeof hash !== "string") {
102
+ return hash;
103
+ }
104
+ let answer = await ask(questions, hash);
105
+ // A hash read from `_contract` is stale once the framework redeploys: read it
106
+ // again, once.
107
+ if (given === undefined &&
108
+ codeOf(answer.error) === "A2005" &&
109
+ answer.error.status === 409) {
110
+ discovered = undefined;
111
+ hash = await hashOf();
112
+ if (typeof hash !== "string") {
113
+ return hash;
114
+ }
115
+ answer = await ask(questions, hash);
116
+ }
117
+ if (answer.error !== null && answer.error !== undefined) {
118
+ return fetchFailure(answer.error);
119
+ }
120
+ const answers = answer.data
121
+ ?.data;
122
+ if (codeOf(answer.data) !== "A1000" ||
123
+ !Array.isArray(answers) ||
124
+ answers.length !== questions.length ||
125
+ !allBooleans(answers)) {
126
+ return failure(500, "The framework's /_access/can answer is not one boolean per question", answer.data);
127
+ }
128
+ return { data: answers, error: null };
129
+ };
130
+ return { access: { can } };
131
+ },
132
+ };
133
+ };
@@ -0,0 +1,164 @@
1
+ import type { jwt } from "better-auth/plugins/jwt";
2
+ import type { AccessPoliciesOptions } from "../access/access-options.js";
3
+ import { type BetterAuthBlock } from "../access/better-auth.hardening.js";
4
+ import { type BetterAuthModules } from "../access/better-auth.loader.js";
5
+ import { type AccessClaimsDeclaration, type AccessClaimsOf, type AccessClaimsSource, type AccessIdentity } from "../access/claims.mapper.js";
6
+ import { type BetterAuthTablesFacts, type CheckedPolicies } from "../access/policies-block.checker.js";
7
+ import { type AuditLog } from "../audit/audit.log.js";
8
+ import type { AccessAuditOptions } from "../audit/audit-event.js";
9
+ import { type AccessMcpOptions } from "../mcp/mcp-options.js";
10
+ import { type AccessPasswordsOptions } from "../passwords/passwords.options.js";
11
+ import { type RegistrySettings } from "../registry/access.registry.js";
12
+ import { type AccessRegistryOptions } from "../registry/registry.options.js";
13
+ import { type AccessResourcesOptions, type ResourcesChoice } from "../resources/auth-tables.guard.js";
14
+ import { type AccessOrganizationBridge, type CheckedBridge } from "./organization.bridge.js";
15
+ type GovernedShape = {
16
+ readonly [resource: string]: readonly string[];
17
+ };
18
+ /**
19
+ * The principal (`me`) an instance hands the engine: the rules' principal type when
20
+ * the rules are the builder, else what `principal` returns (by default the identity).
21
+ */
22
+ type AccessPrincipalOf<R, M> = unknown extends R ? M : R;
23
+ /**
24
+ * The request's principal (`me`) from a verified identity: the app maps `sub` to its
25
+ * own profile, and `null` leaves the caller anonymous. With the builder as the rules,
26
+ * it returns their principal type.
27
+ */
28
+ type AccessPrincipalFunction<C extends AccessClaimsDeclaration, R, M> = (identity: AccessIdentity<C>) => (unknown extends R ? M : NoInfer<R>) | null | Promise<(unknown extends R ? M : NoInfer<R>) | null>;
29
+ /**
30
+ * The `principal` option: optional while the identity (`{ sub, claims }`) is itself
31
+ * the rules' principal type, and required when the rules are typed with another.
32
+ */
33
+ type AccessPrincipalOption<C extends AccessClaimsDeclaration, R, M> = [
34
+ AccessIdentity<C>
35
+ ] extends [R] ? {
36
+ /** The request's principal (`me`) from the verified identity; without it, `me` is the identity, `{ sub, claims }`. */
37
+ readonly principal?: AccessPrincipalFunction<C, R, M>;
38
+ } : {
39
+ /** The request's principal (`me`) from the verified identity, typed as the rules' principal. */
40
+ readonly principal: AccessPrincipalFunction<C, R, M>;
41
+ };
42
+ /** `aventaraAccess()`'s options. */
43
+ export type AventaraAccessOptions<C extends AccessClaimsDeclaration, R, Gov extends GovernedShape, M, E extends boolean = false> = {
44
+ /** The rules and the claims they read, as `createAccess` takes them. */
45
+ readonly policies: AccessPoliciesOptions<C, R, Gov>;
46
+ /**
47
+ * Which Resource of the rules' schema is each of Better Auth's tables, and which
48
+ * of them are Resources of the rules, as `createAccess` takes it. Every other
49
+ * table of Better Auth's is closed, and no grant opens a secret column.
50
+ */
51
+ readonly resources?: AccessResourcesOptions;
52
+ /**
53
+ * The organization bridge: a member's role in the session's active organization
54
+ * (Better Auth's `organization()` plugin) fills one claim of the token, and the
55
+ * organization another. Each grant on the role must be held to the organization.
56
+ */
57
+ readonly organization?: AccessOrganizationBridge<C>;
58
+ /**
59
+ * The audit log, as `createAccess` takes it: `true` writes the `accessAudit` table
60
+ * (Better Auth's `generate`/`migrate` create it), `{ sink }` hands each event to the
61
+ * app instead. Off by default.
62
+ */
63
+ readonly audit?: AccessAuditOptions;
64
+ /**
65
+ * How passwords are hashed, as `createAccess` takes it: argon2id at
66
+ * `"owasp-minimum"` (the default) or `"rfc9106-low-memory"`, and with
67
+ * `legacyScrypt: true` Better Auth's scrypt hashes accepted. A hash at another
68
+ * profile, or scrypt, is rehashed at the profile at its account's next sign-in.
69
+ * An app that sets Better Auth's `emailAndPassword.password` hashes its
70
+ * own way, and may not set this too.
71
+ */
72
+ readonly passwords?: AccessPasswordsOptions;
73
+ /**
74
+ * The policy registry, as `createAccess` takes it: `{ enabled: true }` makes the
75
+ * app the hub other services push their files to and pull them from, its tables
76
+ * this plugin's (Better Auth's `generate`/`migrate` create them) and its routes
77
+ * this plugin's endpoints under `/access/v1/namespaces`; `accessOf(auth).registry`
78
+ * is the operator's API. Off by default: no tables, no routes.
79
+ */
80
+ readonly registry?: AccessRegistryOptions<E>;
81
+ /**
82
+ * Access as the authorization layer of an MCP endpoint, as `createAccess`
83
+ * takes it: `{ resource }`, the endpoint's URL. Better Auth's OAuth provider
84
+ * (`@better-auth/oauth-provider`) goes in `plugins` after this plugin, with the
85
+ * endpoint among its `resources`; `accessOf(auth).mcp` is the framework's
86
+ * `mcp.authorization`.
87
+ */
88
+ readonly mcp?: AccessMcpOptions;
89
+ /**
90
+ * The service's own audience, an opt-in, as `createAccess` takes it (RFC 8707): with
91
+ * it, `accessOf(auth).state` takes a bearer token only when it is the OAuth
92
+ * provider's access token addressed to this audience alone, and refuses the
93
+ * one-org token `GET /token` issues. Off by default.
94
+ */
95
+ readonly audience?: string;
96
+ } & AccessPrincipalOption<C, R, M>;
97
+ /** The `jwt()` plugin `aventaraAccess()` carries inside it. */
98
+ type JwtPlugin = ReturnType<typeof jwt>;
99
+ /** What `aventaraAccess()` returns: a Better Auth plugin, with `jwt()`'s endpoints, hooks and table. */
100
+ export type AventaraAccessPlugin<C extends AccessClaimsDeclaration = AccessClaimsDeclaration, Gov extends GovernedShape = GovernedShape, M = unknown, E extends boolean = boolean, P extends boolean = boolean> = {
101
+ readonly id: "aventara-access";
102
+ readonly version: string;
103
+ readonly init: (context: unknown) => void;
104
+ readonly endpoints: JwtPlugin["endpoints"];
105
+ readonly hooks: JwtPlugin["hooks"];
106
+ readonly schema: JwtPlugin["schema"];
107
+ /** Types only, as Better Auth's plugins carry theirs: `typeof auth.$Infer.AccessMe` is the principal. */
108
+ readonly $Infer: {
109
+ readonly AccessMe: M;
110
+ readonly AccessClaims: AccessClaimsOf<C>;
111
+ readonly AccessGoverned: Gov;
112
+ /** Whether the plugin serves the registry (`registry: { enabled: true }`): `accessOf(auth).registry` is typed present exactly then. */
113
+ readonly AccessRegistryEnabled: E;
114
+ /** Whether `mcp` is given: `accessOf(auth).mcp` is there exactly then. */
115
+ readonly AccessMcp: P;
116
+ };
117
+ };
118
+ /** What Better Auth's options at `init` (or `auth.options`) fixed: the facts the request state and the engine read. */
119
+ type PluginStart = {
120
+ readonly issuer: string;
121
+ readonly crossSite: boolean;
122
+ readonly facts: BetterAuthTablesFacts;
123
+ /** With `mcp`: the MCP endpoint, and the authorization server `accessOf(auth).mcp` names (the issuer). */
124
+ readonly mcp?: {
125
+ readonly resource: string;
126
+ readonly server: string;
127
+ };
128
+ /** With `audience`: the service's own audience. */
129
+ readonly audience?: string;
130
+ /** The provider's UserInfo URL, which a token asked with `openid` names beside its resource. */
131
+ readonly userInfo: string | undefined;
132
+ };
133
+ /** What `aventaraAccess()` was built with, for `accessOf`. */
134
+ type PluginKernel = {
135
+ readonly checked: CheckedPolicies;
136
+ readonly resources: ResourcesChoice;
137
+ readonly bridge: CheckedBridge | undefined;
138
+ readonly modules: BetterAuthModules;
139
+ /** The audit log, when `audit` is on: the decisions of `accessOf(auth).policy` go to it. */
140
+ readonly auditLog: AuditLog | undefined;
141
+ /** The registry's settings when the plugin serves it, for `accessOf(auth).registry`; `undefined` when it is off. */
142
+ readonly registry: RegistrySettings | undefined;
143
+ /** Better Auth's options, checked as `init` checks them. */
144
+ start(options: BetterAuthBlock, own: unknown): PluginStart;
145
+ /** A session's claims, as its token is issued with them; `context` is Better Auth's. */
146
+ claimsFor(context: () => Promise<unknown>, source: AccessClaimsSource): Promise<{
147
+ readonly [claim: string]: string | number | boolean;
148
+ }>;
149
+ };
150
+ /**
151
+ * The Access plugin for Better Auth: `betterAuth({ plugins: [aventaraAccess({ policies })] })`.
152
+ * It carries Better Auth's `jwt()` (whose tokens carry `sub` and the declared claims,
153
+ * under the app's base URL as issuer and audience), so `jwt()` is not added beside
154
+ * it. `accessOf(auth)` is the framework's `access` option. `better-auth` is loaded
155
+ * here, and must be in Access's range, `~1.7.12` (1.7.12 or a later 1.7 patch).
156
+ */
157
+ export declare function aventaraAccess<const C extends AccessClaimsDeclaration, R = unknown, Gov extends GovernedShape = Record<never, never>, M = AccessIdentity<C>, const E extends boolean = false, X extends AccessMcpOptions | undefined = undefined>(options: AventaraAccessOptions<C, R, Gov, M, E> & {
158
+ readonly mcp?: X;
159
+ }): AventaraAccessPlugin<C, Gov, AccessPrincipalOf<R, M>, E, [
160
+ X
161
+ ] extends [undefined] ? false : [X] extends [AccessMcpOptions] ? true : boolean>;
162
+ /** What `aventaraAccess()` was built with, when `plugin` is one; `undefined` for any other plugin. */
163
+ export declare const pluginKernelOf: (plugin: unknown) => PluginKernel | undefined;
164
+ export {};