@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 +19 -2
- package/dist/artifacts.d.ts +88 -26
- package/dist/artifacts.js +305 -100
- package/dist/artifacts.js.map +1 -1
- package/dist/client.d.ts +23 -3
- package/dist/client.js +25 -4
- 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 +6 -3
- package/dist/index.js +7 -4
- 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 +7 -4
- package/docs/artifact-download.md +49 -14
- package/docs/input-preparation.md +54 -5
- package/docs/run-results.md +1 -1
- package/package.json +6 -1
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,6FAA6F;AAC7F,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,oFAAoF;AACpF,kFAAkF;AAClF,mFAAmF;AACnF,wBAAwB;AACxB,cAAc,gBAAgB,CAAC;AAE/B,wEAAwE;AACxE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAarE,+FAA+F;AAC/F,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,iGAAiG;AACjG,kFAAkF;AAClF,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,6FAA6F;AAC7F,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,oFAAoF;AACpF,kFAAkF;AAClF,mFAAmF;AACnF,wBAAwB;AACxB,cAAc,gBAAgB,CAAC;AAE/B,wEAAwE;AACxE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAarE,+FAA+F;AAC/F,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,iGAAiG;AACjG,kFAAkF;AAClF,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAK/E,yFAAyF;AACzF,sEAAsE;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AASpD,uEAAuE;AACvE,wFAAwF;AACxF,0FAA0F;AAC1F,iFAAiF;AACjF,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,qBAAqB,EACrB,0BAA0B,EAC1B,2BAA2B,EAC3B,gCAAgC,EAChC,4BAA4B,GAC7B,MAAM,gBAAgB,CAAC;AAiFxB,+EAA+E;AAC/E,4EAA4E;AAC5E,iFAAiF;AACjF,0EAA0E;AAC1E,OAAO,EACL,eAAe,EACf,uBAAuB,EACvB,2BAA2B,EAC3B,gBAAgB,GACjB,MAAM,oBAAoB,CAAC;AAmD5B,wEAAwE;AACxE,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAWpE,oFAAoF;AACpF,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAS5C,+EAA+E;AAC/E,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,yBAAyB,EACzB,qBAAqB,EACrB,2BAA2B,EAC3B,cAAc,EACd,eAAe,EACf,oBAAoB,EACpB,4BAA4B,EAC5B,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,gCAAgC,EAChC,yBAAyB,EACzB,oBAAoB,EACpB,sBAAsB,EACtB,2BAA2B,EAC3B,kBAAkB,EAClB,qBAAqB,GACtB,MAAM,aAAa,CAAC"}
|
package/dist/product-models.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Pipelex-product wire models — the snake_case JSON shapes the hosted-product
|
|
3
3
|
* routes (`/v1/me`, `/v1/methods`, `/v1/organizations`, `/v1/billing/*`,
|
|
4
4
|
* `/v1/pipelex-api-keys`, `/v1/gateway-api-key`, `/v1/onboarding/submit`,
|
|
5
|
-
* `/v1/resolve-storage-url`, `/v1/upload`, `/v1/runs`) speak.
|
|
5
|
+
* `/v1/resolve-storage-url`, `/v1/upload`, `/v1/upload/grant`, `/v1/runs`) speak.
|
|
6
6
|
*
|
|
7
7
|
* These are the management surface a consumer (today `pipelex-app`) hand-rolls.
|
|
8
8
|
* The wire is snake_case; any camelCase remap is a consumer's UI concern and
|
|
@@ -235,6 +235,47 @@ export interface UploadedFile {
|
|
|
235
235
|
uri: string;
|
|
236
236
|
filename: string;
|
|
237
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* What an upload grant is requested for — `POST /v1/upload/grant`. The file is
|
|
240
|
+
* described, never sent: its bytes go to storage with the grant, from wherever
|
|
241
|
+
* they are held.
|
|
242
|
+
*/
|
|
243
|
+
export interface UploadGrantInput {
|
|
244
|
+
/** The original filename with its extension; the stored object keeps the extension. */
|
|
245
|
+
filename: string;
|
|
246
|
+
/**
|
|
247
|
+
* MIME type, signed into the grant so the upload must carry it. Omitted, `null`
|
|
248
|
+
* or empty, none is signed — a browser's `File.type` is empty for a type it
|
|
249
|
+
* does not know, and means the same.
|
|
250
|
+
*/
|
|
251
|
+
content_type?: string | null;
|
|
252
|
+
/** The file's exact size in bytes. The grant accepts a body of this size and no other. */
|
|
253
|
+
size: number;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* A presigned, create-only `PUT` for one new object in the caller's organization,
|
|
257
|
+
* and the `pipelex-storage://` URI that object will carry. A bearer capability
|
|
258
|
+
* until `expires_at`: whoever holds it can write that one object, once, so it is
|
|
259
|
+
* never logged. `uploadWithGrant` (also on the browser-safe `@pipelex/sdk/upload`
|
|
260
|
+
* entry) is the sender.
|
|
261
|
+
*/
|
|
262
|
+
export interface UploadGrant {
|
|
263
|
+
/** The reference the object carries — it names nothing until the `PUT` has succeeded. */
|
|
264
|
+
uri: string;
|
|
265
|
+
/** Where to `PUT` the file, as the raw request body. */
|
|
266
|
+
url: string;
|
|
267
|
+
/**
|
|
268
|
+
* The signed headers to send unchanged: `If-None-Match: *`, the `Content-Type`
|
|
269
|
+
* when one was declared, and the provenance `x-amz-meta-*` values. `Content-Length`
|
|
270
|
+
* is signed too but is not listed — the body sets it, so the body must be
|
|
271
|
+
* exactly the declared size.
|
|
272
|
+
*/
|
|
273
|
+
headers: Record<string, string>;
|
|
274
|
+
/** ISO-8601 UTC instant after which storage refuses the `PUT`. */
|
|
275
|
+
expires_at: string;
|
|
276
|
+
/** The largest file any grant allows, in bytes (the service's upload cap). */
|
|
277
|
+
max_bytes: number;
|
|
278
|
+
}
|
|
238
279
|
/** Per-pipe progress marker surfaced in a run's `pipe_statuses` map. */
|
|
239
280
|
export type PipeStatus = "scheduled" | "running" | "succeeded" | "failed" | "skipped";
|
|
240
281
|
/**
|
package/dist/product-models.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Pipelex-product wire models — the snake_case JSON shapes the hosted-product
|
|
3
3
|
* routes (`/v1/me`, `/v1/methods`, `/v1/organizations`, `/v1/billing/*`,
|
|
4
4
|
* `/v1/pipelex-api-keys`, `/v1/gateway-api-key`, `/v1/onboarding/submit`,
|
|
5
|
-
* `/v1/resolve-storage-url`, `/v1/upload`, `/v1/runs`) speak.
|
|
5
|
+
* `/v1/resolve-storage-url`, `/v1/upload`, `/v1/upload/grant`, `/v1/runs`) speak.
|
|
6
6
|
*
|
|
7
7
|
* These are the management surface a consumer (today `pipelex-app`) hand-rolls.
|
|
8
8
|
* The wire is snake_case; any camelCase remap is a consumer's UI concern and
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `uploadWithGrant` — send one file straight to Pipelex storage with an upload
|
|
3
|
+
* grant (`PipelexApiClient.requestUploadGrant`, `POST /v1/upload/grant`). The
|
|
4
|
+
* side that holds the credential requests the grant; the side that holds the
|
|
5
|
+
* bytes, typically a browser page, sends them with this. See
|
|
6
|
+
* `docs/input-preparation.md`.
|
|
7
|
+
*
|
|
8
|
+
* **This module is the browser-safe entry, `@pipelex/sdk/upload`.** Its runtime
|
|
9
|
+
* import graph must never reach a Node builtin, `undici` or the client: a page
|
|
10
|
+
* bundles it as is, with nothing marked external. It imports the error classes
|
|
11
|
+
* and the wire types only, and `tests/upload-grant.test.ts` bundles it for the
|
|
12
|
+
* browser with esbuild to hold it to that.
|
|
13
|
+
*/
|
|
14
|
+
import type { UploadGrant } from "./product-models.js";
|
|
15
|
+
export type { UploadGrant, UploadGrantInput } from "./product-models.js";
|
|
16
|
+
export { InputPreparationError, RejectedAssetError, UploadTransportError } from "./errors.js";
|
|
17
|
+
export type { RejectedAssetCode } from "./errors.js";
|
|
18
|
+
/** Per-call options for {@link uploadWithGrant}. */
|
|
19
|
+
export interface UploadWithGrantOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Cancels the upload. A caller's abort propagates as the signal's own reason,
|
|
22
|
+
* never wrapped, so it stays distinguishable from a failure.
|
|
23
|
+
*/
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}
|
|
26
|
+
/** What a successful {@link uploadWithGrant} stored. */
|
|
27
|
+
export interface GrantedUpload {
|
|
28
|
+
/** The grant's `pipelex-storage://` reference, which now names the stored object. */
|
|
29
|
+
uri: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `PUT` a file to storage with an upload grant, and return the reference it now
|
|
33
|
+
* carries. The file goes as the raw body with the grant's signed headers
|
|
34
|
+
* unchanged; the body sets `Content-Length`, which the grant signed, so the file
|
|
35
|
+
* must be exactly the size the grant was requested for.
|
|
36
|
+
*
|
|
37
|
+
* Storage's refusals map onto the input-preparation errors, so a caller that
|
|
38
|
+
* already handles `uploadFile` needs no new case:
|
|
39
|
+
*
|
|
40
|
+
* - `RejectedAssetError` (`status` is storage's, `code` says why) — a `412` when
|
|
41
|
+
* the grant was already used (`grant_used`: a grant writes one object, once); a
|
|
42
|
+
* `403` when the file's size, type or metadata differ from what the grant signed
|
|
43
|
+
* (`signature_mismatch`), when the request carried a header the grant did not sign
|
|
44
|
+
* (`unsigned_header`), or when the grant has expired (`grant_expired`); any other
|
|
45
|
+
* `4xx` (`store_refused`). Each asks for a new grant, or for the file the grant
|
|
46
|
+
* was requested for.
|
|
47
|
+
* - `UploadTransportError` (`status` is storage's when it answered) — storage
|
|
48
|
+
* unreachable, a `5xx`, storage timing out on the body (`400 RequestTimeout`,
|
|
49
|
+
* which wrote nothing), or a redirect, which is refused rather than followed. In a
|
|
50
|
+
* browser a refused cross-origin request looks like an unreachable host, so the
|
|
51
|
+
* message names that too. A grant whose `url` is not an absolute `http(s)` URL
|
|
52
|
+
* free of user info is refused the same way, before anything is sent.
|
|
53
|
+
*
|
|
54
|
+
* A `5xx` leaves it unknown whether the object was written: retrying with the same
|
|
55
|
+
* grant either stores it or answers the `412` of a used grant, and then the grant's
|
|
56
|
+
* `uri` may already name the file. The grant is a bearer capability: nothing here
|
|
57
|
+
* logs it, and no error this throws carries its URL, storage's error body or a
|
|
58
|
+
* runtime error that could hold either.
|
|
59
|
+
*/
|
|
60
|
+
export declare function uploadWithGrant(grant: UploadGrant, file: Blob, options?: UploadWithGrantOptions): Promise<GrantedUpload>;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `uploadWithGrant` — send one file straight to Pipelex storage with an upload
|
|
3
|
+
* grant (`PipelexApiClient.requestUploadGrant`, `POST /v1/upload/grant`). The
|
|
4
|
+
* side that holds the credential requests the grant; the side that holds the
|
|
5
|
+
* bytes, typically a browser page, sends them with this. See
|
|
6
|
+
* `docs/input-preparation.md`.
|
|
7
|
+
*
|
|
8
|
+
* **This module is the browser-safe entry, `@pipelex/sdk/upload`.** Its runtime
|
|
9
|
+
* import graph must never reach a Node builtin, `undici` or the client: a page
|
|
10
|
+
* bundles it as is, with nothing marked external. It imports the error classes
|
|
11
|
+
* and the wire types only, and `tests/upload-grant.test.ts` bundles it for the
|
|
12
|
+
* browser with esbuild to hold it to that.
|
|
13
|
+
*/
|
|
14
|
+
import { RejectedAssetError, UploadTransportError } from "./errors.js";
|
|
15
|
+
// The classes `uploadWithGrant` raises, so a page can branch on them without the main entry.
|
|
16
|
+
export { InputPreparationError, RejectedAssetError, UploadTransportError } from "./errors.js";
|
|
17
|
+
/**
|
|
18
|
+
* `PUT` a file to storage with an upload grant, and return the reference it now
|
|
19
|
+
* carries. The file goes as the raw body with the grant's signed headers
|
|
20
|
+
* unchanged; the body sets `Content-Length`, which the grant signed, so the file
|
|
21
|
+
* must be exactly the size the grant was requested for.
|
|
22
|
+
*
|
|
23
|
+
* Storage's refusals map onto the input-preparation errors, so a caller that
|
|
24
|
+
* already handles `uploadFile` needs no new case:
|
|
25
|
+
*
|
|
26
|
+
* - `RejectedAssetError` (`status` is storage's, `code` says why) — a `412` when
|
|
27
|
+
* the grant was already used (`grant_used`: a grant writes one object, once); a
|
|
28
|
+
* `403` when the file's size, type or metadata differ from what the grant signed
|
|
29
|
+
* (`signature_mismatch`), when the request carried a header the grant did not sign
|
|
30
|
+
* (`unsigned_header`), or when the grant has expired (`grant_expired`); any other
|
|
31
|
+
* `4xx` (`store_refused`). Each asks for a new grant, or for the file the grant
|
|
32
|
+
* was requested for.
|
|
33
|
+
* - `UploadTransportError` (`status` is storage's when it answered) — storage
|
|
34
|
+
* unreachable, a `5xx`, storage timing out on the body (`400 RequestTimeout`,
|
|
35
|
+
* which wrote nothing), or a redirect, which is refused rather than followed. In a
|
|
36
|
+
* browser a refused cross-origin request looks like an unreachable host, so the
|
|
37
|
+
* message names that too. A grant whose `url` is not an absolute `http(s)` URL
|
|
38
|
+
* free of user info is refused the same way, before anything is sent.
|
|
39
|
+
*
|
|
40
|
+
* A `5xx` leaves it unknown whether the object was written: retrying with the same
|
|
41
|
+
* grant either stores it or answers the `412` of a used grant, and then the grant's
|
|
42
|
+
* `uri` may already name the file. The grant is a bearer capability: nothing here
|
|
43
|
+
* logs it, and no error this throws carries its URL, storage's error body or a
|
|
44
|
+
* runtime error that could hold either.
|
|
45
|
+
*/
|
|
46
|
+
export async function uploadWithGrant(grant, file, options = {}) {
|
|
47
|
+
const label = fileLabel(grant, file);
|
|
48
|
+
const { signal } = options;
|
|
49
|
+
// A caller's abort wins over a grant this would refuse, as it does over a failed request.
|
|
50
|
+
if (signal?.aborted)
|
|
51
|
+
throw signal.reason;
|
|
52
|
+
const target = storageTarget(grant, label);
|
|
53
|
+
let response;
|
|
54
|
+
try {
|
|
55
|
+
response = await fetch(grant.url, {
|
|
56
|
+
method: "PUT",
|
|
57
|
+
headers: grant.headers,
|
|
58
|
+
body: file,
|
|
59
|
+
// A presigned URL signs its host, so a redirect could never succeed; refusing
|
|
60
|
+
// it keeps the file from being sent anywhere the grant did not name.
|
|
61
|
+
redirect: "manual",
|
|
62
|
+
signal,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
// The signal's reason, not the runtime's error: some runtimes reject with a
|
|
67
|
+
// generic AbortError instead of the caller's own reason.
|
|
68
|
+
if (signal?.aborted)
|
|
69
|
+
throw signal.reason;
|
|
70
|
+
// Neither the runtime's message nor the error itself is kept: either can carry the
|
|
71
|
+
// request URL, and with it the grant's credential (Bun puts it on the error's `path`).
|
|
72
|
+
throw new UploadTransportError(`Upload of "${label}" could not reach storage at ${target.origin} ` +
|
|
73
|
+
`(${describeNetworkFailure(error)}). In a browser, a refused cross-origin request ` +
|
|
74
|
+
"looks the same: check that the page may connect to the storage origin (its CSP " +
|
|
75
|
+
"connect-src).");
|
|
76
|
+
}
|
|
77
|
+
if (response.status >= 200 && response.status < 300) {
|
|
78
|
+
await response.body?.cancel().catch(() => undefined);
|
|
79
|
+
return { uri: grant.uri };
|
|
80
|
+
}
|
|
81
|
+
// A browser reports a refused redirect as an opaque response with status 0.
|
|
82
|
+
if (response.type === "opaqueredirect" || (response.status >= 300 && response.status < 400)) {
|
|
83
|
+
await response.body?.cancel().catch(() => undefined);
|
|
84
|
+
throw new UploadTransportError(`Upload of "${label}" was redirected by storage, and the redirect was refused: a ` +
|
|
85
|
+
"presigned upload is valid only at the URL it was signed for.",
|
|
86
|
+
// A refused redirect in a browser is opaque, with status 0: no status reached us.
|
|
87
|
+
{ status: response.status || undefined });
|
|
88
|
+
}
|
|
89
|
+
// Only the error's code and message are kept, never the body: S3 echoes the
|
|
90
|
+
// canonical request on a signature mismatch, and with it the grant's credential.
|
|
91
|
+
const body = await response.text().catch(() => {
|
|
92
|
+
// A caller's abort errors the body stream too, and stays the caller's, unwrapped.
|
|
93
|
+
// Chrome and Firefox error it with a generic AbortError, so the reason is the signal's.
|
|
94
|
+
if (signal?.aborted)
|
|
95
|
+
throw signal.reason;
|
|
96
|
+
return "";
|
|
97
|
+
});
|
|
98
|
+
const refusal = parseStorageError(body);
|
|
99
|
+
const status = response.status;
|
|
100
|
+
if (status === 400 && refusal.code === "RequestTimeout") {
|
|
101
|
+
throw new UploadTransportError(`Upload of "${label}" timed out at storage (${describeStatus(response, refusal)}): ` +
|
|
102
|
+
"storage stopped waiting for the file's bytes and wrote nothing. Retry with the same " +
|
|
103
|
+
`grant before it expires at ${grant.expires_at}, or with a new one.`, { status });
|
|
104
|
+
}
|
|
105
|
+
if (status >= 400 && status < 500) {
|
|
106
|
+
const { code, advice } = classifyRefusal(status, refusal, grant);
|
|
107
|
+
throw new RejectedAssetError(`Storage refused the upload of "${label}" (${describeStatus(response, refusal)}): ${advice}`, label, status, { code });
|
|
108
|
+
}
|
|
109
|
+
throw new UploadTransportError(`Upload of "${label}" failed at storage (${describeStatus(response, refusal)})` +
|
|
110
|
+
(refusal.message ? `: ${refusal.message}` : "") +
|
|
111
|
+
". Retrying with the same grant either stores the file or reports the grant as used.", { status });
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The grant's URL, parsed before anything is sent. A runtime that cannot build a request
|
|
115
|
+
* from a URL names the whole URL in its error — Node and Chrome both do, for one that does
|
|
116
|
+
* not parse or that carries user info — and a browser resolves a relative one against the
|
|
117
|
+
* page, which would send the credential to the page's own origin. So only an absolute
|
|
118
|
+
* `http(s)` URL with no user info is sent, and the refusal names none of it.
|
|
119
|
+
*/
|
|
120
|
+
function storageTarget(grant, label) {
|
|
121
|
+
let target;
|
|
122
|
+
try {
|
|
123
|
+
target = new URL(grant.url);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
target = undefined;
|
|
127
|
+
}
|
|
128
|
+
if (target === undefined ||
|
|
129
|
+
(target.protocol !== "https:" && target.protocol !== "http:") ||
|
|
130
|
+
target.username !== "" ||
|
|
131
|
+
target.password !== "") {
|
|
132
|
+
throw new UploadTransportError(`Upload of "${label}" was not sent: the grant's url is not an absolute http(s) URL ` +
|
|
133
|
+
"free of user info, so it is not the one requestUploadGrant returned. Pass the grant " +
|
|
134
|
+
"on unchanged.");
|
|
135
|
+
}
|
|
136
|
+
return target;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A network failure described by the names and codes along its cause chain:
|
|
140
|
+
* "TypeError, caused by Error ENOTFOUND". Both are mutable, so each is kept only when it
|
|
141
|
+
* is a bare identifier, which cannot hold a URL.
|
|
142
|
+
*/
|
|
143
|
+
function describeNetworkFailure(error) {
|
|
144
|
+
const parts = [];
|
|
145
|
+
let current = error;
|
|
146
|
+
for (let depth = 0; depth < 5 && current instanceof Error; depth++) {
|
|
147
|
+
const name = isIdentifier(current.name) ? current.name : "Error";
|
|
148
|
+
const code = current.code;
|
|
149
|
+
parts.push(isIdentifier(code) ? `${name} ${code}` : name);
|
|
150
|
+
current = current.cause;
|
|
151
|
+
}
|
|
152
|
+
return parts.length > 0 ? parts.join(", caused by ") : "a non-Error rejection";
|
|
153
|
+
}
|
|
154
|
+
function isIdentifier(value) {
|
|
155
|
+
return typeof value === "string" && /^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(value);
|
|
156
|
+
}
|
|
157
|
+
/** Read `<Code>` and `<Message>` off an S3 error document. No XML parser: Node has none. */
|
|
158
|
+
function parseStorageError(body) {
|
|
159
|
+
return {
|
|
160
|
+
code: xmlElementText(body, "Code"),
|
|
161
|
+
message: xmlElementText(body, "Message"),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function xmlElementText(body, element) {
|
|
165
|
+
const match = new RegExp(`<${element}>([^<]*)</${element}>`).exec(body);
|
|
166
|
+
if (!match?.[1])
|
|
167
|
+
return undefined;
|
|
168
|
+
return match[1]
|
|
169
|
+
.replace(/</g, "<")
|
|
170
|
+
.replace(/>/g, ">")
|
|
171
|
+
.replace(/"/g, '"')
|
|
172
|
+
.replace(/'/g, "'")
|
|
173
|
+
.replace(/&/g, "&");
|
|
174
|
+
}
|
|
175
|
+
function describeStatus(response, refusal) {
|
|
176
|
+
const reason = refusal.code ?? response.statusText;
|
|
177
|
+
return reason ? `${response.status} ${reason}` : String(response.status);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* What a `4xx` from storage means for the caller: the code it branches on, and words
|
|
181
|
+
* it can act on. An expired grant and an unsigned header both answer `403
|
|
182
|
+
* AccessDenied`, so only S3's message tells them apart.
|
|
183
|
+
*/
|
|
184
|
+
function classifyRefusal(status, refusal, grant) {
|
|
185
|
+
if (status === 412) {
|
|
186
|
+
return {
|
|
187
|
+
code: "grant_used",
|
|
188
|
+
advice: "this grant was already used. A grant writes one object, once. If an earlier attempt " +
|
|
189
|
+
"with it failed at storage, that attempt may have stored the file under the grant's " +
|
|
190
|
+
"URI; otherwise request a new grant.",
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (refusal.code === "SignatureDoesNotMatch") {
|
|
194
|
+
return {
|
|
195
|
+
code: "signature_mismatch",
|
|
196
|
+
advice: "the file's size, content type or metadata differ from what the grant signed. Send " +
|
|
197
|
+
"exactly the file the grant was requested for, with the grant's headers unchanged.",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (refusal.message && /expired/i.test(refusal.message)) {
|
|
201
|
+
return {
|
|
202
|
+
code: "grant_expired",
|
|
203
|
+
advice: `the grant expired at ${grant.expires_at}. Request a new grant.`,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (refusal.message && /not signed/i.test(refusal.message)) {
|
|
207
|
+
return {
|
|
208
|
+
code: "unsigned_header",
|
|
209
|
+
advice: "the request carried a header the grant did not sign. Send the grant's headers and " +
|
|
210
|
+
"no other storage header.",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
code: "store_refused",
|
|
215
|
+
advice: refusal.message ??
|
|
216
|
+
"request a new grant, or check the file against the one it was requested for.",
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/** The file's own name when it has one (`File`), else the object name the grant's URI ends in. */
|
|
220
|
+
function fileLabel(grant, file) {
|
|
221
|
+
const name = file.name;
|
|
222
|
+
if (typeof name === "string" && name.length > 0)
|
|
223
|
+
return name;
|
|
224
|
+
const tail = grant.uri.slice(grant.uri.lastIndexOf("/") + 1);
|
|
225
|
+
return tail.length > 0 ? tail : grant.uri;
|
|
226
|
+
}
|
|
227
|
+
//# sourceMappingURL=upload-grant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-grant.js","sourceRoot":"","sources":["../src/upload-grant.ts"],"names":[],"mappings":"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"}
|
package/dist/upload.d.ts
CHANGED
|
@@ -50,9 +50,10 @@ export declare function guessContentType(filename: string): string;
|
|
|
50
50
|
* being silently misread as text or a URL. `node:fs/promises` is imported
|
|
51
51
|
* dynamically and guarded by `isNodeRuntime()`, so it is only *loaded* in Node.
|
|
52
52
|
* Note: the dynamic import defers execution, not bundling — a browser-targeting
|
|
53
|
-
* bundler still statically resolves this literal specifier, so browser
|
|
54
|
-
* must
|
|
55
|
-
* browser
|
|
53
|
+
* bundler still statically resolves this literal specifier, so a browser consumer
|
|
54
|
+
* of the main entry must mark `node:*` external. A page that stores a file uses the
|
|
55
|
+
* browser-safe `@pipelex/sdk/upload` entry (`upload-grant.ts`) instead, which never
|
|
56
|
+
* reaches this module; making the main entry browser-safe as a whole stays open.
|
|
56
57
|
*/
|
|
57
58
|
export declare function readLocalPath(path: string): Promise<Uint8Array>;
|
|
58
59
|
/** Base64-encode bytes, cross-runtime (Buffer when present, else chunked btoa). */
|
package/dist/upload.js
CHANGED
|
@@ -50,9 +50,10 @@ export function guessContentType(filename) {
|
|
|
50
50
|
* being silently misread as text or a URL. `node:fs/promises` is imported
|
|
51
51
|
* dynamically and guarded by `isNodeRuntime()`, so it is only *loaded* in Node.
|
|
52
52
|
* Note: the dynamic import defers execution, not bundling — a browser-targeting
|
|
53
|
-
* bundler still statically resolves this literal specifier, so browser
|
|
54
|
-
* must
|
|
55
|
-
* browser
|
|
53
|
+
* bundler still statically resolves this literal specifier, so a browser consumer
|
|
54
|
+
* of the main entry must mark `node:*` external. A page that stores a file uses the
|
|
55
|
+
* browser-safe `@pipelex/sdk/upload` entry (`upload-grant.ts`) instead, which never
|
|
56
|
+
* reaches this module; making the main entry browser-safe as a whole stays open.
|
|
56
57
|
*/
|
|
57
58
|
export async function readLocalPath(path) {
|
|
58
59
|
if (!isNodeRuntime()) {
|
|
@@ -126,7 +127,7 @@ function mapUploadError(error, filename) {
|
|
|
126
127
|
if (error instanceof ApiResponseError) {
|
|
127
128
|
switch (error.status) {
|
|
128
129
|
case 413:
|
|
129
|
-
return new RejectedAssetError(`The server rejected "${filename}": ${error.serverMessage ?? "asset exceeds the service size limit"}.`, filename, error.status, { cause: error });
|
|
130
|
+
return new RejectedAssetError(`The server rejected "${filename}": ${error.serverMessage ?? "asset exceeds the service size limit"}.`, filename, error.status, { cause: error, code: "too_large" });
|
|
130
131
|
case 401:
|
|
131
132
|
case 403:
|
|
132
133
|
return new UploadAuthenticationError(`Upload of "${filename}" was not authorized (${error.status}). Check the configured Pipelex API key.`, error.status, { cause: error });
|
|
@@ -134,7 +135,7 @@ function mapUploadError(error, filename) {
|
|
|
134
135
|
return new UnsupportedUploadCapabilityError("The configured Pipelex deployment does not support file upload (no /v1/upload route). " +
|
|
135
136
|
"Upload is a hosted Pipelex capability.", { cause: error });
|
|
136
137
|
default:
|
|
137
|
-
return new UploadTransportError(`Upload of "${filename}" failed (${error.status}): ${error.serverMessage ?? error.statusText}.`, { cause: error });
|
|
138
|
+
return new UploadTransportError(`Upload of "${filename}" failed (${error.status}): ${error.serverMessage ?? error.statusText}.`, { cause: error, status: error.status });
|
|
138
139
|
}
|
|
139
140
|
}
|
|
140
141
|
if (error instanceof ApiUnreachableError) {
|
package/dist/upload.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACL,uBAAuB,EACvB,kBAAkB,EAClB,yBAAyB,EACzB,oBAAoB,EACpB,gCAAgC,GACjC,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAmCpE,MAAM,oBAAoB,GAAG,0BAA0B,CAAC;AACxD,MAAM,gBAAgB,GAAG,YAAY,CAAC;AAEtC,MAAM,cAAc,GAA2B;IAC7C,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,YAAY;IACjB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,eAAe;IACpB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,YAAY;IACjB,IAAI,EAAE,YAAY;IAClB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,iBAAiB;IACtB,GAAG,EAAE,YAAY;IACjB,GAAG,EAAE,UAAU;IACf,IAAI,EAAE,kBAAkB;CACzB,CAAC;AAEF,kEAAkE;AAClE,MAAM,UAAU,aAAa;IAC3B,OAAO,CACL,OAAO,OAAO,KAAK,WAAW,IAAI,OAAO,CAAC,QAAQ,IAAI,IAAI,IAAI,OAAO,CAAC,QAAQ,CAAC,IAAI,IAAI,IAAI,CAC5F,CAAC;AACJ,CAAC;AAED,4FAA4F;AAC5F,SAAS,QAAQ,CAAC,IAAY;IAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAClC,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC;AACzC,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,GAAG,GAAG,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,GAAG,GAAG,CAAC,IAAI,GAAG,KAAK,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,oBAAoB,CAAC;IACxE,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;IAClD,OAAO,cAAc,CAAC,GAAG,CAAC,IAAI,oBAAoB,CAAC;AACrD,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"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"}
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/docs/architecture.md
CHANGED
|
@@ -32,6 +32,8 @@ The package is ESM-only (`"type": "module"`, `dist/index.js` built by `tsc` with
|
|
|
32
32
|
|
|
33
33
|
Condition order in `exports` matters: `types` is listed first (TS/Node resolution convention — the type-checker looks at the first matching condition), then `import`, then `default` as the CJS fallback.
|
|
34
34
|
|
|
35
|
+
The map has a second entry, **`@pipelex/sdk/upload`** (`dist/upload-grant.js`), the browser-safe one. The main entry is Node-first — `upload.ts` names `node:fs/promises` and `artifacts.ts` names `undici`, so a browser-targeting bundler must mark them external — because the client holds an API key and belongs on a server. The subpath carries only what a page needs to store a file with an upload grant: `uploadWithGrant`, the grant types and the three error classes it raises. Its runtime import graph is `errors.ts` and `mthds/protocol`, nothing else, and the unit suite bundles it for the browser with esbuild, nothing marked external, so a Node-only module becoming reachable fails a test rather than a consumer's build. Both entries load the same `dist/errors.js` and `dist/upload-grant.js`, so the function and the error classes are identical whichever entry a consumer imports them through. Whether the main entry should become browser-safe as a whole is a separate question, because it touches how path strings are read.
|
|
36
|
+
|
|
35
37
|
## Wire conventions
|
|
36
38
|
|
|
37
39
|
- Base URL from env (default `https://api.pipelex.com`); every route under `/v1/*`.
|
|
@@ -84,14 +86,15 @@ Flat `src/` (mirrors `mthds-js`'s `runners/api` flatness — the SDK has one cli
|
|
|
84
86
|
- `product-models.ts` — the snake_case wire shapes for the hosted-product management routes (profile, methods catalog, organizations, billing, API keys, gateway key, onboarding, storage, runs list/detail/update). Each interface models only the fields the product actually consumes — no speculative mirrors.
|
|
85
87
|
- `method-source.ts` — `methodSourceToContents`, the canonical parser turning a stored method's polymorphic `mthds` source (raw single-bundle text XOR a JSON `{name, content}[]` file array) into a flat list of file contents. A verbatim port of the platform's `_method_source_to_contents` so the SDK and runtime read a stored source identically; exported from the barrel and consumed by `getMethodClosure`. See [input-preparation.md](./input-preparation.md#getmethodclosure--the-explicit-expansion-utility).
|
|
86
88
|
- `codegen-check.ts` — `runCodegenCheck`, the **pure** offline drift check over a committed codegen tree: the stamp grammar, the `codegen.lock` parser, the hash, and the drift taxonomy, ported from pipelex's `codegen/check.py` so a verdict computed here equals the one `pipelex codegen check` computes over the same bytes (the drift `detail` sentences included, verbatim). It touches neither the client nor `mthds/protocol` — its only import is `smol-toml` — and it hashes through WebCrypto rather than `node:crypto` so it adds no Node builtin to the barrel's import graph (`upload.ts` still names `node:fs/promises`, so a browser-targeting bundler must mark `node:*` external). It raises `CodegenLockError`, which extends `Error` rather than the protocol base. No filesystem, no fetch, no API key — the caller walks its own tree and hands in the text. See [crate-routes.md](./crate-routes.md#the-offline-check--runcodegencheck).
|
|
87
|
-
- `
|
|
89
|
+
- `upload-grant.ts` — `uploadWithGrant`, the sender of an upload grant (`requestUploadGrant`, `POST /v1/upload/grant`): the file as the raw body of a `PUT` to the grant's URL with its signed headers unchanged, redirects refused, and storage's refusals mapped onto the input-preparation errors (`RejectedAssetError` with a `code` for a `4xx` such as the `412` of a used grant or the `403` of a signature mismatch, `UploadTransportError` for a `5xx`, storage's `RequestTimeout`, a redirect or no response). It is the browser-safe `@pipelex/sdk/upload` entry, so it imports the error classes and the wire types only, never the client. See [input-preparation.md](./input-preparation.md#uploading-with-a-grant--requestuploadgrant--uploadwithgrant).
|
|
90
|
+
- `artifacts.ts` — the artifact stack, the download twin of `prepareInputs`, in layers: `locateArtifacts` (a pure, exported walk for `pipelex-storage://` references, each with every `$`-rooted path it sits at) and `collectArtifacts` (the same walk's bare references), `resolveArtifacts` (the bulk resolve route over a whole list, chunked at its bound), `fetchArtifact` (one reference as a bounded `Response`) and `downloadArtifacts` (Node-only, a run's produced files saved under a directory as a produced verdict), plus `artifactFilename`, the rule that names a saved file after the field it fills. The three network operations are client methods over a small structural seam (`ArtifactCapableClient`: the raw `resolveStorageUrls` call and `getRunResult`); the module's static import graph stays free of Node builtins, because `node:fs/promises`, `node:path` and `undici` are loaded by dynamic import behind `isNodeRuntime()` — a browser-targeting bundler still has to mark `node:*` and `undici` external, as it already must for `upload.ts`. `undici` is a runtime dependency for one reason: the fetch runs on an `Agent` carrying `headersTimeout` and `bodyTimeout`, the per-stall bounds an `AbortSignal` budget does not give, and Node exposes its bundled undici's `fetch` but not its `Agent`. See [artifact-download.md](./artifact-download.md).
|
|
88
91
|
- `runs.ts` — the run-lifecycle types (`RunStatus`, `RunRead`, `RunResults`, `RunResultState`) and the single poll loop (`pollUntilResult`). `RunResults.main_stuff` (the resolved main output content) is always present for a completed run: on the hosted path it is the `main_stuff.json` S3 artifact; on the bare-runner blocking path the SDK resolves it from the returned working memory via the response's `main_stuff_name`, so both paths deliver the same shape. Consumers read `main_stuff` directly. `working_memory` — every named stuff of the run, `{ root, aliases }`, typed `DictWorkingMemory` from `models.ts` — reads the same on both paths: the hosted path relays the `working_memory.json` artifact, and the blocking path lifts it off `pipe_output`, which still rides whole (blocking path only). `graph_spec` — the executed graph — also reaches the client on both paths: the hosted path relays the `graphspec.json` artifact, and the blocking path lifts it off `pipe_output` beside `graph_assembly_error`, the graph's twin of `usage_assembly_error`. That error field is declared ahead of the wire on the hosted side, where the platform's results body relays no such key and the field is therefore absent, reading `undefined` rather than `null`. A completed run that cannot deliver a main stuff throws `MissingMainStuffError`. `runs.ts` also owns `TokensUsageRecord` — one client-facing usage record per inference call, carried by `RunResults.tokens_usages` on both paths. That is a mirror of a wire contract this SDK does not own (inference accounting is a Pipelex runtime extension rather than part of the MTHDS standard): every field is optional and the interface carries an index signature so pre-contract artifacts (relayed verbatim, never migrated) still type-check. See [`run-usage.md`](./run-usage.md) for the field reference, the cost/null semantics, and the old-artifact rules, and [`run-results.md`](./run-results.md) for the field-by-field account of `RunResults` — the durable handle, the output and how to read it, the run's working memory, the executed graph and how to render or keep it, and the produced files with their short-lived links.
|
|
89
92
|
- `usage.ts` — `summarizeUsage`, the **pure** fold of a run's usage pair (`tokens_usages` / `usage_assembly_error`) into one `UsageSummary`: a `state` (`records`, `no_inference` or `unavailable`), the null-aware total cost with its `cost_partial` flag, the `input` and `output` token totals, the call count, the relayed assembly error, and a `by_pipe` rollup sorted by cost with the unattributed calls grouped under a `null` `pipe_code`. It applies the rules [`run-usage.md`](./run-usage.md#summarizing-a-run--summarizeusage) states, so that every consumer reads one computation rather than re-deriving those rules by hand. It imports types from `runs.ts` and nothing else: no client, no fetch, no `mthds/protocol`.
|
|
90
93
|
- `execute-result.ts` — `PipelexExecuteResult`, a `DictRunResultExecute` subtype that adds a resolved `.main_stuff` accessor (dug out of `pipe_output`'s working memory via the response's `main_stuff_name`, throwing `MissingMainStuffError` if unlocatable). `execute()` returns it, giving blocking and durable results the **same output accessor** — `result.main_stuff` — so callers never branch on which path ran. The neutral `mthds` wire model leaves `main_stuff_name` in its extension index signature; this Pipelex-branded subtype declares it as a typed field (Pipelex owns that concept). `resultsFromExecute` (the public lift from that result onto `RunResults`, in the same module) reads that accessor too, keeping the resolution single-sourced; the `executeBlocking` fallback is its only caller inside the SDK, and a consumer driving `execute()` directly calls it by name.
|
|
91
94
|
- `errors.ts` — the transport and run-lifecycle typed errors, the input-preparation family under `InputPreparationError`, and the artifact family under `ArtifactOperationError` (`ScopeUnavailableError`, `ArtifactFetchError`, `ArtifactAuthenticationError`, the last carrying the download verdict so far); all derive from the protocol-base `PipelineRequestError` (re-exported from `mthds/protocol`) except `ClientAuthenticationError`. It is not the whole error surface: `codegen-check.ts` exports `CodegenLockError`, which extends plain `Error` because nothing was requested over the wire.
|
|
92
95
|
- `user-agent.ts` — client identification per the workspace spec `docs/specs/client-identification.md`: the exported `AppInfo` option, its RFC 9110 token validation (a `TypeError` on a bad field, a `RangeError` past the 512-character ceiling), its rendering, the runtime detection (Node, Bun, Deno, or a browser, where no header is set) and `buildUserAgent`, which the client calls once at construction. The client's private `requestHeaders` is the one place API request headers are built, so every route carries the value; the artifact fetch of a presigned link never goes through it. See [client-identification.md](./client-identification.md).
|
|
93
96
|
- `version.ts` — `SDK_VERSION`, in its own module so the client stamps it on the `User-Agent` without importing the barrel, which re-exports it.
|
|
94
|
-
- `index.ts` — the public barrel: `export * from "mthds/protocol"` (single import source for the standard surface) plus the client, models, run types, the standalone pure helpers (`runCodegenCheck`, `summarizeUsage`, `collectArtifacts`, `artifactFilename`), and errors.
|
|
97
|
+
- `index.ts` — the public barrel: `export * from "mthds/protocol"` (single import source for the standard surface) plus the client, models, run types, the standalone pure helpers (`runCodegenCheck`, `summarizeUsage`, `locateArtifacts`, `collectArtifacts`, `artifactFilename`), and errors.
|
|
95
98
|
|
|
96
99
|
## Client surface (current)
|
|
97
100
|
|
|
@@ -117,7 +120,7 @@ The hosted catalog/account routes the webapp drives. All ride the same `{base}/v
|
|
|
117
120
|
- **Pipelex API keys (`plx_sk_…`):** `listPipelexApiKeys`, `createPipelexApiKey` (409 `pipelex_api_key_limit_reached`), `revokePipelexApiKey`, `rotatePipelexApiKey`. The plaintext `api_key` is returned once, on create/rotate.
|
|
118
121
|
- **Gateway API key (LLM inference key):** `createGatewayApiKey` (the JSON body is always sent, even with `promo_code: null`), `getGatewayApiKey`.
|
|
119
122
|
- **Onboarding:** `submitOnboarding`.
|
|
120
|
-
- **Storage:** `resolveStorageUrl`, `resolveStorageUrls` (`POST /v1/resolve-storage-url/bulk` — the single route applied to a list of at most 100 references, one verdict per reference in request order, a refused reference a value on its item; a deployment without the route answers a `404` `ApiResponseError`), `upload` (JSON base64 — the multipart hop is browser→BFF only). The higher-level `uploadFile` / `prepareInputs` preparation surface is built on top of `upload`; `prepareInputs` additionally composes one `validate` call with `views: ["input_form"]` for the signature it walks, and takes the method as inline `files`, a `method_ref` or a `method_id` like every other method-taking operation — see [input-preparation.md](./input-preparation.md). The download twin — `collectArtifacts`, `resolveArtifacts`, `fetchArtifact`, `downloadArtifacts` — is built on top of `resolveStorageUrls` and `getRunResult`, never on the embedded `public_url`, and is documented on [artifact-download.md](./artifact-download.md); downloading is a separate explicit gesture, and no run call takes a `download` option.
|
|
123
|
+
- **Storage:** `resolveStorageUrl`, `resolveStorageUrls` (`POST /v1/resolve-storage-url/bulk` — the single route applied to a list of at most 100 references, one verdict per reference in request order, a refused reference a value on its item; a deployment without the route answers a `404` `ApiResponseError`), `upload` (JSON base64 — the multipart hop is browser→BFF only), `requestUploadGrant` (`POST /v1/upload/grant` — a presigned, create-only `PUT` for one new object, which the holder of the bytes sends with the standalone `uploadWithGrant`, also exported from the browser-safe `@pipelex/sdk/upload` entry; a declared size over the cap is a `413` `ApiResponseError`). The higher-level `uploadFile` / `prepareInputs` preparation surface is built on top of `upload`; `prepareInputs` additionally composes one `validate` call with `views: ["input_form"]` for the signature it walks, and takes the method as inline `files`, a `method_ref` or a `method_id` like every other method-taking operation — see [input-preparation.md](./input-preparation.md). The download twin — `locateArtifacts`, `collectArtifacts`, `resolveArtifacts`, `fetchArtifact`, `downloadArtifacts` — is built on top of `resolveStorageUrls` and `getRunResult`, never on the embedded `public_url`, and is documented on [artifact-download.md](./artifact-download.md); downloading is a separate explicit gesture, and no run call takes a `download` option.
|
|
121
124
|
- **Runs list/detail/update:** `listRuns` (by `method_id`) returns **one page**, not the whole history — a `RunPage` (`{items, nextCursor}`) served from a time-ordered index, so its cost is the page size rather than the size of the history. `iterateRuns` is the async iterator that follows the cursor for callers who genuinely want everything (`for await (const run of client.iterateRuns(id))`); it is deliberately not a `listAllRuns(): Promise<PipelineRun[]>`, because an all-at-once helper needs a page cap and a cap means silently returning a truncated list. Both accept `createdFrom` / `createdTo`, applied server-side as index key conditions — these are **instants, not days** (ISO-8601 with a UTC offset; a naive timestamp is a 400), because only the caller knows which timezone's day it means. `getRunDetail` (`GET /v1/runs/{id}`) is the only call returning `mthds_contents` — what the run actually executed — and `inputs`; it is kept off the status read that pollers hit every few seconds and off the list, where the bundle would be multiplied by the page size. `updateRun` is the admin/manual status patch. The status/results/start lifecycle routes live in the run-lifecycle section above.
|
|
122
125
|
|
|
123
126
|
## Hook bundle (`dist-hooks/check.mjs`)
|
|
@@ -129,4 +132,4 @@ The hosted catalog/account routes the webapp drives. All ride the same `{base}/v
|
|
|
129
132
|
Two layers, split by whether a server is in the loop:
|
|
130
133
|
|
|
131
134
|
- **Unit suites (`tests/*.test.ts`, `make test`):** mock the fetch boundary (`vi.spyOn(globalThis, "fetch")`) and pin the client's wire behavior — URLs, request bodies, status→error mapping, poll semantics. These run everywhere, including CI. The codegen-check suite is the exception that proves the rule: it has no server to mock, so it runs against **vendored real codegen output** under `tests/fixtures/codegen/` — artifacts and lock emitted by an actual `pipelex codegen types` run, committed beside the `.mthds` they came from. A hand-written stamp would only prove the port agrees with itself. The artifacts are vendored with a trailing `.txt` and their real names restored at load time, because `format:check`, `lint`, and `typecheck:test` all glob `tests/**/*.ts` — and a single reformatted byte would invalidate the very hash the fixture exists to pin.
|
|
132
|
-
- **E2E suites (`tests/e2e/*.e2e.ts`, `make test-e2e`):** no mocks — they drive a **live pipelex-api server** (`PIPELEX_E2E_BASE_URL`, default `http://localhost:8081`) and verify the diagnostic-endpoint contract end-to-end with correct and malformed MTHDS bundles: clean content lints with zero diagnostics and formats to itself (idempotently), malformed content comes back as a 200 verdict (`diagnostics[]` on lint; `diagnostics[]` plus the content echoed back unchanged with `changed: false` on format; `is_valid: false` + `validation_errors[]` on validate), and malformed formatter options surface as a 422 `ApiResponseError`. The crate suite adds the case only a live server can settle: the `kind` / `target` vocabulary is a mirror of a Python `StrEnum` in another repo, so nothing but a real call proves the two still agree — and it cross-checks that `codegen`'s `crate_fingerprint` equals the `fingerprint` `resolve` returns for the same closure. It then runs `runCodegenCheck` over the artifacts and lock the server just emitted — verbatim first (current, with the lock header's fingerprint matching the response's), then mutated in memory once per drift category, for a `.ts` target and a `.py` one. The unit suite pins the port against vendored bytes; only this pins it against the stamp and lock bytes the server writes **today**, which is the pairing that matters when a consumer commits the server's artifacts and gates CI on the SDK's verdict about them. The distinct `.e2e.ts` suffix + `vitest.e2e.config.ts` keep them out of the default `vitest run`, so `make test` / CI never require a server. An unreachable server is an error, never a silent skip, and it is reported once: `make test-e2e` preflights the target with a single `curl` and bails with a one-line message, instead of letting all three suites fail in `beforeAll` and print a socket-error dump each. The per-suite `beforeAll` guard stays as the backstop for a direct `npm run test:e2e` run. Both probe **`/v1/version`**, deliberately: a bare runner serves the origin-level `/health` and a hosted origin does not (it answers `/v1/health` instead), so `/v1/version` is the one unauthenticated route both answer — probing anything else reports a perfectly reachable hosted origin as down. The target itself is resolved once, in the Makefile, and exported along with `PIPELEX_API_KEY`, so the URL the preflight checks is always the URL the suites call: the shell environment (or a `make test-e2e PIPELEX_E2E_BASE_URL=...` override) wins, then `.env`, then `http://localhost:8081`. The run echoes the resolved target before it starts, since `.env` can now supply it invisibly. Note that a hosted origin cannot run the whole suite today — `lint` and `format` are not exposed there (gateway `403`), while the protocol, build and crate routes all answer — so a fully green run means a local runner. The artifact suite is the mirror image: it uploads a file with `prepareInputs`, runs a pass-through and brings the file back down with `downloadArtifacts` over `working_memory`, bytes equal, so it needs a hosted platform carrying the upload, run-lifecycle and bulk resolve routes and a key for it, and it fails honestly rather than skipping against a bare runner.
|
|
135
|
+
- **E2E suites (`tests/e2e/*.e2e.ts`, `make test-e2e`):** no mocks — they drive a **live pipelex-api server** (`PIPELEX_E2E_BASE_URL`, default `http://localhost:8081`) and verify the diagnostic-endpoint contract end-to-end with correct and malformed MTHDS bundles: clean content lints with zero diagnostics and formats to itself (idempotently), malformed content comes back as a 200 verdict (`diagnostics[]` on lint; `diagnostics[]` plus the content echoed back unchanged with `changed: false` on format; `is_valid: false` + `validation_errors[]` on validate), and malformed formatter options surface as a 422 `ApiResponseError`. The crate suite adds the case only a live server can settle: the `kind` / `target` vocabulary is a mirror of a Python `StrEnum` in another repo, so nothing but a real call proves the two still agree — and it cross-checks that `codegen`'s `crate_fingerprint` equals the `fingerprint` `resolve` returns for the same closure. It then runs `runCodegenCheck` over the artifacts and lock the server just emitted — verbatim first (current, with the lock header's fingerprint matching the response's), then mutated in memory once per drift category, for a `.ts` target and a `.py` one. The unit suite pins the port against vendored bytes; only this pins it against the stamp and lock bytes the server writes **today**, which is the pairing that matters when a consumer commits the server's artifacts and gates CI on the SDK's verdict about them. The distinct `.e2e.ts` suffix + `vitest.e2e.config.ts` keep them out of the default `vitest run`, so `make test` / CI never require a server. An unreachable server is an error, never a silent skip, and it is reported once: `make test-e2e` preflights the target with a single `curl` and bails with a one-line message, instead of letting all three suites fail in `beforeAll` and print a socket-error dump each. The per-suite `beforeAll` guard stays as the backstop for a direct `npm run test:e2e` run. Both probe **`/v1/version`**, deliberately: a bare runner serves the origin-level `/health` and a hosted origin does not (it answers `/v1/health` instead), so `/v1/version` is the one unauthenticated route both answer — probing anything else reports a perfectly reachable hosted origin as down. The target itself is resolved once, in the Makefile, and exported along with `PIPELEX_API_KEY`, so the URL the preflight checks is always the URL the suites call: the shell environment (or a `make test-e2e PIPELEX_E2E_BASE_URL=...` override) wins, then `.env`, then `http://localhost:8081`. The run echoes the resolved target before it starts, since `.env` can now supply it invisibly. Note that a hosted origin cannot run the whole suite today — `lint` and `format` are not exposed there (gateway `403`), while the protocol, build and crate routes all answer — so a fully green run means a local runner. The upload-grant suite needs the same hosted platform: it requests a grant, sends a small PDF with `uploadWithGrant`, reads the bytes back equal through `fetchArtifact`, and proves the refusals the error mapping reads — the `412` of a used grant, the `403 SignatureDoesNotMatch` of a file one byte longer than declared, and the `413` of a declared size over the cap. It runs in Node, whose `fetch` sends no CORS preflight, so the bucket's CORS rule is proven by a page, not by it. The artifact suite is the mirror image: it uploads a file with `prepareInputs`, runs a pass-through and brings the file back down with `downloadArtifacts` over `working_memory`, bytes equal, so it needs a hosted platform carrying the upload, run-lifecycle and bulk resolve routes and a key for it, and it fails honestly rather than skipping against a bare runner.
|