@pipelex/sdk 0.60.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.
@@ -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 bundle's top-level `description` at publish time. */
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: string;
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`). Its draft is
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
- * Derived from the draft's top-level `description`. Read-side only: the
105
- * server recomputes it from the draft on every draft write, so it never appears on
106
- * the write contract.
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
- /** The digest of the original's draft the copy took; `null` for an empty draft. */
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. `files` lists the draft's
158
- * tree in tree path order, each file with its SHA-256, its size and a link to its bytes valid for
159
- * about fifteen minutes, and is empty for an empty draft; `writeMethodTree` brings it to disk.
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: `invalid`, it does not validate; `not_runnable`, it validates
319
- * with pending signatures, so it is valid but does not run yet.
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
- reason: MethodPublishRefusalReason;
343
- /** Says why; for `not_runnable`, that the draft "is valid but does not run yet". */
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`, the valid arm. An entry that names no fault is left out, and a member the
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
  }
@@ -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
@@ -3,4 +3,4 @@
3
3
  * `User-Agent` without importing the public barrel. Kept in sync with
4
4
  * `package.json` — enforced by `tests/index.test.ts`.
5
5
  */
6
- export declare const SDK_VERSION = "0.60.0";
6
+ export declare const SDK_VERSION = "0.61.0";
package/dist/version.js CHANGED
@@ -3,5 +3,5 @@
3
3
  * `User-Agent` without importing the public barrel. Kept in sync with
4
4
  * `package.json` — enforced by `tests/index.test.ts`.
5
5
  */
6
- export const SDK_VERSION = "0.60.0";
6
+ export const SDK_VERSION = "0.61.0";
7
7
  //# sourceMappingURL=version.js.map
@@ -157,8 +157,9 @@ 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 draft's top-level `description` on every draft write), 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`.
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
+ - **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)).
162
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).
package/docs/files.md CHANGED
@@ -231,6 +231,7 @@ The answer is a `DraftFileFromFileResult`, a `200` discriminated on `blob`: `cop
231
231
 
232
232
  ```ts
233
233
  const method = await client.getMethod("mt_abc123");
