@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,60 @@
|
|
|
1
|
+
// `avaccess schema --orm <orm> [--schema <path>] [--out <file>]`: the `AccessSchema`
|
|
2
|
+
// of a service that does not run on Aventara, read from its ORM's schema, written as a
|
|
3
|
+
// TypeScript module its rules are authored against with `definePolicies`. An Aventara
|
|
4
|
+
// app authors against its discovered model instead. No network.
|
|
5
|
+
//
|
|
6
|
+
// When `avAccess.lock.json` pins foreign facts (`avaccess pull --foreign`), the
|
|
7
|
+
// Resources they hold are marked as their owners' (`foreign.merger.ts`): the rules
|
|
8
|
+
// read them in conditions, never grant on them. Until they are pulled, a Resource
|
|
9
|
+
// another namespace owns is not in the model as foreign.
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { mergeForeign } from "../foreign/foreign.merger.js";
|
|
12
|
+
import { emitSchemaModule } from "../generator/schema.emitter.js";
|
|
13
|
+
import { CliFailure, EXIT_ERROR, EXIT_OK, messageOf, positionals, projectFilePath, stringFlag, } from "./cli.context.js";
|
|
14
|
+
import { readForeignPinned, readLockIfAny, writeProjectFile, } from "./lock.file.js";
|
|
15
|
+
import { translatorOf } from "./translator.flag.js";
|
|
16
|
+
/** Where the schema module goes when `--out` does not say. */
|
|
17
|
+
const DEFAULT_SCHEMA_MODULE = "src/generated/avAccess.schema.ts";
|
|
18
|
+
export const schemaCommand = async (context) => {
|
|
19
|
+
positionals(context, "avaccess schema --orm <orm> [--schema <path>] [--out <file>]", 0, 0);
|
|
20
|
+
const translator = translatorOf(context);
|
|
21
|
+
const out = projectFilePath(stringFlag(context, "out") ?? DEFAULT_SCHEMA_MODULE, "--out", DEFAULT_SCHEMA_MODULE, ".ts");
|
|
22
|
+
const source = (stringFlag(context, "schema") ?? translator.defaultSchema).replace(/\\/g, "/");
|
|
23
|
+
let reading;
|
|
24
|
+
try {
|
|
25
|
+
reading = await translator.readSchema(resolve(context.cwd, source));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new CliFailure(EXIT_ERROR, error?.code === "ENOENT"
|
|
29
|
+
? `no ${translator.title} schema at ${source}: pass its file or folder with \`--schema <path>\``
|
|
30
|
+
: `${source} could not be read (${messageOf(error)})`);
|
|
31
|
+
}
|
|
32
|
+
const lock = await readLockIfAny(context.cwd);
|
|
33
|
+
let schema = reading.schema;
|
|
34
|
+
const skipped = [...reading.skipped];
|
|
35
|
+
const facts = lock === undefined ? undefined : await readForeignPinned(context.cwd, lock);
|
|
36
|
+
if (facts !== undefined) {
|
|
37
|
+
const merged = mergeForeign(schema, facts, source);
|
|
38
|
+
schema = merged.schema;
|
|
39
|
+
skipped.push(...merged.skipped);
|
|
40
|
+
}
|
|
41
|
+
const resources = Object.keys(schema.resources);
|
|
42
|
+
if (resources.length === 0) {
|
|
43
|
+
throw new CliFailure(EXIT_ERROR, `${source} declares no model Access can read${skipped.length === 0 ? "" : ` (left out: ${skipped.join("; ")})`}`);
|
|
44
|
+
}
|
|
45
|
+
await writeProjectFile(context.cwd, out, emitSchemaModule({
|
|
46
|
+
schema,
|
|
47
|
+
translator,
|
|
48
|
+
source,
|
|
49
|
+
command: `avaccess schema --orm ${translator.orm} --schema ${source} --out ${out}`,
|
|
50
|
+
}));
|
|
51
|
+
const enums = Object.keys(schema.enums).length;
|
|
52
|
+
const foreign = resources.filter((resource) => schema.resources[resource]?.foreign !== undefined);
|
|
53
|
+
context.out(`wrote ${out}: ${resources.length} Resource${resources.length === 1 ? "" : "s"} (${resources.join(", ")}) and ${enums} enum${enums === 1 ? "" : "s"}, from ${source}${foreign.length === 0
|
|
54
|
+
? ""
|
|
55
|
+
: `; read as foreign: ${foreign.map((resource) => `${resource} (namespace "${schema.resources[resource]?.foreign}")`).join(", ")}`}`);
|
|
56
|
+
for (const line of skipped) {
|
|
57
|
+
context.out(`left out: ${line}`);
|
|
58
|
+
}
|
|
59
|
+
return EXIT_OK;
|
|
60
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type CliContext, type ExitCode } from "./cli.context.js";
|
|
2
|
+
/** The environment variable that asks for the migration on start, as `--migrate` does. */
|
|
3
|
+
export declare const MIGRATE_VARIABLE = "AVACCESS_MIGRATE";
|
|
4
|
+
/** The environment variable read for the port when `--port` is not given. */
|
|
5
|
+
export declare const PORT_VARIABLE = "PORT";
|
|
6
|
+
export declare const serveCommand: (context: CliContext) => Promise<ExitCode>;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// `avaccess serve`: the standalone host, run from the operator's configuration. It
|
|
2
|
+
// loads `access.config.ts` (or the file `--config` names) the way `prisma.config.ts`
|
|
3
|
+
// is loaded, with jiti, so the configuration is a TypeScript module with no build
|
|
4
|
+
// step; the module exports the `createAccess({...})` instance, as its default export
|
|
5
|
+
// or as `access`. Then it serves that instance with `serve()` (the same handler, and
|
|
6
|
+
// `GET /health`) until the process is told to stop (SIGTERM or SIGINT).
|
|
7
|
+
//
|
|
8
|
+
// The database: Better Auth's own migration (the plan `npx auth migrate` runs) runs
|
|
9
|
+
// on start only when asked, with `--migrate` or `AVACCESS_MIGRATE=true`. Without it,
|
|
10
|
+
// a database Better Auth can migrate itself (a Kysely-backed one: a `pg` Pool, a
|
|
11
|
+
// MySQL pool, SQLite) is checked first, and a database that lacks tables or columns
|
|
12
|
+
// these options need is refused with what is missing: the host never starts on a
|
|
13
|
+
// schema it would fail on at the first request. A database behind an ORM's adapter
|
|
14
|
+
// (Prisma, Drizzle) is the ORM's to migrate (`npx auth generate`, then the ORM's
|
|
15
|
+
// migration); `--migrate` refuses it.
|
|
16
|
+
//
|
|
17
|
+
// It listens on 127.0.0.1 unless `--hostname` says otherwise (in a container,
|
|
18
|
+
// `0.0.0.0`), and turns Better Auth's rate limit on whatever NODE_ENV is, unless the
|
|
19
|
+
// configuration sets `betterAuth.rateLimit.enabled` itself: Better Auth turns it on
|
|
20
|
+
// in production only, and this is a server facing the network.
|
|
21
|
+
import { access } from "node:fs/promises";
|
|
22
|
+
import { resolve } from "node:path";
|
|
23
|
+
import { isRecord } from "../policies.compiler.js";
|
|
24
|
+
import { CliFailure, EXIT_ERROR, EXIT_FAILED, EXIT_OK, messageOf, positionals, stringFlag, } from "./cli.context.js";
|
|
25
|
+
import { loadProjectModule, pickExport } from "./module.loader.js";
|
|
26
|
+
/** The configuration files `avaccess serve` looks for, in this order, when `--config` names none. */
|
|
27
|
+
const CONFIG_FILES = [
|
|
28
|
+
"access.config.ts",
|
|
29
|
+
"access.config.mts",
|
|
30
|
+
"access.config.cts",
|
|
31
|
+
"access.config.js",
|
|
32
|
+
"access.config.mjs",
|
|
33
|
+
"access.config.cjs",
|
|
34
|
+
];
|
|
35
|
+
/** The environment variable that asks for the migration on start, as `--migrate` does. */
|
|
36
|
+
export const MIGRATE_VARIABLE = "AVACCESS_MIGRATE";
|
|
37
|
+
/** The environment variable read for the port when `--port` is not given. */
|
|
38
|
+
export const PORT_VARIABLE = "PORT";
|
|
39
|
+
const USAGE = "avaccess serve [--config <file>] [--port <port>] [--hostname <host>] [--max-body-bytes <bytes>] [--migrate]";
|
|
40
|
+
/** The interface `avaccess serve` listens on when `--hostname` names none: this host only. */
|
|
41
|
+
const DEFAULT_HOSTNAME = "127.0.0.1";
|
|
42
|
+
const isServedInstance = (value) => isRecord(value) &&
|
|
43
|
+
typeof value.handler === "function" &&
|
|
44
|
+
isRecord(value.options);
|
|
45
|
+
/** Whether `--migrate` or `AVACCESS_MIGRATE` asks for the migration. */
|
|
46
|
+
const migrateAsked = (context) => {
|
|
47
|
+
if (context.flags.migrate === true) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
const value = context.env[MIGRATE_VARIABLE];
|
|
51
|
+
if (value === undefined ||
|
|
52
|
+
value === "" ||
|
|
53
|
+
value === "false" ||
|
|
54
|
+
value === "0") {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
if (value === "true" || value === "1") {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
throw new CliFailure(EXIT_ERROR, `${MIGRATE_VARIABLE} is "true" or "1" to migrate on start, "false", "0" or unset not to; it was ${JSON.stringify(value)}`);
|
|
61
|
+
};
|
|
62
|
+
const portOf = (context) => {
|
|
63
|
+
const flag = stringFlag(context, "port");
|
|
64
|
+
const value = flag ?? context.env[PORT_VARIABLE];
|
|
65
|
+
if (value === undefined || value === "") {
|
|
66
|
+
return 3000;
|
|
67
|
+
}
|
|
68
|
+
const port = Number(value);
|
|
69
|
+
if (!/^\d+$/.test(value) || port > 65_535) {
|
|
70
|
+
throw new CliFailure(EXIT_ERROR, `${flag === undefined ? PORT_VARIABLE : "--port"} is a port number from 0 to 65535; it was ${JSON.stringify(value)}`);
|
|
71
|
+
}
|
|
72
|
+
return port;
|
|
73
|
+
};
|
|
74
|
+
const maxBodyOf = (context) => {
|
|
75
|
+
const value = stringFlag(context, "max-body-bytes");
|
|
76
|
+
if (value === undefined) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
if (!/^\d+$/.test(value) || Number(value) < 1) {
|
|
80
|
+
throw new CliFailure(EXIT_ERROR, `--max-body-bytes is a number of bytes, 1 or more; it was ${JSON.stringify(value)}`);
|
|
81
|
+
}
|
|
82
|
+
return Number(value);
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Turns Better Auth's rate limit on before the instance is built (it is built on
|
|
86
|
+
* its first request), unless the configuration says whether it is on.
|
|
87
|
+
*/
|
|
88
|
+
const rateLimited = (instance) => {
|
|
89
|
+
const limit = instance.options.rateLimit;
|
|
90
|
+
if (limit?.enabled !== undefined) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
instance.options.rateLimit = { ...limit, enabled: true };
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new CliFailure(EXIT_ERROR, "Better Auth's options are frozen, so avaccess serve cannot turn its rate limit on: set `betterAuth.rateLimit.enabled` in the configuration (true, unless something in front of it limits the rate)");
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
/** The configuration file: `--config`'s, else the first of `CONFIG_FILES` in the working directory. */
|
|
101
|
+
const configPathOf = async (context) => {
|
|
102
|
+
const named = stringFlag(context, "config");
|
|
103
|
+
if (named !== undefined) {
|
|
104
|
+
return named;
|
|
105
|
+
}
|
|
106
|
+
for (const file of CONFIG_FILES) {
|
|
107
|
+
try {
|
|
108
|
+
await access(resolve(context.cwd, file));
|
|
109
|
+
return file;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Not this one.
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw new CliFailure(EXIT_ERROR, `no configuration: ${CONFIG_FILES[0]} is not in ${context.cwd}. Write it (its default export is your \`createAccess({...})\` instance), or pass --config <file>`);
|
|
116
|
+
};
|
|
117
|
+
/** The migration's plan, refused when the database is not one Better Auth migrates itself. */
|
|
118
|
+
const planOf = async (instance, migrate) => {
|
|
119
|
+
const database = instance.options.database;
|
|
120
|
+
// Better Auth's own migration runs on a Kysely-backed database only; on another
|
|
121
|
+
// (an ORM's adapter, or none: memory) its planner would end the process.
|
|
122
|
+
if (database === undefined || typeof database === "function") {
|
|
123
|
+
if (migrate) {
|
|
124
|
+
throw new CliFailure(EXIT_ERROR, database === undefined
|
|
125
|
+
? "--migrate: the configuration names no `betterAuth.database`, so there is nothing to migrate"
|
|
126
|
+
: "--migrate: `betterAuth.database` is an ORM's adapter, which Better Auth does not migrate itself. Generate the schema with `npx auth generate` and apply it with the ORM's migration, then start without --migrate");
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
let migrations;
|
|
131
|
+
try {
|
|
132
|
+
migrations = (await import("better-auth/db/migration"));
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
throw new CliFailure(EXIT_ERROR, `Better Auth's migration could not be loaded (${messageOf(error)}); install better-auth beside @aventara/access`);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
return await migrations.getMigrations(instance.options, {
|
|
139
|
+
throwOnUnsafe: false,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
throw new CliFailure(EXIT_ERROR, `the database could not be read for Better Auth's migration: ${messageOf(error)}`);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
/** What the plan would change, one item per table, column and index. */
|
|
147
|
+
const changesOf = (plan) => [
|
|
148
|
+
...plan.toBeCreated.map((table) => `table ${table.table}`),
|
|
149
|
+
...plan.toBeAdded.flatMap((table) => Object.keys(table.fields).map((field) => `column ${table.table}.${field}`)),
|
|
150
|
+
...plan.toBeAddedIndexes.map((index) => `index ${index.name}`),
|
|
151
|
+
];
|
|
152
|
+
export const serveCommand = async (context) => {
|
|
153
|
+
positionals(context, USAGE, 0, 0);
|
|
154
|
+
const migrate = migrateAsked(context);
|
|
155
|
+
const port = portOf(context);
|
|
156
|
+
const hostname = stringFlag(context, "hostname") ?? DEFAULT_HOSTNAME;
|
|
157
|
+
const maxBodyBytes = maxBodyOf(context);
|
|
158
|
+
const config = await configPathOf(context);
|
|
159
|
+
const instance = pickExport(await loadProjectModule(context.cwd, config), config, "access", isServedInstance, "Access instance (the `createAccess({...})` instance)");
|
|
160
|
+
const plan = await planOf(instance, migrate);
|
|
161
|
+
if (plan !== undefined) {
|
|
162
|
+
if (plan.unsafeChanges.length > 0) {
|
|
163
|
+
throw new CliFailure(EXIT_FAILED, `Better Auth's migration refuses this database: ${plan.unsafeChanges.join("; ")}`);
|
|
164
|
+
}
|
|
165
|
+
const changes = changesOf(plan);
|
|
166
|
+
if (changes.length > 0 && !migrate) {
|
|
167
|
+
throw new CliFailure(EXIT_FAILED, `the database lacks what these options need (${changes.join(", ")}). Start once with --migrate (or ${MIGRATE_VARIABLE}=true) to run Better Auth's migration, or run \`npx auth migrate\` against this configuration`);
|
|
168
|
+
}
|
|
169
|
+
if (changes.length > 0) {
|
|
170
|
+
try {
|
|
171
|
+
await plan.runMigrations();
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
throw new CliFailure(EXIT_FAILED, `Better Auth's migration failed: ${messageOf(error)}`);
|
|
175
|
+
}
|
|
176
|
+
context.out(`migrated: ${changes.join(", ")}`);
|
|
177
|
+
}
|
|
178
|
+
else if (migrate) {
|
|
179
|
+
context.out("migrated: nothing to change");
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
rateLimited(instance);
|
|
183
|
+
const { serve } = await import("../standalone/access.server.js");
|
|
184
|
+
let server;
|
|
185
|
+
try {
|
|
186
|
+
server = await serve(instance, {
|
|
187
|
+
port,
|
|
188
|
+
hostname,
|
|
189
|
+
...(maxBodyBytes === undefined ? {} : { maxBodyBytes }),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
throw new CliFailure(EXIT_ERROR, `could not listen on ${hostname}, port ${port}: ${messageOf(error)}`);
|
|
194
|
+
}
|
|
195
|
+
context.out(`serving ${config} on ${server.url} (health: ${server.url}/health)`);
|
|
196
|
+
const signal = await context.untilStopped();
|
|
197
|
+
await server.close();
|
|
198
|
+
context.out(`stopped (${signal})`);
|
|
199
|
+
return EXIT_OK;
|
|
200
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// `avaccess status`: `avAccess.lock.json` against the registry. For each pin (the
|
|
2
|
+
// namespace's policies file, the org's guardrails) it says whether the lock holds the
|
|
3
|
+
// registry's current revision or is behind it, and exits 1 when either is behind,
|
|
4
|
+
// so CI catches a project built with files older than the registry's.
|
|
5
|
+
import { LOCK_FILE } from "../lock/lock-contract.js";
|
|
6
|
+
import { EXIT_FAILED, EXIT_OK, positionals, } from "./cli.context.js";
|
|
7
|
+
import { readLock } from "./lock.file.js";
|
|
8
|
+
import { registryClient } from "./registry.client.js";
|
|
9
|
+
export const statusCommand = async (context) => {
|
|
10
|
+
positionals(context, "avaccess status", 0, 0);
|
|
11
|
+
const lock = await readLock(context.cwd);
|
|
12
|
+
const registry = registryClient(context);
|
|
13
|
+
const head = await registry.headHash(lock.namespace);
|
|
14
|
+
const guardrails = await registry.guardrails(lock.namespace);
|
|
15
|
+
const behind = [];
|
|
16
|
+
const line = (what, locked, current) => {
|
|
17
|
+
if (locked === current) {
|
|
18
|
+
return `${what}: up to date (${locked ?? "none"})`;
|
|
19
|
+
}
|
|
20
|
+
behind.push(what);
|
|
21
|
+
return `${what}: behind: ${LOCK_FILE} pins ${locked ?? "none"}, the registry's current is ${current ?? "none"}`;
|
|
22
|
+
};
|
|
23
|
+
context.out(line(`policies of namespace "${lock.namespace}"`, lock.policies.hash, head));
|
|
24
|
+
context.out(line("the org's guardrails", lock.guardrails?.hash ?? null, guardrails?.hash ?? null));
|
|
25
|
+
if (behind.length === 0) {
|
|
26
|
+
context.out(`${LOCK_FILE} is up to date with ${registry.url}`);
|
|
27
|
+
return EXIT_OK;
|
|
28
|
+
}
|
|
29
|
+
context.out(`${LOCK_FILE} is behind the registry (${behind.join("; ")}): run \`avaccess pull\`, then commit the pulled files and the lock`);
|
|
30
|
+
return EXIT_FAILED;
|
|
31
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ClientTranslator, type OrmTranslator } from "../generator/orm.translator.js";
|
|
2
|
+
import { type CliContext } from "./cli.context.js";
|
|
3
|
+
/** `avaccess schema`'s ORM translator: `--orm` is required. */
|
|
4
|
+
export declare const translatorOf: (context: CliContext) => OrmTranslator;
|
|
5
|
+
/** `avaccess generate`'s translator: `--orm`, `none` when it is not given. */
|
|
6
|
+
export declare const clientTranslatorOf: (context: CliContext) => ClientTranslator;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// `--orm <orm>`: which translator `avaccess generate` and `avaccess schema` use.
|
|
2
|
+
// `schema` requires it: there is no neutral schema reader. `generate` defaults to
|
|
3
|
+
// `none` (ruled 2026-10-23), and its banner always spells the value out, so a later
|
|
4
|
+
// translator never changes what an existing script generates.
|
|
5
|
+
import { CLIENT_TRANSLATORS, ORM_TRANSLATORS, } from "../generator/orm.translator.js";
|
|
6
|
+
import { CliFailure, EXIT_ERROR, stringFlag, } from "./cli.context.js";
|
|
7
|
+
/** The translator `--orm` names in `registry`; refused, listing them, when it names none. */
|
|
8
|
+
const translatorIn = (context, registry, fallback) => {
|
|
9
|
+
const orm = stringFlag(context, "orm") ?? fallback;
|
|
10
|
+
const known = Object.keys(registry).sort();
|
|
11
|
+
const translator = orm === undefined || !Object.hasOwn(registry, orm)
|
|
12
|
+
? undefined
|
|
13
|
+
: registry[orm];
|
|
14
|
+
if (translator === undefined) {
|
|
15
|
+
throw new CliFailure(EXIT_ERROR, `${orm === undefined
|
|
16
|
+
? "pass the ORM with `--orm <orm>`"
|
|
17
|
+
: `no translator for --orm ${JSON.stringify(orm)}`}; this version has ${known.map((name) => `\`${name}\``).join(", ")}`);
|
|
18
|
+
}
|
|
19
|
+
return translator;
|
|
20
|
+
};
|
|
21
|
+
/** `avaccess schema`'s ORM translator: `--orm` is required. */
|
|
22
|
+
export const translatorOf = (context) => translatorIn(context, ORM_TRANSLATORS, undefined);
|
|
23
|
+
/** `avaccess generate`'s translator: `--orm`, `none` when it is not given. */
|
|
24
|
+
export const clientTranslatorOf = (context) => translatorIn(context, CLIENT_TRANSLATORS, "none");
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// `avaccess verify`: the pulled files on disk against `avAccess.lock.json`, offline.
|
|
2
|
+
// It reads the lock and the files it pins and runs the engine's own check
|
|
3
|
+
// (`lock.verifier.ts`): no registry, no network. It belongs at the front of a
|
|
4
|
+
// project's `aventara:prepare` script, so a pulled file edited by hand stops the
|
|
5
|
+
// build before anything is generated from it.
|
|
6
|
+
import { foreignOwnersOf, verifyLocked } from "../lock/lock.verifier.js";
|
|
7
|
+
import { LOCK_FILE } from "../lock/lock-contract.js";
|
|
8
|
+
import { environmentOf, verifyBeaconPins } from "./beacon-pins.file.js";
|
|
9
|
+
import { CliFailure, codeOf, EXIT_FAILED, EXIT_OK, messageOf, positionals, } from "./cli.context.js";
|
|
10
|
+
import { readForeignPinned, readLock, readPinned } from "./lock.file.js";
|
|
11
|
+
export const verifyCommand = async (context) => {
|
|
12
|
+
positionals(context, "avaccess verify [--env <environment>]", 0, 0);
|
|
13
|
+
const lock = await readLock(context.cwd);
|
|
14
|
+
const policies = await readPinned(context.cwd, lock.policies);
|
|
15
|
+
const guardrails = lock.guardrails === null
|
|
16
|
+
? undefined
|
|
17
|
+
: await readPinned(context.cwd, lock.guardrails);
|
|
18
|
+
try {
|
|
19
|
+
verifyLocked(lock, {
|
|
20
|
+
namespace: policies
|
|
21
|
+
?.namespace,
|
|
22
|
+
policies,
|
|
23
|
+
guardrails,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new CliFailure(EXIT_FAILED, messageOf(error), codeOf(error));
|
|
28
|
+
}
|
|
29
|
+
await readForeignPinned(context.cwd, lock);
|
|
30
|
+
const owners = foreignOwnersOf(policies);
|
|
31
|
+
const environments = await verifyBeaconPins(context.cwd, lock.namespace, owners, environmentOf(context));
|
|
32
|
+
context.out(`verified against ${LOCK_FILE}: ${lock.policies.path} (${lock.policies.hash})${lock.guardrails === null
|
|
33
|
+
? ", and no guardrails pinned"
|
|
34
|
+
: ` and ${lock.guardrails.path} (${lock.guardrails.hash})`}${lock.foreign === undefined
|
|
35
|
+
? ""
|
|
36
|
+
: `, and the foreign facts ${lock.foreign.path} (${lock.foreign.hash})`}${environments.length === 0
|
|
37
|
+
? ""
|
|
38
|
+
: `; the beacon pins of ${environments.map((environment) => `"${environment}"`).join(", ")} cover what the rules read`}`);
|
|
39
|
+
return EXIT_OK;
|
|
40
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { AccessClaimsDeclaration } from "../access/claims.mapper.js";
|
|
2
|
+
import { type AccessProfileKind } from "../access/profile.policy.js";
|
|
3
|
+
import type { SignInIdentifier } from "../audit/audit.log.js";
|
|
4
|
+
import { type AccessKioskVerbName } from "../kiosk/kiosk-verbs.schema.js";
|
|
5
|
+
import { type AccessVerbSchema } from "./access-verbs.schema.js";
|
|
6
|
+
import type { LimitedCall, LimitSettle } from "./verb-rate.limiter.js";
|
|
7
|
+
/** The headers of a request, as the framework hands them to `call`. */
|
|
8
|
+
type RequestHeaders = {
|
|
9
|
+
get(name: string): string | null;
|
|
10
|
+
has(name: string): boolean;
|
|
11
|
+
forEach(callback: (value: string, name: string) => void): void;
|
|
12
|
+
};
|
|
13
|
+
/** One verb as `access.client` publishes it: its route, its shapes and its server call. */
|
|
14
|
+
type AccessClientVerb = {
|
|
15
|
+
readonly method: "GET" | "POST";
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly args?: AccessVerbSchema;
|
|
18
|
+
readonly result: AccessVerbSchema;
|
|
19
|
+
call(input: {
|
|
20
|
+
readonly args: unknown;
|
|
21
|
+
readonly headers: RequestHeaders;
|
|
22
|
+
readonly request?: unknown;
|
|
23
|
+
}): Promise<unknown>;
|
|
24
|
+
};
|
|
25
|
+
/** The verbs `access.client` publishes: each one the instance serves. */
|
|
26
|
+
type AccessClientVerbs = {
|
|
27
|
+
signIn?: AccessClientVerb;
|
|
28
|
+
signUp?: AccessClientVerb;
|
|
29
|
+
signOut?: AccessClientVerb;
|
|
30
|
+
resetPassword?: AccessClientVerb;
|
|
31
|
+
me?: AccessClientVerb;
|
|
32
|
+
} & {
|
|
33
|
+
[V in AccessKioskVerbName]?: AccessClientVerb;
|
|
34
|
+
};
|
|
35
|
+
/** `access.client`. */
|
|
36
|
+
type AccessClient = {
|
|
37
|
+
readonly url: string;
|
|
38
|
+
readonly verbs: Readonly<AccessClientVerbs>;
|
|
39
|
+
};
|
|
40
|
+
/** Better Auth's refusal, as `call` throws one: a status, and a sentence. */
|
|
41
|
+
/** A refusal's body: Better Auth's `{ message, code }`. */
|
|
42
|
+
type RefusalBody = {
|
|
43
|
+
readonly message?: unknown;
|
|
44
|
+
readonly code?: unknown;
|
|
45
|
+
};
|
|
46
|
+
type RefusalError = new (status: string | number, body: RefusalBody, headers?: {
|
|
47
|
+
readonly [name: string]: string;
|
|
48
|
+
}) => Error;
|
|
49
|
+
/**
|
|
50
|
+
* Builds `access.client` for an instance: from its Better Auth options (`baseURL`,
|
|
51
|
+
* `basePath`, `emailAndPassword`, `disabledPaths`), its profile and its declared
|
|
52
|
+
* claims.
|
|
53
|
+
*/
|
|
54
|
+
export declare const accessClient: (input: {
|
|
55
|
+
readonly block: {
|
|
56
|
+
readonly [key: string]: unknown;
|
|
57
|
+
};
|
|
58
|
+
readonly kind: AccessProfileKind;
|
|
59
|
+
readonly claims: AccessClaimsDeclaration;
|
|
60
|
+
/** Better Auth's base URL resolution, as `getBaseURL` resolves it. */
|
|
61
|
+
readonly baseOf: (baseURL: string | undefined, basePath: string | undefined) => string | undefined;
|
|
62
|
+
readonly isDynamicBaseURL: (baseURL: unknown) => boolean;
|
|
63
|
+
/** Better Auth's server API, built on first use. */
|
|
64
|
+
readonly api: () => unknown;
|
|
65
|
+
/** The request's identity, as `access.state` reads it. */
|
|
66
|
+
readonly identityOf: (headers: RequestHeaders, request: unknown) => Promise<unknown>;
|
|
67
|
+
/** Where `call` registers the requests it builds, for the Access plugin's origin check. */
|
|
68
|
+
readonly calls: WeakSet<object>;
|
|
69
|
+
readonly refusal: RefusalError;
|
|
70
|
+
/** How the sign-in route reads its identifier from a body (`email:<address>`). */
|
|
71
|
+
readonly identify: SignInIdentifier;
|
|
72
|
+
/** The rate limit on `signIn`, `signUp` and `resetPassword`: throws Better Auth's 429, else answers what clears the target's count once the call succeeds. */
|
|
73
|
+
readonly limit: (call: LimitedCall) => Promise<LimitSettle | undefined>;
|
|
74
|
+
/** Better Auth's context, once Better Auth is built: its plugins' `onRequest` hooks run with it. */
|
|
75
|
+
readonly context: () => Promise<unknown>;
|
|
76
|
+
}) => AccessClient;
|
|
77
|
+
/** Better Auth's `createAuthEndpoint`, as Access's `me` route calls it. */
|
|
78
|
+
export type MeEndpointFactory = (path: string, options: {
|
|
79
|
+
readonly method: "GET";
|
|
80
|
+
}, handler: (context: {
|
|
81
|
+
readonly headers?: Headers | undefined;
|
|
82
|
+
readonly request?: unknown;
|
|
83
|
+
}) => Promise<Response>) => unknown;
|
|
84
|
+
/**
|
|
85
|
+
* Access's `GET /me` route (under Better Auth's base path), keyed as Better Auth keys
|
|
86
|
+
* a plugin's endpoints (`access.api.accessMe`): the request's identity, `{ sub,
|
|
87
|
+
* claims }`, or `null`, as `access.client.verbs.me.call` answers it. Never cached.
|
|
88
|
+
*/
|
|
89
|
+
export declare const meEndpoint: (endpoint: MeEndpointFactory, identityOf: (headers: RequestHeaders, request: unknown) => Promise<unknown>) => {
|
|
90
|
+
accessMe: unknown;
|
|
91
|
+
};
|
|
92
|
+
export {};
|