@giveitsmaller/sdk 0.11.0 → 0.12.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/dist/builder.d.ts CHANGED
@@ -41,6 +41,20 @@ import type { PresetDefaults, PresetMedia } from './ergonomic/presets/index.js';
41
41
  * @internal — exported for tests + the preset resolver.
42
42
  */
43
43
  export declare function _detectCompressMedia(input: string | Blob): PresetMedia | undefined;
44
+ /**
45
+ * Best-effort classification of whether an audio input is LOSSLESS
46
+ * (flac/wav) vs lossy. The worker rejects `bitrate` on lossless audio
47
+ * (compress.yaml / contracts iakhSy3E), so the preset resolver uses this
48
+ * to drop the shipped-preset bitrate for clear-cut lossless inputs.
49
+ *
50
+ * Detection is filename/MIME only — it CANNOT probe the actual codec, so
51
+ * any ambiguous or unknown input classifies as lossy (keep bitrate). The
52
+ * worker stays authoritative: a user-supplied bitrate on a lossless file
53
+ * still reaches the wire and earns a deliberate 422.
54
+ *
55
+ * @internal — exported for tests + the preset resolver.
56
+ */
57
+ export declare function _detectAudioLossless(input: string | Blob): boolean;
44
58
  /**
45
59
  * Lightweight artifact reference passed to a `.mapEach(...)` fn. Mirrors
46
60
  * the subset of `Artifact` a fan-out callback can use to construct the
package/dist/builder.js CHANGED
@@ -103,6 +103,59 @@ export function _detectCompressMedia(input) {
103
103
  return 'document_office';
104
104
  return undefined;
105
105
  }
106
+ // LOSSLESS audio per the compress.yaml contract (contracts iakhSy3E) —
107
+ // flac/wav ONLY. Everything else audio (mp3/mpeg, aac, ogg, m4a/mp4, opus)
108
+ // and any unknown input is treated as lossy so the shipped-preset bitrate is
109
+ // kept. `aiff` is deliberately absent — not a contract audio format.
110
+ const LOSSLESS_AUDIO_MIMES = new Set([
111
+ 'audio/flac',
112
+ 'audio/x-flac',
113
+ 'audio/wav',
114
+ 'audio/x-wav',
115
+ 'audio/wave',
116
+ ]);
117
+ const LOSSLESS_AUDIO_EXTENSIONS = new Set(['flac', 'wav']);
118
+ /**
119
+ * Best-effort classification of whether an audio input is LOSSLESS
120
+ * (flac/wav) vs lossy. The worker rejects `bitrate` on lossless audio
121
+ * (compress.yaml / contracts iakhSy3E), so the preset resolver uses this
122
+ * to drop the shipped-preset bitrate for clear-cut lossless inputs.
123
+ *
124
+ * Detection is filename/MIME only — it CANNOT probe the actual codec, so
125
+ * any ambiguous or unknown input classifies as lossy (keep bitrate). The
126
+ * worker stays authoritative: a user-supplied bitrate on a lossless file
127
+ * still reaches the wire and earns a deliberate 422.
128
+ *
129
+ * @internal — exported for tests + the preset resolver.
130
+ */
131
+ export function _detectAudioLossless(input) {
132
+ let filename;
133
+ let mime;
134
+ if (typeof input === 'string') {
135
+ filename = input;
136
+ }
137
+ else {
138
+ mime = input.type !== '' ? input.type : undefined;
139
+ const named = input.name;
140
+ if (typeof named === 'string')
141
+ filename = named;
142
+ }
143
+ // MIME-first if a recognised audio MIME is present — Blob.type is canonical.
144
+ // Strip any MIME parameters (`audio/flac; codecs=flac`) before the exact-set
145
+ // lookup so a parameterised type still classifies as lossless — `media` is
146
+ // already `audio` via the prefix check, so a miss would wrongly keep the
147
+ // bitrate (codex 18b6b684).
148
+ if (mime !== undefined && mime.startsWith('audio/')) {
149
+ const bareMime = mime.split(';')[0].trim().toLowerCase();
150
+ return LOSSLESS_AUDIO_MIMES.has(bareMime);
151
+ }
152
+ if (filename === undefined)
153
+ return false;
154
+ const ext = filename.toLowerCase().split('.').pop();
155
+ if (ext === undefined)
156
+ return false;
157
+ return LOSSLESS_AUDIO_EXTENSIONS.has(ext);
158
+ }
106
159
  // ---------------------------------------------------------------------------
