@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.
- package/CHANGELOG.md +450 -0
- package/README.md +1698 -193
- package/bin/capa-codegen.js +192 -5
- package/bin/capa.js +235 -0
- package/bin/graphql-project.js +142 -0
- package/bin/project-env.js +58 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +17 -0
- package/dist/codegen.d.ts +55 -0
- package/dist/codegen.js +320 -39
- package/dist/config.d.ts +5 -36
- package/dist/config.js +47 -1
- package/dist/esm/image/index.d.ts +120 -0
- package/dist/esm/image/index.js +250 -0
- package/dist/esm/image/shared-params.generated.d.ts +190 -0
- package/dist/esm/image/shared-params.generated.js +461 -0
- package/dist/esm/nextjs/image-loader.d.ts +60 -0
- package/dist/esm/nextjs/image-loader.js +67 -0
- package/dist/esm/nextjs/overlay.d.ts +30 -0
- package/dist/esm/nextjs/overlay.js +75 -0
- package/dist/esm/overlay/index.d.ts +32 -0
- package/dist/esm/overlay/index.js +576 -0
- package/dist/esm/overlay/protocol.d.ts +187 -0
- package/dist/esm/overlay/protocol.js +240 -0
- package/dist/esm/package.json +4 -0
- package/dist/graphql-codegen.d.ts +117 -0
- package/dist/graphql-codegen.js +705 -0
- package/dist/http.js +1 -1
- package/dist/image/index.d.ts +120 -0
- package/dist/image/index.js +257 -0
- package/dist/image/shared-params.generated.d.ts +190 -0
- package/dist/image/shared-params.generated.js +471 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -1
- package/dist/next/attrs.d.ts +84 -10
- package/dist/next/attrs.js +119 -2
- package/dist/next/client.d.ts +176 -32
- package/dist/next/client.js +212 -90
- package/dist/next/entry-fields.d.ts +162 -0
- package/dist/next/entry-fields.js +2 -0
- package/dist/next/errors.d.ts +136 -0
- package/dist/next/errors.js +214 -0
- package/dist/next/field-names.d.ts +37 -0
- package/dist/next/field-names.js +145 -0
- package/dist/next/graphql/build.d.ts +27 -0
- package/dist/next/graphql/build.js +98 -0
- package/dist/next/graphql/documents.d.ts +67 -0
- package/dist/next/graphql/documents.js +35 -0
- package/dist/next/graphql/edit-mode.d.ts +16 -0
- package/dist/next/graphql/edit-mode.js +93 -0
- package/dist/next/graphql/filter-values.d.ts +34 -0
- package/dist/next/graphql/filter-values.js +96 -0
- package/dist/next/graphql/introspection.d.ts +89 -0
- package/dist/next/graphql/introspection.js +102 -0
- package/dist/next/graphql/plan.d.ts +115 -0
- package/dist/next/graphql/plan.js +531 -0
- package/dist/next/graphql/request.d.ts +228 -0
- package/dist/next/graphql/request.js +283 -0
- package/dist/next/graphql/rest.d.ts +66 -0
- package/dist/next/graphql/rest.js +502 -0
- package/dist/next/graphql/selection.d.ts +55 -0
- package/dist/next/graphql/selection.js +212 -0
- package/dist/next/graphql/sha256.d.ts +13 -0
- package/dist/next/graphql/sha256.js +86 -0
- package/dist/next/graphql/summary.d.ts +83 -0
- package/dist/next/graphql/summary.js +151 -0
- package/dist/next/graphql/tree-layout.d.ts +36 -0
- package/dist/next/graphql/tree-layout.js +20 -0
- package/dist/next/graphql/tree.d.ts +171 -0
- package/dist/next/graphql/tree.js +249 -0
- package/dist/next/graphql/typed.d.ts +261 -0
- package/dist/next/graphql/typed.js +146 -0
- package/dist/next/index.d.ts +30 -5
- package/dist/next/index.js +32 -1
- package/dist/next/inflate.d.ts +51 -0
- package/dist/next/inflate.js +243 -0
- package/dist/next/key-family.d.ts +31 -0
- package/dist/next/key-family.js +66 -0
- package/dist/next/select-types.d.ts +58 -5
- package/dist/next/system-keys.d.ts +27 -0
- package/dist/next/system-keys.js +42 -0
- package/dist/nextjs/image-loader.d.ts +60 -0
- package/dist/nextjs/image-loader.js +71 -0
- package/dist/nextjs/index.d.ts +484 -5
- package/dist/nextjs/index.js +688 -6
- package/dist/nextjs/overlay.d.ts +30 -0
- package/dist/nextjs/overlay.js +78 -0
- package/dist/overlay/index.d.ts +14 -2
- package/dist/overlay/index.js +282 -43
- package/dist/overlay/protocol.d.ts +98 -2
- package/dist/overlay/protocol.js +151 -4
- 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; } });
|
package/dist/next/attrs.d.ts
CHANGED
|
@@ -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"
|
|
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
|
|
7
|
-
* the API renders every field under its namespace, and the
|
|
8
|
-
* each field row with that same namespace, so one string
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
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 {};
|