@orion-studios/cms 0.5.5 → 0.5.7

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/README.md CHANGED
@@ -41,7 +41,14 @@ Requires Next.js (App Router), React 19, Zod 3. Supabase only in supabase mode.
41
41
  import { createCmsRoutes } from '@orion-studios/cms/server'
42
42
  import { registry } from '@/cms/registry'
43
43
  export const { GET, POST, PATCH, DELETE } = createCmsRoutes({
44
- registry, syncToken: process.env.CMS_SYNC_TOKEN,
44
+ registry,
45
+ syncToken: process.env.CMS_SYNC_TOKEN,
46
+ cronToken: process.env.CRON_SECRET,
47
+ previewSecret: process.env.CMS_PREVIEW_SECRET,
48
+ previewKeyId: process.env.CMS_PREVIEW_KEY_ID,
49
+ analyticsSecret: process.env.CMS_ANALYTICS_HASH_SECRET,
50
+ autoReplyHashSecret: process.env.CMS_AUTO_REPLY_HASH_SECRET,
51
+ projectRef: process.env.CMS_EXPECTED_SUPABASE_PROJECT_REF,
45
52
  })
46
53
 
47
54
  // src/app/studio/page.tsx — the admin
@@ -75,10 +82,20 @@ own role, never delete yourself.
75
82
  | Variable | Where | Purpose |
76
83
  | --- | --- | --- |
77
84
  | `NEXT_PUBLIC_SUPABASE_URL` | local + Vercel | project URL (also selects supabase mode) |
78
- | `NEXT_PUBLIC_SUPABASE_ANON_KEY` | local + Vercel | public reads (RLS: published content only) |
79
- | `SUPABASE_SERVICE_ROLE_KEY` | local + Vercel | server-only; powers the API layer |
80
- | `DATABASE_URL` | local + Vercel | Postgres for bootstrap (use the transaction pooler on Vercel) |
85
+ | `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY` | local + Vercel | public reads (RLS: published content only) |
86
+ | `SUPABASE_SECRET_KEY` | local + Vercel | server-only API access; the legacy service-role alias remains supported |
87
+ | `CMS_EXPECTED_SUPABASE_PROJECT_REF` | local + Vercel | binds the project URL, server credential, previews, and operator database target |
88
+ | `CMS_DATABASE_URL` | local operator only | direct or port 5432 session connection for bootstrap and migrations |
89
+ | `CMS_EXPECTED_DATABASE_ROLE` | local operator only | dedicated CMS migration role with no `app` schema access |
90
+ | `CMS_EXPECTED_DATABASE_NAME` | local operator only | exact database name required by the target guard |
91
+ | `CMS_DATABASE_CONFIRMATION` | local operator only | exact redacted target-receipt hash required before database writes |
92
+ | `CMS_PREVIEW_SECRET` | local + Vercel | signs short-lived, revocable preview grants |
93
+ | `CMS_PREVIEW_KEY_ID` | local + Vercel | non-secret identifier bound into preview grants |
94
+ | `CMS_PREVIEW_PREVIOUS_SECRET`, `CMS_PREVIEW_PREVIOUS_KEY_ID`, `CMS_PREVIEW_PREVIOUS_VALID_UNTIL` | local + Vercel | optional bounded preview-key rotation overlap |
95
+ | `CMS_ANALYTICS_HASH_SECRET` | local + Vercel | hashes analytics session and visitor identifiers |
96
+ | `CMS_AUTO_REPLY_HASH_SECRET` | local + Vercel | hashes form auto-reply rate-limit keys |
81
97
  | `CMS_SYNC_TOKEN` | local + Vercel | authorizes `POST /api/cms/sync` |
98
+ | `CRON_SECRET` | local + Vercel | authorizes scheduled publication only |
82
99
  | `CMS_STATIC=true` | optional | static mode (no CMS) |
83
100
  | `CMS_MEMORY=true` | optional | force memory mode |
84
101
 
@@ -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
  };
@@ -1,5 +1,21 @@
1
1
  // src/content/index.ts
2
2
  import { createClient } from "@supabase/supabase-js";
