@giveitsmaller/sdk 0.18.0 → 0.20.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/README.md +1 -1
  2. package/dist/_audit.js +12 -0
  3. package/dist/builder.d.ts +1 -0
  4. package/dist/client.d.ts +8 -1
  5. package/dist/client.js +27 -32
  6. package/dist/ergonomic/image_output_routes.d.ts +6 -5
  7. package/dist/ergonomic/image_output_routes.js +29 -21
  8. package/dist/ergonomic/option_types.d.ts +85 -11
  9. package/dist/ergonomic/option_types.js +11 -6
  10. package/dist/ergonomic/option_validation.d.ts +21 -3
  11. package/dist/ergonomic/option_validation.js +36 -3
  12. package/dist/ergonomic/preset_resolver.d.ts +1 -1
  13. package/dist/ergonomic/preset_resolver.js +6 -7
  14. package/dist/ergonomic/presets/document_epub_compress.d.ts +0 -2
  15. package/dist/ergonomic/presets/document_epub_compress.js +2 -7
  16. package/dist/ergonomic/presets/document_odf_compress.d.ts +0 -2
  17. package/dist/ergonomic/presets/document_odf_compress.js +2 -7
  18. package/dist/ergonomic/presets/document_office_compress.d.ts +0 -2
  19. package/dist/ergonomic/presets/document_office_compress.js +2 -7
  20. package/dist/errors.d.ts +57 -1
  21. package/dist/errors.js +82 -1
  22. package/dist/file-first.d.ts +195 -8
  23. package/dist/file-first.js +462 -124
  24. package/dist/generated/sdk_spec/enums.d.ts +4 -2
  25. package/dist/generated/sdk_spec/enums.js +11 -5
  26. package/dist/generated/sdk_spec/presets.js +3 -12
  27. package/dist/generated/sdk_spec/version.d.ts +3 -3
  28. package/dist/generated/sdk_spec/version.js +3 -3
  29. package/dist/gisl.d.ts +72 -3
  30. package/dist/gisl.js +72 -2
  31. package/dist/index.core.d.ts +8 -6
  32. package/dist/index.core.js +9 -1
  33. package/dist/merge.d.ts +12 -0
  34. package/dist/merge.js +12 -0
  35. package/dist/retry-metadata.d.ts +37 -0
  36. package/dist/retry-metadata.js +86 -0
  37. package/dist/sse.d.ts +2 -1
  38. package/dist/sse.js +26 -6
  39. package/dist/types.d.ts +43 -1
  40. package/package.json +2 -2