234
+ if (!("updated_at" in method)) throw new Error("only the owner writes a method's draft");
234
235
  const added = await client.copyFileIntoDraft("mt_abc123", {
235
236
  file_id: "fl_0199",
236
237
  path: "assets/logo.png",
@@ -7,9 +7,11 @@ import { PipelexApiClient } from "@pipelex/sdk";
7
7
 
8
8
  const client = new PipelexApiClient();
9
9
  const method = await client.getMethod("mt_abc123");
10
+ // Another organization's public method has no draft to name: its copy takes no token.
11
+ const token = "updated_at" in method ? method.updated_at : null;
10
12
  const result = await client.copyMethod(
11
13
  method.method_id,
12
- { destination_org_id: "org_team", expected_draft_updated_at: method.updated_at },
14
+ { destination_org_id: "org_team", expected_draft_updated_at: token },
13
15
  { idempotencyKey: crypto.randomUUID() },
14
16
  );
15
17
  if (result.outcome === "refused") {
@@ -43,10 +45,12 @@ It takes a bare catalog id, as every method route does, and refuses a suffixed o
43
45
  | Published versions | None: the copy starts unpublished, its `latest_version` and `latest_published` `null`, and its own publish makes version 1 |
44
46
  | Runs | None: a run stays in the organization that ran it and was billed for it |
45
47
  | `method_id`, `org_id`, `created_by_user_id`, `created_at` | A new id, the destination, the copier and the moment of the copy |
46
- | `copied_from` | `{ org_id, method_id, draft_digest }` of the original, as it stood at the copy, its `draft_digest` `null` when the original's draft held no files |
48
+ | `copied_from` | `{ org_id, method_id, draft_digest }` of the original, as it stood at the copy, its `draft_digest` `null` when the original's draft held no files; for another organization's public method, `{ org_id, method_id, draft_digest, version }`, `version` the version copied and `draft_digest` always `null` |
47
49
 
48
50
  **The copy takes the draft, never the latest published version.** The draft is what the person sees in the editor, and the copy goes through its own first publish anyway; an invalid draft is copied as it is, and the copy's publish refuses it as it would any draft. With `expected_draft_updated_at`, the copy takes exactly the draft the caller last saw: a draft that moved since is a `409` `pipelex.catalog.update_conflict`, and nothing is copied. A client that has just saved the editor's pending edits sends the token that save answered.
49
51
 
52
+ **Another organization's public method copies from its latest published version** instead, since its draft is the owner's: that version's tree, description and categories, and the method's saved examples, but not the owner's form inputs. The copy's `copied_from` names the `version` it took. The caller never saw that method's draft, so `expected_draft_updated_at` sent with it is a `422` `request.invalid` naming the field. [public-methods.md](./public-methods.md) describes public methods.
53
+
50
54
  **A stored file named by an input is copied.** A `pipelex-storage://` address resolves only inside the organization it names, so every such address in the original's organization, in the form inputs and in each example's inputs, is copied into the destination's `assets/` under a new address, and the value is rewritten to it; a file named twice is copied once. A duplicate keeps every address of the organization as it is, an upload's above all, since the organization is the same, and copies only the files the original method's own runs stored, under `pipelex-storage://<org_id>/<method_id>/…`, which erasing the original would sweep away. An address of another organization, a malformed one and one whose file is gone are kept unchanged. The destination records each copied file among its files, attributed to the copier. `stored_files_copied` counts the distinct files copied.
51
55
 
52
56
  ## The verdict
@@ -72,12 +76,12 @@ A non-2xx answer means no verdict could be produced, and none of them leaves any
72
76
  |---|---|---|
73
77
  | `403` | `auth.forbidden` | The request is not from a web session, or the caller's role changes no method, `runner` or a role the platform does not know, in the method's organization or in the destination: the role must change. |
74
78
  | `404` | `pipelex.org.not_found` | The destination is one the caller does not belong to, one whose invitation they have not accepted, one still being set up, or one that does not exist. The platform answers them alike, so it never says whether an organization exists. |
75
- | `404` | `pipelex.catalog.not_found` | An unknown method. The destination is judged first, so an unknown method in a destination the caller may not write to reads `pipelex.org.not_found`. |
79
+ | `404` | `pipelex.catalog.not_found` | An unknown method, or another organization's method that is not public. The destination is judged first, so an unknown method in a destination the caller may not write to reads `pipelex.org.not_found`. |
76
80
  | `409` | `pipelex.catalog.being_deleted` | The original's erasure has started. |
77
81
  | `409` | `pipelex.catalog.update_conflict` | The draft moved since `expected_draft_updated_at`; `location.field` names that member. Read the method again and copy with the fresh token. |
78
82
  | `409` | | The method's organization holds no record of the draft's tree, or no longer holds every file of it: its `domain` is `runtime` and its `retry` `no`, a stored record contradicting itself that neither the caller nor a retry can mend. No registered code names this refusal yet; it is not the `request.idempotency_conflict` of an idempotency key reused with other arguments (below). |
79
83
  | `413` | `request.too_large` | A copied example, or the copy's row, does not fit in one stored item. The row keeps room for the summary a publish adds, so a `name` that would leave none is refused. |
80
- | `422` | `request.invalid` | A body that does not validate, such as a name holding a lone surrogate. |
84
+ | `422` | `request.invalid` | A body that does not validate, such as a name holding a lone surrogate, or an `expected_draft_updated_at` sent to copy another organization's public method. |
81
85
  | `422` | `pipelex.catalog.copy_too_large` | The copy passes one of its bounds on the stored files it copies. Its problem's `limit` member, typed `MethodCopyLimit`, names the bound, `max_stored_files` or `max_stored_bytes`, its `value` and what the copy `reached`. Removing examples, or the files they hold, brings it under. |
82
86
  | `502` | `runtime.unavailable` | The platform could not read the caller's memberships. Its `retry` is `yes`. |
83
87
  | `500` | `pipelex.storage.not_configured` | The platform has no usable credentials for storage. Its `retry` is `no`. |
@@ -116,4 +120,4 @@ Without a key, a copy whose answer was lost cannot be told from one that never r
116
120
 
117
121
  ## `copied_from` on a method
118
122
 
119
- Every read of a method, `getMethod` and the `method` of every write, carries `copied_from`: `{ org_id, method_id, draft_digest }` of the original on a copy, `null` on a method created afresh, and absent from a platform that predates copies. It is written once, when the copy is made, and never rewritten, so it names the draft the copy started from even after the original has changed or been erased; its `draft_digest` is `null` for a draft that held no tree. It holds opaque ids only, never a name, and the original's organization may be one the reader does not belong to. The list rows (`MethodSummary`) do not carry it.
123
+ Every read of a method, `getMethod` and the `method` of every write, carries `copied_from`: `{ org_id, method_id, draft_digest, version }` of the original on a copy, `version` naming the published version a copy of another organization's public method took and `null` for a copy of a draft, `null` on a method created afresh, and absent from a platform that predates copies. It is written once, when the copy is made, and never rewritten, so it names what the copy started from even after the original has changed or been erased: the draft, by its `draft_digest`, `null` for a draft that held no tree, or, for a copy of another organization's public method, the published `version`, its `draft_digest` `null`. It holds opaque ids only, never a name, and the original's organization may be one the reader does not belong to. The list rows (`MethodSummary`) do not carry it, and neither does another organization's public method read as a `PublicMethodDetail`.
@@ -147,7 +147,7 @@ const method = await client.createMethod({ name: "Receipts", tree });
147
147
 
148
148
  ## Reading a tree
149
149
 
150
- `getMethod(methodId)` answers a `MethodDetail`: the method, with `files`, the draft's tree listed in tree path order. `getMethodVersion(methodId, version)` answers a `MethodVersion`: the version's summary, with the same `files`. Each file is a `SavedMethodFile`:
150
+ `getMethod(methodId)` answers a `MethodDetail`: the method, with `files`, the draft's tree listed in tree path order; another organization's public method answers a `PublicMethodDetail`, its `files` that of its latest published version ([public-methods.md](./public-methods.md)). `getMethodVersion(methodId, version)` answers a `MethodVersion`: the version's summary, with the same `files`. Each file is a `SavedMethodFile`:
151
151
 
152
152
  | Field | Meaning |
153
153
  |---|---|
@@ -0,0 +1,97 @@
1
+ # Public methods and the template catalog
2
+
3
+ A stored method is `internal` by default: only the members of the organization that holds it see it. An admin of the platform can make one `public`, and every organization then reads, runs and copies its published versions, while every write stays the owner's. Users see the capability through the template catalog: the public methods of the organization the deployment names as its templates organization, which every organization reads the same. No route of this client makes a method public or writes the catalog's order; both are the platform's admin surface. The types are in `src/product-models.ts`. This page is the twin of the Python SDK's `docs/public-methods.md`.
4
+
5
+ ```ts
6
+ import { PipelexApiClient } from "@pipelex/sdk";
7
+
8
+ const client = new PipelexApiClient();
9
+ const page = await client.listMethodTemplates({ ordering: "default", category: "finance", limit: 8 });
10
+ for (const tab of page.categories) console.log(tab.slug, tab.label); // every category with a template
11
+ for (const template of page.items) {
12
+ // The thumbnail whose ratio is closest to a 2:1 card.
13
+ const card = template.thumbnails.reduce<(typeof template.thumbnails)[number] | undefined>(
14
+ (best, thumbnail) =>
15
+ best === undefined ||
16
+ Math.abs(thumbnail.width / thumbnail.height - 2) < Math.abs(best.width / best.height - 2)
17
+ ? thumbnail
18
+ : best,
19
+ undefined,
20
+ );
21
+ console.log(template.name, template.description, card?.url);
22
+ }
23
+ // From a web session, a template copies into a team the caller may change methods in, from its
24
+ // latest published version; an API key, which belongs to one organization, cannot copy.
25
+ const first = page.items[0];
26
+ if (first !== undefined) {
27
+ await client.copyMethod(first.method_id, { destination_org_id: "org_team" });
28
+ }
29
+ ```
30
+
31
+ ## The catalog
32
+
33
+ | Method | Route | Answer |
34
+ |---|---|---|
35
+ | `listMethodTemplates({ ordering?, category?, limit?, cursor? })` | `GET /v1/method-templates` | A `MethodTemplatePage`: `{ items, nextCursor, ordering, categories }`. |
36
+ | `iterateMethodTemplates({ ordering?, category?, limit? })` | the same, following the cursor | Every `MethodTemplate`, in the order served. |
37
+
38
+ **The order is the platform's.** An ordering is a named list an admin writes, such as `default` or one arm of a test. The templates it lists lead, in its order, and every other template follows by name, so a template appears as soon as it is public. `ordering` asks for one by name; an absent, unknown or malformed name serves `default`, and the page's `ordering` names the one served, `null` when there is none, the templates then listed by name. A client never sorts the cards: it keeps the order served.
39
+
40
+ **A category filters and keeps the order.** `category` takes a slug of the catalog's list, such as `human-resources`, and keeps that category's templates in the same order; a slug no template shows keeps none. Whatever the filter, the page's `categories` names, in the catalog's order, every category at least one template shows, each a `MethodCategory` with its `slug` and its English `label`, so a tab bar drawn from it never shows an empty tab. A client translating the labels translates by slug and falls back on the label.
41
+
42
+ **A card is the latest published version.** Each `MethodTemplate` carries `method_id`, `name`, `description` (`null` when it has none), `latest_version`, `categories` (slugs), `organization_name`, the display name of the organization holding it, and `thumbnails`, each a `MethodTemplateThumbnail`: the image's `path`, `media_type`, `width`, `height` and `sha256`, with a `url` signed by storage and its `expires_at`, about fifteen minutes after the read. The ratio is never declared: draw the thumbnail whose `width` and `height` come closest to your frame. The link is a bearer credential until it expires, so it stays out of logs and stored records, and an answer the SDK cannot read is thrown with its links redacted.
43
+
44
+ **Paging.** A page holds 50 templates by default and at most 200; a `limit` outside 1 to 200 is a `422` `request.invalid`. `nextCursor` resumes after the last template the page served, wherever the catalog now places it, so a template added or removed elsewhere between two pages neither repeats nor skips the ones around that point. The cursor holds neither the ordering nor the category: pass both again with it, as `iterateMethodTemplates` does. A cursor of another listing is a `422` `request.invalid` and one that does not decode a `422` `request.malformed`.
45
+
46
+ **Refusals.** `500` `runtime.misconfigured` when the templates organization the deployment names has no record or a link cannot be signed, and `503` `runtime.unavailable`, safe to retry. A deployment that names no templates organization serves an empty catalog.
47
+
48
+ ## Reading another organization's public method
49
+
50
+ A template is a public method, so its `method_id` opens on the method routes of every organization:
51
+
52
+ | Method | From another organization |
53
+ |---|---|
54
+ | `getMethod(id)` | A `PublicMethodDetail`: the latest published version, never the draft |
55
+ | `listMethodVersions(id)`, `getMethodVersion(id, n)` | The published versions, each without `published_by` |
56
+ | `start`, `execute`, `startAndWaitForResult` with `method_id` | The bare id or `mt_…@<n>` runs, belongs to and is billed to the caller's organization; `mt_…@draft` is `404` |
57
+ | `copyMethod(id, …)` | A copy of the latest published version (below) |
58
+ | `writeDraft`, `renameMethod`, `publishMethod`, `deleteMethod`, the examples | `404` `pipelex.catalog.not_found`: every write is the owner's |
59
+
60
+ **`getMethod` answers the owner's shape or the public one**, typed `MethodDetail | PublicMethodDetail`. The caller's own method reads whole, as before. Another organization's public method reads in a narrower shape: `method_id`, `org_id` (the organization holding it), `name`, `created_at`, `visibility`, and the latest published version's `description`, `categories`, `latest_version` and `latest_published`, with `files`, that version's tree, linked in the holding organization, which `writeMethodTree` writes as any listing. It carries nothing of the draft or of the owner's people: no `updated_at`, `draft_digest`, `input_data`, `pipe_output`, `created_by_user_id`, `copied_from` or `deletion_state`. The draft's token `updated_at` is what tells them apart, so narrow before reading the draft. An answer without `updated_at` that still carries a member only the owner's read has is a damaged owner read, and the SDK throws it as an answer it cannot read rather than pass it off as a public method:
61
+
62
+ ```ts
63
+ const method = await client.getMethod(methodId);
64
+ if ("updated_at" in method) {
65
+ await client.writeDraft(method.method_id, { contents, expected_updated_at: method.updated_at });
66
+ } else {
67
+ // Another organization's public method: read it, run it, copy it.
68
+ console.log(`version ${method.latest_version} of ${method.org_id}'s method`);
69
+ }
70
+ ```
71
+
72
+ **A version read leaves out its publisher.** `MethodVersionSummary.published_by` is optional for that reason: the publisher's user id no more crosses organizations than the method's `created_by_user_id` does.
73
+
74
+ **What reads as not found.** An unknown id, another organization's internal method, and a public one made internal again or being erased all answer `404` `pipelex.catalog.not_found`, alike. A method made internal is closed at once, even before the platform's index has caught up.
75
+
76
+ ## Copying a public method
77
+
78
+ `copyMethod` of another organization's public method copies from its **latest published version**, never its draft: that version's tree, description and categories, and the method's saved examples, but not the owner's form inputs. The copy's `copied_from` names the `version` it took, its `draft_digest` `null`. Leave `expected_draft_updated_at` out: the caller never saw that method's draft, and the field sent with it is refused with a `422` `request.invalid` naming it. The rest of the copy, its verdict and its refusals, are [method-copy.md](./method-copy.md)'s.
79
+
80
+ ## Visibility, categories and thumbnails on a method
81
+
82
+ - **`visibility`**, on every method read (`MethodData`, `MethodDetail`, `PublicMethodDetail`): `internal` or `public`, read open, so a value a later platform adds arrives as its raw string. Absent from a platform that predates it, which means `internal`. The list rows (`MethodSummary`) do not carry it.
83
+ - **`categories`**, on a method and on each version summary: the slugs the manifest declares under `[package]` that are on the catalog's list, in their declared order. The platform ignores a value it does not know, so a typo reads as no category.
84
+ - **`thumbnails`**, on each version summary: the valid images directly under the tree's `thumbnails/` folder, each a `MethodThumbnail` (`path`, `media_type`, `width`, `height`, `sha256`), measured by the publish. A PNG, JPEG or WebP of at least 800 by 400 pixels is valid; an invalid image is simply not recorded.
85
+ - **`description`**: the manifest's `description`, else the primary bundle's top-level `description`, else its main pipe's, a blank one counting as none at each step.
86
+
87
+ ## Publishing a public method
88
+
89
+ A public method's publish is held to more than any draft's, so that a template's card never loses its picture and every organization can run what it reads. On top of `invalid` and `not_runnable`, its `refused` verdict may carry:
90
+
91
+ | `reason` | When |
92
+ |---|---|
93
+ | `storage_reference` | A `.mthds` file of the draft names a `pipelex-storage://` file, which other organizations cannot read. |
94
+ | `thumbnail_missing` | The draft's tree holds no file directly under `thumbnails/`, or none was read within the publish's time; a retry may read them in time. |
95
+ | `thumbnail_invalid` | No file directly under `thumbnails/` is a valid thumbnail. |
96
+
97
+ Each is a verdict, a `200` with a `message` for a person and the runner's valid `validation`, never a thrown error. `MethodPublishRefused.reason` is read open, typed `MethodPublishRefusalReason | (string & {})`: a reason a later platform adds arrives as its raw string, still a verdict, so a client shows its `message` when it does not handle the reason. An internal method's publish never meets the reasons of this table. A method made public while its publish ran is a `503` with nothing written, safe to retry.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pipelex/sdk",
3
- "version": "0.60.0",
3
+ "version": "0.61.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": {