@cynodia/axiom-core 0.15.0-alpha.2 → 0.15.0-alpha.3
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/dist/authorization.d.ts +63 -12
- package/dist/authorization.js +52 -14
- package/dist/semantic-identity.d.ts +9 -6
- package/dist/server-ir.d.ts +12 -0
- package/dist/server-ir.js +15 -0
- package/package.json +1 -1
package/dist/authorization.d.ts
CHANGED
|
@@ -21,6 +21,17 @@
|
|
|
21
21
|
* equivalent to an inline `allow` over `PRINCIPAL`; `ReadPolicyDef` (spec10) remains the
|
|
22
22
|
* row-level read mechanism, unified into query authorization by spec15 Phase D.
|
|
23
23
|
*
|
|
24
|
+
* **spec15pt3 — one security-absence-aware evaluator for *every* authorization expression.**
|
|
25
|
+
* `AuthorizationPolicyDef.allow` and the legacy `ActionDef.authorization` expression share
|
|
26
|
+
* {@link evaluateAuthorizationExpression}: a `field(ref(PRINCIPAL), …)` / `field(ref(RESOURCE),
|
|
27
|
+
* …)` read of a key the scope object does not carry is **security-scope absence**, not an
|
|
28
|
+
* ordinary `undefined`, and `neq` / `not` / `or` / any comparison whose truth would depend on
|
|
29
|
+
* that absence is *not satisfied* — so a missing principal attribute can never manufacture
|
|
30
|
+
* authority through either surface (spec15pt3 §5, §7, §32). The two modes differ only in
|
|
31
|
+
* their *final interpretation* (a policy allows on exactly `true`; legacy keeps its historical
|
|
32
|
+
* truthiness), which `decideAuthorization` applies downstream — never in how absence, boolean
|
|
33
|
+
* composition or errors propagate.
|
|
34
|
+
*
|
|
24
35
|
* This module is portable plain data + `Expression` trees; it carries no enforcement (that
|
|
25
36
|
* is Phases C–F). The accessors are **total over any input** so a hand-tampered policy fails
|
|
26
37
|
* closed with a structured diagnostic, never a native exception (spec15 §37).
|
|
@@ -122,6 +133,10 @@ export interface AuthorizationCheckInput {
|
|
|
122
133
|
/**
|
|
123
134
|
* The legacy `ActionDef.authorization` expression outcome, when that expression is present.
|
|
124
135
|
* Its historical truthiness rule is kept: a non-empty array or any truthy value allows.
|
|
136
|
+
* spec15pt3 — this part MUST be produced by {@link evaluateAuthorizationExpression} (mode
|
|
137
|
+
* `'legacy-action'`), so `{ ok: true, value: false }` for a decision that depended on a
|
|
138
|
+
* missing security-scope field: absence reaches this combiner as a plain DENY, never as a
|
|
139
|
+
* truthy value.
|
|
125
140
|
*/
|
|
126
141
|
legacy?: AuthorizationCheckPart;
|
|
127
142
|
}
|
|
@@ -152,21 +167,57 @@ export interface AuthorizationPolicyScope {
|
|
|
152
167
|
operation: string;
|
|
153
168
|
}
|
|
154
169
|
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* `decideAuthorization`
|
|
170
|
+
* Which authorization surface an expression is evaluated for (spec15pt3 §32). Both modes
|
|
171
|
+
* share {@link evaluateAuthorizationExpression}'s security-absence, boolean-composition and
|
|
172
|
+
* error propagation; they differ only downstream, in `decideAuthorization` — a `'policy'`
|
|
173
|
+
* result allows on exactly `true`, a `'legacy-action'` result keeps the historical
|
|
174
|
+
* `ActionDef.authorization` truthiness rule.
|
|
175
|
+
*/
|
|
176
|
+
export type AuthorizationEvaluationMode = 'policy' | 'legacy-action';
|
|
177
|
+
/**
|
|
178
|
+
* The evaluation context for {@link evaluateAuthorizationExpression}. `'policy'` mode uses the
|
|
179
|
+
* closed `{ principal, resource, operation }` scope only. `'legacy-action'` mode additionally
|
|
180
|
+
* honours the historical `ActionDef.authorization` scope, which could also read an ordinary
|
|
181
|
+
* `StateDef`: {@link resolveExternalRef} resolves such a `ref` through the caller's ordinary
|
|
182
|
+
* evaluator, and an id it cannot resolve fails **closed** (never silently allows), exactly as
|
|
183
|
+
* a throwing `runtime.evaluate` did pre-pt3.
|
|
184
|
+
*/
|
|
185
|
+
export interface AuthorizationExpressionContext {
|
|
186
|
+
principal: Record<string, unknown> | null | undefined;
|
|
187
|
+
resource?: Record<string, unknown> | null | undefined;
|
|
188
|
+
operation?: string;
|
|
189
|
+
resolveExternalRef?: (refId: string) => {
|
|
190
|
+
found: boolean;
|
|
191
|
+
value?: unknown;
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* spec15pt3 §32 — the **one** canonical security-absence-aware authorization-expression
|
|
196
|
+
* evaluator, shared by `AuthorizationPolicyDef.allow` (`mode: 'policy'`) and the legacy
|
|
197
|
+
* `ActionDef.authorization` expression (`mode: 'legacy-action'`). Returns the `{ ok, value }`
|
|
198
|
+
* shape `decideAuthorization` consumes:
|
|
158
199
|
*
|
|
159
|
-
* -
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* -
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* - an evaluation error (malformed node reaching the runtime, unsupported builtin
|
|
166
|
-
* `{ ok: false }
|
|
200
|
+
* - the expression reduces to a concrete value ⇒ `{ ok: true, value }` — for `'policy'` only
|
|
201
|
+
* an exact `true` is ALLOW; for `'legacy-action'` the historical truthiness rule (a truthy
|
|
202
|
+
* value / non-empty array) applies, both in `decideAuthorization`;
|
|
203
|
+
* - its truth depends on a **missing PRINCIPAL / RESOURCE field** ⇒ `{ ok: true, value: false }`
|
|
204
|
+
* — absence never creates authority through `eq` / `neq` / `not` / `lt` / `contains` /
|
|
205
|
+
* `or` / any composition (spec15pt2 §8-§12, spec15pt3 §11-§16, §79-§82);
|
|
206
|
+
* - an evaluation error (malformed node reaching the runtime, unsupported builtin, an
|
|
207
|
+
* unresolvable legacy `ref`) ⇒ `{ ok: false }` — fail closed, and the error keeps its
|
|
208
|
+
* provenance through `not` / `or` so it cannot be negated back to ALLOW (spec15pt3 §84).
|
|
167
209
|
*
|
|
168
210
|
* A constant `literal(true)` is `{ ok: true, value: true }` regardless of principal — an
|
|
169
|
-
* explicitly public
|
|
211
|
+
* explicitly public rule still admits an anonymous caller; the invariant is "missing
|
|
212
|
+
* referenced security fields cannot create authority", not "anonymous is forbidden"
|
|
213
|
+
* (spec15pt2 §13, §14; spec15pt3 §17). A `literal` nullish value stays a concrete value,
|
|
214
|
+
* never security absence (spec15pt3 §58).
|
|
215
|
+
*/
|
|
216
|
+
export declare function evaluateAuthorizationExpression(expression: unknown, context: AuthorizationExpressionContext, mode: AuthorizationEvaluationMode): AuthorizationCheckPart;
|
|
217
|
+
/**
|
|
218
|
+
* spec15pt2 F1 — the three-valued `AuthorizationPolicyDef.allow` evaluator. A thin, stable
|
|
219
|
+
* wrapper over {@link evaluateAuthorizationExpression} in `'policy'` mode, kept for every
|
|
220
|
+
* existing policy-bearing call site (spec15pt2 §33).
|
|
170
221
|
*/
|
|
171
222
|
export declare function evaluateAuthorizationPolicyAllow(allow: unknown, scope: AuthorizationPolicyScope): AuthorizationCheckPart;
|
|
172
223
|
/**
|
package/dist/authorization.js
CHANGED
|
@@ -21,6 +21,17 @@
|
|
|
21
21
|
* equivalent to an inline `allow` over `PRINCIPAL`; `ReadPolicyDef` (spec10) remains the
|
|
22
22
|
* row-level read mechanism, unified into query authorization by spec15 Phase D.
|
|
23
23
|
*
|
|
24
|
+
* **spec15pt3 — one security-absence-aware evaluator for *every* authorization expression.**
|
|
25
|
+
* `AuthorizationPolicyDef.allow` and the legacy `ActionDef.authorization` expression share
|
|
26
|
+
* {@link evaluateAuthorizationExpression}: a `field(ref(PRINCIPAL), …)` / `field(ref(RESOURCE),
|
|
27
|
+
* …)` read of a key the scope object does not carry is **security-scope absence**, not an
|
|
28
|
+
* ordinary `undefined`, and `neq` / `not` / `or` / any comparison whose truth would depend on
|
|
29
|
+
* that absence is *not satisfied* — so a missing principal attribute can never manufacture
|
|
30
|
+
* authority through either surface (spec15pt3 §5, §7, §32). The two modes differ only in
|
|
31
|
+
* their *final interpretation* (a policy allows on exactly `true`; legacy keeps its historical
|
|
32
|
+
* truthiness), which `decideAuthorization` applies downstream — never in how absence, boolean
|
|
33
|
+
* composition or errors propagate.
|
|
34
|
+
*
|
|
24
35
|
* This module is portable plain data + `Expression` trees; it carries no enforcement (that
|
|
25
36
|
* is Phases C–F). The accessors are **total over any input** so a hand-tampered policy fails
|
|
26
37
|
* closed with a structured diagnostic, never a native exception (spec15 §37).
|
|
@@ -377,6 +388,14 @@ function evalAuthz(expression, scope, seen) {
|
|
|
377
388
|
return C(scope.resource ?? null);
|
|
378
389
|
if (t === AUTHZ_OPERATION_SCOPE)
|
|
379
390
|
return C(scope.operation);
|
|
391
|
+
// spec15pt3 — a legacy `ActionDef.authorization` expression's historical scope also
|
|
392
|
+
// reaches ordinary `StateDef` refs. Resolve them through the caller's ordinary
|
|
393
|
+
// evaluator; an unresolved ref fails closed (`E` ⇒ DENY), never silently allows. A
|
|
394
|
+
// policy expression's scope is closed, so this branch is unreachable in `'policy'` mode.
|
|
395
|
+
if (scope.mode === 'legacy-action' && scope.resolveExternalRef) {
|
|
396
|
+
const resolved = scope.resolveExternalRef(t);
|
|
397
|
+
return resolved.found ? C(resolved.value) : E;
|
|
398
|
+
}
|
|
380
399
|
return E; // out of the closed scope — validation rejects it; be total
|
|
381
400
|
}
|
|
382
401
|
case 'field': {
|
|
@@ -541,26 +560,37 @@ function applyPolicyBuiltin(fn, args) {
|
|
|
541
560
|
}
|
|
542
561
|
}
|
|
543
562
|
/**
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
* `
|
|
563
|
+
* spec15pt3 §32 — the **one** canonical security-absence-aware authorization-expression
|
|
564
|
+
* evaluator, shared by `AuthorizationPolicyDef.allow` (`mode: 'policy'`) and the legacy
|
|
565
|
+
* `ActionDef.authorization` expression (`mode: 'legacy-action'`). Returns the `{ ok, value }`
|
|
566
|
+
* shape `decideAuthorization` consumes:
|
|
547
567
|
*
|
|
548
|
-
* -
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
* -
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
* - an evaluation error (malformed node reaching the runtime, unsupported builtin
|
|
555
|
-
* `{ ok: false }
|
|
568
|
+
* - the expression reduces to a concrete value ⇒ `{ ok: true, value }` — for `'policy'` only
|
|
569
|
+
* an exact `true` is ALLOW; for `'legacy-action'` the historical truthiness rule (a truthy
|
|
570
|
+
* value / non-empty array) applies, both in `decideAuthorization`;
|
|
571
|
+
* - its truth depends on a **missing PRINCIPAL / RESOURCE field** ⇒ `{ ok: true, value: false }`
|
|
572
|
+
* — absence never creates authority through `eq` / `neq` / `not` / `lt` / `contains` /
|
|
573
|
+
* `or` / any composition (spec15pt2 §8-§12, spec15pt3 §11-§16, §79-§82);
|
|
574
|
+
* - an evaluation error (malformed node reaching the runtime, unsupported builtin, an
|
|
575
|
+
* unresolvable legacy `ref`) ⇒ `{ ok: false }` — fail closed, and the error keeps its
|
|
576
|
+
* provenance through `not` / `or` so it cannot be negated back to ALLOW (spec15pt3 §84).
|
|
556
577
|
*
|
|
557
578
|
* A constant `literal(true)` is `{ ok: true, value: true }` regardless of principal — an
|
|
558
|
-
* explicitly public
|
|
579
|
+
* explicitly public rule still admits an anonymous caller; the invariant is "missing
|
|
580
|
+
* referenced security fields cannot create authority", not "anonymous is forbidden"
|
|
581
|
+
* (spec15pt2 §13, §14; spec15pt3 §17). A `literal` nullish value stays a concrete value,
|
|
582
|
+
* never security absence (spec15pt3 §58).
|
|
559
583
|
*/
|
|
560
|
-
export function
|
|
584
|
+
export function evaluateAuthorizationExpression(expression, context, mode) {
|
|
561
585
|
let result;
|
|
562
586
|
try {
|
|
563
|
-
result = evalAuthz(
|
|
587
|
+
result = evalAuthz(expression, {
|
|
588
|
+
principal: context.principal ?? null,
|
|
589
|
+
resource: context.resource ?? null,
|
|
590
|
+
operation: context.operation ?? 'action.invoke',
|
|
591
|
+
mode,
|
|
592
|
+
...(context.resolveExternalRef ? { resolveExternalRef: context.resolveExternalRef } : {}),
|
|
593
|
+
}, new Set());
|
|
564
594
|
}
|
|
565
595
|
catch {
|
|
566
596
|
return { ok: false };
|
|
@@ -571,6 +601,14 @@ export function evaluateAuthorizationPolicyAllow(allow, scope) {
|
|
|
571
601
|
return { ok: true, value: false };
|
|
572
602
|
return { ok: true, value: result.v };
|
|
573
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* spec15pt2 F1 — the three-valued `AuthorizationPolicyDef.allow` evaluator. A thin, stable
|
|
606
|
+
* wrapper over {@link evaluateAuthorizationExpression} in `'policy'` mode, kept for every
|
|
607
|
+
* existing policy-bearing call site (spec15pt2 §33).
|
|
608
|
+
*/
|
|
609
|
+
export function evaluateAuthorizationPolicyAllow(allow, scope) {
|
|
610
|
+
return evaluateAuthorizationExpression(allow, { principal: scope.principal, resource: scope.resource, operation: scope.operation }, 'policy');
|
|
611
|
+
}
|
|
574
612
|
/**
|
|
575
613
|
* The distinct policy ids a graph node references for authorization — for dependency
|
|
576
614
|
* analysis and `validateGraph` reference resolution. Total over malformed input.
|
|
@@ -101,12 +101,15 @@ export interface AuthorityCompatibilityKey {
|
|
|
101
101
|
serverContract: string;
|
|
102
102
|
semanticFingerprint: string;
|
|
103
103
|
/**
|
|
104
|
-
* spec15pt2 §35 — the runtime authorization-evaluator semantics
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
104
|
+
* spec15pt2 §35, spec15pt3 §37-§39 — the runtime authorization-evaluator semantics
|
|
105
|
+
* version. Successive builds evaluate the *same* Server IR authorization expression
|
|
106
|
+
* differently — `alpha.1` → `alpha.2` for `AuthorizationPolicyDef.allow` (absent-value
|
|
107
|
+
* safety, F1), `alpha.2` → `alpha.3` for the legacy `ActionDef.authorization` expression
|
|
108
|
+
* (F1-legacy) — yet the graph, and therefore `semanticFingerprint`, is identical. This
|
|
109
|
+
* discriminator, present whenever the IR carries an authorization *decision* (a policy
|
|
110
|
+
* reference or a legacy `authorization` expression), keeps builds that disagree from
|
|
111
|
+
* silently co-participating in one authority domain. Absent on a graph with no
|
|
112
|
+
* authorization decision (its evaluation is unchanged across every build).
|
|
110
113
|
*/
|
|
111
114
|
authorizationRuntime?: string;
|
|
112
115
|
}
|
package/dist/server-ir.d.ts
CHANGED
|
@@ -141,6 +141,18 @@ export declare function usesAuthorizationVocabulary(ir: {
|
|
|
141
141
|
instanceAccessPolicy?: unknown;
|
|
142
142
|
}[];
|
|
143
143
|
}): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Whether a document carries any legacy `ActionDef.authorization` expression (spec15pt3
|
|
146
|
+
* §39). This is not 0.15 *vocabulary* — it predates the milestone — but its runtime meaning
|
|
147
|
+
* changed in `0.15.0-alpha.3` (security-absence-aware evaluation, F1-legacy), so a mixed
|
|
148
|
+
* `alpha.2` / `alpha.3` cluster over a graph that can exercise it must fail closed. Total
|
|
149
|
+
* over a tampered IR.
|
|
150
|
+
*/
|
|
151
|
+
export declare function usesLegacyActionAuthorization(ir: {
|
|
152
|
+
actions?: Record<string, {
|
|
153
|
+
authorization?: unknown;
|
|
154
|
+
}>;
|
|
155
|
+
}): boolean;
|
|
144
156
|
/**
|
|
145
157
|
* Authorization vocabulary whose *enforcement* has not shipped yet — the admission gate a
|
|
146
158
|
* build uses to fail closed rather than run a declared policy as a silent no-op (spec4 §4,
|
package/dist/server-ir.js
CHANGED
|
@@ -177,6 +177,21 @@ export function usesAuthorizationVocabulary(ir) {
|
|
|
177
177
|
}
|
|
178
178
|
return false;
|
|
179
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Whether a document carries any legacy `ActionDef.authorization` expression (spec15pt3
|
|
182
|
+
* §39). This is not 0.15 *vocabulary* — it predates the milestone — but its runtime meaning
|
|
183
|
+
* changed in `0.15.0-alpha.3` (security-absence-aware evaluation, F1-legacy), so a mixed
|
|
184
|
+
* `alpha.2` / `alpha.3` cluster over a graph that can exercise it must fail closed. Total
|
|
185
|
+
* over a tampered IR.
|
|
186
|
+
*/
|
|
187
|
+
export function usesLegacyActionAuthorization(ir) {
|
|
188
|
+
for (const action of Object.values(ir.actions ?? {})) {
|
|
189
|
+
if (action && typeof action === 'object' && action.authorization !== undefined) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
180
195
|
/**
|
|
181
196
|
* Authorization vocabulary whose *enforcement* has not shipped yet — the admission gate a
|
|
182
197
|
* build uses to fail closed rather than run a declared policy as a silent no-op (spec4 §4,
|