@giveitsmaller/sdk 0.21.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/dist/builder.js +47 -11
- package/dist/client.d.ts +7 -0
- package/dist/client.js +83 -2
- package/dist/credentials.d.ts +74 -0
- package/dist/credentials.js +123 -0
- package/dist/ergonomic/image_output_routes.d.ts +67 -0
- package/dist/ergonomic/image_output_routes.js +146 -15
- package/dist/ergonomic/option_types.d.ts +11 -2
- package/dist/ergonomic/preset_resolver.d.ts +22 -0
- package/dist/ergonomic/preset_resolver.js +94 -0
- package/dist/ergonomic/presets/image_compress.js +16 -4
- package/dist/ergonomic/presets/video_compress.d.ts +20 -0
- package/dist/errors.d.ts +183 -7
- package/dist/errors.js +203 -7
- package/dist/file-first.js +63 -8
- package/dist/generated/sdk_spec/errors.d.ts +1 -1
- package/dist/generated/sdk_spec/errors.js +176 -1
- package/dist/gisl.d.ts +1 -1
- package/dist/gisl.js +14 -2
- package/dist/handle.js +12 -2
- package/dist/http-downloader.js +25 -7
- package/dist/index.core.d.ts +2 -2
- package/dist/index.core.js +19 -3
- package/dist/merge.d.ts +29 -0
- package/dist/merge.js +12 -2
- package/dist/sse.d.ts +23 -1
- package/dist/sse.js +76 -3
- package/dist/types.d.ts +18 -0
- package/package.json +10 -2
|
@@ -146,15 +146,38 @@ export function resolveOutputRoute(inputToken, outputFormat) {
|
|
|
146
146
|
planned: new Set(cell.planned),
|
|
147
147
|
};
|
|
148
148
|
}
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Input token → EVERY `compress.image*` mime-group that can carry a per-value
|
|
151
|
+
* availability marker for it: the format-specific group when the metadata has one,
|
|
152
|
+
* PLUS the generic `image` group. Most specific first.
|
|
153
|
+
*
|
|
154
|
+
* Both are needed, and the old single-group version lost one or the other whichever
|
|
155
|
+
* way it chose (SB1wmTJz):
|
|
156
|
+
* - The generic group carries CROSS-FORMAT markers — `color_profile: 'srgb'` is planned
|
|
157
|
+
* there and nowhere else, so a lookup that resolved only to `image_jpeg` never saw it.
|
|
158
|
+
* - A specific group carries FORMAT-ONLY markers — `image_svg` marks
|
|
159
|
+
* `output_format: 'original'` planned (SVG→SVG optimisation is not built) and the
|
|
160
|
+
* generic group does not, so a lookup that resolved only to `image` never saw THAT.
|
|
161
|
+
*
|
|
162
|
+
* The previous implementation hard-coded `jpeg|png|avif` and fell through to `image`
|
|
163
|
+
* with a trailing `// webp / gif / svg / tiff`. That comment was true when written and
|
|
164
|
+
* silently stopped being true when `image_svg` and `image_webp` were added to the
|
|
165
|
+
* metadata — so SVG inputs missed the one marker that mattered for them, on this gate
|
|
166
|
+
* AND on the `output()` gate that shares it. Deriving the list from the metadata rather
|
|
167
|
+
* than a hand-written token list is what stops it going stale a second time; the
|
|
168
|
+
* mapping is pinned by `output-route-conformance.test.ts`.
|
|
169
|
+
*
|
|
170
|
+
* `gif`/`tiff` correctly yield `['image']` alone — the metadata genuinely has no
|
|
171
|
+
* concrete group for them (verified against its actual key set, not inferred).
|
|
172
|
+
*/
|
|
173
|
+
function compressGroupsForToken(token) {
|
|
174
|
+
// The historical mapping, PRESERVED EXACTLY. Every verdict it produced today must
|
|
175
|
+
// keep being produced — see the note on additivity in `isPlannedValue`.
|
|
176
|
+
const legacy = token === 'jpeg' ? 'image_jpeg' : token === 'png' ? 'image_png' : token === 'avif' ? 'image_avif' : 'image';
|
|
177
|
+
const specific = `image_${token}`;
|
|
178
|
+
return specific !== legacy && compressMetadata.mime_groups[specific] !== undefined
|
|
179
|
+
? [specific, legacy]
|
|
180
|
+
: [legacy];
|
|
158
181
|
}
|
|
159
182
|
/**
|
|
160
183
|
* Whether a specific VALUE of an option is `availability: 'planned'` for the
|
|
@@ -163,14 +186,32 @@ function compressGroupForToken(token) {
|
|
|
163
186
|
* `compressMetadata` `per_value_availability`; same_format only (the only route
|
|
164
187
|
* where value-level options like `metadata` are honored). Returns false when the
|
|
165
188
|
* option / value / group is unknown (no gate).
|
|
189
|
+
*
|
|
190
|
+
* PURELY ADDITIVE (SB1wmTJz): planned if ANY consulted group marks this value planned.
|
|
191
|
+
* The historical group is still consulted, so **every verdict this returned before still
|
|
192
|
+
* holds** — the change can only turn a missed gate into a gate, never a gate into a
|
|
193
|
+
* pass. That direction matters: a new false ACCEPT would send a request the server
|
|
194
|
+
* rejects, which is the failure this function exists to prevent.
|
|
195
|
+
*
|
|
196
|
+
* Why not "most specific wins", which reads cleaner: it would flip `webp` +
|
|
197
|
+
* `color_profile: 'srgb'` from gated to un-gated, because `image_webp` defines
|
|
198
|
+
* `color_profile` with an empty `per_value_availability`. `RecipeOutputTest`
|
|
199
|
+
* deliberately pins webp srgb as GATED (v2.134 added `srgb: planned` to the generic
|
|
200
|
+
* group), and whether webp srgb actually works on the server is not something this
|
|
201
|
+
* layer can know. Un-gating it on an inference would be exactly the "confident answer
|
|
202
|
+
* from a check that could not tell you otherwise" pattern. Raised as a question instead.
|
|
203
|
+
*
|
|
204
|
+
* What this DOES fix: `image_svg` marks `output_format: 'original'` planned and the
|
|
205
|
+
* generic group does not, so an SVG input previously sailed through the one marker that
|
|
206
|
+
* mattered for it — on this gate and on the `output()` gate that shares it.
|
|
166
207
|
*/
|
|
167
208
|
export function isPlannedValue(inputToken, optionKey, value) {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return
|
|
209
|
+
for (const groupName of compressGroupsForToken(inputToken)) {
|
|
210
|
+
const opt = compressMetadata.mime_groups[groupName]?.options[optionKey];
|
|
211
|
+
if (opt?.per_value_availability[String(value)]?.availability === 'planned')
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
174
215
|
}
|
|
175
216
|
/**
|
|
176
217
|
* Compress-route enum members per image mime-group, mirroring the shipped
|
|
@@ -232,3 +273,93 @@ export function isUnknownEnumValue(inputToken, optionKey, value) {
|
|
|
232
273
|
return false;
|
|
233
274
|
return !(typeof value === 'string' && members.includes(value));
|
|
234
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Contract `depends_on` per compress-image output option, mirroring
|
|
278
|
+
* `availability.json` `operations.compress.mime_groups.<group>.options.<opt>.depends_on`
|
|
279
|
+
* (ehHU08Hu). The rule is option-consistent across every image group that carries
|
|
280
|
+
* the option, so this is a FLAT table (validated group-by-group by
|
|
281
|
+
* `output-route-conformance.test.ts` / PHP `ImageOutputRouteConformanceTest`).
|
|
282
|
+
*
|
|
283
|
+
* Kept as a hand table — NOT a runtime read of the ~238KB availability sidecar —
|
|
284
|
+
* so the gate stays browser-safe with no contracts-version coupling, exactly like
|
|
285
|
+
* {@link COMPRESS_OPTION_VALUES}. Mirrored by PHP
|
|
286
|
+
* `ImageOutputRoutes::OUTPUT_OPTION_DEPENDS_ON`.
|
|
287
|
+
*
|
|
288
|
+
* Generalises the 86gAu5Tr auto_quality gate: every option's dependency is
|
|
289
|
+
* checked uniformly, so quality/lossless/target_size_bytes under `auto_quality`,
|
|
290
|
+
* `target_size_bytes` without `target_size`, `fit` without width/height, etc. are
|
|
291
|
+
* all rejected pre-upload instead of only the one hand-coded case.
|
|
292
|
+
*/
|
|
293
|
+
export const OUTPUT_OPTION_DEPENDS_ON = {
|
|
294
|
+
quality: { requiresKey: 'encoding_mode', requiresValue: 'quality' },
|
|
295
|
+
lossless: { requiresKey: 'encoding_mode', requiresValue: 'quality' },
|
|
296
|
+
quality_preset: { requiresKey: 'encoding_mode', requiresValue: 'auto_quality' },
|
|
297
|
+
target_size_bytes: { requiresKey: 'encoding_mode', requiresValue: 'target_size' },
|
|
298
|
+
fit: { requiresAnyOf: ['width', 'height'] },
|
|
299
|
+
};
|
|
300
|
+
/**
|
|
301
|
+
* Default of each depended-on key — an ABSENT key resolves to this before the
|
|
302
|
+
* dependency check (the server applies the same default). `encoding_mode`
|
|
303
|
+
* defaults to `quality`, so `quality`/`lossless` are valid with no explicit mode,
|
|
304
|
+
* but `target_size_bytes` / `quality_preset` are not. Pinned to `availability.json`
|
|
305
|
+
* defaults by the conformance suite.
|
|
306
|
+
*/
|
|
307
|
+
export const DEPENDS_ON_KEY_DEFAULTS = {
|
|
308
|
+
encoding_mode: 'quality',
|
|
309
|
+
};
|
|
310
|
+
/**
|
|
311
|
+
* The first contract `depends_on` an already-lowered compress-image wire-option
|
|
312
|
+
* set violates for the resolved `route`, or `undefined` when every dependency is
|
|
313
|
+
* satisfied (ehHU08Hu). The caller ({@link Recipe} output lowering) throws
|
|
314
|
+
* `invalid_option_combination` with the returned message + conflictingFields.
|
|
315
|
+
* Only options PRESENT in `wireOptions` are checked; a scalar dependency reads
|
|
316
|
+
* the depended-on key's effective value ({@link DEPENDS_ON_KEY_DEFAULTS} when
|
|
317
|
+
* absent). A scalar (encoding_mode) dependency is skipped on a `format_change`
|
|
318
|
+
* (convert has its own deps); universal deps (e.g. `fit → width|height`, identical
|
|
319
|
+
* in compress + convert) run on BOTH routes. Mirrored by PHP
|
|
320
|
+
* `ImageOutputRoutes::dependsOnViolation`.
|
|
321
|
+
*/
|
|
322
|
+
export function dependsOnViolation(wireOptions, route) {
|
|
323
|
+
for (const [option, rule] of Object.entries(OUTPUT_OPTION_DEPENDS_ON)) {
|
|
324
|
+
// A nullish value is NOT "set" — the contract `set` condition needs a real
|
|
325
|
+
// value, and PHP drops null options before lowering, so treat null == absent
|
|
326
|
+
// for parity (codex: `{ fit: 'max', width: null }` must reject, not bypass).
|
|
327
|
+
if (wireOptions[option] == null)
|
|
328
|
+
continue;
|
|
329
|
+
if ('requiresAnyOf' in rule) {
|
|
330
|
+
if (!rule.requiresAnyOf.some((key) => wireOptions[key] != null)) {
|
|
331
|
+
return {
|
|
332
|
+
conflictingFields: [option, ...rule.requiresAnyOf],
|
|
333
|
+
message: `output(): '${option}' requires at least one of ${rule.requiresAnyOf.join(', ')} to be set ` +
|
|
334
|
+
`(its contract dependency). Set ${rule.requiresAnyOf.join(' or ')}, or drop '${option}'.`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
// Scalar deps in this (compress-image) table are all on `encoding_mode`, a
|
|
340
|
+
// same_format optimiser key — validate them on same_format ONLY. The
|
|
341
|
+
// universal requiresAnyOf dep (fit → width|height) above runs on BOTH routes.
|
|
342
|
+
//
|
|
343
|
+
// A format_change routes via `convert`, which has no encoding_mode and carries
|
|
344
|
+
// its own deps — but those need NO table here (L2Ay7Uak, resolved as a no-op).
|
|
345
|
+
// Every convert image dep is keyed on `output_format`, and the per-target
|
|
346
|
+
// `honored` set the lowering already enforces IS that constraint materialised:
|
|
347
|
+
// `output('gif', { quality: 80 })` is rejected by the honored gate, with a
|
|
348
|
+
// better message, before this function runs. That equivalence is PINNED by
|
|
349
|
+
// `output-route-conformance.test.ts` (+ the PHP mirror), which fails closed if
|
|
350
|
+
// convert ever gains a dep keyed on something other than output_format — which
|
|
351
|
+
// is the case that would genuinely need a gate here.
|
|
352
|
+
if (route !== 'same_format')
|
|
353
|
+
continue;
|
|
354
|
+
const effective = wireOptions[rule.requiresKey] ?? DEPENDS_ON_KEY_DEFAULTS[rule.requiresKey];
|
|
355
|
+
if (effective !== rule.requiresValue) {
|
|
356
|
+
return {
|
|
357
|
+
conflictingFields: [rule.requiresKey, option],
|
|
358
|
+
message: `output(): '${option}' requires ${rule.requiresKey} '${rule.requiresValue}' (its contract ` +
|
|
359
|
+
`dependency), but ${rule.requiresKey} is '${String(effective)}'. Set ${rule.requiresKey}: ` +
|
|
360
|
+
`'${rule.requiresValue}', or drop '${option}'.`,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
@@ -137,6 +137,13 @@ export interface WatermarkOptions {
|
|
|
137
137
|
* overlays on one base image (z-order = array index). MUTUALLY EXCLUSIVE with
|
|
138
138
|
* the flat single-overlay options above; the server rejects mixing the two as
|
|
139
139
|
* `invalid_options`. image_watermark jpeg/png/webp bases only.
|
|
140
|
+
*
|
|
141
|
+
* NOTE: NOT usable via `watermark()` yet — the facade composites a single
|
|
142
|
+
* overlay (the positional `overlay`, wire source src_1), so `overlays[]` would
|
|
143
|
+
* reference sources it cannot create. `watermark()` rejects it at lowering
|
|
144
|
+
* (`overlays_unsupported`); use the flat single-overlay options above instead.
|
|
145
|
+
* Kept as a valid contract wire key — multi-overlay stacking is a future
|
|
146
|
+
* feature (Vbbdq9C4).
|
|
140
147
|
*/
|
|
141
148
|
overlays?: WatermarkOverlay[];
|
|
142
149
|
}
|
|
@@ -154,9 +161,11 @@ export type OutputMetadata = 'strip' | 'keep';
|
|
|
154
161
|
* Compression mode on the optimiser (same_format) route (contract `encoding_mode`
|
|
155
162
|
* enum). `quality` (default) drives the encode by the quality slider; `target_size`
|
|
156
163
|
* targets a byte budget via the worker's encode-measure loop — STABLE since
|
|
157
|
-
* contracts v2.108.0 (jpeg/webp/avif).
|
|
164
|
+
* contracts v2.108.0 (jpeg/webp/avif). `auto_quality` lets the worker pick the
|
|
165
|
+
* quality from a named `quality_preset` (its `depends_on`) — the output lowering
|
|
166
|
+
* infers it for you when you set `quality_preset` without an `encoding_mode`.
|
|
158
167
|
*/
|
|
159
|
-
export type OutputEncodingMode = 'quality' | 'target_size';
|
|
168
|
+
export type OutputEncodingMode = 'quality' | 'target_size' | 'auto_quality';
|
|
160
169
|
/** Chroma subsampling for JPEG output (contract `chroma_subsampling` enum, v2.110.0). `420` smallest → `444` highest fidelity. Honored: same_format jpeg only. */
|
|
161
170
|
export type OutputChromaSubsampling = '420' | '422' | '444';
|
|
162
171
|
/** ICC colour-profile handling (contract `color_profile` enum, v2.112.0). `keep` preserves the embedded profile; `srgb` converts to sRGB; `strip` removes it. Route/value availability is gated by the output lowering. */
|
|
@@ -76,6 +76,28 @@ export interface ResolveCompressOptionsOutput {
|
|
|
76
76
|
*/
|
|
77
77
|
export declare function _parseTargetSize(value: unknown): number;
|
|
78
78
|
export declare const KNOWN_WIRE_FIELDS: Readonly<Record<PresetMedia, ReadonlySet<string>>>;
|
|
79
|
+
/**
|
|
80
|
+
* Compress options that are `availability: planned` per mime-group, mirroring the
|
|
81
|
+
* shipped `availability/availability.json`
|
|
82
|
+
* `operations.compress.mime_groups.<group>.options.<opt>.availability`.
|
|
83
|
+
*
|
|
84
|
+
* Kept as a hand table (NOT a runtime read of the ~238KB availability sidecar) for
|
|
85
|
+
* the same reasons as {@link IMAGE_OUTPUT_ROUTES}: the gate stays browser-safe, and
|
|
86
|
+
* — decisively — it has NO dependency on which `@giveitsmaller/contracts` version a
|
|
87
|
+
* consumer resolved. A generated-metadata read would FAIL OPEN on an older published
|
|
88
|
+
* contracts (the rtkzl9gr failure mode), and fail-open is the wrong direction for a
|
|
89
|
+
* gate whose entire job is to fail closed.
|
|
90
|
+
*
|
|
91
|
+
* PINNED to `availability.json` by `tests/unit/preset-planned-conformance.test.ts`,
|
|
92
|
+
* which fails closed in BOTH directions — a contract regen that marks a new option
|
|
93
|
+
* `planned`, or unmarks one, breaks the build rather than the caller. Mirrored by PHP
|
|
94
|
+
* `PresetResolver::PLANNED_COMPRESS_OPTIONS`.
|
|
95
|
+
*
|
|
96
|
+
* `video.speed` is listed for a faithful projection even though no shipped preset
|
|
97
|
+
* cell emits it; the conformance test pins the whole projection, not just the keys
|
|
98
|
+
* we happen to use today.
|
|
99
|
+
*/
|
|
100
|
+
export declare const PLANNED_COMPRESS_OPTIONS: Readonly<Record<PresetMedia, ReadonlySet<string>>>;
|
|
79
101
|
/**
|
|
80
102
|
* Resolve the wire payload + introspection projection for a compress
|
|
81
103
|
* operation call. Throws {@link GislConfigError} before any network
|
|
@@ -251,6 +251,20 @@ const MEDIA_FIELDS = Object.freeze({
|
|
|
251
251
|
document_odf: new Set(['stripMetadata', 'stripUnusedStyles']),
|
|
252
252
|
document_epub: new Set(['fontSubsetting', 'stripUnusedCss']),
|
|
253
253
|
});
|
|
254
|
+
const OUT = (key) => ({ verb: 'output', key });
|
|
255
|
+
const CROSS_VERB_OVERRIDES = Object.freeze({
|
|
256
|
+
image: Object.freeze({
|
|
257
|
+
width: OUT('width'), height: OUT('height'), fit: OUT('fit'),
|
|
258
|
+
autoOrient: OUT('auto_orient'), colorProfile: OUT('color_profile'),
|
|
259
|
+
progressive: OUT('progressive'), lossless: OUT('lossless'),
|
|
260
|
+
qualityPreset: OUT('quality_preset'), encodingMode: OUT('encoding_mode'),
|
|
261
|
+
targetSizeBytes: OUT('target_size_bytes'), chromaSubsampling: OUT('chroma_subsampling'),
|
|
262
|
+
optimizationLevel: OUT('optimization_level'), avifSpeed: OUT('avif_speed'),
|
|
263
|
+
}),
|
|
264
|
+
// convert() takes the target format positionally, not as an option.
|
|
265
|
+
audio: Object.freeze({ outputFormat: { verb: 'convert', key: null } }),
|
|
266
|
+
video: Object.freeze({ outputFormat: { verb: 'convert', key: null } }),
|
|
267
|
+
});
|
|
254
268
|
function detectMismatchedOverrides(media, overrides) {
|
|
255
269
|
const expected = MEDIA_FIELDS[media];
|
|
256
270
|
const keys = Object.keys(overrides);
|
|
@@ -264,6 +278,37 @@ function detectMismatchedOverrides(media, overrides) {
|
|
|
264
278
|
const unknownFields = keys.filter((k) => !expected.has(k));
|
|
265
279
|
if (unknownFields.length === 0)
|
|
266
280
|
return;
|
|
281
|
+
// Options that are real for THIS media but belong to another verb are
|
|
282
|
+
// answered with that verb, before the other-media guess below — otherwise a
|
|
283
|
+
// key both this media and another one recognises (image `width` vs video
|
|
284
|
+
// `width`) gets blamed on the wrong media.
|
|
285
|
+
// Classified PER FIELD, not all-or-nothing: `{ width, codec }` on an image
|
|
286
|
+
// must still tell the caller that `width` is a legal resize on output(),
|
|
287
|
+
// rather than reverting to "you passed video options" for the pair.
|
|
288
|
+
const crossVerb = CROSS_VERB_OVERRIDES[media];
|
|
289
|
+
const crossVerbFields = crossVerb === undefined ? [] : unknownFields.filter((k) => crossVerb[k] !== undefined);
|
|
290
|
+
if (crossVerbFields.length > 0 && crossVerb !== undefined) {
|
|
291
|
+
const strays = unknownFields.filter((k) => crossVerb[k] === undefined);
|
|
292
|
+
const byVerb = new Map();
|
|
293
|
+
for (const k of crossVerbFields) {
|
|
294
|
+
const target = crossVerb[k];
|
|
295
|
+
const list = byVerb.get(target.verb) ?? [];
|
|
296
|
+
list.push(target.key ?? k);
|
|
297
|
+
byVerb.set(target.verb, list);
|
|
298
|
+
}
|
|
299
|
+
const clauses = [...byVerb.entries()].map(([verb, keys]) => verb === 'convert'
|
|
300
|
+
? `${keys.join(', ')} is the format argument of convert()`
|
|
301
|
+
: `${keys.join(', ')} ${keys.length === 1 ? 'is an option' : 'are options'} on ${verb}()`);
|
|
302
|
+
const outputKeys = byVerb.get('output');
|
|
303
|
+
throw new GislConfigError(`presetOverrides for '${media}' contained ${crossVerbFields.join(', ')}, which ${crossVerbFields.length === 1 ? 'does' : 'do'} not belong on the compress preset surface: ${clauses.join('; ')}.` +
|
|
304
|
+
(strays.length > 0 ? ` Also unrecognised for '${media}': ${strays.join(', ')}.` : ''), {
|
|
305
|
+
reason: 'type_mismatch',
|
|
306
|
+
conflictingFields: unknownFields,
|
|
307
|
+
suggestion: outputKeys !== undefined
|
|
308
|
+
? `Move ${outputKeys.join(', ')} to output(): .output(format, { ${outputKeys[0]}: … }).`
|
|
309
|
+
: 'Use convert(format) to change the output format.',
|
|
310
|
+
});
|
|
311
|
+
}
|
|
267
312
|
// Look up which OTHER media owns every unknown field — if a single
|
|
268
313
|
// OTHER media's field set covers them all, that's a type_mismatch.
|
|
269
314
|
for (const otherMedia of Object.keys(MEDIA_FIELDS)) {
|
|
@@ -319,6 +364,35 @@ export const KNOWN_WIRE_FIELDS = Object.freeze({
|
|
|
319
364
|
document_odf: new Set(['strip_metadata', 'strip_unused_styles']),
|
|
320
365
|
document_epub: new Set(['font_subsetting', 'strip_unused_css']),
|
|
321
366
|
});
|
|
367
|
+
/**
|
|
368
|
+
* Compress options that are `availability: planned` per mime-group, mirroring the
|
|
369
|
+
* shipped `availability/availability.json`
|
|
370
|
+
* `operations.compress.mime_groups.<group>.options.<opt>.availability`.
|
|
371
|
+
*
|
|
372
|
+
* Kept as a hand table (NOT a runtime read of the ~238KB availability sidecar) for
|
|
373
|
+
* the same reasons as {@link IMAGE_OUTPUT_ROUTES}: the gate stays browser-safe, and
|
|
374
|
+
* — decisively — it has NO dependency on which `@giveitsmaller/contracts` version a
|
|
375
|
+
* consumer resolved. A generated-metadata read would FAIL OPEN on an older published
|
|
376
|
+
* contracts (the rtkzl9gr failure mode), and fail-open is the wrong direction for a
|
|
377
|
+
* gate whose entire job is to fail closed.
|
|
378
|
+
*
|
|
379
|
+
* PINNED to `availability.json` by `tests/unit/preset-planned-conformance.test.ts`,
|
|
380
|
+
* which fails closed in BOTH directions — a contract regen that marks a new option
|
|
381
|
+
* `planned`, or unmarks one, breaks the build rather than the caller. Mirrored by PHP
|
|
382
|
+
* `PresetResolver::PLANNED_COMPRESS_OPTIONS`.
|
|
383
|
+
*
|
|
384
|
+
* `video.speed` is listed for a faithful projection even though no shipped preset
|
|
385
|
+
* cell emits it; the conformance test pins the whole projection, not just the keys
|
|
386
|
+
* we happen to use today.
|
|
387
|
+
*/
|
|
388
|
+
export const PLANNED_COMPRESS_OPTIONS = Object.freeze({
|
|
389
|
+
image: new Set([]),
|
|
390
|
+
audio: new Set([]),
|
|
391
|
+
video: new Set(['speed']),
|
|
392
|
+
document_office: new Set(['strip_hidden_data', 'strip_macros', 'strip_unused_fonts']),
|
|
393
|
+
document_odf: new Set(['strip_metadata', 'strip_unused_styles']),
|
|
394
|
+
document_epub: new Set(['font_subsetting', 'strip_unused_css']),
|
|
395
|
+
});
|
|
322
396
|
function validateMerged(media, merged, explicitKeys, winners) {
|
|
323
397
|
// Unknown-field defence-in-depth: every key must belong to the
|
|
324
398
|
// media's wire surface OR be one of the resolver-derived wire keys
|
|
@@ -527,6 +601,26 @@ export function resolveCompressOptions(input) {
|
|
|
527
601
|
delete acc.merged.bitrate;
|
|
528
602
|
acc.winners.delete('bitrate');
|
|
529
603
|
}
|
|
604
|
+
// A shipped preset must never put an `availability: planned` option on the wire.
|
|
605
|
+
// The API rejects a planned option when the KEY IS PRESENT and ignores it when
|
|
606
|
+
// absent (`CreateWorkflowCommandHandler::recordPlannedFromMap` — a materialized
|
|
607
|
+
// default deliberately does NOT trigger it), so a value WE synthesized becomes a
|
|
608
|
+
// 422 `feature_not_available` at create that the caller never asked for. That is
|
|
609
|
+
// what broke every document compress with an `optimizeFor` in 0.21.0 (5Eksm9s7).
|
|
610
|
+
//
|
|
611
|
+
// Drop ONLY the sdkDefault-sourced ones. A planned option from any other layer is
|
|
612
|
+
// a caller choice — `clientDefault` and `scopedDefault` are user-registered via
|
|
613
|
+
// `gisl.create({ presetDefaults })` / `client.withPresetDefaults(...)`, not baked
|
|
614
|
+
// in by us — and is left in place for the server to refuse honestly. Same rule and
|
|
615
|
+
// same reason as the lossless-bitrate drop above: never silently swallow a key the
|
|
616
|
+
// caller chose. Do NOT "simplify" either of these into an always-drop; a silent
|
|
617
|
+
// no-op on an explicit request is worse than an honest 422.
|
|
618
|
+
for (const key of PLANNED_COMPRESS_OPTIONS[media]) {
|
|
619
|
+
if (acc.winners.get(key) === 'sdkDefault') {
|
|
620
|
+
delete acc.merged[key];
|
|
621
|
+
acc.winners.delete(key);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
530
624
|
// 7. Validate the merged payload (post-merge — catches cross-layer
|
|
531
625
|
// disagreements). May throw GislConfigError with resolvedSnapshot.
|
|
532
626
|
const explicitWireKeys = new Set();
|
|
@@ -12,10 +12,22 @@
|
|
|
12
12
|
// `mode` + `iccProfile` were REMOVED — the worker is lossy-only and always
|
|
13
13
|
// strips metadata, so advertising a lossless mode or ICC-profile policy was
|
|
14
14
|
// an over-claim. `progressive` is still a per-JPEG wire option but is no
|
|
15
|
-
// longer carried in the preset cell.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// longer carried in the preset cell.
|
|
16
|
+
//
|
|
17
|
+
// `width`/`height`/`fit`/`autoOrient` were removed earlier on the grounds that
|
|
18
|
+
// "the image-compress worker never resized". CAREFUL — that is still true of
|
|
19
|
+
// the Rust optimiser crate and NOT true end-to-end. Since contract v2.97.0
|
|
20
|
+
// ("resize lives inside Output") the API canonicalises an image compress
|
|
21
|
+
// carrying width/height/fit into a `convert` op, and convert IS the resize
|
|
22
|
+
// engine, so such a request returns a genuinely resized file. Their absence
|
|
23
|
+
// here is therefore a SURFACE choice, not a capability limit: resize is
|
|
24
|
+
// expressed via `output()` (see `OutputOptions`), which the compress
|
|
25
|
+
// conformance gate records as CROSS_VERB_ROUTING and self-verifies. Reading
|
|
26
|
+
// this comment as "unsupported" is what produced cySAEZHR. Whether compress()
|
|
27
|
+
// should ALSO carry them is an open ergonomic-expansion decision, not a bug.
|
|
28
|
+
//
|
|
29
|
+
// Per-call knobs are deliberately excluded — they belong on the per-call
|
|
30
|
+
// argument shape.
|
|
19
31
|
import { shippedDefaultsFor as f3ShippedDefaultsFor } from '../../generated/sdk_spec/presets.js';
|
|
20
32
|
import { translateEnum } from './_translate.js';
|
|
21
33
|
export class ImageCompressPresetOptions {
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import { VideoCodec, VideoPreset, VideoFit, AudioCodec, AudioBitrate, OptimizeFor } from '../../generated/sdk_spec/enums.js';
|
|
2
2
|
export interface VideoCompressPresetOptionsInput {
|
|
3
3
|
readonly codec?: VideoCodec;
|
|
4
|
+
/**
|
|
5
|
+
* Target output size (`'50MB'`-style string — BINARY units, 1 KB = 1024 — or a byte
|
|
6
|
+
* count). Derived by the resolver into `target_size_bytes` + `encoding_mode:
|
|
7
|
+
* 'target_size'`.
|
|
8
|
+
*
|
|
9
|
+
* **NOT AVAILABLE FOR LONG INPUTS.** A compress whose input duration routes to the
|
|
10
|
+
* long-form path rejects it (`reject_long_form_target_size`): that path is
|
|
11
|
+
* single-pass-CRF by construction and two-pass target-size is unbuilt. The request
|
|
12
|
+
* fails during execution, and the SDK cannot warn earlier — routing is decided
|
|
13
|
+
* server-side at create-plan time, so there is nothing here to check it against.
|
|
14
|
+
* The same limit applies to {@link MergeOptions.targetSize}.
|
|
15
|
+
* Short-form compresses honour it normally.
|
|
16
|
+
*
|
|
17
|
+
* The contract CAN now express this — `per_class_availability` scopes an option to
|
|
18
|
+
* a processing class, vendored at v2.195.0 and pinned by
|
|
19
|
+
* `tests/unit/per-class-availability-conformance.test.ts`. That buys an honest 422
|
|
20
|
+
* from the API at CREATE rather than a job dying mid-execution; it does NOT become
|
|
21
|
+
* a client-side gate, because routing is still decided server-side and a duration
|
|
22
|
+
* heuristic here would be wrong at the boundary. Tracked by `zJN6XIi5`.
|
|
23
|
+
*/
|
|
4
24
|
readonly targetSize?: string | number;
|
|
5
25
|
readonly crf?: number;
|
|
6
26
|
readonly preset?: VideoPreset;
|
package/dist/errors.d.ts
CHANGED
|
@@ -159,6 +159,21 @@ export declare class GislBalanceExhaustedError extends GislApiError {
|
|
|
159
159
|
export declare class GislLongFormConcurrencyError extends GislApiError {
|
|
160
160
|
readonly payload: LongFormConcurrencyLimitResponse;
|
|
161
161
|
constructor(statusCode: number, errorMessage: string, payload: LongFormConcurrencyLimitResponse, path?: string, extra?: Omit<GislApiErrorOptions, 'payload'>);
|
|
162
|
+
/**
|
|
163
|
+
* ALWAYS `false`, overriding the base 429-implies-retryable heuristic
|
|
164
|
+
* (UO1xYecu). This 429 is not a rate limit: it carries no `Retry-After` and
|
|
165
|
+
* clears only when an in-flight long-form workflow finishes, so a back-off
|
|
166
|
+
* retries into a wall that no amount of waiting-then-retrying opens. The base
|
|
167
|
+
* accessor reported `true` purely from the status, contradicting this class's
|
|
168
|
+
* own documented handling ("wait on completion or upgrade — do NOT back off")
|
|
169
|
+
* and instructing the one recovery that cannot work.
|
|
170
|
+
*
|
|
171
|
+
* Overridden per-class rather than via a code table because this is the only
|
|
172
|
+
* such code today; the general fix — an explicit taxonomy verdict outranking
|
|
173
|
+
* the status heuristic — arrives with the `error-taxonomy.yaml` `retryable`
|
|
174
|
+
* enum (contracts `plwcAqBr`), tracked on UO1xYecu.
|
|
175
|
+
*/
|
|
176
|
+
get retryable(): boolean;
|
|
162
177
|
/** The pricing / upgrade deep link (`links.upgrade`), or `undefined` when absent. */
|
|
163
178
|
get upgradeUrl(): string | undefined;
|
|
164
179
|
}
|
|
@@ -374,6 +389,31 @@ export declare class GislConfigError extends GislError {
|
|
|
374
389
|
export declare class GislMissingCredentialsError extends GislConfigError {
|
|
375
390
|
constructor(message: string);
|
|
376
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* `streamEvents` was called on a client whose configuration has **no declared
|
|
394
|
+
* SSE stream host**. Local-only — thrown before any I/O.
|
|
395
|
+
*
|
|
396
|
+
* ⚠️ **THIS ERROR IS A CONTROL, NOT A DEFECT.** The stream lives on a second
|
|
397
|
+
* host, and the SDK will not guess it from `baseUrl`. Deriving `stream.*` from
|
|
398
|
+
* `api.*` by string surgery is a *convention*, and a convention is precisely
|
|
399
|
+
* what put production on the gateway path: the frontend's prod build had no
|
|
400
|
+
* stream host configured, fell back to the API host silently, and the failure
|
|
401
|
+
* was invisible until it was measured. Raising here is the loud version of
|
|
402
|
+
* that same situation.
|
|
403
|
+
*
|
|
404
|
+
* Both named environments resolve as of contracts `v2.195.0` (#410), which
|
|
405
|
+
* declared the production stream host. This now fires only for a
|
|
406
|
+
* configuration nothing declares — e.g. a bare `baseUrl` with no
|
|
407
|
+
* `environment` and no `streamBaseUrl`.
|
|
408
|
+
*
|
|
409
|
+
* Recover by passing `{streamBaseUrl}` to `gisl.create()` / `new GislClient()`,
|
|
410
|
+
* setting `GISL_STREAM_BASE_URL`, or constructing with an `{environment}` that
|
|
411
|
+
* declares one. `run()` does NOT surface this error — it treats an undeclared
|
|
412
|
+
* stream host as "SSE unavailable for this configuration" and polls instead.
|
|
413
|
+
*/
|
|
414
|
+
export declare class GislStreamHostNotDeclaredError extends GislConfigError {
|
|
415
|
+
constructor(message: string);
|
|
416
|
+
}
|
|
377
417
|
/**
|
|
378
418
|
* The caller used `gisl.anonymous()` and then invoked an operation that is
|
|
379
419
|
* not in the anonymous-capable allowlist. Local-only — thrown before any I/O.
|
|
@@ -460,17 +500,153 @@ export declare class GislTimeoutError extends GislError {
|
|
|
460
500
|
constructor(message: string, workflowId?: string);
|
|
461
501
|
}
|
|
462
502
|
/**
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
*
|
|
466
|
-
*
|
|
467
|
-
* `
|
|
468
|
-
*
|
|
469
|
-
*
|
|
503
|
+
* A `mapEach` fan-out timed out mid-batch — the deadline elapsed either while a
|
|
504
|
+
* child was still running (the common case) or cleanly between child runs. The
|
|
505
|
+
* parent and some children have ALREADY completed, so re-running the whole batch
|
|
506
|
+
* re-does finished work. This carries their ids so the caller can poll them (via
|
|
507
|
+
* `client.getWorkflowStatus` / `getWorkflowDownloads`) to recover the finished
|
|
508
|
+
* work and re-run ONLY the children that were never created.
|
|
509
|
+
*
|
|
510
|
+
* Subclasses {@link GislTimeoutError}, so an existing
|
|
511
|
+
* `catch (e) { if (e instanceof GislTimeoutError) … }` still catches it. The
|
|
512
|
+
* inherited `workflowId` carries the IN-FLIGHT child — the one that was running
|
|
513
|
+
* when the deadline elapsed (a child's own timeout, the common path) — or stays
|
|
514
|
+
* `undefined` when the deadline elapsed cleanly BETWEEN children (no in-flight
|
|
515
|
+
* child). To recover, poll `workflowId` (if set) + {@link parentWorkflowId} +
|
|
516
|
+
* {@link completedWorkflowIds}, then re-run only the children that never started.
|
|
517
|
+
*
|
|
518
|
+
* NOTE on double-charge: the server-side create-dedupe (DSxwCetg) is what
|
|
519
|
+
* prevents a byte-identical child re-create from settling a SECOND charge within
|
|
520
|
+
* the dedup window; this error's job is efficient RECOVERY (skip the completed
|
|
521
|
+
* work) + defense-in-depth, not the sole charge guard.
|
|
522
|
+
*/
|
|
523
|
+
export declare class GislFanOutTimeoutError extends GislTimeoutError {
|
|
524
|
+
/** The child workflows that completed before the deadline elapsed. */
|
|
525
|
+
readonly completedWorkflowIds: readonly string[];
|
|
526
|
+
/** The parent workflow, which ran to completion before the fan-out began. */
|
|
527
|
+
readonly parentWorkflowId?: string;
|
|
528
|
+
constructor(message: string, opts: {
|
|
529
|
+
completedWorkflowIds: readonly string[];
|
|
530
|
+
parentWorkflowId?: string;
|
|
531
|
+
/**
|
|
532
|
+
* The in-flight child that timed out mid-run (its own deadline elapsed);
|
|
533
|
+
* `undefined` for a clean between-children timeout with no child running.
|
|
534
|
+
*/
|
|
535
|
+
workflowId?: string;
|
|
536
|
+
/** The underlying child {@link GislTimeoutError}, preserved for chaining. */
|
|
537
|
+
cause?: unknown;
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Base for every failure that happened **off the contract envelope** — the
|
|
542
|
+
* request did not come back as a typed API error, it came back (or failed to)
|
|
543
|
+
* at the transport or raw-HTTP level. Subclasses `GislError` rather than
|
|
544
|
+
* `GislApiError` because there is no error envelope to carry.
|
|
545
|
+
*
|
|
546
|
+
* ⚠️ **NEVER THROWN DIRECTLY — it is a hierarchy node, not an error code
|
|
547
|
+
* (`t2qCrjdr`).** Everything that used to throw it now throws
|
|
548
|
+
* {@link GislTransportError} or {@link GislDownloadHttpError}, because the two
|
|
549
|
+
* cases cannot share one honest answer to "should I retry this?":
|
|
550
|
+
*
|
|
551
|
+
* | case | retry? |
|
|
552
|
+
* |---|---|
|
|
553
|
+
* | DNS / TCP / TLS / mid-stream disconnect | **yes** — transient by nature |
|
|
554
|
+
* | a `404` on a signed download URL | **no** — permanent, retrying burns time |
|
|
555
|
+
*
|
|
556
|
+
* `retryable: true` would recommend retrying a permanent failure and
|
|
557
|
+
* `retryable: false` would discourage retrying a genuine transient one, so
|
|
558
|
+
* contracts correctly refused to declare this class in
|
|
559
|
+
* `sdk-spec/error-taxonomy.yaml` at all. The fix is the split, not a caveat in
|
|
560
|
+
* a description field: **a claim must hold on every path that reaches it.**
|
|
561
|
+
*
|
|
562
|
+
* **Kept as the base ON PURPOSE, so this is not a breaking change.** Every
|
|
563
|
+
* existing `catch (e) { if (e instanceof GislNetworkError) … }` — including the
|
|
564
|
+
* SSE poll-fallback in `builder.ts` / `merge.ts` / `handle.ts` /
|
|
565
|
+
* `file-first.ts` — keeps catching exactly what it caught before. Narrow to a
|
|
566
|
+
* subclass only where you actually need to tell the two apart.
|
|
567
|
+
*
|
|
568
|
+
* Mirrors the PHP `Gisl\Sdk\Errors\GislNetworkError`.
|
|
470
569
|
*/
|
|
471
570
|
export declare class GislNetworkError extends GislError {
|
|
472
571
|
constructor(message: string);
|
|
473
572
|
}
|
|
573
|
+
/**
|
|
574
|
+
* The transport could not deliver a usable response: DNS, TCP, TLS, a
|
|
575
|
+
* mid-stream disconnect, a `fetch` rejection, or a 2xx that arrived with no
|
|
576
|
+
* body at all. **Always retryable** — nothing about these says the request was
|
|
577
|
+
* wrong, only that it did not get through.
|
|
578
|
+
*
|
|
579
|
+
* The empty-body case lives here rather than with
|
|
580
|
+
* {@link GislDownloadHttpError} deliberately: the server said 2xx, so it is not
|
|
581
|
+
* an HTTP-level refusal — a response that promised bytes and delivered none is
|
|
582
|
+
* a delivery failure, and retrying is the right advice.
|
|
583
|
+
*
|
|
584
|
+
* The concrete file-first `Downloader` raises this when an output URL cannot be
|
|
585
|
+
* read (a destination-WRITE failure is `GislSinkError` reason `write_failed`).
|
|
586
|
+
*/
|
|
587
|
+
export declare class GislTransportError extends GislNetworkError {
|
|
588
|
+
constructor(message: string);
|
|
589
|
+
/**
|
|
590
|
+
* Always `true`. The request did not get through; nothing about that says it
|
|
591
|
+
* was wrong, so retrying is the correct advice.
|
|
592
|
+
*
|
|
593
|
+
* Present as a real accessor rather than only as prose — an unbacked claim in
|
|
594
|
+
* a docblock is the exact defect `t2qCrjdr` exists to remove, and shipping
|
|
595
|
+
* the split without it would have reproduced it one level down.
|
|
596
|
+
*/
|
|
597
|
+
get retryable(): boolean;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* The request was never put on the wire because the client refused to send it —
|
|
601
|
+
* a malformed URI or an otherwise unsendable request. **Never retryable:**
|
|
602
|
+
* re-issuing the identical request fails identically, so backing off only
|
|
603
|
+
* wastes the caller's deadline.
|
|
604
|
+
*
|
|
605
|
+
* ⚠️ **THE TWO LANGUAGES DETECT THIS DIFFERENTLY, AND TS DETECTS LESS.** PSR-18
|
|
606
|
+
* distinguishes a network failure (`NetworkExceptionInterface`) from an
|
|
607
|
+
* unsendable request (`RequestExceptionInterface`), so the PHP SDK classifies
|
|
608
|
+
* every such failure. `fetch` surfaces both as an indistinguishable
|
|
609
|
+
* `TypeError`, so the TS SDK can only catch the cases it can see BEFORE the
|
|
610
|
+
* call — today, a URL that does not parse (`http-downloader`). A `fetch`
|
|
611
|
+
* rejection is still reported as {@link GislTransportError}, because guessing
|
|
612
|
+
* would put a permanent failure back in the retryable bucket, which is the very
|
|
613
|
+
* thing this split removed.
|
|
614
|
+
*
|
|
615
|
+
* So: same class, same meaning, same `retryable` in both SDKs — narrower
|
|
616
|
+
* detection in TypeScript. Stated here because a cross-language consumer would
|
|
617
|
+
* otherwise reasonably assume parity of COVERAGE from parity of TYPE.
|
|
618
|
+
*/
|
|
619
|
+
export declare class GislRequestNotSentError extends GislNetworkError {
|
|
620
|
+
constructor(message: string);
|
|
621
|
+
/** Always `false` — the request never left, and re-sending it will not change that. */
|
|
622
|
+
get retryable(): boolean;
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* A download URL answered with a **non-2xx status**. The server was reached and
|
|
626
|
+
* replied; it simply refused. Distinct from {@link GislTransportError} because
|
|
627
|
+
* retrying is usually pointless — and `retryable` says so honestly, derived
|
|
628
|
+
* from the status rather than fixed for the class.
|
|
629
|
+
*
|
|
630
|
+
* `status` is carried as a field so a consumer distinguishing a permanent `404`
|
|
631
|
+
* from a transient `503` does not have to parse the message string — the second
|
|
632
|
+
* half of `t2qCrjdr`.
|
|
633
|
+
*
|
|
634
|
+
* Raised on result-download fetches (signed URLs), NOT on GISL-API calls: an
|
|
635
|
+
* API non-2xx carries a contract error envelope and surfaces as the matching
|
|
636
|
+
* {@link GislApiError} subclass instead.
|
|
637
|
+
*/
|
|
638
|
+
export declare class GislDownloadHttpError extends GislNetworkError {
|
|
639
|
+
/** The HTTP status the download URL responded with. */
|
|
640
|
+
readonly status: number;
|
|
641
|
+
constructor(message: string, status: number);
|
|
642
|
+
/**
|
|
643
|
+
* Whether retrying this download could plausibly succeed. Derived from the
|
|
644
|
+
* status by the same rule the API errors use (`408` / `429` / `5xx`), so a
|
|
645
|
+
* `404` reports `false` and a `503` reports `true` — the distinction the
|
|
646
|
+
* unsplit class could not express.
|
|
647
|
+
*/
|
|
648
|
+
get retryable(): boolean;
|
|
649
|
+
}
|
|
474
650
|
/**
|
|
475
651
|
* Internal control-flow marker (TDqmkWpX): the SSE event stream closed cleanly
|
|
476
652
|
* WITHOUT a terminal (`workflow_completed`/`failed`/`partially_failed`) event.
|