@gp2f/server 0.1.6 → 0.1.8
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/README.md +28 -0
- package/index.d.ts +187 -17
- package/index.js +19 -8
- package/lib/policy-builder.js +148 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -72,6 +72,34 @@ async function main() {
|
|
|
72
72
|
main().catch(console.error);
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
### 3. Fluent Policy Builder
|
|
76
|
+
|
|
77
|
+
Build policy ASTs with a chainable API instead of raw JSON objects:
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { p } from '@gp2f/server';
|
|
81
|
+
|
|
82
|
+
// Field equality
|
|
83
|
+
const policy = p.field('/role').eq('admin');
|
|
84
|
+
|
|
85
|
+
// Logical AND
|
|
86
|
+
const policy = p.and(
|
|
87
|
+
p.field('/role').eq('clinician'),
|
|
88
|
+
p.exists('/patient_id'),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Role allow-list
|
|
92
|
+
const policy = p.field('/role').in(['admin', 'editor', 'reviewer']);
|
|
93
|
+
|
|
94
|
+
// Numeric comparison
|
|
95
|
+
const policy = p.field('/score').gte(80);
|
|
96
|
+
|
|
97
|
+
// Vibe Engine gate
|
|
98
|
+
const policy = p.vibe('frustrated').withConfidence(0.8).build();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The builder output is a plain `AstNode` that works with `evaluate()`, `evaluateWithTrace()`, `addActivity()`, and anywhere else a policy AST is accepted.
|
|
102
|
+
|
|
75
103
|
## Development
|
|
76
104
|
This package uses `napi-rs` to build the Rust bindings.
|
|
77
105
|
|
package/index.d.ts
CHANGED
|
@@ -1,30 +1,200 @@
|
|
|
1
|
-
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/* tslint:disable */
|
|
3
|
+
/* auto-generated – mirrors gp2f-node/src/*.rs napi exports */
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
// ── Node kinds ────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export type NodeKind =
|
|
8
|
+
| 'LiteralTrue'
|
|
9
|
+
| 'LiteralFalse'
|
|
10
|
+
| 'And'
|
|
11
|
+
| 'Or'
|
|
12
|
+
| 'Not'
|
|
13
|
+
| 'Eq'
|
|
14
|
+
| 'Neq'
|
|
15
|
+
| 'Gt'
|
|
16
|
+
| 'Gte'
|
|
17
|
+
| 'Lt'
|
|
18
|
+
| 'Lte'
|
|
19
|
+
| 'In'
|
|
20
|
+
| 'Contains'
|
|
21
|
+
| 'Exists'
|
|
22
|
+
| 'Field'
|
|
23
|
+
| 'Call'
|
|
24
|
+
| 'VibeCheck'
|
|
25
|
+
|
|
26
|
+
// ── Core AST ──────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/** A node in the GP2F policy AST. */
|
|
29
|
+
export interface AstNode {
|
|
30
|
+
/** The operation this node performs (required). */
|
|
31
|
+
kind: string
|
|
32
|
+
/** Child nodes for composite operators (AND, OR, NOT, comparison, …). */
|
|
33
|
+
children?: AstNode[]
|
|
34
|
+
/** JSON-pointer path used by `FIELD` and `EXISTS` nodes (e.g. `/user/role`). */
|
|
35
|
+
path?: string
|
|
36
|
+
/** String-encoded scalar value for leaf nodes (e.g. `"admin"`, `"42"`). */
|
|
37
|
+
value?: string
|
|
38
|
+
/** Name of the external function – used only by `CALL` nodes. */
|
|
39
|
+
callName?: string
|
|
8
40
|
}
|
|
9
41
|
|
|
42
|
+
// ── Activity & server configuration ──────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/** Configuration object for a single workflow activity. */
|
|
10
45
|
export interface ActivityConfig {
|
|
11
|
-
|
|
46
|
+
/** Policy AST that governs whether this activity is permitted. */
|
|
47
|
+
policy: AstNode | PolicyInput
|
|
48
|
+
/** Optional name of a registered compensation handler. */
|
|
49
|
+
compensationRef?: string
|
|
50
|
+
/** When `true`, this activity runs as a Local Activity. */
|
|
51
|
+
isLocal?: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Server startup configuration. */
|
|
55
|
+
export interface ServerConfig {
|
|
56
|
+
/** TCP port to listen on. Defaults to 3000. */
|
|
57
|
+
port?: number
|
|
58
|
+
/** Bind address. Defaults to `"0.0.0.0"`. */
|
|
59
|
+
host?: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Context passed to every `onExecute` callback. */
|
|
63
|
+
export interface ExecutionContext {
|
|
64
|
+
/** Unique workflow execution identifier. */
|
|
65
|
+
instanceId: string
|
|
66
|
+
/** Tenant/organisation this execution belongs to. */
|
|
67
|
+
tenantId: string
|
|
68
|
+
/** Name of the activity currently executing. */
|
|
69
|
+
activityName: string
|
|
70
|
+
/** The JSON-encoded state document. Use `JSON.parse(ctx.stateJson)`. */
|
|
71
|
+
stateJson: string
|
|
12
72
|
}
|
|
13
73
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
74
|
+
/** Result of a policy evaluation including the decision trace. */
|
|
75
|
+
export interface EvalResult {
|
|
76
|
+
/** `true` when the policy permits the operation. */
|
|
77
|
+
result: boolean
|
|
78
|
+
/** Human-readable trace of each evaluation step (for debugging). */
|
|
79
|
+
trace: string[]
|
|
18
80
|
}
|
|
19
81
|
|
|
82
|
+
// ── Native functions ──────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Evaluate a policy AST against a JSON state object.
|
|
86
|
+
*
|
|
87
|
+
* Returns `true` when the policy permits the operation.
|
|
88
|
+
*/
|
|
89
|
+
export function evaluate(policy: AstNode, state: object): boolean
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Evaluate a policy AST and return the full evaluation trace.
|
|
93
|
+
*
|
|
94
|
+
* Useful for debugging policies.
|
|
95
|
+
*/
|
|
96
|
+
export function evaluateWithTrace(policy: AstNode, state: object): EvalResult
|
|
20
97
|
|
|
21
|
-
|
|
22
|
-
|
|
98
|
+
// ── Workflow class ────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
export class Workflow {
|
|
101
|
+
constructor(workflowId: string)
|
|
102
|
+
|
|
103
|
+
/** Add an activity to this workflow. */
|
|
23
104
|
addActivity(
|
|
24
105
|
name: string,
|
|
25
106
|
config: ActivityConfig,
|
|
26
|
-
|
|
27
|
-
):
|
|
28
|
-
|
|
29
|
-
|
|
107
|
+
onExecute?: (ctx: ExecutionContext) => Promise<void> | void,
|
|
108
|
+
): string
|
|
109
|
+
|
|
110
|
+
/** The workflow identifier. */
|
|
111
|
+
readonly id: string
|
|
112
|
+
|
|
113
|
+
/** Number of registered activities. */
|
|
114
|
+
readonly activityCount: number
|
|
115
|
+
|
|
116
|
+
/** Evaluate all activity policies against `state` (no side-effects). */
|
|
117
|
+
dryRun(state: object): boolean
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── GP2FServer class ──────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
export class GP2FServer {
|
|
123
|
+
constructor(config?: ServerConfig)
|
|
124
|
+
|
|
125
|
+
/** Register a workflow with this server. */
|
|
126
|
+
register(workflow: Workflow): void
|
|
127
|
+
|
|
128
|
+
/** Start the HTTP server. */
|
|
129
|
+
start(): Promise<void>
|
|
130
|
+
|
|
131
|
+
/** Stop the HTTP server. */
|
|
132
|
+
stop(): Promise<void>
|
|
133
|
+
|
|
134
|
+
/** The configured TCP port. */
|
|
135
|
+
readonly port: number
|
|
136
|
+
|
|
137
|
+
/** `true` when the server is currently accepting connections. */
|
|
138
|
+
readonly isRunning: boolean
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Fluent Policy Builder ─────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
/** Shared interface for all builder objects that can produce an AstNode. */
|
|
144
|
+
export interface Builder {
|
|
145
|
+
build(): AstNode
|
|
146
|
+
toJSON(): AstNode
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** A policy value that is either a raw AstNode or a Builder. */
|
|
150
|
+
export type PolicyInput = AstNode | Builder
|
|
151
|
+
|
|
152
|
+
/** Builder for field-specific policy assertions. */
|
|
153
|
+
export declare class FieldBuilder implements Builder {
|
|
154
|
+
constructor(path: string)
|
|
155
|
+
|
|
156
|
+
equal(value: string | number): AstNode
|
|
157
|
+
eq(value: string | number): AstNode
|
|
158
|
+
notEqual(value: string | number): AstNode
|
|
159
|
+
neq(value: string | number): AstNode
|
|
160
|
+
|
|
161
|
+
greaterThan(value: string | number): AstNode
|
|
162
|
+
gt(value: string | number): AstNode
|
|
163
|
+
greaterThanOrEqual(value: string | number): AstNode
|
|
164
|
+
gte(value: string | number): AstNode
|
|
165
|
+
lessThan(value: string | number): AstNode
|
|
166
|
+
lt(value: string | number): AstNode
|
|
167
|
+
lessThanOrEqual(value: string | number): AstNode
|
|
168
|
+
lte(value: string | number): AstNode
|
|
169
|
+
|
|
170
|
+
in(values: string[]): AstNode
|
|
171
|
+
contains(value: string): AstNode
|
|
172
|
+
|
|
173
|
+
build(): AstNode
|
|
174
|
+
toJSON(): AstNode
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Builder for Semantic Vibe Engine checks. */
|
|
178
|
+
export declare class VibeBuilder implements Builder {
|
|
179
|
+
constructor(intent: string)
|
|
180
|
+
|
|
181
|
+
withConfidence(threshold: number): this
|
|
182
|
+
|
|
183
|
+
build(): AstNode
|
|
184
|
+
toJSON(): AstNode
|
|
30
185
|
}
|
|
186
|
+
|
|
187
|
+
/** Entry point for the fluent policy builder API. */
|
|
188
|
+
export declare class PolicyBuilder {
|
|
189
|
+
static field(path: string): FieldBuilder
|
|
190
|
+
static and(...nodes: PolicyInput[]): AstNode
|
|
191
|
+
static or(...nodes: PolicyInput[]): AstNode
|
|
192
|
+
static not(node: PolicyInput): AstNode
|
|
193
|
+
static exists(path: string): AstNode
|
|
194
|
+
static literalTrue(): AstNode
|
|
195
|
+
static literalFalse(): AstNode
|
|
196
|
+
static vibe(intent: string): VibeBuilder
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Shorthand alias for {@link PolicyBuilder}. */
|
|
200
|
+
export declare const p: typeof PolicyBuilder
|
package/index.js
CHANGED
|
@@ -38,14 +38,25 @@ function loadNative() {
|
|
|
38
38
|
)
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
const
|
|
41
|
+
const { p, PolicyBuilder, FieldBuilder, VibeBuilder } = require('./lib/policy-builder')
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
let native
|
|
44
|
+
try {
|
|
45
|
+
native = loadNative()
|
|
46
|
+
} catch (_) {
|
|
47
|
+
native = {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { ...native, p, PolicyBuilder, FieldBuilder, VibeBuilder }
|
|
44
51
|
|
|
45
52
|
// Explicitly assign named exports so Node.js CJS-ESM bridge can statically analyze them
|
|
46
|
-
module.exports.
|
|
47
|
-
module.exports.
|
|
48
|
-
module.exports.
|
|
49
|
-
module.exports.
|
|
50
|
-
module.exports.
|
|
51
|
-
module.exports.
|
|
53
|
+
module.exports.GP2FServer = native.JsGp2FServer || native.GP2FServer
|
|
54
|
+
module.exports.Workflow = native.JsWorkflow || native.Workflow
|
|
55
|
+
module.exports.AstNode = native.JsAstNode || native.AstNode
|
|
56
|
+
module.exports.NodeKind = native.JsNodeKind || native.NodeKind
|
|
57
|
+
module.exports.evaluate = native.evaluate
|
|
58
|
+
module.exports.evaluateWithTrace = native.evaluateWithTrace
|
|
59
|
+
module.exports.p = p
|
|
60
|
+
module.exports.PolicyBuilder = PolicyBuilder
|
|
61
|
+
module.exports.FieldBuilder = FieldBuilder
|
|
62
|
+
module.exports.VibeBuilder = VibeBuilder
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Fluent Policy Builder API for constructing GP2F policy AST nodes.
|
|
5
|
+
*
|
|
6
|
+
* Provides a chainable alternative to writing raw JSON AST objects.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```js
|
|
10
|
+
* const { p } = require('@gp2f/server');
|
|
11
|
+
*
|
|
12
|
+
* const policy = p.and(
|
|
13
|
+
* p.field('/user/role').eq('admin'),
|
|
14
|
+
* p.exists('/session/token'),
|
|
15
|
+
* );
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ── Internal helper ───────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function resolve(node) {
|
|
22
|
+
return node && typeof node.build === 'function' ? node.build() : node
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── FieldBuilder ──────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
class FieldBuilder {
|
|
28
|
+
constructor(path) {
|
|
29
|
+
this._path = path
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
_op(kind, value) {
|
|
33
|
+
return {
|
|
34
|
+
kind,
|
|
35
|
+
children: [{ kind: 'Field', path: this._path }],
|
|
36
|
+
value: String(value)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Equality
|
|
41
|
+
equal(value) { return this._op('Eq', value) }
|
|
42
|
+
eq(value) { return this.equal(value) }
|
|
43
|
+
notEqual(value) { return this._op('Neq', value) }
|
|
44
|
+
neq(value) { return this.notEqual(value) }
|
|
45
|
+
|
|
46
|
+
// Comparisons
|
|
47
|
+
greaterThan(value) { return this._op('Gt', value) }
|
|
48
|
+
gt(value) { return this.greaterThan(value) }
|
|
49
|
+
greaterThanOrEqual(value) { return this._op('Gte', value) }
|
|
50
|
+
gte(value) { return this.greaterThanOrEqual(value) }
|
|
51
|
+
lessThan(value) { return this._op('Lt', value) }
|
|
52
|
+
lt(value) { return this.lessThan(value) }
|
|
53
|
+
lessThanOrEqual(value) { return this._op('Lte', value) }
|
|
54
|
+
lte(value) { return this.lessThanOrEqual(value) }
|
|
55
|
+
|
|
56
|
+
// Collection
|
|
57
|
+
in(values) {
|
|
58
|
+
return {
|
|
59
|
+
kind: 'In',
|
|
60
|
+
children: [{ kind: 'Field', path: this._path }],
|
|
61
|
+
value: JSON.stringify(values)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
contains(value) {
|
|
66
|
+
return {
|
|
67
|
+
kind: 'Contains',
|
|
68
|
+
children: [{ kind: 'Field', path: this._path }],
|
|
69
|
+
value: String(value)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
build() {
|
|
74
|
+
throw new Error(
|
|
75
|
+
'FieldBuilder must be terminated with an operator (e.g. .eq(), .gt())'
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
toJSON() {
|
|
80
|
+
return this.build()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── VibeBuilder ───────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
class VibeBuilder {
|
|
87
|
+
constructor(intent) {
|
|
88
|
+
this._intent = intent
|
|
89
|
+
this._threshold = undefined
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
withConfidence(threshold) {
|
|
93
|
+
this._threshold = threshold
|
|
94
|
+
return this
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
build() {
|
|
98
|
+
const node = { kind: 'VibeCheck', value: this._intent }
|
|
99
|
+
if (this._threshold !== undefined) {
|
|
100
|
+
node.path = String(this._threshold)
|
|
101
|
+
}
|
|
102
|
+
return node
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
toJSON() {
|
|
106
|
+
return this.build()
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── PolicyBuilder ─────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
class PolicyBuilder {
|
|
113
|
+
static field(path) {
|
|
114
|
+
return new FieldBuilder(path)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
static and(...nodes) {
|
|
118
|
+
return { kind: 'And', children: nodes.map(resolve) }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static or(...nodes) {
|
|
122
|
+
return { kind: 'Or', children: nodes.map(resolve) }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
static not(node) {
|
|
126
|
+
return { kind: 'Not', children: [resolve(node)] }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static exists(path) {
|
|
130
|
+
return { kind: 'Exists', path }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
static literalTrue() {
|
|
134
|
+
return { kind: 'LiteralTrue' }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
static literalFalse() {
|
|
138
|
+
return { kind: 'LiteralFalse' }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static vibe(intent) {
|
|
142
|
+
return new VibeBuilder(intent)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const p = PolicyBuilder
|
|
147
|
+
|
|
148
|
+
module.exports = { p, PolicyBuilder, FieldBuilder, VibeBuilder }
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gp2f/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Native Node.js bindings for the GP2F policy engine – powered by napi-rs",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"index.js",
|
|
9
9
|
"index.d.ts",
|
|
10
|
+
"lib",
|
|
10
11
|
"gp2f_node.*.node"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|