@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,138 @@
|
|
|
1
|
+
// The `policies` block and the `principal` option, checked: one check for
|
|
2
|
+
// `createAccess({ policies, principal })` and `aventaraAccess({ policies, principal })`,
|
|
3
|
+
// run when each is called. It builds the engine once from the rules, the guardrails
|
|
4
|
+
// and the lock, so a file the engine would refuse is refused here, and holds the
|
|
5
|
+
// claims the rules and the guardrails read to `policies.claims`.
|
|
6
|
+
import { engineOf, } from "../access.engine.js";
|
|
7
|
+
import { decisionObserver } from "../audit/audit.log.js";
|
|
8
|
+
import { agentGated } from "../mcp/agent-scope.gate.js";
|
|
9
|
+
import { isRecord } from "../policies.compiler.js";
|
|
10
|
+
import { authTablesOf, } from "../resources/auth-tables.catalog.js";
|
|
11
|
+
import { authTableNaming, guardAuthTables, } from "../resources/auth-tables.guard.js";
|
|
12
|
+
import { AccessError } from "./access.error.js";
|
|
13
|
+
import { prismaAdapterOf, } from "./better-auth.hardening.js";
|
|
14
|
+
import { checkClaimSources, checkClaimsDeclaration, checkRulesClaims, } from "./claims.mapper.js";
|
|
15
|
+
import { subjectOf } from "./principal.subject.js";
|
|
16
|
+
/** The shortest and longest a token may live, in seconds, and the default. */
|
|
17
|
+
const TOKEN_TTL = { min: 300, max: 900, default: 900 };
|
|
18
|
+
const POLICIES_KEYS = new Set([
|
|
19
|
+
"rules",
|
|
20
|
+
"claims",
|
|
21
|
+
"claimsOf",
|
|
22
|
+
"guardrails",
|
|
23
|
+
"lock",
|
|
24
|
+
"requireLock",
|
|
25
|
+
"tokenTtl",
|
|
26
|
+
]);
|
|
27
|
+
const refuse = (message) => {
|
|
28
|
+
throw new AccessError("ACCESS_OPTIONS_INVALID", message);
|
|
29
|
+
};
|
|
30
|
+
const isBuilder = (rules) => isRecord(rules) && typeof rules.contract === "function";
|
|
31
|
+
/** Refuses a `policies` block (`given`) and a `principal` that Access does not take. */
|
|
32
|
+
export const checkPoliciesBlock = (given, principal) => {
|
|
33
|
+
if (!isRecord(given)) {
|
|
34
|
+
return refuse("`policies` is `{ rules, claims, claimsOf?, guardrails?, lock?, requireLock?, tokenTtl? }`.");
|
|
35
|
+
}
|
|
36
|
+
for (const key of Object.keys(given)) {
|
|
37
|
+
if (!POLICIES_KEYS.has(key)) {
|
|
38
|
+
refuse(`\`policies\` does not take \`${key}\`; it takes \`rules\`, \`claims\`, \`claimsOf\`, \`guardrails\`, \`lock\`, \`requireLock\` and \`tokenTtl\`.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const claims = checkClaimsDeclaration(given.claims);
|
|
42
|
+
const { rules, claimsOf, guardrails, lock, requireLock, tokenTtl = TOKEN_TTL.default, } = given;
|
|
43
|
+
if (typeof tokenTtl !== "number" ||
|
|
44
|
+
!Number.isInteger(tokenTtl) ||
|
|
45
|
+
tokenTtl < TOKEN_TTL.min ||
|
|
46
|
+
tokenTtl > TOKEN_TTL.max) {
|
|
47
|
+
refuse(`\`policies.tokenTtl\` is how long a token lives, in whole seconds from ${TOKEN_TTL.min} to ${TOKEN_TTL.max} (5 to 15 minutes); it was ${JSON.stringify(tokenTtl)}.`);
|
|
48
|
+
}
|
|
49
|
+
if (guardrails !== undefined && !isRecord(guardrails)) {
|
|
50
|
+
refuse("`policies.guardrails` is the org's guardrails file, as pulled from the registry (`aventara-access/guardrails@1`).");
|
|
51
|
+
}
|
|
52
|
+
if (claimsOf !== undefined && typeof claimsOf !== "function") {
|
|
53
|
+
refuse("`policies.claimsOf` is a function: `({ user, session }) => ({ … })`.");
|
|
54
|
+
}
|
|
55
|
+
const file = isBuilder(rules) ? rules.contract() : rules;
|
|
56
|
+
if (!isRecord(file)) {
|
|
57
|
+
return refuse("`policies.rules` is the rules, `definePolicies(model)…`, or their compiled file, `.contract()`.");
|
|
58
|
+
}
|
|
59
|
+
if (requireLock !== undefined && typeof requireLock !== "boolean") {
|
|
60
|
+
refuse(`\`policies.requireLock\` is \`true\` (refuse to start without \`lock\`) or \`false\`; it was ${JSON.stringify(requireLock)}.`);
|
|
61
|
+
}
|
|
62
|
+
const engineOptions = guardrails === undefined && lock === undefined && requireLock === undefined
|
|
63
|
+
? undefined
|
|
64
|
+
: {
|
|
65
|
+
...(guardrails === undefined
|
|
66
|
+
? {}
|
|
67
|
+
: { guardrails: guardrails }),
|
|
68
|
+
...(lock === undefined ? {} : { lock: lock }),
|
|
69
|
+
...(requireLock === undefined
|
|
70
|
+
? {}
|
|
71
|
+
: { requireLock: requireLock }),
|
|
72
|
+
};
|
|
73
|
+
const source = isBuilder(rules) ? "rules" : "file";
|
|
74
|
+
// The rules, the guardrails and the lock are checked here; the engine the
|
|
75
|
+
// instance answers with is built on first read, with the built-in rules over
|
|
76
|
+
// Better Auth's tables added (`guardedPolicy`).
|
|
77
|
+
engineOf(file, engineOptions, source);
|
|
78
|
+
checkRulesClaims(file, claims);
|
|
79
|
+
if (guardrails !== undefined) {
|
|
80
|
+
checkRulesClaims(guardrails, claims, "guardrails");
|
|
81
|
+
}
|
|
82
|
+
if (principal !== undefined && typeof principal !== "function") {
|
|
83
|
+
refuse("`principal` is a function: `({ sub, claims }) => me`.");
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
claims,
|
|
87
|
+
file: file,
|
|
88
|
+
source,
|
|
89
|
+
engineOptions,
|
|
90
|
+
claimsOf: claimsOf,
|
|
91
|
+
tokenTtl: tokenTtl,
|
|
92
|
+
principal: principal,
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* The engine the framework asks, with Access's built-in rules over Better Auth's
|
|
97
|
+
* tables (as Better Auth is built: its plugins' tables and Access's own included),
|
|
98
|
+
* named as the app's schema names them, the audit log's observer when it is on, and
|
|
99
|
+
* the agent gate before it all (`mcp/agent-scope.gate.ts`): a principal an agent's
|
|
100
|
+
* access token yielded is held to that token's audience and scopes first.
|
|
101
|
+
*/
|
|
102
|
+
export const guardedPolicy = (input) => {
|
|
103
|
+
const { checked, auditLog, resources, tables, naming } = input;
|
|
104
|
+
const observe = auditLog === undefined ? undefined : decisionObserver(auditLog, subjectOf);
|
|
105
|
+
return agentGated(engineOf(checked.file, checked.engineOptions, checked.source, observe, guardAuthTables({ resources, tables, naming, policies: checked.file })), observe);
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Better Auth's tables for the options it is built with (`options`, its plugins and
|
|
109
|
+
* Access's own included), checked against the claims and the rules: a declared
|
|
110
|
+
* claim the default mapping (no `claimsOf`) reads must come from a user field the
|
|
111
|
+
* user cannot set (`bridged`, the claims the organization bridge fills, are not read
|
|
112
|
+
* from the user); and the rules' schema names each table, on the Prisma adapter by
|
|
113
|
+
* the models it reads, on any other by `resources.authTables`.
|
|
114
|
+
*/
|
|
115
|
+
export const betterAuthTablesFor = (input) => {
|
|
116
|
+
const { options, modules, checked, resources, bridged } = input;
|
|
117
|
+
const betterAuthTables = modules.getAuthTables(options);
|
|
118
|
+
if (checked.claimsOf === undefined) {
|
|
119
|
+
const read = {
|
|
120
|
+
...checked.claims,
|
|
121
|
+
};
|
|
122
|
+
for (const claim of bridged ?? []) {
|
|
123
|
+
delete read[claim];
|
|
124
|
+
}
|
|
125
|
+
checkClaimSources(read, betterAuthTables.user?.fields);
|
|
126
|
+
}
|
|
127
|
+
const schema = checked.file.schema;
|
|
128
|
+
const schemaEmpty = Object.keys(schema).length === 0;
|
|
129
|
+
const naming = authTableNaming({
|
|
130
|
+
prisma: schemaEmpty
|
|
131
|
+
? undefined
|
|
132
|
+
: prismaAdapterOf(options.database, options),
|
|
133
|
+
authTables: resources.authTables,
|
|
134
|
+
models: Object.keys(betterAuthTables),
|
|
135
|
+
schemaEmpty,
|
|
136
|
+
});
|
|
137
|
+
return { tables: authTablesOf(betterAuthTables, schema, naming), naming };
|
|
138
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Marks `me` as derived from `sub`: a non-enumerable property, so a spread,
|
|
3
|
+
* `JSON.stringify` or `Object.keys` of the principal does not show it.
|
|
4
|
+
*
|
|
5
|
+
* Refuses an object that already stands for another `sub`: one object handed out
|
|
6
|
+
* for two users (a principal function that returns a shared object) would carry
|
|
7
|
+
* whichever user came last, and the audit would name the wrong user for the other
|
|
8
|
+
* one's decisions.
|
|
9
|
+
*/
|
|
10
|
+
export declare const carrySubject: (me: unknown, sub: string) => void;
|
|
11
|
+
/** The `sub` of a principal Access derived; `null` for any other principal. */
|
|
12
|
+
export declare const subjectOf: (principal: unknown) => string | null;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// The `sub` a principal came from, carried on the principal itself: a decision
|
|
2
|
+
// event's `userId`. The framework hands the policy engine `ctx.$state.me` and
|
|
3
|
+
// nothing else, and a host may ask one Access instance for the state and another
|
|
4
|
+
// for the policy: Next evaluates the app's `access.ts` once per bundle, and
|
|
5
|
+
// `@aventara/next` shares the framework (and so one bundle's `access.policy`)
|
|
6
|
+
// through `globalThis`. So the `sub` travels with `me`, under a key every copy of
|
|
7
|
+
// Access names the same (`Symbol.for`), never in a map one instance keeps.
|
|
8
|
+
import { AccessError } from "./access.error.js";
|
|
9
|
+
/** The key the `sub` is carried under: the same in every bundle and every copy of Access. */
|
|
10
|
+
const SUBJECT = Symbol.for("@aventara/access:sub");
|
|
11
|
+
/** The key of the one map, per process, holding the `sub` of a principal that takes no property (a frozen one). */
|
|
12
|
+
const FROZEN = Symbol.for("@aventara/access:frozen-subjects");
|
|
13
|
+
const frozenSubjects = () => {
|
|
14
|
+
const holder = globalThis;
|
|
15
|
+
holder[FROZEN] ??= new WeakMap();
|
|
16
|
+
return holder[FROZEN];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Marks `me` as derived from `sub`: a non-enumerable property, so a spread,
|
|
20
|
+
* `JSON.stringify` or `Object.keys` of the principal does not show it.
|
|
21
|
+
*
|
|
22
|
+
* Refuses an object that already stands for another `sub`: one object handed out
|
|
23
|
+
* for two users (a principal function that returns a shared object) would carry
|
|
24
|
+
* whichever user came last, and the audit would name the wrong user for the other
|
|
25
|
+
* one's decisions.
|
|
26
|
+
*/
|
|
27
|
+
export const carrySubject = (me, sub) => {
|
|
28
|
+
if (typeof me !== "object" || me === null) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const carried = subjectOf(me);
|
|
32
|
+
if (carried !== null && carried !== sub) {
|
|
33
|
+
throw new AccessError("ACCESS_OPTIONS_INVALID", "`principal` returned an object it already returned for another user: the audit names the user each principal stands for, and one object cannot stand for two. Return a new object for each user (one cached per user is fine).");
|
|
34
|
+
}
|
|
35
|
+
if (Object.isExtensible(me)) {
|
|
36
|
+
try {
|
|
37
|
+
Object.defineProperty(me, SUBJECT, {
|
|
38
|
+
value: sub,
|
|
39
|
+
enumerable: false,
|
|
40
|
+
writable: false,
|
|
41
|
+
configurable: true,
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// A proxy that refuses the property: kept in the process's map below.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
frozenSubjects().set(me, sub);
|
|
50
|
+
};
|
|
51
|
+
/** The `sub` of a principal Access derived; `null` for any other principal. */
|
|
52
|
+
export const subjectOf = (principal) => {
|
|
53
|
+
if (typeof principal !== "object" || principal === null) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const carried = Reflect.get(principal, SUBJECT);
|
|
57
|
+
return typeof carried === "string"
|
|
58
|
+
? carried
|
|
59
|
+
: (frozenSubjects().get(principal) ?? null);
|
|
60
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { type LanRange } from "../kiosk/lan-range.policy.js";
|
|
2
|
+
import type { AccessClaimsDeclaration, AccessIdentity } from "./claims.mapper.js";
|
|
3
|
+
/** The profiles `createAccess` takes. */
|
|
4
|
+
export type AccessProfileKind = "web" | "service" | "kiosk";
|
|
5
|
+
/**
|
|
6
|
+
* The `kiosk` profile: `web`, plus staff switching by PIN on enrolled devices. A
|
|
7
|
+
* PIN session is bound to the device it was opened on and locks when idle.
|
|
8
|
+
*/
|
|
9
|
+
export type AccessKioskProfile<C extends AccessClaimsDeclaration> = {
|
|
10
|
+
readonly kind: "kiosk";
|
|
11
|
+
/**
|
|
12
|
+
* Who may enroll a device, or revoke one: asked with the identity of a fresh
|
|
13
|
+
* session opened with full credentials (a password, never a PIN). Without a `true`
|
|
14
|
+
* the call is refused.
|
|
15
|
+
*/
|
|
16
|
+
readonly canEnrollDevice: (identity: AccessIdentity<C>) => boolean | Promise<boolean>;
|
|
17
|
+
/**
|
|
18
|
+
* Seconds without a request that may change something (any method but GET, HEAD
|
|
19
|
+
* or OPTIONS; a page that only reads does not count) after which any session
|
|
20
|
+
* bound to an enrolled device locks, a PIN session or one opened there with a
|
|
21
|
+
* password: from 60 to 3600, 300 by default.
|
|
22
|
+
*/
|
|
23
|
+
readonly idleTimeout?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Opt-in for an on-prem till network without TLS: the private IPv4 ranges (CIDRs,
|
|
26
|
+
* a /16 or narrower, inside 10.0.0.0/8, 172.16.0.0/12 or 192.168.0.0/16) on whose
|
|
27
|
+
* addresses the kiosk takes a plain http: base URL, such as
|
|
28
|
+
* `"http://192.168.1.10:3000"`. The device's credential, the PIN sessions and the
|
|
29
|
+
* PINs then cross that network in the clear, so it must be the tills' alone (a
|
|
30
|
+
* router of their own, WPA3, no guests). Without it, http: is taken on a loopback
|
|
31
|
+
* host only.
|
|
32
|
+
*/
|
|
33
|
+
readonly insecureLan?: readonly [
|
|
34
|
+
`${number}.${number}.${number}.${number}/${number}`,
|
|
35
|
+
...`${number}.${number}.${number}.${number}/${number}`[]
|
|
36
|
+
];
|
|
37
|
+
};
|
|
38
|
+
/** The `profile` block: `{ kind: "web" }` (the default), `{ kind: "service" }` or the kiosk profile. */
|
|
39
|
+
export type AccessProfileOptions<C extends AccessClaimsDeclaration> = {
|
|
40
|
+
readonly kind: "web";
|
|
41
|
+
} | {
|
|
42
|
+
readonly kind: "service";
|
|
43
|
+
} | AccessKioskProfile<C>;
|
|
44
|
+
/** The checked kiosk profile. */
|
|
45
|
+
export type KioskSettings = {
|
|
46
|
+
readonly canEnrollDevice: (identity: {
|
|
47
|
+
readonly sub: string;
|
|
48
|
+
readonly claims: {
|
|
49
|
+
readonly [claim: string]: unknown;
|
|
50
|
+
};
|
|
51
|
+
}) => unknown;
|
|
52
|
+
/** In seconds. */
|
|
53
|
+
readonly idleTimeout: number;
|
|
54
|
+
/** `profile.insecureLan`, read: the ranges an http: base URL may be on. */
|
|
55
|
+
readonly insecureLan?: readonly LanRange[];
|
|
56
|
+
};
|
|
57
|
+
/** The checked `profile` block: its kind, and the kiosk's settings when it is the kiosk. */
|
|
58
|
+
type CheckedProfile = {
|
|
59
|
+
readonly kind: AccessProfileKind;
|
|
60
|
+
readonly kiosk?: KioskSettings;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* The checked `profile` block: `web` when none is given. One kind only: the kiosk's
|
|
64
|
+
* keys on another kind, or two kinds at once (`kind: ["kiosk", "service"]`), are
|
|
65
|
+
* refused, since a service hands no caller a session and a kiosk is made of them.
|
|
66
|
+
*/
|
|
67
|
+
export declare const checkProfile: (profile: unknown) => CheckedProfile;
|
|
68
|
+
/** Whether the Better Auth endpoint at `path` (its declared path, as `ctx.path` holds it) starts a session. */
|
|
69
|
+
export declare const startsSession: (path: unknown) => boolean;
|
|
70
|
+
/** Whether a profile serves the endpoint at `path` to a request from outside. */
|
|
71
|
+
export declare const servesRoute: (kind: AccessProfileKind, path: unknown) => boolean;
|
|
72
|
+
/** Whether a profile reads a request's principal from its session cookie. */
|
|
73
|
+
export declare const readsSessionCookie: (kind: AccessProfileKind) => boolean;
|
|
74
|
+
export {};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Profiles: what shape of app an Access instance serves. `web` is Better Auth as
|
|
2
|
+
// configured: cookie sign-in, sessions, tokens. `service` serves callers that bring
|
|
3
|
+
// a token: it hands no caller a new session, and a request's principal comes from
|
|
4
|
+
// its bearer token alone. `kiosk` is `web` plus staff switching by PIN on enrolled
|
|
5
|
+
// devices (`kiosk/kiosk.endpoints.ts`): a PIN session is bound to its device and
|
|
6
|
+
// locks when idle.
|
|
7
|
+
//
|
|
8
|
+
// The service profile holds that two ways, both on Better Auth's own terms rather
|
|
9
|
+
// than on the request's URL (whose spelling, base path and base URL vary): a route
|
|
10
|
+
// that starts a session is refused before it runs, by its endpoint path; and any
|
|
11
|
+
// request that creates a session anyway (a plugin route Access does not know) is
|
|
12
|
+
// refused by that effect, the session revoked before the answer leaves.
|
|
13
|
+
import { checkInsecureLan } from "../kiosk/lan-range.policy.js";
|
|
14
|
+
import { AccessError } from "./access.error.js";
|
|
15
|
+
/** The idle timeout's bounds and default, in seconds. */
|
|
16
|
+
const KIOSK_IDLE_TIMEOUT = { min: 60, max: 3600, default: 300 };
|
|
17
|
+
const KINDS = ["web", "service", "kiosk"];
|
|
18
|
+
/** The keys each kind takes beside `kind`. */
|
|
19
|
+
const KIND_KEYS = {
|
|
20
|
+
web: [],
|
|
21
|
+
service: [],
|
|
22
|
+
kiosk: ["canEnrollDevice", "idleTimeout", "insecureLan"],
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The Better Auth routes (better-auth 1.7.12 to 1.7.14 and its plugins) that start a session
|
|
26
|
+
* for the caller, by endpoint path, relative to Better Auth's base path: every
|
|
27
|
+
* `/sign-in/*` and `/sign-up/*` route, the OAuth callbacks that finish a social or
|
|
28
|
+
* generic OAuth sign-in, and the plugins' routes that sign a user in by a code, a
|
|
29
|
+
* link, a token, a device, a second factor or an impersonation.
|
|
30
|
+
*/
|
|
31
|
+
const SESSION_STARTING = /^\/(?:sign-in|sign-up)(?:\/|$)|^\/(?:oauth2\/)?callback(?:\/|$)|^\/oauth-proxy-callback$/;
|
|
32
|
+
const SESSION_STARTING_ROUTES = new Set([
|
|
33
|
+
"/verify-email",
|
|
34
|
+
"/magic-link/verify",
|
|
35
|
+
"/email-otp/verify-email",
|
|
36
|
+
"/phone-number/verify",
|
|
37
|
+
"/one-tap/callback",
|
|
38
|
+
"/one-time-token/verify",
|
|
39
|
+
"/siwe/verify",
|
|
40
|
+
"/passkey/verify-authentication",
|
|
41
|
+
"/two-factor/verify-totp",
|
|
42
|
+
"/two-factor/verify-otp",
|
|
43
|
+
"/two-factor/verify-backup-code",
|
|
44
|
+
"/device/token",
|
|
45
|
+
"/multi-session/set-active",
|
|
46
|
+
"/admin/impersonate-user",
|
|
47
|
+
"/admin/stop-impersonating",
|
|
48
|
+
// The kiosk profile's PIN switch (Access's own).
|
|
49
|
+
"/kiosk/switch-user",
|
|
50
|
+
]);
|
|
51
|
+
const SHAPE = '`profile` is `{ kind: "web" }` (the default), `{ kind: "service" }` or `{ kind: "kiosk", canEnrollDevice, idleTimeout?, insecureLan? }`';
|
|
52
|
+
/**
|
|
53
|
+
* The checked `profile` block: `web` when none is given. One kind only: the kiosk's
|
|
54
|
+
* keys on another kind, or two kinds at once (`kind: ["kiosk", "service"]`), are
|
|
55
|
+
* refused, since a service hands no caller a session and a kiosk is made of them.
|
|
56
|
+
*/
|
|
57
|
+
export const checkProfile = (profile) => {
|
|
58
|
+
if (profile === undefined) {
|
|
59
|
+
return { kind: "web" };
|
|
60
|
+
}
|
|
61
|
+
const kind = profile?.kind;
|
|
62
|
+
if (typeof profile !== "object" ||
|
|
63
|
+
profile === null ||
|
|
64
|
+
Array.isArray(profile) ||
|
|
65
|
+
!KINDS.includes(kind)) {
|
|
66
|
+
const both = Array.isArray(kind) && kind.includes("kiosk") && kind.includes("service");
|
|
67
|
+
throw new AccessError("ACCESS_OPTIONS_INVALID", both
|
|
68
|
+
? "`profile` names one kind: a `service` hands no caller a session, and a `kiosk` is made of them, so the two cannot be combined. Run the kiosk and the service as two instances."
|
|
69
|
+
: `${SHAPE}; it was ${JSON.stringify(profile)}.`);
|
|
70
|
+
}
|
|
71
|
+
const checked = kind;
|
|
72
|
+
for (const key of Object.keys(profile)) {
|
|
73
|
+
if (key !== "kind" && !KIND_KEYS[checked].includes(key)) {
|
|
74
|
+
throw new AccessError("ACCESS_OPTIONS_INVALID", KIND_KEYS.kiosk.includes(key)
|
|
75
|
+
? `\`profile.${key}\` belongs to the kiosk profile (\`{ kind: "kiosk" }\`), and this profile is \`${checked}\`: a ${checked === "service" ? "service hands no caller a session, so it has no PIN switch" : "web profile has no PIN switch"}.`
|
|
76
|
+
: `${SHAPE}; it does not take \`${key}\`.`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (checked !== "kiosk") {
|
|
80
|
+
return { kind: checked };
|
|
81
|
+
}
|
|
82
|
+
const { canEnrollDevice, idleTimeout, insecureLan } = profile;
|
|
83
|
+
if (typeof canEnrollDevice !== "function") {
|
|
84
|
+
throw new AccessError("ACCESS_OPTIONS_INVALID", "the kiosk profile needs `canEnrollDevice: (identity) => boolean`: who may enroll a device (and revoke one), asked with the identity of a fresh session opened with full credentials.");
|
|
85
|
+
}
|
|
86
|
+
if (idleTimeout !== undefined &&
|
|
87
|
+
(typeof idleTimeout !== "number" ||
|
|
88
|
+
!Number.isInteger(idleTimeout) ||
|
|
89
|
+
idleTimeout < KIOSK_IDLE_TIMEOUT.min ||
|
|
90
|
+
idleTimeout > KIOSK_IDLE_TIMEOUT.max)) {
|
|
91
|
+
throw new AccessError("ACCESS_OPTIONS_INVALID", `\`profile.idleTimeout\` is whole seconds from ${KIOSK_IDLE_TIMEOUT.min} to ${KIOSK_IDLE_TIMEOUT.max} (${KIOSK_IDLE_TIMEOUT.default} by default); it was ${JSON.stringify(idleTimeout)}.`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
kind: "kiosk",
|
|
95
|
+
kiosk: {
|
|
96
|
+
canEnrollDevice: canEnrollDevice,
|
|
97
|
+
idleTimeout: idleTimeout ?? KIOSK_IDLE_TIMEOUT.default,
|
|
98
|
+
...(insecureLan === undefined
|
|
99
|
+
? {}
|
|
100
|
+
: { insecureLan: checkInsecureLan(insecureLan) }),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
/** Whether the Better Auth endpoint at `path` (its declared path, as `ctx.path` holds it) starts a session. */
|
|
105
|
+
export const startsSession = (path) => typeof path === "string" &&
|
|
106
|
+
(SESSION_STARTING.test(path) || SESSION_STARTING_ROUTES.has(path));
|
|
107
|
+
/** Whether a profile serves the endpoint at `path` to a request from outside. */
|
|
108
|
+
export const servesRoute = (kind, path) => kind !== "service" || !startsSession(path);
|
|
109
|
+
/** Whether a profile reads a request's principal from its session cookie. */
|
|
110
|
+
export const readsSessionCookie = (kind) => kind !== "service";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { type CarriedGrant } from "../mcp/agent.grant.js";
|
|
2
|
+
import type { AccessTokenVerifier } from "../oauth/access-token.verifier.js";
|
|
3
|
+
import type { AccessClaimsSource } from "./claims.mapper.js";
|
|
4
|
+
import { type AccessProfileKind } from "./profile.policy.js";
|
|
5
|
+
import { type TokenVerifier } from "./token.verifier.js";
|
|
6
|
+
/** A verified identity, untyped: the `sub` and the claims, each checked against its declared type. */
|
|
7
|
+
type Identity = {
|
|
8
|
+
readonly sub: string;
|
|
9
|
+
readonly claims: {
|
|
10
|
+
readonly [claim: string]: string | number | boolean;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/** The headers of a request, as the framework hands them to `state`. */
|
|
14
|
+
type RequestHeaders = {
|
|
15
|
+
get(name: string): string | null;
|
|
16
|
+
has(name: string): boolean;
|
|
17
|
+
forEach(callback: (value: string, name: string) => void): void;
|
|
18
|
+
};
|
|
19
|
+
/** What the state reads of a built Better Auth instance. */
|
|
20
|
+
type StateAuth = {
|
|
21
|
+
readonly api: unknown;
|
|
22
|
+
readonly $context: Promise<unknown>;
|
|
23
|
+
};
|
|
24
|
+
/** Who a request's credential says the caller is, and, for an OAuth access token, what it grants. */
|
|
25
|
+
type RequestCaller = {
|
|
26
|
+
readonly identity: Identity;
|
|
27
|
+
readonly grant?: CarriedGrant;
|
|
28
|
+
};
|
|
29
|
+
/** Who a request's bearer token or session cookie says the caller is; `null` for nobody. */
|
|
30
|
+
type RequestIdentity = (headers: RequestHeaders, request: unknown) => Promise<RequestCaller | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Who Access's own `GET /me` route answers a request as: the identity of a session
|
|
33
|
+
* token, a session cookie or a token addressed to the service; `null` for nobody, and
|
|
34
|
+
* for an agent's access token, which opens nothing over HTTP but the MCP endpoint.
|
|
35
|
+
*/
|
|
36
|
+
export declare const meOf: (caller: RequestCaller | null) => Identity | null;
|
|
37
|
+
/**
|
|
38
|
+
* Who `access.client.verbs.me` answers a request as: the identity `{ sub, claims }`
|
|
39
|
+
* of any credential the state reads, an agent's included (the framework's MCP `me`
|
|
40
|
+
* tool calls it in-process with the agent's headers). A token's identity is a copy
|
|
41
|
+
* carrying what it grants, so `agentOf` tells an agent's apart.
|
|
42
|
+
*/
|
|
43
|
+
export declare const verbMeOf: (caller: RequestCaller | null) => Identity | null;
|
|
44
|
+
/** A request's state, as the framework's `access` option reads it. */
|
|
45
|
+
type RequestState<M> = (input: {
|
|
46
|
+
readonly headers: RequestHeaders;
|
|
47
|
+
readonly request?: unknown;
|
|
48
|
+
}) => Promise<{
|
|
49
|
+
readonly me: M | null;
|
|
50
|
+
}>;
|
|
51
|
+
/**
|
|
52
|
+
* A request's identity over one Better Auth instance (`auth`, built on first use):
|
|
53
|
+
* from a bearer token the verifier accepts (an agent's access token, the agent
|
|
54
|
+
* verifier, when `mcp` is on), or (profile `web`) from the session Better Auth's
|
|
55
|
+
* store holds for the request's cookie, with the claims `claimsFor` gives that
|
|
56
|
+
* session.
|
|
57
|
+
*/
|
|
58
|
+
export declare const requestIdentity: (input: {
|
|
59
|
+
readonly auth: () => StateAuth;
|
|
60
|
+
readonly verifier: TokenVerifier;
|
|
61
|
+
/** With `mcp`: the verifier of the access tokens agents are issued for the MCP endpoint. */
|
|
62
|
+
readonly agents?: AccessTokenVerifier;
|
|
63
|
+
/** With `audience`: the verifier of the access tokens addressed to this service, the only bearer tokens it then takes. */
|
|
64
|
+
readonly audience?: AccessTokenVerifier;
|
|
65
|
+
readonly kind: AccessProfileKind;
|
|
66
|
+
/** The session cookie is `sameSite: "none"`: read only from trusted sites, on every method. */
|
|
67
|
+
readonly crossSite: boolean;
|
|
68
|
+
/** A session's claims, as its token would be issued with them. */
|
|
69
|
+
readonly claimsFor: (session: AccessClaimsSource) => Promise<{
|
|
70
|
+
readonly [claim: string]: string | number | boolean;
|
|
71
|
+
}>;
|
|
72
|
+
}) => RequestIdentity;
|
|
73
|
+
/**
|
|
74
|
+
* The request state over a request's identity (`requestIdentity`): `me` is the
|
|
75
|
+
* identity, or what the app's `principal` maps it to, with the `sub` carried on it
|
|
76
|
+
* for the audit, and, for an OAuth access token, a copy of it carrying what the
|
|
77
|
+
* token grants. The identity `principal` is given for such a token carries the grant
|
|
78
|
+
* too, so `agentOf(identity)` tells the app an agent's request while it maps it.
|
|
79
|
+
*/
|
|
80
|
+
export declare const requestState: (identityOf: RequestIdentity, principal: ((identity: Identity) => unknown) | undefined) => RequestState<unknown>;
|
|
81
|
+
/**
|
|
82
|
+
* Refuses a token for a session the store no longer has (revoked, signed out,
|
|
83
|
+
* expired): with Better Auth's cookie cache on, a session read from its cookie may
|
|
84
|
+
* have been revoked since, so a token is issued only for one the store still has.
|
|
85
|
+
*/
|
|
86
|
+
export declare const sessionStillLive: (store: () => Promise<unknown>, session: {
|
|
87
|
+
readonly [field: string]: unknown;
|
|
88
|
+
}, refuse: () => Error) => Promise<void>;
|
|
89
|
+
export {};
|