@giveitsmaller/sdk 0.20.0 → 0.22.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.
Files changed (40) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +55 -14
  3. package/dist/_audit.js +2 -4
  4. package/dist/builder.d.ts +4 -4
  5. package/dist/builder.js +47 -22
  6. package/dist/client.d.ts +9 -6
  7. package/dist/client.js +90 -31
  8. package/dist/ergonomic/image_output_routes.d.ts +97 -0
  9. package/dist/ergonomic/image_output_routes.js +227 -25
  10. package/dist/ergonomic/option_types.d.ts +11 -2
  11. package/dist/ergonomic/preset_resolver.d.ts +24 -2
  12. package/dist/ergonomic/preset_resolver.js +100 -10
  13. package/dist/ergonomic/presets/image_compress.js +16 -4
  14. package/dist/ergonomic/presets/index.d.ts +9 -8
  15. package/dist/ergonomic/presets/index.js +1 -9
  16. package/dist/ergonomic/presets/video_compress.d.ts +14 -0
  17. package/dist/errors.d.ts +101 -2
  18. package/dist/errors.js +108 -1
  19. package/dist/file-first.d.ts +81 -13
  20. package/dist/file-first.js +329 -69
  21. package/dist/generated/sdk_spec/enums.d.ts +0 -26
  22. package/dist/generated/sdk_spec/enums.js +0 -16
  23. package/dist/generated/sdk_spec/errors.d.ts +1 -1
  24. package/dist/generated/sdk_spec/errors.js +159 -1
  25. package/dist/generated/sdk_spec/presets.js +0 -14
  26. package/dist/generated/sdk_spec/version.d.ts +2 -2
  27. package/dist/generated/sdk_spec/version.js +2 -2
  28. package/dist/gisl.d.ts +21 -2
  29. package/dist/handle.d.ts +6 -1
  30. package/dist/handle.js +42 -13
  31. package/dist/index.core.d.ts +4 -4
  32. package/dist/index.core.js +6 -3
  33. package/dist/merge.d.ts +23 -0
  34. package/dist/merge.js +2 -2
  35. package/dist/sse.js +49 -1
  36. package/dist/types.d.ts +11 -3
  37. package/dist/types.js +1 -0
  38. package/package.json +3 -3
  39. package/dist/ergonomic/presets/document_pdf_compress.d.ts +0 -12
  40. package/dist/ergonomic/presets/document_pdf_compress.js +0 -33
@@ -1,6 +1,6 @@
1
1
  import type { ResolvedOptions } from '../builder.js';
2
2
  import type { OptimizeFor } from '../generated/sdk_spec/enums.js';
3
- import { type PresetDefaults, type PresetMedia, type PresetOp } from './presets/index.js';
3
+ import { type PresetDefaults, type PresetMedia, type DetectedMedia, type PresetOp } from './presets/index.js';
4
4
  /**
5
5
  * The preset matrix version emitted on every resolve. Re-exported from the
6
6
  * GENERATED `sdk_spec/version.ts` (source of truth: contracts
@@ -17,7 +17,7 @@ export declare const PRESET_VERSION: "1.6";
17
17
  * extend the union.
18
18
  */
