@pipelex/sdk 0.20.1 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -1
- package/dist/artifacts.js +3 -1
- package/dist/artifacts.js.map +1 -1
- package/dist/client.d.ts +41 -1
- package/dist/client.js +50 -17
- 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/hooks/claude-mthds-check.js +2 -2
- package/dist/hooks/claude-mthds-check.js.map +1 -1
- package/dist/hooks/validate-client.d.ts +12 -0
- package/dist/hooks/validate-client.js +15 -0
- package/dist/hooks/validate-client.js.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +4 -1
- 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/user-agent.d.ts +66 -0
- package/dist/user-agent.js +153 -0
- package/dist/user-agent.js.map +1 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.js +7 -0
- package/dist/version.js.map +1 -0
- package/docs/architecture.md +8 -3
- package/docs/client-identification.md +54 -0
- package/docs/input-preparation.md +54 -5
- package/package.json +6 -1
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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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client identification: the `User-Agent` this SDK sends to the Pipelex API.
|
|
3
|
+
*
|
|
4
|
+
* Implements the workspace contract `docs/specs/client-identification.md`. The
|
|
5
|
+
* value is a sequence of RFC 9110 product tokens, outermost first:
|
|
6
|
+
*
|
|
7
|
+
* [<appInfo>] pipelex-sdk-js/<SDK_VERSION> [<runtime>/<version> (<os>; <arch>)]
|
|
8
|
+
*
|
|
9
|
+
* The integrator's `appInfo` comes first, the SDK next, the runtime last. The SDK
|
|
10
|
+
* calls `fetch` itself, so no other library appears on the transport path. In a
|
|
11
|
+
* browser no header is produced at all: browsers either ignore it or turn it into
|
|
12
|
+
* a CORS preflight the API refuses.
|
|
13
|
+
*
|
|
14
|
+
* The header is self-declared and unauthenticated — analytics and diagnostics
|
|
15
|
+
* only, never an authorization signal.
|
|
16
|
+
*/
|
|
17
|
+
/** The SDK's own product token name, from the spec's token registry. */
|
|
18
|
+
export declare const SDK_TOKEN_NAME = "pipelex-sdk-js";
|
|
19
|
+
/** The spec's ceiling on the whole header value. */
|
|
20
|
+
export declare const MAX_USER_AGENT_LENGTH = 512;
|
|
21
|
+
/**
|
|
22
|
+
* The integrator's identity, placed before the SDK's own token (Stripe-style).
|
|
23
|
+
*
|
|
24
|
+
* Renders as `name/version (<details>; +url)`; the `/version` and the comment are
|
|
25
|
+
* dropped when empty. An invalid field is refused at construction with a
|
|
26
|
+
* `TypeError` — it is never silently dropped or rewritten.
|
|
27
|
+
*/
|
|
28
|
+
export interface AppInfo {
|
|
29
|
+
/** An RFC 9110 `token`, lowercase kebab-case by convention, e.g. `acme-invoicer`. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** An RFC 9110 `token`, e.g. `1.4.0`. */
|
|
32
|
+
version?: string;
|
|
33
|
+
/** A URL, rendered in the comment as `+url`. */
|
|
34
|
+
url?: string;
|
|
35
|
+
/** Comment parameters, each a `token` or `token=value`, rendered before `+url`. */
|
|
36
|
+
details?: string[];
|
|
37
|
+
}
|
|
38
|
+
/** The runtime the SDK is running on, as far as the header is concerned. */
|
|
39
|
+
export type RuntimeInfo = {
|
|
40
|
+
kind: "browser";
|
|
41
|
+
} | {
|
|
42
|
+
kind: "server";
|
|
43
|
+
name?: string;
|
|
44
|
+
version?: string;
|
|
45
|
+
os?: string;
|
|
46
|
+
arch?: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Refuse an `appInfo` the spec's grammar does not admit, with a `TypeError` naming
|
|
50
|
+
* the offending field. Empty `version`, `url` and `details` are treated as absent.
|
|
51
|
+
*/
|
|
52
|
+
export declare function validateAppInfo(appInfo: AppInfo): void;
|
|
53
|
+
/** Render a validated `appInfo` as `name/version (<details>; +url)`, omitting empty parts. */
|
|
54
|
+
export declare function renderAppInfo(appInfo: AppInfo): string;
|
|
55
|
+
/**
|
|
56
|
+
* Read the runtime from the globals. A browser (a window with a document, or a
|
|
57
|
+
* web worker) is reported as such so the caller sets no header. Deno is checked
|
|
58
|
+
* before Bun and Node because it also exposes a Node-compatible `process`.
|
|
59
|
+
*/
|
|
60
|
+
export declare function detectRuntime(scope?: Record<string, unknown>): RuntimeInfo;
|
|
61
|
+
/**
|
|
62
|
+
* Build the full `User-Agent` value, or `undefined` in a browser, where no header
|
|
63
|
+
* may be set. Validates `appInfo` first and refuses (with a `RangeError`) a value
|
|
64
|
+
* that would exceed the spec's 512-character ceiling rather than truncating it.
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildUserAgent(appInfo?: AppInfo, runtime?: RuntimeInfo): string | undefined;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client identification: the `User-Agent` this SDK sends to the Pipelex API.
|
|
3
|
+
*
|
|
4
|
+
* Implements the workspace contract `docs/specs/client-identification.md`. The
|
|
5
|
+
* value is a sequence of RFC 9110 product tokens, outermost first:
|
|
6
|
+
*
|
|
7
|
+
* [<appInfo>] pipelex-sdk-js/<SDK_VERSION> [<runtime>/<version> (<os>; <arch>)]
|
|
8
|
+
*
|
|
9
|
+
* The integrator's `appInfo` comes first, the SDK next, the runtime last. The SDK
|
|
10
|
+
* calls `fetch` itself, so no other library appears on the transport path. In a
|
|
11
|
+
* browser no header is produced at all: browsers either ignore it or turn it into
|
|
12
|
+
* a CORS preflight the API refuses.
|
|
13
|
+
*
|
|
14
|
+
* The header is self-declared and unauthenticated — analytics and diagnostics
|
|
15
|
+
* only, never an authorization signal.
|
|
16
|
+
*/
|
|
17
|
+
import { SDK_VERSION } from "./version.js";
|
|
18
|
+
/** The SDK's own product token name, from the spec's token registry. */
|
|
19
|
+
export const SDK_TOKEN_NAME = "pipelex-sdk-js";
|
|
20
|
+
/** The spec's ceiling on the whole header value. */
|
|
21
|
+
export const MAX_USER_AGENT_LENGTH = 512;
|
|
22
|
+
// RFC 9110 §5.6.2: tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
|
|
23
|
+
// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA.
|
|
24
|
+
const TOKEN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
25
|
+
// `value = token / ( name "/" version )`.
|
|
26
|
+
const DETAIL_VALUE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+(\/[!#$%&'*+\-.^_`|~0-9A-Za-z]+)?$/;
|
|
27
|
+
// A URL inside a comment: visible ASCII, no whitespace, and none of the characters
|
|
28
|
+
// that would close the comment or split it into another parameter.
|
|
29
|
+
const COMMENT_URL = /^[!-~]+$/;
|
|
30
|
+
const COMMENT_BREAKERS = /[()\\;]/;
|
|
31
|
+
function isToken(value) {
|
|
32
|
+
return TOKEN.test(value);
|
|
33
|
+
}
|
|
34
|
+
function isDetail(detail) {
|
|
35
|
+
const eq = detail.indexOf("=");
|
|
36
|
+
if (eq === -1)
|
|
37
|
+
return isToken(detail);
|
|
38
|
+
return isToken(detail.slice(0, eq)) && DETAIL_VALUE.test(detail.slice(eq + 1));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Refuse an `appInfo` the spec's grammar does not admit, with a `TypeError` naming
|
|
42
|
+
* the offending field. Empty `version`, `url` and `details` are treated as absent.
|
|
43
|
+
*/
|
|
44
|
+
export function validateAppInfo(appInfo) {
|
|
45
|
+
if (typeof appInfo !== "object" || appInfo === null) {
|
|
46
|
+
throw new TypeError("appInfo must be an object with at least a `name`.");
|
|
47
|
+
}
|
|
48
|
+
if (typeof appInfo.name !== "string" || !isToken(appInfo.name)) {
|
|
49
|
+
throw new TypeError(`appInfo.name ${JSON.stringify(appInfo.name)} is not an RFC 9110 token ` +
|
|
50
|
+
"(letters, digits and !#$%&'*+-.^_`|~ only, non-empty).");
|
|
51
|
+
}
|
|
52
|
+
if (appInfo.version !== undefined && appInfo.version !== "") {
|
|
53
|
+
if (typeof appInfo.version !== "string" || !isToken(appInfo.version)) {
|
|
54
|
+
throw new TypeError(`appInfo.version ${JSON.stringify(appInfo.version)} is not an RFC 9110 token.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (appInfo.url !== undefined && appInfo.url !== "") {
|
|
58
|
+
if (typeof appInfo.url !== "string" ||
|
|
59
|
+
!COMMENT_URL.test(appInfo.url) ||
|
|
60
|
+
COMMENT_BREAKERS.test(appInfo.url)) {
|
|
61
|
+
throw new TypeError(`appInfo.url ${JSON.stringify(appInfo.url)} cannot sit in a User-Agent comment ` +
|
|
62
|
+
"(visible ASCII only, no whitespace, parentheses, backslash or semicolon).");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (appInfo.details !== undefined) {
|
|
66
|
+
if (!Array.isArray(appInfo.details)) {
|
|
67
|
+
throw new TypeError("appInfo.details must be an array of strings.");
|
|
68
|
+
}
|
|
69
|
+
for (const detail of appInfo.details) {
|
|
70
|
+
if (typeof detail !== "string" || !isDetail(detail)) {
|
|
71
|
+
throw new TypeError(`appInfo.details entry ${JSON.stringify(detail)} is neither a token nor token=value.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Render a validated `appInfo` as `name/version (<details>; +url)`, omitting empty parts. */
|
|
77
|
+
export function renderAppInfo(appInfo) {
|
|
78
|
+
let product = appInfo.name;
|
|
79
|
+
if (appInfo.version)
|
|
80
|
+
product += `/${appInfo.version}`;
|
|
81
|
+
const params = [...(appInfo.details ?? [])];
|
|
82
|
+
if (appInfo.url)
|
|
83
|
+
params.push(`+${appInfo.url}`);
|
|
84
|
+
return params.length > 0 ? `${product} (${params.join("; ")})` : product;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Read the runtime from the globals. A browser (a window with a document, or a
|
|
88
|
+
* web worker) is reported as such so the caller sets no header. Deno is checked
|
|
89
|
+
* before Bun and Node because it also exposes a Node-compatible `process`.
|
|
90
|
+
*/
|
|
91
|
+
export function detectRuntime(scope = globalThis) {
|
|
92
|
+
const win = scope["window"];
|
|
93
|
+
if ((win !== undefined && win !== null && win.document !== undefined) ||
|
|
94
|
+
typeof scope["importScripts"] === "function") {
|
|
95
|
+
return { kind: "browser" };
|
|
96
|
+
}
|
|
97
|
+
const deno = scope["Deno"];
|
|
98
|
+
if (deno?.version?.deno) {
|
|
99
|
+
return {
|
|
100
|
+
kind: "server",
|
|
101
|
+
name: "deno",
|
|
102
|
+
version: deno.version.deno,
|
|
103
|
+
os: deno.build?.os,
|
|
104
|
+
arch: deno.build?.arch,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const proc = scope["process"];
|
|
108
|
+
const bun = proc?.versions?.["bun"];
|
|
109
|
+
if (bun) {
|
|
110
|
+
return { kind: "server", name: "bun", version: bun, os: proc.platform, arch: proc.arch };
|
|
111
|
+
}
|
|
112
|
+
const node = proc?.versions?.["node"];
|
|
113
|
+
if (node) {
|
|
114
|
+
return { kind: "server", name: "node", version: node, os: proc.platform, arch: proc.arch };
|
|
115
|
+
}
|
|
116
|
+
return { kind: "server" };
|
|
117
|
+
}
|
|
118
|
+
function renderRuntime(runtime) {
|
|
119
|
+
if (!runtime.name || !runtime.version || !isToken(runtime.version))
|
|
120
|
+
return undefined;
|
|
121
|
+
const token = `${runtime.name}/${runtime.version}`;
|
|
122
|
+
const platform = [runtime.os, runtime.arch].filter((part) => typeof part === "string" && isToken(part));
|
|
123
|
+
return platform.length > 0 ? `${token} (${platform.join("; ")})` : token;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Build the full `User-Agent` value, or `undefined` in a browser, where no header
|
|
127
|
+
* may be set. Validates `appInfo` first and refuses (with a `RangeError`) a value
|
|
128
|
+
* that would exceed the spec's 512-character ceiling rather than truncating it.
|
|
129
|
+
*/
|
|
130
|
+
export function buildUserAgent(appInfo, runtime = detectRuntime()) {
|
|
131
|
+
if (appInfo !== undefined)
|
|
132
|
+
validateAppInfo(appInfo);
|
|
133
|
+
const parts = [];
|
|
134
|
+
if (appInfo !== undefined)
|
|
135
|
+
parts.push(renderAppInfo(appInfo));
|
|
136
|
+
parts.push(`${SDK_TOKEN_NAME}/${SDK_VERSION}`);
|
|
137
|
+
if (runtime.kind === "server") {
|
|
138
|
+
const runtimePart = renderRuntime(runtime);
|
|
139
|
+
if (runtimePart)
|
|
140
|
+
parts.push(runtimePart);
|
|
141
|
+
}
|
|
142
|
+
const value = parts.join(" ");
|
|
143
|
+
// Checked before the browser return, so an `appInfo` refused on a server is refused
|
|
144
|
+
// in a browser too: the refusal is a property of the `appInfo`, not of the runtime.
|
|
145
|
+
if (value.length > MAX_USER_AGENT_LENGTH) {
|
|
146
|
+
throw new RangeError(`The User-Agent built from appInfo is ${value.length} characters; the ceiling is ` +
|
|
147
|
+
`${MAX_USER_AGENT_LENGTH}. Shorten appInfo.details or appInfo.url.`);
|
|
148
|
+
}
|
|
149
|
+
if (runtime.kind === "browser")
|
|
150
|
+
return undefined;
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=user-agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user-agent.js","sourceRoot":"","sources":["../src/user-agent.ts"],"names":[],"mappings":"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"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The package version, in a module of its own so the client can stamp it on the
|
|
3
|
+
* `User-Agent` without importing the public barrel. Kept in sync with
|
|
4
|
+
* `package.json` — enforced by `tests/index.test.ts`.
|
|
5
|
+
*/
|
|
6
|
+
export const SDK_VERSION = "0.22.0";
|
|
7
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,QAAQ,CAAC"}
|
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,11 +86,14 @@ 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).
|
|
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).
|
|
87
90
|
- `artifacts.ts` — the artifact stack, the download twin of `prepareInputs`, in layers: `collectArtifacts` (a pure, exported walk for `pipelex-storage://` 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 filename derivation the MCP server used to own. 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.
|
|
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).
|
|
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.
|
|
92
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`, `collectArtifacts`, `artifactFilename`), and errors.
|
|
93
98
|
|
|
94
99
|
## Client surface (current)
|
|
@@ -115,16 +120,16 @@ The hosted catalog/account routes the webapp drives. All ride the same `{base}/v
|
|
|
115
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.
|
|
116
121
|
- **Gateway API key (LLM inference key):** `createGatewayApiKey` (the JSON body is always sent, even with `promo_code: null`), `getGatewayApiKey`.
|
|
117
122
|
- **Onboarding:** `submitOnboarding`.
|
|
118
|
-
- **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 — `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.
|
|
119
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.
|
|
120
125
|
|
|
121
126
|
## Hook bundle (`dist-hooks/check.mjs`)
|
|
122
127
|
|
|
123
|
-
`src/hooks/` holds the post-edit `.mthds` hook pipeline (local lint/format via `@pipelex/tools-wasm`, then a `POST /v1/validate` verdict through this SDK), bundled into a single dependency-free ESM file by `npm run build:hook`. The one specifier left external is `undici`, reached only by the artifact fetch path through a dynamic import the hook never executes, so the bundle does not inline an HTTP client it cannot use. **Distribution is source-checkout vendoring, not npm:** `pipelex-plugins` runs `make vendor-hook`, which builds `dist-hooks/check.mjs` in a sibling checkout of this repo and copies it in as a static hook asset — the published `@pipelex/sdk` tarball deliberately does not ship the bundle (it inlines the ~4 MB WASM engine, and no npm consumer of the SDK's library API uses it). `dist-hooks/` is gitignored; the consumer repo's own checks gate a stale vendored copy.
|
|
128
|
+
`src/hooks/` holds the post-edit `.mthds` hook pipeline (local lint/format via `@pipelex/tools-wasm`, then a `POST /v1/validate` verdict through this SDK), bundled into a single dependency-free ESM file by `npm run build:hook`. Its validate client is built by `src/hooks/validate-client.ts` with `appInfo: { name: "pipelex-mthds-check", version: SDK_VERSION }`, the `hook` surface's registry token, so the platform attributes its calls to the hook rather than to a plain SDK caller. The one specifier left external is `undici`, reached only by the artifact fetch path through a dynamic import the hook never executes, so the bundle does not inline an HTTP client it cannot use. **Distribution is source-checkout vendoring, not npm:** `pipelex-plugins` runs `make vendor-hook`, which builds `dist-hooks/check.mjs` in a sibling checkout of this repo and copies it in as a static hook asset — the published `@pipelex/sdk` tarball deliberately does not ship the bundle (it inlines the ~4 MB WASM engine, and no npm consumer of the SDK's library API uses it). `dist-hooks/` is gitignored; the consumer repo's own checks gate a stale vendored copy.
|
|
124
129
|
|
|
125
130
|
## Testing
|
|
126
131
|
|
|
127
132
|
Two layers, split by whether a server is in the loop:
|
|
128
133
|
|
|
129
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.
|
|
130
|
-
- **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.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Client identification (`User-Agent` and `appInfo`)
|
|
2
|
+
|
|
3
|
+
Every request `PipelexApiClient` sends to the Pipelex API carries a `User-Agent` header that says which program made it. The hosted platform parses that header into the client surface it reports in product analytics and in its access log, which is how it tells a run started by an SDK script from one started by the web app, the MCP server, the CLI or the coding-agent hook. The convention is shared by every first-party client and is fixed by the workspace spec `docs/specs/client-identification.md`; this page describes how this SDK implements it.
|
|
4
|
+
|
|
5
|
+
## What the SDK sends
|
|
6
|
+
|
|
7
|
+
The value is a sequence of RFC 9110 product tokens, outermost first: the integrator's program (when it names itself through `appInfo`), then this SDK, then the runtime.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pipelex-sdk-js/0.21.0 node/22.4.0 (darwin; arm64)
|
|
11
|
+
acme-invoicer/1.4.0 pipelex-sdk-js/0.21.0 node/22.4.0 (darwin; arm64)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The SDK token is `pipelex-sdk-js/<SDK_VERSION>`, where `SDK_VERSION` is the package version (a test keeps it equal to `package.json`). The runtime token is `node/<version>`, `bun/<version>` or `deno/<version>`, followed by `(<os>; <arch>)`; when the runtime's version cannot be read the runtime token is left out. The SDK calls `fetch` itself, so no other library appears between the SDK token and the runtime.
|
|
15
|
+
|
|
16
|
+
The header is built once, when the client is constructed, and one private helper assembles the headers of every API request, so the protocol routes, the product routes, the extension routes and the origin-level `health()` probe all carry it. The fetch of a presigned object-store link by `fetchArtifact` or `downloadArtifacts` is a request to a third party and carries no header at all, as before.
|
|
17
|
+
|
|
18
|
+
**In a browser the SDK sets no `User-Agent`.** Browsers either ignore the header or turn it into a CORS preflight the API refuses, so a page running the SDK is identified by the browser's own `User-Agent`. The SDK treats a global `window` with a `document`, or a web worker, as a browser.
|
|
19
|
+
|
|
20
|
+
The header is self-declared and unauthenticated. The platform uses it for analytics and diagnostics only, never for authorization, rate limits or entitlements.
|
|
21
|
+
|
|
22
|
+
## Naming your program with `appInfo`
|
|
23
|
+
|
|
24
|
+
An integrator puts its own name in front of the SDK's token by passing `appInfo` at construction, in the same shape as Stripe's option of that name:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { PipelexApiClient, type AppInfo } from "@pipelex/sdk";
|
|
28
|
+
|
|
29
|
+
const appInfo: AppInfo = {
|
|
30
|
+
name: "acme-invoicer",
|
|
31
|
+
version: "1.4.0",
|
|
32
|
+
details: ["batch"],
|
|
33
|
+
url: "https://acme.example",
|
|
34
|
+
};
|
|
35
|
+
const client = new PipelexApiClient({ appInfo });
|
|
36
|
+
// User-Agent: acme-invoicer/1.4.0 (batch; +https://acme.example) pipelex-sdk-js/0.21.0 node/22.4.0 (darwin; arm64)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
| Field | Required | Meaning |
|
|
40
|
+
| --- | --- | --- |
|
|
41
|
+
| `name` | yes | An RFC 9110 token: ASCII letters, digits and the punctuation RFC 9110 allows in a token (no spaces, slashes, parentheses or semicolons), lowercase kebab-case by convention |
|
|
42
|
+
| `version` | no | A token, such as `1.4.0` |
|
|
43
|
+
| `url` | no | A URL, rendered in the comment as `+url` |
|
|
44
|
+
| `details` | no | Comment parameters, each a token or `token=value` (the value may be `name/version`), rendered before `+url` |
|
|
45
|
+
|
|
46
|
+
It renders as `name/version (<details>; +url)`, dropping the `/version` and the comment when they are empty. The constructor validates it and throws a `TypeError` naming the field when a value falls outside the grammar, and a `RangeError` when the whole header would exceed the spec's 512-character ceiling, in a browser as on a server although no header is sent there; the SDK never silently drops or rewrites a field. The header must never carry a secret, a user identifier, an email or a hostname.
|
|
47
|
+
|
|
48
|
+
First-party programs name themselves the same way. The coding-agent validation hook bundled in `dist-hooks/check.mjs` constructs its client with `appInfo: { name: "pipelex-mthds-check", version: SDK_VERSION }` (`src/hooks/validate-client.ts`), so its requests read:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
pipelex-mthds-check/0.21.0 pipelex-sdk-js/0.21.0 node/22.4.0 (darwin; arm64)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A subclass of `PipelexApiClient`, such as the MCP server's size-guarded client, inherits the option through the constructor and passes its own `appInfo` the same way.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Input preparation (`uploadFile` / `prepareInputs`)
|
|
2
2
|
|
|
3
|
-
> **Status: implemented** (`src/upload.ts`, `src/prepare-inputs.ts`). This document records the contract, and is the public account of it. The raw `upload()` primitive described in [architecture.md](./architecture.md) is the wire call `uploadFile` and `prepareInputs` build on.
|
|
3
|
+
> **Status: implemented** (`src/upload.ts`, `src/upload-grant.ts`, `src/prepare-inputs.ts`). This document records the contract, and is the public account of it. The raw `upload()` primitive described in [architecture.md](./architecture.md) is the wire call `uploadFile` and `prepareInputs` build on.
|
|
4
4
|
>
|
|
5
5
|
> **Current scope.** `prepareInputs` names the method the same three ways every other method-taking operation does — inline `files`, a `method_ref` address, or a stored `method_id` — exactly one per call, all three resolved server-side by the one `validate` call it composes. One piece remains deliberately deferred and additive (it does not change this contract): the opt-in ingest of `http(s)` URLs into storage — for now an `http(s)` URL at a file position always passes through unchanged.
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ A hosted run cannot see the caller's filesystem or a browser's selected bytes. T
|
|
|
10
10
|
|
|
11
11
|
Preparation is **explicit and separate from running.** `execute` / `start` never silently upload local files. The payoff: file-access errors happen *before* a run exists, prepared inputs are inspectable and reusable across a model sweep or retries without re-uploading, and `start` keeps a deterministic JSON-input contract.
|
|
12
12
|
|
|
13
|
-
## The
|
|
13
|
+
## The operations
|
|
14
14
|
|
|
15
15
|
### `uploadFile` — single-asset convenience
|
|
16
16
|
|
|
@@ -21,7 +21,7 @@ Uploads one asset and returns its upload record. It is the language-native conve
|
|
|
21
21
|
- A string that is an **HTTP(S) URL** or an existing **`pipelex-storage://` URI** is not a local asset and passes through in any runtime (see pass-through rules below).
|
|
22
22
|
- Open file objects and streams are **deferred** — they can be added later without removing anything.
|
|
23
23
|
|
|
24
|
-
> **Runtime acceptance vs. bundling.** These source types are accepted at *runtime* in every environment, but at the *packaging* level `@pipelex/sdk` is **Node-first**: it statically references `node:fs/promises` (to read path strings), so bundling
|
|
24
|
+
> **Runtime acceptance vs. bundling.** These source types are accepted at *runtime* in every environment, but at the *packaging* level the main `@pipelex/sdk` entry is **Node-first**: it statically references `node:fs/promises` (to read path strings), so bundling it for a browser or edge target requires marking `node:*` external. This is a deliberate stance — the client holds an API key, so it runs server-side (Next.js Server Actions, the MCP server, the plugins hook). A browser page that must store a file uses the upload grant below instead, whose sender ships from the browser-safe `@pipelex/sdk/upload` entry.
|
|
25
25
|
|
|
26
26
|
The returned **upload record** guarantees, beyond the source identity:
|
|
27
27
|
|
|
@@ -35,6 +35,55 @@ The returned **upload record** guarantees, beyond the source identity:
|
|
|
35
35
|
|
|
36
36
|
The MIME type and size are known client-side, so the record is assembled without extending the `/v1/upload` response.
|
|
37
37
|
|
|
38
|
+
### Uploading with a grant — `requestUploadGrant` / `uploadWithGrant`
|
|
39
|
+
|
|
40
|
+
`uploadFile` needs the client, and the client needs the API key. A browser page holds the bytes but must never hold the key, so the work is split in two: the side holding the key requests a **grant** for one file, hands it to the page, and the page sends the file straight to storage with it. The bytes cross neither the credential-holding server nor the API gateway, so the gateway's request quota, which caps a base64 `/v1/upload` near 7.5 MiB, does not apply: the file may be as large as the service's own limit, which the grant reports as `max_bytes`.
|
|
41
|
+
|
|
42
|
+
**Requesting the grant** is a client method over `POST /v1/upload/grant`:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const grant = await client.requestUploadGrant({
|
|
46
|
+
filename: file.name,
|
|
47
|
+
content_type: file.type, // optional; empty or omitted signs no type
|
|
48
|
+
size: file.size,
|
|
49
|
+
});
|
|
50
|
+
// { uri, url, headers, expires_at, max_bytes }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The file is described, never sent. The grant (`UploadGrant`) is a presigned, create-only `PUT` for one new object under the caller's organization: `url` is where to send it, `headers` are the signed headers to send unchanged, `uri` is the `pipelex-storage://` reference the object will carry, and `expires_at` is when storage stops accepting it, a few minutes later. The wire is snake_case like every product route, so the grant can travel to the page as JSON without a remap. A declared `size` over the limit is a `413` `ApiResponseError` whose `code` is `payload_too_large`, and a deployment without the route answers a `404`.
|
|
54
|
+
|
|
55
|
+
**The grant is a bearer capability.** Until it expires, whoever holds it can write that one object, so the SDK never logs it, and its caller should not either. The route never replays a grant for a repeated request: a caller that lost one asks for a new one.
|
|
56
|
+
|
|
57
|
+
**Sending the file** is the standalone `uploadWithGrant(grant, file, { signal? })`, which takes a `Blob` or a `File` and returns `{ uri }`:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { uploadWithGrant, RejectedAssetError } from "@pipelex/sdk/upload";
|
|
61
|
+
|
|
62
|
+
const { uri } = await uploadWithGrant(grant, file);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
It sends the file as the raw body of a `PUT` to `grant.url`, with `grant.headers` exactly as received. The grant signed `Content-Length` too, and the body sets it, so the file must be exactly the declared size. Redirects are refused rather than followed, because a presigned URL is valid only at the address it was signed for. **The `uri` names nothing until the `PUT` has succeeded**, so fill a form field or a run input with it only after `uploadWithGrant` resolves.
|
|
66
|
+
|
|
67
|
+
**The browser-safe entry.** `uploadWithGrant`, the grant types and the three error classes it raises (`InputPreparationError`, `RejectedAssetError`, `UploadTransportError`) are exported from `@pipelex/sdk/upload`, whose import graph reaches no Node builtin, no `undici` and not the client, so a page bundles it with nothing marked external. They are the same function and the same classes the main entry exports, so an `instanceof` check agrees whichever entry the error came through. The unit suite bundles the entry for the browser with esbuild and fails if anything Node-only becomes reachable.
|
|
68
|
+
|
|
69
|
+
**Storage's refusals** map onto the input-preparation errors, so a caller that already handles `uploadFile` needs no new case:
|
|
70
|
+
|
|
71
|
+
| Storage answers | Error | `code` | Meaning |
|
|
72
|
+
| --- | --- | --- | --- |
|
|
73
|
+
| `2xx` | none | | Stored; `uri` names the object. |
|
|
74
|
+
| `412 PreconditionFailed` | `RejectedAssetError` | `grant_used` | The grant was already used. A grant writes one object, once — so after an earlier attempt that failed at storage, the grant's `uri` may already name the file. |
|
|
75
|
+
| `403 SignatureDoesNotMatch` | `RejectedAssetError` | `signature_mismatch` | The file's size, content type or metadata differ from what the grant signed. |
|
|
76
|
+
| `403 AccessDenied`, "Request has expired" | `RejectedAssetError` | `grant_expired` | The grant expired. |
|
|
77
|
+
| `403 AccessDenied`, headers "not signed" | `RejectedAssetError` | `unsigned_header` | The request carried a storage header the grant did not sign. |
|
|
78
|
+
| `400 RequestTimeout` | `UploadTransportError` | | Storage stopped waiting for the body and wrote nothing, so the same grant can be retried while it is valid. |
|
|
79
|
+
| any other `4xx` | `RejectedAssetError` | `store_refused` | Storage's own message is relayed. |
|
|
80
|
+
| `5xx` | `UploadTransportError` | | Whether the object was written is unknown: retrying with the same grant either stores it or answers the `412` of a used grant. |
|
|
81
|
+
| a redirect | `UploadTransportError` | | Refused, not followed. |
|
|
82
|
+
| no response | `UploadTransportError` | | Storage unreachable. In a browser a refused cross-origin request looks exactly like this, so the message also points at the page's CSP `connect-src`. |
|
|
83
|
+
| nothing sent | `UploadTransportError` | | The grant's `url` is not an absolute `http(s)` URL free of user info, so it is refused before any request. A browser would resolve a relative URL against the page and send the credential there. |
|
|
84
|
+
|
|
85
|
+
`RejectedAssetError.code` is what a caller branches on — to request a new grant on `grant_used` or `grant_expired`, say — rather than the message or the status, since an expired grant and an unsigned header both answer `403 AccessDenied` and only S3's message tells them apart. Its `status` is storage's, and its `filename` is the `File`'s name, or the object name the grant's `uri` ends in for a nameless `Blob`. `UploadTransportError.status` is storage's status when storage answered, and undefined when it did not. Neither error wraps storage's response, and only S3's error code and message reach a message: its error body can echo the signed request, credential included, so it is never kept. **No error carries the grant's URL.** The runtime's network error can: Node and Chrome name the whole URL when they cannot build a request from it, and Bun puts it on the error's `path`. So an unreachable storage is described by its origin and by the error names and codes along the cause chain (`TypeError, caused by Error ENOTFOUND`), and the runtime's error is neither relayed nor kept as `cause`. A caller's abort propagates as the signal's own reason, unwrapped, including one that lands while storage's error body is still arriving.
|
|
86
|
+
|
|
38
87
|
### `prepareInputs` — signature-driven input preparation
|
|
39
88
|
|
|
40
89
|
```
|
|
@@ -173,10 +222,10 @@ The contract distinguishes these semantic outcomes, each a typed subclass of `In
|
|
|
173
222
|
- **an unresolvable signature** (`InputPreparationError`) — the closure did not validate (`is_valid: false`, carrying the first error's message), the report carried no `input_form` descriptor, or no pipe could be chosen (an unknown or bare `pipe_ref`, or no default). A *no-verdict* condition from `/v1/validate` — a malformed selector, an unknown or foreign-org `method_id`, no package at the address, auth, a server fault — stays an `ApiResponseError` and propagates unchanged;
|
|
174
223
|
- **empty method source** (`EmptyMethodSourceError`, carries `methodId`) — `getMethodClosure` found the stored method but its `mthds` source parses to nothing (the row exists, no runnable source yet). Distinct from the `getMethod` `404` for an unknown/foreign id, which stays an `ApiResponseError`. `prepareInputs` never raises it: it hands the id to the server, which answers a sourceless method with a `422`;
|
|
175
224
|
- **invalid local source** (`InvalidLocalSourceError`) — missing, unreadable, or a path string outside Node;
|
|
176
|
-
- **rejected asset** (`RejectedAssetError`) — the server refused it (e.g. a `413` past the service-defined size cap — see "Storage policy" — surfaced as a clear rejection
|
|
225
|
+
- **rejected asset** (`RejectedAssetError`) — the server refused it (e.g. a `413` past the service-defined size cap — see "Storage policy" — surfaced as a clear rejection with `code` `too_large`, not a raw transport error), or storage refused an upload with a grant (a used or expired grant, or a file that differs from what the grant signed — see the table above for each `code`);
|
|
177
226
|
- **unsupported server capability** (`UnsupportedUploadCapabilityError`) — the configured deployment has no upload route;
|
|
178
227
|
- **authentication / authorization failure** (`UploadAuthenticationError`) — `401` / `403`;
|
|
179
|
-
- **transport failure** (`UploadTransportError`) — a network or server fault, a malformed data URL payload, or any other unexpected upload failure.
|
|
228
|
+
- **transport failure** (`UploadTransportError`) — a network or server fault, a malformed data URL payload, or any other unexpected upload failure; its `status` is the HTTP status when a response produced it.
|
|
180
229
|
|
|
181
230
|
All preparation failures are raised **before any run is created**.
|
|
182
231
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pipelex/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "TypeScript SDK for the Pipelex hosted API — execute methods, manage runs, and call the product surface.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"import": "./dist/index.js",
|
|
20
20
|
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./upload": {
|
|
23
|
+
"types": "./dist/upload-grant.d.ts",
|
|
24
|
+
"import": "./dist/upload-grant.js",
|
|
25
|
+
"default": "./dist/upload-grant.js"
|
|
21
26
|
}
|
|
22
27
|
},
|
|
23
28
|
"scripts": {
|