@pipelex/sdk 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -167,6 +167,17 @@ for await (const saved of client.iterateMethodExamples(created.method_id)) {
167
167
  // every example, oldest first
168
168
  }
169
169
 
170
+ // Copy a method into a team, or into your personal workspace, with its draft, its form inputs and
171
+ // its examples (web sessions only). A bundle naming storage the destination cannot read answers
172
+ // `refused`, a verdict rather than an error; the key makes a retry copy once.
173
+ const copy = await client.copyMethod(
174
+ created.method_id,
175
+ { destination_org_id: "org_team", expected_draft_updated_at: draft.updated_at },
176
+ { idempotencyKey: crypto.randomUUID() },
177
+ );
178
+ if (copy.outcome === "copied") console.log(copy.method.copied_from); // the original it names
179
+ const copies = await client.listMethodCopies(created.method_id); // where the copies went
180
+
170
181
  // The organization's files, newest first: uploads, test files and the files runs made. Each row's
171
182
  // `uri` goes into a run's inputs as any address does. A filtered page can be empty and still carry
172
183
  // a nextCursor; iterateFiles follows it.
@@ -273,6 +284,7 @@ These pages ship inside the published package, so a reader who has only installe
273
284
  | [`docs/method-trees.md`](./docs/method-trees.md) | A method's tree: the four forms a run, a validation, a save and the crate routes take it in (a directory on disk among them), how it travels inline or by hash and what is uploaded, the files a step reads, the template check's warnings and which step reads which file of the tree, the deployment's limits, the read's listing with its links, `writeMethodTree`, and `MethodFileTransferError` |
274
285
  | [`docs/files.md`](./docs/files.md) | An organization's files: `listFiles` and `iterateFiles`, the row, the whole record and its provenance, the query and its repeated filters, paging through filtered pages, one file and its uses, the records of an address, the rename rule, links that name the file, a zip of files and its caps, a run's files, the copy into a method's draft, and an upload recorded as a method's test file with the refusals that come with it |
275
286
  | [`docs/method-examples.md`](./docs/method-examples.md) | A method's saved examples: the create, the paged list and its iterator, the edit that raises the input revision, the latest-run write and its `recorded` or `stale` verdict, the build assistant's save and its `kept` or `created` verdict, how an integer past `Number.MAX_SAFE_INTEGER` in the inputs stays exact, and what is refused before sending |
287
+ | [`docs/method-copy.md`](./docs/method-copy.md) | Copying a method into another organization or duplicating it: what a copy holds and the stored files it copies, the `copied` or `refused` verdict, every refusal, the copy's idempotency key, the copies a method lists, and `copied_from` |
276
288
  | [`docs/organization-branding.md`](./docs/organization-branding.md) | An organization's logos and colours: `getBranding`, which every member and API key reads, `updateOrganizationBranding`, an admin's write that sets, clears or keeps each key, and how a refused logo or colour names its slot in `errors` so a settings screen shows the sentence under it |
277
289
  | [`docs/exact-integers.md`](./docs/exact-integers.md) | Which reads keep an integer past `Number.MAX_SAFE_INTEGER` exact, as a `bigint` (a run's inputs and outputs, an example's inputs), how a request body writes it back with its digits, and `parseExactJson` and `stringifyExactJson` for code showing such data as JSON text |
278
290
  | [`docs/crate-routes.md`](./docs/crate-routes.md) | `resolve`, `codegen` and `pipeIo` — the method's tree as the protocol's `files` map, with its dependencies, the normalized crate, the stamped types, and a method's I/O artifacts without a validation — and the offline `runCodegenCheck` that guards a committed tree |
@@ -15,7 +15,7 @@ import type { CommandIO } from "./io.js";
15
15
  * update notice and npm 12's run notices off the script's stderr, which carries the command's
16
16
  * own lines, while npm's warnings and errors still print.
17
17
  */
18
- export declare const SCRIPT_RUNNER = "npx --yes --no-update-notifier --loglevel=warn @pipelex/sdk@0.50.0";
18
+ export declare const SCRIPT_RUNNER = "npx --yes --no-update-notifier --loglevel=warn @pipelex/sdk@0.51.0";
19
19
  export declare const SCRIPT_WRITER = "@pipelex/sdk";
20
20
  export declare const SCRIPT_NEEDS = "Node 22.12 or later";
21
21
  /** Run `pipelex-sdk script` and return its exit code. */
