@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 +21 -4
- package/dist/{chunk-LQTXMLKZ.js → chunk-CFZP7674.js} +21 -0
- package/dist/{chunk-AYV6KYDP.js → chunk-NSAZCP4I.js} +20 -1
- package/dist/{chunk-2BCEB26C.js → chunk-ULE565KD.js} +27 -0
- package/dist/content/index.js +1 -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 +94 -19
- package/dist/server/index.js +803 -152
- package/dist/studio/index.d.ts +1 -2
- package/dist/studio/index.js +64 -10
- 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
- package/sql/bootstrap.sql +313 -6
- package/sql/migrations/20260829032027_cms_security_and_sync_contracts.sql +390 -0
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,
|
|
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
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
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.
|
|
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
|
};
|
package/dist/content/index.js
CHANGED
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
|
|
@@ -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
|
|
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
|
-
/**
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
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,
|
|
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: '
|
|
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 };
|