@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,28 @@
|
|
|
1
|
+
import type { AccessLock } from "../lock/lock-contract.js";
|
|
2
|
+
import type { AccessDenyRule, AccessPolicies } from "../policies-contract.js";
|
|
3
|
+
import type { ClientTranslator } from "./orm.translator.js";
|
|
4
|
+
/** What a generated client is made from. */
|
|
5
|
+
type ClientInput = {
|
|
6
|
+
/** The pulled policies file, checked and held to the lock. */
|
|
7
|
+
readonly policies: AccessPolicies;
|
|
8
|
+
/** The denies the org's pulled guardrails add to it, composed from both files' canonical forms (none when the lock pins none). */
|
|
9
|
+
readonly guardrailDenies: readonly AccessDenyRule[];
|
|
10
|
+
readonly lock: AccessLock;
|
|
11
|
+
readonly translator: ClientTranslator;
|
|
12
|
+
/** The command that regenerates the file, as the banner prints it. */
|
|
13
|
+
readonly command: string;
|
|
14
|
+
/** The `@aventara/access` version whose decisions the file carries. */
|
|
15
|
+
readonly version: string;
|
|
16
|
+
/** How `avAccess.ts` names `avAccess.internal.ts`: `./avAccess.internal.js`, as the project's tsconfig asks. */
|
|
17
|
+
readonly internalSpecifier: string;
|
|
18
|
+
};
|
|
19
|
+
/** The two files of a generated client. */
|
|
20
|
+
type ClientFiles = {
|
|
21
|
+
/** `avAccess.ts`: `access` and the client's types. */
|
|
22
|
+
readonly client: string;
|
|
23
|
+
/** `avAccess.internal.ts`: the copied decisions and translator. */
|
|
24
|
+
readonly internal: string;
|
|
25
|
+
};
|
|
26
|
+
/** The generated client's two files. */
|
|
27
|
+
export declare const emitClient: (input: ClientInput) => Promise<ClientFiles>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// The generated client's text: two TypeScript files a service that does not run on
|
|
2
|
+
// Aventara decides with, and nothing else of Access.
|
|
3
|
+
//
|
|
4
|
+
// - `avAccess.ts`, the file the service imports: the banner (generated, do not edit,
|
|
5
|
+
// the command that regenerates it, the pinned files), the namespace's Resource
|
|
6
|
+
// types, the pulled policies file as a constant with the guardrails' denies beside
|
|
7
|
+
// it, then `access`: `can`, `scopeOf`, `fieldsOf` (ORM-neutral, answering in
|
|
8
|
+
// Aventara's `where`), `checkBeacons` and the translator's functions. It exports
|
|
9
|
+
// `access` and the client's types only. When the rules read other namespaces'
|
|
10
|
+
// Resources as foreign, `access` decides nothing until
|
|
11
|
+
// `access.checkBeacons(reader, pins)` has passed at startup.
|
|
12
|
+
// - `avAccess.internal.ts` beside it: the banner, then the copied decision modules
|
|
13
|
+
// and the translator's module, as they are. Their names stay exported there (the
|
|
14
|
+
// client imports them, and a declaration nothing reads would fail
|
|
15
|
+
// `noUnusedLocals`), and none of them is reachable from `avAccess.ts`.
|
|
16
|
+
//
|
|
17
|
+
// The texts are a function of their input alone: the same pulled files, translator,
|
|
18
|
+
// command, package version and import spelling give the same bytes. Every list they
|
|
19
|
+
// write is sorted, and the policies file is written in its canonical member order.
|
|
20
|
+
import { foreignOwnersOf } from "../lock/lock.verifier.js";
|
|
21
|
+
import { canonicalPolicies } from "../policies.hash.js";
|
|
22
|
+
import { DECISION_MODULES, readCopiedModules, } from "./copied-modules.reader.js";
|
|
23
|
+
import { namespaceFactsOf, propertyKey, } from "./namespace.facts.js";
|
|
24
|
+
import { typescriptLiteral } from "./typescript-literal.writer.js";
|
|
25
|
+
/** What the client file reads from the internal one, besides the translator's names. */
|
|
26
|
+
const DECISION_IMPORTS = {
|
|
27
|
+
values: [
|
|
28
|
+
"beaconMismatch",
|
|
29
|
+
"beaconProblem",
|
|
30
|
+
"createDecider",
|
|
31
|
+
"grantsQuestionProblem",
|
|
32
|
+
"newTrace",
|
|
33
|
+
],
|
|
34
|
+
types: [
|
|
35
|
+
"AccessAction",
|
|
36
|
+
"AccessBeaconPins",
|
|
37
|
+
"AccessBeaconReader",
|
|
38
|
+
"AccessCanQuestion",
|
|
39
|
+
"AccessDenyRule",
|
|
40
|
+
"AccessFields",
|
|
41
|
+
"AccessGrantsQuestion",
|
|
42
|
+
"AccessOrigin",
|
|
43
|
+
"AccessPolicies",
|
|
44
|
+
"AccessWhere",
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
const RULE = "// ---------------------------------------------------------------------------";
|
|
48
|
+
const section = (title) => `${RULE}\n// ${title}\n${RULE}`;
|
|
49
|
+
/** What ends a comment's line: control characters and the line separators. */
|
|
50
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: the characters it removes
|
|
51
|
+
const BREAKS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/g;
|
|
52
|
+
/**
|
|
53
|
+
* Text from the project (a path, the command line) as a comment carries it: no line
|
|
54
|
+
* break, which would end a `//` comment, and no star followed by a slash, which
|
|
55
|
+
* would end a block comment, so nothing in it becomes code.
|
|
56
|
+
*/
|
|
57
|
+
const inComment = (text) => text.replace(BREAKS, " ").replace(/\*\//g, "*\\/");
|
|
58
|
+
const banner = (input, file) => {
|
|
59
|
+
const { lock } = input;
|
|
60
|
+
const pins = [
|
|
61
|
+
`// ${inComment(lock.policies.path)} ${lock.policies.hash}`,
|
|
62
|
+
lock.guardrails === null
|
|
63
|
+
? "// (the lock pins no guardrails)"
|
|
64
|
+
: `// ${inComment(lock.guardrails.path)} ${lock.guardrails.hash}`,
|
|
65
|
+
];
|
|
66
|
+
return [
|
|
67
|
+
"// biome-ignore-all format: generated output",
|
|
68
|
+
"// biome-ignore-all lint: generated output",
|
|
69
|
+
"/* eslint-disable */",
|
|
70
|
+
`// Generated by \`avaccess generate\` (@aventara/access ${input.version}). Do not edit this`,
|
|
71
|
+
"// file: change the rules where they are authored, push them, pull them, and",
|
|
72
|
+
"// regenerate it with",
|
|
73
|
+
"//",
|
|
74
|
+
`// ${inComment(input.command)}`,
|
|
75
|
+
"//",
|
|
76
|
+
`// Namespace ${JSON.stringify(lock.namespace)}, from avAccess.lock.json:`,
|
|
77
|
+
...pins,
|
|
78
|
+
"//",
|
|
79
|
+
...(file === "client"
|
|
80
|
+
? [
|
|
81
|
+
"// It decides from the pulled files with a copy of Access's decisions, kept in",
|
|
82
|
+
`// ${JSON.stringify(input.internalSpecifier)} beside it, and imports nothing else: no @aventara`,
|
|
83
|
+
`// package and no ${input.translator.title ?? "ORM"} module. The scope covers the queries you pass it`,
|
|
84
|
+
"// to, and only those.",
|
|
85
|
+
]
|
|
86
|
+
: [
|
|
87
|
+
"// The copy of Access's decisions and translator the client beside it imports.",
|
|
88
|
+
"// Import `access` and its types from the client; nothing here is its API.",
|
|
89
|
+
]),
|
|
90
|
+
].join("\n");
|
|
91
|
+
};
|
|
92
|
+
const rowTypes = (namespace) => {
|
|
93
|
+
const known = new Set(namespace.resources.map((resource) => resource.name));
|
|
94
|
+
const rows = namespace.resources.map((resource) => {
|
|
95
|
+
const members = [
|
|
96
|
+
...resource.fields.map((field) => `\t\treadonly ${propertyKey(field)}?: unknown;`),
|
|
97
|
+
// A relation to a Resource the file does not list (a trimmed model) is typed
|
|
98
|
+
// `unknown`: there is no row type to name.
|
|
99
|
+
...resource.relations.map((relation) => `\t\treadonly ${propertyKey(relation.name)}?: ${!known.has(relation.target)
|
|
100
|
+
? "unknown"
|
|
101
|
+
: relation.cardinality === "one"
|
|
102
|
+
? `ResourceRows[${JSON.stringify(relation.target)}] | null`
|
|
103
|
+
: `readonly ResourceRows[${JSON.stringify(relation.target)}][]`};`),
|
|
104
|
+
];
|
|
105
|
+
return `\treadonly ${propertyKey(resource.name)}: {\n${members.join("\n")}\n\t};`;
|
|
106
|
+
});
|
|
107
|
+
const fields = namespace.resources.map((resource) => `\treadonly ${propertyKey(resource.name)}: ${resource.fields.length === 0
|
|
108
|
+
? "never"
|
|
109
|
+
: resource.fields.map((field) => JSON.stringify(field)).join(" | ")};`);
|
|
110
|
+
const names = namespace.resources
|
|
111
|
+
.map((resource) => JSON.stringify(resource.name))
|
|
112
|
+
.join(" | ");
|
|
113
|
+
return `/** The Resources the rules name: the namespace's own, and the ones they read from another namespace. */
|
|
114
|
+
export type ResourceName = ${names === "" ? "never" : names};
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A row of each Resource as \`can\` reads it: any of its fields. A relation on it is
|
|
118
|
+
* accepted, so a row your ORM returns fits, and never read: a relation condition is
|
|
119
|
+
* asked through \`exists\`. Never put relation data a client sent on a row. The
|
|
120
|
+
* policies file names fields, not their types, so each is \`unknown\`.
|
|
121
|
+
*/
|
|
122
|
+
export type ResourceRows = {
|
|
123
|
+
${rows.join("\n")}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/** The scalar fields of each Resource. */
|
|
127
|
+
export type ResourceFields = {
|
|
128
|
+
${fields.join("\n")}
|
|
129
|
+
};`;
|
|
130
|
+
};
|
|
131
|
+
const API = `/** Who asks about one of the namespace's Resources, and through which layer. */
|
|
132
|
+
export type QuestionOf<R extends ResourceName> = {
|
|
133
|
+
/** Your principal: an object the rules read (\`me.<path>\`, its \`claims\`), or \`null\` for anyone signed out. */
|
|
134
|
+
readonly principal: unknown;
|
|
135
|
+
/** The layer the request came through: \`"client"\` for a user's request, \`"agent"\` for an AI agent's, \`"application"\` for the service's own. */
|
|
136
|
+
readonly origin: AccessOrigin;
|
|
137
|
+
readonly resource: R;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
/** \`can\`: without a row, whether the principal could ever do \`action\`; with one, whether it may on this row. */
|
|
141
|
+
export type CanQuestionOf<R extends ResourceName> = QuestionOf<R> &
|
|
142
|
+
(
|
|
143
|
+
| {
|
|
144
|
+
readonly action: "update";
|
|
145
|
+
/**
|
|
146
|
+
* On an update with a \`row\` only: what \`grantsOf\` answered about the row before
|
|
147
|
+
* the update, asked before you wrote it. The grant that covers \`row\` must then be
|
|
148
|
+
* one of these. With \`before\` too, each one's \`where\` is checked on it in memory;
|
|
149
|
+
* a relation condition on it is not asked again, so it is the one the database
|
|
150
|
+
* held before the update.
|
|
151
|
+
*/
|
|
152
|
+
readonly beforeGrants?: readonly string[];
|
|
153
|
+
}
|
|
154
|
+
| {
|
|
155
|
+
readonly action: Exclude<AccessAction, "update">;
|
|
156
|
+
/** An update's only. */
|
|
157
|
+
readonly beforeGrants?: undefined;
|
|
158
|
+
}
|
|
159
|
+
) & {
|
|
160
|
+
/**
|
|
161
|
+
* The row a create inserts, an update leaves, or a read or delete is about. For an
|
|
162
|
+
* update, limit the update itself to the rows \`scopeOf({ action: "update" })\`
|
|
163
|
+
* returns.
|
|
164
|
+
*/
|
|
165
|
+
readonly row?: ResourceRows[R];
|
|
166
|
+
/**
|
|
167
|
+
* On an update with a \`row\` only: the row as it was before the update. One update
|
|
168
|
+
* grant must then cover both rows, its \`where\` this one and its \`where\` and
|
|
169
|
+
* \`after\` the row the update leaves, so no update moves a row from the rows one
|
|
170
|
+
* grant reaches to another's. Without it, \`can\` checks only the row the update
|
|
171
|
+
* leaves.
|
|
172
|
+
*/
|
|
173
|
+
readonly before?: ResourceRows[R];
|
|
174
|
+
/** Whether a row of \`resource\` meets \`where\`, unscoped: what \`row\` does not carry is asked through it. */
|
|
175
|
+
readonly exists?: (
|
|
176
|
+
resource: ResourceName,
|
|
177
|
+
where: AccessWhere,
|
|
178
|
+
) => Promise<boolean>;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/** \`grantsOf\`: the update grants that cover \`row\`, the row you are about to update, as it is now. */
|
|
182
|
+
export type GrantsQuestionOf<R extends ResourceName> = QuestionOf<R> & {
|
|
183
|
+
readonly action: "update";
|
|
184
|
+
readonly row: ResourceRows[R];
|
|
185
|
+
/** Whether a row of \`resource\` meets \`where\`, unscoped: what \`row\` does not carry is asked through it. */
|
|
186
|
+
readonly exists?: (
|
|
187
|
+
resource: ResourceName,
|
|
188
|
+
where: AccessWhere,
|
|
189
|
+
) => Promise<boolean>;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/** \`scopeOf\`: the rows the principal may read, update or delete. */
|
|
193
|
+
export type ScopeQuestionOf<R extends ResourceName> = QuestionOf<R> & {
|
|
194
|
+
readonly action: "read" | "update" | "delete";
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/** \`fieldsOf\`: the fields the principal may read, or write by one action. */
|
|
198
|
+
export type FieldsQuestionOf<R extends ResourceName> = QuestionOf<R> &
|
|
199
|
+
(
|
|
200
|
+
| { readonly access: "read" }
|
|
201
|
+
| { readonly access: "write"; readonly action?: "create" | "update" }
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
/** Whether the principal may do \`action\`; on \`row\` when one is given. Default deny, any grant allows, any deny wins. */
|
|
205
|
+
const can = <R extends ResourceName>(
|
|
206
|
+
question: CanQuestionOf<R>,
|
|
207
|
+
): boolean | Promise<boolean> =>
|
|
208
|
+
decider.can(question as AccessCanQuestion, newTrace());
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The update grants that cover \`row\` before you update it: opaque keys, no rule data.
|
|
212
|
+
* Ask it before the write, in the update's transaction, and pass the answer to \`can\`
|
|
213
|
+
* as \`beforeGrants\` with the row the update leaves.
|
|
214
|
+
*/
|
|
215
|
+
const grantsOf = <R extends ResourceName>(
|
|
216
|
+
question: GrantsQuestionOf<R>,
|
|
217
|
+
): readonly string[] | Promise<readonly string[]> =>
|
|
218
|
+
decider.grantsOf(question as AccessGrantsQuestion);
|
|
219
|
+
|
|
220
|
+
/** The rows the principal may \`action\`: every row (\`true\`), none (\`false\`), or the rows a \`where\` matches. */
|
|
221
|
+
const scopeOf = <R extends ResourceName>(
|
|
222
|
+
question: ScopeQuestionOf<R>,
|
|
223
|
+
): boolean | AccessWhere => decider.scopeOf(question);
|
|
224
|
+
|
|
225
|
+
/** The fields usable on every reachable row (\`always\`), and those usable on some rows only (\`perRow\`, with their \`where\`). */
|
|
226
|
+
const fieldsOf = <R extends ResourceName>(
|
|
227
|
+
question: FieldsQuestionOf<R>,
|
|
228
|
+
): AccessFields => decider.fieldsOf(question);
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The startup check that this service reaches the database of each namespace whose
|
|
232
|
+
* Resources the rules read as foreign: its row in \`_aventara_beacon\`, read through
|
|
233
|
+
* \`reader\` (this service's own connection), hashes to its pin in \`pins\`, the pins of
|
|
234
|
+
* \`environment\` (avAccess.beacons.<environment>.json). Await it before the service
|
|
235
|
+
* takes requests: while the rules read another namespace, \`access\` answers nothing
|
|
236
|
+
* until it has passed, and nothing once a check has failed, for good (an error with
|
|
237
|
+
* \`code: "ACCESS_BEACON_MISMATCH"\`); calls run one after another. With none read it
|
|
238
|
+
* passes at once.
|
|
239
|
+
*/
|
|
240
|
+
const checkBeacons = (
|
|
241
|
+
reader: AccessBeaconReader,
|
|
242
|
+
pins: AccessBeaconPins,
|
|
243
|
+
environment: string,
|
|
244
|
+
): Promise<void> => {
|
|
245
|
+
const run = async (): Promise<void> => {
|
|
246
|
+
if (foreignOwners.length === 0) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (failed !== undefined) {
|
|
250
|
+
throw beaconMismatch(
|
|
251
|
+
\`a beacon check failed before, and the client stays shut: \${failed}\`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
const problem = await beaconProblem(
|
|
255
|
+
pulledPolicies.namespace ?? "",
|
|
256
|
+
foreignOwners,
|
|
257
|
+
pins,
|
|
258
|
+
environment,
|
|
259
|
+
reader,
|
|
260
|
+
);
|
|
261
|
+
if (problem !== undefined) {
|
|
262
|
+
failed = problem;
|
|
263
|
+
refusal = problem;
|
|
264
|
+
throw beaconMismatch(problem);
|
|
265
|
+
}
|
|
266
|
+
refusal = undefined;
|
|
267
|
+
};
|
|
268
|
+
const result = checking.then(run);
|
|
269
|
+
checking = result.catch(() => undefined);
|
|
270
|
+
return result;
|
|
271
|
+
};`;
|
|
272
|
+
const copied = (modules) => modules
|
|
273
|
+
.map((module) => `${section(`${module.path.replace(/^src\//, "")}, from @aventara/access`)}\n\n${module.text}`)
|
|
274
|
+
.join("\n");
|
|
275
|
+
/** The client file's one import, and its re-export of the translator's types. */
|
|
276
|
+
const imports = (input, ormPart) => {
|
|
277
|
+
const entries = [];
|
|
278
|
+
// Sorted by name, case aside (as Biome's import organizer sorts them), so a
|
|
279
|
+
// project that formats generated files finds nothing to change.
|
|
280
|
+
for (const part of [DECISION_IMPORTS, ormPart.imports]) {
|
|
281
|
+
for (const name of part.values) {
|
|
282
|
+
entries.push(`${name.toLowerCase()} ${name}`);
|
|
283
|
+
}
|
|
284
|
+
for (const name of part.types) {
|
|
285
|
+
entries.push(`${name.toLowerCase()} type ${name}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
entries.sort();
|
|
289
|
+
let list = "";
|
|
290
|
+
for (const entry of entries) {
|
|
291
|
+
list += `\t${entry.slice(entry.indexOf(" ") + 1)},\n`;
|
|
292
|
+
}
|
|
293
|
+
const specifier = JSON.stringify(input.internalSpecifier);
|
|
294
|
+
const reexport = ormPart.exports.length === 0
|
|
295
|
+
? ""
|
|
296
|
+
: `\n\nexport type { ${[...ormPart.exports].sort().join(", ")} } from ${specifier};`;
|
|
297
|
+
return `import {\n${list}} from ${specifier};${reexport}`;
|
|
298
|
+
};
|
|
299
|
+
/** The generated client's two files. */
|
|
300
|
+
export const emitClient = async (input) => {
|
|
301
|
+
const modules = await readCopiedModules([
|
|
302
|
+
...DECISION_MODULES,
|
|
303
|
+
...input.translator.modules,
|
|
304
|
+
]);
|
|
305
|
+
const namespace = namespaceFactsOf(input.lock.namespace, input.policies);
|
|
306
|
+
const ormPart = input.translator.clientPart(namespace);
|
|
307
|
+
const pulled = JSON.parse(canonicalPolicies(input.policies));
|
|
308
|
+
const members = [
|
|
309
|
+
"can",
|
|
310
|
+
"grantsOf",
|
|
311
|
+
"scopeOf",
|
|
312
|
+
"fieldsOf",
|
|
313
|
+
"checkBeacons",
|
|
314
|
+
...ormPart.members,
|
|
315
|
+
];
|
|
316
|
+
const internal = `${banner(input, "internal")}
|
|
317
|
+
|
|
318
|
+
${copied(modules)}`;
|
|
319
|
+
const client = `${banner(input, "client")}
|
|
320
|
+
|
|
321
|
+
${imports(input, ormPart)}
|
|
322
|
+
|
|
323
|
+
${section(`Namespace ${JSON.stringify(namespace.namespace)}`)}
|
|
324
|
+
|
|
325
|
+
${rowTypes(namespace)}
|
|
326
|
+
|
|
327
|
+
/** The namespace's policies file, as \`avaccess pull\` wrote it (${inComment(input.lock.policies.path)}). */
|
|
328
|
+
const pulledPolicies: AccessPolicies = ${typescriptLiteral(pulled)};
|
|
329
|
+
|
|
330
|
+
/** The denies the org's guardrails add to the namespace's rules${input.lock.guardrails === null ? " (the lock pins none)" : ` (${inComment(input.lock.guardrails.path)})`}. */
|
|
331
|
+
const guardrailDenies: readonly AccessDenyRule[] = ${typescriptLiteral(input.guardrailDenies)};
|
|
332
|
+
|
|
333
|
+
/** The file the decisions run from: the namespace's rules, narrowed by the guardrails. */
|
|
334
|
+
const policies: AccessPolicies = {
|
|
335
|
+
...pulledPolicies,
|
|
336
|
+
denies: [...pulledPolicies.denies, ...guardrailDenies],
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The namespaces whose Resources the rules read as foreign: \`checkBeacons\` holds each
|
|
341
|
+
* one's row in \`_aventara_beacon\`, read through this service's connection, to its pin.
|
|
342
|
+
*/
|
|
343
|
+
const foreignOwners: readonly string[] = ${typescriptLiteral(foreignOwnersOf(input.policies))};
|
|
344
|
+
|
|
345
|
+
/** Why the client answers nothing yet: \`undefined\` once the beacons have passed, or when the rules read no other namespace. */
|
|
346
|
+
let refusal: string | undefined =
|
|
347
|
+
foreignOwners.length === 0
|
|
348
|
+
? undefined
|
|
349
|
+
: "the rules read other namespaces' Resources as foreign, and the beacons were not checked: \`await access.checkBeacons(reader, pins, environment)\` when the service starts, before it takes requests, with this environment's avAccess.beacons.<environment>.json";
|
|
350
|
+
|
|
351
|
+
/** A failed beacon check: no later check opens the client again. */
|
|
352
|
+
let failed: string | undefined;
|
|
353
|
+
|
|
354
|
+
/** The beacon checks, one after another. */
|
|
355
|
+
let checking: Promise<void> = Promise.resolve();
|
|
356
|
+
|
|
357
|
+
const open = (): void => {
|
|
358
|
+
if (refusal !== undefined) {
|
|
359
|
+
throw beaconMismatch(refusal);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const decided = createDecider(policies);
|
|
364
|
+
|
|
365
|
+
/** The decisions, each refused while the beacons the lock pins have not passed. */
|
|
366
|
+
const decider: typeof decided = {
|
|
367
|
+
...decided,
|
|
368
|
+
can: (question, trace) => {
|
|
369
|
+
open();
|
|
370
|
+
return decided.can(question, trace);
|
|
371
|
+
},
|
|
372
|
+
grantsOf: (question) => {
|
|
373
|
+
open();
|
|
374
|
+
const problem = grantsQuestionProblem(question);
|
|
375
|
+
if (problem !== undefined) {
|
|
376
|
+
// Shaped as the engine's \`AccessError\`: the client imports nothing.
|
|
377
|
+
throw Object.assign(new Error(\`Aventara Access: \${problem}\`), {
|
|
378
|
+
name: "AccessError",
|
|
379
|
+
code: "ACCESS_QUESTION_INVALID" as const,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
return decided.grantsOf(question);
|
|
383
|
+
},
|
|
384
|
+
scopeOf: (question) => {
|
|
385
|
+
open();
|
|
386
|
+
return decided.scopeOf(question);
|
|
387
|
+
},
|
|
388
|
+
fieldsOf: (question) => {
|
|
389
|
+
open();
|
|
390
|
+
return decided.fieldsOf(question);
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
${section("Decisions")}
|
|
395
|
+
|
|
396
|
+
${API}
|
|
397
|
+
|
|
398
|
+
${input.translator.title === undefined
|
|
399
|
+
? ""
|
|
400
|
+
: `${section(`The ${input.translator.title} translator`)}
|
|
401
|
+
|
|
402
|
+
${ormPart.text}
|
|
403
|
+
`}/** Access's decisions for namespace ${JSON.stringify(namespace.namespace)}, ${input.translator.title === undefined
|
|
404
|
+
? "answering in Aventara's `where`"
|
|
405
|
+
: `with the ${input.translator.title} translator`}. */
|
|
406
|
+
export const access = {
|
|
407
|
+
${members.map((member) => `\t${member},`).join("\n")}
|
|
408
|
+
};
|
|
409
|
+
`;
|
|
410
|
+
return { client, internal };
|
|
411
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The decision modules every generated client carries, in an order that defines each name before it is read. */
|
|
2
|
+
export declare const DECISION_MODULES: readonly string[];
|
|
3
|
+
/** One module's text as the generated client carries it. */
|
|
4
|
+
export type CopiedModule = {
|
|
5
|
+
/** Its path in the package, `src/scope.resolver.ts`. */
|
|
6
|
+
readonly path: string;
|
|
7
|
+
/** Its declarations: the text without its header comment and its imports. */
|
|
8
|
+
readonly text: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Reads `paths` (package paths, in order) as the generated client carries them.
|
|
12
|
+
* Throws when a module imports something that is not among `paths` before it.
|
|
13
|
+
*/
|
|
14
|
+
export declare const readCopiedModules: (paths: readonly string[]) => Promise<readonly CopiedModule[]>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// The modules `avaccess generate` copies into a generated client, read from the
|
|
2
|
+
// package's own TypeScript sources: the policies file's types, the principal fold,
|
|
3
|
+
// Aventara's `where`, the row evaluator and the decisions, then the chosen ORM
|
|
4
|
+
// translator's module. One source: the engine runs these modules, and the generated
|
|
5
|
+
// client carries a copy of the same text, so the two decide alike.
|
|
6
|
+
//
|
|
7
|
+
// The package ships these sources beside `dist/` (its `files`), and this reader finds
|
|
8
|
+
// them two directories up from itself, which is the package's root from `src/generator/`
|
|
9
|
+
// and from `dist/generator/` alike. Each module's header comment and imports are left
|
|
10
|
+
// out of the copy; an import of anything but another copied module is refused, so a
|
|
11
|
+
// copied module can never pull a dependency into a service's generated file.
|
|
12
|
+
import { readFile } from "node:fs/promises";
|
|
13
|
+
/** The decision modules every generated client carries, in an order that defines each name before it is read. */
|
|
14
|
+
export const DECISION_MODULES = [
|
|
15
|
+
"src/policies-contract.ts",
|
|
16
|
+
"src/scope.resolver.ts",
|
|
17
|
+
"src/aventara-where.translator.ts",
|
|
18
|
+
"src/predicate.evaluator.ts",
|
|
19
|
+
"src/canonical-json.writer.ts",
|
|
20
|
+
"src/sha256.digest.ts",
|
|
21
|
+
"src/policies.decider.ts",
|
|
22
|
+
"src/beacon/beacon.check.ts",
|
|
23
|
+
];
|
|
24
|
+
const IMPORT = /^import\s[^;]*?\sfrom\s+"([^"]+)";\n/gm;
|
|
25
|
+
/** Any other way to load a module: a re-export, an import left over, `import()`, `require`. */
|
|
26
|
+
const LEFT_IMPORT = /^\s*(?:import|export)\s[^;]*?\sfrom\s+"|^\s*import\s+"|\bimport\s*\(|\brequire\s*\(/m;
|
|
27
|
+
/** The package's root: `src/generator/` and `dist/generator/` are both two levels below it. */
|
|
28
|
+
const packageFile = (path) => decodeURIComponent(new URL(`../../${path}`, import.meta.url).pathname);
|
|
29
|
+
/** The package path an import names, from the module at `from`; `undefined` when it leaves the package's sources. */
|
|
30
|
+
const resolveImport = (from, specifier) => {
|
|
31
|
+
if (!specifier.startsWith("./") && !specifier.startsWith("../")) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const parts = from.split("/").slice(0, -1);
|
|
35
|
+
for (const part of specifier.split("/")) {
|
|
36
|
+
if (part === "..") {
|
|
37
|
+
parts.pop();
|
|
38
|
+
}
|
|
39
|
+
else if (part !== ".") {
|
|
40
|
+
parts.push(part);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return parts.join("/").replace(/\.js$/, ".ts");
|
|
44
|
+
};
|
|
45
|
+
/** The header comment: the comment lines, and the blank lines, before the first line of code. */
|
|
46
|
+
const withoutHeader = (text) => {
|
|
47
|
+
const lines = text.split("\n");
|
|
48
|
+
let first = 0;
|
|
49
|
+
while (first < lines.length &&
|
|
50
|
+
((lines[first] ?? "").startsWith("//") || (lines[first] ?? "") === "")) {
|
|
51
|
+
first += 1;
|
|
52
|
+
}
|
|
53
|
+
return lines.slice(first).join("\n");
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Reads `paths` (package paths, in order) as the generated client carries them.
|
|
57
|
+
* Throws when a module imports something that is not among `paths` before it.
|
|
58
|
+
*/
|
|
59
|
+
export const readCopiedModules = async (paths) => {
|
|
60
|
+
const modules = [];
|
|
61
|
+
for (const [index, path] of paths.entries()) {
|
|
62
|
+
const source = await readFile(packageFile(path), "utf8");
|
|
63
|
+
const earlier = new Set(paths.slice(0, index));
|
|
64
|
+
const text = withoutHeader(source)
|
|
65
|
+
.replace(IMPORT, (_statement, specifier) => {
|
|
66
|
+
const target = resolveImport(path, specifier);
|
|
67
|
+
if (target === undefined || !earlier.has(target)) {
|
|
68
|
+
throw new Error(`Aventara Access: ${path} imports ${JSON.stringify(specifier)}, and a module copied into a generated client imports only the copied modules before it (${[...earlier].join(", ") || "none"})`);
|
|
69
|
+
}
|
|
70
|
+
return "";
|
|
71
|
+
})
|
|
72
|
+
.replace(/^\n+/, "")
|
|
73
|
+
.replace(/\n+$/, "\n");
|
|
74
|
+
if (LEFT_IMPORT.test(text)) {
|
|
75
|
+
throw new Error(`Aventara Access: ${path} loads a module in a form the generator does not copy (a re-export, \`import()\` or \`require\`); a module copied into a generated client imports only the copied modules before it`);
|
|
76
|
+
}
|
|
77
|
+
modules.push({ path, text });
|
|
78
|
+
}
|
|
79
|
+
return modules;
|
|
80
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AccessPolicies } from "../policies-contract.js";
|
|
2
|
+
/** One relation of a Resource, as the generated row types follow it. */
|
|
3
|
+
type RelationFacts = {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly target: string;
|
|
6
|
+
readonly cardinality: "one" | "many";
|
|
7
|
+
};
|
|
8
|
+
/** One Resource of the namespace. */
|
|
9
|
+
export type ResourceFacts = {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
/** The namespace that owns it, when it is not this file's. */
|
|
12
|
+
readonly foreign: string | undefined;
|
|
13
|
+
/** Its scalar fields, sorted. */
|
|
14
|
+
readonly fields: readonly string[];
|
|
15
|
+
/** Its relations, sorted by name. */
|
|
16
|
+
readonly relations: readonly RelationFacts[];
|
|
17
|
+
};
|
|
18
|
+
/** The namespace a generated client decides for. */
|
|
19
|
+
export type NamespaceFacts = {
|
|
20
|
+
readonly namespace: string;
|
|
21
|
+
/** Its Resources, sorted by name. */
|
|
22
|
+
readonly resources: readonly ResourceFacts[];
|
|
23
|
+
};
|
|
24
|
+
/** The facts of a pulled policies file. */
|
|
25
|
+
export declare const namespaceFactsOf: (namespace: string, policies: AccessPolicies) => NamespaceFacts;
|
|
26
|
+
/** A property key as TypeScript writes it: bare when it is an identifier, quoted otherwise. */
|
|
27
|
+
export declare const propertyKey: (name: string) => string;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// What a generated client says about its namespace, read from the pulled policies
|
|
2
|
+
// file alone: its Resources (its own and the foreign ones its rules read), each
|
|
3
|
+
// one's scalar fields and relations. The policies file names fields, never their
|
|
4
|
+
// types, so neither does the generated client. Every list is sorted, so the client's
|
|
5
|
+
// text depends on the file's content and never on its member order.
|
|
6
|
+
const byCodeUnit = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
7
|
+
/** The facts of a pulled policies file. */
|
|
8
|
+
export const namespaceFactsOf = (namespace, policies) => ({
|
|
9
|
+
namespace,
|
|
10
|
+
resources: Object.keys(policies.schema)
|
|
11
|
+
.sort(byCodeUnit)
|
|
12
|
+
.map((name) => {
|
|
13
|
+
const facts = policies.schema[name];
|
|
14
|
+
const relations = facts?.relations ?? {};
|
|
15
|
+
return {
|
|
16
|
+
name,
|
|
17
|
+
foreign: facts?.foreign,
|
|
18
|
+
fields: (facts?.fields ?? [])
|
|
19
|
+
.filter((field) => relations[field] === undefined)
|
|
20
|
+
.sort(byCodeUnit),
|
|
21
|
+
relations: Object.keys(relations)
|
|
22
|
+
.sort(byCodeUnit)
|
|
23
|
+
.map((relation) => ({
|
|
24
|
+
name: relation,
|
|
25
|
+
target: relations[relation]?.target ?? "",
|
|
26
|
+
cardinality: relations[relation]?.cardinality ?? "one",
|
|
27
|
+
})),
|
|
28
|
+
};
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
/** A property key as TypeScript writes it: bare when it is an identifier, quoted otherwise. */
|
|
32
|
+
export const propertyKey = (name) => /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AccessSchema } from "../access-schema.js";
|
|
2
|
+
import type { NamespaceFacts } from "./namespace.facts.js";
|
|
3
|
+
/** What `avaccess schema` read: the `AccessSchema`, and what it left out and why. */
|
|
4
|
+
export type OrmSchemaReading = {
|
|
5
|
+
readonly schema: AccessSchema;
|
|
6
|
+
readonly skipped: readonly string[];
|
|
7
|
+
};
|
|
8
|
+
/** The typed functions a translator adds to the generated client. */
|
|
9
|
+
export type OrmClientPart = {
|
|
10
|
+
/** Their declarations, in TypeScript. */
|
|
11
|
+
readonly text: string;
|
|
12
|
+
/** The names they add to the generated `access` object, in order. */
|
|
13
|
+
readonly members: readonly string[];
|
|
14
|
+
/** What they read from the translator's copied module, in the internal file. */
|
|
15
|
+
readonly imports: {
|
|
16
|
+
readonly values: readonly string[];
|
|
17
|
+
readonly types: readonly string[];
|
|
18
|
+
};
|
|
19
|
+
/** The copied module's types the client file re-exports. */
|
|
20
|
+
readonly exports: readonly string[];
|
|
21
|
+
};
|
|
22
|
+
/** What `avaccess generate` reads of a translator: `none`, or an ORM's. */
|
|
23
|
+
export type ClientTranslator = {
|
|
24
|
+
/** The `--orm` value: `none`, or the ORM and its major, `prisma7`. */
|
|
25
|
+
readonly orm: string;
|
|
26
|
+
/** The ORM's name and major as a reader says them, `Prisma 7`; `undefined` for `none`. */
|
|
27
|
+
readonly title: string | undefined;
|
|
28
|
+
/** The package paths of the translator's own modules, copied after the decisions. */
|
|
29
|
+
readonly modules: readonly string[];
|
|
30
|
+
/** The functions the generated client adds for this ORM. */
|
|
31
|
+
clientPart(namespace: NamespaceFacts): OrmClientPart;
|
|
32
|
+
};
|
|
33
|
+
/** One ORM's translator: its client part, and its schema reader. */
|
|
34
|
+
export type OrmTranslator = ClientTranslator & {
|
|
35
|
+
readonly title: string;
|
|
36
|
+
/** The schema `avaccess schema` reads when `--schema` is not given, from the project's root. */
|
|
37
|
+
readonly defaultSchema: string;
|
|
38
|
+
/** `avaccess schema`: the ORM's schema at `path` as an `AccessSchema`. */
|
|
39
|
+
readSchema(path: string): Promise<OrmSchemaReading>;
|
|
40
|
+
};
|
|
41
|
+
/** Every ORM translator, by its `--orm` value: what `avaccess schema` reads with. */
|
|
42
|
+
export declare const ORM_TRANSLATORS: {
|
|
43
|
+
readonly [orm: string]: OrmTranslator;
|
|
44
|
+
};
|
|
45
|
+
/** Every translator `avaccess generate` takes, by its `--orm` value. */
|
|
46
|
+
export declare const CLIENT_TRANSLATORS: {
|
|
47
|
+
readonly [orm: string]: ClientTranslator;
|
|
48
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The translator seam: one translator per ORM major, chosen with `--orm`. A client
|
|
2
|
+
// translator gives `avaccess generate` the modules it copies after the decisions and
|
|
3
|
+
// the typed functions the generated client adds for its ORM; an ORM translator also
|
|
4
|
+
// gives `avaccess schema` the reader that turns its ORM's schema into an
|
|
5
|
+
// `AccessSchema`. The decisions stay ORM-neutral: they answer in Aventara's `where`,
|
|
6
|
+
// and only a translator speaks an ORM's words.
|
|
7
|
+
//
|
|
8
|
+
// `none` (the default for `generate`, ruled 2026-10-23) adds nothing: the client
|
|
9
|
+
// answers in the neutral `where` and names no ORM. Prisma 7 is the first ORM
|
|
10
|
+
// (`prisma7`). Drizzle follows as `drizzle1` once Drizzle ORM v1 is stable.
|
|
11
|
+
import { prisma7Translator } from "./prisma7/prisma7.translator.js";
|
|
12
|
+
/** Every ORM translator, by its `--orm` value: what `avaccess schema` reads with. */
|
|
13
|
+
export const ORM_TRANSLATORS = {
|
|
14
|
+
prisma7: prisma7Translator,
|
|
15
|
+
};
|
|
16
|
+
/** `--orm none`: a client that answers in the neutral `where` and names no ORM. */
|
|
17
|
+
const NO_TRANSLATOR = {
|
|
18
|
+
orm: "none",
|
|
19
|
+
title: undefined,
|
|
20
|
+
modules: [],
|
|
21
|
+
clientPart: () => ({
|
|
22
|
+
text: "",
|
|
23
|
+
members: [],
|
|
24
|
+
imports: { values: [], types: [] },
|
|
25
|
+
exports: [],
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
/** Every translator `avaccess generate` takes, by its `--orm` value. */
|
|
29
|
+
export const CLIENT_TRANSLATORS = { none: NO_TRANSLATOR, ...ORM_TRANSLATORS };
|