siteplane 0.1.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +5 -0
- package/README.md +32 -0
- package/dist/bin/siteplane.js +4383 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2260 -0
- package/dist/next.d.ts +91 -0
- package/dist/next.js +2710 -0
- package/package.json +45 -0
|
@@ -0,0 +1,4383 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bin/run-siteplane.ts
|
|
4
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
5
|
+
import { access as access2, readFile as readFile8 } from "fs/promises";
|
|
6
|
+
import { join as join7, resolve } from "path";
|
|
7
|
+
|
|
8
|
+
// ../cli/dist/analytics/manifest.js
|
|
9
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
10
|
+
import { dirname } from "path";
|
|
11
|
+
|
|
12
|
+
// ../shared/dist/actors.js
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
var ACTOR_TYPES = ["developer", "client", "agent", "system"];
|
|
15
|
+
var actorTypeSchema = z.enum(ACTOR_TYPES);
|
|
16
|
+
|
|
17
|
+
// ../shared/dist/analytics-defaults.js
|
|
18
|
+
var ANALYTICS_BATCH_LIMITS = {
|
|
19
|
+
maxEventsPerRequest: 50,
|
|
20
|
+
maxJsonBodyBytes: 64 * 1024,
|
|
21
|
+
maxPropertiesJsonBytes: 8 * 1024
|
|
22
|
+
};
|
|
23
|
+
var ANALYTICS_RETENTION_DEFAULTS = {
|
|
24
|
+
enabledRawEventRetentionDays: 30,
|
|
25
|
+
higherTierRawEventRetentionDays: 90,
|
|
26
|
+
maxRawEventRetentionDaysWithoutAdr: 180,
|
|
27
|
+
diagnosticsRetentionDays: 30,
|
|
28
|
+
rateLimitBucketRetentionDays: 2,
|
|
29
|
+
lateEventWindowHours: 72
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// ../shared/dist/analytics-booking.js
|
|
33
|
+
import { z as z2 } from "zod";
|
|
34
|
+
var bookingProviders = ["treatwell", "beautinda", "other"];
|
|
35
|
+
var bookingAttributionModes = [
|
|
36
|
+
"outbound_click_and_utm",
|
|
37
|
+
"provider_redirect",
|
|
38
|
+
"provider_reported",
|
|
39
|
+
"manual_import",
|
|
40
|
+
"verified_webhook"
|
|
41
|
+
];
|
|
42
|
+
var bookingSignalSources = [
|
|
43
|
+
"outbound_click",
|
|
44
|
+
"provider_redirect",
|
|
45
|
+
"provider_reported",
|
|
46
|
+
"manual_import",
|
|
47
|
+
"verified_webhook"
|
|
48
|
+
];
|
|
49
|
+
var bookingAttributionConfidences = [
|
|
50
|
+
"provider_redirect",
|
|
51
|
+
"provider_reported",
|
|
52
|
+
"manual_import",
|
|
53
|
+
"verified_webhook"
|
|
54
|
+
];
|
|
55
|
+
var bookingProviderSchema = z2.enum(bookingProviders);
|
|
56
|
+
var bookingAttributionModeSchema = z2.enum(bookingAttributionModes);
|
|
57
|
+
var bookingSignalSourceSchema = z2.enum(bookingSignalSources);
|
|
58
|
+
var bookingAttributionConfidenceSchema = z2.enum(bookingAttributionConfidences);
|
|
59
|
+
var bookingProviderHintSchema = z2.object({
|
|
60
|
+
bookingProvider: bookingProviderSchema,
|
|
61
|
+
bookingUrlHost: z2.string().min(1).max(253),
|
|
62
|
+
bookingAttributionMode: bookingAttributionModeSchema,
|
|
63
|
+
utmContent: z2.string().min(1).max(200).optional(),
|
|
64
|
+
successCondition: z2.literal("redirect_status=succeeded").optional()
|
|
65
|
+
}).superRefine((hint, context) => {
|
|
66
|
+
if (hint.successCondition !== void 0 && hint.bookingAttributionMode !== "provider_redirect") {
|
|
67
|
+
context.addIssue({
|
|
68
|
+
code: "custom",
|
|
69
|
+
message: "successCondition is allowed only for provider_redirect booking hints.",
|
|
70
|
+
path: ["successCondition"]
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ../shared/dist/analytics-contract.js
|
|
76
|
+
import { z as z3 } from "zod";
|
|
77
|
+
var ANALYTICS_CONTRACT_VERSION = "analytics-contract.v1";
|
|
78
|
+
var ANALYTICS_CONTRACT_HASH = "sha256:a91bddcc3c5e4196933b0335d03eaf41ff929cb736bdd694fe54d07ab52dada1";
|
|
79
|
+
var ANALYTICS_IMPORT_SCHEMA_VERSION = "analytics-provider-import.v1";
|
|
80
|
+
var analyticsImportTargetIdSchema = z3.string().min(1).max(120).regex(/^[a-zA-Z][a-zA-Z0-9]*(?:[._-][a-zA-Z0-9]+)*$/u);
|
|
81
|
+
var analyticsProviderImportRowSchema = z3.object({
|
|
82
|
+
targetId: analyticsImportTargetIdSchema,
|
|
83
|
+
occurredAt: z3.string().datetime({ offset: true }),
|
|
84
|
+
attributionConfidence: z3.enum(["provider_reported", "manual_import"]),
|
|
85
|
+
providerEventId: z3.string().trim().min(1).max(160).optional(),
|
|
86
|
+
idempotencyKey: z3.string().trim().min(1).max(160).optional()
|
|
87
|
+
}).strict().superRefine((row, context) => {
|
|
88
|
+
if (!row.providerEventId && !row.idempotencyKey) {
|
|
89
|
+
context.addIssue({
|
|
90
|
+
code: "custom",
|
|
91
|
+
message: "Each import row requires providerEventId or idempotencyKey.",
|
|
92
|
+
path: ["providerEventId"]
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
var analyticsProviderImportSchema = z3.object({
|
|
97
|
+
schemaVersion: z3.literal(ANALYTICS_IMPORT_SCHEMA_VERSION),
|
|
98
|
+
provider: z3.enum(["treatwell", "beautinda", "other"]),
|
|
99
|
+
sourceName: z3.string().trim().min(1).max(160).optional(),
|
|
100
|
+
rows: z3.array(analyticsProviderImportRowSchema).min(1).max(1e3)
|
|
101
|
+
}).strict();
|
|
102
|
+
|
|
103
|
+
// ../shared/dist/analytics-events.js
|
|
104
|
+
import { z as z4 } from "zod";
|
|
105
|
+
var ANALYTICS_SCHEMA_VERSION = "analytics.v1";
|
|
106
|
+
var ANALYTICS_KEY_PATTERN = /^(pk|sk)_siteplane_([A-Za-z0-9_-]{16})_[A-Za-z0-9_-]{43}$/;
|
|
107
|
+
var analyticsSessionIdSchema = z4.string().regex(/^cpa_s_[a-f0-9]{32}$/u);
|
|
108
|
+
var analyticsVisitorIdSchema = z4.string().regex(/^cpa_v_[a-f0-9]{32}$/u);
|
|
109
|
+
var analyticsPersistedIdentityHashSchema = z4.string().regex(/^[a-f0-9]{64}$/u);
|
|
110
|
+
var analyticsEventNames = [
|
|
111
|
+
"page_view",
|
|
112
|
+
"section_view",
|
|
113
|
+
"cta_click",
|
|
114
|
+
"outbound_click",
|
|
115
|
+
"email_click",
|
|
116
|
+
"phone_click",
|
|
117
|
+
"file_download",
|
|
118
|
+
"form_start",
|
|
119
|
+
"form_submit",
|
|
120
|
+
"form_error",
|
|
121
|
+
"lead_created",
|
|
122
|
+
"checkout_started",
|
|
123
|
+
"conversion",
|
|
124
|
+
"checkout_completed",
|
|
125
|
+
"payment_succeeded",
|
|
126
|
+
"subscription_started",
|
|
127
|
+
"purchase_completed",
|
|
128
|
+
"trial_started"
|
|
129
|
+
];
|
|
130
|
+
var serverOnlyAnalyticsEvents = [
|
|
131
|
+
"checkout_completed",
|
|
132
|
+
"payment_succeeded",
|
|
133
|
+
"subscription_started",
|
|
134
|
+
"purchase_completed",
|
|
135
|
+
"trial_started"
|
|
136
|
+
];
|
|
137
|
+
var analyticsEventSources = [
|
|
138
|
+
"browser",
|
|
139
|
+
"server",
|
|
140
|
+
"test",
|
|
141
|
+
"editor_preview",
|
|
142
|
+
"system"
|
|
143
|
+
];
|
|
144
|
+
var analyticsIngestionChannels = [
|
|
145
|
+
"browser_collector",
|
|
146
|
+
"server_sdk",
|
|
147
|
+
"provider_endpoint",
|
|
148
|
+
"provider_import",
|
|
149
|
+
"test",
|
|
150
|
+
"editor_preview",
|
|
151
|
+
"system_job"
|
|
152
|
+
];
|
|
153
|
+
var analyticsTargetTypes = [
|
|
154
|
+
"cta",
|
|
155
|
+
"form",
|
|
156
|
+
"section",
|
|
157
|
+
"file",
|
|
158
|
+
"link",
|
|
159
|
+
"conversion",
|
|
160
|
+
"server_conversion"
|
|
161
|
+
];
|
|
162
|
+
var analyticsDataAttributeEvents = [
|
|
163
|
+
"cta_click",
|
|
164
|
+
"outbound_click",
|
|
165
|
+
"email_click",
|
|
166
|
+
"phone_click",
|
|
167
|
+
"file_download",
|
|
168
|
+
"section_view",
|
|
169
|
+
"checkout_started",
|
|
170
|
+
"form"
|
|
171
|
+
];
|
|
172
|
+
var analyticsDeviceTypes = [
|
|
173
|
+
"desktop",
|
|
174
|
+
"tablet",
|
|
175
|
+
"mobile",
|
|
176
|
+
"unknown"
|
|
177
|
+
];
|
|
178
|
+
var analyticsBotClassifications = [
|
|
179
|
+
"normal",
|
|
180
|
+
"known_bot",
|
|
181
|
+
"suspicious",
|
|
182
|
+
"invalid"
|
|
183
|
+
];
|
|
184
|
+
var analyticsValidityStatuses = [
|
|
185
|
+
"accepted",
|
|
186
|
+
"rejected",
|
|
187
|
+
"ignored"
|
|
188
|
+
];
|
|
189
|
+
var analyticsEventNameSchema = z4.enum(analyticsEventNames);
|
|
190
|
+
var serverOnlyAnalyticsEventSchema = z4.enum(serverOnlyAnalyticsEvents);
|
|
191
|
+
var analyticsEventSourceSchema = z4.enum(analyticsEventSources);
|
|
192
|
+
var analyticsIngestionChannelSchema = z4.enum(analyticsIngestionChannels);
|
|
193
|
+
var analyticsTargetTypeSchema = z4.enum(analyticsTargetTypes);
|
|
194
|
+
var analyticsDataAttributeEventSchema = z4.enum(analyticsDataAttributeEvents);
|
|
195
|
+
var analyticsDeviceTypeSchema = z4.enum(analyticsDeviceTypes);
|
|
196
|
+
var analyticsBotClassificationSchema = z4.enum(analyticsBotClassifications);
|
|
197
|
+
var analyticsValidityStatusSchema = z4.enum(analyticsValidityStatuses);
|
|
198
|
+
var analyticsPropertiesSchema = z4.record(z4.string(), z4.unknown());
|
|
199
|
+
var analyticsTimestampSchema = z4.string().datetime({ offset: true });
|
|
200
|
+
var analyticsBrowserEventSchema = z4.object({
|
|
201
|
+
eventId: z4.string().min(1).max(160),
|
|
202
|
+
eventName: analyticsEventNameSchema,
|
|
203
|
+
eventSource: analyticsEventSourceSchema.optional().default("browser"),
|
|
204
|
+
targetId: z4.string().min(1).max(160).optional(),
|
|
205
|
+
targetType: analyticsTargetTypeSchema.optional(),
|
|
206
|
+
visitorId: analyticsVisitorIdSchema.optional(),
|
|
207
|
+
sessionId: analyticsSessionIdSchema,
|
|
208
|
+
idempotencyKey: z4.string().min(1).max(240).optional(),
|
|
209
|
+
occurredAt: analyticsTimestampSchema,
|
|
210
|
+
pageUrl: z4.string().url().max(2048).optional(),
|
|
211
|
+
pagePath: z4.string().min(1).max(1024).optional(),
|
|
212
|
+
routeTemplate: z4.string().min(1).max(1024).optional(),
|
|
213
|
+
referrer: z4.string().url().max(2048).optional(),
|
|
214
|
+
utmSource: z4.string().min(1).max(200).optional(),
|
|
215
|
+
utmMedium: z4.string().min(1).max(200).optional(),
|
|
216
|
+
utmCampaign: z4.string().min(1).max(200).optional(),
|
|
217
|
+
utmContent: z4.string().min(1).max(200).optional(),
|
|
218
|
+
utmTerm: z4.string().min(1).max(200).optional(),
|
|
219
|
+
deviceType: analyticsDeviceTypeSchema.optional(),
|
|
220
|
+
browserName: z4.string().min(1).max(120).optional(),
|
|
221
|
+
osName: z4.string().min(1).max(120).optional(),
|
|
222
|
+
country: z4.string().min(1).max(80).optional(),
|
|
223
|
+
region: z4.string().min(1).max(120).optional(),
|
|
224
|
+
properties: analyticsPropertiesSchema.optional().default({})
|
|
225
|
+
}).superRefine((event, context) => {
|
|
226
|
+
if (isServerOnlyAnalyticsEvent(event.eventName)) {
|
|
227
|
+
context.addIssue({
|
|
228
|
+
code: "custom",
|
|
229
|
+
message: "Browser analytics payloads cannot contain server-only events.",
|
|
230
|
+
path: ["eventName"]
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
if (event.eventSource === "server" || event.eventSource === "system") {
|
|
234
|
+
context.addIssue({
|
|
235
|
+
code: "custom",
|
|
236
|
+
message: "Browser analytics payloads cannot use server/system sources.",
|
|
237
|
+
path: ["eventSource"]
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
if (event.targetType === "server_conversion") {
|
|
241
|
+
context.addIssue({
|
|
242
|
+
code: "custom",
|
|
243
|
+
message: "Browser analytics payloads cannot use server conversion targets.",
|
|
244
|
+
path: ["targetType"]
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
var analyticsBrowserEventPayloadSchema = z4.object({
|
|
249
|
+
siteKey: z4.string().min(1),
|
|
250
|
+
schemaVersion: z4.literal(ANALYTICS_SCHEMA_VERSION),
|
|
251
|
+
mode: z4.enum(["live", "test"]).optional().default("live"),
|
|
252
|
+
events: z4.array(analyticsBrowserEventSchema).min(1).max(50)
|
|
253
|
+
});
|
|
254
|
+
var analyticsNormalizedEventBaseSchema = z4.object({
|
|
255
|
+
site_id: z4.string().uuid(),
|
|
256
|
+
event_id: z4.string().min(1).max(160),
|
|
257
|
+
schema_version: z4.literal(ANALYTICS_SCHEMA_VERSION),
|
|
258
|
+
event_name: analyticsEventNameSchema,
|
|
259
|
+
event_source: analyticsEventSourceSchema,
|
|
260
|
+
ingestion_channel: analyticsIngestionChannelSchema,
|
|
261
|
+
target_id: z4.string().min(1).max(160).nullable().optional(),
|
|
262
|
+
target_type: analyticsTargetTypeSchema.nullable().optional(),
|
|
263
|
+
dedupe_key: z4.string().regex(/^analytics_dedupe_v1_[a-f0-9]{64}$/u).nullable().optional(),
|
|
264
|
+
idempotency_key: z4.string().min(1).max(240).nullable().optional(),
|
|
265
|
+
bot_classification: analyticsBotClassificationSchema,
|
|
266
|
+
validity_status: analyticsValidityStatusSchema,
|
|
267
|
+
rejection_reason: z4.string().min(1).max(240).nullable().optional(),
|
|
268
|
+
occurred_at: analyticsTimestampSchema,
|
|
269
|
+
received_at: analyticsTimestampSchema,
|
|
270
|
+
page_url: z4.string().url().max(2048).nullable().optional(),
|
|
271
|
+
page_path: z4.string().min(1).max(1024).nullable().optional(),
|
|
272
|
+
route_template: z4.string().min(1).max(1024).nullable().optional(),
|
|
273
|
+
referrer: z4.string().url().max(2048).nullable().optional(),
|
|
274
|
+
utm_source: z4.string().min(1).max(200).nullable().optional(),
|
|
275
|
+
utm_medium: z4.string().min(1).max(200).nullable().optional(),
|
|
276
|
+
utm_campaign: z4.string().min(1).max(200).nullable().optional(),
|
|
277
|
+
utm_content: z4.string().min(1).max(200).nullable().optional(),
|
|
278
|
+
utm_term: z4.string().min(1).max(200).nullable().optional(),
|
|
279
|
+
country: z4.string().min(1).max(80).nullable().optional(),
|
|
280
|
+
region: z4.string().min(1).max(120).nullable().optional(),
|
|
281
|
+
device_type: analyticsDeviceTypeSchema,
|
|
282
|
+
browser_name: z4.string().min(1).max(120).nullable().optional(),
|
|
283
|
+
os_name: z4.string().min(1).max(120).nullable().optional(),
|
|
284
|
+
provider: z4.string().min(1).max(120).nullable().optional(),
|
|
285
|
+
provider_event_id: z4.string().min(1).max(240).nullable().optional(),
|
|
286
|
+
properties: analyticsPropertiesSchema
|
|
287
|
+
});
|
|
288
|
+
var analyticsRawNormalizedEventSchema = analyticsNormalizedEventBaseSchema.extend({
|
|
289
|
+
visitor_id: analyticsVisitorIdSchema.nullable().optional(),
|
|
290
|
+
session_id: analyticsSessionIdSchema.nullable().optional()
|
|
291
|
+
});
|
|
292
|
+
var analyticsNormalizedEventSchema = analyticsNormalizedEventBaseSchema.extend({
|
|
293
|
+
visitor_id: analyticsPersistedIdentityHashSchema.nullable().optional(),
|
|
294
|
+
session_id: analyticsPersistedIdentityHashSchema.nullable().optional()
|
|
295
|
+
});
|
|
296
|
+
function isServerOnlyAnalyticsEvent(eventName) {
|
|
297
|
+
return serverOnlyAnalyticsEvents.includes(eventName);
|
|
298
|
+
}
|
|
299
|
+
function getAnalyticsKeyLookup(rawKey) {
|
|
300
|
+
const match = ANALYTICS_KEY_PATTERN.exec(rawKey);
|
|
301
|
+
return match ? `${match[1]}_siteplane_${match[2]}` : null;
|
|
302
|
+
}
|
|
303
|
+
function expandAnalyticsDataAttributeEvent(event) {
|
|
304
|
+
if (event === "form") {
|
|
305
|
+
return ["form_start", "form_submit", "form_error"];
|
|
306
|
+
}
|
|
307
|
+
return [event];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ../shared/dist/analytics-manifest.js
|
|
311
|
+
import { z as z5 } from "zod";
|
|
312
|
+
var analyticsTargetIdSchema = z5.string().min(1).max(120).regex(/^[a-zA-Z][a-zA-Z0-9]*(?:[._-][a-zA-Z0-9]+)*$/u, {
|
|
313
|
+
message: "Analytics target IDs must be stable identifiers without whitespace or visible copy."
|
|
314
|
+
});
|
|
315
|
+
var analyticsManifestRecordSchema = z5.record(z5.string(), z5.unknown());
|
|
316
|
+
var analyticsFormPurposes = [
|
|
317
|
+
"contact",
|
|
318
|
+
"lead",
|
|
319
|
+
"newsletter",
|
|
320
|
+
"search",
|
|
321
|
+
"login",
|
|
322
|
+
"other"
|
|
323
|
+
];
|
|
324
|
+
var analyticsFormPurposeSchema = z5.enum(analyticsFormPurposes);
|
|
325
|
+
var analyticsManifestRouteSchema = z5.object({
|
|
326
|
+
path: z5.string().min(1).max(1024),
|
|
327
|
+
pageType: z5.string().min(1).max(120).optional(),
|
|
328
|
+
label: z5.string().min(1).max(200).optional()
|
|
329
|
+
}).passthrough();
|
|
330
|
+
var analyticsManifestTargetSchema = z5.object({
|
|
331
|
+
id: analyticsTargetIdSchema,
|
|
332
|
+
type: analyticsTargetTypeSchema,
|
|
333
|
+
event: analyticsEventNameSchema.optional(),
|
|
334
|
+
events: z5.array(analyticsEventNameSchema).min(1).max(20).optional(),
|
|
335
|
+
route: z5.string().min(1).max(1024).optional(),
|
|
336
|
+
label: z5.string().min(1).max(200).optional(),
|
|
337
|
+
description: z5.string().min(1).max(1e3).optional(),
|
|
338
|
+
autoActive: z5.boolean().optional(),
|
|
339
|
+
sourceHints: analyticsManifestRecordSchema.optional()
|
|
340
|
+
}).passthrough().superRefine((target, context) => {
|
|
341
|
+
if (target.event === void 0 && target.events === void 0) {
|
|
342
|
+
context.addIssue({
|
|
343
|
+
code: "custom",
|
|
344
|
+
message: "Analytics targets require event or events.",
|
|
345
|
+
path: ["event"]
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (target.event !== void 0 && target.events !== void 0) {
|
|
349
|
+
context.addIssue({
|
|
350
|
+
code: "custom",
|
|
351
|
+
message: "Analytics targets must use event or events, never both.",
|
|
352
|
+
path: ["events"]
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
if (target.type === "form" && !analyticsFormPurposeSchema.safeParse(target.sourceHints?.formPurpose).success) {
|
|
356
|
+
context.addIssue({
|
|
357
|
+
code: "custom",
|
|
358
|
+
message: "Analytics form targets require a typed sourceHints.formPurpose.",
|
|
359
|
+
path: ["sourceHints", "formPurpose"]
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
var analyticsManifestSchema = z5.object({
|
|
364
|
+
schemaVersion: z5.literal(ANALYTICS_SCHEMA_VERSION),
|
|
365
|
+
siteId: z5.string().min(1),
|
|
366
|
+
siteKeyRef: z5.string().regex(/^env:[A-Z][A-Z0-9_]*$/u, {
|
|
367
|
+
message: "siteKeyRef must be an environment-variable reference."
|
|
368
|
+
}),
|
|
369
|
+
siteKeyLookup: z5.string().regex(/^pk_siteplane_[A-Za-z0-9_-]{16}$/),
|
|
370
|
+
routes: z5.array(analyticsManifestRouteSchema),
|
|
371
|
+
targets: z5.array(analyticsManifestTargetSchema),
|
|
372
|
+
metadata: analyticsManifestRecordSchema.optional(),
|
|
373
|
+
siteKey: z5.string().optional(),
|
|
374
|
+
secretKey: z5.string().optional(),
|
|
375
|
+
rawKey: z5.string().optional(),
|
|
376
|
+
token: z5.string().optional(),
|
|
377
|
+
accessToken: z5.string().optional()
|
|
378
|
+
}).passthrough().superRefine((manifest, context) => {
|
|
379
|
+
const targetIds = /* @__PURE__ */ new Set();
|
|
380
|
+
for (const [index, target] of manifest.targets.entries()) {
|
|
381
|
+
if (targetIds.has(target.id)) {
|
|
382
|
+
context.addIssue({
|
|
383
|
+
code: "custom",
|
|
384
|
+
message: `Duplicate analytics target ID: ${target.id}`,
|
|
385
|
+
path: ["targets", index, "id"]
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
targetIds.add(target.id);
|
|
389
|
+
}
|
|
390
|
+
for (const issue of findUnsafeSecretReferences(manifest)) {
|
|
391
|
+
context.addIssue({
|
|
392
|
+
code: "custom",
|
|
393
|
+
message: "Raw secret material is not allowed in analytics manifests.",
|
|
394
|
+
path: issue
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
var redactedManifestKeys = /* @__PURE__ */ new Set([
|
|
399
|
+
"siteKey",
|
|
400
|
+
"secretKey",
|
|
401
|
+
"rawKey",
|
|
402
|
+
"token",
|
|
403
|
+
"accessToken"
|
|
404
|
+
]);
|
|
405
|
+
var sensitiveManifestKeyPattern = /(?:^|_)(?:access_token|api_key|authorization|bearer|cookie|password|private_key|raw_key|secret(?:_key)?|token)(?:$|_)/iu;
|
|
406
|
+
var secretValuePattern = /(?:\bsk_(?:live|test)_|\bcpb_|\bpmt_(?:pc|setup|cli)_|\bservice_role\b|bearer\s+[a-z0-9._~-]+)/iu;
|
|
407
|
+
function redactAnalyticsManifest(manifest) {
|
|
408
|
+
const redactedKeys = [];
|
|
409
|
+
const redacted = redactManifestValue(manifest, [], redactedKeys);
|
|
410
|
+
return {
|
|
411
|
+
manifest: analyticsManifestSchema.parse(redacted),
|
|
412
|
+
redactedKeys: redactedKeys.sort()
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function redactManifestValue(value, path, redactedKeys) {
|
|
416
|
+
if (Array.isArray(value)) {
|
|
417
|
+
return value.map((item, index) => redactManifestValue(item, [...path, String(index)], redactedKeys));
|
|
418
|
+
}
|
|
419
|
+
if (!isPlainRecord(value)) {
|
|
420
|
+
return value;
|
|
421
|
+
}
|
|
422
|
+
const redacted = {};
|
|
423
|
+
for (const [key, childValue] of Object.entries(value)) {
|
|
424
|
+
const childPath = [...path, key];
|
|
425
|
+
if (redactedManifestKeys.has(key) || sensitiveManifestKeyPattern.test(normalizeManifestKey(key)) || typeof childValue === "string" && secretValuePattern.test(childValue)) {
|
|
426
|
+
redactedKeys.push(childPath.join("."));
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
const redactedChild = redactManifestValue(childValue, childPath, redactedKeys);
|
|
430
|
+
if (redactedChild !== void 0) {
|
|
431
|
+
redacted[key] = redactedChild;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (path.length > 0 && Object.keys(redacted).length === 0) {
|
|
435
|
+
return void 0;
|
|
436
|
+
}
|
|
437
|
+
return redacted;
|
|
438
|
+
}
|
|
439
|
+
function normalizeManifestKey(key) {
|
|
440
|
+
return key.replace(/([a-z0-9])([A-Z])/gu, "$1_$2").replace(/-/gu, "_").toLowerCase();
|
|
441
|
+
}
|
|
442
|
+
function findUnsafeSecretReferences(value, path = []) {
|
|
443
|
+
if (Array.isArray(value)) {
|
|
444
|
+
return value.flatMap((item, index) => findUnsafeSecretReferences(item, [...path, index]));
|
|
445
|
+
}
|
|
446
|
+
if (!isPlainRecord(value)) {
|
|
447
|
+
return [];
|
|
448
|
+
}
|
|
449
|
+
return Object.entries(value).flatMap(([key, childValue]) => {
|
|
450
|
+
const childPath = [...path, key];
|
|
451
|
+
const referenceLikeKey = /(?:ref|lookup|prefix)$/iu.test(key);
|
|
452
|
+
const unsafeHere = referenceLikeKey && typeof childValue === "string" && secretValuePattern.test(childValue);
|
|
453
|
+
return [
|
|
454
|
+
...unsafeHere ? [childPath] : [],
|
|
455
|
+
...findUnsafeSecretReferences(childValue, childPath)
|
|
456
|
+
];
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
function isPlainRecord(value) {
|
|
460
|
+
return typeof value === "object" && value !== null;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// ../shared/dist/analytics-metrics.js
|
|
464
|
+
import { z as z6 } from "zod";
|
|
465
|
+
var analyticsVisitorQualities = ["exact", "estimated"];
|
|
466
|
+
var analyticsVisitorQualitySchema = z6.enum(analyticsVisitorQualities);
|
|
467
|
+
var analyticsKpiKeys = [
|
|
468
|
+
"pageViews",
|
|
469
|
+
"sessions",
|
|
470
|
+
"visitors",
|
|
471
|
+
"estimatedVisitors",
|
|
472
|
+
"inquiries",
|
|
473
|
+
"leads",
|
|
474
|
+
"conversions",
|
|
475
|
+
"conversionRate",
|
|
476
|
+
"inquiryRate"
|
|
477
|
+
];
|
|
478
|
+
var analyticsKpiKeySchema = z6.enum(analyticsKpiKeys);
|
|
479
|
+
|
|
480
|
+
// ../shared/dist/analytics-privacy.js
|
|
481
|
+
var serverPropertySchemas = {
|
|
482
|
+
lead_created: {
|
|
483
|
+
conversionId: "identifier",
|
|
484
|
+
formId: "identifier",
|
|
485
|
+
formType: "enum",
|
|
486
|
+
customerRef: "identifier"
|
|
487
|
+
},
|
|
488
|
+
conversion: {
|
|
489
|
+
conversionId: "identifier",
|
|
490
|
+
conversionType: "enum",
|
|
491
|
+
attributionConfidence: "enum",
|
|
492
|
+
bookingSignalSource: "enum",
|
|
493
|
+
bookingProvider: "enum",
|
|
494
|
+
redirectStatus: "enum",
|
|
495
|
+
importId: "identifier"
|
|
496
|
+
},
|
|
497
|
+
checkout_completed: revenuePropertySchema(),
|
|
498
|
+
payment_succeeded: revenuePropertySchema(),
|
|
499
|
+
subscription_started: revenuePropertySchema(),
|
|
500
|
+
purchase_completed: revenuePropertySchema(),
|
|
501
|
+
trial_started: revenuePropertySchema()
|
|
502
|
+
};
|
|
503
|
+
function revenuePropertySchema() {
|
|
504
|
+
return {
|
|
505
|
+
conversionId: "identifier",
|
|
506
|
+
checkoutId: "identifier",
|
|
507
|
+
customerRef: "identifier",
|
|
508
|
+
amount: "amount",
|
|
509
|
+
currency: "enum",
|
|
510
|
+
planId: "identifier",
|
|
511
|
+
productId: "identifier"
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ../shared/dist/assets.js
|
|
516
|
+
import { z as z7 } from "zod";
|
|
517
|
+
var ASSET_STAGES = ["staging", "final", "deleted"];
|
|
518
|
+
var ASSET_VISIBILITIES = ["private", "public", "deleted"];
|
|
519
|
+
var ASSET_USAGE_ENTITY_TYPES = [
|
|
520
|
+
"branding",
|
|
521
|
+
"draft",
|
|
522
|
+
"change_set",
|
|
523
|
+
"published_value"
|
|
524
|
+
];
|
|
525
|
+
var assetStageSchema = z7.enum(ASSET_STAGES);
|
|
526
|
+
var assetVisibilitySchema = z7.enum(ASSET_VISIBILITIES);
|
|
527
|
+
var assetUsageEntityTypeSchema = z7.enum(ASSET_USAGE_ENTITY_TYPES);
|
|
528
|
+
var brandingAccentColorSchema = z7.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/);
|
|
529
|
+
|
|
530
|
+
// ../shared/dist/booking-contracts.js
|
|
531
|
+
import { z as z8 } from "zod";
|
|
532
|
+
var bookingStatuses = [
|
|
533
|
+
"pending_payment",
|
|
534
|
+
"confirmed",
|
|
535
|
+
"payment_expired",
|
|
536
|
+
"cancelled",
|
|
537
|
+
"no_show"
|
|
538
|
+
];
|
|
539
|
+
var bookingPaymentStatuses = [
|
|
540
|
+
"payment_not_required",
|
|
541
|
+
"checkout_open",
|
|
542
|
+
"processing",
|
|
543
|
+
"succeeded",
|
|
544
|
+
"failed",
|
|
545
|
+
"cancelled",
|
|
546
|
+
"expired"
|
|
547
|
+
];
|
|
548
|
+
var bookingAccountReadinessStatuses = [
|
|
549
|
+
"not_started",
|
|
550
|
+
"setup_required",
|
|
551
|
+
"under_review",
|
|
552
|
+
"payments_ready",
|
|
553
|
+
"payouts_restricted",
|
|
554
|
+
"payments_restricted",
|
|
555
|
+
"deactivated"
|
|
556
|
+
];
|
|
557
|
+
var bookingRefundStatuses = [
|
|
558
|
+
"none",
|
|
559
|
+
"pending",
|
|
560
|
+
"partially_refunded",
|
|
561
|
+
"refunded",
|
|
562
|
+
"failed"
|
|
563
|
+
];
|
|
564
|
+
var bookingDisputeStatuses = [
|
|
565
|
+
"none",
|
|
566
|
+
"needs_response",
|
|
567
|
+
"under_review",
|
|
568
|
+
"won",
|
|
569
|
+
"lost"
|
|
570
|
+
];
|
|
571
|
+
var bookingPaymentModes = [
|
|
572
|
+
"onsite_only",
|
|
573
|
+
"online_required",
|
|
574
|
+
"customer_choice"
|
|
575
|
+
];
|
|
576
|
+
var bookingPaymentCollectionTypes = [
|
|
577
|
+
"full_amount",
|
|
578
|
+
"deposit_percent"
|
|
579
|
+
];
|
|
580
|
+
var bookingResourceKinds = [
|
|
581
|
+
"person",
|
|
582
|
+
"room",
|
|
583
|
+
"equipment",
|
|
584
|
+
"other"
|
|
585
|
+
];
|
|
586
|
+
var bookingStatusSchema = z8.enum(bookingStatuses);
|
|
587
|
+
var bookingPaymentStatusSchema = z8.enum(bookingPaymentStatuses);
|
|
588
|
+
var bookingAccountReadinessStatusSchema = z8.enum(bookingAccountReadinessStatuses);
|
|
589
|
+
var bookingRefundStatusSchema = z8.enum(bookingRefundStatuses);
|
|
590
|
+
var bookingDisputeStatusSchema = z8.enum(bookingDisputeStatuses);
|
|
591
|
+
var bookingPaymentModeSchema = z8.enum(bookingPaymentModes);
|
|
592
|
+
var bookingPaymentCollectionTypeSchema = z8.enum(bookingPaymentCollectionTypes);
|
|
593
|
+
var bookingPaymentCollectionSchema = z8.discriminatedUnion("type", [
|
|
594
|
+
z8.object({ type: z8.literal("full_amount") }).strict(),
|
|
595
|
+
z8.object({
|
|
596
|
+
type: z8.literal("deposit_percent"),
|
|
597
|
+
depositPercent: z8.number().int().min(10).max(90)
|
|
598
|
+
}).strict()
|
|
599
|
+
]);
|
|
600
|
+
var bookingPaymentChoices = ["onsite", "online"];
|
|
601
|
+
var bookingPaymentChoiceSchema = z8.enum(bookingPaymentChoices);
|
|
602
|
+
var bookingPaymentSnapshotSchema = z8.object({
|
|
603
|
+
snapshotId: z8.string().uuid(),
|
|
604
|
+
snapshotVersion: z8.number().int().positive(),
|
|
605
|
+
currency: z8.literal("EUR"),
|
|
606
|
+
totalAmountCents: z8.number().int().min(0),
|
|
607
|
+
onlineAmountCents: z8.number().int().min(0),
|
|
608
|
+
onsiteRemainderCents: z8.number().int().min(0),
|
|
609
|
+
maxStripeRefundAmountCents: z8.number().int().min(0)
|
|
610
|
+
}).strict().superRefine((snapshot, context) => {
|
|
611
|
+
if (snapshot.onlineAmountCents + snapshot.onsiteRemainderCents !== snapshot.totalAmountCents) {
|
|
612
|
+
context.addIssue({
|
|
613
|
+
code: "custom",
|
|
614
|
+
message: "Payment snapshot amounts must add up to the total amount."
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
if (snapshot.maxStripeRefundAmountCents > snapshot.onlineAmountCents) {
|
|
618
|
+
context.addIssue({
|
|
619
|
+
code: "custom",
|
|
620
|
+
message: "Stripe refund basis cannot exceed the online amount."
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
var bookingServiceVariantSchema = z8.object({
|
|
625
|
+
id: z8.string().uuid(),
|
|
626
|
+
name: z8.string().trim().min(1).max(120),
|
|
627
|
+
durationMinutes: z8.number().int().min(1).max(1440),
|
|
628
|
+
priceCents: z8.number().int().min(0).nullable()
|
|
629
|
+
}).strict();
|
|
630
|
+
var bookingServiceVariantsSchema = z8.array(bookingServiceVariantSchema).min(1).max(20);
|
|
631
|
+
var isoDateSchema = z8.string().regex(/^\d{4}-\d{2}-\d{2}$/);
|
|
632
|
+
var utcDateTimeSchema = z8.string().datetime();
|
|
633
|
+
var bookingLocalTimeSchema = z8.string().regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/);
|
|
634
|
+
var bookingAvailabilityIntervalsSchema = z8.array(z8.object({
|
|
635
|
+
start: bookingLocalTimeSchema,
|
|
636
|
+
end: bookingLocalTimeSchema
|
|
637
|
+
}).strict().refine((interval) => interval.end > interval.start)).superRefine((intervals, context) => {
|
|
638
|
+
const sorted = [...intervals].sort((left, right) => left.start.localeCompare(right.start));
|
|
639
|
+
for (let index = 1; index < sorted.length; index += 1) {
|
|
640
|
+
if (sorted[index].start < sorted[index - 1].end) {
|
|
641
|
+
context.addIssue({
|
|
642
|
+
code: "custom",
|
|
643
|
+
message: "Booking availability intervals must not overlap."
|
|
644
|
+
});
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
var bookingPublicConfigSchema = z8.object({
|
|
650
|
+
site: z8.object({
|
|
651
|
+
name: z8.string().trim().min(1),
|
|
652
|
+
timezone: z8.string().trim().min(1),
|
|
653
|
+
locale: z8.string().trim().min(2),
|
|
654
|
+
bookingPageSlug: z8.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).min(2).max(64),
|
|
655
|
+
privacyUrl: z8.string().url().nullable()
|
|
656
|
+
}),
|
|
657
|
+
branding: z8.object({
|
|
658
|
+
logoUrl: z8.string().url().nullable(),
|
|
659
|
+
accentColor: z8.string().trim().min(1).nullable()
|
|
660
|
+
}),
|
|
661
|
+
settings: z8.object({
|
|
662
|
+
slotIntervalMinutes: z8.number().int().positive(),
|
|
663
|
+
minLeadMinutes: z8.number().int().min(0),
|
|
664
|
+
maxAdvanceDays: z8.number().int().min(1),
|
|
665
|
+
requirePhone: z8.boolean(),
|
|
666
|
+
collectNote: z8.boolean(),
|
|
667
|
+
confirmationMessage: z8.string().nullable()
|
|
668
|
+
}),
|
|
669
|
+
/** Freitexte des Salons, im Kassen-Schritt vor der Bestaetigung sichtbar. */
|
|
670
|
+
policies: z8.object({
|
|
671
|
+
cancellation: z8.string().nullable(),
|
|
672
|
+
lateArrival: z8.string().nullable()
|
|
673
|
+
}),
|
|
674
|
+
payment: z8.object({
|
|
675
|
+
mode: bookingPaymentModeSchema,
|
|
676
|
+
collection: bookingPaymentCollectionSchema,
|
|
677
|
+
onlinePaymentsReady: z8.boolean()
|
|
678
|
+
}).strict(),
|
|
679
|
+
serviceCategories: z8.array(z8.object({
|
|
680
|
+
id: z8.string().uuid(),
|
|
681
|
+
name: z8.string().trim().min(1),
|
|
682
|
+
sortOrder: z8.number().int()
|
|
683
|
+
})),
|
|
684
|
+
serviceSubcategories: z8.array(z8.object({
|
|
685
|
+
id: z8.string().uuid(),
|
|
686
|
+
categoryId: z8.string().uuid(),
|
|
687
|
+
name: z8.string().trim().min(1),
|
|
688
|
+
sortOrder: z8.number().int()
|
|
689
|
+
})),
|
|
690
|
+
services: z8.array(z8.object({
|
|
691
|
+
id: z8.string().trim().min(1),
|
|
692
|
+
name: z8.string().trim().min(1),
|
|
693
|
+
durationMinutes: z8.number().int().positive(),
|
|
694
|
+
priceCents: z8.number().int().min(0).nullable(),
|
|
695
|
+
currency: z8.string().regex(/^[A-Z]{3}$/),
|
|
696
|
+
description: z8.string().nullable(),
|
|
697
|
+
categoryId: z8.string().uuid(),
|
|
698
|
+
subcategoryId: z8.string().uuid().nullable(),
|
|
699
|
+
variants: bookingServiceVariantsSchema,
|
|
700
|
+
allowsAnyResource: z8.boolean(),
|
|
701
|
+
sortOrder: z8.number().int()
|
|
702
|
+
})),
|
|
703
|
+
resources: z8.array(z8.object({
|
|
704
|
+
id: z8.string().trim().min(1),
|
|
705
|
+
name: z8.string().trim().min(1),
|
|
706
|
+
kind: z8.enum(bookingResourceKinds)
|
|
707
|
+
})),
|
|
708
|
+
serviceResources: z8.array(z8.object({
|
|
709
|
+
serviceId: z8.string().trim().min(1),
|
|
710
|
+
resourceId: z8.string().trim().min(1)
|
|
711
|
+
}))
|
|
712
|
+
});
|
|
713
|
+
var bookingItemSchema = z8.object({
|
|
714
|
+
serviceId: z8.string().trim().min(1),
|
|
715
|
+
variantId: z8.string().trim().min(1)
|
|
716
|
+
}).strict();
|
|
717
|
+
var bookingItemsSchema = z8.array(bookingItemSchema).min(1).max(10);
|
|
718
|
+
function parseBookingItemsParam(value) {
|
|
719
|
+
const items = value.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "").map((entry) => {
|
|
720
|
+
const [serviceId, variantId, ...rest] = entry.split(":");
|
|
721
|
+
return serviceId && variantId && rest.length === 0 ? { serviceId, variantId } : null;
|
|
722
|
+
});
|
|
723
|
+
return items.every((item) => item !== null) ? items : null;
|
|
724
|
+
}
|
|
725
|
+
var bookingSlotsQuerySchema = z8.object({
|
|
726
|
+
items: z8.preprocess((value) => typeof value === "string" ? parseBookingItemsParam(value) : value, bookingItemsSchema),
|
|
727
|
+
resourceId: z8.string().trim().min(1).optional(),
|
|
728
|
+
fromDate: isoDateSchema,
|
|
729
|
+
toDate: isoDateSchema
|
|
730
|
+
});
|
|
731
|
+
var bookingSlotsResponseSchema = z8.object({
|
|
732
|
+
timezone: z8.string().trim().min(1),
|
|
733
|
+
items: bookingItemsSchema,
|
|
734
|
+
// Reine Behandlungsdauer ohne Puffer: das UI zeigt damit die Terminspanne,
|
|
735
|
+
// ohne selbst rechnen zu muessen.
|
|
736
|
+
totalDurationMinutes: z8.number().int().min(1),
|
|
737
|
+
mode: z8.enum(["any", "resource"]),
|
|
738
|
+
slots: z8.array(z8.object({
|
|
739
|
+
startUtc: utcDateTimeSchema,
|
|
740
|
+
resourceId: z8.string().trim().min(1).optional()
|
|
741
|
+
}))
|
|
742
|
+
});
|
|
743
|
+
var bookingAvailabilityQuerySchema = z8.object({
|
|
744
|
+
items: z8.preprocess((value) => typeof value === "string" ? parseBookingItemsParam(value) : value, bookingItemsSchema),
|
|
745
|
+
resourceId: z8.string().trim().min(1).optional(),
|
|
746
|
+
fromDate: isoDateSchema,
|
|
747
|
+
toDate: isoDateSchema
|
|
748
|
+
});
|
|
749
|
+
var bookingAvailabilityResponseSchema = z8.object({
|
|
750
|
+
timezone: z8.string().trim().min(1),
|
|
751
|
+
totalDurationMinutes: z8.number().int().min(1),
|
|
752
|
+
days: z8.array(z8.object({
|
|
753
|
+
date: isoDateSchema,
|
|
754
|
+
hasSlots: z8.boolean()
|
|
755
|
+
})),
|
|
756
|
+
/** Erster Tag mit Slot ab `fromDate`, auch ausserhalb des Fensters. */
|
|
757
|
+
nextAvailableDate: isoDateSchema.nullable()
|
|
758
|
+
});
|
|
759
|
+
var bookingPublicErrorCodes = [
|
|
760
|
+
"slot_unavailable",
|
|
761
|
+
"validation_error",
|
|
762
|
+
"origin_not_allowed",
|
|
763
|
+
"invalid_key",
|
|
764
|
+
"rate_limited",
|
|
765
|
+
"payment_required",
|
|
766
|
+
"payment_unavailable",
|
|
767
|
+
"payment_amount_too_small",
|
|
768
|
+
"already_cancelled",
|
|
769
|
+
"too_late",
|
|
770
|
+
"not_found"
|
|
771
|
+
];
|
|
772
|
+
var bookingPublicErrorSchema = z8.object({
|
|
773
|
+
error: z8.enum(bookingPublicErrorCodes),
|
|
774
|
+
message: z8.string().optional(),
|
|
775
|
+
requestId: z8.string().optional()
|
|
776
|
+
}).strict();
|
|
777
|
+
var bookingCreateRequestSchema = z8.object({
|
|
778
|
+
items: bookingItemsSchema,
|
|
779
|
+
resourceId: z8.string().min(1).nullable(),
|
|
780
|
+
startUtc: utcDateTimeSchema,
|
|
781
|
+
customer: z8.object({
|
|
782
|
+
name: z8.string().min(1).max(120),
|
|
783
|
+
email: z8.string().email().max(254),
|
|
784
|
+
phone: z8.string().max(32).optional(),
|
|
785
|
+
note: z8.string().max(1e3).optional()
|
|
786
|
+
}).strict(),
|
|
787
|
+
hp: z8.string().optional(),
|
|
788
|
+
clientToken: z8.string().uuid(),
|
|
789
|
+
paymentChoice: bookingPaymentChoiceSchema,
|
|
790
|
+
// Werbe-Einwilligung. Bestaetigung, Erinnerung und Storno-Link sind davon
|
|
791
|
+
// unabhaengig und werden immer versendet.
|
|
792
|
+
consent: z8.object({
|
|
793
|
+
marketingEmail: z8.boolean(),
|
|
794
|
+
marketingSms: z8.boolean()
|
|
795
|
+
}).strict().optional()
|
|
796
|
+
}).strict();
|
|
797
|
+
var bookingPublicPaymentResponseSchema = z8.object({
|
|
798
|
+
clientSecret: z8.string().min(1),
|
|
799
|
+
stripeAccountId: z8.string().startsWith("acct_")
|
|
800
|
+
}).strict();
|
|
801
|
+
var bookingCreateResponseSchema = z8.union([
|
|
802
|
+
z8.object({
|
|
803
|
+
status: z8.literal("confirmed"),
|
|
804
|
+
booking: z8.object({
|
|
805
|
+
id: z8.string().min(1),
|
|
806
|
+
startUtc: utcDateTimeSchema,
|
|
807
|
+
endUtc: utcDateTimeSchema,
|
|
808
|
+
timezone: z8.string().trim().min(1),
|
|
809
|
+
serviceName: z8.string().trim().min(1),
|
|
810
|
+
resourceName: z8.string().trim().min(1)
|
|
811
|
+
}).strict(),
|
|
812
|
+
manageUrl: z8.string().url(),
|
|
813
|
+
confirmationMessage: z8.string().nullable()
|
|
814
|
+
}).strict(),
|
|
815
|
+
z8.object({
|
|
816
|
+
status: z8.literal("pending_payment"),
|
|
817
|
+
bookingId: z8.string().min(1),
|
|
818
|
+
holdExpiresAt: utcDateTimeSchema,
|
|
819
|
+
payment: bookingPublicPaymentResponseSchema,
|
|
820
|
+
statusToken: z8.string().min(20),
|
|
821
|
+
manageUrl: z8.string().url().optional()
|
|
822
|
+
}).strict(),
|
|
823
|
+
bookingPublicErrorSchema
|
|
824
|
+
]);
|
|
825
|
+
var bookingStatusResponseSchema = z8.object({
|
|
826
|
+
bookingStatus: bookingStatusSchema,
|
|
827
|
+
paymentStatus: bookingPaymentStatusSchema,
|
|
828
|
+
refundStatus: bookingRefundStatusSchema,
|
|
829
|
+
disputeStatus: bookingDisputeStatusSchema,
|
|
830
|
+
holdExpiresAt: utcDateTimeSchema.nullable()
|
|
831
|
+
}).strict();
|
|
832
|
+
var bookingCancelRequestSchema = z8.object({
|
|
833
|
+
token: z8.string().min(20)
|
|
834
|
+
});
|
|
835
|
+
var bookingCancelResponseSchema = z8.union([
|
|
836
|
+
z8.object({
|
|
837
|
+
status: z8.enum(["cancelled", "already_cancelled", "too_late", "not_found"])
|
|
838
|
+
}),
|
|
839
|
+
bookingPublicErrorSchema
|
|
840
|
+
]);
|
|
841
|
+
var bookingMoveErrorCodes = [
|
|
842
|
+
"slot_unavailable",
|
|
843
|
+
"booking_not_movable",
|
|
844
|
+
"not_found"
|
|
845
|
+
];
|
|
846
|
+
var bookingMoveRequestSchema = z8.object({
|
|
847
|
+
bookingId: z8.string().uuid(),
|
|
848
|
+
newStartUtc: utcDateTimeSchema,
|
|
849
|
+
newResourceId: z8.string().uuid().optional(),
|
|
850
|
+
notifyCustomer: z8.boolean().default(false),
|
|
851
|
+
reason: z8.string().max(500).optional()
|
|
852
|
+
});
|
|
853
|
+
var bookingMoveResponseSchema = z8.union([
|
|
854
|
+
z8.object({
|
|
855
|
+
status: z8.literal("moved"),
|
|
856
|
+
bookingId: z8.string().uuid(),
|
|
857
|
+
startUtc: utcDateTimeSchema,
|
|
858
|
+
endUtc: utcDateTimeSchema,
|
|
859
|
+
resourceId: z8.string().uuid()
|
|
860
|
+
}),
|
|
861
|
+
z8.object({
|
|
862
|
+
error: z8.enum(bookingMoveErrorCodes),
|
|
863
|
+
message: z8.string().optional(),
|
|
864
|
+
requestId: z8.string().optional()
|
|
865
|
+
})
|
|
866
|
+
]);
|
|
867
|
+
var bookingManifestSchema = z8.object({
|
|
868
|
+
schemaVersion: z8.number().int().positive(),
|
|
869
|
+
siteId: z8.string().trim().min(1),
|
|
870
|
+
siteKeyPrefix: z8.string().startsWith("bk_pub_"),
|
|
871
|
+
bookingPageSlug: z8.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/).min(2).max(64),
|
|
872
|
+
embedMode: z8.enum(["script", "web_component", "iframe"]).optional(),
|
|
873
|
+
productionOrigin: z8.string().url().optional()
|
|
874
|
+
}).passthrough();
|
|
875
|
+
var secretManifestKeys = /* @__PURE__ */ new Set([
|
|
876
|
+
"siteKey",
|
|
877
|
+
"rawKey",
|
|
878
|
+
"token",
|
|
879
|
+
"accessToken"
|
|
880
|
+
]);
|
|
881
|
+
function redactBookingManifest(value) {
|
|
882
|
+
if (Array.isArray(value)) {
|
|
883
|
+
return value.map((item) => redactBookingManifest(item));
|
|
884
|
+
}
|
|
885
|
+
if (value && typeof value === "object") {
|
|
886
|
+
return Object.fromEntries(Object.entries(value).filter(([key]) => !secretManifestKeys.has(key)).map(([key, item]) => [key, redactBookingManifest(item)]));
|
|
887
|
+
}
|
|
888
|
+
return value;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// ../shared/dist/booking-defaults.js
|
|
892
|
+
var BOOKING_CREATE_PAYLOAD_LIMIT_BYTES = 16 * 1024;
|
|
893
|
+
|
|
894
|
+
// ../shared/dist/bridge-protocol.js
|
|
895
|
+
import { z as z15 } from "zod";
|
|
896
|
+
|
|
897
|
+
// ../shared/dist/field-id.js
|
|
898
|
+
import { z as z9 } from "zod";
|
|
899
|
+
var FIELD_ID_PATTERN = /^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)+$/;
|
|
900
|
+
var fieldIdSchema = z9.string().regex(FIELD_ID_PATTERN, "Invalid field id");
|
|
901
|
+
function isFieldId(value) {
|
|
902
|
+
return typeof value === "string" && fieldIdSchema.safeParse(value).success;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// ../shared/dist/field-types.js
|
|
906
|
+
import { z as z10 } from "zod";
|
|
907
|
+
var FIELD_TYPES = ["text", "longText", "image", "link"];
|
|
908
|
+
var fieldTypeSchema = z10.enum(FIELD_TYPES);
|
|
909
|
+
|
|
910
|
+
// ../shared/dist/image-value.js
|
|
911
|
+
import { z as z11 } from "zod";
|
|
912
|
+
var SUPPORTED_IMAGE_CONTENT_TYPES = [
|
|
913
|
+
"image/jpeg",
|
|
914
|
+
"image/png",
|
|
915
|
+
"image/webp",
|
|
916
|
+
"image/avif"
|
|
917
|
+
];
|
|
918
|
+
var maxImageUploadBytes = 10 * 1024 * 1024;
|
|
919
|
+
var largeImageWarningBytes = 2 * 1024 * 1024;
|
|
920
|
+
var imageContentTypeSchema = z11.enum(SUPPORTED_IMAGE_CONTENT_TYPES);
|
|
921
|
+
var imageCropSchema = z11.object({
|
|
922
|
+
x: z11.number().min(0).max(100),
|
|
923
|
+
y: z11.number().min(0).max(100),
|
|
924
|
+
zoom: z11.number().min(1).max(4),
|
|
925
|
+
aspectRatio: z11.number().positive().max(10)
|
|
926
|
+
});
|
|
927
|
+
var imageValueSchema = z11.object({
|
|
928
|
+
src: z11.string().trim().min(1),
|
|
929
|
+
alt: z11.string().trim().min(1).max(300),
|
|
930
|
+
width: z11.number().int().positive().optional(),
|
|
931
|
+
height: z11.number().int().positive().optional(),
|
|
932
|
+
crop: imageCropSchema.optional(),
|
|
933
|
+
assetId: z11.string().uuid().optional(),
|
|
934
|
+
finalStorageProvider: z11.enum([
|
|
935
|
+
"repo_static_assets",
|
|
936
|
+
"repo_handoff",
|
|
937
|
+
"site_owned_storage",
|
|
938
|
+
"siteplane_managed_fallback"
|
|
939
|
+
]).optional()
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// ../shared/dist/site-setup.js
|
|
943
|
+
import { z as z14 } from "zod";
|
|
944
|
+
|
|
945
|
+
// ../shared/dist/site-field-contract.js
|
|
946
|
+
import { z as z13 } from "zod";
|
|
947
|
+
|
|
948
|
+
// ../shared/dist/fallback-hash.js
|
|
949
|
+
import { z as z12 } from "zod";
|
|
950
|
+
var SAFE_REPRESENTATION_REASONS = [
|
|
951
|
+
"asset_import_rewritten",
|
|
952
|
+
"relative_url_normalized",
|
|
953
|
+
"line_endings_normalized",
|
|
954
|
+
"image_value_serialized"
|
|
955
|
+
];
|
|
956
|
+
var safeRepresentationReasonSchema = z12.enum(SAFE_REPRESENTATION_REASONS);
|
|
957
|
+
function canonicalizeFallbackValue(input) {
|
|
958
|
+
const fieldType = fieldTypeSchema.parse(input.fieldType);
|
|
959
|
+
if (input.safeRepresentationReason !== void 0) {
|
|
960
|
+
safeRepresentationReasonSchema.parse(input.safeRepresentationReason);
|
|
961
|
+
}
|
|
962
|
+
if (fieldType === "text" || fieldType === "longText") {
|
|
963
|
+
if (typeof input.fallbackValue !== "string") {
|
|
964
|
+
throw new Error("Text fallback values must be strings.");
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
fieldType,
|
|
968
|
+
fallbackValue: normalizeLineEndings(input.fallbackValue)
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
if (fieldType === "link") {
|
|
972
|
+
const link = linkFallbackSchema.parse(input.fallbackValue);
|
|
973
|
+
return {
|
|
974
|
+
fieldType,
|
|
975
|
+
fallbackValue: sortObjectKeys({
|
|
976
|
+
...link,
|
|
977
|
+
href: normalizeExplicitHref(link.href)
|
|
978
|
+
})
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
const image = imageValueSchema.parse(input.fallbackValue);
|
|
982
|
+
return {
|
|
983
|
+
fieldType,
|
|
984
|
+
fallbackValue: sortObjectKeys(image)
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function createCanonicalFallbackHash(input) {
|
|
988
|
+
const canonical = canonicalizeFallbackValue(input);
|
|
989
|
+
const payload = stableJsonStringify(canonical);
|
|
990
|
+
return `sha256:${sha256Hex(payload)}`;
|
|
991
|
+
}
|
|
992
|
+
var linkFallbackSchema = z12.object({
|
|
993
|
+
href: z12.string().trim().min(1),
|
|
994
|
+
label: z12.string().trim().min(1).optional(),
|
|
995
|
+
target: z12.enum(["_self", "_blank"]).optional()
|
|
996
|
+
}).passthrough();
|
|
997
|
+
function normalizeLineEndings(value) {
|
|
998
|
+
return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
999
|
+
}
|
|
1000
|
+
function normalizeExplicitHref(value) {
|
|
1001
|
+
const href = value.trim();
|
|
1002
|
+
try {
|
|
1003
|
+
const parsed = new URL(href);
|
|
1004
|
+
const hasRootSlashOnly = parsed.pathname === "/" && parsed.search === "" && parsed.hash === "";
|
|
1005
|
+
if (hasRootSlashOnly) {
|
|
1006
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
1007
|
+
}
|
|
1008
|
+
} catch {
|
|
1009
|
+
return href;
|
|
1010
|
+
}
|
|
1011
|
+
return href;
|
|
1012
|
+
}
|
|
1013
|
+
function stableJsonStringify(value) {
|
|
1014
|
+
return JSON.stringify(sortObjectKeys(value));
|
|
1015
|
+
}
|
|
1016
|
+
function sortObjectKeys(value) {
|
|
1017
|
+
if (Array.isArray(value)) {
|
|
1018
|
+
return value.map(sortObjectKeys);
|
|
1019
|
+
}
|
|
1020
|
+
if (!value || typeof value !== "object") {
|
|
1021
|
+
return value;
|
|
1022
|
+
}
|
|
1023
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => [key, sortObjectKeys(entryValue)]));
|
|
1024
|
+
}
|
|
1025
|
+
function sha256Hex(input) {
|
|
1026
|
+
const bytes = new TextEncoder().encode(input);
|
|
1027
|
+
const paddedLength = Math.ceil((bytes.length + 1 + 8) / 64) * 64;
|
|
1028
|
+
const padded = new Uint8Array(paddedLength);
|
|
1029
|
+
padded.set(bytes);
|
|
1030
|
+
padded[bytes.length] = 128;
|
|
1031
|
+
const bitLength = bytes.length * 8;
|
|
1032
|
+
const view = new DataView(padded.buffer);
|
|
1033
|
+
view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296));
|
|
1034
|
+
view.setUint32(paddedLength - 4, bitLength >>> 0);
|
|
1035
|
+
let h0 = 1779033703;
|
|
1036
|
+
let h1 = 3144134277;
|
|
1037
|
+
let h2 = 1013904242;
|
|
1038
|
+
let h3 = 2773480762;
|
|
1039
|
+
let h4 = 1359893119;
|
|
1040
|
+
let h5 = 2600822924;
|
|
1041
|
+
let h6 = 528734635;
|
|
1042
|
+
let h7 = 1541459225;
|
|
1043
|
+
const words = new Array(64).fill(0);
|
|
1044
|
+
for (let chunk = 0; chunk < paddedLength; chunk += 64) {
|
|
1045
|
+
for (let index = 0; index < 16; index += 1) {
|
|
1046
|
+
words[index] = view.getUint32(chunk + index * 4);
|
|
1047
|
+
}
|
|
1048
|
+
for (let index = 16; index < 64; index += 1) {
|
|
1049
|
+
words[index] = smallSigma1(words[index - 2] ?? 0) + (words[index - 7] ?? 0) + smallSigma0(words[index - 15] ?? 0) + (words[index - 16] ?? 0) >>> 0;
|
|
1050
|
+
}
|
|
1051
|
+
let a = h0;
|
|
1052
|
+
let b = h1;
|
|
1053
|
+
let c = h2;
|
|
1054
|
+
let d = h3;
|
|
1055
|
+
let e = h4;
|
|
1056
|
+
let f = h5;
|
|
1057
|
+
let g = h6;
|
|
1058
|
+
let h = h7;
|
|
1059
|
+
for (let index = 0; index < 64; index += 1) {
|
|
1060
|
+
const temp1 = h + bigSigma1(e) + choose(e, f, g) + SHA256_CONSTANTS[index] + (words[index] ?? 0) >>> 0;
|
|
1061
|
+
const temp2 = bigSigma0(a) + majority(a, b, c) >>> 0;
|
|
1062
|
+
h = g;
|
|
1063
|
+
g = f;
|
|
1064
|
+
f = e;
|
|
1065
|
+
e = d + temp1 >>> 0;
|
|
1066
|
+
d = c;
|
|
1067
|
+
c = b;
|
|
1068
|
+
b = a;
|
|
1069
|
+
a = temp1 + temp2 >>> 0;
|
|
1070
|
+
}
|
|
1071
|
+
h0 = h0 + a >>> 0;
|
|
1072
|
+
h1 = h1 + b >>> 0;
|
|
1073
|
+
h2 = h2 + c >>> 0;
|
|
1074
|
+
h3 = h3 + d >>> 0;
|
|
1075
|
+
h4 = h4 + e >>> 0;
|
|
1076
|
+
h5 = h5 + f >>> 0;
|
|
1077
|
+
h6 = h6 + g >>> 0;
|
|
1078
|
+
h7 = h7 + h >>> 0;
|
|
1079
|
+
}
|
|
1080
|
+
return [h0, h1, h2, h3, h4, h5, h6, h7].map((value) => value.toString(16).padStart(8, "0")).join("");
|
|
1081
|
+
}
|
|
1082
|
+
function rightRotate(value, bits) {
|
|
1083
|
+
return value >>> bits | value << 32 - bits;
|
|
1084
|
+
}
|
|
1085
|
+
function choose(x, y, zValue) {
|
|
1086
|
+
return x & y ^ ~x & zValue;
|
|
1087
|
+
}
|
|
1088
|
+
function majority(x, y, zValue) {
|
|
1089
|
+
return x & y ^ x & zValue ^ y & zValue;
|
|
1090
|
+
}
|
|
1091
|
+
function bigSigma0(value) {
|
|
1092
|
+
return rightRotate(value, 2) ^ rightRotate(value, 13) ^ rightRotate(value, 22);
|
|
1093
|
+
}
|
|
1094
|
+
function bigSigma1(value) {
|
|
1095
|
+
return rightRotate(value, 6) ^ rightRotate(value, 11) ^ rightRotate(value, 25);
|
|
1096
|
+
}
|
|
1097
|
+
function smallSigma0(value) {
|
|
1098
|
+
return rightRotate(value, 7) ^ rightRotate(value, 18) ^ value >>> 3;
|
|
1099
|
+
}
|
|
1100
|
+
function smallSigma1(value) {
|
|
1101
|
+
return rightRotate(value, 17) ^ rightRotate(value, 19) ^ value >>> 10;
|
|
1102
|
+
}
|
|
1103
|
+
var SHA256_CONSTANTS = [
|
|
1104
|
+
1116352408,
|
|
1105
|
+
1899447441,
|
|
1106
|
+
3049323471,
|
|
1107
|
+
3921009573,
|
|
1108
|
+
961987163,
|
|
1109
|
+
1508970993,
|
|
1110
|
+
2453635748,
|
|
1111
|
+
2870763221,
|
|
1112
|
+
3624381080,
|
|
1113
|
+
310598401,
|
|
1114
|
+
607225278,
|
|
1115
|
+
1426881987,
|
|
1116
|
+
1925078388,
|
|
1117
|
+
2162078206,
|
|
1118
|
+
2614888103,
|
|
1119
|
+
3248222580,
|
|
1120
|
+
3835390401,
|
|
1121
|
+
4022224774,
|
|
1122
|
+
264347078,
|
|
1123
|
+
604807628,
|
|
1124
|
+
770255983,
|
|
1125
|
+
1249150122,
|
|
1126
|
+
1555081692,
|
|
1127
|
+
1996064986,
|
|
1128
|
+
2554220882,
|
|
1129
|
+
2821834349,
|
|
1130
|
+
2952996808,
|
|
1131
|
+
3210313671,
|
|
1132
|
+
3336571891,
|
|
1133
|
+
3584528711,
|
|
1134
|
+
113926993,
|
|
1135
|
+
338241895,
|
|
1136
|
+
666307205,
|
|
1137
|
+
773529912,
|
|
1138
|
+
1294757372,
|
|
1139
|
+
1396182291,
|
|
1140
|
+
1695183700,
|
|
1141
|
+
1986661051,
|
|
1142
|
+
2177026350,
|
|
1143
|
+
2456956037,
|
|
1144
|
+
2730485921,
|
|
1145
|
+
2820302411,
|
|
1146
|
+
3259730800,
|
|
1147
|
+
3345764771,
|
|
1148
|
+
3516065817,
|
|
1149
|
+
3600352804,
|
|
1150
|
+
4094571909,
|
|
1151
|
+
275423344,
|
|
1152
|
+
430227734,
|
|
1153
|
+
506948616,
|
|
1154
|
+
659060556,
|
|
1155
|
+
883997877,
|
|
1156
|
+
958139571,
|
|
1157
|
+
1322822218,
|
|
1158
|
+
1537002063,
|
|
1159
|
+
1747873779,
|
|
1160
|
+
1955562222,
|
|
1161
|
+
2024104815,
|
|
1162
|
+
2227730452,
|
|
1163
|
+
2361852424,
|
|
1164
|
+
2428436474,
|
|
1165
|
+
2756734187,
|
|
1166
|
+
3204031479,
|
|
1167
|
+
3329325298
|
|
1168
|
+
];
|
|
1169
|
+
|
|
1170
|
+
// ../shared/dist/site-field-contract.js
|
|
1171
|
+
var fieldContractVersion = "1.0.0";
|
|
1172
|
+
var scannerContractVersion = "1.0.0";
|
|
1173
|
+
var siteFieldStatusSchema = z13.enum(["hidden", "active"]);
|
|
1174
|
+
var semanticVersionSchema = z13.string().regex(/^\d+\.\d+\.\d+$/u);
|
|
1175
|
+
var siteFieldRouteKeySchema = z13.string().trim().refine((value) => value === "$global" || value.startsWith("/") && !value.includes("?") && !value.includes("#") && (value === "/" || !value.endsWith("/")), "Route keys must be canonical paths or $global.");
|
|
1176
|
+
var repositoryRelativePosixPathSchema = z13.string().trim().min(1).refine((value) => !value.startsWith("/") && !value.includes("\\") && !/^[a-z]:/iu.test(value) && !value.split("/").includes(".."), "Source paths must be repository-relative POSIX paths.");
|
|
1177
|
+
var sourceMappingSchema = z13.object({
|
|
1178
|
+
kind: z13.literal("function"),
|
|
1179
|
+
filePath: repositoryRelativePosixPathSchema,
|
|
1180
|
+
exportName: z13.string().trim().min(1).optional(),
|
|
1181
|
+
sourcePath: z13.string().trim().min(1).optional(),
|
|
1182
|
+
editTarget: fieldIdSchema.optional()
|
|
1183
|
+
}).strict();
|
|
1184
|
+
var domMappingSchema = z13.object({
|
|
1185
|
+
attribute: z13.literal("data-siteplane-field-id"),
|
|
1186
|
+
previewStrategy: z13.enum(["dom", "event"])
|
|
1187
|
+
}).strict();
|
|
1188
|
+
var fieldBaseSchema = z13.object({
|
|
1189
|
+
fieldId: fieldIdSchema,
|
|
1190
|
+
routeKey: siteFieldRouteKeySchema,
|
|
1191
|
+
source: sourceMappingSchema,
|
|
1192
|
+
dom: domMappingSchema.optional()
|
|
1193
|
+
}).strict();
|
|
1194
|
+
var linkFallbackSchema2 = z13.object({
|
|
1195
|
+
href: z13.string().trim().min(1),
|
|
1196
|
+
label: z13.string().trim().min(1).optional(),
|
|
1197
|
+
target: z13.enum(["_self", "_blank"]).optional()
|
|
1198
|
+
}).strict();
|
|
1199
|
+
var imageFallbackSchema = z13.object({
|
|
1200
|
+
src: z13.string().trim().min(1),
|
|
1201
|
+
alt: z13.string().trim().max(300),
|
|
1202
|
+
width: z13.number().int().positive().optional(),
|
|
1203
|
+
height: z13.number().int().positive().optional(),
|
|
1204
|
+
crop: z13.object({
|
|
1205
|
+
x: z13.number().min(0).max(100),
|
|
1206
|
+
y: z13.number().min(0).max(100),
|
|
1207
|
+
zoom: z13.number().min(1).max(4),
|
|
1208
|
+
aspectRatio: z13.number().positive().max(10)
|
|
1209
|
+
}).strict().optional()
|
|
1210
|
+
}).strict();
|
|
1211
|
+
var siteFieldContractFieldSchema = z13.discriminatedUnion("fieldType", [
|
|
1212
|
+
fieldBaseSchema.extend({
|
|
1213
|
+
fieldType: z13.literal("text"),
|
|
1214
|
+
fallback: z13.string()
|
|
1215
|
+
}),
|
|
1216
|
+
fieldBaseSchema.extend({
|
|
1217
|
+
fieldType: z13.literal("longText"),
|
|
1218
|
+
fallback: z13.string()
|
|
1219
|
+
}),
|
|
1220
|
+
fieldBaseSchema.extend({
|
|
1221
|
+
fieldType: z13.literal("link"),
|
|
1222
|
+
fallback: linkFallbackSchema2
|
|
1223
|
+
}),
|
|
1224
|
+
fieldBaseSchema.extend({
|
|
1225
|
+
fieldType: z13.literal("image"),
|
|
1226
|
+
fallback: imageFallbackSchema
|
|
1227
|
+
})
|
|
1228
|
+
]);
|
|
1229
|
+
var siteFieldContractV1Schema = z13.object({
|
|
1230
|
+
version: z13.literal(1),
|
|
1231
|
+
fieldContractVersion: semanticVersionSchema,
|
|
1232
|
+
scannerContractVersion: semanticVersionSchema,
|
|
1233
|
+
fields: z13.array(siteFieldContractFieldSchema)
|
|
1234
|
+
}).strict().superRefine((contract, context) => {
|
|
1235
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1236
|
+
for (const [index, field] of contract.fields.entries()) {
|
|
1237
|
+
if (seen.has(field.fieldId)) {
|
|
1238
|
+
context.addIssue({
|
|
1239
|
+
code: "custom",
|
|
1240
|
+
message: `Duplicate field id: ${field.fieldId}`,
|
|
1241
|
+
path: ["fields", index, "fieldId"]
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
seen.add(field.fieldId);
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
function normalizeSiteFieldContract(input) {
|
|
1248
|
+
const contract = siteFieldContractV1Schema.parse(input);
|
|
1249
|
+
return {
|
|
1250
|
+
...contract,
|
|
1251
|
+
fields: [...contract.fields].sort((left, right) => left.fieldId.localeCompare(right.fieldId))
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function createSiteFieldContractHash(input) {
|
|
1255
|
+
const payload = stableJsonStringify(normalizeSiteFieldContract(input));
|
|
1256
|
+
return `sha256:${sha256Hex(payload)}`;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// ../shared/dist/site-setup.js
|
|
1260
|
+
var SITE_SETUP_RUN_STATUSES = [
|
|
1261
|
+
"waiting_for_agent",
|
|
1262
|
+
"working",
|
|
1263
|
+
"action_required",
|
|
1264
|
+
"ready_for_review",
|
|
1265
|
+
"completed"
|
|
1266
|
+
];
|
|
1267
|
+
var SITE_SETUP_RUN_MODES = ["initial", "update"];
|
|
1268
|
+
var SITE_SETUP_ERROR_CODES = [
|
|
1269
|
+
"action_required",
|
|
1270
|
+
"deployment_evidence_required",
|
|
1271
|
+
"field_contract_scan_failed",
|
|
1272
|
+
"invalid_editor_definition",
|
|
1273
|
+
"invalid_field_contract",
|
|
1274
|
+
"missing_build_revision",
|
|
1275
|
+
"missing_field_contract",
|
|
1276
|
+
"review_locked",
|
|
1277
|
+
"stale_deployment_contract",
|
|
1278
|
+
"stale_field_contract",
|
|
1279
|
+
"stale_setup_review",
|
|
1280
|
+
"update_required"
|
|
1281
|
+
];
|
|
1282
|
+
var CONTINUE_SITE_SETUP_MAX_ITEMS = 50;
|
|
1283
|
+
var CONTINUE_SITE_SETUP_RENDERED_PATH_MAX_BYTES = 512;
|
|
1284
|
+
var CONTINUE_SITE_SETUP_VISIBLE_VALUE_MAX_BYTES = 256;
|
|
1285
|
+
var CONTINUE_SITE_SETUP_LOCATOR_HINT_MAX_BYTES = 512;
|
|
1286
|
+
var CONTINUE_SITE_SETUP_ITEMS_MAX_BYTES = 32 * 1024;
|
|
1287
|
+
var CONTINUE_SITE_SETUP_HTTP_BODY_MAX_BYTES = 64 * 1024;
|
|
1288
|
+
var siteSetupRunStatusSchema = z14.enum(SITE_SETUP_RUN_STATUSES);
|
|
1289
|
+
var siteSetupRunModeSchema = z14.enum(SITE_SETUP_RUN_MODES);
|
|
1290
|
+
var siteSetupErrorCodeSchema = z14.enum(SITE_SETUP_ERROR_CODES);
|
|
1291
|
+
var editorFieldSchema = z14.object({
|
|
1292
|
+
fieldId: fieldIdSchema,
|
|
1293
|
+
label: z14.string().trim().min(1),
|
|
1294
|
+
description: z14.string().trim().min(1).optional(),
|
|
1295
|
+
required: z14.boolean().optional(),
|
|
1296
|
+
maxLength: z14.number().int().positive().optional()
|
|
1297
|
+
}).strict();
|
|
1298
|
+
var editorSectionSchema = z14.object({
|
|
1299
|
+
label: z14.string().trim().min(1),
|
|
1300
|
+
fields: z14.array(editorFieldSchema).min(1)
|
|
1301
|
+
}).strict();
|
|
1302
|
+
var editorRouteSchema = z14.object({
|
|
1303
|
+
routeKey: siteFieldRouteKeySchema,
|
|
1304
|
+
label: z14.string().trim().min(1),
|
|
1305
|
+
sections: z14.array(editorSectionSchema).min(1)
|
|
1306
|
+
}).strict().superRefine((route, context) => {
|
|
1307
|
+
const sections = /* @__PURE__ */ new Set();
|
|
1308
|
+
for (const [index, section] of route.sections.entries()) {
|
|
1309
|
+
if (sections.has(section.label)) {
|
|
1310
|
+
context.addIssue({
|
|
1311
|
+
code: "custom",
|
|
1312
|
+
message: `Duplicate section label: ${section.label}`,
|
|
1313
|
+
path: ["sections", index, "label"]
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
sections.add(section.label);
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
var editorDefinitionV1Schema = z14.object({
|
|
1320
|
+
version: z14.literal(1),
|
|
1321
|
+
routes: z14.array(editorRouteSchema).min(1)
|
|
1322
|
+
}).strict().superRefine((definition, context) => {
|
|
1323
|
+
const routes = /* @__PURE__ */ new Set();
|
|
1324
|
+
const fields = /* @__PURE__ */ new Set();
|
|
1325
|
+
for (const [routeIndex, route] of definition.routes.entries()) {
|
|
1326
|
+
if (routes.has(route.routeKey)) {
|
|
1327
|
+
context.addIssue({
|
|
1328
|
+
code: "custom",
|
|
1329
|
+
message: `Duplicate route key: ${route.routeKey}`,
|
|
1330
|
+
path: ["routes", routeIndex, "routeKey"]
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
routes.add(route.routeKey);
|
|
1334
|
+
for (const [sectionIndex, section] of route.sections.entries()) {
|
|
1335
|
+
for (const [fieldIndex, field] of section.fields.entries()) {
|
|
1336
|
+
if (fields.has(field.fieldId)) {
|
|
1337
|
+
context.addIssue({
|
|
1338
|
+
code: "custom",
|
|
1339
|
+
message: `Duplicate editor field id: ${field.fieldId}`,
|
|
1340
|
+
path: [
|
|
1341
|
+
"routes",
|
|
1342
|
+
routeIndex,
|
|
1343
|
+
"sections",
|
|
1344
|
+
sectionIndex,
|
|
1345
|
+
"fields",
|
|
1346
|
+
fieldIndex,
|
|
1347
|
+
"fieldId"
|
|
1348
|
+
]
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
fields.add(field.fieldId);
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
});
|
|
1356
|
+
var forbiddenControlOrBidi = /[\u0000-\u001f\u007f-\u009f\u202a-\u202e\u2066-\u2069]/u;
|
|
1357
|
+
var unsafeLocator = /<\/?[a-z][^>]*>|\b(?:javascript|data:text\/html)\s*:|\bon[a-z]+\s*=/iu;
|
|
1358
|
+
var renderedPathSchema = z14.string().trim().transform((value) => value !== "/" ? value.replace(/\/+$/u, "") : value).superRefine((value, context) => {
|
|
1359
|
+
if (!value.startsWith("/") || value === "$global" || value.includes("?") || value.includes("#")) {
|
|
1360
|
+
context.addIssue({
|
|
1361
|
+
code: "custom",
|
|
1362
|
+
message: "renderedPath must be a canonical real path."
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
addSafeByteIssues(value, CONTINUE_SITE_SETUP_RENDERED_PATH_MAX_BYTES, "renderedPath", context);
|
|
1366
|
+
});
|
|
1367
|
+
function normalizedSafeTextSchema(maxBytes, label) {
|
|
1368
|
+
return z14.string().transform((value) => value.trim().replace(new RegExp("\\p{White_Space}+", "gu"), " ")).pipe(z14.string().min(1)).superRefine((value, context) => {
|
|
1369
|
+
addSafeByteIssues(value, maxBytes, label, context);
|
|
1370
|
+
if (label === "locatorHint" && unsafeLocator.test(value)) {
|
|
1371
|
+
context.addIssue({
|
|
1372
|
+
code: "custom",
|
|
1373
|
+
message: "locatorHint contains unsafe markup or executable content."
|
|
1374
|
+
});
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
var correctionItemBase = {
|
|
1379
|
+
renderedPath: renderedPathSchema,
|
|
1380
|
+
visibleValue: normalizedSafeTextSchema(CONTINUE_SITE_SETUP_VISIBLE_VALUE_MAX_BYTES, "visibleValue"),
|
|
1381
|
+
locatorHint: normalizedSafeTextSchema(CONTINUE_SITE_SETUP_LOCATOR_HINT_MAX_BYTES, "locatorHint")
|
|
1382
|
+
};
|
|
1383
|
+
var setupCorrectionItemSchema = z14.discriminatedUnion("kind", [
|
|
1384
|
+
z14.object({ kind: z14.literal("text"), ...correctionItemBase }).strict(),
|
|
1385
|
+
z14.object({ kind: z14.literal("link"), ...correctionItemBase }).strict(),
|
|
1386
|
+
z14.object({ kind: z14.literal("image"), ...correctionItemBase }).strict()
|
|
1387
|
+
]);
|
|
1388
|
+
var canonicalHashSchema = z14.string().regex(/^sha256:[0-9a-f]{64}$/iu);
|
|
1389
|
+
var deploymentRevisionSchema = normalizedSafeTextSchema(256, "deploymentRevision");
|
|
1390
|
+
var continueSiteSetupInputSchema = z14.object({
|
|
1391
|
+
siteId: z14.string().uuid(),
|
|
1392
|
+
runId: z14.string().uuid(),
|
|
1393
|
+
expectedFieldContractHash: canonicalHashSchema,
|
|
1394
|
+
expectedDeploymentRevision: deploymentRevisionSchema,
|
|
1395
|
+
correctionItems: z14.array(setupCorrectionItemSchema).max(CONTINUE_SITE_SETUP_MAX_ITEMS)
|
|
1396
|
+
}).strict().superRefine((input, context) => {
|
|
1397
|
+
const bytes = utf8Bytes(stableJsonStringify(input.correctionItems));
|
|
1398
|
+
if (bytes > CONTINUE_SITE_SETUP_ITEMS_MAX_BYTES) {
|
|
1399
|
+
context.addIssue({
|
|
1400
|
+
code: "custom",
|
|
1401
|
+
message: "Correction items exceed the 32 KiB canonical payload limit.",
|
|
1402
|
+
path: ["correctionItems"]
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
var siteSetupContextInputSchema = z14.object({}).strict();
|
|
1407
|
+
var siteSetupApplyInputSchema = z14.object({
|
|
1408
|
+
fieldContract: siteFieldContractV1Schema,
|
|
1409
|
+
editorDefinition: editorDefinitionV1Schema
|
|
1410
|
+
}).strict().superRefine((input, context) => {
|
|
1411
|
+
const contractRoutesByFieldId = new Map(input.fieldContract.fields.map((field) => [
|
|
1412
|
+
field.fieldId,
|
|
1413
|
+
field.routeKey
|
|
1414
|
+
]));
|
|
1415
|
+
const definitionFieldIds = /* @__PURE__ */ new Set();
|
|
1416
|
+
for (const [routeIndex, route] of input.editorDefinition.routes.entries()) {
|
|
1417
|
+
for (const [sectionIndex, section] of route.sections.entries()) {
|
|
1418
|
+
for (const [fieldIndex, field] of section.fields.entries()) {
|
|
1419
|
+
const fieldPath = [
|
|
1420
|
+
"editorDefinition",
|
|
1421
|
+
"routes",
|
|
1422
|
+
routeIndex,
|
|
1423
|
+
"sections",
|
|
1424
|
+
sectionIndex,
|
|
1425
|
+
"fields",
|
|
1426
|
+
fieldIndex,
|
|
1427
|
+
"fieldId"
|
|
1428
|
+
];
|
|
1429
|
+
const contractRouteKey = contractRoutesByFieldId.get(field.fieldId);
|
|
1430
|
+
const contractField = input.fieldContract.fields.find((candidate) => candidate.fieldId === field.fieldId);
|
|
1431
|
+
definitionFieldIds.add(field.fieldId);
|
|
1432
|
+
if (!contractRouteKey) {
|
|
1433
|
+
context.addIssue({
|
|
1434
|
+
code: "custom",
|
|
1435
|
+
message: `Unknown editor field id: ${field.fieldId}`,
|
|
1436
|
+
path: fieldPath
|
|
1437
|
+
});
|
|
1438
|
+
} else if (contractRouteKey !== route.routeKey) {
|
|
1439
|
+
context.addIssue({
|
|
1440
|
+
code: "custom",
|
|
1441
|
+
message: `Editor field ${field.fieldId} must use route ${contractRouteKey}.`,
|
|
1442
|
+
path: fieldPath
|
|
1443
|
+
});
|
|
1444
|
+
} else if (field.maxLength !== void 0 && contractField?.fieldType !== "text" && contractField?.fieldType !== "longText") {
|
|
1445
|
+
context.addIssue({
|
|
1446
|
+
code: "custom",
|
|
1447
|
+
message: `maxLength is not supported for ${contractField?.fieldType ?? "unknown"} fields.`,
|
|
1448
|
+
path: [
|
|
1449
|
+
"editorDefinition",
|
|
1450
|
+
"routes",
|
|
1451
|
+
routeIndex,
|
|
1452
|
+
"sections",
|
|
1453
|
+
sectionIndex,
|
|
1454
|
+
"fields",
|
|
1455
|
+
fieldIndex,
|
|
1456
|
+
"maxLength"
|
|
1457
|
+
]
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
for (const field of input.fieldContract.fields) {
|
|
1464
|
+
if (!definitionFieldIds.has(field.fieldId)) {
|
|
1465
|
+
context.addIssue({
|
|
1466
|
+
code: "custom",
|
|
1467
|
+
message: `Missing editor field id: ${field.fieldId}`,
|
|
1468
|
+
path: ["editorDefinition", "routes"]
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
});
|
|
1473
|
+
var siteSetupVerifyInputSchema = z14.object({
|
|
1474
|
+
deploymentUrl: z14.string().url().refine((value) => value.startsWith("https://")),
|
|
1475
|
+
deploymentRevision: deploymentRevisionSchema,
|
|
1476
|
+
wait: z14.boolean().optional()
|
|
1477
|
+
}).strict();
|
|
1478
|
+
var siteSetupEvidenceInputSchema = z14.object({
|
|
1479
|
+
siteId: z14.string().uuid(),
|
|
1480
|
+
runId: z14.string().uuid(),
|
|
1481
|
+
expectedFieldContractHash: canonicalHashSchema,
|
|
1482
|
+
expectedDeploymentOrigin: z14.string().url().refine((value) => value.startsWith("https://")),
|
|
1483
|
+
expectedDeploymentRevision: deploymentRevisionSchema,
|
|
1484
|
+
publicRuntimeKeyId: z14.string().uuid(),
|
|
1485
|
+
bridgeSessionId: normalizedSafeTextSchema(256, "bridgeSessionId"),
|
|
1486
|
+
observations: z14.array(z14.object({
|
|
1487
|
+
fieldId: fieldIdSchema,
|
|
1488
|
+
routeKey: siteFieldRouteKeySchema,
|
|
1489
|
+
renderedPath: renderedPathSchema,
|
|
1490
|
+
targetResolved: z14.boolean(),
|
|
1491
|
+
previewAck: z14.boolean()
|
|
1492
|
+
}).strict().refine((value) => !value.previewAck || value.targetResolved, "Preview ACK requires a resolved target.")).min(1).max(200)
|
|
1493
|
+
}).strict();
|
|
1494
|
+
var completeSiteSetupInputSchema = z14.object({
|
|
1495
|
+
siteId: z14.string().uuid(),
|
|
1496
|
+
runId: z14.string().uuid(),
|
|
1497
|
+
expectedFieldContractHash: canonicalHashSchema,
|
|
1498
|
+
expectedDeploymentRevision: deploymentRevisionSchema,
|
|
1499
|
+
accessMode: z14.enum(["fallback", "custom_domain"]),
|
|
1500
|
+
siteDomainId: z14.string().uuid().optional(),
|
|
1501
|
+
defaultCanPublish: z14.boolean()
|
|
1502
|
+
}).strict().superRefine((input, context) => {
|
|
1503
|
+
if (input.accessMode === "fallback" && input.siteDomainId) {
|
|
1504
|
+
context.addIssue({
|
|
1505
|
+
code: "custom",
|
|
1506
|
+
message: "Fallback access cannot use a custom domain.",
|
|
1507
|
+
path: ["siteDomainId"]
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
if (input.accessMode === "custom_domain" && !input.siteDomainId) {
|
|
1511
|
+
context.addIssue({
|
|
1512
|
+
code: "custom",
|
|
1513
|
+
message: "Custom-domain access requires a site domain.",
|
|
1514
|
+
path: ["siteDomainId"]
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1518
|
+
var refreshSiteSetupDeploymentInputSchema = z14.object({
|
|
1519
|
+
siteId: z14.string().uuid(),
|
|
1520
|
+
deploymentUrl: z14.string().url().refine((value) => value.startsWith("https://")),
|
|
1521
|
+
deploymentRevision: deploymentRevisionSchema
|
|
1522
|
+
}).strict();
|
|
1523
|
+
function addSafeByteIssues(value, maxBytes, label, context) {
|
|
1524
|
+
if (utf8Bytes(value) > maxBytes) {
|
|
1525
|
+
context.addIssue({
|
|
1526
|
+
code: "custom",
|
|
1527
|
+
message: `${label} exceeds ${maxBytes} UTF-8 bytes.`
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
if (forbiddenControlOrBidi.test(value)) {
|
|
1531
|
+
context.addIssue({
|
|
1532
|
+
code: "custom",
|
|
1533
|
+
message: `${label} contains a forbidden control or bidi character.`
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
function utf8Bytes(value) {
|
|
1538
|
+
return new TextEncoder().encode(value).byteLength;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// ../shared/dist/bridge-protocol.js
|
|
1542
|
+
var BRIDGE_PROTOCOL_VERSION = 1;
|
|
1543
|
+
var bridgeRectSchema = z15.object({
|
|
1544
|
+
x: z15.number(),
|
|
1545
|
+
y: z15.number(),
|
|
1546
|
+
width: z15.number().nonnegative(),
|
|
1547
|
+
height: z15.number().nonnegative()
|
|
1548
|
+
});
|
|
1549
|
+
var previewTextValueSchema = z15.object({
|
|
1550
|
+
fieldType: z15.enum(["text", "longText"]),
|
|
1551
|
+
value: z15.string()
|
|
1552
|
+
});
|
|
1553
|
+
var previewLinkValueSchema = z15.object({
|
|
1554
|
+
fieldType: z15.literal("link"),
|
|
1555
|
+
value: z15.object({
|
|
1556
|
+
href: z15.string().trim().min(1),
|
|
1557
|
+
label: z15.string().trim().min(1).optional()
|
|
1558
|
+
})
|
|
1559
|
+
});
|
|
1560
|
+
var previewImageValueSchema = z15.object({
|
|
1561
|
+
fieldType: z15.literal("image"),
|
|
1562
|
+
value: imageValueSchema
|
|
1563
|
+
});
|
|
1564
|
+
var bridgePreviewValueSchema = z15.discriminatedUnion("fieldType", [
|
|
1565
|
+
previewTextValueSchema,
|
|
1566
|
+
previewLinkValueSchema,
|
|
1567
|
+
previewImageValueSchema
|
|
1568
|
+
]);
|
|
1569
|
+
var FIELD_BRIDGE_MESSAGE_TYPES = [
|
|
1570
|
+
"siteplane:field-bridge:init",
|
|
1571
|
+
"siteplane:field-bridge:ready",
|
|
1572
|
+
"siteplane:field-bridge:fields",
|
|
1573
|
+
"siteplane:field-bridge:select-field",
|
|
1574
|
+
"siteplane:field-bridge:set-selected-field",
|
|
1575
|
+
"siteplane:field-bridge:apply-preview-values",
|
|
1576
|
+
"siteplane:field-bridge:test-preview-values",
|
|
1577
|
+
"siteplane:field-bridge:preview-ack",
|
|
1578
|
+
"siteplane:field-bridge:set-editability-review",
|
|
1579
|
+
"siteplane:field-bridge:missing-content",
|
|
1580
|
+
"siteplane:field-bridge:error"
|
|
1581
|
+
];
|
|
1582
|
+
var canonicalFieldContractHashSchema = z15.string().regex(/^sha256:[0-9a-f]{64}$/u);
|
|
1583
|
+
var realRenderedPathSchema = z15.string().trim().refine((value) => value.startsWith("/") && value !== "$global" && !value.includes("?") && !value.includes("#"), "A field bridge rendered path must be a real canonical path.");
|
|
1584
|
+
var fieldBridgeIdentitySchema = z15.object({
|
|
1585
|
+
publicRuntimeKeyId: z15.string().uuid(),
|
|
1586
|
+
fieldContractHash: canonicalFieldContractHashSchema,
|
|
1587
|
+
deploymentRevision: z15.string().trim().min(1).max(256)
|
|
1588
|
+
}).strict();
|
|
1589
|
+
var fieldBridgeBaseSchema = z15.object({
|
|
1590
|
+
version: z15.literal(BRIDGE_PROTOCOL_VERSION),
|
|
1591
|
+
type: z15.enum(FIELD_BRIDGE_MESSAGE_TYPES),
|
|
1592
|
+
siteId: z15.string().uuid(),
|
|
1593
|
+
sessionId: z15.string().trim().min(1),
|
|
1594
|
+
publicRuntimeKeyId: z15.string().uuid(),
|
|
1595
|
+
fieldContractHash: canonicalFieldContractHashSchema,
|
|
1596
|
+
deploymentRevision: z15.string().trim().min(1).max(256),
|
|
1597
|
+
requestId: z15.string().trim().min(1),
|
|
1598
|
+
sentAt: z15.string().datetime()
|
|
1599
|
+
}).strict();
|
|
1600
|
+
var fieldBridgeFieldSchema = z15.object({
|
|
1601
|
+
fieldId: fieldIdSchema,
|
|
1602
|
+
fieldType: fieldTypeSchema,
|
|
1603
|
+
routeKey: siteFieldRouteKeySchema,
|
|
1604
|
+
renderedPath: realRenderedPathSchema,
|
|
1605
|
+
sourcePath: z15.string().trim().min(1).optional(),
|
|
1606
|
+
editTarget: fieldIdSchema,
|
|
1607
|
+
rect: bridgeRectSchema,
|
|
1608
|
+
targetResolved: z15.literal(true),
|
|
1609
|
+
currentValue: bridgePreviewValueSchema.optional()
|
|
1610
|
+
}).strict();
|
|
1611
|
+
var fieldBridgeInitMessageSchema = fieldBridgeBaseSchema.extend({
|
|
1612
|
+
type: z15.literal("siteplane:field-bridge:init"),
|
|
1613
|
+
payload: z15.object({
|
|
1614
|
+
adminOrigin: z15.string().url(),
|
|
1615
|
+
customerOrigin: z15.string().url(),
|
|
1616
|
+
renderedPath: realRenderedPathSchema
|
|
1617
|
+
}).strict()
|
|
1618
|
+
});
|
|
1619
|
+
var fieldBridgeReadyMessageSchema = fieldBridgeBaseSchema.extend({
|
|
1620
|
+
type: z15.literal("siteplane:field-bridge:ready"),
|
|
1621
|
+
payload: z15.object({
|
|
1622
|
+
renderedPath: realRenderedPathSchema,
|
|
1623
|
+
fieldCount: z15.number().int().nonnegative(),
|
|
1624
|
+
capabilities: z15.tuple([
|
|
1625
|
+
z15.literal("field_selection"),
|
|
1626
|
+
z15.literal("preview_values"),
|
|
1627
|
+
z15.literal("field_evidence"),
|
|
1628
|
+
z15.literal("editability_review")
|
|
1629
|
+
])
|
|
1630
|
+
}).strict()
|
|
1631
|
+
});
|
|
1632
|
+
var fieldBridgeFieldsMessageSchema = fieldBridgeBaseSchema.extend({
|
|
1633
|
+
type: z15.literal("siteplane:field-bridge:fields"),
|
|
1634
|
+
payload: z15.object({
|
|
1635
|
+
renderedPath: realRenderedPathSchema,
|
|
1636
|
+
fields: z15.array(fieldBridgeFieldSchema)
|
|
1637
|
+
}).strict()
|
|
1638
|
+
});
|
|
1639
|
+
var fieldBridgeSelectFieldMessageSchema = fieldBridgeBaseSchema.extend({
|
|
1640
|
+
type: z15.literal("siteplane:field-bridge:select-field"),
|
|
1641
|
+
payload: z15.object({ fieldId: fieldIdSchema }).strict()
|
|
1642
|
+
});
|
|
1643
|
+
var fieldBridgeSetSelectedFieldMessageSchema = fieldBridgeBaseSchema.extend({
|
|
1644
|
+
type: z15.literal("siteplane:field-bridge:set-selected-field"),
|
|
1645
|
+
payload: z15.object({
|
|
1646
|
+
fieldId: fieldIdSchema.nullable(),
|
|
1647
|
+
scrollIntoView: z15.boolean().optional()
|
|
1648
|
+
}).strict()
|
|
1649
|
+
});
|
|
1650
|
+
var fieldBridgeApplyPreviewValuesMessageSchema = fieldBridgeBaseSchema.extend({
|
|
1651
|
+
type: z15.literal("siteplane:field-bridge:apply-preview-values"),
|
|
1652
|
+
payload: z15.object({ values: z15.record(fieldIdSchema, bridgePreviewValueSchema) }).strict()
|
|
1653
|
+
});
|
|
1654
|
+
var fieldBridgeTestPreviewValuesMessageSchema = fieldBridgeBaseSchema.extend({
|
|
1655
|
+
type: z15.literal("siteplane:field-bridge:test-preview-values"),
|
|
1656
|
+
payload: z15.object({ fieldIds: z15.array(fieldIdSchema).min(1).max(200) }).strict()
|
|
1657
|
+
});
|
|
1658
|
+
var fieldBridgePreviewAckMessageSchema = fieldBridgeBaseSchema.extend({
|
|
1659
|
+
type: z15.literal("siteplane:field-bridge:preview-ack"),
|
|
1660
|
+
payload: z15.object({
|
|
1661
|
+
ackRequestId: z15.string().trim().min(1),
|
|
1662
|
+
kind: z15.enum(["apply", "test"]),
|
|
1663
|
+
renderedPath: realRenderedPathSchema,
|
|
1664
|
+
fieldIds: z15.array(fieldIdSchema).max(200)
|
|
1665
|
+
}).strict()
|
|
1666
|
+
});
|
|
1667
|
+
var fieldBridgeSetEditabilityReviewMessageSchema = fieldBridgeBaseSchema.extend({
|
|
1668
|
+
type: z15.literal("siteplane:field-bridge:set-editability-review"),
|
|
1669
|
+
payload: z15.object({ enabled: z15.boolean() }).strict()
|
|
1670
|
+
});
|
|
1671
|
+
var fieldBridgeMissingContentMessageSchema = fieldBridgeBaseSchema.extend({
|
|
1672
|
+
type: z15.literal("siteplane:field-bridge:missing-content"),
|
|
1673
|
+
payload: z15.object({ item: setupCorrectionItemSchema }).strict()
|
|
1674
|
+
});
|
|
1675
|
+
var fieldBridgeErrorMessageSchema = fieldBridgeBaseSchema.extend({
|
|
1676
|
+
type: z15.literal("siteplane:field-bridge:error"),
|
|
1677
|
+
payload: z15.object({
|
|
1678
|
+
code: z15.string().trim().min(1),
|
|
1679
|
+
message: z15.string().trim().min(1),
|
|
1680
|
+
recoverable: z15.boolean().default(false)
|
|
1681
|
+
}).strict()
|
|
1682
|
+
});
|
|
1683
|
+
var fieldBridgeMessageSchema = z15.discriminatedUnion("type", [
|
|
1684
|
+
fieldBridgeInitMessageSchema,
|
|
1685
|
+
fieldBridgeReadyMessageSchema,
|
|
1686
|
+
fieldBridgeFieldsMessageSchema,
|
|
1687
|
+
fieldBridgeSelectFieldMessageSchema,
|
|
1688
|
+
fieldBridgeSetSelectedFieldMessageSchema,
|
|
1689
|
+
fieldBridgeApplyPreviewValuesMessageSchema,
|
|
1690
|
+
fieldBridgeTestPreviewValuesMessageSchema,
|
|
1691
|
+
fieldBridgePreviewAckMessageSchema,
|
|
1692
|
+
fieldBridgeSetEditabilityReviewMessageSchema,
|
|
1693
|
+
fieldBridgeMissingContentMessageSchema,
|
|
1694
|
+
fieldBridgeErrorMessageSchema
|
|
1695
|
+
]);
|
|
1696
|
+
|
|
1697
|
+
// ../shared/dist/commands.js
|
|
1698
|
+
import { z as z17 } from "zod";
|
|
1699
|
+
|
|
1700
|
+
// ../shared/dist/errors.js
|
|
1701
|
+
import { z as z16 } from "zod";
|
|
1702
|
+
var UI_ERROR_CATEGORIES = [
|
|
1703
|
+
"client",
|
|
1704
|
+
"developer",
|
|
1705
|
+
"internal"
|
|
1706
|
+
];
|
|
1707
|
+
var uiErrorCategorySchema = z16.enum(UI_ERROR_CATEGORIES);
|
|
1708
|
+
var commandErrorPayloadSchema = z16.object({
|
|
1709
|
+
code: z16.string().min(1),
|
|
1710
|
+
message: z16.string().min(1),
|
|
1711
|
+
category: uiErrorCategorySchema,
|
|
1712
|
+
details: z16.record(z16.string(), z16.unknown()).optional()
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
// ../shared/dist/commands.js
|
|
1716
|
+
var commandSuccessSchema = z17.object({
|
|
1717
|
+
ok: z17.literal(true),
|
|
1718
|
+
data: z17.unknown()
|
|
1719
|
+
});
|
|
1720
|
+
var commandErrorSchema = z17.object({
|
|
1721
|
+
ok: z17.literal(false),
|
|
1722
|
+
error: commandErrorPayloadSchema
|
|
1723
|
+
});
|
|
1724
|
+
var commandResultSchema = z17.discriminatedUnion("ok", [
|
|
1725
|
+
commandSuccessSchema,
|
|
1726
|
+
commandErrorSchema
|
|
1727
|
+
]);
|
|
1728
|
+
|
|
1729
|
+
// ../shared/dist/fallback-portal.js
|
|
1730
|
+
import { z as z18 } from "zod";
|
|
1731
|
+
var FALLBACK_PORTAL_SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1732
|
+
var fallbackPortalSlugSchema = z18.string().regex(FALLBACK_PORTAL_SLUG_PATTERN, "Invalid fallback portal slug");
|
|
1733
|
+
|
|
1734
|
+
// ../shared/dist/entitlements.js
|
|
1735
|
+
import { z as z19 } from "zod";
|
|
1736
|
+
var entitlementSnapshotSchema = z19.object({
|
|
1737
|
+
planKey: z19.string().trim().min(1),
|
|
1738
|
+
sitesLimit: z19.number().int().positive(),
|
|
1739
|
+
clientAccountsPerSiteLimit: z19.number().int().positive(),
|
|
1740
|
+
customAdminDomainsEnabled: z19.boolean(),
|
|
1741
|
+
agentMcpEnabled: z19.boolean(),
|
|
1742
|
+
emailNotificationsEnabled: z19.boolean(),
|
|
1743
|
+
analyticsEnabled: z19.boolean(),
|
|
1744
|
+
analyticsSitesLimit: z19.number().int().min(0),
|
|
1745
|
+
analyticsClientPerformanceEnabled: z19.boolean(),
|
|
1746
|
+
analyticsMonthlyReportsEnabled: z19.boolean(),
|
|
1747
|
+
analyticsAiSummaryEnabled: z19.boolean(),
|
|
1748
|
+
analyticsRawEventRetentionDays: z19.number().int().min(1).max(ANALYTICS_RETENTION_DEFAULTS.maxRawEventRetentionDaysWithoutAdr),
|
|
1749
|
+
analyticsMonthlyEventLimit: z19.number().int().min(0),
|
|
1750
|
+
bookingEnabled: z19.boolean().default(false),
|
|
1751
|
+
bookingSitesLimit: z19.number().int().min(0).default(0),
|
|
1752
|
+
bookingClientManagementEnabled: z19.boolean().default(false),
|
|
1753
|
+
bookingResourcesPerSiteLimit: z19.number().int().min(0).default(0),
|
|
1754
|
+
bookingServicesPerSiteLimit: z19.number().int().min(0).default(0),
|
|
1755
|
+
bookingMonthlyBookingsLimit: z19.number().int().min(0).default(0),
|
|
1756
|
+
bookingRemindersEnabled: z19.boolean().default(false)
|
|
1757
|
+
});
|
|
1758
|
+
var EARLY_ACCESS_PLAN = {
|
|
1759
|
+
planKey: "early_access_2026",
|
|
1760
|
+
sitesLimit: 3,
|
|
1761
|
+
clientAccountsPerSiteLimit: 5,
|
|
1762
|
+
customAdminDomainsEnabled: true,
|
|
1763
|
+
agentMcpEnabled: true,
|
|
1764
|
+
emailNotificationsEnabled: true,
|
|
1765
|
+
analyticsEnabled: false,
|
|
1766
|
+
analyticsSitesLimit: 0,
|
|
1767
|
+
analyticsClientPerformanceEnabled: false,
|
|
1768
|
+
analyticsMonthlyReportsEnabled: false,
|
|
1769
|
+
analyticsAiSummaryEnabled: false,
|
|
1770
|
+
analyticsRawEventRetentionDays: ANALYTICS_RETENTION_DEFAULTS.enabledRawEventRetentionDays,
|
|
1771
|
+
analyticsMonthlyEventLimit: 0,
|
|
1772
|
+
bookingEnabled: true,
|
|
1773
|
+
bookingSitesLimit: 3,
|
|
1774
|
+
bookingClientManagementEnabled: true,
|
|
1775
|
+
bookingResourcesPerSiteLimit: 10,
|
|
1776
|
+
bookingServicesPerSiteLimit: 50,
|
|
1777
|
+
bookingMonthlyBookingsLimit: 0,
|
|
1778
|
+
bookingRemindersEnabled: true
|
|
1779
|
+
};
|
|
1780
|
+
|
|
1781
|
+
// ../shared/dist/readiness.js
|
|
1782
|
+
import { z as z20 } from "zod";
|
|
1783
|
+
var READINESS_CHECK_KEYS = [
|
|
1784
|
+
"agent_instructions_installed",
|
|
1785
|
+
"runtime_package_connected",
|
|
1786
|
+
"secure_server_access_configured",
|
|
1787
|
+
"preview_bridge_working",
|
|
1788
|
+
"admin_access_url_selected",
|
|
1789
|
+
"default_publishing_mode_selected"
|
|
1790
|
+
];
|
|
1791
|
+
var READINESS_STATUSES = [
|
|
1792
|
+
"pending",
|
|
1793
|
+
"passing",
|
|
1794
|
+
"failing",
|
|
1795
|
+
"warning"
|
|
1796
|
+
];
|
|
1797
|
+
var readinessCheckKeySchema = z20.enum(READINESS_CHECK_KEYS);
|
|
1798
|
+
var readinessStatusSchema = z20.enum(READINESS_STATUSES);
|
|
1799
|
+
|
|
1800
|
+
// ../shared/dist/site.js
|
|
1801
|
+
import { z as z21 } from "zod";
|
|
1802
|
+
var SITE_STATUSES = [
|
|
1803
|
+
"setup",
|
|
1804
|
+
"client_ready",
|
|
1805
|
+
"disabled",
|
|
1806
|
+
"archived"
|
|
1807
|
+
];
|
|
1808
|
+
var siteStatusSchema = z21.enum(SITE_STATUSES);
|
|
1809
|
+
var websiteUrlSchema = z21.string().trim().url().refine((value) => value.startsWith("https://") || value.startsWith("http://"), "Website URL must use http or https");
|
|
1810
|
+
|
|
1811
|
+
// ../cli/dist/analytics/manifest.js
|
|
1812
|
+
async function readAnalyticsManifestFile(manifestPath) {
|
|
1813
|
+
return validateAnalyticsManifestInput(JSON.parse(await readFile(manifestPath, "utf8")));
|
|
1814
|
+
}
|
|
1815
|
+
async function writeAnalyticsManifestFile(manifestPath, manifest) {
|
|
1816
|
+
await mkdir(dirname(manifestPath), {
|
|
1817
|
+
recursive: true
|
|
1818
|
+
});
|
|
1819
|
+
await writeFile(manifestPath, `${JSON.stringify(analyticsManifestSchema.parse(manifest), null, 2)}
|
|
1820
|
+
`, "utf8");
|
|
1821
|
+
}
|
|
1822
|
+
function validateAnalyticsManifestInput(input) {
|
|
1823
|
+
return redactAnalyticsManifest(analyticsManifestSchema.parse(input));
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// ../cli/dist/analytics/static-scan.js
|
|
1827
|
+
import ts from "typescript";
|
|
1828
|
+
|
|
1829
|
+
// ../cli/dist/analytics/booking-providers.js
|
|
1830
|
+
function buildBookingProviderHintFromHref(href, mode, options = {}) {
|
|
1831
|
+
const url = parseStaticHttpUrl(href);
|
|
1832
|
+
if (!url) {
|
|
1833
|
+
return void 0;
|
|
1834
|
+
}
|
|
1835
|
+
const bookingProvider = inferKnownBookingProviderFromHost(url.hostname) ?? inferExplicitOtherProvider(options.explicitProvider);
|
|
1836
|
+
if (!bookingProvider) {
|
|
1837
|
+
return void 0;
|
|
1838
|
+
}
|
|
1839
|
+
return {
|
|
1840
|
+
bookingProvider,
|
|
1841
|
+
bookingUrlHost: normalizeHost(url.hostname),
|
|
1842
|
+
bookingAttributionMode: mode
|
|
1843
|
+
};
|
|
1844
|
+
}
|
|
1845
|
+
function inferKnownBookingProviderFromHost(host) {
|
|
1846
|
+
const normalizedHost = normalizeHost(host);
|
|
1847
|
+
if (matchesProviderDomain(normalizedHost, "treatwell")) {
|
|
1848
|
+
return "treatwell";
|
|
1849
|
+
}
|
|
1850
|
+
if (matchesProviderDomain(normalizedHost, "beautinda")) {
|
|
1851
|
+
return "beautinda";
|
|
1852
|
+
}
|
|
1853
|
+
return void 0;
|
|
1854
|
+
}
|
|
1855
|
+
function inferExplicitOtherProvider(explicitProvider) {
|
|
1856
|
+
return explicitProvider === "other" ? "other" : void 0;
|
|
1857
|
+
}
|
|
1858
|
+
var providerRegistrableDomains = {
|
|
1859
|
+
treatwell: /* @__PURE__ */ new Set([
|
|
1860
|
+
"treatwell.at",
|
|
1861
|
+
"treatwell.be",
|
|
1862
|
+
"treatwell.ch",
|
|
1863
|
+
"treatwell.co.uk",
|
|
1864
|
+
"treatwell.de",
|
|
1865
|
+
"treatwell.es",
|
|
1866
|
+
"treatwell.fr",
|
|
1867
|
+
"treatwell.ie",
|
|
1868
|
+
"treatwell.it",
|
|
1869
|
+
"treatwell.nl",
|
|
1870
|
+
"treatwell.pt"
|
|
1871
|
+
]),
|
|
1872
|
+
beautinda: /* @__PURE__ */ new Set(["beautinda.de"])
|
|
1873
|
+
};
|
|
1874
|
+
function matchesProviderDomain(host, provider) {
|
|
1875
|
+
return Array.from(providerRegistrableDomains[provider]).some((domain) => host === domain || host.endsWith(`.${domain}`));
|
|
1876
|
+
}
|
|
1877
|
+
function parseStaticHttpUrl(href) {
|
|
1878
|
+
try {
|
|
1879
|
+
const url = new URL(href);
|
|
1880
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url : void 0;
|
|
1881
|
+
} catch {
|
|
1882
|
+
return void 0;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
function normalizeHost(host) {
|
|
1886
|
+
return host.toLowerCase().replace(/\.$/u, "");
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// ../cli/dist/analytics/static-scan.js
|
|
1890
|
+
function scanAnalyticsSourceFiles(files, input) {
|
|
1891
|
+
const targets = [];
|
|
1892
|
+
const hardErrors = [];
|
|
1893
|
+
const warnings = [];
|
|
1894
|
+
const suggestions = [];
|
|
1895
|
+
for (const file of files) {
|
|
1896
|
+
scanSourceFile(file, targets, hardErrors, suggestions);
|
|
1897
|
+
}
|
|
1898
|
+
const manifest = analyticsManifestSchema.parse({
|
|
1899
|
+
schemaVersion: ANALYTICS_SCHEMA_VERSION,
|
|
1900
|
+
siteId: input.siteId,
|
|
1901
|
+
siteKeyRef: input.siteKeyRef,
|
|
1902
|
+
siteKeyLookup: input.siteKeyLookup,
|
|
1903
|
+
routes: createRoutes(targets),
|
|
1904
|
+
targets: uniqueTargets(targets).map(createManifestTarget),
|
|
1905
|
+
...input.metadata ? { metadata: input.metadata } : {}
|
|
1906
|
+
});
|
|
1907
|
+
return {
|
|
1908
|
+
manifest,
|
|
1909
|
+
hardErrors,
|
|
1910
|
+
warnings,
|
|
1911
|
+
suggestions
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
function scanSourceFile(file, targets, hardErrors, suggestions) {
|
|
1915
|
+
const sourceFile = ts.createSourceFile(file.filePath, file.sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
1916
|
+
function visit(node) {
|
|
1917
|
+
if (ts.isJsxSelfClosingElement(node) || ts.isJsxOpeningElement(node)) {
|
|
1918
|
+
const tagName = getJsxTagName(node.tagName);
|
|
1919
|
+
const attributes = readJsxAttributes(node.attributes);
|
|
1920
|
+
const eventValue = stringValue(attributes.get("data-siteplane-event"));
|
|
1921
|
+
const href = stringValue(attributes.get("href"));
|
|
1922
|
+
if (eventValue === void 0 && href !== void 0 && isClickableLinkTag(tagName) && buildBookingProviderHintFromHref(href, "outbound_click_and_utm") !== void 0) {
|
|
1923
|
+
suggestions.push({
|
|
1924
|
+
code: "analytics_scan.booking_provider_tracking_missing",
|
|
1925
|
+
message: "Booking provider link is missing Siteplane analytics tracking on the clickable link.",
|
|
1926
|
+
filePath: file.filePath
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
if (tagName === "form" && eventValue === void 0) {
|
|
1930
|
+
suggestions.push({
|
|
1931
|
+
code: "analytics_scan.form_tracking_missing",
|
|
1932
|
+
message: "Form is missing Siteplane analytics tracking.",
|
|
1933
|
+
filePath: file.filePath
|
|
1934
|
+
});
|
|
1935
|
+
}
|
|
1936
|
+
if (eventValue !== void 0) {
|
|
1937
|
+
extractAnalyticsTarget(file, tagName, attributes, eventValue, hardErrors, targets);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
ts.forEachChild(node, visit);
|
|
1941
|
+
}
|
|
1942
|
+
visit(sourceFile);
|
|
1943
|
+
}
|
|
1944
|
+
function extractAnalyticsTarget(file, tagName, attributes, eventValue, hardErrors, targets) {
|
|
1945
|
+
const events = parseDataAttributeEvents(eventValue);
|
|
1946
|
+
const targetId = stringValue(attributes.get("data-siteplane-target-id")) ?? stringValue(attributes.get("data-siteplane-section-id"));
|
|
1947
|
+
if (events.length === 0) {
|
|
1948
|
+
hardErrors.push({
|
|
1949
|
+
code: "analytics_scan.invalid_event",
|
|
1950
|
+
message: `Invalid analytics data attribute event: ${eventValue}`,
|
|
1951
|
+
filePath: file.filePath,
|
|
1952
|
+
...targetId ? { targetId } : {}
|
|
1953
|
+
});
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
if (targetId === void 0) {
|
|
1957
|
+
hardErrors.push({
|
|
1958
|
+
code: "analytics_scan.target_id_missing",
|
|
1959
|
+
message: "Analytics data attribute is missing a static target id.",
|
|
1960
|
+
filePath: file.filePath
|
|
1961
|
+
});
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
for (const event of events) {
|
|
1965
|
+
if (isServerOnlyAnalyticsEvent(event)) {
|
|
1966
|
+
hardErrors.push({
|
|
1967
|
+
code: "analytics_scan.server_only_event_in_browser",
|
|
1968
|
+
message: `Server-only analytics event cannot be used in browser data attributes: ${event}`,
|
|
1969
|
+
filePath: file.filePath,
|
|
1970
|
+
targetId
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
const route = inferRoute(file.filePath);
|
|
1975
|
+
const type = inferTargetType(attributes, events);
|
|
1976
|
+
const bookingSourceHints = createBookingProviderSourceHints({
|
|
1977
|
+
attributes,
|
|
1978
|
+
events,
|
|
1979
|
+
tagName,
|
|
1980
|
+
targetId,
|
|
1981
|
+
type
|
|
1982
|
+
});
|
|
1983
|
+
if ("sourceHints" in bookingSourceHints && !hasRequiredBookingAttribution(stringValue(attributes.get("href")), targetId)) {
|
|
1984
|
+
hardErrors.push({
|
|
1985
|
+
code: "analytics_scan.booking_provider_attribution_missing",
|
|
1986
|
+
message: "Booking provider links require siteplane source/medium plus campaign and target UTMs.",
|
|
1987
|
+
filePath: file.filePath,
|
|
1988
|
+
targetId
|
|
1989
|
+
});
|
|
1990
|
+
}
|
|
1991
|
+
const formSourceHints = createFormSourceHints({
|
|
1992
|
+
attributes,
|
|
1993
|
+
tagName,
|
|
1994
|
+
targetId,
|
|
1995
|
+
type
|
|
1996
|
+
});
|
|
1997
|
+
targets.push({
|
|
1998
|
+
id: targetId,
|
|
1999
|
+
type,
|
|
2000
|
+
events,
|
|
2001
|
+
...route ? { route } : {},
|
|
2002
|
+
...mergeSourceHints(bookingSourceHints, formSourceHints)
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
function createFormSourceHints(input) {
|
|
2006
|
+
if (input.type !== "form" || input.tagName !== "form")
|
|
2007
|
+
return {};
|
|
2008
|
+
const explicit = analyticsFormPurposeSchema.safeParse(input.attributes.get("data-siteplane-form-purpose"));
|
|
2009
|
+
if (explicit.success) {
|
|
2010
|
+
return { sourceHints: { formPurpose: explicit.data } };
|
|
2011
|
+
}
|
|
2012
|
+
const tokens = [
|
|
2013
|
+
input.targetId,
|
|
2014
|
+
stringValue(input.attributes.get("id")),
|
|
2015
|
+
stringValue(input.attributes.get("name")),
|
|
2016
|
+
stringValue(input.attributes.get("action")),
|
|
2017
|
+
stringValue(input.attributes.get("aria-label"))
|
|
2018
|
+
].filter((value) => Boolean(value)).join(" ").toLowerCase();
|
|
2019
|
+
const formPurpose = /newsletter|subscribe|subscription/u.test(tokens) ? "newsletter" : /search/u.test(tokens) ? "search" : /login|log-in|sign-in|signin|auth/u.test(tokens) ? "login" : /lead/u.test(tokens) ? "lead" : /contact|inquiry|enquiry|quote|request/u.test(tokens) ? "contact" : "other";
|
|
2020
|
+
return { sourceHints: { formPurpose } };
|
|
2021
|
+
}
|
|
2022
|
+
function mergeSourceHints(...hints) {
|
|
2023
|
+
const sourceHints = Object.assign({}, ...hints.flatMap((hint) => "sourceHints" in hint ? [hint.sourceHints] : []));
|
|
2024
|
+
return Object.keys(sourceHints).length > 0 ? { sourceHints } : {};
|
|
2025
|
+
}
|
|
2026
|
+
function hasRequiredBookingAttribution(href, targetId) {
|
|
2027
|
+
if (!href)
|
|
2028
|
+
return false;
|
|
2029
|
+
try {
|
|
2030
|
+
const url = new URL(href);
|
|
2031
|
+
return url.searchParams.get("utm_source") === "siteplane" && url.searchParams.get("utm_medium") === "website" && Boolean(url.searchParams.get("utm_campaign")) && url.searchParams.get("utm_content") === targetId;
|
|
2032
|
+
} catch {
|
|
2033
|
+
return false;
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
function parseDataAttributeEvents(value) {
|
|
2037
|
+
const dataAttributeEvent = analyticsDataAttributeEventSchema.safeParse(value);
|
|
2038
|
+
if (dataAttributeEvent.success) {
|
|
2039
|
+
return expandAnalyticsDataAttributeEvent(dataAttributeEvent.data);
|
|
2040
|
+
}
|
|
2041
|
+
const eventName = analyticsEventNameSchema.safeParse(value);
|
|
2042
|
+
return eventName.success ? [eventName.data] : [];
|
|
2043
|
+
}
|
|
2044
|
+
function inferTargetType(attributes, events) {
|
|
2045
|
+
const explicit = analyticsTargetTypeSchema.safeParse(attributes.get("data-siteplane-target-type"));
|
|
2046
|
+
if (explicit.success) {
|
|
2047
|
+
return explicit.data;
|
|
2048
|
+
}
|
|
2049
|
+
if (events.includes("section_view")) {
|
|
2050
|
+
return "section";
|
|
2051
|
+
}
|
|
2052
|
+
if (events.includes("form_start") || events.includes("form_submit") || events.includes("form_error")) {
|
|
2053
|
+
return "form";
|
|
2054
|
+
}
|
|
2055
|
+
if (events.includes("file_download")) {
|
|
2056
|
+
return "file";
|
|
2057
|
+
}
|
|
2058
|
+
if (events.includes("outbound_click") || events.includes("email_click") || events.includes("phone_click")) {
|
|
2059
|
+
return "link";
|
|
2060
|
+
}
|
|
2061
|
+
if (events.includes("conversion") || events.includes("lead_created")) {
|
|
2062
|
+
return "conversion";
|
|
2063
|
+
}
|
|
2064
|
+
return "cta";
|
|
2065
|
+
}
|
|
2066
|
+
function createManifestTarget(target) {
|
|
2067
|
+
const base = {
|
|
2068
|
+
id: target.id,
|
|
2069
|
+
type: target.type,
|
|
2070
|
+
...target.route ? { route: target.route } : {},
|
|
2071
|
+
...target.sourceHints ? { sourceHints: target.sourceHints } : {}
|
|
2072
|
+
};
|
|
2073
|
+
if (target.events.length === 1) {
|
|
2074
|
+
return {
|
|
2075
|
+
...base,
|
|
2076
|
+
event: target.events[0]
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
return {
|
|
2080
|
+
...base,
|
|
2081
|
+
events: target.events
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
function createBookingProviderSourceHints(input) {
|
|
2085
|
+
if (input.type !== "link" || !input.events.includes("outbound_click") || !isClickableLinkTag(input.tagName)) {
|
|
2086
|
+
return {};
|
|
2087
|
+
}
|
|
2088
|
+
const href = stringValue(input.attributes.get("href"));
|
|
2089
|
+
if (!href) {
|
|
2090
|
+
return {};
|
|
2091
|
+
}
|
|
2092
|
+
const explicitProvider = bookingProviderSchema.safeParse(input.attributes.get("data-siteplane-booking-provider"));
|
|
2093
|
+
const attributionMode = bookingAttributionModeSchema.safeParse(input.attributes.get("data-siteplane-booking-attribution-mode"));
|
|
2094
|
+
const baseHint = buildBookingProviderHintFromHref(href, attributionMode.success ? attributionMode.data : "outbound_click_and_utm", explicitProvider.success ? { explicitProvider: explicitProvider.data } : void 0);
|
|
2095
|
+
if (!baseHint) {
|
|
2096
|
+
return {};
|
|
2097
|
+
}
|
|
2098
|
+
return {
|
|
2099
|
+
sourceHints: {
|
|
2100
|
+
...baseHint,
|
|
2101
|
+
href,
|
|
2102
|
+
utmContent: input.targetId
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
2105
|
+
}
|
|
2106
|
+
function isClickableLinkTag(tagName) {
|
|
2107
|
+
return tagName === "a" || tagName === "Link";
|
|
2108
|
+
}
|
|
2109
|
+
function uniqueTargets(targets) {
|
|
2110
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2111
|
+
const unique = [];
|
|
2112
|
+
for (const target of targets) {
|
|
2113
|
+
if (seen.has(target.id)) {
|
|
2114
|
+
continue;
|
|
2115
|
+
}
|
|
2116
|
+
seen.add(target.id);
|
|
2117
|
+
unique.push(target);
|
|
2118
|
+
}
|
|
2119
|
+
return unique;
|
|
2120
|
+
}
|
|
2121
|
+
function createRoutes(targets) {
|
|
2122
|
+
const paths = /* @__PURE__ */ new Set();
|
|
2123
|
+
for (const target of targets) {
|
|
2124
|
+
if (target.route) {
|
|
2125
|
+
paths.add(target.route);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
return [...paths].map((path) => ({ path }));
|
|
2129
|
+
}
|
|
2130
|
+
function readJsxAttributes(attributes) {
|
|
2131
|
+
const values = /* @__PURE__ */ new Map();
|
|
2132
|
+
for (const attribute of attributes.properties) {
|
|
2133
|
+
if (!ts.isJsxAttribute(attribute)) {
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
const name = attribute.name.getText();
|
|
2137
|
+
if (!attribute.initializer) {
|
|
2138
|
+
values.set(name, true);
|
|
2139
|
+
continue;
|
|
2140
|
+
}
|
|
2141
|
+
if (ts.isStringLiteral(attribute.initializer)) {
|
|
2142
|
+
values.set(name, attribute.initializer.text);
|
|
2143
|
+
continue;
|
|
2144
|
+
}
|
|
2145
|
+
if (ts.isJsxExpression(attribute.initializer)) {
|
|
2146
|
+
values.set(name, staticExpressionValue(attribute.initializer.expression));
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
return values;
|
|
2150
|
+
}
|
|
2151
|
+
function staticExpressionValue(expression) {
|
|
2152
|
+
if (!expression) {
|
|
2153
|
+
return void 0;
|
|
2154
|
+
}
|
|
2155
|
+
if (ts.isStringLiteral(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) {
|
|
2156
|
+
return expression.text;
|
|
2157
|
+
}
|
|
2158
|
+
return void 0;
|
|
2159
|
+
}
|
|
2160
|
+
function getJsxTagName(tagName) {
|
|
2161
|
+
return tagName.getText();
|
|
2162
|
+
}
|
|
2163
|
+
function stringValue(value) {
|
|
2164
|
+
return typeof value === "string" ? value : void 0;
|
|
2165
|
+
}
|
|
2166
|
+
function inferRoute(filePath) {
|
|
2167
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
2168
|
+
const appIndex = normalized.indexOf("app/");
|
|
2169
|
+
if (appIndex === -1 || !normalized.endsWith("page.tsx")) {
|
|
2170
|
+
return void 0;
|
|
2171
|
+
}
|
|
2172
|
+
const route = normalized.slice(appIndex + 4, -"page.tsx".length).split("/").filter((segment) => segment && !segment.startsWith("(")).join("/");
|
|
2173
|
+
return route ? `/${route}` : "/";
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
// ../cli/dist/booking/manifest.js
|
|
2177
|
+
import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
2178
|
+
import { dirname as dirname2 } from "path";
|
|
2179
|
+
async function readBookingManifestFile(manifestPath) {
|
|
2180
|
+
return validateBookingManifestInput(JSON.parse(await readFile2(manifestPath, "utf8")));
|
|
2181
|
+
}
|
|
2182
|
+
async function writeBookingManifestFile(manifestPath, manifest) {
|
|
2183
|
+
await mkdir2(dirname2(manifestPath), {
|
|
2184
|
+
recursive: true
|
|
2185
|
+
});
|
|
2186
|
+
await writeFile2(manifestPath, `${JSON.stringify(bookingManifestSchema.parse(manifest), null, 2)}
|
|
2187
|
+
`, "utf8");
|
|
2188
|
+
}
|
|
2189
|
+
function validateBookingManifestInput(input) {
|
|
2190
|
+
const redactedKeys = collectRedactedBookingKeys(input);
|
|
2191
|
+
const redacted = redactBookingManifest(input);
|
|
2192
|
+
return {
|
|
2193
|
+
manifest: bookingManifestSchema.parse(redacted),
|
|
2194
|
+
redactedKeys: [...redactedKeys].sort()
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
var secretManifestKeys2 = /* @__PURE__ */ new Set([
|
|
2198
|
+
"siteKey",
|
|
2199
|
+
"rawKey",
|
|
2200
|
+
"token",
|
|
2201
|
+
"accessToken"
|
|
2202
|
+
]);
|
|
2203
|
+
function collectRedactedBookingKeys(input, prefix = "") {
|
|
2204
|
+
if (Array.isArray(input)) {
|
|
2205
|
+
return input.flatMap((item, index) => collectRedactedBookingKeys(item, `${prefix}[${index}]`));
|
|
2206
|
+
}
|
|
2207
|
+
if (!input || typeof input !== "object") {
|
|
2208
|
+
return [];
|
|
2209
|
+
}
|
|
2210
|
+
return Object.entries(input).flatMap(([key, value]) => {
|
|
2211
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
2212
|
+
if (secretManifestKeys2.has(key)) {
|
|
2213
|
+
return [path];
|
|
2214
|
+
}
|
|
2215
|
+
return collectRedactedBookingKeys(value, path);
|
|
2216
|
+
});
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// ../cli/dist/booking/static-scan.js
|
|
2220
|
+
function scanBookingSourceFiles(files, input) {
|
|
2221
|
+
const embeds = files.flatMap(scanBookingEmbedFile);
|
|
2222
|
+
const findings = embeds.map(createEmbedFoundIssue);
|
|
2223
|
+
const hardErrors = embeds.length === 0 ? [
|
|
2224
|
+
{
|
|
2225
|
+
code: "booking_scan.embed_missing",
|
|
2226
|
+
message: "No Siteplane Booking embed script, web component or iframe was found.",
|
|
2227
|
+
filePath: "siteplane.booking.json"
|
|
2228
|
+
}
|
|
2229
|
+
] : [];
|
|
2230
|
+
const warnings = [
|
|
2231
|
+
...createPageSlugWarnings(embeds, input.bookingPageSlug),
|
|
2232
|
+
...createOriginWarnings(files, input.productionOrigin)
|
|
2233
|
+
];
|
|
2234
|
+
const selectedEmbed = selectManifestEmbed(embeds);
|
|
2235
|
+
const manifest = bookingManifestSchema.parse({
|
|
2236
|
+
schemaVersion: 1,
|
|
2237
|
+
siteId: input.siteId,
|
|
2238
|
+
siteKeyPrefix: input.siteKeyPrefix,
|
|
2239
|
+
bookingPageSlug: selectedEmbed?.pageSlug ?? input.bookingPageSlug,
|
|
2240
|
+
...selectedEmbed ? { embedMode: selectedEmbed.embedMode } : {},
|
|
2241
|
+
...input.productionOrigin ? { productionOrigin: input.productionOrigin } : {}
|
|
2242
|
+
});
|
|
2243
|
+
return {
|
|
2244
|
+
manifest,
|
|
2245
|
+
findings,
|
|
2246
|
+
hardErrors,
|
|
2247
|
+
warnings,
|
|
2248
|
+
suggestions: []
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
function scanBookingEmbedFile(file) {
|
|
2252
|
+
const embeds = [];
|
|
2253
|
+
const source = file.sourceText;
|
|
2254
|
+
if (/\/embed\/booking\.js/u.test(source)) {
|
|
2255
|
+
embeds.push({
|
|
2256
|
+
filePath: file.filePath,
|
|
2257
|
+
embedMode: "script"
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
if (/<siteplane-booking\b/u.test(source)) {
|
|
2261
|
+
const pageSlug = readAttributeValue(source, "booking-page-slug");
|
|
2262
|
+
embeds.push({
|
|
2263
|
+
filePath: file.filePath,
|
|
2264
|
+
embedMode: "web_component",
|
|
2265
|
+
...pageSlug ? { pageSlug } : {}
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
for (const match of source.matchAll(/<iframe\b[^>]*>/giu)) {
|
|
2269
|
+
const tag = match[0];
|
|
2270
|
+
const src = readAttributeValue(tag, "src");
|
|
2271
|
+
const pageSlug = src ? readBookingPageSlug(src) : void 0;
|
|
2272
|
+
if (/data-siteplane-booking/u.test(tag) || pageSlug !== void 0) {
|
|
2273
|
+
embeds.push({
|
|
2274
|
+
filePath: file.filePath,
|
|
2275
|
+
embedMode: "iframe",
|
|
2276
|
+
...pageSlug ? { pageSlug } : {}
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
return embeds;
|
|
2281
|
+
}
|
|
2282
|
+
function createEmbedFoundIssue(embed) {
|
|
2283
|
+
return {
|
|
2284
|
+
code: "booking_scan.embed_found",
|
|
2285
|
+
message: "Siteplane Booking embed was found.",
|
|
2286
|
+
filePath: embed.filePath,
|
|
2287
|
+
embedMode: embed.embedMode,
|
|
2288
|
+
...embed.pageSlug ? { pageSlug: embed.pageSlug } : {}
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
function createPageSlugWarnings(embeds, expectedSlug) {
|
|
2292
|
+
return embeds.filter((embed) => embed.pageSlug && embed.pageSlug !== expectedSlug).map((embed) => {
|
|
2293
|
+
const pageSlug = embed.pageSlug;
|
|
2294
|
+
return {
|
|
2295
|
+
code: "booking_scan.page_slug_mismatch",
|
|
2296
|
+
message: `Booking embed references page slug ${pageSlug}, but the manifest is configured for ${expectedSlug}.`,
|
|
2297
|
+
filePath: embed.filePath,
|
|
2298
|
+
embedMode: embed.embedMode,
|
|
2299
|
+
...pageSlug ? { pageSlug } : {}
|
|
2300
|
+
};
|
|
2301
|
+
});
|
|
2302
|
+
}
|
|
2303
|
+
function createOriginWarnings(files, productionOrigin) {
|
|
2304
|
+
if (!productionOrigin) {
|
|
2305
|
+
return [];
|
|
2306
|
+
}
|
|
2307
|
+
if (files.some((file) => file.sourceText.includes(productionOrigin))) {
|
|
2308
|
+
return [];
|
|
2309
|
+
}
|
|
2310
|
+
return [
|
|
2311
|
+
{
|
|
2312
|
+
code: "booking_scan.origin_not_detected",
|
|
2313
|
+
message: "The configured production origin was not found statically; verify it is registered as an allowed Booking origin in Siteplane.",
|
|
2314
|
+
filePath: "siteplane.booking.json"
|
|
2315
|
+
}
|
|
2316
|
+
];
|
|
2317
|
+
}
|
|
2318
|
+
function selectManifestEmbed(embeds) {
|
|
2319
|
+
return embeds.find((embed) => embed.embedMode === "web_component") ?? embeds.find((embed) => embed.embedMode === "iframe") ?? embeds[0];
|
|
2320
|
+
}
|
|
2321
|
+
function readAttributeValue(source, attributeName) {
|
|
2322
|
+
const escapedName = escapeRegExp(attributeName);
|
|
2323
|
+
const match = new RegExp(`${escapedName}\\s*=\\s*["']([^"']+)["']`, "iu").exec(source);
|
|
2324
|
+
return match?.[1];
|
|
2325
|
+
}
|
|
2326
|
+
function readBookingPageSlug(value) {
|
|
2327
|
+
return /\/book\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:[/?#]|$)/u.exec(value)?.[1];
|
|
2328
|
+
}
|
|
2329
|
+
function escapeRegExp(value) {
|
|
2330
|
+
return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// ../cli/dist/commands/analytics/check.js
|
|
2334
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
2335
|
+
async function analyticsCheckCommand(input) {
|
|
2336
|
+
const validatedManifest = await readAnalyticsManifestFile(input.manifestPath);
|
|
2337
|
+
const sourceFiles = await readSourceFiles(input.sourceFilePaths);
|
|
2338
|
+
const scanResult = scanAnalyticsSourceFiles(sourceFiles, {
|
|
2339
|
+
siteId: validatedManifest.manifest.siteId,
|
|
2340
|
+
siteKeyRef: validatedManifest.manifest.siteKeyRef,
|
|
2341
|
+
siteKeyLookup: validatedManifest.manifest.siteKeyLookup,
|
|
2342
|
+
...validatedManifest.manifest.metadata ? { metadata: validatedManifest.manifest.metadata } : {}
|
|
2343
|
+
});
|
|
2344
|
+
const scannedManifest = preserveManualServerTargets(scanResult.manifest, validatedManifest.manifest);
|
|
2345
|
+
const sdkInstalled = await hasAnalyticsSdk(input.packageJsonPath);
|
|
2346
|
+
const hardErrors = [
|
|
2347
|
+
...createBoundaryErrors(validatedManifest),
|
|
2348
|
+
...sdkInstalled ? [] : [createSdkMissingIssue(input.manifestPath)],
|
|
2349
|
+
...createIntegrationErrors(validatedManifest.manifest, sourceFiles, input.manifestPath),
|
|
2350
|
+
...await readAgentArtifactErrors(input.artifactFilePaths ?? []),
|
|
2351
|
+
...scanResult.hardErrors
|
|
2352
|
+
];
|
|
2353
|
+
const warnings = [
|
|
2354
|
+
...scanResult.warnings,
|
|
2355
|
+
...await readCspWarnings(input.cspFilePaths ?? [])
|
|
2356
|
+
];
|
|
2357
|
+
return {
|
|
2358
|
+
ok: hardErrors.length === 0,
|
|
2359
|
+
manifest: validatedManifest.manifest,
|
|
2360
|
+
scannedManifest,
|
|
2361
|
+
sdkInstalled,
|
|
2362
|
+
redactedKeys: validatedManifest.redactedKeys,
|
|
2363
|
+
hardErrors,
|
|
2364
|
+
warnings,
|
|
2365
|
+
suggestions: scanResult.suggestions
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
function preserveManualServerTargets(scanned, configured) {
|
|
2369
|
+
const scannedIds = new Set(scanned.targets.map((target) => target.id));
|
|
2370
|
+
const manualServerTargets = configured.targets.filter((target) => target.type === "server_conversion" && !scannedIds.has(target.id));
|
|
2371
|
+
return manualServerTargets.length === 0 ? scanned : { ...scanned, targets: [...scanned.targets, ...manualServerTargets] };
|
|
2372
|
+
}
|
|
2373
|
+
function createIntegrationErrors(manifest, sourceFiles, manifestPath) {
|
|
2374
|
+
const sourceText = sourceFiles.map((file) => file.sourceText).join("\n");
|
|
2375
|
+
const issues = [];
|
|
2376
|
+
if (!sourceText.includes("@siteplane/analytics") || !sourceText.includes("initSiteplaneAnalytics")) {
|
|
2377
|
+
issues.push({
|
|
2378
|
+
code: "analytics_check.sdk_not_initialized",
|
|
2379
|
+
message: "The Analytics SDK is not initialized in scanned customer source.",
|
|
2380
|
+
filePath: manifestPath
|
|
2381
|
+
});
|
|
2382
|
+
}
|
|
2383
|
+
if (!sourceText.includes("NEXT_PUBLIC_SITEPLANE_ANALYTICS_ENDPOINT") && !sourceText.includes("/api/analytics/collect")) {
|
|
2384
|
+
issues.push({
|
|
2385
|
+
code: "analytics_check.collector_endpoint_missing",
|
|
2386
|
+
message: "Customer source does not configure the Siteplane collector endpoint.",
|
|
2387
|
+
filePath: manifestPath
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
if (manifest.metadata?.siteplanePreviewSuppression !== true) {
|
|
2391
|
+
issues.push({
|
|
2392
|
+
code: "analytics_check.preview_suppression_unverified",
|
|
2393
|
+
message: "Manifest does not attest Siteplane editor-preview suppression.",
|
|
2394
|
+
filePath: manifestPath
|
|
2395
|
+
});
|
|
2396
|
+
}
|
|
2397
|
+
return issues;
|
|
2398
|
+
}
|
|
2399
|
+
async function readAgentArtifactErrors(filePaths) {
|
|
2400
|
+
const issues = [];
|
|
2401
|
+
for (const filePath of filePaths) {
|
|
2402
|
+
let sourceText;
|
|
2403
|
+
try {
|
|
2404
|
+
sourceText = await readFile3(filePath, "utf8");
|
|
2405
|
+
} catch {
|
|
2406
|
+
issues.push({
|
|
2407
|
+
code: "analytics_check.agent_artifact_missing",
|
|
2408
|
+
message: "Required Analytics agent instructions are missing.",
|
|
2409
|
+
filePath
|
|
2410
|
+
});
|
|
2411
|
+
continue;
|
|
2412
|
+
}
|
|
2413
|
+
if (!sourceText.includes(`Analytics contract version: ${ANALYTICS_CONTRACT_VERSION}`) || !sourceText.includes(`Analytics contract hash: ${ANALYTICS_CONTRACT_HASH}`)) {
|
|
2414
|
+
issues.push({
|
|
2415
|
+
code: "analytics_check.agent_artifact_stale",
|
|
2416
|
+
message: "Analytics agent instructions do not match the installed CLI/manifest contract.",
|
|
2417
|
+
filePath
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
return issues;
|
|
2422
|
+
}
|
|
2423
|
+
async function readSourceFiles(filePaths) {
|
|
2424
|
+
return Promise.all(filePaths.map(async (filePath) => ({
|
|
2425
|
+
filePath,
|
|
2426
|
+
sourceText: await readFile3(filePath, "utf8")
|
|
2427
|
+
})));
|
|
2428
|
+
}
|
|
2429
|
+
function createBoundaryErrors(validation) {
|
|
2430
|
+
return validation.redactedKeys.map((key) => ({
|
|
2431
|
+
code: "analytics_check.raw_key_in_manifest",
|
|
2432
|
+
message: `Analytics manifest contains a raw key-like field: ${key}`,
|
|
2433
|
+
filePath: "siteplane.analytics.json"
|
|
2434
|
+
}));
|
|
2435
|
+
}
|
|
2436
|
+
function createSdkMissingIssue(filePath) {
|
|
2437
|
+
return {
|
|
2438
|
+
code: "analytics_check.sdk_missing",
|
|
2439
|
+
message: "Project package.json does not list @siteplane/analytics.",
|
|
2440
|
+
filePath
|
|
2441
|
+
};
|
|
2442
|
+
}
|
|
2443
|
+
async function hasAnalyticsSdk(packageJsonPath) {
|
|
2444
|
+
if (!packageJsonPath) {
|
|
2445
|
+
return false;
|
|
2446
|
+
}
|
|
2447
|
+
const packageJson = JSON.parse(await readFile3(packageJsonPath, "utf8"));
|
|
2448
|
+
return Boolean(packageJson.dependencies?.["@siteplane/analytics"] ?? packageJson.devDependencies?.["@siteplane/analytics"] ?? packageJson.peerDependencies?.["@siteplane/analytics"]);
|
|
2449
|
+
}
|
|
2450
|
+
async function readCspWarnings(filePaths) {
|
|
2451
|
+
const warnings = [];
|
|
2452
|
+
for (const filePath of filePaths) {
|
|
2453
|
+
const sourceText = await readFile3(filePath, "utf8");
|
|
2454
|
+
if (/Content-Security-Policy|connect-src/u.test(sourceText) && !/siteplane|SITEPLANE/u.test(sourceText)) {
|
|
2455
|
+
warnings.push({
|
|
2456
|
+
code: "analytics_check.csp_connect_src_risk",
|
|
2457
|
+
message: "Static CSP connect-src may need the Siteplane analytics collector origin.",
|
|
2458
|
+
filePath
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
return warnings;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// ../cli/dist/commands/analytics/init.js
|
|
2466
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
|
|
2467
|
+
import { dirname as dirname3, join } from "path";
|
|
2468
|
+
async function analyticsInitCommand(input) {
|
|
2469
|
+
const manifest = {
|
|
2470
|
+
schemaVersion: ANALYTICS_SCHEMA_VERSION,
|
|
2471
|
+
siteId: input.siteId,
|
|
2472
|
+
siteKeyRef: input.siteKeyRef,
|
|
2473
|
+
siteKeyLookup: input.siteKeyLookup,
|
|
2474
|
+
routes: [],
|
|
2475
|
+
targets: [],
|
|
2476
|
+
metadata: { siteplanePreviewSuppression: true }
|
|
2477
|
+
};
|
|
2478
|
+
const manifestPath = join(input.projectDir, "siteplane.analytics.json");
|
|
2479
|
+
const artifacts = createAnalyticsArtifacts(input.agentClient);
|
|
2480
|
+
await writeAnalyticsManifestFile(manifestPath, manifest);
|
|
2481
|
+
for (const artifact of artifacts) {
|
|
2482
|
+
await writeProjectFile(input.projectDir, artifact.path, artifact.template);
|
|
2483
|
+
}
|
|
2484
|
+
return {
|
|
2485
|
+
manifestPath,
|
|
2486
|
+
manifest,
|
|
2487
|
+
artifactPaths: artifacts.map((artifact) => artifact.path)
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
function createAnalyticsArtifacts(agentClient) {
|
|
2491
|
+
const artifacts = [
|
|
2492
|
+
{
|
|
2493
|
+
path: ".siteplane/analytics.md",
|
|
2494
|
+
template: analyticsRulesTemplate
|
|
2495
|
+
}
|
|
2496
|
+
];
|
|
2497
|
+
if (agentClient === "codex") {
|
|
2498
|
+
artifacts.push({
|
|
2499
|
+
path: ".agents/skills/siteplane-analytics/SKILL.md",
|
|
2500
|
+
template: analyticsSkillTemplate("Codex")
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
if (agentClient === "claude") {
|
|
2504
|
+
artifacts.push({
|
|
2505
|
+
path: ".claude/skills/siteplane-analytics/SKILL.md",
|
|
2506
|
+
template: analyticsSkillTemplate("Claude Code")
|
|
2507
|
+
});
|
|
2508
|
+
}
|
|
2509
|
+
if (agentClient === "cursor") {
|
|
2510
|
+
artifacts.push({
|
|
2511
|
+
path: ".cursor/rules/siteplane-analytics.mdc",
|
|
2512
|
+
template: analyticsCursorRulesTemplate
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
return artifacts;
|
|
2516
|
+
}
|
|
2517
|
+
async function writeProjectFile(projectDir, relativePath, contents) {
|
|
2518
|
+
const path = join(projectDir, relativePath);
|
|
2519
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
2520
|
+
await writeFile3(path, contents, "utf8");
|
|
2521
|
+
}
|
|
2522
|
+
var analyticsRulesTemplate = `# Siteplane Analytics Rules
|
|
2523
|
+
|
|
2524
|
+
Analytics contract version: ${ANALYTICS_CONTRACT_VERSION}
|
|
2525
|
+
Analytics contract hash: ${ANALYTICS_CONTRACT_HASH}
|
|
2526
|
+
|
|
2527
|
+
- Do not change visible layout, copy, styling, animation or business logic while adding analytics.
|
|
2528
|
+
- Use stable data-siteplane-target-id values that do not depend on visible copy.
|
|
2529
|
+
- Use public site keys only through environment references.
|
|
2530
|
+
- Do not write secret analytics keys, setup tokens or project CLI tokens into source files.
|
|
2531
|
+
- Suppress editor-preview traffic from live analytics.
|
|
2532
|
+
- Run npx siteplane analytics check, npx siteplane analytics sync and npx siteplane analytics test after setup.
|
|
2533
|
+
`;
|
|
2534
|
+
function analyticsSkillTemplate(agentName) {
|
|
2535
|
+
return `# Siteplane Analytics Skill
|
|
2536
|
+
|
|
2537
|
+
Use this skill in ${agentName} before adding Siteplane Analytics tracking.
|
|
2538
|
+
|
|
2539
|
+
${analyticsRulesTemplate}`;
|
|
2540
|
+
}
|
|
2541
|
+
var analyticsCursorRulesTemplate = `---
|
|
2542
|
+
description: Siteplane Analytics rules
|
|
2543
|
+
alwaysApply: true
|
|
2544
|
+
---
|
|
2545
|
+
|
|
2546
|
+
${analyticsRulesTemplate}`;
|
|
2547
|
+
|
|
2548
|
+
// ../cli/dist/commands/analytics/import.js
|
|
2549
|
+
import { createHash } from "crypto";
|
|
2550
|
+
|
|
2551
|
+
// ../cli/dist/commands/analytics/sync.js
|
|
2552
|
+
async function analyticsSyncCommand(input) {
|
|
2553
|
+
const redacted = validateAnalyticsManifestInput(input.manifest);
|
|
2554
|
+
const fetcher = input.fetchImpl ?? fetch;
|
|
2555
|
+
return fetcher(`${input.config.apiBaseUrl}/api/cli/analytics/manifest/sync`, {
|
|
2556
|
+
method: "POST",
|
|
2557
|
+
headers: createAnalyticsCliRequestHeaders(input.config),
|
|
2558
|
+
body: JSON.stringify({
|
|
2559
|
+
siteId: input.config.siteId,
|
|
2560
|
+
source: "cli",
|
|
2561
|
+
manifest: redacted.manifest
|
|
2562
|
+
})
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
function createAnalyticsCliRequestHeaders(config) {
|
|
2566
|
+
const headers = {
|
|
2567
|
+
authorization: `Bearer ${config.accessToken}`,
|
|
2568
|
+
"content-type": "application/json"
|
|
2569
|
+
};
|
|
2570
|
+
const bypassSecret = config.vercelAutomationBypassSecret?.trim();
|
|
2571
|
+
if (bypassSecret) {
|
|
2572
|
+
headers["x-vercel-protection-bypass"] = bypassSecret;
|
|
2573
|
+
}
|
|
2574
|
+
return headers;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
// ../cli/dist/commands/analytics/import.js
|
|
2578
|
+
async function analyticsImportCommand(input) {
|
|
2579
|
+
const payload = analyticsProviderImportSchema.parse(input.payload);
|
|
2580
|
+
const payloadHash = hashAnalyticsProviderImport(payload);
|
|
2581
|
+
if (input.apply && !input.yes) {
|
|
2582
|
+
throw new Error("Analytics import apply requires --yes.");
|
|
2583
|
+
}
|
|
2584
|
+
if (input.apply && input.confirmHash !== payloadHash) {
|
|
2585
|
+
throw new Error(`Analytics import apply requires --confirm-hash ${payloadHash} for this exact file.`);
|
|
2586
|
+
}
|
|
2587
|
+
const fetcher = input.fetchImpl ?? fetch;
|
|
2588
|
+
return fetcher(`${input.config.apiBaseUrl.replace(/\/$/u, "")}/api/cli/analytics/import`, {
|
|
2589
|
+
method: "POST",
|
|
2590
|
+
headers: createAnalyticsCliRequestHeaders(input.config),
|
|
2591
|
+
body: JSON.stringify({
|
|
2592
|
+
mode: input.apply ? "apply" : "dry_run",
|
|
2593
|
+
payloadHash,
|
|
2594
|
+
...input.apply ? { confirmHash: input.confirmHash } : {},
|
|
2595
|
+
import: payload
|
|
2596
|
+
})
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
function hashAnalyticsProviderImport(payload) {
|
|
2600
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(payload)).digest("hex")}`;
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// ../cli/dist/commands/analytics/readiness.js
|
|
2604
|
+
async function analyticsReadinessCommand(input) {
|
|
2605
|
+
const fetcher = input.fetchImpl ?? fetch;
|
|
2606
|
+
return fetcher(`${input.config.apiBaseUrl.replace(/\/$/u, "")}/api/cli/analytics/readiness`, {
|
|
2607
|
+
method: "POST",
|
|
2608
|
+
headers: createAnalyticsCliRequestHeaders(input.config),
|
|
2609
|
+
body: JSON.stringify({ siteId: input.config.siteId })
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// ../cli/dist/commands/analytics/public-key.js
|
|
2614
|
+
import { z as z22 } from "zod";
|
|
2615
|
+
var responseSchema = z22.object({
|
|
2616
|
+
ok: z22.literal(true),
|
|
2617
|
+
data: z22.object({
|
|
2618
|
+
rawPublicKey: z22.string().regex(/^pk_siteplane_[A-Za-z0-9_-]{16}_[A-Za-z0-9_-]{43}$/)
|
|
2619
|
+
})
|
|
2620
|
+
});
|
|
2621
|
+
var errorResponseSchema = z22.object({
|
|
2622
|
+
error: z22.object({
|
|
2623
|
+
code: z22.string().regex(/^[a-z][a-z0-9_.-]+$/u)
|
|
2624
|
+
})
|
|
2625
|
+
});
|
|
2626
|
+
async function analyticsPublicKeyCommand(input) {
|
|
2627
|
+
const fetcher = input.fetchImpl ?? fetch;
|
|
2628
|
+
const response = await fetcher(`${input.config.apiBaseUrl.replace(/\/$/u, "")}/api/cli/analytics/public-key`, {
|
|
2629
|
+
method: "POST",
|
|
2630
|
+
headers: createAnalyticsCliRequestHeaders(input.config),
|
|
2631
|
+
body: JSON.stringify({ siteId: input.config.siteId })
|
|
2632
|
+
});
|
|
2633
|
+
const payload = await response.json();
|
|
2634
|
+
const parsed = responseSchema.safeParse(payload);
|
|
2635
|
+
if (!response.ok || !parsed.success) {
|
|
2636
|
+
const error = errorResponseSchema.safeParse(payload);
|
|
2637
|
+
throw new Error(`${error.success ? error.data.error.code : "analytics_public_key.invalid_response"}: Unable to read the active Analytics public key.`);
|
|
2638
|
+
}
|
|
2639
|
+
return parsed.data.data.rawPublicKey;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
// ../cli/dist/commands/analytics/test.js
|
|
2643
|
+
import { randomUUID } from "crypto";
|
|
2644
|
+
async function analyticsTestCommand(input) {
|
|
2645
|
+
const fetcher = input.fetchImpl ?? fetch;
|
|
2646
|
+
const occurredAt = input.occurredAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2647
|
+
const sessionId = `cpa_s_${randomUUID().replaceAll("-", "")}`;
|
|
2648
|
+
return fetcher(`${input.config.apiBaseUrl}/api/cli/analytics/test`, {
|
|
2649
|
+
method: "POST",
|
|
2650
|
+
headers: createAnalyticsCliRequestHeaders(input.config),
|
|
2651
|
+
body: JSON.stringify({
|
|
2652
|
+
siteId: input.config.siteId,
|
|
2653
|
+
mode: "test",
|
|
2654
|
+
collectorPayload: {
|
|
2655
|
+
siteKey: input.siteKey,
|
|
2656
|
+
schemaVersion: ANALYTICS_SCHEMA_VERSION,
|
|
2657
|
+
mode: "test",
|
|
2658
|
+
events: [
|
|
2659
|
+
{
|
|
2660
|
+
eventId: input.eventId ?? `cli_test_${Date.now()}`,
|
|
2661
|
+
eventName: "page_view",
|
|
2662
|
+
eventSource: "test",
|
|
2663
|
+
sessionId,
|
|
2664
|
+
occurredAt,
|
|
2665
|
+
...input.targetId ? { targetId: input.targetId } : {},
|
|
2666
|
+
...input.pageUrl ? { pageUrl: input.pageUrl } : {},
|
|
2667
|
+
...input.pagePath ? { pagePath: input.pagePath } : {}
|
|
2668
|
+
}
|
|
2669
|
+
]
|
|
2670
|
+
}
|
|
2671
|
+
})
|
|
2672
|
+
});
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
// ../cli/dist/commands/booking/check.js
|
|
2676
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
2677
|
+
async function bookingCheckCommand(input) {
|
|
2678
|
+
const validatedManifest = await readBookingManifestFile(input.manifestPath);
|
|
2679
|
+
const sourceFiles = await readSourceFiles2(input.sourceFilePaths);
|
|
2680
|
+
const scanInput = {
|
|
2681
|
+
siteId: validatedManifest.manifest.siteId,
|
|
2682
|
+
siteKeyPrefix: validatedManifest.manifest.siteKeyPrefix,
|
|
2683
|
+
bookingPageSlug: validatedManifest.manifest.bookingPageSlug,
|
|
2684
|
+
...validatedManifest.manifest.productionOrigin ? { productionOrigin: validatedManifest.manifest.productionOrigin } : {}
|
|
2685
|
+
};
|
|
2686
|
+
const scanResult = scanBookingSourceFiles(sourceFiles, {
|
|
2687
|
+
...scanInput
|
|
2688
|
+
});
|
|
2689
|
+
const hardErrors = [
|
|
2690
|
+
...createBoundaryErrors2(validatedManifest),
|
|
2691
|
+
...scanResult.hardErrors
|
|
2692
|
+
];
|
|
2693
|
+
return {
|
|
2694
|
+
ok: hardErrors.length === 0,
|
|
2695
|
+
manifest: validatedManifest.manifest,
|
|
2696
|
+
scannedManifest: scanResult.manifest,
|
|
2697
|
+
redactedKeys: validatedManifest.redactedKeys,
|
|
2698
|
+
hardErrors,
|
|
2699
|
+
warnings: scanResult.warnings,
|
|
2700
|
+
suggestions: scanResult.suggestions,
|
|
2701
|
+
findings: scanResult.findings
|
|
2702
|
+
};
|
|
2703
|
+
}
|
|
2704
|
+
async function readSourceFiles2(filePaths) {
|
|
2705
|
+
return Promise.all(filePaths.map(async (filePath) => ({
|
|
2706
|
+
filePath,
|
|
2707
|
+
sourceText: await readFile4(filePath, "utf8")
|
|
2708
|
+
})));
|
|
2709
|
+
}
|
|
2710
|
+
function createBoundaryErrors2(validation) {
|
|
2711
|
+
return validation.redactedKeys.map((key) => ({
|
|
2712
|
+
code: "booking_check.raw_key_in_manifest",
|
|
2713
|
+
message: `Booking manifest contains a raw key-like field: ${key}`,
|
|
2714
|
+
filePath: "siteplane.booking.json"
|
|
2715
|
+
}));
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
// ../cli/dist/commands/booking/init.js
|
|
2719
|
+
import { mkdir as mkdir4, writeFile as writeFile4 } from "fs/promises";
|
|
2720
|
+
import { dirname as dirname4, join as join2 } from "path";
|
|
2721
|
+
async function bookingInitCommand(input) {
|
|
2722
|
+
const manifest = {
|
|
2723
|
+
schemaVersion: 1,
|
|
2724
|
+
siteId: input.siteId,
|
|
2725
|
+
siteKeyPrefix: input.siteKeyPrefix,
|
|
2726
|
+
bookingPageSlug: input.bookingPageSlug,
|
|
2727
|
+
...input.productionOrigin ? { productionOrigin: input.productionOrigin } : {}
|
|
2728
|
+
};
|
|
2729
|
+
const manifestPath = join2(input.projectDir, "siteplane.booking.json");
|
|
2730
|
+
const artifacts = createBookingArtifacts(input.agentClient);
|
|
2731
|
+
await writeBookingManifestFile(manifestPath, manifest);
|
|
2732
|
+
for (const artifact of artifacts) {
|
|
2733
|
+
await writeProjectFile2(input.projectDir, artifact.path, artifact.template);
|
|
2734
|
+
}
|
|
2735
|
+
return {
|
|
2736
|
+
manifestPath,
|
|
2737
|
+
manifest,
|
|
2738
|
+
artifactPaths: artifacts.map((artifact) => artifact.path)
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
function createBookingArtifacts(agentClient) {
|
|
2742
|
+
const artifacts = [
|
|
2743
|
+
{
|
|
2744
|
+
path: ".siteplane/booking.md",
|
|
2745
|
+
template: bookingRulesTemplate
|
|
2746
|
+
}
|
|
2747
|
+
];
|
|
2748
|
+
if (agentClient === "codex") {
|
|
2749
|
+
artifacts.push({
|
|
2750
|
+
path: ".agents/skills/siteplane-booking/SKILL.md",
|
|
2751
|
+
template: bookingSkillTemplate("Codex")
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
if (agentClient === "claude") {
|
|
2755
|
+
artifacts.push({
|
|
2756
|
+
path: ".claude/skills/siteplane-booking/SKILL.md",
|
|
2757
|
+
template: bookingSkillTemplate("Claude Code")
|
|
2758
|
+
});
|
|
2759
|
+
}
|
|
2760
|
+
if (agentClient === "cursor") {
|
|
2761
|
+
artifacts.push({
|
|
2762
|
+
path: ".cursor/rules/siteplane-booking.mdc",
|
|
2763
|
+
template: bookingCursorRulesTemplate
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
return artifacts;
|
|
2767
|
+
}
|
|
2768
|
+
async function writeProjectFile2(projectDir, relativePath, contents) {
|
|
2769
|
+
const path = join2(projectDir, relativePath);
|
|
2770
|
+
await mkdir4(dirname4(path), { recursive: true });
|
|
2771
|
+
await writeFile4(path, contents, "utf8");
|
|
2772
|
+
}
|
|
2773
|
+
var bookingRulesTemplate = `# Siteplane Booking Rules
|
|
2774
|
+
|
|
2775
|
+
- Do not change layout, copy, styling, animation or business logic while adding Booking.
|
|
2776
|
+
- Add only the Siteplane Booking embed script and a booking iframe or web component.
|
|
2777
|
+
- Use the configured booking page slug from siteplane.booking.json.
|
|
2778
|
+
- Register the production website origin in Siteplane before testing cross-origin embeds.
|
|
2779
|
+
- Do not write setup tokens, project CLI tokens or private keys into source files.
|
|
2780
|
+
- Run npx siteplane booking check, npx siteplane booking sync and npx siteplane booking test after setup.
|
|
2781
|
+
- booking test stays in test_mode; live booking only starts after explicit dashboard activation.
|
|
2782
|
+
`;
|
|
2783
|
+
function bookingSkillTemplate(agentName) {
|
|
2784
|
+
return `# Siteplane Booking Skill
|
|
2785
|
+
|
|
2786
|
+
Use this skill in ${agentName} before adding Siteplane Booking embeds.
|
|
2787
|
+
|
|
2788
|
+
${bookingRulesTemplate}`;
|
|
2789
|
+
}
|
|
2790
|
+
var bookingCursorRulesTemplate = `---
|
|
2791
|
+
description: Siteplane Booking rules
|
|
2792
|
+
alwaysApply: true
|
|
2793
|
+
---
|
|
2794
|
+
|
|
2795
|
+
${bookingRulesTemplate}`;
|
|
2796
|
+
|
|
2797
|
+
// ../cli/dist/commands/booking/sync.js
|
|
2798
|
+
async function bookingSyncCommand(input) {
|
|
2799
|
+
const redacted = validateBookingManifestInput(input.manifest);
|
|
2800
|
+
const fetcher = input.fetchImpl ?? fetch;
|
|
2801
|
+
return fetcher(`${input.config.apiBaseUrl}/api/cli/booking/sync`, {
|
|
2802
|
+
method: "POST",
|
|
2803
|
+
headers: createBookingCliRequestHeaders(input.config),
|
|
2804
|
+
body: JSON.stringify({
|
|
2805
|
+
siteId: input.config.siteId,
|
|
2806
|
+
source: "cli",
|
|
2807
|
+
manifest: redacted.manifest
|
|
2808
|
+
})
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
function createBookingCliRequestHeaders(config) {
|
|
2812
|
+
const headers = {
|
|
2813
|
+
authorization: `Bearer ${config.accessToken}`,
|
|
2814
|
+
"content-type": "application/json"
|
|
2815
|
+
};
|
|
2816
|
+
const bypassSecret = config.vercelAutomationBypassSecret?.trim();
|
|
2817
|
+
if (bypassSecret) {
|
|
2818
|
+
headers["x-vercel-protection-bypass"] = bypassSecret;
|
|
2819
|
+
}
|
|
2820
|
+
return headers;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// ../cli/dist/commands/booking/test.js
|
|
2824
|
+
async function bookingTestCommand(input) {
|
|
2825
|
+
const fetcher = input.fetchImpl ?? fetch;
|
|
2826
|
+
const booking = bookingCreateRequestSchema.parse(input.booking);
|
|
2827
|
+
return fetcher(`${input.config.apiBaseUrl}/api/cli/booking/test`, {
|
|
2828
|
+
method: "POST",
|
|
2829
|
+
headers: createBookingCliRequestHeaders(input.config),
|
|
2830
|
+
body: JSON.stringify({
|
|
2831
|
+
siteId: input.config.siteId,
|
|
2832
|
+
mode: "test",
|
|
2833
|
+
...input.manifest ? { manifest: input.manifest } : {},
|
|
2834
|
+
booking
|
|
2835
|
+
})
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
|
|
2839
|
+
// ../cli/dist/config/project-config.js
|
|
2840
|
+
import { access, chmod, open, readFile as readFile5, rename, rm } from "fs/promises";
|
|
2841
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2842
|
+
import { join as join3 } from "path";
|
|
2843
|
+
import { z as z23 } from "zod";
|
|
2844
|
+
var siteplaneProjectConfigSchema = z23.object({
|
|
2845
|
+
version: z23.literal(1),
|
|
2846
|
+
apiBaseUrl: z23.string().url(),
|
|
2847
|
+
siteId: z23.string().uuid(),
|
|
2848
|
+
publicSiteKeyId: z23.string().uuid(),
|
|
2849
|
+
publicSiteKey: z23.string().trim().min(1),
|
|
2850
|
+
fieldContractHash: z23.string().regex(/^sha256:[0-9a-f]{64}$/iu).optional()
|
|
2851
|
+
}).strict();
|
|
2852
|
+
async function readProjectPackageJson(projectDir) {
|
|
2853
|
+
return JSON.parse(await readFile5(join3(projectDir, "package.json"), "utf8"));
|
|
2854
|
+
}
|
|
2855
|
+
async function detectNextProject(projectDir) {
|
|
2856
|
+
const packageJson = await readProjectPackageJson(projectDir).catch(() => null);
|
|
2857
|
+
if (packageJson?.dependencies?.next || packageJson?.devDependencies?.next) {
|
|
2858
|
+
return true;
|
|
2859
|
+
}
|
|
2860
|
+
for (const fileName of [
|
|
2861
|
+
"next.config.js",
|
|
2862
|
+
"next.config.mjs",
|
|
2863
|
+
"next.config.ts"
|
|
2864
|
+
]) {
|
|
2865
|
+
try {
|
|
2866
|
+
await access(join3(projectDir, fileName));
|
|
2867
|
+
return true;
|
|
2868
|
+
} catch {
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
return false;
|
|
2872
|
+
}
|
|
2873
|
+
async function writeProjectConfig(projectDir, config) {
|
|
2874
|
+
const path = join3(projectDir, "siteplane.config.json");
|
|
2875
|
+
const parsed = siteplaneProjectConfigSchema.parse(config);
|
|
2876
|
+
await writeJsonAtomically(path, parsed, 420);
|
|
2877
|
+
return path;
|
|
2878
|
+
}
|
|
2879
|
+
async function readProjectConfig(projectDir) {
|
|
2880
|
+
return siteplaneProjectConfigSchema.parse(JSON.parse(await readFile5(join3(projectDir, "siteplane.config.json"), "utf8")));
|
|
2881
|
+
}
|
|
2882
|
+
async function writeJsonAtomically(path, value, mode) {
|
|
2883
|
+
const temporaryPath = `${path}.${randomUUID2()}.tmp`;
|
|
2884
|
+
const file = await open(temporaryPath, "wx", mode);
|
|
2885
|
+
try {
|
|
2886
|
+
await file.writeFile(`${JSON.stringify(value, null, 2)}
|
|
2887
|
+
`, "utf8");
|
|
2888
|
+
await file.sync();
|
|
2889
|
+
} catch (error) {
|
|
2890
|
+
await file.close();
|
|
2891
|
+
await rm(temporaryPath, { force: true });
|
|
2892
|
+
throw error;
|
|
2893
|
+
}
|
|
2894
|
+
await file.close();
|
|
2895
|
+
try {
|
|
2896
|
+
await rename(temporaryPath, path);
|
|
2897
|
+
await chmod(path, mode);
|
|
2898
|
+
} catch (error) {
|
|
2899
|
+
await rm(temporaryPath, { force: true });
|
|
2900
|
+
throw error;
|
|
2901
|
+
}
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
// ../cli/dist/config/credentials.js
|
|
2905
|
+
import { execFile } from "child_process";
|
|
2906
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2907
|
+
import { chmod as chmod2, mkdir as mkdir5, open as open2, readFile as readFile6, rename as rename2, rm as rm2 } from "fs/promises";
|
|
2908
|
+
import { dirname as dirname5, join as join4, relative } from "path";
|
|
2909
|
+
import { promisify } from "util";
|
|
2910
|
+
import { z as z24 } from "zod";
|
|
2911
|
+
var siteplaneCredentialsSchema = z24.object({
|
|
2912
|
+
accessToken: z24.string().trim().min(1),
|
|
2913
|
+
siteRevalidationSecret: z24.string().trim().min(1)
|
|
2914
|
+
}).strict();
|
|
2915
|
+
async function writeLocalCredentials(projectDir, credentials, options = {}) {
|
|
2916
|
+
const path = join4(projectDir, ".siteplane/credentials.json");
|
|
2917
|
+
const isTrackedFile = options.isTrackedFile ?? ((targetPath) => gitPathMatches(projectDir, targetPath, "ls-files"));
|
|
2918
|
+
const isIgnoredFile = options.isIgnoredFile ?? ((targetPath) => gitPathMatches(projectDir, targetPath, "check-ignore"));
|
|
2919
|
+
if (await isTrackedFile(path)) {
|
|
2920
|
+
throw new Error("Refusing to write Siteplane credentials into a tracked file.");
|
|
2921
|
+
}
|
|
2922
|
+
if (!await isIgnoredFile(path)) {
|
|
2923
|
+
throw new Error("Add .siteplane/credentials.json to .gitignore before running siteplane init.");
|
|
2924
|
+
}
|
|
2925
|
+
const parsed = siteplaneCredentialsSchema.parse(credentials);
|
|
2926
|
+
await mkdir5(dirname5(path), { recursive: true, mode: 448 });
|
|
2927
|
+
await writeCredentialsAtomically(path, parsed);
|
|
2928
|
+
return path;
|
|
2929
|
+
}
|
|
2930
|
+
async function readLocalCredentials(projectDir) {
|
|
2931
|
+
return siteplaneCredentialsSchema.parse(JSON.parse(await readFile6(join4(projectDir, ".siteplane/credentials.json"), "utf8")));
|
|
2932
|
+
}
|
|
2933
|
+
async function writeCredentialsAtomically(path, credentials) {
|
|
2934
|
+
const temporaryPath = `${path}.${randomUUID3()}.tmp`;
|
|
2935
|
+
const file = await open2(temporaryPath, "wx", 384);
|
|
2936
|
+
try {
|
|
2937
|
+
await file.writeFile(`${JSON.stringify(credentials, null, 2)}
|
|
2938
|
+
`, "utf8");
|
|
2939
|
+
await file.sync();
|
|
2940
|
+
} catch (error) {
|
|
2941
|
+
await file.close();
|
|
2942
|
+
await rm2(temporaryPath, { force: true });
|
|
2943
|
+
throw error;
|
|
2944
|
+
}
|
|
2945
|
+
await file.close();
|
|
2946
|
+
try {
|
|
2947
|
+
await rename2(temporaryPath, path);
|
|
2948
|
+
await chmod2(path, 384);
|
|
2949
|
+
} catch (error) {
|
|
2950
|
+
await rm2(temporaryPath, { force: true });
|
|
2951
|
+
throw error;
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
var execFileAsync = promisify(execFile);
|
|
2955
|
+
async function gitPathMatches(projectDir, path, command) {
|
|
2956
|
+
const args = command === "check-ignore" ? ["check-ignore", "--quiet", relative(projectDir, path)] : ["ls-files", "--error-unmatch", relative(projectDir, path)];
|
|
2957
|
+
try {
|
|
2958
|
+
await execFileAsync("git", args, { cwd: projectDir });
|
|
2959
|
+
return true;
|
|
2960
|
+
} catch {
|
|
2961
|
+
return false;
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
// ../cli/dist/commands/init.js
|
|
2966
|
+
import { join as join5 } from "path";
|
|
2967
|
+
import { z as z25 } from "zod";
|
|
2968
|
+
async function initCommand(input) {
|
|
2969
|
+
if (!input.setupToken.trim()) {
|
|
2970
|
+
throw new Error("A Siteplane setup token is required.");
|
|
2971
|
+
}
|
|
2972
|
+
if (!await detectNextProject(input.projectDir)) {
|
|
2973
|
+
throw new Error("Siteplane init currently supports Next.js projects.");
|
|
2974
|
+
}
|
|
2975
|
+
const bootstrap = await bootstrapProjectConnection(input);
|
|
2976
|
+
const paths = bootstrap.credentialPurpose === "site_setup" ? await persistSiteSetupBootstrap(input, bootstrap) : await persistFeatureBootstrap(input, bootstrap);
|
|
2977
|
+
await acknowledgeBootstrapResponse(input);
|
|
2978
|
+
return {
|
|
2979
|
+
...paths,
|
|
2980
|
+
nextSteps: [
|
|
2981
|
+
"npx siteplane setup context",
|
|
2982
|
+
"Instrument the site and create EditorDefinitionV1.",
|
|
2983
|
+
"npx siteplane setup apply --definition /tmp/siteplane-editor.json"
|
|
2984
|
+
]
|
|
2985
|
+
};
|
|
2986
|
+
}
|
|
2987
|
+
var siteSetupBootstrapDataSchema = z25.object({
|
|
2988
|
+
credentialPurpose: z25.literal("site_setup"),
|
|
2989
|
+
apiBaseUrl: z25.string().url(),
|
|
2990
|
+
siteId: z25.string().uuid(),
|
|
2991
|
+
publicSiteKeyId: z25.string().uuid(),
|
|
2992
|
+
publicSiteKey: z25.string().trim().min(1),
|
|
2993
|
+
accessToken: z25.string().trim().min(1),
|
|
2994
|
+
siteRevalidationSecret: z25.string().regex(/^[A-Za-z0-9_-]{43}$/u)
|
|
2995
|
+
}).strict();
|
|
2996
|
+
var featureBootstrapDataSchema = z25.object({
|
|
2997
|
+
credentialPurpose: z25.enum(["analytics", "booking"]),
|
|
2998
|
+
apiBaseUrl: z25.string().url(),
|
|
2999
|
+
siteId: z25.string().uuid(),
|
|
3000
|
+
accessToken: z25.string().trim().min(1)
|
|
3001
|
+
}).strict();
|
|
3002
|
+
var bootstrapResponseSchema = z25.object({
|
|
3003
|
+
ok: z25.literal(true),
|
|
3004
|
+
data: z25.discriminatedUnion("credentialPurpose", [
|
|
3005
|
+
siteSetupBootstrapDataSchema,
|
|
3006
|
+
featureBootstrapDataSchema
|
|
3007
|
+
])
|
|
3008
|
+
}).strict();
|
|
3009
|
+
async function bootstrapProjectConnection(input) {
|
|
3010
|
+
const fetcher = input.fetcher ?? fetch;
|
|
3011
|
+
const response = await fetcher(`${input.apiBaseUrl.replace(/\/$/, "")}/api/cli/setup/bootstrap`, {
|
|
3012
|
+
method: "POST",
|
|
3013
|
+
headers: createBootstrapHeaders(input),
|
|
3014
|
+
body: JSON.stringify({
|
|
3015
|
+
action: "bootstrap",
|
|
3016
|
+
agentClient: input.agentClient ?? "codex"
|
|
3017
|
+
})
|
|
3018
|
+
});
|
|
3019
|
+
const payload = await response.json();
|
|
3020
|
+
const parsed = bootstrapResponseSchema.safeParse(payload);
|
|
3021
|
+
if (!response.ok || !parsed.success) {
|
|
3022
|
+
throw new Error(readErrorMessage(payload) ?? "Siteplane init failed.");
|
|
3023
|
+
}
|
|
3024
|
+
return parsed.data.data;
|
|
3025
|
+
}
|
|
3026
|
+
async function acknowledgeBootstrapResponse(input) {
|
|
3027
|
+
const fetcher = input.fetcher ?? fetch;
|
|
3028
|
+
const response = await fetcher(`${input.apiBaseUrl.replace(/\/$/, "")}/api/cli/setup/bootstrap`, {
|
|
3029
|
+
method: "POST",
|
|
3030
|
+
headers: createBootstrapHeaders(input),
|
|
3031
|
+
body: JSON.stringify({ action: "acknowledge" })
|
|
3032
|
+
});
|
|
3033
|
+
if (!response.ok) {
|
|
3034
|
+
throw new Error("Siteplane setup acknowledgement failed.");
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
async function persistSiteSetupBootstrap(input, bootstrap) {
|
|
3038
|
+
const credentialsPath = await writeLocalCredentials(input.projectDir, {
|
|
3039
|
+
accessToken: bootstrap.accessToken,
|
|
3040
|
+
siteRevalidationSecret: bootstrap.siteRevalidationSecret
|
|
3041
|
+
}, {
|
|
3042
|
+
...input.isIgnoredFile ? { isIgnoredFile: input.isIgnoredFile } : {},
|
|
3043
|
+
...input.isTrackedFile ? { isTrackedFile: input.isTrackedFile } : {}
|
|
3044
|
+
});
|
|
3045
|
+
const configPath = await writeProjectConfig(input.projectDir, {
|
|
3046
|
+
version: 1,
|
|
3047
|
+
apiBaseUrl: bootstrap.apiBaseUrl,
|
|
3048
|
+
siteId: bootstrap.siteId,
|
|
3049
|
+
publicSiteKeyId: bootstrap.publicSiteKeyId,
|
|
3050
|
+
publicSiteKey: bootstrap.publicSiteKey
|
|
3051
|
+
});
|
|
3052
|
+
const [credentials, config] = await Promise.all([
|
|
3053
|
+
readLocalCredentials(input.projectDir),
|
|
3054
|
+
readProjectConfig(input.projectDir)
|
|
3055
|
+
]);
|
|
3056
|
+
if (credentials.accessToken !== bootstrap.accessToken || credentials.siteRevalidationSecret !== bootstrap.siteRevalidationSecret || config.apiBaseUrl !== bootstrap.apiBaseUrl || config.siteId !== bootstrap.siteId || config.publicSiteKeyId !== bootstrap.publicSiteKeyId || config.publicSiteKey !== bootstrap.publicSiteKey) {
|
|
3057
|
+
throw new Error("Siteplane setup files could not be verified.");
|
|
3058
|
+
}
|
|
3059
|
+
return { configPath, credentialsPath };
|
|
3060
|
+
}
|
|
3061
|
+
async function persistFeatureBootstrap(input, bootstrap) {
|
|
3062
|
+
const [config, existingCredentials] = await Promise.all([
|
|
3063
|
+
readProjectConfig(input.projectDir),
|
|
3064
|
+
readLocalCredentials(input.projectDir)
|
|
3065
|
+
]);
|
|
3066
|
+
if (config.siteId !== bootstrap.siteId) {
|
|
3067
|
+
throw new Error("The setup token belongs to a different Siteplane site.");
|
|
3068
|
+
}
|
|
3069
|
+
const credentialsPath = await writeLocalCredentials(input.projectDir, {
|
|
3070
|
+
accessToken: bootstrap.accessToken,
|
|
3071
|
+
siteRevalidationSecret: existingCredentials.siteRevalidationSecret
|
|
3072
|
+
}, {
|
|
3073
|
+
...input.isIgnoredFile ? { isIgnoredFile: input.isIgnoredFile } : {},
|
|
3074
|
+
...input.isTrackedFile ? { isTrackedFile: input.isTrackedFile } : {}
|
|
3075
|
+
});
|
|
3076
|
+
const persistedCredentials = await readLocalCredentials(input.projectDir);
|
|
3077
|
+
if (persistedCredentials.accessToken !== bootstrap.accessToken || persistedCredentials.siteRevalidationSecret !== existingCredentials.siteRevalidationSecret) {
|
|
3078
|
+
throw new Error("Siteplane credentials could not be verified.");
|
|
3079
|
+
}
|
|
3080
|
+
return {
|
|
3081
|
+
configPath: join5(input.projectDir, "siteplane.config.json"),
|
|
3082
|
+
credentialsPath
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
function createBootstrapHeaders(input) {
|
|
3086
|
+
return new Headers({
|
|
3087
|
+
authorization: `Bearer ${input.setupToken}`,
|
|
3088
|
+
"content-type": "application/json",
|
|
3089
|
+
...input.vercelAutomationBypassSecret?.trim() ? {
|
|
3090
|
+
"x-vercel-protection-bypass": input.vercelAutomationBypassSecret.trim()
|
|
3091
|
+
} : {}
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
function readErrorMessage(payload) {
|
|
3095
|
+
if (payload && typeof payload === "object" && "error" in payload && payload.error && typeof payload.error === "object" && "message" in payload.error && typeof payload.error.message === "string") {
|
|
3096
|
+
const code = "code" in payload.error && typeof payload.error.code === "string" ? payload.error.code : null;
|
|
3097
|
+
return code ? `${code}: ${payload.error.message}` : payload.error.message;
|
|
3098
|
+
}
|
|
3099
|
+
return null;
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
// ../cli/dist/commands/scan.js
|
|
3103
|
+
import { readdir } from "fs/promises";
|
|
3104
|
+
import { extname, join as join6 } from "path";
|
|
3105
|
+
var defaultScanRoots = ["app", "src", "pages", "components", "lib"];
|
|
3106
|
+
var excludedDirectories = /* @__PURE__ */ new Set([
|
|
3107
|
+
".git",
|
|
3108
|
+
".next",
|
|
3109
|
+
".turbo",
|
|
3110
|
+
".vercel",
|
|
3111
|
+
"build",
|
|
3112
|
+
"coverage",
|
|
3113
|
+
"dist",
|
|
3114
|
+
"node_modules"
|
|
3115
|
+
]);
|
|
3116
|
+
var sourceExtensions = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
3117
|
+
async function resolveScanFilePaths(filePaths, options = {}) {
|
|
3118
|
+
if (filePaths.length > 0) {
|
|
3119
|
+
return filePaths;
|
|
3120
|
+
}
|
|
3121
|
+
const projectDir = options.projectDir ?? process.cwd();
|
|
3122
|
+
const discovered = await Promise.all(defaultScanRoots.map((root) => collectSourceFiles(join6(projectDir, root))));
|
|
3123
|
+
return [...new Set(discovered.flat())].sort();
|
|
3124
|
+
}
|
|
3125
|
+
async function collectSourceFiles(directory) {
|
|
3126
|
+
const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
|
|
3127
|
+
const files = [];
|
|
3128
|
+
for (const entry of entries) {
|
|
3129
|
+
const entryPath = join6(directory, entry.name);
|
|
3130
|
+
if (entry.isDirectory()) {
|
|
3131
|
+
if (!excludedDirectories.has(entry.name)) {
|
|
3132
|
+
files.push(...await collectSourceFiles(entryPath));
|
|
3133
|
+
}
|
|
3134
|
+
continue;
|
|
3135
|
+
}
|
|
3136
|
+
if (entry.isFile() && sourceExtensions.has(extname(entry.name))) {
|
|
3137
|
+
files.push(entryPath);
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
return files;
|
|
3141
|
+
}
|
|
3142
|
+
|
|
3143
|
+
// ../cli/dist/commands/site-setup.js
|
|
3144
|
+
import { execFile as execFile2 } from "child_process";
|
|
3145
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
3146
|
+
import { promisify as promisify2 } from "util";
|
|
3147
|
+
import { z as z26 } from "zod";
|
|
3148
|
+
|
|
3149
|
+
// ../cli/dist/scan/next-source-scan.js
|
|
3150
|
+
import ts2 from "typescript";
|
|
3151
|
+
import { isAbsolute, relative as relative2 } from "path";
|
|
3152
|
+
var eventPreviewHandlerMissingMessage = "Event preview fields must consume preview values with useSiteplanePreviewText from @siteplane/runtime-next/client or a siteplane:preview-value-applied listener.";
|
|
3153
|
+
var valueCallUnusedMessage = "Siteplane value calls must feed rendered content. Assign or await the returned value and render that value instead of ignoring the call.";
|
|
3154
|
+
var legacyEditableComponentPrefix = "Editable";
|
|
3155
|
+
var editableComponentNames = new Set(["Image", "Text", "LongText", "Link"].map((name) => `${legacyEditableComponentPrefix}${name}`));
|
|
3156
|
+
var functionTypeByName = {
|
|
3157
|
+
text: "text",
|
|
3158
|
+
longText: "longText",
|
|
3159
|
+
link: "link",
|
|
3160
|
+
image: "image"
|
|
3161
|
+
};
|
|
3162
|
+
function scanNextSourceFilesToSiteFieldContract(files, options) {
|
|
3163
|
+
const scan = scanSourceFacts(files);
|
|
3164
|
+
const fields = [];
|
|
3165
|
+
for (const field of uniqueFields(scan.fields)) {
|
|
3166
|
+
const routeKey = field.technicalRouteKey ?? field.routeHint;
|
|
3167
|
+
if (!routeKey) {
|
|
3168
|
+
scan.hardErrors.push({
|
|
3169
|
+
code: "field_scan.route_key_missing",
|
|
3170
|
+
message: `Field ${field.fieldId} has no canonical route key.`,
|
|
3171
|
+
fieldId: field.fieldId,
|
|
3172
|
+
filePath: field.sourceFilePath ?? "unknown"
|
|
3173
|
+
});
|
|
3174
|
+
continue;
|
|
3175
|
+
}
|
|
3176
|
+
const source = {
|
|
3177
|
+
kind: "function",
|
|
3178
|
+
filePath: toRepositoryRelativePosixPath(field.sourceFilePath ?? "unknown", options.projectRoot)
|
|
3179
|
+
};
|
|
3180
|
+
assignIfDefined(source, "exportName", field.sourceExportName);
|
|
3181
|
+
assignIfDefined(source, "sourcePath", field.sourcePath);
|
|
3182
|
+
assignIfDefined(source, "editTarget", field.editTarget);
|
|
3183
|
+
const parsed = siteFieldContractFieldSchema.safeParse({
|
|
3184
|
+
fieldId: field.fieldId,
|
|
3185
|
+
fieldType: field.fieldType,
|
|
3186
|
+
fallback: field.fallback,
|
|
3187
|
+
routeKey,
|
|
3188
|
+
source,
|
|
3189
|
+
...field.hasDomBinding ? {
|
|
3190
|
+
dom: {
|
|
3191
|
+
attribute: "data-siteplane-field-id",
|
|
3192
|
+
previewStrategy: field.previewStrategy ?? "dom"
|
|
3193
|
+
}
|
|
3194
|
+
} : {}
|
|
3195
|
+
});
|
|
3196
|
+
if (!parsed.success) {
|
|
3197
|
+
scan.hardErrors.push({
|
|
3198
|
+
code: "field_scan.invalid_site_field_contract",
|
|
3199
|
+
message: parsed.error.issues[0]?.message ?? "Invalid field contract.",
|
|
3200
|
+
fieldId: field.fieldId,
|
|
3201
|
+
filePath: field.sourceFilePath ?? "unknown"
|
|
3202
|
+
});
|
|
3203
|
+
continue;
|
|
3204
|
+
}
|
|
3205
|
+
fields.push(parsed.data);
|
|
3206
|
+
}
|
|
3207
|
+
return {
|
|
3208
|
+
contract: siteFieldContractV1Schema.parse({
|
|
3209
|
+
version: 1,
|
|
3210
|
+
fieldContractVersion,
|
|
3211
|
+
scannerContractVersion,
|
|
3212
|
+
fields
|
|
3213
|
+
}),
|
|
3214
|
+
hardErrors: scan.hardErrors,
|
|
3215
|
+
warnings: scan.warnings,
|
|
3216
|
+
suggestions: scan.suggestions
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
3219
|
+
function scanSourceFacts(files) {
|
|
3220
|
+
const fields = [];
|
|
3221
|
+
const hardErrors = [];
|
|
3222
|
+
const warnings = [];
|
|
3223
|
+
const suggestions = [];
|
|
3224
|
+
const eventPreviewAttrs = [];
|
|
3225
|
+
const eventPreviewComponentPropConsumers = [];
|
|
3226
|
+
const eventPreviewComponentPropUsages = [];
|
|
3227
|
+
const eventPreviewConsumerFieldIds = /* @__PURE__ */ new Set();
|
|
3228
|
+
let hasGenericEventPreviewConsumer = false;
|
|
3229
|
+
for (const file of files) {
|
|
3230
|
+
const facts = scanSourceFile2(file, fields, hardErrors);
|
|
3231
|
+
eventPreviewAttrs.push(...facts.eventPreviewAttrs);
|
|
3232
|
+
eventPreviewComponentPropConsumers.push(...facts.eventPreviewComponentPropConsumers);
|
|
3233
|
+
eventPreviewComponentPropUsages.push(...facts.eventPreviewComponentPropUsages);
|
|
3234
|
+
facts.eventPreviewConsumerFieldIds.forEach((fieldId) => {
|
|
3235
|
+
eventPreviewConsumerFieldIds.add(fieldId);
|
|
3236
|
+
});
|
|
3237
|
+
hasGenericEventPreviewConsumer ||= facts.hasGenericEventPreviewConsumer;
|
|
3238
|
+
}
|
|
3239
|
+
for (const usage of eventPreviewComponentPropUsages) {
|
|
3240
|
+
if (eventPreviewComponentPropConsumers.some((consumer) => consumer.componentName === usage.componentName && consumer.propName === usage.propName)) {
|
|
3241
|
+
eventPreviewConsumerFieldIds.add(usage.fieldId);
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
markMissingEventPreviewHandlerErrors(eventPreviewAttrs, eventPreviewConsumerFieldIds, hasGenericEventPreviewConsumer, hardErrors);
|
|
3245
|
+
validateScannedFields(fields, hardErrors, warnings, suggestions);
|
|
3246
|
+
return {
|
|
3247
|
+
fields,
|
|
3248
|
+
hardErrors,
|
|
3249
|
+
warnings,
|
|
3250
|
+
suggestions
|
|
3251
|
+
};
|
|
3252
|
+
}
|
|
3253
|
+
function uniqueFields(fields) {
|
|
3254
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3255
|
+
const unique = [];
|
|
3256
|
+
for (const field of fields) {
|
|
3257
|
+
if (seen.has(field.fieldId)) {
|
|
3258
|
+
continue;
|
|
3259
|
+
}
|
|
3260
|
+
seen.add(field.fieldId);
|
|
3261
|
+
unique.push(field);
|
|
3262
|
+
}
|
|
3263
|
+
return unique;
|
|
3264
|
+
}
|
|
3265
|
+
function scanSourceFile2(file, fields, hardErrors) {
|
|
3266
|
+
const fieldsInFile = [];
|
|
3267
|
+
const attrFieldIds = /* @__PURE__ */ new Set();
|
|
3268
|
+
const attrRouteRefs = /* @__PURE__ */ new Set();
|
|
3269
|
+
const eventPreviewAttrs = [];
|
|
3270
|
+
const eventPreviewAttrVariables = /* @__PURE__ */ new Map();
|
|
3271
|
+
const eventPreviewComponentPropConsumers = [];
|
|
3272
|
+
const eventPreviewComponentPropUsages = [];
|
|
3273
|
+
const eventPreviewConsumerFieldIds = /* @__PURE__ */ new Set();
|
|
3274
|
+
let hasGenericEventPreviewConsumer = false;
|
|
3275
|
+
const sourceFile = ts2.createSourceFile(file.filePath, file.sourceText, ts2.ScriptTarget.Latest, true, ts2.ScriptKind.TSX);
|
|
3276
|
+
function visit(node, currentComponentName) {
|
|
3277
|
+
const nextComponentName = getFunctionComponentName(node) ?? currentComponentName;
|
|
3278
|
+
if (ts2.isJsxSelfClosingElement(node) || ts2.isJsxOpeningElement(node)) {
|
|
3279
|
+
const componentName = getJsxTagName2(node.tagName);
|
|
3280
|
+
if (editableComponentNames.has(componentName)) {
|
|
3281
|
+
hardErrors.push({
|
|
3282
|
+
code: "field_scan.value_first_required",
|
|
3283
|
+
message: "Siteplane MVP uses value-first APIs. Replace this field with siteplane.route(...) or a field value call.",
|
|
3284
|
+
filePath: file.filePath
|
|
3285
|
+
});
|
|
3286
|
+
}
|
|
3287
|
+
collectEventPreviewComponentPropUsages(node, eventPreviewAttrVariables, eventPreviewComponentPropUsages, file);
|
|
3288
|
+
}
|
|
3289
|
+
if (ts2.isCallExpression(node)) {
|
|
3290
|
+
collectAttrsCall(node, attrFieldIds, attrRouteRefs, eventPreviewAttrs, eventPreviewAttrVariables, file);
|
|
3291
|
+
collectUnusedValueCallError(node, hardErrors, file);
|
|
3292
|
+
const extractedFields = extractFunctionFields(node, file);
|
|
3293
|
+
for (const field of extractedFields) {
|
|
3294
|
+
fieldsInFile.push(field);
|
|
3295
|
+
fields.push(field);
|
|
3296
|
+
}
|
|
3297
|
+
const previewHookFieldId = getPreviewHookFieldId(node, eventPreviewAttrVariables);
|
|
3298
|
+
if (previewHookFieldId) {
|
|
3299
|
+
eventPreviewConsumerFieldIds.add(previewHookFieldId);
|
|
3300
|
+
} else if (nextComponentName) {
|
|
3301
|
+
const previewHookPropName = getPreviewHookPropName(node, eventPreviewAttrVariables);
|
|
3302
|
+
if (previewHookPropName) {
|
|
3303
|
+
eventPreviewComponentPropConsumers.push({
|
|
3304
|
+
componentName: nextComponentName,
|
|
3305
|
+
propName: previewHookPropName
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
if (isGenericEventPreviewConsumerReference(node)) {
|
|
3311
|
+
hasGenericEventPreviewConsumer = true;
|
|
3312
|
+
}
|
|
3313
|
+
ts2.forEachChild(node, (child) => visit(child, nextComponentName));
|
|
3314
|
+
}
|
|
3315
|
+
visit(sourceFile);
|
|
3316
|
+
markMissingAttrsWarnings(fieldsInFile, attrFieldIds, attrRouteRefs);
|
|
3317
|
+
return {
|
|
3318
|
+
eventPreviewAttrs,
|
|
3319
|
+
eventPreviewComponentPropConsumers,
|
|
3320
|
+
eventPreviewComponentPropUsages,
|
|
3321
|
+
eventPreviewConsumerFieldIds: [...eventPreviewConsumerFieldIds],
|
|
3322
|
+
hasGenericEventPreviewConsumer
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
function collectUnusedValueCallError(node, hardErrors, file) {
|
|
3326
|
+
const valueFunctionName = getSiteplaneValueFunctionName(node);
|
|
3327
|
+
if (!valueFunctionName || !isIgnoredValueCall(node)) {
|
|
3328
|
+
return;
|
|
3329
|
+
}
|
|
3330
|
+
const fieldId = valueFunctionName === "route" ? void 0 : stringValue2(staticExpressionValue2(node.arguments[0]));
|
|
3331
|
+
hardErrors.push({
|
|
3332
|
+
code: "field_scan.value_call_unused",
|
|
3333
|
+
message: valueCallUnusedMessage,
|
|
3334
|
+
...fieldId ? { fieldId } : {},
|
|
3335
|
+
filePath: file.filePath
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
function getSiteplaneValueFunctionName(node) {
|
|
3339
|
+
if (!ts2.isPropertyAccessExpression(node.expression)) {
|
|
3340
|
+
return void 0;
|
|
3341
|
+
}
|
|
3342
|
+
if (node.expression.expression.getText() !== "siteplane") {
|
|
3343
|
+
return void 0;
|
|
3344
|
+
}
|
|
3345
|
+
const functionName = node.expression.name.text;
|
|
3346
|
+
if (functionName === "route" || functionTypeByName[functionName]) {
|
|
3347
|
+
return functionName;
|
|
3348
|
+
}
|
|
3349
|
+
return void 0;
|
|
3350
|
+
}
|
|
3351
|
+
function isIgnoredValueCall(node) {
|
|
3352
|
+
if (ts2.isVoidExpression(node.parent)) {
|
|
3353
|
+
return true;
|
|
3354
|
+
}
|
|
3355
|
+
if (ts2.isExpressionStatement(node.parent)) {
|
|
3356
|
+
return true;
|
|
3357
|
+
}
|
|
3358
|
+
return ts2.isAwaitExpression(node.parent) && ts2.isExpressionStatement(node.parent.parent);
|
|
3359
|
+
}
|
|
3360
|
+
function extractFunctionFields(node, file) {
|
|
3361
|
+
if (!ts2.isPropertyAccessExpression(node.expression)) {
|
|
3362
|
+
return [];
|
|
3363
|
+
}
|
|
3364
|
+
if (node.expression.expression.getText() !== "siteplane") {
|
|
3365
|
+
return [];
|
|
3366
|
+
}
|
|
3367
|
+
const functionName = node.expression.name.text;
|
|
3368
|
+
if (functionName === "route") {
|
|
3369
|
+
return extractRouteFields(node, file);
|
|
3370
|
+
}
|
|
3371
|
+
const fieldType = functionTypeByName[functionName];
|
|
3372
|
+
if (!fieldType) {
|
|
3373
|
+
return [];
|
|
3374
|
+
}
|
|
3375
|
+
const fieldId = staticExpressionValue2(node.arguments[0]);
|
|
3376
|
+
if (typeof fieldId !== "string") {
|
|
3377
|
+
return [];
|
|
3378
|
+
}
|
|
3379
|
+
const input = {
|
|
3380
|
+
fieldId,
|
|
3381
|
+
fieldType,
|
|
3382
|
+
fallback: staticExpressionValue2(node.arguments[1]),
|
|
3383
|
+
source: "function",
|
|
3384
|
+
sourceFilePath: file.filePath,
|
|
3385
|
+
sourceExportName: functionName,
|
|
3386
|
+
editTarget: fieldId
|
|
3387
|
+
};
|
|
3388
|
+
const routeHint = inferRouteHint(file.filePath);
|
|
3389
|
+
const options = staticExpressionValue2(node.arguments[2]);
|
|
3390
|
+
const fieldOptions = options && typeof options === "object" && !Array.isArray(options) ? options : {};
|
|
3391
|
+
assignIfDefined(input, "routeHint", routeHint);
|
|
3392
|
+
assignIfDefined(input, "technicalRouteKey", stringValue2(fieldOptions.routeKey) ?? routeHint);
|
|
3393
|
+
assignIfDefined(input, "previewStrategy", previewStrategyValue(fieldOptions.previewStrategy));
|
|
3394
|
+
assignIfDefined(input, "label", stringValue2(fieldOptions.label));
|
|
3395
|
+
assignIfDefined(input, "description", stringValue2(fieldOptions.description));
|
|
3396
|
+
assignIfDefined(input, "required", booleanValue(fieldOptions.required));
|
|
3397
|
+
assignIfDefined(input, "maxLength", numberValue(fieldOptions.maxLength));
|
|
3398
|
+
return [createField(input)];
|
|
3399
|
+
}
|
|
3400
|
+
function extractRouteFields(node, file) {
|
|
3401
|
+
const routeId = staticExpressionValue2(node.arguments[0]);
|
|
3402
|
+
const fallbacks = staticExpressionValue2(node.arguments[1]);
|
|
3403
|
+
const options = staticExpressionValue2(node.arguments[2]);
|
|
3404
|
+
if (typeof routeId !== "string" || !fallbacks || typeof fallbacks !== "object" || Array.isArray(fallbacks)) {
|
|
3405
|
+
return [];
|
|
3406
|
+
}
|
|
3407
|
+
const routeVariableName = getAssignedVariableName(node);
|
|
3408
|
+
const fieldsConfig = options && typeof options === "object" && !Array.isArray(options) ? options.fields : void 0;
|
|
3409
|
+
const routeHint = inferRouteHint(file.filePath);
|
|
3410
|
+
const fields = [];
|
|
3411
|
+
for (const [key, fallback] of Object.entries(fallbacks)) {
|
|
3412
|
+
const config = fieldsConfig && typeof fieldsConfig === "object" && !Array.isArray(fieldsConfig) ? fieldsConfig[key] : void 0;
|
|
3413
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
3414
|
+
continue;
|
|
3415
|
+
}
|
|
3416
|
+
const fieldConfig = config;
|
|
3417
|
+
const fieldId = stringValue2(fieldConfig.id);
|
|
3418
|
+
const fieldType = fieldTypeValue(fieldConfig.type);
|
|
3419
|
+
if (!fieldId || !fieldType) {
|
|
3420
|
+
continue;
|
|
3421
|
+
}
|
|
3422
|
+
const input = {
|
|
3423
|
+
fieldId,
|
|
3424
|
+
fieldType,
|
|
3425
|
+
fallback,
|
|
3426
|
+
source: "function",
|
|
3427
|
+
sourceFilePath: file.filePath,
|
|
3428
|
+
sourceExportName: "route",
|
|
3429
|
+
routeId,
|
|
3430
|
+
sourcePath: `$.${key}`,
|
|
3431
|
+
editTarget: fieldId,
|
|
3432
|
+
routeFieldKey: key
|
|
3433
|
+
};
|
|
3434
|
+
assignIfDefined(input, "routeVariableName", routeVariableName);
|
|
3435
|
+
assignIfDefined(input, "routeHint", routeHint);
|
|
3436
|
+
assignIfDefined(input, "technicalRouteKey", stringValue2(fieldConfig.routeKey) ?? routeHint);
|
|
3437
|
+
assignIfDefined(input, "previewStrategy", previewStrategyValue(fieldConfig.previewStrategy));
|
|
3438
|
+
assignIfDefined(input, "label", stringValue2(fieldConfig.label));
|
|
3439
|
+
assignIfDefined(input, "description", stringValue2(fieldConfig.description));
|
|
3440
|
+
assignIfDefined(input, "required", booleanValue(fieldConfig.required));
|
|
3441
|
+
assignIfDefined(input, "maxLength", numberValue(fieldConfig.maxLength));
|
|
3442
|
+
fields.push(createField(input));
|
|
3443
|
+
}
|
|
3444
|
+
return fields;
|
|
3445
|
+
}
|
|
3446
|
+
function createField(input) {
|
|
3447
|
+
const field = {
|
|
3448
|
+
fieldId: input.fieldId,
|
|
3449
|
+
fieldType: input.fieldType,
|
|
3450
|
+
fallback: input.fallback,
|
|
3451
|
+
source: input.source,
|
|
3452
|
+
sourceFilePath: input.sourceFilePath,
|
|
3453
|
+
warnings: [],
|
|
3454
|
+
suggestions: []
|
|
3455
|
+
};
|
|
3456
|
+
assignIfDefined(field, "label", input.label);
|
|
3457
|
+
assignIfDefined(field, "description", input.description);
|
|
3458
|
+
assignIfDefined(field, "required", input.required);
|
|
3459
|
+
assignIfDefined(field, "maxLength", input.maxLength);
|
|
3460
|
+
assignIfDefined(field, "sourceExportName", input.sourceExportName);
|
|
3461
|
+
assignIfDefined(field, "componentHint", input.componentHint);
|
|
3462
|
+
assignIfDefined(field, "routeHint", input.routeHint);
|
|
3463
|
+
assignIfDefined(field, "routeId", input.routeId);
|
|
3464
|
+
assignIfDefined(field, "sourcePath", input.sourcePath);
|
|
3465
|
+
assignIfDefined(field, "editTarget", input.editTarget);
|
|
3466
|
+
assignMetadata(field, {
|
|
3467
|
+
canonicalFallbackHash: createFallbackHash(input.fieldType, input.fallback)
|
|
3468
|
+
});
|
|
3469
|
+
assignIfDefined(field, "routeVariableName", input.routeVariableName);
|
|
3470
|
+
assignIfDefined(field, "routeFieldKey", input.routeFieldKey);
|
|
3471
|
+
assignIfDefined(field, "technicalRouteKey", input.technicalRouteKey);
|
|
3472
|
+
assignIfDefined(field, "previewStrategy", input.previewStrategy);
|
|
3473
|
+
if (field.fieldType === "text" && field.maxLength === void 0) {
|
|
3474
|
+
field.warnings.push("field_scan.text_max_length_missing");
|
|
3475
|
+
}
|
|
3476
|
+
if (isPositionalFieldReference(field.fieldId) || isPositionalFieldReference(field.sourcePath)) {
|
|
3477
|
+
field.warnings.push("field_scan.positional_repeating_id");
|
|
3478
|
+
}
|
|
3479
|
+
return field;
|
|
3480
|
+
}
|
|
3481
|
+
function assignMetadata(field, metadata) {
|
|
3482
|
+
const entries = Object.entries(metadata).filter(([, value]) => value !== void 0);
|
|
3483
|
+
if (entries.length === 0) {
|
|
3484
|
+
return;
|
|
3485
|
+
}
|
|
3486
|
+
field.metadata = {
|
|
3487
|
+
...field.metadata ?? {},
|
|
3488
|
+
...Object.fromEntries(entries)
|
|
3489
|
+
};
|
|
3490
|
+
}
|
|
3491
|
+
function createFallbackHash(fieldType, fallback) {
|
|
3492
|
+
try {
|
|
3493
|
+
return createCanonicalFallbackHash({ fieldType, fallbackValue: fallback });
|
|
3494
|
+
} catch {
|
|
3495
|
+
return void 0;
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
function collectAttrsCall(node, attrFieldIds, attrRouteRefs, eventPreviewAttrs, eventPreviewAttrVariables, file) {
|
|
3499
|
+
if (!ts2.isPropertyAccessExpression(node.expression)) {
|
|
3500
|
+
return;
|
|
3501
|
+
}
|
|
3502
|
+
if (node.expression.expression.getText() !== "siteplane" || node.expression.name.text !== "attrs") {
|
|
3503
|
+
return;
|
|
3504
|
+
}
|
|
3505
|
+
const firstArg = node.arguments[0];
|
|
3506
|
+
const secondArg = staticExpressionValue2(node.arguments[1]);
|
|
3507
|
+
const directFieldId = staticExpressionValue2(firstArg);
|
|
3508
|
+
if (typeof directFieldId === "string") {
|
|
3509
|
+
attrFieldIds.add(directFieldId);
|
|
3510
|
+
if (hasEventPreviewStrategy(secondArg)) {
|
|
3511
|
+
eventPreviewAttrs.push({
|
|
3512
|
+
fieldId: directFieldId,
|
|
3513
|
+
filePath: file.filePath
|
|
3514
|
+
});
|
|
3515
|
+
const variableName = getAssignedVariableName(node);
|
|
3516
|
+
if (variableName) {
|
|
3517
|
+
eventPreviewAttrVariables.set(variableName, directFieldId);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
return;
|
|
3521
|
+
}
|
|
3522
|
+
if (firstArg && ts2.isIdentifier(firstArg) && typeof secondArg === "string") {
|
|
3523
|
+
attrRouteRefs.add(`${firstArg.text}:${secondArg}`);
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
function markMissingEventPreviewHandlerErrors(eventPreviewAttrs, eventPreviewConsumerFieldIds, hasGenericEventPreviewConsumer, hardErrors) {
|
|
3527
|
+
if (hasGenericEventPreviewConsumer) {
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
for (const attr of uniqueEventPreviewAttrs(eventPreviewAttrs)) {
|
|
3531
|
+
if (eventPreviewConsumerFieldIds.has(attr.fieldId)) {
|
|
3532
|
+
continue;
|
|
3533
|
+
}
|
|
3534
|
+
hardErrors.push({
|
|
3535
|
+
code: "field_scan.event_preview_handler_missing",
|
|
3536
|
+
message: eventPreviewHandlerMissingMessage,
|
|
3537
|
+
fieldId: attr.fieldId,
|
|
3538
|
+
filePath: attr.filePath
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
function uniqueEventPreviewAttrs(eventPreviewAttrs) {
|
|
3543
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3544
|
+
const unique = [];
|
|
3545
|
+
for (const attr of eventPreviewAttrs) {
|
|
3546
|
+
const key = `${attr.filePath}:${attr.fieldId}`;
|
|
3547
|
+
if (seen.has(key)) {
|
|
3548
|
+
continue;
|
|
3549
|
+
}
|
|
3550
|
+
seen.add(key);
|
|
3551
|
+
unique.push(attr);
|
|
3552
|
+
}
|
|
3553
|
+
return unique;
|
|
3554
|
+
}
|
|
3555
|
+
function hasEventPreviewStrategy(options) {
|
|
3556
|
+
return !!options && typeof options === "object" && !Array.isArray(options) && options.previewStrategy === "event";
|
|
3557
|
+
}
|
|
3558
|
+
function isGenericEventPreviewConsumerReference(node) {
|
|
3559
|
+
return (ts2.isStringLiteral(node) || ts2.isNoSubstitutionTemplateLiteral(node)) && node.text === "siteplane:preview-value-applied";
|
|
3560
|
+
}
|
|
3561
|
+
function getPreviewHookFieldId(node, eventPreviewAttrVariables) {
|
|
3562
|
+
const firstArg = getUseSiteplanePreviewTextFirstArg(node);
|
|
3563
|
+
if (!firstArg) {
|
|
3564
|
+
return void 0;
|
|
3565
|
+
}
|
|
3566
|
+
const directFieldId = staticExpressionValue2(firstArg);
|
|
3567
|
+
if (typeof directFieldId === "string") {
|
|
3568
|
+
return directFieldId;
|
|
3569
|
+
}
|
|
3570
|
+
if (ts2.isIdentifier(firstArg)) {
|
|
3571
|
+
return eventPreviewAttrVariables.get(firstArg.text);
|
|
3572
|
+
}
|
|
3573
|
+
return void 0;
|
|
3574
|
+
}
|
|
3575
|
+
function getPreviewHookPropName(node, eventPreviewAttrVariables) {
|
|
3576
|
+
const firstArg = getUseSiteplanePreviewTextFirstArg(node);
|
|
3577
|
+
if (!firstArg) {
|
|
3578
|
+
return void 0;
|
|
3579
|
+
}
|
|
3580
|
+
if (ts2.isIdentifier(firstArg) && !eventPreviewAttrVariables.has(firstArg.text)) {
|
|
3581
|
+
return firstArg.text;
|
|
3582
|
+
}
|
|
3583
|
+
if (ts2.isPropertyAccessExpression(firstArg) && ts2.isIdentifier(firstArg.expression)) {
|
|
3584
|
+
return firstArg.name.text;
|
|
3585
|
+
}
|
|
3586
|
+
return void 0;
|
|
3587
|
+
}
|
|
3588
|
+
function getUseSiteplanePreviewTextFirstArg(node) {
|
|
3589
|
+
if (ts2.isIdentifier(node.expression) && node.expression.text === "useSiteplanePreviewText") {
|
|
3590
|
+
return node.arguments[0];
|
|
3591
|
+
}
|
|
3592
|
+
return void 0;
|
|
3593
|
+
}
|
|
3594
|
+
function markMissingAttrsWarnings(fields, attrFieldIds, attrRouteRefs) {
|
|
3595
|
+
for (const field of fields) {
|
|
3596
|
+
const hasDirectAttrs = attrFieldIds.has(field.fieldId);
|
|
3597
|
+
const hasRouteAttrs = field.routeVariableName && field.routeFieldKey ? attrRouteRefs.has(`${field.routeVariableName}:${field.routeFieldKey}`) : false;
|
|
3598
|
+
field.hasDomBinding = Boolean(hasDirectAttrs || hasRouteAttrs);
|
|
3599
|
+
if (!field.hasDomBinding && (field.fieldType === "text" || field.fieldType === "longText")) {
|
|
3600
|
+
field.warnings.push("field_scan.attrs_missing");
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
function validateScannedFields(fields, hardErrors, warnings, suggestions) {
|
|
3605
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3606
|
+
for (const field of fields) {
|
|
3607
|
+
if (!isFieldId(field.fieldId)) {
|
|
3608
|
+
hardErrors.push({
|
|
3609
|
+
code: "field_scan.invalid_field_id",
|
|
3610
|
+
message: `Invalid field id: ${field.fieldId}`,
|
|
3611
|
+
fieldId: field.fieldId,
|
|
3612
|
+
filePath: field.sourceFilePath ?? "unknown"
|
|
3613
|
+
});
|
|
3614
|
+
}
|
|
3615
|
+
const existing = seen.get(field.fieldId);
|
|
3616
|
+
if (existing) {
|
|
3617
|
+
hardErrors.push({
|
|
3618
|
+
code: "field_scan.duplicate_id",
|
|
3619
|
+
message: `Duplicate field id: ${field.fieldId}`,
|
|
3620
|
+
fieldId: field.fieldId,
|
|
3621
|
+
filePath: field.sourceFilePath ?? "unknown"
|
|
3622
|
+
});
|
|
3623
|
+
if (existing.fieldType !== field.fieldType) {
|
|
3624
|
+
hardErrors.push({
|
|
3625
|
+
code: "field_scan.type_conflict",
|
|
3626
|
+
message: `Field id ${field.fieldId} is used with multiple types.`,
|
|
3627
|
+
fieldId: field.fieldId,
|
|
3628
|
+
filePath: field.sourceFilePath ?? "unknown"
|
|
3629
|
+
});
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
seen.set(field.fieldId, field);
|
|
3633
|
+
for (const warning of field.warnings) {
|
|
3634
|
+
warnings.push({
|
|
3635
|
+
code: warning,
|
|
3636
|
+
message: getScanIssueMessage(warning),
|
|
3637
|
+
fieldId: field.fieldId,
|
|
3638
|
+
filePath: field.sourceFilePath ?? "unknown"
|
|
3639
|
+
});
|
|
3640
|
+
}
|
|
3641
|
+
for (const suggestion of field.suggestions) {
|
|
3642
|
+
suggestions.push({
|
|
3643
|
+
code: suggestion,
|
|
3644
|
+
message: getScanIssueMessage(suggestion),
|
|
3645
|
+
fieldId: field.fieldId,
|
|
3646
|
+
filePath: field.sourceFilePath ?? "unknown"
|
|
3647
|
+
});
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
function getScanIssueMessage(code) {
|
|
3652
|
+
if (code === "field_scan.attrs_missing") {
|
|
3653
|
+
return "Field renders a value but has no DOM discovery attrs. Add siteplane.attrs(...) to the nearest stable element so the visual editor can select it.";
|
|
3654
|
+
}
|
|
3655
|
+
if (code === "field_scan.positional_repeating_id") {
|
|
3656
|
+
return "Repeating content needs stable Siteplane field IDs for the MVP. Replace positional array/index field ids with stable item ids such as services.haircut.price.";
|
|
3657
|
+
}
|
|
3658
|
+
if (code === "field_scan.animation_wrapper") {
|
|
3659
|
+
return 'Put siteplane.attrs(...) on the stable wrapper and make the animation respect data-siteplane-editor="true" or useSiteplaneEditorMode().';
|
|
3660
|
+
}
|
|
3661
|
+
if (code === "field_scan.event_preview_handler_missing") {
|
|
3662
|
+
return eventPreviewHandlerMissingMessage;
|
|
3663
|
+
}
|
|
3664
|
+
return code;
|
|
3665
|
+
}
|
|
3666
|
+
function staticExpressionValue2(expression) {
|
|
3667
|
+
if (!expression) {
|
|
3668
|
+
return void 0;
|
|
3669
|
+
}
|
|
3670
|
+
if (ts2.isStringLiteral(expression) || ts2.isNoSubstitutionTemplateLiteral(expression)) {
|
|
3671
|
+
return expression.text;
|
|
3672
|
+
}
|
|
3673
|
+
if (ts2.isNumericLiteral(expression)) {
|
|
3674
|
+
return Number(expression.text);
|
|
3675
|
+
}
|
|
3676
|
+
if (expression.kind === ts2.SyntaxKind.TrueKeyword) {
|
|
3677
|
+
return true;
|
|
3678
|
+
}
|
|
3679
|
+
if (expression.kind === ts2.SyntaxKind.FalseKeyword) {
|
|
3680
|
+
return false;
|
|
3681
|
+
}
|
|
3682
|
+
if (ts2.isObjectLiteralExpression(expression)) {
|
|
3683
|
+
return readObjectLiteral(expression);
|
|
3684
|
+
}
|
|
3685
|
+
return void 0;
|
|
3686
|
+
}
|
|
3687
|
+
function readObjectLiteral(expression) {
|
|
3688
|
+
const value = {};
|
|
3689
|
+
for (const property of expression.properties) {
|
|
3690
|
+
if (!ts2.isPropertyAssignment(property)) {
|
|
3691
|
+
continue;
|
|
3692
|
+
}
|
|
3693
|
+
const name = getPropertyName(property.name);
|
|
3694
|
+
if (!name) {
|
|
3695
|
+
continue;
|
|
3696
|
+
}
|
|
3697
|
+
value[name] = staticExpressionValue2(property.initializer);
|
|
3698
|
+
}
|
|
3699
|
+
return value;
|
|
3700
|
+
}
|
|
3701
|
+
function getPropertyName(name) {
|
|
3702
|
+
if (ts2.isIdentifier(name) || ts2.isStringLiteral(name)) {
|
|
3703
|
+
return name.text;
|
|
3704
|
+
}
|
|
3705
|
+
return null;
|
|
3706
|
+
}
|
|
3707
|
+
function collectEventPreviewComponentPropUsages(node, eventPreviewAttrVariables, eventPreviewComponentPropUsages, file) {
|
|
3708
|
+
const componentName = getJsxTagName2(node.tagName);
|
|
3709
|
+
if (!isComponentName(componentName)) {
|
|
3710
|
+
return;
|
|
3711
|
+
}
|
|
3712
|
+
for (const property of node.attributes.properties) {
|
|
3713
|
+
if (!ts2.isJsxAttribute(property)) {
|
|
3714
|
+
continue;
|
|
3715
|
+
}
|
|
3716
|
+
const propName = getJsxAttributeName(property.name);
|
|
3717
|
+
const fieldId = getJsxFieldIdValue(property.initializer, eventPreviewAttrVariables);
|
|
3718
|
+
if (!propName || !fieldId || !isFieldId(fieldId)) {
|
|
3719
|
+
continue;
|
|
3720
|
+
}
|
|
3721
|
+
eventPreviewComponentPropUsages.push({
|
|
3722
|
+
componentName,
|
|
3723
|
+
fieldId,
|
|
3724
|
+
filePath: file.filePath,
|
|
3725
|
+
propName
|
|
3726
|
+
});
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
function getJsxAttributeName(name) {
|
|
3730
|
+
if (ts2.isIdentifier(name)) {
|
|
3731
|
+
return name.text;
|
|
3732
|
+
}
|
|
3733
|
+
return void 0;
|
|
3734
|
+
}
|
|
3735
|
+
function getJsxFieldIdValue(initializer, eventPreviewAttrVariables) {
|
|
3736
|
+
if (!initializer) {
|
|
3737
|
+
return void 0;
|
|
3738
|
+
}
|
|
3739
|
+
if (ts2.isStringLiteral(initializer)) {
|
|
3740
|
+
return initializer.text;
|
|
3741
|
+
}
|
|
3742
|
+
if (ts2.isJsxExpression(initializer) && initializer.expression && ts2.isIdentifier(initializer.expression)) {
|
|
3743
|
+
return eventPreviewAttrVariables.get(initializer.expression.text);
|
|
3744
|
+
}
|
|
3745
|
+
return void 0;
|
|
3746
|
+
}
|
|
3747
|
+
function getFunctionComponentName(node) {
|
|
3748
|
+
if (ts2.isFunctionDeclaration(node) && node.name && isComponentName(node.name.text)) {
|
|
3749
|
+
return node.name.text;
|
|
3750
|
+
}
|
|
3751
|
+
if (ts2.isVariableDeclaration(node) && ts2.isIdentifier(node.name) && isComponentName(node.name.text) && node.initializer && (ts2.isArrowFunction(node.initializer) || ts2.isFunctionExpression(node.initializer))) {
|
|
3752
|
+
return node.name.text;
|
|
3753
|
+
}
|
|
3754
|
+
return void 0;
|
|
3755
|
+
}
|
|
3756
|
+
function isComponentName(name) {
|
|
3757
|
+
return /^[A-Z]/.test(name);
|
|
3758
|
+
}
|
|
3759
|
+
function getAssignedVariableName(node) {
|
|
3760
|
+
let current = node;
|
|
3761
|
+
while (ts2.isAwaitExpression(current.parent)) {
|
|
3762
|
+
current = current.parent;
|
|
3763
|
+
}
|
|
3764
|
+
if (ts2.isVariableDeclaration(current.parent) && ts2.isIdentifier(current.parent.name)) {
|
|
3765
|
+
return current.parent.name.text;
|
|
3766
|
+
}
|
|
3767
|
+
return void 0;
|
|
3768
|
+
}
|
|
3769
|
+
function getJsxTagName2(tagName) {
|
|
3770
|
+
return tagName.getText();
|
|
3771
|
+
}
|
|
3772
|
+
function stringValue2(value) {
|
|
3773
|
+
return typeof value === "string" ? value : void 0;
|
|
3774
|
+
}
|
|
3775
|
+
function fieldTypeValue(value) {
|
|
3776
|
+
if (value === "text" || value === "longText" || value === "image" || value === "link") {
|
|
3777
|
+
return value;
|
|
3778
|
+
}
|
|
3779
|
+
return void 0;
|
|
3780
|
+
}
|
|
3781
|
+
function previewStrategyValue(value) {
|
|
3782
|
+
return value === "dom" || value === "event" ? value : void 0;
|
|
3783
|
+
}
|
|
3784
|
+
function numberValue(value) {
|
|
3785
|
+
return typeof value === "number" ? value : void 0;
|
|
3786
|
+
}
|
|
3787
|
+
function booleanValue(value) {
|
|
3788
|
+
return typeof value === "boolean" ? value : void 0;
|
|
3789
|
+
}
|
|
3790
|
+
function assignIfDefined(object, key, value) {
|
|
3791
|
+
if (value !== void 0) {
|
|
3792
|
+
object[key] = value;
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
function inferRouteHint(filePath) {
|
|
3796
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
3797
|
+
const appIndex = normalized.indexOf("app/");
|
|
3798
|
+
if (appIndex === -1 || !normalized.endsWith("page.tsx")) {
|
|
3799
|
+
return void 0;
|
|
3800
|
+
}
|
|
3801
|
+
const route = normalized.slice(appIndex + 4, -"page.tsx".length).split("/").filter((segment) => segment && !segment.startsWith("(")).join("/");
|
|
3802
|
+
return route ? `/${route}` : "/";
|
|
3803
|
+
}
|
|
3804
|
+
function toRepositoryRelativePosixPath(filePath, projectRoot) {
|
|
3805
|
+
const path = isAbsolute(filePath) ? relative2(projectRoot, filePath) : filePath;
|
|
3806
|
+
return path.replaceAll("\\", "/");
|
|
3807
|
+
}
|
|
3808
|
+
function isPositionalFieldReference(value) {
|
|
3809
|
+
return value ? /\[\d+\]|\.\d+(?:\.|$)/.test(value) : false;
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
// ../cli/dist/commands/site-setup.js
|
|
3813
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
3814
|
+
var safeErrorSchema = z26.object({
|
|
3815
|
+
code: z26.string().trim().min(1),
|
|
3816
|
+
message: z26.string().trim().min(1)
|
|
3817
|
+
}).passthrough();
|
|
3818
|
+
var errorResponseSchema2 = z26.object({ ok: z26.literal(false), error: safeErrorSchema }).passthrough();
|
|
3819
|
+
var contextDataSchema = z26.object({
|
|
3820
|
+
site: z26.object({ siteId: z26.string().uuid(), websiteUrl: z26.string().url() }),
|
|
3821
|
+
run: z26.object({
|
|
3822
|
+
runId: z26.string().uuid(),
|
|
3823
|
+
mode: z26.enum(["initial", "update"]),
|
|
3824
|
+
status: z26.enum([
|
|
3825
|
+
"waiting_for_agent",
|
|
3826
|
+
"working",
|
|
3827
|
+
"action_required",
|
|
3828
|
+
"ready_for_review"
|
|
3829
|
+
]),
|
|
3830
|
+
fieldContractHash: z26.string().nullable(),
|
|
3831
|
+
deploymentOrigin: z26.string().nullable(),
|
|
3832
|
+
deploymentRevision: z26.string().nullable(),
|
|
3833
|
+
errorCode: z26.string().nullable(),
|
|
3834
|
+
errorMessage: z26.string().nullable()
|
|
3835
|
+
}),
|
|
3836
|
+
allowedTasks: z26.array(z26.string()),
|
|
3837
|
+
requiredEnvironmentNames: z26.array(z26.string())
|
|
3838
|
+
}).strict();
|
|
3839
|
+
var contextResponseSchema = z26.object({ ok: z26.literal(true), data: contextDataSchema }).strict();
|
|
3840
|
+
var applyDataSchema = z26.object({
|
|
3841
|
+
status: z26.enum(["applied", "ready_for_review"]),
|
|
3842
|
+
runId: z26.string().uuid().optional(),
|
|
3843
|
+
contractVersionId: z26.string().uuid().optional(),
|
|
3844
|
+
fieldContractHash: z26.string().regex(/^sha256:[0-9a-f]{64}$/u),
|
|
3845
|
+
editorDefinitionHash: z26.string().regex(/^sha256:[0-9a-f]{64}$/u).optional(),
|
|
3846
|
+
fieldCount: z26.number().int().nonnegative().optional(),
|
|
3847
|
+
routeCount: z26.number().int().nonnegative().optional()
|
|
3848
|
+
}).strict();
|
|
3849
|
+
var applyResponseSchema = z26.object({ ok: z26.literal(true), data: applyDataSchema }).strict();
|
|
3850
|
+
var verifyDataSchema = z26.object({
|
|
3851
|
+
status: z26.literal("ready_for_review"),
|
|
3852
|
+
runId: z26.string().uuid().optional(),
|
|
3853
|
+
deploymentOrigin: z26.string().url().optional(),
|
|
3854
|
+
deploymentRevision: z26.string().optional()
|
|
3855
|
+
}).strict();
|
|
3856
|
+
var verifyResponseSchema = z26.object({ ok: z26.literal(true), data: verifyDataSchema }).strict();
|
|
3857
|
+
async function siteSetupContextCommand(input) {
|
|
3858
|
+
const response = await postSiteSetup(input, "context", {});
|
|
3859
|
+
const parsed = contextResponseSchema.safeParse(response.payload);
|
|
3860
|
+
if (response.ok && parsed.success) {
|
|
3861
|
+
return { status: "ok", ...parsed.data.data };
|
|
3862
|
+
}
|
|
3863
|
+
return actionRequired(response.payload, "Siteplane setup context is unavailable.");
|
|
3864
|
+
}
|
|
3865
|
+
async function siteSetupApplyCommand(input) {
|
|
3866
|
+
const editorDefinition = editorDefinitionV1Schema.parse(input.editorDefinition);
|
|
3867
|
+
const resolveSourceFiles = input.resolveSourceFiles ?? resolveScanFilePaths;
|
|
3868
|
+
const readSourceFile = input.readSourceFile ?? ((path) => readFile7(path, "utf8"));
|
|
3869
|
+
const scanSourceFiles = input.scanSourceFiles ?? scanNextSourceFilesToSiteFieldContract;
|
|
3870
|
+
const paths = await resolveSourceFiles([], { projectDir: input.projectDir });
|
|
3871
|
+
const files = await Promise.all(paths.map(async (filePath) => ({
|
|
3872
|
+
filePath,
|
|
3873
|
+
sourceText: await readSourceFile(filePath)
|
|
3874
|
+
})));
|
|
3875
|
+
const scan = scanSourceFiles(files, { projectRoot: input.projectDir });
|
|
3876
|
+
if (scan.hardErrors.length > 0) {
|
|
3877
|
+
return {
|
|
3878
|
+
status: "action_required",
|
|
3879
|
+
code: "field_contract_scan_failed",
|
|
3880
|
+
message: scan.hardErrors.map((issue) => issue.message).join("\n")
|
|
3881
|
+
};
|
|
3882
|
+
}
|
|
3883
|
+
const payload = siteSetupApplyInputSchema.parse({
|
|
3884
|
+
fieldContract: scan.contract,
|
|
3885
|
+
editorDefinition
|
|
3886
|
+
});
|
|
3887
|
+
const response = await postSiteSetup(input, "apply", payload);
|
|
3888
|
+
const parsed = applyResponseSchema.safeParse(response.payload);
|
|
3889
|
+
if (!response.ok || !parsed.success) {
|
|
3890
|
+
return actionRequired(response.payload, "Siteplane setup apply failed.");
|
|
3891
|
+
}
|
|
3892
|
+
const localFieldContractHash = createSiteFieldContractHash(scan.contract);
|
|
3893
|
+
if (parsed.data.data.fieldContractHash !== localFieldContractHash) {
|
|
3894
|
+
return {
|
|
3895
|
+
status: "action_required",
|
|
3896
|
+
code: "field_contract_hash_mismatch",
|
|
3897
|
+
message: "The applied field contract hash does not match the final local source scan."
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
3900
|
+
const config = await readProjectConfig(input.projectDir);
|
|
3901
|
+
await writeProjectConfig(input.projectDir, {
|
|
3902
|
+
...config,
|
|
3903
|
+
fieldContractHash: parsed.data.data.fieldContractHash
|
|
3904
|
+
});
|
|
3905
|
+
return parsed.data.data;
|
|
3906
|
+
}
|
|
3907
|
+
async function siteSetupVerifyCommand(input) {
|
|
3908
|
+
const deploymentRevision = await (input.readRevision ?? readGitRevision)(input.projectDir);
|
|
3909
|
+
const response = await postSiteSetup(input, "verify", {
|
|
3910
|
+
deploymentUrl: input.deploymentUrl,
|
|
3911
|
+
deploymentRevision,
|
|
3912
|
+
...input.wait ? { wait: true } : {}
|
|
3913
|
+
});
|
|
3914
|
+
const parsed = verifyResponseSchema.safeParse(response.payload);
|
|
3915
|
+
if (response.ok && parsed.success) {
|
|
3916
|
+
return parsed.data.data;
|
|
3917
|
+
}
|
|
3918
|
+
return actionRequired(response.payload, "Siteplane deployment verification failed.");
|
|
3919
|
+
}
|
|
3920
|
+
async function postSiteSetup(input, operation, body) {
|
|
3921
|
+
const [config, credentials] = await Promise.all([
|
|
3922
|
+
readProjectConfig(input.projectDir),
|
|
3923
|
+
readLocalCredentials(input.projectDir)
|
|
3924
|
+
]);
|
|
3925
|
+
const response = await (input.fetcher ?? fetch)(`${config.apiBaseUrl.replace(/\/$/u, "")}/api/cli/setup/${operation}`, {
|
|
3926
|
+
method: "POST",
|
|
3927
|
+
headers: {
|
|
3928
|
+
authorization: `Bearer ${credentials.accessToken}`,
|
|
3929
|
+
"content-type": "application/json",
|
|
3930
|
+
...input.vercelAutomationBypassSecret?.trim() ? {
|
|
3931
|
+
"x-vercel-protection-bypass": input.vercelAutomationBypassSecret.trim()
|
|
3932
|
+
} : {}
|
|
3933
|
+
},
|
|
3934
|
+
body: JSON.stringify(body)
|
|
3935
|
+
});
|
|
3936
|
+
const payload = await response.json().catch(() => null);
|
|
3937
|
+
return { ok: response.ok, payload };
|
|
3938
|
+
}
|
|
3939
|
+
function actionRequired(payload, fallbackMessage) {
|
|
3940
|
+
const parsed = errorResponseSchema2.safeParse(payload);
|
|
3941
|
+
return parsed.success ? {
|
|
3942
|
+
status: "action_required",
|
|
3943
|
+
code: parsed.data.error.code,
|
|
3944
|
+
message: parsed.data.error.message
|
|
3945
|
+
} : {
|
|
3946
|
+
status: "action_required",
|
|
3947
|
+
code: "invalid_server_response",
|
|
3948
|
+
message: fallbackMessage
|
|
3949
|
+
};
|
|
3950
|
+
}
|
|
3951
|
+
async function readGitRevision(projectDir) {
|
|
3952
|
+
const { stdout } = await execFileAsync2("git", ["rev-parse", "HEAD"], {
|
|
3953
|
+
cwd: projectDir,
|
|
3954
|
+
encoding: "utf8"
|
|
3955
|
+
});
|
|
3956
|
+
const revision = stdout.trim();
|
|
3957
|
+
if (!revision) {
|
|
3958
|
+
throw new Error("A committed Git revision is required before setup verify.");
|
|
3959
|
+
}
|
|
3960
|
+
return revision;
|
|
3961
|
+
}
|
|
3962
|
+
|
|
3963
|
+
// src/bin/run-siteplane.ts
|
|
3964
|
+
async function runSiteplaneCli(args, dependencies = createDefaultDependencies()) {
|
|
3965
|
+
const [command, ...commandArgs] = args;
|
|
3966
|
+
if (command === "init") {
|
|
3967
|
+
const setupToken = readOption(commandArgs, "--setup-token");
|
|
3968
|
+
const apiBaseUrl = readOption(commandArgs, "--api-base-url") ?? dependencies.env.SITEPLANE_API_BASE_URL ?? "https://app.siteplane.io";
|
|
3969
|
+
if (!setupToken) {
|
|
3970
|
+
throw new Error("Missing --setup-token.");
|
|
3971
|
+
}
|
|
3972
|
+
const result = await dependencies.commands.initCommand({
|
|
3973
|
+
projectDir: dependencies.cwd(),
|
|
3974
|
+
setupToken,
|
|
3975
|
+
apiBaseUrl,
|
|
3976
|
+
...readOptionalVercelAutomationBypassSecret(dependencies.env)
|
|
3977
|
+
});
|
|
3978
|
+
dependencies.stdout(`Wrote ${result.configPath}`);
|
|
3979
|
+
dependencies.stdout(`Wrote ${result.credentialsPath}`);
|
|
3980
|
+
dependencies.stdout("Next steps:");
|
|
3981
|
+
for (const nextStep of result.nextSteps) {
|
|
3982
|
+
dependencies.stdout(`- ${nextStep}`);
|
|
3983
|
+
}
|
|
3984
|
+
return { exitCode: 0 };
|
|
3985
|
+
}
|
|
3986
|
+
if (command === "booking") {
|
|
3987
|
+
return runBookingCommand(commandArgs, dependencies);
|
|
3988
|
+
}
|
|
3989
|
+
if (command === "analytics") {
|
|
3990
|
+
return runAnalyticsCommand(commandArgs, dependencies);
|
|
3991
|
+
}
|
|
3992
|
+
if (command === "setup") {
|
|
3993
|
+
return runSetupCommand(commandArgs, dependencies);
|
|
3994
|
+
}
|
|
3995
|
+
if (command === "--version" || command === "-v") {
|
|
3996
|
+
dependencies.stdout(await dependencies.readPackageVersion());
|
|
3997
|
+
return { exitCode: 0 };
|
|
3998
|
+
}
|
|
3999
|
+
if (command === "--help" || command === "-h") {
|
|
4000
|
+
dependencies.stdout("Usage: siteplane <init|setup|analytics|booking>");
|
|
4001
|
+
return { exitCode: 0 };
|
|
4002
|
+
}
|
|
4003
|
+
dependencies.stdout("Usage: siteplane <init|setup|analytics|booking>");
|
|
4004
|
+
return { exitCode: 1 };
|
|
4005
|
+
}
|
|
4006
|
+
async function runAnalyticsCommand(args, dependencies) {
|
|
4007
|
+
const [subcommand, ...subcommandArgs] = args;
|
|
4008
|
+
const projectDir = dependencies.cwd();
|
|
4009
|
+
if (!subcommand || subcommand === "--help" || subcommand === "-h") {
|
|
4010
|
+
dependencies.stdout(
|
|
4011
|
+
"Usage: siteplane analytics <init|check|sync|test|import>"
|
|
4012
|
+
);
|
|
4013
|
+
return { exitCode: subcommand ? 0 : 1 };
|
|
4014
|
+
}
|
|
4015
|
+
const [projectConfig, credentials] = await Promise.all([
|
|
4016
|
+
dependencies.commands.readProjectConfig(projectDir),
|
|
4017
|
+
dependencies.commands.readLocalCredentials(projectDir)
|
|
4018
|
+
]);
|
|
4019
|
+
const config = {
|
|
4020
|
+
apiBaseUrl: projectConfig.apiBaseUrl,
|
|
4021
|
+
accessToken: credentials.accessToken,
|
|
4022
|
+
siteId: projectConfig.siteId,
|
|
4023
|
+
...readOptionalVercelAutomationBypassSecret(dependencies.env)
|
|
4024
|
+
};
|
|
4025
|
+
const manifestPath = join7(projectDir, "siteplane.analytics.json");
|
|
4026
|
+
if (subcommand === "init") {
|
|
4027
|
+
const rawPublicKey = await dependencies.commands.analyticsPublicKeyCommand({
|
|
4028
|
+
config
|
|
4029
|
+
});
|
|
4030
|
+
const siteKeyLookup = getAnalyticsKeyLookup(rawPublicKey);
|
|
4031
|
+
if (!siteKeyLookup) {
|
|
4032
|
+
throw new Error("Project config contains an invalid Analytics public site key.");
|
|
4033
|
+
}
|
|
4034
|
+
const agentClient = readAnalyticsAgentClient(subcommandArgs);
|
|
4035
|
+
const result = await dependencies.commands.analyticsInitCommand({
|
|
4036
|
+
projectDir,
|
|
4037
|
+
siteId: projectConfig.siteId,
|
|
4038
|
+
siteKeyRef: "env:NEXT_PUBLIC_SITEPLANE_ANALYTICS_SITE_KEY",
|
|
4039
|
+
siteKeyLookup,
|
|
4040
|
+
...agentClient ? { agentClient } : {}
|
|
4041
|
+
});
|
|
4042
|
+
dependencies.stdout(`Wrote ${result.manifestPath}`);
|
|
4043
|
+
for (const artifactPath of result.artifactPaths) {
|
|
4044
|
+
dependencies.stdout(`Wrote ${artifactPath}`);
|
|
4045
|
+
}
|
|
4046
|
+
return { exitCode: 0 };
|
|
4047
|
+
}
|
|
4048
|
+
if (subcommand === "check" || subcommand === "sync") {
|
|
4049
|
+
const sourceFilePaths = await dependencies.commands.resolveScanFilePaths([], {
|
|
4050
|
+
projectDir
|
|
4051
|
+
});
|
|
4052
|
+
const artifactFilePaths = [join7(projectDir, ".siteplane/analytics.md")];
|
|
4053
|
+
const cspFilePaths = await existingPaths([
|
|
4054
|
+
join7(projectDir, "next.config.js"),
|
|
4055
|
+
join7(projectDir, "next.config.mjs"),
|
|
4056
|
+
join7(projectDir, "next.config.ts"),
|
|
4057
|
+
join7(projectDir, "middleware.ts"),
|
|
4058
|
+
join7(projectDir, "src/middleware.ts")
|
|
4059
|
+
]);
|
|
4060
|
+
const check = await dependencies.commands.analyticsCheckCommand({
|
|
4061
|
+
manifestPath,
|
|
4062
|
+
sourceFilePaths,
|
|
4063
|
+
packageJsonPath: join7(projectDir, "package.json"),
|
|
4064
|
+
cspFilePaths,
|
|
4065
|
+
artifactFilePaths
|
|
4066
|
+
});
|
|
4067
|
+
if (subcommand === "check") {
|
|
4068
|
+
const readinessResponse = await dependencies.commands.analyticsReadinessCommand({
|
|
4069
|
+
config
|
|
4070
|
+
});
|
|
4071
|
+
const readiness = await readResponseJson(readinessResponse);
|
|
4072
|
+
dependencies.stdout(JSON.stringify({ ...check, readiness }, null, 2));
|
|
4073
|
+
return { exitCode: check.ok && readinessResponse.ok ? 0 : 1 };
|
|
4074
|
+
}
|
|
4075
|
+
if (!check.ok) {
|
|
4076
|
+
dependencies.stdout(JSON.stringify(check, null, 2));
|
|
4077
|
+
return { exitCode: 1 };
|
|
4078
|
+
}
|
|
4079
|
+
return printAnalyticsResponseWithReadiness(
|
|
4080
|
+
await dependencies.commands.analyticsSyncCommand({
|
|
4081
|
+
config,
|
|
4082
|
+
manifest: check.scannedManifest
|
|
4083
|
+
}),
|
|
4084
|
+
config,
|
|
4085
|
+
dependencies
|
|
4086
|
+
);
|
|
4087
|
+
}
|
|
4088
|
+
if (subcommand === "test") {
|
|
4089
|
+
const rawPublicKey = await dependencies.commands.analyticsPublicKeyCommand({
|
|
4090
|
+
config
|
|
4091
|
+
});
|
|
4092
|
+
return printAnalyticsResponseWithReadiness(
|
|
4093
|
+
await dependencies.commands.analyticsTestCommand({
|
|
4094
|
+
config,
|
|
4095
|
+
siteKey: rawPublicKey,
|
|
4096
|
+
...readOption(subcommandArgs, "--page-url") ? { pageUrl: readOption(subcommandArgs, "--page-url") } : {}
|
|
4097
|
+
}),
|
|
4098
|
+
config,
|
|
4099
|
+
dependencies
|
|
4100
|
+
);
|
|
4101
|
+
}
|
|
4102
|
+
if (subcommand === "import") {
|
|
4103
|
+
const payload = await readPayloadFile(subcommandArgs, dependencies);
|
|
4104
|
+
return printResponse(
|
|
4105
|
+
await dependencies.commands.analyticsImportCommand({
|
|
4106
|
+
config,
|
|
4107
|
+
payload,
|
|
4108
|
+
apply: subcommandArgs.includes("--apply"),
|
|
4109
|
+
...readOption(subcommandArgs, "--confirm-hash") ? { confirmHash: readOption(subcommandArgs, "--confirm-hash") } : {},
|
|
4110
|
+
yes: subcommandArgs.includes("--yes")
|
|
4111
|
+
}),
|
|
4112
|
+
dependencies
|
|
4113
|
+
);
|
|
4114
|
+
}
|
|
4115
|
+
dependencies.stdout(
|
|
4116
|
+
"Usage: siteplane analytics <init|check|sync|test|import>"
|
|
4117
|
+
);
|
|
4118
|
+
return { exitCode: 1 };
|
|
4119
|
+
}
|
|
4120
|
+
async function runSetupCommand(args, dependencies) {
|
|
4121
|
+
const [area, ...areaArgs] = args;
|
|
4122
|
+
const projectDir = dependencies.cwd();
|
|
4123
|
+
const connectionOptions = readOptionalVercelAutomationBypassSecret(
|
|
4124
|
+
dependencies.env
|
|
4125
|
+
);
|
|
4126
|
+
if (area === "context") {
|
|
4127
|
+
const result = await dependencies.commands.siteSetupContextCommand({
|
|
4128
|
+
projectDir,
|
|
4129
|
+
...connectionOptions
|
|
4130
|
+
});
|
|
4131
|
+
dependencies.stdout(JSON.stringify(result, null, 2));
|
|
4132
|
+
return { exitCode: result.status === "action_required" ? 1 : 0 };
|
|
4133
|
+
}
|
|
4134
|
+
if (area === "apply") {
|
|
4135
|
+
const editorDefinition = await dependencies.readJsonFile(
|
|
4136
|
+
readRequiredOption(areaArgs, "--definition")
|
|
4137
|
+
);
|
|
4138
|
+
const result = await dependencies.commands.siteSetupApplyCommand({
|
|
4139
|
+
projectDir,
|
|
4140
|
+
editorDefinition,
|
|
4141
|
+
...connectionOptions
|
|
4142
|
+
});
|
|
4143
|
+
dependencies.stdout(JSON.stringify(result, null, 2));
|
|
4144
|
+
return { exitCode: result.status === "action_required" ? 1 : 0 };
|
|
4145
|
+
}
|
|
4146
|
+
if (area === "verify") {
|
|
4147
|
+
const result = await dependencies.commands.siteSetupVerifyCommand({
|
|
4148
|
+
projectDir,
|
|
4149
|
+
deploymentUrl: readRequiredOption(areaArgs, "--deployment-url"),
|
|
4150
|
+
wait: areaArgs.includes("--wait"),
|
|
4151
|
+
...connectionOptions
|
|
4152
|
+
});
|
|
4153
|
+
dependencies.stdout(JSON.stringify(result, null, 2));
|
|
4154
|
+
return { exitCode: result.status === "ready_for_review" ? 0 : 1 };
|
|
4155
|
+
}
|
|
4156
|
+
dependencies.stdout(
|
|
4157
|
+
"Usage: siteplane setup <context|apply|verify>"
|
|
4158
|
+
);
|
|
4159
|
+
return { exitCode: 1 };
|
|
4160
|
+
}
|
|
4161
|
+
async function runBookingCommand(args, dependencies) {
|
|
4162
|
+
const [subcommand, ...subcommandArgs] = args;
|
|
4163
|
+
const projectDir = dependencies.cwd();
|
|
4164
|
+
const manifestPath = join7(projectDir, "siteplane.booking.json");
|
|
4165
|
+
if (subcommand === "init") {
|
|
4166
|
+
const siteId = readRequiredOption(subcommandArgs, "--site-id");
|
|
4167
|
+
const siteKeyPrefix = readRequiredOption(
|
|
4168
|
+
subcommandArgs,
|
|
4169
|
+
"--site-key-prefix"
|
|
4170
|
+
);
|
|
4171
|
+
const bookingPageSlug = readRequiredOption(
|
|
4172
|
+
subcommandArgs,
|
|
4173
|
+
"--booking-page-slug"
|
|
4174
|
+
);
|
|
4175
|
+
const productionOrigin = readOption(subcommandArgs, "--production-origin");
|
|
4176
|
+
const agentClient = readBookingAgentClient(subcommandArgs);
|
|
4177
|
+
const result = await dependencies.commands.bookingInitCommand({
|
|
4178
|
+
projectDir,
|
|
4179
|
+
siteId,
|
|
4180
|
+
siteKeyPrefix,
|
|
4181
|
+
bookingPageSlug,
|
|
4182
|
+
...productionOrigin ? { productionOrigin } : {},
|
|
4183
|
+
...agentClient ? { agentClient } : {}
|
|
4184
|
+
});
|
|
4185
|
+
dependencies.stdout(`Wrote ${result.manifestPath}`);
|
|
4186
|
+
for (const artifactPath of result.artifactPaths) {
|
|
4187
|
+
dependencies.stdout(`Wrote ${artifactPath}`);
|
|
4188
|
+
}
|
|
4189
|
+
return { exitCode: 0 };
|
|
4190
|
+
}
|
|
4191
|
+
if (subcommand === "check") {
|
|
4192
|
+
const sourceFilePaths = await dependencies.commands.resolveScanFilePaths(subcommandArgs, {
|
|
4193
|
+
projectDir
|
|
4194
|
+
});
|
|
4195
|
+
const result = await dependencies.commands.bookingCheckCommand({
|
|
4196
|
+
manifestPath,
|
|
4197
|
+
sourceFilePaths
|
|
4198
|
+
});
|
|
4199
|
+
dependencies.stdout(JSON.stringify(result, null, 2));
|
|
4200
|
+
return { exitCode: result.ok ? 0 : 1 };
|
|
4201
|
+
}
|
|
4202
|
+
if (subcommand === "sync") {
|
|
4203
|
+
const { config, manifest } = await readBookingProjectState(
|
|
4204
|
+
projectDir,
|
|
4205
|
+
manifestPath,
|
|
4206
|
+
dependencies
|
|
4207
|
+
);
|
|
4208
|
+
const response = await dependencies.commands.bookingSyncCommand({
|
|
4209
|
+
config,
|
|
4210
|
+
manifest
|
|
4211
|
+
});
|
|
4212
|
+
dependencies.stdout(await response.text());
|
|
4213
|
+
return { exitCode: response.ok ? 0 : 1 };
|
|
4214
|
+
}
|
|
4215
|
+
if (subcommand === "test") {
|
|
4216
|
+
const { config, manifest } = await readBookingProjectState(
|
|
4217
|
+
projectDir,
|
|
4218
|
+
manifestPath,
|
|
4219
|
+
dependencies
|
|
4220
|
+
);
|
|
4221
|
+
const customer = {
|
|
4222
|
+
name: readRequiredOption(subcommandArgs, "--name"),
|
|
4223
|
+
email: readRequiredOption(subcommandArgs, "--email"),
|
|
4224
|
+
...readOption(subcommandArgs, "--phone") ? { phone: readOption(subcommandArgs, "--phone") } : {},
|
|
4225
|
+
...readOption(subcommandArgs, "--note") ? { note: readOption(subcommandArgs, "--note") } : {}
|
|
4226
|
+
};
|
|
4227
|
+
const response = await dependencies.commands.bookingTestCommand({
|
|
4228
|
+
config,
|
|
4229
|
+
manifest,
|
|
4230
|
+
booking: {
|
|
4231
|
+
items: [
|
|
4232
|
+
{
|
|
4233
|
+
serviceId: readRequiredOption(subcommandArgs, "--service-id"),
|
|
4234
|
+
variantId: readRequiredOption(subcommandArgs, "--variant-id")
|
|
4235
|
+
}
|
|
4236
|
+
],
|
|
4237
|
+
resourceId: readOption(subcommandArgs, "--resource-id") ?? null,
|
|
4238
|
+
startUtc: readRequiredOption(subcommandArgs, "--start-utc"),
|
|
4239
|
+
customer,
|
|
4240
|
+
clientToken: readOption(subcommandArgs, "--client-token") ?? randomUUID4(),
|
|
4241
|
+
paymentChoice: "onsite"
|
|
4242
|
+
}
|
|
4243
|
+
});
|
|
4244
|
+
dependencies.stdout(await response.text());
|
|
4245
|
+
return { exitCode: response.ok ? 0 : 1 };
|
|
4246
|
+
}
|
|
4247
|
+
dependencies.stdout(
|
|
4248
|
+
"Usage: siteplane booking <init|check|sync|test>"
|
|
4249
|
+
);
|
|
4250
|
+
return { exitCode: 1 };
|
|
4251
|
+
}
|
|
4252
|
+
async function readBookingProjectState(projectDir, manifestPath, dependencies) {
|
|
4253
|
+
const [projectConfig, credentials, bookingManifest] = await Promise.all([
|
|
4254
|
+
dependencies.commands.readProjectConfig(projectDir),
|
|
4255
|
+
dependencies.commands.readLocalCredentials(projectDir),
|
|
4256
|
+
dependencies.commands.readBookingManifestFile(manifestPath)
|
|
4257
|
+
]);
|
|
4258
|
+
return {
|
|
4259
|
+
config: {
|
|
4260
|
+
apiBaseUrl: projectConfig.apiBaseUrl,
|
|
4261
|
+
accessToken: credentials.accessToken,
|
|
4262
|
+
siteId: projectConfig.siteId,
|
|
4263
|
+
...readOptionalVercelAutomationBypassSecret(dependencies.env)
|
|
4264
|
+
},
|
|
4265
|
+
manifest: bookingManifest.manifest
|
|
4266
|
+
};
|
|
4267
|
+
}
|
|
4268
|
+
function readOptionalVercelAutomationBypassSecret(env) {
|
|
4269
|
+
const secret = env.VERCEL_AUTOMATION_BYPASS_SECRET?.trim();
|
|
4270
|
+
return secret ? { vercelAutomationBypassSecret: secret } : {};
|
|
4271
|
+
}
|
|
4272
|
+
function createDefaultDependencies() {
|
|
4273
|
+
return {
|
|
4274
|
+
cwd: () => process.cwd(),
|
|
4275
|
+
env: process.env,
|
|
4276
|
+
stdout: (line) => console.log(line),
|
|
4277
|
+
stderr: (line) => console.error(line),
|
|
4278
|
+
readJsonFile: async (path) => JSON.parse(await readFile8(resolve(process.cwd(), path), "utf8")),
|
|
4279
|
+
readPackageVersion: async () => {
|
|
4280
|
+
const packageJson = JSON.parse(
|
|
4281
|
+
await readFile8(new URL("../../package.json", import.meta.url), "utf8")
|
|
4282
|
+
);
|
|
4283
|
+
return packageJson.version;
|
|
4284
|
+
},
|
|
4285
|
+
commands: {
|
|
4286
|
+
siteSetupApplyCommand,
|
|
4287
|
+
siteSetupContextCommand,
|
|
4288
|
+
siteSetupVerifyCommand,
|
|
4289
|
+
initCommand,
|
|
4290
|
+
bookingInitCommand,
|
|
4291
|
+
bookingCheckCommand,
|
|
4292
|
+
bookingSyncCommand,
|
|
4293
|
+
bookingTestCommand,
|
|
4294
|
+
analyticsCheckCommand,
|
|
4295
|
+
analyticsImportCommand,
|
|
4296
|
+
analyticsInitCommand,
|
|
4297
|
+
analyticsPublicKeyCommand,
|
|
4298
|
+
analyticsReadinessCommand,
|
|
4299
|
+
analyticsSyncCommand,
|
|
4300
|
+
analyticsTestCommand,
|
|
4301
|
+
readProjectConfig,
|
|
4302
|
+
readLocalCredentials,
|
|
4303
|
+
readBookingManifestFile,
|
|
4304
|
+
resolveScanFilePaths
|
|
4305
|
+
}
|
|
4306
|
+
};
|
|
4307
|
+
}
|
|
4308
|
+
function readRequiredOption(args, option) {
|
|
4309
|
+
const value = readOption(args, option);
|
|
4310
|
+
if (!value) {
|
|
4311
|
+
throw new Error(`Missing ${option}.`);
|
|
4312
|
+
}
|
|
4313
|
+
return value;
|
|
4314
|
+
}
|
|
4315
|
+
function readOption(args, option) {
|
|
4316
|
+
const index = args.indexOf(option);
|
|
4317
|
+
return index >= 0 ? args[index + 1] : void 0;
|
|
4318
|
+
}
|
|
4319
|
+
async function readPayloadFile(args, dependencies) {
|
|
4320
|
+
return dependencies.readJsonFile(readRequiredOption(args, "--file"));
|
|
4321
|
+
}
|
|
4322
|
+
async function printResponse(response, dependencies) {
|
|
4323
|
+
dependencies.stdout(await response.text());
|
|
4324
|
+
return { exitCode: response.ok ? 0 : 1 };
|
|
4325
|
+
}
|
|
4326
|
+
async function printAnalyticsResponseWithReadiness(response, config, dependencies) {
|
|
4327
|
+
const readinessResponse = await dependencies.commands.analyticsReadinessCommand({
|
|
4328
|
+
config
|
|
4329
|
+
});
|
|
4330
|
+
const [operation, readiness] = await Promise.all([
|
|
4331
|
+
readResponseJson(response),
|
|
4332
|
+
readResponseJson(readinessResponse)
|
|
4333
|
+
]);
|
|
4334
|
+
dependencies.stdout(JSON.stringify({ operation, readiness }, null, 2));
|
|
4335
|
+
return { exitCode: response.ok && readinessResponse.ok ? 0 : 1 };
|
|
4336
|
+
}
|
|
4337
|
+
async function readResponseJson(response) {
|
|
4338
|
+
const text = await response.text();
|
|
4339
|
+
try {
|
|
4340
|
+
return JSON.parse(text);
|
|
4341
|
+
} catch {
|
|
4342
|
+
return { ok: false, status: response.status, message: text };
|
|
4343
|
+
}
|
|
4344
|
+
}
|
|
4345
|
+
async function existingPaths(paths) {
|
|
4346
|
+
const existing = [];
|
|
4347
|
+
for (const path of paths) {
|
|
4348
|
+
try {
|
|
4349
|
+
await access2(path);
|
|
4350
|
+
existing.push(path);
|
|
4351
|
+
} catch {
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
return existing;
|
|
4355
|
+
}
|
|
4356
|
+
function readAnalyticsAgentClient(args) {
|
|
4357
|
+
const value = readOption(args, "--agent-client");
|
|
4358
|
+
if (!value) return void 0;
|
|
4359
|
+
if (value === "codex" || value === "claude" || value === "cursor") {
|
|
4360
|
+
return value;
|
|
4361
|
+
}
|
|
4362
|
+
throw new Error("--agent-client must be codex, claude or cursor.");
|
|
4363
|
+
}
|
|
4364
|
+
function readBookingAgentClient(args) {
|
|
4365
|
+
const value = readOption(args, "--agent-client");
|
|
4366
|
+
if (!value) {
|
|
4367
|
+
return void 0;
|
|
4368
|
+
}
|
|
4369
|
+
if (value === "codex" || value === "claude" || value === "cursor") {
|
|
4370
|
+
return value;
|
|
4371
|
+
}
|
|
4372
|
+
throw new Error("--agent-client must be codex, claude or cursor.");
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
// src/bin/siteplane.ts
|
|
4376
|
+
async function main() {
|
|
4377
|
+
const result = await runSiteplaneCli(process.argv.slice(2));
|
|
4378
|
+
process.exitCode = result.exitCode;
|
|
4379
|
+
}
|
|
4380
|
+
await main().catch((error) => {
|
|
4381
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
4382
|
+
process.exitCode = 1;
|
|
4383
|
+
});
|