@hollware/client 0.10.0 → 0.11.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/index.d.mts CHANGED
@@ -1,5 +1,209 @@
1
1
  import { WebhookCheck, describeSite, getEntry, getTranslations, listEntries, search, signWebhook, verifyWebhook } from "@hollware/core";
2
2
  import * as z from "zod";
3
+ //#region src/preview.d.ts
4
+ /**
5
+ * What a site does with a preview link (T-103 of docs/security/threat-model.md): the token travels
6
+ * in a URL once, and in a cookie after that, so it does not stay in the address bar, the history,
7
+ * a `Referer` or a copied link. These are the pure parts, shared by `@hollware/nuxt` and
8
+ * `@hollware/astro`: the integrations only wire them to their framework's requests.
9
+ */
10
+ /** The cookie that holds the token of a preview, on the site's own origin. */
11
+ export declare const PREVIEW_COOKIE = "hollware_preview";
12
+ /** How long the cookie lives, in seconds: the hour of the token. */
13
+ export declare const PREVIEW_COOKIE_MAX_AGE = 3600;
14
+ /** Whether a value is shaped like a preview token. A value that is not is never kept. */
15
+ export declare function isPreviewToken(value: unknown): value is string;
16
+ /**
17
+ * The preview token of a `Cookie` request header, or `undefined`: none, or one that is not shaped
18
+ * like a token.
19
+ */
20
+ export declare function previewTokenFromCookie(header: string | null | undefined): string | undefined;
21
+ /** What to answer a request that carries `?hollware-preview=`. */
22
+ interface PreviewExchange {
23
+ /** The path and query of the same URL without the parameter, other parameters kept in order. */
24
+ readonly location: string;
25
+ /** The token, when it is shaped like one; `undefined` when it is not, and no cookie is set. */
26
+ readonly token: string | undefined;
27
+ }
28
+ /**
29
+ * The exchange of a URL that carries `?hollware-preview=<token>`: where to redirect (`303`) and
30
+ * the token to keep in a cookie, or `undefined` when the URL has no such parameter. The token is
31
+ * not checked here: only Hollware can, when the page reads.
32
+ */
33
+ export declare function exchangePreviewLink(url: URL | string, makeNonce?: () => string): PreviewExchange | undefined;
34
+ /**
35
+ * Whether the cookie is `Secure` on a request for this URL: always, but on `http://localhost` and
36
+ * `http://127.0.0.1`, so local development works over plain HTTP.
37
+ */
38
+ export declare function isSecureRequest(url: URL | string): boolean;
39
+ /** The `Set-Cookie` that keeps a token for its hour: no script reads it, and only this site gets it. */
40
+ export declare function previewCookie(token: string, options: {
41
+ readonly secure: boolean;
42
+ }): string;
43
+ /** The `Set-Cookie` that removes the cookie. */
44
+ export declare function clearedPreviewCookie(options: {
45
+ readonly secure: boolean;
46
+ }): string;
47
+ /** The headers of the redirect that exchanges a preview link: no cache, and no `Referer` to follow. */
48
+ export declare function exchangeHeaders(exchange: PreviewExchange, url: URL | string): Array<[string, string]>;
49
+ /**
50
+ * Whether an error is Hollware's refusal of a token as expired or invalid, its code
51
+ * `invalid_preview` (over the binding, or over HTTP as a `HollwareClientError`), looking through
52
+ * `cause`. A token for another entry is no refusal: Hollware answers it as no entry, and it must
53
+ * stay so.
54
+ */
55
+ export declare function isPreviewRefusal(error: unknown): boolean;
56
+ //#endregion
57
+ //#region src/hollware.d.ts
58
+ /** Where to read from: a reader (a service binding is one), or a Hollware URL and a read token. */
59
+ type ReaderOptions = {
60
+ /**
61
+ * A service binding to the `Hollware` entrypoint, or a function that gives it: on Workers a
62
+ * binding exists only while a request runs, so a module-level `createHollware` passes
63
+ * `() => hollwareBinding(env.HOLLWARE)`.
64
+ */
65
+ readonly reader: HollwareReader | (() => HollwareReader);
66
+ } | {
67
+ /** The public URL of the Hollware. */
68
+ readonly url: string;
69
+ /** A read token (`hw_read_`, from `hollware token create --read`). */
70
+ readonly token: string;
71
+ /** `fetch`, when not the global one. */
72
+ readonly fetch?: (request: Request) => Promise<Response>;
73
+ };
74
+ /** The options of `createHollware`: the key of the site, and where to read it from. */
75
+ type HollwareOptions = ReaderOptions & {
76
+ /** The key of the site in the Hollware config. */
77
+ readonly site: string;
78
+ };
79
+ /** What a page shows of an entry in a listing: no verdict, no revision, no state of review. */
80
+ interface PageSummary {
81
+ readonly id: string;
82
+ readonly collection: string;
83
+ readonly locale: string;
84
+ readonly slug: string;
85
+ readonly translationGroup: string | null;
86
+ readonly frontmatter: Record<string, unknown>;
87
+ readonly tags: readonly string[];
88
+ readonly publishedAt: string | null;
89
+ readonly updatedAt: string;
90
+ }
91
+ /** The document of an entry: what `<HollwareContent>` of `@hollware/react` and `@hollware/vue` renders. */
92
+ interface PageDocument {
93
+ readonly nodes: readonly unknown[];
94
+ readonly frontmatter: Record<string, unknown>;
95
+ readonly meta: Record<string, unknown>;
96
+ }
97
+ /**
98
+ * An entry for a page. A framework that renders on the server and hydrates in the browser sends
99
+ * what a loader returns to the browser, so the entry holds only what a reader sees: its verdict,
100
+ * its revision and its last publication stay on the server.
101
+ */
102
+ interface PageEntry extends PageSummary {
103
+ readonly document?: PageDocument;
104
+ }
105
+ /** What `getEntry` answers: the entry, and whether it is a preview. */
106
+ interface EntryAnswer {
107
+ readonly entry: PageEntry;
108
+ /**
109
+ * `true` when the entry was read with a preview token, so it may be a draft and nothing may keep
110
+ * the page: answer it `Cache-Control: no-store` (`previewHeaders` does).
111
+ */
112
+ readonly preview: boolean;
113
+ }
114
+ /** The options of `getEntry`. */
115
+ interface GetEntryOptions {
116
+ /**
117
+ * The incoming request: its `hollware_preview` cookie, when it has one, is the preview token
118
+ * (`src/preview.ts`).
119
+ */
120
+ readonly request?: Request;
121
+ /**
122
+ * The value of the `hollware_preview` cookie, when the framework gives cookies on their own
123
+ * (`cookies()` of Next.js): it wins over `request`, and a value that is no token is ignored.
124
+ */
125
+ readonly preview?: string;
126
+ }
127
+ /** What a listing narrows to. */
128
+ interface ListEntriesOptions {
129
+ readonly locale?: string;
130
+ readonly tag?: string;
131
+ /** The most entries to return, 100 at the most; Hollware's default when not set. */
132
+ readonly limit?: number;
133
+ /** The `nextCursor` of the previous page. */
134
+ readonly cursor?: string;
135
+ }
136
+ /** A page of a listing: published entries, newest first. */
137
+ interface EntryPage {
138
+ readonly entries: PageSummary[];
139
+ readonly nextCursor: string | null;
140
+ }
141
+ /** The same entry in another locale. */
142
+ interface Translation {
143
+ readonly locale: string;
144
+ readonly slug: string;
145
+ }
146
+ /** The read side of one Hollware site, for any code that turns a `Request` into data. */
147
+ interface Hollware {
148
+ /**
149
+ * The entry in its locale, with its Comark document, or `undefined` when it is not published
150
+ * (or, with a preview token, does not exist). With `request`, a preview token in its cookie
151
+ * reads the latest revision of the entry the token names, a draft included. A token that is
152
+ * expired or not valid is an error, never the published version: send it to
153
+ * `previewHeaders(request, error)` to clear the cookie.
154
+ */
155
+ getEntry(collection: string, locale: string, slug: string, options?: GetEntryOptions): Promise<EntryAnswer | undefined>;
156
+ /** The published entries of a collection, newest first. */
157
+ listEntries(collection: string, options?: ListEntriesOptions): Promise<EntryPage>;
158
+ /** The published entries of a translation group of a collection, one per locale. */
159
+ getTranslations(collection: string, group: string): Promise<Translation[]>;
160
+ }
161
+ /**
162
+ * The read side of a Hollware site for any app that renders on a server, whatever its framework: a React Router
163
+ * loader, a Next.js Server Component, a TanStack Start server function. It reads published
164
+ * content, through a service binding or over HTTP, and it never runs in the browser: a loader's
165
+ * data goes there, and `PageEntry` holds only what a reader sees.
166
+ *
167
+ * ```ts
168
+ * const hollware = createHollware({ site: 'my-site', reader: () => hollwareBinding(env.HOLLWARE) })
169
+ * const answer = await hollware.getEntry('blog', 'en', slug, { request })
170
+ * ```
171
+ */
172
+ export declare function createHollware(options: HollwareOptions): Hollware;
173
+ //#endregion
174
+ //#region src/preview-request.d.ts
175
+ /**
176
+ * Previews of drafts (T-103 of docs/security/threat-model.md) for any code that handles a
177
+ * `Request`: the link of the admin's "Preview on the site" carries `?hollware-preview=<token>`,
178
+ * and the site exchanges it once for an `HttpOnly` cookie, so the token does not stay in the
179
+ * address bar, the history or a `Referer`. The shape of the token, the cookie and the URL without
180
+ * the parameter are pure functions of `@hollware/client`.
181
+ */
182
+ /**
183
+ * The answer to the link of a preview: a `303` to the same URL without the parameter, with the
184
+ * token in the `hollware_preview` cookie (an hour, `HttpOnly`, `SameSite=Lax`, `Secure` but on
185
+ * `http://localhost`), `no-store` and `Referrer-Policy: no-referrer`. `null` when the request is
186
+ * not a `GET` or `HEAD` with that parameter. The token is not checked: Hollware does, when the
187
+ * page reads. A value not shaped like a token sets no cookie.
188
+ */
189
+ export declare function previewRedirect(request: Request): Response | null;
190
+ /**
191
+ * The `Set-Cookie` that removes the preview cookie when `error` says Hollware refused the token
192
+ * as expired or not valid, and `null` for any other error. A token for another entry is no
193
+ * refusal: Hollware answers it as no entry, and the cookie stays.
194
+ */
195
+ export declare function previewCookieClear(error: unknown, request: Request): string | null;
196
+ /** Whether the request carries a preview token in its cookie. */
197
+ export declare function isPreviewRequest(request: Request): boolean;
198
+ /**
199
+ * The headers a page of a visitor who previews must carry: `Cache-Control: no-store` for any
200
+ * request that holds a token, whatever Hollware answered, and the `Set-Cookie` that clears it
201
+ * when `error` is a refusal. Without a token it is empty, so a published page is cached as
202
+ * usual. Merge it into the headers of the response (a `Headers` is what `data()`, `Response`
203
+ * and `NextResponse` take).
204
+ */
205
+ export declare function previewHeaders(request: Request, error?: unknown): Headers;
206
+ //#endregion
3
207
  //#region src/index.d.ts
