@pipelex/sdk 0.21.0 → 0.23.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.
@@ -1,8 +1,8 @@
1
- # Artifact download (`collectArtifacts` / `resolveArtifacts` / `fetchArtifact` / `downloadArtifacts`)
1
+ # Artifact download (`locateArtifacts` / `collectArtifacts` / `resolveArtifacts` / `fetchArtifact` / `downloadArtifacts`)
2
2
 
3
- > **Status: implemented** (`src/artifacts.ts`). This is the download twin of [input preparation](./input-preparation.md): where `prepareInputs` turns local files into `pipelex-storage://` references before a run, these four operations turn the references a run produced back into bytes on disk, or into a bounded stream, afterwards. They are layered so that each is usable without the next.
3
+ > **Status: implemented** (`src/artifacts.ts`). This is the download twin of [input preparation](./input-preparation.md): where `prepareInputs` turns local files into `pipelex-storage://` references before a run, these operations turn the references a run produced back into bytes on disk, or into a bounded stream, afterwards. They are layered so that each is usable without the next.
4
4
  >
5
- > **They need a platform that serves the bulk resolve route.** Everything below `collectArtifacts` mints its links through `POST /v1/resolve-storage-url/bulk`, a hosted-platform route. The public bare runner (`pipelex-api`) has no resolve route at all, single or bulk, and a hosted deployment that predates the route answers a `404`; in both cases the operation throws the existing `ApiResponseError` and nothing is downloaded. `resolveStorageUrl`, the single-reference primitive, stays for the callers that have one link to mint.
5
+ > **They need a platform that serves the bulk resolve route.** Everything below the pure walk mints its links through `POST /v1/resolve-storage-url/bulk`, a hosted-platform route. The public bare runner (`pipelex-api`) has no resolve route at all, single or bulk, and a hosted deployment that predates the route answers a `404`; in both cases the operation throws the existing `ApiResponseError` and nothing is downloaded. `resolveStorageUrl`, the single-reference primitive, stays for the callers that have one link to mint.
6
6
 
7
7
  ## Why this exists
8
8
 
@@ -12,20 +12,28 @@ Downloading is **explicit and separate from running**, the input-preparation rul
12
12
 
13
13
  Nothing here reads the embedded `public_url`. Every link is minted fresh by the platform, which is what makes a download work long after the embedded link died, and what keeps the tenant boundary where the platform enforces it.
14
14
 
15
- ## The four operations
15
+ ## The operations
16
16
 
17
- ### `collectArtifacts(value)` — the pure walk
17
+ ### `locateArtifacts(value)` and `collectArtifacts(value)` — the pure walk
18
18
 
19
19
  ```ts
20
- import { collectArtifacts } from "@pipelex/sdk";
20
+ import { collectArtifacts, locateArtifacts } from "@pipelex/sdk";
21
+
22
+ const locations = locateArtifacts(results.main_stuff);
23
+ // [{ uri: "pipelex-storage://org/runs/01J…/outputs/2325fcfe.png",
24
+ // found_at: ["$.rooms[0].staged_photo.url"] }, …]
21
25
 
22
26
  const uris = collectArtifacts(results.main_stuff);
23
- // ["pipelex-storage://org/runs/01J…/outputs/illustration.png", …]
27
+ // ["pipelex-storage://org/runs/01J…/outputs/2325fcfe.png", …] — the same walk, references only
24
28
  ```
25
29
 
