@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.
@@ -0,0 +1,426 @@
1
+ /**
2
+ * Shared types for @tuvl/client.
3
+ *
4
+ * Mirrors the Python-side StepEvent dataclass, the SSE envelope produced by
5
+ * tuvl.core.engine.streaming, and the system_router manifest shapes.
6
+ */
7
+ /** Returned by all /auth/* login and refresh endpoints. */
8
+ interface TokenResponse {
9
+ access_token: string;
10
+ token_type: string;
11
+ }
12
+ /**
13
+ * Decoded identity and permissions for the currently authenticated user.
14
+ * Returned by GET /auth/me.
15
+ */
16
+ interface MeResponse {
17
+ /** User UUID extracted from the `user()` Datalog fact in the token. */
18
+ user_id: string;
19
+ /** Role names from all `group()` facts — e.g. ["hr_manager", "member"]. */
20
+ groups: string[];
21
+ /** Permission scopes from all `scope()` facts — e.g. ["candidate:read"]. */
22
+ scopes: string[];
23
+ }
24
+ /** A single execution event streamed by the tuvl server (SSE or gRPC). */
25
+ interface StepEvent {
26
+ event_type: "step";
27
+ step_id: string;
28
+ kind: string;
29
+ signal: string;
30
+ snapshot: Record<string, unknown>;
31
+ duration_ms: number;
32
+ error_detail?: string | null;
33
+ }
34
+ /**
35
+ * Terminal event emitted when the workflow finishes (success OR business failure).
36
+ * Wire shape from tuvl.core.api.manager._sse_generator:
37
+ * {success: true, data: <output>, error: null}
38
+ * {success: false, data: <partial>, error: {...workflow error...}}
39
+ */
40
+ interface DoneEvent {
41
+ event_type: "done";
42
+ success: boolean;
43
+ data: unknown;
44
+ error: WorkflowErrorPayload | null;
45
+ }
46
+ /** Terminal event emitted when the workflow engine itself raised an exception. */
47
+ interface ErrorEvent {
48
+ event_type: "error";
49
+ message: string;
50
+ details?: string | null;
51
+ }
52
+ /** Terminal event emitted when a workflow hits a Human-in-the-Loop step. */
53
+ interface SuspendedEvent {
54
+ event_type: "suspended";
55
+ instance_id?: string;
56
+ paused_step_id?: string;
57
+ ui?: unknown;
58
+ schema?: unknown;
59
+ context_data?: Record<string, unknown>;
60
+ output_key?: string;
61
+ [key: string]: unknown;
62
+ }
63
+ /** Shape of the `error` field on a non-success DoneEvent / REST envelope. */
64
+ interface WorkflowErrorPayload {
65
+ code?: string;
66
+ message?: string;
67
+ details?: unknown;
68
+ [key: string]: unknown;
69
+ }
70
+ /**
71
+ * Shape of the plain REST response body returned by
72
+ * `POST <workflow_trigger_path>` (and the versioned variant).
73
+ *
74
+ * `execute()` unwraps this and returns just `data` on success, throwing
75
+ * `TuvlWorkflowError` on `success === false`.
76
+ */
77
+ interface RestEnvelope<TData = unknown> {
78
+ success: boolean;
79
+ status_code: number;
80
+ data: TData | null;
81
+ error: WorkflowErrorPayload | null;
82
+ }
83
+ /** Manifest for a single workflow (from GET /api/_system/workflows/{name}). */
84
+ interface WorkflowManifest {
85
+ name: string;
86
+ trigger_path: string;
87
+ trigger_method: string;
88
+ has_slow_steps: boolean;
89
+ slow_kinds_present: string[];
90
+ required_scope: string | null;
91
+ required_group: string | null;
92
+ steps: Array<{
93
+ id: string;
94
+ kind: string;
95
+ }>;
96
+ }
97
+ /** Map of workflow manifests (from GET /api/_system/workflows). */
98
+ type WorkflowManifestMap = Record<string, Omit<WorkflowManifest, "name" | "steps">>;
99
+ /** Options passed to TuvlClient.execute(). */
100
+ interface ExecuteOptions<_TOutput = unknown> {
101
+ payload?: Record<string, unknown>;
102
+ onProgress?: (event: StepEvent) => void;
103
+ onSuspended?: (event: SuspendedEvent) => void;
104
+ mode?: "sse" | "grpc" | "rest";
105
+ token?: string;
106
+ signal?: AbortSignal;
107
+ }
108
+ /** Options passed to TuvlClient.executeVersioned(). */
109
+ interface ExecuteVersionedOptions<_TOutput = unknown> extends ExecuteOptions<_TOutput> {
110
+ /** The workflow's schema_version, e.g. "v2". Calls /{version}/run/{name}. */
111
+ version: string;
112
+ }
113
+ /** Body sent to POST /api/workflows/resume. */
114
+ interface ResumeRequest {
115
+ instance_id: string;
116
+ human_input?: Record<string, unknown>;
117
+ }
118
+ /** Options passed to TuvlClient.resumeWorkflow(). */
119
+ interface ResumeOptions<_TOutput = unknown> {
120
+ instanceId: string;
121
+ humanInput?: Record<string, unknown>;
122
+ onProgress?: (event: StepEvent) => void;
123
+ onSuspended?: (event: SuspendedEvent) => void;
124
+ mode?: "sse" | "rest";
125
+ token?: string;
126
+ signal?: AbortSignal;
127
+ }
128
+ /** Options passed to TuvlClient constructor. */
129
+ interface TuvlClientOptions {
130
+ baseUrl: string;
131
+ token?: string;
132
+ manifestCacheTtl?: number;
133
+ }
134
+ /**
135
+ * Thrown by `execute()` when a workflow finishes with `success: false`.
136
+ */
137
+ declare class TuvlWorkflowError extends Error {
138
+ readonly data: unknown;
139
+ readonly error: WorkflowErrorPayload | null;
140
+ constructor(message: string, data: unknown, error: WorkflowErrorPayload | null);
141
+ }
142
+ /**
143
+ * Thrown by `execute()` when a workflow suspends at a Human-in-the-Loop step.
144
+ */
145
+ declare class TuvlWorkflowSuspendedError extends Error {
146
+ readonly suspended: SuspendedEvent;
147
+ constructor(suspended: SuspendedEvent);
148
+ }
149
+
150
+ /**
151
+ * TuvlClient — the main entry point for @tuvl/client.
152
+ *
153
+ * Auto-detects the best transport for each workflow call:
154
+ *
155
+ * mode="rest" (default) → plain POST, unwraps the REST envelope
156
+ * mode="sse" (auto) → SSE streaming when onProgress is provided
157
+ * AND the workflow has_slow_steps === true
158
+ * mode="grpc" → gRPC-Web via sonora (requires peer deps)
159
+ *
160
+ * Return-value contract — IDENTICAL across all three transports:
161
+ *
162
+ * • on success → resolves with the `data` value
163
+ * • on workflow-business error → rejects with `TuvlWorkflowError`
164
+ * • on HITL suspension → rejects with `TuvlWorkflowSuspendedError`
165
+ * • on transport / engine error → rejects with a plain `Error`
166
+ *
167
+ * Manifest caching avoids extra round-trips on repeated calls to the same
168
+ * workflow. TTL defaults to 60 s and is configurable.
169
+ */
170
+
171
+ declare class TuvlClient {
172
+ private readonly transport;
173
+ private readonly manifestCache;
174
+ private readonly manifestCacheTtl;
175
+ constructor(options: TuvlClientOptions);
176
+ /** Update the default auth token (e.g. after token refresh). */
177
+ setToken(token: string): void;
178
+ /** Expose the base URL for callers that need raw access (e.g. gRPC). */
179
+ get baseUrl(): string;
180
+ /** Fetch (and cache) the manifest for a single workflow. */
181
+ getManifest(workflowName: string, options?: {
182
+ token?: string;
183
+ signal?: AbortSignal;
184
+ }): Promise<WorkflowManifest>;
185
+ /** Fetch manifests for all registered workflows. */
186
+ listWorkflows(options?: {
187
+ token?: string;
188
+ signal?: AbortSignal;
189
+ }): Promise<WorkflowManifestMap>;
190
+ /** Invalidate the cached manifest for a workflow (or all if no name given). */
191
+ invalidateManifest(workflowName?: string): void;
192
+ /**
193
+ * Execute a workflow and return its `data` payload.
194
+ *
195
+ * Transport selection:
196
+ * 1. mode="grpc" → gRPC-Web (always, regardless of onProgress)
197
+ * 2. mode="sse" → SSE stream
198
+ * 3. onProgress provided → SSE if workflow has_slow_steps, else REST
199
+ * 4. default → REST
200
+ *
201
+ * @throws {TuvlWorkflowError} when the workflow returns success=false
202
+ * @throws {TuvlWorkflowSuspendedError} when the workflow suspends at HITL
203
+ * @throws {Error} on transport / engine failures
204
+ */
205
+ execute<TOutput = unknown>(workflowName: string, options?: ExecuteOptions<TOutput>): Promise<TOutput>;
206
+ /**
207
+ * Execute a specific version of a workflow via `/{version}/run/{name}`.
208
+ *
209
+ * Useful when you want to pin to a particular schema_version without
210
+ * relying on the currently-active default trigger path.
211
+ *
212
+ * @throws {TuvlWorkflowError} when the workflow returns success=false
213
+ * @throws {TuvlWorkflowSuspendedError} when the workflow suspends at HITL
214
+ * @throws {Error} on transport / engine failures
215
+ */
216
+ executeVersioned<TOutput = unknown>(workflowName: string, version: string, options?: ExecuteVersionedOptions<TOutput> | ExecuteOptions<TOutput>): Promise<TOutput>;
217
+ /**
218
+ * Resume a HITL-suspended workflow instance.
219
+ *
220
+ * After the workflow throws `TuvlWorkflowSuspendedError`, capture the
221
+ * `event.instance_id` from the suspended event and pass it here along with
222
+ * the human-provided data to continue execution.
223
+ *
224
+ * @throws {TuvlWorkflowError} when the resumed workflow returns success=false
225
+ * @throws {TuvlWorkflowSuspendedError} when the workflow suspends again at another HITL step
226
+ * @throws {Error} on transport / engine failures
227
+ */
228
+ resumeWorkflow<TOutput = unknown>(options: ResumeOptions<TOutput>): Promise<TOutput>;
229
+ private _executeSse;
230
+ private _executeGrpc;
231
+ private _unwrapRest;
232
+ }
233
+
234
+ /**
235
+ * TuvlAuth — authentication helpers for @tuvl/client.
236
+ *
237
+ * Wraps the most common /auth/* endpoints (login, refresh, logout, who-am-I,
238
+ * OAuth start, bootstrap) so consumers never have to manually construct
239
+ * form-encoded bodies or manage Bearer headers for auth operations.
240
+ *
241
+ * Admin endpoints (user/role/scope CRUD, federation config) are NOT wrapped
242
+ * — call them directly via `Transport` or `fetch` with the token.
243
+ */
244
+
245
+ interface TuvlAuthOptions {
246
+ /** Base URL of the tuvl server. Trailing slash is stripped automatically. */
247
+ baseUrl: string;
248
+ }
249
+ interface BootstrapRequest {
250
+ email: string;
251
+ password: string;
252
+ /** Optional extra scope to grant the superadmin role on creation. */
253
+ admin_scope?: string;
254
+ }
255
+ declare class TuvlAuth {
256
+ private readonly baseUrl;
257
+ constructor(options: TuvlAuthOptions);
258
+ /**
259
+ * Decode the current token server-side and return the user's identity,
260
+ * role memberships (`groups`), and permission scopes.
261
+ *
262
+ * Biscuit tokens are protobuf-encoded and cannot be decoded in pure JS,
263
+ * so this is the correct way to read "who is logged in" and "what can
264
+ * they do" from the TypeScript SDK.
265
+ *
266
+ * @throws if the token is invalid, expired, or revoked.
267
+ */
268
+ getMe(token: string): Promise<MeResponse>;
269
+ /**
270
+ * Exchange an email + password for a Biscuit bearer token.
271
+ *
272
+ * Calls `POST /auth/token` with an `application/x-www-form-urlencoded`
273
+ * body (OAuth2 password-grant). The `username` field must be the user's
274
+ * email address.
275
+ */
276
+ loginWithPassword(email: string, password: string): Promise<TokenResponse>;
277
+ /**
278
+ * Create the first superadmin user (one-time IAM bootstrap).
279
+ *
280
+ * Calls `POST /auth/bootstrap`. Returns 409 on every subsequent call
281
+ * once any user exists.
282
+ */
283
+ bootstrap(req: BootstrapRequest): Promise<TokenResponse>;
284
+ /**
285
+ * Return the URL the browser should navigate to in order to start an
286
+ * OAuth2 login flow for the given provider.
287
+ *
288
+ * Supported built-ins: `"google"`, `"github"`, `"microsoft"`. Any
289
+ * provider configured in the project's `federation/` directory also
290
+ * works.
291
+ *
292
+ * After the OAuth dance the server either:
293
+ * - redirects to `TUVL_OAUTH_UI_REDIRECT_URL?token=<biscuit_b64>`, OR
294
+ * - returns a JSON `{access_token, token_type}` body (CLI / server flows).
295
+ */
296
+ getOAuthLoginUrl(provider: string): string;
297
+ /**
298
+ * Exchange a valid (non-expired, non-revoked) token for a fresh one.
299
+ * The old token is immediately blacklisted — discard it after this call.
300
+ */
301
+ refresh(token: string): Promise<TokenResponse>;
302
+ /**
303
+ * Revoke the given token (logout). After this the token is blacklisted
304
+ * across all workers that share a Redis instance. Resolves silently on
305
+ * success (HTTP 204).
306
+ */
307
+ logout(token: string): Promise<void>;
308
+ }
309
+
310
+ /**
311
+ * SSE transport for @tuvl/client.
312
+ *
313
+ * Uses the Fetch API ReadableStream to parse SSE frames from a POST response
314
+ * opened by Transport.postStream(). This avoids the browser's native
315
+ * EventSource (which only supports GET) and works in Node 18+.
316
+ *
317
+ * Wire format produced by tuvl.core.engine.streaming:
318
+ *
319
+ * event: step data: {step_id, kind, signal, snapshot, duration_ms, error_detail}
320
+ * event: suspended data: {<hitl_request payload>}
321
+ * event: done data: {success, data, error}
322
+ * event: error data: {message, details}
323
+ *
324
+ * The JSON body has NO `event_type` field — it lives only on the `event:`
325
+ * line. This parser injects the event name as `event_type` into the yielded
326
+ * object so downstream consumers can switch on a single discriminator.
327
+ */
328
+
329
+ type SseFrame = StepEvent | DoneEvent | ErrorEvent | SuspendedEvent;
330
+ /**
331
+ * Parse an SSE stream from a Fetch Response body.
332
+ *
333
+ * Yields typed frames as they arrive. The generator terminates when it
334
+ * receives an event with event_type "done", "error", or "suspended", or
335
+ * when the stream closes.
336
+ */
337
+ declare function parseSseStream(response: Response, signal?: AbortSignal): AsyncGenerator<SseFrame>;
338
+
339
+ /**
340
+ * gRPC-Web transport for @tuvl/client.
341
+ *
342
+ * This module is an OPTIONAL transport that requires the peer dependencies:
343
+ * @protobuf-ts/grpcweb-transport
344
+ * @protobuf-ts/runtime-rpc
345
+ *
346
+ * It uses dynamic import() so the main bundle has zero overhead if you never
347
+ * use mode="grpc". The protobuf definitions are created inline rather than
348
+ * from generated stubs, keeping the SDK self-contained (no make proto step
349
+ * required on the client side).
350
+ *
351
+ * The wire format matches execution.proto:
352
+ * service ExecutionService { rpc RunWorkflow(RunRequest) → stream StepEvent }
353
+ */
354
+ /**
355
+ * gRPC StepEvent on the wire. Unlike the typed SSE `StepEvent` (which is
356
+ * narrowed to `event_type: "step"`), this can carry "step" | "done" |
357
+ * "error" | "suspended" — the discriminator the servicer chose.
358
+ */
359
+ interface GrpcStepEvent {
360
+ event_type: "step" | "done" | "error" | "suspended";
361
+ step_id: string;
362
+ kind: string;
363
+ signal: string;
364
+ /** Decoded from `snapshot_json` on the wire. */
365
+ snapshot: Record<string, unknown>;
366
+ duration_ms: number;
367
+ error_detail?: string;
368
+ }
369
+ interface GrpcRunOptions {
370
+ baseUrl: string;
371
+ workflowName: string;
372
+ payloadJson: string;
373
+ tokenFallback?: string;
374
+ token?: string;
375
+ signal?: AbortSignal;
376
+ }
377
+ /**
378
+ * Open a gRPC-Web server-streaming call to ExecutionService.RunWorkflow.
379
+ *
380
+ * Yields `GrpcStepEvent` objects (with `snapshot_json` already parsed into
381
+ * `snapshot`). The caller decides what each `event_type` means.
382
+ *
383
+ * Throws if the peer deps are not installed.
384
+ */
385
+ declare function openGrpcStream(options: GrpcRunOptions): AsyncGenerator<GrpcStepEvent>;
386
+
387
+ /**
388
+ * Low-level HTTP transport for @tuvl/client.
389
+ *
390
+ * Handles auth header injection, JSON serialisation, and returns raw
391
+ * Response objects so higher-level transports (SSE, gRPC) can consume them.
392
+ */
393
+ interface TransportOptions {
394
+ baseUrl: string;
395
+ defaultToken?: string;
396
+ }
397
+ declare class Transport {
398
+ /** Public so TuvlClient can forward it to the gRPC subtransport. */
399
+ readonly baseUrl: string;
400
+ private defaultToken;
401
+ constructor(options: TransportOptions);
402
+ /** Update the default token (e.g. after refresh). */
403
+ setToken(token: string): void;
404
+ /** Build the Authorization header value, or undefined if no token. */
405
+ private authHeader;
406
+ /** Perform a plain JSON POST and return the parsed response body. */
407
+ post<TBody = unknown, TResponse = unknown>(path: string, body: TBody, options?: {
408
+ token?: string;
409
+ signal?: AbortSignal;
410
+ }): Promise<TResponse>;
411
+ /**
412
+ * Open an SSE-capable stream and return the raw Response.
413
+ * Callers are responsible for reading `response.body`.
414
+ */
415
+ postStream(path: string, body: unknown, options?: {
416
+ token?: string;
417
+ signal?: AbortSignal;
418
+ }): Promise<Response>;
419
+ /** Simple GET returning parsed JSON. */
420
+ get<TResponse = unknown>(path: string, options?: {
421
+ token?: string;
422
+ signal?: AbortSignal;
423
+ }): Promise<TResponse>;
424
+ }
425
+
426
+ export { type BootstrapRequest, type DoneEvent, type ErrorEvent, type ExecuteOptions, type ExecuteVersionedOptions, type MeResponse, type RestEnvelope, type ResumeOptions, type ResumeRequest, type StepEvent, type SuspendedEvent, type TokenResponse, Transport, TuvlAuth, type TuvlAuthOptions, TuvlClient, type TuvlClientOptions, TuvlWorkflowError, TuvlWorkflowSuspendedError, type WorkflowErrorPayload, type WorkflowManifest, type WorkflowManifestMap, openGrpcStream, parseSseStream };