@giveitsmaller/sdk 0.16.0 → 0.18.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.
@@ -0,0 +1,143 @@
1
+ import { convertMetadata, thumbnailMetadata, textWatermarkMetadata, imageWatermarkMetadata, videoWatermarkMetadata, } from '@giveitsmaller/contracts/operations';
2
+ import { GislConfigError } from '../errors.js';
3
+ import { VERB_OPTION_KEYS } from './option_types.js';
4
+ /**
5
+ * Eager, synchronous, PRE-UPLOAD option-key validation for the ergonomic verbs
6
+ * (card Dhje3Faq). The file-first builders accept verb options as untyped bags;
7
+ * a user typo (`{ quaity: 80 }`) would otherwise flow to the server and 422.
8
+ * This module rejects unknown keys at the verb call — before any upload —
9
+ * mirroring the existing `compress()` optimize-check and the watermark eager
10
+ * gate. The allowed key set is read from the generated `OperationMetadata`
11
+ * sidecars (the same contract-anchored source the wire-key conformance guard
12
+ * uses), so it can never silently drift from the contract.
13
+ *
14
+ * SCOPE: `convert` / `thumbnail` / `textWatermark` / `watermark` only. `compress`
15
+ * is deliberately EXCLUDED — its bag legitimately carries SDK-only keys
16
+ * (`optimize`, `presetOverrides`) and camelCase resolver aliases (`targetSize`,
17
+ * `outputFormat`) that are not `operationOptionKeys(compressMetadata)`; it has its
18
+ * own `unknown_field` validation through the preset resolver.
19
+ *
20
+ * Mirrored by the PHP `OptionValidation` helper — keep the two in lockstep.
21
+ */
22
+ /**
23
+ * OPERATION-LEVEL contract option keys (the keys valid in `OperationDef.options`):
24
+ * the union of every mime group's `options` plus `direct_options` for
25
+ * media-agnostic ops. Deliberately EXCLUDES `per_input_options` (valid only on a
26
+ * merge input). Promoted from the wire-key conformance guard for runtime reuse.
27
+ */
28
+ export function operationOptionKeys(metadata) {
29
+ const keys = new Set();
30
+ for (const group of Object.values(metadata.mime_groups)) {
31
+ for (const k of Object.keys(group.options))
32
+ keys.add(k);
33
+ }
34
+ for (const k of Object.keys(metadata.direct_options ?? {}))
35
+ keys.add(k);
36
+ return keys;
37
+ }
38
+ function union(...sets) {
39
+ const out = new Set();
40
+ for (const set of sets)
41
+ for (const k of set)
42
+ out.add(k);
43
+ return out;
44
+ }
45
+ /**
46
+ * The contract option-key set per validated verb (the GENERIC allowed set).
47
+ * `watermark` is the UNION of `image_watermark` + `video_watermark` because the
48
+ * base media may be undetectable at the `.watermark()` call; the existing
49
+ * media-routing gate still rejects the wrong route by media.
50
+ */
51
+ const ALLOWED_KEYS = {
52
+ convert: operationOptionKeys(convertMetadata),
53
+ thumbnail: operationOptionKeys(thumbnailMetadata),
54
+ textWatermark: operationOptionKeys(textWatermarkMetadata),
55
+ watermark: union(operationOptionKeys(imageWatermarkMetadata), operationOptionKeys(videoWatermarkMetadata)),
56
+ // `output` is the image Output facade — its allowed keys are the UNION of every
57
+ // image route's honored+planned options (the image-output-routes projection),
58
+ // INCLUDING `output_format` (in every cell's honored set) which — like `convert`
59
+ // — is in the allowed set but rejected first by the positional-owned guard. This
60
+ // is the COARSE static gate (reject keys no image route ever honors, e.g. a video
61
+ // `crf`); the precise per-route honored/planned narrowing happens in the
62
+ // `output()` lowering (`resolveOutputRoute`). Pinned to the projection union by
63
+ // the output-route conformance test.
64
+ output: new Set([...VERB_OPTION_KEYS.output, 'output_format']),
65
+ };
66
+ /**
67
+ * Keys a verb OWNS via a positional argument: a user must not also supply them
68
+ * in the options bag (they would be silently overridden by the positional). The
69
+ * guard runs BEFORE the generic check so these get a specific, actionable
70
+ * message rather than the generic "unknown option" one. `format` is an SDK alias
71
+ * for the positional (not a contract key) and is owned too.
72
+ */
73
+ const POSITIONAL_OWNED = {
74
+ convert: ['output_format', 'format'],
75
+ textWatermark: ['text'],
76
+ // `output(format, …)` sets the target format via its first argument; the wire
77
+ // key `output_format` and the SDK alias `format` must not be supplied in the bag.
78
+ output: ['output_format', 'format'],
79
+ };
80
+ /** Accessor for the conformance guard (pins these sets to the contract metadata). */
81
+ export function allowedKeysFor(verb) {
82
+ return ALLOWED_KEYS[verb];
83
+ }
84
+ /**
85
+ * Validate a USER-supplied options bag for an ergonomic verb. Throws
86
+ * {@link GislConfigError} (reason `unknown_field`) synchronously, BEFORE any
87
+ * upload or wire-key injection. Call this at the TOP of every verb body, before
88
+ * the `format`-drop / `output_format` / `text` injection.
89
+ *
90
+ * @throws {GislConfigError} reason `unknown_field` when the bag carries a key the
91
+ * verb owns via a positional argument, or a key absent from the op's contract
92
+ * option set.
93
+ */
94
+ export function validateVerbOptions(verb, options) {
95
+ // The typed signatures require an options object, but an untyped JS caller can
96
+ // still omit it (e.g. `thumbnail()` — which dropped its `= {}` default when
97
+ // width/height became required). A nullish bag has no keys to reject; the
98
+ // separate `assertThumbnailDimensions` then reports the missing dimensions as a
99
+ // clean GislConfigError rather than a raw TypeError.
100
+ if (options === null || options === undefined)
101
+ return;
102
+ const owned = POSITIONAL_OWNED[verb];
103
+ if (owned !== undefined) {
104
+ for (const key of owned) {
105
+ if (Object.prototype.hasOwnProperty.call(options, key)) {
106
+ const arg = verb === 'textWatermark' ? 'text' : 'output format';
107
+ throw new GislConfigError(`${verb}() sets the ${arg} via its first argument; remove '${key}' from the options bag.`, { reason: 'unknown_field', conflictingFields: [key] });
108
+ }
109
+ }
110
+ }
111
+ const allowed = ALLOWED_KEYS[verb];
112
+ for (const key of Object.keys(options)) {
113
+ if (!allowed.has(key)) {
114
+ throw new GislConfigError(`${verb}: unknown option '${key}'. Valid options: ${[...allowed].sort().join(', ')}.`, { reason: 'unknown_field', conflictingFields: [key] });
115
+ }
116
+ }
117
+ }
118
+ /**
119
+ * Assert thumbnail `width` AND `height` are both present and non-nullish (the
120
+ * contract marks both `required` for image/video/document). The typed signature
121
+ * already enforces this at compile time; this RUNTIME guard catches JS callers and
122
+ * an explicit `undefined`/`null` BEFORE upload. Rejecting `null` (not just
123
+ * `undefined`) keeps TS in lockstep with the PHP `assertThumbnailDimensions`, which
124
+ * must reject `null` because PHP drops null values pre-lower — so a `null` dimension
125
+ * is a pre-upload error in BOTH languages, never a wire `null` that 422s. Mirrored
126
+ * in PHP.
127
+ *
128
+ * @throws {GislConfigError} reason `missing_required_field` naming the absent
129
+ * dimension(s) in `conflictingFields`.
130
+ */
131
+ export function assertThumbnailDimensions(options) {
132
+ // Null-safe so an untyped JS `thumbnail()` (no args, no default) reports both
133
+ // dimensions missing as a clean GislConfigError instead of a raw TypeError.
134
+ const o = options ?? {};
135
+ const missing = [];
136
+ if (o.width === undefined || o.width === null)
137
+ missing.push('width');
138
+ if (o.height === undefined || o.height === null)
139
+ missing.push('height');
140
+ if (missing.length > 0) {
141
+ throw new GislConfigError(`thumbnail requires both width and height (the contract marks both required); missing: ${missing.join(', ')}.`, { reason: 'missing_required_field', conflictingFields: missing });
142
+ }
143
+ }
@@ -9,7 +9,7 @@ import { type PresetDefaults, type PresetMedia, type PresetOp } from './presets/
9
9
  * construction. Previously a hand-typed literal that the v2.73.0 regen had to
