@pipelex/sdk 0.59.0 → 0.61.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 +14 -2
- package/dist/cli/script.d.ts +1 -1
- package/dist/client.d.ts +132 -65
- package/dist/client.js +296 -82
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/models.d.ts +7 -3
- package/dist/product-models.d.ts +214 -56
- package/dist/product-models.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webhooks.d.ts +19 -1
- package/docs/architecture.md +6 -5
- package/docs/crate-routes.md +5 -5
- package/docs/errors.md +2 -0
- package/docs/files.md +1 -0
- package/docs/input-preparation.md +1 -1
- package/docs/method-copy.md +14 -19
- package/docs/method-trees.md +1 -1
- package/docs/public-methods.md +97 -0
- package/docs/webhooks.md +15 -4
- package/package.json +1 -1
package/dist/product-models.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pipelex-product wire models — the snake_case JSON shapes the hosted-product
|
|
3
|
-
* routes (`/v1/me`, `/v1/methods` and its examples, `/v1/organizations`, `/v1/branding`, `/v1/billing/*`,
|
|
3
|
+
* routes (`/v1/me`, `/v1/methods` and its examples, `/v1/method-templates`, `/v1/organizations`, `/v1/branding`, `/v1/billing/*`,
|
|
4
4
|
* `/v1/pipelex-api-keys`, `/v1/onboarding/submit`,
|
|
5
5
|
* `/v1/resolve-storage-url`, `/v1/upload`, `/v1/upload/grant`, `/v1/files`, `/v1/runs`) speak.
|
|
6
6
|
*
|
|
@@ -39,9 +39,31 @@ export interface SavedMethodFile {
|
|
|
39
39
|
/** ISO-8601 UTC instant the link stops working, about fifteen minutes after the read. */
|
|
40
40
|
expires_at: string;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* One image directly under a method tree's `thumbnails/` folder, as the publish that recorded it on
|
|
44
|
+
* a version measured it from its bytes. Its aspect ratio is never declared: read it from `width`
|
|
45
|
+
* and `height`, and draw the thumbnail closest to the shape you draw. A version records only the
|
|
46
|
+
* valid ones (a PNG, JPEG or WebP of at least 800×400 pixels), so an image the platform refused is
|
|
47
|
+
* simply absent.
|
|
48
|
+
*/
|
|
49
|
+
export interface MethodThumbnail {
|
|
50
|
+
/** The image's tree path, `thumbnails/` and its file name. */
|
|
51
|
+
path: string;
|
|
52
|
+
/** The media type sniffed from its bytes: `image/png`, `image/jpeg` or `image/webp`. */
|
|
53
|
+
media_type: string;
|
|
54
|
+
/** Its width in pixels, as a browser draws it. */
|
|
55
|
+
width: number;
|
|
56
|
+
/** Its height in pixels, as a browser draws it. */
|
|
57
|
+
height: number;
|
|
58
|
+
/** The SHA-256 of its bytes, 64 lowercase hex characters: its hash in the tree. */
|
|
59
|
+
sha256: string;
|
|
60
|
+
}
|
|
42
61
|
/**
|
|
43
62
|
* One published version without its files — what `listMethodVersions` lists, what a
|
|
44
63
|
* method read carries as `latest_published`, and what a publish answers as `version`.
|
|
64
|
+
*
|
|
65
|
+
* Read from another organization's public method, it carries no `published_by`: the
|
|
66
|
+
* publisher's user id does not cross organizations.
|
|
45
67
|
*/
|
|
46
68
|
export interface MethodVersionSummary {
|
|
47
69
|
/** The version number, from 1, never reused and never renumbered. */
|
|
@@ -61,20 +83,33 @@ export interface MethodVersionSummary {
|
|
|
61
83
|
crate_fingerprint: string | null;
|
|
62
84
|
/** The version of the runner that validated the bundle and computed the fingerprint. */
|
|
63
85
|
runner_version: string | null;
|
|
64
|
-
/** The
|
|
86
|
+
/** The method's description at publish time, as the draft held it (see `MethodData.description`). */
|
|
65
87
|
description: string | null;
|
|
88
|
+
/**
|
|
89
|
+
* The method's categories at publish time, as slugs of the catalog's list (see
|
|
90
|
+
* `MethodData.categories`); absent from a platform that predates them.
|
|
91
|
+
*/
|
|
92
|
+
categories?: string[];
|
|
93
|
+
/**
|
|
94
|
+
* The valid images directly under the version's `thumbnails/` folder, in tree path order, as
|
|
95
|
+
* the publish measured them; empty when the version records none, and absent from a platform
|
|
96
|
+
* that predates them.
|
|
97
|
+
*/
|
|
98
|
+
thumbnails?: MethodThumbnail[];
|
|
66
99
|
/** ISO-8601 UTC instant of the publish. */
|
|
67
100
|
published_at: string;
|
|
68
101
|
/**
|
|
69
102
|
* The publisher's canonical user id, or `system:publish-initial` for a version the one-time
|
|
70
|
-
* migration wrote for a method saved before versions existed.
|
|
103
|
+
* migration wrote for a method saved before versions existed. Absent when another organization
|
|
104
|
+
* reads a public method.
|
|
71
105
|
*/
|
|
72
|
-
published_by
|
|
106
|
+
published_by?: string;
|
|
73
107
|
}
|
|
74
108
|
/**
|
|
75
109
|
* One published version with its files — `getMethodVersion`. It carries no `name`, which
|
|
76
110
|
* belongs to the method and changes without a publish, and no `input_data`, which is the
|
|
77
|
-
* editor's form state and not part of what a caller runs.
|
|
111
|
+
* editor's form state and not part of what a caller runs. Read from another organization's public
|
|
112
|
+
* method, it carries no `published_by`, and its files are linked in the organization that holds it.
|
|
78
113
|
*/
|
|
79
114
|
export interface MethodVersion extends MethodVersionSummary {
|
|
80
115
|
method_id: string;
|
|
@@ -83,7 +118,8 @@ export interface MethodVersion extends MethodVersionSummary {
|
|
|
83
118
|
}
|
|
84
119
|
/**
|
|
85
120
|
* A saved method — `createMethod`, `writeDraft`, `renameMethod`, and the `method` of every
|
|
86
|
-
* publish outcome; `getMethod` answers it with the draft's files (`MethodDetail`)
|
|
121
|
+
* publish outcome; `getMethod` answers it with the draft's files (`MethodDetail`) for the caller's
|
|
122
|
+
* own method, and another organization's public one as a `PublicMethodDetail`. Its draft is
|
|
87
123
|
* named by `draft_digest`; the latest published version is summarized in `latest_published`.
|
|
88
124
|
*
|
|
89
125
|
* The publish state a client shows is derived, never stored: never published
|
|
@@ -101,11 +137,19 @@ export interface MethodData {
|
|
|
101
137
|
/** Legacy persisted output spec; optional. */
|
|
102
138
|
pipe_output?: Record<string, unknown> | null;
|
|
103
139
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* the
|
|
140
|
+
* The draft's description, read from its files: the `description` of its `METHODS.toml`
|
|
141
|
+
* manifest, else the top-level `description` of its primary bundle, else its main pipe's, a
|
|
142
|
+
* blank one counting as none at each step. Read-side only: the server recomputes it from the
|
|
143
|
+
* draft on every draft write, so it never appears on the write contract.
|
|
107
144
|
*/
|
|
108
145
|
description?: string | null;
|
|
146
|
+
/**
|
|
147
|
+
* The draft's categories, read from its manifest's `categories` under `[package]` and kept when
|
|
148
|
+
* they are on the catalog's list, as slugs such as `human-resources`, in their declared order;
|
|
149
|
+
* empty for a draft with no manifest or none it knows, and absent from a platform that predates
|
|
150
|
+
* them. A person reads a category's label, which `listMethodTemplates` names beside its slug.
|
|
151
|
+
*/
|
|
152
|
+
categories?: string[];
|
|
109
153
|
created_at: string;
|
|
110
154
|
/**
|
|
111
155
|
* **The draft's token.** It moves on every draft write and on nothing else: creation sets
|
|
@@ -138,7 +182,17 @@ export interface MethodData {
|
|
|
138
182
|
* is made, and never rewritten. The list rows (`MethodSummary`) do not carry it.
|
|
139
183
|
*/
|
|
140
184
|
copied_from?: MethodCopiedFrom | null;
|
|
185
|
+
/**
|
|
186
|
+
* Who may see the method: `internal`, the members of the organization that holds it, or
|
|
187
|
+
* `public`, every organization, which reads, runs and copies its published versions. Only an
|
|
188
|
+
* admin of the platform sets it, and no route of this client writes it. Absent from a platform
|
|
189
|
+
* that predates it, which means `internal`, and read open, so a value a later platform adds
|
|
190
|
+
* arrives as its raw string. The list rows (`MethodSummary`) do not carry it.
|
|
191
|
+
*/
|
|
192
|
+
visibility?: MethodVisibility | (string & {});
|
|
141
193
|
}
|
|
194
|
+
/** Who may see a method — `MethodData.visibility`. */
|
|
195
|
+
export type MethodVisibility = "internal" | "public";
|
|
142
196
|
/**
|
|
143
197
|
* The method a copy was made from, as it stood at the copy — `MethodData.copied_from`. Opaque ids
|
|
144
198
|
* only, never a name: the original may have changed or been erased since, and its organization
|
|
@@ -150,17 +204,59 @@ export interface MethodCopiedFrom {
|
|
|
150
204
|
org_id: string;
|
|
151
205
|
/** The original's method id. */
|
|
152
206
|
method_id: string;
|
|
153
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* The digest of the original's draft the copy took; `null` for an empty draft, and for a copy of
|
|
209
|
+
* another organization's public method, which takes a published version rather than the draft.
|
|
210
|
+
*/
|
|
154
211
|
draft_digest: string | null;
|
|
212
|
+
/**
|
|
213
|
+
* The published version the copy took, on a copy of another organization's public method;
|
|
214
|
+
* `null` or absent on a copy of a draft.
|
|
215
|
+
*/
|
|
216
|
+
version?: number | null;
|
|
155
217
|
}
|
|
156
218
|
/**
|
|
157
|
-
* A saved method with its draft's files — what `getMethod` answers
|
|
158
|
-
*
|
|
159
|
-
*
|
|
219
|
+
* A saved method of the caller's organization with its draft's files — what `getMethod` answers
|
|
220
|
+
* for one of the caller's own methods. `files` lists the draft's tree in tree path order, each
|
|
221
|
+
* file with its SHA-256, its size and a link to its bytes valid for about fifteen minutes, and is
|
|
222
|
+
* empty for an empty draft; `writeMethodTree` brings it to disk.
|
|
160
223
|
*/
|
|
161
224
|
export interface MethodDetail extends MethodData {
|
|
162
225
|
files: SavedMethodFile[];
|
|
163
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Another organization's public method, as a reader outside that organization sees it — what
|
|
229
|
+
* `getMethod` answers for a method of another organization that is public. It is the latest
|
|
230
|
+
* published version, never the draft: `description` and `categories` are that version's, and
|
|
231
|
+
* `files` is its tree, linked in the organization that holds it, which `writeMethodTree` writes
|
|
232
|
+
* as it writes any listing.
|
|
233
|
+
*
|
|
234
|
+
* It carries nothing of the draft and nothing of the owner's people: no `updated_at` (the draft's
|
|
235
|
+
* token), `draft_digest`, `input_data`, `pipe_output`, `created_by_user_id`, `copied_from` or
|
|
236
|
+
* `deletion_state`, and its `latest_published` no `published_by`. So `"updated_at" in method`
|
|
237
|
+
* tells a `MethodDetail` from it. Every write of it is the owner's alone: from another
|
|
238
|
+
* organization, a write answers `404` `pipelex.catalog.not_found`, while `copyMethod` copies it and
|
|
239
|
+
* the run routes run it by its bare id or `mt_…@<n>`, never `@draft`.
|
|
240
|
+
*/
|
|
241
|
+
export interface PublicMethodDetail {
|
|
242
|
+
method_id: string;
|
|
243
|
+
/** The canonical id of the organization that holds the method. */
|
|
244
|
+
org_id: string;
|
|
245
|
+
name: string;
|
|
246
|
+
/** The latest published version's description. */
|
|
247
|
+
description: string | null;
|
|
248
|
+
/** The latest published version's categories, as slugs of the catalog's list. */
|
|
249
|
+
categories: string[];
|
|
250
|
+
created_at: string;
|
|
251
|
+
/** The number of the latest published version, the one a reader reads, runs and copies. */
|
|
252
|
+
latest_version: number;
|
|
253
|
+
/** The latest published version's summary, without its publisher. */
|
|
254
|
+
latest_published: MethodVersionSummary;
|
|
255
|
+
/** `public`: another organization reads a method only while it is public. */
|
|
256
|
+
visibility: MethodVisibility | (string & {});
|
|
257
|
+
/** The latest published version's tree, one entry per file in tree path order. */
|
|
258
|
+
files: SavedMethodFile[];
|
|
259
|
+
}
|
|
164
260
|
/** Where a method is in the erasure cascade; absent on a normal method. */
|
|
165
261
|
export type MethodDeletionState = "pending" | "in_progress" | "failed";
|
|
166
262
|
/**
|
|
@@ -315,10 +411,22 @@ export interface MethodPublishInput {
|
|
|
315
411
|
/** What a publish did with the draft — the discriminant of `MethodPublishResult`. */
|
|
316
412
|
export type MethodPublishOutcome = "published" | "unchanged" | "refused";
|
|
317
413
|
/**
|
|
318
|
-
* Why a draft was not published:
|
|
319
|
-
*
|
|
414
|
+
* Why a draft was not published:
|
|
415
|
+
*
|
|
416
|
+
* - `invalid`: it does not validate;
|
|
417
|
+
* - `not_runnable`: it validates with pending signatures, so it is valid but does not run yet;
|
|
418
|
+
* - `thumbnail_missing`: the method is public, and the draft's tree holds no file directly under
|
|
419
|
+
* `thumbnails/`, or none was read in time;
|
|
420
|
+
* - `thumbnail_invalid`: the method is public, and no file directly under the draft's
|
|
421
|
+
* `thumbnails/` is a valid thumbnail (a PNG, JPEG or WebP of at least 800×400 pixels);
|
|
422
|
+
* - `storage_reference`: the method is public, and a `.mthds` file of the draft names a
|
|
423
|
+
* `pipelex-storage://` file, which other organizations cannot read.
|
|
424
|
+
*
|
|
425
|
+
* `thumbnail_missing`, `thumbnail_invalid` and `storage_reference` refuse only a public method's
|
|
426
|
+
* publish, so that the card of a template never loses its picture and every organization can run
|
|
427
|
+
* what it reads.
|
|
320
428
|
*/
|
|
321
|
-
export type MethodPublishRefusalReason = "invalid" | "not_runnable";
|
|
429
|
+
export type MethodPublishRefusalReason = "invalid" | "not_runnable" | "thumbnail_missing" | "thumbnail_invalid" | "storage_reference";
|
|
322
430
|
/** A publish that wrote a new version. */
|
|
323
431
|
export interface MethodPublished {
|
|
324
432
|
outcome: "published";
|
|
@@ -339,14 +447,22 @@ export interface MethodPublishUnchanged {
|
|
|
339
447
|
/** A publish the draft's content refused: nothing was published. */
|
|
340
448
|
export interface MethodPublishRefused {
|
|
341
449
|
outcome: "refused";
|
|
342
|
-
|
|
343
|
-
|
|
450
|
+
/**
|
|
451
|
+
* Why: a `MethodPublishRefusalReason`, or the raw string of one this SDK does not know, which a
|
|
452
|
+
* later platform may add. A refusal is a verdict whatever its reason, so show its `message`
|
|
453
|
+
* when the reason is one you do not handle.
|
|
454
|
+
*/
|
|
455
|
+
reason: MethodPublishRefusalReason | (string & {});
|
|
456
|
+
/**
|
|
457
|
+
* Says why, for a person; for `not_runnable`, that the draft "is valid but does not run yet",
|
|
458
|
+
* and for the thumbnail refusals, what the image must be.
|
|
459
|
+
*/
|
|
344
460
|
message: string;
|
|
345
461
|
/**
|
|
346
462
|
* The runner's `POST /v1/validate` answer for the draft, read member by member as `validate`
|
|
347
463
|
* reads it: an invalid arm whose `errors`, problem items, say what to fix, or, for
|
|
348
|
-
* `not_runnable
|
|
349
|
-
* standard declares is kept only when it is well-formed.
|
|
464
|
+
* `not_runnable` and the refusals of a public method, the valid arm. An entry that names no
|
|
465
|
+
* fault is left out, and a member the standard declares is kept only when it is well-formed.
|
|
350
466
|
*/
|
|
351
467
|
validation: PipelexValidationResult;
|
|
352
468
|
method: MethodData;
|
|
@@ -379,6 +495,81 @@ export interface MethodVersionPage {
|
|
|
379
495
|
items: MethodVersionSummary[];
|
|
380
496
|
nextCursor: string | null;
|
|
381
497
|
}
|
|
498
|
+
/** One category of the catalog's list: the slug a manifest declares, and the English label a person reads. */
|
|
499
|
+
export interface MethodCategory {
|
|
500
|
+
/** The stable slug, such as `human-resources`, which a method's `categories` hold. */
|
|
501
|
+
slug: string;
|
|
502
|
+
/** The English label, such as `Human resources`; a client translating by slug falls back on it. */
|
|
503
|
+
label: string;
|
|
504
|
+
}
|
|
505
|
+
/** One thumbnail of a template's latest published version, with a link to its bytes. */
|
|
506
|
+
export interface MethodTemplateThumbnail extends MethodThumbnail {
|
|
507
|
+
/**
|
|
508
|
+
* A link to the image's bytes, signed by storage, which a browser loads with no credential. It is
|
|
509
|
+
* a bearer credential until it expires: keep it out of logs, messages and stored records.
|
|
510
|
+
*/
|
|
511
|
+
url: string;
|
|
512
|
+
/** ISO-8601 UTC instant the link stops working, about fifteen minutes after the read. */
|
|
513
|
+
expires_at: string;
|
|
514
|
+
}
|
|
515
|
+
/** One card of the template catalog: a template as its latest published version shows it. */
|
|
516
|
+
export interface MethodTemplate {
|
|
517
|
+
/** The template's catalog id, which `getMethod`, the run routes and `copyMethod` take. */
|
|
518
|
+
method_id: string;
|
|
519
|
+
/** The method's name. */
|
|
520
|
+
name: string;
|
|
521
|
+
/** The latest published version's description; `null` when it has none. */
|
|
522
|
+
description: string | null;
|
|
523
|
+
/** The number of the latest published version, the one a reader reads, runs and copies. */
|
|
524
|
+
latest_version: number;
|
|
525
|
+
/** The latest published version's categories, as slugs of the catalog's list. */
|
|
526
|
+
categories: string[];
|
|
527
|
+
/** The display name of the organization that holds the template. */
|
|
528
|
+
organization_name: string;
|
|
529
|
+
/**
|
|
530
|
+
* The latest published version's thumbnails, each with its size and a link; draw the one whose
|
|
531
|
+
* `width` and `height` come closest to the shape you draw.
|
|
532
|
+
*/
|
|
533
|
+
thumbnails: MethodTemplateThumbnail[];
|
|
534
|
+
}
|
|
535
|
+
/** Query for one page of the template catalog — `listMethodTemplates`. */
|
|
536
|
+
export interface ListMethodTemplatesQuery {
|
|
537
|
+
/**
|
|
538
|
+
* The ordering to serve, such as `default` or an arm of a test; an absent, unknown or malformed
|
|
539
|
+
* name serves `default`. The answer's `ordering` names the one served.
|
|
540
|
+
*/
|
|
541
|
+
ordering?: string;
|
|
542
|
+
/**
|
|
543
|
+
* Keep the templates of this category, by its slug, in the same order; a slug no template shows
|
|
544
|
+
* keeps none.
|
|
545
|
+
*/
|
|
546
|
+
category?: string;
|
|
547
|
+
/** Page size, from 1 to 200; the API defaults to 50 and refuses one outside that range with a `422`. */
|
|
548
|
+
limit?: number;
|
|
549
|
+
/** Opaque `nextCursor` from the previous page, sent with the same `ordering` and `category`. */
|
|
550
|
+
cursor?: string;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* One page of the template catalog, in the order served, kept to the category asked for.
|
|
554
|
+
*
|
|
555
|
+
* `nextCursor` is opaque — pass it back to `listMethodTemplates` with the same `ordering` and
|
|
556
|
+
* `category`, which the cursor does not hold; `null` means this was the last page. A page resumes
|
|
557
|
+
* after the last template the page before served, wherever the catalog now places it, so a template
|
|
558
|
+
* added or removed elsewhere between two pages neither repeats nor skips the ones around that point.
|
|
559
|
+
* `ordering` and `categories` describe the whole catalog on every page.
|
|
560
|
+
*/
|
|
561
|
+
export interface MethodTemplatePage {
|
|
562
|
+
/** The page's templates: those the ordering lists first, in its order, then the others by name. */
|
|
563
|
+
items: MethodTemplate[];
|
|
564
|
+
nextCursor: string | null;
|
|
565
|
+
/** The name of the ordering served; `null` when there is none, the templates then listed by name. */
|
|
566
|
+
ordering: string | null;
|
|
567
|
+
/**
|
|
568
|
+
* Every category at least one template shows, whatever the filter, in the catalog's order, each
|
|
569
|
+
* with its label: the tabs a client draws, none of them ever empty.
|
|
570
|
+
*/
|
|
571
|
+
categories: MethodCategory[];
|
|
572
|
+
}
|
|
382
573
|
/**
|
|
383
574
|
* The copy — `copyMethod`, `POST /v1/methods/{id}/copy`. Its wire body is closed: the client
|
|
384
575
|
* sends these members and nothing else, a `null` reading as not given.
|
|
@@ -396,6 +587,9 @@ export interface MethodCopyInput {
|
|
|
396
587
|
* The original's draft token (`MethodData.updated_at`) the caller last saw, echoed verbatim: a
|
|
397
588
|
* draft that moved since is refused with a `409` `pipelex.catalog.update_conflict`, and nothing
|
|
398
589
|
* is copied. Omitted or `null`, the copy takes the draft as it stands when the request is served.
|
|
590
|
+
* Leave it out when copying another organization's public method: that copy takes the latest
|
|
591
|
+
* published version, never the draft, and the field sent with it is refused with a `422`
|
|
592
|
+
* `request.invalid` naming it.
|
|
399
593
|
*/
|
|
400
594
|
expected_draft_updated_at?: string | null;
|
|
401
595
|
}
|
|
@@ -467,42 +661,6 @@ export interface MethodCopyLimit {
|
|
|
467
661
|
value: number;
|
|
468
662
|
reached: number;
|
|
469
663
|
}
|
|
470
|
-
/** Query for one page of a method's copies. */
|
|
471
|
-
export interface ListMethodCopiesQuery {
|
|
472
|
-
/**
|
|
473
|
-
* Page size, from 1 to 100; the API defaults to 50 and refuses a `limit` outside that range
|
|
474
|
-
* with a `422`.
|
|
475
|
-
*/
|
|
476
|
-
limit?: number;
|
|
477
|
-
/** Opaque `nextCursor` from the previous page. */
|
|
478
|
-
cursor?: string;
|
|
479
|
-
}
|
|
480
|
-
/**
|
|
481
|
-
* The original's record of one copy made of it — a row of `listMethodCopies`. It names the
|
|
482
|
-
* destination by its id alone, never its name, so the listing tells no reader the name of an
|
|
483
|
-
* organization they do not belong to: show a destination's name only from the reader's own
|
|
484
|
-
* memberships. The marker stays when its copy is erased, and the copy then reads as not found.
|
|
485
|
-
*/
|
|
486
|
-
export interface MethodCopyMarker {
|
|
487
|
-
/** The copy's method id, in the destination organization. */
|
|
488
|
-
copy_method_id: string;
|
|
489
|
-
/** The canonical id of the organization the copy was made in. */
|
|
490
|
-
destination_org_id: string;
|
|
491
|
-
/** The canonical id of the member who made the copy. */
|
|
492
|
-
copied_by_user_id: string;
|
|
493
|
-
/** ISO 8601 UTC instant the copy's row was written. */
|
|
494
|
-
copied_at: string;
|
|
495
|
-
}
|
|
496
|
-
/**
|
|
497
|
-
* One page of a method's copies, in the order of the copies' ids, which says nothing of when each
|
|
498
|
-
* was made. `nextCursor` is opaque — pass it straight back to `listMethodCopies` to continue;
|
|
499
|
-
* `null` means this was the last page. A method never copied, and a copy, which names its
|
|
500
|
-
* original through `copied_from` and has no copies of its own, answer an empty page.
|
|
501
|
-
*/
|
|
502
|
-
export interface MethodCopyPage {
|
|
503
|
-
items: MethodCopyMarker[];
|
|
504
|
-
nextCursor: string | null;
|
|
505
|
-
}
|
|
506
664
|
/**
|
|
507
665
|
* One saved example of a method — what `createMethodExample`, `getMethodExample`,
|
|
508
666
|
* `updateMethodExample` and both arms of `recordMethodExampleLatestRun` and
|
package/dist/product-models.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pipelex-product wire models — the snake_case JSON shapes the hosted-product
|
|
3
|
-
* routes (`/v1/me`, `/v1/methods` and its examples, `/v1/organizations`, `/v1/branding`, `/v1/billing/*`,
|
|
3
|
+
* routes (`/v1/me`, `/v1/methods` and its examples, `/v1/method-templates`, `/v1/organizations`, `/v1/branding`, `/v1/billing/*`,
|
|
4
4
|
* `/v1/pipelex-api-keys`, `/v1/onboarding/submit`,
|
|
5
5
|
* `/v1/resolve-storage-url`, `/v1/upload`, `/v1/upload/grant`, `/v1/files`, `/v1/runs`) speak.
|
|
6
6
|
*
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED
package/dist/webhooks.d.ts
CHANGED
|
@@ -206,7 +206,11 @@ export interface WebhookDeliveryAttempt {
|
|
|
206
206
|
}
|
|
207
207
|
/** One delivery of a run's webhook, as anyone who can read the run sees it. */
|
|
208
208
|
export interface WebhookDeliverySummary {
|
|
209
|
-
/**
|
|
209
|
+
/**
|
|
210
|
+
* The receiver's `webhook-id`, the same on every attempt and replay: `whd_` followed by ASCII
|
|
211
|
+
* letters, digits and underscores, at most 64 characters in all. Treat it as opaque, a key to
|
|
212
|
+
* deduplicate on and nothing to parse.
|
|
213
|
+
*/
|
|
210
214
|
delivery_id: string;
|
|
211
215
|
/** The run whose event the delivery carries. */
|
|
212
216
|
pipeline_run_id: string;
|
|
@@ -255,6 +259,20 @@ export interface WebhookReplayAccepted {
|
|
|
255
259
|
*/
|
|
256
260
|
attempt: number;
|
|
257
261
|
}
|
|
262
|
+
/** What a delivery's replay sends beside its path: the request's own header. */
|
|
263
|
+
export interface WebhookReplayOptions {
|
|
264
|
+
/**
|
|
265
|
+
* The replay's idempotency key, sent as the `Idempotency-Key` header, and not sent at all when
|
|
266
|
+
* absent or `null`. Generate one per replay the caller means to ask for (a `crypto.randomUUID()`
|
|
267
|
+
* is a good key), and keep it across that replay's retries: the same replay sent again under the
|
|
268
|
+
* same key is answered with the stored answer, naming the same attempt, so a replay whose answer
|
|
269
|
+
* was lost can be sent again without the delivery being sent twice. A key reused for another
|
|
270
|
+
* request is refused `409` `request.idempotency_conflict`, and one whose earlier request is still
|
|
271
|
+
* being handled `409` `request.idempotency_key_in_use`, whose `retry` is `yes`. A key is 1 to 255
|
|
272
|
+
* visible ASCII characters, anything else a `RequestArgumentError` before any request.
|
|
273
|
+
*/
|
|
274
|
+
idempotencyKey?: string | null;
|
|
275
|
+
}
|
|
258
276
|
/**
|
|
259
277
|
* An organization's webhook signing secret — `GET /v1/organizations/{org_id}/webhook-secret` and
|
|
260
278
|
* its rotation. Only the current secret's value is served: the previous one, still signing until
|
package/docs/architecture.md
CHANGED
|
@@ -146,7 +146,7 @@ Flat `src/` (mirrors `mthds-js`'s `runners/api` flatness — the SDK has one cli
|
|
|
146
146
|
- **An idempotency key on a run start (hosted):** `start` and `startAndWaitForResult` take `idempotencyKey` (`PipelexStartRequestOptions`, intersected into `PipelexStartOptions`), sent as the `Idempotency-Key` header of `POST /v1/start` and never in the body; with no key there is no header. The hosted API binds the key to the run, answers a repeat of the same start with that run, and refuses the key with other options; a bare runner ignores the header. The client refuses a key that is not 1 to 255 visible ASCII characters with a `RequestArgumentError` before any request, the blocking `execute` fallback of `startAndWaitForResult` sends none, and the key leaves the error's problem as it is: a start that may have reached the server stays `client.outcome_unknown`, and the caller who sent a key recovers the run by sending the same start again under it. [errors.md](./errors.md#a-starts-idempotency-key--idempotencykey) holds the contract.
|
|
147
147
|
- **Method selectors on `validate`:** the first argument is a `PipelexValidateSource` — the method's tree in one of its four forms, `{ method_ref }` (runner-resolved by address) or `{ method_id }` (hosted, platform-resolved) — under the strict tooling XOR: exactly one, no linkage exception, refused with a `RequestArgumentError` before any request. Its options (`ValidateOptions`: `render`, `views`, `timeoutMs`, `signal`) ride the third argument. A selector-resolution failure is a non-2xx `ApiResponseError`, never an `is_valid: false` verdict.
|
|
148
148
|
- **The files a step reads ride the tree.** A `PipeDocGen` step's `template_file` is a file of the method's tree like any other, at the tree path the step names; the separate `resources` field is gone from every route. The valid verdict's `warnings` carry the template check's findings as problem items, located by the template file they name, and its optional `tree_files_read` and `tree_file_readers` name the files of the method's tree each step reads, whatever form the method was sent in ([which step reads which file](./method-trees.md#which-step-reads-which-file)). See [method-trees.md](./method-trees.md#the-files-a-step-reads).
|
|
149
|
-
- **Crate routes (`/v1/resolve`, `/v1/codegen`, `/v1/pipe-io`):** `resolve` emits the **normalized library crate** (the MTHDS standard's Library Crate Format, fingerprint riding inside the payload); `codegen` projects that crate through two explicit axes — `kind` (`types`) × `target` (`ts-zod` / `python-pydantic` / `python-structures`) — into stamped artifacts plus their `codegen.lock`. All three share one closure envelope (the method's tree, in one of the four forms a run takes, XOR a `method_ref`, the address form server-resolved and the registry form a `
|
|
149
|
+
- **Crate routes (`/v1/resolve`, `/v1/codegen`, `/v1/pipe-io`):** `resolve` emits the **normalized library crate** (the MTHDS standard's Library Crate Format, fingerprint riding inside the payload); `codegen` projects that crate through two explicit axes — `kind` (`types`) × `target` (`ts-zod` / `python-pydantic` / `python-structures`) — into stamped artifacts plus their `codegen.lock`. All three share one closure envelope (the method's tree, in one of the four forms a run takes, XOR a `method_ref`, the address form server-resolved and the registry form a `422` `request.unsupported`) and return a **discriminated 200 verdict** (`is_valid`, with `CrateInvalidReport` as the shared invalid arm) rather than a bare payload, the same discipline as `validate`. They also take the hosted `method_id` (`PipelexHostedToolingExtensions`) as a third closure form under the strict three-way XOR — a pass-through the platform resolves, meaningless against a bare runner. Writing the artifacts and lock verbatim reproduces a local `pipelex codegen types` run byte-for-byte, so the offline `pipelex codegen check` passes — the SDK stays transport-only and does not write files. Verifying such a tree afterwards is `runCodegenCheck`, a standalone pure helper rather than a client method, because it needs no server at all. `pipeIo` derives a method's three I/O artifacts — the standard's `PipeIOContracts`, `InputForm` and `OutputForm`, imported from `mthds/protocol` — for the pipe the route selects (the request's qualified `pipe_ref`, else the method's own entry pipe) or for every pipe under `all_pipes`, with the runnability facts beside them and the closure's files echoed on `include_files`, and it runs no dry run; a selection it refuses is a `422` problem coded `request.pipe_not_found`, `request.pipe_unspecified` or `request.pipe_ambiguous`. See [crate-routes.md](./crate-routes.md).
|
|
150
150
|
- **Model reference check (`/v1/models/check`):** `checkModelReference(reference, category?)`, a Pipelex API extension served by any `pipelex-api` runner from pipelex 0.78.0 and on the hosted API, whose platform relays it byte-identically. It is a **diagnostic endpoint** like `validate`: a reference that resolves nowhere is a `200` verdict with `resolution: "not_found"` and the names the caller may have meant (`suggestions`, `other_kinds`, `other_categories`), never a thrown error. Only a request that cannot produce a verdict throws `ApiResponseError`, a `422` `request.invalid` whose `location.field` names the parameter it refused: `reference` for a reference that is missing, repeated, blank, a sigil or a namespace alone, or past the runner's length limit, and `type` for a category that is repeated, empty or unknown. The SDK validates none of it, so the runner's rule is the only one. `ModelReferenceVerdict` is a union of one arm per reference kind, discriminated on the wire's own `kind` (`PresetReferenceVerdict`, `AliasReferenceVerdict`, `WaterfallReferenceVerdict`, `HandleReferenceVerdict`): a `matches` entry carries no kind of its own, but every entry of one verdict shares the verdict's, so narrowing on `kind` types `matches` as `PresetMatch[]`, `AliasMatch[]`, `WaterfallMatch[]` or `HandleMatch[]` without a field the wire does not carry. `ModelCheckCategory` is the protocol's model categories plus `doc_gen`, the family of `PipeDocGen`, which the protocol and so `models` leave out but a method's `model` field can name. `resolution` is typed open, `ModelReferenceResolution | (string & {})`, under the spec's reader rule that a client reading a value it does not know treats the reference as unresolved, and so is every category a verdict names, `ModelCheckCategory | (string & {})` as `mthds` types `ModelInfo.type`, since a runner of a later version may add one; the `category` a request names stays closed. A match's `resolves_to` is `null` when the reference resolves by name but a run through it would reach no model the runner can call, which a consumer shows as a warning rather than as an unknown name.
|
|
151
151
|
- **Tools (`/v1/lint`, `/v1/format`):** `lint`, `format` — single-file static diagnostics and canonical formatting, served by any pipelex-api runner (no inference, no bundle load). Both are **diagnostic endpoints** like `validate`: a malformed `.mthds` file is a produced verdict on a **200** carrying `diagnostics[]`, never a thrown error — `format` additionally echoes the content back unchanged with `changed: false`. Only a no-verdict condition is non-2xx: a malformed body, **malformed formatter `options`** (e.g. a non-numeric `column_width` ⇒ 422), auth, or a server fault, all mapped to the typed `ApiResponseError`. `lint` is the cheap static check (syntax / schema / semantic); it does not replace `validate`, which loads the bundle, resolves across files, and dry-runs the pipes.
|
|
152
152
|
- **Durable run lifecycle (hosted extension — NOT protocol):** `getRunStatus`, `getRunProgress` (`GET /v1/runs/{id}/progress`, the run's progress snapshot, see [run-results.md](./run-results.md#following-a-run-while-it-runs--onprogress-and-getrunprogress)), `getRunResult` (with an optional `artifacts` selection sent as `?artifacts=`, see [run-results.md](./run-results.md#reading-only-some-artifacts--artifacts)), `waitForResult` (whose `onProgress` reads the progress route on each poll that finds the run running and delivers each fresher snapshot by `isFresherRunProgress`, a failed progress read never failing the wait), `startAndWaitForResult` (handshakes `/v1/version`, takes the durable start+poll path on a hosted deployment, and self-heals to the blocking `execute` against a bare runner). The handshake's outcome is cached for the client's lifetime, under the rule the Python SDK follows too: an answer that is not a usable version assumes hosted and lets the start surface the real error, while no answer at all throws its `ApiUnreachableError` at once, caches nothing and sends no start. An answer is anything the server sent back: an error status, a body that is not JSON, not UTF-8 or no version, and a body that arrived but could not be decoded, such as a broken gzip stream, which fetch fails with the decompressor's code (`Z_DATA_ERROR` and the other zlib codes, brotli's `ERR__ERROR_…`, zstd's `ZSTD_error_…`). A run that ended without completing answers the results read with a `409` `run.failed` problem document, naming the terminal status as its `run_status` parameter and nesting the run's own problem as `run_error`; `getRunResult` turns it into the `failed` arm (`status`, `message` from `detail`, `problem`, `error`) and the waiting calls on the durable path into a `RunFailedError` carrying the same (the blocking fallback's failure is the runner's `ApiResponseError`). The status read serves the run's own problem as `RunRead.error`. See [errors.md](./errors.md#a-failed-run--runfailederror). The status read also carries `degraded` and the `degradation` that explains it, whose `reason` tells a status the platform could not refresh (`status_unavailable`) from a run whose workflow task keeps failing until the platform ends it (`workflow_task_failing`), checked field by field so it never fails the poll, and the run's `request_id`, the id to quote to support. See [run-results.md](./run-results.md#a-degraded-status--degraded-and-degradation). Reading what a completed run consumed is `summarizeUsage(results)`, a standalone pure helper rather than a client method, because it folds the usage pair the results already carry and needs no server. See [run-usage.md](./run-usage.md#summarizing-a-run--summarizeusage).
|
|
@@ -157,12 +157,13 @@ Flat `src/` (mirrors `mthds-js`'s `runners/api` flatness — the SDK has one cli
|
|
|
157
157
|
The hosted catalog/account routes the webapp drives. All ride the same `{base}/v1/*` + `Authorization: Bearer` + org-from-JWT contract as the protocol routes, go through the shared `requestProduct<T>` helper (30s management timeout, empty-body tolerant), and map a non-2xx RFC 9457 `problem+json` to a typed `ApiResponseError`. Every route parses a 2xx body through one private helper, so an answer that is not JSON (a gateway's HTML page, an empty body where one was due) is an `ApiResponseError` carrying its status and raw text, `client.response_unreadable`, rather than a bare `SyntaxError`. A product route whose answer the SDK or its caller reads before anything else (`listMethods`, `listRuns`, `getRunDetail`, `upload`, `requestUploadGrant`, `resolveStorageUrls`, and the methods routes answering one stored method) goes through `requestProductObject` or a helper built on the same read instead, which also refuses an empty body and JSON that is not an object; the two paged routes then refuse, the same way, through one page reader, a page whose `items` is not an array or whose `next_cursor` is neither a string nor `null`, `upload` an answer with no non-empty string `uri`, and `getMethod` and `getMethodVersion` a `files` member that is not a listing of files, each with its string `path`, `sha256`, `url` and `expires_at` and a non-negative integer `size`. Every refusal the platform renders is a problem document under a registered code, a core one (`request.invalid`, `auth.forbidden`, …) or a Pipelex one (`pipelex.catalog.not_found`, `pipelex.billing.plan_limit`, …), and consumers branch on its `code`, as the hosted-envelope spec (`conformance/specs/pipelex-hosted-envelope.md`, in the `conformance` repository) says, never on the HTTP status. An answer whose body is no problem document comes from the hosted edge and reads by its status. See [errors.md](./errors.md).
|
|
158
158
|
|
|
159
159
|
- **User profile:** `getMe` (`GET /v1/me`).
|
|
160
|
-
- **Methods catalog:** `listMethods`, `getMethod`, `createMethod`, `writeDraft`, `renameMethod`, `publishMethod`, `listMethodVersions`, `getMethodVersion`, `deleteMethod`. **A saved method has a draft and immutable published versions.** Its draft is a method tree with its `input_data`, which `writeDraft` (`PUT /v1/methods/{id}/draft`) replaces whole, without validating it; `updated_at` is the draft's token, which moves on every draft write and nothing else, and `draft_digest` the draft's tree digest (`sha256:` and 64 hex digits), `null` for an empty draft. A save takes the tree as `files`, sent as given, or as `directory`, `tree` or `contents`, sent inline or by hash after uploading the blobs the organization lacks, and a read lists the tree's files, each with a short-lived link to its bytes, which `writeMethodTree` writes to disk ([method-trees.md](./method-trees.md)). A draft write may carry `expected_updated_at`, the token the caller last saw, and is then a compare-and-swap: a draft that moved since is a `409` `pipelex.catalog.update_conflict` and nothing is written. `renameMethod` (`PATCH /v1/methods/{id}`) changes the name alone and moves no token. `publishMethod` (`POST /v1/methods/{id}/publish`) takes the required `expected_draft_updated_at` and answers a `MethodPublishResult` discriminated on `outcome` — `published` with the new version, `unchanged` with the existing latest version when the draft's digest equals it, `refused` with a `reason` (`invalid` or `not_runnable`), a `message` and the runner's `validation` verdict, its problem items read member by member as `validate` reads them — every arm carrying the `method`; the SDK reads the nested method as `getMethod` does, and an answer whose `outcome` is none of these, or a refusal whose `validation` is not a verdict object, is an answer it cannot read. A publish that produced no verdict is thrown rather than answered: only a draft that differs from the latest version reaches the runner, and a runner that cannot be reached, answers unusably or does not finish within the platform's deadline is a `502` or `503` with nothing written, so a retry is safe, and a retry of a publish that landed while its answer was lost answers `unchanged` with the version it wrote. A method is stored as one item that keeps room for the summary a publish adds, so a draft that saves can always be published: a creation or a draft write is a `413` `request.too_large` when it would grow the method past that room, below the store's item limit, and a rename is the same `413` for a name so long it would leave the method too large to publish. A version is numbered from 1, never reused, and holds the draft's tree as it stood, its `tree_digest` (`null` for a version stored before methods were trees), its `crate_fingerprint` (`null` when the runner could not resolve the bundle in memory), the runner's version, and when and by whom it was published; `listMethodVersions` pages the summaries newest first (`MethodVersionPage`, `{items, nextCursor}`, a `limit` from 1 to 100, 20 by default, and a `422` outside that range), and `getMethodVersion` reads one with its `files` listed as a method's are, or answers a `500` `runtime.unexpected` for a version stored before methods were trees and never rewritten as one. A method read carries `latest_version` and `latest_published`, both `null` until the first publish, so the publish state is derived: never published, draft unchanged (`draft_digest` not `null` and equal to `latest_published.tree_digest`), or draft ahead. The method routes take a bare id and address the method itself, never one of its versions; the platform does not parse a suffix there, so `mt_x@3` would answer a `404` that reads as a method that does not exist, and each route refuses a suffixed id with `RequestArgumentError` before anything is sent, naming `parseMethodSelector` and `getMethodVersion`. The whole-method `updateMethod` is gone, replaced by the draft write and the rename. **`deleteMethod` is asynchronous and its return type says so:** `DELETE /v1/methods/{id}` answers `202` with a `MethodDeletionAccepted` (`method_id`, `deletion_state`, `deletion_job_id`) the moment the platform has claimed the method and terminated its in-flight workflows — the rest of the cascade (runs, events, S3 objects) is enqueued. A resolved promise means "accepted", never "gone"; completion is the row disappearing from `listMethods`, not any field of that body. The claim is a conditional write, so a double-clicked delete is a `409` `pipelex.catalog.being_deleted` rather than a second cascade over the same runs, and an unknown or foreign-org id is a `404`. The erasure deletes the method's versions with the rest. (This doc previously stated the SDK had no `delete` because the platform had no such route — the route has always existed; the claim was simply false.) `listMethods` returns **one page**, not the whole catalog — a `MethodPage` (`{items, nextCursor}`) served from a narrow index projection, so its cost is the page size rather than the size of the catalog. It used to return every method with its full `.mthds` bundle attached, which hit DynamoDB's 1 MB page cap at a couple hundred methods and came back truncated with no error and no flag, so methods silently vanished from the UI. Rows are therefore `MethodSummary`, **not** `MethodDetail`: no `files`, no `updated_at`, because neither is in the projection and putting the source back is what restored the truncation. Reach for `getMethod(id)` when you need a method's files. `query.q` is applied server-side across the whole catalog, not over the page — filtering one page client-side would be searching 50 rows of 10,000 and calling it a search. `query.published` filters the same way, sent as `published=true` (the methods with a published version) or `published=false` (those never published), and combines with `q`. Each row carries the method's publish state, so a list tells its methods apart without reading each one: `latest_version` and `latest_published` (a `MethodVersionSummary`), both `null` when the method was never published, and `draft_digest`, `null` for an empty draft; all three are optional, absent from a platform that does not serve them. Never published is `latest_version === null`, the draft unchanged is a `draft_digest` that is not `null` and equals `latest_published.tree_digest`, and the draft ahead is two digests that differ. Both filters run after the index read, so **a filtered page can come back empty and still carry a `nextCursor`**: an empty page is not the end of the list, and only a `null` cursor is. `iterateMethods` is the async iterator that follows the cursor for callers who genuinely want everything (`for await (const m of client.iterateMethods())`); it keeps going **past empty pages**, since `{items: [], nextCursor: "…"}` means "nothing matched in the slice I just read, keep going". A server that keeps minting fresh cursors forever stops it at a page ceiling far past any real catalog with a `PagingNotTerminatingError` carrying `pageLimit`, never a silently truncated stream; `iterateRuns` shares the same loop. It gives up only when the server stops advancing its cursor, and **throws** rather than returning a partial answer; like `iterateRuns` it is deliberately not a `listAllMethods(): Promise<MethodSummary[]>`, because an all-at-once helper needs a page cap and a cap means silently returning a truncated list. `MethodSummary.deletion_state` rides the list rows: a method mid-erasure stays listed so the UI can render it as "Deleting…", while `getMethod` refuses it with a 409. Full read responses (`MethodDetail`, from `getMethod`) carry `org_id`, `created_by_user_id`, a server-derived `description` (recomputed from the
|
|
160
|
+
- **Methods catalog:** `listMethods`, `getMethod`, `createMethod`, `writeDraft`, `renameMethod`, `publishMethod`, `listMethodVersions`, `getMethodVersion`, `deleteMethod`. **A saved method has a draft and immutable published versions.** Its draft is a method tree with its `input_data`, which `writeDraft` (`PUT /v1/methods/{id}/draft`) replaces whole, without validating it; `updated_at` is the draft's token, which moves on every draft write and nothing else, and `draft_digest` the draft's tree digest (`sha256:` and 64 hex digits), `null` for an empty draft. A save takes the tree as `files`, sent as given, or as `directory`, `tree` or `contents`, sent inline or by hash after uploading the blobs the organization lacks, and a read lists the tree's files, each with a short-lived link to its bytes, which `writeMethodTree` writes to disk ([method-trees.md](./method-trees.md)). A draft write may carry `expected_updated_at`, the token the caller last saw, and is then a compare-and-swap: a draft that moved since is a `409` `pipelex.catalog.update_conflict` and nothing is written. `renameMethod` (`PATCH /v1/methods/{id}`) changes the name alone and moves no token. `publishMethod` (`POST /v1/methods/{id}/publish`) takes the required `expected_draft_updated_at` and answers a `MethodPublishResult` discriminated on `outcome` — `published` with the new version, `unchanged` with the existing latest version when the draft's digest equals it, `refused` with a `reason` (`invalid` or `not_runnable`, and for a public method `storage_reference`, `thumbnail_missing` or `thumbnail_invalid`, read open so a later reason arrives as its raw string), a `message` and the runner's `validation` verdict, its problem items read member by member as `validate` reads them — every arm carrying the `method`; the SDK reads the nested method as `getMethod` does, and an answer whose `outcome` is none of these, or a refusal whose `validation` is not a verdict object, is an answer it cannot read. A publish that produced no verdict is thrown rather than answered: only a draft that differs from the latest version reaches the runner, and a runner that cannot be reached, answers unusably or does not finish within the platform's deadline is a `502` or `503` with nothing written, so a retry is safe, and a retry of a publish that landed while its answer was lost answers `unchanged` with the version it wrote. A method is stored as one item that keeps room for the summary a publish adds, so a draft that saves can always be published: a creation or a draft write is a `413` `request.too_large` when it would grow the method past that room, below the store's item limit, and a rename is the same `413` for a name so long it would leave the method too large to publish. A version is numbered from 1, never reused, and holds the draft's tree as it stood, its `tree_digest` (`null` for a version stored before methods were trees), its `crate_fingerprint` (`null` when the runner could not resolve the bundle in memory), the runner's version, and when and by whom it was published; `listMethodVersions` pages the summaries newest first (`MethodVersionPage`, `{items, nextCursor}`, a `limit` from 1 to 100, 20 by default, and a `422` outside that range), and `getMethodVersion` reads one with its `files` listed as a method's are, or answers a `500` `runtime.unexpected` for a version stored before methods were trees and never rewritten as one. A method read carries `latest_version` and `latest_published`, both `null` until the first publish, so the publish state is derived: never published, draft unchanged (`draft_digest` not `null` and equal to `latest_published.tree_digest`), or draft ahead. The method routes take a bare id and address the method itself, never one of its versions; the platform does not parse a suffix there, so `mt_x@3` would answer a `404` that reads as a method that does not exist, and each route refuses a suffixed id with `RequestArgumentError` before anything is sent, naming `parseMethodSelector` and `getMethodVersion`. The whole-method `updateMethod` is gone, replaced by the draft write and the rename. **`deleteMethod` is asynchronous and its return type says so:** `DELETE /v1/methods/{id}` answers `202` with a `MethodDeletionAccepted` (`method_id`, `deletion_state`, `deletion_job_id`) the moment the platform has claimed the method and terminated its in-flight workflows — the rest of the cascade (runs, events, S3 objects) is enqueued. A resolved promise means "accepted", never "gone"; completion is the row disappearing from `listMethods`, not any field of that body. The claim is a conditional write, so a double-clicked delete is a `409` `pipelex.catalog.being_deleted` rather than a second cascade over the same runs, and an unknown or foreign-org id is a `404`. The erasure deletes the method's versions with the rest. (This doc previously stated the SDK had no `delete` because the platform had no such route — the route has always existed; the claim was simply false.) `listMethods` returns **one page**, not the whole catalog — a `MethodPage` (`{items, nextCursor}`) served from a narrow index projection, so its cost is the page size rather than the size of the catalog. It used to return every method with its full `.mthds` bundle attached, which hit DynamoDB's 1 MB page cap at a couple hundred methods and came back truncated with no error and no flag, so methods silently vanished from the UI. Rows are therefore `MethodSummary`, **not** `MethodDetail`: no `files`, no `updated_at`, because neither is in the projection and putting the source back is what restored the truncation. Reach for `getMethod(id)` when you need a method's files. `query.q` is applied server-side across the whole catalog, not over the page — filtering one page client-side would be searching 50 rows of 10,000 and calling it a search. `query.published` filters the same way, sent as `published=true` (the methods with a published version) or `published=false` (those never published), and combines with `q`. Each row carries the method's publish state, so a list tells its methods apart without reading each one: `latest_version` and `latest_published` (a `MethodVersionSummary`), both `null` when the method was never published, and `draft_digest`, `null` for an empty draft; all three are optional, absent from a platform that does not serve them. Never published is `latest_version === null`, the draft unchanged is a `draft_digest` that is not `null` and equals `latest_published.tree_digest`, and the draft ahead is two digests that differ. Both filters run after the index read, so **a filtered page can come back empty and still carry a `nextCursor`**: an empty page is not the end of the list, and only a `null` cursor is. `iterateMethods` is the async iterator that follows the cursor for callers who genuinely want everything (`for await (const m of client.iterateMethods())`); it keeps going **past empty pages**, since `{items: [], nextCursor: "…"}` means "nothing matched in the slice I just read, keep going". A server that keeps minting fresh cursors forever stops it at a page ceiling far past any real catalog with a `PagingNotTerminatingError` carrying `pageLimit`, never a silently truncated stream; `iterateRuns` shares the same loop. It gives up only when the server stops advancing its cursor, and **throws** rather than returning a partial answer; like `iterateRuns` it is deliberately not a `listAllMethods(): Promise<MethodSummary[]>`, because an all-at-once helper needs a page cap and a cap means silently returning a truncated list. `MethodSummary.deletion_state` rides the list rows: a method mid-erasure stays listed so the UI can render it as "Deleting…", while `getMethod` refuses it with a 409. Full read responses (`MethodDetail`, from `getMethod`) carry `org_id`, `created_by_user_id`, a server-derived `description` (recomputed on every draft write from the manifest's `description`, else the primary bundle's top-level `description`, else its main pipe's), `categories` and `visibility`, the draft token and digest, the latest version's summary and the draft's `files` on top of the write contract's `name` / `input_data`; the writes and the rename answer `MethodData`, the same method without `files`. Another organization's public method reads as a `PublicMethodDetail`, its latest published version and nothing of its draft, so `getMethod` is typed `MethodDetail | PublicMethodDetail`, told apart by `updated_at`; an answer without it that carries a member only the owner's read has is an answer the SDK cannot read, and so is a public read without its `latest_version` and `latest_published` ([public-methods.md](./public-methods.md)).
|
|
161
161
|
- **Method examples:** `createMethodExample`, `listMethodExamples`, `iterateMethodExamples`, `getMethodExample`, `updateMethodExample`, `recordMethodExampleLatestRun`, `saveAssistantMethodExample`, `deleteMethodExample`, on `/v1/methods/{id}/examples`. An example is a named set of inputs saved on a method, shared across the organization, with an `input_revision` that rises each time its inputs change and the `latest_run_id` of those inputs, which an inputs edit clears. `recordMethodExampleLatestRun` records a run against the revision whose inputs it used and answers a `MethodExampleLatestRunResult` discriminated on `outcome`, `recorded` or `stale`, each arm carrying the `example`: a run that outlasted an edit is a verdict, never thrown, and an answer off the union is one the SDK cannot read; `saveAssistantMethodExample` is the build assistant's save, which writes into the example it names only while that example is still the assistant's (`kept_by_assistant`) at the revision it read, and otherwise creates one, answering a `MethodExampleAssistantSaveResult` discriminated on `outcome`, `kept` or `created`; one helper, `readVerdict`, reads these verdicts and `publishMethod`'s, the example checked once, by the reader every example goes through. Every answer carrying an example is read with `parseExactJson` and every request body written with `stringifyExactJson`, so an integer past `Number.MAX_SAFE_INTEGER` in the inputs reads as a `bigint` and goes back digit for digit, to an edit or as a run's inputs; `3.0` still reads as `3`, and inputs holding one sent back unchanged are an edit for the platform, which compares inputs as canonical JSON. Every example the client hands back, the page rows and the verdict's `example` included, is checked by `readMethodExample` against the fields the Python SDK's model requires, with the strict types it holds them to, an example breaking one being an answer the SDK cannot read. The list is paged oldest first by an opaque cursor, and a page may be short while `nextCursor` is set; `iterateMethodExamples` follows it through the loop `iterateMethods` and `iterateRuns` share, and refuses a method id at the call rather than at the first pull. Every method takes a bare catalog id, and the client refuses an example id that is empty, `.` or `..` or holds a lone surrogate, a create without its name or inputs or whose `run_id` is given but empty, an edit naming neither `name` nor `inputs`, and a latest-run write without a run id or a positive integer revision before sending. See [method-examples.md](./method-examples.md).
|
|
162
|
-
- **
|
|
162
|
+
- **Template catalog:** `listMethodTemplates`, `iterateMethodTemplates`, on `GET /v1/method-templates`. The public methods of the deployment's templates organization, in the order the platform serves for the `ordering` asked for, filtered by `category`, with the page's `categories` and each card's thumbnails behind short-lived links. `listMethodTemplates` reads the page with its own reader rather than `requestPage`, since its `ordering` and `categories` sit beside `items` and `nextCursor`, and redacts the links of an answer it cannot read; `iterateMethodTemplates` follows the cursor with the same ordering and category ([public-methods.md](./public-methods.md)).
|
|
163
|
+
- **Method copies:** `copyMethod`, on `/v1/methods/{id}/copy`. `copyMethod` makes a new, unpublished method in an organization the caller may change methods in, the method's own for a duplicate, holding the original's draft tree, its form inputs and its examples, the stored files they name copied into the destination, and answers a `MethodCopyResult` discriminated on `outcome`: `copied` with the copy as `method` and what was copied, or `refused` with the `references` of a bundle naming storage the destination cannot read or, in a duplicate, a file under the original's run files, which erasing the original would remove. The two arms carry different members, so the verdict is read by the copy itself rather than by `readVerdict`, which reads verdicts whose arms share one: an `outcome` off the union, a `copied` answer whose `method` is not an object and a `refused` one whose `references` is not an array are each an answer the SDK cannot read. Its body is closed, `destination_org_id` with `name` and `expected_draft_updated_at` when given, and its last argument takes an `idempotencyKey`, sent as the `Idempotency-Key` header through the same `requestRaw` option and the same `idempotencyKeyOf` check a start uses. Every method read carries `copied_from`, the original a copy names, `null` on a method created afresh. `MethodErrorCode` names the copy's own codes, `pipelex.org.not_found` and `pipelex.catalog.copy_too_large`, whose `limit` member is typed `MethodCopyLimit`. It takes a bare catalog id. See [method-copy.md](./method-copy.md).
|
|
163
164
|
- **Organizations:** `listMemberships`, `createOrganization`, `renameOrganization` (`PATCH`). Each membership carries the caller's `role_in_org`, an `OrgRole` (`admin`, `member` or `runner`) read open: a role a later platform adds arrives as its raw string, so a `switch` over it says what an unknown role does. The role is for display; what it may do is the platform's to decide, and a call refused to the caller's role is an `ApiResponseError`. The org "switch" is a WorkOS-session op (`/api/active-org`), not a `/v1` route — out of SDK scope.
|
|
164
165
|
- **Organization branding:** `getBranding` (`GET /v1/branding`, any role, web session or API key) and `updateOrganizationBranding` (`PATCH /v1/organizations/{org_id}/branding`, an admin's write) answer the same `OrganizationBranding`: the organization's name, its light and dark colour sets (`BrandingColors`) and its square and wide logos (`LogoDescriptor`). The write's `OrganizationBrandingUpdate` sets a key given a value, clears a key given `null` and keeps a key left out; a refused logo or colour is named in `ApiResponseError.errors`, by slot or by dotted key, a logo the platform measured with a `LogoRefusalCode`. See [organization-branding.md](./organization-branding.md).
|
|
165
|
-
- **Caller webhooks:** `listRunWebhookDeliveries`, `getRunWebhookDelivery` and `replayRunWebhookDelivery`, on `/v1/runs/{id}/webhook-deliveries`, `/v1/runs/{id}/webhook-deliveries/{delivery_id}` and its `/replay`, read a run's delivery log and send a settled delivery again, any member by web session or API key; `getWebhookSecret`, on `GET /v1/organizations/{org_id}/webhook-secret`, and `rotateWebhookSecret`, on `POST /v1/organizations/{org_id}/webhook-secret/rotate`, read and rotate the organization's signing secret, an admin's routes from a web session only, the rotation taking an `idempotencyKey` the platform never replays. See [webhooks.md](./webhooks.md).
|
|
166
|
+
- **Caller webhooks:** `listRunWebhookDeliveries`, `getRunWebhookDelivery` and `replayRunWebhookDelivery`, on `/v1/runs/{id}/webhook-deliveries`, `/v1/runs/{id}/webhook-deliveries/{delivery_id}` and its `/replay`, read a run's delivery log and send a settled delivery again, any member by web session or API key, the replay taking an `idempotencyKey` under which the platform stores its answer; `getWebhookSecret`, on `GET /v1/organizations/{org_id}/webhook-secret`, and `rotateWebhookSecret`, on `POST /v1/organizations/{org_id}/webhook-secret/rotate`, read and rotate the organization's signing secret, an admin's routes from a web session only, the rotation taking an `idempotencyKey` the platform never replays. See [webhooks.md](./webhooks.md).
|
|
166
167
|
- **Billing:** `getSubscription`, `listPlans`, `listInvoices`, `createCheckout`, `changePlan` (409 `conflict` ⇒ no subscription), `getBillingPortal` (409 `conflict` ⇒ no subscription).
|
|
167
168
|
- **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.
|
|
168
169
|
- **Onboarding:** `submitOnboarding`.
|
|
@@ -183,4 +184,4 @@ Two layers, split by whether a server is in the loop:
|
|
|
183
184
|
- **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.
|
|
184
185
|
- **The command's case table (`tests/cli.test.ts`):** runs every case of `tests/fixtures/cli-cases.json` through the `pipelex-sdk` command in-process, in a fresh working directory, with `fetch` answering from the case's recorded routes, so the command runs on the real client. The table is written for the Python SDK's command of the same name to run too; [`cli.md`](cli.md) describes the format.
|
|
185
186
|
- **The packed command (`make test-pack`):** packs the package with `npm pack` and runs `pipelex-sdk --help` and `--version` from the tarball through `npx`, from a directory outside the package. It needs the registry for the dependencies, so CI's `js (make all)` job runs it and `make test` does not.
|
|
186
|
-
- **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` + `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 same suite drives `pipeIo` across all three selectors: the artifacts under the resolved ref, their equality with `validate`'s views for the same closure, the `include_files` echo, `all_pipes` on a method with no entry pipe, a `method_ref` package's manifest entry pipe, the invalid arm, and the
|
|
187
|
+
- **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` + `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 same suite drives `pipeIo` across all three selectors: the artifacts under the resolved ref, their equality with `validate`'s views for the same closure, the `include_files` echo, `all_pipes` on a method with no entry pipe, a `method_ref` package's manifest entry pipe, the invalid arm, the selection `422` coded `request.pipe_not_found` and naming the pipe in its `params`, and a registry-form `method_ref`'s `422 request.unsupported`, which `resolve` answers too. Its `method_id` case, like the `prepareInputs` suite's, runs only when `PIPELEX_E2E_METHOD_ID` names a stored method the key's organization owns, since only a hosted origin can resolve one, and is skipped otherwise. 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: the suite's global setup (`tests/e2e/global-setup.ts`) probes the target a single time and stops the run with a one-line message, instead of letting every suite fail in `beforeAll` and print a socket-error dump each. It probes only after the account check below went ahead, so a run the check refuses sends no request at all, and `make test-e2e` sends none before it. The per-suite `beforeAll` guard stays as the backstop. 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, so the URL the global setup checks and probes 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 global setup prints the resolved target before the first case, since `.env` can supply it invisibly, as its scheme and host alone. The key is never read from `.env`: against a hosted origin the suites act for the registered test account, QA Builder in QA SDK (dev), whose key the workspace's `test-account run` hands over beside what the registry expects of it, and the suite's global setup, `tests/e2e/live-account.ts`, compares `GET /v1/me` with those expectations before the first case, refuses the production API unless `PIPELEX_E2E_ALLOW_PRODUCTION=1` asks for it, and says nothing is checked against a loopback runner ([`docs/live-accounts.md`](https://github.com/Pipelex/pipelex-sdk/blob/main/docs/live-accounts.md) at the repository's root, which the package does not ship). A hosted origin serves every route the suite calls but `lint` and `format`, which it does not expose (gateway `403`), so their cases need a bare runner and skip on a hosted origin, as the `method_id` cases skip without a stored method: `tests/e2e/origin.ts` holds the one predicate every file asks, `isHostedOrigin`, which reads any origin whose host is not one of this machine's loopback spellings (`localhost`, a subdomain of it, `127.x.x.x` or `::1`) as a hosted plane. A local runner runs them. 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 into a `RejectedAssetError`'s `reason` — `grant_used` for the `412` of a used grant, `signature_mismatch` for the `403 SignatureDoesNotMatch` of a file one byte longer than declared, `store_refused` for the `400 BadDigest` of other bytes than the hash named — and the platform's `413 request.too_large` for a declared size over the cap, its `params` naming the cap and the size. 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 `POST /v1/pipe-io` (which `prepareInputs` reads), the upload, run-lifecycle and bulk resolve routes, and a key for it, and it fails honestly rather than skipping against a bare runner. The problems suite proves that the problem on a platform refusal is the one the server sent: an `ApiResponseError` reads a problem document as received, and builds one from the status by the edge's rules when the body is none, so each case asserts which it was against `responseBody`. A run of a method id the catalog does not hold and a request body the platform's own validation refuses are the platform's problems, the second a `request.invalid` whose `location` and `params` name the field, with no `errors`, since a problem with a single fault carries none. A rejected API key is refused before the platform sees it, by the gateway's authorizer, with a bare `403 {"message":"Forbidden"}`, so its problem is the SDK's reading of the status, `auth.invalid_credential`. It needs a hosted platform and a key for it, and fails honestly against a bare runner.
|