@giveitsmaller/sdk 0.8.0 → 0.10.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 (46) hide show
  1. package/README.md +8 -0
  2. package/dist/_audit.js +5 -1
  3. package/dist/builder.d.ts +1 -8
  4. package/dist/builder.js +72 -18
  5. package/dist/client.d.ts +38 -2
  6. package/dist/client.js +131 -7
  7. package/dist/credentials.js +4 -2
  8. package/dist/ergonomic/preset_resolver.js +4 -5
  9. package/dist/ergonomic/presets/image_compress.d.ts +1 -9
  10. package/dist/ergonomic/presets/image_compress.js +6 -25
  11. package/dist/ergonomic/presets/index.d.ts +1 -1
  12. package/dist/ergonomic/presets/index.js +1 -1
  13. package/dist/errors.d.ts +75 -1
  14. package/dist/errors.js +73 -0
  15. package/dist/file-first.d.ts +456 -4
  16. package/dist/file-first.js +1042 -83
  17. package/dist/generated/sdk_spec/enums.d.ts +0 -11
  18. package/dist/generated/sdk_spec/enums.js +0 -7
  19. package/dist/generated/sdk_spec/errors.d.ts +1 -1
  20. package/dist/generated/sdk_spec/errors.js +26 -0
  21. package/dist/generated/sdk_spec/presets.js +0 -3
  22. package/dist/generated/sdk_spec/version.d.ts +2 -2
  23. package/dist/generated/sdk_spec/version.js +2 -2
  24. package/dist/gisl.d.ts +22 -1
  25. package/dist/gisl.js +31 -1
  26. package/dist/handle.d.ts +153 -0
  27. package/dist/handle.js +273 -0
  28. package/dist/index.browser.d.ts +1 -0
  29. package/dist/index.browser.js +14 -0
  30. package/dist/index.core.d.ts +35 -0
  31. package/dist/index.core.js +102 -0
  32. package/dist/index.d.ts +1 -30
  33. package/dist/index.js +9 -73
  34. package/dist/lazy-downloader.d.ts +19 -0
  35. package/dist/lazy-downloader.js +19 -0
  36. package/dist/merge.d.ts +13 -1
  37. package/dist/merge.js +186 -55
  38. package/dist/node-fs.browser.d.ts +17 -0
  39. package/dist/node-fs.browser.js +7 -0
  40. package/dist/node-fs.d.ts +14 -0
  41. package/dist/node-fs.js +14 -0
  42. package/dist/sha256.d.ts +20 -0
  43. package/dist/sha256.js +108 -0
  44. package/dist/types.d.ts +54 -2
  45. package/dist/types.js +2 -0
  46. package/package.json +15 -2
package/README.md CHANGED
@@ -40,6 +40,14 @@ const dls = await client.getWorkflowDownloads(workflow.workflowId);
40
40
  console.log('Compressed:', dls.downloads[0].files[0].downloadUrl);
