@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.
- package/dist/{chunk-VPUODCNH.js → chunk-CFZP7674.js} +29 -8
- package/dist/{chunk-WQDHEQDE.js → chunk-ULE565KD.js} +27 -1
- 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 +26 -4
- package/dist/server/index.js +340 -103
- package/dist/studio/index.d.ts +1 -2
- package/dist/studio/index.js +42 -8
- package/dist/{submission-BKdBedOe.d.ts → submission-CGz0lElf.d.ts} +11 -2
- package/dist/{submission-CzrfXu17.d.ts → submission-CKZgx1h7.d.ts} +2 -0
- package/package.json +2 -1
- package/sql/bootstrap.sql +61 -9
- package/sql/migrations/20260827164127_sec_014_form_notify_privacy.sql +41 -0
- package/sql/migrations/20260827172120_sec_015_analytics_ingest_limits.sql +101 -0
- package/sql/migrations/20260827232146_sec_016_media_privacy.sql +21 -0
|
@@ -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
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
hits.set(key,
|
|
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,
|
|
251
|
-
if (
|
|
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
|
|
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
|
};
|
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
|
|
@@ -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
|
-
*
|
|
127
|
-
*
|
|
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 };
|