@orion-studios/cms 0.5.5 → 0.5.6
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/dist/{chunk-LQTXMLKZ.js → chunk-CFZP7674.js} +21 -0
- package/dist/{chunk-2BCEB26C.js → chunk-ULE565KD.js} +27 -0
- package/dist/forms/index.d.ts +1 -1
- package/dist/forms/index.js +5 -1
- package/dist/forms/react.d.ts +1 -1
- package/dist/forms/react.js +1 -1
- package/dist/server/index.d.ts +11 -2
- package/dist/server/index.js +102 -42
- package/dist/studio/index.d.ts +1 -2
- package/dist/studio/index.js +39 -5
- package/dist/{submission-D_x2qNJl.d.ts → submission-CGz0lElf.d.ts} +10 -1
- package/dist/{submission-CzrfXu17.d.ts → submission-CKZgx1h7.d.ts} +2 -0
- package/package.json +1 -1
|
@@ -274,6 +274,25 @@ function collectFieldDefinitions(config) {
|
|
|
274
274
|
}
|
|
275
275
|
return definitions;
|
|
276
276
|
}
|
|
277
|
+
function getAutoReplyEmailFields(config) {
|
|
278
|
+
const fieldTypes = /* @__PURE__ */ new Map();
|
|
279
|
+
for (const definition of collectFieldDefinitions(config)) {
|
|
280
|
+
const types = fieldTypes.get(definition.name) || /* @__PURE__ */ new Set();
|
|
281
|
+
types.add(definition.type);
|
|
282
|
+
fieldTypes.set(definition.name, types);
|
|
283
|
+
}
|
|
284
|
+
return [...fieldTypes.entries()].filter(([, types]) => types.size === 1 && types.has("email")).map(([name]) => name);
|
|
285
|
+
}
|
|
286
|
+
function resolveAutoReplyEmailField(config) {
|
|
287
|
+
const eligibleFields = getAutoReplyEmailFields(config);
|
|
288
|
+
const configuredValue = config.notify?.autoReplyEmailField;
|
|
289
|
+
if (configuredValue !== void 0 && typeof configuredValue !== "string") return null;
|
|
290
|
+
const configuredField = configuredValue?.trim();
|
|
291
|
+
if (configuredField) {
|
|
292
|
+
return eligibleFields.includes(configuredField) ? configuredField : null;
|
|
293
|
+
}
|
|
294
|
+
return eligibleFields.length === 1 ? eligibleFields[0] : null;
|
|
295
|
+
}
|
|
277
296
|
var MAX_UNKNOWN_FIELDS = 20;
|
|
278
297
|
var MAX_VALUE_LENGTH = 5e3;
|
|
279
298
|
function validateChoice(definition, rawValue) {
|
|
@@ -443,6 +462,8 @@ export {
|
|
|
443
462
|
fieldOptions,
|
|
444
463
|
HONEYPOT_FIELD_NAME,
|
|
445
464
|
createMemoryRateLimitStore,
|
|
465
|
+
getAutoReplyEmailFields,
|
|
466
|
+
resolveAutoReplyEmailField,
|
|
446
467
|
processSubmission,
|
|
447
468
|
isOriginAllowed
|
|
448
469
|
};
|
|
@@ -221,6 +221,32 @@ function fieldOptions(field) {
|
|
|
221
221
|
|
|
222
222
|
// src/forms/submission.ts
|
|
223
223
|
var HONEYPOT_FIELD_NAME = "website_url_confirm";
|
|
224
|
+
function collectFieldDefinitions(config) {
|
|
225
|
+
const definitions = [];
|
|
226
|
+
for (const step of config.steps || []) {
|
|
227
|
+
for (const field of step.fields || []) {
|
|
228
|
+
const name = typeof field.name === "string" ? field.name.trim() : "";
|
|
229
|
+
if (!name) continue;
|
|
230
|
+
definitions.push({
|
|
231
|
+
name,
|
|
232
|
+
type: inferFieldType(field),
|
|
233
|
+
required: field.required === true,
|
|
234
|
+
label: typeof field.label === "string" && field.label.length > 0 ? field.label : name,
|
|
235
|
+
options: fieldOptions(field)
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return definitions;
|
|
240
|
+
}
|
|
241
|
+
function getAutoReplyEmailFields(config) {
|
|
242
|
+
const fieldTypes = /* @__PURE__ */ new Map();
|
|
243
|
+
for (const definition of collectFieldDefinitions(config)) {
|
|
244
|
+
const types = fieldTypes.get(definition.name) || /* @__PURE__ */ new Set();
|
|
245
|
+
types.add(definition.type);
|
|
246
|
+
fieldTypes.set(definition.name, types);
|
|
247
|
+
}
|
|
248
|
+
return [...fieldTypes.entries()].filter(([, types]) => types.size === 1 && types.has("email")).map(([name]) => name);
|
|
249
|
+
}
|
|
224
250
|
|
|
225
251
|
// src/forms/react.tsx
|
|
226
252
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -522,5 +548,6 @@ function FormRenderer({
|
|
|
522
548
|
|
|
523
549
|
export {
|
|
524
550
|
FORM_FIELD_TYPES,
|
|
551
|
+
getAutoReplyEmailFields,
|
|
525
552
|
FormRenderer
|
|
526
553
|
};
|
package/dist/forms/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { F as FormConfig, a as FormFieldConfig, b as FormNotifyConfig, H as HONEYPOT_FIELD_NAME, P as ProcessSubmissionArgs, c as ProcessSubmissionResult, R as RateLimitStore, d as createMemoryRateLimitStore, p as processSubmission } from '../submission-
|
|
1
|
+
export { F as FormConfig, a as FormFieldConfig, b as FormNotifyConfig, H as HONEYPOT_FIELD_NAME, P as ProcessSubmissionArgs, c as ProcessSubmissionResult, R as RateLimitStore, d as createMemoryRateLimitStore, g as getAutoReplyEmailFields, p as processSubmission, r as resolveAutoReplyEmailField } from '../submission-CGz0lElf.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Shared, isomorphic form validation and normalization.
|
package/dist/forms/index.js
CHANGED
|
@@ -4,31 +4,35 @@ import {
|
|
|
4
4
|
createMemoryRateLimitStore,
|
|
5
5
|
fieldOptions,
|
|
6
6
|
formatPhoneUS,
|
|
7
|
+
getAutoReplyEmailFields,
|
|
7
8
|
inferFieldType,
|
|
8
9
|
normalizeEmail,
|
|
9
10
|
normalizeFieldValue,
|
|
10
11
|
normalizePhone,
|
|
11
12
|
normalizeUrl,
|
|
12
13
|
processSubmission,
|
|
14
|
+
resolveAutoReplyEmailField,
|
|
13
15
|
validateDate,
|
|
14
16
|
validateEmail,
|
|
15
17
|
validateNumber,
|
|
16
18
|
validatePhoneUS,
|
|
17
19
|
validateRequired,
|
|
18
20
|
validateUrl
|
|
19
|
-
} from "../chunk-
|
|
21
|
+
} from "../chunk-CFZP7674.js";
|
|
20
22
|
export {
|
|
21
23
|
FORM_FIELD_TYPES,
|
|
22
24
|
HONEYPOT_FIELD_NAME,
|
|
23
25
|
createMemoryRateLimitStore,
|
|
24
26
|
fieldOptions,
|
|
25
27
|
formatPhoneUS,
|
|
28
|
+
getAutoReplyEmailFields,
|
|
26
29
|
inferFieldType,
|
|
27
30
|
normalizeEmail,
|
|
28
31
|
normalizeFieldValue,
|
|
29
32
|
normalizePhone,
|
|
30
33
|
normalizeUrl,
|
|
31
34
|
processSubmission,
|
|
35
|
+
resolveAutoReplyEmailField,
|
|
32
36
|
validateDate,
|
|
33
37
|
validateEmail,
|
|
34
38
|
validateNumber,
|
package/dist/forms/react.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { F as FormConfig } from '../submission-
|
|
3
|
+
import { F as FormConfig } from '../submission-CKZgx1h7.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The shared, config-driven form renderer: the same component renders a form
|
package/dist/forms/react.js
CHANGED
package/dist/server/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SupabaseClient } from '@supabase/supabase-js';
|
|
2
2
|
import { BlockRegistry, PageLayout } from '../blocks/index.js';
|
|
3
|
-
import { F as FormConfig, R as RateLimitStore } from '../submission-
|
|
3
|
+
import { F as FormConfig, R as RateLimitStore } from '../submission-CGz0lElf.js';
|
|
4
4
|
export { CONTENT_CACHE_TAG } from '../content/index.js';
|
|
5
5
|
import 'react';
|
|
6
6
|
import 'zod';
|
|
@@ -34,6 +34,8 @@ type NotifySubmissionArgs = {
|
|
|
34
34
|
successMessage?: string;
|
|
35
35
|
data: Record<string, unknown>;
|
|
36
36
|
siteName?: string;
|
|
37
|
+
/** Caller confirmation that abuse controls admitted this auto-reply. */
|
|
38
|
+
autoReplyAllowed?: boolean;
|
|
37
39
|
};
|
|
38
40
|
/**
|
|
39
41
|
* Sends the notification email(s) for one accepted submission: a copy to each
|
|
@@ -79,6 +81,12 @@ type CmsRoutesOptions = {
|
|
|
79
81
|
siteEventsPerMinute: number;
|
|
80
82
|
siteEventsPerDay: number;
|
|
81
83
|
}>;
|
|
84
|
+
/** Optional stricter auto-reply budgets. Values cannot raise defaults. */
|
|
85
|
+
autoReplyLimits?: Partial<{
|
|
86
|
+
recipientPerHour: number;
|
|
87
|
+
sitePerMinute: number;
|
|
88
|
+
sitePerDay: number;
|
|
89
|
+
}>;
|
|
82
90
|
/**
|
|
83
91
|
* Dev-only: uploads are stored as data URLs instead of Supabase Storage so
|
|
84
92
|
* the memory backend can serve them. Never enable in production.
|
|
@@ -112,6 +120,7 @@ declare function createCmsRoutes(options: CmsRoutesOptions): {
|
|
|
112
120
|
};
|
|
113
121
|
|
|
114
122
|
declare const PREVIEW_TOKEN_TTL_MS: number;
|
|
123
|
+
declare const PREVIEW_SESSION_COOKIE = "orion_preview_session";
|
|
115
124
|
declare function createPreviewToken(pageId: string, secret: string, ttlMs?: number): string;
|
|
116
125
|
/** Returns the page id when the token is valid and unexpired, else null. */
|
|
117
126
|
declare function verifyPreviewToken(token: string, secret: string): string | null;
|
|
@@ -414,4 +423,4 @@ declare function createMemoryCms(): MemoryCms;
|
|
|
414
423
|
/** Process-wide singleton for Next.js dev servers (module state survives HMR via globalThis). */
|
|
415
424
|
declare function getMemoryCms(): MemoryCms;
|
|
416
425
|
|
|
417
|
-
export { type AnalyticsEventType, type AnalyticsKpis, type AnalyticsSummary, type CmsAction, type CmsEnv, type CmsRole, type CmsRoutesOptions, type CmsUser, type EmailMessage, type EmailSender, type IncomingEvent, MEMORY_DEV_TOKEN, type MemoryCms, PREVIEW_TOKEN_TTL_MS, type PreviewPage, type ResendSenderOptions, type StoredEvent, type SyncFormInput, type SyncGlobalInput, type SyncInput, type SyncMediaInput, type SyncPageInput, type SyncResult, aggregateAnalytics, can, createCmsRoutes, createDurableRateLimitStore, createMemoryCms, createPreviewToken, createResendSender, deviceFrom, formatSubmissionText, geoFrom, getMemoryCms, getPreviewPage, getServiceClient, isBotRequest, isStructuralChange, notifySubmission, parseEventBatch, readCmsEnv, resolveUser, runContentSync, sessionKeyFor, setServiceClientForTesting, verifyPreviewToken, visitorKeyFor };
|
|
426
|
+
export { type AnalyticsEventType, type AnalyticsKpis, type AnalyticsSummary, type CmsAction, type CmsEnv, type CmsRole, type CmsRoutesOptions, type CmsUser, type EmailMessage, type EmailSender, type IncomingEvent, MEMORY_DEV_TOKEN, type MemoryCms, PREVIEW_SESSION_COOKIE, PREVIEW_TOKEN_TTL_MS, type PreviewPage, type ResendSenderOptions, type StoredEvent, type SyncFormInput, type SyncGlobalInput, type SyncInput, type SyncMediaInput, type SyncPageInput, type SyncResult, aggregateAnalytics, can, createCmsRoutes, createDurableRateLimitStore, createMemoryCms, createPreviewToken, createResendSender, deviceFrom, formatSubmissionText, geoFrom, getMemoryCms, getPreviewPage, getServiceClient, isBotRequest, isStructuralChange, notifySubmission, parseEventBatch, readCmsEnv, resolveUser, runContentSync, sessionKeyFor, setServiceClientForTesting, verifyPreviewToken, visitorKeyFor };
|
package/dist/server/index.js
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createMemoryRateLimitStore,
|
|
3
|
-
isOriginAllowed,
|
|
4
|
-
processSubmission
|
|
5
|
-
} from "../chunk-LQTXMLKZ.js";
|
|
6
1
|
import {
|
|
7
2
|
CONTENT_CACHE_TAG
|
|
8
3
|
} from "../chunk-AYV6KYDP.js";
|
|
4
|
+
import {
|
|
5
|
+
createMemoryRateLimitStore,
|
|
6
|
+
getAutoReplyEmailFields,
|
|
7
|
+
isOriginAllowed,
|
|
8
|
+
processSubmission,
|
|
9
|
+
resolveAutoReplyEmailField
|
|
10
|
+
} from "../chunk-CFZP7674.js";
|
|
9
11
|
|
|
10
12
|
// src/server/routes.ts
|
|
11
|
-
import { createHash as createHash2 } from "crypto";
|
|
13
|
+
import { createHash as createHash2, createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
12
14
|
|
|
13
15
|
// src/analytics/aggregate.ts
|
|
14
16
|
var CONVERSION_NAMES = /* @__PURE__ */ new Set(["call", "email"]);
|
|
@@ -340,12 +342,16 @@ ${rendered}
|
|
|
340
342
|
}
|
|
341
343
|
return lines.join("\n");
|
|
342
344
|
}
|
|
345
|
+
function resolveSubmitterEmail(config, data) {
|
|
346
|
+
const fieldName = resolveAutoReplyEmailField(config);
|
|
347
|
+
if (!fieldName) return void 0;
|
|
348
|
+
const value = data[fieldName];
|
|
349
|
+
return isEmail(value) ? value.trim().toLowerCase() : void 0;
|
|
350
|
+
}
|
|
343
351
|
async function notifySubmission(args) {
|
|
344
352
|
const notify = args.config.notify || {};
|
|
345
353
|
const recipients = (notify.emails || []).filter(isEmail);
|
|
346
|
-
const submitterEmail =
|
|
347
|
-
([key, value]) => key.toLowerCase().includes("email") && isEmail(value)
|
|
348
|
-
)?.[1];
|
|
354
|
+
const submitterEmail = resolveSubmitterEmail(args.config, args.data);
|
|
349
355
|
const subject = (notify.subject || "New {form} submission").replace(
|
|
350
356
|
/\{form\}/g,
|
|
351
357
|
args.formTitle || "form"
|
|
@@ -362,7 +368,7 @@ async function notifySubmission(args) {
|
|
|
362
368
|
console.error("[orion-cms] submission notification failed:", error);
|
|
363
369
|
}
|
|
364
370
|
}
|
|
365
|
-
if (notify.autoReply && submitterEmail) {
|
|
371
|
+
if (notify.autoReply === true && submitterEmail && args.autoReplyAllowed === true) {
|
|
366
372
|
try {
|
|
367
373
|
await args.sendEmail({
|
|
368
374
|
to: [submitterEmail],
|
|
@@ -431,12 +437,15 @@ var encode = (value) => Buffer.from(value, "utf8").toString("base64url");
|
|
|
431
437
|
var decode = (value) => Buffer.from(value, "base64url").toString("utf8");
|
|
432
438
|
var sign = (payload, secret) => createHmac2("sha256", secret).update(payload).digest("base64url");
|
|
433
439
|
var PREVIEW_TOKEN_TTL_MS = 60 * 60 * 1e3;
|
|
440
|
+
var PREVIEW_SESSION_COOKIE = "orion_preview_session";
|
|
434
441
|
function createPreviewToken(pageId, secret, ttlMs = PREVIEW_TOKEN_TTL_MS) {
|
|
435
442
|
const payload = encode(`${pageId}|${Date.now() + ttlMs}`);
|
|
436
443
|
return `${payload}.${sign(payload, secret)}`;
|
|
437
444
|
}
|
|
438
445
|
function verifyPreviewToken(token, secret) {
|
|
439
|
-
const
|
|
446
|
+
const segments = token.split(".");
|
|
447
|
+
if (segments.length !== 2) return null;
|
|
448
|
+
const [payload, signature] = segments;
|
|
440
449
|
if (!payload || !signature) return null;
|
|
441
450
|
const expected = sign(payload, secret);
|
|
442
451
|
const expectedBuffer = Buffer.from(expected);
|
|
@@ -649,7 +658,6 @@ async function runContentSync(client, registry, input) {
|
|
|
649
658
|
}
|
|
650
659
|
|
|
651
660
|
// src/server/routes.ts
|
|
652
|
-
import { timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
653
661
|
var tokenEquals = (candidate, secret) => {
|
|
654
662
|
if (!candidate || !secret) return false;
|
|
655
663
|
const a = Buffer.from(candidate);
|
|
@@ -802,6 +810,44 @@ function createCmsRoutes(options) {
|
|
|
802
810
|
return options.previewSecret || process.env.SUPABASE_SERVICE_ROLE_KEY || (options.memoryMode ? "orion-memory-preview-secret" : "");
|
|
803
811
|
};
|
|
804
812
|
const analyticsLimit = (max, windowMs, bucket) => options.memoryMode ? createMemoryRateLimitStore({ max, windowMs }) : createDurableRateLimitStore(db, { max, windowMs, bucket, failClosed: true });
|
|
813
|
+
const autoReplyLimit = (max, windowMs, bucket) => options.memoryMode ? createMemoryRateLimitStore({ max, windowMs }) : createDurableRateLimitStore(db, { max, windowMs, bucket, failClosed: true });
|
|
814
|
+
const boundedAutoReplyMax = (name, fallback) => {
|
|
815
|
+
const candidate = options.autoReplyLimits?.[name];
|
|
816
|
+
return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate > 0 ? Math.min(candidate, fallback) : fallback;
|
|
817
|
+
};
|
|
818
|
+
const autoReplyLimits = {
|
|
819
|
+
recipientPerHour: autoReplyLimit(
|
|
820
|
+
boundedAutoReplyMax("recipientPerHour", 3),
|
|
821
|
+
36e5,
|
|
822
|
+
"auto-reply-recipient-hour"
|
|
823
|
+
),
|
|
824
|
+
sitePerMinute: autoReplyLimit(
|
|
825
|
+
boundedAutoReplyMax("sitePerMinute", 10),
|
|
826
|
+
6e4,
|
|
827
|
+
"auto-reply-site-minute"
|
|
828
|
+
),
|
|
829
|
+
sitePerDay: autoReplyLimit(
|
|
830
|
+
boundedAutoReplyMax("sitePerDay", 200),
|
|
831
|
+
864e5,
|
|
832
|
+
"auto-reply-site-day"
|
|
833
|
+
)
|
|
834
|
+
};
|
|
835
|
+
const autoReplyRecipientKey = (email) => createHmac3(
|
|
836
|
+
"sha256",
|
|
837
|
+
previewSecret() || syncToken || "orion-memory-auto-reply-secret"
|
|
838
|
+
).update(`recipient:${email}`).digest("hex").slice(0, 24);
|
|
839
|
+
const maySendAutoReply = async (config, data) => {
|
|
840
|
+
if (config.notify?.autoReply !== true) return false;
|
|
841
|
+
const recipient = resolveSubmitterEmail(config, data);
|
|
842
|
+
if (!recipient) return false;
|
|
843
|
+
const now = Date.now();
|
|
844
|
+
if (await autoReplyLimits.recipientPerHour.isLimited(autoReplyRecipientKey(recipient), now)) {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
if (await autoReplyLimits.sitePerMinute.isLimited("all", now)) return false;
|
|
848
|
+
if (await autoReplyLimits.sitePerDay.isLimited("all", now)) return false;
|
|
849
|
+
return true;
|
|
850
|
+
};
|
|
805
851
|
const boundedAnalyticsMax = (name, fallback) => {
|
|
806
852
|
const candidate = options.analyticsLimits?.[name];
|
|
807
853
|
return typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate > 0 ? Math.min(candidate, fallback) : fallback;
|
|
@@ -1126,27 +1172,16 @@ function createCmsRoutes(options) {
|
|
|
1126
1172
|
const { data } = await db().from("cms_pages").select("id, path").eq("id", id).maybeSingle();
|
|
1127
1173
|
if (!data) return errors.notFound();
|
|
1128
1174
|
const token = createPreviewToken(id, secret);
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
if (!data) return errors.notFound();
|
|
1140
|
-
return json({
|
|
1141
|
-
page: {
|
|
1142
|
-
id: data.id,
|
|
1143
|
-
slug: data.slug,
|
|
1144
|
-
path: data.path,
|
|
1145
|
-
title: data.title,
|
|
1146
|
-
seo: data.seo ?? {},
|
|
1147
|
-
layout: data.draft_layout ?? []
|
|
1148
|
-
}
|
|
1149
|
-
});
|
|
1175
|
+
const previewUrl = `/cms-preview/${encodeURIComponent(String(data.id))}`;
|
|
1176
|
+
const response = json({ path: data.path, url: previewUrl });
|
|
1177
|
+
const secure = process.env.NODE_ENV === "production" || new URL(request.url).protocol === "https:";
|
|
1178
|
+
response.headers.set("cache-control", "private, no-store");
|
|
1179
|
+
response.headers.set("referrer-policy", "no-referrer");
|
|
1180
|
+
response.headers.append(
|
|
1181
|
+
"set-cookie",
|
|
1182
|
+
`${PREVIEW_SESSION_COOKIE}=${token}; Path=${previewUrl}; HttpOnly; SameSite=Strict; Max-Age=${Math.floor(PREVIEW_TOKEN_TTL_MS / 1e3)}${secure ? "; Secure" : ""}`
|
|
1183
|
+
);
|
|
1184
|
+
return response;
|
|
1150
1185
|
};
|
|
1151
1186
|
const listVersions = async (request, id) => {
|
|
1152
1187
|
const auth = await guard(request, "pages.restore");
|
|
@@ -1478,8 +1513,32 @@ function createCmsRoutes(options) {
|
|
|
1478
1513
|
const body = await readJson(request);
|
|
1479
1514
|
if (!body) return errors.badRequest("Invalid body.");
|
|
1480
1515
|
const config = isRecord2(body.config) ? { ...body.config } : {};
|
|
1481
|
-
|
|
1516
|
+
if (body.notify !== void 0 && !isRecord2(body.notify)) {
|
|
1517
|
+
return errors.badRequest("Notification settings must be an object.");
|
|
1518
|
+
}
|
|
1519
|
+
if (body.notify === void 0 && config.notify !== void 0 && !isRecord2(config.notify)) {
|
|
1520
|
+
return errors.badRequest("Notification settings must be an object.");
|
|
1521
|
+
}
|
|
1522
|
+
const rawNotify = isRecord2(body.notify) ? body.notify : isRecord2(config.notify) ? config.notify : {};
|
|
1482
1523
|
delete config.notify;
|
|
1524
|
+
if (rawNotify.autoReply !== void 0 && typeof rawNotify.autoReply !== "boolean") {
|
|
1525
|
+
return errors.badRequest("autoReply must be a boolean.");
|
|
1526
|
+
}
|
|
1527
|
+
if (rawNotify.autoReplyEmailField !== void 0 && (typeof rawNotify.autoReplyEmailField !== "string" || !rawNotify.autoReplyEmailField.trim())) {
|
|
1528
|
+
return errors.badRequest("autoReplyEmailField must name a declared email field.");
|
|
1529
|
+
}
|
|
1530
|
+
const emailFields = getAutoReplyEmailFields(config);
|
|
1531
|
+
const selectedEmailField = typeof rawNotify.autoReplyEmailField === "string" ? rawNotify.autoReplyEmailField.trim() : "";
|
|
1532
|
+
if (selectedEmailField && !emailFields.includes(selectedEmailField)) {
|
|
1533
|
+
return errors.badRequest("autoReplyEmailField must name a declared email field.");
|
|
1534
|
+
}
|
|
1535
|
+
if (rawNotify.autoReply === true && emailFields.length !== 1 && !selectedEmailField) {
|
|
1536
|
+
return errors.badRequest("Auto-reply requires one selected declared email field.");
|
|
1537
|
+
}
|
|
1538
|
+
const notify = {
|
|
1539
|
+
...rawNotify,
|
|
1540
|
+
...selectedEmailField ? { autoReplyEmailField: selectedEmailField } : {}
|
|
1541
|
+
};
|
|
1483
1542
|
const { data, error } = await db().from("cms_forms").upsert(
|
|
1484
1543
|
{
|
|
1485
1544
|
slug,
|
|
@@ -1542,7 +1601,7 @@ function createCmsRoutes(options) {
|
|
|
1542
1601
|
const csvEscape = (value) => {
|
|
1543
1602
|
let text = Array.isArray(value) ? value.join("; ") : typeof value === "string" ? value : value === null || value === void 0 ? "" : JSON.stringify(value);
|
|
1544
1603
|
if (/^[=+\-@\t\r]/.test(text)) text = `'${text}`;
|
|
1545
|
-
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
1604
|
+
return /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
1546
1605
|
};
|
|
1547
1606
|
const exportSubmissions = async (request) => {
|
|
1548
1607
|
const auth = await guard(request, "submissions.read");
|
|
@@ -1660,17 +1719,18 @@ function createCmsRoutes(options) {
|
|
|
1660
1719
|
} catch {
|
|
1661
1720
|
}
|
|
1662
1721
|
if (sendEmail) {
|
|
1722
|
+
const notificationConfig = {
|
|
1723
|
+
...form.config || {},
|
|
1724
|
+
...isRecord2(form.notify) && Object.keys(form.notify).length > 0 ? { notify: form.notify } : {}
|
|
1725
|
+
};
|
|
1663
1726
|
await notifySubmission({
|
|
1664
1727
|
sendEmail,
|
|
1665
1728
|
formTitle: String(form.title || form.slug),
|
|
1666
|
-
|
|
1667
|
-
config: {
|
|
1668
|
-
...form.config || {},
|
|
1669
|
-
...isRecord2(form.notify) && Object.keys(form.notify).length > 0 ? { notify: form.notify } : {}
|
|
1670
|
-
},
|
|
1729
|
+
config: notificationConfig,
|
|
1671
1730
|
successMessage: String(form.success_message || ""),
|
|
1672
1731
|
data: result.normalizedData,
|
|
1673
|
-
siteName: options.siteName
|
|
1732
|
+
siteName: options.siteName,
|
|
1733
|
+
autoReplyAllowed: await maySendAutoReply(notificationConfig, result.normalizedData)
|
|
1674
1734
|
});
|
|
1675
1735
|
}
|
|
1676
1736
|
return json({ success: true, id: created.id });
|
|
@@ -2035,7 +2095,6 @@ function createCmsRoutes(options) {
|
|
|
2035
2095
|
return listVersions(request, second);
|
|
2036
2096
|
}
|
|
2037
2097
|
}
|
|
2038
|
-
if (head === "preview" && !second && method === "GET") return previewPage(request);
|
|
2039
2098
|
if (head === "versions" && second) {
|
|
2040
2099
|
if (third === "restore" && method === "POST") return restoreVersion(request, second);
|
|
2041
2100
|
if (!third && method === "GET") return getVersion(request, second);
|
|
@@ -2586,6 +2645,7 @@ function getMemoryCms() {
|
|
|
2586
2645
|
export {
|
|
2587
2646
|
CONTENT_CACHE_TAG,
|
|
2588
2647
|
MEMORY_DEV_TOKEN,
|
|
2648
|
+
PREVIEW_SESSION_COOKIE,
|
|
2589
2649
|
PREVIEW_TOKEN_TTL_MS,
|
|
2590
2650
|
aggregateAnalytics,
|
|
2591
2651
|
can,
|
package/dist/studio/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as react from 'react';
|
|
|
2
2
|
import { ComponentType, InputHTMLAttributes } from 'react';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { SupabaseClient, Session } from '@supabase/supabase-js';
|
|
5
|
-
import { F as FormConfig, a as FormNotifyConfig } from '../submission-
|
|
5
|
+
import { F as FormConfig, a as FormNotifyConfig } from '../submission-CKZgx1h7.js';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Editor field derivation: turns a block's Zod schema into a default editor
|
|
@@ -395,7 +395,6 @@ declare function createStudioApi(options: {
|
|
|
395
395
|
success: true;
|
|
396
396
|
}>;
|
|
397
397
|
previewToken: (id: string) => Promise<{
|
|
398
|
-
token: string;
|
|
399
398
|
path: string;
|
|
400
399
|
url: string;
|
|
401
400
|
}>;
|
package/dist/studio/index.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
"use client";
|
|
3
3
|
import {
|
|
4
4
|
FORM_FIELD_TYPES,
|
|
5
|
-
FormRenderer
|
|
6
|
-
|
|
5
|
+
FormRenderer,
|
|
6
|
+
getAutoReplyEmailFields
|
|
7
|
+
} from "../chunk-ULE565KD.js";
|
|
7
8
|
|
|
8
9
|
// src/studio/Studio.tsx
|
|
9
10
|
import { useCallback as useCallback5, useEffect as useEffect11, useMemo as useMemo5, useState as useState12 } from "react";
|
|
@@ -988,6 +989,7 @@ function FormEditor({
|
|
|
988
989
|
const [notifyEmails, setNotifyEmails] = useState5("");
|
|
989
990
|
const [notifySubject, setNotifySubject] = useState5("");
|
|
990
991
|
const [autoReply, setAutoReply] = useState5(false);
|
|
992
|
+
const [autoReplyEmailField, setAutoReplyEmailField] = useState5("");
|
|
991
993
|
const [dirty, setDirty] = useState5(false);
|
|
992
994
|
const [message, setMessage] = useState5("");
|
|
993
995
|
const [error, setError] = useState5("");
|
|
@@ -1005,6 +1007,10 @@ function FormEditor({
|
|
|
1005
1007
|
setNotifyEmails((notify.emails || []).join(", "));
|
|
1006
1008
|
setNotifySubject(notify.subject || "");
|
|
1007
1009
|
setAutoReply(notify.autoReply === true);
|
|
1010
|
+
const emailFields = getAutoReplyEmailFields(loadedConfig);
|
|
1011
|
+
setAutoReplyEmailField(
|
|
1012
|
+
notify.autoReplyEmailField && emailFields.includes(notify.autoReplyEmailField) ? notify.autoReplyEmailField : emailFields.length === 1 ? emailFields[0] : ""
|
|
1013
|
+
);
|
|
1008
1014
|
}, (e) => setError(e.message));
|
|
1009
1015
|
}, [api, slug]);
|
|
1010
1016
|
const touch = () => {
|
|
@@ -1027,6 +1033,12 @@ function FormEditor({
|
|
|
1027
1033
|
const save = async () => {
|
|
1028
1034
|
setError("");
|
|
1029
1035
|
const emails = notifyEmails.split(/[,\s]+/).map((entry) => entry.trim()).filter(Boolean);
|
|
1036
|
+
const emailFields = getAutoReplyEmailFields(config);
|
|
1037
|
+
const selectedEmailField = emailFields.includes(autoReplyEmailField) ? autoReplyEmailField : emailFields.length === 1 ? emailFields[0] : "";
|
|
1038
|
+
if (autoReply && !selectedEmailField) {
|
|
1039
|
+
setError("Choose one declared email field before enabling auto-reply.");
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1030
1042
|
try {
|
|
1031
1043
|
const { form: saved } = await api.updateForm(slug, {
|
|
1032
1044
|
title,
|
|
@@ -1034,7 +1046,8 @@ function FormEditor({
|
|
|
1034
1046
|
notify: {
|
|
1035
1047
|
emails,
|
|
1036
1048
|
...notifySubject.trim() ? { subject: notifySubject.trim() } : {},
|
|
1037
|
-
autoReply
|
|
1049
|
+
autoReply,
|
|
1050
|
+
...autoReply && selectedEmailField ? { autoReplyEmailField: selectedEmailField } : {}
|
|
1038
1051
|
},
|
|
1039
1052
|
successMessage
|
|
1040
1053
|
});
|
|
@@ -1048,6 +1061,8 @@ function FormEditor({
|
|
|
1048
1061
|
if (!form && !error) return /* @__PURE__ */ jsx5("div", { className: "ost-loading", children: "Loading form\u2026" });
|
|
1049
1062
|
if (error && !form) return /* @__PURE__ */ jsx5("div", { className: "ost-error", children: error });
|
|
1050
1063
|
const steps = config.steps || [];
|
|
1064
|
+
const autoReplyEmailFields = getAutoReplyEmailFields(config);
|
|
1065
|
+
const selectedAutoReplyEmailField = autoReplyEmailFields.includes(autoReplyEmailField) ? autoReplyEmailField : autoReplyEmailFields.length === 1 ? autoReplyEmailFields[0] : "";
|
|
1051
1066
|
return /* @__PURE__ */ jsxs5("div", { className: "ost-view ost-view-wide", children: [
|
|
1052
1067
|
/* @__PURE__ */ jsxs5("header", { className: "ost-view-header", children: [
|
|
1053
1068
|
/* @__PURE__ */ jsxs5("div", { className: "ost-row", children: [
|
|
@@ -1212,7 +1227,26 @@ function FormEditor({
|
|
|
1212
1227
|
),
|
|
1213
1228
|
"Auto-reply to the submitter with the success message"
|
|
1214
1229
|
] }),
|
|
1215
|
-
/* @__PURE__ */
|
|
1230
|
+
autoReply ? /* @__PURE__ */ jsxs5("label", { className: "ost-label", children: [
|
|
1231
|
+
"Auto-reply email field",
|
|
1232
|
+
/* @__PURE__ */ jsxs5(
|
|
1233
|
+
"select",
|
|
1234
|
+
{
|
|
1235
|
+
className: "ost-input",
|
|
1236
|
+
disabled: !canWrite || autoReplyEmailFields.length === 0,
|
|
1237
|
+
onChange: (event) => {
|
|
1238
|
+
setAutoReplyEmailField(event.target.value);
|
|
1239
|
+
touch();
|
|
1240
|
+
},
|
|
1241
|
+
value: selectedAutoReplyEmailField,
|
|
1242
|
+
children: [
|
|
1243
|
+
autoReplyEmailFields.length !== 1 ? /* @__PURE__ */ jsx5("option", { value: "", children: "Choose an email field" }) : null,
|
|
1244
|
+
autoReplyEmailFields.map((fieldName) => /* @__PURE__ */ jsx5("option", { value: fieldName, children: fieldName }, fieldName))
|
|
1245
|
+
]
|
|
1246
|
+
}
|
|
1247
|
+
)
|
|
1248
|
+
] }) : null,
|
|
1249
|
+
/* @__PURE__ */ jsx5("p", { className: "ost-muted", children: "Auto-reply requires a declared email field and configured email sending (RESEND_API_KEY)." })
|
|
1216
1250
|
] })
|
|
1217
1251
|
] }),
|
|
1218
1252
|
/* @__PURE__ */ jsxs5("div", { className: "ost-form-preview", children: [
|
|
@@ -2225,7 +2259,7 @@ function PageEditor({
|
|
|
2225
2259
|
setBusy("preview");
|
|
2226
2260
|
try {
|
|
2227
2261
|
const { url } = await api.previewToken(pageId);
|
|
2228
|
-
window.open(url, "_blank", "noopener");
|
|
2262
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
2229
2263
|
} catch (previewError) {
|
|
2230
2264
|
setError(previewError instanceof Error ? previewError.message : "Preview failed.");
|
|
2231
2265
|
} finally {
|
|
@@ -24,6 +24,8 @@ type FormNotifyConfig = {
|
|
|
24
24
|
subject?: string;
|
|
25
25
|
/** Send the success message back to the submitter's email field. */
|
|
26
26
|
autoReply?: boolean;
|
|
27
|
+
/** Declared email field used for staff reply-to and auto-replies. */
|
|
28
|
+
autoReplyEmailField?: string;
|
|
27
29
|
};
|
|
28
30
|
type FormConfig = {
|
|
29
31
|
steps?: Array<{
|
|
@@ -39,6 +41,13 @@ declare function createMemoryRateLimitStore(options?: {
|
|
|
39
41
|
max?: number;
|
|
40
42
|
windowMs?: number;
|
|
41
43
|
}): RateLimitStore;
|
|
44
|
+
/**
|
|
45
|
+
* Returns unambiguous declared email fields in form order. A repeated name is
|
|
46
|
+
* eligible only when every declaration for that name is an email field.
|
|
47
|
+
*/
|
|
48
|
+
declare function getAutoReplyEmailFields(config: FormConfig): string[];
|
|
49
|
+
/** Resolves the one declared field that may control submission email delivery. */
|
|
50
|
+
declare function resolveAutoReplyEmailField(config: FormConfig): string | null;
|
|
42
51
|
type ProcessSubmissionArgs = {
|
|
43
52
|
config: FormConfig;
|
|
44
53
|
data: Record<string, unknown>;
|
|
@@ -58,4 +67,4 @@ type ProcessSubmissionResult = {
|
|
|
58
67
|
};
|
|
59
68
|
declare function processSubmission(args: ProcessSubmissionArgs): ProcessSubmissionResult;
|
|
60
69
|
|
|
61
|
-
export { type FormConfig as F, HONEYPOT_FIELD_NAME as H, type ProcessSubmissionArgs as P, type RateLimitStore as R, type FormFieldConfig as a, type FormNotifyConfig as b, type ProcessSubmissionResult as c, createMemoryRateLimitStore as d, processSubmission as p };
|
|
70
|
+
export { type FormConfig as F, HONEYPOT_FIELD_NAME as H, type ProcessSubmissionArgs as P, type RateLimitStore as R, type FormFieldConfig as a, type FormNotifyConfig as b, type ProcessSubmissionResult as c, createMemoryRateLimitStore as d, getAutoReplyEmailFields as g, processSubmission as p, resolveAutoReplyEmailField as r };
|
|
@@ -18,6 +18,8 @@ type FormNotifyConfig = {
|
|
|
18
18
|
subject?: string;
|
|
19
19
|
/** Send the success message back to the submitter's email field. */
|
|
20
20
|
autoReply?: boolean;
|
|
21
|
+
/** Declared email field used for staff reply-to and auto-replies. */
|
|
22
|
+
autoReplyEmailField?: string;
|
|
21
23
|
};
|
|
22
24
|
type FormConfig = {
|
|
23
25
|
steps?: Array<{
|