10
10
  * bump manually (yREs0srv).
11
11
  */
12
- export declare const PRESET_VERSION: "1.2";
12
+ export declare const PRESET_VERSION: "1.4";
13
13
  /**
14
14
  * Inputs to {@link resolveCompressOptions}. `media` selects which leaf
15
15
  * DTO drives sdkDefault + clientDefault lookups + invalid-combo
@@ -21,8 +21,8 @@
21
21
  // `quality: 100`, `codec: 'h264'`, …).
22
22
  //
23
23
  // Validation runs AFTER merge so post-merge-only invariants
24
- // (targetSize + codec, Lossless + quality) catch combinations where
25
- // e.g. the explicit knob and a layered default disagree. Resolver
24
+ // (e.g. targetSize + codec on video) catch combinations where
25
+ // the explicit knob and a layered default disagree. Resolver
26
26
  // throws `GislConfigError` BEFORE any network round-trip, with a
27
27
  // `resolvedSnapshot` showing the would-have-been-sent payload.
28
28
  //
@@ -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, } from './presets/index.js';
40
+ import { ImageCompressPresetOptions, AudioCompressPresetOptions, VideoCompressPresetOptions, DocumentPdfCompressPresetOptions, 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
@@ -53,18 +53,16 @@ export const PRESET_VERSION = GENERATED_PRESET_VERSION;
53
53
  //
54
54
  // Lifted from docs/plans/sdk-ergonomics/plan.md §11a. Maps camelCase
55
55
  // ergonomic-DTO field names to their snake_case wire counterparts.
56
- // Fields whose ergonomic name IS the wire name (`mode`, `quality`,
57
- // `codec`, …) are NOT in this map — `applyAlias` returns them
58
- // unchanged. A generic snake-case regex would mistranslate names like
59
- // `iccProfile` to `i_c_c_profile`; the declarative map is the only
60
- // safe path.
56
+ // Fields whose ergonomic name IS the wire name (`quality`, `codec`,
57
+ // …) are NOT in this map — `applyAlias` returns them unchanged. A
58
+ // generic snake-case regex would mistranslate acronym/camel names
59
+ // (e.g. an `outputFormat` `output_format` rename or an acronym like
60
+ // the former `iccProfile`); the declarative map is the only safe path.
61
61
  const WIRE_ALIASES = Object.freeze({
62
- iccProfile: 'icc_profile',
63
62
  outputFormat: 'output_format',
64
63
  sampleRate: 'sample_rate',
65
64
  audioCodec: 'audio_codec',
66
65
  audioBitrate: 'audio_bitrate',
67
- flattenForms: 'flatten_forms',
68
66
  imageQuality: 'image_quality',
69
67
  stripMacros: 'strip_macros',
70
68
  stripHiddenData: 'strip_hidden_data',
@@ -201,10 +199,10 @@ function presetDefaultsCellRecord(defaults, media, op, optimize) {
201
199
  if (op !== 'compress')
202
200
  return undefined;
203
201
  // Each overload returns `<Specific>PresetOptions | undefined`. We
204
- // erase the per-media type at runtime by spreading the instance
205
- // into a plain Record. `cellFor` returns `undefined` when no delta
206
- // was registered for the tuple — the resolver treats that the same
207
- // as "this layer did not participate."
202
+ // erase the per-media type at runtime by reducing the instance to a
203
+ // plain Record. `cellFor` returns `undefined` when no delta was
204
+ // registered for the tuple — the resolver treats that the same as
205
+ // "this layer did not participate."
208
206
  let cell;
209
207
  switch (media) {
210
208
  case 'image':
@@ -231,7 +229,16 @@ function presetDefaultsCellRecord(defaults, media, op, optimize) {
231
229
  }
232
230
  if (cell === undefined)
233
231
  return undefined;
234
- return { ...cell };
232
+ // Reduce to a SPARSE camelCase record, dropping undefined-valued keys.
233
+ // The leaf DTO's field DECLARATIONS define every field as an
234
+ // own-enumerable `undefined` property under `useDefineForClassFields`
235
+ // (ES2022) even when the ctor skipped the assignment — a naive spread
236
+ // would carry those `undefined`s into the presetConfigHash input,
237
+ // diverging from PHP's sparse `leafToRecord` (SVQcoR1K). `mergeLayer`
238
+ // already ignores `undefined`, so this only affects the hash path.
239
+ // Reuses the same helper `PresetDefaults.merge` uses for this exact
240
+ // `useDefineForClassFields` problem.
241
+ return definedFieldsOf(cell);
235
242
  }
236
243
  // ---------------------------------------------------------------------------
237
244
  // presetOverrides type-mismatch detection
@@ -243,10 +250,10 @@ function presetDefaultsCellRecord(defaults, media, op, optimize) {
243
250
  // for clearly-typed plain objects whose key set only intersects with a
244
251
  // non-matching media.
245
252
  const MEDIA_FIELDS = Object.freeze({
246
- image: new Set(['mode', 'quality', 'metadata', 'iccProfile', 'progressive', 'outputFormat']),
253
+ image: new Set(['quality', 'metadata', 'outputFormat']),
247
254
  audio: new Set(['bitrate', 'channels', 'sampleRate', 'normalize']),
248
255
  video: new Set(['codec', 'targetSize', 'crf', 'preset', 'width', 'height', 'fit', 'fps', 'faststart', 'audioCodec', 'audioBitrate']),
249
- document_pdf: new Set(['profile', 'colorspace', 'flattenForms']),
256
+ document_pdf: new Set(['profile', 'grayscale']),
250
257
  document_office: new Set(['imageQuality', 'stripMacros', 'stripHiddenData', 'stripUnusedFonts']),
251
258
  document_odf: new Set(['imageQuality', 'stripMetadata', 'stripUnusedStyles']),
252
259
  document_epub: new Set(['imageQuality', 'fontSubsetting', 'stripUnusedCss']),
@@ -312,10 +319,10 @@ function mergeLayer(acc, layer, source) {
312
319
  // can pin this hand-maintained allowlist to the generated contract metadata: every
313
320
  // field the resolver may emit MUST be a real contract option key for `compress`.
314
321
  export const KNOWN_WIRE_FIELDS = Object.freeze({
315
- image: new Set(['mode', 'quality', 'metadata', 'icc_profile', 'progressive', 'output_format']),
322
+ image: new Set(['quality', 'metadata', 'output_format']),
316
323
  audio: new Set(['bitrate', 'channels', 'sample_rate', 'normalize', 'trim_start', 'trim_end']),
317
324
  video: new Set(['codec', 'encoding_mode', 'crf', 'target_size_bytes', 'preset', 'width', 'height', 'fit', 'fps', 'faststart', 'audio_codec', 'audio_bitrate', 'trim_start', 'trim_end']),
318
- document_pdf: new Set(['profile', 'colorspace', 'pages', 'flatten_forms']),
325
+ document_pdf: new Set(['profile', 'grayscale']),
319
326
  document_office: new Set(['image_quality', 'strip_macros', 'strip_hidden_data', 'strip_unused_fonts']),
320
327
  document_odf: new Set(['image_quality', 'strip_metadata', 'strip_unused_styles']),
321
328
  document_epub: new Set(['image_quality', 'font_subsetting', 'strip_unused_css']),
@@ -337,19 +344,6 @@ function validateMerged(media, merged, explicitKeys, winners) {
337
344
  });
338
345
  }
339
346
  }
340
- // Image: `mode: Lossless` + `quality` set is invalid per the wire
341
- // contract (`depends_on: { mode: lossy }`). Runs on post-merge so a
342
- // caller passing explicit `quality` and inheriting `mode=Lossless`
343
- // from a client preset is caught.
344
- if (media === 'image' && merged.mode === 'lossless' && merged.quality !== undefined) {
345
- const snapshot = { ...merged };
346
- throw new GislConfigError(`Image compress: 'quality' is ignored when 'mode' is Lossless — passing both is a configuration bug.`, {
347
- reason: 'missing_dependency',
348
- conflictingFields: ['quality', 'mode'],
349
- resolvedSnapshot: Object.freeze(snapshot),
350
- suggestion: "Either drop 'quality' for lossless output, or set 'mode' to Lossy.",
351
- });
352
- }
353
347
  // Video: targetSize-derived encoding_mode='target_size' is only
354
348
  // valid for H264 today. Catch the combination post-merge — explicit
355
349
  // codec overrides a layered default and either resolution must end
@@ -408,7 +402,15 @@ function canonicalJson(value) {
408
402
  return '[' + value.map(canonicalJson).join(',') + ']';
409
403
  }
410
404
  const record = value;
411
- const keys = Object.keys(record).sort();
405
+ // Skip undefined-valued keys (mirror `JSON.stringify` object semantics,
406
+ // which omit undefined members). Without this, `canonicalJson(undefined)`
407
+ // would serialise the literal token `undefined` into the hash input — a
408
+ // latent foot-gun. The registered-cell records are already sparse (see
409
+ // `presetDefaultsCellRecord`), so this is defense-in-depth; the
410
+ // override-path anchors carry no undefined and are unaffected (SVQcoR1K).
411
+ const keys = Object.keys(record)
412
+ .filter((k) => record[k] !== undefined)
413
+ .sort();
412
414
  return ('{' +
413
415
  keys
414
416
  .map((k) => JSON.stringify(k) + ':' + canonicalJson(record[k]))
@@ -1,13 +1,11 @@
1
- import { PdfProfile, PdfColorspace, OptimizeFor } from '../../generated/sdk_spec/enums.js';
1
+ import { PdfProfile, OptimizeFor } from '../../generated/sdk_spec/enums.js';
2
2
  export interface DocumentPdfCompressPresetOptionsInput {
3
3
  readonly profile?: PdfProfile;
4
- readonly colorspace?: PdfColorspace;
5
- readonly flattenForms?: boolean;
4
+ readonly grayscale?: boolean;
6
5
  }
7
6
  export declare class DocumentPdfCompressPresetOptions {
8
7
  readonly profile?: PdfProfile;
9
- readonly colorspace?: PdfColorspace;
10
- readonly flattenForms?: boolean;
8
+ readonly grayscale?: boolean;
11
9
  private constructor();
12
10
  static from(input: DocumentPdfCompressPresetOptionsInput): DocumentPdfCompressPresetOptions;
13
11
  static shippedDefaultsFor(level: OptimizeFor): DocumentPdfCompressPresetOptions;
@@ -1,20 +1,20 @@
1
1
  // T4a — DocumentPdfCompressPresetOptions leaf DTO.
2
2
  //
3
- // Field set per ticket VhIj4S7T: PDF = 3 fields (profile, colorspace, flattenForms).
4
- // Deliberately excluded: `pages` (per-call content selection).
3
+ // Field set (2): profile, grayscale the worker-honored stable PDF controls
4
+ // (contracts v2.96.0 Acrobat-PDF realignment Lw1LseYr). The earlier
5
+ // {profile, colorspace, flattenForms} set was retired: colorspace + flatten_forms
6
+ // are `planned` (not read by the worker) so presets never emit them, and
7
+ // `image_dpi` + `pages` are per-call knobs, not preset cells.
5
8
  import { shippedDefaultsFor as f3ShippedDefaultsFor } from '../../generated/sdk_spec/presets.js';
6
9
  import { translateEnum } from './_translate.js';
7
10
  export class DocumentPdfCompressPresetOptions {
8
11
  profile;
9
- colorspace;
10
- flattenForms;
12
+ grayscale;
11
13
  constructor(input) {
12
14
  if (input.profile !== undefined)
13
15
  this.profile = input.profile;
14
- if (input.colorspace !== undefined)
15
- this.colorspace = input.colorspace;
16
- if (input.flattenForms !== undefined)
17
- this.flattenForms = input.flattenForms;
16
+ if (input.grayscale !== undefined)
17
+ this.grayscale = input.grayscale;
18
18
  Object.freeze(this);
19
19
  }
20
20
  static from(input) {
@@ -26,10 +26,8 @@ export class DocumentPdfCompressPresetOptions {
26
26
  const mut = input;
27
27
  if ('profile' in cell)
28
28
  mut.profile = translateEnum('PdfProfile', cell.profile);
29
- if ('colorspace' in cell)
30
- mut.colorspace = translateEnum('PdfColorspace', cell.colorspace);
31
- if ('flattenForms' in cell)
32
- mut.flattenForms = cell.flattenForms;
29
+ if ('grayscale' in cell)
30
+ mut.grayscale = cell.grayscale;
33
31
  return new DocumentPdfCompressPresetOptions(input);
34
32
  }
35
33
  }
@@ -1,18 +1,12 @@
1
- import { ImageMode, ImageMetadataPolicy, IccProfilePolicy, ImageFormat, OptimizeFor } from '../../generated/sdk_spec/enums.js';
1
+ import { ImageMetadataPolicy, ImageFormat, OptimizeFor } from '../../generated/sdk_spec/enums.js';
2
2
  export interface ImageCompressPresetOptionsInput {
3
- readonly mode?: ImageMode;
4
3
  readonly quality?: number;
5
4
  readonly metadata?: ImageMetadataPolicy;
6
- readonly iccProfile?: IccProfilePolicy;
7
- readonly progressive?: boolean;
8
5
  readonly outputFormat?: ImageFormat;
9
6
  }
10
7
  export declare class ImageCompressPresetOptions {
11
- readonly mode?: ImageMode;
12
8
  readonly quality?: number;
13
9
  readonly metadata?: ImageMetadataPolicy;
14
- readonly iccProfile?: IccProfilePolicy;
15
- readonly progressive?: boolean;
16
10
  readonly outputFormat?: ImageFormat;
17
11
  private constructor();
18
12
  /**
@@ -26,10 +20,9 @@ export declare class ImageCompressPresetOptions {
26
20
  * the given level. Reads the F3 PRESETS matrix and translates member
27
21
  * names to wire backing values.
28
22
  *
29
- * Note for OptimizeFor.Quality: the F3 PRESETS cell deliberately
30
- * omits `quality` because the contract has `depends_on: { mode: lossy }`
31
- * on the quality field — under `mode: Lossless` the API ignores
32
- * `quality`, so shipping a default would mislead callers.
23
+ * Since the v2.80.0 honesty pass the worker is lossy-only, so every
24
+ * level ships a concrete `quality` (Size 65 / Balanced 80 / Quality 92),
25
+ * `metadata: All`, and `outputFormat: Original`.
33
26
  */
