@pipelex/sdk 0.60.0 → 0.62.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/dist/client.js CHANGED
@@ -1935,7 +1935,84 @@ export class PipelexApiClient {
1935
1935
  yield* followCursor("listMethods", (cursor) => this.listMethods({ ...query, cursor }));
1936
1936
  }
1937
1937
  /**
1938
- * Read one method with its draft's files — `GET /v1/methods/{id}`: its identity, its draft
1938
+ * One page of the template catalog — `GET /v1/method-templates`: the public methods of the
1939
+ * organization the deployment names as its templates organization, the same for every caller.
1940
+ *
1941
+ * The templates the ordering lists lead, in its order, and every other template follows by name.
1942
+ * `ordering` names the ordering to serve, such as `default` or an arm of a test; an absent,
1943
+ * unknown or malformed name serves `default`, and the page's `ordering` names the one served,
1944
+ * `null` when there is none. `category` keeps one category's templates, by slug, in the same
1945
+ * order; an unknown slug keeps none. Whatever the filter, the page's `categories` names, with its
1946
+ * label, every category at least one template shows, in the catalog's order, so no tab drawn
1947
+ * from it is ever empty.
1948
+ *
1949
+ * Each `MethodTemplate` carries the method's id and name, its latest published version's number,
1950
+ * description and categories, its organization's display name and each thumbnail with its size
1951
+ * and a link to its bytes valid for about fifteen minutes. A template reads, runs and copies by
1952
+ * its `method_id`, as another organization's public method does (`getMethod`, `copyMethod`).
1953
+ *
1954
+ * Pass the page's `nextCursor` back as `cursor` with the same `ordering` and `category`, which the
1955
+ * cursor does not hold; `null` is the last page. `iterateMethodTemplates` follows it for you.
1956
+ *
1957
+ * Throws `ApiResponseError`: `422` `request.invalid` for a `limit` outside 1 to 200 or a cursor of
1958
+ * another listing, `422` `request.malformed` for a cursor that does not decode, `500`
1959
+ * `runtime.misconfigured` when the templates organization the deployment names has no record or
1960
+ * a link cannot be signed, and `503` `runtime.unavailable`, safe to retry. A page whose `items`
1961
+ * or `categories` is not an array, whose `next_cursor` or `ordering` is neither a string nor
1962
+ * `null`, a template lacking a member a card is drawn from (a string `method_id`, `name` and
1963
+ * `organization_name`, a `description` that is a string or `null`, a positive integer
1964
+ * `latest_version`, an array of string `categories`, and `thumbnails` each with a string `path`,
1965
+ * `media_type`, `sha256`, `url` and `expires_at` and a positive integer `width` and `height`), and a
1966
+ * category without a string `slug` and `label` are an answer the SDK cannot read, thrown as an
1967
+ * `ApiResponseError` too, its links redacted.
1968
+ */
1969
+ async listMethodTemplates(query = {}) {
1970
+ const search = productQuery({
1971
+ ordering: query.ordering,
1972
+ category: query.category,
1973
+ limit: query.limit,
1974
+ cursor: query.cursor,
1975
+ });
1976
+ const endpoint = `method-templates${search}`;
1977
+ const res = withLinks(await this.requestProductAnswer("GET", endpoint, undefined));
1978
+ const page = this.readObjectAnswer("GET", endpoint, res);
1979
+ const { items, next_cursor: nextCursor, ordering, categories } = page;
1980
+ let fault = null;
1981
+ if (!Array.isArray(items))
1982
+ fault = "`items` is not an array";
1983
+ else if (nextCursor !== null && typeof nextCursor !== "string") {
1984
+ fault = "`next_cursor` is neither a string nor null";
1985
+ }
1986
+ else if (ordering !== null && typeof ordering !== "string") {
1987
+ fault = "`ordering` is neither a string nor null";
1988
+ }
1989
+ else if (!Array.isArray(categories))
1990
+ fault = "`categories` is not an array";
1991
+ else
1992
+ fault = methodTemplatesFault(items) ?? methodCategoriesFault(categories);
1993
+ if (fault !== null) {
1994
+ throw this.unreadableAnswer("GET", `/${API_PREFIX}/${endpoint}`, res, `a page of templates whose ${fault}`);
1995
+ }
1996
+ return {
1997
+ items: items,
1998
+ nextCursor: nextCursor,
1999
+ ordering: ordering,
2000
+ categories: categories,
2001
+ };
2002
+ }
2003
+ /**
2004
+ * Every template of the catalog, in the order served — follows `listMethodTemplates`'s cursor
2005
+ * for you, with the same `ordering` and `category` on every page. It stops on a `null` cursor or
2006
+ * on a cursor the server failed to advance, and throws `PagingNotTerminatingError` past a page
2007
+ * ceiling no real catalog reaches. The ordering served and the categories are on each page, so
2008
+ * read one page with `listMethodTemplates` when you need them.
2009
+ */
2010
+ async *iterateMethodTemplates(query = {}) {
2011
+ yield* followCursor("listMethodTemplates", (cursor) => this.listMethodTemplates({ ...query, cursor }));
2012
+ }
2013
+ /**
2014
+ * Read one method with its files — `GET /v1/methods/{id}`. One of the caller's organization's
2015
+ * methods answers a `MethodDetail`: its identity, its draft
1939
2016
  * (`files`, `input_data`) with the draft's token (`updated_at`) and digest (`draft_digest`),
1940
2017
  * and its latest published version's summary (`latest_version`, `latest_published`), whatever
1941
2018
  * the publish state. A method never published reads with both `null`, never as an error.
@@ -1943,6 +2020,11 @@ export class PipelexApiClient {
1943
2020
  * link to its bytes valid for about fifteen minutes, and is empty for an empty draft;
1944
2021
  * `writeMethodTree` brings the tree to disk.
1945
2022
  *
2023
+ * **Another organization's public method reads too, in a narrower shape**, a
2024
+ * `PublicMethodDetail`: its latest published version, that version's description, categories
2025
+ * and files, and nothing of its draft or of the owner's people, so no `updated_at` to write or
2026
+ * publish with. Narrow the answer with `"updated_at" in method` before reading the draft.
2027
+ *
1946
2028
  * Takes a bare catalog id: the method routes address the method itself, never a version of
1947
2029
  * it. A caller holding `mt_…@3` strips the suffix with `parseMethodSelector` and reads that
1948
2030
  * version with `getMethodVersion`. Every method route throws `RequestArgumentError` for a
@@ -1950,16 +2032,26 @@ export class PipelexApiClient {
1950
2032
  * and for an id that is empty, `.` or `..`, which the URL would resolve into another route's
1951
2033
  * path.
1952
2034
  *
1953
- * Throws `ApiResponseError`: `404` `pipelex.catalog.not_found` for an unknown or foreign-org
1954
- * method, `409` `pipelex.catalog.being_deleted` while its erasure runs, and a `500`
1955
- * `runtime.unexpected` for a method whose draft names a tree the platform holds no record of. An
1956
- * answer whose `files` is not a listing is an answer the SDK cannot read, thrown as an
1957
- * `ApiResponseError` too.
2035
+ * Throws `ApiResponseError`: `404` `pipelex.catalog.not_found` for an unknown method, for
2036
+ * another organization's internal one, and for a public one made internal again or being
2037
+ * erased; `409` `pipelex.catalog.being_deleted` while the erasure of the caller's own method
2038
+ * runs; and a `500` `runtime.unexpected` for a method whose tree the platform holds no record
2039
+ * of. An answer whose `files` is not a listing is an answer the SDK cannot read, thrown as an
2040
+ * `ApiResponseError` too, its links redacted, and so is one that lacks `updated_at` while it
2041
+ * carries a member only the owner's read has (`draft_digest`, `created_by_user_id`,
2042
+ * `input_data`, `pipe_output`, `copied_from`, `deletion_state`), which would otherwise pass for
2043
+ * another organization's public method, and a public read without an integer `latest_version`
2044
+ * and a `latest_published` object.
1958
2045
  */
