@tuvl/client 0.0.1 → 2026.2.1-beta.1
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 +315 -3
- package/dist/index.d.mts +426 -0
- package/dist/index.d.ts +426 -0
- package/dist/index.js +697 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +689 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +60 -12
- package/index.js +0 -2
package/README.md
CHANGED
|
@@ -1,5 +1,317 @@
|
|
|
1
|
-
# tuvl
|
|
1
|
+
# @tuvl/client
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
TypeScript client SDK for the [tuvl](https://tuvl.io) workflow orchestration engine.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Supports three transports — plain REST, Server-Sent Events, and gRPC-Web — with automatic transport selection based on the workflow's streaming hints. Also ships first-class support for Human-in-the-Loop (HITL) resumption and explicit version pinning.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install @tuvl/client
|
|
13
|
+
# or
|
|
14
|
+
pnpm add @tuvl/client
|
|
15
|
+
# or
|
|
16
|
+
yarn add @tuvl/client
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**gRPC-Web** is optional. Install the peer dependencies only if you need `mode: "grpc"`:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @protobuf-ts/grpcweb-transport @protobuf-ts/runtime-rpc
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { TuvlAuth, TuvlClient } from "@tuvl/client";
|
|
31
|
+
|
|
32
|
+
// 1. Login
|
|
33
|
+
const auth = new TuvlAuth({ baseUrl: "http://localhost:8000" });
|
|
34
|
+
const { access_token } = await auth.loginWithPassword("me@example.com", "secret");
|
|
35
|
+
|
|
36
|
+
// 2. Who am I? (roles + scopes without touching Biscuit internals)
|
|
37
|
+
const me = await auth.getMe(access_token);
|
|
38
|
+
console.log(me.groups); // ["hr_manager"]
|
|
39
|
+
console.log(me.scopes); // ["candidate:read", "requisition:write"]
|
|
40
|
+
|
|
41
|
+
// 3. Run a workflow
|
|
42
|
+
const client = new TuvlClient({ baseUrl: "http://localhost:8000", token: access_token });
|
|
43
|
+
|
|
44
|
+
// Plain REST call
|
|
45
|
+
const result = await client.execute("hello", {
|
|
46
|
+
payload: { message: "world" },
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// SSE streaming — auto-detected when onProgress is provided
|
|
50
|
+
// and the workflow has slow steps (agent / mcp / api_call)
|
|
51
|
+
const result2 = await client.execute("screen-candidate", {
|
|
52
|
+
payload: { candidate_id: 42 },
|
|
53
|
+
onProgress: (ev) => console.log(`[${ev.step_id}] ${ev.duration_ms}ms`),
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Authentication
|
|
60
|
+
|
|
61
|
+
The `TuvlAuth` class wraps the most common `/auth/*` endpoints — `bootstrap`, `loginWithPassword`, `getOAuthLoginUrl`, `getMe`, `refresh`, `logout` — so you don't have to hand-craft form bodies or Biscuit decoding logic. Admin endpoints (user / role / scope CRUD, federation config) are intentionally not wrapped; call them directly with `fetch` or the lower-level `Transport` using the same Bearer token.
|
|
62
|
+
|
|
63
|
+
### `new TuvlAuth(options)`
|
|
64
|
+
|
|
65
|
+
| Option | Type | Description |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `baseUrl` | `string` | tuvl server URL |
|
|
68
|
+
|
|
69
|
+
### Password login
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const auth = new TuvlAuth({ baseUrl: "http://localhost:8000" });
|
|
73
|
+
const { access_token } = await auth.loginWithPassword("me@example.com", "secret");
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Bootstrap (one-time IAM init)
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// First call ever: creates the superadmin user and returns a token.
|
|
80
|
+
// Every subsequent call rejects with HTTP 409.
|
|
81
|
+
const { access_token } = await auth.bootstrap({
|
|
82
|
+
email: "admin@example.com",
|
|
83
|
+
password: "change-me-now",
|
|
84
|
+
admin_scope: "iam:admin",
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### OAuth2 login (browser)
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// Redirect the browser to the provider's login page
|
|
92
|
+
window.location.href = auth.getOAuthLoginUrl("google"); // or "github" / "microsoft"
|
|
93
|
+
|
|
94
|
+
// On the landing page (TUVL_OAUTH_UI_REDIRECT_URL), pick up the token:
|
|
95
|
+
const token = new URLSearchParams(window.location.search).get("token")!;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Get current user identity and roles
|
|
99
|
+
|
|
100
|
+
Biscuit tokens are protobuf-encoded and cannot be decoded in pure JS. Use `getMe()` to ask the server to decode the token and return the user's identity, group memberships, and permission scopes:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const me = await auth.getMe(token);
|
|
104
|
+
// me.user_id — user UUID
|
|
105
|
+
// me.groups — role names e.g. ["hr_manager", "member"]
|
|
106
|
+
// me.scopes — permission scopes e.g. ["candidate:read"]
|
|
107
|
+
|
|
108
|
+
if (me.groups.includes("hr_manager")) {
|
|
109
|
+
// show HR features
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (me.scopes.includes("iam:admin")) {
|
|
113
|
+
// show admin panel
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Token refresh
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const { access_token: newToken } = await auth.refresh(currentToken);
|
|
121
|
+
client.setToken(newToken); // update the workflow client in-place
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Logout
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
await auth.logout(token);
|
|
128
|
+
// delete token from localStorage / state after this
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Transport selection
|
|
134
|
+
|
|
135
|
+
`execute()` picks the transport automatically — you never need to branch on it:
|
|
136
|
+
|
|
137
|
+
| Condition | Transport |
|
|
138
|
+
|---|---|
|
|
139
|
+
| `mode: "rest"` | Plain JSON POST |
|
|
140
|
+
| `mode: "sse"` | SSE stream |
|
|
141
|
+
| `mode: "grpc"` | gRPC-Web stream |
|
|
142
|
+
| `onProgress` provided **and** workflow `has_slow_steps` | SSE (auto) |
|
|
143
|
+
| Default | Plain JSON POST |
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## API
|
|
148
|
+
|
|
149
|
+
### `new TuvlClient(options)`
|
|
150
|
+
|
|
151
|
+
| Option | Type | Default | Description |
|
|
152
|
+
|---|---|---|---|
|
|
153
|
+
| `baseUrl` | `string` | — | tuvl server URL (trailing slash stripped) |
|
|
154
|
+
| `token` | `string` | — | Biscuit Bearer token for all requests |
|
|
155
|
+
| `manifestCacheTtl` | `number` | `60000` | Manifest cache TTL in ms. `0` disables caching |
|
|
156
|
+
|
|
157
|
+
### `client.execute(workflowName, options?)`
|
|
158
|
+
|
|
159
|
+
Executes a workflow and returns `Promise<TOutput>` — **the unwrapped `data` payload, identical across REST / SSE / gRPC**.
|
|
160
|
+
|
|
161
|
+
| Option | Type | Description |
|
|
162
|
+
|---|---|---|
|
|
163
|
+
| `payload` | `Record<string, unknown>` | JSON body matching the workflow's `input_schema` |
|
|
164
|
+
| `onProgress` | `(event: StepEvent) => void` | Called for every step event (SSE / gRPC only) |
|
|
165
|
+
| `onSuspended` | `(event: SuspendedEvent) => void` | Called once when the workflow hits a HITL step. After this fires the promise rejects with `TuvlWorkflowSuspendedError` |
|
|
166
|
+
| `mode` | `"rest" \| "sse" \| "grpc"` | Force a specific transport |
|
|
167
|
+
| `token` | `string` | Per-call token override |
|
|
168
|
+
| `signal` | `AbortSignal` | Cancellation signal |
|
|
169
|
+
|
|
170
|
+
#### Return-value contract
|
|
171
|
+
|
|
172
|
+
| Outcome | `execute()` does |
|
|
173
|
+
|---|---|
|
|
174
|
+
| Workflow returns `success: true` | Resolves with `data` |
|
|
175
|
+
| Workflow returns `success: false` | Rejects with `TuvlWorkflowError` (carries `.data` and `.error`) |
|
|
176
|
+
| Workflow suspends at a HITL step | Rejects with `TuvlWorkflowSuspendedError` (carries `.suspended`) |
|
|
177
|
+
| Engine raises an unhandled exception | Rejects with a plain `Error` |
|
|
178
|
+
| HTTP / network failure | Rejects with a plain `Error` |
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import { TuvlClient, TuvlWorkflowError, TuvlWorkflowSuspendedError } from "@tuvl/client";
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const candidate = await client.execute<Candidate>("screen-candidate", {
|
|
185
|
+
payload: { id: 42 },
|
|
186
|
+
onProgress: (ev) => console.log(ev.step_id, ev.duration_ms),
|
|
187
|
+
onSuspended: (s) => renderHitlForm(s),
|
|
188
|
+
});
|
|
189
|
+
// candidate is the unwrapped data, fully typed
|
|
190
|
+
} catch (err) {
|
|
191
|
+
if (err instanceof TuvlWorkflowSuspendedError) {
|
|
192
|
+
// err.suspended.ui, err.suspended.schema, err.suspended.instance_id …
|
|
193
|
+
} else if (err instanceof TuvlWorkflowError) {
|
|
194
|
+
// err.data is the partial snapshot, err.error is the structured failure
|
|
195
|
+
} else {
|
|
196
|
+
// transport / engine failure
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### `client.executeVersioned(workflowName, version, options?)`
|
|
202
|
+
|
|
203
|
+
Runs a workflow pinned to a specific schema version (`/{version}/run/{name}`) without relying on the server's active default trigger path. Accepts the same options as `execute()`.
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
// Always run the v2 contract of "screen-candidate", even if v3 is the default
|
|
207
|
+
const result = await client.executeVersioned("screen-candidate", "v2", {
|
|
208
|
+
payload: { id: 42 },
|
|
209
|
+
onProgress: (ev) => console.log(ev.step_id),
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### `client.resumeWorkflow(options)`
|
|
214
|
+
|
|
215
|
+
Resumes a workflow instance that was suspended at a Human-in-the-Loop (HITL) step.
|
|
216
|
+
|
|
217
|
+
| Option | Type | Description |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| `instanceId` | `string` | UUID from `SuspendedEvent.instance_id` |
|
|
220
|
+
| `humanInput` | `Record<string, unknown>` | Operator-supplied data to inject into the paused step |
|
|
221
|
+
| `onProgress` | `(event: StepEvent) => void` | Step events after the resume point (SSE mode) |
|
|
222
|
+
| `onSuspended` | `(event: SuspendedEvent) => void` | Called if the workflow suspends again at another HITL step |
|
|
223
|
+
| `mode` | `"rest" \| "sse"` | Force a specific transport (default: REST, SSE when `onProgress` is provided) |
|
|
224
|
+
| `token` | `string` | Per-call token override |
|
|
225
|
+
| `signal` | `AbortSignal` | Cancellation signal |
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import {
|
|
229
|
+
TuvlClient,
|
|
230
|
+
TuvlWorkflowSuspendedError,
|
|
231
|
+
type SuspendedEvent,
|
|
232
|
+
} from "@tuvl/client";
|
|
233
|
+
|
|
234
|
+
let suspendedEvent: SuspendedEvent | undefined;
|
|
235
|
+
|
|
236
|
+
// First pass — workflow suspends at an approval step
|
|
237
|
+
try {
|
|
238
|
+
await client.execute("hire-candidate", {
|
|
239
|
+
payload: { candidate_id: 99 },
|
|
240
|
+
onSuspended: (s) => { suspendedEvent = s; },
|
|
241
|
+
});
|
|
242
|
+
} catch (err) {
|
|
243
|
+
if (!(err instanceof TuvlWorkflowSuspendedError)) throw err;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Human fills in the form …
|
|
247
|
+
const decision = { approved: true, note: "Strong fit" };
|
|
248
|
+
|
|
249
|
+
// Second pass — resume from where it paused
|
|
250
|
+
const finalResult = await client.resumeWorkflow({
|
|
251
|
+
instanceId: suspendedEvent!.instance_id,
|
|
252
|
+
humanInput: decision,
|
|
253
|
+
onProgress: (ev) => console.log(`[resumed] ${ev.step_id}`),
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### `client.getManifest(workflowName)`
|
|
258
|
+
|
|
259
|
+
Fetches (and caches) the workflow manifest from `/api/_system/workflows/{name}`. Called automatically by `execute()` and `executeVersioned()`.
|
|
260
|
+
|
|
261
|
+
### `client.listWorkflows()`
|
|
262
|
+
|
|
263
|
+
Returns all registered workflows with their trigger paths and streaming hints.
|
|
264
|
+
|
|
265
|
+
### `client.setToken(token)`
|
|
266
|
+
|
|
267
|
+
Updates the default auth token without recreating the client.
|
|
268
|
+
|
|
269
|
+
### `client.invalidateManifest(name?)`
|
|
270
|
+
|
|
271
|
+
Clears the manifest cache for one workflow, or all if no name is given.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## TypeScript types
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import type {
|
|
279
|
+
// Auth
|
|
280
|
+
TokenResponse, // { access_token, token_type }
|
|
281
|
+
MeResponse, // { user_id, groups, scopes }
|
|
282
|
+
// Workflow execution
|
|
283
|
+
StepEvent, // single step update (event_type: "step")
|
|
284
|
+
DoneEvent, // terminal envelope { event_type:"done", success, data, error }
|
|
285
|
+
ErrorEvent, // engine-level error { event_type:"error", message, details }
|
|
286
|
+
SuspendedEvent, // HITL suspension { event_type:"suspended", instance_id, ui, schema, … }
|
|
287
|
+
RestEnvelope, // shape of the plain REST response body
|
|
288
|
+
WorkflowErrorPayload, // shape of the .error field on failures
|
|
289
|
+
WorkflowManifest, // shape of /api/_system/workflows/{name}
|
|
290
|
+
ExecuteOptions, // options for execute()
|
|
291
|
+
ExecuteVersionedOptions, // options for executeVersioned() — extends ExecuteOptions + version
|
|
292
|
+
ResumeOptions, // options for resumeWorkflow()
|
|
293
|
+
ResumeRequest, // raw body sent to POST /api/workflows/resume
|
|
294
|
+
TuvlClientOptions, // constructor options
|
|
295
|
+
} from "@tuvl/client";
|
|
296
|
+
|
|
297
|
+
// Error classes are real classes — usable with `instanceof`
|
|
298
|
+
import { TuvlWorkflowError, TuvlWorkflowSuspendedError } from "@tuvl/client";
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
## Lower-level exports
|
|
304
|
+
|
|
305
|
+
The package also exports its internal building blocks for advanced use:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
import { Transport } from "@tuvl/client"; // raw HTTP layer
|
|
309
|
+
import { parseSseStream } from "@tuvl/client"; // SSE frame parser (async generator)
|
|
310
|
+
import { openGrpcStream } from "@tuvl/client"; // gRPC-Web stream (async generator)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
315
|
+
## License
|
|
316
|
+
|
|
317
|
+
MIT
|