@pipelex/sdk 0.21.0 → 0.22.0
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 +18 -1
- package/dist/artifacts.js +3 -1
- package/dist/artifacts.js.map +1 -1
- package/dist/client.d.ts +20 -1
- package/dist/client.js +22 -2
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +25 -7
- package/dist/errors.js +15 -7
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/product-models.d.ts +42 -1
- package/dist/product-models.js +1 -1
- package/dist/upload-grant.d.ts +60 -0
- package/dist/upload-grant.js +227 -0
- package/dist/upload-grant.js.map +1 -0
- package/dist/upload.d.ts +4 -3
- package/dist/upload.js +6 -5
- package/dist/upload.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +5 -2
- package/docs/input-preparation.md +54 -5
- package/package.json +6 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Input preparation (`uploadFile` / `prepareInputs`)
|
|
2
2
|
|
|
3
|
-
> **Status: implemented** (`src/upload.ts`, `src/prepare-inputs.ts`). This document records the contract, and is the public account of it. The raw `upload()` primitive described in [architecture.md](./architecture.md) is the wire call `uploadFile` and `prepareInputs` build on.
|
|
3
|
+
> **Status: implemented** (`src/upload.ts`, `src/upload-grant.ts`, `src/prepare-inputs.ts`). This document records the contract, and is the public account of it. The raw `upload()` primitive described in [architecture.md](./architecture.md) is the wire call `uploadFile` and `prepareInputs` build on.
|
|
4
4
|
>
|
|
5
5
|
> **Current scope.** `prepareInputs` names the method the same three ways every other method-taking operation does — inline `files`, a `method_ref` address, or a stored `method_id` — exactly one per call, all three resolved server-side by the one `validate` call it composes. One piece remains deliberately deferred and additive (it does not change this contract): the opt-in ingest of `http(s)` URLs into storage — for now an `http(s)` URL at a file position always passes through unchanged.
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ A hosted run cannot see the caller's filesystem or a browser's selected bytes. T
|
|
|
10
10
|
|
|
11
11
|
Preparation is **explicit and separate from running.** `execute` / `start` never silently upload local files. The payoff: file-access errors happen *before* a run exists, prepared inputs are inspectable and reusable across a model sweep or retries without re-uploading, and `start` keeps a deterministic JSON-input contract.
|
|
12
12
|
|
|
13
|
-
## The
|
|
13
|
+
## The operations
|
|
14
14
|
|
|
15
15
|
### `uploadFile` — single-asset convenience
|
|
16
16
|
|
|
@@ -21,7 +21,7 @@ Uploads one asset and returns its upload record. It is the language-native conve
|
|
|
21
21
|
- A string that is an **HTTP(S) URL** or an existing **`pipelex-storage://` URI** is not a local asset and passes through in any runtime (see pass-through rules below).
|
|
22
22
|
- Open file objects and streams are **deferred** — they can be added later without removing anything.
|
|
23
23
|
|
|
24
|
-
> **Runtime acceptance vs. bundling.** These source types are accepted at *runtime* in every environment, but at the *packaging* level `@pipelex/sdk` is **Node-first**: it statically references `node:fs/promises` (to read path strings), so bundling
|
|
24
|
+
> **Runtime acceptance vs. bundling.** These source types are accepted at *runtime* in every environment, but at the *packaging* level the main `@pipelex/sdk` entry is **Node-first**: it statically references `node:fs/promises` (to read path strings), so bundling it for a browser or edge target requires marking `node:*` external. This is a deliberate stance — the client holds an API key, so it runs server-side (Next.js Server Actions, the MCP server, the plugins hook). A browser page that must store a file uses the upload grant below instead, whose sender ships from the browser-safe `@pipelex/sdk/upload` entry.
|
|
25
25
|
|
|
26
26
|
The returned **upload record** guarantees, beyond the source identity:
|
|
27
27
|
|
|
@@ -35,6 +35,55 @@ The returned **upload record** guarantees, beyond the source identity:
|
|
|
35
35
|
|
|
36
36
|
The MIME type and size are known client-side, so the record is assembled without extending the `/v1/upload` response.
|
|
37
37
|
|
|
38
|
+
### Uploading with a grant — `requestUploadGrant` / `uploadWithGrant`
|
|
39
|
+
|
|
40
|
+
`uploadFile` needs the client, and the client needs the API key. A browser page holds the bytes but must never hold the key, so the work is split in two: the side holding the key requests a **grant** for one file, hands it to the page, and the page sends the file straight to storage with it. The bytes cross neither the credential-holding server nor the API gateway, so the gateway's request quota, which caps a base64 `/v1/upload` near 7.5 MiB, does not apply: the file may be as large as the service's own limit, which the grant reports as `max_bytes`.
|
|
41
|
+
|
|
42
|
+
**Requesting the grant** is a client method over `POST /v1/upload/grant`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const grant = await client.requestUploadGrant({
|
|
46
|
+
filename: file.name,
|
|
47
|
+
content_type: file.type, // optional; empty or omitted signs no type
|
|
48
|
+
size: file.size,
|
|
49
|
+
});
|
|
50
|
+
// { uri, url, headers, expires_at, max_bytes }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The file is described, never sent. The grant (`UploadGrant`) is a presigned, create-only `PUT` for one new object under the caller's organization: `url` is where to send it, `headers` are the signed headers to send unchanged, `uri` is the `pipelex-storage://` reference the object will carry, and `expires_at` is when storage stops accepting it, a few minutes later. The wire is snake_case like every product route, so the grant can travel to the page as JSON without a remap. A declared `size` over the limit is a `413` `ApiResponseError` whose `code` is `payload_too_large`, and a deployment without the route answers a `404`.
|
|
54
|
+
|
|
55
|
+
**The grant is a bearer capability.** Until it expires, whoever holds it can write that one object, so the SDK never logs it, and its caller should not either. The route never replays a grant for a repeated request: a caller that lost one asks for a new one.
|
|
56
|
+
|
|
57
|
+
**Sending the file** is the standalone `uploadWithGrant(grant, file, { signal? })`, which takes a `Blob` or a `File` and returns `{ uri }`:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { uploadWithGrant, RejectedAssetError } from "@pipelex/sdk/upload";
|
|
61
|
+
|
|
62
|
+
const { uri } = await uploadWithGrant(grant, file);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
It sends the file as the raw body of a `PUT` to `grant.url`, with `grant.headers` exactly as received. The grant signed `Content-Length` too, and the body sets it, so the file must be exactly the declared size. Redirects are refused rather than followed, because a presigned URL is valid only at the address it was signed for. **The `uri` names nothing until the `PUT` has succeeded**, so fill a form field or a run input with it only after `uploadWithGrant` resolves.
|
|
66
|
+
|
|
67
|
+
**The browser-safe entry.** `uploadWithGrant`, the grant types and the three error classes it raises (`InputPreparationError`, `RejectedAssetError`, `UploadTransportError`) are exported from `@pipelex/sdk/upload`, whose import graph reaches no Node builtin, no `undici` and not the client, so a page bundles it with nothing marked external. They are the same function and the same classes the main entry exports, so an `instanceof` check agrees whichever entry the error came through. The unit suite bundles the entry for the browser with esbuild and fails if anything Node-only becomes reachable.
|
|
68
|
+
|
|
69
|
+
**Storage's refusals** map onto the input-preparation errors, so a caller that already handles `uploadFile` needs no new case:
|
|
70
|
+
|
|
71
|
+
| Storage answers | Error | `code` | Meaning |
|
|
72
|
+
| --- | --- | --- | --- |
|
|
73
|
+
| `2xx` | none | | Stored; `uri` names the object. |
|
|
74
|
+
| `412 PreconditionFailed` | `RejectedAssetError` | `grant_used` | The grant was already used. A grant writes one object, once — so after an earlier attempt that failed at storage, the grant's `uri` may already name the file. |
|
|
75
|
+
| `403 SignatureDoesNotMatch` | `RejectedAssetError` | `signature_mismatch` | The file's size, content type or metadata differ from what the grant signed. |
|
|
76
|
+
| `403 AccessDenied`, "Request has expired" | `RejectedAssetError` | `grant_expired` | The grant expired. |
|
|
77
|
+
| `403 AccessDenied`, headers "not signed" | `RejectedAssetError` | `unsigned_header` | The request carried a storage header the grant did not sign. |
|
|
78
|
+
| `400 RequestTimeout` | `UploadTransportError` | | Storage stopped waiting for the body and wrote nothing, so the same grant can be retried while it is valid. |
|
|
79
|
+
| any other `4xx` | `RejectedAssetError` | `store_refused` | Storage's own message is relayed. |
|
|
80
|
+
| `5xx` | `UploadTransportError` | | Whether the object was written is unknown: retrying with the same grant either stores it or answers the `412` of a used grant. |
|
|
81
|
+
| a redirect | `UploadTransportError` | | Refused, not followed. |
|
|
82
|
+
| no response | `UploadTransportError` | | Storage unreachable. In a browser a refused cross-origin request looks exactly like this, so the message also points at the page's CSP `connect-src`. |
|
|
83
|
+
| nothing sent | `UploadTransportError` | | The grant's `url` is not an absolute `http(s)` URL free of user info, so it is refused before any request. A browser would resolve a relative URL against the page and send the credential there. |
|
|
84
|
+
|
|
85
|
+
`RejectedAssetError.code` is what a caller branches on — to request a new grant on `grant_used` or `grant_expired`, say — rather than the message or the status, since an expired grant and an unsigned header both answer `403 AccessDenied` and only S3's message tells them apart. Its `status` is storage's, and its `filename` is the `File`'s name, or the object name the grant's `uri` ends in for a nameless `Blob`. `UploadTransportError.status` is storage's status when storage answered, and undefined when it did not. Neither error wraps storage's response, and only S3's error code and message reach a message: its error body can echo the signed request, credential included, so it is never kept. **No error carries the grant's URL.** The runtime's network error can: Node and Chrome name the whole URL when they cannot build a request from it, and Bun puts it on the error's `path`. So an unreachable storage is described by its origin and by the error names and codes along the cause chain (`TypeError, caused by Error ENOTFOUND`), and the runtime's error is neither relayed nor kept as `cause`. A caller's abort propagates as the signal's own reason, unwrapped, including one that lands while storage's error body is still arriving.
|
|
86
|
+
|
|
38
87
|
### `prepareInputs` — signature-driven input preparation
|
|
39
88
|
|
|
40
89
|
```
|
|
@@ -173,10 +222,10 @@ The contract distinguishes these semantic outcomes, each a typed subclass of `In
|
|
|
173
222
|
- **an unresolvable signature** (`InputPreparationError`) — the closure did not validate (`is_valid: false`, carrying the first error's message), the report carried no `input_form` descriptor, or no pipe could be chosen (an unknown or bare `pipe_ref`, or no default). A *no-verdict* condition from `/v1/validate` — a malformed selector, an unknown or foreign-org `method_id`, no package at the address, auth, a server fault — stays an `ApiResponseError` and propagates unchanged;
|
|
174
223
|
- **empty method source** (`EmptyMethodSourceError`, carries `methodId`) — `getMethodClosure` found the stored method but its `mthds` source parses to nothing (the row exists, no runnable source yet). Distinct from the `getMethod` `404` for an unknown/foreign id, which stays an `ApiResponseError`. `prepareInputs` never raises it: it hands the id to the server, which answers a sourceless method with a `422`;
|
|
175
224
|
- **invalid local source** (`InvalidLocalSourceError`) — missing, unreadable, or a path string outside Node;
|
|
176
|
-
- **rejected asset** (`RejectedAssetError`) — the server refused it (e.g. a `413` past the service-defined size cap — see "Storage policy" — surfaced as a clear rejection
|
|
225
|
+
- **rejected asset** (`RejectedAssetError`) — the server refused it (e.g. a `413` past the service-defined size cap — see "Storage policy" — surfaced as a clear rejection with `code` `too_large`, not a raw transport error), or storage refused an upload with a grant (a used or expired grant, or a file that differs from what the grant signed — see the table above for each `code`);
|
|
177
226
|
- **unsupported server capability** (`UnsupportedUploadCapabilityError`) — the configured deployment has no upload route;
|
|
178
227
|
- **authentication / authorization failure** (`UploadAuthenticationError`) — `401` / `403`;
|
|
179
|
-
- **transport failure** (`UploadTransportError`) — a network or server fault, a malformed data URL payload, or any other unexpected upload failure.
|
|
228
|
+
- **transport failure** (`UploadTransportError`) — a network or server fault, a malformed data URL payload, or any other unexpected upload failure; its `status` is the HTTP status when a response produced it.
|
|
180
229
|
|
|
181
230
|
All preparation failures are raised **before any run is created**.
|
|
182
231
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipelex/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "TypeScript SDK for the Pipelex hosted API — execute methods, manage runs, and call the product surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"import": "./dist/index.js",
|
|
20
20
|
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./upload": {
|
|
23
|
+
"types": "./dist/upload-grant.d.ts",
|
|
24
|
+
"import": "./dist/upload-grant.js",
|
|
25
|
+
"default": "./dist/upload-grant.js"
|
|
21
26
|
}
|
|
22
27
|
},
|
|
23
28
|
"scripts": {
|