@bettercms-ai/sdk 1.20.0 → 1.21.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/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _bettercms_ai_types from '@bettercms-ai/types';
2
- import { ContentPageResult, Content, DeliveredLayout, ContentModelField, BetterCMSErrorCode, LayoutStructureDocument, LayoutDataDocument, PageLayoutOverrideDocument, ManagementLayoutCommand, AuthUser, AuthSession, SignInInput, SignUpInput, Perspective, DeliveryEntry, DeliveryList, DeliveryPage, DeliveryComponent, PageMetaJson, DeliveryEntrySeo, SiteSeoDefaults, Page, MediaAsset, Form, FormSubmission, Workspace, ApiKey, ApiKeyPermission, ApiKeyTokenType } from '@bettercms-ai/types';
2
+ import { ContentPageResult, Content, DeliveredLayout, ContentModelField, BetterCMSErrorCode, LayoutStructureDocument, LayoutDataDocument, PageLayoutOverrideDocument, ManagementLayoutCommand, AuthUser, AuthSession, SignInInput, SignUpInput, Perspective, DeliveryEntry, DeliveryList, DeliveryPage, DeliveryComponent, PageMetaJson, DeliveryEntrySeo, SiteSeoDefaults, Page, MediaAsset, FormField, Form, FormSubmission, Workspace, ApiKey, ApiKeyPermission, ApiKeyTokenType } from '@bettercms-ai/types';
3
3
  export { ApiError, ApiKey, ApiKeyPermission, ApiKeyTokenType, AssertEqual, AuthSession, AuthUser, BetterCMSErrorCode, BlockType, ButtonBlock, ButtonProps, ColumnsBlock, ColumnsProps, Content, ContentBlock, ContentEntry, ContentModel, ContentModelField, ContentPageResult, ContentResponse, DeepReadonly, DeliveryComponent, DeliveryEntry, DeliveryList, DeliveryPage, Form, FormBlock, FormField, FormProps, FormSubmission, HeadingBlock, HeadingProps, ImageBlock, ImageProps, MediaAsset, Page, PageMetaJson, PaginatedResult, Perspective, Redirect, SignInInput, SignUpInput, SiteSeoDefaults, SpacerBlock, SpacerProps, TextBlock, TextProps, VideoBlock, VideoProps, Workspace, getBlockType, isBlock } from '@bettercms-ai/types';
4
4
  export { ImageFit, ImageFormat, ImageSource, ImageUrlBuilder, ImageUrlOptions, imageUrl, default as imageUrlBuilder } from '@bettercms-ai/image-url';
5
5
 
@@ -397,14 +397,21 @@ declare function createModel(client: ManagementClient, input: CreateModelInput):
397
397
  /** Update a content model (additive — fields/name/slug/description). */
398
398
  declare function updateModel(client: ManagementClient, id: string, input: UpdateModelInput): Promise<ManagedContentModel>;
399
399
  /** Create a content entry under a model. */
400
- declare function createEntry(client: ManagementClient, input: CreateEntryInput): Promise<ManagedContentEntry>;
400
+ /**
401
+ * Advisory notes the server attached to a write — e.g. `REFERENCE_NOT_FOUND: …` for a reference
402
+ * whose target was deleted. The write succeeded; present only when there is something to say.
403
+ */
404
+ type EntryWriteWarnings = {
405
+ warnings?: string[];
406
+ };
407
+ declare function createEntry(client: ManagementClient, input: CreateEntryInput): Promise<ManagedContentEntry & EntryWriteWarnings>;
401
408
  /**
402
409
  * Update a content entry. If `opts.ifMatch` (current optimisticVersion) is supplied,
403
410
  * the update is optimistic-locked; omit it for programmatic last-write-wins.
404
411
  */
405
412
  declare function updateEntry(client: ManagementClient, id: string, input: UpdateEntryInput, opts?: {
406
413
  ifMatch?: number;
407
- }): Promise<ManagedContentEntry>;
414
+ }): Promise<ManagedContentEntry & EntryWriteWarnings>;
408
415
 
409
416
  /**
410
417
  * Management API SDK method — media upload (P2).
@@ -568,6 +575,75 @@ interface DeliveryFormField {
568
575
  * trip — the server check is the one that counts.
569
576
  */
