riverbankcms 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 footerPreferencesSchema, C as stableStringify, Ct as themeSchema, D as evaluateSiteCommandBatchExposure, Fn as buildMediaIdentifierFromFilename, Gn as normalizeAmbiguousStoragePathForSite, Hn as getExtensionForMimeType, J as createContentEntryRef, Jn as MAX_MANAGEMENT_MEDIA_UPLOAD_BYTES, N as entryIdentifierForCommand, O as evaluateSiteCommandExposure, Qt as pageDesignIntentSchemaVersion, Rn as resolveMediaIdentifier, T as parseAndPlanCommandBatchForTypes, Un as getMimeType, Ut as buttonStyleSelectionSchema, Zt as pageDesignBoundaryTreatmentSchema, it as slugPart, k as getSiteCommandMetadata, lr as asPageFlowPresetId, m as isInternalResolvedLinkValue, n as sdkCourseBookingSettingsSchema, or as asAppearancePresetId, q as createCommandRef, qn as FILE_SIZE_LIMITS, r as sdkEventOccurrenceInputSchema, s as RESOLVED_INTERNAL_LINK_ENTITY_TYPES, tt as createSiteCommandSiteRef, ur as asSiteStyleId, w as createDryRunReport, wn as getDirectImageUrlForSiteScopedPath, y as eventVisibilitySchema, z as eventBookingOfferIntentSchema, zn as slugifyMediaIdentifier } from "./sdk-event-upsert-BqjzK2zT.mjs";
3
+ import { Bn as ok, Cn as unsafeAsOccurrenceId, M as parseFooterCompositionV1, Pn as unsafeAsStaffMemberId, S as parseManagementKey, Xt as assertNever, a as canonicalizeConfiguredBookingVerticalConfig, an as parsePageId, b as createSecret, bn as unsafeAsEventSeriesId, c as resolveSiteVerticalBookingConfig, i as areConfiguredBookingVerticalConfigsEqual, l as DEFAULT_EVENT_TYPE_PRESETS, m as publicReadFreshnessSeconds, o as parseVerticalId, s as parseVerticalPresetId, sn as parseSiteId, t as resolveEventCatalogue, u as EVENT_PRESET_VALUES, un as unsafeAsAppointmentResourceId, x as exposeSecret, yn as unsafeAsEventOfferingId, zn as err } from "./src-zzwJUef5.mjs";
4
+ import { n as CliSdkPeerError, t as loadConfigFile } from "./load-config-j58KSqxl.mjs";
5
5
  import { Command } from "commander";
6
6
  import { AsyncLocalStorage } from "node:async_hooks";
7
7
  import * as fs$1 from "fs/promises";
@@ -4434,6 +4434,31 @@ function parsePublicProductCategorySelector(value) {
4434
4434
  return undefined;
4435
4435
  }
4436
4436
 
4437
+ //#endregion
4438
+ //#region ../api/src/apiEndpointTypes.ts
4439
+ function allowedDemoMutationPolicy() {
4440
+ return {
4441
+ classification: "allowed",
4442
+ publicMessage: ""
4443
+ };
4444
+ }
4445
+ function blockedDemoMutationPolicy(classification, publicMessage) {
4446
+ if (publicMessage.trim() === "") {
4447
+ throw new Error(`Demo mutation policy ${classification} requires publicMessage.`);
4448
+ }
4449
+ return {
4450
+ classification,
4451
+ publicMessage
4452
+ };
4453
+ }
4454
+
4455
+ //#endregion
4456
+ //#region ../api/src/demoMutationPolicies.ts
4457
+ const DEMO_MUTATION_BLOCKED_HIDDEN = blockedDemoMutationPolicy("blocked-hidden", "This action is not available in demo sites.");
4458
+ const DEMO_MUTATION_BLOCKED_DISABLED = blockedDemoMutationPolicy("blocked-disabled", "This setting is disabled in demo sites.");
4459
+ const DEMO_MUTATION_INTERNAL_ONLY = blockedDemoMutationPolicy("internal-only", "This internal operation is not available in demo sites.");
4460
+ const DEMO_MUTATION_ALLOWED = allowedDemoMutationPolicy();
4461
+
4437
4462
  //#endregion
4438
4463
  //#region ../api/src/defineEndpointDefinitions.ts
4439
4464
  /**
@@ -4444,6 +4469,50 @@ function defineEndpointDefinitions(definitions) {
4444
4469
  return definitions;
4445
4470
  }
4446
4471
 
4472
+ //#endregion
4473
+ //#region ../api/src/bookingAttentionEndpointDefinitions.ts
4474
+ const BOOKING_ATTENTION_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
4475
+ dismissBookingAttention: {
4476
+ demoMutation: DEMO_MUTATION_ALLOWED,
4477
+ path: "/sites/{siteId}/bookings/attention/dismiss",
4478
+ method: "POST",
4479
+ tags: ["site-{siteId}"],
4480
+ auth: "user",
4481
+ responseKind: "json",
4482
+ errors: [
4483
+ "validation:invalid_input",
4484
+ "auth:forbidden",
4485
+ "server:internal_error"
4486
+ ]
4487
+ },
4488
+ resendWaitlistInvite: {
4489
+ path: "/sites/{siteId}/bookings/waitlist-entries/{entryId}/invite",
4490
+ method: "POST",
4491
+ tags: ["site-{siteId}"],
4492
+ auth: "user",
4493
+ responseKind: "json",
4494
+ errors: [
4495
+ "validation:invalid_input",
4496
+ "resource:not_found",
4497
+ "resource:conflict",
4498
+ "server:internal_error"
4499
+ ]
4500
+ },
4501
+ resendAppointmentPaymentRequest: {
4502
+ demoMutation: DEMO_MUTATION_BLOCKED_DISABLED,
4503
+ path: "/sites/{siteId}/bookings/appointment-payment-requests/{requestId}/resend",
4504
+ method: "POST",
4505
+ tags: ["site-{siteId}"],
4506
+ auth: "user",
4507
+ responseKind: "json",
4508
+ errors: [
4509
+ "validation:invalid_input",
4510
+ "auth:forbidden",
4511
+ "server:internal_error"
4512
+ ]
4513
+ }
4514
+ });
4515
+
4447
4516
  //#endregion
4448
4517
  //#region ../api/src/previewAuthEndpointDefinitions.ts
4449
4518
  const PREVIEW_AUTH_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
@@ -4470,31 +4539,6 @@ const PREVIEW_AUTH_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
4470
4539
  }
4471
4540
  });
4472
4541
 
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
4542
  //#endregion
4499
4543
  //#region ../api/src/siteLifecycleEndpoints.ts
4500
4544
  const SITE_LIFECYCLE_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
@@ -4827,6 +4871,42 @@ const SDK_ENTRY_READ_ENDPOINTS = {
4827
4871
  }
4828
4872
  };
4829
4873
 
4874
+ //#endregion
4875
+ //#region ../api/src/appointmentRequestEndpointDefinitions.ts
4876
+ const APPOINTMENT_REQUEST_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
4877
+ approveAppointmentRequest: {
4878
+ path: "/sites/{siteId}/bookings/appointments/{appointmentId}/approve",
4879
+ method: "POST",
4880
+ auth: "user",
4881
+ responseKind: "json",
4882
+ errors: [
4883
+ "validation:invalid_input",
4884
+ "resource:not_found",
4885
+ "auth:forbidden",
4886
+ "server:internal_error"
4887
+ ]
4888
+ },
4889
+ declineAppointmentRequest: {
4890
+ path: "/sites/{siteId}/bookings/appointments/{appointmentId}/decline",
4891
+ method: "POST",
4892
+ auth: "user",
4893
+ responseKind: "json",
4894
+ errors: [
4895
+ "validation:invalid_input",
4896
+ "resource:not_found",
4897
+ "auth:forbidden",
4898
+ "server:internal_error"
4899
+ ]
4900
+ },
4901
+ listWaitingAppointmentRequests: {
4902
+ path: "/sites/{siteId}/bookings/appointments/requests",
4903
+ method: "GET",
4904
+ auth: "user",
4905
+ responseKind: "json",
4906
+ errors: ["auth:forbidden", "server:internal_error"]
4907
+ }
4908
+ });
4909
+
4830
4910
  //#endregion
4831
4911
  //#region ../api/src/appointmentServiceCategoryEndpoints.ts
4832
4912
  const APPOINTMENT_SERVICE_CATEGORY_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
@@ -4911,6 +4991,7 @@ const APPOINTMENT_SERVICE_CATEGORY_ENDPOINT_DEFINITIONS = defineEndpointDefiniti
4911
4991
  //#region ../api/src/appointmentEndpointDefinitions.ts
4912
4992
  const APPOINTMENT_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
4913
4993
  ...APPOINTMENT_SERVICE_CATEGORY_ENDPOINT_DEFINITIONS,
4994
+ ...APPOINTMENT_REQUEST_ENDPOINT_DEFINITIONS,
4914
4995
  listAppointmentResources: {
4915
4996
  path: "/sites/{siteId}/bookings/resources",
4916
4997
  method: "GET",
@@ -6883,6 +6964,14 @@ const GIFTING_ENDPOINT_DEFINITIONS = defineEndpointDefinitions({
6883
6964
  auth: "public",
6884
6965
  responseKind: "json"
6885
6966
  },
6967
+ previewGiftCardClaimPublic: {
6968
+ path: "/public/sites/{siteId}/gifting/claim",
6969
+ method: "GET",
6970
+ tags: [TAG_TEMPLATES.publicGifting],
6971
+ errors: [],
6972
+ auth: "public",
6973
+ responseKind: "json"
6974
+ },
6886
6975
  claimGiftCardPublic: {
6887
6976
  path: "/public/sites/{siteId}/gifting/claim",
6888
6977
  method: "POST",
@@ -10631,6 +10720,14 @@ const ENDPOINT_DEFINITIONS = {
10631
10720
  auth: "public",
10632
10721
  responseKind: "text"
10633
10722
  },
10723
+ getPublicAppointmentCalendarIcs: {
10724
+ path: "/public/sites/{siteId}/bookings/appointments/{token}/appointment.ics",
10725
+ method: "GET",
10726
+ tags: ["public-appointment-calendar-{siteId}"],
10727
+ auth: "public",
10728
+ responseKind: "text",
10729
+ errors: ["resource:not_found", "server:internal_error"]
10730
+ },
10634
10731
  getPublicBookingCalendarFeedIcs: {
10635
10732
  path: "/public/sites/{siteId}/bookings/calendar-feeds/{token}/feed.ics",
10636
10733
  method: "GET",
@@ -10967,19 +11064,7 @@ const ENDPOINT_DEFINITIONS = {
10967
11064
  responseKind: "json",
10968
11065
  errors: ["validation:invalid_input", "server:internal_error"]
10969
11066
  },
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
- },
11067
+ ...BOOKING_ATTENTION_ENDPOINT_DEFINITIONS,
10983
11068
  cancelWaitlistEntry: {
10984
11069
  path: "/sites/{siteId}/bookings/waitlist-entries/{entryId}/cancel",
10985
11070
  method: "POST",
@@ -13834,1653 +13919,1648 @@ function createBearerAPIClient(token, baseUrl) {
13834
13919
  }
13835
13920
 
13836
13921
  //#endregion
13837
- //#region src/push-config-sync.ts
13838
- function isConfigSync(sync) {
13839
- return "ignoredDocumentSections" in sync;
13840
- }
13841
- function getPushConfigSync(result) {
13842
- if (result.configSync) return result.configSync;
13843
- if (result.sync) return result.sync;
13844
- return null;
13845
- }
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
- ];
13922
+ //#region ../api/src/appointmentServiceVisibility.ts
13923
+ const APPOINTMENT_SERVICE_VISIBILITY_VALUES = ["public", "unlisted"];
13924
+ function isAppointmentServiceVisibility(value) {
13925
+ return value === "public" || value === "unlisted";
13910
13926
  }
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
- }] : []];
13927
+
13928
+ //#endregion
13929
+ //#region ../api/src/appointmentServiceLocation.ts
13930
+ const APPOINTMENT_SERVICE_LOCATION_TYPE_VALUES = [
13931
+ "virtual",
13932
+ "in_person",
13933
+ "hybrid"
13934
+ ];
13935
+ function isAppointmentServiceLocationType(value) {
13936
+ return value === "virtual" || value === "in_person" || value === "hybrid";
13917
13937
  }
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
- } };
13938
+
13939
+ //#endregion
13940
+ //#region ../api/src/bookingOperations.ts
13941
+ const APPOINTMENT_RESERVATION_STATUS_VALUES = [
13942
+ "active",
13943
+ "paused",
13944
+ "ended"
13945
+ ];
13946
+
13947
+ //#endregion
13948
+ //#region ../api/src/utils/isRecord.ts
13949
+ function isRecord$5(value) {
13950
+ return typeof value === "object" && value !== null && !Array.isArray(value);
13927
13951
  }
13928
13952
 
13929
13953
  //#endregion
13930
- //#region src/push-config.ts
13954
+ //#region ../api/src/utils/error-propagation.ts
13931
13955
  /**
13932
- * push-config CLI command
13933
- *
13934
- * Pushes SDK site configuration from a riverbank.config.ts file to the dashboard API.
13956
+ * Extracts an error message from an API error response body.
13957
+ * Checks for our standard API error format: { error: { message: "..." } }
13958
+ * Also handles legacy formats with `error` or `message` as strings.
13935
13959
  */
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);
13960
+ function extractApiErrorMessage(body) {
13961
+ if (!isRecord$5(body)) {
13962
+ return null;
13970
13963
  }
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);
13964
+ if (isRecord$5(body.error)) {
13965
+ if (typeof body.error.message === "string" && body.error.message.trim().length > 0) {
13966
+ return body.error.message;
14020
13967
  }
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
13968
  }
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("; ")}`);
13969
+ if (typeof body.error === "string" && body.error.trim().length > 0) {
13970
+ return body.error;
14033
13971
  }
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
- };
13972
+ if (typeof body.message === "string" && body.message.trim().length > 0) {
13973
+ return body.message;
14047
13974
  }
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
- };
13975
+ return null;
14068
13976
  }
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) });
14098
- }
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);
14101
- }
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
13977
+ /**
13978
+ * Normalizes various error types and throws with an appropriate message.
13979
+ *
13980
+ * - For ApiRequestError: extracts message from body.error or body.message
13981
+ * - For Error instances: re-throws as-is
13982
+ * - For unknown errors: wraps with fallback message
13983
+ *
13984
+ * @param error - The error to normalize
13985
+ * @param fallback - Fallback message if no specific message can be extracted
13986
+ * @throws {Error} Always throws - this function never returns
13987
+ */
13988
+ function normalizeApiError(error, fallback) {
13989
+ if (error instanceof ApiRequestError) {
13990
+ const message = extractApiErrorMessage(error.body);
13991
+ throw new ApiRequestError(message ?? fallback, {
13992
+ endpoint: error.endpoint,
13993
+ status: error.status,
13994
+ method: error.method,
13995
+ auth: error.auth,
13996
+ requestId: error.requestId,
13997
+ body: error.body,
13998
+ cause: error,
13999
+ errorCode: error.errorCode,
14000
+ retryAfterMs: error.retryAfterMs
14110
14001
  });
14111
- } else {
14112
- output.success(analysis.resultMode === "preview" ? "Config preview completed. No changes were applied." : "Config pushed successfully!");
14113
14002
  }
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 };
14003
+ if (error instanceof Error) {
14004
+ throw error;
14141
14005
  }
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
- };
14192
- }
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;
14197
- }
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}` : ""}`);
14202
- }
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
- };
14209
- }
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.`);
14218
- }
14219
- return {
14220
- config,
14221
- preview,
14222
- forceContentTypes: options.forceContentTypes ?? false
14223
- };
14224
- }
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) {
14231
- 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
- };
14240
- } catch (error) {
14241
- return {
14242
- ok: false,
14243
- error: error instanceof Error ? error : new Error(String(error))
14244
- };
14245
- }
14246
- }
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
- }
14375
- }
14376
- if (typeof body.error === "string" && body.error.trim().length > 0) {
14377
- return body.error;
14378
- }
14379
- if (typeof body.message === "string" && body.message.trim().length > 0) {
14380
- return body.message;
14381
- }
14382
- return null;
14383
- }
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
- });
14409
- }
14410
- if (error instanceof Error) {
14411
- throw error;
14412
- }
14413
- throw new Error(fallback);
14414
- }
14415
-
14416
- //#endregion
14417
- //#region ../api/src/navigationMenuValidation.ts
14418
- function hasChildren(item) {
14419
- return Array.isArray(item.children) && item.children.length > 0;
14420
- }
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);
14423
- }
14424
- /**
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.
14430
- */
14431
- function collectPortalLinkStats(items, depth = 0) {
14432
- const result = {
14433
- count: 0,
14434
- nestedLabels: [],
14435
- withChildrenLabels: []
14436
- };
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
- }
14455
- }
14456
- return result;
14457
- }
14458
- /**
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.
14465
- */
14466
- function collectCartLinkStats(items, depth = 0) {
14467
- const result = {
14468
- count: 0,
14469
- nestedLabels: [],
14470
- withChildrenLabels: [],
14471
- ctaLabels: [],
14472
- missingVisibleTextLabels: []
14473
- };
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
- }
14490
- }
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);
14499
- }
14500
- }
14501
- return result;
14502
- }
14503
- function isNavigationMenuValidationItem(value) {
14504
- if (!isRecord$5(value)) return false;
14505
- return typeof value.label === "string";
14006
+ throw new Error(fallback);
14506
14007
  }
