@giveitsmaller/sdk 0.20.0 → 0.21.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/client.js CHANGED
@@ -4,8 +4,8 @@
4
4
  // blobByteSource, which never touches these). Kept as a STATIC import (not a
5
5
  // dynamic one) so `vi.mock('node:fs/promises')` still intercepts it in tests.
6
6
  import { open, stat, basename } from './node-fs.js';
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
- import { GislAbortError, GislApiError, GislAuthError, GislAuthRejectionError, GislBalanceExhaustedError, GislError, GislFeatureNotAvailableError, GislFeatureTierRestrictedError, GislMultipartPartCountError, GislMultipartPartError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTierRestrictedError, GislTimeoutError, GislProbePendingError, GislUploadCapExceededError, GislValidationError, GislWorkflowExpiredError, } from './errors.js';
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, LongFormConcurrencyLimitResponseFromJSON, TierRestrictionKind, TierRestrictionResponseFromJSON, UserTier, WorkflowExpiredResponseFromJSON, ProbePendingResponseFromJSON, UploadSizeExceedsTierResponseFromJSON, UploadDurationExceedsTierResponseFromJSON, UploadConstraintsAppliedProcessingClassPreAssignmentEnum, UploadThresholdsSingleShotMaxBytesEnum, UploadThresholdsMultipartChunkSizeEnum, UploadThresholdsMultipartConcurrencyDefaultEnum, } from '@giveitsmaller/contracts/openapi';
8
+ import { GislAbortError, GislApiError, GislAuthError, GislAuthRejectionError, GislBalanceExhaustedError, GislError, GislFeatureNotAvailableError, GislFeatureTierRestrictedError, GislLongFormConcurrencyError, GislMultipartPartCountError, GislMultipartPartError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTierRestrictedError, GislTimeoutError, GislProbePendingError, GislUploadCapExceededError, GislValidationError, GislWorkflowExpiredError, } from './errors.js';
9
9
  // The `Retry-After` millisecond parser lives in the shared retry-metadata
10
10
  // module (extracted to break the client ↔ errors circular import); re-imported
11
11
  // here so the retry-loop timing stays byte-identical.
@@ -59,7 +59,25 @@ const S3_MAX_MULTIPART_PARTS = 10_000;
59
59
  // exists to prevent). codex review (high).
60
60
  const RECOMMENDED_CHUNK_SIZE_MAX_BYTES = 104_857_600; // 100 MiB
61
61
  const DEFAULT_POLL_INTERVAL_MS = 2_000;
62
- const DEFAULT_POLL_TIMEOUT_MS = 300_000; // 5 min
62
+ const DEFAULT_POLL_TIMEOUT_MS = 600_000; // 10 min
63
+ /**
64
+ * Re-tag a bare per-request transport {@link GislTimeoutError} with the workflow
65
+ * it was scoped to, so a timed-out workflow read (status / downloads) stays
66
+ * recoverable. No-op for any other error and for a timeout that already carries
67
+ * an id. See `oYumKo6y`. (PHP transport failures surface as `GislNetworkError`,
68
+ * so this enrichment is TS-only.)
69
+ */
70
+ function withWorkflowIdOnTimeout(err, workflowId) {
71
+ if (!(err instanceof GislTimeoutError) || err.workflowId !== undefined)
72
+ return err;
73
+ const enriched = new GislTimeoutError(err.message, workflowId);
74
+ // Preserve the original throw site + chain the cause — the enriched error is
75
+ // a re-tag, not a new failure, so diagnostics should still point at the
76
+ // transport timeout.
77
+ enriched.stack = err.stack;
78
+ enriched.cause = err;
79
+ return enriched;
80
+ }
63
81
  // Anonymous-read capability header. An anonymous (null-owner) workflow create
64
82
  // returns a one-time `cap` token (WorkflowCreateResponse.cap); the session-less
65
83
  // caller passes it back on status/downloads/events reads via this header so the
@@ -98,6 +116,18 @@ function headersToRecord(headers) {
98
116
  });
99
117
  return r;
100
118
  }