570
577
  validation?: DeliveryFormFieldValidation;
578
+ /**
579
+ * Presentation options the renderer honours. Unlike `validation`, the API does NOT enforce
580
+ * these on submit — they change what the visitor is shown, not what is accepted.
581
+ */
582
+ ui?: DeliveryFormFieldUi;
583
+ }
584
+ /**
585
+ * @see FormFieldUi in @bettercms-ai/types — same shape, delivered to the site.
586
+ *
587
+ * ── The phone composition contract every renderer implements ──
588
+ *
589
+ * A picker-enabled phone field is rendered as a country control PLUS a `tel` input, but it
590
+ * submits ONE value under the field's own key, in E.164. Whatever a renderer uses to hold the
591
+ * country (a React state, a `{key}__country` select) is scaffolding: it must never reach the
592
+ * POST body.
593
+ *
594
+ * Composition, from the visible input's raw text:
595
+ * - empty → `""`. NEVER `"+CC"`, and the reason is worse than "the server rejects it". The
596
+ * server's loose phone shape is `{3,32}` chars, so the outcome depends on the calling
597
+ * code's length: `"+1"` is REFUSED (an optional untouched field starts 400ing), while
598
+ * `"+91"` is ACCEPTED and stored as a real-looking submission that is only a country code.
599
+ * The silent case is the dangerous one. Pinned in `__tests__/lib/forms-validate.test.ts`.
600
+ * - starts with `"+"` → `"+" + digits(raw)`. The visitor typed an international number; the
601
+ * country control is ignored rather than fighting them.
602
+ * - otherwise → `"+" + callingCode + digits(raw).replace(/^0/, "")`, dropping ONE trunk zero.
603
+ * Known exception: Italian landlines keep their trunk zero, so this loses it. The value is
604
+ * still accepted (shape-only check) — it is a formatting loss, not a refusal.
605
+ *
606
+ * `ui` changes what the visitor SEES. It never changes what the API accepts: enforcing a format
607
+ * is `validation.phoneFormat`'s job and the two are independent.
608
+ *
609
+ * Renderer parity is behavioural, not implementational. FIVE lanes render this field, and they
610
+ * agree on the VALUE, not on the implementation:
611
+ *
612
+ * | Lane | Control | As-you-type formatting |
613
+ * |-------------------------------|---------------------------------|------------------------|
614
+ * | dashboard form preview | searchable picker + formatter | yes |
615
+ * | @bettercms-ai/next <BcmsForm> | native select + formatter | yes (lazy chunk) |
616
+ * | hosted sites (render-page) | native select + tel input | NO |
617
+ * | embed snippet | native select + tel input | NO |
618
+ * | @bettercms-ai/astro BcmsForm | native select + tel input | NO |
619
+ *
620
+ * The missing as-you-type formatting on the three vanilla lanes is a NAMED degradation, not an
621
+ * oversight: it is what keeps libphonenumber off a visitor's page entirely. `packages/next`
622
+ * pays for it only on pages that actually render a picker, via a separate chunk.
623
+ *
624
+ * When a lane builds that option list, never pass a libphonenumber helper as a bare callback:
625
+ * `getCountries().filter(isSupportedCountry)` hands it the array INDEX as its `metadata`
626
+ * argument and throws. Always wrap — `.filter((iso) => isSupportedCountry(iso))`.
627
+ *
628
+ * And when a lane ships the composition rule inside a JS TEMPLATE LITERAL (the embed snippet
629
+ * does), `\D` collapses to `D` and the regex silently becomes `/D/g` — stripping the letter D
630
+ * and leaving every other separator in place. Write `\\D` there. Raw HTML `<script>` text (the
631
+ * hosted renderer, the Astro inline script) takes a single backslash. Nothing throws either
632
+ * way; numbers just arrive malformed, so each lane pins it with an executed-script test.
633
+ */
634
+ interface DeliveryFormFieldUi {
635
+ /** `phone` — render a country picker beside the number. The submitted value is E.164. */
636
+ countryPicker?: boolean;
637
+ /** `phone` — uppercase ISO 3166-1 alpha-2 the picker starts on. */
638
+ defaultCountry?: string;
639
+ /**
640
+ * `phone` — show the country's flag beside the dialling code.
641
+ *
642
+ * Honoured only by a renderer with a custom picker. The native `<select>` the site lanes use
643
+ * cannot hold an SVG, and an emoji flag renders as two grey letters on Windows, so those
644
+ * lanes show the country NAME instead — the same information, legibly, everywhere.
645
+ */
646
+ showFlags?: boolean;
571
647
  }