26
- Walks any JSON-shaped value and returns every string that **is** a `pipelex-storage://` reference — the whole string, scheme first, with something after the scheme. A string that merely contains a reference does not count; the bare scheme does not count; nothing else is looked at. The result is deduplicated and kept in discovery order. It is a contract rather than a heuristic, because the scheme is unambiguous: the runtime serializes a produced file as content carrying its reference in `url`, and nothing else on the wire starts that way.
30
+ Walks any JSON-shaped value and returns every string that **is** a `pipelex-storage://` reference — the whole string, scheme first, with something after the scheme. A string that merely contains a reference does not count; the bare scheme does not count; nothing else is looked at. The result is deduplicated and kept in discovery order, the order of each reference's first sighting. It is a contract rather than a heuristic, because the scheme is unambiguous: the runtime serializes a produced file as content carrying its reference in `url`, and nothing else on the wire starts that way.
31
+
32
+ `locateArtifacts` also says where each reference sits. Its `found_at` lists every path at which the reference occurs, in walk order, so a reference the output repeats is one entry with several paths, and `found_at[0]` is where it was first seen — the path a saved file is named after. `collectArtifacts` is the same walk's references alone.
27
33
 
28
- It is pure — no network, no key — and exported standalone, so a consumer can count or list a result's files without resolving any of them.
34
+ **Path notation.** A path is rooted at `$`, the walked value itself. An object key matching `^[A-Za-z_][A-Za-z0-9_]*$` is written `.key`, any other key `["…"]` in JSON string escaping, and an array index `[n]`. So a nested field reads `$.rooms[3].staged_photo.url`, a list member `$.items[0].url` (a list output arrives as the `{ items: [...] }` envelope), a key that is not an identifier `$["a key"].url`, and an output that is itself a reference `$`. A path is the exact location of the string, the final `url` of a content object included, so a consumer can follow it into the JSON without guessing.
35
+
36
+ Both are pure — no network, no key — and exported standalone, so a consumer can count, list or place a result's files without resolving any of them.
29
37
 
30
38
  ### `resolveArtifacts(uris)` — fresh links for a whole list
31
39
 
