@giveitsmaller/sdk 0.19.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.
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
  });
@@ -74,6 +74,12 @@ export interface ThumbnailOptions {
74
74
  /** 1-based page index for document input. */
75
75
  page?: number;
76
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
+ }
77
83
  export interface TextWatermarkOptions {
78
84
  /** Font size in pixels (8-512). */
79
85
  font_size?: number;
@@ -96,6 +102,24 @@ export interface TextWatermarkOptions {
96
102
  /** Overlay opacity (0-1). */
97
103
  opacity?: number;
98
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
+ }
99
123
  export interface WatermarkOptions {
100
124
  /** 9-grid anchor position. */
101
125
  anchor?: WatermarkAnchor;
@@ -107,6 +131,14 @@ export interface WatermarkOptions {
107
131
  opacity?: number;
108
132
  /** Overlay width (e.g. '120px' or '20%'). */
109
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[];
110
142
  }
111
143
  /** Resize mode (contract `fit` enum, v2.97.0). */
112
144
  export type OutputFit = 'max' | 'crop' | 'scale';
@@ -182,7 +214,8 @@ export interface OutputOptions {
182
214
  export declare const VERB_OPTION_KEYS: {
183
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"];
184
216
  readonly thumbnail: readonly ["width", "height", "fit", "format", "quality", "background", "timestamp", "source", "page"];
217
+ readonly transform: readonly ["rotate", "flip"];
185
218
  readonly textWatermark: readonly ["font_size", "color", "font_family", "rotation", "watermark_mode", "tile_spacing", "anchor", "margin_x", "margin_y", "opacity"];
186
- readonly watermark: readonly ["anchor", "margin_x", "margin_y", "opacity", "overlay_width"];
219
+ readonly watermark: readonly ["anchor", "margin_x", "margin_y", "opacity", "overlay_width", "overlays"];
187
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"];
188
221
  };
@@ -21,12 +21,13 @@ const CONVERT_OPTION_KEYS = [
21
21
  const THUMBNAIL_OPTION_KEYS = [
22
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
33
  'quality', 'quality_preset', 'encoding_mode', 'target_size_bytes', 'chroma_subsampling', 'width', 'height', 'fit',
@@ -35,12 +36,14 @@ const OUTPUT_OPTION_KEYS = [
35
36
  ];
36
37
  const _convertKeysMatch = true;
37
38
  const _thumbnailKeysMatch = true;
39
+ const _transformKeysMatch = true;
38
40
  const _textWatermarkKeysMatch = true;
39
41
  const _watermarkKeysMatch = true;
40
42
  const _outputKeysMatch = true;
41
43
  // Reference the assertions so `noUnusedLocals` doesn't strip them.
42
44
  void _convertKeysMatch;
43
45
  void _thumbnailKeysMatch;
46
+ void _transformKeysMatch;
44
47
  void _textWatermarkKeysMatch;
45
48
  void _watermarkKeysMatch;
46
49
  void _outputKeysMatch;
@@ -52,6 +55,7 @@ void _outputKeysMatch;
52
55
  export const VERB_OPTION_KEYS = {
53
56
  convert: CONVERT_OPTION_KEYS,
54
57
  thumbnail: THUMBNAIL_OPTION_KEYS,
58
+ transform: TRANSFORM_OPTION_KEYS,
55
59
  textWatermark: TEXT_WATERMARK_OPTION_KEYS,
56
60
  watermark: WATERMARK_OPTION_KEYS,
57
61
  output: OUTPUT_OPTION_KEYS,
@@ -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
  /**
@@ -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
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { AuthErrorResponse, AuthRejectionEnvelope, AuthRejectionEnvelopeErrorTypeEnum, BalanceExhaustedResponse, FeatureNotAvailableResponse, FeatureTierRestrictedResponse, ProbePendingResponse, TierRestrictionResponse, UploadDurationExceedsTierResponse, UploadSizeExceedsTierResponse, WorkflowExpiredResponse } from '@giveitsmaller/contracts/openapi';
2
+ import type { ErrorCategory } from './generated/sdk_spec/errors.js';
3
+ import type { RateLimitSnapshot } from './retry-metadata.js';
2
4
  export declare class GislError extends Error {
3
5
  constructor(message: string);
4
6
  }
@@ -14,6 +16,11 @@ export interface GislApiErrorOptions {
14
16
  readonly locale?: string;
15
17
  readonly messageParams?: Record<string, unknown>;
16
18
  readonly payload?: unknown;
19
+ /**
20
+ * The wire-stable machine error code (the response envelope's `error` field,
21
+ * SCREAMING_SNAKE, never localised). See {@link GislApiError.errorCode}.
22
+ */
23
+ readonly errorCode?: string;
17
24
  /**
18
25
  * The response headers from the HTTP response that produced this error.
19
26
  * Keys are LOWERCASED (HTTP header names are case-insensitive per RFC 9110,
@@ -33,6 +40,19 @@ export interface GislApiErrorOptions {
33
40
  export declare class GislApiError extends GislError {
34
41
  readonly statusCode: number;
35
42
  readonly errorMessage: string;
43
+ /**
44
+ * The wire-stable machine error code — the response envelope's `error` field
45
+ * (SCREAMING_SNAKE, never localised). DISTINCT from {@link errorMessage},
46
+ * which is the human `message`. Mirrors the PHP `GislApiError.errorCode`.
47
+ *
48
+ * Optional here (PHP's is a required field defaulting to `'unknown_error'`):
49
+ * a DELIBERATE optional-vs-sentinel divergence — `undefined` when the wire
50
+ * envelope carries no `error` (e.g. a non-JSON / invalid-JSON response). When
51
+ * the wire DOES carry `error`, both SDKs surface the same value. Machine
52
+ * dispatch still keys off the typed subclasses (`payload.errorType`); this is
53
+ * the flat machine code for a base `GislApiError` (e.g. a plain 404).
54
+ */
55
+ readonly errorCode?: string;
36
56
  readonly path?: string;
37
57
  readonly details?: unknown;
38
58
  readonly messageKey?: string;
@@ -53,6 +73,42 @@ export declare class GislApiError extends GislError {
53
73
  */
54
74
  readonly contentLanguage?: string;
55
75
  constructor(statusCode: number, errorMessage: string, path?: string, details?: unknown, options?: GislApiErrorOptions);
76
+ /**
77
+ * Resolve the generated `ERROR_CODES` entry for this error, SOURCE-AWARE
78
+ * (plan D1). ~9 registry codes are keyed by the `error_type` discriminator
79
+ * rather than the envelope `error` field, so try the typed discriminator
80
+ * FIRST (camel `errorType`, raw-snake `error_type` fallback), then fall back
81
+ * to the flat machine {@link errorCode}. Returns `undefined` when neither
82
+ * resolves — e.g. a bare base error whose payload carries no discriminator.
83
+ * NEVER throws on a missing payload / discriminator.
84
+ */
85
+ private resolveErrorEntry;
86
+ /**
87
+ * Whether retrying this request could plausibly succeed. `true` when the HTTP
88
+ * status is inherently retryable (408 / 429 / 5xx) OR the resolved taxonomy
89
+ * entry marks the code retryable (e.g. `probe_pending`). Note: logical OR
90
+ * (not `??`) — a 429 is retryable regardless of the taxonomy, and a
91
+ * registry-retryable code is retryable regardless of status.
92
+ */
93
+ get retryable(): boolean;
94
+ /**
95
+ * The taxonomy category for this error's machine code, from the generated
96
+ * `ERROR_CODES` registry, or `undefined` when the code isn't in the registry
97
+ * (e.g. a bare base error whose payload carries no discriminator).
98
+ */
99
+ get category(): ErrorCategory | undefined;
100
+ /**
101
+ * The rate-limit snapshot parsed from the `x-ratelimit-*` response headers,
102
+ * or `undefined` when they aren't all present as non-negative integers. Read
103
+ * this after a 429 to schedule a back-off.
104
+ */
105
+ get rateLimit(): RateLimitSnapshot | undefined;
106
+ /**
107
+ * The server-suggested back-off delay in whole seconds, parsed from the
108
+ * `Retry-After` response header, or `undefined` when absent / zero / past /
109
+ * malformed. Mirrors the retry-loop parser's semantics.
110
+ */
111
+ get retryAfterSeconds(): number | undefined;
56
112
  }
57
113
  /**
58
114
  * Shape of a single validation detail entry. Mirrors the v2
@@ -439,7 +495,7 @@ export type GislSinkErrorReason = 'not_single_output' | 'downloader_unavailable'
439
495
  /**
440
496
  * Thrown by the file-first `RunResult` sinks (`toFile()` / `downloadTo()`,
441
497
  * FF1) when they cannot deliver. The machine-readable `reason` discriminates
442
- * the three cases, mirroring the `reason`-bag convention on
498
+ * the six cases below, mirroring the `reason`-bag convention on
443
499
  * {@link GislConfigError}:
444
500
  *
445
501
  * - `not_single_output` — `toFile()` requires exactly one output but the
package/dist/errors.js CHANGED
@@ -1,3 +1,12 @@
1
+ // W8v4jWzx — the generated error-taxonomy registry stays INTERNAL to this
2
+ // module (only the `ErrorCategory` TYPE is re-exported from the public barrel).
3
+ import { ERROR_CODES } from './generated/sdk_spec/errors.js';
4
+ import { isApiRetryableStatus, rateLimitFromHeaders, retryAfterSecondsFromHeaders, } from './retry-metadata.js';
5
+ // Registry keys are lowercase_snake; normalise the wire code / discriminator
6
+ // (trim + lowercase) before looking it up in ERROR_CODES.
7
+ function normalizeErrorCode(rawCode) {
8
+ return rawCode.trim().toLowerCase();
9
+ }
1
10
  export class GislError extends Error {
2
11
  constructor(message) {
3
12
  super(message);
@@ -7,6 +16,19 @@ export class GislError extends Error {
7
16
  export class GislApiError extends GislError {
8
17
  statusCode;
9
18
  errorMessage;
19
+ /**
20
+ * The wire-stable machine error code — the response envelope's `error` field
21
+ * (SCREAMING_SNAKE, never localised). DISTINCT from {@link errorMessage},
22
+ * which is the human `message`. Mirrors the PHP `GislApiError.errorCode`.
23
+ *
24
+ * Optional here (PHP's is a required field defaulting to `'unknown_error'`):
25
+ * a DELIBERATE optional-vs-sentinel divergence — `undefined` when the wire
26
+ * envelope carries no `error` (e.g. a non-JSON / invalid-JSON response). When
27
+ * the wire DOES carry `error`, both SDKs surface the same value. Machine
28
+ * dispatch still keys off the typed subclasses (`payload.errorType`); this is
29
+ * the flat machine code for a base `GislApiError` (e.g. a plain 404).
30
+ */
31
+ errorCode;
10
32
  path;
11
33
  details;
12
34
  messageKey;
@@ -41,10 +63,69 @@ export class GislApiError extends GislError {
41
63
  this.locale = options.locale;
42
64
  this.messageParams = options.messageParams;
43
65
  this.payload = options.payload;
66
+ this.errorCode = options.errorCode;
44
67
  this.responseHeaders = options.responseHeaders;
45
68
  this.contentLanguage = options.contentLanguage;
46
69
  }
47
70
  }
71
+ /**
72
+ * Resolve the generated `ERROR_CODES` entry for this error, SOURCE-AWARE
73
+ * (plan D1). ~9 registry codes are keyed by the `error_type` discriminator
74
+ * rather than the envelope `error` field, so try the typed discriminator
75
+ * FIRST (camel `errorType`, raw-snake `error_type` fallback), then fall back
76
+ * to the flat machine {@link errorCode}. Returns `undefined` when neither
77
+ * resolves — e.g. a bare base error whose payload carries no discriminator.
78
+ * NEVER throws on a missing payload / discriminator.
79
+ */
80
+ resolveErrorEntry() {
81
+ const payload = this.payload;
82
+ const rawErrorType = payload?.errorType ?? payload?.error_type;
83
+ if (typeof rawErrorType === 'string') {
84
+ const byType = ERROR_CODES[normalizeErrorCode(rawErrorType)];
85
+ if (byType !== undefined)
86
+ return byType;
87
+ }
88
+ if (this.errorCode !== undefined) {
89
+ const byCode = ERROR_CODES[normalizeErrorCode(this.errorCode)];
90
+ if (byCode !== undefined)
91
+ return byCode;
92
+ }
93
+ return undefined;
94
+ }
95
+ /**
96
+ * Whether retrying this request could plausibly succeed. `true` when the HTTP
97
+ * status is inherently retryable (408 / 429 / 5xx) OR the resolved taxonomy
98
+ * entry marks the code retryable (e.g. `probe_pending`). Note: logical OR
99
+ * (not `??`) — a 429 is retryable regardless of the taxonomy, and a
100
+ * registry-retryable code is retryable regardless of status.
101
+ */
102
+ get retryable() {
103
+ return (isApiRetryableStatus(this.statusCode) || (this.resolveErrorEntry()?.retryable ?? false));
104
+ }
105
+ /**
106
+ * The taxonomy category for this error's machine code, from the generated
107
+ * `ERROR_CODES` registry, or `undefined` when the code isn't in the registry
108
+ * (e.g. a bare base error whose payload carries no discriminator).
109
+ */
110
+ get category() {
111
+ return this.resolveErrorEntry()?.category;
112
+ }
113
+ /**
114
+ * The rate-limit snapshot parsed from the `x-ratelimit-*` response headers,
115
+ * or `undefined` when they aren't all present as non-negative integers. Read
116
+ * this after a 429 to schedule a back-off.
117
+ */
118
+ get rateLimit() {
119
+ return rateLimitFromHeaders(this.responseHeaders);
120
+ }
121
+ /**
122
+ * The server-suggested back-off delay in whole seconds, parsed from the
123
+ * `Retry-After` response header, or `undefined` when absent / zero / past /
124
+ * malformed. Mirrors the retry-loop parser's semantics.
125
+ */
126
+ get retryAfterSeconds() {
127
+ return retryAfterSecondsFromHeaders(this.responseHeaders);
128
+ }
48
129
  }
49
130
  export class GislValidationError extends GislApiError {
50
131
  constructor(statusCode, errorMessage, details, path, options) {
@@ -479,7 +560,7 @@ export class GislResultNotReadyError extends GislError {
479
560
  /**
480
561
  * Thrown by the file-first `RunResult` sinks (`toFile()` / `downloadTo()`,
481
562
  * FF1) when they cannot deliver. The machine-readable `reason` discriminates
482
- * the three cases, mirroring the `reason`-bag convention on
563
+ * the six cases below, mirroring the `reason`-bag convention on
483
564
  * {@link GislConfigError}:
484
565
  *
485
566
  * - `not_single_output` — `toFile()` requires exactly one output but the