107
160
  // OperationBuilder
108
161
  // ---------------------------------------------------------------------------
@@ -177,6 +230,9 @@ export class OperationBuilder {
177
230
  op: 'compress',
178
231
  explicitOptions,
179
232
  };
233
+ if (media === 'audio') {
234
+ input.audioLossless = _detectAudioLossless(this.input);
235
+ }
180
236
  if (this.presetDefaults !== undefined) {
181
237
  input.presetDefaults = this.presetDefaults;
182
238
  }
@@ -36,6 +36,12 @@ export interface ResolveCompressOptionsInput {
36
36
  * layer.
37
37
  */
38
38
  readonly explicitOptions: Readonly<Record<string, unknown>>;
39
+ /**
40
+ * Classifier result from media detection. When `true` on audio, the
41
+ * shipped-preset (sdkDefault) bitrate is dropped — the worker rejects
42
+ * `bitrate` on lossless outputs (flac/wav — contracts iakhSy3E).
43
+ */
44
+ readonly audioLossless?: boolean;
39
45
  }
40
46
  /**
41
47
  * Output of {@link resolveCompressOptions}. `wireOptions` is the
@@ -432,7 +432,7 @@ function computePresetConfigHash(clientDefault, scopedDefault, callPresetOverrid
432
432
  * `optimize` unset ⇒ layer 1 contributes nothing; `resolvedOptions.preset = null`.
433
433
  */
434
434
  export function resolveCompressOptions(input) {
435
- const { media, op, presetDefaults, scopedPresetDefaults, presetOverrides, optimize, explicitOptions } = input;
435
+ const { media, op, presetDefaults, scopedPresetDefaults, presetOverrides, optimize, explicitOptions, audioLossless } = input;
436
436
  if (op !== 'compress') {
437
437
  throw new GislConfigError(`Preset resolution is only wired for compress operations today; got op='${op}'.`, { reason: 'unsupported_op' });
438
438
  }
@@ -510,6 +510,15 @@ export function resolveCompressOptions(input) {
510
510
  acc.winners.set('encoding_mode', crfSource);
511
511
  }
512
512
  }
513
+ // audio_compress bakes a bitrate (Size 96 / Balanced 192 / Quality 320); the
514
+ // worker rejects `bitrate` on lossless outputs (flac/wav — contracts iakhSy3E).
515
+ // Drop ONLY the shipped-preset (sdkDefault) bitrate for clear-cut lossless
516
+ // audio; any user-supplied bitrate (client/scoped default, per-call override
517
+ // or explicit) is left for the worker to reject — no silent-ignore.
518
+ if (media === 'audio' && audioLossless === true && acc.winners.get('bitrate') === 'sdkDefault') {
519
+ delete acc.merged.bitrate;
520
+ acc.winners.delete('bitrate');
521
+ }
513
522
  // 7. Validate the merged payload (post-merge — catches cross-layer
514
523
  // disagreements). May throw GislConfigError with resolvedSnapshot.
515
524
  const explicitWireKeys = new Set();
package/dist/errors.d.ts CHANGED
@@ -342,6 +342,18 @@ export declare class GislChainCardinalityMismatchError extends GislConfigError {
342
342
  readonly attemptedOperation: string;
343
343
  constructor(previousOperation: string, attemptedOperation: string);
344
344
  }