package/dist/client.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { MTHDSProtocol, ModelCategory, ModelDeck, RunOptions, StartOptions, VersionInfo } from "mthds/protocol";
2
2
  import type { CodegenRequest, CodegenResponse, CrateRouteOptions, DictPipeOutput, FormatResponse, LintResponse, ModelCheckCategory, ModelReferenceVerdict, PipeIORequest, PipeIOResponse, PipelexRunResultStart, PipelexValidateSource, PipelexValidationResult, ResolveRequest, ResolveResponse, ValidateMethodSelector } from "./models.js";
3
3
  import { type GetRunResultOptions, type RunProgress, type RunRead, type RunResults, type RunResultState, type StartAndWaitForResultOptions, type WaitForResultOptions } from "./runs.js";
4
- import type { BillingPortalResponse, ChangePlanResponse, CheckoutResponse, DraftFileFromFileInput, DraftFileFromFileResult, FileArchive, FileArchiveRequest, FileDetail, FileLinkTarget, FileLinksResult, FilePage, FileRecord, FileRecordList, FileRenameInput, FileSummary, GetFileQuery, GetFilesByUriOptions, InvoiceView, Membership, MembershipsResponse, OrganizationBranding, OrganizationBrandingUpdate, ListFilesQuery, ListMethodExamplesQuery, ListMethodsQuery, ListMethodVersionsQuery, ListRunFilesQuery, MethodData, MethodDeletionAccepted, MethodDetail, MethodDraftInput, MethodExample, MethodExampleAssistantSaveInput, MethodExampleAssistantSaveResult, MethodExampleCreateInput, MethodExampleLatestRunInput, MethodExampleLatestRunResult, MethodExamplePage, MethodExampleUpdateInput, MethodPage, MethodPublishInput, MethodPublishResult, MethodRenameInput, MethodSummary, MethodVersion, MethodVersionPage, MethodWriteInput, OnboardingSubmission, PipelexApiKeyCreated, PipelexApiKeyList, ListRunsQuery, RunHistoryItem, RunDetail, RunFilesPage, RunPage, PlanView, ResolvedStorageUrl, SavedMethodFile, SubscriptionResponse, UpdateRunInput, UploadGrant, UploadGrantInput, UploadInput, UploadedFile, UserProfile } from "./product-models.js";
4
+ import type { BillingPortalResponse, ChangePlanResponse, CheckoutResponse, DraftFileFromFileInput, DraftFileFromFileResult, FileArchive, FileArchiveRequest, FileDetail, FileLinkTarget, FileLinksResult, FilePage, FileRecord, FileRecordList, FileRenameInput, FileSummary, GetFileQuery, GetFilesByUriOptions, InvoiceView, Membership, MembershipsResponse, OrganizationBranding, OrganizationBrandingUpdate, ListFilesQuery, ListMethodCopiesQuery, ListMethodExamplesQuery, ListMethodsQuery, ListMethodVersionsQuery, ListRunFilesQuery, MethodCopyInput, MethodCopyPage, MethodCopyResult, MethodData, MethodDeletionAccepted, MethodDetail, MethodDraftInput, MethodExample, MethodExampleAssistantSaveInput, MethodExampleAssistantSaveResult, MethodExampleCreateInput, MethodExampleLatestRunInput, MethodExampleLatestRunResult, MethodExamplePage, MethodExampleUpdateInput, MethodPage, MethodPublishInput, MethodPublishResult, MethodRenameInput, MethodSummary, MethodVersion, MethodVersionPage, MethodWriteInput, OnboardingSubmission, PipelexApiKeyCreated, PipelexApiKeyList, ListRunsQuery, RunHistoryItem, RunDetail, RunFilesPage, RunPage, PlanView, ResolvedStorageUrl, SavedMethodFile, SubscriptionResponse, UpdateRunInput, UploadGrant, UploadGrantInput, UploadInput, UploadedFile, UserProfile } from "./product-models.js";
5
5
  import type { MethodTreeForms, MethodTreeLimits } from "./method-trees.js";
6
6
  import type { AppInfo } from "./user-agent.js";
7
7
  import type { UploadableAsset, UploadFileOptions, UploadRecord } from "./upload.js";
@@ -122,6 +122,34 @@ export interface PipelexStartRequestOptions {
122
122
  */
123
123
  idempotencyKey?: string | null;
124
124
  }
