@capacms/sdk 1.0.0-next.0 → 1.0.0-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +450 -0
  2. package/README.md +1754 -156
  3. package/bin/capa-codegen.js +192 -5
  4. package/bin/capa.js +235 -0
  5. package/bin/graphql-project.js +142 -0
  6. package/bin/project-env.js +58 -0
  7. package/dist/client.d.ts +5 -0
  8. package/dist/client.js +17 -0
  9. package/dist/codegen.d.ts +55 -0
  10. package/dist/codegen.js +320 -39
  11. package/dist/config.d.ts +5 -36
  12. package/dist/config.js +47 -1
  13. package/dist/esm/image/index.d.ts +120 -0
  14. package/dist/esm/image/index.js +250 -0
  15. package/dist/esm/image/shared-params.generated.d.ts +190 -0
  16. package/dist/esm/image/shared-params.generated.js +461 -0
  17. package/dist/esm/nextjs/image-loader.d.ts +60 -0
  18. package/dist/esm/nextjs/image-loader.js +67 -0
  19. package/dist/esm/nextjs/overlay.d.ts +30 -0
  20. package/dist/esm/nextjs/overlay.js +75 -0
  21. package/dist/esm/overlay/index.d.ts +32 -0
  22. package/dist/esm/overlay/index.js +576 -0
  23. package/dist/esm/overlay/protocol.d.ts +187 -0
  24. package/dist/esm/overlay/protocol.js +240 -0
  25. package/dist/esm/package.json +4 -0
  26. package/dist/graphql-codegen.d.ts +117 -0
  27. package/dist/graphql-codegen.js +705 -0
  28. package/dist/http.js +1 -1
  29. package/dist/image/index.d.ts +120 -0
  30. package/dist/image/index.js +257 -0
  31. package/dist/image/shared-params.generated.d.ts +190 -0
  32. package/dist/image/shared-params.generated.js +471 -0
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +2 -1
  35. package/dist/next/attrs.d.ts +98 -0
  36. package/dist/next/attrs.js +125 -0
  37. package/dist/next/client.d.ts +176 -32
  38. package/dist/next/client.js +212 -90
  39. package/dist/next/entry-fields.d.ts +162 -0
  40. package/dist/next/entry-fields.js +2 -0
  41. package/dist/next/errors.d.ts +136 -0
  42. package/dist/next/errors.js +214 -0
  43. package/dist/next/field-names.d.ts +37 -0
  44. package/dist/next/field-names.js +145 -0
  45. package/dist/next/graphql/build.d.ts +27 -0
  46. package/dist/next/graphql/build.js +98 -0
  47. package/dist/next/graphql/documents.d.ts +67 -0
  48. package/dist/next/graphql/documents.js +35 -0
  49. package/dist/next/graphql/edit-mode.d.ts +16 -0
  50. package/dist/next/graphql/edit-mode.js +93 -0
  51. package/dist/next/graphql/filter-values.d.ts +34 -0
  52. package/dist/next/graphql/filter-values.js +96 -0
  53. package/dist/next/graphql/introspection.d.ts +89 -0
  54. package/dist/next/graphql/introspection.js +102 -0
  55. package/dist/next/graphql/plan.d.ts +115 -0
  56. package/dist/next/graphql/plan.js +531 -0
  57. package/dist/next/graphql/request.d.ts +228 -0
  58. package/dist/next/graphql/request.js +283 -0
  59. package/dist/next/graphql/rest.d.ts +66 -0
  60. package/dist/next/graphql/rest.js +502 -0
  61. package/dist/next/graphql/selection.d.ts +55 -0
  62. package/dist/next/graphql/selection.js +212 -0
  63. package/dist/next/graphql/sha256.d.ts +13 -0
  64. package/dist/next/graphql/sha256.js +86 -0
  65. package/dist/next/graphql/summary.d.ts +83 -0
  66. package/dist/next/graphql/summary.js +151 -0
  67. package/dist/next/graphql/tree-layout.d.ts +36 -0
  68. package/dist/next/graphql/tree-layout.js +20 -0
  69. package/dist/next/graphql/tree.d.ts +171 -0
  70. package/dist/next/graphql/tree.js +249 -0
  71. package/dist/next/graphql/typed.d.ts +261 -0
  72. package/dist/next/graphql/typed.js +146 -0
  73. package/dist/next/index.d.ts +30 -3
  74. package/dist/next/index.js +34 -1
  75. package/dist/next/inflate.d.ts +51 -0
  76. package/dist/next/inflate.js +243 -0
  77. package/dist/next/key-family.d.ts +31 -0
  78. package/dist/next/key-family.js +66 -0
  79. package/dist/next/select-types.d.ts +58 -5
  80. package/dist/next/system-keys.d.ts +27 -0
  81. package/dist/next/system-keys.js +42 -0
  82. package/dist/nextjs/image-loader.d.ts +60 -0
  83. package/dist/nextjs/image-loader.js +71 -0
  84. package/dist/nextjs/index.d.ts +484 -5
  85. package/dist/nextjs/index.js +704 -9
  86. package/dist/nextjs/overlay.d.ts +30 -0
  87. package/dist/nextjs/overlay.js +78 -0
  88. package/dist/overlay/index.d.ts +32 -0
  89. package/dist/overlay/index.js +596 -0
  90. package/dist/overlay/protocol.d.ts +187 -0
  91. package/dist/overlay/protocol.js +253 -0
  92. package/package.json +70 -15
