@aventara/access 0.0.0-stage → 0.1.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +831 -0
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +2439 -2
- package/dist/access/access-options.d.ts +66 -0
- package/dist/access/access-options.js +5 -0
- package/dist/access/access.error.d.ts +35 -0
- package/dist/access/access.error.js +11 -0
- package/dist/access/access.factory.d.ts +273 -0
- package/dist/access/access.factory.js +440 -0
- package/dist/access/better-auth.hardening.d.ts +120 -0
- package/dist/access/better-auth.hardening.js +420 -0
- package/dist/access/better-auth.loader.d.ts +67 -0
- package/dist/access/better-auth.loader.js +65 -0
- package/dist/access/better-auth.range.d.ts +9 -0
- package/dist/access/better-auth.range.js +28 -0
- package/dist/access/better-auth.require.d.ts +90 -0
- package/dist/access/better-auth.require.js +6 -0
- package/dist/access/better-auth.require.node.d.ts +7 -0
- package/dist/access/better-auth.require.node.js +52 -0
- package/dist/access/claims.mapper.d.ts +79 -0
- package/dist/access/claims.mapper.js +152 -0
- package/dist/access/key-set.cache.d.ts +10 -0
- package/dist/access/key-set.cache.js +63 -0
- package/dist/access/policies-block.checker.d.ts +64 -0
- package/dist/access/policies-block.checker.js +138 -0
- package/dist/access/principal.subject.d.ts +12 -0
- package/dist/access/principal.subject.js +60 -0
- package/dist/access/profile.policy.d.ts +74 -0
- package/dist/access/profile.policy.js +110 -0
- package/dist/access/request.state.d.ts +89 -0
- package/dist/access/request.state.js +196 -0
- package/dist/access/session.hooks.d.ts +114 -0
- package/dist/access/session.hooks.js +258 -0
- package/dist/access/token.verifier.d.ts +35 -0
- package/dist/access/token.verifier.js +69 -0
- package/dist/access-schema.d.ts +48 -0
- package/dist/access-schema.js +5 -0
- package/dist/access-warning.emitter.d.ts +2 -0
- package/dist/access-warning.emitter.js +20 -0
- package/dist/access.builder.d.ts +299 -0
- package/dist/access.builder.js +66 -0
- package/dist/access.engine.d.ts +142 -0
- package/dist/access.engine.js +470 -0
- package/dist/audit/audit-event.d.ts +135 -0
- package/dist/audit/audit-event.js +8 -0
- package/dist/audit/audit.log.d.ts +122 -0
- package/dist/audit/audit.log.js +345 -0
- package/dist/audit/audit.schema.d.ts +143 -0
- package/dist/audit/audit.schema.js +50 -0
- package/dist/auth/index.d.ts +12 -0
- package/dist/auth/index.js +2 -0
- package/dist/aventara-where.translator.d.ts +19 -0
- package/dist/aventara-where.translator.js +87 -0
- package/dist/beacon/beacon.check.d.ts +59 -0
- package/dist/beacon/beacon.check.js +128 -0
- package/dist/beacon/beacon.schema.d.ts +41 -0
- package/dist/beacon/beacon.schema.js +48 -0
- package/dist/beacon/beacon.store.d.ts +29 -0
- package/dist/beacon/beacon.store.js +78 -0
- package/dist/better-auth/access.client-plugin.d.ts +75 -0
- package/dist/better-auth/access.client-plugin.js +133 -0
- package/dist/better-auth/access.plugin.d.ts +164 -0
- package/dist/better-auth/access.plugin.js +348 -0
- package/dist/better-auth/access.provider.d.ts +78 -0
- package/dist/better-auth/access.provider.js +100 -0
- package/dist/better-auth/index.d.ts +7 -0
- package/dist/better-auth/index.js +4 -0
- package/dist/better-auth/organization.bridge.d.ts +66 -0
- package/dist/better-auth/organization.bridge.js +144 -0
- package/dist/canonical-json.writer.d.ts +12 -0
- package/dist/canonical-json.writer.js +30 -0
- package/dist/cli/avaccess.bin.d.ts +2 -0
- package/dist/cli/avaccess.bin.js +26 -0
- package/dist/cli/avaccess.program.d.ts +15 -0
- package/dist/cli/avaccess.program.js +166 -0
- package/dist/cli/beacon-pins.file.d.ts +14 -0
- package/dist/cli/beacon-pins.file.js +62 -0
- package/dist/cli/beacon.loader.d.ts +20 -0
- package/dist/cli/beacon.loader.js +71 -0
- package/dist/cli/cli.context.d.ts +49 -0
- package/dist/cli/cli.context.js +66 -0
- package/dist/cli/diff.command.d.ts +2 -0
- package/dist/cli/diff.command.js +66 -0
- package/dist/cli/generate.command.d.ts +2 -0
- package/dist/cli/generate.command.js +82 -0
- package/dist/cli/keys.command.d.ts +2 -0
- package/dist/cli/keys.command.js +96 -0
- package/dist/cli/lock.file.d.ts +31 -0
- package/dist/cli/lock.file.js +181 -0
- package/dist/cli/module.loader.d.ts +13 -0
- package/dist/cli/module.loader.js +63 -0
- package/dist/cli/policies.diff.d.ts +16 -0
- package/dist/cli/policies.diff.js +83 -0
- package/dist/cli/pull.command.d.ts +2 -0
- package/dist/cli/pull.command.js +234 -0
- package/dist/cli/push.command.d.ts +2 -0
- package/dist/cli/push.command.js +59 -0
- package/dist/cli/registry.client.d.ts +63 -0
- package/dist/cli/registry.client.js +287 -0
- package/dist/cli/schema.command.d.ts +2 -0
- package/dist/cli/schema.command.js +60 -0
- package/dist/cli/serve.command.d.ts +6 -0
- package/dist/cli/serve.command.js +200 -0
- package/dist/cli/status.command.d.ts +2 -0
- package/dist/cli/status.command.js +31 -0
- package/dist/cli/translator.flag.d.ts +6 -0
- package/dist/cli/translator.flag.js +24 -0
- package/dist/cli/verify.command.d.ts +2 -0
- package/dist/cli/verify.command.js +40 -0
- package/dist/client/access-client.factory.d.ts +92 -0
- package/dist/client/access-client.factory.js +269 -0
- package/dist/client/access-verbs.schema.d.ts +66 -0
- package/dist/client/access-verbs.schema.js +159 -0
- package/dist/client/verb-rate.limiter.d.ts +116 -0
- package/dist/client/verb-rate.limiter.js +358 -0
- package/dist/foreign/foreign-facts.d.ts +30 -0
- package/dist/foreign/foreign-facts.js +75 -0
- package/dist/foreign/foreign.merger.d.ts +10 -0
- package/dist/foreign/foreign.merger.js +52 -0
- package/dist/foreign-references.check.d.ts +12 -0
- package/dist/foreign-references.check.js +134 -0
- package/dist/generator/client.emitter.d.ts +28 -0
- package/dist/generator/client.emitter.js +411 -0
- package/dist/generator/copied-modules.reader.d.ts +14 -0
- package/dist/generator/copied-modules.reader.js +80 -0
- package/dist/generator/namespace.facts.d.ts +28 -0
- package/dist/generator/namespace.facts.js +32 -0
- package/dist/generator/orm.translator.d.ts +48 -0
- package/dist/generator/orm.translator.js +29 -0
- package/dist/generator/prisma7/prisma-schema.reader.d.ts +9 -0
- package/dist/generator/prisma7/prisma-schema.reader.js +394 -0
- package/dist/generator/prisma7/prisma-where.translator.d.ts +72 -0
- package/dist/generator/prisma7/prisma-where.translator.js +136 -0
- package/dist/generator/prisma7/prisma7.translator.d.ts +2 -0
- package/dist/generator/prisma7/prisma7.translator.js +87 -0
- package/dist/generator/schema.emitter.d.ts +14 -0
- package/dist/generator/schema.emitter.js +23 -0
- package/dist/generator/tsconfig.reader.d.ts +44 -0
- package/dist/generator/tsconfig.reader.js +110 -0
- package/dist/generator/typescript-literal.writer.d.ts +5 -0
- package/dist/generator/typescript-literal.writer.js +42 -0
- package/dist/guardrails/guardrails-contract.d.ts +45 -0
- package/dist/guardrails/guardrails-contract.js +12 -0
- package/dist/guardrails/guardrails.builder.d.ts +48 -0
- package/dist/guardrails/guardrails.builder.js +32 -0
- package/dist/guardrails/guardrails.compiler.d.ts +10 -0
- package/dist/guardrails/guardrails.compiler.js +254 -0
- package/dist/guardrails/guardrails.composer.d.ts +7 -0
- package/dist/guardrails/guardrails.composer.js +95 -0
- package/dist/guardrails/guardrails.hash.d.ts +10 -0
- package/dist/guardrails/guardrails.hash.js +38 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +6 -0
- package/dist/kiosk/kiosk-verbs.schema.d.ts +19 -0
- package/dist/kiosk/kiosk-verbs.schema.js +61 -0
- package/dist/kiosk/kiosk.plugin.d.ts +217 -0
- package/dist/kiosk/kiosk.plugin.js +877 -0
- package/dist/kiosk/kiosk.schema.d.ts +176 -0
- package/dist/kiosk/kiosk.schema.js +101 -0
- package/dist/kiosk/kiosk.store.d.ts +171 -0
- package/dist/kiosk/kiosk.store.js +478 -0
- package/dist/kiosk/lan-range.policy.d.ts +31 -0
- package/dist/kiosk/lan-range.policy.js +113 -0
- package/dist/kiosk/pin.policy.d.ts +9 -0
- package/dist/kiosk/pin.policy.js +143 -0
- package/dist/kiosk/session-use.marker.d.ts +4 -0
- package/dist/kiosk/session-use.marker.js +16 -0
- package/dist/lock/lock-contract.d.ts +38 -0
- package/dist/lock/lock-contract.js +67 -0
- package/dist/lock/lock.verifier.d.ts +27 -0
- package/dist/lock/lock.verifier.js +62 -0
- package/dist/mcp/agent-scope.gate.d.ts +11 -0
- package/dist/mcp/agent-scope.gate.js +75 -0
- package/dist/mcp/agent.grant.d.ts +36 -0
- package/dist/mcp/agent.grant.js +92 -0
- package/dist/mcp/mcp-options.d.ts +60 -0
- package/dist/mcp/mcp-options.js +72 -0
- package/dist/oauth/access-token.verifier.d.ts +66 -0
- package/dist/oauth/access-token.verifier.js +201 -0
- package/dist/oauth/oauth-provider.hardening.d.ts +62 -0
- package/dist/oauth/oauth-provider.hardening.js +261 -0
- package/dist/passwords/argon2id.hasher.d.ts +97 -0
- package/dist/passwords/argon2id.hasher.js +256 -0
- package/dist/passwords/node-rs-argon2.adapter.d.ts +25 -0
- package/dist/passwords/node-rs-argon2.adapter.js +42 -0
- package/dist/passwords/passwords.options.d.ts +79 -0
- package/dist/passwords/passwords.options.js +212 -0
- package/dist/policies-contract.d.ts +192 -0
- package/dist/policies-contract.js +8 -0
- package/dist/policies.compiler.d.ts +36 -0
- package/dist/policies.compiler.js +1358 -0
- package/dist/policies.decider.d.ts +103 -0
- package/dist/policies.decider.js +382 -0
- package/dist/policies.hash.d.ts +10 -0
- package/dist/policies.hash.js +53 -0
- package/dist/predicate.evaluator.d.ts +62 -0
- package/dist/predicate.evaluator.js +378 -0
- package/dist/random-secret.generator.d.ts +2 -0
- package/dist/random-secret.generator.js +15 -0
- package/dist/reference-reads.check.d.ts +12 -0
- package/dist/reference-reads.check.js +148 -0
- package/dist/registry/access.registry.d.ts +173 -0
- package/dist/registry/access.registry.js +865 -0
- package/dist/registry/beacon.ledger.d.ts +30 -0
- package/dist/registry/beacon.ledger.js +93 -0
- package/dist/registry/ownership.check.d.ts +54 -0
- package/dist/registry/ownership.check.js +215 -0
- package/dist/registry/registry.endpoints.d.ts +50 -0
- package/dist/registry/registry.endpoints.js +193 -0
- package/dist/registry/registry.key.d.ts +7 -0
- package/dist/registry/registry.key.js +13 -0
- package/dist/registry/registry.options.d.ts +13 -0
- package/dist/registry/registry.options.js +28 -0
- package/dist/registry/registry.refusal.d.ts +6 -0
- package/dist/registry/registry.refusal.js +11 -0
- package/dist/registry/registry.schema.d.ts +238 -0
- package/dist/registry/registry.schema.js +127 -0
- package/dist/resources/auth-tables.catalog.d.ts +91 -0
- package/dist/resources/auth-tables.catalog.js +102 -0
- package/dist/resources/auth-tables.guard.d.ts +95 -0
- package/dist/resources/auth-tables.guard.js +405 -0
- package/dist/scope.resolver.d.ts +138 -0
- package/dist/scope.resolver.js +512 -0
- package/dist/sha256.digest.d.ts +2 -0
- package/dist/sha256.digest.js +81 -0
- package/dist/standalone/access.server.d.ts +41 -0
- package/dist/standalone/access.server.js +301 -0
- package/dist/standalone/index.d.ts +1 -0
- package/dist/standalone/index.js +1 -0
- package/package.json +101 -3
- package/src/aventara-where.translator.ts +116 -0
- package/src/beacon/beacon.check.ts +204 -0
- package/src/canonical-json.writer.ts +51 -0
- package/src/generator/prisma7/prisma-where.translator.ts +241 -0
- package/src/policies-contract.ts +184 -0
- package/src/policies.decider.ts +650 -0
- package/src/predicate.evaluator.ts +574 -0
- package/src/scope.resolver.ts +791 -0
- package/src/sha256.digest.ts +85 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
// In-memory evaluation of a folded condition against a row's own scalar fields, and
|
|
2
|
+
// the `exists` questions for the rest. A to-one step whose references the row carries
|
|
3
|
+
// is asked on the row they name, with the row's values; whatever else an existing row
|
|
4
|
+
// leaves is asked about the row itself, by its identifier. A relation member on the
|
|
5
|
+
// row is never read: core strips them, and any other caller could have forged one.
|
|
6
|
+
// What cannot be asked is unknown, and the caller fails closed on it.
|
|
7
|
+
import { mergeWhere, translateWhere, } from "./aventara-where.translator.js";
|
|
8
|
+
import { allOf, andOf, FALSE, notOf, nullSafe, orOf, ownOf, TRUE, } from "./scope.resolver.js";
|
|
9
|
+
/** The type `can` compares `field` of `resource` by. */
|
|
10
|
+
const typeOf = (policies, resource, field) => ownOf(ownOf(policies.schema, resource)?.types, field);
|
|
11
|
+
const DECIMAL = /^([+-]?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/;
|
|
12
|
+
/**
|
|
13
|
+
* A decimal's value as `{ sign, digits, point }`: `0.<digits> × 10^point`, with no
|
|
14
|
+
* leading or trailing zero in `digits` (zero is `{ sign: "", digits: "", point: 0 }`).
|
|
15
|
+
* `undefined` for text that is no decimal.
|
|
16
|
+
*/
|
|
17
|
+
export const decimalParts = (text) => {
|
|
18
|
+
const match = DECIMAL.exec(text);
|
|
19
|
+
if (match === null) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
// Indexed, not destructured: destructuring a match costs the type checker its
|
|
23
|
+
// iterator types (about 1,000 instantiations, the type budget's whole margin).
|
|
24
|
+
const sign = match[1] ?? "";
|
|
25
|
+
const whole = match[2] ?? "";
|
|
26
|
+
const fraction = match[3] ?? "";
|
|
27
|
+
const exponent = match[4] ?? "0";
|
|
28
|
+
const shift = Number(exponent);
|
|
29
|
+
if (!Number.isSafeInteger(shift)) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
let digits = whole + fraction;
|
|
33
|
+
let point = whole.length + shift;
|
|
34
|
+
const leading = digits.length - digits.replace(/^0+/, "").length;
|
|
35
|
+
digits = digits.slice(leading).replace(/0+$/, "");
|
|
36
|
+
point -= leading;
|
|
37
|
+
return digits === ""
|
|
38
|
+
? { sign: "", digits: "", point: 0 }
|
|
39
|
+
: { sign: sign === "-" ? "-" : "", digits, point };
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* An ISO-8601 date, or date and time with its offset (never a local time), to the
|
|
43
|
+
* millisecond at most.
|
|
44
|
+
*/
|
|
45
|
+
const INSTANT = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?(Z|([+-])(\d{2}):(\d{2})))?$/;
|
|
46
|
+
/**
|
|
47
|
+
* The epoch milliseconds of an ISO-8601 string that reads back as written: no
|
|
48
|
+
* February 30th, no hour 24, no offset past 23:59, nothing past the millisecond.
|
|
49
|
+
*/
|
|
50
|
+
const instantOfText = (text) => {
|
|
51
|
+
const match = INSTANT.exec(text);
|
|
52
|
+
if (match === null) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const part = (index) => Number(match[index] ?? "0");
|
|
56
|
+
const year = part(1);
|
|
57
|
+
const month = part(2);
|
|
58
|
+
const day = part(3);
|
|
59
|
+
const hour = part(4);
|
|
60
|
+
const minute = part(5);
|
|
61
|
+
const second = part(6);
|
|
62
|
+
const millisecond = Number((match[7] ?? "").padEnd(3, "0"));
|
|
63
|
+
const local = Date.UTC(year, month - 1, day, hour, minute, second, millisecond);
|
|
64
|
+
const read = new Date(local);
|
|
65
|
+
if (read.getUTCFullYear() !== year ||
|
|
66
|
+
read.getUTCMonth() !== month - 1 ||
|
|
67
|
+
read.getUTCDate() !== day ||
|
|
68
|
+
read.getUTCHours() !== hour ||
|
|
69
|
+
read.getUTCMinutes() !== minute ||
|
|
70
|
+
read.getUTCSeconds() !== second ||
|
|
71
|
+
part(10) > 23 ||
|
|
72
|
+
part(11) > 59) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const offset = (part(10) * 60 + part(11)) * 60_000;
|
|
76
|
+
return match[9] === "-" ? local + offset : local - offset;
|
|
77
|
+
};
|
|
78
|
+
/** An instant's epoch milliseconds, from a valid `Date` or an ISO-8601 string that reads back as written. */
|
|
79
|
+
export const instantOf = (value) => {
|
|
80
|
+
if (value instanceof Date) {
|
|
81
|
+
const time = value.getTime();
|
|
82
|
+
return Number.isNaN(time) ? undefined : time;
|
|
83
|
+
}
|
|
84
|
+
return typeof value === "string" ? instantOfText(value) : undefined;
|
|
85
|
+
};
|
|
86
|
+
/** A decimal object's text: core's `AvDecimal`, an ORM's decimal, anything whose `toString()` is decimal digits. */
|
|
87
|
+
const decimalText = (value) => typeof value === "string"
|
|
88
|
+
? value
|
|
89
|
+
: typeof value === "number"
|
|
90
|
+
? Number.isFinite(value)
|
|
91
|
+
? String(value)
|
|
92
|
+
: undefined
|
|
93
|
+
: typeof value === "object" &&
|
|
94
|
+
value !== null &&
|
|
95
|
+
!(value instanceof Date) &&
|
|
96
|
+
!Array.isArray(value)
|
|
97
|
+
? String(value)
|
|
98
|
+
: undefined;
|
|
99
|
+
/**
|
|
100
|
+
* The key a value is compared by, for a field of `type`: equal keys, equal values.
|
|
101
|
+
* `undefined` when the value is not one of the type's (a type mismatch).
|
|
102
|
+
*/
|
|
103
|
+
const comparisonKey = (value, type) => {
|
|
104
|
+
switch (type) {
|
|
105
|
+
case "decimal": {
|
|
106
|
+
const text = decimalText(value);
|
|
107
|
+
const parts = text === undefined ? undefined : decimalParts(text);
|
|
108
|
+
return parts === undefined
|
|
109
|
+
? undefined
|
|
110
|
+
: `${parts.sign}${parts.digits}@${parts.point}`;
|
|
111
|
+
}
|
|
112
|
+
case "datetime":
|
|
113
|
+
return instantOf(value);
|
|
114
|
+
case "bigint":
|
|
115
|
+
return typeof value === "bigint"
|
|
116
|
+
? value
|
|
117
|
+
: typeof value === "number" && Number.isSafeInteger(value)
|
|
118
|
+
? BigInt(value)
|
|
119
|
+
: typeof value === "string" && /^-?\d+$/.test(value)
|
|
120
|
+
? BigInt(value)
|
|
121
|
+
: undefined;
|
|
122
|
+
default:
|
|
123
|
+
return typeof value === "string" ||
|
|
124
|
+
typeof value === "boolean" ||
|
|
125
|
+
(typeof value === "number" && Number.isFinite(value))
|
|
126
|
+
? value
|
|
127
|
+
: undefined;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* One comparison of a row's value with a rule's, three-valued: a value of another
|
|
132
|
+
* type than the field's, or than the rule's, is `"unknown"`.
|
|
133
|
+
*/
|
|
134
|
+
const compare = (rowValue, cmp, value, type) => {
|
|
135
|
+
if (rowValue === null) {
|
|
136
|
+
// A comparison on NULL is false, except `equals: null` (IS NULL); a list never
|
|
137
|
+
// holds it for `can`.
|
|
138
|
+
return cmp === "eq" && value === null;
|
|
139
|
+
}
|
|
140
|
+
const actual = comparisonKey(rowValue, type);
|
|
141
|
+
const equal = (literal) => {
|
|
142
|
+
const expected = comparisonKey(literal, type);
|
|
143
|
+
return actual === undefined ||
|
|
144
|
+
expected === undefined ||
|
|
145
|
+
typeof actual !== typeof expected
|
|
146
|
+
? "unknown"
|
|
147
|
+
: actual === expected;
|
|
148
|
+
};
|
|
149
|
+
if (cmp === "eq" || cmp === "ne") {
|
|
150
|
+
if (value === null) {
|
|
151
|
+
return cmp === "ne";
|
|
152
|
+
}
|
|
153
|
+
const truth = equal(value);
|
|
154
|
+
return truth === "unknown" ? truth : cmp === "eq" ? truth : !truth;
|
|
155
|
+
}
|
|
156
|
+
let unknown = false;
|
|
157
|
+
for (const literal of value) {
|
|
158
|
+
if (literal === null) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const truth = equal(literal);
|
|
162
|
+
if (truth === true) {
|
|
163
|
+
return cmp === "in";
|
|
164
|
+
}
|
|
165
|
+
unknown ||= truth === "unknown";
|
|
166
|
+
}
|
|
167
|
+
return unknown ? "unknown" : cmp === "notIn";
|
|
168
|
+
};
|
|
169
|
+
const UNKNOWN = { k: "unknown" };
|
|
170
|
+
/**
|
|
171
|
+
* A conjunction, three-valued. A part that is unknown makes the whole unknown unless
|
|
172
|
+
* another part is false: an unknown never waits on an `exists` question, and the
|
|
173
|
+
* caller fails closed on it.
|
|
174
|
+
*/
|
|
175
|
+
const and3 = (k, args) => args.some((arg) => arg.k === "false")
|
|
176
|
+
? FALSE
|
|
177
|
+
: args.some((arg) => arg.k === "unknown")
|
|
178
|
+
? UNKNOWN
|
|
179
|
+
: (k === "and" ? andOf : allOf)(args);
|
|
180
|
+
const or3 = (args) => args.some((arg) => arg.k === "true")
|
|
181
|
+
? TRUE
|
|
182
|
+
: args.some((arg) => arg.k === "unknown")
|
|
183
|
+
? UNKNOWN
|
|
184
|
+
: orOf(args);
|
|
185
|
+
/** A row's own field: an inherited one is not on the row. */
|
|
186
|
+
const fieldOf = (row, field) => Object.hasOwn(row, field) ? row[field] : undefined;
|
|
187
|
+
/**
|
|
188
|
+
* Evaluates what the row carries. The answer is `TRUE`, `FALSE`, `UNKNOWN` (a value
|
|
189
|
+
* of another type than its field's), or the residual condition: the part the row
|
|
190
|
+
* could not decide, with the decided parts folded away. A relation member on the row
|
|
191
|
+
* is ignored: every relation step stays whole in the residual, for `exists`. Each comparison is made by its
|
|
192
|
+
* field's type (`types`): a decimal by its digits, a datetime by its instant, a
|
|
193
|
+
* bigint exactly.
|
|
194
|
+
*/
|
|
195
|
+
export const evaluateOnRow = (policies, resource, node, row) => {
|
|
196
|
+
switch (node.k) {
|
|
197
|
+
case "true":
|
|
198
|
+
case "false":
|
|
199
|
+
return node;
|
|
200
|
+
case "and":
|
|
201
|
+
case "all":
|
|
202
|
+
return and3(node.k, node.args.map((arg) => evaluateOnRow(policies, resource, arg, row)));
|
|
203
|
+
case "or":
|
|
204
|
+
return or3(node.args.map((arg) => evaluateOnRow(policies, resource, arg, row)));
|
|
205
|
+
case "not": {
|
|
206
|
+
const arg = evaluateOnRow(policies, resource, node.arg, row);
|
|
207
|
+
return arg.k === "unknown" ? arg : notOf(arg);
|
|
208
|
+
}
|
|
209
|
+
case "cmp": {
|
|
210
|
+
const value = fieldOf(row, node.field);
|
|
211
|
+
if (value === undefined) {
|
|
212
|
+
return node;
|
|
213
|
+
}
|
|
214
|
+
const truth = compare(value, node.cmp, node.value, typeOf(policies, resource, node.field));
|
|
215
|
+
return truth === "unknown" ? UNKNOWN : truth ? TRUE : FALSE;
|
|
216
|
+
}
|
|
217
|
+
case "rel":
|
|
218
|
+
case "some":
|
|
219
|
+
case "every":
|
|
220
|
+
case "none":
|
|
221
|
+
// A relation is never read off the row: whoever built the row could have put
|
|
222
|
+
// anything there. It is decided through the row's identifier or references
|
|
223
|
+
// and `exists`, on what the database holds.
|
|
224
|
+
return node;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const ask = async (context, resource, where) => {
|
|
228
|
+
if (context.exists === undefined) {
|
|
229
|
+
return "unknown";
|
|
230
|
+
}
|
|
231
|
+
const answer = (await context.exists(resource, where)) === true;
|
|
232
|
+
context.lookups.push({ resource, where, answer });
|
|
233
|
+
return answer;
|
|
234
|
+
};
|
|
235
|
+
/** An `exists` question's `where`: the row's own keys and the residual, null-safe. */
|
|
236
|
+
const combine = (policies, resource, where, residual) => {
|
|
237
|
+
const condition = translateWhere(nullSafe(policies, resource, residual));
|
|
238
|
+
return mergeWhere([where, condition]) ?? { AND: [where, condition] };
|
|
239
|
+
};
|
|
240
|
+
/** A row's value as an `exists` question's `where` states it: a `Date` as its ISO instant, a bigint as itself (exact). */
|
|
241
|
+
const whereValue = (value) => value instanceof Date ? value.toISOString() : value;
|
|
242
|
+
/** Asks about an existing row, by its first identifier the row carries in full. */
|
|
243
|
+
const askByIdentifier = async (context, resource, residual) => {
|
|
244
|
+
const identifiers = ownOf(context.policies.schema, resource)?.identifiers ?? [];
|
|
245
|
+
const identifier = identifiers.find((fields) => fields.every((field) => fieldOf(context.row, field) !== undefined &&
|
|
246
|
+
fieldOf(context.row, field) !== null));
|
|
247
|
+
if (identifier === undefined) {
|
|
248
|
+
return "unknown";
|
|
249
|
+
}
|
|
250
|
+
const where = Object.fromEntries(identifier.map((field) => [
|
|
251
|
+
field,
|
|
252
|
+
{ equals: whereValue(fieldOf(context.row, field)) },
|
|
253
|
+
]));
|
|
254
|
+
return ask(context, resource, combine(context.policies, resource, where, residual));
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* The reference values a to-one step's relation takes from the row: `undefined` when
|
|
258
|
+
* this end holds no references, or the row does not carry them all.
|
|
259
|
+
*/
|
|
260
|
+
const carriedReferences = (context, resource, node) => {
|
|
261
|
+
const relation = ownOf(ownOf(context.policies.schema, resource)?.relations, node.relation);
|
|
262
|
+
if (relation?.referenceFields === undefined ||
|
|
263
|
+
relation.references === undefined) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
const values = relation.referenceFields.map((field) => fieldOf(context.row, field));
|
|
267
|
+
return values.some((value) => value === undefined)
|
|
268
|
+
? undefined
|
|
269
|
+
: { references: relation.references, values };
|
|
270
|
+
};
|
|
271
|
+
/** Whether a residual holds, outside any relation, a to-one step the row's references decide. */
|
|
272
|
+
const stepsByReference = (context, resource, node) => {
|
|
273
|
+
switch (node.k) {
|
|
274
|
+
case "and":
|
|
275
|
+
case "all":
|
|
276
|
+
case "or":
|
|
277
|
+
return node.args.some((arg) => stepsByReference(context, resource, arg));
|
|
278
|
+
case "not":
|
|
279
|
+
return stepsByReference(context, resource, node.arg);
|
|
280
|
+
case "rel":
|
|
281
|
+
return carriedReferences(context, resource, node) !== undefined;
|
|
282
|
+
default:
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* Decides a residual through `exists`. A to-one step whose references the row carries
|
|
288
|
+
* is asked on the row they name, with the row's own values: for a created row, and for
|
|
289
|
+
* an updated one too, since a caller may ask before the write, when the database still
|
|
290
|
+
* holds the old reference. Whatever else an existing row leaves is asked about the row
|
|
291
|
+
* itself, by its identifier (in one question when no step goes by reference); for a
|
|
292
|
+
* created row it is unknown.
|
|
293
|
+
*/
|
|
294
|
+
const askOn = async (context, resource, node, kind) => {
|
|
295
|
+
if (kind === "existing" && !stepsByReference(context, resource, node)) {
|
|
296
|
+
return node.k === "true"
|
|
297
|
+
? true
|
|
298
|
+
: node.k === "false"
|
|
299
|
+
? false
|
|
300
|
+
: askByIdentifier(context, resource, node);
|
|
301
|
+
}
|
|
302
|
+
switch (node.k) {
|
|
303
|
+
case "true":
|
|
304
|
+
return true;
|
|
305
|
+
case "false":
|
|
306
|
+
return false;
|
|
307
|
+
case "and":
|
|
308
|
+
case "all": {
|
|
309
|
+
let unknown = false;
|
|
310
|
+
for (const arg of node.args) {
|
|
311
|
+
const answer = await askOn(context, resource, arg, kind);
|
|
312
|
+
if (answer === false) {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
unknown ||= answer === "unknown";
|
|
316
|
+
}
|
|
317
|
+
return unknown ? "unknown" : true;
|
|
318
|
+
}
|
|
319
|
+
case "or": {
|
|
320
|
+
let unknown = false;
|
|
321
|
+
for (const arg of node.args) {
|
|
322
|
+
const answer = await askOn(context, resource, arg, kind);
|
|
323
|
+
if (answer === true) {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
unknown ||= answer === "unknown";
|
|
327
|
+
}
|
|
328
|
+
return unknown ? "unknown" : false;
|
|
329
|
+
}
|
|
330
|
+
case "not": {
|
|
331
|
+
const answer = await askOn(context, resource, node.arg, kind);
|
|
332
|
+
return answer === "unknown" ? "unknown" : !answer;
|
|
333
|
+
}
|
|
334
|
+
case "rel": {
|
|
335
|
+
const carried = carriedReferences(context, resource, node);
|
|
336
|
+
if (carried === undefined) {
|
|
337
|
+
return kind === "existing"
|
|
338
|
+
? askByIdentifier(context, resource, node)
|
|
339
|
+
: "unknown";
|
|
340
|
+
}
|
|
341
|
+
if (carried.values.some((value) => value === null)) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
const where = Object.fromEntries(carried.references.map((field, index) => [
|
|
345
|
+
field,
|
|
346
|
+
{ equals: whereValue(carried.values[index]) },
|
|
347
|
+
]));
|
|
348
|
+
return ask(context, node.target, combine(context.policies, node.target, where, node.where));
|
|
349
|
+
}
|
|
350
|
+
default:
|
|
351
|
+
return kind === "existing"
|
|
352
|
+
? askByIdentifier(context, resource, node)
|
|
353
|
+
: "unknown";
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
/**
|
|
357
|
+
* Decides a folded condition on a row: in memory first, then through `exists` for
|
|
358
|
+
* what the row does not carry. `"unknown"` when neither can decide.
|
|
359
|
+
*/
|
|
360
|
+
export const decideOnRow = async (args) => {
|
|
361
|
+
const residual = evaluateOnRow(args.policies, args.resource, args.condition, args.row);
|
|
362
|
+
if (residual.k === "true") {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
if (residual.k === "false") {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
if (residual.k === "unknown") {
|
|
369
|
+
return "unknown";
|
|
370
|
+
}
|
|
371
|
+
const context = {
|
|
372
|
+
policies: args.policies,
|
|
373
|
+
row: args.row,
|
|
374
|
+
exists: args.exists,
|
|
375
|
+
lookups: args.lookups,
|
|
376
|
+
};
|
|
377
|
+
return askOn(context, args.resource, residual, args.kind);
|
|
378
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// The secrets Access issues (registry keys, kiosk device secrets, beacons): a
|
|
2
|
+
// prefix that names what the secret is, then 256 random bits from the runtime's
|
|
3
|
+
// CSPRNG in base64url (43 characters, no padding).
|
|
4
|
+
const base64url = (bytes) => {
|
|
5
|
+
let binary = "";
|
|
6
|
+
for (const byte of bytes) {
|
|
7
|
+
binary += String.fromCharCode(byte);
|
|
8
|
+
}
|
|
9
|
+
return btoa(binary)
|
|
10
|
+
.replace(/\+/g, "-")
|
|
11
|
+
.replace(/\//g, "_")
|
|
12
|
+
.replace(/=+$/, "");
|
|
13
|
+
};
|
|
14
|
+
/** `prefix` and 256 random bits from the runtime's CSPRNG, in base64url. */
|
|
15
|
+
export const randomSecret = (prefix) => `${prefix}${base64url(crypto.getRandomValues(new Uint8Array(32)))}`;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AccessPolicies } from "./policies-contract.js";
|
|
2
|
+
/**
|
|
3
|
+
* The warnings for `policies` hosted by a framework that holds references to the
|
|
4
|
+
* Resources in `held`, one per writing role and target Resource, each naming every
|
|
5
|
+
* reference that role may write to it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const referenceReadWarnings: (policies: AccessPolicies, held: ReadonlySet<string>) => readonly string[];
|
|
8
|
+
/**
|
|
9
|
+
* Emits `policies`' warnings under a framework that holds references to the
|
|
10
|
+
* Resources in `held`, each once per process and namespace.
|
|
11
|
+
*/
|
|
12
|
+
export declare const warnReferenceReads: (policies: AccessPolicies, held: ReadonlySet<string>) => void;
|
|
@@ -0,0 +1,148 @@
|
|
|
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 Resource no read grant reaches for that role. Aventara holds a reference a
|
|
4
|
+
// write sends to the rows the caller may read, so such a write is refused at run
|
|
5
|
+
// time (A4002) whatever the write grant says. It holds only references to the
|
|
6
|
+
// Resources its Core Contract shows, and the app's root exemptions decide those,
|
|
7
|
+
// which the rules cannot see. So the check runs when the framework hosts the
|
|
8
|
+
// engine: core calls `engine.hosted({ held })` once it is built, `held` being the
|
|
9
|
+
// Resources it holds references to, and only those are warned about. Compiling the
|
|
10
|
+
// rules (`.contract()`, `avaccess push`) and building their engine warn about
|
|
11
|
+
// nothing, and a service that does not run on Aventara (the generated client, the
|
|
12
|
+
// standalone host) holds no reference and is never told.
|
|
13
|
+
//
|
|
14
|
+
// It warns and never refuses: the engine answers exactly as before. The warning is
|
|
15
|
+
// Node.js's (`process.emitWarning`, code ACCESS_REFERENCE_UNREADABLE, silenced
|
|
16
|
+
// with `--disable-warning=ACCESS_REFERENCE_UNREADABLE`), else `console.warn`;
|
|
17
|
+
// each one is emitted once per process and namespace.
|
|
18
|
+
//
|
|
19
|
+
// "Reaches" errs towards silence, since the rules cannot say which roles one
|
|
20
|
+
// caller holds together. A read grant on the target (or on `*`) reaches a role when
|
|
21
|
+
// it names that role, `anyone`, or `signedIn` (for any role but `anyone`). A grant
|
|
22
|
+
// that names the target itself also reaches a `signedIn` writer whatever role it
|
|
23
|
+
// names (the caller may hold it), and reaches a role held on rows of one Resource
|
|
24
|
+
// when it names another role held on that same Resource. A held target the
|
|
25
|
+
// rules' schema does not have is reached by no grant (`*` does not reach it, and a
|
|
26
|
+
// grant cannot name it), so a write to it is warned about for every writer. Not
|
|
27
|
+
// warned about: a reference the grant's `write.except` locks (its relation or a
|
|
28
|
+
// reference field; the write is refused for the field itself), a target the
|
|
29
|
+
// framework does not hold, a target the rules read as foreign (the engine lists it
|
|
30
|
+
// in `unheld`, so the framework holds no reference to it; the write grant decides
|
|
31
|
+
// which row may be linked, foreign-references.check.ts), and a writing Resource it
|
|
32
|
+
// does not serve (`held` is its Core Contract's Resources, and no write reaches one
|
|
33
|
+
// outside it).
|
|
34
|
+
import { emitAccessWarning } from "./access-warning.emitter.js";
|
|
35
|
+
import { ownOf } from "./scope.resolver.js";
|
|
36
|
+
const WARNING_CODE = "ACCESS_REFERENCE_UNREADABLE";
|
|
37
|
+
/** The Resource a row role is held on, or `undefined` for `signedIn`, `anyone` and a global role. */
|
|
38
|
+
const heldOn = (policies, role) => {
|
|
39
|
+
const definition = ownOf(policies.roles, role);
|
|
40
|
+
return definition !== undefined && "on" in definition
|
|
41
|
+
? definition.on
|
|
42
|
+
: undefined;
|
|
43
|
+
};
|
|
44
|
+
const reaches = (policies, read, writer, target) => read.subjects.some((subject) => {
|
|
45
|
+
if (subject === writer ||
|
|
46
|
+
subject === "anyone" ||
|
|
47
|
+
(subject === "signedIn" && writer !== "anyone")) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (read.resource !== target) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
if (writer === "signedIn") {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const on = heldOn(policies, writer);
|
|
57
|
+
return on !== undefined && heldOn(policies, subject) === on;
|
|
58
|
+
});
|
|
59
|
+
const listed = (names) => names.length < 2
|
|
60
|
+
? names.join("")
|
|
61
|
+
: `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
|
|
62
|
+
/**
|
|
63
|
+
* The warnings for `policies` hosted by a framework that holds references to the
|
|
64
|
+
* Resources in `held`, one per writing role and target Resource, each naming every
|
|
65
|
+
* reference that role may write to it.
|
|
66
|
+
*/
|
|
67
|
+
export const referenceReadWarnings = (policies, held) => {
|
|
68
|
+
const reads = policies.grants.filter((grant) => grant.actions.includes("read"));
|
|
69
|
+
const found = new Map();
|
|
70
|
+
for (const grant of policies.grants) {
|
|
71
|
+
if (!grant.actions.some((a) => a === "create" || a === "update")) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const locked = new Set(grant.write?.except ?? []);
|
|
75
|
+
const resources = grant.resource === "*" ? Object.keys(policies.schema) : [grant.resource];
|
|
76
|
+
for (const resource of resources) {
|
|
77
|
+
const facts = ownOf(policies.schema, resource);
|
|
78
|
+
// A Resource the framework does not serve is never written through it
|
|
79
|
+
// (its Core Contract has no such Resource, nor a relation to it).
|
|
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 target = ownOf(policies.schema, fact.target);
|
|
88
|
+
if (fact.cardinality !== "one" ||
|
|
89
|
+
fields === undefined ||
|
|
90
|
+
locked.has(relation) ||
|
|
91
|
+
fields.some((field) => locked.has(field))) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// A foreign target is never held: the engine lists it in `unheld`.
|
|
95
|
+
if (!held.has(fact.target) || target?.foreign !== undefined) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// A target the schema does not have takes no grant: `*` does not
|
|
99
|
+
// reach it (`rulesFor`), and a grant cannot name it.
|
|
100
|
+
const own = target !== undefined;
|
|
101
|
+
const readsOfTarget = own
|
|
102
|
+
? reads.filter((read) => read.resource === fact.target || read.resource === "*")
|
|
103
|
+
: [];
|
|
104
|
+
for (const writer of grant.subjects) {
|
|
105
|
+
if (readsOfTarget.some((read) => reaches(policies, read, writer, fact.target))) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const key = `${writer}\u0000${fact.target}`;
|
|
109
|
+
const entry = found.get(key) ?? {
|
|
110
|
+
writer,
|
|
111
|
+
target: fact.target,
|
|
112
|
+
own,
|
|
113
|
+
references: [],
|
|
114
|
+
};
|
|
115
|
+
for (const field of fields) {
|
|
116
|
+
const name = `${resource}.${field}`;
|
|
117
|
+
if (!entry.references.includes(name)) {
|
|
118
|
+
entry.references.push(name);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
found.set(key, entry);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return [...found.values()].map(({ writer, target, own, references }) => {
|
|
127
|
+
const one = references.length === 1;
|
|
128
|
+
const what = `${writer} may write ${listed(references)}, ${one ? "a reference" : "references"} to Resource "${target}"`;
|
|
129
|
+
const hold = "Aventara holds a reference a write sends to the rows the caller may read, so these writes are refused (A4002).";
|
|
130
|
+
const out = "the reference out of the write (`write: { except: [...] }`).";
|
|
131
|
+
if (!own) {
|
|
132
|
+
return `${what}, which the rules' schema does not have: no read grant in these rules reaches it, and none can. ${hold} Leave ${out}`;
|
|
133
|
+
}
|
|
134
|
+
const to = writer === "signedIn" || writer === "anyone"
|
|
135
|
+
? writer
|
|
136
|
+
: `${writer} (or to signedIn)`;
|
|
137
|
+
return `${what}, but no read grant on "${target}" reaches ${writer}. ${hold} Grant read on "${target}" to ${to}, or leave ${out}`;
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
/**
|
|
141
|
+
* Emits `policies`' warnings under a framework that holds references to the
|
|
142
|
+
* Resources in `held`, each once per process and namespace.
|
|
143
|
+
*/
|
|
144
|
+
export const warnReferenceReads = (policies, held) => {
|
|
145
|
+
for (const message of referenceReadWarnings(policies, held)) {
|
|
146
|
+
emitAccessWarning(WARNING_CODE, policies.namespace, message);
|
|
147
|
+
}
|
|
148
|
+
};
|