@aventara/access 0.0.0-stage → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +877 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +2536 -2
- package/dist/access/access-options.d.ts +74 -0
- package/dist/access/access-options.js +5 -0
- package/dist/access/access.error.d.ts +35 -0
- package/dist/access/access.error.js +11 -0
- package/dist/access/access.factory.d.ts +275 -0
- package/dist/access/access.factory.js +440 -0
- package/dist/access/better-auth.hardening.d.ts +120 -0
- package/dist/access/better-auth.hardening.js +420 -0
- package/dist/access/better-auth.loader.d.ts +67 -0
- package/dist/access/better-auth.loader.js +65 -0
- package/dist/access/better-auth.range.d.ts +9 -0
- package/dist/access/better-auth.range.js +28 -0
- package/dist/access/better-auth.require.d.ts +90 -0
- package/dist/access/better-auth.require.js +6 -0
- package/dist/access/better-auth.require.node.d.ts +7 -0
- package/dist/access/better-auth.require.node.js +52 -0
- package/dist/access/claims.mapper.d.ts +79 -0
- package/dist/access/claims.mapper.js +152 -0
- package/dist/access/key-set.cache.d.ts +10 -0
- package/dist/access/key-set.cache.js +63 -0
- package/dist/access/policies-block.checker.d.ts +64 -0
- package/dist/access/policies-block.checker.js +138 -0
- package/dist/access/principal.subject.d.ts +22 -0
- package/dist/access/principal.subject.js +89 -0
- package/dist/access/profile.policy.d.ts +74 -0
- package/dist/access/profile.policy.js +110 -0
- package/dist/access/request.state.d.ts +89 -0
- package/dist/access/request.state.js +266 -0
- package/dist/access/session.hooks.d.ts +114 -0
- package/dist/access/session.hooks.js +258 -0
- package/dist/access/token.verifier.d.ts +35 -0
- package/dist/access/token.verifier.js +69 -0
- package/dist/access-schema.d.ts +48 -0
- package/dist/access-schema.js +5 -0
- package/dist/access-warning.emitter.d.ts +4 -0
- package/dist/access-warning.emitter.js +24 -0
- package/dist/access.builder.d.ts +340 -0
- package/dist/access.builder.js +75 -0
- package/dist/access.engine.d.ts +155 -0
- package/dist/access.engine.js +477 -0
- package/dist/audit/audit-event.d.ts +135 -0
- package/dist/audit/audit-event.js +8 -0
- package/dist/audit/audit.log.d.ts +122 -0
- package/dist/audit/audit.log.js +345 -0
- package/dist/audit/audit.schema.d.ts +143 -0
- package/dist/audit/audit.schema.js +50 -0
- package/dist/auth/index.d.ts +12 -0
- package/dist/auth/index.js +2 -0
- package/dist/aventara-where.translator.d.ts +19 -0
- package/dist/aventara-where.translator.js +87 -0
- package/dist/beacon/beacon.check.d.ts +59 -0
- package/dist/beacon/beacon.check.js +128 -0
- package/dist/beacon/beacon.schema.d.ts +41 -0
- package/dist/beacon/beacon.schema.js +48 -0
- package/dist/beacon/beacon.store.d.ts +29 -0
- package/dist/beacon/beacon.store.js +78 -0
- package/dist/better-auth/access.client-plugin.d.ts +75 -0
- package/dist/better-auth/access.client-plugin.js +133 -0
- package/dist/better-auth/access.plugin.d.ts +164 -0
- package/dist/better-auth/access.plugin.js +348 -0
- package/dist/better-auth/access.provider.d.ts +80 -0
- package/dist/better-auth/access.provider.js +100 -0
- package/dist/better-auth/index.d.ts +7 -0
- package/dist/better-auth/index.js +4 -0
- package/dist/better-auth/organization.bridge.d.ts +66 -0
- package/dist/better-auth/organization.bridge.js +144 -0
- package/dist/canonical-json.writer.d.ts +12 -0
- package/dist/canonical-json.writer.js +30 -0
- package/dist/cli/avaccess.bin.d.ts +2 -0
- package/dist/cli/avaccess.bin.js +26 -0
- package/dist/cli/avaccess.program.d.ts +15 -0
- package/dist/cli/avaccess.program.js +166 -0
- package/dist/cli/beacon-pins.file.d.ts +14 -0
- package/dist/cli/beacon-pins.file.js +62 -0
- package/dist/cli/beacon.loader.d.ts +20 -0
- package/dist/cli/beacon.loader.js +71 -0
- package/dist/cli/cli.context.d.ts +49 -0
- package/dist/cli/cli.context.js +66 -0
- package/dist/cli/diff.command.d.ts +2 -0
- package/dist/cli/diff.command.js +66 -0
- package/dist/cli/generate.command.d.ts +2 -0
- package/dist/cli/generate.command.js +82 -0
- package/dist/cli/keys.command.d.ts +2 -0
- package/dist/cli/keys.command.js +96 -0
- package/dist/cli/lock.file.d.ts +31 -0
- package/dist/cli/lock.file.js +181 -0
- package/dist/cli/module.loader.d.ts +13 -0
- package/dist/cli/module.loader.js +63 -0
- package/dist/cli/policies.diff.d.ts +16 -0
- package/dist/cli/policies.diff.js +101 -0
- package/dist/cli/pull.command.d.ts +2 -0
- package/dist/cli/pull.command.js +234 -0
- package/dist/cli/push.command.d.ts +2 -0
- package/dist/cli/push.command.js +59 -0
- package/dist/cli/registry.client.d.ts +63 -0
- package/dist/cli/registry.client.js +287 -0
- package/dist/cli/schema.command.d.ts +2 -0
- package/dist/cli/schema.command.js +60 -0
- package/dist/cli/serve.command.d.ts +6 -0
- package/dist/cli/serve.command.js +200 -0
- package/dist/cli/status.command.d.ts +2 -0
- package/dist/cli/status.command.js +31 -0
- package/dist/cli/translator.flag.d.ts +6 -0
- package/dist/cli/translator.flag.js +24 -0
- package/dist/cli/verify.command.d.ts +2 -0
- package/dist/cli/verify.command.js +40 -0
- package/dist/client/access-client.factory.d.ts +92 -0
- package/dist/client/access-client.factory.js +269 -0
- package/dist/client/access-verbs.schema.d.ts +66 -0
- package/dist/client/access-verbs.schema.js +159 -0
- package/dist/client/verb-rate.limiter.d.ts +116 -0
- package/dist/client/verb-rate.limiter.js +358 -0
- package/dist/foreign/foreign-facts.d.ts +30 -0
- package/dist/foreign/foreign-facts.js +75 -0
- package/dist/foreign/foreign.merger.d.ts +10 -0
- package/dist/foreign/foreign.merger.js +52 -0
- package/dist/foreign-references.check.d.ts +6 -0
- package/dist/foreign-references.check.js +131 -0
- package/dist/generator/client.emitter.d.ts +28 -0
- package/dist/generator/client.emitter.js +411 -0
- package/dist/generator/copied-modules.reader.d.ts +14 -0
- package/dist/generator/copied-modules.reader.js +80 -0
- package/dist/generator/namespace.facts.d.ts +28 -0
- package/dist/generator/namespace.facts.js +32 -0
- package/dist/generator/orm.translator.d.ts +48 -0
- package/dist/generator/orm.translator.js +29 -0
- package/dist/generator/prisma7/prisma-schema.reader.d.ts +9 -0
- package/dist/generator/prisma7/prisma-schema.reader.js +394 -0
- package/dist/generator/prisma7/prisma-where.translator.d.ts +73 -0
- package/dist/generator/prisma7/prisma-where.translator.js +138 -0
- package/dist/generator/prisma7/prisma7.translator.d.ts +2 -0
- package/dist/generator/prisma7/prisma7.translator.js +87 -0
- package/dist/generator/schema.emitter.d.ts +14 -0
- package/dist/generator/schema.emitter.js +23 -0
- package/dist/generator/tsconfig.reader.d.ts +44 -0
- package/dist/generator/tsconfig.reader.js +110 -0
- package/dist/generator/typescript-literal.writer.d.ts +5 -0
- package/dist/generator/typescript-literal.writer.js +42 -0
- package/dist/guardrails/guardrails-contract.d.ts +45 -0
- package/dist/guardrails/guardrails-contract.js +12 -0
- package/dist/guardrails/guardrails.builder.d.ts +48 -0
- package/dist/guardrails/guardrails.builder.js +32 -0
- package/dist/guardrails/guardrails.compiler.d.ts +10 -0
- package/dist/guardrails/guardrails.compiler.js +254 -0
- package/dist/guardrails/guardrails.composer.d.ts +7 -0
- package/dist/guardrails/guardrails.composer.js +95 -0
- package/dist/guardrails/guardrails.hash.d.ts +10 -0
- package/dist/guardrails/guardrails.hash.js +38 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +6 -0
- package/dist/kiosk/kiosk-verbs.schema.d.ts +19 -0
- package/dist/kiosk/kiosk-verbs.schema.js +61 -0
- package/dist/kiosk/kiosk.plugin.d.ts +217 -0
- package/dist/kiosk/kiosk.plugin.js +877 -0
- package/dist/kiosk/kiosk.schema.d.ts +176 -0
- package/dist/kiosk/kiosk.schema.js +101 -0
- package/dist/kiosk/kiosk.store.d.ts +171 -0
- package/dist/kiosk/kiosk.store.js +478 -0
- package/dist/kiosk/lan-range.policy.d.ts +31 -0
- package/dist/kiosk/lan-range.policy.js +113 -0
- package/dist/kiosk/pin.policy.d.ts +9 -0
- package/dist/kiosk/pin.policy.js +143 -0
- package/dist/kiosk/session-use.marker.d.ts +4 -0
- package/dist/kiosk/session-use.marker.js +16 -0
- package/dist/lock/lock-contract.d.ts +38 -0
- package/dist/lock/lock-contract.js +67 -0
- package/dist/lock/lock.verifier.d.ts +27 -0
- package/dist/lock/lock.verifier.js +62 -0
- package/dist/mcp/agent-scope.gate.d.ts +13 -0
- package/dist/mcp/agent-scope.gate.js +75 -0
- package/dist/mcp/agent.grant.d.ts +40 -0
- package/dist/mcp/agent.grant.js +92 -0
- package/dist/mcp/mcp-options.d.ts +60 -0
- package/dist/mcp/mcp-options.js +72 -0
- package/dist/oauth/access-token.verifier.d.ts +66 -0
- package/dist/oauth/access-token.verifier.js +201 -0
- package/dist/oauth/oauth-provider.hardening.d.ts +62 -0
- package/dist/oauth/oauth-provider.hardening.js +261 -0
- package/dist/passwords/argon2id.hasher.d.ts +97 -0
- package/dist/passwords/argon2id.hasher.js +256 -0
- package/dist/passwords/node-rs-argon2.adapter.d.ts +25 -0
- package/dist/passwords/node-rs-argon2.adapter.js +42 -0
- package/dist/passwords/passwords.options.d.ts +79 -0
- package/dist/passwords/passwords.options.js +212 -0
- package/dist/policies-contract.d.ts +219 -0
- package/dist/policies-contract.js +8 -0
- package/dist/policies.compiler.d.ts +38 -0
- package/dist/policies.compiler.js +1433 -0
- package/dist/policies.decider.d.ts +103 -0
- package/dist/policies.decider.js +390 -0
- package/dist/policies.hash.d.ts +10 -0
- package/dist/policies.hash.js +54 -0
- package/dist/predicate.evaluator.d.ts +62 -0
- package/dist/predicate.evaluator.js +378 -0
- package/dist/random-secret.generator.d.ts +2 -0
- package/dist/random-secret.generator.js +15 -0
- package/dist/read-governed-unique.check.d.ts +3 -0
- package/dist/read-governed-unique.check.js +21 -0
- package/dist/reference-reads.check.d.ts +16 -0
- package/dist/reference-reads.check.js +158 -0
- package/dist/registry/access.registry.d.ts +173 -0
- package/dist/registry/access.registry.js +865 -0
- package/dist/registry/beacon.ledger.d.ts +30 -0
- package/dist/registry/beacon.ledger.js +93 -0
- package/dist/registry/ownership.check.d.ts +54 -0
- package/dist/registry/ownership.check.js +221 -0
- package/dist/registry/registry.endpoints.d.ts +50 -0
- package/dist/registry/registry.endpoints.js +193 -0
- package/dist/registry/registry.key.d.ts +7 -0
- package/dist/registry/registry.key.js +13 -0
- package/dist/registry/registry.options.d.ts +13 -0
- package/dist/registry/registry.options.js +28 -0
- package/dist/registry/registry.refusal.d.ts +6 -0
- package/dist/registry/registry.refusal.js +11 -0
- package/dist/registry/registry.schema.d.ts +238 -0
- package/dist/registry/registry.schema.js +127 -0
- package/dist/resources/auth-tables.catalog.d.ts +96 -0
- package/dist/resources/auth-tables.catalog.js +110 -0
- package/dist/resources/auth-tables.guard.d.ts +106 -0
- package/dist/resources/auth-tables.guard.js +462 -0
- package/dist/scope.resolver.d.ts +144 -0
- package/dist/scope.resolver.js +529 -0
- package/dist/sha256.digest.d.ts +2 -0
- package/dist/sha256.digest.js +81 -0
- package/dist/standalone/access.server.d.ts +41 -0
- package/dist/standalone/access.server.js +301 -0
- package/dist/standalone/index.d.ts +1 -0
- package/dist/standalone/index.js +1 -0
- package/package.json +102 -3
- package/skill/SKILL.md +170 -0
- package/skill/reference/callers.md +99 -0
- package/skill/reference/errors.md +89 -0
- package/skill/reference/recipes.md +281 -0
- package/skill/reference/rules.md +153 -0
- package/skill/reference/setup.md +334 -0
- package/src/aventara-where.translator.ts +116 -0
- package/src/beacon/beacon.check.ts +204 -0
- package/src/canonical-json.writer.ts +51 -0
- package/src/generator/prisma7/prisma-where.translator.ts +243 -0
- package/src/policies-contract.ts +215 -0
- package/src/policies.decider.ts +662 -0
- package/src/predicate.evaluator.ts +574 -0
- package/src/scope.resolver.ts +819 -0
- package/src/sha256.digest.ts +85 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// A check of the rules against the framework that hosts them: a role that may
|
|
2
|
+
// create or update a row holding a reference (a to-one relation's reference fields)
|
|
3
|
+
// to a foreign Resource, with no condition of the write grant through that
|
|
4
|
+
// relation. A foreign Resource is read in conditions and never granted on, so the
|
|
5
|
+
// engine lists it in `unheld` and Aventara holds no written reference to it to the
|
|
6
|
+
// caller's read scope (which is `false` for every caller there): the write grant
|
|
7
|
+
// alone decides which row may be linked. A grant that reads nothing through the
|
|
8
|
+
// relation lets the caller link any row of the foreign Resource by its identifier.
|
|
9
|
+
//
|
|
10
|
+
// A grant constrains the link when one of the conditions checked on the row the
|
|
11
|
+
// write leaves reads the relation: on a create, its `where`, its `in`, and the row
|
|
12
|
+
// condition of a subject role held on the writing Resource; on an update, those and
|
|
13
|
+
// its `after` (an update grant's `where` is checked on the row it leaves too). A
|
|
14
|
+
// condition reads the relation when it filters through it (`{ campaign: { … } }`)
|
|
15
|
+
// or compares its reference field with `equals` or `in` (`{ campaignId: me.id }`),
|
|
16
|
+
// itself or in every branch of an `OR` or any part of an `AND`; one under a `NOT`,
|
|
17
|
+
// or a `not` / `notIn` comparison, excludes rows and leaves every other one
|
|
18
|
+
// linkable. "Constrains" says nothing of whether the condition is the right one:
|
|
19
|
+
// it only finds the grants that check nothing.
|
|
20
|
+
//
|
|
21
|
+
// It runs when the framework hosts the engine (`hosted({ held })`), as
|
|
22
|
+
// ACCESS_REFERENCE_UNREADABLE does, for the writing Resources the framework serves
|
|
23
|
+
// (`held`). It warns and never refuses: the engine answers exactly as before. Not
|
|
24
|
+
// warned about: a reference the grant's `write.except` locks.
|
|
25
|
+
import { emitAccessWarning, listed } from "./access-warning.emitter.js";
|
|
26
|
+
import { ownOf } from "./scope.resolver.js";
|
|
27
|
+
const WARNING_CODE = "ACCESS_FOREIGN_REFERENCE_UNCHECKED";
|
|
28
|
+
/** Whether `predicate`, holding on a row, limits which row its `relation` links. */
|
|
29
|
+
const constrains = (predicate, relation, fields) => {
|
|
30
|
+
if (predicate === undefined) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
switch (predicate.op) {
|
|
34
|
+
case "false":
|
|
35
|
+
return true;
|
|
36
|
+
case "and":
|
|
37
|
+
return predicate.args.some((arg) => constrains(arg, relation, fields));
|
|
38
|
+
case "or":
|
|
39
|
+
return (predicate.args.length > 0 &&
|
|
40
|
+
predicate.args.every((arg) => constrains(arg, relation, fields)));
|
|
41
|
+
case "cmp":
|
|
42
|
+
return (fields.includes(predicate.field) &&
|
|
43
|
+
(predicate.cmp === "eq" || predicate.cmp === "in"));
|
|
44
|
+
case "rel":
|
|
45
|
+
return predicate.relation === relation && predicate.where.op !== "true";
|
|
46
|
+
default:
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/** The row condition of `subject` when it is a role held on rows of `resource`. */
|
|
51
|
+
const subjectRowCondition = (policies, subject, resource) => {
|
|
52
|
+
const definition = ownOf(policies.roles, subject);
|
|
53
|
+
return definition !== undefined &&
|
|
54
|
+
"on" in definition &&
|
|
55
|
+
definition.on === resource
|
|
56
|
+
? definition.where
|
|
57
|
+
: undefined;
|
|
58
|
+
};
|
|
59
|
+
/** Whether `grant`, for `subject`, constrains the link `relation` makes on `action`. */
|
|
60
|
+
const grantConstrains = (policies, grant, subject, resource, action, relation, fields) => grant.in === relation ||
|
|
61
|
+
constrains(grant.where, relation, fields) ||
|
|
62
|
+
(action === "update" && constrains(grant.after, relation, fields)) ||
|
|
63
|
+
constrains(subjectRowCondition(policies, subject, resource), relation, fields);
|
|
64
|
+
/**
|
|
65
|
+
* The warnings for `policies` hosted by a framework that serves the Resources in
|
|
66
|
+
* `held`, one per writing role and foreign target, each naming every reference
|
|
67
|
+
* that role may write to it unconstrained.
|
|
68
|
+
*/
|
|
69
|
+
const foreignReferenceWarnings = (policies, held) => {
|
|
70
|
+
const found = new Map();
|
|
71
|
+
for (const grant of policies.grants) {
|
|
72
|
+
const actions = grant.actions.filter((action) => action === "create" || action === "update");
|
|
73
|
+
if (actions.length === 0) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const locked = new Set(grant.write?.except ?? []);
|
|
77
|
+
const resources = grant.resource === "*" ? Object.keys(policies.schema) : [grant.resource];
|
|
78
|
+
for (const resource of resources) {
|
|
79
|
+
const facts = ownOf(policies.schema, resource);
|
|
80
|
+
if (facts === undefined ||
|
|
81
|
+
facts.foreign !== undefined ||
|
|
82
|
+
!held.has(resource)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
for (const [relation, fact] of Object.entries(facts.relations)) {
|
|
86
|
+
const fields = fact.referenceFields;
|
|
87
|
+
const owner = ownOf(policies.schema, fact.target)?.foreign;
|
|
88
|
+
if (owner === undefined ||
|
|
89
|
+
fact.cardinality !== "one" ||
|
|
90
|
+
fields === undefined ||
|
|
91
|
+
locked.has(relation) ||
|
|
92
|
+
fields.some((field) => locked.has(field))) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
for (const writer of grant.subjects) {
|
|
96
|
+
if (actions.every((action) => grantConstrains(policies, grant, writer, resource, action, relation, fields))) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const key = `${writer}\u0000${fact.target}`;
|
|
100
|
+
const entry = found.get(key) ?? {
|
|
101
|
+
writer,
|
|
102
|
+
target: fact.target,
|
|
103
|
+
owner,
|
|
104
|
+
relation,
|
|
105
|
+
references: [],
|
|
106
|
+
};
|
|
107
|
+
for (const field of fields) {
|
|
108
|
+
const name = `${resource}.${field}`;
|
|
109
|
+
if (!entry.references.includes(name)) {
|
|
110
|
+
entry.references.push(name);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
found.set(key, entry);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [...found.values()].map(({ writer, target, owner, relation, references }) => {
|
|
119
|
+
const one = references.length === 1;
|
|
120
|
+
return `${writer} may write ${listed(references)}, ${one ? "a reference" : "references"} to Resource "${target}", which namespace "${owner}" owns (foreign), and no condition of the write grant reads that relation, so any row of "${target}" can be linked by its identifier. Aventara does not hold a reference to a foreign Resource to what the caller may read: the write grant decides which row may be linked. Constrain it through the relation (a \`where\` on a create grant, an \`after\` on an update grant, e.g. \`{ ${relation}: { … } }\`), or leave the reference out of the write (\`write: { except: [...] }\`).`;
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Emits `policies`' warnings under a framework that serves the Resources in
|
|
125
|
+
* `held`, each once per process and namespace.
|
|
126
|
+
*/
|
|
127
|
+
export const warnForeignReferences = (policies, held) => {
|
|
128
|
+
for (const message of foreignReferenceWarnings(policies, held)) {
|
|
129
|
+
emitAccessWarning(WARNING_CODE, policies.namespace, message);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
@@ -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
|
+
};
|