@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,155 @@
|
|
|
1
|
+
import type { AccessWhere } from "./aventara-where.translator.js";
|
|
2
|
+
import { type AccessBeaconPins, type AccessBeaconReader } from "./beacon/beacon.check.js";
|
|
3
|
+
import type { AccessGuardrails, AccessGuardrailsJson } from "./guardrails/guardrails-contract.js";
|
|
4
|
+
import type { AccessLock } from "./lock/lock-contract.js";
|
|
5
|
+
import { type AccessCanQuestion, type AccessFields, type AccessFieldsQuestion, type AccessGrantsQuestion, type AccessRuleOutcome, type AccessScopeQuestion } from "./policies.decider.js";
|
|
6
|
+
import type { AccessPolicies, AccessPoliciesJson } from "./policies-contract.js";
|
|
7
|
+
import type { AccessLookup } from "./predicate.evaluator.js";
|
|
8
|
+
import { type AuthTablesGuard } from "./resources/auth-tables.guard.js";
|
|
9
|
+
/** The `govern` block, by Resource: the list form (each field for both) or a mode per field. */
|
|
10
|
+
type GovernedShape = {
|
|
11
|
+
readonly [resource: string]: readonly string[] | {
|
|
12
|
+
readonly [field: string]: "read" | "write" | "both";
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
/** One Resource's fields governed for reading: its list, or its fields not `"write"`. */
|
|
16
|
+
type ReadGoverned<G> = G extends readonly string[] ? G[number] : {
|
|
17
|
+
[F in keyof G]: G[F] extends "write" ? never : F;
|
|
18
|
+
}[keyof G] & string;
|
|
19
|
+
/** `explain`'s answer: the decision, each rule's part in it, and the `exists` lookups. */
|
|
20
|
+
export type AccessExplanation = {
|
|
21
|
+
readonly allowed: boolean;
|
|
22
|
+
readonly grants: readonly {
|
|
23
|
+
readonly id: string;
|
|
24
|
+
readonly subject: string;
|
|
25
|
+
readonly outcome: AccessRuleOutcome;
|
|
26
|
+
}[];
|
|
27
|
+
readonly denies: readonly {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly outcome: AccessRuleOutcome;
|
|
30
|
+
}[];
|
|
31
|
+
readonly lookups: readonly AccessLookup[];
|
|
32
|
+
};
|
|
33
|
+
/** The engine `access.engine()` and `createAccessEngine(policies)` return. */
|
|
34
|
+
export interface AccessEngine<Governed extends GovernedShape = GovernedShape> {
|
|
35
|
+
/**
|
|
36
|
+
* The fields governed for reading (`"read"`, `"both"` or the list form), by
|
|
37
|
+
* Resource: out of every default projection. A `"write"` field is read as any other
|
|
38
|
+
* field, so it is not here.
|
|
39
|
+
*/
|
|
40
|
+
readonly governed: {
|
|
41
|
+
readonly [R in keyof Governed as [ReadGoverned<Governed[R]>] extends [never] ? never : R]: Governed[R] extends readonly string[] ? Governed[R] : readonly ReadGoverned<Governed[R]>[];
|
|
42
|
+
};
|
|
43
|
+
/** Every relation path and operator each rule filters on, by rule id. */
|
|
44
|
+
readonly paths: {
|
|
45
|
+
readonly [rule: string]: readonly (readonly string[])[];
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* The Resources where the row an update leaves is checked: an update grant there
|
|
49
|
+
* reads the row (`where`, `after`, a role held on a row), or an update deny has a
|
|
50
|
+
* `where`.
|
|
51
|
+
*/
|
|
52
|
+
readonly afterChecks: {
|
|
53
|
+
readonly [resource: string]: readonly "update"[];
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* The Resources the rules read as foreign: read in conditions, never granted on,
|
|
57
|
+
* so no caller reads a row of one. Aventara (`@aventara/core` `1.0.0-rc.10` or
|
|
58
|
+
* later) then holds no written reference to one to the caller's read scope: the
|
|
59
|
+
* write grant decides which row may be linked (its `where`, its `after`).
|
|
60
|
+
*/
|
|
61
|
+
readonly unheld: readonly string[];
|
|
62
|
+
can(question: AccessCanQuestion): boolean | Promise<boolean>;
|
|
63
|
+
/**
|
|
64
|
+
* The update grants that cover `row`, the row an update in `afterChecks` will
|
|
65
|
+
* change, as it is before the write: each grant's `where` and its subject's row
|
|
66
|
+
* condition, relation conditions asked through `exists`. Core asks it before the
|
|
67
|
+
* write and hands the answer back to `can` as `beforeGrants` (`@aventara/core`
|
|
68
|
+
* `1.0.0-rc.10` or later). The keys are opaque: a grant's place in the file, no
|
|
69
|
+
* rule data.
|
|
70
|
+
*/
|
|
71
|
+
grantsOf(question: AccessGrantsQuestion): readonly string[] | Promise<readonly string[]>;
|
|
72
|
+
scopeOf(question: AccessScopeQuestion): boolean | AccessWhere;
|
|
73
|
+
fieldsOf(question: AccessFieldsQuestion): AccessFields;
|
|
74
|
+
/**
|
|
75
|
+
* Called by the framework once it is built on this engine (`@aventara/core`
|
|
76
|
+
* `1.0.0-rc.10` or later): `held` lists the Resources a written reference is
|
|
77
|
+
* held to the caller's read scope of. The rules are checked against them, and a
|
|
78
|
+
* role that may write a reference to one no read grant reaches for it is warned
|
|
79
|
+
* about (ACCESS_REFERENCE_UNREADABLE), and so is a role whose create or update
|
|
80
|
+
* grant may write a reference to a foreign Resource with no condition through
|
|
81
|
+
* that relation (ACCESS_FOREIGN_REFERENCE_UNCHECKED), each once per process and
|
|
82
|
+
* namespace. It changes nothing the engine answers.
|
|
83
|
+
*/
|
|
84
|
+
hosted(host: {
|
|
85
|
+
readonly held: readonly string[];
|
|
86
|
+
}): void;
|
|
87
|
+
/** Development only: why `can` answers as it does. Throws when `NODE_ENV` is `production`. */
|
|
88
|
+
explain(question: AccessCanQuestion): Promise<AccessExplanation>;
|
|
89
|
+
/**
|
|
90
|
+
* The startup check that the service reaches the database of each namespace
|
|
91
|
+
* whose Resources the rules read as foreign: its row in `_aventara_beacon`, read
|
|
92
|
+
* through `reader` (the service's own connection), hashes to its pin in `pins`,
|
|
93
|
+
* the pins of `environment` (`avAccess.beacons.<environment>.json`). Await it
|
|
94
|
+
* before the service takes requests: an engine whose rules read another
|
|
95
|
+
* namespace answers no question until it has passed, and none once a check has
|
|
96
|
+
* failed, for good (`ACCESS_BEACON_MISMATCH`); calls run one after another. An
|
|
97
|
+
* engine that reads no other namespace passes at once.
|
|
98
|
+
*/
|
|
99
|
+
checkBeacons(reader: AccessBeaconReader, pins: AccessBeaconPins, environment: string): Promise<void>;
|
|
100
|
+
}
|
|
101
|
+
/** What an engine is built with beside its policies file. */
|
|
102
|
+
export type AccessEngineOptions = {
|
|
103
|
+
/**
|
|
104
|
+
* The org's guardrails, as pulled from the registry: deny-only rules on top of the
|
|
105
|
+
* file's own. They only narrow; the governed fields, the paths and the types are
|
|
106
|
+
* the file's.
|
|
107
|
+
*/
|
|
108
|
+
readonly guardrails?: AccessGuardrails | AccessGuardrailsJson;
|
|
109
|
+
/**
|
|
110
|
+
* The project's `avAccess.lock.json`, as `avaccess pull` wrote it. With it, the
|
|
111
|
+
* engine refuses to start (`ACCESS_LOCK_MISMATCH`) unless what it runs from is
|
|
112
|
+
* what was pulled: the guardrails hash to the lock's pin (and are given exactly
|
|
113
|
+
* when it pins some), and a policies file handed to `createAccessEngine` hashes
|
|
114
|
+
* to its own. Typed rules (`rules.engine()`) are the project's own source: only
|
|
115
|
+
* their namespace is held to the lock's, unless `requireLock` is set.
|
|
116
|
+
*/
|
|
117
|
+
readonly lock?: AccessLock;
|
|
118
|
+
/**
|
|
119
|
+
* `true`: refuse to build without `lock` (`ACCESS_LOCK_MISMATCH`), so a deploy that
|
|
120
|
+
* lost its `avAccess.lock.json` fails at startup instead of running unpinned files;
|
|
121
|
+
* typed rules are then held to the lock's policies hash too. Recommended in
|
|
122
|
+
* production.
|
|
123
|
+
*/
|
|
124
|
+
readonly requireLock?: boolean;
|
|
125
|
+
};
|
|
126
|
+
/** Where an engine's policies file comes from: a file handed over, or the project's typed rules. */
|
|
127
|
+
export type PoliciesSource = "file" | "rules";
|
|
128
|
+
/**
|
|
129
|
+
* What decided one `can` question: the grant that allowed it, or the deny that
|
|
130
|
+
* refused it; `null` when it was refused because no grant matched.
|
|
131
|
+
*/
|
|
132
|
+
export type DecidingRule = {
|
|
133
|
+
readonly kind: "grant" | "deny";
|
|
134
|
+
readonly id: string;
|
|
135
|
+
} | null;
|
|
136
|
+
/**
|
|
137
|
+
* Told of every `can` answer, after it is known and before `can` returns it: the
|
|
138
|
+
* audit log's hook into the engine. `can` waits for what it returns.
|
|
139
|
+
*/
|
|
140
|
+
export type DecisionObserver = (question: AccessCanQuestion, allowed: boolean, rule: DecidingRule) => void | Promise<void>;
|
|
141
|
+
/**
|
|
142
|
+
* Builds the engine from a policies file and, optionally, the org's guardrails. Both
|
|
143
|
+
* are checked first; a bad one throws, naming the rule. With `lock`, both are held
|
|
144
|
+
* to the hashes `avAccess.lock.json` pins (`ACCESS_LOCK_MISMATCH`).
|
|
145
|
+
*/
|
|
146
|
+
export declare const createAccessEngine: <Governed extends GovernedShape = GovernedShape>(policies: AccessPolicies<Governed> | AccessPoliciesJson<Governed>, options?: AccessEngineOptions) => AccessEngine<Governed>;
|
|
147
|
+
/**
|
|
148
|
+
* The engine, from a policies file that came from `source`: with a `lock`, a file
|
|
149
|
+
* handed over is held to the lock's pin, and typed rules (the project's own
|
|
150
|
+
* source) to its namespace only. `createAccess` adds its built-in rules over Better
|
|
151
|
+
* Auth's tables (`guard`): their denies join the file's, and `fieldsOf` never lists a
|
|
152
|
+
* secret column or a field a write may not set.
|
|
153
|
+
*/
|
|
154
|
+
export declare const engineOf: <Governed extends GovernedShape = GovernedShape>(policies: AccessPolicies<Governed>, options: AccessEngineOptions | undefined, source: PoliciesSource, observe?: DecisionObserver, guard?: AuthTablesGuard) => AccessEngine<Governed>;
|
|
155
|
+
export {};
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
// The engine: a plain object that answers core's `AvPolicyEngine` questions by shape
|
|
2
|
+
// (`can`, `grantsOf`, `scopeOf`, `fieldsOf`, the startup facts `governed`,
|
|
3
|
+
// `paths`, `afterChecks`, `unheld` (the Resources the rules read as foreign), and
|
|
4
|
+
// `hosted`, where the framework says which Resources it holds a written reference
|
|
5
|
+
// to and the rules are checked against them, reference-reads.check.ts and
|
|
6
|
+
// foreign-references.check.ts), run from a `@1` policies file alone. Default deny, any grant
|
|
7
|
+
// allows, any deny wins, order never matters. Neither package imports the other.
|
|
8
|
+
// The decisions themselves are `policies.decider.ts`'s; the engine adds the lock,
|
|
9
|
+
// the guardrails, the decision observer, `explain`, the startup facts and, when it
|
|
10
|
+
// runs from a lock and its rules read other namespaces' Resources as foreign, the
|
|
11
|
+
// startup check that the service reaches their database (`checkBeacons`): until it
|
|
12
|
+
// has passed, the engine decides nothing.
|
|
13
|
+
import { AccessError } from "./access/access.error.js";
|
|
14
|
+
import { operatorOf } from "./aventara-where.translator.js";
|
|
15
|
+
import { beaconProblem, } from "./beacon/beacon.check.js";
|
|
16
|
+
import { warnForeignReferences } from "./foreign-references.check.js";
|
|
17
|
+
import { composeGuardrails } from "./guardrails/guardrails.composer.js";
|
|
18
|
+
import { foreignOwnersOf, verifyLocked } from "./lock/lock.verifier.js";
|
|
19
|
+
import { checkLock } from "./lock/lock-contract.js";
|
|
20
|
+
import { checkPolicies, fail, isRecord } from "./policies.compiler.js";
|
|
21
|
+
import { afterChecksOf, createDecider, fieldsQuestionProblem, grantsQuestionProblem, newTrace, } from "./policies.decider.js";
|
|
22
|
+
import { warnReadGovernedUnique } from "./read-governed-unique.check.js";
|
|
23
|
+
import { warnReferenceReads } from "./reference-reads.check.js";
|
|
24
|
+
import { guardedFields, withBuiltInRules, } from "./resources/auth-tables.guard.js";
|
|
25
|
+
import { governedFor, nullSafe, ownOf, } from "./scope.resolver.js";
|
|
26
|
+
const ENGINE_OPTIONS = new Set(["guardrails", "lock", "requireLock"]);
|
|
27
|
+
/**
|
|
28
|
+
* The engine's own copy of a file handed to it: what is checked and hashed is what
|
|
29
|
+
* runs, whatever the caller does to its object afterwards.
|
|
30
|
+
*/
|
|
31
|
+
const copyOf = (what, file) => {
|
|
32
|
+
try {
|
|
33
|
+
return structuredClone(file);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return fail(what, "is JSON data: it holds a value that cannot be copied");
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/** Freezes a copied file all the way down, once it is checked. */
|
|
40
|
+
const deepFreeze = (value) => {
|
|
41
|
+
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
|
|
42
|
+
Object.freeze(value);
|
|
43
|
+
for (const member of Object.values(value)) {
|
|
44
|
+
deepFreeze(member);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
};
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Startup facts
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
const pathCollector = (policies) => {
|
|
53
|
+
const collect = (predicate, prefix, into, roles = new Set()) => {
|
|
54
|
+
switch (predicate.op) {
|
|
55
|
+
case "and":
|
|
56
|
+
case "or":
|
|
57
|
+
for (const arg of predicate.args) {
|
|
58
|
+
collect(arg, prefix, into, roles);
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
case "not":
|
|
62
|
+
collect(predicate.arg, prefix, into, roles);
|
|
63
|
+
return;
|
|
64
|
+
case "cmp":
|
|
65
|
+
into.push([...prefix, predicate.field, operatorOf(predicate.cmp)]);
|
|
66
|
+
return;
|
|
67
|
+
case "rel":
|
|
68
|
+
collect(predicate.where, [...prefix, predicate.relation], into, roles);
|
|
69
|
+
return;
|
|
70
|
+
case "some":
|
|
71
|
+
case "every":
|
|
72
|
+
case "none":
|
|
73
|
+
collect(predicate.where, [...prefix, predicate.relation, predicate.op], into, roles);
|
|
74
|
+
return;
|
|
75
|
+
case "role": {
|
|
76
|
+
const role = ownOf(policies.roles, predicate.role);
|
|
77
|
+
if (role !== undefined && "on" in role && !roles.has(predicate.role)) {
|
|
78
|
+
collect(role.where, prefix, into, new Set([...roles, predicate.role]));
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
default:
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
return collect;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* A rule's condition as the null-safe rewrite reads it: roles followed, a `me` value
|
|
90
|
+
* stood in for by a string (never `null`: a missing one folds the comparison away).
|
|
91
|
+
* Nothing is folded, so every comparison stays to be counted.
|
|
92
|
+
*/
|
|
93
|
+
const resolvedOf = (policies, predicate, roles = new Set()) => {
|
|
94
|
+
switch (predicate.op) {
|
|
95
|
+
case "true":
|
|
96
|
+
case "false":
|
|
97
|
+
return { k: predicate.op };
|
|
98
|
+
case "and":
|
|
99
|
+
case "or":
|
|
100
|
+
return {
|
|
101
|
+
k: predicate.op,
|
|
102
|
+
args: predicate.args.map((arg) => resolvedOf(policies, arg, roles)),
|
|
103
|
+
};
|
|
104
|
+
case "not":
|
|
105
|
+
return { k: "not", arg: resolvedOf(policies, predicate.arg, roles) };
|
|
106
|
+
case "cmp": {
|
|
107
|
+
const literal = (operand) => operand !== null && typeof operand === "object"
|
|
108
|
+
? ""
|
|
109
|
+
: operand;
|
|
110
|
+
return {
|
|
111
|
+
k: "cmp",
|
|
112
|
+
field: predicate.field,
|
|
113
|
+
cmp: predicate.cmp,
|
|
114
|
+
value: Array.isArray(predicate.value)
|
|
115
|
+
? predicate.value.map(literal)
|
|
116
|
+
: literal(predicate.value),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
case "rel":
|
|
120
|
+
case "some":
|
|
121
|
+
case "every":
|
|
122
|
+
case "none":
|
|
123
|
+
return {
|
|
124
|
+
k: predicate.op,
|
|
125
|
+
relation: predicate.relation,
|
|
126
|
+
target: predicate.target,
|
|
127
|
+
where: resolvedOf(policies, predicate.where, roles),
|
|
128
|
+
};
|
|
129
|
+
case "role": {
|
|
130
|
+
const role = ownOf(policies.roles, predicate.role);
|
|
131
|
+
return role !== undefined && "on" in role && !roles.has(predicate.role)
|
|
132
|
+
? resolvedOf(policies, role.where, new Set([...roles, predicate.role]))
|
|
133
|
+
: { k: "false" };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
/** The filter paths a condition the null-safe rewrite wrote uses. */
|
|
138
|
+
const resolvedPaths = (node, prefix, into) => {
|
|
139
|
+
switch (node.k) {
|
|
140
|
+
case "true":
|
|
141
|
+
case "false":
|
|
142
|
+
return;
|
|
143
|
+
case "cmp":
|
|
144
|
+
into.push([...prefix, node.field, operatorOf(node.cmp)]);
|
|
145
|
+
return;
|
|
146
|
+
case "and":
|
|
147
|
+
case "all":
|
|
148
|
+
case "or":
|
|
149
|
+
for (const arg of node.args) {
|
|
150
|
+
resolvedPaths(arg, prefix, into);
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
case "not":
|
|
154
|
+
resolvedPaths(node.arg, prefix, into);
|
|
155
|
+
return;
|
|
156
|
+
case "rel":
|
|
157
|
+
resolvedPaths(node.where, [...prefix, node.relation], into);
|
|
158
|
+
return;
|
|
159
|
+
case "some":
|
|
160
|
+
case "every":
|
|
161
|
+
case "none":
|
|
162
|
+
resolvedPaths(node.where, [...prefix, node.relation, node.k], into);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* The paths the null-safe rewrite adds to a condition on `resource`, read as it is
|
|
167
|
+
* (a grant, an `exists` question) and, for a deny, negated too (its scope): the
|
|
168
|
+
* `IS NULL` guards (`equals`), a reference's `IS NULL`, `not` for a `notIn` left
|
|
169
|
+
* empty, and the `none` and `some` an `every` becomes.
|
|
170
|
+
*/
|
|
171
|
+
const nullSafePaths = (policies, resource, predicate, negated, into) => {
|
|
172
|
+
const node = resolvedOf(policies, predicate);
|
|
173
|
+
resolvedPaths(nullSafe(policies, resource, node), [resource], into);
|
|
174
|
+
if (negated) {
|
|
175
|
+
resolvedPaths(nullSafe(policies, resource, { k: "not", arg: node }), [resource], into);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
/** The to-one steps at a created row's own level, roles followed: each is one `exists` on its target. */
|
|
179
|
+
const createSteps = (policies, predicate, roles = new Set()) => {
|
|
180
|
+
switch (predicate.op) {
|
|
181
|
+
case "and":
|
|
182
|
+
case "or":
|
|
183
|
+
return predicate.args.flatMap((arg) => createSteps(policies, arg, roles));
|
|
184
|
+
case "not":
|
|
185
|
+
return createSteps(policies, predicate.arg, roles);
|
|
186
|
+
case "rel":
|
|
187
|
+
return [predicate];
|
|
188
|
+
case "role": {
|
|
189
|
+
const role = ownOf(policies.roles, predicate.role);
|
|
190
|
+
return role !== undefined && "on" in role && !roles.has(predicate.role)
|
|
191
|
+
? createSteps(policies, role.where, new Set([...roles, predicate.role]))
|
|
192
|
+
: [];
|
|
193
|
+
}
|
|
194
|
+
default:
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const subjectPredicate = (policies, grant, subject) => {
|
|
199
|
+
const role = ownOf(policies.roles, subject);
|
|
200
|
+
if (role === undefined || !("on" in role)) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
const held = { op: "role", role: subject };
|
|
204
|
+
return grant.in === undefined
|
|
205
|
+
? held
|
|
206
|
+
: { op: "rel", relation: grant.in, target: role.on, where: held };
|
|
207
|
+
};
|
|
208
|
+
const pathsOf = (policies) => {
|
|
209
|
+
const collect = pathCollector(policies);
|
|
210
|
+
const paths = {};
|
|
211
|
+
const existsPaths = (resource, predicate, into) => {
|
|
212
|
+
for (const step of createSteps(policies, predicate)) {
|
|
213
|
+
const relation = ownOf(ownOf(policies.schema, resource)?.relations, step.relation);
|
|
214
|
+
for (const reference of relation?.references ?? []) {
|
|
215
|
+
into.push([step.target, reference, "equals"]);
|
|
216
|
+
}
|
|
217
|
+
collect(step.where, [step.target], into);
|
|
218
|
+
nullSafePaths(policies, step.target, step.where, false, into);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
const keep = (rule, found) => {
|
|
222
|
+
const unique = new Map(found.map((path) => [JSON.stringify(path), path]));
|
|
223
|
+
if (unique.size > 0) {
|
|
224
|
+
paths[rule] = [...unique.values()];
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
for (const grant of policies.grants) {
|
|
228
|
+
if (grant.resource === "*") {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const found = [];
|
|
232
|
+
const parts = [
|
|
233
|
+
grant.where,
|
|
234
|
+
...grant.subjects.map((subject) => subjectPredicate(policies, grant, subject)),
|
|
235
|
+
].filter((part) => part !== undefined);
|
|
236
|
+
for (const part of parts) {
|
|
237
|
+
collect(part, [grant.resource], found);
|
|
238
|
+
nullSafePaths(policies, grant.resource, part, false, found);
|
|
239
|
+
}
|
|
240
|
+
if (grant.after !== undefined) {
|
|
241
|
+
collect(grant.after, [grant.resource], found);
|
|
242
|
+
nullSafePaths(policies, grant.resource, grant.after, false, found);
|
|
243
|
+
const identifier = ownOf(policies.schema, grant.resource)?.identifiers[0] ?? [];
|
|
244
|
+
for (const field of identifier) {
|
|
245
|
+
found.push([grant.resource, field, "equals"]);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (grant.actions.includes("create")) {
|
|
249
|
+
for (const part of parts) {
|
|
250
|
+
existsPaths(grant.resource, part, found);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
keep(grant.id, found);
|
|
254
|
+
}
|
|
255
|
+
for (const deny of policies.denies) {
|
|
256
|
+
if (deny.where === undefined) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const found = [];
|
|
260
|
+
collect(deny.where, [deny.resource], found);
|
|
261
|
+
// A scope (read, update, delete) negates the deny; a create only asks it as it is.
|
|
262
|
+
const scoped = deny.actions === "*" ||
|
|
263
|
+
deny.actions.some((action) => action !== "create");
|
|
264
|
+
nullSafePaths(policies, deny.resource, deny.where, scoped, found);
|
|
265
|
+
if (deny.actions === "*" || deny.actions.includes("create")) {
|
|
266
|
+
existsPaths(deny.resource, deny.where, found);
|
|
267
|
+
}
|
|
268
|
+
keep(deny.id, found);
|
|
269
|
+
}
|
|
270
|
+
return paths;
|
|
271
|
+
};
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// Decisions
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
/** The rule a trace shows deciding: the first grant matched, or the first deny that counted (fail closed). */
|
|
276
|
+
const decidingRule = (trace, allowed) => {
|
|
277
|
+
if (allowed) {
|
|
278
|
+
for (const [grant, outcome] of trace.grants) {
|
|
279
|
+
if (outcome === "matched") {
|
|
280
|
+
return { kind: "grant", id: grant.grant.id };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
for (const [deny, outcome] of trace.denies) {
|
|
286
|
+
if (outcome === "matched" || outcome === "unknown") {
|
|
287
|
+
return { kind: "deny", id: deny.deny.id };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
};
|
|
292
|
+
/** Refuses a `fieldsOf` question the types rule out but a caller can still send (the decider refuses it too). */
|
|
293
|
+
const checkFieldsQuestion = (question) => {
|
|
294
|
+
const problem = fieldsQuestionProblem(question);
|
|
295
|
+
if (problem !== undefined) {
|
|
296
|
+
throw new AccessError("ACCESS_QUESTION_INVALID", problem);
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
/**
|
|
300
|
+
* Builds the engine from a policies file and, optionally, the org's guardrails. Both
|
|
301
|
+
* are checked first; a bad one throws, naming the rule. With `lock`, both are held
|
|
302
|
+
* to the hashes `avAccess.lock.json` pins (`ACCESS_LOCK_MISMATCH`).
|
|
303
|
+
*/
|
|
304
|
+
export const createAccessEngine = (policies, options) => engineOf(policies, options, "file");
|
|
305
|
+
/**
|
|
306
|
+
* The engine, from a policies file that came from `source`: with a `lock`, a file
|
|
307
|
+
* handed over is held to the lock's pin, and typed rules (the project's own
|
|
308
|
+
* source) to its namespace only. `createAccess` adds its built-in rules over Better
|
|
309
|
+
* Auth's tables (`guard`): their denies join the file's, and `fieldsOf` never lists a
|
|
310
|
+
* secret column or a field a write may not set.
|
|
311
|
+
*/
|
|
312
|
+
export const engineOf = (policies, options, source, observe, guard) => {
|
|
313
|
+
// The engine's own copy, checked, held to the lock, then frozen: nothing the
|
|
314
|
+
// caller does to its objects afterwards reaches what the engine runs from.
|
|
315
|
+
const own = copyOf("policies", policies);
|
|
316
|
+
checkPolicies(own);
|
|
317
|
+
warnReadGovernedUnique(own);
|
|
318
|
+
if (options !== undefined &&
|
|
319
|
+
(!isRecord(options) ||
|
|
320
|
+
Object.keys(options).some((key) => !ENGINE_OPTIONS.has(key)) ||
|
|
321
|
+
(options.requireLock !== undefined &&
|
|
322
|
+
typeof options.requireLock !== "boolean"))) {
|
|
323
|
+
fail("engine", "the engine's options are `{ guardrails?, lock?, requireLock?: boolean }`");
|
|
324
|
+
}
|
|
325
|
+
// A guardrails file imported as JSON is typed by its outline only; it is checked
|
|
326
|
+
// here (`composeGuardrails`), as a typed one is.
|
|
327
|
+
const guardrails = options?.guardrails === undefined
|
|
328
|
+
? undefined
|
|
329
|
+
: copyOf("guardrails", options.guardrails);
|
|
330
|
+
if (options?.lock === undefined) {
|
|
331
|
+
if (options?.requireLock === true) {
|
|
332
|
+
throw new AccessError("ACCESS_LOCK_MISMATCH", "`requireLock` is set and no `lock` was given: pass the project's avAccess.lock.json (`avaccess pull` writes it), so the engine runs only from the files it pins");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
verifyLocked(checkLock(options.lock, "engine"), {
|
|
337
|
+
namespace: own.namespace,
|
|
338
|
+
// Typed rules are the project's own source; with `requireLock` they are held to
|
|
339
|
+
// the lock's pin too, so a lock that pins other rules is refused.
|
|
340
|
+
...(source === "file" || options.requireLock === true
|
|
341
|
+
? { policies: own }
|
|
342
|
+
: {}),
|
|
343
|
+
guardrails,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
// The namespaces the rules read as foreign, from the file's own markers: with a
|
|
347
|
+
// lock or without, they are read only once the beacons have passed.
|
|
348
|
+
const owners = foreignOwnersOf(own);
|
|
349
|
+
const guarded = guardrails === undefined ? own : composeGuardrails(own, guardrails);
|
|
350
|
+
const file = deepFreeze(guard === undefined ? guarded : withBuiltInRules(guarded, guard));
|
|
351
|
+
if (file !== own) {
|
|
352
|
+
checkPolicies(file, true);
|
|
353
|
+
}
|
|
354
|
+
deepFreeze(own);
|
|
355
|
+
const decider = createDecider(file);
|
|
356
|
+
// The beacons' state: `undefined` once they have passed (or when the rules read
|
|
357
|
+
// no other namespace), otherwise why the engine answers nothing yet.
|
|
358
|
+
let refusal = owners.length === 0
|
|
359
|
+
? undefined
|
|
360
|
+
: `the rules read ${owners.map((owner) => `namespace "${owner}"'s`).join(" and ")} Resources as foreign, and the beacons were not checked: \`await access.policy.checkBeacons(beacon, beaconPins, environment)\` (\`engine.checkBeacons\` on an engine of your own) when the service starts, before it takes requests, with this environment's avAccess.beacons.<environment>.json`;
|
|
361
|
+
// A failed check latches: no later check opens the engine again.
|
|
362
|
+
let failed;
|
|
363
|
+
// The checks, one after another.
|
|
364
|
+
let checking = Promise.resolve();
|
|
365
|
+
const open = () => {
|
|
366
|
+
if (refusal !== undefined) {
|
|
367
|
+
throw new AccessError("ACCESS_BEACON_MISMATCH", refusal);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
const fieldsOf = (question) => {
|
|
371
|
+
open();
|
|
372
|
+
checkFieldsQuestion(question);
|
|
373
|
+
return guard === undefined
|
|
374
|
+
? decider.fieldsOf(question)
|
|
375
|
+
: guardedFields(guard, question, decider.fieldsOf(question), {
|
|
376
|
+
fields: ownOf(file.schema, question.resource)?.fields ?? [],
|
|
377
|
+
// What `"all"` stands for: every field reads do not govern.
|
|
378
|
+
governed: governedFor(file, question.resource, "read"),
|
|
379
|
+
});
|
|
380
|
+
};
|
|
381
|
+
// The seam's `governed`: the fields reads leave out. The built-in rules govern
|
|
382
|
+
// for writing only, so the file's own reads are the same.
|
|
383
|
+
const governed = deepFreeze(Object.fromEntries(Object.keys(file.govern).flatMap((resource) => {
|
|
384
|
+
const fields = governedFor(file, resource, "read");
|
|
385
|
+
return fields.length === 0 ? [] : [[resource, [...fields]]];
|
|
386
|
+
})));
|
|
387
|
+
return {
|
|
388
|
+
governed: governed,
|
|
389
|
+
paths: deepFreeze(pathsOf(file)),
|
|
390
|
+
afterChecks: deepFreeze(afterChecksOf(file)),
|
|
391
|
+
unheld: Object.freeze(Object.entries(own.schema)
|
|
392
|
+
.filter(([, facts]) => facts.foreign !== undefined)
|
|
393
|
+
.map(([resource]) => resource)),
|
|
394
|
+
can: observe === undefined
|
|
395
|
+
? (question) => {
|
|
396
|
+
open();
|
|
397
|
+
return decider.can(question, newTrace());
|
|
398
|
+
}
|
|
399
|
+
: (question) => {
|
|
400
|
+
open();
|
|
401
|
+
const trace = newTrace();
|
|
402
|
+
const report = async (allowed) => {
|
|
403
|
+
await observe(question, allowed, decidingRule(trace, allowed));
|
|
404
|
+
return allowed;
|
|
405
|
+
};
|
|
406
|
+
const answer = decider.can(question, trace);
|
|
407
|
+
return typeof answer === "boolean"
|
|
408
|
+
? report(answer)
|
|
409
|
+
: answer.then(report);
|
|
410
|
+
},
|
|
411
|
+
grantsOf: (question) => {
|
|
412
|
+
open();
|
|
413
|
+
const problem = grantsQuestionProblem(question);
|
|
414
|
+
if (problem !== undefined) {
|
|
415
|
+
throw new AccessError("ACCESS_QUESTION_INVALID", problem);
|
|
416
|
+
}
|
|
417
|
+
return decider.grantsOf(question);
|
|
418
|
+
},
|
|
419
|
+
scopeOf: (question) => {
|
|
420
|
+
open();
|
|
421
|
+
return decider.scopeOf(question);
|
|
422
|
+
},
|
|
423
|
+
fieldsOf,
|
|
424
|
+
hosted: (host) => {
|
|
425
|
+
const held = isRecord(host) ? host.held : undefined;
|
|
426
|
+
if (!Array.isArray(held) ||
|
|
427
|
+
!held.every((resource) => typeof resource === "string")) {
|
|
428
|
+
throw new AccessError("ACCESS_QUESTION_INVALID", "`hosted` takes `{ held }`, the list of Resource names the framework holds a written reference to");
|
|
429
|
+
}
|
|
430
|
+
const served = new Set(held);
|
|
431
|
+
warnReferenceReads(own, served, guard?.closed);
|
|
432
|
+
warnForeignReferences(own, served);
|
|
433
|
+
},
|
|
434
|
+
explain: async (question) => {
|
|
435
|
+
open();
|
|
436
|
+
if (globalThis.process
|
|
437
|
+
?.env?.NODE_ENV === "production") {
|
|
438
|
+
throw new Error("Aventara Access: explain() is for development only");
|
|
439
|
+
}
|
|
440
|
+
const trace = newTrace();
|
|
441
|
+
const allowed = await decider.can(question, trace);
|
|
442
|
+
return {
|
|
443
|
+
allowed,
|
|
444
|
+
grants: [...trace.grants].map(([grant, outcome]) => ({
|
|
445
|
+
id: grant.grant.id,
|
|
446
|
+
subject: grant.subject,
|
|
447
|
+
outcome,
|
|
448
|
+
})),
|
|
449
|
+
denies: [...trace.denies].map(([deny, outcome]) => ({
|
|
450
|
+
id: deny.deny.id,
|
|
451
|
+
outcome,
|
|
452
|
+
})),
|
|
453
|
+
lookups: trace.lookups,
|
|
454
|
+
};
|
|
455
|
+
},
|
|
456
|
+
checkBeacons: (reader, pins, environment) => {
|
|
457
|
+
const run = async () => {
|
|
458
|
+
if (owners.length === 0) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (failed !== undefined) {
|
|
462
|
+
throw new AccessError("ACCESS_BEACON_MISMATCH", `a beacon check failed before, and the engine stays shut: ${failed}`);
|
|
463
|
+
}
|
|
464
|
+
const problem = await beaconProblem(own.namespace ?? "", owners, pins, environment, reader);
|
|
465
|
+
if (problem !== undefined) {
|
|
466
|
+
failed = problem;
|
|
467
|
+
refusal = problem;
|
|
468
|
+
throw new AccessError("ACCESS_BEACON_MISMATCH", problem);
|
|
469
|
+
}
|
|
470
|
+
refusal = undefined;
|
|
471
|
+
};
|
|
472
|
+
const result = checking.then(run);
|
|
473
|
+
checking = result.catch(() => undefined);
|
|
474
|
+
return result;
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
};
|