3
+
4
+ // src/supabase-keys.ts
5
+ var isServerSupabaseKey = (key) => {
6
+ if (key.startsWith("sb_secret_")) return true;
7
+ const payload = key.split(".")[1];
8
+ if (!payload) return false;
9
+ try {
10
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
11
+ const decoded = JSON.parse(atob(normalized));
12
+ return decoded.role === "service_role";
13
+ } catch {
14
+ return false;
15
+ }
16
+ };
17
+
18
+ // src/content/index.ts
3
19
  var CONTENT_CACHE_TAG = "orion-content";
4
20
  var toPublicPage = (row) => ({
5
21
  id: String(row.id),
@@ -11,12 +27,15 @@ var toPublicPage = (row) => ({
11
27
  });
12
28
  function createContentClient(options = {}) {
13
29
  const supabaseUrl = options.supabaseUrl || process.env.NEXT_PUBLIC_SUPABASE_URL || process.env.SUPABASE_URL || "";
14
- const anonKey = options.anonKey || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_ANON_KEY || process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY || process.env.SUPABASE_PUBLISHABLE_KEY || "";
30
+ const anonKey = options.anonKey || process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || process.env.SUPABASE_PUBLISHABLE_KEY || process.env.SUPABASE_ANON_KEY || "";
15
31
  if (!supabaseUrl || !anonKey) {
16
32
  throw new Error(
17
33
  "Orion CMS: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY (or NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY) must be set."
18
34
  );
19
35
  }
36
+ if (isServerSupabaseKey(anonKey)) {
37
+ throw new Error("Orion CMS: a Supabase server key cannot be used for public content reads.");
38
+ }
20
39
  const client = createClient(supabaseUrl, anonKey, {
21
40
  auth: { persistSession: false, autoRefreshToken: false }
22
41
  });
@@ -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,7 +1,7 @@
1
1
  import {
2
2
  CONTENT_CACHE_TAG,
3
3
  createContentClient
4
- } from "../chunk-AYV6KYDP.js";
4
+ } from "../chunk-NSAZCP4I.js";
5
5
  export {
6
6
  CONTENT_CACHE_TAG,
7
7
  createContentClient
@@ -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
@@ -50,8 +52,10 @@ type CmsRoutesOptions = {
50
52
  allowedOrigins?: string[];
51
53
  /** Rate limiter for public submissions. Defaults to in-memory (5/min/IP). */
52
54
  rateLimitStore?: RateLimitStore | null;
53
- /** Token accepted by POST /sync and POST /cron/* in addition to admin bearer auth. */
55
+ /** Token accepted only by POST /sync in addition to admin bearer auth. */
54
56
  syncToken?: string;
57
+ /** Token accepted only by POST or GET /cron/publish-due. */
58
+ cronToken?: string;
55
59
  /** Extra domains never flagged as email typos. */
56
60
  knownGoodEmailDomains?: string[];
57
61
  /**
@@ -61,8 +65,20 @@ type CmsRoutesOptions = {
61
65
  sendEmail?: EmailSender | null;
62
66
  /** Site name used in notification emails. */
63
67
  siteName?: string;
64
- /** Secret for draft preview tokens. Defaults to the service-role key. */
68
+ /** Purpose-specific secret for draft preview grants. */
65
69
  previewSecret?: string;
70
+ /** Stable identifier for the active preview signing key. */
71
+ previewKeyId?: string;
72
+ /** Optional prior preview key accepted only through the bounded overlap. */
73
+ previewPreviousSecret?: string;
74
+ previewPreviousKeyId?: string;
75
+ previewPreviousValidUntil?: number;
76
+ /** Stable Supabase project reference bound into preview grants. */
77
+ projectRef?: string;
78
+ /** Purpose-specific secret for analytics visitor and session hashing. */
79
+ analyticsSecret?: string;
80
+ /** Purpose-specific secret for auto-reply recipient hashing. */
81
+ autoReplyHashSecret?: string;
66
82
  /** Cloudflare Turnstile secret — when set, public submits must include a valid `_turnstileToken`. */
67
83
  turnstileSecret?: string;
68
84
  /** Max upload size in bytes (default 15 MB). */
@@ -79,6 +95,12 @@ type CmsRoutesOptions = {
79
95
  siteEventsPerMinute: number;
80
96
  siteEventsPerDay: number;
81
97
  }>;
98
+ /** Optional stricter auto-reply budgets. Values cannot raise defaults. */
99
+ autoReplyLimits?: Partial<{
100
+ recipientPerHour: number;
101
+ sitePerMinute: number;
102
+ sitePerDay: number;
103
+ }>;
82
104
  /**
83
105
  * Dev-only: uploads are stored as data URLs instead of Supabase Storage so
84
106
  * the memory backend can serve them. Never enable in production.
@@ -112,9 +134,35 @@ declare function createCmsRoutes(options: CmsRoutesOptions): {
112
134
  };
113
135
 
114
136
  declare const PREVIEW_TOKEN_TTL_MS: number;
115
- declare function createPreviewToken(pageId: string, secret: string, ttlMs?: number): string;
116
- /** Returns the page id when the token is valid and unexpired, else null. */
117
- declare function verifyPreviewToken(token: string, secret: string): string | null;
137
+ declare const PREVIEW_TOKEN_MAX_USES = 8;
138
+ declare const PREVIEW_SESSION_COOKIE = "orion_preview_session";
139
+ type PreviewSigningKey = {
140
+ id: string;
141
+ secret: string;
142
+ };
143
+ type PreviewKeyRing = {
144
+ active: PreviewSigningKey;
145
+ previous?: PreviewSigningKey & {
146
+ acceptUntil: number;
147
+ };
148
+ };
149
+ type PreviewGrantClaims = {
150
+ version: 1;
151
+ audience: 'cms-preview';
152
+ operation: 'read-draft';
153
+ keyId: string;
154
+ grantId: string;
155
+ pageId: string;
156
+ userId: string;
157
+ projectRef: string;
158
+ issuedAt: number;
159
+ expiresAt: number;
160
+ };
161
+ declare function createPreviewToken(grant: Omit<PreviewGrantClaims, 'version' | 'audience' | 'operation' | 'keyId' | 'issuedAt' | 'expiresAt'>, keys: PreviewKeyRing | string, ttlMs?: number): string;
162
+ /** Returns bound claims when the token is valid and unexpired, else null. */
163
+ declare function verifyPreviewGrantToken(token: string, keys: PreviewKeyRing | string, now?: number): PreviewGrantClaims | null;
164
+ /** Compatibility helper for callers that only need the signed page binding. */
165
+ declare function verifyPreviewToken(token: string, keys: PreviewKeyRing | string): string | null;
118
166
  type PreviewPage = {
119
167
  id: string;
120
168
  slug: string;
@@ -127,7 +175,7 @@ type PreviewPage = {
127
175
  * Site-side helper: verifies a preview token and loads the page's DRAFT
128
176
  * layout with the given (service or memory) client.
129
177
  */
130
- declare function getPreviewPage(client: SupabaseClient, token: string, secret: string): Promise<PreviewPage | null>;
178
+ declare function getPreviewPage(client: SupabaseClient, token: string, keys: PreviewKeyRing | string, expectedProjectRef: string): Promise<PreviewPage | null>;
131
179
 
132
180
  /**
133
181
  * Pure analytics aggregation: raw event rows in, the full dashboard payload
@@ -292,9 +340,11 @@ declare function geoFrom(request: Request): {
292
340
  * access token), verified via auth.getUser(). Stateless: no cookie plumbing.
293
341
  */
294
342
  type CmsEnv = {
343
+ expectedProjectRef: string;
295
344
  supabaseUrl: string;
296
345
  serviceRoleKey: string;
297
346
  };
347
+ declare function validateCmsEnv(env: CmsEnv): CmsEnv;
298
348
  declare function readCmsEnv(): CmsEnv;
299
349
  declare function getServiceClient(env?: CmsEnv): SupabaseClient;
300
350
  /** Test seam: inject a fake client. */
@@ -324,14 +374,6 @@ declare function can(user: CmsUser | null, action: CmsAction): boolean;
324
374
  */
325
375
  declare function isStructuralChange(previous: PageLayout, next: PageLayout): boolean;
326
376
 
327
- /**
328
- * Content-as-code sync: the single implementation used by both the
329
- * POST /api/cms/sync route and local `npm run sync` scripts.
330
- *
331
- * Contract (proven on Currin): create missing pages, refresh metadata always,
332
- * write layouts only where the page is not builder-owned, upsert globals and
333
- * forms. Idempotent.
334
- */
335
377
  type SyncPageInput = {
336
378
  slug: string;
337
379
  path?: string;
@@ -349,6 +391,11 @@ type SyncFormInput = {
349
391
  config?: Record<string, unknown>;
350
392
  successMessage?: string;
351
393
  };
394
+ type SyncRedirectInput = {
395
+ fromPath: string;
396
+ toPath: string;
397
+ permanent?: boolean;
398
+ };
352
399
  type SyncMediaInput = {
353
400
  storagePath: string;
354
401
  filename?: string;
@@ -364,8 +411,27 @@ type SyncInput = {
364
411
  globals?: SyncGlobalInput[];
365
412
  forms?: SyncFormInput[];
366
413
  media?: SyncMediaInput[];
414
+ redirects?: SyncRedirectInput[];
415
+ };
416
+ type SyncOperation = {
417
+ kind: 'page' | 'global' | 'form' | 'media' | 'redirect';
418
+ key: string;
419
+ action: 'create' | 'update' | 'noop' | 'conflict' | 'blocked-delete';
420
+ };
421
+ type SyncPlan = {
422
+ mode: 'dry-run';
423
+ manifestHash: string;
424
+ targetReceiptHash: string;
425
+ operations: SyncOperation[];
426
+ skipped: Array<{
427
+ slug: string;
428
+ issues: unknown;
429
+ }>;
367
430
  };
368
431
  type SyncResult = {
432
+ mode: 'apply';
433
+ manifestHash: string;
434
+ targetReceiptHash: string;
369
435
  synced: string[];
370
436
  skipped: Array<{
371
437
  slug: string;
@@ -374,14 +440,23 @@ type SyncResult = {
374
440
  globals: number;
375
441
  forms: number;
376
442
  media: number;
377
- /** Non-page resources that failed to write (globals/forms). */
378
443
  failed: Array<{
379
- kind: 'global' | 'form';
444
+ kind: SyncOperation['kind'];
380
445
  key: string;
381
446
  error: string;
382
447
  }>;
448
+ conflicts: SyncOperation[];
449
+ };
450
+ type SyncOptions = {
451
+ mode?: 'dry-run';
452
+ expectedProjectRef?: string;
453
+ } | {
454
+ mode: 'apply';
455
+ expectedManifestHash: string;
456
+ expectedProjectRef?: string;
457
+ expectedTargetReceiptHash: string;
383
458
  };
384
- declare function runContentSync(client: SupabaseClient, registry: BlockRegistry, input: SyncInput): Promise<SyncResult>;
459
+ declare function runContentSync(client: SupabaseClient, registry: BlockRegistry, input: SyncInput, options?: SyncOptions): Promise<SyncPlan | SyncResult>;
385
460
 
386
461
  /**
387
462
  * In-memory CMS backend for local development and end-to-end tests.
@@ -414,4 +489,4 @@ declare function createMemoryCms(): MemoryCms;
414
489
  /** Process-wide singleton for Next.js dev servers (module state survives HMR via globalThis). */
415
490
  declare function getMemoryCms(): MemoryCms;
416
491
 
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 };
492
+ 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_MAX_USES, PREVIEW_TOKEN_TTL_MS, type PreviewGrantClaims, type PreviewPage, type ResendSenderOptions, type StoredEvent, type SyncFormInput, type SyncGlobalInput, type SyncInput, type SyncMediaInput, type SyncOperation, type SyncOptions, type SyncPageInput, type SyncPlan, type SyncResult, aggregateAnalytics, can, createCmsRoutes, createDurableRateLimitStore, createMemoryCms, createPreviewToken, createResendSender, deviceFrom, formatSubmissionText, geoFrom, getMemoryCms, getPreviewPage, getServiceClient, isBotRequest, isStructuralChange, notifySubmission, parseEventBatch, readCmsEnv, resolveUser, runContentSync, sessionKeyFor, setServiceClientForTesting, validateCmsEnv, verifyPreviewGrantToken, verifyPreviewToken, visitorKeyFor };