@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.
package/README.md CHANGED
@@ -84,6 +84,23 @@ const client = new PipelexApiClient({ appInfo: { name: "acme-invoicer", version:
84
84
  // User-Agent: acme-invoicer/1.4.0 pipelex-sdk-js/<version> node/<version> (<os>; <arch>)
85
85
  ```
86
86
 
87
+ ### Uploading from a browser
88
+
89
+ A browser page that holds a file but not the API key can still store it: the server that holds the key asks for an upload grant, and the page sends the file straight to storage with it. The file never crosses your server or the API gateway, so it can be as large as the service's own limit. The page imports `@pipelex/sdk/upload`, the browser-safe entry, which bundles with no Node builtin to mark external; the main `@pipelex/sdk` entry is Node-first.
90
+
91
+ ```ts
92
+ // On the server, which holds the key:
93
+ const grant = await client.requestUploadGrant({
94
+ filename: "report.pdf",
95
+ content_type: "application/pdf",
96
+ size: 48213,
97
+ });
98
+
99
+ // In the page, which holds the file:
100
+ import { uploadWithGrant } from "@pipelex/sdk/upload";
101
+ const { uri } = await uploadWithGrant(grant, file); // pipelex-storage://…
102
+ ```
103
+
87
104
  The full client surface is documented in [`docs/architecture.md`](./docs/architecture.md).
88
105
 
89
106
  ## Documentation
@@ -95,8 +112,8 @@ These pages ship inside the published package, so a reader who has only installe
95
112
  | [`docs/architecture.md`](./docs/architecture.md) | The whole client surface: the request pipeline, every route, the typed errors |
96
113
  | [`docs/run-results.md`](./docs/run-results.md) | Every field of `RunResults` — the run id as a durable handle, `main_stuff`, `working_memory`, `graph_spec`, the usage pair, produced files |
97
114
  | [`docs/run-usage.md`](./docs/run-usage.md) | What a run consumed, record by record, and `summarizeUsage` which folds them into one reading |
98
- | [`docs/artifact-download.md`](./docs/artifact-download.md) | Turning the `pipelex-storage://` references a run produced back into bytes: `collectArtifacts`, `resolveArtifacts`, `fetchArtifact`, `downloadArtifacts` |
99
- | [`docs/input-preparation.md`](./docs/input-preparation.md) | The other direction — `uploadFile` and `prepareInputs`, which turn local files into references a run can take |
115
+ | [`docs/artifact-download.md`](./docs/artifact-download.md) | Turning the `pipelex-storage://` references a run produced back into bytes: `locateArtifacts`, `collectArtifacts`, `resolveArtifacts`, `fetchArtifact`, `downloadArtifacts`, and how a saved file is named after the field it fills |
116
+ | [`docs/input-preparation.md`](./docs/input-preparation.md) | The other direction — `uploadFile` and `prepareInputs`, which turn local files into references a run can take, and the upload grant a browser page sends a file with |
100
117
  | [`docs/crate-routes.md`](./docs/crate-routes.md) | `resolve` and `codegen`, and the offline `runCodegenCheck` that guards a committed tree |
101
118
  | [`docs/build-routes.md`](./docs/build-routes.md) | The `/v1/build/*` projections: `buildInputs`, `buildOutput`, `buildRunner` |
102
119
  | [`docs/client-identification.md`](./docs/client-identification.md) | The `User-Agent` every API request carries, and `appInfo`, the option that puts your program's name in front of it |
@@ -2,8 +2,9 @@
2
2
  * The artifact stack — the download twin of `prepareInputs`, in layers so each
3
3
  * operation is usable without the next:
4
4
  *
5
- * - {@link collectArtifacts} — a pure walk of any JSON value for the strings that
6
- * ARE `pipelex-storage://` references. No network, no key.
5
+ * - {@link locateArtifacts} — a pure walk of any JSON value for the strings that
6
+ * ARE `pipelex-storage://` references, each with every path it sits at.
7
+ * {@link collectArtifacts} is the same walk's bare references. No network, no key.
7
8
  * - {@link resolveArtifacts} — the platform's bulk resolve route over a whole
8
9
  * list, chunked at the route's bound, one verdict per reference.
9
10
  * - {@link fetchArtifact} — a bounded `Response` for one reference: resolved
@@ -20,7 +21,7 @@
20
21
  *
21
22
  * `node:fs/promises`, `node:path` and `undici` are imported dynamically, behind
22
23
  * `isNodeRuntime()`, so the module's static import graph stays free of Node
23
- * builtins: `collectArtifacts` and `resolveArtifacts` run anywhere, `fetchArtifact`
24
+ * builtins: the walk and `resolveArtifacts` run anywhere, `fetchArtifact`
24
25
  * runs anywhere the global `fetch` does (the dispatcher is a Node refinement), and
25
26
  * only `downloadArtifacts` touches a filesystem.
26
27
  */
@@ -135,14 +136,36 @@ export type DownloadArtifactsRequest = DownloadArtifactsOptions & ({
135
136
  results: RunResults;
136
137
  run_id?: never;
137
138
  });
139
+ /**
140
+ * Where one `pipelex-storage://` reference sits in a walked value — what
141
+ * {@link locateArtifacts} answers per reference. `found_at` lists every path at
142
+ * which the reference occurs, in walk order, and is never empty: `found_at[0]` is
143
+ * where it was first seen, and the path a saved file is named after.
144
+ *
145
+ * A path is `$`-rooted: `$` is the walked value itself, an object key matching
146
+ * `^[A-Za-z_][A-Za-z0-9_]*$` is `.key`, any other key is `["…"]` in JSON string
147
+ * escaping, and an array index is `[n]` — `$.rooms[3].staged_photo.url`,
148
+ * `$.items[0].url`, `$["a key"].url`. It is the exact path of the string, the
149
+ * final `url` of a content object included.
150
+ */
151
+ export interface ArtifactLocation {
152
+ /** The reference, exactly as it appears in the walked value. */
153
+ uri: string;
154
+ /** Every `$`-rooted path at which the reference occurs, in walk order. */
155
+ found_at: string[];
156
+ }
138
157
  /**
139
158
  * One reference's outcome in a download verdict — one shape with nullable fields,
140
159
  * like {@link ResolvedArtifact}: either `path` and `size` are set and `error` is
141
- * null, or `error` is set and both are null. `content_type` is the platform's
142
- * guess from the reference's extension, known before the fetch, on both arms.
160
+ * null, or `error` is set and both are null. `found_at` says where the reference
161
+ * sits in the walked scope, and `content_type` is the platform's guess from the
162
+ * reference's extension, known before the fetch; both are on both arms, so an
163
+ * item that was not saved still says which field it would have filled.
143
164
  */
144
165
  export type DownloadedArtifact = {
145
166
  uri: string;
167
+ /** Every `$`-rooted path in the walked scope where the reference sits; the first one named the file. */
168
+ found_at: string[];
146
169
  /** Absolute path of the written file. */
147
170
  path: string;
148
171
  content_type: string | null;
@@ -151,6 +174,8 @@ export type DownloadedArtifact = {
151
174
  error: null;
152
175
  } | {
153
176
  uri: string;
177
+ /** Every `$`-rooted path in the walked scope where the reference sits. */
178
+ found_at: string[];
154
179
  path: null;
155
180
  content_type: string | null;
156
181
  size: null;
@@ -185,32 +210,68 @@ export interface ArtifactCapableClient {
185
210
  signal?: AbortSignal;
186
211
  }): Promise<RunResultState>;
187
212
  }
213
+ /**
214
+ * Every `pipelex-storage://` reference inside a JSON-shaped value, each with
215
+ * every path at which it occurs. The references are deduplicated and kept in
216
+ * discovery order (the order of their first sighting), and each one's `found_at`
217
+ * lists its paths in walk order, so `found_at[0]` is where it was first seen.
218
+ *
219
+ * The string test is {@link collectArtifacts}'s: a string counts only when it IS
220
+ * a reference. A path is rooted at `$`, the walked value itself; an object key
221
+ * matching `^[A-Za-z_][A-Za-z0-9_]*$` is written `.key`, any other key `["…"]` in
222
+ * JSON string escaping, and an array index `[n]`. The runtime serializes a
223
+ * produced image or document as content carrying its reference in `url`, so a
224
+ * typical path ends there: `$.rooms[3].staged_photo.url`, `$.items[0].url`, or
225
+ * `$.url` for an output that is one image. Pure — no network, no key.
226
+ */
227
+ export declare function locateArtifacts(value: unknown): ArtifactLocation[];
188
228
  /**
189
229
  * Every `pipelex-storage://` reference inside a JSON-shaped value, deduplicated,
190
- * in discovery order. A string counts only when it IS a reference — the whole
191
- * string, scheme first, with something after the scheme; text that merely
192
- * contains one does not. The scheme is unambiguous, so this walk is a contract
193
- * rather than a heuristic: the runtime serializes a produced image or document
194
- * as content carrying its reference in `url`, beside an expiring `public_url`
195
- * this walk ignores. Pure — no network, no key — so a consumer can count or
196
- * list a run's produced files without resolving any of them.
230
+ * in discovery order — the references of {@link locateArtifacts}, without their
231
+ * paths. A string counts only when it IS a reference — the whole string, scheme
232
+ * first, with something after the scheme; text that merely contains one does
233
+ * not. The scheme is unambiguous, so this walk is a contract rather than a
234
+ * heuristic: the runtime serializes a produced image or document as content
235
+ * carrying its reference in `url`, beside an expiring `public_url` this walk
236
+ * ignores. Pure — no network, no key — so a consumer can count or list a run's
237
+ * produced files without resolving any of them.
197
238
  */
198
239
  export declare function collectArtifacts(value: unknown): string[];
199
240
  /** A string that is a storage reference: the scheme, then at least one character. */
200
241
  export declare function isStorageReference(value: string): boolean;
201
242
  /**
202
- * The bare filename a storage reference is saved under: the last segment of the
203
- * storage key, reduced to a conservative character set so it can never name
204
- * anything but a regular file directly inside the target directory. Path
205
- * separators are the split point, so no traversal survives; leading dots are
206
- * stripped, so no hidden file and no `..`; everything outside `[A-Za-z0-9._-]`
207
- * becomes `_`; an empty result falls back to a numbered `artifact-N`. Length is
208
- * capped with the extension preserved, and an extension is added from the
209
- * content type when the key carries none. A collision on disk is not this
243
+ * The bare filename a reference is saved under, named after the field it fills:
244
+ * the path in `location.found_at[0]`, where the reference was first seen.
245
+ *
246
+ * 1. A final object key `url` is dropped, since the runtime's image and
247
+ * document contents carry their reference there; a reference under any other
248
+ * key keeps that key, and a `url` key that is not final is kept.
249
+ * 2. Each key is reduced to `[A-Za-z0-9_]`, every other character (`-` and `.`
250
+ * included) becoming `_`; an index stays its decimal digits.
251
+ * 3. The segments are joined with `-`. An empty result — the reference is the
252
+ * walked value itself, or its `url` — becomes the scope's name.
253
+ * 4. Over the filename length cap (`MAX_FILENAME_LENGTH`, extension included),
254
+ * the tail is kept: whole leading segments are dropped first, since the last
255
+ * ones are the specific ones, and a single segment still too long is cut to
256
+ * fit.
257
+ * 5. A stem Windows reserves for a device (`con`, `prn`, `aux`, `nul`,
258
+ * `com0`–`com9`, `lpt0`–`lpt9`, in any case) gets a trailing `_`, so
259
+ * `$.aux.url` is saved as `aux_.png`.
260
+ * 6. The extension is the storage key's own, reduced to `[A-Za-z0-9]`, when it
261
+ * has a short one; otherwise the content type's, for the types a run
262
+ * produces; otherwise there is none.
263
+ *
264
+ * So `$.rooms[3].staged_photo.url` is saved as `rooms-3-staged_photo.png`, and
265
+ * `$.url` in `main_stuff` as `main_stuff.png`. The stem is ASCII letters, digits,
266
+ * `_` and the `-` joins, never empty and never a device name, so the name can
267
+ * only ever be a regular file directly inside the target directory. A collision on disk is not this
210
268
  * function's concern: `downloadArtifacts` suffixes the stem (`name-1.ext`) on
211
269
  * exclusive creation, so a file is never overwritten.
270
+ *
271
+ * Throws `ArtifactOperationError` for a location whose `found_at[0]` is not a path
272
+ * in the notation {@link locateArtifacts} writes, or for an unknown scope.
212
273
  */
213
- export declare function artifactFilename(uri: string, contentType: string | null | undefined, index: number): string;
274
+ export declare function artifactFilename(location: ArtifactLocation, contentType: string | null | undefined, scope: ArtifactScope): string;
214
275
  /**
215
276
  * Resolve a list of references through the bulk route, chunked at
216
277
  * {@link BULK_RESOLVE_MAX_URIS} per request, and answer one
@@ -264,19 +325,20 @@ export declare function writeFully(writer: ChunkWriter, chunk: Uint8Array): Prom
264
325
  * Save a run's produced files under `dir` — Node-only, like the path-string arm
265
326
  * of `uploadFile`. Takes a `run_id` (the results are re-read, so a run is
266
327
  * downloadable days later) or a `RunResults` in hand; walks the requested scope
267
- * with {@link collectArtifacts}; resolves the whole set through the bulk route
328
+ * with {@link locateArtifacts}; resolves the whole set through the bulk route
268
329
  * ahead of the workers; then a bounded pool of workers each fetch → open with
269
330
  * `wx` → write, re-resolving any link that has expired by the time a worker
270
- * reaches it. The embedded `public_url` is never used. Files are named by
271
- * {@link artifactFilename} and never overwritten; a failed or aborted download
272
- * unlinks its partial file.
331
+ * reaches it. The embedded `public_url` is never used. Each file is named after
332
+ * the field it fills, by {@link artifactFilename}'s rule, and never overwritten;
333
+ * a failed or aborted download unlinks its partial file.
273
334
  *
274
335
  * Returns a produced verdict: one entry per reference, errors as values. It
275
336
  * throws only when no verdict can be produced — `RunStillRunningError` or
276
337
  * `RunFailedError` for a run that has not completed, `ScopeUnavailableError`
277
338
  * when the scope's artifact is null or missing, `ArtifactAuthenticationError`
278
339
  * (carrying the verdict so far) when the resolve route refuses the credential,
279
- * `ArtifactOperationError` outside Node or for an unusable `dir`, and the
340
+ * `ArtifactOperationError` outside Node, for an unusable `dir` or for an
341
+ * unknown `scope`, and the
280
342
  * transport and lifecycle errors of the reads it makes (`ApiResponseError` for
281
343
  * a deployment without the bulk route, `RunLifecycleUnavailableError` for a
282
344
  * bare runner asked by id, `ApiUnreachableError`).