@gp2f/client-sdk 1.0.4 → 1.0.5
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/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/policy-builder.d.ts +163 -0
- package/dist/policy-builder.js +249 -0
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/policy-builder.ts +322 -0
package/dist/index.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export { UndoButton } from "./UndoButton";
|
|
|
7
7
|
export type { UndoButtonProps } from "./UndoButton";
|
|
8
8
|
export { MergeModal } from "./MergeModal";
|
|
9
9
|
export type { MergeModalProps } from "./MergeModal";
|
|
10
|
+
export { p, PolicyBuilder, FieldBuilder, VibeBuilder } from "./policy-builder";
|
|
11
|
+
export type { AstNode, Builder } from "./policy-builder";
|
|
10
12
|
/**
|
|
11
13
|
* The shape of the lazily-loaded policy engine module.
|
|
12
14
|
*
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export { Gp2fClient, applyOptimisticUpdate } from "./client";
|
|
|
4
4
|
export { ReconciliationBanner } from "./ReconciliationBanner";
|
|
5
5
|
export { UndoButton } from "./UndoButton";
|
|
6
6
|
export { MergeModal } from "./MergeModal";
|
|
7
|
+
// ── Policy Builder ────────────────────────────────────────────────────────────
|
|
8
|
+
export { p, PolicyBuilder, FieldBuilder, VibeBuilder } from "./policy-builder";
|
|
7
9
|
/**
|
|
8
10
|
* Lazily load the GP2F WASM policy engine.
|
|
9
11
|
*
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fluent Policy Builder API for constructing GP2F policy AST nodes.
|
|
3
|
+
*
|
|
4
|
+
* Provides a chainable, type-safe alternative to writing raw JSON AST objects.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { p } from '@gp2f/client-sdk';
|
|
9
|
+
*
|
|
10
|
+
* const policy = p.and(
|
|
11
|
+
* p.field('/user/role').eq('admin'),
|
|
12
|
+
* p.field('/session/active').equal('true'),
|
|
13
|
+
* p.exists('/session/token'),
|
|
14
|
+
* );
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* A node in the GP2F policy AST.
|
|
19
|
+
*
|
|
20
|
+
* Mirrors `policy_core::AstNode` from Rust.
|
|
21
|
+
*/
|
|
22
|
+
export interface AstNode {
|
|
23
|
+
/** The operation this node performs. */
|
|
24
|
+
kind: string;
|
|
25
|
+
/** Child nodes for composite operators (And, Or, Not, comparison ops). */
|
|
26
|
+
children?: AstNode[];
|
|
27
|
+
/** JSON-pointer path used by Field and Exists nodes (e.g. `/user/role`). */
|
|
28
|
+
path?: string;
|
|
29
|
+
/** String-encoded scalar value for leaf nodes (e.g. `"admin"`, `"42"`). */
|
|
30
|
+
value?: string;
|
|
31
|
+
/** Name of the external function – used only by Call nodes. */
|
|
32
|
+
callName?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Shared interface for all builder objects that can produce an AstNode.
|
|
36
|
+
*/
|
|
37
|
+
export interface Builder {
|
|
38
|
+
build(): AstNode;
|
|
39
|
+
toJSON(): AstNode;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A builder for field-specific policy assertions.
|
|
43
|
+
*
|
|
44
|
+
* Obtained via `p.field('/some/path')`.
|
|
45
|
+
*/
|
|
46
|
+
export declare class FieldBuilder implements Builder {
|
|
47
|
+
private readonly _path;
|
|
48
|
+
constructor(_path: string);
|
|
49
|
+
/** Assert that the field equals `value`. */
|
|
50
|
+
equal(value: string | number): AstNode;
|
|
51
|
+
/** Alias for {@link equal}. */
|
|
52
|
+
eq(value: string | number): AstNode;
|
|
53
|
+
/** Assert that the field does not equal `value`. */
|
|
54
|
+
notEqual(value: string | number): AstNode;
|
|
55
|
+
/** Alias for {@link notEqual}. */
|
|
56
|
+
neq(value: string | number): AstNode;
|
|
57
|
+
/** Assert that the field is greater than `value`. */
|
|
58
|
+
greaterThan(value: string | number): AstNode;
|
|
59
|
+
/** Alias for {@link greaterThan}. */
|
|
60
|
+
gt(value: string | number): AstNode;
|
|
61
|
+
/** Assert that the field is greater than or equal to `value`. */
|
|
62
|
+
greaterThanOrEqual(value: string | number): AstNode;
|
|
63
|
+
/** Alias for {@link greaterThanOrEqual}. */
|
|
64
|
+
gte(value: string | number): AstNode;
|
|
65
|
+
/** Assert that the field is less than `value`. */
|
|
66
|
+
lessThan(value: string | number): AstNode;
|
|
67
|
+
/** Alias for {@link lessThan}. */
|
|
68
|
+
lt(value: string | number): AstNode;
|
|
69
|
+
/** Assert that the field is less than or equal to `value`. */
|
|
70
|
+
lessThanOrEqual(value: string | number): AstNode;
|
|
71
|
+
/** Alias for {@link lessThanOrEqual}. */
|
|
72
|
+
lte(value: string | number): AstNode;
|
|
73
|
+
/**
|
|
74
|
+
* Assert that the field value is contained in the given array.
|
|
75
|
+
*
|
|
76
|
+
* Produces an `In` node where the left child is a Field and the right child
|
|
77
|
+
* holds the serialised array value.
|
|
78
|
+
*/
|
|
79
|
+
in(values: string[]): AstNode;
|
|
80
|
+
/**
|
|
81
|
+
* Assert that the field (string or array) contains `value`.
|
|
82
|
+
*
|
|
83
|
+
* Produces a `Contains` node.
|
|
84
|
+
*/
|
|
85
|
+
contains(value: string): AstNode;
|
|
86
|
+
build(): AstNode;
|
|
87
|
+
toJSON(): AstNode;
|
|
88
|
+
private _op;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Builder for Semantic Vibe Engine checks.
|
|
92
|
+
*
|
|
93
|
+
* Obtained via `p.vibe('intent')`.
|
|
94
|
+
*/
|
|
95
|
+
export declare class VibeBuilder implements Builder {
|
|
96
|
+
private readonly _intent;
|
|
97
|
+
private _threshold?;
|
|
98
|
+
constructor(_intent: string);
|
|
99
|
+
/**
|
|
100
|
+
* Set the minimum confidence threshold (0–1) for the vibe check.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* p.vibe('frustrated').withConfidence(0.8)
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
withConfidence(threshold: number): this;
|
|
108
|
+
build(): AstNode;
|
|
109
|
+
toJSON(): AstNode;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Entry point for the fluent policy builder API.
|
|
113
|
+
*
|
|
114
|
+
* All methods are static – use `p` (the shorthand export) or `PolicyBuilder`
|
|
115
|
+
* directly without instantiation.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* import { p } from '@gp2f/client-sdk';
|
|
120
|
+
*
|
|
121
|
+
* const policy = p.and(
|
|
122
|
+
* p.field('/role').eq('admin'),
|
|
123
|
+
* p.exists('/session/token'),
|
|
124
|
+
* );
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
export declare class PolicyBuilder {
|
|
128
|
+
/**
|
|
129
|
+
* Begin a field assertion for the JSON-pointer `path`.
|
|
130
|
+
*
|
|
131
|
+
* Returns a {@link FieldBuilder} that can be chained with comparison
|
|
132
|
+
* operators (`eq`, `gt`, `in`, …).
|
|
133
|
+
*/
|
|
134
|
+
static field(path: string): FieldBuilder;
|
|
135
|
+
/** Combine multiple nodes with a logical AND. */
|
|
136
|
+
static and(...nodes: (AstNode | Builder)[]): AstNode;
|
|
137
|
+
/** Combine multiple nodes with a logical OR. */
|
|
138
|
+
static or(...nodes: (AstNode | Builder)[]): AstNode;
|
|
139
|
+
/** Negate a node with a logical NOT. */
|
|
140
|
+
static not(node: AstNode | Builder): AstNode;
|
|
141
|
+
/** Assert that the JSON-pointer `path` exists (is non-null) in the state. */
|
|
142
|
+
static exists(path: string): AstNode;
|
|
143
|
+
/** A policy that always evaluates to `true`. */
|
|
144
|
+
static literalTrue(): AstNode;
|
|
145
|
+
/** A policy that always evaluates to `false`. */
|
|
146
|
+
static literalFalse(): AstNode;
|
|
147
|
+
/**
|
|
148
|
+
* Begin a Semantic Vibe Engine check for the given intent string.
|
|
149
|
+
*
|
|
150
|
+
* Optionally chain `.withConfidence(threshold)` before using the node.
|
|
151
|
+
*/
|
|
152
|
+
static vibe(intent: string): VibeBuilder;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Shorthand alias for {@link PolicyBuilder}.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```ts
|
|
159
|
+
* import { p } from '@gp2f/client-sdk';
|
|
160
|
+
* const policy = p.and(p.field('/role').eq('admin'), p.exists('/token'));
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export declare const p: typeof PolicyBuilder;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fluent Policy Builder API for constructing GP2F policy AST nodes.
|
|
3
|
+
*
|
|
4
|
+
* Provides a chainable, type-safe alternative to writing raw JSON AST objects.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { p } from '@gp2f/client-sdk';
|
|
9
|
+
*
|
|
10
|
+
* const policy = p.and(
|
|
11
|
+
* p.field('/user/role').eq('admin'),
|
|
12
|
+
* p.field('/session/active').equal('true'),
|
|
13
|
+
* p.exists('/session/token'),
|
|
14
|
+
* );
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
// ── FieldBuilder ──────────────────────────────────────────────────────────────
|
|
18
|
+
/**
|
|
19
|
+
* A builder for field-specific policy assertions.
|
|
20
|
+
*
|
|
21
|
+
* Obtained via `p.field('/some/path')`.
|
|
22
|
+
*/
|
|
23
|
+
export class FieldBuilder {
|
|
24
|
+
constructor(_path) {
|
|
25
|
+
this._path = _path;
|
|
26
|
+
}
|
|
27
|
+
// -- Equality --
|
|
28
|
+
/** Assert that the field equals `value`. */
|
|
29
|
+
equal(value) {
|
|
30
|
+
return this._op("Eq", String(value));
|
|
31
|
+
}
|
|
32
|
+
/** Alias for {@link equal}. */
|
|
33
|
+
eq(value) {
|
|
34
|
+
return this.equal(value);
|
|
35
|
+
}
|
|
36
|
+
/** Assert that the field does not equal `value`. */
|
|
37
|
+
notEqual(value) {
|
|
38
|
+
return this._op("Neq", String(value));
|
|
39
|
+
}
|
|
40
|
+
/** Alias for {@link notEqual}. */
|
|
41
|
+
neq(value) {
|
|
42
|
+
return this.notEqual(value);
|
|
43
|
+
}
|
|
44
|
+
// -- Comparisons --
|
|
45
|
+
/** Assert that the field is greater than `value`. */
|
|
46
|
+
greaterThan(value) {
|
|
47
|
+
return this._op("Gt", String(value));
|
|
48
|
+
}
|
|
49
|
+
/** Alias for {@link greaterThan}. */
|
|
50
|
+
gt(value) {
|
|
51
|
+
return this.greaterThan(value);
|
|
52
|
+
}
|
|
53
|
+
/** Assert that the field is greater than or equal to `value`. */
|
|
54
|
+
greaterThanOrEqual(value) {
|
|
55
|
+
return this._op("Gte", String(value));
|
|
56
|
+
}
|
|
57
|
+
/** Alias for {@link greaterThanOrEqual}. */
|
|
58
|
+
gte(value) {
|
|
59
|
+
return this.greaterThanOrEqual(value);
|
|
60
|
+
}
|
|
61
|
+
/** Assert that the field is less than `value`. */
|
|
62
|
+
lessThan(value) {
|
|
63
|
+
return this._op("Lt", String(value));
|
|
64
|
+
}
|
|
65
|
+
/** Alias for {@link lessThan}. */
|
|
66
|
+
lt(value) {
|
|
67
|
+
return this.lessThan(value);
|
|
68
|
+
}
|
|
69
|
+
/** Assert that the field is less than or equal to `value`. */
|
|
70
|
+
lessThanOrEqual(value) {
|
|
71
|
+
return this._op("Lte", String(value));
|
|
72
|
+
}
|
|
73
|
+
/** Alias for {@link lessThanOrEqual}. */
|
|
74
|
+
lte(value) {
|
|
75
|
+
return this.lessThanOrEqual(value);
|
|
76
|
+
}
|
|
77
|
+
// -- Collection --
|
|
78
|
+
/**
|
|
79
|
+
* Assert that the field value is contained in the given array.
|
|
80
|
+
*
|
|
81
|
+
* Produces an `In` node where the left child is a Field and the right child
|
|
82
|
+
* holds the serialised array value.
|
|
83
|
+
*/
|
|
84
|
+
in(values) {
|
|
85
|
+
return {
|
|
86
|
+
kind: "In",
|
|
87
|
+
children: [
|
|
88
|
+
{ kind: "Field", path: this._path },
|
|
89
|
+
{ kind: "Literal", value: JSON.stringify(values) },
|
|
90
|
+
],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Assert that the field (string or array) contains `value`.
|
|
95
|
+
*
|
|
96
|
+
* Produces a `Contains` node.
|
|
97
|
+
*/
|
|
98
|
+
contains(value) {
|
|
99
|
+
return {
|
|
100
|
+
kind: "Contains",
|
|
101
|
+
children: [
|
|
102
|
+
{ kind: "Field", path: this._path },
|
|
103
|
+
{ kind: "Literal", value },
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// -- Builder interface --
|
|
108
|
+
build() {
|
|
109
|
+
throw new Error("FieldBuilder must be terminated with an operator (e.g. .eq(), .gt())");
|
|
110
|
+
}
|
|
111
|
+
toJSON() {
|
|
112
|
+
return this.build();
|
|
113
|
+
}
|
|
114
|
+
// -- Internal --
|
|
115
|
+
_op(kind, value) {
|
|
116
|
+
return {
|
|
117
|
+
kind,
|
|
118
|
+
children: [
|
|
119
|
+
{ kind: "Field", path: this._path },
|
|
120
|
+
{ kind: "Literal", value },
|
|
121
|
+
],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// ── VibeBuilder ───────────────────────────────────────────────────────────────
|
|
126
|
+
/**
|
|
127
|
+
* Builder for Semantic Vibe Engine checks.
|
|
128
|
+
*
|
|
129
|
+
* Obtained via `p.vibe('intent')`.
|
|
130
|
+
*/
|
|
131
|
+
export class VibeBuilder {
|
|
132
|
+
constructor(_intent) {
|
|
133
|
+
this._intent = _intent;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Set the minimum confidence threshold (0–1) for the vibe check.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* p.vibe('frustrated').withConfidence(0.8)
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
withConfidence(threshold) {
|
|
144
|
+
this._threshold = threshold;
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
build() {
|
|
148
|
+
return {
|
|
149
|
+
kind: "VibeCheck",
|
|
150
|
+
value: this._intent,
|
|
151
|
+
...(this._threshold !== undefined
|
|
152
|
+
? { path: String(this._threshold) }
|
|
153
|
+
: {}),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
toJSON() {
|
|
157
|
+
return this.build();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ── PolicyBuilder ─────────────────────────────────────────────────────────────
|
|
161
|
+
/** Resolve a value that may be a raw `AstNode` or any `Builder` instance. */
|
|
162
|
+
function resolve(node) {
|
|
163
|
+
return "build" in node && typeof node.build === "function"
|
|
164
|
+
? node.build()
|
|
165
|
+
: node;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Entry point for the fluent policy builder API.
|
|
169
|
+
*
|
|
170
|
+
* All methods are static – use `p` (the shorthand export) or `PolicyBuilder`
|
|
171
|
+
* directly without instantiation.
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* ```ts
|
|
175
|
+
* import { p } from '@gp2f/client-sdk';
|
|
176
|
+
*
|
|
177
|
+
* const policy = p.and(
|
|
178
|
+
* p.field('/role').eq('admin'),
|
|
179
|
+
* p.exists('/session/token'),
|
|
180
|
+
* );
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export class PolicyBuilder {
|
|
184
|
+
// -- Field entry point --
|
|
185
|
+
/**
|
|
186
|
+
* Begin a field assertion for the JSON-pointer `path`.
|
|
187
|
+
*
|
|
188
|
+
* Returns a {@link FieldBuilder} that can be chained with comparison
|
|
189
|
+
* operators (`eq`, `gt`, `in`, …).
|
|
190
|
+
*/
|
|
191
|
+
static field(path) {
|
|
192
|
+
return new FieldBuilder(path);
|
|
193
|
+
}
|
|
194
|
+
// -- Logical operators --
|
|
195
|
+
/** Combine multiple nodes with a logical AND. */
|
|
196
|
+
static and(...nodes) {
|
|
197
|
+
return {
|
|
198
|
+
kind: "And",
|
|
199
|
+
children: nodes.map(resolve),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/** Combine multiple nodes with a logical OR. */
|
|
203
|
+
static or(...nodes) {
|
|
204
|
+
return {
|
|
205
|
+
kind: "Or",
|
|
206
|
+
children: nodes.map(resolve),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/** Negate a node with a logical NOT. */
|
|
210
|
+
static not(node) {
|
|
211
|
+
return {
|
|
212
|
+
kind: "Not",
|
|
213
|
+
children: [resolve(node)],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
// -- Existence --
|
|
217
|
+
/** Assert that the JSON-pointer `path` exists (is non-null) in the state. */
|
|
218
|
+
static exists(path) {
|
|
219
|
+
return { kind: "Exists", path };
|
|
220
|
+
}
|
|
221
|
+
// -- Literal shortcuts --
|
|
222
|
+
/** A policy that always evaluates to `true`. */
|
|
223
|
+
static literalTrue() {
|
|
224
|
+
return { kind: "LiteralTrue" };
|
|
225
|
+
}
|
|
226
|
+
/** A policy that always evaluates to `false`. */
|
|
227
|
+
static literalFalse() {
|
|
228
|
+
return { kind: "LiteralFalse" };
|
|
229
|
+
}
|
|
230
|
+
// -- Vibe Engine --
|
|
231
|
+
/**
|
|
232
|
+
* Begin a Semantic Vibe Engine check for the given intent string.
|
|
233
|
+
*
|
|
234
|
+
* Optionally chain `.withConfidence(threshold)` before using the node.
|
|
235
|
+
*/
|
|
236
|
+
static vibe(intent) {
|
|
237
|
+
return new VibeBuilder(intent);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Shorthand alias for {@link PolicyBuilder}.
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```ts
|
|
245
|
+
* import { p } from '@gp2f/client-sdk';
|
|
246
|
+
* const policy = p.and(p.field('/role').eq('admin'), p.exists('/token'));
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
export const p = PolicyBuilder;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -24,6 +24,10 @@ export type { UndoButtonProps } from "./UndoButton";
|
|
|
24
24
|
export { MergeModal } from "./MergeModal";
|
|
25
25
|
export type { MergeModalProps } from "./MergeModal";
|
|
26
26
|
|
|
27
|
+
// ── Policy Builder ────────────────────────────────────────────────────────────
|
|
28
|
+
export { p, PolicyBuilder, FieldBuilder, VibeBuilder } from "./policy-builder";
|
|
29
|
+
export type { AstNode, Builder } from "./policy-builder";
|
|
30
|
+
|
|
27
31
|
// ── Lazy Policy Engine ────────────────────────────────────────────────────────
|
|
28
32
|
|
|
29
33
|
/**
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fluent Policy Builder API for constructing GP2F policy AST nodes.
|
|
3
|
+
*
|
|
4
|
+
* Provides a chainable, type-safe alternative to writing raw JSON AST objects.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { p } from '@gp2f/client-sdk';
|
|
9
|
+
*
|
|
10
|
+
* const policy = p.and(
|
|
11
|
+
* p.field('/user/role').eq('admin'),
|
|
12
|
+
* p.field('/session/active').equal('true'),
|
|
13
|
+
* p.exists('/session/token'),
|
|
14
|
+
* );
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ── Shared interfaces ─────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A node in the GP2F policy AST.
|
|
22
|
+
*
|
|
23
|
+
* Mirrors `policy_core::AstNode` from Rust.
|
|
24
|
+
*/
|
|
25
|
+
export interface AstNode {
|
|
26
|
+
/** The operation this node performs. */
|
|
27
|
+
kind: string;
|
|
28
|
+
/** Child nodes for composite operators (And, Or, Not, comparison ops). */
|
|
29
|
+
children?: AstNode[];
|
|
30
|
+
/** JSON-pointer path used by Field and Exists nodes (e.g. `/user/role`). */
|
|
31
|
+
path?: string;
|
|
32
|
+
/** String-encoded scalar value for leaf nodes (e.g. `"admin"`, `"42"`). */
|
|
33
|
+
value?: string;
|
|
34
|
+
/** Name of the external function – used only by Call nodes. */
|
|
35
|
+
callName?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Shared interface for all builder objects that can produce an AstNode.
|
|
40
|
+
*/
|
|
41
|
+
export interface Builder {
|
|
42
|
+
build(): AstNode;
|
|
43
|
+
toJSON(): AstNode;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── FieldBuilder ──────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* A builder for field-specific policy assertions.
|
|
50
|
+
*
|
|
51
|
+
* Obtained via `p.field('/some/path')`.
|
|
52
|
+
*/
|
|
53
|
+
export class FieldBuilder implements Builder {
|
|
54
|
+
constructor(private readonly _path: string) {}
|
|
55
|
+
|
|
56
|
+
// -- Equality --
|
|
57
|
+
|
|
58
|
+
/** Assert that the field equals `value`. */
|
|
59
|
+
equal(value: string | number): AstNode {
|
|
60
|
+
return this._op("Eq", String(value));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Alias for {@link equal}. */
|
|
64
|
+
eq(value: string | number): AstNode {
|
|
65
|
+
return this.equal(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Assert that the field does not equal `value`. */
|
|
69
|
+
notEqual(value: string | number): AstNode {
|
|
70
|
+
return this._op("Neq", String(value));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Alias for {@link notEqual}. */
|
|
74
|
+
neq(value: string | number): AstNode {
|
|
75
|
+
return this.notEqual(value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// -- Comparisons --
|
|
79
|
+
|
|
80
|
+
/** Assert that the field is greater than `value`. */
|
|
81
|
+
greaterThan(value: string | number): AstNode {
|
|
82
|
+
return this._op("Gt", String(value));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Alias for {@link greaterThan}. */
|
|
86
|
+
gt(value: string | number): AstNode {
|
|
87
|
+
return this.greaterThan(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Assert that the field is greater than or equal to `value`. */
|
|
91
|
+
greaterThanOrEqual(value: string | number): AstNode {
|
|
92
|
+
return this._op("Gte", String(value));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Alias for {@link greaterThanOrEqual}. */
|
|
96
|
+
gte(value: string | number): AstNode {
|
|
97
|
+
return this.greaterThanOrEqual(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Assert that the field is less than `value`. */
|
|
101
|
+
lessThan(value: string | number): AstNode {
|
|
102
|
+
return this._op("Lt", String(value));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Alias for {@link lessThan}. */
|
|
106
|
+
lt(value: string | number): AstNode {
|
|
107
|
+
return this.lessThan(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Assert that the field is less than or equal to `value`. */
|
|
111
|
+
lessThanOrEqual(value: string | number): AstNode {
|
|
112
|
+
return this._op("Lte", String(value));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Alias for {@link lessThanOrEqual}. */
|
|
116
|
+
lte(value: string | number): AstNode {
|
|
117
|
+
return this.lessThanOrEqual(value);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// -- Collection --
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Assert that the field value is contained in the given array.
|
|
124
|
+
*
|
|
125
|
+
* Produces an `In` node where the left child is a Field and the right child
|
|
126
|
+
* holds the serialised array value.
|
|
127
|
+
*/
|
|
128
|
+
in(values: string[]): AstNode {
|
|
129
|
+
return {
|
|
130
|
+
kind: "In",
|
|
131
|
+
children: [
|
|
132
|
+
{ kind: "Field", path: this._path },
|
|
133
|
+
{ kind: "Literal", value: JSON.stringify(values) },
|
|
134
|
+
],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Assert that the field (string or array) contains `value`.
|
|
140
|
+
*
|
|
141
|
+
* Produces a `Contains` node.
|
|
142
|
+
*/
|
|
143
|
+
contains(value: string): AstNode {
|
|
144
|
+
return {
|
|
145
|
+
kind: "Contains",
|
|
146
|
+
children: [
|
|
147
|
+
{ kind: "Field", path: this._path },
|
|
148
|
+
{ kind: "Literal", value },
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// -- Builder interface --
|
|
154
|
+
|
|
155
|
+
build(): AstNode {
|
|
156
|
+
throw new Error(
|
|
157
|
+
"FieldBuilder must be terminated with an operator (e.g. .eq(), .gt())",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
toJSON(): AstNode {
|
|
162
|
+
return this.build();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// -- Internal --
|
|
166
|
+
|
|
167
|
+
private _op(kind: string, value: string): AstNode {
|
|
168
|
+
return {
|
|
169
|
+
kind,
|
|
170
|
+
children: [
|
|
171
|
+
{ kind: "Field", path: this._path },
|
|
172
|
+
{ kind: "Literal", value },
|
|
173
|
+
],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── VibeBuilder ───────────────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Builder for Semantic Vibe Engine checks.
|
|
182
|
+
*
|
|
183
|
+
* Obtained via `p.vibe('intent')`.
|
|
184
|
+
*/
|
|
185
|
+
export class VibeBuilder implements Builder {
|
|
186
|
+
private _threshold?: number;
|
|
187
|
+
|
|
188
|
+
constructor(private readonly _intent: string) {}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Set the minimum confidence threshold (0–1) for the vibe check.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* p.vibe('frustrated').withConfidence(0.8)
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
withConfidence(threshold: number): this {
|
|
199
|
+
this._threshold = threshold;
|
|
200
|
+
return this;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
build(): AstNode {
|
|
204
|
+
return {
|
|
205
|
+
kind: "VibeCheck",
|
|
206
|
+
value: this._intent,
|
|
207
|
+
...(this._threshold !== undefined
|
|
208
|
+
? { path: String(this._threshold) }
|
|
209
|
+
: {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
toJSON(): AstNode {
|
|
214
|
+
return this.build();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── PolicyBuilder ─────────────────────────────────────────────────────────────
|
|
219
|
+
|
|
220
|
+
/** Resolve a value that may be a raw `AstNode` or any `Builder` instance. */
|
|
221
|
+
function resolve(node: AstNode | Builder): AstNode {
|
|
222
|
+
return "build" in node && typeof (node as Builder).build === "function"
|
|
223
|
+
? (node as Builder).build()
|
|
224
|
+
: (node as AstNode);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Entry point for the fluent policy builder API.
|
|
229
|
+
*
|
|
230
|
+
* All methods are static – use `p` (the shorthand export) or `PolicyBuilder`
|
|
231
|
+
* directly without instantiation.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* import { p } from '@gp2f/client-sdk';
|
|
236
|
+
*
|
|
237
|
+
* const policy = p.and(
|
|
238
|
+
* p.field('/role').eq('admin'),
|
|
239
|
+
* p.exists('/session/token'),
|
|
240
|
+
* );
|
|
241
|
+
* ```
|
|
242
|
+
*/
|
|
243
|
+
export class PolicyBuilder {
|
|
244
|
+
// -- Field entry point --
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Begin a field assertion for the JSON-pointer `path`.
|
|
248
|
+
*
|
|
249
|
+
* Returns a {@link FieldBuilder} that can be chained with comparison
|
|
250
|
+
* operators (`eq`, `gt`, `in`, …).
|
|
251
|
+
*/
|
|
252
|
+
static field(path: string): FieldBuilder {
|
|
253
|
+
return new FieldBuilder(path);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// -- Logical operators --
|
|
257
|
+
|
|
258
|
+
/** Combine multiple nodes with a logical AND. */
|
|
259
|
+
static and(...nodes: (AstNode | Builder)[]): AstNode {
|
|
260
|
+
return {
|
|
261
|
+
kind: "And",
|
|
262
|
+
children: nodes.map(resolve),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** Combine multiple nodes with a logical OR. */
|
|
267
|
+
static or(...nodes: (AstNode | Builder)[]): AstNode {
|
|
268
|
+
return {
|
|
269
|
+
kind: "Or",
|
|
270
|
+
children: nodes.map(resolve),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Negate a node with a logical NOT. */
|
|
275
|
+
static not(node: AstNode | Builder): AstNode {
|
|
276
|
+
return {
|
|
277
|
+
kind: "Not",
|
|
278
|
+
children: [resolve(node)],
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// -- Existence --
|
|
283
|
+
|
|
284
|
+
/** Assert that the JSON-pointer `path` exists (is non-null) in the state. */
|
|
285
|
+
static exists(path: string): AstNode {
|
|
286
|
+
return { kind: "Exists", path };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// -- Literal shortcuts --
|
|
290
|
+
|
|
291
|
+
/** A policy that always evaluates to `true`. */
|
|
292
|
+
static literalTrue(): AstNode {
|
|
293
|
+
return { kind: "LiteralTrue" };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** A policy that always evaluates to `false`. */
|
|
297
|
+
static literalFalse(): AstNode {
|
|
298
|
+
return { kind: "LiteralFalse" };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// -- Vibe Engine --
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Begin a Semantic Vibe Engine check for the given intent string.
|
|
305
|
+
*
|
|
306
|
+
* Optionally chain `.withConfidence(threshold)` before using the node.
|
|
307
|
+
*/
|
|
308
|
+
static vibe(intent: string): VibeBuilder {
|
|
309
|
+
return new VibeBuilder(intent);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Shorthand alias for {@link PolicyBuilder}.
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```ts
|
|
318
|
+
* import { p } from '@gp2f/client-sdk';
|
|
319
|
+
* const policy = p.and(p.field('/role').eq('admin'), p.exists('/token'));
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
export const p = PolicyBuilder;
|