@govplane/runtime-sdk 0.2.4 → 0.5.0
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 +400 -216
- package/dist/index.cjs +187 -91
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -14
- package/dist/index.d.ts +65 -14
- package/dist/index.js +187 -91
- package/dist/index.js.map +1 -1
- package/docs/README.md +89 -0
- package/docs/SUMMARY.md +34 -0
- package/docs/installation/GettingStarted.md +474 -0
- package/docs/reference/Configuration.md +198 -0
- package/docs/reference/TypesAndInterfaces.md +373 -0
- package/docs/usage/BundleLifecycle.md +209 -0
- package/docs/usage/ConditionalRules.md +251 -0
- package/docs/usage/ContextPolicy.md +196 -0
- package/docs/usage/CustomEffect.md +217 -0
- package/docs/usage/DecisionTrace.md +289 -0
- package/docs/usage/Effects.md +213 -0
- package/docs/usage/Evaluate.md +164 -0
- package/docs/usage/PolicyDefaults.md +220 -0
- package/package.json +3 -5
package/dist/index.d.ts
CHANGED
|
@@ -20,15 +20,24 @@ type Effect = {
|
|
|
20
20
|
windowSeconds: number;
|
|
21
21
|
key: string;
|
|
22
22
|
};
|
|
23
|
+
} | {
|
|
24
|
+
type: "custom";
|
|
25
|
+
value: string;
|
|
23
26
|
};
|
|
24
27
|
type WhenAstV1 = {
|
|
28
|
+
op: "and" | "or";
|
|
29
|
+
conditions: WhenAstV1[];
|
|
30
|
+
} | {
|
|
25
31
|
op: "and" | "or";
|
|
26
32
|
args: WhenAstV1[];
|
|
33
|
+
} | {
|
|
34
|
+
op: "not";
|
|
35
|
+
condition: WhenAstV1;
|
|
27
36
|
} | {
|
|
28
37
|
op: "not";
|
|
29
38
|
arg: WhenAstV1;
|
|
30
39
|
} | {
|
|
31
|
-
op: "eq" | "ne" | "gt" | "gte" | "lt" | "lte";
|
|
40
|
+
op: "eq" | "neq" | "ne" | "gt" | "gte" | "lt" | "lte";
|
|
32
41
|
path: string;
|
|
33
42
|
value: any;
|
|
34
43
|
} | {
|
|
@@ -45,15 +54,38 @@ type RuntimeRule = {
|
|
|
45
54
|
priority: number;
|
|
46
55
|
target: Target;
|
|
47
56
|
when?: WhenAstV1;
|
|
57
|
+
/** Effect applied when `when` evaluates to true (falls back to `effect`). */
|
|
58
|
+
thenEffect?: Effect;
|
|
59
|
+
/** Effect applied when `when` evaluates to false (rule is skipped if absent). */
|
|
60
|
+
elseEffect?: Effect;
|
|
48
61
|
effect: Effect;
|
|
49
62
|
description?: string;
|
|
50
63
|
};
|
|
64
|
+
type PolicyDefault = {
|
|
65
|
+
effect: "allow";
|
|
66
|
+
} | {
|
|
67
|
+
effect: "deny";
|
|
68
|
+
} | {
|
|
69
|
+
effect: "kill_switch";
|
|
70
|
+
killSwitch: {
|
|
71
|
+
service: string;
|
|
72
|
+
reason?: string;
|
|
73
|
+
};
|
|
74
|
+
} | {
|
|
75
|
+
effect: "throttle";
|
|
76
|
+
throttle: {
|
|
77
|
+
limit: number;
|
|
78
|
+
windowSeconds: number;
|
|
79
|
+
key: string;
|
|
80
|
+
};
|
|
81
|
+
} | {
|
|
82
|
+
effect: "custom";
|
|
83
|
+
customEffect: string;
|
|
84
|
+
};
|
|
51
85
|
type RuntimePolicy = {
|
|
52
86
|
policyKey: string;
|
|
53
87
|
activeVersion: number;
|
|
54
|
-
defaults
|
|
55
|
-
effect: "allow" | "deny";
|
|
56
|
-
};
|
|
88
|
+
defaults?: PolicyDefault;
|
|
57
89
|
rules: RuntimeRule[];
|
|
58
90
|
};
|
|
59
91
|
type RuntimeBundleV1 = {
|
|
@@ -76,23 +108,30 @@ type Decision = {
|
|
|
76
108
|
ruleId?: string;
|
|
77
109
|
} | {
|
|
78
110
|
decision: "kill_switch";
|
|
79
|
-
reason: "rule";
|
|
80
|
-
policyKey
|
|
81
|
-
ruleId
|
|
111
|
+
reason: "default" | "rule";
|
|
112
|
+
policyKey?: string;
|
|
113
|
+
ruleId?: string;
|
|
82
114
|
killSwitch: {
|
|
83
115
|
service: string;
|
|
84
116
|
reason?: string;
|
|
85
117
|
};
|
|
86
118
|
} | {
|
|
87
119
|
decision: "throttle";
|
|
88
|
-
reason: "rule";
|
|
89
|
-
policyKey
|
|
90
|
-
ruleId
|
|
120
|
+
reason: "default" | "rule";
|
|
121
|
+
policyKey?: string;
|
|
122
|
+
ruleId?: string;
|
|
91
123
|
throttle: {
|
|
92
124
|
limit: number;
|
|
93
125
|
windowSeconds: number;
|
|
94
126
|
key: string;
|
|
95
127
|
};
|
|
128
|
+
} | {
|
|
129
|
+
decision: "custom";
|
|
130
|
+
reason: "default" | "rule";
|
|
131
|
+
policyKey?: string;
|
|
132
|
+
ruleId?: string;
|
|
133
|
+
value: string;
|
|
134
|
+
parsedValue?: unknown;
|
|
96
135
|
};
|
|
97
136
|
type TraceDiscardReason = "disabled" | "target_mismatch" | "when_false" | "invalid_effect";
|
|
98
137
|
type TraceRule = {
|
|
@@ -115,6 +154,7 @@ type DecisionTrace = {
|
|
|
115
154
|
deny: number;
|
|
116
155
|
throttle: number;
|
|
117
156
|
allow: number;
|
|
157
|
+
custom: number;
|
|
118
158
|
};
|
|
119
159
|
};
|
|
120
160
|
winner?: {
|
|
@@ -163,6 +203,7 @@ type DecisionTraceCompact = {
|
|
|
163
203
|
deny: number;
|
|
164
204
|
throttle: number;
|
|
165
205
|
allow: number;
|
|
206
|
+
custom: number;
|
|
166
207
|
};
|
|
167
208
|
};
|
|
168
209
|
winner?: {
|
|
@@ -202,6 +243,7 @@ type StructuredTraceEvent = {
|
|
|
202
243
|
deny: number;
|
|
203
244
|
throttle: number;
|
|
204
245
|
allow: number;
|
|
246
|
+
custom: number;
|
|
205
247
|
};
|
|
206
248
|
};
|
|
207
249
|
rules?: Array<{
|
|
@@ -241,9 +283,6 @@ declare function validateContext(ctx: Record<string, unknown>, policy: ContextPo
|
|
|
241
283
|
type RuntimeClientConfig = {
|
|
242
284
|
baseUrl: string;
|
|
243
285
|
runtimeKey: string;
|
|
244
|
-
orgId: string;
|
|
245
|
-
projectId: string;
|
|
246
|
-
env: string;
|
|
247
286
|
pollMs?: number;
|
|
248
287
|
burstPollMs?: number;
|
|
249
288
|
burstDurationMs?: number;
|
|
@@ -259,6 +298,11 @@ type RuntimeClientConfig = {
|
|
|
259
298
|
engine?: {
|
|
260
299
|
validateContext?: boolean;
|
|
261
300
|
contextPolicy?: ContextPolicy;
|
|
301
|
+
/**
|
|
302
|
+
* When true, custom-effect `value` strings are JSON-parsed and the result
|
|
303
|
+
* is attached as `parsedValue` on the returned decision.
|
|
304
|
+
*/
|
|
305
|
+
parseCustomEffect?: boolean;
|
|
262
306
|
};
|
|
263
307
|
/**
|
|
264
308
|
* Decision trace (SDK-only, zero PII)
|
|
@@ -369,6 +413,7 @@ declare class RuntimeClient<TBundle = RuntimeBundleV1> {
|
|
|
369
413
|
private computeBackoffDelayMs;
|
|
370
414
|
private bundleUrl;
|
|
371
415
|
private commonHeaders;
|
|
416
|
+
private fetchWithTimeout;
|
|
372
417
|
private headBundle;
|
|
373
418
|
private getBundle;
|
|
374
419
|
private enableBurst;
|
|
@@ -401,6 +446,12 @@ type EngineOpts = {
|
|
|
401
446
|
traceQueueMax?: number;
|
|
402
447
|
traceQueueDropPolicy?: TraceQueueDropPolicy;
|
|
403
448
|
onTraceSinkError?: (err: unknown) => void;
|
|
449
|
+
/**
|
|
450
|
+
* When true, custom effect `value` strings are JSON-parsed and the result
|
|
451
|
+
* is attached as `parsedValue` on the decision. Parsing errors are
|
|
452
|
+
* silently swallowed and `parsedValue` is left undefined.
|
|
453
|
+
*/
|
|
454
|
+
parseCustomEffect?: boolean;
|
|
404
455
|
};
|
|
405
456
|
declare function createPolicyEngine(opts: EngineOpts): PolicyEngine;
|
|
406
457
|
|
|
@@ -410,4 +461,4 @@ type FormatOpts = {
|
|
|
410
461
|
};
|
|
411
462
|
declare function formatTrace(trace: DecisionTraceCompact | DecisionTraceFull, opts?: FormatOpts): string;
|
|
412
463
|
|
|
413
|
-
export { type BundleMeta, type ContextPolicy, DEFAULT_CONTEXT_POLICY, type Decision, type DecisionTrace, type DecisionTraceCompact, type DecisionTraceFull, type DecisionTraceHook, type DecisionWithOptionalTrace, type DecisionWithTrace, type Effect, GovplaneError, HttpError, type PolicyEngine, type PolicyEngineEvaluateInput, type RefreshResult, type RuntimeBundleV1, type RuntimeCache, RuntimeClient, type RuntimeClientConfig, type RuntimeClientOptions, type RuntimePolicy, type RuntimeRule, type RuntimeStatus, type StructuredTraceEvent, type Target, type TraceDiscardReason, type TraceLevel, type TraceMode, type TraceOptions, type TraceQueueDropPolicy, type TraceRule, type TraceSink, type TraceSinkAsync, type WhenAstV1, createPolicyEngine, formatTrace, validateContext };
|
|
464
|
+
export { type BundleMeta, type ContextPolicy, DEFAULT_CONTEXT_POLICY, type Decision, type DecisionTrace, type DecisionTraceCompact, type DecisionTraceFull, type DecisionTraceHook, type DecisionWithOptionalTrace, type DecisionWithTrace, type Effect, GovplaneError, HttpError, type PolicyDefault, type PolicyEngine, type PolicyEngineEvaluateInput, type RefreshResult, type RuntimeBundleV1, type RuntimeCache, RuntimeClient, type RuntimeClientConfig, type RuntimeClientOptions, type RuntimePolicy, type RuntimeRule, type RuntimeStatus, type StructuredTraceEvent, type Target, type TraceDiscardReason, type TraceLevel, type TraceMode, type TraceOptions, type TraceQueueDropPolicy, type TraceRule, type TraceSink, type TraceSinkAsync, type WhenAstV1, createPolicyEngine, formatTrace, validateContext };
|
package/dist/index.js
CHANGED
|
@@ -5,19 +5,16 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
5
5
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
6
|
});
|
|
7
7
|
|
|
8
|
-
// src/client/RuntimeClient.ts
|
|
9
|
-
import { request } from "undici";
|
|
10
|
-
|
|
11
8
|
// src/engine/context.ts
|
|
12
9
|
var DEFAULT_CONTEXT_POLICY = {
|
|
13
10
|
allowedKeys: [
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
11
|
+
"plan",
|
|
12
|
+
"country",
|
|
13
|
+
"requestTier",
|
|
14
|
+
"feature",
|
|
15
|
+
"amount",
|
|
16
|
+
"isAuthenticated",
|
|
17
|
+
"role"
|
|
21
18
|
],
|
|
22
19
|
maxStringLen: 64,
|
|
23
20
|
maxArrayLen: 10,
|
|
@@ -45,19 +42,18 @@ var DEFAULT_POLICY = {
|
|
|
45
42
|
blockLikelyPiiKeys: true
|
|
46
43
|
};
|
|
47
44
|
function validateContext(ctx, policy) {
|
|
48
|
-
const allowed = new Set(policy.allowedKeys);
|
|
45
|
+
const allowed = new Set(policy.allowedKeys.map((k) => k.startsWith("ctx.") ? k.slice(4) : k));
|
|
49
46
|
const maxStringLen = policy.maxStringLen ?? DEFAULT_POLICY.maxStringLen;
|
|
50
47
|
const maxArrayLen = policy.maxArrayLen ?? DEFAULT_POLICY.maxArrayLen;
|
|
51
48
|
const blockLikelyPiiKeys = policy.blockLikelyPiiKeys ?? DEFAULT_POLICY.blockLikelyPiiKeys;
|
|
52
49
|
for (const [k, v] of Object.entries(ctx)) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
throw new Error(`Context key not allowed: ${path}`);
|
|
50
|
+
if (!allowed.has(k)) {
|
|
51
|
+
throw new Error(`Context key not allowed: ${k}`);
|
|
56
52
|
}
|
|
57
53
|
if (blockLikelyPiiKeys) {
|
|
58
54
|
for (const re of PII_KEY_PATTERNS) {
|
|
59
55
|
if (re.test(k)) {
|
|
60
|
-
throw new Error(`Context key looks like PII and is blocked: ${
|
|
56
|
+
throw new Error(`Context key looks like PII and is blocked: ${k}`);
|
|
61
57
|
}
|
|
62
58
|
}
|
|
63
59
|
}
|
|
@@ -65,25 +61,26 @@ function validateContext(ctx, policy) {
|
|
|
65
61
|
const t = typeof v;
|
|
66
62
|
if (t === "boolean" || t === "number") continue;
|
|
67
63
|
if (t === "string") {
|
|
68
|
-
if (v.length > maxStringLen) throw new Error(`Context value too long: ${
|
|
64
|
+
if (v.length > maxStringLen) throw new Error(`Context value too long: ${k}`);
|
|
69
65
|
continue;
|
|
70
66
|
}
|
|
71
67
|
if (Array.isArray(v)) {
|
|
72
|
-
if (v.length > maxArrayLen) throw new Error(`Context array too long: ${
|
|
68
|
+
if (v.length > maxArrayLen) throw new Error(`Context array too long: ${k}`);
|
|
73
69
|
for (const it of v) {
|
|
74
|
-
if (typeof it !== "string") throw new Error(`Invalid array value type: ${
|
|
75
|
-
if (it.length > maxStringLen) throw new Error(`Invalid array value length: ${
|
|
70
|
+
if (typeof it !== "string") throw new Error(`Invalid array value type: ${k}`);
|
|
71
|
+
if (it.length > maxStringLen) throw new Error(`Invalid array value length: ${k}`);
|
|
76
72
|
}
|
|
77
73
|
continue;
|
|
78
74
|
}
|
|
79
|
-
throw new Error(`Invalid context type for ${
|
|
75
|
+
throw new Error(`Invalid context type for ${k}`);
|
|
80
76
|
}
|
|
81
77
|
}
|
|
82
78
|
|
|
83
79
|
// src/engine/when.ts
|
|
84
|
-
function getPath(
|
|
85
|
-
const
|
|
86
|
-
|
|
80
|
+
function getPath(ctx, path) {
|
|
81
|
+
const normalizedPath = path.startsWith("ctx.") ? path.slice(4) : path;
|
|
82
|
+
const parts = normalizedPath.split(".");
|
|
83
|
+
let cur = ctx;
|
|
87
84
|
for (const p of parts) {
|
|
88
85
|
if (!cur || typeof cur !== "object") return void 0;
|
|
89
86
|
cur = cur[p];
|
|
@@ -92,30 +89,40 @@ function getPath(obj, path) {
|
|
|
92
89
|
}
|
|
93
90
|
function evalWhen(node, ctx) {
|
|
94
91
|
switch (node.op) {
|
|
95
|
-
case "and":
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
case "
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
case "and": {
|
|
93
|
+
const children = node.conditions ?? node.args ?? [];
|
|
94
|
+
return children.every((n) => evalWhen(n, ctx));
|
|
95
|
+
}
|
|
96
|
+
case "or": {
|
|
97
|
+
const children = node.conditions ?? node.args ?? [];
|
|
98
|
+
return children.some((n) => evalWhen(n, ctx));
|
|
99
|
+
}
|
|
100
|
+
case "not": {
|
|
101
|
+
const child = node.condition ?? node.arg;
|
|
102
|
+
return !evalWhen(child, ctx);
|
|
103
|
+
}
|
|
104
|
+
case "exists": {
|
|
105
|
+
const v = getPath(ctx, node.path);
|
|
106
|
+
return v !== void 0 && v !== null;
|
|
107
|
+
}
|
|
103
108
|
case "in": {
|
|
104
|
-
const v = getPath(
|
|
109
|
+
const v = getPath(ctx, node.path);
|
|
105
110
|
return node.values.some((x) => x === v);
|
|
106
111
|
}
|
|
107
112
|
case "eq":
|
|
108
|
-
return getPath(
|
|
113
|
+
return getPath(ctx, node.path) === node.value;
|
|
114
|
+
// `neq` is the canonical spelling; `ne` is kept for legacy bundles
|
|
115
|
+
case "neq":
|
|
109
116
|
case "ne":
|
|
110
|
-
return getPath(
|
|
117
|
+
return getPath(ctx, node.path) !== node.value;
|
|
111
118
|
case "gt":
|
|
112
|
-
return Number(getPath(
|
|
119
|
+
return Number(getPath(ctx, node.path)) > Number(node.value);
|
|
113
120
|
case "gte":
|
|
114
|
-
return Number(getPath(
|
|
121
|
+
return Number(getPath(ctx, node.path)) >= Number(node.value);
|
|
115
122
|
case "lt":
|
|
116
|
-
return Number(getPath(
|
|
123
|
+
return Number(getPath(ctx, node.path)) < Number(node.value);
|
|
117
124
|
case "lte":
|
|
118
|
-
return Number(getPath(
|
|
125
|
+
return Number(getPath(ctx, node.path)) <= Number(node.value);
|
|
119
126
|
default:
|
|
120
127
|
return false;
|
|
121
128
|
}
|
|
@@ -325,7 +332,7 @@ function createPolicyEngine(opts) {
|
|
|
325
332
|
policiesSeen: 0,
|
|
326
333
|
rulesSeen: 0,
|
|
327
334
|
matched: 0,
|
|
328
|
-
considered: { kill_switch: 0, deny: 0, throttle: 0, allow: 0 }
|
|
335
|
+
considered: { kill_switch: 0, deny: 0, throttle: 0, allow: 0, custom: 0 }
|
|
329
336
|
},
|
|
330
337
|
rules: []
|
|
331
338
|
} : null;
|
|
@@ -344,23 +351,24 @@ function createPolicyEngine(opts) {
|
|
|
344
351
|
const denies = [];
|
|
345
352
|
const throttles = [];
|
|
346
353
|
const allows = [];
|
|
354
|
+
const customs = [];
|
|
347
355
|
const policies = bundle.policies ?? [];
|
|
348
356
|
if (wantTrace) traceBase.summary.policiesSeen = policies.length;
|
|
349
357
|
for (const p of policies) {
|
|
350
358
|
const policyKey = String(p.policyKey ?? "");
|
|
351
359
|
const rules = p.rules ?? [];
|
|
360
|
+
let policyHadMatch = false;
|
|
352
361
|
for (const r of rules) {
|
|
353
362
|
if (wantTrace) traceBase.summary.rulesSeen += 1;
|
|
354
363
|
const ruleId = String(r?.id ?? "");
|
|
355
364
|
const priority = Number(r?.priority ?? 0);
|
|
356
|
-
const effectType = safeEffectType(r?.effect);
|
|
357
365
|
if (r?.status !== "active") {
|
|
358
366
|
if (wantTrace) {
|
|
359
367
|
traceBase.rules.push({
|
|
360
368
|
policyKey,
|
|
361
369
|
ruleId,
|
|
362
370
|
priority,
|
|
363
|
-
effectType,
|
|
371
|
+
effectType: safeEffectType(r?.effect),
|
|
364
372
|
matched: false,
|
|
365
373
|
discardedReason: "disabled"
|
|
366
374
|
});
|
|
@@ -373,26 +381,39 @@ function createPolicyEngine(opts) {
|
|
|
373
381
|
policyKey,
|
|
374
382
|
ruleId,
|
|
375
383
|
priority,
|
|
376
|
-
effectType,
|
|
384
|
+
effectType: safeEffectType(r?.effect),
|
|
377
385
|
matched: false,
|
|
378
386
|
discardedReason: "target_mismatch"
|
|
379
387
|
});
|
|
380
388
|
}
|
|
381
389
|
continue;
|
|
382
390
|
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
391
|
+
let resolvedEffect;
|
|
392
|
+
if (r.when !== void 0) {
|
|
393
|
+
const whenResult = evalWhen(r.when, ctx);
|
|
394
|
+
if (whenResult) {
|
|
395
|
+
resolvedEffect = r.thenEffect ?? r.effect;
|
|
396
|
+
} else {
|
|
397
|
+
if (r.elseEffect) {
|
|
398
|
+
resolvedEffect = r.elseEffect;
|
|
399
|
+
} else {
|
|
400
|
+
if (wantTrace) {
|
|
401
|
+
traceBase.rules.push({
|
|
402
|
+
policyKey,
|
|
403
|
+
ruleId,
|
|
404
|
+
priority,
|
|
405
|
+
effectType: safeEffectType(r?.effect),
|
|
406
|
+
matched: false,
|
|
407
|
+
discardedReason: "when_false"
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
393
412
|
}
|
|
394
|
-
|
|
413
|
+
} else {
|
|
414
|
+
resolvedEffect = r.effect;
|
|
395
415
|
}
|
|
416
|
+
const effectType = safeEffectType(resolvedEffect);
|
|
396
417
|
if (!effectType) {
|
|
397
418
|
if (wantTrace) {
|
|
398
419
|
traceBase.rules.push({
|
|
@@ -406,6 +427,7 @@ function createPolicyEngine(opts) {
|
|
|
406
427
|
}
|
|
407
428
|
continue;
|
|
408
429
|
}
|
|
430
|
+
policyHadMatch = true;
|
|
409
431
|
if (wantTrace) {
|
|
410
432
|
traceBase.summary.matched += 1;
|
|
411
433
|
traceBase.rules.push({
|
|
@@ -416,7 +438,7 @@ function createPolicyEngine(opts) {
|
|
|
416
438
|
matched: true
|
|
417
439
|
});
|
|
418
440
|
}
|
|
419
|
-
const m = { policyKey, ruleId, priority, effect:
|
|
441
|
+
const m = { policyKey, ruleId, priority, effect: resolvedEffect };
|
|
420
442
|
if (effectType === "kill_switch") {
|
|
421
443
|
kills.push(m);
|
|
422
444
|
if (wantTrace) traceBase.summary.considered.kill_switch += 1;
|
|
@@ -429,16 +451,55 @@ function createPolicyEngine(opts) {
|
|
|
429
451
|
} else if (effectType === "allow") {
|
|
430
452
|
allows.push(m);
|
|
431
453
|
if (wantTrace) traceBase.summary.considered.allow += 1;
|
|
454
|
+
} else if (effectType === "custom") {
|
|
455
|
+
customs.push(m);
|
|
456
|
+
if (wantTrace) traceBase.summary.considered.custom += 1;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (!policyHadMatch) {
|
|
460
|
+
const defaults = p.defaults;
|
|
461
|
+
if (defaults?.effect) {
|
|
462
|
+
const defEffectType = String(defaults.effect);
|
|
463
|
+
let syntheticEffect;
|
|
464
|
+
if (defEffectType === "custom") {
|
|
465
|
+
syntheticEffect = { type: "custom", value: String(defaults.customEffect ?? "") };
|
|
466
|
+
} else if (defEffectType === "throttle" && defaults.throttle) {
|
|
467
|
+
syntheticEffect = { type: "throttle", throttle: defaults.throttle };
|
|
468
|
+
} else if (defEffectType === "kill_switch" && defaults.killSwitch) {
|
|
469
|
+
syntheticEffect = { type: "kill_switch", killSwitch: defaults.killSwitch };
|
|
470
|
+
} else if (defEffectType === "allow" || defEffectType === "deny") {
|
|
471
|
+
syntheticEffect = { type: defEffectType };
|
|
472
|
+
}
|
|
473
|
+
if (syntheticEffect) {
|
|
474
|
+
const dm = { policyKey, ruleId: "__default__", priority: -1, effect: syntheticEffect };
|
|
475
|
+
if (defEffectType === "kill_switch") {
|
|
476
|
+
kills.push(dm);
|
|
477
|
+
if (wantTrace) traceBase.summary.considered.kill_switch += 1;
|
|
478
|
+
} else if (defEffectType === "deny") {
|
|
479
|
+
denies.push(dm);
|
|
480
|
+
if (wantTrace) traceBase.summary.considered.deny += 1;
|
|
481
|
+
} else if (defEffectType === "throttle") {
|
|
482
|
+
throttles.push(dm);
|
|
483
|
+
if (wantTrace) traceBase.summary.considered.throttle += 1;
|
|
484
|
+
} else if (defEffectType === "allow") {
|
|
485
|
+
allows.push(dm);
|
|
486
|
+
if (wantTrace) traceBase.summary.considered.allow += 1;
|
|
487
|
+
} else if (defEffectType === "custom") {
|
|
488
|
+
customs.push(dm);
|
|
489
|
+
if (wantTrace) traceBase.summary.considered.custom += 1;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
432
492
|
}
|
|
433
493
|
}
|
|
434
494
|
}
|
|
435
495
|
if (kills.length) {
|
|
436
496
|
const w = kills.sort(byRuleOrder)[0];
|
|
497
|
+
const isDefault = w.ruleId === "__default__";
|
|
437
498
|
const decision2 = {
|
|
438
499
|
decision: "kill_switch",
|
|
439
|
-
reason: "rule",
|
|
500
|
+
reason: isDefault ? "default" : "rule",
|
|
440
501
|
policyKey: w.policyKey,
|
|
441
|
-
ruleId: w.ruleId,
|
|
502
|
+
ruleId: isDefault ? void 0 : w.ruleId,
|
|
442
503
|
killSwitch: w.effect.killSwitch
|
|
443
504
|
};
|
|
444
505
|
if (!wantTrace) return decision2;
|
|
@@ -452,11 +513,12 @@ function createPolicyEngine(opts) {
|
|
|
452
513
|
}
|
|
453
514
|
if (denies.length) {
|
|
454
515
|
const w = denies.sort(byRuleOrder)[0];
|
|
516
|
+
const isDefault = w.ruleId === "__default__";
|
|
455
517
|
const decision2 = {
|
|
456
518
|
decision: "deny",
|
|
457
|
-
reason: "rule",
|
|
519
|
+
reason: isDefault ? "default" : "rule",
|
|
458
520
|
policyKey: w.policyKey,
|
|
459
|
-
ruleId: w.ruleId
|
|
521
|
+
ruleId: isDefault ? void 0 : w.ruleId
|
|
460
522
|
};
|
|
461
523
|
if (!wantTrace) return decision2;
|
|
462
524
|
return {
|
|
@@ -476,11 +538,12 @@ function createPolicyEngine(opts) {
|
|
|
476
538
|
if (A.windowSeconds !== B.windowSeconds) return A.windowSeconds - B.windowSeconds;
|
|
477
539
|
return byRuleOrder(a, b);
|
|
478
540
|
})[0];
|
|
541
|
+
const isDefault = w.ruleId === "__default__";
|
|
479
542
|
const decision2 = {
|
|
480
543
|
decision: "throttle",
|
|
481
|
-
reason: "rule",
|
|
544
|
+
reason: isDefault ? "default" : "rule",
|
|
482
545
|
policyKey: w.policyKey,
|
|
483
|
-
ruleId: w.ruleId,
|
|
546
|
+
ruleId: isDefault ? void 0 : w.ruleId,
|
|
484
547
|
throttle: w.effect.throttle
|
|
485
548
|
};
|
|
486
549
|
if (!wantTrace) return decision2;
|
|
@@ -494,11 +557,12 @@ function createPolicyEngine(opts) {
|
|
|
494
557
|
}
|
|
495
558
|
if (allows.length) {
|
|
496
559
|
const w = allows.sort(byRuleOrder)[0];
|
|
560
|
+
const isDefault = w.ruleId === "__default__";
|
|
497
561
|
const decision2 = {
|
|
498
562
|
decision: "allow",
|
|
499
|
-
reason: "rule",
|
|
563
|
+
reason: isDefault ? "default" : "rule",
|
|
500
564
|
policyKey: w.policyKey,
|
|
501
|
-
ruleId: w.ruleId
|
|
565
|
+
ruleId: isDefault ? void 0 : w.ruleId
|
|
502
566
|
};
|
|
503
567
|
if (!wantTrace) return decision2;
|
|
504
568
|
return {
|
|
@@ -509,6 +573,34 @@ function createPolicyEngine(opts) {
|
|
|
509
573
|
}
|
|
510
574
|
};
|
|
511
575
|
}
|
|
576
|
+
if (customs.length) {
|
|
577
|
+
const w = customs.sort(byRuleOrder)[0];
|
|
578
|
+
const isDefault = w.ruleId === "__default__";
|
|
579
|
+
const rawValue = String(w.effect.value ?? "");
|
|
580
|
+
let parsedValue;
|
|
581
|
+
if (opts.parseCustomEffect) {
|
|
582
|
+
try {
|
|
583
|
+
parsedValue = JSON.parse(rawValue);
|
|
584
|
+
} catch {
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const decision2 = {
|
|
588
|
+
decision: "custom",
|
|
589
|
+
reason: isDefault ? "default" : "rule",
|
|
590
|
+
policyKey: w.policyKey,
|
|
591
|
+
ruleId: isDefault ? void 0 : w.ruleId,
|
|
592
|
+
value: rawValue,
|
|
593
|
+
...parsedValue !== void 0 ? { parsedValue } : {}
|
|
594
|
+
};
|
|
595
|
+
if (!wantTrace) return decision2;
|
|
596
|
+
return {
|
|
597
|
+
...decision2,
|
|
598
|
+
trace: {
|
|
599
|
+
...traceBase,
|
|
600
|
+
winner: { policyKey: w.policyKey, ruleId: w.ruleId, effectType: "custom", priority: w.priority }
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
512
604
|
const decision = { decision: "deny", reason: "default" };
|
|
513
605
|
if (!wantTrace) return decision;
|
|
514
606
|
return {
|
|
@@ -612,6 +704,7 @@ var RuntimeClient = class {
|
|
|
612
704
|
installedSignalHandler = false;
|
|
613
705
|
sigHandler;
|
|
614
706
|
constructor(config) {
|
|
707
|
+
console.log("[RuntimeClient] initializing with config");
|
|
615
708
|
this.cfg = {
|
|
616
709
|
...config,
|
|
617
710
|
pollMs: config.pollMs ?? 5e3,
|
|
@@ -630,6 +723,7 @@ var RuntimeClient = class {
|
|
|
630
723
|
getBundle: () => this.cache.bundle,
|
|
631
724
|
validateContext: config.engine?.validateContext !== false,
|
|
632
725
|
contextPolicy: config.engine?.contextPolicy,
|
|
726
|
+
parseCustomEffect: config.engine?.parseCustomEffect,
|
|
633
727
|
traceDefaults: config.trace?.defaults,
|
|
634
728
|
traceSink: config.trace?.onDecisionTrace,
|
|
635
729
|
traceSinkAsync: config.trace?.onDecisionTraceAsync,
|
|
@@ -826,34 +920,38 @@ var RuntimeClient = class {
|
|
|
826
920
|
}
|
|
827
921
|
bundleUrl() {
|
|
828
922
|
const u = new URL(this.cfg.baseUrl);
|
|
829
|
-
u.pathname = "/v1/runtime/bundle";
|
|
830
|
-
u.searchParams.set("projectId", this.cfg.projectId);
|
|
831
|
-
u.searchParams.set("env", this.cfg.env);
|
|
832
923
|
return u.toString();
|
|
833
924
|
}
|
|
834
925
|
commonHeaders(extra) {
|
|
835
926
|
return {
|
|
836
|
-
Authorization:
|
|
927
|
+
Authorization: this.cfg.runtimeKey,
|
|
837
928
|
"User-Agent": this.cfg.userAgent ?? "govplane-runtime-sdk/0.x",
|
|
838
929
|
...extra
|
|
839
930
|
};
|
|
840
931
|
}
|
|
932
|
+
async fetchWithTimeout(url, init) {
|
|
933
|
+
const ac = new AbortController();
|
|
934
|
+
const t = setTimeout(() => ac.abort(), this.cfg.timeoutMs);
|
|
935
|
+
try {
|
|
936
|
+
return await fetch(url, { ...init, signal: ac.signal });
|
|
937
|
+
} finally {
|
|
938
|
+
clearTimeout(t);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
841
941
|
async headBundle() {
|
|
842
942
|
const url = this.bundleUrl();
|
|
843
|
-
const
|
|
943
|
+
const res = await this.fetchWithTimeout(url, {
|
|
844
944
|
method: "HEAD",
|
|
845
|
-
headers: this.commonHeaders()
|
|
846
|
-
bodyTimeout: this.cfg.timeoutMs,
|
|
847
|
-
headersTimeout: this.cfg.timeoutMs
|
|
945
|
+
headers: this.commonHeaders()
|
|
848
946
|
});
|
|
849
|
-
|
|
850
|
-
if (
|
|
851
|
-
if (
|
|
852
|
-
if (
|
|
853
|
-
const etag = headers
|
|
947
|
+
console.log(`[RuntimeClient] HEAD bundle status=${res.status}`);
|
|
948
|
+
if (res.status === 401 || res.status === 403) throw new Error(`Unauthorized (${res.status})`);
|
|
949
|
+
if (res.status >= 500) throw new Error(`Runtime server error (${res.status})`);
|
|
950
|
+
if (res.status !== 200 && res.status !== 304) return this.cache.meta;
|
|
951
|
+
const etag = res.headers.get("etag") ?? "";
|
|
854
952
|
if (!etag) return this.cache.meta;
|
|
855
|
-
const bundleVersionRaw = headers
|
|
856
|
-
const updatedAt = headers
|
|
953
|
+
const bundleVersionRaw = res.headers.get("x-gp-bundle-version") ?? void 0;
|
|
954
|
+
const updatedAt = res.headers.get("x-gp-updated-at") ?? void 0;
|
|
857
955
|
return {
|
|
858
956
|
etag,
|
|
859
957
|
bundleVersion: bundleVersionRaw ? Number(bundleVersionRaw) : void 0,
|
|
@@ -863,26 +961,24 @@ var RuntimeClient = class {
|
|
|
863
961
|
async getBundle() {
|
|
864
962
|
const url = this.bundleUrl();
|
|
865
963
|
const ifNoneMatch = this.cache.meta?.etag;
|
|
866
|
-
const
|
|
964
|
+
const res = await this.fetchWithTimeout(url, {
|
|
867
965
|
method: "GET",
|
|
868
|
-
headers: this.commonHeaders(ifNoneMatch ? { "If-None-Match": ifNoneMatch } : void 0)
|
|
869
|
-
bodyTimeout: this.cfg.timeoutMs,
|
|
870
|
-
headersTimeout: this.cfg.timeoutMs
|
|
966
|
+
headers: this.commonHeaders(ifNoneMatch ? { "If-None-Match": ifNoneMatch } : void 0)
|
|
871
967
|
});
|
|
872
|
-
const txt = await
|
|
873
|
-
if (
|
|
874
|
-
const etag2 = headers
|
|
875
|
-
const bundleVersionRaw2 = headers
|
|
876
|
-
const updatedAt2 = headers
|
|
968
|
+
const txt = await res.text();
|
|
969
|
+
if (res.status === 304) {
|
|
970
|
+
const etag2 = res.headers.get("etag") ?? ifNoneMatch ?? "";
|
|
971
|
+
const bundleVersionRaw2 = res.headers.get("x-gp-bundle-version") ?? void 0;
|
|
972
|
+
const updatedAt2 = res.headers.get("x-gp-updated-at") ?? void 0;
|
|
877
973
|
const meta2 = etag2 ? { etag: etag2, bundleVersion: bundleVersionRaw2 ? Number(bundleVersionRaw2) : void 0, updatedAt: updatedAt2 } : this.cache.meta;
|
|
878
974
|
if (meta2) this.cache.meta = meta2;
|
|
879
975
|
return { changed: false, meta: meta2 };
|
|
880
976
|
}
|
|
881
|
-
if (
|
|
882
|
-
if (
|
|
883
|
-
const etag = headers
|
|
884
|
-
const bundleVersionRaw = headers
|
|
885
|
-
const updatedAt = headers
|
|
977
|
+
if (res.status === 401 || res.status === 403) throw new Error(`Unauthorized (${res.status})`);
|
|
978
|
+
if (res.status >= 400) throw new Error(`Runtime HTTP error (${res.status}): ${txt.slice(0, 200)}`);
|
|
979
|
+
const etag = res.headers.get("etag") ?? "";
|
|
980
|
+
const bundleVersionRaw = res.headers.get("x-gp-bundle-version") ?? void 0;
|
|
981
|
+
const updatedAt = res.headers.get("x-gp-updated-at") ?? void 0;
|
|
886
982
|
const meta = {
|
|
887
983
|
etag: etag || (ifNoneMatch ?? ""),
|
|
888
984
|
bundleVersion: bundleVersionRaw ? Number(bundleVersionRaw) : void 0,
|