@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.
@@ -0,0 +1,474 @@
1
+ ---
2
+ description: >-
3
+ Install the Govplane Runtime SDK and make your first policy evaluation in
4
+ under five minutes.
5
+ ---
6
+
7
+ # Installation & Quick Start
8
+
9
+ ## Requirements
10
+
11
+ * **Node.js** 18 or later
12
+ * **TypeScript** 4.9+ (if using TypeScript)
13
+
14
+ ## Install
15
+
16
+ {% tabs %}
17
+ {% tab title="npm" %}
18
+ ```bash
19
+ npm install @govplane/runtime-sdk
20
+ ```
21
+ {% endtab %}
22
+
23
+ {% tab title="yarn" %}
24
+ ```bash
25
+ yarn add @govplane/runtime-sdk
26
+ ```
27
+ {% endtab %}
28
+
29
+ {% tab title="pnpm" %}
30
+ ```bash
31
+ pnpm add @govplane/runtime-sdk
32
+ ```
33
+ {% endtab %}
34
+ {% endtabs %}
35
+
36
+ {% hint style="info" %}
37
+ `undici` is bundled in Node 18+. If you are targeting Node 16 install it explicitly: `npm install undici`.
38
+ {% endhint %}
39
+
40
+ ---
41
+
42
+ ## Quick start
43
+
44
+ ```typescript
45
+ import { RuntimeClient } from "@govplane/runtime-sdk";
46
+
47
+ const client = new RuntimeClient({
48
+ baseUrl: "https://runtime.govplane.io",
49
+ runtimeKey: "rk_live_••••••••",
50
+ });
51
+
52
+ await client.warmStart(); // block until the first bundle is cached
53
+ client.start(); // start background polling (every 5 s by default)
54
+
55
+ const result = client.evaluate({
56
+ target: { service: "api", resource: "documents", action: "read" },
57
+ context: { user: { role: "viewer" } },
58
+ });
59
+
60
+ console.log(result.decision); // "allow" | "deny" | "throttle" | "kill_switch" | "custom"
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Effect types
66
+
67
+ The engine returns one of five decision types.
68
+
69
+ | `decision` | When it fires |
70
+ |----------------|--------------------------------------------------------------------|
71
+ | `allow` | An `allow` rule matched, or the policy default is `allow`. |
72
+ | `deny` | A `deny` rule matched, or no rule matched (deny-by-default). |
73
+ | `kill_switch` | A `kill_switch` rule or policy default is active. |
74
+ | `throttle` | A `throttle` rule or policy default matched. |
75
+ | `custom` | A `custom` rule or policy default matched (carries a string value).|
76
+
77
+ Precedence (highest → lowest): `kill_switch > deny > throttle > allow > custom > deny-by-default`
78
+
79
+ ---
80
+
81
+ ## Custom effects
82
+
83
+ A `custom` effect lets the bundle carry an arbitrary string — including a
84
+ JSON-encoded object — back to the caller. This is useful for feature-flag
85
+ responses, A/B test variants, or any structured metadata that does not fit the
86
+ standard allow/deny/throttle shape.
87
+
88
+ ### Bundle shape
89
+
90
+ ```json
91
+ {
92
+ "id": "rule-feature-flags",
93
+ "status": "active",
94
+ "priority": 10,
95
+ "target": { "service": "app", "resource": "feature/checkout-v2", "action": "read" },
96
+ "effect": {
97
+ "type": "custom",
98
+ "value": "{\"enabled\": true, \"variant\": \"B\"}"
99
+ }
100
+ }
101
+ ```
102
+
103
+ ### Evaluating a custom effect (raw string)
104
+
105
+ ```typescript
106
+ const result = client.evaluate({
107
+ target: { service: "app", resource: "feature/checkout-v2", action: "read" },
108
+ context: { user: { id: "u_123" } },
109
+ });
110
+
111
+ if (result.decision === "custom") {
112
+ console.log(result.value); // '{"enabled": true, "variant": "B"}'
113
+ }
114
+ ```
115
+
116
+ ### Automatically parsing the JSON value
117
+
118
+ Pass `engine.parseCustomEffect: true` to have the SDK call `JSON.parse()`
119
+ on the custom value and attach the result as `parsedValue`. Non-JSON strings
120
+ are silently left as `undefined` in `parsedValue` so the raw `value` is
121
+ always available as a fallback.
122
+
123
+ ```typescript
124
+ const client = new RuntimeClient({
125
+ baseUrl: "https://runtime.govplane.io",
126
+ runtimeKey: "rk_live_••••••••",
127
+ engine: {
128
+ parseCustomEffect: true, // ← enable JSON parsing
129
+ },
130
+ });
131
+
132
+ const result = client.evaluate({
133
+ target: { service: "app", resource: "feature/checkout-v2", action: "read" },
134
+ context: { user: { id: "u_123" } },
135
+ });
136
+
137
+ if (result.decision === "custom") {
138
+ const flags = result.parsedValue as { enabled: boolean; variant: string };
139
+ console.log(flags.enabled, flags.variant); // true "B"
140
+ }
141
+ ```
142
+
143
+ ### Using `createPolicyEngine` directly
144
+
145
+ If you manage the bundle yourself (e.g. loaded from a file), you can create
146
+ the engine without `RuntimeClient`:
147
+
148
+ ```typescript
149
+ import { createPolicyEngine } from "@govplane/runtime-sdk";
150
+
151
+ const bundle = JSON.parse(await fs.readFile("bundle.json", "utf8"));
152
+
153
+ const engine = createPolicyEngine({
154
+ getBundle: () => bundle,
155
+ parseCustomEffect: true,
156
+ });
157
+
158
+ const result = engine.evaluate({
159
+ target: { service: "api", resource: "public", action: "read" },
160
+ context: { user: { role: "editor" }, tenant: { flags: { premium: true } } },
161
+ });
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Policy-level default effects
167
+
168
+ Every policy can declare a `defaults` object that fires when **no rule in that
169
+ policy produces a match** for the requested target + context. Defaults support
170
+ all five effect types.
171
+
172
+ ### `custom` default
173
+
174
+ ```json
175
+ {
176
+ "policyKey": "feature-flags",
177
+ "activeVersion": 7,
178
+ "defaults": {
179
+ "effect": "custom",
180
+ "customEffect": "{\"test\": true, \"allow\": true}"
181
+ },
182
+ "rules": []
183
+ }
184
+ ```
185
+
186
+ When evaluated with `engine.parseCustomEffect: true` the decision is:
187
+
188
+ ```typescript
189
+ {
190
+ decision: "custom",
191
+ reason: "default",
192
+ policyKey: "feature-flags",
193
+ value: '{"test": true, "allow": true}',
194
+ parsedValue: { test: true, allow: true }
195
+ }
196
+ ```
197
+
198
+ ### `allow` / `deny` defaults
199
+
200
+ ```json
201
+ {
202
+ "policyKey": "open-api",
203
+ "defaults": { "effect": "allow" },
204
+ "rules": [
205
+ {
206
+ "id": "block-admin",
207
+ "status": "active",
208
+ "priority": 1,
209
+ "target": { "service": "api", "resource": "admin", "action": "*" },
210
+ "effect": { "type": "deny" }
211
+ }
212
+ ]
213
+ }
214
+ ```
215
+
216
+ The `admin` resource is denied by the rule; everything else falls through to
217
+ the `allow` default.
218
+
219
+ ### `throttle` default
220
+
221
+ ```json
222
+ {
223
+ "policyKey": "rate-limited-api",
224
+ "defaults": {
225
+ "effect": "throttle",
226
+ "throttle": { "limit": 100, "windowSeconds": 60, "key": "tenant" }
227
+ },
228
+ "rules": []
229
+ }
230
+ ```
231
+
232
+ ### `kill_switch` default
233
+
234
+ ```json
235
+ {
236
+ "policyKey": "payments",
237
+ "defaults": {
238
+ "effect": "kill_switch",
239
+ "killSwitch": { "service": "payments", "reason": "Scheduled maintenance" }
240
+ },
241
+ "rules": []
242
+ }
243
+ ```
244
+
245
+ ---
246
+
247
+ ## Conditional rules (`when` / `thenEffect` / `elseEffect`)
248
+
249
+ Rules can carry a `when` clause, which is a condition AST evaluated
250
+ client-side against the `context` you pass at call time.
251
+
252
+ ```typescript
253
+ const result = client.evaluate({
254
+ target: { service: "api", resource: "export", action: "create" },
255
+ context: {
256
+ user: { role: "editor", emailVerified: true },
257
+ tenant: { plan: "enterprise" },
258
+ },
259
+ });
260
+ ```
261
+
262
+ ### `thenEffect` / `elseEffect`
263
+
264
+ These optional fields allow different effects depending on whether `when`
265
+ evaluates to `true` or `false`, without needing two separate rules.
266
+
267
+ ```json
268
+ {
269
+ "id": "rule-beta-toggle",
270
+ "status": "active",
271
+ "priority": 10,
272
+ "target": { "service": "app", "resource": "feature/beta", "action": "read" },
273
+
274
+ "when": { "op": "eq", "path": "user.beta_tester", "value": true },
275
+ "thenEffect": { "type": "allow" },
276
+ "elseEffect": { "type": "deny" },
277
+
278
+ "effect": { "type": "deny" }
279
+ }
280
+ ```
281
+
282
+ | `when` | Effect used |
283
+ |----------|---------------------------|
284
+ | `true` | `thenEffect` → `effect` |
285
+ | `false` | `elseEffect` → skip rule |
286
+ | absent | `effect` always |
287
+
288
+ ### Condition operators
289
+
290
+ | `op` | Description | Example |
291
+ |----------|-----------------------------------------------|---------------------------------------------------------|
292
+ | `eq` | Equal | `{"op":"eq","path":"user.role","value":"admin"}` |
293
+ | `neq` | Not equal | `{"op":"neq","path":"tenant.plan","value":"free"}` |
294
+ | `gt/gte` | Greater than (or equal) | `{"op":"gte","path":"user.level","value":3}` |
295
+ | `lt/lte` | Less than (or equal) | `{"op":"lte","path":"request.size","value":1048576}` |
296
+ | `in` | Value is one of a list | `{"op":"in","path":"user.role","values":["admin","moderator"]}` |
297
+ | `exists` | Path is present and not null/undefined | `{"op":"exists","path":"tenant.flags.premium"}` |
298
+ | `and` | All child conditions must be true | see below |
299
+ | `or` | At least one child condition must be true | see below |
300
+ | `not` | Inverts a single child condition | `{"op":"not","condition":{"op":"eq","path":"user.status","value":"banned"}}` |
301
+
302
+ ```json
303
+ {
304
+ "op": "and",
305
+ "conditions": [
306
+ { "op": "eq", "path": "user.emailVerified", "value": true },
307
+ {
308
+ "op": "or",
309
+ "conditions": [
310
+ { "op": "eq", "path": "user.role", "value": "admin" },
311
+ { "op": "eq", "path": "tenant.plan", "value": "enterprise" }
312
+ ]
313
+ }
314
+ ]
315
+ }
316
+ ```
317
+
318
+ ---
319
+
320
+ ## Real-world examples
321
+
322
+ ### 1 — Feature flags with a JSON custom effect
323
+
324
+ The bundle encodes feature-flag state for the whole application in a single
325
+ `custom` policy. The SDK caller reads the parsed object and activates
326
+ features accordingly.
327
+
328
+ ```typescript
329
+ // Evaluate feature flags for the current user
330
+ const result = client.evaluate({
331
+ target: { service: "app", resource: "features", action: "read" },
332
+ context: { user: { id: "u_abc", plan: "pro" } },
333
+ });
334
+
335
+ if (result.decision === "custom" && result.parsedValue) {
336
+ const flags = result.parsedValue as Record<string, boolean>;
337
+ if (flags["new-checkout"]) enableNewCheckout();
338
+ if (flags["ai-suggestions"]) enableAI();
339
+ }
340
+ ```
341
+
342
+ Corresponding bundle rule:
343
+
344
+ ```json
345
+ {
346
+ "id": "rule-feature-flags-pro",
347
+ "status": "active",
348
+ "priority": 20,
349
+ "target": { "service": "app", "resource": "features", "action": "read" },
350
+ "when": { "op": "eq", "path": "user.plan", "value": "pro" },
351
+ "thenEffect": {
352
+ "type": "custom",
353
+ "value": "{\"new-checkout\": true, \"ai-suggestions\": true}"
354
+ },
355
+ "elseEffect": {
356
+ "type": "custom",
357
+ "value": "{\"new-checkout\": false, \"ai-suggestions\": false}"
358
+ },
359
+ "effect": { "type": "deny" }
360
+ }
361
+ ```
362
+
363
+ ### 2 — RBAC with policy default deny
364
+
365
+ Only admin/superuser roles can write settings. Everything else is denied by
366
+ the policy default, so no explicit deny rules are needed for other resources.
367
+
368
+ ```typescript
369
+ const result = client.evaluate({
370
+ target: { service: "control", resource: "settings", action: "write" },
371
+ context: { user: { role: "viewer" } },
372
+ });
373
+ // → { decision: "deny", reason: "default", policyKey: "rbac-settings" }
374
+ ```
375
+
376
+ Bundle:
377
+
378
+ ```json
379
+ {
380
+ "policyKey": "rbac-settings",
381
+ "defaults": { "effect": "deny" },
382
+ "rules": [
383
+ {
384
+ "id": "allow-admins",
385
+ "status": "active",
386
+ "priority": 10,
387
+ "target": { "service": "control", "resource": "settings", "action": "write" },
388
+ "when": {
389
+ "op": "in",
390
+ "path": "user.role",
391
+ "values": ["admin", "superuser"]
392
+ },
393
+ "thenEffect": { "type": "allow" },
394
+ "effect": { "type": "deny" }
395
+ }
396
+ ]
397
+ }
398
+ ```
399
+
400
+ ### 3 — Plan-gated throttling with a custom default
401
+
402
+ Free-plan users are throttled. Paid users get a `custom` response carrying
403
+ their effective rate-limit metadata for the UI to display.
404
+
405
+ ```typescript
406
+ const result = client.evaluate({
407
+ target: { service: "api", resource: "export", action: "create" },
408
+ context: { tenant: { plan: "free" } },
409
+ });
410
+
411
+ if (result.decision === "throttle") {
412
+ reply.status(429).send({ retryAfter: result.throttle.windowSeconds });
413
+ }
414
+ if (result.decision === "custom" && result.parsedValue) {
415
+ // pass metadata through to the client
416
+ reply.header("X-Rate-Info", JSON.stringify(result.parsedValue));
417
+ }
418
+ ```
419
+
420
+ ### 4 — Kill switch via policy default
421
+
422
+ Immediately stop all traffic to the payments service by pushing a new bundle
423
+ with a `kill_switch` default. No code deployment required.
424
+
425
+ ```json
426
+ {
427
+ "policyKey": "payments-circuit-breaker",
428
+ "defaults": {
429
+ "effect": "kill_switch",
430
+ "killSwitch": { "service": "payments", "reason": "Database degradation detected" }
431
+ },
432
+ "rules": []
433
+ }
434
+ ```
435
+
436
+ ```typescript
437
+ const result = client.evaluate({
438
+ target: { service: "payments", resource: "checkout", action: "create" },
439
+ context: { user: { id: "u_xyz" } },
440
+ });
441
+
442
+ if (result.decision === "kill_switch") {
443
+ throw new ServiceUnavailableError(result.killSwitch.reason);
444
+ }
445
+ ```
446
+
447
+ ---
448
+
449
+ ## Configuration reference
450
+
451
+ ```typescript
452
+ new RuntimeClient({
453
+ // Required
454
+ baseUrl: "https://runtime.govplane.io",
455
+ runtimeKey: "rk_live_••••••••",
456
+
457
+ // Polling
458
+ pollMs: 5000, // default poll interval
459
+ burstPollMs: 500, // burst poll interval
460
+ burstDurationMs: 30000, // how long burst lasts
461
+
462
+ // Engine
463
+ engine: {
464
+ validateContext: true, // validate context shape (default true)
465
+ parseCustomEffect: true, // JSON-parse custom effect values (default false)
466
+ },
467
+
468
+ // Decision tracing (zero PII)
469
+ trace: {
470
+ defaults: { level: "sampled", sampling: 0.05 },
471
+ onDecisionTrace: (evt) => logger.info(evt),
472
+ },
473
+ });
474
+ ```
@@ -0,0 +1,198 @@
1
+ ---
2
+ description: >-
3
+ Complete reference for every RuntimeClientConfig field, with types, defaults,
4
+ and guidance on when to change them.
5
+ ---
6
+
7
+ # Configuration Reference
8
+
9
+ ## `RuntimeClientConfig`
10
+
11
+ Passed to the `RuntimeClient` constructor.
12
+
13
+ ```typescript
14
+ import { RuntimeClient } from "@govplane/runtime-sdk";
15
+
16
+ const client = new RuntimeClient({ ...config });
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Required fields
22
+
23
+ | Field | Type | Description |
24
+ |---|---|---|
25
+ | `baseUrl` | `string` | Base URL of the Govplane bundle endpoint. |
26
+ | `runtimeKey` | `string` | Runtime API key (`rk_live_…` or `rk_test_…`). |
27
+
28
+ ---
29
+
30
+ ## Polling
31
+
32
+ | Field | Type | Default | Description |
33
+ |---|---|---|---|
34
+ | `pollMs` | `number` | `5000` | Normal polling interval in milliseconds. |
35
+ | `burstPollMs` | `number` | `500` | Burst polling interval. Used during burst mode and incident activation. |
36
+ | `burstDurationMs` | `number` | `30000` | How long burst mode stays active after triggering (ms). |
37
+
38
+ ---
39
+
40
+ ## HTTP
41
+
42
+ | Field | Type | Default | Description |
43
+ |---|---|---|---|
44
+ | `timeoutMs` | `number` | `5000` | HTTP request timeout in milliseconds (applies to each HEAD and GET). |
45
+ | `userAgent` | `string` | `govplane-runtime-sdk/0.x` | Custom `User-Agent` header value. |
46
+
47
+ ---
48
+
49
+ ## Backoff & degraded mode
50
+
51
+ | Field | Type | Default | Description |
52
+ |---|---|---|---|
53
+ | `backoffBaseMs` | `number` | `500` | Initial backoff delay in ms. The delay doubles with each consecutive failure. |
54
+ | `backoffMaxMs` | `number` | `30000` | Maximum backoff cap in ms. |
55
+ | `backoffJitter` | `number` | `0.2` | Jitter factor (`0..1`). The computed delay is varied by ±`backoffJitter × delay`. |
56
+ | `degradeAfterFailures` | `number` | `3` | Number of consecutive failures before the client reports `state: "degraded"`. |
57
+
58
+ ---
59
+
60
+ ## Engine
61
+
62
+ ```typescript
63
+ engine?: {
64
+ validateContext?: boolean; // default true
65
+ contextPolicy?: ContextPolicy;
66
+ parseCustomEffect?: boolean; // default false
67
+ }
68
+ ```
69
+
70
+ | Field | Default | Description |
71
+ |---|---|---|
72
+ | `validateContext` | `true` | Validate the `context` object against `contextPolicy` before evaluation. Set to `false` only in controlled test environments. |
73
+ | `contextPolicy` | `DEFAULT_CONTEXT_POLICY` | Allowed keys, max string/array lengths, and PII heuristic. See [Context Policy](../usage/ContextPolicy.md). |
74
+ | `parseCustomEffect` | `false` | When `true`, automatically `JSON.parse()` the `value` of any `custom` decision and attach the result as `parsedValue`. Parse errors are silently swallowed. |
75
+
76
+ ---
77
+
78
+ ## Tracing
79
+
80
+ ```typescript
81
+ trace?: {
82
+ defaults?: TraceOptions;
83
+ onDecisionTrace?: (evt: StructuredTraceEvent) => void;
84
+ onDecisionTraceAsync?: (evt: StructuredTraceEvent) => Promise<void>;
85
+ queueMax?: number;
86
+ dropPolicy?: "drop_new" | "drop_old";
87
+ onTraceError?: (err: unknown) => void;
88
+ }
89
+ ```
90
+
91
+ | Field | Default | Description |
92
+ |---|---|---|
93
+ | `defaults` | — | Default `TraceOptions` applied to every `evaluateWithTrace()` call. Can be overridden per call. |
94
+ | `onDecisionTrace` | — | Synchronous trace sink. Called immediately after each traced evaluation. |
95
+ | `onDecisionTraceAsync` | — | Async trace sink. Buffered internally; does not block evaluation. |
96
+ | `queueMax` | `1000` | Maximum events in the async queue before dropping begins. |
97
+ | `dropPolicy` | `"drop_new"` | How to handle overflow: `"drop_new"` discards the incoming event; `"drop_old"` evicts the oldest. |
98
+ | `onTraceError` | — | Called when either sink throws an error. |
99
+
100
+ ### `TraceOptions`
101
+
102
+ ```typescript
103
+ type TraceOptions = {
104
+ level?: "off" | "errors" | "sampled" | "full";
105
+ sampling?: number; // 0..1, default 0.01
106
+ force?: boolean; // bypass sampling + budget
107
+ budget?: {
108
+ maxTraces: number;
109
+ windowMs: number;
110
+ };
111
+ };
112
+ ```
113
+
114
+ | Field | Default | Description |
115
+ |---|---|---|
116
+ | `level` | `"sampled"` | Which evaluations produce a trace. |
117
+ | `sampling` | `0.01` | Fraction of evaluations traced when `level === "sampled"`. `1` = 100%. |
118
+ | `force` | `false` | Always produce a trace, bypassing sampling and budget. |
119
+ | `budget.maxTraces` | `60` | Maximum traces allowed per `budget.windowMs`. |
120
+ | `budget.windowMs` | `60000` | Sliding window for the trace budget (ms). |
121
+
122
+ ---
123
+
124
+ ## Incident controls
125
+
126
+ | Field | Type | Default | Description |
127
+ |---|---|---|---|
128
+ | `incidentEnvFlag` | `string` | `"GP_RUNTIME_INCIDENT"` | Environment variable name. Any truthy value activates burst polling. |
129
+ | `incidentFilePath` | `string` | — | Path to a JSON file polled for incident directives. |
130
+ | `incidentFilePollMs` | `number` | `1000` | How often the incident file is checked (ms). |
131
+ | `incidentSignal` | `"SIGUSR1" \| false` | `"SIGUSR1"` | Process signal that triggers an immediate burst refresh. Set to `false` to disable. |
132
+
133
+ ### Incident file format
134
+
135
+ ```json
136
+ {
137
+ "burst": true,
138
+ "burstDurationMs": 60000,
139
+ "burstPollMs": 200,
140
+ "refreshNow": true
141
+ }
142
+ ```
143
+
144
+ See the [Incident Playbook](../operations/Govplane_Incident_Playbook.md) for step-by-step procedures.
145
+
146
+ ---
147
+
148
+ ## Full example
149
+
150
+ ```typescript
151
+ import { RuntimeClient } from "@govplane/runtime-sdk";
152
+
153
+ const client = new RuntimeClient({
154
+ baseUrl: "https://runtime.govplane.io",
155
+ runtimeKey: process.env.GP_RUNTIME_KEY!,
156
+
157
+ pollMs: 10_000,
158
+ burstPollMs: 300,
159
+ burstDurationMs: 60_000,
160
+ timeoutMs: 8_000,
161
+
162
+ backoffBaseMs: 1_000,
163
+ backoffMaxMs: 60_000,
164
+ backoffJitter: 0.3,
165
+ degradeAfterFailures: 5,
166
+
167
+ engine: {
168
+ validateContext: true,
169
+ parseCustomEffect: true,
170
+ contextPolicy: {
171
+ allowedKeys: ["plan", "role", "country", "isAuthenticated", "requestTier"],
172
+ maxStringLen: 64,
173
+ maxArrayLen: 10,
174
+ blockLikelyPiiKeys: true,
175
+ },
176
+ },
177
+
178
+ trace: {
179
+ defaults: {
180
+ level: "sampled",
181
+ sampling: 0.1,
182
+ budget: { maxTraces: 120, windowMs: 60_000 },
183
+ },
184
+ onDecisionTraceAsync: async (evt) => {
185
+ await logger.shipTrace(evt);
186
+ },
187
+ queueMax: 2000,
188
+ dropPolicy: "drop_old",
189
+ onTraceError: (err) => logger.error("trace error", err),
190
+ },
191
+
192
+ incidentFilePath: "/etc/govplane/incident.json",
193
+ incidentSignal: "SIGUSR1",
194
+ });
195
+
196
+ await client.warmStart({ timeoutMs: 15_000 });
197
+ client.start();
198
+ ```