14507
14008
 
14508
14009
  //#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
14521
- }
14522
- };
14010
+ //#region ../api/src/bookingActivity.ts
14011
+ const BOOKING_ACTIVITY_CATEGORY_VALUES = [
14012
+ "booking",
14013
+ "purchase",
14014
+ "cancellation",
14015
+ "refund",
14016
+ "transfer",
14017
+ "failure"
14018
+ ];
14019
+ const BOOKING_ACTIVITY_TYPE_VALUES = [
14020
+ "appointment_booked",
14021
+ "appointment_requested",
14022
+ "appointment_moved",
14023
+ "waitlist_joined",
14024
+ "balance_paid",
14025
+ "appointment_cancelled",
14026
+ "booking_closure_created",
14027
+ "booking_closure_removed",
14028
+ "event_registration",
14029
+ "event_registration_cancelled",
14030
+ "event_series_registration",
14031
+ "event_series_registration_cancelled",
14032
+ "pass_purchased",
14033
+ "membership_started",
14034
+ "membership_paused",
14035
+ "membership_cancelled",
14036
+ "event_refund",
14037
+ "pass_refund",
14038
+ "membership_refund",
14039
+ "event_series_registration_refund",
14040
+ "ticket_transfer",
14041
+ "payment_failed"
14042
+ ];
14523
14043
 
14524
14044
  //#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;
14533
- }
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";
14541
- }
14542
- /**
14543
- * Narrow internal link to content/entry type.
14544
- */
14545
- function isEntryLink(link$1) {
14546
- return link$1.entityType === "content";
14547
- }
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) {
14569
- return {
14570
- urlType: "url",
14571
- url: null,
14572
- error: "Link is required for non-dropdown items"
14573
- };
14574
- }
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
- };
14617
- }
14618
- }
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) {
14625
- return {
14626
- urlType: "internal",
14627
- url: null,
14628
- error: `Page '${identifier}' not found`
14629
- };
14630
- }
14631
- return {
14632
- urlType: "internal",
14633
- url: {
14634
- kind: "page",
14635
- identifier
14636
- },
14637
- error: null
14638
- };
14639
- }
14640
- /**
14641
- * Resolve an entry identifier to a canonical entry link payload.
14642
- */
14643
- function resolveEntryLink(contentType, identifier, entryMap) {
14644
- const entryInfo = entryMap.get(`${contentType}:${identifier}`);
14645
- if (!entryInfo) {
14646
- return {
14647
- urlType: "internal",
14648
- url: null,
14649
- error: `Entry '${contentType}:${identifier}' not found or not routable`
14650
- };
14651
- }
14652
- return {
14653
- urlType: "internal",
14654
- url: {
14655
- kind: "entry",
14656
- contentType,
14657
- identifier
14658
- },
14659
- error: null
14660
- };
14661
- }
14045
+ //#region ../api/src/bookingRecords.ts
14046
+ const CUSTOMER_PASS_STATUS_VALUES = [
14047
+ "active",
14048
+ "exhausted",
14049
+ "expired",
14050
+ "refunded"
14051
+ ];
14052
+ const CUSTOMER_MEMBERSHIP_STATUS_VALUES = [
14053
+ "active",
14054
+ "trialing",
14055
+ "past_due",
14056
+ "paused",
14057
+ "canceled"
14058
+ ];
14059
+ const BOOKING_LEDGER_KIND_VALUES = [
14060
+ "appointment",
14061
+ "event_registration",
14062
+ "event_series_registration"
14063
+ ];
14064
+ const BOOKING_LEDGER_DATE_BASIS_VALUES = [
14065
+ "created_at",
14066
+ "scheduled_at",
14067
+ "cancelled_at"
14068
+ ];
14069
+ const BOOKING_LEDGER_PERIOD_PRESET_VALUES = [
14070
+ "7d",
14071
+ "30d",
14072
+ "90d",
14073
+ "365d",
14074
+ "all"
14075
+ ];
14076
+ const BOOKING_LEDGER_CANCELLATION_MODE_VALUES = [
14077
+ "exclude_cancelled",
14078
+ "include_cancelled",
14079
+ "cancelled_only"
14080
+ ];
14081
+ const BOOKING_QUESTION_ANSWER_EXPORT_SCOPES = {
14082
+ site: "site",
14083
+ appointments: "appointments",
14084
+ eventSeries: "event_series"
14085
+ };
14086
+ const BOOKING_QUESTION_ANSWER_EXPORT_SCOPE_VALUES = [
14087
+ BOOKING_QUESTION_ANSWER_EXPORT_SCOPES.site,
14088
+ BOOKING_QUESTION_ANSWER_EXPORT_SCOPES.appointments,
14089
+ BOOKING_QUESTION_ANSWER_EXPORT_SCOPES.eventSeries
14090
+ ];
14662
14091
 
14663
14092
  //#endregion
14664
- //#region ../api/src/navigation/boundaries.ts
14665
- function isNavigationDropdownSentinel(link$1) {
14666
- return link$1?.kind === "dropdown";
14093
+ //#region ../api/src/bookingPeople.ts
14094
+ const PEOPLE_LIST_SORT_VALUES = [
14095
+ "name",
14096
+ "passes",
14097
+ "memberships",
14098
+ "lastBooking",
14099
+ "added"
14100
+ ];
14101
+ const PEOPLE_LIST_FILTER_VALUES = [
14102
+ "everyone",
14103
+ "hasBought",
14104
+ "hasActivePass",
14105
+ "hasMembership",
14106
+ "guests",
14107
+ "nothingBought"
14108
+ ];
14109
+ /** What can be attached to a person, in the order the dialog lists it. */
14110
+ const PERSON_DELETION_BLOCKER_CATEGORY_VALUES = [
14111
+ "bookings",
14112
+ "guest_places",
14113
+ "waitlist_places",
14114
+ "payments",
14115
+ "orders",
14116
+ "passes",
14117
+ "memberships",
14118
+ "credit",
14119
+ "digital_products",
14120
+ "discounts",
14121
+ "gifts",
14122
+ "notes",
14123
+ "forms",
14124
+ "terms_agreements",
14125
+ "uploads",
14126
+ "contact_corrections"
14127
+ ];
14128
+
14129
+ //#endregion
14130
+ //#region ../api/src/wholeSeriesRuns.ts
14131
+ const DELETE_EVENT_OFFERING_CONFLICT_REASONS = [
14132
+ "has_registrations",
14133
+ "has_payments",
14134
+ "published_page",
14135
+ "inconsistent_ownership"
14136
+ ];
14137
+ function isDeleteEventOfferingConflictReason(value) {
14138
+ return DELETE_EVENT_OFFERING_CONFLICT_REASONS.some((reason) => reason === value);
14667
14139
  }
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);
14679
- }
14680
- return commandItem;
14140
+
14141
+ //#endregion
14142
+ //#region ../api/src/bookings.ts
14143
+ const BOOKING_QUESTION_REQUIREMENT_SCOPES = ["before_booking_booking_level", "after_booking_due_by_participant"];
14144
+ function isAfterBookingQuestionRequirementScope(scope) {
14145
+ return scope === "after_booking_due_by_participant";
14681
14146
  }
