@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 CHANGED
@@ -1,5 +1,317 @@
1
- # tuvl
1
+ # @tuvl/client
2
2
 
3
- This package name has been reserved for the upcoming **TUVL Framework** – a lightweight, developer-friendly orchestration engine for CRUD APIs and LLM agents.
3
+ TypeScript client SDK for the [tuvl](https://tuvl.io) workflow orchestration engine.
4
4
 
5
- More information coming soon! Contact: sooraj@tuvl.io
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