flowforge-client 0.1.2

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.mjs ADDED
@@ -0,0 +1,774 @@
1
+ // src/builder.ts
2
+ var QueryBuilder = class {
3
+ constructor(request, basePath, responseKey) {
4
+ this.request = request;
5
+ this.basePath = basePath;
6
+ this.responseKey = responseKey;
7
+ this.params = { filters: {} };
8
+ }
9
+ /**
10
+ * Filter by exact match.
11
+ */
12
+ eq(field, value) {
13
+ this.params.filters[field] = value;
14
+ return this;
15
+ }
16
+ /**
17
+ * Order results by a field.
18
+ */
19
+ order(field, direction = "asc") {
20
+ this.params.orderBy = field;
21
+ this.params.orderDir = direction;
22
+ return this;
23
+ }
24
+ /**
25
+ * Limit the number of results.
26
+ */
27
+ limit(n) {
28
+ this.params.limitValue = n;
29
+ return this;
30
+ }
31
+ /**
32
+ * Skip the first n results (for pagination).
33
+ */
34
+ offset(n) {
35
+ this.params.offsetValue = n;
36
+ return this;
37
+ }
38
+ /**
39
+ * Build query string from params.
40
+ */
41
+ buildQueryString() {
42
+ const searchParams = new URLSearchParams();
43
+ for (const [key, value] of Object.entries(this.params.filters)) {
44
+ if (value !== void 0 && value !== null) {
45
+ searchParams.set(key, String(value));
46
+ }
47
+ }
48
+ if (this.params.orderBy) {
49
+ searchParams.set("order_by", this.params.orderBy);
50
+ searchParams.set("order_dir", this.params.orderDir || "asc");
51
+ }
52
+ if (this.params.limitValue !== void 0) {
53
+ searchParams.set("limit", String(this.params.limitValue));
54
+ }
55
+ if (this.params.offsetValue !== void 0) {
56
+ searchParams.set("offset", String(this.params.offsetValue));
57
+ }
58
+ const query = searchParams.toString();
59
+ return query ? `?${query}` : "";
60
+ }
61
+ /**
62
+ * Execute the query and return multiple results.
63
+ */
64
+ async execute() {
65
+ const path = `${this.basePath}${this.buildQueryString()}`;
66
+ const result = await this.request("GET", path);
67
+ if (result.error) {
68
+ return { data: null, error: result.error };
69
+ }
70
+ const items = result.data[this.responseKey] || [];
71
+ return { data: items, error: null };
72
+ }
73
+ /**
74
+ * Execute the query and return a single result.
75
+ * Returns an error if no results found.
76
+ */
77
+ async single() {
78
+ const result = await this.limit(1).execute();
79
+ if (result.error) {
80
+ return { data: null, error: result.error };
81
+ }
82
+ if (result.data.length === 0) {
83
+ const error = {
84
+ message: "No results found",
85
+ status: 404,
86
+ name: "FlowForgeError"
87
+ };
88
+ return { data: null, error };
89
+ }
90
+ return { data: result.data[0], error: null };
91
+ }
92
+ };
93
+
94
+ // src/resources/events.ts
95
+ var EventsResource = class {
96
+ constructor(request) {
97
+ this.request = request;
98
+ }
99
+ /**
100
+ * Send an event to trigger matching workflows.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const { data, error } = await ff.events.send('order/created', {
105
+ * order_id: '123',
106
+ * customer: 'Alice'
107
+ * });
108
+ * if (error) console.error(error.message);
109
+ * else console.log('Triggered runs:', data.runs);
110
+ * ```
111
+ */
112
+ async send(name, data, options) {
113
+ return this.request("POST", "/events", {
114
+ name,
115
+ data,
116
+ id: options?.id,
117
+ user_id: options?.user_id,
118
+ timestamp: options?.timestamp
119
+ });
120
+ }
121
+ /**
122
+ * Get a specific event by ID.
123
+ *
124
+ * @example
125
+ * ```ts
126
+ * const { data: event } = await ff.events.get('event-id');
127
+ * ```
128
+ */
129
+ async get(eventId) {
130
+ return this.request("GET", `/events/${eventId}`);
131
+ }
132
+ /**
133
+ * Start building a query to select events.
134
+ *
135
+ * @example
136
+ * ```ts
137
+ * const { data: events } = await ff.events
138
+ * .select()
139
+ * .eq('name', 'order/*')
140
+ * .limit(10)
141
+ * .execute();
142
+ * ```
143
+ */
144
+ select() {
145
+ return new QueryBuilder(
146
+ this.request,
147
+ "/events",
148
+ "events"
149
+ );
150
+ }
151
+ };
152
+
153
+ // src/resources/runs.ts
154
+ var RunsResource = class {
155
+ constructor(request) {
156
+ this.request = request;
157
+ }
158
+ /**
159
+ * Get a run by ID, including its steps.
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * const { data: run } = await ff.runs.get('run-id');
164
+ * console.log(run.status, run.steps);
165
+ * ```
166
+ */
167
+ async get(runId) {
168
+ return this.request("GET", `/runs/${runId}`);
169
+ }
170
+ /**
171
+ * Start building a query to select runs.
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * const { data: runs } = await ff.runs
176
+ * .select()
177
+ * .eq('status', 'completed')
178
+ * .order('created_at', 'desc')
179
+ * .limit(10)
180
+ * .execute();
181
+ * ```
182
+ */
183
+ select() {
184
+ return new QueryBuilder(this.request, "/runs", "runs");
185
+ }
186
+ /**
187
+ * Cancel a running workflow.
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * const { error } = await ff.runs.cancel('run-id');
192
+ * if (error) console.error('Failed to cancel:', error.message);
193
+ * ```
194
+ */
195
+ async cancel(runId) {
196
+ return this.request(
197
+ "POST",
198
+ `/runs/${runId}/cancel`
199
+ );
200
+ }
201
+ /**
202
+ * Replay a completed or failed run.
203
+ *
204
+ * @example
205
+ * ```ts
206
+ * const { data: newRun } = await ff.runs.replay('run-id');
207
+ * console.log('New run ID:', newRun.id);
208
+ * ```
209
+ */
210
+ async replay(runId) {
211
+ return this.request("POST", `/runs/${runId}/replay`);
212
+ }
213
+ /**
214
+ * Wait for a run to complete (polling).
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * const { data: completedRun, error } = await ff.runs.waitFor('run-id', {
219
+ * timeout: 60000
220
+ * });
221
+ * if (error) console.error('Timeout or error:', error.message);
222
+ * else console.log('Run completed with status:', completedRun.status);
223
+ * ```
224
+ */
225
+ async waitFor(runId, options) {
226
+ const timeout = options?.timeout ?? 6e4;
227
+ const interval = options?.interval ?? 1e3;
228
+ const start = Date.now();
229
+ while (Date.now() - start < timeout) {
230
+ const result = await this.get(runId);
231
+ if (result.error) {
232
+ return result;
233
+ }
234
+ if (["completed", "failed", "cancelled"].includes(result.data.status)) {
235
+ return result;
236
+ }
237
+ await new Promise((resolve) => setTimeout(resolve, interval));
238
+ }
239
+ const error = {
240
+ message: "Timeout waiting for run to complete",
241
+ status: 408,
242
+ name: "FlowForgeError"
243
+ };
244
+ return { data: null, error };
245
+ }
246
+ };
247
+
248
+ // src/resources/functions.ts
249
+ var FunctionsResource = class {
250
+ constructor(request) {
251
+ this.request = request;
252
+ }
253
+ /**
254
+ * Get a function by ID.
255
+ *
256
+ * @example
257
+ * ```ts
258
+ * const { data: fn } = await ff.functions.get('my-function');
259
+ * console.log(fn.name, fn.is_active);
260
+ * ```
261
+ */
262
+ async get(functionId) {
263
+ return this.request("GET", `/functions/${functionId}`);
264
+ }
265
+ /**
266
+ * Start building a query to select functions.
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * const { data: fns } = await ff.functions
271
+ * .select()
272
+ * .eq('is_active', true)
273
+ * .eq('trigger_type', 'event')
274
+ * .execute();
275
+ * ```
276
+ */
277
+ select() {
278
+ return new QueryBuilder(
279
+ this.request,
280
+ "/functions",
281
+ "functions"
282
+ );
283
+ }
284
+ /**
285
+ * Create a new function.
286
+ *
287
+ * @example
288
+ * ```ts
289
+ * const { data: fn, error } = await ff.functions.create({
290
+ * id: 'order-processor',
291
+ * name: 'Order Processor',
292
+ * trigger_type: 'event',
293
+ * trigger_value: 'order/created',
294
+ * endpoint_url: 'http://localhost:3000/api/workflows/order',
295
+ * });
296
+ * ```
297
+ */
298
+ async create(input) {
299
+ return this.request("POST", "/functions", input);
300
+ }
301
+ /**
302
+ * Update an existing function.
303
+ *
304
+ * @example
305
+ * ```ts
306
+ * const { error } = await ff.functions.update('my-function', {
307
+ * is_active: false
308
+ * });
309
+ * ```
310
+ */
311
+ async update(functionId, input) {
312
+ return this.request(
313
+ "PATCH",
314
+ `/functions/${functionId}`,
315
+ input
316
+ );
317
+ }
318
+ /**
319
+ * Delete a function.
320
+ *
321
+ * @example
322
+ * ```ts
323
+ * const { error } = await ff.functions.delete('my-function');
324
+ * if (error) console.error('Failed to delete:', error.message);
325
+ * ```
326
+ */
327
+ async delete(functionId) {
328
+ return this.request(
329
+ "DELETE",
330
+ `/functions/${functionId}`
331
+ );
332
+ }
333
+ };
334
+
335
+ // src/resources/tools.ts
336
+ var ToolsResource = class {
337
+ constructor(request) {
338
+ this.request = request;
339
+ }
340
+ /**
341
+ * Get a tool by name.
342
+ *
343
+ * @example
344
+ * ```ts
345
+ * const { data: tool } = await ff.tools.get('send-email');
346
+ * console.log(tool.description, tool.requires_approval);
347
+ * ```
348
+ */
349
+ async get(toolName) {
350
+ return this.request("GET", `/tools/${toolName}`);
351
+ }
352
+ /**
353
+ * Start building a query to select tools.
354
+ *
355
+ * @example
356
+ * ```ts
357
+ * const { data: tools } = await ff.tools
358
+ * .select()
359
+ * .eq('requires_approval', true)
360
+ * .eq('is_active', true)
361
+ * .execute();
362
+ * ```
363
+ */
364
+ select() {
365
+ return new QueryBuilder(this.request, "/tools", "tools");
366
+ }
367
+ /**
368
+ * Create a new tool.
369
+ *
370
+ * @example
371
+ * ```ts
372
+ * const { data: tool, error } = await ff.tools.create({
373
+ * name: 'send-email',
374
+ * description: 'Send an email to a recipient',
375
+ * parameters: {
376
+ * type: 'object',
377
+ * properties: {
378
+ * to: { type: 'string', description: 'Recipient email' },
379
+ * subject: { type: 'string', description: 'Email subject' },
380
+ * body: { type: 'string', description: 'Email body' },
381
+ * },
382
+ * required: ['to', 'subject', 'body'],
383
+ * },
384
+ * requires_approval: true,
385
+ * });
386
+ * ```
387
+ */
388
+ async create(input) {
389
+ return this.request("POST", "/tools", input);
390
+ }
391
+ /**
392
+ * Update an existing tool.
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * const { error } = await ff.tools.update('send-email', {
397
+ * requires_approval: false
398
+ * });
399
+ * ```
400
+ */
401
+ async update(toolName, input) {
402
+ return this.request("PATCH", `/tools/${toolName}`, input);
403
+ }
404
+ /**
405
+ * Delete a tool.
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * const { error } = await ff.tools.delete('my-tool');
410
+ * if (error) console.error('Failed to delete:', error.message);
411
+ * ```
412
+ */
413
+ async delete(toolName) {
414
+ return this.request(
415
+ "DELETE",
416
+ `/tools/${toolName}`
417
+ );
418
+ }
419
+ };
420
+
421
+ // src/resources/approvals.ts
422
+ var ApprovalsResource = class {
423
+ constructor(request) {
424
+ this.request = request;
425
+ }
426
+ /**
427
+ * Get an approval by ID.
428
+ *
429
+ * @example
430
+ * ```ts
431
+ * const { data: approval } = await ff.approvals.get('approval-id');
432
+ * console.log(approval.tool_name, approval.status);
433
+ * ```
434
+ */
435
+ async get(approvalId) {
436
+ return this.request("GET", `/approvals/${approvalId}`);
437
+ }
438
+ /**
439
+ * Start building a query to select approvals.
440
+ *
441
+ * @example
442
+ * ```ts
443
+ * const { data: pending } = await ff.approvals
444
+ * .select()
445
+ * .eq('status', 'pending')
446
+ * .execute();
447
+ * ```
448
+ */
449
+ select() {
450
+ return new QueryBuilder(
451
+ this.request,
452
+ "/approvals",
453
+ "approvals"
454
+ );
455
+ }
456
+ /**
457
+ * Approve a pending tool call.
458
+ *
459
+ * @example
460
+ * ```ts
461
+ * const { error } = await ff.approvals.approve('approval-id');
462
+ * if (error) console.error('Failed to approve:', error.message);
463
+ * ```
464
+ *
465
+ * @example With modified arguments
466
+ * ```ts
467
+ * const { error } = await ff.approvals.approve('approval-id', {
468
+ * modifiedArguments: { amount: 50 } // Override the original amount
469
+ * });
470
+ * ```
471
+ */
472
+ async approve(approvalId, options) {
473
+ return this.request(
474
+ "POST",
475
+ `/approvals/${approvalId}/approve`,
476
+ {
477
+ modified_arguments: options?.modifiedArguments
478
+ }
479
+ );
480
+ }
481
+ /**
482
+ * Reject a pending tool call.
483
+ *
484
+ * @example
485
+ * ```ts
486
+ * const { error } = await ff.approvals.reject('approval-id', 'Too risky');
487
+ * if (error) console.error('Failed to reject:', error.message);
488
+ * ```
489
+ */
490
+ async reject(approvalId, reason) {
491
+ return this.request(
492
+ "POST",
493
+ `/approvals/${approvalId}/reject`,
494
+ { reason }
495
+ );
496
+ }
497
+ };
498
+
499
+ // src/resources/health.ts
500
+ var HealthResource = class {
501
+ constructor(request) {
502
+ this.request = request;
503
+ }
504
+ /**
505
+ * Check if the server is healthy.
506
+ *
507
+ * @example
508
+ * ```ts
509
+ * const { data: healthy, error } = await ff.health.check();
510
+ * if (healthy) console.log('Server is healthy');
511
+ * else console.error('Server is unhealthy:', error?.message);
512
+ * ```
513
+ */
514
+ async check() {
515
+ const result = await this.request("GET", "/health");
516
+ if (result.error) {
517
+ return { data: false, error: null };
518
+ }
519
+ return { data: result.data.status === "healthy", error: null };
520
+ }
521
+ /**
522
+ * Get server statistics.
523
+ *
524
+ * @example
525
+ * ```ts
526
+ * const { data: stats } = await ff.health.stats();
527
+ * console.log('Total runs:', stats.runs.total);
528
+ * console.log('Active functions:', stats.functions.active);
529
+ * ```
530
+ */
531
+ async stats() {
532
+ return this.request("GET", "/stats");
533
+ }
534
+ };
535
+
536
+ // src/resources/users.ts
537
+ var UsersResource = class {
538
+ constructor(request) {
539
+ this.request = request;
540
+ }
541
+ /**
542
+ * List all users (admin only).
543
+ *
544
+ * @param options - Filter options
545
+ * @returns List of users
546
+ *
547
+ * @example
548
+ * ```ts
549
+ * const { data } = await ff.users.list({ includeInactive: true });
550
+ * console.log('Total users:', data?.total);
551
+ * ```
552
+ */
553
+ async list(options) {
554
+ const params = new URLSearchParams();
555
+ if (options?.includeInactive) {
556
+ params.set("include_inactive", "true");
557
+ }
558
+ const query = params.toString();
559
+ return this.request(
560
+ "GET",
561
+ `/users${query ? `?${query}` : ""}`
562
+ );
563
+ }
564
+ /**
565
+ * Get a user by ID (admin only).
566
+ *
567
+ * @param userId - User ID
568
+ * @returns User details
569
+ */
570
+ async get(userId) {
571
+ return this.request("GET", `/users/${userId}`);
572
+ }
573
+ /**
574
+ * Create a new user (admin only).
575
+ *
576
+ * @param input - User details
577
+ * @returns Created user
578
+ *
579
+ * @example
580
+ * ```ts
581
+ * const { data, error } = await ff.users.create({
582
+ * email: 'user@example.com',
583
+ * password: 'securepassword',
584
+ * name: 'John Doe',
585
+ * role: 'member'
586
+ * });
587
+ * ```
588
+ */
589
+ async create(input) {
590
+ return this.request("POST", "/users", input);
591
+ }
592
+ /**
593
+ * Update a user (admin only).
594
+ *
595
+ * @param userId - User ID
596
+ * @param input - Fields to update
597
+ * @returns Updated user
598
+ */
599
+ async update(userId, input) {
600
+ return this.request("PATCH", `/users/${userId}`, input);
601
+ }
602
+ /**
603
+ * Delete a user (admin only).
604
+ *
605
+ * @param userId - User ID
606
+ * @returns Success message
607
+ */
608
+ async delete(userId) {
609
+ return this.request("DELETE", `/users/${userId}`);
610
+ }
611
+ };
612
+
613
+ // src/resources/api-keys.ts
614
+ var ApiKeysResource = class {
615
+ constructor(request) {
616
+ this.request = request;
617
+ }
618
+ /**
619
+ * List all API keys for the tenant.
620
+ *
621
+ * @param options - Filter options
622
+ * @returns List of API keys (without the actual key values)
623
+ *
624
+ * @example
625
+ * ```ts
626
+ * const { data } = await ff.apiKeys.list();
627
+ * data?.keys.forEach(key => {
628
+ * console.log(`${key.name}: ${key.key_prefix}... (${key.key_type})`);
629
+ * });
630
+ * ```
631
+ */
632
+ async list(options) {
633
+ const params = new URLSearchParams();
634
+ if (options?.includeRevoked) {
635
+ params.set("include_revoked", "true");
636
+ }
637
+ const query = params.toString();
638
+ return this.request(
639
+ "GET",
640
+ `/auth/keys${query ? `?${query}` : ""}`
641
+ );
642
+ }
643
+ /**
644
+ * Get an API key by ID.
645
+ *
646
+ * @param keyId - API key ID
647
+ * @returns API key details (without the actual key value)
648
+ */
649
+ async get(keyId) {
650
+ return this.request("GET", `/auth/keys/${keyId}`);
651
+ }
652
+ /**
653
+ * Create a new API key.
654
+ *
655
+ * @param input - API key configuration
656
+ * @returns Created API key WITH the actual key value (only returned once!)
657
+ *
658
+ * @example
659
+ * ```ts
660
+ * const { data, error } = await ff.apiKeys.create({
661
+ * name: 'Production API Key',
662
+ * key_type: 'live',
663
+ * scopes: ['events:send', 'runs:read']
664
+ * });
665
+ *
666
+ * if (data) {
667
+ * // IMPORTANT: Store this key - it won't be shown again!
668
+ * console.log('New API key:', data.key); // ff_live_a1b2c3...
669
+ * }
670
+ * ```
671
+ */
672
+ async create(input) {
673
+ return this.request("POST", "/auth/keys", input);
674
+ }
675
+ /**
676
+ * Revoke an API key.
677
+ *
678
+ * @param keyId - API key ID
679
+ * @param reason - Optional reason for revocation
680
+ * @returns Success message
681
+ *
682
+ * @example
683
+ * ```ts
684
+ * const { error } = await ff.apiKeys.revoke('key-id', 'Compromised');
685
+ * if (!error) {
686
+ * console.log('Key revoked successfully');
687
+ * }
688
+ * ```
689
+ */
690
+ async revoke(keyId, reason) {
691
+ return this.request(
692
+ "DELETE",
693
+ `/auth/keys/${keyId}`,
694
+ reason ? { reason } : void 0
695
+ );
696
+ }
697
+ };
698
+
699
+ // src/client.ts
700
+ function createClient(baseUrl, options = {}) {
701
+ const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
702
+ const fetchFn = options.fetch || globalThis.fetch;
703
+ async function request(method, path, body) {
704
+ const headers = {
705
+ "Content-Type": "application/json"
706
+ };
707
+ if (options.apiKey) {
708
+ headers["X-FlowForge-API-Key"] = options.apiKey;
709
+ }
710
+ try {
711
+ const response = await fetchFn(`${normalizedBaseUrl}/api/v1${path}`, {
712
+ method,
713
+ headers,
714
+ body: body ? JSON.stringify(body) : void 0
715
+ });
716
+ if (!response.ok) {
717
+ const errorBody = await response.json().catch(() => ({}));
718
+ const error = {
719
+ message: errorBody.detail || `HTTP ${response.status}`,
720
+ status: response.status,
721
+ code: errorBody.code,
722
+ detail: errorBody,
723
+ name: "FlowForgeError"
724
+ };
725
+ return { data: null, error };
726
+ }
727
+ const data = await response.json();
728
+ return { data, error: null };
729
+ } catch (err) {
730
+ const error = {
731
+ message: err instanceof Error ? err.message : "Network error",
732
+ status: 0,
733
+ code: "NETWORK_ERROR",
734
+ detail: err,
735
+ name: "FlowForgeError"
736
+ };
737
+ return { data: null, error };
738
+ }
739
+ }
740
+ return {
741
+ events: new EventsResource(request),
742
+ runs: new RunsResource(request),
743
+ functions: new FunctionsResource(request),
744
+ tools: new ToolsResource(request),
745
+ approvals: new ApprovalsResource(request),
746
+ health: new HealthResource(request),
747
+ users: new UsersResource(request),
748
+ apiKeys: new ApiKeysResource(request)
749
+ };
750
+ }
751
+
752
+ // src/types.ts
753
+ var FlowForgeError = class extends Error {
754
+ constructor(message, status, code, detail) {
755
+ super(message);
756
+ this.status = status;
757
+ this.code = code;
758
+ this.detail = detail;
759
+ this.name = "FlowForgeError";
760
+ }
761
+ };
762
+ export {
763
+ ApiKeysResource,
764
+ ApprovalsResource,
765
+ EventsResource,
766
+ FlowForgeError,
767
+ FunctionsResource,
768
+ HealthResource,
769
+ QueryBuilder,
770
+ RunsResource,
771
+ ToolsResource,
772
+ UsersResource,
773
+ createClient
774
+ };