125
+ /**
126
+ * What a method copy sends beside its body: the request's own header, which no copy argument
127
+ * reaches, camelCase since it is no wire field of the copy.
128
+ */
129
+ export interface MethodCopyRequestOptions {
130
+ /**
131
+ * The copy's idempotency key, sent as the `Idempotency-Key` header of
132
+ * `POST /v1/methods/{id}/copy`, and not sent at all when absent or `null`. Generate one per copy
133
+ * the caller means to make (a `crypto.randomUUID()` is a good key), and keep it, with the
134
+ * arguments unchanged, across that copy's retries.
135
+ *
136
+ * The hosted API stores the copy's answer under the key for 24 hours, scoped to the caller and
137
+ * the method's organization, so the same copy sent again under the same key is answered with the
138
+ * stored answer and copies nothing more: a copy whose answer was lost, as on a client timeout,
139
+ * can be sent again unchanged and makes one copy, not two. A verdict and a `4xx` refusal of the
140
+ * copy are stored alike, so the same copy under the same key is answered the same again; a `5xx`
141
+ * and a refusal for its timing (a `408`, `425` or `429`) are not stored, and a copy that failed
142
+ * with one left nothing visible, so the same key sends it afresh.
143
+ * The same key with a different body is refused (`409` `conflict`), as is a key whose earlier
144
+ * request is still being handled (`409` `idempotency_key_in_use`, retryable: send it again
145
+ * under the same key in a few seconds); a changed copy, such as one with a fresh draft token,
146
+ * goes under a new key. See `docs/method-copy.md`.
147
+ *
148
+ * A key is held to the rule a start's is (`PipelexStartRequestOptions.idempotencyKey`): 1 to
149
+ * 255 visible ASCII characters, anything else a `RequestArgumentError` before any request.
150
+ */
151
+ idempotencyKey?: string | null;
152
+ }
125
153
  /**
126
154
  * `start()` / `startAndWaitForResult()` options — the same, for the durable path, plus the start
127
155
  * request's own `idempotencyKey`.
@@ -231,7 +259,7 @@ export declare class PipelexApiClient implements HostedProtocolSurface {
231
259
  * resolved absolute URL. `mayStartRun` marks a request that may create a run,
232
260
  * whose `ApiUnreachableError` then says whether it may have (see `RunCreation`).
233
261
  * `idempotencyKey`, already checked by `idempotencyKeyOf`, is sent as the
234
- * `Idempotency-Key` header; only `start` passes one.
262
+ * `Idempotency-Key` header; only `start` and `copyMethod` pass one.
235
263
  */
236
264
  private requestRaw;
237
265
  /**
@@ -1215,6 +1243,71 @@ export declare class PipelexApiClient implements HostedProtocolSurface {
1215
1243
  * never rewritten as one, which names no tree to serve.
1216
1244
  */
1217
1245
  getMethodVersion(methodId: string, version: number): Promise<MethodVersion>;
