riverbankcms 0.21.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { C as stableStringify, Cn as getDirectImageUrlForSiteScopedPath, D as evaluateSiteCommandBatchExposure, Hn as getMimeType, Ht as buttonStyleSelectionSchema, J as createContentEntryRef, Kn as FILE_SIZE_LIMITS, Ln as resolveMediaIdentifier, N as entryIdentifierForCommand, O as evaluateSiteCommandExposure, Pn as buildMediaIdentifierFromFilename, Qt as footerPreferencesSchema, Rn as slugifyMediaIdentifier, St as themeSchema, T as parseAndPlanCommandBatchForTypes, Vn as getExtensionForMimeType, Wn as normalizeAmbiguousStoragePathForSite, Xt as pageDesignBoundaryTreatmentSchema, Zt as pageDesignIntentSchemaVersion, ar as asAppearancePresetId, cr as asPageFlowPresetId, it as slugPart, k as getSiteCommandMetadata, lr as asSiteStyleId, m as isInternalResolvedLinkValue, n as sdkCourseBookingSettingsSchema, q as createCommandRef, qn as MAX_MANAGEMENT_MEDIA_UPLOAD_BYTES, r as sdkEventOccurrenceInputSchema, s as RESOLVED_INTERNAL_LINK_ENTITY_TYPES, tt as createSiteCommandSiteRef, w as createDryRunReport, y as eventVisibilitySchema, z as eventBookingOfferIntentSchema } from "./sdk-event-upsert-CrOuJrxo.mjs";
3
- import { In as err, Ln as ok, M as parseFooterCompositionV1, S as parseManagementKey, _n as unsafeAsEventSeriesId, a as canonicalizeConfiguredBookingVerticalConfig, b as createSecret, bn as unsafeAsOccurrenceId, c as resolveSiteVerticalBookingConfig, gn as unsafeAsEventOfferingId, i as areConfiguredBookingVerticalConfigsEqual, in as parseSiteId, jn as unsafeAsStaffMemberId, l as DEFAULT_EVENT_TYPE_PRESETS, m as publicReadFreshnessSeconds, nn as parsePageId, o as parseVerticalId, qt as assertNever, s as parseVerticalPresetId, sn as unsafeAsAppointmentResourceId, t as resolveEventCatalogue, u as EVENT_PRESET_VALUES, x as exposeSecret } from "./src-BENi4Q5k.mjs";
4
- import { n as CliSdkPeerError, t as loadConfigFile } from "./load-config-B68gtN45.mjs";
2
+ import { $t as pageDesignIntentSchemaVersion, A as getSiteCommandMetadata, B as eventBookingOfferIntentSchema, Dn as getDirectImageUrlForSiteScopedPath, E as parseAndPlanCommandBatchForTypes, Gn as getExtensionForMimeType, Hn as slugifyMediaIdentifier, J as createCommandRef, Jn as normalizeAmbiguousStoragePathForSite, Kn as getMimeType, O as evaluateSiteCommandBatchExposure, P as entryIdentifierForCommand, Qt as pageDesignBoundaryTreatmentSchema, Rn as buildMediaIdentifierFromFilename, T as createDryRunReport, Vn as resolveMediaIdentifier, Wt as buttonStyleSelectionSchema, Xn as FILE_SIZE_LIMITS, Y as createContentEntryRef, Zn as MAX_MANAGEMENT_MEDIA_UPLOAD_BYTES, at as slugPart, en as footerPreferencesSchema, fr as asPageFlowPresetId, k as evaluateSiteCommandExposure, lr as asAppearancePresetId, m as isInternalResolvedLinkValue, n as sdkCourseBookingSettingsSchema, nt as createSiteCommandSiteRef, pr as asSiteStyleId, r as sdkEventOccurrenceInputSchema, s as RESOLVED_INTERNAL_LINK_ENTITY_TYPES, w as stableStringify, wt as themeSchema, x as resolvePageUpsertSeo, y as eventVisibilitySchema } from "./sdk-event-upsert-B3gflo1K.mjs";
3
+ import { Hn as ok, In as unsafeAsStaffMemberId, M as parseFooterCompositionV1, Qt as assertNever, S as parseManagementKey, Sn as unsafeAsEventSeriesId, Tn as unsafeAsOccurrenceId, Vn as err, a as canonicalizeConfiguredBookingVerticalConfig, b as createSecret, c as resolveSiteVerticalBookingConfig, fn as unsafeAsAppointmentResourceId, i as areConfiguredBookingVerticalConfigsEqual, l as DEFAULT_EVENT_TYPE_PRESETS, ln as parseSiteId, m as publicReadFreshnessSeconds, o as parseVerticalId, s as parseVerticalPresetId, sn as parsePageId, t as resolveEventCatalogue, u as EVENT_PRESET_VALUES, x as exposeSecret, xn as unsafeAsEventOfferingId } from "./src-RfhyMOpX.mjs";
4
+ import { n as CliSdkPeerError, t as loadConfigFile } from "./load-config-pAPKpeE0.mjs";
5
5
  import { Command } from "commander";
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
7
  import * as fs$1 from "fs/promises";
@@ -414,6 +414,12 @@ const newsletterUsageLocationSchemas = [z.object({
414
414
  campaignName: z.string()
415
415
  })];
