riverbankcms 0.19.0 → 0.20.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,4635 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Ar as asAppearancePresetId, Nr as asPageFlowPresetId, Pr as asSiteStyleId, Qn as buildMediaIdentifierFromFilename, T as eventBookingOfferIntentSchema, ar as getExtensionForMimeType, cr as normalizeAmbiguousStoragePathForSite, d as stableStringify, dn as footerPreferencesSchema, dr as MAX_MANAGEMENT_MEDIA_UPLOAD_BYTES, en as buttonStyleSelectionSchema, ln as pageDesignBoundaryTreatmentSchema, n as sdkCourseBookingSettingsSchema, nr as slugifyMediaIdentifier, or as getMimeType, r as sdkEventOccurrenceInputSchema, s as eventVisibilitySchema, tr as resolveMediaIdentifier, un as pageDesignIntentSchemaVersion, ur as FILE_SIZE_LIMITS, ut as themeSchema, zn as getDirectImageUrlForSiteScopedPath } from "./sdk-event-upsert-BdvfnCyV.mjs";
3
- import { A as parseFooterCompositionV1, Dn as ok, En as err, Rt as assertNever, a as canonicalizeConfiguredBookingVerticalConfig, c as resolveSiteVerticalBookingConfig, l as DEFAULT_EVENT_TYPE_PRESETS, o as parseVerticalId, on as unsafeAsEventOfferingId, s as parseVerticalPresetId, sn as unsafeAsEventSeriesId, t as resolveEventCatalogue, u as EVENT_PRESET_VALUES, un as unsafeAsOccurrenceId, y as exposeSecret } from "./src-yL0DBH7S.mjs";
4
- import * as fs$1 from "fs/promises";
5
- import * as path$1 from "path";
6
- import { z } from "zod";
7
- import { constants } from "node:fs";
8
- import { basename, dirname as dirname$1, join } from "node:path";
9
- import { createHash, randomUUID } from "node:crypto";
10
- import { access, chmod, link, lstat, mkdir, open, rename, unlink as unlink$1 } from "node:fs/promises";
11
-
12
- //#region ../api/src/bookingVerticals.ts
13
- /**
14
- * Transport adapters for the booking-vertical domain.
15
- *
16
- * The domain catalog and all pure resolution/selection rules live in core;
17
- * this module owns only the Zod boundary used by API payloads.
18
- */
19
- function formatRawDiagnosticValue(value) {
20
- const serialized = JSON.stringify(value);
21
- return serialized === undefined ? String(value) : serialized;
22
- }
23
- function describeSiteVerticalConfigIssue(issue) {
24
- switch (issue.code) {
25
- case "unknown_vertical": return `${issue.code}: ${formatRawDiagnosticValue(issue.rawValue)}`;
26
- case "unknown_preset": return `${issue.code}: ${formatRawDiagnosticValue(issue.rawValue)}`;
27
- case "missing_vertical":
28
- case "empty_selected_presets":
29
- case "preset_vertical_mismatch":
30
- case "duplicate_preset_id":
31
- case "duplicate_canonical_behaviour":
32
- case "future_preset_selected": return issue.code;
33
- default: return assertNever(issue);
34
- }
35
- }
36
- const VerticalIdSchema = z.string().trim().transform((value, ctx) => {
37
- const parsed = parseVerticalId(value);
38
- if (!parsed) {
39
- ctx.addIssue({
40
- code: z.ZodIssueCode.custom,
41
- message: "Unknown booking vertical"
42
- });
43
- return z.NEVER;
44
- }
45
- return parsed;
46
- });
47
- const VerticalPresetIdSchema = z.string().trim().transform((value, ctx) => {
48
- const parsed = parseVerticalPresetId(value);
49
- if (!parsed) {
50
- ctx.addIssue({
51
- code: z.ZodIssueCode.custom,
52
- message: "Unknown booking preset"
53
- });
54
- return z.NEVER;
55
- }
56
- return parsed;
57
- });
58
- const BookingVerticalConfigSchema = z.object({
59
- verticalId: VerticalIdSchema.nullable(),
60
- selectedPresetIds: z.array(VerticalPresetIdSchema).max(20)
61
- }).transform((value, ctx) => {
62
- const resolved = resolveSiteVerticalBookingConfig(value);
63
- if (resolved.kind === "invalid") {
64
- ctx.addIssue({
65
- code: z.ZodIssueCode.custom,
66
- message: `Invalid booking vertical config: ${resolved.issues.map(describeSiteVerticalConfigIssue).join(", ")}`
67
- });
68
- return z.NEVER;
69
- }
70
- return resolved.config;
71
- });
72
-
73
- //#endregion
74
- //#region ../api/src/sdkBookingVerticalSchemas.ts
75
- const sdkConfiguredBookingVerticalConfigSchema = z.object({
76
- verticalId: VerticalIdSchema,
77
- selectedPresetIds: z.tuple([VerticalPresetIdSchema]).rest(VerticalPresetIdSchema)
78
- }).transform((config, context) => {
79
- const canonical = canonicalizeConfiguredBookingVerticalConfig(config);
80
- if (canonical.ok) return canonical.config;
81
- for (const issue of canonical.issues) {
82
- context.addIssue({
83
- code: z.ZodIssueCode.custom,
84
- path: ["selectedPresetIds"],
85
- message: issue.code
86
- });
87
- }
88
- return z.NEVER;
89
- });
90
- const sdkConfigureBookingVerticalRequestSchema = z.discriminatedUnion("mode", [z.object({
91
- mode: z.literal("preview"),
92
- verticalConfig: sdkConfiguredBookingVerticalConfigSchema,
93
- expectedRevision: z.string().min(1).nullable().optional()
94
- }).strict(), z.object({
95
- mode: z.literal("apply"),
96
- verticalConfig: sdkConfiguredBookingVerticalConfigSchema,
97
- expectedRevision: z.string().min(1).nullable()
98
- }).strict()]);
99
- const sdkVerticalConfigIssueSchema = z.discriminatedUnion("code", [
100
- z.object({
101
- code: z.literal("unknown_vertical"),
102
- rawValue: z.unknown()
103
- }),
104
- z.object({
105
- code: z.literal("unknown_preset"),
106
- rawValue: z.unknown()
107
- }),
108
- z.object({ code: z.literal("missing_vertical") }),
109
- z.object({ code: z.literal("empty_selected_presets") }),
110
- z.object({ code: z.literal("preset_vertical_mismatch") }),
111
- z.object({ code: z.literal("duplicate_preset_id") }),
112
- z.object({ code: z.literal("duplicate_canonical_behaviour") }),
113
- z.object({ code: z.literal("future_preset_selected") })
114
- ]);
115
- const sdkBookingSettingsWarningSchema = z.discriminatedUnion("code", [z.object({
116
- code: z.literal("booking_module_disabled"),
117
- module: z.enum(["appointments", "events"])
118
- }), z.object({
119
- code: z.literal("replaced_invalid_current_config"),
120
- issues: z.array(sdkVerticalConfigIssueSchema)
121
- })]);
122
- const sdkPullSettingsWarningSchema = z.object({
123
- code: z.literal("invalid_booking_vertical_config"),
124
- issues: z.array(sdkVerticalConfigIssueSchema)
125
- });
126
-
127
- //#endregion
128
- //#region ../api/src/sdkCommandResponseSchemas.ts
129
- const sdkCommandEffectSchema = z.discriminatedUnion("kind", [
130
- z.object({ kind: z.literal("created") }),
131
- z.object({ kind: z.literal("updated") }),
132
- z.object({ kind: z.literal("unchanged") }),
133
- z.object({ kind: z.literal("deleted") }),
134
- z.object({
135
- kind: z.literal("transitioned"),
136
- transition: z.enum(["published", "unpublished"])
137
- })
138
- ]);
139
- function sdkCommandApplicationResultSchema(value) {
140
- return z.object({
141
- value,
142
- effect: sdkCommandEffectSchema
143
- }).passthrough();
144
- }
145
-
146
- //#endregion
147
- //#region ../api/src/sdkBlockUpsertContracts.ts
148
- const sdkBlockAppearanceChoiceSchema = z.discriminatedUnion("kind", [z.object({
149
- kind: z.literal("pinned"),
150
- appearancePresetId: z.string().min(1).transform(asAppearancePresetId)
151
- }), z.object({ kind: z.literal("automatic") })]);
152
- const sdkBlockAppearanceOutcomeSchema = z.discriminatedUnion("kind", [z.object({
153
- kind: z.literal("applied"),
154
- choice: sdkBlockAppearanceChoiceSchema,
155
- effect: z.enum(["updated", "unchanged"])
156
- }), z.object({
157
- kind: z.literal("not-applied"),
158
- code: z.enum(["STALE_PAGE_DESIGN_STATE", "SAVE_PAGE_DESIGN_STATE_FAILED"]),
159
- message: z.string(),
160
- retryable: z.literal(true)
161
- })]);
162
- const pageBlockDesignSyncWarningSchema = z.object({
163
- kind: z.enum([
164
- "blocked",
165
- "failed",
166
- "skipped-invalid"
167
- ]),
168
- operation: z.enum([
169
- "create",
170
- "update",
171
- "delete",
172
- "reorder"
173
- ]),
174
- message: z.string(),
175
- blocker: z.string().optional(),
176
- source: z.enum(["draft", "published-fallback"]).optional(),
177
- reason: z.string().optional()
178
- });
179
-
180
- //#endregion
181
- //#region ../api/src/sdkOccurrenceResponseSchemas.ts
182
- const identifierSchema$1 = z.string().min(1);
183
- const timestampSchema = z.string().min(1);
184
- const sdkManagementEventOccurrenceSchema = z.object({
185
- id: identifierSchema$1.transform(unsafeAsOccurrenceId),
186
- seriesId: identifierSchema$1.transform(unsafeAsEventSeriesId),
187
- startsAt: timestampSchema,
188
- endsAt: timestampSchema,
189
- timeZone: z.string().min(1),
190
- status: z.enum([
191
- "scheduled",
192
- "cancelled",
193
- "completed"
194
- ]),
195
- provenance: z.enum([
196
- "manual",
197
- "recurrence_generated",
198
- "recurrence_exception"
199
- ]),
200
- capacityOverride: z.number().int().nonnegative().nullable(),
201
- capacity: z.number().int().nonnegative().nullable(),
202
- confirmedPlaces: z.number().int().nonnegative().optional(),
203
- registrations: z.number().int().nonnegative(),
204
- deletionEligibility: z.discriminatedUnion("kind", [
205
- z.object({ kind: z.literal("allowed") }),
206
- z.object({
207
- kind: z.literal("blocked_by_registrations"),
208
- registrations: z.number().int().positive()
209
- }),
210
- z.object({ kind: z.literal("blocked_generated_occurrence") }),
211
- z.object({ kind: z.literal("blocked_completed_occurrence") })
212
- ])
213
- });
214
- const sdkListEventOccurrencesResponseSchema = z.object({
215
- occurrences: z.array(sdkManagementEventOccurrenceSchema),
216
- nextCursor: z.string().nullable()
217
- });
218
- const businessCancellationResultSchema = z.object({
219
- kind: z.enum([
220
- "finished",
221
- "unfinished",
222
- "partial_application"
223
- ]),
224
- paymentTerms: z.enum([
225
- "complete",
226
- "unfinished",
227
- "unavailable"
228
- ]),
229
- refunds: z.enum([
230
- "complete",
231
- "processing",
232
- "needs_attention",
233
- "unavailable"
234
- ]),
235
- emails: z.enum([
236
- "complete",
237
- "queued",
238
- "needs_attention",
239
- "unavailable"
240
- ]),
241
- operationId: identifierSchema$1,
242
- directContactAttendeeIds: z.array(identifierSchema$1),
243
- cancelledBookings: z.number().int().nonnegative(),
244
- refundedBookings: z.number().int().nonnegative(),
245
- restoredPassCredits: z.number().int().nonnegative(),
246
- pending: z.object({
247
- refundAttendeeIds: z.array(identifierSchema$1),
248
- creditAttendeeIds: z.array(identifierSchema$1),
249
- emailAttendeeIds: z.array(identifierSchema$1)
250
- }),
251
- structural: z.object({
252
- created: z.number().int().nonnegative(),
253
- deleted: z.number().int().nonnegative(),
254
- cancelled: z.number().int().nonnegative(),
255
- unfinishedOccurrenceIds: z.array(identifierSchema$1),
256
- unfinishedCreateCount: z.number().int().nonnegative().optional(),
257
- reason: z.enum([
258
- "superseded",
259
- "not_found",
260
- "restore_blocked",
261
- "failed",
262
- "result_unavailable"
263
- ]).optional()
264
- })
265
- });
266
- const sdkEventOccurrenceResponseSchema = z.object({
267
- occurrence: sdkManagementEventOccurrenceSchema,
268
- cancellationResult: businessCancellationResultSchema.optional(),
269
- cancellationResults: z.array(businessCancellationResultSchema).optional()
270
- });
271
- const sdkDeleteEventOccurrenceResponseSchema = z.object({ deleted: z.literal(true) });
272
- const sdkReorderResponseSchema = z.object({
273
- reordered: z.literal(true),
274
- effect: sdkCommandEffectSchema,
275
- designSyncWarning: pageBlockDesignSyncWarningSchema.optional()
276
- });
277
-
278
- //#endregion
279
- //#region ../api/src/sdkPageDesign.ts
280
- /**
281
- * The portable page design document (issue 1497).
282
- *
283
- * A stored `PageDesignIntent` names blocks by their database ids, so it only
284
- * makes sense on the page it came from. The portable document is the same
285
- * intent with every block reference expressed as the block's management
286
- * identifier (`hero-0`, `body-text-1`), which is what lets a design travel from
287
- * one page to another page built from the same blocks.
288
- *
289
- * A boundary is carried as its identifier pair only. Its stored `boundaryId`
290
- * is derived from the pair after mapping, so the two can never disagree.
291
- *
292
- * The document is parsed once, here, at the JSON boundary. Whether it fits a
293
- * particular destination page is a separate, semantic decision made by the
294
- * dashboard against that page's blocks and site style.
295
- */
296
- const unsafeAsBlockIdentifier = (value) => value;
297
- const blockIdentifierSchema = z.string().min(1).transform(unsafeAsBlockIdentifier);
298
- const portableBlockLookPinSchema = z.object({
299
- blockIdentifier: blockIdentifierSchema,
300
- blockKind: z.string().min(1),
301
- choice: z.object({
302
- kind: z.literal("user-pinned"),
303
- value: z.string().min(1).transform(asAppearancePresetId)
304
- }).strict()
305
- }).strict();
306
- const portableBoundaryPinSchema = z.object({
307
- previousBlockIdentifier: blockIdentifierSchema,
308
- nextBlockIdentifier: blockIdentifierSchema,
309
- choice: z.object({
310
- kind: z.literal("user-pinned"),
311
- value: pageDesignBoundaryTreatmentSchema
312
- }).strict()
313
- }).strict();
314
- function isRepeated(items, same) {
315
- return (item, index) => items.findIndex((other) => same(item, other)) !== index;
316
- }
317
- function refineUniquePins(design, ctx) {
318
- design.blockLookPins.filter(isRepeated(design.blockLookPins, (a, b) => a.blockIdentifier === b.blockIdentifier)).forEach((pin) => ctx.addIssue({
319
- code: "custom",
320
- path: ["blockLookPins"],
321
- message: `The design pins block "${pin.blockIdentifier}" more than once.`
322
- }));
323
- design.boundaryPins.filter(isRepeated(design.boundaryPins, (a, b) => a.previousBlockIdentifier === b.previousBlockIdentifier && a.nextBlockIdentifier === b.nextBlockIdentifier)).forEach((pin) => ctx.addIssue({
324
- code: "custom",
325
- path: ["boundaryPins"],
326
- message: `The design pins the break between "${pin.previousBlockIdentifier}" and "${pin.nextBlockIdentifier}" more than once.`
327
- }));
328
- }
329
- const portablePageDesignSchema = z.object({
330
- schemaVersion: z.literal(pageDesignIntentSchemaVersion),
331
- siteStyleId: z.string().min(1).transform(asSiteStyleId),
332
- pageFlowPresetId: z.string().min(1).transform(asPageFlowPresetId),
333
- blockLookPins: z.array(portableBlockLookPinSchema),
334
- boundaryPins: z.array(portableBoundaryPinSchema)
335
- }).strict().superRefine(refineUniquePins);
336
- const pageDesignRevisionTokenSchema = z.discriminatedUnion("kind", [
337
- z.object({ kind: z.literal("absent") }).strict(),
338
- z.object({
339
- kind: z.literal("draft"),
340
- contentVersionId: z.string().min(1),
341
- updatedAt: z.string().min(1)
342
- }).strict(),
343
- z.object({
344
- kind: z.literal("published"),
345
- contentVersionId: z.string().min(1),
346
- updatedAt: z.string().min(1)
347
- }).strict()
348
- ]);
349
- const sdkSavePageDesignBodySchema = z.object({
350
- design: portablePageDesignSchema,
351
- expectedRevision: pageDesignRevisionTokenSchema
352
- }).strict();
353
- const omittedPagePinSchema = z.discriminatedUnion("kind", [z.object({
354
- kind: z.literal("look-on-missing-block"),
355
- blockId: z.string().min(1)
356
- }), z.object({
357
- kind: z.literal("boundary-on-missing-block"),
358
- previousBlockId: z.string().min(1),
359
- nextBlockId: z.string().min(1)
360
- })]);
361
- const sdkGetPageDesignResponseSchema = z.discriminatedUnion("kind", [z.object({ kind: z.literal("absent") }), z.object({
362
- kind: z.literal("stored"),
363
- source: z.enum(["draft", "published"]),
364
- design: portablePageDesignSchema,
365
- revision: pageDesignRevisionTokenSchema,
366
- omitted: z.array(omittedPagePinSchema)
367
- })]);
368
- const sdkSavePageDesignResponseSchema = z.object({
369
- outcome: z.enum(["created", "updated"]),
370
- revision: pageDesignRevisionTokenSchema
371
- });
372
-
373
- //#endregion
374
- //#region ../api/src/sdkMediaResponseSchemas.ts
375
- const identifierSchema = z.string().min(1);
376
- const nullablePathSchema = z.string().nullable();
377
- const managementMediaAssetSchema = z.object({
378
- id: identifierSchema,
379
- siteId: identifierSchema,
380
- identifier: identifierSchema,
381
- storageBucket: identifierSchema,
382
- storagePath: identifierSchema,
383
- filename: identifierSchema,
384
- mimeType: identifierSchema,
385
- width: z.number().int().nonnegative().nullable(),
386
- height: z.number().int().nonnegative().nullable(),
387
- bytes: z.number().int().nonnegative().nullable(),
388
- checksumSha256: z.string().nullable(),
389
- createdAt: identifierSchema,
390
- updatedAt: identifierSchema
391
- });
392
- const blockLocationFields = {
393
- siteId: identifierSchema,
394
- blockId: identifierSchema,
395
- blockKind: identifierSchema,
396
- blockIdentifier: z.string().nullable(),
397
- stage: z.enum(["draft", "published"])
398
- };
399
- const newsletterUsageLocationSchemas = [z.object({
400
- kind: z.literal("newsletterTemplate"),
401
- siteId: identifierSchema,
402
- templateId: identifierSchema,
403
- templateName: z.string()
404
- }), z.object({
405
- kind: z.literal("newsletterCampaign"),
406
- siteId: identifierSchema,
407
- campaignId: identifierSchema,
408
- campaignName: z.string()
409
- })];
410
- const mediaUsageLocationSchema = z.discriminatedUnion("kind", [
411
- ...newsletterUsageLocationSchemas,
412
- z.object({
413
- kind: z.literal("pageBlock"),
414
- ...blockLocationFields,
415
- pageId: identifierSchema,
416
- pageTitle: z.string().nullable(),
417
- pageDraftPath: nullablePathSchema,
418
- pagePublishedPath: nullablePathSchema
419
- }),
420
- z.object({
421
- kind: z.literal("entrySlotBlock"),
422
- ...blockLocationFields,
423
- entryId: identifierSchema,
424
- entryTitle: z.string(),
425
- typeKey: identifierSchema,
426
- routeDraftPath: nullablePathSchema,
427
- routePublishedPath: nullablePathSchema,
428
- slotKey: identifierSchema
429
- }),
430
- z.object({
431
- kind: z.literal("surfaceSlotBlock"),
432
- ...blockLocationFields,
433
- pageSurfaceId: identifierSchema,
434
- surfaceSlotId: identifierSchema,
435
- slotKey: identifierSchema
436
- }),
437
- z.object({
438
- kind: z.literal("contentEntry"),
439
- siteId: identifierSchema,
440
- entryId: identifierSchema,
441
- entryTitle: z.string(),
442
- typeKey: identifierSchema,
443
- routeDraftPath: nullablePathSchema,
444
- routePublishedPath: nullablePathSchema,
445
- stage: z.enum(["draft", "published"])
446
- }),
447
- z.object({
448
- kind: z.literal("productFileDownload"),
449
- siteId: identifierSchema,
450
- productId: identifierSchema,
451
- productTitle: z.string()
452
- })
453
- ]);
454
- const mediaUsageItemSchema = z.object({
455
- assetId: identifierSchema,
456
- filename: z.string().nullable(),
457
- usedCount: z.number().int().nonnegative(),
458
- locations: z.array(mediaUsageLocationSchema)
459
- });
460
- const mediaDeleteWarningSchema = z.discriminatedUnion("kind", [z.object({
461
- kind: z.literal("ORPHAN_CLEANUP_FAILED"),
462
- message: z.string(),
463
- storageBucket: identifierSchema,
464
- storagePath: identifierSchema
465
- }), z.object({
466
- kind: z.literal("LABEL_CLEANUP_FAILED"),
467
- message: z.string()
468
- })]);
469
- const sdkMediaListResponseSchema = z.object({
470
- items: z.array(managementMediaAssetSchema),
471
- total: z.number().int().nonnegative(),
472
- limit: z.number().int().positive(),
473
- offset: z.number().int().nonnegative(),
474
- nextOffset: z.number().int().nonnegative().optional()
475
- });
476
- const sdkMediaDownloadResponseSchema = z.object({
477
- transferUrl: z.string().url(),
478
- asset: managementMediaAssetSchema
479
- });
480
- const sdkMediaDeletePreviewResponseSchema = z.discriminatedUnion("kind", [
481
- z.object({ kind: z.literal("missing") }),
482
- z.object({
483
- kind: z.literal("in_use"),
484
- usage: mediaUsageItemSchema
485
- }),
486
- z.object({
487
- kind: z.literal("deletable"),
488
- assetId: identifierSchema
489
- })
490
- ]);
491
- const sdkMediaDeleteApplyResponseSchema = z.discriminatedUnion("kind", [
492
- z.object({ kind: z.literal("missing") }),
493
- z.object({
494
- kind: z.literal("in_use"),
495
- usage: mediaUsageItemSchema
496
- }),
497
- z.object({ kind: z.literal("deleted") }),
498
- z.object({
499
- kind: z.literal("deleted_with_warnings"),
500
- warnings: z.tuple([mediaDeleteWarningSchema], mediaDeleteWarningSchema)
501
- })
502
- ]);
503
-
504
- //#endregion
505
- //#region ../blocks/src/system/types/link.ts
506
- const nullableString = z.union([z.string().min(1), z.null()]);
507
- const RESOLVED_INTERNAL_LINK_ENTITY_TYPES = [
508
- "page",
509
- "content",
510
- "pageSurface"
511
- ];
512
- function isResolvedInternalLinkEntityType(value) {
513
- return typeof value === "string" && RESOLVED_INTERNAL_LINK_ENTITY_TYPES.includes(value);
514
- }
515
- const resolvedInternalLinkKeys = [
516
- "entityId",
517
- "entityType",
518
- "href",
519
- "title",
520
- "typeLabel"
521
- ];
522
- const internalRouteOnlyLinkSchema = z.object({
523
- kind: z.literal("internal"),
524
- routeId: z.string().min(1)
525
- }).passthrough().superRefine((value, ctx) => {
526
- const hasResolvedMetadata = resolvedInternalLinkKeys.some((key) => value[key] != null);
527
- if (!hasResolvedMetadata) return;
528
- ctx.addIssue({
529
- code: "custom",
530
- message: "Internal links with resolved metadata must include entityId, entityType, href, title, and typeLabel"
531
- });
532
- });
533
- const internalResolvedLinkSchema = z.object({
534
- kind: z.literal("internal"),
535
- routeId: z.string().min(1),
536
- entityId: z.string().min(1),
537
- entityType: z.enum(RESOLVED_INTERNAL_LINK_ENTITY_TYPES),
538
- href: z.string().min(1),
539
- title: z.string().min(1),
540
- typeLabel: z.string().min(1),
541
- contentTypeKey: nullableString.optional(),
542
- contentTypeName: nullableString.optional(),
543
- updatedAt: nullableString.optional()
544
- }).passthrough();
545
- const internalLinkSchema = z.union([internalResolvedLinkSchema, internalRouteOnlyLinkSchema]);
546
- const externalLinkSchema$1 = z.object({
547
- kind: z.literal("external"),
548
- href: z.string().min(1)
549
- }).passthrough();
550
- const customLinkSchema$1 = z.object({
551
- kind: z.literal("url"),
552
- href: z.string().min(1)
553
- }).passthrough();
554
- const pageLinkSchema$1 = z.object({
555
- kind: z.literal("page"),
556
- identifier: z.string().min(1)
557
- }).passthrough();
558
- const entryLinkSchema$1 = z.object({
559
- kind: z.literal("entry"),
560
- contentType: z.string().min(1),
561
- identifier: z.string().min(1)
562
- }).passthrough();
563
- const linkSchema = z.union([
564
- internalLinkSchema,
565
- externalLinkSchema$1,
566
- customLinkSchema$1,
567
- pageLinkSchema$1,
568
- entryLinkSchema$1
569
- ]);
570
- function visitLinkValueReferences(value, visitor) {
571
- const parsed = linkSchema.safeParse(value);
572
- if (!parsed.success) {
573
- return;
574
- }
575
- const link$1 = parsed.data;
576
- switch (link$1.kind) {
577
- case "internal":
578
- visitor.routeIds.add(link$1.routeId);
579
- return;
580
- case "page":
581
- visitor.pageIdentifiers.add(link$1.identifier);
582
- return;
583
- case "entry":
584
- visitor.entryIdentifiers.add(link$1.contentType, link$1.identifier);
585
- return;
586
- case "external":
587
- case "url": return;
588
- }
589
- }
590
- function isInternalResolvedLinkValue(value) {
591
- return value?.kind === "internal" && "entityId" in value && "entityType" in value && "href" in value && "title" in value && "typeLabel" in value && typeof value.entityId === "string" && isResolvedInternalLinkEntityType(value.entityType) && typeof value.href === "string" && typeof value.title === "string" && typeof value.typeLabel === "string";
592
- }
593
-
594
- //#endregion
595
- //#region ../api/src/navigation/contracts.ts
596
- /**
597
- * Canonical navigation/link Zod contracts.
598
- *
599
- * Goal: a single source-of-truth for link shapes used across dashboard + SDK.
600
- * - Types live in `packages/api/src/navigation/types.ts`
601
- * - Schemas live here and are tied to types via `satisfies z.ZodType<...>`
602
- */
603
- const portalLinkPayloadSchema = z.object({
604
- kind: z.literal("portal"),
605
- path: z.string().trim().startsWith("/").optional(),
606
- labels: z.object({
607
- loggedOut: z.string().trim().min(1).optional(),
608
- loggedIn: z.string().trim().min(1).optional()
609
- }).optional()
610
- });
611
- const cartLinkLabelConfigSchema = z.discriminatedUnion("kind", [z.object({ kind: z.literal("icon-only") }), z.object({
612
- kind: z.literal("icon-with-label"),
613
- label: z.string().trim().min(1)
614
- })]);
615
- const cartLinkPayloadSchema = z.object({
616
- kind: z.literal("cart"),
617
- badge: z.enum(["show-count", "hidden"]),
618
- label: cartLinkLabelConfigSchema
619
- });
620
- const internalLinkPayloadSchema = z.object({
621
- kind: z.literal("internal"),
622
- routeId: z.string().uuid(),
623
- entityId: z.string().uuid(),
624
- entityType: z.enum(RESOLVED_INTERNAL_LINK_ENTITY_TYPES),
625
- href: z.string().min(1),
626
- title: z.string().min(1),
627
- typeLabel: z.string().min(1),
628
- contentTypeKey: z.string().nullable().optional(),
629
- contentTypeName: z.string().nullable().optional(),
630
- updatedAt: z.string().nullable().optional()
631
- }).passthrough();
632
- const externalLinkSchema = z.object({
633
- kind: z.literal("external"),
634
- href: z.string().url()
635
- });
636
- const customLinkSchema = z.object({
637
- kind: z.literal("url"),
638
- href: z.string().min(1)
639
- });
640
- const pageLinkSchema = z.object({
641
- kind: z.literal("page"),
642
- identifier: z.string().min(1)
643
- });
644
- const entryLinkSchema = z.object({
645
- kind: z.literal("entry"),
646
- contentType: z.string().min(1),
647
- identifier: z.string().min(1)
648
- });
649
- const linkPayloadSchema = z.discriminatedUnion("kind", [
650
- internalLinkPayloadSchema,
651
- pageLinkSchema,
652
- entryLinkSchema,
653
- externalLinkSchema,
654
- customLinkSchema,
655
- portalLinkPayloadSchema,
656
- cartLinkPayloadSchema
657
- ]);
658
- const navigationLinkInputSchemaOptions = [
659
- pageLinkSchema,
660
- entryLinkSchema,
661
- externalLinkSchema,
662
- customLinkSchema,
663
- portalLinkPayloadSchema,
664
- cartLinkPayloadSchema
665
- ];
666
- const navigationLinkInputSchema = z.discriminatedUnion("kind", navigationLinkInputSchemaOptions);
667
- const dashboardNavigationLinkSchema = linkPayloadSchema;
668
- const sdkNavigationLinkInputSchema = navigationLinkInputSchema;
669
-
670
- //#endregion
671
- //#region ../api/src/sdkThemeWire.ts
672
- /**
673
- * Validate a wire value without replacing it with the schema's parsed output.
674
- * The returned type is deliberately the schema input, not its canonical output.
675
- */
676
- function losslessValidatedSchema(schema) {
677
- return z.unknown().superRefine((value, ctx) => {
678
- const parsed = schema.safeParse(value);
679
- if (parsed.success) return;
680
- for (const issue of parsed.error.issues) {
681
- ctx.addIssue({
682
- code: "custom",
683
- path: issue.path,
684
- message: issue.message,
685
- params: { validationCode: issue.code }
686
- });
687
- }
688
- }).transform((value) => value);
689
- }
690
- const sdkThemeWireSchema = losslessValidatedSchema(themeSchema);
691
- /** Explicitly cross from the lossless wire model into Riverbank's canonical theme. */
692
- function normalizeSdkThemeWire(theme) {
693
- return themeSchema.parse(theme);
694
- }
695
-
696
- //#endregion
697
- //#region ../api/src/sdkPullResponseSchemas.ts
698
- const sdkIdentifierSchema$1 = z.string().min(1);
699
- const sdkTimestampSchema$2 = z.string().min(1);
700
- const sdkJsonObjectSchema$1 = z.record(z.string(), z.unknown());
701
- const sdkEntrySlotBlocksSchema = z.record(z.string(), z.array(z.object({
702
- identifier: z.string().nullable(),
703
- kind: sdkIdentifierSchema$1,
704
- content: sdkJsonObjectSchema$1
705
- })));
706
- const sdkPulledEntrySchema = z.object({
707
- identifier: sdkIdentifierSchema$1,
708
- title: z.string(),
709
- data: sdkJsonObjectSchema$1,
710
- status: z.enum(["draft", "published"]),
711
- hasUnpublishedChanges: z.boolean(),
712
- slug: z.string().optional(),
713
- blocks: sdkEntrySlotBlocksSchema.optional()
714
- });
715
- const sdkPullMetaSchema = z.object({ pulledAt: sdkTimestampSchema$2 });
716
- const sdkPullEntryTimestampsSchema = z.object({
717
- createdAt: sdkTimestampSchema$2,
718
- updatedAt: sdkTimestampSchema$2,
719
- publishedAt: sdkTimestampSchema$2.nullable()
720
- });
721
- const sdkPullPaginationSchema = z.object({
722
- page: z.number().int().nonnegative(),
723
- limit: z.number().int().positive(),
724
- total: z.number().int().nonnegative(),
725
- hasMore: z.boolean()
726
- });
727
- const sdkPullTruncationSchema = z.union([
728
- z.object({
729
- truncated: z.literal(false),
730
- returned: z.number().int().nonnegative()
731
- }).strict(),
732
- z.object({
733
- truncated: z.literal(true),
734
- returned: z.number().int().nonnegative(),
735
- droppedTypes: z.tuple([sdkIdentifierSchema$1], sdkIdentifierSchema$1),
736
- capPerType: z.number().int().positive(),
737
- capTotal: z.number().int().positive().optional()
738
- }),
739
- z.object({
740
- truncated: z.literal(true),
741
- returned: z.number().int().nonnegative(),
742
- droppedTypes: z.tuple([sdkIdentifierSchema$1], sdkIdentifierSchema$1),
743
- capPerType: z.number().int().positive().optional(),
744
- capTotal: z.number().int().positive()
745
- })
746
- ]);
747
- const sdkPullPageSchema = z.object({
748
- id: sdkIdentifierSchema$1,
749
- identifier: sdkIdentifierSchema$1,
750
- title: z.string(),
751
- path: z.string(),
752
- status: z.enum(["draft", "published"]),
753
- hasUnpublishedChanges: z.boolean(),
754
- updatedAt: sdkTimestampSchema$2,
755
- createdAt: sdkTimestampSchema$2,
756
- publishedAt: sdkTimestampSchema$2.nullable(),
757
- blocks: z.array(z.object({
758
- id: sdkIdentifierSchema$1,
759
- identifier: sdkIdentifierSchema$1,
760
- kind: sdkIdentifierSchema$1,
761
- position: z.number().int().nonnegative(),
762
- data: sdkJsonObjectSchema$1,
763
- createdAt: sdkTimestampSchema$2,
764
- updatedAt: sdkTimestampSchema$2
765
- }))
766
- });
767
- const sdkPullNavigationItemSchema = z.lazy(() => z.object({
768
- id: sdkIdentifierSchema$1,
769
- menuId: sdkIdentifierSchema$1,
770
- label: z.string(),
771
- link: linkPayloadSchema.nullable(),
772
- isCta: z.boolean().optional(),
773
- position: z.number().int().nonnegative(),
774
- parentId: sdkIdentifierSchema$1.nullable(),
775
- children: z.array(sdkPullNavigationItemSchema).optional(),
776
- resolvedIdentifier: z.string().nullable().optional(),
777
- resolvedContentType: z.string().nullable().optional()
778
- }));
779
- const sdkPullNavigationMenuSchema = z.object({
780
- id: sdkIdentifierSchema$1,
781
- siteId: sdkIdentifierSchema$1,
782
- location: z.enum([
783
- "main",
784
- "footer",
785
- "legal"
786
- ]),
787
- items: z.array(sdkPullNavigationItemSchema),
788
- createdAt: sdkTimestampSchema$2,
789
- updatedAt: sdkTimestampSchema$2
790
- });
791
- const siteTitleSchema = z.string().min(1, "Site title is required");
792
- const sdkSeoDefaultsSchema = z.object({
793
- title: z.string().optional(),
794
- description: z.string().optional(),
795
- ogImage: z.string().optional(),
796
- allowIndexingInProduction: z.boolean().optional()
797
- });
798
- const sdkSettingsUpdateSchema = z.object({
799
- siteTitle: siteTitleSchema.optional(),
800
- homepageId: z.string().nullable().optional(),
801
- seoDefaults: sdkSeoDefaultsSchema.nullable().optional(),
802
- logoIdentifier: z.string().min(1).nullable().optional(),
803
- logoDarkBackgroundIdentifier: z.string().min(1).nullable().optional(),
804
- faviconIdentifier: z.string().min(1).nullable().optional()
805
- }).strict();
806
- const sdkPullSettingsSchema = sdkSettingsUpdateSchema.extend({
807
- homepageId: z.string().nullable(),
808
- seoDefaults: sdkSeoDefaultsSchema.nullable(),
809
- verticalConfig: sdkConfiguredBookingVerticalConfigSchema.optional()
810
- });
811
- /** A form's schema as the SDK pulls it; the push route extends it. */
812
- const sdkPullFormSchema = z.object({
813
- version: z.string().optional(),
814
- fields: z.array(z.object({
815
- id: sdkIdentifierSchema$1,
816
- label: z.string(),
817
- type: z.string(),
818
- required: z.boolean().optional(),
819
- placeholder: z.string().optional(),
820
- helpText: z.string().optional(),
821
- layout: z.object({ width: z.enum(["full", "half"]).optional() }).optional(),
822
- options: z.array(z.object({
823
- value: z.string(),
824
- label: z.string()
825
- })).optional(),
826
- multiple: z.boolean().optional(),
827
- minLength: z.number().optional(),
828
- maxLength: z.number().optional(),
829
- pattern: z.string().optional(),
830
- min: z.number().optional(),
831
- max: z.number().optional()
832
- }))
833
- });
834
- const sdkPullFormSchemaWithId = z.object({
835
- id: sdkIdentifierSchema$1,
836
- slug: sdkIdentifierSchema$1,
837
- name: z.string(),
838
- schema: sdkPullFormSchema,
839
- settings: sdkJsonObjectSchema$1,
840
- updatedAt: sdkTimestampSchema$2,
841
- createdAt: sdkTimestampSchema$2
842
- });
843
- const sdkPulledFormSchema = sdkPullFormSchemaWithId.omit({ id: true });
844
- const sdkPullVenueSchema = z.object({
845
- slug: sdkIdentifierSchema$1,
846
- title: z.string(),
847
- description: z.string().nullable(),
848
- timezone: z.string().nullable(),
849
- capacity: z.number().int().nonnegative().nullable(),
850
- address: z.object({
851
- line1: z.string().optional(),
852
- line2: z.string().optional(),
853
- city: z.string().optional(),
854
- region: z.string().optional(),
855
- postcode: z.string().optional(),
856
- country: z.string().optional()
857
- }),
858
- location: z.object({
859
- lat: z.number(),
860
- lng: z.number()
861
- }).partial().nullable(),
862
- mapEmbed: z.string().nullable(),
863
- updatedAt: sdkTimestampSchema$2,
864
- createdAt: sdkTimestampSchema$2
865
- });
866
- const sdkPullEventCategorySchema = z.object({
867
- identifier: sdkIdentifierSchema$1,
868
- name: z.string(),
869
- description: z.string().nullable(),
870
- color: z.string().nullable(),
871
- sortOrder: z.number().int(),
872
- updatedAt: sdkTimestampSchema$2,
873
- createdAt: sdkTimestampSchema$2
874
- });
875
- const sdkEventScheduleSchema = z.object({
876
- recurrenceRule: z.string().nullable(),
877
- startDate: z.string().nullable().optional(),
878
- timezone: z.string().nullable().optional(),
879
- defaultStartTime: z.string(),
880
- durationMinutes: z.number().int().nonnegative()
881
- });
882
- const sdkPullEventSchema = z.object({
883
- identifier: sdkIdentifierSchema$1,
884
- courseBooking: sdkCourseBookingSettingsSchema.optional(),
885
- administrativeStatus: z.enum(["active", "archived"]),
886
- title: z.string(),
887
- slug: sdkIdentifierSchema$1,
888
- path: z.string().nullable(),
889
- preset: z.enum(EVENT_PRESET_VALUES),
890
- categoryRef: z.string().nullable(),
891
- runRefs: z.tuple([sdkIdentifierSchema$1], sdkIdentifierSchema$1),
892
- content: sdkJsonObjectSchema$1,
893
- updatedAt: sdkTimestampSchema$2,
894
- createdAt: sdkTimestampSchema$2,
895
- contentUpdatedAt: sdkTimestampSchema$2.nullable(),
896
- blocks: sdkEntrySlotBlocksSchema.optional()
897
- });
898
- const sdkPullEventRunSchema = z.object({
899
- identifier: sdkIdentifierSchema$1,
900
- eventRef: sdkIdentifierSchema$1,
901
- name: z.string().nullable(),
902
- status: z.enum([
903
- "draft",
904
- "active",
905
- "archived",
906
- "cancelled",
907
- "completed"
908
- ]),
909
- visibility: eventVisibilitySchema.optional(),
910
- bookingScope: z.enum(["single_occurrence", "whole_series"]),
911
- occurrences: z.array(sdkEventOccurrenceInputSchema),
912
- capacity: z.number().int().nonnegative().nullable(),
913
- bookingOffer: eventBookingOfferIntentSchema,
914
- venueRef: z.string().nullable(),
915
- formRef: z.string().nullable(),
916
- settings: sdkJsonObjectSchema$1,
917
- presentation: z.unknown().optional(),
918
- tags: z.array(z.string()),
919
- updatedAt: sdkTimestampSchema$2,
920
- createdAt: sdkTimestampSchema$2
921
- });
922
- const sdkPullEntriesResponseSchema = z.object({
923
- entries: z.array(sdkPulledEntrySchema.extend({ contentType: sdkIdentifierSchema$1 })),
924
- meta: sdkPullMetaSchema.extend({
925
- entries: z.record(z.string(), sdkPullEntryTimestampsSchema),
926
- truncation: sdkPullTruncationSchema
927
- }),
928
- pagination: sdkPullPaginationSchema
929
- });
930
- const sdkPullEntriesByTypeResponseSchema = z.object({
931
- contentType: sdkIdentifierSchema$1,
932
- entries: z.array(sdkPulledEntrySchema),
933
- meta: sdkPullMetaSchema.extend({ entries: z.record(z.string(), sdkPullEntryTimestampsSchema) }),
934
- pagination: sdkPullPaginationSchema
935
- });
936
- const sdkPullEntriesAllResponseSchema = z.object({
937
- entries: z.record(z.string(), z.array(sdkPulledEntrySchema)),
938
- meta: sdkPullMetaSchema.extend({
939
- totalEntries: z.number().int().nonnegative(),
940
- contentTypes: z.array(sdkIdentifierSchema$1),
941
- entries: z.record(z.string(), sdkPullEntryTimestampsSchema),
942
- truncation: sdkPullTruncationSchema
943
- })
944
- });
945
- const sdkPullPagesResponseSchema = z.object({
946
- pages: z.array(sdkPullPageSchema),
947
- meta: sdkPullMetaSchema
948
- });
949
- const sdkPullNavigationResponseSchema = z.object({
950
- menus: z.array(sdkPullNavigationMenuSchema),
951
- meta: sdkPullMetaSchema
952
- });
953
- const sdkPullSettingsResponseSchema = z.object({
954
- settings: sdkPullSettingsSchema,
955
- warnings: z.array(z.union([z.string(), sdkPullSettingsWarningSchema])).optional(),
956
- meta: sdkPullMetaSchema.extend({ updatedAt: sdkTimestampSchema$2.nullable() })
957
- });
958
- const sdkPullFormsResponseSchema = z.object({
959
- forms: z.array(sdkPulledFormSchema),
960
- meta: sdkPullMetaSchema
961
- });
962
- const sdkPullThemeResponseSchema = z.object({
963
- theme: sdkThemeWireSchema.nullable(),
964
- themeId: sdkIdentifierSchema$1.nullable(),
965
- selectionId: sdkIdentifierSchema$1.nullable(),
966
- selectionVersion: z.number().int().nonnegative(),
967
- meta: sdkPullMetaSchema.extend({ updatedAt: sdkTimestampSchema$2.nullable() })
968
- }).passthrough();
969
- const sdkFooterContentSchema = z.object({
970
- composition: z.unknown().transform((value, ctx) => {
971
- const parsed = parseFooterCompositionV1(value);
972
- if (parsed.ok) return parsed.value;
973
- ctx.addIssue({
974
- code: "custom",
975
- message: "Expected a canonical footer composition"
976
- });
977
- return z.NEVER;
978
- }),
979
- revision: z.number().int().nonnegative()
980
- }).passthrough();
981
- const sdkPullFooterResponseSchema = z.object({
982
- footer: sdkFooterContentSchema.nullable(),
983
- meta: sdkPullMetaSchema.extend({ updatedAt: sdkTimestampSchema$2.nullable() })
984
- });
985
- const sdkPullVenuesResponseSchema = z.object({
986
- venues: z.array(sdkPullVenueSchema),
987
- meta: sdkPullMetaSchema
988
- });
989
- const sdkPullEventCategoriesResponseSchema = z.object({
990
- eventCategories: z.array(sdkPullEventCategorySchema),
991
- meta: sdkPullMetaSchema
992
- });
993
- const sdkPullEventsResponseSchema = z.object({
994
- events: z.array(sdkPullEventSchema),
995
- eventRuns: z.array(sdkPullEventRunSchema),
996
- meta: sdkPullMetaSchema
997
- });
998
-
999
- //#endregion
1000
- //#region ../api/src/bookingSettingsSections.ts
1001
- const BOOKING_SETTINGS_SECTION_LABELS = {
1002
- basics: "Booking basics",
1003
- payments: "Payments",
1004
- events: "Events",
1005
- appointments: "Appointments",
1006
- messages: "Messages"
1007
- };
1008
- const BOOKING_SETTINGS_SECTION_KEYS = {
1009
- basics: [
1010
- "modules",
1011
- "verticalConfig",
1012
- "eventTypePresets",
1013
- "personRecordMode",
1014
- "defaultTimezone",
1015
- "calendarDisplay",
1016
- "icsFeedsEnabled",
1017
- "waitlists",
1018
- "teamMemberProfilePages"
1019
- ],
1020
- payments: [
1021
- "paymentDefaults",
1022
- "currency",
1023
- "refundsIssueCredit"
1024
- ],
1025
- events: [
1026
- "defaultVenueId",
1027
- "eventReminderHours",
1028
- "eventReminderOverrides",
1029
- "eventScheduleWindowMonths",
1030
- "eventOrganizerNotifications",
1031
- "eventOrganizerEmail",
1032
- "eventTicketingDefaultEnabled",
1033
- "collectAllAttendeeDetails",
1034
- "eventTypeBookingDefaults",
1035
- "eventBookingPolicies",
1036
- "defaultEventImage",
1037
- "eventDestinationPolicy"
1038
- ],
1039
- appointments: [
1040
- "appointmentReminderHours",
1041
- "appointmentReminderOverrides",
1042
- "appointmentRescheduleHours",
1043
- "appointmentDefaultMinLeadMinutes",
1044
- "appointmentDefaultMaxBookingNoticeMinutes",
1045
- "appointmentBookingPolicies"
1046
- ],
1047
- messages: [
1048
- "remindersEnabled",
1049
- "notificationSettings",
1050
- "appointmentStaffReminder",
1051
- "appointmentStaffChangeNotifications",
1052
- "eventFeedback",
1053
- "appointmentFollowUp"
1054
- ]
1055
- };
1056
-
1057
- //#endregion
1058
- //#region ../api/src/event-destination-policy.ts
1059
- const eventDestinationModeValues = ["cms-managed", "system-managed"];
1060
- const eventDestinationPresetValues = EVENT_PRESET_VALUES;
1061
- const DEFAULT_EVENT_DESTINATION_POLICY = {
1062
- defaultMode: "cms-managed",
1063
- presetOverrides: {}
1064
- };
1065
- function isEventDestinationMode(value) {
1066
- return eventDestinationModeValues.some((mode) => mode === value);
1067
- }
1068
- function resolveEventDestinationMode(input) {
1069
- const policy = input.policy ?? DEFAULT_EVENT_DESTINATION_POLICY;
1070
- if (input.preset) {
1071
- const presetOverride = policy.presetOverrides[input.preset];
1072
- if (presetOverride) return presetOverride;
1073
- }
1074
- if (input.publicExperience === "hosted_booking") return "system-managed";
1075
- return policy.defaultMode;
1076
- }
1077
- function macadamiaDefaultEventDestinationPolicy(selectedPresets) {
1078
- const presetOverrides = {};
1079
- for (const preset of selectedPresets) {
1080
- presetOverrides[preset] = preset === "class" ? "system-managed" : "cms-managed";
1081
- }
1082
- return {
1083
- defaultMode: "cms-managed",
1084
- presetOverrides
1085
- };
1086
- }
1087
-
1088
- //#endregion
1089
- //#region ../api/src/bookingConfig.ts
1090
- /** Every option kind; the checkout routes validate the requested kind against it. */
1091
- const DIRECT_CHECKOUT_COLLECTION_OPTION_KINDS = [
1092
- "upfront",
1093
- "deferred_manual",
1094
- "instalment_plan",
1095
- "flexible_balance"
1096
- ];
1097
- const eventOccurrenceRestorationSummarySchema = z.object({
1098
- restoredOccurrenceIds: z.array(z.string().uuid()),
1099
- excludedBookings: z.array(z.object({
1100
- attendeeId: z.string().uuid(),
1101
- occurrenceId: z.string().uuid(),
1102
- customerId: z.string().uuid(),
1103
- customerName: z.string().nullable(),
1104
- customerEmail: z.string().nullable()
1105
- }))
1106
- });
1107
- const DEFAULT_BOOKING_NOTIFICATION_SEND_RULE = {
1108
- kind: "send",
1109
- sendToAssignedStaff: true,
1110
- extraEmail: null,
1111
- sendToSiteOwnerIfNoStaff: true
1112
- };
1113
- const DEFAULT_BOOKING_NOTIFICATION_SETTINGS = {
1114
- state: "configured",
1115
- defaultRule: DEFAULT_BOOKING_NOTIFICATION_SEND_RULE,
1116
- overrides: {}
1117
- };
1118
- const DEFAULT_APPOINTMENT_STAFF_DIGEST_LOCAL_SEND_TIME = "07:00";
1119
- const BOOKING_SETTINGS_SECTIONS = [
1120
- "basics",
1121
- "payments",
1122
- "events",
1123
- "appointments",
1124
- "messages"
1125
- ];
1126
- const unownedBookingSettingsKeys = {};
1127
- void unownedBookingSettingsKeys;
1128
- function pickBookingSettingsSection(settings, section) {
1129
- const entries = BOOKING_SETTINGS_SECTION_KEYS[section].map((key) => [key, settings[key]]);
1130
- return Object.fromEntries(entries);
1131
- }
1132
- function completeBookingSettingsForSectionUpdate(settings) {
1133
- return {
1134
- ...settings,
1135
- eventTypePresets: settings.eventTypePresets ?? DEFAULT_EVENT_TYPE_PRESETS,
1136
- verticalConfig: settings.verticalConfig ?? {
1137
- verticalId: null,
1138
- selectedPresetIds: []
1139
- },
1140
- teamMemberProfilePages: settings.teamMemberProfilePages ?? { policy: "selected" },
1141
- eventDestinationPolicy: settings.eventDestinationPolicy ?? {
1142
- defaultMode: "cms-managed",
1143
- presetOverrides: {}
1144
- },
1145
- eventTypeBookingDefaults: settings.eventTypeBookingDefaults ?? {}
1146
- };
1147
- }
1148
- function createBookingSettingsSectionUpdate(settings, section) {
1149
- const completeSettings = completeBookingSettingsForSectionUpdate(settings);
1150
- switch (section) {
1151
- case "basics": return {
1152
- section,
1153
- settings: pickBookingSettingsSection(completeSettings, section)
1154
- };
1155
- case "payments": return {
1156
- section,
1157
- settings: pickBookingSettingsSection(completeSettings, section)
1158
- };
1159
- case "events": return {
1160
- section,
1161
- settings: pickBookingSettingsSection(completeSettings, section)
1162
- };
1163
- case "appointments": return {
1164
- section,
1165
- settings: pickBookingSettingsSection(completeSettings, section)
1166
- };
1167
- case "messages": return {
1168
- section,
1169
- settings: pickBookingSettingsSection(completeSettings, section)
1170
- };
1171
- }
1172
- }
1173
- function createBookingSettingsUpdateRequest(settings, sections = BOOKING_SETTINGS_SECTIONS) {
1174
- return { updates: sections.map((section) => createBookingSettingsSectionUpdate(settings, section)) };
1175
- }
1176
- function applyBookingSettingsSectionUpdate(settings, update) {
1177
- return {
1178
- ...settings,
1179
- ...update.settings
1180
- };
1181
- }
1182
- function applyBookingSettingsSectionUpdates(settings, updates) {
1183
- return updates.reduce(applyBookingSettingsSectionUpdate, settings);
1184
- }
1185
- /** Normalize optional wire defaults once before projecting operational Event policy. */
1186
- function resolveBookingSettingsEventCatalogue(settings) {
1187
- return resolveEventCatalogue({
1188
- verticalConfig: settings.verticalConfig,
1189
- eventTypePresets: settings.eventTypePresets ?? DEFAULT_EVENT_TYPE_PRESETS
1190
- });
1191
- }
1192
-
1193
- //#endregion
1194
- //#region ../api/src/sdkManagementResponseSchemas.ts
1195
- const sdkIdentifierSchema = z.string().min(1);
1196
- const sdkTimestampSchema$1 = z.string().min(1);
1197
- const sdkJsonObjectSchema = z.record(z.string(), z.unknown());
1198
- const sdkManagementEntrySchema = z.object({
1199
- id: sdkIdentifierSchema,
1200
- identifier: sdkIdentifierSchema,
1201
- contentType: sdkIdentifierSchema,
1202
- slug: z.string().optional(),
1203
- title: z.string(),
1204
- data: sdkJsonObjectSchema,
1205
- publishedData: sdkJsonObjectSchema.nullable().optional(),
1206
- status: z.enum(["draft", "published"]),
1207
- hasUnpublishedChanges: z.boolean(),
1208
- createdAt: sdkTimestampSchema$1,
1209
- updatedAt: sdkTimestampSchema$1,
1210
- publishedAt: sdkTimestampSchema$1.nullable(),
1211
- manualOrder: z.number(),
1212
- blocks: sdkEntrySlotBlocksSchema.optional()
1213
- });
1214
- const sdkManagementEntryWithoutPublishedDataSchema = sdkManagementEntrySchema.omit({ publishedData: true });
1215
- const sdkManagementPageSchema = z.object({
1216
- id: sdkIdentifierSchema,
1217
- identifier: sdkIdentifierSchema,
1218
- title: z.string(),
1219
- path: z.string(),
1220
- status: z.enum(["draft", "published"]),
1221
- hasUnpublishedChanges: z.boolean(),
1222
- createdAt: sdkTimestampSchema$1,
1223
- updatedAt: sdkTimestampSchema$1,
1224
- publishedAt: sdkTimestampSchema$1.nullable()
1225
- });
1226
- const sdkManagementBlockSchema = z.object({
1227
- id: sdkIdentifierSchema,
1228
- identifier: sdkIdentifierSchema,
1229
- kind: sdkIdentifierSchema,
1230
- position: z.number().int().nonnegative(),
1231
- data: sdkJsonObjectSchema,
1232
- createdAt: sdkTimestampSchema$1,
1233
- updatedAt: sdkTimestampSchema$1
1234
- });
1235
- const sdkManagementNavigationMenuSchema = sdkPullNavigationMenuSchema.omit({ siteId: true });
1236
- const sdkPublicationOutcomeSchema = (code) => z.union([z.object({ published: z.literal(true) }), z.object({
1237
- published: z.literal(false),
1238
- publishError: z.object({
1239
- code: z.literal(code),
1240
- message: z.string().min(1)
1241
- })
1242
- })]);
1243
- const sdkUpsertEntryDraftResponseSchema = sdkCommandApplicationResultSchema(sdkManagementEntryWithoutPublishedDataSchema).extend({
1244
- saved: z.never().optional(),
1245
- publication: z.never().optional()
1246
- });
1247
- const sdkUpsertEntryPublishResponseSchema = sdkCommandApplicationResultSchema(sdkManagementEntryWithoutPublishedDataSchema).extend({
1248
- saved: z.literal(true),
1249
- publication: sdkPublicationOutcomeSchema("PUBLISH_ENTRY_FAILED")
1250
- });
1251
- const sdkUpsertPageDraftResponseSchema = sdkCommandApplicationResultSchema(sdkManagementPageSchema).extend({
1252
- saved: z.never().optional(),
1253
- publication: z.never().optional()
1254
- });
1255
- const sdkUpsertPagePublishResponseSchema = sdkCommandApplicationResultSchema(sdkManagementPageSchema).extend({
1256
- saved: z.literal(true),
1257
- publication: sdkPublicationOutcomeSchema("PUBLISH_PAGE_FAILED")
1258
- });
1259
- const sdkUpsertPageResponseSchema = z.union([sdkUpsertPageDraftResponseSchema, sdkUpsertPagePublishResponseSchema]);
1260
- const sdkUpsertEntryResponseSchema = z.union([sdkUpsertEntryDraftResponseSchema, sdkUpsertEntryPublishResponseSchema]);
1261
- const sdkDeleteResponseSchema = sdkCommandApplicationResultSchema(z.object({ deleted: z.literal(true) }));
1262
- const sdkDeleteBlockResponseSchema = sdkDeleteResponseSchema.extend({ designSyncWarning: pageBlockDesignSyncWarningSchema.optional() });
1263
- const sdkBlockContentWarningSchema = z.object({
1264
- field: z.string(),
1265
- code: z.literal("theme-unavailable-variant"),
1266
- message: z.string(),
1267
- value: z.string()
1268
- });
1269
- const sdkUpsertBlockResponseSchema = sdkCommandApplicationResultSchema(sdkManagementBlockSchema).extend({
1270
- warnings: z.array(sdkBlockContentWarningSchema).optional(),
1271
- appearance: sdkBlockAppearanceOutcomeSchema.optional(),
1272
- designSyncWarning: pageBlockDesignSyncWarningSchema.optional()
1273
- });
1274
- const sdkPatchPageBlockResponseSchema = z.object({
1275
- block: z.object({
1276
- id: sdkIdentifierSchema,
1277
- identifier: sdkIdentifierSchema,
1278
- kind: sdkIdentifierSchema,
1279
- position: z.number().int().nonnegative(),
1280
- data: sdkJsonObjectSchema,
1281
- contentHash: sdkIdentifierSchema
1282
- }),
1283
- warnings: z.array(sdkBlockContentWarningSchema).optional()
1284
- });
1285
- const sdkUpsertNavigationMenuResponseSchema = sdkCommandApplicationResultSchema(sdkManagementNavigationMenuSchema);
1286
- const sdkUpsertSettingsResponseSchema = sdkCommandApplicationResultSchema(sdkPullSettingsSchema);
1287
- const sdkUpsertFormResponseSchema = sdkCommandApplicationResultSchema(sdkPullFormSchemaWithId);
1288
- const sdkUpsertVenueResponseSchema = sdkCommandApplicationResultSchema(sdkPullVenueSchema.extend({ id: sdkIdentifierSchema }));
1289
- const sdkUpsertEventCategoryResponseSchema = sdkCommandApplicationResultSchema(sdkPullEventCategorySchema.extend({ id: sdkIdentifierSchema }));
1290
- const sdkManagementEventSchema = sdkPullEventSchema.extend({
1291
- id: sdkIdentifierSchema.transform(unsafeAsEventOfferingId),
1292
- pageState: z.enum(["online", "offline"]).optional()
1293
- });
1294
- const sdkManagementEventRunSchema = sdkPullEventRunSchema.extend({
1295
- id: sdkIdentifierSchema.transform(unsafeAsEventSeriesId),
1296
- eventId: sdkIdentifierSchema.transform(unsafeAsEventOfferingId),
1297
- schedule: sdkEventScheduleSchema
1298
- });
1299
- const sdkManagementEventGraphSchema = z.object({
1300
- event: sdkManagementEventSchema,
1301
- runs: z.tuple([sdkManagementEventRunSchema], sdkManagementEventRunSchema)
1302
- });
1303
- const sdkListEventsResponseSchema = z.object({
1304
- events: z.array(sdkManagementEventSchema),
1305
- eventRuns: z.array(sdkManagementEventRunSchema)
1306
- });
1307
- const sdkGetEventResponseSchema = sdkManagementEventGraphSchema;
1308
- const sdkEventSeriesIdSchema = sdkIdentifierSchema.transform(unsafeAsEventSeriesId);
1309
- /**
1310
- * The lifecycle outcome for `POST /sdk/{siteId}/events/{identifier}/lifecycle`.
1311
- * `applied` always changed something: at least one run, or the Event page.
1312
- */
1313
- const sdkTakeEventOfflineResponseSchema = z.union([
1314
- z.object({
1315
- kind: z.literal("applied"),
1316
- changedRunIds: z.tuple([sdkEventSeriesIdSchema], sdkEventSeriesIdSchema),
1317
- pageTakenOffline: z.boolean()
1318
- }),
1319
- z.object({
1320
- kind: z.literal("applied"),
1321
- changedRunIds: z.tuple([]),
1322
- pageTakenOffline: z.literal(true)
1323
- }),
1324
- z.object({ kind: z.literal("already-offline") })
1325
- ]);
1326
- const sdkUpsertEventResponseSchema = z.union([sdkCommandApplicationResultSchema(sdkManagementEventGraphSchema).extend({
1327
- cancellationResult: businessCancellationResultSchema.optional(),
1328
- restoration: eventOccurrenceRestorationSummarySchema.optional()
1329
- }), z.object({
1330
- kind: z.literal("accepted_unavailable"),
1331
- id: sdkEventSeriesIdSchema,
1332
- effect: z.object({ kind: z.literal("updated") }),
1333
- cancellationResult: businessCancellationResultSchema
1334
- })]);
1335
- const sdkUpsertThemeResponseSchema = sdkCommandApplicationResultSchema(z.object({
1336
- themeId: sdkIdentifierSchema,
1337
- selectionId: sdkIdentifierSchema,
1338
- savedAt: sdkTimestampSchema$1
1339
- }));
1340
- const sdkApplySiteStyleSelectionResponseSchema = sdkCommandApplicationResultSchema(z.object({
1341
- success: z.boolean(),
1342
- themeId: sdkIdentifierSchema,
1343
- selectionId: sdkIdentifierSchema,
1344
- selectionVersion: z.number().int().nonnegative(),
1345
- selectionUpdatedAt: sdkTimestampSchema$1.nullable(),
1346
- savedAt: sdkTimestampSchema$1,
1347
- theme: themeSchema,
1348
- siteStyleId: sdkIdentifierSchema,
1349
- buttonStyleSelection: buttonStyleSelectionSchema,
1350
- headerLayoutId: sdkIdentifierSchema,
1351
- headerStyleId: sdkIdentifierSchema,
1352
- footerLookId: sdkIdentifierSchema,
1353
- footerPreferences: footerPreferencesSchema
1354
- }));
1355
- const sdkUpsertFooterResponseSchema = sdkCommandApplicationResultSchema(sdkFooterContentSchema.nullable());
1356
-
1357
- //#endregion
1358
- //#region ../api/src/sdkResponseSchemas.ts
1359
- const sdkTimestampSchema = z.string().min(1);
1360
- const sdkConfigureBookingVerticalResponseSchema = sdkCommandApplicationResultSchema(z.object({
1361
- verticalConfig: sdkConfiguredBookingVerticalConfigSchema,
1362
- revision: sdkTimestampSchema.nullable()
1363
- })).extend({ warnings: z.array(sdkBookingSettingsWarningSchema).optional() });
1364
-
1365
- //#endregion
1366
- //#region ../management-client/src/responseValidation.ts
1367
- function isRecord$3(value) {
1368
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
1369
- }
1370
- function pullSchemaForRequest(path$2) {
1371
- switch (path$2) {
1372
- case "/pull/entries/all": return sdkPullEntriesAllResponseSchema;
1373
- case "/pull/entries": return sdkPullEntriesResponseSchema;
1374
- case "/pull/pages": return sdkPullPagesResponseSchema;
1375
- case "/pull/navigation": return sdkPullNavigationResponseSchema;
1376
- case "/pull/settings": return sdkPullSettingsResponseSchema;
1377
- case "/pull/forms": return sdkPullFormsResponseSchema;
1378
- case "/pull/footer": return sdkPullFooterResponseSchema;
1379
- case "/pull/venues": return sdkPullVenuesResponseSchema;
1380
- case "/pull/event-categories": return sdkPullEventCategoriesResponseSchema;
1381
- case "/pull/events": return sdkPullEventsResponseSchema;
1382
- default: return /^\/pull\/entries\/[^/]+$/.test(path$2) ? sdkPullEntriesByTypeResponseSchema : undefined;
1383
- }
1384
- }
1385
- function rootMutationSchema(method, path$2, body) {
1386
- if (method === "POST") {
1387
- switch (path$2) {
1388
- case "/entries": return isRecord$3(body) && body.publication === "publish" ? sdkUpsertEntryPublishResponseSchema : sdkUpsertEntryDraftResponseSchema;
1389
- case "/pages": return isRecord$3(body) && body.publication === "publish" ? sdkUpsertPagePublishResponseSchema : sdkUpsertPageDraftResponseSchema;
1390
- case "/navigation": return sdkUpsertNavigationMenuResponseSchema;
1391
- case "/forms": return sdkUpsertFormResponseSchema;
1392
- case "/venues": return sdkUpsertVenueResponseSchema;
1393
- case "/event-categories": return sdkUpsertEventCategoryResponseSchema;
1394
- case "/events": return sdkUpsertEventResponseSchema;
1395
- case "/footer": return sdkUpsertFooterResponseSchema;
1396
- }
1397
- }
1398
- if (method === "PATCH" && path$2 === "/settings") return sdkUpsertSettingsResponseSchema;
1399
- if (method === "POST" && path$2 === "/settings/booking/vertical") return sdkConfigureBookingVerticalResponseSchema;
1400
- return undefined;
1401
- }
1402
- function isDeleteMutationPath(path$2) {
1403
- return [
1404
- /^\/entries\/[^/]+\/[^/]+$/,
1405
- /^\/pages\/[^/]+$/,
1406
- /^\/pages\/[^/]+\/blocks\/[^/]+$/,
1407
- /^\/navigation\/[^/]+$/,
1408
- /^\/forms\/[^/]+$/,
1409
- /^\/venues\/[^/]+$/,
1410
- /^\/event-categories\/[^/]+$/,
1411
- /^\/events\/[^/]+$/
1412
- ].some((pattern) => pattern.test(path$2));
1413
- }
1414
- function blockMutationSchema(method, path$2) {
1415
- if (method === "POST" && /^\/pages\/[^/]+\/blocks$/.test(path$2)) return sdkUpsertBlockResponseSchema;
1416
- if (method === "POST" && /^\/pages\/[^/]+\/blocks\/reorder$/.test(path$2)) return sdkReorderResponseSchema;
1417
- if (method === "PATCH" && /^\/pages\/[^/]+\/blocks\/[^/]+$/.test(path$2)) return sdkPatchPageBlockResponseSchema;
1418
- if (method === "DELETE" && /^\/pages\/[^/]+\/blocks\/[^/]+$/.test(path$2)) return sdkDeleteBlockResponseSchema;
1419
- return undefined;
1420
- }
1421
- function nestedMutationSchema(method, path$2) {
1422
- const blockSchema = blockMutationSchema(method, path$2);
1423
- if (blockSchema) return blockSchema;
1424
- if (method === "POST" && /^\/entries\/[^/]+\/[^/]+\/(publish|unpublish)$/.test(path$2)) {
1425
- return sdkCommandApplicationResultSchema(sdkUpsertEntryDraftResponseSchema.shape.value);
1426
- }
1427
- if (method === "POST" && /^\/pages\/[^/]+\/(publish|unpublish)$/.test(path$2)) {
1428
- return sdkCommandApplicationResultSchema(sdkUpsertPageDraftResponseSchema.shape.value);
1429
- }
1430
- if (method === "DELETE" && isDeleteMutationPath(path$2)) return sdkDeleteResponseSchema;
1431
- return undefined;
1432
- }
1433
- function occurrenceResponseSchema(method, path$2) {
1434
- const collection = /^\/events\/[^/]+\/runs\/[^/]+\/occurrences$/;
1435
- const member = /^\/events\/[^/]+\/runs\/[^/]+\/occurrences\/[^/]+$/;
1436
- if (method === "GET" && collection.test(path$2)) return sdkListEventOccurrencesResponseSchema;
1437
- if (method === "POST" && collection.test(path$2)) return sdkEventOccurrenceResponseSchema;
1438
- if (method === "PATCH" && member.test(path$2)) return sdkEventOccurrenceResponseSchema;
1439
- if (method === "DELETE" && member.test(path$2)) return sdkDeleteEventOccurrenceResponseSchema;
1440
- return method === "POST" && /^\/events\/[^/]+\/runs\/[^/]+\/occurrences\/[^/]+\/cancel$/.test(path$2) ? sdkEventOccurrenceResponseSchema : undefined;
1441
- }
1442
- function mediaResponseSchema(method, path$2) {
1443
- if (method === "GET" && path$2 === "/media") return sdkMediaListResponseSchema;
1444
- if (method === "GET" && /^\/media\/[^/]+\/download$/.test(path$2)) return sdkMediaDownloadResponseSchema;
1445
- if (method === "POST" && /^\/media\/[^/]+\/delete-preview$/.test(path$2)) return sdkMediaDeletePreviewResponseSchema;
1446
- if (method === "DELETE" && /^\/media\/[^/]+$/.test(path$2)) return sdkMediaDeleteApplyResponseSchema;
1447
- return undefined;
1448
- }
1449
- function schemaForManagementRequest(method, path$2, body) {
1450
- if (method === "GET") {
1451
- return pullSchemaForRequest(path$2) ?? occurrenceResponseSchema(method, path$2) ?? mediaResponseSchema(method, path$2);
1452
- }
1453
- return rootMutationSchema(method, path$2, body) ?? occurrenceResponseSchema(method, path$2) ?? mediaResponseSchema(method, path$2) ?? nestedMutationSchema(method, path$2);
1454
- }
1455
- function validateManagementResponse(value, schema) {
1456
- const parsed = schema.safeParse(value);
1457
- if (parsed.success) return { valid: true };
1458
- return {
1459
- valid: false,
1460
- details: { issues: parsed.error.issues.slice(0, 8).map((issue) => ({
1461
- path: issue.path.map(String),
1462
- message: issue.message,
1463
- ...issue.code ? { code: issue.code } : {}
1464
- })) }
1465
- };
1466
- }
1467
-
1468
- //#endregion
1469
- //#region ../management-client/src/transportFailure.ts
1470
- /**
1471
- * Sanitised transport failures (issue 1496).
1472
- *
1473
- * When `fetch` rejects, the caught value can carry anything the platform put
1474
- * there: a message naming the host and port, a cause chain, a stack, and in
1475
- * some runtimes echoes of the request. None of that may leave the client. The
1476
- * caught value is inspected here, and only here, for one thing: whether its
1477
- * platform code (`cause.code`, else `code`) is one of an allow-listed set of
1478
- * Node and undici codes. The result is a closed union whose `code` is always
1479
- * one of those literals, and a fixed message per kind. Nothing else from the caught value is kept.
1480
- *
1481
- * Code lists were checked against Node 24.18 (undici bundled with it) by
1482
- * provoking each failure: DNS lookups fail with `ENOTFOUND`/`EAI_AGAIN`, a
1483
- * closed port with `ECONNREFUSED` (an `AggregateError` carrying the same code
1484
- * when happy-eyeballs tries several addresses), an unroutable host with
1485
- * undici's `UND_ERR_CONNECT_TIMEOUT`, a peer reset with `ECONNRESET`, a peer
1486
- * closing mid-response with `UND_ERR_SOCKET`, and TLS failures with the
1487
- * OpenSSL verification codes and `ERR_TLS_CERT_ALTNAME_INVALID`.
1488
- *
1489
- * Bun puts its code on the error itself rather than on `cause`, so that is read
1490
- * as a fallback. Bun 1.3 reports resets and TLS failures with the same codes
1491
- * as Node; it reports refused connections *and* failed DNS lookups alike as
1492
- * `ConnectionRefused`, which is deliberately not listed and stays `unknown`.
1493
- */
1494
- const DNS_CODES = ["ENOTFOUND", "EAI_AGAIN"];
1495
- const CONNECT_CODES = [
1496
- "ECONNREFUSED",
1497
- "UND_ERR_CONNECT_TIMEOUT",
1498
- "ECONNABORTED",
1499
- "EHOSTUNREACH",
1500
- "ENETUNREACH"
1501
- ];
1502
- const RESET_CODES = [
1503
- "ECONNRESET",
1504
- "EPIPE",
1505
- "UND_ERR_SOCKET"
1506
- ];
1507
- const TLS_CODES = [
1508
- "CERT_HAS_EXPIRED",
1509
- "DEPTH_ZERO_SELF_SIGNED_CERT",
1510
- "SELF_SIGNED_CERT_IN_CHAIN",
1511
- "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
1512
- "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
1513
- "ERR_TLS_CERT_ALTNAME_INVALID",
1514
- "ERR_SSL_WRONG_VERSION_NUMBER"
1515
- ];
1516
- const TIMEOUT_CODES = [
1517
- "UND_ERR_HEADERS_TIMEOUT",
1518
- "UND_ERR_BODY_TIMEOUT",
1519
- "ETIMEDOUT"
1520
- ];
1521
- function isOneOf(values, value) {
1522
- return values.includes(value);
1523
- }
1524
- /**
1525
- * Read one property of an arbitrary value. A throwing getter or a hostile
1526
- * proxy yields `undefined`, so inspection can never throw past the boundary.
1527
- */
1528
- function readProperty(value, key) {
1529
- if (typeof value !== "object" || value === null) return undefined;
1530
- try {
1531
- return Reflect.get(value, key);
1532
- } catch {
1533
- return undefined;
1534
- }
1535
- }
1536
- function stringCode(value) {
1537
- const code = readProperty(value, "code");
1538
- return typeof code === "string" ? code : undefined;
1539
- }
1540
- /**
1541
- * The platform code for a rejected `fetch`: Node/undici put it on `cause`;
1542
- * Bun puts its own on the error itself. Nothing else is read.
1543
- */
1544
- function platformCode(caught) {
1545
- return stringCode(readProperty(caught, "cause")) ?? stringCode(caught);
1546
- }
1547
- /** Map a raw code onto the closed union; anything unlisted is `unknown`. */
1548
- function transportFailureFromCode(code) {
1549
- if (code === undefined) return { kind: "unknown" };
1550
- if (isOneOf(DNS_CODES, code)) return {
1551
- kind: "dns",
1552
- code
1553
- };
1554
- if (isOneOf(CONNECT_CODES, code)) return {
1555
- kind: "connect",
1556
- code
1557
- };
1558
- if (isOneOf(RESET_CODES, code)) return {
1559
- kind: "reset",
1560
- code
1561
- };
1562
- if (isOneOf(TLS_CODES, code)) return {
1563
- kind: "tls",
1564
- code
1565
- };
1566
- if (isOneOf(TIMEOUT_CODES, code)) return {
1567
- kind: "timeout",
1568
- code
1569
- };
1570
- return { kind: "unknown" };
1571
- }
1572
- /** True when the client's own `AbortSignal.timeout` ended the request. */
1573
- function isClientTimeout(caught) {
1574
- return readProperty(caught, "name") === "TimeoutError";
1575
- }
1576
- /**
1577
- * Classify a rejected `fetch` or body read. The caught value is read only for
1578
- * its `name` and platform code (guarded, see `readProperty`). `timeoutMs` is
1579
- * the limit the client itself applied, so the timeout message can name it.
1580
- */
1581
- function classifyTransportFailure(caught, timeoutMs) {
1582
- return isClientTimeout(caught) ? {
1583
- code: "sdk:timeout",
1584
- transport: { kind: "timeout" },
1585
- timeoutMs
1586
- } : {
1587
- code: "sdk:network-error",
1588
- transport: transportFailureFromCode(platformCode(caught))
1589
- };
1590
- }
1591
- /** The fixed message for a classified failure. */
1592
- function transportErrorMessage(failure) {
1593
- return failure.code === "sdk:timeout" ? `Request timed out after ${failure.timeoutMs}ms` : transportFailureMessage(failure.transport);
1594
- }
1595
- /** The fixed, safe message for each kind of network failure. */
1596
- function transportFailureMessage(failure) {
1597
- switch (failure.kind) {
1598
- case "dns": return "Could not resolve the dashboard hostname";
1599
- case "connect": return "Could not open a connection to the dashboard";
1600
- case "reset": return "The connection to the dashboard closed before a response arrived";
1601
- case "tls": return "The TLS handshake with the dashboard failed";
1602
- case "timeout": return "The connection to the dashboard timed out";
1603
- case "unknown": return "Network request failed";
1604
- }
1605
- }
1606
-
1607
- //#endregion
1608
- //#region ../management-client/src/http.ts
1609
- /**
1610
- * Error thrown when management API request fails
1611
- */
1612
- var ManagementApiError = class extends Error {
1613
- constructor(message, code, details, statusCode, request, tag) {
1614
- super(message);
1615
- this.name = "ManagementApiError";
1616
- this.code = code;
1617
- this.details = details;
1618
- this.statusCode = statusCode;
1619
- this.request = request;
1620
- this.tag = tag;
1621
- }
1622
- };
1623
- /** A successful HTTP response whose payload violated its runtime contract. */
1624
- var ManagementInvalidResponseError = class extends ManagementApiError {
1625
- constructor(details, statusCode, request) {
1626
- super("Management API returned an invalid response", "invalid_response", details, statusCode, request);
1627
- this.code = "invalid_response";
1628
- this.name = "ManagementInvalidResponseError";
1629
- this.details = details;
1630
- this.request = request;
1631
- }
1632
- };
1633
- /**
1634
- * The request never produced a dashboard answer. The message is fixed per
1635
- * failure kind and nothing from the platform error (its message, cause, stack
1636
- * or anything echoing the request) is kept: `transport` is the only diagnostic.
1637
- */
1638
- var ManagementTransportError = class extends ManagementApiError {
1639
- /** The message is derived from the classification; no free text is accepted. */
1640
- constructor(classified, request) {
1641
- super(transportErrorMessage(classified), classified.code, undefined, undefined, request);
1642
- this.details = undefined;
1643
- this.name = "ManagementTransportError";
1644
- this.code = classified.code;
1645
- this.transport = classified.transport;
1646
- }
1647
- };
1648
- /**
1649
- * Turn a rejected `fetch` or body read into a {@link ManagementTransportError}.
1650
- * Every catch site around a management `fetch` or response-body read goes
1651
- * through here so none of them can leak the platform error.
1652
- */
1653
- function managementTransportError(caught, timeoutMs, request) {
1654
- return new ManagementTransportError(classifyTransportFailure(caught, timeoutMs), request);
1655
- }
1656
- /**
1657
- * Type guard to check if an error is a 404 Not Found response.
1658
- * Use this to handle "resource not found" scenarios gracefully.
1659
- *
1660
- * @example
1661
- * ```ts
1662
- * try {
1663
- * return await http.get<Entry>(`/entries/${type}/${id}`);
1664
- * } catch (error) {
1665
- * if (is404Error(error)) {
1666
- * return null;
1667
- * }
1668
- * throw error;
1669
- * }
1670
- * ```
1671
- */
1672
- function is404Error(error) {
1673
- return error instanceof ManagementApiError && error.statusCode === 404;
1674
- }
1675
- function parseRuntimeResponse(value, schema, requestInfo, statusCode) {
1676
- const parsed = schema.safeParse(value);
1677
- if (parsed.success) return parsed.data;
1678
- const validation = validateManagementResponse(value, schema);
1679
- if (validation.valid) throw new Error("Response schema parsing was inconsistent");
1680
- throw new ManagementInvalidResponseError(validation.details, statusCode, requestInfo);
1681
- }
1682
- function isRecord$2(value) {
1683
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
1684
- }
1685
- function isFieldError(value) {
1686
- return isRecord$2(value) && typeof value.field === "string" && typeof value.code === "string" && typeof value.message === "string";
1687
- }
1688
- function parseApiResponse(value) {
1689
- if (!isRecord$2(value)) return null;
1690
- if (value.success === true) {
1691
- return {
1692
- success: true,
1693
- data: value.data
1694
- };
1695
- }
1696
- if (value.success === false) {
1697
- const error = value.error;
1698
- if (!isRecord$2(error) || typeof error.code !== "string" || typeof error.message !== "string") {
1699
- return {
1700
- success: false,
1701
- error: {
1702
- code: "sdk:http-error",
1703
- message: "Request failed",
1704
- details: error
1705
- }
1706
- };
1707
- }
1708
- const fieldErrorsRaw = error.fieldErrors;
1709
- const fieldErrors = Array.isArray(fieldErrorsRaw) ? fieldErrorsRaw.filter(isFieldError) : undefined;
1710
- return {
1711
- success: false,
1712
- error: {
1713
- code: error.code,
1714
- message: error.message,
1715
- details: error.details,
1716
- ...fieldErrors ? { fieldErrors } : {},
1717
- ...typeof error.tag === "string" ? { tag: error.tag } : {}
1718
- }
1719
- };
1720
- }
1721
- return null;
1722
- }
1723
- function buildRequestInfo(options) {
1724
- return {
1725
- method: options.method,
1726
- intent: options.intent,
1727
- path: options.path,
1728
- url: options.url,
1729
- baseUrl: options.baseUrl,
1730
- dashboardUrl: options.dashboardUrl,
1731
- siteId: options.siteId
1732
- };
1733
- }
1734
- /**
1735
- * Read a response body as text. A body that fails mid-read (a reset, undici's
1736
- * body timeout, the client's own abort) is a transport failure like any other.
1737
- */
1738
- async function readResponseText(response, timeoutMs, requestInfo) {
1739
- try {
1740
- return await response.text();
1741
- } catch (error) {
1742
- throw managementTransportError(error, timeoutMs, requestInfo);
1743
- }
1744
- }
1745
- async function readApiResponse(response, requestInfo, options) {
1746
- const contentType = response.headers.get("content-type");
1747
- const text = await readResponseText(response, options.timeoutMs, requestInfo);
1748
- if (!contentType?.includes("application/json")) {
1749
- if (response.ok && options.allowNonJsonOk) return undefined;
1750
- if (response.ok && options.invalidOkResponse) {
1751
- throw new ManagementInvalidResponseError({ issues: [{
1752
- path: [],
1753
- message: "Expected a JSON response"
1754
- }] }, response.status, requestInfo);
1755
- }
1756
- throw new ManagementApiError(`Request failed with status ${response.status}`, "sdk:http-error", { statusCode: response.status }, response.status, requestInfo);
1757
- }
1758
- let parsed;
1759
- try {
1760
- parsed = text ? JSON.parse(text) : null;
1761
- } catch {
1762
- if (response.ok && options.invalidOkResponse) {
1763
- throw new ManagementInvalidResponseError({ issues: [{
1764
- path: [],
1765
- message: "Response was not valid JSON"
1766
- }] }, response.status, requestInfo);
1767
- }
1768
- throw new ManagementApiError(`Invalid JSON response (status ${response.status})`, "sdk:http-error", { statusCode: response.status }, response.status, requestInfo);
1769
- }
1770
- const envelope = parseApiResponse(parsed);
1771
- if (!envelope) {
1772
- if (response.ok && options.invalidOkResponse) {
1773
- throw new ManagementInvalidResponseError({ issues: [{
1774
- path: [],
1775
- message: "Response did not match the API envelope"
1776
- }] }, response.status, requestInfo);
1777
- }
1778
- throw new ManagementApiError(`Invalid API response (status ${response.status})`, "sdk:http-error", { statusCode: response.status }, response.status, requestInfo);
1779
- }
1780
- return envelope;
1781
- }
1782
- function requestUrl(baseUrl, path$2, params) {
1783
- if (!params || Object.keys(params).length === 0) return `${baseUrl}${path$2}`;
1784
- const query = new URLSearchParams(params).toString();
1785
- return `${baseUrl}${path$2}?${query}`;
1786
- }
1787
- const DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
1788
- function requestTimeoutMs(config) {
1789
- return config.timeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
1790
- }
1791
- function requestFetchOptions(config, method, body) {
1792
- const options = {
1793
- method,
1794
- headers: {
1795
- Authorization: `Bearer ${exposeSecret(config.managementApiKey)}`,
1796
- "Content-Type": "application/json"
1797
- },
1798
- signal: AbortSignal.timeout(requestTimeoutMs(config))
1799
- };
1800
- if (body !== undefined && method !== "GET") options.body = JSON.stringify(body);
1801
- return options;
1802
- }
1803
- async function fetchManagementResponse(config, method, intent, path$2, url, baseUrl, options) {
1804
- const requestInfo = buildRequestInfo({
1805
- method,
1806
- intent,
1807
- path: path$2,
1808
- url,
1809
- baseUrl,
1810
- dashboardUrl: config.dashboardUrl,
1811
- siteId: config.siteId
1812
- });
1813
- try {
1814
- return {
1815
- response: await fetch(url, options),
1816
- requestInfo
1817
- };
1818
- } catch (error) {
1819
- throw managementTransportError(error, requestTimeoutMs(config), requestInfo);
1820
- }
1821
- }
1822
- function throwApiError(response, json, requestInfo) {
1823
- if (response.ok && json.success) return;
1824
- if ("error" in json && json.error) {
1825
- const details = json.error.fieldErrors ? { fieldErrors: json.error.fieldErrors } : json.error.details;
1826
- throw new ManagementApiError(json.error.message, json.error.code, details, response.status, requestInfo, json.error.tag);
1827
- }
1828
- throw new ManagementApiError(`Request failed with status ${response.status}`, "sdk:http-error", undefined, response.status, requestInfo);
1829
- }
1830
- async function requestWithConfig(config, method, path$2, options) {
1831
- const baseUrl = `${config.dashboardUrl}/api/sdk/${config.siteId}`;
1832
- const url = requestUrl(baseUrl, path$2, options.params);
1833
- const fetchOptions = requestFetchOptions(config, method, options.body);
1834
- const { response, requestInfo } = await fetchManagementResponse(config, method, options.intent, path$2, url, baseUrl, fetchOptions);
1835
- const responseSchema = options.responseSchema ?? schemaForManagementRequest(method, path$2, options.body);
1836
- const timeoutMs = requestTimeoutMs(config);
1837
- const json = responseSchema ? await readApiResponse(response, requestInfo, {
1838
- timeoutMs,
1839
- invalidOkResponse: true
1840
- }) : await readApiResponse(response, requestInfo, {
1841
- timeoutMs,
1842
- allowNonJsonOk: true
1843
- });
1844
- if (!json) return undefined;
1845
- throwApiError(response, json, requestInfo);
1846
- return responseSchema ? parseRuntimeResponse(json.data, responseSchema, requestInfo, response.status) : json.data;
1847
- }
1848
- function createHttpClient(config) {
1849
- const send = (method, path$2, options) => requestWithConfig(config, method, path$2, options);
1850
- return {
1851
- get: (path$2, params) => send("GET", path$2, {
1852
- intent: "read",
1853
- params
1854
- }),
1855
- post: (path$2, body, intent) => send("POST", path$2, {
1856
- intent,
1857
- body
1858
- }),
1859
- patch: (path$2, body) => send("PATCH", path$2, {
1860
- intent: "write",
1861
- body
1862
- }),
1863
- delete: (path$2, body) => send("DELETE", path$2, {
1864
- intent: "write",
1865
- body
1866
- }),
1867
- getValidated: (path$2, responseSchema, params) => send("GET", path$2, {
1868
- intent: "read",
1869
- params,
1870
- responseSchema
1871
- }),
1872
- postValidated: (path$2, responseSchema, body, intent) => send("POST", path$2, {
1873
- intent,
1874
- body,
1875
- responseSchema
1876
- }),
1877
- patchValidated: (path$2, responseSchema, body) => send("PATCH", path$2, {
1878
- intent: "write",
1879
- body,
1880
- responseSchema
1881
- }),
1882
- deleteValidated: (path$2, responseSchema, body) => send("DELETE", path$2, {
1883
- intent: "write",
1884
- body,
1885
- responseSchema
1886
- })
1887
- };
1888
- }
1889
-
1890
- //#endregion
1891
- //#region src/media/walk.ts
1892
- const isRecordValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1893
- const walkValueTree = (value, location, visitor) => {
1894
- visitor(value, location);
1895
- if (Array.isArray(value)) {
1896
- value.forEach((entry, index) => {
1897
- walkValueTree(entry, `${location}[${index}]`, visitor);
1898
- });
1899
- return;
1900
- }
1901
- if (isRecordValue(value)) {
1902
- Object.entries(value).forEach(([key, entry]) => {
1903
- walkValueTree(entry, location ? `${location}.${key}` : key, visitor);
1904
- });
1905
- }
1906
- };
1907
-
1908
- //#endregion
1909
- //#region src/media/value.ts
1910
- const MEDIA_VALUE_TYPES = [
1911
- "image",
1912
- "video",
1913
- "audio",
1914
- "document",
1915
- "spreadsheet",
1916
- "archive"
1917
- ];
1918
- const MEDIA_VALUE_TYPE_SET = new Set(MEDIA_VALUE_TYPES);
1919
- const isOptionalString = (value) => value === undefined || typeof value === "string";
1920
- const isOptionalNumber = (value) => value === undefined || typeof value === "number";
1921
- const hasValidKnownMediaFields = (value) => (value.identifier === null || isOptionalString(value.identifier)) && isOptionalString(value.assetId) && isOptionalString(value.storagePath) && isOptionalString(value.storageBucket) && isOptionalString(value.src) && isOptionalString(value.mimeType) && isOptionalNumber(value.width) && isOptionalNumber(value.height);
1922
- const hasMediaReference = (value) => typeof value.identifier === "string" || typeof value.assetId === "string" || typeof value.storagePath === "string" || typeof value.src === "string" || typeof value.mimeType === "string";
1923
- const isMediaValue = (value) => isRecordValue(value) && !("attrs" in value) && typeof value.type === "string" && MEDIA_VALUE_TYPE_SET.has(value.type) && hasValidKnownMediaFields(value) && hasMediaReference(value);
1924
-
1925
- //#endregion
1926
- //#region src/content/media-manifest.ts
1927
- const MANIFEST_VERSION = 2;
1928
- const getManifestPath = (contentDir) => path$1.join(contentDir, "media", ".manifest.json");
1929
- const ensureDir = async (dirPath) => {
1930
- try {
1931
- await fs$1.mkdir(dirPath, { recursive: true });
1932
- } catch (error) {
1933
- if (error.code !== "EEXIST") {
1934
- throw error;
1935
- }
1936
- }
1937
- };
1938
- const writeJsonFile = async (filePath, data) => {
1939
- const content = JSON.stringify(data, null, 2) + "\n";
1940
- await fs$1.writeFile(filePath, content, "utf-8");
1941
- };
1942
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1943
- const hasManifestShape = (value) => {
1944
- if (!isRecord$1(value) || !("version" in value) || !("assets" in value)) {
1945
- return false;
1946
- }
1947
- return isRecord$1(value.assets);
1948
- };
1949
- const toManifestEntry = (value) => {
1950
- if (!isRecord$1(value)) return null;
1951
- const filename = typeof value.filename === "string" ? value.filename : "";
1952
- if (!filename) return null;
1953
- return {
1954
- filename,
1955
- checksum: typeof value.checksum === "string" ? value.checksum : undefined,
1956
- mimeType: typeof value.mimeType === "string" ? value.mimeType : undefined
1957
- };
1958
- };
1959
- const toV2ManifestEntry = (value) => {
1960
- if (!isRecord$1(value)) return null;
1961
- if (value.checksum !== undefined && typeof value.checksum !== "string" || value.mimeType !== undefined && typeof value.mimeType !== "string") {
1962
- return null;
1963
- }
1964
- return toManifestEntry(value);
1965
- };
1966
- const parseV2Manifest = (parsed, assets) => {
1967
- if (typeof parsed.updatedAt !== "string") return null;
1968
- const parsedAssets = {};
1969
- for (const [identifier, value] of Object.entries(assets)) {
1970
- const entry = toV2ManifestEntry(value);
1971
- if (!entry) return null;
1972
- parsedAssets[identifier] = entry;
1973
- }
1974
- return {
1975
- version: MANIFEST_VERSION,
1976
- updatedAt: parsed.updatedAt,
1977
- assets: parsedAssets
1978
- };
1979
- };
1980
- const migrateV1Assets = (assets) => Object.fromEntries(Object.entries(assets).flatMap(([identifier, value]) => {
1981
- const entry = toManifestEntry(value);
1982
- return entry ? [[identifier, entry]] : [];
1983
- }));
1984
- const migrateV1Manifest = (parsed, assets) => {
1985
- return {
1986
- version: MANIFEST_VERSION,
1987
- updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : new Date().toISOString(),
1988
- assets: migrateV1Assets(assets)
1989
- };
1990
- };
1991
- const parseManifest = (content) => {
1992
- const parsed = JSON.parse(content);
1993
- if (!hasManifestShape(parsed)) return null;
1994
- const assets = parsed.assets;
1995
- if (parsed.version === 1) return migrateV1Manifest(parsed, assets);
1996
- if (parsed.version === MANIFEST_VERSION) {
1997
- return parseV2Manifest(parsed, assets);
1998
- }
1999
- return null;
2000
- };
2001
- const createEmptyMediaManifest = () => ({
2002
- version: MANIFEST_VERSION,
2003
- updatedAt: new Date().toISOString(),
2004
- assets: {}
2005
- });
2006
- async function readMediaManifest(contentDir) {
2007
- const manifestPath = getManifestPath(contentDir);
2008
- try {
2009
- const content = await fs$1.readFile(manifestPath, "utf-8");
2010
- return parseManifest(content) ?? createEmptyMediaManifest();
2011
- } catch (error) {
2012
- const nodeError = error;
2013
- if (nodeError.code !== "ENOENT") {
2014
- console.warn(`Warning: failed to read media manifest: ${nodeError.message}`);
2015
- }
2016
- }
2017
- return createEmptyMediaManifest();
2018
- }
2019
- async function writeMediaManifest(contentDir, manifest) {
2020
- const mediaDir = path$1.join(contentDir, "media");
2021
- await ensureDir(mediaDir);
2022
- const updatedManifest = {
2023
- ...manifest,
2024
- version: MANIFEST_VERSION,
2025
- updatedAt: new Date().toISOString()
2026
- };
2027
- const manifestPath = getManifestPath(contentDir);
2028
- await writeJsonFile(manifestPath, updatedManifest);
2029
- return manifestPath;
2030
- }
2031
-
2032
- //#endregion
2033
- //#region ../management-client/src/settings-branding.ts
2034
- const SETTINGS_BASE_FIELDS = [
2035
- "siteTitle",
2036
- "homepageId",
2037
- "seoDefaults"
2038
- ];
2039
- const SETTINGS_BRANDING_FIELDS = [
2040
- "logoIdentifier",
2041
- "logoDarkBackgroundIdentifier",
2042
- "faviconIdentifier"
2043
- ];
2044
- const SETTINGS_FIELDS = [...SETTINGS_BASE_FIELDS, ...SETTINGS_BRANDING_FIELDS];
2045
- /** Fields owned by content/settings.json. Booking configuration has its own
2046
- * mutation operation and must never enter the generic settings update input. */
2047
- const SETTINGS_FILE_FIELDS = [...SETTINGS_FIELDS, "verticalConfig"];
2048
- const SETTINGS_BRANDING_LABELS = {
2049
- logoIdentifier: "logo",
2050
- logoDarkBackgroundIdentifier: "dark-background logo",
2051
- faviconIdentifier: "favicon"
2052
- };
2053
- function hasOwnSettingsField(settings, field) {
2054
- return hasOwnField(settings, field);
2055
- }
2056
- function hasOwnField(settings, field) {
2057
- return Object.prototype.hasOwnProperty.call(settings, field);
2058
- }
2059
- function buildSettingsUpdateInput(settings) {
2060
- const update = {};
2061
- for (const field of SETTINGS_FIELDS) {
2062
- if (!hasOwnSettingsField(settings, field)) {
2063
- continue;
2064
- }
2065
- switch (field) {
2066
- case "siteTitle":
2067
- update.siteTitle = settings.siteTitle;
2068
- break;
2069
- case "homepageId":
2070
- update.homepageId = settings.homepageId;
2071
- break;
2072
- case "seoDefaults":
2073
- update.seoDefaults = settings.seoDefaults;
2074
- break;
2075
- case "logoIdentifier":
2076
- update.logoIdentifier = settings.logoIdentifier;
2077
- break;
2078
- case "logoDarkBackgroundIdentifier":
2079
- update.logoDarkBackgroundIdentifier = settings.logoDarkBackgroundIdentifier;
2080
- break;
2081
- case "faviconIdentifier":
2082
- update.faviconIdentifier = settings.faviconIdentifier;
2083
- break;
2084
- }
2085
- }
2086
- return update;
2087
- }
2088
- function applySettingsBrandingPullResolutions(baseSettings, resolutions) {
2089
- const settings = {
2090
- homepageId: baseSettings.homepageId,
2091
- seoDefaults: baseSettings.seoDefaults
2092
- };
2093
- const warnings = [];
2094
- for (const resolution of resolutions) {
2095
- switch (resolution.kind) {
2096
- case "resolved":
2097
- settings[resolution.field] = resolution.identifier;
2098
- break;
2099
- case "missing":
2100
- settings[resolution.field] = null;
2101
- break;
2102
- case "unresolved":
2103
- warnings.push(`Remote ${SETTINGS_BRANDING_LABELS[resolution.field]} asset ` + `"${resolution.assetId}" has no media identifier, so ${resolution.field} was omitted from content/settings.json.`);
2104
- break;
2105
- }
2106
- }
2107
- return {
2108
- settings,
2109
- warnings
2110
- };
2111
- }
2112
- function getSettingsBrandingLabel(field) {
2113
- return SETTINGS_BRANDING_LABELS[field];
2114
- }
2115
- function collectSettingsBrandingIdentifiers(settings) {
2116
- if (!settings) {
2117
- return [];
2118
- }
2119
- const identifiers = [];
2120
- for (const field of SETTINGS_BRANDING_FIELDS) {
2121
- const identifier = settings[field];
2122
- if (typeof identifier === "string" && identifier) {
2123
- identifiers.push({
2124
- field,
2125
- identifier
2126
- });
2127
- }
2128
- }
2129
- return identifiers;
2130
- }
2131
- function isImageMimeType(mimeType) {
2132
- return typeof mimeType === "string" && mimeType.startsWith("image/");
2133
- }
2134
-
2135
- //#endregion
2136
- //#region src/sync/media.ts
2137
- /**
2138
- * Media Sync Helper
2139
- *
2140
- * Handles syncing media assets between environments during pull/push operations.
2141
- *
2142
- * With relative paths, storagePath values in content are like "originals/2024/12/abc.jpg"
2143
- * (without site prefix). The actual files are stored at "sites/{siteId}/originals/...".
2144
- *
2145
- * When syncing:
2146
- * 1. Extract all relative storagePath values from content
2147
- * 2. Download files from source: sites/{sourceSiteId}/{relativePath}
2148
- * 3. Upload files to target: sites/{targetSiteId}/{relativePath}
2149
- *
2150
- * No path rewriting in content data is needed since paths are relative!
2151
- */
2152
- /**
2153
- * Default retry configuration for network operations.
2154
- */
2155
- const DEFAULT_RETRY_CONFIG = {
2156
- maxRetries: 3,
2157
- baseDelayMs: 1e3,
2158
- maxDelayMs: 1e4
2159
- };
2160
- /**
2161
- * Sleep for a given number of milliseconds.
2162
- */
2163
- function sleep(ms) {
2164
- return new Promise((resolve$2) => setTimeout(resolve$2, ms));
2165
- }
2166
- /**
2167
- * Calculate exponential backoff delay with jitter.
2168
- * @param attempt - Current attempt number (0-indexed)
2169
- * @param baseDelayMs - Base delay in milliseconds
2170
- * @param maxDelayMs - Maximum delay cap
2171
- */
2172
- function getBackoffDelay(attempt, baseDelayMs, maxDelayMs) {
2173
- const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
2174
- const jitter = exponentialDelay * (.75 + Math.random() * .5);
2175
- return Math.min(jitter, maxDelayMs);
2176
- }
2177
- /**
2178
- * Execute a function with exponential backoff retry.
2179
- * @param fn - Function to execute
2180
- * @param options - Retry options
2181
- */
2182
- async function withRetry(fn, options = {}) {
2183
- const { maxRetries, baseDelayMs, maxDelayMs } = {
2184
- ...DEFAULT_RETRY_CONFIG,
2185
- ...options
2186
- };
2187
- let lastError;
2188
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
2189
- try {
2190
- return await fn();
2191
- } catch (error) {
2192
- lastError = error instanceof Error ? error : new Error(String(error));
2193
- if (attempt < maxRetries) {
2194
- const delay = getBackoffDelay(attempt, baseDelayMs, maxDelayMs);
2195
- options.onRetry?.(attempt + 1, lastError);
2196
- await sleep(delay);
2197
- }
2198
- }
2199
- }
2200
- throw lastError;
2201
- }
2202
- /**
2203
- * Extract all unique storagePath values from content data.
2204
- * Recursively walks through objects and arrays to find all media references.
2205
- */
2206
- function extractMediaPaths(data) {
2207
- const paths = new Set();
2208
- walkValueTree(data, "", (value) => {
2209
- if (!isRecordValue(value)) return;
2210
- if (typeof value.storagePath === "string" && value.storagePath) {
2211
- paths.add(value.storagePath);
2212
- }
2213
- });
2214
- return paths;
2215
- }
2216
- /**
2217
- * Extract all unique media identifiers from content data.
2218
- * Recursively walks through objects and arrays to find media references.
2219
- */
2220
- function extractMediaIdentifiers(data) {
2221
- const identifiers = new Set();
2222
- if (isRecordValue(data)) {
2223
- const rootSettings = isRecordValue(data.settings) ? data.settings : null;
2224
- for (const { identifier } of collectSettingsBrandingIdentifiers(rootSettings)) {
2225
- identifiers.add(identifier);
2226
- }
2227
- }
2228
- walkValueTree(data, "", (value) => {
2229
- if (!isRecordValue(value)) return;
2230
- if (isMediaValue(value) && value.identifier) {
2231
- identifiers.add(value.identifier);
2232
- }
2233
- if (value.type === "image" && isRecordValue(value.attrs)) {
2234
- if (typeof value.attrs.identifier === "string" && value.attrs.identifier) {
2235
- identifiers.add(value.attrs.identifier);
2236
- }
2237
- }
2238
- });
2239
- return identifiers;
2240
- }
2241
- function buildStorageUrl(supabaseUrl, storagePath, siteId, bucket = "media") {
2242
- const normalized = normalizeAmbiguousStoragePathForSite(siteId, storagePath);
2243
- if (!normalized.ok) {
2244
- return {
2245
- ok: false,
2246
- error: {
2247
- tag: "invalid_storage_path",
2248
- siteId,
2249
- storagePath,
2250
- cause: normalized.error
2251
- }
2252
- };
2253
- }
2254
- return {
2255
- ok: true,
2256
- value: getDirectImageUrlForSiteScopedPath(supabaseUrl, normalized.value.fullPath, bucket)
2257
- };
2258
- }
2259
- /**
2260
- * File extensions that support Supabase image transforms.
2261
- */
2262
- const TRANSFORMABLE_EXTENSIONS = [
2263
- "jpg",
2264
- "jpeg",
2265
- "png",
2266
- "webp",
2267
- "gif"
2268
- ];
2269
- /**
2270
- * Check if a file path is an image that can be transformed by Supabase.
2271
- * @param path - The file path or storage path
2272
- */
2273
- function isTransformableImage(path$2) {
2274
- const ext = path$2.split(".").pop()?.toLowerCase();
2275
- return TRANSFORMABLE_EXTENSIONS.includes(ext ?? "");
2276
- }
2277
- /**
2278
- * Build a URL using Supabase's image transform endpoint.
2279
- * Returns a resized version to avoid exceeding upload size limits.
2280
- *
2281
- * @param supabaseUrl - The Supabase project URL
2282
- * @param storagePath - Relative or site-scoped path within the bucket
2283
- * @param siteId - The site ID context for relative paths
2284
- * @param options - Transform options (width, quality)
2285
- */
2286
- function buildImageTransformUrl(supabaseUrl, storagePath, siteId, options = {}) {
2287
- const { width = 1920, quality = 85 } = options;
2288
- const normalized = normalizeAmbiguousStoragePathForSite(siteId, storagePath);
2289
- if (!normalized.ok) {
2290
- return {
2291
- ok: false,
2292
- error: {
2293
- tag: "invalid_storage_path",
2294
- siteId,
2295
- storagePath,
2296
- cause: normalized.error
2297
- }
2298
- };
2299
- }
2300
- return {
2301
- ok: true,
2302
- value: getDirectImageUrlForSiteScopedPath(supabaseUrl, normalized.value.fullPath, "media", {
2303
- width,
2304
- quality
2305
- })
2306
- };
2307
- }
2308
- /**
2309
- * Download a file from a public Supabase storage URL.
2310
- * Returns the file data as a Buffer, or null if download fails after retries.
2311
- *
2312
- * Uses exponential backoff retry for transient network failures.
2313
- */
2314
- async function downloadMedia(url, options) {
2315
- try {
2316
- return await withRetry(async () => {
2317
- const response = await fetch(url);
2318
- if (!response.ok) {
2319
- if (response.status >= 400 && response.status < 500 && response.status !== 429) {
2320
- throw new DownloadError(`HTTP ${response.status}`, response.status, false);
2321
- }
2322
- throw new DownloadError(`HTTP ${response.status}`, response.status, true);
2323
- }
2324
- const contentType = response.headers.get("content-type") ?? "application/octet-stream";
2325
- const arrayBuffer = await response.arrayBuffer();
2326
- const data = Buffer.from(arrayBuffer);
2327
- return {
2328
- data,
2329
- contentType
2330
- };
2331
- }, {
2332
- maxRetries: options?.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries,
2333
- onRetry: (attempt, error) => {
2334
- console.warn(`[media-sync] Retry ${attempt} for ${url}: ${error.message}`);
2335
- }
2336
- });
2337
- } catch (error) {
2338
- const message = error instanceof Error ? error.message : String(error);
2339
- console.error(`[media-sync] Download failed after retries: ${url} - ${message}`);
2340
- return null;
2341
- }
2342
- }
2343
- /**
2344
- * Custom error for download operations with retryability info.
2345
- */
2346
- var DownloadError = class extends Error {
2347
- constructor(message, status, retryable) {
2348
- super(message);
2349
- this.status = status;
2350
- this.retryable = retryable;
2351
- this.name = "DownloadError";
2352
- }
2353
- };
2354
- /**
2355
- * Sync media files from source to target environment.
2356
- *
2357
- * With relative paths, content data doesn't need to be modified.
2358
- * We just copy files from source site folder to target site folder.
2359
- *
2360
- * @param content - The content data containing media references
2361
- * @param client - Client for uploading to target environment
2362
- * @param options - Sync options
2363
- * @returns Summary of sync results (content unchanged since paths are relative)
2364
- */
2365
- async function syncMedia(content, client, options) {
2366
- const { sourceSupabaseUrl, sourceSiteId, bucket = "media", onProgress } = options;
2367
- const mediaPaths = extractMediaPaths(content);
2368
- const total = mediaPaths.size;
2369
- const results = [];
2370
- let completed = 0;
2371
- for (const relativePath of mediaPaths) {
2372
- const filename = relativePath.split("/").pop() ?? "file";
2373
- onProgress?.(completed, total, filename);
2374
- try {
2375
- const exists = await client.mediaExists(relativePath);
2376
- if (exists) {
2377
- results.push({
2378
- relativePath,
2379
- success: true,
2380
- skipped: true
2381
- });
2382
- completed++;
2383
- continue;
2384
- }
2385
- } catch {}
2386
- const urlResult = buildStorageUrl(sourceSupabaseUrl, relativePath, sourceSiteId, bucket);
2387
- if (!urlResult.ok) {
2388
- results.push({
2389
- relativePath,
2390
- success: false,
2391
- error: `Invalid media storage path: ${urlResult.error.cause.tag}`
2392
- });
2393
- completed++;
2394
- continue;
2395
- }
2396
- const url = urlResult.value;
2397
- const downloaded = await downloadMedia(url);
2398
- if (!downloaded) {
2399
- results.push({
2400
- relativePath,
2401
- success: false,
2402
- error: `Failed to download from ${url}`
2403
- });
2404
- completed++;
2405
- continue;
2406
- }
2407
- const uploadResult = await client.uploadMedia({
2408
- data: downloaded.data,
2409
- filename,
2410
- contentType: downloaded.contentType,
2411
- storagePath: relativePath
2412
- });
2413
- if ("error" in uploadResult) {
2414
- results.push({
2415
- relativePath,
2416
- success: false,
2417
- error: uploadResult.error
2418
- });
2419
- } else {
2420
- results.push({
2421
- relativePath,
2422
- success: true
2423
- });
2424
- }
2425
- completed++;
2426
- }
2427
- onProgress?.(total, total, "complete");
2428
- const summary = {
2429
- total,
2430
- synced: results.filter((r) => r.success && !r.skipped).length,
2431
- skipped: results.filter((r) => r.skipped).length,
2432
- failed: results.filter((r) => !r.success).length,
2433
- results
2434
- };
2435
- return {
2436
- summary,
2437
- content
2438
- };
2439
- }
2440
-
2441
- //#endregion
2442
- //#region src/media/identifiers.ts
2443
- const walkContentMedia = (content, handler) => {
2444
- const visit = (value, location) => {
2445
- if (isMediaValue(value)) {
2446
- handler(value, location);
2447
- }
2448
- };
2449
- for (const [contentType, entries] of content.entries) {
2450
- for (const entry of entries) {
2451
- walkValueTree(entry.data, `entries.${contentType}.${entry.identifier}.data`, visit);
2452
- for (const [slotKey, blocks] of Object.entries(entry.blocks ?? {})) {
2453
- for (const block of blocks) {
2454
- walkValueTree(block.content, `entries.${contentType}.${entry.identifier}.blocks.${slotKey}.${block.identifier ?? "unidentified"}.content`, visit);
2455
- }
2456
- }
2457
- }
2458
- }
2459
- for (const page of content.pages) {
2460
- for (const block of page.blocks ?? []) {
2461
- walkValueTree(block.data, `pages.${page.identifier}.blocks.${block.identifier}.data`, visit);
2462
- }
2463
- }
2464
- walkValueTree(content.navigation, "navigation", visit);
2465
- for (const { field, identifier } of collectSettingsBrandingIdentifiers(content.settings)) {
2466
- handler({
2467
- type: "image",
2468
- identifier
2469
- }, `settings.${field}`);
2470
- }
2471
- walkValueTree(content.settings, "settings", visit);
2472
- walkValueTree(content.forms, "forms", visit);
2473
- walkValueTree(content.footer, "footer", visit);
2474
- walkValueTree(content.venues, "venues", visit);
2475
- walkValueTree(content.eventCategories, "eventCategories", visit);
2476
- for (const event of content.events ?? []) {
2477
- walkValueTree(event, `events.${event.identifier}`, visit);
2478
- }
2479
- };
2480
- const localContentSections = (content) => ({
2481
- entries: content.entries?.entries() ?? [],
2482
- pages: content.pages ?? [],
2483
- navigation: content.navigation,
2484
- settings: content.settings,
2485
- forms: content.forms,
2486
- footer: content.footer,
2487
- venues: content.venues,
2488
- eventCategories: content.eventCategories,
2489
- events: content.events
2490
- });
2491
- const pulledContentSections = (content) => ({
2492
- entries: Object.entries(content.entries),
2493
- pages: content.pages,
2494
- navigation: content.navigation,
2495
- settings: content.settings,
2496
- forms: content.forms,
2497
- venues: content.venues,
2498
- eventCategories: content.eventCategories,
2499
- events: content.events
2500
- });
2501
- const walkLocalContentMedia = (content, handler) => {
2502
- walkContentMedia(localContentSections(content), handler);
2503
- };
2504
- const walkPulledContentMedia = (content, handler) => {
2505
- walkContentMedia(pulledContentSections(content), handler);
2506
- };
2507
-
2508
- //#endregion
2509
- //#region src/media/value-utils.ts
2510
- /**
2511
- * Normalize a media identifier to the canonical slug form.
2512
- * Returns null when the raw value is empty or invalid.
2513
- */
2514
- const normalizeMediaIdentifierInput = (raw) => {
2515
- if (!raw) return null;
2516
- const trimmed = raw.trim();
2517
- if (!trimmed) return null;
2518
- if (/[./\\]/.test(trimmed)) {
2519
- return null;
2520
- }
2521
- const normalized = slugifyMediaIdentifier(trimmed);
2522
- return normalized || null;
2523
- };
2524
- /**
2525
- * Normalize a media value to the portable identifier-only representation.
2526
- *
2527
- * Returns a new media value by:
2528
- * - setting `identifier` (required)
2529
- * - removing env-specific fields (`assetId`, `storagePath`, `storageBucket`, `src`, `mimeType`)
2530
- */
2531
- const normalizeMediaValueToIdentifierOnly = (value, identifier, asset) => {
2532
- const normalized = {
2533
- ...value,
2534
- identifier
2535
- };
2536
- delete normalized.assetId;
2537
- delete normalized.storageBucket;
2538
- delete normalized.storagePath;
2539
- delete normalized.mimeType;
2540
- delete normalized.src;
2541
- if (asset) {
2542
- applyAuthoritativeDimension(normalized, "width", asset.width);
2543
- applyAuthoritativeDimension(normalized, "height", asset.height);
2544
- }
2545
- return normalized;
2546
- };
2547
- function applyAuthoritativeDimension(value, key, dimension) {
2548
- if (dimension === null || dimension === undefined) {
2549
- delete value[key];
2550
- return;
2551
- }
2552
- value[key] = dimension;
2553
- }
2554
-
2555
- //#endregion
2556
- //#region src/media/rich-text.ts
2557
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2558
- /**
2559
- * Walk any JSON-like payload and collect TipTap rich text image node attrs.
2560
- *
2561
- * TipTap nodes look like: { type: 'image', attrs: { src?: string, identifier?: string, ... } }
2562
- */
2563
- function collectRichTextImageAttrs(value, out = [], location = "") {
2564
- if (Array.isArray(value)) {
2565
- value.forEach((child, index) => collectRichTextImageAttrs(child, out, `${location}[${index}]`));
2566
- return out;
2567
- }
2568
- if (!isRecord(value)) return out;
2569
- if (value.type === "image" && isRecord(value.attrs)) {
2570
- out.push({
2571
- attrs: value.attrs,
2572
- location: location ? `${location}.attrs` : "attrs"
2573
- });
2574
- }
2575
- for (const [key, child] of Object.entries(value)) {
2576
- const next = location ? `${location}.${key}` : key;
2577
- collectRichTextImageAttrs(child, out, next);
2578
- }
2579
- return out;
2580
- }
2581
-
2582
- //#endregion
2583
- //#region src/media/pull-manifest.ts
2584
- const buildLocalFilename = (identifier, asset) => {
2585
- const extensionFromFilename = path$1.extname(asset.filename ?? "");
2586
- const extension = extensionFromFilename || getExtensionForMimeType(asset.mimeType) || "";
2587
- return extension ? `${identifier}${extension}` : identifier;
2588
- };
2589
- const updateManifestEntry = (manifest, identifier, asset, localFile) => {
2590
- const previous = manifest.assets[identifier];
2591
- const filename = localFile?.filename ?? previous?.filename ?? buildLocalFilename(identifier, asset);
2592
- const checksum = localFile?.checksum ?? asset.checksumSha256 ?? previous?.checksum;
2593
- const mimeType = localFile?.mimeType ?? asset.mimeType ?? previous?.mimeType;
2594
- const entry = {
2595
- checksum,
2596
- filename,
2597
- mimeType
2598
- };
2599
- manifest.assets[identifier] = entry;
2600
- };
2601
- const normalizeStoragePath = (storagePath, siteId) => {
2602
- const prefix = `sites/${siteId}/`;
2603
- if (storagePath.startsWith(prefix)) {
2604
- return storagePath.slice(prefix.length);
2605
- }
2606
- return storagePath;
2607
- };
2608
-
2609
- //#endregion
2610
- //#region src/media/push-preparation-plan.ts
2611
- const cloneLocalFile = (file) => ({
2612
- identifier: file.identifier,
2613
- filename: file.filename,
2614
- filePath: file.filePath,
2615
- mimeType: file.mimeType,
2616
- checksum: file.checksum,
2617
- dataBase64: Buffer.from(file.data).toString("base64"),
2618
- dataLength: file.data.length
2619
- });
2620
- const collectLocalMediaReferences = (content) => {
2621
- const mediaReferences = [];
2622
- const richTextReferences = [];
2623
- const warnings = [];
2624
- const errors = [];
2625
- const locationsByIdentifier = new Map();
2626
- walkLocalContentMedia(content, (value, location) => {
2627
- const rawIdentifier = typeof value.identifier === "string" ? value.identifier : "";
2628
- const hasLegacyRef = Boolean(value.assetId || value.storagePath || value.src);
2629
- if (!rawIdentifier) {
2630
- if (hasLegacyRef) {
2631
- errors.push(`Media reference at ${location} is missing identifier.`);
2632
- }
2633
- return;
2634
- }
2635
- if (/[./\\]/.test(rawIdentifier)) {
2636
- errors.push(`Media identifier "${rawIdentifier}" at ${location} must not include an extension.`);
2637
- return;
2638
- }
2639
- const normalized = normalizeMediaIdentifierInput(rawIdentifier);
2640
- if (!normalized) {
2641
- errors.push(`Media identifier "${rawIdentifier}" at ${location} is invalid.`);
2642
- return;
2643
- }
2644
- if (normalized !== rawIdentifier) {
2645
- warnings.push(`Normalized media identifier "${rawIdentifier}" to "${normalized}" at ${location}.`);
2646
- value.identifier = normalized;
2647
- }
2648
- mediaReferences.push({
2649
- identifier: normalized,
2650
- rawIdentifier,
2651
- value,
2652
- location
2653
- });
2654
- const locations = locationsByIdentifier.get(normalized) ?? [];
2655
- locations.push(location);
2656
- locationsByIdentifier.set(normalized, locations);
2657
- });
2658
- for (const node of collectRichTextImageAttrs(content)) {
2659
- const attrs = node.attrs;
2660
- const rawIdentifier = typeof attrs.identifier === "string" ? attrs.identifier : "";
2661
- const rawSrc = typeof attrs.src === "string" ? attrs.src : "";
2662
- if (!rawIdentifier) {
2663
- if (rawSrc) {
2664
- errors.push(`Rich text image at ${node.location} is missing identifier.`);
2665
- }
2666
- continue;
2667
- }
2668
- if (/[./\\]/.test(rawIdentifier)) {
2669
- errors.push(`Rich text image identifier "${rawIdentifier}" at ${node.location} must not include an extension.`);
2670
- continue;
2671
- }
2672
- const normalized = normalizeMediaIdentifierInput(rawIdentifier);
2673
- if (!normalized) {
2674
- errors.push(`Rich text image identifier "${rawIdentifier}" at ${node.location} is invalid.`);
2675
- continue;
2676
- }
2677
- if (normalized !== rawIdentifier) {
2678
- warnings.push(`Normalized rich text image identifier "${rawIdentifier}" to "${normalized}" at ${node.location}.`);
2679
- attrs.identifier = normalized;
2680
- }
2681
- delete attrs.src;
2682
- richTextReferences.push({
2683
- identifier: normalized,
2684
- rawIdentifier,
2685
- attrs,
2686
- location: node.location
2687
- });
2688
- const locations = locationsByIdentifier.get(normalized) ?? [];
2689
- locations.push(node.location);
2690
- locationsByIdentifier.set(normalized, locations);
2691
- }
2692
- return {
2693
- mediaReferences,
2694
- richTextReferences,
2695
- warnings,
2696
- errors,
2697
- locationsByIdentifier
2698
- };
2699
- };
2700
- const buildLocationNote = (locationsByIdentifier, identifier) => {
2701
- const locations = locationsByIdentifier.get(identifier);
2702
- return locations?.length ? ` Referenced at: ${locations.join(", ")}.` : "";
2703
- };
2704
- const locationsForIdentifier = (locationsByIdentifier, identifier) => locationsByIdentifier.get(identifier) ?? [];
2705
- const operationPriority = (operation) => {
2706
- switch (operation.kind) {
2707
- case "upload": return 0;
2708
- case "conflict":
2709
- case "missing_local_file":
2710
- case "reused":
2711
- case "skipped":
2712
- case "unchanged": return 1;
2713
- }
2714
- };
2715
- const sortOperations = (operations) => operations.slice().sort((a, b) => {
2716
- const byIdentifier = a.identifier.localeCompare(b.identifier);
2717
- return byIdentifier || operationPriority(a) - operationPriority(b);
2718
- });
2719
- const emptyPushSummary = (totalReferences, identifiers) => ({
2720
- totalReferences,
2721
- identifiers,
2722
- resolved: 0,
2723
- reused: 0,
2724
- uploaded: 0,
2725
- skipped: 0,
2726
- pendingUploads: 0
2727
- });
2728
- const stableAssetList = (assets) => assets.slice().sort((a, b) => a.id.localeCompare(b.id)).map((asset) => ({ ...asset }));
2729
- const stableMediaReadSet = (observations) => stableStringify(observations.slice().sort((a, b) => a.identifier.localeCompare(b.identifier)).map((observation) => ({
2730
- identifier: observation.identifier,
2731
- localChecksum: observation.localChecksum.kind === "present" && observation.localChecksum.lookup.kind === "read" ? {
2732
- ...observation.localChecksum,
2733
- lookup: {
2734
- ...observation.localChecksum.lookup,
2735
- assets: stableAssetList(observation.localChecksum.lookup.assets)
2736
- }
2737
- } : observation.localChecksum,
2738
- resolvedAsset: observation.resolvedAsset,
2739
- missing: observation.missing
2740
- })));
2741
- const buildMediaReadSet = (options) => options.identifiers.map((identifier) => {
2742
- const checksum = options.localChecksums.get(identifier);
2743
- const resolvedAsset = options.initialAssets.get(identifier);
2744
- const localChecksum = checksum === undefined ? { kind: "missing" } : {
2745
- kind: "present",
2746
- checksum,
2747
- lookup: options.checksumAssetsByChecksum.has(checksum) ? {
2748
- kind: "read",
2749
- assets: stableAssetList(options.checksumAssetsByChecksum.get(checksum) ?? [])
2750
- } : { kind: "not-read" }
2751
- };
2752
- return {
2753
- identifier,
2754
- localChecksum,
2755
- resolvedAsset: resolvedAsset ? { ...resolvedAsset } : null,
2756
- missing: options.initialMissing.has(identifier)
2757
- };
2758
- });
2759
- const localFileForOperation = (localFile) => ({
2760
- filename: localFile.filename,
2761
- checksum: localFile.checksum,
2762
- mimeType: localFile.mimeType
2763
- });
2764
- const classifyExistingAsset = (options, hasConflict) => {
2765
- const { identifier, asset, localFile, locations, locationNote } = options;
2766
- if (asset && !localFile) {
2767
- return {
2768
- operations: [{
2769
- kind: "skipped",
2770
- identifier,
2771
- locations,
2772
- message: "Using existing CMS asset because no local file was found."
2773
- }],
2774
- warnings: [`Missing local file for "${identifier}". Using CMS asset instead.${locationNote}`],
2775
- errors: []
2776
- };
2777
- }
2778
- if (hasConflict && asset) {
2779
- const message = `Identifier "${identifier}" points to a different file on the CMS. Delete the existing asset first or rename your local identifier.`;
2780
- return {
2781
- operations: [{
2782
- kind: "conflict",
2783
- identifier,
2784
- locations,
2785
- message
2786
- }],
2787
- errors: [`${message}${locationNote}`]
2788
- };
2789
- }
2790
- if (asset && localFile && !hasConflict) {
2791
- return {
2792
- operations: [{
2793
- kind: "unchanged",
2794
- identifier,
2795
- asset: { ...asset },
2796
- file: cloneLocalFile(localFile),
2797
- locations
2798
- }],
2799
- manifestAsset: asset,
2800
- manifestFile: localFileForOperation(localFile),
2801
- errors: []
2802
- };
2803
- }
2804
- return undefined;
2805
- };
2806
- const classifySingularChecksumMatch = (options) => {
2807
- const { identifier, localFile, locations, initialOperations } = options;
2808
- const exact = options.checksumMatches.find((candidate) => candidate.identifier === identifier);
2809
- if (exact) {
2810
- return {
2811
- operations: [...initialOperations, {
2812
- kind: "reused",
2813
- identifier,
2814
- asset: { ...exact },
2815
- file: cloneLocalFile(localFile),
2816
- locations
2817
- }],
2818
- manifestAsset: exact,
2819
- manifestFile: localFileForOperation(localFile),
2820
- errors: []
2821
- };
2822
- }
2823
- return undefined;
2824
- };
2825
- const classifyChecksumMatches = (options) => {
2826
- const { identifier, localFile, checksumMatches, locations, initialOperations, locationNote } = options;
2827
- const singular = classifySingularChecksumMatch(options);
2828
- if (singular) return singular;
2829
- if (checksumMatches.length > 0) {
2830
- const identifiers = checksumMatches.map((asset) => asset.identifier).join(", ");
2831
- const message = `The same media bytes already exist under ${identifiers}; use that identifier or delete the existing asset first.`;
2832
- return {
2833
- operations: [...initialOperations, {
2834
- kind: "conflict",
2835
- identifier,
2836
- locations,
2837
- message
2838
- }],
2839
- errors: [`${message}${locationNote}`]
2840
- };
2841
- }
2842
- return {
2843
- operations: [...initialOperations, {
2844
- kind: "upload",
2845
- identifier,
2846
- file: cloneLocalFile(localFile),
2847
- source: "local_file",
2848
- locations
2849
- }],
2850
- errors: []
2851
- };
2852
- };
2853
- const classifyMissingReference = (options) => {
2854
- const { identifier, localFile, locationNote, initialOperations, locations } = options;
2855
- if (!localFile) {
2856
- const message = `Add content/media/${identifier}.* or pull with --local-media first.`;
2857
- return {
2858
- operations: [...initialOperations, {
2859
- kind: "missing_local_file",
2860
- identifier,
2861
- locations,
2862
- message
2863
- }],
2864
- errors: [`Identifier "${identifier}" does not exist on the CMS and no local file was found. ${message}${locationNote}`]
2865
- };
2866
- }
2867
- return classifyChecksumMatches({
2868
- identifier,
2869
- localFile,
2870
- checksumMatches: options.checksumMatches,
2871
- locations,
2872
- initialOperations,
2873
- locationNote
2874
- });
2875
- };
2876
- const classifyLocalMediaReference = (options) => {
2877
- const { asset, localFile, missing, locations } = options;
2878
- const hasConflict = Boolean(asset && localFile && asset.checksumSha256 && asset.checksumSha256 !== localFile.checksum);
2879
- const existing = classifyExistingAsset(options, hasConflict);
2880
- if (existing) return existing;
2881
- const initialOperations = [];
2882
- if (!missing && !hasConflict) return {
2883
- operations: initialOperations,
2884
- errors: []
2885
- };
2886
- return classifyMissingReference({
2887
- identifier: options.identifier,
2888
- localFile,
2889
- checksumMatches: options.checksumMatches,
2890
- locations,
2891
- initialOperations,
2892
- locationNote: options.locationNote
2893
- });
2894
- };
2895
- const applyManifestClassification = (manifest, identifier, classification) => {
2896
- if (classification.manifestAsset) {
2897
- updateManifestEntry(manifest, identifier, classification.manifestAsset, classification.manifestFile);
2898
- }
2899
- };
2900
-
2901
- //#endregion
2902
- //#region src/utils/checksum.ts
2903
- function sha256(buffer) {
2904
- return createHash("sha256").update(buffer).digest("hex");
2905
- }
2906
-
2907
- //#endregion
2908
- //#region src/files/stable-binary-file.ts
2909
- function isNodeError(error) {
2910
- return error instanceof Error && "code" in error;
2911
- }
2912
- function observationsMatch(left, right) {
2913
- return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
2914
- }
2915
- function admitStableBinaryRead(stats, maxBytes) {
2916
- if (!stats.isFile()) return err({ tag: "not_a_regular_file" });
2917
- return stats.size > maxBytes ? err({
2918
- tag: "file_too_large",
2919
- maxBytes,
2920
- actualBytes: stats.size
2921
- }) : ok(undefined);
2922
- }
2923
- function mapStableBinaryReadFailure(error) {
2924
- if (!isNodeError(error)) return { tag: "read_failed" };
2925
- if (error.code === "ENOENT") return { tag: "file_not_found" };
2926
- return error.code === "ELOOP" || error.code === "EISDIR" ? { tag: "not_a_regular_file" } : { tag: "read_failed" };
2927
- }
2928
- async function readObservedByteLength(handle, byteLength) {
2929
- const data = Buffer.allocUnsafe(byteLength);
2930
- let offset = 0;
2931
- while (offset < byteLength) {
2932
- const read = await handle.read(data, offset, byteLength - offset, offset);
2933
- if (read.bytesRead === 0) return null;
2934
- offset += read.bytesRead;
2935
- }
2936
- const growthProbe = Buffer.allocUnsafe(1);
2937
- const extra = await handle.read(growthProbe, 0, 1, byteLength);
2938
- return extra.bytesRead === 0 ? data : null;
2939
- }
2940
- async function readStableBinaryFile(filePath, maxBytes) {
2941
- let handle;
2942
- try {
2943
- const noFollow = constants.O_NOFOLLOW ?? 0;
2944
- handle = await open(filePath, constants.O_RDONLY | noFollow);
2945
- const before = await handle.stat();
2946
- const admission = admitStableBinaryRead(before, maxBytes);
2947
- if (!admission.ok) return admission;
2948
- const data = await readObservedByteLength(handle, before.size);
2949
- const after = await handle.stat();
2950
- if (data === null || !observationsMatch(before, after)) {
2951
- return err({ tag: "file_changed_during_read" });
2952
- }
2953
- return ok(data);
2954
- } catch (error) {
2955
- return err(mapStableBinaryReadFailure(error));
2956
- } finally {
2957
- await handle?.close().catch(() => undefined);
2958
- }
2959
- }
2960
- async function inspectDestination(filePath, overwrite) {
2961
- try {
2962
- const stats = await lstat(filePath);
2963
- if (overwrite === "refuse") return err({ tag: "destination_exists" });
2964
- return stats.isFile() ? ok(undefined) : err({ tag: "unsafe_destination" });
2965
- } catch (error) {
2966
- if (isNodeError(error) && error.code === "ENOENT") return ok(undefined);
2967
- return err({ tag: "write_failed" });
2968
- }
2969
- }
2970
- /**
2971
- * Admit an output path before a remote operation while preserving the atomic
2972
- * writer's second check at finalization time to close the race window.
2973
- */
2974
- async function admitBinaryWriteDestination(filePath, overwrite) {
2975
- const destination = await inspectDestination(filePath, overwrite);
2976
- if (!destination.ok) return destination;
2977
- try {
2978
- const parent = dirname$1(filePath);
2979
- const parentStats = await lstat(parent);
2980
- if (!parentStats.isDirectory()) return err({ tag: "unsafe_destination" });
2981
- await access(parent, constants.W_OK);
2982
- return ok(undefined);
2983
- } catch {
2984
- return err({ tag: "write_failed" });
2985
- }
2986
- }
2987
- async function syncDirectory(directory) {
2988
- if (process.platform === "win32") return true;
2989
- let handle;
2990
- try {
2991
- handle = await open(directory, "r");
2992
- await handle.sync();
2993
- return true;
2994
- } catch {
2995
- return false;
2996
- } finally {
2997
- await handle?.close().catch(() => undefined);
2998
- }
2999
- }
3000
- function mapAtomicBinaryWriteFailure(error) {
3001
- return isNodeError(error) && (error.code === "ELOOP" || error.code === "EISDIR" || error.code === "ENOTDIR") ? { tag: "unsafe_destination" } : { tag: "write_failed" };
3002
- }
3003
- async function writeTemporaryBinaryFile(temporaryPath, data, mode) {
3004
- let handle;
3005
- try {
3006
- handle = await open(temporaryPath, "wx", mode);
3007
- await handle.writeFile(data);
3008
- await handle.sync();
3009
- await handle.close();
3010
- handle = undefined;
3011
- await chmod(temporaryPath, mode);
3012
- } finally {
3013
- await handle?.close().catch(() => undefined);
3014
- }
3015
- }
3016
- async function finalizeTemporaryBinaryFile(temporaryPath, filePath, overwrite) {
3017
- if (overwrite === "allow") {
3018
- await rename(temporaryPath, filePath);
3019
- return ok([]);
3020
- }
3021
- try {
3022
- await link(temporaryPath, filePath);
3023
- } catch (error) {
3024
- return isNodeError(error) && error.code === "EEXIST" ? err({ tag: "destination_exists" }) : err(mapAtomicBinaryWriteFailure(error));
3025
- }
3026
- try {
3027
- await unlink$1(temporaryPath);
3028
- return ok([]);
3029
- } catch {
3030
- return ok([{
3031
- tag: "temporary_cleanup_failed",
3032
- temporaryPath
3033
- }]);
3034
- }
3035
- }
3036
- function toAtomicBinaryWriteOutcome(warnings) {
3037
- const [first, ...rest] = warnings;
3038
- return first === undefined ? { tag: "written" } : {
3039
- tag: "written_with_warnings",
3040
- warnings: [first, ...rest]
3041
- };
3042
- }
3043
- async function writeBinaryFileAtomic(filePath, data, options) {
3044
- const parent = dirname$1(filePath);
3045
- const mode = options.mode ?? 384;
3046
- const suffix = options.randomSuffix ?? (() => `${process.pid}-${Date.now()}-${randomUUID()}`);
3047
- let temporaryPath;
3048
- try {
3049
- if (options.ensureParent === true) await mkdir(parent, { recursive: true });
3050
- const admitted = await inspectDestination(filePath, options.overwrite);
3051
- if (!admitted.ok) return admitted;
3052
- temporaryPath = join(parent, `.${basename(filePath)}.${suffix()}.tmp`);
3053
- await writeTemporaryBinaryFile(temporaryPath, data, mode);
3054
- const finalized = await finalizeTemporaryBinaryFile(temporaryPath, filePath, options.overwrite);
3055
- if (!finalized.ok) return finalized;
3056
- temporaryPath = undefined;
3057
- const warnings = await syncDirectory(parent) ? finalized.value : [...finalized.value, { tag: "directory_sync_failed" }];
3058
- return ok(toAtomicBinaryWriteOutcome(warnings));
3059
- } catch (error) {
3060
- return err(mapAtomicBinaryWriteFailure(error));
3061
- } finally {
3062
- if (temporaryPath) await unlink$1(temporaryPath).catch(() => undefined);
3063
- }
3064
- }
3065
-
3066
- //#endregion
3067
- //#region src/media/local-media.ts
3068
- const MEDIA_DIR_NAME = "media";
3069
- const fileExists = async (filePath) => {
3070
- try {
3071
- await fs$1.access(filePath);
3072
- return true;
3073
- } catch {
3074
- return false;
3075
- }
3076
- };
3077
- const listMediaDirFiles = async (mediaDir) => {
3078
- try {
3079
- return await fs$1.readdir(mediaDir);
3080
- } catch (error) {
3081
- const nodeError = error;
3082
- if (nodeError.code === "ENOENT") return [];
3083
- throw error;
3084
- }
3085
- };
3086
- const findMatchingFilename = async (contentDir, identifier, manifest) => {
3087
- const mediaDir = path$1.join(contentDir, MEDIA_DIR_NAME);
3088
- const manifestEntry = manifest.assets[identifier];
3089
- if (manifestEntry?.filename) {
3090
- const candidatePath = path$1.join(mediaDir, manifestEntry.filename);
3091
- if (await fileExists(candidatePath)) {
3092
- return manifestEntry.filename;
3093
- }
3094
- }
3095
- const files = await listMediaDirFiles(mediaDir);
3096
- const matches = files.filter((name) => {
3097
- if (name.startsWith(".")) return false;
3098
- const parsed = path$1.parse(name);
3099
- return parsed.name === identifier;
3100
- });
3101
- if (matches.length > 1) {
3102
- throw new Error(`Multiple media files match identifier "${identifier}": ${matches.join(", ")}`);
3103
- }
3104
- return matches[0] ?? null;
3105
- };
3106
- async function loadLocalMediaFile(contentDir, identifier, manifest) {
3107
- const filename = await findMatchingFilename(contentDir, identifier, manifest);
3108
- if (!filename) return null;
3109
- const mediaDir = path$1.join(contentDir, MEDIA_DIR_NAME);
3110
- const filePath = path$1.join(mediaDir, filename);
3111
- const read = await readStableBinaryFile(filePath, MAX_MANAGEMENT_MEDIA_UPLOAD_BYTES);
3112
- if (!read.ok) {
3113
- throw new Error(`Failed to read media file ${filePath}: ${read.error.tag}`);
3114
- }
3115
- const data = read.value;
3116
- const checksum = sha256(data);
3117
- const mimeType = getMimeType(filename);
3118
- manifest.assets[identifier] = {
3119
- ...manifest.assets[identifier],
3120
- filename,
3121
- checksum,
3122
- mimeType
3123
- };
3124
- return {
3125
- identifier,
3126
- filename,
3127
- filePath,
3128
- mimeType,
3129
- checksum,
3130
- data
3131
- };
3132
- }
3133
- async function writeLocalMediaFile(contentDir, filename, data) {
3134
- const mediaDir = path$1.join(contentDir, MEDIA_DIR_NAME);
3135
- await fs$1.mkdir(mediaDir, { recursive: true });
3136
- const filePath = path$1.join(mediaDir, filename);
3137
- const written = await writeBinaryFileAtomic(filePath, data, {
3138
- overwrite: "allow",
3139
- mode: 420
3140
- });
3141
- if (!written.ok) {
3142
- throw new Error(`Failed to write media file ${filePath}: ${written.error.tag}`);
3143
- }
3144
- return written.value.tag === "written" ? {
3145
- kind: "written",
3146
- filePath
3147
- } : {
3148
- kind: "written_with_warnings",
3149
- filePath,
3150
- warnings: written.value.warnings
3151
- };
3152
- }
3153
- async function readLocalMediaChecksum(filePath, maxBytes) {
3154
- const read = await readStableBinaryFile(filePath, maxBytes);
3155
- if (!read.ok) {
3156
- throw new Error(`Failed to read media file ${filePath}: ${read.error.tag}`);
3157
- }
3158
- return sha256(read.value);
3159
- }
3160
- async function localMediaFileExists(filePath) {
3161
- return fileExists(filePath);
3162
- }
3163
-
3164
- //#endregion
3165
- //#region src/media/pull-naming.ts
3166
- const GENERIC_MEDIA_NAMES = new Set([
3167
- "image",
3168
- "photo",
3169
- "picture",
3170
- "pic",
3171
- "img",
3172
- "media",
3173
- "file"
3174
- ]);
3175
- const buildIdentifierBaseFromLocation = (location) => {
3176
- const parts = location.split(".");
3177
- if (parts.length < 2) return null;
3178
- if (parts[0] === "entries") {
3179
- const contentType = parts[1];
3180
- const entryIdentifier = parts[2];
3181
- const dataIndex = parts.indexOf("data");
3182
- const fieldName = dataIndex >= 0 ? parts[dataIndex + 1] : "media";
3183
- if (contentType && entryIdentifier && fieldName) {
3184
- return `${contentType}-${entryIdentifier}-${fieldName}`;
3185
- }
3186
- }
3187
- if (parts[0] === "pages") {
3188
- const pageIdentifier = parts[1];
3189
- const blockIndex = parts.indexOf("blocks");
3190
- const blockIdentifier = blockIndex >= 0 ? parts[blockIndex + 1] : null;
3191
- const dataIndex = parts.indexOf("data");
3192
- const fieldName = dataIndex >= 0 ? parts[dataIndex + 1] : "media";
3193
- if (pageIdentifier && blockIdentifier && fieldName) {
3194
- return `${pageIdentifier}-${blockIdentifier}-${fieldName}`;
3195
- }
3196
- if (pageIdentifier && fieldName) {
3197
- return `${pageIdentifier}-${fieldName}`;
3198
- }
3199
- }
3200
- return null;
3201
- };
3202
- const chooseIdentifierBase = (location, asset) => {
3203
- const baseFromFilename = buildMediaIdentifierFromFilename(asset.filename ?? "");
3204
- const baseFromContext = buildIdentifierBaseFromLocation(location);
3205
- if (baseFromFilename && !GENERIC_MEDIA_NAMES.has(baseFromFilename)) {
3206
- return baseFromFilename;
3207
- }
3208
- return baseFromContext || baseFromFilename || "media";
3209
- };
3210
- const allocateIdentifier = (base, existingIdentifiers) => resolveMediaIdentifier(base, existingIdentifiers);
3211
- const parseFilenameFromUrl = (src) => {
3212
- try {
3213
- const url = new URL(src);
3214
- const raw = url.pathname.split("/").filter(Boolean).pop() ?? "";
3215
- const decoded = raw ? decodeURIComponent(raw) : "";
3216
- const trimmed = decoded.trim();
3217
- return trimmed.length > 0 ? trimmed : null;
3218
- } catch {
3219
- return null;
3220
- }
3221
- };
3222
- const ensureFilenameExtension = (filename, mimeType) => {
3223
- if (path$1.extname(filename)) return filename;
3224
- const ext = getExtensionForMimeType(mimeType);
3225
- return ext ? `${filename}${ext}` : filename;
3226
- };
3227
- const chooseRichTextIdentifierBase = (location, src) => {
3228
- const filename = parseFilenameFromUrl(src) ?? "";
3229
- const baseFromFilename = filename ? buildMediaIdentifierFromFilename(filename) : null;
3230
- const baseFromContext = buildIdentifierBaseFromLocation(location);
3231
- const base = baseFromFilename && !GENERIC_MEDIA_NAMES.has(baseFromFilename) ? baseFromFilename : baseFromContext || baseFromFilename || "rich-text-image";
3232
- return `${base}-rt`;
3233
- };
3234
-
3235
- //#endregion
3236
- //#region src/media/push-preparation-reads.ts
3237
- const readMediaManifestAndLocalFiles = async (options) => {
3238
- const manifest = options.manifest ?? await readMediaManifest(options.contentDir);
3239
- const errors = [];
3240
- const localFiles = new Map();
3241
- for (const identifier of options.identifiers) {
3242
- try {
3243
- const localFile = await loadLocalMediaFile(options.contentDir, identifier, manifest);
3244
- localFiles.set(identifier, localFile);
3245
- } catch (error) {
3246
- const message = error instanceof Error ? error.message : String(error);
3247
- errors.push(`Failed to load media file for "${identifier}": ${message}`);
3248
- localFiles.set(identifier, null);
3249
- }
3250
- }
3251
- return {
3252
- manifest,
3253
- localFiles,
3254
- errors
3255
- };
3256
- };
3257
- const resolveMediaAssets = async (client, identifiers) => {
3258
- const resolved = await client.media.resolve({ identifiers: [...identifiers] });
3259
- const assetsByIdentifier = new Map();
3260
- for (const asset of resolved.assets) {
3261
- if (asset.identifier) assetsByIdentifier.set(asset.identifier, asset);
3262
- }
3263
- return {
3264
- assetsByIdentifier,
3265
- missingIdentifiers: new Set(resolved.missingIdentifiers)
3266
- };
3267
- };
3268
- const readChecksumMatches = async (options) => {
3269
- const matchesByChecksum = new Map();
3270
- for (const identifier of options.identifiers) {
3271
- if (options.preparedIdentifiers.has(identifier)) continue;
3272
- const localFile = options.localFiles.get(identifier);
3273
- const asset = options.assetsByIdentifier.get(identifier);
3274
- const hasConflict = Boolean(asset && localFile && asset.checksumSha256 && asset.checksumSha256 !== localFile.checksum);
3275
- if (hasConflict) continue;
3276
- if (!localFile || !options.missingIdentifiers.has(identifier) && !hasConflict) {
3277
- continue;
3278
- }
3279
- const matches = (await options.client.media.findByChecksum([localFile.checksum])).filter((candidate) => candidate.checksumSha256 === localFile.checksum);
3280
- const stableMatches = matches.slice().sort((a, b) => a.id.localeCompare(b.id)).map((candidate) => ({ ...candidate }));
3281
- matchesByChecksum.set(localFile.checksum, stableMatches);
3282
- }
3283
- return matchesByChecksum;
3284
- };
3285
- const readRichTextImageSource = async (options) => {
3286
- const downloaded = await downloadMedia(options.source);
3287
- if (!downloaded) {
3288
- throw new Error(`Failed to download rich text image from ${options.source}`);
3289
- }
3290
- const mimeType = downloaded.contentType.split(";")[0]?.trim() || "application/octet-stream";
3291
- if (!mimeType.startsWith("image/")) {
3292
- throw new Error(`Unsupported content-type for rich text image import: ${mimeType}`);
3293
- }
3294
- const data = Buffer.from(downloaded.data);
3295
- const checksum = sha256(data);
3296
- const checksumMatches = (await options.client.media.findByChecksum([checksum])).filter((candidate) => candidate.checksumSha256 === checksum).slice().sort((a, b) => a.id.localeCompare(b.id)).map((candidate) => ({ ...candidate }));
3297
- const filename = ensureFilenameExtension(parseFilenameFromUrl(options.source) ?? "image", mimeType);
3298
- return {
3299
- location: options.location,
3300
- attrs: options.attrs,
3301
- source: options.source,
3302
- mimeType,
3303
- checksum,
3304
- file: cloneLocalFile({
3305
- identifier: "planned-rich-text-image",
3306
- filename,
3307
- filePath: options.source,
3308
- mimeType,
3309
- checksum,
3310
- data
3311
- }),
3312
- checksumMatches
3313
- };
3314
- };
3315
- /**
3316
- * Reads remote rich-text image sources without changing content or CMS state.
3317
- * Identifier allocation and operation selection happen in the pure planner.
3318
- */
3319
- const readRichTextImageImports = async (options) => {
3320
- const successful = [];
3321
- const failures = [];
3322
- for (const node of collectRichTextImageAttrs(options.content)) {
3323
- const rawIdentifier = typeof node.attrs.identifier === "string" ? node.attrs.identifier.trim() : "";
3324
- const source = typeof node.attrs.src === "string" ? node.attrs.src.trim() : "";
3325
- if (rawIdentifier || !source) continue;
3326
- try {
3327
- successful.push(await readRichTextImageSource({
3328
- client: options.client,
3329
- location: node.location,
3330
- attrs: node.attrs,
3331
- source
3332
- }));
3333
- } catch (error) {
3334
- failures.push({
3335
- location: node.location,
3336
- error: error instanceof Error ? error.message : String(error)
3337
- });
3338
- }
3339
- }
3340
- return {
3341
- successful,
3342
- failures
3343
- };
3344
- };
3345
- const localFileChecksums = (identifiers, localFiles) => new Map(identifiers.map((identifier) => [identifier, localFiles.get(identifier)?.checksum]));
3346
- const cloneAssetMap = (assets) => new Map(Array.from(assets.entries()).map(([identifier, asset]) => [identifier, { ...asset }]));
3347
- const cloneMissingSet = (missing) => new Set(missing);
3348
-
3349
- //#endregion
3350
- //#region src/media/push-rich-text-plan.ts
3351
- const localFileWithIdentifier = (file, identifier) => ({
3352
- ...file,
3353
- identifier
3354
- });
3355
- const withLocation = (locations, identifier, location) => {
3356
- const current = locations.get(identifier) ?? [];
3357
- current.push(location);
3358
- locations.set(identifier, current);
3359
- };
3360
- const chooseExistingRichTextAsset = (matches) => matches.slice().sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
3361
- const normalizeRichTextIdentifiers = (options) => {
3362
- for (const node of collectRichTextImageAttrs(options.content)) {
3363
- const attrs = node.attrs;
3364
- const rawIdentifier = typeof attrs.identifier === "string" ? attrs.identifier.trim() : "";
3365
- if (!rawIdentifier) continue;
3366
- const normalized = normalizeMediaIdentifierInput(rawIdentifier);
3367
- if (!normalized) {
3368
- options.errors.push(`Rich text image identifier "${rawIdentifier}" at ${node.location} is invalid.`);
3369
- delete attrs.src;
3370
- continue;
3371
- }
3372
- if (normalized !== rawIdentifier) {
3373
- options.warnings.push(`Normalized rich text image identifier "${rawIdentifier}" to "${normalized}" at ${node.location}.`);
3374
- }
3375
- attrs.identifier = normalized;
3376
- delete attrs.src;
3377
- options.existingIdentifiers.add(normalized);
3378
- withLocation(options.locationsByIdentifier, normalized, node.location);
3379
- }
3380
- };
3381
- /**
3382
- * Converts already-read rich-text sources into identifier-only content and
3383
- * closed media operations. It performs no filesystem or CMS I/O.
3384
- */
3385
- const planRichTextImageImports = (options) => {
3386
- const operations = [];
3387
- const warnings = [];
3388
- const errors = options.imports.failures.map((failure) => `Failed to import rich text image @ ${failure.location}: ${failure.error}`);
3389
- const preparedIdentifiers = new Set();
3390
- const locationsByIdentifier = new Map();
3391
- const checksumAssetsByChecksum = new Map();
3392
- normalizeRichTextIdentifiers({
3393
- content: options.content,
3394
- existingIdentifiers: options.existingIdentifiers,
3395
- warnings,
3396
- errors,
3397
- locationsByIdentifier
3398
- });
3399
- for (const imported of options.imports.successful) {
3400
- const attrs = imported.attrs;
3401
- const matches = imported.checksumMatches;
3402
- checksumAssetsByChecksum.set(imported.checksum, matches);
3403
- const existing = chooseExistingRichTextAsset(matches);
3404
- const planned = existing ? {
3405
- identifier: existing.identifier,
3406
- operation: {
3407
- kind: "unchanged",
3408
- identifier: existing.identifier,
3409
- asset: { ...existing },
3410
- file: localFileWithIdentifier(imported.file, existing.identifier),
3411
- locations: [imported.location]
3412
- }
3413
- } : (() => {
3414
- const base = chooseRichTextIdentifierBase(imported.location, imported.source);
3415
- const identifier = resolveMediaIdentifier(base, options.existingIdentifiers);
3416
- options.existingIdentifiers.add(identifier);
3417
- withLocation(locationsByIdentifier, identifier, imported.location);
3418
- const file = localFileWithIdentifier(imported.file, identifier);
3419
- return {
3420
- identifier,
3421
- operation: {
3422
- kind: "upload",
3423
- identifier,
3424
- file,
3425
- source: "rich_text",
3426
- locations: [imported.location]
3427
- }
3428
- };
3429
- })();
3430
- attrs.identifier = planned.identifier;
3431
- delete attrs.src;
3432
- options.existingIdentifiers.add(planned.identifier);
3433
- preparedIdentifiers.add(planned.identifier);
3434
- operations.push(planned.operation);
3435
- }
3436
- return {
3437
- operations,
3438
- errors,
3439
- warnings,
3440
- preparedIdentifiers,
3441
- locationsByIdentifier,
3442
- checksumAssetsByChecksum
3443
- };
3444
- };
3445
-
3446
- //#endregion
3447
- //#region src/media/content-normalization.ts
3448
- const normalizeRichTextImage = (node, assetsByIdentifier) => {
3449
- const attrs = node.attrs;
3450
- if (!isRecordValue(attrs)) return node;
3451
- const identifier = typeof attrs.identifier === "string" ? attrs.identifier : "";
3452
- if (!identifier) return node;
3453
- const normalizedAttrs = { ...attrs };
3454
- delete normalizedAttrs.src;
3455
- const asset = assetsByIdentifier.get(identifier);
3456
- if (asset) {
3457
- if (asset.width === null || asset.width === undefined) {
3458
- delete normalizedAttrs.width;
3459
- } else {
3460
- normalizedAttrs.width = asset.width;
3461
- }
3462
- if (asset.height === null || asset.height === undefined) {
3463
- delete normalizedAttrs.height;
3464
- } else {
3465
- normalizedAttrs.height = asset.height;
3466
- }
3467
- }
3468
- return {
3469
- ...node,
3470
- attrs: normalizedAttrs
3471
- };
3472
- };
3473
- const normalizeValueTree = (value, assetsByIdentifier) => {
3474
- if (Array.isArray(value)) {
3475
- return value.map((item) => normalizeValueTree(item, assetsByIdentifier));
3476
- }
3477
- if (!isRecordValue(value)) return value;
3478
- const normalizedChildren = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, normalizeValueTree(child, assetsByIdentifier)]));
3479
- if (isMediaValue(value) && typeof value.identifier === "string") {
3480
- return normalizeMediaValueToIdentifierOnly(value, value.identifier, assetsByIdentifier.get(value.identifier));
3481
- }
3482
- const normalized = normalizedChildren;
3483
- if (value.type === "image") {
3484
- return normalizeRichTextImage(normalized, assetsByIdentifier);
3485
- }
3486
- return normalized;
3487
- };
3488
- const normalizeRecord = (value, assetsByIdentifier) => {
3489
- const normalized = normalizeValueTree(value, assetsByIdentifier);
3490
- return normalized;
3491
- };
3492
- /**
3493
- * Return local content with media references in the same portable shape that
3494
- * the push diff and command compiler receive. The input is never mutated.
3495
- */
3496
- const normalizeLocalContentMedia = (content, assetsByIdentifier) => ({
3497
- ...content,
3498
- entries: new Map(Array.from(content.entries.entries(), ([contentType, entries]) => [contentType, entries.map((entry) => ({
3499
- ...entry,
3500
- data: normalizeRecord(entry.data, assetsByIdentifier),
3501
- blocks: entry.blocks ? Object.fromEntries(Object.entries(entry.blocks).map(([slotKey, blocks]) => [slotKey, blocks.map((block) => ({
3502
- ...block,
3503
- content: normalizeRecord(block.content, assetsByIdentifier)
3504
- }))])) : entry.blocks
3505
- }))])),
3506
- pages: content.pages.map((page) => ({
3507
- ...page,
3508
- blocks: page.blocks?.map((block) => ({
3509
- ...block,
3510
- data: normalizeRecord(block.data, assetsByIdentifier)
3511
- }))
3512
- })),
3513
- navigation: normalizeValueTree(content.navigation, assetsByIdentifier),
3514
- settings: normalizeValueTree(content.settings, assetsByIdentifier),
3515
- forms: content.forms.map((form) => ({
3516
- ...form,
3517
- schema: normalizeRecord(form.schema, assetsByIdentifier),
3518
- settings: normalizeRecord(form.settings, assetsByIdentifier)
3519
- })),
3520
- footer: normalizeValueTree(content.footer, assetsByIdentifier),
3521
- venues: content.venues.map((venue) => normalizeRecord(venue, assetsByIdentifier)),
3522
- eventCategories: content.eventCategories.map((category) => normalizeRecord(category, assetsByIdentifier)),
3523
- events: content.events.map((event) => normalizeRecord(event, assetsByIdentifier))
3524
- });
3525
- const addAsset = (assetsByIdentifier, identifier, asset) => {
3526
- assetsByIdentifier.set(identifier, {
3527
- ...asset,
3528
- identifier
3529
- });
3530
- };
3531
- /** Build the CMS asset lookup available while applying a prepared media plan. */
3532
- const assetsForPushMediaPlan = (options) => {
3533
- const assetsByIdentifier = new Map();
3534
- const observationsByAssetId = new Map();
3535
- for (const observation of options.observations) {
3536
- if (observation.resolvedAsset) {
3537
- addAsset(assetsByIdentifier, observation.identifier, observation.resolvedAsset);
3538
- observationsByAssetId.set(observation.resolvedAsset.id, observation.resolvedAsset);
3539
- }
3540
- const checksumAssets = observation.localChecksum.kind === "present" && observation.localChecksum.lookup.kind === "read" ? observation.localChecksum.lookup.assets : [];
3541
- for (const asset of checksumAssets) {
3542
- observationsByAssetId.set(asset.id, asset);
3543
- }
3544
- }
3545
- for (const operation of options.operations) {
3546
- if (operation.kind === "unchanged" || operation.kind === "reused") {
3547
- addAsset(assetsByIdentifier, operation.identifier, operation.asset);
3548
- continue;
3549
- }
3550
- }
3551
- return assetsByIdentifier;
3552
- };
3553
-
3554
- //#endregion
3555
- //#region src/media/push-preparation.ts
3556
- const cloneLocalContent = (content) => structuredClone(content);
3557
- const collectExistingIdentifiers = (content) => {
3558
- const identifiers = new Set();
3559
- walkLocalContentMedia(content, (value) => {
3560
- if (typeof value.identifier === "string" && value.identifier.trim()) {
3561
- identifiers.add(value.identifier.trim());
3562
- }
3563
- });
3564
- return identifiers;
3565
- };
3566
- const mergeLocations = (locations, identifier, location) => {
3567
- const current = locations.get(identifier) ?? [];
3568
- if (!current.includes(location)) current.push(location);
3569
- locations.set(identifier, current);
3570
- };
3571
- const mergeRichTextLocations = (locations, additional) => {
3572
- for (const [identifier, identifierLocations] of additional) {
3573
- for (const location of identifierLocations) {
3574
- mergeLocations(locations, identifier, location);
3575
- }
3576
- }
3577
- };
3578
- const initialAssetMap = (assetsByIdentifier) => cloneAssetMap(assetsByIdentifier);
3579
- const initialMissingSet = (missingIdentifiers) => cloneMissingSet(missingIdentifiers);
3580
- const appendClassification = (options) => {
3581
- options.operations.push(...options.classification.operations);
3582
- options.errors.push(...options.classification.errors);
3583
- options.warnings.push(...options.classification.warnings ?? []);
3584
- applyManifestClassification(options.manifest, options.identifier, options.classification);
3585
- };
3586
- const selectPushReferences = (options) => {
3587
- const collected = collectLocalMediaReferences(options.content);
3588
- const warnings = [...options.richTextPlan.warnings, ...collected.warnings];
3589
- const errors = [...options.richTextPlan.errors, ...collected.errors];
3590
- const locationsByIdentifier = new Map();
3591
- mergeRichTextLocations(locationsByIdentifier, options.richTextPlan.locationsByIdentifier);
3592
- mergeRichTextLocations(locationsByIdentifier, collected.locationsByIdentifier);
3593
- const references = [...collected.mediaReferences.map((reference) => ({
3594
- identifier: reference.identifier,
3595
- location: reference.location,
3596
- richText: false
3597
- })), ...collected.richTextReferences.map((reference) => ({
3598
- identifier: reference.identifier,
3599
- location: reference.location,
3600
- richText: true
3601
- }))];
3602
- for (const reference of references) {
3603
- mergeLocations(locationsByIdentifier, reference.identifier, reference.location);
3604
- }
3605
- const identifiers = Array.from(new Set(references.map((reference) => reference.identifier))).sort();
3606
- return {
3607
- references,
3608
- identifiers,
3609
- locationsByIdentifier,
3610
- warnings,
3611
- errors,
3612
- summary: emptyPushSummary(references.length, identifiers.length)
3613
- };
3614
- };
3615
- const prepareLocalReferenceOperations = async (options) => {
3616
- const loaded = await readMediaManifestAndLocalFiles({
3617
- contentDir: options.prepare.contentDir,
3618
- identifiers: options.selection.identifiers,
3619
- manifest: options.manifest
3620
- });
3621
- const errors = [...loaded.errors];
3622
- const warnings = [];
3623
- const resolved = await resolveMediaAssets(options.prepare.client, options.selection.identifiers);
3624
- const initialAssets = initialAssetMap(resolved.assetsByIdentifier);
3625
- const initialMissing = initialMissingSet(resolved.missingIdentifiers);
3626
- const checksumMatches = await readChecksumMatches({
3627
- client: options.prepare.client,
3628
- identifiers: options.selection.identifiers,
3629
- localFiles: loaded.localFiles,
3630
- assetsByIdentifier: resolved.assetsByIdentifier,
3631
- missingIdentifiers: resolved.missingIdentifiers,
3632
- preparedIdentifiers: options.richTextPlan.preparedIdentifiers
3633
- });
3634
- const operations = [];
3635
- for (const identifier of options.selection.identifiers) {
3636
- if (options.richTextPlan.preparedIdentifiers.has(identifier)) continue;
3637
- const localFile = loaded.localFiles.get(identifier);
3638
- const classification = classifyLocalMediaReference({
3639
- identifier,
3640
- asset: resolved.assetsByIdentifier.get(identifier),
3641
- localFile,
3642
- missing: resolved.missingIdentifiers.has(identifier),
3643
- checksumMatches: localFile ? checksumMatches.get(localFile.checksum) ?? [] : [],
3644
- locations: locationsForIdentifier(options.selection.locationsByIdentifier, identifier),
3645
- locationNote: buildLocationNote(options.selection.locationsByIdentifier, identifier)
3646
- });
3647
- appendClassification({
3648
- identifier,
3649
- classification,
3650
- manifest: options.manifest,
3651
- operations,
3652
- errors,
3653
- warnings
3654
- });
3655
- }
3656
- const sortedOperations = sortOperations(operations);
3657
- const localChecksums = localFileChecksums(options.selection.identifiers, loaded.localFiles);
3658
- for (const operation of sortedOperations) {
3659
- if ("file" in operation && operation.file) {
3660
- localChecksums.set(operation.identifier, operation.file.checksum);
3661
- }
3662
- }
3663
- const checksumAssetsByChecksum = new Map(options.richTextPlan.checksumAssetsByChecksum);
3664
- for (const [checksum, matches] of checksumMatches) {
3665
- checksumAssetsByChecksum.set(checksum, matches);
3666
- }
3667
- return {
3668
- operations: sortedOperations,
3669
- errors,
3670
- warnings,
3671
- mediaReadSet: buildMediaReadSet({
3672
- identifiers: options.selection.identifiers,
3673
- localChecksums,
3674
- initialAssets,
3675
- initialMissing,
3676
- checksumAssetsByChecksum
3677
- })
3678
- };
3679
- };
3680
- async function prepareLocalMediaForPush(options) {
3681
- const content = cloneLocalContent(options.content);
3682
- const manifest = await readMediaManifest(options.contentDir);
3683
- const existingIdentifiers = collectExistingIdentifiers(content);
3684
- const richTextReads = await readRichTextImageImports({
3685
- content,
3686
- client: options.client
3687
- });
3688
- const richTextPlan = planRichTextImageImports({
3689
- content,
3690
- imports: richTextReads,
3691
- existingIdentifiers
3692
- });
3693
- const selection = selectPushReferences({
3694
- content,
3695
- richTextPlan
3696
- });
3697
- const prepared = selection.identifiers.length > 0 ? await prepareLocalReferenceOperations({
3698
- prepare: options,
3699
- manifest,
3700
- selection,
3701
- richTextPlan
3702
- }) : {
3703
- operations: [],
3704
- errors: [],
3705
- warnings: [],
3706
- mediaReadSet: []
3707
- };
3708
- const contentWithPortableMedia = normalizeLocalContentMedia(content, assetsForPushMediaPlan({
3709
- operations: [...richTextPlan.operations, ...prepared.operations],
3710
- observations: prepared.mediaReadSet
3711
- }));
3712
- return {
3713
- content: contentWithPortableMedia,
3714
- manifest,
3715
- operations: sortOperations([...richTextPlan.operations, ...prepared.operations]),
3716
- references: selection.references,
3717
- summary: selection.summary,
3718
- warnings: [...selection.warnings, ...prepared.warnings],
3719
- errors: [...selection.errors, ...prepared.errors],
3720
- targetEnv: options.targetEnv,
3721
- mediaReadSet: prepared.mediaReadSet
3722
- };
3723
- }
3724
-
3725
- //#endregion
3726
- //#region src/media/push-failure.ts
3727
- const includesAny = (value, needles) => needles.some((needle) => value.includes(needle));
3728
- const managementErrorRules = [
3729
- {
3730
- kind: "max_object_size",
3731
- matches: (error) => {
3732
- const code = error.code.toLowerCase();
3733
- const message = error.message.toLowerCase();
3734
- return error.statusCode === 413 || code.includes("too_large") || message.includes("too large");
3735
- }
3736
- },
3737
- {
3738
- kind: "unsupported_mime",
3739
- matches: (error) => {
3740
- const code = error.code.toLowerCase();
3741
- const message = error.message.toLowerCase();
3742
- return error.statusCode === 415 || includesAny(code, ["mime", "content-type"]) || includesAny(message, ["mime", "content-type"]);
3743
- }
3744
- },
3745
- {
3746
- kind: "quota_or_storage_limit",
3747
- matches: (error) => {
3748
- const code = error.code.toLowerCase();
3749
- const message = error.message.toLowerCase();
3750
- return error.statusCode === 402 || error.statusCode === 507 || includesAny(code, ["quota", "storage"]) || includesAny(message, ["quota", "storage limit"]);
3751
- }
3752
- },
3753
- {
3754
- kind: "auth_or_permission",
3755
- matches: (error) => {
3756
- const code = error.code.toLowerCase();
3757
- return error.statusCode === 401 || error.statusCode === 403 || includesAny(code, ["auth", "permission"]);
3758
- }
3759
- },
3760
- {
3761
- kind: "network",
3762
- matches: (error) => {
3763
- const code = error.code.toLowerCase();
3764
- return code === "sdk:network-error" || code === "sdk:timeout";
3765
- }
3766
- }
3767
- ];
3768
- const genericErrorRules = [
3769
- {
3770
- kind: "network",
3771
- matches: (error) => includesAny(error.message.toLowerCase(), [
3772
- "network",
3773
- "timeout",
3774
- "timed out"
3775
- ])
3776
- },
3777
- {
3778
- kind: "max_object_size",
3779
- matches: (error) => includesAny(error.message.toLowerCase(), [
3780
- "too large",
3781
- "payload too large",
3782
- "413"
3783
- ])
3784
- },
3785
- {
3786
- kind: "unsupported_mime",
3787
- matches: (error) => includesAny(error.message.toLowerCase(), [
3788
- "mime",
3789
- "content-type",
3790
- "unsupported media"
3791
- ])
3792
- },
3793
- {
3794
- kind: "quota_or_storage_limit",
3795
- matches: (error) => includesAny(error.message.toLowerCase(), ["quota", "storage limit"])
3796
- },
3797
- {
3798
- kind: "auth_or_permission",
3799
- matches: (error) => includesAny(error.message.toLowerCase(), [
3800
- "unauthorized",
3801
- "forbidden",
3802
- "permission"
3803
- ])
3804
- }
3805
- ];
3806
- const classifyWithRules = (error, rules) => rules.find((rule) => rule.matches(error))?.kind;
3807
- const classifyManagementApiError = (error) => {
3808
- const classified = classifyWithRules(error, managementErrorRules);
3809
- if (classified) return classified;
3810
- if (error.code.toLowerCase().startsWith("sdk:") || typeof error.statusCode === "number") {
3811
- return "cms_api";
3812
- }
3813
- return "unknown";
3814
- };
3815
- const classifyErrorMessage = (error) => classifyWithRules(error, genericErrorRules) ?? "unknown";
3816
- function classifyLocalMediaUploadFailure(error) {
3817
- if (error instanceof ManagementApiError) {
3818
- return classifyManagementApiError(error);
3819
- }
3820
- if (error instanceof Error) {
3821
- return classifyErrorMessage(error);
3822
- }
3823
- return "unknown";
3824
- }
3825
-
3826
- //#endregion
3827
- //#region src/media/push-application.ts
3828
- const rawErrorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
3829
- const makeReport = (params) => {
3830
- const failedIdentifiers = params.items.filter((item) => item.status === "failed").map((item) => item.identifier);
3831
- const missingIdentifiers = params.items.filter((item) => item.status === "missing_local_file").map((item) => item.identifier);
3832
- return {
3833
- targetEnv: params.targetEnv,
3834
- summary: { ...params.summary },
3835
- items: params.items,
3836
- failedIdentifiers,
3837
- missingIdentifiers
3838
- };
3839
- };
3840
- const reportForOperation = (operation, status, targetEnv, message, failureKind, rawError) => {
3841
- const file = "file" in operation ? operation.file : undefined;
3842
- return {
3843
- identifier: operation.identifier,
3844
- status,
3845
- targetEnv,
3846
- ...file ? {
3847
- filename: file.filename,
3848
- sizeBytes: file.dataLength,
3849
- mimeType: file.mimeType
3850
- } : {},
3851
- locations: operation.locations,
3852
- message,
3853
- failureKind,
3854
- rawError
3855
- };
3856
- };
3857
- const applySimpleMediaOperation = (operation, state, targetEnv) => {
3858
- switch (operation.kind) {
3859
- case "unchanged":
3860
- state.assetsByIdentifier.set(operation.identifier, operation.asset);
3861
- state.summary.resolved += 1;
3862
- state.reportItems.push(reportForOperation(operation, "resolved", targetEnv));
3863
- return;
3864
- case "reused":
3865
- state.assetsByIdentifier.set(operation.identifier, operation.asset);
3866
- state.summary.resolved += 1;
3867
- state.summary.reused += 1;
3868
- state.reportItems.push(reportForOperation(operation, "reused", targetEnv, "Reused existing CMS asset with matching identifier and checksum."));
3869
- return;
3870
- case "skipped":
3871
- state.summary.skipped += 1;
3872
- state.reportItems.push(reportForOperation(operation, "skipped", targetEnv, operation.message));
3873
- return;
3874
- case "missing_local_file":
3875
- state.reportItems.push(reportForOperation(operation, "missing_local_file", targetEnv, operation.message));
3876
- return;
3877
- case "conflict":
3878
- state.reportItems.push(reportForOperation(operation, "failed", targetEnv, operation.message, "unknown"));
3879
- return;
3880
- default: return assertNever(operation);
3881
- }
3882
- };
3883
- const applyUpload = async (operation, context, state) => {
3884
- const pendingItem = reportForOperation(operation, "pending_upload", context.targetEnv);
3885
- if (context.dryRun) {
3886
- state.summary.pendingUploads += 1;
3887
- state.reportItems.push(pendingItem);
3888
- return;
3889
- }
3890
- context.onEvent?.({
3891
- type: "upload_started",
3892
- item: pendingItem
3893
- });
3894
- try {
3895
- const result = await context.client.media.upload({
3896
- data: Buffer.from(operation.file.dataBase64, "base64"),
3897
- filename: operation.file.filename,
3898
- contentType: operation.file.mimeType,
3899
- identifier: operation.identifier
3900
- });
3901
- state.uploadedIdentifiers.push(operation.identifier);
3902
- if (result.status === "created") state.summary.uploaded += 1;
3903
- state.summary.resolved += 1;
3904
- const uploadedItem = reportForOperation(operation, "uploaded", context.targetEnv, result.status === "created" ? "Uploaded local media file." : "CMS reported media already exists.");
3905
- state.reportItems.push(uploadedItem);
3906
- context.onEvent?.({
3907
- type: "upload_succeeded",
3908
- item: uploadedItem
3909
- });
3910
- } catch (error) {
3911
- const message = rawErrorMessage$1(error);
3912
- const failedItem = reportForOperation(operation, "failed", context.targetEnv, `Failed to upload "${operation.identifier}": ${message}`, classifyLocalMediaUploadFailure(error), message);
3913
- state.reportItems.push(failedItem);
3914
- state.errors.push(`Failed to upload "${operation.identifier}" (${operation.file.filename}, ${operation.file.mimeType}): ${message}`);
3915
- context.onEvent?.({
3916
- type: "upload_failed",
3917
- item: failedItem
3918
- });
3919
- }
3920
- };
3921
- const applyMediaOperation = async (operation, context, state) => {
3922
- switch (operation.kind) {
3923
- case "unchanged":
3924
- case "reused":
3925
- case "skipped":
3926
- case "missing_local_file":
3927
- case "conflict":
3928
- applySimpleMediaOperation(operation, state, context.targetEnv);
3929
- return;
3930
- case "upload":
3931
- await applyUpload(operation, context, state);
3932
- return;
3933
- default: return assertNever(operation);
3934
- }
3935
- };
3936
- const createApplyState = (plan) => ({
3937
- summary: { ...plan.summary },
3938
- errors: [...plan.errors],
3939
- reportItems: [],
3940
- assetsByIdentifier: new Map(),
3941
- uploadedIdentifiers: []
3942
- });
3943
- const applyMediaOperations = async (operations, context, state) => {
3944
- for (const operation of operations) {
3945
- await applyMediaOperation(operation, context, state);
3946
- }
3947
- };
3948
- const resolveUploadedAssets = async (state, client, dryRun) => {
3949
- if (dryRun || state.uploadedIdentifiers.length === 0) return;
3950
- const resolvedUploads = await client.media.resolve({ identifiers: state.uploadedIdentifiers });
3951
- for (const asset of resolvedUploads.assets) {
3952
- if (asset.identifier) {
3953
- state.assetsByIdentifier.set(asset.identifier, asset);
3954
- }
3955
- }
3956
- for (const missing of resolvedUploads.missingIdentifiers) {
3957
- state.errors.push(`Upload succeeded but identifier "${missing}" was not found on the CMS.`);
3958
- }
3959
- };
3960
- const fileByIdentifier = (operations) => {
3961
- const files = new Map();
3962
- for (const operation of operations) {
3963
- if ("file" in operation && !files.has(operation.identifier)) {
3964
- files.set(operation.identifier, operation.file);
3965
- }
3966
- }
3967
- return files;
3968
- };
3969
- const rewriteAppliedReferences = (plan, state) => {
3970
- const content = normalizeLocalContentMedia(plan.content, state.assetsByIdentifier);
3971
- const manifest = structuredClone(plan.manifest);
3972
- const files = fileByIdentifier(plan.operations);
3973
- for (const reference of plan.references) {
3974
- const asset = state.assetsByIdentifier.get(reference.identifier);
3975
- if (!asset) continue;
3976
- updateManifestEntry(manifest, reference.identifier, asset, files.get(reference.identifier));
3977
- }
3978
- return {
3979
- content,
3980
- manifest
3981
- };
3982
- };
3983
- const finalSummary = (summary, dryRun) => ({
3984
- ...summary,
3985
- pendingUploads: dryRun ? summary.pendingUploads : 0
3986
- });
3987
- async function applyLocalMediaPush(options) {
3988
- const { plan, client, dryRun, onEvent } = options;
3989
- const state = createApplyState(plan);
3990
- const context = {
3991
- client,
3992
- dryRun,
3993
- targetEnv: plan.targetEnv,
3994
- onEvent
3995
- };
3996
- await applyMediaOperations(plan.operations, context, state);
3997
- await resolveUploadedAssets(state, client, dryRun);
3998
- const rewritten = rewriteAppliedReferences(plan, state);
3999
- const summary = finalSummary(state.summary, dryRun);
4000
- const hasChanges = summary.uploaded > 0 || summary.pendingUploads > 0;
4001
- return {
4002
- ...rewritten,
4003
- summary,
4004
- report: makeReport({
4005
- targetEnv: plan.targetEnv,
4006
- summary,
4007
- items: state.reportItems
4008
- }),
4009
- warnings: [...plan.warnings],
4010
- errors: state.errors,
4011
- hasChanges
4012
- };
4013
- }
4014
-
4015
- //#endregion
4016
- //#region src/media/pull-assets.ts
4017
- const buildPullAssetIndexes = (resolved) => {
4018
- const assetsById = new Map(resolved.assets.map((asset) => [asset.id, asset]));
4019
- const assetsByIdentifier = new Map();
4020
- for (const asset of resolved.assets) {
4021
- assetsByIdentifier.set(asset.identifier, asset);
4022
- }
4023
- return {
4024
- assetsById,
4025
- assetsByIdentifier
4026
- };
4027
- };
4028
- const resolveReferencedAsset = (reference, indexes) => {
4029
- const byId = reference.value.assetId ? indexes.assetsById.get(reference.value.assetId) : undefined;
4030
- if (byId) return byId;
4031
- return reference.value.identifier ? indexes.assetsByIdentifier.get(reference.value.identifier) : undefined;
4032
- };
4033
- const normalizePullIdentifier = (options) => {
4034
- const { initial, location, existingIdentifiers } = options;
4035
- if (!initial) return {
4036
- identifier: null,
4037
- canonicalized: 0
4038
- };
4039
- const normalized = slugifyMediaIdentifier(initial);
4040
- if (!normalized) {
4041
- return {
4042
- identifier: initial,
4043
- canonicalized: 0,
4044
- warning: `Could not normalize identifier "${initial}" at ${location}.`
4045
- };
4046
- }
4047
- existingIdentifiers.add(normalized);
4048
- return {
4049
- identifier: normalized,
4050
- canonicalized: normalized === initial ? 0 : 1
4051
- };
4052
- };
4053
- const planPullAssetReference = (options) => {
4054
- const resolvedAsset = resolveReferencedAsset(options.reference, options.indexes);
4055
- let canonicalized = 0;
4056
- let identifier = resolvedAsset?.identifier ?? options.reference.value.identifier ?? null;
4057
- const normalized = normalizePullIdentifier({
4058
- initial: identifier,
4059
- location: options.reference.location,
4060
- existingIdentifiers: options.existingIdentifiers
4061
- });
4062
- canonicalized += normalized.canonicalized;
4063
- identifier = normalized.identifier;
4064
- if (normalized.warning && normalized.identifier) {
4065
- return {
4066
- plan: {
4067
- kind: "identified",
4068
- reference: options.reference,
4069
- resolvedAsset,
4070
- identifier: normalized.identifier
4071
- },
4072
- canonicalized,
4073
- warning: normalized.warning
4074
- };
4075
- }
4076
- if (resolvedAsset && identifier) {
4077
- options.indexes.assetsByIdentifier.set(identifier, resolvedAsset);
4078
- }
4079
- return {
4080
- plan: identifier ? {
4081
- kind: "identified",
4082
- reference: options.reference,
4083
- resolvedAsset,
4084
- identifier
4085
- } : {
4086
- kind: "unidentified",
4087
- reference: options.reference,
4088
- resolvedAsset,
4089
- identifier: null
4090
- },
4091
- canonicalized
4092
- };
4093
- };
4094
- const planPullAssetIdentifiers = (options) => {
4095
- const indexes = buildPullAssetIndexes(options.resolved);
4096
- const assetsById = new Map(indexes.assetsById);
4097
- const assetsByIdentifier = new Map(indexes.assetsByIdentifier);
4098
- const existingIdentifiers = new Set(options.identifiers);
4099
- for (const asset of options.resolved.assets) {
4100
- existingIdentifiers.add(asset.identifier);
4101
- }
4102
- let referencesCanonicalized = 0;
4103
- const warnings = [];
4104
- const references = [];
4105
- for (const reference of options.references) {
4106
- const planned = planPullAssetReference({
4107
- reference,
4108
- indexes: {
4109
- assetsById,
4110
- assetsByIdentifier
4111
- },
4112
- existingIdentifiers
4113
- });
4114
- referencesCanonicalized += planned.canonicalized;
4115
- if (planned.warning) warnings.push(planned.warning);
4116
- references.push(planned.plan);
4117
- }
4118
- return {
4119
- indexes: {
4120
- assetsById,
4121
- assetsByIdentifier
4122
- },
4123
- references,
4124
- summary: { referencesCanonicalized },
4125
- warnings
4126
- };
4127
- };
4128
- const applyPullAssetPlan = (options) => {
4129
- const assetsById = new Map(options.plan.indexes.assetsById);
4130
- const assetsByIdentifier = new Map(options.plan.indexes.assetsByIdentifier);
4131
- const references = [];
4132
- for (const planned of options.plan.references) {
4133
- if (planned.kind === "unidentified") {
4134
- references.push(planned);
4135
- continue;
4136
- }
4137
- planned.reference.value.identifier = planned.identifier;
4138
- if (planned.resolvedAsset) {
4139
- updateManifestEntry(options.manifest, planned.identifier, planned.resolvedAsset, null);
4140
- }
4141
- references.push(planned);
4142
- }
4143
- return {
4144
- indexes: {
4145
- assetsById,
4146
- assetsByIdentifier
4147
- },
4148
- references
4149
- };
4150
- };
4151
-
4152
- //#endregion
4153
- //#region src/media/pull-references.ts
4154
- /**
4155
- * Discover ordinary media values and normalize rich-text nodes that already
4156
- * have an identifier. Rich-text nodes with only a source URL are returned as
4157
- * explicit import intents; importing is an effectful stage owned by the
4158
- * workflow, not by content traversal.
4159
- */
4160
- const collectPullMediaReferences = (content) => {
4161
- const references = [];
4162
- const identifiers = new Set();
4163
- const assetIds = new Set();
4164
- const warnings = [];
4165
- walkPulledContentMedia(content, (value, location) => {
4166
- references.push({
4167
- value,
4168
- location
4169
- });
4170
- if (typeof value.identifier === "string") {
4171
- identifiers.add(value.identifier);
4172
- }
4173
- if (typeof value.assetId === "string") {
4174
- assetIds.add(value.assetId);
4175
- }
4176
- });
4177
- const richTextImports = [];
4178
- let referencesCanonicalized = 0;
4179
- const richTextNodes = collectRichTextImageAttrs(content);
4180
- for (const node of richTextNodes) {
4181
- const attrs = node.attrs;
4182
- const rawIdentifier = typeof attrs.identifier === "string" ? attrs.identifier.trim() : "";
4183
- const rawSrc = typeof attrs.src === "string" ? attrs.src.trim() : "";
4184
- if (rawIdentifier) {
4185
- const normalized = normalizeMediaIdentifierInput(rawIdentifier);
4186
- if (!normalized) {
4187
- warnings.push(`Invalid rich text image identifier "${rawIdentifier}" at ${node.location}.`);
4188
- continue;
4189
- }
4190
- if (normalized !== rawIdentifier) {
4191
- attrs.identifier = normalized;
4192
- referencesCanonicalized += 1;
4193
- }
4194
- delete attrs.src;
4195
- identifiers.add(normalized);
4196
- references.push({
4197
- value: {
4198
- type: "image",
4199
- identifier: normalized
4200
- },
4201
- location: node.location
4202
- });
4203
- continue;
4204
- }
4205
- if (rawSrc) {
4206
- richTextImports.push({
4207
- attrs,
4208
- location: node.location,
4209
- src: rawSrc
4210
- });
4211
- }
4212
- }
4213
- return {
4214
- references,
4215
- identifiers,
4216
- assetIds,
4217
- richTextImports,
4218
- referencesCanonicalized,
4219
- cmsImportsPlanned: 0,
4220
- cmsAssetsImported: 0,
4221
- warnings
4222
- };
4223
- };
4224
- const recordRichTextImport = (collection, intent, identifier) => {
4225
- intent.attrs.identifier = identifier;
4226
- delete intent.attrs.src;
4227
- collection.identifiers.add(identifier);
4228
- const reference = {
4229
- value: {
4230
- type: "image",
4231
- identifier
4232
- },
4233
- location: intent.location
4234
- };
4235
- collection.references.push(reference);
4236
- return {
4237
- identifier,
4238
- reference
4239
- };
4240
- };
4241
-
4242
- //#endregion
4243
- //#region src/media/pull-rich-text.ts
4244
- const chooseChecksumIdentifier = (candidates) => {
4245
- const chosen = candidates.slice().sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
4246
- if (!chosen) return null;
4247
- return chosen.identifier;
4248
- };
4249
- const assignOrUploadRichTextIdentifier = async (options) => {
4250
- for (let attempt = 0; attempt < 10; attempt += 1) {
4251
- const candidate = resolveMediaIdentifier(options.base, options.existingIdentifiers);
4252
- options.existingIdentifiers.add(candidate);
4253
- if (options.dryRun) return {
4254
- kind: "planned-upload",
4255
- identifier: candidate
4256
- };
4257
- try {
4258
- const uploaded = await options.client.media.upload({
4259
- data: options.data,
4260
- filename: options.filename,
4261
- contentType: options.mimeType,
4262
- identifier: candidate
4263
- });
4264
- return {
4265
- kind: "uploaded",
4266
- identifier: uploaded.identifier
4267
- };
4268
- } catch {
4269
- continue;
4270
- }
4271
- }
4272
- throw new Error("Unable to assign a unique identifier for rich text image import");
4273
- };
4274
- const importRichTextImageSrcToIdentifier = async (options) => {
4275
- const { client, src, location, existingIdentifiers, dryRun } = options;
4276
- const downloaded = await downloadMedia(src);
4277
- if (!downloaded) {
4278
- throw new Error(`Failed to download rich text image from ${src}`);
4279
- }
4280
- const mimeType = downloaded.contentType.split(";")[0]?.trim() || "application/octet-stream";
4281
- if (!mimeType.startsWith("image/")) {
4282
- throw new Error(`Unsupported content-type for rich text image import: ${mimeType}`);
4283
- }
4284
- const checksum = sha256(downloaded.data);
4285
- const matches = await client.media.findByChecksum([checksum]);
4286
- const candidates = matches.filter((asset) => asset.checksumSha256 === checksum);
4287
- const existingIdentifier = chooseChecksumIdentifier(candidates);
4288
- if (existingIdentifier) return {
4289
- kind: "existing",
4290
- identifier: existingIdentifier
4291
- };
4292
- return assignOrUploadRichTextIdentifier({
4293
- client,
4294
- base: chooseRichTextIdentifierBase(location, src),
4295
- existingIdentifiers,
4296
- dryRun,
4297
- data: downloaded.data,
4298
- filename: ensureFilenameExtension(parseFilenameFromUrl(src) ?? "image", mimeType),
4299
- mimeType
4300
- });
4301
- };
4302
- async function importRichTextPullReferences(options) {
4303
- const { collection, client, dryRun, allowCmsWrites } = options;
4304
- const richTextExisting = new Set(collection.identifiers);
4305
- for (const intent of collection.richTextImports) {
4306
- try {
4307
- const identifier = await importRichTextImageSrcToIdentifier({
4308
- client,
4309
- src: intent.src,
4310
- location: intent.location,
4311
- existingIdentifiers: richTextExisting,
4312
- dryRun: dryRun || !allowCmsWrites
4313
- });
4314
- if (identifier.kind === "planned-upload") {
4315
- collection.cmsImportsPlanned += 1;
4316
- if (!dryRun && !allowCmsWrites) continue;
4317
- }
4318
- if (identifier.kind === "uploaded") {
4319
- collection.cmsAssetsImported += 1;
4320
- }
4321
- recordRichTextImport(collection, intent, identifier.identifier);
4322
- richTextExisting.add(identifier.identifier);
4323
- } catch (error) {
4324
- const message = error instanceof Error ? error.message : String(error);
4325
- collection.warnings.push(`Failed to import rich text image @ ${intent.location}: ${message}`);
4326
- }
4327
- }
4328
- }
4329
-
4330
- //#endregion
4331
- //#region src/media/pull-download.ts
4332
- const planLocalMediaDownload = (options) => {
4333
- const { reference, asset, identifier, existingEntry, local } = options;
4334
- if (!asset) return {
4335
- kind: "missing-asset",
4336
- reference
4337
- };
4338
- const filename = buildLocalFilename(identifier, asset);
4339
- if (local.exists) {
4340
- if (asset.checksumSha256) {
4341
- if (existingEntry?.checksum === asset.checksumSha256) {
4342
- return {
4343
- kind: "skip",
4344
- reference,
4345
- asset,
4346
- identifier,
4347
- filename
4348
- };
4349
- }
4350
- if (local.checksum === asset.checksumSha256) {
4351
- return {
4352
- kind: "skip",
4353
- reference,
4354
- asset,
4355
- identifier,
4356
- filename
4357
- };
4358
- }
4359
- } else if (existingEntry?.checksum) {
4360
- return {
4361
- kind: "skip",
4362
- reference,
4363
- asset,
4364
- identifier,
4365
- filename
4366
- };
4367
- }
4368
- }
4369
- return {
4370
- kind: "download",
4371
- reference,
4372
- asset,
4373
- identifier,
4374
- filename
4375
- };
4376
- };
4377
- const readExistingLocalMedia = async (options) => {
4378
- const exists = await localMediaFileExists(options.localFilePath);
4379
- if (!exists) return { exists: false };
4380
- if (!options.asset.checksumSha256) {
4381
- return { exists: true };
4382
- }
4383
- const manifestChecksum = options.existingEntry?.checksum;
4384
- if (manifestChecksum === options.asset.checksumSha256) {
4385
- return {
4386
- exists: true,
4387
- checksum: manifestChecksum
4388
- };
4389
- }
4390
- const localChecksum = manifestChecksum ?? await sha256FromFile(options.localFilePath);
4391
- return {
4392
- exists: true,
4393
- checksum: localChecksum
4394
- };
4395
- };
4396
- const removeStaleLocalFile = async (options) => {
4397
- if (options.dryRun || !options.previousFilename || options.previousFilename === options.filename) {
4398
- return;
4399
- }
4400
- const previousPath = path$1.join(options.contentDir, "media", options.previousFilename);
4401
- if (await localMediaFileExists(previousPath)) {
4402
- await fs$1.unlink(previousPath).catch(() => {});
4403
- }
4404
- };
4405
- const applySkipDecision = (decision, context, local, existingEntry) => {
4406
- if (decision.asset.checksumSha256 && local.checksum === decision.asset.checksumSha256 && existingEntry?.checksum !== decision.asset.checksumSha256) {
4407
- context.manifest.assets[decision.identifier] = {
4408
- ...existingEntry,
4409
- checksum: local.checksum,
4410
- filename: decision.filename,
4411
- mimeType: decision.asset.mimeType ?? existingEntry?.mimeType
4412
- };
4413
- }
4414
- context.summary.skipped += 1;
4415
- updateManifestEntry(context.manifest, decision.identifier, decision.asset, null);
4416
- };
4417
- const applyDownloadDecision = async (decision, context) => {
4418
- context.summary.pendingDownloads += 1;
4419
- if (context.dryRun) return;
4420
- const relativePath = normalizeStoragePath(decision.asset.storagePath, context.siteId);
4421
- const urlResult = buildStorageUrl(context.supabaseUrl, relativePath, context.siteId, decision.asset.storageBucket ?? "media");
4422
- if (!urlResult.ok) {
4423
- context.warnings.push(`Invalid storage path for "${decision.identifier}": ${urlResult.error.cause.tag}.`);
4424
- return;
4425
- }
4426
- const downloaded = await downloadMedia(urlResult.value);
4427
- if (!downloaded) {
4428
- context.warnings.push(`Failed to download media for "${decision.identifier}" from ${urlResult.value}.`);
4429
- return;
4430
- }
4431
- const written = await writeLocalMediaFile(context.contentDir, decision.filename, downloaded.data);
4432
- context.warnings.push(...localMediaWriteWarningMessages(decision.identifier, written));
4433
- context.manifest.assets[decision.identifier] = {
4434
- checksum: sha256(downloaded.data),
4435
- filename: decision.filename,
4436
- mimeType: decision.asset.mimeType
4437
- };
4438
- context.summary.downloaded += 1;
4439
- };
4440
- const syncPullMediaDownload = async (planned, indexes, context) => {
4441
- const { reference, identifier } = planned;
4442
- const byId = reference.value.assetId ? indexes.assetsById.get(reference.value.assetId) : undefined;
4443
- const asset = byId ?? indexes.assetsByIdentifier.get(identifier);
4444
- if (!asset) {
4445
- context.warnings.push(`Unable to resolve asset for "${identifier}" at ${reference.location}.`);
4446
- return;
4447
- }
4448
- const filename = buildLocalFilename(identifier, asset);
4449
- const localFilePath = path$1.join(context.contentDir, "media", filename);
4450
- const existingEntry = context.manifest.assets[identifier];
4451
- await removeStaleLocalFile({
4452
- contentDir: context.contentDir,
4453
- previousFilename: existingEntry?.filename,
4454
- filename,
4455
- dryRun: context.dryRun
4456
- });
4457
- const local = await readExistingLocalMedia({
4458
- localFilePath,
4459
- existingEntry,
4460
- asset
4461
- });
4462
- const decision = planLocalMediaDownload({
4463
- reference,
4464
- asset,
4465
- identifier,
4466
- existingEntry,
4467
- local
4468
- });
4469
- switch (decision.kind) {
4470
- case "missing-asset":
4471
- context.warnings.push(`Unable to resolve asset for "${identifier}" at ${reference.location}.`);
4472
- return;
4473
- case "skip":
4474
- applySkipDecision(decision, context, local, existingEntry);
4475
- return;
4476
- case "download": await applyDownloadDecision(decision, context);
4477
- }
4478
- };
4479
- const syncPullMediaDownloads = async (options) => {
4480
- for (const planned of options.references) {
4481
- if (planned.kind === "identified") {
4482
- await syncPullMediaDownload(planned, options.indexes, options);
4483
- }
4484
- }
4485
- if (!options.dryRun) options.summary.pendingDownloads = 0;
4486
- };
4487
- const sha256FromFile = (filePath) => readLocalMediaChecksum(filePath, FILE_SIZE_LIMITS.rawVideoBeforeCompression);
4488
- function localMediaWriteWarningMessages(identifier, outcome) {
4489
- if (outcome.kind === "written") return [];
4490
- return outcome.warnings.map((warning) => {
4491
- switch (warning.tag) {
4492
- case "temporary_cleanup_failed": return `Downloaded media for "${identifier}", but could not remove temporary file ${warning.temporaryPath}.`;
4493
- case "directory_sync_failed": return `Downloaded media for "${identifier}", but could not confirm the destination directory was synced.`;
4494
- }
4495
- });
4496
- }
4497
-
4498
- //#endregion
4499
- //#region src/media/pull-workflow.ts
4500
- async function syncLocalMediaForPull(options) {
4501
- const { contentDir, content, client, dryRun, allowCmsWrites = true, supabaseUrl, siteId, downloadFiles } = options;
4502
- const manifest = await readMediaManifest(contentDir);
4503
- const collection = collectPullMediaReferences(content);
4504
- await importRichTextPullReferences({
4505
- collection,
4506
- client,
4507
- dryRun,
4508
- allowCmsWrites
4509
- });
4510
- const summary = {
4511
- totalReferences: collection.references.length,
4512
- referencesCanonicalized: collection.referencesCanonicalized,
4513
- cmsImportsPlanned: collection.cmsImportsPlanned,
4514
- cmsAssetsImported: collection.cmsAssetsImported,
4515
- downloaded: 0,
4516
- skipped: 0,
4517
- pendingDownloads: 0
4518
- };
4519
- const warnings = [...collection.warnings];
4520
- if (collection.references.length === 0) {
4521
- return {
4522
- content,
4523
- manifest,
4524
- summary,
4525
- warnings
4526
- };
4527
- }
4528
- const resolved = await client.media.resolve({
4529
- ...collection.identifiers.size > 0 ? { identifiers: Array.from(collection.identifiers) } : {},
4530
- ...collection.assetIds.size > 0 ? { assetIds: Array.from(collection.assetIds) } : {}
4531
- });
4532
- const assetPlan = planPullAssetIdentifiers({
4533
- references: collection.references,
4534
- resolved,
4535
- identifiers: collection.identifiers
4536
- });
4537
- warnings.push(...assetPlan.warnings);
4538
- summary.referencesCanonicalized += assetPlan.summary.referencesCanonicalized;
4539
- const applied = applyPullAssetPlan({
4540
- plan: assetPlan,
4541
- manifest
4542
- });
4543
- if (!downloadFiles) {
4544
- return {
4545
- content,
4546
- manifest,
4547
- summary,
4548
- warnings
4549
- };
4550
- }
4551
- if (!supabaseUrl || !siteId) {
4552
- throw new Error("syncLocalMediaForPull requires supabaseUrl and siteId when downloadFiles=true.");
4553
- }
4554
- await syncPullMediaDownloads({
4555
- contentDir,
4556
- manifest,
4557
- references: applied.references,
4558
- indexes: applied.indexes,
4559
- supabaseUrl,
4560
- siteId,
4561
- dryRun,
4562
- summary,
4563
- warnings
4564
- });
4565
- return {
4566
- content,
4567
- manifest,
4568
- summary,
4569
- warnings
4570
- };
4571
- }
4572
-
4573
- //#endregion
4574
- //#region src/media/local-sync.ts
4575
- const rawErrorMessage = (error) => error instanceof Error ? error.message : String(error);
4576
- async function revalidateLocalMediaPushPlan(plan, client) {
4577
- const identifiers = plan.mediaReadSet.map((observation) => observation.identifier);
4578
- if (identifiers.length === 0) return { ok: true };
4579
- try {
4580
- const resolved = await client.media.resolve({ identifiers });
4581
- const assets = new Map(resolved.assets.map((asset) => [asset.identifier, asset]));
4582
- const localChecksums = new Map(plan.mediaReadSet.map((observation) => [observation.identifier, observation.localChecksum.kind === "present" ? observation.localChecksum.checksum : undefined]));
4583
- const observedChecksums = Array.from(new Set(plan.mediaReadSet.flatMap((observation) => observation.localChecksum.kind === "present" && observation.localChecksum.lookup.kind === "read" ? [observation.localChecksum.checksum] : [])));
4584
- const currentChecksumAssets = observedChecksums.length > 0 ? await client.media.findByChecksum(observedChecksums) : [];
4585
- const checksumAssetsByChecksum = new Map(observedChecksums.map((checksum) => [checksum, []]));
4586
- for (const asset of currentChecksumAssets) {
4587
- if (!asset.checksumSha256) continue;
4588
- const matches = checksumAssetsByChecksum.get(asset.checksumSha256);
4589
- if (!matches) continue;
4590
- matches.push(asset);
4591
- }
4592
- const current = buildMediaReadSet({
4593
- identifiers,
4594
- localChecksums,
4595
- initialAssets: assets,
4596
- initialMissing: new Set(resolved.missingIdentifiers),
4597
- checksumAssetsByChecksum
4598
- });
4599
- if (stableMediaReadSet(current) !== stableMediaReadSet(plan.mediaReadSet)) {
4600
- return {
4601
- ok: false,
4602
- error: {
4603
- kind: "remote-media-drift",
4604
- message: "Remote media state changed after preflight; re-run the push to refresh its media plan."
4605
- }
4606
- };
4607
- }
4608
- return { ok: true };
4609
- } catch (error) {
4610
- return {
4611
- ok: false,
4612
- error: {
4613
- kind: "remote-media-read-failed",
4614
- message: `Failed to revalidate remote media state: ${rawErrorMessage(error)}`
4615
- }
4616
- };
4617
- }
4618
- }
4619
- async function syncLocalMediaForPush(options) {
4620
- const plan = await prepareLocalMediaForPush(options);
4621
- const result = await applyLocalMediaPush({
4622
- plan,
4623
- client: options.client,
4624
- dryRun: options.dryRun,
4625
- onEvent: options.onEvent
4626
- });
4627
- Object.assign(options.content, result.content);
4628
- return {
4629
- ...result,
4630
- content: options.content
4631
- };
4632
- }
4633
-
4634
- //#endregion
4635
- export { sdkUpsertEventResponseSchema as A, isInternalResolvedLinkValue as B, createHttpClient as C, sdkGetEventResponseSchema as D, sdkConfigureBookingVerticalResponseSchema as E, entryLinkSchema$1 as F, sdkGetPageDesignResponseSchema as G, linkSchema as H, externalLinkSchema$1 as I, sdkEventOccurrenceResponseSchema as J, sdkSavePageDesignResponseSchema as K, internalLinkSchema as L, linkPayloadSchema as M, RESOLVED_INTERNAL_LINK_ENTITY_TYPES as N, sdkListEventsResponseSchema as O, customLinkSchema$1 as P, internalResolvedLinkSchema as R, buildRequestInfo as S, managementTransportError as T, pageLinkSchema$1 as U, isResolvedInternalLinkEntityType as V, portablePageDesignSchema as W, sdkReorderResponseSchema as X, sdkListEventOccurrencesResponseSchema as Y, sdkConfiguredBookingVerticalConfigSchema as Z, buildSettingsUpdateInput as _, prepareLocalMediaForPush as a, isMediaValue as b, writeBinaryFileAtomic as c, normalizeMediaValueToIdentifierOnly as d, walkLocalContentMedia as f, SETTINGS_FILE_FIELDS as g, extractMediaIdentifiers as h, applyLocalMediaPush as i, sdkSettingsUpdateSchema as j, sdkTakeEventOfflineResponseSchema as k, sha256 as l, downloadMedia as m, syncLocalMediaForPush as n, admitBinaryWriteDestination as o, buildStorageUrl as p, sdkDeleteEventOccurrenceResponseSchema as q, syncLocalMediaForPull as r, readStableBinaryFile as s, revalidateLocalMediaPushPlan as t, normalizeMediaIdentifierInput as u, hasOwnSettingsField as v, is404Error as w, ManagementApiError as x, writeMediaManifest as y, internalRouteOnlyLinkSchema as z };