@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/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: string;
81
- ruleId: string;
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: string;
90
- ruleId: string;
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
- "ctx.plan",
15
- "ctx.country",
16
- "ctx.requestTier",
17
- "ctx.feature",
18
- "ctx.amount",
19
- "ctx.isAuthenticated",
20
- "ctx.role"
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
- const path = `ctx.${k}`;
54
- if (!allowed.has(path)) {
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: ${path}`);
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: ${path}`);
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: ${path}`);
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: ${path}`);
75
- if (it.length > maxStringLen) throw new Error(`Invalid array value length: ${path}`);
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 ${path}`);
75
+ throw new Error(`Invalid context type for ${k}`);
80
76
  }
81
77
  }
82
78
 
83
79
  // src/engine/when.ts
84
- function getPath(obj, path) {
85
- const parts = path.split(".");
86
- let cur = obj;
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
- return node.args.every((n) => evalWhen(n, ctx));
97
- case "or":
98
- return node.args.some((n) => evalWhen(n, ctx));
99
- case "not":
100
- return !evalWhen(node.arg, ctx);
101
- case "exists":
102
- return getPath({ ctx }, node.path) !== void 0;
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({ ctx }, node.path);
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({ ctx }, node.path) === node.value;
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({ ctx }, node.path) !== node.value;
117
+ return getPath(ctx, node.path) !== node.value;
111
118
  case "gt":
112
- return Number(getPath({ ctx }, node.path)) > Number(node.value);
119
+ return Number(getPath(ctx, node.path)) > Number(node.value);
113
120
  case "gte":
114
- return Number(getPath({ ctx }, node.path)) >= Number(node.value);
121
+ return Number(getPath(ctx, node.path)) >= Number(node.value);
115
122
  case "lt":
116
- return Number(getPath({ ctx }, node.path)) < Number(node.value);
123
+ return Number(getPath(ctx, node.path)) < Number(node.value);
117
124
  case "lte":
118
- return Number(getPath({ ctx }, node.path)) <= Number(node.value);
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
- if (r.when && !evalWhen(r.when, ctx)) {
384
- if (wantTrace) {
385
- traceBase.rules.push({
386
- policyKey,
387
- ruleId,
388
- priority,
389
- effectType,
390
- matched: false,
391
- discardedReason: "when_false"
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
- continue;
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: r.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: `Bearer ${this.cfg.runtimeKey}`,
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 { statusCode, headers, body } = await request(url, {
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
- await body.text().catch(() => void 0);
850
- if (statusCode === 401 || statusCode === 403) throw new Error(`Unauthorized (${statusCode})`);
851
- if (statusCode >= 500) throw new Error(`Runtime server error (${statusCode})`);
852
- if (statusCode !== 200 && statusCode !== 304) return this.cache.meta;
853
- const etag = headers["etag"] ?? "";
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["x-gp-bundle-version"];
856
- const updatedAt = headers["x-gp-updated-at"];
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 { statusCode, headers, body } = await request(url, {
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 body.text();
873
- if (statusCode === 304) {
874
- const etag2 = headers["etag"] ?? ifNoneMatch ?? "";
875
- const bundleVersionRaw2 = headers["x-gp-bundle-version"];
876
- const updatedAt2 = headers["x-gp-updated-at"];
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 (statusCode === 401 || statusCode === 403) throw new Error(`Unauthorized (${statusCode})`);
882
- if (statusCode >= 400) throw new Error(`Runtime HTTP error (${statusCode}): ${txt.slice(0, 200)}`);
883
- const etag = headers["etag"] ?? "";
884
- const bundleVersionRaw = headers["x-gp-bundle-version"];
885
- const updatedAt = headers["x-gp-updated-at"];
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,