119
+ // Canonical human-readable fallback when a failure envelope carries NO `message`
120
+ // field. Kept IDENTICAL to the PHP SDK (`GislClient::fallbackErrorMessage`) so a
121
+ // message-absent error surfaces byte-identical `.message` text across SDKs
122
+ // (card U7MACpOj). Used by BOTH error-construction paths (`handleResponse` and
123
+ // the `getSchema` raw-response path) so they cannot drift. The machine code
124
+ // stays on `.errorCode` — this string is human DISPLAY text only; never parse
125
+ // it. `error` absent → `unknown_error` (PHP parity). Historically TS leaked the
126
+ // SCREAMING_SNAKE `error` code as `.message`, diverging from PHP's synthetic
127
+ // sentence; this unifies them on the human-shaped form.
128
+ function fallbackErrorMessage(status, errorCode) {
129
+ return `Request failed with status ${status} (${errorCode ?? 'unknown_error'}).`;
130
+ }
101
131
  function isValidationDetails(value) {
102
132
  return (Array.isArray(value) &&
103
133
  value.length > 0 &&
@@ -484,13 +514,16 @@ export class GislClient {
484
514
  };
485
515
  // Human-readable text comes from `message` (the I26 localised field).
486
516
  // `error` is the stable, never-localised SCREAMING_SNAKE machine code —
487
- // NOT display text. Surfacing `error` as the thrown error's `.message`
488
- // regressed consumers that render the human string (x9Lbf6uy). Fall back
489
- // to `error` when `message` is absent (deployed contract guarantees
490
- // `message` on conforming error envelopes). Machine dispatch keys off
491
- // `error_type` (below), unchanged.
517
+ // NOT display text (surfacing it as `.message` regressed consumers that
518
+ // render the human string, x9Lbf6uy). When `message` is absent (the
519
+ // deployed contract guarantees it on conforming error envelopes), fall
520
+ // back to the canonical synthetic sentence byte-identical to the PHP
521
+ // SDK — instead of leaking the machine code (card U7MACpOj). The code is
522
+ // still carried on `.errorCode`. Machine dispatch keys off `error_type`
523
+ // (below), unchanged.
492
524
  const status = response.status;
493
- const errorMessage = json.message ?? json.error ?? 'Unknown error';
525
+ const errorMessage = json.message ??
526
+ fallbackErrorMessage(status, typeof json.error === 'string' ? json.error : undefined);
494
527
  // Validation-details branch first — preserve existing shape so callers
495
528
  // matching on `instanceof GislValidationError` keep working.
496
529
  if (isValidationDetails(json.details)) {
@@ -595,6 +628,15 @@ export class GislClient {
595
628
  tryThrowCap(UploadDurationExceedsTierResponseFromJSON, 'duration_tier', (p) => isInEnum(p.currentTier, UserTier) &&
596
629
  typeof p.maxDurationSeconds === 'number');
597
630
  }
631
+ // Long-form concurrency limit (429) — a TIER quota, DISTINCT from a
632
+ // generic infra rate-limit 429. Dispatched on the machine `error` CODE
633
+ // (this envelope carries NO `error_type`); a generic rate-limit 429 has a
634
+ // different/absent code, so it falls through to the base GislApiError
635
+ // where `retryAfterSeconds` applies. The validator re-asserts the code so
636
+ // a malformed envelope falls through rather than mis-typing.
637
+ if (status === 429 && json.error === 'LONG_FORM_CONCURRENCY_LIMIT_EXCEEDED') {
638
+ tryThrowStructured(LongFormConcurrencyLimitResponseFromJSON, GislLongFormConcurrencyError, (p) => p.error === 'LONG_FORM_CONCURRENCY_LIMIT_EXCEEDED');
639
+ }
598
640
  // 413 = the absolute across-tier cap. The contract models 413 as a
599
641
  // plain `ErrorEnvelope` (no `error_type` discriminator, no typed
600
642
  // payload — api.yaml), so dispatch purely on status with no FromJSON
@@ -1603,10 +1645,15 @@ export class GislClient {
1603
1645
  * authenticated reads. A wrong/missing cap on a null-owner workflow is a 404.
1604
1646
  */
1605
1647
  async getWorkflowStatus(workflowId, opts = {}) {
1606
- return this.request('GET', `/api/workflows/${encodeURIComponent(workflowId)}/status`, {
1607
- deserialize: WorkflowStatusResponseFromJSON,
1608
- headers: workflowCapabilityHeaders(opts.capability),
1609
- });
1648
+ try {
1649
+ return await this.request('GET', `/api/workflows/${encodeURIComponent(workflowId)}/status`, {
1650
+ deserialize: WorkflowStatusResponseFromJSON,
1651
+ headers: workflowCapabilityHeaders(opts.capability),
1652
+ });
1653
+ }
1654
+ catch (err) {
1655
+ throw withWorkflowIdOnTimeout(err, workflowId);
1656
+ }
1610
1657
  }
1611
1658
  /**
1612
1659
  * Poll until the workflow reaches a terminal status.
@@ -1624,7 +1671,7 @@ export class GislClient {
1624
1671
  return status;
1625
1672
  }
1626
1673
  if (Date.now() + intervalMs > deadline) {
1627
- throw new GislTimeoutError(`Workflow ${workflowId} did not complete within ${timeoutMs}ms`);
1674
+ throw new GislTimeoutError(`Workflow ${workflowId} did not complete within ${timeoutMs}ms`, workflowId);
1628
1675
  }
1629
1676
  await new Promise((resolve) => setTimeout(resolve, intervalMs));
1630
1677
  }
@@ -1681,10 +1728,15 @@ export class GislClient {
1681
1728
  * authenticated reads. A wrong/missing cap on a null-owner workflow is a 404.
1682
1729
  */
1683
1730
  async getWorkflowDownloads(workflowId, opts = {}) {
1684
- return this.request('GET', `/api/workflows/${encodeURIComponent(workflowId)}/downloads`, {
1685
- deserialize: WorkflowDownloadResponseFromJSON,
1686
- headers: workflowCapabilityHeaders(opts.capability),
1687
- });
1731
+ try {
1732
+ return await this.request('GET', `/api/workflows/${encodeURIComponent(workflowId)}/downloads`, {
1733
+ deserialize: WorkflowDownloadResponseFromJSON,
1734
+ headers: workflowCapabilityHeaders(opts.capability),
1735
+ });
1736
+ }
1737
+ catch (err) {
1738
+ throw withWorkflowIdOnTimeout(err, workflowId);
1739
+ }
1688
1740
  }
1689
1741
  /**
1690
1742
  * Stream SSE events for a workflow. Returns an async iterable.
@@ -1862,22 +1914,26 @@ export class GislClient {
1862
1914
  return { notModified: true, etag, lastModified };
1863
1915
  }
1864
1916
  if (!response.ok) {
1865
- let errorMessage = 'Unknown error';
1866
1917
  let errorCode;
1918
+ // Seed with the canonical synthetic sentence (unknown_error) so the
1919
+ // message-absent JSON branch below reuses the SAME helper as
1920
+ // handleResponse + PHP — that message-absent JSON envelope is the
1921
+ // U7MACpOj parity target. A non-JSON body is a rare edge that keeps
1922
+ // this seed (it does NOT claim byte-parity with PHP's non-JSON path,
1923
+ // which throws a distinct GislError).
1924
+ let errorMessage = fallbackErrorMessage(response.status, undefined);
1867
1925
  try {
1868
1926
  const errJson = (await response.json());
1869
- // Prefer the human `message`; `error` is the machine code (x9Lbf6uy).
1870
- if (errJson.message)
1871
- errorMessage = errJson.message;
1872
- else if (errJson.error)
1873
- errorMessage = errJson.error;
1874
1927
  // Surface the machine code as errorCode too (parity with handleResponse
1875
1928
  // + PHP), even when `message` supplied the human text.
1876
1929
  if (typeof errJson.error === 'string')
1877
1930
  errorCode = errJson.error;
1931
+ // Prefer the human `message`; else the canonical synthetic sentence —
1932
+ // NOT the raw machine code (x9Lbf6uy / U7MACpOj).
1933
+ errorMessage = errJson.message ?? fallbackErrorMessage(response.status, errorCode);
1878
1934
  }
1879
1935
  catch {
1880
- // Non-JSON body — keep generic message, no machine code.
1936
+ // Non-JSON body — keep the generic synthetic message, no machine code.
1881
1937
  }
1882
1938
  // This throw is OUTSIDE handleResponse (rawResponse:true / 304 path), so
1883
1939
  // build the response-header surface from the in-scope `response` here.
@@ -1949,15 +2005,18 @@ export class GislClient {
1949
2005
  * via that cookie when the client is configured with
1950
2006
  * `useSessionCookie: true`.
1951
2007
  *
1952
- * Failure modes per ticket FX6mbTJD:
1953
- * - **401** `invalid_credentials` (collapsed with unverified
1954
- * accounts for anti-enumeration) `GislAuthError`.
1955
- * - **403** account-state failures (`account_locked`,
1956
- * `account_disabled`, `account_deleted`,
1957
- * `account_deletion_expired`) → `GislAuthError`.
2008
+ * Failure modes per ticket FX6mbTJD (login narrowed at contracts
2009
+ * v2.166.0 authsec no 403 account-state branch on login):
2010
+ * - **401** `invalid_credentials` (wrong password, unverified, OR
2011
+ * unknown account all collapsed for anti-enumeration)
2012
+ * `GislAuthError`.
1958
2013
  * - **429** infrastructure rate-limit → `GislApiError` with
1959
2014
  * the `Retry-After` header echoed on the response.
1960
2015
  *
2016
+ * The account-status error types (`account_locked` / `account_disabled`
2017
+ * / `account_deleted` / `account_deletion_expired`) still exist but are
2018
+ * emitted on the API-key path + live-session enforcement, not on login.
2019
+ *
1961
2020
  * Node session persistence (cookie-jar across processes) is out of
1962
2021
  * scope — this method only touches the request side.
1963
2022
  */
@@ -87,4 +87,34 @@ export declare function resolveOutputRoute(inputToken: string, outputFormat: str
87
87
  * option / value / group is unknown (no gate).
88
88
  */
89
89
  export declare function isPlannedValue(inputToken: string, optionKey: string, value: unknown): boolean;
90
+ /**
91
+ * Compress-route enum members per image mime-group, mirroring the shipped
92
+ * `availability/availability.json` `operations.compress.mime_groups.<group>.
93
+ * options.<opt>.values`. Kept as a hand table (NOT a runtime read of the ~238KB
94
+ * availability sidecar) so the enum-membership gate stays browser-safe, exactly
95
+ * like {@link IMAGE_OUTPUT_ROUTES} — and, crucially, so the gate has NO
96
+ * dependency on a contracts version that carries the enum in a compact form (a
97
+ * generated-metadata `values` field would fail open on an older published
98
+ * `@giveitsmaller/contracts`). PINNED to `availability.json` by
99
+ * `output-route-conformance.test.ts`; a contract regen that adds/changes an
100
+ * enum member fails there. Mirrored by PHP `ImageOutputRoutes::COMPRESS_OPTION_VALUES`.
101
+ *
102
+ * `image_svg`/`image_avif` carry the NARROW `metadata: ['strip','all']` (no
103
+ * `keep`) — the reason a value gate that consulted only the generic `image`
104
+ * group (`['strip','keep','all']`) let `metadata: 'keep'` reach a server 422 on
105
+ * those bases (rtkzl9gr). `output_format` is listed for a faithful projection
106
+ * mirror but is never gated here (the Output lowering owns it positionally).
107
+ */
108
+ export declare const COMPRESS_OPTION_VALUES: Readonly<Record<string, Readonly<Record<string, readonly string[]>>>>;
109
+ /**
110
+ * Whether a VALUE lies OUTSIDE the option's compress-route enum for the given
111
+ * input format — the pre-upload enum-membership gate (rtkzl9gr). Reads the hand
112
+ * {@link COMPRESS_OPTION_VALUES} table. Returns false when the option is not an
113
+ * enum on this group (no entry), so a non-enum option (e.g. integer `quality`)
114
+ * is never gated. Meaningful only on the same_format (compress) route, where
115
+ * the compress option enums definitionally apply. Membership is STRICT: a value
116
+ * whose type differs from the string enum members (e.g. numeric `420`) is
117
+ * treated as unknown rather than coerced to a match.
118
+ */
119
+ export declare function isUnknownEnumValue(inputToken: string, optionKey: string, value: unknown): boolean;
90
120
  export {};
@@ -29,8 +29,18 @@
29
29
  import { compressMetadata } from '@giveitsmaller/contracts/operations';
30
30
  /** The resize option keys — input-keyed, raster-only (see module doc). */
31
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);
32
+ /**
33
+ * Options whose availability follows the INPUT format's raster capability, not
34
+ * the output format — resize (`width`/`height`/`fit`) plus `auto_orient`. The
35
+ * projection lists them on every `format_change` cell (keyed by OUTPUT), so on
36
+ * a format change they must be re-gated against the INPUT's `same_format` cell:
37
+ * a raster input carries them, an SVG (vector) input does not. Before rtkzl9gr
38
+ * only the resize keys were input-gated, so `auto_orient` leaked onto the
39
+ * `svg → raster` route and was rejected server-side.
40
+ */
41
+ const INPUT_GATED_KEYS = [...RESIZE_KEYS, 'auto_orient'];
42
+ /** Set form of {@link INPUT_GATED_KEYS} for `string`-keyed membership tests. */
43
+ const INPUT_GATED_KEY_SET = new Set(INPUT_GATED_KEYS);
34
44
  /** Image area cap shared by every resizable route (projection `max_output_pixels`). */
35
45
  export const MAX_OUTPUT_PIXELS = 16_000_000;
36
46
  /**
@@ -117,21 +127,22 @@ export function resolveOutputRoute(inputToken, outputFormat) {
117
127
  const cell = IMAGE_OUTPUT_ROUTES.format_change[outToken];
118
128
  if (cell === undefined)
119
129
  return undefined;
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));
130
+ // Resize + auto_orient are INPUT-gated (see {@link INPUT_GATED_KEYS}). Since
131
+ // v2.103.0 convert is the resize engine, so the projection lists width/height/
132
+ // fit AND auto_orient on EVERY format_change cell but an SVG INPUT cannot be
133
+ // raster-resized or auto-oriented (the convert worker rejects it). So strip
134
+ // the cell's input-gated keys and re-add only those the INPUT's same_format
135
+ // cell honors: raster inputs carry them, svg does not. The transcoder options
136
+ // (output_format/quality/background/color_profile) ride the cell directly.
137
+ const transcoderHonored = cell.honored.filter((k) => !INPUT_GATED_KEY_SET.has(k));
127
138
  const inCell = IMAGE_OUTPUT_ROUTES.same_format[inputToken];
128
- const resize = inCell ? RESIZE_KEYS.filter((k) => inCell.honored.includes(k)) : [];
139
+ const inputGated = inCell ? INPUT_GATED_KEYS.filter((k) => inCell.honored.includes(k)) : [];
129
140
  return {
130
141
  route: 'format_change',
131
142
  sourceOp: 'convert',
132
143
  outputFormatWire: outToken,
133
144
  inputToken,
134
- honored: new Set([...transcoderHonored, ...resize]),
145
+ honored: new Set([...transcoderHonored, ...inputGated]),
135
146
  planned: new Set(cell.planned),
136
147
  };
137
148
  }
@@ -161,3 +172,63 @@ export function isPlannedValue(inputToken, optionKey, value) {
161
172
  const entry = opt.per_value_availability[String(value)];
162
173
  return entry?.availability === 'planned';
163
174
  }
175
+ /**
176
+ * Compress-route enum members per image mime-group, mirroring the shipped
177
+ * `availability/availability.json` `operations.compress.mime_groups.<group>.
178
+ * options.<opt>.values`. Kept as a hand table (NOT a runtime read of the ~238KB
179
+ * availability sidecar) so the enum-membership gate stays browser-safe, exactly
180
+ * like {@link IMAGE_OUTPUT_ROUTES} — and, crucially, so the gate has NO
181
+ * dependency on a contracts version that carries the enum in a compact form (a
182
+ * generated-metadata `values` field would fail open on an older published
183
+ * `@giveitsmaller/contracts`). PINNED to `availability.json` by
184
+ * `output-route-conformance.test.ts`; a contract regen that adds/changes an
185
+ * enum member fails there. Mirrored by PHP `ImageOutputRoutes::COMPRESS_OPTION_VALUES`.
186
+ *
187
+ * `image_svg`/`image_avif` carry the NARROW `metadata: ['strip','all']` (no
188
+ * `keep`) — the reason a value gate that consulted only the generic `image`
189
+ * group (`['strip','keep','all']`) let `metadata: 'keep'` reach a server 422 on
190
+ * those bases (rtkzl9gr). `output_format` is listed for a faithful projection
191
+ * mirror but is never gated here (the Output lowering owns it positionally).
192
+ */
193
+ export const COMPRESS_OPTION_VALUES = {
194
+ image: { color_profile: ['keep', 'srgb', 'strip'], fit: ['max', 'crop', 'scale'], metadata: ['strip', 'keep', 'all'], output_format: ['original', 'webp', 'auto', 'smallest'] },
195
+ image_jpeg: { chroma_subsampling: ['420', '422', '444'], color_profile: ['keep', 'srgb', 'strip'], encoding_mode: ['quality', 'target_size', 'auto_quality'], fit: ['max', 'crop', 'scale'], metadata: ['strip', 'keep', 'all'], output_format: ['original', 'webp', 'auto', 'smallest'], quality_preset: ['best', 'good', 'fair', 'low'] },
196
+ image_png: { color_profile: ['keep', 'srgb', 'strip'], fit: ['max', 'crop', 'scale'], metadata: ['strip', 'keep', 'all'], output_format: ['original', 'webp', 'auto', 'smallest'] },
197
+ image_avif: { color_profile: ['keep', 'srgb', 'strip'], encoding_mode: ['quality', 'target_size', 'auto_quality'], fit: ['max', 'crop', 'scale'], metadata: ['strip', 'all'], output_format: ['original', 'webp', 'auto', 'smallest'], quality_preset: ['best', 'good', 'fair', 'low'] },
198
+ image_svg: { metadata: ['strip', 'all'], output_format: ['original', 'webp', 'auto', 'smallest'] },
199
+ image_webp: { color_profile: ['keep', 'srgb', 'strip'], encoding_mode: ['quality', 'target_size', 'auto_quality'], fit: ['max', 'crop', 'scale'], metadata: ['strip', 'keep', 'all'], output_format: ['original', 'webp', 'auto', 'smallest'], quality_preset: ['best', 'good', 'fair', 'low'] },
200
+ };
201
+ /**
202
+ * The compress mime-group whose enum members are authoritative for an image
203
+ * token's SAME_FORMAT route — the exact `image_<token>` group when
204
+ * {@link COMPRESS_OPTION_VALUES} carries one, else the generic `image` group
205
+ * (gif/tiff).
206
+ *
207
+ * Deliberately DISTINCT from {@link compressGroupForToken} (which the planned
208
+ * gate uses). The planned gate routes webp/gif/svg/tiff through the generic
209
+ * `image` group, where cross-format `planned` markers live (e.g. `srgb`).
210
+ * Enum MEMBERSHIP is the opposite: it needs the format-specific enum, because
211
+ * `image_svg`'s `metadata` enum is the narrow `[strip, all]` while the generic
212
+ * group's is `[strip, keep, all]` — so only the specific group rejects
213
+ * `metadata: 'keep'` on SVG (and AVIF, which already maps specifically).
214
+ */
215
+ function enumGroupForToken(token) {
216
+ const specific = `image_${token}`;
217
+ return COMPRESS_OPTION_VALUES[specific] !== undefined ? specific : 'image';
218
+ }
219
+ /**
220
+ * Whether a VALUE lies OUTSIDE the option's compress-route enum for the given
221
+ * input format — the pre-upload enum-membership gate (rtkzl9gr). Reads the hand
222
+ * {@link COMPRESS_OPTION_VALUES} table. Returns false when the option is not an
223
+ * enum on this group (no entry), so a non-enum option (e.g. integer `quality`)
224
+ * is never gated. Meaningful only on the same_format (compress) route, where
225
+ * the compress option enums definitionally apply. Membership is STRICT: a value
226
+ * whose type differs from the string enum members (e.g. numeric `420`) is
227
+ * treated as unknown rather than coerced to a match.
228
+ */
229
+ export function isUnknownEnumValue(inputToken, optionKey, value) {
230
+ const members = COMPRESS_OPTION_VALUES[enumGroupForToken(inputToken)]?.[optionKey];
231
+ if (members === undefined)
232
+ return false;
233
+ return !(typeof value === 'string' && members.includes(value));
234
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ResolvedOptions } from '../builder.js';
2
2
  import type { OptimizeFor } from '../generated/sdk_spec/enums.js';
3
- import { type PresetDefaults, type PresetMedia, type PresetOp } from './presets/index.js';
3
+ import { type PresetDefaults, type PresetMedia, type DetectedMedia, type PresetOp } from './presets/index.js';
4
4
  /**
5
5
  * The preset matrix version emitted on every resolve. Re-exported from the
6
6
  * GENERATED `sdk_spec/version.ts` (source of truth: contracts
@@ -17,7 +17,7 @@ export declare const PRESET_VERSION: "1.6";
17
17
  * extend the union.
18
18
  */
19
19
  export interface ResolveCompressOptionsInput {
20
- readonly media: PresetMedia;
20
+ readonly media: DetectedMedia;
21
21
  readonly op: PresetOp;
22
22
  /** Defaults registered via `gisl.create({ presetDefaults: ... })`. */
23
23
  readonly presetDefaults?: PresetDefaults;
@@ -37,7 +37,7 @@
37
37
  import { sha256Hex } from '../sha256.js';
38
38
  import { GislConfigError } from '../errors.js';
39
39
  import { PRESET_VERSION as GENERATED_PRESET_VERSION } from '../generated/sdk_spec/version.js';
40
- import { ImageCompressPresetOptions, AudioCompressPresetOptions, VideoCompressPresetOptions, DocumentPdfCompressPresetOptions, DocumentOfficeCompressPresetOptions, DocumentOdfCompressPresetOptions, DocumentEpubCompressPresetOptions, definedFieldsOf, } from './presets/index.js';
40
+ import { ImageCompressPresetOptions, AudioCompressPresetOptions, VideoCompressPresetOptions, DocumentOfficeCompressPresetOptions, DocumentOdfCompressPresetOptions, DocumentEpubCompressPresetOptions, definedFieldsOf, } from './presets/index.js';
41
41
  /**
42
42
  * The preset matrix version emitted on every resolve. Re-exported from the
43
43
  * GENERATED `sdk_spec/version.ts` (source of truth: contracts
@@ -175,8 +175,6 @@ function sdkDefaultRecord(media, op, optimize) {
175
175
  return { ...AudioCompressPresetOptions.shippedDefaultsFor(optimize) };
176
176
  case 'video':
177
177
  return { ...VideoCompressPresetOptions.shippedDefaultsFor(optimize) };
178
- case 'document_pdf':
179
- return { ...DocumentPdfCompressPresetOptions.shippedDefaultsFor(optimize) };
180
178
  case 'document_office':
181
179
  return { ...DocumentOfficeCompressPresetOptions.shippedDefaultsFor(optimize) };
182
180
  case 'document_odf':
@@ -213,9 +211,6 @@ function presetDefaultsCellRecord(defaults, media, op, optimize) {
213
211
  case 'video':
214
212
  cell = defaults.cellFor('video', 'compress', optimize);
215
213
  break;
216
- case 'document_pdf':
217
- cell = defaults.cellFor('document_pdf', 'compress', optimize);
218
- break;
219
214
  case 'document_office':
220
215
  cell = defaults.cellFor('document_office', 'compress', optimize);
221
216
  break;
@@ -252,7 +247,6 @@ const MEDIA_FIELDS = Object.freeze({
252
247
  image: new Set(['quality', 'metadata', 'outputFormat']),
253
248
  audio: new Set(['bitrate', 'channels', 'sampleRate', 'normalize']),
254
249
  video: new Set(['codec', 'targetSize', 'crf', 'preset', 'width', 'height', 'fit', 'fps', 'faststart', 'audioCodec', 'audioBitrate']),
255
- document_pdf: new Set(['profile', 'grayscale']),
256
250
  document_office: new Set(['stripMacros', 'stripHiddenData', 'stripUnusedFonts']),
257
251
  document_odf: new Set(['stripMetadata', 'stripUnusedStyles']),
258
252
  document_epub: new Set(['fontSubsetting', 'stripUnusedCss']),
@@ -278,9 +272,9 @@ function detectMismatchedOverrides(media, overrides) {
278
272
  const otherSet = MEDIA_FIELDS[otherMedia];
279
273
  if (unknownFields.every((k) => otherSet.has(k))) {
280
274
  // PascalCase every underscore-separated segment so multi-segment
281
- // media (`document_pdf` → `DocumentPdf…`) emit the actual exported
275
+ // media (`document_office` → `DocumentOffice…`) emit the actual exported
282
276
  // class name (code-review MEDIUM: previously emitted
283
- // `Documentpdf…` which doesn't resolve in user code).
277
+ // `Documentoffice…` which doesn't resolve in user code).
284
278
  const className = otherMedia
285
279
  .split('_')
286
280
  .map((s) => s.charAt(0).toUpperCase() + s.slice(1))
@@ -321,7 +315,6 @@ export const KNOWN_WIRE_FIELDS = Object.freeze({
321
315
  image: new Set(['quality', 'metadata', 'output_format']),
322
316
  audio: new Set(['bitrate', 'channels', 'sample_rate', 'normalize', 'trim_start', 'trim_end']),
323
317
  video: new Set(['codec', 'encoding_mode', 'crf', 'target_size_bytes', 'preset', 'width', 'height', 'fit', 'fps', 'faststart', 'audio_codec', 'audio_bitrate', 'trim_start', 'trim_end']),
324
- document_pdf: new Set(['profile', 'grayscale']),
325
318
  document_office: new Set(['strip_macros', 'strip_hidden_data', 'strip_unused_fonts']),
326
319
  document_odf: new Set(['strip_metadata', 'strip_unused_styles']),
327
320
  document_epub: new Set(['font_subsetting', 'strip_unused_css']),
@@ -444,6 +437,9 @@ function computePresetConfigHash(clientDefault, scopedDefault, callPresetOverrid
444
437
  * `optimize` unset ⇒ layer 1 contributes nothing; `resolvedOptions.preset = null`.
445
438
  */
446
439
  export function resolveCompressOptions(input) {
440
+ if (input.media === 'document_pdf') {
441
+ throw new GislConfigError('PDF compression was removed at contracts v2.166.0; convert() / transform() still accept PDF.', { reason: 'unsupported_media' });
442
+ }
447
443
  const { media, op, presetDefaults, scopedPresetDefaults, presetOverrides, optimize, explicitOptions, audioLossless } = input;
448
444
  if (op !== 'compress') {
449
445
  throw new GislConfigError(`Preset resolution is only wired for compress operations today; got op='${op}'.`, { reason: 'unsupported_op' });
@@ -2,27 +2,31 @@ import { OptimizeFor } from '../../generated/sdk_spec/enums.js';
2
2
  import { ImageCompressPresetOptions, type ImageCompressPresetOptionsInput } from './image_compress.js';
3
3
  import { AudioCompressPresetOptions, type AudioCompressPresetOptionsInput } from './audio_compress.js';
4
4
  import { VideoCompressPresetOptions, type VideoCompressPresetOptionsInput } from './video_compress.js';
5
- import { DocumentPdfCompressPresetOptions, type DocumentPdfCompressPresetOptionsInput } from './document_pdf_compress.js';
6
5
  import { DocumentOfficeCompressPresetOptions, type DocumentOfficeCompressPresetOptionsInput } from './document_office_compress.js';
7
6
  import { DocumentOdfCompressPresetOptions, type DocumentOdfCompressPresetOptionsInput } from './document_odf_compress.js';
8
7
  import { DocumentEpubCompressPresetOptions, type DocumentEpubCompressPresetOptionsInput } from './document_epub_compress.js';
9
8
  export { ImageCompressPresetOptions, type ImageCompressPresetOptionsInput, } from './image_compress.js';
10
9
  export { AudioCompressPresetOptions, type AudioCompressPresetOptionsInput, } from './audio_compress.js';
11
10
  export { VideoCompressPresetOptions, type VideoCompressPresetOptionsInput, } from './video_compress.js';
12
- export { DocumentPdfCompressPresetOptions, type DocumentPdfCompressPresetOptionsInput, } from './document_pdf_compress.js';
13
11
  export { DocumentOfficeCompressPresetOptions, type DocumentOfficeCompressPresetOptionsInput, } from './document_office_compress.js';
14
12
  export { DocumentOdfCompressPresetOptions, type DocumentOdfCompressPresetOptionsInput, } from './document_odf_compress.js';
15
13
  export { DocumentEpubCompressPresetOptions, type DocumentEpubCompressPresetOptionsInput, } from './document_epub_compress.js';
16
- export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, PdfProfile, PdfColorspace, } from '../../generated/sdk_spec/enums.js';
14
+ export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, } from '../../generated/sdk_spec/enums.js';
17
15
  /** Supported media×op pairs for preset cells in T4a. Compress-only. */
18
- export type PresetMedia = 'image' | 'audio' | 'video' | 'document_pdf' | 'document_office' | 'document_odf' | 'document_epub';
16
+ export type PresetMedia = 'image' | 'audio' | 'video' | 'document_office' | 'document_odf' | 'document_epub';
17
+ /**
18
+ * Media the file-first detector can identify — the compressible
19
+ * `PresetMedia` set PLUS `document_pdf`, which is detectable (and a valid
20
+ * watermark-reject / convert / transform base) but NOT compressible.
21
+ */
22
+ export type DetectedMedia = PresetMedia | 'document_pdf';
19
23
  export type PresetOp = 'compress';
20
24
  /**
21
25
  * Union of leaf-DTO types the resolver will see from `cellFor()`.
22
26
  * Discriminated by which `media` the caller passes — the type system
23
27
  * narrows the return automatically via the overload set below.
24
28
  */
25
- export type AnyPresetOptions = ImageCompressPresetOptions | AudioCompressPresetOptions | VideoCompressPresetOptions | DocumentPdfCompressPresetOptions | DocumentOfficeCompressPresetOptions | DocumentOdfCompressPresetOptions | DocumentEpubCompressPresetOptions;
29
+ export type AnyPresetOptions = ImageCompressPresetOptions | AudioCompressPresetOptions | VideoCompressPresetOptions | DocumentOfficeCompressPresetOptions | DocumentOdfCompressPresetOptions | DocumentEpubCompressPresetOptions;
26
30
  /**
27
31
  * Per-cell field-merge: parent fields ⊕ child fields where defined.
28
32
  * Re-construct the leaf DTO via the matching `<LeafClass>.from(merged)`
@@ -73,8 +77,6 @@ export declare class PresetDefaults {
73
77
  audioCompress(level: OptimizeFor, input?: AudioCompressPresetOptionsInput): PresetDefaults;
74
78
  /** Register a (level, delta) on the video-compress cell. Immutable. */
75
79
  videoCompress(level: OptimizeFor, input?: VideoCompressPresetOptionsInput): PresetDefaults;
76
- /** Register a (level, delta) on the document-pdf-compress cell. Immutable. */
77
- pdfCompress(level: OptimizeFor, input?: DocumentPdfCompressPresetOptionsInput): PresetDefaults;
78
80
  /** Register a (level, delta) on the document-office-compress cell. Immutable. */
79
81
  officeCompress(level: OptimizeFor, input?: DocumentOfficeCompressPresetOptionsInput): PresetDefaults;
80
82
  /** Register a (level, delta) on the document-odf-compress cell. Immutable. */
@@ -84,7 +86,6 @@ export declare class PresetDefaults {
84
86
  /** @internal */ cellFor(media: 'image', op: 'compress', level: OptimizeFor): ImageCompressPresetOptions | undefined;
85
87
  /** @internal */ cellFor(media: 'audio', op: 'compress', level: OptimizeFor): AudioCompressPresetOptions | undefined;
86
88
  /** @internal */ cellFor(media: 'video', op: 'compress', level: OptimizeFor): VideoCompressPresetOptions | undefined;
87
- /** @internal */ cellFor(media: 'document_pdf', op: 'compress', level: OptimizeFor): DocumentPdfCompressPresetOptions | undefined;
88
89
  /** @internal */ cellFor(media: 'document_office', op: 'compress', level: OptimizeFor): DocumentOfficeCompressPresetOptions | undefined;
89
90
  /** @internal */ cellFor(media: 'document_odf', op: 'compress', level: OptimizeFor): DocumentOdfCompressPresetOptions | undefined;
90
91
  /** @internal */ cellFor(media: 'document_epub', op: 'compress', level: OptimizeFor): DocumentEpubCompressPresetOptions | undefined;
@@ -28,7 +28,6 @@
28
28
  import { ImageCompressPresetOptions, } from './image_compress.js';
29
29
  import { AudioCompressPresetOptions, } from './audio_compress.js';
30
30
  import { VideoCompressPresetOptions, } from './video_compress.js';
31
- import { DocumentPdfCompressPresetOptions, } from './document_pdf_compress.js';
32
31
  import { DocumentOfficeCompressPresetOptions, } from './document_office_compress.js';
33
32
  import { DocumentOdfCompressPresetOptions, } from './document_odf_compress.js';
34
33
  import { DocumentEpubCompressPresetOptions, } from './document_epub_compress.js';
@@ -36,12 +35,11 @@ import { DocumentEpubCompressPresetOptions, } from './document_epub_compress.js'
36
35
  export { ImageCompressPresetOptions, } from './image_compress.js';
37
36
  export { AudioCompressPresetOptions, } from './audio_compress.js';
38
37
  export { VideoCompressPresetOptions, } from './video_compress.js';
39
- export { DocumentPdfCompressPresetOptions, } from './document_pdf_compress.js';
40
38
  export { DocumentOfficeCompressPresetOptions, } from './document_office_compress.js';
41
39
  export { DocumentOdfCompressPresetOptions, } from './document_odf_compress.js';
42
40
  export { DocumentEpubCompressPresetOptions, } from './document_epub_compress.js';
43
41
  // Re-export ergonomic enums for callers (single canonical path).
44
- export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, PdfProfile, PdfColorspace, } from '../../generated/sdk_spec/enums.js';
42
+ export { OptimizeFor, ImageMetadataPolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, } from '../../generated/sdk_spec/enums.js';
45
43
  function cellKeyOf(media, op) {
46
44
  return `${media}_${op}`;
47
45
  }
@@ -89,8 +87,6 @@ function mergePresetOptions(cellKey, parentOpts, childOpts) {
89
87
  return AudioCompressPresetOptions.from(mergedFields);
90
88
  case 'video_compress':
91
89
  return VideoCompressPresetOptions.from(mergedFields);
92
- case 'document_pdf_compress':
93
- return DocumentPdfCompressPresetOptions.from(mergedFields);
94
90
  case 'document_office_compress':
95
91
  return DocumentOfficeCompressPresetOptions.from(mergedFields);
96
92
  case 'document_odf_compress':
@@ -178,10 +174,6 @@ export class PresetDefaults {
178
174
  videoCompress(level, input = {}) {
179
175
  return new PresetDefaults(withCellEntry(this.cells, 'video_compress', level, VideoCompressPresetOptions.from(input)));
180
176
  }
181
- /** Register a (level, delta) on the document-pdf-compress cell. Immutable. */
182
- pdfCompress(level, input = {}) {
183
- return new PresetDefaults(withCellEntry(this.cells, 'document_pdf_compress', level, DocumentPdfCompressPresetOptions.from(input)));
184
- }
185
177
  /** Register a (level, delta) on the document-office-compress cell. Immutable. */
186
178
  officeCompress(level, input = {}) {
187
179
  return new PresetDefaults(withCellEntry(this.cells, 'document_office_compress', level, DocumentOfficeCompressPresetOptions.from(input)));
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AuthErrorResponse, AuthRejectionEnvelope, AuthRejectionEnvelopeErrorTypeEnum, BalanceExhaustedResponse, FeatureNotAvailableResponse, FeatureTierRestrictedResponse, ProbePendingResponse, TierRestrictionResponse, UploadDurationExceedsTierResponse, UploadSizeExceedsTierResponse, WorkflowExpiredResponse } from '@giveitsmaller/contracts/openapi';
1
+ import type { AuthErrorResponse, AuthRejectionEnvelope, AuthRejectionEnvelopeErrorTypeEnum, BalanceExhaustedResponse, FeatureNotAvailableResponse, FeatureTierRestrictedResponse, LongFormConcurrencyLimitResponse, ProbePendingResponse, TierRestrictionResponse, UploadDurationExceedsTierResponse, UploadSizeExceedsTierResponse, WorkflowExpiredResponse } from '@giveitsmaller/contracts/openapi';
2
2
  import type { ErrorCategory } from './generated/sdk_spec/errors.js';
3
3
  import type { RateLimitSnapshot } from './retry-metadata.js';
4
4
  export declare class GislError extends Error {
@@ -134,6 +134,34 @@ export declare class GislBalanceExhaustedError extends GislApiError {
134
134
  readonly payload: BalanceExhaustedResponse;
135
135
  constructor(statusCode: number, errorMessage: string, payload: BalanceExhaustedResponse, path?: string, extra?: Omit<GislApiErrorOptions, 'payload'>);
136
136
  }
137
+ /**
138
+ * `429` on `POST /api/workflows` when the caller already holds the maximum
139
+ * number of concurrent in-flight long-form (Fargate) workflows their tier
140
+ * permits (Pro 2 / Max 5; Enterprise uncapped). DISTINCT from an infrastructure
141
+ * rate-limit `429`: it carries the machine code `LONG_FORM_CONCURRENCY_LIMIT_EXCEEDED`
142
+ * and a `links.upgrade` deep link, and has **no `Retry-After`** — the limit clears
143
+ * when an in-flight long-form workflow finishes, not on a timer. A generic infra
144
+ * rate-limit `429` (no matching code) surfaces as the base {@link GislApiError}
145
+ * instead, where {@link GislApiError.retryAfterSeconds} applies.
146
+ *
147
+ * Dispatched on the `error` CODE, not `error_type` (the envelope carries none).
148
+ *
149
+ * @example
150
+ * try {
151
+ * await client.createWorkflow({ jobs });
152
+ * } catch (e) {
153
+ * if (e instanceof GislLongFormConcurrencyError) {
154
+ * showUpgradeCta(e.upgradeUrl); // wait on completion or upgrade — do NOT back off
155
+ * }
156
+ * throw e;
157
+ * }
158
+ */
159
+ export declare class GislLongFormConcurrencyError extends GislApiError {
160
+ readonly payload: LongFormConcurrencyLimitResponse;
161
+ constructor(statusCode: number, errorMessage: string, payload: LongFormConcurrencyLimitResponse, path?: string, extra?: Omit<GislApiErrorOptions, 'payload'>);
162
+ /** The pricing / upgrade deep link (`links.upgrade`), or `undefined` when absent. */
163
+ get upgradeUrl(): string | undefined;
164
+ }
137
165
  export declare class GislTierRestrictedError extends GislApiError {
138
166
  readonly payload: TierRestrictionResponse;
139
167
  constructor(statusCode: number, errorMessage: string, payload: TierRestrictionResponse, path?: string, extra?: Omit<GislApiErrorOptions, 'payload'>);
@@ -411,7 +439,25 @@ export declare class GislBundleAlreadyArchivedError extends GislConfigError {
411
439
  constructor();
412
440
  }
413
441
  export declare class GislTimeoutError extends GislError {
414
- constructor(message: string);
442
+ /**
443
+ * The workflow this timeout is scoped to, when the SDK knows it. Set on a
444
+ * timed-out `run()` / `wait()` / poll / download once the workflow has been
445
+ * created: a timeout does NOT mean the work failed — the server keeps
446
+ * processing, so poll `client.getWorkflowStatus(workflowId)` /
447
+ * `getWorkflowDownloads(workflowId)` to recover a result that completed after
448
+ * the deadline, instead of re-running (a re-run re-uploads and, for
449
+ * authenticated callers, settles a SECOND charge for the same deliverable).
450
+ *
451
+ * `undefined` when the SDK has no id to offer. That is NOT a guarantee that
452
+ * nothing was created or charged: it covers both the safe case (an upload /
453
+ * probe timeout before any workflow existed) AND the AMBIGUOUS case (the
454
+ * `POST /api/workflows` request itself timed out — the server may have
455
+ * created and charged the workflow before its response was lost). Treat an
456
+ * absent id as "cannot auto-recover", not "clean slate": reconcile (e.g. list
457
+ * recent workflows) before re-running rather than assuming nothing happened.
458
+ */
459
+ readonly workflowId?: string;
460
+ constructor(message: string, workflowId?: string);
415
461
  }
416
462
  /**
417
463
  * Transport-level failure: the underlying `fetch` (or other transport) could