@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,574 @@
|
|
|
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
|
+
|
|
8
|
+
import {
|
|
9
|
+
type AccessWhere,
|
|
10
|
+
mergeWhere,
|
|
11
|
+
translateWhere,
|
|
12
|
+
} from "./aventara-where.translator.js";
|
|
13
|
+
import type {
|
|
14
|
+
AccessComparison,
|
|
15
|
+
AccessLiteral,
|
|
16
|
+
AccessPolicies,
|
|
17
|
+
} from "./policies-contract.js";
|
|
18
|
+
import {
|
|
19
|
+
allOf,
|
|
20
|
+
andOf,
|
|
21
|
+
FALSE,
|
|
22
|
+
notOf,
|
|
23
|
+
nullSafe,
|
|
24
|
+
orOf,
|
|
25
|
+
ownOf,
|
|
26
|
+
type Resolved,
|
|
27
|
+
TRUE,
|
|
28
|
+
} from "./scope.resolver.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A row as core hands it to `can`: its scalar fields. A relation member on it is
|
|
32
|
+
* ignored; never put relation data a client sent on it.
|
|
33
|
+
*/
|
|
34
|
+
export type AccessRow = { readonly [field: string]: unknown };
|
|
35
|
+
|
|
36
|
+
/** Core's `exists`: whether a row of `resource` meets `where`, unscoped. */
|
|
37
|
+
export type AccessExists = (
|
|
38
|
+
resource: string,
|
|
39
|
+
where: AccessWhere,
|
|
40
|
+
) => Promise<boolean>;
|
|
41
|
+
|
|
42
|
+
/** One `exists` question the engine asked, and its answer. */
|
|
43
|
+
export type AccessLookup = {
|
|
44
|
+
readonly resource: string;
|
|
45
|
+
readonly where: AccessWhere;
|
|
46
|
+
readonly answer: boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** A three-valued answer: decided, or not decidable from what the engine has. */
|
|
50
|
+
export type AccessTruth = boolean | "unknown";
|
|
51
|
+
|
|
52
|
+
/** Whether the row is about to be created, or already exists (an after check). */
|
|
53
|
+
export type AccessRowKind = "new" | "existing";
|
|
54
|
+
|
|
55
|
+
/** A field's type when `can` compares it by more than its JavaScript value (`types`). */
|
|
56
|
+
type ComparedType = "bigint" | "datetime" | "decimal" | undefined;
|
|
57
|
+
|
|
58
|
+
/** The type `can` compares `field` of `resource` by. */
|
|
59
|
+
const typeOf = (
|
|
60
|
+
policies: AccessPolicies,
|
|
61
|
+
resource: string,
|
|
62
|
+
field: string,
|
|
63
|
+
): ComparedType => ownOf(ownOf(policies.schema, resource)?.types, field);
|
|
64
|
+
|
|
65
|
+
const DECIMAL = /^([+-]?)(\d+)(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A decimal's value as `{ sign, digits, point }`: `0.<digits> × 10^point`, with no
|
|
69
|
+
* leading or trailing zero in `digits` (zero is `{ sign: "", digits: "", point: 0 }`).
|
|
70
|
+
* `undefined` for text that is no decimal.
|
|
71
|
+
*/
|
|
72
|
+
export const decimalParts = (
|
|
73
|
+
text: string,
|
|
74
|
+
):
|
|
75
|
+
| { readonly sign: "" | "-"; readonly digits: string; readonly point: number }
|
|
76
|
+
| undefined => {
|
|
77
|
+
const match = DECIMAL.exec(text);
|
|
78
|
+
if (match === null) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
// Indexed, not destructured: destructuring a match costs the type checker its
|
|
82
|
+
// iterator types (about 1,000 instantiations, the type budget's whole margin).
|
|
83
|
+
const sign = match[1] ?? "";
|
|
84
|
+
const whole = match[2] ?? "";
|
|
85
|
+
const fraction = match[3] ?? "";
|
|
86
|
+
const exponent = match[4] ?? "0";
|
|
87
|
+
const shift = Number(exponent);
|
|
88
|
+
if (!Number.isSafeInteger(shift)) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
let digits = whole + fraction;
|
|
92
|
+
let point = whole.length + shift;
|
|
93
|
+
const leading = digits.length - digits.replace(/^0+/, "").length;
|
|
94
|
+
digits = digits.slice(leading).replace(/0+$/, "");
|
|
95
|
+
point -= leading;
|
|
96
|
+
return digits === ""
|
|
97
|
+
? { sign: "", digits: "", point: 0 }
|
|
98
|
+
: { sign: sign === "-" ? "-" : "", digits, point };
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* An ISO-8601 date, or date and time with its offset (never a local time), to the
|
|
103
|
+
* millisecond at most.
|
|
104
|
+
*/
|
|
105
|
+
const INSTANT =
|
|
106
|
+
/^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?(Z|([+-])(\d{2}):(\d{2})))?$/;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The epoch milliseconds of an ISO-8601 string that reads back as written: no
|
|
110
|
+
* February 30th, no hour 24, no offset past 23:59, nothing past the millisecond.
|
|
111
|
+
*/
|
|
112
|
+
const instantOfText = (text: string): number | undefined => {
|
|
113
|
+
const match = INSTANT.exec(text);
|
|
114
|
+
if (match === null) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
const part = (index: number): number => Number(match[index] ?? "0");
|
|
118
|
+
const year = part(1);
|
|
119
|
+
const month = part(2);
|
|
120
|
+
const day = part(3);
|
|
121
|
+
const hour = part(4);
|
|
122
|
+
const minute = part(5);
|
|
123
|
+
const second = part(6);
|
|
124
|
+
const millisecond = Number((match[7] ?? "").padEnd(3, "0"));
|
|
125
|
+
const local = Date.UTC(
|
|
126
|
+
year,
|
|
127
|
+
month - 1,
|
|
128
|
+
day,
|
|
129
|
+
hour,
|
|
130
|
+
minute,
|
|
131
|
+
second,
|
|
132
|
+
millisecond,
|
|
133
|
+
);
|
|
134
|
+
const read = new Date(local);
|
|
135
|
+
if (
|
|
136
|
+
read.getUTCFullYear() !== year ||
|
|
137
|
+
read.getUTCMonth() !== month - 1 ||
|
|
138
|
+
read.getUTCDate() !== day ||
|
|
139
|
+
read.getUTCHours() !== hour ||
|
|
140
|
+
read.getUTCMinutes() !== minute ||
|
|
141
|
+
read.getUTCSeconds() !== second ||
|
|
142
|
+
part(10) > 23 ||
|
|
143
|
+
part(11) > 59
|
|
144
|
+
) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const offset = (part(10) * 60 + part(11)) * 60_000;
|
|
148
|
+
return match[9] === "-" ? local + offset : local - offset;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/** An instant's epoch milliseconds, from a valid `Date` or an ISO-8601 string that reads back as written. */
|
|
152
|
+
export const instantOf = (value: unknown): number | undefined => {
|
|
153
|
+
if (value instanceof Date) {
|
|
154
|
+
const time = value.getTime();
|
|
155
|
+
return Number.isNaN(time) ? undefined : time;
|
|
156
|
+
}
|
|
157
|
+
return typeof value === "string" ? instantOfText(value) : undefined;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** A decimal object's text: core's `AvDecimal`, an ORM's decimal, anything whose `toString()` is decimal digits. */
|
|
161
|
+
const decimalText = (value: unknown): string | undefined =>
|
|
162
|
+
typeof value === "string"
|
|
163
|
+
? value
|
|
164
|
+
: typeof value === "number"
|
|
165
|
+
? Number.isFinite(value)
|
|
166
|
+
? String(value)
|
|
167
|
+
: undefined
|
|
168
|
+
: typeof value === "object" &&
|
|
169
|
+
value !== null &&
|
|
170
|
+
!(value instanceof Date) &&
|
|
171
|
+
!Array.isArray(value)
|
|
172
|
+
? String(value)
|
|
173
|
+
: undefined;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The key a value is compared by, for a field of `type`: equal keys, equal values.
|
|
177
|
+
* `undefined` when the value is not one of the type's (a type mismatch).
|
|
178
|
+
*/
|
|
179
|
+
const comparisonKey = (value: unknown, type: ComparedType): unknown => {
|
|
180
|
+
switch (type) {
|
|
181
|
+
case "decimal": {
|
|
182
|
+
const text = decimalText(value);
|
|
183
|
+
const parts = text === undefined ? undefined : decimalParts(text);
|
|
184
|
+
return parts === undefined
|
|
185
|
+
? undefined
|
|
186
|
+
: `${parts.sign}${parts.digits}@${parts.point}`;
|
|
187
|
+
}
|
|
188
|
+
case "datetime":
|
|
189
|
+
return instantOf(value);
|
|
190
|
+
case "bigint":
|
|
191
|
+
return typeof value === "bigint"
|
|
192
|
+
? value
|
|
193
|
+
: typeof value === "number" && Number.isSafeInteger(value)
|
|
194
|
+
? BigInt(value)
|
|
195
|
+
: typeof value === "string" && /^-?\d+$/.test(value)
|
|
196
|
+
? BigInt(value)
|
|
197
|
+
: undefined;
|
|
198
|
+
default:
|
|
199
|
+
return typeof value === "string" ||
|
|
200
|
+
typeof value === "boolean" ||
|
|
201
|
+
(typeof value === "number" && Number.isFinite(value))
|
|
202
|
+
? value
|
|
203
|
+
: undefined;
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* One comparison of a row's value with a rule's, three-valued: a value of another
|
|
209
|
+
* type than the field's, or than the rule's, is `"unknown"`.
|
|
210
|
+
*/
|
|
211
|
+
const compare = (
|
|
212
|
+
rowValue: unknown,
|
|
213
|
+
cmp: AccessComparison,
|
|
214
|
+
value: AccessLiteral | readonly AccessLiteral[],
|
|
215
|
+
type: ComparedType,
|
|
216
|
+
): AccessTruth => {
|
|
217
|
+
if (rowValue === null) {
|
|
218
|
+
// A comparison on NULL is false, except `equals: null` (IS NULL); a list never
|
|
219
|
+
// holds it for `can`.
|
|
220
|
+
return cmp === "eq" && value === null;
|
|
221
|
+
}
|
|
222
|
+
const actual = comparisonKey(rowValue, type);
|
|
223
|
+
const equal = (literal: AccessLiteral): AccessTruth => {
|
|
224
|
+
const expected = comparisonKey(literal, type);
|
|
225
|
+
return actual === undefined ||
|
|
226
|
+
expected === undefined ||
|
|
227
|
+
typeof actual !== typeof expected
|
|
228
|
+
? "unknown"
|
|
229
|
+
: actual === expected;
|
|
230
|
+
};
|
|
231
|
+
if (cmp === "eq" || cmp === "ne") {
|
|
232
|
+
if (value === null) {
|
|
233
|
+
return cmp === "ne";
|
|
234
|
+
}
|
|
235
|
+
const truth = equal(value as AccessLiteral);
|
|
236
|
+
return truth === "unknown" ? truth : cmp === "eq" ? truth : !truth;
|
|
237
|
+
}
|
|
238
|
+
let unknown = false;
|
|
239
|
+
for (const literal of value as readonly AccessLiteral[]) {
|
|
240
|
+
if (literal === null) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const truth = equal(literal);
|
|
244
|
+
if (truth === true) {
|
|
245
|
+
return cmp === "in";
|
|
246
|
+
}
|
|
247
|
+
unknown ||= truth === "unknown";
|
|
248
|
+
}
|
|
249
|
+
return unknown ? "unknown" : cmp === "notIn";
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/** A condition a row decided as neither true nor false: a value of the wrong type. */
|
|
253
|
+
export type Unknown = { readonly k: "unknown" };
|
|
254
|
+
|
|
255
|
+
const UNKNOWN: Unknown = { k: "unknown" };
|
|
256
|
+
|
|
257
|
+
/** What evaluating on a row gives: `TRUE`, `FALSE`, `UNKNOWN`, or the residual. */
|
|
258
|
+
export type Evaluated = Resolved | Unknown;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* A conjunction, three-valued. A part that is unknown makes the whole unknown unless
|
|
262
|
+
* another part is false: an unknown never waits on an `exists` question, and the
|
|
263
|
+
* caller fails closed on it.
|
|
264
|
+
*/
|
|
265
|
+
const and3 = (k: "and" | "all", args: readonly Evaluated[]): Evaluated =>
|
|
266
|
+
args.some((arg) => arg.k === "false")
|
|
267
|
+
? FALSE
|
|
268
|
+
: args.some((arg) => arg.k === "unknown")
|
|
269
|
+
? UNKNOWN
|
|
270
|
+
: (k === "and" ? andOf : allOf)(args as readonly Resolved[]);
|
|
271
|
+
|
|
272
|
+
const or3 = (args: readonly Evaluated[]): Evaluated =>
|
|
273
|
+
args.some((arg) => arg.k === "true")
|
|
274
|
+
? TRUE
|
|
275
|
+
: args.some((arg) => arg.k === "unknown")
|
|
276
|
+
? UNKNOWN
|
|
277
|
+
: orOf(args as readonly Resolved[]);
|
|
278
|
+
|
|
279
|
+
/** A row's own field: an inherited one is not on the row. */
|
|
280
|
+
const fieldOf = (row: AccessRow, field: string): unknown =>
|
|
281
|
+
Object.hasOwn(row, field) ? row[field] : undefined;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Evaluates what the row carries. The answer is `TRUE`, `FALSE`, `UNKNOWN` (a value
|
|
285
|
+
* of another type than its field's), or the residual condition: the part the row
|
|
286
|
+
* could not decide, with the decided parts folded away. A relation member on the row
|
|
287
|
+
* is ignored: every relation step stays whole in the residual, for `exists`. Each comparison is made by its
|
|
288
|
+
* field's type (`types`): a decimal by its digits, a datetime by its instant, a
|
|
289
|
+
* bigint exactly.
|
|
290
|
+
*/
|
|
291
|
+
export const evaluateOnRow = (
|
|
292
|
+
policies: AccessPolicies,
|
|
293
|
+
resource: string,
|
|
294
|
+
node: Resolved,
|
|
295
|
+
row: AccessRow,
|
|
296
|
+
): Evaluated => {
|
|
297
|
+
switch (node.k) {
|
|
298
|
+
case "true":
|
|
299
|
+
case "false":
|
|
300
|
+
return node;
|
|
301
|
+
case "and":
|
|
302
|
+
case "all":
|
|
303
|
+
return and3(
|
|
304
|
+
node.k,
|
|
305
|
+
node.args.map((arg) => evaluateOnRow(policies, resource, arg, row)),
|
|
306
|
+
);
|
|
307
|
+
case "or":
|
|
308
|
+
return or3(
|
|
309
|
+
node.args.map((arg) => evaluateOnRow(policies, resource, arg, row)),
|
|
310
|
+
);
|
|
311
|
+
case "not": {
|
|
312
|
+
const arg = evaluateOnRow(policies, resource, node.arg, row);
|
|
313
|
+
return arg.k === "unknown" ? arg : notOf(arg);
|
|
314
|
+
}
|
|
315
|
+
case "cmp": {
|
|
316
|
+
const value = fieldOf(row, node.field);
|
|
317
|
+
if (value === undefined) {
|
|
318
|
+
return node;
|
|
319
|
+
}
|
|
320
|
+
const truth = compare(
|
|
321
|
+
value,
|
|
322
|
+
node.cmp,
|
|
323
|
+
node.value,
|
|
324
|
+
typeOf(policies, resource, node.field),
|
|
325
|
+
);
|
|
326
|
+
return truth === "unknown" ? UNKNOWN : truth ? TRUE : FALSE;
|
|
327
|
+
}
|
|
328
|
+
case "rel":
|
|
329
|
+
case "some":
|
|
330
|
+
case "every":
|
|
331
|
+
case "none":
|
|
332
|
+
// A relation is never read off the row: whoever built the row could have put
|
|
333
|
+
// anything there. It is decided through the row's identifier or references
|
|
334
|
+
// and `exists`, on what the database holds.
|
|
335
|
+
return node;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
type AskContext = {
|
|
340
|
+
readonly policies: AccessPolicies;
|
|
341
|
+
readonly row: AccessRow;
|
|
342
|
+
readonly exists: AccessExists | undefined;
|
|
343
|
+
readonly lookups: AccessLookup[];
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const ask = async (
|
|
347
|
+
context: AskContext,
|
|
348
|
+
resource: string,
|
|
349
|
+
where: AccessWhere,
|
|
350
|
+
): Promise<AccessTruth> => {
|
|
351
|
+
if (context.exists === undefined) {
|
|
352
|
+
return "unknown";
|
|
353
|
+
}
|
|
354
|
+
const answer = (await context.exists(resource, where)) === true;
|
|
355
|
+
context.lookups.push({ resource, where, answer });
|
|
356
|
+
return answer;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/** An `exists` question's `where`: the row's own keys and the residual, null-safe. */
|
|
360
|
+
const combine = (
|
|
361
|
+
policies: AccessPolicies,
|
|
362
|
+
resource: string,
|
|
363
|
+
where: AccessWhere,
|
|
364
|
+
residual: Resolved,
|
|
365
|
+
): AccessWhere => {
|
|
366
|
+
const condition = translateWhere(nullSafe(policies, resource, residual));
|
|
367
|
+
return mergeWhere([where, condition]) ?? { AND: [where, condition] };
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/** A row's value as an `exists` question's `where` states it: a `Date` as its ISO instant, a bigint as itself (exact). */
|
|
371
|
+
const whereValue = (value: unknown): unknown =>
|
|
372
|
+
value instanceof Date ? value.toISOString() : value;
|
|
373
|
+
|
|
374
|
+
/** Asks about an existing row, by its first identifier the row carries in full. */
|
|
375
|
+
const askByIdentifier = async (
|
|
376
|
+
context: AskContext,
|
|
377
|
+
resource: string,
|
|
378
|
+
residual: Resolved,
|
|
379
|
+
): Promise<AccessTruth> => {
|
|
380
|
+
const identifiers =
|
|
381
|
+
ownOf(context.policies.schema, resource)?.identifiers ?? [];
|
|
382
|
+
const identifier = identifiers.find((fields) =>
|
|
383
|
+
fields.every(
|
|
384
|
+
(field) =>
|
|
385
|
+
fieldOf(context.row, field) !== undefined &&
|
|
386
|
+
fieldOf(context.row, field) !== null,
|
|
387
|
+
),
|
|
388
|
+
);
|
|
389
|
+
if (identifier === undefined) {
|
|
390
|
+
return "unknown";
|
|
391
|
+
}
|
|
392
|
+
const where = Object.fromEntries(
|
|
393
|
+
identifier.map((field) => [
|
|
394
|
+
field,
|
|
395
|
+
{ equals: whereValue(fieldOf(context.row, field)) },
|
|
396
|
+
]),
|
|
397
|
+
);
|
|
398
|
+
return ask(
|
|
399
|
+
context,
|
|
400
|
+
resource,
|
|
401
|
+
combine(context.policies, resource, where, residual),
|
|
402
|
+
);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The reference values a to-one step's relation takes from the row: `undefined` when
|
|
407
|
+
* this end holds no references, or the row does not carry them all.
|
|
408
|
+
*/
|
|
409
|
+
const carriedReferences = (
|
|
410
|
+
context: AskContext,
|
|
411
|
+
resource: string,
|
|
412
|
+
node: Extract<Resolved, { k: "rel" }>,
|
|
413
|
+
):
|
|
414
|
+
| {
|
|
415
|
+
readonly references: readonly string[];
|
|
416
|
+
readonly values: readonly unknown[];
|
|
417
|
+
}
|
|
418
|
+
| undefined => {
|
|
419
|
+
const relation = ownOf(
|
|
420
|
+
ownOf(context.policies.schema, resource)?.relations,
|
|
421
|
+
node.relation,
|
|
422
|
+
);
|
|
423
|
+
if (
|
|
424
|
+
relation?.referenceFields === undefined ||
|
|
425
|
+
relation.references === undefined
|
|
426
|
+
) {
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
const values = relation.referenceFields.map((field) =>
|
|
430
|
+
fieldOf(context.row, field),
|
|
431
|
+
);
|
|
432
|
+
return values.some((value) => value === undefined)
|
|
433
|
+
? undefined
|
|
434
|
+
: { references: relation.references, values };
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
/** Whether a residual holds, outside any relation, a to-one step the row's references decide. */
|
|
438
|
+
const stepsByReference = (
|
|
439
|
+
context: AskContext,
|
|
440
|
+
resource: string,
|
|
441
|
+
node: Resolved,
|
|
442
|
+
): boolean => {
|
|
443
|
+
switch (node.k) {
|
|
444
|
+
case "and":
|
|
445
|
+
case "all":
|
|
446
|
+
case "or":
|
|
447
|
+
return node.args.some((arg) => stepsByReference(context, resource, arg));
|
|
448
|
+
case "not":
|
|
449
|
+
return stepsByReference(context, resource, node.arg);
|
|
450
|
+
case "rel":
|
|
451
|
+
return carriedReferences(context, resource, node) !== undefined;
|
|
452
|
+
default:
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Decides a residual through `exists`. A to-one step whose references the row carries
|
|
459
|
+
* is asked on the row they name, with the row's own values: for a created row, and for
|
|
460
|
+
* an updated one too, since a caller may ask before the write, when the database still
|
|
461
|
+
* holds the old reference. Whatever else an existing row leaves is asked about the row
|
|
462
|
+
* itself, by its identifier (in one question when no step goes by reference); for a
|
|
463
|
+
* created row it is unknown.
|
|
464
|
+
*/
|
|
465
|
+
const askOn = async (
|
|
466
|
+
context: AskContext,
|
|
467
|
+
resource: string,
|
|
468
|
+
node: Resolved,
|
|
469
|
+
kind: AccessRowKind,
|
|
470
|
+
): Promise<AccessTruth> => {
|
|
471
|
+
if (kind === "existing" && !stepsByReference(context, resource, node)) {
|
|
472
|
+
return node.k === "true"
|
|
473
|
+
? true
|
|
474
|
+
: node.k === "false"
|
|
475
|
+
? false
|
|
476
|
+
: askByIdentifier(context, resource, node);
|
|
477
|
+
}
|
|
478
|
+
switch (node.k) {
|
|
479
|
+
case "true":
|
|
480
|
+
return true;
|
|
481
|
+
case "false":
|
|
482
|
+
return false;
|
|
483
|
+
case "and":
|
|
484
|
+
case "all": {
|
|
485
|
+
let unknown = false;
|
|
486
|
+
for (const arg of node.args) {
|
|
487
|
+
const answer = await askOn(context, resource, arg, kind);
|
|
488
|
+
if (answer === false) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
unknown ||= answer === "unknown";
|
|
492
|
+
}
|
|
493
|
+
return unknown ? "unknown" : true;
|
|
494
|
+
}
|
|
495
|
+
case "or": {
|
|
496
|
+
let unknown = false;
|
|
497
|
+
for (const arg of node.args) {
|
|
498
|
+
const answer = await askOn(context, resource, arg, kind);
|
|
499
|
+
if (answer === true) {
|
|
500
|
+
return true;
|
|
501
|
+
}
|
|
502
|
+
unknown ||= answer === "unknown";
|
|
503
|
+
}
|
|
504
|
+
return unknown ? "unknown" : false;
|
|
505
|
+
}
|
|
506
|
+
case "not": {
|
|
507
|
+
const answer = await askOn(context, resource, node.arg, kind);
|
|
508
|
+
return answer === "unknown" ? "unknown" : !answer;
|
|
509
|
+
}
|
|
510
|
+
case "rel": {
|
|
511
|
+
const carried = carriedReferences(context, resource, node);
|
|
512
|
+
if (carried === undefined) {
|
|
513
|
+
return kind === "existing"
|
|
514
|
+
? askByIdentifier(context, resource, node)
|
|
515
|
+
: "unknown";
|
|
516
|
+
}
|
|
517
|
+
if (carried.values.some((value) => value === null)) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
const where = Object.fromEntries(
|
|
521
|
+
carried.references.map((field, index) => [
|
|
522
|
+
field,
|
|
523
|
+
{ equals: whereValue(carried.values[index]) },
|
|
524
|
+
]),
|
|
525
|
+
);
|
|
526
|
+
return ask(
|
|
527
|
+
context,
|
|
528
|
+
node.target,
|
|
529
|
+
combine(context.policies, node.target, where, node.where),
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
default:
|
|
533
|
+
return kind === "existing"
|
|
534
|
+
? askByIdentifier(context, resource, node)
|
|
535
|
+
: "unknown";
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Decides a folded condition on a row: in memory first, then through `exists` for
|
|
541
|
+
* what the row does not carry. `"unknown"` when neither can decide.
|
|
542
|
+
*/
|
|
543
|
+
export const decideOnRow = async (args: {
|
|
544
|
+
readonly policies: AccessPolicies;
|
|
545
|
+
readonly resource: string;
|
|
546
|
+
readonly condition: Resolved;
|
|
547
|
+
readonly row: AccessRow;
|
|
548
|
+
readonly kind: AccessRowKind;
|
|
549
|
+
readonly exists: AccessExists | undefined;
|
|
550
|
+
readonly lookups: AccessLookup[];
|
|
551
|
+
}): Promise<AccessTruth> => {
|
|
552
|
+
const residual = evaluateOnRow(
|
|
553
|
+
args.policies,
|
|
554
|
+
args.resource,
|
|
555
|
+
args.condition,
|
|
556
|
+
args.row,
|
|
557
|
+
);
|
|
558
|
+
if (residual.k === "true") {
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
if (residual.k === "false") {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
if (residual.k === "unknown") {
|
|
565
|
+
return "unknown";
|
|
566
|
+
}
|
|
567
|
+
const context: AskContext = {
|
|
568
|
+
policies: args.policies,
|
|
569
|
+
row: args.row,
|
|
570
|
+
exists: args.exists,
|
|
571
|
+
lookups: args.lookups,
|
|
572
|
+
};
|
|
573
|
+
return askOn(context, args.resource, residual, args.kind);
|
|
574
|
+
};
|