@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,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 {};
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// `aventaraAccess()`: Access as a Better Auth plugin, for an app that builds Better
|
|
2
|
+
// Auth itself (`betterAuth({ plugins: [aventaraAccess({ policies })] })`). It is the
|
|
3
|
+
// other way in beside `createAccess`, and runs the same code: the `policies` block
|
|
4
|
+
// and `principal` checked when it is called; Better Auth's options checked when
|
|
5
|
+
// Better Auth initialises the plugin, as `createAccess` checks its `betterAuth` block
|
|
6
|
+
// (the fixed issuer, the session cookie's site, the claims' sources, which Resource
|
|
7
|
+
// is which of Better Auth's tables, no second `jwt()`); and tokens issued by Better
|
|
8
|
+
// Auth's own `jwt()`, which the plugin carries inside it, with `sub` and the declared
|
|
9
|
+
// claims only. `accessOf(auth)` then gives the framework its `access` option.
|
|
10
|
+
//
|
|
11
|
+
// Passwords are hashed with argon2id as under `createAccess` (the `passwords` block,
|
|
12
|
+
// or the app's own `emailAndPassword.password`, which passes through): the plugin
|
|
13
|
+
// puts the hook in Better Auth's context when Better Auth initialises it.
|
|
14
|
+
//
|
|
15
|
+
// The audit log is here as under `createAccess` (`audit: true` or `{ sink }`): sign-ins,
|
|
16
|
+
// sign-outs, refused sign-ins and tokens from Better Auth's hooks, and the decisions
|
|
17
|
+
// of `accessOf(auth).policy`.
|
|
18
|
+
//
|
|
19
|
+
// The registry is here too, as under `createAccess` (`registry: { enabled: true }`),
|
|
20
|
+
// which makes the app the hub other services push to and pull from: its tables are
|
|
21
|
+
// this plugin's schema, so Better Auth's `migrate`/`generate` create them through the
|
|
22
|
+
// app's own adapter, and its routes are this plugin's endpoints, so Better Auth serves
|
|
23
|
+
// them wherever the app mounts it. A service reaches them only with its namespace's
|
|
24
|
+
// key; the operator's API (namespaces, keys, the org's guardrails) is
|
|
25
|
+
// `accessOf(auth).registry`, in the app's own process, and no route opens it. What
|
|
26
|
+
// else `createAccess` adds beside Better Auth (profiles, its `handler`, the standalone
|
|
27
|
+
// host) is not here: the app owns its Better Auth instance and mounts it as it
|
|
28
|
+
// already does.
|
|
29
|
+
//
|
|
30
|
+
// `jwt()` is carried inside this plugin, under the Access plugin's id. A Better Auth
|
|
31
|
+
// plugin that looks `jwt()` up by its id (`ctx.getPlugin("jwt")`, as the OAuth
|
|
32
|
+
// provider does for its issuer and keys) finds the carried one: the plugin answers
|
|
33
|
+
// for that id in Better Auth's context (`getPlugin`, `hasPlugin`) when Better Auth
|
|
34
|
+
// starts it, so a plugin that reads it as it starts comes after this one in
|
|
35
|
+
// `plugins`.
|
|
36
|
+
//
|
|
37
|
+
// With `mcp` or `audience`, Access reads the tokens Better Auth's OAuth provider (in
|
|
38
|
+
// `plugins` after this plugin) issues, as under `createAccess`: PKCE is required of
|
|
39
|
+
// every authorization request, `accessOf(auth).mcp` is the framework's
|
|
40
|
+
// `mcp.authorization`, and with `audience` the service takes only bearer tokens
|
|
41
|
+
// addressed to it. Access issues no token and serves no OAuth route.
|
|
42
|
+
import { AccessError } from "../access/access.error.js";
|
|
43
|
+
import { ACCESS_PLUGIN_ID, cookieCacheOn, crossSiteSessionCookie, issuerOf, JWT_PLUGIN_ID, normalisedId, refuseInstalledPlugins, refuseUnverifiedInvitations, } from "../access/better-auth.hardening.js";
|
|
44
|
+
import { loadBetterAuth, } from "../access/better-auth.loader.js";
|
|
45
|
+
import { issuedClaims, } from "../access/claims.mapper.js";
|
|
46
|
+
import { betterAuthTablesFor, checkPoliciesBlock, } from "../access/policies-block.checker.js";
|
|
47
|
+
import { sessionStillLive } from "../access/request.state.js";
|
|
48
|
+
import { sessionWatch } from "../access/session.hooks.js";
|
|
49
|
+
import { auditDestinationOf, createAuditLog, } from "../audit/audit.log.js";
|
|
50
|
+
import { auditSchema } from "../audit/audit.schema.js";
|
|
51
|
+
import { authorizationServerOf, checkMcpOptions, } from "../mcp/mcp-options.js";
|
|
52
|
+
import { userInfoAudience } from "../oauth/access-token.verifier.js";
|
|
53
|
+
import { checkOAuthProvider, checkResourceUrl, oauthUseOf, pkceGuard, } from "../oauth/oauth-provider.hardening.js";
|
|
54
|
+
import { checkPasswordsOptions, passwordHookOf, passwordRehashHook, } from "../passwords/passwords.options.js";
|
|
55
|
+
import { isRecord } from "../policies.compiler.js";
|
|
56
|
+
import { createRegistry, } from "../registry/access.registry.js";
|
|
57
|
+
import { registryEndpoints, registryOnRequest, } from "../registry/registry.endpoints.js";
|
|
58
|
+
import { registryOptionsOf, } from "../registry/registry.options.js";
|
|
59
|
+
import { registrySchema } from "../registry/registry.schema.js";
|
|
60
|
+
import { checkResourcesOptions, } from "../resources/auth-tables.guard.js";
|
|
61
|
+
import { bridgedClaims, checkBridge, checkBridgePlugins, } from "./organization.bridge.js";
|
|
62
|
+
/**
|
|
63
|
+
* What each plugin `aventaraAccess()` built was built with, by plugin: a map this
|
|
64
|
+
* module alone holds, so no object but one it returned passes for the plugin.
|
|
65
|
+
*/
|
|
66
|
+
const KERNELS = new WeakMap();
|
|
67
|
+
const TOP_LEVEL = new Set([
|
|
68
|
+
"policies",
|
|
69
|
+
"principal",
|
|
70
|
+
"resources",
|
|
71
|
+
"organization",
|
|
72
|
+
"audit",
|
|
73
|
+
"passwords",
|
|
74
|
+
"registry",
|
|
75
|
+
"mcp",
|
|
76
|
+
"audience",
|
|
77
|
+
]);
|
|
78
|
+
/** The plugins `aventaraAccess()` carries itself, by id: a second one in `plugins` would answer for its routes. */
|
|
79
|
+
const CARRIED = { [JWT_PLUGIN_ID]: "jwt()" };
|
|
80
|
+
/**
|
|
81
|
+
* The Access plugin for Better Auth: `betterAuth({ plugins: [aventaraAccess({ policies })] })`.
|
|
82
|
+
* It carries Better Auth's `jwt()` (whose tokens carry `sub` and the declared claims,
|
|
83
|
+
* under the app's base URL as issuer and audience), so `jwt()` is not added beside
|
|
84
|
+
* it. `accessOf(auth)` is the framework's `access` option. `better-auth` is loaded
|
|
85
|
+
* here, and must be in Access's range, `~1.7.12` (1.7.12 or a later 1.7 patch).
|
|
86
|
+
*/
|
|
87
|
+
export function aventaraAccess(options) {
|
|
88
|
+
if (!isRecord(options)) {
|
|
89
|
+
throw new AccessError("ACCESS_OPTIONS_INVALID", "aventaraAccess takes one object: `{ policies, principal?, resources?, organization?, audit?, passwords?, registry?, mcp?, audience? }`.");
|
|
90
|
+
}
|
|
91
|
+
for (const key of Object.keys(options)) {
|
|
92
|
+
if (!TOP_LEVEL.has(key)) {
|
|
93
|
+
throw new AccessError("ACCESS_OPTIONS_INVALID", `aventaraAccess does not take \`${key}\`; it takes \`policies\`, \`principal\`, \`resources\`, \`organization\`, \`audit\`, \`passwords\`, \`registry\`, \`mcp\` and \`audience\`. Better Auth's own options go to betterAuth({...}) beside it.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const given = options;
|
|
97
|
+
if (given.policies === undefined) {
|
|
98
|
+
throw new AccessError("ACCESS_OPTIONS_INVALID", "the `policies` option is missing: pass `{ rules, claims }`, the rules (`definePolicies(model)…`) and the claims they read.");
|
|
99
|
+
}
|
|
100
|
+
const checked = checkPoliciesBlock(given.policies, given.principal);
|
|
101
|
+
const resources = checkResourcesOptions(given.resources);
|
|
102
|
+
const bridge = checkBridge(given.organization, checked.claims);
|
|
103
|
+
const passwords = checkPasswordsOptions(given.passwords);
|
|
104
|
+
const registrySettings = registryOptionsOf(given.registry);
|
|
105
|
+
// The block's shape now; its resource against the issuer when Better Auth starts.
|
|
106
|
+
const mcp = checkMcpOptions(given.mcp, "mcp", "");
|
|
107
|
+
const oauthOn = oauthUseOf(mcp, given.audience === undefined
|
|
108
|
+
? undefined
|
|
109
|
+
: checkResourceUrl(given.audience, "audience", ""), undefined);
|
|
110
|
+
const modules = loadBetterAuth();
|
|
111
|
+
// The bridge owns its two claims: neither the user's field of the same name nor
|
|
112
|
+
// `claimsOf` fills them.
|
|
113
|
+
const bridged = bridge === undefined ? [] : [bridge.claim, bridge.organizationClaim];
|
|
114
|
+
const ownClaims = {
|
|
115
|
+
...checked.claims,
|
|
116
|
+
};
|
|
117
|
+
for (const claim of bridged) {
|
|
118
|
+
delete ownClaims[claim];
|
|
119
|
+
}
|
|
120
|
+
const claimsOf = checked.claimsOf === undefined || bridge === undefined
|
|
121
|
+
? checked.claimsOf
|
|
122
|
+
: async (source) => {
|
|
123
|
+
const answer = await checked.claimsOf(source);
|
|
124
|
+
if (!isRecord(answer)) {
|
|
125
|
+
return answer;
|
|
126
|
+
}
|
|
127
|
+
const kept = { ...answer };
|
|
128
|
+
for (const claim of bridged) {
|
|
129
|
+
delete kept[claim];
|
|
130
|
+
}
|
|
131
|
+
return kept;
|
|
132
|
+
};
|
|
133
|
+
const claimsFor = async (context, source) => {
|
|
134
|
+
const claims = await issuedClaims(source, bridge === undefined ? checked.claims : ownClaims, claimsOf);
|
|
135
|
+
if (bridge === undefined) {
|
|
136
|
+
return claims;
|
|
137
|
+
}
|
|
138
|
+
const fromBridge = await bridgedClaims(bridge, (await context()), source);
|
|
139
|
+
return fromBridge === undefined ? claims : { ...claims, ...fromBridge };
|
|
140
|
+
};
|
|
141
|
+
const start = (block, own) => {
|
|
142
|
+
const plugins = refuseInstalledPlugins(block.plugins, {
|
|
143
|
+
by: "aventaraAccess()",
|
|
144
|
+
at: "plugins",
|
|
145
|
+
installs: CARRIED,
|
|
146
|
+
own,
|
|
147
|
+
registry: registrySettings !== undefined,
|
|
148
|
+
});
|
|
149
|
+
for (const plugin of plugins) {
|
|
150
|
+
const id = plugin?.id;
|
|
151
|
+
if (plugin !== own && normalisedId(id) === ACCESS_PLUGIN_ID) {
|
|
152
|
+
throw new AccessError("ACCESS_PLUGIN_DUPLICATE", `plugins holds a second Access plugin (plugin id "${String(id)}") beside aventaraAccess(): Better Auth runs one. Remove it.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (resources.members) {
|
|
156
|
+
refuseUnverifiedInvitations(plugins, "`resources: { members: true }`");
|
|
157
|
+
}
|
|
158
|
+
if (bridge !== undefined) {
|
|
159
|
+
checkBridgePlugins(plugins, bridge);
|
|
160
|
+
}
|
|
161
|
+
const issuer = issuerOf(block, modules, "baseURL");
|
|
162
|
+
const checkedMcp = checkMcpOptions(given.mcp, "mcp", issuer);
|
|
163
|
+
const audience = given.audience === undefined
|
|
164
|
+
? undefined
|
|
165
|
+
: checkResourceUrl(given.audience, "audience", issuer);
|
|
166
|
+
const userInfo = userInfoAudience(block, modules);
|
|
167
|
+
const use = oauthUseOf(checkedMcp, audience, userInfo);
|
|
168
|
+
if (use !== undefined) {
|
|
169
|
+
checkOAuthProvider(plugins, use, { at: "plugins", own });
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
...(checkedMcp === undefined
|
|
173
|
+
? {}
|
|
174
|
+
: {
|
|
175
|
+
mcp: {
|
|
176
|
+
resource: checkedMcp.resource,
|
|
177
|
+
server: authorizationServerOf(issuer, "baseURL"),
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
...(audience === undefined ? {} : { audience }),
|
|
181
|
+
userInfo,
|
|
182
|
+
issuer,
|
|
183
|
+
crossSite: crossSiteSessionCookie(block, "trustedOrigins"),
|
|
184
|
+
facts: betterAuthTablesFor({
|
|
185
|
+
options: block,
|
|
186
|
+
modules,
|
|
187
|
+
checked,
|
|
188
|
+
resources,
|
|
189
|
+
bridged,
|
|
190
|
+
}),
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
// Fixed when Better Auth initialises the plugin, from its options: the tokens'
|
|
194
|
+
// issuer and audience, and the context the claims, the session check and the
|
|
195
|
+
// audit log read. `jwt()` hands its payload function the session alone, never
|
|
196
|
+
// Better Auth's context, so these are the plugin's, not each context's: a plugin
|
|
197
|
+
// started again (Better Auth's migrations build another context from the same
|
|
198
|
+
// options) must find every one of them as it was, or it is refused.
|
|
199
|
+
let first;
|
|
200
|
+
const contextOf = async () => first?.context;
|
|
201
|
+
const auditTo = auditDestinationOf(given.audit);
|
|
202
|
+
const auditLog = auditTo === undefined
|
|
203
|
+
? undefined
|
|
204
|
+
: createAuditLog(auditTo, async () => (await contextOf()));
|
|
205
|
+
const watch = sessionWatch({
|
|
206
|
+
kind: "web",
|
|
207
|
+
auditLog,
|
|
208
|
+
modules,
|
|
209
|
+
plugins: () => first?.plugins ?? [],
|
|
210
|
+
});
|
|
211
|
+
const carried = modules.jwt({
|
|
212
|
+
jwt: {
|
|
213
|
+
get issuer() {
|
|
214
|
+
return first?.issuer;
|
|
215
|
+
},
|
|
216
|
+
get audience() {
|
|
217
|
+
return first?.issuer;
|
|
218
|
+
},
|
|
219
|
+
expirationTime: `${checked.tokenTtl}s`,
|
|
220
|
+
definePayload: async (source) => {
|
|
221
|
+
if (first?.cookieCache === true) {
|
|
222
|
+
await sessionStillLive(contextOf, source.session, () => new modules.APIError("UNAUTHORIZED", {
|
|
223
|
+
message: "The session has ended.",
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
return claimsFor(contextOf, source);
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
// The registry's routes store through the adapter of the Better Auth that started
|
|
231
|
+
// the plugin: every instance it is in has the same database (`init` refuses one
|
|
232
|
+
// that does not), and no route runs before Better Auth has started it.
|
|
233
|
+
const registryStore = registrySettings === undefined
|
|
234
|
+
? undefined
|
|
235
|
+
: createRegistry(async () => {
|
|
236
|
+
const context = first?.context;
|
|
237
|
+
if (context?.adapter === undefined) {
|
|
238
|
+
throw new AccessError("ACCESS_OPTIONS_INVALID", "the registry was asked before Better Auth started aventaraAccess(): build Better Auth with it in its plugins first.");
|
|
239
|
+
}
|
|
240
|
+
return context.adapter;
|
|
241
|
+
}, registrySettings);
|
|
242
|
+
const registryRoutes = registryStore === undefined
|
|
243
|
+
? undefined
|
|
244
|
+
: registryEndpoints(registryStore, modules.createAuthEndpoint);
|
|
245
|
+
const carriedHooks = carried.hooks;
|
|
246
|
+
const plugin = {
|
|
247
|
+
id: ACCESS_PLUGIN_ID,
|
|
248
|
+
version: carried.version,
|
|
249
|
+
init(ctx) {
|
|
250
|
+
const { options: block } = ctx;
|
|
251
|
+
const started = start(block, plugin);
|
|
252
|
+
const now = {
|
|
253
|
+
issuer: started.issuer,
|
|
254
|
+
crossSite: started.crossSite,
|
|
255
|
+
cookieCache: cookieCacheOn(block),
|
|
256
|
+
database: block.database,
|
|
257
|
+
secondaryStorage: block.secondaryStorage,
|
|
258
|
+
};
|
|
259
|
+
if (first === undefined) {
|
|
260
|
+
first = {
|
|
261
|
+
...now,
|
|
262
|
+
context: ctx,
|
|
263
|
+
plugins: Array.isArray(block.plugins) ? block.plugins : [],
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
else if (now.issuer !== first.issuer ||
|
|
267
|
+
now.crossSite !== first.crossSite ||
|
|
268
|
+
now.cookieCache !== first.cookieCache ||
|
|
269
|
+
now.database !== first.database ||
|
|
270
|
+
now.secondaryStorage !== first.secondaryStorage) {
|
|
271
|
+
throw new AccessError("ACCESS_PLUGIN_DUPLICATE", "one aventaraAccess() plugin is in two Better Auth instances whose options differ (database, secondary storage, base URL, session cookie or cookie cache): call aventaraAccess() for each betterAuth({...}).");
|
|
272
|
+
}
|
|
273
|
+
// Better Auth's password hook, argon2id unless the app set its own: put in
|
|
274
|
+
// Better Auth's context, which Better Auth built from its options before
|
|
275
|
+
// initialising its plugins.
|
|
276
|
+
const hook = passwordHookOf(passwords, block.emailAndPassword, modules, contextOf, "emailAndPassword");
|
|
277
|
+
const watched = watch.init?.();
|
|
278
|
+
// The carried `jwt()`, found by its own id in Better Auth's context, as a plugin
|
|
279
|
+
// beside it would be: the OAuth provider reads its issuer and keys there.
|
|
280
|
+
const { getPlugin, hasPlugin } = ctx;
|
|
281
|
+
const jwtLookup = {
|
|
282
|
+
getPlugin: (id) => id === JWT_PLUGIN_ID ? carried : getPlugin(id),
|
|
283
|
+
hasPlugin: (id) => id === JWT_PLUGIN_ID || hasPlugin(id),
|
|
284
|
+
};
|
|
285
|
+
const { password } = ctx;
|
|
286
|
+
return {
|
|
287
|
+
...watched,
|
|
288
|
+
context: {
|
|
289
|
+
...watched?.context,
|
|
290
|
+
...jwtLookup,
|
|
291
|
+
...(hook === undefined
|
|
292
|
+
? {}
|
|
293
|
+
: {
|
|
294
|
+
password: {
|
|
295
|
+
...password,
|
|
296
|
+
hash: hook.hash,
|
|
297
|
+
verify: hook.verify,
|
|
298
|
+
},
|
|
299
|
+
}),
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
endpoints: registryRoutes === undefined
|
|
304
|
+
? carried.endpoints
|
|
305
|
+
: { ...carried.endpoints, ...registryRoutes },
|
|
306
|
+
// A push's key is checked before Better Auth reads its body, on any mount.
|
|
307
|
+
...(registryStore === undefined
|
|
308
|
+
? {}
|
|
309
|
+
: { onRequest: registryOnRequest(registryStore) }),
|
|
310
|
+
hooks: {
|
|
311
|
+
before: [
|
|
312
|
+
...(carriedHooks.before ?? []),
|
|
313
|
+
...(oauthOn === undefined ? [] : [pkceGuard(modules)]),
|
|
314
|
+
...(watch.hooks?.before ?? []),
|
|
315
|
+
],
|
|
316
|
+
after: [
|
|
317
|
+
...(carriedHooks.after ?? []),
|
|
318
|
+
...(watch.hooks?.after ?? []),
|
|
319
|
+
// Inert unless Better Auth's password hook is Access's (the app's own passes through).
|
|
320
|
+
passwordRehashHook(modules),
|
|
321
|
+
],
|
|
322
|
+
},
|
|
323
|
+
schema: {
|
|
324
|
+
...carried.schema,
|
|
325
|
+
...(registrySettings === undefined ? {} : registrySchema),
|
|
326
|
+
...(auditTo?.kind === "table" ? auditSchema : {}),
|
|
327
|
+
},
|
|
328
|
+
$Infer: {},
|
|
329
|
+
};
|
|
330
|
+
const kernel = {
|
|
331
|
+
checked,
|
|
332
|
+
resources,
|
|
333
|
+
bridge,
|
|
334
|
+
modules,
|
|
335
|
+
auditLog,
|
|
336
|
+
registry: registrySettings,
|
|
337
|
+
start,
|
|
338
|
+
claimsFor,
|
|
339
|
+
};
|
|
340
|
+
KERNELS.set(plugin, kernel);
|
|
341
|
+
// The declared return type states it; a cast restating it costs the type-checker
|
|
342
|
+
// the whole plugin type again.
|
|
343
|
+
return plugin;
|
|
344
|
+
}
|
|
345
|
+
/** What `aventaraAccess()` was built with, when `plugin` is one; `undefined` for any other plugin. */
|
|
346
|
+
export const pluginKernelOf = (plugin) => typeof plugin === "object" && plugin !== null
|
|
347
|
+
? KERNELS.get(plugin)
|
|
348
|
+
: undefined;
|