@bluelibs/runner 6.3.0 → 6.3.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,154 @@
1
+ # BlueLibs Runner: Remote Lanes AI Field Guide
2
+
3
+ ## Event Lanes (Node)
4
+
5
+ Event Lanes route lane-assigned events to queues using explicit lane references.
6
+
7
+ - Runtime boundary: `eventLanesResource` attaches interception at runtime for lane-assigned emissions only; non-lane events keep normal local behavior.
8
+ - Define lanes with `r.eventLane("email-lane").build()` (or `eventLane(...)`).
9
+ - Lane async-context policy is lane-level: `r.eventLane("...").asyncContexts([...])` (default is `[]`, so none are forwarded unless explicitly allowlisted).
10
+ - Assign events to lanes with `r.eventLane("...").applyTo([eventOrId])`.
11
+ - Define topology with `r.eventLane.topology({ profiles, bindings })`.
12
+ - `profiles[profile].consume` is object-based: `[{ lane, hooks?: { only?: [hookA, hookB] } }]`.
13
+ - Boundary reminder: Event Lanes are async fire-and-forget queue routing; use RPC Lanes for synchronous task/event RPC (`readmes/REMOTE_LANES.md`).
14
+ - Register `eventLanesResource` (from `@bluelibs/runner/node`) with:
15
+ - `profile` + `topology` + optional `mode` (`"network"` | `"transparent"` | `"local-simulated"`)
16
+ - `bindings: [{ lane, queue, auth?, prefetch?, maxAttempts?, retryDelayMs? }]` where `queue` can be a queue instance or a queue resource
17
+ - Use profile constants when desired:
18
+ - `const Profiles = { API: "api", WORKER: "worker" } as const`
19
+ - `profile: Profiles.API`
20
+ - `mode: "network"` (default):
21
+ - Lane-assigned event emissions (`applyTo`) are intercepted and enqueued to bound queues.
22
+ - Active profile `consume` lanes start dequeue workers on `r.system.events.ready`.
23
+ - Payload is deserialized with `serializer.parse(...)`, then re-emitted in-process.
24
+ - If `consume[].hooks.only` is configured for that lane, only those hooks run for the relay re-emit.
25
+ - Allowlisted async contexts are serialized on the producer side and rehydrated on the consumer side.
26
+ - Auth readiness is role-based: consumed lanes require verifier material; non-consumed lanes require signer material.
27
+ - In `jwt_asymmetric`, this enables producer-only private key and consumer-only public key setups.
28
+ - `mode: "transparent"`:
29
+ - Lane transport is bypassed.
30
+ - Lane-assigned events execute locally through the normal event pipeline.
31
+ - `mode: "local-simulated"`:
32
+ - Lane-assigned events use an in-memory simulated relay path.
33
+ - Payload crosses a serializer boundary (`stringify -> parse`) before local re-emit.
34
+ - Lane `asyncContexts` allowlist still applies in `local-simulated` (default `[]`, so no implicit forwarding).
35
+ - If `binding.auth` is configured, the simulated path also signs+verifies JWT lane tokens before relay emit.
36
+ - In `jwt_asymmetric`, local-simulated must have both signer and verifier key material available.
37
+ - Local emulation options without extra services:
38
+ - `transparent` for fastest feedback loops.
39
+ - `local-simulated` for serializer-boundary simulation.
40
+ - Runtime guard rails:
41
+ - lane ids must be non-empty strings (`defineEventLane`, `defineRpcLane`)
42
+ - `applyTo` string ids are validated against container definitions and type (event only).
43
+ - Event cannot be on two different `eventLane`s.
44
+ - Event cannot be on both `eventLane` and `rpcLane` via lane assignment.
45
+ - Deprecated `tags.eventLane` and `tags.eventLaneHook` fail fast at startup.
46
+ - Missing signer material fails fast (`remoteLanes-auth-signerMissing`) for producer roles.
47
+ - Missing verifier material fails fast (`remoteLanes-auth-verifierMissing`) for consumer roles.
48
+ - In `transparent` and `local-simulated`, profile `consume` does not start network consumers, but it can still declare lane presence and relay hook policy (`hooks.only`).
49
+ - Relay re-emits bypass lane interception to prevent loops.
50
+ - Hooks run based on event subscriptions after relay re-emit.
51
+ - When debug event emission logging is enabled (`logEventEmissionOnRun`), Event Lanes emits routing diagnostics: `event-lanes.enqueue`, `event-lanes.relay-emit`, and `event-lanes.skip-inactive-lane`.
52
+ - Consumer queue prefetch is resolved from lane binding `prefetch`.
53
+ - Event Lanes supports binding-level retry policy:
54
+ - `maxAttempts` (default `1`) controls retry budget before final fail path.
55
+ - `retryDelayMs` adds a delay before requeue retries.
56
+ - Final failure settles with `nack(false)`: dead-letter behavior is broker/queue-policy owned (Runner does not manually publish to DLQ).
57
+ - Multiple lanes can share one queue, but each lane can only have one binding.
58
+
59
+ Built-in queue adapters:
60
+
61
+ - `MemoryEventLaneQueue`
62
+ - `RabbitMQEventLaneQueue`
63
+
64
+ `RabbitMQEventLaneQueue` supports queue wiring options for common broker setups:
65
+
66
+ - All options are optional except `queue.name`.
67
+ - `queue.durable` (default `true`)
68
+ - `queue.assert` (`"active" | "passive"`, default `"active"`)
69
+ - `queue.arguments` (extra `assertQueue` arguments)
70
+ - `queue.deadLetter` (plain string shorthand like `"my.dlq"`, or `{ queue, exchange, routingKey }`)
71
+ - `publishOptions` (defaults to `{ persistent: true }`; message publish properties, intentionally separate from queue declaration settings)
72
+ - `publishConfirm` (default `true`; waits for broker confirms when available)
73
+ - `reconnect` (`{ enabled?, maxAttempts?, initialDelayMs?, maxDelayMs? }`; retry/recovery policy for connection/channel drops)
74
+
75
+ Custom backends implement `IEventLaneQueue` (`enqueue`, `consume`, `ack`, `nack`, optional `cooldown`, `setPrefetch`, `init`, `dispose`).
76
+
77
+ ```ts
78
+ import { randomUUID } from "node:crypto";
79
+ import type {
80
+ EventLaneMessage,
81
+ EventLaneMessageHandler,
82
+ IEventLaneQueue,
83
+ } from "@bluelibs/runner/node";
84
+
85
+ class CustomEventLaneQueue implements IEventLaneQueue {
86
+ async enqueue(
87
+ _message: Omit<EventLaneMessage, "id" | "createdAt" | "attempts">,
88
+ ): Promise<string> {
89
+ return randomUUID();
90
+ }
91
+
92
+ async consume(_handler: EventLaneMessageHandler): Promise<void> {}
93
+ async ack(_messageId: string): Promise<void> {}
94
+ async nack(_messageId: string, _requeue: boolean = true): Promise<void> {}
95
+ async setPrefetch(_count: number): Promise<void> {}
96
+ }
97
+ ```
98
+
99
+ ## RPC Lanes (Node)
100
+
101
+ RPC Lanes route lane-assigned tasks/events across runners using profile/topology bindings.
102
+
103
+ - Runtime boundary: `rpcLanesResource` routes lane-assigned events via interception and lane-assigned tasks via runtime task decoration; non-lane flows remain unchanged.
104
+ - Define lanes with `r.rpcLane("billing-lane").build()`.
105
+ - Lane async-context policy is lane-level: `r.rpcLane("...").asyncContexts([...])` (default is `[]`, so none are forwarded unless explicitly allowlisted).
106
+ - Optional lane-side assignment: `r.rpcLane("...").applyTo([taskOrEventOrId])`.
107
+ - Tag tasks/events with `tags.rpcLane.with({ lane })`.
108
+ - Define topology with `r.rpcLane.topology({ profiles, bindings })`:
109
+ - `profiles[profile].serve` selects lanes this runtime serves locally.
110
+ - `bindings[]` maps `lane -> communicator resource` plus async-context policy and optional lane JWT material (`auth`).
111
+ - Register `rpcLanesResource` (from `@bluelibs/runner/node`) with:
112
+ - `profile` + `topology` + optional `mode` (`"network"` | `"transparent"` | `"local-simulated"`) + optional `exposure.http`.
113
+ - Exposure `http.auth` and lane JWT auth are separate:
114
+ - `exposure.http.auth` gates HTTP endpoint access.
115
+ - `binding.auth` gates lane authorization for task/event execution.
116
+ - Communicator resources are container-aware and can use:
117
+ - `init(r.rpcLane.httpClient({ client: "fetch" | "mixed" | "smart", ... }))`
118
+ - `fetch` is universal (`createHttpClient`)
119
+ - `mixed` / `smart` are Node presets.
120
+ - Routing behavior in `mode: "network"`:
121
+ - Lane in `serve` -> task/event executes locally.
122
+ - Lane not in `serve` -> task/event routes remotely via communicator.
123
+ - Every assigned or served lane must have a communicator binding.
124
+ - Mode overrides:
125
+ - `transparent`: lane-assigned tasks/events execute locally (no lane transport).
126
+ - `local-simulated`: lane-assigned tasks/events go through a local serializer roundtrip simulation and still enforce lane JWT when `binding.auth` is enabled.
127
+ - In `jwt_asymmetric`, `local-simulated` requires both signer and verifier key material (same runtime signs and verifies).
128
+ - Local emulation options:
129
+ - `transparent` for pure local smoke tests.
130
+ - `local-simulated` for local transport-shape simulation.
131
+ - In `transparent` and `local-simulated`, profile `serve` is ignored for routing decisions.
132
+ - Exposure behavior:
133
+ - HTTP exposure starts only when the active profile resolves at least one served task/event endpoint.
134
+ - If `exposure.http` is configured but nothing is served, startup skips exposure and logs `rpc-lanes.exposure.skipped`.
135
+ - `serve` lanes derive server allow-list automatically for lane-assigned tasks/events.
136
+ - Auth remains fail-closed unless explicitly configured otherwise.
137
+ - Lane JWT authorization is validated per served lane before task/event execution.
138
+ - Runtime guard rails:
139
+ - `applyTo` string ids are validated against container definitions and type (task/event).
140
+ - Task/event cannot be on two different `rpcLane`s.
141
+ - Missing signer material fails fast (`remoteLanes-auth-signerMissing`).
142
+ - Missing verifier material fails fast (`remoteLanes-auth-verifierMissing`).
143
+
144
+ ## HTTP RPC Transport
145
+
146
+ HTTP RPC transport is used by RPC Lane communicators (`fetch`, `mixed`, `smart`) and keeps RPC capabilities intact:
147
+
148
+ - JSON payloads
149
+ - multipart uploads
150
+ - octet-stream/duplex paths (Node smart/mixed)
151
+ - typed error rethrow via `errorRegistry`
152
+ - async-context header propagation (policy-controlled per rpc lane binding)
153
+ - request-id/correlation headers and discovery endpoints
154
+ - event return payload support (`eventWithResult`)
@@ -0,0 +1,330 @@
1
+ # Runner Remote Lanes HTTP Protocol Policy (v1.0)
2
+
3
+ > **Status**: Draft reference derived from Runner implementation. This document formalizes the wire protocol used by HTTP RPC communicators and clients, enabling interoperability, debugging, and future extensions. It is not a normative standard but reflects the current behavior of RPC-lanes-owned HTTP exposure (`rpcLanesResource.with({ exposure: { http: ... } })`) and clients such as `createHttpClient`, `createHttpSmartClient`, and `createHttpMixedClient`. For usage, see [REMOTE_LANES.md](REMOTE_LANES.md).
4
+
5
+ > **Boundary**: This protocol is intended for inter-runner/service-to-service communication, not as a public web API contract for untrusted internet clients.
6
+
7
+ > **Mode Note**: This policy applies to Remote Lanes in `mode: "network"`. `transparent` and `local-simulated` are local runtime modes and do not define additional HTTP wire behavior.
8
+
9
+ ## Table of Contents
10
+
11
+ - [Runner Remote Lanes HTTP Protocol Policy (v1.0)](#runner-remote-lanes-http-protocol-policy-v10)
12
+ - [Table of Contents](#table-of-contents)
13
+ - [Overview](#overview)
14
+ - [Goals](#goals)
15
+ - [Base Path](#base-path)
16
+ - [Protocol Envelope](#protocol-envelope)
17
+ - [Common Elements](#common-elements)
18
+ - [Serialization](#serialization)
19
+ - [Authentication](#authentication)
20
+ - [Header Reference](#header-reference)
21
+ - [Error Handling](#error-handling)
22
+ - [CORS](#cors)
23
+ - [Endpoints](#endpoints)
24
+ - [Task Invocation (`POST /task/{taskId}`)](#task-invocation-post-tasktaskid)
25
+ - [Event Emission (`POST /event/{eventId}`)](#event-emission-post-eventeventid)
26
+ - [Discovery (`GET /discovery`)](#discovery-get-discovery)
27
+ - [Request Modes](#request-modes)
28
+ - [JSON Mode](#json-mode)
29
+ - [Multipart Mode](#multipart-mode)
30
+ - [Octet-Stream Mode](#octet-stream-mode)
31
+ - [Response Modes](#response-modes)
32
+ - [Extensions](#extensions)
33
+ - [Abort and Timeouts](#abort-and-timeouts)
34
+ - [Compression](#compression)
35
+ - [Context Propagation (Node-Only)](#context-propagation-node-only)
36
+ - [Streaming](#streaming)
37
+ - [Examples](#examples)
38
+ - [JSON Task (curl)](#json-task-curl)
39
+ - [Multipart Upload (Node-like, conceptual curl)](#multipart-upload-node-like-conceptual-curl)
40
+ - [Octet Duplex (Node-only, conceptual)](#octet-duplex-node-only-conceptual)
41
+ - [Event Emission](#event-emission)
42
+ - [References](#references)
43
+
44
+ ## Overview
45
+
46
+ The Runner remote lanes HTTP protocol enables remote invocation of tasks and RPC-style event emission across processes (e.g., Node server to browser or CLI) using standard HTTP. It is stateless, extensible, and optimized for Runner's dependency injection, middleware, and validation model.
47
+
48
+ ### Goals
49
+
50
+ - **Simplicity**: Minimal overhead; leverages HTTP/1.1+ with serialized JSON for structured data.
51
+ - **Cross-Platform**: Works in Node (streams/files) and browsers (fetch/FormData).
52
+ - **Security**: Auth (fail-closed by default), allow-lists, CORS, abort handling.
53
+ - **Efficiency**: Supports streaming (duplex/raw) and files (manifest-based multipart) without buffering.
54
+ - **Observability**: Logs, context propagation, discovery endpoint.
55
+
56
+ ### Base Path
57
+
58
+ All endpoints are under a configurable base path (default: `/__runner`). Example: `http://localhost:7070/__runner/task/app.tasks.add`.
59
+
60
+ - IDs (`taskId`/`eventId`): URL-encoded strings (e.g., `app.tasks.add%2Fsub` for `app.tasks.add/sub`).
61
+ - Query params are ignored by current handlers and are not part of the protocol contract.
62
+
63
+ ### Protocol Envelope
64
+
65
+ Task requests wrap payloads as `{ input: <value> }`. Event requests use `{ payload?: <value>, returnPayload?: boolean }`. Responses use a standard envelope:
66
+
67
+ ```json
68
+ { "ok": true, "result": <output> } // Success
69
+ { "ok": false, "error": { "code": "FORBIDDEN", "message": "Description" } } // Error
70
+ ```
71
+
72
+ - `ok`: Boolean.
73
+ - `result`: Task output, or event result when `returnPayload` is requested.
74
+ - `error`: Details on failure (HTTP status is provided by the HTTP response status code; `error.code` is a string).
75
+ - `meta` (optional/reserved): Present in the shared `ProtocolEnvelope` shape but currently not emitted by the RPC lanes HTTP exposure runtime.
76
+
77
+ ## Common Elements
78
+
79
+ ### Serialization
80
+
81
+ - All JSON bodies/responses are serialized with Runner's serializer to preserve types like `Date`, `RegExp`, and custom classes (via `addType`).
82
+ - Files are **not** custom serializer types: use sentinels `{"$runnerFile": "File", "id": "<uuid>", "meta": {...}}` (see Multipart Mode).
83
+ - Charset: UTF-8.
84
+ - Custom Types: Client/server must sync explicit `addType({ id, is, serialize, deserialize, ... })` registrations on the serializer resource (`resources.serializer`).
85
+
86
+ ### Authentication
87
+
88
+ - **Header**: Default `x-runner-token: <token>` (configurable via `exposure.http.auth.header` and in clients).
89
+ - **Lane JWT**: Remote Lanes may use binding-level JWT auth via `binding.auth` (default header `authorization: Bearer <jwt>` unless binding overrides header).
90
+ - **Layering**: `x-runner-token` (or custom `auth.header`) is exposure-level auth; lane JWT is an independent lane authorization layer.
91
+ - **Token**: `auth.token` supports a string or string[] (any match is accepted).
92
+ - **Validators**: If tasks tagged with `tags.authValidator` exist, they are executed (OR logic); any validator returning `{ ok: true }` authenticates the request.
93
+ - **Anonymous access**: If no token and no validators exist, RPC lanes HTTP exposure fails closed by default with `500 AUTH_NOT_CONFIGURED`. Set `auth.allowAnonymous: true` to explicitly allow unauthenticated access.
94
+ - **Dynamic headers**: Clients can override per-request headers via `options.headers` and mutate headers in `onRequest({ headers })`.
95
+ - **Allow-Lists**: Server restricts to configured exposure allow-list sources (`rpcLanesResource` serve topology in `mode: "network"`). Unknown IDs → 403 Forbidden.
96
+ - **Lane authorization**: For served RPC lanes with binding auth enabled, token verification is lane-specific and happens before task/event execution.
97
+ - **Served endpoints required**: RPC-lanes-owned HTTP exposure only starts when the active profile serves at least one RPC task or event. If nothing is served, startup skips HTTP exposure and logs `rpc-lanes.exposure.skipped`; `auth.allowAnonymous` does not force exposure to boot.
98
+ - **Auth audit logs**: Failed authentication attempts are logged (`exposure.auth.failure`) with request metadata and correlation id.
99
+
100
+ ### Header Reference
101
+
102
+ | Header | Direction | Required | Description |
103
+ | ------------------------ | ----------------- | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
104
+ | `x-runner-token` | client -> server | Yes (unless `auth.allowAnonymous: true`) | Authentication token. Header name can be overridden by `auth.header`. |
105
+ | `x-runner-request-id` | client <-> server | Optional | Correlation id. Server accepts valid incoming ids and otherwise generates one; response echoes final id. |
106
+ | `x-runner-context` | client -> server | Optional | Serializer-encoded async-context map. Server restores only registered contexts and applies lane/exposure async-context policy (lane `asyncContexts` allowlist defaults to none; legacy `allowAsyncContext` bridge can temporarily allow all). Invalid entries are ignored. |
107
+ | `content-type` | client -> server | Recommended | Request mode selector (`application/json`, `multipart/form-data`, `application/octet-stream`). If omitted, the server falls back to the JSON path. |
108
+ | `X-Content-Type-Options` | server -> client | Always | Security header set to `nosniff`. |
109
+ | `X-Frame-Options` | server -> client | Always | Security header set to `DENY`. |
110
+
111
+ ### Error Handling
112
+
113
+ - **HTTP Status**: 200 (OK/success), 4xx (client errors), 5xx (server errors).
114
+ - **JSON Errors**: Enveloped when the response has not started yet; once a stream/response is written, subsequent errors are best-effort only.
115
+ - **Sanitization**: For `500` errors, RPC lanes HTTP exposure sanitizes the payload to avoid leaking sensitive internals:
116
+ - `error.message` becomes `"Internal Error"` unless the server recognized a typed error.
117
+ - typed errors may preserve `error.id`, `error.data`, and the typed error message.
118
+ - **Common Codes**:
119
+ | HTTP | error.code | Description |
120
+ |------|------------|-------------|
121
+ | 400 | INVALID_JSON | Malformed JSON body. |
122
+ | 400 | INVALID_MULTIPART | Invalid multipart payload or manifest. |
123
+ | 400 | MISSING_MANIFEST | Multipart missing `__manifest`. |
124
+ | 400 | PARALLEL_EVENT_RETURN_UNSUPPORTED | Event is `parallel`, so `returnPayload` is not supported. |
125
+ | 401 | UNAUTHORIZED | Invalid token or failed auth validators. |
126
+ | 403 | FORBIDDEN | Exposure not enabled or id not in allow-list. |
127
+ | 404 | NOT_FOUND | Task/event not found (after allow-list checks). |
128
+ | 405 | METHOD_NOT_ALLOWED | Non-POST (except discovery). |
129
+ | 413 | PAYLOAD_TOO_LARGE | JSON/multipart exceeded configured limits. |
130
+ | 499 | REQUEST_ABORTED | Client aborted/closed the request. |
131
+ | 500 | INTERNAL_ERROR | Task exception or server error (sanitized). |
132
+ | 500 | STREAM_ERROR | Multipart stream error (sanitized). |
133
+ | 500 | MISSING_FILE_PART | Expected file not in multipart. |
134
+ | 500 | AUTH_NOT_CONFIGURED | No auth is configured and `allowAnonymous` is not enabled. |
135
+ - **Logging**: Server logs errors through the logger resource (`resources.logger`, for example `exposure.task.error`), plus auth failures.
136
+ - **Correlation ID**: Requests carry/receive `x-runner-request-id` (generated when absent) for end-to-end tracing.
137
+ - **Security headers**: RPC lanes HTTP exposure sets `X-Content-Type-Options: nosniff` and `X-Frame-Options: DENY` on responses.
138
+
139
+ ### CORS
140
+
141
+ - Configurable via exposure (`http.cors`).
142
+ - Defaults: If `http.cors` is omitted, RPC lanes HTTP exposure sets `Access-Control-Allow-Origin: *`.
143
+ - Credentials: If `credentials: true`, you must also set an explicit `origin`; otherwise no `Access-Control-Allow-Origin` is sent (browsers will block cross-origin access).
144
+ - Preflight (OPTIONS): Auto-handled with `204`.
145
+ - Headers: Defaults `Access-Control-Allow-Methods: POST, OPTIONS`; allowed headers echo `Access-Control-Request-Headers` unless `allowedHeaders` is provided; `Vary: Origin` is appended when needed.
146
+
147
+ ## Endpoints
148
+
149
+ ### Task Invocation (`POST /task/{taskId}`)
150
+
151
+ - **Purpose**: Run a remote task with input; returns result.
152
+ - **Auth**: Required.
153
+ - **Allow-List**: Checked against server exposure policies (`rpcLanesResource` serve topology).
154
+ - **Body Modes**: See [Request Modes](#request-modes).
155
+ - **Context**: Task receives `useRpcLaneRequestContext()` (Node-only: `{ req, res, url, headers, method, signal }`).
156
+ - **Response**:
157
+ - Success: 200 + JSON envelope or stream (see [Response Modes](#response-modes)).
158
+ - Errors: 4xx/5xx + JSON envelope.
159
+
160
+ ### Event Emission (`POST /event/{eventId}`)
161
+
162
+ - **Purpose**: Emit a remote event with payload using RPC semantics.
163
+ - **Auth**: Required.
164
+ - **Allow-List**: Checked.
165
+ - **Body**: Always JSON mode: `{ payload?: <any>, returnPayload?: boolean }` (serialized).
166
+ - **Response**:
167
+ - default: 200 + `{ ok: true }` (fire-and-forget behavior)
168
+ - if `returnPayload: true`: 200 + `{ ok: true, result: <payload> }` (RPC-style event result; not supported when the event is marked `parallel`).
169
+ - **No Context**: Events don't provide `useRpcLaneRequestContext()`.
170
+
171
+ ### Discovery (`GET /discovery`)
172
+
173
+ - **Purpose**: Query server allow-list for validation/discovery.
174
+ - **Auth**: Required.
175
+ - **Body**: None.
176
+ - **Methods**: `GET` only. Other non-`OPTIONS` methods return `405 METHOD_NOT_ALLOWED`.
177
+ - **Disabled Mode**: If `http.disableDiscovery` is `true`, the endpoint returns `404 NOT_FOUND`.
178
+ - **Response**: 200 + JSON:
179
+ ```json
180
+ {
181
+ "ok": true,
182
+ "result": {
183
+ "allowList": {
184
+ "enabled": true,
185
+ "tasks": ["app.tasks.add", "app.tasks.upload"],
186
+ "events": ["app.events.notify"]
187
+ }
188
+ }
189
+ }
190
+ ```
191
+ - **Use Case**: Clients fetch to check reachable IDs dynamically.
192
+
193
+ ## Request Modes
194
+
195
+ Server primarily routes by `Content-Type`. If the header is omitted, requests fall back to the JSON path.
196
+
197
+ ### JSON Mode
198
+
199
+ - **When**: No files/streams (default fallback).
200
+ - **Content-Type**: Usually `application/json; charset=utf-8` (omitting `content-type` still falls back to this path).
201
+ - **Body**: JSON `{ input: <any> }` (or bare `<input>`; the server treats non-object bodies as the input directly).
202
+ - **Handling**: Server parses JSON (via Runner serializer), runs task with input, serializes result.
203
+ - **Limits**: Default max body size is 2MB (`http.limits.json.maxSize`); over-limit returns 413/PAYLOAD_TOO_LARGE.
204
+ - **Limitations**: No files (use multipart); no raw streams (use octet).
205
+
206
+ ### Multipart Mode
207
+
208
+ - **When**: Input contains File sentinels (client-detected).
209
+ - **Content-Type**: `multipart/form-data; boundary=<boundary>` (RFC 7578).
210
+ - **Body Parts**:
211
+ - `__manifest` (field): JSON string of `{ input?: <obj> }`.
212
+ - The server treats this as a plain field value; the per-part `Content-Type` is not enforced (some clients send `application/json; charset=utf-8`).
213
+ - File placeholders: `{"$runnerFile": "File", "id": "<uuid>", "meta": { "name": string, "type"?: string, "size"?: number, "lastModified"?: number, "extra"?: object }}`
214
+ - `<uuid>`: Client-generated (unique per request).
215
+ - `file:<id>` (binary): File bytes for each sentinel.
216
+ - `Content-Disposition: form-data; name="file:<id>"; filename="<name>"`
217
+ - `Content-Type`: From meta (fallback: detected or `application/octet-stream`).
218
+ - **Handling**:
219
+ - Parse manifest → hydrate input (replace sentinels with `InputFile` objects: `{ name, type, size?, lastModified?, extra?, resolve(): Promise<{stream: Readable}> }`).
220
+ - Meta Precedence: Manifest overrides part headers (e.g., name/type).
221
+ - All expected files must arrive; unconnected → 500/MISSING_FILE_PART.
222
+ - Single-use streams: `resolve()` consumes once.
223
+ - **Limits**: Defaults are 20MB per file, 10 files, 100 fields, 1MB per field (`http.limits.multipart`); over-limit returns 413/PAYLOAD_TOO_LARGE.
224
+ - **Client Prep**: Universal/fetch clients use `buildUniversalManifest`; the Node smart client builds the equivalent manifest through its Node upload path.
225
+ - **Limitations**: Browser: Blobs/FormData. Node: Buffers/streams.
226
+
227
+ ### Octet-Stream Mode
228
+
229
+ - **When**: Input is raw `Readable` (Node duplex, client-detected).
230
+ - **Content-Type**: `application/octet-stream`
231
+ - **Body**: Raw binary (piped stream).
232
+ - **Handling**: No parsing; request body is not pre-consumed and the task accesses bytes via `useRpcLaneRequestContext().req` (IncomingMessage stream).
233
+ - **Async context**: `x-runner-context` still applies (it is a header, independent of body mode).
234
+ - **Limitations**: Node-only; no JSON input (wrap in File sentinel + multipart if needed).
235
+
236
+ ## Response Modes
237
+
238
+ - **Default**: JSON envelope (serialized).
239
+ - **Streaming**: If task returns `Readable` or `{ stream: Readable }`:
240
+ - Status: 200.
241
+ - Content-Type: `application/octet-stream` (or custom via res).
242
+ - Body: Piped stream (chunked encoding).
243
+ - No envelope (direct bytes).
244
+ - **Events**: JSON envelope. Default is `{ ok: true }`; `returnPayload: true` returns `{ ok: true, result: <payload> }`.
245
+ - **Errors**: JSON envelope (even on streams, if not already written).
246
+
247
+ ## Extensions
248
+
249
+ ### Abort and Timeouts
250
+
251
+ - **Client**: Set `timeoutMs` → `AbortController` (signal aborts request).
252
+ - **Server**: Wires signal to task (`useRpcLaneRequestContext().signal`); aborts streams.
253
+ - **Response**: 499/REQUEST_ABORTED on abort.
254
+ - **Hook**: Tasks check `signal.aborted` or listen for "abort".
255
+
256
+ ### Compression
257
+
258
+ - **Status**: Not implemented in core (use a proxy like nginx for compression); future: zlib integration.
259
+ - **Security**: Avoid BREACH risks (no secrets near user data).
260
+
261
+ ### Context Propagation (Node-Only)
262
+
263
+ - **Mechanism**: Snapshots registered async contexts created via `defineAsyncContext({ id })`.
264
+ - **Transport**: A Serializer-encoded map sent in `x-runner-context` header (applies to JSON, multipart, and octet-stream).
265
+ - **Rules**: Stable IDs; optional `serialize`/`parse` hooks. Contexts that cannot be captured or parsed are skipped rather than failing the whole request.
266
+ - **Security**: Server only restores known registered contexts; invalid headers/entries are ignored.
267
+ - **Gate**: Set `allowAsyncContext: false` on the relevant server RPC-lane binding to disable server-side hydration of `x-runner-context` as a legacy bridge when no lane `asyncContexts` allowlist is configured.
268
+
269
+ ### Streaming
270
+
271
+ - **Duplex**: Octet mode (req → task processing → res stream).
272
+ - **Server Push**: Task returns stream → piped to res.
273
+ - **Client**: `createHttpSmartClient` returns `Readable`; `createHttpMixedClient` auto-switches.
274
+ - **Abort**: Signal destroys pipes.
275
+
276
+ ## Examples
277
+
278
+ ### JSON Task (curl)
279
+
280
+ ```bash
281
+ curl -X POST http://localhost:7070/__runner/task/app.tasks.add \
282
+ -H "x-runner-token: secret" \
283
+ -H "Content-Type: application/json" \
284
+ -d '{"input": {"a": 1, "b": 2}}'
285
+ ```
286
+
287
+ Response:
288
+
289
+ ```json
290
+ { "ok": true, "result": 3 }
291
+ ```
292
+
293
+ ### Multipart Upload (Node-like, conceptual curl)
294
+
295
+ Manifest JSON: `{"input": {"file": {"$runnerFile": "File", "id": "f1", "meta": {"name": "doc.txt", "type": "text/plain"}}}}`
296
+
297
+ ```bash
298
+ curl -X POST http://localhost:7070/__runner/task/app.tasks.upload \
299
+ -H "x-runner-token: secret" \
300
+ -F '__manifest={"input": {"file": {"$runnerFile": "File", "id": "f1", "meta": {"name": "doc.txt"}}}}' \
301
+ -F 'file:f1=@/path/to/doc.txt'
302
+ ```
303
+
304
+ Response: `{"ok": true, "result": {"bytes": 1024}}`
305
+
306
+ ### Octet Duplex (Node-only, conceptual)
307
+
308
+ Client pipes `Readable` (e.g., fs.createReadStream); server echoes via `req.pipe(res)`.
309
+
310
+ ### Event Emission
311
+
312
+ ```bash
313
+ curl -X POST http://localhost:7070/__runner/event/app.events.notify \
314
+ -H "x-runner-token: secret" \
315
+ -H "Content-Type: application/json" \
316
+ -d '{"payload": {"message": "hi"}}'
317
+ ```
318
+
319
+ Response: `{"ok": true}`
320
+
321
+ ## References
322
+
323
+ - [COMPACT_GUIDE.md](./COMPACT_GUIDE.md): High-level fluent API.
324
+ - [REMOTE_LANES.md](REMOTE_LANES.md): Usage, examples, troubleshooting.
325
+ - Code: `src/node/rpc-lanes/rpcLanes.exposure.ts`, `src/node/exposure/` (server), [`src/http-client.ts`](../src/http-client.ts), [`src/node/http/http-smart-client.model.ts`](../src/node/http/http-smart-client.model.ts), and [`src/node/http/http-mixed-client.ts`](../src/node/http/http-mixed-client.ts) (clients).
326
+ - Standards: HTTP/1.1 (RFC 7230), Multipart (RFC 7578), JSON.
327
+
328
+ ---
329
+
330
+ _Last Updated: March 17, 2026_