@ironflow/node 0.20.2 → 0.21.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.
Files changed (145) hide show
  1. package/dist/agent/agent.d.ts +60 -0
  2. package/dist/agent/agent.d.ts.map +1 -0
  3. package/dist/agent/agent.js +133 -0
  4. package/dist/agent/agent.js.map +1 -0
  5. package/dist/agent/approve.d.ts +23 -0
  6. package/dist/agent/approve.d.ts.map +1 -0
  7. package/dist/agent/approve.js +42 -0
  8. package/dist/agent/approve.js.map +1 -0
  9. package/dist/agent/dispatch.d.ts +63 -0
  10. package/dist/agent/dispatch.d.ts.map +1 -0
  11. package/dist/agent/dispatch.js +130 -0
  12. package/dist/agent/dispatch.js.map +1 -0
  13. package/dist/agent/errors.d.ts +90 -0
  14. package/dist/agent/errors.d.ts.map +1 -0
  15. package/dist/agent/errors.js +136 -0
  16. package/dist/agent/errors.js.map +1 -0
  17. package/dist/agent/index.d.ts +35 -0
  18. package/dist/agent/index.d.ts.map +1 -0
  19. package/dist/agent/index.js +32 -0
  20. package/dist/agent/index.js.map +1 -0
  21. package/dist/agent/internal-registry.d.ts +27 -0
  22. package/dist/agent/internal-registry.d.ts.map +1 -0
  23. package/dist/agent/internal-registry.js +36 -0
  24. package/dist/agent/internal-registry.js.map +1 -0
  25. package/dist/agent/internal.d.ts +24 -0
  26. package/dist/agent/internal.d.ts.map +1 -0
  27. package/dist/agent/internal.js +29 -0
  28. package/dist/agent/internal.js.map +1 -0
  29. package/dist/agent/llm.d.ts +39 -0
  30. package/dist/agent/llm.d.ts.map +1 -0
  31. package/dist/agent/llm.js +59 -0
  32. package/dist/agent/llm.js.map +1 -0
  33. package/dist/agent/mcp.d.ts +51 -0
  34. package/dist/agent/mcp.d.ts.map +1 -0
  35. package/dist/agent/mcp.js +155 -0
  36. package/dist/agent/mcp.js.map +1 -0
  37. package/dist/agent/memory.d.ts +74 -0
  38. package/dist/agent/memory.d.ts.map +1 -0
  39. package/dist/agent/memory.js +130 -0
  40. package/dist/agent/memory.js.map +1 -0
  41. package/dist/agent/spawn.d.ts +20 -0
  42. package/dist/agent/spawn.d.ts.map +1 -0
  43. package/dist/agent/spawn.js +29 -0
  44. package/dist/agent/spawn.js.map +1 -0
  45. package/dist/agent/tool.d.ts +39 -0
  46. package/dist/agent/tool.d.ts.map +1 -0
  47. package/dist/agent/tool.js +103 -0
  48. package/dist/agent/tool.js.map +1 -0
  49. package/dist/agent/types.d.ts +363 -0
  50. package/dist/agent/types.d.ts.map +1 -0
  51. package/dist/agent/types.js +9 -0
  52. package/dist/agent/types.js.map +1 -0
  53. package/dist/client.d.ts +942 -0
  54. package/dist/client.d.ts.map +1 -0
  55. package/dist/client.js +1557 -0
  56. package/dist/client.js.map +1 -0
  57. package/dist/command-dedup.d.ts +61 -0
  58. package/dist/command-dedup.d.ts.map +1 -0
  59. package/dist/command-dedup.js +129 -0
  60. package/dist/command-dedup.js.map +1 -0
  61. package/dist/config-client.d.ts +58 -0
  62. package/dist/config-client.d.ts.map +1 -0
  63. package/dist/config-client.js +171 -0
  64. package/dist/config-client.js.map +1 -0
  65. package/dist/function.d.ts +53 -0
  66. package/dist/function.d.ts.map +1 -0
  67. package/dist/function.js +72 -0
  68. package/dist/function.js.map +1 -0
  69. package/dist/index.d.ts +71 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +70 -0
  72. package/dist/index.js.map +1 -0
  73. package/dist/internal/assert-defined.d.ts +10 -0
  74. package/dist/internal/assert-defined.d.ts.map +1 -0
  75. package/dist/internal/assert-defined.js +15 -0
  76. package/dist/internal/assert-defined.js.map +1 -0
  77. package/dist/internal/context.d.ts +142 -0
  78. package/dist/internal/context.d.ts.map +1 -0
  79. package/dist/internal/context.js +306 -0
  80. package/dist/internal/context.js.map +1 -0
  81. package/dist/internal/errors.d.ts +66 -0
  82. package/dist/internal/errors.d.ts.map +1 -0
  83. package/dist/internal/errors.js +29 -0
  84. package/dist/internal/errors.js.map +1 -0
  85. package/dist/internal/run-context.d.ts +10 -0
  86. package/dist/internal/run-context.d.ts.map +1 -0
  87. package/dist/internal/run-context.js +23 -0
  88. package/dist/internal/run-context.js.map +1 -0
  89. package/dist/kv.d.ts +86 -0
  90. package/dist/kv.d.ts.map +1 -0
  91. package/dist/kv.js +261 -0
  92. package/dist/kv.js.map +1 -0
  93. package/dist/projection-runner.d.ts +83 -0
  94. package/dist/projection-runner.d.ts.map +1 -0
  95. package/dist/projection-runner.js +498 -0
  96. package/dist/projection-runner.js.map +1 -0
  97. package/dist/projection.d.ts +36 -0
  98. package/dist/projection.d.ts.map +1 -0
  99. package/dist/projection.js +55 -0
  100. package/dist/projection.js.map +1 -0
  101. package/dist/secrets.d.ts +6 -0
  102. package/dist/secrets.d.ts.map +1 -0
  103. package/dist/secrets.js +19 -0
  104. package/dist/secrets.js.map +1 -0
  105. package/dist/serve.d.ts +71 -0
  106. package/dist/serve.d.ts.map +1 -0
  107. package/dist/serve.js +460 -0
  108. package/dist/serve.js.map +1 -0
  109. package/dist/step.d.ts +18 -0
  110. package/dist/step.d.ts.map +1 -0
  111. package/dist/step.js +581 -0
  112. package/dist/step.js.map +1 -0
  113. package/dist/subscribe.d.ts +164 -0
  114. package/dist/subscribe.d.ts.map +1 -0
  115. package/dist/subscribe.js +487 -0
  116. package/dist/subscribe.js.map +1 -0
  117. package/dist/test/index.d.ts +22 -0
  118. package/dist/test/index.d.ts.map +1 -0
  119. package/dist/test/index.js +112 -0
  120. package/dist/test/index.js.map +1 -0
  121. package/dist/test/test-step.d.ts +21 -0
  122. package/dist/test/test-step.d.ts.map +1 -0
  123. package/dist/test/test-step.js +83 -0
  124. package/dist/test/test-step.js.map +1 -0
  125. package/dist/types.d.ts +108 -0
  126. package/dist/types.d.ts.map +1 -0
  127. package/dist/types.js +5 -0
  128. package/dist/types.js.map +1 -0
  129. package/dist/version.d.ts +2 -0
  130. package/dist/version.d.ts.map +1 -0
  131. package/dist/version.js +4 -0
  132. package/dist/version.js.map +1 -0
  133. package/dist/webhook.d.ts +22 -0
  134. package/dist/webhook.d.ts.map +1 -0
  135. package/dist/webhook.js +23 -0
  136. package/dist/webhook.js.map +1 -0
  137. package/dist/worker-streaming.d.ts +17 -0
  138. package/dist/worker-streaming.d.ts.map +1 -0
  139. package/dist/worker-streaming.js +510 -0
  140. package/dist/worker-streaming.js.map +1 -0
  141. package/dist/worker.d.ts +28 -0
  142. package/dist/worker.d.ts.map +1 -0
  143. package/dist/worker.js +559 -0
  144. package/dist/worker.js.map +1 -0
  145. package/package.json +3 -3