1246
+ /**
1247
+ * Copy a method into an organization the caller may change methods in —
1248
+ * `POST /v1/methods/{id}/copy`: the method's own organization for a duplicate, another team, or
1249
+ * the caller's personal workspace.
1250
+ *
1251
+ * The copy is a new, unpublished method of the destination, the caller's, holding the
1252
+ * original's draft tree (the same `draft_digest`), its name unless `name` gives another, its
1253
+ * form inputs and every saved example, each stored file they name copied into the destination,
1254
+ * and `copied_from` naming the original. Published versions and runs are not copied, and the
1255
+ * original stays as it was. The copy takes the draft, never the latest published version; with
1256
+ * `expected_draft_updated_at`, it takes exactly the draft the caller last saw.
1257
+ *
1258
+ * The answer is a `MethodCopyResult` discriminated on `outcome` — branch on it: `copied` with
1259
+ * the copy as `method`, `examples_copied` and `stored_files_copied`; `refused` with the
1260
+ * `reason` `bundle_names_other_storage`, a `message` and every `references` entry (`{ path, uri
1261
+ * }`) a `.mthds` file of the draft holds that the destination cannot read, nothing written.
1262
+ *
1263
+ * The route serves web sessions only: a Pipelex API key belongs to one organization and cannot
1264
+ * speak for another. `options.idempotencyKey` is sent as the `Idempotency-Key` header, so a copy
1265
+ * sent again under the same key is answered with the first one's answer rather than copied
1266
+ * twice (see `MethodCopyRequestOptions`).
1267
+ *
1268
+ * Takes a bare catalog id, as `getMethod` does. Sends `destination_org_id` and, when given,
1269
+ * `name` and `expected_draft_updated_at`, and nothing else: the body is closed, and a `null`
1270
+ * reads as not given. Throws `RequestArgumentError` before any request for a
1271
+ * `destination_org_id` that is not a non-empty string, a `name` or an
1272
+ * `expected_draft_updated_at` given that is not a string, and a malformed idempotency key.
1273
+ * Throws `ApiResponseError` when no verdict was produced, each refusal writing nothing a reader
1274
+ * sees: `403` `forbidden` for a request that is not from a web session; `403`
1275
+ * `role_not_permitted` for a role that may not change methods, in the method's organization or
1276
+ * in the destination; `404` `organization_not_found` for a destination the caller does not
1277
+ * belong to, still being set up or unknown, alike; `404` `not_found` for an unknown method;
1278
+ * `409` `method_being_deleted`; `409` `method_update_conflict` for a draft that moved since
1279
+ * `expected_draft_updated_at`; `409` `conflict`, `runtime`, for a draft whose tree the
1280
+ * organization holds no full record of; `413` `payload_too_large` for an example or a copy too
1281
+ * large for one stored item; `422` `validation_failed` for a body that does not validate, such
1282
+ * as a name holding a lone surrogate; `422` `method_copy_too_large` past one of the copy's
1283
+ * bounds on the stored files it copies, named by `problemDocument.limit` (`MethodCopyLimit`);
1284
+ * and, each retryable, a `502` when the platform could not read the caller's memberships, a
1285
+ * `500` `storage_backend_failed` and a `503` past the copy's deadline. An answer whose
1286
+ * `outcome` is neither arm's, a `copied` one whose `method` is not an object or a `refused` one
1287
+ * whose `references` is not an array, is an answer the SDK cannot read, thrown as an
1288
+ * `ApiResponseError` too.
1289
+ */
1290
+ copyMethod(methodId: string, input: MethodCopyInput, options?: MethodCopyRequestOptions): Promise<MethodCopyResult>;
1291
+ /**
1292
+ * One page of the copies made of a method, in the order of the copies' ids, which says nothing
1293
+ * of when each was made — `GET /v1/methods/{id}/copies`.
1294
+ *
1295
+ * Each row is the `MethodCopyMarker` the original keeps of a copy: the copy's id, the
1296
+ * destination's id, the member who made it and when. The destination is named by its id alone,
1297
+ * never its name, so a surface shows a destination's name only from the reader's own
1298
+ * memberships, and a copy in an organization the reader does not belong to stays a bare id. A
1299
+ * marker stays when its copy is erased, and the copy then reads as not found. Pass the page's
1300
+ * `nextCursor` back as `cursor` to continue; `null` is the last page. A method never copied,
1301
+ * and a copy itself, answer an empty page. The listing is open to every member of the method's
1302
+ * organization, a runner included, as every read of a method is.
1303
+ *
1304
+ * Takes a bare catalog id, as `getMethod` does. Throws `ApiResponseError`: `404` `not_found` for
1305
+ * an unknown method, `409` `method_being_deleted`, `400` `malformed_request` for a cursor the
1306
+ * listing did not issue for this method, `422` for a `limit` outside 1 to 100. A page whose
1307
+ * `items` is not an array or whose `next_cursor` is neither a string nor `null` is an answer the
1308
+ * SDK cannot read, thrown as an `ApiResponseError` too.
1309
+ */
1310
+ listMethodCopies(methodId: string, query?: ListMethodCopiesQuery): Promise<MethodCopyPage>;
1218
1311
  /**
1219
1312
  * Erase a method and everything it produced — `DELETE /v1/methods/{id}`.
1220
1313
  *
package/dist/client.js CHANGED
@@ -233,6 +233,11 @@ const ARCHIVE_RUN_ID_ARGUMENT = "a run_id naming the run whose files to zip, a n
233
233
  const ARCHIVE_NAME_ARGUMENT = "a name for the zip, a string, or none";
234
234
  const DRAFT_PATH_ARGUMENT = "the tree path the file takes in the draft, a non-empty string";
235
235
  const DRAFT_TOKEN_ARGUMENT = "the draft token the caller last read, the method's `updated_at`, a string, or none";
236
+ /** What each argument of a method copy is, as a refusal of it names it. */
237
+ const COPY_DESTINATION_ARGUMENT = "a destination_org_id, the canonical id of the organization to copy into, a non-empty string";
238
+ const COPY_NAME_ARGUMENT = "a name for the copy, a string, or none";
239
+ const COPY_DRAFT_TOKEN_ARGUMENT = "an expected_draft_updated_at, the draft token the caller last read, the method's " +
240
+ "`updated_at`, a string, or none";
236
241
  /**
237
242
  * The refusal of an argument a call cannot take, worded as every such refusal is:
238
243
  * `<call>() takes <what>; got <got>.`, with what made it unusable as `cause` when there is one.
@@ -341,6 +346,8 @@ const isString = (value) => typeof value === "string";
341
346
  const FILE_ARCHIVE_STATES = ["queued", "building", "ready", "failed"];
342
347
  /** What a copy into a draft did in the blob store, the field its answer is discriminated on. */