345
+ /**
346
+ * Thrown by `.bundle(...)` when the target builder's terminal job is already an
347
+ * `archive` op — double-bundle prevention (a builder that already produces an
348
+ * archive cannot be bundled again). No HTTP: raised during lowering, before any
349
+ * upload. Per the lowering spec
350
+ * (`docs/plans/sdk-ergonomics/lowering.md:484`, id `bundle_already_archived_error`).
351
+ * Dormant until `.bundle()` ships (wpHoJhuo) — the type lands here so that PR is
352
+ * a pure addition.
353
+ */
354
+ export declare class GislBundleAlreadyArchivedError extends GislConfigError {
355
+ constructor();
356
+ }
345
357
  export declare class GislTimeoutError extends GislError {
346
358
  constructor(message: string);
347
359
  }
package/dist/errors.js CHANGED
@@ -349,6 +349,22 @@ export class GislChainCardinalityMismatchError extends GislConfigError {
349
349
  this.attemptedOperation = attemptedOperation;
350
350
  }
351
351
  }
352
+ /**
353
+ * Thrown by `.bundle(...)` when the target builder's terminal job is already an
354
+ * `archive` op — double-bundle prevention (a builder that already produces an
355
+ * archive cannot be bundled again). No HTTP: raised during lowering, before any
356
+ * upload. Per the lowering spec
357
+ * (`docs/plans/sdk-ergonomics/lowering.md:484`, id `bundle_already_archived_error`).
358
+ * Dormant until `.bundle()` ships (wpHoJhuo) — the type lands here so that PR is
359
+ * a pure addition.
360
+ */
361
+ export class GislBundleAlreadyArchivedError extends GislConfigError {
362
+ constructor() {
363
+ super('This builder already produces an archive (bundle); .bundle() cannot be ' +
364
+ 'applied to an already-bundled builder.');
365
+ this.name = 'GislBundleAlreadyArchivedError';
366
+ }
367
+ }
352
368
  export class GislTimeoutError extends GislError {
353
369
  constructor(message) {
354
370
  super(message);
@@ -329,23 +329,31 @@ export declare class Recipe {
329
329
  /**
330
330
  * Reduce file size. `optimize` selects a per-media preset (resolved to
331
331
  * concrete wire fields at lower-time, exactly as `client.compress()` does).
332
+ * `options` carries the full per-op options bag (mirrors
333
+ * `client.compress(input, options)`); the explicit `optimize` param wins
334
+ * over any `optimize` key in the bag.
332
335
  */
333
- compress(optimize?: OptimizeFor): Recipe;
334
- /** Change format. `format` is lowered verbatim to the `format` wire option. */
335
- convert(format: string): Recipe;
336
+ compress(optimize?: OptimizeFor, options?: Record<string, unknown>): Recipe;
336
337
  /**
337
- * Generate a preview. Width and/or height in pixels; an omitted dimension is
338
- * dropped from the wire options (not sent as `undefined`).
338
+ * Change format. `format` is lowered verbatim to the `format` wire option;
339
+ * `options` carries any additional per-op convert options.
340
+ */
341
+ convert(format: string, options?: Record<string, unknown>): Recipe;
342
+ /**
343
+ * Generate a preview. Width and/or height in pixels; any additional per-op
344
+ * thumbnail options pass through. An omitted (`undefined`) value is dropped
345
+ * from the wire options (not sent as `undefined`).
339
346
  */
340
347
  thumbnail(options?: {
341
348
  width?: number;
342
349
  height?: number;
343
- }): Recipe;
350
+ } & Record<string, unknown>): Recipe;
344
351
  /**
345
352
  * Apply a text watermark. Single-input (the text is an option, not a
346
- * secondary file) — lowers to the `text_watermark` op with a `text` option.
353
+ * secondary file) — lowers to the `text_watermark` op with a `text` option;
354
+ * `options` carries any additional per-op watermark options.
347
355
  */
348
- textWatermark(text: string): Recipe;
356
+ textWatermark(text: string, options?: Record<string, unknown>): Recipe;
349
357
  /**
350
358
  * Lower this recipe to a workflow-create payload against a resolved upload
351
359
  * id. Single-input chain → ONE job, `source: upload(fileId)`, ordered
@@ -417,6 +425,7 @@ export declare class Recipe {
417
425
  private lowerStep;
418
426
  private lowerCompressOptions;
419
427
  private compressMediaHint;
428
+ private compressAudioLossless;
420
429
  }
421
430
  /**
422
431
  * The homogeneous fan-out builder value (FF3a). `client.files([a, b, c])`
@@ -455,16 +464,16 @@ export declare class FilesRecipe {
455
464
  * preset). Reuses {@link Recipe}'s validation — a directly-constructed
456
465
  * lowering builds an internal Recipe that throws the same `GislConfigError`.
457
466
  */
458
- compress(optimize?: OptimizeFor): FilesRecipe;
467
+ compress(optimize?: OptimizeFor, options?: Record<string, unknown>): FilesRecipe;
459
468
  /** Change every input's format. `format` lowers verbatim to the `format` option. */
460
- convert(format: string): FilesRecipe;
469
+ convert(format: string, options?: Record<string, unknown>): FilesRecipe;
461
470
  /** Generate a preview of every input. Omitted dimensions are dropped from the wire options. */
462
471
  thumbnail(options?: {
463
472
  width?: number;
464
473
  height?: number;
465
- }): FilesRecipe;
474
+ } & Record<string, unknown>): FilesRecipe;
466
475
  /** Apply the same text watermark to every input. */
