@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,103 @@
|
|
|
1
|
+
import { type AccessWhere } from "./aventara-where.translator.js";
|
|
2
|
+
import type { AccessAction, AccessOrigin, AccessPolicies } from "./policies-contract.js";
|
|
3
|
+
import { type AccessExists, type AccessLookup, type AccessRow } from "./predicate.evaluator.js";
|
|
4
|
+
import { type FoldedDeny, type FoldedGrant } from "./scope.resolver.js";
|
|
5
|
+
/** What every question states. */
|
|
6
|
+
export type AccessQuestion = {
|
|
7
|
+
/** What the host derived for the request: an object, or `null` (anonymous). */
|
|
8
|
+
readonly principal: unknown;
|
|
9
|
+
readonly origin: AccessOrigin;
|
|
10
|
+
readonly resource: string;
|
|
11
|
+
};
|
|
12
|
+
/** `can`: without a row, admission; with one, the row a create inserts or an update leaves. */
|
|
13
|
+
export type AccessCanQuestion = AccessQuestion & {
|
|
14
|
+
readonly action: AccessAction;
|
|
15
|
+
readonly row?: AccessRow;
|
|
16
|
+
/**
|
|
17
|
+
* On an update with a `row` only: the row as it was before the update. One update
|
|
18
|
+
* grant must then cover both rows, its `where` this one and its `where` and
|
|
19
|
+
* `after` the row the update leaves. Without it, any update grant that covers the
|
|
20
|
+
* row the update leaves lets it in. Read nowhere else.
|
|
21
|
+
*/
|
|
22
|
+
readonly before?: AccessRow;
|
|
23
|
+
/**
|
|
24
|
+
* On an update with a `row` only: what `grantsOf` answered about the row before the
|
|
25
|
+
* update, asked before the write. The grant that covers `row` (its `where` and
|
|
26
|
+
* `after`) must then be one of these. With `before` too, each such grant's `where`
|
|
27
|
+
* is checked on `before` in memory, so a list from another row or principal fails
|
|
28
|
+
* on a scalar condition; a relation condition on it is not asked again: it was
|
|
29
|
+
* decided before anything the update wrote. Read nowhere else.
|
|
30
|
+
*/
|
|
31
|
+
readonly beforeGrants?: readonly string[];
|
|
32
|
+
readonly exists?: AccessExists;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* `grantsOf`: the update grants that cover `row`, the row an update will change, as it
|
|
36
|
+
* is now. Ask it before the write, and hand its answer back to `can` as `beforeGrants`.
|
|
37
|
+
*/
|
|
38
|
+
export type AccessGrantsQuestion = AccessQuestion & {
|
|
39
|
+
readonly action: "update";
|
|
40
|
+
readonly row: AccessRow;
|
|
41
|
+
readonly exists?: AccessExists;
|
|
42
|
+
};
|
|
43
|
+
/** `scopeOf`: the rows the principal may read, update or delete. */
|
|
44
|
+
export type AccessScopeQuestion = AccessQuestion & {
|
|
45
|
+
readonly action: "read" | "update" | "delete";
|
|
46
|
+
};
|
|
47
|
+
/** `fieldsOf`: reading, or writing by one action. */
|
|
48
|
+
export type AccessFieldsQuestion = AccessQuestion & ({
|
|
49
|
+
readonly access: "read";
|
|
50
|
+
} | {
|
|
51
|
+
readonly access: "write";
|
|
52
|
+
readonly action?: "create" | "update";
|
|
53
|
+
});
|
|
54
|
+
/** `fieldsOf`'s answer: fields usable on every reachable row, and fields usable on some rows. */
|
|
55
|
+
export type AccessFields = {
|
|
56
|
+
readonly always: "all" | readonly string[];
|
|
57
|
+
readonly perRow: {
|
|
58
|
+
readonly [field: string]: AccessWhere;
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
/** How one rule took part in a decision. */
|
|
62
|
+
export type AccessRuleOutcome = "matched" | "not matched" | "unknown" | "not asked";
|
|
63
|
+
/** How each rule took part in one `can` decision, and the `exists` lookups it made. */
|
|
64
|
+
export type DecisionTrace = {
|
|
65
|
+
readonly grants: Map<FoldedGrant, AccessRuleOutcome>;
|
|
66
|
+
readonly denies: Map<FoldedDeny, AccessRuleOutcome>;
|
|
67
|
+
readonly lookups: AccessLookup[];
|
|
68
|
+
};
|
|
69
|
+
/** A trace with nothing in it yet. */
|
|
70
|
+
export declare const newTrace: () => DecisionTrace;
|
|
71
|
+
/**
|
|
72
|
+
* The Resources where the row an update leaves can fail what allowed the update:
|
|
73
|
+
* an update grant whose condition reads the row (a `where`, an `after`, a role held
|
|
74
|
+
* on a row), or an update deny with a `where`. Core asks `can` about each row such
|
|
75
|
+
* an update changed, and refuses these updates where it has no interactive
|
|
76
|
+
* transaction to roll one back in.
|
|
77
|
+
*/
|
|
78
|
+
export declare const afterChecksOf: (policies: AccessPolicies) => Record<string, "update"[]>;
|
|
79
|
+
/**
|
|
80
|
+
* What is wrong with a `fieldsOf` question the types rule out but a caller can still
|
|
81
|
+
* send, or `undefined`: answered as asked, it would read as a read, or as no field.
|
|
82
|
+
*/
|
|
83
|
+
export declare const fieldsQuestionProblem: (question: AccessFieldsQuestion) => string | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* What is wrong with a `grantsOf` question the types rule out but a caller can still
|
|
86
|
+
* send, or `undefined`: it asks about an update, and nothing else.
|
|
87
|
+
*/
|
|
88
|
+
export declare const grantsQuestionProblem: (question: AccessGrantsQuestion) => string | undefined;
|
|
89
|
+
/** The questions a policies file answers. */
|
|
90
|
+
export type PoliciesDecider = {
|
|
91
|
+
/** `can`, recording each rule's part in the answer in `trace`. */
|
|
92
|
+
can(question: AccessCanQuestion, trace: DecisionTrace): boolean | Promise<boolean>;
|
|
93
|
+
/**
|
|
94
|
+
* The keys of the update grants whose `where`, and their subject's row condition,
|
|
95
|
+
* hold on `row` as it is now: opaque and stable for this file, never rule data.
|
|
96
|
+
* None when an update deny is decided on `row` in memory.
|
|
97
|
+
*/
|
|
98
|
+
grantsOf(question: AccessGrantsQuestion): readonly string[] | Promise<readonly string[]>;
|
|
99
|
+
scopeOf(question: AccessScopeQuestion): boolean | AccessWhere;
|
|
100
|
+
fieldsOf(question: AccessFieldsQuestion): AccessFields;
|
|
101
|
+
};
|
|
102
|
+
/** The decisions of one policies file, already checked. */
|
|
103
|
+
export declare const createDecider: (file: AccessPolicies) => PoliciesDecider;
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// The decisions a policies file makes: `can`, `grantsOf`, `scopeOf` and `fieldsOf`,
|
|
2
|
+
// answered from the file alone. Default deny, any grant allows, any deny wins, order
|
|
3
|
+
// never matters. The engine (`access.engine.ts`) runs this with its lock, guardrails,
|
|
4
|
+
// decision observer and `explain` around it; `avaccess generate` copies this module,
|
|
5
|
+
// with the modules it imports, into a service's generated client.
|
|
6
|
+
//
|
|
7
|
+
// Like those modules, it imports nothing but them: no Node.js, no package.
|
|
8
|
+
import { translateAnswer, translateWhere, } from "./aventara-where.translator.js";
|
|
9
|
+
import { canonicalJson } from "./canonical-json.writer.js";
|
|
10
|
+
import { decideOnRow, evaluateOnRow, } from "./predicate.evaluator.js";
|
|
11
|
+
import { FALSE, foldDenies, foldGrants, governedFor, nullSafe, orOf, ownOf, principalFactsOf, rulesFor, scopeCondition, TRUE, } from "./scope.resolver.js";
|
|
12
|
+
import { sha256Hex } from "./sha256.digest.js";
|
|
13
|
+
/** A trace with nothing in it yet. */
|
|
14
|
+
export const newTrace = () => ({
|
|
15
|
+
grants: new Map(),
|
|
16
|
+
denies: new Map(),
|
|
17
|
+
lookups: [],
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* The Resources where the row an update leaves can fail what allowed the update:
|
|
21
|
+
* an update grant whose condition reads the row (a `where`, an `after`, a role held
|
|
22
|
+
* on a row), or an update deny with a `where`. Core asks `can` about each row such
|
|
23
|
+
* an update changed, and refuses these updates where it has no interactive
|
|
24
|
+
* transaction to roll one back in.
|
|
25
|
+
*/
|
|
26
|
+
export const afterChecksOf = (policies) => {
|
|
27
|
+
const afterChecks = {};
|
|
28
|
+
const reads = (grant) => grant.where !== undefined ||
|
|
29
|
+
grant.after !== undefined ||
|
|
30
|
+
grant.in !== undefined ||
|
|
31
|
+
grant.subjects.some((subject) => {
|
|
32
|
+
const role = ownOf(policies.roles, subject);
|
|
33
|
+
return role !== undefined && "on" in role;
|
|
34
|
+
});
|
|
35
|
+
for (const grant of policies.grants) {
|
|
36
|
+
if (grant.resource !== "*" &&
|
|
37
|
+
grant.actions.includes("update") &&
|
|
38
|
+
reads(grant)) {
|
|
39
|
+
afterChecks[grant.resource] = ["update"];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const deny of policies.denies) {
|
|
43
|
+
if (deny.resource !== "*" &&
|
|
44
|
+
deny.where !== undefined &&
|
|
45
|
+
(deny.actions === "*" || deny.actions.includes("update"))) {
|
|
46
|
+
afterChecks[deny.resource] = ["update"];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return afterChecks;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The fields each grant's `write.except` locks. A relation and its reference fields
|
|
53
|
+
* are locked together: writing the relation (`$connect`, `$set`) writes those
|
|
54
|
+
* fields, and writing a reference field moves the relation, so naming either one
|
|
55
|
+
* locks both.
|
|
56
|
+
*/
|
|
57
|
+
const lockedFieldsOf = (policies) => {
|
|
58
|
+
const locks = new Map();
|
|
59
|
+
for (const grant of policies.grants) {
|
|
60
|
+
if (grant.write === undefined) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const named = new Set(grant.write.except);
|
|
64
|
+
const locked = new Set(named);
|
|
65
|
+
const relations = ownOf(policies.schema, grant.resource)?.relations ?? {};
|
|
66
|
+
for (const [relation, fact] of Object.entries(relations)) {
|
|
67
|
+
const references = fact.referenceFields ?? [];
|
|
68
|
+
if (named.has(relation) || references.some((field) => named.has(field))) {
|
|
69
|
+
locked.add(relation);
|
|
70
|
+
for (const field of references) {
|
|
71
|
+
locked.add(field);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
locks.set(grant, locked);
|
|
76
|
+
}
|
|
77
|
+
return locks;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* What is wrong with a `fieldsOf` question the types rule out but a caller can still
|
|
81
|
+
* send, or `undefined`: answered as asked, it would read as a read, or as no field.
|
|
82
|
+
*/
|
|
83
|
+
export const fieldsQuestionProblem = (question) => {
|
|
84
|
+
const { access, action } = question;
|
|
85
|
+
const fits = access === "read"
|
|
86
|
+
? action === undefined
|
|
87
|
+
: access === "write" &&
|
|
88
|
+
(action === undefined || action === "create" || action === "update");
|
|
89
|
+
return fits
|
|
90
|
+
? undefined
|
|
91
|
+
: `fieldsOf takes \`access: "read"\`, or \`access: "write"\` with an optional \`action\` ("create" or "update"); it was asked ${JSON.stringify({ access, action })}`;
|
|
92
|
+
};
|
|
93
|
+
const outcomeOf = (truth) => truth === "unknown" ? "unknown" : truth ? "matched" : "not matched";
|
|
94
|
+
const isDecided = (node) => node.k === "true" || node.k === "false";
|
|
95
|
+
/**
|
|
96
|
+
* What is wrong with a `grantsOf` question the types rule out but a caller can still
|
|
97
|
+
* send, or `undefined`: it asks about an update, and nothing else.
|
|
98
|
+
*/
|
|
99
|
+
export const grantsQuestionProblem = (question) => {
|
|
100
|
+
const { action } = question;
|
|
101
|
+
return action === "update"
|
|
102
|
+
? undefined
|
|
103
|
+
: `grantsOf takes \`action: "update"\`; it was asked ${JSON.stringify({ action })}`;
|
|
104
|
+
};
|
|
105
|
+
/** Whether two field lists name the same fields. */
|
|
106
|
+
const sameFields = (a, b) => a.length === b.length && a.every((field) => b.includes(field));
|
|
107
|
+
/** The decisions of one policies file, already checked. */
|
|
108
|
+
export const createDecider = (file) => {
|
|
109
|
+
const locks = lockedFieldsOf(file);
|
|
110
|
+
// A grant's key: a short hash of the file's grants, in their order, then its place
|
|
111
|
+
// in the file and its subject's place in the grant. It names a grant for one
|
|
112
|
+
// subject without carrying any of its data, a list from another file names no grant
|
|
113
|
+
// of this one, and it is the same for the folded grant `grantsOf` reads (its
|
|
114
|
+
// `where`) and the one `can` reads (its `where` and `after`).
|
|
115
|
+
const mark = sha256Hex(canonicalJson(file.grants)).slice(0, 8);
|
|
116
|
+
const places = new Map(file.grants.map((grant, index) => [grant, index]));
|
|
117
|
+
const keyOf = (grant) => `${mark}:${places.get(grant.grant) ?? -1}.${grant.grant.subjects.indexOf(grant.subject)}`;
|
|
118
|
+
const can = (question, trace) => {
|
|
119
|
+
const facts = principalFactsOf(question.principal, question.origin);
|
|
120
|
+
const { grants, denies } = rulesFor(file, question.resource, question.action);
|
|
121
|
+
const { row } = question;
|
|
122
|
+
if (row === undefined) {
|
|
123
|
+
// Admission: could this principal ever do this? Row conditions are the scope's.
|
|
124
|
+
const folded = foldGrants(file, grants, facts);
|
|
125
|
+
const foldedDenies = foldDenies(file, denies, facts);
|
|
126
|
+
for (const grant of folded) {
|
|
127
|
+
trace.grants.set(grant, grant.condition.k === "false" ? "not matched" : "matched");
|
|
128
|
+
}
|
|
129
|
+
for (const deny of foldedDenies) {
|
|
130
|
+
trace.denies.set(deny, deny.condition.k === "true" ? "matched" : "not matched");
|
|
131
|
+
}
|
|
132
|
+
return (folded.some((grant) => grant.condition.k !== "false") &&
|
|
133
|
+
!foldedDenies.some((deny) => deny.condition.k === "true"));
|
|
134
|
+
}
|
|
135
|
+
if (typeof row !== "object" || row === null) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
// The row a create inserts, or the row an update leaves (core's after check, or
|
|
139
|
+
// any caller's): an update's row must still meet the `where` and the `after` of
|
|
140
|
+
// one update grant, and no update deny may match it.
|
|
141
|
+
const isUpdate = question.action === "update";
|
|
142
|
+
const kind = question.action === "create" ? "new" : "existing";
|
|
143
|
+
// With the row before the update: the grant that covers the row
|
|
144
|
+
// the update leaves must be one whose `where` covered the row before it, so no
|
|
145
|
+
// update moves a row from the rows one grant reaches to another's. With
|
|
146
|
+
// `beforeGrants`, what covered it was decided before the write (`grantsOf`):
|
|
147
|
+
// `before` is still held to each listed grant's `where` in memory, since the list
|
|
148
|
+
// is the caller's to hand back, but a relation condition on it is not asked again.
|
|
149
|
+
const beforeGrants = isUpdate ? question.beforeGrants : undefined;
|
|
150
|
+
if (beforeGrants !== undefined &&
|
|
151
|
+
(!Array.isArray(beforeGrants) ||
|
|
152
|
+
beforeGrants.some((key) => typeof key !== "string"))) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const coveredBefore = beforeGrants === undefined ? undefined : new Set(beforeGrants);
|
|
156
|
+
const before = isUpdate ? question.before : undefined;
|
|
157
|
+
if (before !== undefined &&
|
|
158
|
+
(typeof before !== "object" || before === null)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const folded = foldGrants(file, grants, facts, isUpdate);
|
|
162
|
+
const reached = before === undefined ? undefined : foldGrants(file, grants, facts);
|
|
163
|
+
const foldedDenies = foldDenies(file, denies, facts);
|
|
164
|
+
const grantResiduals = [];
|
|
165
|
+
let granted = false;
|
|
166
|
+
for (const [index, grant] of folded.entries()) {
|
|
167
|
+
if (coveredBefore !== undefined && !coveredBefore.has(keyOf(grant))) {
|
|
168
|
+
// It did not cover the row before the update.
|
|
169
|
+
trace.grants.set(grant, "not matched");
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const residual = evaluateOnRow(file, question.resource, grant.condition, row);
|
|
173
|
+
// Folded alike, the two lists pair grant for grant; a missing pair covers nothing.
|
|
174
|
+
const evaluated = reached === undefined || before === undefined
|
|
175
|
+
? undefined
|
|
176
|
+
: evaluateOnRow(file, question.resource, reached?.[index]?.condition ?? FALSE, before);
|
|
177
|
+
// Listed in `beforeGrants`: what is left of its `where` on `before` is a relation
|
|
178
|
+
// condition `grantsOf` decided before the write; only a decided false, or a
|
|
179
|
+
// value of the wrong type, refuses.
|
|
180
|
+
const prior = coveredBefore !== undefined &&
|
|
181
|
+
evaluated?.k !== "false" &&
|
|
182
|
+
evaluated?.k !== "unknown"
|
|
183
|
+
? undefined
|
|
184
|
+
: evaluated;
|
|
185
|
+
if (residual.k === "unknown" || prior?.k === "unknown") {
|
|
186
|
+
// A value of the wrong type: no grant.
|
|
187
|
+
trace.grants.set(grant, "unknown");
|
|
188
|
+
}
|
|
189
|
+
else if (residual.k === "false" || prior?.k === "false") {
|
|
190
|
+
trace.grants.set(grant, "not matched");
|
|
191
|
+
}
|
|
192
|
+
else if (residual.k === "true" &&
|
|
193
|
+
(prior === undefined || prior.k === "true")) {
|
|
194
|
+
trace.grants.set(grant, "matched");
|
|
195
|
+
granted = true;
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
trace.grants.set(grant, "not asked");
|
|
199
|
+
grantResiduals.push({
|
|
200
|
+
grant,
|
|
201
|
+
residual,
|
|
202
|
+
prior: prior === undefined || prior.k === "true" ? undefined : prior,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
const denyResiduals = [];
|
|
207
|
+
for (const deny of foldedDenies) {
|
|
208
|
+
const residual = evaluateOnRow(file, question.resource, deny.condition, row);
|
|
209
|
+
if (residual.k === "unknown") {
|
|
210
|
+
// A value of the wrong type: the deny counts (fail closed).
|
|
211
|
+
trace.denies.set(deny, "unknown");
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
if (isDecided(residual)) {
|
|
215
|
+
trace.denies.set(deny, residual.k === "true" ? "matched" : "not matched");
|
|
216
|
+
if (residual.k === "true") {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
trace.denies.set(deny, "not asked");
|
|
222
|
+
denyResiduals.push({ deny, residual });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (!granted && grantResiduals.length === 0) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
if (denyResiduals.length === 0 && granted) {
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
const ask = (condition, on = row) => decideOnRow({
|
|
232
|
+
policies: file,
|
|
233
|
+
resource: question.resource,
|
|
234
|
+
condition,
|
|
235
|
+
row: on,
|
|
236
|
+
kind,
|
|
237
|
+
exists: question.exists,
|
|
238
|
+
lookups: trace.lookups,
|
|
239
|
+
});
|
|
240
|
+
return (async () => {
|
|
241
|
+
for (const { deny, residual } of denyResiduals) {
|
|
242
|
+
const truth = await ask(residual);
|
|
243
|
+
trace.denies.set(deny, outcomeOf(truth));
|
|
244
|
+
// A deny that cannot be decided counts: fail closed.
|
|
245
|
+
if (truth !== false) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (granted) {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
for (const { grant, residual, prior } of grantResiduals) {
|
|
253
|
+
let truth = await ask(residual);
|
|
254
|
+
if (truth === true && prior !== undefined && before !== undefined) {
|
|
255
|
+
truth = await ask(prior, before);
|
|
256
|
+
}
|
|
257
|
+
trace.grants.set(grant, outcomeOf(truth));
|
|
258
|
+
if (truth === true) {
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
})();
|
|
264
|
+
};
|
|
265
|
+
const fieldsOf = (question) => {
|
|
266
|
+
const problem = fieldsQuestionProblem(question);
|
|
267
|
+
if (problem !== undefined) {
|
|
268
|
+
throw new Error(`Aventara Access: ${problem}`);
|
|
269
|
+
}
|
|
270
|
+
const facts = principalFactsOf(question.principal, question.origin);
|
|
271
|
+
// The fields governed for this access: only a grant that names one opens it.
|
|
272
|
+
const governed = governedFor(file, question.resource, question.access);
|
|
273
|
+
// `"all"` stands for every field the read side does not govern (the framework's
|
|
274
|
+
// default projection, whatever the access): a write answers it only when the
|
|
275
|
+
// write side governs the same fields.
|
|
276
|
+
const allStandsFor = question.access === "read" ||
|
|
277
|
+
sameFields(governed, governedFor(file, question.resource, "read"));
|
|
278
|
+
const actions = question.access === "read"
|
|
279
|
+
? ["read"]
|
|
280
|
+
: question.action !== undefined
|
|
281
|
+
? [question.action]
|
|
282
|
+
: ["create", "update"];
|
|
283
|
+
const grants = [
|
|
284
|
+
...new Set(actions.flatMap((action) => rulesFor(file, question.resource, action).grants)),
|
|
285
|
+
];
|
|
286
|
+
const live = foldGrants(file, grants, facts).filter((grant) => grant.condition.k !== "false");
|
|
287
|
+
if (live.length === 0) {
|
|
288
|
+
return { always: [], perRow: {} };
|
|
289
|
+
}
|
|
290
|
+
const excludes = (grant, field) => question.access === "write" &&
|
|
291
|
+
(locks.get(grant.grant)?.has(field) ?? false);
|
|
292
|
+
const names = (grant, field) => grant.grant.fields?.includes(`${question.resource}.${field}`) ?? false;
|
|
293
|
+
const answer = (condition) => condition.k === "true"
|
|
294
|
+
? {}
|
|
295
|
+
: translateWhere(nullSafe(file, question.resource, condition));
|
|
296
|
+
const perRow = {};
|
|
297
|
+
for (const field of governed) {
|
|
298
|
+
const condition = orOf(live
|
|
299
|
+
.filter((grant) => names(grant, field) && !excludes(grant, field))
|
|
300
|
+
.map((grant) => grant.condition));
|
|
301
|
+
if (condition.k !== "false") {
|
|
302
|
+
perRow[field] = answer(condition);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const excluded = new Set(live.flatMap((grant) => question.access === "write" ? [...(locks.get(grant.grant) ?? [])] : []));
|
|
306
|
+
if (excluded.size === 0 && allStandsFor) {
|
|
307
|
+
return { always: "all", perRow };
|
|
308
|
+
}
|
|
309
|
+
const always = [];
|
|
310
|
+
for (const field of ownOf(file.schema, question.resource)?.fields ?? []) {
|
|
311
|
+
if (governed.includes(field)) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (!excluded.has(field)) {
|
|
315
|
+
always.push(field);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const condition = orOf(live
|
|
319
|
+
.filter((grant) => !excludes(grant, field))
|
|
320
|
+
.map((grant) => grant.condition));
|
|
321
|
+
if (condition.k === "true") {
|
|
322
|
+
always.push(field);
|
|
323
|
+
}
|
|
324
|
+
else if (condition.k !== "false") {
|
|
325
|
+
perRow[field] = answer(condition);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { always, perRow };
|
|
329
|
+
};
|
|
330
|
+
const scopeOf = (question) => {
|
|
331
|
+
const facts = principalFactsOf(question.principal, question.origin);
|
|
332
|
+
const { grants, denies } = rulesFor(file, question.resource, question.action);
|
|
333
|
+
return translateAnswer(nullSafe(file, question.resource, scopeCondition(foldGrants(file, grants, facts), foldDenies(file, denies, facts))));
|
|
334
|
+
};
|
|
335
|
+
const grantsOf = (question) => {
|
|
336
|
+
const problem = grantsQuestionProblem(question);
|
|
337
|
+
if (problem !== undefined) {
|
|
338
|
+
throw new Error(`Aventara Access: ${problem}`);
|
|
339
|
+
}
|
|
340
|
+
const { row } = question;
|
|
341
|
+
if (typeof row !== "object" || row === null) {
|
|
342
|
+
// No row, no grant: a `can` handed this answer refuses.
|
|
343
|
+
return [];
|
|
344
|
+
}
|
|
345
|
+
const facts = principalFactsOf(question.principal, question.origin);
|
|
346
|
+
const { grants, denies } = rulesFor(file, question.resource, "update");
|
|
347
|
+
// An update deny decided on the row as it is (or on a value of the wrong type,
|
|
348
|
+
// failing closed): no grant lets the update in. A deny on a relation is the row
|
|
349
|
+
// after's to answer.
|
|
350
|
+
if (foldDenies(file, denies, facts).some((deny) => {
|
|
351
|
+
const truth = evaluateOnRow(file, question.resource, deny.condition, row).k;
|
|
352
|
+
return truth === "true" || truth === "unknown";
|
|
353
|
+
})) {
|
|
354
|
+
return [];
|
|
355
|
+
}
|
|
356
|
+
// Each grant's `where` and its subject's row condition, without its `after`: the
|
|
357
|
+
// condition `scopeOf` reads, asked about the row as it is. The answer lists the
|
|
358
|
+
// keys in the file's order.
|
|
359
|
+
const decided = foldGrants(file, grants, facts).map((grant) => ({
|
|
360
|
+
key: keyOf(grant),
|
|
361
|
+
residual: evaluateOnRow(file, question.resource, grant.condition, row),
|
|
362
|
+
}));
|
|
363
|
+
const covering = () => decided.flatMap(({ key, residual }) => residual.k === "true" ? [key] : []);
|
|
364
|
+
if (decided.every(({ residual }) => isDecided(residual) || residual.k === "unknown")) {
|
|
365
|
+
return covering();
|
|
366
|
+
}
|
|
367
|
+
return (async () => {
|
|
368
|
+
const lookups = [];
|
|
369
|
+
for (const entry of decided) {
|
|
370
|
+
const { residual } = entry;
|
|
371
|
+
if (isDecided(residual) || residual.k === "unknown") {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const truth = await decideOnRow({
|
|
375
|
+
policies: file,
|
|
376
|
+
resource: question.resource,
|
|
377
|
+
condition: residual,
|
|
378
|
+
row,
|
|
379
|
+
kind: "existing",
|
|
380
|
+
exists: question.exists,
|
|
381
|
+
lookups,
|
|
382
|
+
});
|
|
383
|
+
// A grant that cannot be decided does not cover the row.
|
|
384
|
+
entry.residual = truth === true ? TRUE : FALSE;
|
|
385
|
+
}
|
|
386
|
+
return covering();
|
|
387
|
+
})();
|
|
388
|
+
};
|
|
389
|
+
return { can, grantsOf, scopeOf, fieldsOf };
|
|
390
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AccessPolicies } from "./policies-contract.js";
|
|
2
|
+
/** The canonical text of a `@1` file (see the header). The file is not checked here. */
|
|
3
|
+
export declare const canonicalPolicies: (policies: AccessPolicies) => string;
|
|
4
|
+
/**
|
|
5
|
+
* The canonical hash of a policies file: `sha256:` and the SHA-256 of its canonical
|
|
6
|
+
* byte form, in lowercase hex. The same rules give the same hash however the file's
|
|
7
|
+
* members and rules are ordered; any changed name or literal gives a different one.
|
|
8
|
+
* The file is checked first, so a file the engine would refuse has no hash.
|
|
9
|
+
*/
|
|
10
|
+
export declare const policiesHash: (policies: AccessPolicies) => string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// The canonical byte form of a `policies@1` file and its hash. The
|
|
2
|
+
// registry keys revisions by this hash, `avAccess.lock.json` pins it, and the build
|
|
3
|
+
// and the CLI both call this code to check a pulled file, so one function decides
|
|
4
|
+
// what "the same file" means.
|
|
5
|
+
//
|
|
6
|
+
// The canonical form is the file's JSON text, written by `canonical-json.writer.ts`
|
|
7
|
+
// (members sorted by key, no whitespace) and encoded as UTF-8, with:
|
|
8
|
+
// - the lists whose order the format gives no meaning sorted: `grants` and `denies`
|
|
9
|
+
// by `id`; a rule's `subjects`, `actions`, `fields` and `write.except`; each
|
|
10
|
+
// `govern` list; each schema entry's `fields` and `nullable`. `governModes` is objects
|
|
11
|
+
// only, so its members are sorted like any other.
|
|
12
|
+
// Every other list keeps its order: identifiers (the first is the primary one),
|
|
13
|
+
// relation reference pairs, principal and claim paths, a condition's arguments and
|
|
14
|
+
// an `in` list's values.
|
|
15
|
+
import { byId, canonicalJson, sortedNames as sorted, } from "./canonical-json.writer.js";
|
|
16
|
+
import { checkPolicies } from "./policies.compiler.js";
|
|
17
|
+
import { sha256Hex } from "./sha256.digest.js";
|
|
18
|
+
const canonicalGrant = (grant) => ({
|
|
19
|
+
...grant,
|
|
20
|
+
subjects: sorted(grant.subjects),
|
|
21
|
+
actions: sorted(grant.actions),
|
|
22
|
+
...(grant.fields !== undefined ? { fields: sorted(grant.fields) } : {}),
|
|
23
|
+
...(grant.write !== undefined
|
|
24
|
+
? { write: { except: sorted(grant.write.except) } }
|
|
25
|
+
: {}),
|
|
26
|
+
});
|
|
27
|
+
const canonicalDeny = (deny) => ({
|
|
28
|
+
...deny,
|
|
29
|
+
actions: deny.actions === "*" ? "*" : sorted(deny.actions),
|
|
30
|
+
});
|
|
31
|
+
const canonicalFacts = (facts) => ({
|
|
32
|
+
...facts,
|
|
33
|
+
fields: sorted(facts.fields),
|
|
34
|
+
...(facts.nullable !== undefined ? { nullable: sorted(facts.nullable) } : {}),
|
|
35
|
+
});
|
|
36
|
+
const mapValues = (record, map) => Object.fromEntries(Object.entries(record).map(([key, value]) => [key, map(value)]));
|
|
37
|
+
/** The canonical text of a `@1` file (see the header). The file is not checked here. */
|
|
38
|
+
export const canonicalPolicies = (policies) => canonicalJson({
|
|
39
|
+
...policies,
|
|
40
|
+
schema: mapValues(policies.schema, canonicalFacts),
|
|
41
|
+
govern: mapValues(policies.govern, (fields) => sorted(fields)),
|
|
42
|
+
grants: [...policies.grants].map(canonicalGrant).sort(byId),
|
|
43
|
+
denies: [...policies.denies].map(canonicalDeny).sort(byId),
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* The canonical hash of a policies file: `sha256:` and the SHA-256 of its canonical
|
|
47
|
+
* byte form, in lowercase hex. The same rules give the same hash however the file's
|
|
48
|
+
* members and rules are ordered; any changed name or literal gives a different one.
|
|
49
|
+
* The file is checked first, so a file the engine would refuse has no hash.
|
|
50
|
+
*/
|
|
51
|
+
export const policiesHash = (policies) => {
|
|
52
|
+
checkPolicies(policies);
|
|
53
|
+
return `sha256:${sha256Hex(canonicalPolicies(policies))}`;
|
|
54
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type AccessWhere } from "./aventara-where.translator.js";
|
|
2
|
+
import type { AccessPolicies } from "./policies-contract.js";
|
|
3
|
+
import { type Resolved } from "./scope.resolver.js";
|
|
4
|
+
/**
|
|
5
|
+
* A row as core hands it to `can`: its scalar fields. A relation member on it is
|
|
6
|
+
* ignored; never put relation data a client sent on it.
|
|
7
|
+
*/
|
|
8
|
+
export type AccessRow = {
|
|
9
|
+
readonly [field: string]: unknown;
|
|
10
|
+
};
|
|
11
|
+
/** Core's `exists`: whether a row of `resource` meets `where`, unscoped. */
|
|
12
|
+
export type AccessExists = (resource: string, where: AccessWhere) => Promise<boolean>;
|
|
13
|
+
/** One `exists` question the engine asked, and its answer. */
|
|
14
|
+
export type AccessLookup = {
|
|
15
|
+
readonly resource: string;
|
|
16
|
+
readonly where: AccessWhere;
|
|
17
|
+
readonly answer: boolean;
|
|
18
|
+
};
|
|
19
|
+
/** A three-valued answer: decided, or not decidable from what the engine has. */
|
|
20
|
+
export type AccessTruth = boolean | "unknown";
|
|
21
|
+
/** Whether the row is about to be created, or already exists (an after check). */
|
|
22
|
+
export type AccessRowKind = "new" | "existing";
|
|
23
|
+
/**
|
|
24
|
+
* A decimal's value as `{ sign, digits, point }`: `0.<digits> × 10^point`, with no
|
|
25
|
+
* leading or trailing zero in `digits` (zero is `{ sign: "", digits: "", point: 0 }`).
|
|
26
|
+
* `undefined` for text that is no decimal.
|
|
27
|
+
*/
|
|
28
|
+
export declare const decimalParts: (text: string) => {
|
|
29
|
+
readonly sign: "" | "-";
|
|
30
|
+
readonly digits: string;
|
|
31
|
+
readonly point: number;
|
|
32
|
+
} | undefined;
|
|
33
|
+
/** An instant's epoch milliseconds, from a valid `Date` or an ISO-8601 string that reads back as written. */
|
|
34
|
+
export declare const instantOf: (value: unknown) => number | undefined;
|
|
35
|
+
/** A condition a row decided as neither true nor false: a value of the wrong type. */
|
|
36
|
+
export type Unknown = {
|
|
37
|
+
readonly k: "unknown";
|
|
38
|
+
};
|
|
39
|
+
/** What evaluating on a row gives: `TRUE`, `FALSE`, `UNKNOWN`, or the residual. */
|
|
40
|
+
export type Evaluated = Resolved | Unknown;
|
|
41
|
+
/**
|
|
42
|
+
* Evaluates what the row carries. The answer is `TRUE`, `FALSE`, `UNKNOWN` (a value
|
|
43
|
+
* of another type than its field's), or the residual condition: the part the row
|
|
44
|
+
* could not decide, with the decided parts folded away. A relation member on the row
|
|
45
|
+
* is ignored: every relation step stays whole in the residual, for `exists`. Each comparison is made by its
|
|
46
|
+
* field's type (`types`): a decimal by its digits, a datetime by its instant, a
|
|
47
|
+
* bigint exactly.
|
|
48
|
+
*/
|
|
49
|
+
export declare const evaluateOnRow: (policies: AccessPolicies, resource: string, node: Resolved, row: AccessRow) => Evaluated;
|
|
50
|
+
/**
|
|
51
|
+
* Decides a folded condition on a row: in memory first, then through `exists` for
|
|
52
|
+
* what the row does not carry. `"unknown"` when neither can decide.
|
|
53
|
+
*/
|
|
54
|
+
export declare const decideOnRow: (args: {
|
|
55
|
+
readonly policies: AccessPolicies;
|
|
56
|
+
readonly resource: string;
|
|
57
|
+
readonly condition: Resolved;
|
|
58
|
+
readonly row: AccessRow;
|
|
59
|
+
readonly kind: AccessRowKind;
|
|
60
|
+
readonly exists: AccessExists | undefined;
|
|
61
|
+
readonly lookups: AccessLookup[];
|
|
62
|
+
}) => Promise<AccessTruth>;
|