@bettercms-ai/sdk 1.36.0 → 1.38.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/README.md +27 -1
- package/dist/draft.cjs +15 -0
- package/dist/draft.cjs.map +1 -1
- package/dist/draft.d.cts +7 -1
- package/dist/draft.d.ts +7 -1
- package/dist/draft.js +14 -0
- package/dist/draft.js.map +1 -1
- package/dist/index.cjs +32 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +35 -8
- package/dist/index.d.ts +35 -8
- package/dist/index.js +31 -12
- package/dist/index.js.map +1 -1
- package/dist/{read-client-tNsCHy0p.d.cts → read-client-B1uDVNKk.d.cts} +6 -0
- package/dist/{read-client-tNsCHy0p.d.ts → read-client-B1uDVNKk.d.ts} +6 -0
- package/dist/read.cjs +13 -7
- package/dist/read.cjs.map +1 -1
- package/dist/read.d.cts +1 -1
- package/dist/read.d.ts +1 -1
- package/dist/read.js +13 -7
- package/dist/read.js.map +1 -1
- package/dist/sections.cjs +13 -7
- package/dist/sections.cjs.map +1 -1
- package/dist/sections.d.cts +1 -1
- package/dist/sections.d.ts +1 -1
- package/dist/sections.js +13 -7
- package/dist/sections.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -119,6 +119,32 @@ Published reads never carry provenance, whatever `stega` is set to: the payload
|
|
|
119
119
|
production build. Strip it yourself with `stripStega(value)` if you need the clean string, and
|
|
120
120
|
decode it with `decodeStega(value)`.
|
|
121
121
|
|
|
122
|
+
## Rich text
|
|
123
|
+
|
|
124
|
+
A rich-text field arrives as `{ format, value, html }` (or as a bare string, if the field was
|
|
125
|
+
switched from text). Render `html` with `rich()`, or render the structure (`value`) yourself:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
import { rich, plain, portableText, embeds, resolved } from "@bettercms-ai/sdk";
|
|
129
|
+
|
|
130
|
+
const entry = await client.getEntry<{ body: RichTextValue }>("launch");
|
|
131
|
+
|
|
132
|
+
rich(entry.data.body); // HTML for set:html / dangerouslySetInnerHTML: everything drawn
|
|
133
|
+
plain(entry.data.body); // text for <title>, meta description, alt
|
|
134
|
+
|
|
135
|
+
// Rendering the structure with @bettercms-ai/ui (or your own components):
|
|
136
|
+
<PortableText
|
|
137
|
+
value={portableText(entry.data.body) ?? entry.data.body}
|
|
138
|
+
embeds={embeds(entry.data.body) ?? undefined}
|
|
139
|
+
resolved={resolved(entry.data.body, entry) ?? undefined}
|
|
140
|
+
/>;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Related posts and author bios are resolved by the server for each response, from the published
|
|
144
|
+
entries, and never stored. An envelope value carries what it resolved to (`value.resolved`); a
|
|
145
|
+
Body stored as a bare block array cannot, so the entry carries the merged map beside `data`
|
|
146
|
+
(`entry.resolved`). `resolved(value, entry)` returns the value's own map, else the entry's.
|
|
147
|
+
|
|
122
148
|
## API
|
|
123
149
|
|
|
124
150
|
### `BetterCMS.site(options)`
|
|
@@ -127,7 +153,7 @@ decode it with `decodeStega(value)`.
|
|
|
127
153
|
| --------- | ------ | -------- | -------------------------- |
|
|
128
154
|
| `workspace` | `string` | Yes | — |
|
|
129
155
|
| `apiKey` | `string` | No | — |
|
|
130
|
-
| `baseUrl` | `string` | No | `https://api.bettercms.ai/v1` |
|
|
156
|
+
| `baseUrl` | `string` | No | `https://api.bettercms.ai/api/v1/delivery` |
|
|
131
157
|
|
|
132
158
|
Returns a `BetterCMSDeliveryClient` instance.
|
|
133
159
|
|
package/dist/draft.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var draft_exports = {};
|
|
22
22
|
__export(draft_exports, {
|
|
23
23
|
DRAFT_COOKIE: () => DRAFT_COOKIE,
|
|
24
|
+
decodeTokenBranch: () => decodeTokenBranch,
|
|
24
25
|
decodeTokenSlug: () => decodeTokenSlug,
|
|
25
26
|
draftDisableResponse: () => draftDisableResponse,
|
|
26
27
|
draftEnableResponse: () => draftEnableResponse,
|
|
@@ -58,6 +59,19 @@ async function verifyValue(signed, secret) {
|
|
|
58
59
|
function safeRedirectPath(to) {
|
|
59
60
|
return to.startsWith("/") && !to.startsWith("//") && !to.startsWith("/\\") ? to : "/";
|
|
60
61
|
}
|
|
62
|
+
function tokenPayload(token) {
|
|
63
|
+
try {
|
|
64
|
+
const part = token.split(".")[1];
|
|
65
|
+
if (!part) return null;
|
|
66
|
+
const bytes = Uint8Array.from(atob(part.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
|
67
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function decodeTokenBranch(token) {
|
|
73
|
+
return token && tokenPayload(token)?.branch || null;
|
|
74
|
+
}
|
|
61
75
|
function decodeTokenSlug(token) {
|
|
62
76
|
try {
|
|
63
77
|
const part = token.split(".")[1];
|
|
@@ -106,6 +120,7 @@ function draftDisableResponse(request) {
|
|
|
106
120
|
// Annotate the CommonJS export names for ESM import in node:
|
|
107
121
|
0 && (module.exports = {
|
|
108
122
|
DRAFT_COOKIE,
|
|
123
|
+
decodeTokenBranch,
|
|
109
124
|
decodeTokenSlug,
|
|
110
125
|
draftDisableResponse,
|
|
111
126
|
draftEnableResponse,
|
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\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":[]}
|
|
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\nfunction tokenPayload(token: string): { entrySlug?: string; branch?: 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 return JSON.parse(new TextDecoder().decode(bytes)) as { entrySlug?: string; branch?: string };\n } catch {\n return null;\n }\n}\n\n/**\n * The content branch a preview token was minted on, or null for main. Read UNVERIFIED: an adapter\n * only stores a token the backend accepted, and branch reads need the site's draft key regardless.\n * Pass it as the client's `branch` in draft mode and the whole site previews from that branch.\n */\nexport function decodeTokenBranch(token: string | null | undefined): string | null {\n return (token && tokenPayload(token)?.branch) || null;\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
|
@@ -13,6 +13,12 @@ declare function signValue(value: string, secret: string): Promise<string>;
|
|
|
13
13
|
declare function verifyValue(signed: string, secret: string): Promise<string | null>;
|
|
14
14
|
/** Only same-origin relative paths: blocks "//evil.com", "https://evil.com" and "/\\evil.com". */
|
|
15
15
|
declare function safeRedirectPath(to: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* The content branch a preview token was minted on, or null for main. Read UNVERIFIED: an adapter
|
|
18
|
+
* only stores a token the backend accepted, and branch reads need the site's draft key regardless.
|
|
19
|
+
* Pass it as the client's `branch` in draft mode and the whole site previews from that branch.
|
|
20
|
+
*/
|
|
21
|
+
declare function decodeTokenBranch(token: string | null | undefined): string | null;
|
|
16
22
|
/** A preview token's `entrySlug`, read from its payload UNVERIFIED (the backend verifies it). */
|
|
17
23
|
declare function decodeTokenSlug(token: string): string | null;
|
|
18
24
|
/**
|
|
@@ -35,4 +41,4 @@ declare function draftEnableResponse(request: Request, opts: {
|
|
|
35
41
|
/** Leave draft mode: clear the cookie and redirect (`?redirect=/x`, same-origin only). */
|
|
36
42
|
declare function draftDisableResponse(request: Request): Response;
|
|
37
43
|
|
|
38
|
-
export { DRAFT_COOKIE, decodeTokenSlug, draftDisableResponse, draftEnableResponse, draftTokenFrom, previewTokenValid, safeRedirectPath, signValue, verifyValue };
|
|
44
|
+
export { DRAFT_COOKIE, decodeTokenBranch, decodeTokenSlug, draftDisableResponse, draftEnableResponse, draftTokenFrom, previewTokenValid, safeRedirectPath, signValue, verifyValue };
|
package/dist/draft.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ declare function signValue(value: string, secret: string): Promise<string>;
|
|
|
13
13
|
declare function verifyValue(signed: string, secret: string): Promise<string | null>;
|
|
14
14
|
/** Only same-origin relative paths: blocks "//evil.com", "https://evil.com" and "/\\evil.com". */
|
|
15
15
|
declare function safeRedirectPath(to: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* The content branch a preview token was minted on, or null for main. Read UNVERIFIED: an adapter
|
|
18
|
+
* only stores a token the backend accepted, and branch reads need the site's draft key regardless.
|
|
19
|
+
* Pass it as the client's `branch` in draft mode and the whole site previews from that branch.
|
|
20
|
+
*/
|
|
21
|
+
declare function decodeTokenBranch(token: string | null | undefined): string | null;
|
|
16
22
|
/** A preview token's `entrySlug`, read from its payload UNVERIFIED (the backend verifies it). */
|
|
17
23
|
declare function decodeTokenSlug(token: string): string | null;
|
|
18
24
|
/**
|
|
@@ -35,4 +41,4 @@ declare function draftEnableResponse(request: Request, opts: {
|
|
|
35
41
|
/** Leave draft mode: clear the cookie and redirect (`?redirect=/x`, same-origin only). */
|
|
36
42
|
declare function draftDisableResponse(request: Request): Response;
|
|
37
43
|
|
|
38
|
-
export { DRAFT_COOKIE, decodeTokenSlug, draftDisableResponse, draftEnableResponse, draftTokenFrom, previewTokenValid, safeRedirectPath, signValue, verifyValue };
|
|
44
|
+
export { DRAFT_COOKIE, decodeTokenBranch, decodeTokenSlug, draftDisableResponse, draftEnableResponse, draftTokenFrom, previewTokenValid, safeRedirectPath, signValue, verifyValue };
|
package/dist/draft.js
CHANGED
|
@@ -26,6 +26,19 @@ async function verifyValue(signed, secret) {
|
|
|
26
26
|
function safeRedirectPath(to) {
|
|
27
27
|
return to.startsWith("/") && !to.startsWith("//") && !to.startsWith("/\\") ? to : "/";
|
|
28
28
|
}
|
|
29
|
+
function tokenPayload(token) {
|
|
30
|
+
try {
|
|
31
|
+
const part = token.split(".")[1];
|
|
32
|
+
if (!part) return null;
|
|
33
|
+
const bytes = Uint8Array.from(atob(part.replace(/-/g, "+").replace(/_/g, "/")), (c) => c.charCodeAt(0));
|
|
34
|
+
return JSON.parse(new TextDecoder().decode(bytes));
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function decodeTokenBranch(token) {
|
|
40
|
+
return token && tokenPayload(token)?.branch || null;
|
|
41
|
+
}
|
|
29
42
|
function decodeTokenSlug(token) {
|
|
30
43
|
try {
|
|
31
44
|
const part = token.split(".")[1];
|
|
@@ -73,6 +86,7 @@ function draftDisableResponse(request) {
|
|
|
73
86
|
}
|
|
74
87
|
export {
|
|
75
88
|
DRAFT_COOKIE,
|
|
89
|
+
decodeTokenBranch,
|
|
76
90
|
decodeTokenSlug,
|
|
77
91
|
draftDisableResponse,
|
|
78
92
|
draftEnableResponse,
|
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\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":[]}
|
|
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\nfunction tokenPayload(token: string): { entrySlug?: string; branch?: 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 return JSON.parse(new TextDecoder().decode(bytes)) as { entrySlug?: string; branch?: string };\n } catch {\n return null;\n }\n}\n\n/**\n * The content branch a preview token was minted on, or null for main. Read UNVERIFIED: an adapter\n * only stores a token the backend accepted, and branch reads need the site's draft key regardless.\n * Pass it as the client's `branch` in draft mode and the whole site previews from that branch.\n */\nexport function decodeTokenBranch(token: string | null | undefined): string | null {\n return (token && tokenPayload(token)?.branch) || null;\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;AAEA,SAAS,aAAa,OAA+D;AACnF,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,WAAO,KAAK,MAAM,IAAI,YAAY,EAAE,OAAO,KAAK,CAAC;AAAA,EACnD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,SAAS,kBAAkB,OAAiD;AACjF,SAAQ,SAAS,aAAa,KAAK,GAAG,UAAW;AACnD;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.cjs
CHANGED
|
@@ -110,6 +110,7 @@ __export(index_exports, {
|
|
|
110
110
|
resetPublishedContentCache: () => resetPublishedContentCache,
|
|
111
111
|
resolveSeo: () => resolveSeo,
|
|
112
112
|
resolveStega: () => resolveStega,
|
|
113
|
+
resolved: () => resolved,
|
|
113
114
|
revokeApiKey: () => revokeApiKey,
|
|
114
115
|
rich: () => rich,
|
|
115
116
|
search: () => search,
|
|
@@ -281,7 +282,7 @@ async function listContent(client, opts) {
|
|
|
281
282
|
perPage: String(opts?.perPage ?? 20)
|
|
282
283
|
});
|
|
283
284
|
const data = await client.fetchJSON(
|
|
284
|
-
`${client.
|
|
285
|
+
`${client.pagesUrl()}?${params2}`
|
|
285
286
|
);
|
|
286
287
|
return data.data;
|
|
287
288
|
}
|
|
@@ -306,7 +307,7 @@ function listContentAll(client, opts) {
|
|
|
306
307
|
perPage: String(perPage)
|
|
307
308
|
});
|
|
308
309
|
const result = await client.fetchJSON(
|
|
309
|
-
`${client.
|
|
310
|
+
`${client.pagesUrl()}?${params2}`
|
|
310
311
|
);
|
|
311
312
|
const pageData = result.data;
|
|
312
313
|
if (!pageData?.items?.length) break;
|
|
@@ -362,6 +363,10 @@ var BetterCMSDeliveryClient = class {
|
|
|
362
363
|
throw error;
|
|
363
364
|
}
|
|
364
365
|
}
|
|
366
|
+
/** The published pages list (paginated). */
|
|
367
|
+
pagesUrl() {
|
|
368
|
+
return `${this.baseUrl}/${this.workspace}/pages`;
|
|
369
|
+
}
|
|
365
370
|
/** Build the full URL for a content path. */
|
|
366
371
|
url(path) {
|
|
367
372
|
return `${this.baseUrl}/${this.workspace}/content/${path}`;
|
|
@@ -498,7 +503,7 @@ function addWebhookMethods(client, projectId) {
|
|
|
498
503
|
}
|
|
499
504
|
|
|
500
505
|
// src/admin-client.ts
|
|
501
|
-
var DEFAULT_BASE_URL = "https://api.bettercms.ai/v1";
|
|
506
|
+
var DEFAULT_BASE_URL = "https://api.bettercms.ai/api/v1";
|
|
502
507
|
var BetterCMSAdminClient = class extends BetterCMSDeliveryClient {
|
|
503
508
|
_token;
|
|
504
509
|
constructor(options) {
|
|
@@ -974,7 +979,7 @@ async function publishManagedLayout(client, input) {
|
|
|
974
979
|
}
|
|
975
980
|
|
|
976
981
|
// src/management-client.ts
|
|
977
|
-
var DEFAULT_BASE_URL2 = "https://api.bettercms.ai/v1";
|
|
982
|
+
var DEFAULT_BASE_URL2 = "https://api.bettercms.ai/api/v1";
|
|
978
983
|
var DEFAULT_TIMEOUT2 = 1e4;
|
|
979
984
|
var BetterCMSManagementClient = class extends BetterCMSDeliveryClient {
|
|
980
985
|
timeout;
|
|
@@ -1254,7 +1259,7 @@ function buildAuthResult(data) {
|
|
|
1254
1259
|
}
|
|
1255
1260
|
|
|
1256
1261
|
// src/client.ts
|
|
1257
|
-
var DEFAULT_BASE_URL3 = "https://api.bettercms.ai/v1";
|
|
1262
|
+
var DEFAULT_BASE_URL3 = "https://api.bettercms.ai/api/v1/delivery";
|
|
1258
1263
|
var BetterCMS = {
|
|
1259
1264
|
/**
|
|
1260
1265
|
* Returns a client for fetching published content.
|
|
@@ -1349,6 +1354,7 @@ function createClient(options) {
|
|
|
1349
1354
|
const apiUrl = options.apiUrl.replace(/\/+$/, "");
|
|
1350
1355
|
const { workspace, apiKey, perspective = "published", previewToken } = options;
|
|
1351
1356
|
const defaultProjectId = options.projectId;
|
|
1357
|
+
const branch = options.branch && options.branch !== "main" ? options.branch : void 0;
|
|
1352
1358
|
const timeout = options.timeout ?? DEFAULT_TIMEOUT3;
|
|
1353
1359
|
const base = `${apiUrl}/api/v1/delivery/${workspace}`;
|
|
1354
1360
|
const headers = () => {
|
|
@@ -1398,7 +1404,7 @@ function createClient(options) {
|
|
|
1398
1404
|
return {
|
|
1399
1405
|
perspective,
|
|
1400
1406
|
async getEntry(slug, opts) {
|
|
1401
|
-
const url = draft && previewToken ? `${apiUrl}/api/v1/preview/${encodeURIComponent(slug)}${qs({
|
|
1407
|
+
const url = draft && previewToken && tokenSlug(previewToken) === slug ? `${apiUrl}/api/v1/preview/${encodeURIComponent(slug)}${qs({
|
|
1402
1408
|
token: previewToken,
|
|
1403
1409
|
depth: opts?.depth,
|
|
1404
1410
|
select: opts?.select?.join(","),
|
|
@@ -1408,7 +1414,8 @@ function createClient(options) {
|
|
|
1408
1414
|
select: opts?.select?.join(","),
|
|
1409
1415
|
perspective: draft ? "draft" : void 0,
|
|
1410
1416
|
stega: draft && stega ? "1" : void 0,
|
|
1411
|
-
projectId: defaultProjectId
|
|
1417
|
+
projectId: defaultProjectId,
|
|
1418
|
+
branch
|
|
1412
1419
|
})}`;
|
|
1413
1420
|
return unwrap(await fetchJSON(url));
|
|
1414
1421
|
},
|
|
@@ -1421,7 +1428,8 @@ function createClient(options) {
|
|
|
1421
1428
|
select: opts?.select?.join(","),
|
|
1422
1429
|
perspective: draft ? "draft" : void 0,
|
|
1423
1430
|
stega: draft && stega ? "1" : void 0,
|
|
1424
|
-
projectId: defaultProjectId
|
|
1431
|
+
projectId: defaultProjectId,
|
|
1432
|
+
branch
|
|
1425
1433
|
})}`;
|
|
1426
1434
|
return unwrap(await fetchJSON(url));
|
|
1427
1435
|
},
|
|
@@ -1458,7 +1466,8 @@ function createClient(options) {
|
|
|
1458
1466
|
const url = `${base}/pages${qs({
|
|
1459
1467
|
page: opts?.page,
|
|
1460
1468
|
perPage: opts?.perPage,
|
|
1461
|
-
projectId: defaultProjectId
|
|
1469
|
+
projectId: defaultProjectId,
|
|
1470
|
+
branch
|
|
1462
1471
|
})}`;
|
|
1463
1472
|
return unwrap(await fetchJSON(url));
|
|
1464
1473
|
},
|
|
@@ -1466,12 +1475,13 @@ function createClient(options) {
|
|
|
1466
1475
|
const url = `${base}/components${qs({
|
|
1467
1476
|
perspective: draft ? "draft" : void 0,
|
|
1468
1477
|
token: draft ? previewToken : void 0,
|
|
1469
|
-
projectId: defaultProjectId
|
|
1478
|
+
projectId: defaultProjectId,
|
|
1479
|
+
branch
|
|
1470
1480
|
})}`;
|
|
1471
1481
|
return unwrap(await fetchJSON(url));
|
|
1472
1482
|
},
|
|
1473
1483
|
async getLayout() {
|
|
1474
|
-
const url = `${base}/layout${qs({ perspective: draft ? "draft" : void 0, projectId: defaultProjectId })}`;
|
|
1484
|
+
const url = `${base}/layout${qs({ perspective: draft ? "draft" : void 0, projectId: defaultProjectId, branch })}`;
|
|
1475
1485
|
try {
|
|
1476
1486
|
return unwrap(await fetchJSON(url));
|
|
1477
1487
|
} catch (error) {
|
|
@@ -1481,7 +1491,8 @@ function createClient(options) {
|
|
|
1481
1491
|
},
|
|
1482
1492
|
async listForms(opts) {
|
|
1483
1493
|
const url = `${base}/forms${qs({
|
|
1484
|
-
projectId: opts?.projectId ?? defaultProjectId
|
|
1494
|
+
projectId: opts?.projectId ?? defaultProjectId,
|
|
1495
|
+
branch
|
|
1485
1496
|
})}`;
|
|
1486
1497
|
return unwrap(
|
|
1487
1498
|
await fetchJSON(url)
|
|
@@ -1591,6 +1602,14 @@ function embeds(value) {
|
|
|
1591
1602
|
const e = value.embeds;
|
|
1592
1603
|
return typeof e === "object" && e !== null && typeof e.cards === "object" && e.cards !== null ? e : null;
|
|
1593
1604
|
}
|
|
1605
|
+
var resolvedMap = (r) => typeof r === "object" && r !== null && !Array.isArray(r) ? r : null;
|
|
1606
|
+
function resolved(value, entry) {
|
|
1607
|
+
const own = isRichText(value) ? resolvedMap(value.resolved) : null;
|
|
1608
|
+
if (own) return own;
|
|
1609
|
+
if (entry) return resolvedMap(entry.resolved);
|
|
1610
|
+
const v = value;
|
|
1611
|
+
return v && typeof v === "object" && !Array.isArray(v) && "data" in v ? resolvedMap(v.resolved) : null;
|
|
1612
|
+
}
|
|
1594
1613
|
function isRichText(value) {
|
|
1595
1614
|
return typeof value === "object" && value !== null && !Array.isArray(value) && ("html" in value || "format" in value);
|
|
1596
1615
|
}
|
|
@@ -2189,6 +2208,7 @@ function inEditor(win = globalThis.window) {
|
|
|
2189
2208
|
resetPublishedContentCache,
|
|
2190
2209
|
resolveSeo,
|
|
2191
2210
|
resolveStega,
|
|
2211
|
+
resolved,
|
|
2192
2212
|
revokeApiKey,
|
|
2193
2213
|
rich,
|
|
2194
2214
|
search,
|