34
27
  static shippedDefaultsFor(level: OptimizeFor): ImageCompressPresetOptions;
35
28
  }
@@ -7,32 +7,26 @@
7
7
  // values ARE the wire backing values — so the leaf DTO is wire-compatible
8
8
  // once the resolver (T4b) snake_cases the property names.
9
9
  //
10
- // Field set (6) per EsD1hs5u / contracts v2.60.0: image =
11
- // (mode, quality, metadata, iccProfile, progressive, outputFormat).
12
- // `width`/`height`/`fit`/`autoOrient` were REMOVED — the image-compress
13
- // worker never resized (resize-fit lives on thumbnail/convert; video keeps
14
- // its own fit). Trim / per-call knobs are deliberately excluded — they
15
- // belong on the per-call argument shape, not the preset cell.
10
+ // Field set (3) per contracts v2.80.0 compress.image honesty pass (Option B,
11
+ // lossy-only): image = (quality, metadata, outputFormat).
12
+ // `mode` + `iccProfile` were REMOVED — the worker is lossy-only and always
13
+ // strips metadata, so advertising a lossless mode or ICC-profile policy was
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.
16
19
  import { shippedDefaultsFor as f3ShippedDefaultsFor } from '../../generated/sdk_spec/presets.js';
17
20
  import { translateEnum } from './_translate.js';
