@pipelex/sdk 0.58.0 → 0.59.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.
@@ -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.58.0";
18
+ export declare const SCRIPT_RUNNER = "npx --yes --no-update-notifier --loglevel=warn @pipelex/sdk@0.59.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
@@ -4,6 +4,7 @@ import { type GetRunResultOptions, type RunProgress, type RunRead, type RunResul
4
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
+ import type { WebhookDeliveryDetail, WebhookDeliveryList, WebhookReplayAccepted, WebhookSecret, WebhookSecretRotateOptions } from "./webhooks.js";
7
8
  import type { UploadableAsset, UploadFileOptions, UploadRecord } from "./upload.js";
8
9
  import type { PrepareInputsRequest, PreparedInputs } from "./prepare-inputs.js";
9
10
  import type { BulkResolveStorageUrlsInput, BulkResolvedStorageUrls, DownloadArtifactsRequest, DownloadArtifactsResult, FetchArtifactOptions, ResolvedArtifact } from "./artifacts.js";
@@ -37,6 +38,27 @@ export interface PipelexApiRunExtensions {
37
38
  * An empty string is treated as absent and is not sent.
38
39
  */
39
40
  method_ref?: string | null;
41
+ /**
42
+ * Webhook URLs to call once the run has ended, each with a signed `run.finished` event whose
43
+ * `data` is the run's status read, so a caller learns of the end without polling. Read the call
44
+ * with `verifyWebhook`, then fetch the results with the event's `pipeline_run_id`; see
45
+ * `docs/webhooks.md`.
46
+ *
47
+ * A layer-2 extension: the hosted API and a self-hosted runner both take it on `start` and
48
+ * `execute`, and each holds the URLs to its own rules, which this client does not restate. The
49
+ * hosted API takes at most 5 HTTPS URLs of at most 2,048 characters, none repeated, and refuses a
50
+ * URL whose host resolves to a private, loopback, link-local or metadata address with a `422`
51
+ * `request.invalid` naming it as `callback_urls.<index>`; a deployment that does not deliver
52
+ * caller webhooks refuses the list with a `422` `request.unsupported`. A self-hosted runner
53
+ * whose operator configured no signing secret refuses it with a `422`
54
+ * `pipelex.webhook.secret_not_configured`. The start's acknowledgement and every read of the run
55
+ * list the URLs redacted, as `callback_urls`.
56
+ *
57
+ * Not a run source: a run still needs something to run. A value that is not an array of strings
58
+ * is a `RequestArgumentError` before anything is sent, and an empty array is treated as absent
59
+ * and is not sent.
60
+ */
61
+ callback_urls?: readonly string[] | null;
40
62
  }
41
63
  /**
42
64
  * The hosted API's own run arguments — the layer-3 extensions this client adds
@@ -1557,6 +1579,31 @@ export declare class PipelexApiClient implements HostedProtocolSurface {
1557
1579
  * the write may have landed or not: the same input sent again is safe.
1558
1580
  */
1559
1581
  updateOrganizationBranding(orgId: string, input: OrganizationBrandingUpdate): Promise<OrganizationBranding>;
1582
+ /**
1583
+ * The organization's webhook signing secret — `GET /v1/organizations/{org_id}/webhook-secret`:
1584
+ * the `whsec_` secret every caller webhook of the organization is signed with, which a receiver
1585
+ * hands to `verifyWebhook`. The platform generates it on this first read when the organization
1586
+ * has none. Treat the answer as a credential: never log or cache it.
1587
+ *
1588
+ * An admin's route, from a web session only: the hosted gateway refuses an API key on it. Throws
1589
+ * `ApiResponseError`: `403` `auth.forbidden` whose `params.requirement` names `admin` for any
1590
+ * other role, `403` `pipelex.org.mismatch` for an organization other than the caller's active
1591
+ * one, and `404` `pipelex.org.not_found` for one with no record.
1592
+ */
1593
+ getWebhookSecret(orgId: string): Promise<WebhookSecret>;
1594
+ /**
1595
+ * Rotate the organization's webhook signing secret — `POST
1596
+ * /v1/organizations/{org_id}/webhook-secret/rotate`: a new secret is current at once, and the
1597
+ * one it replaces keeps signing beside it for 24 hours, then is dropped, so a receiver has a day
1598
+ * to take the new one. An organization with no secret yet gets its first. Answers the new secret,
1599
+ * as `getWebhookSecret` does, with its guards and refusals.
1600
+ *
1601
+ * `options.idempotencyKey` is sent as the `Idempotency-Key` header. The answer carries a secret,
1602
+ * so the platform never replays it: the same key sent again is refused `409`
1603
+ * `request.already_executed` and rotates nothing more, and the secret is then read with
1604
+ * `getWebhookSecret`. A malformed key is a `RequestArgumentError` before anything is sent.
1605
+ */
1606
+ rotateWebhookSecret(orgId: string, options?: WebhookSecretRotateOptions): Promise<WebhookSecret>;
1560
1607
  /** The active org's subscription state — `GET /v1/billing/subscription`. */
1561
1608
  getSubscription(): Promise<SubscriptionResponse>;
1562
1609
  /** Available plans (with `is_current`) — `GET /v1/billing/plans`. */
@@ -2066,6 +2113,50 @@ export declare class PipelexApiClient implements HostedProtocolSurface {
2066
2113
  * string nor `null`, is an answer the SDK cannot read, thrown as an `ApiResponseError` too.
2067
2114
  */
2068
2115
  listRunFiles(runId: string, query?: ListRunFilesQuery): Promise<RunFilesPage>;
2116
+ /**
2117
+ * A run's caller-webhook deliveries — `GET /v1/runs/{id}/webhook-deliveries`: one per URL the
2118
+ * run registered in `callback_urls`, in their order, once the run has ended, each with its
2119
+ * state and every attempt, its URL redacted. A run that registered none, or has not ended, lists
2120
+ * none. Any member of the organization reads them, by web session or API key.
2121
+ *
2122
+ * Throws `RequestArgumentError` for a run id that is empty, `.` or `..`, or holds a lone
2123
+ * surrogate, before any request. Throws `ApiResponseError`: `404` `run.not_found` for a run that
2124
+ * is not the organization's, exactly as for one that does not exist. An answer whose
2125
+ * `deliveries` is not an array is an answer the SDK cannot read, thrown as an `ApiResponseError`
2126
+ * too.
2127
+ */
2128
+ listRunWebhookDeliveries(runId: string): Promise<WebhookDeliveryList>;
2129
+ /**
2130
+ * One delivery of a run's webhooks, with its event as sent —
2131
+ * `GET /v1/runs/{id}/webhook-deliveries/{delivery_id}`: the body of its latest attempt, which a
2132
+ * receiver's log can be matched against byte for byte once written as compact JSON.
2133
+ * `deliveryId` is the delivery's `delivery_id`, the receiver's `webhook-id`.
2134
+ *
2135
+ * Throws `RequestArgumentError` for an id that is empty, `.` or `..`, or holds a lone surrogate,
2136
+ * before any request. Throws `ApiResponseError`: `404` `run.not_found` for a run the caller
2137
+ * cannot read, and `422` `request.invalid` naming `delivery_id` for a delivery the run does not
2138
+ * have.
2139
+ */
2140
+ getRunWebhookDelivery(runId: string, deliveryId: string): Promise<WebhookDeliveryDetail>;
2141
+ /**
2142
+ * Send a settled delivery again — `POST /v1/runs/{id}/webhook-deliveries/{delivery_id}/replay`:
2143
+ * one new attempt under the same `webhook-id`, with the same `data`, the next `attempt`, a fresh
2144
+ * timestamp, and the signatures of the secrets live when it is sent. A replay the receiver
2145
+ * accepts marks the delivery `delivered`; one it does not leaves its state as it was, and the
2146
+ * attempt is recorded either way. A replay never returns a delivery to `pending`. Anyone who
2147
+ * can read the run asks for one, an API key included, under the organization's replay rate.
2148
+ *
2149
+ * Answers the attempt number the replay is sent under; a replay asked again before it is sent is
2150
+ * that same attempt. Throws `RequestArgumentError` for an id that is empty, `.` or `..`, or holds
2151
+ * a lone surrogate, before any request. Throws `ApiResponseError`: `404` `run.not_found` for a
2152
+ * run the caller cannot read; `422` `request.invalid` naming `delivery_id` for a delivery the run
2153
+ * does not have; `409` `pipelex.org.not_manageable` with the reason `webhook_delivery_pending`
2154
+ * for a delivery still on its retry schedule, or `webhook_delivery_attempts_exhausted` for one
2155
+ * holding the most attempts a delivery holds; `429` `account.rate_limited`, with its
2156
+ * `retry_after`, past the organization's replay rate; and a `500` or `503` when the replay could
2157
+ * not be queued, which then counts nothing against the rate.
2158
+ */
2159
+ replayRunWebhookDelivery(runId: string, deliveryId: string): Promise<WebhookReplayAccepted>;
2069
2160
  /** Patch a run's status (admin/manual) — `PUT /v1/runs/{id}`. */
2070
2161
  updateRun(runId: string, input: UpdateRunInput): Promise<void>;
2071
2162
  /**
@@ -2079,8 +2170,8 @@ export declare class PipelexApiClient implements HostedProtocolSurface {
2079
2170
  /**
2080
2171
  * Blocking `POST /v1/execute` adapted onto `RunResults` — the bare-runner
2081
2172
  * path. Forwards every protocol field PLUS every extension surface: the
2082
- * runner-resolved `method_ref`, the hosted `method_id`, and the generic
2083
- * `extra` passthrough. An extension-only call (`{ extra }` with no
2173
+ * runner-resolved `method_ref`, the run's `callback_urls`, the hosted
2174
+ * `method_id`, and the generic `extra` passthrough. An extension-only call (`{ extra }` with no
2084
2175
  * pipe_code/bundle) or a vendor selector riding `extra` must survive this
2085
2176
  * path, not just the durable one — a `method_ref` run must run the same
2086
2177
  * fetched package here, and a hosted `method_id` must reach the server too,
package/dist/client.js CHANGED
@@ -240,6 +240,10 @@ const ARCHIVE_ID_ARGUMENT = {
240
240
  name: "archive_id",
241
241
  what: "an archive id, the archive's non-empty `archive_id`",
242
242
  };
243
+ const DELIVERY_ID_ARGUMENT = {
244
+ name: "delivery_id",
245
+ what: "a delivery id, the delivery's non-empty `delivery_id`",
246
+ };
243
247
  /** What the run id of each example write is, as a refusal of it names it. */
244
248
  const CREATE_RUN_ID_ARGUMENT = {
245
249
  name: "run_id",
@@ -2939,6 +2943,39 @@ export class PipelexApiClient {
2939
2943
  const segment = idPathSegment(orgId, "updateOrganizationBranding", ORG_ID_ARGUMENT);
2940
2944
  return this.requestProductObject("PATCH", `organizations/${segment}/branding`, input);
2941
2945
  }
2946
+ /**
2947
+ * The organization's webhook signing secret — `GET /v1/organizations/{org_id}/webhook-secret`:
2948
+ * the `whsec_` secret every caller webhook of the organization is signed with, which a receiver
2949
+ * hands to `verifyWebhook`. The platform generates it on this first read when the organization
2950
+ * has none. Treat the answer as a credential: never log or cache it.
2951
+ *
2952
+ * An admin's route, from a web session only: the hosted gateway refuses an API key on it. Throws
2953
+ * `ApiResponseError`: `403` `auth.forbidden` whose `params.requirement` names `admin` for any
2954
+ * other role, `403` `pipelex.org.mismatch` for an organization other than the caller's active
2955
+ * one, and `404` `pipelex.org.not_found` for one with no record.
2956
+ */
2957
+ async getWebhookSecret(orgId) {
2958
+ const segment = idPathSegment(orgId, "getWebhookSecret", ORG_ID_ARGUMENT);
2959
+ return this.requestProductObject("GET", `organizations/${segment}/webhook-secret`);
2960
+ }
2961
+ /**
2962
+ * Rotate the organization's webhook signing secret — `POST
2963
+ * /v1/organizations/{org_id}/webhook-secret/rotate`: a new secret is current at once, and the
2964
+ * one it replaces keeps signing beside it for 24 hours, then is dropped, so a receiver has a day
2965
+ * to take the new one. An organization with no secret yet gets its first. Answers the new secret,
2966
+ * as `getWebhookSecret` does, with its guards and refusals.
2967
+ *
2968
+ * `options.idempotencyKey` is sent as the `Idempotency-Key` header. The answer carries a secret,
2969
+ * so the platform never replays it: the same key sent again is refused `409`
2970
+ * `request.already_executed` and rotates nothing more, and the secret is then read with
2971
+ * `getWebhookSecret`. A malformed key is a `RequestArgumentError` before anything is sent.
2972
+ */
2973
+ async rotateWebhookSecret(orgId, options = {}) {
2974
+ const endpoint = `organizations/${idPathSegment(orgId, "rotateWebhookSecret", ORG_ID_ARGUMENT)}/webhook-secret/rotate`;
2975
+ const idempotencyKey = idempotencyKeyOf(options, "rotate the secret");
2976
+ const res = await this.requestProductAnswer("POST", endpoint, undefined, { idempotencyKey });
2977
+ return this.readObjectAnswer("POST", endpoint, res);
2978
+ }
2942
2979
  /** The active org's subscription state — `GET /v1/billing/subscription`. */
2943
2980
  async getSubscription() {
2944
2981
  return this.requestProduct("GET", "billing/subscription");
@@ -3669,6 +3706,69 @@ export class PipelexApiClient {
3669
3706
  }
3670
3707
  return { ...rest, nextCursor };
3671
3708
  }
3709
+ /**
3710
+ * A run's caller-webhook deliveries — `GET /v1/runs/{id}/webhook-deliveries`: one per URL the
3711
+ * run registered in `callback_urls`, in their order, once the run has ended, each with its
3712
+ * state and every attempt, its URL redacted. A run that registered none, or has not ended, lists
3713
+ * none. Any member of the organization reads them, by web session or API key.
3714
+ *
3715
+ * Throws `RequestArgumentError` for a run id that is empty, `.` or `..`, or holds a lone
3716
+ * surrogate, before any request. Throws `ApiResponseError`: `404` `run.not_found` for a run that
3717
+ * is not the organization's, exactly as for one that does not exist. An answer whose
3718
+ * `deliveries` is not an array is an answer the SDK cannot read, thrown as an `ApiResponseError`
3719
+ * too.
3720
+ */
3721
+ async listRunWebhookDeliveries(runId) {
3722
+ const segment = idPathSegment(runId, "listRunWebhookDeliveries", RUN_ID_ARGUMENT);
3723
+ const endpoint = `runs/${segment}/webhook-deliveries`;
3724
+ const res = await this.requestProductAnswer("GET", endpoint, undefined);
3725
+ const list = this.readObjectAnswer("GET", endpoint, res);
3726
+ if (!Array.isArray(list.deliveries)) {
3727
+ throw this.unreadableAnswer("GET", `/${API_PREFIX}/${endpoint}`, res, "a run's deliveries whose `deliveries` is not an array");
3728
+ }
3729
+ return list;
3730
+ }
3731
+ /**
3732
+ * One delivery of a run's webhooks, with its event as sent —
3733
+ * `GET /v1/runs/{id}/webhook-deliveries/{delivery_id}`: the body of its latest attempt, which a
3734
+ * receiver's log can be matched against byte for byte once written as compact JSON.
3735
+ * `deliveryId` is the delivery's `delivery_id`, the receiver's `webhook-id`.
3736
+ *
3737
+ * Throws `RequestArgumentError` for an id that is empty, `.` or `..`, or holds a lone surrogate,
3738
+ * before any request. Throws `ApiResponseError`: `404` `run.not_found` for a run the caller
3739
+ * cannot read, and `422` `request.invalid` naming `delivery_id` for a delivery the run does not
3740
+ * have.
3741
+ */
3742
+ async getRunWebhookDelivery(runId, deliveryId) {
3743
+ const call = "getRunWebhookDelivery";
3744
+ const run = idPathSegment(runId, call, RUN_ID_ARGUMENT);
3745
+ const delivery = idPathSegment(deliveryId, call, DELIVERY_ID_ARGUMENT);
3746
+ return this.requestProductObject("GET", `runs/${run}/webhook-deliveries/${delivery}`);
3747
+ }
3748
+ /**
3749
+ * Send a settled delivery again — `POST /v1/runs/{id}/webhook-deliveries/{delivery_id}/replay`:
3750
+ * one new attempt under the same `webhook-id`, with the same `data`, the next `attempt`, a fresh
3751
+ * timestamp, and the signatures of the secrets live when it is sent. A replay the receiver
3752
+ * accepts marks the delivery `delivered`; one it does not leaves its state as it was, and the
3753
+ * attempt is recorded either way. A replay never returns a delivery to `pending`. Anyone who
3754
+ * can read the run asks for one, an API key included, under the organization's replay rate.
3755
+ *
3756
+ * Answers the attempt number the replay is sent under; a replay asked again before it is sent is
3757
+ * that same attempt. Throws `RequestArgumentError` for an id that is empty, `.` or `..`, or holds
3758
+ * a lone surrogate, before any request. Throws `ApiResponseError`: `404` `run.not_found` for a
3759
+ * run the caller cannot read; `422` `request.invalid` naming `delivery_id` for a delivery the run
3760
+ * does not have; `409` `pipelex.org.not_manageable` with the reason `webhook_delivery_pending`
3761
+ * for a delivery still on its retry schedule, or `webhook_delivery_attempts_exhausted` for one
3762
+ * holding the most attempts a delivery holds; `429` `account.rate_limited`, with its
3763
+ * `retry_after`, past the organization's replay rate; and a `500` or `503` when the replay could
3764
+ * not be queued, which then counts nothing against the rate.
3765
+ */
3766
+ async replayRunWebhookDelivery(runId, deliveryId) {
3767
+ const call = "replayRunWebhookDelivery";
3768
+ const run = idPathSegment(runId, call, RUN_ID_ARGUMENT);
3769
+ const delivery = idPathSegment(deliveryId, call, DELIVERY_ID_ARGUMENT);
3770
+ return this.requestProductObject("POST", `runs/${run}/webhook-deliveries/${delivery}/replay`);
3771
+ }
3672
3772
  /** Patch a run's status (admin/manual) — `PUT /v1/runs/{id}`. */
3673
3773
  async updateRun(runId, input) {
3674
3774
  const segment = idPathSegment(runId, "updateRun", RUN_ID_ARGUMENT);
@@ -3685,6 +3785,8 @@ export class PipelexApiClient {
3685
3785
  const extensions = buildExtensions(options.extra);
3686
3786
  const api = buildApiRunExtensions(options);
3687
3787
  const hosted = buildHostedRunExtensions(options);
3788
+ // Not a run source, so outside `api`, whose size the precondition below reads.
3789
+ const webhooks = buildCallbackUrls(call, options);
3688
3790
  const forms = protocolTreeForms(call, options);
3689
3791
  const treeForms = treeFormsGiven(forms);
3690
3792
  if (!options.pipe_code &&
@@ -3696,13 +3798,13 @@ export class PipelexApiClient {
3696
3798
  }
3697
3799
  assertMethodRefPairsWithNothing(options, treeForms);
3698
3800
  assertLinkageMethodIdIsBare(options, treeForms);
3699
- return { forms, extensions, api, hosted };
3801
+ return { forms, extensions, api, hosted, webhooks };
3700
3802
  }
3701
3803
  /**
3702
3804
  * Blocking `POST /v1/execute` adapted onto `RunResults` — the bare-runner
3703
3805
  * path. Forwards every protocol field PLUS every extension surface: the
3704
- * runner-resolved `method_ref`, the hosted `method_id`, and the generic
3705
- * `extra` passthrough. An extension-only call (`{ extra }` with no
3806
+ * runner-resolved `method_ref`, the run's `callback_urls`, the hosted
3807
+ * `method_id`, and the generic `extra` passthrough. An extension-only call (`{ extra }` with no
3706
3808
  * pipe_code/bundle) or a vendor selector riding `extra` must survive this
3707
3809
  * path, not just the durable one — a `method_ref` run must run the same
3708
3810
  * fetched package here, and a hosted `method_id` must reach the server too,
@@ -3724,6 +3826,8 @@ export class PipelexApiClient {
3724
3826
  tree: options.tree ?? undefined,
3725
3827
  files: options.files ?? undefined,
3726
3828
  method_ref: options.method_ref ?? undefined,
3829
+ // A runner calls the run's webhooks once the blocking run has ended, as the platform does.
3830
+ callback_urls: options.callback_urls ?? undefined,
3727
3831
  method_id: options.method_id ?? undefined,
3728
3832
  extra: options.extra ?? undefined,
3729
3833
  }, signal, sending);
@@ -3798,12 +3902,12 @@ const PROTOCOL_REQUEST_KEYS = [
3798
3902
  "dynamic_output_concept_ref",
3799
3903
  "files",
3800
3904
  ];
3801
- // The PIPELEX API's own request field — the layer-2 extension the runner reads
3802
- // itself (`method_ref`, a run source in its own right). Reserved on `extra` for
3803
- // the same reason the protocol fields are: `extra` merges last, so a smuggled
3804
- // copy would overwrite the validated named option and bypass the
3805
- // selector-exclusivity check.
3806
- const PIPELEX_API_REQUEST_KEYS = ["method_ref"];
3905
+ // The PIPELEX API's own request fields — the layer-2 extensions the runner reads
3906
+ // itself: `method_ref`, a run source in its own right, and `callback_urls`, the
3907
+ // webhooks to call once the run has ended. Reserved on `extra` for the same
3908
+ // reason the protocol fields are: `extra` merges last, so a smuggled copy would
3909
+ // overwrite the validated named option and bypass its checks.
3910
+ const PIPELEX_API_REQUEST_KEYS = ["method_ref", "callback_urls"];
3807
3911
  // The HOSTED API's own request fields — the layer-3 extensions this client
3808
3912
  // names itself. Reserved on `extra` for the same reason the protocol fields
3809
3913
  // are: `extra` merges last, so a smuggled copy would overwrite the validated
@@ -3906,6 +4010,7 @@ function runRequestBody(call, options, files, parts) {
3906
4010
  : options.dynamic_output_concept_ref,
3907
4011
  files: files ?? undefined,
3908
4012
  ...parts.api,
4013
+ ...parts.webhooks,
3909
4014
  ...parts.hosted,
3910
4015
  ...parts.extensions,
3911
4016
  };
@@ -3986,6 +4091,29 @@ function buildApiRunExtensions(options) {
3986
4091
  const methodRef = nonEmptyString(options.method_ref);
3987
4092
  return methodRef === undefined ? {} : { method_ref: methodRef };
3988
4093
  }
4094
+ /**
4095
+ * Copy a run's `callback_urls` onto the wire body, `call` naming the caller's method in a refusal.
4096
+ * An absent, `null` or empty list yields an empty object, so the key is not sent, as the platform
4097
+ * treats an empty list as absent. A value that is not an array, or an array holding anything but
4098
+ * strings, is refused before anything is sent; the URLs themselves are the server's to judge,
4099
+ * since the hosted API and a self-hosted runner each hold them to their own rules. The Python SDK
4100
+ * reads the argument the same way.
4101
+ */
4102
+ function buildCallbackUrls(call, options) {
4103
+ const urls = options.callback_urls;
4104
+ if (urls == null)
4105
+ return {};
4106
+ if (!Array.isArray(urls)) {
4107
+ throw new RequestArgumentError(`${call}() takes callback_urls as an array of URLs; got ${typeof urls}.`, { problem: argumentInvalidProblem("callback_urls", "not_a_list") });
4108
+ }
4109
+ const copy = [...urls];
4110
+ if (copy.some((url) => typeof url !== "string")) {
4111
+ throw new RequestArgumentError(`${call}() takes each of callback_urls as a string URL.`, {
4112
+ problem: argumentInvalidProblem("callback_urls", "not_a_string"),
4113
+ });
4114
+ }
4115
+ return copy.length === 0 ? {} : { callback_urls: copy };
4116
+ }
3989
4117
  /**
3990
4118
  * Copy the hosted API's own run arguments onto the wire body. Named options,
3991
4119
  * not `extra` entries — see `PipelexHostedRunExtensions`.