@createcms/core 0.1.1

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.
Files changed (83) hide show
  1. package/README.md +169 -0
  2. package/dist/ab-edge/index.cjs +214 -0
  3. package/dist/ab-edge/index.d.cts +121 -0
  4. package/dist/ab-edge/index.d.ts +121 -0
  5. package/dist/ab-edge/index.js +205 -0
  6. package/dist/bin/createcms.js +3082 -0
  7. package/dist/db.cjs +496 -0
  8. package/dist/db.d.cts +128 -0
  9. package/dist/db.d.ts +128 -0
  10. package/dist/db.js +488 -0
  11. package/dist/index.cjs +13789 -0
  12. package/dist/index.d.cts +10277 -0
  13. package/dist/index.d.ts +10277 -0
  14. package/dist/index.js +13737 -0
  15. package/dist/nanoid.cjs +50 -0
  16. package/dist/nanoid.d.cts +29 -0
  17. package/dist/nanoid.d.ts +29 -0
  18. package/dist/nanoid.js +47 -0
  19. package/dist/next/index.cjs +60 -0
  20. package/dist/next/index.d.cts +141 -0
  21. package/dist/next/index.d.ts +141 -0
  22. package/dist/next/index.js +58 -0
  23. package/dist/next/middleware.cjs +113 -0
  24. package/dist/next/middleware.d.cts +77 -0
  25. package/dist/next/middleware.d.ts +77 -0
  26. package/dist/next/middleware.js +111 -0
  27. package/dist/plugins/ab-test/analytics/upstash.cjs +345 -0
  28. package/dist/plugins/ab-test/analytics/upstash.d.cts +193 -0
  29. package/dist/plugins/ab-test/analytics/upstash.d.ts +193 -0
  30. package/dist/plugins/ab-test/analytics/upstash.js +343 -0
  31. package/dist/plugins/ab-test/client.cjs +686 -0
  32. package/dist/plugins/ab-test/client.d.cts +233 -0
  33. package/dist/plugins/ab-test/client.d.ts +233 -0
  34. package/dist/plugins/ab-test/client.js +684 -0
  35. package/dist/plugins/ab-test/index.cjs +3400 -0
  36. package/dist/plugins/ab-test/index.d.cts +1131 -0
  37. package/dist/plugins/ab-test/index.d.ts +1131 -0
  38. package/dist/plugins/ab-test/index.js +3367 -0
  39. package/dist/plugins/client.cjs +20 -0
  40. package/dist/plugins/client.d.cts +3 -0
  41. package/dist/plugins/client.d.ts +3 -0
  42. package/dist/plugins/client.js +3 -0
  43. package/dist/plugins/consent/client.cjs +315 -0
  44. package/dist/plugins/consent/client.d.cts +145 -0
  45. package/dist/plugins/consent/client.d.ts +145 -0
  46. package/dist/plugins/consent/client.js +313 -0
  47. package/dist/plugins/consent/index.cjs +267 -0
  48. package/dist/plugins/consent/index.d.cts +618 -0
  49. package/dist/plugins/consent/index.d.ts +618 -0
  50. package/dist/plugins/consent/index.js +258 -0
  51. package/dist/plugins/i18n/index.cjs +2177 -0
  52. package/dist/plugins/i18n/index.d.cts +562 -0
  53. package/dist/plugins/i18n/index.d.ts +562 -0
  54. package/dist/plugins/i18n/index.js +2150 -0
  55. package/dist/plugins/media-optimize/index.cjs +315 -0
  56. package/dist/plugins/media-optimize/index.d.cts +144 -0
  57. package/dist/plugins/media-optimize/index.d.ts +144 -0
  58. package/dist/plugins/media-optimize/index.js +311 -0
  59. package/dist/plugins/multi-tenant/index.cjs +210 -0
  60. package/dist/plugins/multi-tenant/index.d.cts +431 -0
  61. package/dist/plugins/multi-tenant/index.d.ts +431 -0
  62. package/dist/plugins/multi-tenant/index.js +207 -0
  63. package/dist/plugins/server.cjs +24 -0
  64. package/dist/plugins/server.d.cts +3 -0
  65. package/dist/plugins/server.d.ts +3 -0
  66. package/dist/plugins/server.js +3 -0
  67. package/dist/react/blocks.cjs +233 -0
  68. package/dist/react/blocks.d.cts +320 -0
  69. package/dist/react/blocks.d.ts +320 -0
  70. package/dist/react/blocks.js +226 -0
  71. package/dist/react/index.cjs +901 -0
  72. package/dist/react/index.d.cts +992 -0
  73. package/dist/react/index.d.ts +992 -0
  74. package/dist/react/index.js +872 -0
  75. package/dist/react/tracking.cjs +243 -0
  76. package/dist/react/tracking.d.cts +364 -0
  77. package/dist/react/tracking.d.ts +364 -0
  78. package/dist/react/tracking.js +216 -0
  79. package/dist/react/variant.cjs +59 -0
  80. package/dist/react/variant.d.cts +26 -0
  81. package/dist/react/variant.d.ts +26 -0
  82. package/dist/react/variant.js +57 -0
  83. package/package.json +303 -0