416
416
  const mediaUsageLocationSchema = z.discriminatedUnion("kind", [
417
+ z.object({
418
+ kind: z.literal("appointmentService"),
419
+ siteId: identifierSchema,
420
+ serviceId: identifierSchema,
421
+ serviceTitle: z.string()
422
+ }),
417
423
  ...newsletterUsageLocationSchemas,
418
424
  z.object({
419
425
  kind: z.literal("pageBlock"),
@@ -661,6 +667,8 @@ const sdkPullTruncationSchema = z.union([
661
667
  })
662
668
  ]);
663
669
  const sdkPullPageSchema = z.object({
670
+ seoTitle: z.string().nullable().optional(),
671
+ seoDescription: z.string().nullable().optional(),
664
672
  id: sdkIdentifierSchema$1,
665
673
  identifier: sdkIdentifierSchema$1,
666
674
  title: z.string(),
@@ -1839,6 +1847,34 @@ function createHttpClient(config) {
1839
1847
  };
1840
1848
  }
1841
1849
 
1850
+ //#endregion
1851
+ //#region ../management-client/src/appointmentServices.ts
1852
+ function createAppointmentServiceOperations(http) {
1853
+ return {
1854
+ async list() {
1855
+ const result = await http.get("/appointment-services");
1856
+ return result.services;
1857
+ },
1858
+ async get(serviceId) {
1859
+ try {
1860
+ const result = await http.get(`/appointment-services/${encodeURIComponent(serviceId)}`);
1861
+ return result.service;
1862
+ } catch (error) {
1863
+ if (is404Error(error)) return null;
1864
+ throw error;
1865
+ }
1866
+ },
1867
+ async create(input) {
1868
+ const result = await http.post("/appointment-services", input, "write");
1869
+ return result.service;
1870
+ },
1871
+ async update(serviceId, input) {
1872
+ const result = await http.patch(`/appointment-services/${encodeURIComponent(serviceId)}`, input);
1873
+ return result.service;
1874
+ }
1875
+ };
1876
+ }
1877
+
1842
1878
  //#endregion
1843
1879
  //#region ../management-client/src/entries.ts
1844
1880
  /**
@@ -1907,6 +1943,8 @@ function mapPulledPage(page) {
1907
1943
  id: page.id,
1908
1944
  identifier: page.identifier,
1909
1945
  title: page.title,
1946
+ ...page.seoTitle !== undefined ? { seoTitle: page.seoTitle } : {},
1947
+ ...page.seoDescription !== undefined ? { seoDescription: page.seoDescription } : {},
1910
1948
  path: page.path,
1911
1949
  status: page.status,
1912
1950
  hasUnpublishedChanges: page.hasUnpublishedChanges,
@@ -2617,6 +2655,7 @@ function createManagementClient(config) {
2617
2655
  }
2618
2656
  const http = createHttpClient(config);
2619
2657
  return {
2658
+ appointmentServices: createAppointmentServiceOperations(http),
2620
2659
  entries: createEntryOperations(http),
2621
2660
  pages: createPageOperations(http),
2622
2661
  blocks: createBlockOperations(http),
@@ -4434,6 +4473,31 @@ function parsePublicProductCategorySelector(value) {
4434
4473
  return undefined;
4435
4474
  }
4436
4475
 
4476
+ //#endregion
4477
+ //#region ../api/src/apiEndpointTypes.ts
4478
+ function allowedDemoMutationPolicy() {
4479
+ return {
4480
+ classification: "allowed",
4481
+ publicMessage: ""
4482
+ };
4483
+ }
4484
+ function blockedDemoMutationPolicy(classification, publicMessage) {
4485
+ if (publicMessage.trim() === "") {
4486
+ throw new Error(`Demo mutation policy ${classification} requires publicMessage.`);
4487
+ }
4488
+ return {
4489
+ classification,
4490
+ publicMessage
4491
+ };
4492
+ }
4493
+
4494
+ //#endregion
4495
+ //#region ../api/src/demoMutationPolicies.ts
4496
+ const DEMO_MUTATION_BLOCKED_HIDDEN = blockedDemoMutationPolicy("blocked-hidden", "This action is not available in demo sites.");
4497
+ const DEMO_MUTATION_BLOCKED_DISABLED = blockedDemoMutationPolicy("blocked-disabled", "This setting is disabled in demo sites.");
4498
+ const DEMO_MUTATION_INTERNAL_ONLY = blockedDemoMutationPolicy("internal-only", "This internal operation is not available in demo sites.");
4499
+ const DEMO_MUTATION_ALLOWED = allowedDemoMutationPolicy();
4500
+
4437
4501
  //#endregion
4438
4502
  //#region ../api/src/defineEndpointDefinitions.ts
4439
4503
  /**
@@ -4444,6 +4508,50 @@ function defineEndpointDefinitions(definitions) {
4444
4508
  return definitions;
4445
4509
  }
4446
4510
 
4511
+ //#endregion
4512
+ //#region ../api/src/bookingAttentionEndpointDefinitions.ts
4513
+ const BOOKING_ATTENTION_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
4514
+ dismissBookingAttention: {
4515
+ demoMutation: DEMO_MUTATION_ALLOWED,
4516
+ path: "/sites/{siteId}/bookings/attention/dismiss",
4517
+ method: "POST",
4518
+ tags: ["site-{siteId}"],
4519
+ auth: "user",
4520
+ responseKind: "json",
4521
+ errors: [
4522
+ "validation:invalid_input",
4523
+ "auth:forbidden",
4524
+ "server:internal_error"
4525
+ ]
4526
+ },
4527
+ resendWaitlistInvite: {
4528
+ path: "/sites/{siteId}/bookings/waitlist-entries/{entryId}/invite",
4529
+ method: "POST",
4530
+ tags: ["site-{siteId}"],
4531
+ auth: "user",
4532
+ responseKind: "json",
4533
+ errors: [
4534
+ "validation:invalid_input",
4535
+ "resource:not_found",
4536
+ "resource:conflict",
4537
+ "server:internal_error"
4538
+ ]
4539
+ },
4540
+ resendAppointmentPaymentRequest: {
4541
+ demoMutation: DEMO_MUTATION_BLOCKED_DISABLED,
4542
+ path: "/sites/{siteId}/bookings/appointment-payment-requests/{requestId}/resend",
4543
+ method: "POST",
4544
+ tags: ["site-{siteId}"],
4545
+ auth: "user",
4546
+ responseKind: "json",
4547
+ errors: [
4548
+ "validation:invalid_input",
4549
+ "auth:forbidden",
4550
+ "server:internal_error"
4551
+ ]
4552
+ }
4553
+ });
4554
+
4447
4555
  //#endregion
4448
4556
  //#region ../api/src/previewAuthEndpointDefinitions.ts
4449
4557
  const PREVIEW_AUTH_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
@@ -4470,31 +4578,6 @@ const PREVIEW_AUTH_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
4470
4578
  }
4471
4579
  });
4472
4580
 
4473
- //#endregion
4474
- //#region ../api/src/apiEndpointTypes.ts
4475
- function allowedDemoMutationPolicy() {
4476
- return {
4477
- classification: "allowed",
4478
- publicMessage: ""
4479
- };
4480
- }
4481
- function blockedDemoMutationPolicy(classification, publicMessage) {
4482
- if (publicMessage.trim() === "") {
4483
- throw new Error(`Demo mutation policy ${classification} requires publicMessage.`);
4484
- }
4485
- return {
4486
- classification,
4487
- publicMessage
4488
- };
4489
- }
4490
-
4491
- //#endregion
4492
- //#region ../api/src/demoMutationPolicies.ts
4493
- const DEMO_MUTATION_BLOCKED_HIDDEN = blockedDemoMutationPolicy("blocked-hidden", "This action is not available in demo sites.");
4494
- const DEMO_MUTATION_BLOCKED_DISABLED = blockedDemoMutationPolicy("blocked-disabled", "This setting is disabled in demo sites.");
4495
- const DEMO_MUTATION_INTERNAL_ONLY = blockedDemoMutationPolicy("internal-only", "This internal operation is not available in demo sites.");
4496
- const DEMO_MUTATION_ALLOWED = allowedDemoMutationPolicy();
4497
-
4498
4581
  //#endregion
4499
4582
  //#region ../api/src/siteLifecycleEndpoints.ts
4500
4583
  const SITE_LIFECYCLE_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
@@ -4827,6 +4910,42 @@ const SDK_ENTRY_READ_ENDPOINTS = {
4827
4910
  }
4828
4911
  };
4829
4912
 
4913
+ //#endregion
4914
+ //#region ../api/src/appointmentRequestEndpointDefinitions.ts
4915
+ const APPOINTMENT_REQUEST_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
4916
+ approveAppointmentRequest: {
4917
+ path: "/sites/{siteId}/bookings/appointments/{appointmentId}/approve",
4918
+ method: "POST",
4919
+ auth: "user",
4920
+ responseKind: "json",
4921
+ errors: [
4922
+ "validation:invalid_input",
4923
+ "resource:not_found",
4924
+ "auth:forbidden",
4925
+ "server:internal_error"
4926
+ ]
4927
+ },
4928
+ declineAppointmentRequest: {
4929
+ path: "/sites/{siteId}/bookings/appointments/{appointmentId}/decline",
4930
+ method: "POST",
4931
+ auth: "user",
4932
+ responseKind: "json",
4933
+ errors: [
4934
+ "validation:invalid_input",
4935
+ "resource:not_found",
4936
+ "auth:forbidden",
4937
+ "server:internal_error"
4938
+ ]
4939
+ },
4940
+ listWaitingAppointmentRequests: {
4941
+ path: "/sites/{siteId}/bookings/appointments/requests",
4942
+ method: "GET",
4943
+ auth: "user",
4944
+ responseKind: "json",
4945
+ errors: ["auth:forbidden", "server:internal_error"]
4946
+ }
4947
+ });
4948
+
4830
4949
  //#endregion
4831
4950
  //#region ../api/src/appointmentServiceCategoryEndpoints.ts
4832
4951
  const APPOINTMENT_SERVICE_CATEGORY_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
@@ -4911,6 +5030,7 @@ const APPOINTMENT_SERVICE_CATEGORY_ENDPOINT_DEFINITIONS = defineEndpointDefiniti
4911
5030
  //#region ../api/src/appointmentEndpointDefinitions.ts
4912
5031
  const APPOINTMENT_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
4913
5032
  ...APPOINTMENT_SERVICE_CATEGORY_ENDPOINT_DEFINITIONS,
5033
+ ...APPOINTMENT_REQUEST_ENDPOINT_DEFINITIONS,
4914
5034
  listAppointmentResources: {
4915
5035
  path: "/sites/{siteId}/bookings/resources",
4916
5036
  method: "GET",
@@ -6276,6 +6396,47 @@ const SDK_EVENT_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
6276
6396
  }
6277
6397
  });
6278
6398
 
6399
+ //#endregion
6400
+ //#region ../api/src/sdkAppointmentServiceEndpointDefinitions.ts
6401
+ const SDK_APPOINTMENT_SERVICE_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
6402
+ sdkListAppointmentServices: {
6403
+ path: "/sdk/{siteId}/appointment-services",
6404
+ method: "GET",
6405
+ auth: "service",
6406
+ responseKind: "json",
6407
+ errors: ["server:internal_error"]
6408
+ },
6409
+ sdkCreateAppointmentService: {
6410
+ path: "/sdk/{siteId}/appointment-services",
6411
+ method: "POST",
6412
+ auth: "service",
6413
+ responseKind: "json",
6414
+ errors: [
6415
+ "validation:invalid_input",
6416
+ "resource:not_found",
6417
+ "server:internal_error"
6418
+ ]
6419
+ },
6420
+ sdkGetAppointmentService: {
6421
+ path: "/sdk/{siteId}/appointment-services/{serviceId}",
6422
+ method: "GET",
6423
+ auth: "service",
6424
+ responseKind: "json",
6425
+ errors: ["resource:not_found", "server:internal_error"]
6426
+ },
6427
+ sdkUpdateAppointmentService: {
6428
+ path: "/sdk/{siteId}/appointment-services/{serviceId}",
6429
+ method: "PATCH",
6430
+ auth: "service",
6431
+ responseKind: "json",
6432
+ errors: [
6433
+ "validation:invalid_input",
6434
+ "resource:not_found",
6435
+ "server:internal_error"
6436
+ ]
6437
+ }
6438
+ });
6439
+
6279
6440
  //#endregion
6280
6441
  //#region ../api/src/sdkPageDesignEndpointDefinitions.ts
6281
6442
  /**
@@ -6883,6 +7044,14 @@ const GIFTING_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
6883
7044
  auth: "public",
6884
7045
  responseKind: "json"
6885
7046
  },
7047
+ previewGiftCardClaimPublic: {
7048
+ path: "/public/sites/{siteId}/gifting/claim",
7049
+ method: "GET",
7050
+ tags: [TAG_TEMPLATES.publicGifting],
7051
+ errors: [],
7052
+ auth: "public",
7053
+ responseKind: "json"
7054
+ },
6886
7055
  claimGiftCardPublic: {
6887
7056
  path: "/public/sites/{siteId}/gifting/claim",
6888
7057
  method: "POST",
@@ -10631,6 +10800,14 @@ const ENDPOINT_DEFINITIONS = {
10631
10800
  auth: "public",
10632
10801
  responseKind: "text"
10633
10802
  },
10803
+ getPublicAppointmentCalendarIcs: {
10804
+ path: "/public/sites/{siteId}/bookings/appointments/{token}/appointment.ics",
10805
+ method: "GET",
10806
+ tags: ["public-appointment-calendar-{siteId}"],
10807
+ auth: "public",
10808
+ responseKind: "text",
10809
+ errors: ["resource:not_found", "server:internal_error"]
10810
+ },
10634
10811
  getPublicBookingCalendarFeedIcs: {
10635
10812
  path: "/public/sites/{siteId}/bookings/calendar-feeds/{token}/feed.ics",
10636
10813
  method: "GET",
@@ -10967,19 +11144,7 @@ const ENDPOINT_DEFINITIONS = {
10967
11144
  responseKind: "json",
10968
11145
  errors: ["validation:invalid_input", "server:internal_error"]
10969
11146
  },
10970
- resendWaitlistInvite: {
10971
- path: "/sites/{siteId}/bookings/waitlist-entries/{entryId}/invite",
10972
- method: "POST",
10973
- tags: ["site-{siteId}"],
10974
- auth: "user",
10975
- responseKind: "json",
10976
- errors: [
10977
- "validation:invalid_input",
10978
- "resource:not_found",
10979
- "resource:conflict",
10980
- "server:internal_error"
10981
- ]
10982
- },
11147
+ ...BOOKING_ATTENTION_ENDPOINT_DEFINITIONS,
10983
11148
  cancelWaitlistEntry: {
10984
11149
  path: "/sites/{siteId}/bookings/waitlist-entries/{entryId}/cancel",
10985
11150
  method: "POST",
@@ -12319,6 +12484,7 @@ const ENDPOINT_DEFINITIONS = {
12319
12484
  auth: "service",
12320
12485
  responseKind: "json"
12321
12486
  },
12487
+ ...SDK_APPOINTMENT_SERVICE_ENDPOINT_DEFINITIONS,
12322
12488
  sdkUpsertEventCategory: {
12323
12489
  path: "/sdk/{siteId}/event-categories",
12324
12490
  method: "POST",
@@ -13834,1652 +14000,1647 @@ function createBearerAPIClient(token, baseUrl) {
13834
14000
  }
13835
14001
 
13836
14002
  //#endregion
13837
- //#region src/push-config-sync.ts
13838
- function isConfigSync(sync) {
13839
- return "ignoredDocumentSections" in sync;
14003
+ //#region ../api/src/appointmentServiceVisibility.ts
14004
+ const APPOINTMENT_SERVICE_VISIBILITY_VALUES = ["public", "unlisted"];
14005
+ function isAppointmentServiceVisibility(value) {
14006
+ return value === "public" || value === "unlisted";
13840
14007
  }
13841
- function getPushConfigSync(result) {
13842
- if (result.configSync) return result.configSync;
13843
- if (result.sync) return result.sync;
13844
- return null;
14008
+
14009
+ //#endregion
14010
+ //#region ../api/src/appointmentServiceLocation.ts
14011
+ const APPOINTMENT_SERVICE_LOCATION_TYPE_VALUES = [
14012
+ "virtual",
14013
+ "in_person",
14014
+ "hybrid"
14015
+ ];
14016
+ function isAppointmentServiceLocationType(value) {
14017
+ return value === "virtual" || value === "in_person" || value === "hybrid";
13845
14018
  }
13846
- function collectPushConfigSyncErrors(sync) {
13847
- return [
13848
- ...sync.contentTypes.errors.map((message) => ({
13849
- area: "contentTypes",
13850
- message
13851
- })),
13852
- ...sync.customBlocks.errors.map((message) => ({
13853
- area: "customBlocks",
13854
- message
13855
- })),
13856
- ...sync.templateBlocks.errors.map((message) => ({
13857
- area: "templateBlocks",
13858
- message
13859
- })),
13860
- ...sync.settings.error ? [{
13861
- area: "settings",
13862
- message: sync.settings.error
13863
- }] : [],
13864
- ...!isConfigSync(sync) ? sync.pages.errors.map((message) => ({
13865
- area: "pages",
13866
- message
13867
- })) : [],
13868
- ...!isConfigSync(sync) ? sync.entries.errors.map((message) => ({
13869
- area: "entries",
13870
- message
13871
- })) : [],
13872
- ...!isConfigSync(sync) ? sync.navigationMenus.errors.map((message) => ({
13873
- area: "navigationMenus",
13874
- message
13875
- })) : []
13876
- ];
13877
- }
13878
- function collectPushConfigSyncWarnings(sync) {
13879
- return [
13880
- ...sync.contentTypes.warnings.map((message) => ({
13881
- area: "contentTypes",
13882
- message
13883
- })),
13884
- ...sync.customBlocks.warnings.map((message) => ({
13885
- area: "customBlocks",
13886
- message
13887
- })),
13888
- ...sync.templateBlocks.warnings.map((message) => ({
13889
- area: "templateBlocks",
13890
- message
13891
- })),
13892
- ...isConfigSync(sync) ? sync.ignoredDocumentSections.map((section) => ({
13893
- area: "ignoredDocuments",
13894
- message: formatIgnoredDocumentSyncSectionWarning(section)
13895
- })) : [
13896
- ...sync.pages.warnings.map((message) => ({
13897
- area: "pages",
13898
- message
13899
- })),
13900
- ...sync.entries.warnings.map((message) => ({
13901
- area: "entries",
13902
- message
13903
- })),
13904
- ...sync.navigationMenus.warnings.map((message) => ({
13905
- area: "navigationMenus",
13906
- message
13907
- }))
13908
- ]
13909
- ];
13910
- }
13911
- function groupPushConfigSyncWarningsForHuman(warnings) {
13912
- const ignoredDocumentCount = warnings.filter((warning) => warning.area === "ignoredDocuments").length;
13913
- return [...warnings.filter((warning) => warning.area !== "ignoredDocuments"), ...ignoredDocumentCount > 0 ? [{
13914
- area: "ignoredDocuments",
13915
- message: `${ignoredDocumentCount} document content section${ignoredDocumentCount === 1 ? "" : "s"} ignored by push-config. Use \`riverbankcms push\` for pages, entries, and navigation content.`
13916
- }] : []];
13917
- }
13918
- function buildPreviewSummary(sync) {
13919
- return { wouldConsider: {
13920
- contentTypes: sync.contentTypes.skipped,
13921
- customBlocks: sync.customBlocks.skipped,
13922
- templateBlocks: sync.templateBlocks.skipped,
13923
- settings: sync.settings.updated,
13924
- ...isConfigSync(sync) ? { footerBlocks: sync.footerBlocks.skipped } : {},
13925
- ...isConfigSync(sync) ? { ignoredDocumentSections: sync.ignoredDocumentSections } : {}
13926
- } };
14019
+
14020
+ //#endregion
14021
+ //#region ../api/src/bookingOperations.ts
14022
+ const APPOINTMENT_RESERVATION_STATUS_VALUES = [
14023
+ "active",
14024
+ "paused",
14025
+ "ended"
14026
+ ];
14027
+
14028
+ //#endregion
14029
+ //#region ../api/src/utils/isRecord.ts
14030
+ function isRecord$5(value) {
14031
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13927
14032
  }
13928
14033
 
13929
14034
  //#endregion
13930
- //#region src/push-config.ts
14035
+ //#region ../api/src/utils/error-propagation.ts
13931
14036
  /**
13932
- * push-config CLI command
13933
- *
13934
- * Pushes SDK site configuration from a riverbank.config.ts file to the dashboard API.
14037
+ * Extracts an error message from an API error response body.
14038
+ * Checks for our standard API error format: { error: { message: "..." } }
14039
+ * Also handles legacy formats with `error` or `message` as strings.
13935
14040
  */
13936
- const DEFAULT_CONFIG_PATH = "./riverbank.config.ts";
13937
- function formatPushConfigActionFailure() {
13938
- return "push-config failed";
13939
- }
13940
- function isRecord$6(value) {
13941
- return typeof value === "object" && value !== null;
13942
- }
13943
- function parseErrorDetails(value) {
13944
- if (!value) return {};
13945
- if (typeof value === "string") {
13946
- return { message: value };
13947
- }
13948
- if (!isRecord$6(value)) return {};
13949
- const message = typeof value.message === "string" ? value.message : undefined;
13950
- const fieldErrorsRaw = value.fieldErrors;
13951
- const fieldErrors = Array.isArray(fieldErrorsRaw) ? fieldErrorsRaw.filter(isRecord$6).map((d) => ({
13952
- field: typeof d.field === "string" ? d.field : "",
13953
- message: typeof d.message === "string" ? d.message : ""
13954
- })).filter((d) => d.field.length > 0 && d.message.length > 0) : undefined;
13955
- return {
13956
- message,
13957
- fieldErrors
13958
- };
13959
- }
13960
- function formatFieldErrors(fieldErrors) {
13961
- if (!fieldErrors || fieldErrors.length === 0) return "";
13962
- return ":\n" + fieldErrors.map((d) => ` - ${d.field}: ${d.message}`).join("\n");
13963
- }
13964
- function getDashboardErrorMessage(responseJson, fallback) {
13965
- if (!responseJson) return fallback;
13966
- if (isRecord$6(responseJson) && "error" in responseJson) {
13967
- const parsed = parseErrorDetails(responseJson.error);
13968
- const msg = parsed.message ?? fallback;
13969
- return msg + formatFieldErrors(parsed.fieldErrors);
14041
+ function extractApiErrorMessage(body) {
14042
+ if (!isRecord$5(body)) {
14043
+ return null;
13970
14044
  }
13971
- return fallback;
13972
- }
13973
- /**
13974
- * Push config to dashboard API
13975
- */
13976
- async function pushToDashboard(dashboardUrl, siteId, apiKey, config, options) {
13977
- const normalizedDashboardUrl = dashboardUrl.replace(/\/+$/, "");
13978
- const baseUrl = `${normalizedDashboardUrl}/api`;
13979
- const path$3 = `/sites/${siteId}/sdk-config`;
13980
- const pushUrl = `${baseUrl}${path$3}`;
13981
- const requestInfo = {
13982
- method: "POST",
13983
- intent: "write",
13984
- path: path$3,
13985
- url: pushUrl,
13986
- baseUrl,
13987
- dashboardUrl: normalizedDashboardUrl,
13988
- siteId
13989
- };
13990
- const client = createBearerAPIClient(exposeSecret(apiKey), baseUrl);
13991
- const timeoutMs = currentRequestTimeoutMs() ?? 3e4;
13992
- try {
13993
- return unwrapResponse(await client({
13994
- endpoint: "pushSdkConfig",
13995
- params: { siteId },
13996
- body: {
13997
- config,
13998
- forceContentTypes: options?.forceContentTypes ?? false,
13999
- mode: options?.dryRun ? "preview" : "apply",
14000
- expectedMailingListProviderScope: options?.expectedMailingListProviderScope ?? undefined
14001
- },
14002
- options: { signal: AbortSignal.timeout(timeoutMs) }
14003
- }));
14004
- } catch (error) {
14005
- if (error instanceof ApiEnvelopeError) {
14006
- const details = {
14007
- success: false,
14008
- error: {
14009
- code: error.code,
14010
- message: error.message,
14011
- fieldErrors: error.fieldErrors
14012
- }
14013
- };
14014
- const errorMessage = getDashboardErrorMessage(details, error.message);
14015
- throw new ManagementApiError(errorMessage, "sdk:http-error", details, error.status, requestInfo);
14016
- }
14017
- if (error instanceof ApiRequestError) {
14018
- const errorMessage = getDashboardErrorMessage(error.body, `Dashboard returned ${error.status}`);
14019
- throw new ManagementApiError(errorMessage, "sdk:http-error", error.body, error.status, requestInfo);
14045
+ if (isRecord$5(body.error)) {
14046
+ if (typeof body.error.message === "string" && body.error.message.trim().length > 0) {
14047
+ return body.error.message;
14020
14048
  }
14021
- throw managementTransportError(error, timeoutMs, requestInfo);
14022
- }
14023
- }
14024
- function validatePushConfig(rawConfig) {
14025
- const parsed = riverbankSiteConfigSchema.safeParse(rawConfig);
14026
- if (!parsed.success) {
14027
- const issues = parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
14028
- throw new Error(`Invalid config: ${issues}`);
14029
14049
  }
14030
- const conflicts = validateFieldIdConflicts(parsed.data.blockFieldExtensions);
14031
- if (conflicts.length > 0) {
14032
- throw new Error(`Field ID conflicts detected in blockFieldExtensions: ${conflicts.map((conflict) => conflict.message).join("; ")}`);
14050
+ if (typeof body.error === "string" && body.error.trim().length > 0) {
14051
+ return body.error;
14033
14052
  }
14034
- return parsed.data;
14035
- }
14036
- async function loadValidatedPushConfig(target, configPath) {
14037
- return validatePushConfig(await withRiverbankEnv(target, () => loadConfigFile(configPath)));
14038
- }
14039
- function analyzePushConfigResponse(response, dryRun) {
14040
- const resultMode = response.mode ?? "apply";
14041
- if (dryRun && resultMode !== "preview") {
14042
- return {
14043
- success: false,
14044
- kind: "unsafe-preview",
14045
- receivedMode: response.mode
14046
- };
14053
+ if (typeof body.message === "string" && body.message.trim().length > 0) {
14054
+ return body.message;
14047
14055
  }
14048
- const sync = getPushConfigSync(response);
14049
- if (!sync) return {
14050
- success: true,
14051
- resultMode,
14052
- sync: null,
14053
- warnings: []
14054
- };
14055
- const errors = collectPushConfigSyncErrors(sync);
14056
- if (errors.length > 0 || sync.success === false) return {
14057
- success: false,
14058
- kind: "sync-errors",
14059
- sync,
14060
- errors
14061
- };
14062
- return {
14063
- success: true,
14064
- resultMode,
14065
- sync,
14066
- warnings: collectPushConfigSyncWarnings(sync)
14067
- };
14056
+ return null;
14068
14057
  }
14069
- function renderPushConfigResponse(output, response, mode, dryRun) {
14070
- const analysis = analyzePushConfigResponse(response, dryRun);
14071
- if (!analysis.success) {
14072
- if (analysis.kind === "unsafe-preview") {
14073
- output.fail("Config preview was not confirmed by the dashboard; no safe dry-run result is available.", {
14074
- expectedMode: "preview",
14075
- receivedMode: analysis.receivedMode ?? null,
14076
- suggestion: "Update the dashboard API before relying on push-config --dry-run."
14077
- });
14078
- return { success: false };
14079
- }
14080
- if (mode === "json") {
14081
- output.markFailed();
14082
- output.json({
14083
- success: false,
14084
- sync: analysis.sync,
14085
- errors: analysis.errors
14086
- });
14087
- } else {
14088
- output.markFailed();
14089
- output.fail("Config pushed, but SDK config sync reported errors", {
14090
- errors: analysis.errors,
14091
- summary: analysis.sync
14092
- });
14093
- }
14094
- return { success: false };
14095
- }
14096
- if (mode === "human" && analysis.warnings.length > 0) {
14097
- output.warn("SDK config sync reported warnings", { warnings: groupPushConfigSyncWarningsForHuman(analysis.warnings) });
14058
+ /**
14059
+ * Normalizes various error types and throws with an appropriate message.
14060
+ *
14061
+ * - For ApiRequestError: extracts message from body.error or body.message
14062
+ * - For Error instances: re-throws as-is
14063
+ * - For unknown errors: wraps with fallback message
14064
+ *
14065
+ * @param error - The error to normalize
14066
+ * @param fallback - Fallback message if no specific message can be extracted
14067
+ * @throws {Error} Always throws - this function never returns
14068
+ */
14069
+ function normalizeApiError(error, fallback) {
14070
+ if (error instanceof ApiRequestError) {
14071
+ const message = extractApiErrorMessage(error.body);
14072
+ throw new ApiRequestError(message ?? fallback, {
14073
+ endpoint: error.endpoint,
14074
+ status: error.status,
14075
+ method: error.method,
14076
+ auth: error.auth,
14077
+ requestId: error.requestId,
14078
+ body: error.body,
14079
+ cause: error,
14080
+ errorCode: error.errorCode,
14081
+ retryAfterMs: error.retryAfterMs
14082
+ });
14098
14083
  }
14099
- if (mode === "human" && analysis.sync) {
14100
- output.info(analysis.resultMode === "preview" ? "SDK config preview summary" : "SDK config sync summary", analysis.resultMode === "preview" ? buildPreviewSummary(analysis.sync) : analysis.sync);
14084
+ if (error instanceof Error) {
14085
+ throw error;
14101
14086
  }
14102
- if (mode === "json") {
14103
- output.json({
14104
- success: true,
14105
- ...analysis.resultMode === "preview" ? {
14106
- dryRun: true,
14107
- mode: analysis.resultMode
14108
- } : {},
14109
- sync: analysis.sync
14110
- });
14111
- } else {
14112
- output.success(analysis.resultMode === "preview" ? "Config preview completed. No changes were applied." : "Config pushed successfully!");
14113
- }
14114
- return { success: true };
14115
- }
14116
- /** Main push-config action, retained for `push --with-config`. */
14117
- async function pushConfigAction(output, options) {
14118
- const target = options.target;
14119
- try {
14120
- if (!options.isJsonOutput) {
14121
- output.info("push-config target", {
14122
- env: target,
14123
- dashboardUrl: options.dashboard,
14124
- configPath: options.config ?? DEFAULT_CONFIG_PATH
14125
- });
14126
- output.info("Validating config...");
14127
- }
14128
- const config = await loadValidatedPushConfig(target, options.config);
14129
- const apiKey = resolveManagementApiKey(output, options.apiKey, target);
14130
- const response = await pushToDashboard(options.dashboard, config.siteId, apiKey, config, {
14131
- forceContentTypes: options.forceContentTypes ?? false,
14132
- dryRun: options.dryRun ?? false
14133
- });
14134
- return renderPushConfigResponse(output, response, options.isJsonOutput ? "json" : "human", options.dryRun ?? false);
14135
- } catch (error) {
14136
- handleCommandError(error, output, {
14137
- target,
14138
- dashboardUrl: options.dashboard
14139
- });
14140
- return { success: false };
14141
- }
14142
- }
14143
- function resolveDashboardUrlValue(cliOption, target) {
14144
- const envVar = getEnvVarName("DASHBOARD_URL", target);
14145
- const url = cliOption || process.env[envVar];
14146
- if (!url) {
14147
- return {
14148
- ok: false,
14149
- error: {
14150
- message: "Dashboard URL is required",
14151
- details: { suggestion: `Provide --dashboard <url> or set ${envVar} environment variable.` }
14152
- }
14153
- };
14154
- }
14155
- return {
14156
- ok: true,
14157
- value: url
14158
- };
14159
- }
14160
- /**
14161
- * Resolve management API key from CLI option or environment variable.
14162
- */
14163
- function resolveManagementApiKeyValue(cliOption, target) {
14164
- const envVar = getEnvVarName("MGMT_API_KEY", target);
14165
- const apiKey = cliOption || process.env[envVar];
14166
- if (!apiKey) {
14167
- return {
14168
- ok: false,
14169
- error: {
14170
- message: "Management API key is required",
14171
- details: { suggestion: `Provide --api-key <key> or set ${envVar} environment variable.` }
14172
- }
14173
- };
14174
- }
14175
- const parsedApiKey = parseManagementKey(apiKey);
14176
- if (!parsedApiKey.ok) {
14177
- return {
14178
- ok: false,
14179
- error: {
14180
- message: "Invalid management API key format",
14181
- details: {
14182
- expected: "Key starting with bld_mgmt_sk_",
14183
- suggestion: `Check your ${envVar} environment variable.`
14184
- }
14185
- }
14186
- };
14187
- }
14188
- return {
14189
- ok: true,
14190
- value: parsedApiKey.value
14191
- };
14087
+ throw new Error(fallback);
14192
14088
  }
14193
- function resolveManagementApiKey(output, cliOption, target) {
14194
- const result = resolveManagementApiKeyValue(cliOption, target);
14195
- if (!result.ok) output.error(result.error.message, result.error.details);
14196
- return result.value;
14089
+
14090
+ //#endregion
14091
+ //#region ../api/src/bookingActivity.ts
14092
+ const BOOKING_ACTIVITY_CATEGORY_VALUES = [
14093
+ "booking",
14094
+ "purchase",
14095
+ "cancellation",
14096
+ "refund",
14097
+ "transfer",
14098
+ "failure"
14099
+ ];
14100
+ const BOOKING_ACTIVITY_TYPE_VALUES = [
14101
+ "appointment_booked",
14102
+ "appointment_requested",
14103
+ "appointment_moved",
14104
+ "waitlist_joined",
14105
+ "balance_paid",
14106
+ "appointment_cancelled",
14107
+ "booking_closure_created",
14108
+ "booking_closure_removed",
14109
+ "event_registration",
14110
+ "event_registration_cancelled",
14111
+ "event_series_registration",
14112
+ "event_series_registration_cancelled",
14113
+ "pass_purchased",
14114
+ "membership_started",
14115
+ "membership_paused",
14116
+ "membership_cancelled",
14117
+ "event_refund",
14118
+ "pass_refund",
14119
+ "membership_refund",
14120
+ "event_series_registration_refund",
14121
+ "ticket_transfer",
14122
+ "payment_failed"
14123
+ ];
14124
+
14125
+ //#endregion
14126
+ //#region ../api/src/bookingRecords.ts
14127
+ const CUSTOMER_PASS_STATUS_VALUES = [
14128
+ "active",
14129
+ "exhausted",
14130
+ "expired",
14131
+ "refunded"
14132
+ ];
14133
+ const CUSTOMER_MEMBERSHIP_STATUS_VALUES = [
14134
+ "active",
14135
+ "trialing",
14136
+ "past_due",
14137
+ "paused",
14138
+ "canceled"
14139
+ ];
14140
+ const BOOKING_LEDGER_KIND_VALUES = [
14141
+ "appointment",
14142
+ "event_registration",
14143
+ "event_series_registration"
14144
+ ];
14145
+ const BOOKING_LEDGER_DATE_BASIS_VALUES = [
14146
+ "created_at",
14147
+ "scheduled_at",
14148
+ "cancelled_at"
14149
+ ];
14150
+ const BOOKING_LEDGER_PERIOD_PRESET_VALUES = [
14151
+ "7d",
14152
+ "30d",
14153
+ "90d",
14154
+ "365d",
14155
+ "all"
14156
+ ];
14157
+ const BOOKING_LEDGER_CANCELLATION_MODE_VALUES = [
14158
+ "exclude_cancelled",
14159
+ "include_cancelled",
14160
+ "cancelled_only"
14161
+ ];
14162
+ const BOOKING_QUESTION_ANSWER_EXPORT_SCOPES = {
14163
+ site: "site",
14164
+ appointments: "appointments",
14165
+ eventSeries: "event_series"
14166
+ };
14167
+ const BOOKING_QUESTION_ANSWER_EXPORT_SCOPE_VALUES = [
14168
+ BOOKING_QUESTION_ANSWER_EXPORT_SCOPES.site,
14169
+ BOOKING_QUESTION_ANSWER_EXPORT_SCOPES.appointments,
14170
+ BOOKING_QUESTION_ANSWER_EXPORT_SCOPES.eventSeries
14171
+ ];
14172
+
14173
+ //#endregion
14174
+ //#region ../api/src/bookingPeople.ts
14175
+ const PEOPLE_LIST_SORT_VALUES = [
14176
+ "name",
14177
+ "passes",
14178
+ "memberships",
14179
+ "lastBooking",
14180
+ "added"
14181
+ ];
14182
+ const PEOPLE_LIST_FILTER_VALUES = [
14183
+ "everyone",
14184
+ "hasBought",
14185
+ "hasActivePass",
14186
+ "hasMembership",
14187
+ "guests",
14188
+ "nothingBought"
14189
+ ];
14190
+ /** What can be attached to a person, in the order the dialog lists it. */
14191
+ const PERSON_DELETION_BLOCKER_CATEGORY_VALUES = [
14192
+ "bookings",
14193
+ "guest_places",
14194
+ "waitlist_places",
14195
+ "payments",
14196
+ "orders",
14197
+ "passes",
14198
+ "memberships",
14199
+ "credit",
14200
+ "digital_products",
14201
+ "discounts",
14202
+ "gifts",
14203
+ "notes",
14204
+ "forms",
14205
+ "terms_agreements",
14206
+ "uploads",
14207
+ "contact_corrections"
14208
+ ];
14209
+
14210
+ //#endregion
14211
+ //#region ../api/src/wholeSeriesRuns.ts
14212
+ const DELETE_EVENT_OFFERING_CONFLICT_REASONS = [
14213
+ "has_registrations",
14214
+ "has_payments",
14215
+ "published_page",
14216
+ "inconsistent_ownership"
14217
+ ];
14218
+ function isDeleteEventOfferingConflictReason(value) {
14219
+ return DELETE_EVENT_OFFERING_CONFLICT_REASONS.some((reason) => reason === value);
14197
14220
  }
14198
- function unwrapPushConfigTargetValue(result) {
14199
- if (result.ok) return result.value;
14200
- const suggestion = result.error.details.suggestion;
14201
- throw new Error(`${result.error.message}${typeof suggestion === "string" ? `. ${suggestion}` : ""}`);
14221
+
14222
+ //#endregion
14223
+ //#region ../api/src/bookings.ts
14224
+ const BOOKING_QUESTION_REQUIREMENT_SCOPES = ["before_booking_booking_level", "after_booking_due_by_participant"];
14225
+ function isAfterBookingQuestionRequirementScope(scope) {
14226
+ return scope === "after_booking_due_by_participant";
14202
14227
  }
14203
- async function loadPushConfigTarget(target, options) {
14204
- return {
14205
- target,
14206
- dashboard: unwrapPushConfigTargetValue(resolveDashboardUrlValue(options.dashboard, target)),
14207
- apiKey: unwrapPushConfigTargetValue(resolveManagementApiKeyValue(options.apiKey, target))
14208
- };
14228
+ function isBeforeBookingQuestionRequirementScope(scope) {
14229
+ return scope === "before_booking_booking_level";
14209
14230
  }
14210
- async function preparePushConfigTarget(context, options) {
14211
- const config = await loadValidatedPushConfig(context.target, options.config);
14212
- const preview = await pushToDashboard(context.dashboard, config.siteId, context.apiKey, config, {
14213
- forceContentTypes: options.forceContentTypes ?? false,
14214
- dryRun: true
14215
- });
14216
- if (!analyzePushConfigResponse(preview, true).success) {
14217
- throw new Error(`Config preview failed for ${context.target} environment.`);
14231
+ async function getBookingSettingsRequest(apiClient, siteId, options) {
14232
+ try {
14233
+ const response = unwrapResponse(await apiClient({
14234
+ endpoint: "getBookingSettings",
14235
+ params: { siteId },
14236
+ options
14237
+ }));
14238
+ return response.settings;
14239
+ } catch (error) {
14240
+ normalizeApiError(error, "Failed to load booking settings");
14218
14241
  }
14219
- return {
14220
- config,
14221
- preview,
14222
- forceContentTypes: options.forceContentTypes ?? false
14223
- };
14224
14242
  }
14225
- /**
14226
- * Re-preview a prepared config value without reloading the local config file.
14227
- * The caller uses this immediately before applying a multi-target plan so a
14228
- * dashboard-side schema change is classified as stale before any mutation.
14229
- */
14230
- async function revalidatePushConfigPlan(context, plan) {
14243
+ async function updateBookingSettingsRequest(apiClient, siteId, payload, options) {
14231
14244
  try {
14232
- const current = await pushToDashboard(context.dashboard, plan.config.siteId, context.apiKey, plan.config, {
14233
- forceContentTypes: plan.forceContentTypes,
14234
- dryRun: true
14235
- });
14236
- return stableStringify(current) === stableStringify(plan.preview) ? { ok: true } : {
14237
- ok: false,
14238
- error: new Error("SDK config changed after preflight; the push plan is stale.")
14239
- };
14245
+ const response = unwrapResponse(await apiClient({
14246
+ endpoint: "updateBookingSettings",
14247
+ params: { siteId },
14248
+ body: payload,
14249
+ options
14250
+ }));
14251
+ return response.settings;
14240
14252
  } catch (error) {
14241
- return {
14242
- ok: false,
14243
- error: error instanceof Error ? error : new Error(String(error))
14244
- };
14253
+ normalizeApiError(error, "Failed to save booking settings");
14245
14254
  }
14246
14255
  }
14247
- async function applyPushConfigPlan(output, context, plan, options, isJsonOutput) {
14248
- const response = options.dryRun ? plan.preview : await pushToDashboard(context.dashboard, plan.config.siteId, context.apiKey, plan.config, {
14249
- forceContentTypes: plan.forceContentTypes,
14250
- dryRun: false,
14251
- expectedMailingListProviderScope: plan.preview.mailingListProviderScope
14252
- });
14253
- return {
14254
- response,
14255
- action: isJsonOutput ? { success: analyzePushConfigResponse(response, options.dryRun ?? false).success } : renderPushConfigResponse(output, response, "human", options.dryRun ?? false)
14256
- };
14257
- }
14258
- function isPushConfigApplySuccess(result) {
14259
- return result.action.success;
14260
- }
14261
- function renderPushConfigJsonResult(output, execution, dryRun) {
14262
- output.json({
14263
- success: execution.succeeded,
14264
- dryRun,
14265
- environments: mapEnvironmentExecutionOutcomesJson(execution, ({ result }) => ({
14266
- response: result.response,
14267
- success: true
14268
- }), ({ result, message }) => ({
14269
- response: result.response,
14270
- success: false,
14271
- error: message
14272
- }))
14273
- });
14274
- }
14275
- function getProductionPushConfigCommandDeps() {
14276
- return {
14277
- getOutputContext,
14278
- planEnvironmentExecution,
14279
- executeEnvironmentPlan,
14280
- confirmAction,
14281
- loadTarget: loadPushConfigTarget,
14282
- prepareTarget: preparePushConfigTarget,
14283
- revalidatePlan: revalidatePushConfigPlan,
14284
- applyPlan: applyPushConfigPlan
14285
- };
14286
- }
14287
- /**
14288
- * push-config command definition
14289
- */
14290
- const createPushConfigCommand = (deps = getProductionPushConfigCommandDeps()) => new Command("push-config").description("Push SDK config to dashboard").option("--api-key <key>", "Management API key (or set RIVERBANK_*_MGMT_API_KEY)").option("--dashboard <url>", "Dashboard URL (or set RIVERBANK_*_DASHBOARD_URL env var)").option("--config <path>", `Path to config file (default: ${DEFAULT_CONFIG_PATH})`).option("--force-content-types", "Force overwrite content type schema even if modified in dashboard after last SDK sync").option("--dry-run", "Preview SDK config changes without updating the dashboard").option("--yes", "Confirm applying config changes across all selected environments").addHelpText("after", `
14291
- Description:
14292
- Syncs your local riverbank.config.ts schema/config to the CMS dashboard, including:
14293
- - Custom blocks
14294
- - Block field extensions
14295
- - Block field options
14296
- - Content type definitions and templates
14297
- - SDK-managed site settings and footer blocks
14298
-
14299
- Pages, content entries, and navigation menus are content documents.
14300
- Sync them with "riverbankcms push pages", "riverbankcms push entries",
14301
- or "riverbankcms push navigation".
14302
-
14303
- Examples:
14304
- $ pnpm exec riverbankcms push-config
14305
- $ pnpm exec riverbankcms push-config --dry-run
14306
- $ pnpm exec riverbankcms push-config --env=remote
14307
- $ pnpm exec riverbankcms push-config --api-key bld_mgmt_sk_... --dashboard https://www.riverbankcms.com
14308
- $ pnpm exec riverbankcms push-config --config ./src/riverbank.config.ts
14309
- `).action(async (options, command) => {
14310
- const { output, selection, globalOpts } = deps.getOutputContext(command);
14311
- const isJsonOutput = globalOpts.json ?? false;
14312
- const authorization = requireEnvironmentWriteAuthorization({
14313
- ...options,
14314
- json: isJsonOutput,
14315
- interactive: process.stdin.isTTY === true
14316
- }, output);
14317
- if (!authorization) return;
14318
- const planned = deps.planEnvironmentExecution({
14319
- command: "push-config",
14320
- selection: "both",
14321
- failure: "abort",
14322
- confirmation: "command",
14323
- revalidation: "required"
14324
- }, selection);
14325
- if (!planned.ok) return output.error(planned.error.message, { suggestion: planned.error.suggestion });
14326
- const execution = await deps.executeEnvironmentPlan(planned.value, {
14327
- loadTarget: (target) => deps.loadTarget(target, options),
14328
- preflight: async (_target, loaded) => deps.prepareTarget(loaded.context, {
14329
- ...options,
14330
- dashboard: loaded.context.dashboard
14331
- }),
14332
- confirm: async ({ ready }) => {
14333
- if (authorization.kind === "preauthorized") return true;
14334
- for (const { target, context } of ready) {
14335
- const sync = getPushConfigSync(context.preview);
14336
- output.info(`${target} config preview`, sync ? buildPreviewSummary(sync) : { changes: "No sync summary returned" });
14337
- }
14338
- return deps.confirmAction(`Apply SDK config to ${ready.length} environment${ready.length === 1 ? "" : "s"} (${ready.map(({ target }) => target).join(" then ")})?`, { yes: options.yes }, false);
14339
- },
14340
- revalidate: async (_target, loaded, prepared) => options.dryRun ? { ok: true } : deps.revalidatePlan(loaded.context, prepared.context),
14341
- apply: async (target, loaded, prepared) => {
14342
- if (planned.value.targets.length > 1) output.info(`\n--- Pushing to ${target} environment ---`);
14343
- return deps.applyPlan(output, loaded.context, prepared.context, {
14344
- ...options,
14345
- dashboard: loaded.context.dashboard
14346
- }, isJsonOutput);
14347
- },
14348
- isSuccess: isPushConfigApplySuccess,
14349
- formatFailure: formatPushConfigActionFailure
14350
- }, output);
14351
- if (isJsonOutput) renderPushConfigJsonResult(output, execution, options.dryRun ?? false);
14352
- });
14353
-
14354
- //#endregion
14355
- //#region ../api/src/utils/isRecord.ts
14356
- function isRecord$5(value) {
14357
- return typeof value === "object" && value !== null && !Array.isArray(value);
14358
- }
14359
-
14360
- //#endregion
14361
- //#region ../api/src/utils/error-propagation.ts
14362
- /**
14363
- * Extracts an error message from an API error response body.
14364
- * Checks for our standard API error format: { error: { message: "..." } }
14365
- * Also handles legacy formats with `error` or `message` as strings.
14366
- */
14367
- function extractApiErrorMessage(body) {
14368
- if (!isRecord$5(body)) {
14369
- return null;
14370
- }
14371
- if (isRecord$5(body.error)) {
14372
- if (typeof body.error.message === "string" && body.error.message.trim().length > 0) {
14373
- return body.error.message;
14374
- }
14256
+ async function createAppointmentRequest(apiClient, siteId, payload, options) {
14257
+ try {
14258
+ return unwrapResponse(await apiClient({
14259
+ endpoint: "createAppointment",
14260
+ params: { siteId },
14261
+ body: payload,
14262
+ options
14263
+ }));
14264
+ } catch (error) {
14265
+ normalizeApiError(error, "Failed to create appointment");
14375
14266
  }
14376
- if (typeof body.error === "string" && body.error.trim().length > 0) {
14377
- return body.error;
14267
+ }
14268
+ async function deleteEventOfferingRequest(apiClient, siteId, eventOfferingId, options) {
14269
+ try {
14270
+ return unwrapResponse(await apiClient({
14271
+ endpoint: "deleteEventOffering",
14272
+ params: {
14273
+ siteId,
14274
+ eventOfferingId
14275
+ },
14276
+ options
14277
+ }));
14278
+ } catch (error) {
14279
+ normalizeApiError(error, "Failed to delete event offering");
14378
14280
  }
14379
- if (typeof body.message === "string" && body.message.trim().length > 0) {
14380
- return body.message;
14281
+ }
14282
+ async function addEventSeriesRegistrationRequest(apiClient, siteId, eventSeriesId, payload, options) {
14283
+ try {
14284
+ return unwrapResponse(await apiClient({
14285
+ endpoint: "addEventSeriesRegistration",
14286
+ params: {
14287
+ siteId,
14288
+ seriesId: eventSeriesId
14289
+ },
14290
+ body: payload,
14291
+ options
14292
+ }));
14293
+ } catch (error) {
14294
+ normalizeApiError(error, "Failed to add Event run registration");
14381
14295
  }
14382
- return null;
14383
14296
  }
14384
- /**
14385
- * Normalizes various error types and throws with an appropriate message.
14386
- *
14387
- * - For ApiRequestError: extracts message from body.error or body.message
14388
- * - For Error instances: re-throws as-is
14389
- * - For unknown errors: wraps with fallback message
14390
- *
14391
- * @param error - The error to normalize
14392
- * @param fallback - Fallback message if no specific message can be extracted
14393
- * @throws {Error} Always throws - this function never returns
14394
- */
14395
- function normalizeApiError(error, fallback) {
14396
- if (error instanceof ApiRequestError) {
14397
- const message = extractApiErrorMessage(error.body);
14398
- throw new ApiRequestError(message ?? fallback, {
14399
- endpoint: error.endpoint,
14400
- status: error.status,
14401
- method: error.method,
14402
- auth: error.auth,
14403
- requestId: error.requestId,
14404
- body: error.body,
14405
- cause: error,
14406
- errorCode: error.errorCode,
14407
- retryAfterMs: error.retryAfterMs
14408
- });
14297
+ async function updateEventSeriesRegistrationRequest(apiClient, siteId, eventSeriesId, registrationId, payload, options) {
14298
+ try {
14299
+ return unwrapResponse(await apiClient({
14300
+ endpoint: "updateEventSeriesRegistration",
14301
+ params: {
14302
+ siteId,
14303
+ seriesId: eventSeriesId,
14304
+ registrationId
14305
+ },
14306
+ body: payload,
14307
+ options
14308
+ }));
14309
+ } catch (error) {
14310
+ normalizeApiError(error, "Failed to update Event run registration");
14409
14311
  }
14410
- if (error instanceof Error) {
14411
- throw error;
14312
+ }
14313
+ async function addEventOccurrenceAttendeeRequest(apiClient, siteId, seriesId, occurrenceId, payload, options) {
14314
+ try {
14315
+ return unwrapResponse(await apiClient({
14316
+ endpoint: "addEventOccurrenceAttendee",
14317
+ params: {
14318
+ siteId,
14319
+ seriesId,
14320
+ occurrenceId
14321
+ },
14322
+ body: payload,
14323
+ options
14324
+ }));
14325
+ } catch (error) {
14326
+ normalizeApiError(error, "Failed to add attendee");
14412
14327
  }
14413
- throw new Error(fallback);
14414
14328
  }
14415
14329
 
14416
14330
  //#endregion
14417
- //#region ../api/src/navigationMenuValidation.ts
14418
- function hasChildren(item) {
14419
- return Array.isArray(item.children) && item.children.length > 0;
14331
+ //#region src/push-config-sync.ts
14332
+ function isConfigSync(sync) {
14333
+ return "ignoredDocumentSections" in sync;
14420
14334
  }
14421
- function isCartLinkMissingVisibleLabel(link$1) {
14422
- return link$1?.kind === "cart" && link$1.label?.kind === "icon-with-label" && (typeof link$1.label.label !== "string" || link$1.label.label.trim().length === 0);
14335
+ function getPushConfigSync(result) {
14336
+ if (result.configSync) return result.configSync;
14337
+ if (result.sync) return result.sync;
14338
+ return null;
14339
+ }
14340
+ function collectPushConfigSyncErrors(sync) {
14341
+ return [
14342
+ ...sync.contentTypes.errors.map((message) => ({
14343
+ area: "contentTypes",
14344
+ message
14345
+ })),
14346
+ ...sync.customBlocks.errors.map((message) => ({
14347
+ area: "customBlocks",
14348
+ message
14349
+ })),
14350
+ ...sync.templateBlocks.errors.map((message) => ({
14351
+ area: "templateBlocks",
14352
+ message
14353
+ })),
14354
+ ...sync.settings.error ? [{
14355
+ area: "settings",
14356
+ message: sync.settings.error
14357
+ }] : [],
14358
+ ...!isConfigSync(sync) ? sync.pages.errors.map((message) => ({
14359
+ area: "pages",
14360
+ message
14361
+ })) : [],
14362
+ ...!isConfigSync(sync) ? sync.entries.errors.map((message) => ({
14363
+ area: "entries",
14364
+ message
14365
+ })) : [],
14366
+ ...!isConfigSync(sync) ? sync.navigationMenus.errors.map((message) => ({
14367
+ area: "navigationMenus",
14368
+ message
14369
+ })) : []
14370
+ ];
14371
+ }
14372
+ function collectPushConfigSyncWarnings(sync) {
14373
+ return [
14374
+ ...sync.contentTypes.warnings.map((message) => ({
14375
+ area: "contentTypes",
14376
+ message
14377
+ })),
14378
+ ...sync.customBlocks.warnings.map((message) => ({
14379
+ area: "customBlocks",
14380
+ message
14381
+ })),
14382
+ ...sync.templateBlocks.warnings.map((message) => ({
14383
+ area: "templateBlocks",
14384
+ message
14385
+ })),
14386
+ ...isConfigSync(sync) ? sync.ignoredDocumentSections.map((section) => ({
14387
+ area: "ignoredDocuments",
14388
+ message: formatIgnoredDocumentSyncSectionWarning(section)
14389
+ })) : [
14390
+ ...sync.pages.warnings.map((message) => ({
14391
+ area: "pages",
14392
+ message
14393
+ })),
14394
+ ...sync.entries.warnings.map((message) => ({
14395
+ area: "entries",
14396
+ message
14397
+ })),
14398
+ ...sync.navigationMenus.warnings.map((message) => ({
14399
+ area: "navigationMenus",
14400
+ message
14401
+ }))
14402
+ ]
14403
+ ];
14404
+ }
14405
+ function groupPushConfigSyncWarningsForHuman(warnings) {
14406
+ const ignoredDocumentCount = warnings.filter((warning) => warning.area === "ignoredDocuments").length;
14407
+ return [...warnings.filter((warning) => warning.area !== "ignoredDocuments"), ...ignoredDocumentCount > 0 ? [{
14408
+ area: "ignoredDocuments",
14409
+ message: `${ignoredDocumentCount} document content section${ignoredDocumentCount === 1 ? "" : "s"} ignored by push-config. Use \`riverbankcms push\` for pages, entries, and navigation content.`
14410
+ }] : []];
14411
+ }
14412
+ function buildPreviewSummary(sync) {
14413
+ return { wouldConsider: {
14414
+ contentTypes: sync.contentTypes.skipped,
14415
+ customBlocks: sync.customBlocks.skipped,
14416
+ templateBlocks: sync.templateBlocks.skipped,
14417
+ settings: sync.settings.updated,
14418
+ ...isConfigSync(sync) ? { footerBlocks: sync.footerBlocks.skipped } : {},
14419
+ ...isConfigSync(sync) ? { ignoredDocumentSections: sync.ignoredDocumentSections } : {}
14420
+ } };
14423
14421
  }
14422
+
14423
+ //#endregion
14424
+ //#region src/push-config.ts
14424
14425
  /**
14425
- * Recursively collects portal-link constraints for a single menu tree.
14426
- * Rules:
14427
- * - At most one portal link per menu.
14428
- * - Portal links must be root-level.
14429
- * - Portal links cannot have children.
14426
+ * push-config CLI command
14427
+ *
14428
+ * Pushes SDK site configuration from a riverbank.config.ts file to the dashboard API.
14430
14429
  */
14431
- function collectPortalLinkStats(items, depth = 0) {
14432
- const result = {
14433
- count: 0,
14434
- nestedLabels: [],
14435
- withChildrenLabels: []
14430
+ const DEFAULT_CONFIG_PATH = "./riverbank.config.ts";
14431
+ function formatPushConfigActionFailure() {
14432
+ return "push-config failed";
14433
+ }
14434
+ function isRecord$6(value) {
14435
+ return typeof value === "object" && value !== null;
14436
+ }
14437
+ function parseErrorDetails(value) {
14438
+ if (!value) return {};
14439
+ if (typeof value === "string") {
14440
+ return { message: value };
14441
+ }
14442
+ if (!isRecord$6(value)) return {};
14443
+ const message = typeof value.message === "string" ? value.message : undefined;
14444
+ const fieldErrorsRaw = value.fieldErrors;
14445
+ const fieldErrors = Array.isArray(fieldErrorsRaw) ? fieldErrorsRaw.filter(isRecord$6).map((d) => ({
14446
+ field: typeof d.field === "string" ? d.field : "",
14447
+ message: typeof d.message === "string" ? d.message : ""
14448
+ })).filter((d) => d.field.length > 0 && d.message.length > 0) : undefined;
14449
+ return {
14450
+ message,
14451
+ fieldErrors
14436
14452
  };
14437
- for (const item of items) {
14438
- const isPortal = item.link?.kind === "portal";
14439
- if (isPortal) {
14440
- result.count += 1;
14441
- if (depth > 0) {
14442
- result.nestedLabels.push(item.label);
14443
- }
14444
- if (hasChildren(item)) {
14445
- result.withChildrenLabels.push(item.label);
14446
- }
14447
- }
14448
- if (hasChildren(item)) {
14449
- const childItems = (item.children ?? []).filter(isNavigationMenuValidationItem);
14450
- const child = collectPortalLinkStats(childItems, depth + 1);
14451
- result.count += child.count;
14452
- result.nestedLabels.push(...child.nestedLabels);
14453
- result.withChildrenLabels.push(...child.withChildrenLabels);
14454
- }
14453
+ }
14454
+ function formatFieldErrors(fieldErrors) {
14455
+ if (!fieldErrors || fieldErrors.length === 0) return "";
14456
+ return ":\n" + fieldErrors.map((d) => ` - ${d.field}: ${d.message}`).join("\n");
14457
+ }
14458
+ function getDashboardErrorMessage(responseJson, fallback) {
14459
+ if (!responseJson) return fallback;
14460
+ if (isRecord$6(responseJson) && "error" in responseJson) {
14461
+ const parsed = parseErrorDetails(responseJson.error);
14462
+ const msg = parsed.message ?? fallback;
14463
+ return msg + formatFieldErrors(parsed.fieldErrors);
14455
14464
  }
14456
- return result;
14465
+ return fallback;
14457
14466
  }
14458
14467
  /**
14459
- * Recursively collects cart-link constraints for a single menu tree.
14460
- * Rules:
14461
- * - Cart links must be root-level.
14462
- * - Cart links cannot have children.
14463
- * - Cart links cannot be CTA items.
14464
- * - Cart links using `icon-with-label` must provide visible text.
14468
+ * Push config to dashboard API
14465
14469
  */
14466
- function collectCartLinkStats(items, depth = 0) {
14467
- const result = {
14468
- count: 0,
14469
- nestedLabels: [],
14470
- withChildrenLabels: [],
14471
- ctaLabels: [],
14472
- missingVisibleTextLabels: []
14470
+ async function pushToDashboard(dashboardUrl, siteId, apiKey, config, options) {
14471
+ const normalizedDashboardUrl = dashboardUrl.replace(/\/+$/, "");
14472
+ const baseUrl = `${normalizedDashboardUrl}/api`;
14473
+ const path$3 = `/sites/${siteId}/sdk-config`;
14474
+ const pushUrl = `${baseUrl}${path$3}`;
14475
+ const requestInfo = {
14476
+ method: "POST",
14477
+ intent: "write",
14478
+ path: path$3,
14479
+ url: pushUrl,
14480
+ baseUrl,
14481
+ dashboardUrl: normalizedDashboardUrl,
14482
+ siteId
14473
14483
  };
14474
- for (const item of items) {
14475
- const isCart = item.link?.kind === "cart";
14476
- if (isCart) {
14477
- result.count += 1;
14478
- if (depth > 0) {
14479
- result.nestedLabels.push(item.label);
14480
- }
14481
- if (hasChildren(item)) {
14482
- result.withChildrenLabels.push(item.label);
14483
- }
14484
- if (item.isCta === true) {
14485
- result.ctaLabels.push(item.label);
14486
- }
14487
- if (isCartLinkMissingVisibleLabel(item.link ?? null)) {
14488
- result.missingVisibleTextLabels.push(item.label);
14489
- }
14484
+ const client = createBearerAPIClient(exposeSecret(apiKey), baseUrl);
14485
+ const timeoutMs = currentRequestTimeoutMs() ?? 3e4;
14486
+ try {
14487
+ return unwrapResponse(await client({
14488
+ endpoint: "pushSdkConfig",
14489
+ params: { siteId },
14490
+ body: {
14491
+ config,
14492
+ forceContentTypes: options?.forceContentTypes ?? false,
14493
+ mode: options?.dryRun ? "preview" : "apply",
14494
+ expectedMailingListProviderScope: options?.expectedMailingListProviderScope ?? undefined
14495
+ },
14496
+ options: { signal: AbortSignal.timeout(timeoutMs) }
14497
+ }));
14498
+ } catch (error) {
14499
+ if (error instanceof ApiEnvelopeError) {
14500
+ const details = {
14501
+ success: false,
14502
+ error: {
14503
+ code: error.code,
14504
+ message: error.message,
14505
+ fieldErrors: error.fieldErrors
14506
+ }
14507
+ };
14508
+ const errorMessage = getDashboardErrorMessage(details, error.message);
14509
+ throw new ManagementApiError(errorMessage, "sdk:http-error", details, error.status, requestInfo);
14490
14510
  }
14491
- if (hasChildren(item)) {
14492
- const childItems = (item.children ?? []).filter(isNavigationMenuValidationItem);
14493
- const child = collectCartLinkStats(childItems, depth + 1);
14494
- result.count += child.count;
14495
- result.nestedLabels.push(...child.nestedLabels);
14496
- result.withChildrenLabels.push(...child.withChildrenLabels);
14497
- result.ctaLabels.push(...child.ctaLabels);
14498
- result.missingVisibleTextLabels.push(...child.missingVisibleTextLabels);
14511
+ if (error instanceof ApiRequestError) {
14512
+ const errorMessage = getDashboardErrorMessage(error.body, `Dashboard returned ${error.status}`);
14513
+ throw new ManagementApiError(errorMessage, "sdk:http-error", error.body, error.status, requestInfo);
14499
14514
  }
14515
+ throw managementTransportError(error, timeoutMs, requestInfo);
14500
14516
  }
14501
- return result;
14502
- }
14503
- function isNavigationMenuValidationItem(value) {
14504
- if (!isRecord$5(value)) return false;
14505
- return typeof value.label === "string";
14506
14517
  }
14507
-
14508
- //#endregion
14509
- //#region ../api/src/navigation/bookingPortalLink.ts
14510
- const YOUR_BOOKINGS_LABEL = "Your bookings";
14511
- /**
14512
- * The booking page's link to the customer portal, in the header and the
14513
- * footer alike. It is a portal link, so renderers apply the portal link rules
14514
- * (including the portal-enabled gate); the label stays the same signed in.
14515
- */
14516
- const YOUR_BOOKINGS_PORTAL_LINK = {
14517
- kind: "portal",
14518
- labels: {
14519
- loggedOut: YOUR_BOOKINGS_LABEL,
14520
- loggedIn: YOUR_BOOKINGS_LABEL
14518
+ function validatePushConfig(rawConfig) {
14519
+ const parsed = riverbankSiteConfigSchema.safeParse(rawConfig);
14520
+ if (!parsed.success) {
14521
+ const issues = parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
14522
+ throw new Error(`Invalid config: ${issues}`);
14521
14523
  }
14522
- };
14523
-
14524
- //#endregion
14525
- //#region ../api/src/navigation/linkResolver.ts
14526
- /**
14527
- * Type guard for internal links.
14528
- * Checks if a link payload is an internal link with entityId and entityType.
14529
- */
14530
- function isInternalLink(link$1) {
14531
- if (!link$1 || typeof link$1 !== "object") {
14532
- return false;
14524
+ const conflicts = validateFieldIdConflicts(parsed.data.blockFieldExtensions);
14525
+ if (conflicts.length > 0) {
14526
+ throw new Error(`Field ID conflicts detected in blockFieldExtensions: ${conflicts.map((conflict) => conflict.message).join("; ")}`);
14533
14527
  }
14534
- return link$1.kind === "internal" && "entityId" in link$1 && "entityType" in link$1 && typeof link$1.entityId === "string" && typeof link$1.entityType === "string";
14535
- }
14536
- /**
14537
- * Narrow internal link to page type.
14538
- */
14539
- function isPageLink(link$1) {
14540
- return link$1.entityType === "page";
14528
+ return parsed.data;
14541
14529
  }
14542
- /**
14543
- * Narrow internal link to content/entry type.
14544
- */
14545
- function isEntryLink(link$1) {
14546
- return link$1.entityType === "content";
14530
+ async function loadValidatedPushConfig(target, configPath) {
14531
+ return validatePushConfig(await withRiverbankEnv(target, () => loadConfigFile(configPath)));
14547
14532
  }
14548
- /**
14549
- * Resolve a navigation link input to URL type and link payload.
14550
- *
14551
- * Supports identifier-based page/entry links and validates them against routable
14552
- * lookup maps before persisting.
14553
- *
14554
- * @param link - The navigation link input from SDK config or API
14555
- * @param options - Resolution options (isDropdown flag)
14556
- * @param pageMap - Map of page identifier -> PageNavLinkInfo
14557
- * @param entryMap - Map of `contentType:identifier` -> EntryNavLinkInfo
14558
- * @returns Resolved link with urlType, url payload, and any error
14559
- */
14560
- function resolveNavigationLinkInput(link$1, options, pageMap, entryMap) {
14561
- if (options.isDropdown) {
14562
- return {
14563
- urlType: "dropdown",
14564
- url: null,
14565
- error: null
14566
- };
14567
- }
14568
- if (!link$1) {
14533
+ function analyzePushConfigResponse(response, dryRun) {
14534
+ const resultMode = response.mode ?? "apply";
14535
+ if (dryRun && resultMode !== "preview") {
14569
14536
  return {
14570
- urlType: "url",
14571
- url: null,
14572
- error: "Link is required for non-dropdown items"
14537
+ success: false,
14538
+ kind: "unsafe-preview",
14539
+ receivedMode: response.mode
14573
14540
  };
14574
14541
  }
14575
- switch (link$1.kind) {
14576
- case "page": return resolvePageLink(link$1.identifier, pageMap);
14577
- case "entry": return resolveEntryLink(link$1.contentType, link$1.identifier, entryMap);
14578
- case "external": return {
14579
- urlType: "external",
14580
- url: {
14581
- kind: "external",
14582
- href: link$1.href
14583
- },
14584
- error: null
14585
- };
14586
- case "url": return {
14587
- urlType: "url",
14588
- url: {
14589
- kind: "url",
14590
- href: link$1.href
14591
- },
14592
- error: null
14593
- };
14594
- case "portal": return {
14595
- urlType: "portal",
14596
- url: {
14597
- kind: "portal",
14598
- ...link$1.path ? { path: link$1.path } : {},
14599
- ...link$1.labels ? { labels: link$1.labels } : {}
14600
- },
14601
- error: null
14602
- };
14603
- case "cart": return {
14604
- urlType: "cart",
14605
- url: {
14606
- kind: "cart",
14607
- badge: link$1.badge,
14608
- label: link$1.label
14609
- },
14610
- error: null
14611
- };
14612
- default: return {
14613
- urlType: "url",
14614
- url: null,
14615
- error: "Unknown link type"
14616
- };
14542
+ const sync = getPushConfigSync(response);
14543
+ if (!sync) return {
14544
+ success: true,
14545
+ resultMode,
14546
+ sync: null,
14547
+ warnings: []
14548
+ };
14549
+ const errors = collectPushConfigSyncErrors(sync);
14550
+ if (errors.length > 0 || sync.success === false) return {
14551
+ success: false,
14552
+ kind: "sync-errors",
14553
+ sync,
14554
+ errors
14555
+ };
14556
+ return {
14557
+ success: true,
14558
+ resultMode,
14559
+ sync,
14560
+ warnings: collectPushConfigSyncWarnings(sync)
14561
+ };
14562
+ }
14563
+ function renderPushConfigResponse(output, response, mode, dryRun) {
14564
+ const analysis = analyzePushConfigResponse(response, dryRun);
14565
+ if (!analysis.success) {
14566
+ if (analysis.kind === "unsafe-preview") {
14567
+ output.fail("Config preview was not confirmed by the dashboard; no safe dry-run result is available.", {
14568
+ expectedMode: "preview",
14569
+ receivedMode: analysis.receivedMode ?? null,
14570
+ suggestion: "Update the dashboard API before relying on push-config --dry-run."
14571
+ });
14572
+ return { success: false };
14573
+ }
14574
+ if (mode === "json") {
14575
+ output.markFailed();
14576
+ output.json({
14577
+ success: false,
14578
+ sync: analysis.sync,
14579
+ errors: analysis.errors
14580
+ });
14581
+ } else {
14582
+ output.markFailed();
14583
+ output.fail("Config pushed, but SDK config sync reported errors", {
14584
+ errors: analysis.errors,
14585
+ summary: analysis.sync
14586
+ });
14587
+ }
14588
+ return { success: false };
14589
+ }
14590
+ if (mode === "human" && analysis.warnings.length > 0) {
14591
+ output.warn("SDK config sync reported warnings", { warnings: groupPushConfigSyncWarningsForHuman(analysis.warnings) });
14592
+ }
14593
+ if (mode === "human" && analysis.sync) {
14594
+ output.info(analysis.resultMode === "preview" ? "SDK config preview summary" : "SDK config sync summary", analysis.resultMode === "preview" ? buildPreviewSummary(analysis.sync) : analysis.sync);
14595
+ }
14596
+ if (mode === "json") {
14597
+ output.json({
14598
+ success: true,
14599
+ ...analysis.resultMode === "preview" ? {
14600
+ dryRun: true,
14601
+ mode: analysis.resultMode
14602
+ } : {},
14603
+ sync: analysis.sync
14604
+ });
14605
+ } else {
14606
+ output.success(analysis.resultMode === "preview" ? "Config preview completed. No changes were applied." : "Config pushed successfully!");
14617
14607
  }
14608
+ return { success: true };
14618
14609
  }
14619
- /**
14620
- * Resolve a page identifier to a canonical page link payload.
14621
- */
14622
- function resolvePageLink(identifier, pageMap) {
14623
- const pageInfo = pageMap.get(identifier);
14624
- if (!pageInfo) {
14610
+ /** Main push-config action, retained for `push --with-config`. */
14611
+ async function pushConfigAction(output, options) {
14612
+ const target = options.target;
14613
+ try {
14614
+ if (!options.isJsonOutput) {
14615
+ output.info("push-config target", {
14616
+ env: target,
14617
+ dashboardUrl: options.dashboard,
14618
+ configPath: options.config ?? DEFAULT_CONFIG_PATH
14619
+ });
14620
+ output.info("Validating config...");
14621
+ }
14622
+ const config = await loadValidatedPushConfig(target, options.config);
14623
+ const apiKey = resolveManagementApiKey(output, options.apiKey, target);
14624
+ const response = await pushToDashboard(options.dashboard, config.siteId, apiKey, config, {
14625
+ forceContentTypes: options.forceContentTypes ?? false,
14626
+ dryRun: options.dryRun ?? false
14627
+ });
14628
+ return renderPushConfigResponse(output, response, options.isJsonOutput ? "json" : "human", options.dryRun ?? false);
14629
+ } catch (error) {
14630
+ handleCommandError(error, output, {
14631
+ target,
14632
+ dashboardUrl: options.dashboard
14633
+ });
14634
+ return { success: false };
14635
+ }
14636
+ }
14637
+ function resolveDashboardUrlValue(cliOption, target) {
14638
+ const envVar = getEnvVarName("DASHBOARD_URL", target);
14639
+ const url = cliOption || process.env[envVar];
14640
+ if (!url) {
14625
14641
  return {
14626
- urlType: "internal",
14627
- url: null,
14628
- error: `Page '${identifier}' not found`
14642
+ ok: false,
14643
+ error: {
14644
+ message: "Dashboard URL is required",
14645
+ details: { suggestion: `Provide --dashboard <url> or set ${envVar} environment variable.` }
14646
+ }
14629
14647
  };
14630
14648
  }
14631
14649
  return {
14632
- urlType: "internal",
14633
- url: {
14634
- kind: "page",
14635
- identifier
14636
- },
14637
- error: null
14650
+ ok: true,
14651
+ value: url
14638
14652
  };
14639
14653
  }
14640
14654
  /**
14641
- * Resolve an entry identifier to a canonical entry link payload.
14655
+ * Resolve management API key from CLI option or environment variable.
14642
14656
  */
14643
- function resolveEntryLink(contentType, identifier, entryMap) {
14644
- const entryInfo = entryMap.get(`${contentType}:${identifier}`);
14645
- if (!entryInfo) {
14657
+ function resolveManagementApiKeyValue(cliOption, target) {
14658
+ const envVar = getEnvVarName("MGMT_API_KEY", target);
14659
+ const apiKey = cliOption || process.env[envVar];
14660
+ if (!apiKey) {
14646
14661
  return {
14647
- urlType: "internal",
14648
- url: null,
14649
- error: `Entry '${contentType}:${identifier}' not found or not routable`
14662
+ ok: false,
14663
+ error: {
14664
+ message: "Management API key is required",
14665
+ details: { suggestion: `Provide --api-key <key> or set ${envVar} environment variable.` }
14666
+ }
14650
14667
  };
14651
14668
  }
14652
- return {
14653
- urlType: "internal",
14654
- url: {
14655
- kind: "entry",
14656
- contentType,
14657
- identifier
14658
- },
14659
- error: null
14660
- };
14661
- }
14662
-
14663
- //#endregion
14664
- //#region ../api/src/navigation/boundaries.ts
14665
- function isNavigationDropdownSentinel(link$1) {
14666
- return link$1?.kind === "dropdown";
14667
- }
14668
- function normalizeNavigationConfigItemForCommand(item) {
14669
- const commandItem = { label: item.label };
14670
- if (item.isCta !== undefined) {
14671
- commandItem.isCta = item.isCta;
14672
- }
14673
- const commandLink = normalizeNavigationAuthoringLinkForCommand(item.link);
14674
- if (commandLink) {
14675
- commandItem.link = commandLink;
14676
- }
14677
- if (item.children && item.children.length > 0) {
14678
- commandItem.children = item.children.map(normalizeNavigationConfigItemForCommand);
14669
+ const parsedApiKey = parseManagementKey(apiKey);
14670
+ if (!parsedApiKey.ok) {
14671
+ return {
14672
+ ok: false,
14673
+ error: {
14674
+ message: "Invalid management API key format",
14675
+ details: {
14676
+ expected: "Key starting with bld_mgmt_sk_",
14677
+ suggestion: `Check your ${envVar} environment variable.`
14678
+ }
14679
+ }
14680
+ };
14679
14681
  }
14680
- return commandItem;
14681
- }
14682
- function navigationCommandItemToManagementInput(item) {
14683
- const link$1 = normalizeNavigationAuthoringLinkForCommand(item.link);
14684
14682
  return {
14685
- label: item.label,
14686
- ...link$1 ? { link: link$1 } : {},
14687
- ...item.isCta !== undefined ? { isCta: item.isCta } : {},
14688
- ...item.children && item.children.length > 0 ? { children: item.children.map(navigationCommandItemToManagementInput) } : {}
14689
- };
14690
- }
14691
- function normalizeNavigationAuthoringLinkForCommand(link$1) {
14692
- if (!link$1 || isNavigationDropdownSentinel(link$1)) return undefined;
14693
- return navigationAuthoringLinkToCommandLink(link$1);
14694
- }
14695
- function navigationApiItemToSdkLocalItem(item) {
14696
- const link$1 = navigationReadLinkToLocalLink(item);
14697
- const localItem = {
14698
- label: item.label,
14699
- ...item.isCta ? { isCta: true } : {},
14700
- ...link$1 ? { link: link$1 } : {},
14701
- ...item.children && item.children.length > 0 ? { children: item.children.map(navigationApiItemToSdkLocalItem) } : {}
14683
+ ok: true,
14684
+ value: parsedApiKey.value
14702
14685
  };
14703
- return localItem;
14704
- }
14705
- function navigationAuthoringLinkToCommandLink(link$1) {
14706
- switch (link$1.kind) {
14707
- case "page": return {
14708
- kind: "page",
14709
- identifier: link$1.identifier
14710
- };
14711
- case "entry": return {
14712
- kind: "entry",
14713
- contentType: link$1.contentType,
14714
- identifier: link$1.identifier
14715
- };
14716
- case "external": return {
14717
- kind: "external",
14718
- href: link$1.href
14719
- };
14720
- case "url": return {
14721
- kind: "url",
14722
- href: link$1.href
14723
- };
14724
- case "portal": return {
14725
- kind: "portal",
14726
- ...link$1.path ? { path: link$1.path } : {},
14727
- ...portalLabelsForCommand(link$1.labels)
14728
- };
14729
- case "cart": return {
14730
- kind: "cart",
14731
- badge: link$1.badge,
14732
- label: link$1.label
14733
- };
14734
- default: return assertNever(link$1);
14735
- }
14736
14686
  }
14737
- function portalLabelsForCommand(labels) {
14738
- if (!labels) return {};
14739
- const commandLabels = {
14740
- ...typeof labels.loggedOut === "string" ? { loggedOut: labels.loggedOut } : {},
14741
- ...typeof labels.loggedIn === "string" ? { loggedIn: labels.loggedIn } : {}
14742
- };
14743
- return Object.keys(commandLabels).length > 0 ? { labels: commandLabels } : {};
14687
+ function resolveManagementApiKey(output, cliOption, target) {
14688
+ const result = resolveManagementApiKeyValue(cliOption, target);
14689
+ if (!result.ok) output.error(result.error.message, result.error.details);
14690
+ return result.value;
14744
14691
  }
14745
- function navigationReadLinkToLocalLink(item) {
14746
- const { link: link$1 } = item;
14747
- if (!link$1) return undefined;
14748
- switch (link$1.kind) {
14749
- case "page": return {
14750
- kind: "page",
14751
- identifier: link$1.identifier
14752
- };
14753
- case "entry": return {
14754
- kind: "entry",
14755
- contentType: link$1.contentType,
14756
- identifier: link$1.identifier
14757
- };
14758
- case "internal": return internalNavigationLinkToLocalLink(item);
14759
- case "external": return {
14760
- kind: "external",
14761
- href: link$1.href
14762
- };
14763
- case "url": return {
14764
- kind: "url",
14765
- href: link$1.href
14766
- };
14767
- case "portal": return {
14768
- kind: "portal",
14769
- ...link$1.path ? { path: link$1.path } : {},
14770
- ...link$1.labels ? { labels: link$1.labels } : {}
14771
- };
14772
- case "cart": return {
14773
- kind: "cart",
14774
- badge: link$1.badge,
14775
- label: link$1.label
14776
- };
14777
- default: return assertNever(link$1);
14778
- }
14692
+ function unwrapPushConfigTargetValue(result) {
14693
+ if (result.ok) return result.value;
14694
+ const suggestion = result.error.details.suggestion;
14695
+ throw new Error(`${result.error.message}${typeof suggestion === "string" ? `. ${suggestion}` : ""}`);
14779
14696
  }
14780
- function internalNavigationLinkToLocalLink(item) {
14781
- const { link: link$1 } = item;
14782
- if (!link$1 || link$1.kind !== "internal") return undefined;
14783
- if (item.resolvedIdentifier) {
14784
- if (link$1.entityType === "page") {
14785
- return {
14786
- kind: "page",
14787
- identifier: item.resolvedIdentifier
14788
- };
14789
- }
14790
- if (link$1.entityType === "content" && item.resolvedContentType) {
14791
- return {
14792
- kind: "entry",
14793
- contentType: item.resolvedContentType,
14794
- identifier: item.resolvedIdentifier
14795
- };
14796
- }
14697
+ async function loadPushConfigTarget(target, options) {
14698
+ return {
14699
+ target,
14700
+ dashboard: unwrapPushConfigTargetValue(resolveDashboardUrlValue(options.dashboard, target)),
14701
+ apiKey: unwrapPushConfigTargetValue(resolveManagementApiKeyValue(options.apiKey, target))
14702
+ };
14703
+ }
14704
+ async function preparePushConfigTarget(context, options) {
14705
+ const config = await loadValidatedPushConfig(context.target, options.config);
14706
+ const preview = await pushToDashboard(context.dashboard, config.siteId, context.apiKey, config, {
14707
+ forceContentTypes: options.forceContentTypes ?? false,
14708
+ dryRun: true
14709
+ });
14710
+ if (!analyzePushConfigResponse(preview, true).success) {
14711
+ throw new Error(`Config preview failed for ${context.target} environment.`);
14797
14712
  }
14798
- if (link$1.href) {
14713
+ return {
14714
+ config,
14715
+ preview,
14716
+ forceContentTypes: options.forceContentTypes ?? false
14717
+ };
14718
+ }
14719
+ /**
14720
+ * Re-preview a prepared config value without reloading the local config file.
14721
+ * The caller uses this immediately before applying a multi-target plan so a
14722
+ * dashboard-side schema change is classified as stale before any mutation.
14723
+ */
14724
+ async function revalidatePushConfigPlan(context, plan) {
14725
+ try {
14726
+ const current = await pushToDashboard(context.dashboard, plan.config.siteId, context.apiKey, plan.config, {
14727
+ forceContentTypes: plan.forceContentTypes,
14728
+ dryRun: true
14729
+ });
14730
+ return stableStringify(current) === stableStringify(plan.preview) ? { ok: true } : {
14731
+ ok: false,
14732
+ error: new Error("SDK config changed after preflight; the push plan is stale.")
14733
+ };
14734
+ } catch (error) {
14799
14735
  return {
14800
- kind: "url",
14801
- href: link$1.href
14736
+ ok: false,
14737
+ error: error instanceof Error ? error : new Error(String(error))
14802
14738
  };
14803
14739
  }
14804
- return undefined;
14805
14740
  }
14741
+ async function applyPushConfigPlan(output, context, plan, options, isJsonOutput) {
14742
+ const response = options.dryRun ? plan.preview : await pushToDashboard(context.dashboard, plan.config.siteId, context.apiKey, plan.config, {
14743
+ forceContentTypes: plan.forceContentTypes,
14744
+ dryRun: false,
14745
+ expectedMailingListProviderScope: plan.preview.mailingListProviderScope
14746
+ });
14747
+ return {
14748
+ response,
14749
+ action: isJsonOutput ? { success: analyzePushConfigResponse(response, options.dryRun ?? false).success } : renderPushConfigResponse(output, response, "human", options.dryRun ?? false)
14750
+ };
14751
+ }
14752
+ function isPushConfigApplySuccess(result) {
14753
+ return result.action.success;
14754
+ }
14755
+ function renderPushConfigJsonResult(output, execution, dryRun) {
14756
+ output.json({
14757
+ success: execution.succeeded,
14758
+ dryRun,
14759
+ environments: mapEnvironmentExecutionOutcomesJson(execution, ({ result }) => ({
14760
+ response: result.response,
14761
+ success: true
14762
+ }), ({ result, message }) => ({
14763
+ response: result.response,
14764
+ success: false,
14765
+ error: message
14766
+ }))
14767
+ });
14768
+ }
14769
+ function getProductionPushConfigCommandDeps() {
14770
+ return {
14771
+ getOutputContext,
14772
+ planEnvironmentExecution,
14773
+ executeEnvironmentPlan,
14774
+ confirmAction,
14775
+ loadTarget: loadPushConfigTarget,
14776
+ prepareTarget: preparePushConfigTarget,
14777
+ revalidatePlan: revalidatePushConfigPlan,
14778
+ applyPlan: applyPushConfigPlan
14779
+ };
14780
+ }
14781
+ /**
14782
+ * push-config command definition
14783
+ */
14784
+ const createPushConfigCommand = (deps = getProductionPushConfigCommandDeps()) => new Command("push-config").description("Push SDK config to dashboard").option("--api-key <key>", "Management API key (or set RIVERBANK_*_MGMT_API_KEY)").option("--dashboard <url>", "Dashboard URL (or set RIVERBANK_*_DASHBOARD_URL env var)").option("--config <path>", `Path to config file (default: ${DEFAULT_CONFIG_PATH})`).option("--force-content-types", "Force overwrite content type schema even if modified in dashboard after last SDK sync").option("--dry-run", "Preview SDK config changes without updating the dashboard").option("--yes", "Confirm applying config changes across all selected environments").addHelpText("after", `
14785
+ Description:
14786
+ Syncs your local riverbank.config.ts schema/config to the CMS dashboard, including:
14787
+ - Custom blocks
14788
+ - Block field extensions
14789
+ - Block field options
14790
+ - Content type definitions and templates
14791
+ - SDK-managed site settings and footer blocks
14792
+
14793
+ Pages, content entries, and navigation menus are content documents.
14794
+ Sync them with "riverbankcms push pages", "riverbankcms push entries",
14795
+ or "riverbankcms push navigation".
14796
+
14797
+ Examples:
14798
+ $ pnpm exec riverbankcms push-config
14799
+ $ pnpm exec riverbankcms push-config --dry-run
14800
+ $ pnpm exec riverbankcms push-config --env=remote
14801
+ $ pnpm exec riverbankcms push-config --api-key bld_mgmt_sk_... --dashboard https://www.riverbankcms.com
14802
+ $ pnpm exec riverbankcms push-config --config ./src/riverbank.config.ts
14803
+ `).action(async (options, command) => {
14804
+ const { output, selection, globalOpts } = deps.getOutputContext(command);
14805
+ const isJsonOutput = globalOpts.json ?? false;
14806
+ const authorization = requireEnvironmentWriteAuthorization({
14807
+ ...options,
14808
+ json: isJsonOutput,
14809
+ interactive: process.stdin.isTTY === true
14810
+ }, output);
14811
+ if (!authorization) return;
14812
+ const planned = deps.planEnvironmentExecution({
14813
+ command: "push-config",
14814
+ selection: "both",
14815
+ failure: "abort",
14816
+ confirmation: "command",
14817
+ revalidation: "required"
14818
+ }, selection);
14819
+ if (!planned.ok) return output.error(planned.error.message, { suggestion: planned.error.suggestion });
14820
+ const execution = await deps.executeEnvironmentPlan(planned.value, {
14821
+ loadTarget: (target) => deps.loadTarget(target, options),
14822
+ preflight: async (_target, loaded) => deps.prepareTarget(loaded.context, {
14823
+ ...options,
14824
+ dashboard: loaded.context.dashboard
14825
+ }),
14826
+ confirm: async ({ ready }) => {
14827
+ if (authorization.kind === "preauthorized") return true;
14828
+ for (const { target, context } of ready) {
14829
+ const sync = getPushConfigSync(context.preview);
14830
+ output.info(`${target} config preview`, sync ? buildPreviewSummary(sync) : { changes: "No sync summary returned" });
14831
+ }
14832
+ return deps.confirmAction(`Apply SDK config to ${ready.length} environment${ready.length === 1 ? "" : "s"} (${ready.map(({ target }) => target).join(" then ")})?`, { yes: options.yes }, false);
14833
+ },
14834
+ revalidate: async (_target, loaded, prepared) => options.dryRun ? { ok: true } : deps.revalidatePlan(loaded.context, prepared.context),
14835
+ apply: async (target, loaded, prepared) => {
14836
+ if (planned.value.targets.length > 1) output.info(`\n--- Pushing to ${target} environment ---`);
14837
+ return deps.applyPlan(output, loaded.context, prepared.context, {
14838
+ ...options,
14839
+ dashboard: loaded.context.dashboard
14840
+ }, isJsonOutput);
14841
+ },
14842
+ isSuccess: isPushConfigApplySuccess,
14843
+ formatFailure: formatPushConfigActionFailure
14844
+ }, output);
14845
+ if (isJsonOutput) renderPushConfigJsonResult(output, execution, options.dryRun ?? false);
14846
+ });
14806
14847
 
14807
14848
  //#endregion
14808
- //#region ../api/src/navigation/routableLink.ts
14849
+ //#region ../api/src/navigationMenuValidation.ts
14850
+ function hasChildren(item) {
14851
+ return Array.isArray(item.children) && item.children.length > 0;
14852
+ }
14853
+ function isCartLinkMissingVisibleLabel(link$1) {
14854
+ return link$1?.kind === "cart" && link$1.label?.kind === "icon-with-label" && (typeof link$1.label.label !== "string" || link$1.label.label.trim().length === 0);
14855
+ }
14809
14856
  /**
14810
- * Prefer identifier-based links when available so links survive route/path updates.
14811
- * Falls back to an internal route payload for legacy rows or incomplete data.
14857
+ * Recursively collects portal-link constraints for a single menu tree.
14858
+ * Rules:
14859
+ * - At most one portal link per menu.
14860
+ * - Portal links must be root-level.
14861
+ * - Portal links cannot have children.
14812
14862
  */
14813
- function toRoutableLinkPayload(item) {
14814
- switch (item.entityType) {
14815
- case "page":
14816
- if (item.identifier) {
14817
- return {
14818
- kind: "page",
14819
- identifier: item.identifier
14820
- };
14863
+ function collectPortalLinkStats(items, depth = 0) {
14864
+ const result = {
14865
+ count: 0,
14866
+ nestedLabels: [],
14867
+ withChildrenLabels: []
14868
+ };
14869
+ for (const item of items) {
14870
+ const isPortal = item.link?.kind === "portal";
14871
+ if (isPortal) {
14872
+ result.count += 1;
14873
+ if (depth > 0) {
14874
+ result.nestedLabels.push(item.label);
14821
14875
  }
14822
- return toResolvedInternalPayload(item);
14823
- case "content":
14824
- if (item.identifier && item.contentTypeKey) {
14825
- return {
14826
- kind: "entry",
14827
- contentType: item.contentTypeKey,
14828
- identifier: item.identifier
14829
- };
14876
+ if (hasChildren(item)) {
14877
+ result.withChildrenLabels.push(item.label);
14830
14878
  }
14831
- return toResolvedInternalPayload(item);
14832
- case "pageSurface": return toResolvedInternalPayload(item);
14833
- default: return assertNever(item);
14879
+ }
14880
+ if (hasChildren(item)) {
14881
+ const childItems = (item.children ?? []).filter(isNavigationMenuValidationItem);
14882
+ const child = collectPortalLinkStats(childItems, depth + 1);
14883
+ result.count += child.count;
14884
+ result.nestedLabels.push(...child.nestedLabels);
14885
+ result.withChildrenLabels.push(...child.withChildrenLabels);
14886
+ }
14834
14887
  }
14888
+ return result;
14835
14889
  }
14836
- function toResolvedInternalPayload(item) {
14837
- return {
14838
- kind: "internal",
14839
- routeId: item.routeId,
14840
- entityId: item.entityId,
14841
- entityType: item.entityType,
14842
- href: item.path ?? "/",
14843
- title: item.title ?? item.draftTitle ?? "Untitled",
14844
- typeLabel: item.typeLabel,
14845
- contentTypeKey: item.contentTypeKey,
14846
- contentTypeName: item.contentTypeName,
14847
- updatedAt: item.updatedAt ?? null
14890
+ /**
14891
+ * Recursively collects cart-link constraints for a single menu tree.
14892
+ * Rules:
14893
+ * - Cart links must be root-level.
14894
+ * - Cart links cannot have children.
14895
+ * - Cart links cannot be CTA items.
14896
+ * - Cart links using `icon-with-label` must provide visible text.
14897
+ */
14898
+ function collectCartLinkStats(items, depth = 0) {
14899
+ const result = {
14900
+ count: 0,
14901
+ nestedLabels: [],
14902
+ withChildrenLabels: [],
14903
+ ctaLabels: [],
14904
+ missingVisibleTextLabels: []
14848
14905
  };
14906
+ for (const item of items) {
14907
+ const isCart = item.link?.kind === "cart";
14908
+ if (isCart) {
14909
+ result.count += 1;
14910
+ if (depth > 0) {
14911
+ result.nestedLabels.push(item.label);
14912
+ }
14913
+ if (hasChildren(item)) {
14914
+ result.withChildrenLabels.push(item.label);
14915
+ }
14916
+ if (item.isCta === true) {
14917
+ result.ctaLabels.push(item.label);
14918
+ }
14919
+ if (isCartLinkMissingVisibleLabel(item.link ?? null)) {
14920
+ result.missingVisibleTextLabels.push(item.label);
14921
+ }
14922
+ }
14923
+ if (hasChildren(item)) {
14924
+ const childItems = (item.children ?? []).filter(isNavigationMenuValidationItem);
14925
+ const child = collectCartLinkStats(childItems, depth + 1);
14926
+ result.count += child.count;
14927
+ result.nestedLabels.push(...child.nestedLabels);
14928
+ result.withChildrenLabels.push(...child.withChildrenLabels);
14929
+ result.ctaLabels.push(...child.ctaLabels);
14930
+ result.missingVisibleTextLabels.push(...child.missingVisibleTextLabels);
14931
+ }
14932
+ }
14933
+ return result;
14849
14934
  }
14850
-
14851
- //#endregion
14852
- //#region ../api/src/navigation/linkUrlType.ts
14853
- const LINK_PAYLOAD_NAVIGATION_URL_TYPES = {
14854
- page: "internal",
14855
- entry: "internal",
14856
- internal: "internal",
14857
- external: "external",
14858
- url: "url",
14859
- portal: "portal",
14860
- cart: "cart"
14861
- };
14862
- function linkPayloadKindToNavigationUrlType(kind) {
14863
- return LINK_PAYLOAD_NAVIGATION_URL_TYPES[kind];
14864
- }
14865
-
14866
- //#endregion
14867
- //#region ../api/src/accessAdmin.ts
14868
- const STAFF_OPERATIONAL_CAPABILITIES = ["appointments", "event_ops"];
14869
- function getDefaultStaffCapabilities(surfaces) {
14870
- if (surfaces.events && !surfaces.appointments) return ["event_ops"];
14871
- return ["appointments"];
14935
+ function isNavigationMenuValidationItem(value) {
14936
+ if (!isRecord$5(value)) return false;
14937
+ return typeof value.label === "string";
14872
14938
  }
14873
- const managedDomainAddOnPricingSnapshotSchema = z.object({
14874
- formulaVersion: z.literal("managed-domain-overage-v1"),
14875
- conversionRule: z.literal("usd-to-gbp-1-to-1-v1"),
14876
- registrarAnnualCost: z.number().nonnegative(),
14877
- registrarCurrency: z.literal("USD"),
14878
- includedAnnualAllowance: z.number().nonnegative(),
14879
- includedAnnualAllowanceCurrency: z.literal("USD"),
14880
- monthlyAmountCents: z.number().int().nonnegative(),
14881
- monthlyCurrency: z.literal("gbp")
14882
- });
14883
14939
 
14884
14940
  //#endregion
14885
- //#region ../api/src/siteMembers.ts
14941
+ //#region ../api/src/navigation/bookingPortalLink.ts
14942
+ const YOUR_BOOKINGS_LABEL = "Your bookings";
14886
14943
  /**
14887
- * Higher number means more permissions.
14944
+ * The booking page's link to the customer portal, in the header and the
14945
+ * footer alike. It is a portal link, so renderers apply the portal link rules
14946
+ * (including the portal-enabled gate); the label stays the same signed in.
14888
14947
  */
14889
- const SITE_ROLE_PERMISSION_LEVEL = {
14890
- team_member: 0,
14891
- editor: 1,
14892
- administrator: 2,
14893
- owner: 3
14948
+ const YOUR_BOOKINGS_PORTAL_LINK = {
14949
+ kind: "portal",
14950
+ labels: {
14951
+ loggedOut: YOUR_BOOKINGS_LABEL,
14952
+ loggedIn: YOUR_BOOKINGS_LABEL
14953
+ }
14894
14954
  };
14955
+
14956
+ //#endregion
14957
+ //#region ../api/src/navigation/linkResolver.ts
14895
14958
  /**
14896
- * Lower number sorts earlier in UI role pickers.
14959
+ * Type guard for internal links.
14960
+ * Checks if a link payload is an internal link with entityId and entityType.
14897
14961
  */
14898
- const SITE_ROLE_DISPLAY_ORDER = {
14899
- owner: 0,
14900
- administrator: 1,
14901
- editor: 2,
14902
- team_member: 3
14903
- };
14904
- async function getSiteMembers(apiClient, { siteId, options }) {
14905
- try {
14906
- return unwrapResponse(await apiClient({
14907
- endpoint: "listSiteMembers",
14908
- params: { siteId },
14909
- options
14910
- }));
14911
- } catch (error) {
14912
- return normalizeApiError(error, "Failed to load site members");
14962
+ function isInternalLink(link$1) {
14963
+ if (!link$1 || typeof link$1 !== "object") {
14964
+ return false;
14913
14965
  }
14966
+ return link$1.kind === "internal" && "entityId" in link$1 && "entityType" in link$1 && typeof link$1.entityId === "string" && typeof link$1.entityType === "string";
14914
14967
  }
14915
- async function inviteSiteMember(apiClient, { siteId, body, options }) {
14916
- try {
14917
- return unwrapResponse(await apiClient({
14918
- endpoint: "inviteSiteMember",
14919
- params: { siteId },
14920
- body,
14921
- options
14922
- }));
14923
- } catch (error) {
14924
- return normalizeApiError(error, "Failed to invite collaborator");
14925
- }
14968
+ /**
14969
+ * Narrow internal link to page type.
14970
+ */
14971
+ function isPageLink(link$1) {
14972
+ return link$1.entityType === "page";
14926
14973
  }
14927
- async function updateSiteMemberRole(apiClient, { siteId, memberId, body, options }) {
14928
- try {
14929
- return unwrapResponse(await apiClient({
14930
- endpoint: "updateSiteMemberRole",
14931
- params: {
14932
- siteId,
14933
- memberId
14934
- },
14935
- body,
14936
- options
14937
- }));
14938
- } catch (error) {
14939
- return normalizeApiError(error, "Failed to update member role");
14940
- }
14974
+ /**
14975
+ * Narrow internal link to content/entry type.
14976
+ */
14977
+ function isEntryLink(link$1) {
14978
+ return link$1.entityType === "content";
14941
14979
  }
14942
- async function linkCurrentUserPractitioner(apiClient, { siteId, body, options }) {
14943
- try {
14944
- return unwrapResponse(await apiClient({
14945
- endpoint: "linkCurrentUserPractitioner",
14946
- params: { siteId },
14947
- body,
14948
- options
14949
- }));
14950
- } catch (error) {
14951
- return normalizeApiError(error, "Failed to link practitioner profile");
14980
+ /**
14981
+ * Resolve a navigation link input to URL type and link payload.
14982
+ *
14983
+ * Supports identifier-based page/entry links and validates them against routable
14984
+ * lookup maps before persisting.
14985
+ *
14986
+ * @param link - The navigation link input from SDK config or API
14987
+ * @param options - Resolution options (isDropdown flag)
14988
+ * @param pageMap - Map of page identifier -> PageNavLinkInfo
14989
+ * @param entryMap - Map of `contentType:identifier` -> EntryNavLinkInfo
14990
+ * @returns Resolved link with urlType, url payload, and any error
14991
+ */
14992
+ function resolveNavigationLinkInput(link$1, options, pageMap, entryMap) {
14993
+ if (options.isDropdown) {
14994
+ return {
14995
+ urlType: "dropdown",
14996
+ url: null,
14997
+ error: null
14998
+ };
14952
14999
  }
14953
- }
14954
- async function unlinkCurrentUserPractitioner(apiClient, { siteId, options }) {
14955
- try {
14956
- return unwrapResponse(await apiClient({
14957
- endpoint: "unlinkCurrentUserPractitioner",
14958
- params: { siteId },
14959
- options
14960
- }));
14961
- } catch (error) {
14962
- return normalizeApiError(error, "Failed to unlink practitioner profile");
15000
+ if (!link$1) {
15001
+ return {
15002
+ urlType: "url",
15003
+ url: null,
15004
+ error: "Link is required for non-dropdown items"
15005
+ };
14963
15006
  }
14964
- }
14965
- async function removeSiteMember(apiClient, { siteId, memberId, options }) {
14966
- try {
14967
- return unwrapResponse(await apiClient({
14968
- endpoint: "removeSiteMember",
14969
- params: {
14970
- siteId,
14971
- memberId
15007
+ switch (link$1.kind) {
15008
+ case "page": return resolvePageLink(link$1.identifier, pageMap);
15009
+ case "entry": return resolveEntryLink(link$1.contentType, link$1.identifier, entryMap);
15010
+ case "external": return {
15011
+ urlType: "external",
15012
+ url: {
15013
+ kind: "external",
15014
+ href: link$1.href
14972
15015
  },
14973
- options
14974
- }));
14975
- } catch (error) {
14976
- return normalizeApiError(error, "Failed to remove collaborator");
14977
- }
14978
- }
14979
- async function revokeSiteInvitation(apiClient, { siteId, invitationId, options }) {
14980
- try {
14981
- return unwrapResponse(await apiClient({
14982
- endpoint: "revokeSiteInvitation",
14983
- params: {
14984
- siteId,
14985
- invitationId
15016
+ error: null
15017
+ };
15018
+ case "url": return {
15019
+ urlType: "url",
15020
+ url: {
15021
+ kind: "url",
15022
+ href: link$1.href
14986
15023
  },
14987
- options
14988
- }));
14989
- } catch (error) {
14990
- return normalizeApiError(error, "Failed to revoke invitation");
14991
- }
14992
- }
14993
- async function resendSiteInvitation(apiClient, { siteId, invitationId, options }) {
14994
- try {
14995
- return unwrapResponse(await apiClient({
14996
- endpoint: "resendSiteInvitation",
14997
- params: {
14998
- siteId,
14999
- invitationId
15024
+ error: null
15025
+ };
15026
+ case "portal": return {
15027
+ urlType: "portal",
15028
+ url: {
15029
+ kind: "portal",
15030
+ ...link$1.path ? { path: link$1.path } : {},
15031
+ ...link$1.labels ? { labels: link$1.labels } : {}
15000
15032
  },
15001
- options
15002
- }));
15003
- } catch (error) {
15004
- return normalizeApiError(error, "Failed to resend invitation");
15033
+ error: null
15034
+ };
15035
+ case "cart": return {
15036
+ urlType: "cart",
15037
+ url: {
15038
+ kind: "cart",
15039
+ badge: link$1.badge,
15040
+ label: link$1.label
15041
+ },
15042
+ error: null
15043
+ };
15044
+ default: return {
15045
+ urlType: "url",
15046
+ url: null,
15047
+ error: "Unknown link type"
15048
+ };
15005
15049
  }
15006
15050
  }
15007
- async function transferSiteOwnership(apiClient, { siteId, body, options }) {
15008
- try {
15009
- return unwrapResponse(await apiClient({
15010
- endpoint: "transferSiteOwnership",
15011
- params: { siteId },
15012
- body,
15013
- options
15014
- }));
15015
- } catch (error) {
15016
- return normalizeApiError(error, "Failed to transfer ownership");
15051
+ /**
15052
+ * Resolve a page identifier to a canonical page link payload.
15053
+ */
15054
+ function resolvePageLink(identifier, pageMap) {
15055
+ const pageInfo = pageMap.get(identifier);
15056
+ if (!pageInfo) {
15057
+ return {
15058
+ urlType: "internal",
15059
+ url: null,
15060
+ error: `Page '${identifier}' not found`
15061
+ };
15017
15062
  }
15063
+ return {
15064
+ urlType: "internal",
15065
+ url: {
15066
+ kind: "page",
15067
+ identifier
15068
+ },
15069
+ error: null
15070
+ };
15018
15071
  }
15019
- async function acceptSiteInvitation(apiClient, { body, options }) {
15020
- try {
15021
- return unwrapResponse(await apiClient({
15022
- endpoint: "acceptSiteInvitation",
15023
- body,
15024
- options
15025
- }));
15026
- } catch (error) {
15027
- return normalizeApiError(error, "Failed to accept invitation");
15072
+ /**
15073
+ * Resolve an entry identifier to a canonical entry link payload.
15074
+ */
15075
+ function resolveEntryLink(contentType, identifier, entryMap) {
15076
+ const entryInfo = entryMap.get(`${contentType}:${identifier}`);
15077
+ if (!entryInfo) {
15078
+ return {
15079
+ urlType: "internal",
15080
+ url: null,
15081
+ error: `Entry '${contentType}:${identifier}' not found or not routable`
15082
+ };
15028
15083
  }
15084
+ return {
15085
+ urlType: "internal",
15086
+ url: {
15087
+ kind: "entry",
15088
+ contentType,
15089
+ identifier
15090
+ },
15091
+ error: null
15092
+ };
15029
15093
  }
15030
15094
 
15031
15095
  //#endregion
15032
- //#region ../api/src/staffAccess.ts
15033
- function roleAtLeast$1(role, minimum) {
15034
- return SITE_ROLE_PERMISSION_LEVEL[role] >= SITE_ROLE_PERMISSION_LEVEL[minimum];
15035
- }
15036
- function roleAtLeastOrNone(role, minimum) {
15037
- if (!role) return false;
15038
- return roleAtLeast$1(role, minimum);
15039
- }
15040
- function appointmentResourceIdFromStaffMemberId(staffMemberId) {
15041
- return unsafeAsAppointmentResourceId(staffMemberId);
15042
- }
15043
- function normalizeStaffCapabilities(capabilities) {
15044
- if (!capabilities) return null;
15045
- return STAFF_OPERATIONAL_CAPABILITIES.filter((capability) => capabilities.includes(capability));
15096
+ //#region ../api/src/navigation/boundaries.ts
15097
+ function isNavigationDropdownSentinel(link$1) {
15098
+ return link$1?.kind === "dropdown";
15046
15099
  }
15047
- function deriveStaffScope(input) {
15048
- const staffMemberId = input.linkedStaffMemberId ? unsafeAsStaffMemberId(input.linkedStaffMemberId) : null;
15049
- const persistedCapabilities = normalizeStaffCapabilities(input.staffCapabilities);
15050
- switch (input.membershipRole) {
15051
- case null:
15052
- case "owner":
15053
- case "administrator":
15054
- case "editor": return { kind: "none" };
15055
- case "team_member":
15056
- if (!staffMemberId) {
15057
- return {
15058
- kind: "staff_setup_required",
15059
- reason: "missing_staff_member"
15060
- };
15061
- }
15062
- if (persistedCapabilities === null) {
15063
- return {
15064
- kind: "appointments",
15065
- staffMemberId
15066
- };
15067
- }
15068
- if (persistedCapabilities.length === 0) {
15069
- return {
15070
- kind: "staff_setup_required",
15071
- reason: "missing_capabilities"
15072
- };
15073
- }
15074
- if (persistedCapabilities.includes("appointments") && persistedCapabilities.includes("event_ops")) {
15075
- return {
15076
- kind: "hybrid",
15077
- staffMemberId
15078
- };
15079
- }
15080
- if (persistedCapabilities.includes("event_ops")) {
15081
- return {
15082
- kind: "event_ops_only",
15083
- staffMemberId
15084
- };
15085
- }
15086
- return {
15087
- kind: "appointments",
15088
- staffMemberId
15089
- };
15100
+ function normalizeNavigationConfigItemForCommand(item) {
15101
+ const commandItem = { label: item.label };
15102
+ if (item.isCta !== undefined) {
15103
+ commandItem.isCta = item.isCta;
15090
15104
  }
15091
- }
15092
- function isStaffScoped(scope) {
15093
- return scope.kind !== "none";
15094
- }
15095
- function isTeamMemberScope(scope) {
15096
- return scope.kind === "appointments" || scope.kind === "staff_setup_required";
15097
- }
15098
- function hasAppointmentStaffAccess(scope) {
15099
- return scope.kind === "appointments" || scope.kind === "hybrid";
15100
- }
15101
- function hasEventOpsAccess(scope) {
15102
- return scope.kind === "event_ops_only" || scope.kind === "hybrid";
15103
- }
15104
- function getLinkedStaffMemberId(scope) {
15105
- switch (scope.kind) {
15106
- case "appointments":
15107
- case "event_ops_only":
15108
- case "hybrid": return scope.staffMemberId;
15109
- case "none":
15110
- case "staff_setup_required": return null;
15105
+ const commandLink = normalizeNavigationAuthoringLinkForCommand(item.link);
15106
+ if (commandLink) {
15107
+ commandItem.link = commandLink;
15111
15108
  }
15112
- }
15113
- function getOwnAppointmentResourceId(scope) {
15114
- const staffMemberId = getLinkedStaffMemberId(scope);
15115
- return staffMemberId ? appointmentResourceIdFromStaffMemberId(staffMemberId) : null;
15116
- }
15117
- function canAccessOwnAppointmentResource(scope, resourceId) {
15118
- const ownResourceId = getOwnAppointmentResourceId(scope);
15119
- if (!ownResourceId) return false;
15120
- const normalizedResourceId = typeof resourceId === "string" ? unsafeAsAppointmentResourceId(resourceId) : resourceId;
15121
- return ownResourceId === normalizedResourceId;
15122
- }
15123
- function deriveLinkedAppointmentIdentity(input) {
15124
- if (!input.membershipRole || !input.linkedStaffMemberId) {
15125
- return { kind: "none" };
15109
+ if (item.children && item.children.length > 0) {
15110
+ commandItem.children = item.children.map(normalizeNavigationConfigItemForCommand);
15126
15111
  }
15127
- const staffMemberId = unsafeAsStaffMemberId(input.linkedStaffMemberId);
15112
+ return commandItem;
15113
+ }
15114
+ function navigationCommandItemToManagementInput(item) {
15115
+ const link$1 = normalizeNavigationAuthoringLinkForCommand(item.link);
15128
15116
  return {
15129
- kind: "linked",
15130
- staffMemberId,
15131
- resourceId: appointmentResourceIdFromStaffMemberId(staffMemberId)
15117
+ label: item.label,
15118
+ ...link$1 ? { link: link$1 } : {},
15119
+ ...item.isCta !== undefined ? { isCta: item.isCta } : {},
15120
+ ...item.children && item.children.length > 0 ? { children: item.children.map(navigationCommandItemToManagementInput) } : {}
15132
15121
  };
15133
15122
  }
15134
- function getIdentityStaffMemberId(identity) {
15135
- switch (identity.kind) {
15136
- case "linked": return identity.staffMemberId;
15137
- case "none": return null;
15138
- }
15139
- }
15140
- function getIdentityStaffMemberIdValue(identity) {
15141
- return getIdentityStaffMemberId(identity);
15123
+ function normalizeNavigationAuthoringLinkForCommand(link$1) {
15124
+ if (!link$1 || isNavigationDropdownSentinel(link$1)) return undefined;
15125
+ return navigationAuthoringLinkToCommandLink(link$1);
15142
15126
  }
15143
- function getIdentityAppointmentResourceId(identity) {
15144
- return identity.kind === "linked" ? identity.resourceId : null;
15127
+ function navigationApiItemToSdkLocalItem(item) {
15128
+ const link$1 = navigationReadLinkToLocalLink(item);
15129
+ const localItem = {
15130
+ label: item.label,
15131
+ ...item.isCta ? { isCta: true } : {},
15132
+ ...link$1 ? { link: link$1 } : {},
15133
+ ...item.children && item.children.length > 0 ? { children: item.children.map(navigationApiItemToSdkLocalItem) } : {}
15134
+ };
15135
+ return localItem;
15145
15136
  }
15146
- function getIdentityAppointmentResourceIdValue(identity) {
15147
- return getIdentityAppointmentResourceId(identity);
15137
+ function navigationAuthoringLinkToCommandLink(link$1) {
15138
+ switch (link$1.kind) {
15139
+ case "page": return {
15140
+ kind: "page",
15141
+ identifier: link$1.identifier
15142
+ };
15143
+ case "entry": return {
15144
+ kind: "entry",
15145
+ contentType: link$1.contentType,
15146
+ identifier: link$1.identifier
15147
+ };
15148
+ case "external": return {
15149
+ kind: "external",
15150
+ href: link$1.href
15151
+ };
15152
+ case "url": return {
15153
+ kind: "url",
15154
+ href: link$1.href
15155
+ };
15156
+ case "portal": return {
15157
+ kind: "portal",
15158
+ ...link$1.path ? { path: link$1.path } : {},
15159
+ ...portalLabelsForCommand(link$1.labels)
15160
+ };
15161
+ case "cart": return {
15162
+ kind: "cart",
15163
+ badge: link$1.badge,
15164
+ label: link$1.label
15165
+ };
15166
+ default: return assertNever(link$1);
15167
+ }
15148
15168
  }
15149
- function isLinkedAppointmentResource(identity, resourceId) {
15150
- if (identity.kind !== "linked") return false;
15151
- return identity.resourceId === resourceId;
15169
+ function portalLabelsForCommand(labels) {
15170
+ if (!labels) return {};
15171
+ const commandLabels = {
15172
+ ...typeof labels.loggedOut === "string" ? { loggedOut: labels.loggedOut } : {},
15173
+ ...typeof labels.loggedIn === "string" ? { loggedIn: labels.loggedIn } : {}
15174
+ };
15175
+ return Object.keys(commandLabels).length > 0 ? { labels: commandLabels } : {};
15152
15176
  }
15153
- function deriveStaffSiteHomeDestination(staffScope) {
15154
- if (hasAppointmentStaffAccess(staffScope)) {
15155
- return { kind: "staff_appointments" };
15156
- }
15157
- if (hasEventOpsAccess(staffScope)) {
15158
- return { kind: "ops_site_events" };
15177
+ function navigationReadLinkToLocalLink(item) {
15178
+ const { link: link$1 } = item;
15179
+ if (!link$1) return undefined;
15180
+ switch (link$1.kind) {
15181
+ case "page": return {
15182
+ kind: "page",
15183
+ identifier: link$1.identifier
15184
+ };
15185
+ case "entry": return {
15186
+ kind: "entry",
15187
+ contentType: link$1.contentType,
15188
+ identifier: link$1.identifier
15189
+ };
15190
+ case "internal": return internalNavigationLinkToLocalLink(item);
15191
+ case "external": return {
15192
+ kind: "external",
15193
+ href: link$1.href
15194
+ };
15195
+ case "url": return {
15196
+ kind: "url",
15197
+ href: link$1.href
15198
+ };
15199
+ case "portal": return {
15200
+ kind: "portal",
15201
+ ...link$1.path ? { path: link$1.path } : {},
15202
+ ...link$1.labels ? { labels: link$1.labels } : {}
15203
+ };
15204
+ case "cart": return {
15205
+ kind: "cart",
15206
+ badge: link$1.badge,
15207
+ label: link$1.label
15208
+ };
15209
+ default: return assertNever(link$1);
15159
15210
  }
15160
- return { kind: "dashboard_site_home" };
15161
- }
15162
- function canUseAppointmentsLabel(staffScope) {
15163
- return staffScope.kind === "appointments" || staffScope.kind === "hybrid" || staffScope.kind === "staff_setup_required";
15164
- }
15165
- function isEventOpsOnlyUser(sites) {
15166
- if (sites.length === 0) return false;
15167
- return sites.every((site) => deriveStaffScope(site).kind === "event_ops_only");
15168
- }
15169
- function deriveSiteListingNavigationPlan(site) {
15170
- const staffScope = deriveStaffScope(site);
15171
- const staffHomeDestination = deriveStaffSiteHomeDestination(staffScope);
15172
- const isStaffScopedMembership = isStaffScoped(staffScope);
15173
- return {
15174
- canUseDashboard: roleAtLeastOrNone(site.membershipRole, "editor") || isStaffScopedMembership,
15175
- primaryDestination: isStaffScopedMembership ? staffHomeDestination : { kind: "dashboard_site_open" },
15176
- manageDestination: isStaffScopedMembership ? staffHomeDestination : { kind: "dashboard_site_home" },
15177
- manageLabel: staffScope.kind === "event_ops_only" ? "Ops" : canUseAppointmentsLabel(staffScope) ? "Appointments" : "Manage"
15178
- };
15179
15211
  }
15180
- function shouldRedirectStaffScopeToOps(staffScope) {
15181
- return staffScope.kind === "event_ops_only";
15182
- }
15183
- function deriveOpsLandingDestination(sites) {
15184
- const onlySite = sites.length === 1 ? sites[0] : undefined;
15185
- return onlySite?.id ? {
15186
- kind: "ops_site_events",
15187
- siteId: onlySite.id
15188
- } : { kind: "ops_home" };
15212
+ function internalNavigationLinkToLocalLink(item) {
15213
+ const { link: link$1 } = item;
15214
+ if (!link$1 || link$1.kind !== "internal") return undefined;
15215
+ if (item.resolvedIdentifier) {
15216
+ if (link$1.entityType === "page") {
15217
+ return {
15218
+ kind: "page",
15219
+ identifier: item.resolvedIdentifier
15220
+ };
15221
+ }
15222
+ if (link$1.entityType === "content" && item.resolvedContentType) {
15223
+ return {
15224
+ kind: "entry",
15225
+ contentType: item.resolvedContentType,
15226
+ identifier: item.resolvedIdentifier
15227
+ };
15228
+ }
15229
+ }
15230
+ if (link$1.href) {
15231
+ return {
15232
+ kind: "url",
15233
+ href: link$1.href
15234
+ };
15235
+ }
15236
+ return undefined;
15189
15237
  }
15190
15238
 
15191
15239
  //#endregion
15192
- //#region ../api/src/appointmentServiceVisibility.ts
15193
- const APPOINTMENT_SERVICE_VISIBILITY_VALUES = ["public", "unlisted"];
15194
- function isAppointmentServiceVisibility(value) {
15195
- return value === "public" || value === "unlisted";
15240
+ //#region ../api/src/navigation/routableLink.ts
15241
+ /**
15242
+ * Prefer identifier-based links when available so links survive route/path updates.
15243
+ * Falls back to an internal route payload for legacy rows or incomplete data.
15244
+ */
15245
+ function toRoutableLinkPayload(item) {
15246
+ switch (item.entityType) {
15247
+ case "page":
15248
+ if (item.identifier) {
15249
+ return {
15250
+ kind: "page",
15251
+ identifier: item.identifier
15252
+ };
15253
+ }
15254
+ return toResolvedInternalPayload(item);
15255
+ case "content":
15256
+ if (item.identifier && item.contentTypeKey) {
15257
+ return {
15258
+ kind: "entry",
15259
+ contentType: item.contentTypeKey,
15260
+ identifier: item.identifier
15261
+ };
15262
+ }
15263
+ return toResolvedInternalPayload(item);
15264
+ case "pageSurface": return toResolvedInternalPayload(item);
15265
+ default: return assertNever(item);
15266
+ }
15196
15267
  }
15197
-
15198
- //#endregion
15199
- //#region ../api/src/appointmentServiceLocation.ts
15200
- const APPOINTMENT_SERVICE_LOCATION_TYPE_VALUES = [
15201
- "virtual",
15202
- "in_person",
15203
- "hybrid"
15204
- ];
15205
- function isAppointmentServiceLocationType(value) {
15206
- return value === "virtual" || value === "in_person" || value === "hybrid";
15268
+ function toResolvedInternalPayload(item) {
15269
+ return {
15270
+ kind: "internal",
15271
+ routeId: item.routeId,
15272
+ entityId: item.entityId,
15273
+ entityType: item.entityType,
15274
+ href: item.path ?? "/",
15275
+ title: item.title ?? item.draftTitle ?? "Untitled",
15276
+ typeLabel: item.typeLabel,
15277
+ contentTypeKey: item.contentTypeKey,
15278
+ contentTypeName: item.contentTypeName,
15279
+ updatedAt: item.updatedAt ?? null
15280
+ };
15207
15281
  }
15208
15282
 
15209
15283
  //#endregion
15210
- //#region ../api/src/bookingOperations.ts
15211
- const APPOINTMENT_RESERVATION_STATUS_VALUES = [
15212
- "active",
15213
- "paused",
15214
- "ended"
15215
- ];
15216
- const APPOINTMENT_HISTORY_STATUS_VALUES = [
15217
- "scheduled",
15218
- "pending",
15219
- "confirmed",
15220
- "cancelled"
15221
- ];
15222
-
15223
- //#endregion
15224
- //#region ../api/src/common/cancellation.ts
15225
- const STAFF_CANCELLATION_CONTEXT_UI_CATEGORIES = [
15226
- "customer_requested",
15227
- "schedule_conflict",
15228
- "duplicate_or_admin_error",
15229
- "non_payment",
15230
- "policy_or_eligibility",
15231
- "other"
15232
- ];
15233
- const STAFF_CANCELLATION_CONTEXT_CATEGORIES = [...STAFF_CANCELLATION_CONTEXT_UI_CATEGORIES, "legacy_client_unspecified"];
15234
-
15235
- //#endregion
15236
- //#region ../api/src/siteOperations.ts
15237
- const FLEXIBLE_BALANCE_ADMIN_ACTION_SOURCES = [
15238
- "order_detail",
15239
- "payment_issues_queue",
15240
- "appointment_detail",
15241
- "event_attendee_detail",
15242
- "event_series_registration_detail"
15243
- ];
15244
-
15245
- //#endregion
15246
- //#region ../api/src/bookingRecords.ts
15247
- const CUSTOMER_PASS_STATUS_VALUES = [
15248
- "active",
15249
- "exhausted",
15250
- "expired",
15251
- "refunded"
15252
- ];
15253
- const CUSTOMER_MEMBERSHIP_STATUS_VALUES = [
15254
- "active",
15255
- "trialing",
15256
- "past_due",
15257
- "paused",
15258
- "canceled"
15259
- ];
15260
- const BOOKING_ACTIVITY_CATEGORY_VALUES = [
15261
- "booking",
15262
- "purchase",
15263
- "cancellation",
15264
- "refund",
15265
- "transfer",
15266
- "failure"
15267
- ];
15268
- const BOOKING_ACTIVITY_TYPE_VALUES = [
15269
- "appointment_booked",
15270
- "appointment_cancelled",
15271
- "booking_closure_created",
15272
- "booking_closure_removed",
15273
- "event_registration",
15274
- "event_registration_cancelled",
15275
- "event_series_registration",
15276
- "event_series_registration_cancelled",
15277
- "pass_purchased",
15278
- "membership_started",
15279
- "membership_paused",
15280
- "membership_cancelled",
15281
- "event_refund",
15282
- "pass_refund",
15283
- "membership_refund",
15284
- "event_series_registration_refund",
15285
- "ticket_transfer",
15286
- "payment_failed"
15287
- ];
15288
- const BOOKING_LEDGER_KIND_VALUES = [
15289
- "appointment",
15290
- "event_registration",
15291
- "event_series_registration"
15292
- ];
15293
- const BOOKING_LEDGER_DATE_BASIS_VALUES = [
15294
- "created_at",
15295
- "scheduled_at",
15296
- "cancelled_at"
15297
- ];
15298
- const BOOKING_LEDGER_PERIOD_PRESET_VALUES = [
15299
- "7d",
15300
- "30d",
15301
- "90d",
15302
- "365d",
15303
- "all"
15304
- ];
15305
- const BOOKING_LEDGER_CANCELLATION_MODE_VALUES = [
15306
- "exclude_cancelled",
15307
- "include_cancelled",
15308
- "cancelled_only"
15309
- ];
15310
- const BOOKING_QUESTION_ANSWER_EXPORT_SCOPES = {
15311
- site: "site",
15312
- appointments: "appointments",
15313
- eventSeries: "event_series"
15314
- };
15315
- const BOOKING_QUESTION_ANSWER_EXPORT_SCOPE_VALUES = [
15316
- BOOKING_QUESTION_ANSWER_EXPORT_SCOPES.site,
15317
- BOOKING_QUESTION_ANSWER_EXPORT_SCOPES.appointments,
15318
- BOOKING_QUESTION_ANSWER_EXPORT_SCOPES.eventSeries
15319
- ];
15320
-
15321
- //#endregion
15322
- //#region ../api/src/bookingPeople.ts
15323
- const PEOPLE_LIST_SORT_VALUES = [
15324
- "name",
15325
- "passes",
15326
- "memberships",
15327
- "lastBooking",
15328
- "added"
15329
- ];
15330
- const PEOPLE_LIST_FILTER_VALUES = [
15331
- "everyone",
15332
- "hasBought",
15333
- "hasActivePass",
15334
- "hasMembership",
15335
- "guests",
15336
- "nothingBought"
15337
- ];
15338
- /** What can be attached to a person, in the order the dialog lists it. */
15339
- const PERSON_DELETION_BLOCKER_CATEGORY_VALUES = [
15340
- "bookings",
15341
- "guest_places",
15342
- "waitlist_places",
15343
- "payments",
15344
- "orders",
15345
- "passes",
15346
- "memberships",
15347
- "credit",
15348
- "digital_products",
15349
- "discounts",
15350
- "gifts",
15351
- "notes",
15352
- "forms",
15353
- "terms_agreements",
15354
- "uploads",
15355
- "contact_corrections"
15356
- ];
15284
+ //#region ../api/src/navigation/linkUrlType.ts
15285
+ const LINK_PAYLOAD_NAVIGATION_URL_TYPES = {
15286
+ page: "internal",
15287
+ entry: "internal",
15288
+ internal: "internal",
15289
+ external: "external",
15290
+ url: "url",
15291
+ portal: "portal",
15292
+ cart: "cart"
15293
+ };
15294
+ function linkPayloadKindToNavigationUrlType(kind) {
15295
+ return LINK_PAYLOAD_NAVIGATION_URL_TYPES[kind];
15296
+ }
15357
15297
 
15358
15298
  //#endregion
15359
- //#region ../api/src/wholeSeriesRuns.ts
15360
- const DELETE_EVENT_OFFERING_CONFLICT_REASONS = [
15361
- "has_registrations",
15362
- "has_payments",
15363
- "published_page",
15364
- "inconsistent_ownership"
15365
- ];
15366
- function isDeleteEventOfferingConflictReason(value) {
15367
- return DELETE_EVENT_OFFERING_CONFLICT_REASONS.some((reason) => reason === value);
15299
+ //#region ../api/src/accessAdmin.ts
15300
+ const STAFF_OPERATIONAL_CAPABILITIES = ["appointments", "event_ops"];
15301
+ function getDefaultStaffCapabilities(surfaces) {
15302
+ if (surfaces.events && !surfaces.appointments) return ["event_ops"];
15303
+ return ["appointments"];
15368
15304
  }
15305
+ const managedDomainAddOnPricingSnapshotSchema = z.object({
15306
+ formulaVersion: z.literal("managed-domain-overage-v1"),
15307
+ conversionRule: z.literal("usd-to-gbp-1-to-1-v1"),
15308
+ registrarAnnualCost: z.number().nonnegative(),
15309
+ registrarCurrency: z.literal("USD"),
15310
+ includedAnnualAllowance: z.number().nonnegative(),
15311
+ includedAnnualAllowanceCurrency: z.literal("USD"),
15312
+ monthlyAmountCents: z.number().int().nonnegative(),
15313
+ monthlyCurrency: z.literal("gbp")
15314
+ });
15369
15315
 
15370
15316
  //#endregion
15371
- //#region ../api/src/bookings.ts
15372
- const APPOINTMENT_STATUS_VALUES = [
15373
- "pending",
15374
- "confirmed",
15375
- "cancelled",
15376
- "no_show"
15377
- ];
15378
- const BOOKING_QUESTION_REQUIREMENT_SCOPES = ["before_booking_booking_level", "after_booking_due_by_participant"];
15379
- function isAfterBookingQuestionRequirementScope(scope) {
15380
- return scope === "after_booking_due_by_participant";
15317
+ //#region ../api/src/siteMembers.ts
15318
+ /**
15319
+ * Higher number means more permissions.
15320
+ */
15321
+ const SITE_ROLE_PERMISSION_LEVEL = {
15322
+ team_member: 0,
15323
+ editor: 1,
15324
+ administrator: 2,
15325
+ owner: 3
15326
+ };
15327
+ /**
15328
+ * Lower number sorts earlier in UI role pickers.
15329
+ */
15330
+ const SITE_ROLE_DISPLAY_ORDER = {
15331
+ owner: 0,
15332
+ administrator: 1,
15333
+ editor: 2,
15334
+ team_member: 3
15335
+ };
15336
+ async function getSiteMembers(apiClient, { siteId, options }) {
15337
+ try {
15338
+ return unwrapResponse(await apiClient({
15339
+ endpoint: "listSiteMembers",
15340
+ params: { siteId },
15341
+ options
15342
+ }));
15343
+ } catch (error) {
15344
+ return normalizeApiError(error, "Failed to load site members");
15345
+ }
15381
15346
  }
15382
- function isBeforeBookingQuestionRequirementScope(scope) {
15383
- return scope === "before_booking_booking_level";
15347
+ async function inviteSiteMember(apiClient, { siteId, body, options }) {
15348
+ try {
15349
+ return unwrapResponse(await apiClient({
15350
+ endpoint: "inviteSiteMember",
15351
+ params: { siteId },
15352
+ body,
15353
+ options
15354
+ }));
15355
+ } catch (error) {
15356
+ return normalizeApiError(error, "Failed to invite collaborator");
15357
+ }
15384
15358
  }
15385
- async function getBookingSettingsRequest(apiClient, siteId, options) {
15359
+ async function updateSiteMemberRole(apiClient, { siteId, memberId, body, options }) {
15386
15360
  try {
15387
- const response = unwrapResponse(await apiClient({
15388
- endpoint: "getBookingSettings",
15361
+ return unwrapResponse(await apiClient({
15362
+ endpoint: "updateSiteMemberRole",
15363
+ params: {
15364
+ siteId,
15365
+ memberId
15366
+ },
15367
+ body,
15368
+ options
15369
+ }));
15370
+ } catch (error) {
15371
+ return normalizeApiError(error, "Failed to update member role");
15372
+ }
15373
+ }
15374
+ async function linkCurrentUserPractitioner(apiClient, { siteId, body, options }) {
15375
+ try {
15376
+ return unwrapResponse(await apiClient({
15377
+ endpoint: "linkCurrentUserPractitioner",
15389
15378
  params: { siteId },
15379
+ body,
15390
15380
  options
15391
15381
  }));
15392
- return response.settings;
15393
15382
  } catch (error) {
15394
- normalizeApiError(error, "Failed to load booking settings");
15383
+ return normalizeApiError(error, "Failed to link practitioner profile");
15384
+ }
15385
+ }
15386
+ async function unlinkCurrentUserPractitioner(apiClient, { siteId, options }) {
15387
+ try {
15388
+ return unwrapResponse(await apiClient({
15389
+ endpoint: "unlinkCurrentUserPractitioner",
15390
+ params: { siteId },
15391
+ options
15392
+ }));
15393
+ } catch (error) {
15394
+ return normalizeApiError(error, "Failed to unlink practitioner profile");
15395
+ }
15396
+ }
15397
+ async function removeSiteMember(apiClient, { siteId, memberId, options }) {
15398
+ try {
15399
+ return unwrapResponse(await apiClient({
15400
+ endpoint: "removeSiteMember",
15401
+ params: {
15402
+ siteId,
15403
+ memberId
15404
+ },
15405
+ options
15406
+ }));
15407
+ } catch (error) {
15408
+ return normalizeApiError(error, "Failed to remove collaborator");
15409
+ }
15410
+ }
15411
+ async function revokeSiteInvitation(apiClient, { siteId, invitationId, options }) {
15412
+ try {
15413
+ return unwrapResponse(await apiClient({
15414
+ endpoint: "revokeSiteInvitation",
15415
+ params: {
15416
+ siteId,
15417
+ invitationId
15418
+ },
15419
+ options
15420
+ }));
15421
+ } catch (error) {
15422
+ return normalizeApiError(error, "Failed to revoke invitation");
15423
+ }
15424
+ }
15425
+ async function resendSiteInvitation(apiClient, { siteId, invitationId, options }) {
15426
+ try {
15427
+ return unwrapResponse(await apiClient({
15428
+ endpoint: "resendSiteInvitation",
15429
+ params: {
15430
+ siteId,
15431
+ invitationId
15432
+ },
15433
+ options
15434
+ }));
15435
+ } catch (error) {
15436
+ return normalizeApiError(error, "Failed to resend invitation");
15437
+ }
15438
+ }
15439
+ async function transferSiteOwnership(apiClient, { siteId, body, options }) {
15440
+ try {
15441
+ return unwrapResponse(await apiClient({
15442
+ endpoint: "transferSiteOwnership",
15443
+ params: { siteId },
15444
+ body,
15445
+ options
15446
+ }));
15447
+ } catch (error) {
15448
+ return normalizeApiError(error, "Failed to transfer ownership");
15449
+ }
15450
+ }
15451
+ async function acceptSiteInvitation(apiClient, { body, options }) {
15452
+ try {
15453
+ return unwrapResponse(await apiClient({
15454
+ endpoint: "acceptSiteInvitation",
15455
+ body,
15456
+ options
15457
+ }));
15458
+ } catch (error) {
15459
+ return normalizeApiError(error, "Failed to accept invitation");
15460
+ }
15461
+ }
15462
+
15463
+ //#endregion
15464
+ //#region ../api/src/staffAccess.ts
15465
+ function roleAtLeast$1(role, minimum) {
15466
+ return SITE_ROLE_PERMISSION_LEVEL[role] >= SITE_ROLE_PERMISSION_LEVEL[minimum];
15467
+ }
15468
+ function roleAtLeastOrNone(role, minimum) {
15469
+ if (!role) return false;
15470
+ return roleAtLeast$1(role, minimum);
15471
+ }
15472
+ function appointmentResourceIdFromStaffMemberId(staffMemberId) {
15473
+ return unsafeAsAppointmentResourceId(staffMemberId);
15474
+ }
15475
+ function normalizeStaffCapabilities(capabilities) {
15476
+ if (!capabilities) return null;
15477
+ return STAFF_OPERATIONAL_CAPABILITIES.filter((capability) => capabilities.includes(capability));
15478
+ }
15479
+ function deriveStaffScope(input) {
15480
+ const staffMemberId = input.linkedStaffMemberId ? unsafeAsStaffMemberId(input.linkedStaffMemberId) : null;
15481
+ const persistedCapabilities = normalizeStaffCapabilities(input.staffCapabilities);
15482
+ switch (input.membershipRole) {
15483
+ case null:
15484
+ case "owner":
15485
+ case "administrator":
15486
+ case "editor": return { kind: "none" };
15487
+ case "team_member":
15488
+ if (!staffMemberId) {
15489
+ return {
15490
+ kind: "staff_setup_required",
15491
+ reason: "missing_staff_member"
15492
+ };
15493
+ }
15494
+ if (persistedCapabilities === null) {
15495
+ return {
15496
+ kind: "appointments",
15497
+ staffMemberId
15498
+ };
15499
+ }
15500
+ if (persistedCapabilities.length === 0) {
15501
+ return {
15502
+ kind: "staff_setup_required",
15503
+ reason: "missing_capabilities"
15504
+ };
15505
+ }
15506
+ if (persistedCapabilities.includes("appointments") && persistedCapabilities.includes("event_ops")) {
15507
+ return {
15508
+ kind: "hybrid",
15509
+ staffMemberId
15510
+ };
15511
+ }
15512
+ if (persistedCapabilities.includes("event_ops")) {
15513
+ return {
15514
+ kind: "event_ops_only",
15515
+ staffMemberId
15516
+ };
15517
+ }
15518
+ return {
15519
+ kind: "appointments",
15520
+ staffMemberId
15521
+ };
15522
+ }
15523
+ }
15524
+ function isStaffScoped(scope) {
15525
+ return scope.kind !== "none";
15526
+ }
15527
+ function isTeamMemberScope(scope) {
15528
+ return scope.kind === "appointments" || scope.kind === "staff_setup_required";
15529
+ }
15530
+ function hasAppointmentStaffAccess(scope) {
15531
+ return scope.kind === "appointments" || scope.kind === "hybrid";
15532
+ }
15533
+ function hasEventOpsAccess(scope) {
15534
+ return scope.kind === "event_ops_only" || scope.kind === "hybrid";
15535
+ }
15536
+ function getLinkedStaffMemberId(scope) {
15537
+ switch (scope.kind) {
15538
+ case "appointments":
15539
+ case "event_ops_only":
15540
+ case "hybrid": return scope.staffMemberId;
15541
+ case "none":
15542
+ case "staff_setup_required": return null;
15395
15543
  }
15396
15544
  }
15397
- async function updateBookingSettingsRequest(apiClient, siteId, payload, options) {
15398
- try {
15399
- const response = unwrapResponse(await apiClient({
15400
- endpoint: "updateBookingSettings",
15401
- params: { siteId },
15402
- body: payload,
15403
- options
15404
- }));
15405
- return response.settings;
15406
- } catch (error) {
15407
- normalizeApiError(error, "Failed to save booking settings");
15408
- }
15545
+ function getOwnAppointmentResourceId(scope) {
15546
+ const staffMemberId = getLinkedStaffMemberId(scope);
15547
+ return staffMemberId ? appointmentResourceIdFromStaffMemberId(staffMemberId) : null;
15409
15548
  }
15410
- async function createAppointmentRequest(apiClient, siteId, payload, options) {
15411
- try {
15412
- return unwrapResponse(await apiClient({
15413
- endpoint: "createAppointment",
15414
- params: { siteId },
15415
- body: payload,
15416
- options
15417
- }));
15418
- } catch (error) {
15419
- normalizeApiError(error, "Failed to create appointment");
15420
- }
15549
+ function canAccessOwnAppointmentResource(scope, resourceId) {
15550
+ const ownResourceId = getOwnAppointmentResourceId(scope);
15551
+ if (!ownResourceId) return false;
15552
+ const normalizedResourceId = typeof resourceId === "string" ? unsafeAsAppointmentResourceId(resourceId) : resourceId;
15553
+ return ownResourceId === normalizedResourceId;
15421
15554
  }
15422
- async function deleteEventOfferingRequest(apiClient, siteId, eventOfferingId, options) {
15423
- try {
15424
- return unwrapResponse(await apiClient({
15425
- endpoint: "deleteEventOffering",
15426
- params: {
15427
- siteId,
15428
- eventOfferingId
15429
- },
15430
- options
15431
- }));
15432
- } catch (error) {
15433
- normalizeApiError(error, "Failed to delete event offering");
15555
+ function deriveLinkedAppointmentIdentity(input) {
15556
+ if (!input.membershipRole || !input.linkedStaffMemberId) {
15557
+ return { kind: "none" };
15434
15558
  }
15559
+ const staffMemberId = unsafeAsStaffMemberId(input.linkedStaffMemberId);
15560
+ return {
15561
+ kind: "linked",
15562
+ staffMemberId,
15563
+ resourceId: appointmentResourceIdFromStaffMemberId(staffMemberId)
15564
+ };
15435
15565
  }
15436
- async function addEventSeriesRegistrationRequest(apiClient, siteId, eventSeriesId, payload, options) {
15437
- try {
15438
- return unwrapResponse(await apiClient({
15439
- endpoint: "addEventSeriesRegistration",
15440
- params: {
15441
- siteId,
15442
- seriesId: eventSeriesId
15443
- },
15444
- body: payload,
15445
- options
15446
- }));
15447
- } catch (error) {
15448
- normalizeApiError(error, "Failed to add Event run registration");
15566
+ function getIdentityStaffMemberId(identity) {
15567
+ switch (identity.kind) {
15568
+ case "linked": return identity.staffMemberId;
15569
+ case "none": return null;
15449
15570
  }
15450
15571
  }
15451
- async function updateEventSeriesRegistrationRequest(apiClient, siteId, eventSeriesId, registrationId, payload, options) {
15452
- try {
15453
- return unwrapResponse(await apiClient({
15454
- endpoint: "updateEventSeriesRegistration",
15455
- params: {
15456
- siteId,
15457
- seriesId: eventSeriesId,
15458
- registrationId
15459
- },
15460
- body: payload,
15461
- options
15462
- }));
15463
- } catch (error) {
15464
- normalizeApiError(error, "Failed to update Event run registration");
15465
- }
15572
+ function getIdentityStaffMemberIdValue(identity) {
15573
+ return getIdentityStaffMemberId(identity);
15466
15574
  }
15467
- async function addEventOccurrenceAttendeeRequest(apiClient, siteId, seriesId, occurrenceId, payload, options) {
15468
- try {
15469
- return unwrapResponse(await apiClient({
15470
- endpoint: "addEventOccurrenceAttendee",
15471
- params: {
15472
- siteId,
15473
- seriesId,
15474
- occurrenceId
15475
- },
15476
- body: payload,
15477
- options
15478
- }));
15479
- } catch (error) {
15480
- normalizeApiError(error, "Failed to add attendee");
15575
+ function getIdentityAppointmentResourceId(identity) {
15576
+ return identity.kind === "linked" ? identity.resourceId : null;
15577
+ }
15578
+ function getIdentityAppointmentResourceIdValue(identity) {
15579
+ return getIdentityAppointmentResourceId(identity);
15580
+ }
15581
+ function isLinkedAppointmentResource(identity, resourceId) {
15582
+ if (identity.kind !== "linked") return false;
15583
+ return identity.resourceId === resourceId;
15584
+ }
15585
+ function deriveStaffSiteHomeDestination(staffScope) {
15586
+ if (hasAppointmentStaffAccess(staffScope)) {
15587
+ return { kind: "staff_appointments" };
15588
+ }
15589
+ if (hasEventOpsAccess(staffScope)) {
15590
+ return { kind: "ops_site_events" };
15481
15591
  }
15592
+ return { kind: "dashboard_site_home" };
15593
+ }
15594
+ function canUseAppointmentsLabel(staffScope) {
15595
+ return staffScope.kind === "appointments" || staffScope.kind === "hybrid" || staffScope.kind === "staff_setup_required";
15596
+ }
15597
+ function isEventOpsOnlyUser(sites) {
15598
+ if (sites.length === 0) return false;
15599
+ return sites.every((site) => deriveStaffScope(site).kind === "event_ops_only");
15600
+ }
15601
+ function deriveSiteListingNavigationPlan(site) {
15602
+ const staffScope = deriveStaffScope(site);
15603
+ const staffHomeDestination = deriveStaffSiteHomeDestination(staffScope);
15604
+ const isStaffScopedMembership = isStaffScoped(staffScope);
15605
+ return {
15606
+ canUseDashboard: roleAtLeastOrNone(site.membershipRole, "editor") || isStaffScopedMembership,
15607
+ primaryDestination: isStaffScopedMembership ? staffHomeDestination : { kind: "dashboard_site_open" },
15608
+ manageDestination: isStaffScopedMembership ? staffHomeDestination : { kind: "dashboard_site_home" },
15609
+ manageLabel: staffScope.kind === "event_ops_only" ? "Ops" : canUseAppointmentsLabel(staffScope) ? "Appointments" : "Manage"
15610
+ };
15611
+ }
15612
+ function shouldRedirectStaffScopeToOps(staffScope) {
15613
+ return staffScope.kind === "event_ops_only";
15482
15614
  }
15615
+ function deriveOpsLandingDestination(sites) {
15616
+ const onlySite = sites.length === 1 ? sites[0] : undefined;
15617
+ return onlySite?.id ? {
15618
+ kind: "ops_site_events",
15619
+ siteId: onlySite.id
15620
+ } : { kind: "ops_home" };
15621
+ }
15622
+
15623
+ //#endregion
15624
+ //#region ../api/src/common/cancellation.ts
15625
+ const STAFF_CANCELLATION_CONTEXT_UI_CATEGORIES = [
15626
+ "customer_requested",
15627
+ "schedule_conflict",
15628
+ "duplicate_or_admin_error",
15629
+ "non_payment",
15630
+ "policy_or_eligibility",
15631
+ "other"
15632
+ ];
15633
+ const STAFF_CANCELLATION_CONTEXT_CATEGORIES = [...STAFF_CANCELLATION_CONTEXT_UI_CATEGORIES, "legacy_client_unspecified"];
15634
+
15635
+ //#endregion
15636
+ //#region ../api/src/siteOperations.ts
15637
+ const FLEXIBLE_BALANCE_ADMIN_ACTION_SOURCES = [
15638
+ "order_detail",
15639
+ "payment_issues_queue",
15640
+ "appointment_detail",
15641
+ "event_attendee_detail",
15642
+ "event_series_registration_detail"
15643
+ ];
15483
15644
 
15484
15645
  //#endregion
15485
15646
  //#region ../api/src/navigation/visibility.ts
@@ -16291,19 +16452,8 @@ function buildDashboardBookingsNavPlan(modules, ctx, catalogue) {
16291
16452
  if (policy.kind === "staff_scoped") {
16292
16453
  return buildStaffScopedBookingsNavPlan(policy);
16293
16454
  }
16294
- const groups = [{
16295
- label: "Overview",
16296
- items: [{
16297
- label: "Dashboard",
16298
- destination: { kind: "bookings_dashboard" },
16299
- keywords: [
16300
- "overview",
16301
- "summary",
16302
- "activity"
16303
- ]
16304
- }]
16305
- }];
16306
16455
  const hasEnabledModules = modules.appointments || modules.events;
16456
+ const groups = [buildOverviewNavGroup(hasEnabledModules)];
16307
16457
  groups.push({
16308
16458
  label: "Events",
16309
16459
  items: modules.events ? [
@@ -16520,6 +16670,32 @@ function buildDashboardBookingsNavPlan(modules, ctx, catalogue) {
16520
16670
  }
16521
16671
  return groups;
16522
16672
  }
16673
+ /** At a glance: the bookings Overview, and the Timetable once anything can be booked. */
16674
+ function buildOverviewNavGroup(hasEnabledModules) {
16675
+ return {
16676
+ label: "At a glance",
16677
+ items: [{
16678
+ label: "Overview",
16679
+ destination: { kind: "bookings_dashboard" },
16680
+ keywords: [
16681
+ "overview",
16682
+ "summary",
16683
+ "activity"
16684
+ ]
16685
+ }, ...hasEnabledModules ? [{
16686
+ label: "Timetable",
16687
+ destination: { kind: "timetable" },
16688
+ keywords: [
16689
+ "schedule",
16690
+ "calendar",
16691
+ "week",
16692
+ "diary",
16693
+ "what's on",
16694
+ "clashes"
16695
+ ]
16696
+ }] : []]
16697
+ };
16698
+ }
16523
16699
  function buildStaffScopedBookingsNavPlan(policy) {
16524
16700
  if (policy.appointments.kind !== "calendar") return [];
16525
16701
  const appointmentItems = [{
@@ -17000,6 +17176,8 @@ function convertPulledPagesToLocal(remotePages) {
17000
17176
  return remotePages.map((page) => ({
17001
17177
  identifier: page.identifier,
17002
17178
  title: page.title,
17179
+ ...page.seoTitle != null ? { seoTitle: page.seoTitle } : {},
17180
+ ...page.seoDescription != null ? { seoDescription: page.seoDescription } : {},
17003
17181
  path: page.path,
17004
17182
  status: page.status,
17005
17183
  hasUnpublishedChanges: page.hasUnpublishedChanges,
@@ -20736,6 +20914,18 @@ function calculateEntryDiffs(localEntries, remoteEntries, options) {
20736
20914
  };
20737
20915
  }
20738
20916
 
20917
+ //#endregion
20918
+ //#region src/site-commands/pageMetadata.ts
20919
+ /** The supported metadata sent by a CLI page upsert; empty SEO is omitted. */
20920
+ function pageMetadataForUpsert(page) {
20921
+ return {
20922
+ title: page.title,
20923
+ path: page.path,
20924
+ ...page.seoTitle ? { seoTitle: page.seoTitle } : {},
20925
+ ...page.seoDescription ? { seoDescription: page.seoDescription } : {}
20926
+ };
20927
+ }
20928
+
20739
20929
  //#endregion
20740
20930
  //#region src/sync/pageDiff.ts
20741
20931
  function createPageSummary() {
@@ -20784,7 +20974,9 @@ function recordBlockDiff(summary, diff) {
20784
20974
  case "unchanged":
20785
20975
  summary.unchanged++;
20786
20976
  break;
20787
- case "reorder": break;
20977
+ case "reorder":
20978
+ summary.update++;
20979
+ break;
20788
20980
  }
20789
20981
  }
20790
20982
  function createBlockDiffs(blocks, pageIdentifier, summary) {
@@ -20827,64 +21019,47 @@ function classifyBlock(localBlock, remoteBlock, pageIdentifier) {
20827
21019
  };
20828
21020
  }
20829
21021
  function buildRemoteBlockLookup(remoteBlocks) {
20830
- const lookup = new Map();
20831
- for (const block of remoteBlocks) lookup.set(block.identifier, block);
20832
- return lookup;
21022
+ return new Map(remoteBlocks.map((block) => [block.identifier, block]));
20833
21023
  }
20834
- function appendLocalBlockDiffs(localBlocks, remoteLookup, pageIdentifier, summary) {
20835
- const diffs = [];
20836
- for (const localBlock of localBlocks) {
20837
- const diff = classifyBlock(localBlock, remoteLookup.get(localBlock.identifier), pageIdentifier);
20838
- diffs.push(diff);
20839
- recordBlockDiff(summary, diff);
20840
- }
20841
- return diffs;
21024
+ function localBlockDiffs(localBlocks, remoteLookup, pageIdentifier) {
21025
+ return localBlocks.map((block) => classifyBlock(block, remoteLookup.get(block.identifier), pageIdentifier));
20842
21026
  }
20843
- function appendDeletedBlockDiffs(remoteBlocks, localBlockIdentifiers, pageIdentifier, summary, diffs) {
20844
- for (const remoteBlock of remoteBlocks) {
20845
- if (localBlockIdentifiers.has(remoteBlock.identifier)) continue;
20846
- const diff = {
20847
- type: "delete",
20848
- identifier: remoteBlock.identifier,
20849
- pageIdentifier
20850
- };
20851
- diffs.push(diff);
20852
- recordBlockDiff(summary, diff);
20853
- }
21027
+ function deletedBlockDiffs(remoteBlocks, localBlockIdentifiers, pageIdentifier) {
21028
+ return remoteBlocks.filter((block) => !localBlockIdentifiers.has(block.identifier)).map((block) => ({
21029
+ type: "delete",
21030
+ identifier: block.identifier,
21031
+ pageIdentifier
21032
+ }));
20854
21033
  }
20855
- function appendReorderDiff(localBlocks, remoteBlocks, remoteLookup, localBlockIdentifiers, pageIdentifier, diffs) {
20856
- const hasCreates = diffs.some((diff) => diff.type === "create");
20857
- if (hasCreates && remoteBlocks.length > 0) {
20858
- diffs.push({
20859
- type: "reorder",
20860
- identifier: pageIdentifier,
20861
- pageIdentifier
20862
- });
20863
- return;
20864
- }
21034
+ function blockReorderDiff(localBlocks, remoteBlocks, remoteLookup, localBlockIdentifiers, pageIdentifier, diffs) {
21035
+ const reorder = {
21036
+ type: "reorder",
21037
+ identifier: pageIdentifier,
21038
+ pageIdentifier
21039
+ };
21040
+ if (diffs.some((diff) => diff.type === "create") && remoteBlocks.length > 0) return reorder;
20865
21041
  const localOrder = localBlocks.filter((block) => remoteLookup.has(block.identifier)).map((block) => block.identifier);
20866
21042
  const remoteOrder = remoteBlocks.filter((block) => localBlockIdentifiers.has(block.identifier)).sort((a, b) => a.position - b.position).map((block) => block.identifier);
20867
- if (localOrder.length > 1 && !equal(localOrder, remoteOrder)) {
20868
- diffs.push({
20869
- type: "reorder",
20870
- identifier: pageIdentifier,
20871
- pageIdentifier
20872
- });
20873
- }
21043
+ return localOrder.length > 1 && !equal(localOrder, remoteOrder) ? reorder : undefined;
20874
21044
  }
20875
21045
  function calculateBlockDiffs(localBlocks, remoteBlocks, pageIdentifier, summary) {
20876
21046
  const remoteLookup = buildRemoteBlockLookup(remoteBlocks);
20877
21047
  const localBlockIdentifiers = new Set(localBlocks.map((block) => block.identifier));
20878
- const diffs = appendLocalBlockDiffs(localBlocks, remoteLookup, pageIdentifier, summary);
20879
- appendDeletedBlockDiffs(remoteBlocks, localBlockIdentifiers, pageIdentifier, summary, diffs);
20880
- appendReorderDiff(localBlocks, remoteBlocks, remoteLookup, localBlockIdentifiers, pageIdentifier, diffs);
21048
+ const contentDiffs = [...localBlockDiffs(localBlocks, remoteLookup, pageIdentifier), ...deletedBlockDiffs(remoteBlocks, localBlockIdentifiers, pageIdentifier)];
21049
+ const reorder = blockReorderDiff(localBlocks, remoteBlocks, remoteLookup, localBlockIdentifiers, pageIdentifier, contentDiffs);
21050
+ const diffs = [...contentDiffs, ...reorder ? [reorder] : []];
21051
+ diffs.forEach((diff) => recordBlockDiff(summary, diff));
20881
21052
  return diffs;
20882
21053
  }
20883
21054
  function changedPageFields(localPage, remotePage) {
20884
- const changes = [];
20885
- if (localPage.title !== remotePage.title) changes.push("title");
20886
- if (localPage.path !== remotePage.path) changes.push("path");
20887
- return changes;
21055
+ const localSeo = resolvePageUpsertSeo(pageMetadataForUpsert(localPage));
21056
+ const remoteSeo = resolvePageUpsertSeo(remotePage);
21057
+ return [
21058
+ ...localPage.title !== remotePage.title ? ["title"] : [],
21059
+ ...localPage.path !== remotePage.path ? ["path"] : [],
21060
+ ...localSeo.seoTitle !== remoteSeo.seoTitle ? ["seoTitle"] : [],
21061
+ ...localSeo.seoDescription !== remoteSeo.seoDescription ? ["seoDescription"] : []
21062
+ ];
20888
21063
  }
20889
21064
  function classifyPage(localPage, remotePage, options, blockSummary) {
20890
21065
  if (!remotePage) {
@@ -20907,20 +21082,12 @@ function classifyPage(localPage, remotePage, options, blockSummary) {
20907
21082
  }
20908
21083
  const blockDiffs = calculateBlockDiffs(localPage.blocks ?? [], remotePage.blocks ?? [], localPage.identifier, blockSummary);
20909
21084
  const changes = changedPageFields(localPage, remotePage);
20910
- const hasBlockChanges = blockDiffs.some((diff) => diff.type !== "unchanged");
20911
- if (changes.length > 0 || hasBlockChanges) {
20912
- return {
20913
- type: "update",
20914
- identifier: localPage.identifier,
20915
- changes: changes.length > 0 ? changes : undefined,
20916
- blocks: blockDiffs,
20917
- remoteBlockIdentifiers: (remotePage.blocks ?? []).map((block) => block.identifier),
20918
- statusChange
20919
- };
20920
- }
20921
21085
  return {
20922
- type: "unchanged",
21086
+ type: changes.length > 0 ? "update" : "unchanged",
20923
21087
  identifier: localPage.identifier,
21088
+ changes: changes.length > 0 ? changes : undefined,
21089
+ blocks: blockDiffs,
21090
+ remoteBlockIdentifiers: (remotePage.blocks ?? []).map((block) => block.identifier),
20924
21091
  statusChange
20925
21092
  };
20926
21093
  }
@@ -20942,6 +21109,10 @@ function calculatePageDiffs(localPages, remotePages, options) {
20942
21109
  blockSummary
20943
21110
  };
20944
21111
  }
21112
+ /** A page may have block operations without changing its metadata. */
21113
+ function hasPageOperations(diff) {
21114
+ return diff.type !== "unchanged" || Boolean(diff.statusChange) || (diff.blocks ?? []).some((block) => block.type !== "unchanged");
21115
+ }
20945
21116
 
20946
21117
  //#endregion
20947
21118
  //#region src/sync/navigationDiffMatching.ts
@@ -21111,7 +21282,10 @@ function buildBlockChanges(pageDiff, localPages, remotePages, mode) {
21111
21282
  function buildPageChanges(diff, lookups, mode) {
21112
21283
  const changes = [];
21113
21284
  for (const pageDiff of diff.pages) {
21114
- if (pageDiff.type === "unchanged" && !pageDiff.statusChange) continue;
21285
+ if (pageDiff.type === "unchanged" && !pageDiff.statusChange) {
21286
+ changes.push(...buildBlockChanges(pageDiff, lookups.localPages, lookups.remotePages, mode));
21287
+ continue;
21288
+ }
21115
21289
  const change = {
21116
21290
  type: "page",
21117
21291
  identifier: pageDiff.identifier,
@@ -21341,11 +21515,13 @@ function calculatePullBlockDiffs(remoteBlocks, localBlocks, pageIdentifier, summ
21341
21515
  return diff;
21342
21516
  });
21343
21517
  }
21344
- function changedPullPageFields(remotePage, localPage) {
21345
- const changes = [];
21346
- if (remotePage.title !== localPage.title) changes.push("title");
21347
- if (remotePage.path !== localPage.path) changes.push("path");
21348
- return changes;
21518
+ function changedPullPageFields(localPage, remotePage) {
21519
+ return [
21520
+ ...localPage.title !== remotePage.title ? ["title"] : [],
21521
+ ...localPage.path !== remotePage.path ? ["path"] : [],
21522
+ ...localPage.seoTitle !== (remotePage.seoTitle ?? undefined) ? ["seoTitle"] : [],
21523
+ ...localPage.seoDescription !== (remotePage.seoDescription ?? undefined) ? ["seoDescription"] : []
21524
+ ];
21349
21525
  }
21350
21526
  function calculatePullPageDiffs(remotePages, local) {
21351
21527
  const diffs = [];
@@ -21365,7 +21541,7 @@ function calculatePullPageDiffs(remotePages, local) {
21365
21541
  continue;
21366
21542
  }
21367
21543
  const blockDiffs = calculatePullBlockDiffs(remotePage.blocks ?? [], localPage.blocks ?? [], remotePage.identifier, blockSummary);
21368
- const changes = changedPullPageFields(remotePage, localPage);
21544
+ const changes = changedPullPageFields(localPage, remotePage);
21369
21545
  const diff = changes.length > 0 || blockDiffs.some((block) => block.type !== "unchanged") ? {
21370
21546
  type: "update",
21371
21547
  identifier: remotePage.identifier,
@@ -21621,11 +21797,17 @@ function formatPageDetails(diff, localPages, remotePages) {
21621
21797
  const localPage = localPages.get(diff.identifier);
21622
21798
  const remotePage = remotePages.get(diff.identifier);
21623
21799
  if (!localPage || !remotePage) return [];
21624
- const localFlat = { ...localPage };
21625
- const remoteFlat = { ...remotePage };
21626
- delete localFlat.blocks;
21627
- delete remoteFlat.blocks;
21628
- return findChangedFieldsWithValues(localFlat, remoteFlat).map((change) => ` ${formatFieldChange(change)}`);
21800
+ const localFields = {
21801
+ title: localPage.title,
21802
+ path: localPage.path,
21803
+ ...resolvePageUpsertSeo(pageMetadataForUpsert(localPage))
21804
+ };
21805
+ const remoteFields = {
21806
+ title: remotePage.title,
21807
+ path: remotePage.path,
21808
+ ...resolvePageUpsertSeo(remotePage)
21809
+ };
21810
+ return findChangedFieldsWithValues(localFields, remoteFields).map((change) => ` ${formatFieldChange(change)}`);
21629
21811
  }
21630
21812
  function buildIdentifierLookup(items) {
21631
21813
  const lookup = new Map();
@@ -21642,6 +21824,11 @@ function formatHumanDiff(diff, local, remote) {
21642
21824
  const localPages = buildIdentifierLookup(local.pages);
21643
21825
  const remotePages = buildIdentifierLookup(remote.pages);
21644
21826
  appendOperationSections$1(lines, "Pages", diff.pages, CREATE_UPDATE_OPERATIONS, (page) => page.identifier, (page) => formatPageDetails(page, localPages, remotePages));
21827
+ appendOperationSections$1(lines, "Blocks", diff.pages.flatMap((page) => page.blocks ?? []), [...ENTRY_OPERATIONS, {
21828
+ type: "reorder",
21829
+ label: "reordered",
21830
+ marker: "~"
21831
+ }], (block) => `${block.pageIdentifier}/${block.identifier}`);
21645
21832
  appendOperationSections$1(lines, "Navigation", diff.navigation, CREATE_UPDATE_OPERATIONS, (navigation) => navigation.type === "update" && navigation.changes ? `${navigation.location} [${navigation.changes.join(", ")}]` : navigation.location);
21646
21833
  if (diff.settings) {
21647
21834
  lines.push("\nSettings (updated):");
@@ -21861,13 +22048,21 @@ function appendSingleUpdateSection(lines, title, detail) {
21861
22048
  lines.push(`\n${title}:`);
21862
22049
  lines.push(` ~ ${detail}`);
21863
22050
  }
22051
+ function appendPageDetailSections(lines, pages) {
22052
+ appendOperationSections(lines, "Pages", pages, CREATE_UPDATE_DETAIL_OPERATIONS, (page) => formatIdentifierDetail(page.identifier, page.changes));
22053
+ appendOperationSections(lines, "Blocks", pages.flatMap((page) => page.blocks ?? []), [...CREATE_UPDATE_DELETE_DETAIL_OPERATIONS, {
22054
+ type: "reorder",
22055
+ label: "reordered",
22056
+ marker: "~"
22057
+ }], (block) => formatIdentifierDetail(`${block.pageIdentifier}/${block.identifier}`, block.changes));
22058
+ }
21864
22059
  /** Format detailed diff for display. */
21865
22060
  function formatDiffDetail(diff) {
21866
22061
  const lines = [];
21867
22062
  for (const [contentType, entryDiffs] of diff.entries) {
21868
22063
  appendOperationSections(lines, contentType, entryDiffs, CREATE_UPDATE_DELETE_DETAIL_OPERATIONS, (entry) => formatIdentifierDetail(entry.identifier, entry.changes));
21869
22064
  }
21870
- appendOperationSections(lines, "Pages", diff.pages, CREATE_UPDATE_DETAIL_OPERATIONS, (page) => formatIdentifierDetail(page.identifier, page.changes));
22065
+ appendPageDetailSections(lines, diff.pages);
21871
22066
  appendOperationSections(lines, "Navigation", diff.navigation, CREATE_UPDATE_DETAIL_OPERATIONS, (navigation) => navigation.location);
21872
22067
  if (diff.settings) {
21873
22068
  appendSingleUpdateSection(lines, "Settings (updated)", `[${diff.settings.changes.join(", ")}]`);
@@ -23107,4 +23302,4 @@ function optionalTitle(data) {
23107
23302
  }
23108
23303
 
23109
23304
  //#endregion
23110
- export { buildStorageUrl as $, createClientForEnv as $t, formatDiffDetail as A, createOutput as An, convertPulledFooterToLocal as At, formatFieldPath as B, linkPayloadSchema as Bn, createPushConfigCommand as Bt, reportFromOutcomes as C, planEnvironmentWriteAuthorization as Cn, readJsonFile as Ct, normalizeRemoteHomepageReference as D, formatError as Dn, duplicateNavigationIdentityMessage as Dt, createEmptySyncResult as E, ErrorCodes as En, isMediaValue as Et, calculatePullDiff as F, loadSdkEnvironment as Fn, normalizeNavigationConfigItemForCommand as Ft, prepareLocalMediaForPush as G, createBearerAPIClient as Gt, syncLocalMediaForPush as H, sdkConfiguredBookingVerticalConfigSchema as Hn, preparePushConfigTarget as Ht, buildJsonDiff as I, createManagementClient as In, extractApiErrorMessage as It, writeBinaryFileAtomic as J, deriveCmsApiUrlFromDashboardUrl as Jt, admitBinaryWriteDestination as K, ApiEnvelopeError as Kt, calculateEntryDiffs as L, ManagementApiError as Ln, normalizeApiError as Lt, hasPendingChanges as M, getEnvironmentDebugInfo as Mn, convertPulledPagesToLocal as Mt, findChangedFieldsWithValues as N, isLocalConfigured as Nn, APPOINTMENT_SERVICE_VISIBILITY_VALUES as Nt, calculateDiff as O, transportSuggestion as On, navigationIdentity as Ot, formatHumanDiff as P, isRemoteConfigured as Pn, navigationCommandItemToManagementInput as Pt, walkLocalContentMedia as Q, confirmAction as Qt, buildFieldChanges as R, sdkEventScheduleSchema as Rn, isRecord$5 as Rt, planCliCommandBatchForBindings as S, planEnvironmentExecution as Sn, listFiles as St, executeResidualSettingsSyncPlan as T, withRiverbankEnv as Tn, writeJsonFileAtomic as Tt, syncLocalMediaForPull as U, revalidatePushConfigPlan as Ut, revalidateLocalMediaPushPlan as V, portablePageDesignSchema as Vn, loadPushConfigTarget as Vt, applyLocalMediaPush as W, ApiRequestError as Wt, normalizeMediaIdentifierInput as X, mapEnvironmentExecutionOutcomesJson as Xt, sha256 as Y, API_ENDPOINTS as Yt, normalizeMediaValueToIdentifierOnly as Z, bindRequestTimeout as Zt, commandExecutionToReport as _, withConfirmation as _n, readMetadataFile as _t, executeEntryCommandPlanAsReport as a, formatDateShort as an, readEventCategoriesMeta as at, executeCompiledCliCommandPlanAsReport as b, executeEnvironmentPlan as bn, ensureDir$1 as bt, titleFromEntryData as c, handleCommandError as cn, readFormsMeta as ct, unsafeAsCliEntryIdentifier as d, parseJsonArray as dn, readSettingsMeta as dt, createCommandContext as en, downloadMedia as et, unsafeAsCliFormSlug as f, parseJsonData as fn, readVenuesMeta as ft, commandCompileFailureToReport as g, withCommandContext as gn, metadataTargetFromEnvironment as gt, mutationCounterForEffect as h, runWithInvocation as hn, metadataFilePath as ht, entrySiteCommandTypes as i, currentInvocationEnvelope as in, readAllMeta as it, formatDiffSummary as j, getEnvPrefix as jn, convertPulledNavigationToLocal as jt, calculateFooterDiff as k, CliExit as kn, navigationIdentityMap as kt, unsafeAsCliBlockIdentifier as l, managementClientConfigFor as ln, readNavigationMeta as lt, commandReportToSyncResult as m, resolveCommandPath as mn, ensureMetadataDir as mt, entryCommandCompileErrorToReport as n, createListCommand as nn, findLegacyEntryFiles as nt, executeEntryCommandPlanAsSyncResult as o, getOutputContext as on, readEventsMeta as ot, unsafeAsCliPageIdentifier as p, parsePaginationOptions as pn, writeMediaManifest as pt, readStableBinaryFile as q, unwrapResponse as qt, entryCommandReportToSyncResult as r, createOutputForCommand as rn, readEntries as rt, syncResultForEntryCommandCompileError as s, getOutputOnlyContext as sn, readFooterMeta as st, compileEntryPushCommands as t, createGetCommand as tn, extractMediaIdentifiers as tt, unsafeAsCliContentTypeKey as u, mapEntryForOutput as un, readPagesMeta as ut, commandReportHasFailures as v, withErrorHandling as vn, writeMetadataFile as vt, mergeManySyncResults as w, requireEnvironmentWriteAuthorization as wn, writeJsonFile$1 as wt, formatManagementError as x, parseEnvironmentSelection as xn, fileExists$1 as xt, executeCliCommandPlan as y, withOutputOnlyErrorHandling as yn, writeMetadataRecordMap as yt, formatFieldChange as z, sdkSettingsUpdateSchema as zn, applyPushConfigPlan as zt };
23305
+ export { normalizeMediaValueToIdentifierOnly as $, bindRequestTimeout as $t, formatDiffDetail as A, transportSuggestion as An, navigationIdentity as At, buildFieldChanges as B, sdkEventScheduleSchema as Bn, preparePushConfigTarget as Bt, reportFromOutcomes as C, parseEnvironmentSelection as Cn, fileExists$1 as Ct, normalizeRemoteHomepageReference as D, withRiverbankEnv as Dn, writeJsonFileAtomic as Dt, createEmptySyncResult as E, requireEnvironmentWriteAuthorization as En, writeJsonFile$1 as Et, calculatePullDiff as F, isLocalConfigured as Fn, navigationCommandItemToManagementInput as Ft, syncLocalMediaForPull as G, APPOINTMENT_SERVICE_VISIBILITY_VALUES as Gt, formatFieldPath as H, linkPayloadSchema as Hn, extractApiErrorMessage as Ht, buildJsonDiff as I, isRemoteConfigured as In, normalizeNavigationConfigItemForCommand as It, admitBinaryWriteDestination as J, ApiEnvelopeError as Jt, applyLocalMediaPush as K, ApiRequestError as Kt, hasPageOperations as L, loadSdkEnvironment as Ln, applyPushConfigPlan as Lt, hasPendingChanges as M, createOutput as Mn, convertPulledFooterToLocal as Mt, findChangedFieldsWithValues as N, getEnvPrefix as Nn, convertPulledNavigationToLocal as Nt, calculateDiff as O, ErrorCodes as On, isMediaValue as Ot, formatHumanDiff as P, getEnvironmentDebugInfo as Pn, convertPulledPagesToLocal as Pt, normalizeMediaIdentifierInput as Q, mapEnvironmentExecutionOutcomesJson as Qt, pageMetadataForUpsert as R, createManagementClient as Rn, createPushConfigCommand as Rt, planCliCommandBatchForBindings as S, executeEnvironmentPlan as Sn, ensureDir$1 as St, executeResidualSettingsSyncPlan as T, planEnvironmentWriteAuthorization as Tn, readJsonFile as Tt, revalidateLocalMediaPushPlan as U, portablePageDesignSchema as Un, normalizeApiError as Ut, formatFieldChange as V, sdkSettingsUpdateSchema as Vn, revalidatePushConfigPlan as Vt, syncLocalMediaForPush as W, sdkConfiguredBookingVerticalConfigSchema as Wn, isRecord$5 as Wt, writeBinaryFileAtomic as X, deriveCmsApiUrlFromDashboardUrl as Xt, readStableBinaryFile as Y, unwrapResponse as Yt, sha256 as Z, API_ENDPOINTS as Zt, commandExecutionToReport as _, runWithInvocation as _n, metadataFilePath as _t, executeEntryCommandPlanAsReport as a, createOutputForCommand as an, readEntries as at, executeCompiledCliCommandPlanAsReport as b, withErrorHandling as bn, writeMetadataFile as bt, titleFromEntryData as c, getOutputContext as cn, readEventsMeta as ct, unsafeAsCliEntryIdentifier as d, managementClientConfigFor as dn, readNavigationMeta as dt, confirmAction as en, walkLocalContentMedia as et, unsafeAsCliFormSlug as f, mapEntryForOutput as fn, readPagesMeta as ft, commandCompileFailureToReport as g, resolveCommandPath as gn, ensureMetadataDir as gt, mutationCounterForEffect as h, parsePaginationOptions as hn, writeMediaManifest as ht, entrySiteCommandTypes as i, createListCommand as in, findLegacyEntryFiles as it, formatDiffSummary as j, CliExit as jn, navigationIdentityMap as jt, calculateFooterDiff as k, formatError as kn, duplicateNavigationIdentityMessage as kt, unsafeAsCliBlockIdentifier as l, getOutputOnlyContext as ln, readFooterMeta as lt, commandReportToSyncResult as m, parseJsonData as mn, readVenuesMeta as mt, entryCommandCompileErrorToReport as n, createCommandContext as nn, downloadMedia as nt, executeEntryCommandPlanAsSyncResult as o, currentInvocationEnvelope as on, readAllMeta as ot, unsafeAsCliPageIdentifier as p, parseJsonArray as pn, readSettingsMeta as pt, prepareLocalMediaForPush as q, createBearerAPIClient as qt, entryCommandReportToSyncResult as r, createGetCommand as rn, extractMediaIdentifiers as rt, syncResultForEntryCommandCompileError as s, formatDateShort as sn, readEventCategoriesMeta as st, compileEntryPushCommands as t, createClientForEnv as tn, buildStorageUrl as tt, unsafeAsCliContentTypeKey as u, handleCommandError as un, readFormsMeta as ut, commandReportHasFailures as v, withCommandContext as vn, metadataTargetFromEnvironment as vt, mergeManySyncResults as w, planEnvironmentExecution as wn, listFiles as wt, formatManagementError as x, withOutputOnlyErrorHandling as xn, writeMetadataRecordMap as xt, executeCliCommandPlan as y, withConfirmation as yn, readMetadataFile as yt, calculateEntryDiffs as z, ManagementApiError as zn, loadPushConfigTarget as zt };