@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,166 @@
|
|
|
1
|
+
// The `avaccess` program: the command line read, the command run, its refusal printed
|
|
2
|
+
// and its exit code returned. The bin (`avaccess.bin.ts`) only calls it.
|
|
3
|
+
import { parseArgs } from "node:util";
|
|
4
|
+
import { CLIENT_TRANSLATORS, ORM_TRANSLATORS, } from "../generator/orm.translator.js";
|
|
5
|
+
import { ENV_VARIABLE } from "./beacon-pins.file.js";
|
|
6
|
+
import { CliFailure, EXIT_ERROR, EXIT_OK, messageOf, versionOf, } from "./cli.context.js";
|
|
7
|
+
import { diffCommand } from "./diff.command.js";
|
|
8
|
+
import { generateCommand } from "./generate.command.js";
|
|
9
|
+
import { keysCommand } from "./keys.command.js";
|
|
10
|
+
import { pullCommand } from "./pull.command.js";
|
|
11
|
+
import { pushCommand } from "./push.command.js";
|
|
12
|
+
import { KEY_VARIABLE, REGISTRY_URL_VARIABLE } from "./registry.client.js";
|
|
13
|
+
import { schemaCommand } from "./schema.command.js";
|
|
14
|
+
import { MIGRATE_VARIABLE, PORT_VARIABLE, serveCommand, } from "./serve.command.js";
|
|
15
|
+
import { statusCommand } from "./status.command.js";
|
|
16
|
+
import { verifyCommand } from "./verify.command.js";
|
|
17
|
+
const REGISTRY = { type: "string" };
|
|
18
|
+
const NAMESPACE = { type: "string" };
|
|
19
|
+
const ORM = { type: "string" };
|
|
20
|
+
const BEACON = { type: "string" };
|
|
21
|
+
const ENV = { type: "string" };
|
|
22
|
+
/** Each command, what it runs, the flags it takes and its line in the help. */
|
|
23
|
+
const COMMANDS = {
|
|
24
|
+
push: {
|
|
25
|
+
run: pushCommand,
|
|
26
|
+
flags: { registry: REGISTRY, beacon: BEACON },
|
|
27
|
+
help: "push <rules> [--beacon <module>] push the namespace's policies file (a .json file, or the module holding the rules); --beacon sends the namespace's beacon and those of the namespaces it reads as foreign",
|
|
28
|
+
},
|
|
29
|
+
pull: {
|
|
30
|
+
run: pullCommand,
|
|
31
|
+
flags: {
|
|
32
|
+
registry: REGISTRY,
|
|
33
|
+
namespace: NAMESPACE,
|
|
34
|
+
out: { type: "string" },
|
|
35
|
+
foreign: { type: "string", multiple: true },
|
|
36
|
+
beacon: BEACON,
|
|
37
|
+
env: ENV,
|
|
38
|
+
},
|
|
39
|
+
help: "pull [--namespace <ns>] [--out <dir>] [--foreign <ns>:<Resource>,…] [--beacon <module>] [--env <env>] pull the namespace's file and the org's guardrails, and the foreign facts --foreign names; pin them in avAccess.lock.json, and the beacons of the namespaces read as foreign (checked through --beacon) in avAccess.beacons.<env>.json",
|
|
40
|
+
},
|
|
41
|
+
status: {
|
|
42
|
+
run: statusCommand,
|
|
43
|
+
flags: { registry: REGISTRY },
|
|
44
|
+
help: "status is avAccess.lock.json behind the registry? (exit 1 when it is)",
|
|
45
|
+
},
|
|
46
|
+
diff: {
|
|
47
|
+
run: diffCommand,
|
|
48
|
+
flags: { registry: REGISTRY, namespace: NAMESPACE },
|
|
49
|
+
help: "diff [<from> [<to>]] [--namespace <ns>] the rules that changed between two revisions (lock, head, a hash); default lock → head",
|
|
50
|
+
},
|
|
51
|
+
verify: {
|
|
52
|
+
run: verifyCommand,
|
|
53
|
+
flags: { env: ENV },
|
|
54
|
+
help: "verify [--env <env>] the pulled files against avAccess.lock.json, and each avAccess.beacons.<env>.json against the rules, offline (exit 1 on a changed file)",
|
|
55
|
+
},
|
|
56
|
+
generate: {
|
|
57
|
+
run: generateCommand,
|
|
58
|
+
flags: { orm: ORM, out: { type: "string" }, tsconfig: { type: "string" } },
|
|
59
|
+
help: "generate [--orm <orm>] [--out <file>] [--tsconfig <file>] the client of a service not on Aventara, from the pulled files: --out and <name>.internal.ts beside it (default orm: none; default out: src/generated/avAccess.ts)",
|
|
60
|
+
},
|
|
61
|
+
schema: {
|
|
62
|
+
run: schemaCommand,
|
|
63
|
+
flags: { orm: ORM, schema: { type: "string" }, out: { type: "string" } },
|
|
64
|
+
help: "schema --orm <orm> [--schema <path>] [--out <file>] the AccessSchema of the ORM's schema, to author rules with definePolicies (default out: src/generated/avAccess.schema.ts)",
|
|
65
|
+
},
|
|
66
|
+
serve: {
|
|
67
|
+
run: serveCommand,
|
|
68
|
+
flags: {
|
|
69
|
+
config: { type: "string" },
|
|
70
|
+
port: { type: "string" },
|
|
71
|
+
hostname: { type: "string" },
|
|
72
|
+
"max-body-bytes": { type: "string" },
|
|
73
|
+
migrate: { type: "boolean" },
|
|
74
|
+
},
|
|
75
|
+
help: "serve [--config <file>] [--port <port>] [--hostname <host>] [--max-body-bytes <bytes>] [--migrate] the standalone host, serving access.config.ts's instance on 127.0.0.1 unless --hostname says (--migrate or AVACCESS_MIGRATE=true runs Better Auth's migration first)",
|
|
76
|
+
},
|
|
77
|
+
keys: {
|
|
78
|
+
run: keysCommand,
|
|
79
|
+
flags: { server: { type: "string" } },
|
|
80
|
+
help: "keys issue <ns> | rotate <ns> <push|pull> | revoke <ns> <push|pull> --server <module> the operator's keys",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const isCommand = (name) => Object.hasOwn(COMMANDS, name);
|
|
84
|
+
const HELP = [
|
|
85
|
+
"avaccess: the Aventara Access registry, from a project",
|
|
86
|
+
"",
|
|
87
|
+
"usage: avaccess <command> [arguments] [flags]",
|
|
88
|
+
"",
|
|
89
|
+
...Object.values(COMMANDS).map((command) => ` ${command.help}`),
|
|
90
|
+
"",
|
|
91
|
+
`The registry: --registry <url> or ${REGISTRY_URL_VARIABLE} (the Access server's URL).`,
|
|
92
|
+
`The key: ${KEY_VARIABLE}, the namespace's push or pull key (never a flag).`,
|
|
93
|
+
`The environment (beacon pins): --env <env> or ${ENV_VARIABLE}.`,
|
|
94
|
+
`serve: the port from --port or ${PORT_VARIABLE} (default 3000); the migration on start only with --migrate or ${MIGRATE_VARIABLE}=true.`,
|
|
95
|
+
`The translators (--orm): for generate ${Object.keys(CLIENT_TRANSLATORS).sort().join(", ")} (default none); for schema, required, ${Object.keys(ORM_TRANSLATORS).sort().join(", ")}.`,
|
|
96
|
+
"Exit codes: 0 done; 1 the check failed or the registry refused; 2 the command could not run.",
|
|
97
|
+
].join("\n");
|
|
98
|
+
/** The control characters but the line feed (U+0000 to U+001F, U+007F to U+009F): what a terminal may act on. */
|
|
99
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: the characters it removes
|
|
100
|
+
const TERMINAL_CONTROL = /[\u0000-\u0009\u000b-\u001f\u007f-\u009f]/g;
|
|
101
|
+
/** A line as the program prints it: whatever text it carries, no terminal control. */
|
|
102
|
+
const plain = (line) => line.replace(TERMINAL_CONTROL, " ");
|
|
103
|
+
/** Never resolves: a program run without a way to be stopped serves until its process ends. */
|
|
104
|
+
const NEVER_STOPPED = () => new Promise(() => { });
|
|
105
|
+
/**
|
|
106
|
+
* Runs `avaccess` with `args` (the command line after the bin's name) in `cwd`, and
|
|
107
|
+
* answers its exit code. `untilStopped` resolves when a command that runs until
|
|
108
|
+
* stopped (`serve`) should stop; the bin resolves it on SIGTERM or SIGINT.
|
|
109
|
+
*/
|
|
110
|
+
export const runAvaccess = async (args, cwd, env, printed, untilStopped = NEVER_STOPPED) => {
|
|
111
|
+
const output = {
|
|
112
|
+
out: (line) => printed.out(plain(line)),
|
|
113
|
+
err: (line) => printed.err(plain(line)),
|
|
114
|
+
};
|
|
115
|
+
const [name, ...rest] = args;
|
|
116
|
+
if (name === undefined ||
|
|
117
|
+
name === "help" ||
|
|
118
|
+
name === "--help" ||
|
|
119
|
+
name === "-h") {
|
|
120
|
+
output.out(HELP);
|
|
121
|
+
return name === undefined ? EXIT_ERROR : EXIT_OK;
|
|
122
|
+
}
|
|
123
|
+
if (name === "--version" || name === "-v") {
|
|
124
|
+
output.out(await versionOf());
|
|
125
|
+
return EXIT_OK;
|
|
126
|
+
}
|
|
127
|
+
if (!isCommand(name)) {
|
|
128
|
+
output.err(`avaccess: no command ${JSON.stringify(name)}\n\n${HELP}`);
|
|
129
|
+
return EXIT_ERROR;
|
|
130
|
+
}
|
|
131
|
+
const command = COMMANDS[name];
|
|
132
|
+
try {
|
|
133
|
+
let parsed;
|
|
134
|
+
try {
|
|
135
|
+
parsed = parseArgs({
|
|
136
|
+
args: rest,
|
|
137
|
+
options: { ...command.flags, help: { type: "boolean", short: "h" } },
|
|
138
|
+
allowPositionals: true,
|
|
139
|
+
strict: true,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
throw new CliFailure(EXIT_ERROR, `${messageOf(error)}\nusage: avaccess ${command.help}`);
|
|
144
|
+
}
|
|
145
|
+
if (parsed.values.help === true) {
|
|
146
|
+
output.out(`usage: avaccess ${command.help}`);
|
|
147
|
+
return EXIT_OK;
|
|
148
|
+
}
|
|
149
|
+
return await command.run({
|
|
150
|
+
cwd,
|
|
151
|
+
env,
|
|
152
|
+
flags: parsed.values,
|
|
153
|
+
positionals: parsed.positionals,
|
|
154
|
+
out: output.out,
|
|
155
|
+
untilStopped,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
if (error instanceof CliFailure) {
|
|
160
|
+
output.err(`avaccess ${name}: ${error.code === undefined ? "" : `${error.code}: `}${error.message}`);
|
|
161
|
+
return error.exitCode;
|
|
162
|
+
}
|
|
163
|
+
output.err(`avaccess ${name}: ${messageOf(error)}`);
|
|
164
|
+
return EXIT_ERROR;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CliContext } from "./cli.context.js";
|
|
2
|
+
/** The environment variable that names the environment, when `--env` does not. */
|
|
3
|
+
export declare const ENV_VARIABLE = "AVACCESS_ENV";
|
|
4
|
+
/** The pins file of `environment`, at the project's root. */
|
|
5
|
+
export declare const beaconPinsFile: (environment: string) => string;
|
|
6
|
+
/** The environment `--env` or `AVACCESS_ENV` selects; `undefined` when neither does. */
|
|
7
|
+
export declare const environmentOf: (context: CliContext) => string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Checks every `avAccess.beacons.<environment>.json` in the project against the
|
|
10
|
+
* lock's namespace and the `owners` its pulled file reads, and that `selected`'s is
|
|
11
|
+
* there when one is selected. Answers the environments checked. A file that fails
|
|
12
|
+
* the check fails it (`ACCESS_LOCK_MISMATCH`).
|
|
13
|
+
*/
|
|
14
|
+
export declare const verifyBeaconPins: (cwd: string, namespace: string, owners: readonly string[], selected: string | undefined) => Promise<readonly string[]>;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// `avAccess.beacons.<environment>.json`: one environment's beacon pins. A beacon is
|
|
2
|
+
// its database's own, so a staging and a production database have different ones;
|
|
3
|
+
// the pins live beside `avAccess.lock.json` in one file per environment, and the lock
|
|
4
|
+
// itself (the policy and foreign facts hashes) holds against any registry.
|
|
5
|
+
//
|
|
6
|
+
// The environment is `--env <name>`, else `AVACCESS_ENV`, as the registry's URL is
|
|
7
|
+
// `--registry` else `AVACCESS_REGISTRY_URL`. `avaccess pull` writes the selected
|
|
8
|
+
// environment's file; `avaccess verify` checks every one in the project, and the
|
|
9
|
+
// selected one must be there. At startup the app hands its environment's pins to
|
|
10
|
+
// `checkBeacons`, read from this file or from its own environment.
|
|
11
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { beaconPinsProblem, } from "../beacon/beacon.check.js";
|
|
14
|
+
import { CliFailure, EXIT_ERROR, EXIT_FAILED, messageOf, stringFlag, } from "./cli.context.js";
|
|
15
|
+
/** The environment variable that names the environment, when `--env` does not. */
|
|
16
|
+
export const ENV_VARIABLE = "AVACCESS_ENV";
|
|
17
|
+
const ENVIRONMENT = /^[a-z][a-z0-9_-]{0,62}$/;
|
|
18
|
+
const FILE = /^avAccess\.beacons\.([a-z][a-z0-9_-]{0,62})\.json$/;
|
|
19
|
+
/** The pins file of `environment`, at the project's root. */
|
|
20
|
+
export const beaconPinsFile = (environment) => `avAccess.beacons.${environment}.json`;
|
|
21
|
+
/** The environment `--env` or `AVACCESS_ENV` selects; `undefined` when neither does. */
|
|
22
|
+
export const environmentOf = (context) => {
|
|
23
|
+
const given = stringFlag(context, "env") ?? context.env[ENV_VARIABLE];
|
|
24
|
+
if (given === undefined || given === "") {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
if (!ENVIRONMENT.test(given)) {
|
|
28
|
+
throw new CliFailure(EXIT_ERROR, `the environment ${JSON.stringify(given)} is not an environment name (a lowercase letter, then lowercase letters, digits, "_" or "-"; such as "production")`);
|
|
29
|
+
}
|
|
30
|
+
return given;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Checks every `avAccess.beacons.<environment>.json` in the project against the
|
|
34
|
+
* lock's namespace and the `owners` its pulled file reads, and that `selected`'s is
|
|
35
|
+
* there when one is selected. Answers the environments checked. A file that fails
|
|
36
|
+
* the check fails it (`ACCESS_LOCK_MISMATCH`).
|
|
37
|
+
*/
|
|
38
|
+
export const verifyBeaconPins = async (cwd, namespace, owners, selected) => {
|
|
39
|
+
const environments = (await readdir(cwd, { withFileTypes: true }))
|
|
40
|
+
.filter((entry) => entry.isFile())
|
|
41
|
+
.map((entry) => FILE.exec(entry.name)?.[1])
|
|
42
|
+
.filter((name) => name !== undefined)
|
|
43
|
+
.sort();
|
|
44
|
+
if (selected !== undefined && !environments.includes(selected)) {
|
|
45
|
+
throw new CliFailure(EXIT_FAILED, `no ${beaconPinsFile(selected)}: the rules ${owners.length === 0 ? "need no beacon pins" : `read ${owners.map((owner) => `"${owner}"`).join(", ")} as foreign`}, and environment "${selected}" has no pins. Pull them with \`avaccess pull --env ${selected} --beacon <module>\``, "ACCESS_LOCK_MISMATCH");
|
|
46
|
+
}
|
|
47
|
+
for (const environment of environments) {
|
|
48
|
+
const path = beaconPinsFile(environment);
|
|
49
|
+
let pins;
|
|
50
|
+
try {
|
|
51
|
+
pins = JSON.parse(await readFile(resolve(cwd, path), "utf8"));
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw new CliFailure(EXIT_FAILED, `${path} is no longer JSON (${messageOf(error)}): pull it again with \`avaccess pull --env ${environment} --beacon <module>\``, "ACCESS_LOCK_MISMATCH");
|
|
55
|
+
}
|
|
56
|
+
const problem = beaconPinsProblem(namespace, owners, pins, environment);
|
|
57
|
+
if (problem !== undefined) {
|
|
58
|
+
throw new CliFailure(EXIT_FAILED, `${path}: ${problem}`, "ACCESS_LOCK_MISMATCH");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return environments;
|
|
62
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AccessBeaconStore } from "../beacon/beacon.store.js";
|
|
2
|
+
import { type CliContext } from "./cli.context.js";
|
|
3
|
+
/** The store `--beacon` names; `undefined` when the flag is not given. */
|
|
4
|
+
export declare const beaconStoreOf: (context: CliContext) => Promise<AccessBeaconStore | undefined>;
|
|
5
|
+
/**
|
|
6
|
+
* The namespace's own beacon, its row written the first time; a row the registry
|
|
7
|
+
* does not hold (`registered`) is refused, never adopted (`ensureBeacon`).
|
|
8
|
+
*/
|
|
9
|
+
export declare const ownBeacon: (store: AccessBeaconStore, namespace: string, registered: string | null) => Promise<string>;
|
|
10
|
+
/**
|
|
11
|
+
* `owner`'s beacon as the store's connection reads it. No row, or a row that holds
|
|
12
|
+
* no beacon, is refused here: the service does not reach `owner`'s database, and the
|
|
13
|
+
* registry would refuse the push for it.
|
|
14
|
+
*/
|
|
15
|
+
export declare const readOwnerBeacon: (store: AccessBeaconStore, owner: string) => Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Holds `owner`'s beacon, as the store reads it, to the hash `owner` pushed (`null`:
|
|
18
|
+
* none): the pull's check that this service reaches `owner`'s database.
|
|
19
|
+
*/
|
|
20
|
+
export declare const holdOwnerBeacon: (store: AccessBeaconStore, owner: string, pushed: string | null) => Promise<void>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// `--beacon <module>`: the project's beacon store, for `avaccess push` and `avaccess
|
|
2
|
+
// pull`. The module (TypeScript or JavaScript, loaded as `push <rules>` loads the
|
|
3
|
+
// rules) exports an `AccessBeaconStore` over the service's own data layer, as its
|
|
4
|
+
// default export or as `beacon`: `read(namespace)` reads a namespace's row in
|
|
5
|
+
// `_aventara_beacon`, `create(namespace, value)` writes one. The CLI uses it to write
|
|
6
|
+
// and read the namespace's own beacon, and to read the beacons of the namespaces its
|
|
7
|
+
// rules read as foreign, through the connection the service itself runs on. No line
|
|
8
|
+
// it prints quotes a beacon.
|
|
9
|
+
import { beaconHashOf } from "../beacon/beacon.check.js";
|
|
10
|
+
import { BEACON_TABLE, ensureBeacon, isBeacon, } from "../beacon/beacon.store.js";
|
|
11
|
+
import { isRecord } from "../policies.compiler.js";
|
|
12
|
+
import { CliFailure, codeOf, EXIT_ERROR, EXIT_FAILED, messageOf, stringFlag, } from "./cli.context.js";
|
|
13
|
+
import { loadProjectModule, pickExport } from "./module.loader.js";
|
|
14
|
+
const isStore = (value) => isRecord(value) &&
|
|
15
|
+
typeof value.read === "function" &&
|
|
16
|
+
typeof value.create === "function";
|
|
17
|
+
/** The store `--beacon` names; `undefined` when the flag is not given. */
|
|
18
|
+
export const beaconStoreOf = async (context) => {
|
|
19
|
+
const path = stringFlag(context, "beacon");
|
|
20
|
+
if (path === undefined) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const exports = await loadProjectModule(context.cwd, path);
|
|
24
|
+
return pickExport(exports, path, "beacon", isStore, "beacon store (`{ read(namespace), create(namespace, value) }`)");
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The namespace's own beacon, its row written the first time; a row the registry
|
|
28
|
+
* does not hold (`registered`) is refused, never adopted (`ensureBeacon`).
|
|
29
|
+
*/
|
|
30
|
+
export const ownBeacon = async (store, namespace, registered) => {
|
|
31
|
+
try {
|
|
32
|
+
return (await ensureBeacon(store, namespace, registered)).value;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw new CliFailure(codeOf(error) === undefined ? EXIT_ERROR : EXIT_FAILED, `namespace "${namespace}"'s own beacon could not be read or written through the --beacon store (${messageOf(error)})`, codeOf(error));
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* `owner`'s beacon as the store's connection reads it. No row, or a row that holds
|
|
40
|
+
* no beacon, is refused here: the service does not reach `owner`'s database, and the
|
|
41
|
+
* registry would refuse the push for it.
|
|
42
|
+
*/
|
|
43
|
+
export const readOwnerBeacon = async (store, owner) => {
|
|
44
|
+
let value;
|
|
45
|
+
try {
|
|
46
|
+
value = await store.read(owner);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw new CliFailure(EXIT_ERROR, `reading namespace "${owner}"'s row in ${BEACON_TABLE} through the --beacon store failed (${messageOf(error)})`);
|
|
50
|
+
}
|
|
51
|
+
if (value === null || value === undefined) {
|
|
52
|
+
throw new CliFailure(EXIT_FAILED, `the rules read namespace "${owner}"'s Resources as foreign, and the --beacon store's connection reads no row of "${owner}" in ${BEACON_TABLE}: this service does not reach "${owner}"'s database. A foreign Resource is read only within one database; across databases, carry the fact in a JWT claim instead (\`policies.claims\`)`, "ACCESS_FOREIGN_CROSS_DATABASE");
|
|
53
|
+
}
|
|
54
|
+
if (!isBeacon(value)) {
|
|
55
|
+
throw new CliFailure(EXIT_FAILED, `namespace "${owner}"'s row in ${BEACON_TABLE} holds no beacon (avb_ and 43 base64url characters): it was changed outside the service that writes it`, "ACCESS_BEACON_MISMATCH");
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Holds `owner`'s beacon, as the store reads it, to the hash `owner` pushed (`null`:
|
|
61
|
+
* none): the pull's check that this service reaches `owner`'s database.
|
|
62
|
+
*/
|
|
63
|
+
export const holdOwnerBeacon = async (store, owner, pushed) => {
|
|
64
|
+
if (pushed === null) {
|
|
65
|
+
throw new CliFailure(EXIT_FAILED, `the rules read namespace "${owner}"'s Resources as foreign, and "${owner}" has pushed no beacon, so nothing shows the two services share a database: "${owner}"'s service pushes its file with \`avaccess push --beacon <module>\` first. Nothing was written`, "ACCESS_FOREIGN_CROSS_DATABASE");
|
|
66
|
+
}
|
|
67
|
+
const read = await readOwnerBeacon(store, owner);
|
|
68
|
+
if ((await beaconHashOf(owner, read)) !== pushed) {
|
|
69
|
+
throw new CliFailure(EXIT_FAILED, `namespace "${owner}"'s beacon, as the --beacon store's connection reads it, is not the one "${owner}" pushed: this service and "${owner}" are on different databases. A foreign Resource is read only within one database; across databases, carry the fact in a JWT claim instead (\`policies.claims\`). Nothing was written`, "ACCESS_FOREIGN_CROSS_DATABASE");
|
|
70
|
+
}
|
|
71
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** The command did what it says. */
|
|
2
|
+
export declare const EXIT_OK = 0;
|
|
3
|
+
/** It ran, and the answer is no. */
|
|
4
|
+
export declare const EXIT_FAILED = 1;
|
|
5
|
+
/** It could not run. */
|
|
6
|
+
export declare const EXIT_ERROR = 2;
|
|
7
|
+
export type ExitCode = typeof EXIT_OK | typeof EXIT_FAILED | typeof EXIT_ERROR;
|
|
8
|
+
/** A command's refusal: the exit code, the message, and the error code when Access or the registry named one. */
|
|
9
|
+
export declare class CliFailure extends Error {
|
|
10
|
+
readonly exitCode: typeof EXIT_FAILED | typeof EXIT_ERROR;
|
|
11
|
+
readonly code: string | undefined;
|
|
12
|
+
constructor(exitCode: typeof EXIT_FAILED | typeof EXIT_ERROR, message: string, code?: string);
|
|
13
|
+
}
|
|
14
|
+
/** The flags a command reads, parsed. */
|
|
15
|
+
export type CliFlags = {
|
|
16
|
+
readonly [name: string]: string | boolean | readonly (string | boolean)[] | undefined;
|
|
17
|
+
};
|
|
18
|
+
export type CliContext = {
|
|
19
|
+
/** The project's directory: the lock and the pulled files are read and written under it. */
|
|
20
|
+
readonly cwd: string;
|
|
21
|
+
readonly env: {
|
|
22
|
+
readonly [name: string]: string | undefined;
|
|
23
|
+
};
|
|
24
|
+
readonly flags: CliFlags;
|
|
25
|
+
readonly positionals: readonly string[];
|
|
26
|
+
/** Writes one line to standard output. */
|
|
27
|
+
out(line: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Resolves, with the signal's name, once the process is told to stop (SIGTERM or
|
|
30
|
+
* SIGINT). Only a command that runs until stopped (`serve`) calls it; the bin
|
|
31
|
+
* listens for the signals from that call on.
|
|
32
|
+
*/
|
|
33
|
+
untilStopped(): Promise<string>;
|
|
34
|
+
};
|
|
35
|
+
/** A string flag's value, or `undefined`. */
|
|
36
|
+
export declare const stringFlag: (context: CliContext, name: string) => string | undefined;
|
|
37
|
+
/** The command's positional arguments, refused when there are more or fewer than it takes. */
|
|
38
|
+
export declare const positionals: (context: CliContext, usage: string, min: number, max: number) => readonly string[];
|
|
39
|
+
/**
|
|
40
|
+
* A file path inside the project, `/`-separated and relative to it, from a flag's
|
|
41
|
+
* value; refused when it leaves the project or does not end in `extension`.
|
|
42
|
+
*/
|
|
43
|
+
export declare const projectFilePath: (value: string, flag: string, example: string, extension: string) => string;
|
|
44
|
+
/** The version of `@aventara/access` this bin is, from its own manifest. */
|
|
45
|
+
export declare const versionOf: () => Promise<string>;
|
|
46
|
+
/** An error's message without the package's prefix: the CLI names itself. */
|
|
47
|
+
export declare const messageOf: (error: unknown) => string;
|
|
48
|
+
/** The `code` an Access error carries (read by shape: a module the CLI loads may hold its own copy of Access). */
|
|
49
|
+
export declare const codeOf: (error: unknown) => string | undefined;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// What every `avaccess` command runs with: the project's directory, the environment,
|
|
2
|
+
// the parsed flags, and where its lines go. A command answers with an exit code, or
|
|
3
|
+
// throws a `CliFailure` that says what to change; the program prints it.
|
|
4
|
+
//
|
|
5
|
+
// Exit codes: 0 the command did what it says (a check passed); 1 it ran and the
|
|
6
|
+
// answer is no (a lock is behind, a file was changed after pull, the registry
|
|
7
|
+
// refused); 2 it could not run (a usage error, missing configuration, an unreadable
|
|
8
|
+
// file, an unreachable registry).
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
/** The command did what it says. */
|
|
11
|
+
export const EXIT_OK = 0;
|
|
12
|
+
/** It ran, and the answer is no. */
|
|
13
|
+
export const EXIT_FAILED = 1;
|
|
14
|
+
/** It could not run. */
|
|
15
|
+
export const EXIT_ERROR = 2;
|
|
16
|
+
/** A command's refusal: the exit code, the message, and the error code when Access or the registry named one. */
|
|
17
|
+
export class CliFailure extends Error {
|
|
18
|
+
exitCode;
|
|
19
|
+
code;
|
|
20
|
+
constructor(exitCode, message, code) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.exitCode = exitCode;
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** A string flag's value, or `undefined`. */
|
|
27
|
+
export const stringFlag = (context, name) => {
|
|
28
|
+
const value = context.flags[name];
|
|
29
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
30
|
+
};
|
|
31
|
+
/** The command's positional arguments, refused when there are more or fewer than it takes. */
|
|
32
|
+
export const positionals = (context, usage, min, max) => {
|
|
33
|
+
if (context.positionals.length < min || context.positionals.length > max) {
|
|
34
|
+
throw new CliFailure(EXIT_ERROR, `usage: ${usage}`);
|
|
35
|
+
}
|
|
36
|
+
return context.positionals;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* A file path inside the project, `/`-separated and relative to it, from a flag's
|
|
40
|
+
* value; refused when it leaves the project or does not end in `extension`.
|
|
41
|
+
*/
|
|
42
|
+
export const projectFilePath = (value, flag, example, extension) => {
|
|
43
|
+
const path = value.replace(/\\/g, "/").replace(/^\.\/+/, "");
|
|
44
|
+
if (path === "" ||
|
|
45
|
+
path.startsWith("/") ||
|
|
46
|
+
/^[A-Za-z]:/.test(path) ||
|
|
47
|
+
path.split("/").includes("..") ||
|
|
48
|
+
!path.endsWith(extension)) {
|
|
49
|
+
throw new CliFailure(EXIT_ERROR, `${flag} is a ${extension} file inside the project, such as ${JSON.stringify(example)}; it was ${JSON.stringify(value)}`);
|
|
50
|
+
}
|
|
51
|
+
return path;
|
|
52
|
+
};
|
|
53
|
+
/** The version of `@aventara/access` this bin is, from its own manifest. */
|
|
54
|
+
export const versionOf = async () => {
|
|
55
|
+
const manifest = JSON.parse(await readFile(decodeURIComponent(new URL("../../package.json", import.meta.url).pathname), "utf8"));
|
|
56
|
+
return manifest.version;
|
|
57
|
+
};
|
|
58
|
+
/** An error's message without the package's prefix: the CLI names itself. */
|
|
59
|
+
export const messageOf = (error) => String(error instanceof Error ? error.message : error).replace(/^Aventara Access: /, "");
|
|
60
|
+
/** The `code` an Access error carries (read by shape: a module the CLI loads may hold its own copy of Access). */
|
|
61
|
+
export const codeOf = (error) => {
|
|
62
|
+
const code = error?.code;
|
|
63
|
+
return typeof code === "string" && code.startsWith("ACCESS_")
|
|
64
|
+
? code
|
|
65
|
+
: undefined;
|
|
66
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// `avaccess diff [<from> [<to>]] [--namespace <namespace>]`: the rules that changed
|
|
2
|
+
// between two revisions of the namespace's policies file, by name. A revision is
|
|
3
|
+
// `lock` (the one `avAccess.lock.json` pins), `head` (the registry's current), its
|
|
4
|
+
// hash, or the start of its hash; `from` defaults to `lock` and `to` to `head`, so a
|
|
5
|
+
// bare `avaccess diff` says what a pull would bring. Both files come from the
|
|
6
|
+
// registry and are hashed again before they are compared.
|
|
7
|
+
import { LOCK_FILE } from "../lock/lock-contract.js";
|
|
8
|
+
import { CliFailure, EXIT_ERROR, EXIT_FAILED, EXIT_OK, positionals, stringFlag, } from "./cli.context.js";
|
|
9
|
+
import { readLockIfAny } from "./lock.file.js";
|
|
10
|
+
import { changeLine, diffPolicies } from "./policies.diff.js";
|
|
11
|
+
import { registryClient } from "./registry.client.js";
|
|
12
|
+
const USAGE = "avaccess diff [<from> [<to>]] [--namespace <namespace>]";
|
|
13
|
+
/** The shortest start of a hash `diff` takes. */
|
|
14
|
+
const MIN_PREFIX = 7;
|
|
15
|
+
/** A revision named on the command line, as the registry's full hash. */
|
|
16
|
+
const resolveRevision = async (reference, namespace, lock, registry) => {
|
|
17
|
+
if (reference === "lock") {
|
|
18
|
+
if (lock === undefined || lock.namespace !== namespace) {
|
|
19
|
+
throw new CliFailure(EXIT_ERROR, `"lock" names the revision ${LOCK_FILE} pins, and the project has no lock for namespace "${namespace}"`);
|
|
20
|
+
}
|
|
21
|
+
return lock.policies.hash;
|
|
22
|
+
}
|
|
23
|
+
if (reference === "head") {
|
|
24
|
+
const head = await registry.headHash(namespace);
|
|
25
|
+
if (head === null) {
|
|
26
|
+
throw new CliFailure(EXIT_FAILED, `namespace "${namespace}" has no file pushed yet`);
|
|
27
|
+
}
|
|
28
|
+
return head;
|
|
29
|
+
}
|
|
30
|
+
const hex = reference.replace(/^sha256:/, "").toLowerCase();
|
|
31
|
+
if (!/^[0-9a-f]+$/.test(hex) || hex.length < MIN_PREFIX || hex.length > 64) {
|
|
32
|
+
throw new CliFailure(EXIT_ERROR, `a revision is "lock", "head", a hash (sha256:<hex>) or its first ${MIN_PREFIX} or more hex digits; it was ${JSON.stringify(reference)}`);
|
|
33
|
+
}
|
|
34
|
+
if (hex.length === 64) {
|
|
35
|
+
return `sha256:${hex}`;
|
|
36
|
+
}
|
|
37
|
+
const matches = (await registry.revisions(namespace)).filter((entry) => entry.hash.startsWith(`sha256:${hex}`));
|
|
38
|
+
if (matches.length !== 1) {
|
|
39
|
+
throw new CliFailure(EXIT_FAILED, matches.length === 0
|
|
40
|
+
? `namespace "${namespace}" has no revision starting ${reference}`
|
|
41
|
+
: `${reference} starts ${matches.length} revisions of namespace "${namespace}": give more of the hash`);
|
|
42
|
+
}
|
|
43
|
+
return matches[0].hash;
|
|
44
|
+
};
|
|
45
|
+
export const diffCommand = async (context) => {
|
|
46
|
+
const [from = "lock", to = "head"] = positionals(context, USAGE, 0, 2);
|
|
47
|
+
const lock = await readLockIfAny(context.cwd);
|
|
48
|
+
const namespace = stringFlag(context, "namespace") ?? lock?.namespace;
|
|
49
|
+
if (namespace === undefined) {
|
|
50
|
+
throw new CliFailure(EXIT_ERROR, `no namespace: pass --namespace <namespace> (the project has no ${LOCK_FILE} to read it from)`);
|
|
51
|
+
}
|
|
52
|
+
const registry = registryClient(context);
|
|
53
|
+
const fromHash = await resolveRevision(from, namespace, lock, registry);
|
|
54
|
+
const toHash = await resolveRevision(to, namespace, lock, registry);
|
|
55
|
+
const before = await registry.revision(namespace, fromHash);
|
|
56
|
+
const after = toHash === fromHash ? before : await registry.revision(namespace, toHash);
|
|
57
|
+
const changes = diffPolicies(before.file, after.file);
|
|
58
|
+
context.out(`namespace "${namespace}": ${fromHash} → ${toHash}`);
|
|
59
|
+
if (changes.length === 0) {
|
|
60
|
+
context.out(" no rule changed");
|
|
61
|
+
}
|
|
62
|
+
for (const change of changes) {
|
|
63
|
+
context.out(` ${changeLine(change)}`);
|
|
64
|
+
}
|
|
65
|
+
return EXIT_OK;
|
|
66
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// `avaccess generate --orm <orm> [--out <file>]`: the generated client of a service
|
|
2
|
+
// that does not run on Aventara, written from the pulled files `avAccess.lock.json`
|
|
3
|
+
// pins, with one ORM's translator or none (`--orm none`, the default). Two TypeScript files: `--out` (default
|
|
4
|
+
// `src/generated/avAccess.ts`), which the service imports, and
|
|
5
|
+
// `<name>.internal.ts` beside it, the copied decisions it imports, named with the
|
|
6
|
+
// extension the project's tsconfig asks for (`tsconfig.reader.ts`). It runs the
|
|
7
|
+
// offline check `avaccess verify` runs first: a pulled file edited by hand is refused
|
|
8
|
+
// (`ACCESS_LOCK_MISMATCH`) and nothing is written. No network.
|
|
9
|
+
import { relative, resolve } from "node:path";
|
|
10
|
+
import { emitClient } from "../generator/client.emitter.js";
|
|
11
|
+
import { importExtensionOf, TsconfigRefusal, } from "../generator/tsconfig.reader.js";
|
|
12
|
+
import { composeGuardrails } from "../guardrails/guardrails.composer.js";
|
|
13
|
+
import { canonicalGuardrails } from "../guardrails/guardrails.hash.js";
|
|
14
|
+
import { verifyLocked } from "../lock/lock.verifier.js";
|
|
15
|
+
import { canonicalPolicies } from "../policies.hash.js";
|
|
16
|
+
import { CliFailure, codeOf, EXIT_ERROR, EXIT_FAILED, EXIT_OK, messageOf, positionals, projectFilePath, stringFlag, versionOf, } from "./cli.context.js";
|
|
17
|
+
import { readLock, readPinned, writeProjectFile } from "./lock.file.js";
|
|
18
|
+
import { clientTranslatorOf } from "./translator.flag.js";
|
|
19
|
+
/** Where the generated client goes when `--out` does not say. */
|
|
20
|
+
const DEFAULT_CLIENT = "src/generated/avAccess.ts";
|
|
21
|
+
/** The internal file beside a client file: `avAccess.ts` → `avAccess.internal.ts`. */
|
|
22
|
+
const internalFileOf = (client) => client.replace(/\.ts$/, ".internal.ts");
|
|
23
|
+
export const generateCommand = async (context) => {
|
|
24
|
+
positionals(context, "avaccess generate [--orm <orm>] [--out <file>] [--tsconfig <file>]", 0, 0);
|
|
25
|
+
const translator = clientTranslatorOf(context);
|
|
26
|
+
const out = projectFilePath(stringFlag(context, "out") ?? DEFAULT_CLIENT, "--out", DEFAULT_CLIENT, ".ts");
|
|
27
|
+
const lock = await readLock(context.cwd);
|
|
28
|
+
const policies = await readPinned(context.cwd, lock.policies);
|
|
29
|
+
const guardrails = lock.guardrails === null
|
|
30
|
+
? undefined
|
|
31
|
+
: await readPinned(context.cwd, lock.guardrails);
|
|
32
|
+
let guardrailDenies = [];
|
|
33
|
+
try {
|
|
34
|
+
verifyLocked(lock, {
|
|
35
|
+
namespace: policies
|
|
36
|
+
?.namespace,
|
|
37
|
+
policies,
|
|
38
|
+
guardrails,
|
|
39
|
+
});
|
|
40
|
+
if (guardrails !== undefined) {
|
|
41
|
+
// Composed from the canonical forms, so a pulled file's member order (which
|
|
42
|
+
// its hash ignores) never reaches the client's text.
|
|
43
|
+
const file = JSON.parse(canonicalPolicies(policies));
|
|
44
|
+
guardrailDenies = composeGuardrails(file, JSON.parse(canonicalGuardrails(guardrails))).denies.slice(file.denies.length);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw new CliFailure(EXIT_FAILED, messageOf(error), codeOf(error));
|
|
49
|
+
}
|
|
50
|
+
const internal = internalFileOf(out);
|
|
51
|
+
// Relative to the project, with forward slashes, as `--out` is: the banner's bytes
|
|
52
|
+
// do not depend on how the path was typed.
|
|
53
|
+
const named = stringFlag(context, "tsconfig");
|
|
54
|
+
const tsconfig = named === undefined
|
|
55
|
+
? undefined
|
|
56
|
+
: relative(context.cwd, resolve(context.cwd, named)).replace(/\\/g, "/");
|
|
57
|
+
let extension;
|
|
58
|
+
try {
|
|
59
|
+
extension = await importExtensionOf(context.cwd, resolve(context.cwd, out), tsconfig);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw new CliFailure(EXIT_ERROR, error instanceof TsconfigRefusal
|
|
63
|
+
? error.message
|
|
64
|
+
: `the project's tsconfig.json could not be read to name ${internal} (${messageOf(error)}). Nothing was written.`);
|
|
65
|
+
}
|
|
66
|
+
const internalName = (internal.split("/").pop() ?? internal).replace(/\.ts$/, "");
|
|
67
|
+
const files = await emitClient({
|
|
68
|
+
policies: policies,
|
|
69
|
+
guardrailDenies,
|
|
70
|
+
lock,
|
|
71
|
+
translator,
|
|
72
|
+
command: `avaccess generate --orm ${translator.orm} --out ${out}${tsconfig === undefined ? "" : ` --tsconfig ${tsconfig}`}`,
|
|
73
|
+
version: await versionOf(),
|
|
74
|
+
internalSpecifier: `./${internalName}${extension}`,
|
|
75
|
+
});
|
|
76
|
+
await writeProjectFile(context.cwd, internal, files.internal);
|
|
77
|
+
await writeProjectFile(context.cwd, out, files.client);
|
|
78
|
+
context.out(`wrote ${out} and ${internal}: namespace "${lock.namespace}" from ${lock.policies.path}${lock.guardrails === null ? "" : ` and ${lock.guardrails.path}`}, ${translator.title === undefined
|
|
79
|
+
? "with no ORM translator (--orm none)"
|
|
80
|
+
: `with the ${translator.title} translator`}`);
|
|
81
|
+
return EXIT_OK;
|
|
82
|
+
};
|