572
648
  /** @see FormFieldValidation in @bettercms-ai/types — same shape, delivered to the site. */
573
649
  interface DeliveryFormFieldValidation {
@@ -663,6 +739,14 @@ interface ManagedFormInput {
663
739
  submitLabel?: string;
664
740
  successMessage?: string;
665
741
  redirectUrl?: string;
742
+ /** Require a Cloudflare Turnstile check on submit. Its secret key is set in the dashboard only. */
743
+ turnstileEnabled?: boolean;
744
+ /** Name of a hidden field that must stay empty; a filled one marks the submission as spam. */
745
+ honeypotField?: string | null;
746
+ /** Addresses notified of each submission. */
747
+ notifyEmails?: string[];
748
+ /** URL that receives each submission as a POST. */
749
+ webhookUrl?: string | null;
666
750
  }
667
751
 
668
752
  /**
@@ -889,10 +973,10 @@ declare class BetterCMSManagementClient extends BetterCMSDeliveryClient {
889
973
  setPageContent(id: string, input: SetPageContentInput): Promise<ManagedContentEntry>;
890
974
  /** Append fields to a content model (additive — rejects existing keys). */
891
975
  addModelFields(id: string, input: AddPageFieldsInput): Promise<ManagedContentModel>;
892
- createEntry(input: CreateEntryInput): Promise<ManagedContentEntry>;
976
+ createEntry(input: CreateEntryInput): Promise<ManagedContentEntry & EntryWriteWarnings>;
893
977
  updateEntry(id: string, input: UpdateEntryInput, opts?: {
894
978
  ifMatch?: number;
895
- }): Promise<ManagedContentEntry>;
979
+ }): Promise<ManagedContentEntry & EntryWriteWarnings>;
896
980
  /** Soft-delete a page and its content entries. */