@@ -1,20 +1,55 @@
1
- import { type CapaNextClient, type CapaNextConfig, type PagesResource, type PreviewClaim } from "../next";
1
+ import { gql, LAYOUT_PAGE, type CapaDocumentResult, type CapaDocuments, type CapaDocumentVariables, type CapaNextClient, type CapaNextConfig, type GraphQLCallOptions, type GraphQLResult, type NotARecordedDocument, type PagesResource, type PreviewClaim, type TypedDocument, type UntypedQuery, type VariablesThenOptions } from "../next";
2
2
  export interface CacheOptions {
3
3
  tags?: string[];
4
4
  revalidate?: number | false;
5
5
  }
6
6
  /** Add Next.js fetch-cache options without importing `next/*`. */
7
7
  export declare function withCache(fetchImpl: typeof fetch, options: CacheOptions): typeof fetch;
8
+ /**
9
+ * The Next.js cache tag for every read of a model, by its namespace. A GraphQL
10
+ * query names its models by namespace (`articles`), not by id, so this is the
11
+ * tag a GraphQL read is cached under, and `revalidateFromWebhook` revalidates
12
+ * it when an entry of that model changes.
13
+ */
14
+ export declare function modelTag(namespace: string): string;
15
+ /**
16
+ * The tag `graphql()` keeps a read under when it gives a `revalidate` and no
17
+ * `tags`, and that `revalidateFromWebhook` revalidates on every content
18
+ * change: such a page is never stale after a publish, at the price of
19
+ * refreshing on any publish. Name the models it reads with
20
+ * `tagsFor({ namespace })` to refresh it only when one of those changes. A
21
+ * read with neither `tags` nor `revalidate` is not kept at all.
22
+ */
23
+ export declare const GRAPHQL_TAG = "capa:graphql";
24
+ /**
25
+ * The tag `tagsFor({ namespace })` adds beside its model tags, and that
26
+ * `revalidateFromWebhook` revalidates on a media event: a GraphQL read shows
27
+ * a file's URL and alt text whichever models it names, so editing a file in
28
+ * the media library refreshes it.
29
+ */
30
+ export declare const MEDIA_TAG = "capa:media";
31
+ /**
32
+ * Next.js cache tags for a read. `model`, `entry`, `key` and `tenant` are the
33
+ * API's surrogate keys (`m:`, `e:`, `k:`, `t:`), by id; `namespace` is one
34
+ * `capa:model:<namespace>` tag per model a GraphQL query reads, which
35
+ * `capa-codegen --graphql` lists as `<Name>Models`, and `MEDIA_TAG`, since
36
+ * the query may show a file from the media library. Each pairs with
37
+ * `revalidateFromWebhook`.
38
+ */
8
39
  export declare function tagsFor(input: {
9
40
  model?: string;
10
41
  entry?: string;
11
42
  key?: string;
12
43
  tenant?: string;
44
+ namespace?: string | readonly string[];
13
45
  }): string[];