467
- textWatermark(text: string): FilesRecipe;
476
+ textWatermark(text: string, options?: Record<string, unknown>): FilesRecipe;
468
477
  /**
469
478
  * Combine the inputs into ONE output (N→1), in array order (FF3b). Returns a
470
479
  * single-output {@link MergedRecipe} you chain further ops on
@@ -587,14 +596,14 @@ export declare class MergedRecipe {
587
596
  private readonly client?;
588
597
  constructor(inputs: readonly FileInput[], mergeOptions: MergeOptions, postSteps?: readonly RecipeStep[], presetDefaults?: PresetDefaults | undefined, scopedPresetDefaults?: PresetDefaults | undefined, client?: GislClient | undefined);
589
598
  /** Reduce the merged output's size. See {@link Recipe.compress}. */
590
- compress(optimize?: OptimizeFor): MergedRecipe;
599
+ compress(optimize?: OptimizeFor, options?: Record<string, unknown>): MergedRecipe;
591
600
  /** Change the merged output's format. See {@link Recipe.convert}. */
592
- convert(format: string): MergedRecipe;
601
+ convert(format: string, options?: Record<string, unknown>): MergedRecipe;
593
602
  /** Thumbnail the merged output. Omitted dimensions are dropped from the wire options. */
594
603
  thumbnail(options?: {
595
604
  width?: number;
596
605
  height?: number;
597
- }): MergedRecipe;
606
+ } & Record<string, unknown>): MergedRecipe;
598
607
  /**
599
608
  * Lower to the merge DAG: one `passthrough` source job per input + one
600
609
  * `merge` job whose `operations[]` is `[merge, ...post-combine ops]`. The
@@ -10,7 +10,7 @@
10
10
  * Mirrors `packages/php/src/FileFirst/*`.
11
11
  */
12
12
  import { GislConfigError, GislNetworkError, GislNoSuchKeyError, GislSinkError, GislTimeoutError, SseEndedWithoutTerminal } from './errors.js';
13
- import { _detectCompressMedia, _consumeSseToTerminal, _pollToTerminal, _parseMaxWait, _checkAborted, } from './builder.js';
13
+ import { _detectCompressMedia, _detectAudioLossless, _consumeSseToTerminal, _pollToTerminal, _parseMaxWait, _checkAborted, } from './builder.js';
14
14
  import { LazyHttpDownloader } from './lazy-downloader.js';
15
15
  import { resolveCompressOptions, } from './ergonomic/preset_resolver.js';
16
16
  import { OptimizeFor } from './generated/sdk_spec/enums.js';
