@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,50 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ var nanoid$1 = require('nanoid');
4
+
5
+ const nanoid = nanoid$1.customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 20);
6
+ const prefixes = {
7
+ root: 'rot',
8
+ commit: 'cmt',
9
+ branch: 'brn',
10
+ blockVersion: 'blv',
11
+ block: 'blk',
12
+ mergeRequest: 'mrq',
13
+ mergeConflict: 'mcf',
14
+ approval: 'apr',
15
+ assetFolder: 'afl',
16
+ asset: 'ast',
17
+ contentUsage: 'cus',
18
+ commentThread: 'cth',
19
+ commentMessage: 'cmg',
20
+ commentMention: 'cmn',
21
+ variable: 'var',
22
+ template: 'tpl',
23
+ tplVarUsage: 'tvu',
24
+ notification: 'ntf',
25
+ si: 'sid',
26
+ redirect: 'rdr'
27
+ };
28
+ const customPrefixes = new Map();
29
+ function registerIdPrefix(key, prefix) {
30
+ if (key in prefixes) {
31
+ throw new Error(`Cannot override core prefix "${key}"`);
32
+ }
33
+ if (prefix.length < 2 || prefix.length > 5) {
34
+ throw new Error(`Prefix "${prefix}" must be 2-5 characters`);
35
+ }
36
+ if (!/^[a-z]+$/.test(prefix)) {
37
+ throw new Error(`Prefix "${prefix}" must be lowercase letters only`);
38
+ }
39
+ customPrefixes.set(key, prefix);
40
+ }
41
+ function newId(prefix) {
42
+ const resolved = prefixes[prefix] ?? customPrefixes.get(prefix);
43
+ if (!resolved) {
44
+ throw new Error(`Unknown ID prefix "${prefix}". Register it with registerIdPrefix() first.`);
45
+ }
46
+ return `${resolved}_${nanoid()}`;
47
+ }
48
+
49
+ exports.newId = newId;
50
+ exports.registerIdPrefix = registerIdPrefix;
@@ -0,0 +1,29 @@
1
+ declare const prefixes: {
2
+ readonly root: "rot";
3
+ readonly commit: "cmt";
4
+ readonly branch: "brn";
5
+ readonly blockVersion: "blv";
6
+ readonly block: "blk";
7
+ readonly mergeRequest: "mrq";
8
+ readonly mergeConflict: "mcf";
9
+ readonly approval: "apr";
10
+ readonly assetFolder: "afl";
11
+ readonly asset: "ast";
12
+ readonly contentUsage: "cus";
13
+ readonly commentThread: "cth";
14
+ readonly commentMessage: "cmg";
15
+ readonly commentMention: "cmn";
16
+ readonly variable: "var";
17
+ readonly template: "tpl";
18
+ readonly tplVarUsage: "tvu";
19
+ readonly notification: "ntf";
20
+ readonly si: "sid";
21
+ readonly redirect: "rdr";
22
+ };
23
+ type CorePrefix = keyof typeof prefixes;
24
+ declare function registerIdPrefix(key: string, prefix: string): void;
25
+ type IdPrefix = CorePrefix | (string & {});
26
+ declare function newId(prefix: IdPrefix): string;
27
+
28
+ export { newId, registerIdPrefix };
29
+ export type { IdPrefix };
@@ -0,0 +1,29 @@
1
+ declare const prefixes: {
2
+ readonly root: "rot";
3
+ readonly commit: "cmt";
4
+ readonly branch: "brn";
5
+ readonly blockVersion: "blv";
6
+ readonly block: "blk";
7
+ readonly mergeRequest: "mrq";
8
+ readonly mergeConflict: "mcf";
9
+ readonly approval: "apr";
10
+ readonly assetFolder: "afl";
11
+ readonly asset: "ast";
12
+ readonly contentUsage: "cus";
13
+ readonly commentThread: "cth";
14
+ readonly commentMessage: "cmg";
15
+ readonly commentMention: "cmn";
16
+ readonly variable: "var";
17
+ readonly template: "tpl";
18
+ readonly tplVarUsage: "tvu";
19
+ readonly notification: "ntf";
20
+ readonly si: "sid";
21
+ readonly redirect: "rdr";
22
+ };
23
+ type CorePrefix = keyof typeof prefixes;
24
+ declare function registerIdPrefix(key: string, prefix: string): void;
25
+ type IdPrefix = CorePrefix | (string & {});
26
+ declare function newId(prefix: IdPrefix): string;
27
+
28
+ export { newId, registerIdPrefix };
29
+ export type { IdPrefix };
package/dist/nanoid.js ADDED
@@ -0,0 +1,47 @@
1
+ import { customAlphabet } from 'nanoid';
2
+
3
+ const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 20);
4
+ const prefixes = {
5
+ root: 'rot',
6
+ commit: 'cmt',
7
+ branch: 'brn',
8
+ blockVersion: 'blv',
9
+ block: 'blk',
10
+ mergeRequest: 'mrq',
11
+ mergeConflict: 'mcf',
12
+ approval: 'apr',
13
+ assetFolder: 'afl',
14
+ asset: 'ast',
15
+ contentUsage: 'cus',
16
+ commentThread: 'cth',
17
+ commentMessage: 'cmg',
18
+ commentMention: 'cmn',
19
+ variable: 'var',
20
+ template: 'tpl',
21
+ tplVarUsage: 'tvu',
22
+ notification: 'ntf',
23
+ si: 'sid',
24
+ redirect: 'rdr'
25
+ };
26
+ const customPrefixes = new Map();
27
+ function registerIdPrefix(key, prefix) {
28
+ if (key in prefixes) {
29
+ throw new Error(`Cannot override core prefix "${key}"`);
30
+ }
31
+ if (prefix.length < 2 || prefix.length > 5) {
32
+ throw new Error(`Prefix "${prefix}" must be 2-5 characters`);
33
+ }
34
+ if (!/^[a-z]+$/.test(prefix)) {
35
+ throw new Error(`Prefix "${prefix}" must be lowercase letters only`);
36
+ }
37
+ customPrefixes.set(key, prefix);
38
+ }
39
+ function newId(prefix) {
40
+ const resolved = prefixes[prefix] ?? customPrefixes.get(prefix);
41
+ if (!resolved) {
42
+ throw new Error(`Unknown ID prefix "${prefix}". Register it with registerIdPrefix() first.`);
43
+ }
44
+ return `${resolved}_${nanoid()}`;
45
+ }
46
+
47
+ export { newId, registerIdPrefix };
@@ -0,0 +1,60 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ var crypto = require('crypto');
4
+
5
+ function constantTimeEqual(a, b) {
6
+ const encoder = new TextEncoder();
7
+ const bufA = encoder.encode(a);
8
+ const bufB = encoder.encode(b);
9
+ if (bufA.byteLength !== bufB.byteLength) return false;
10
+ return crypto.timingSafeEqual(bufA, bufB);
11
+ }
12
+ /**
13
+ * Creates a Next.js API route handler for receiving CMS revalidation
14
+ * webhook events. Validates the shared secret using a constant-time
15
+ * comparison, then calls `revalidatePath` for each path in the event.
16
+ *
17
+ * @example
18
+ * ```ts
19
+ * // app/api/cms/revalidate/route.ts
20
+ * import { createRevalidateHandler } from '@createcms/core/next';
21
+ *
22
+ * export const POST = createRevalidateHandler({
23
+ * secret: process.env.REVALIDATION_SECRET!,
24
+ * });
25
+ * ```
26
+ */ function createRevalidateHandler(options) {
27
+ return async (request)=>{
28
+ const secret = request.headers.get('x-revalidate-secret');
29
+ if (!secret || !constantTimeEqual(secret, options.secret)) {
30
+ return Response.json({
31
+ error: 'Unauthorized'
32
+ }, {
33
+ status: 401
34
+ });
35
+ }
36
+ const event = await request.json();
37
+ const paths = event.paths ?? [];
38
+ const tags = event.tags ?? [];
39
+ if (paths.length > 0 || tags.length > 0) {
40
+ // Dynamic import avoids compile-time resolution of next/cache
41
+ // which is only available when next is installed as a peer dependency.
42
+ const nextCache = await Function('return import("next/cache")')();
43
+ for (const path of paths){
44
+ nextCache.revalidatePath(path);
45
+ }
46
+ // Tags invalidate a root's control + all its variant-coded cache entries
47
+ // (AB_FANOUT FA3b); the A/B render routes tag their fetch by rootRevalidateTag.
48
+ for (const tag of tags){
49
+ nextCache.revalidateTag(tag);
50
+ }
51
+ }
52
+ return Response.json({
53
+ revalidated: true,
54
+ paths,
55
+ tags
56
+ });
57
+ };
58
+ }
59
+
60
+ exports.createRevalidateHandler = createRevalidateHandler;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Endpoint key used as a hook action identifier.
3
+ * Accepts any string so internal plumbing doesn't need the full union,
4
+ * but the exported `CMSEndpointKey` (from `index.ts`) provides a narrowed
5
+ * union with autocomplete for hook authors.
6
+ */
7
+ type CMSHookAction = string & {};
8
+
9
+ type BlockTypes = {
10
+ string: string;
11
+ number: number;
12
+ boolean: boolean;
13
+ date: string;
14
+ richText: string;
15
+ image: string;
16
+ select: string;
17
+ reference: string;
18
+ };
19
+ type BlockPropertyType = keyof BlockTypes;
20
+ type SelectOption = {
21
+ readonly label: string;
22
+ readonly value: string;
23
+ };
24
+ type BlockPropertySpec<T extends BlockPropertyType> = {
25
+ type: T;
26
+ required?: boolean;
27
+ defaultValue?: BlockTypes[T];
28
+ label: string;
29
+ description?: string;
30
+ placeholder?: string;
31
+ } & (T extends 'select' ? {
32
+ options: readonly SelectOption[];
33
+ } : {}) & (T extends 'reference' ? {
34
+ collection: string;
35
+ } : {});
36
+ /** Discriminated union over all concrete block-property specs. */
37
+ type BlockProperty = {
38
+ [K in BlockPropertyType]: BlockPropertySpec<K>;
39
+ }[BlockPropertyType];
40
+ /** Scalar property subset usable as an event parameter (no references/media). */
41
+ type ScalarBlockProperty = Extract<BlockProperty, {
42
+ type: 'string' | 'number' | 'boolean' | 'select' | 'date';
43
+ }>;
44
+ /**
45
+ * Declares a meaningful event a functional block can emit (e.g. a form's
46
+ * `submitSuccess`). Living on the block DEFINITION makes it the single source of
47
+ * truth for the typed `fire(...)` union, the test-creation goal picker, and the
48
+ * analytics wire name. `name` overrides the GA4/dataLayer wire name (defaults to
49
+ * `cms_<blockType>_<eventKey>`, computed by the measurement layer). Whether an
50
+ * event counts as a conversion is decided per test in the UI, not here.
51
+ */
52
+ type EventDeclaration = {
53
+ /** Analytics wire-name override (snake_case). Defaults to cms_<type>_<key>. */
54
+ name?: string;
55
+ /** Typed parameters carried with the event (scalar only). */
56
+ params?: Record<string, ScalarBlockProperty>;
57
+ /** Human label for the goal picker. */
58
+ label?: string;
59
+ };
60
+ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>, TEvents extends Record<string, EventDeclaration> = Record<string, never>> = {
61
+ properties: TProps;
62
+ label: string;
63
+ description?: string;
64
+ previewImageUrl?: string;
65
+ /** Events this (functional) block can emit — see {@link EventDeclaration}. */
66
+ events?: TEvents;
67
+ } & ({
68
+ allowChildren?: false;
69
+ } | {
70
+ allowChildren: true;
71
+ allowedChildBlocks?: string[];
72
+ });
73
+ type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
74
+ type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
75
+ properties: TProps;
76
+ };
77
+ type SlugConfig = {
78
+ enabled: false;
79
+ } | {
80
+ enabled: true;
81
+ root: string;
82
+ allowRoot?: boolean;
83
+ normalize?: boolean;
84
+ nested?: boolean;
85
+ };
86
+ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>, TBlocks extends Record<string, AnyBlockDefinition> = Record<string, AnyBlockDefinition>> = {
87
+ slug?: SlugConfig;
88
+ root: RootDefinition<TProps>;
89
+ blocks?: TBlocks;
90
+ label: string;
91
+ description?: string;
92
+ /**
93
+ * Marks this collection as one whose roots are meant to be EMBEDDED into other
94
+ * roots via a `reference` property (a "reusable block" library). Purely an
95
+ * ergonomic hint — it informs editor pickers and which endpoints to surface; it
96
+ * NEVER gates safety (the delete-in-use guard protects every referenced root
97
+ * regardless of this flag). Any collection can still be a reference target.
98
+ */
99
+ reusableBlock?: boolean;
100
+ };
101
+ type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
102
+ type RevalidateEvent<TCollections extends Record<string, AnyCollectionDefinition> = Record<string, AnyCollectionDefinition>> = {
103
+ action: CMSHookAction;
104
+ collection: keyof TCollections & string;
105
+ rootId: string;
106
+ branchId: string;
107
+ slug: string | null;
108
+ paths: string[];
109
+ /**
110
+ * Next.js cache tags to revalidate alongside `paths` (AB_FANOUT FA3b). Always
111
+ * includes the affected root's tag (`rootRevalidateTag(rootId)`); the A/B
112
+ * variant-coded render routes tag their getPublishedContent fetch with it, so
113
+ * one `revalidateTag` invalidates a root's control + every variant cache entry
114
+ * (and, via cascade, its hosts) on a content change. Consumed by
115
+ * `createRevalidateHandler`.
116
+ */
117
+ tags?: string[];
118
+ };
119
+
120
+ type CreateRevalidateHandlerOptions = {
121
+ secret: string;
122
+ };
123
+ /**
124
+ * Creates a Next.js API route handler for receiving CMS revalidation
125
+ * webhook events. Validates the shared secret using a constant-time
126
+ * comparison, then calls `revalidatePath` for each path in the event.
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * // app/api/cms/revalidate/route.ts
131
+ * import { createRevalidateHandler } from '@createcms/core/next';
132
+ *
133
+ * export const POST = createRevalidateHandler({
134
+ * secret: process.env.REVALIDATION_SECRET!,
135
+ * });
136
+ * ```
137
+ */
138
+ declare function createRevalidateHandler(options: CreateRevalidateHandlerOptions): (request: Request) => Promise<Response>;
139
+
140
+ export { createRevalidateHandler };
141
+ export type { CreateRevalidateHandlerOptions, RevalidateEvent };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Endpoint key used as a hook action identifier.
3
+ * Accepts any string so internal plumbing doesn't need the full union,
4
+ * but the exported `CMSEndpointKey` (from `index.ts`) provides a narrowed
5
+ * union with autocomplete for hook authors.
6
+ */
7
+ type CMSHookAction = string & {};
8
+
9
+ type BlockTypes = {
10
+ string: string;
11
+ number: number;
12
+ boolean: boolean;
13
+ date: string;
14
+ richText: string;
15
+ image: string;
16
+ select: string;
17
+ reference: string;
18
+ };
19
+ type BlockPropertyType = keyof BlockTypes;
20
+ type SelectOption = {
21
+ readonly label: string;
22
+ readonly value: string;
23
+ };
24
+ type BlockPropertySpec<T extends BlockPropertyType> = {
25
+ type: T;
26
+ required?: boolean;
27
+ defaultValue?: BlockTypes[T];
28
+ label: string;
29
+ description?: string;
30
+ placeholder?: string;
31
+ } & (T extends 'select' ? {
32
+ options: readonly SelectOption[];
33
+ } : {}) & (T extends 'reference' ? {
34
+ collection: string;
35
+ } : {});
36
+ /** Discriminated union over all concrete block-property specs. */
37
+ type BlockProperty = {
38
+ [K in BlockPropertyType]: BlockPropertySpec<K>;
39
+ }[BlockPropertyType];
40
+ /** Scalar property subset usable as an event parameter (no references/media). */
41
+ type ScalarBlockProperty = Extract<BlockProperty, {
42
+ type: 'string' | 'number' | 'boolean' | 'select' | 'date';
43
+ }>;
44
+ /**
45
+ * Declares a meaningful event a functional block can emit (e.g. a form's
46
+ * `submitSuccess`). Living on the block DEFINITION makes it the single source of
47
+ * truth for the typed `fire(...)` union, the test-creation goal picker, and the
48
+ * analytics wire name. `name` overrides the GA4/dataLayer wire name (defaults to
49
+ * `cms_<blockType>_<eventKey>`, computed by the measurement layer). Whether an
50
+ * event counts as a conversion is decided per test in the UI, not here.
51
+ */
52
+ type EventDeclaration = {
53
+ /** Analytics wire-name override (snake_case). Defaults to cms_<type>_<key>. */
54
+ name?: string;
55
+ /** Typed parameters carried with the event (scalar only). */
56
+ params?: Record<string, ScalarBlockProperty>;
57
+ /** Human label for the goal picker. */
58
+ label?: string;
59
+ };
60
+ type BlockDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>, TEvents extends Record<string, EventDeclaration> = Record<string, never>> = {
61
+ properties: TProps;
62
+ label: string;
63
+ description?: string;
64
+ previewImageUrl?: string;
65
+ /** Events this (functional) block can emit — see {@link EventDeclaration}. */
66
+ events?: TEvents;
67
+ } & ({
68
+ allowChildren?: false;
69
+ } | {
70
+ allowChildren: true;
71
+ allowedChildBlocks?: string[];
72
+ });
73
+ type AnyBlockDefinition = BlockDefinition<Record<string, BlockProperty>, Record<string, EventDeclaration>>;
74
+ type RootDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>> = {
75
+ properties: TProps;
76
+ };
77
+ type SlugConfig = {
78
+ enabled: false;
79
+ } | {
80
+ enabled: true;
81
+ root: string;
82
+ allowRoot?: boolean;
83
+ normalize?: boolean;
84
+ nested?: boolean;
85
+ };
86
+ type CollectionDefinition<TProps extends Record<string, BlockProperty> = Record<string, BlockProperty>, TBlocks extends Record<string, AnyBlockDefinition> = Record<string, AnyBlockDefinition>> = {
87
+ slug?: SlugConfig;
88
+ root: RootDefinition<TProps>;
89
+ blocks?: TBlocks;
90
+ label: string;
91
+ description?: string;
92
+ /**
93
+ * Marks this collection as one whose roots are meant to be EMBEDDED into other
94
+ * roots via a `reference` property (a "reusable block" library). Purely an
95
+ * ergonomic hint — it informs editor pickers and which endpoints to surface; it
96
+ * NEVER gates safety (the delete-in-use guard protects every referenced root
97
+ * regardless of this flag). Any collection can still be a reference target.
98
+ */
99
+ reusableBlock?: boolean;
100
+ };
101
+ type AnyCollectionDefinition = CollectionDefinition<Record<string, BlockProperty>, Record<string, AnyBlockDefinition>>;
102
+ type RevalidateEvent<TCollections extends Record<string, AnyCollectionDefinition> = Record<string, AnyCollectionDefinition>> = {
103
+ action: CMSHookAction;
104
+ collection: keyof TCollections & string;
105
+ rootId: string;
106
+ branchId: string;
107
+ slug: string | null;
108
+ paths: string[];
109
+ /**
110
+ * Next.js cache tags to revalidate alongside `paths` (AB_FANOUT FA3b). Always
111
+ * includes the affected root's tag (`rootRevalidateTag(rootId)`); the A/B
112
+ * variant-coded render routes tag their getPublishedContent fetch with it, so
113
+ * one `revalidateTag` invalidates a root's control + every variant cache entry
114
+ * (and, via cascade, its hosts) on a content change. Consumed by
115
+ * `createRevalidateHandler`.
116
+ */
117
+ tags?: string[];
118
+ };
119
+
120
+ type CreateRevalidateHandlerOptions = {
121
+ secret: string;
122
+ };
123
+ /**
124
+ * Creates a Next.js API route handler for receiving CMS revalidation
125
+ * webhook events. Validates the shared secret using a constant-time
126
+ * comparison, then calls `revalidatePath` for each path in the event.
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * // app/api/cms/revalidate/route.ts
131
+ * import { createRevalidateHandler } from '@createcms/core/next';
132
+ *
133
+ * export const POST = createRevalidateHandler({
134
+ * secret: process.env.REVALIDATION_SECRET!,
135
+ * });
136
+ * ```
137
+ */
138
+ declare function createRevalidateHandler(options: CreateRevalidateHandlerOptions): (request: Request) => Promise<Response>;
139
+
140
+ export { createRevalidateHandler };
141
+ export type { CreateRevalidateHandlerOptions, RevalidateEvent };
@@ -0,0 +1,58 @@
1
+ import { timingSafeEqual } from 'crypto';
2
+
3
+ function constantTimeEqual(a, b) {
4
+ const encoder = new TextEncoder();
5
+ const bufA = encoder.encode(a);
6
+ const bufB = encoder.encode(b);
7
+ if (bufA.byteLength !== bufB.byteLength) return false;
8
+ return timingSafeEqual(bufA, bufB);
9
+ }
10
+ /**
11
+ * Creates a Next.js API route handler for receiving CMS revalidation
12
+ * webhook events. Validates the shared secret using a constant-time
13
+ * comparison, then calls `revalidatePath` for each path in the event.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * // app/api/cms/revalidate/route.ts
18
+ * import { createRevalidateHandler } from '@createcms/core/next';
19
+ *
20
+ * export const POST = createRevalidateHandler({
21
+ * secret: process.env.REVALIDATION_SECRET!,
22
+ * });
23
+ * ```
24
+ */ function createRevalidateHandler(options) {
25
+ return async (request)=>{
26
+ const secret = request.headers.get('x-revalidate-secret');
27
+ if (!secret || !constantTimeEqual(secret, options.secret)) {
28
+ return Response.json({
29
+ error: 'Unauthorized'
30
+ }, {
31
+ status: 401
32
+ });
33
+ }
34
+ const event = await request.json();
35
+ const paths = event.paths ?? [];
36
+ const tags = event.tags ?? [];
37
+ if (paths.length > 0 || tags.length > 0) {
38
+ // Dynamic import avoids compile-time resolution of next/cache
39
+ // which is only available when next is installed as a peer dependency.
40
+ const nextCache = await Function('return import("next/cache")')();
41
+ for (const path of paths){
42
+ nextCache.revalidatePath(path);
43
+ }
44
+ // Tags invalidate a root's control + all its variant-coded cache entries
45
+ // (AB_FANOUT FA3b); the A/B render routes tag their fetch by rootRevalidateTag.
46
+ for (const tag of tags){
47
+ nextCache.revalidateTag(tag);
48
+ }
49
+ }
50
+ return Response.json({
51
+ revalidated: true,
52
+ paths,
53
+ tags
54
+ });
55
+ };
56
+ }
57
+
58
+ export { createRevalidateHandler };
@@ -0,0 +1,113 @@
1
+ Object.defineProperty(exports, '__esModule', { value: true });
2
+
3
+ var server = require('next/server');
4
+ var index_cjs = require('../ab-edge/index.cjs');
5
+
6
+ // ============================================================================
7
+ // AB_FANOUT — Next.js edge middleware (Pattern A: cache-per-variant)
8
+ // ============================================================================
9
+ //
10
+ // A THIN adapter over the framework-agnostic core in `@createcms/core/ab-edge`:
11
+ // supplies the Next primitives (NextRequest cookies, NextResponse.rewrite, the
12
+ // resolve fetch). The bucketing + rewrite decision lives in `decideEdgeVariant`.
13
+ // EDGE-SAFE (only `next/server` + the core; no Node built-ins).
14
+ //
15
+ // ALWAYS-REWRITE + CONSENT-FREE: every request is rewritten to
16
+ // `<prefix>/<code><pathname>` (the assigned branch, or the control sentinel) so
17
+ // it lands on the single `[abVariant]` route. The variant is kept consistent via
18
+ // a VARIANT-ONLY cookie `ab_<testId>=<code>` — no visitor id, no behavioural
19
+ // data, no third-party transmission → ePrivacy "strictly necessary" exemption,
20
+ // so fresh ad traffic gets real variants (not always control). The consent-gated
21
+ // pieces (persistent visitor id, GA4/dataLayer forwarding) live in the client
22
+ // pipeline, NOT here. There is NO passthrough, so scope this to PUBLIC CMS paths
23
+ // only (compose it in your proxy after the auth gate).
24
+ const DEFAULT_CMS_BASE = '/api/cms';
25
+ const DEFAULT_VARIANT_COOKIE_PREFIX = 'ab_';
26
+ const THIRTY_DAYS_SEC = 2_592_000;
27
+ async function defaultResolve(request, options, path) {
28
+ const base = options.cmsBaseUrl ?? DEFAULT_CMS_BASE;
29
+ const url = new URL(`${base}/${options.collection}/resolveAbVariant`, request.nextUrl.origin);
30
+ url.searchParams.set('path', path);
31
+ try {
32
+ const res = await fetch(url, {
33
+ headers: {
34
+ cookie: request.headers.get('cookie') ?? ''
35
+ }
36
+ });
37
+ if (!res.ok) return {
38
+ test: null
39
+ };
40
+ return await res.json();
41
+ } catch {
42
+ return {
43
+ test: null
44
+ }; // fail open to control — render/paint never blocks
45
+ }
46
+ }
47
+ /**
48
+ * Next.js Pattern A A/B fan-out — ALWAYS rewrites the request to the variant-
49
+ * coded `[abVariant]` route. Compose it inside your `proxy.ts` (Next 16) AFTER
50
+ * the auth gate, and only for PUBLIC CMS paths (it has no passthrough):
51
+ * ```ts
52
+ * // proxy.ts
53
+ * const abTest = abTestMiddleware({ collection: 'pages' });
54
+ * export default async function proxy(request) {
55
+ * if (isProtected(request)) return authGate(request); // not rewritten
56
+ * return abTest(request); // public CMS content → /ab/<code>/<path>
57
+ * }
58
+ * ```
59
+ */ function abTestMiddleware(options) {
60
+ const controlCode = options.controlCode ?? index_cjs.CONTROL_CODE;
61
+ const variantPrefix = options.variantPrefix ?? index_cjs.DEFAULT_VARIANT_PREFIX;
62
+ const cookiePrefix = options.variantCookiePrefix ?? DEFAULT_VARIANT_COOKIE_PREFIX;
63
+ const cookieMaxAge = options.variantCookieMaxAge ?? THIRTY_DAYS_SEC;
64
+ const resolve = options.resolve ?? ((req, path)=>defaultResolve(req, options, path));
65
+ return async (request)=>{
66
+ const { pathname } = request.nextUrl;
67
+ // Resolve, reuse-or-assign the variant, then ALWAYS rewrite to
68
+ // `<prefix>/<code><pathname>`. Any failure fails closed to the control code,
69
+ // so the request still lands on the `[abVariant]` route (never a 404).
70
+ let rewritePath;
71
+ let setCookie = null;
72
+ try {
73
+ const resolved = await resolve(request, pathname);
74
+ const testId = resolved.test?.testId ?? null;
75
+ const assignedCode = testId ? request.cookies.get(`${cookiePrefix}${testId}`)?.value ?? null : null;
76
+ const decision = index_cjs.decideEdgeVariant({
77
+ pathname,
78
+ resolve: resolved,
79
+ assignedCode,
80
+ controlCode,
81
+ variantPrefix
82
+ });
83
+ rewritePath = decision.rewritePath;
84
+ // A first assignment → persist the chosen variant code (variant-only).
85
+ if (decision.assignCode && decision.testId) {
86
+ setCookie = {
87
+ name: `${cookiePrefix}${decision.testId}`,
88
+ value: decision.assignCode
89
+ };
90
+ }
91
+ } catch {
92
+ rewritePath = index_cjs.variantRewritePath(variantPrefix, controlCode, pathname);
93
+ }
94
+ const rewriteUrl = request.nextUrl.clone();
95
+ rewriteUrl.pathname = rewritePath; // transparent — browser URL unchanged
96
+ const response = server.NextResponse.rewrite(rewriteUrl);
97
+ if (setCookie) {
98
+ response.cookies.set(setCookie.name, setCookie.value, {
99
+ path: '/',
100
+ maxAge: cookieMaxAge,
101
+ sameSite: 'lax',
102
+ secure: true,
103
+ // httpOnly: the cookie is sent with every request, so cross-page
104
+ // conversion attribution reads it SERVER-side (no client read needed) —
105
+ // keep it httpOnly to harden against XSS. It holds only the variant code.
106
+ httpOnly: true
107
+ });
108
+ }
109
+ return response;
110
+ };
111
+ }
112
+
113
+ exports.abTestMiddleware = abTestMiddleware;