riverbankcms 0.18.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,4634 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Hr as asPageFlowPresetId, Jn as getDirectImageUrlForSiteScopedPath, T as eventBookingOfferIntentSchema, Ur as asSiteStyleId, _r as normalizeAmbiguousStoragePathForSite, bn as pageDesignBoundaryTreatmentSchema, br as MAX_MANAGEMENT_MEDIA_UPLOAD_BYTES, d as stableStringify, dr as slugifyMediaIdentifier, ft as themeSchema, hn as buttonStyleSelectionSchema, hr as getMimeType, mr as getExtensionForMimeType, n as sdkCourseBookingSettingsSchema, r as sdkEventOccurrenceInputSchema, s as eventVisibilitySchema, sr as buildMediaIdentifierFromFilename, ur as resolveMediaIdentifier, xn as pageDesignIntentSchemaVersion, yr as FILE_SIZE_LIMITS, zr as asAppearancePresetId } from "./sdk-event-upsert-BamyD2d6.mjs";
3
- import { En as ok, Lt as assertNever, Tn as err, a as canonicalizeConfiguredBookingVerticalConfig, an as unsafeAsEventOfferingId, c as resolveSiteVerticalBookingConfig, j as parseFooterCompositionV1, l as DEFAULT_EVENT_TYPE_PRESETS, ln as unsafeAsOccurrenceId, o as parseVerticalId, on as unsafeAsEventSeriesId, s as parseVerticalPresetId, t as resolveEventCatalogue, u as EVENT_PRESET_VALUES, y as exposeSecret } from "./src-BhrSG8A4.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.nullable()
1353
- }));
1354
- const sdkUpsertFooterResponseSchema = sdkCommandApplicationResultSchema(sdkFooterContentSchema.nullable());
1355
-
1356
- //#endregion
1357
- //#region ../api/src/sdkResponseSchemas.ts
1358
- const sdkTimestampSchema = z.string().min(1);
1359
- const sdkConfigureBookingVerticalResponseSchema = sdkCommandApplicationResultSchema(z.object({
1360
- verticalConfig: sdkConfiguredBookingVerticalConfigSchema,
1361
- revision: sdkTimestampSchema.nullable()
1362
- })).extend({ warnings: z.array(sdkBookingSettingsWarningSchema).optional() });
1363
-
1364
- //#endregion
1365
- //#region ../management-client/src/responseValidation.ts
1366
- function isRecord$3(value) {
1367
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
1368
- }
1369
- function pullSchemaForRequest(path$2) {
1370
- switch (path$2) {
1371
- case "/pull/entries/all": return sdkPullEntriesAllResponseSchema;
1372
- case "/pull/entries": return sdkPullEntriesResponseSchema;
1373
- case "/pull/pages": return sdkPullPagesResponseSchema;
1374
- case "/pull/navigation": return sdkPullNavigationResponseSchema;
1375
- case "/pull/settings": return sdkPullSettingsResponseSchema;
1376
- case "/pull/forms": return sdkPullFormsResponseSchema;
1377
- case "/pull/footer": return sdkPullFooterResponseSchema;
1378
- case "/pull/venues": return sdkPullVenuesResponseSchema;
1379
- case "/pull/event-categories": return sdkPullEventCategoriesResponseSchema;
1380
- case "/pull/events": return sdkPullEventsResponseSchema;
1381
- default: return /^\/pull\/entries\/[^/]+$/.test(path$2) ? sdkPullEntriesByTypeResponseSchema : undefined;
1382
- }
1383
- }
1384
- function rootMutationSchema(method, path$2, body) {
1385
- if (method === "POST") {
1386
- switch (path$2) {
1387
- case "/entries": return isRecord$3(body) && body.publication === "publish" ? sdkUpsertEntryPublishResponseSchema : sdkUpsertEntryDraftResponseSchema;
1388
- case "/pages": return isRecord$3(body) && body.publication === "publish" ? sdkUpsertPagePublishResponseSchema : sdkUpsertPageDraftResponseSchema;
1389
- case "/navigation": return sdkUpsertNavigationMenuResponseSchema;
1390
- case "/forms": return sdkUpsertFormResponseSchema;
1391
- case "/venues": return sdkUpsertVenueResponseSchema;
1392
- case "/event-categories": return sdkUpsertEventCategoryResponseSchema;
1393
- case "/events": return sdkUpsertEventResponseSchema;
1394
- case "/footer": return sdkUpsertFooterResponseSchema;
1395
- }
1396
- }
1397
- if (method === "PATCH" && path$2 === "/settings") return sdkUpsertSettingsResponseSchema;
1398
- if (method === "POST" && path$2 === "/settings/booking/vertical") return sdkConfigureBookingVerticalResponseSchema;
1399
- return undefined;
1400
- }
1401
- function isDeleteMutationPath(path$2) {
1402
- return [
1403
- /^\/entries\/[^/]+\/[^/]+$/,
1404
- /^\/pages\/[^/]+$/,
1405
- /^\/pages\/[^/]+\/blocks\/[^/]+$/,
1406
- /^\/navigation\/[^/]+$/,
1407
- /^\/forms\/[^/]+$/,
1408
- /^\/venues\/[^/]+$/,
1409
- /^\/event-categories\/[^/]+$/,
1410
- /^\/events\/[^/]+$/
1411
- ].some((pattern) => pattern.test(path$2));
1412
- }
1413
- function blockMutationSchema(method, path$2) {
1414
- if (method === "POST" && /^\/pages\/[^/]+\/blocks$/.test(path$2)) return sdkUpsertBlockResponseSchema;
1415
- if (method === "POST" && /^\/pages\/[^/]+\/blocks\/reorder$/.test(path$2)) return sdkReorderResponseSchema;
1416
- if (method === "PATCH" && /^\/pages\/[^/]+\/blocks\/[^/]+$/.test(path$2)) return sdkPatchPageBlockResponseSchema;
1417
- if (method === "DELETE" && /^\/pages\/[^/]+\/blocks\/[^/]+$/.test(path$2)) return sdkDeleteBlockResponseSchema;
1418
- return undefined;
1419
- }
1420
- function nestedMutationSchema(method, path$2) {
1421
- const blockSchema = blockMutationSchema(method, path$2);
1422
- if (blockSchema) return blockSchema;
1423
- if (method === "POST" && /^\/entries\/[^/]+\/[^/]+\/(publish|unpublish)$/.test(path$2)) {
1424
- return sdkCommandApplicationResultSchema(sdkUpsertEntryDraftResponseSchema.shape.value);
1425
- }
1426
- if (method === "POST" && /^\/pages\/[^/]+\/(publish|unpublish)$/.test(path$2)) {
1427
- return sdkCommandApplicationResultSchema(sdkUpsertPageDraftResponseSchema.shape.value);
1428
- }
1429
- if (method === "DELETE" && isDeleteMutationPath(path$2)) return sdkDeleteResponseSchema;
1430
- return undefined;
1431
- }
1432
- function occurrenceResponseSchema(method, path$2) {
1433
- const collection = /^\/events\/[^/]+\/runs\/[^/]+\/occurrences$/;
1434
- const member = /^\/events\/[^/]+\/runs\/[^/]+\/occurrences\/[^/]+$/;
1435
- if (method === "GET" && collection.test(path$2)) return sdkListEventOccurrencesResponseSchema;
1436
- if (method === "POST" && collection.test(path$2)) return sdkEventOccurrenceResponseSchema;
1437
- if (method === "PATCH" && member.test(path$2)) return sdkEventOccurrenceResponseSchema;
1438
- if (method === "DELETE" && member.test(path$2)) return sdkDeleteEventOccurrenceResponseSchema;
1439
- return method === "POST" && /^\/events\/[^/]+\/runs\/[^/]+\/occurrences\/[^/]+\/cancel$/.test(path$2) ? sdkEventOccurrenceResponseSchema : undefined;
1440
- }
1441
- function mediaResponseSchema(method, path$2) {
1442
- if (method === "GET" && path$2 === "/media") return sdkMediaListResponseSchema;
1443
- if (method === "GET" && /^\/media\/[^/]+\/download$/.test(path$2)) return sdkMediaDownloadResponseSchema;
1444
- if (method === "POST" && /^\/media\/[^/]+\/delete-preview$/.test(path$2)) return sdkMediaDeletePreviewResponseSchema;
1445
- if (method === "DELETE" && /^\/media\/[^/]+$/.test(path$2)) return sdkMediaDeleteApplyResponseSchema;
1446
- return undefined;
1447
- }
1448
- function schemaForManagementRequest(method, path$2, body) {
1449
- if (method === "GET") {
1450
- return pullSchemaForRequest(path$2) ?? occurrenceResponseSchema(method, path$2) ?? mediaResponseSchema(method, path$2);
1451
- }
1452
- return rootMutationSchema(method, path$2, body) ?? occurrenceResponseSchema(method, path$2) ?? mediaResponseSchema(method, path$2) ?? nestedMutationSchema(method, path$2);
1453
- }
1454
- function validateManagementResponse(value, schema) {
1455
- const parsed = schema.safeParse(value);
1456
- if (parsed.success) return { valid: true };
1457
- return {
1458
- valid: false,
1459
- details: { issues: parsed.error.issues.slice(0, 8).map((issue) => ({
1460
- path: issue.path.map(String),
1461
- message: issue.message,
1462
- ...issue.code ? { code: issue.code } : {}
1463
- })) }
1464
- };
1465
- }
1466
-
1467
- //#endregion
1468
- //#region ../management-client/src/transportFailure.ts
1469
- /**
1470
- * Sanitised transport failures (issue 1496).
1471
- *
1472
- * When `fetch` rejects, the caught value can carry anything the platform put
1473
- * there: a message naming the host and port, a cause chain, a stack, and in
1474
- * some runtimes echoes of the request. None of that may leave the client. The
1475
- * caught value is inspected here, and only here, for one thing: whether its
1476
- * platform code (`cause.code`, else `code`) is one of an allow-listed set of
1477
- * Node and undici codes. The result is a closed union whose `code` is always
1478
- * one of those literals, and a fixed message per kind. Nothing else from the caught value is kept.
1479
- *
1480
- * Code lists were checked against Node 24.18 (undici bundled with it) by
1481
- * provoking each failure: DNS lookups fail with `ENOTFOUND`/`EAI_AGAIN`, a
1482
- * closed port with `ECONNREFUSED` (an `AggregateError` carrying the same code
1483
- * when happy-eyeballs tries several addresses), an unroutable host with
1484
- * undici's `UND_ERR_CONNECT_TIMEOUT`, a peer reset with `ECONNRESET`, a peer
1485
- * closing mid-response with `UND_ERR_SOCKET`, and TLS failures with the
1486
- * OpenSSL verification codes and `ERR_TLS_CERT_ALTNAME_INVALID`.
1487
- *
1488
- * Bun puts its code on the error itself rather than on `cause`, so that is read
1489
- * as a fallback. Bun 1.3 reports resets and TLS failures with the same codes
1490
- * as Node; it reports refused connections *and* failed DNS lookups alike as
1491
- * `ConnectionRefused`, which is deliberately not listed and stays `unknown`.
1492
- */
1493
- const DNS_CODES = ["ENOTFOUND", "EAI_AGAIN"];
1494
- const CONNECT_CODES = [
1495
- "ECONNREFUSED",
1496
- "UND_ERR_CONNECT_TIMEOUT",
1497
- "ECONNABORTED",
1498
- "EHOSTUNREACH",
1499
- "ENETUNREACH"
1500
- ];
1501
- const RESET_CODES = [
1502
- "ECONNRESET",
1503
- "EPIPE",
1504
- "UND_ERR_SOCKET"
1505
- ];
1506
- const TLS_CODES = [
1507
- "CERT_HAS_EXPIRED",
1508
- "DEPTH_ZERO_SELF_SIGNED_CERT",
1509
- "SELF_SIGNED_CERT_IN_CHAIN",
1510
- "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
1511
- "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
1512
- "ERR_TLS_CERT_ALTNAME_INVALID",
1513
- "ERR_SSL_WRONG_VERSION_NUMBER"
1514
- ];
1515
- const TIMEOUT_CODES = [
1516
- "UND_ERR_HEADERS_TIMEOUT",
1517
- "UND_ERR_BODY_TIMEOUT",
1518
- "ETIMEDOUT"
1519
- ];
1520
- function isOneOf(values, value) {
1521
- return values.includes(value);
1522
- }
1523
- /**
1524
- * Read one property of an arbitrary value. A throwing getter or a hostile
1525
- * proxy yields `undefined`, so inspection can never throw past the boundary.
1526
- */
1527
- function readProperty(value, key) {
1528
- if (typeof value !== "object" || value === null) return undefined;
1529
- try {
1530
- return Reflect.get(value, key);
1531
- } catch {
1532
- return undefined;
1533
- }
1534
- }
1535
- function stringCode(value) {
1536
- const code = readProperty(value, "code");
1537
- return typeof code === "string" ? code : undefined;
1538
- }
1539
- /**
1540
- * The platform code for a rejected `fetch`: Node/undici put it on `cause`;
1541
- * Bun puts its own on the error itself. Nothing else is read.
1542
- */
1543
- function platformCode(caught) {
1544
- return stringCode(readProperty(caught, "cause")) ?? stringCode(caught);
1545
- }
1546
- /** Map a raw code onto the closed union; anything unlisted is `unknown`. */
1547
- function transportFailureFromCode(code) {
1548
- if (code === undefined) return { kind: "unknown" };
1549
- if (isOneOf(DNS_CODES, code)) return {
1550
- kind: "dns",
1551
- code
1552
- };
1553
- if (isOneOf(CONNECT_CODES, code)) return {
1554
- kind: "connect",
1555
- code
1556
- };
1557
- if (isOneOf(RESET_CODES, code)) return {
1558
- kind: "reset",
1559
- code
1560
- };
1561
- if (isOneOf(TLS_CODES, code)) return {
1562
- kind: "tls",
1563
- code
1564
- };
1565
- if (isOneOf(TIMEOUT_CODES, code)) return {
1566
- kind: "timeout",
1567
- code
1568
- };
1569
- return { kind: "unknown" };
1570
- }
1571
- /** True when the client's own `AbortSignal.timeout` ended the request. */
1572
- function isClientTimeout(caught) {
1573
- return readProperty(caught, "name") === "TimeoutError";
1574
- }
1575
- /**
1576
- * Classify a rejected `fetch` or body read. The caught value is read only for
1577
- * its `name` and platform code (guarded, see `readProperty`). `timeoutMs` is
1578
- * the limit the client itself applied, so the timeout message can name it.
1579
- */
1580
- function classifyTransportFailure(caught, timeoutMs) {
1581
- return isClientTimeout(caught) ? {
1582
- code: "sdk:timeout",
1583
- transport: { kind: "timeout" },
1584
- timeoutMs
1585
- } : {
1586
- code: "sdk:network-error",
1587
- transport: transportFailureFromCode(platformCode(caught))
1588
- };
1589
- }
1590
- /** The fixed message for a classified failure. */
1591
- function transportErrorMessage(failure) {
1592
- return failure.code === "sdk:timeout" ? `Request timed out after ${failure.timeoutMs}ms` : transportFailureMessage(failure.transport);
1593
- }
1594
- /** The fixed, safe message for each kind of network failure. */
1595
- function transportFailureMessage(failure) {
1596
- switch (failure.kind) {
1597
- case "dns": return "Could not resolve the dashboard hostname";
1598
- case "connect": return "Could not open a connection to the dashboard";
1599
- case "reset": return "The connection to the dashboard closed before a response arrived";
1600
- case "tls": return "The TLS handshake with the dashboard failed";
1601
- case "timeout": return "The connection to the dashboard timed out";
1602
- case "unknown": return "Network request failed";
1603
- }
1604
- }
1605
-
1606
- //#endregion
1607
- //#region ../management-client/src/http.ts
1608
- /**
1609
- * Error thrown when management API request fails
1610
- */
1611
- var ManagementApiError = class extends Error {
1612
- constructor(message, code, details, statusCode, request, tag) {
1613
- super(message);
1614
- this.name = "ManagementApiError";
1615
- this.code = code;
1616
- this.details = details;
1617
- this.statusCode = statusCode;
1618
- this.request = request;
1619
- this.tag = tag;
1620
- }
1621
- };
1622
- /** A successful HTTP response whose payload violated its runtime contract. */
1623
- var ManagementInvalidResponseError = class extends ManagementApiError {
1624
- constructor(details, statusCode, request) {
1625
- super("Management API returned an invalid response", "invalid_response", details, statusCode, request);
1626
- this.code = "invalid_response";
1627
- this.name = "ManagementInvalidResponseError";
1628
- this.details = details;
1629
- this.request = request;
1630
- }
1631
- };
1632
- /**
1633
- * The request never produced a dashboard answer. The message is fixed per
1634
- * failure kind and nothing from the platform error (its message, cause, stack
1635
- * or anything echoing the request) is kept: `transport` is the only diagnostic.
1636
- */
1637
- var ManagementTransportError = class extends ManagementApiError {
1638
- /** The message is derived from the classification; no free text is accepted. */
1639
- constructor(classified, request) {
1640
- super(transportErrorMessage(classified), classified.code, undefined, undefined, request);
1641
- this.details = undefined;
1642
- this.name = "ManagementTransportError";
1643
- this.code = classified.code;
1644
- this.transport = classified.transport;
1645
- }
1646
- };
1647
- /**
1648
- * Turn a rejected `fetch` or body read into a {@link ManagementTransportError}.
1649
- * Every catch site around a management `fetch` or response-body read goes
1650
- * through here so none of them can leak the platform error.
1651
- */
1652
- function managementTransportError(caught, timeoutMs, request) {
1653
- return new ManagementTransportError(classifyTransportFailure(caught, timeoutMs), request);
1654
- }
1655
- /**
1656
- * Type guard to check if an error is a 404 Not Found response.
1657
- * Use this to handle "resource not found" scenarios gracefully.
1658
- *
1659
- * @example
1660
- * ```ts
1661
- * try {
1662
- * return await http.get<Entry>(`/entries/${type}/${id}`);
1663
- * } catch (error) {
1664
- * if (is404Error(error)) {
1665
- * return null;
1666
- * }
1667
- * throw error;
1668
- * }
1669
- * ```
1670
- */
1671
- function is404Error(error) {
1672
- return error instanceof ManagementApiError && error.statusCode === 404;
1673
- }
1674
- function parseRuntimeResponse(value, schema, requestInfo, statusCode) {
1675
- const parsed = schema.safeParse(value);
1676
- if (parsed.success) return parsed.data;
1677
- const validation = validateManagementResponse(value, schema);
1678
- if (validation.valid) throw new Error("Response schema parsing was inconsistent");
1679
- throw new ManagementInvalidResponseError(validation.details, statusCode, requestInfo);
1680
- }
1681
- function isRecord$2(value) {
1682
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
1683
- }
1684
- function isFieldError(value) {
1685
- return isRecord$2(value) && typeof value.field === "string" && typeof value.code === "string" && typeof value.message === "string";
1686
- }
1687
- function parseApiResponse(value) {
1688
- if (!isRecord$2(value)) return null;
1689
- if (value.success === true) {
1690
- return {
1691
- success: true,
1692
- data: value.data
1693
- };
1694
- }
1695
- if (value.success === false) {
1696
- const error = value.error;
1697
- if (!isRecord$2(error) || typeof error.code !== "string" || typeof error.message !== "string") {
1698
- return {
1699
- success: false,
1700
- error: {
1701
- code: "sdk:http-error",
1702
- message: "Request failed",
1703
- details: error
1704
- }
1705
- };
1706
- }
1707
- const fieldErrorsRaw = error.fieldErrors;
1708
- const fieldErrors = Array.isArray(fieldErrorsRaw) ? fieldErrorsRaw.filter(isFieldError) : undefined;
1709
- return {
1710
- success: false,
1711
- error: {
1712
- code: error.code,
1713
- message: error.message,
1714
- details: error.details,
1715
- ...fieldErrors ? { fieldErrors } : {},
1716
- ...typeof error.tag === "string" ? { tag: error.tag } : {}
1717
- }
1718
- };
1719
- }
1720
- return null;
1721
- }
1722
- function buildRequestInfo(options) {
1723
- return {
1724
- method: options.method,
1725
- intent: options.intent,
1726
- path: options.path,
1727
- url: options.url,
1728
- baseUrl: options.baseUrl,
1729
- dashboardUrl: options.dashboardUrl,
1730
- siteId: options.siteId
1731
- };
1732
- }
1733
- /**
1734
- * Read a response body as text. A body that fails mid-read (a reset, undici's
1735
- * body timeout, the client's own abort) is a transport failure like any other.
1736
- */
1737
- async function readResponseText(response, timeoutMs, requestInfo) {
1738
- try {
1739
- return await response.text();
1740
- } catch (error) {
1741
- throw managementTransportError(error, timeoutMs, requestInfo);
1742
- }
1743
- }
1744
- async function readApiResponse(response, requestInfo, options) {
1745
- const contentType = response.headers.get("content-type");
1746
- const text = await readResponseText(response, options.timeoutMs, requestInfo);
1747
- if (!contentType?.includes("application/json")) {
1748
- if (response.ok && options.allowNonJsonOk) return undefined;
1749
- if (response.ok && options.invalidOkResponse) {
1750
- throw new ManagementInvalidResponseError({ issues: [{
1751
- path: [],
1752
- message: "Expected a JSON response"
1753
- }] }, response.status, requestInfo);
1754
- }
1755
- throw new ManagementApiError(`Request failed with status ${response.status}`, "sdk:http-error", { statusCode: response.status }, response.status, requestInfo);
1756
- }
1757
- let parsed;
1758
- try {
1759
- parsed = text ? JSON.parse(text) : null;
1760
- } catch {
1761
- if (response.ok && options.invalidOkResponse) {
1762
- throw new ManagementInvalidResponseError({ issues: [{
1763
- path: [],
1764
- message: "Response was not valid JSON"
1765
- }] }, response.status, requestInfo);
1766
- }
1767
- throw new ManagementApiError(`Invalid JSON response (status ${response.status})`, "sdk:http-error", { statusCode: response.status }, response.status, requestInfo);
1768
- }
1769
- const envelope = parseApiResponse(parsed);
1770
- if (!envelope) {
1771
- if (response.ok && options.invalidOkResponse) {
1772
- throw new ManagementInvalidResponseError({ issues: [{
1773
- path: [],
1774
- message: "Response did not match the API envelope"
1775
- }] }, response.status, requestInfo);
1776
- }
1777
- throw new ManagementApiError(`Invalid API response (status ${response.status})`, "sdk:http-error", { statusCode: response.status }, response.status, requestInfo);
1778
- }
1779
- return envelope;
1780
- }
1781
- function requestUrl(baseUrl, path$2, params) {
1782
- if (!params || Object.keys(params).length === 0) return `${baseUrl}${path$2}`;
1783
- const query = new URLSearchParams(params).toString();
1784
- return `${baseUrl}${path$2}?${query}`;
1785
- }
1786
- const DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
1787
- function requestTimeoutMs(config) {
1788
- return config.timeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
1789
- }
1790
- function requestFetchOptions(config, method, body) {
1791
- const options = {
1792
- method,
1793
- headers: {
1794
- Authorization: `Bearer ${exposeSecret(config.managementApiKey)}`,
1795
- "Content-Type": "application/json"
1796
- },
1797
- signal: AbortSignal.timeout(requestTimeoutMs(config))
1798
- };
1799
- if (body !== undefined && method !== "GET") options.body = JSON.stringify(body);
1800
- return options;
1801
- }
1802
- async function fetchManagementResponse(config, method, intent, path$2, url, baseUrl, options) {
1803
- const requestInfo = buildRequestInfo({
1804
- method,
1805
- intent,
1806
- path: path$2,
1807
- url,
1808
- baseUrl,
1809
- dashboardUrl: config.dashboardUrl,
1810
- siteId: config.siteId
1811
- });
1812
- try {
1813
- return {
1814
- response: await fetch(url, options),
1815
- requestInfo
1816
- };
1817
- } catch (error) {
1818
- throw managementTransportError(error, requestTimeoutMs(config), requestInfo);
1819
- }
1820
- }
1821
- function throwApiError(response, json, requestInfo) {
1822
- if (response.ok && json.success) return;
1823
- if ("error" in json && json.error) {
1824
- const details = json.error.fieldErrors ? { fieldErrors: json.error.fieldErrors } : json.error.details;
1825
- throw new ManagementApiError(json.error.message, json.error.code, details, response.status, requestInfo, json.error.tag);
1826
- }
1827
- throw new ManagementApiError(`Request failed with status ${response.status}`, "sdk:http-error", undefined, response.status, requestInfo);
1828
- }
1829
- async function requestWithConfig(config, method, path$2, options) {
1830
- const baseUrl = `${config.dashboardUrl}/api/sdk/${config.siteId}`;
1831
- const url = requestUrl(baseUrl, path$2, options.params);
1832
- const fetchOptions = requestFetchOptions(config, method, options.body);
1833
- const { response, requestInfo } = await fetchManagementResponse(config, method, options.intent, path$2, url, baseUrl, fetchOptions);
1834
- const responseSchema = options.responseSchema ?? schemaForManagementRequest(method, path$2, options.body);
1835
- const timeoutMs = requestTimeoutMs(config);
1836
- const json = responseSchema ? await readApiResponse(response, requestInfo, {
1837
- timeoutMs,
1838
- invalidOkResponse: true
1839
- }) : await readApiResponse(response, requestInfo, {
1840
- timeoutMs,
1841
- allowNonJsonOk: true
1842
- });
1843
- if (!json) return undefined;
1844
- throwApiError(response, json, requestInfo);
1845
- return responseSchema ? parseRuntimeResponse(json.data, responseSchema, requestInfo, response.status) : json.data;
1846
- }
1847
- function createHttpClient(config) {
1848
- const send = (method, path$2, options) => requestWithConfig(config, method, path$2, options);
1849
- return {
1850
- get: (path$2, params) => send("GET", path$2, {
1851
- intent: "read",
1852
- params
1853
- }),
1854
- post: (path$2, body, intent) => send("POST", path$2, {
1855
- intent,
1856
- body
1857
- }),
1858
- patch: (path$2, body) => send("PATCH", path$2, {
1859
- intent: "write",
1860
- body
1861
- }),
1862
- delete: (path$2, body) => send("DELETE", path$2, {
1863
- intent: "write",
1864
- body
1865
- }),
1866
- getValidated: (path$2, responseSchema, params) => send("GET", path$2, {
1867
- intent: "read",
1868
- params,
1869
- responseSchema
1870
- }),
1871
- postValidated: (path$2, responseSchema, body, intent) => send("POST", path$2, {
1872
- intent,
1873
- body,
1874
- responseSchema
1875
- }),
1876
- patchValidated: (path$2, responseSchema, body) => send("PATCH", path$2, {
1877
- intent: "write",
1878
- body,
1879
- responseSchema
1880
- }),
1881
- deleteValidated: (path$2, responseSchema, body) => send("DELETE", path$2, {
1882
- intent: "write",
1883
- body,
1884
- responseSchema
1885
- })
1886
- };
1887
- }
1888
-
1889
- //#endregion
1890
- //#region src/media/walk.ts
1891
- const isRecordValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1892
- const walkValueTree = (value, location, visitor) => {
1893
- visitor(value, location);
1894
- if (Array.isArray(value)) {
1895
- value.forEach((entry, index) => {
1896
- walkValueTree(entry, `${location}[${index}]`, visitor);
1897
- });
1898
- return;
1899
- }
1900
- if (isRecordValue(value)) {
1901
- Object.entries(value).forEach(([key, entry]) => {
1902
- walkValueTree(entry, location ? `${location}.${key}` : key, visitor);
1903
- });
1904
- }
1905
- };
1906
-
1907
- //#endregion
1908
- //#region src/media/value.ts
1909
- const MEDIA_VALUE_TYPES = [
1910
- "image",
1911
- "video",
1912
- "audio",
1913
- "document",
1914
- "spreadsheet",
1915
- "archive"
1916
- ];
1917
- const MEDIA_VALUE_TYPE_SET = new Set(MEDIA_VALUE_TYPES);
1918
- const isOptionalString = (value) => value === undefined || typeof value === "string";
1919
- const isOptionalNumber = (value) => value === undefined || typeof value === "number";
1920
- 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);
1921
- const hasMediaReference = (value) => typeof value.identifier === "string" || typeof value.assetId === "string" || typeof value.storagePath === "string" || typeof value.src === "string" || typeof value.mimeType === "string";
1922
- const isMediaValue = (value) => isRecordValue(value) && !("attrs" in value) && typeof value.type === "string" && MEDIA_VALUE_TYPE_SET.has(value.type) && hasValidKnownMediaFields(value) && hasMediaReference(value);
1923
-
1924
- //#endregion
1925
- //#region src/content/media-manifest.ts
1926
- const MANIFEST_VERSION = 2;
1927
- const getManifestPath = (contentDir) => path$1.join(contentDir, "media", ".manifest.json");
1928
- const ensureDir = async (dirPath) => {
1929
- try {
1930
- await fs$1.mkdir(dirPath, { recursive: true });
1931
- } catch (error) {
1932
- if (error.code !== "EEXIST") {
1933
- throw error;
1934
- }
1935
- }
1936
- };
1937
- const writeJsonFile = async (filePath, data) => {
1938
- const content = JSON.stringify(data, null, 2) + "\n";
1939
- await fs$1.writeFile(filePath, content, "utf-8");
1940
- };
1941
- const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1942
- const hasManifestShape = (value) => {
1943
- if (!isRecord$1(value) || !("version" in value) || !("assets" in value)) {
1944
- return false;
1945
- }
1946
- return isRecord$1(value.assets);
1947
- };
1948
- const toManifestEntry = (value) => {
1949
- if (!isRecord$1(value)) return null;
1950
- const filename = typeof value.filename === "string" ? value.filename : "";
1951
- if (!filename) return null;
1952
- return {
1953
- filename,
1954
- checksum: typeof value.checksum === "string" ? value.checksum : undefined,
1955
- mimeType: typeof value.mimeType === "string" ? value.mimeType : undefined
1956
- };
1957
- };
1958
- const toV2ManifestEntry = (value) => {
1959
- if (!isRecord$1(value)) return null;
1960
- if (value.checksum !== undefined && typeof value.checksum !== "string" || value.mimeType !== undefined && typeof value.mimeType !== "string") {
1961
- return null;
1962
- }
1963
- return toManifestEntry(value);
1964
- };
1965
- const parseV2Manifest = (parsed, assets) => {
1966
- if (typeof parsed.updatedAt !== "string") return null;
1967
- const parsedAssets = {};
1968
- for (const [identifier, value] of Object.entries(assets)) {
1969
- const entry = toV2ManifestEntry(value);
1970
- if (!entry) return null;
1971
- parsedAssets[identifier] = entry;
1972
- }
1973
- return {
1974
- version: MANIFEST_VERSION,
1975
- updatedAt: parsed.updatedAt,
1976
- assets: parsedAssets
1977
- };
1978
- };
1979
- const migrateV1Assets = (assets) => Object.fromEntries(Object.entries(assets).flatMap(([identifier, value]) => {
1980
- const entry = toManifestEntry(value);
1981
- return entry ? [[identifier, entry]] : [];
1982
- }));
1983
- const migrateV1Manifest = (parsed, assets) => {
1984
- return {
1985
- version: MANIFEST_VERSION,
1986
- updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : new Date().toISOString(),
1987
- assets: migrateV1Assets(assets)
1988
- };
1989
- };
1990
- const parseManifest = (content) => {
1991
- const parsed = JSON.parse(content);
1992
- if (!hasManifestShape(parsed)) return null;
1993
- const assets = parsed.assets;
1994
- if (parsed.version === 1) return migrateV1Manifest(parsed, assets);
1995
- if (parsed.version === MANIFEST_VERSION) {
1996
- return parseV2Manifest(parsed, assets);
1997
- }
1998
- return null;
1999
- };
2000
- const createEmptyMediaManifest = () => ({
2001
- version: MANIFEST_VERSION,
2002
- updatedAt: new Date().toISOString(),
2003
- assets: {}
2004
- });
2005
- async function readMediaManifest(contentDir) {
2006
- const manifestPath = getManifestPath(contentDir);
2007
- try {
2008
- const content = await fs$1.readFile(manifestPath, "utf-8");
2009
- return parseManifest(content) ?? createEmptyMediaManifest();
2010
- } catch (error) {
2011
- const nodeError = error;
2012
- if (nodeError.code !== "ENOENT") {
2013
- console.warn(`Warning: failed to read media manifest: ${nodeError.message}`);
2014
- }
2015
- }
2016
- return createEmptyMediaManifest();
2017
- }
2018
- async function writeMediaManifest(contentDir, manifest) {
2019
- const mediaDir = path$1.join(contentDir, "media");
2020
- await ensureDir(mediaDir);
2021
- const updatedManifest = {
2022
- ...manifest,
2023
- version: MANIFEST_VERSION,
2024
- updatedAt: new Date().toISOString()
2025
- };
2026
- const manifestPath = getManifestPath(contentDir);
2027
- await writeJsonFile(manifestPath, updatedManifest);
2028
- return manifestPath;
2029
- }
2030
-
2031
- //#endregion
2032
- //#region ../management-client/src/settings-branding.ts
2033
- const SETTINGS_BASE_FIELDS = [
2034
- "siteTitle",
2035
- "homepageId",
2036
- "seoDefaults"
2037
- ];
2038
- const SETTINGS_BRANDING_FIELDS = [
2039
- "logoIdentifier",
2040
- "logoDarkBackgroundIdentifier",
2041
- "faviconIdentifier"
2042
- ];
2043
- const SETTINGS_FIELDS = [...SETTINGS_BASE_FIELDS, ...SETTINGS_BRANDING_FIELDS];
2044
- /** Fields owned by content/settings.json. Booking configuration has its own
2045
- * mutation operation and must never enter the generic settings update input. */
2046
- const SETTINGS_FILE_FIELDS = [...SETTINGS_FIELDS, "verticalConfig"];
2047
- const SETTINGS_BRANDING_LABELS = {
2048
- logoIdentifier: "logo",
2049
- logoDarkBackgroundIdentifier: "dark-background logo",
2050
- faviconIdentifier: "favicon"
2051
- };
2052
- function hasOwnSettingsField(settings, field) {
2053
- return hasOwnField(settings, field);
2054
- }
2055
- function hasOwnField(settings, field) {
2056
- return Object.prototype.hasOwnProperty.call(settings, field);
2057
- }
2058
- function buildSettingsUpdateInput(settings) {
2059
- const update = {};
2060
- for (const field of SETTINGS_FIELDS) {
2061
- if (!hasOwnSettingsField(settings, field)) {
2062
- continue;
2063
- }
2064
- switch (field) {
2065
- case "siteTitle":
2066
- update.siteTitle = settings.siteTitle;
2067
- break;
2068
- case "homepageId":
2069
- update.homepageId = settings.homepageId;
2070
- break;
2071
- case "seoDefaults":
2072
- update.seoDefaults = settings.seoDefaults;
2073
- break;
2074
- case "logoIdentifier":
2075
- update.logoIdentifier = settings.logoIdentifier;
2076
- break;
2077
- case "logoDarkBackgroundIdentifier":
2078
- update.logoDarkBackgroundIdentifier = settings.logoDarkBackgroundIdentifier;
2079
- break;
2080
- case "faviconIdentifier":
2081
- update.faviconIdentifier = settings.faviconIdentifier;
2082
- break;
2083
- }
2084
- }
2085
- return update;
2086
- }
2087
- function applySettingsBrandingPullResolutions(baseSettings, resolutions) {
2088
- const settings = {
2089
- homepageId: baseSettings.homepageId,
2090
- seoDefaults: baseSettings.seoDefaults
2091
- };
2092
- const warnings = [];
2093
- for (const resolution of resolutions) {
2094
- switch (resolution.kind) {
2095
- case "resolved":
2096
- settings[resolution.field] = resolution.identifier;
2097
- break;
2098
- case "missing":
2099
- settings[resolution.field] = null;
2100
- break;
2101
- case "unresolved":
2102
- warnings.push(`Remote ${SETTINGS_BRANDING_LABELS[resolution.field]} asset ` + `"${resolution.assetId}" has no media identifier, so ${resolution.field} was omitted from content/settings.json.`);
2103
- break;
2104
- }
2105
- }
2106
- return {
2107
- settings,
2108
- warnings
2109
- };
2110
- }
2111
- function getSettingsBrandingLabel(field) {
2112
- return SETTINGS_BRANDING_LABELS[field];
2113
- }
2114
- function collectSettingsBrandingIdentifiers(settings) {
2115
- if (!settings) {
2116
- return [];
2117
- }
2118
- const identifiers = [];
2119
- for (const field of SETTINGS_BRANDING_FIELDS) {
2120
- const identifier = settings[field];
2121
- if (typeof identifier === "string" && identifier) {
2122
- identifiers.push({
2123
- field,
2124
- identifier
2125
- });
2126
- }
2127
- }
2128
- return identifiers;
2129
- }
2130
- function isImageMimeType(mimeType) {
2131
- return typeof mimeType === "string" && mimeType.startsWith("image/");
2132
- }
2133
-
2134
- //#endregion
2135
- //#region src/sync/media.ts
2136
- /**
2137
- * Media Sync Helper
2138
- *
2139
- * Handles syncing media assets between environments during pull/push operations.
2140
- *
2141
- * With relative paths, storagePath values in content are like "originals/2024/12/abc.jpg"
2142
- * (without site prefix). The actual files are stored at "sites/{siteId}/originals/...".
2143
- *
2144
- * When syncing:
2145
- * 1. Extract all relative storagePath values from content
2146
- * 2. Download files from source: sites/{sourceSiteId}/{relativePath}
2147
- * 3. Upload files to target: sites/{targetSiteId}/{relativePath}
2148
- *
2149
- * No path rewriting in content data is needed since paths are relative!
2150
- */
2151
- /**
2152
- * Default retry configuration for network operations.
2153
- */
2154
- const DEFAULT_RETRY_CONFIG = {
2155
- maxRetries: 3,
2156
- baseDelayMs: 1e3,
2157
- maxDelayMs: 1e4
2158
- };
2159
- /**
2160
- * Sleep for a given number of milliseconds.
2161
- */
2162
- function sleep(ms) {
2163
- return new Promise((resolve$2) => setTimeout(resolve$2, ms));
2164
- }
2165
- /**
2166
- * Calculate exponential backoff delay with jitter.
2167
- * @param attempt - Current attempt number (0-indexed)
2168
- * @param baseDelayMs - Base delay in milliseconds
2169
- * @param maxDelayMs - Maximum delay cap
2170
- */
2171
- function getBackoffDelay(attempt, baseDelayMs, maxDelayMs) {
2172
- const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
2173
- const jitter = exponentialDelay * (.75 + Math.random() * .5);
2174
- return Math.min(jitter, maxDelayMs);
2175
- }
2176
- /**
2177
- * Execute a function with exponential backoff retry.
2178
- * @param fn - Function to execute
2179
- * @param options - Retry options
2180
- */
2181
- async function withRetry(fn, options = {}) {
2182
- const { maxRetries, baseDelayMs, maxDelayMs } = {
2183
- ...DEFAULT_RETRY_CONFIG,
2184
- ...options
2185
- };
2186
- let lastError;
2187
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
2188
- try {
2189
- return await fn();
2190
- } catch (error) {
2191
- lastError = error instanceof Error ? error : new Error(String(error));
2192
- if (attempt < maxRetries) {
2193
- const delay = getBackoffDelay(attempt, baseDelayMs, maxDelayMs);
2194
- options.onRetry?.(attempt + 1, lastError);
2195
- await sleep(delay);
2196
- }
2197
- }
2198
- }
2199
- throw lastError;
2200
- }
2201
- /**
2202
- * Extract all unique storagePath values from content data.
2203
- * Recursively walks through objects and arrays to find all media references.
2204
- */
2205
- function extractMediaPaths(data) {
2206
- const paths = new Set();
2207
- walkValueTree(data, "", (value) => {
2208
- if (!isRecordValue(value)) return;
2209
- if (typeof value.storagePath === "string" && value.storagePath) {
2210
- paths.add(value.storagePath);
2211
- }
2212
- });
2213
- return paths;
2214
- }
2215
- /**
2216
- * Extract all unique media identifiers from content data.
2217
- * Recursively walks through objects and arrays to find media references.
2218
- */
2219
- function extractMediaIdentifiers(data) {
2220
- const identifiers = new Set();
2221
- if (isRecordValue(data)) {
2222
- const rootSettings = isRecordValue(data.settings) ? data.settings : null;
2223
- for (const { identifier } of collectSettingsBrandingIdentifiers(rootSettings)) {
2224
- identifiers.add(identifier);
2225
- }
2226
- }
2227
- walkValueTree(data, "", (value) => {
2228
- if (!isRecordValue(value)) return;
2229
- if (isMediaValue(value) && value.identifier) {
2230
- identifiers.add(value.identifier);
2231
- }
2232
- if (value.type === "image" && isRecordValue(value.attrs)) {
2233
- if (typeof value.attrs.identifier === "string" && value.attrs.identifier) {
2234
- identifiers.add(value.attrs.identifier);
2235
- }
2236
- }
2237
- });
2238
- return identifiers;
2239
- }
2240
- function buildStorageUrl(supabaseUrl, storagePath, siteId, bucket = "media") {
2241
- const normalized = normalizeAmbiguousStoragePathForSite(siteId, storagePath);
2242
- if (!normalized.ok) {
2243
- return {
2244
- ok: false,
2245
- error: {
2246
- tag: "invalid_storage_path",
2247
- siteId,
2248
- storagePath,
2249
- cause: normalized.error
2250
- }
2251
- };
2252
- }
2253
- return {
2254
- ok: true,
2255
- value: getDirectImageUrlForSiteScopedPath(supabaseUrl, normalized.value.fullPath, bucket)
2256
- };
2257
- }
2258
- /**
2259
- * File extensions that support Supabase image transforms.
2260
- */
2261
- const TRANSFORMABLE_EXTENSIONS = [
2262
- "jpg",
2263
- "jpeg",
2264
- "png",
2265
- "webp",
2266
- "gif"
2267
- ];
2268
- /**
2269
- * Check if a file path is an image that can be transformed by Supabase.
2270
- * @param path - The file path or storage path
2271
- */
2272
- function isTransformableImage(path$2) {
2273
- const ext = path$2.split(".").pop()?.toLowerCase();
2274
- return TRANSFORMABLE_EXTENSIONS.includes(ext ?? "");
2275
- }
2276
- /**
2277
- * Build a URL using Supabase's image transform endpoint.
2278
- * Returns a resized version to avoid exceeding upload size limits.
2279
- *
2280
- * @param supabaseUrl - The Supabase project URL
2281
- * @param storagePath - Relative or site-scoped path within the bucket
2282
- * @param siteId - The site ID context for relative paths
2283
- * @param options - Transform options (width, quality)
2284
- */
2285
- function buildImageTransformUrl(supabaseUrl, storagePath, siteId, options = {}) {
2286
- const { width = 1920, quality = 85 } = options;
2287
- const normalized = normalizeAmbiguousStoragePathForSite(siteId, storagePath);
2288
- if (!normalized.ok) {
2289
- return {
2290
- ok: false,
2291
- error: {
2292
- tag: "invalid_storage_path",
2293
- siteId,
2294
- storagePath,
2295
- cause: normalized.error
2296
- }
2297
- };
2298
- }
2299
- return {
2300
- ok: true,
2301
- value: getDirectImageUrlForSiteScopedPath(supabaseUrl, normalized.value.fullPath, "media", {
2302
- width,
2303
- quality
2304
- })
2305
- };
2306
- }
2307
- /**
2308
- * Download a file from a public Supabase storage URL.
2309
- * Returns the file data as a Buffer, or null if download fails after retries.
2310
- *
2311
- * Uses exponential backoff retry for transient network failures.
2312
- */
2313
- async function downloadMedia(url, options) {
2314
- try {
2315
- return await withRetry(async () => {
2316
- const response = await fetch(url);
2317
- if (!response.ok) {
2318
- if (response.status >= 400 && response.status < 500 && response.status !== 429) {
2319
- throw new DownloadError(`HTTP ${response.status}`, response.status, false);
2320
- }
2321
- throw new DownloadError(`HTTP ${response.status}`, response.status, true);
2322
- }
2323
- const contentType = response.headers.get("content-type") ?? "application/octet-stream";
2324
- const arrayBuffer = await response.arrayBuffer();
2325
- const data = Buffer.from(arrayBuffer);
2326
- return {
2327
- data,
2328
- contentType
2329
- };
2330
- }, {
2331
- maxRetries: options?.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries,
2332
- onRetry: (attempt, error) => {
2333
- console.warn(`[media-sync] Retry ${attempt} for ${url}: ${error.message}`);
2334
- }
2335
- });
2336
- } catch (error) {
2337
- const message = error instanceof Error ? error.message : String(error);
2338
- console.error(`[media-sync] Download failed after retries: ${url} - ${message}`);
2339
- return null;
2340
- }
2341
- }
2342
- /**
2343
- * Custom error for download operations with retryability info.
2344
- */
2345
- var DownloadError = class extends Error {
2346
- constructor(message, status, retryable) {
2347
- super(message);
2348
- this.status = status;
2349
- this.retryable = retryable;
2350
- this.name = "DownloadError";
2351
- }
2352
- };
2353
- /**
2354
- * Sync media files from source to target environment.
2355
- *
2356
- * With relative paths, content data doesn't need to be modified.
2357
- * We just copy files from source site folder to target site folder.
2358
- *
2359
- * @param content - The content data containing media references
2360
- * @param client - Client for uploading to target environment
2361
- * @param options - Sync options
2362
- * @returns Summary of sync results (content unchanged since paths are relative)
2363
- */
2364
- async function syncMedia(content, client, options) {
2365
- const { sourceSupabaseUrl, sourceSiteId, bucket = "media", onProgress } = options;
2366
- const mediaPaths = extractMediaPaths(content);
2367
- const total = mediaPaths.size;
2368
- const results = [];
2369
- let completed = 0;
2370
- for (const relativePath of mediaPaths) {
2371
- const filename = relativePath.split("/").pop() ?? "file";
2372
- onProgress?.(completed, total, filename);
2373
- try {
2374
- const exists = await client.mediaExists(relativePath);
2375
- if (exists) {
2376
- results.push({
2377
- relativePath,
2378
- success: true,
2379
- skipped: true
2380
- });
2381
- completed++;
2382
- continue;
2383
- }
2384
- } catch {}
2385
- const urlResult = buildStorageUrl(sourceSupabaseUrl, relativePath, sourceSiteId, bucket);
2386
- if (!urlResult.ok) {
2387
- results.push({
2388
- relativePath,
2389
- success: false,
2390
- error: `Invalid media storage path: ${urlResult.error.cause.tag}`
2391
- });
2392
- completed++;
2393
- continue;
2394
- }
2395
- const url = urlResult.value;
2396
- const downloaded = await downloadMedia(url);
2397
- if (!downloaded) {
2398
- results.push({
2399
- relativePath,
2400
- success: false,
2401
- error: `Failed to download from ${url}`
2402
- });
2403
- completed++;
2404
- continue;
2405
- }
2406
- const uploadResult = await client.uploadMedia({
2407
- data: downloaded.data,
2408
- filename,
2409
- contentType: downloaded.contentType,
2410
- storagePath: relativePath
2411
- });
2412
- if ("error" in uploadResult) {
2413
- results.push({
2414
- relativePath,
2415
- success: false,
2416
- error: uploadResult.error
2417
- });
2418
- } else {
2419
- results.push({
2420
- relativePath,
2421
- success: true
2422
- });
2423
- }
2424
- completed++;
2425
- }
2426
- onProgress?.(total, total, "complete");
2427
- const summary = {
2428
- total,
2429
- synced: results.filter((r) => r.success && !r.skipped).length,
2430
- skipped: results.filter((r) => r.skipped).length,
2431
- failed: results.filter((r) => !r.success).length,
2432
- results
2433
- };
2434
- return {
2435
- summary,
2436
- content
2437
- };
2438
- }
2439
-
2440
- //#endregion
2441
- //#region src/media/identifiers.ts
2442
- const walkContentMedia = (content, handler) => {
2443
- const visit = (value, location) => {
2444
- if (isMediaValue(value)) {
2445
- handler(value, location);
2446
- }
2447
- };
2448
- for (const [contentType, entries] of content.entries) {
2449
- for (const entry of entries) {
2450
- walkValueTree(entry.data, `entries.${contentType}.${entry.identifier}.data`, visit);
2451
- for (const [slotKey, blocks] of Object.entries(entry.blocks ?? {})) {
2452
- for (const block of blocks) {
2453
- walkValueTree(block.content, `entries.${contentType}.${entry.identifier}.blocks.${slotKey}.${block.identifier ?? "unidentified"}.content`, visit);
2454
- }
2455
- }
2456
- }
2457
- }
2458
- for (const page of content.pages) {
2459
- for (const block of page.blocks ?? []) {
2460
- walkValueTree(block.data, `pages.${page.identifier}.blocks.${block.identifier}.data`, visit);
2461
- }
2462
- }
2463
- walkValueTree(content.navigation, "navigation", visit);
2464
- for (const { field, identifier } of collectSettingsBrandingIdentifiers(content.settings)) {
2465
- handler({
2466
- type: "image",
2467
- identifier
2468
- }, `settings.${field}`);
2469
- }
2470
- walkValueTree(content.settings, "settings", visit);
2471
- walkValueTree(content.forms, "forms", visit);
2472
- walkValueTree(content.footer, "footer", visit);
2473
- walkValueTree(content.venues, "venues", visit);
2474
- walkValueTree(content.eventCategories, "eventCategories", visit);
2475
- for (const event of content.events ?? []) {
2476
- walkValueTree(event, `events.${event.identifier}`, visit);
2477
- }
2478
- };
2479
- const localContentSections = (content) => ({
2480
- entries: content.entries?.entries() ?? [],
2481
- pages: content.pages ?? [],
2482
- navigation: content.navigation,
2483
- settings: content.settings,
2484
- forms: content.forms,
2485
- footer: content.footer,
2486
- venues: content.venues,
2487
- eventCategories: content.eventCategories,
2488
- events: content.events
2489
- });
2490
- const pulledContentSections = (content) => ({
2491
- entries: Object.entries(content.entries),
2492
- pages: content.pages,
2493
- navigation: content.navigation,
2494
- settings: content.settings,
2495
- forms: content.forms,
2496
- venues: content.venues,
2497
- eventCategories: content.eventCategories,
2498
- events: content.events
2499
- });
2500
- const walkLocalContentMedia = (content, handler) => {
2501
- walkContentMedia(localContentSections(content), handler);
2502
- };
2503
- const walkPulledContentMedia = (content, handler) => {
2504
- walkContentMedia(pulledContentSections(content), handler);
2505
- };
2506
-
2507
- //#endregion
2508
- //#region src/media/value-utils.ts
2509
- /**
2510
- * Normalize a media identifier to the canonical slug form.
2511
- * Returns null when the raw value is empty or invalid.
2512
- */
2513
- const normalizeMediaIdentifierInput = (raw) => {
2514
- if (!raw) return null;
2515
- const trimmed = raw.trim();
2516
- if (!trimmed) return null;
2517
- if (/[./\\]/.test(trimmed)) {
2518
- return null;
2519
- }
2520
- const normalized = slugifyMediaIdentifier(trimmed);
2521
- return normalized || null;
2522
- };
2523
- /**
2524
- * Normalize a media value to the portable identifier-only representation.
2525
- *
2526
- * Returns a new media value by:
2527
- * - setting `identifier` (required)
2528
- * - removing env-specific fields (`assetId`, `storagePath`, `storageBucket`, `src`, `mimeType`)
2529
- */
2530
- const normalizeMediaValueToIdentifierOnly = (value, identifier, asset) => {
2531
- const normalized = {
2532
- ...value,
2533
- identifier
2534
- };
2535
- delete normalized.assetId;
2536
- delete normalized.storageBucket;
2537
- delete normalized.storagePath;
2538
- delete normalized.mimeType;
2539
- delete normalized.src;
2540
- if (asset) {
2541
- applyAuthoritativeDimension(normalized, "width", asset.width);
2542
- applyAuthoritativeDimension(normalized, "height", asset.height);
2543
- }
2544
- return normalized;
2545
- };
2546
- function applyAuthoritativeDimension(value, key, dimension) {
2547
- if (dimension === null || dimension === undefined) {
2548
- delete value[key];
2549
- return;
2550
- }
2551
- value[key] = dimension;
2552
- }
2553
-
2554
- //#endregion
2555
- //#region src/media/rich-text.ts
2556
- const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
2557
- /**
2558
- * Walk any JSON-like payload and collect TipTap rich text image node attrs.
2559
- *
2560
- * TipTap nodes look like: { type: 'image', attrs: { src?: string, identifier?: string, ... } }
2561
- */
2562
- function collectRichTextImageAttrs(value, out = [], location = "") {
2563
- if (Array.isArray(value)) {
2564
- value.forEach((child, index) => collectRichTextImageAttrs(child, out, `${location}[${index}]`));
2565
- return out;
2566
- }
2567
- if (!isRecord(value)) return out;
2568
- if (value.type === "image" && isRecord(value.attrs)) {
2569
- out.push({
2570
- attrs: value.attrs,
2571
- location: location ? `${location}.attrs` : "attrs"
2572
- });
2573
- }
2574
- for (const [key, child] of Object.entries(value)) {
2575
- const next = location ? `${location}.${key}` : key;
2576
- collectRichTextImageAttrs(child, out, next);
2577
- }
2578
- return out;
2579
- }
2580
-
2581
- //#endregion
2582
- //#region src/media/pull-manifest.ts
2583
- const buildLocalFilename = (identifier, asset) => {
2584
- const extensionFromFilename = path$1.extname(asset.filename ?? "");
2585
- const extension = extensionFromFilename || getExtensionForMimeType(asset.mimeType) || "";
2586
- return extension ? `${identifier}${extension}` : identifier;
2587
- };
2588
- const updateManifestEntry = (manifest, identifier, asset, localFile) => {
2589
- const previous = manifest.assets[identifier];
2590
- const filename = localFile?.filename ?? previous?.filename ?? buildLocalFilename(identifier, asset);
2591
- const checksum = localFile?.checksum ?? asset.checksumSha256 ?? previous?.checksum;
2592
- const mimeType = localFile?.mimeType ?? asset.mimeType ?? previous?.mimeType;
2593
- const entry = {
2594
- checksum,
2595
- filename,
2596
- mimeType
2597
- };
2598
- manifest.assets[identifier] = entry;
2599
- };
2600
- const normalizeStoragePath = (storagePath, siteId) => {
2601
- const prefix = `sites/${siteId}/`;
2602
- if (storagePath.startsWith(prefix)) {
2603
- return storagePath.slice(prefix.length);
2604
- }
2605
- return storagePath;
2606
- };
2607
-
2608
- //#endregion
2609
- //#region src/media/push-preparation-plan.ts
2610
- const cloneLocalFile = (file) => ({
2611
- identifier: file.identifier,
2612
- filename: file.filename,
2613
- filePath: file.filePath,
2614
- mimeType: file.mimeType,
2615
- checksum: file.checksum,
2616
- dataBase64: Buffer.from(file.data).toString("base64"),
2617
- dataLength: file.data.length
2618
- });
2619
- const collectLocalMediaReferences = (content) => {
2620
- const mediaReferences = [];
2621
- const richTextReferences = [];
2622
- const warnings = [];
2623
- const errors = [];
2624
- const locationsByIdentifier = new Map();
2625
- walkLocalContentMedia(content, (value, location) => {
2626
- const rawIdentifier = typeof value.identifier === "string" ? value.identifier : "";
2627
- const hasLegacyRef = Boolean(value.assetId || value.storagePath || value.src);
2628
- if (!rawIdentifier) {
2629
- if (hasLegacyRef) {
2630
- errors.push(`Media reference at ${location} is missing identifier.`);
2631
- }
2632
- return;
2633
- }
2634
- if (/[./\\]/.test(rawIdentifier)) {
2635
- errors.push(`Media identifier "${rawIdentifier}" at ${location} must not include an extension.`);
2636
- return;
2637
- }
2638
- const normalized = normalizeMediaIdentifierInput(rawIdentifier);
2639
- if (!normalized) {
2640
- errors.push(`Media identifier "${rawIdentifier}" at ${location} is invalid.`);
2641
- return;
2642
- }
2643
- if (normalized !== rawIdentifier) {
2644
- warnings.push(`Normalized media identifier "${rawIdentifier}" to "${normalized}" at ${location}.`);
2645
- value.identifier = normalized;
2646
- }
2647
- mediaReferences.push({
2648
- identifier: normalized,
2649
- rawIdentifier,
2650
- value,
2651
- location
2652
- });
2653
- const locations = locationsByIdentifier.get(normalized) ?? [];
2654
- locations.push(location);
2655
- locationsByIdentifier.set(normalized, locations);
2656
- });
2657
- for (const node of collectRichTextImageAttrs(content)) {
2658
- const attrs = node.attrs;
2659
- const rawIdentifier = typeof attrs.identifier === "string" ? attrs.identifier : "";
2660
- const rawSrc = typeof attrs.src === "string" ? attrs.src : "";
2661
- if (!rawIdentifier) {
2662
- if (rawSrc) {
2663
- errors.push(`Rich text image at ${node.location} is missing identifier.`);
2664
- }
2665
- continue;
2666
- }
2667
- if (/[./\\]/.test(rawIdentifier)) {
2668
- errors.push(`Rich text image identifier "${rawIdentifier}" at ${node.location} must not include an extension.`);
2669
- continue;
2670
- }
2671
- const normalized = normalizeMediaIdentifierInput(rawIdentifier);
2672
- if (!normalized) {
2673
- errors.push(`Rich text image identifier "${rawIdentifier}" at ${node.location} is invalid.`);
2674
- continue;
2675
- }
2676
- if (normalized !== rawIdentifier) {
2677
- warnings.push(`Normalized rich text image identifier "${rawIdentifier}" to "${normalized}" at ${node.location}.`);
2678
- attrs.identifier = normalized;
2679
- }
2680
- delete attrs.src;
2681
- richTextReferences.push({
2682
- identifier: normalized,
2683
- rawIdentifier,
2684
- attrs,
2685
- location: node.location
2686
- });
2687
- const locations = locationsByIdentifier.get(normalized) ?? [];
2688
- locations.push(node.location);
2689
- locationsByIdentifier.set(normalized, locations);
2690
- }
2691
- return {
2692
- mediaReferences,
2693
- richTextReferences,
2694
- warnings,
2695
- errors,
2696
- locationsByIdentifier
2697
- };
2698
- };
2699
- const buildLocationNote = (locationsByIdentifier, identifier) => {
2700
- const locations = locationsByIdentifier.get(identifier);
2701
- return locations?.length ? ` Referenced at: ${locations.join(", ")}.` : "";
2702
- };
2703
- const locationsForIdentifier = (locationsByIdentifier, identifier) => locationsByIdentifier.get(identifier) ?? [];
2704
- const operationPriority = (operation) => {
2705
- switch (operation.kind) {
2706
- case "upload": return 0;
2707
- case "conflict":
2708
- case "missing_local_file":
2709
- case "reused":
2710
- case "skipped":
2711
- case "unchanged": return 1;
2712
- }
2713
- };
2714
- const sortOperations = (operations) => operations.slice().sort((a, b) => {
2715
- const byIdentifier = a.identifier.localeCompare(b.identifier);
2716
- return byIdentifier || operationPriority(a) - operationPriority(b);
2717
- });
2718
- const emptyPushSummary = (totalReferences, identifiers) => ({
2719
- totalReferences,
2720
- identifiers,
2721
- resolved: 0,
2722
- reused: 0,
2723
- uploaded: 0,
2724
- skipped: 0,
2725
- pendingUploads: 0
2726
- });
2727
- const stableAssetList = (assets) => assets.slice().sort((a, b) => a.id.localeCompare(b.id)).map((asset) => ({ ...asset }));
2728
- const stableMediaReadSet = (observations) => stableStringify(observations.slice().sort((a, b) => a.identifier.localeCompare(b.identifier)).map((observation) => ({
2729
- identifier: observation.identifier,
2730
- localChecksum: observation.localChecksum.kind === "present" && observation.localChecksum.lookup.kind === "read" ? {
2731
- ...observation.localChecksum,
2732
- lookup: {
2733
- ...observation.localChecksum.lookup,
2734
- assets: stableAssetList(observation.localChecksum.lookup.assets)
2735
- }
2736
- } : observation.localChecksum,
2737
- resolvedAsset: observation.resolvedAsset,
2738
- missing: observation.missing
2739
- })));
2740
- const buildMediaReadSet = (options) => options.identifiers.map((identifier) => {
2741
- const checksum = options.localChecksums.get(identifier);
2742
- const resolvedAsset = options.initialAssets.get(identifier);
2743
- const localChecksum = checksum === undefined ? { kind: "missing" } : {
2744
- kind: "present",
2745
- checksum,
2746
- lookup: options.checksumAssetsByChecksum.has(checksum) ? {
2747
- kind: "read",
2748
- assets: stableAssetList(options.checksumAssetsByChecksum.get(checksum) ?? [])
2749
- } : { kind: "not-read" }
2750
- };
2751
- return {
2752
- identifier,
2753
- localChecksum,
2754
- resolvedAsset: resolvedAsset ? { ...resolvedAsset } : null,
2755
- missing: options.initialMissing.has(identifier)
2756
- };
2757
- });
2758
- const localFileForOperation = (localFile) => ({
2759
- filename: localFile.filename,
2760
- checksum: localFile.checksum,
2761
- mimeType: localFile.mimeType
2762
- });
2763
- const classifyExistingAsset = (options, hasConflict) => {
2764
- const { identifier, asset, localFile, locations, locationNote } = options;
2765
- if (asset && !localFile) {
2766
- return {
2767
- operations: [{
2768
- kind: "skipped",
2769
- identifier,
2770
- locations,
2771
- message: "Using existing CMS asset because no local file was found."
2772
- }],
2773
- warnings: [`Missing local file for "${identifier}". Using CMS asset instead.${locationNote}`],
2774
- errors: []
2775
- };
2776
- }
2777
- if (hasConflict && asset) {
2778
- const message = `Identifier "${identifier}" points to a different file on the CMS. Delete the existing asset first or rename your local identifier.`;
2779
- return {
2780
- operations: [{
2781
- kind: "conflict",
2782
- identifier,
2783
- locations,
2784
- message
2785
- }],
2786
- errors: [`${message}${locationNote}`]
2787
- };
2788
- }
2789
- if (asset && localFile && !hasConflict) {
2790
- return {
2791
- operations: [{
2792
- kind: "unchanged",
2793
- identifier,
2794
- asset: { ...asset },
2795
- file: cloneLocalFile(localFile),
2796
- locations
2797
- }],
2798
- manifestAsset: asset,
2799
- manifestFile: localFileForOperation(localFile),
2800
- errors: []
2801
- };
2802
- }
2803
- return undefined;
2804
- };
2805
- const classifySingularChecksumMatch = (options) => {
2806
- const { identifier, localFile, locations, initialOperations } = options;
2807
- const exact = options.checksumMatches.find((candidate) => candidate.identifier === identifier);
2808
- if (exact) {
2809
- return {
2810
- operations: [...initialOperations, {
2811
- kind: "reused",
2812
- identifier,
2813
- asset: { ...exact },
2814
- file: cloneLocalFile(localFile),
2815
- locations
2816
- }],
2817
- manifestAsset: exact,
2818
- manifestFile: localFileForOperation(localFile),
2819
- errors: []
2820
- };
2821
- }
2822
- return undefined;
2823
- };
2824
- const classifyChecksumMatches = (options) => {
2825
- const { identifier, localFile, checksumMatches, locations, initialOperations, locationNote } = options;
2826
- const singular = classifySingularChecksumMatch(options);
2827
- if (singular) return singular;
2828
- if (checksumMatches.length > 0) {
2829
- const identifiers = checksumMatches.map((asset) => asset.identifier).join(", ");
2830
- const message = `The same media bytes already exist under ${identifiers}; use that identifier or delete the existing asset first.`;
2831
- return {
2832
- operations: [...initialOperations, {
2833
- kind: "conflict",
2834
- identifier,
2835
- locations,
2836
- message
2837
- }],
2838
- errors: [`${message}${locationNote}`]
2839
- };
2840
- }
2841
- return {
2842
- operations: [...initialOperations, {
2843
- kind: "upload",
2844
- identifier,
2845
- file: cloneLocalFile(localFile),
2846
- source: "local_file",
2847
- locations
2848
- }],
2849
- errors: []
2850
- };
2851
- };
2852
- const classifyMissingReference = (options) => {
2853
- const { identifier, localFile, locationNote, initialOperations, locations } = options;
2854
- if (!localFile) {
2855
- const message = `Add content/media/${identifier}.* or pull with --local-media first.`;
2856
- return {
2857
- operations: [...initialOperations, {
2858
- kind: "missing_local_file",
2859
- identifier,
2860
- locations,
2861
- message
2862
- }],
2863
- errors: [`Identifier "${identifier}" does not exist on the CMS and no local file was found. ${message}${locationNote}`]
2864
- };
2865
- }
2866
- return classifyChecksumMatches({
2867
- identifier,
2868
- localFile,
2869
- checksumMatches: options.checksumMatches,
2870
- locations,
2871
- initialOperations,
2872
- locationNote
2873
- });
2874
- };
2875
- const classifyLocalMediaReference = (options) => {
2876
- const { asset, localFile, missing, locations } = options;
2877
- const hasConflict = Boolean(asset && localFile && asset.checksumSha256 && asset.checksumSha256 !== localFile.checksum);
2878
- const existing = classifyExistingAsset(options, hasConflict);
2879
- if (existing) return existing;
2880
- const initialOperations = [];
2881
- if (!missing && !hasConflict) return {
2882
- operations: initialOperations,
2883
- errors: []
2884
- };
2885
- return classifyMissingReference({
2886
- identifier: options.identifier,
2887
- localFile,
2888
- checksumMatches: options.checksumMatches,
2889
- locations,
2890
- initialOperations,
2891
- locationNote: options.locationNote
2892
- });
2893
- };
2894
- const applyManifestClassification = (manifest, identifier, classification) => {
2895
- if (classification.manifestAsset) {
2896
- updateManifestEntry(manifest, identifier, classification.manifestAsset, classification.manifestFile);
2897
- }
2898
- };
2899
-
2900
- //#endregion
2901
- //#region src/utils/checksum.ts
2902
- function sha256(buffer) {
2903
- return createHash("sha256").update(buffer).digest("hex");
2904
- }
2905
-
2906
- //#endregion
2907
- //#region src/files/stable-binary-file.ts
2908
- function isNodeError(error) {
2909
- return error instanceof Error && "code" in error;
2910
- }
2911
- function observationsMatch(left, right) {
2912
- return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
2913
- }
2914
- function admitStableBinaryRead(stats, maxBytes) {
2915
- if (!stats.isFile()) return err({ tag: "not_a_regular_file" });
2916
- return stats.size > maxBytes ? err({
2917
- tag: "file_too_large",
2918
- maxBytes,
2919
- actualBytes: stats.size
2920
- }) : ok(undefined);
2921
- }
2922
- function mapStableBinaryReadFailure(error) {
2923
- if (!isNodeError(error)) return { tag: "read_failed" };
2924
- if (error.code === "ENOENT") return { tag: "file_not_found" };
2925
- return error.code === "ELOOP" || error.code === "EISDIR" ? { tag: "not_a_regular_file" } : { tag: "read_failed" };
2926
- }
2927
- async function readObservedByteLength(handle, byteLength) {
2928
- const data = Buffer.allocUnsafe(byteLength);
2929
- let offset = 0;
2930
- while (offset < byteLength) {
2931
- const read = await handle.read(data, offset, byteLength - offset, offset);
2932
- if (read.bytesRead === 0) return null;
2933
- offset += read.bytesRead;
2934
- }
2935
- const growthProbe = Buffer.allocUnsafe(1);
2936
- const extra = await handle.read(growthProbe, 0, 1, byteLength);
2937
- return extra.bytesRead === 0 ? data : null;
2938
- }
2939
- async function readStableBinaryFile(filePath, maxBytes) {
2940
- let handle;
2941
- try {
2942
- const noFollow = constants.O_NOFOLLOW ?? 0;
2943
- handle = await open(filePath, constants.O_RDONLY | noFollow);
2944
- const before = await handle.stat();
2945
- const admission = admitStableBinaryRead(before, maxBytes);
2946
- if (!admission.ok) return admission;
2947
- const data = await readObservedByteLength(handle, before.size);
2948
- const after = await handle.stat();
2949
- if (data === null || !observationsMatch(before, after)) {
2950
- return err({ tag: "file_changed_during_read" });
2951
- }
2952
- return ok(data);
2953
- } catch (error) {
2954
- return err(mapStableBinaryReadFailure(error));
2955
- } finally {
2956
- await handle?.close().catch(() => undefined);
2957
- }
2958
- }
2959
- async function inspectDestination(filePath, overwrite) {
2960
- try {
2961
- const stats = await lstat(filePath);
2962
- if (overwrite === "refuse") return err({ tag: "destination_exists" });
2963
- return stats.isFile() ? ok(undefined) : err({ tag: "unsafe_destination" });
2964
- } catch (error) {
2965
- if (isNodeError(error) && error.code === "ENOENT") return ok(undefined);
2966
- return err({ tag: "write_failed" });
2967
- }
2968
- }
2969
- /**
2970
- * Admit an output path before a remote operation while preserving the atomic
2971
- * writer's second check at finalization time to close the race window.
2972
- */
2973
- async function admitBinaryWriteDestination(filePath, overwrite) {
2974
- const destination = await inspectDestination(filePath, overwrite);
2975
- if (!destination.ok) return destination;
2976
- try {
2977
- const parent = dirname$1(filePath);
2978
- const parentStats = await lstat(parent);
2979
- if (!parentStats.isDirectory()) return err({ tag: "unsafe_destination" });
2980
- await access(parent, constants.W_OK);
2981
- return ok(undefined);
2982
- } catch {
2983
- return err({ tag: "write_failed" });
2984
- }
2985
- }
2986
- async function syncDirectory(directory) {
2987
- if (process.platform === "win32") return true;
2988
- let handle;
2989
- try {
2990
- handle = await open(directory, "r");
2991
- await handle.sync();
2992
- return true;
2993
- } catch {
2994
- return false;
2995
- } finally {
2996
- await handle?.close().catch(() => undefined);
2997
- }
2998
- }
2999
- function mapAtomicBinaryWriteFailure(error) {
3000
- return isNodeError(error) && (error.code === "ELOOP" || error.code === "EISDIR" || error.code === "ENOTDIR") ? { tag: "unsafe_destination" } : { tag: "write_failed" };
3001
- }
3002
- async function writeTemporaryBinaryFile(temporaryPath, data, mode) {
3003
- let handle;
3004
- try {
3005
- handle = await open(temporaryPath, "wx", mode);
3006
- await handle.writeFile(data);
3007
- await handle.sync();
3008
- await handle.close();
3009
- handle = undefined;
3010
- await chmod(temporaryPath, mode);
3011
- } finally {
3012
- await handle?.close().catch(() => undefined);
3013
- }
3014
- }
3015
- async function finalizeTemporaryBinaryFile(temporaryPath, filePath, overwrite) {
3016
- if (overwrite === "allow") {
3017
- await rename(temporaryPath, filePath);
3018
- return ok([]);
3019
- }
3020
- try {
3021
- await link(temporaryPath, filePath);
3022
- } catch (error) {
3023
- return isNodeError(error) && error.code === "EEXIST" ? err({ tag: "destination_exists" }) : err(mapAtomicBinaryWriteFailure(error));
3024
- }
3025
- try {
3026
- await unlink$1(temporaryPath);
3027
- return ok([]);
3028
- } catch {
3029
- return ok([{
3030
- tag: "temporary_cleanup_failed",
3031
- temporaryPath
3032
- }]);
3033
- }
3034
- }
3035
- function toAtomicBinaryWriteOutcome(warnings) {
3036
- const [first, ...rest] = warnings;
3037
- return first === undefined ? { tag: "written" } : {
3038
- tag: "written_with_warnings",
3039
- warnings: [first, ...rest]
3040
- };
3041
- }
3042
- async function writeBinaryFileAtomic(filePath, data, options) {
3043
- const parent = dirname$1(filePath);
3044
- const mode = options.mode ?? 384;
3045
- const suffix = options.randomSuffix ?? (() => `${process.pid}-${Date.now()}-${randomUUID()}`);
3046
- let temporaryPath;
3047
- try {
3048
- if (options.ensureParent === true) await mkdir(parent, { recursive: true });
3049
- const admitted = await inspectDestination(filePath, options.overwrite);
3050
- if (!admitted.ok) return admitted;
3051
- temporaryPath = join(parent, `.${basename(filePath)}.${suffix()}.tmp`);
3052
- await writeTemporaryBinaryFile(temporaryPath, data, mode);
3053
- const finalized = await finalizeTemporaryBinaryFile(temporaryPath, filePath, options.overwrite);
3054
- if (!finalized.ok) return finalized;
3055
- temporaryPath = undefined;
3056
- const warnings = await syncDirectory(parent) ? finalized.value : [...finalized.value, { tag: "directory_sync_failed" }];
3057
- return ok(toAtomicBinaryWriteOutcome(warnings));
3058
- } catch (error) {
3059
- return err(mapAtomicBinaryWriteFailure(error));
3060
- } finally {
3061
- if (temporaryPath) await unlink$1(temporaryPath).catch(() => undefined);
3062
- }
3063
- }
3064
-
3065
- //#endregion
3066
- //#region src/media/local-media.ts
3067
- const MEDIA_DIR_NAME = "media";
3068
- const fileExists = async (filePath) => {
3069
- try {
3070
- await fs$1.access(filePath);
3071
- return true;
3072
- } catch {
3073
- return false;
3074
- }
3075
- };
3076
- const listMediaDirFiles = async (mediaDir) => {
3077
- try {
3078
- return await fs$1.readdir(mediaDir);
3079
- } catch (error) {
3080
- const nodeError = error;
3081
- if (nodeError.code === "ENOENT") return [];
3082
- throw error;
3083
- }
3084
- };
3085
- const findMatchingFilename = async (contentDir, identifier, manifest) => {
3086
- const mediaDir = path$1.join(contentDir, MEDIA_DIR_NAME);
3087
- const manifestEntry = manifest.assets[identifier];
3088
- if (manifestEntry?.filename) {
3089
- const candidatePath = path$1.join(mediaDir, manifestEntry.filename);
3090
- if (await fileExists(candidatePath)) {
3091
- return manifestEntry.filename;
3092
- }
3093
- }
3094
- const files = await listMediaDirFiles(mediaDir);
3095
- const matches = files.filter((name) => {
3096
- if (name.startsWith(".")) return false;
3097
- const parsed = path$1.parse(name);
3098
- return parsed.name === identifier;
3099
- });
3100
- if (matches.length > 1) {
3101
- throw new Error(`Multiple media files match identifier "${identifier}": ${matches.join(", ")}`);
3102
- }
3103
- return matches[0] ?? null;
3104
- };
3105
- async function loadLocalMediaFile(contentDir, identifier, manifest) {
3106
- const filename = await findMatchingFilename(contentDir, identifier, manifest);
3107
- if (!filename) return null;
3108
- const mediaDir = path$1.join(contentDir, MEDIA_DIR_NAME);
3109
- const filePath = path$1.join(mediaDir, filename);
3110
- const read = await readStableBinaryFile(filePath, MAX_MANAGEMENT_MEDIA_UPLOAD_BYTES);
3111
- if (!read.ok) {
3112
- throw new Error(`Failed to read media file ${filePath}: ${read.error.tag}`);
3113
- }
3114
- const data = read.value;
3115
- const checksum = sha256(data);
3116
- const mimeType = getMimeType(filename);
3117
- manifest.assets[identifier] = {
3118
- ...manifest.assets[identifier],
3119
- filename,
3120
- checksum,
3121
- mimeType
3122
- };
3123
- return {
3124
- identifier,
3125
- filename,
3126
- filePath,
3127
- mimeType,
3128
- checksum,
3129
- data
3130
- };
3131
- }
3132
- async function writeLocalMediaFile(contentDir, filename, data) {
3133
- const mediaDir = path$1.join(contentDir, MEDIA_DIR_NAME);
3134
- await fs$1.mkdir(mediaDir, { recursive: true });
3135
- const filePath = path$1.join(mediaDir, filename);
3136
- const written = await writeBinaryFileAtomic(filePath, data, {
3137
- overwrite: "allow",
3138
- mode: 420
3139
- });
3140
- if (!written.ok) {
3141
- throw new Error(`Failed to write media file ${filePath}: ${written.error.tag}`);
3142
- }
3143
- return written.value.tag === "written" ? {
3144
- kind: "written",
3145
- filePath
3146
- } : {
3147
- kind: "written_with_warnings",
3148
- filePath,
3149
- warnings: written.value.warnings
3150
- };
3151
- }
3152
- async function readLocalMediaChecksum(filePath, maxBytes) {
3153
- const read = await readStableBinaryFile(filePath, maxBytes);
3154
- if (!read.ok) {
3155
- throw new Error(`Failed to read media file ${filePath}: ${read.error.tag}`);
3156
- }
3157
- return sha256(read.value);
3158
- }
3159
- async function localMediaFileExists(filePath) {
3160
- return fileExists(filePath);
3161
- }
3162
-
3163
- //#endregion
3164
- //#region src/media/pull-naming.ts
3165
- const GENERIC_MEDIA_NAMES = new Set([
3166
- "image",
3167
- "photo",
3168
- "picture",
3169
- "pic",
3170
- "img",
3171
- "media",
3172
- "file"
3173
- ]);
3174
- const buildIdentifierBaseFromLocation = (location) => {
3175
- const parts = location.split(".");
3176
- if (parts.length < 2) return null;
3177
- if (parts[0] === "entries") {
3178
- const contentType = parts[1];
3179
- const entryIdentifier = parts[2];
3180
- const dataIndex = parts.indexOf("data");
3181
- const fieldName = dataIndex >= 0 ? parts[dataIndex + 1] : "media";
3182
- if (contentType && entryIdentifier && fieldName) {
3183
- return `${contentType}-${entryIdentifier}-${fieldName}`;
3184
- }
3185
- }
3186
- if (parts[0] === "pages") {
3187
- const pageIdentifier = parts[1];
3188
- const blockIndex = parts.indexOf("blocks");
3189
- const blockIdentifier = blockIndex >= 0 ? parts[blockIndex + 1] : null;
3190
- const dataIndex = parts.indexOf("data");
3191
- const fieldName = dataIndex >= 0 ? parts[dataIndex + 1] : "media";
3192
- if (pageIdentifier && blockIdentifier && fieldName) {
3193
- return `${pageIdentifier}-${blockIdentifier}-${fieldName}`;
3194
- }
3195
- if (pageIdentifier && fieldName) {
3196
- return `${pageIdentifier}-${fieldName}`;
3197
- }
3198
- }
3199
- return null;
3200
- };
3201
- const chooseIdentifierBase = (location, asset) => {
3202
- const baseFromFilename = buildMediaIdentifierFromFilename(asset.filename ?? "");
3203
- const baseFromContext = buildIdentifierBaseFromLocation(location);
3204
- if (baseFromFilename && !GENERIC_MEDIA_NAMES.has(baseFromFilename)) {
3205
- return baseFromFilename;
3206
- }
3207
- return baseFromContext || baseFromFilename || "media";
3208
- };
3209
- const allocateIdentifier = (base, existingIdentifiers) => resolveMediaIdentifier(base, existingIdentifiers);
3210
- const parseFilenameFromUrl = (src) => {
3211
- try {
3212
- const url = new URL(src);
3213
- const raw = url.pathname.split("/").filter(Boolean).pop() ?? "";
3214
- const decoded = raw ? decodeURIComponent(raw) : "";
3215
- const trimmed = decoded.trim();
3216
- return trimmed.length > 0 ? trimmed : null;
3217
- } catch {
3218
- return null;
3219
- }
3220
- };
3221
- const ensureFilenameExtension = (filename, mimeType) => {
3222
- if (path$1.extname(filename)) return filename;
3223
- const ext = getExtensionForMimeType(mimeType);
3224
- return ext ? `${filename}${ext}` : filename;
3225
- };
3226
- const chooseRichTextIdentifierBase = (location, src) => {
3227
- const filename = parseFilenameFromUrl(src) ?? "";
3228
- const baseFromFilename = filename ? buildMediaIdentifierFromFilename(filename) : null;
3229
- const baseFromContext = buildIdentifierBaseFromLocation(location);
3230
- const base = baseFromFilename && !GENERIC_MEDIA_NAMES.has(baseFromFilename) ? baseFromFilename : baseFromContext || baseFromFilename || "rich-text-image";
3231
- return `${base}-rt`;
3232
- };
3233
-
3234
- //#endregion
3235
- //#region src/media/push-preparation-reads.ts
3236
- const readMediaManifestAndLocalFiles = async (options) => {
3237
- const manifest = options.manifest ?? await readMediaManifest(options.contentDir);
3238
- const errors = [];
3239
- const localFiles = new Map();
3240
- for (const identifier of options.identifiers) {
3241
- try {
3242
- const localFile = await loadLocalMediaFile(options.contentDir, identifier, manifest);
3243
- localFiles.set(identifier, localFile);
3244
- } catch (error) {
3245
- const message = error instanceof Error ? error.message : String(error);
3246
- errors.push(`Failed to load media file for "${identifier}": ${message}`);
3247
- localFiles.set(identifier, null);
3248
- }
3249
- }
3250
- return {
3251
- manifest,
3252
- localFiles,
3253
- errors
3254
- };
3255
- };
3256
- const resolveMediaAssets = async (client, identifiers) => {
3257
- const resolved = await client.media.resolve({ identifiers: [...identifiers] });
3258
- const assetsByIdentifier = new Map();
3259
- for (const asset of resolved.assets) {
3260
- if (asset.identifier) assetsByIdentifier.set(asset.identifier, asset);
3261
- }
3262
- return {
3263
- assetsByIdentifier,
3264
- missingIdentifiers: new Set(resolved.missingIdentifiers)
3265
- };
3266
- };
3267
- const readChecksumMatches = async (options) => {
3268
- const matchesByChecksum = new Map();
3269
- for (const identifier of options.identifiers) {
3270
- if (options.preparedIdentifiers.has(identifier)) continue;
3271
- const localFile = options.localFiles.get(identifier);
3272
- const asset = options.assetsByIdentifier.get(identifier);
3273
- const hasConflict = Boolean(asset && localFile && asset.checksumSha256 && asset.checksumSha256 !== localFile.checksum);
3274
- if (hasConflict) continue;
3275
- if (!localFile || !options.missingIdentifiers.has(identifier) && !hasConflict) {
3276
- continue;
3277
- }
3278
- const matches = (await options.client.media.findByChecksum([localFile.checksum])).filter((candidate) => candidate.checksumSha256 === localFile.checksum);
3279
- const stableMatches = matches.slice().sort((a, b) => a.id.localeCompare(b.id)).map((candidate) => ({ ...candidate }));
3280
- matchesByChecksum.set(localFile.checksum, stableMatches);
3281
- }
3282
- return matchesByChecksum;
3283
- };
3284
- const readRichTextImageSource = async (options) => {
3285
- const downloaded = await downloadMedia(options.source);
3286
- if (!downloaded) {
3287
- throw new Error(`Failed to download rich text image from ${options.source}`);
3288
- }
3289
- const mimeType = downloaded.contentType.split(";")[0]?.trim() || "application/octet-stream";
3290
- if (!mimeType.startsWith("image/")) {
3291
- throw new Error(`Unsupported content-type for rich text image import: ${mimeType}`);
3292
- }
3293
- const data = Buffer.from(downloaded.data);
3294
- const checksum = sha256(data);
3295
- 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 }));
3296
- const filename = ensureFilenameExtension(parseFilenameFromUrl(options.source) ?? "image", mimeType);
3297
- return {
3298
- location: options.location,
3299
- attrs: options.attrs,
3300
- source: options.source,
3301
- mimeType,
3302
- checksum,
3303
- file: cloneLocalFile({
3304
- identifier: "planned-rich-text-image",
3305
- filename,
3306
- filePath: options.source,
3307
- mimeType,
3308
- checksum,
3309
- data
3310
- }),
3311
- checksumMatches
3312
- };
3313
- };
3314
- /**
3315
- * Reads remote rich-text image sources without changing content or CMS state.
3316
- * Identifier allocation and operation selection happen in the pure planner.
3317
- */
3318
- const readRichTextImageImports = async (options) => {
3319
- const successful = [];
3320
- const failures = [];
3321
- for (const node of collectRichTextImageAttrs(options.content)) {
3322
- const rawIdentifier = typeof node.attrs.identifier === "string" ? node.attrs.identifier.trim() : "";
3323
- const source = typeof node.attrs.src === "string" ? node.attrs.src.trim() : "";
3324
- if (rawIdentifier || !source) continue;
3325
- try {
3326
- successful.push(await readRichTextImageSource({
3327
- client: options.client,
3328
- location: node.location,
3329
- attrs: node.attrs,
3330
- source
3331
- }));
3332
- } catch (error) {
3333
- failures.push({
3334
- location: node.location,
3335
- error: error instanceof Error ? error.message : String(error)
3336
- });
3337
- }
3338
- }
3339
- return {
3340
- successful,
3341
- failures
3342
- };
3343
- };
3344
- const localFileChecksums = (identifiers, localFiles) => new Map(identifiers.map((identifier) => [identifier, localFiles.get(identifier)?.checksum]));
3345
- const cloneAssetMap = (assets) => new Map(Array.from(assets.entries()).map(([identifier, asset]) => [identifier, { ...asset }]));
3346
- const cloneMissingSet = (missing) => new Set(missing);
3347
-
3348
- //#endregion
3349
- //#region src/media/push-rich-text-plan.ts
3350
- const localFileWithIdentifier = (file, identifier) => ({
3351
- ...file,
3352
- identifier
3353
- });
3354
- const withLocation = (locations, identifier, location) => {
3355
- const current = locations.get(identifier) ?? [];
3356
- current.push(location);
3357
- locations.set(identifier, current);
3358
- };
3359
- const chooseExistingRichTextAsset = (matches) => matches.slice().sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
3360
- const normalizeRichTextIdentifiers = (options) => {
3361
- for (const node of collectRichTextImageAttrs(options.content)) {
3362
- const attrs = node.attrs;
3363
- const rawIdentifier = typeof attrs.identifier === "string" ? attrs.identifier.trim() : "";
3364
- if (!rawIdentifier) continue;
3365
- const normalized = normalizeMediaIdentifierInput(rawIdentifier);
3366
- if (!normalized) {
3367
- options.errors.push(`Rich text image identifier "${rawIdentifier}" at ${node.location} is invalid.`);
3368
- delete attrs.src;
3369
- continue;
3370
- }
3371
- if (normalized !== rawIdentifier) {
3372
- options.warnings.push(`Normalized rich text image identifier "${rawIdentifier}" to "${normalized}" at ${node.location}.`);
3373
- }
3374
- attrs.identifier = normalized;
3375
- delete attrs.src;
3376
- options.existingIdentifiers.add(normalized);
3377
- withLocation(options.locationsByIdentifier, normalized, node.location);
3378
- }
3379
- };
3380
- /**
3381
- * Converts already-read rich-text sources into identifier-only content and
3382
- * closed media operations. It performs no filesystem or CMS I/O.
3383
- */
3384
- const planRichTextImageImports = (options) => {
3385
- const operations = [];
3386
- const warnings = [];
3387
- const errors = options.imports.failures.map((failure) => `Failed to import rich text image @ ${failure.location}: ${failure.error}`);
3388
- const preparedIdentifiers = new Set();
3389
- const locationsByIdentifier = new Map();
3390
- const checksumAssetsByChecksum = new Map();
3391
- normalizeRichTextIdentifiers({
3392
- content: options.content,
3393
- existingIdentifiers: options.existingIdentifiers,
3394
- warnings,
3395
- errors,
3396
- locationsByIdentifier
3397
- });
3398
- for (const imported of options.imports.successful) {
3399
- const attrs = imported.attrs;
3400
- const matches = imported.checksumMatches;
3401
- checksumAssetsByChecksum.set(imported.checksum, matches);
3402
- const existing = chooseExistingRichTextAsset(matches);
3403
- const planned = existing ? {
3404
- identifier: existing.identifier,
3405
- operation: {
3406
- kind: "unchanged",
3407
- identifier: existing.identifier,
3408
- asset: { ...existing },
3409
- file: localFileWithIdentifier(imported.file, existing.identifier),
3410
- locations: [imported.location]
3411
- }
3412
- } : (() => {
3413
- const base = chooseRichTextIdentifierBase(imported.location, imported.source);
3414
- const identifier = resolveMediaIdentifier(base, options.existingIdentifiers);
3415
- options.existingIdentifiers.add(identifier);
3416
- withLocation(locationsByIdentifier, identifier, imported.location);
3417
- const file = localFileWithIdentifier(imported.file, identifier);
3418
- return {
3419
- identifier,
3420
- operation: {
3421
- kind: "upload",
3422
- identifier,
3423
- file,
3424
- source: "rich_text",
3425
- locations: [imported.location]
3426
- }
3427
- };
3428
- })();
3429
- attrs.identifier = planned.identifier;
3430
- delete attrs.src;
3431
- options.existingIdentifiers.add(planned.identifier);
3432
- preparedIdentifiers.add(planned.identifier);
3433
- operations.push(planned.operation);
3434
- }
3435
- return {
3436
- operations,
3437
- errors,
3438
- warnings,
3439
- preparedIdentifiers,
3440
- locationsByIdentifier,
3441
- checksumAssetsByChecksum
3442
- };
3443
- };
3444
-
3445
- //#endregion
3446
- //#region src/media/content-normalization.ts
3447
- const normalizeRichTextImage = (node, assetsByIdentifier) => {
3448
- const attrs = node.attrs;
3449
- if (!isRecordValue(attrs)) return node;
3450
- const identifier = typeof attrs.identifier === "string" ? attrs.identifier : "";
3451
- if (!identifier) return node;
3452
- const normalizedAttrs = { ...attrs };
3453
- delete normalizedAttrs.src;
3454
- const asset = assetsByIdentifier.get(identifier);
3455
- if (asset) {
3456
- if (asset.width === null || asset.width === undefined) {
3457
- delete normalizedAttrs.width;
3458
- } else {
3459
- normalizedAttrs.width = asset.width;
3460
- }
3461
- if (asset.height === null || asset.height === undefined) {
3462
- delete normalizedAttrs.height;
3463
- } else {
3464
- normalizedAttrs.height = asset.height;
3465
- }
3466
- }
3467
- return {
3468
- ...node,
3469
- attrs: normalizedAttrs
3470
- };
3471
- };
3472
- const normalizeValueTree = (value, assetsByIdentifier) => {
3473
- if (Array.isArray(value)) {
3474
- return value.map((item) => normalizeValueTree(item, assetsByIdentifier));
3475
- }
3476
- if (!isRecordValue(value)) return value;
3477
- const normalizedChildren = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, normalizeValueTree(child, assetsByIdentifier)]));
3478
- if (isMediaValue(value) && typeof value.identifier === "string") {
3479
- return normalizeMediaValueToIdentifierOnly(value, value.identifier, assetsByIdentifier.get(value.identifier));
3480
- }
3481
- const normalized = normalizedChildren;
3482
- if (value.type === "image") {
3483
- return normalizeRichTextImage(normalized, assetsByIdentifier);
3484
- }
3485
- return normalized;
3486
- };
3487
- const normalizeRecord = (value, assetsByIdentifier) => {
3488
- const normalized = normalizeValueTree(value, assetsByIdentifier);
3489
- return normalized;
3490
- };
3491
- /**
3492
- * Return local content with media references in the same portable shape that
3493
- * the push diff and command compiler receive. The input is never mutated.
3494
- */
3495
- const normalizeLocalContentMedia = (content, assetsByIdentifier) => ({
3496
- ...content,
3497
- entries: new Map(Array.from(content.entries.entries(), ([contentType, entries]) => [contentType, entries.map((entry) => ({
3498
- ...entry,
3499
- data: normalizeRecord(entry.data, assetsByIdentifier),
3500
- blocks: entry.blocks ? Object.fromEntries(Object.entries(entry.blocks).map(([slotKey, blocks]) => [slotKey, blocks.map((block) => ({
3501
- ...block,
3502
- content: normalizeRecord(block.content, assetsByIdentifier)
3503
- }))])) : entry.blocks
3504
- }))])),
3505
- pages: content.pages.map((page) => ({
3506
- ...page,
3507
- blocks: page.blocks?.map((block) => ({
3508
- ...block,
3509
- data: normalizeRecord(block.data, assetsByIdentifier)
3510
- }))
3511
- })),
3512
- navigation: normalizeValueTree(content.navigation, assetsByIdentifier),
3513
- settings: normalizeValueTree(content.settings, assetsByIdentifier),
3514
- forms: content.forms.map((form) => ({
3515
- ...form,
3516
- schema: normalizeRecord(form.schema, assetsByIdentifier),
3517
- settings: normalizeRecord(form.settings, assetsByIdentifier)
3518
- })),
3519
- footer: normalizeValueTree(content.footer, assetsByIdentifier),
3520
- venues: content.venues.map((venue) => normalizeRecord(venue, assetsByIdentifier)),
3521
- eventCategories: content.eventCategories.map((category) => normalizeRecord(category, assetsByIdentifier)),
3522
- events: content.events.map((event) => normalizeRecord(event, assetsByIdentifier))
3523
- });
3524
- const addAsset = (assetsByIdentifier, identifier, asset) => {
3525
- assetsByIdentifier.set(identifier, {
3526
- ...asset,
3527
- identifier
3528
- });
3529
- };
3530
- /** Build the CMS asset lookup available while applying a prepared media plan. */
3531
- const assetsForPushMediaPlan = (options) => {
3532
- const assetsByIdentifier = new Map();
3533
- const observationsByAssetId = new Map();
3534
- for (const observation of options.observations) {
3535
- if (observation.resolvedAsset) {
3536
- addAsset(assetsByIdentifier, observation.identifier, observation.resolvedAsset);
3537
- observationsByAssetId.set(observation.resolvedAsset.id, observation.resolvedAsset);
3538
- }
3539
- const checksumAssets = observation.localChecksum.kind === "present" && observation.localChecksum.lookup.kind === "read" ? observation.localChecksum.lookup.assets : [];
3540
- for (const asset of checksumAssets) {
3541
- observationsByAssetId.set(asset.id, asset);
3542
- }
3543
- }
3544
- for (const operation of options.operations) {
3545
- if (operation.kind === "unchanged" || operation.kind === "reused") {
3546
- addAsset(assetsByIdentifier, operation.identifier, operation.asset);
3547
- continue;
3548
- }
3549
- }
3550
- return assetsByIdentifier;
3551
- };
3552
-
3553
- //#endregion
3554
- //#region src/media/push-preparation.ts
3555
- const cloneLocalContent = (content) => structuredClone(content);
3556
- const collectExistingIdentifiers = (content) => {
3557
- const identifiers = new Set();
3558
- walkLocalContentMedia(content, (value) => {
3559
- if (typeof value.identifier === "string" && value.identifier.trim()) {
3560
- identifiers.add(value.identifier.trim());
3561
- }
3562
- });
3563
- return identifiers;
3564
- };
3565
- const mergeLocations = (locations, identifier, location) => {
3566
- const current = locations.get(identifier) ?? [];
3567
- if (!current.includes(location)) current.push(location);
3568
- locations.set(identifier, current);
3569
- };
3570
- const mergeRichTextLocations = (locations, additional) => {
3571
- for (const [identifier, identifierLocations] of additional) {
3572
- for (const location of identifierLocations) {
3573
- mergeLocations(locations, identifier, location);
3574
- }
3575
- }
3576
- };
3577
- const initialAssetMap = (assetsByIdentifier) => cloneAssetMap(assetsByIdentifier);
3578
- const initialMissingSet = (missingIdentifiers) => cloneMissingSet(missingIdentifiers);
3579
- const appendClassification = (options) => {
3580
- options.operations.push(...options.classification.operations);
3581
- options.errors.push(...options.classification.errors);
3582
- options.warnings.push(...options.classification.warnings ?? []);
3583
- applyManifestClassification(options.manifest, options.identifier, options.classification);
3584
- };
3585
- const selectPushReferences = (options) => {
3586
- const collected = collectLocalMediaReferences(options.content);
3587
- const warnings = [...options.richTextPlan.warnings, ...collected.warnings];
3588
- const errors = [...options.richTextPlan.errors, ...collected.errors];
3589
- const locationsByIdentifier = new Map();
3590
- mergeRichTextLocations(locationsByIdentifier, options.richTextPlan.locationsByIdentifier);
3591
- mergeRichTextLocations(locationsByIdentifier, collected.locationsByIdentifier);
3592
- const references = [...collected.mediaReferences.map((reference) => ({
3593
- identifier: reference.identifier,
3594
- location: reference.location,
3595
- richText: false
3596
- })), ...collected.richTextReferences.map((reference) => ({
3597
- identifier: reference.identifier,
3598
- location: reference.location,
3599
- richText: true
3600
- }))];
3601
- for (const reference of references) {
3602
- mergeLocations(locationsByIdentifier, reference.identifier, reference.location);
3603
- }
3604
- const identifiers = Array.from(new Set(references.map((reference) => reference.identifier))).sort();
3605
- return {
3606
- references,
3607
- identifiers,
3608
- locationsByIdentifier,
3609
- warnings,
3610
- errors,
3611
- summary: emptyPushSummary(references.length, identifiers.length)
3612
- };
3613
- };
3614
- const prepareLocalReferenceOperations = async (options) => {
3615
- const loaded = await readMediaManifestAndLocalFiles({
3616
- contentDir: options.prepare.contentDir,
3617
- identifiers: options.selection.identifiers,
3618
- manifest: options.manifest
3619
- });
3620
- const errors = [...loaded.errors];
3621
- const warnings = [];
3622
- const resolved = await resolveMediaAssets(options.prepare.client, options.selection.identifiers);
3623
- const initialAssets = initialAssetMap(resolved.assetsByIdentifier);
3624
- const initialMissing = initialMissingSet(resolved.missingIdentifiers);
3625
- const checksumMatches = await readChecksumMatches({
3626
- client: options.prepare.client,
3627
- identifiers: options.selection.identifiers,
3628
- localFiles: loaded.localFiles,
3629
- assetsByIdentifier: resolved.assetsByIdentifier,
3630
- missingIdentifiers: resolved.missingIdentifiers,
3631
- preparedIdentifiers: options.richTextPlan.preparedIdentifiers
3632
- });
3633
- const operations = [];
3634
- for (const identifier of options.selection.identifiers) {
3635
- if (options.richTextPlan.preparedIdentifiers.has(identifier)) continue;
3636
- const localFile = loaded.localFiles.get(identifier);
3637
- const classification = classifyLocalMediaReference({
3638
- identifier,
3639
- asset: resolved.assetsByIdentifier.get(identifier),
3640
- localFile,
3641
- missing: resolved.missingIdentifiers.has(identifier),
3642
- checksumMatches: localFile ? checksumMatches.get(localFile.checksum) ?? [] : [],
3643
- locations: locationsForIdentifier(options.selection.locationsByIdentifier, identifier),
3644
- locationNote: buildLocationNote(options.selection.locationsByIdentifier, identifier)
3645
- });
3646
- appendClassification({
3647
- identifier,
3648
- classification,
3649
- manifest: options.manifest,
3650
- operations,
3651
- errors,
3652
- warnings
3653
- });
3654
- }
3655
- const sortedOperations = sortOperations(operations);
3656
- const localChecksums = localFileChecksums(options.selection.identifiers, loaded.localFiles);
3657
- for (const operation of sortedOperations) {
3658
- if ("file" in operation && operation.file) {
3659
- localChecksums.set(operation.identifier, operation.file.checksum);
3660
- }
3661
- }
3662
- const checksumAssetsByChecksum = new Map(options.richTextPlan.checksumAssetsByChecksum);
3663
- for (const [checksum, matches] of checksumMatches) {
3664
- checksumAssetsByChecksum.set(checksum, matches);
3665
- }
3666
- return {
3667
- operations: sortedOperations,
3668
- errors,
3669
- warnings,
3670
- mediaReadSet: buildMediaReadSet({
3671
- identifiers: options.selection.identifiers,
3672
- localChecksums,
3673
- initialAssets,
3674
- initialMissing,
3675
- checksumAssetsByChecksum
3676
- })
3677
- };
3678
- };
3679
- async function prepareLocalMediaForPush(options) {
3680
- const content = cloneLocalContent(options.content);
3681
- const manifest = await readMediaManifest(options.contentDir);
3682
- const existingIdentifiers = collectExistingIdentifiers(content);
3683
- const richTextReads = await readRichTextImageImports({
3684
- content,
3685
- client: options.client
3686
- });
3687
- const richTextPlan = planRichTextImageImports({
3688
- content,
3689
- imports: richTextReads,
3690
- existingIdentifiers
3691
- });
3692
- const selection = selectPushReferences({
3693
- content,
3694
- richTextPlan
3695
- });
3696
- const prepared = selection.identifiers.length > 0 ? await prepareLocalReferenceOperations({
3697
- prepare: options,
3698
- manifest,
3699
- selection,
3700
- richTextPlan
3701
- }) : {
3702
- operations: [],
3703
- errors: [],
3704
- warnings: [],
3705
- mediaReadSet: []
3706
- };
3707
- const contentWithPortableMedia = normalizeLocalContentMedia(content, assetsForPushMediaPlan({
3708
- operations: [...richTextPlan.operations, ...prepared.operations],
3709
- observations: prepared.mediaReadSet
3710
- }));
3711
- return {
3712
- content: contentWithPortableMedia,
3713
- manifest,
3714
- operations: sortOperations([...richTextPlan.operations, ...prepared.operations]),
3715
- references: selection.references,
3716
- summary: selection.summary,
3717
- warnings: [...selection.warnings, ...prepared.warnings],
3718
- errors: [...selection.errors, ...prepared.errors],
3719
- targetEnv: options.targetEnv,
3720
- mediaReadSet: prepared.mediaReadSet
3721
- };
3722
- }
3723
-
3724
- //#endregion
3725
- //#region src/media/push-failure.ts
3726
- const includesAny = (value, needles) => needles.some((needle) => value.includes(needle));
3727
- const managementErrorRules = [
3728
- {
3729
- kind: "max_object_size",
3730
- matches: (error) => {
3731
- const code = error.code.toLowerCase();
3732
- const message = error.message.toLowerCase();
3733
- return error.statusCode === 413 || code.includes("too_large") || message.includes("too large");
3734
- }
3735
- },
3736
- {
3737
- kind: "unsupported_mime",
3738
- matches: (error) => {
3739
- const code = error.code.toLowerCase();
3740
- const message = error.message.toLowerCase();
3741
- return error.statusCode === 415 || includesAny(code, ["mime", "content-type"]) || includesAny(message, ["mime", "content-type"]);
3742
- }
3743
- },
3744
- {
3745
- kind: "quota_or_storage_limit",
3746
- matches: (error) => {
3747
- const code = error.code.toLowerCase();
3748
- const message = error.message.toLowerCase();
3749
- return error.statusCode === 402 || error.statusCode === 507 || includesAny(code, ["quota", "storage"]) || includesAny(message, ["quota", "storage limit"]);
3750
- }
3751
- },
3752
- {
3753
- kind: "auth_or_permission",
3754
- matches: (error) => {
3755
- const code = error.code.toLowerCase();
3756
- return error.statusCode === 401 || error.statusCode === 403 || includesAny(code, ["auth", "permission"]);
3757
- }
3758
- },
3759
- {
3760
- kind: "network",
3761
- matches: (error) => {
3762
- const code = error.code.toLowerCase();
3763
- return code === "sdk:network-error" || code === "sdk:timeout";
3764
- }
3765
- }
3766
- ];
3767
- const genericErrorRules = [
3768
- {
3769
- kind: "network",
3770
- matches: (error) => includesAny(error.message.toLowerCase(), [
3771
- "network",
3772
- "timeout",
3773
- "timed out"
3774
- ])
3775
- },
3776
- {
3777
- kind: "max_object_size",
3778
- matches: (error) => includesAny(error.message.toLowerCase(), [
3779
- "too large",
3780
- "payload too large",
3781
- "413"
3782
- ])
3783
- },
3784
- {
3785
- kind: "unsupported_mime",
3786
- matches: (error) => includesAny(error.message.toLowerCase(), [
3787
- "mime",
3788
- "content-type",
3789
- "unsupported media"
3790
- ])
3791
- },
3792
- {
3793
- kind: "quota_or_storage_limit",
3794
- matches: (error) => includesAny(error.message.toLowerCase(), ["quota", "storage limit"])
3795
- },
3796
- {
3797
- kind: "auth_or_permission",
3798
- matches: (error) => includesAny(error.message.toLowerCase(), [
3799
- "unauthorized",
3800
- "forbidden",
3801
- "permission"
3802
- ])
3803
- }
3804
- ];
3805
- const classifyWithRules = (error, rules) => rules.find((rule) => rule.matches(error))?.kind;
3806
- const classifyManagementApiError = (error) => {
3807
- const classified = classifyWithRules(error, managementErrorRules);
3808
- if (classified) return classified;
3809
- if (error.code.toLowerCase().startsWith("sdk:") || typeof error.statusCode === "number") {
3810
- return "cms_api";
3811
- }
3812
- return "unknown";
3813
- };
3814
- const classifyErrorMessage = (error) => classifyWithRules(error, genericErrorRules) ?? "unknown";
3815
- function classifyLocalMediaUploadFailure(error) {
3816
- if (error instanceof ManagementApiError) {
3817
- return classifyManagementApiError(error);
3818
- }
3819
- if (error instanceof Error) {
3820
- return classifyErrorMessage(error);
3821
- }
3822
- return "unknown";
3823
- }
3824
-
3825
- //#endregion
3826
- //#region src/media/push-application.ts
3827
- const rawErrorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
3828
- const makeReport = (params) => {
3829
- const failedIdentifiers = params.items.filter((item) => item.status === "failed").map((item) => item.identifier);
3830
- const missingIdentifiers = params.items.filter((item) => item.status === "missing_local_file").map((item) => item.identifier);
3831
- return {
3832
- targetEnv: params.targetEnv,
3833
- summary: { ...params.summary },
3834
- items: params.items,
3835
- failedIdentifiers,
3836
- missingIdentifiers
3837
- };
3838
- };
3839
- const reportForOperation = (operation, status, targetEnv, message, failureKind, rawError) => {
3840
- const file = "file" in operation ? operation.file : undefined;
3841
- return {
3842
- identifier: operation.identifier,
3843
- status,
3844
- targetEnv,
3845
- ...file ? {
3846
- filename: file.filename,
3847
- sizeBytes: file.dataLength,
3848
- mimeType: file.mimeType
3849
- } : {},
3850
- locations: operation.locations,
3851
- message,
3852
- failureKind,
3853
- rawError
3854
- };
3855
- };
3856
- const applySimpleMediaOperation = (operation, state, targetEnv) => {
3857
- switch (operation.kind) {
3858
- case "unchanged":
3859
- state.assetsByIdentifier.set(operation.identifier, operation.asset);
3860
- state.summary.resolved += 1;
3861
- state.reportItems.push(reportForOperation(operation, "resolved", targetEnv));
3862
- return;
3863
- case "reused":
3864
- state.assetsByIdentifier.set(operation.identifier, operation.asset);
3865
- state.summary.resolved += 1;
3866
- state.summary.reused += 1;
3867
- state.reportItems.push(reportForOperation(operation, "reused", targetEnv, "Reused existing CMS asset with matching identifier and checksum."));
3868
- return;
3869
- case "skipped":
3870
- state.summary.skipped += 1;
3871
- state.reportItems.push(reportForOperation(operation, "skipped", targetEnv, operation.message));
3872
- return;
3873
- case "missing_local_file":
3874
- state.reportItems.push(reportForOperation(operation, "missing_local_file", targetEnv, operation.message));
3875
- return;
3876
- case "conflict":
3877
- state.reportItems.push(reportForOperation(operation, "failed", targetEnv, operation.message, "unknown"));
3878
- return;
3879
- default: return assertNever(operation);
3880
- }
3881
- };
3882
- const applyUpload = async (operation, context, state) => {
3883
- const pendingItem = reportForOperation(operation, "pending_upload", context.targetEnv);
3884
- if (context.dryRun) {
3885
- state.summary.pendingUploads += 1;
3886
- state.reportItems.push(pendingItem);
3887
- return;
3888
- }
3889
- context.onEvent?.({
3890
- type: "upload_started",
3891
- item: pendingItem
3892
- });
3893
- try {
3894
- const result = await context.client.media.upload({
3895
- data: Buffer.from(operation.file.dataBase64, "base64"),
3896
- filename: operation.file.filename,
3897
- contentType: operation.file.mimeType,
3898
- identifier: operation.identifier
3899
- });
3900
- state.uploadedIdentifiers.push(operation.identifier);
3901
- if (result.status === "created") state.summary.uploaded += 1;
3902
- state.summary.resolved += 1;
3903
- const uploadedItem = reportForOperation(operation, "uploaded", context.targetEnv, result.status === "created" ? "Uploaded local media file." : "CMS reported media already exists.");
3904
- state.reportItems.push(uploadedItem);
3905
- context.onEvent?.({
3906
- type: "upload_succeeded",
3907
- item: uploadedItem
3908
- });
3909
- } catch (error) {
3910
- const message = rawErrorMessage$1(error);
3911
- const failedItem = reportForOperation(operation, "failed", context.targetEnv, `Failed to upload "${operation.identifier}": ${message}`, classifyLocalMediaUploadFailure(error), message);
3912
- state.reportItems.push(failedItem);
3913
- state.errors.push(`Failed to upload "${operation.identifier}" (${operation.file.filename}, ${operation.file.mimeType}): ${message}`);
3914
- context.onEvent?.({
3915
- type: "upload_failed",
3916
- item: failedItem
3917
- });
3918
- }
3919
- };
3920
- const applyMediaOperation = async (operation, context, state) => {
3921
- switch (operation.kind) {
3922
- case "unchanged":
3923
- case "reused":
3924
- case "skipped":
3925
- case "missing_local_file":
3926
- case "conflict":
3927
- applySimpleMediaOperation(operation, state, context.targetEnv);
3928
- return;
3929
- case "upload":
3930
- await applyUpload(operation, context, state);
3931
- return;
3932
- default: return assertNever(operation);
3933
- }
3934
- };
3935
- const createApplyState = (plan) => ({
3936
- summary: { ...plan.summary },
3937
- errors: [...plan.errors],
3938
- reportItems: [],
3939
- assetsByIdentifier: new Map(),
3940
- uploadedIdentifiers: []
3941
- });
3942
- const applyMediaOperations = async (operations, context, state) => {
3943
- for (const operation of operations) {
3944
- await applyMediaOperation(operation, context, state);
3945
- }
3946
- };
3947
- const resolveUploadedAssets = async (state, client, dryRun) => {
3948
- if (dryRun || state.uploadedIdentifiers.length === 0) return;
3949
- const resolvedUploads = await client.media.resolve({ identifiers: state.uploadedIdentifiers });
3950
- for (const asset of resolvedUploads.assets) {
3951
- if (asset.identifier) {
3952
- state.assetsByIdentifier.set(asset.identifier, asset);
3953
- }
3954
- }
3955
- for (const missing of resolvedUploads.missingIdentifiers) {
3956
- state.errors.push(`Upload succeeded but identifier "${missing}" was not found on the CMS.`);
3957
- }
3958
- };
3959
- const fileByIdentifier = (operations) => {
3960
- const files = new Map();
3961
- for (const operation of operations) {
3962
- if ("file" in operation && !files.has(operation.identifier)) {
3963
- files.set(operation.identifier, operation.file);
3964
- }
3965
- }
3966
- return files;
3967
- };
3968
- const rewriteAppliedReferences = (plan, state) => {
3969
- const content = normalizeLocalContentMedia(plan.content, state.assetsByIdentifier);
3970
- const manifest = structuredClone(plan.manifest);
3971
- const files = fileByIdentifier(plan.operations);
3972
- for (const reference of plan.references) {
3973
- const asset = state.assetsByIdentifier.get(reference.identifier);
3974
- if (!asset) continue;
3975
- updateManifestEntry(manifest, reference.identifier, asset, files.get(reference.identifier));
3976
- }
3977
- return {
3978
- content,
3979
- manifest
3980
- };
3981
- };
3982
- const finalSummary = (summary, dryRun) => ({
3983
- ...summary,
3984
- pendingUploads: dryRun ? summary.pendingUploads : 0
3985
- });
3986
- async function applyLocalMediaPush(options) {
3987
- const { plan, client, dryRun, onEvent } = options;
3988
- const state = createApplyState(plan);
3989
- const context = {
3990
- client,
3991
- dryRun,
3992
- targetEnv: plan.targetEnv,
3993
- onEvent
3994
- };
3995
- await applyMediaOperations(plan.operations, context, state);
3996
- await resolveUploadedAssets(state, client, dryRun);
3997
- const rewritten = rewriteAppliedReferences(plan, state);
3998
- const summary = finalSummary(state.summary, dryRun);
3999
- const hasChanges = summary.uploaded > 0 || summary.pendingUploads > 0;
4000
- return {
4001
- ...rewritten,
4002
- summary,
4003
- report: makeReport({
4004
- targetEnv: plan.targetEnv,
4005
- summary,
4006
- items: state.reportItems
4007
- }),
4008
- warnings: [...plan.warnings],
4009
- errors: state.errors,
4010
- hasChanges
4011
- };
4012
- }
4013
-
4014
- //#endregion
4015
- //#region src/media/pull-assets.ts
4016
- const buildPullAssetIndexes = (resolved) => {
4017
- const assetsById = new Map(resolved.assets.map((asset) => [asset.id, asset]));
4018
- const assetsByIdentifier = new Map();
4019
- for (const asset of resolved.assets) {
4020
- assetsByIdentifier.set(asset.identifier, asset);
4021
- }
4022
- return {
4023
- assetsById,
4024
- assetsByIdentifier
4025
- };
4026
- };
4027
- const resolveReferencedAsset = (reference, indexes) => {
4028
- const byId = reference.value.assetId ? indexes.assetsById.get(reference.value.assetId) : undefined;
4029
- if (byId) return byId;
4030
- return reference.value.identifier ? indexes.assetsByIdentifier.get(reference.value.identifier) : undefined;
4031
- };
4032
- const normalizePullIdentifier = (options) => {
4033
- const { initial, location, existingIdentifiers } = options;
4034
- if (!initial) return {
4035
- identifier: null,
4036
- canonicalized: 0
4037
- };
4038
- const normalized = slugifyMediaIdentifier(initial);
4039
- if (!normalized) {
4040
- return {
4041
- identifier: initial,
4042
- canonicalized: 0,
4043
- warning: `Could not normalize identifier "${initial}" at ${location}.`
4044
- };
4045
- }
4046
- existingIdentifiers.add(normalized);
4047
- return {
4048
- identifier: normalized,
4049
- canonicalized: normalized === initial ? 0 : 1
4050
- };
4051
- };
4052
- const planPullAssetReference = (options) => {
4053
- const resolvedAsset = resolveReferencedAsset(options.reference, options.indexes);
4054
- let canonicalized = 0;
4055
- let identifier = resolvedAsset?.identifier ?? options.reference.value.identifier ?? null;
4056
- const normalized = normalizePullIdentifier({
4057
- initial: identifier,
4058
- location: options.reference.location,
4059
- existingIdentifiers: options.existingIdentifiers
4060
- });
4061
- canonicalized += normalized.canonicalized;
4062
- identifier = normalized.identifier;
4063
- if (normalized.warning && normalized.identifier) {
4064
- return {
4065
- plan: {
4066
- kind: "identified",
4067
- reference: options.reference,
4068
- resolvedAsset,
4069
- identifier: normalized.identifier
4070
- },
4071
- canonicalized,
4072
- warning: normalized.warning
4073
- };
4074
- }
4075
- if (resolvedAsset && identifier) {
4076
- options.indexes.assetsByIdentifier.set(identifier, resolvedAsset);
4077
- }
4078
- return {
4079
- plan: identifier ? {
4080
- kind: "identified",
4081
- reference: options.reference,
4082
- resolvedAsset,
4083
- identifier
4084
- } : {
4085
- kind: "unidentified",
4086
- reference: options.reference,
4087
- resolvedAsset,
4088
- identifier: null
4089
- },
4090
- canonicalized
4091
- };
4092
- };
4093
- const planPullAssetIdentifiers = (options) => {
4094
- const indexes = buildPullAssetIndexes(options.resolved);
4095
- const assetsById = new Map(indexes.assetsById);
4096
- const assetsByIdentifier = new Map(indexes.assetsByIdentifier);
4097
- const existingIdentifiers = new Set(options.identifiers);
4098
- for (const asset of options.resolved.assets) {
4099
- existingIdentifiers.add(asset.identifier);
4100
- }
4101
- let referencesCanonicalized = 0;
4102
- const warnings = [];
4103
- const references = [];
4104
- for (const reference of options.references) {
4105
- const planned = planPullAssetReference({
4106
- reference,
4107
- indexes: {
4108
- assetsById,
4109
- assetsByIdentifier
4110
- },
4111
- existingIdentifiers
4112
- });
4113
- referencesCanonicalized += planned.canonicalized;
4114
- if (planned.warning) warnings.push(planned.warning);
4115
- references.push(planned.plan);
4116
- }
4117
- return {
4118
- indexes: {
4119
- assetsById,
4120
- assetsByIdentifier
4121
- },
4122
- references,
4123
- summary: { referencesCanonicalized },
4124
- warnings
4125
- };
4126
- };
4127
- const applyPullAssetPlan = (options) => {
4128
- const assetsById = new Map(options.plan.indexes.assetsById);
4129
- const assetsByIdentifier = new Map(options.plan.indexes.assetsByIdentifier);
4130
- const references = [];
4131
- for (const planned of options.plan.references) {
4132
- if (planned.kind === "unidentified") {
4133
- references.push(planned);
4134
- continue;
4135
- }
4136
- planned.reference.value.identifier = planned.identifier;
4137
- if (planned.resolvedAsset) {
4138
- updateManifestEntry(options.manifest, planned.identifier, planned.resolvedAsset, null);
4139
- }
4140
- references.push(planned);
4141
- }
4142
- return {
4143
- indexes: {
4144
- assetsById,
4145
- assetsByIdentifier
4146
- },
4147
- references
4148
- };
4149
- };
4150
-
4151
- //#endregion
4152
- //#region src/media/pull-references.ts
4153
- /**
4154
- * Discover ordinary media values and normalize rich-text nodes that already
4155
- * have an identifier. Rich-text nodes with only a source URL are returned as
4156
- * explicit import intents; importing is an effectful stage owned by the
4157
- * workflow, not by content traversal.
4158
- */
4159
- const collectPullMediaReferences = (content) => {
4160
- const references = [];
4161
- const identifiers = new Set();
4162
- const assetIds = new Set();
4163
- const warnings = [];
4164
- walkPulledContentMedia(content, (value, location) => {
4165
- references.push({
4166
- value,
4167
- location
4168
- });
4169
- if (typeof value.identifier === "string") {
4170
- identifiers.add(value.identifier);
4171
- }
4172
- if (typeof value.assetId === "string") {
4173
- assetIds.add(value.assetId);
4174
- }
4175
- });
4176
- const richTextImports = [];
4177
- let referencesCanonicalized = 0;
4178
- const richTextNodes = collectRichTextImageAttrs(content);
4179
- for (const node of richTextNodes) {
4180
- const attrs = node.attrs;
4181
- const rawIdentifier = typeof attrs.identifier === "string" ? attrs.identifier.trim() : "";
4182
- const rawSrc = typeof attrs.src === "string" ? attrs.src.trim() : "";
4183
- if (rawIdentifier) {
4184
- const normalized = normalizeMediaIdentifierInput(rawIdentifier);
4185
- if (!normalized) {
4186
- warnings.push(`Invalid rich text image identifier "${rawIdentifier}" at ${node.location}.`);
4187
- continue;
4188
- }
4189
- if (normalized !== rawIdentifier) {
4190
- attrs.identifier = normalized;
4191
- referencesCanonicalized += 1;
4192
- }
4193
- delete attrs.src;
4194
- identifiers.add(normalized);
4195
- references.push({
4196
- value: {
4197
- type: "image",
4198
- identifier: normalized
4199
- },
4200
- location: node.location
4201
- });
4202
- continue;
4203
- }
4204
- if (rawSrc) {
4205
- richTextImports.push({
4206
- attrs,
4207
- location: node.location,
4208
- src: rawSrc
4209
- });
4210
- }
4211
- }
4212
- return {
4213
- references,
4214
- identifiers,
4215
- assetIds,
4216
- richTextImports,
4217
- referencesCanonicalized,
4218
- cmsImportsPlanned: 0,
4219
- cmsAssetsImported: 0,
4220
- warnings
4221
- };
4222
- };
4223
- const recordRichTextImport = (collection, intent, identifier) => {
4224
- intent.attrs.identifier = identifier;
4225
- delete intent.attrs.src;
4226
- collection.identifiers.add(identifier);
4227
- const reference = {
4228
- value: {
4229
- type: "image",
4230
- identifier
4231
- },
4232
- location: intent.location
4233
- };
4234
- collection.references.push(reference);
4235
- return {
4236
- identifier,
4237
- reference
4238
- };
4239
- };
4240
-
4241
- //#endregion
4242
- //#region src/media/pull-rich-text.ts
4243
- const chooseChecksumIdentifier = (candidates) => {
4244
- const chosen = candidates.slice().sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
4245
- if (!chosen) return null;
4246
- return chosen.identifier;
4247
- };
4248
- const assignOrUploadRichTextIdentifier = async (options) => {
4249
- for (let attempt = 0; attempt < 10; attempt += 1) {
4250
- const candidate = resolveMediaIdentifier(options.base, options.existingIdentifiers);
4251
- options.existingIdentifiers.add(candidate);
4252
- if (options.dryRun) return {
4253
- kind: "planned-upload",
4254
- identifier: candidate
4255
- };
4256
- try {
4257
- const uploaded = await options.client.media.upload({
4258
- data: options.data,
4259
- filename: options.filename,
4260
- contentType: options.mimeType,
4261
- identifier: candidate
4262
- });
4263
- return {
4264
- kind: "uploaded",
4265
- identifier: uploaded.identifier
4266
- };
4267
- } catch {
4268
- continue;
4269
- }
4270
- }
4271
- throw new Error("Unable to assign a unique identifier for rich text image import");
4272
- };
4273
- const importRichTextImageSrcToIdentifier = async (options) => {
4274
- const { client, src, location, existingIdentifiers, dryRun } = options;
4275
- const downloaded = await downloadMedia(src);
4276
- if (!downloaded) {
4277
- throw new Error(`Failed to download rich text image from ${src}`);
4278
- }
4279
- const mimeType = downloaded.contentType.split(";")[0]?.trim() || "application/octet-stream";
4280
- if (!mimeType.startsWith("image/")) {
4281
- throw new Error(`Unsupported content-type for rich text image import: ${mimeType}`);
4282
- }
4283
- const checksum = sha256(downloaded.data);
4284
- const matches = await client.media.findByChecksum([checksum]);
4285
- const candidates = matches.filter((asset) => asset.checksumSha256 === checksum);
4286
- const existingIdentifier = chooseChecksumIdentifier(candidates);
4287
- if (existingIdentifier) return {
4288
- kind: "existing",
4289
- identifier: existingIdentifier
4290
- };
4291
- return assignOrUploadRichTextIdentifier({
4292
- client,
4293
- base: chooseRichTextIdentifierBase(location, src),
4294
- existingIdentifiers,
4295
- dryRun,
4296
- data: downloaded.data,
4297
- filename: ensureFilenameExtension(parseFilenameFromUrl(src) ?? "image", mimeType),
4298
- mimeType
4299
- });
4300
- };
4301
- async function importRichTextPullReferences(options) {
4302
- const { collection, client, dryRun, allowCmsWrites } = options;
4303
- const richTextExisting = new Set(collection.identifiers);
4304
- for (const intent of collection.richTextImports) {
4305
- try {
4306
- const identifier = await importRichTextImageSrcToIdentifier({
4307
- client,
4308
- src: intent.src,
4309
- location: intent.location,
4310
- existingIdentifiers: richTextExisting,
4311
- dryRun: dryRun || !allowCmsWrites
4312
- });
4313
- if (identifier.kind === "planned-upload") {
4314
- collection.cmsImportsPlanned += 1;
4315
- if (!dryRun && !allowCmsWrites) continue;
4316
- }
4317
- if (identifier.kind === "uploaded") {
4318
- collection.cmsAssetsImported += 1;
4319
- }
4320
- recordRichTextImport(collection, intent, identifier.identifier);
4321
- richTextExisting.add(identifier.identifier);
4322
- } catch (error) {
4323
- const message = error instanceof Error ? error.message : String(error);
4324
- collection.warnings.push(`Failed to import rich text image @ ${intent.location}: ${message}`);
4325
- }
4326
- }
4327
- }
4328
-
4329
- //#endregion
4330
- //#region src/media/pull-download.ts
4331
- const planLocalMediaDownload = (options) => {
4332
- const { reference, asset, identifier, existingEntry, local } = options;
4333
- if (!asset) return {
4334
- kind: "missing-asset",
4335
- reference
4336
- };
4337
- const filename = buildLocalFilename(identifier, asset);
4338
- if (local.exists) {
4339
- if (asset.checksumSha256) {
4340
- if (existingEntry?.checksum === asset.checksumSha256) {
4341
- return {
4342
- kind: "skip",
4343
- reference,
4344
- asset,
4345
- identifier,
4346
- filename
4347
- };
4348
- }
4349
- if (local.checksum === asset.checksumSha256) {
4350
- return {
4351
- kind: "skip",
4352
- reference,
4353
- asset,
4354
- identifier,
4355
- filename
4356
- };
4357
- }
4358
- } else if (existingEntry?.checksum) {
4359
- return {
4360
- kind: "skip",
4361
- reference,
4362
- asset,
4363
- identifier,
4364
- filename
4365
- };
4366
- }
4367
- }
4368
- return {
4369
- kind: "download",
4370
- reference,
4371
- asset,
4372
- identifier,
4373
- filename
4374
- };
4375
- };
4376
- const readExistingLocalMedia = async (options) => {
4377
- const exists = await localMediaFileExists(options.localFilePath);
4378
- if (!exists) return { exists: false };
4379
- if (!options.asset.checksumSha256) {
4380
- return { exists: true };
4381
- }
4382
- const manifestChecksum = options.existingEntry?.checksum;
4383
- if (manifestChecksum === options.asset.checksumSha256) {
4384
- return {
4385
- exists: true,
4386
- checksum: manifestChecksum
4387
- };
4388
- }
4389
- const localChecksum = manifestChecksum ?? await sha256FromFile(options.localFilePath);
4390
- return {
4391
- exists: true,
4392
- checksum: localChecksum
4393
- };
4394
- };
4395
- const removeStaleLocalFile = async (options) => {
4396
- if (options.dryRun || !options.previousFilename || options.previousFilename === options.filename) {
4397
- return;
4398
- }
4399
- const previousPath = path$1.join(options.contentDir, "media", options.previousFilename);
4400
- if (await localMediaFileExists(previousPath)) {
4401
- await fs$1.unlink(previousPath).catch(() => {});
4402
- }
4403
- };
4404
- const applySkipDecision = (decision, context, local, existingEntry) => {
4405
- if (decision.asset.checksumSha256 && local.checksum === decision.asset.checksumSha256 && existingEntry?.checksum !== decision.asset.checksumSha256) {
4406
- context.manifest.assets[decision.identifier] = {
4407
- ...existingEntry,
4408
- checksum: local.checksum,
4409
- filename: decision.filename,
4410
- mimeType: decision.asset.mimeType ?? existingEntry?.mimeType
4411
- };
4412
- }
4413
- context.summary.skipped += 1;
4414
- updateManifestEntry(context.manifest, decision.identifier, decision.asset, null);
4415
- };
4416
- const applyDownloadDecision = async (decision, context) => {
4417
- context.summary.pendingDownloads += 1;
4418
- if (context.dryRun) return;
4419
- const relativePath = normalizeStoragePath(decision.asset.storagePath, context.siteId);
4420
- const urlResult = buildStorageUrl(context.supabaseUrl, relativePath, context.siteId, decision.asset.storageBucket ?? "media");
4421
- if (!urlResult.ok) {
4422
- context.warnings.push(`Invalid storage path for "${decision.identifier}": ${urlResult.error.cause.tag}.`);
4423
- return;
4424
- }
4425
- const downloaded = await downloadMedia(urlResult.value);
4426
- if (!downloaded) {
4427
- context.warnings.push(`Failed to download media for "${decision.identifier}" from ${urlResult.value}.`);
4428
- return;
4429
- }
4430
- const written = await writeLocalMediaFile(context.contentDir, decision.filename, downloaded.data);
4431
- context.warnings.push(...localMediaWriteWarningMessages(decision.identifier, written));
4432
- context.manifest.assets[decision.identifier] = {
4433
- checksum: sha256(downloaded.data),
4434
- filename: decision.filename,
4435
- mimeType: decision.asset.mimeType
4436
- };
4437
- context.summary.downloaded += 1;
4438
- };
4439
- const syncPullMediaDownload = async (planned, indexes, context) => {
4440
- const { reference, identifier } = planned;
4441
- const byId = reference.value.assetId ? indexes.assetsById.get(reference.value.assetId) : undefined;
4442
- const asset = byId ?? indexes.assetsByIdentifier.get(identifier);
4443
- if (!asset) {
4444
- context.warnings.push(`Unable to resolve asset for "${identifier}" at ${reference.location}.`);
4445
- return;
4446
- }
4447
- const filename = buildLocalFilename(identifier, asset);
4448
- const localFilePath = path$1.join(context.contentDir, "media", filename);
4449
- const existingEntry = context.manifest.assets[identifier];
4450
- await removeStaleLocalFile({
4451
- contentDir: context.contentDir,
4452
- previousFilename: existingEntry?.filename,
4453
- filename,
4454
- dryRun: context.dryRun
4455
- });
4456
- const local = await readExistingLocalMedia({
4457
- localFilePath,
4458
- existingEntry,
4459
- asset
4460
- });
4461
- const decision = planLocalMediaDownload({
4462
- reference,
4463
- asset,
4464
- identifier,
4465
- existingEntry,
4466
- local
4467
- });
4468
- switch (decision.kind) {
4469
- case "missing-asset":
4470
- context.warnings.push(`Unable to resolve asset for "${identifier}" at ${reference.location}.`);
4471
- return;
4472
- case "skip":
4473
- applySkipDecision(decision, context, local, existingEntry);
4474
- return;
4475
- case "download": await applyDownloadDecision(decision, context);
4476
- }
4477
- };
4478
- const syncPullMediaDownloads = async (options) => {
4479
- for (const planned of options.references) {
4480
- if (planned.kind === "identified") {
4481
- await syncPullMediaDownload(planned, options.indexes, options);
4482
- }
4483
- }
4484
- if (!options.dryRun) options.summary.pendingDownloads = 0;
4485
- };
4486
- const sha256FromFile = (filePath) => readLocalMediaChecksum(filePath, FILE_SIZE_LIMITS.rawVideoBeforeCompression);
4487
- function localMediaWriteWarningMessages(identifier, outcome) {
4488
- if (outcome.kind === "written") return [];
4489
- return outcome.warnings.map((warning) => {
4490
- switch (warning.tag) {
4491
- case "temporary_cleanup_failed": return `Downloaded media for "${identifier}", but could not remove temporary file ${warning.temporaryPath}.`;
4492
- case "directory_sync_failed": return `Downloaded media for "${identifier}", but could not confirm the destination directory was synced.`;
4493
- }
4494
- });
4495
- }
4496
-
4497
- //#endregion
4498
- //#region src/media/pull-workflow.ts
4499
- async function syncLocalMediaForPull(options) {
4500
- const { contentDir, content, client, dryRun, allowCmsWrites = true, supabaseUrl, siteId, downloadFiles } = options;
4501
- const manifest = await readMediaManifest(contentDir);
4502
- const collection = collectPullMediaReferences(content);
4503
- await importRichTextPullReferences({
4504
- collection,
4505
- client,
4506
- dryRun,
4507
- allowCmsWrites
4508
- });
4509
- const summary = {
4510
- totalReferences: collection.references.length,
4511
- referencesCanonicalized: collection.referencesCanonicalized,
4512
- cmsImportsPlanned: collection.cmsImportsPlanned,
4513
- cmsAssetsImported: collection.cmsAssetsImported,
4514
- downloaded: 0,
4515
- skipped: 0,
4516
- pendingDownloads: 0
4517
- };
4518
- const warnings = [...collection.warnings];
4519
- if (collection.references.length === 0) {
4520
- return {
4521
- content,
4522
- manifest,
4523
- summary,
4524
- warnings
4525
- };
4526
- }
4527
- const resolved = await client.media.resolve({
4528
- ...collection.identifiers.size > 0 ? { identifiers: Array.from(collection.identifiers) } : {},
4529
- ...collection.assetIds.size > 0 ? { assetIds: Array.from(collection.assetIds) } : {}
4530
- });
4531
- const assetPlan = planPullAssetIdentifiers({
4532
- references: collection.references,
4533
- resolved,
4534
- identifiers: collection.identifiers
4535
- });
4536
- warnings.push(...assetPlan.warnings);
4537
- summary.referencesCanonicalized += assetPlan.summary.referencesCanonicalized;
4538
- const applied = applyPullAssetPlan({
4539
- plan: assetPlan,
4540
- manifest
4541
- });
4542
- if (!downloadFiles) {
4543
- return {
4544
- content,
4545
- manifest,
4546
- summary,
4547
- warnings
4548
- };
4549
- }
4550
- if (!supabaseUrl || !siteId) {
4551
- throw new Error("syncLocalMediaForPull requires supabaseUrl and siteId when downloadFiles=true.");
4552
- }
4553
- await syncPullMediaDownloads({
4554
- contentDir,
4555
- manifest,
4556
- references: applied.references,
4557
- indexes: applied.indexes,
4558
- supabaseUrl,
4559
- siteId,
4560
- dryRun,
4561
- summary,
4562
- warnings
4563
- });
4564
- return {
4565
- content,
4566
- manifest,
4567
- summary,
4568
- warnings
4569
- };
4570
- }
4571
-
4572
- //#endregion
4573
- //#region src/media/local-sync.ts
4574
- const rawErrorMessage = (error) => error instanceof Error ? error.message : String(error);
4575
- async function revalidateLocalMediaPushPlan(plan, client) {
4576
- const identifiers = plan.mediaReadSet.map((observation) => observation.identifier);
4577
- if (identifiers.length === 0) return { ok: true };
4578
- try {
4579
- const resolved = await client.media.resolve({ identifiers });
4580
- const assets = new Map(resolved.assets.map((asset) => [asset.identifier, asset]));
4581
- const localChecksums = new Map(plan.mediaReadSet.map((observation) => [observation.identifier, observation.localChecksum.kind === "present" ? observation.localChecksum.checksum : undefined]));
4582
- const observedChecksums = Array.from(new Set(plan.mediaReadSet.flatMap((observation) => observation.localChecksum.kind === "present" && observation.localChecksum.lookup.kind === "read" ? [observation.localChecksum.checksum] : [])));
4583
- const currentChecksumAssets = observedChecksums.length > 0 ? await client.media.findByChecksum(observedChecksums) : [];
4584
- const checksumAssetsByChecksum = new Map(observedChecksums.map((checksum) => [checksum, []]));
4585
- for (const asset of currentChecksumAssets) {
4586
- if (!asset.checksumSha256) continue;
4587
- const matches = checksumAssetsByChecksum.get(asset.checksumSha256);
4588
- if (!matches) continue;
4589
- matches.push(asset);
4590
- }
4591
- const current = buildMediaReadSet({
4592
- identifiers,
4593
- localChecksums,
4594
- initialAssets: assets,
4595
- initialMissing: new Set(resolved.missingIdentifiers),
4596
- checksumAssetsByChecksum
4597
- });
4598
- if (stableMediaReadSet(current) !== stableMediaReadSet(plan.mediaReadSet)) {
4599
- return {
4600
- ok: false,
4601
- error: {
4602
- kind: "remote-media-drift",
4603
- message: "Remote media state changed after preflight; re-run the push to refresh its media plan."
4604
- }
4605
- };
4606
- }
4607
- return { ok: true };
4608
- } catch (error) {
4609
- return {
4610
- ok: false,
4611
- error: {
4612
- kind: "remote-media-read-failed",
4613
- message: `Failed to revalidate remote media state: ${rawErrorMessage(error)}`
4614
- }
4615
- };
4616
- }
4617
- }
4618
- async function syncLocalMediaForPush(options) {
4619
- const plan = await prepareLocalMediaForPush(options);
4620
- const result = await applyLocalMediaPush({
4621
- plan,
4622
- client: options.client,
4623
- dryRun: options.dryRun,
4624
- onEvent: options.onEvent
4625
- });
4626
- Object.assign(options.content, result.content);
4627
- return {
4628
- ...result,
4629
- content: options.content
4630
- };
4631
- }
4632
-
4633
- //#endregion
4634
- 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 };