@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.
- package/CHANGELOG.md +877 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +2536 -2
- package/dist/access/access-options.d.ts +74 -0
- package/dist/access/access-options.js +5 -0
- package/dist/access/access.error.d.ts +35 -0
- package/dist/access/access.error.js +11 -0
- package/dist/access/access.factory.d.ts +275 -0
- package/dist/access/access.factory.js +440 -0
- package/dist/access/better-auth.hardening.d.ts +120 -0
- package/dist/access/better-auth.hardening.js +420 -0
- package/dist/access/better-auth.loader.d.ts +67 -0
- package/dist/access/better-auth.loader.js +65 -0
- package/dist/access/better-auth.range.d.ts +9 -0
- package/dist/access/better-auth.range.js +28 -0
- package/dist/access/better-auth.require.d.ts +90 -0
- package/dist/access/better-auth.require.js +6 -0
- package/dist/access/better-auth.require.node.d.ts +7 -0
- package/dist/access/better-auth.require.node.js +52 -0
- package/dist/access/claims.mapper.d.ts +79 -0
- package/dist/access/claims.mapper.js +152 -0
- package/dist/access/key-set.cache.d.ts +10 -0
- package/dist/access/key-set.cache.js +63 -0
- package/dist/access/policies-block.checker.d.ts +64 -0
- package/dist/access/policies-block.checker.js +138 -0
- package/dist/access/principal.subject.d.ts +22 -0
- package/dist/access/principal.subject.js +89 -0
- package/dist/access/profile.policy.d.ts +74 -0
- package/dist/access/profile.policy.js +110 -0
- package/dist/access/request.state.d.ts +89 -0
- package/dist/access/request.state.js +266 -0
- package/dist/access/session.hooks.d.ts +114 -0
- package/dist/access/session.hooks.js +258 -0
- package/dist/access/token.verifier.d.ts +35 -0
- package/dist/access/token.verifier.js +69 -0
- package/dist/access-schema.d.ts +48 -0
- package/dist/access-schema.js +5 -0
- package/dist/access-warning.emitter.d.ts +4 -0
- package/dist/access-warning.emitter.js +24 -0
- package/dist/access.builder.d.ts +340 -0
- package/dist/access.builder.js +75 -0
- package/dist/access.engine.d.ts +155 -0
- package/dist/access.engine.js +477 -0
- package/dist/audit/audit-event.d.ts +135 -0
- package/dist/audit/audit-event.js +8 -0
- package/dist/audit/audit.log.d.ts +122 -0
- package/dist/audit/audit.log.js +345 -0
- package/dist/audit/audit.schema.d.ts +143 -0
- package/dist/audit/audit.schema.js +50 -0
- package/dist/auth/index.d.ts +12 -0
- package/dist/auth/index.js +2 -0
- package/dist/aventara-where.translator.d.ts +19 -0
- package/dist/aventara-where.translator.js +87 -0
- package/dist/beacon/beacon.check.d.ts +59 -0
- package/dist/beacon/beacon.check.js +128 -0
- package/dist/beacon/beacon.schema.d.ts +41 -0
- package/dist/beacon/beacon.schema.js +48 -0
- package/dist/beacon/beacon.store.d.ts +29 -0
- package/dist/beacon/beacon.store.js +78 -0
- package/dist/better-auth/access.client-plugin.d.ts +75 -0
- package/dist/better-auth/access.client-plugin.js +133 -0
- package/dist/better-auth/access.plugin.d.ts +164 -0
- package/dist/better-auth/access.plugin.js +348 -0
- package/dist/better-auth/access.provider.d.ts +80 -0
- package/dist/better-auth/access.provider.js +100 -0
- package/dist/better-auth/index.d.ts +7 -0
- package/dist/better-auth/index.js +4 -0
- package/dist/better-auth/organization.bridge.d.ts +66 -0
- package/dist/better-auth/organization.bridge.js +144 -0
- package/dist/canonical-json.writer.d.ts +12 -0
- package/dist/canonical-json.writer.js +30 -0
- package/dist/cli/avaccess.bin.d.ts +2 -0
- package/dist/cli/avaccess.bin.js +26 -0
- package/dist/cli/avaccess.program.d.ts +15 -0
- package/dist/cli/avaccess.program.js +166 -0
- package/dist/cli/beacon-pins.file.d.ts +14 -0
- package/dist/cli/beacon-pins.file.js +62 -0
- package/dist/cli/beacon.loader.d.ts +20 -0
- package/dist/cli/beacon.loader.js +71 -0
- package/dist/cli/cli.context.d.ts +49 -0
- package/dist/cli/cli.context.js +66 -0
- package/dist/cli/diff.command.d.ts +2 -0
- package/dist/cli/diff.command.js +66 -0
- package/dist/cli/generate.command.d.ts +2 -0
- package/dist/cli/generate.command.js +82 -0
- package/dist/cli/keys.command.d.ts +2 -0
- package/dist/cli/keys.command.js +96 -0
- package/dist/cli/lock.file.d.ts +31 -0
- package/dist/cli/lock.file.js +181 -0
- package/dist/cli/module.loader.d.ts +13 -0
- package/dist/cli/module.loader.js +63 -0
- package/dist/cli/policies.diff.d.ts +16 -0
- package/dist/cli/policies.diff.js +101 -0
- package/dist/cli/pull.command.d.ts +2 -0
- package/dist/cli/pull.command.js +234 -0
- package/dist/cli/push.command.d.ts +2 -0
- package/dist/cli/push.command.js +59 -0
- package/dist/cli/registry.client.d.ts +63 -0
- package/dist/cli/registry.client.js +287 -0
- package/dist/cli/schema.command.d.ts +2 -0
- package/dist/cli/schema.command.js +60 -0
- package/dist/cli/serve.command.d.ts +6 -0
- package/dist/cli/serve.command.js +200 -0
- package/dist/cli/status.command.d.ts +2 -0
- package/dist/cli/status.command.js +31 -0
- package/dist/cli/translator.flag.d.ts +6 -0
- package/dist/cli/translator.flag.js +24 -0
- package/dist/cli/verify.command.d.ts +2 -0
- package/dist/cli/verify.command.js +40 -0
- package/dist/client/access-client.factory.d.ts +92 -0
- package/dist/client/access-client.factory.js +269 -0
- package/dist/client/access-verbs.schema.d.ts +66 -0
- package/dist/client/access-verbs.schema.js +159 -0
- package/dist/client/verb-rate.limiter.d.ts +116 -0
- package/dist/client/verb-rate.limiter.js +358 -0
- package/dist/foreign/foreign-facts.d.ts +30 -0
- package/dist/foreign/foreign-facts.js +75 -0
- package/dist/foreign/foreign.merger.d.ts +10 -0
- package/dist/foreign/foreign.merger.js +52 -0
- package/dist/foreign-references.check.d.ts +6 -0
- package/dist/foreign-references.check.js +131 -0
- package/dist/generator/client.emitter.d.ts +28 -0
- package/dist/generator/client.emitter.js +411 -0
- package/dist/generator/copied-modules.reader.d.ts +14 -0
- package/dist/generator/copied-modules.reader.js +80 -0
- package/dist/generator/namespace.facts.d.ts +28 -0
- package/dist/generator/namespace.facts.js +32 -0
- package/dist/generator/orm.translator.d.ts +48 -0
- package/dist/generator/orm.translator.js +29 -0
- package/dist/generator/prisma7/prisma-schema.reader.d.ts +9 -0
- package/dist/generator/prisma7/prisma-schema.reader.js +394 -0
- package/dist/generator/prisma7/prisma-where.translator.d.ts +73 -0
- package/dist/generator/prisma7/prisma-where.translator.js +138 -0
- package/dist/generator/prisma7/prisma7.translator.d.ts +2 -0
- package/dist/generator/prisma7/prisma7.translator.js +87 -0
- package/dist/generator/schema.emitter.d.ts +14 -0
- package/dist/generator/schema.emitter.js +23 -0
- package/dist/generator/tsconfig.reader.d.ts +44 -0
- package/dist/generator/tsconfig.reader.js +110 -0
- package/dist/generator/typescript-literal.writer.d.ts +5 -0
- package/dist/generator/typescript-literal.writer.js +42 -0
- package/dist/guardrails/guardrails-contract.d.ts +45 -0
- package/dist/guardrails/guardrails-contract.js +12 -0
- package/dist/guardrails/guardrails.builder.d.ts +48 -0
- package/dist/guardrails/guardrails.builder.js +32 -0
- package/dist/guardrails/guardrails.compiler.d.ts +10 -0
- package/dist/guardrails/guardrails.compiler.js +254 -0
- package/dist/guardrails/guardrails.composer.d.ts +7 -0
- package/dist/guardrails/guardrails.composer.js +95 -0
- package/dist/guardrails/guardrails.hash.d.ts +10 -0
- package/dist/guardrails/guardrails.hash.js +38 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +6 -0
- package/dist/kiosk/kiosk-verbs.schema.d.ts +19 -0
- package/dist/kiosk/kiosk-verbs.schema.js +61 -0
- package/dist/kiosk/kiosk.plugin.d.ts +217 -0
- package/dist/kiosk/kiosk.plugin.js +877 -0
- package/dist/kiosk/kiosk.schema.d.ts +176 -0
- package/dist/kiosk/kiosk.schema.js +101 -0
- package/dist/kiosk/kiosk.store.d.ts +171 -0
- package/dist/kiosk/kiosk.store.js +478 -0
- package/dist/kiosk/lan-range.policy.d.ts +31 -0
- package/dist/kiosk/lan-range.policy.js +113 -0
- package/dist/kiosk/pin.policy.d.ts +9 -0
- package/dist/kiosk/pin.policy.js +143 -0
- package/dist/kiosk/session-use.marker.d.ts +4 -0
- package/dist/kiosk/session-use.marker.js +16 -0
- package/dist/lock/lock-contract.d.ts +38 -0
- package/dist/lock/lock-contract.js +67 -0
- package/dist/lock/lock.verifier.d.ts +27 -0
- package/dist/lock/lock.verifier.js +62 -0
- package/dist/mcp/agent-scope.gate.d.ts +13 -0
- package/dist/mcp/agent-scope.gate.js +75 -0
- package/dist/mcp/agent.grant.d.ts +40 -0
- package/dist/mcp/agent.grant.js +92 -0
- package/dist/mcp/mcp-options.d.ts +60 -0
- package/dist/mcp/mcp-options.js +72 -0
- package/dist/oauth/access-token.verifier.d.ts +66 -0
- package/dist/oauth/access-token.verifier.js +201 -0
- package/dist/oauth/oauth-provider.hardening.d.ts +62 -0
- package/dist/oauth/oauth-provider.hardening.js +261 -0
- package/dist/passwords/argon2id.hasher.d.ts +97 -0
- package/dist/passwords/argon2id.hasher.js +256 -0
- package/dist/passwords/node-rs-argon2.adapter.d.ts +25 -0
- package/dist/passwords/node-rs-argon2.adapter.js +42 -0
- package/dist/passwords/passwords.options.d.ts +79 -0
- package/dist/passwords/passwords.options.js +212 -0
- package/dist/policies-contract.d.ts +219 -0
- package/dist/policies-contract.js +8 -0
- package/dist/policies.compiler.d.ts +38 -0
- package/dist/policies.compiler.js +1433 -0
- package/dist/policies.decider.d.ts +103 -0
- package/dist/policies.decider.js +390 -0
- package/dist/policies.hash.d.ts +10 -0
- package/dist/policies.hash.js +54 -0
- package/dist/predicate.evaluator.d.ts +62 -0
- package/dist/predicate.evaluator.js +378 -0
- package/dist/random-secret.generator.d.ts +2 -0
- package/dist/random-secret.generator.js +15 -0
- package/dist/read-governed-unique.check.d.ts +3 -0
- package/dist/read-governed-unique.check.js +21 -0
- package/dist/reference-reads.check.d.ts +16 -0
- package/dist/reference-reads.check.js +158 -0
- package/dist/registry/access.registry.d.ts +173 -0
- package/dist/registry/access.registry.js +865 -0
- package/dist/registry/beacon.ledger.d.ts +30 -0
- package/dist/registry/beacon.ledger.js +93 -0
- package/dist/registry/ownership.check.d.ts +54 -0
- package/dist/registry/ownership.check.js +221 -0
- package/dist/registry/registry.endpoints.d.ts +50 -0
- package/dist/registry/registry.endpoints.js +193 -0
- package/dist/registry/registry.key.d.ts +7 -0
- package/dist/registry/registry.key.js +13 -0
- package/dist/registry/registry.options.d.ts +13 -0
- package/dist/registry/registry.options.js +28 -0
- package/dist/registry/registry.refusal.d.ts +6 -0
- package/dist/registry/registry.refusal.js +11 -0
- package/dist/registry/registry.schema.d.ts +238 -0
- package/dist/registry/registry.schema.js +127 -0
- package/dist/resources/auth-tables.catalog.d.ts +96 -0
- package/dist/resources/auth-tables.catalog.js +110 -0
- package/dist/resources/auth-tables.guard.d.ts +106 -0
- package/dist/resources/auth-tables.guard.js +462 -0
- package/dist/scope.resolver.d.ts +144 -0
- package/dist/scope.resolver.js +529 -0
- package/dist/sha256.digest.d.ts +2 -0
- package/dist/sha256.digest.js +81 -0
- package/dist/standalone/access.server.d.ts +41 -0
- package/dist/standalone/access.server.js +301 -0
- package/dist/standalone/index.d.ts +1 -0
- package/dist/standalone/index.js +1 -0
- package/package.json +102 -3
- package/skill/SKILL.md +170 -0
- package/skill/reference/callers.md +99 -0
- package/skill/reference/errors.md +89 -0
- package/skill/reference/recipes.md +281 -0
- package/skill/reference/rules.md +153 -0
- package/skill/reference/setup.md +334 -0
- package/src/aventara-where.translator.ts +116 -0
- package/src/beacon/beacon.check.ts +204 -0
- package/src/canonical-json.writer.ts +51 -0
- package/src/generator/prisma7/prisma-where.translator.ts +243 -0
- package/src/policies-contract.ts +215 -0
- package/src/policies.decider.ts +662 -0
- package/src/predicate.evaluator.ts +574 -0
- package/src/scope.resolver.ts +819 -0
- 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 {};
|