@fabric-platform/sdk 0.2.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/README.md +686 -0
- package/dist/index.cjs +1986 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1884 -0
- package/dist/index.d.ts +1884 -0
- package/dist/index.js +1921 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
# @fabric-platform/sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for the Fabric AI workflow platform. Designed for Next.js (server components, API routes, client-side) with zero runtime dependencies.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @fabric-platform/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Node.js 18+.
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { FabricClient } from '@fabric-platform/sdk';
|
|
17
|
+
|
|
18
|
+
const fabric = new FabricClient({
|
|
19
|
+
baseUrl: 'https://api.fabric.ai',
|
|
20
|
+
auth: { type: 'api-key', key: 'fab_...' },
|
|
21
|
+
organizationId: 'your-org-id',
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Submit a workflow and stream live progress
|
|
25
|
+
const run = await fabric.workflows.runs.submitAndWait('research/deep_research', {
|
|
26
|
+
input: { query: 'AI trends in 2026' },
|
|
27
|
+
onEvent(event) {
|
|
28
|
+
console.log(`${event.kind}: ${event.node_key}`);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
console.log(run.status); // "completed"
|
|
33
|
+
console.log(run.output); // workflow result
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Authentication
|
|
37
|
+
|
|
38
|
+
The SDK is **stateless** — it does not store tokens. Your application owns the session.
|
|
39
|
+
|
|
40
|
+
### API Key (server-side)
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
const fabric = new FabricClient({
|
|
44
|
+
auth: { type: 'api-key', key: process.env.FABRIC_API_KEY! },
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Bearer Token (client-side)
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
const fabric = new FabricClient({
|
|
52
|
+
auth: { type: 'bearer', token: supabaseSession.access_token },
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Dynamic Token (Next.js server components)
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { cookies } from 'next/headers';
|
|
60
|
+
|
|
61
|
+
const fabric = new FabricClient({
|
|
62
|
+
auth: async () => {
|
|
63
|
+
const token = cookies().get('fabric_access_token')?.value;
|
|
64
|
+
if (!token) throw new Error('Not authenticated');
|
|
65
|
+
return token;
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### OAuth Client Credentials (service-to-service)
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
const fabric = new FabricClient({
|
|
74
|
+
auth: { type: 'oauth', clientId: '...', clientSecret: '...' },
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The OAuth strategy auto-exchanges credentials for an access token and caches it until expiry.
|
|
79
|
+
|
|
80
|
+
### Auth Endpoints
|
|
81
|
+
|
|
82
|
+
The `auth` resource exposes login/signup methods that **return** tokens without storing them:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
// No auth needed for these
|
|
86
|
+
const fabric = new FabricClient({ baseUrl: 'https://api.fabric.ai' });
|
|
87
|
+
|
|
88
|
+
const { access_token, refresh_token, user } = await fabric.auth.login('user@example.com', 'password');
|
|
89
|
+
const { access_token: newToken } = await fabric.auth.refresh(refresh_token);
|
|
90
|
+
|
|
91
|
+
await fabric.auth.signup('new@example.com', 'password');
|
|
92
|
+
await fabric.auth.magicLink('user@example.com');
|
|
93
|
+
await fabric.auth.forgotPassword('user@example.com');
|
|
94
|
+
await fabric.auth.resetPassword(recoveryToken, 'new-password');
|
|
95
|
+
|
|
96
|
+
// Social login — returns a redirect URL
|
|
97
|
+
const url = fabric.auth.socialLoginUrl('google');
|
|
98
|
+
|
|
99
|
+
// MFA
|
|
100
|
+
const factor = await fabric.auth.mfa.enroll('totp');
|
|
101
|
+
const challenge = await fabric.auth.mfa.challenge(factor.id);
|
|
102
|
+
const verified = await fabric.auth.mfa.verify(factor.id, challenge.id, '123456');
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
interface FabricConfig {
|
|
109
|
+
/** Base URL of the Fabric API. Default: http://localhost:3001 */
|
|
110
|
+
baseUrl?: string;
|
|
111
|
+
/** Authentication strategy or dynamic token provider. */
|
|
112
|
+
auth?: AuthStrategy | (() => Promise<string>);
|
|
113
|
+
/** Default organization ID for scoped requests. */
|
|
114
|
+
organizationId?: string;
|
|
115
|
+
/** Default team ID for scoped requests. */
|
|
116
|
+
teamId?: string;
|
|
117
|
+
/** Request timeout in milliseconds. Default: 30000 */
|
|
118
|
+
timeout?: number;
|
|
119
|
+
/** Custom fetch implementation (e.g., Next.js enhanced fetch). */
|
|
120
|
+
fetch?: typeof globalThis.fetch;
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Next.js Fetch Caching
|
|
125
|
+
|
|
126
|
+
Pass Next.js's extended `fetch` for cache control:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
const fabric = new FabricClient({
|
|
130
|
+
auth: { type: 'api-key', key: process.env.FABRIC_API_KEY! },
|
|
131
|
+
fetch: fetch, // Next.js enhanced fetch
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Workflows
|
|
136
|
+
|
|
137
|
+
### Registry (create, list, manage)
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
// Create a workflow definition
|
|
141
|
+
const entry = await fabric.workflows.registry.create({
|
|
142
|
+
name: 'my-pipeline',
|
|
143
|
+
language: 'python',
|
|
144
|
+
source: 'custom',
|
|
145
|
+
entry_point: 'main.py',
|
|
146
|
+
description: 'My custom pipeline',
|
|
147
|
+
input_schema: { type: 'object', properties: { query: { type: 'string' } } },
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// List workflows (hierarchical: team > org > global)
|
|
151
|
+
const { items, pagination } = await fabric.workflows.registry.list();
|
|
152
|
+
const { items: orgWorkflows } = await fabric.workflows.registry.list({
|
|
153
|
+
organization_id: 'org-uuid',
|
|
154
|
+
limit: 50,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Get a specific workflow by name
|
|
158
|
+
const workflow = await fabric.workflows.registry.get('research/deep_research');
|
|
159
|
+
|
|
160
|
+
// Delete a workflow
|
|
161
|
+
await fabric.workflows.registry.delete('my-pipeline');
|
|
162
|
+
|
|
163
|
+
// Compose multiple workflows into a pipeline
|
|
164
|
+
const composed = await fabric.workflows.compose({
|
|
165
|
+
name: 'research-to-hooks',
|
|
166
|
+
steps: ['research/deep_research', 'hooks/generate'],
|
|
167
|
+
description: 'Research a topic then generate hooks',
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Runs (execute, monitor, control)
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// Submit a run (returns immediately)
|
|
175
|
+
const run = await fabric.workflows.runs.submit('research/deep_research', {
|
|
176
|
+
input: { query: 'AI trends' },
|
|
177
|
+
metadata: { source: 'dashboard' },
|
|
178
|
+
priority: 75,
|
|
179
|
+
idempotencyKey: 'unique-request-id', // prevent double-submissions
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Get run status
|
|
183
|
+
const status = await fabric.workflows.runs.get(run.id);
|
|
184
|
+
console.log(status.status); // "pending" | "running" | "completed" | "failed" | ...
|
|
185
|
+
console.log(status.progress); // { total_tasks: 5, completed_tasks: 3, percentage: 60, ... }
|
|
186
|
+
|
|
187
|
+
// List runs
|
|
188
|
+
const { items: runs } = await fabric.workflows.runs.list({ limit: 20 });
|
|
189
|
+
|
|
190
|
+
// Cancel a run
|
|
191
|
+
await fabric.workflows.runs.cancel(run.id, 'No longer needed');
|
|
192
|
+
|
|
193
|
+
// Pause / Resume
|
|
194
|
+
await fabric.workflows.runs.pause(run.id, 'Waiting for approval');
|
|
195
|
+
await fabric.workflows.runs.resume(run.id);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Submit and Wait
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// Submit and block until completion, with live progress callbacks
|
|
202
|
+
const result = await fabric.workflows.runs.submitAndWait('research/deep_research', {
|
|
203
|
+
input: { query: 'AI trends' },
|
|
204
|
+
timeoutMs: 300_000, // 5 minutes
|
|
205
|
+
onEvent(event) {
|
|
206
|
+
if (event.kind === 'workflow.node.completed') {
|
|
207
|
+
console.log(`Step done: ${event.node_key}`);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
console.log(result.output);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Real-Time Events (SSE)
|
|
216
|
+
|
|
217
|
+
### Stream events for a workflow run
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const stream = fabric.workflows.runs.streamEvents(run.id, {
|
|
221
|
+
onEvent(event) {
|
|
222
|
+
switch (event.kind) {
|
|
223
|
+
case 'workflow.run.started':
|
|
224
|
+
console.log('Workflow started');
|
|
225
|
+
break;
|
|
226
|
+
case 'workflow.node.started':
|
|
227
|
+
console.log(`Node ${event.node_key} running...`);
|
|
228
|
+
break;
|
|
229
|
+
case 'workflow.node.progress':
|
|
230
|
+
console.log(`Progress: ${JSON.stringify(event.payload)}`);
|
|
231
|
+
break;
|
|
232
|
+
case 'workflow.node.completed':
|
|
233
|
+
console.log(`Node ${event.node_key} done`);
|
|
234
|
+
break;
|
|
235
|
+
case 'workflow.run.completed':
|
|
236
|
+
console.log('Workflow finished!');
|
|
237
|
+
break;
|
|
238
|
+
case 'workflow.run.failed':
|
|
239
|
+
console.error(`Failed: ${event.payload?.error}`);
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
onError(err) {
|
|
244
|
+
console.error('Stream error:', err);
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Cancel the stream
|
|
249
|
+
stream.abort();
|
|
250
|
+
|
|
251
|
+
// Wait for the stream to finish
|
|
252
|
+
await stream.done;
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Features:
|
|
256
|
+
- Replays existing events on connect (no missed events)
|
|
257
|
+
- Auto-reconnects via `Last-Event-ID` header
|
|
258
|
+
- Auto-closes on terminal events (`completed`, `failed`, `cancelled`)
|
|
259
|
+
- Works in Node.js, browsers, and Edge Runtime
|
|
260
|
+
|
|
261
|
+
### Stream all events
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
const stream = fabric.events.stream({
|
|
265
|
+
onEvent(event) { console.log(event.kind); },
|
|
266
|
+
});
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### WebSocket (for dashboards)
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
const ws = fabric.events.connectWebSocket({
|
|
273
|
+
onEvent(event) { console.log(event); },
|
|
274
|
+
onError(err) { console.error(err); },
|
|
275
|
+
onClose() { console.log('Disconnected'); },
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
ws.subscribeToRun('run-uuid'); // Filter to a specific run
|
|
279
|
+
ws.unsubscribe(); // Receive all events
|
|
280
|
+
ws.close(); // Disconnect
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Built-in event logger
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
import { logEvents } from '@fabric-platform/sdk';
|
|
287
|
+
|
|
288
|
+
// Pretty-prints events to the console with timing
|
|
289
|
+
fabric.workflows.runs.streamEvents(run.id, { onEvent: logEvents });
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### React Component Example
|
|
293
|
+
|
|
294
|
+
```tsx
|
|
295
|
+
'use client';
|
|
296
|
+
import { useEffect, useState } from 'react';
|
|
297
|
+
import { FabricClient, type DomainEvent } from '@fabric-platform/sdk';
|
|
298
|
+
|
|
299
|
+
function WorkflowProgress({ runId, token }: { runId: string; token: string }) {
|
|
300
|
+
const [events, setEvents] = useState<DomainEvent[]>([]);
|
|
301
|
+
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
const fabric = new FabricClient({
|
|
304
|
+
baseUrl: process.env.NEXT_PUBLIC_FABRIC_URL,
|
|
305
|
+
auth: { type: 'bearer', token },
|
|
306
|
+
});
|
|
307
|
+
const stream = fabric.workflows.runs.streamEvents(runId, {
|
|
308
|
+
onEvent(event) {
|
|
309
|
+
setEvents(prev => [...prev, event]);
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
return () => stream.abort(); // Cleanup on unmount
|
|
313
|
+
}, [runId, token]);
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<ul>
|
|
317
|
+
{events.map((e) => (
|
|
318
|
+
<li key={e.id}>{e.kind}: {e.node_key}</li>
|
|
319
|
+
))}
|
|
320
|
+
</ul>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Node Logs
|
|
326
|
+
|
|
327
|
+
Fetch stdout/stderr excerpts for completed workflow nodes:
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
const attempts = await fabric.workflows.runs.getNodeAttempts(run.id, 'my-node-key');
|
|
331
|
+
for (const attempt of attempts) {
|
|
332
|
+
console.log('stdout:', attempt.stdout_excerpt);
|
|
333
|
+
console.log('stderr:', attempt.stderr_excerpt);
|
|
334
|
+
console.log('duration:', attempt.duration_ms, 'ms');
|
|
335
|
+
}
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Organizations
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
const org = await fabric.organizations.create({ name: 'Acme Corp' });
|
|
342
|
+
const { items: orgs } = await fabric.organizations.list({ limit: 20 });
|
|
343
|
+
const details = await fabric.organizations.get(org.id);
|
|
344
|
+
|
|
345
|
+
// Members and teams
|
|
346
|
+
const { items: members } = await fabric.organizations.members(org.id);
|
|
347
|
+
const { items: teams } = await fabric.organizations.teams(org.id);
|
|
348
|
+
|
|
349
|
+
// Usage and audit
|
|
350
|
+
const usage = await fabric.organizations.usage(org.id);
|
|
351
|
+
const { items: logs } = await fabric.organizations.auditLogs(org.id);
|
|
352
|
+
|
|
353
|
+
// Budget
|
|
354
|
+
const budget = await fabric.organizations.getBudget(org.id);
|
|
355
|
+
await fabric.organizations.setBudget(org.id, 500.00);
|
|
356
|
+
|
|
357
|
+
// Secrets
|
|
358
|
+
await fabric.organizations.createSecret(org.id, { name: 'OPENAI_KEY', value: 'sk-...' });
|
|
359
|
+
const secrets = await fabric.organizations.listSecrets(org.id);
|
|
360
|
+
await fabric.organizations.deleteSecret(org.id, 'OPENAI_KEY');
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Teams
|
|
364
|
+
|
|
365
|
+
```typescript
|
|
366
|
+
const team = await fabric.teams.create({
|
|
367
|
+
organization_id: org.id,
|
|
368
|
+
name: 'Engineering',
|
|
369
|
+
});
|
|
370
|
+
const details = await fabric.teams.get(team.id);
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## Assets
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
// Upload a file
|
|
377
|
+
const asset = await fabric.assets.upload(fileBlob, {
|
|
378
|
+
contentType: 'image/png',
|
|
379
|
+
filename: 'avatar.png',
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// List assets
|
|
383
|
+
const { items: assets } = await fabric.assets.list();
|
|
384
|
+
|
|
385
|
+
// Get a signed download URL
|
|
386
|
+
const { url } = await fabric.assets.signedUrl(asset.id, 3600); // 1 hour expiry
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Galleries
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
const gallery = await fabric.galleries.create({
|
|
393
|
+
name: 'Portraits',
|
|
394
|
+
kind: 'portrait',
|
|
395
|
+
tags: ['professional'],
|
|
396
|
+
});
|
|
397
|
+
const { items: galleries } = await fabric.galleries.list();
|
|
398
|
+
|
|
399
|
+
// Add/list/remove items
|
|
400
|
+
const item = await fabric.galleries.addItem(gallery.id, {
|
|
401
|
+
asset_id: 'asset-uuid',
|
|
402
|
+
label: 'Professional Woman #1',
|
|
403
|
+
tags: ['female', 'business'],
|
|
404
|
+
});
|
|
405
|
+
const { items } = await fabric.galleries.listItems(gallery.id);
|
|
406
|
+
await fabric.galleries.removeItem(item.id);
|
|
407
|
+
await fabric.galleries.delete(gallery.id);
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## API Keys
|
|
411
|
+
|
|
412
|
+
```typescript
|
|
413
|
+
const { id, secret } = await fabric.apiKeys.create({
|
|
414
|
+
description: 'CI/CD key',
|
|
415
|
+
scopes: ['workflows:execute', 'assets:read'],
|
|
416
|
+
});
|
|
417
|
+
// `secret` is only returned at creation time
|
|
418
|
+
|
|
419
|
+
const { items: keys } = await fabric.apiKeys.list();
|
|
420
|
+
const key = await fabric.apiKeys.get(id);
|
|
421
|
+
|
|
422
|
+
await fabric.apiKeys.disable(id);
|
|
423
|
+
const rotated = await fabric.apiKeys.rotate(id); // new secret issued
|
|
424
|
+
await fabric.apiKeys.delete(id);
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Invitations
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
const invite = await fabric.invitations.create({
|
|
431
|
+
organization_id: org.id,
|
|
432
|
+
email: 'colleague@example.com',
|
|
433
|
+
role: 'member',
|
|
434
|
+
});
|
|
435
|
+
await fabric.invitations.accept(invite.id);
|
|
436
|
+
await fabric.invitations.revoke(invite.id);
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Permissions
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
// Check a single permission
|
|
443
|
+
const { allowed } = await fabric.permissions.check({
|
|
444
|
+
resource: `organization:${org.id}`,
|
|
445
|
+
action: 'update',
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// Batch check
|
|
449
|
+
const results = await fabric.permissions.checkBatch([
|
|
450
|
+
{ resource: `organization:${org.id}`, action: 'read' },
|
|
451
|
+
{ resource: `organization:${org.id}`, action: 'delete' },
|
|
452
|
+
]);
|
|
453
|
+
|
|
454
|
+
// Grant / revoke
|
|
455
|
+
const perm = await fabric.permissions.grant({
|
|
456
|
+
principal_id: 'user-uuid',
|
|
457
|
+
resource_type: 'organization',
|
|
458
|
+
resource_id: org.id,
|
|
459
|
+
action: 'update',
|
|
460
|
+
});
|
|
461
|
+
await fabric.permissions.revoke(perm.id);
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
## Webhooks
|
|
465
|
+
|
|
466
|
+
```typescript
|
|
467
|
+
const webhook = await fabric.webhooks.create(org.id, {
|
|
468
|
+
url: 'https://example.com/webhook',
|
|
469
|
+
event_filter: ['workflow.run.completed', 'workflow.run.failed'],
|
|
470
|
+
});
|
|
471
|
+
const { items: webhooks } = await fabric.webhooks.list(org.id);
|
|
472
|
+
await fabric.webhooks.update(webhook.id, { active: false });
|
|
473
|
+
const { items: deliveries } = await fabric.webhooks.deliveries(webhook.id);
|
|
474
|
+
await fabric.webhooks.delete(webhook.id);
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
## Service Accounts
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
const sa = await fabric.serviceAccounts.create({ name: 'ci-bot' });
|
|
481
|
+
const { items: accounts } = await fabric.serviceAccounts.list();
|
|
482
|
+
|
|
483
|
+
const apiKey = await fabric.serviceAccounts.createApiKey(sa.id);
|
|
484
|
+
const keys = await fabric.serviceAccounts.listApiKeys(sa.id);
|
|
485
|
+
await fabric.serviceAccounts.rotateApiKey(sa.id, apiKey.id);
|
|
486
|
+
await fabric.serviceAccounts.revokeApiKey(sa.id, apiKey.id);
|
|
487
|
+
|
|
488
|
+
await fabric.serviceAccounts.disable(sa.id);
|
|
489
|
+
await fabric.serviceAccounts.enable(sa.id);
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
## OAuth Clients
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
const client = await fabric.oauth.createClient({ client_name: 'My App' });
|
|
496
|
+
const clients = await fabric.oauth.listClients();
|
|
497
|
+
|
|
498
|
+
const tokens = await fabric.oauth.token(client.client_id, 'client-secret');
|
|
499
|
+
const refreshed = await fabric.oauth.refresh(tokens.refresh_token);
|
|
500
|
+
await fabric.oauth.revoke(tokens.access_token);
|
|
501
|
+
|
|
502
|
+
await fabric.oauth.deleteClient(client.id);
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
## Current User
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
const me = await fabric.me.get();
|
|
509
|
+
const myOrgs = await fabric.me.organizations();
|
|
510
|
+
const myTeams = await fabric.me.teams();
|
|
511
|
+
const myPerms = await fabric.me.permissions();
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
## Admin
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
// Dev bootstrap (creates initial org + admin)
|
|
518
|
+
await fabric.admin.bootstrap();
|
|
519
|
+
|
|
520
|
+
// Concurrency limits
|
|
521
|
+
const limits = await fabric.admin.listConcurrencyLimits();
|
|
522
|
+
await fabric.admin.setConcurrencyLimit('org:uuid', 10);
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
## Pagination
|
|
526
|
+
|
|
527
|
+
All list endpoints return `PaginatedResponse<T>`:
|
|
528
|
+
|
|
529
|
+
```typescript
|
|
530
|
+
const { items, pagination } = await fabric.organizations.list({ limit: 20 });
|
|
531
|
+
// pagination: { count: 20, has_more: true, next_cursor: "20" }
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
### Auto-pagination
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
import { paginate, paginateAll } from '@fabric-platform/sdk';
|
|
538
|
+
|
|
539
|
+
// Async iterator — page by page
|
|
540
|
+
for await (const page of paginate((params) => fabric.organizations.list(params))) {
|
|
541
|
+
for (const org of page) {
|
|
542
|
+
console.log(org.name);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Collect everything into one array
|
|
547
|
+
const allOrgs = await paginateAll((params) => fabric.organizations.list(params));
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
## Error Handling
|
|
551
|
+
|
|
552
|
+
All errors are typed with status code, error code, request ID, and trace ID:
|
|
553
|
+
|
|
554
|
+
```typescript
|
|
555
|
+
import {
|
|
556
|
+
FabricError,
|
|
557
|
+
FabricAuthError,
|
|
558
|
+
FabricNotFoundError,
|
|
559
|
+
FabricConflictError,
|
|
560
|
+
FabricRateLimitError,
|
|
561
|
+
} from '@fabric-platform/sdk';
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
await fabric.organizations.create({ name: 'Acme' });
|
|
565
|
+
} catch (err) {
|
|
566
|
+
if (err instanceof FabricConflictError) {
|
|
567
|
+
console.log('Slug already taken');
|
|
568
|
+
} else if (err instanceof FabricAuthError) {
|
|
569
|
+
console.log('Re-authenticate');
|
|
570
|
+
} else if (err instanceof FabricRateLimitError) {
|
|
571
|
+
console.log(`Retry after ${err.retryAfterMs}ms`);
|
|
572
|
+
} else if (err instanceof FabricNotFoundError) {
|
|
573
|
+
console.log('Not found');
|
|
574
|
+
} else if (err instanceof FabricError) {
|
|
575
|
+
console.log(`${err.code}: ${err.message} (request: ${err.requestId})`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
Error hierarchy:
|
|
581
|
+
|
|
582
|
+
| Class | Status |
|
|
583
|
+
|-------|--------|
|
|
584
|
+
| `FabricValidationError` | 400, 422 |
|
|
585
|
+
| `FabricAuthError` | 401 |
|
|
586
|
+
| `FabricForbiddenError` | 403 |
|
|
587
|
+
| `FabricNotFoundError` | 404 |
|
|
588
|
+
| `FabricConflictError` | 409 |
|
|
589
|
+
| `FabricRateLimitError` | 429 |
|
|
590
|
+
| `FabricServerError` | 5xx |
|
|
591
|
+
|
|
592
|
+
All extend `FabricError` which extends `Error`.
|
|
593
|
+
|
|
594
|
+
## Event Types
|
|
595
|
+
|
|
596
|
+
```typescript
|
|
597
|
+
type EventKind =
|
|
598
|
+
// Run lifecycle
|
|
599
|
+
| 'workflow.run.created' | 'workflow.run.queued' | 'workflow.run.promoted'
|
|
600
|
+
| 'workflow.run.started' | 'workflow.run.completed'
|
|
601
|
+
| 'workflow.run.failed' | 'workflow.run.cancelled'
|
|
602
|
+
// Node lifecycle
|
|
603
|
+
| 'workflow.node.ready' | 'workflow.node.claimed'
|
|
604
|
+
| 'workflow.node.started' | 'workflow.node.progress'
|
|
605
|
+
| 'workflow.node.completed'| 'workflow.node.failed'
|
|
606
|
+
| 'workflow.node.retried' | 'workflow.node.skipped'
|
|
607
|
+
| 'workflow.node.cancelled'| 'workflow.node.waiting_for_event'
|
|
608
|
+
| 'workflow.node.resumed';
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
## Next.js Integration Patterns
|
|
612
|
+
|
|
613
|
+
### Server-side client factory
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
// lib/fabric.ts
|
|
617
|
+
import { cookies } from 'next/headers';
|
|
618
|
+
import { FabricClient, FabricAuthError } from '@fabric-platform/sdk';
|
|
619
|
+
|
|
620
|
+
export function getFabricClient() {
|
|
621
|
+
return new FabricClient({
|
|
622
|
+
baseUrl: process.env.FABRIC_URL,
|
|
623
|
+
auth: async () => {
|
|
624
|
+
const token = cookies().get('fabric_access_token')?.value;
|
|
625
|
+
if (!token) throw new FabricAuthError({
|
|
626
|
+
code: 'no_session', status: 401, message: 'Not authenticated',
|
|
627
|
+
});
|
|
628
|
+
return token;
|
|
629
|
+
},
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
### Login API route
|
|
635
|
+
|
|
636
|
+
```typescript
|
|
637
|
+
// app/api/auth/login/route.ts
|
|
638
|
+
import { FabricClient } from '@fabric-platform/sdk';
|
|
639
|
+
import { cookies } from 'next/headers';
|
|
640
|
+
|
|
641
|
+
export async function POST(req: Request) {
|
|
642
|
+
const { email, password } = await req.json();
|
|
643
|
+
const fabric = new FabricClient({ baseUrl: process.env.FABRIC_URL });
|
|
644
|
+
const auth = await fabric.auth.login(email, password);
|
|
645
|
+
|
|
646
|
+
cookies().set('fabric_access_token', auth.access_token, {
|
|
647
|
+
httpOnly: true,
|
|
648
|
+
secure: true,
|
|
649
|
+
maxAge: auth.expires_in,
|
|
650
|
+
});
|
|
651
|
+
cookies().set('fabric_refresh_token', auth.refresh_token, {
|
|
652
|
+
httpOnly: true,
|
|
653
|
+
secure: true,
|
|
654
|
+
maxAge: 30 * 86400,
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
return Response.json({ user: auth.user });
|
|
658
|
+
}
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### SSE proxy route
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
// app/api/workflows/[runId]/events/route.ts
|
|
665
|
+
import { cookies } from 'next/headers';
|
|
666
|
+
|
|
667
|
+
export async function GET(req: Request, { params }: { params: { runId: string } }) {
|
|
668
|
+
const token = cookies().get('fabric_access_token')?.value;
|
|
669
|
+
const upstream = await fetch(
|
|
670
|
+
`${process.env.FABRIC_URL}/v1/workflow-runs/${params.runId}/events`,
|
|
671
|
+
{ headers: { Authorization: `Bearer ${token}`, Accept: 'text/event-stream' } },
|
|
672
|
+
);
|
|
673
|
+
return new Response(upstream.body, {
|
|
674
|
+
headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' },
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
## Development
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
npm run build # Build with tsup (ESM + CJS + declarations)
|
|
683
|
+
npm run typecheck # Type-check without emitting
|
|
684
|
+
npm test # Run tests with vitest
|
|
685
|
+
npm run dev # Watch mode
|
|
686
|
+
```
|