343
348
  const DRAFT_FILE_BLOBS = ["copied", "already_held"];
349
+ /** What a method copy did, the field its answer is discriminated on. */
350
+ const METHOD_COPY_OUTCOMES = ["copied", "refused"];
344
351
  /** Whether a run's records are all written, the field a reader of its files branches on. */
345
352
  const RUN_FILES_STATES = ["listing", "listed"];
346
353
  /** The lists of a page of a run's files, each an array on every page. */
@@ -479,7 +486,7 @@ export class PipelexApiClient {
479
486
  * resolved absolute URL. `mayStartRun` marks a request that may create a run,
480
487
  * whose `ApiUnreachableError` then says whether it may have (see `RunCreation`).
481
488
  * `idempotencyKey`, already checked by `idempotencyKeyOf`, is sent as the
482
- * `Idempotency-Key` header; only `start` passes one.
489
+ * `Idempotency-Key` header; only `start` and `copyMethod` pass one.
483
490
  */
484
491
  async requestRaw(method, url, options = {}) {
485
492
  const hasBody = options.body !== undefined;
@@ -692,6 +699,7 @@ export class PipelexApiClient {
692
699
  body,
693
700
  timeoutMs: options.timeoutMs ?? POLL_REQUEST_TIMEOUT_MS,
694
701
  signal: options.signal,
702
+ idempotencyKey: options.idempotencyKey,
695
703
  });
696
704
  if (res.status < 200 || res.status >= 300) {
697
705
  this.throwApiResponseError(method, endpoint, res);
@@ -2347,6 +2355,116 @@ export class PipelexApiClient {
2347
2355
  const res = await this.requestProductAnswer("GET", endpoint, undefined);
2348
2356
  return this.readSavedTree("GET", endpoint, withLinks(res), "a published version");
2349
2357
  }
2358
+ /**
2359
+ * Copy a method into an organization the caller may change methods in —
2360
+ * `POST /v1/methods/{id}/copy`: the method's own organization for a duplicate, another team, or
2361
+ * the caller's personal workspace.
2362
+ *
2363
+ * The copy is a new, unpublished method of the destination, the caller's, holding the
2364
+ * original's draft tree (the same `draft_digest`), its name unless `name` gives another, its
2365
+ * form inputs and every saved example, each stored file they name copied into the destination,
2366
+ * and `copied_from` naming the original. Published versions and runs are not copied, and the
2367
+ * original stays as it was. The copy takes the draft, never the latest published version; with
2368
+ * `expected_draft_updated_at`, it takes exactly the draft the caller last saw.
2369
+ *
2370
+ * The answer is a `MethodCopyResult` discriminated on `outcome` — branch on it: `copied` with
2371
+ * the copy as `method`, `examples_copied` and `stored_files_copied`; `refused` with the
2372
+ * `reason` `bundle_names_other_storage`, a `message` and every `references` entry (`{ path, uri
2373
+ * }`) a `.mthds` file of the draft holds that the destination cannot read, nothing written.
2374
+ *
2375
+ * The route serves web sessions only: a Pipelex API key belongs to one organization and cannot
2376
+ * speak for another. `options.idempotencyKey` is sent as the `Idempotency-Key` header, so a copy
2377
+ * sent again under the same key is answered with the first one's answer rather than copied
2378
+ * twice (see `MethodCopyRequestOptions`).
2379
+ *
2380
+ * Takes a bare catalog id, as `getMethod` does. Sends `destination_org_id` and, when given,
2381
+ * `name` and `expected_draft_updated_at`, and nothing else: the body is closed, and a `null`
2382
+ * reads as not given. Throws `RequestArgumentError` before any request for a
2383
+ * `destination_org_id` that is not a non-empty string, a `name` or an
2384
+ * `expected_draft_updated_at` given that is not a string, and a malformed idempotency key.
2385
+ * Throws `ApiResponseError` when no verdict was produced, each refusal writing nothing a reader
2386
+ * sees: `403` `forbidden` for a request that is not from a web session; `403`
2387
+ * `role_not_permitted` for a role that may not change methods, in the method's organization or
2388
+ * in the destination; `404` `organization_not_found` for a destination the caller does not
2389
+ * belong to, still being set up or unknown, alike; `404` `not_found` for an unknown method;
2390
+ * `409` `method_being_deleted`; `409` `method_update_conflict` for a draft that moved since
2391
+ * `expected_draft_updated_at`; `409` `conflict`, `runtime`, for a draft whose tree the
2392
+ * organization holds no full record of; `413` `payload_too_large` for an example or a copy too
2393
+ * large for one stored item; `422` `validation_failed` for a body that does not validate, such
2394
+ * as a name holding a lone surrogate; `422` `method_copy_too_large` past one of the copy's
2395
+ * bounds on the stored files it copies, named by `problemDocument.limit` (`MethodCopyLimit`);
2396
+ * and, each retryable, a `502` when the platform could not read the caller's memberships, a
2397
+ * `500` `storage_backend_failed` and a `503` past the copy's deadline. An answer whose
2398
+ * `outcome` is neither arm's, a `copied` one whose `method` is not an object or a `refused` one
2399
+ * whose `references` is not an array, is an answer the SDK cannot read, thrown as an
2400
+ * `ApiResponseError` too.
2401
+ */
2402
+ async copyMethod(methodId, input, options = {}) {
2403
+ const call = "copyMethod";
2404
+ const endpoint = `${methodPath(methodId, call)}/copy`;
2405
+ const { destination_org_id: destinationOrgId, name, expected_draft_updated_at: token, } = input ?? {};
2406
+ const body = {
2407
+ destination_org_id: requireNonEmptyId(destinationOrgId, call, COPY_DESTINATION_ARGUMENT),
2408
+ };
2409
+ if (name != null) {
2410
+ if (typeof name !== "string") {
2411
+ throw argumentRefusal(call, COPY_NAME_ARGUMENT, describeNumberArgument(name));
2412
+ }
2413
+ body.name = name;
2414
+ }
2415
+ if (token != null) {
2416
+ if (typeof token !== "string") {
2417
+ throw argumentRefusal(call, COPY_DRAFT_TOKEN_ARGUMENT, describeNumberArgument(token));
2418
+ }
2419
+ body.expected_draft_updated_at = token;
2420
+ }
2421
+ const idempotencyKey = idempotencyKeyOf(options, "copy the method");
2422
+ const res = await this.requestProductAnswer("POST", endpoint, body, { idempotencyKey });
2423
+ const answer = this.readObjectAnswer("POST", endpoint, res);
2424
+ let fault;
2425
+ switch (answer.outcome) {
2426
+ case "copied":
2427
+ if (!isPlainObject(answer.method)) {
2428
+ fault = "a `copied` copy result whose `method` is not an object";
2429
+ }
2430
+ break;
2431
+ case "refused":
2432
+ if (!Array.isArray(answer.references)) {
2433
+ fault = "a `refused` copy result whose `references` is not an array";
2434
+ }
2435
+ break;
2436
+ default:
2437
+ fault = `a copy result whose \`outcome\` is not ${inWords(METHOD_COPY_OUTCOMES.map((outcome) => `\`${outcome}\``), "or")}`;
2438
+ }
2439
+ if (fault !== undefined) {
2440
+ throw this.unreadableAnswer("POST", `/${API_PREFIX}/${endpoint}`, res, fault);
2441
+ }
2442
+ return answer;
2443
+ }
2444
+ /**
2445
+ * One page of the copies made of a method, in the order of the copies' ids, which says nothing
2446
+ * of when each was made — `GET /v1/methods/{id}/copies`.
2447
+ *
2448
+ * Each row is the `MethodCopyMarker` the original keeps of a copy: the copy's id, the
2449
+ * destination's id, the member who made it and when. The destination is named by its id alone,
2450
+ * never its name, so a surface shows a destination's name only from the reader's own
2451
+ * memberships, and a copy in an organization the reader does not belong to stays a bare id. A
2452
+ * marker stays when its copy is erased, and the copy then reads as not found. Pass the page's
2453
+ * `nextCursor` back as `cursor` to continue; `null` is the last page. A method never copied,
2454
+ * and a copy itself, answer an empty page. The listing is open to every member of the method's
2455
+ * organization, a runner included, as every read of a method is.
2456
+ *
2457
+ * Takes a bare catalog id, as `getMethod` does. Throws `ApiResponseError`: `404` `not_found` for
2458
+ * an unknown method, `409` `method_being_deleted`, `400` `malformed_request` for a cursor the
2459
+ * listing did not issue for this method, `422` for a `limit` outside 1 to 100. A page whose
2460
+ * `items` is not an array or whose `next_cursor` is neither a string nor `null` is an answer the
2461
+ * SDK cannot read, thrown as an `ApiResponseError` too.
2462
+ */
2463
+ async listMethodCopies(methodId, query = {}) {
2464
+ const search = productQuery({ limit: query.limit, cursor: query.cursor });
2465
+ const endpoint = `${methodPath(methodId, "listMethodCopies")}/copies`;
2466
+ return this.requestPage(`${endpoint}${search}`);
2467
+ }
2350
2468
  /**
2351
2469
  * Erase a method and everything it produced — `DELETE /v1/methods/{id}`.
2352
2470
  *
@@ -3802,7 +3920,7 @@ function assertLinkageMethodIdIsBare(options, treeForms) {
3802
3920
  function nonEmptyString(value) {
3803
3921
  return value != null && value.length > 0 ? value : undefined;
3804
3922
  }
3805
- /** The request header the hosted API reads a run start's idempotency key from. */
3923
+ /** The request header the hosted API reads a request's idempotency key from. */
3806
3924
  const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
3807
3925
  /**
3808
3926
  * The longest idempotency key a start sends. It is room for any generated key, and keeps the
@@ -3817,21 +3935,22 @@ const MAX_IDEMPOTENCY_KEY_LENGTH = 255;
3817
3935
  */
3818
3936
  const VISIBLE_ASCII = /^[\x21-\x7E]+$/;
3819
3937
  /**
3820
- * The idempotency key a start sends, or `undefined` when the options carry none (absent or
3821
- * `null`). A key that is not a string of 1 to 255 visible ASCII characters is refused with a
3822
- * `RequestArgumentError` rather than dropped, an empty one included, since a start sent without
3823
- * the key its caller gave would lose the protection the caller asked for without a word. The
3824
- * message never quotes the key, which a caller may derive from something private. The Python SDK
3825
- * holds a key to the same rule, in the same words.
3938
+ * The idempotency key a start or a method copy sends, or `undefined` when the options carry none
3939
+ * (absent or `null`). A key that is not a string of 1 to 255 visible ASCII characters is refused
3940
+ * with a `RequestArgumentError` rather than dropped, an empty one included, since a request sent
3941
+ * without the key its caller gave would lose the protection the caller asked for without a word.
3942
+ * `request` says what the request does, as the refusal's last sentence names it ("start the run",
3943
+ * "copy the method"). The message never quotes the key, which a caller may derive from something
3944
+ * private. The Python SDK holds a key to the same rule, in the same words.
3826
3945
  */
3827
- function idempotencyKeyOf(options) {
3946
+ function idempotencyKeyOf(options, request = "start the run") {
3828
3947
  const key = options.idempotencyKey;
3829
3948
  if (key == null)
3830
3949
  return undefined;
3831
3950
  const fault = idempotencyKeyFault(key);
3832
3951
  if (fault !== undefined) {
3833
3952
  throw new RequestArgumentError(`The idempotency key must be 1 to ${MAX_IDEMPOTENCY_KEY_LENGTH} visible ASCII characters ` +
3834
- `(\`!\` to \`~\`), and this one ${fault}. Leave it out to start the run without one.`);
3953
+ `(\`!\` to \`~\`), and this one ${fault}. Leave it out to ${request} without one.`);
3835
3954
  }
3836
3955
  return key;
3837
3956
  }