@cynodia/axiom-core 0.14.0-alpha.5 → 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/authority.d.ts +14 -0
- package/dist/authority.js +14 -0
- package/dist/authorization.d.ts +177 -0
- package/dist/authorization.js +588 -0
- package/dist/diagnostics.d.ts +8 -0
- package/dist/diagnostics.js +11 -0
- package/dist/graph.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/nodes.d.ts +7 -0
- package/dist/query.d.ts +6 -0
- package/dist/semantic-identity.d.ts +16 -1
- package/dist/semantic-identity.js +12 -1
- package/dist/server-ir.d.ts +42 -1
- package/dist/server-ir.js +50 -1
- package/dist/types.d.ts +3 -2
- package/dist/types.js +1 -0
- package/dist/validate.js +37 -0
- package/dist/workflows.d.ts +15 -0
- package/package.json +1 -1
package/dist/authority.d.ts
CHANGED
|
@@ -26,6 +26,20 @@ export declare const AUTHORITIES: readonly Authority[];
|
|
|
26
26
|
* It is bound only where a server evaluates. A client never sees it.
|
|
27
27
|
*/
|
|
28
28
|
export declare const PRINCIPAL: NodeId;
|
|
29
|
+
/**
|
|
30
|
+
* The semantic object an authorization decision is made *about* — a provider record, a
|
|
31
|
+
* workflow instance, or (for `action.invoke`, where there is no per-record target) a stable
|
|
32
|
+
* descriptor of the operation's target. Bound only where an authority evaluates an
|
|
33
|
+
* `AuthorizationPolicyDef` (spec15 §34, §99); like `PRINCIPAL` it is never client-visible.
|
|
34
|
+
*/
|
|
35
|
+
export declare const RESOURCE: NodeId;
|
|
36
|
+
/**
|
|
37
|
+
* The canonical semantic operation identity an authorization decision is made *for* — one of
|
|
38
|
+
* `AUTHORIZATION_OPERATIONS` (`'action.invoke'`, `'query.read'`, …). `ref(OPERATION)`
|
|
39
|
+
* resolves to that string directly. Bound only where an authority evaluates an
|
|
40
|
+
* `AuthorizationPolicyDef` (spec15 §34, §98).
|
|
41
|
+
*/
|
|
42
|
+
export declare const OPERATION: NodeId;
|
|
29
43
|
/**
|
|
30
44
|
* The authority of a state. Absent metadata means `client`, so every 0.5.x graph keeps
|
|
31
45
|
* executing exactly as it did.
|
package/dist/authority.js
CHANGED
|
@@ -16,6 +16,20 @@ export const AUTHORITIES = ['client', 'server'];
|
|
|
16
16
|
* It is bound only where a server evaluates. A client never sees it.
|
|
17
17
|
*/
|
|
18
18
|
export const PRINCIPAL = 'axiom_principal';
|
|
19
|
+
/**
|
|
20
|
+
* The semantic object an authorization decision is made *about* — a provider record, a
|
|
21
|
+
* workflow instance, or (for `action.invoke`, where there is no per-record target) a stable
|
|
22
|
+
* descriptor of the operation's target. Bound only where an authority evaluates an
|
|
23
|
+
* `AuthorizationPolicyDef` (spec15 §34, §99); like `PRINCIPAL` it is never client-visible.
|
|
24
|
+
*/
|
|
25
|
+
export const RESOURCE = 'axiom_resource';
|
|
26
|
+
/**
|
|
27
|
+
* The canonical semantic operation identity an authorization decision is made *for* — one of
|
|
28
|
+
* `AUTHORIZATION_OPERATIONS` (`'action.invoke'`, `'query.read'`, …). `ref(OPERATION)`
|
|
29
|
+
* resolves to that string directly. Bound only where an authority evaluates an
|
|
30
|
+
* `AuthorizationPolicyDef` (spec15 §34, §98).
|
|
31
|
+
*/
|
|
32
|
+
export const OPERATION = 'axiom_operation';
|
|
19
33
|
/**
|
|
20
34
|
* The authority of a state. Absent metadata means `client`, so every 0.5.x graph keeps
|
|
21
35
|
* executing exactly as it did.
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization completeness (spec15) — the canonical, portable authorization policy model.
|
|
3
|
+
*
|
|
4
|
+
* Authorization in Axiom is **semantic**: whether a principal may perform a semantic
|
|
5
|
+
* operation is executable meaning, not presentation. It therefore lives in the graph, in
|
|
6
|
+
* `semanticFingerprint`, in authority compatibility, in the Server IR and in conformance —
|
|
7
|
+
* never as a host-language callback (spec15 §6, §7).
|
|
8
|
+
*
|
|
9
|
+
* There is exactly **one** authorization language: an `AuthorizationPolicyDef` with a single
|
|
10
|
+
* boolean `allow` expression over a **closed scope** — `ref('PRINCIPAL')` (the canonical
|
|
11
|
+
* caller), `ref('RESOURCE')` (the semantic object the operation targets, where one exists)
|
|
12
|
+
* and `ref('OPERATION')` (the canonical operation identity). No `StateDef`, no `QueryDef`,
|
|
13
|
+
* no `now` / `uuid` / `random`, no ambient runtime state (spec15 §34). `allow` evaluating to
|
|
14
|
+
* exactly `true` is **ALLOW**; `false`, absence, or any evaluation error is **DENY** — the
|
|
15
|
+
* safe direction is always refusal (spec15 §8, §123).
|
|
16
|
+
*
|
|
17
|
+
* A policy is referenced by id from the surface it protects — `ActionDef.authorizationPolicy`,
|
|
18
|
+
* `QueryDef.authorizationPolicy`, `WorkflowDef.startPolicy` / `instanceAccessPolicy` — so the
|
|
19
|
+
* same evaluator and the same decision model apply everywhere (spec15 §5, §96). The legacy
|
|
20
|
+
* `ActionDef.authorization` boolean expression remains supported and is canonically
|
|
21
|
+
* equivalent to an inline `allow` over `PRINCIPAL`; `ReadPolicyDef` (spec10) remains the
|
|
22
|
+
* row-level read mechanism, unified into query authorization by spec15 Phase D.
|
|
23
|
+
*
|
|
24
|
+
* This module is portable plain data + `Expression` trees; it carries no enforcement (that
|
|
25
|
+
* is Phases C–F). The accessors are **total over any input** so a hand-tampered policy fails
|
|
26
|
+
* closed with a structured diagnostic, never a native exception (spec15 §37).
|
|
27
|
+
*/
|
|
28
|
+
import type { Expression } from './expressions.js';
|
|
29
|
+
import type { NodeId } from './ids.js';
|
|
30
|
+
import type { NodeBase } from './nodes.js';
|
|
31
|
+
/**
|
|
32
|
+
* The canonical caller. The **same** reserved id `ActionDef.authorization` uses
|
|
33
|
+
* (`'axiom_principal'`, exported from `authority.ts` as `PRINCIPAL`) — write a policy with
|
|
34
|
+
* `field(ref(PRINCIPAL), F_ROLE)`, exactly as a legacy authorization expression.
|
|
35
|
+
*/
|
|
36
|
+
export declare const AUTHZ_PRINCIPAL_SCOPE: NodeId;
|
|
37
|
+
/** The semantic object the operation targets (a record, a workflow instance, …), where one exists. */
|
|
38
|
+
export declare const AUTHZ_RESOURCE_SCOPE: NodeId;
|
|
39
|
+
/** The canonical operation identity — `ref(OPERATION)` resolves to an `AuthorizationOperation` string. */
|
|
40
|
+
export declare const AUTHZ_OPERATION_SCOPE: NodeId;
|
|
41
|
+
/** Every id a policy `allow` expression's `ref` may resolve. Nothing else is in scope. */
|
|
42
|
+
export declare const AUTHORIZATION_SCOPE_IDS: readonly string[];
|
|
43
|
+
/** Builtins forbidden in a policy expression — authorization must be deterministic (spec15 §34). */
|
|
44
|
+
export declare const AUTHORIZATION_NONDETERMINISTIC_BUILTINS: ReadonlySet<string>;
|
|
45
|
+
/**
|
|
46
|
+
* The closed set of canonical semantic operations an authorization decision is made about
|
|
47
|
+
* (spec15 §98). Policies reason over these, never over transport method names. Enforcement
|
|
48
|
+
* of each arrives with its phase (C: `action.invoke` / `record.*` / `state.*`; D:
|
|
49
|
+
* `query.read`; E: `workflow.*`; F: `live.*` / `subscription.*`).
|
|
50
|
+
*/
|
|
51
|
+
export declare const AUTHORIZATION_OPERATIONS: readonly ["action.invoke", "query.read", "record.read", "record.mutate", "state.read", "state.mutate", "workflow.start", "workflow.inspect", "workflow.history", "workflow.cancel", "live.open", "live.resume", "subscription.open", "event.ingress"];
|
|
52
|
+
export type AuthorizationOperation = (typeof AUTHORIZATION_OPERATIONS)[number];
|
|
53
|
+
export interface AuthorizationPolicyDef extends NodeBase {
|
|
54
|
+
kind: 'authorization-policy';
|
|
55
|
+
/**
|
|
56
|
+
* Boolean, closed scope (`PRINCIPAL` / `RESOURCE` / `OPERATION` only). Exactly `true` ⇒
|
|
57
|
+
* ALLOW; `false` / absent / evaluation error ⇒ DENY (spec15 §8). Deterministic — no
|
|
58
|
+
* `now` / `uuid` / `random`, no `StateDef` / `QueryDef` (spec15 §34).
|
|
59
|
+
*/
|
|
60
|
+
allow: Expression;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* What a protected surface does when it has **no** attached policy (spec15 §9). Deliberately
|
|
64
|
+
* one canonical rule, applied consistently, never left to the runtime:
|
|
65
|
+
*
|
|
66
|
+
* - a surface whose *current* (pre-0.15) contract is public — an unrestricted `ActionDef`,
|
|
67
|
+
* an unrestricted `QueryDef` — keeps that public contract (`'public'`);
|
|
68
|
+
* - a surface whose current contract is already restricted — a `WorkflowDef` instance
|
|
69
|
+
* operation (0.14 owner-fingerprint), an `ActionDef` with a legacy `authorization`
|
|
70
|
+
* expression, a `QueryDef` with a `ReadPolicyDef` — keeps that restriction;
|
|
71
|
+
* - a *new* privileged surface with no policy fails closed (`'deny'`).
|
|
72
|
+
*
|
|
73
|
+
* These constants name the rule so docs, tests and a future independent runtime agree.
|
|
74
|
+
*/
|
|
75
|
+
export declare const AUTHORIZATION_DEFAULT: {
|
|
76
|
+
/** No policy, previously-public surface ⇒ still public. */
|
|
77
|
+
readonly PUBLIC_SURFACE: "public";
|
|
78
|
+
/** No policy, previously-restricted surface ⇒ keep the prior restriction. */
|
|
79
|
+
readonly KEEP_PRIOR_RESTRICTION: "keep-prior-restriction";
|
|
80
|
+
/** No policy, new privileged surface ⇒ DENY. */
|
|
81
|
+
readonly FAIL_CLOSED: "deny";
|
|
82
|
+
};
|
|
83
|
+
/** Every `Expression` a policy embeds — total over malformed input. */
|
|
84
|
+
export declare function authorizationPolicyExpressions(policy: unknown): Expression[];
|
|
85
|
+
export interface AuthorizationPolicyProblem {
|
|
86
|
+
code: 'AUTHORIZATION_INVALID_POLICY' | 'AUTHORIZATION_INVALID_SCOPE' | 'AUTHORIZATION_NONDETERMINISTIC';
|
|
87
|
+
message: string;
|
|
88
|
+
}
|
|
89
|
+
export declare function authorizationPolicyProblems(policy: unknown): AuthorizationPolicyProblem[];
|
|
90
|
+
export interface AuthorizationPolicyDependencies {
|
|
91
|
+
/** Field ids the policy's `allow` expression reads off `PRINCIPAL`. */
|
|
92
|
+
principalFields: string[];
|
|
93
|
+
/** Field ids it reads off `RESOURCE`. */
|
|
94
|
+
resourceFields: string[];
|
|
95
|
+
/** Whether it references `OPERATION` at all. */
|
|
96
|
+
readsOperation: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* A verdict when `allow` is a constant `literal` (spec15 §8): a literal `true` always
|
|
99
|
+
* allows, anything else always denies. `null` when the decision depends on the inputs.
|
|
100
|
+
*/
|
|
101
|
+
constant: 'always-allow' | 'always-deny' | null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* What an `AuthorizationPolicyDef` depends on — for explainability, static coverage analysis
|
|
105
|
+
* and AI authoring (spec15 §35, §44). Total over malformed input (empty result). Reports
|
|
106
|
+
* *structure only*, never a runtime value.
|
|
107
|
+
*/
|
|
108
|
+
export declare function authorizationPolicyDependencies(policy: unknown): AuthorizationPolicyDependencies;
|
|
109
|
+
export declare const AUTHORIZATION_DECISIONS: readonly ["ALLOW", "DENY"];
|
|
110
|
+
export type AuthorizationDecision = (typeof AUTHORIZATION_DECISIONS)[number];
|
|
111
|
+
/** One evaluated boolean input to an authorization decision. `ok: false` ⇒ evaluation failed. */
|
|
112
|
+
export interface AuthorizationCheckPart {
|
|
113
|
+
ok: boolean;
|
|
114
|
+
value?: unknown;
|
|
115
|
+
}
|
|
116
|
+
export interface AuthorizationCheckInput {
|
|
117
|
+
/**
|
|
118
|
+
* The `AuthorizationPolicyDef.allow` outcome, when a policy is attached. Per spec15 §8 a
|
|
119
|
+
* policy allows only when it evaluates to **exactly `true`**.
|
|
120
|
+
*/
|
|
121
|
+
policy?: AuthorizationCheckPart;
|
|
122
|
+
/**
|
|
123
|
+
* The legacy `ActionDef.authorization` expression outcome, when that expression is present.
|
|
124
|
+
* Its historical truthiness rule is kept: a non-empty array or any truthy value allows.
|
|
125
|
+
*/
|
|
126
|
+
legacy?: AuthorizationCheckPart;
|
|
127
|
+
}
|
|
128
|
+
export interface AuthorizationCheckResult {
|
|
129
|
+
decision: AuthorizationDecision;
|
|
130
|
+
reason: 'no-policy' | 'allowed' | 'policy-denied' | 'policy-error' | 'legacy-denied' | 'legacy-error';
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The canonical ALLOW/DENY combination (spec15 §8, §123) — a pure function of already
|
|
134
|
+
* evaluated inputs, so it is identical on every surface and independently checkable.
|
|
135
|
+
*
|
|
136
|
+
* - neither a policy nor a legacy expression ⇒ ALLOW (`no-policy`);
|
|
137
|
+
* - a policy that failed to evaluate ⇒ DENY (`policy-error`) — an evaluation error never allows;
|
|
138
|
+
* - a policy whose value is not exactly `true` ⇒ DENY (`policy-denied`);
|
|
139
|
+
* - a legacy expression that failed to evaluate ⇒ DENY (`legacy-error`);
|
|
140
|
+
* - a legacy expression that is falsy / an empty collection ⇒ DENY (`legacy-denied`);
|
|
141
|
+
* - otherwise ALLOW (`allowed`). Both, when present, must pass (conjunction).
|
|
142
|
+
*/
|
|
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;
|
|
172
|
+
/**
|
|
173
|
+
* The distinct policy ids a graph node references for authorization — for dependency
|
|
174
|
+
* analysis and `validateGraph` reference resolution. Total over malformed input.
|
|
175
|
+
*/
|
|
176
|
+
export declare function nodeAuthorizationPolicyRefs(node: unknown): string[];
|
|
177
|
+
//# sourceMappingURL=authorization.d.ts.map
|
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authorization completeness (spec15) — the canonical, portable authorization policy model.
|
|
3
|
+
*
|
|
4
|
+
* Authorization in Axiom is **semantic**: whether a principal may perform a semantic
|
|
5
|
+
* operation is executable meaning, not presentation. It therefore lives in the graph, in
|
|
6
|
+
* `semanticFingerprint`, in authority compatibility, in the Server IR and in conformance —
|
|
7
|
+
* never as a host-language callback (spec15 §6, §7).
|
|
8
|
+
*
|
|
9
|
+
* There is exactly **one** authorization language: an `AuthorizationPolicyDef` with a single
|
|
10
|
+
* boolean `allow` expression over a **closed scope** — `ref('PRINCIPAL')` (the canonical
|
|
11
|
+
* caller), `ref('RESOURCE')` (the semantic object the operation targets, where one exists)
|
|
12
|
+
* and `ref('OPERATION')` (the canonical operation identity). No `StateDef`, no `QueryDef`,
|
|
13
|
+
* no `now` / `uuid` / `random`, no ambient runtime state (spec15 §34). `allow` evaluating to
|
|
14
|
+
* exactly `true` is **ALLOW**; `false`, absence, or any evaluation error is **DENY** — the
|
|
15
|
+
* safe direction is always refusal (spec15 §8, §123).
|
|
16
|
+
*
|
|
17
|
+
* A policy is referenced by id from the surface it protects — `ActionDef.authorizationPolicy`,
|
|
18
|
+
* `QueryDef.authorizationPolicy`, `WorkflowDef.startPolicy` / `instanceAccessPolicy` — so the
|
|
19
|
+
* same evaluator and the same decision model apply everywhere (spec15 §5, §96). The legacy
|
|
20
|
+
* `ActionDef.authorization` boolean expression remains supported and is canonically
|
|
21
|
+
* equivalent to an inline `allow` over `PRINCIPAL`; `ReadPolicyDef` (spec10) remains the
|
|
22
|
+
* row-level read mechanism, unified into query authorization by spec15 Phase D.
|
|
23
|
+
*
|
|
24
|
+
* This module is portable plain data + `Expression` trees; it carries no enforcement (that
|
|
25
|
+
* is Phases C–F). The accessors are **total over any input** so a hand-tampered policy fails
|
|
26
|
+
* closed with a structured diagnostic, never a native exception (spec15 §37).
|
|
27
|
+
*/
|
|
28
|
+
import { OPERATION, PRINCIPAL, RESOURCE } from './authority.js';
|
|
29
|
+
import { EXPRESSION_KINDS } from './expressions.js';
|
|
30
|
+
// --------------------------------------------------------------------- reserved scope ids
|
|
31
|
+
/**
|
|
32
|
+
* The canonical caller. The **same** reserved id `ActionDef.authorization` uses
|
|
33
|
+
* (`'axiom_principal'`, exported from `authority.ts` as `PRINCIPAL`) — write a policy with
|
|
34
|
+
* `field(ref(PRINCIPAL), F_ROLE)`, exactly as a legacy authorization expression.
|
|
35
|
+
*/
|
|
36
|
+
export const AUTHZ_PRINCIPAL_SCOPE = PRINCIPAL;
|
|
37
|
+
/** The semantic object the operation targets (a record, a workflow instance, …), where one exists. */
|
|
38
|
+
export const AUTHZ_RESOURCE_SCOPE = RESOURCE;
|
|
39
|
+
/** The canonical operation identity — `ref(OPERATION)` resolves to an `AuthorizationOperation` string. */
|
|
40
|
+
export const AUTHZ_OPERATION_SCOPE = OPERATION;
|
|
41
|
+
/** Every id a policy `allow` expression's `ref` may resolve. Nothing else is in scope. */
|
|
42
|
+
export const AUTHORIZATION_SCOPE_IDS = [
|
|
43
|
+
AUTHZ_PRINCIPAL_SCOPE,
|
|
44
|
+
AUTHZ_RESOURCE_SCOPE,
|
|
45
|
+
AUTHZ_OPERATION_SCOPE,
|
|
46
|
+
];
|
|
47
|
+
/** Builtins forbidden in a policy expression — authorization must be deterministic (spec15 §34). */
|
|
48
|
+
export const AUTHORIZATION_NONDETERMINISTIC_BUILTINS = new Set([
|
|
49
|
+
'now',
|
|
50
|
+
'uuid',
|
|
51
|
+
'random',
|
|
52
|
+
]);
|
|
53
|
+
// ----------------------------------------------------------------------- operation identity
|
|
54
|
+
/**
|
|
55
|
+
* The closed set of canonical semantic operations an authorization decision is made about
|
|
56
|
+
* (spec15 §98). Policies reason over these, never over transport method names. Enforcement
|
|
57
|
+
* of each arrives with its phase (C: `action.invoke` / `record.*` / `state.*`; D:
|
|
58
|
+
* `query.read`; E: `workflow.*`; F: `live.*` / `subscription.*`).
|
|
59
|
+
*/
|
|
60
|
+
export const AUTHORIZATION_OPERATIONS = [
|
|
61
|
+
'action.invoke',
|
|
62
|
+
'query.read',
|
|
63
|
+
'record.read',
|
|
64
|
+
'record.mutate',
|
|
65
|
+
'state.read',
|
|
66
|
+
'state.mutate',
|
|
67
|
+
'workflow.start',
|
|
68
|
+
'workflow.inspect',
|
|
69
|
+
'workflow.history',
|
|
70
|
+
'workflow.cancel',
|
|
71
|
+
'live.open',
|
|
72
|
+
'live.resume',
|
|
73
|
+
'subscription.open',
|
|
74
|
+
'event.ingress',
|
|
75
|
+
];
|
|
76
|
+
// --------------------------------------------------------------------------- default rule
|
|
77
|
+
/**
|
|
78
|
+
* What a protected surface does when it has **no** attached policy (spec15 §9). Deliberately
|
|
79
|
+
* one canonical rule, applied consistently, never left to the runtime:
|
|
80
|
+
*
|
|
81
|
+
* - a surface whose *current* (pre-0.15) contract is public — an unrestricted `ActionDef`,
|
|
82
|
+
* an unrestricted `QueryDef` — keeps that public contract (`'public'`);
|
|
83
|
+
* - a surface whose current contract is already restricted — a `WorkflowDef` instance
|
|
84
|
+
* operation (0.14 owner-fingerprint), an `ActionDef` with a legacy `authorization`
|
|
85
|
+
* expression, a `QueryDef` with a `ReadPolicyDef` — keeps that restriction;
|
|
86
|
+
* - a *new* privileged surface with no policy fails closed (`'deny'`).
|
|
87
|
+
*
|
|
88
|
+
* These constants name the rule so docs, tests and a future independent runtime agree.
|
|
89
|
+
*/
|
|
90
|
+
export const AUTHORIZATION_DEFAULT = {
|
|
91
|
+
/** No policy, previously-public surface ⇒ still public. */
|
|
92
|
+
PUBLIC_SURFACE: 'public',
|
|
93
|
+
/** No policy, previously-restricted surface ⇒ keep the prior restriction. */
|
|
94
|
+
KEEP_PRIOR_RESTRICTION: 'keep-prior-restriction',
|
|
95
|
+
/** No policy, new privileged surface ⇒ DENY. */
|
|
96
|
+
FAIL_CLOSED: 'deny',
|
|
97
|
+
};
|
|
98
|
+
// -------------------------------------------------------------------------------- accessors
|
|
99
|
+
function isPlainObject(value) {
|
|
100
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
101
|
+
}
|
|
102
|
+
/** Every `Expression` a policy embeds — total over malformed input. */
|
|
103
|
+
export function authorizationPolicyExpressions(policy) {
|
|
104
|
+
if (!isPlainObject(policy))
|
|
105
|
+
return [];
|
|
106
|
+
const allow = policy.allow;
|
|
107
|
+
return isPlainObject(allow) ? [allow] : [];
|
|
108
|
+
}
|
|
109
|
+
/** Walk an expression tree collecting every scope `ref` id and every nondeterministic call. */
|
|
110
|
+
function walkPolicyExpression(expression, refs, nondeterministic, seen = new Set()) {
|
|
111
|
+
if (!isPlainObject(expression) || seen.has(expression))
|
|
112
|
+
return;
|
|
113
|
+
seen.add(expression);
|
|
114
|
+
if (expression.kind === 'ref' && expression.targetId !== undefined) {
|
|
115
|
+
refs.add(String(expression.targetId));
|
|
116
|
+
}
|
|
117
|
+
if (expression.kind === 'call' &&
|
|
118
|
+
typeof expression.function === 'string' &&
|
|
119
|
+
AUTHORIZATION_NONDETERMINISTIC_BUILTINS.has(expression.function)) {
|
|
120
|
+
nondeterministic.add(expression.function);
|
|
121
|
+
}
|
|
122
|
+
if (expression.kind === 'literal')
|
|
123
|
+
return;
|
|
124
|
+
for (const value of Object.values(expression)) {
|
|
125
|
+
if (Array.isArray(value)) {
|
|
126
|
+
for (const entry of value)
|
|
127
|
+
walkPolicyExpression(entry, refs, nondeterministic, seen);
|
|
128
|
+
}
|
|
129
|
+
else if (isPlainObject(value)) {
|
|
130
|
+
walkPolicyExpression(value, refs, nondeterministic, seen);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* A **total** structural + scope check on an `AuthorizationPolicyDef` value — for any input,
|
|
136
|
+
* including a hand-tampered Server IR where the policy is the wrong shape entirely (spec15
|
|
137
|
+
* §37). Cross-node reference resolution (does an `authorizationPolicy` id point at a real
|
|
138
|
+
* policy) is `validateGraph`'s job, not this function's.
|
|
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
|
+
}
|
|
214
|
+
export function authorizationPolicyProblems(policy) {
|
|
215
|
+
const problems = [];
|
|
216
|
+
const pid = isPlainObject(policy) ? String(policy.id ?? '<unknown>') : '<unknown>';
|
|
217
|
+
if (!isPlainObject(policy)) {
|
|
218
|
+
return [{ code: 'AUTHORIZATION_INVALID_POLICY', message: `Authorization policy ${pid} is not an object` }];
|
|
219
|
+
}
|
|
220
|
+
if (!isPlainObject(policy.allow)) {
|
|
221
|
+
problems.push({
|
|
222
|
+
code: 'AUTHORIZATION_INVALID_POLICY',
|
|
223
|
+
message: `Authorization policy ${pid} has no boolean 'allow' expression`,
|
|
224
|
+
});
|
|
225
|
+
return problems;
|
|
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
|
+
}
|
|
233
|
+
const refs = new Set();
|
|
234
|
+
const nondeterministic = new Set();
|
|
235
|
+
walkPolicyExpression(policy.allow, refs, nondeterministic);
|
|
236
|
+
for (const id of refs) {
|
|
237
|
+
if (!AUTHORIZATION_SCOPE_IDS.includes(id)) {
|
|
238
|
+
problems.push({
|
|
239
|
+
code: 'AUTHORIZATION_INVALID_SCOPE',
|
|
240
|
+
message: `Authorization policy ${pid} references ${id}, which is not in the policy scope (PRINCIPAL / RESOURCE / OPERATION)`,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
for (const fn of nondeterministic) {
|
|
245
|
+
problems.push({
|
|
246
|
+
code: 'AUTHORIZATION_NONDETERMINISTIC',
|
|
247
|
+
message: `Authorization policy ${pid} calls ${fn}(), which is not deterministic and not allowed in a policy expression`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return problems;
|
|
251
|
+
}
|
|
252
|
+
function collectScopeFieldReads(expression, principal, resource, operation, seen = new Set()) {
|
|
253
|
+
if (!isPlainObject(expression) || seen.has(expression))
|
|
254
|
+
return;
|
|
255
|
+
seen.add(expression);
|
|
256
|
+
if (expression.kind === 'ref' && String(expression.targetId) === AUTHZ_OPERATION_SCOPE) {
|
|
257
|
+
operation.seen = true;
|
|
258
|
+
}
|
|
259
|
+
if (expression.kind === 'field' &&
|
|
260
|
+
isPlainObject(expression.source) &&
|
|
261
|
+
expression.source.kind === 'ref' &&
|
|
262
|
+
expression.fieldId !== undefined) {
|
|
263
|
+
const target = String(expression.source.targetId);
|
|
264
|
+
if (target === AUTHZ_PRINCIPAL_SCOPE)
|
|
265
|
+
principal.add(String(expression.fieldId));
|
|
266
|
+
else if (target === AUTHZ_RESOURCE_SCOPE)
|
|
267
|
+
resource.add(String(expression.fieldId));
|
|
268
|
+
}
|
|
269
|
+
if (expression.kind === 'literal')
|
|
270
|
+
return;
|
|
271
|
+
for (const value of Object.values(expression)) {
|
|
272
|
+
if (Array.isArray(value)) {
|
|
273
|
+
for (const entry of value)
|
|
274
|
+
collectScopeFieldReads(entry, principal, resource, operation, seen);
|
|
275
|
+
}
|
|
276
|
+
else if (isPlainObject(value)) {
|
|
277
|
+
collectScopeFieldReads(value, principal, resource, operation, seen);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* What an `AuthorizationPolicyDef` depends on — for explainability, static coverage analysis
|
|
283
|
+
* and AI authoring (spec15 §35, §44). Total over malformed input (empty result). Reports
|
|
284
|
+
* *structure only*, never a runtime value.
|
|
285
|
+
*/
|
|
286
|
+
export function authorizationPolicyDependencies(policy) {
|
|
287
|
+
const principal = new Set();
|
|
288
|
+
const resource = new Set();
|
|
289
|
+
const operation = { seen: false };
|
|
290
|
+
const allow = isPlainObject(policy) ? policy.allow : undefined;
|
|
291
|
+
if (isPlainObject(allow)) {
|
|
292
|
+
collectScopeFieldReads(allow, principal, resource, operation);
|
|
293
|
+
}
|
|
294
|
+
let constant = null;
|
|
295
|
+
if (isPlainObject(allow) && allow.kind === 'literal') {
|
|
296
|
+
constant = allow.value === true ? 'always-allow' : 'always-deny';
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
principalFields: [...principal].sort(),
|
|
300
|
+
resourceFields: [...resource].sort(),
|
|
301
|
+
readsOperation: operation.seen,
|
|
302
|
+
constant,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// --------------------------------------------------------------- the decision (spec15 §8)
|
|
306
|
+
export const AUTHORIZATION_DECISIONS = ['ALLOW', 'DENY'];
|
|
307
|
+
/**
|
|
308
|
+
* The canonical ALLOW/DENY combination (spec15 §8, §123) — a pure function of already
|
|
309
|
+
* evaluated inputs, so it is identical on every surface and independently checkable.
|
|
310
|
+
*
|
|
311
|
+
* - neither a policy nor a legacy expression ⇒ ALLOW (`no-policy`);
|
|
312
|
+
* - a policy that failed to evaluate ⇒ DENY (`policy-error`) — an evaluation error never allows;
|
|
313
|
+
* - a policy whose value is not exactly `true` ⇒ DENY (`policy-denied`);
|
|
314
|
+
* - a legacy expression that failed to evaluate ⇒ DENY (`legacy-error`);
|
|
315
|
+
* - a legacy expression that is falsy / an empty collection ⇒ DENY (`legacy-denied`);
|
|
316
|
+
* - otherwise ALLOW (`allowed`). Both, when present, must pass (conjunction).
|
|
317
|
+
*/
|
|
318
|
+
export function decideAuthorization(input) {
|
|
319
|
+
const { policy, legacy } = input;
|
|
320
|
+
if (!policy && !legacy)
|
|
321
|
+
return { decision: 'ALLOW', reason: 'no-policy' };
|
|
322
|
+
if (policy) {
|
|
323
|
+
if (!policy.ok)
|
|
324
|
+
return { decision: 'DENY', reason: 'policy-error' };
|
|
325
|
+
if (policy.value !== true)
|
|
326
|
+
return { decision: 'DENY', reason: 'policy-denied' };
|
|
327
|
+
}
|
|
328
|
+
if (legacy) {
|
|
329
|
+
if (!legacy.ok)
|
|
330
|
+
return { decision: 'DENY', reason: 'legacy-error' };
|
|
331
|
+
const permitted = Array.isArray(legacy.value) ? legacy.value.length > 0 : Boolean(legacy.value);
|
|
332
|
+
if (!permitted)
|
|
333
|
+
return { decision: 'DENY', reason: 'legacy-denied' };
|
|
334
|
+
}
|
|
335
|
+
return { decision: 'ALLOW', reason: 'allowed' };
|
|
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
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* The distinct policy ids a graph node references for authorization — for dependency
|
|
576
|
+
* analysis and `validateGraph` reference resolution. Total over malformed input.
|
|
577
|
+
*/
|
|
578
|
+
export function nodeAuthorizationPolicyRefs(node) {
|
|
579
|
+
if (!isPlainObject(node))
|
|
580
|
+
return [];
|
|
581
|
+
const out = [];
|
|
582
|
+
for (const key of ['authorizationPolicy', 'startPolicy', 'instanceAccessPolicy']) {
|
|
583
|
+
const value = node[key];
|
|
584
|
+
if (typeof value === 'string')
|
|
585
|
+
out.push(value);
|
|
586
|
+
}
|
|
587
|
+
return out;
|
|
588
|
+
}
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -204,5 +204,13 @@ export declare const VALIDATION_CODES: {
|
|
|
204
204
|
readonly migrationTransformImpure: "MIGRATION_TRANSFORM_IMPURE";
|
|
205
205
|
/** A `transform-field` whose declared `toType` does not match the field's type in the target schema, or an `add-field` `populate` whose value cannot satisfy the field (spec11 §77). */
|
|
206
206
|
readonly migrationTransformTypeMismatch: "MIGRATION_TRANSFORM_TYPE_MISMATCH";
|
|
207
|
+
/** An `AuthorizationPolicyDef` with no boolean `allow` expression, or a non-object policy value. */
|
|
208
|
+
readonly authorizationInvalidPolicy: "AUTHORIZATION_INVALID_POLICY";
|
|
209
|
+
/** An `AuthorizationPolicyDef.allow` expression references an id outside the policy scope (`PRINCIPAL` / `RESOURCE` / `OPERATION`). */
|
|
210
|
+
readonly authorizationInvalidScope: "AUTHORIZATION_INVALID_SCOPE";
|
|
211
|
+
/** An `authorizationPolicy` / `startPolicy` / `instanceAccessPolicy` id that does not resolve to an `authorization-policy` node. */
|
|
212
|
+
readonly authorizationUnknownPolicy: "AUTHORIZATION_UNKNOWN_POLICY";
|
|
213
|
+
/** An `AuthorizationPolicyDef.allow` expression calls `now` / `uuid` / `random` — authorization must be deterministic (spec15 §34). */
|
|
214
|
+
readonly authorizationNondeterministic: "AUTHORIZATION_NONDETERMINISTIC";
|
|
207
215
|
};
|
|
208
216
|
//# sourceMappingURL=diagnostics.d.ts.map
|
package/dist/diagnostics.js
CHANGED
|
@@ -200,4 +200,15 @@ export const VALIDATION_CODES = {
|
|
|
200
200
|
migrationTransformImpure: 'MIGRATION_TRANSFORM_IMPURE',
|
|
201
201
|
/** A `transform-field` whose declared `toType` does not match the field's type in the target schema, or an `add-field` `populate` whose value cannot satisfy the field (spec11 §77). */
|
|
202
202
|
migrationTransformTypeMismatch: 'MIGRATION_TRANSFORM_TYPE_MISMATCH',
|
|
203
|
+
// Authorization completeness (0.15). `validateGraph` rejects a malformed or inconsistent
|
|
204
|
+
// authorization policy before it can be compiled or executed; every failure is structured,
|
|
205
|
+
// never a native exception (spec15 §36, §37, §38).
|
|
206
|
+
/** An `AuthorizationPolicyDef` with no boolean `allow` expression, or a non-object policy value. */
|
|
207
|
+
authorizationInvalidPolicy: 'AUTHORIZATION_INVALID_POLICY',
|
|
208
|
+
/** An `AuthorizationPolicyDef.allow` expression references an id outside the policy scope (`PRINCIPAL` / `RESOURCE` / `OPERATION`). */
|
|
209
|
+
authorizationInvalidScope: 'AUTHORIZATION_INVALID_SCOPE',
|
|
210
|
+
/** An `authorizationPolicy` / `startPolicy` / `instanceAccessPolicy` id that does not resolve to an `authorization-policy` node. */
|
|
211
|
+
authorizationUnknownPolicy: 'AUTHORIZATION_UNKNOWN_POLICY',
|
|
212
|
+
/** An `AuthorizationPolicyDef.allow` expression calls `now` / `uuid` / `random` — authorization must be deterministic (spec15 §34). */
|
|
213
|
+
authorizationNondeterministic: 'AUTHORIZATION_NONDETERMINISTIC',
|
|
203
214
|
};
|
package/dist/graph.js
CHANGED
|
@@ -23,7 +23,7 @@ export class ApplicationGraph {
|
|
|
23
23
|
/** Bumped by every change, so the derived edge index can never serve stale data. */
|
|
24
24
|
revision = 0;
|
|
25
25
|
semanticIndex;
|
|
26
|
-
constructor(id, name, version = '0.
|
|
26
|
+
constructor(id, name, version = '0.15.0') {
|
|
27
27
|
this.data = { id, name, version, nodes: {}, edges: {} };
|
|
28
28
|
}
|
|
29
29
|
get id() {
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from './storage.js';
|
|
|
16
16
|
export * from './query.js';
|
|
17
17
|
export * from './live-query.js';
|
|
18
18
|
export * from './workflows.js';
|
|
19
|
+
export * from './authorization.js';
|
|
19
20
|
export * from './relationships.js';
|
|
20
21
|
export * from './read-policy.js';
|
|
21
22
|
export * from './migration.js';
|
package/dist/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from './storage.js';
|
|
|
16
16
|
export * from './query.js';
|
|
17
17
|
export * from './live-query.js';
|
|
18
18
|
export * from './workflows.js';
|
|
19
|
+
export * from './authorization.js';
|
|
19
20
|
export * from './relationships.js';
|
|
20
21
|
export * from './read-policy.js';
|
|
21
22
|
export * from './migration.js';
|
package/dist/nodes.d.ts
CHANGED
|
@@ -229,6 +229,13 @@ export interface ActionDef extends NodeBase {
|
|
|
229
229
|
* claiming to. `requiresConfirmation` is UX and is not an authorization mechanism.
|
|
230
230
|
*/
|
|
231
231
|
authorization?: Expression;
|
|
232
|
+
/**
|
|
233
|
+
* The `AuthorizationPolicyDef` id governing `action.invoke` (spec15). The canonical 0.15
|
|
234
|
+
* form of `authorization` above: one policy language, one evaluator. If both are present
|
|
235
|
+
* the effective decision is their conjunction (ALLOW iff both ALLOW). Absent + no legacy
|
|
236
|
+
* `authorization` ⇒ the action's pre-0.15 public contract is preserved (spec15 §9).
|
|
237
|
+
*/
|
|
238
|
+
authorizationPolicy?: NodeId;
|
|
232
239
|
/** What the confirmation says, when a plain message is not enough. */
|
|
233
240
|
confirmation?: ConfirmationPresentation;
|
|
234
241
|
/** Restricts which invocation sources may reach this action. Absent means both. */
|
package/dist/query.d.ts
CHANGED
|
@@ -200,6 +200,12 @@ export interface QueryDef extends NodeBase {
|
|
|
200
200
|
pagination?: QueryPagination;
|
|
201
201
|
/** The `ReadPolicyDef` whose predicate is AND-ed into `filter` before execution. */
|
|
202
202
|
readPolicyId?: NodeId;
|
|
203
|
+
/**
|
|
204
|
+
* The `AuthorizationPolicyDef` id governing `query.read` — whether this principal may run
|
|
205
|
+
* the query at all (spec15), distinct from `readPolicyId` which filters *which rows* the
|
|
206
|
+
* result contains. Absent ⇒ the query's pre-0.15 contract is preserved (spec15 §9).
|
|
207
|
+
*/
|
|
208
|
+
authorizationPolicy?: NodeId;
|
|
203
209
|
}
|
|
204
210
|
export declare function queryPaginationStrategy(query: QueryDef): QueryPaginationStrategy;
|
|
205
211
|
export declare function queryMaxPageSize(query: QueryDef): number;
|
|
@@ -33,6 +33,12 @@ import type { ApplicationGraph } from './graph.js';
|
|
|
33
33
|
* - `StorageDef` — `readAuthorization` / `uploadAuthorization` expressions, `retry`.
|
|
34
34
|
* - `RelationshipDef` — endpoints and cardinality (also in the schema fingerprint; repeated
|
|
35
35
|
* here so a semantic-only comparison is self-contained).
|
|
36
|
+
* - `AuthorizationPolicyDef` — the `allow` expression (spec15). Whether a principal may
|
|
37
|
+
* perform a semantic operation is executable meaning: a policy edited from ALLOW to DENY
|
|
38
|
+
* moves the fingerprint and makes a mixed-build authority incompatible (spec15 §6, §45,
|
|
39
|
+
* §46). The `authorizationPolicy` / `startPolicy` / `instanceAccessPolicy` id an action /
|
|
40
|
+
* query / workflow references is a field of those already-projected nodes, so a re-pointed
|
|
41
|
+
* reference moves the fingerprint too.
|
|
36
42
|
* - `WorkflowDef` — `inputs`, `bindings`, `entry`, and every step's kind, control-flow edges
|
|
37
43
|
* and step-specific executable semantics (the `ActionDef` / `EventDef` a step targets, an
|
|
38
44
|
* `action` step's argument expressions / `retry` policy, a `wait-event` step's correlation
|
|
@@ -65,7 +71,7 @@ export declare const SEMANTIC_FINGERPRINT_VERSION = 1;
|
|
|
65
71
|
* so a future primitive cannot be added to one and silently omitted from the other
|
|
66
72
|
* (spec14pt3 §189, §190 — the deeper architectural correction behind Phase 22 F3).
|
|
67
73
|
*/
|
|
68
|
-
export declare const EXECUTABLE_KINDS: readonly ["action", "integration", "integration-operation", "trigger", "event", "subscription", "read-policy", "query", "expression", "constraint", "transition-constraint", "storage", "relationship", "workflow"];
|
|
74
|
+
export declare const EXECUTABLE_KINDS: readonly ["action", "integration", "integration-operation", "trigger", "event", "subscription", "read-policy", "query", "expression", "constraint", "transition-constraint", "storage", "relationship", "workflow", "authorization-policy"];
|
|
69
75
|
export type ExecutableKind = (typeof EXECUTABLE_KINDS)[number];
|
|
70
76
|
export interface SemanticProjection {
|
|
71
77
|
fingerprintVersion: number;
|
|
@@ -94,6 +100,15 @@ export interface AuthorityCompatibilityKey {
|
|
|
94
100
|
schemaFingerprint: string;
|
|
95
101
|
serverContract: string;
|
|
96
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;
|
|
97
112
|
}
|
|
98
113
|
export declare function authorityCompatibilityKey(parts: AuthorityCompatibilityKey): AuthorityCompatibilityKey;
|
|
99
114
|
/** A stable, comparable string form for storing on a durable work item (spec12 §43). */
|
|
@@ -36,6 +36,12 @@ import { canonicalWorkflowForFingerprint } from './workflows.js';
|
|
|
36
36
|
* - `StorageDef` — `readAuthorization` / `uploadAuthorization` expressions, `retry`.
|
|
37
37
|
* - `RelationshipDef` — endpoints and cardinality (also in the schema fingerprint; repeated
|
|
38
38
|
* here so a semantic-only comparison is self-contained).
|
|
39
|
+
* - `AuthorizationPolicyDef` — the `allow` expression (spec15). Whether a principal may
|
|
40
|
+
* perform a semantic operation is executable meaning: a policy edited from ALLOW to DENY
|
|
41
|
+
* moves the fingerprint and makes a mixed-build authority incompatible (spec15 §6, §45,
|
|
42
|
+
* §46). The `authorizationPolicy` / `startPolicy` / `instanceAccessPolicy` id an action /
|
|
43
|
+
* query / workflow references is a field of those already-projected nodes, so a re-pointed
|
|
44
|
+
* reference moves the fingerprint too.
|
|
39
45
|
* - `WorkflowDef` — `inputs`, `bindings`, `entry`, and every step's kind, control-flow edges
|
|
40
46
|
* and step-specific executable semantics (the `ActionDef` / `EventDef` a step targets, an
|
|
41
47
|
* `action` step's argument expressions / `retry` policy, a `wait-event` step's correlation
|
|
@@ -83,6 +89,7 @@ export const EXECUTABLE_KINDS = [
|
|
|
83
89
|
'storage',
|
|
84
90
|
'relationship',
|
|
85
91
|
'workflow',
|
|
92
|
+
'authorization-policy',
|
|
86
93
|
];
|
|
87
94
|
/** Keys removed everywhere in the tree — human metadata, never executable meaning. */
|
|
88
95
|
const NON_SEMANTIC_KEYS = new Set(['name', 'description', 'label', 'metadata', AUTHORING_METADATA_KEY]);
|
|
@@ -131,6 +138,7 @@ export function authorityCompatibilityKey(parts) {
|
|
|
131
138
|
schemaFingerprint: parts.schemaFingerprint,
|
|
132
139
|
serverContract: parts.serverContract,
|
|
133
140
|
semanticFingerprint: parts.semanticFingerprint,
|
|
141
|
+
...(parts.authorizationRuntime !== undefined ? { authorizationRuntime: parts.authorizationRuntime } : {}),
|
|
134
142
|
};
|
|
135
143
|
}
|
|
136
144
|
/** A stable, comparable string form for storing on a durable work item (spec12 §43). */
|
|
@@ -148,7 +156,10 @@ export function compareAuthorityCompatibility(a, b) {
|
|
|
148
156
|
'schemaFingerprint',
|
|
149
157
|
'serverContract',
|
|
150
158
|
'semanticFingerprint',
|
|
159
|
+
'authorizationRuntime',
|
|
151
160
|
];
|
|
152
|
-
|
|
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));
|
|
153
164
|
return { compatible: mismatches.length === 0, mismatches };
|
|
154
165
|
}
|
package/dist/server-ir.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { RelationshipDef } from './relationships.js';
|
|
|
12
12
|
import type { ReadPolicyDef } from './read-policy.js';
|
|
13
13
|
import type { MigrationDef } from './migration.js';
|
|
14
14
|
import type { WorkflowDef } from './workflows.js';
|
|
15
|
+
import type { AuthorizationPolicyDef } from './authorization.js';
|
|
15
16
|
/**
|
|
16
17
|
* The contracts a Server IR may declare. A runtime that does not recognize the value MUST
|
|
17
18
|
* refuse the IR rather than interpret it partially.
|
|
@@ -27,7 +28,7 @@ import type { WorkflowDef } from './workflows.js';
|
|
|
27
28
|
* Every existing application therefore still compiles to a byte-identical
|
|
28
29
|
* `axiom.server.v1` document, and the frozen conformance fixtures stay frozen.
|
|
29
30
|
*/
|
|
30
|
-
export declare const SERVER_IR_CONTRACTS: readonly ["axiom.server.v1", "axiom.server.v2", "axiom.server.v3", "axiom.server.v4", "axiom.server.v5", "axiom.server.v6", "axiom.server.v7", "axiom.server.v8"];
|
|
31
|
+
export declare const SERVER_IR_CONTRACTS: readonly ["axiom.server.v1", "axiom.server.v2", "axiom.server.v3", "axiom.server.v4", "axiom.server.v5", "axiom.server.v6", "axiom.server.v7", "axiom.server.v8", "axiom.server.v9"];
|
|
31
32
|
export type ServerIRContract = (typeof SERVER_IR_CONTRACTS)[number];
|
|
32
33
|
/** The oldest contract, and the one a document declares unless it needs more. */
|
|
33
34
|
export declare const SERVER_IR_CONTRACT: ServerIRContract;
|
|
@@ -121,6 +122,37 @@ export declare function usesMigrationVocabulary(ir: {
|
|
|
121
122
|
export declare function usesWorkflowVocabulary(ir: {
|
|
122
123
|
workflows?: readonly unknown[];
|
|
123
124
|
}): boolean;
|
|
125
|
+
/**
|
|
126
|
+
* Whether a document uses 0.15 authorization vocabulary — any `AuthorizationPolicyDef`, or
|
|
127
|
+
* any `authorizationPolicy` / `startPolicy` / `instanceAccessPolicy` reference on an action,
|
|
128
|
+
* query or workflow. Any of it present requires `axiom.server.v9`; a document with none is
|
|
129
|
+
* byte-identical to the v1–v8 document it always was (spec15 §70). Total over a tampered IR.
|
|
130
|
+
*/
|
|
131
|
+
export declare function usesAuthorizationVocabulary(ir: {
|
|
132
|
+
authorizationPolicies?: readonly unknown[];
|
|
133
|
+
actions?: Record<string, {
|
|
134
|
+
authorizationPolicy?: unknown;
|
|
135
|
+
}>;
|
|
136
|
+
queries?: readonly {
|
|
137
|
+
authorizationPolicy?: unknown;
|
|
138
|
+
}[];
|
|
139
|
+
workflows?: readonly {
|
|
140
|
+
startPolicy?: unknown;
|
|
141
|
+
instanceAccessPolicy?: unknown;
|
|
142
|
+
}[];
|
|
143
|
+
}): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Authorization vocabulary whose *enforcement* has not shipped yet — the admission gate a
|
|
146
|
+
* build uses to fail closed rather than run a declared policy as a silent no-op (spec4 §4,
|
|
147
|
+
* spec15 §128). spec15 landed enforcement in phases: C — `ActionDef.authorizationPolicy`
|
|
148
|
+
* (and every state / provider-record mutation it drives); D — `QueryDef.authorizationPolicy`
|
|
149
|
+
* (`query.read`, one-shot / live-open / query-operation); E — `WorkflowDef.startPolicy` /
|
|
150
|
+
* `instanceAccessPolicy` (`workflow.start` / `.inspect` / `.history` / `.cancel`). Every
|
|
151
|
+
* `AuthorizationPolicyDef` reference the graph vocabulary defines is now enforced, so this
|
|
152
|
+
* predicate is `false` for every valid IR. It is kept as the extension point: a later phase
|
|
153
|
+
* that introduces authorization vocabulary ahead of its enforcement re-populates it.
|
|
154
|
+
*/
|
|
155
|
+
export declare function usesUnenforcedAuthorizationVocabulary(_ir: unknown): boolean;
|
|
124
156
|
/**
|
|
125
157
|
* Whether a document uses 0.10's semantic data-access vocabulary — a `QueryDef`, a
|
|
126
158
|
* `RelationshipDef`, a `ReadPolicyDef`, or a `query` operation inside an action. A v5
|
|
@@ -149,6 +181,7 @@ export declare function serverIRExpressions(ir: {
|
|
|
149
181
|
storages?: readonly StorageDef[];
|
|
150
182
|
queries?: readonly QueryDef[];
|
|
151
183
|
readPolicies?: readonly ReadPolicyDef[];
|
|
184
|
+
authorizationPolicies?: readonly AuthorizationPolicyDef[];
|
|
152
185
|
migrations?: readonly MigrationDef[];
|
|
153
186
|
workflows?: readonly WorkflowDef[];
|
|
154
187
|
}): Expression[];
|
|
@@ -243,6 +276,14 @@ export interface ServerIR {
|
|
|
243
276
|
* cannot satisfy one by claiming to.
|
|
244
277
|
*/
|
|
245
278
|
readPolicies?: ReadPolicyDef[];
|
|
279
|
+
/**
|
|
280
|
+
* Canonical authorization policies (spec15, `axiom.server.v9`). Each is a boolean `allow`
|
|
281
|
+
* expression over the closed scope `PRINCIPAL` / `RESOURCE` / `OPERATION`; a surface points
|
|
282
|
+
* at one by id (`ActionDef.authorizationPolicy`, `QueryDef.authorizationPolicy`,
|
|
283
|
+
* `WorkflowDef.startPolicy` / `instanceAccessPolicy`). Portable plain data — no callback,
|
|
284
|
+
* no host object. Present only in an `axiom.server.v9` document.
|
|
285
|
+
*/
|
|
286
|
+
authorizationPolicies?: AuthorizationPolicyDef[];
|
|
246
287
|
/**
|
|
247
288
|
* Semantic migrations between consecutive schema versions (spec11 §14). Present only in an
|
|
248
289
|
* `axiom.server.v7` document. Portable plain data — a closed operation vocabulary and
|
package/dist/server-ir.js
CHANGED
|
@@ -2,6 +2,7 @@ import { walkExpression } from './expressions.js';
|
|
|
2
2
|
import { queryExpressions } from './query.js';
|
|
3
3
|
import { migrationExpressions } from './migration.js';
|
|
4
4
|
import { workflowExpressions } from './workflows.js';
|
|
5
|
+
import { authorizationPolicyExpressions } from './authorization.js';
|
|
5
6
|
/**
|
|
6
7
|
* The contracts a Server IR may declare. A runtime that does not recognize the value MUST
|
|
7
8
|
* refuse the IR rather than interpret it partially.
|
|
@@ -26,11 +27,12 @@ export const SERVER_IR_CONTRACTS = [
|
|
|
26
27
|
'axiom.server.v6',
|
|
27
28
|
'axiom.server.v7',
|
|
28
29
|
'axiom.server.v8',
|
|
30
|
+
'axiom.server.v9',
|
|
29
31
|
];
|
|
30
32
|
/** The oldest contract, and the one a document declares unless it needs more. */
|
|
31
33
|
export const SERVER_IR_CONTRACT = 'axiom.server.v1';
|
|
32
34
|
/** The newest contract this implementation produces and executes. */
|
|
33
|
-
export const SERVER_IR_LATEST_CONTRACT = 'axiom.server.
|
|
35
|
+
export const SERVER_IR_LATEST_CONTRACT = 'axiom.server.v9';
|
|
34
36
|
/** Operation kinds no contract before `axiom.server.v5` contains. */
|
|
35
37
|
export const SERVER_IR_V5_OPERATION_KINDS = [
|
|
36
38
|
'blob-metadata',
|
|
@@ -146,6 +148,49 @@ export function usesWorkflowVocabulary(ir) {
|
|
|
146
148
|
// but is not workflow vocabulary); a malformed present value is refused at admission.
|
|
147
149
|
return Array.isArray(ir.workflows) && ir.workflows.length > 0;
|
|
148
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Whether a document uses 0.15 authorization vocabulary — any `AuthorizationPolicyDef`, or
|
|
153
|
+
* any `authorizationPolicy` / `startPolicy` / `instanceAccessPolicy` reference on an action,
|
|
154
|
+
* query or workflow. Any of it present requires `axiom.server.v9`; a document with none is
|
|
155
|
+
* byte-identical to the v1–v8 document it always was (spec15 §70). Total over a tampered IR.
|
|
156
|
+
*/
|
|
157
|
+
export function usesAuthorizationVocabulary(ir) {
|
|
158
|
+
if (Array.isArray(ir.authorizationPolicies) && ir.authorizationPolicies.length > 0)
|
|
159
|
+
return true;
|
|
160
|
+
for (const action of Object.values(ir.actions ?? {})) {
|
|
161
|
+
if (action && typeof action === 'object' && action.authorizationPolicy !== undefined) {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const query of Array.isArray(ir.queries) ? ir.queries : []) {
|
|
166
|
+
if (query && typeof query === 'object' && query.authorizationPolicy !== undefined) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
for (const workflow of Array.isArray(ir.workflows) ? ir.workflows : []) {
|
|
171
|
+
if (workflow &&
|
|
172
|
+
typeof workflow === 'object' &&
|
|
173
|
+
(workflow.startPolicy !== undefined ||
|
|
174
|
+
workflow.instanceAccessPolicy !== undefined)) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Authorization vocabulary whose *enforcement* has not shipped yet — the admission gate a
|
|
182
|
+
* build uses to fail closed rather than run a declared policy as a silent no-op (spec4 §4,
|
|
183
|
+
* spec15 §128). spec15 landed enforcement in phases: C — `ActionDef.authorizationPolicy`
|
|
184
|
+
* (and every state / provider-record mutation it drives); D — `QueryDef.authorizationPolicy`
|
|
185
|
+
* (`query.read`, one-shot / live-open / query-operation); E — `WorkflowDef.startPolicy` /
|
|
186
|
+
* `instanceAccessPolicy` (`workflow.start` / `.inspect` / `.history` / `.cancel`). Every
|
|
187
|
+
* `AuthorizationPolicyDef` reference the graph vocabulary defines is now enforced, so this
|
|
188
|
+
* predicate is `false` for every valid IR. It is kept as the extension point: a later phase
|
|
189
|
+
* that introduces authorization vocabulary ahead of its enforcement re-populates it.
|
|
190
|
+
*/
|
|
191
|
+
export function usesUnenforcedAuthorizationVocabulary(_ir) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
149
194
|
/**
|
|
150
195
|
* Whether a document uses 0.10's semantic data-access vocabulary — a `QueryDef`, a
|
|
151
196
|
* `RelationshipDef`, a `ReadPolicyDef`, or a `query` operation inside an action. A v5
|
|
@@ -210,6 +255,10 @@ export function serverIRExpressions(ir) {
|
|
|
210
255
|
for (const policy of ir.readPolicies ?? []) {
|
|
211
256
|
found.push(policy.predicate);
|
|
212
257
|
}
|
|
258
|
+
const authorizationPolicies = ir.authorizationPolicies;
|
|
259
|
+
for (const policy of Array.isArray(authorizationPolicies) ? authorizationPolicies : []) {
|
|
260
|
+
found.push(...authorizationPolicyExpressions(policy));
|
|
261
|
+
}
|
|
213
262
|
for (const migration of ir.migrations ?? []) {
|
|
214
263
|
found.push(...migrationExpressions(migration));
|
|
215
264
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -12,11 +12,12 @@ import type { RelationshipDef } from './relationships.js';
|
|
|
12
12
|
import type { ReadPolicyDef } from './read-policy.js';
|
|
13
13
|
import type { MigrationDef } from './migration.js';
|
|
14
14
|
import type { WorkflowDef } from './workflows.js';
|
|
15
|
-
|
|
15
|
+
import type { AuthorizationPolicyDef } from './authorization.js';
|
|
16
|
+
export type SemanticNodeKind = 'entity' | 'state' | 'action' | 'constraint' | 'transition-constraint' | 'route' | 'expression' | 'integration' | 'integration-operation' | 'event' | 'trigger' | 'subscription' | 'storage' | 'query' | 'relationship' | 'read-policy' | 'migration' | 'workflow' | 'authorization-policy';
|
|
16
17
|
/** Every semantic node kind, enumerated so tests can walk them. */
|
|
17
18
|
export declare const SEMANTIC_NODE_KINDS: readonly SemanticNodeKind[];
|
|
18
19
|
export type NodeKind = SemanticNodeKind | UINodeKind;
|
|
19
|
-
export type AnyNode = EntityDef | StateDef | ActionDef | ConstraintDef | TransitionConstraintDef | RouteDef | ExpressionDef | IntegrationDef | IntegrationOperationDef | EventDef | TriggerDef | SubscriptionDef | StorageDef | QueryDef | RelationshipDef | ReadPolicyDef | MigrationDef | WorkflowDef | UINode;
|
|
20
|
+
export type AnyNode = EntityDef | StateDef | ActionDef | ConstraintDef | TransitionConstraintDef | RouteDef | ExpressionDef | IntegrationDef | IntegrationOperationDef | EventDef | TriggerDef | SubscriptionDef | StorageDef | QueryDef | RelationshipDef | ReadPolicyDef | MigrationDef | WorkflowDef | AuthorizationPolicyDef | UINode;
|
|
20
21
|
export type NodeOfKind<K extends NodeKind> = Extract<AnyNode, {
|
|
21
22
|
kind: K;
|
|
22
23
|
}>;
|
package/dist/types.js
CHANGED
package/dist/validate.js
CHANGED
|
@@ -6,6 +6,7 @@ import { BLOB_REF_FIELDS } from './storage.js';
|
|
|
6
6
|
import { queryPaginationStrategy, sortKeyDirection } from './query.js';
|
|
7
7
|
import { queryStateReferences } from './live-query.js';
|
|
8
8
|
import { WORKFLOW_EVENT_SCOPE, WORKFLOW_PRINCIPAL_SCOPE, WORKFLOW_STEP_TYPES, workflowHasCycle, workflowReachableSteps, workflowStepById, workflowStepExpressions, workflowStepSuccessors, } from './workflows.js';
|
|
9
|
+
import { authorizationPolicyProblems } from './authorization.js';
|
|
9
10
|
import { relationshipIsToOne } from './relationships.js';
|
|
10
11
|
import { validateMigrations } from './validate-migration.js';
|
|
11
12
|
import { collectionType, entityType } from './type-ref.js';
|
|
@@ -176,6 +177,9 @@ function validateNode(node, context) {
|
|
|
176
177
|
case 'workflow':
|
|
177
178
|
validateWorkflow(node, context);
|
|
178
179
|
return;
|
|
180
|
+
case 'authorization-policy':
|
|
181
|
+
validateAuthorizationPolicyNode(node, context);
|
|
182
|
+
return;
|
|
179
183
|
case 'migration':
|
|
180
184
|
// A `MigrationDef` is validated by the graph-level `validateMigrations` pass, which
|
|
181
185
|
// needs every migration and `graph.schemaVersion` at once — chain contiguity and
|
|
@@ -265,6 +269,7 @@ function validateAction(action, context) {
|
|
|
265
269
|
if (action.authorization) {
|
|
266
270
|
validateExpression(action.authorization, action.id, context, new Set());
|
|
267
271
|
}
|
|
272
|
+
requireAuthorizationPolicy(action.authorizationPolicy, action.id, context);
|
|
268
273
|
if (action.invocation?.allowedSources && action.invocation.allowedSources.length === 0) {
|
|
269
274
|
context.errors.push({
|
|
270
275
|
code: VALIDATION_CODES.invalidInvocationSource,
|
|
@@ -836,6 +841,35 @@ function validateStorage(storage, context) {
|
|
|
836
841
|
* its row scope does not collide. Whether the predicate may read `PRINCIPAL` is the
|
|
837
842
|
* authority boundary's job, checked in `validate-authority.ts`.
|
|
838
843
|
*/
|
|
844
|
+
/**
|
|
845
|
+
* spec15 — an `AuthorizationPolicyDef`. Total over any input: a malformed policy produces a
|
|
846
|
+
* structured `AUTHORIZATION_*` diagnostic, never a native exception (spec15 §36, §37).
|
|
847
|
+
* `authorizationPolicyProblems` (core) covers structure + closed scope + determinism; the
|
|
848
|
+
* cross-node ref check (does an `authorizationPolicy` id point here) is on the referencing
|
|
849
|
+
* node's validator.
|
|
850
|
+
*/
|
|
851
|
+
function validateAuthorizationPolicyNode(policy, context) {
|
|
852
|
+
for (const problem of authorizationPolicyProblems(policy)) {
|
|
853
|
+
context.errors.push({
|
|
854
|
+
code: problem.code,
|
|
855
|
+
message: problem.message,
|
|
856
|
+
nodeId: policy?.id,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
/** Resolve an `authorizationPolicy` / `startPolicy` / `instanceAccessPolicy` id, if present. */
|
|
861
|
+
function requireAuthorizationPolicy(id, ownerId, context) {
|
|
862
|
+
if (id === undefined || id === null)
|
|
863
|
+
return;
|
|
864
|
+
const node = typeof id === 'string' ? context.nodes.get(id) : undefined;
|
|
865
|
+
if (!node || node.kind !== 'authorization-policy') {
|
|
866
|
+
context.errors.push({
|
|
867
|
+
code: VALIDATION_CODES.authorizationUnknownPolicy,
|
|
868
|
+
message: `${ownerId} references authorization policy ${String(id)}, which is not an authorization-policy node`,
|
|
869
|
+
nodeId: ownerId,
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
839
873
|
function validateReadPolicy(policy, context) {
|
|
840
874
|
requireKind(policy.entityId, 'entity', policy.id, context, VALIDATION_CODES.unknownQueryEntity);
|
|
841
875
|
const entity = context.nodes.get(policy.entityId);
|
|
@@ -903,6 +937,8 @@ function policyRowScope(rowScopeId, entityId, ownerId, context, entity) {
|
|
|
903
937
|
* diagnostic — never a thrown `TypeError` on a malformed step.
|
|
904
938
|
*/
|
|
905
939
|
function validateWorkflow(workflow, context) {
|
|
940
|
+
requireAuthorizationPolicy(workflow.startPolicy, workflow.id, context);
|
|
941
|
+
requireAuthorizationPolicy(workflow.instanceAccessPolicy, workflow.id, context);
|
|
906
942
|
const steps = Array.isArray(workflow.steps) ? workflow.steps : [];
|
|
907
943
|
const stepIds = new Set();
|
|
908
944
|
for (const step of steps) {
|
|
@@ -1217,6 +1253,7 @@ function fieldTypeOf(fieldId, context) {
|
|
|
1217
1253
|
*/
|
|
1218
1254
|
function validateQuery(query, context) {
|
|
1219
1255
|
requireKind(query.source, 'entity', query.id, context, VALIDATION_CODES.unknownQueryEntity);
|
|
1256
|
+
requireAuthorizationPolicy(query.authorizationPolicy, query.id, context);
|
|
1220
1257
|
const source = context.nodes.get(query.source);
|
|
1221
1258
|
const sourceEntity = source?.kind === 'entity' ? source : undefined;
|
|
1222
1259
|
// The base scope every query expression is evaluated in: one source row, plus the typed
|
package/dist/workflows.d.ts
CHANGED
|
@@ -108,6 +108,21 @@ export interface WorkflowDef extends NodeBase {
|
|
|
108
108
|
bindings?: WorkflowBinding[];
|
|
109
109
|
entry: NodeId;
|
|
110
110
|
steps: WorkflowStep[];
|
|
111
|
+
/**
|
|
112
|
+
* The `AuthorizationPolicyDef` id governing `workflow.start` (spec15 §100). Ability to
|
|
113
|
+
* discover a `WorkflowDef` is not ability to start it. Absent ⇒ any principal may start
|
|
114
|
+
* it (its pre-0.15 contract), separate from each action step's own authorization, which
|
|
115
|
+
* is always re-evaluated (spec15 §10, §101).
|
|
116
|
+
*/
|
|
117
|
+
startPolicy?: NodeId;
|
|
118
|
+
/**
|
|
119
|
+
* The `AuthorizationPolicyDef` id governing `workflow.inspect` / `workflow.history` /
|
|
120
|
+
* `workflow.cancel` on a *running instance* (spec15 §13-§15). Absent ⇒ the 0.14
|
|
121
|
+
* owner-fingerprint rule (the caller's principal fingerprint must equal the instance's).
|
|
122
|
+
* A policy may broaden this, but only explicitly — a role like `admin` never bypasses
|
|
123
|
+
* owner-only unless the policy says so (spec15 §14, §74).
|
|
124
|
+
*/
|
|
125
|
+
instanceAccessPolicy?: NodeId;
|
|
111
126
|
}
|
|
112
127
|
/**
|
|
113
128
|
* Whether a value is a structurally recognizable workflow step — an object with an `id` and
|