@cynodia/axiom-core 0.15.0-alpha.1 → 0.15.0-alpha.2
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 +28 -6
- package/dist/authorization.js +318 -0
- package/dist/semantic-identity.d.ts +9 -0
- package/dist/semantic-identity.js +5 -1
- package/package.json +1 -1
package/dist/authorization.d.ts
CHANGED
|
@@ -86,12 +86,6 @@ export interface AuthorizationPolicyProblem {
|
|
|
86
86
|
code: 'AUTHORIZATION_INVALID_POLICY' | 'AUTHORIZATION_INVALID_SCOPE' | 'AUTHORIZATION_NONDETERMINISTIC';
|
|
87
87
|
message: string;
|
|
88
88
|
}
|
|
89
|
-
/**
|
|
90
|
-
* A **total** structural + scope check on an `AuthorizationPolicyDef` value — for any input,
|
|
91
|
-
* including a hand-tampered Server IR where the policy is the wrong shape entirely (spec15
|
|
92
|
-
* §37). Cross-node reference resolution (does an `authorizationPolicy` id point at a real
|
|
93
|
-
* policy) is `validateGraph`'s job, not this function's.
|
|
94
|
-
*/
|
|
95
89
|
export declare function authorizationPolicyProblems(policy: unknown): AuthorizationPolicyProblem[];
|
|
96
90
|
export interface AuthorizationPolicyDependencies {
|
|
97
91
|
/** Field ids the policy's `allow` expression reads off `PRINCIPAL`. */
|
|
@@ -147,6 +141,34 @@ export interface AuthorizationCheckResult {
|
|
|
147
141
|
* - otherwise ALLOW (`allowed`). Both, when present, must pass (conjunction).
|
|
148
142
|
*/
|
|
149
143
|
export declare function decideAuthorization(input: AuthorizationCheckInput): AuthorizationCheckResult;
|
|
144
|
+
/**
|
|
145
|
+
* The closed scope an `AuthorizationPolicyDef.allow` expression is evaluated against. Each
|
|
146
|
+
* field is a plain record or `null` (anonymous / no resource); a `field` read of a key that
|
|
147
|
+
* is not present is **security-scope absence**, not an ordinary `undefined` (spec15pt2 §5-§7).
|
|
148
|
+
*/
|
|
149
|
+
export interface AuthorizationPolicyScope {
|
|
150
|
+
principal: Record<string, unknown> | null | undefined;
|
|
151
|
+
resource: Record<string, unknown> | null | undefined;
|
|
152
|
+
operation: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* spec15pt2 F1 — the **canonical** three-valued `AuthorizationPolicyDef.allow` evaluator,
|
|
156
|
+
* used by every policy-bearing surface (spec15pt2 §33). Returns the `{ ok, value }` shape
|
|
157
|
+
* `decideAuthorization` already consumes:
|
|
158
|
+
*
|
|
159
|
+
* - `allow` reduces to **exactly `true`** (with every required security input present) ⇒
|
|
160
|
+
* `{ ok: true, value: true }` — the only ALLOW;
|
|
161
|
+
* - `allow` reduces to any other concrete value ⇒ `{ ok: true, value: false }`;
|
|
162
|
+
* - the truth of `allow` depends on a **missing PRINCIPAL / RESOURCE field**
|
|
163
|
+
* (`AbsentSecurityValue`) ⇒ `{ ok: true, value: false }` — absence never creates authority
|
|
164
|
+
* through `eq` / `neq` / `not` / `or` (spec15pt2 §8-§12);
|
|
165
|
+
* - an evaluation error (malformed node reaching the runtime, unsupported builtin) ⇒
|
|
166
|
+
* `{ ok: false }`.
|
|
167
|
+
*
|
|
168
|
+
* A constant `literal(true)` is `{ ok: true, value: true }` regardless of principal — an
|
|
169
|
+
* explicitly public policy still admits an anonymous caller (spec15pt2 §13, §14).
|
|
170
|
+
*/
|
|
171
|
+
export declare function evaluateAuthorizationPolicyAllow(allow: unknown, scope: AuthorizationPolicyScope): AuthorizationCheckPart;
|
|
150
172
|
/**
|
|
151
173
|
* The distinct policy ids a graph node references for authorization — for dependency
|
|
152
174
|
* analysis and `validateGraph` reference resolution. Total over malformed input.
|
package/dist/authorization.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* closed with a structured diagnostic, never a native exception (spec15 §37).
|
|
27
27
|
*/
|
|
28
28
|
import { OPERATION, PRINCIPAL, RESOURCE } from './authority.js';
|
|
29
|
+
import { EXPRESSION_KINDS } from './expressions.js';
|
|
29
30
|
// --------------------------------------------------------------------- reserved scope ids
|
|
30
31
|
/**
|
|
31
32
|
* The canonical caller. The **same** reserved id `ActionDef.authorization` uses
|
|
@@ -136,6 +137,80 @@ function walkPolicyExpression(expression, refs, nondeterministic, seen = new Set
|
|
|
136
137
|
* §37). Cross-node reference resolution (does an `authorizationPolicy` id point at a real
|
|
137
138
|
* policy) is `validateGraph`'s job, not this function's.
|
|
138
139
|
*/
|
|
140
|
+
/**
|
|
141
|
+
* spec15pt2 §39-§41 (F2) — a **total** structural check on an `allow` expression tree.
|
|
142
|
+
* Every node must be a plain object whose `kind` is a real `EXPRESSION_KINDS` member and
|
|
143
|
+
* whose per-kind required children are present and themselves structurally valid. Returns a
|
|
144
|
+
* short reason on the first problem, `null` when the tree is well-formed. Never throws.
|
|
145
|
+
*/
|
|
146
|
+
function malformedExpressionReason(expression, seen = new Set()) {
|
|
147
|
+
if (!isPlainObject(expression))
|
|
148
|
+
return 'a policy expression node is not an object';
|
|
149
|
+
if (seen.has(expression))
|
|
150
|
+
return 'a policy expression contains a cycle';
|
|
151
|
+
seen.add(expression);
|
|
152
|
+
const kind = expression.kind;
|
|
153
|
+
if (typeof kind !== 'string' || !EXPRESSION_KINDS.includes(kind)) {
|
|
154
|
+
return `unknown expression kind ${JSON.stringify(kind)}`;
|
|
155
|
+
}
|
|
156
|
+
const child = (name) => malformedExpressionReason(expression[name], seen);
|
|
157
|
+
const list = (name) => {
|
|
158
|
+
const arr = expression[name];
|
|
159
|
+
if (!Array.isArray(arr))
|
|
160
|
+
return `${kind}.${name} is not an array`;
|
|
161
|
+
for (const entry of arr) {
|
|
162
|
+
const r = malformedExpressionReason(entry, seen);
|
|
163
|
+
if (r)
|
|
164
|
+
return r;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
};
|
|
168
|
+
switch (kind) {
|
|
169
|
+
case 'literal':
|
|
170
|
+
return 'value' in expression ? null : 'literal has no value';
|
|
171
|
+
case 'ref':
|
|
172
|
+
return typeof expression.targetId === 'string' ? null : 'ref has no targetId';
|
|
173
|
+
case 'field':
|
|
174
|
+
return (typeof expression.fieldId === 'string' ? null : 'field has no fieldId') ?? child('source');
|
|
175
|
+
case 'unary':
|
|
176
|
+
return (typeof expression.operator === 'string' ? null : 'unary has no operator') ?? child('operand');
|
|
177
|
+
case 'binary':
|
|
178
|
+
return (typeof expression.operator === 'string' ? null : 'binary has no operator') ?? child('left') ?? child('right');
|
|
179
|
+
case 'call':
|
|
180
|
+
return (typeof expression.function === 'string' ? null : 'call has no function') ?? list('arguments');
|
|
181
|
+
case 'conditional':
|
|
182
|
+
return child('condition') ?? child('whenTrue') ?? child('whenFalse');
|
|
183
|
+
case 'object': {
|
|
184
|
+
const entries = expression.entries;
|
|
185
|
+
if (!Array.isArray(entries))
|
|
186
|
+
return 'object has no entries array';
|
|
187
|
+
for (const e of entries) {
|
|
188
|
+
if (!isPlainObject(e) || typeof e.fieldId !== 'string')
|
|
189
|
+
return 'an object entry is malformed';
|
|
190
|
+
const r = malformedExpressionReason(e.value, seen);
|
|
191
|
+
if (r)
|
|
192
|
+
return r;
|
|
193
|
+
}
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
case 'filter':
|
|
197
|
+
case 'find':
|
|
198
|
+
case 'every':
|
|
199
|
+
case 'some':
|
|
200
|
+
return child('source') ?? child('predicate');
|
|
201
|
+
case 'map':
|
|
202
|
+
return child('source') ?? child('projection');
|
|
203
|
+
case 'sort':
|
|
204
|
+
case 'group':
|
|
205
|
+
return child('source') ?? child('by');
|
|
206
|
+
case 'flatten':
|
|
207
|
+
return child('source');
|
|
208
|
+
case 'expression-ref':
|
|
209
|
+
return typeof expression.targetId === 'string' ? null : 'expression-ref has no targetId';
|
|
210
|
+
default:
|
|
211
|
+
return `unsupported expression kind ${kind}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
139
214
|
export function authorizationPolicyProblems(policy) {
|
|
140
215
|
const problems = [];
|
|
141
216
|
const pid = isPlainObject(policy) ? String(policy.id ?? '<unknown>') : '<unknown>';
|
|
@@ -149,6 +224,12 @@ export function authorizationPolicyProblems(policy) {
|
|
|
149
224
|
});
|
|
150
225
|
return problems;
|
|
151
226
|
}
|
|
227
|
+
// spec15pt2 F2 — a malformed `allow` tree is rejected structurally here, before compile,
|
|
228
|
+
// rather than surviving to a native exception at evaluation.
|
|
229
|
+
const malformed = malformedExpressionReason(policy.allow);
|
|
230
|
+
if (malformed) {
|
|
231
|
+
return [{ code: 'AUTHORIZATION_INVALID_POLICY', message: `Authorization policy ${pid} has a malformed 'allow' expression: ${malformed}` }];
|
|
232
|
+
}
|
|
152
233
|
const refs = new Set();
|
|
153
234
|
const nondeterministic = new Set();
|
|
154
235
|
walkPolicyExpression(policy.allow, refs, nondeterministic);
|
|
@@ -253,6 +334,243 @@ export function decideAuthorization(input) {
|
|
|
253
334
|
}
|
|
254
335
|
return { decision: 'ALLOW', reason: 'allowed' };
|
|
255
336
|
}
|
|
337
|
+
const U = { t: 'u' };
|
|
338
|
+
const E = { t: 'e' };
|
|
339
|
+
const C = (v) => ({ t: 'c', v });
|
|
340
|
+
function asBool(v) {
|
|
341
|
+
return v === true ? true : v === false ? false : Boolean(v);
|
|
342
|
+
}
|
|
343
|
+
function deepEq(a, b) {
|
|
344
|
+
if (a === b)
|
|
345
|
+
return true;
|
|
346
|
+
try {
|
|
347
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/** Whether an expression tree reads only through `field` / `ref` rooted at PRINCIPAL / RESOURCE. */
|
|
354
|
+
function isSecurityRooted(expression) {
|
|
355
|
+
if (!isPlainObject(expression))
|
|
356
|
+
return false;
|
|
357
|
+
if (expression.kind === 'ref') {
|
|
358
|
+
const t = String(expression.targetId);
|
|
359
|
+
return t === AUTHZ_PRINCIPAL_SCOPE || t === AUTHZ_RESOURCE_SCOPE;
|
|
360
|
+
}
|
|
361
|
+
if (expression.kind === 'field')
|
|
362
|
+
return isSecurityRooted(expression.source);
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
function evalAuthz(expression, scope, seen) {
|
|
366
|
+
if (!isPlainObject(expression) || seen.has(expression))
|
|
367
|
+
return E;
|
|
368
|
+
seen.add(expression);
|
|
369
|
+
switch (expression.kind) {
|
|
370
|
+
case 'literal':
|
|
371
|
+
return C(expression.value);
|
|
372
|
+
case 'ref': {
|
|
373
|
+
const t = String(expression.targetId);
|
|
374
|
+
if (t === AUTHZ_PRINCIPAL_SCOPE)
|
|
375
|
+
return C(scope.principal ?? null);
|
|
376
|
+
if (t === AUTHZ_RESOURCE_SCOPE)
|
|
377
|
+
return C(scope.resource ?? null);
|
|
378
|
+
if (t === AUTHZ_OPERATION_SCOPE)
|
|
379
|
+
return C(scope.operation);
|
|
380
|
+
return E; // out of the closed scope — validation rejects it; be total
|
|
381
|
+
}
|
|
382
|
+
case 'field': {
|
|
383
|
+
const src = evalAuthz(expression.source, scope, seen);
|
|
384
|
+
if (src.t === 'e')
|
|
385
|
+
return E;
|
|
386
|
+
if (src.t === 'u')
|
|
387
|
+
return U;
|
|
388
|
+
const key = String(expression.fieldId);
|
|
389
|
+
const rooted = isSecurityRooted(expression.source);
|
|
390
|
+
const obj = src.v;
|
|
391
|
+
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
|
392
|
+
return rooted ? U : C(null);
|
|
393
|
+
}
|
|
394
|
+
const present = Object.prototype.hasOwnProperty.call(obj, key) && obj[key] !== undefined;
|
|
395
|
+
if (!present)
|
|
396
|
+
return rooted ? U : C(null);
|
|
397
|
+
return C(obj[key]);
|
|
398
|
+
}
|
|
399
|
+
case 'unary': {
|
|
400
|
+
const operand = evalAuthz(expression.operand, scope, seen);
|
|
401
|
+
if (operand.t !== 'c')
|
|
402
|
+
return operand; // NOT unknown ⇒ unknown; NOT error ⇒ error (§9, §12)
|
|
403
|
+
return expression.operator === 'not' ? C(!asBool(operand.v)) : C(-Number(operand.v));
|
|
404
|
+
}
|
|
405
|
+
case 'binary': {
|
|
406
|
+
const op = String(expression.operator);
|
|
407
|
+
if (op === 'and') {
|
|
408
|
+
const l = evalAuthz(expression.left, scope, seen);
|
|
409
|
+
if (l.t === 'c' && !asBool(l.v))
|
|
410
|
+
return C(false); // FALSE AND _ ⇒ FALSE (short-circuit-independent)
|
|
411
|
+
const r = evalAuthz(expression.right, scope, seen);
|
|
412
|
+
if (l.t === 'e' || r.t === 'e')
|
|
413
|
+
return E;
|
|
414
|
+
if (r.t === 'c' && !asBool(r.v))
|
|
415
|
+
return C(false);
|
|
416
|
+
if (l.t === 'c' && r.t === 'c')
|
|
417
|
+
return C(true); // both concrete-true
|
|
418
|
+
return U;
|
|
419
|
+
}
|
|
420
|
+
if (op === 'or') {
|
|
421
|
+
const l = evalAuthz(expression.left, scope, seen);
|
|
422
|
+
if (l.t === 'c' && asBool(l.v))
|
|
423
|
+
return C(true); // TRUE OR _ ⇒ TRUE
|
|
424
|
+
const r = evalAuthz(expression.right, scope, seen);
|
|
425
|
+
if (l.t === 'e' || r.t === 'e')
|
|
426
|
+
return E;
|
|
427
|
+
if (r.t === 'c' && asBool(r.v))
|
|
428
|
+
return C(true);
|
|
429
|
+
if (l.t === 'c' && r.t === 'c')
|
|
430
|
+
return C(false); // both concrete-false
|
|
431
|
+
return U;
|
|
432
|
+
}
|
|
433
|
+
const l = evalAuthz(expression.left, scope, seen);
|
|
434
|
+
const r = evalAuthz(expression.right, scope, seen);
|
|
435
|
+
if (l.t === 'e' || r.t === 'e')
|
|
436
|
+
return E;
|
|
437
|
+
if (l.t === 'u' || r.t === 'u')
|
|
438
|
+
return U; // any comparison touching absence ⇒ non-satisfied (§8)
|
|
439
|
+
const a = l.v;
|
|
440
|
+
const b = r.v;
|
|
441
|
+
switch (op) {
|
|
442
|
+
case 'eq':
|
|
443
|
+
return C(deepEq(a, b));
|
|
444
|
+
case 'neq':
|
|
445
|
+
return C(!deepEq(a, b));
|
|
446
|
+
case 'lt':
|
|
447
|
+
return C(a < b);
|
|
448
|
+
case 'lte':
|
|
449
|
+
return C(a <= b);
|
|
450
|
+
case 'gt':
|
|
451
|
+
return C(a > b);
|
|
452
|
+
case 'gte':
|
|
453
|
+
return C(a >= b);
|
|
454
|
+
case 'add':
|
|
455
|
+
return C(a + b);
|
|
456
|
+
case 'subtract':
|
|
457
|
+
return C(a - b);
|
|
458
|
+
case 'multiply':
|
|
459
|
+
return C(a * b);
|
|
460
|
+
case 'divide':
|
|
461
|
+
return C(a / b);
|
|
462
|
+
default:
|
|
463
|
+
return E;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
case 'conditional': {
|
|
467
|
+
const c = evalAuthz(expression.condition, scope, seen);
|
|
468
|
+
if (c.t !== 'c')
|
|
469
|
+
return c;
|
|
470
|
+
return asBool(c.v) ? evalAuthz(expression.whenTrue, scope, seen) : evalAuthz(expression.whenFalse, scope, seen);
|
|
471
|
+
}
|
|
472
|
+
case 'object': {
|
|
473
|
+
const out = {};
|
|
474
|
+
for (const entry of expression.entries ?? []) {
|
|
475
|
+
const v = evalAuthz(entry.value, scope, seen);
|
|
476
|
+
if (v.t === 'e')
|
|
477
|
+
return E;
|
|
478
|
+
if (v.t === 'u')
|
|
479
|
+
return U;
|
|
480
|
+
out[String(entry.fieldId)] = v.v;
|
|
481
|
+
}
|
|
482
|
+
return C(out);
|
|
483
|
+
}
|
|
484
|
+
case 'call': {
|
|
485
|
+
const args = [];
|
|
486
|
+
for (const argument of expression.arguments ?? []) {
|
|
487
|
+
const v = evalAuthz(argument, scope, seen);
|
|
488
|
+
if (v.t === 'e')
|
|
489
|
+
return E;
|
|
490
|
+
if (v.t === 'u')
|
|
491
|
+
return U; // conservative: any absent input ⇒ non-satisfied (spec15pt2 §57, §85)
|
|
492
|
+
args.push(v.v);
|
|
493
|
+
}
|
|
494
|
+
return applyPolicyBuiltin(String(expression.function), args);
|
|
495
|
+
}
|
|
496
|
+
default:
|
|
497
|
+
return E;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function applyPolicyBuiltin(fn, args) {
|
|
501
|
+
switch (fn) {
|
|
502
|
+
case 'required':
|
|
503
|
+
return C(args[0] !== null && args[0] !== undefined);
|
|
504
|
+
case 'is-empty':
|
|
505
|
+
return C(args[0] === null || args[0] === undefined || args[0] === '' || (Array.isArray(args[0]) && args[0].length === 0));
|
|
506
|
+
case 'non-empty':
|
|
507
|
+
return C(!(args[0] === null || args[0] === undefined || args[0] === '' || (Array.isArray(args[0]) && args[0].length === 0)));
|
|
508
|
+
case 'length':
|
|
509
|
+
return C(Array.isArray(args[0]) || typeof args[0] === 'string' ? args[0].length : 0);
|
|
510
|
+
case 'contains':
|
|
511
|
+
return C(typeof args[0] === 'string'
|
|
512
|
+
? args[0].includes(String(args[1]))
|
|
513
|
+
: Array.isArray(args[0])
|
|
514
|
+
? args[0].some((x) => deepEq(x, args[1]))
|
|
515
|
+
: false);
|
|
516
|
+
case 'one-of':
|
|
517
|
+
return C(args.slice(1).some((x) => deepEq(x, args[0])));
|
|
518
|
+
case 'concat':
|
|
519
|
+
return C(args.map((x) => (x === null || x === undefined ? '' : String(x))).join(''));
|
|
520
|
+
case 'to-string':
|
|
521
|
+
return C(args[0] === null || args[0] === undefined ? '' : String(args[0]));
|
|
522
|
+
case 'lowercase':
|
|
523
|
+
return C(String(args[0] ?? '').toLowerCase());
|
|
524
|
+
case 'trim':
|
|
525
|
+
return C(String(args[0] ?? '').trim());
|
|
526
|
+
case 'substring-before': {
|
|
527
|
+
const s = String(args[0] ?? '');
|
|
528
|
+
const i = s.indexOf(String(args[1] ?? ''));
|
|
529
|
+
return C(i < 0 ? '' : s.slice(0, i));
|
|
530
|
+
}
|
|
531
|
+
case 'substring-after': {
|
|
532
|
+
const s = String(args[0] ?? '');
|
|
533
|
+
const needle = String(args[1] ?? '');
|
|
534
|
+
const i = s.indexOf(needle);
|
|
535
|
+
return C(i < 0 ? '' : s.slice(i + needle.length));
|
|
536
|
+
}
|
|
537
|
+
case 'coalesce':
|
|
538
|
+
return C(args.find((x) => x !== null && x !== undefined) ?? null);
|
|
539
|
+
default:
|
|
540
|
+
return E; // an unknown / non-deterministic builtin fails closed
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* spec15pt2 F1 — the **canonical** three-valued `AuthorizationPolicyDef.allow` evaluator,
|
|
545
|
+
* used by every policy-bearing surface (spec15pt2 §33). Returns the `{ ok, value }` shape
|
|
546
|
+
* `decideAuthorization` already consumes:
|
|
547
|
+
*
|
|
548
|
+
* - `allow` reduces to **exactly `true`** (with every required security input present) ⇒
|
|
549
|
+
* `{ ok: true, value: true }` — the only ALLOW;
|
|
550
|
+
* - `allow` reduces to any other concrete value ⇒ `{ ok: true, value: false }`;
|
|
551
|
+
* - the truth of `allow` depends on a **missing PRINCIPAL / RESOURCE field**
|
|
552
|
+
* (`AbsentSecurityValue`) ⇒ `{ ok: true, value: false }` — absence never creates authority
|
|
553
|
+
* through `eq` / `neq` / `not` / `or` (spec15pt2 §8-§12);
|
|
554
|
+
* - an evaluation error (malformed node reaching the runtime, unsupported builtin) ⇒
|
|
555
|
+
* `{ ok: false }`.
|
|
556
|
+
*
|
|
557
|
+
* A constant `literal(true)` is `{ ok: true, value: true }` regardless of principal — an
|
|
558
|
+
* explicitly public policy still admits an anonymous caller (spec15pt2 §13, §14).
|
|
559
|
+
*/
|
|
560
|
+
export function evaluateAuthorizationPolicyAllow(allow, scope) {
|
|
561
|
+
let result;
|
|
562
|
+
try {
|
|
563
|
+
result = evalAuthz(allow, scope, new Set());
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
return { ok: false };
|
|
567
|
+
}
|
|
568
|
+
if (result.t === 'e')
|
|
569
|
+
return { ok: false };
|
|
570
|
+
if (result.t === 'u')
|
|
571
|
+
return { ok: true, value: false };
|
|
572
|
+
return { ok: true, value: result.v };
|
|
573
|
+
}
|
|
256
574
|
/**
|
|
257
575
|
* The distinct policy ids a graph node references for authorization — for dependency
|
|
258
576
|
* analysis and `validateGraph` reference resolution. Total over malformed input.
|
|
@@ -100,6 +100,15 @@ export interface AuthorityCompatibilityKey {
|
|
|
100
100
|
schemaFingerprint: string;
|
|
101
101
|
serverContract: string;
|
|
102
102
|
semanticFingerprint: string;
|
|
103
|
+
/**
|
|
104
|
+
* spec15pt2 §35 — the runtime authorization-evaluator semantics version. `0.15.0-alpha.1`
|
|
105
|
+
* and `0.15.0-alpha.2` evaluate the *same* Server IR authorization policy differently
|
|
106
|
+
* (absent-value safety, F1), yet the graph — and therefore `semanticFingerprint` — is
|
|
107
|
+
* identical. This discriminator, present only when the IR carries authorization
|
|
108
|
+
* vocabulary, keeps the two builds from silently co-participating in one authority domain.
|
|
109
|
+
* Absent on a graph with no authorization policy (its evaluation is unchanged).
|
|
110
|
+
*/
|
|
111
|
+
authorizationRuntime?: string;
|
|
103
112
|
}
|
|
104
113
|
export declare function authorityCompatibilityKey(parts: AuthorityCompatibilityKey): AuthorityCompatibilityKey;
|
|
105
114
|
/** A stable, comparable string form for storing on a durable work item (spec12 §43). */
|
|
@@ -138,6 +138,7 @@ export function authorityCompatibilityKey(parts) {
|
|
|
138
138
|
schemaFingerprint: parts.schemaFingerprint,
|
|
139
139
|
serverContract: parts.serverContract,
|
|
140
140
|
semanticFingerprint: parts.semanticFingerprint,
|
|
141
|
+
...(parts.authorizationRuntime !== undefined ? { authorizationRuntime: parts.authorizationRuntime } : {}),
|
|
141
142
|
};
|
|
142
143
|
}
|
|
143
144
|
/** A stable, comparable string form for storing on a durable work item (spec12 §43). */
|
|
@@ -155,7 +156,10 @@ export function compareAuthorityCompatibility(a, b) {
|
|
|
155
156
|
'schemaFingerprint',
|
|
156
157
|
'serverContract',
|
|
157
158
|
'semanticFingerprint',
|
|
159
|
+
'authorizationRuntime',
|
|
158
160
|
];
|
|
159
|
-
|
|
161
|
+
// `authorizationRuntime` absent on both sides (a non-authorization graph) is a match;
|
|
162
|
+
// present-vs-absent (alpha.1 stored key vs alpha.2) is a mismatch (spec15pt2 §35, §76).
|
|
163
|
+
const mismatches = fields.filter((field) => (a[field] ?? null) !== (b[field] ?? null));
|
|
160
164
|
return { compatible: mismatches.length === 0, mismatches };
|
|
161
165
|
}
|