@capacms/sdk 1.0.0-next.1 → 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 +1698 -193
  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 +84 -10
  36. package/dist/next/attrs.js +119 -2
  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 -5
  74. package/dist/next/index.js +32 -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 +688 -6
  86. package/dist/nextjs/overlay.d.ts +30 -0
  87. package/dist/nextjs/overlay.js +78 -0
  88. package/dist/overlay/index.d.ts +14 -2
  89. package/dist/overlay/index.js +282 -43
  90. package/dist/overlay/protocol.d.ts +98 -2
  91. package/dist/overlay/protocol.js +151 -4
  92. package/package.json +63 -15
@@ -0,0 +1,471 @@
1
+ "use strict";
2
+ // GENERATED by scripts/shared-image.js from packages/shared/src/image/params.ts, the rules the API serves
3
+ // /files/* by. Do not edit: change the source, and the API and this package follow.
4
+ /**
5
+ * Query parsing, validation and canonicalisation for `GET /files/*`
6
+ * (ADMIN_UI_OVERHAUL §0i.1, "The image processor, made tight").
7
+ *
8
+ * PURE ON PURPOSE. Nothing here touches sharp, the network or the database, so
9
+ * the whole parameter surface — every accepted value, every rejection, and the
10
+ * canonical spelling of every URL — is testable without a server and without a
11
+ * live Backblaze object. That matters for this route in particular: its eight
12
+ * parity fixtures are all 400/404 cases because a DB hit immediately leaves the
13
+ * process (docs/TURBINE_PORT_NOTES.md, "Deferred to the P6 cloud parity pass"),
14
+ * so the transform surface has no golden coverage at all unless it is reachable
15
+ * from a unit test.
16
+ *
17
+ * THE RULE THAT SHAPES EVERYTHING BELOW: a caller that sends only today's three
18
+ * parameters (`width`, `height`, `blur`) must get today's response. Every
19
+ * default here is therefore today's behaviour, and the one place that could
20
+ * have broken it — the canonical-query redirect — is deliberately gated on a
21
+ * NEW parameter being present. See `canonicalise`.
22
+ *
23
+ * WHY IT LIVES IN @capa/shared. `@capacms/sdk` builds image URLs, and a URL
24
+ * that is not spelled the way `canonicalise` spells it costs every first view
25
+ * a 301 that the edge then keeps for a year. So the SDK compiles this file into
26
+ * its own build rather than keeping a second copy of the rules, and its tests
27
+ * run what it builds back through `canonicalise`. The API imports it from here.
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.NEW_PARAMS = exports.LEGACY_PARAMS = exports.AVIF_DEFAULT_QUALITY = exports.DEFAULT_QUALITY = exports.MAX_BLUR = exports.MIN_BLUR = exports.MAX_OUTPUT_EDGE = exports.OUTPUT_MIME = void 0;
31
+ exports.defaultQuality = defaultQuality;
32
+ exports.droppableQuality = droppableQuality;
33
+ exports.parseImageParams = parseImageParams;
34
+ exports.canonicalise = canonicalise;
35
+ exports.canonicalQuery = canonicalQuery;
36
+ exports.buildSignature = buildSignature;
37
+ exports.resolveAutoFormat = resolveAutoFormat;
38
+ /** Content type emitted for each explicitly requestable output format. */
39
+ exports.OUTPUT_MIME = {
40
+ webp: "image/webp",
41
+ avif: "image/avif",
42
+ jpeg: "image/jpeg",
43
+ png: "image/png",
44
+ };
45
+ /**
46
+ * The longest edge this route will ever produce.
47
+ *
48
+ * A cap rather than a clamp, because a clamp makes two different URLs return
49
+ * the same bytes and therefore occupy two edge objects for one representation.
50
+ * 4096 is the §0i.1 figure; it is also comfortably above any real layout width
51
+ * at dpr 3 (1365 CSS px).
52
+ */
53
+ exports.MAX_OUTPUT_EDGE = 4096;
54
+ /** Today's blur bounds, previously applied as a silent clamp. */
55
+ exports.MIN_BLUR = 1;
56
+ exports.MAX_BLUR = 256;
57
+ /** Default quality for lossy outputs except AVIF. Ignored for png, which is lossless. */
58
+ exports.DEFAULT_QUALITY = 80;
59
+ /**
60
+ * AVIF's own default quality (Penelope Hospitality onboarding, #300). At 80,
61
+ * AVIF came back larger than WebP at 80; 60 at effort 2 is 16 to 18% smaller
62
+ * than WebP with equal or better SSIM on eight client photos. The route's
63
+ * encoder and the effort live in apps/api (image-transform.ts); the number
64
+ * lives here because canonicalQuery has to know it (see droppableQuality).
65
+ */
66
+ exports.AVIF_DEFAULT_QUALITY = 60;
67
+ /**
68
+ * The quality an encode into `output` uses when the URL names none.
69
+ *
70
+ * The route only ever calls this with a format the URL asked for, explicitly or
71
+ * through `auto`: with no `format` and no `quality` it selects no encoder at
72
+ * all and the source keeps its own format (`resolveOutputFormat`).
73
+ */
74
+ function defaultQuality(output) {
75
+ return output === "avif" ? exports.AVIF_DEFAULT_QUALITY : exports.DEFAULT_QUALITY;
76
+ }
77
+ /**
78
+ * The `quality` canonicalQuery drops from a URL with this `format`, or null
79
+ * when it keeps every value.
80
+ *
81
+ * CANONICALISATION MUST NEVER CHANGE WHAT A URL RENDERS. A value may be dropped
82
+ * only when the URL without it encodes at that same value. After #300 gave
83
+ * AVIF its own default, dropping 80 everywhere sent `format=avif&quality=80`
84
+ * to `format=avif`, which is quality 60.
85
+ *
86
+ * - `avif`: null. 80 is not AVIF's default, so it must stay. 60 is, and could
87
+ * be dropped without changing the bytes; it is KEPT ON PURPOSE. The SDK's
88
+ * frozen URLs (#316, and @capacms/sdk's own tests) include
89
+ * `format=avif&quality=60`, which would start taking a 301; and an explicit
90
+ * number keeps meaning that number if AVIF's default is retuned, which
91
+ * `format=avif` does not. The cost is two edge objects for one image when a
92
+ * caller spells the default out.
93
+ * - `auto`: null. Negotiation picks AVIF (default 60) or WebP or the source's
94
+ * own format (default 80), so no single value means "no quality" for every
95
+ * browser.
96
+ * - `webp`, `jpeg`, `png` and no format: DEFAULT_QUALITY, as before.
97
+ *
98
+ * KNOWN GAP, older than #300 and left as it is because a URL with no `format`
99
+ * keeps its spelling: with no format the output follows the SOURCE, which a URL
100
+ * cannot see. `?quality=80&width=200` on an AVIF or HEIF source is an AVIF at
101
+ * 80, and `?width=200` keeps sharp's own AVIF default (50). `?quality=80` with
102
+ * nothing else redirects to the bare URL, which is the stored file, not a
103
+ * re-encode.
104
+ */
105
+ function droppableQuality(format) {
106
+ if (format === "avif" || format === "auto")
107
+ return null;
108
+ return exports.DEFAULT_QUALITY;
109
+ }
110
+ /**
111
+ * The parameters that existed before §0i.1.
112
+ *
113
+ * A URL built only from these is GRANDFATHERED: it is treated as already
114
+ * canonical and is never redirected, however it is spelled. Without this, every
115
+ * live `?width=100&height=50` in every customer's markup would start taking a
116
+ * 301 — which is not a behaviour anyone asked to change, and "keep today's
117
+ * calls byte-identical" outranks de-duplicating an edge object that has been
118
+ * duplicated for years already.
119
+ */
120
+ exports.LEGACY_PARAMS = new Set(["width", "height", "blur"]);
121
+ /** Parameters introduced by §0i.1. Their presence is what enables canonicalisation. */
122
+ exports.NEW_PARAMS = new Set(["format", "quality", "fit", "dpr", "upscale", "keepMetadata"]);
123
+ /**
124
+ * Fastify gives `?a=1&a=2` as an array. A repeated parameter has no defensible
125
+ * meaning here and silently picking one is exactly the "never silently ignored"
126
+ * failure §0i.1 calls out, so it is a rejection.
127
+ */
128
+ function single(raw, param) {
129
+ if (raw === undefined || raw === null)
130
+ return { ok: true };
131
+ if (Array.isArray(raw)) {
132
+ return { ok: false, rejection: { error: `\`${param}\` was given more than once`, param } };
133
+ }
134
+ if (typeof raw !== "string") {
135
+ return { ok: false, rejection: { error: `\`${param}\` must be a single value`, param } };
136
+ }
137
+ return { ok: true, value: raw };
138
+ }
139
+ /**
140
+ * Strict integer parse.
141
+ *
142
+ * `parseInt` is what this route used to do and it is the reason `?width=abc`
143
+ * was a 500: `parseInt("abc")` is NaN, NaN reaches sharp, sharp throws, and the
144
+ * outer catch turns a bad request into "Failed to process file". `parseInt`
145
+ * also accepts `12px` and `0x10`. Neither is a width.
146
+ */
147
+ function strictInt(value) {
148
+ if (!/^-?\d+$/.test(value.trim()))
149
+ return null;
150
+ const n = Number(value.trim());
151
+ return Number.isSafeInteger(n) ? n : null;
152
+ }
153
+ function strictNumber(value) {
154
+ if (!/^-?\d+(\.\d+)?$/.test(value.trim()))
155
+ return null;
156
+ const n = Number(value.trim());
157
+ return Number.isFinite(n) ? n : null;
158
+ }
159
+ const BOOL_TRUE = new Set(["1", "true"]);
160
+ const BOOL_FALSE = new Set(["0", "false"]);
161
+ /**
162
+ * Parse and validate the whole query in one pass.
163
+ *
164
+ * Unknown VALUES for a known parameter are rejected. Unknown KEYS are not:
165
+ * `?v=3` cache-busters and analytics tags are real traffic on a public CDN
166
+ * origin, and turning them into 400s would break callers who are not using the
167
+ * image surface at all. They are carried through canonicalisation untouched.
168
+ */
169
+ function parseImageParams(query) {
170
+ const reject = (error, param) => ({ ok: false, rejection: { error, param } });
171
+ // --- dpr first: it multiplies the dimensions, so the cap has to see it.
172
+ const dprRaw = single(query.dpr, "dpr");
173
+ if (!dprRaw.ok)
174
+ return { ok: false, rejection: dprRaw.rejection };
175
+ let dpr = 1;
176
+ if (dprRaw.value !== undefined) {
177
+ const n = strictNumber(dprRaw.value);
178
+ if (n === null || n < 1 || n > 3) {
179
+ return reject("`dpr` must be a number between 1 and 3", "dpr");
180
+ }
181
+ dpr = n;
182
+ }
183
+ const dimension = (name) => {
184
+ const raw = single(query[name], name);
185
+ if (!raw.ok)
186
+ return raw;
187
+ if (raw.value === undefined)
188
+ return { ok: true };
189
+ const n = strictInt(raw.value);
190
+ if (n === null || n < 1) {
191
+ return { ok: false, rejection: { error: `\`${name}\` must be a positive integer`, param: name } };
192
+ }
193
+ const effective = Math.round(n * dpr);
194
+ if (effective > exports.MAX_OUTPUT_EDGE) {
195
+ return {
196
+ ok: false,
197
+ rejection: {
198
+ error: `\`${name}\` × \`dpr\` is ${effective}px; the maximum output edge is ${exports.MAX_OUTPUT_EDGE}px`,
199
+ param: name,
200
+ },
201
+ };
202
+ }
203
+ return { ok: true, value: effective };
204
+ };
205
+ const w = dimension("width");
206
+ if (!w.ok)
207
+ return { ok: false, rejection: w.rejection };
208
+ const h = dimension("height");
209
+ if (!h.ok)
210
+ return { ok: false, rejection: h.rejection };
211
+ const blurRaw = single(query.blur, "blur");
212
+ if (!blurRaw.ok)
213
+ return { ok: false, rejection: blurRaw.rejection };
214
+ let blur;
215
+ if (blurRaw.value !== undefined) {
216
+ const n = strictInt(blurRaw.value);
217
+ // This used to be `Math.max(1, Math.min(256, parseInt(blur)))` — a silent
218
+ // clamp, so `blur=99999` and `blur=256` were one representation under two
219
+ // URLs, and `blur=abc` was a 500.
220
+ if (n === null || n < exports.MIN_BLUR || n > exports.MAX_BLUR) {
221
+ return reject(`\`blur\` must be an integer between ${exports.MIN_BLUR} and ${exports.MAX_BLUR}`, "blur");
222
+ }
223
+ blur = n;
224
+ }
225
+ const formatRaw = single(query.format, "format");
226
+ if (!formatRaw.ok)
227
+ return { ok: false, rejection: formatRaw.rejection };
228
+ let format;
229
+ if (formatRaw.value !== undefined) {
230
+ const v = formatRaw.value.trim().toLowerCase();
231
+ const canon = v === "jpg" ? "jpeg" : v;
232
+ if (canon !== "webp" && canon !== "avif" && canon !== "jpeg" && canon !== "png" && canon !== "auto") {
233
+ return reject("`format` must be one of webp, avif, jpeg, png, auto", "format");
234
+ }
235
+ format = canon;
236
+ }
237
+ const qualityRaw = single(query.quality, "quality");
238
+ if (!qualityRaw.ok)
239
+ return { ok: false, rejection: qualityRaw.rejection };
240
+ let quality;
241
+ if (qualityRaw.value !== undefined) {
242
+ const n = strictInt(qualityRaw.value);
243
+ if (n === null || n < 1 || n > 100) {
244
+ return reject("`quality` must be an integer between 1 and 100", "quality");
245
+ }
246
+ quality = n;
247
+ }
248
+ const fitRaw = single(query.fit, "fit");
249
+ if (!fitRaw.ok)
250
+ return { ok: false, rejection: fitRaw.rejection };
251
+ let fit = "inside";
252
+ if (fitRaw.value !== undefined) {
253
+ const v = fitRaw.value.trim().toLowerCase();
254
+ if (v !== "inside" && v !== "cover" && v !== "contain") {
255
+ return reject("`fit` must be one of inside, cover, contain", "fit");
256
+ }
257
+ fit = v;
258
+ }
259
+ const flag = (name) => {
260
+ const raw = single(query[name], name);
261
+ if (!raw.ok)
262
+ return raw;
263
+ if (raw.value === undefined)
264
+ return { ok: true, value: false };
265
+ const v = raw.value.trim().toLowerCase();
266
+ if (BOOL_TRUE.has(v))
267
+ return { ok: true, value: true };
268
+ if (BOOL_FALSE.has(v))
269
+ return { ok: true, value: false };
270
+ return { ok: false, rejection: { error: `\`${name}\` must be 0 or 1`, param: name } };
271
+ };
272
+ const upscale = flag("upscale");
273
+ if (!upscale.ok)
274
+ return { ok: false, rejection: upscale.rejection };
275
+ const keepMetadata = flag("keepMetadata");
276
+ if (!keepMetadata.ok)
277
+ return { ok: false, rejection: keepMetadata.rejection };
278
+ /**
279
+ * What counts as "the caller asked for a transform".
280
+ *
281
+ * `fit`, `dpr` and `upscale` only modify a resize, so on their own they
282
+ * change nothing — a URL carrying just `?fit=cover` must stay the pass-through
283
+ * it is today. `keepMetadata` likewise: there is nothing to strip if we are
284
+ * not re-encoding.
285
+ */
286
+ const wantsTransform = w.value !== undefined ||
287
+ h.value !== undefined ||
288
+ blur !== undefined ||
289
+ format !== undefined ||
290
+ quality !== undefined;
291
+ return {
292
+ ok: true,
293
+ params: {
294
+ width: w.value,
295
+ height: h.value,
296
+ blur,
297
+ format,
298
+ quality,
299
+ fit,
300
+ dpr,
301
+ upscale: upscale.value,
302
+ keepMetadata: keepMetadata.value,
303
+ wantsTransform,
304
+ },
305
+ };
306
+ }
307
+ /** Render a number the way the canonical URL spells it (no trailing zeros). */
308
+ function num(n) {
309
+ return String(Number(n));
310
+ }
311
+ /**
312
+ * One key or value of the canonical query, percent-encoded so that a browser
313
+ * sends it back byte for byte.
314
+ *
315
+ * `encodeURIComponent` leaves `'` as it is, but a browser's URL parser
316
+ * percent-encodes `'` in the query of an http(s) URL. A redirect whose Location
317
+ * carried a raw `'` was therefore requested as `%27`, which is not the spelling
318
+ * this module wrote, so it was redirected again, forever. Every other
319
+ * character `encodeURIComponent` leaves alone (`!` `(` `)` `*` `~` and the
320
+ * unreserved set) a browser leaves alone too.
321
+ */
322
+ function encodePart(text) {
323
+ return encodeURIComponent(text).replace(/'/g, "%27");
324
+ }
325
+ /**
326
+ * The canonical spelling of a query, so `?width=200&format=webp` and
327
+ * `?format=webp&width=200` are one edge object rather than two.
328
+ *
329
+ * GATED, DELIBERATELY. Canonicalisation only applies once the caller uses a
330
+ * parameter introduced by §0i.1. A URL made only of `width`/`height`/`blur` is
331
+ * declared canonical as received, whatever its key order — see LEGACY_PARAMS
332
+ * for why. The gate is also what keeps the `files-404-params-ignored` parity
333
+ * fixture (`?width=100&height=50&blur=9`) from becoming a 301.
334
+ *
335
+ * @param rawQuery the query string as received, without the leading `?`
336
+ */
337
+ function canonicalise(rawQuery, params) {
338
+ const received = new URLSearchParams(rawQuery);
339
+ const usesNewParam = [...received.keys()].some((k) => exports.NEW_PARAMS.has(k));
340
+ if (!usesNewParam)
341
+ return { query: rawQuery, isCanonical: true };
342
+ const query = canonicalQuery(rawQuery, params);
343
+ return { query, isCanonical: query === rawQuery };
344
+ }
345
+ /**
346
+ * The canonical spelling itself, without `canonicalise`'s gate.
347
+ *
348
+ * `canonicalise` is this plus the LEGACY_PARAMS grandfather clause. For a query
349
+ * that uses a new parameter the two agree; for one made only of
350
+ * `width`/`height`/`blur` this still sorts and re-spells it, which the API
351
+ * accepts as canonical too, since the gate declares any legacy spelling
352
+ * canonical. So a URL built with this is never redirected, whichever
353
+ * parameters it carries. @capacms/sdk builds every image URL with it.
354
+ *
355
+ * @param rawQuery the query string, without the leading `?`; read only for the
356
+ * keys this module does not know, which are kept
357
+ */
358
+ function canonicalQuery(rawQuery, params) {
359
+ const received = new URLSearchParams(rawQuery);
360
+ const keys = [...new Set([...received.keys()])];
361
+ const out = new Map();
362
+ // Known parameters are re-rendered from the PARSED value, so `width=0200`,
363
+ // `format=JPG` and `upscale=true` all collapse onto one spelling. Defaults are
364
+ // dropped: a URL that asks for the default is the same representation as one
365
+ // that does not mention it. `quality` only where that is true for every
366
+ // output the URL can produce (droppableQuality).
367
+ //
368
+ // `width`/`height` are re-rendered PRE-dpr, because dpr stays in the URL as
369
+ // its own term; folding it into the dimension would make `?width=200&dpr=2`
370
+ // redirect to `?width=400`, which is a different request as far as the caller
371
+ // who wrote it is concerned.
372
+ if (params.width !== undefined)
373
+ out.set("width", num(Math.round(params.width / params.dpr)));
374
+ if (params.height !== undefined)
375
+ out.set("height", num(Math.round(params.height / params.dpr)));
376
+ if (params.blur !== undefined)
377
+ out.set("blur", num(params.blur));
378
+ if (params.format !== undefined)
379
+ out.set("format", params.format);
380
+ if (params.quality !== undefined && params.quality !== droppableQuality(params.format))
381
+ out.set("quality", num(params.quality));
382
+ if (params.fit !== "inside")
383
+ out.set("fit", params.fit);
384
+ if (params.dpr !== 1)
385
+ out.set("dpr", num(params.dpr));
386
+ if (params.upscale)
387
+ out.set("upscale", "1");
388
+ if (params.keepMetadata)
389
+ out.set("keepMetadata", "1");
390
+ // Unknown keys are preserved verbatim (first value wins, repeats collapse),
391
+ // so a cache-buster survives the redirect instead of being silently dropped.
392
+ //
393
+ // An empty pair (`&=`, an empty key with an empty value) is dropped: written
394
+ // back it would be an empty segment, which parses to nothing, so the URL it
395
+ // redirected to was itself redirected again.
396
+ for (const key of keys) {
397
+ if (exports.LEGACY_PARAMS.has(key) || exports.NEW_PARAMS.has(key))
398
+ continue;
399
+ const value = received.get(key) ?? "";
400
+ if (key === "" && value === "")
401
+ continue;
402
+ out.set(key, value);
403
+ }
404
+ const sorted = [...out.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
405
+ return sorted
406
+ .map(([k, v]) => (v === "" ? encodePart(k) : `${encodePart(k)}=${encodePart(v)}`))
407
+ .join("&");
408
+ }
409
+ /**
410
+ * A stable string identifying the transform, for the ETag.
411
+ *
412
+ * Built from the RESOLVED parameters rather than the URL, so two spellings of
413
+ * one representation share an ETag even when the redirect is not in play.
414
+ * `auto` resolves per request, so the caller passes the resolved format in.
415
+ */
416
+ function buildSignature(params, resolvedFormat) {
417
+ if (!params.wantsTransform)
418
+ return "raw";
419
+ const parts = [];
420
+ if (params.width !== undefined)
421
+ parts.push(`w${params.width}`);
422
+ if (params.height !== undefined)
423
+ parts.push(`h${params.height}`);
424
+ if (params.width !== undefined || params.height !== undefined) {
425
+ parts.push(`f${params.fit}`);
426
+ if (params.upscale)
427
+ parts.push("up");
428
+ }
429
+ if (params.blur !== undefined)
430
+ parts.push(`b${params.blur}`);
431
+ if (resolvedFormat)
432
+ parts.push(`o${resolvedFormat}`);
433
+ if (params.quality !== undefined)
434
+ parts.push(`q${params.quality}`);
435
+ if (params.keepMetadata)
436
+ parts.push("meta");
437
+ return parts.join("_") || "raw";
438
+ }
439
+ /**
440
+ * Which concrete format `format=auto` resolves to for this request.
441
+ *
442
+ * Returns null when the client advertises neither modern format, in which case
443
+ * the source format is kept — `auto` never makes a response worse than the
444
+ * object already is.
445
+ *
446
+ * `q=0` is an explicit REFUSAL in RFC 9110 content negotiation, not a weak
447
+ * preference, so an entry carrying it is skipped rather than matched.
448
+ */
449
+ function resolveAutoFormat(accept) {
450
+ if (!accept)
451
+ return null;
452
+ const offered = new Map();
453
+ for (const entry of accept.split(",")) {
454
+ const [type, ...paramParts] = entry.split(";");
455
+ const name = type.trim().toLowerCase();
456
+ if (!name)
457
+ continue;
458
+ let q = 1;
459
+ for (const p of paramParts) {
460
+ const m = /^\s*q\s*=\s*([\d.]+)\s*$/i.exec(p);
461
+ if (m)
462
+ q = Number(m[1]);
463
+ }
464
+ offered.set(name, Number.isFinite(q) ? q : 1);
465
+ }
466
+ if ((offered.get("image/avif") ?? 0) > 0)
467
+ return "avif";
468
+ if ((offered.get("image/webp") ?? 0) > 0)
469
+ return "webp";
470
+ return null;
471
+ }
package/dist/index.d.ts CHANGED
@@ -6,5 +6,5 @@ export { WEBHOOKS_NEED_ACCESS_TOKEN } from "./webhooks";
6
6
  export type { CreateWebhookEndpointInput, ListWebhookDeliveriesOptions, ResumeWebhookEndpointOptions, ResumeWebhookEndpointResult, UpdateWebhookEndpointInput, WebhookDeliveriesPage, WebhookDeliveriesResource, WebhookDelivery, WebhookDeliveryDetail, WebhookDeliveryStatus, WebhookDisabledReason, WebhookEndpoint, WebhookEndpointDetail, WebhookEndpointsResource, WebhookEndpointWithSecret, WebhookEventCatalogueEntry, WebhookEventGroup, WebhookPagination, WebhooksResource, } from "./webhooks";
7
7
  export { DEFAULT_WEBHOOK_TOLERANCE_SECONDS, parseWebhookSignatureHeader, signWebhookPayload, verifyWebhookSignature, WEBHOOK_SIGNATURE_HEADER, } from "./webhook-signature";
8
8
  export type { ParsedWebhookSignature, VerifyWebhookSignatureInput, } from "./webhook-signature";
9
- export { generate, normalizeTypes, readStampedChecksum, typeNames } from "./codegen";
10
- export type { CodegenResult } from "./codegen";
9
+ export { generate, normalizeTypes, readStampedChecksum, restTypesFromIntrospection, typeNames } from "./codegen";
10
+ export type { CodegenResult, RestTypesResult } from "./codegen";
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.typeNames = exports.readStampedChecksum = exports.normalizeTypes = exports.generate = exports.WEBHOOK_SIGNATURE_HEADER = exports.verifyWebhookSignature = exports.signWebhookPayload = exports.parseWebhookSignatureHeader = exports.DEFAULT_WEBHOOK_TOLERANCE_SECONDS = exports.WEBHOOKS_NEED_ACCESS_TOKEN = exports.CapaError = exports.instanceIdOf = exports.createClient = void 0;
3
+ exports.typeNames = exports.restTypesFromIntrospection = exports.readStampedChecksum = exports.normalizeTypes = exports.generate = exports.WEBHOOK_SIGNATURE_HEADER = exports.verifyWebhookSignature = exports.signWebhookPayload = exports.parseWebhookSignatureHeader = exports.DEFAULT_WEBHOOK_TOLERANCE_SECONDS = exports.WEBHOOKS_NEED_ACCESS_TOKEN = exports.CapaError = exports.instanceIdOf = exports.createClient = void 0;
4
4
  var client_1 = require("./client");
5
5
  Object.defineProperty(exports, "createClient", { enumerable: true, get: function () { return client_1.createClient; } });
6
6
  Object.defineProperty(exports, "instanceIdOf", { enumerable: true, get: function () { return client_1.instanceIdOf; } });
@@ -18,4 +18,5 @@ var codegen_1 = require("./codegen");
18
18
  Object.defineProperty(exports, "generate", { enumerable: true, get: function () { return codegen_1.generate; } });
19
19
  Object.defineProperty(exports, "normalizeTypes", { enumerable: true, get: function () { return codegen_1.normalizeTypes; } });
20
20
  Object.defineProperty(exports, "readStampedChecksum", { enumerable: true, get: function () { return codegen_1.readStampedChecksum; } });
21
+ Object.defineProperty(exports, "restTypesFromIntrospection", { enumerable: true, get: function () { return codegen_1.restTypesFromIntrospection; } });
21
22
  Object.defineProperty(exports, "typeNames", { enumerable: true, get: function () { return codegen_1.typeNames; } });
@@ -1,15 +1,31 @@
1
1
  /**
2
2
  * The attributes that make a rendered field clickable in Capa's live preview.
3
3
  *
4
- * <h1 {...capaAttrs(entry, "title", isDraft)}>{entry.fields.title}</h1>
4
+ * <h1 {...capaAttrs(entry, "title")}>{entry.fields.title}</h1>
5
+ * <h1 {...capaAttrs(node, "title")}>{node.title}</h1> // a GraphQL node
5
6
  *
6
- * `field` is the field's namespace, which is the key it has in `entry.fields`:
7
- * the API renders every field under its namespace, and the Capa editor tags
8
- * each field row with that same namespace, so one string names the field on
9
- * both sides of the preview frame.
7
+ * For a REST entry, `field` is the field's namespace, which is the key it has
8
+ * in `entry.fields`: the API renders every field under its namespace, and the
9
+ * Capa editor tags each field row with that same namespace, so one string
10
+ * names the field on both sides of the preview frame.
10
11
  *
11
- * Pass `enabled = false` outside draft mode and the element carries nothing, so
12
- * a published page never ships entry ids in its markup.
12
+ * For a GraphQL node (an object that selected `id` and `model`), `field` is
13
+ * the field as selected. A field GraphQL renamed (`hero_image` for
14
+ * `hero-image`, `status_field` for `status`) is tagged by its namespace: a
15
+ * client in edit mode reads the key's schema to know which those are.
16
+ *
17
+ * EDIT MODE DECIDES, NOT THE CALLER. An entry read by a client created with
18
+ * `editMode: true` carries a hidden edit mark (`markEditEntries`), and
19
+ * `capaAttrs` tags only marked entries. So a published page, read with a normal
20
+ * client, ships no entry ids in its markup without the site passing anything,
21
+ * and the same component in the Capa editor is clickable. The mark is a
22
+ * non-enumerable symbol: it does not show up in JSON, logs or a spread copy, and
23
+ * it does not survive being passed to a client component as a prop, which is
24
+ * the safe direction to fail in.
25
+ *
26
+ * Pass `enabled` to override: `true` tags an unmarked entry, `false` tags
27
+ * nothing. Sites written before edit mode passed `isDraft` here and keep
28
+ * working unchanged.
13
29
  */
14
30
  export type CapaAttrs = {
15
31
  "data-capa-entry": string;
@@ -18,7 +34,65 @@ export type CapaAttrs = {
18
34
  "data-capa-entry"?: undefined;
19
35
  "data-capa-field"?: undefined;
20
36
  };
21
- export declare function capaAttrs<T = Record<string, unknown>>(entry: {
37
+ /** `Symbol.for`, so two copies of the SDK in one bundle agree on the mark. */
38
+ export declare const CAPA_EDIT: unique symbol;
39
+ /** Whether an entry was read in edit mode. */
40
+ export declare function isEditEntry(entry: unknown): boolean;
41
+ /**
42
+ * Mark every entry inside `value`, related entries included, so a click on an
43
+ * author inside an article opens the author. Walks arrays and plain objects,
44
+ * never the same object twice. Returns `value` for chaining.
45
+ */
46
+ export declare function markEditEntries<T>(value: T): T;
47
+ /**
48
+ * Mark every entry inside a GraphQL result's `data`: each object that
49
+ * selected `id` and `model`, related entries included. `renamed(model)` gives
50
+ * that model's fields whose GraphQL name is not their namespace, by GraphQL
51
+ * name, so `capaAttrs` tags those by namespace. Returns `data`.
52
+ */
53
+ export declare function markGraphQLEntries<T>(data: T, renamed?: (model: string) => Readonly<Record<string, string>> | undefined): T;
54
+ /** Whether a GraphQL result's `data` holds any entry `markGraphQLEntries` would mark. */
55
+ export declare function hasGraphQLEntry(data: unknown): boolean;
56
+ /** Keep `from`'s edit mark on `to`, for a copy of an entry in another shape (`toTree`). */
57
+ export declare function carryEditMark<T extends object>(from: unknown, to: T): T;
58
+ /** GraphQL's system fields, which name the entry rather than one of its fields. */
59
+ type GraphQLSystemField = "id" | "model" | "status" | "createdAt" | "updatedAt" | "publishedAt" | "_version" | "_tags" | "_folder" | "__typename";
60
+ /**
61
+ * The `field` `capaAttrs` takes for `E`: a key of `fields` for a REST entry;
62
+ * a field as selected for a GraphQL node, which selected `id` and `model`;
63
+ * any name for a bare `{ id }`. A GraphQL node without `model` takes none,
64
+ * and the compiler says why.
65
+ */
66
+ export type TaggableField<E> = unknown extends FieldsOf<E> ? E extends {
67
+ model: string;
68
+ } ? Exclude<Extract<keyof E, string>, GraphQLSystemField> : [Exclude<keyof E, "id">] extends [never] ? string : "select model on this node: capaAttrs tags an entry that selected id and model" : Extract<keyof NonNullable<FieldsOf<E>>, string>;
69
+ /** A REST entry's `fields` type; `unknown` for anything without one (a GraphQL node, a bare `{ id }`). */
70
+ type FieldsOf<E> = "fields" extends keyof E ? E["fields" & keyof E] : unknown;
71
+ export declare function capaAttrs<E extends {
22
72
  id: string;
23
- fields?: T;
24
- }, field: Extract<keyof T, string>, enabled?: boolean): CapaAttrs;
73
+ }>(entry: E, field: TaggableField<E>, enabled?: boolean): CapaAttrs;
74
+ /**
75
+ * The attributes for every field of `T`, one `CapaAttrs` per field name.
76
+ * `capa-codegen` writes `<Model>Attrs = FieldAttrs<Model>` beside each
77
+ * `<Model>Select`, so `const a: ArticleAttrs = fieldAttrs(entry)` fails to
78
+ * compile on a wrong field name (M5). `fieldAttrs` of a REST entry typed with
79
+ * `Model` returns exactly this type.
80
+ */
81
+ export type FieldAttrs<T> = {
82
+ readonly [K in Extract<keyof T, string>]: CapaAttrs;
83
+ };
84
+ /**
85
+ * Typed attributes for every field of one entry (M5), or of one GraphQL node:
86
+ *
87
+ * const a = fieldAttrs(article);
88
+ * <h1 {...a.title}>…</h1> // a.titel is a compile error
89
+ *
90
+ * Same rule as `capaAttrs`: empty unless the entry was read in edit mode, or
91
+ * `enabled` says otherwise.
92
+ */
93
+ export declare function fieldAttrs<E extends {
94
+ id: string;
95
+ }>(entry: E, enabled?: boolean): {
96
+ readonly [K in TaggableField<E>]: CapaAttrs;
97
+ };
98
+ export {};