18
21
  export class ImageCompressPresetOptions {
19
- mode;
20
22
  quality;
21
23
  metadata;
22
- iccProfile;
23
- progressive;
24
24
  outputFormat;
25
25
  constructor(input) {
26
- if (input.mode !== undefined)
27
- this.mode = input.mode;
28
26
  if (input.quality !== undefined)
29
27
  this.quality = input.quality;
30
28
  if (input.metadata !== undefined)
31
29
  this.metadata = input.metadata;
32
- if (input.iccProfile !== undefined)
33
- this.iccProfile = input.iccProfile;
34
- if (input.progressive !== undefined)
35
- this.progressive = input.progressive;
36
30
  if (input.outputFormat !== undefined)
37
31
  this.outputFormat = input.outputFormat;
38
32
  Object.freeze(this);
@@ -50,25 +44,18 @@ export class ImageCompressPresetOptions {
50
44
  * the given level. Reads the F3 PRESETS matrix and translates member
51
45
  * names to wire backing values.
52
46
  *
53
- * Note for OptimizeFor.Quality: the F3 PRESETS cell deliberately
54
- * omits `quality` because the contract has `depends_on: { mode: lossy }`
55
- * on the quality field — under `mode: Lossless` the API ignores
56
- * `quality`, so shipping a default would mislead callers.
47
+ * Since the v2.80.0 honesty pass the worker is lossy-only, so every
48
+ * level ships a concrete `quality` (Size 65 / Balanced 80 / Quality 92),
49
+ * `metadata: All`, and `outputFormat: Original`.
57
50
  */
58
51
  static shippedDefaultsFor(level) {
59
52
  const cell = f3ShippedDefaultsFor('image_compress', level);
60
53
  const input = {};
61
54
  const mut = input;
62
- if ('mode' in cell)
63
- mut.mode = translateEnum('ImageMode', cell.mode);
64
55
  if ('quality' in cell)
65
56
  mut.quality = cell.quality;
66
57
  if ('metadata' in cell)
67
58
  mut.metadata = translateEnum('ImageMetadataPolicy', cell.metadata);
68
- if ('iccProfile' in cell)
69
- mut.iccProfile = translateEnum('IccProfilePolicy', cell.iccProfile);
70
- if ('progressive' in cell)
71
- mut.progressive = cell.progressive;
72
59
  if ('outputFormat' in cell)
73
60
  mut.outputFormat = translateEnum('ImageFormat', cell.outputFormat);
74
61
  return new ImageCompressPresetOptions(input);
@@ -13,7 +13,7 @@ export { DocumentPdfCompressPresetOptions, type DocumentPdfCompressPresetOptions
13
13
  export { DocumentOfficeCompressPresetOptions, type DocumentOfficeCompressPresetOptionsInput, } from './document_office_compress.js';
14
14
  export { DocumentOdfCompressPresetOptions, type DocumentOdfCompressPresetOptionsInput, } from './document_odf_compress.js';
15
15
  export { DocumentEpubCompressPresetOptions, type DocumentEpubCompressPresetOptionsInput, } from './document_epub_compress.js';
16
- export { OptimizeFor, ImageMode, ImageMetadataPolicy, IccProfilePolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, PdfProfile, PdfColorspace, } from '../../generated/sdk_spec/enums.js';
16
+ export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, PdfProfile, PdfColorspace, } from '../../generated/sdk_spec/enums.js';
17
17
  /** Supported media×op pairs for preset cells in T4a. Compress-only. */
18
18
  export type PresetMedia = 'image' | 'audio' | 'video' | 'document_pdf' | 'document_office' | 'document_odf' | 'document_epub';
19
19
  export type PresetOp = 'compress';
@@ -23,6 +23,26 @@ export type PresetOp = 'compress';
23
23
  * narrows the return automatically via the overload set below.
24
24
  */
25
25
  export type AnyPresetOptions = ImageCompressPresetOptions | AudioCompressPresetOptions | VideoCompressPresetOptions | DocumentPdfCompressPresetOptions | DocumentOfficeCompressPresetOptions | DocumentOdfCompressPresetOptions | DocumentEpubCompressPresetOptions;
26
+ /**
27
+ * Per-cell field-merge: parent fields ⊕ child fields where defined.
28
+ * Re-construct the leaf DTO via the matching `<LeafClass>.from(merged)`
29
+ * call so the result is a freshly-frozen `*PresetOptions` instance —
30
+ * NOT a mutated reference into either input. Used by
31
+ * {@link PresetDefaults.merge} when both parent and child registered
32
+ * the same `(cellKey, level)` tuple.
33
+ *
34
+ * `definedFieldsOf` filters undefined values out of each instance
35
+ * BEFORE the merge: with TS `useDefineForClassFields` (the ES2022
36
+ * default), `readonly outputFormat?: ImageFormat` declarations initialise
37
+ * the field as an enumerable own property with value `undefined` BEFORE
38
+ * the ctor body runs. A naive `Object.assign({}, parent, child)`
39
+ * therefore lets child's `undefined` overwrite parent's defined value
40
+ * — caught by CI on PR #125 first run. Filter-then-spread restores
41
+ * the documented merge-not-replace semantics.
42
+ *
43
+ * @internal
44
+ */
45
+ export declare function definedFieldsOf<T extends object>(opts: T): Partial<Record<string, unknown>>;
26
46
  export declare class PresetDefaults {
27
47
  private readonly cells;
28
48
  private constructor();
@@ -41,7 +41,7 @@ export { DocumentOfficeCompressPresetOptions, } from './document_office_compress
41
41
  export { DocumentOdfCompressPresetOptions, } from './document_odf_compress.js';
42
42
  export { DocumentEpubCompressPresetOptions, } from './document_epub_compress.js';
43
43
  // Re-export ergonomic enums for callers (single canonical path).
44
- export { OptimizeFor, ImageMode, ImageMetadataPolicy, IccProfilePolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, PdfProfile, PdfColorspace, } from '../../generated/sdk_spec/enums.js';
44
+ export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, PdfProfile, PdfColorspace, } from '../../generated/sdk_spec/enums.js';
45
45
  function cellKeyOf(media, op) {
46
46
  return `${media}_${op}`;
47
47
  }
@@ -55,8 +55,8 @@ function cellKeyOf(media, op) {
55
55
  *
56
56
  * `definedFieldsOf` filters undefined values out of each instance
57
57
  * BEFORE the merge: with TS `useDefineForClassFields` (the ES2022
58
- * default), `readonly mode?: ImageMode` declarations initialise the
59
- * field as an enumerable own property with value `undefined` BEFORE
58
+ * default), `readonly outputFormat?: ImageFormat` declarations initialise
59
+ * the field as an enumerable own property with value `undefined` BEFORE
60
60
  * the ctor body runs. A naive `Object.assign({}, parent, child)`
61
61
  * therefore lets child's `undefined` overwrite parent's defined value
62
62
  * — caught by CI on PR #125 first run. Filter-then-spread restores
@@ -64,7 +64,7 @@ function cellKeyOf(media, op) {
64
64
  *
65
65
  * @internal
66
66
  */
67
- function definedFieldsOf(opts) {
67
+ export function definedFieldsOf(opts) {
68
68
  const out = {};
69
69
  for (const key of Object.keys(opts)) {
70
70
  const value = opts[key];
package/dist/errors.d.ts CHANGED
@@ -461,3 +461,28 @@ export declare class GislSinkError extends GislError {
461
461
  readonly reason: GislSinkErrorReason;
462
462
  });
463
463
  }
464
+ /**
465
+ * A terminal item failure in {@link RunResult.failed} — an input whose job did
466
+ * not reach `completed`. Stored in `ItemFailure.error` so a caller can branch on
467
+ * the failure reason WITHOUT string-parsing.
468
+ *
469
+ * - `state`: the terminal lifecycle state (`failed` / `expired` / `cancelled` /
470
+ * `partially_failed` / `paused_insufficient_credits`, or a per-job
471
+ * non-`completed` status).
472
+ * - `errorMessage` / `errorCode`: the human + machine fields read from the first
473
+ * failing operation (`OperationResponse.error_message` / `.error_code`). BOTH
474
+ * are absent for non-`failed` terminal states — cancel / expire / credit-pause
475
+ * carry only the bare `state`.
476
+ *
477
+ * `message` is `state` optionally suffixed `: errorMessage`, preserving the
478
+ * pre-typed string exactly (an empty-string `errorMessage` still adds the colon).
479
+ *
480
+ * Mirrors the PHP `Gisl\Sdk\Errors\GislItemFailedError`.
481
+ */
482
+ export declare class GislItemFailedError extends GislError {
483
+ readonly key: string | null;
484
+ readonly state: string;
485
+ readonly errorMessage?: string;
486
+ readonly errorCode?: string;
487
+ constructor(key: string | null, state: string, errorMessage?: string, errorCode?: string);
488
+ }
package/dist/errors.js CHANGED
@@ -503,3 +503,37 @@ export class GislSinkError extends GislError {
503
503
  this.reason = options.reason;
504
504
  }
505
505
  }
506
+ /**
507
+ * A terminal item failure in {@link RunResult.failed} — an input whose job did
508
+ * not reach `completed`. Stored in `ItemFailure.error` so a caller can branch on
509
+ * the failure reason WITHOUT string-parsing.
510
+ *
511
+ * - `state`: the terminal lifecycle state (`failed` / `expired` / `cancelled` /
512
+ * `partially_failed` / `paused_insufficient_credits`, or a per-job
513
+ * non-`completed` status).
514
+ * - `errorMessage` / `errorCode`: the human + machine fields read from the first
515
+ * failing operation (`OperationResponse.error_message` / `.error_code`). BOTH
516
+ * are absent for non-`failed` terminal states — cancel / expire / credit-pause
517
+ * carry only the bare `state`.
518
+ *
519
+ * `message` is `state` optionally suffixed `: errorMessage`, preserving the
520
+ * pre-typed string exactly (an empty-string `errorMessage` still adds the colon).
521
+ *
522
+ * Mirrors the PHP `Gisl\Sdk\Errors\GislItemFailedError`.
523
+ */
524
+ export class GislItemFailedError extends GislError {
525
+ key;
526
+ state;
527
+ errorMessage;
528
+ errorCode;
529
+ constructor(key, state, errorMessage, errorCode) {
530
+ super(state + (errorMessage !== undefined ? `: ${errorMessage}` : ''));
531
+ this.name = 'GislItemFailedError';
532
+ this.key = key;
533
+ this.state = state;
534
+ if (errorMessage !== undefined)
535
+ this.errorMessage = errorMessage;
536
+ if (errorCode !== undefined)
537
+ this.errorCode = errorCode;
538
+ }
539
+ }