package/dist/client.js ADDED
@@ -0,0 +1,1557 @@
1
+ /**
2
+ * Ironflow Node.js Client
3
+ *
4
+ * HTTP client for interacting with the Ironflow server.
5
+ * Provides methods for registering functions, triggering events, and managing runs.
6
+ */
7
+ import { getCurrentRunId } from "./internal/run-context.js";
8
+ import { API_ENDPOINTS, DEFAULT_SERVER_URL, getServerUrl, IronflowError, RunFailedError, RunCancelledError, UnauthenticatedError, EnterpriseRequiredError, UnauthorizedError, peelProjectionEnvelope, } from "@ironflow/core";
9
+ import { KVClient } from "./kv.js";
10
+ import { CommandDedup } from "./command-dedup.js";
11
+ import { ConfigClient } from "./config-client.js";
12
+ // ============================================================================
13
+ // Client Implementation
14
+ // ============================================================================
15
+ /**
16
+ * Ironflow client for server-side operations
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * import { createClient } from "@ironflow/node";
21
+ *
22
+ * const client = createClient({
23
+ * serverUrl: "http://localhost:9123",
24
+ * });
25
+ *
26
+ * // Register a function
27
+ * await client.registerFunction({
28
+ * id: "my-function",
29
+ * name: "My Function",
30
+ * triggers: [{ event: "my.event" }],
31
+ * endpointUrl: "http://localhost:3000/api/ironflow",
32
+ * preferredMode: "push",
33
+ * });
34
+ *
35
+ * // Emit an event
36
+ * const result = await client.emit("my.event", { data: "value" });
37
+ * console.log("Created runs:", result.runIds);
38
+ * ```
39
+ */
40
+ export class IronflowClient {
41
+ serverUrl;
42
+ apiKey;
43
+ timeout;
44
+ onErrorHandler;
45
+ constructor(config = {}) {
46
+ this.serverUrl = config.serverUrl || getServerUrl() || DEFAULT_SERVER_URL;
47
+ this.apiKey = config.apiKey;
48
+ this.timeout = config.timeout ?? 30000;
49
+ this.onErrorHandler = config.onError;
50
+ }
51
+ /**
52
+ * Register a function with the Ironflow server
53
+ */
54
+ async registerFunction(request) {
55
+ const body = {
56
+ id: request.id,
57
+ };
58
+ if (request.name)
59
+ body.name = request.name;
60
+ if (request.description)
61
+ body.description = request.description;
62
+ if (request.triggers)
63
+ body.triggers = request.triggers;
64
+ if (request.retry)
65
+ body.retry = request.retry;
66
+ if (request.timeoutMs)
67
+ body.timeoutMs = request.timeoutMs;
68
+ if (request.concurrency)
69
+ body.concurrency = request.concurrency;
70
+ if (request.debounce) {
71
+ // Server expects snake_case period_ms / max_wait_ms;
72
+ // SDK uses camelCase for parity with the rest of the TS surface.
73
+ body.debounce = {
74
+ period_ms: request.debounce.periodMs,
75
+ key: request.debounce.key ?? "",
76
+ ...(request.debounce.maxWaitMs != null
77
+ ? { max_wait_ms: request.debounce.maxWaitMs }
78
+ : {}),
79
+ };
80
+ }
81
+ if (request.preferredMode)
82
+ body.preferredMode = request.preferredMode;
83
+ if (request.endpointUrl)
84
+ body.endpointUrl = request.endpointUrl;
85
+ if (request.actorKey)
86
+ body.actorKey = request.actorKey;
87
+ if (request.pauseBehavior)
88
+ body.pauseBehavior = request.pauseBehavior;
89
+ if (request.compensateOnCancel)
90
+ body.compensateOnCancel = true;
91
+ if (request.cancelOn?.length)
92
+ body.cancelOn = request.cancelOn;
93
+ const response = await this.request(API_ENDPOINTS.REGISTER_FUNCTION, body, "registerFunction");
94
+ return { created: response.created };
95
+ }
96
+ /**
97
+ * Emit an event to trigger workflows
98
+ *
99
+ * @example
100
+ * ```typescript
101
+ * const result = await client.emit("order.placed", {
102
+ * orderId: "123",
103
+ * total: 99.99,
104
+ * });
105
+ * console.log("Created runs:", result.runIds);
106
+ * ```
107
+ */
108
+ async emit(eventName, data, options) {
109
+ const body = {
110
+ event: eventName,
111
+ data,
112
+ };
113
+ if (options?.version)
114
+ body.version = options.version;
115
+ if (options?.idempotencyKey)
116
+ body.idempotencyKey = options.idempotencyKey;
117
+ if (options?.metadata)
118
+ body.metadata = options.metadata;
119
+ const response = await this.request(API_ENDPOINTS.TRIGGER, body, "emit");
120
+ return {
121
+ runIds: response.runIds || [],
122
+ eventId: response.eventId,
123
+ };
124
+ }
125
+ /**
126
+ * Emit an event synchronously — waits for the triggered run to complete and returns the result.
127
+ *
128
+ * Calls the TriggerSync endpoint, which blocks until the run finishes or the timeout elapses.
129
+ * Throws RunFailedError if the run fails, RunCancelledError if it is cancelled.
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * const result = await client.emitSync("order.placed", { orderId: "123" });
134
+ * console.log("Output:", result.output);
135
+ * ```
136
+ */
137
+ async emitSync(eventName, data, options) {
138
+ const timeout = options?.timeout ?? 30000;
139
+ const fetchTimeout = timeout + 5000;
140
+ const url = `${this.serverUrl}/ironflow.v1.IronflowService/TriggerSync`;
141
+ const headers = {
142
+ "Content-Type": "application/json",
143
+ };
144
+ if (this.apiKey) {
145
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
146
+ }
147
+ const controller = new AbortController();
148
+ const timeoutId = setTimeout(() => controller.abort(), fetchTimeout);
149
+ let status;
150
+ try {
151
+ const response = await fetch(url, {
152
+ method: "POST",
153
+ headers,
154
+ body: JSON.stringify({ event: eventName, data, timeout_ms: timeout }),
155
+ signal: controller.signal,
156
+ });
157
+ status = response.status;
158
+ if (!response.ok) {
159
+ const errorBody = await response.text();
160
+ let errorMessage = `Request failed with status ${response.status}`;
161
+ try {
162
+ const errorJson = JSON.parse(errorBody);
163
+ if (errorJson.message)
164
+ errorMessage = errorJson.message;
165
+ else if (errorJson.code)
166
+ errorMessage = `Error code: ${errorJson.code}`;
167
+ else
168
+ errorMessage = errorBody;
169
+ }
170
+ catch {
171
+ errorMessage = errorBody;
172
+ }
173
+ this.throwTypedError(response.status, errorMessage);
174
+ }
175
+ const body = await response.json();
176
+ if (!body.results?.length) {
177
+ throw new IronflowError("No results returned from TriggerSync", { code: "NO_RESULTS", retryable: false });
178
+ }
179
+ const result = body.results[0];
180
+ if (!result) {
181
+ throw new IronflowError("No results returned from TriggerSync", { code: "NO_RESULTS", retryable: false });
182
+ }
183
+ if (result.status === "failed") {
184
+ throw new RunFailedError(result.runId, result.error);
185
+ }
186
+ if (result.status === "cancelled") {
187
+ throw new RunCancelledError(result.runId);
188
+ }
189
+ return {
190
+ runId: result.runId,
191
+ functionId: result.functionId,
192
+ status: result.status,
193
+ output: result.output,
194
+ durationMs: result.durationMs,
195
+ };
196
+ }
197
+ catch (error) {
198
+ await this.callOnError(error, { method: "emitSync", endpoint: "/ironflow.v1.IronflowService/TriggerSync", statusCode: status });
199
+ throw error;
200
+ }
201
+ finally {
202
+ clearTimeout(timeoutId);
203
+ }
204
+ }
205
+ /**
206
+ * Publish a message to a developer pub/sub topic.
207
+ * Unlike emit(), this does NOT trigger workflow functions.
208
+ *
209
+ * @example
210
+ * ```typescript
211
+ * const result = await client.publish("notifications", {
212
+ * userId: "123",
213
+ * message: "Hello!",
214
+ * });
215
+ * console.log("Published:", result.eventId, result.sequence);
216
+ * ```
217
+ */
218
+ async publish(topic, data, options) {
219
+ const body = {
220
+ topic,
221
+ data: data ?? {},
222
+ };
223
+ if (options?.idempotencyKey) {
224
+ body.idempotencyKey = options.idempotencyKey;
225
+ }
226
+ const response = await this.request("/ironflow.v1.PubSubService/Publish", body, "publish");
227
+ return {
228
+ eventId: response.eventId,
229
+ sequence: parseInt(response.sequence, 10) || 0,
230
+ };
231
+ }
232
+ /**
233
+ * List all active developer pub/sub topics.
234
+ *
235
+ * @example
236
+ * ```typescript
237
+ * const topics = await client.listTopics();
238
+ * for (const t of topics) {
239
+ * console.log(t.name, t.messageCount);
240
+ * }
241
+ * ```
242
+ */
243
+ async listTopics() {
244
+ const response = await this.request("/ironflow.v1.PubSubService/ListTopics", {}, "listTopics");
245
+ return (response.topics ?? []).map((t) => ({
246
+ name: String(t.name ?? ""),
247
+ messageCount: Number(t.messageCount ?? 0),
248
+ consumerCount: Number(t.consumerCount ?? 0),
249
+ firstMessageAt: t.firstMessageAt ? String(t.firstMessageAt) : undefined,
250
+ lastMessageAt: t.lastMessageAt ? String(t.lastMessageAt) : undefined,
251
+ }));
252
+ }
253
+ /**
254
+ * Get detailed statistics for a topic.
255
+ *
256
+ * @example
257
+ * ```typescript
258
+ * const stats = await client.getTopicStats("notifications");
259
+ * console.log("Messages:", stats.messageCount, "Lag:", stats.lag);
260
+ * ```
261
+ */
262
+ async getTopicStats(topic) {
263
+ const response = await this.request("/ironflow.v1.PubSubService/GetTopicStats", { topic }, "getTopicStats");
264
+ return {
265
+ name: String(response.name ?? ""),
266
+ messageCount: Number(response.messageCount ?? 0),
267
+ consumerCount: Number(response.consumerCount ?? 0),
268
+ lag: Number(response.lag ?? 0),
269
+ firstSeq: Number(response.firstSeq ?? 0),
270
+ lastSeq: Number(response.lastSeq ?? 0),
271
+ };
272
+ }
273
+ /**
274
+ * Get a run by ID
275
+ */
276
+ async getRun(runId) {
277
+ return this.request(API_ENDPOINTS.GET_RUN, { id: runId }, "getRun");
278
+ }
279
+ /**
280
+ * List runs with optional filtering
281
+ */
282
+ async listRuns(options) {
283
+ const body = {};
284
+ if (options?.functionId)
285
+ body.functionId = options.functionId;
286
+ if (options?.status)
287
+ body.status = options.status;
288
+ if (options?.limit)
289
+ body.limit = options.limit;
290
+ if (options?.cursor)
291
+ body.cursor = options.cursor;
292
+ return this.request(API_ENDPOINTS.LIST_RUNS, body, "listRuns");
293
+ }
294
+ /**
295
+ * Cancel a running workflow
296
+ */
297
+ async cancelRun(runId, reason) {
298
+ return this.request(API_ENDPOINTS.CANCEL_RUN, {
299
+ id: runId,
300
+ reason: reason || "",
301
+ }, "cancelRun");
302
+ }
303
+ /**
304
+ * Retry a failed run
305
+ */
306
+ async retryRun(runId, fromStep) {
307
+ const body = { id: runId };
308
+ if (fromStep)
309
+ body.fromStep = fromStep;
310
+ return this.request(API_ENDPOINTS.RETRY_RUN, body, "retryRun");
311
+ }
312
+ /**
313
+ * Health check
314
+ */
315
+ async health() {
316
+ const response = await this.request(API_ENDPOINTS.HEALTH, {}, "health");
317
+ return response.status;
318
+ }
319
+ /**
320
+ * Entity stream operations
321
+ *
322
+ * @example
323
+ * ```typescript
324
+ * // Append an event to a stream
325
+ * const result = await client.streams.append("order-123", {
326
+ * name: "order.created",
327
+ * data: { total: 100 },
328
+ * entityType: "order",
329
+ * });
330
+ *
331
+ * // Read events from a stream
332
+ * const { events } = await client.streams.read("order-123", { limit: 10 });
333
+ *
334
+ * // Get stream info
335
+ * const info = await client.streams.getInfo("order-123");
336
+ * ```
337
+ */
338
+ streams = {
339
+ /**
340
+ * Append an event to an entity stream
341
+ */
342
+ append: async (entityId, input, options) => {
343
+ const body = {
344
+ entity_id: entityId,
345
+ entity_type: input.entityType,
346
+ event_name: input.name,
347
+ data: input.data,
348
+ expected_version: options?.expectedVersion ?? -1,
349
+ idempotency_key: options?.idempotencyKey ?? "",
350
+ version: options?.version ?? 1,
351
+ };
352
+ if (options?.metadata !== undefined) {
353
+ body.metadata = options.metadata;
354
+ }
355
+ const response = await this.request("/ironflow.v1.EntityStreamService/AppendEvent", body, "streams.append");
356
+ return {
357
+ entityVersion: Number(response.entityVersion ?? 0),
358
+ eventId: response.eventId,
359
+ };
360
+ },
361
+ /**
362
+ * Read events from an entity stream
363
+ */
364
+ read: async (entityId, options) => {
365
+ const response = await this.request("/ironflow.v1.EntityStreamService/ReadStream", {
366
+ entity_id: entityId,
367
+ from_version: options?.fromVersion ?? 0,
368
+ limit: options?.limit ?? 0,
369
+ direction: options?.direction ?? "forward",
370
+ }, "streams.read");
371
+ return {
372
+ events: (response.events ?? []).map((e) => ({
373
+ id: e.id,
374
+ name: e.name,
375
+ data: e.data ?? {},
376
+ entityVersion: Number(e.entityVersion ?? 0),
377
+ version: e.version,
378
+ timestamp: e.timestamp,
379
+ source: e.source,
380
+ metadata: e.metadata,
381
+ })),
382
+ totalCount: response.totalCount ?? 0,
383
+ };
384
+ },
385
+ /**
386
+ * Get information about an entity stream.
387
+ *
388
+ * Returns `null` if no events have been written to this stream yet — safe to
389
+ * pass `expectedVersion: 0` to `append()` in that case to create the first event.
390
+ *
391
+ * @example
392
+ * ```typescript
393
+ * const info = await client.streams.getInfo("order-123");
394
+ * await client.streams.append("order-123", event, {
395
+ * expectedVersion: info ? info.version : 0,
396
+ * });
397
+ * ```
398
+ */
399
+ getInfo: async (entityId) => {
400
+ try {
401
+ const response = await this.request("/ironflow.v1.EntityStreamService/GetStreamInfo", {
402
+ entity_id: entityId,
403
+ }, "streams.getInfo");
404
+ return {
405
+ entityId: response.entityId,
406
+ entityType: response.entityType,
407
+ version: Number(response.version ?? 0),
408
+ eventCount: Number(response.eventCount ?? 0),
409
+ createdAt: response.createdAt,
410
+ updatedAt: response.updatedAt,
411
+ };
412
+ }
413
+ catch (err) {
414
+ if (err instanceof IronflowError && err.message === "stream not found") {
415
+ return null;
416
+ }
417
+ throw err;
418
+ }
419
+ },
420
+ /**
421
+ * Create a snapshot of the materialized state at a specific stream version.
422
+ * Use snapshots to speed up state reconstruction for long-lived entity streams.
423
+ */
424
+ createSnapshot: async (entityId, input) => {
425
+ const response = await this.request("/ironflow.v1.EntityStreamService/CreateSnapshot", {
426
+ entity_id: entityId,
427
+ entity_type: input.entityType,
428
+ entity_version: input.entityVersion,
429
+ state: input.state,
430
+ }, "streams.createSnapshot");
431
+ return { snapshotId: response.snapshotId };
432
+ },
433
+ /**
434
+ * Get the latest snapshot at or before a given version.
435
+ * Returns the snapshot closest to the requested version without exceeding it.
436
+ */
437
+ getSnapshot: async (entityId, options) => {
438
+ const response = await this.request("/ironflow.v1.EntityStreamService/GetSnapshot", {
439
+ entity_id: entityId,
440
+ before_version: options?.beforeVersion ?? 0,
441
+ }, "streams.getSnapshot");
442
+ return {
443
+ snapshotId: response.snapshotId,
444
+ entityId: response.entityId,
445
+ entityType: response.entityType,
446
+ entityVersion: Number(response.entityVersion ?? 0),
447
+ state: response.state,
448
+ createdAt: response.createdAt,
449
+ };
450
+ },
451
+ /**
452
+ * List all entity streams.
453
+ */
454
+ listStreams: async () => {
455
+ const resp = await this.restRequest("GET", "/api/v1/streams", undefined, "streams.listStreams");
456
+ return resp.streams ?? [];
457
+ },
458
+ /**
459
+ * Get the full event history for an entity.
460
+ */
461
+ getEntityHistory: async (entityId) => {
462
+ const resp = await this.restRequest("GET", `/api/v1/streams/${encodeURIComponent(entityId)}/history`, undefined, "streams.getEntityHistory");
463
+ return resp.events ?? [];
464
+ },
465
+ };
466
+ /**
467
+ * SQL-backed projections
468
+ *
469
+ * Create materialized SQL tables from event streams. Events are processed
470
+ * server-side using parameterized SQL handlers.
471
+ *
472
+ * @example
473
+ * ```typescript
474
+ * // Create a SQL projection
475
+ * await client.sqlProjections.create({
476
+ * name: "board",
477
+ * tableSql: "CREATE TABLE proj_board (id TEXT PRIMARY KEY, title TEXT, status TEXT)",
478
+ * eventHandlers: {
479
+ * "issue.created": "INSERT INTO proj_board (id, title, status) VALUES (:entity_id, :data.title, 'OPEN')",
480
+ * "issue.status_changed": "UPDATE proj_board SET status = :data.to WHERE id = :entity_id",
481
+ * },
482
+ * events: ["issue.created", "issue.status_changed"],
483
+ * });
484
+ *
485
+ * // Query the projection
486
+ * const result = await client.sqlProjections.query("board", {
487
+ * where: "status = 'OPEN'",
488
+ * orderBy: "title ASC",
489
+ * limit: 50,
490
+ * });
491
+ * ```
492
+ */
493
+ sqlProjections = {
494
+ /**
495
+ * Create a SQL-backed projection with a materialized table and event handlers.
496
+ */
497
+ create: async (input) => {
498
+ const response = await this.request("/ironflow.v1.ProjectionService/CreateSQLProjection", {
499
+ name: input.name,
500
+ table_sql: input.tableSql,
501
+ event_handlers: input.eventHandlers,
502
+ events: input.events,
503
+ description: input.description ?? "",
504
+ }, "sqlProjections.create");
505
+ return { name: response.name, status: response.status };
506
+ },
507
+ /**
508
+ * Query a SQL-backed projection table with optional filtering, ordering, and pagination.
509
+ */
510
+ query: async (name, options) => {
511
+ const response = await this.request("/ironflow.v1.ProjectionService/QuerySQLProjection", {
512
+ name,
513
+ where: options?.where ?? "",
514
+ order_by: options?.orderBy ?? "",
515
+ limit: options?.limit ?? 100,
516
+ offset: options?.offset ?? 0,
517
+ }, "sqlProjections.query");
518
+ return {
519
+ columns: response.columns ?? [],
520
+ rows: (response.rows ?? []).map((r) => r.values),
521
+ totalCount: response.totalCount ?? 0,
522
+ };
523
+ },
524
+ };
525
+ /**
526
+ * API key management
527
+ *
528
+ * @example
529
+ * ```typescript
530
+ * // Create an API key
531
+ * const { key } = await client.apiKeys.create({ name: "ci-key" });
532
+ *
533
+ * // List all API keys
534
+ * const keys = await client.apiKeys.list();
535
+ *
536
+ * // Rotate a key
537
+ * const rotated = await client.apiKeys.rotate(keys[0].id);
538
+ * ```
539
+ */
540
+ apiKeys = {
541
+ /** Create a new API key */
542
+ create: async (input) => {
543
+ return this.restRequest("POST", "/api/v1/apikeys", input, "apiKeys.create");
544
+ },
545
+ /** List all API keys */
546
+ list: async () => {
547
+ return this.restRequest("GET", "/api/v1/apikeys", undefined, "apiKeys.list");
548
+ },
549
+ /** Get an API key by ID */
550
+ get: async (id) => {
551
+ return this.restRequest("GET", `/api/v1/apikeys/${id}`, undefined, "apiKeys.get");
552
+ },
553
+ /** Delete an API key */
554
+ delete: async (id) => {
555
+ await this.restRequest("DELETE", `/api/v1/apikeys/${id}`, undefined, "apiKeys.delete");
556
+ },
557
+ /** Rotate an API key (generates a new secret) */
558
+ rotate: async (id) => {
559
+ return this.restRequest("POST", `/api/v1/apikeys/${id}/rotate`, undefined, "apiKeys.rotate");
560
+ },
561
+ };
562
+ /**
563
+ * Organization management (enterprise)
564
+ *
565
+ * @example
566
+ * ```typescript
567
+ * const org = await client.orgs.create({ name: "Acme Corp" });
568
+ * const orgs = await client.orgs.list();
569
+ * await client.orgs.update(org.id, { name: "Acme Inc" });
570
+ * ```
571
+ */
572
+ orgs = {
573
+ /** Create a new organization */
574
+ create: async (input) => {
575
+ return this.restRequest("POST", "/api/v1/orgs", input, "orgs.create");
576
+ },
577
+ /** List all organizations */
578
+ list: async () => {
579
+ return this.restRequest("GET", "/api/v1/orgs", undefined, "orgs.list");
580
+ },
581
+ /** Get an organization by ID */
582
+ get: async (id) => {
583
+ return this.restRequest("GET", `/api/v1/orgs/${id}`, undefined, "orgs.get");
584
+ },
585
+ /** Update an organization */
586
+ update: async (id, input) => {
587
+ return this.restRequest("PATCH", `/api/v1/orgs/${id}`, input, "orgs.update");
588
+ },
589
+ /** Delete an organization */
590
+ delete: async (id) => {
591
+ await this.restRequest("DELETE", `/api/v1/orgs/${id}`, undefined, "orgs.delete");
592
+ },
593
+ };
594
+ /**
595
+ * Role management (enterprise)
596
+ *
597
+ * @example
598
+ * ```typescript
599
+ * const role = await client.roles.create({ name: "deployer", org_id: orgId });
600
+ * await client.roles.assignPolicy(role.id, policyId);
601
+ * const roles = await client.roles.list(orgId);
602
+ * ```
603
+ */
604
+ roles = {
605
+ /** Create a new role */
606
+ create: async (input) => {
607
+ return this.restRequest("POST", "/api/v1/roles", input, "roles.create");
608
+ },
609
+ /** List roles, optionally filtered by organization */
610
+ list: async (orgId) => {
611
+ const query = orgId ? `?org_id=${encodeURIComponent(orgId)}` : "";
612
+ return this.restRequest("GET", `/api/v1/roles${query}`, undefined, "roles.list");
613
+ },
614
+ /** Get a role by ID */
615
+ get: async (id) => {
616
+ return this.restRequest("GET", `/api/v1/roles/${id}`, undefined, "roles.get");
617
+ },
618
+ /** Update a role */
619
+ update: async (id, input) => {
620
+ return this.restRequest("PATCH", `/api/v1/roles/${id}`, input, "roles.update");
621
+ },
622
+ /** Delete a role */
623
+ delete: async (id) => {
624
+ await this.restRequest("DELETE", `/api/v1/roles/${id}`, undefined, "roles.delete");
625
+ },
626
+ /** Assign a policy to a role */
627
+ assignPolicy: async (roleId, policyId) => {
628
+ await this.restRequest("POST", `/api/v1/roles/${roleId}/policies`, {
629
+ policy_id: policyId,
630
+ }, "roles.assignPolicy");
631
+ },
632
+ /** Remove a policy from a role */
633
+ removePolicy: async (roleId, policyId) => {
634
+ await this.restRequest("DELETE", `/api/v1/roles/${roleId}/policies/${policyId}`, undefined, "roles.removePolicy");
635
+ },
636
+ };
637
+ /**
638
+ * Policy management (enterprise)
639
+ *
640
+ * @example
641
+ * ```typescript
642
+ * const policy = await client.policies.create({
643
+ * name: "allow-emit",
644
+ * effect: "allow",
645
+ * actions: "emit:*",
646
+ * resources: "*",
647
+ * org_id: orgId,
648
+ * });
649
+ * const policies = await client.policies.list(orgId);
650
+ * ```
651
+ */
652
+ policies = {
653
+ /** Create a new policy */
654
+ create: async (input) => {
655
+ return this.restRequest("POST", "/api/v1/policies", input, "policies.create");
656
+ },
657
+ /** List policies, optionally filtered by organization */
658
+ list: async (orgId) => {
659
+ const query = orgId ? `?org_id=${encodeURIComponent(orgId)}` : "";
660
+ return this.restRequest("GET", `/api/v1/policies${query}`, undefined, "policies.list");
661
+ },
662
+ /** Get a policy by ID */
663
+ get: async (id) => {
664
+ return this.restRequest("GET", `/api/v1/policies/${id}`, undefined, "policies.get");
665
+ },
666
+ /** Update a policy */
667
+ update: async (id, input) => {
668
+ return this.restRequest("PATCH", `/api/v1/policies/${id}`, input, "policies.update");
669
+ },
670
+ /** Delete a policy */
671
+ delete: async (id) => {
672
+ await this.restRequest("DELETE", `/api/v1/policies/${id}`, undefined, "policies.delete");
673
+ },
674
+ };
675
+ /**
676
+ * Projection management
677
+ *
678
+ * @example
679
+ * ```typescript
680
+ * const state = await client.projections.get("order-summary");
681
+ * const statuses = await client.projections.list();
682
+ * await client.projections.rebuild("order-summary");
683
+ * ```
684
+ */
685
+ projections = {
686
+ /**
687
+ * Get the current materialized state of a projection.
688
+ *
689
+ * Returns a flat `ProjectionStateResult<TState>` (see `@ironflow/core`).
690
+ * The server returns a wrapped envelope and this method peels it via
691
+ * `peelProjectionEnvelope`. See issue #610 / CHANGELOG 0.20.0.
692
+ *
693
+ * For a freshly registered projection with no events applied, returns
694
+ * empty `state`, `lastEventTime: undefined`, `version: 0`.
695
+ */
696
+ get: async (name, options) => {
697
+ // Normalize empty-string partition to undefined so the helper falls back
698
+ // to "__global__" instead of returning empty-string partition.
699
+ const partition = options?.partition ? options.partition : undefined;
700
+ const path = partition
701
+ ? `/api/v1/projections/${encodeURIComponent(name)}?partition=${encodeURIComponent(partition)}`
702
+ : `/api/v1/projections/${encodeURIComponent(name)}`;
703
+ const raw = await this.restRequest("GET", path, undefined, "projections.get");
704
+ return peelProjectionEnvelope(raw, partition);
705
+ },
706
+ /** List all projection statuses */
707
+ list: async () => {
708
+ return this.restRequest("GET", "/api/v1/projections", undefined, "projections.list");
709
+ },
710
+ /** Get operational status for a projection */
711
+ getStatus: async (name) => {
712
+ return this.restRequest("GET", `/api/v1/projections/${encodeURIComponent(name)}/status`, undefined, "projections.getStatus");
713
+ },
714
+ /** Trigger a full rebuild of a projection */
715
+ rebuild: async (name) => {
716
+ return this.restRequest("POST", `/api/v1/projections/${encodeURIComponent(name)}/rebuild`, undefined, "projections.rebuild");
717
+ },
718
+ /** Get the status of an in-progress or completed rebuild job */
719
+ getRebuildJob: async (name) => {
720
+ return this.restRequest("GET", `/api/v1/projections/${encodeURIComponent(name)}/rebuild`, undefined, "projections.getRebuildJob");
721
+ },
722
+ /** Delete a projection */
723
+ delete: async (name) => {
724
+ await this.restRequest("DELETE", `/api/v1/projections/${encodeURIComponent(name)}`, undefined, "projections.delete");
725
+ },
726
+ /** Pause a projection (stop consuming new events) */
727
+ pause: async (name) => {
728
+ await this.restRequest("POST", `/api/v1/projections/${encodeURIComponent(name)}/pause`, undefined, "projections.pause");
729
+ },
730
+ /** Resume a paused projection */
731
+ resume: async (name) => {
732
+ await this.restRequest("POST", `/api/v1/projections/${encodeURIComponent(name)}/resume`, undefined, "projections.resume");
733
+ },
734
+ /** Cancel an in-progress rebuild */
735
+ cancelRebuild: async (name) => {
736
+ await this.restRequest("POST", `/api/v1/projections/${encodeURIComponent(name)}/cancel`, undefined, "projections.cancelRebuild");
737
+ },
738
+ /**
739
+ * Wait until the named projection has processed events up to `minSeq`,
740
+ * or the timeout elapses. Read-your-writes primitive for CQRS: pair
741
+ * with `sequence` from a `streams.append` response.
742
+ *
743
+ * ```typescript
744
+ * const { sequence } = await client.streams.append(orderId, event);
745
+ * await client.projections.waitForCatchup("order-detail-view", {
746
+ * minSeq: sequence,
747
+ * partition: orderId,
748
+ * timeoutMs: 5000,
749
+ * });
750
+ * ```
751
+ *
752
+ * Errors: 404 (projection not found), 409 (paused/rebuilding/partition
753
+ * unsupported for external), 429 (wait capacity exceeded).
754
+ *
755
+ * Issue #473.
756
+ */
757
+ waitForCatchup: async (name, opts) => {
758
+ const params = new URLSearchParams();
759
+ params.set("minSeq", String(opts.minSeq));
760
+ if (opts.timeoutMs !== undefined) {
761
+ params.set("timeout", String(opts.timeoutMs));
762
+ }
763
+ if (opts.partition) {
764
+ params.set("partition", opts.partition);
765
+ }
766
+ return this.restRequest("GET", `/api/v1/projections/${encodeURIComponent(name)}/catchup?${params.toString()}`, undefined, "projections.waitForCatchup");
767
+ },
768
+ /**
769
+ * Wait on multiple projections in a single request. All items share
770
+ * a single timeout deadline and a single atomic slot reservation on
771
+ * the server — if the server's cap cannot absorb N items, the whole
772
+ * batch is rejected with 429. Per-item failures are returned per
773
+ * element via `error` fields.
774
+ *
775
+ * Max 16 items. Issue #473.
776
+ */
777
+ waitForCatchupBatch: async (items, opts = {}) => {
778
+ // Always send minSeq as a string. uint64 sequences can exceed JS's
779
+ // safe-integer range (2^53-1); stringifying keeps the value exact
780
+ // across the JSON boundary and matches protojson's convention for
781
+ // 64-bit ints.
782
+ const body = {
783
+ items: items.map((i) => ({
784
+ name: i.name,
785
+ minSeq: String(i.minSeq),
786
+ ...(i.partition ? { partition: i.partition } : {}),
787
+ })),
788
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
789
+ };
790
+ const resp = await this.restRequest("POST", "/api/v1/projections/catchup/batch", body, "projections.waitForCatchupBatch");
791
+ return resp.results ?? [];
792
+ },
793
+ /**
794
+ * Wait for a specific event (identified by `eventId` from a
795
+ * `streams.append` response) to be processed by the given projection.
796
+ * The server resolves eventId → NATS seq internally.
797
+ *
798
+ * Errors: 404 (event not found), 409 (event predates sequence
799
+ * tracking — fall back to waitForCatchup with minSeq from a
800
+ * fresh write), plus the standard wait errors.
801
+ *
802
+ * Issue #473.
803
+ */
804
+ waitForEvent: async (eventId, projection, opts = {}) => {
805
+ const body = {
806
+ eventId,
807
+ projection,
808
+ ...(opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {}),
809
+ ...(opts.partition ? { partition: opts.partition } : {}),
810
+ };
811
+ return this.restRequest("POST", "/api/v1/projections/wait-for-event", body, "projections.waitForEvent");
812
+ },
813
+ };
814
+ /**
815
+ * Secrets management
816
+ *
817
+ * @example
818
+ * ```typescript
819
+ * await client.secrets.set("stripe-key", "sk_live_...");
820
+ * const secret = await client.secrets.get("stripe-key");
821
+ * const all = await client.secrets.list();
822
+ * await client.secrets.delete("stripe-key");
823
+ * ```
824
+ */
825
+ secrets = {
826
+ /** Get a secret by name (returns value) */
827
+ get: async (name) => {
828
+ return this.restRequest("GET", `/api/v1/secrets/${encodeURIComponent(name)}`, undefined, "secrets.get");
829
+ },
830
+ /** Create a new secret */
831
+ set: async (name, value) => {
832
+ return this.restRequest("POST", "/api/v1/secrets", { name, value }, "secrets.set");
833
+ },
834
+ /** Update an existing secret's value */
835
+ update: async (name, value) => {
836
+ return this.restRequest("PUT", `/api/v1/secrets/${encodeURIComponent(name)}`, { value }, "secrets.update");
837
+ },
838
+ /** List all secrets (names only, no values) */
839
+ list: async () => {
840
+ return this.restRequest("GET", "/api/v1/secrets", undefined, "secrets.list");
841
+ },
842
+ /** Delete a secret */
843
+ delete: async (name) => {
844
+ await this.restRequest("DELETE", `/api/v1/secrets/${encodeURIComponent(name)}`, undefined, "secrets.delete");
845
+ },
846
+ };
847
+ /**
848
+ * Project management
849
+ *
850
+ * @example
851
+ * ```typescript
852
+ * const project = await client.projects.create({ name: "my-service" });
853
+ * const projects = await client.projects.list();
854
+ * await client.projects.update(project.id, { name: "renamed-service" });
855
+ * await client.projects.delete(project.id);
856
+ * ```
857
+ */
858
+ projects = {
859
+ /** List all projects */
860
+ list: async () => {
861
+ return this.restRequest("GET", "/api/v1/projects", undefined, "projects.list");
862
+ },
863
+ /** Create a new project */
864
+ create: async (input) => {
865
+ return this.restRequest("POST", "/api/v1/projects", input, "projects.create");
866
+ },
867
+ /** Update a project */
868
+ update: async (id, input) => {
869
+ return this.restRequest("PUT", `/api/v1/projects/${encodeURIComponent(id)}`, input, "projects.update");
870
+ },
871
+ /** Delete a project */
872
+ delete: async (id) => {
873
+ await this.restRequest("DELETE", `/api/v1/projects/${encodeURIComponent(id)}`, undefined, "projects.delete");
874
+ },
875
+ };
876
+ /**
877
+ * Environment management
878
+ *
879
+ * @example
880
+ * ```typescript
881
+ * const env = await client.environments.create({ name: "staging", projectId: "proj_..." });
882
+ * const envs = await client.environments.list();
883
+ * await client.environments.update(env.id, { name: "staging-v2" });
884
+ * await client.environments.delete(env.id);
885
+ * ```
886
+ */
887
+ environments = {
888
+ /** List all environments */
889
+ list: async () => {
890
+ return this.restRequest("GET", "/api/v1/environments", undefined, "environments.list");
891
+ },
892
+ /** Create a new environment */
893
+ create: async (input) => {
894
+ return this.restRequest("POST", "/api/v1/environments", input, "environments.create");
895
+ },
896
+ /** Update an environment */
897
+ update: async (id, input) => {
898
+ return this.restRequest("PUT", `/api/v1/environments/${encodeURIComponent(id)}`, input, "environments.update");
899
+ },
900
+ /** Delete an environment */
901
+ delete: async (id) => {
902
+ await this.restRequest("DELETE", `/api/v1/environments/${encodeURIComponent(id)}`, undefined, "environments.delete");
903
+ },
904
+ };
905
+ /**
906
+ * Event schema registry operations
907
+ *
908
+ * @example
909
+ * ```typescript
910
+ * // Register a schema
911
+ * const schema = await client.schemas.register({
912
+ * name: "order.placed",
913
+ * version: 1,
914
+ * schema: { type: "object", properties: { orderId: { type: "string" } } },
915
+ * });
916
+ *
917
+ * // List all schemas
918
+ * const schemas = await client.schemas.list();
919
+ *
920
+ * // Get latest version of a schema
921
+ * const latest = await client.schemas.get("order.placed");
922
+ *
923
+ * // Get a specific version
924
+ * const v1 = await client.schemas.getVersion("order.placed", 1);
925
+ *
926
+ * // Test an upcast transformation
927
+ * const result = await client.schemas.testUpcast({
928
+ * eventName: "order.placed",
929
+ * fromVersion: 1,
930
+ * toVersion: 2,
931
+ * data: { orderId: "123" },
932
+ * });
933
+ * ```
934
+ */
935
+ schemas = {
936
+ /** Register a new event schema (or a new version of an existing schema) */
937
+ register: async (input) => {
938
+ return this.restRequest("POST", "/api/v1/events/schemas", {
939
+ event_name: input.name,
940
+ version: input.version,
941
+ schema_json: JSON.stringify(input.schema),
942
+ }, "schemas.register");
943
+ },
944
+ /** List all registered event schemas */
945
+ list: async () => {
946
+ const resp = await this.restRequest("GET", "/api/v1/events/schemas", undefined, "schemas.list");
947
+ return resp.schemas ?? [];
948
+ },
949
+ /** Get the latest version of an event schema by name */
950
+ get: async (name) => {
951
+ return this.restRequest("GET", `/api/v1/events/schemas/${encodeURIComponent(name)}`, undefined, "schemas.get");
952
+ },
953
+ /** Get a specific version of an event schema */
954
+ getVersion: async (name, version) => {
955
+ return this.restRequest("GET", `/api/v1/events/schemas/${encodeURIComponent(name)}/${version}`, undefined, "schemas.getVersion");
956
+ },
957
+ /** Delete a specific version of an event schema */
958
+ delete: async (name, version) => {
959
+ await this.restRequest("DELETE", `/api/v1/events/schemas/${encodeURIComponent(name)}/${version}`, undefined, "schemas.delete");
960
+ },
961
+ /** Test an upcast transformation between two schema versions */
962
+ testUpcast: async (input) => {
963
+ return this.restRequest("POST", "/api/v1/events/upcast", input, "schemas.testUpcast");
964
+ },
965
+ };
966
+ /**
967
+ * Get the reconstructed state of a run at a specific point in time.
968
+ *
969
+ * @param runId The run ID to query
970
+ * @param timestamp The point in time to reconstruct state at
971
+ */
972
+ async getRunStateAt(runId, timestamp) {
973
+ return this.request("/ironflow.v1.TimeTravelService/GetRunStateAt", { run_id: runId, timestamp: timestamp.toISOString() }, "getRunStateAt");
974
+ }
975
+ /**
976
+ * Get the timeline of events for a run (for time-travel debugging).
977
+ *
978
+ * @param runId The run ID to query
979
+ */
980
+ async getRunTimeline(runId) {
981
+ const response = await this.request("/ironflow.v1.TimeTravelService/GetRunTimeline", { run_id: runId }, "getRunTimeline");
982
+ return response.events ?? [];
983
+ }
984
+ /**
985
+ * Get the output of a specific step at a point in time.
986
+ *
987
+ * @param runId The run ID
988
+ * @param stepId The step ID
989
+ * @param timestamp The point in time to query
990
+ */
991
+ async getStepOutputAt(runId, stepId, timestamp) {
992
+ return this.request("/ironflow.v1.TimeTravelService/GetStepOutputAt", { run_id: runId, step_id: stepId, timestamp: timestamp.toISOString() }, "getStepOutputAt");
993
+ }
994
+ /**
995
+ * Get the audit trail for a run.
996
+ *
997
+ * @param runId The run ID to retrieve the audit trail for
998
+ */
999
+ async getAuditTrail(runId) {
1000
+ const response = await this.request("/ironflow.v1.AuditService/GetAuditTrail", { run_id: runId }, "getAuditTrail");
1001
+ return response.entries ?? [];
1002
+ }
1003
+ /**
1004
+ * Webhook management operations
1005
+ *
1006
+ * @example
1007
+ * ```typescript
1008
+ * // List all webhook sources
1009
+ * const sources = await client.webhooks.listSources();
1010
+ *
1011
+ * // Delete a webhook source
1012
+ * await client.webhooks.deleteSource("my-webhook");
1013
+ *
1014
+ * // List deliveries for a source
1015
+ * const { deliveries } = await client.webhooks.listDeliveries({ sourceId: "my-webhook" });
1016
+ * ```
1017
+ */
1018
+ webhooks = {
1019
+ /** Create a new webhook source */
1020
+ create: async (input) => {
1021
+ const response = await this.request("/ironflow.v1.WebhookService/CreateWebhookSource", {
1022
+ id: input.id,
1023
+ event_prefix: input.eventPrefix,
1024
+ verify_header: input.verifyHeader ?? "",
1025
+ verify_algorithm: input.verifyAlgorithm ?? "",
1026
+ verify_secret: input.verifySecret ?? "",
1027
+ metadata: input.metadata,
1028
+ }, "webhooks.create");
1029
+ return {
1030
+ id: response.id,
1031
+ eventPrefix: response.eventPrefix,
1032
+ verifyHeader: response.verifyHeader,
1033
+ verifyAlgorithm: response.verifyAlgorithm,
1034
+ sourceType: response.sourceType,
1035
+ metadata: response.metadata,
1036
+ createdAt: response.createdAt,
1037
+ updatedAt: response.updatedAt,
1038
+ };
1039
+ },
1040
+ /** List all registered webhook sources */
1041
+ listSources: async () => {
1042
+ const response = await this.request("/ironflow.v1.WebhookService/ListWebhookSources", { limit: 0, offset: 0 }, "webhooks.listSources");
1043
+ return (response.sources ?? []).map((s) => ({
1044
+ id: s.id,
1045
+ eventPrefix: s.eventPrefix,
1046
+ verifyHeader: s.verifyHeader,
1047
+ verifyAlgorithm: s.verifyAlgorithm,
1048
+ sourceType: s.sourceType,
1049
+ metadata: s.metadata,
1050
+ createdAt: s.createdAt,
1051
+ updatedAt: s.updatedAt,
1052
+ }));
1053
+ },
1054
+ /** Delete a webhook source by ID */
1055
+ deleteSource: async (id) => {
1056
+ await this.request("/ironflow.v1.WebhookService/DeleteWebhookSource", { id }, "webhooks.deleteSource");
1057
+ },
1058
+ /** List webhook deliveries with optional filtering */
1059
+ listDeliveries: async (opts) => {
1060
+ const response = await this.request("/ironflow.v1.WebhookService/ListWebhookDeliveries", {
1061
+ source_id: opts?.sourceId ?? "",
1062
+ status: opts?.status ?? "",
1063
+ limit: opts?.limit ?? 0,
1064
+ offset: opts?.offset ?? 0,
1065
+ }, "webhooks.listDeliveries");
1066
+ return {
1067
+ deliveries: (response.deliveries ?? []).map((d) => ({
1068
+ id: d.id,
1069
+ sourceId: d.sourceId,
1070
+ externalId: d.externalId,
1071
+ status: d.status,
1072
+ eventId: d.eventId,
1073
+ error: d.error,
1074
+ createdAt: d.createdAt,
1075
+ })),
1076
+ totalCount: response.totalCount ?? 0,
1077
+ };
1078
+ },
1079
+ };
1080
+ /**
1081
+ * User management operations
1082
+ *
1083
+ * @example
1084
+ * ```typescript
1085
+ * // Create a user
1086
+ * const user = await client.users.create({ email: "alice@example.com", password: "secret", roles: ["admin"] });
1087
+ *
1088
+ * // List users
1089
+ * const users = await client.users.list();
1090
+ *
1091
+ * // Update a user
1092
+ * await client.users.update(user.id, { name: "Alice" });
1093
+ *
1094
+ * // Delete a user
1095
+ * await client.users.delete(user.id);
1096
+ * ```
1097
+ */
1098
+ users = {
1099
+ /** Create a new user (admin only) */
1100
+ create: async (input) => {
1101
+ return this.restRequest("POST", "/api/v1/users", input, "users.create");
1102
+ },
1103
+ /** List all users in the current organization (admin only) */
1104
+ list: async () => {
1105
+ return this.restRequest("GET", "/api/v1/users", undefined, "users.list");
1106
+ },
1107
+ /** Get a user by ID */
1108
+ get: async (id) => {
1109
+ return this.restRequest("GET", `/api/v1/users/${encodeURIComponent(id)}`, undefined, "users.get");
1110
+ },
1111
+ /** Update a user's profile (admin only) */
1112
+ update: async (id, input) => {
1113
+ return this.restRequest("PATCH", `/api/v1/users/${encodeURIComponent(id)}`, input, "users.update");
1114
+ },
1115
+ /** Delete a user (admin only) */
1116
+ delete: async (id) => {
1117
+ await this.restRequest("DELETE", `/api/v1/users/${encodeURIComponent(id)}`, undefined, "users.delete");
1118
+ },
1119
+ };
1120
+ /**
1121
+ * Tenant management operations (enterprise-only)
1122
+ *
1123
+ * @example
1124
+ * ```typescript
1125
+ * // List all tenants
1126
+ * const tenants = await client.tenants.list();
1127
+ * console.log(tenants.map(t => t.name));
1128
+ * ```
1129
+ */
1130
+ tenants = {
1131
+ /** List all tenants (enterprise-only) */
1132
+ list: async () => {
1133
+ return this.restRequest("GET", "/api/v1/tenants", undefined, "tenants.list");
1134
+ },
1135
+ };
1136
+ /**
1137
+ * KV store operations
1138
+ *
1139
+ * @example
1140
+ * ```typescript
1141
+ * const kv = client.kv();
1142
+ * const bucket = await kv.createBucket({ name: "sessions", ttlSeconds: 3600 });
1143
+ * const handle = kv.bucket("sessions");
1144
+ * const { revision } = await handle.put("user.123", { token: "abc" });
1145
+ * const entry = await handle.get("user.123");
1146
+ * ```
1147
+ */
1148
+ kv() {
1149
+ return new KVClient({
1150
+ serverUrl: this.serverUrl,
1151
+ apiKey: this.apiKey,
1152
+ timeout: this.timeout,
1153
+ onError: this.onErrorHandler,
1154
+ });
1155
+ }
1156
+ /**
1157
+ * Create a CommandDedup instance for atomic command-level idempotency.
1158
+ *
1159
+ * Uses the claim-first pattern backed by NATS KV. The KV bucket is created
1160
+ * lazily on the first operation. Store the returned instance and reuse it
1161
+ * across requests — do not call commandDedup() per request.
1162
+ *
1163
+ * @example
1164
+ * ```typescript
1165
+ * const dedup = client.commandDedup<OrderResult>("order-commands");
1166
+ * const prior = await dedup.tryClaim(commandId, { orderId, claimedAt: new Date().toISOString() });
1167
+ * if (prior !== null) return prior;
1168
+ * try {
1169
+ * const result = await runOrderHandler();
1170
+ * await dedup.finalize(commandId, result);
1171
+ * return result;
1172
+ * } catch (err) {
1173
+ * await dedup.release(commandId).catch(() => {}); // swallow — don't mask the original error
1174
+ * throw err;
1175
+ * }
1176
+ * ```
1177
+ */
1178
+ commandDedup(bucketName, options) {
1179
+ return new CommandDedup(this.kv(), bucketName, options?.ttlSeconds);
1180
+ }
1181
+ /**
1182
+ * Config management operations
1183
+ *
1184
+ * @example
1185
+ * ```typescript
1186
+ * const config = client.config();
1187
+ * await config.set("app", { featureX: true });
1188
+ * const { data } = await config.get("app");
1189
+ * await config.patch("app", { maxRetries: 5 });
1190
+ * const configs = await config.list();
1191
+ * await config.delete("app");
1192
+ * ```
1193
+ */
1194
+ config() {
1195
+ return new ConfigClient({
1196
+ serverUrl: this.serverUrl,
1197
+ apiKey: this.apiKey,
1198
+ timeout: this.timeout,
1199
+ onError: this.onErrorHandler,
1200
+ });
1201
+ }
1202
+ /**
1203
+ * Patch a step's output (hot patching)
1204
+ */
1205
+ async patchStep(stepId, output, reason) {
1206
+ const endpoint = "/api/v1/steps/patch";
1207
+ const url = `${this.serverUrl}${endpoint}`;
1208
+ const headers = {
1209
+ "Content-Type": "application/json",
1210
+ };
1211
+ if (this.apiKey) {
1212
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
1213
+ }
1214
+ const controller = new AbortController();
1215
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1216
+ let status;
1217
+ try {
1218
+ const response = await fetch(url, {
1219
+ method: "POST",
1220
+ headers,
1221
+ body: JSON.stringify({ step_id: stepId, output, reason: reason || "" }),
1222
+ signal: controller.signal,
1223
+ });
1224
+ status = response.status;
1225
+ if (!response.ok) {
1226
+ const errorBody = await response.text();
1227
+ throw new Error(errorBody || `Patch step failed: ${response.status}`);
1228
+ }
1229
+ }
1230
+ catch (error) {
1231
+ await this.callOnError(error, { method: "patchStep", endpoint, statusCode: status });
1232
+ throw error;
1233
+ }
1234
+ finally {
1235
+ clearTimeout(timeoutId);
1236
+ }
1237
+ }
1238
+ /**
1239
+ * Resume a paused or failed run
1240
+ */
1241
+ async resumeRun(runId, fromStep) {
1242
+ const endpoint = "/api/v1/runs/resume";
1243
+ const url = `${this.serverUrl}${endpoint}`;
1244
+ const headers = {
1245
+ "Content-Type": "application/json",
1246
+ };
1247
+ if (this.apiKey) {
1248
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
1249
+ }
1250
+ const controller = new AbortController();
1251
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1252
+ let status;
1253
+ try {
1254
+ const response = await fetch(url, {
1255
+ method: "POST",
1256
+ headers,
1257
+ body: JSON.stringify({ run_id: runId, from_step: fromStep || "" }),
1258
+ signal: controller.signal,
1259
+ });
1260
+ status = response.status;
1261
+ if (!response.ok) {
1262
+ const errorBody = await response.text();
1263
+ throw new Error(errorBody || `Resume run failed: ${response.status}`);
1264
+ }
1265
+ return response.json();
1266
+ }
1267
+ catch (error) {
1268
+ await this.callOnError(error, { method: "resumeRun", endpoint, statusCode: status });
1269
+ throw error;
1270
+ }
1271
+ finally {
1272
+ clearTimeout(timeoutId);
1273
+ }
1274
+ }
1275
+ /**
1276
+ * Pause a running workflow run (scoped injection).
1277
+ *
1278
+ * @example
1279
+ * ```typescript
1280
+ * const result = await client.pauseRun("run_abc123");
1281
+ * console.log(result.status); // "paused"
1282
+ * ```
1283
+ */
1284
+ async pauseRun(runId) {
1285
+ return this.request("/ironflow.v1.IronflowService/PauseRun", { run_id: runId }, "pauseRun");
1286
+ }
1287
+ /**
1288
+ * Get the paused state of a run, including completed steps and next step hint.
1289
+ *
1290
+ * @example
1291
+ * ```typescript
1292
+ * const state = await client.getPausedState("run_abc123");
1293
+ * for (const step of state.steps) {
1294
+ * console.log(step.name, step.output, step.injected);
1295
+ * }
1296
+ * console.log("Next step:", state.nextStepHint);
1297
+ * ```
1298
+ */
1299
+ async getPausedState(runId) {
1300
+ const response = await this.request("/ironflow.v1.IronflowService/GetPausedState", { run_id: runId }, "getPausedState");
1301
+ return {
1302
+ steps: (response.steps || []).map((s) => ({
1303
+ id: s.id,
1304
+ name: s.name,
1305
+ output: s.output ? JSON.parse(s.output) : null,
1306
+ injected: s.injected,
1307
+ completedAt: s.completedAt,
1308
+ })),
1309
+ nextStepHint: response.nextStepHint,
1310
+ pauseReason: response.pauseReason,
1311
+ };
1312
+ }
1313
+ /**
1314
+ * Inject new output for a step in a paused run (scoped injection).
1315
+ *
1316
+ * @example
1317
+ * ```typescript
1318
+ * const result = await client.injectStepOutput(
1319
+ * "run_abc123",
1320
+ * "step_xyz",
1321
+ * { corrected: true },
1322
+ * "Manual correction"
1323
+ * );
1324
+ * console.log("Previous output:", result.previousOutput);
1325
+ * ```
1326
+ */
1327
+ async injectStepOutput(runId, stepId, newOutput, reason) {
1328
+ const response = await this.request("/ironflow.v1.IronflowService/InjectStepOutput", {
1329
+ run_id: runId,
1330
+ step_id: stepId,
1331
+ new_output: JSON.stringify(newOutput),
1332
+ reason: reason ?? "",
1333
+ }, "injectStepOutput");
1334
+ return {
1335
+ stepId: response.stepId,
1336
+ previousOutput: response.previousOutput
1337
+ ? JSON.parse(response.previousOutput)
1338
+ : null,
1339
+ };
1340
+ }
1341
+ /**
1342
+ * List registered functions
1343
+ */
1344
+ async listFunctions() {
1345
+ const endpoint = "/api/v1/functions";
1346
+ const url = `${this.serverUrl}${endpoint}`;
1347
+ const headers = {};
1348
+ if (this.apiKey) {
1349
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
1350
+ }
1351
+ const controller = new AbortController();
1352
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1353
+ let status;
1354
+ try {
1355
+ const response = await fetch(url, {
1356
+ method: "GET",
1357
+ headers,
1358
+ signal: controller.signal,
1359
+ });
1360
+ status = response.status;
1361
+ if (!response.ok) {
1362
+ throw new Error(`List functions failed: ${response.status}`);
1363
+ }
1364
+ const data = (await response.json());
1365
+ return data.functions || [];
1366
+ }
1367
+ catch (error) {
1368
+ await this.callOnError(error, { method: "listFunctions", endpoint, statusCode: status });
1369
+ throw error;
1370
+ }
1371
+ finally {
1372
+ clearTimeout(timeoutId);
1373
+ }
1374
+ }
1375
+ /**
1376
+ * List connected workers
1377
+ */
1378
+ async listWorkers() {
1379
+ const endpoint = "/api/v1/workers";
1380
+ const url = `${this.serverUrl}${endpoint}`;
1381
+ const headers = {};
1382
+ if (this.apiKey) {
1383
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
1384
+ }
1385
+ const controller = new AbortController();
1386
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1387
+ let status;
1388
+ try {
1389
+ const response = await fetch(url, {
1390
+ method: "GET",
1391
+ headers,
1392
+ signal: controller.signal,
1393
+ });
1394
+ status = response.status;
1395
+ if (!response.ok) {
1396
+ throw new Error(`List workers failed: ${response.status}`);
1397
+ }
1398
+ const data = (await response.json());
1399
+ return data.workers || [];
1400
+ }
1401
+ catch (error) {
1402
+ await this.callOnError(error, { method: "listWorkers", endpoint, statusCode: status });
1403
+ throw error;
1404
+ }
1405
+ finally {
1406
+ clearTimeout(timeoutId);
1407
+ }
1408
+ }
1409
+ /**
1410
+ * Make an HTTP request to the server
1411
+ */
1412
+ async request(endpoint, body, method) {
1413
+ const url = `${this.serverUrl}${endpoint}`;
1414
+ const headers = {
1415
+ "Content-Type": "application/json",
1416
+ };
1417
+ if (this.apiKey) {
1418
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
1419
+ }
1420
+ const runId = getCurrentRunId();
1421
+ if (runId) {
1422
+ headers["X-Ironflow-Run-ID"] = runId;
1423
+ }
1424
+ const controller = new AbortController();
1425
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1426
+ let status;
1427
+ try {
1428
+ const response = await fetch(url, {
1429
+ method: "POST",
1430
+ headers,
1431
+ body: JSON.stringify(body),
1432
+ signal: controller.signal,
1433
+ });
1434
+ status = response.status;
1435
+ if (!response.ok) {
1436
+ const errorBody = await response.text();
1437
+ let errorMessage = `Request failed with status ${response.status}`;
1438
+ if (errorBody) {
1439
+ try {
1440
+ const errorJson = JSON.parse(errorBody);
1441
+ if (errorJson.message) {
1442
+ errorMessage = errorJson.message;
1443
+ }
1444
+ else if (errorJson.code) {
1445
+ errorMessage = `Error code: ${errorJson.code}`;
1446
+ }
1447
+ else {
1448
+ errorMessage = errorBody;
1449
+ }
1450
+ }
1451
+ catch {
1452
+ // Not a JSON response, use raw text.
1453
+ errorMessage = errorBody;
1454
+ }
1455
+ }
1456
+ this.throwTypedError(response.status, errorMessage);
1457
+ }
1458
+ return response.json();
1459
+ }
1460
+ catch (error) {
1461
+ if (method) {
1462
+ await this.callOnError(error, { method, endpoint, statusCode: status });
1463
+ }
1464
+ throw error;
1465
+ }
1466
+ finally {
1467
+ clearTimeout(timeoutId);
1468
+ }
1469
+ }
1470
+ /**
1471
+ * Throw a typed error based on HTTP status code.
1472
+ */
1473
+ throwTypedError(status, message) {
1474
+ switch (status) {
1475
+ case 401:
1476
+ throw new UnauthenticatedError(message);
1477
+ case 402:
1478
+ throw new EnterpriseRequiredError(message);
1479
+ case 403:
1480
+ throw new UnauthorizedError(message);
1481
+ default:
1482
+ throw new IronflowError(message);
1483
+ }
1484
+ }
1485
+ /**
1486
+ * Make a REST HTTP request to the server (supports GET, POST, PATCH, DELETE)
1487
+ */
1488
+ async restRequest(httpMethod, path, body, method) {
1489
+ const url = `${this.serverUrl}${path}`;
1490
+ const headers = {};
1491
+ if (this.apiKey) {
1492
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
1493
+ }
1494
+ const options = { method: httpMethod, headers };
1495
+ if (body && (httpMethod === "POST" || httpMethod === "PATCH" || httpMethod === "PUT")) {
1496
+ headers["Content-Type"] = "application/json";
1497
+ options.body = JSON.stringify(body);
1498
+ }
1499
+ const controller = new AbortController();
1500
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
1501
+ let status;
1502
+ try {
1503
+ const response = await fetch(url, {
1504
+ ...options,
1505
+ signal: controller.signal,
1506
+ });
1507
+ status = response.status;
1508
+ if (!response.ok) {
1509
+ const errBody = await response
1510
+ .json()
1511
+ .catch(() => ({ error: response.statusText }));
1512
+ const message = errBody.error ||
1513
+ errBody.message ||
1514
+ response.statusText;
1515
+ this.throwTypedError(response.status, message);
1516
+ }
1517
+ if (response.status === 204)
1518
+ return undefined;
1519
+ return response.json();
1520
+ }
1521
+ catch (error) {
1522
+ if (method) {
1523
+ await this.callOnError(error, { method, endpoint: path, statusCode: status });
1524
+ }
1525
+ throw error;
1526
+ }
1527
+ finally {
1528
+ clearTimeout(timeoutId);
1529
+ }
1530
+ }
1531
+ /**
1532
+ * Call the global onError handler if registered.
1533
+ * Swallows any errors thrown by the callback.
1534
+ */
1535
+ async callOnError(error, context) {
1536
+ if (!this.onErrorHandler)
1537
+ return;
1538
+ try {
1539
+ await this.onErrorHandler(error, context);
1540
+ }
1541
+ catch (callbackError) {
1542
+ console.error("[ironflow] onError callback threw:", callbackError);
1543
+ }
1544
+ }
1545
+ }
1546
+ /**
1547
+ * Create a new Ironflow client
1548
+ *
1549
+ * @example
1550
+ * ```typescript
1551
+ * const client = createClient({ serverUrl: "http://localhost:9123" });
1552
+ * ```
1553
+ */
1554
+ export function createClient(config) {
1555
+ return new IronflowClient(config);
1556
+ }
1557
+ //# sourceMappingURL=client.js.map