897
981
  deletePage(id: string): Promise<{
898
982
  id: string;
@@ -1457,14 +1541,20 @@ interface CreateFormInput {
1457
1541
  redirectUrl?: string;
1458
1542
  webhookUrl?: string;
1459
1543
  }
1460
- interface FormFieldInput {
1461
- key: string;
1462
- label: string;
1463
- type: "text" | "email" | "textarea" | "select" | "checkbox" | "number";
1464
- placeholder?: string;
1465
- required?: boolean;
1466
- options?: string[];
1467
- }
1544
+ /**
1545
+ * A field in an admin create/update payload.
1546
+ *
1547
+ * An ALIAS of `FormField`, not a copy. It used to be a hand-written interface and it had gone
1548
+ * stale: it listed six of the thirteen field types the builder can author — `phone`, `date`,
1549
+ * `url`, `radio`, `checkboxes`, `consent` and `hidden` were all missing — and omitted
1550
+ * `helpText`, `hidden`, `defaultValue`, `showIf`, `validation` and `ui` entirely. A typed
1551
+ * consumer of this client therefore could not express a phone field at all, let alone one with
1552
+ * a country picker, even though the route accepts every one of them.
1553
+ *
1554
+ * The admin routes validate against `formFieldSchema`, which mirrors `FormField`, so aliasing
1555
+ * is what keeps this honest: there is no second declaration left to drift.
1556
+ */
1557
+ type FormFieldInput = FormField;
1468
1558
  interface UpdateFormInput extends Partial<Omit<CreateFormInput, "fields">> {
1469
1559
  fields?: FormFieldInput[];
1470
1560
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _bettercms_ai_types from '@bettercms-ai/types';
2
- import { ContentPageResult, Content, DeliveredLayout, ContentModelField, BetterCMSErrorCode, LayoutStructureDocument, LayoutDataDocument, PageLayoutOverrideDocument, ManagementLayoutCommand, AuthUser, AuthSession, SignInInput, SignUpInput, Perspective, DeliveryEntry, DeliveryList, DeliveryPage, DeliveryComponent, PageMetaJson, DeliveryEntrySeo, SiteSeoDefaults, Page, MediaAsset, Form, FormSubmission, Workspace, ApiKey, ApiKeyPermission, ApiKeyTokenType } from '@bettercms-ai/types';
2
+ import { ContentPageResult, Content, DeliveredLayout, ContentModelField, BetterCMSErrorCode, LayoutStructureDocument, LayoutDataDocument, PageLayoutOverrideDocument, ManagementLayoutCommand, AuthUser, AuthSession, SignInInput, SignUpInput, Perspective, DeliveryEntry, DeliveryList, DeliveryPage, DeliveryComponent, PageMetaJson, DeliveryEntrySeo, SiteSeoDefaults, Page, MediaAsset, FormField, Form, FormSubmission, Workspace, ApiKey, ApiKeyPermission, ApiKeyTokenType } from '@bettercms-ai/types';
3
3
  export { ApiError, ApiKey, ApiKeyPermission, ApiKeyTokenType, AssertEqual, AuthSession, AuthUser, BetterCMSErrorCode, BlockType, ButtonBlock, ButtonProps, ColumnsBlock, ColumnsProps, Content, ContentBlock, ContentEntry, ContentModel, ContentModelField, ContentPageResult, ContentResponse, DeepReadonly, DeliveryComponent, DeliveryEntry, DeliveryList, DeliveryPage, Form, FormBlock, FormField, FormProps, FormSubmission, HeadingBlock, HeadingProps, ImageBlock, ImageProps, MediaAsset, Page, PageMetaJson, PaginatedResult, Perspective, Redirect, SignInInput, SignUpInput, SiteSeoDefaults, SpacerBlock, SpacerProps, TextBlock, TextProps, VideoBlock, VideoProps, Workspace, getBlockType, isBlock } from '@bettercms-ai/types';
4
4
  export { ImageFit, ImageFormat, ImageSource, ImageUrlBuilder, ImageUrlOptions, imageUrl, default as imageUrlBuilder } from '@bettercms-ai/image-url';
5
5
 
@@ -397,14 +397,21 @@ declare function createModel(client: ManagementClient, input: CreateModelInput):
397
397
  /** Update a content model (additive — fields/name/slug/description). */
398
398
  declare function updateModel(client: ManagementClient, id: string, input: UpdateModelInput): Promise<ManagedContentModel>;
399
399
  /** Create a content entry under a model. */
400
- declare function createEntry(client: ManagementClient, input: CreateEntryInput): Promise<ManagedContentEntry>;
400
+ /**
401
+ * Advisory notes the server attached to a write — e.g. `REFERENCE_NOT_FOUND: …` for a reference
402
+ * whose target was deleted. The write succeeded; present only when there is something to say.
403
+ */
404
+ type EntryWriteWarnings = {
405
+ warnings?: string[];
406
+ };
407
+ declare function createEntry(client: ManagementClient, input: CreateEntryInput): Promise<ManagedContentEntry & EntryWriteWarnings>;
401
408
  /**
402
409
  * Update a content entry. If `opts.ifMatch` (current optimisticVersion) is supplied,
403
410
  * the update is optimistic-locked; omit it for programmatic last-write-wins.
404
411
  */
405
412
  declare function updateEntry(client: ManagementClient, id: string, input: UpdateEntryInput, opts?: {
406
413
  ifMatch?: number;
407
- }): Promise<ManagedContentEntry>;
414
+ }): Promise<ManagedContentEntry & EntryWriteWarnings>;
408
415
 
409
416
  /**
410
417
  * Management API SDK method — media upload (P2).
@@ -568,6 +575,75 @@ interface DeliveryFormField {
568
575
  * trip — the server check is the one that counts.
569
576
  */
570
577
  validation?: DeliveryFormFieldValidation;