4
208
  export declare const name = "@hollware/client";
5
209
  type Output<Spec extends {
@@ -17,6 +221,13 @@ export interface HollwareReader {
17
221
  getEntry(site: string, collection: string, slug: string, locale: string, options?: {
18
222
  readonly formats?: readonly Format[];
19
223
  readonly related?: boolean;
224
+ /**
225
+ * A preview token (`hw_prv_…`, made in the admin, or the `hollware-preview` of a page's
226
+ * URL): the entry it names is read in its latest revision, a draft included, and the answer
227
+ * must never be cached. A token that does not hold is an `invalid_input` error (a
228
+ * `HollwareClientError` over HTTP), never the published version in its place.
229
+ */
230
+ readonly preview?: string;
20
231
  }): Promise<Output<typeof getEntry>>;
21
232
  listEntries(site: string, collection: string, options?: {
22
233
  readonly locale?: string;
@@ -59,5 +270,11 @@ export declare class HollwareClientError extends Error {
59
270
  * of the HTTP reader only has to be that site's key.
60
271
  */
61
272
  export declare function createClient(options: ClientOptions): HollwareReader;
273
+ /**
274
+ * A service binding to the `Hollware` entrypoint as a reader. `wrangler types` cannot see across
275
+ * Workers, so the binding comes typed as `Service`: this checks it is there, and gives the
276
+ * mistake a name when it is not. `@hollware/astro`, `@hollware/react` and `@hollware/vue` re-export it.
277
+ */
278
+ export declare function hollwareBinding(binding: unknown, name?: string): HollwareReader;
62
279
  //#endregion
63
- export { type WebhookCheck, signWebhook, verifyWebhook };
280
+ export { type EntryAnswer, type EntryPage, type GetEntryOptions, type Hollware, type HollwareOptions, type ListEntriesOptions, type PageDocument, type PageEntry, type PageSummary, type PreviewExchange, type ReaderOptions, type Translation, type WebhookCheck, signWebhook, verifyWebhook };
package/dist/index.mjs CHANGED
@@ -1,4 +1,242 @@
1
- import { signWebhook, verifyWebhook } from "@hollware/core";
1
+ import { PREVIEW_QUERY_PARAM, signWebhook, verifyWebhook } from "@hollware/core";
2
+ //#region src/preview.ts
3
+ /**
4
+ * What a site does with a preview link (T-103 of docs/security/threat-model.md): the token travels
5
+ * in a URL once, and in a cookie after that, so it does not stay in the address bar, the history,
6
+ * a `Referer` or a copied link. These are the pure parts, shared by `@hollware/nuxt` and
7
+ * `@hollware/astro`: the integrations only wire them to their framework's requests.
8
+ */
9
+ /** The cookie that holds the token of a preview, on the site's own origin. */
10
+ const PREVIEW_COOKIE = "hollware_preview";
11
+ /** How long the cookie lives, in seconds: the hour of the token. */
12
+ const PREVIEW_COOKIE_MAX_AGE = 3600;
13
+ /** The shape of a token, `hw_prv_<base64url>.<base64url>`. The site cannot check more: Hollware does. */
14
+ const TOKEN_SHAPE = /^hw_prv_[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
15
+ /** Whether a value is shaped like a preview token. A value that is not is never kept. */
16
+ function isPreviewToken(value) {
17
+ return typeof value === "string" && TOKEN_SHAPE.test(value);
18
+ }
19
+ /**
20
+ * The preview token of a `Cookie` request header, or `undefined`: none, or one that is not shaped
21
+ * like a token.
22
+ */
23
+ function previewTokenFromCookie(header) {
24
+ if (header === null || header === void 0) return void 0;
25
+ for (const part of header.split(";")) {
26
+ const at = part.indexOf("=");
27
+ if (at === -1 || part.slice(0, at).trim() !== "hollware_preview") continue;
28
+ const value = part.slice(at + 1).trim();
29
+ return isPreviewToken(value) ? value : void 0;
30
+ }
31
+ }
32
+ /**
33
+ * The exchange of a URL that carries `?hollware-preview=<token>`: where to redirect (`303`) and
34
+ * the token to keep in a cookie, or `undefined` when the URL has no such parameter. The token is
35
+ * not checked here: only Hollware can, when the page reads.
36
+ */
37
+ function exchangePreviewLink(url, makeNonce = nonce) {
38
+ const { pathname, search } = typeof url === "string" ? new URL(url) : url;
39
+ const kept = [];
40
+ let found;
41
+ let seen = false;
42
+ for (const pair of search.slice(1).split("&")) {
43
+ if (pair === "") continue;
44
+ const at = pair.indexOf("=");
45
+ if (safeDecode(at === -1 ? pair : pair.slice(0, at)) !== PREVIEW_QUERY_PARAM) {
46
+ kept.push(pair);
47
+ continue;
48
+ }
49
+ if (seen) continue;
50
+ seen = true;
51
+ found = at === -1 ? "" : safeDecode(pair.slice(at + 1));
52
+ }
53
+ if (!seen) return void 0;
54
+ if (isPreviewToken(found)) kept.push(`${PREVIEW_VIEW_PARAM}=${makeNonce()}`);
55
+ return {
56
+ location: kept.length === 0 ? pathname : `${pathname}?${kept.join("&")}`,
57
+ token: isPreviewToken(found) ? found : void 0
58
+ };
59
+ }
60
+ /** The parameter that keeps a previewed page out of every cache: a random value, no secret. */
61
+ const PREVIEW_VIEW_PARAM = "hollware-view";
62
+ function nonce() {
63
+ const bytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(9));
64
+ return btoa(String.fromCharCode(...bytes)).replaceAll("+", "-").replaceAll("/", "_");
65
+ }
66
+ function safeDecode(text) {
67
+ try {
68
+ return decodeURIComponent(text.replaceAll("+", " "));
69
+ } catch {
70
+ return text;
71
+ }
72
+ }
73
+ /**
74
+ * Whether the cookie is `Secure` on a request for this URL: always, but on `http://localhost` and
75
+ * `http://127.0.0.1`, so local development works over plain HTTP.
76
+ */
77
+ function isSecureRequest(url) {
78
+ const { protocol, hostname } = typeof url === "string" ? new URL(url) : url;
79
+ return !(protocol === "http:" && (hostname === "localhost" || hostname === "127.0.0.1"));
80
+ }
81
+ /** The `Set-Cookie` that keeps a token for its hour: no script reads it, and only this site gets it. */
82
+ function previewCookie(token, options) {
83
+ return cookieLine(token, PREVIEW_COOKIE_MAX_AGE, options.secure);
84
+ }
85
+ /** The `Set-Cookie` that removes the cookie. */
86
+ function clearedPreviewCookie(options) {
87
+ return cookieLine("", 0, options.secure);
88
+ }
89
+ function cookieLine(value, maxAge, secure) {
90
+ return `${PREVIEW_COOKIE}=${value}; Path=/; HttpOnly;${secure ? " Secure;" : ""} SameSite=Lax; Max-Age=${maxAge}`;
91
+ }
92
+ /** The headers of the redirect that exchanges a preview link: no cache, and no `Referer` to follow. */
93
+ function exchangeHeaders(exchange, url) {
94
+ const headers = [
95
+ ["location", exchange.location],
96
+ ["cache-control", "no-store"],
97
+ ["referrer-policy", "no-referrer"]
98
+ ];
99
+ if (exchange.token !== void 0) headers.push(["set-cookie", previewCookie(exchange.token, { secure: isSecureRequest(url) })]);
100
+ return headers;
101
+ }
102
+ /**
103
+ * Whether an error is Hollware's refusal of a token as expired or invalid, its code
104
+ * `invalid_preview` (over the binding, or over HTTP as a `HollwareClientError`), looking through
105
+ * `cause`. A token for another entry is no refusal: Hollware answers it as no entry, and it must
106
+ * stay so.
107
+ */
108
+ function isPreviewRefusal(error) {
109
+ let current = error;
110
+ for (let depth = 0; depth < 4 && typeof current === "object" && current !== null; depth++) {
111
+ if (Reflect.get(current, "code") === "invalid_preview") return true;
112
+ current = Reflect.get(current, "cause");
113
+ }
114
+ return false;
115
+ }
116
+ //#endregion
117
+ //#region src/hollware.ts
118
+ function readerOf(options) {
119
+ if ("reader" in options) return typeof options.reader === "function" ? options.reader() : options.reader;
120
+ return createClient({
121
+ url: options.url,
122
+ token: options.token,
123
+ fetch: options.fetch
124
+ });
125
+ }
126
+ /** The fields of a summary that a page may see, picked one by one so no other one travels. */
127
+ function summaryOf(entry) {
128
+ return {
129
+ id: entry.id,
130
+ collection: entry.collection,
131
+ locale: entry.locale,
132
+ slug: entry.slug,
133
+ translationGroup: entry.translationGroup,
134
+ frontmatter: entry.frontmatter,
135
+ tags: entry.tags,
136
+ publishedAt: entry.publishedAt,
137
+ updatedAt: entry.updatedAt
138
+ };
139
+ }
140
+ /**
141
+ * The read side of a Hollware site for any app that renders on a server, whatever its framework: a React Router
142
+ * loader, a Next.js Server Component, a TanStack Start server function. It reads published
143
+ * content, through a service binding or over HTTP, and it never runs in the browser: a loader's
144
+ * data goes there, and `PageEntry` holds only what a reader sees.
145
+ *
146
+ * ```ts
147
+ * const hollware = createHollware({ site: 'my-site', reader: () => hollwareBinding(env.HOLLWARE) })
148
+ * const answer = await hollware.getEntry('blog', 'en', slug, { request })
149
+ * ```
150
+ */
151
+ function createHollware(options) {
152
+ const { site } = options;
153
+ return {
154
+ async getEntry(collection, locale, slug, getOptions = {}) {
155
+ const preview = isPreviewToken(getOptions.preview) ? getOptions.preview : previewTokenFromCookie(getOptions.request?.headers.get("cookie"));
156
+ const answer = await readerOf(options).getEntry(site, collection, slug, locale, {
157
+ formats: ["document"],
158
+ ...preview === void 0 ? {} : { preview }
159
+ });
160
+ if (answer.outcome === "not_found" || answer.entry === void 0) return void 0;
161
+ const { document } = answer.entry;
162
+ return {
163
+ entry: {
164
+ ...summaryOf(answer.entry),
165
+ ...document === void 0 ? {} : { document }
166
+ },
167
+ preview: preview !== void 0
168
+ };
169
+ },
170
+ async listEntries(collection, listOptions = {}) {
171
+ const page = await readerOf(options).listEntries(site, collection, listOptions);
172
+ return {
173
+ entries: page.entries.map(summaryOf),
174
+ nextCursor: page.nextCursor
175
+ };
176
+ },
177
+ async getTranslations(collection, group) {
178
+ return ((await readerOf(options).getTranslations(site, collection, group)).entries ?? []).map((entry) => ({
179
+ locale: entry.locale,
180
+ slug: entry.slug
181
+ }));
182
+ }
183
+ };
184
+ }
185
+ //#endregion
186
+ //#region src/preview-request.ts
187
+ /**
188
+ * Previews of drafts (T-103 of docs/security/threat-model.md) for any code that handles a
189
+ * `Request`: the link of the admin's "Preview on the site" carries `?hollware-preview=<token>`,
190
+ * and the site exchanges it once for an `HttpOnly` cookie, so the token does not stay in the
191
+ * address bar, the history or a `Referer`. The shape of the token, the cookie and the URL without
192
+ * the parameter are pure functions of `@hollware/client`.
193
+ */
194
+ /**
195
+ * The answer to the link of a preview: a `303` to the same URL without the parameter, with the
196
+ * token in the `hollware_preview` cookie (an hour, `HttpOnly`, `SameSite=Lax`, `Secure` but on
197
+ * `http://localhost`), `no-store` and `Referrer-Policy: no-referrer`. `null` when the request is
198
+ * not a `GET` or `HEAD` with that parameter. The token is not checked: Hollware does, when the
199
+ * page reads. A value not shaped like a token sets no cookie.
200
+ */
201
+ function previewRedirect(request) {
202
+ if (request.method !== "GET" && request.method !== "HEAD") return null;
203
+ const url = new URL(request.url);
204
+ const exchange = exchangePreviewLink(url);
205
+ if (exchange === void 0) return null;
206
+ return new Response(null, {
207
+ status: 303,
208
+ headers: exchangeHeaders(exchange, url)
209
+ });
210
+ }
211
+ /**
212
+ * The `Set-Cookie` that removes the preview cookie when `error` says Hollware refused the token
213
+ * as expired or not valid, and `null` for any other error. A token for another entry is no
214
+ * refusal: Hollware answers it as no entry, and the cookie stays.
215
+ */
216
+ function previewCookieClear(error, request) {
217
+ if (!isPreviewRefusal(error)) return null;
218
+ return clearedPreviewCookie({ secure: isSecureRequest(request.url) });
219
+ }
220
+ /** Whether the request carries a preview token in its cookie. */
221
+ function isPreviewRequest(request) {
222
+ return previewTokenFromCookie(request.headers.get("cookie")) !== void 0;
223
+ }
224
+ /**
225
+ * The headers a page of a visitor who previews must carry: `Cache-Control: no-store` for any
226
+ * request that holds a token, whatever Hollware answered, and the `Set-Cookie` that clears it
227
+ * when `error` is a refusal. Without a token it is empty, so a published page is cached as
228
+ * usual. Merge it into the headers of the response (a `Headers` is what `data()`, `Response`
229
+ * and `NextResponse` take).
230
+ */
231
+ function previewHeaders(request, error) {
232
+ const headers = new Headers();
233
+ if (!isPreviewRequest(request)) return headers;
234
+ headers.set("cache-control", "no-store");
235
+ const clear = previewCookieClear(error, request);
236
+ if (clear !== null) headers.append("set-cookie", clear);
237
+ return headers;
238
+ }
239
+ //#endregion
2
240
  //#region src/index.ts
3
241
  const name = "@hollware/client";
4
242
  /** An answer of the HTTP read API that is no result: its code, message and fix. */
@@ -38,7 +276,8 @@ function httpReader(url, token, send) {
38
276
  return {
39
277
  getEntry: (_site, collection, slug, locale, options) => get(`${segment(collection)}/${segment(locale)}/${segment(slug)}`, {
40
278
  formats: options?.formats?.join(","),
41
- related: options?.related === true ? "true" : void 0
279
+ related: options?.related === true ? "true" : void 0,
280
+ preview: options?.preview
42
281
  }),
43
282
  listEntries: (_site, collection, options = {}) => get(segment(collection), {
44
283
  locale: options.locale,
@@ -78,5 +317,17 @@ function createClient(options) {
78
317
  function narrow(body) {
79
318
  return body;
80
319
  }
320
+ function isReader(value) {
321
+ return typeof value === "object" && value !== null && "getEntry" in value;
322
+ }
323
+ /**
324
+ * A service binding to the `Hollware` entrypoint as a reader. `wrangler types` cannot see across
325
+ * Workers, so the binding comes typed as `Service`: this checks it is there, and gives the
326
+ * mistake a name when it is not. `@hollware/astro`, `@hollware/react` and `@hollware/vue` re-export it.
327
+ */
328
+ function hollwareBinding(binding, name = "HOLLWARE") {
329
+ if (isReader(binding)) return binding;
330
+ throw new Error(`Hollware: the Worker has no service binding ${name} to the Hollware entrypoint: add it to services in wrangler.jsonc.`);
331
+ }
81
332
  //#endregion
82
- export { HollwareClientError, createClient, name, signWebhook, verifyWebhook };
333
+ export { HollwareClientError, PREVIEW_COOKIE, PREVIEW_COOKIE_MAX_AGE, clearedPreviewCookie, createClient, createHollware, exchangeHeaders, exchangePreviewLink, hollwareBinding, isPreviewRefusal, isPreviewRequest, isPreviewToken, isSecureRequest, name, previewCookie, previewCookieClear, previewHeaders, previewRedirect, previewTokenFromCookie, signWebhook, verifyWebhook };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hollware/client",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Typed Hollware client over a service binding or HTTP.",
5
5
  "homepage": "https://hollware.com",
6
6
  "bugs": {
@@ -16,6 +16,7 @@
16
16
  "dist"
17
17
  ],
18
18
  "type": "module",
19
+ "sideEffects": false,
19
20
  "exports": {
20
21
  ".": {
21
22
  "types": "./dist/index.d.mts",
@@ -28,7 +29,7 @@
28
29
  "provenance": true
29
30
  },
30
31
  "dependencies": {
31
- "@hollware/core": "0.10.0"
32
+ "@hollware/core": "0.11.0"
32
33
  },
33
34
  "devDependencies": {
34
35
  "@types/node": "24.19.2",