@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.
@@ -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
  };
@@ -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-D_x2qNJl.js';
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.
@@ -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-LQTXMLKZ.js";
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,
@@ -1,6 +1,6 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { F as FormConfig } from '../submission-CzrfXu17.js';
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
@@ -2,7 +2,7 @@
2
2
  "use client";
3
3
  import {
4
4
  FormRenderer
5
- } from "../chunk-2BCEB26C.js";
5
+ } from "../chunk-ULE565KD.js";
6
6
  export {
7
7
  FormRenderer
8
8
  };
@@ -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-D_x2qNJl.js';
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 };
@@ -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 = Object.entries(args.data).find(
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 [payload, signature] = token.split(".");
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
- return json({ token, path: data.path, url: `${data.path}?preview=${encodeURIComponent(token)}` });
1130
- };
1131
- const previewPage = async (request) => {
1132
- if (hostedMemoryMode) return errors.notFound();
1133
- const secret = previewSecret();
1134
- if (!secret) return errors.notFound();
1135
- const token = new URL(request.url).searchParams.get("token") || "";
1136
- const pageId = verifyPreviewToken(token, secret);
1137
- if (!pageId) return errors.forbidden();
1138
- const { data } = await db().from("cms_pages").select("id, slug, path, title, seo, draft_layout").eq("id", pageId).maybeSingle();
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
- const notify = isRecord2(body.notify) ? body.notify : isRecord2(config.notify) ? config.notify : {};
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
- // Notify settings live in their own column; merge for legacy configs.
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,
@@ -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-CzrfXu17.js';
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
  }>;
@@ -2,8 +2,9 @@
2
2
  "use client";
3
3
  import {
4
4
  FORM_FIELD_TYPES,
5
- FormRenderer
6
- } from "../chunk-2BCEB26C.js";
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__ */ jsx5("p", { className: "ost-muted", children: "Requires the site's email sending to be configured (RESEND_API_KEY)." })
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<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orion-studios/cms",
3
- "version": "0.5.5",
3
+ "version": "0.5.6",
4
4
  "description": "Orion CMS v2 core engine \u2014 JSONB content model on Supabase primitives",
5
5
  "type": "module",
6
6
  "exports": {