14682
- function navigationCommandItemToManagementInput(item) {
14683
- const link$1 = normalizeNavigationAuthoringLinkForCommand(item.link);
14684
- 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
- };
14147
+ function isBeforeBookingQuestionRequirementScope(scope) {
14148
+ return scope === "before_booking_booking_level";
14690
14149
  }
14691
- function normalizeNavigationAuthoringLinkForCommand(link$1) {
14692
- if (!link$1 || isNavigationDropdownSentinel(link$1)) return undefined;
14693
- return navigationAuthoringLinkToCommandLink(link$1);
14150
+ async function getBookingSettingsRequest(apiClient, siteId, options) {
14151
+ try {
14152
+ const response = unwrapResponse(await apiClient({
14153
+ endpoint: "getBookingSettings",
14154
+ params: { siteId },
14155
+ options
14156
+ }));
14157
+ return response.settings;
14158
+ } catch (error) {
14159
+ normalizeApiError(error, "Failed to load booking settings");
14160
+ }
14694
14161
  }
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) } : {}
14702
- };
14703
- return localItem;
14162
+ async function updateBookingSettingsRequest(apiClient, siteId, payload, options) {
14163
+ try {
14164
+ const response = unwrapResponse(await apiClient({
14165
+ endpoint: "updateBookingSettings",
14166
+ params: { siteId },
14167
+ body: payload,
14168
+ options
14169
+ }));
14170
+ return response.settings;
14171
+ } catch (error) {
14172
+ normalizeApiError(error, "Failed to save booking settings");
14173
+ }
14704
14174
  }
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);
14175
+ async function createAppointmentRequest(apiClient, siteId, payload, options) {
14176
+ try {
14177
+ return unwrapResponse(await apiClient({
14178
+ endpoint: "createAppointment",
14179
+ params: { siteId },
14180
+ body: payload,
14181
+ options
14182
+ }));
14183
+ } catch (error) {
14184
+ normalizeApiError(error, "Failed to create appointment");
14735
14185
  }
14736
14186
  }
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 } : {};
14187
+ async function deleteEventOfferingRequest(apiClient, siteId, eventOfferingId, options) {
14188
+ try {
14189
+ return unwrapResponse(await apiClient({
14190
+ endpoint: "deleteEventOffering",
14191
+ params: {
14192
+ siteId,
14193
+ eventOfferingId
14194
+ },
14195
+ options
14196
+ }));
14197
+ } catch (error) {
14198
+ normalizeApiError(error, "Failed to delete event offering");
14199
+ }
14744
14200
  }
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);
14201
+ async function addEventSeriesRegistrationRequest(apiClient, siteId, eventSeriesId, payload, options) {
14202
+ try {
14203
+ return unwrapResponse(await apiClient({
14204
+ endpoint: "addEventSeriesRegistration",
14205
+ params: {
14206
+ siteId,
14207
+ seriesId: eventSeriesId
14208
+ },
14209
+ body: payload,
14210
+ options
14211
+ }));
14212
+ } catch (error) {
14213
+ normalizeApiError(error, "Failed to add Event run registration");
14778
14214
  }
14779
14215
  }
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
- }
14216
+ async function updateEventSeriesRegistrationRequest(apiClient, siteId, eventSeriesId, registrationId, payload, options) {
14217
+ try {
14218
+ return unwrapResponse(await apiClient({
14219
+ endpoint: "updateEventSeriesRegistration",
14220
+ params: {
14221
+ siteId,
14222
+ seriesId: eventSeriesId,
14223
+ registrationId
14224
+ },
14225
+ body: payload,
14226
+ options
14227
+ }));
14228
+ } catch (error) {
14229
+ normalizeApiError(error, "Failed to update Event run registration");
14797
14230
  }
14798
- if (link$1.href) {
14799
- return {
14800
- kind: "url",
14801
- href: link$1.href
14802
- };
14231
+ }
14232
+ async function addEventOccurrenceAttendeeRequest(apiClient, siteId, seriesId, occurrenceId, payload, options) {
14233
+ try {
14234
+ return unwrapResponse(await apiClient({
14235
+ endpoint: "addEventOccurrenceAttendee",
14236
+ params: {
14237
+ siteId,
14238
+ seriesId,
14239
+ occurrenceId
14240
+ },
14241
+ body: payload,
14242
+ options
14243
+ }));
14244
+ } catch (error) {
14245
+ normalizeApiError(error, "Failed to add attendee");
14803
14246
  }
14804
- return undefined;
14805
14247
  }
14806
14248
 
14807
14249
  //#endregion
14808
- //#region ../api/src/navigation/routableLink.ts
14809
- /**
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.
14812
- */
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
- };
14821
- }
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
- };
14830
- }
14831
- return toResolvedInternalPayload(item);
14832
- case "pageSurface": return toResolvedInternalPayload(item);
14833
- default: return assertNever(item);
14834
- }
14250
+ //#region src/push-config-sync.ts
14251
+ function isConfigSync(sync) {
14252
+ return "ignoredDocumentSections" in sync;
14835
14253
  }
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
14848
- };
14254
+ function getPushConfigSync(result) {
14255
+ if (result.configSync) return result.configSync;
14256
+ if (result.sync) return result.sync;
14257
+ return null;
14849
14258
  }
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];
14259
+ function collectPushConfigSyncErrors(sync) {
14260
+ return [
14261
+ ...sync.contentTypes.errors.map((message) => ({
14262
+ area: "contentTypes",
14263
+ message
14264
+ })),
14265
+ ...sync.customBlocks.errors.map((message) => ({
14266
+ area: "customBlocks",
14267
+ message
14268
+ })),
14269
+ ...sync.templateBlocks.errors.map((message) => ({
14270
+ area: "templateBlocks",
14271
+ message
14272
+ })),
14273
+ ...sync.settings.error ? [{
14274
+ area: "settings",
14275
+ message: sync.settings.error
14276
+ }] : [],
14277
+ ...!isConfigSync(sync) ? sync.pages.errors.map((message) => ({
14278
+ area: "pages",
14279
+ message
14280
+ })) : [],
14281
+ ...!isConfigSync(sync) ? sync.entries.errors.map((message) => ({
14282
+ area: "entries",
14283
+ message
14284
+ })) : [],
14285
+ ...!isConfigSync(sync) ? sync.navigationMenus.errors.map((message) => ({
14286
+ area: "navigationMenus",
14287
+ message
14288
+ })) : []
14289
+ ];
14864
14290
  }
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"];
14291
+ function collectPushConfigSyncWarnings(sync) {
14292
+ return [
14293
+ ...sync.contentTypes.warnings.map((message) => ({
14294
+ area: "contentTypes",
14295
+ message
14296
+ })),
14297
+ ...sync.customBlocks.warnings.map((message) => ({
14298
+ area: "customBlocks",
14299
+ message
14300
+ })),
14301
+ ...sync.templateBlocks.warnings.map((message) => ({
14302
+ area: "templateBlocks",
14303
+ message
14304
+ })),
14305
+ ...isConfigSync(sync) ? sync.ignoredDocumentSections.map((section) => ({
14306
+ area: "ignoredDocuments",
14307
+ message: formatIgnoredDocumentSyncSectionWarning(section)
14308
+ })) : [
14309
+ ...sync.pages.warnings.map((message) => ({
14310
+ area: "pages",
14311
+ message
14312
+ })),
14313
+ ...sync.entries.warnings.map((message) => ({
14314
+ area: "entries",
14315
+ message
14316
+ })),
14317
+ ...sync.navigationMenus.warnings.map((message) => ({
14318
+ area: "navigationMenus",
14319
+ message
14320
+ }))
14321
+ ]
14322
+ ];
14323
+ }
14324
+ function groupPushConfigSyncWarningsForHuman(warnings) {
14325
+ const ignoredDocumentCount = warnings.filter((warning) => warning.area === "ignoredDocuments").length;
14326
+ return [...warnings.filter((warning) => warning.area !== "ignoredDocuments"), ...ignoredDocumentCount > 0 ? [{
14327
+ area: "ignoredDocuments",
14328
+ message: `${ignoredDocumentCount} document content section${ignoredDocumentCount === 1 ? "" : "s"} ignored by push-config. Use \`riverbankcms push\` for pages, entries, and navigation content.`
14329
+ }] : []];
14330
+ }
14331
+ function buildPreviewSummary(sync) {
14332
+ return { wouldConsider: {
14333
+ contentTypes: sync.contentTypes.skipped,
14334
+ customBlocks: sync.customBlocks.skipped,
14335
+ templateBlocks: sync.templateBlocks.skipped,
14336
+ settings: sync.settings.updated,
14337
+ ...isConfigSync(sync) ? { footerBlocks: sync.footerBlocks.skipped } : {},
14338
+ ...isConfigSync(sync) ? { ignoredDocumentSections: sync.ignoredDocumentSections } : {}
14339
+ } };
14872
14340
  }
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
14341
 
14884
14342
  //#endregion
14885
- //#region ../api/src/siteMembers.ts
14343
+ //#region src/push-config.ts
14886
14344
  /**
14887
- * Higher number means more permissions.
14345
+ * push-config CLI command
14346
+ *
14347
+ * Pushes SDK site configuration from a riverbank.config.ts file to the dashboard API.
14888
14348
  */