578
+ /**
579
+ * Presentation options the renderer honours. Unlike `validation`, the API does NOT enforce
580
+ * these on submit — they change what the visitor is shown, not what is accepted.
581
+ */
582
+ ui?: DeliveryFormFieldUi;
583
+ }
584
+ /**
585
+ * @see FormFieldUi in @bettercms-ai/types — same shape, delivered to the site.
586
+ *
587
+ * ── The phone composition contract every renderer implements ──
588
+ *
589
+ * A picker-enabled phone field is rendered as a country control PLUS a `tel` input, but it
590
+ * submits ONE value under the field's own key, in E.164. Whatever a renderer uses to hold the
591
+ * country (a React state, a `{key}__country` select) is scaffolding: it must never reach the
592
+ * POST body.
593
+ *
594
+ * Composition, from the visible input's raw text:
595
+ * - empty → `""`. NEVER `"+CC"`, and the reason is worse than "the server rejects it". The
596
+ * server's loose phone shape is `{3,32}` chars, so the outcome depends on the calling
597
+ * code's length: `"+1"` is REFUSED (an optional untouched field starts 400ing), while
598
+ * `"+91"` is ACCEPTED and stored as a real-looking submission that is only a country code.
599
+ * The silent case is the dangerous one. Pinned in `__tests__/lib/forms-validate.test.ts`.
600
+ * - starts with `"+"` → `"+" + digits(raw)`. The visitor typed an international number; the
601
+ * country control is ignored rather than fighting them.
602
+ * - otherwise → `"+" + callingCode + digits(raw).replace(/^0/, "")`, dropping ONE trunk zero.
603
+ * Known exception: Italian landlines keep their trunk zero, so this loses it. The value is
604
+ * still accepted (shape-only check) — it is a formatting loss, not a refusal.
605
+ *
606
+ * `ui` changes what the visitor SEES. It never changes what the API accepts: enforcing a format
607
+ * is `validation.phoneFormat`'s job and the two are independent.
608
+ *
609
+ * Renderer parity is behavioural, not implementational. FIVE lanes render this field, and they
610
+ * agree on the VALUE, not on the implementation:
611
+ *
612
+ * | Lane | Control | As-you-type formatting |
613
+ * |-------------------------------|---------------------------------|------------------------|
614
+ * | dashboard form preview | searchable picker + formatter | yes |
615
+ * | @bettercms-ai/next <BcmsForm> | native select + formatter | yes (lazy chunk) |
616
+ * | hosted sites (render-page) | native select + tel input | NO |
617
+ * | embed snippet | native select + tel input | NO |
618
+ * | @bettercms-ai/astro BcmsForm | native select + tel input | NO |
619
+ *
620
+ * The missing as-you-type formatting on the three vanilla lanes is a NAMED degradation, not an
621
+ * oversight: it is what keeps libphonenumber off a visitor's page entirely. `packages/next`
622
+ * pays for it only on pages that actually render a picker, via a separate chunk.
623
+ *
624
+ * When a lane builds that option list, never pass a libphonenumber helper as a bare callback:
625
+ * `getCountries().filter(isSupportedCountry)` hands it the array INDEX as its `metadata`
626
+ * argument and throws. Always wrap — `.filter((iso) => isSupportedCountry(iso))`.
627
+ *
628
+ * And when a lane ships the composition rule inside a JS TEMPLATE LITERAL (the embed snippet
629
+ * does), `\D` collapses to `D` and the regex silently becomes `/D/g` — stripping the letter D
630
+ * and leaving every other separator in place. Write `\\D` there. Raw HTML `<script>` text (the
631
+ * hosted renderer, the Astro inline script) takes a single backslash. Nothing throws either
632
+ * way; numbers just arrive malformed, so each lane pins it with an executed-script test.
633
+ */
634
+ interface DeliveryFormFieldUi {
635
+ /** `phone` — render a country picker beside the number. The submitted value is E.164. */
636
+ countryPicker?: boolean;
637
+ /** `phone` — uppercase ISO 3166-1 alpha-2 the picker starts on. */
638
+ defaultCountry?: string;
639
+ /**
640
+ * `phone` — show the country's flag beside the dialling code.
641
+ *
642
+ * Honoured only by a renderer with a custom picker. The native `<select>` the site lanes use
643
+ * cannot hold an SVG, and an emoji flag renders as two grey letters on Windows, so those
644
+ * lanes show the country NAME instead — the same information, legibly, everywhere.
645
+ */
646
+ showFlags?: boolean;
571
647
  }
572
648
  /** @see FormFieldValidation in @bettercms-ai/types — same shape, delivered to the site. */
