@orion-studios/cms 0.5.4 → 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.
@@ -241,19 +241,19 @@ function createMemoryRateLimitStore(options) {
241
241
  const windowMs = options?.windowMs ?? 6e4;
242
242
  const hits = /* @__PURE__ */ new Map();
243
243
  return {
244
- isLimited(key, now) {
245
- const windowStart = now - windowMs;
246
- const entries = (hits.get(key) || []).filter((timestamp) => timestamp > windowStart);
247
- entries.push(now);
248
- hits.set(key, entries);
244
+ isLimited(key, now, cost = 1) {
245
+ const normalizedCost = Number.isSafeInteger(cost) && cost > 0 ? cost : max + 1;
246
+ const existing = hits.get(key);
247
+ const entry = !existing || now - existing.windowStart >= windowMs ? { windowStart: now, count: normalizedCost } : { ...existing, count: existing.count + normalizedCost };
248
+ hits.set(key, entry);
249
249
  if (hits.size > 1e4) {
250
- for (const [entryKey, timestamps] of hits) {
251
- if (timestamps.every((timestamp) => timestamp <= windowStart)) {
250
+ for (const [entryKey, candidate] of hits) {
251
+ if (now - candidate.windowStart >= windowMs) {
252
252
  hits.delete(entryKey);
253
253
  }
254
254
  }
255
255
  }
256
- return entries.length > max;
256
+ return entry.count > max;
257
257
  }
258
258
  };
259
259
  }
@@ -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";
@@ -348,7 +374,6 @@ function FormRenderer({
348
374
  return;
349
375
  }
350
376
  setState("success");
351
- funnel("submit");
352
377
  onSuccess?.();
353
378
  } catch {
354
379
  setState("error");
@@ -523,5 +548,6 @@ function FormRenderer({
523
548
 
524
549
  export {
525
550
  FORM_FIELD_TYPES,
551
+ getAutoReplyEmailFields,
526
552
  FormRenderer
527
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-BKdBedOe.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-VPUODCNH.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-WQDHEQDE.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-BKdBedOe.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
@@ -69,6 +71,22 @@ type CmsRoutesOptions = {
69
71
  maxUploadBytes?: number;
70
72
  /** Days of raw analytics events to keep (default 90). */
71
73
  analyticsRetentionDays?: number;
74
+ /** Optional stricter public analytics budgets. Values cannot raise defaults. */
75
+ analyticsLimits?: Partial<{
76
+ requestsPerMinute: number;
77
+ siteRequestsPerMinute: number;
78
+ siteRequestsPerDay: number;
79
+ clientEventsPerMinute: number;
80
+ clientEventsPerDay: number;
81
+ siteEventsPerMinute: number;
82
+ siteEventsPerDay: number;
83
+ }>;
84
+ /** Optional stricter auto-reply budgets. Values cannot raise defaults. */
85
+ autoReplyLimits?: Partial<{
86
+ recipientPerHour: number;
87
+ sitePerMinute: number;
88
+ sitePerDay: number;
89
+ }>;
72
90
  /**
73
91
  * Dev-only: uploads are stored as data URLs instead of Supabase Storage so
74
92
  * the memory backend can serve them. Never enable in production.
@@ -92,6 +110,7 @@ declare function createDurableRateLimitStore(getClient: () => SupabaseClient, op
92
110
  max?: number;
93
111
  windowMs?: number;
94
112
  bucket?: string;
113
+ failClosed?: boolean;
95
114
  }): RateLimitStore;
96
115
  declare function createCmsRoutes(options: CmsRoutesOptions): {
97
116
  GET: (request: Request, context: RouteContext) => Promise<Response>;
@@ -101,6 +120,7 @@ declare function createCmsRoutes(options: CmsRoutesOptions): {
101
120
  };
102
121
 
103
122
  declare const PREVIEW_TOKEN_TTL_MS: number;
123
+ declare const PREVIEW_SESSION_COOKIE = "orion_preview_session";
104
124
  declare function createPreviewToken(pageId: string, secret: string, ttlMs?: number): string;
105
125
  /** Returns the page id when the token is valid and unexpired, else null. */
106
126
  declare function verifyPreviewToken(token: string, secret: string): string | null;
@@ -123,8 +143,9 @@ declare function getPreviewPage(client: SupabaseClient, token: string, secret: s
123
143
  * out. Isomorphic and dependency-free so the same code serves the Supabase
124
144
  * and memory backends and is trivially testable.
125
145
  *
126
- * Conversion = a call tap, a form submission, or an email click. A session
127
- * "converted" when it contains at least one conversion event.
146
+ * Call and email taps are useful interaction counts, but browser events are
147
+ * forgeable. A conversion requires a server-verified event, currently a form
148
+ * submission recorded after the authoritative lead write succeeds.
128
149
  */
129
150
  type StoredEvent = {
130
151
  id?: number | string;
@@ -139,6 +160,7 @@ type StoredEvent = {
139
160
  region: string;
140
161
  city: string;
141
162
  meta: Record<string, unknown> | null;
163
+ server_verified?: boolean;
142
164
  created_at: string;
143
165
  };
144
166
  type AnalyticsKpis = {
@@ -401,4 +423,4 @@ declare function createMemoryCms(): MemoryCms;
401
423
  /** Process-wide singleton for Next.js dev servers (module state survives HMR via globalThis). */
402
424
  declare function getMemoryCms(): MemoryCms;
403
425
 
404
- 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 };