@bettercms-ai/sdk 1.33.0 → 1.35.0
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/dist/draft.cjs +32 -0
- package/dist/draft.cjs.map +1 -1
- package/dist/draft.d.cts +15 -1
- package/dist/draft.d.ts +15 -1
- package/dist/draft.js +29 -0
- package/dist/draft.js.map +1 -1
- package/dist/index.d.cts +5 -392
- package/dist/index.d.ts +5 -392
- package/dist/read-client-tNsCHy0p.d.cts +357 -0
- package/dist/read-client-tNsCHy0p.d.ts +357 -0
- package/dist/read.cjs +457 -0
- package/dist/read.cjs.map +1 -0
- package/dist/read.d.cts +40 -0
- package/dist/read.d.ts +40 -0
- package/dist/read.js +423 -0
- package/dist/read.js.map +1 -0
- package/dist/sections.cjs +516 -0
- package/dist/sections.cjs.map +1 -0
- package/dist/sections.d.cts +49 -0
- package/dist/sections.d.ts +49 -0
- package/dist/sections.js +486 -0
- package/dist/sections.js.map +1 -0
- package/package.json +11 -1
package/dist/draft.cjs
CHANGED
|
@@ -22,6 +22,9 @@ var draft_exports = {};
|
|
|
22
22
|
__export(draft_exports, {
|
|
23
23
|
DRAFT_COOKIE: () => DRAFT_COOKIE,
|
|
24
24
|
decodeTokenSlug: () => decodeTokenSlug,
|
|
25
|
+
draftDisableResponse: () => draftDisableResponse,
|
|
26
|
+
draftEnableResponse: () => draftEnableResponse,
|
|
27
|
+
draftTokenFrom: () => draftTokenFrom,
|
|
25
28
|
previewTokenValid: () => previewTokenValid,
|
|
26
29
|
safeRedirectPath: () => safeRedirectPath,
|
|
27
30
|
signValue: () => signValue,
|
|
@@ -74,10 +77,39 @@ async function previewTokenValid(apiUrl, token, fetchImpl = fetch) {
|
|
|
74
77
|
});
|
|
75
78
|
return res.ok;
|
|
76
79
|
}
|
|
80
|
+
async function draftTokenFrom(cookieHeader, secret) {
|
|
81
|
+
if (!cookieHeader || !secret) return null;
|
|
82
|
+
const m = new RegExp(`(?:^|;\\s*)${DRAFT_COOKIE}=([^;]+)`).exec(cookieHeader);
|
|
83
|
+
if (!m) return null;
|
|
84
|
+
try {
|
|
85
|
+
return await verifyValue(decodeURIComponent(m[1]), secret);
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function draftEnableResponse(request, opts) {
|
|
91
|
+
const url = new URL(request.url);
|
|
92
|
+
const token = url.searchParams.get("token");
|
|
93
|
+
if (!opts.secret) return new Response("BCMS_DRAFT_SECRET is not configured", { status: 500 });
|
|
94
|
+
if (!token) return new Response("Missing token", { status: 400 });
|
|
95
|
+
if (!await previewTokenValid(opts.apiUrl, token, opts.fetchImpl)) return new Response("Invalid or expired token", { status: 401 });
|
|
96
|
+
const cookie = `${DRAFT_COOKIE}=${encodeURIComponent(await signValue(token, opts.secret))}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400${url.protocol === "https:" ? "; Secure" : ""}`;
|
|
97
|
+
return new Response(null, { status: 302, headers: { Location: safeRedirectPath(url.searchParams.get("redirect") ?? "/"), "Set-Cookie": cookie } });
|
|
98
|
+
}
|
|
99
|
+
function draftDisableResponse(request) {
|
|
100
|
+
const url = new URL(request.url);
|
|
101
|
+
return new Response(null, {
|
|
102
|
+
status: 302,
|
|
103
|
+
headers: { Location: safeRedirectPath(url.searchParams.get("redirect") ?? "/"), "Set-Cookie": `${DRAFT_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0` }
|
|
104
|
+
});
|
|
105
|
+
}
|
|
77
106
|
// Annotate the CommonJS export names for ESM import in node:
|
|
78
107
|
0 && (module.exports = {
|
|
79
108
|
DRAFT_COOKIE,
|
|
80
109
|
decodeTokenSlug,
|
|
110
|
+
draftDisableResponse,
|
|
111
|
+
draftEnableResponse,
|
|
112
|
+
draftTokenFrom,
|
|
81
113
|
previewTokenValid,
|
|
82
114
|
safeRedirectPath,
|
|
83
115
|
signValue,
|
package/dist/draft.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/draft.ts"],"sourcesContent":["/**\n * Draft mode, shared by every framework adapter: the signed cookie that says \"this visitor is\n * previewing\", the preview token's slug, and the redirect a draft route may follow.\n *\n * Web Crypto and `atob` only, so it runs in Node, edge runtimes and workers alike. Each adapter\n * mounts its own routes around these; the rules live here once.\n */\n\n/** The draft cookie every adapter sets: one name, so an editor link works on any framework. */\nexport const DRAFT_COOKIE = \"bcms-draft\";\n\nconst enc = new TextEncoder();\n\nfunction base64url(bytes: ArrayBuffer): string {\n const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));\n return b.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmac(value: string, secret: string): Promise<string> {\n const key = await crypto.subtle.importKey(\"raw\", enc.encode(secret), { name: \"HMAC\", hash: \"SHA-256\" }, false, [\"sign\"]);\n return base64url(await crypto.subtle.sign(\"HMAC\", key, enc.encode(value)));\n}\n\n/** Sign a value as `value.sig` for tamper-evident cookie storage (HMAC-SHA256, base64url). */\nexport async function signValue(value: string, secret: string): Promise<string> {\n return `${value}.${await hmac(value, secret)}`;\n}\n\n/** The original value of a signed one, or null when it was tampered with. */\nexport async function verifyValue(signed: string, secret: string): Promise<string | null> {\n const idx = signed.lastIndexOf(\".\");\n if (idx < 0) return null;\n const value = signed.slice(0, idx);\n const sig = signed.slice(idx + 1);\n const expected = await hmac(value, secret);\n if (sig.length !== expected.length) return null;\n let diff = 0;\n for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);\n return diff === 0 ? value : null;\n}\n\n/** Only same-origin relative paths: blocks \"//evil.com\", \"https://evil.com\" and \"/\\\\evil.com\". */\nexport function safeRedirectPath(to: string): string {\n return to.startsWith(\"/\") && !to.startsWith(\"//\") && !to.startsWith(\"/\\\\\") ? to : \"/\";\n}\n\n/** A preview token's `entrySlug`, read from its payload UNVERIFIED (the backend verifies it). */\nexport function decodeTokenSlug(token: string): string | null {\n try {\n const part = token.split(\".\")[1];\n if (!part) return null;\n // atob gives bytes as Latin-1: decoded as UTF-8, a slug like `über-uns` survives.\n const bytes = Uint8Array.from(atob(part.replace(/-/g, \"+\").replace(/_/g, \"/\")), (c) => c.charCodeAt(0));\n const json = JSON.parse(new TextDecoder().decode(bytes)) as { entrySlug?: string };\n return json.entrySlug ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Ask the backend whether a preview token is good: the backend is the signature authority, so a\n * draft route enables draft mode only on its yes.\n */\nexport async function previewTokenValid(apiUrl: string, token: string, fetchImpl: typeof fetch = fetch): Promise<boolean> {\n const slug = decodeTokenSlug(token);\n if (!slug) return false;\n const res = await fetchImpl(`${apiUrl.replace(/\\/+$/, \"\")}/api/v1/preview/${encodeURIComponent(slug)}?token=${encodeURIComponent(token)}`);\n await res.body?.cancel().catch(() => {});\n return res.ok;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASO,IAAM,eAAe;AAE5B,IAAM,MAAM,IAAI,YAAY;AAE5B,SAAS,UAAU,OAA4B;AAC7C,QAAM,IAAI,KAAK,OAAO,aAAa,GAAG,IAAI,WAAW,KAAK,CAAC,CAAC;AAC5D,SAAO,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AACpE;AAEA,eAAe,KAAK,OAAe,QAAiC;AAClE,QAAM,MAAM,MAAM,OAAO,OAAO,UAAU,OAAO,IAAI,OAAO,MAAM,GAAG,EAAE,MAAM,QAAQ,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;AACvH,SAAO,UAAU,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,IAAI,OAAO,KAAK,CAAC,CAAC;AAC3E;AAGA,eAAsB,UAAU,OAAe,QAAiC;AAC9E,SAAO,GAAG,KAAK,IAAI,MAAM,KAAK,OAAO,MAAM,CAAC;AAC9C;AAGA,eAAsB,YAAY,QAAgB,QAAwC;AACxF,QAAM,MAAM,OAAO,YAAY,GAAG;AAClC,MAAI,MAAM,EAAG,QAAO;AACpB,QAAM,QAAQ,OAAO,MAAM,GAAG,GAAG;AACjC,QAAM,MAAM,OAAO,MAAM,MAAM,CAAC;AAChC,QAAM,WAAW,MAAM,KAAK,OAAO,MAAM;AACzC,MAAI,IAAI,WAAW,SAAS,OAAQ,QAAO;AAC3C,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,IAAK,SAAQ,IAAI,WAAW,CAAC,IAAI,SAAS,WAAW,CAAC;AACtF,SAAO,SAAS,IAAI,QAAQ;AAC9B;AAGO,SAAS,iBAAiB,IAAoB;AACnD,SAAO,GAAG,WAAW,GAAG,KAAK,CAAC,GAAG,WAAW,IAAI,KAAK,CAAC,GAAG,WAAW,KAAK,IAAI,KAAK;AACpF;AAGO,SAAS,gBAAgB,OAA8B;AAC5D,MAAI;AACF,UAAM,OAAO,MAAM,MAAM,GAAG,EAAE,CAAC;AAC/B,QAAI,CAAC,KAAM,QAAO;AAElB,UAAM,QAAQ,WAAW,KAAK,KAAK,KAAK,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AACtG,UAAM,OAAO,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,KAAK,CAAC;AACvD,WAAO,KAAK,aAAa;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,eAAsB,kBAAkB,QAAgB,OAAe,YAA0B,OAAyB;AACxH,QAAM,OAAO,gBAAgB,KAAK;AAClC,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,MAAM,MAAM,UAAU,GAAG,OAAO,QAAQ,QAAQ,EAAE,CAAC,mBAAmB,mBAAmB,IAAI,CAAC,UAAU,mBAAmB,KAAK,CAAC,EAAE;AACzI,QAAM,IAAI,MAAM,OAAO,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AACvC,SAAO,IAAI;AACb;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/draft.ts"],"sourcesContent":["/**\n * Draft mode, shared by every framework adapter: the signed cookie that says \"this visitor is\n * previewing\", the preview token's slug, and the redirect a draft route may follow.\n *\n * Web Crypto and `atob` only, so it runs in Node, edge runtimes and workers alike. Each adapter\n * mounts its own routes around these; the rules live here once.\n */\n\n/** The draft cookie every adapter sets: one name, so an editor link works on any framework. */\nexport const DRAFT_COOKIE = \"bcms-draft\";\n\nconst enc = new TextEncoder();\n\nfunction base64url(bytes: ArrayBuffer): string {\n const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));\n return b.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmac(value: string, secret: string): Promise<string> {\n const key = await crypto.subtle.importKey(\"raw\", enc.encode(secret), { name: \"HMAC\", hash: \"SHA-256\" }, false, [\"sign\"]);\n return base64url(await crypto.subtle.sign(\"HMAC\", key, enc.encode(value)));\n}\n\n/** Sign a value as `value.sig` for tamper-evident cookie storage (HMAC-SHA256, base64url). */\nexport async function signValue(value: string, secret: string): Promise<string> {\n return `${value}.${await hmac(value, secret)}`;\n}\n\n/** The original value of a signed one, or null when it was tampered with. */\nexport async function verifyValue(signed: string, secret: string): Promise<string | null> {\n const idx = signed.lastIndexOf(\".\");\n if (idx < 0) return null;\n const value = signed.slice(0, idx);\n const sig = signed.slice(idx + 1);\n const expected = await hmac(value, secret);\n if (sig.length !== expected.length) return null;\n let diff = 0;\n for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);\n return diff === 0 ? value : null;\n}\n\n/** Only same-origin relative paths: blocks \"//evil.com\", \"https://evil.com\" and \"/\\\\evil.com\". */\nexport function safeRedirectPath(to: string): string {\n return to.startsWith(\"/\") && !to.startsWith(\"//\") && !to.startsWith(\"/\\\\\") ? to : \"/\";\n}\n\n/** A preview token's `entrySlug`, read from its payload UNVERIFIED (the backend verifies it). */\nexport function decodeTokenSlug(token: string): string | null {\n try {\n const part = token.split(\".\")[1];\n if (!part) return null;\n // atob gives bytes as Latin-1: decoded as UTF-8, a slug like `über-uns` survives.\n const bytes = Uint8Array.from(atob(part.replace(/-/g, \"+\").replace(/_/g, \"/\")), (c) => c.charCodeAt(0));\n const json = JSON.parse(new TextDecoder().decode(bytes)) as { entrySlug?: string };\n return json.entrySlug ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Ask the backend whether a preview token is good: the backend is the signature authority, so a\n * draft route enables draft mode only on its yes.\n */\nexport async function previewTokenValid(apiUrl: string, token: string, fetchImpl: typeof fetch = fetch): Promise<boolean> {\n const slug = decodeTokenSlug(token);\n if (!slug) return false;\n const res = await fetchImpl(`${apiUrl.replace(/\\/+$/, \"\")}/api/v1/preview/${encodeURIComponent(slug)}?token=${encodeURIComponent(token)}`);\n await res.body?.cancel().catch(() => {});\n return res.ok;\n}\n\n/** The draft cookie's preview token from a `Cookie` header, or null when absent or tampered with. */\nexport async function draftTokenFrom(cookieHeader: string | null | undefined, secret: string | undefined): Promise<string | null> {\n if (!cookieHeader || !secret) return null;\n const m = new RegExp(`(?:^|;\\\\s*)${DRAFT_COOKIE}=([^;]+)`).exec(cookieHeader);\n if (!m) return null;\n try {\n return await verifyValue(decodeURIComponent(m[1]!), secret);\n } catch {\n return null;\n }\n}\n\n/**\n * The draft-enable route for any server that speaks Web `Request`/`Response` (SvelteKit `+server.ts`,\n * Nuxt via `toWebRequest`, Remix, Hono): `GET ?token=…&redirect=/x` checks the token with the\n * backend, sets the signed draft cookie and redirects. 401 on a refused token; 500 without a secret.\n */\nexport async function draftEnableResponse(request: Request, opts: { apiUrl: string; secret: string | undefined; fetchImpl?: typeof fetch }): Promise<Response> {\n const url = new URL(request.url);\n const token = url.searchParams.get(\"token\");\n if (!opts.secret) return new Response(\"BCMS_DRAFT_SECRET is not configured\", { status: 500 });\n if (!token) return new Response(\"Missing token\", { status: 400 });\n if (!(await previewTokenValid(opts.apiUrl, token, opts.fetchImpl))) return new Response(\"Invalid or expired token\", { status: 401 });\n const cookie = `${DRAFT_COOKIE}=${encodeURIComponent(await signValue(token, opts.secret))}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400${url.protocol === \"https:\" ? \"; Secure\" : \"\"}`;\n return new Response(null, { status: 302, headers: { Location: safeRedirectPath(url.searchParams.get(\"redirect\") ?? \"/\"), \"Set-Cookie\": cookie } });\n}\n\n/** Leave draft mode: clear the cookie and redirect (`?redirect=/x`, same-origin only). */\nexport function draftDisableResponse(request: Request): Response {\n const url = new URL(request.url);\n return new Response(null, {\n status: 302,\n headers: { Location: safeRedirectPath(url.searchParams.get(\"redirect\") ?? \"/\"), \"Set-Cookie\": `${DRAFT_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0` },\n });\n}\n"],"mappings":"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","names":[]}
|
package/dist/draft.d.cts
CHANGED
|
@@ -20,5 +20,19 @@ declare function decodeTokenSlug(token: string): string | null;
|
|
|
20
20
|
* draft route enables draft mode only on its yes.
|
|
21
21
|
*/
|
|
22
22
|
declare function previewTokenValid(apiUrl: string, token: string, fetchImpl?: typeof fetch): Promise<boolean>;
|
|
23
|
+
/** The draft cookie's preview token from a `Cookie` header, or null when absent or tampered with. */
|
|
24
|
+
declare function draftTokenFrom(cookieHeader: string | null | undefined, secret: string | undefined): Promise<string | null>;
|
|
25
|
+
/**
|
|
26
|
+
* The draft-enable route for any server that speaks Web `Request`/`Response` (SvelteKit `+server.ts`,
|
|
27
|
+
* Nuxt via `toWebRequest`, Remix, Hono): `GET ?token=…&redirect=/x` checks the token with the
|
|
28
|
+
* backend, sets the signed draft cookie and redirects. 401 on a refused token; 500 without a secret.
|
|
29
|
+
*/
|
|
30
|
+
declare function draftEnableResponse(request: Request, opts: {
|
|
31
|
+
apiUrl: string;
|
|
32
|
+
secret: string | undefined;
|
|
33
|
+
fetchImpl?: typeof fetch;
|
|
34
|
+
}): Promise<Response>;
|
|
35
|
+
/** Leave draft mode: clear the cookie and redirect (`?redirect=/x`, same-origin only). */
|
|
36
|
+
declare function draftDisableResponse(request: Request): Response;
|
|
23
37
|
|
|
24
|
-
export { DRAFT_COOKIE, decodeTokenSlug, previewTokenValid, safeRedirectPath, signValue, verifyValue };
|
|
38
|
+
export { DRAFT_COOKIE, decodeTokenSlug, draftDisableResponse, draftEnableResponse, draftTokenFrom, previewTokenValid, safeRedirectPath, signValue, verifyValue };
|
package/dist/draft.d.ts
CHANGED
|
@@ -20,5 +20,19 @@ declare function decodeTokenSlug(token: string): string | null;
|
|
|
20
20
|
* draft route enables draft mode only on its yes.
|
|
21
21
|
*/
|
|
22
22
|
declare function previewTokenValid(apiUrl: string, token: string, fetchImpl?: typeof fetch): Promise<boolean>;
|
|
23
|
+
/** The draft cookie's preview token from a `Cookie` header, or null when absent or tampered with. */
|
|
24
|
+
declare function draftTokenFrom(cookieHeader: string | null | undefined, secret: string | undefined): Promise<string | null>;
|
|
25
|
+
/**
|
|
26
|
+
* The draft-enable route for any server that speaks Web `Request`/`Response` (SvelteKit `+server.ts`,
|
|
27
|
+
* Nuxt via `toWebRequest`, Remix, Hono): `GET ?token=…&redirect=/x` checks the token with the
|
|
28
|
+
* backend, sets the signed draft cookie and redirects. 401 on a refused token; 500 without a secret.
|
|
29
|
+
*/
|
|
30
|
+
declare function draftEnableResponse(request: Request, opts: {
|
|
31
|
+
apiUrl: string;
|
|
32
|
+
secret: string | undefined;
|
|
33
|
+
fetchImpl?: typeof fetch;
|
|
34
|
+
}): Promise<Response>;
|
|
35
|
+
/** Leave draft mode: clear the cookie and redirect (`?redirect=/x`, same-origin only). */
|
|
36
|
+
declare function draftDisableResponse(request: Request): Response;
|
|
23
37
|
|
|
24
|
-
export { DRAFT_COOKIE, decodeTokenSlug, previewTokenValid, safeRedirectPath, signValue, verifyValue };
|
|
38
|
+
export { DRAFT_COOKIE, decodeTokenSlug, draftDisableResponse, draftEnableResponse, draftTokenFrom, previewTokenValid, safeRedirectPath, signValue, verifyValue };
|
package/dist/draft.js
CHANGED
|
@@ -45,9 +45,38 @@ async function previewTokenValid(apiUrl, token, fetchImpl = fetch) {
|
|
|
45
45
|
});
|
|
46
46
|
return res.ok;
|
|
47
47
|
}
|
|
48
|
+
async function draftTokenFrom(cookieHeader, secret) {
|
|
49
|
+
if (!cookieHeader || !secret) return null;
|
|
50
|
+
const m = new RegExp(`(?:^|;\\s*)${DRAFT_COOKIE}=([^;]+)`).exec(cookieHeader);
|
|
51
|
+
if (!m) return null;
|
|
52
|
+
try {
|
|
53
|
+
return await verifyValue(decodeURIComponent(m[1]), secret);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function draftEnableResponse(request, opts) {
|
|
59
|
+
const url = new URL(request.url);
|
|
60
|
+
const token = url.searchParams.get("token");
|
|
61
|
+
if (!opts.secret) return new Response("BCMS_DRAFT_SECRET is not configured", { status: 500 });
|
|
62
|
+
if (!token) return new Response("Missing token", { status: 400 });
|
|
63
|
+
if (!await previewTokenValid(opts.apiUrl, token, opts.fetchImpl)) return new Response("Invalid or expired token", { status: 401 });
|
|
64
|
+
const cookie = `${DRAFT_COOKIE}=${encodeURIComponent(await signValue(token, opts.secret))}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400${url.protocol === "https:" ? "; Secure" : ""}`;
|
|
65
|
+
return new Response(null, { status: 302, headers: { Location: safeRedirectPath(url.searchParams.get("redirect") ?? "/"), "Set-Cookie": cookie } });
|
|
66
|
+
}
|
|
67
|
+
function draftDisableResponse(request) {
|
|
68
|
+
const url = new URL(request.url);
|
|
69
|
+
return new Response(null, {
|
|
70
|
+
status: 302,
|
|
71
|
+
headers: { Location: safeRedirectPath(url.searchParams.get("redirect") ?? "/"), "Set-Cookie": `${DRAFT_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0` }
|
|
72
|
+
});
|
|
73
|
+
}
|
|
48
74
|
export {
|
|
49
75
|
DRAFT_COOKIE,
|
|
50
76
|
decodeTokenSlug,
|
|
77
|
+
draftDisableResponse,
|
|
78
|
+
draftEnableResponse,
|
|
79
|
+
draftTokenFrom,
|
|
51
80
|
previewTokenValid,
|
|
52
81
|
safeRedirectPath,
|
|
53
82
|
signValue,
|
package/dist/draft.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/draft.ts"],"sourcesContent":["/**\n * Draft mode, shared by every framework adapter: the signed cookie that says \"this visitor is\n * previewing\", the preview token's slug, and the redirect a draft route may follow.\n *\n * Web Crypto and `atob` only, so it runs in Node, edge runtimes and workers alike. Each adapter\n * mounts its own routes around these; the rules live here once.\n */\n\n/** The draft cookie every adapter sets: one name, so an editor link works on any framework. */\nexport const DRAFT_COOKIE = \"bcms-draft\";\n\nconst enc = new TextEncoder();\n\nfunction base64url(bytes: ArrayBuffer): string {\n const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));\n return b.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmac(value: string, secret: string): Promise<string> {\n const key = await crypto.subtle.importKey(\"raw\", enc.encode(secret), { name: \"HMAC\", hash: \"SHA-256\" }, false, [\"sign\"]);\n return base64url(await crypto.subtle.sign(\"HMAC\", key, enc.encode(value)));\n}\n\n/** Sign a value as `value.sig` for tamper-evident cookie storage (HMAC-SHA256, base64url). */\nexport async function signValue(value: string, secret: string): Promise<string> {\n return `${value}.${await hmac(value, secret)}`;\n}\n\n/** The original value of a signed one, or null when it was tampered with. */\nexport async function verifyValue(signed: string, secret: string): Promise<string | null> {\n const idx = signed.lastIndexOf(\".\");\n if (idx < 0) return null;\n const value = signed.slice(0, idx);\n const sig = signed.slice(idx + 1);\n const expected = await hmac(value, secret);\n if (sig.length !== expected.length) return null;\n let diff = 0;\n for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);\n return diff === 0 ? value : null;\n}\n\n/** Only same-origin relative paths: blocks \"//evil.com\", \"https://evil.com\" and \"/\\\\evil.com\". */\nexport function safeRedirectPath(to: string): string {\n return to.startsWith(\"/\") && !to.startsWith(\"//\") && !to.startsWith(\"/\\\\\") ? to : \"/\";\n}\n\n/** A preview token's `entrySlug`, read from its payload UNVERIFIED (the backend verifies it). */\nexport function decodeTokenSlug(token: string): string | null {\n try {\n const part = token.split(\".\")[1];\n if (!part) return null;\n // atob gives bytes as Latin-1: decoded as UTF-8, a slug like `über-uns` survives.\n const bytes = Uint8Array.from(atob(part.replace(/-/g, \"+\").replace(/_/g, \"/\")), (c) => c.charCodeAt(0));\n const json = JSON.parse(new TextDecoder().decode(bytes)) as { entrySlug?: string };\n return json.entrySlug ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Ask the backend whether a preview token is good: the backend is the signature authority, so a\n * draft route enables draft mode only on its yes.\n */\nexport async function previewTokenValid(apiUrl: string, token: string, fetchImpl: typeof fetch = fetch): Promise<boolean> {\n const slug = decodeTokenSlug(token);\n if (!slug) return false;\n const res = await fetchImpl(`${apiUrl.replace(/\\/+$/, \"\")}/api/v1/preview/${encodeURIComponent(slug)}?token=${encodeURIComponent(token)}`);\n await res.body?.cancel().catch(() => {});\n return res.ok;\n}\n"],"mappings":";AASO,IAAM,eAAe;AAE5B,IAAM,MAAM,IAAI,YAAY;AAE5B,SAAS,UAAU,OAA4B;AAC7C,QAAM,IAAI,KAAK,OAAO,aAAa,GAAG,IAAI,WAAW,KAAK,CAAC,CAAC;AAC5D,SAAO,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AACpE;AAEA,eAAe,KAAK,OAAe,QAAiC;AAClE,QAAM,MAAM,MAAM,OAAO,OAAO,UAAU,OAAO,IAAI,OAAO,MAAM,GAAG,EAAE,MAAM,QAAQ,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;AACvH,SAAO,UAAU,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,IAAI,OAAO,KAAK,CAAC,CAAC;AAC3E;AAGA,eAAsB,UAAU,OAAe,QAAiC;AAC9E,SAAO,GAAG,KAAK,IAAI,MAAM,KAAK,OAAO,MAAM,CAAC;AAC9C;AAGA,eAAsB,YAAY,QAAgB,QAAwC;AACxF,QAAM,MAAM,OAAO,YAAY,GAAG;AAClC,MAAI,MAAM,EAAG,QAAO;AACpB,QAAM,QAAQ,OAAO,MAAM,GAAG,GAAG;AACjC,QAAM,MAAM,OAAO,MAAM,MAAM,CAAC;AAChC,QAAM,WAAW,MAAM,KAAK,OAAO,MAAM;AACzC,MAAI,IAAI,WAAW,SAAS,OAAQ,QAAO;AAC3C,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,IAAK,SAAQ,IAAI,WAAW,CAAC,IAAI,SAAS,WAAW,CAAC;AACtF,SAAO,SAAS,IAAI,QAAQ;AAC9B;AAGO,SAAS,iBAAiB,IAAoB;AACnD,SAAO,GAAG,WAAW,GAAG,KAAK,CAAC,GAAG,WAAW,IAAI,KAAK,CAAC,GAAG,WAAW,KAAK,IAAI,KAAK;AACpF;AAGO,SAAS,gBAAgB,OAA8B;AAC5D,MAAI;AACF,UAAM,OAAO,MAAM,MAAM,GAAG,EAAE,CAAC;AAC/B,QAAI,CAAC,KAAM,QAAO;AAElB,UAAM,QAAQ,WAAW,KAAK,KAAK,KAAK,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AACtG,UAAM,OAAO,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,KAAK,CAAC;AACvD,WAAO,KAAK,aAAa;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,eAAsB,kBAAkB,QAAgB,OAAe,YAA0B,OAAyB;AACxH,QAAM,OAAO,gBAAgB,KAAK;AAClC,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,MAAM,MAAM,UAAU,GAAG,OAAO,QAAQ,QAAQ,EAAE,CAAC,mBAAmB,mBAAmB,IAAI,CAAC,UAAU,mBAAmB,KAAK,CAAC,EAAE;AACzI,QAAM,IAAI,MAAM,OAAO,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AACvC,SAAO,IAAI;AACb;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/draft.ts"],"sourcesContent":["/**\n * Draft mode, shared by every framework adapter: the signed cookie that says \"this visitor is\n * previewing\", the preview token's slug, and the redirect a draft route may follow.\n *\n * Web Crypto and `atob` only, so it runs in Node, edge runtimes and workers alike. Each adapter\n * mounts its own routes around these; the rules live here once.\n */\n\n/** The draft cookie every adapter sets: one name, so an editor link works on any framework. */\nexport const DRAFT_COOKIE = \"bcms-draft\";\n\nconst enc = new TextEncoder();\n\nfunction base64url(bytes: ArrayBuffer): string {\n const b = btoa(String.fromCharCode(...new Uint8Array(bytes)));\n return b.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=+$/, \"\");\n}\n\nasync function hmac(value: string, secret: string): Promise<string> {\n const key = await crypto.subtle.importKey(\"raw\", enc.encode(secret), { name: \"HMAC\", hash: \"SHA-256\" }, false, [\"sign\"]);\n return base64url(await crypto.subtle.sign(\"HMAC\", key, enc.encode(value)));\n}\n\n/** Sign a value as `value.sig` for tamper-evident cookie storage (HMAC-SHA256, base64url). */\nexport async function signValue(value: string, secret: string): Promise<string> {\n return `${value}.${await hmac(value, secret)}`;\n}\n\n/** The original value of a signed one, or null when it was tampered with. */\nexport async function verifyValue(signed: string, secret: string): Promise<string | null> {\n const idx = signed.lastIndexOf(\".\");\n if (idx < 0) return null;\n const value = signed.slice(0, idx);\n const sig = signed.slice(idx + 1);\n const expected = await hmac(value, secret);\n if (sig.length !== expected.length) return null;\n let diff = 0;\n for (let i = 0; i < sig.length; i++) diff |= sig.charCodeAt(i) ^ expected.charCodeAt(i);\n return diff === 0 ? value : null;\n}\n\n/** Only same-origin relative paths: blocks \"//evil.com\", \"https://evil.com\" and \"/\\\\evil.com\". */\nexport function safeRedirectPath(to: string): string {\n return to.startsWith(\"/\") && !to.startsWith(\"//\") && !to.startsWith(\"/\\\\\") ? to : \"/\";\n}\n\n/** A preview token's `entrySlug`, read from its payload UNVERIFIED (the backend verifies it). */\nexport function decodeTokenSlug(token: string): string | null {\n try {\n const part = token.split(\".\")[1];\n if (!part) return null;\n // atob gives bytes as Latin-1: decoded as UTF-8, a slug like `über-uns` survives.\n const bytes = Uint8Array.from(atob(part.replace(/-/g, \"+\").replace(/_/g, \"/\")), (c) => c.charCodeAt(0));\n const json = JSON.parse(new TextDecoder().decode(bytes)) as { entrySlug?: string };\n return json.entrySlug ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Ask the backend whether a preview token is good: the backend is the signature authority, so a\n * draft route enables draft mode only on its yes.\n */\nexport async function previewTokenValid(apiUrl: string, token: string, fetchImpl: typeof fetch = fetch): Promise<boolean> {\n const slug = decodeTokenSlug(token);\n if (!slug) return false;\n const res = await fetchImpl(`${apiUrl.replace(/\\/+$/, \"\")}/api/v1/preview/${encodeURIComponent(slug)}?token=${encodeURIComponent(token)}`);\n await res.body?.cancel().catch(() => {});\n return res.ok;\n}\n\n/** The draft cookie's preview token from a `Cookie` header, or null when absent or tampered with. */\nexport async function draftTokenFrom(cookieHeader: string | null | undefined, secret: string | undefined): Promise<string | null> {\n if (!cookieHeader || !secret) return null;\n const m = new RegExp(`(?:^|;\\\\s*)${DRAFT_COOKIE}=([^;]+)`).exec(cookieHeader);\n if (!m) return null;\n try {\n return await verifyValue(decodeURIComponent(m[1]!), secret);\n } catch {\n return null;\n }\n}\n\n/**\n * The draft-enable route for any server that speaks Web `Request`/`Response` (SvelteKit `+server.ts`,\n * Nuxt via `toWebRequest`, Remix, Hono): `GET ?token=…&redirect=/x` checks the token with the\n * backend, sets the signed draft cookie and redirects. 401 on a refused token; 500 without a secret.\n */\nexport async function draftEnableResponse(request: Request, opts: { apiUrl: string; secret: string | undefined; fetchImpl?: typeof fetch }): Promise<Response> {\n const url = new URL(request.url);\n const token = url.searchParams.get(\"token\");\n if (!opts.secret) return new Response(\"BCMS_DRAFT_SECRET is not configured\", { status: 500 });\n if (!token) return new Response(\"Missing token\", { status: 400 });\n if (!(await previewTokenValid(opts.apiUrl, token, opts.fetchImpl))) return new Response(\"Invalid or expired token\", { status: 401 });\n const cookie = `${DRAFT_COOKIE}=${encodeURIComponent(await signValue(token, opts.secret))}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400${url.protocol === \"https:\" ? \"; Secure\" : \"\"}`;\n return new Response(null, { status: 302, headers: { Location: safeRedirectPath(url.searchParams.get(\"redirect\") ?? \"/\"), \"Set-Cookie\": cookie } });\n}\n\n/** Leave draft mode: clear the cookie and redirect (`?redirect=/x`, same-origin only). */\nexport function draftDisableResponse(request: Request): Response {\n const url = new URL(request.url);\n return new Response(null, {\n status: 302,\n headers: { Location: safeRedirectPath(url.searchParams.get(\"redirect\") ?? \"/\"), \"Set-Cookie\": `${DRAFT_COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0` },\n });\n}\n"],"mappings":";AASO,IAAM,eAAe;AAE5B,IAAM,MAAM,IAAI,YAAY;AAE5B,SAAS,UAAU,OAA4B;AAC7C,QAAM,IAAI,KAAK,OAAO,aAAa,GAAG,IAAI,WAAW,KAAK,CAAC,CAAC;AAC5D,SAAO,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AACpE;AAEA,eAAe,KAAK,OAAe,QAAiC;AAClE,QAAM,MAAM,MAAM,OAAO,OAAO,UAAU,OAAO,IAAI,OAAO,MAAM,GAAG,EAAE,MAAM,QAAQ,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC;AACvH,SAAO,UAAU,MAAM,OAAO,OAAO,KAAK,QAAQ,KAAK,IAAI,OAAO,KAAK,CAAC,CAAC;AAC3E;AAGA,eAAsB,UAAU,OAAe,QAAiC;AAC9E,SAAO,GAAG,KAAK,IAAI,MAAM,KAAK,OAAO,MAAM,CAAC;AAC9C;AAGA,eAAsB,YAAY,QAAgB,QAAwC;AACxF,QAAM,MAAM,OAAO,YAAY,GAAG;AAClC,MAAI,MAAM,EAAG,QAAO;AACpB,QAAM,QAAQ,OAAO,MAAM,GAAG,GAAG;AACjC,QAAM,MAAM,OAAO,MAAM,MAAM,CAAC;AAChC,QAAM,WAAW,MAAM,KAAK,OAAO,MAAM;AACzC,MAAI,IAAI,WAAW,SAAS,OAAQ,QAAO;AAC3C,MAAI,OAAO;AACX,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,IAAK,SAAQ,IAAI,WAAW,CAAC,IAAI,SAAS,WAAW,CAAC;AACtF,SAAO,SAAS,IAAI,QAAQ;AAC9B;AAGO,SAAS,iBAAiB,IAAoB;AACnD,SAAO,GAAG,WAAW,GAAG,KAAK,CAAC,GAAG,WAAW,IAAI,KAAK,CAAC,GAAG,WAAW,KAAK,IAAI,KAAK;AACpF;AAGO,SAAS,gBAAgB,OAA8B;AAC5D,MAAI;AACF,UAAM,OAAO,MAAM,MAAM,GAAG,EAAE,CAAC;AAC/B,QAAI,CAAC,KAAM,QAAO;AAElB,UAAM,QAAQ,WAAW,KAAK,KAAK,KAAK,QAAQ,MAAM,GAAG,EAAE,QAAQ,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;AACtG,UAAM,OAAO,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,KAAK,CAAC;AACvD,WAAO,KAAK,aAAa;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,eAAsB,kBAAkB,QAAgB,OAAe,YAA0B,OAAyB;AACxH,QAAM,OAAO,gBAAgB,KAAK;AAClC,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,MAAM,MAAM,UAAU,GAAG,OAAO,QAAQ,QAAQ,EAAE,CAAC,mBAAmB,mBAAmB,IAAI,CAAC,UAAU,mBAAmB,KAAK,CAAC,EAAE;AACzI,QAAM,IAAI,MAAM,OAAO,EAAE,MAAM,MAAM;AAAA,EAAC,CAAC;AACvC,SAAO,IAAI;AACb;AAGA,eAAsB,eAAe,cAAyC,QAAoD;AAChI,MAAI,CAAC,gBAAgB,CAAC,OAAQ,QAAO;AACrC,QAAM,IAAI,IAAI,OAAO,cAAc,YAAY,UAAU,EAAE,KAAK,YAAY;AAC5E,MAAI,CAAC,EAAG,QAAO;AACf,MAAI;AACF,WAAO,MAAM,YAAY,mBAAmB,EAAE,CAAC,CAAE,GAAG,MAAM;AAAA,EAC5D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOA,eAAsB,oBAAoB,SAAkB,MAAmG;AAC7J,QAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,QAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;AAC1C,MAAI,CAAC,KAAK,OAAQ,QAAO,IAAI,SAAS,uCAAuC,EAAE,QAAQ,IAAI,CAAC;AAC5F,MAAI,CAAC,MAAO,QAAO,IAAI,SAAS,iBAAiB,EAAE,QAAQ,IAAI,CAAC;AAChE,MAAI,CAAE,MAAM,kBAAkB,KAAK,QAAQ,OAAO,KAAK,SAAS,EAAI,QAAO,IAAI,SAAS,4BAA4B,EAAE,QAAQ,IAAI,CAAC;AACnI,QAAM,SAAS,GAAG,YAAY,IAAI,mBAAmB,MAAM,UAAU,OAAO,KAAK,MAAM,CAAC,CAAC,kDAAkD,IAAI,aAAa,WAAW,aAAa,EAAE;AACtL,SAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,SAAS,EAAE,UAAU,iBAAiB,IAAI,aAAa,IAAI,UAAU,KAAK,GAAG,GAAG,cAAc,OAAO,EAAE,CAAC;AACnJ;AAGO,SAAS,qBAAqB,SAA4B;AAC/D,QAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,SAAO,IAAI,SAAS,MAAM;AAAA,IACxB,QAAQ;AAAA,IACR,SAAS,EAAE,UAAU,iBAAiB,IAAI,aAAa,IAAI,UAAU,KAAK,GAAG,GAAG,cAAc,GAAG,YAAY,+CAA+C;AAAA,EAC9J,CAAC;AACH;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import * as _bettercms_ai_types from '@bettercms-ai/types';
|
|
2
|
-
import { ContentPageResult, Content, DeliveredLayout, ContentModelField, Fieldset, ComponentPreview,
|
|
2
|
+
import { ContentPageResult, Content, DeliveredLayout, ContentModelField, Fieldset, ComponentPreview, LayoutStructureDocument, LayoutDataDocument, PageLayoutOverrideDocument, ManagementLayoutCommand, AuthUser, AuthSession, SignInInput, SignUpInput, Page, MediaAsset, FormField, Form, FormSubmission, Workspace, ApiKey, ApiKeyPermission, ApiKeyTokenType } from '@bettercms-ai/types';
|
|
3
3
|
export { ApiError, ApiKey, ApiKeyPermission, ApiKeyTokenType, AssertEqual, AuthSession, AuthUser, BetterCMSErrorCode, BlockType, ButtonBlock, ButtonProps, ColumnsBlock, ColumnsProps, Content, ContentBlock, ContentEntry, ContentModel, ContentModelField, ContentPageResult, ContentResponse, DeepReadonly, DeliveryComponent, DeliveryEntry, DeliveryList, DeliveryPage, Form, FormBlock, FormField, FormProps, FormSubmission, HeadingBlock, HeadingProps, ImageBlock, ImageProps, MediaAsset, Page, PageMetaJson, PaginatedResult, Perspective, Redirect, SignInInput, SignUpInput, SiteSeoDefaults, SpacerBlock, SpacerProps, TextBlock, TextProps, VideoBlock, VideoProps, Workspace, getBlockType, isBlock } from '@bettercms-ai/types';
|
|
4
|
+
import { D as DeliveryFormField } from './read-client-tNsCHy0p.cjs';
|
|
5
|
+
export { a as BCMSClientError, E as BCMSErrorCodes, a as BetterCMSError, B as BetterCMSReadClient, C as CreateClientOptions, b as DeliveryForm, c as DeliveryFormFieldType, E as ErrorCodes, F as FormSubmitError, d as FormValue, e as FormValues, G as GetEntryOptions, L as ListEntriesOptions, f as ListPagesOptions, S as SubmitFormOptions, g as SubmitFormResult, h as asStringArray, i as createClient, j as formInitialValues, k as isMultiValueField, s as shouldShowField, l as submitForm, t as toggleOption } from './read-client-tNsCHy0p.cjs';
|
|
4
6
|
export { ResolvedSeo, SeoInput, SeoLocation, entrySeoInput, resolveSeo } from './seo.cjs';
|
|
7
|
+
export { SearchHit, SearchOptions, search } from './read.cjs';
|
|
5
8
|
export { ImageFit, ImageFormat, ImageSource, ImageUrlBuilder, ImageUrlOptions, imageUrl, default as imageUrlBuilder, imageValueObjectPosition, imageValueUrl } from '@bettercms-ai/image-url';
|
|
6
9
|
|
|
7
10
|
/**
|
|
@@ -600,253 +603,6 @@ interface ManagedMediaAsset {
|
|
|
600
603
|
createdAt: string;
|
|
601
604
|
}
|
|
602
605
|
|
|
603
|
-
/** Semantic error codes used by the SDK. */
|
|
604
|
-
declare const ErrorCodes: {
|
|
605
|
-
readonly CONTENT_NOT_FOUND: "CONTENT_NOT_FOUND";
|
|
606
|
-
readonly UNAUTHORIZED: "UNAUTHORIZED";
|
|
607
|
-
readonly FORBIDDEN: "FORBIDDEN";
|
|
608
|
-
readonly RATE_LIMITED: "RATE_LIMITED";
|
|
609
|
-
readonly INTERNAL_ERROR: "INTERNAL_ERROR";
|
|
610
|
-
readonly VALIDATION_ERROR: "VALIDATION_ERROR";
|
|
611
|
-
readonly NETWORK_ERROR: "NETWORK_ERROR";
|
|
612
|
-
readonly ADMIN_NOT_IMPLEMENTED: "ADMIN_NOT_IMPLEMENTED";
|
|
613
|
-
};
|
|
614
|
-
/**
|
|
615
|
-
* BetterCMSError — normalized SDK error with a semantic code.
|
|
616
|
-
*/
|
|
617
|
-
declare class BetterCMSError extends Error {
|
|
618
|
-
readonly status: number;
|
|
619
|
-
readonly code: BetterCMSErrorCode;
|
|
620
|
-
/**
|
|
621
|
-
* Raw machine-readable `code` from the response body (e.g. "PROJECT_DELETED"),
|
|
622
|
-
* when the API supplies one. Distinct from `code` (which is derived from the
|
|
623
|
-
* HTTP status), this lets callers branch on a specific server condition that
|
|
624
|
-
* shares a status with others (e.g. 409 slug-conflict vs 409 project-deleted).
|
|
625
|
-
*/
|
|
626
|
-
readonly bodyCode?: string;
|
|
627
|
-
/**
|
|
628
|
-
* Per-field validation messages from a 400 `{ errors: { field: message } }` body.
|
|
629
|
-
* The backend returns validation failures in this shape (not `message`/`error`), so
|
|
630
|
-
* without this the SDK collapsed every validation 400 to a bare "Bad Request" with no
|
|
631
|
-
* hint which field or rule failed (FLO-474). Mirrors the forms client's `fieldErrors`.
|
|
632
|
-
*/
|
|
633
|
-
readonly fieldErrors?: Record<string, string>;
|
|
634
|
-
/**
|
|
635
|
-
* The parsed response body, when there was one.
|
|
636
|
-
*
|
|
637
|
-
* Several refusals carry STRUCTURED detail that neither `message` nor `fieldErrors` can hold,
|
|
638
|
-
* and which is the whole point of the refusal: a component publish 409s with the full
|
|
639
|
-
* `readiness` tuple (what evidence is missing, whether a human still has to approve it) and
|
|
640
|
-
* 422s with the `issues` naming the Layouts it would break. Collapsing those to a sentence
|
|
641
|
-
* leaves a caller able to report that something failed and not what to do about it.
|
|
642
|
-
*/
|
|
643
|
-
readonly body?: unknown;
|
|
644
|
-
constructor(message: string, status: number, code: BetterCMSErrorCode, bodyCode?: string, fieldErrors?: Record<string, string>, body?: unknown);
|
|
645
|
-
toJSON(): {
|
|
646
|
-
name: string;
|
|
647
|
-
message: string;
|
|
648
|
-
status: number;
|
|
649
|
-
code: BetterCMSErrorCode;
|
|
650
|
-
bodyCode: string | undefined;
|
|
651
|
-
fieldErrors: Record<string, string> | undefined;
|
|
652
|
-
};
|
|
653
|
-
/**
|
|
654
|
-
* Factory — creates a BetterCMSError from a failed fetch Response. Reads the body's
|
|
655
|
-
* `message` (or `error`) for the human message and `code` for a machine-readable
|
|
656
|
-
* condition surfaced as `bodyCode`. Validation failures arrive as `{ errors: {...} }`
|
|
657
|
-
* (no `message`/`error`), so fall back to the first field error for the message and
|
|
658
|
-
* expose the full map as `fieldErrors` (FLO-474).
|
|
659
|
-
*/
|
|
660
|
-
static from(res: Response): Promise<BetterCMSError>;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
/**
|
|
664
|
-
* Public forms — the headless integration surface for sites built on or imported
|
|
665
|
-
* into BetterCMS (the "render it yourself" model, like Sanity's delivery API).
|
|
666
|
-
*
|
|
667
|
-
* Form definitions ship in the build-time content snapshot (`bcms-content.json` →
|
|
668
|
-
* `forms` / `turnstileSiteKey`) and from `GET /api/v1/delivery/:workspace/forms`.
|
|
669
|
-
* A site renders them with its own components (see `@bettercms-ai/next`'s <BcmsForm>)
|
|
670
|
-
* and posts here. No API key — the endpoint is Turnstile-gated (Webflow model).
|
|
671
|
-
*
|
|
672
|
-
* This module is the single source of truth for the delivered form shape + the
|
|
673
|
-
* pure logic (conditional visibility, initial values) the renderers share, so the
|
|
674
|
-
* vanilla embed and the React component never drift.
|
|
675
|
-
*/
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Field types the delivery API serializes. Must stay equal to the authoring
|
|
679
|
-
* vocabulary in `packages/db/src/schema/forms.ts` — this list is what every site
|
|
680
|
-
* renderer switches on, so a type missing HERE renders as a plain text input on
|
|
681
|
-
* every published site while the builder happily offers it. `radio`/`checkboxes`
|
|
682
|
-
* were exactly that gap.
|
|
683
|
-
*/
|
|
684
|
-
type DeliveryFormFieldType = "text" | "email" | "phone" | "url" | "number" | "date" | "textarea" | "select" | "radio" | "checkboxes" | "checkbox" | "consent" | "hidden" | "step";
|
|
685
|
-
interface DeliveryFormField {
|
|
686
|
-
key: string;
|
|
687
|
-
label: string;
|
|
688
|
-
type: DeliveryFormFieldType;
|
|
689
|
-
placeholder?: string;
|
|
690
|
-
/** Hint rendered under the control. */
|
|
691
|
-
helpText?: string;
|
|
692
|
-
required?: boolean;
|
|
693
|
-
options?: string[];
|
|
694
|
-
defaultValue?: string;
|
|
695
|
-
/** Conditional visibility — render this field only when another field equals `equals`. */
|
|
696
|
-
showIf?: {
|
|
697
|
-
field: string;
|
|
698
|
-
equals: string;
|
|
699
|
-
};
|
|
700
|
-
/**
|
|
701
|
-
* Per-field rules the API enforces on submit. Renderers should mirror them onto the
|
|
702
|
-
* native control (`min`/`max`/`pattern`) so the browser reports them before the round
|
|
703
|
-
* trip — the server check is the one that counts.
|
|
704
|
-
*/
|
|
705
|
-
validation?: DeliveryFormFieldValidation;
|
|
706
|
-
/**
|
|
707
|
-
* Presentation options the renderer honours. Unlike `validation`, the API does NOT enforce
|
|
708
|
-
* these on submit — they change what the visitor is shown, not what is accepted.
|
|
709
|
-
*/
|
|
710
|
-
ui?: DeliveryFormFieldUi;
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* @see FormFieldUi in @bettercms-ai/types — same shape, delivered to the site.
|
|
714
|
-
*
|
|
715
|
-
* ── The phone composition contract every renderer implements ──
|
|
716
|
-
*
|
|
717
|
-
* A picker-enabled phone field is rendered as a country control PLUS a `tel` input, but it
|
|
718
|
-
* submits ONE value under the field's own key, in E.164. Whatever a renderer uses to hold the
|
|
719
|
-
* country (a React state, a `{key}__country` select) is scaffolding: it must never reach the
|
|
720
|
-
* POST body.
|
|
721
|
-
*
|
|
722
|
-
* Composition, from the visible input's raw text:
|
|
723
|
-
* - empty → `""`. NEVER `"+CC"`, and the reason is worse than "the server rejects it". The
|
|
724
|
-
* server's loose phone shape is `{3,32}` chars, so the outcome depends on the calling
|
|
725
|
-
* code's length: `"+1"` is REFUSED (an optional untouched field starts 400ing), while
|
|
726
|
-
* `"+91"` is ACCEPTED and stored as a real-looking submission that is only a country code.
|
|
727
|
-
* The silent case is the dangerous one. Pinned in `__tests__/lib/forms-validate.test.ts`.
|
|
728
|
-
* - starts with `"+"` → `"+" + digits(raw)`. The visitor typed an international number; the
|
|
729
|
-
* country control is ignored rather than fighting them.
|
|
730
|
-
* - otherwise → `"+" + callingCode + digits(raw).replace(/^0/, "")`, dropping ONE trunk zero.
|
|
731
|
-
* Known exception: Italian landlines keep their trunk zero, so this loses it. The value is
|
|
732
|
-
* still accepted (shape-only check) — it is a formatting loss, not a refusal.
|
|
733
|
-
*
|
|
734
|
-
* `ui` changes what the visitor SEES. It never changes what the API accepts: enforcing a format
|
|
735
|
-
* is `validation.phoneFormat`'s job and the two are independent.
|
|
736
|
-
*
|
|
737
|
-
* Renderer parity is behavioural, not implementational. FIVE lanes render this field, and they
|
|
738
|
-
* agree on the VALUE, not on the implementation:
|
|
739
|
-
*
|
|
740
|
-
* | Lane | Control | As-you-type formatting |
|
|
741
|
-
* |-------------------------------|---------------------------------|------------------------|
|
|
742
|
-
* | dashboard form preview | searchable picker + formatter | yes |
|
|
743
|
-
* | @bettercms-ai/next <BcmsForm> | native select + formatter | yes (lazy chunk) |
|
|
744
|
-
* | hosted sites (render-page) | native select + tel input | NO |
|
|
745
|
-
* | embed snippet | native select + tel input | NO |
|
|
746
|
-
* | @bettercms-ai/astro BcmsForm | native select + tel input | NO |
|
|
747
|
-
*
|
|
748
|
-
* The missing as-you-type formatting on the three vanilla lanes is a NAMED degradation, not an
|
|
749
|
-
* oversight: it is what keeps libphonenumber off a visitor's page entirely. `packages/next`
|
|
750
|
-
* pays for it only on pages that actually render a picker, via a separate chunk.
|
|
751
|
-
*
|
|
752
|
-
* When a lane builds that option list, never pass a libphonenumber helper as a bare callback:
|
|
753
|
-
* `getCountries().filter(isSupportedCountry)` hands it the array INDEX as its `metadata`
|
|
754
|
-
* argument and throws. Always wrap — `.filter((iso) => isSupportedCountry(iso))`.
|
|
755
|
-
*
|
|
756
|
-
* And when a lane ships the composition rule inside a JS TEMPLATE LITERAL (the embed snippet
|
|
757
|
-
* does), `\D` collapses to `D` and the regex silently becomes `/D/g` — stripping the letter D
|
|
758
|
-
* and leaving every other separator in place. Write `\\D` there. Raw HTML `<script>` text (the
|
|
759
|
-
* hosted renderer, the Astro inline script) takes a single backslash. Nothing throws either
|
|
760
|
-
* way; numbers just arrive malformed, so each lane pins it with an executed-script test.
|
|
761
|
-
*/
|
|
762
|
-
interface DeliveryFormFieldUi {
|
|
763
|
-
/** `phone` — render a country picker beside the number. The submitted value is E.164. */
|
|
764
|
-
countryPicker?: boolean;
|
|
765
|
-
/** `phone` — uppercase ISO 3166-1 alpha-2 the picker starts on. */
|
|
766
|
-
defaultCountry?: string;
|
|
767
|
-
/**
|
|
768
|
-
* `phone` — show the country's flag beside the dialling code.
|
|
769
|
-
*
|
|
770
|
-
* Honoured only by a renderer with a custom picker. The native `<select>` the site lanes use
|
|
771
|
-
* cannot hold an SVG, and an emoji flag renders as two grey letters on Windows, so those
|
|
772
|
-
* lanes show the country NAME instead — the same information, legibly, everywhere.
|
|
773
|
-
*/
|
|
774
|
-
showFlags?: boolean;
|
|
775
|
-
}
|
|
776
|
-
/** @see FormFieldValidation in @bettercms-ai/types — same shape, delivered to the site. */
|
|
777
|
-
interface DeliveryFormFieldValidation {
|
|
778
|
-
emailPolicy?: "any" | "business";
|
|
779
|
-
min?: number;
|
|
780
|
-
max?: number;
|
|
781
|
-
phoneFormat?: "any" | "e164";
|
|
782
|
-
pattern?: string;
|
|
783
|
-
}
|
|
784
|
-
/** The pick-many type, whose value is an array rather than a scalar. */
|
|
785
|
-
declare function isMultiValueField(type: DeliveryFormFieldType): boolean;
|
|
786
|
-
/** A form as delivered to a published/imported site. Mirrors the delivery `/forms` payload. */
|
|
787
|
-
interface DeliveryForm {
|
|
788
|
-
id: string;
|
|
789
|
-
name: string;
|
|
790
|
-
/** The project the form belongs to. */
|
|
791
|
-
projectId?: string | null;
|
|
792
|
-
/**
|
|
793
|
-
* A `draft` form refuses every submission: render it as unavailable rather than as a form that
|
|
794
|
-
* silently drops what a visitor sends. Delivered for exactly that reason, and missing from this
|
|
795
|
-
* type until the contract test held the two together.
|
|
796
|
-
*/
|
|
797
|
-
status?: "draft" | "published" | (string & {});
|
|
798
|
-
/** Set on a form the site's owner told BetterCMS to leave alone (ISO time); such a form is not wired. */
|
|
799
|
-
ignoredAt?: string | null;
|
|
800
|
-
fields: DeliveryFormField[];
|
|
801
|
-
submitLabel?: string;
|
|
802
|
-
successMessage?: string;
|
|
803
|
-
redirectUrl?: string;
|
|
804
|
-
turnstileEnabled?: boolean;
|
|
805
|
-
honeypotField?: string | null;
|
|
806
|
-
}
|
|
807
|
-
/** `string[]` is the `checkboxes` (pick-many) value; every other type is scalar. */
|
|
808
|
-
type FormValue = string | boolean | string[];
|
|
809
|
-
type FormValues = Record<string, FormValue>;
|
|
810
|
-
/** Read a `checkboxes` value defensively — stored data may predate the array shape. */
|
|
811
|
-
declare function asStringArray(value: FormValue | undefined): string[];
|
|
812
|
-
/** Add/remove `option` from a `checkboxes` value, preserving the field's option order. */
|
|
813
|
-
declare function toggleOption(value: FormValue | undefined, option: string, on: boolean): string[];
|
|
814
|
-
/**
|
|
815
|
-
* Conditional visibility. A `showIf` field is shown only when its trigger field's
|
|
816
|
-
* current value equals the target; non-conditional fields are always shown. The
|
|
817
|
-
* renderer must skip hidden fields when collecting/validating so a hidden required
|
|
818
|
-
* field never blocks submit.
|
|
819
|
-
*/
|
|
820
|
-
declare function shouldShowField(field: DeliveryFormField, values: FormValues): boolean;
|
|
821
|
-
/**
|
|
822
|
-
* Build the initial value map: checkbox/consent default to false, checkboxes to an
|
|
823
|
-
* empty array, every other field to its `defaultValue`, with `prefill` (e.g. URL
|
|
824
|
-
* query params) taking precedence.
|
|
825
|
-
*/
|
|
826
|
-
declare function formInitialValues(form: DeliveryForm, prefill?: Record<string, string>): FormValues;
|
|
827
|
-
interface SubmitFormOptions {
|
|
828
|
-
formId: string;
|
|
829
|
-
data: FormValues;
|
|
830
|
-
/** Cloudflare Turnstile token; required when the form has `turnstileEnabled`. */
|
|
831
|
-
turnstileToken?: string;
|
|
832
|
-
/** API origin. Defaults to https://api.bettercms.ai. */
|
|
833
|
-
baseUrl?: string;
|
|
834
|
-
/** Injectable fetch for tests / non-browser runtimes. */
|
|
835
|
-
fetchImpl?: typeof fetch;
|
|
836
|
-
}
|
|
837
|
-
interface SubmitFormResult {
|
|
838
|
-
id: string;
|
|
839
|
-
}
|
|
840
|
-
/** A submit failure that carries per-field validation messages from the API (HTTP 400). */
|
|
841
|
-
type FormSubmitError = BetterCMSError & {
|
|
842
|
-
fieldErrors?: Record<string, string>;
|
|
843
|
-
};
|
|
844
|
-
/**
|
|
845
|
-
* Submit a public form. Resolves with the new submission id, or throws a
|
|
846
|
-
* BetterCMSError — field-level validation errors are attached as `.fieldErrors`.
|
|
847
|
-
*/
|
|
848
|
-
declare function submitForm(opts: SubmitFormOptions): Promise<SubmitFormResult>;
|
|
849
|
-
|
|
850
606
|
/**
|
|
851
607
|
* Management API SDK methods — read-only form discovery.
|
|
852
608
|
*
|
|
@@ -1168,113 +924,6 @@ declare const BetterCMS: {
|
|
|
1168
924
|
signUp(options: SignUpInput): Promise<AuthResult>;
|
|
1169
925
|
};
|
|
1170
926
|
|
|
1171
|
-
/**
|
|
1172
|
-
* createClient — framework-agnostic typed read client for the Delivery API.
|
|
1173
|
-
*
|
|
1174
|
-
* Shared by @bettercms-ai/astro and @bettercms-ai/next so the fetch/typing/retry
|
|
1175
|
-
* logic lives in one place. Read-only; for schema authoring use the
|
|
1176
|
-
* Management client, for sessions use the Admin client.
|
|
1177
|
-
*
|
|
1178
|
-
* const client = createClient({ apiUrl, workspace, apiKey });
|
|
1179
|
-
* await client.getPage("home");
|
|
1180
|
-
* await client.listEntries({ model: "post", perPage: 50 });
|
|
1181
|
-
*/
|
|
1182
|
-
|
|
1183
|
-
interface CreateClientOptions {
|
|
1184
|
-
/** Backend base, e.g. `https://api.bettercms.ai` (no trailing slash needed). */
|
|
1185
|
-
apiUrl: string;
|
|
1186
|
-
/** Workspace slug. */
|
|
1187
|
-
workspace: string;
|
|
1188
|
-
/** Delivery API key. Use a `content:read:draft` key for drafts. */
|
|
1189
|
-
apiKey?: string;
|
|
1190
|
-
/**
|
|
1191
|
-
* The project every read is addressed to. A workspace holds many projects whose slugs
|
|
1192
|
-
* collide (each has a `blog` model and a `home` page), so a site names its own; published
|
|
1193
|
-
* reads need no key for it. BetterCMS hosting supplies `PUBLIC_BCMS_PROJECT_ID`.
|
|
1194
|
-
*/
|
|
1195
|
-
projectId?: string;
|
|
1196
|
-
/** `published` (default) or `drafts`. */
|
|
1197
|
-
perspective?: Perspective;
|
|
1198
|
-
/**
|
|
1199
|
-
* Preview-token JWT for token-based single-document draft reads via the
|
|
1200
|
-
* `/preview/:slug` endpoint. Only used when `perspective === "drafts"`.
|
|
1201
|
-
*/
|
|
1202
|
-
previewToken?: string;
|
|
1203
|
-
/**
|
|
1204
|
-
* Embed invisible visual-editing provenance in draft reads, so a preview build can
|
|
1205
|
-
* deep-link each rendered value back to its editor. Requires `perspective: "drafts"`,
|
|
1206
|
-
* and then either a `previewToken` or a `content:read:draft` key. Ignored on published
|
|
1207
|
-
* reads — the payload must never reach a production build.
|
|
1208
|
-
*
|
|
1209
|
-
* **Defaults to `true` on `perspective: "drafts"`** (since 1.15.0). Provenance is the whole
|
|
1210
|
-
* reason the drafts perspective exists in a preview build, and an option nobody knew to pass
|
|
1211
|
-
* meant every such build rendered anonymous strings: the editor could bind them only by
|
|
1212
|
-
* guessing at their text. It stays OFF on every other perspective, unconditionally, so a
|
|
1213
|
-
* published build cannot acquire a payload by accident — pass `stega: false` to opt a draft
|
|
1214
|
-
* read out (a diff or a checksum over draft content is the case that wants it).
|
|
1215
|
-
*/
|
|
1216
|
-
stega?: boolean;
|
|
1217
|
-
/** Per-attempt timeout in ms. Defaults to 10000. */
|
|
1218
|
-
timeout?: number;
|
|
1219
|
-
/**
|
|
1220
|
-
* Collapse identical GETs to one network round-trip for the life of this client.
|
|
1221
|
-
* Default false.
|
|
1222
|
-
*
|
|
1223
|
-
* Only correct where the content is a POINT-IN-TIME SNAPSHOT — i.e. a static build,
|
|
1224
|
-
* where every route renders from the same published dataset. `@bettercms-ai/astro`
|
|
1225
|
-
* turns this on automatically when Astro's `command === "build"` and never for `dev`,
|
|
1226
|
-
* SSR or the drafts perspective, where a repeat read must see the current row.
|
|
1227
|
-
*
|
|
1228
|
-
* A static build without it re-fetches the same nav/settings/collection once PER ROUTE.
|
|
1229
|
-
* That is what made a large site exhaust the delivery rate limit: hundreds of identical
|
|
1230
|
-
* requests, and `getPage()` re-scanning every page of the paginated list each time it is
|
|
1231
|
-
* called.
|
|
1232
|
-
*/
|
|
1233
|
-
cache?: boolean;
|
|
1234
|
-
}
|
|
1235
|
-
interface ListEntriesOptions {
|
|
1236
|
-
/** Filter by content-model slug. */
|
|
1237
|
-
model?: string;
|
|
1238
|
-
page?: number;
|
|
1239
|
-
perPage?: number;
|
|
1240
|
-
/** Reference hydration depth (lists cap at 1). */
|
|
1241
|
-
depth?: 0 | 1;
|
|
1242
|
-
/** Projection — field names to return. */
|
|
1243
|
-
select?: string[];
|
|
1244
|
-
}
|
|
1245
|
-
interface GetEntryOptions {
|
|
1246
|
-
depth?: 0 | 1 | 2;
|
|
1247
|
-
select?: string[];
|
|
1248
|
-
}
|
|
1249
|
-
interface ListPagesOptions {
|
|
1250
|
-
page?: number;
|
|
1251
|
-
perPage?: number;
|
|
1252
|
-
}
|
|
1253
|
-
interface BetterCMSReadClient {
|
|
1254
|
-
readonly perspective: Perspective;
|
|
1255
|
-
getEntry<T = Record<string, unknown>>(slug: string, opts?: GetEntryOptions): Promise<DeliveryEntry<T>>;
|
|
1256
|
-
listEntries<T = Record<string, unknown>>(opts?: ListEntriesOptions): Promise<DeliveryList<DeliveryEntry<T>>>;
|
|
1257
|
-
getPage(slug: string): Promise<DeliveryPage | null>;
|
|
1258
|
-
listPages(opts?: ListPagesOptions): Promise<DeliveryList<DeliveryPage>>;
|
|
1259
|
-
/**
|
|
1260
|
-
* The workspace's reusable component definitions, for resolving `component` blocks.
|
|
1261
|
-
* Under `perspective: "drafts"` this is the DRAFT definition — what makes an SSR render
|
|
1262
|
-
* of a draft page look like the page the editor is showing.
|
|
1263
|
-
*/
|
|
1264
|
-
listComponents(): Promise<DeliveryComponent[]>;
|
|
1265
|
-
/** Fetch the project-scoped Global Layout for the active perspective. */
|
|
1266
|
-
getLayout(): Promise<DeliveredLayout | null>;
|
|
1267
|
-
listForms(opts?: {
|
|
1268
|
-
projectId?: string;
|
|
1269
|
-
}): Promise<{
|
|
1270
|
-
items: DeliveryForm[];
|
|
1271
|
-
turnstileSiteKey: string | null;
|
|
1272
|
-
}>;
|
|
1273
|
-
/** Build a deterministic Next/cache tag for a resource. */
|
|
1274
|
-
tag(resource: "entry" | "page" | "forms", id?: string): string;
|
|
1275
|
-
}
|
|
1276
|
-
declare function createClient(options: CreateClientOptions): BetterCMSReadClient;
|
|
1277
|
-
|
|
1278
927
|
/** Provenance describing where a string came from, for editor deep-linking. */
|
|
1279
928
|
interface StegaPayload {
|
|
1280
929
|
/** Workspace slug. */
|
|
@@ -1476,42 +1125,6 @@ declare function rich(value: TextOrRich, fallback?: string): string;
|
|
|
1476
1125
|
*/
|
|
1477
1126
|
declare function lines(value: TextOrRich, fallback?: string): string;
|
|
1478
1127
|
|
|
1479
|
-
/**
|
|
1480
|
-
* Public site search — the headless surface for the visitor-facing search box.
|
|
1481
|
-
*
|
|
1482
|
-
* Queries `GET /api/v1/delivery/search?project=…&q=…`, the same key-less public
|
|
1483
|
-
* endpoint the embeddable widget uses. Powers <BcmsSearch> in @bettercms-ai/next
|
|
1484
|
-
* and @bettercms-ai/astro, so the component and any custom UI never drift.
|
|
1485
|
-
*
|
|
1486
|
-
* `snippet` is HTML-safe: the server escapes the field content and keeps only the
|
|
1487
|
-
* <mark> highlight wrappers, so it can be rendered with dangerouslySetInnerHTML.
|
|
1488
|
-
*/
|
|
1489
|
-
interface SearchHit {
|
|
1490
|
-
title: string;
|
|
1491
|
-
slug: string;
|
|
1492
|
-
type: "page" | "entry";
|
|
1493
|
-
/** HTML-safe snippet with <mark> highlights around matched terms. */
|
|
1494
|
-
snippet: string;
|
|
1495
|
-
/** Site-relative URL, e.g. "/about". */
|
|
1496
|
-
url: string;
|
|
1497
|
-
}
|
|
1498
|
-
interface SearchOptions {
|
|
1499
|
-
/** The project id (from the dashboard's Site Search settings). */
|
|
1500
|
-
project: string;
|
|
1501
|
-
/** The query string. Calls with fewer than 1 char resolve to []. */
|
|
1502
|
-
q: string;
|
|
1503
|
-
/** Max hits, 1–20 (default 8). */
|
|
1504
|
-
limit?: number;
|
|
1505
|
-
/** API origin. Defaults to https://api.bettercms.ai. */
|
|
1506
|
-
baseUrl?: string;
|
|
1507
|
-
/** Injectable fetch for tests / non-browser runtimes. */
|
|
1508
|
-
fetchImpl?: typeof fetch;
|
|
1509
|
-
/** AbortSignal so callers can cancel a stale in-flight search. */
|
|
1510
|
-
signal?: AbortSignal;
|
|
1511
|
-
}
|
|
1512
|
-
/** Search a published site. Resolves with hits, or [] on an empty query / network error. */
|
|
1513
|
-
declare function search(opts: SearchOptions): Promise<SearchHit[]>;
|
|
1514
|
-
|
|
1515
1128
|
/**
|
|
1516
1129
|
* The published content snapshot, read where the site RUNS — not bundled into it.
|
|
1517
1130
|
*
|
|
@@ -1875,4 +1488,4 @@ interface FramedWindow {
|
|
|
1875
1488
|
/** True inside the editor's canvas. Safe where there is no window (a server render): false. */
|
|
1876
1489
|
declare function inEditor(win?: FramedWindow | undefined): boolean;
|
|
1877
1490
|
|
|
1878
|
-
export { type AddPageFieldsInput, type ApiKeyUsage, type ApiKeyWithRaw, type AuthResult,
|
|
1491
|
+
export { type AddPageFieldsInput, type ApiKeyUsage, type ApiKeyWithRaw, type AuthResult, BetterCMS, BetterCMSAdminClient, type BetterCMSAdminOptions, BetterCMSDeliveryClient, BetterCMSManagementClient, type BetterCMSManagementOptions, type BetterCMSSiteOptions, CONTENT_APPLIED, type CommandManagedLayoutInput, type ContentAppliedMessage, type CreateApiKeyInput, type CreateEntryInput, type CreateFormInput, type CreateManagedPageInput, type CreateModelInput, type CreatePageInput, type CreateWebhookInput, type CreateWorkspaceInput, DeliveryFormField, EDITOR_FRAME_NAME, type ExtractionCandidate, type FormFieldInput, type FormListOptions, type GetManagedLayoutOptions, type ListContentAllOptions, type ListContentOptions, type ListEntriesFilter, type ManagedComponent, type ManagedComponentInput, type ManagedComponentSummary, type ManagedContentEntry, type ManagedContentModel, type ManagedForm, type ManagedFormInput, type ManagedLayoutClient, type ManagedLayoutDocument, type ManagedMediaAsset, type ManagedPage, type ManagementClient, type MediaListOptions, type MediaMetadata, type Member, type MemberRole, type ModelFieldset, PORTABLE_TEXT_FORMAT, type PageCreateReport, type PageListOptions, type PendingInvite, type PublishManagedLayoutInput, type PublishedContent, type RichTextValue, STEGA_OPEN, type SeoMeta, type SeoMetaInput, type SetPageContentInput, type StegaNode, type StegaPayload, type SubmissionListOptions, type TextOrRich, type UpdateApiKeyInput, type UpdateAssetInput, type UpdateEntryInput, type UpdateFormInput, type UpdateModelInput, type UpdatePageInput, type UpdateWebhookInput, type UpdateWorkspaceInput, type UploadAssetInput, type UploadedAsset, type Webhook, type WebhookAdvancedConfig, type WebhookEvent, type WebhookListResponse, type WebhookWithSecret, type WriteContentInput, addModelFields, addPageFields, commandManagedLayout, contentApplied, createApiKey, createEntry, createForm, createManagedPage, createModel, createPage, createWorkspace, decodeStega, deleteForm, deleteMedia, deletePage, deleteSubmission, deleteWorkspace, encodeStega, getApiKeyUsage, getEntry, getForm, getManagedLayout, getManagedPage, getMedia, getMember, getModel, getPage, getSubmission, getWorkspace, inEditor, inviteMember, isRichText, lines, listApiKeys, listEntries, listForms, listManagedPages, listMedia, listMembers, listModels, listPages, listSubmissions, listWorkspaces, onContentApplied, plain, portableText, publishManagedLayout, publishPage, publishedContentFile, publishedEntry, readPublishedContent, regenerateApiKey, removeMember, resetPublishedContentCache, resolveStega, revokeApiKey, rich, setPageContent, signIn, signInWithGithub, signInWithGoogle, signUp, stegaClean, stripStega, updateApiKey, updateEntry, updateForm, updateMemberRole, updateModel, updatePage, updatePageModel, updateWorkspace, uploadMedia };
|