573
649
  interface DeliveryFormFieldValidation {
@@ -663,6 +739,14 @@ interface ManagedFormInput {
663
739
  submitLabel?: string;
664
740
  successMessage?: string;
665
741
  redirectUrl?: string;
742
+ /** Require a Cloudflare Turnstile check on submit. Its secret key is set in the dashboard only. */
743
+ turnstileEnabled?: boolean;
744
+ /** Name of a hidden field that must stay empty; a filled one marks the submission as spam. */
745
+ honeypotField?: string | null;
746
+ /** Addresses notified of each submission. */
747
+ notifyEmails?: string[];
748
+ /** URL that receives each submission as a POST. */
749
+ webhookUrl?: string | null;
666
750
  }
667
751
 
668
752
  /**
@@ -889,10 +973,10 @@ declare class BetterCMSManagementClient extends BetterCMSDeliveryClient {
889
973
  setPageContent(id: string, input: SetPageContentInput): Promise<ManagedContentEntry>;
890
974
  /** Append fields to a content model (additive — rejects existing keys). */
891
975
  addModelFields(id: string, input: AddPageFieldsInput): Promise<ManagedContentModel>;
892
- createEntry(input: CreateEntryInput): Promise<ManagedContentEntry>;
976
+ createEntry(input: CreateEntryInput): Promise<ManagedContentEntry & EntryWriteWarnings>;
893
977
  updateEntry(id: string, input: UpdateEntryInput, opts?: {
894
978
  ifMatch?: number;
895
- }): Promise<ManagedContentEntry>;
979
+ }): Promise<ManagedContentEntry & EntryWriteWarnings>;
896
980
  /** Soft-delete a page and its content entries. */
897
981
  deletePage(id: string): Promise<{
898
982
  id: string;
@@ -1457,14 +1541,20 @@ interface CreateFormInput {
1457
1541
  redirectUrl?: string;
1458
1542
  webhookUrl?: string;
1459
1543
  }
1460
- interface FormFieldInput {
1461
- key: string;
1462
- label: string;
1463
- type: "text" | "email" | "textarea" | "select" | "checkbox" | "number";
1464
- placeholder?: string;
1465
- required?: boolean;
1466
- options?: string[];
1467
- }
1544
+ /**
1545
+ * A field in an admin create/update payload.
1546
+ *
1547
+ * An ALIAS of `FormField`, not a copy. It used to be a hand-written interface and it had gone
1548
+ * stale: it listed six of the thirteen field types the builder can author — `phone`, `date`,
1549
+ * `url`, `radio`, `checkboxes`, `consent` and `hidden` were all missing — and omitted
1550
+ * `helpText`, `hidden`, `defaultValue`, `showIf`, `validation` and `ui` entirely. A typed
1551
+ * consumer of this client therefore could not express a phone field at all, let alone one with
1552
+ * a country picker, even though the route accepts every one of them.
1553
+ *
1554
+ * The admin routes validate against `formFieldSchema`, which mirrors `FormField`, so aliasing
1555
+ * is what keeps this honest: there is no second declaration left to drift.
1556
+ */
1557
+ type FormFieldInput = FormField;
1468
1558
  interface UpdateFormInput extends Partial<Omit<CreateFormInput, "fields">> {
1469
1559
  fields?: FormFieldInput[];
1470
1560
  }
package/dist/index.js CHANGED
@@ -519,12 +519,13 @@ async function updateModel(client, id, input) {
519
519
  );
520
520
  return res.data;
521
521
  }
522
+ var withWarnings = (res) => res.warnings?.length ? { ...res.data, warnings: res.warnings } : res.data;
522
523
  async function createEntry(client, input) {
523
524
  const res = await client.fetchJSON(
524
525
  client.url("/management/content/entries"),
525
526
  { method: "POST", body: JSON.stringify(input) }
526
527
  );
527
- return res.data;
528
+ return withWarnings(res);
528
529
  }
529
530
  async function updateEntry(client, id, input, opts) {
530
531
  const headers = {};
@@ -533,7 +534,7 @@ async function updateEntry(client, id, input, opts) {
533
534
  client.url(`/management/content/entries/${id}`),
534
535
  { method: "PATCH", body: JSON.stringify(input), headers }
535
536
  );
536
- return res.data;
537
+ return withWarnings(res);
537
538
  }
538
539
  async function deleteManagedPage(client, id) {
539
540
  const res = await client.fetchJSON(