@@ -421,36 +421,50 @@ export class Recipe {
421
421
  /**
422
422
  * Reduce file size. `optimize` selects a per-media preset (resolved to
423
423
  * concrete wire fields at lower-time, exactly as `client.compress()` does).
424
+ * `options` carries the full per-op options bag (mirrors
425
+ * `client.compress(input, options)`); the explicit `optimize` param wins
426
+ * over any `optimize` key in the bag.
424
427
  */
425
- compress(optimize) {
428
+ compress(optimize, options = {}) {
426
429
  if (optimize !== undefined && !Object.values(OptimizeFor).includes(optimize)) {
427
430
  const allowed = Object.values(OptimizeFor).join(', ');
428
431
  throw new GislConfigError(`compress 'optimize' must be one of ${allowed}; got '${String(optimize)}'.`, { reason: 'invalid_optimize', conflictingFields: ['optimize'] });
429
432
  }
430
- return this.withStep({ opType: 'compress', options: optimize === undefined ? {} : { optimize } });
433
+ return this.withStep({
434
+ opType: 'compress',
435
+ options: { ...options, ...(optimize !== undefined ? { optimize } : {}) },
436
+ });
431
437
  }
432
- /** Change format. `format` is lowered verbatim to the `format` wire option. */
433
- convert(format) {
434
- return this.withStep({ opType: 'convert', options: { format } });
438
+ /**
439
+ * Change format. `format` is lowered verbatim to the `format` wire option;
440
+ * `options` carries any additional per-op convert options.
441
+ */
442
+ convert(format, options = {}) {
443
+ // Spread options FIRST so the explicit `format` argument is authoritative —
444
+ // a `format` key in the bag must NOT silently override the call's format.
445
+ return this.withStep({ opType: 'convert', options: { ...options, format } });
435
446
  }
436
447
  /**
437
- * Generate a preview. Width and/or height in pixels; an omitted dimension is
438
- * dropped from the wire options (not sent as `undefined`).
448
+ * Generate a preview. Width and/or height in pixels; any additional per-op
449
+ * thumbnail options pass through. An omitted (`undefined`) value is dropped
450
+ * from the wire options (not sent as `undefined`).
439
451
  */
440
452
  thumbnail(options = {}) {
441
453
  const wire = {};
442
- if (options.width !== undefined)
443
- wire.width = options.width;
444
- if (options.height !== undefined)
445
- wire.height = options.height;
454
+ for (const [key, value] of Object.entries(options)) {
455
+ if (value !== undefined)
456
+ wire[key] = value;
457
+ }
446
458
  return this.withStep({ opType: 'thumbnail', options: wire });
447
459
  }
448
460
  /**
449
461
  * Apply a text watermark. Single-input (the text is an option, not a
450
- * secondary file) — lowers to the `text_watermark` op with a `text` option.
462
+ * secondary file) — lowers to the `text_watermark` op with a `text` option;
463
+ * `options` carries any additional per-op watermark options.
451
464
  */
452
- textWatermark(text) {
453
- return this.withStep({ opType: 'text_watermark', options: { text } });
465
+ textWatermark(text, options = {}) {
466
+ // Spread options FIRST so the explicit `text` argument is authoritative.
467
+ return this.withStep({ opType: 'text_watermark', options: { ...options, text } });
454
468
  }
455
469
  /**
456
470
  * Lower this recipe to a workflow-create payload against a resolved upload
@@ -636,21 +650,55 @@ export class Recipe {
636
650
  ? { type: step.opType }
637
651
  : { type: step.opType, options };
638
652
  }
639
- lowerCompressOptions(options) {
640
- const optimize = options.optimize;
653
+ lowerCompressOptions(stepOptions) {
654
+ // Mirror the op-first resolver precedence (OperationBuilder._resolve in
655
+ // builder.ts): optimize = preset layer, presetOverrides = callPresetOverride
656
+ // layer, the rest = explicit layer.
657
+ const { optimize, presetOverrides, ...explicitOptions } = stepOptions;
658
+ // Validate the special bag keys at this chokepoint (every compress lowers
659
+ // through here). The chain methods' shorthand-param guard only covers a
660
+ // PARAM-supplied optimize; a bag-supplied optimize / presetOverrides must be
661
+ // validated too, so a bad value raises the typed SDK error rather than
662
+ // surfacing as a raw preset-lookup error or TypeError downstream. Mirrors
663
+ // PHP coerceOptimize + OperationBuilder::normalisePresetOverrides.
664
+ if (optimize !== undefined && !Object.values(OptimizeFor).includes(optimize)) {
665
+ const allowed = Object.values(OptimizeFor).join(', ');
666
+ throw new GislConfigError(`compress 'optimize' must be one of ${allowed}; got '${String(optimize)}'.`, { reason: 'invalid_optimize', conflictingFields: ['optimize'] });
667
+ }
668
+ if (presetOverrides !== undefined &&
669
+ (presetOverrides === null || typeof presetOverrides !== 'object' || Array.isArray(presetOverrides))) {
670
+ const got = Array.isArray(presetOverrides)
671
+ ? 'array'
672
+ : presetOverrides === null
673
+ ? 'null'
674
+ : typeof presetOverrides;
675
+ throw new GislConfigError(`compress 'presetOverrides' must be a *CompressPresetOptions object; got ${got}.`, { reason: 'invalid_preset_overrides', conflictingFields: ['presetOverrides'] });
676
+ }
641
677
  const media = this.compressMediaHint();
642
678
  if (media === undefined) {
643
679
  // Cannot infer a media class (a Blob without a recognised name, or a
644
680
  // bare upload id) → preset resolution is impossible. Fail FAST rather
645
681
  // than silently dropping an explicit `optimize`; bare compress() is fine.
682
+ // When no optimize is set, pass any explicit options through verbatim
683
+ // (exactly as op-first `_resolve` does when media is undefined).
646
684
  if (optimize !== undefined) {
647
685
  throw new GislConfigError(`compress(optimize: ${String(optimize)}) needs a media type to resolve the preset, but the ` +
648
686
  'input has no inferable media (a pre-uploaded file id or unnamed Blob carries no extension). ' +
649
687
  'Use a path with a file extension, or call compress() without optimize.', { reason: 'media_unknown', conflictingFields: ['optimize'] });
650
688
  }
651
- return {};
689
+ // presetOverrides override a resolved preset; with no media there is no
690
+ // preset to override, so fail fast rather than silently dropping them.
691
+ if (presetOverrides !== undefined) {
692
+ throw new GislConfigError('compress(presetOverrides) needs a media type to resolve the preset to override, but the ' +
693
+ 'input has no inferable media (a pre-uploaded file id or unnamed Blob carries no extension). ' +
694
+ 'Use a path with a file extension.', { reason: 'media_unknown', conflictingFields: ['presetOverrides'] });
695
+ }
696
+ return { ...explicitOptions };
697
+ }
698
+ const input = { media, op: 'compress', explicitOptions };
699
+ if (media === 'audio') {
700
+ input.audioLossless = this.compressAudioLossless();
652
701
  }
653
- const input = { media, op: 'compress', explicitOptions: {} };
654
702
  if (this.presetDefaults !== undefined) {
655
703
  input.presetDefaults = this.presetDefaults;
656
704
  }
@@ -658,6 +706,11 @@ export class Recipe {
658
706
  input.scopedPresetDefaults =
659
707
  this.scopedPresetDefaults;
660
708
  }
709
+ if (presetOverrides !== undefined) {
710
+ // Validated above to be a non-null, non-array object.
711
+ input.presetOverrides =
712
+ presetOverrides;
713
+ }
661
714
  if (optimize !== undefined) {
662
715
  input.optimize = optimize;
663
716
  }
@@ -672,6 +725,13 @@ export class Recipe {
672
725
  }
673
726
  return undefined;
674
727
  }
728
+ compressAudioLossless() {
729
+ if (this.input.kind === 'path')
730
+ return _detectAudioLossless(this.input.path);
731
+ if (this.input.kind === 'blob')
732
+ return _detectAudioLossless(this.input.blob);
733
+ return false;
734
+ }
675
735
  }
676
736
  /**
677
737
  * The homogeneous fan-out builder value (FF3a). `client.files([a, b, c])`
@@ -716,20 +776,20 @@ export class FilesRecipe {
716
776
  * preset). Reuses {@link Recipe}'s validation — a directly-constructed
717
777
  * lowering builds an internal Recipe that throws the same `GislConfigError`.
718
778
  */
719
- compress(optimize) {
720
- return this.withStep(this.baseRecipe().compress(optimize));
779
+ compress(optimize, options = {}) {
780
+ return this.withStep(this.baseRecipe().compress(optimize, options));
721
781
  }
722
782
  /** Change every input's format. `format` lowers verbatim to the `format` option. */
723
- convert(format) {
724
- return this.withStep(this.baseRecipe().convert(format));
783
+ convert(format, options = {}) {
784
+ return this.withStep(this.baseRecipe().convert(format, options));
725
785
  }
726
786
  /** Generate a preview of every input. Omitted dimensions are dropped from the wire options. */
727
787
  thumbnail(options = {}) {
728
788
  return this.withStep(this.baseRecipe().thumbnail(options));
729
789
  }
730
790
  /** Apply the same text watermark to every input. */
731
- textWatermark(text) {
732
- return this.withStep(this.baseRecipe().textWatermark(text));
791
+ textWatermark(text, options = {}) {
792
+ return this.withStep(this.baseRecipe().textWatermark(text, options));
733
793
  }
734
794
  /**
735
795
  * Combine the inputs into ONE output (N→1), in array order (FF3b). Returns a
@@ -981,24 +1041,29 @@ export class MergedRecipe {
981
1041
  this.client = client;
982
1042
  }
983
1043
  /** Reduce the merged output's size. See {@link Recipe.compress}. */
984
- compress(optimize) {
1044
+ compress(optimize, options = {}) {
985
1045
  if (optimize !== undefined && !Object.values(OptimizeFor).includes(optimize)) {
986
1046
  const allowed = Object.values(OptimizeFor).join(', ');
987
1047
  throw new GislConfigError(`compress 'optimize' must be one of ${allowed}; got '${String(optimize)}'.`, { reason: 'invalid_optimize', conflictingFields: ['optimize'] });
988
1048
  }
989
- return this.withStep({ opType: 'compress', options: optimize === undefined ? {} : { optimize } });
1049
+ return this.withStep({
1050
+ opType: 'compress',
1051
+ options: { ...options, ...(optimize !== undefined ? { optimize } : {}) },
1052
+ });
990
1053
  }
991
1054
  /** Change the merged output's format. See {@link Recipe.convert}. */
992
- convert(format) {
993
- return this.withStep({ opType: 'convert', options: { format } });
1055
+ convert(format, options = {}) {
1056
+ // Spread options FIRST so the explicit `format` argument is authoritative —
1057
+ // a `format` key in the bag must NOT silently override the call's format.
1058
+ return this.withStep({ opType: 'convert', options: { ...options, format } });
994
1059
  }
995
1060
  /** Thumbnail the merged output. Omitted dimensions are dropped from the wire options. */
996
1061
  thumbnail(options = {}) {
997
1062
  const wire = {};
998
- if (options.width !== undefined)
999
- wire.width = options.width;
1000
- if (options.height !== undefined)
1001
- wire.height = options.height;
1063
+ for (const [key, value] of Object.entries(options)) {
1064
+ if (value !== undefined)
1065
+ wire[key] = value;
1066
+ }
1002
1067
  return this.withStep({ opType: 'thumbnail', options: wire });
1003
1068
  }
1004
1069
  /**
@@ -3,7 +3,7 @@ export { parseSseStream } from './sse.js';
3
3
  export type { CreditsUsageOptions, ListWorkflowsOptions, GetSchemaOptions, GetSchemaResult, PreflightClipError, PreflightClipsResult, GislClientConfig, GislSseEvent, UploadOptions, WaitOptions, WorkflowCreatePayload, OperationDef, WorkflowSourcePayload, MultiInputSourcePayload, UploadSourcePayload, JobOutputSourcePayload, ExternalImportSourcePayload, ConnectionSourcePayload, JobInputV2Payload, JobDefinitionPayload, ExternalDestinationPayload, DeliveryPayload, DeliveryModePayload, DeliveryBundleFormatPayload, DeliverySelectionPayload, DeliverySelectionTypePayload, DeliveryOutputRefPayload, WorkflowProcessingPayload, ProcessingClassHintPayload, MultipartCheckpointState, _Sdk3HandCodedUploadedPart, _Sdk3HandCodedMultipartStatusResult, _Sdk3HandCodedPresignedPart, _Sdk3HandCodedPresignPartsResult, _Sdk3HandCodedKeepaliveResult, } from './types.js';
4
4
  export { uploadSource, jobOutputSource, externalImportSource, connectionSource, } from './types.js';
5
5
  export type { GislConfigErrorMetadata } from './errors.js';
6
- export { GislError, GislApiError, GislValidationError, GislBalanceExhaustedError, GislTierRestrictedError, GislFeatureTierRestrictedError, GislFeatureNotAvailableError, GislWorkflowExpiredError, GislProbePendingError, GislAuthError, GislUploadCapExceededError, GislMultipartPartError, GislMultipartPartCountError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTimeoutError, GislAbortError, GislNetworkError, GislConfigError, GislMissingCredentialsError, GislFeatureRequiresAuthError, GislUndeclaredAssetError, GislUnusedAssetError, GislPerInputOptionsNotSupportedError, GislChainCardinalityMismatchError, GislNoSuchKeyError, GislSinkError, GislResultNotReadyError, } from './errors.js';
6
+ export { GislError, GislApiError, GislValidationError, GislBalanceExhaustedError, GislTierRestrictedError, GislFeatureTierRestrictedError, GislFeatureNotAvailableError, GislWorkflowExpiredError, GislProbePendingError, GislAuthError, GislUploadCapExceededError, GislMultipartPartError, GislMultipartPartCountError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTimeoutError, GislAbortError, GislNetworkError, GislConfigError, GislMissingCredentialsError, GislFeatureRequiresAuthError, GislUndeclaredAssetError, GislUnusedAssetError, GislPerInputOptionsNotSupportedError, GislChainCardinalityMismatchError, GislBundleAlreadyArchivedError, GislNoSuchKeyError, GislSinkError, GislResultNotReadyError, } from './errors.js';
7
7
  export type { GislApiErrorOptions, GislUploadCapKind } from './errors.js';
8
8
  export { RunResult } from './file-first.js';
9
9
  export type { OutputFile, ItemResult, ItemFailure, Manifest, Downloader, } from './file-first.js';
@@ -23,6 +23,9 @@ GislUndeclaredAssetError, GislUnusedAssetError, GislPerInputOptionsNotSupportedE
23
23
  // methods on OperationBuilder ship; type + audit registration land
24
24
  // here so the future chain-method PR is a pure addition).
25
25
  GislChainCardinalityMismatchError,
26
+ // P4d / hv3FpLjm — double-bundle prevention; raised by `.bundle()` (wpHoJhuo).
27
+ // Dormant until `.bundle()` ships, so the type lands here as a pure addition.
28
+ GislBundleAlreadyArchivedError,
26
29
  // FF1 / 3BIxEnfR — file-first result sink errors.
27
30
  GislNoSuchKeyError, GislSinkError,
28
31
  // FF5a / Ao8RPVxD — thrown by the file-first Handle.result() when the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giveitsmaller/sdk",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Node.js SDK for the GISL (Give It Smaller) file compression and processing API",
5
5
  "license": "MIT",
6
6
  "type": "module",