@@ -0,0 +1,618 @@
1
+ import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
2
+ import { AnyPgTable, PgDatabase } from 'drizzle-orm/pg-core';
3
+ import { AnyColumn, SQL } from 'drizzle-orm';
4
+ import { Endpoint, Middleware } from 'better-call';
5
+
6
+ type ResolvedUserConfig = {
7
+ table: AnyPgTable;
8
+ tableName: string;
9
+ schemaName: string | null;
10
+ idColumn: AnyColumn;
11
+ /** The camelCase key used in the Drizzle table definition (e.g. "id"). */
12
+ idColumnKey: string;
13
+ /** The actual database column name (e.g. "id" or "user_id"). */
14
+ idColumnDbName: string;
15
+ allColumns: Record<string, AnyColumn>;
16
+ /** Allowlist (camelCase keys) of columns exposable via `withUser`. */
17
+ exposeColumns: string[];
18
+ sqlTableRef: string;
19
+ };
20
+
21
+ type DrizzleInstance = PgDatabase<any, Record<string, unknown>, any>;
22
+
23
+ declare const notificationTypeEnum: drizzle_orm_pg_core.PgEnum<["mention", "comment", "threadResolved", "approvalRequested", "approvalApproved", "approvalRejected", "mergeRequestOpened", "mergeRequestMerged", "mergeRequestClosed", "mergeRequestReopened", "published", "custom"]>;
24
+
25
+ type NotificationType = (typeof notificationTypeEnum.enumValues)[number];
26
+ type NotificationPayload = {
27
+ id: string;
28
+ recipientId: string;
29
+ actorId: string | null;
30
+ type: NotificationType;
31
+ title: string;
32
+ body: string | null;
33
+ resourceType: string | null;
34
+ resourceId: string | null;
35
+ collection: string | null;
36
+ meta: Record<string, unknown> | null;
37
+ createdAt: Date;
38
+ };
39
+ type OnNotificationHandler = (notification: NotificationPayload) => void | Promise<void>;
40
+ type NotificationInput = Omit<NotificationPayload, 'id' | 'createdAt'>;
41
+
42
+ type NotificationService = ReturnType<typeof createNotificationService>;
43
+ declare function createNotificationService(db: DrizzleInstance, handlers: OnNotificationHandler[]): {
44
+ notify(input: NotificationInput): Promise<NotificationPayload>;
45
+ notifyMany(inputs: NotificationInput[]): Promise<NotificationPayload[]>;
46
+ };
47
+
48
+ /**
49
+ * Per-request scope produced by a ScopeConditionFactory.
50
+ * `where` — appended to SELECT/UPDATE/DELETE queries.
51
+ * `insertColumns` — snake_case column name → value pairs merged directly
52
+ * into the raw SQL INSERT via `scopedInsert` / `scopedInsertBatch`.
53
+ */
54
+ type TableScope = {
55
+ where?: SQL;
56
+ insertColumns?: Record<string, unknown>;
57
+ };
58
+ /**
59
+ * `roots` scope additionally supports a per-NEW-ENTRY column contributor: a
60
+ * plugin can compute fresh insert columns once per newly-created logical entry
61
+ * (e.g. a freshly minted translation-group id), which the static `insertColumns`
62
+ * channel (same value on every row) can't express. Called once per
63
+ * createRoot / root-duplication. Generic — core names no column. (Seam D.)
64
+ */
65
+ type RootTableScope = TableScope & {
66
+ newEntryColumns?: () => Record<string, unknown>;
67
+ /**
68
+ * Scope columns to EXCLUDE from cross-scope read filtering — columns the
69
+ * plugin varies INDEPENDENTLY of a query so that cross-scope reads (a
70
+ * reference/host/usage that legitimately spans them) must not filter on them.
71
+ * The i18n plugin declares `['language']` (a host/reference in any sibling
72
+ * language still counts; the read path already resolved a specific sibling).
73
+ * Generic — core names no column; passed to `rootScopeConditions` as its
74
+ * `exclude`. Empty/absent → every scope column filters. (Seam D6.)
75
+ */
76
+ crossScopeExclude?: readonly string[];
77
+ };
78
+ /**
79
+ * A plugin-provided resolver for reference values (rootId / group-key strings),
80
+ * carried on the resolved scope and consumed by the read path and the A/B
81
+ * co-render walk. Core ships an IDENTITY default (`coreReferenceResolver`)
82
+ * reproducing the single-language, no-plugin behaviour byte-for-byte; the i18n
83
+ * plugin supplies a real one that understands translation groups + the fallback
84
+ * chain. Core never names any i18n concept — it knows only this interface.
85
+ *
86
+ * `db` AND `scopeColumns` are passed PER CALL (not closed over): `db` so a
87
+ * caller inside a transaction (e.g. the A/B →running guard under FOR UPDATE)
88
+ * resolves against its own tx handle; `scopeColumns` because the MERGED root
89
+ * scope columns (tenant + language) exist only AFTER every scope factory has
90
+ * run — the i18n factory that builds the resolver sees only its OWN column at
91
+ * build time. The resolver therefore closes over just its resolution POLICY
92
+ * (e.g. the i18n active language + fallback chain). `scopeColumns` is the
93
+ * scope predicate; the resolver excludes its own cross-scope columns.
94
+ * (Seam B.)
95
+ */
96
+ type ReferenceResolver = {
97
+ /**
98
+ * Read-time render pick: stored reference value → the ONE rootId it renders
99
+ * as (omit a key to leave it unresolved). Identity default: `value → value`.
100
+ */
101
+ resolveRenderTargets(db: DrizzleInstance, scopeColumns: Record<string, unknown> | undefined, collection: string, storedValues: string[]): Promise<Map<string, string>>;
102
+ /**
103
+ * Conflict superset: stored reference keys → ALL rootIds they could render as
104
+ * (a group key expands to its whole group). Used by the A/B co-render walk;
105
+ * collection-agnostic (a reference may target any collection). Identity
106
+ * default: the existing, non-archived roots among `storedKeys` (by id).
107
+ */
108
+ resolveConflictTargets(db: DrizzleInstance, scopeColumns: Record<string, unknown> | undefined, storedKeys: string[]): Promise<string[]>;
109
+ /** rootIds → all their group siblings. Identity default: the input rootIds. */
110
+ expandGroup(db: DrizzleInstance, scopeColumns: Record<string, unknown> | undefined, rootIds: string[]): Promise<string[]>;
111
+ /** rootIds → the group keys a host could embed them by. Default: `[]`. */
112
+ groupKeysFor(db: DrizzleInstance, scopeColumns: Record<string, unknown> | undefined, rootIds: string[]): Promise<string[]>;
113
+ };
114
+ /** One variant branch of a running A/B test on a referenced root. */
115
+ type RunningAbTestVariant = {
116
+ branchId: string;
117
+ isControl: boolean;
118
+ };
119
+ /** A running A/B test on one root: the test plus its variant branches. */
120
+ type RunningAbTest = {
121
+ testId: string;
122
+ trafficPercentage: number;
123
+ variants: RunningAbTestVariant[];
124
+ };
125
+ /**
126
+ * A plugin-provided resolver that reports which referenced roots currently have
127
+ * a RUNNING A/B test (with that test's variant branches). Carried on the
128
+ * resolved scope and consumed by the read path's reference loader to fan the one
129
+ * XOR-guaranteed varying block's branches out to the client (AB_FANOUT F2). Core
130
+ * ships NO default — when absent (no ab-test plugin) the read path assumes no
131
+ * running tests and every embed stays on its deterministic single pick (F0).
132
+ * Core never names any A/B concept beyond this interface. (Seam F.)
133
+ */
134
+ type AbTestResolver = {
135
+ /**
136
+ * The subset of `rootIds` that have a running test, each mapped to its test +
137
+ * variant branches. `db` AND `scopeColumns` are passed PER CALL (same
138
+ * rationale as {@link ReferenceResolver}). The caller passes already
139
+ * render-resolved (active-language) rootIds, so this needs no group expansion.
140
+ */
141
+ runningTests(db: DrizzleInstance, scopeColumns: Record<string, unknown> | undefined, rootIds: string[]): Promise<Map<string, RunningAbTest>>;
142
+ };
143
+ type ResolvedScope = {
144
+ roots?: RootTableScope;
145
+ assets?: TableScope;
146
+ assetFolders?: TableScope;
147
+ redirects?: TableScope;
148
+ /**
149
+ * Plugin-provided reference resolver (i18n translation-group resolution). When
150
+ * absent, callers use core's identity default. Generic — see `ReferenceResolver`.
151
+ */
152
+ referenceResolver?: ReferenceResolver;
153
+ /**
154
+ * Plugin-provided running-A/B-test resolver (AB_FANOUT F2 server fan-out).
155
+ * When absent, the read path assumes no running tests. Generic — see
156
+ * {@link AbTestResolver}.
157
+ */
158
+ abTestResolver?: AbTestResolver;
159
+ /**
160
+ * Opaque per-plugin context slots, keyed by plugin id. Core never reads it;
161
+ * each plugin stashes its own per-request context here from a scope factory
162
+ * and reads it back via its own exported accessor. Merged generically in
163
+ * computeScope (shallow, last-writer-wins per slot).
164
+ */
165
+ pluginContext?: Record<string, unknown>;
166
+ };
167
+ /**
168
+ * Factory registered by plugins during `init`.
169
+ * Called once per request with the middleware result to produce
170
+ * table-level WHERE conditions and extra INSERT values.
171
+ */
172
+ type ScopeConditionFactory = (mwResult: MiddlewareResult) => ResolvedScope;
173
+ type BlockTypes = {
174
+ string: string;
175
+ number: number;
176
+ boolean: boolean;
177
+ date: string;
178
+ richText: string;
179
+ image: string;
180
+ select: string;
181
+ reference: string;
182
+ };
183
+ type BlockPropertyType = keyof BlockTypes;
184
+ type SelectOption = {
185
+ readonly label: string;
186
+ readonly value: string;
187
+ };
188
+ type BlockPropertySpec<T extends BlockPropertyType> = {
189
+ type: T;
190
+ required?: boolean;
191
+ defaultValue?: BlockTypes[T];
192
+ label: string;
193
+ description?: string;
194
+ placeholder?: string;
195
+ } & (T extends 'select' ? {
196
+ options: readonly SelectOption[];
197
+ } : {}) & (T extends 'reference' ? {
198
+ collection: string;
199
+ } : {});
200
+ /** Discriminated union over all concrete block-property specs. */
201
+ type BlockProperty = {
202
+ [K in BlockPropertyType]: BlockPropertySpec<K>;
203
+ }[BlockPropertyType];
204
+ /** Scalar property subset usable as an event parameter (no references/media). */
205
+ type ScalarBlockProperty = Extract<BlockProperty, {
206
+ type: 'string' | 'number' | 'boolean' | 'select' | 'date';
207
+ }>;
208
+ /**
209
+ * Declares a meaningful event a functional block can emit (e.g. a form's
210
+ * `submitSuccess`). Living on the block DEFINITION makes it the single source of
211
+ * truth for the typed `fire(...)` union, the test-creation goal picker, and the
212
+ * analytics wire name. `name` overrides the GA4/dataLayer wire name (defaults to
213
+ * `cms_<blockType>_<eventKey>`, computed by the measurement layer). Whether an
214
+ * event counts as a conversion is decided per test in the UI, not here.
215
+ */
216
+ type EventDeclaration = {
217
+ /** Analytics wire-name override (snake_case). Defaults to cms_<type>_<key>. */
218
+ name?: string;
219
+ /** Typed parameters carried with the event (scalar only). */
220
+ params?: Record<string, ScalarBlockProperty>;
221
+ /** Human label for the goal picker. */
222
+ label?: string;
223
+ };
224
+ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>, TEvents extends Record<string, EventDeclaration> = Record<string, never>> = {
225
+ properties: TProps;
226
+ label: string;
227
+ description?: string;
228
+ previewImageUrl?: string;
229
+ /** Events this (functional) block can emit — see {@link EventDeclaration}. */
230
+ events?: TEvents;
231
+ } & ({
232
+ allowChildren?: false;
233
+ } | {
234
+ allowChildren: true;
235
+ allowedChildBlocks?: string[];
236
+ });
237
+ type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
238
+ type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
239
+ properties: TProps;
240
+ };
241
+ type SlugConfig = {
242
+ enabled: false;
243
+ } | {
244
+ enabled: true;
245
+ root: string;
246
+ allowRoot?: boolean;
247
+ normalize?: boolean;
248
+ nested?: boolean;
249
+ };
250
+ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>, TBlocks extends Record<string, AnyBlockDefinition> = Record<string, AnyBlockDefinition>> = {
251
+ slug?: SlugConfig;
252
+ root: RootDefinition<TProps>;
253
+ blocks?: TBlocks;
254
+ label: string;
255
+ description?: string;
256
+ /**
257
+ * Marks this collection as one whose roots are meant to be EMBEDDED into other
258
+ * roots via a `reference` property (a "reusable block" library). Purely an
259
+ * ergonomic hint — it informs editor pickers and which endpoints to surface; it
260
+ * NEVER gates safety (the delete-in-use guard protects every referenced root
261
+ * regardless of this flag). Any collection can still be a reference target.
262
+ */
263
+ reusableBlock?: boolean;
264
+ };
265
+ type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
266
+ type CollectionWithName = Omit<AnyCollectionDefinition, 'blocks'> & {
267
+ name: string;
268
+ blocks: Record<string, AnyBlockDefinition>;
269
+ };
270
+ type DataRetentionConfig = {
271
+ keepDays: number;
272
+ keepMinCommits: number;
273
+ /**
274
+ * Grace period (days) before a soft-archived root (`archivedAt`) is physically
275
+ * hard-deleted by pruning. Defaults to `keepDays` when omitted — a trash
276
+ * window after which the page and its whole history are reclaimed.
277
+ */
278
+ archiveKeepDays?: number;
279
+ };
280
+ /** Result that user middleware can return to extend context */
281
+ type MiddlewareResult = {
282
+ userId?: string;
283
+ [key: string]: unknown;
284
+ };
285
+ /** Base ctx injected by withCMSContext middleware. */
286
+ type CMSProcedureCtx = {
287
+ db: DrizzleInstance;
288
+ collections: Record<string, CollectionWithName>;
289
+ dataRetention?: DataRetentionConfig;
290
+ scopeConditions?: ScopeConditionFactory[];
291
+ notificationService?: NotificationService;
292
+ resolvedUser?: ResolvedUserConfig;
293
+ };
294
+
295
+ type SchemaNamespace = 'cms';
296
+ type ColumnScalarType = 'text' | 'boolean' | 'integer' | 'timestamp' | 'jsonb' | 'tsvector';
297
+ type ColumnType<EnumTarget extends string = string> = ColumnScalarType | {
298
+ enum: EnumTarget;
299
+ };
300
+ type DefaultValue = {
301
+ kind: 'literal';
302
+ value: boolean | number | string | string[] | Record<string, unknown>;
303
+ } | {
304
+ kind: 'sql';
305
+ value: string;
306
+ };
307
+ type ForeignKeyAction = 'cascade' | 'restrict' | 'no action' | 'set null' | 'set default';
308
+ type IndexUsing = 'btree' | 'gin';
309
+ type ColumnDefinition<ReferenceTarget extends string = string, EnumTarget extends string = string> = {
310
+ type: ColumnType<EnumTarget>;
311
+ columnName?: string;
312
+ notNull?: boolean;
313
+ primaryKey?: boolean;
314
+ unique?: boolean;
315
+ default?: DefaultValue;
316
+ defaultId?: boolean;
317
+ defaultIdPrefix?: string;
318
+ defaultNow?: boolean;
319
+ jsonType?: string;
320
+ references?: {
321
+ table: ReferenceTarget;
322
+ column: string;
323
+ onDelete?: ForeignKeyAction;
324
+ onUpdate?: ForeignKeyAction;
325
+ };
326
+ };
327
+ type TableColumns<ReferenceTarget extends string = string, EnumTarget extends string = string> = Record<string, ColumnDefinition<ReferenceTarget, EnumTarget>>;
328
+ type IndexDefinition<ColumnName extends string> = {
329
+ columns: readonly ColumnName[];
330
+ unique?: boolean;
331
+ using?: IndexUsing;
332
+ where?: string;
333
+ };
334
+ type CompositePrimaryKey<ColumnName extends string> = {
335
+ columns: readonly ColumnName[];
336
+ };
337
+ type TableLevelForeignKey<ColumnName extends string = string> = {
338
+ columns: readonly ColumnName[];
339
+ foreignTable: string;
340
+ foreignColumns: readonly string[];
341
+ name?: string;
342
+ onDelete?: ForeignKeyAction;
343
+ onUpdate?: ForeignKeyAction;
344
+ };
345
+ type TableDefinition<Columns extends TableColumns = TableColumns, ReferenceTarget extends string = string, EnumTarget extends string = string> = {
346
+ tableName?: string;
347
+ indexPrefix?: string;
348
+ columns: Columns;
349
+ indexes?: Record<string, IndexDefinition<Extract<keyof Columns, string>>>;
350
+ compositePrimaryKey?: CompositePrimaryKey<Extract<keyof Columns, string>>;
351
+ foreignKeys?: TableLevelForeignKey<Extract<keyof Columns, string>>[];
352
+ };
353
+ type EnumDefinition = {
354
+ values: readonly string[];
355
+ enumName?: string;
356
+ };
357
+ type EnumMap = Record<string, EnumDefinition>;
358
+ type TableMap = Record<string, TableDefinition>;
359
+ type SchemaModule<_Namespace extends SchemaNamespace = SchemaNamespace, Tables extends TableMap = {}, Enums extends EnumMap = {}, Extensions = {}> = {
360
+ enums?: Enums;
361
+ tables?: Tables;
362
+ extend?: Extensions;
363
+ };
364
+
365
+ type Awaitable<T> = T | Promise<T>;
366
+ /**
367
+ * Endpoint key used as a hook action identifier.
368
+ * Accepts any string so internal plumbing doesn't need the full union,
369
+ * but the exported `CMSEndpointKey` (from `index.ts`) provides a narrowed
370
+ * union with autocomplete for hook authors.
371
+ */
372
+ type CMSHookAction = string & {};
373
+ type CMSBeforeHookContext = {
374
+ action: CMSHookAction;
375
+ collection: string;
376
+ db: DrizzleInstance;
377
+ input: Record<string, unknown>;
378
+ /**
379
+ * The resolved per-request scope (tenant/i18n), available to before-hooks so
380
+ * they can tenant-scope any cross-resource reads they perform. Set by the
381
+ * endpoint wrapper before hooks run; may be undefined for hooks invoked
382
+ * outside that path.
383
+ */
384
+ scope?: ResolvedScope;
385
+ };
386
+ type CMSAfterHookContext = CMSBeforeHookContext & {
387
+ result: unknown;
388
+ };
389
+ type CMSBeforeHook = {
390
+ action: CMSHookAction | '*';
391
+ collection?: string;
392
+ handler: (ctx: CMSBeforeHookContext) => Promise<void | {
393
+ override?: Record<string, unknown>;
394
+ }>;
395
+ };
396
+ type CMSAfterHook = {
397
+ action: CMSHookAction | '*';
398
+ collection?: string;
399
+ handler: (ctx: CMSAfterHookContext) => Promise<void | {
400
+ response: unknown;
401
+ }>;
402
+ };
403
+ type CMSHooks = {
404
+ before?: CMSBeforeHook[];
405
+ after?: CMSAfterHook[];
406
+ };
407
+ type CMSPluginContext = CMSProcedureCtx & {
408
+ collections: Record<string, CollectionWithName>;
409
+ };
410
+ type CMSCoreRootPruningPlan = {
411
+ rootId: string;
412
+ deletableCommitIds: string[];
413
+ deletableBlockVersionIds: string[];
414
+ deletableSnapshotCount: number;
415
+ deletableMergeRequestIds: string[];
416
+ deletableApprovalIds: string[];
417
+ initialCommitId: string;
418
+ };
419
+ type CMSPluginPruningMetrics = Record<string, number>;
420
+ type CMSPluginRootPruningPlan<TData = unknown> = {
421
+ rootId: string;
422
+ data?: TData;
423
+ metrics?: CMSPluginPruningMetrics;
424
+ };
425
+ type CMSPluginPruningPlanContext = Omit<CMSPluginContext, 'dataRetention'> & {
426
+ db: DrizzleInstance;
427
+ dataRetention: DataRetentionConfig;
428
+ rootPlan: CMSCoreRootPruningPlan;
429
+ };
430
+ type CMSPluginPruningExecuteContext<TData = unknown> = Omit<CMSPluginContext, 'db' | 'dataRetention'> & {
431
+ tx: DrizzleInstance;
432
+ dataRetention: DataRetentionConfig;
433
+ rootPlan: CMSCoreRootPruningPlan;
434
+ pluginPlan: CMSPluginRootPruningPlan<TData>;
435
+ };
436
+ type CMSPluginPruningExecuteResult = {
437
+ metrics?: CMSPluginPruningMetrics;
438
+ };
439
+ type CMSPluginPruning<TData = unknown> = {
440
+ plan: (ctx: CMSPluginPruningPlanContext) => Promise<CMSPluginRootPruningPlan<TData> | null>;
441
+ execute?: (ctx: CMSPluginPruningExecuteContext<TData>) => Promise<CMSPluginPruningExecuteResult | void>;
442
+ };
443
+ type CMSPluginInitOptions = {
444
+ hooks?: CMSHooks;
445
+ };
446
+ type CMSPluginInitResult = {
447
+ context?: Record<string, unknown>;
448
+ options?: Partial<CMSPluginInitOptions>;
449
+ };
450
+ /**
451
+ * A CMS plugin can extend the system with custom endpoints, hooks,
452
+ * middleware, database tables, and data-retention pruning logic.
453
+ *
454
+ * @example
455
+ * ```ts
456
+ * const myPlugin: CMSPlugin = {
457
+ * id: 'my-plugin',
458
+ * endpoints: { ... },
459
+ * hooks: { before: [...], after: [...] },
460
+ * async init(ctx) { return { context: { myService } }; },
461
+ * };
462
+ * ```
463
+ */
464
+ type CMSPlugin<TPruningData = unknown> = {
465
+ id: string;
466
+ endpoints?: Record<string, Endpoint>;
467
+ /**
468
+ * Per-COLLECTION endpoints contributed by the plugin: called once per
469
+ * collection during API assembly, returning routes merged into THAT
470
+ * collection's endpoint record (so they surface at `cms.api.<collection>.x`,
471
+ * not the flat `cms.api.<pluginId>.x`). The per-collection analogue of the
472
+ * flat `endpoints` above. Generic — any plugin can attach a route to every
473
+ * collection (the i18n plugin uses it for createTranslation / listTranslations).
474
+ * (Seam A.)
475
+ */
476
+ collectionEndpoints?: (def: CollectionWithName, ctx: CMSPluginContext) => Record<string, Endpoint>;
477
+ hooks?: {
478
+ before?: CMSBeforeHook[];
479
+ after?: CMSAfterHook[];
480
+ };
481
+ middlewares?: {
482
+ path: string;
483
+ middleware: Middleware;
484
+ }[];
485
+ schema?: SchemaModule;
486
+ pruning?: CMSPluginPruning<TPruningData>;
487
+ init?: (ctx: CMSPluginContext) => Awaitable<CMSPluginInitResult | void>;
488
+ onRequest?: (request: Request, ctx: CMSPluginContext) => Promise<{
489
+ response: Response;
490
+ } | {
491
+ request: Request;
492
+ } | void>;
493
+ onResponse?: (response: Response, ctx: CMSPluginContext) => Promise<{
494
+ response: Response;
495
+ } | void>;
496
+ onNotification?: OnNotificationHandler;
497
+ $ERROR_CODES?: Record<string, {
498
+ status: number;
499
+ message: string;
500
+ }>;
501
+ };
502
+
503
+ type ConsentSignal = 'granted' | 'denied';
504
+ /**
505
+ * The four Google Consent Mode v2 signals. Every major CMP (Cookiebot,
506
+ * Usercentrics, OneTrust) emits these, so a single inbound contract covers all.
507
+ * `analytics_storage` gates the A/B + analytics path; the `ad_*` signals gate
508
+ * any ad-related fan-out.
509
+ */
510
+ type ConsentState = {
511
+ analytics_storage: ConsentSignal;
512
+ ad_storage: ConsentSignal;
513
+ ad_user_data: ConsentSignal;
514
+ ad_personalization: ConsentSignal;
515
+ };
516
+ type ConsentPurpose = keyof ConsentState;
517
+
518
+ /** Default-deny: nothing is granted until a CMP / Consent Mode signal says so. */
519
+ declare const DENIED_ALL: ConsentState;
520
+ /**
521
+ * How long (ms) to buffer events before resolving the gate when no consent
522
+ * DECISION has arrived yet — the Consent Mode `wait_for_update` window. Render
523
+ * is NEVER blocked on this; only event emission waits.
524
+ */
525
+ declare const CONSENT_WAIT_MS = 2000;
526
+ /** `default` = the pre-interaction seed; `update` = a real consent decision. */
527
+ type ConsentMode = 'default' | 'update';
528
+ type ParsedConsentEntry = {
529
+ mode: ConsentMode;
530
+ state: Partial<ConsentState>;
531
+ };
532
+ /**
533
+ * Extracts the mode + partial {@link ConsentState} from a single dataLayer entry
534
+ * IF it is a Consent Mode command (`gtag('consent','default'|'update',{...})`,
535
+ * which lands on the dataLayer as an arguments-like `['consent', mode, params]`).
536
+ * Returns `null` for any non-consent entry. The `mode` matters: a `default` only
537
+ * seeds state, while an `update` is the user's decision (see {@link ConsentGate}).
538
+ */
539
+ declare function parseConsentEntry(entry: unknown): ParsedConsentEntry | null;
540
+ /** Parses every Consent Mode command on a dataLayer, in order. */
541
+ declare function parseConsentEntries(dataLayer: readonly unknown[]): ParsedConsentEntry[];
542
+ type ConsentGate = {
543
+ getState(): ConsentState;
544
+ isGranted(purpose: ConsentPurpose): boolean;
545
+ /** True once a real decision arrived or the wait-window elapsed. */
546
+ isResolved(): boolean;
547
+ /**
548
+ * Seed state from a Consent Mode `default` command. Updates state but does NOT
549
+ * resolve the gate — a denied default must not collapse the wait-window before
550
+ * the async CMP `update` arrives.
551
+ */
552
+ applyDefault(update: Partial<ConsentState>): void;
553
+ /**
554
+ * Apply a real consent decision — a Consent Mode `update` or an explicit host
555
+ * `setConsent`. Resolves + drains the buffer once the decision carries an
556
+ * `analytics_storage` value (a partial update touching only `ad_*` keeps the
557
+ * gate pending so a later analytics grant still flushes).
558
+ */
559
+ applyUpdate(update: Partial<ConsentState>): void;
560
+ /** Resolve the wait-window with whatever we have (stays default-deny). */
561
+ resolve(): void;
562
+ /**
563
+ * Queue an `analytics_storage`-gated side effect. Runs immediately if already
564
+ * resolved+granted, calls `onDrop` if resolved+denied, and buffers while
565
+ * pending. `onDrop` lets callers release a dedup guard so a later grant can
566
+ * re-fire.
567
+ */
568
+ run(effect: () => void, onDrop?: () => void): void;
569
+ /** Subscribe to state changes (apply / resolve / reset). Returns unsubscribe. */
570
+ onChange(listener: (state: ConsentState, resolved: boolean) => void): () => void;
571
+ /** Revoke consent (e.g. `abTest.reset()`): back to denied, stops fan-out. */
572
+ reset(): void;
573
+ };
574
+ declare function createConsentGate(initial?: ConsentState): ConsentGate;
575
+ /**
576
+ * Decides which visitor key to use and whether it may be persisted. Before
577
+ * `analytics_storage` is granted, the key is in-memory only (page lifetime, no
578
+ * device storage). On grant, an existing cookie wins; otherwise the in-memory
579
+ * key is promoted to the cookie so a buffered impression and later events share
580
+ * one identity.
581
+ */
582
+ declare function resolveVisitorKey(opts: {
583
+ granted: boolean;
584
+ cookieKey: string | null;
585
+ memKey: string | null;
586
+ generate: () => string;
587
+ }): {
588
+ key: string;
589
+ persist: boolean;
590
+ memKey: string;
591
+ };
592
+
593
+ /**
594
+ * Zero-config consent: reads Consent Mode v2 commands off `window.dataLayer`
595
+ * (already-present `default`/`update` entries and future pushes) and feeds the
596
+ * gate. Resilient to GTM/gtag.js loading LATER — which reassigns `dataLayer` /
597
+ * its `push` and would discard an in-place patch — via a short re-scan poll over
598
+ * the wait window that re-reads `window.dataLayer` fresh each tick (and re-scans
599
+ * from 0 if the array was replaced). The `push` patch is only a fast path. When
600
+ * running GTM, driving consent explicitly via `setConsent` from the CMP's
601
+ * Consent Mode update callback is the most reliable path.
602
+ */
603
+ declare function startConsentAutoRead(gate: ConsentGate): void;
604
+
605
+ /**
606
+ * The consent plugin. Owns the generic Google Consent Mode v2 infrastructure
607
+ * (the buffer-then-flush gate, the dataLayer/CMP auto-read, the state model)
608
+ * that any consumer can ride — A/B tracking, analytics sinks, or consent-gated
609
+ * rendering of embedded third-party content.
610
+ *
611
+ * Server-side it is a marker plugin (no schema/endpoints/hooks): consent is a
612
+ * client-side concern today. The client capability (gate + setConsent/getConsent
613
+ * + the <ConsentGate> render wrapper) is exposed via the client entry.
614
+ */
615
+ declare function consent(): CMSPlugin;
616
+
617
+ export { CONSENT_WAIT_MS, DENIED_ALL, consent, createConsentGate, parseConsentEntries, parseConsentEntry, resolveVisitorKey, startConsentAutoRead };
618
+ export type { ConsentGate, ConsentMode, ConsentPurpose, ConsentSignal, ConsentState, ParsedConsentEntry };