41
41
  ```
42
42
 
43
+ > **Reusing an upload id across clients?** An upload created by an
44
+ > authenticated caller is owned by that caller. If you persist a `fileId` and
45
+ > later reference it (via `fileInput.uploadId(id)`) from a client configured
46
+ > with a *different* `apiKey`/session, workflow-create returns
47
+ > `404 upload_not_found` — the server enforces ownership. Reference an upload id
48
+ > only under the same auth that created it; the upload-then-create flow above is
49
+ > consistent by construction. Anonymous-intake uploads are unaffected.
50
+
43
51
  ## Full documentation
44
52
 
45
53
  Docs are published in the [giveitsmaller-sdks](https://github.com/AntonioCS/giveitsmaller-sdks) repository — they are **not** shipped in the npm tarball (only `dist/` is published).
package/dist/_audit.js CHANGED
@@ -115,7 +115,6 @@ export function _runAudit() {
115
115
  accept();
116
116
  accept();
117
117
  accept();
118
- accept();
119
118
  // T4b / 27rE1fZn — preset resolver public types.
120
119
  accept();
121
120
  accept();
@@ -129,7 +128,12 @@ export function _runAudit() {
129
128
  accept();
130
129
  accept();
131
130
  accept();
131
+ // FF3a / u0hBt6fl — homogeneous fan-out builder surface.
132
+ accept();
133
+ accept();
134
+ accept();
132
135
  accept();
136
+ // FF5a / Ao8RPVxD — file-first Handle reattach surface.
133
137
  accept();
134
138
  accept();
135
139
  }
package/dist/builder.d.ts CHANGED
@@ -26,6 +26,7 @@
26
26
  */
27
27
  import type { GislClient } from './client.js';
28
28
  import type { OperationDownload, WorkflowStatusResponse, SseOperationProgressDataStatusEnum } from '@giveitsmaller/contracts/openapi';
29
+ import { Handle } from './handle.js';
29
30
  import type { PresetDefaults, PresetMedia } from './ergonomic/presets/index.js';
30
31
  /**
31
32
  * Best-effort detection of the compress-operation media from the
@@ -183,14 +184,6 @@ export interface Result {
183
184
  */
184
185
  readonly resolvedOptions: ResolvedOptions;
185
186
  }
186
- /**
187
- * Lighter return value from `.submit({webhook})` — no SSE/poll wait,
188
- * caller reconciles completion via the webhook.
189
- */
190
- export interface Handle {
191
- readonly workflowId: string;
192
- readonly webhookSecret?: string;
193
- }
194
187
  /**
195
188
  * Upload-phase progress event. The byte counter comes from
196
189
  * `UploadOptions.onProgress` — there is no `phase` field on the wire.
package/dist/builder.js CHANGED
@@ -26,7 +26,11 @@
26
26
  */
27
27
  import { SseEventType, SseOperationProgressDataFromJSON, } from '@giveitsmaller/contracts/openapi';
28
28
  import { uploadSource } from './types.js';
29
- import { GislTimeoutError } from './errors.js';
29
+ import { GislTimeoutError, GislNetworkError, SseEndedWithoutTerminal } from './errors.js';
30
+ // Deferred-usage-only import: `Handle` is constructed inside submit() at call
31
+ // time, not at module load, so the builder.ts <-> handle.ts cycle is safe
32
+ // under ESM (handle.ts imports the await-primitives from this module).
33
+ import { Handle } from './handle.js';
30
34
  import { resolveCompressOptions, } from './ergonomic/preset_resolver.js';
31
35
  /**
32
36
  * Best-effort detection of the compress-operation media from the
@@ -241,6 +245,12 @@ export class OperationBuilder {
241
245
  throw new GislTimeoutError(`Workflow ${created.workflowId} reached terminal status but maxWait elapsed before downloads could be fetched`);
242
246
  }
243
247
  const downloads = await this.client.getWorkflowDownloads(created.workflowId);
248
+ // TDqmkWpX: the maxWait deadline also covers the downloads fetch itself — a
249
+ // slow getWorkflowDownloads must not return a success after the advertised
250
+ // whole-run deadline. Re-check AFTER the call (the check above is BEFORE).
251
+ if (Date.now() >= deadline) {
252
+ throw new GislTimeoutError(`Workflow ${created.workflowId} downloads fetch completed after maxWait elapsed`);
253
+ }
244
254
  return _projectResult(finalStatus, downloads.downloads, resolved.wireOptions, resolved.resolvedOptions);
245
255
  }
246
256
  /**
@@ -264,11 +274,9 @@ export class OperationBuilder {
264
274
  callback_url: options.webhook,
265
275
  };
266
276
  const created = await this.client.createWorkflow(payload);
267
- const handle = {
268
- workflowId: created.workflowId,
269
- ...(created.webhookSecret != null ? { webhookSecret: created.webhookSecret } : {}),
270
- };
271
- return handle;
277
+ // No client passed → the returned Handle's status()/wait()/result()
278
+ // throw `no_client`; the operation-first submit reconciles via webhook.
279
+ return new Handle(created.workflowId, created.webhookSecret != null ? created.webhookSecret : undefined);
272
280
  }
273
281
  /**
274
282
  * Fan-out chain: run this builder to completion, then for each artifact
@@ -309,14 +317,15 @@ export class OperationBuilder {
309
317
  return await _consumeSseToTerminal(this.client, args);
310
318
  }
311
319
  catch (err) {
312
- // Caller-aborted or deadline-elapsed errors MUST propagate — they
313
- // are NOT transient SSE failures. Only fall through to poll on a
314
- // genuine SSE connect/mid-stream error (codex-reviewer P0).
315
- if (err instanceof GislTimeoutError)
320
+ // TDqmkWpX: poll-fallback ONLY on a clean SSE stream-end
321
+ // (SseEndedWithoutTerminal) or a typed transport error (GislNetworkError).
322
+ // Everything else timeout, abort, API error, an onProgress callback
323
+ // throw, anything unexpected — MUST propagate; re-issuing the same doomed
324
+ // request via poll would mask the real failure.
325
+ if (!(err instanceof SseEndedWithoutTerminal || err instanceof GislNetworkError)) {
316
326
  throw err;
317
- if (err instanceof DOMException && err.name === 'AbortError')
318
- throw err;
319
- // Genuine SSE connect / stream error — fall through to poll fallback.
327
+ }
328
+ // Genuine SSE stream-end / transport error — fall through to poll fallback.
320
329
  }
321
330
  }
322
331
  return await _pollToTerminal(this.client, args);
@@ -423,6 +432,20 @@ const TERMINAL_STATUS = new Set([
423
432
  'expired',
424
433
  'paused_insufficient_credits',
425
434
  ]);
435
+ /**
436
+ * Internal marker (TDqmkWpX): tags an error thrown by the caller's `onProgress`
437
+ * callback so the mid-stream transport-error wrap in {@link _consumeSseToTerminal}
438
+ * cannot mistake it for a transport failure (a callback that throws a `TypeError`
439
+ * would otherwise be wrapped as `GislNetworkError` → masked by poll-fallback).
440
+ * The inner catch unwraps it and rethrows the ORIGINAL `cause`, so the caller
441
+ * sees their own error and the run never silently succeeds.
442
+ */
443
+ class _OnProgressThrew {
444
+ cause;
445
+ constructor(cause) {
446
+ this.cause = cause;
447
+ }
448
+ }
426
449
  /** @internal — exported for reuse by `merge.ts` (T3) and future builders. */
427
450
  export async function _consumeSseToTerminal(client, args) {
428
451
  const remainingMs = args.deadline - Date.now();
@@ -458,6 +481,13 @@ export async function _consumeSseToTerminal(client, args) {
458
481
  if (deadlineExpired && err instanceof DOMException && err.name === 'AbortError') {
459
482
  throw new GislTimeoutError(`Workflow ${args.workflowId} did not complete before maxWait deadline`);
460
483
  }
484
+ // TDqmkWpX: a genuine connect-phase TRANSPORT failure surfaces as a raw
485
+ // `TypeError` from `fetch` (DNS/TCP/TLS) — wrap it as a typed
486
+ // GislNetworkError so the await-terminal callers poll-fallback on it
487
+ // (and ONLY on it / a clean stream-end), never on an onProgress throw.
488
+ if (err instanceof TypeError) {
489
+ throw new GislNetworkError(`SSE connect to workflow ${args.workflowId} events failed: ${err.message}`);
490
+ }
461
491
  throw err;
462
492
  }
463
493
  // for-await also throws if the iterator's .next() rejects (e.g. the
@@ -488,7 +518,15 @@ export async function _consumeSseToTerminal(client, args) {
488
518
  ? { phaseTotalInputs: data.phaseTotalInputs }
489
519
  : {}),
490
520
  };
491
- args.onProgress(proj);
521
+ // TDqmkWpX: an onProgress callback throw (ANY type, incl. TypeError)
522
+ // must propagate, never be mistaken for a transport failure. Tag it so
523
+ // the mid-stream TypeError wrap in the catch below skips it.
524
+ try {
525
+ args.onProgress(proj);
526
+ }
527
+ catch (cbErr) {
528
+ throw new _OnProgressThrew(cbErr);
529
+ }
492
530
  }
493
531
  if (event.event === SseEventType.workflow_completed ||
494
532
  event.event === SseEventType.workflow_failed ||
@@ -509,17 +547,33 @@ export async function _consumeSseToTerminal(client, args) {
509
547
  if (deadlineExpired) {
510
548
  throw new GislTimeoutError(`Workflow ${args.workflowId} did not complete before maxWait deadline`);
511
549
  }
512
- // Otherwise it was a clean server-side close — fall back to poll.
513
- throw new Error('SSE stream ended without terminal event');
550
+ // Otherwise it was a clean server-side close — fall back to poll. TDqmkWpX:
551
+ // a sealed marker (not a bare Error) so callers poll ONLY on this + a typed
552
+ // transport error, never on an onProgress callback throw.
553
+ throw new SseEndedWithoutTerminal();
514
554
  }
515
555
  catch (innerErr) {
516
- // Same conversion as the outer catch: if deadline expired and the
517
- // iterator rejected with AbortError, surface as GislTimeoutError.
556
+ // TDqmkWpX: an onProgress callback throw was tagged so it is NEVER treated
557
+ // as a transport failure unwrap and rethrow the ORIGINAL cause so it
558
+ // propagates to the caller (never masked by a poll retry), even when the
559
+ // callback threw a TypeError.
560
+ if (innerErr instanceof _OnProgressThrew) {
561
+ throw innerErr.cause;
562
+ }
563
+ // If deadline expired and the iterator rejected with AbortError, surface
564
+ // as GislTimeoutError.
518
565
  if (deadlineExpired &&
519
566
  innerErr instanceof DOMException &&
520
567
  innerErr.name === 'AbortError') {
521
568
  throw new GislTimeoutError(`Workflow ${args.workflowId} did not complete before maxWait deadline`);
522
569
  }
570
+ // A genuine mid-stream TRANSPORT failure (reader disconnect) surfaces as a
571
+ // raw `TypeError` from the iterator — wrap as GislNetworkError so callers
572
+ // poll-fallback. (An onProgress throw was already handled above, so a
573
+ // TypeError here is unambiguously transport.)
574
+ if (innerErr instanceof TypeError) {
575
+ throw new GislNetworkError(`SSE stream for workflow ${args.workflowId} failed mid-stream: ${innerErr.message}`);
576
+ }
523
577
  throw innerErr;
524
578
  }
525
579
  }
package/dist/client.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { AudioWatermarkDecodeRequest, AudioWatermarkDecodeResponse, ExternalImportCreatedResponse, ExternalImportRequest, LoginUserRequest, LoginUser200ResponseData, ContactRequest, CreditsBalanceResponse, CreditsUsageResponse, UploadResponse, UploadProbeResponse, WorkflowCancelResponse, WorkflowCreateResponse, WorkflowResumeResponse, WorkflowStatusResponse, WorkflowDownloadResponse, MetadataResponse, RetryResponse } from '@giveitsmaller/contracts/openapi';
2
- import type { CreditsUsageOptions, GetSchemaOptions, GetSchemaResult, GislClientConfig, GislSseEvent, PreflightClipsResult, UploadOptions, WaitOptions, WorkflowCreatePayload, _Sdk3HandCodedKeepaliveResult, _Sdk3HandCodedMultipartStatusResult, _Sdk3HandCodedPresignPartsResult } from './types.js';
1
+ import type { AudioWatermarkDecodeRequest, AudioWatermarkDecodeResponse, ExternalImportCreatedResponse, ExternalImportRequest, LoginUserRequest, LoginUser200ResponseData, ContactRequest, 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, 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 {
@@ -277,6 +277,12 @@ export declare class GislClient {
277
277
  * until the external-import infrastructure ships. The method
278
278
  * exists today so consumers can write the integration ahead of
279
279
  * time.
280
+ *
281
+ * Auth-ownership: the import id this returns is owned by the
282
+ * authenticated caller that created it. Referencing it from a client
283
+ * with a different auth context 404s `upload_not_found` at
284
+ * workflow-create — same ownership rule as `fileInput.uploadId` (api
285
+ * PqpD9ySv).
280
286
  */
281
287
  createExternalImport(payload: ExternalImportRequest): Promise<ExternalImportCreatedResponse>;
282
288
  /**
@@ -324,4 +330,34 @@ export declare class GislClient {
324
330
  * Server defaults: `limit=20`, `offset=0`. Most-recent-first.
325
331
  */
326
332
  getCreditsUsage(options?: CreditsUsageOptions): Promise<CreditsUsageResponse>;
333
+ /**
334
+ * List the caller's workflows — a cursor-paginated, user-scoped summary
335
+ * list, most-recent-first. Each row is a lightweight {@link WorkflowSummary}
336
+ * (id / status / created_at + per-job type+status + a deliverable-output
337
+ * count); it does NOT inline per-op `result_metadata` or output details —
338
+ * drill in via {@link getWorkflowStatus} / {@link getWorkflowDownloads}.
339
+ *
340
+ * Auth is REQUIRED (the list is user-scoped; an anonymous caller gets a 401
341
+ * → `GislAuthError`). Walk pages by passing each response's `nextCursor` as
342
+ * the next call's `cursor` until `isTruncated` is false, or use
343
+ * {@link workflows} to auto-paginate. Mirrors the PHP
344
+ * `GislClient::listWorkflows`.
345
+ */
346
+ listWorkflows(options?: ListWorkflowsOptions): Promise<WorkflowListResponse>;
347
+ /**
348
+ * Auto-paginating async iterator over ALL of the caller's workflows,
349
+ * yielding each {@link WorkflowSummary} most-recent-first across page
350
+ * boundaries — the ergonomic companion to {@link listWorkflows}. Walks
351
+ * `nextCursor` until the server reports `isTruncated: false`. Mirrors the
352
+ * PHP `workflows()` generator.
353
+ *
354
+ * ```ts
355
+ * for await (const wf of client.workflows()) {
356
+ * console.log(wf.workflowId, wf.status);
357
+ * }
358
+ * ```
359
+ */
360
+ workflows(options?: {
361
+ limit?: number;
362
+ }): AsyncGenerator<WorkflowSummary, void, undefined>;
327
363
  }
package/dist/client.js CHANGED
@@ -1,7 +1,11 @@
1
- import { open, stat } from 'node:fs/promises';
2
- import { basename } from 'node:path';
3
- import { AudioWatermarkDecodeRequestToJSON, AudioWatermarkDecodeResponseFromJSON, ExternalImportCreatedResponseFromJSON, ExternalImportRequestToJSON, LoginUser200ResponseDataFromJSON, CreditsBalanceResponseFromJSON, CreditsUsageResponseFromJSON, UploadResponseFromJSON, UploadProbeResponseFromJSON, MultipartInitiateResponseFromJSON, MultipartInitiateRequestMetadataHintToJSON, MultipartCompleteResponseFromJSON, MultipartCompleteRequestToJSON, WorkflowCancelResponseFromJSON, WorkflowCreateResponseFromJSON, WorkflowResumeResponseFromJSON, WorkflowStatusResponseFromJSON, WorkflowDownloadResponseFromJSON, MetadataResponseFromJSON, OperationsSchemaResponseFromJSON, RetryResponseFromJSON, WorkflowStatus, AuthErrorResponseFromJSON, AuthErrorType, BalanceExhaustedResponseFromJSON, BalanceExhaustedResponseRequiredActionEnum, FeatureNotAvailableResponseFromJSON, FeatureTierRestrictedResponseFromJSON, TierRestrictionKind, TierRestrictionResponseFromJSON, UserTier, WorkflowExpiredResponseFromJSON, ProbePendingResponseFromJSON, UploadSizeExceedsTierResponseFromJSON, UploadDurationExceedsTierResponseFromJSON, UploadConstraintsAppliedProcessingClassPreAssignmentEnum, UploadThresholdsSingleShotMaxBytesEnum, UploadThresholdsMultipartChunkSizeEnum, UploadThresholdsMultipartConcurrencyDefaultEnum, } from '@giveitsmaller/contracts/openapi';
4
- import { GislAbortError, GislApiError, GislAuthError, GislBalanceExhaustedError, GislError, GislFeatureNotAvailableError, GislFeatureTierRestrictedError, GislMultipartPartCountError, GislMultipartPartError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTierRestrictedError, GislTimeoutError, GislProbePendingError, GislUploadCapExceededError, GislValidationError, GislWorkflowExpiredError, } from './errors.js';
1
+ // node:fs/promises + node:path are reached only through `./node-fs.js`, which
2
+ // the package.json `browser` field swaps for a stub in browser bundles — so the
3
+ // SDK entry graph stays browser-safe (a browser caller uploads a Blob via
4
+ // blobByteSource, which never touches these). Kept as a STATIC import (not a
5
+ // dynamic one) so `vi.mock('node:fs/promises')` still intercepts it in tests.
6
+ import { open, stat, basename } from './node-fs.js';
7
+ import { AudioWatermarkDecodeRequestToJSON, AudioWatermarkDecodeResponseFromJSON, ExternalImportCreatedResponseFromJSON, ExternalImportRequestToJSON, LoginUser200ResponseDataFromJSON, 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';
5
9
  import { parseSseStream } from './sse.js';
6
10
  const DEFAULT_TIMEOUT_MS = 30_000;
7
11
  // SDK-internal aliases derived from the contract-pinned UploadThresholds enums
@@ -66,6 +70,19 @@ const TERMINAL_STATUSES = new Set([
66
70
  WorkflowStatus.expired,
67
71
  WorkflowStatus.paused_insufficient_credits,
68
72
  ]);
73
+ // Flatten a `Headers` object into a plain `Record<string,string>` for
74
+ // attaching to a thrown `GislApiError`. `Headers.forEach` yields LOWERCASED
75
+ // keys (HTTP header names are case-insensitive per RFC 9110) and collapses
76
+ // any multi-value header (e.g. `set-cookie`) into a single comma-joined
77
+ // value — the record is for content-language / vary / x-request-id reads,
78
+ // not cookies.
79
+ function headersToRecord(headers) {
80
+ const r = {};
81
+ headers.forEach((v, k) => {
82
+ r[k] = v;
83
+ });
84
+ return r;
85
+ }
69
86
  function isValidationDetails(value) {
70
87
  return (Array.isArray(value) &&
71
88
  value.length > 0 &&
@@ -294,6 +311,18 @@ export class GislClient {
294
311
  if (config.apiKey) {
295
312
  this.headers['Authorization'] = `Bearer ${config.apiKey}`;
296
313
  }
314
+ // The dedicated `locale` option wins over any `Accept-Language` the caller
315
+ // passed via `headers` (mirrors apiKey -> Authorization). Drop any caller
316
+ // variant case-insensitively first so the request never carries two keys
317
+ // (e.g. `accept-language` + `Accept-Language`) with undefined precedence.
318
+ if (config.locale) {
319
+ for (const key of Object.keys(this.headers)) {
320
+ if (key.toLowerCase() === 'accept-language') {
321
+ delete this.headers[key];
322
+ }
323
+ }
324
+ this.headers['Accept-Language'] = config.locale;
325
+ }
297
326
  }
298
327
  // -----------------------------------------------------------------------
299
328
  // Internal HTTP
@@ -369,11 +398,18 @@ export class GislClient {
369
398
  return this.handleResponse(response, path, opts.deserialize);
370
399
  }
371
400
  async handleResponse(response, path, deserialize) {
401
+ // Surface the response headers (lowercased Record) and the resolved
402
+ // `Content-Language` on every GislApiError thrown from this handler.
403
+ const responseHeaders = headersToRecord(response.headers);
404
+ const contentLanguage = response.headers.get('content-language') ?? undefined;
372
405
  const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
373
406
  const isJsonContent = contentType.includes('application/json') || contentType.includes('+json');
374
407
  if (!isJsonContent) {
375
408
  if (!response.ok) {
376
- throw new GislApiError(response.status, 'Non-JSON response', path);
409
+ throw new GislApiError(response.status, 'Non-JSON response', path, undefined, {
410
+ responseHeaders,
411
+ contentLanguage,
412
+ });
377
413
  }
378
414
  return undefined;
379
415
  }
@@ -386,7 +422,10 @@ export class GislClient {
386
422
  json = await response.json();
387
423
  }
388
424
  catch {
389
- throw new GislApiError(response.status, 'Invalid JSON response', path);
425
+ throw new GislApiError(response.status, 'Invalid JSON response', path, undefined, {
426
+ responseHeaders,
427
+ contentLanguage,
428
+ });
390
429
  }
391
430
  // Standard envelope: { success, data } or { success, error, details }
392
431
  if (!response.ok || json.success === false) {
@@ -398,6 +437,8 @@ export class GislClient {
398
437
  messageKey: json.message_key,
399
438
  locale: json.locale,
400
439
  messageParams: json.message_params,
440
+ responseHeaders,
441
+ contentLanguage,
401
442
  };
402
443
  // Human-readable text comes from `message` (the I26 localised field).
403
444
  // `error` is the stable, never-localised SCREAMING_SNAKE machine code —
@@ -543,6 +584,18 @@ export class GislClient {
543
584
  if (status === 422 && errorType === 'FILE_TOO_LARGE_FOR_MULTIPART') {
544
585
  throw new GislUploadCapExceededError(status, errorMessage, 'cap_v2_multipart', undefined, path, i18n);
545
586
  }
587
+ // 422 auth-side-effect domain rejection (per contracts ADR-0019).
588
+ // Flat AuthRejectionEnvelope — NO `details[]` — on register /
589
+ // verify-email / api-keys (`error_type: unprocessable_entity`) and
590
+ // profile PATCH email-unchanged (`error_type: email_same`). The
591
+ // `validation_error` branch of the same auth-422 `oneOf` carries
592
+ // `details[]` and is already routed to GislValidationError by the
593
+ // shape-based branch above. Mirrors
594
+ // `packages/php/src/GislClient.php` GislAuthRejectionError branch.
595
+ if (status === 422 &&
596
+ isInEnum(errorType, AuthRejectionEnvelopeErrorTypeEnum)) {
597
+ tryThrowStructured(AuthRejectionEnvelopeFromJSON, GislAuthRejectionError, (p) => isInEnum(p.errorType, AuthRejectionEnvelopeErrorTypeEnum));
598
+ }
546
599
  throw new GislApiError(status, errorMessage, path, json.details, { ...i18n, payload: json });
547
600
  }
548
601
  const data = json.data ?? json;
@@ -1756,7 +1809,12 @@ export class GislClient {
1756
1809
  catch {
1757
1810
  // Non-JSON body — keep generic message.
1758
1811
  }
1759
- throw new GislApiError(response.status, errorMessage, path);
1812
+ // This throw is OUTSIDE handleResponse (rawResponse:true / 304 path), so
1813
+ // build the response-header surface from the in-scope `response` here.
1814
+ throw new GislApiError(response.status, errorMessage, path, undefined, {
1815
+ responseHeaders: headersToRecord(response.headers),
1816
+ contentLanguage: response.headers.get('content-language') ?? undefined,
1817
+ });
1760
1818
  }
1761
1819
  const raw = await response.json();
1762
1820
  const data = OperationsSchemaResponseFromJSON(raw);
@@ -1874,6 +1932,12 @@ export class GislClient {
1874
1932
  * until the external-import infrastructure ships. The method
1875
1933
  * exists today so consumers can write the integration ahead of
1876
1934
  * time.
1935
+ *
1936
+ * Auth-ownership: the import id this returns is owned by the
1937
+ * authenticated caller that created it. Referencing it from a client
1938
+ * with a different auth context 404s `upload_not_found` at
1939
+ * workflow-create — same ownership rule as `fileInput.uploadId` (api
1940
+ * PqpD9ySv).
1877
1941
  */
1878
1942
  async createExternalImport(payload) {
1879
1943
  return this.request('POST', '/api/external-imports', {
@@ -1979,4 +2043,64 @@ export class GislClient {
1979
2043
  deserialize: CreditsUsageResponseFromJSON,
1980
2044
  });
1981
2045
  }
2046
+ /**
2047
+ * List the caller's workflows — a cursor-paginated, user-scoped summary
2048
+ * list, most-recent-first. Each row is a lightweight {@link WorkflowSummary}
2049
+ * (id / status / created_at + per-job type+status + a deliverable-output
2050
+ * count); it does NOT inline per-op `result_metadata` or output details —
2051
+ * drill in via {@link getWorkflowStatus} / {@link getWorkflowDownloads}.
2052
+ *
2053
+ * Auth is REQUIRED (the list is user-scoped; an anonymous caller gets a 401
2054
+ * → `GislAuthError`). Walk pages by passing each response's `nextCursor` as
2055
+ * the next call's `cursor` until `isTruncated` is false, or use
2056
+ * {@link workflows} to auto-paginate. Mirrors the PHP
2057
+ * `GislClient::listWorkflows`.
2058
+ */
2059
+ async listWorkflows(options = {}) {
2060
+ const params = new URLSearchParams();
2061
+ // Guard `null` too — runtime (untyped) callers may pass `cursor: null`,
2062
+ // which must be omitted, not serialised as the literal string "null".
2063
+ if (options.cursor !== undefined && options.cursor !== null && options.cursor !== '') {
2064
+ params.set('cursor', options.cursor);
2065
+ }
2066
+ if (options.limit !== undefined)
2067
+ params.set('limit', String(options.limit));
2068
+ const query = params.toString();
2069
+ // String concatenation (not template) so the contract-drift path scanner
2070
+ // picks up the literal path. See getCreditsUsage for the same pattern.
2071
+ const path = query.length > 0 ? '/api/workflows' + '?' + query : '/api/workflows';
2072
+ return this.request('GET', path, {
2073
+ deserialize: WorkflowListResponseFromJSON,
2074
+ });
2075
+ }
2076
+ /**
2077
+ * Auto-paginating async iterator over ALL of the caller's workflows,
2078
+ * yielding each {@link WorkflowSummary} most-recent-first across page
2079
+ * boundaries — the ergonomic companion to {@link listWorkflows}. Walks
2080
+ * `nextCursor` until the server reports `isTruncated: false`. Mirrors the
2081
+ * PHP `workflows()` generator.
2082
+ *
2083
+ * ```ts
2084
+ * for await (const wf of client.workflows()) {
2085
+ * console.log(wf.workflowId, wf.status);
2086
+ * }
2087
+ * ```
2088
+ */
2089
+ async *workflows(options = {}) {
2090
+ let cursor;
2091
+ for (;;) {
2092
+ const page = await this.listWorkflows({ cursor, limit: options.limit });
2093
+ for (const summary of page.workflows) {
2094
+ yield summary;
2095
+ }
2096
+ const next = page.nextCursor;
2097
+ // Stop on a final page, an empty cursor, OR a non-advancing cursor — a
2098
+ // server that repeats the same cursor on a truncated page would
2099
+ // otherwise loop forever, refetching + re-yielding the same rows.
2100
+ if (!page.isTruncated || next === undefined || next === null || next === '' || next === cursor) {
2101
+ return;
2102
+ }
2103
+ cursor = next;
2104
+ }
2105
+ }
1982
2106
  }
@@ -137,8 +137,10 @@ function defaultProfilePath() {
137
137
  async function readProfile(path, profileName) {
138
138
  let raw;
139
139
  try {
140
- // Lazy import keeps `node:fs` out of browser bundles.
141
- const fs = await import('node:fs/promises');
140
+ // Lazy import keeps `node:fs` out of browser bundles. webpackIgnore stops
141
+ // webpack bundling it for browser targets; Vite externalises node: builtins
142
+ // itself (no @vite-ignore — that would also bypass test mocks of node:fs).
143
+ const fs = await import(/* webpackIgnore: true */ 'node:fs/promises');
142
144
  raw = await fs.readFile(path, 'utf8');
143
145
  }
144
146
  catch (err) {
@@ -34,7 +34,7 @@
34
34
  // plan's canonical example and the contract minimum.
35
35
  // On a parse, the resolver also writes `encoding_mode='target_size'`
36
36
  // to the wire; conversely if `crf` is explicit, `encoding_mode='crf'`.
37
- import { createHash } from 'node:crypto';
37
+ import { sha256Hex } from '../sha256.js';
38
38
  import { GislConfigError } from '../errors.js';
39
39
  import { ImageCompressPresetOptions, AudioCompressPresetOptions, VideoCompressPresetOptions, DocumentPdfCompressPresetOptions, DocumentOfficeCompressPresetOptions, DocumentOdfCompressPresetOptions, DocumentEpubCompressPresetOptions, } from './presets/index.js';
40
40
  /** Bumped on any change to a `*PresetOptions.shippedDefaultsFor(...)` cell value. */
@@ -52,7 +52,6 @@ export const PRESET_VERSION = '1.0';
52
52
  // safe path.
53
53
  const WIRE_ALIASES = Object.freeze({
54
54
  iccProfile: 'icc_profile',
55
- autoOrient: 'auto_orient',
56
55
  outputFormat: 'output_format',
57
56
  sampleRate: 'sample_rate',
58
57
  audioCodec: 'audio_codec',
@@ -236,7 +235,7 @@ function presetDefaultsCellRecord(defaults, media, op, optimize) {
236
235
  // for clearly-typed plain objects whose key set only intersects with a
237
236
  // non-matching media.
238
237
  const MEDIA_FIELDS = Object.freeze({
239
- image: new Set(['mode', 'quality', 'width', 'height', 'fit', 'metadata', 'iccProfile', 'autoOrient', 'progressive', 'outputFormat']),
238
+ image: new Set(['mode', 'quality', 'metadata', 'iccProfile', 'progressive', 'outputFormat']),
240
239
  audio: new Set(['bitrate', 'channels', 'sampleRate', 'normalize']),
241
240
  video: new Set(['codec', 'targetSize', 'crf', 'preset', 'width', 'height', 'fit', 'fps', 'faststart', 'audioCodec', 'audioBitrate']),
242
241
  document_pdf: new Set(['profile', 'colorspace', 'flattenForms']),
@@ -302,7 +301,7 @@ function mergeLayer(acc, layer, source) {
302
301
  // Validations on the merged wire payload
303
302
  // ---------------------------------------------------------------------------
304
303
  const KNOWN_WIRE_FIELDS = Object.freeze({
305
- image: new Set(['mode', 'quality', 'width', 'height', 'fit', 'metadata', 'icc_profile', 'auto_orient', 'progressive', 'output_format']),
304
+ image: new Set(['mode', 'quality', 'metadata', 'icc_profile', 'progressive', 'output_format']),
306
305
  audio: new Set(['bitrate', 'channels', 'sample_rate', 'normalize', 'trim_start', 'trim_end']),
307
306
  video: new Set(['codec', 'encoding_mode', 'crf', 'target_size_bytes', 'preset', 'width', 'height', 'fit', 'fps', 'faststart', 'audio_codec', 'audio_bitrate', 'trim_start', 'trim_end']),
308
307
  document_pdf: new Set(['profile', 'colorspace', 'pages', 'flatten_forms']),
@@ -417,7 +416,7 @@ function computePresetConfigHash(clientDefault, scopedDefault, callPresetOverrid
417
416
  scopedDefault: scopedDefault ?? null,
418
417
  callPresetOverride: callPresetOverride ?? null,
419
418
  });
420
- return `sha256:${createHash('sha256').update(canonical).digest('hex')}`;
419
+ return `sha256:${sha256Hex(canonical)}`;
421
420
  }
422
421
  // ---------------------------------------------------------------------------
423
422
  // Main entry point
@@ -1,25 +1,17 @@
1
- import { ImageMode, ImageFit, ImageMetadataPolicy, IccProfilePolicy, ImageFormat, OptimizeFor } from '../../generated/sdk_spec/enums.js';
1
+ import { ImageMode, ImageMetadataPolicy, IccProfilePolicy, ImageFormat, OptimizeFor } from '../../generated/sdk_spec/enums.js';
2
2
  export interface ImageCompressPresetOptionsInput {
3
3
  readonly mode?: ImageMode;
4
4
  readonly quality?: number;
5
- readonly width?: number;
6
- readonly height?: number;
7
- readonly fit?: ImageFit;
8
5
  readonly metadata?: ImageMetadataPolicy;
9
6
  readonly iccProfile?: IccProfilePolicy;
10
- readonly autoOrient?: boolean;
11
7
  readonly progressive?: boolean;
12
8
  readonly outputFormat?: ImageFormat;
13
9
  }
14
10
  export declare class ImageCompressPresetOptions {
15
11
  readonly mode?: ImageMode;
16
12
  readonly quality?: number;
17
- readonly width?: number;
18
- readonly height?: number;
19
- readonly fit?: ImageFit;
20
13
  readonly metadata?: ImageMetadataPolicy;
21
14
  readonly iccProfile?: IccProfilePolicy;
22
- readonly autoOrient?: boolean;
23
15
  readonly progressive?: boolean;
24
16
  readonly outputFormat?: ImageFormat;
25
17
  private constructor();
@@ -7,22 +7,19 @@
7
7
  // values ARE the wire backing values — so the leaf DTO is wire-compatible
8
8
  // once the resolver (T4b) snake_cases the property names.
9
9
  //
10
- // Field set per ticket VhIj4S7T (codex r3 lock): image = 10 fields
11
- // (mode, quality, width, height, fit, metadata, iccProfile, autoOrient,
12
- // progressive, outputFormat).
13
- // Trim / per-call knobs are deliberately excluded — they belong on the
14
- // per-call argument shape, not the preset cell.
10
+ // Field set (6) per EsD1hs5u / contracts v2.60.0: image =
11
+ // (mode, quality, metadata, iccProfile, progressive, outputFormat).
12
+ // `width`/`height`/`fit`/`autoOrient` were REMOVED — the image-compress
13
+ // worker never resized (resize-fit lives on thumbnail/convert; video keeps
14
+ // its own fit). Trim / per-call knobs are deliberately excluded they
15
+ // belong on the per-call argument shape, not the preset cell.
15
16
  import { shippedDefaultsFor as f3ShippedDefaultsFor } from '../../generated/sdk_spec/presets.js';
16
17
  import { translateEnum } from './_translate.js';
17
18
  export class ImageCompressPresetOptions {
18
19
  mode;
19
20
  quality;
20
- width;
21
- height;
22
- fit;
23
21
  metadata;
24
22
  iccProfile;
25
- autoOrient;
26
23
  progressive;
27
24
  outputFormat;
28
25
  constructor(input) {
@@ -30,18 +27,10 @@ export class ImageCompressPresetOptions {
30
27
  this.mode = input.mode;
31
28
  if (input.quality !== undefined)
32
29
  this.quality = input.quality;
33
- if (input.width !== undefined)
34
- this.width = input.width;
35
- if (input.height !== undefined)
36
- this.height = input.height;
37
- if (input.fit !== undefined)
38
- this.fit = input.fit;
39
30
  if (input.metadata !== undefined)
40
31
  this.metadata = input.metadata;
41
32
  if (input.iccProfile !== undefined)
42
33
  this.iccProfile = input.iccProfile;
43
- if (input.autoOrient !== undefined)
44
- this.autoOrient = input.autoOrient;
45
34
  if (input.progressive !== undefined)
46
35
  this.progressive = input.progressive;
47
36
  if (input.outputFormat !== undefined)
@@ -74,18 +63,10 @@ export class ImageCompressPresetOptions {
74
63
  mut.mode = translateEnum('ImageMode', cell.mode);
75
64
  if ('quality' in cell)
76
65
  mut.quality = cell.quality;
77
- if ('width' in cell)
78
- mut.width = cell.width;
79
- if ('height' in cell)
80
- mut.height = cell.height;
81
- if ('fit' in cell)
82
- mut.fit = translateEnum('ImageFit', cell.fit);
83
66
  if ('metadata' in cell)
84
67
  mut.metadata = translateEnum('ImageMetadataPolicy', cell.metadata);
85
68
  if ('iccProfile' in cell)
86
69
  mut.iccProfile = translateEnum('IccProfilePolicy', cell.iccProfile);
87
- if ('autoOrient' in cell)
88
- mut.autoOrient = cell.autoOrient;
89
70
  if ('progressive' in cell)
90
71
  mut.progressive = cell.progressive;
91
72
  if ('outputFormat' in cell)
@@ -13,7 +13,7 @@ export { DocumentPdfCompressPresetOptions, type DocumentPdfCompressPresetOptions
13
13
  export { DocumentOfficeCompressPresetOptions, type DocumentOfficeCompressPresetOptionsInput, } from './document_office_compress.js';
14
14
  export { DocumentOdfCompressPresetOptions, type DocumentOdfCompressPresetOptionsInput, } from './document_odf_compress.js';
15
15
  export { DocumentEpubCompressPresetOptions, type DocumentEpubCompressPresetOptionsInput, } from './document_epub_compress.js';
16
- export { OptimizeFor, ImageMode, ImageFit, ImageMetadataPolicy, IccProfilePolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, PdfProfile, PdfColorspace, } from '../../generated/sdk_spec/enums.js';
16
+ export { OptimizeFor, ImageMode, ImageMetadataPolicy, IccProfilePolicy, ImageFormat, VideoCodec, VideoPreset, VideoFit, AudioBitrate, AudioCodec, AudioSampleRate, PdfProfile, PdfColorspace, } from '../../generated/sdk_spec/enums.js';
17
17
  /** Supported media×op pairs for preset cells in T4a. Compress-only. */
18
18
  export type PresetMedia = 'image' | 'audio' | 'video' | 'document_pdf' | 'document_office' | 'document_odf' | 'document_epub';
19
19
  export type PresetOp = 'compress';