@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.
- package/README.md +169 -0
- package/dist/ab-edge/index.cjs +214 -0
- package/dist/ab-edge/index.d.cts +121 -0
- package/dist/ab-edge/index.d.ts +121 -0
- package/dist/ab-edge/index.js +205 -0
- package/dist/bin/createcms.js +3082 -0
- package/dist/db.cjs +496 -0
- package/dist/db.d.cts +128 -0
- package/dist/db.d.ts +128 -0
- package/dist/db.js +488 -0
- package/dist/index.cjs +13789 -0
- package/dist/index.d.cts +10277 -0
- package/dist/index.d.ts +10277 -0
- package/dist/index.js +13737 -0
- package/dist/nanoid.cjs +50 -0
- package/dist/nanoid.d.cts +29 -0
- package/dist/nanoid.d.ts +29 -0
- package/dist/nanoid.js +47 -0
- package/dist/next/index.cjs +60 -0
- package/dist/next/index.d.cts +141 -0
- package/dist/next/index.d.ts +141 -0
- package/dist/next/index.js +58 -0
- package/dist/next/middleware.cjs +113 -0
- package/dist/next/middleware.d.cts +77 -0
- package/dist/next/middleware.d.ts +77 -0
- package/dist/next/middleware.js +111 -0
- package/dist/plugins/ab-test/analytics/upstash.cjs +345 -0
- package/dist/plugins/ab-test/analytics/upstash.d.cts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.d.ts +193 -0
- package/dist/plugins/ab-test/analytics/upstash.js +343 -0
- package/dist/plugins/ab-test/client.cjs +686 -0
- package/dist/plugins/ab-test/client.d.cts +233 -0
- package/dist/plugins/ab-test/client.d.ts +233 -0
- package/dist/plugins/ab-test/client.js +684 -0
- package/dist/plugins/ab-test/index.cjs +3400 -0
- package/dist/plugins/ab-test/index.d.cts +1131 -0
- package/dist/plugins/ab-test/index.d.ts +1131 -0
- package/dist/plugins/ab-test/index.js +3367 -0
- package/dist/plugins/client.cjs +20 -0
- package/dist/plugins/client.d.cts +3 -0
- package/dist/plugins/client.d.ts +3 -0
- package/dist/plugins/client.js +3 -0
- package/dist/plugins/consent/client.cjs +315 -0
- package/dist/plugins/consent/client.d.cts +145 -0
- package/dist/plugins/consent/client.d.ts +145 -0
- package/dist/plugins/consent/client.js +313 -0
- package/dist/plugins/consent/index.cjs +267 -0
- package/dist/plugins/consent/index.d.cts +618 -0
- package/dist/plugins/consent/index.d.ts +618 -0
- package/dist/plugins/consent/index.js +258 -0
- package/dist/plugins/i18n/index.cjs +2177 -0
- package/dist/plugins/i18n/index.d.cts +562 -0
- package/dist/plugins/i18n/index.d.ts +562 -0
- package/dist/plugins/i18n/index.js +2150 -0
- package/dist/plugins/media-optimize/index.cjs +315 -0
- package/dist/plugins/media-optimize/index.d.cts +144 -0
- package/dist/plugins/media-optimize/index.d.ts +144 -0
- package/dist/plugins/media-optimize/index.js +311 -0
- package/dist/plugins/multi-tenant/index.cjs +210 -0
- package/dist/plugins/multi-tenant/index.d.cts +431 -0
- package/dist/plugins/multi-tenant/index.d.ts +431 -0
- package/dist/plugins/multi-tenant/index.js +207 -0
- package/dist/plugins/server.cjs +24 -0
- package/dist/plugins/server.d.cts +3 -0
- package/dist/plugins/server.d.ts +3 -0
- package/dist/plugins/server.js +3 -0
- package/dist/react/blocks.cjs +233 -0
- package/dist/react/blocks.d.cts +320 -0
- package/dist/react/blocks.d.ts +320 -0
- package/dist/react/blocks.js +226 -0
- package/dist/react/index.cjs +901 -0
- package/dist/react/index.d.cts +992 -0
- package/dist/react/index.d.ts +992 -0
- package/dist/react/index.js +872 -0
- package/dist/react/tracking.cjs +243 -0
- package/dist/react/tracking.d.cts +364 -0
- package/dist/react/tracking.d.ts +364 -0
- package/dist/react/tracking.js +216 -0
- package/dist/react/variant.cjs +59 -0
- package/dist/react/variant.d.cts +26 -0
- package/dist/react/variant.d.ts +26 -0
- package/dist/react/variant.js +57 -0
- package/package.json +303 -0
package/dist/nanoid.cjs
ADDED
|
@@ -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 };
|
package/dist/nanoid.d.ts
ADDED
|
@@ -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;
|