@orion-studios/cms 0.5.6 → 0.5.8

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.
@@ -52,8 +52,10 @@ type CmsRoutesOptions = {
52
52
  allowedOrigins?: string[];
53
53
  /** Rate limiter for public submissions. Defaults to in-memory (5/min/IP). */
54
54
  rateLimitStore?: RateLimitStore | null;
55
- /** 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. */
56
56
  syncToken?: string;
57
+ /** Token accepted only by POST or GET /cron/publish-due. */
58
+ cronToken?: string;
57
59
  /** Extra domains never flagged as email typos. */
58
60
  knownGoodEmailDomains?: string[];
59
61
  /**
@@ -63,8 +65,20 @@ type CmsRoutesOptions = {
63
65
  sendEmail?: EmailSender | null;
64
66
  /** Site name used in notification emails. */
65
67
  siteName?: string;
66
- /** Secret for draft preview tokens. Defaults to the service-role key. */
68
+ /** Purpose-specific secret for draft preview grants. */
67
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;
68
82
  /** Cloudflare Turnstile secret — when set, public submits must include a valid `_turnstileToken`. */
69
83
  turnstileSecret?: string;
70
84
  /** Max upload size in bytes (default 15 MB). */
@@ -120,10 +134,35 @@ declare function createCmsRoutes(options: CmsRoutesOptions): {
120
134
  };
121
135
 
122
136
  declare const PREVIEW_TOKEN_TTL_MS: number;
137
+ declare const PREVIEW_TOKEN_MAX_USES = 8;
123
138
  declare const PREVIEW_SESSION_COOKIE = "orion_preview_session";
124
- declare function createPreviewToken(pageId: string, secret: string, ttlMs?: number): string;
125
- /** Returns the page id when the token is valid and unexpired, else null. */
126
- declare function verifyPreviewToken(token: string, secret: string): string | null;
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;
127
166
  type PreviewPage = {
128
167
  id: string;
129
168
  slug: string;
@@ -136,7 +175,7 @@ type PreviewPage = {
136
175
  * Site-side helper: verifies a preview token and loads the page's DRAFT
137
176
  * layout with the given (service or memory) client.
138
177
  */
139
- 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>;
140
179
 
141
180
  /**
142
181
  * Pure analytics aggregation: raw event rows in, the full dashboard payload
@@ -144,8 +183,8 @@ declare function getPreviewPage(client: SupabaseClient, token: string, secret: s
144
183
  * and memory backends and is trivially testable.
145
184
  *
146
185
  * 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.
186
+ * forgeable. Form conversions come from authoritative submission rows. Any
187
+ * other conversion requires a server-verified event.
149
188
  */
150
189
  type StoredEvent = {
151
190
  id?: number | string;
@@ -163,6 +202,12 @@ type StoredEvent = {
163
202
  server_verified?: boolean;
164
203
  created_at: string;
165
204
  };
205
+ /** Minimal authoritative form record used by aggregate analytics. */
206
+ type StoredFormSubmission = {
207
+ form: string;
208
+ session_key: string;
209
+ created_at: string;
210
+ };
166
211
  type AnalyticsKpis = {
167
212
  visitors: number;
168
213
  identifiedVisitors: number;
@@ -228,7 +273,7 @@ type AnalyticsSummary = {
228
273
  declare function aggregateAnalytics(events: StoredEvent[], previousEvents: StoredEvent[], range: {
229
274
  from: string;
230
275
  to: string;
231
- }): AnalyticsSummary;
276
+ }, submissions?: StoredFormSubmission[], previousSubmissions?: StoredFormSubmission[]): AnalyticsSummary;
232
277
 
233
278
  /**
234
279
  * Server-side analytics ingest: validation, bot filtering, and the
@@ -301,9 +346,11 @@ declare function geoFrom(request: Request): {
301
346
  * access token), verified via auth.getUser(). Stateless: no cookie plumbing.
302
347
  */
303
348
  type CmsEnv = {
349
+ expectedProjectRef: string;
304
350
  supabaseUrl: string;
305
351
  serviceRoleKey: string;
306
352
  };
353
+ declare function validateCmsEnv(env: CmsEnv): CmsEnv;
307
354
  declare function readCmsEnv(): CmsEnv;
308
355
  declare function getServiceClient(env?: CmsEnv): SupabaseClient;
309
356
  /** Test seam: inject a fake client. */
@@ -333,14 +380,6 @@ declare function can(user: CmsUser | null, action: CmsAction): boolean;
333
380
  */
334
381
  declare function isStructuralChange(previous: PageLayout, next: PageLayout): boolean;
335
382
 
336
- /**
337
- * Content-as-code sync: the single implementation used by both the
338
- * POST /api/cms/sync route and local `npm run sync` scripts.
339
- *
340
- * Contract (proven on Currin): create missing pages, refresh metadata always,
341
- * write layouts only where the page is not builder-owned, upsert globals and
342
- * forms. Idempotent.
343
- */
344
383
  type SyncPageInput = {
345
384
  slug: string;
346
385
  path?: string;
@@ -358,6 +397,11 @@ type SyncFormInput = {
358
397
  config?: Record<string, unknown>;
359
398
  successMessage?: string;
360
399
  };
400
+ type SyncRedirectInput = {
401
+ fromPath: string;
402
+ toPath: string;
403
+ permanent?: boolean;
404
+ };
361
405
  type SyncMediaInput = {
362
406
  storagePath: string;
363
407
  filename?: string;
@@ -373,8 +417,27 @@ type SyncInput = {
373
417
  globals?: SyncGlobalInput[];
374
418
  forms?: SyncFormInput[];
375
419
  media?: SyncMediaInput[];
420
+ redirects?: SyncRedirectInput[];
421
+ };
422
+ type SyncOperation = {
423
+ kind: 'page' | 'global' | 'form' | 'media' | 'redirect';
424
+ key: string;
425
+ action: 'create' | 'update' | 'noop' | 'conflict' | 'blocked-delete';
426
+ };
427
+ type SyncPlan = {
428
+ mode: 'dry-run';
429
+ manifestHash: string;
430
+ targetReceiptHash: string;
431
+ operations: SyncOperation[];
432
+ skipped: Array<{
433
+ slug: string;
434
+ issues: unknown;
435
+ }>;
376
436
  };
377
437
  type SyncResult = {
438
+ mode: 'apply';
439
+ manifestHash: string;
440
+ targetReceiptHash: string;
378
441
  synced: string[];
379
442
  skipped: Array<{
380
443
  slug: string;
@@ -383,14 +446,23 @@ type SyncResult = {
383
446
  globals: number;
384
447
  forms: number;
385
448
  media: number;
386
- /** Non-page resources that failed to write (globals/forms). */
387
449
  failed: Array<{
388
- kind: 'global' | 'form';
450
+ kind: SyncOperation['kind'];
389
451
  key: string;
390
452
  error: string;
391
453
  }>;
454
+ conflicts: SyncOperation[];
455
+ };
456
+ type SyncOptions = {
457
+ mode?: 'dry-run';
458
+ expectedProjectRef?: string;
459
+ } | {
460
+ mode: 'apply';
461
+ expectedManifestHash: string;
462
+ expectedProjectRef?: string;
463
+ expectedTargetReceiptHash: string;
392
464
  };
393
- declare function runContentSync(client: SupabaseClient, registry: BlockRegistry, input: SyncInput): Promise<SyncResult>;
465
+ declare function runContentSync(client: SupabaseClient, registry: BlockRegistry, input: SyncInput, options?: SyncOptions): Promise<SyncPlan | SyncResult>;
394
466
 
395
467
  /**
396
468
  * In-memory CMS backend for local development and end-to-end tests.
@@ -423,4 +495,4 @@ declare function createMemoryCms(): MemoryCms;
423
495
  /** Process-wide singleton for Next.js dev servers (module state survives HMR via globalThis). */
424
496
  declare function getMemoryCms(): MemoryCms;
425
497
 
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 };
498
+ 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 StoredFormSubmission, 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 };