@aventara/access 0.0.0-stage → 1.0.0-rc.1
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 +877 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +2536 -2
- package/dist/access/access-options.d.ts +74 -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 +275 -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 +22 -0
- package/dist/access/principal.subject.js +89 -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 +266 -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 +4 -0
- package/dist/access-warning.emitter.js +24 -0
- package/dist/access.builder.d.ts +340 -0
- package/dist/access.builder.js +75 -0
- package/dist/access.engine.d.ts +155 -0
- package/dist/access.engine.js +477 -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 +80 -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 +101 -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 +6 -0
- package/dist/foreign-references.check.js +131 -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 +73 -0
- package/dist/generator/prisma7/prisma-where.translator.js +138 -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 +13 -0
- package/dist/mcp/agent-scope.gate.js +75 -0
- package/dist/mcp/agent.grant.d.ts +40 -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 +219 -0
- package/dist/policies-contract.js +8 -0
- package/dist/policies.compiler.d.ts +38 -0
- package/dist/policies.compiler.js +1433 -0
- package/dist/policies.decider.d.ts +103 -0
- package/dist/policies.decider.js +390 -0
- package/dist/policies.hash.d.ts +10 -0
- package/dist/policies.hash.js +54 -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/read-governed-unique.check.d.ts +3 -0
- package/dist/read-governed-unique.check.js +21 -0
- package/dist/reference-reads.check.d.ts +16 -0
- package/dist/reference-reads.check.js +158 -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 +221 -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 +96 -0
- package/dist/resources/auth-tables.catalog.js +110 -0
- package/dist/resources/auth-tables.guard.d.ts +106 -0
- package/dist/resources/auth-tables.guard.js +462 -0
- package/dist/scope.resolver.d.ts +144 -0
- package/dist/scope.resolver.js +529 -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 +102 -3
- package/skill/SKILL.md +170 -0
- package/skill/reference/callers.md +99 -0
- package/skill/reference/errors.md +89 -0
- package/skill/reference/recipes.md +281 -0
- package/skill/reference/rules.md +153 -0
- package/skill/reference/setup.md +334 -0
- 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 +243 -0
- package/src/policies-contract.ts +215 -0
- package/src/policies.decider.ts +662 -0
- package/src/predicate.evaluator.ts +574 -0
- package/src/scope.resolver.ts +819 -0
- package/src/sha256.digest.ts +85 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// The registry, as `avaccess` reaches it: its read-only routes and its push route,
|
|
2
|
+
// over HTTP, with the namespace's key in the `x-access-key` header. The CLI stores
|
|
3
|
+
// nothing of its own; every answer is the registry's. A revision's file is hashed
|
|
4
|
+
// again here, so a file is never written or compared under a hash it does not have.
|
|
5
|
+
//
|
|
6
|
+
// The registry's address is `--registry` or `AVACCESS_REGISTRY_URL`: the Access
|
|
7
|
+
// server's URL, with Better Auth's base path the way Better Auth's own client reads
|
|
8
|
+
// it (a URL with no path gets `/api/auth`). It is `https:`, or `http:` on a loopback
|
|
9
|
+
// address only, since the key travels in a header. The key is `AVACCESS_KEY` only,
|
|
10
|
+
// never a flag, so it stays out of shell history and process lists, and no line the
|
|
11
|
+
// CLI prints quotes it. The CLI follows no redirect: a key goes to the address it
|
|
12
|
+
// was given and nowhere else. What the registry answers is data: a file is held to
|
|
13
|
+
// its hash and to the namespace asked for, and its text is printed without the
|
|
14
|
+
// control characters a terminal would act on.
|
|
15
|
+
import { guardrailsHash } from "../guardrails/guardrails.hash.js";
|
|
16
|
+
import { isNamespace } from "../policies.compiler.js";
|
|
17
|
+
import { policiesHash } from "../policies.hash.js";
|
|
18
|
+
import { KEY_HEADER, KEY_OWNER_PATH, REGISTRY_PATH, } from "../registry/registry.endpoints.js";
|
|
19
|
+
import { CliFailure, EXIT_ERROR, EXIT_FAILED, messageOf, stringFlag, } from "./cli.context.js";
|
|
20
|
+
/** The environment variable that holds the registry's URL, when `--registry` does not. */
|
|
21
|
+
export const REGISTRY_URL_VARIABLE = "AVACCESS_REGISTRY_URL";
|
|
22
|
+
/** The environment variable that holds the namespace's key. */
|
|
23
|
+
export const KEY_VARIABLE = "AVACCESS_KEY";
|
|
24
|
+
/** Better Auth's default base path, added to a URL that has none. */
|
|
25
|
+
const DEFAULT_BASE_PATH = "/api/auth";
|
|
26
|
+
/** A revision's hash, as the registry names it. */
|
|
27
|
+
const HASH = /^sha256:[0-9a-f]{64}$/;
|
|
28
|
+
/**
|
|
29
|
+
* The characters a terminal may act on or that change how the text around them
|
|
30
|
+
* reads: the control characters (`Cc`, U+0000 to U+001F and U+007F to U+009F), the
|
|
31
|
+
* format characters (`Cf`: bidirectional overrides and isolates such as U+202E and
|
|
32
|
+
* U+2066, zero-width ones, U+FEFF), the line and paragraph separators (U+2028,
|
|
33
|
+
* U+2029) and lone surrogates.
|
|
34
|
+
*/
|
|
35
|
+
const CONTROL = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Cs}]/gu;
|
|
36
|
+
/** Text the registry sent, as the CLI prints it: none of those characters, at most 2,000 characters. */
|
|
37
|
+
const printable = (text) => {
|
|
38
|
+
const plain = text.replace(CONTROL, " ");
|
|
39
|
+
return plain.length > 2_000 ? `${plain.slice(0, 2_000)}…` : plain;
|
|
40
|
+
};
|
|
41
|
+
/** Whether a URL's host is a loopback address: `localhost`, 127.0.0.0/8 or `::1`. */
|
|
42
|
+
const isLoopback = (url) => url.hostname === "localhost" ||
|
|
43
|
+
url.hostname === "[::1]" ||
|
|
44
|
+
/^127\.(?:\d{1,3})\.(?:\d{1,3})\.(?:\d{1,3})$/.test(url.hostname);
|
|
45
|
+
/** The registry's base URL: the Access server's URL and Better Auth's base path. */
|
|
46
|
+
const baseUrlOf = (context) => {
|
|
47
|
+
const given = stringFlag(context, "registry") ?? context.env[REGISTRY_URL_VARIABLE];
|
|
48
|
+
if (given === undefined || given === "") {
|
|
49
|
+
throw new CliFailure(EXIT_ERROR, `no registry: pass --registry <url> or set ${REGISTRY_URL_VARIABLE} to the Access server's URL (such as https://access.example.com)`);
|
|
50
|
+
}
|
|
51
|
+
let url;
|
|
52
|
+
try {
|
|
53
|
+
url = new URL(given);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
throw new CliFailure(EXIT_ERROR, `the registry's URL ${JSON.stringify(given)} is not a URL`);
|
|
57
|
+
}
|
|
58
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
59
|
+
throw new CliFailure(EXIT_ERROR, `the registry's URL is http or https; it was ${url.protocol}`);
|
|
60
|
+
}
|
|
61
|
+
if (url.protocol === "http:" && !isLoopback(url)) {
|
|
62
|
+
throw new CliFailure(EXIT_ERROR, `the registry's URL ${url.origin} is http:, which sends the key in the clear: use its https: URL (http: is taken only on a loopback address: localhost, 127.0.0.0/8 or [::1])`);
|
|
63
|
+
}
|
|
64
|
+
if (url.username !== "" || url.password !== "") {
|
|
65
|
+
throw new CliFailure(EXIT_ERROR, "the registry's URL holds a user name or password; the registry takes its key in AVACCESS_KEY only");
|
|
66
|
+
}
|
|
67
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
68
|
+
return `${url.origin}${path === "" ? DEFAULT_BASE_PATH : path}`;
|
|
69
|
+
};
|
|
70
|
+
const keyOf = (context) => {
|
|
71
|
+
const key = context.env[KEY_VARIABLE];
|
|
72
|
+
if (key === undefined || key === "") {
|
|
73
|
+
throw new CliFailure(EXIT_ERROR, `no registry key: set ${KEY_VARIABLE} to the namespace's key (its pull key, or its push key to push)`);
|
|
74
|
+
}
|
|
75
|
+
return key;
|
|
76
|
+
};
|
|
77
|
+
/** The registry the context names, with its key. */
|
|
78
|
+
export const registryClient = (context) => {
|
|
79
|
+
const base = baseUrlOf(context);
|
|
80
|
+
const key = keyOf(context);
|
|
81
|
+
/** One call; a refusal names the registry's code and message, never the key. */
|
|
82
|
+
const call = async (method, path, body, under = REGISTRY_PATH) => {
|
|
83
|
+
const url = `${base}${under}${path}`;
|
|
84
|
+
let response;
|
|
85
|
+
try {
|
|
86
|
+
response = await fetch(url, {
|
|
87
|
+
method,
|
|
88
|
+
// A redirect is answered, never followed: following it would send the
|
|
89
|
+
// key, and a push's file, to the address it names.
|
|
90
|
+
redirect: "manual",
|
|
91
|
+
headers: {
|
|
92
|
+
[KEY_HEADER]: key,
|
|
93
|
+
accept: "application/json",
|
|
94
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
95
|
+
},
|
|
96
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
throw new CliFailure(EXIT_ERROR, `the registry at ${base} did not answer (${messageOf(error.cause ?? error)})`);
|
|
101
|
+
}
|
|
102
|
+
if (response.type === "opaqueredirect" ||
|
|
103
|
+
(response.status >= 300 && response.status < 400)) {
|
|
104
|
+
const location = response.headers.get("location");
|
|
105
|
+
throw new CliFailure(EXIT_ERROR, `the registry at ${base} answered ${response.status}, a redirect${location === null ? "" : ` to ${printable(location)}`}. avaccess follows no redirect, so the key is sent to no other address: set --registry or ${REGISTRY_URL_VARIABLE} to the registry's own URL`);
|
|
106
|
+
}
|
|
107
|
+
const text = await response.text();
|
|
108
|
+
let json;
|
|
109
|
+
try {
|
|
110
|
+
json = text === "" ? null : JSON.parse(text);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
json = undefined;
|
|
114
|
+
}
|
|
115
|
+
if (response.status === 404 &&
|
|
116
|
+
typeof json?.code !== "string") {
|
|
117
|
+
throw new CliFailure(EXIT_ERROR, `${base} serves no registry: is it the Access server's URL, with the registry on (\`registry: { enabled: true }\`)?`);
|
|
118
|
+
}
|
|
119
|
+
return { status: response.status, json };
|
|
120
|
+
};
|
|
121
|
+
const refused = (answer) => {
|
|
122
|
+
const body = (answer.json ?? {});
|
|
123
|
+
const code = typeof body.code === "string" ? printable(body.code) : undefined;
|
|
124
|
+
const message = typeof body.message === "string"
|
|
125
|
+
? printable(messageOf(body.message))
|
|
126
|
+
: `the registry answered ${answer.status}`;
|
|
127
|
+
throw new CliFailure(answer.status >= 500 ? EXIT_ERROR : EXIT_FAILED, message, code);
|
|
128
|
+
};
|
|
129
|
+
const segment = (text) => encodeURIComponent(text);
|
|
130
|
+
const hashed = (json, hash, what) => {
|
|
131
|
+
const revision = json;
|
|
132
|
+
let found;
|
|
133
|
+
try {
|
|
134
|
+
found = hash(revision.file);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
throw new CliFailure(EXIT_FAILED, `the registry served ${what} that is no file the engine runs from (${printable(messageOf(error))})`);
|
|
138
|
+
}
|
|
139
|
+
if (found !== revision.hash) {
|
|
140
|
+
throw new CliFailure(EXIT_FAILED, `the registry served ${what} under ${printable(String(revision.hash))}, and it hashes to ${found}`);
|
|
141
|
+
}
|
|
142
|
+
return { hash: found, file: revision.file };
|
|
143
|
+
};
|
|
144
|
+
/** A namespace's file, held to the namespace it was asked for. */
|
|
145
|
+
const namespaced = (pulled, namespace, what) => {
|
|
146
|
+
if (pulled.file.namespace !== namespace) {
|
|
147
|
+
throw new CliFailure(EXIT_FAILED, `the registry served ${what} as a file of ${pulled.file.namespace === undefined
|
|
148
|
+
? "no namespace"
|
|
149
|
+
: `namespace ${JSON.stringify(pulled.file.namespace)}`}: a namespace's revision is its own file. Nothing was written`);
|
|
150
|
+
}
|
|
151
|
+
return pulled;
|
|
152
|
+
};
|
|
153
|
+
/** A hash the registry named, checked to be one. */
|
|
154
|
+
const hashNamed = (value, what) => {
|
|
155
|
+
if (typeof value !== "string" || !HASH.test(value)) {
|
|
156
|
+
throw new CliFailure(EXIT_FAILED, `the registry named ${what} with no sha256 hash`);
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
};
|
|
160
|
+
return {
|
|
161
|
+
url: base,
|
|
162
|
+
/** The namespace the key belongs to, as the registry names it. */
|
|
163
|
+
keyNamespace: async () => {
|
|
164
|
+
const answer = await call("GET", "", undefined, KEY_OWNER_PATH);
|
|
165
|
+
if (answer.status !== 200) {
|
|
166
|
+
return refused(answer);
|
|
167
|
+
}
|
|
168
|
+
const namespace = answer.json
|
|
169
|
+
?.namespace;
|
|
170
|
+
if (!isNamespace(namespace)) {
|
|
171
|
+
throw new CliFailure(EXIT_FAILED, "the registry did not name the key's namespace");
|
|
172
|
+
}
|
|
173
|
+
return namespace;
|
|
174
|
+
},
|
|
175
|
+
/** The hash of the namespace's current beacon (`null`: it has pushed none). */
|
|
176
|
+
namespaceBeacon: async (namespace) => {
|
|
177
|
+
const answer = await call("GET", `/${segment(namespace)}`);
|
|
178
|
+
if (answer.status !== 200) {
|
|
179
|
+
return refused(answer);
|
|
180
|
+
}
|
|
181
|
+
const beacon = answer.json
|
|
182
|
+
?.beacon;
|
|
183
|
+
return beacon === null || beacon === undefined
|
|
184
|
+
? null
|
|
185
|
+
: hashNamed(beacon, `namespace "${namespace}"'s beacon`);
|
|
186
|
+
},
|
|
187
|
+
/** The namespace's current revision's hash (`null`: nothing pushed yet). */
|
|
188
|
+
headHash: async (namespace) => {
|
|
189
|
+
const answer = await call("GET", `/${segment(namespace)}`);
|
|
190
|
+
if (answer.status !== 200) {
|
|
191
|
+
return refused(answer);
|
|
192
|
+
}
|
|
193
|
+
const head = answer.json?.head;
|
|
194
|
+
return head === null || head === undefined
|
|
195
|
+
? null
|
|
196
|
+
: hashNamed(head, `namespace "${namespace}"'s head`);
|
|
197
|
+
},
|
|
198
|
+
/** The namespace's current revision, with its file. */
|
|
199
|
+
head: async (namespace) => {
|
|
200
|
+
const answer = await call("GET", `/${segment(namespace)}/head`);
|
|
201
|
+
if (answer.status !== 200) {
|
|
202
|
+
return refused(answer);
|
|
203
|
+
}
|
|
204
|
+
return namespaced(hashed(answer.json, policiesHash, `namespace "${namespace}"'s head`), namespace, `namespace "${namespace}"'s head`);
|
|
205
|
+
},
|
|
206
|
+
/** One of the namespace's revisions, with its file. */
|
|
207
|
+
revision: async (namespace, hash) => {
|
|
208
|
+
const answer = await call("GET", `/${segment(namespace)}/revisions/${segment(hash)}`);
|
|
209
|
+
if (answer.status !== 200) {
|
|
210
|
+
return refused(answer);
|
|
211
|
+
}
|
|
212
|
+
return namespaced(hashed(answer.json, policiesHash, `revision ${hash}`), namespace, `revision ${hash}`);
|
|
213
|
+
},
|
|
214
|
+
/** The namespace's revisions, newest first. */
|
|
215
|
+
revisions: async (namespace) => {
|
|
216
|
+
const answer = await call("GET", `/${segment(namespace)}/revisions`);
|
|
217
|
+
if (answer.status !== 200) {
|
|
218
|
+
return refused(answer);
|
|
219
|
+
}
|
|
220
|
+
const revisions = answer.json
|
|
221
|
+
?.revisions;
|
|
222
|
+
if (!Array.isArray(revisions)) {
|
|
223
|
+
throw new CliFailure(EXIT_FAILED, `the registry did not list namespace "${namespace}"'s revisions`);
|
|
224
|
+
}
|
|
225
|
+
return revisions.map((entry) => ({
|
|
226
|
+
hash: hashNamed(entry?.hash, `a revision of namespace "${namespace}"`),
|
|
227
|
+
createdAt: printable(String(entry?.createdAt ?? "")),
|
|
228
|
+
}));
|
|
229
|
+
},
|
|
230
|
+
/** The org's current guardrails, with their file (`null`: none pushed yet). */
|
|
231
|
+
guardrails: async (namespace) => {
|
|
232
|
+
const answer = await call("GET", `/${segment(namespace)}/guardrails`);
|
|
233
|
+
if (answer.status === 404) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
if (answer.status !== 200) {
|
|
237
|
+
return refused(answer);
|
|
238
|
+
}
|
|
239
|
+
return hashed(answer.json, guardrailsHash, "the org's guardrails");
|
|
240
|
+
},
|
|
241
|
+
/**
|
|
242
|
+
* What `namespace` reads of `owner` as foreign: the hash of `owner`'s beacon,
|
|
243
|
+
* and its schema entry of each of `resources`. Each entry is the owner's own,
|
|
244
|
+
* for a Resource asked for; anything else is refused.
|
|
245
|
+
*/
|
|
246
|
+
foreign: async (namespace, owner, resources) => {
|
|
247
|
+
const query = resources.length === 0
|
|
248
|
+
? ""
|
|
249
|
+
: `?resources=${resources.map(segment).join(",")}`;
|
|
250
|
+
const answer = await call("GET", `/${segment(namespace)}/foreign/${segment(owner)}${query}`);
|
|
251
|
+
if (answer.status !== 200) {
|
|
252
|
+
return refused(answer);
|
|
253
|
+
}
|
|
254
|
+
const body = (answer.json ?? {});
|
|
255
|
+
const entries = body.resources;
|
|
256
|
+
if (body.owner !== owner ||
|
|
257
|
+
(body.beacon !== null &&
|
|
258
|
+
(typeof body.beacon !== "string" || !HASH.test(body.beacon))) ||
|
|
259
|
+
typeof entries !== "object" ||
|
|
260
|
+
entries === null ||
|
|
261
|
+
Array.isArray(entries) ||
|
|
262
|
+
Object.keys(entries).some((name) => !resources.includes(name)) ||
|
|
263
|
+
resources.some((name) => !Object.hasOwn(entries, name))) {
|
|
264
|
+
throw new CliFailure(EXIT_FAILED, `the registry answered what namespace "${namespace}" reads of "${owner}" with something else than the beacon's hash and the Resources asked for. Nothing was written`);
|
|
265
|
+
}
|
|
266
|
+
return {
|
|
267
|
+
beacon: body.beacon,
|
|
268
|
+
resources: entries,
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
/**
|
|
272
|
+
* Pushes the namespace's file, with the beacons its service read when there
|
|
273
|
+
* are; answers its hash and whether the registry stored it now.
|
|
274
|
+
*/
|
|
275
|
+
push: async (namespace, file, beacons) => {
|
|
276
|
+
const answer = await call("POST", `/${segment(namespace)}/revisions`, beacons === undefined ? file : { file, beacons });
|
|
277
|
+
if (answer.status !== 200 && answer.status !== 201) {
|
|
278
|
+
return refused(answer);
|
|
279
|
+
}
|
|
280
|
+
const pushed = answer.json;
|
|
281
|
+
return {
|
|
282
|
+
hash: hashNamed(pushed?.hash, "the pushed revision"),
|
|
283
|
+
created: pushed?.created === true,
|
|
284
|
+
};
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
};
|
|
@@ -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");
|