1959
2046
  async getMethod(methodId) {
1960
2047
  const endpoint = methodPath(methodId, "getMethod");
1961
- const res = await this.requestProductAnswer("GET", endpoint, undefined);
1962
- return this.readSavedTree("GET", endpoint, withLinks(res), "a stored method");
2048
+ const res = withLinks(await this.requestProductAnswer("GET", endpoint, undefined));
2049
+ const method = this.readSavedTree("GET", endpoint, res, "a stored method");
2050
+ const fault = methodReadFault(method);
2051
+ if (fault !== null) {
2052
+ throw this.unreadableAnswer("GET", `/${API_PREFIX}/${endpoint}`, res, `a stored method ${fault}`);
2053
+ }
2054
+ return method;
1963
2055
  }
1964
2056
  /**
1965
2057
  * Create a method — `POST /v1/methods`. The new method holds its tree as its draft and has no
@@ -2399,9 +2491,12 @@ export class PipelexApiClient {
2399
2491
  *
2400
2492
  * The answer is a `MethodPublishResult` discriminated on `outcome` — branch on it:
2401
2493
  * `published` with the new `version`; `unchanged` with the existing latest `version`;
2402
- * `refused` with a `reason` (`invalid`, or `not_runnable` for a draft that validates with
2403
- * pending signatures), a `message` and the runner's `validation` verdict, whose problem items
2404
- * are read member by member as `validate` reads them. Every arm carries the `method`. A publish
2494
+ * `refused` with a `reason`, a `message` and the runner's `validation` verdict, whose problem
2495
+ * items are read member by member as `validate` reads them. The reason is `invalid`, or
2496
+ * `not_runnable` for a draft that validates with pending signatures, and for a public method
2497
+ * also `thumbnail_missing`, `thumbnail_invalid` or `storage_reference` (see
2498
+ * `MethodPublishRefusalReason`); a reason this SDK does not know is passed through as its raw
2499
+ * string, still a verdict, so show its `message`. Every arm carries the `method`. A publish
2405
2500
  * moves no token.
2406
2501
  *
2407
2502
  * Takes a bare catalog id, as `getMethod` does. Throws `RequestArgumentError` when
@@ -2416,11 +2511,14 @@ export class PipelexApiClient {
2416
2511
  * cannot be reached, answers unusably or does not finish within the platform's deadline is a
2417
2512
  * `502` or a `503` with nothing written, so a retry is safe; a runner that refuses the request
2418
2513
  * itself is relayed under its own status. A retry of a publish that landed while its answer was
2419
- * lost, as on a client timeout, answers `unchanged` with the version it wrote.
2514
+ * lost, as on a client timeout, answers `unchanged` with the version it wrote. A method made
2515
+ * public while its publish ran is a `503` with nothing written, safe to retry, which a retry
2516
+ * judges as a public method's publish.
2420
2517
  *
2421
2518
  * An answer whose `outcome` is not one of the outcomes above, that carries no `method` object,
2422
- * or a refusal whose `validation` is not a verdict object with a boolean `is_valid`, is an answer
2423
- * the SDK cannot read, thrown as an `ApiResponseError` too.
2519
+ * or a refusal whose `reason` or `message` is not a string or whose `validation` is not a verdict
2520
+ * object with a boolean `is_valid`, is an answer the SDK cannot read, thrown as an
2521
+ * `ApiResponseError` too.
2424
2522
  */