@@ -100,7 +108,34 @@ if (!verdict.all_saved) {
100
108
 
101
109
  **How it downloads.** The whole set is resolved through the bulk route ahead of the workers, then a bounded number of workers (`concurrency`, default 4) each take the next reference and fetch it → open its file with `wx` → stream the body in. Resolution is just-in-time where it matters: a link that has expired by the time its worker reaches it — a large set downloaded a few at a time can outlive the fifteen-minute link — is resolved again for that reference alone, so no fetch ever runs on a stale signature.
102
110
 
103
- **Filenames.** Each file is named by `artifactFilename` (exported): the last segment of the storage key, reduced to `[A-Za-z0-9._-]` with leading dots stripped so it can never name anything outside `dir`, capped in length with the extension preserved, given an extension from the content type when the key has none, and falling back to `artifact-N`. Files are **never overwritten**: a name already on disk gets a numeric suffix (`report-1.pdf`, `report-2.pdf`), through exclusive creation rather than an exists-check, so two workers cannot race for one name. `dir` is created if missing.
111
+ **Filenames: each file is named after the field it fills.** The name comes from the first path in the reference's `found_at`, by the rule `artifactFilename(location, contentType, scope)` (exported) applies:
112
+
113
+ 1. A final `url` key is dropped, since the runtime's image and document contents carry their reference there. A reference under any other key keeps that key, and a `url` key that is not final is kept.
114
+ 2. Each key is reduced to `[A-Za-z0-9_]`, every other character becoming `_` — `-` and `.` included, since one is the separator and the other would fake an extension. An index stays its digits.
115
+ 3. The segments are joined with `-`. A reference that is the walked value itself, or its `url`, has no segment left and takes the scope's name, so an output that is one image is saved as `main_stuff.png`.
116
+ 4. A name over the length cap (128 characters, extension included) keeps its tail: whole leading segments are dropped first, since the last ones are the specific ones, and a single segment still too long is cut to fit.
117
+ 5. A stem Windows reserves for a device — `con`, `prn`, `aux`, `nul`, `com0` to `com9` or `lpt0` to `lpt9`, in any case — gets a trailing `_`, so a field named `aux` is saved as `aux_.png`. Windows reserves those names whatever the extension, and a field name is the method author's to choose.
118
+ 6. The extension is the one the storage key's last segment carries, reduced to `[A-Za-z0-9]`, when it has a short one; otherwise the content type's, for the types a run produces (`image/png` gives `.png`, `application/pdf` gives `.pdf`); otherwise there is none.
119
+
120
+ For example, a home-staging method whose output is
121
+
122
+ ```json
123
+ {
124
+ "rooms": [
125
+ {
126
+ "original_photo": { "url": "pipelex-storage://org/assets/53174b03.png", "public_url": "…" },
127
+ "staged_photo": { "url": "pipelex-storage://org/runs/01J…/outputs/2325fcfe.png", "public_url": "…" }
128
+ },
129
+ { "original_photo": { "url": "…" }, "staged_photo": { "url": "…" } }
130
+ ]
131
+ }
132
+ ```
133
+
134
+ is saved as `rooms-0-original_photo.png`, `rooms-0-staged_photo.png`, `rooms-1-original_photo.png` and `rooms-1-staged_photo.png`, where the storage keys alone (`53174b03.png`, `2325fcfe.png`) would not say which picture is which. Each verdict item's `found_at` carries the unreduced path, `$.rooms[0].staged_photo.url`.
135
+
136
+ The result is always a bare filename — ASCII letters, digits, `_` and the `-` joins, then an optional extension — never empty, never starting with a dot and never a device name, so it can name nothing but a regular file directly inside `dir`. `artifactFilename` takes a location from `locateArtifacts`, which is how a consumer predicts a name before downloading; it throws `ArtifactOperationError` for a location with no string `uri`, no `found_at` array, or one whose `found_at[0]` is not a path in the notation above, or for an unknown scope.
137
+
138
+ Files are **never overwritten**: a name already on disk gets a numeric suffix (`report-1.pdf`, `report-2.pdf`), through exclusive creation rather than an exists-check, so two workers cannot race for one name. Two references whose paths reduce to one name (`"staged photo"` and `"staged-photo"`) are told apart by the same suffix, and the verdict says which file is which. A reference found at several paths is saved once, under the name of the first. `dir` is created if missing.
104
139
 
105
140
  **Cleanup.** A failed or aborted download unlinks its partial file; nothing truncated is ever left under a final name.
106
141
 
@@ -110,8 +145,8 @@ if (!verdict.all_saved) {
110
145
  {
111
146
  scope: "main_stuff" | "working_memory",
112
147
  artifacts: [
113
- { uri, path, content_type, size, error: null }, // saved — path is absolute
114
- { uri, path: null, content_type, size: null, error: { code, detail } }, // not saved
148
+ { uri, found_at, path, content_type, size, error: null }, // saved — path is absolute
149
+ { uri, found_at, path: null, content_type, size: null, error: { code, detail } }, // not saved
115
150
  ],
116
151
  saved_paths: [ /* the absolute paths of the saved ones, same order */ ],
117
152
  all_saved: boolean, // every reference saved; vacuously true for an empty walk
@@ -119,7 +154,7 @@ if (!verdict.all_saved) {
119
154
  }
120
155
  ```
121
156
 
122
- `artifacts.length` is the count of references walked, errors included. An empty walk over a present scope — an output that references no stored file — is a verdict with empty lists and `all_saved: true`, not an error, and it touches neither the network nor the disk. `content_type` is the platform's guess from the reference, known before the fetch, on both arms.
157
+ `artifacts.length` is the count of references walked, errors included. An empty walk over a present scope — an output that references no stored file — is a verdict with empty lists and `all_saved: true`, not an error, and it touches neither the network nor the disk. `found_at` is the reference's paths in the walked scope, exactly as `locateArtifacts` reports them, and `content_type` is the platform's guess from the reference, known before the fetch; both are on both arms, so an item that was not saved still says which field it would have filled.
123
158
 
124
159
  Per-item `error.code` is the fetch vocabulary above plus the download's own: `resolve_failed` (an expired link could not be re-resolved, for a reason that is not the credential), `total_limit_exceeded` (the item that would take the call past `maxTotalBytes`, and, once the files already saved leave no room, every item not yet started, whose `detail` says it was skipped; an item refused only because files still in flight hold the room stops nothing else, since one of them may yet fail and give it back), `write_failed` (the file could not be created, written or closed) and `aborted` (in flight or not yet started when the signal fired).
125
160
 
@@ -128,7 +163,7 @@ Per-item `error.code` is the fetch vocabulary above plus the download's own: `re
128
163
  - `RunStillRunningError` (with the retry hint) or `RunFailedError` — a `run_id` naming a run that has not completed;
129
164
  - `ScopeUnavailableError` — the requested scope's artifact is `null` or missing from the body (`scope` and `runId` on the error). Reading by `run_id`, a null `main_stuff` is already `MissingMainStuffError` from `getRunResult`;
130
165
  - `ArtifactAuthenticationError` — the resolve route refused the credential (`401` / `403`), on the first resolve or on a re-resolve part-way through. It carries `verdict`, the result as it stood: the refusal stops the workers taking new items but lets the fetches already running finish, since they are on presigned links that do not carry the credential, so every file saved is real and listed and the rest are marked `aborted` with a detail naming the credential failure;
131
- - `ArtifactOperationError` — outside Node, an unusable `dir`, both selectors or neither, or nonsense bounds;
166
+ - `ArtifactOperationError` — outside Node, an unusable `dir`, both selectors or neither, an unknown `scope`, or nonsense bounds;
132
167
  - and the transport and lifecycle errors of the reads it makes, unchanged: `ApiResponseError` for a deployment without the bulk route, `RunLifecycleUnavailableError` for a bare runner asked by `run_id`, `ApiUnreachableError`.
133
168
 
134
169
  Everything else that can go wrong with one reference is that reference's `error`.
@@ -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 two operations
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 the SDK for a browser or edge target currently requires marking `node:*` external. This is a deliberate stance — every consumer runs the SDK server-side in Node (Next.js Server Actions, the MCP server, the plugins hook) — revisited only if the SDK is ever imported into a browser client bundle or an edge route.
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, not a raw transport error);
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
 
@@ -201,4 +201,4 @@ const resolved = await client.resolveStorageUrl({ uri: "pipelex-storage://..." }
201
201
 
202
202
  The same rule holds in a browser: resolve on the server, hand the client a link it uses immediately, and never let a presigned URL outlive the request it was minted for.
203
203
 
204
- Bringing the files down is the SDK's job, not something to re-implement over `resolveStorageUrl`: `collectArtifacts` lists a result's references without touching the network, `resolveArtifacts` mints fresh links for a whole list in one bulk call, `fetchArtifact` streams one file within safe bounds, and `downloadArtifacts` saves everything a run produced under a directory, by run id even days later. All four are on their own page, [`artifact-download.md`](./artifact-download.md). The upload direction — turning local files into `pipelex-storage://` references before a run — is the mirror of this and has its own page too, [`input-preparation.md`](./input-preparation.md).
204
+ Bringing the files down is the SDK's job, not something to re-implement over `resolveStorageUrl`: `locateArtifacts` lists a result's references without touching the network, each with the paths of the fields it fills (`collectArtifacts` gives the references alone), `resolveArtifacts` mints fresh links for a whole list in one bulk call, `fetchArtifact` streams one file within safe bounds, and `downloadArtifacts` saves everything a run produced under a directory, each file named after the field it fills, by run id even days later. They all share one page, [`artifact-download.md`](./artifact-download.md). The upload direction — turning local files into `pipelex-storage://` references before a run — is the mirror of this and has its own page too, [`input-preparation.md`](./input-preparation.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipelex/sdk",
3
- "version": "0.21.0",
3
+ "version": "0.23.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": {