package/README.md CHANGED
@@ -67,7 +67,7 @@ Docs are published in the [giveitsmaller-sdks](https://github.com/AntonioCS/give
67
67
  - **Webhooks** — [`docs/typescript/webhook.md`](https://github.com/AntonioCS/giveitsmaller-sdks/blob/main/docs/typescript/webhook.md)
68
68
  - **Errors & retry guidance** — [`docs/typescript/errors.md`](https://github.com/AntonioCS/giveitsmaller-sdks/blob/main/docs/typescript/errors.md)
69
69
  - **Troubleshooting** — [`docs/typescript/troubleshooting.md`](https://github.com/AntonioCS/giveitsmaller-sdks/blob/main/docs/typescript/troubleshooting.md)
70
- - **Examples** — compress, thumbnail, convert, merge, archive — [`docs/typescript/examples/`](https://github.com/AntonioCS/giveitsmaller-sdks/tree/main/docs/typescript/examples)
70
+ - **Examples** — compress, thumbnail, convert, merge, archive, watermark, output — [`docs/typescript/examples/`](https://github.com/AntonioCS/giveitsmaller-sdks/tree/main/docs/typescript/examples)
71
71
 
72
72
  ## Contributing: the committed `dist/`
73
73
 
package/dist/_audit.js CHANGED
@@ -139,6 +139,8 @@ export function _runAudit() {
139
139
  accept();
140
140
  // FF3a / u0hBt6fl — homogeneous fan-out builder surface.
141
141
  accept();
142
+ // FF7 / MFaCjL8d — keyed multi-recipe batch builder surface.
143
+ accept();
142
144
  // FF4a / Z7zTr789 — multi-input watermark recipe surface.
143
145
  accept();
144
146
  accept();
@@ -149,4 +151,14 @@ export function _runAudit() {
149
151
  // FF5a / Ao8RPVxD — file-first Handle reattach surface.
150
152
  accept();
151
153
  accept();
154
+ // TYNjcjpo — SSE parse-failure diagnostic surface.
155
+ accept();
156
+ // qUhxfDA5 — capabilities() projection surface + the three contract
157
+ // capability types it exposes.
158
+ accept();
159
+ accept();
160
+ accept();
161
+ accept();
162
+ // W8v4jWzx — error-taxonomy category union surfaced by GislApiError.category.
163
+ accept();
152
164
  }
package/dist/builder.d.ts CHANGED
@@ -217,6 +217,7 @@ export interface UploadProgressEvent {
217
217
  export interface ProcessingProgressEvent {
218
218
  readonly phase: 'processing';
219
219
  readonly status?: SseOperationProgressDataStatusEnum;
220
+ /** Percent complete, `0-100` (the wire integer) — NOT a 0..1 fraction. */
220
221
  readonly progress: number;
221
222
  readonly jobRef: string;
222
223
  readonly operationId: string;
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { AudioWatermarkDecodeRequest, AudioWatermarkDecodeResponse, ExternalImportCreatedResponse, ExternalImportRequest, LoginUserRequest, LoginUser200ResponseData, ContactRequest, AccountLimits, CreditsBalanceResponse, CreditsUsageResponse, UploadResponse, UploadProbeResponse, WorkflowCancelResponse, WorkflowCreateResponse, WorkflowResumeResponse, WorkflowStatusResponse, WorkflowListResponse, WorkflowSummary, WorkflowDownloadResponse, MetadataResponse, RetryResponse } from '@giveitsmaller/contracts/openapi';
2
- import type { CreditsUsageOptions, ListWorkflowsOptions, GetSchemaOptions, GetSchemaResult, GislClientConfig, GislSseEvent, PreflightClipsResult, ProbeWaitOptions, ProbeWaitResult, ReadCapabilityOptions, UploadOptions, WaitOptions, WorkflowCreatePayload, _Sdk3HandCodedKeepaliveResult, _Sdk3HandCodedMultipartStatusResult, _Sdk3HandCodedPresignPartsResult } from './types.js';
2
+ import type { CreditsUsageOptions, ListWorkflowsOptions, GetSchemaOptions, GetSchemaResult, GislClientConfig, GislSseEvent, GislSseParseFailure, PreflightClipsResult, ProbeWaitOptions, ProbeWaitResult, ReadCapabilityOptions, UploadOptions, WaitOptions, WorkflowCreatePayload, _Sdk3HandCodedKeepaliveResult, _Sdk3HandCodedMultipartStatusResult, _Sdk3HandCodedPresignPartsResult } from './types.js';
3
3
  export declare const MULTIPART_CONCURRENCY_DEFAULT: 4;
4
4
  export declare const DEFAULT_MULTIPART_FIRST_CHUNK_SIZE: number;
5
5
  export interface ValidationDetail {
@@ -199,6 +199,13 @@ export declare class GislClient {
199
199
  streamEvents(workflowId: string, opts?: {
200
200
  signal?: AbortSignal;
201
201
  capability?: string;
202
+ /**
203
+ * Observe malformed-JSON SSE frames (TYNjcjpo). A frame whose `data:` body
204
+ * fails to parse is SKIPPED from the stream (kept resilient) and reported
205
+ * here as a typed {@link GislSseParseFailure} instead of being silently lost.
206
+ * Omit to drop malformed frames silently (the default; PHP parity).
207
+ */
208
+ onParseError?: (diagnostic: GislSseParseFailure) => void;
202
209
  }): Promise<AsyncGenerator<GislSseEvent>>;
203
210
  /**
204
211
  * Get metadata for an uploaded file.
package/dist/client.js CHANGED
@@ -6,6 +6,10 @@
6
6
  import { open, stat, basename } from './node-fs.js';
7
7
  import { AudioWatermarkDecodeRequestToJSON, AudioWatermarkDecodeResponseFromJSON, ExternalImportCreatedResponseFromJSON, ExternalImportRequestToJSON, LoginUser200ResponseDataFromJSON, AccountLimitsFromJSON, CreditsBalanceResponseFromJSON, CreditsUsageResponseFromJSON, UploadResponseFromJSON, UploadProbeResponseFromJSON, MultipartInitiateResponseFromJSON, MultipartInitiateRequestMetadataHintToJSON, MultipartCompleteResponseFromJSON, MultipartCompleteRequestToJSON, WorkflowCancelResponseFromJSON, WorkflowCreateResponseFromJSON, WorkflowResumeResponseFromJSON, WorkflowStatusResponseFromJSON, WorkflowListResponseFromJSON, WorkflowDownloadResponseFromJSON, MetadataResponseFromJSON, OperationsSchemaResponseFromJSON, RetryResponseFromJSON, WorkflowStatus, AuthErrorResponseFromJSON, AuthErrorType, AuthRejectionEnvelopeFromJSON, AuthRejectionEnvelopeErrorTypeEnum, BalanceExhaustedResponseFromJSON, BalanceExhaustedResponseRequiredActionEnum, FeatureNotAvailableResponseFromJSON, FeatureTierRestrictedResponseFromJSON, TierRestrictionKind, TierRestrictionResponseFromJSON, UserTier, WorkflowExpiredResponseFromJSON, ProbePendingResponseFromJSON, UploadSizeExceedsTierResponseFromJSON, UploadDurationExceedsTierResponseFromJSON, UploadConstraintsAppliedProcessingClassPreAssignmentEnum, UploadThresholdsSingleShotMaxBytesEnum, UploadThresholdsMultipartChunkSizeEnum, UploadThresholdsMultipartConcurrencyDefaultEnum, } from '@giveitsmaller/contracts/openapi';
8
8
  import { GislAbortError, GislApiError, GislAuthError, GislAuthRejectionError, GislBalanceExhaustedError, GislError, GislFeatureNotAvailableError, GislFeatureTierRestrictedError, GislMultipartPartCountError, GislMultipartPartError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTierRestrictedError, GislTimeoutError, GislProbePendingError, GislUploadCapExceededError, GislValidationError, GislWorkflowExpiredError, } from './errors.js';
9
+ // The `Retry-After` millisecond parser lives in the shared retry-metadata
10
+ // module (extracted to break the client ↔ errors circular import); re-imported
11
+ // here so the retry-loop timing stays byte-identical.
12
+ import { parseRetryAfterMs } from './retry-metadata.js';
9
13
  import { parseSseStream } from './sse.js';
10
14
  const DEFAULT_TIMEOUT_MS = 30_000;
11
15
  // SDK-internal aliases derived from the contract-pinned UploadThresholds enums
@@ -109,12 +113,14 @@ function isAbortError(err) {
109
113
  typeof err === 'object' &&
110
114
  err.name === 'AbortError');
111
115
  }
112
- // Retryable S3 PUT response statuses: 429 throttling, 503 slow-down, and any
113
- // other 5xx (502/504 are common transients behind CloudFront/S3). 4xx other
114
- // than 429 (403 signed-URL expiry, 400 SignatureDoesNotMatch, etc.) are
115
- // configuration / authority issues retrying just delays the real failure.
116
+ // Retryable S3 PUT response statuses: 408 request timeout, 429 throttling, 503
117
+ // slow-down, and any other 5xx (502/504 are common transients behind
118
+ // CloudFront/S3). 408 is a transient timeout on the PUT itself, so it is retried
119
+ // (matching the PHP SDK's S3-PUT predicate qz7MjNTy cross-SDK alignment). Other
120
+ // 4xx (403 signed-URL expiry, 400 SignatureDoesNotMatch, etc.) are configuration
121
+ // / authority issues — retrying just delays the real failure.
116
122
  function isRetryableStatus(status) {
117
- return status === 429 || (status >= 500 && status <= 599);
123
+ return status === 408 || status === 429 || (status >= 500 && status <= 599);
118
124
  }
119
125
  // fetch surfaces network failures (DNS, TLS, TCP reset, mid-body disconnect)
120
126
  // as TypeError. Abort surfaces as a DOMException with name='AbortError', not
@@ -123,31 +129,6 @@ function isRetryableStatus(status) {
123
129
  function isRetryableNetworkError(err) {
124
130
  return err instanceof TypeError;
125
131
  }
126
- // Parse an HTTP `Retry-After` header into milliseconds. Accepts the two RFC
127
- // 9110 forms: delta-seconds (e.g. "5") or an HTTP-date. Returns `undefined`
128
- // for an absent / unparseable / negative value (caller falls back to its own
129
- // backoff). A past HTTP-date clamps to 0.
130
- function parseRetryAfterMs(headerValue) {
131
- if (headerValue === undefined)
132
- return undefined;
133
- const trimmed = headerValue.trim();
134
- if (trimmed === '')
135
- return undefined;
136
- let ms;
137
- if (/^\d+$/.test(trimmed)) {
138
- ms = Number(trimmed) * 1000;
139
- }
140
- else {
141
- const when = Date.parse(trimmed);
142
- if (Number.isNaN(when))
143
- return undefined;
144
- ms = when - Date.now();
145
- }
146
- // A non-positive Retry-After (e.g. "0" or a past HTTP-date) must NOT short-
147
- // circuit the backoff to zero — treat it as absent so the caller falls back
148
- // to jitter and the loop can't busy-poll until timeout.
149
- return ms > 0 ? ms : undefined;
150
- }
151
132
  // Cancellable sleep for poll loops. Resolves after `ms`, or rejects with
152
133
  // `GislAbortError` if `signal` aborts. Resolves immediately for ms <= 0.
153
134
  function cancellableSleep(ms, signal) {
@@ -493,6 +474,11 @@ export class GislClient {
493
474
  messageKey: json.message_key,
494
475
  locale: json.locale,
495
476
  messageParams: json.message_params,
477
+ // The wire-stable machine code (SCREAMING_SNAKE `error`), surfaced as
478
+ // `error.errorCode` on every dispatched error (PHP parity). Threaded via
479
+ // this single options object → base GislApiError, GislValidationError,
480
+ // and every structured subclass (passed through as `extra`).
481
+ errorCode: typeof json.error === 'string' ? json.error : undefined,
496
482
  responseHeaders,
497
483
  contentLanguage,
498
484
  };
@@ -1747,7 +1733,10 @@ export class GislClient {
1747
1733
  releaseConsumerSignal();
1748
1734
  }
1749
1735
  }
1750
- const inner = parseSseStream(response, { signal: controller.signal });
1736
+ const inner = parseSseStream(response, {
1737
+ signal: controller.signal,
1738
+ ...(opts.onParseError !== undefined ? { onParseError: opts.onParseError } : {}),
1739
+ });
1751
1740
  let started = false;
1752
1741
  let settled = false;
1753
1742
  // Idempotent teardown. `abort` only on consumer-driven early
@@ -1874,6 +1863,7 @@ export class GislClient {
1874
1863
  }
1875
1864
  if (!response.ok) {
1876
1865
  let errorMessage = 'Unknown error';
1866
+ let errorCode;
1877
1867
  try {
1878
1868
  const errJson = (await response.json());
1879
1869
  // Prefer the human `message`; `error` is the machine code (x9Lbf6uy).
@@ -1881,13 +1871,18 @@ export class GislClient {
1881
1871
  errorMessage = errJson.message;
1882
1872
  else if (errJson.error)
1883
1873
  errorMessage = errJson.error;
1874
+ // Surface the machine code as errorCode too (parity with handleResponse
1875
+ // + PHP), even when `message` supplied the human text.
1876
+ if (typeof errJson.error === 'string')
1877
+ errorCode = errJson.error;
1884
1878
  }
1885
1879
  catch {
1886
- // Non-JSON body — keep generic message.
1880
+ // Non-JSON body — keep generic message, no machine code.
1887
1881
  }
1888
1882
  // This throw is OUTSIDE handleResponse (rawResponse:true / 304 path), so
1889
1883
  // build the response-header surface from the in-scope `response` here.
1890
1884
  throw new GislApiError(response.status, errorMessage, path, undefined, {
1885
+ errorCode,
1891
1886
  responseHeaders: headersToRecord(response.headers),
1892
1887
  contentLanguage: response.headers.get('content-language') ?? undefined,
1893
1888
  });
@@ -12,11 +12,12 @@
12
12
  *
13
13
  * Each route cell lists the options the worker HONORS (live) and PLANS (advertised,
14
14
  * not yet honored — gated unavailable). Resize (`width`/`height`/`fit`) is
15
- * **INPUT-keyed**: it lives only on the `same_format[input]` cell but applies on
16
- * EITHER route, gated by input resizability (raster only `svg` is vector and
17
- * carries no resize). So a `png webp + resize` request reads its resize
18
- * capability from `same_format.png` and its transcoder options from
19
- * `format_change.webp`.
15
+ * **INPUT-gated**: since v2.103.0 convert is the resize engine, so the projection
16
+ * lists resize on every `format_change` cell toobut resizability is keyed to the
17
+ * INPUT (raster only `svg` is vector and carries no resize). The lowering reads
18
+ * resize capability from `same_format[input]` on BOTH routes, so a `png → webp +
19
+ * resize` request resizes (png is raster) while an `svg → png + resize` request
20
+ * does NOT (svg's same_format cell has no resize keys).
20
21
  *
21
22
  * This hand table MIRRORS the generated projection and is PINNED to it by
22
23
  * `output-route-conformance.test.ts` (the watermark-capability-gate precedent) —
@@ -12,11 +12,12 @@
12
12
  *
13
13
  * Each route cell lists the options the worker HONORS (live) and PLANS (advertised,
14
14
  * not yet honored — gated unavailable). Resize (`width`/`height`/`fit`) is
15
- * **INPUT-keyed**: it lives only on the `same_format[input]` cell but applies on
16
- * EITHER route, gated by input resizability (raster only `svg` is vector and
17
- * carries no resize). So a `png webp + resize` request reads its resize
18
- * capability from `same_format.png` and its transcoder options from
19
- * `format_change.webp`.
15
+ * **INPUT-gated**: since v2.103.0 convert is the resize engine, so the projection
16
+ * lists resize on every `format_change` cell toobut resizability is keyed to the
17
+ * INPUT (raster only `svg` is vector and carries no resize). The lowering reads
18
+ * resize capability from `same_format[input]` on BOTH routes, so a `png → webp +
19
+ * resize` request resizes (png is raster) while an `svg → png + resize` request
20
+ * does NOT (svg's same_format cell has no resize keys).
20
21
  *
21
22
  * This hand table MIRRORS the generated projection and is PINNED to it by
22
23
  * `output-route-conformance.test.ts` (the watermark-capability-gate precedent) —
@@ -28,6 +29,8 @@
28
29
  import { compressMetadata } from '@giveitsmaller/contracts/operations';
29
30
  /** The resize option keys — input-keyed, raster-only (see module doc). */
30
31
  export const RESIZE_KEYS = ['width', 'height', 'fit'];
32
+ /** Set form of {@link RESIZE_KEYS} for `string`-keyed membership tests. */
33
+ const RESIZE_KEY_SET = new Set(RESIZE_KEYS);
31
34
  /** Image area cap shared by every resizable route (projection `max_output_pixels`). */
32
35
  export const MAX_OUTPUT_PIXELS = 16_000_000;
33
36
  /**
@@ -61,21 +64,21 @@ const EXT_TOKEN = {
61
64
  */
62
65
  export const IMAGE_OUTPUT_ROUTES = {
63
66
  same_format: {
64
- avif: { honored: ['avif_speed', 'fit', 'height', 'metadata', 'output_format', 'quality', 'width'], planned: [] },
65
- gif: { honored: ['fit', 'height', 'metadata', 'output_format', 'quality', 'width'], planned: [] },
66
- jpeg: { honored: ['fit', 'height', 'lossless', 'metadata', 'output_format', 'progressive', 'quality', 'width'], planned: [] },
67
- png: { honored: ['fit', 'height', 'metadata', 'optimization_level', 'output_format', 'quality', 'width'], planned: ['lossy'] },
68
- svg: { honored: ['metadata', 'output_format', 'quality'], planned: [] },
69
- tiff: { honored: ['fit', 'height', 'metadata', 'output_format', 'quality', 'width'], planned: [] },
70
- webp: { honored: ['fit', 'height', 'lossless', 'metadata', 'output_format', 'quality', 'width'], planned: [] },
67
+ avif: { honored: ['auto_orient', 'avif_speed', 'color_profile', 'encoding_mode', 'fit', 'height', 'metadata', 'output_format', 'quality', 'quality_preset', 'target_size_bytes', 'width'], planned: [] },
68
+ gif: { honored: ['auto_orient', 'color_profile', 'fit', 'height', 'metadata', 'output_format', 'quality', 'width'], planned: [] },
69
+ jpeg: { honored: ['auto_orient', 'chroma_subsampling', 'color_profile', 'encoding_mode', 'fit', 'height', 'lossless', 'metadata', 'output_format', 'progressive', 'quality', 'quality_preset', 'target_size_bytes', 'width'], planned: [] },
70
+ png: { honored: ['auto_orient', 'color_profile', 'fit', 'height', 'metadata', 'optimization_level', 'output_format', 'quality', 'width'], planned: [] },
71
+ svg: { honored: ['metadata', 'output_format'], planned: [] },
72
+ tiff: { honored: ['auto_orient', 'color_profile', 'fit', 'height', 'metadata', 'output_format', 'quality', 'width'], planned: [] },
73
+ webp: { honored: ['auto_orient', 'color_profile', 'encoding_mode', 'fit', 'height', 'lossless', 'metadata', 'output_format', 'quality', 'quality_preset', 'target_size_bytes', 'width'], planned: [] },
71
74
  },
72
75
  format_change: {
73
- avif: { honored: ['output_format', 'quality'], planned: [] },
74
- gif: { honored: ['output_format'], planned: [] },
75
- jpeg: { honored: ['background', 'output_format', 'quality'], planned: [] },
76
- png: { honored: ['output_format'], planned: [] },
77
- tiff: { honored: ['output_format'], planned: [] },
78
- webp: { honored: ['output_format', 'quality'], planned: [] },
76
+ avif: { honored: ['auto_orient', 'color_profile', 'fit', 'height', 'output_format', 'quality', 'width'], planned: ['metadata'] },
77
+ gif: { honored: ['auto_orient', 'color_profile', 'fit', 'height', 'output_format', 'width'], planned: ['metadata'] },
78
+ jpeg: { honored: ['auto_orient', 'background', 'color_profile', 'fit', 'height', 'output_format', 'quality', 'width'], planned: ['metadata'] },
79
+ png: { honored: ['auto_orient', 'color_profile', 'fit', 'height', 'output_format', 'width'], planned: ['metadata'] },
80
+ tiff: { honored: ['auto_orient', 'color_profile', 'fit', 'height', 'output_format', 'width'], planned: ['metadata'] },
81
+ webp: { honored: ['auto_orient', 'color_profile', 'fit', 'height', 'output_format', 'quality', 'width'], planned: ['metadata'] },
79
82
  },
80
83
  };
81
84
  /** The bare format token for a MIME type, or undefined if not a known image MIME. */
@@ -114,8 +117,13 @@ export function resolveOutputRoute(inputToken, outputFormat) {
114
117
  const cell = IMAGE_OUTPUT_ROUTES.format_change[outToken];
115
118
  if (cell === undefined)
116
119
  return undefined;
117
- // Input-keyed resize: the format_change cell carries only transcoder options;
118
- // resize capability comes from the INPUT's same_format cell (raster only).
120
+ // Resize is INPUT-gated. Since v2.103.0 convert is the resize engine, so the
121
+ // projection lists width/height/fit on EVERY format_change cell but an SVG
122
+ // INPUT cannot be raster-resized (the convert worker rejects it). So strip the
123
+ // cell's resize keys and re-add only those the INPUT's same_format cell honors:
124
+ // raster inputs carry them, svg does not. The transcoder options (output_format/
125
+ // quality/background) ride the cell directly.
126
+ const transcoderHonored = cell.honored.filter((k) => !RESIZE_KEY_SET.has(k));
119
127
  const inCell = IMAGE_OUTPUT_ROUTES.same_format[inputToken];
120
128
  const resize = inCell ? RESIZE_KEYS.filter((k) => inCell.honored.includes(k)) : [];
121
129
  return {
@@ -123,7 +131,7 @@ export function resolveOutputRoute(inputToken, outputFormat) {
123
131
  sourceOp: 'convert',
124
132
  outputFormatWire: outToken,
125
133
  inputToken,
126
- honored: new Set([...cell.honored, ...resize]),
134
+ honored: new Set([...transcoderHonored, ...resize]),
127
135
  planned: new Set(cell.planned),
128
136
  };
129
137
  }
@@ -29,8 +29,18 @@ export interface ConvertOptions {
29
29
  trim_end?: number;
30
30
  /** Output frame rate for video → GIF. */
31
31
  fps?: number;
32
- /** Max output width in pixels (video → GIF downscale cap). */
32
+ /** Resize width in px. Image convert: resize-on-convert (1-16384, width*height ≤ 16MP, v2.103.0). Video → GIF: downscale cap. */
33
33
  width?: number;
34
+ /** Resize height in px for image convert (1-16384, v2.103.0). Omit for width-only / aspect-preserving resize. Not honored for SVG input. */
35
+ height?: number;
36
+ /** Resize mode for image convert (applies when width or height is set, v2.103.0). `max` never upscales. */
37
+ fit?: 'max' | 'crop' | 'scale';
38
+ /** Metadata policy for image convert (`strip` removes EXIF/IPTC/XMP, `keep` preserves). PLANNED on convert.image (v2.106.0). */
39
+ metadata?: 'strip' | 'keep';
40
+ /** ICC colour-profile handling for image convert (`keep`/`srgb`/`strip`). PLANNED on convert.image (v2.112.0). */
41
+ color_profile?: 'keep' | 'srgb' | 'strip';
42
+ /** Auto-rotate per EXIF orientation for image convert. STABLE since v2.120.0. */
43
+ auto_orient?: boolean;
34
44
  /** GIF palette size (2-256). */
35
45
  max_colors?: number;
36
46
  /** GIF loop count (0 infinite, N>0 N times, -1 once). */
@@ -55,6 +65,8 @@ export interface ThumbnailOptions {
55
65
  format?: 'jpg' | 'png' | 'webp';
56
66
  /** Output quality for lossy thumbnail formats (image input). */
57
67
  quality?: number;
68
+ /** Background fill colour (hex) for transparency flattened to JPG output (v2.118.0). */
69
+ background?: string;
58
70
  /** Frame timestamp for video input (e.g. '00:00:01'). */
59
71
  timestamp?: string;
60
72
  /** Document source: a printed page or the cover. */
@@ -62,6 +74,12 @@ export interface ThumbnailOptions {
62
74
  /** 1-based page index for document input. */
63
75
  page?: number;
64
76
  }
77
+ export interface TransformOptions {
78
+ /** Clockwise rotation in degrees. document_pdf honors `rotate` only. */
79
+ rotate?: 0 | 90 | 180 | 270;
80
+ /** Mirror axis (applied after `rotate`). Not honored on document_pdf input. */
81
+ flip?: 'none' | 'horizontal' | 'vertical' | 'both';
82
+ }
65
83
  export interface TextWatermarkOptions {
66
84
  /** Font size in pixels (8-512). */
67
85
  font_size?: number;
@@ -84,6 +102,24 @@ export interface TextWatermarkOptions {
84
102
  /** Overlay opacity (0-1). */
85
103
  opacity?: number;
86
104
  }
105
+ /**
106
+ * One entry in the multi-overlay stack (contract `overlays[]` items, v2.152.0).
107
+ * Index-aligned to the overlay-role sources — `overlays[i]` places overlay
108
+ * source `i` — and mirrors the flat single-overlay option shape. Matches the
109
+ * generated `ImageWatermarkImageOverlaysItem`.
110
+ */
111
+ export interface WatermarkOverlay {
112
+ /** 9-grid anchor position for this overlay. */
113
+ anchor?: WatermarkAnchor;
114
+ /** Horizontal offset from the anchor (e.g. '40px' or '5%'). */
115
+ margin_x?: string;
116
+ /** Vertical offset from the anchor. */
117
+ margin_y?: string;
118
+ /** Overlay opacity (0-1). */
119
+ opacity?: number;
120
+ /** Overlay width (e.g. '120px' or '20%'). */
121
+ overlay_width?: string;
122
+ }
87
123
  export interface WatermarkOptions {
88
124
  /** 9-grid anchor position. */
89
125
  anchor?: WatermarkAnchor;
@@ -95,11 +131,38 @@ export interface WatermarkOptions {
95
131
  opacity?: number;
96
132
  /** Overlay width (e.g. '120px' or '20%'). */
97
133
  overlay_width?: string;
134
+ /**
135
+ * Per-overlay placement for the multi-overlay stack (contract `overlays[]`,
136
+ * v2.152.0) — one entry per overlay source, index-aligned; stacks up to 8
137
+ * overlays on one base image (z-order = array index). MUTUALLY EXCLUSIVE with
138
+ * the flat single-overlay options above; the server rejects mixing the two as
139
+ * `invalid_options`. image_watermark jpeg/png/webp bases only.
140
+ */
141
+ overlays?: WatermarkOverlay[];
98
142
  }
99
143
  /** Resize mode (contract `fit` enum, v2.97.0). */
100
144
  export type OutputFit = 'max' | 'crop' | 'scale';
101
- /** Metadata policy (contract `metadata` enum). Both values stable since v2.102.0 (keep/strip un-parked). */
102
- export type OutputMetadata = 'all' | 'keep';
145
+ /**
146
+ * Metadata policy (contract `metadata` enum). `strip` (default) removes all
147
+ * EXIF/IPTC/XMP; `keep` preserves them. Honored on same_format; PLANNED on
148
+ * format_change (v2.106.0). Renamed in contracts v2.107.0 — the legacy `all`
149
+ * token is now a DEPRECATED alias of `strip` (still accepted on the wire, emits
150
+ * a Deprecation header); use `strip`.
151
+ */
152
+ export type OutputMetadata = 'strip' | 'keep';
153
+ /**
154
+ * Compression mode on the optimiser (same_format) route (contract `encoding_mode`
155
+ * enum). `quality` (default) drives the encode by the quality slider; `target_size`
156
+ * targets a byte budget via the worker's encode-measure loop — STABLE since
157
+ * contracts v2.108.0 (jpeg/webp/avif).
158
+ */
159
+ export type OutputEncodingMode = 'quality' | 'target_size';
160
+ /** Chroma subsampling for JPEG output (contract `chroma_subsampling` enum, v2.110.0). `420` smallest → `444` highest fidelity. Honored: same_format jpeg only. */
161
+ export type OutputChromaSubsampling = '420' | '422' | '444';
162
+ /** ICC colour-profile handling (contract `color_profile` enum, v2.112.0). `keep` preserves the embedded profile; `srgb` converts to sRGB; `strip` removes it. Route/value availability is gated by the output lowering. */
163
+ export type OutputColorProfile = 'keep' | 'srgb' | 'strip';
164
+ /** Named quality preset (contract `quality_preset` enum, v2.148.0) — an alternative to the numeric `quality` slider. `best` highest fidelity → `low` smallest. Honored: same_format avif/jpeg/webp. */
165
+ export type OutputQualityPreset = 'best' | 'good' | 'fair' | 'low';
103
166
  /**
104
167
  * Options for the file-first `output()` image transform. The KEY SET is the
105
168
  * UNION of every image route's honored + planned option keys (image-output-routes
@@ -110,8 +173,16 @@ export type OutputMetadata = 'all' | 'keep';
110
173
  * `fit`) is honored on raster routes; `height` is optional (width-only resize).
111
174
  */
112
175
  export interface OutputOptions {
113
- /** Output quality for lossy formats (1-100). Honored: avif/jpeg/webp routes. */
176
+ /** Output quality (1-100). Honored on same-format avif/jpeg/png/webp and lossy format-change routes. */
114
177
  quality?: number;
178
+ /** Named quality preset (`best`/`good`/`fair`/`low`) — an alternative to the numeric `quality` slider (v2.148.0). Honored: same_format avif/jpeg/webp. */
179
+ quality_preset?: OutputQualityPreset;
180
+ /** Compression mode (same_format avif/jpeg/webp). `quality` (default) or `target_size` — STABLE since v2.108.0. */
181
+ encoding_mode?: OutputEncodingMode;
182
+ /** Target output size in bytes (≥1024) for `encoding_mode: 'target_size'`. STABLE since v2.108.0. Honored: same_format avif/jpeg/webp. */
183
+ target_size_bytes?: number;
184
+ /** Chroma subsampling for JPEG output. Honored: same_format jpeg only (v2.110.0). */
185
+ chroma_subsampling?: OutputChromaSubsampling;
115
186
  /** Resize target width in px (1-16384; width*height <= 16MP). */
116
187
  width?: number;
117
188
  /** Resize target height in px (optional — width-only resize preserves aspect). */
@@ -126,12 +197,14 @@ export interface OutputOptions {
126
197
  optimization_level?: number;
127
198
  /** AVIF encode speed. Honored: same_format avif only. */
128
199
  avif_speed?: number;
129
- /** Metadata policy. Honored: same_format routes (both `all` and `keep` since v2.102.0). */
200
+ /** Metadata policy (`strip` default / `keep`). Honored: same_format; PLANNED on format_change (v2.106.0). */
130
201
  metadata?: OutputMetadata;
202
+ /** ICC colour-profile handling (`keep`/`srgb`/`strip`). Availability depends on route and value. */
203
+ color_profile?: OutputColorProfile;
204
+ /** Auto-rotate per EXIF orientation. STABLE since v2.120.0 (both routes). */
205
+ auto_orient?: boolean;
131
206
  /** JPEG/WebP lossless. Honored: same_format jpeg/webp (stable since v2.101.0). */
132
207
  lossless?: boolean;
133
- /** Lossy PNG quantization. PLANNED (gated unavailable; licence-gated). */
134
- lossy?: boolean;
135
208
  }
136
209
  /**
137
210
  * The user-supplyable option keys per verb (excludes positional-owned keys).
@@ -139,9 +212,10 @@ export interface OutputOptions {
139
212
  * positional-owned keys equals the contract `operationOptionKeys(metadata)`.
140
213
  */
141
214
  export declare const VERB_OPTION_KEYS: {
142
- readonly convert: readonly ["quality", "background", "crf", "trim_start", "trim_end", "fps", "width", "max_colors", "loop", "dither", "bitrate", "pages", "dpi"];
143
- readonly thumbnail: readonly ["width", "height", "fit", "format", "quality", "timestamp", "source", "page"];
215
+ readonly convert: readonly ["quality", "background", "crf", "trim_start", "trim_end", "fps", "width", "height", "fit", "metadata", "color_profile", "auto_orient", "max_colors", "loop", "dither", "bitrate", "pages", "dpi"];
216
+ readonly thumbnail: readonly ["width", "height", "fit", "format", "quality", "background", "timestamp", "source", "page"];
217
+ readonly transform: readonly ["rotate", "flip"];
144
218
  readonly textWatermark: readonly ["font_size", "color", "font_family", "rotation", "watermark_mode", "tile_spacing", "anchor", "margin_x", "margin_y", "opacity"];
145
- readonly watermark: readonly ["anchor", "margin_x", "margin_y", "opacity", "overlay_width"];
146
- readonly output: readonly ["quality", "width", "height", "fit", "background", "progressive", "optimization_level", "avif_speed", "metadata", "lossless", "lossy"];
219
+ readonly watermark: readonly ["anchor", "margin_x", "margin_y", "opacity", "overlay_width", "overlays"];
220
+ readonly output: readonly ["quality", "quality_preset", "encoding_mode", "target_size_bytes", "chroma_subsampling", "width", "height", "fit", "background", "progressive", "optimization_level", "avif_speed", "metadata", "color_profile", "auto_orient", "lossless"];
147
221
  };
@@ -15,31 +15,35 @@
15
15
  * Mirrored by the PHP array-shape docblocks — keep in lockstep.
16
16
  */
17
17
  const CONVERT_OPTION_KEYS = [
18
- 'quality', 'background', 'crf', 'trim_start', 'trim_end', 'fps', 'width',
19
- 'max_colors', 'loop', 'dither', 'bitrate', 'pages', 'dpi',
18
+ 'quality', 'background', 'crf', 'trim_start', 'trim_end', 'fps', 'width', 'height', 'fit', 'metadata',
19
+ 'color_profile', 'auto_orient', 'max_colors', 'loop', 'dither', 'bitrate', 'pages', 'dpi',
20
20
  ];
21
21
  const THUMBNAIL_OPTION_KEYS = [
22
- 'width', 'height', 'fit', 'format', 'quality', 'timestamp', 'source', 'page',
22
+ 'width', 'height', 'fit', 'format', 'quality', 'background', 'timestamp', 'source', 'page',
23
23
  ];
24
+ const TRANSFORM_OPTION_KEYS = ['rotate', 'flip'];
24
25
  const TEXT_WATERMARK_OPTION_KEYS = [
25
26
  'font_size', 'color', 'font_family', 'rotation', 'watermark_mode',
26
27
  'tile_spacing', 'anchor', 'margin_x', 'margin_y', 'opacity',
27
28
  ];
28
29
  const WATERMARK_OPTION_KEYS = [
29
- 'anchor', 'margin_x', 'margin_y', 'opacity', 'overlay_width',
30
+ 'anchor', 'margin_x', 'margin_y', 'opacity', 'overlay_width', 'overlays',
30
31
  ];
31
32
  const OUTPUT_OPTION_KEYS = [
32
- 'quality', 'width', 'height', 'fit', 'background', 'progressive',
33
- 'optimization_level', 'avif_speed', 'metadata', 'lossless', 'lossy',
33
+ 'quality', 'quality_preset', 'encoding_mode', 'target_size_bytes', 'chroma_subsampling', 'width', 'height', 'fit',
34
+ 'background', 'progressive', 'optimization_level', 'avif_speed', 'metadata',
35
+ 'color_profile', 'auto_orient', 'lossless',
34
36
  ];
35
37
  const _convertKeysMatch = true;
36
38
  const _thumbnailKeysMatch = true;
39
+ const _transformKeysMatch = true;
37
40
  const _textWatermarkKeysMatch = true;
38
41
  const _watermarkKeysMatch = true;
39
42
  const _outputKeysMatch = true;
40
43
  // Reference the assertions so `noUnusedLocals` doesn't strip them.
41
44
  void _convertKeysMatch;
42
45
  void _thumbnailKeysMatch;
46
+ void _transformKeysMatch;
43
47
  void _textWatermarkKeysMatch;
44
48
  void _watermarkKeysMatch;
45
49
  void _outputKeysMatch;
@@ -51,6 +55,7 @@ void _outputKeysMatch;
51
55
  export const VERB_OPTION_KEYS = {
52
56
  convert: CONVERT_OPTION_KEYS,
53
57
  thumbnail: THUMBNAIL_OPTION_KEYS,
58
+ transform: TRANSFORM_OPTION_KEYS,
54
59
  textWatermark: TEXT_WATERMARK_OPTION_KEYS,
55
60
  watermark: WATERMARK_OPTION_KEYS,
56
61
  output: OUTPUT_OPTION_KEYS,
@@ -9,8 +9,8 @@ import { type OperationMetadata } from '@giveitsmaller/contracts/operations';
9
9
  * sidecars (the same contract-anchored source the wire-key conformance guard
10
10
  * uses), so it can never silently drift from the contract.
11
11
  *
12
- * SCOPE: `convert` / `thumbnail` / `textWatermark` / `watermark` only. `compress`
13
- * is deliberately EXCLUDED — its bag legitimately carries SDK-only keys
12
+ * SCOPE: `convert` / `thumbnail` / `textWatermark` / `watermark` / `output`
13
+ * only. `compress` is deliberately EXCLUDED — its bag legitimately carries SDK-only keys
14
14
  * (`optimize`, `presetOverrides`) and camelCase resolver aliases (`targetSize`,
15
15
  * `outputFormat`) that are not `operationOptionKeys(compressMetadata)`; it has its
16
16
  * own `unknown_field` validation through the preset resolver.
@@ -25,7 +25,7 @@ import { type OperationMetadata } from '@giveitsmaller/contracts/operations';
25
25
  */
26
26
  export declare function operationOptionKeys(metadata: OperationMetadata): ReadonlySet<string>;
27
27
  /** The ergonomic verbs whose option bags this module key-validates. */
28
- export type ValidatedVerb = 'convert' | 'thumbnail' | 'textWatermark' | 'watermark' | 'output';
28
+ export type ValidatedVerb = 'convert' | 'thumbnail' | 'transform' | 'textWatermark' | 'watermark' | 'output';
29
29
  /** Accessor for the conformance guard (pins these sets to the contract metadata). */
30
30
  export declare function allowedKeysFor(verb: ValidatedVerb): ReadonlySet<string>;
31
31
  /**
@@ -39,6 +39,24 @@ export declare function allowedKeysFor(verb: ValidatedVerb): ReadonlySet<string>
39
39
  * option set.
40
40
  */
41
41
  export declare function validateVerbOptions(verb: ValidatedVerb, options: object | null | undefined): void;
42
+ /**
43
+ * Validate the option bag for the SINGLE-OP builder `gisl().convert(input, options)`
44
+ * (ExVcchMz). DISTINCT from `validateVerbOptions('convert', ...)`, which is for the
45
+ * file-first `Recipe.convert(format, options)` where `output_format` is set by the
46
+ * positional `format` arg and is therefore positional-owned (rejected in the bag).
47
+ * The single-op builder has NO positional format — its target is carried in the bag
48
+ * as the wire key `output_format` — so this guard ALLOWS `output_format` (and only
49
+ * that; the SDK alias `format` is NOT accepted, the single-op bag lowers verbatim to
50
+ * the wire) while still rejecting any other unknown key, AND requires `output_format`
51
+ * to be present (a convert with no target is a guaranteed server 422). `format`, the
52
+ * SDK alias, is intentionally excluded so a caller using it gets a clear unknown-key
53
+ * error rather than a silent wire `format` the server 422s.
54
+ *
55
+ * @throws {GislConfigError} reason `unknown_field` for a key outside the convert
56
+ * contract set ∪ {output_format}; reason `missing_required_field` when
57
+ * `output_format` is absent/nullish.
58
+ */
59
+ export declare function validateSingleOpConvertOptions(options: object | null | undefined): void;
42
60
  /**
43
61
  * Assert thumbnail `width` AND `height` are both present and non-nullish (the
44
62
  * contract marks both `required` for image/video/document). The typed signature
@@ -1,4 +1,4 @@
1
- import { convertMetadata, thumbnailMetadata, textWatermarkMetadata, imageWatermarkMetadata, videoWatermarkMetadata, } from '@giveitsmaller/contracts/operations';
1
+ import { convertMetadata, thumbnailMetadata, transformMetadata, textWatermarkMetadata, imageWatermarkMetadata, videoWatermarkMetadata, } from '@giveitsmaller/contracts/operations';
2
2
  import { GislConfigError } from '../errors.js';
3
3
  import { VERB_OPTION_KEYS } from './option_types.js';
4
4
  /**
@@ -11,8 +11,8 @@ import { VERB_OPTION_KEYS } from './option_types.js';
11
11
  * sidecars (the same contract-anchored source the wire-key conformance guard
12
12
  * uses), so it can never silently drift from the contract.
13
13
  *
14
- * SCOPE: `convert` / `thumbnail` / `textWatermark` / `watermark` only. `compress`
15
- * is deliberately EXCLUDED — its bag legitimately carries SDK-only keys
14
+ * SCOPE: `convert` / `thumbnail` / `textWatermark` / `watermark` / `output`
15
+ * only. `compress` is deliberately EXCLUDED — its bag legitimately carries SDK-only keys
16
16
  * (`optimize`, `presetOverrides`) and camelCase resolver aliases (`targetSize`,
17
17
  * `outputFormat`) that are not `operationOptionKeys(compressMetadata)`; it has its
18
18
  * own `unknown_field` validation through the preset resolver.
@@ -51,6 +51,9 @@ function union(...sets) {
51
51
  const ALLOWED_KEYS = {
52
52
  convert: operationOptionKeys(convertMetadata),
53
53
  thumbnail: operationOptionKeys(thumbnailMetadata),
54
+ // transform is a passthrough verb (rotate/flip). The generic allowed set is
55
+ // the op-wide union {rotate, flip}; `flip`-on-PDF is narrowed server-side.
56
+ transform: operationOptionKeys(transformMetadata),
54
57
  textWatermark: operationOptionKeys(textWatermarkMetadata),
55
58
  watermark: union(operationOptionKeys(imageWatermarkMetadata), operationOptionKeys(videoWatermarkMetadata)),
56
59
  // `output` is the image Output facade — its allowed keys are the UNION of every
@@ -115,6 +118,36 @@ export function validateVerbOptions(verb, options) {
115
118
  }
116
119
  }
117
120
  }
121
+ /**
122
+ * Validate the option bag for the SINGLE-OP builder `gisl().convert(input, options)`
123
+ * (ExVcchMz). DISTINCT from `validateVerbOptions('convert', ...)`, which is for the
124
+ * file-first `Recipe.convert(format, options)` where `output_format` is set by the
125
+ * positional `format` arg and is therefore positional-owned (rejected in the bag).
126
+ * The single-op builder has NO positional format — its target is carried in the bag
127
+ * as the wire key `output_format` — so this guard ALLOWS `output_format` (and only
128
+ * that; the SDK alias `format` is NOT accepted, the single-op bag lowers verbatim to
129
+ * the wire) while still rejecting any other unknown key, AND requires `output_format`
130
+ * to be present (a convert with no target is a guaranteed server 422). `format`, the
131
+ * SDK alias, is intentionally excluded so a caller using it gets a clear unknown-key
132
+ * error rather than a silent wire `format` the server 422s.
133
+ *
134
+ * @throws {GislConfigError} reason `unknown_field` for a key outside the convert
135
+ * contract set ∪ {output_format}; reason `missing_required_field` when
136
+ * `output_format` is absent/nullish.
137
+ */
138
+ export function validateSingleOpConvertOptions(options) {
139
+ const o = (options ?? {});
140
+ const allowed = new Set([...ALLOWED_KEYS.convert, 'output_format']);
141
+ for (const key of Object.keys(o)) {
142
+ if (!allowed.has(key)) {
143
+ throw new GislConfigError(`convert: unknown option '${key}'. Valid options: ${[...allowed].sort().join(', ')}.`, { reason: 'unknown_field', conflictingFields: [key] });
144
+ }
145
+ }
146
+ if (o.output_format === undefined || o.output_format === null) {
147
+ throw new GislConfigError(`convert requires 'output_format' (the target format) in the options bag; ` +
148
+ `e.g. gisl().convert(input, { output_format: 'webp' }).`, { reason: 'missing_required_field', conflictingFields: ['output_format'] });
149
+ }
150
+ }
118
151
  /**
119
152
  * Assert thumbnail `width` AND `height` are both present and non-nullish (the
120
153
  * contract marks both `required` for image/video/document). The typed signature
@@ -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.4";
12
+ export declare const PRESET_VERSION: "1.6";
13
13
  /**
14
14
  * Inputs to {@link resolveCompressOptions}. `media` selects which leaf
15
15
  * DTO drives sdkDefault + clientDefault lookups + invalid-combo