@aventara/access 0.0.0-stage → 0.1.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +831 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +2439 -2
- package/dist/access/access-options.d.ts +66 -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 +273 -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 +12 -0
- package/dist/access/principal.subject.js +60 -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 +196 -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 +2 -0
- package/dist/access-warning.emitter.js +20 -0
- package/dist/access.builder.d.ts +299 -0
- package/dist/access.builder.js +66 -0
- package/dist/access.engine.d.ts +142 -0
- package/dist/access.engine.js +470 -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 +78 -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 +83 -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 +12 -0
- package/dist/foreign-references.check.js +134 -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 +72 -0
- package/dist/generator/prisma7/prisma-where.translator.js +136 -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 +11 -0
- package/dist/mcp/agent-scope.gate.js +75 -0
- package/dist/mcp/agent.grant.d.ts +36 -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 +192 -0
- package/dist/policies-contract.js +8 -0
- package/dist/policies.compiler.d.ts +36 -0
- package/dist/policies.compiler.js +1358 -0
- package/dist/policies.decider.d.ts +103 -0
- package/dist/policies.decider.js +382 -0
- package/dist/policies.hash.d.ts +10 -0
- package/dist/policies.hash.js +53 -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/reference-reads.check.d.ts +12 -0
- package/dist/reference-reads.check.js +148 -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 +215 -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 +91 -0
- package/dist/resources/auth-tables.catalog.js +102 -0
- package/dist/resources/auth-tables.guard.d.ts +95 -0
- package/dist/resources/auth-tables.guard.js +405 -0
- package/dist/scope.resolver.d.ts +138 -0
- package/dist/scope.resolver.js +512 -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 +101 -3
- 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 +241 -0
- package/src/policies-contract.ts +184 -0
- package/src/policies.decider.ts +650 -0
- package/src/predicate.evaluator.ts +574 -0
- package/src/scope.resolver.ts +791 -0
- package/src/sha256.digest.ts +85 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { AccessEngine } from "../access.engine.js";
|
|
2
|
+
import type { AccessMcpAuthorization } from "../mcp/mcp-options.js";
|
|
3
|
+
import { type AccessRegistry } from "../registry/access.registry.js";
|
|
4
|
+
type GovernedShape = {
|
|
5
|
+
readonly [resource: string]: readonly string[];
|
|
6
|
+
};
|
|
7
|
+
/** The headers of a request, as the framework hands them to `state`. */
|
|
8
|
+
type RequestHeaders = {
|
|
9
|
+
get(name: string): string | null;
|
|
10
|
+
has(name: string): boolean;
|
|
11
|
+
forEach(callback: (value: string, name: string) => void): void;
|
|
12
|
+
};
|
|
13
|
+
/** What `accessOf` reads of a Better Auth instance: its options, its API and its context. */
|
|
14
|
+
export type AccessAuth = {
|
|
15
|
+
readonly options: {
|
|
16
|
+
readonly plugins?: readonly unknown[] | undefined;
|
|
17
|
+
};
|
|
18
|
+
readonly api: unknown;
|
|
19
|
+
readonly $context: Promise<unknown>;
|
|
20
|
+
};
|
|
21
|
+
/** The `aventaraAccess()` plugin among an instance's plugins, by type. */
|
|
22
|
+
type AccessPluginOf<A extends AccessAuth> = Extract<NonNullable<A["options"]["plugins"]>[number], {
|
|
23
|
+
readonly id: "aventara-access";
|
|
24
|
+
}>;
|
|
25
|
+
/** `accessOf(auth).registry`: the operator's API when the plugin serves the registry, absent when it does not. */
|
|
26
|
+
type AccessProviderRegistry<E extends boolean> = [E] extends [true] ? {
|
|
27
|
+
/** The registry's operator API: namespaces, keys and the org's guardrails. */
|
|
28
|
+
readonly registry: AccessRegistry;
|
|
29
|
+
} : [E] extends [false] ? {
|
|
30
|
+
readonly registry?: undefined;
|
|
31
|
+
} : {
|
|
32
|
+
readonly registry?: AccessRegistry | undefined;
|
|
33
|
+
};
|
|
34
|
+
/** What `accessOf(auth)` returns: the framework's `access` option, and its types. */
|
|
35
|
+
export type AccessProvider<Claims, Gov extends GovernedShape, M, E extends boolean = false, P extends boolean = false> = {
|
|
36
|
+
/** The policy engine, asked about every client and agent request. */
|
|
37
|
+
readonly policy: AccessEngine<Gov>;
|
|
38
|
+
/** A request's state: `me`, from its bearer token or its session cookie, else `null`. */
|
|
39
|
+
state(input: {
|
|
40
|
+
readonly headers: RequestHeaders;
|
|
41
|
+
readonly request?: unknown;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
readonly me: M | null;
|
|
44
|
+
}>;
|
|
45
|
+
/** Types only, as Better Auth's `$Infer`: `typeof access.$Infer.Me` is the principal. */
|
|
46
|
+
readonly $Infer: {
|
|
47
|
+
readonly Me: M;
|
|
48
|
+
readonly Claims: Claims;
|
|
49
|
+
};
|
|
50
|
+
} & AccessProviderRegistry<E> & AccessProviderMcp<Claims, P>;
|
|
51
|
+
/** `accessOf(auth).mcp`: the framework's `mcp.authorization` when the plugin has `mcp`, absent when it has not. */
|
|
52
|
+
type AccessProviderMcp<Claims, P extends boolean> = [P] extends [true] ? {
|
|
53
|
+
/** The framework's `mcp.authorization`: `mcp: { path: "/mcp", authorization: access.mcp }`. */
|
|
54
|
+
readonly mcp: AccessMcpAuthorization<Claims>;
|
|
55
|
+
} : [P] extends [false] ? {
|
|
56
|
+
readonly mcp?: undefined;
|
|
57
|
+
} : {
|
|
58
|
+
readonly mcp?: AccessMcpAuthorization<Claims> | undefined;
|
|
59
|
+
};
|
|
60
|
+
/** `accessOf`'s answer for an instance, typed from its `aventaraAccess()` plugin's `$Infer`. */
|
|
61
|
+
export type AccessProviderOf<A extends AccessAuth> = AccessPluginOf<A> extends {
|
|
62
|
+
readonly $Infer: infer I extends {
|
|
63
|
+
readonly AccessMe: unknown;
|
|
64
|
+
readonly AccessClaims: unknown;
|
|
65
|
+
readonly AccessGoverned: GovernedShape;
|
|
66
|
+
readonly AccessRegistryEnabled: boolean;
|
|
67
|
+
readonly AccessMcp: boolean;
|
|
68
|
+
};
|
|
69
|
+
} ? AccessProvider<I["AccessClaims"], I["AccessGoverned"], I["AccessMe"], I["AccessRegistryEnabled"], I["AccessMcp"]> : AccessProvider<{
|
|
70
|
+
readonly [claim: string]: string | number | boolean;
|
|
71
|
+
}, GovernedShape, unknown, boolean, boolean>;
|
|
72
|
+
/**
|
|
73
|
+
* The framework's `access` option from Better Auth built with `aventaraAccess()` in
|
|
74
|
+
* its plugins: `createFramework({ ...config, access: accessOf(auth) })`. Refuses an
|
|
75
|
+
* instance without the plugin, and the configurations the plugin refuses.
|
|
76
|
+
*/
|
|
77
|
+
export declare function accessOf<A extends AccessAuth>(given: A): AccessProviderOf<A>;
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// `accessOf(auth)`: the framework's `access` option from a Better Auth instance built
|
|
2
|
+
// with `aventaraAccess()`. It satisfies the framework's access provider by shape
|
|
3
|
+
// (`policy`, `state`) without importing it, as `createAccess`'s instance does, and
|
|
4
|
+
// is built from the same modules: the engine with Access's built-in rules over
|
|
5
|
+
// Better Auth's tables, and the request state (a bearer token against the key set,
|
|
6
|
+
// or the session cookie from a trusted site). It checks `auth.options` as the
|
|
7
|
+
// plugin's `init` does, so a configuration Access refuses fails the framework's
|
|
8
|
+
// construction, not the first request. When the plugin serves the registry (the app
|
|
9
|
+
// is the hub), `registry` is the operator's API, as `createAccess`'s instance has it:
|
|
10
|
+
// it stores through the instance's own adapter, in this process, behind no route.
|
|
11
|
+
// With the plugin's `mcp`, it gives the framework's `mcp.authorization` too
|
|
12
|
+
// (`accessOf(auth).mcp`), and its state reads an agent's access token as
|
|
13
|
+
// `createAccess`'s does; with the plugin's `audience`, it takes only bearer tokens
|
|
14
|
+
// addressed to the service.
|
|
15
|
+
import { AccessError } from "../access/access.error.js";
|
|
16
|
+
import { guardedPolicy } from "../access/policies-block.checker.js";
|
|
17
|
+
import { requestIdentity, requestState } from "../access/request.state.js";
|
|
18
|
+
import { createTokenVerifier } from "../access/token.verifier.js";
|
|
19
|
+
import { createAccessTokenVerifier, mcpAuthorizationOf, } from "../oauth/access-token.verifier.js";
|
|
20
|
+
import { createRegistry, } from "../registry/access.registry.js";
|
|
21
|
+
import { pluginKernelOf } from "./access.plugin.js";
|
|
22
|
+
/**
|
|
23
|
+
* The framework's `access` option from Better Auth built with `aventaraAccess()` in
|
|
24
|
+
* its plugins: `createFramework({ ...config, access: accessOf(auth) })`. Refuses an
|
|
25
|
+
* instance without the plugin, and the configurations the plugin refuses.
|
|
26
|
+
*/
|
|
27
|
+
export function accessOf(given) {
|
|
28
|
+
const auth = given;
|
|
29
|
+
const plugins = Array.isArray(auth?.options?.plugins)
|
|
30
|
+
? auth.options.plugins
|
|
31
|
+
: [];
|
|
32
|
+
let own;
|
|
33
|
+
for (const plugin of plugins) {
|
|
34
|
+
if (own === undefined && pluginKernelOf(plugin) !== undefined) {
|
|
35
|
+
own = plugin;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const kernel = pluginKernelOf(own);
|
|
39
|
+
if (kernel === undefined) {
|
|
40
|
+
throw new AccessError("ACCESS_OPTIONS_INVALID", "accessOf takes a Better Auth instance built with aventaraAccess() in its plugins: `betterAuth({ plugins: [aventaraAccess({ policies })] })`.");
|
|
41
|
+
}
|
|
42
|
+
const { checked, resources } = kernel;
|
|
43
|
+
const start = kernel.start(auth.options, own);
|
|
44
|
+
let guarded;
|
|
45
|
+
const keys = async () => (await auth.api.getJwks());
|
|
46
|
+
const tokensFor = (resource) => createAccessTokenVerifier({
|
|
47
|
+
keys,
|
|
48
|
+
issuer: start.issuer,
|
|
49
|
+
resource,
|
|
50
|
+
userInfo: start.userInfo,
|
|
51
|
+
context: () => auth.$context,
|
|
52
|
+
claimsFor: (session) => kernel.claimsFor(() => auth.$context, session),
|
|
53
|
+
});
|
|
54
|
+
const agents = start.mcp === undefined ? undefined : tokensFor(start.mcp.resource);
|
|
55
|
+
const ownTokens = start.audience === undefined ? undefined : tokensFor(start.audience);
|
|
56
|
+
const provider = {
|
|
57
|
+
$Infer: {},
|
|
58
|
+
...(agents === undefined || start.mcp === undefined
|
|
59
|
+
? {}
|
|
60
|
+
: { mcp: mcpAuthorizationOf(agents, start.mcp.server) }),
|
|
61
|
+
state: requestState(requestIdentity({
|
|
62
|
+
auth: () => auth,
|
|
63
|
+
verifier: createTokenVerifier({
|
|
64
|
+
claims: checked.claims,
|
|
65
|
+
keys,
|
|
66
|
+
issuer: start.issuer,
|
|
67
|
+
tokenTtl: checked.tokenTtl,
|
|
68
|
+
}),
|
|
69
|
+
...(agents === undefined ? {} : { agents }),
|
|
70
|
+
...(ownTokens === undefined ? {} : { audience: ownTokens }),
|
|
71
|
+
kind: "web",
|
|
72
|
+
crossSite: start.crossSite,
|
|
73
|
+
claimsFor: (session) => kernel.claimsFor(() => auth.$context, session),
|
|
74
|
+
}), checked.principal),
|
|
75
|
+
...(kernel.registry === undefined
|
|
76
|
+
? {}
|
|
77
|
+
: {
|
|
78
|
+
registry: Object.freeze({
|
|
79
|
+
...createRegistry(async () => (await auth.$context).adapter, kernel.registry).operator,
|
|
80
|
+
}),
|
|
81
|
+
}),
|
|
82
|
+
};
|
|
83
|
+
// Built on first read, which is the framework's construction: a `resources` key
|
|
84
|
+
// whose tables are missing refuses the app's start. Not enumerable, so a walk
|
|
85
|
+
// over the provider runs no startup check.
|
|
86
|
+
Object.defineProperty(provider, "policy", {
|
|
87
|
+
get: () => {
|
|
88
|
+
guarded ??= guardedPolicy({
|
|
89
|
+
checked,
|
|
90
|
+
auditLog: kernel.auditLog,
|
|
91
|
+
resources,
|
|
92
|
+
tables: start.facts.tables,
|
|
93
|
+
naming: start.facts.naming,
|
|
94
|
+
});
|
|
95
|
+
return guarded;
|
|
96
|
+
},
|
|
97
|
+
enumerable: false,
|
|
98
|
+
});
|
|
99
|
+
return provider;
|
|
100
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { AccessIdentity } from "../access/claims.mapper.js";
|
|
2
|
+
export { type AccessAgent, agentOf } from "../mcp/agent.grant.js";
|
|
3
|
+
export type { AccessAgentIdentity, AccessMcpAuthorization, AccessMcpOptions, } from "../mcp/mcp-options.js";
|
|
4
|
+
export { type AccessCanError, type AccessCanResult, type AccessQuestion, type AventaraAccessClientOptions, type AventaraAccessClientPlugin, aventaraAccessClient, } from "./access.client-plugin.js";
|
|
5
|
+
export { type AventaraAccessOptions, type AventaraAccessPlugin, aventaraAccess, } from "./access.plugin.js";
|
|
6
|
+
export { type AccessAuth, type AccessProvider, type AccessProviderOf, accessOf, } from "./access.provider.js";
|
|
7
|
+
export type { AccessOrganizationBridge } from "./organization.bridge.js";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { AccessClaimsDeclaration, AccessClaimsSource } from "../access/claims.mapper.js";
|
|
2
|
+
/** The claims of a declaration that are strings, by name: those the bridge can fill. */
|
|
3
|
+
type StringClaimOf<C extends AccessClaimsDeclaration> = {
|
|
4
|
+
[K in keyof C]: C[K] extends "string" ? K : never;
|
|
5
|
+
}[keyof C] & string;
|
|
6
|
+
/**
|
|
7
|
+
* The `organization` option: which claims a member's role fills, and with what. A
|
|
8
|
+
* member whose roles `roles` does not name (or no member, or no active organization)
|
|
9
|
+
* gets neither claim.
|
|
10
|
+
*/
|
|
11
|
+
export type AccessOrganizationBridge<C extends AccessClaimsDeclaration = AccessClaimsDeclaration> = {
|
|
12
|
+
/** The claim the role's mapped value fills: one `policies.claims` declares as `"string"`. */
|
|
13
|
+
readonly claim: StringClaimOf<C>;
|
|
14
|
+
/**
|
|
15
|
+
* The claim the organization's id fills, beside the role: one `policies.claims`
|
|
16
|
+
* declares as `"string"`. Each grant on the bridged role is held to it.
|
|
17
|
+
*/
|
|
18
|
+
readonly organizationClaim: StringClaimOf<C>;
|
|
19
|
+
/**
|
|
20
|
+
* Better Auth organization roles (`owner`, `admin`, `member`, or the app's own
|
|
21
|
+
* static roles), in order, each with the value the claim takes: a member with
|
|
22
|
+
* several roles gets the value of the first one listed here.
|
|
23
|
+
*/
|
|
24
|
+
readonly roles: readonly (readonly [role: string, value: string])[];
|
|
25
|
+
};
|
|
26
|
+
/** The bridge, checked: its two claims and its roles in order. */
|
|
27
|
+
export type CheckedBridge = {
|
|
28
|
+
readonly claim: string;
|
|
29
|
+
readonly organizationClaim: string;
|
|
30
|
+
readonly roles: readonly (readonly [role: string, value: string])[];
|
|
31
|
+
};
|
|
32
|
+
/** The `organization` option, checked against the declared claims; `undefined` when it is not given. */
|
|
33
|
+
export declare const checkBridge: (value: unknown, claims: AccessClaimsDeclaration) => CheckedBridge | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Refuses a bridge over Better Auth without its organization plugin, and over one
|
|
36
|
+
* that lets a member be made otherwise than the app decided: by an unverified email
|
|
37
|
+
* accepting an invitation, by any user creating an organization, or (with dynamic
|
|
38
|
+
* access control) under a role an organization's admins name.
|
|
39
|
+
*/
|
|
40
|
+
export declare const checkBridgePlugins: (plugins: readonly unknown[], bridge: CheckedBridge) => void;
|
|
41
|
+
/** What the bridge reads of Better Auth's context: its database adapter. */
|
|
42
|
+
export type BridgeContext = {
|
|
43
|
+
readonly adapter: {
|
|
44
|
+
findOne(input: {
|
|
45
|
+
readonly model: string;
|
|
46
|
+
readonly where: readonly {
|
|
47
|
+
readonly field: string;
|
|
48
|
+
readonly value: string;
|
|
49
|
+
}[];
|
|
50
|
+
}): Promise<{
|
|
51
|
+
readonly [field: string]: unknown;
|
|
52
|
+
} | null>;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The bridged claims for a session: the mapped value of the member's role in the
|
|
57
|
+
* session's active organization, and that organization's id, read from Better Auth's
|
|
58
|
+
* store; `undefined` when there is no active organization, no membership, or no role
|
|
59
|
+
* the bridge maps. The member's roles are read as Better Auth reads them, split on
|
|
60
|
+
* `,` and taken as they are: a stored role with whitespace in it (`member, owner`) is
|
|
61
|
+
* one Better Auth does not grant, and gives nothing here.
|
|
62
|
+
*/
|
|
63
|
+
export declare const bridgedClaims: (bridge: CheckedBridge, context: BridgeContext, source: AccessClaimsSource) => Promise<{
|
|
64
|
+
readonly [claim: string]: string;
|
|
65
|
+
} | undefined>;
|
|
66
|
+
export {};
|
|
@@ -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,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 {};
|