14889
- const SITE_ROLE_PERMISSION_LEVEL = {
14890
- team_member: 0,
14891
- editor: 1,
14892
- administrator: 2,
14893
- owner: 3
14894
- };
14349
+ const DEFAULT_CONFIG_PATH = "./riverbank.config.ts";
14350
+ function formatPushConfigActionFailure() {
14351
+ return "push-config failed";
14352
+ }
14353
+ function isRecord$6(value) {
14354
+ return typeof value === "object" && value !== null;
14355
+ }
14356
+ function parseErrorDetails(value) {
14357
+ if (!value) return {};
14358
+ if (typeof value === "string") {
14359
+ return { message: value };
14360
+ }
14361
+ if (!isRecord$6(value)) return {};
14362
+ const message = typeof value.message === "string" ? value.message : undefined;
14363
+ const fieldErrorsRaw = value.fieldErrors;
14364
+ const fieldErrors = Array.isArray(fieldErrorsRaw) ? fieldErrorsRaw.filter(isRecord$6).map((d) => ({
14365
+ field: typeof d.field === "string" ? d.field : "",
14366
+ message: typeof d.message === "string" ? d.message : ""
14367
+ })).filter((d) => d.field.length > 0 && d.message.length > 0) : undefined;
14368
+ return {
14369
+ message,
14370
+ fieldErrors
14371
+ };
14372
+ }
14373
+ function formatFieldErrors(fieldErrors) {
14374
+ if (!fieldErrors || fieldErrors.length === 0) return "";
14375
+ return ":\n" + fieldErrors.map((d) => ` - ${d.field}: ${d.message}`).join("\n");
14376
+ }
14377
+ function getDashboardErrorMessage(responseJson, fallback) {
14378
+ if (!responseJson) return fallback;
14379
+ if (isRecord$6(responseJson) && "error" in responseJson) {
14380
+ const parsed = parseErrorDetails(responseJson.error);
14381
+ const msg = parsed.message ?? fallback;
14382
+ return msg + formatFieldErrors(parsed.fieldErrors);
14383
+ }
14384
+ return fallback;
14385
+ }
14895
14386
  /**
14896
- * Lower number sorts earlier in UI role pickers.
14387
+ * Push config to dashboard API
14897
14388
  */
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 }) {
14389
+ async function pushToDashboard(dashboardUrl, siteId, apiKey, config, options) {
14390
+ const normalizedDashboardUrl = dashboardUrl.replace(/\/+$/, "");
14391
+ const baseUrl = `${normalizedDashboardUrl}/api`;
14392
+ const path$3 = `/sites/${siteId}/sdk-config`;
14393
+ const pushUrl = `${baseUrl}${path$3}`;
14394
+ const requestInfo = {
14395
+ method: "POST",
14396
+ intent: "write",
14397
+ path: path$3,
14398
+ url: pushUrl,
14399
+ baseUrl,
14400
+ dashboardUrl: normalizedDashboardUrl,
14401
+ siteId
14402
+ };
14403
+ const client = createBearerAPIClient(exposeSecret(apiKey), baseUrl);
14404
+ const timeoutMs = currentRequestTimeoutMs() ?? 3e4;
14905
14405
  try {
14906
- return unwrapResponse(await apiClient({
14907
- endpoint: "listSiteMembers",
14406
+ return unwrapResponse(await client({
14407
+ endpoint: "pushSdkConfig",
14908
14408
  params: { siteId },
14909
- options
14409
+ body: {
14410
+ config,
14411
+ forceContentTypes: options?.forceContentTypes ?? false,
14412
+ mode: options?.dryRun ? "preview" : "apply",
14413
+ expectedMailingListProviderScope: options?.expectedMailingListProviderScope ?? undefined
14414
+ },
14415
+ options: { signal: AbortSignal.timeout(timeoutMs) }
14910
14416
  }));
14911
14417
  } catch (error) {
14912
- return normalizeApiError(error, "Failed to load site members");
14418
+ if (error instanceof ApiEnvelopeError) {
14419
+ const details = {
14420
+ success: false,
14421
+ error: {
14422
+ code: error.code,
14423
+ message: error.message,
14424
+ fieldErrors: error.fieldErrors
14425
+ }
14426
+ };
14427
+ const errorMessage = getDashboardErrorMessage(details, error.message);
14428
+ throw new ManagementApiError(errorMessage, "sdk:http-error", details, error.status, requestInfo);
14429
+ }
14430
+ if (error instanceof ApiRequestError) {
14431
+ const errorMessage = getDashboardErrorMessage(error.body, `Dashboard returned ${error.status}`);
14432
+ throw new ManagementApiError(errorMessage, "sdk:http-error", error.body, error.status, requestInfo);
14433
+ }
14434
+ throw managementTransportError(error, timeoutMs, requestInfo);
14913
14435
  }
14914
14436
  }
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");
14437
+ function validatePushConfig(rawConfig) {
14438
+ const parsed = riverbankSiteConfigSchema.safeParse(rawConfig);
14439
+ if (!parsed.success) {
14440
+ const issues = parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
14441
+ throw new Error(`Invalid config: ${issues}`);
14442
+ }
14443
+ const conflicts = validateFieldIdConflicts(parsed.data.blockFieldExtensions);
14444
+ if (conflicts.length > 0) {
14445
+ throw new Error(`Field ID conflicts detected in blockFieldExtensions: ${conflicts.map((conflict) => conflict.message).join("; ")}`);
14925
14446
  }
14447
+ return parsed.data;
14926
14448
  }
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");
14449
+ async function loadValidatedPushConfig(target, configPath) {
14450
+ return validatePushConfig(await withRiverbankEnv(target, () => loadConfigFile(configPath)));
14451
+ }
14452
+ function analyzePushConfigResponse(response, dryRun) {
14453
+ const resultMode = response.mode ?? "apply";
14454
+ if (dryRun && resultMode !== "preview") {
14455
+ return {
14456
+ success: false,
14457
+ kind: "unsafe-preview",
14458
+ receivedMode: response.mode
14459
+ };
14940
14460
  }
14461
+ const sync = getPushConfigSync(response);
14462
+ if (!sync) return {
14463
+ success: true,
14464
+ resultMode,
14465
+ sync: null,
14466
+ warnings: []
14467
+ };
14468
+ const errors = collectPushConfigSyncErrors(sync);
14469
+ if (errors.length > 0 || sync.success === false) return {
14470
+ success: false,
14471
+ kind: "sync-errors",
14472
+ sync,
14473
+ errors
14474
+ };
14475
+ return {
14476
+ success: true,
14477
+ resultMode,
14478
+ sync,
14479
+ warnings: collectPushConfigSyncWarnings(sync)
14480
+ };
14941
14481
  }
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");
14482
+ function renderPushConfigResponse(output, response, mode, dryRun) {
14483
+ const analysis = analyzePushConfigResponse(response, dryRun);
14484
+ if (!analysis.success) {
14485
+ if (analysis.kind === "unsafe-preview") {
14486
+ output.fail("Config preview was not confirmed by the dashboard; no safe dry-run result is available.", {
14487
+ expectedMode: "preview",
14488
+ receivedMode: analysis.receivedMode ?? null,
14489
+ suggestion: "Update the dashboard API before relying on push-config --dry-run."
14490
+ });
14491
+ return { success: false };
14492
+ }
14493
+ if (mode === "json") {
14494
+ output.markFailed();
14495
+ output.json({
14496
+ success: false,
14497
+ sync: analysis.sync,
14498
+ errors: analysis.errors
14499
+ });
14500
+ } else {
14501
+ output.markFailed();
14502
+ output.fail("Config pushed, but SDK config sync reported errors", {
14503
+ errors: analysis.errors,
14504
+ summary: analysis.sync
14505
+ });
14506
+ }
14507
+ return { success: false };
14508
+ }
14509
+ if (mode === "human" && analysis.warnings.length > 0) {
14510
+ output.warn("SDK config sync reported warnings", { warnings: groupPushConfigSyncWarningsForHuman(analysis.warnings) });
14511
+ }
14512
+ if (mode === "human" && analysis.sync) {
14513
+ output.info(analysis.resultMode === "preview" ? "SDK config preview summary" : "SDK config sync summary", analysis.resultMode === "preview" ? buildPreviewSummary(analysis.sync) : analysis.sync);
14514
+ }
14515
+ if (mode === "json") {
14516
+ output.json({
14517
+ success: true,
14518
+ ...analysis.resultMode === "preview" ? {
14519
+ dryRun: true,
14520
+ mode: analysis.resultMode
14521
+ } : {},
14522
+ sync: analysis.sync
14523
+ });
14524
+ } else {
14525
+ output.success(analysis.resultMode === "preview" ? "Config preview completed. No changes were applied." : "Config pushed successfully!");
14952
14526
  }
14527
+ return { success: true };
14953
14528
  }
14954
- async function unlinkCurrentUserPractitioner(apiClient, { siteId, options }) {
14529
+ /** Main push-config action, retained for `push --with-config`. */
14530
+ async function pushConfigAction(output, options) {
14531
+ const target = options.target;
14955
14532
  try {
14956
- return unwrapResponse(await apiClient({
14957
- endpoint: "unlinkCurrentUserPractitioner",
14958
- params: { siteId },
14959
- options
14960
- }));
14533
+ if (!options.isJsonOutput) {
14534
+ output.info("push-config target", {
14535
+ env: target,
14536
+ dashboardUrl: options.dashboard,
14537
+ configPath: options.config ?? DEFAULT_CONFIG_PATH
14538
+ });
14539
+ output.info("Validating config...");
14540
+ }
14541
+ const config = await loadValidatedPushConfig(target, options.config);
14542
+ const apiKey = resolveManagementApiKey(output, options.apiKey, target);
14543
+ const response = await pushToDashboard(options.dashboard, config.siteId, apiKey, config, {
14544
+ forceContentTypes: options.forceContentTypes ?? false,
14545
+ dryRun: options.dryRun ?? false
14546
+ });
14547
+ return renderPushConfigResponse(output, response, options.isJsonOutput ? "json" : "human", options.dryRun ?? false);
14961
14548
  } catch (error) {
14962
- return normalizeApiError(error, "Failed to unlink practitioner profile");
14549
+ handleCommandError(error, output, {
14550
+ target,
14551
+ dashboardUrl: options.dashboard
14552
+ });
14553
+ return { success: false };
14554
+ }
14555
+ }
14556
+ function resolveDashboardUrlValue(cliOption, target) {
14557
+ const envVar = getEnvVarName("DASHBOARD_URL", target);
14558
+ const url = cliOption || process.env[envVar];
14559
+ if (!url) {
14560
+ return {
14561
+ ok: false,
14562
+ error: {
14563
+ message: "Dashboard URL is required",
14564
+ details: { suggestion: `Provide --dashboard <url> or set ${envVar} environment variable.` }
14565
+ }
14566
+ };
14567
+ }
14568
+ return {
14569
+ ok: true,
14570
+ value: url
14571
+ };
14572
+ }
14573
+ /**
14574
+ * Resolve management API key from CLI option or environment variable.
14575
+ */
14576
+ function resolveManagementApiKeyValue(cliOption, target) {
14577
+ const envVar = getEnvVarName("MGMT_API_KEY", target);
14578
+ const apiKey = cliOption || process.env[envVar];
14579
+ if (!apiKey) {
14580
+ return {
14581
+ ok: false,
14582
+ error: {
14583
+ message: "Management API key is required",
14584
+ details: { suggestion: `Provide --api-key <key> or set ${envVar} environment variable.` }
14585
+ }
14586
+ };
14587
+ }
14588
+ const parsedApiKey = parseManagementKey(apiKey);
14589
+ if (!parsedApiKey.ok) {
14590
+ return {
14591
+ ok: false,
14592
+ error: {
14593
+ message: "Invalid management API key format",
14594
+ details: {
14595
+ expected: "Key starting with bld_mgmt_sk_",
14596
+ suggestion: `Check your ${envVar} environment variable.`
14597
+ }
14598
+ }
14599
+ };
14963
14600
  }
14601
+ return {
14602
+ ok: true,
14603
+ value: parsedApiKey.value
14604
+ };
14964
14605
  }
14965
- async function removeSiteMember(apiClient, { siteId, memberId, options }) {
14966
- try {
14967
- return unwrapResponse(await apiClient({
14968
- endpoint: "removeSiteMember",
14969
- params: {
14970
- siteId,
14971
- memberId
14972
- },
14973
- options
14974
- }));
14975
- } catch (error) {
14976
- return normalizeApiError(error, "Failed to remove collaborator");
14977
- }
14606
+ function resolveManagementApiKey(output, cliOption, target) {
14607
+ const result = resolveManagementApiKeyValue(cliOption, target);
14608
+ if (!result.ok) output.error(result.error.message, result.error.details);
14609
+ return result.value;
14978
14610
  }
14979
- async function revokeSiteInvitation(apiClient, { siteId, invitationId, options }) {
14980
- try {
14981
- return unwrapResponse(await apiClient({
14982
- endpoint: "revokeSiteInvitation",
14983
- params: {
14984
- siteId,
14985
- invitationId
14986
- },
14987
- options
14988
- }));
14989
- } catch (error) {
14990
- return normalizeApiError(error, "Failed to revoke invitation");
14991
- }
14611
+ function unwrapPushConfigTargetValue(result) {
14612
+ if (result.ok) return result.value;
14613
+ const suggestion = result.error.details.suggestion;
14614
+ throw new Error(`${result.error.message}${typeof suggestion === "string" ? `. ${suggestion}` : ""}`);
14992
14615
  }
14993
- async function resendSiteInvitation(apiClient, { siteId, invitationId, options }) {
14994
- try {
14995
- return unwrapResponse(await apiClient({
14996
- endpoint: "resendSiteInvitation",
14997
- params: {
14998
- siteId,
14999
- invitationId
15000
- },
15001
- options
15002
- }));
15003
- } catch (error) {
15004
- return normalizeApiError(error, "Failed to resend invitation");
15005
- }
14616
+ async function loadPushConfigTarget(target, options) {
14617
+ return {
14618
+ target,
14619
+ dashboard: unwrapPushConfigTargetValue(resolveDashboardUrlValue(options.dashboard, target)),
14620
+ apiKey: unwrapPushConfigTargetValue(resolveManagementApiKeyValue(options.apiKey, target))
14621
+ };
15006
14622
  }
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");
14623
+ async function preparePushConfigTarget(context, options) {
14624
+ const config = await loadValidatedPushConfig(context.target, options.config);
14625
+ const preview = await pushToDashboard(context.dashboard, config.siteId, context.apiKey, config, {
14626
+ forceContentTypes: options.forceContentTypes ?? false,
14627
+ dryRun: true
14628
+ });
14629
+ if (!analyzePushConfigResponse(preview, true).success) {
14630
+ throw new Error(`Config preview failed for ${context.target} environment.`);
15017
14631
  }
14632
+ return {
14633
+ config,
14634
+ preview,
14635
+ forceContentTypes: options.forceContentTypes ?? false
14636
+ };
15018
14637
  }
15019
- async function acceptSiteInvitation(apiClient, { body, options }) {
14638
+ /**
14639
+ * Re-preview a prepared config value without reloading the local config file.
14640
+ * The caller uses this immediately before applying a multi-target plan so a
14641
+ * dashboard-side schema change is classified as stale before any mutation.
14642
+ */
14643
+ async function revalidatePushConfigPlan(context, plan) {
15020
14644
  try {
15021
- return unwrapResponse(await apiClient({
15022
- endpoint: "acceptSiteInvitation",
15023
- body,
15024
- options
15025
- }));
14645
+ const current = await pushToDashboard(context.dashboard, plan.config.siteId, context.apiKey, plan.config, {
14646
+ forceContentTypes: plan.forceContentTypes,
14647
+ dryRun: true
14648
+ });
14649
+ return stableStringify(current) === stableStringify(plan.preview) ? { ok: true } : {
14650
+ ok: false,
14651
+ error: new Error("SDK config changed after preflight; the push plan is stale.")
14652
+ };
15026
14653
  } catch (error) {
15027
- return normalizeApiError(error, "Failed to accept invitation");
14654
+ return {
14655
+ ok: false,
14656
+ error: error instanceof Error ? error : new Error(String(error))
14657
+ };
15028
14658
  }
15029
14659
  }
15030
-
15031
- //#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];
14660
+ async function applyPushConfigPlan(output, context, plan, options, isJsonOutput) {
14661
+ const response = options.dryRun ? plan.preview : await pushToDashboard(context.dashboard, plan.config.siteId, context.apiKey, plan.config, {
14662
+ forceContentTypes: plan.forceContentTypes,
14663
+ dryRun: false,
14664
+ expectedMailingListProviderScope: plan.preview.mailingListProviderScope
14665
+ });
14666
+ return {
14667
+ response,
14668
+ action: isJsonOutput ? { success: analyzePushConfigResponse(response, options.dryRun ?? false).success } : renderPushConfigResponse(output, response, "human", options.dryRun ?? false)
14669
+ };
15035
14670
  }
15036
- function roleAtLeastOrNone(role, minimum) {
15037
- if (!role) return false;
15038
- return roleAtLeast$1(role, minimum);
14671
+ function isPushConfigApplySuccess(result) {
14672
+ return result.action.success;
15039
14673
  }
15040
- function appointmentResourceIdFromStaffMemberId(staffMemberId) {
15041
- return unsafeAsAppointmentResourceId(staffMemberId);
14674
+ function renderPushConfigJsonResult(output, execution, dryRun) {
14675
+ output.json({
14676
+ success: execution.succeeded,
14677
+ dryRun,
14678
+ environments: mapEnvironmentExecutionOutcomesJson(execution, ({ result }) => ({
14679
+ response: result.response,
14680
+ success: true
14681
+ }), ({ result, message }) => ({
14682
+ response: result.response,
14683
+ success: false,
14684
+ error: message
14685
+ }))
14686
+ });
15042
14687
  }
15043
- function normalizeStaffCapabilities(capabilities) {
15044
- if (!capabilities) return null;
15045
- return STAFF_OPERATIONAL_CAPABILITIES.filter((capability) => capabilities.includes(capability));
14688
+ function getProductionPushConfigCommandDeps() {
14689
+ return {
14690
+ getOutputContext,
14691
+ planEnvironmentExecution,
14692
+ executeEnvironmentPlan,
14693
+ confirmAction,
14694
+ loadTarget: loadPushConfigTarget,
14695
+ prepareTarget: preparePushConfigTarget,
14696
+ revalidatePlan: revalidatePushConfigPlan,
14697
+ applyPlan: applyPushConfigPlan
14698
+ };
15046
14699
  }
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
- };
14700
+ /**
14701
+ * push-config command definition
14702
+ */
14703
+ 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", `
14704
+ Description:
14705
+ Syncs your local riverbank.config.ts schema/config to the CMS dashboard, including:
14706
+ - Custom blocks
14707
+ - Block field extensions
14708
+ - Block field options
14709
+ - Content type definitions and templates
14710
+ - SDK-managed site settings and footer blocks
14711
+
14712
+ Pages, content entries, and navigation menus are content documents.
14713
+ Sync them with "riverbankcms push pages", "riverbankcms push entries",
14714
+ or "riverbankcms push navigation".
14715
+
14716
+ Examples:
14717
+ $ pnpm exec riverbankcms push-config
14718
+ $ pnpm exec riverbankcms push-config --dry-run
14719
+ $ pnpm exec riverbankcms push-config --env=remote
14720
+ $ pnpm exec riverbankcms push-config --api-key bld_mgmt_sk_... --dashboard https://www.riverbankcms.com
14721
+ $ pnpm exec riverbankcms push-config --config ./src/riverbank.config.ts
14722
+ `).action(async (options, command) => {
14723
+ const { output, selection, globalOpts } = deps.getOutputContext(command);
14724
+ const isJsonOutput = globalOpts.json ?? false;
14725
+ const authorization = requireEnvironmentWriteAuthorization({
14726
+ ...options,
14727
+ json: isJsonOutput,
14728
+ interactive: process.stdin.isTTY === true
14729
+ }, output);
14730
+ if (!authorization) return;
14731
+ const planned = deps.planEnvironmentExecution({
14732
+ command: "push-config",
14733
+ selection: "both",
14734
+ failure: "abort",
14735
+ confirmation: "command",
14736
+ revalidation: "required"
14737
+ }, selection);
14738
+ if (!planned.ok) return output.error(planned.error.message, { suggestion: planned.error.suggestion });
14739
+ const execution = await deps.executeEnvironmentPlan(planned.value, {
14740
+ loadTarget: (target) => deps.loadTarget(target, options),
14741
+ preflight: async (_target, loaded) => deps.prepareTarget(loaded.context, {
14742
+ ...options,
14743
+ dashboard: loaded.context.dashboard
14744
+ }),
14745
+ confirm: async ({ ready }) => {
14746
+ if (authorization.kind === "preauthorized") return true;
14747
+ for (const { target, context } of ready) {
14748
+ const sync = getPushConfigSync(context.preview);
14749
+ output.info(`${target} config preview`, sync ? buildPreviewSummary(sync) : { changes: "No sync summary returned" });
14750
+ }
14751
+ return deps.confirmAction(`Apply SDK config to ${ready.length} environment${ready.length === 1 ? "" : "s"} (${ready.map(({ target }) => target).join(" then ")})?`, { yes: options.yes }, false);
14752
+ },
14753
+ revalidate: async (_target, loaded, prepared) => options.dryRun ? { ok: true } : deps.revalidatePlan(loaded.context, prepared.context),
14754
+ apply: async (target, loaded, prepared) => {
14755
+ if (planned.value.targets.length > 1) output.info(`\n--- Pushing to ${target} environment ---`);
14756
+ return deps.applyPlan(output, loaded.context, prepared.context, {
14757
+ ...options,
14758
+ dashboard: loaded.context.dashboard
14759
+ }, isJsonOutput);
14760
+ },
14761
+ isSuccess: isPushConfigApplySuccess,
14762
+ formatFailure: formatPushConfigActionFailure
14763
+ }, output);
14764
+ if (isJsonOutput) renderPushConfigJsonResult(output, execution, options.dryRun ?? false);
14765
+ });
14766
+
14767
+ //#endregion
14768
+ //#region ../api/src/navigationMenuValidation.ts
14769
+ function hasChildren(item) {
14770
+ return Array.isArray(item.children) && item.children.length > 0;
14771
+ }
14772
+ function isCartLinkMissingVisibleLabel(link$1) {
14773
+ 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);
14774
+ }
14775
+ /**
14776
+ * Recursively collects portal-link constraints for a single menu tree.
14777
+ * Rules:
14778
+ * - At most one portal link per menu.
14779
+ * - Portal links must be root-level.
14780
+ * - Portal links cannot have children.
14781
+ */
14782
+ function collectPortalLinkStats(items, depth = 0) {
14783
+ const result = {
14784
+ count: 0,
14785
+ nestedLabels: [],
14786
+ withChildrenLabels: []
14787
+ };
14788
+ for (const item of items) {
14789
+ const isPortal = item.link?.kind === "portal";
14790
+ if (isPortal) {
14791
+ result.count += 1;
14792
+ if (depth > 0) {
14793
+ result.nestedLabels.push(item.label);
15061
14794
  }
15062
- if (persistedCapabilities === null) {
15063
- return {
15064
- kind: "appointments",
15065
- staffMemberId
15066
- };
14795
+ if (hasChildren(item)) {
14796
+ result.withChildrenLabels.push(item.label);
15067
14797
  }
15068
- if (persistedCapabilities.length === 0) {
15069
- return {
15070
- kind: "staff_setup_required",
15071
- reason: "missing_capabilities"
15072
- };
14798
+ }
14799
+ if (hasChildren(item)) {
14800
+ const childItems = (item.children ?? []).filter(isNavigationMenuValidationItem);
14801
+ const child = collectPortalLinkStats(childItems, depth + 1);
14802
+ result.count += child.count;
14803
+ result.nestedLabels.push(...child.nestedLabels);
14804
+ result.withChildrenLabels.push(...child.withChildrenLabels);
14805
+ }
14806
+ }
14807
+ return result;
14808
+ }
14809
+ /**
14810
+ * Recursively collects cart-link constraints for a single menu tree.
14811
+ * Rules:
14812
+ * - Cart links must be root-level.
14813
+ * - Cart links cannot have children.
14814
+ * - Cart links cannot be CTA items.
14815
+ * - Cart links using `icon-with-label` must provide visible text.
14816
+ */
14817
+ function collectCartLinkStats(items, depth = 0) {
14818
+ const result = {
14819
+ count: 0,
14820
+ nestedLabels: [],
14821
+ withChildrenLabels: [],
14822
+ ctaLabels: [],
14823
+ missingVisibleTextLabels: []
14824
+ };
14825
+ for (const item of items) {
14826
+ const isCart = item.link?.kind === "cart";
14827
+ if (isCart) {
14828
+ result.count += 1;
14829
+ if (depth > 0) {
14830
+ result.nestedLabels.push(item.label);
15073
14831
  }
15074
- if (persistedCapabilities.includes("appointments") && persistedCapabilities.includes("event_ops")) {
15075
- return {
15076
- kind: "hybrid",
15077
- staffMemberId
15078
- };
14832
+ if (hasChildren(item)) {
14833
+ result.withChildrenLabels.push(item.label);
15079
14834
  }
15080
- if (persistedCapabilities.includes("event_ops")) {
15081
- return {
15082
- kind: "event_ops_only",
15083
- staffMemberId
15084
- };
14835
+ if (item.isCta === true) {
14836
+ result.ctaLabels.push(item.label);
15085
14837
  }
15086
- return {
15087
- kind: "appointments",
15088
- staffMemberId
15089
- };
14838
+ if (isCartLinkMissingVisibleLabel(item.link ?? null)) {
14839
+ result.missingVisibleTextLabels.push(item.label);
14840
+ }
14841
+ }
14842
+ if (hasChildren(item)) {
14843
+ const childItems = (item.children ?? []).filter(isNavigationMenuValidationItem);
14844
+ const child = collectCartLinkStats(childItems, depth + 1);
14845
+ result.count += child.count;
14846
+ result.nestedLabels.push(...child.nestedLabels);
14847
+ result.withChildrenLabels.push(...child.withChildrenLabels);
14848
+ result.ctaLabels.push(...child.ctaLabels);
14849
+ result.missingVisibleTextLabels.push(...child.missingVisibleTextLabels);
14850
+ }
15090
14851
  }
14852
+ return result;
15091
14853
  }
15092
- function isStaffScoped(scope) {
15093
- return scope.kind !== "none";
14854
+ function isNavigationMenuValidationItem(value) {
14855
+ if (!isRecord$5(value)) return false;
14856
+ return typeof value.label === "string";
15094
14857
  }
15095
- function isTeamMemberScope(scope) {
15096
- return scope.kind === "appointments" || scope.kind === "staff_setup_required";
14858
+
14859
+ //#endregion
14860
+ //#region ../api/src/navigation/bookingPortalLink.ts
14861
+ const YOUR_BOOKINGS_LABEL = "Your bookings";
14862
+ /**
14863
+ * The booking page's link to the customer portal, in the header and the
14864
+ * footer alike. It is a portal link, so renderers apply the portal link rules
14865
+ * (including the portal-enabled gate); the label stays the same signed in.
14866
+ */
14867
+ const YOUR_BOOKINGS_PORTAL_LINK = {
14868
+ kind: "portal",
14869
+ labels: {
14870
+ loggedOut: YOUR_BOOKINGS_LABEL,
14871
+ loggedIn: YOUR_BOOKINGS_LABEL
14872
+ }
14873
+ };
14874
+
14875
+ //#endregion
14876
+ //#region ../api/src/navigation/linkResolver.ts
14877
+ /**
14878
+ * Type guard for internal links.
14879
+ * Checks if a link payload is an internal link with entityId and entityType.
14880
+ */
14881
+ function isInternalLink(link$1) {
14882
+ if (!link$1 || typeof link$1 !== "object") {
14883
+ return false;
14884
+ }
14885
+ return link$1.kind === "internal" && "entityId" in link$1 && "entityType" in link$1 && typeof link$1.entityId === "string" && typeof link$1.entityType === "string";
15097
14886
  }
15098
- function hasAppointmentStaffAccess(scope) {
15099
- return scope.kind === "appointments" || scope.kind === "hybrid";
14887
+ /**
14888
+ * Narrow internal link to page type.
14889
+ */
14890
+ function isPageLink(link$1) {
14891
+ return link$1.entityType === "page";
15100
14892
  }
15101
- function hasEventOpsAccess(scope) {
15102
- return scope.kind === "event_ops_only" || scope.kind === "hybrid";
14893
+ /**
14894
+ * Narrow internal link to content/entry type.
14895
+ */
14896
+ function isEntryLink(link$1) {
14897
+ return link$1.entityType === "content";
15103
14898
  }
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;
14899
+ /**
14900
+ * Resolve a navigation link input to URL type and link payload.
14901
+ *
14902
+ * Supports identifier-based page/entry links and validates them against routable
14903
+ * lookup maps before persisting.
14904
+ *
14905
+ * @param link - The navigation link input from SDK config or API
14906
+ * @param options - Resolution options (isDropdown flag)
14907
+ * @param pageMap - Map of page identifier -> PageNavLinkInfo
14908
+ * @param entryMap - Map of `contentType:identifier` -> EntryNavLinkInfo
14909
+ * @returns Resolved link with urlType, url payload, and any error
14910
+ */
14911
+ function resolveNavigationLinkInput(link$1, options, pageMap, entryMap) {
14912
+ if (options.isDropdown) {
14913
+ return {
14914
+ urlType: "dropdown",
14915
+ url: null,
14916
+ error: null
14917
+ };
14918
+ }
14919
+ if (!link$1) {
14920
+ return {
14921
+ urlType: "url",
14922
+ url: null,
14923
+ error: "Link is required for non-dropdown items"
14924
+ };
14925
+ }
14926
+ switch (link$1.kind) {
14927
+ case "page": return resolvePageLink(link$1.identifier, pageMap);
14928
+ case "entry": return resolveEntryLink(link$1.contentType, link$1.identifier, entryMap);
14929
+ case "external": return {
14930
+ urlType: "external",
14931
+ url: {
14932
+ kind: "external",
14933
+ href: link$1.href
14934
+ },
14935
+ error: null
14936
+ };
14937
+ case "url": return {
14938
+ urlType: "url",
14939
+ url: {
14940
+ kind: "url",
14941
+ href: link$1.href
14942
+ },
14943
+ error: null
14944
+ };
14945
+ case "portal": return {
14946
+ urlType: "portal",
14947
+ url: {
14948
+ kind: "portal",
14949
+ ...link$1.path ? { path: link$1.path } : {},
14950
+ ...link$1.labels ? { labels: link$1.labels } : {}
14951
+ },
14952
+ error: null
14953
+ };
14954
+ case "cart": return {
14955
+ urlType: "cart",
14956
+ url: {
14957
+ kind: "cart",
14958
+ badge: link$1.badge,
14959
+ label: link$1.label
14960
+ },
14961
+ error: null
14962
+ };
14963
+ default: return {
14964
+ urlType: "url",
14965
+ url: null,
14966
+ error: "Unknown link type"
14967
+ };
15111
14968
  }
15112
14969
  }
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" };
14970
+ /**
14971
+ * Resolve a page identifier to a canonical page link payload.
14972
+ */
14973
+ function resolvePageLink(identifier, pageMap) {
14974
+ const pageInfo = pageMap.get(identifier);
14975
+ if (!pageInfo) {
14976
+ return {
14977
+ urlType: "internal",
14978
+ url: null,
14979
+ error: `Page '${identifier}' not found`
14980
+ };
15126
14981
  }
15127
- const staffMemberId = unsafeAsStaffMemberId(input.linkedStaffMemberId);
15128
14982
  return {
15129
- kind: "linked",
15130
- staffMemberId,
15131
- resourceId: appointmentResourceIdFromStaffMemberId(staffMemberId)
14983
+ urlType: "internal",
14984
+ url: {
14985
+ kind: "page",
14986
+ identifier
14987
+ },
14988
+ error: null
15132
14989
  };
15133
14990
  }
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);
15142
- }
15143
- function getIdentityAppointmentResourceId(identity) {
15144
- return identity.kind === "linked" ? identity.resourceId : null;
15145
- }
15146
- function getIdentityAppointmentResourceIdValue(identity) {
15147
- return getIdentityAppointmentResourceId(identity);
14991
+ /**
14992
+ * Resolve an entry identifier to a canonical entry link payload.
14993
+ */
14994
+ function resolveEntryLink(contentType, identifier, entryMap) {
14995
+ const entryInfo = entryMap.get(`${contentType}:${identifier}`);
14996
+ if (!entryInfo) {
14997
+ return {
14998
+ urlType: "internal",
14999
+ url: null,
15000
+ error: `Entry '${contentType}:${identifier}' not found or not routable`
15001
+ };
15002
+ }
15003
+ return {
15004
+ urlType: "internal",
15005
+ url: {
15006
+ kind: "entry",
15007
+ contentType,
15008
+ identifier
15009
+ },
15010
+ error: null
15011
+ };
15148
15012
  }
15149
- function isLinkedAppointmentResource(identity, resourceId) {
15150
- if (identity.kind !== "linked") return false;
15151
- return identity.resourceId === resourceId;
15013
+
15014
+ //#endregion
15015
+ //#region ../api/src/navigation/boundaries.ts
15016
+ function isNavigationDropdownSentinel(link$1) {
15017
+ return link$1?.kind === "dropdown";
15152
15018
  }
15153
- function deriveStaffSiteHomeDestination(staffScope) {
15154
- if (hasAppointmentStaffAccess(staffScope)) {
15155
- return { kind: "staff_appointments" };
15019
+ function normalizeNavigationConfigItemForCommand(item) {
15020
+ const commandItem = { label: item.label };
15021
+ if (item.isCta !== undefined) {
15022
+ commandItem.isCta = item.isCta;
15156
15023
  }
15157
- if (hasEventOpsAccess(staffScope)) {
15158
- return { kind: "ops_site_events" };
15024
+ const commandLink = normalizeNavigationAuthoringLinkForCommand(item.link);
15025
+ if (commandLink) {
15026
+ commandItem.link = commandLink;
15159
15027
  }
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");
15028
+ if (item.children && item.children.length > 0) {
15029
+ commandItem.children = item.children.map(normalizeNavigationConfigItemForCommand);
15030
+ }
15031
+ return commandItem;
15168
15032
  }
15169
- function deriveSiteListingNavigationPlan(site) {
15170
- const staffScope = deriveStaffScope(site);
15171
- const staffHomeDestination = deriveStaffSiteHomeDestination(staffScope);
15172
- const isStaffScopedMembership = isStaffScoped(staffScope);
15033
+ function navigationCommandItemToManagementInput(item) {
15034
+ const link$1 = normalizeNavigationAuthoringLinkForCommand(item.link);
15173
15035
  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"
15036
+ label: item.label,
15037
+ ...link$1 ? { link: link$1 } : {},
15038
+ ...item.isCta !== undefined ? { isCta: item.isCta } : {},
15039
+ ...item.children && item.children.length > 0 ? { children: item.children.map(navigationCommandItemToManagementInput) } : {}
15178
15040
  };
15179
15041
  }
15180
- function shouldRedirectStaffScopeToOps(staffScope) {
15181
- return staffScope.kind === "event_ops_only";
15042
+ function normalizeNavigationAuthoringLinkForCommand(link$1) {
15043
+ if (!link$1 || isNavigationDropdownSentinel(link$1)) return undefined;
15044
+ return navigationAuthoringLinkToCommandLink(link$1);
15182
15045
  }
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" };
15046
+ function navigationApiItemToSdkLocalItem(item) {
15047
+ const link$1 = navigationReadLinkToLocalLink(item);
15048
+ const localItem = {
15049
+ label: item.label,
15050
+ ...item.isCta ? { isCta: true } : {},
15051
+ ...link$1 ? { link: link$1 } : {},
15052
+ ...item.children && item.children.length > 0 ? { children: item.children.map(navigationApiItemToSdkLocalItem) } : {}
15053
+ };
15054
+ return localItem;
15189
15055
  }
15190
-
15191
- //#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";
15056
+ function navigationAuthoringLinkToCommandLink(link$1) {
15057
+ switch (link$1.kind) {
15058
+ case "page": return {
15059
+ kind: "page",
15060
+ identifier: link$1.identifier
15061
+ };
15062
+ case "entry": return {
15063
+ kind: "entry",
15064
+ contentType: link$1.contentType,
15065
+ identifier: link$1.identifier
15066
+ };
15067
+ case "external": return {
15068
+ kind: "external",
15069
+ href: link$1.href
15070
+ };
15071
+ case "url": return {
15072
+ kind: "url",
15073
+ href: link$1.href
15074
+ };
15075
+ case "portal": return {
15076
+ kind: "portal",
15077
+ ...link$1.path ? { path: link$1.path } : {},
15078
+ ...portalLabelsForCommand(link$1.labels)
15079
+ };
15080
+ case "cart": return {
15081
+ kind: "cart",
15082
+ badge: link$1.badge,
15083
+ label: link$1.label
15084
+ };
15085
+ default: return assertNever(link$1);
15086
+ }
15196
15087
  }
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";
15088
+ function portalLabelsForCommand(labels) {
15089
+ if (!labels) return {};
15090
+ const commandLabels = {
15091
+ ...typeof labels.loggedOut === "string" ? { loggedOut: labels.loggedOut } : {},
15092
+ ...typeof labels.loggedIn === "string" ? { loggedIn: labels.loggedIn } : {}
15093
+ };
15094
+ return Object.keys(commandLabels).length > 0 ? { labels: commandLabels } : {};
15095
+ }
15096
+ function navigationReadLinkToLocalLink(item) {
15097
+ const { link: link$1 } = item;
15098
+ if (!link$1) return undefined;
15099
+ switch (link$1.kind) {
15100
+ case "page": return {
15101
+ kind: "page",
15102
+ identifier: link$1.identifier
15103
+ };
15104
+ case "entry": return {
15105
+ kind: "entry",
15106
+ contentType: link$1.contentType,
15107
+ identifier: link$1.identifier
15108
+ };
15109
+ case "internal": return internalNavigationLinkToLocalLink(item);
15110
+ case "external": return {
15111
+ kind: "external",
15112
+ href: link$1.href
15113
+ };
15114
+ case "url": return {
15115
+ kind: "url",
15116
+ href: link$1.href
15117
+ };
15118
+ case "portal": return {
15119
+ kind: "portal",
15120
+ ...link$1.path ? { path: link$1.path } : {},
15121
+ ...link$1.labels ? { labels: link$1.labels } : {}
15122
+ };
15123
+ case "cart": return {
15124
+ kind: "cart",
15125
+ badge: link$1.badge,
15126
+ label: link$1.label
15127
+ };
15128
+ default: return assertNever(link$1);
15129
+ }
15130
+ }
15131
+ function internalNavigationLinkToLocalLink(item) {
15132
+ const { link: link$1 } = item;
15133
+ if (!link$1 || link$1.kind !== "internal") return undefined;
15134
+ if (item.resolvedIdentifier) {
15135
+ if (link$1.entityType === "page") {
15136
+ return {
15137
+ kind: "page",
15138
+ identifier: item.resolvedIdentifier
15139
+ };
15140
+ }
15141
+ if (link$1.entityType === "content" && item.resolvedContentType) {
15142
+ return {
15143
+ kind: "entry",
15144
+ contentType: item.resolvedContentType,
15145
+ identifier: item.resolvedIdentifier
15146
+ };
15147
+ }
15148
+ }
15149
+ if (link$1.href) {
15150
+ return {
15151
+ kind: "url",
15152
+ href: link$1.href
15153
+ };
15154
+ }
15155
+ return undefined;
15207
15156
  }
15208
15157
 
15209
15158
  //#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
- ];
15159
+ //#region ../api/src/navigation/routableLink.ts
15160
+ /**
15161
+ * Prefer identifier-based links when available so links survive route/path updates.
15162
+ * Falls back to an internal route payload for legacy rows or incomplete data.
15163
+ */
15164
+ function toRoutableLinkPayload(item) {
15165
+ switch (item.entityType) {
15166
+ case "page":
15167
+ if (item.identifier) {
15168
+ return {
15169
+ kind: "page",
15170
+ identifier: item.identifier
15171
+ };
15172
+ }
15173
+ return toResolvedInternalPayload(item);
15174
+ case "content":
15175
+ if (item.identifier && item.contentTypeKey) {
15176
+ return {
15177
+ kind: "entry",
15178
+ contentType: item.contentTypeKey,
15179
+ identifier: item.identifier
15180
+ };
15181
+ }
15182
+ return toResolvedInternalPayload(item);
15183
+ case "pageSurface": return toResolvedInternalPayload(item);
15184
+ default: return assertNever(item);
15185
+ }
15186
+ }
15187
+ function toResolvedInternalPayload(item) {
15188
+ return {
15189
+ kind: "internal",
15190
+ routeId: item.routeId,
15191
+ entityId: item.entityId,
15192
+ entityType: item.entityType,
15193
+ href: item.path ?? "/",
15194
+ title: item.title ?? item.draftTitle ?? "Untitled",
15195
+ typeLabel: item.typeLabel,
15196
+ contentTypeKey: item.contentTypeKey,
15197
+ contentTypeName: item.contentTypeName,
15198
+ updatedAt: item.updatedAt ?? null
15199
+ };
15200
+ }
15357
15201
 
15358
15202
  //#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);
15203
+ //#region ../api/src/navigation/linkUrlType.ts
15204
+ const LINK_PAYLOAD_NAVIGATION_URL_TYPES = {
15205
+ page: "internal",
15206
+ entry: "internal",
15207
+ internal: "internal",
15208
+ external: "external",
15209
+ url: "url",
15210
+ portal: "portal",
15211
+ cart: "cart"
15212
+ };
15213
+ function linkPayloadKindToNavigationUrlType(kind) {
15214
+ return LINK_PAYLOAD_NAVIGATION_URL_TYPES[kind];
15368
15215
  }
15369
15216
 
15370
15217
  //#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";
15218
+ //#region ../api/src/accessAdmin.ts
15219
+ const STAFF_OPERATIONAL_CAPABILITIES = ["appointments", "event_ops"];
15220
+ function getDefaultStaffCapabilities(surfaces) {
15221
+ if (surfaces.events && !surfaces.appointments) return ["event_ops"];
15222
+ return ["appointments"];
15381
15223
  }
15382
- function isBeforeBookingQuestionRequirementScope(scope) {
15383
- return scope === "before_booking_booking_level";
15224
+ const managedDomainAddOnPricingSnapshotSchema = z.object({
15225
+ formulaVersion: z.literal("managed-domain-overage-v1"),
15226
+ conversionRule: z.literal("usd-to-gbp-1-to-1-v1"),
15227
+ registrarAnnualCost: z.number().nonnegative(),
15228
+ registrarCurrency: z.literal("USD"),
15229
+ includedAnnualAllowance: z.number().nonnegative(),
15230
+ includedAnnualAllowanceCurrency: z.literal("USD"),
15231
+ monthlyAmountCents: z.number().int().nonnegative(),
15232
+ monthlyCurrency: z.literal("gbp")
15233
+ });
15234
+
15235
+ //#endregion
15236
+ //#region ../api/src/siteMembers.ts
15237
+ /**
15238
+ * Higher number means more permissions.
15239
+ */
15240
+ const SITE_ROLE_PERMISSION_LEVEL = {
15241
+ team_member: 0,
15242
+ editor: 1,
15243
+ administrator: 2,
15244
+ owner: 3
15245
+ };
15246
+ /**
15247
+ * Lower number sorts earlier in UI role pickers.
15248
+ */
15249
+ const SITE_ROLE_DISPLAY_ORDER = {
15250
+ owner: 0,
15251
+ administrator: 1,
15252
+ editor: 2,
15253
+ team_member: 3
15254
+ };
15255
+ async function getSiteMembers(apiClient, { siteId, options }) {
15256
+ try {
15257
+ return unwrapResponse(await apiClient({
15258
+ endpoint: "listSiteMembers",
15259
+ params: { siteId },
15260
+ options
15261
+ }));
15262
+ } catch (error) {
15263
+ return normalizeApiError(error, "Failed to load site members");
15264
+ }
15384
15265
  }
15385
- async function getBookingSettingsRequest(apiClient, siteId, options) {
15266
+ async function inviteSiteMember(apiClient, { siteId, body, options }) {
15386
15267
  try {
15387
- const response = unwrapResponse(await apiClient({
15388
- endpoint: "getBookingSettings",
15268
+ return unwrapResponse(await apiClient({
15269
+ endpoint: "inviteSiteMember",
15389
15270
  params: { siteId },
15271
+ body,
15390
15272
  options
15391
15273
  }));
15392
- return response.settings;
15393
15274
  } catch (error) {
15394
- normalizeApiError(error, "Failed to load booking settings");
15275
+ return normalizeApiError(error, "Failed to invite collaborator");
15395
15276
  }
15396
15277
  }
15397
- async function updateBookingSettingsRequest(apiClient, siteId, payload, options) {
15278
+ async function updateSiteMemberRole(apiClient, { siteId, memberId, body, options }) {
15398
15279
  try {
15399
- const response = unwrapResponse(await apiClient({
15400
- endpoint: "updateBookingSettings",
15280
+ return unwrapResponse(await apiClient({
15281
+ endpoint: "updateSiteMemberRole",
15282
+ params: {
15283
+ siteId,
15284
+ memberId
15285
+ },
15286
+ body,
15287
+ options
15288
+ }));
15289
+ } catch (error) {
15290
+ return normalizeApiError(error, "Failed to update member role");
15291
+ }
15292
+ }
15293
+ async function linkCurrentUserPractitioner(apiClient, { siteId, body, options }) {
15294
+ try {
15295
+ return unwrapResponse(await apiClient({
15296
+ endpoint: "linkCurrentUserPractitioner",
15401
15297
  params: { siteId },
15402
- body: payload,
15298
+ body,
15403
15299
  options
15404
15300
  }));
15405
- return response.settings;
15406
15301
  } catch (error) {
15407
- normalizeApiError(error, "Failed to save booking settings");
15302
+ return normalizeApiError(error, "Failed to link practitioner profile");
15408
15303
  }
15409
15304
  }
15410
- async function createAppointmentRequest(apiClient, siteId, payload, options) {
15305
+ async function unlinkCurrentUserPractitioner(apiClient, { siteId, options }) {
15411
15306
  try {
15412
15307
  return unwrapResponse(await apiClient({
15413
- endpoint: "createAppointment",
15308
+ endpoint: "unlinkCurrentUserPractitioner",
15414
15309
  params: { siteId },
15415
- body: payload,
15416
15310
  options
15417
15311
  }));
15418
15312
  } catch (error) {
15419
- normalizeApiError(error, "Failed to create appointment");
15313
+ return normalizeApiError(error, "Failed to unlink practitioner profile");
15314
+ }
15315
+ }
15316
+ async function removeSiteMember(apiClient, { siteId, memberId, options }) {
15317
+ try {
15318
+ return unwrapResponse(await apiClient({
15319
+ endpoint: "removeSiteMember",
15320
+ params: {
15321
+ siteId,
15322
+ memberId
15323
+ },
15324
+ options
15325
+ }));
15326
+ } catch (error) {
15327
+ return normalizeApiError(error, "Failed to remove collaborator");
15420
15328
  }
15421
15329
  }
15422
- async function deleteEventOfferingRequest(apiClient, siteId, eventOfferingId, options) {
15330
+ async function revokeSiteInvitation(apiClient, { siteId, invitationId, options }) {
15423
15331
  try {
15424
15332
  return unwrapResponse(await apiClient({
15425
- endpoint: "deleteEventOffering",
15333
+ endpoint: "revokeSiteInvitation",
15426
15334
  params: {
15427
15335
  siteId,
15428
- eventOfferingId
15336
+ invitationId
15429
15337
  },
15430
15338
  options
15431
15339
  }));
15432
15340
  } catch (error) {
15433
- normalizeApiError(error, "Failed to delete event offering");
15341
+ return normalizeApiError(error, "Failed to revoke invitation");
15434
15342
  }
15435
15343
  }
15436
- async function addEventSeriesRegistrationRequest(apiClient, siteId, eventSeriesId, payload, options) {
15344
+ async function resendSiteInvitation(apiClient, { siteId, invitationId, options }) {
15437
15345
  try {
15438
15346
  return unwrapResponse(await apiClient({
15439
- endpoint: "addEventSeriesRegistration",
15347
+ endpoint: "resendSiteInvitation",
15440
15348
  params: {
15441
15349
  siteId,
15442
- seriesId: eventSeriesId
15350
+ invitationId
15443
15351
  },
15444
- body: payload,
15445
15352
  options
15446
15353
  }));
15447
15354
  } catch (error) {
15448
- normalizeApiError(error, "Failed to add Event run registration");
15355
+ return normalizeApiError(error, "Failed to resend invitation");
15449
15356
  }
15450
15357
  }
15451
- async function updateEventSeriesRegistrationRequest(apiClient, siteId, eventSeriesId, registrationId, payload, options) {
15358
+ async function transferSiteOwnership(apiClient, { siteId, body, options }) {
15452
15359
  try {
15453
15360
  return unwrapResponse(await apiClient({
15454
- endpoint: "updateEventSeriesRegistration",
15455
- params: {
15456
- siteId,
15457
- seriesId: eventSeriesId,
15458
- registrationId
15459
- },
15460
- body: payload,
15361
+ endpoint: "transferSiteOwnership",
15362
+ params: { siteId },
15363
+ body,
15461
15364
  options
15462
15365
  }));
15463
15366
  } catch (error) {
15464
- normalizeApiError(error, "Failed to update Event run registration");
15367
+ return normalizeApiError(error, "Failed to transfer ownership");
15465
15368
  }
15466
15369
  }
15467
- async function addEventOccurrenceAttendeeRequest(apiClient, siteId, seriesId, occurrenceId, payload, options) {
15370
+ async function acceptSiteInvitation(apiClient, { body, options }) {
15468
15371
  try {
15469
15372
  return unwrapResponse(await apiClient({
15470
- endpoint: "addEventOccurrenceAttendee",
15471
- params: {
15472
- siteId,
15473
- seriesId,
15474
- occurrenceId
15475
- },
15476
- body: payload,
15373
+ endpoint: "acceptSiteInvitation",
15374
+ body,
15477
15375
  options
15478
15376
  }));
15479
15377
  } catch (error) {
15480
- normalizeApiError(error, "Failed to add attendee");
15378
+ return normalizeApiError(error, "Failed to accept invitation");
15379
+ }
15380
+ }
15381
+
15382
+ //#endregion
15383
+ //#region ../api/src/staffAccess.ts
15384
+ function roleAtLeast$1(role, minimum) {
15385
+ return SITE_ROLE_PERMISSION_LEVEL[role] >= SITE_ROLE_PERMISSION_LEVEL[minimum];
15386
+ }
15387
+ function roleAtLeastOrNone(role, minimum) {
15388
+ if (!role) return false;
15389
+ return roleAtLeast$1(role, minimum);
15390
+ }
15391
+ function appointmentResourceIdFromStaffMemberId(staffMemberId) {
15392
+ return unsafeAsAppointmentResourceId(staffMemberId);
15393
+ }
15394
+ function normalizeStaffCapabilities(capabilities) {
15395
+ if (!capabilities) return null;
15396
+ return STAFF_OPERATIONAL_CAPABILITIES.filter((capability) => capabilities.includes(capability));
15397
+ }
15398
+ function deriveStaffScope(input) {
15399
+ const staffMemberId = input.linkedStaffMemberId ? unsafeAsStaffMemberId(input.linkedStaffMemberId) : null;
15400
+ const persistedCapabilities = normalizeStaffCapabilities(input.staffCapabilities);
15401
+ switch (input.membershipRole) {
15402
+ case null:
15403
+ case "owner":
15404
+ case "administrator":
15405
+ case "editor": return { kind: "none" };
15406
+ case "team_member":
15407
+ if (!staffMemberId) {
15408
+ return {
15409
+ kind: "staff_setup_required",
15410
+ reason: "missing_staff_member"
15411
+ };
15412
+ }
15413
+ if (persistedCapabilities === null) {
15414
+ return {
15415
+ kind: "appointments",
15416
+ staffMemberId
15417
+ };
15418
+ }
15419
+ if (persistedCapabilities.length === 0) {
15420
+ return {
15421
+ kind: "staff_setup_required",
15422
+ reason: "missing_capabilities"
15423
+ };
15424
+ }
15425
+ if (persistedCapabilities.includes("appointments") && persistedCapabilities.includes("event_ops")) {
15426
+ return {
15427
+ kind: "hybrid",
15428
+ staffMemberId
15429
+ };
15430
+ }
15431
+ if (persistedCapabilities.includes("event_ops")) {
15432
+ return {
15433
+ kind: "event_ops_only",
15434
+ staffMemberId
15435
+ };
15436
+ }
15437
+ return {
15438
+ kind: "appointments",
15439
+ staffMemberId
15440
+ };
15441
+ }
15442
+ }
15443
+ function isStaffScoped(scope) {
15444
+ return scope.kind !== "none";
15445
+ }
15446
+ function isTeamMemberScope(scope) {
15447
+ return scope.kind === "appointments" || scope.kind === "staff_setup_required";
15448
+ }
15449
+ function hasAppointmentStaffAccess(scope) {
15450
+ return scope.kind === "appointments" || scope.kind === "hybrid";
15451
+ }
15452
+ function hasEventOpsAccess(scope) {
15453
+ return scope.kind === "event_ops_only" || scope.kind === "hybrid";
15454
+ }
15455
+ function getLinkedStaffMemberId(scope) {
15456
+ switch (scope.kind) {
15457
+ case "appointments":
15458
+ case "event_ops_only":
15459
+ case "hybrid": return scope.staffMemberId;
15460
+ case "none":
15461
+ case "staff_setup_required": return null;
15462
+ }
15463
+ }
15464
+ function getOwnAppointmentResourceId(scope) {
15465
+ const staffMemberId = getLinkedStaffMemberId(scope);
15466
+ return staffMemberId ? appointmentResourceIdFromStaffMemberId(staffMemberId) : null;
15467
+ }
15468
+ function canAccessOwnAppointmentResource(scope, resourceId) {
15469
+ const ownResourceId = getOwnAppointmentResourceId(scope);
15470
+ if (!ownResourceId) return false;
15471
+ const normalizedResourceId = typeof resourceId === "string" ? unsafeAsAppointmentResourceId(resourceId) : resourceId;
15472
+ return ownResourceId === normalizedResourceId;
15473
+ }
15474
+ function deriveLinkedAppointmentIdentity(input) {
15475
+ if (!input.membershipRole || !input.linkedStaffMemberId) {
15476
+ return { kind: "none" };
15477
+ }
15478
+ const staffMemberId = unsafeAsStaffMemberId(input.linkedStaffMemberId);
15479
+ return {
15480
+ kind: "linked",
15481
+ staffMemberId,
15482
+ resourceId: appointmentResourceIdFromStaffMemberId(staffMemberId)
15483
+ };
15484
+ }
15485
+ function getIdentityStaffMemberId(identity) {
15486
+ switch (identity.kind) {
15487
+ case "linked": return identity.staffMemberId;
15488
+ case "none": return null;
15489
+ }
15490
+ }
15491
+ function getIdentityStaffMemberIdValue(identity) {
15492
+ return getIdentityStaffMemberId(identity);
15493
+ }
15494
+ function getIdentityAppointmentResourceId(identity) {
15495
+ return identity.kind === "linked" ? identity.resourceId : null;
15496
+ }
15497
+ function getIdentityAppointmentResourceIdValue(identity) {
15498
+ return getIdentityAppointmentResourceId(identity);
15499
+ }
15500
+ function isLinkedAppointmentResource(identity, resourceId) {
15501
+ if (identity.kind !== "linked") return false;
15502
+ return identity.resourceId === resourceId;
15503
+ }
15504
+ function deriveStaffSiteHomeDestination(staffScope) {
15505
+ if (hasAppointmentStaffAccess(staffScope)) {
15506
+ return { kind: "staff_appointments" };
15507
+ }
15508
+ if (hasEventOpsAccess(staffScope)) {
15509
+ return { kind: "ops_site_events" };
15481
15510
  }
15511
+ return { kind: "dashboard_site_home" };
15512
+ }
15513
+ function canUseAppointmentsLabel(staffScope) {
15514
+ return staffScope.kind === "appointments" || staffScope.kind === "hybrid" || staffScope.kind === "staff_setup_required";
15515
+ }
15516
+ function isEventOpsOnlyUser(sites) {
15517
+ if (sites.length === 0) return false;
15518
+ return sites.every((site) => deriveStaffScope(site).kind === "event_ops_only");
15519
+ }
15520
+ function deriveSiteListingNavigationPlan(site) {
15521
+ const staffScope = deriveStaffScope(site);
15522
+ const staffHomeDestination = deriveStaffSiteHomeDestination(staffScope);
15523
+ const isStaffScopedMembership = isStaffScoped(staffScope);
15524
+ return {
15525
+ canUseDashboard: roleAtLeastOrNone(site.membershipRole, "editor") || isStaffScopedMembership,
15526
+ primaryDestination: isStaffScopedMembership ? staffHomeDestination : { kind: "dashboard_site_open" },
15527
+ manageDestination: isStaffScopedMembership ? staffHomeDestination : { kind: "dashboard_site_home" },
15528
+ manageLabel: staffScope.kind === "event_ops_only" ? "Ops" : canUseAppointmentsLabel(staffScope) ? "Appointments" : "Manage"
15529
+ };
15530
+ }
15531
+ function shouldRedirectStaffScopeToOps(staffScope) {
15532
+ return staffScope.kind === "event_ops_only";
15533
+ }
15534
+ function deriveOpsLandingDestination(sites) {
15535
+ const onlySite = sites.length === 1 ? sites[0] : undefined;
15536
+ return onlySite?.id ? {
15537
+ kind: "ops_site_events",
15538
+ siteId: onlySite.id
15539
+ } : { kind: "ops_home" };
15482
15540
  }
15483
15541
 
15542
+ //#endregion
15543
+ //#region ../api/src/common/cancellation.ts
15544
+ const STAFF_CANCELLATION_CONTEXT_UI_CATEGORIES = [
15545
+ "customer_requested",
15546
+ "schedule_conflict",
15547
+ "duplicate_or_admin_error",
15548
+ "non_payment",
15549
+ "policy_or_eligibility",
15550
+ "other"
15551
+ ];
15552
+ const STAFF_CANCELLATION_CONTEXT_CATEGORIES = [...STAFF_CANCELLATION_CONTEXT_UI_CATEGORIES, "legacy_client_unspecified"];
15553
+
15554
+ //#endregion
15555
+ //#region ../api/src/siteOperations.ts
15556
+ const FLEXIBLE_BALANCE_ADMIN_ACTION_SOURCES = [
15557
+ "order_detail",
15558
+ "payment_issues_queue",
15559
+ "appointment_detail",
15560
+ "event_attendee_detail",
15561
+ "event_series_registration_detail"
15562
+ ];
15563
+
15484
15564
  //#endregion
15485
15565
  //#region ../api/src/navigation/visibility.ts
15486
15566
  /**
@@ -16291,19 +16371,8 @@ function buildDashboardBookingsNavPlan(modules, ctx, catalogue) {
16291
16371
  if (policy.kind === "staff_scoped") {
16292
16372
  return buildStaffScopedBookingsNavPlan(policy);
16293
16373
  }
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
16374
  const hasEnabledModules = modules.appointments || modules.events;
16375
+ const groups = [buildOverviewNavGroup(hasEnabledModules)];
16307
16376
  groups.push({
16308
16377
  label: "Events",
16309
16378
  items: modules.events ? [
@@ -16520,6 +16589,32 @@ function buildDashboardBookingsNavPlan(modules, ctx, catalogue) {
16520
16589
  }
16521
16590
  return groups;
16522
16591
  }
16592
+ /** At a glance: the bookings Overview, and the Timetable once anything can be booked. */
16593
+ function buildOverviewNavGroup(hasEnabledModules) {
16594
+ return {
16595
+ label: "At a glance",
16596
+ items: [{
16597
+ label: "Overview",
16598
+ destination: { kind: "bookings_dashboard" },
16599
+ keywords: [
16600
+ "overview",
16601
+ "summary",
16602
+ "activity"
16603
+ ]
16604
+ }, ...hasEnabledModules ? [{
16605
+ label: "Timetable",
16606
+ destination: { kind: "timetable" },
16607
+ keywords: [
16608
+ "schedule",
16609
+ "calendar",
16610
+ "week",
16611
+ "diary",
16612
+ "what's on",
16613
+ "clashes"
16614
+ ]
16615
+ }] : []]
16616
+ };
16617
+ }
16523
16618
  function buildStaffScopedBookingsNavPlan(policy) {
16524
16619
  if (policy.appointments.kind !== "calendar") return [];
16525
16620
  const appointmentItems = [{
@@ -23107,4 +23202,4 @@ function optionalTitle(data) {
23107
23202
  }
23108
23203
 
23109
23204
  //#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 };
23205
+ export { buildStorageUrl as $, createClientForEnv as $t, formatDiffDetail as A, createOutput as An, convertPulledFooterToLocal as At, formatFieldPath as B, linkPayloadSchema as Bn, extractApiErrorMessage 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, applyPushConfigPlan as Ft, prepareLocalMediaForPush as G, createBearerAPIClient as Gt, syncLocalMediaForPush as H, sdkConfiguredBookingVerticalConfigSchema as Hn, isRecord$5 as Ht, buildJsonDiff as I, createManagementClient as In, createPushConfigCommand as It, writeBinaryFileAtomic as J, deriveCmsApiUrlFromDashboardUrl as Jt, admitBinaryWriteDestination as K, ApiEnvelopeError as Kt, calculateEntryDiffs as L, ManagementApiError as Ln, loadPushConfigTarget as Lt, hasPendingChanges as M, getEnvironmentDebugInfo as Mn, convertPulledPagesToLocal as Mt, findChangedFieldsWithValues as N, isLocalConfigured as Nn, navigationCommandItemToManagementInput as Nt, calculateDiff as O, transportSuggestion as On, navigationIdentity as Ot, formatHumanDiff as P, isRemoteConfigured as Pn, normalizeNavigationConfigItemForCommand as Pt, walkLocalContentMedia as Q, confirmAction as Qt, buildFieldChanges as R, sdkEventScheduleSchema as Rn, preparePushConfigTarget as Rt, planCliCommandBatchForBindings as S, planEnvironmentExecution as Sn, listFiles as St, executeResidualSettingsSyncPlan as T, withRiverbankEnv as Tn, writeJsonFileAtomic as Tt, syncLocalMediaForPull as U, APPOINTMENT_SERVICE_VISIBILITY_VALUES as Ut, revalidateLocalMediaPushPlan as V, portablePageDesignSchema as Vn, normalizeApiError 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, revalidatePushConfigPlan as zt };