@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,212 @@
|
|
|
1
|
+
// The `passwords` block of `createAccess`, checked, and the password hook Better
|
|
2
|
+
// Auth is built with. Better Auth hashes with scrypt; Access hashes with argon2id
|
|
3
|
+
// three ways:
|
|
4
|
+
//
|
|
5
|
+
// - by default, when the app sets no `betterAuth.emailAndPassword.password`, at the
|
|
6
|
+
// `"owasp-minimum"` profile;
|
|
7
|
+
// - at the profile the app names: `passwords: { argon2id: "rfc9106-low-memory" }`;
|
|
8
|
+
// - not at all when the app set `betterAuth.emailAndPassword.password` itself: its
|
|
9
|
+
// hook passes through untouched, as everything in the `betterAuth` block does.
|
|
10
|
+
//
|
|
11
|
+
// Naming a profile and setting the hook too is refused, so one hasher is never in
|
|
12
|
+
// doubt. The hook goes in whether or not email and password sign-in is on: Better
|
|
13
|
+
// Auth's plugins (`admin`, `username`, `phone-number`, `email-otp`) hash and verify
|
|
14
|
+
// through it too.
|
|
15
|
+
//
|
|
16
|
+
// Better Auth's scrypt hashes verify only with `legacyScrypt: true`. A sign-in whose
|
|
17
|
+
// password matches a hash that is not at the current profile (a scrypt hash, or
|
|
18
|
+
// argon2id at another profile's parameters) rehashes it at the profile (an after
|
|
19
|
+
// hook on Better Auth's `/sign-in/*` routes), so each such account moves to the
|
|
20
|
+
// profile at its next sign-in.
|
|
21
|
+
import { AccessError } from "../access/access.error.js";
|
|
22
|
+
import { isRecord } from "../policies.compiler.js";
|
|
23
|
+
import { ARGON2ID_PROFILES, argon2idHasher, DEFAULT_ARGON2ID_PROFILE, isLegacyScryptHash, needsRehash, rehashPolicyOf, } from "./argon2id.hasher.js";
|
|
24
|
+
import { NODE_RS_ARGON2_VERSION } from "./node-rs-argon2.adapter.js";
|
|
25
|
+
/** Better Auth's default `emailAndPassword.maxPasswordLength`. */
|
|
26
|
+
const BETTER_AUTH_MAX_PASSWORD_LENGTH = 128;
|
|
27
|
+
const refuse = (message) => {
|
|
28
|
+
throw new AccessError("ACCESS_OPTIONS_INVALID", message);
|
|
29
|
+
};
|
|
30
|
+
const PROFILE_NAMES = Object.keys(ARGON2ID_PROFILES)
|
|
31
|
+
.map((name) => `"${name}"`)
|
|
32
|
+
.join(" or ");
|
|
33
|
+
/** The checked `passwords` block, `undefined` when it is absent. */
|
|
34
|
+
export const checkPasswordsOptions = (passwords) => {
|
|
35
|
+
if (passwords === undefined) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
const profile = isRecord(passwords) ? passwords.argon2id : undefined;
|
|
39
|
+
const legacy = isRecord(passwords) ? passwords.legacyScrypt : undefined;
|
|
40
|
+
if (!isRecord(passwords) ||
|
|
41
|
+
Object.keys(passwords).some((key) => key !== "argon2id" && key !== "legacyScrypt") ||
|
|
42
|
+
typeof profile !== "string" ||
|
|
43
|
+
!Object.hasOwn(ARGON2ID_PROFILES, profile) ||
|
|
44
|
+
(legacy !== undefined && typeof legacy !== "boolean")) {
|
|
45
|
+
return refuse(`\`passwords\` is \`{ argon2id: ${PROFILE_NAMES}, legacyScrypt?: boolean }\`; it was ${JSON.stringify(passwords)}.`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
profile: profile,
|
|
49
|
+
legacyScrypt: legacy === true,
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The after hook that moves a password to the current profile: on a sign-in that
|
|
54
|
+
* started a session with a password, when Better Auth's password hook is Access's
|
|
55
|
+
* and the user's credential holds a hash {@link needsRehash} names (a scrypt hash
|
|
56
|
+
* under `legacyScrypt`, or argon2id at other parameters than the profile's), and
|
|
57
|
+
* this password matches it (checked again here, so no route's own reading of
|
|
58
|
+
* `password` is trusted), the password is hashed at the profile and stored in its
|
|
59
|
+
* place.
|
|
60
|
+
*
|
|
61
|
+
* The write is a compare-and-set through Better Auth's adapter: it changes the
|
|
62
|
+
* account only while it still holds the hash just checked. A password changed or
|
|
63
|
+
* reset meanwhile is never overwritten, and of two sign-ins at once the later write
|
|
64
|
+
* wins with an equally valid hash. It goes past Better Auth's account update hooks:
|
|
65
|
+
* the password is the same one, encoded anew. On a current hash it costs one read
|
|
66
|
+
* and nothing else. A failure is logged (never the password or a hash) and leaves
|
|
67
|
+
* the stored hash; the sign-in stands.
|
|
68
|
+
*/
|
|
69
|
+
export const passwordRehashHook = (modules) => ({
|
|
70
|
+
matcher: (ctx) => typeof ctx?.path === "string" && ctx.path.startsWith("/sign-in/"),
|
|
71
|
+
handler: modules.createAuthMiddleware(async (ctx) => {
|
|
72
|
+
const context = ctx?.context;
|
|
73
|
+
const userId = context?.newSession?.user?.id;
|
|
74
|
+
const password = ctx?.body?.password;
|
|
75
|
+
const policy = rehashPolicyOf(context?.password?.verify);
|
|
76
|
+
if (context === undefined ||
|
|
77
|
+
policy === undefined ||
|
|
78
|
+
typeof userId !== "string" ||
|
|
79
|
+
typeof password !== "string") {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const account = await context.internalAdapter.findCredentialAccount(userId);
|
|
84
|
+
const stored = account?.password;
|
|
85
|
+
const accountId = account?.id;
|
|
86
|
+
if (typeof stored !== "string" ||
|
|
87
|
+
typeof accountId !== "string" ||
|
|
88
|
+
!needsRehash(stored, policy)) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
const matches = isLegacyScryptHash(stored)
|
|
92
|
+
? await modules.passwords.verifyScrypt({ hash: stored, password })
|
|
93
|
+
: await context.password.verify({ hash: stored, password });
|
|
94
|
+
if (matches !== true) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
await context.adapter.updateMany({
|
|
98
|
+
model: "account",
|
|
99
|
+
where: [
|
|
100
|
+
{ field: "id", value: accountId },
|
|
101
|
+
{ field: "password", value: stored },
|
|
102
|
+
],
|
|
103
|
+
update: { password: await context.password.hash(password) },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
context.logger.error("Aventara Access: rehashing a password at the current profile failed; the stored hash stays until the next sign-in.");
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
/**
|
|
113
|
+
* The password hook Better Auth runs with: argon2id at the checked block's profile,
|
|
114
|
+
* or `undefined` when the app set `emailAndPassword.password` itself (its hook
|
|
115
|
+
* passes through). `at` names that option as the app writes it.
|
|
116
|
+
*/
|
|
117
|
+
export const passwordHookOf = (settings, emailAndPassword, modules,
|
|
118
|
+
/** Better Auth's context, once it is built: where the hook logs. */
|
|
119
|
+
context, at) => {
|
|
120
|
+
const given = emailAndPassword ?? {};
|
|
121
|
+
if (!isRecord(given)) {
|
|
122
|
+
return refuse(`\`${at}\` holds Better Auth's email and password options, as \`betterAuth({...})\` takes them.`);
|
|
123
|
+
}
|
|
124
|
+
if (given.password !== undefined) {
|
|
125
|
+
if (settings !== undefined) {
|
|
126
|
+
refuse(`\`passwords\` and \`${at}.password\` both say how passwords are hashed: keep \`passwords\` for Access's argon2id, or your own \`password\` hook alone.`);
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const { crypto, verifyScrypt } = modules.passwords;
|
|
131
|
+
if (typeof crypto.argon2 !== "function") {
|
|
132
|
+
return refuse(`argon2id needs Node.js's \`crypto.argon2\` (Node.js 24.7.0 and later), which this runtime does not have: on Node.js 22, install @node-rs/argon2 (npm install --save-exact @node-rs/argon2@${NODE_RS_ARGON2_VERSION}); or set \`${at}.password\` to hash passwords yourself.`);
|
|
133
|
+
}
|
|
134
|
+
return argon2idHasher({
|
|
135
|
+
profile: settings?.profile ?? DEFAULT_ARGON2ID_PROFILE,
|
|
136
|
+
// Better Auth's own reading of it, so the cap is the one its routes check.
|
|
137
|
+
maxPasswordLength: given.maxPasswordLength ||
|
|
138
|
+
BETTER_AUTH_MAX_PASSWORD_LENGTH,
|
|
139
|
+
crypto: crypto,
|
|
140
|
+
verifyScrypt,
|
|
141
|
+
legacyScrypt: settings?.legacyScrypt === true,
|
|
142
|
+
tooLong: () => new modules.APIError("BAD_REQUEST", {
|
|
143
|
+
code: "PASSWORD_TOO_LONG",
|
|
144
|
+
message: "Password too long",
|
|
145
|
+
}),
|
|
146
|
+
log: async (level, message) => {
|
|
147
|
+
try {
|
|
148
|
+
const built = (await context());
|
|
149
|
+
built.logger[level](`Aventara Access: ${message}`);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Better Auth could not be built: there is no logger to report to.
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* What `createAccess` builds Better Auth with for passwords: its `emailAndPassword`
|
|
159
|
+
* block, the app's with the argon2id hook in it, or `undefined` when the app set
|
|
160
|
+
* `password` itself and its block passes through as given; and, with the argon2id
|
|
161
|
+
* hook, the after hook the Access plugin adds to rehash a password at the profile.
|
|
162
|
+
*/
|
|
163
|
+
export const passwordsOf = (passwords, betterAuth, modules,
|
|
164
|
+
/** Better Auth's context, once it is built: where the hook logs. */
|
|
165
|
+
context) => {
|
|
166
|
+
const settings = checkPasswordsOptions(passwords);
|
|
167
|
+
const hook = passwordHookOf(settings, betterAuth.emailAndPassword, modules, context, "betterAuth.emailAndPassword");
|
|
168
|
+
return hook === undefined
|
|
169
|
+
? { emailAndPassword: undefined, rehash: undefined }
|
|
170
|
+
: {
|
|
171
|
+
emailAndPassword: {
|
|
172
|
+
...betterAuth.emailAndPassword,
|
|
173
|
+
password: hook,
|
|
174
|
+
},
|
|
175
|
+
rehash: passwordRehashHook(modules),
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* The kiosk profile's PIN hasher: argon2id at the `passwords` block's profile (the
|
|
180
|
+
* default without one), whatever hashes the app's passwords, since a PIN is a weak
|
|
181
|
+
* secret and its hash must be slow wherever it is read. A PIN is at most
|
|
182
|
+
* `maxPinLength` digits; Better Auth's scrypt hashes never verify as PINs.
|
|
183
|
+
*/
|
|
184
|
+
export const pinHasherOf = (passwords, modules,
|
|
185
|
+
/** Better Auth's context, once it is built: where the hasher logs. */
|
|
186
|
+
context, maxPinLength) => {
|
|
187
|
+
const settings = checkPasswordsOptions(passwords);
|
|
188
|
+
const { crypto, verifyScrypt } = modules.passwords;
|
|
189
|
+
if (typeof crypto.argon2 !== "function") {
|
|
190
|
+
return refuse(`the kiosk profile hashes PINs with argon2id, which needs Node.js's \`crypto.argon2\` (Node.js 24.7.0 and later): on Node.js 22, install @node-rs/argon2 (npm install --save-exact @node-rs/argon2@${NODE_RS_ARGON2_VERSION}).`);
|
|
191
|
+
}
|
|
192
|
+
return argon2idHasher({
|
|
193
|
+
profile: settings?.profile ?? DEFAULT_ARGON2ID_PROFILE,
|
|
194
|
+
maxPasswordLength: maxPinLength,
|
|
195
|
+
crypto: crypto,
|
|
196
|
+
verifyScrypt,
|
|
197
|
+
legacyScrypt: false,
|
|
198
|
+
tooLong: () => new modules.APIError("BAD_REQUEST", {
|
|
199
|
+
code: "PIN_TOO_WEAK",
|
|
200
|
+
message: "A PIN is at most 12 digits.",
|
|
201
|
+
}),
|
|
202
|
+
log: async (level, message) => {
|
|
203
|
+
try {
|
|
204
|
+
const built = (await context());
|
|
205
|
+
built.logger[level](`Aventara Access: ${message.replace(/password/g, "PIN")}`);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Better Auth could not be built: there is no logger to report to.
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/** The format marker of every `@1` file. */
|
|
2
|
+
export declare const POLICIES_FORMAT = "aventara-access/policies@1";
|
|
3
|
+
/** The format marker of the unstable v0 files, refused by name (no users, no shim). */
|
|
4
|
+
export declare const RETIRED_POLICIES_FORMAT = "aventara-access/policies@0";
|
|
5
|
+
/** The four standard operations a rule names. */
|
|
6
|
+
export type AccessAction = "read" | "create" | "update" | "delete";
|
|
7
|
+
/** The layer a request runs under (core's `AvRequestOrigin`, copied by shape). */
|
|
8
|
+
export type AccessOrigin = "application" | "client" | "agent";
|
|
9
|
+
/** The two built-in subjects: `signedIn` (a non-null principal) and `anyone` (null included). */
|
|
10
|
+
export type AccessBuiltInSubject = "signedIn" | "anyone";
|
|
11
|
+
/** A literal a rule compares with. */
|
|
12
|
+
export type AccessLiteral = string | number | boolean | null;
|
|
13
|
+
/** A value read off the principal at question time (`me.id` → `{ principal: ["id"] }`). */
|
|
14
|
+
export type AccessPrincipalOperand = {
|
|
15
|
+
readonly principal: readonly [string, ...string[]];
|
|
16
|
+
};
|
|
17
|
+
/** What a field comparison compares with. */
|
|
18
|
+
export type AccessOperand = AccessLiteral | AccessPrincipalOperand;
|
|
19
|
+
/** The comparisons a rule may make. Each maps one-to-one to an Aventara filter operator. */
|
|
20
|
+
export type AccessComparison = "eq" | "ne" | "in" | "notIn";
|
|
21
|
+
/**
|
|
22
|
+
* A row condition in Access's neutral form. Field names are the Resource's own;
|
|
23
|
+
* a relation step names its target so the file needs no schema to walk it.
|
|
24
|
+
*/
|
|
25
|
+
export type AccessPredicate = {
|
|
26
|
+
readonly op: "true";
|
|
27
|
+
} | {
|
|
28
|
+
readonly op: "false";
|
|
29
|
+
} | {
|
|
30
|
+
readonly op: "and";
|
|
31
|
+
readonly args: readonly AccessPredicate[];
|
|
32
|
+
} | {
|
|
33
|
+
readonly op: "or";
|
|
34
|
+
readonly args: readonly AccessPredicate[];
|
|
35
|
+
} | {
|
|
36
|
+
readonly op: "not";
|
|
37
|
+
readonly arg: AccessPredicate;
|
|
38
|
+
} | {
|
|
39
|
+
readonly op: "cmp";
|
|
40
|
+
readonly field: string;
|
|
41
|
+
readonly cmp: AccessComparison;
|
|
42
|
+
readonly value: AccessOperand | readonly AccessOperand[];
|
|
43
|
+
} | {
|
|
44
|
+
readonly op: "rel";
|
|
45
|
+
readonly relation: string;
|
|
46
|
+
readonly target: string;
|
|
47
|
+
readonly where: AccessPredicate;
|
|
48
|
+
} | {
|
|
49
|
+
readonly op: "some" | "every" | "none";
|
|
50
|
+
readonly relation: string;
|
|
51
|
+
readonly target: string;
|
|
52
|
+
readonly where: AccessPredicate;
|
|
53
|
+
} | {
|
|
54
|
+
readonly op: "role";
|
|
55
|
+
readonly role: string;
|
|
56
|
+
};
|
|
57
|
+
/** A condition on the principal alone: its claims and the request's origin. */
|
|
58
|
+
export type AccessPrincipalCondition = {
|
|
59
|
+
readonly op: "and";
|
|
60
|
+
readonly args: readonly AccessPrincipalCondition[];
|
|
61
|
+
} | {
|
|
62
|
+
readonly op: "cmp";
|
|
63
|
+
readonly claim: readonly [string, ...string[]];
|
|
64
|
+
readonly cmp: "eq";
|
|
65
|
+
readonly value: AccessLiteral;
|
|
66
|
+
} | {
|
|
67
|
+
readonly op: "origin";
|
|
68
|
+
readonly value: AccessOrigin;
|
|
69
|
+
};
|
|
70
|
+
/** A role: global (read from claims) or held on a row of one Resource. */
|
|
71
|
+
export type AccessRoleDefinition = {
|
|
72
|
+
readonly global: AccessPrincipalCondition;
|
|
73
|
+
} | {
|
|
74
|
+
readonly on: string;
|
|
75
|
+
readonly where: AccessPredicate;
|
|
76
|
+
};
|
|
77
|
+
/** One grant. Any grant allows (default deny). */
|
|
78
|
+
export type AccessGrantRule = {
|
|
79
|
+
readonly id: string;
|
|
80
|
+
readonly subjects: readonly string[];
|
|
81
|
+
readonly actions: readonly AccessAction[];
|
|
82
|
+
readonly resource: string;
|
|
83
|
+
/** The to-one relation whose target holds each subject's row role. */
|
|
84
|
+
readonly in?: string;
|
|
85
|
+
readonly where?: AccessPredicate;
|
|
86
|
+
/** Governed fields this grant opens, as `Resource.field`. */
|
|
87
|
+
readonly fields?: readonly string[];
|
|
88
|
+
/** Fields this grant does not let a create or update write. */
|
|
89
|
+
readonly write?: {
|
|
90
|
+
readonly except: readonly string[];
|
|
91
|
+
};
|
|
92
|
+
/** A condition the row an update leaves must still meet. */
|
|
93
|
+
readonly after?: AccessPredicate;
|
|
94
|
+
};
|
|
95
|
+
/** One deny. Any deny wins over every grant. */
|
|
96
|
+
export type AccessDenyRule = {
|
|
97
|
+
readonly id: string;
|
|
98
|
+
readonly actions: "*" | readonly AccessAction[];
|
|
99
|
+
readonly resource: string;
|
|
100
|
+
readonly where?: AccessPredicate;
|
|
101
|
+
readonly if?: AccessPrincipalCondition;
|
|
102
|
+
};
|
|
103
|
+
/** A relation as the engine needs it: where it leads, and the reference pairs when this end holds them. */
|
|
104
|
+
export type AccessSchemaRelationFact = {
|
|
105
|
+
readonly target: string;
|
|
106
|
+
readonly cardinality: "one" | "many";
|
|
107
|
+
readonly referenceFields?: readonly [string, ...string[]];
|
|
108
|
+
readonly references?: readonly [string, ...string[]];
|
|
109
|
+
};
|
|
110
|
+
/** The schema facts the engine reads, per Resource. */
|
|
111
|
+
export type AccessSchemaResourceFacts = {
|
|
112
|
+
readonly identifiers: readonly (readonly string[])[];
|
|
113
|
+
readonly fields: readonly string[];
|
|
114
|
+
/**
|
|
115
|
+
* The scalar fields that may hold NULL, when there are any. A scope negates a
|
|
116
|
+
* comparison on one of them null-safely (`NOT (f = v) OR f IS NULL`), so it keeps
|
|
117
|
+
* the rows `can` keeps.
|
|
118
|
+
*/
|
|
119
|
+
readonly nullable?: readonly string[];
|
|
120
|
+
/**
|
|
121
|
+
* The scalar fields `can` compares by their type rather than by their JavaScript
|
|
122
|
+
* value, when there are any: a `decimal` as its canonical decimal digits (a decimal
|
|
123
|
+
* object, a string and a number alike), a `datetime` as its instant, a `bigint`
|
|
124
|
+
* exactly. A file without `types` has none of them.
|
|
125
|
+
*/
|
|
126
|
+
readonly types?: {
|
|
127
|
+
readonly [field: string]: "bigint" | "datetime" | "decimal";
|
|
128
|
+
};
|
|
129
|
+
readonly relations: {
|
|
130
|
+
readonly [relation: string]: AccessSchemaRelationFact;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* The namespace that owns this Resource, when it is not this file's. A foreign
|
|
134
|
+
* Resource is read in conditions and never granted on.
|
|
135
|
+
*/
|
|
136
|
+
readonly foreign?: string;
|
|
137
|
+
};
|
|
138
|
+
/** A claim's type, never its value. */
|
|
139
|
+
export type AccessClaimType = {
|
|
140
|
+
readonly scalar: "string" | "number" | "boolean";
|
|
141
|
+
};
|
|
142
|
+
/** The `@1` policies file. `Governed` keeps the `govern` block's literal type through the builder. */
|
|
143
|
+
export type AccessPolicies<Governed extends {
|
|
144
|
+
readonly [resource: string]: readonly string[];
|
|
145
|
+
} = {
|
|
146
|
+
readonly [resource: string]: readonly string[];
|
|
147
|
+
}> = {
|
|
148
|
+
readonly format: typeof POLICIES_FORMAT;
|
|
149
|
+
/** The namespace that owns this file's Resources. A single app needs none. */
|
|
150
|
+
readonly namespace?: string;
|
|
151
|
+
readonly claims: {
|
|
152
|
+
readonly [claim: string]: AccessClaimType;
|
|
153
|
+
};
|
|
154
|
+
readonly schema: {
|
|
155
|
+
readonly [resource: string]: AccessSchemaResourceFacts;
|
|
156
|
+
};
|
|
157
|
+
readonly roles: {
|
|
158
|
+
readonly [role: string]: AccessRoleDefinition;
|
|
159
|
+
};
|
|
160
|
+
readonly govern: Governed;
|
|
161
|
+
readonly grants: readonly AccessGrantRule[];
|
|
162
|
+
readonly denies: readonly AccessDenyRule[];
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* A policies file as a JSON import types it: `resolveJsonModule` widens `format` to
|
|
166
|
+
* `string` and each tuple to an array, so the file `avaccess pull` wrote, imported
|
|
167
|
+
* with `import policies from "./access/blog.policies.json" with { type: "json" }`,
|
|
168
|
+
* is not an `AccessPolicies`. `policies.rules` and `createAccessEngine` take it as it
|
|
169
|
+
* is: only its outline is typed, and the engine checks the rest when it is built.
|
|
170
|
+
*/
|
|
171
|
+
export type AccessPoliciesJson<Governed extends {
|
|
172
|
+
readonly [resource: string]: readonly string[];
|
|
173
|
+
} = {
|
|
174
|
+
readonly [resource: string]: readonly string[];
|
|
175
|
+
}> = {
|
|
176
|
+
readonly format: string;
|
|
177
|
+
readonly namespace?: string;
|
|
178
|
+
readonly claims: {
|
|
179
|
+
readonly [claim: string]: {
|
|
180
|
+
readonly scalar: string;
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
readonly schema: {
|
|
184
|
+
readonly [resource: string]: object;
|
|
185
|
+
};
|
|
186
|
+
readonly roles: {
|
|
187
|
+
readonly [role: string]: object;
|
|
188
|
+
};
|
|
189
|
+
readonly govern: Governed;
|
|
190
|
+
readonly grants: readonly object[];
|
|
191
|
+
readonly denies: readonly object[];
|
|
192
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The declarative policies file, format `aventara-access/policies@1`. The builder
|
|
2
|
+
// compiles to it and the engine runs from it alone. It is passive JSON: no closures,
|
|
3
|
+
// no claim values, no provider names. `@1` is frozen: a change to its shape is `policies@2` beside it, never an
|
|
4
|
+
// edit of `@1`. Its canonical byte form and hash are `policies.hash.ts`'s.
|
|
5
|
+
/** The format marker of every `@1` file. */
|
|
6
|
+
export const POLICIES_FORMAT = "aventara-access/policies@1";
|
|
7
|
+
/** The format marker of the unstable v0 files, refused by name (no users, no shim). */
|
|
8
|
+
export const RETIRED_POLICIES_FORMAT = "aventara-access/policies@0";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { AccessDefinition } from "./access.builder.js";
|
|
2
|
+
import { type AccessAction, type AccessClaimType, type AccessOrigin, type AccessPolicies, type AccessPrincipalCondition } from "./policies-contract.js";
|
|
3
|
+
/** The marker a `me` value carries; the compiler writes it as `{ principal: [...] }`. */
|
|
4
|
+
export declare const PRINCIPAL_REF: unique symbol;
|
|
5
|
+
export declare const ACTIONS: readonly AccessAction[];
|
|
6
|
+
export declare const ORIGINS: readonly AccessOrigin[];
|
|
7
|
+
export declare const CLAIM_SCALARS: readonly string[];
|
|
8
|
+
export declare const fail: (where: string, message: string) => never;
|
|
9
|
+
export declare const isRecord: (value: unknown) => value is {
|
|
10
|
+
readonly [key: string]: unknown;
|
|
11
|
+
};
|
|
12
|
+
export declare const asList: <T>(value: T | readonly T[]) => readonly T[];
|
|
13
|
+
/** The claims a file compares, by the type each is compared as. */
|
|
14
|
+
export type ClaimTypes = Record<string, AccessClaimType>;
|
|
15
|
+
export declare const idAllocator: () => (prefix: string, name: unknown) => string;
|
|
16
|
+
/**
|
|
17
|
+
* A rule's `if` (`{ claims?, origin? }`), compiled to a condition on the principal
|
|
18
|
+
* alone. Each claim's type is recorded in `claimTypes`, and a claim compared as two
|
|
19
|
+
* types is refused.
|
|
20
|
+
*/
|
|
21
|
+
export declare const compilePrincipalIf: (rule: string, condition: unknown, claimTypes: ClaimTypes) => AccessPrincipalCondition;
|
|
22
|
+
/** Compiles the builder's rules to the `@1` policies file and checks it. */
|
|
23
|
+
export declare const compilePolicies: <Governed extends {
|
|
24
|
+
readonly [resource: string]: readonly string[];
|
|
25
|
+
}>(definition: AccessDefinition) => AccessPolicies<Governed>;
|
|
26
|
+
export declare const checkActions: (rule: string, actions: readonly string[]) => void;
|
|
27
|
+
/** Whether `value` is a name a file may give a Resource or a field. */
|
|
28
|
+
export declare const isName: (value: unknown) => value is string;
|
|
29
|
+
/** Whether `value` is a namespace name. */
|
|
30
|
+
export declare const isNamespace: (value: unknown) => value is string;
|
|
31
|
+
/**
|
|
32
|
+
* Checks a policies file before an engine runs from it. Throws an `Error` naming
|
|
33
|
+
* the rule. The file is trusted for nothing it can be checked for. `composed`: the
|
|
34
|
+
* file the engine composed itself, whose denies may take the reserved ids.
|
|
35
|
+
*/
|
|
36
|
+
export declare const checkPolicies: (policies: AccessPolicies, composed?: boolean) => void;
|