19
19
  export interface ResolveCompressOptionsInput {
20
- readonly media: PresetMedia;
20
+ readonly media: DetectedMedia;
21
21
  readonly op: PresetOp;
22
22
  /** Defaults registered via `gisl.create({ presetDefaults: ... })`. */
23
23
  readonly presetDefaults?: PresetDefaults;
@@ -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
@@ -37,7 +37,7 @@
37
37
  import { sha256Hex } from '../sha256.js';
38
38
  import { GislConfigError } from '../errors.js';
39
39
  import { PRESET_VERSION as GENERATED_PRESET_VERSION } from '../generated/sdk_spec/version.js';
40
- import { ImageCompressPresetOptions, AudioCompressPresetOptions, VideoCompressPresetOptions, DocumentPdfCompressPresetOptions, DocumentOfficeCompressPresetOptions, DocumentOdfCompressPresetOptions, DocumentEpubCompressPresetOptions, definedFieldsOf, } from './presets/index.js';
40
+ import { ImageCompressPresetOptions, AudioCompressPresetOptions, VideoCompressPresetOptions, DocumentOfficeCompressPresetOptions, DocumentOdfCompressPresetOptions, DocumentEpubCompressPresetOptions, definedFieldsOf, } from './presets/index.js';
41
41
  /**
42
42
  * The preset matrix version emitted on every resolve. Re-exported from the
43
43
  * GENERATED `sdk_spec/version.ts` (source of truth: contracts
@@ -175,8 +175,6 @@ function sdkDefaultRecord(media, op, optimize) {
175
175
  return { ...AudioCompressPresetOptions.shippedDefaultsFor(optimize) };
176
176
  case 'video':
177
177
  return { ...VideoCompressPresetOptions.shippedDefaultsFor(optimize) };
178
- case 'document_pdf':
179
- return { ...DocumentPdfCompressPresetOptions.shippedDefaultsFor(optimize) };
180
178
  case 'document_office':
181
179
  return { ...DocumentOfficeCompressPresetOptions.shippedDefaultsFor(optimize) };
182
180
  case 'document_odf':
@@ -213,9 +211,6 @@ function presetDefaultsCellRecord(defaults, media, op, optimize) {
213
211
  case 'video':
214
212
  cell = defaults.cellFor('video', 'compress', optimize);
215
213
  break;
216
- case 'document_pdf':
217
- cell = defaults.cellFor('document_pdf', 'compress', optimize);
218
- break;
219
214
  case 'document_office':
220
215
  cell = defaults.cellFor('document_office', 'compress', optimize);
221
216
  break;
@@ -252,11 +247,24 @@ const MEDIA_FIELDS = Object.freeze({
252
247
  image: new Set(['quality', 'metadata', 'outputFormat']),
253
248
  audio: new Set(['bitrate', 'channels', 'sampleRate', 'normalize']),
254
249
  video: new Set(['codec', 'targetSize', 'crf', 'preset', 'width', 'height', 'fit', 'fps', 'faststart', 'audioCodec', 'audioBitrate']),
255
- document_pdf: new Set(['profile', 'grayscale']),
256
250
  document_office: new Set(['stripMacros', 'stripHiddenData', 'stripUnusedFonts']),
257
251
  document_odf: new Set(['stripMetadata', 'stripUnusedStyles']),
258
252
  document_epub: new Set(['fontSubsetting', 'stripUnusedCss']),
259
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
+ });
260
268
  function detectMismatchedOverrides(media, overrides) {
261
269
  const expected = MEDIA_FIELDS[media];
262
270
  const keys = Object.keys(overrides);
@@ -270,6 +278,37 @@ function detectMismatchedOverrides(media, overrides) {
270
278
  const unknownFields = keys.filter((k) => !expected.has(k));
271
279
  if (unknownFields.length === 0)
272
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
+ }
273
312
  // Look up which OTHER media owns every unknown field — if a single
274
313
  // OTHER media's field set covers them all, that's a type_mismatch.
275
314
  for (const otherMedia of Object.keys(MEDIA_FIELDS)) {
@@ -278,9 +317,9 @@ function detectMismatchedOverrides(media, overrides) {
278
317
  const otherSet = MEDIA_FIELDS[otherMedia];
279
318
  if (unknownFields.every((k) => otherSet.has(k))) {
280
319
  // PascalCase every underscore-separated segment so multi-segment
281
- // media (`document_pdf` → `DocumentPdf…`) emit the actual exported
320
+ // media (`document_office` → `DocumentOffice…`) emit the actual exported
282
321
  // class name (code-review MEDIUM: previously emitted
283
- // `Documentpdf…` which doesn't resolve in user code).
322
+ // `Documentoffice…` which doesn't resolve in user code).
284
323
  const className = otherMedia
285
324
  .split('_')
286
325
  .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
@@ -321,11 +360,39 @@ export const KNOWN_WIRE_FIELDS = Object.freeze({
321
360
  image: new Set(['quality', 'metadata', 'output_format']),
322
361
  audio: new Set(['bitrate', 'channels', 'sample_rate', 'normalize', 'trim_start', 'trim_end']),
323
362
  video: new Set(['codec', 'encoding_mode', 'crf', 'target_size_bytes', 'preset', 'width', 'height', 'fit', 'fps', 'faststart', 'audio_codec', 'audio_bitrate', 'trim_start', 'trim_end']),
324
- document_pdf: new Set(['profile', 'grayscale']),
325
363
  document_office: new Set(['strip_macros', 'strip_hidden_data', 'strip_unused_fonts']),
326
364
  document_odf: new Set(['strip_metadata', 'strip_unused_styles']),
327
365
  document_epub: new Set(['font_subsetting', 'strip_unused_css']),
328
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
+ });
329
396
  function validateMerged(media, merged, explicitKeys, winners) {
330
397
  // Unknown-field defence-in-depth: every key must belong to the
331
398
  // media's wire surface OR be one of the resolver-derived wire keys
@@ -444,6 +511,9 @@ function computePresetConfigHash(clientDefault, scopedDefault, callPresetOverrid
444
511
  * `optimize` unset ⇒ layer 1 contributes nothing; `resolvedOptions.preset = null`.
445
512
  */
446
513
  export function resolveCompressOptions(input) {
514
+ if (input.media === 'document_pdf') {
515
+ throw new GislConfigError('PDF compression was removed at contracts v2.166.0; convert() / transform() still accept PDF.', { reason: 'unsupported_media' });
516
+ }
447
517
  const { media, op, presetDefaults, scopedPresetDefaults, presetOverrides, optimize, explicitOptions, audioLossless } = input;
448
518
  if (op !== 'compress') {
449
519
  throw new GislConfigError(`Preset resolution is only wired for compress operations today; got op='${op}'.`, { reason: 'unsupported_op' });
@@ -531,6 +601,26 @@ export function resolveCompressOptions(input) {
531
601
  delete acc.merged.bitrate;
532
602
  acc.winners.delete('bitrate');
533
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
+ }
534
624
  // 7. Validate the merged payload (post-merge — catches cross-layer
535
625
  // disagreements). May throw GislConfigError with resolvedSnapshot.
536
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. `width`/`height`/`fit`/`autoOrient`
16
- // were removed earlier — the image-compress worker never resized (resize-fit
17
- // lives on thumbnail/convert; video keeps its own fit). Per-call knobs are
18
- // deliberately excluded they belong on the per-call argument shape.
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 {
@@ -2,27 +2,31 @@ import { OptimizeFor } from '../../generated/sdk_spec/enums.js';
2
2
  import { ImageCompressPresetOptions, type ImageCompressPresetOptionsInput } from './image_compress.js';
3
3
  import { AudioCompressPresetOptions, type AudioCompressPresetOptionsInput } from './audio_compress.js';
4
4
  import { VideoCompressPresetOptions, type VideoCompressPresetOptionsInput } from './video_compress.js';
5
- import { DocumentPdfCompressPresetOptions, type DocumentPdfCompressPresetOptionsInput } from './document_pdf_compress.js';
6
5
  import { DocumentOfficeCompressPresetOptions, type DocumentOfficeCompressPresetOptionsInput } from './document_office_compress.js';
7
6
  import { DocumentOdfCompressPresetOptions, type DocumentOdfCompressPresetOptionsInput } from './document_odf_compress.js';
8
7
  import { DocumentEpubCompressPresetOptions, type DocumentEpubCompressPresetOptionsInput } from './document_epub_compress.js';
9
8
  export { ImageCompressPresetOptions, type ImageCompressPresetOptionsInput, } from './image_compress.js';
10
9
  export { AudioCompressPresetOptions, type AudioCompressPresetOptionsInput, } from './audio_compress.js';
11
10
  export { VideoCompressPresetOptions, type VideoCompressPresetOptionsInput, } from './video_compress.js';
12
- export { DocumentPdfCompressPresetOptions, type DocumentPdfCompressPresetOptionsInput, } from './document_pdf_compress.js';
13
11
  export { DocumentOfficeCompressPresetOptions, type DocumentOfficeCompressPresetOptionsInput, } from './document_office_compress.js';
14
12
  export { DocumentOdfCompressPresetOptions, type DocumentOdfCompressPresetOptionsInput, } from './document_odf_compress.js';
15
13
  export { DocumentEpubCompressPresetOptions, type DocumentEpubCompressPresetOptionsInput, } from './document_epub_compress.js';
16
- export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, PdfProfile, PdfColorspace, } from '../../generated/sdk_spec/enums.js';
14
+ export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, } from '../../generated/sdk_spec/enums.js';
17
15
  /** Supported media×op pairs for preset cells in T4a. Compress-only. */
18
- export type PresetMedia = 'image' | 'audio' | 'video' | 'document_pdf' | 'document_office' | 'document_odf' | 'document_epub';
16
+ export type PresetMedia = 'image' | 'audio' | 'video' | 'document_office' | 'document_odf' | 'document_epub';
17
+ /**
18
+ * Media the file-first detector can identify — the compressible
19
+ * `PresetMedia` set PLUS `document_pdf`, which is detectable (and a valid
20
+ * watermark-reject / convert / transform base) but NOT compressible.
21
+ */
22
+ export type DetectedMedia = PresetMedia | 'document_pdf';
19
23
  export type PresetOp = 'compress';
20
24
  /**
21
25
  * Union of leaf-DTO types the resolver will see from `cellFor()`.
22
26
  * Discriminated by which `media` the caller passes — the type system
23
27
  * narrows the return automatically via the overload set below.
24
28
  */
25
- export type AnyPresetOptions = ImageCompressPresetOptions | AudioCompressPresetOptions | VideoCompressPresetOptions | DocumentPdfCompressPresetOptions | DocumentOfficeCompressPresetOptions | DocumentOdfCompressPresetOptions | DocumentEpubCompressPresetOptions;
29
+ export type AnyPresetOptions = ImageCompressPresetOptions | AudioCompressPresetOptions | VideoCompressPresetOptions | DocumentOfficeCompressPresetOptions | DocumentOdfCompressPresetOptions | DocumentEpubCompressPresetOptions;
26
30
  /**
27
31
  * Per-cell field-merge: parent fields ⊕ child fields where defined.
28
32
  * Re-construct the leaf DTO via the matching `<LeafClass>.from(merged)`
@@ -73,8 +77,6 @@ export declare class PresetDefaults {
73
77
  audioCompress(level: OptimizeFor, input?: AudioCompressPresetOptionsInput): PresetDefaults;
74
78
  /** Register a (level, delta) on the video-compress cell. Immutable. */
75
79
  videoCompress(level: OptimizeFor, input?: VideoCompressPresetOptionsInput): PresetDefaults;
76
- /** Register a (level, delta) on the document-pdf-compress cell. Immutable. */
77
- pdfCompress(level: OptimizeFor, input?: DocumentPdfCompressPresetOptionsInput): PresetDefaults;
78
80
  /** Register a (level, delta) on the document-office-compress cell. Immutable. */
79
81
  officeCompress(level: OptimizeFor, input?: DocumentOfficeCompressPresetOptionsInput): PresetDefaults;
80
82
  /** Register a (level, delta) on the document-odf-compress cell. Immutable. */
@@ -84,7 +86,6 @@ export declare class PresetDefaults {
84
86
  /** @internal */ cellFor(media: 'image', op: 'compress', level: OptimizeFor): ImageCompressPresetOptions | undefined;
85
87
  /** @internal */ cellFor(media: 'audio', op: 'compress', level: OptimizeFor): AudioCompressPresetOptions | undefined;
86
88
  /** @internal */ cellFor(media: 'video', op: 'compress', level: OptimizeFor): VideoCompressPresetOptions | undefined;
87
- /** @internal */ cellFor(media: 'document_pdf', op: 'compress', level: OptimizeFor): DocumentPdfCompressPresetOptions | undefined;
88
89
  /** @internal */ cellFor(media: 'document_office', op: 'compress', level: OptimizeFor): DocumentOfficeCompressPresetOptions | undefined;
89
90
  /** @internal */ cellFor(media: 'document_odf', op: 'compress', level: OptimizeFor): DocumentOdfCompressPresetOptions | undefined;
90
91
  /** @internal */ cellFor(media: 'document_epub', op: 'compress', level: OptimizeFor): DocumentEpubCompressPresetOptions | undefined;
@@ -28,7 +28,6 @@
28
28
  import { ImageCompressPresetOptions, } from './image_compress.js';
29
29
  import { AudioCompressPresetOptions, } from './audio_compress.js';
30
30
  import { VideoCompressPresetOptions, } from './video_compress.js';
31
- import { DocumentPdfCompressPresetOptions, } from './document_pdf_compress.js';
32
31
  import { DocumentOfficeCompressPresetOptions, } from './document_office_compress.js';
33
32
  import { DocumentOdfCompressPresetOptions, } from './document_odf_compress.js';
34
33
  import { DocumentEpubCompressPresetOptions, } from './document_epub_compress.js';
@@ -36,12 +35,11 @@ import { DocumentEpubCompressPresetOptions, } from './document_epub_compress.js'
36
35
  export { ImageCompressPresetOptions, } from './image_compress.js';
37
36
  export { AudioCompressPresetOptions, } from './audio_compress.js';
38
37
  export { VideoCompressPresetOptions, } from './video_compress.js';
39
- export { DocumentPdfCompressPresetOptions, } from './document_pdf_compress.js';
40
38
  export { DocumentOfficeCompressPresetOptions, } from './document_office_compress.js';
41
39
  export { DocumentOdfCompressPresetOptions, } from './document_odf_compress.js';
42
40
  export { DocumentEpubCompressPresetOptions, } from './document_epub_compress.js';
43
41
  // Re-export ergonomic enums for callers (single canonical path).
44
- export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, PdfProfile, PdfColorspace, } from '../../generated/sdk_spec/enums.js';
42
+ export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, } from '../../generated/sdk_spec/enums.js';
45
43
  function cellKeyOf(media, op) {
46
44
  return `${media}_${op}`;
47
45
  }
@@ -89,8 +87,6 @@ function mergePresetOptions(cellKey, parentOpts, childOpts) {
89
87
  return AudioCompressPresetOptions.from(mergedFields);
90
88
  case 'video_compress':
91
89
  return VideoCompressPresetOptions.from(mergedFields);
92
- case 'document_pdf_compress':
93
- return DocumentPdfCompressPresetOptions.from(mergedFields);
94
90
  case 'document_office_compress':
95
91
  return DocumentOfficeCompressPresetOptions.from(mergedFields);
96
92
  case 'document_odf_compress':
@@ -178,10 +174,6 @@ export class PresetDefaults {
178
174
  videoCompress(level, input = {}) {
179
175
  return new PresetDefaults(withCellEntry(this.cells, 'video_compress', level, VideoCompressPresetOptions.from(input)));
180
176
  }
181
- /** Register a (level, delta) on the document-pdf-compress cell. Immutable. */
182
- pdfCompress(level, input = {}) {
183
- return new PresetDefaults(withCellEntry(this.cells, 'document_pdf_compress', level, DocumentPdfCompressPresetOptions.from(input)));
184
- }
185
177
  /** Register a (level, delta) on the document-office-compress cell. Immutable. */
186
178
  officeCompress(level, input = {}) {
187
179
  return new PresetDefaults(withCellEntry(this.cells, 'document_office_compress', level, DocumentOfficeCompressPresetOptions.from(input)));
@@ -1,6 +1,20 @@
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
+ * Short-form compresses honour it normally. Tracked by `zJN6XIi5`, blocked on a
15
+ * contract that can express per-execution-path availability. The same limit applies
16
+ * to {@link MergeOptions.targetSize}.
17
+ */
4
18
  readonly targetSize?: string | number;
5
19
  readonly crf?: number;
6
20
  readonly preset?: VideoPreset;
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AuthErrorResponse, AuthRejectionEnvelope, AuthRejectionEnvelopeErrorTypeEnum, BalanceExhaustedResponse, FeatureNotAvailableResponse, FeatureTierRestrictedResponse, ProbePendingResponse, TierRestrictionResponse, UploadDurationExceedsTierResponse, UploadSizeExceedsTierResponse, WorkflowExpiredResponse } from '@giveitsmaller/contracts/openapi';
1
+ import type { AuthErrorResponse, AuthRejectionEnvelope, AuthRejectionEnvelopeErrorTypeEnum, BalanceExhaustedResponse, FeatureNotAvailableResponse, FeatureTierRestrictedResponse, LongFormConcurrencyLimitResponse, ProbePendingResponse, TierRestrictionResponse, UploadDurationExceedsTierResponse, UploadSizeExceedsTierResponse, WorkflowExpiredResponse } from '@giveitsmaller/contracts/openapi';
2
2
  import type { ErrorCategory } from './generated/sdk_spec/errors.js';
3
3
  import type { RateLimitSnapshot } from './retry-metadata.js';
4
4
  export declare class GislError extends Error {
@@ -134,6 +134,49 @@ export declare class GislBalanceExhaustedError extends GislApiError {
134
134
  readonly payload: BalanceExhaustedResponse;
135
135
  constructor(statusCode: number, errorMessage: string, payload: BalanceExhaustedResponse, path?: string, extra?: Omit<GislApiErrorOptions, 'payload'>);
136
136
  }
137
+ /**
138
+ * `429` on `POST /api/workflows` when the caller already holds the maximum
139
+ * number of concurrent in-flight long-form (Fargate) workflows their tier
140
+ * permits (Pro 2 / Max 5; Enterprise uncapped). DISTINCT from an infrastructure
141
+ * rate-limit `429`: it carries the machine code `LONG_FORM_CONCURRENCY_LIMIT_EXCEEDED`
142
+ * and a `links.upgrade` deep link, and has **no `Retry-After`** — the limit clears
143
+ * when an in-flight long-form workflow finishes, not on a timer. A generic infra
144
+ * rate-limit `429` (no matching code) surfaces as the base {@link GislApiError}
145
+ * instead, where {@link GislApiError.retryAfterSeconds} applies.
146
+ *
147
+ * Dispatched on the `error` CODE, not `error_type` (the envelope carries none).
148
+ *
149
+ * @example
150
+ * try {
151
+ * await client.createWorkflow({ jobs });
152
+ * } catch (e) {
153
+ * if (e instanceof GislLongFormConcurrencyError) {
154
+ * showUpgradeCta(e.upgradeUrl); // wait on completion or upgrade — do NOT back off
155
+ * }
156
+ * throw e;
157
+ * }
158
+ */
159
+ export declare class GislLongFormConcurrencyError extends GislApiError {
160
+ readonly payload: LongFormConcurrencyLimitResponse;
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;
177
+ /** The pricing / upgrade deep link (`links.upgrade`), or `undefined` when absent. */
178
+ get upgradeUrl(): string | undefined;
179
+ }
137
180
  export declare class GislTierRestrictedError extends GislApiError {
138
181
  readonly payload: TierRestrictionResponse;
139
182
  constructor(statusCode: number, errorMessage: string, payload: TierRestrictionResponse, path?: string, extra?: Omit<GislApiErrorOptions, 'payload'>);
@@ -411,7 +454,63 @@ export declare class GislBundleAlreadyArchivedError extends GislConfigError {
411
454
  constructor();
412
455
  }
413
456
  export declare class GislTimeoutError extends GislError {
414
- constructor(message: string);
457
+ /**
458
+ * The workflow this timeout is scoped to, when the SDK knows it. Set on a
459
+ * timed-out `run()` / `wait()` / poll / download once the workflow has been
460
+ * created: a timeout does NOT mean the work failed — the server keeps
461
+ * processing, so poll `client.getWorkflowStatus(workflowId)` /
462
+ * `getWorkflowDownloads(workflowId)` to recover a result that completed after
463
+ * the deadline, instead of re-running (a re-run re-uploads and, for
464
+ * authenticated callers, settles a SECOND charge for the same deliverable).
465
+ *
466
+ * `undefined` when the SDK has no id to offer. That is NOT a guarantee that
467
+ * nothing was created or charged: it covers both the safe case (an upload /
468
+ * probe timeout before any workflow existed) AND the AMBIGUOUS case (the
469
+ * `POST /api/workflows` request itself timed out — the server may have
470
+ * created and charged the workflow before its response was lost). Treat an
471
+ * absent id as "cannot auto-recover", not "clean slate": reconcile (e.g. list
472
+ * recent workflows) before re-running rather than assuming nothing happened.
473
+ */
474
+ readonly workflowId?: string;
475
+ constructor(message: string, workflowId?: string);
476
+ }
477
+ /**
478
+ * A `mapEach` fan-out timed out mid-batch — the deadline elapsed either while a
479
+ * child was still running (the common case) or cleanly between child runs. The
480
+ * parent and some children have ALREADY completed, so re-running the whole batch
481
+ * re-does finished work. This carries their ids so the caller can poll them (via
482
+ * `client.getWorkflowStatus` / `getWorkflowDownloads`) to recover the finished
483
+ * work and re-run ONLY the children that were never created.
484
+ *
485
+ * Subclasses {@link GislTimeoutError}, so an existing
486
+ * `catch (e) { if (e instanceof GislTimeoutError) … }` still catches it. The
487
+ * inherited `workflowId` carries the IN-FLIGHT child — the one that was running
488
+ * when the deadline elapsed (a child's own timeout, the common path) — or stays
489
+ * `undefined` when the deadline elapsed cleanly BETWEEN children (no in-flight
490
+ * child). To recover, poll `workflowId` (if set) + {@link parentWorkflowId} +
491
+ * {@link completedWorkflowIds}, then re-run only the children that never started.
492
+ *
493
+ * NOTE on double-charge: the server-side create-dedupe (DSxwCetg) is what
494
+ * prevents a byte-identical child re-create from settling a SECOND charge within
495
+ * the dedup window; this error's job is efficient RECOVERY (skip the completed
496
+ * work) + defense-in-depth, not the sole charge guard.
497
+ */
498
+ export declare class GislFanOutTimeoutError extends GislTimeoutError {
499
+ /** The child workflows that completed before the deadline elapsed. */
500
+ readonly completedWorkflowIds: readonly string[];
501
+ /** The parent workflow, which ran to completion before the fan-out began. */
502
+ readonly parentWorkflowId?: string;
503
+ constructor(message: string, opts: {
504
+ completedWorkflowIds: readonly string[];
505
+ parentWorkflowId?: string;
506
+ /**
507
+ * The in-flight child that timed out mid-run (its own deadline elapsed);
508
+ * `undefined` for a clean between-children timeout with no child running.
509
+ */
510
+ workflowId?: string;
511
+ /** The underlying child {@link GislTimeoutError}, preserved for chaining. */
512
+ cause?: unknown;
513
+ });
415
514
  }
416
515
  /**
417
516
  * Transport-level failure: the underlying `fetch` (or other transport) could
package/dist/errors.js CHANGED
@@ -145,6 +145,55 @@ export class GislBalanceExhaustedError extends GislApiError {
145
145
  this.name = 'GislBalanceExhaustedError';
146
146
  }
147
147
  }
148
+ /**
149
+ * `429` on `POST /api/workflows` when the caller already holds the maximum
150
+ * number of concurrent in-flight long-form (Fargate) workflows their tier
151
+ * permits (Pro 2 / Max 5; Enterprise uncapped). DISTINCT from an infrastructure
152
+ * rate-limit `429`: it carries the machine code `LONG_FORM_CONCURRENCY_LIMIT_EXCEEDED`
153
+ * and a `links.upgrade` deep link, and has **no `Retry-After`** — the limit clears
154
+ * when an in-flight long-form workflow finishes, not on a timer. A generic infra
155
+ * rate-limit `429` (no matching code) surfaces as the base {@link GislApiError}
156
+ * instead, where {@link GislApiError.retryAfterSeconds} applies.
157
+ *
158
+ * Dispatched on the `error` CODE, not `error_type` (the envelope carries none).
159
+ *
160
+ * @example
161
+ * try {
162
+ * await client.createWorkflow({ jobs });
163
+ * } catch (e) {
164
+ * if (e instanceof GislLongFormConcurrencyError) {
165
+ * showUpgradeCta(e.upgradeUrl); // wait on completion or upgrade — do NOT back off
166
+ * }
167
+ * throw e;
168
+ * }
169
+ */
170
+ export class GislLongFormConcurrencyError extends GislApiError {
171
+ constructor(statusCode, errorMessage, payload, path, extra) {
172
+ super(statusCode, errorMessage, path, undefined, buildOptionsWithPayload(payload, extra));
173
+ this.name = 'GislLongFormConcurrencyError';
174
+ }
175
+ /**
176
+ * ALWAYS `false`, overriding the base 429-implies-retryable heuristic
177
+ * (UO1xYecu). This 429 is not a rate limit: it carries no `Retry-After` and
178
+ * clears only when an in-flight long-form workflow finishes, so a back-off
179
+ * retries into a wall that no amount of waiting-then-retrying opens. The base
180
+ * accessor reported `true` purely from the status, contradicting this class's
181
+ * own documented handling ("wait on completion or upgrade — do NOT back off")
182
+ * and instructing the one recovery that cannot work.
183
+ *
184
+ * Overridden per-class rather than via a code table because this is the only
185
+ * such code today; the general fix — an explicit taxonomy verdict outranking
186
+ * the status heuristic — arrives with the `error-taxonomy.yaml` `retryable`
187
+ * enum (contracts `plwcAqBr`), tracked on UO1xYecu.
188
+ */
189
+ get retryable() {
190
+ return false;
191
+ }
192
+ /** The pricing / upgrade deep link (`links.upgrade`), or `undefined` when absent. */
193
+ get upgradeUrl() {
194
+ return this.payload.links?.upgrade;
195
+ }
196
+ }
148
197
  export class GislTierRestrictedError extends GislApiError {
149
198
  constructor(statusCode, errorMessage, payload, path, extra) {
150
199
  super(statusCode, errorMessage, path, undefined, buildOptionsWithPayload(payload, extra));
@@ -447,9 +496,67 @@ export class GislBundleAlreadyArchivedError extends GislConfigError {
447
496
  }
448
497
  }
449
498
  export class GislTimeoutError extends GislError {
450
- constructor(message) {
499
+ /**
500
+ * The workflow this timeout is scoped to, when the SDK knows it. Set on a
501
+ * timed-out `run()` / `wait()` / poll / download once the workflow has been
502
+ * created: a timeout does NOT mean the work failed — the server keeps
503
+ * processing, so poll `client.getWorkflowStatus(workflowId)` /
504
+ * `getWorkflowDownloads(workflowId)` to recover a result that completed after
505
+ * the deadline, instead of re-running (a re-run re-uploads and, for
506
+ * authenticated callers, settles a SECOND charge for the same deliverable).
507
+ *
508
+ * `undefined` when the SDK has no id to offer. That is NOT a guarantee that
509
+ * nothing was created or charged: it covers both the safe case (an upload /
510
+ * probe timeout before any workflow existed) AND the AMBIGUOUS case (the
511
+ * `POST /api/workflows` request itself timed out — the server may have
512
+ * created and charged the workflow before its response was lost). Treat an
513
+ * absent id as "cannot auto-recover", not "clean slate": reconcile (e.g. list
514
+ * recent workflows) before re-running rather than assuming nothing happened.
515
+ */
516
+ workflowId;
517
+ constructor(message, workflowId) {
451
518
  super(message);
452
519
  this.name = 'GislTimeoutError';
520
+ // Normalise an empty id to "absent" — an empty string is not a usable
521
+ // recovery handle (some throw sites derive the id as `… ?? ''`).
522
+ this.workflowId = workflowId === '' ? undefined : workflowId;
523
+ }
524
+ }
525
+ /**
526
+ * A `mapEach` fan-out timed out mid-batch — the deadline elapsed either while a
527
+ * child was still running (the common case) or cleanly between child runs. The
528
+ * parent and some children have ALREADY completed, so re-running the whole batch
529
+ * re-does finished work. This carries their ids so the caller can poll them (via
530
+ * `client.getWorkflowStatus` / `getWorkflowDownloads`) to recover the finished
531
+ * work and re-run ONLY the children that were never created.
532
+ *
533
+ * Subclasses {@link GislTimeoutError}, so an existing
534
+ * `catch (e) { if (e instanceof GislTimeoutError) … }` still catches it. The
535
+ * inherited `workflowId` carries the IN-FLIGHT child — the one that was running
536
+ * when the deadline elapsed (a child's own timeout, the common path) — or stays
537
+ * `undefined` when the deadline elapsed cleanly BETWEEN children (no in-flight
538
+ * child). To recover, poll `workflowId` (if set) + {@link parentWorkflowId} +
539
+ * {@link completedWorkflowIds}, then re-run only the children that never started.
540
+ *
541
+ * NOTE on double-charge: the server-side create-dedupe (DSxwCetg) is what
542
+ * prevents a byte-identical child re-create from settling a SECOND charge within
543
+ * the dedup window; this error's job is efficient RECOVERY (skip the completed
544
+ * work) + defense-in-depth, not the sole charge guard.
545
+ */
546
+ export class GislFanOutTimeoutError extends GislTimeoutError {
547
+ /** The child workflows that completed before the deadline elapsed. */
548
+ completedWorkflowIds;
549
+ /** The parent workflow, which ran to completion before the fan-out began. */
550
+ parentWorkflowId;
551
+ constructor(message, opts) {
552
+ // The inherited workflowId is the in-flight child (or undefined between children).
553
+ super(message, opts.workflowId);
554
+ this.name = 'GislFanOutTimeoutError';
555
+ this.completedWorkflowIds = [...opts.completedWorkflowIds];
556
+ this.parentWorkflowId = opts.parentWorkflowId === '' ? undefined : opts.parentWorkflowId;
557
+ if (opts.cause !== undefined) {
558
+ this.cause = opts.cause;
559
+ }
453
560
  }
454
561
  }
455
562
  /**