14
46
  export interface WebhookPayload {
47
+ type?: unknown;
15
48
  data?: {
16
49
  instanceId?: unknown;
17
50
  modelId?: unknown;
51
+ modelNamespace?: unknown;
52
+ namespace?: unknown;
18
53
  [key: string]: unknown;
19
54
  };
20
55
  instanceId?: unknown;
@@ -22,17 +57,34 @@ export interface WebhookPayload {
22
57
  modelId?: unknown;
23
58
  [key: string]: unknown;
24
59
  }
25
- /** Revalidate the concrete entry and model identities carried by a webhook. */
60
+ /**
61
+ * Revalidate every tag a webhook's entry and model can be cached under: the
62
+ * entry (`e:`), the model by id (`m:`) and by namespace (`capa:model:`, the
63
+ * tag a GraphQL read uses). The namespace is `data.modelNamespace` on
64
+ * `instance.published`, `instance.unpublished` and `model.published`, and
65
+ * `data.namespace` on the other events (docs/WEBHOOKS.md). Any of those, and
66
+ * a `media.` event, also revalidates `GRAPHQL_TAG`, the tag of a `graphql()`
67
+ * read that named no tags. A `media.` event (a file's alt text, name or
68
+ * visibility changed, or the file went) revalidates the file's own key
69
+ * (`f:<fileId>`, as REST's `Surrogate-Key` names it) and `MEDIA_TAG`, which
70
+ * every read tagged by namespace carries, since it may show that file.
71
+ */
26
72
  export declare function revalidateFromWebhook(input: {
27
73
  payload: WebhookPayload;
28
74
  revalidateTag: (tag: string) => void | Promise<void>;
29
75
  }): Promise<string[]>;
30
- /** Select a client using server-only draft state supplied by the caller. */
31
- export declare function draftClient(input: {
76
+ /**
77
+ * Select a client using server-only draft state supplied by the caller. Pass
78
+ * codegen's `CapaQuery` to type the builder, as with `createClient`:
79
+ * `draftClient<CapaQuery>({ ... })`. `production` may hold the legacy key a
80
+ * site already has; `draft` needs a `cap_` key, and a legacy one throws a
81
+ * `TypeError` when draft mode selects it.
82
+ */
83
+ export declare function draftClient<Q = UntypedQuery>(input: {
32
84
  production: CapaNextConfig;
33
85
  draft: CapaNextConfig;
34
86
  isDraft: () => boolean | Promise<boolean>;
35
- }): Promise<CapaNextClient>;
87
+ }): Promise<CapaNextClient<Q>>;
36
88
  export declare function routeOf(file: string): string;
37
89
  /**
38
90
  * Verify a preview token from a Next route handler.
@@ -50,4 +102,431 @@ export declare function preview(token: string, client: CapaNextClient): Promise<
50
102
  * this and never hold the whole client.
51
103
  */
52
104
  export declare function pagesFor(client: CapaNextClient): PagesResource;
105
+ export { gql, LAYOUT_PAGE };
53
106
  export type { PreviewClaim };
107
+ export { capaImageLoader, createCapaImageLoader } from "./image-loader";
108
+ export type { CapaImageLoader, CapaImageLoaderOptions, CapaImageLoaderProps } from "./image-loader";
109
+ /**
110
+ * The query parameter that turns edit mode on for one request without draft
111
+ * content: `?capa-edit=<token>`, where the token is a Capa preview token. The
112
+ * Capa editor's Published view sends it so the published page is still
113
+ * clickable. It never switches the site to draft data.
114
+ */
115
+ export declare const EDIT_PARAM = "capa-edit";
116
+ /** The query parameter a Capa preview link carries: `?capa-preview=<token>`. */
117
+ export declare const PREVIEW_PARAM = "capa-preview";
118
+ /** The query parameter of the editor's Published view: `?capa-view=published`. */
119
+ export declare const VIEW_PARAM = "capa-view";
120
+ /**
121
+ * The request header `resolveEditRequest` sets once a `capa-edit` token has
122
+ * been verified, for `editMode()` to read. Any copy a browser sent is removed
123
+ * first, so it cannot be forged from outside.
124
+ */
125
+ export declare const EDIT_HEADER = "x-capa-edit";
126
+ /** The cookie Next's `draftMode().enable()` sets. */
127
+ export declare const DRAFT_COOKIE = "__prerender_bypass";
128
+ /** What every edit-mode response must send: never cached, never shared. */
129
+ export declare const EDIT_CACHE_CONTROL = "private, no-store";
130
+ interface HeaderReader {
131
+ get(name: string): string | null;
132
+ }
133
+ /**
134
+ * Whether this request renders in edit mode: Next draft mode is on, or the
135
+ * request carried a verified `capa-edit` token (see `resolveEditRequest`).
136
+ *
137
+ * Pass Next's own functions; this package imports nothing from `next`:
138
+ *
139
+ * import { draftMode, headers } from "next/headers";
140
+ * const edit = await editMode({ draftMode, headers });
141
+ * const client = createClient({ ...config, editMode: edit });
142
+ *
143
+ * A client built with `editMode: edit` marks what it reads, and `capaAttrs`
144
+ * then tags those entries and only those.
145
+ */
146
+ export declare function editMode(input: {
147
+ draftMode: () => {
148
+ isEnabled: boolean;
149
+ } | Promise<{
150
+ isEnabled: boolean;
151
+ }>;
152
+ headers: () => HeaderReader | Promise<HeaderReader>;
153
+ }): Promise<boolean>;
154
+ export interface EditRequest {
155
+ /** True when the page must render in edit mode. */
156
+ edit: boolean;
157
+ /** True when a `capa-edit` token was presented and Capa accepted it. */
158
+ verified: boolean;
159
+ /**
160
+ * The request headers to forward: `x-capa-edit` removed, then set to `1`
161
+ * when the token verified. Hand them to `NextResponse.next({ request: { headers } })`.
162
+ */
163
+ headers: Headers;
164
+ /** `EDIT_CACHE_CONTROL` in edit mode, otherwise null. Set it on the response. */
165
+ cacheControl: string | null;
166
+ /** `DRAFT_ROBOTS_TAG` in edit mode, otherwise null. Set it on the response as `X-Robots-Tag`. */
167
+ robotsTag: string | null;
168
+ }
169
+ /**
170
+ * The middleware half of edit mode.
171
+ *
172
+ * export async function middleware(request: NextRequest) {
173
+ * const edit = await resolveEditRequest(request, publishedClient());
174
+ * const response = NextResponse.next({ request: { headers: edit.headers } });
175
+ * if (edit.cacheControl) response.headers.set("Cache-Control", edit.cacheControl);
176
+ * if (edit.robotsTag) response.headers.set("X-Robots-Tag", edit.robotsTag);
177
+ * return response;
178
+ * }
179
+ *
180
+ * The token is checked with Capa (`client.preview`), exactly as a preview link
181
+ * is: the site never holds the signing key. A bad, expired or unverifiable
182
+ * token means not in edit mode; it never throws, because a broken edit link
183
+ * must still render the public page.
184
+ */
185
+ export declare function resolveEditRequest(request: {
186
+ url: string | URL;
187
+ headers: Headers;
188
+ cookies?: {
189
+ has(name: string): boolean;
190
+ };
191
+ }, client: Pick<CapaNextClient, "preview">): Promise<EditRequest>;
192
+ /**
193
+ * Where the env-driven helpers read their settings (M6): the one pair of
194
+ * names the whole product uses (the MCP server, `capa-codegen`, `capa
195
+ * persist`, every curl example in the API docs).
196
+ */
197
+ export declare const CAPA_ENV: {
198
+ readonly baseUrl: "CAPA_API_URL";
199
+ readonly apiKey: "CAPA_KEY";
200
+ readonly draftKey: "CAPA_DRAFT_KEY";
201
+ readonly version: "CAPA_API_VERSION";
202
+ };
203
+ /** Older names that still work, read only when the name above is unset. */
204
+ export declare const CAPA_ENV_ALIASES: Readonly<Record<string, string>>;
205
+ export declare const DEFAULT_API_VERSION = "2026-10-01";
206
+ type DraftModeFn = () => {
207
+ isEnabled: boolean;
208
+ enable?: () => void;
209
+ disable?: () => void;
210
+ } | Promise<{
211
+ isEnabled: boolean;
212
+ enable?: () => void;
213
+ disable?: () => void;
214
+ }>;
215
+ /**
216
+ * The published-key client from env: what verifying a token needs. `overrides`
217
+ * win over env, and pass codegen's `CapaQuery` to type the builder:
218
+ * `getPublishedClient<CapaQuery>()`.
219
+ */
220
+ export declare function getPublishedClient<Q = UntypedQuery>(overrides?: Partial<CapaNextConfig>): CapaNextClient<Q>;
221
+ /**
222
+ * The client for this request, from env: the draft key under draft mode,
223
+ * otherwise the published key, and `editMode` worked out for you.
224
+ *
225
+ * import { draftMode, headers } from "next/headers";
226
+ * const capa = await getCapaClient<CapaQuery>({ draftMode, headers });
227
+ * const { data } = await capa.graphql.query(selection, { tags: tagsFor({ namespace: "articles" }) });
228
+ *
229
+ * `CapaQuery` (from `capa-codegen --graphql`) types the builder, as with
230
+ * `createClient`; leave it out for an untyped client. `config` wins over env.
231
+ *
232
+ * Its REST reads are sent as `createClient` sends them, which Next does not
233
+ * keep. Its GraphQL reads, a document or the builder, are sent the same way
234
+ * unless a call gives `tags` or `revalidate`, so a publish shows up on both
235
+ * alike. Given either, a read is kept in Next's data cache exactly as
236
+ * `graphql()` keeps it: a published read with no errors, never a draft or an
237
+ * edit-mode page.
238
+ */
239
+ export declare function getCapaClient<Q = UntypedQuery>(input: {
240
+ draftMode: DraftModeFn;
241
+ headers: () => HeaderReader | Promise<HeaderReader>;
242
+ config?: Partial<CapaNextConfig>;
243
+ /** Next's `unstable_cache`, as `graphql()` takes it. Leave it out: Next's own is loaded. */
244
+ unstable_cache?: UnstableCache;
245
+ }): Promise<CapaNextClient<Q, NextCacheOptions>>;
246
+ /** Where a GraphQL read in Next keeps its answer: Next's data cache, as `graphql()` and `getCapaClient` keep it. */
247
+ export interface NextCacheOptions extends GraphQLCallOptions {
248
+ /**
249
+ * Next.js cache tags to keep this read under. `tagsFor({ namespace:
250
+ * <Name>Models })`, with the list `capa-codegen --graphql` writes beside
251
+ * each document, tags it with every model the query reads, which
252
+ * `revalidateFromWebhook` revalidates on a publish. Given a `revalidate`
253
+ * and no tags, the read is kept under `GRAPHQL_TAG`, which every publish
254
+ * revalidates. Given neither, the read is not kept: it is sent as a REST
255
+ * read is, on every render.
256
+ */
257
+ tags?: string[];
258
+ /** Seconds to cache, `false` to cache until a tag is revalidated, or 0 not to cache. */
259
+ revalidate?: number | false;
260
+ }
261
+ export interface NextGraphQLOptions extends NextCacheOptions {
262
+ /**
263
+ * Next's `draftMode` and `headers`, to work out draft and edit mode as
264
+ * `getCapaClient` does: drafts under draft mode, and in edit mode every
265
+ * entry the query reads marked for `capaAttrs`.
266
+ */
267
+ draftMode?: DraftModeFn;
268
+ headers?: () => HeaderReader | Promise<HeaderReader>;
269
+ /**
270
+ * Read drafts with `CAPA_DRAFT_KEY`, uncached. Worked out from `draftMode`
271
+ * when that is passed; set it to decide yourself.
272
+ */
273
+ draft?: boolean;
274
+ /** Overrides for the env-derived client (base URL, key, version, edit mode, fetch). Each one given is not read from env. */
275
+ config?: Partial<CapaNextConfig>;
276
+ /**
277
+ * Next's `unstable_cache`, from `next/cache`. Leave it out: `graphql()`
278
+ * loads Next's own. A published read given `tags` or a `revalidate` is kept
279
+ * in Next's data cache only when it answered with no `errors`, under `tags`
280
+ * for `revalidate` seconds, or with no `revalidate` until one of its tags is
281
+ * revalidated. Next's fetch
282
+ * cache is not used for it: that cache keeps any 200, and a GraphQL error
283
+ * is a 200 (a root field that timed out), and in Next 15 it keeps nothing
284
+ * without a `revalidate`, tags or not. Pass it only to supply another.
285
+ */
286
+ unstable_cache?: UnstableCache;
287
+ }
288
+ /**
289
+ * Next's `unstable_cache` (`import { unstable_cache } from "next/cache"`),
290
+ * typed here so this package never imports `next/*`.
291
+ */
292
+ export type UnstableCache = <T extends (...args: any[]) => Promise<any>>(cb: T, keyParts?: string[], options?: {
293
+ revalidate?: number | false;
294
+ tags?: string[];
295
+ }) => T;
296
+ /**
297
+ * GraphQL in a server component, one line, from env:
298
+ *
299
+ * import { draftMode, headers } from "next/headers";
300
+ * import { graphql, tagsFor } from "@capacms/sdk/nextjs";
301
+ * import { LatestModels } from "./capa-graphql";
302
+ * const LATEST = `#graphql
303
+ * query Latest($first: Int) { articles(first: $first) { nodes { id model title } } }
304
+ * `;
305
+ * const { data } = await graphql(LATEST, { first: 5 }, { draftMode, headers, tags: tagsFor({ namespace: LatestModels }) });
306
+ *
307
+ * Once `capa-codegen --graphql` has seen the literal, `data` and the
308
+ * variables are typed from it, as they are for a `<Name>Document` it writes,
309
+ * and `LatestModels` lists the models it reads.
310
+ *
311
+ * With no `tags` and no `revalidate` a read is sent as the REST reads are,
312
+ * and Next keeps nothing: the page shows a publish on its next render, as a
313
+ * page reading by REST does. Given either, a published read is kept in Next's
314
+ * data cache (`unstable_cache`) when it answered with no errors: under `tags`,
315
+ * for `revalidate` seconds, or with no `revalidate` until
316
+ * `revalidateFromWebhook` revalidates one of its tags on a publish. It goes
317
+ * as a GET, which the CDN and the API also cache for the published key; a
318
+ * document too long for a GET URL goes as a POST, which only Next's data
319
+ * cache keeps. Under draft mode the read uses `CAPA_DRAFT_KEY`, a
320
+ * `cap_` key, and bypasses the cache (the API answers a development key's GET
321
+ * `no-store`), the same split `getCapaClient` makes for REST, and in edit mode
322
+ * each entry that selects `id` and `model` is marked for `capaAttrs`, uncached.
323
+ * Outside a Next request (a script, a test) the read is simply sent.
324
+ *
325
+ * `persisted: true` sends only the hash, and pays off once the documents are
326
+ * stored with `capa persist`, which takes a development key: a production
327
+ * key never stores one. Until then a
328
+ * hash misses, and the client remembers the miss for five minutes and sends
329
+ * the document by POST meanwhile, so it costs one extra GET per five minutes.
330
+ */
331
+ export declare function graphql<D extends keyof CapaDocuments>(document: D, ...rest: VariablesThenOptions<CapaDocumentVariables<D>, NextGraphQLOptions>): Promise<GraphQLResult<CapaDocumentResult<D>>>;
332
+ export declare function graphql<TData = Record<string, unknown>, TVariables = Record<string, unknown>, D extends string = string>(document: NotARecordedDocument<D, TypedDocument<TData, TVariables> | D>, ...rest: VariablesThenOptions<TVariables, NextGraphQLOptions>): Promise<GraphQLResult<TData>>;
333
+ /** Only a path on this site: never `//elsewhere.example` or a full URL. */
334
+ export declare function safeSitePath(value: string | null | undefined): string;
335
+ /** The Capa admin's origin: what frames a draft in the editor, unless a site names another. */
336
+ export declare const CAPA_ADMIN_ORIGIN = "https://app.capacms.com";
337
+ /** `X-Robots-Tag` on every draft and edit-mode response: a draft is never indexed, nor its links followed. */
338
+ export declare const DRAFT_ROBOTS_TAG = "noindex, nofollow";
339
+ /** How long the draft cookie lasts, in seconds: one hour, as a preview token does. */
340
+ export declare const DRAFT_COOKIE_MAX_AGE = 3600;
341
+ /**
342
+ * `frame-ancestors 'self' <origins>`, the CSP directive that lets the Capa
343
+ * editor frame a draft and nothing else frame it. Each origin is a scheme and
344
+ * a host, with a port when it has one, and is written as the URL parser
345
+ * normalises it, so no `;` or quote can reach the policy. Anything else, a
346
+ * path or a query included, throws a `TypeError`.
347
+ */
348
+ export declare function frameAncestors(adminOrigins?: readonly string[]): string;
349
+ /**
350
+ * The headers every draft response carries: `X-Robots-Tag: noindex, nofollow`,
351
+ * and `Content-Security-Policy: frame-ancestors 'self' https://app.capacms.com`
352
+ * (or the `adminOrigins` given). Throws for an origin that is not one.
353
+ */
354
+ export declare function draftHeaders(options?: {
355
+ adminOrigins?: readonly string[];
356
+ }): Record<string, string>;
357
+ /** One rule of `next.config`'s `headers()`, in the shape Next takes. */
358
+ export interface NextHeaderRule {
359
+ source: string;
360
+ has: Array<{
361
+ type: "cookie" | "query";
362
+ key: string;
363
+ }>;
364
+ headers: Array<{
365
+ key: string;
366
+ value: string;
367
+ }>;
368
+ }
369
+ /**
370
+ * Rules for `next.config`'s `headers()` that send `draftHeaders` on draft
371
+ * responses only:
372
+ *
373
+ * // next.config.mjs
374
+ * import { capaHeaders } from "@capacms/sdk/nextjs";
375
+ * export default { async headers() { return [...capaHeaders()]; } };
376
+ *
377
+ * A rule matches a request carrying the draft cookie, or a `capa-preview`,
378
+ * `capa-edit` or `capa-view` query. A visitor's request carries none of
379
+ * them, so its response, cached or not, is exactly what it was. Next checks
380
+ * the cookie by name, not by value, so a forged cookie only adds these
381
+ * headers to the forger's own response.
382
+ *
383
+ * A site that sends its own `frame-ancestors` or `X-Frame-Options` must leave
384
+ * them off draft responses (`missing: [{ type: "cookie", key: DRAFT_COOKIE }]`
385
+ * on its rule): a browser applies every CSP it is sent, so the strictest wins.
386
+ */
387
+ export declare function capaHeaders(options?: {
388
+ adminOrigins?: readonly string[];
389
+ }): NextHeaderRule[];
390
+ /** The part of Next's `cookies()` store (`next/headers`) the draft-cookie helpers use. */
391
+ export interface DraftCookieJar {
392
+ get(name: string): {
393
+ value: string;
394
+ } | undefined;
395
+ set(cookie: {
396
+ name: string;
397
+ value: string;
398
+ path: string;
399
+ httpOnly: boolean;
400
+ secure: boolean;
401
+ sameSite: "none";
402
+ partitioned: boolean;
403
+ maxAge?: number;
404
+ expires?: Date;
405
+ }): unknown;
406
+ }
407
+ /** Next's `cookies` from `next/headers`, or anything shaped like it. */
408
+ export type CookiesFn = () => DraftCookieJar | Promise<DraftCookieJar>;
409
+ /**
410
+ * Re-set the cookie `draftMode().enable()` just set, so the Capa editor can
411
+ * use it: `HttpOnly; Secure; SameSite=None; Partitioned; Path=/` and
412
+ * `Max-Age` one hour (`maxAge` seconds). Next sets it with no `Max-Age`, so
413
+ * it unlocks every draft on the site until the browser closes, and without
414
+ * `Partitioned`, which Safari 26.2 and later need to send a cookie into a
415
+ * cross-site frame. Call it after `enable()`, in the same route handler.
416
+ * Resolves false, and sets nothing, when there is no draft cookie to re-set.
417
+ */
418
+ export declare function frameDraftCookie(cookies: CookiesFn, options?: {
419
+ maxAge?: number;
420
+ }): Promise<boolean>;
421
+ /**
422
+ * Delete the draft cookie `frameDraftCookie` set. A partitioned cookie is
423
+ * deleted only by a `Set-Cookie` that is partitioned too, which
424
+ * `draftMode().disable()` is not. Call it after `disable()`; it replaces
425
+ * `disable()`'s own deletion, since a response sets one cookie per name. A
426
+ * draft cookie set without `Partitioned`, before a site used
427
+ * `frameDraftCookie`, ends when the browser closes, as it always did.
428
+ */
429
+ export declare function clearDraftCookie(cookies: CookiesFn): Promise<void>;
430
+ /**
431
+ * `app/api/capa/preview/route.ts`:
432
+ *
433
+ * import { cookies, draftMode } from "next/headers";
434
+ * export const GET = createPreviewRoute({ draftMode, cookies });
435
+ *
436
+ * Checks the token with Capa (the site never holds the signing key), turns
437
+ * draft mode on and lands on the entry's page. A bad or expired token lands on
438
+ * the page without draft mode and `?preview=expired`; Capa unreachable gives
439
+ * `?preview=unavailable`. The token is checked with any key the site holds,
440
+ * its legacy key included (`getPublishedClient`, from `CAPA_KEY`).
441
+ *
442
+ * Given `cookies`, the draft cookie is re-set by `frameDraftCookie`: one hour,
443
+ * and sent inside the editor's frame. With no `redirect`, the route answers
444
+ * with its own 307, marked `X-Robots-Tag: noindex, nofollow`,
445
+ * `Referrer-Policy: no-referrer` (the token is in the URL),
446
+ * `Cache-Control: private, no-store` and the editor's `frame-ancestors`.
447
+ * Given Next's `redirect`, it is called instead, as before, and the redirect
448
+ * carries none of those.
449
+ */
450
+ export declare function createPreviewRoute(input: {
451
+ draftMode: DraftModeFn;
452
+ /** Next's `cookies`. Given, the draft cookie lasts `maxAge` seconds and works in the editor's frame. */
453
+ cookies?: CookiesFn;
454
+ /** Next's `redirect`. Leave it out for a redirect that carries the draft headers. */
455
+ redirect?: (url: string) => never | void;
456
+ client?: () => Pick<CapaNextClient, "preview">;
457
+ /** Runs after draft mode is enabled and the cookie re-set, before the redirect (cookie tweaks). */
458
+ onEnable?: () => void | Promise<void>;
459
+ /** The origins allowed to frame a draft. `[CAPA_ADMIN_ORIGIN]` when left out. */
460
+ adminOrigins?: readonly string[];
461
+ /** Seconds the draft cookie lasts, given `cookies`. `DRAFT_COOKIE_MAX_AGE` (one hour) when left out. */
462
+ maxAge?: number;
463
+ }): (request: Request) => Promise<Response | void>;
464
+ /**
465
+ * `app/api/capa/exit/route.ts`:
466
+ *
467
+ * import { cookies, draftMode } from "next/headers";
468
+ * export const GET = exitPreviewRoute({ draftMode, cookies });
469
+ *
470
+ * Turns draft mode off and lands on `?path=`. Given `cookies`, the framed
471
+ * cookie `createPreviewRoute` set is deleted too. With no `redirect`, the route
472
+ * answers with its own 307, marked noindex and never cached.
473
+ */
474
+ export declare function exitPreviewRoute(input: {
475
+ draftMode: DraftModeFn;
476
+ cookies?: CookiesFn;
477
+ redirect?: (url: string) => never | void;
478
+ }): (request: Request) => Promise<Response | void>;
479
+ interface MiddlewareRequest {
480
+ url: string;
481
+ headers: Headers;
482
+ nextUrl: URL & {
483
+ clone(): URL;
484
+ };
485
+ cookies: {
486
+ getAll(): Array<{
487
+ name: string;
488
+ value: string;
489
+ }>;
490
+ };
491
+ }
492
+ interface NextResponseLike {
493
+ next(init?: {
494
+ request?: {
495
+ headers?: Headers;
496
+ };
497
+ }): {
498
+ headers: Headers;
499
+ };
500
+ rewrite(url: URL): unknown;
501
+ }
502
+ /**
503
+ * `middleware.ts` in one line:
504
+ *
505
+ * import { NextResponse } from "next/server";
506
+ * export const middleware = capaMiddleware({ NextResponse });
507
+ *
508
+ * - `?capa-preview=<token>` on any page goes to `previewRoute`, which turns
509
+ * draft mode on and comes back;
510
+ * - `?capa-view=published` renders without the draft cookie, for the editor's
511
+ * Published view;
512
+ * - `?capa-edit=<token>` turns edit mode on (ids and overlay, published data);
513
+ * - every edit-mode response is `private, no-store`, and it and every request
514
+ * carrying `capa-edit` or `capa-view` is `X-Robots-Tag: noindex, nofollow`.
515
+ *
516
+ * Give it a `matcher` so a visitor's request never runs it (Next reads
517
+ * `config` from the file itself, so it is written out there):
518
+ *
519
+ * export const config = {
520
+ * matcher: [
521
+ * { source: "/:path*", has: [{ type: "query", key: "capa-preview" }] },
522
+ * { source: "/:path*", has: [{ type: "query", key: "capa-edit" }] },
523
+ * { source: "/:path*", has: [{ type: "query", key: "capa-view" }] },
524
+ * { source: "/:path*", has: [{ type: "cookie", key: "__prerender_bypass" }] },
525
+ * ],
526
+ * };
527
+ */
528
+ export declare function capaMiddleware(input: {
529
+ NextResponse: NextResponseLike;
530
+ previewRoute?: string;
531
+ client?: () => Pick<CapaNextClient, "preview">;
532
+ }): (request: MiddlewareRequest) => Promise<unknown>;