@giveitsmaller/sdk 0.22.0 → 0.25.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/errors.d.ts CHANGED
@@ -389,6 +389,31 @@ export declare class GislConfigError extends GislError {
389
389
  export declare class GislMissingCredentialsError extends GislConfigError {
390
390
  constructor(message: string);
391
391
  }
392
+ /**
393
+ * `streamEvents` was called on a client whose configuration has **no declared
394
+ * SSE stream host**. Local-only — thrown before any I/O.
395
+ *
396
+ * ⚠️ **THIS ERROR IS A CONTROL, NOT A DEFECT.** The stream lives on a second
397
+ * host, and the SDK will not guess it from `baseUrl`. Deriving `stream.*` from
398
+ * `api.*` by string surgery is a *convention*, and a convention is precisely
399
+ * what put production on the gateway path: the frontend's prod build had no
400
+ * stream host configured, fell back to the API host silently, and the failure
401
+ * was invisible until it was measured. Raising here is the loud version of
402
+ * that same situation.
403
+ *
404
+ * Both named environments resolve as of contracts `v2.195.0` (#410), which
405
+ * declared the production stream host. This now fires only for a
406
+ * configuration nothing declares — e.g. a bare `baseUrl` with no
407
+ * `environment` and no `streamBaseUrl`.
408
+ *
409
+ * Recover by passing `{streamBaseUrl}` to `gisl.create()` / `new GislClient()`,
410
+ * setting `GISL_STREAM_BASE_URL`, or constructing with an `{environment}` that
411
+ * declares one. `run()` does NOT surface this error — it treats an undeclared
412
+ * stream host as "SSE unavailable for this configuration" and polls instead.
413
+ */
414
+ export declare class GislStreamHostNotDeclaredError extends GislConfigError {
415
+ constructor(message: string);
416
+ }
392
417
  /**
393
418
  * The caller used `gisl.anonymous()` and then invoked an operation that is
394
419
  * not in the anonymous-capable allowlist. Local-only — thrown before any I/O.
@@ -513,17 +538,115 @@ export declare class GislFanOutTimeoutError extends GislTimeoutError {
513
538
  });
514
539
  }
515
540
  /**
516
- * Transport-level failure: the underlying `fetch` (or other transport) could
517
- * not produce a usable response DNS, TCP, TLS, a mid-stream disconnect, or a
518
- * non-ok status / empty body when fetching a result download. Mirrors the PHP
519
- * `Gisl\Sdk\Errors\GislNetworkError`. Subclasses `GislError` (not
520
- * `GislApiError`) because it carries no contract error envelope. The concrete
521
- * file-first {@link Downloader} raises this when the output URL cannot be read
522
- * (a destination-WRITE failure is `GislSinkError` reason `write_failed`).
541
+ * Base for every failure that happened **off the contract envelope** — the
542
+ * request did not come back as a typed API error, it came back (or failed to)
543
+ * at the transport or raw-HTTP level. Subclasses `GislError` rather than
544
+ * `GislApiError` because there is no error envelope to carry.
545
+ *
546
+ * ⚠️ **NEVER THROWN DIRECTLY it is a hierarchy node, not an error code
547
+ * (`t2qCrjdr`).** Everything that used to throw it now throws
548
+ * {@link GislTransportError} or {@link GislDownloadHttpError}, because the two
549
+ * cases cannot share one honest answer to "should I retry this?":
550
+ *
551
+ * | case | retry? |
552
+ * |---|---|
553
+ * | DNS / TCP / TLS / mid-stream disconnect | **yes** — transient by nature |
554
+ * | a `404` on a signed download URL | **no** — permanent, retrying burns time |
555
+ *
556
+ * `retryable: true` would recommend retrying a permanent failure and
557
+ * `retryable: false` would discourage retrying a genuine transient one, so
558
+ * contracts correctly refused to declare this class in
559
+ * `sdk-spec/error-taxonomy.yaml` at all. The fix is the split, not a caveat in
560
+ * a description field: **a claim must hold on every path that reaches it.**
561
+ *
562
+ * **Kept as the base ON PURPOSE, so this is not a breaking change.** Every
563
+ * existing `catch (e) { if (e instanceof GislNetworkError) … }` — including the
564
+ * SSE poll-fallback in `builder.ts` / `merge.ts` / `handle.ts` /
565
+ * `file-first.ts` — keeps catching exactly what it caught before. Narrow to a
566
+ * subclass only where you actually need to tell the two apart.
567
+ *
568
+ * Mirrors the PHP `Gisl\Sdk\Errors\GislNetworkError`.
523
569
  */
524
570
  export declare class GislNetworkError extends GislError {
525
571
  constructor(message: string);
526
572
  }
573
+ /**
574
+ * The transport could not deliver a usable response: DNS, TCP, TLS, a
575
+ * mid-stream disconnect, a `fetch` rejection, or a 2xx that arrived with no
576
+ * body at all. **Always retryable** — nothing about these says the request was
577
+ * wrong, only that it did not get through.
578
+ *
579
+ * The empty-body case lives here rather than with
580
+ * {@link GislDownloadHttpError} deliberately: the server said 2xx, so it is not
581
+ * an HTTP-level refusal — a response that promised bytes and delivered none is
582
+ * a delivery failure, and retrying is the right advice.
583
+ *
584
+ * The concrete file-first `Downloader` raises this when an output URL cannot be
585
+ * read (a destination-WRITE failure is `GislSinkError` reason `write_failed`).
586
+ */
587
+ export declare class GislTransportError extends GislNetworkError {
588
+ constructor(message: string);
589
+ /**
590
+ * Always `true`. The request did not get through; nothing about that says it
591
+ * was wrong, so retrying is the correct advice.
592
+ *
593
+ * Present as a real accessor rather than only as prose — an unbacked claim in
594
+ * a docblock is the exact defect `t2qCrjdr` exists to remove, and shipping
595
+ * the split without it would have reproduced it one level down.
596
+ */
597
+ get retryable(): boolean;
598
+ }
599
+ /**
600
+ * The request was never put on the wire because the client refused to send it —
601
+ * a malformed URI or an otherwise unsendable request. **Never retryable:**
602
+ * re-issuing the identical request fails identically, so backing off only
603
+ * wastes the caller's deadline.
604
+ *
605
+ * ⚠️ **THE TWO LANGUAGES DETECT THIS DIFFERENTLY, AND TS DETECTS LESS.** PSR-18
606
+ * distinguishes a network failure (`NetworkExceptionInterface`) from an
607
+ * unsendable request (`RequestExceptionInterface`), so the PHP SDK classifies
608
+ * every such failure. `fetch` surfaces both as an indistinguishable
609
+ * `TypeError`, so the TS SDK can only catch the cases it can see BEFORE the
610
+ * call — today, a URL that does not parse (`http-downloader`). A `fetch`
611
+ * rejection is still reported as {@link GislTransportError}, because guessing
612
+ * would put a permanent failure back in the retryable bucket, which is the very
613
+ * thing this split removed.
614
+ *
615
+ * So: same class, same meaning, same `retryable` in both SDKs — narrower
616
+ * detection in TypeScript. Stated here because a cross-language consumer would
617
+ * otherwise reasonably assume parity of COVERAGE from parity of TYPE.
618
+ */
619
+ export declare class GislRequestNotSentError extends GislNetworkError {
620
+ constructor(message: string);
621
+ /** Always `false` — the request never left, and re-sending it will not change that. */
622
+ get retryable(): boolean;
623
+ }
624
+ /**
625
+ * A download URL answered with a **non-2xx status**. The server was reached and
626
+ * replied; it simply refused. Distinct from {@link GislTransportError} because
627
+ * retrying is usually pointless — and `retryable` says so honestly, derived
628
+ * from the status rather than fixed for the class.
629
+ *
630
+ * `status` is carried as a field so a consumer distinguishing a permanent `404`
631
+ * from a transient `503` does not have to parse the message string — the second
632
+ * half of `t2qCrjdr`.
633
+ *
634
+ * Raised on result-download fetches (signed URLs), NOT on GISL-API calls: an
635
+ * API non-2xx carries a contract error envelope and surfaces as the matching
636
+ * {@link GislApiError} subclass instead.
637
+ */
638
+ export declare class GislDownloadHttpError extends GislNetworkError {
639
+ /** The HTTP status the download URL responded with. */
640
+ readonly status: number;
641
+ constructor(message: string, status: number);
642
+ /**
643
+ * Whether retrying this download could plausibly succeed. Derived from the
644
+ * status by the same rule the API errors use (`408` / `429` / `5xx`), so a
645
+ * `404` reports `false` and a `503` reports `true` — the distinction the
646
+ * unsplit class could not express.
647
+ */
648
+ get retryable(): boolean;
649
+ }
527
650
  /**
528
651
  * Internal control-flow marker (TDqmkWpX): the SSE event stream closed cleanly
529
652
  * WITHOUT a terminal (`workflow_completed`/`failed`/`partially_failed`) event.
package/dist/errors.js CHANGED
@@ -398,6 +398,34 @@ export class GislMissingCredentialsError extends GislConfigError {
398
398
  this.name = 'GislMissingCredentialsError';
399
399
  }
400
400
  }
401
+ /**
402
+ * `streamEvents` was called on a client whose configuration has **no declared
403
+ * SSE stream host**. Local-only — thrown before any I/O.
404
+ *
405
+ * ⚠️ **THIS ERROR IS A CONTROL, NOT A DEFECT.** The stream lives on a second
406
+ * host, and the SDK will not guess it from `baseUrl`. Deriving `stream.*` from
407
+ * `api.*` by string surgery is a *convention*, and a convention is precisely
408
+ * what put production on the gateway path: the frontend's prod build had no
409
+ * stream host configured, fell back to the API host silently, and the failure
410
+ * was invisible until it was measured. Raising here is the loud version of
411
+ * that same situation.
412
+ *
413
+ * Both named environments resolve as of contracts `v2.195.0` (#410), which
414
+ * declared the production stream host. This now fires only for a
415
+ * configuration nothing declares — e.g. a bare `baseUrl` with no
416
+ * `environment` and no `streamBaseUrl`.
417
+ *
418
+ * Recover by passing `{streamBaseUrl}` to `gisl.create()` / `new GislClient()`,
419
+ * setting `GISL_STREAM_BASE_URL`, or constructing with an `{environment}` that
420
+ * declares one. `run()` does NOT surface this error — it treats an undeclared
421
+ * stream host as "SSE unavailable for this configuration" and polls instead.
422
+ */
423
+ export class GislStreamHostNotDeclaredError extends GislConfigError {
424
+ constructor(message) {
425
+ super(message);
426
+ this.name = 'GislStreamHostNotDeclaredError';
427
+ }
428
+ }
401
429
  /**
402
430
  * The caller used `gisl.anonymous()` and then invoked an operation that is
403
431
  * not in the anonymous-capable allowlist. Local-only — thrown before any I/O.
@@ -560,13 +588,34 @@ export class GislFanOutTimeoutError extends GislTimeoutError {
560
588
  }
561
589
  }
562
590
  /**
563
- * Transport-level failure: the underlying `fetch` (or other transport) could
564
- * not produce a usable response DNS, TCP, TLS, a mid-stream disconnect, or a
565
- * non-ok status / empty body when fetching a result download. Mirrors the PHP
566
- * `Gisl\Sdk\Errors\GislNetworkError`. Subclasses `GislError` (not
567
- * `GislApiError`) because it carries no contract error envelope. The concrete
568
- * file-first {@link Downloader} raises this when the output URL cannot be read
569
- * (a destination-WRITE failure is `GislSinkError` reason `write_failed`).
591
+ * Base for every failure that happened **off the contract envelope** — the
592
+ * request did not come back as a typed API error, it came back (or failed to)
593
+ * at the transport or raw-HTTP level. Subclasses `GislError` rather than
594
+ * `GislApiError` because there is no error envelope to carry.
595
+ *
596
+ * ⚠️ **NEVER THROWN DIRECTLY it is a hierarchy node, not an error code
597
+ * (`t2qCrjdr`).** Everything that used to throw it now throws
598
+ * {@link GislTransportError} or {@link GislDownloadHttpError}, because the two
599
+ * cases cannot share one honest answer to "should I retry this?":
600
+ *
601
+ * | case | retry? |
602
+ * |---|---|
603
+ * | DNS / TCP / TLS / mid-stream disconnect | **yes** — transient by nature |
604
+ * | a `404` on a signed download URL | **no** — permanent, retrying burns time |
605
+ *
606
+ * `retryable: true` would recommend retrying a permanent failure and
607
+ * `retryable: false` would discourage retrying a genuine transient one, so
608
+ * contracts correctly refused to declare this class in
609
+ * `sdk-spec/error-taxonomy.yaml` at all. The fix is the split, not a caveat in
610
+ * a description field: **a claim must hold on every path that reaches it.**
611
+ *
612
+ * **Kept as the base ON PURPOSE, so this is not a breaking change.** Every
613
+ * existing `catch (e) { if (e instanceof GislNetworkError) … }` — including the
614
+ * SSE poll-fallback in `builder.ts` / `merge.ts` / `handle.ts` /
615
+ * `file-first.ts` — keeps catching exactly what it caught before. Narrow to a
616
+ * subclass only where you actually need to tell the two apart.
617
+ *
618
+ * Mirrors the PHP `Gisl\Sdk\Errors\GislNetworkError`.
570
619
  */
571
620
  export class GislNetworkError extends GislError {
572
621
  constructor(message) {
@@ -574,6 +623,99 @@ export class GislNetworkError extends GislError {
574
623
  this.name = 'GislNetworkError';
575
624
  }
576
625
  }
626
+ /**
627
+ * The transport could not deliver a usable response: DNS, TCP, TLS, a
628
+ * mid-stream disconnect, a `fetch` rejection, or a 2xx that arrived with no
629
+ * body at all. **Always retryable** — nothing about these says the request was
630
+ * wrong, only that it did not get through.
631
+ *
632
+ * The empty-body case lives here rather than with
633
+ * {@link GislDownloadHttpError} deliberately: the server said 2xx, so it is not
634
+ * an HTTP-level refusal — a response that promised bytes and delivered none is
635
+ * a delivery failure, and retrying is the right advice.
636
+ *
637
+ * The concrete file-first `Downloader` raises this when an output URL cannot be
638
+ * read (a destination-WRITE failure is `GislSinkError` reason `write_failed`).
639
+ */
640
+ export class GislTransportError extends GislNetworkError {
641
+ constructor(message) {
642
+ super(message);
643
+ this.name = 'GislTransportError';
644
+ }
645
+ /**
646
+ * Always `true`. The request did not get through; nothing about that says it
647
+ * was wrong, so retrying is the correct advice.
648
+ *
649
+ * Present as a real accessor rather than only as prose — an unbacked claim in
650
+ * a docblock is the exact defect `t2qCrjdr` exists to remove, and shipping
651
+ * the split without it would have reproduced it one level down.
652
+ */
653
+ get retryable() {
654
+ return true;
655
+ }
656
+ }
657
+ /**
658
+ * The request was never put on the wire because the client refused to send it —
659
+ * a malformed URI or an otherwise unsendable request. **Never retryable:**
660
+ * re-issuing the identical request fails identically, so backing off only
661
+ * wastes the caller's deadline.
662
+ *
663
+ * ⚠️ **THE TWO LANGUAGES DETECT THIS DIFFERENTLY, AND TS DETECTS LESS.** PSR-18
664
+ * distinguishes a network failure (`NetworkExceptionInterface`) from an
665
+ * unsendable request (`RequestExceptionInterface`), so the PHP SDK classifies
666
+ * every such failure. `fetch` surfaces both as an indistinguishable
667
+ * `TypeError`, so the TS SDK can only catch the cases it can see BEFORE the
668
+ * call — today, a URL that does not parse (`http-downloader`). A `fetch`
669
+ * rejection is still reported as {@link GislTransportError}, because guessing
670
+ * would put a permanent failure back in the retryable bucket, which is the very
671
+ * thing this split removed.
672
+ *
673
+ * So: same class, same meaning, same `retryable` in both SDKs — narrower
674
+ * detection in TypeScript. Stated here because a cross-language consumer would
675
+ * otherwise reasonably assume parity of COVERAGE from parity of TYPE.
676
+ */
677
+ export class GislRequestNotSentError extends GislNetworkError {
678
+ constructor(message) {
679
+ super(message);
680
+ this.name = 'GislRequestNotSentError';
681
+ }
682
+ /** Always `false` — the request never left, and re-sending it will not change that. */
683
+ get retryable() {
684
+ return false;
685
+ }
686
+ }
687
+ /**
688
+ * A download URL answered with a **non-2xx status**. The server was reached and
689
+ * replied; it simply refused. Distinct from {@link GislTransportError} because
690
+ * retrying is usually pointless — and `retryable` says so honestly, derived
691
+ * from the status rather than fixed for the class.
692
+ *
693
+ * `status` is carried as a field so a consumer distinguishing a permanent `404`
694
+ * from a transient `503` does not have to parse the message string — the second
695
+ * half of `t2qCrjdr`.
696
+ *
697
+ * Raised on result-download fetches (signed URLs), NOT on GISL-API calls: an
698
+ * API non-2xx carries a contract error envelope and surfaces as the matching
699
+ * {@link GislApiError} subclass instead.
700
+ */
701
+ export class GislDownloadHttpError extends GislNetworkError {
702
+ /** The HTTP status the download URL responded with. */
703
+ status;
704
+ constructor(message, status) {
705
+ super(message);
706
+ this.name = 'GislDownloadHttpError';
707
+ this.status = status;
708
+ }
709
+ /**
710
+ * Whether retrying this download could plausibly succeed. Derived from the
711
+ * status by the same rule the API errors use (`408` / `429` / `5xx`), so a
712
+ * `404` reports `false` and a `503` reports `true` — the distinction the
713
+ * unsplit class could not express.
714
+ */
715
+ get retryable() {
716
+ return isApiRetryableStatus(this.status);
717
+ }
718
+ }
577
719
  /**
578
720
  * Internal control-flow marker (TDqmkWpX): the SSE event stream closed cleanly
579
721
  * WITHOUT a terminal (`workflow_completed`/`failed`/`partially_failed`) event.
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * Mirrors `packages/php/src/FileFirst/*`.
11
11
  */
12
- import { GislConfigError, GislItemFailedError, GislNetworkError, GislNoSuchKeyError, GislSinkError, GislTimeoutError, SseEndedWithoutTerminal } from './errors.js';
12
+ import { GislConfigError, GislItemFailedError, GislNetworkError, GislNoSuchKeyError, GislSinkError, GislStreamHostNotDeclaredError, GislTimeoutError, SseEndedWithoutTerminal } from './errors.js';
13
13
  import { _detectCompressMedia, _detectAudioLossless, _consumeSseToTerminal, _pollToTerminal, _parseMaxWait, _checkAborted, _cappedProbeTimeoutMs, } from './builder.js';
14
14
  import { LazyHttpDownloader } from './lazy-downloader.js';
15
15
  import { resolveCompressOptions, } from './ergonomic/preset_resolver.js';
@@ -629,7 +629,17 @@ async function _awaitTerminal(client, args) {
629
629
  // unexpected — MUST propagate; re-issuing the same doomed request via poll
630
630
  // would mask it. Mirrors the PHP BuilderInternals::awaitTerminal sealed-
631
631
  // marker discipline.
632
- if (!(err instanceof SseEndedWithoutTerminal || err instanceof GislNetworkError)) {
632
+ if (!(err instanceof SseEndedWithoutTerminal ||
633
+ err instanceof GislNetworkError ||
634
+ // VUozk5Bc: no stream host is DECLARED for this configuration (a
635
+ // configuration nothing declares; both named environments resolve as of
636
+ // contracts v2.195.0). That is not a failure to recover from,
637
+ // it is SSE being unavailable here, and polling is a working
638
+ // transport. Failing hard instead would strand every caller on a host
639
+ // nobody has declared yet. A DIRECT `streamEvents` caller still gets
640
+ // the hard error — they asked for the stream specifically; a `run()`
641
+ // caller asked for a result.
642
+ err instanceof GislStreamHostNotDeclaredError)) {
633
643
  throw err;
634
644
  }
635
645
  }
@@ -1,4 +1,4 @@
1
- export type ErrorCode = "missing_credentials" | "feature_requires_auth" | "undeclared_asset" | "unused_asset" | "per_input_options_not_supported" | "chain_cardinality_mismatch" | "multipart_part_invalid" | "multipart_part_count_exceeded" | "timeout" | "aborted" | "validation_failed" | "validation_error" | "cyclic_workflow_edges" | "workflow_edge_references_unknown_job" | "reserved_job_id_pattern" | "cyclic_job_output_source_graph" | "auth_failed" | "feature_tier_restricted" | "tier_restriction" | "multipart_session_ownership" | "multipart_session_auth_required" | "multipart_session_not_found" | "upload_not_found" | "workflow_expired" | "balance_exhausted" | "feature_not_available" | "upload_size_exceeds_tier" | "upload_duration_exceeds_tier" | "probe_pending" | "requires_reencode" | "invalid_options" | "invalid_combination" | "missing_dependency" | "unsupported_value" | "type_mismatch" | "image_dimensions_too_large" | "upload_failed" | "workflow_failed" | "long_form_concurrency_limit_exceeded" | "unprocessable_entity" | "email_same" | "config_error" | "bundle_already_archived" | "fan_out_timeout" | "no_such_key" | "result_not_ready" | "sink_error" | "item_failed";
1
+ export type ErrorCode = "missing_credentials" | "feature_requires_auth" | "undeclared_asset" | "unused_asset" | "per_input_options_not_supported" | "chain_cardinality_mismatch" | "multipart_part_invalid" | "multipart_part_count_exceeded" | "timeout" | "aborted" | "validation_failed" | "validation_error" | "cyclic_workflow_edges" | "workflow_edge_references_unknown_job" | "reserved_job_id_pattern" | "cyclic_job_output_source_graph" | "auth_failed" | "feature_tier_restricted" | "tier_restriction" | "multipart_session_ownership" | "multipart_session_auth_required" | "multipart_session_not_found" | "upload_not_found" | "workflow_expired" | "balance_exhausted" | "feature_not_available" | "upload_size_exceeds_tier" | "upload_duration_exceeds_tier" | "probe_pending" | "requires_reencode" | "invalid_options" | "invalid_combination" | "missing_dependency" | "unsupported_value" | "type_mismatch" | "image_dimensions_too_large" | "upload_failed" | "workflow_failed" | "sse_connection_limit_exceeded" | "sse_capacity_exhausted" | "long_form_concurrency_limit_exceeded" | "unprocessable_entity" | "email_same" | "config_error" | "bundle_already_archived" | "fan_out_timeout" | "no_such_key" | "result_not_ready" | "sink_error" | "item_failed";
2
2
  export type ErrorCategory = 'api' | 'config' | 'network' | 'auth' | 'validation' | 'chain';
3
3
  export type ErrorStatus = 'wired' | 'planned';
4
4
  export interface ErrorEntry {
@@ -506,6 +506,33 @@ export const ERROR_CODES = Object.freeze({
506
506
  "jobErrors": "array",
507
507
  }),
508
508
  }),
509
+ "sse_connection_limit_exceeded": Object.freeze({
510
+ code: "sse_connection_limit_exceeded",
511
+ category: "api",
512
+ source: "ErrorEnvelope.error",
513
+ status: "planned",
514
+ httpStatus: 429,
515
+ retryable: true,
516
+ sdkClass: "GislSseConnectionLimitError",
517
+ description: "429 — the CALLER's own concurrent event-stream allowance is exhausted. Same semantics as the tier long-form concurrency limit and therefore the same status. NOT for a global-capacity refusal; see sse_capacity_exhausted.",
518
+ metadataSchema: Object.freeze({
519
+ "openStreams": "integer",
520
+ "maxStreams": "integer",
521
+ }),
522
+ }),
523
+ "sse_capacity_exhausted": Object.freeze({
524
+ code: "sse_capacity_exhausted",
525
+ category: "api",
526
+ source: "ErrorEnvelope.error",
527
+ status: "planned",
528
+ httpStatus: 503,
529
+ retryable: true,
530
+ sdkClass: "GislSseCapacityError",
531
+ description: "503 — GLOBAL event-stream capacity is exhausted, and it says nothing about this caller. A caller who has opened no streams can receive it. ⚠️ `EventSource` does not expose the HTTP status to page script, so a browser client cannot distinguish this from 500 — the status serves non-browser clients, proxies and observability.",
532
+ metadataSchema: Object.freeze({
533
+ "links": "object",
534
+ }),
535
+ }),
509
536
  "long_form_concurrency_limit_exceeded": Object.freeze({
510
537
  code: "long_form_concurrency_limit_exceeded",
511
538
  category: "api",
@@ -658,6 +685,8 @@ export const ERROR_CATEGORIES = Object.freeze({
658
685
  "requires_reencode",
659
686
  "image_dimensions_too_large",
660
687
  "workflow_failed",
688
+ "sse_connection_limit_exceeded",
689
+ "sse_capacity_exhausted",
661
690
  "long_form_concurrency_limit_exceeded",
662
691
  "item_failed",
663
692
  ]),
package/dist/gisl.d.ts CHANGED
@@ -41,7 +41,7 @@ import { Handle } from './handle.js';
41
41
  * @internal
42
42
  */
43
43
  export declare const ANONYMOUS_ALLOWLIST: readonly [];
44
- export interface GislCreateOptions extends ResolveCredentialsOptions, ResolveEndpointOptions, Omit<GislClientConfig, 'baseUrl' | 'apiKey' | 'useSessionCookie'> {
44
+ export interface GislCreateOptions extends ResolveCredentialsOptions, ResolveEndpointOptions, Omit<GislClientConfig, 'baseUrl' | 'apiKey' | 'useSessionCookie' | 'streamBaseUrl'> {
45
45
  /**
46
46
  * Layered ergonomic preset defaults (T4a / VhIj4S7T). Built via
47
47
  * `presetDefaults().<cell>(level, overrides?)…`. The resolver wiring
package/dist/gisl.js CHANGED
@@ -18,7 +18,7 @@
18
18
  */
19
19
  import { GislClient } from './client.js';
20
20
  import { GislConfigError, GislFeatureRequiresAuthError, GislMissingCredentialsError, } from './errors.js';
21
- import { resolveApiKey, resolveEndpoint, } from './credentials.js';
21
+ import { resolveApiKey, resolveEndpoint, resolveStreamEndpoint, } from './credentials.js';
22
22
  import { OperationBuilder } from './builder.js';
23
23
  import { validateVerbOptions, validateSingleOpConvertOptions, assertThumbnailDimensions, } from './ergonomic/option_validation.js';
24
24
  import { MergeBuilder, asset } from './merge.js';
@@ -279,13 +279,19 @@ function isMergeOptions(value) {
279
279
  * @internal
280
280
  */
281
281
  async function _createInternal(opts) {
282
- const { apiKey: explicitKey, profile, profilePath, useSessionCookie, baseUrl, environment, allowAnonymous,
282
+ const { apiKey: explicitKey, profile, profilePath, useSessionCookie, baseUrl, environment, streamBaseUrl, allowAnonymous,
283
283
  // T4a slot — stripped from transportConfig so it does not leak
284
284
  // into the low-level `GislClientConfig` spread. The T4b resolver
285
285
  // reads `opts.presetDefaults` directly via its own path.
286
286
  presetDefaults: _presetDefaults, ...transportConfig } = opts;
287
287
  void _presetDefaults;
288
288
  const resolvedBaseUrl = resolveEndpoint({ baseUrl, environment });
289
+ // Resolved SEPARATELY and never from `resolvedBaseUrl`. `null` here means
290
+ // "nothing declares a stream host for this configuration" — a legitimate
291
+ // state that `streamEvents` reports and `run()` handles by polling. Both
292
+ // resolvers throw on an unknown explicit `environment`, so a typo cannot
293
+ // split the two hosts across environments.
294
+ const resolvedStreamBaseUrl = resolveStreamEndpoint({ baseUrl, environment, streamBaseUrl });
289
295
  // Anonymous mode entirely BYPASSES the credential chain. Any env / profile
290
296
  // key that happens to exist on the host MUST NOT leak into the request
291
297
  // (codex r1 high e9e1c1182d56). Cookie-mode also bypasses, since the
@@ -295,6 +301,9 @@ async function _createInternal(opts) {
295
301
  baseUrl: resolvedBaseUrl,
296
302
  ...transportConfig,
297
303
  };
304
+ if (resolvedStreamBaseUrl !== null) {
305
+ config.streamBaseUrl = resolvedStreamBaseUrl;
306
+ }
298
307
  if (useSessionCookie !== undefined) {
299
308
  config.useSessionCookie = useSessionCookie;
300
309
  }
@@ -327,6 +336,9 @@ async function _createInternal(opts) {
327
336
  baseUrl: resolvedBaseUrl,
328
337
  ...transportConfig,
329
338
  };
339
+ if (resolvedStreamBaseUrl !== null) {
340
+ config.streamBaseUrl = resolvedStreamBaseUrl;
341
+ }
330
342
  if (resolvedKey !== null) {
331
343
  config.apiKey = resolvedKey;
332
344
  }
package/dist/handle.js CHANGED
@@ -32,7 +32,7 @@
32
32
  *
33
33
  * Mirrors the PHP `Gisl\Sdk\Ergonomic\Handle` + `Gisl\Sdk\Ergonomic\StatusSnapshot`.
34
34
  */
35
- import { GislConfigError, GislNetworkError, GislResultNotReadyError, GislTimeoutError, SseEndedWithoutTerminal, } from './errors.js';
35
+ import { GislConfigError, GislNetworkError, GislResultNotReadyError, GislTimeoutError, GislStreamHostNotDeclaredError, SseEndedWithoutTerminal, } from './errors.js';
36
36
  import { _consumeSseToTerminal, _pollToTerminal, _parseMaxWait, } from './builder.js';
37
37
  import { projectDownloadsToRunResult, projectMultiJobToRunResult, isFanoutStatus, isMergeStatus, isArchiveStatus, isWatermarkStatus, isSoleOpChainStatus, soleOpChainDeliverableRef, _POST_STEP_JOB_REF, } from './file-first.js';
38
38
  import { LazyHttpDownloader } from './lazy-downloader.js';
@@ -172,7 +172,17 @@ export class Handle {
172
172
  // (GislNetworkError). Everything else (timeout, abort, API, an onProgress
173
173
  // callback throw, anything unexpected) MUST propagate — re-issuing the same
174
174
  // doomed request via poll would mask the real failure.
175
- if (!(err instanceof SseEndedWithoutTerminal || err instanceof GislNetworkError)) {
175
+ if (!(err instanceof SseEndedWithoutTerminal ||
176
+ err instanceof GislNetworkError ||
177
+ // VUozk5Bc: no stream host is DECLARED for this configuration (a
178
+ // configuration nothing declares; both named environments resolve as of
179
+ // contracts v2.195.0). That is not a failure to recover from,
180
+ // it is SSE being unavailable here, and polling is a working
181
+ // transport. Failing hard instead would strand every caller on a host
182
+ // nobody has declared yet. A DIRECT `streamEvents` caller still gets
183
+ // the hard error — they asked for the stream specifically; a `run()`
184
+ // caller asked for a result.
185
+ err instanceof GislStreamHostNotDeclaredError)) {
176
186
  throw err;
177
187
  }
178
188
  finalStatus = await _pollToTerminal(client, {
@@ -7,7 +7,7 @@
7
7
  import { createWriteStream } from 'node:fs';
8
8
  import { Readable } from 'node:stream';
9
9
  import { pipeline } from 'node:stream/promises';
10
- import { GislNetworkError, GislSinkError } from './errors.js';
10
+ import { GislDownloadHttpError, GislRequestNotSentError, GislSinkError, GislTransportError, } from './errors.js';
11
11
  /**
12
12
  * Streams a (typically pre-signed) URL to a local path without buffering the
13
13
  * whole body in memory. Pre-signed download URLs require no SDK auth, so this
@@ -21,22 +21,40 @@ export class HttpDownloader {
21
21
  // on the RunResult sink side. Parity-critical: the FF1 sink contract tells
22
22
  // callers to narrow with instanceof, so the source-read error type must
23
23
  // match across languages.
24
+ // codex f46340e1d58a: a malformed URL fails DETERMINISTICALLY, so it must
25
+ // not land in the always-retryable bucket with DNS and TLS. `fetch` rejects
26
+ // both with an indistinguishable TypeError, so the only way to tell them
27
+ // apart is to check BEFORE the call — which also gives
28
+ // GislRequestNotSentError a real throw site in TypeScript rather than
29
+ // leaving it declared-but-dormant.
30
+ try {
31
+ new URL(url);
32
+ }
33
+ catch {
34
+ throw new GislRequestNotSentError(`Download source is not a valid URL: ${url}`);
35
+ }
24
36
  let res;
25
37
  try {
26
38
  res = await fetch(url);
27
39
  }
28
40
  catch (cause) {
29
41
  // A rejected fetch (DNS, TCP, TLS, mid-flight disconnect) must surface as
30
- // GislNetworkError too — not the raw TypeError — so callers can narrow
31
- // every download-source failure with `instanceof GislNetworkError`
32
- // (codex review medium).
33
- throw new GislNetworkError(`Failed to fetch download source: ${cause instanceof Error ? cause.message : String(cause)}`);
42
+ // a typed error — not the raw TypeError — so callers can narrow every
43
+ // download-source failure with `instanceof GislNetworkError`
44
+ // (codex review medium). t2qCrjdr: TRANSPORT specifically, so the
45
+ // retry advice is `true` here and status-derived below.
46
+ throw new GislTransportError(`Failed to fetch download source: ${cause instanceof Error ? cause.message : String(cause)}`);
34
47
  }
35
48
  if (!res.ok) {
36
- throw new GislNetworkError(`Download failed with status ${res.status}`);
49
+ // t2qCrjdr: the server was REACHED and refused. Carries the status so a
50
+ // consumer telling a permanent 404 from a transient 503 never has to
51
+ // parse the message.
52
+ throw new GislDownloadHttpError(`Download failed with status ${res.status}`, res.status);
37
53
  }
38
54
  if (res.body === null) {
39
- throw new GislNetworkError('Download response had no body');
55
+ // 2xx with nothing in it — the server did not refuse, it under-delivered.
56
+ // Transport rather than HTTP, and retrying is the right advice.
57
+ throw new GislTransportError('Download response had no body');
40
58
  }
41
59
  // `fetch`'s WHATWG ReadableStream and Node's `stream/web` ReadableStream
42
60
  // are structurally the same at runtime but typed in two different lib
@@ -3,7 +3,7 @@ export { parseSseStream } from './sse.js';
3
3
  export type { CreditsUsageOptions, ListWorkflowsOptions, GetSchemaOptions, GetSchemaResult, CapabilitiesSnapshot, PreflightClipError, PreflightClipsResult, ProbeWaitOptions, ProbeWaitResult, GislClientConfig, GislSseEvent, GislSseParseFailure, UploadOptions, WaitOptions, WorkflowCreatePayload, OperationDef, WorkflowSourcePayload, MultiInputSourcePayload, UploadSourcePayload, JobOutputSourcePayload, ExternalImportSourcePayload, ConnectionSourcePayload, JobInputV2Payload, JobDefinitionPayload, ExternalDestinationPayload, DeliveryPayload, DeliveryModePayload, DeliveryBundleFormatPayload, DeliverySelectionPayload, DeliverySelectionTypePayload, DeliveryOutputRefPayload, WorkflowProcessingPayload, ProcessingClassHintPayload, MultipartCheckpointState, _Sdk3HandCodedUploadedPart, _Sdk3HandCodedMultipartStatusResult, _Sdk3HandCodedPresignedPart, _Sdk3HandCodedPresignPartsResult, _Sdk3HandCodedKeepaliveResult, } from './types.js';
4
4
  export { uploadSource, jobOutputSource, externalImportSource, connectionSource, } from './types.js';
5
5
  export type { GislConfigErrorMetadata } from './errors.js';
6
- export { GislError, GislApiError, GislValidationError, GislBalanceExhaustedError, GislLongFormConcurrencyError, GislTierRestrictedError, GislFeatureTierRestrictedError, GislFeatureNotAvailableError, GislWorkflowExpiredError, GislProbePendingError, GislAuthError, GislUploadCapExceededError, GislMultipartPartError, GislMultipartPartCountError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTimeoutError, GislFanOutTimeoutError, GislAbortError, GislNetworkError, GislConfigError, GislMissingCredentialsError, GislFeatureRequiresAuthError, GislUndeclaredAssetError, GislUnusedAssetError, GislPerInputOptionsNotSupportedError, GislChainCardinalityMismatchError, GislBundleAlreadyArchivedError, GislNoSuchKeyError, GislSinkError, GislItemFailedError, GislResultNotReadyError, } from './errors.js';
6
+ export { GislError, GislApiError, GislValidationError, GislBalanceExhaustedError, GislLongFormConcurrencyError, GislTierRestrictedError, GislFeatureTierRestrictedError, GislFeatureNotAvailableError, GislWorkflowExpiredError, GislProbePendingError, GislAuthError, GislUploadCapExceededError, GislMultipartPartError, GislMultipartPartCountError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTimeoutError, GislFanOutTimeoutError, GislAbortError, GislNetworkError, GislTransportError, GislDownloadHttpError, GislRequestNotSentError, GislConfigError, GislMissingCredentialsError, GislStreamHostNotDeclaredError, GislFeatureRequiresAuthError, GislUndeclaredAssetError, GislUnusedAssetError, GislPerInputOptionsNotSupportedError, GislChainCardinalityMismatchError, GislBundleAlreadyArchivedError, GislNoSuchKeyError, GislSinkError, GislItemFailedError, GislResultNotReadyError, } from './errors.js';
7
7
  export type { GislApiErrorOptions, GislUploadCapKind } from './errors.js';
8
8
  export type { ErrorCategory } from './generated/sdk_spec/errors.js';
9
9
  export { RunResult } from './file-first.js';
@@ -33,7 +33,7 @@ export type { SseOperationProgressData, SseOperationCompletedData, SseOperationF
33
33
  export type { MultiOutputCompletion, PageIndexed, PositionIndexed, Unindexed, } from '@giveitsmaller/contracts/asyncapi';
34
34
  import type { MultiOutputCompletion as _MultiOutputCompletion } from '@giveitsmaller/contracts/asyncapi';
35
35
  export type OperationResultOutputEntry = _MultiOutputCompletion['outputs'][number];
36
- export type { CompressImageOptions, CompressImageJpegOptions, CompressImagePngOptions, CompressImageAvifOptions, CompressVideoOptions, CompressAudioOptions, CompressDocumentOfficeOptions, CompressDocumentOdfOptions, CompressDocumentEpubOptions, ThumbnailImageOptions, ThumbnailVideoOptions, ThumbnailDocumentOptions, TransformImageOptions, TransformImageGifOptions, TransformVideoOptions, TransformDocumentPdfOptions, ConvertImageOptions, ConvertVideoOptions, ConvertAudioOptions, ConvertDocumentPdfOptions, MergeImageOptions, MergeVideoOptions, MergeVideoPerInputOptions, MergeAudioOptions, MergeAudioPerInputOptions, ArchiveOptions, ImageWatermarkImageOptions, ImageWatermarkImageGifOptions, TextWatermarkImageOptions, CustomLumaVideoOptions, AudioOverlayAudioOptions, AudioOverlayVideoOptions, AudioWatermarkAudioOptions, AudioWatermarkVideoOptions, AudioToVideoAudioOptions, VideoWatermarkVideoOptions, VideoTextWatermarkVideoOptions, SplitImageGifOptions, SplitDocumentPdfOptions, SplitAudioOptions, SplitVideoOptions, } from '@giveitsmaller/contracts/operations';
36
+ export type { CompressImageOptions, CompressImageJpegOptions, CompressImagePngOptions, CompressImageAvifOptions, CompressVideoOptions, CompressAudioOptions, CompressDocumentOfficeOptions, CompressDocumentOdfOptions, CompressDocumentEpubOptions, ThumbnailImageOptions, ThumbnailVideoOptions, ThumbnailDocumentOfficeOptions, ThumbnailDocumentPdfOptions, ThumbnailDocumentEpubOptions, TransformImageOptions, TransformImageGifOptions, TransformVideoOptions, TransformDocumentPdfOptions, ConvertImageOptions, ConvertVideoOptions, ConvertAudioOptions, ConvertDocumentPdfOptions, MergeImageOptions, MergeVideoOptions, MergeVideoPerInputOptions, MergeAudioOptions, MergeAudioPerInputOptions, ArchiveOptions, ImageWatermarkImageOptions, ImageWatermarkImageGifOptions, TextWatermarkImageOptions, CustomLumaVideoOptions, AudioOverlayAudioOptions, AudioOverlayVideoOptions, AudioWatermarkAudioOptions, AudioWatermarkVideoOptions, AudioToVideoAudioOptions, VideoWatermarkVideoOptions, VideoTextWatermarkVideoOptions, SplitImageGifOptions, SplitDocumentPdfOptions, SplitAudioOptions, SplitVideoOptions, } from '@giveitsmaller/contracts/operations';
37
37
  export { ImageWatermarkImageAnchor, ImageWatermarkImageGifAnchor, TextWatermarkImageAnchor, TextWatermarkImageFontFamily, TextWatermarkImageWatermarkMode, AudioOverlayAudioMode, AudioOverlayVideoMode, AudioOverlayVideoNoAudioTrackBehaviour, AudioWatermarkAudioMethod, AudioWatermarkAudioRobustness, AudioWatermarkAudioDensity, AudioWatermarkVideoMethod, AudioWatermarkVideoRobustness, AudioWatermarkVideoDensity, AudioToVideoAudioOutputResolution, AudioToVideoAudioImageFit, AudioToVideoAudioOutputFormat, VideoWatermarkVideoAnchor, VideoTextWatermarkVideoFontFamily, VideoTextWatermarkVideoWatermarkMode, VideoTextWatermarkVideoAnchor, SplitImageGifOutputFormat, SplitDocumentPdfMode, SplitAudioMode, SplitAudioPrecision, SplitVideoMode, SplitVideoPrecision, } from '@giveitsmaller/contracts/operations';
38
38
  export { archiveMetadata, audioOverlayMetadata, audioWatermarkMetadata, compressMetadata, convertMetadata, customLumaMetadata, imageWatermarkMetadata, mergeMetadata, textWatermarkMetadata, thumbnailMetadata, audioToVideoMetadata, videoWatermarkMetadata, videoTextWatermarkMetadata, splitMetadata, transformMetadata, } from '@giveitsmaller/contracts/operations';
39
39
  export type { ConvertOptions, ThumbnailOptions, TransformOptions, TextWatermarkOptions, WatermarkOptions, WatermarkOverlay, WatermarkAnchor, } from './ergonomic/option_types.js';
@@ -15,11 +15,24 @@ GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMulti
15
15
  // 4G4FaA9X — mapEach fan-out timed out mid-batch; carries the completed
16
16
  // child ids + parent id so the caller can recover without a whole-batch re-run.
17
17
  GislFanOutTimeoutError, GislAbortError,
18
- // FF2b / tywwynmN — transport-level failure (mirrors PHP GislNetworkError);
18
+ // FF2b / tywwynmN — off-envelope failure base (mirrors PHP GislNetworkError);
19
19
  // raised by the file-first HttpDownloader when an output URL cannot be read.
20
+ // t2qCrjdr: NEVER THROWN DIRECTLY any more — it is the hierarchy node the two
21
+ // subclasses below share, kept so existing `instanceof GislNetworkError`
22
+ // narrowing (including the SSE poll-fallback) is unchanged.
20
23
  GislNetworkError,
24
+ // t2qCrjdr — the split. One `retryable` could not be honest for both a DNS
25
+ // failure and a 404, so each case is now its own class with its own answer.
26
+ GislTransportError, GislDownloadHttpError,
27
+ // The request never left the client — never retryable. TS detects only what
28
+ // it can see BEFORE the call (an unparseable URL); PHP also classifies
29
+ // PSR-18's RequestExceptionInterface, which `fetch` gives no equivalent of.
30
+ GislRequestNotSentError,
21
31
  // T1 / wVU4xHx3 — local config-error tree (pre-I/O; sibling of GislApiError).
22
- GislConfigError, GislMissingCredentialsError, GislFeatureRequiresAuthError,
32
+ GislConfigError, GislMissingCredentialsError,
33
+ // VUozk5Bc — `streamEvents` on a client with no DECLARED stream host. The
34
+ // SDK refuses to derive `stream.*` from `api.*`; `run()` polls instead.
35
+ GislStreamHostNotDeclaredError, GislFeatureRequiresAuthError,
23
36
  // T3 / cuecCmb5 — merge-compose local validation errors.
24
37
  GislUndeclaredAssetError, GislUnusedAssetError, GislPerInputOptionsNotSupportedError,
25
38
  // T6 / aDR1jnyZ — chain-cardinality validation (dormant until chain
package/dist/merge.d.ts CHANGED
@@ -123,8 +123,14 @@ export interface MergeOptions {
123
123
  * construction and two-pass target-size is unbuilt. The request fails during
124
124
  * execution, and the SDK cannot warn earlier: the routing decision is made
125
125
  * server-side at create-plan time, so there is nothing here to check it against.
126
- * Short-form merges honour it normally. Tracked by `zJN6XIi5`, blocked on a
127
- * contract that can express per-execution-path availability.
126
+ * Short-form merges honour it normally.
127
+ *
128
+ * The contract CAN now express this — `per_class_availability` scopes an option to
129
+ * a processing class, vendored at v2.195.0 and pinned by
130
+ * `tests/unit/per-class-availability-conformance.test.ts`. That buys an honest 422
131
+ * from the API at CREATE rather than a job dying mid-execution; it does NOT become
132
+ * a client-side gate, because routing is still decided server-side and a duration
133
+ * heuristic here would be wrong at the boundary. Tracked by `zJN6XIi5`.
128
134
  */
129
135
  readonly targetSize?: string | number;
130
136
  readonly transitionDuration?: number;