2425
2523
  async publishMethod(methodId, input) {
2426
2524
  const token = input
@@ -2445,8 +2543,16 @@ export class PipelexApiClient {
2445
2543
  // The refused arm carries the runner's verdict, whose problem items are read member by
2446
2544
  // member as `validate` reads them, so one malformed item never costs the caller the others.
2447
2545
  const validation = answer.validation;
2448
- if (!isPlainObject(validation) || typeof validation.is_valid !== "boolean") {
2449
- throw this.unreadableAnswer("POST", `/${API_PREFIX}/${endpoint}`, res, "a publish result whose `validation` is not an object");
2546
+ let fault = null;
2547
+ if (typeof answer.reason !== "string")
2548
+ fault = "`reason` is not a string";
2549
+ else if (typeof answer.message !== "string")
2550
+ fault = "`message` is not a string";
2551
+ else if (!isPlainObject(validation) || typeof validation.is_valid !== "boolean") {
2552
+ fault = "`validation` is not an object";
2553
+ }
2554
+ if (fault !== null) {
2555
+ throw this.unreadableAnswer("POST", `/${API_PREFIX}/${endpoint}`, res, `a publish result whose ${fault}`);
2450
2556
  }
2451
2557
  return {
2452
2558
  ...answer,
@@ -2461,11 +2567,13 @@ export class PipelexApiClient {
2461
2567
  *
2462
2568
  * Pass the page's `nextCursor` back as `cursor` to continue; `null` is the last page. A page
2463
2569
  * may be short while `nextCursor` is set, because the platform reads versions whole. A method
2464
- * never published answers an empty page.
2570
+ * never published answers an empty page. Another organization's public method lists its
2571
+ * versions too, each without its `published_by`.
2465
2572
  *
2466
2573
  * Takes a bare catalog id, as `getMethod` does. Throws `ApiResponseError`: `404`
2467
- * `pipelex.catalog.not_found` for an unknown method, `409` `pipelex.catalog.being_deleted`, and
2468
- * `422` `request.invalid` for a cursor the listing did not issue for this method or a `limit`
2574
+ * `pipelex.catalog.not_found` for an unknown method and for another organization's internal
2575
+ * one, `409` `pipelex.catalog.being_deleted` while the erasure of the caller's own method runs,
2576
+ * and `422` `request.invalid` for a cursor the listing did not issue for this method or a `limit`
2469
2577
  * outside 1 to 100. A page whose `items` is not an array or whose `next_cursor` is neither a
2470
2578
  * string nor `null` is an answer the SDK cannot read, thrown as an `ApiResponseError` too.
2471
2579
  */
@@ -2477,12 +2585,15 @@ export class PipelexApiClient {
2477
2585
  /**
2478
2586
  * One published version of a method, with its files — `GET /v1/methods/{id}/versions/{n}`.
2479
2587
  * `files` lists the version's tree as `getMethod` lists the draft's, each file with a link to
2480
- * its bytes valid for about fifteen minutes, which `writeMethodTree` writes to disk.
2588
+ * its bytes valid for about fifteen minutes, which `writeMethodTree` writes to disk. Another
2589
+ * organization's public method reads its versions too, without their `published_by`, the files
2590
+ * linked in the organization that holds it.
2481
2591
  *
2482
2592
  * Takes a bare catalog id, as `getMethod` does, and the version number. Throws
2483
2593
  * `RequestArgumentError` for a `version` that is not a positive integer, before any request.
2484
2594
  * Throws `ApiResponseError`: `404` `pipelex.catalog.not_found` for a version the method never
2485
- * published and for an unknown method, `409` `pipelex.catalog.being_deleted`, and `500`
2595
+ * published, for an unknown method and for another organization's internal one, `409`
2596
+ * `pipelex.catalog.being_deleted` while the erasure of the caller's own method runs, and `500`
2486
2597
  * `runtime.unexpected` for a version stored before methods were trees and never rewritten as
2487
2598
  * one, which names no tree to serve.
2488
2599
  */
@@ -2506,10 +2617,16 @@ export class PipelexApiClient {
2506
2617
  * original stays as it was. The copy takes the draft, never the latest published version; with
2507
2618
  * `expected_draft_updated_at`, it takes exactly the draft the caller last saw.
2508
2619
  *
2620
+ * **Another organization's public method copies too**, a template of the catalog among them,
2621
+ * from its latest published version rather than its draft: that version's tree, description and
2622
+ * categories, and the method's examples, never the owner's form inputs, with `copied_from`
2623
+ * naming the `version`. Leave `expected_draft_updated_at` out then: sent with such a method, it
2624
+ * is refused with a `422` `request.invalid` naming it.
2625
+ *
2509
2626
  * The answer is a `MethodCopyResult` discriminated on `outcome` — branch on it: `copied` with
2510
2627
  * the copy as `method`, `examples_copied` and `stored_files_copied`; `refused` with the
2511
2628
  * `reason` `bundle_names_other_storage`, a `message` and every `references` entry (`{ path, uri
2512
- * }`) a `.mthds` file of the draft holds that the destination cannot read and, in a duplicate,
2629
+ * }`) a `.mthds` file of the tree holds that the destination cannot read and, in a duplicate,
2513
2630
  * that names a file under the original's run files, which erasing the original would remove,
2514
2631
  * nothing written.
2515
2632
  *
@@ -2528,7 +2645,8 @@ export class PipelexApiClient {
2528
2645
  * that is not from a web session, or for a role that may not change methods, in the method's
2529
2646
  * organization or in the destination; `404` `pipelex.org.not_found` for a destination the
2530
2647
  * caller does not belong to, still being set up or unknown, alike; `404`
2531
- * `pipelex.catalog.not_found` for an unknown method; `409` `pipelex.catalog.being_deleted`;
2648
+ * `pipelex.catalog.not_found` for an unknown method and for another organization's internal
2649
+ * one; `409` `pipelex.catalog.being_deleted` while the erasure of the caller's own method runs;
2532
2650
  * `409` `pipelex.catalog.update_conflict` for a draft that moved since
2533
2651
  * `expected_draft_updated_at`; a `409` whose `domain` is `runtime` and `retry` `no`, for a
2534
2652
  * draft whose tree the method's organization holds no record of or no longer holds every file
@@ -2881,34 +2999,44 @@ export class PipelexApiClient {
2881
2999
  }
2882
3000
  /**
2883
3001
  * The branding of the caller's active organization — `GET /v1/branding`: its name, its light
2884
- * and dark colour sets and its two logos. Any role reads it, `runner` included, by web session
2885
- * or API key, so an app scaffolded from the method-app template reads its organization's
2886
- * branding with its own key. A personal workspace, or a team that set nothing, answers every
2887
- * colour and logo `null`. A logo's `uri` is a `pipelex-storage://` reference, which
2888
- * `resolveStorageUrl` turns into a short-lived link to its bytes.
2889
- *
2890
- * Throws `ApiResponseError`: `400` when the caller has no active organization, `404` when its
2891
- * row is missing, and an answer that is not a JSON object, an empty one included.
3002
+ * and dark colour sets, its two logos, its app style and typeface, and whether it holds the
3003
+ * white-label entitlement. Any role reads it, `runner` included, by web session or API key, so
3004
+ * an app scaffolded from the method-app template reads its organization's branding with its own
3005
+ * key. A personal workspace, or a team that set nothing, answers every colour and logo `null`
3006
+ * and no style or typeface. A logo's `uri` is a `pipelex-storage://` reference, which
3007
+ * `resolveStorageUrl` turns into a short-lived link to its bytes. An answer without `style`,
3008
+ * `typeface` or `white_label`, as a platform that predates them sends, reads as `null`, `null`
3009
+ * and `false`.
3010
+ *
3011
+ * Throws `ApiResponseError`: `403` `pipelex.org.required` when the caller has no active
3012
+ * organization, `404` when its row is missing, and an answer that is not a JSON object, an
3013
+ * empty one included.
2892
3014
  */
2893
3015
  async getBranding() {
2894
- return this.requestProductObject("GET", "branding");
3016
+ const answer = await this.requestProductObject("GET", "branding");
3017
+ return withCheckedBrandingChoices(answer);
2895
3018
  }
2896
3019
  /**
2897
- * Set or clear an organization's branding — `PATCH /v1/organizations/{org_id}/branding`, an
2898
- * admin's write to the active organization. Every key of `input` is optional: a value sets it,
2899
- * `null` clears it, and a key left out keeps its value (`OrganizationBrandingUpdate`), so `{}`
2900
- * writes nothing. A new logo is the `pipelex-storage://` URI of a file the organization uploaded
2901
- * on its own, which the platform reads and measures, then copies under `branding/`: the answer's
2902
- * `uri` is that copy, and sending it back unchanged keeps the logo. The logo a write replaces or
2903
- * clears is erased. Answers the branding as the write left it.
3020
+ * Set or clear an organization's branding — `PATCH /v1/organizations/{org_id}/branding`, a
3021
+ * write to the active organization by the `admin` or the `member` role, never a `runner`. Every
3022
+ * key of `input` is optional: a value sets it, `null` clears it, and a key left out keeps its
3023
+ * value (`OrganizationBrandingUpdate`), so `{}` writes nothing. A new logo is the
3024
+ * `pipelex-storage://` URI of a file the organization uploaded on its own, which the platform
3025
+ * reads and measures, then copies under `branding/`: the answer's `uri` is that copy, and sending
3026
+ * it back unchanged keeps the logo. The logo a write replaces or clears is erased. A style or a
3027
+ * typeface is an id of the shape `^[a-z0-9-]{1,32}$`, stored whether or not it names one that
3028
+ * exists. Answers the branding as the write left it, read as `getBranding` reads it.
2904
3029
  *
2905
3030
  * Throws `RequestArgumentError` for an organization id that is empty, `.` or `..`, before any
2906
3031
  * request. Throws `ApiResponseError`, refusing every change of the request when it refuses one:
2907
3032
  * `403` `pipelex.org.mismatch` when `orgId` is not the active organization, `403`
2908
- * `auth.forbidden` for a caller who is not its admin, `404` `pipelex.org.not_found` for a missing
2909
- * organization, `409` `pipelex.org.not_manageable` for a personal workspace, which has no
2910
- * branding, and `422` `request.invalid` for a colour that is not `#rrggbb` or a key the body does
2911
- * not know. A logo's refusal names its slot in its problem's `errors`, each item's
3033
+ * `auth.forbidden` for a runner or a caller with no recognised role, `404`
3034
+ * `pipelex.org.not_found` for a missing organization, `409` `pipelex.org.not_manageable` for a
3035
+ * personal workspace, which has no branding, and `422` `request.invalid` for a colour that is
3036
+ * not `#rrggbb`, a style or a typeface of another shape, or a key the body does not know, which
3037
+ * names the field in `location.field`: the problem's own when it refuses one field alone, each
3038
+ * item's of its `errors` when it refuses several. `white_label` is such a key, since only
3039
+ * Pipelex staff grant it. A logo's refusal names its slot in its problem's `errors`, each item's
2912
3040
  * `location.field` being `square_logo` or `wide_logo` and its `detail` the sentence to show under
2913
3041
  * that slot: `422` `pipelex.storage.uri_invalid` for a URI that is not a storage URI, `403`
2914
3042
  * `pipelex.org.mismatch` for another organization's file, and `422` `request.invalid` with an
@@ -2918,7 +3046,8 @@ export class PipelexApiClient {
2918
3046
  */
2919
3047
  async updateOrganizationBranding(orgId, input) {
2920
3048
  const segment = idPathSegment(orgId, "updateOrganizationBranding", ORG_ID_ARGUMENT);
2921
- return this.requestProductObject("PATCH", `organizations/${segment}/branding`, input);
3049
+ const answer = await this.requestProductObject("PATCH", `organizations/${segment}/branding`, input);
3050
+ return withCheckedBrandingChoices(answer);
2922
3051
  }
2923
3052
  /**
2924
3053
  * The organization's webhook signing secret — `GET /v1/organizations/{org_id}/webhook-secret`:
@@ -4387,6 +4516,106 @@ function savedFilesFault(files) {
4387
4516
  }
4388
4517
  return null;
4389
4518
  }
4519
+ /** The members only the owner's read of a method carries, never another organization's. */
4520
+ const OWNER_ONLY_METHOD_MEMBERS = [
4521
+ "draft_digest",
4522
+ "created_by_user_id",
4523
+ "input_data",
4524
+ "pipe_output",
4525
+ "copied_from",
4526
+ "deletion_state",
4527
+ ];
4528
+ /**
4529
+ * Why a method read is not one the SDK reads, or `null` when it is. The owner's read carries the
4530
+ * draft's token, `updated_at`, which is what tells it from another organization's public method;
4531
+ * so an answer without the token that still carries a member only the owner's read has is a
4532
+ * damaged owner read rather than a public one, and a public read must carry the latest published
4533
+ * version it is about.
4534
+ */
4535
+ function methodReadFault(method) {
4536
+ if ("updated_at" in method)
4537
+ return null;
4538
+ const ownerMember = OWNER_ONLY_METHOD_MEMBERS.find((member) => member in method);
4539
+ if (ownerMember !== undefined) {
4540
+ return `without \`updated_at\` that carries the owner's \`${ownerMember}\``;
4541
+ }
4542
+ if (!isPositiveInteger(method.latest_version)) {
4543
+ return "read as another organization's public method whose `latest_version` is not a positive integer";
4544
+ }
4545
+ if (!isPlainObject(method.latest_published)) {
4546
+ return "read as another organization's public method whose `latest_published` is not an object";
4547
+ }
4548
+ return null;
4549
+ }
4550
+ function isPositiveInteger(value) {
4551
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
4552
+ }
4553
+ function isStringArray(value) {
4554
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
4555
+ }
4556
+ /**
4557
+ * Why the catalog's templates are not ones the SDK reads, or `null` when they are: each an object
4558
+ * with what a card is drawn from, a string `method_id`, `name` and `organization_name`, a
4559
+ * `description` that is a string or `null`, a positive integer `latest_version`, an array of string `categories`, and `thumbnails`, each with
4560
+ * a string `path`, `media_type`, `sha256`, `url` and `expires_at` and a positive integer `width`
4561
+ * and `height`.
4562
+ */
4563
+ function methodTemplatesFault(items) {
4564
+ for (let index = 0; index < items.length; index += 1) {
4565
+ const item = items[index];
4566
+ if (!isPlainObject(item))
4567
+ return `\`items\` holds an entry that is not an object, at [${index}]`;
4568
+ for (const member of ["method_id", "name", "organization_name"]) {
4569
+ if (typeof item[member] !== "string") {
4570
+ return `\`items\` holds a template whose \`${member}\` is not a string, at [${index}]`;
4571
+ }
4572
+ }
4573
+ if (item.description !== null &&
4574
+ item.description !== undefined &&
4575
+ typeof item.description !== "string") {
4576
+ return `\`items\` holds a template whose \`description\` is neither a string nor null, at [${index}]`;
4577
+ }
4578
+ if (!isPositiveInteger(item.latest_version)) {
4579
+ return `\`items\` holds a template whose \`latest_version\` is not a positive integer, at [${index}]`;
4580
+ }
4581
+ if (!isStringArray(item.categories)) {
4582
+ return `\`items\` holds a template whose \`categories\` is not an array of strings, at [${index}]`;
4583
+ }
4584
+ const thumbnails = item.thumbnails;
4585
+ if (!Array.isArray(thumbnails)) {
4586
+ return `\`items\` holds a template whose \`thumbnails\` is not an array, at [${index}]`;
4587
+ }
4588
+ for (let position = 0; position < thumbnails.length; position += 1) {
4589
+ const thumbnail = thumbnails[position];
4590
+ const where = `at [${index}].thumbnails[${position}]`;
4591
+ if (!isPlainObject(thumbnail))
4592
+ return `\`items\` holds a thumbnail that is not an object, ${where}`;
4593
+ for (const member of ["path", "media_type", "sha256", "url", "expires_at"]) {
4594
+ if (typeof thumbnail[member] !== "string") {
4595
+ return `\`items\` holds a thumbnail whose \`${member}\` is not a string, ${where}`;
4596
+ }
4597
+ }
4598
+ for (const member of ["width", "height"]) {
4599
+ if (!isPositiveInteger(thumbnail[member])) {
4600
+ return `\`items\` holds a thumbnail whose \`${member}\` is not a positive integer, ${where}`;
4601
+ }
4602
+ }
4603
+ }
4604
+ }
4605
+ return null;
4606
+ }
4607
+ /** Why the catalog's categories are not ones the SDK reads, or `null`: each a string `slug` and `label`. */
4608
+ function methodCategoriesFault(categories) {
4609
+ for (let index = 0; index < categories.length; index += 1) {
4610
+ const category = categories[index];
4611
+ if (!isPlainObject(category) ||
4612
+ typeof category.slug !== "string" ||
4613
+ typeof category.label !== "string") {
4614
+ return `\`categories\` holds an entry without a string \`slug\` and \`label\`, at [${index}]`;
4615
+ }
4616
+ }
4617
+ return null;
4618
+ }
4390
4619
  function extractNetworkErrorCode(err) {
4391
4620
  if (err instanceof DOMException && err.name === "TimeoutError") {
4392
4621
  return "ABORT_TIMEOUT";
@@ -4531,6 +4760,21 @@ function withCheckedUse(use) {
4531
4760
  method_version: Number.isInteger(version) || version === "draft" ? version : null,
4532
4761
  };
4533
4762
  }
4763
+ /**
4764
+ * A branding with its `style` and `typeface` kept only when each is a string, each read as `null`
4765
+ * otherwise, and its `white_label` read as `true` only when it is `true`: when the answer leaves a
4766
+ * member out, as a platform that predates it does, sends `null`, or sends any other type. So each
4767
+ * is what `OrganizationBranding` declares, never `undefined`, and anything but the entitlement
4768
+ * itself reads as no entitlement, which keeps Pipelex's mark. The rest is returned as it came.
4769
+ */
4770
+ function withCheckedBrandingChoices(answer) {
4771
+ return {
4772
+ ...answer,
4773
+ style: typeof answer.style === "string" ? answer.style : null,
4774
+ typeface: typeof answer.typeface === "string" ? answer.typeface : null,
4775
+ white_label: answer.white_label === true,
4776
+ };
4777
+ }
4534
4778
  /**
4535
4779
  * A status read with its `degradation` checked by `readRunDegradation`. Only when the read carries
4536
4780
  * the key, so a platform that predates it still leaves it absent; `degraded` is left as sent.