@giveitsmaller/sdk 0.21.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/README.md +84 -0
- package/dist/builder.js +47 -11
- package/dist/client.d.ts +7 -0
- package/dist/client.js +83 -2
- package/dist/credentials.d.ts +74 -0
- package/dist/credentials.js +123 -0
- package/dist/ergonomic/image_output_routes.d.ts +67 -0
- package/dist/ergonomic/image_output_routes.js +146 -15
- package/dist/ergonomic/option_types.d.ts +11 -2
- package/dist/ergonomic/preset_resolver.d.ts +22 -0
- package/dist/ergonomic/preset_resolver.js +94 -0
- package/dist/ergonomic/presets/image_compress.js +16 -4
- package/dist/ergonomic/presets/video_compress.d.ts +20 -0
- package/dist/errors.d.ts +183 -7
- package/dist/errors.js +203 -7
- package/dist/file-first.js +63 -8
- package/dist/generated/sdk_spec/errors.d.ts +1 -1
- package/dist/generated/sdk_spec/errors.js +176 -1
- package/dist/gisl.d.ts +1 -1
- package/dist/gisl.js +14 -2
- package/dist/handle.js +12 -2
- package/dist/http-downloader.js +25 -7
- package/dist/index.core.d.ts +2 -2
- package/dist/index.core.js +19 -3
- package/dist/merge.d.ts +29 -0
- package/dist/merge.js +12 -2
- package/dist/sse.d.ts +23 -1
- package/dist/sse.js +76 -3
- package/dist/types.d.ts +18 -0
- package/package.json +10 -2
package/dist/errors.js
CHANGED
|
@@ -172,6 +172,23 @@ export class GislLongFormConcurrencyError extends GislApiError {
|
|
|
172
172
|
super(statusCode, errorMessage, path, undefined, buildOptionsWithPayload(payload, extra));
|
|
173
173
|
this.name = 'GislLongFormConcurrencyError';
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* ALWAYS `false`, overriding the base 429-implies-retryable heuristic
|
|
177
|
+
* (UO1xYecu). This 429 is not a rate limit: it carries no `Retry-After` and
|
|
178
|
+
* clears only when an in-flight long-form workflow finishes, so a back-off
|
|
179
|
+
* retries into a wall that no amount of waiting-then-retrying opens. The base
|
|
180
|
+
* accessor reported `true` purely from the status, contradicting this class's
|
|
181
|
+
* own documented handling ("wait on completion or upgrade — do NOT back off")
|
|
182
|
+
* and instructing the one recovery that cannot work.
|
|
183
|
+
*
|
|
184
|
+
* Overridden per-class rather than via a code table because this is the only
|
|
185
|
+
* such code today; the general fix — an explicit taxonomy verdict outranking
|
|
186
|
+
* the status heuristic — arrives with the `error-taxonomy.yaml` `retryable`
|
|
187
|
+
* enum (contracts `plwcAqBr`), tracked on UO1xYecu.
|
|
188
|
+
*/
|
|
189
|
+
get retryable() {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
175
192
|
/** The pricing / upgrade deep link (`links.upgrade`), or `undefined` when absent. */
|
|
176
193
|
get upgradeUrl() {
|
|
177
194
|
return this.payload.links?.upgrade;
|
|
@@ -381,6 +398,34 @@ export class GislMissingCredentialsError extends GislConfigError {
|
|
|
381
398
|
this.name = 'GislMissingCredentialsError';
|
|
382
399
|
}
|
|
383
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
|
+
}
|
|
384
429
|
/**
|
|
385
430
|
* The caller used `gisl.anonymous()` and then invoked an operation that is
|
|
386
431
|
* not in the anonymous-capable allowlist. Local-only — thrown before any I/O.
|
|
@@ -506,13 +551,71 @@ export class GislTimeoutError extends GislError {
|
|
|
506
551
|
}
|
|
507
552
|
}
|
|
508
553
|
/**
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
* `
|
|
514
|
-
*
|
|
515
|
-
*
|
|
554
|
+
* A `mapEach` fan-out timed out mid-batch — the deadline elapsed either while a
|
|
555
|
+
* child was still running (the common case) or cleanly between child runs. The
|
|
556
|
+
* parent and some children have ALREADY completed, so re-running the whole batch
|
|
557
|
+
* re-does finished work. This carries their ids so the caller can poll them (via
|
|
558
|
+
* `client.getWorkflowStatus` / `getWorkflowDownloads`) to recover the finished
|
|
559
|
+
* work and re-run ONLY the children that were never created.
|
|
560
|
+
*
|
|
561
|
+
* Subclasses {@link GislTimeoutError}, so an existing
|
|
562
|
+
* `catch (e) { if (e instanceof GislTimeoutError) … }` still catches it. The
|
|
563
|
+
* inherited `workflowId` carries the IN-FLIGHT child — the one that was running
|
|
564
|
+
* when the deadline elapsed (a child's own timeout, the common path) — or stays
|
|
565
|
+
* `undefined` when the deadline elapsed cleanly BETWEEN children (no in-flight
|
|
566
|
+
* child). To recover, poll `workflowId` (if set) + {@link parentWorkflowId} +
|
|
567
|
+
* {@link completedWorkflowIds}, then re-run only the children that never started.
|
|
568
|
+
*
|
|
569
|
+
* NOTE on double-charge: the server-side create-dedupe (DSxwCetg) is what
|
|
570
|
+
* prevents a byte-identical child re-create from settling a SECOND charge within
|
|
571
|
+
* the dedup window; this error's job is efficient RECOVERY (skip the completed
|
|
572
|
+
* work) + defense-in-depth, not the sole charge guard.
|
|
573
|
+
*/
|
|
574
|
+
export class GislFanOutTimeoutError extends GislTimeoutError {
|
|
575
|
+
/** The child workflows that completed before the deadline elapsed. */
|
|
576
|
+
completedWorkflowIds;
|
|
577
|
+
/** The parent workflow, which ran to completion before the fan-out began. */
|
|
578
|
+
parentWorkflowId;
|
|
579
|
+
constructor(message, opts) {
|
|
580
|
+
// The inherited workflowId is the in-flight child (or undefined between children).
|
|
581
|
+
super(message, opts.workflowId);
|
|
582
|
+
this.name = 'GislFanOutTimeoutError';
|
|
583
|
+
this.completedWorkflowIds = [...opts.completedWorkflowIds];
|
|
584
|
+
this.parentWorkflowId = opts.parentWorkflowId === '' ? undefined : opts.parentWorkflowId;
|
|
585
|
+
if (opts.cause !== undefined) {
|
|
586
|
+
this.cause = opts.cause;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
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`.
|
|
516
619
|
*/
|
|
517
620
|
export class GislNetworkError extends GislError {
|
|
518
621
|
constructor(message) {
|
|
@@ -520,6 +623,99 @@ export class GislNetworkError extends GislError {
|
|
|
520
623
|
this.name = 'GislNetworkError';
|
|
521
624
|
}
|
|
522
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
|
+
}
|
|
523
719
|
/**
|
|
524
720
|
* Internal control-flow marker (TDqmkWpX): the SSE event stream closed cleanly
|
|
525
721
|
* WITHOUT a terminal (`workflow_completed`/`failed`/`partially_failed`) event.
|
package/dist/file-first.js
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
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';
|
|
16
16
|
import { validateVerbOptions, assertThumbnailDimensions } from './ergonomic/option_validation.js';
|
|
17
|
-
import { resolveOutputRoute, tokenForMime, tokenForPath, isPlannedValue, isUnknownEnumValue, FACADE_MANAGED_OUTPUTS, } from './ergonomic/image_output_routes.js';
|
|
17
|
+
import { resolveOutputRoute, tokenForMime, tokenForPath, isPlannedValue, isUnknownEnumValue, dependsOnViolation, FACADE_MANAGED_OUTPUTS, } from './ergonomic/image_output_routes.js';
|
|
18
18
|
import { OptimizeFor } from './generated/sdk_spec/enums.js';
|
|
19
19
|
import { uploadSource, jobOutputSource } from './types.js';
|
|
20
20
|
// Value import used only at call-time (inside MergedRecipe.toWorkflowPayload),
|
|
@@ -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 ||
|
|
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
|
}
|
|
@@ -1101,7 +1111,7 @@ export class Recipe {
|
|
|
1101
1111
|
if (requested !== undefined && FACADE_MANAGED_OUTPUTS.includes(requested)) {
|
|
1102
1112
|
const facade = { output_format: requested };
|
|
1103
1113
|
for (const [key, value] of Object.entries(step.options)) {
|
|
1104
|
-
if (key === 'output_format' || value === undefined)
|
|
1114
|
+
if (key === 'output_format' || value === undefined || value === null)
|
|
1105
1115
|
continue;
|
|
1106
1116
|
if (key !== 'quality') {
|
|
1107
1117
|
throw new GislConfigError(`output(): '${key}' needs a detectable input format to route; reference the file by ` +
|
|
@@ -1122,7 +1132,9 @@ export class Recipe {
|
|
|
1122
1132
|
}
|
|
1123
1133
|
const wireOptions = { output_format: resolved.outputFormatWire };
|
|
1124
1134
|
for (const [key, value] of Object.entries(step.options)) {
|
|
1125
|
-
|
|
1135
|
+
// Drop a null value (as PHP does) so a null option never reaches the wire
|
|
1136
|
+
// and is treated as absent by the depends_on gate — full null parity (codex).
|
|
1137
|
+
if (key === 'output_format' || value === undefined || value === null)
|
|
1126
1138
|
continue;
|
|
1127
1139
|
if (resolved.planned.has(key)) {
|
|
1128
1140
|
throw new GislConfigError(`output(): '${key}' is advertised but not available yet on the ${resolved.route} route ` +
|
|
@@ -1147,6 +1159,28 @@ export class Recipe {
|
|
|
1147
1159
|
}
|
|
1148
1160
|
wireOptions[key] = value;
|
|
1149
1161
|
}
|
|
1162
|
+
// quality_preset's contract `depends_on: { encoding_mode: auto_quality }`
|
|
1163
|
+
// (86gAu5Tr) — infer the mode when the caller set NONE so the preset forms a
|
|
1164
|
+
// VALID request. SAME_FORMAT only: encoding_mode is a compress optimiser and
|
|
1165
|
+
// quality_preset isn't honored on a format_change. An explicitly-conflicting
|
|
1166
|
+
// mode is rejected by the general depends_on gate below.
|
|
1167
|
+
if (resolved.route === 'same_format' &&
|
|
1168
|
+
wireOptions.quality_preset !== undefined &&
|
|
1169
|
+
wireOptions.encoding_mode === undefined) {
|
|
1170
|
+
wireOptions.encoding_mode = 'auto_quality';
|
|
1171
|
+
}
|
|
1172
|
+
// General contract `depends_on` validation (ehHU08Hu), scoped per route: the
|
|
1173
|
+
// universal fit→width|height dep runs on BOTH routes (identical in compress +
|
|
1174
|
+
// convert); the encoding_mode-family deps run on same_format only. Subsumes
|
|
1175
|
+
// the 86gAu5Tr auto_quality gate plus target_size_bytes-without-target_size,
|
|
1176
|
+
// fit-without-width/height, and any future compress-image depends_on.
|
|
1177
|
+
const dependency = dependsOnViolation(wireOptions, resolved.route);
|
|
1178
|
+
if (dependency !== undefined) {
|
|
1179
|
+
throw new GislConfigError(dependency.message, {
|
|
1180
|
+
reason: 'invalid_option_combination',
|
|
1181
|
+
conflictingFields: [...dependency.conflictingFields],
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1150
1184
|
return { type: resolved.sourceOp, options: wireOptions };
|
|
1151
1185
|
}
|
|
1152
1186
|
/**
|
|
@@ -1466,9 +1500,22 @@ function _validateWatermarkOverlay(overlay) {
|
|
|
1466
1500
|
}
|
|
1467
1501
|
}
|
|
1468
1502
|
function _lowerWatermarkOp(wireOp, options) {
|
|
1469
|
-
//
|
|
1470
|
-
//
|
|
1471
|
-
// the `
|
|
1503
|
+
// `overlays[]` (the multi-overlay stack) is a live contract option but is NOT
|
|
1504
|
+
// reachable through watermark(): the facade composites exactly ONE overlay —
|
|
1505
|
+
// the positional `overlay` (wire source src_1) — so overlays[1..] reference
|
|
1506
|
+
// sources it cannot create, any entry is invalid on a non-image base, and the
|
|
1507
|
+
// contract's `minItems: 1` makes an empty array invalid too. Reject it here at
|
|
1508
|
+
// lowering (mutation-safe — reads the FINAL options, catching a post-watermark()
|
|
1509
|
+
// `opts.overlays = [...]`) and point callers at the single-overlay knobs.
|
|
1510
|
+
// Real multi-overlay stacking is a future feature (Vbbdq9C4).
|
|
1511
|
+
if (options.overlays !== undefined) {
|
|
1512
|
+
throw new GislConfigError("watermark(): 'overlays[]' (multi-overlay stacking) is not supported — watermark() composites a " +
|
|
1513
|
+
'single overlay (the positional overlay argument). Use the top-level anchor / opacity / margin_x / ' +
|
|
1514
|
+
'margin_y / overlay_width options to place it. Multi-overlay stacking is a future feature.', { reason: 'overlays_unsupported', conflictingFields: ['overlays'] });
|
|
1515
|
+
}
|
|
1516
|
+
// The remaining watermark options (anchor/opacity/margin_x/margin_y/
|
|
1517
|
+
// overlay_width) are already wire keys; empty options omit the `options` key
|
|
1518
|
+
// (byte-identical to PHP).
|
|
1472
1519
|
const wire = { ...options };
|
|
1473
1520
|
return Object.keys(wire).length === 0 ? { type: wireOp } : { type: wireOp, options: wire };
|
|
1474
1521
|
}
|
|
@@ -1489,6 +1536,14 @@ function _lowerWatermarkOp(wireOp, options) {
|
|
|
1489
1536
|
*/
|
|
1490
1537
|
async function _uploadInputsAndCreate(client, inputs, toPayload, opts) {
|
|
1491
1538
|
const { webhook, deadline, onProgress, signal, probeBeforeCreate, probeTimeoutMs, uploadsLabel, workflowLabel } = opts;
|
|
1539
|
+
// Preflight: lower the composed chains with placeholder ids so a route-invalid
|
|
1540
|
+
// option (or any lowering-time gate — overlays, sole_op split, route/enum) in
|
|
1541
|
+
// ANY input's chain — a watermark base/overlay, a merge/archive member — throws
|
|
1542
|
+
// BEFORE we spend a single upload byte. The multi-input analog of the
|
|
1543
|
+
// single-input Recipe.assertOperationsLowerable preflight (0azjb6Rg); mirrors
|
|
1544
|
+
// PHP. The placeholder ids never reach the wire — the payload is discarded
|
|
1545
|
+
// (T3ltXsou). toWorkflowPayload is pure, so re-lowering at create is cheap.
|
|
1546
|
+
toPayload(inputs.map((_, i) => `preflight_${i}`));
|
|
1492
1547
|
const fileIds = [];
|
|
1493
1548
|
// Track each freshly-uploaded input's probe-gate inputs (a pre-uploaded id
|
|
1494
1549
|
// carries no local mime/size, so it is excluded — never probed).
|
|
@@ -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";
|
|
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 {
|
|
@@ -377,7 +377,7 @@ export const ERROR_CODES = Object.freeze({
|
|
|
377
377
|
status: "wired",
|
|
378
378
|
httpStatus: 422,
|
|
379
379
|
retryable: true,
|
|
380
|
-
sdkClass: "
|
|
380
|
+
sdkClass: "GislProbePendingError",
|
|
381
381
|
description: "422 on workflow create — upload probing not yet complete; retry after the upload finishes probing. Wire `error_type: \"probe_pending\"`.",
|
|
382
382
|
metadataSchema: Object.freeze({
|
|
383
383
|
"jobRef": "string",
|
|
@@ -506,6 +506,169 @@ 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
|
+
}),
|
|
536
|
+
"long_form_concurrency_limit_exceeded": Object.freeze({
|
|
537
|
+
code: "long_form_concurrency_limit_exceeded",
|
|
538
|
+
category: "api",
|
|
539
|
+
source: "ErrorEnvelope.error",
|
|
540
|
+
status: "wired",
|
|
541
|
+
httpStatus: 429,
|
|
542
|
+
retryable: false,
|
|
543
|
+
sdkClass: "GislLongFormConcurrencyError",
|
|
544
|
+
description: "429 — the caller's tier long-form concurrency allowance is exhausted. Wire value is UPPERCASE `LONG_FORM_CONCURRENCY_LIMIT_EXCEEDED` (api.yaml:1995); this file's `code:` follows the taxonomy's lowercase convention. Dispatched on the machine `error` code, NOT on the status: a generic infra rate-limit 429 carries a different/absent code and falls through to base GislApiError where retryAfterSeconds applies. Do not widen this entry to the status.",
|
|
545
|
+
metadataSchema: Object.freeze({
|
|
546
|
+
"currentTier": "string",
|
|
547
|
+
"maxDurationSeconds": "number",
|
|
548
|
+
}),
|
|
549
|
+
}),
|
|
550
|
+
"unprocessable_entity": Object.freeze({
|
|
551
|
+
code: "unprocessable_entity",
|
|
552
|
+
category: "auth",
|
|
553
|
+
source: "error_type",
|
|
554
|
+
status: "wired",
|
|
555
|
+
httpStatus: 422,
|
|
556
|
+
retryable: false,
|
|
557
|
+
sdkClass: "GislAuthRejectionError",
|
|
558
|
+
description: "422 on an auth-side-effect endpoint (register / verify-email / api-keys) — the request was well-formed but rejected on domain grounds. Dispatched on MEMBERSHIP of AuthRejectionEnvelope.error_type, per ADR-0019.",
|
|
559
|
+
metadataSchema: Object.freeze({
|
|
560
|
+
"errorType": "string",
|
|
561
|
+
}),
|
|
562
|
+
}),
|
|
563
|
+
"email_same": Object.freeze({
|
|
564
|
+
code: "email_same",
|
|
565
|
+
category: "auth",
|
|
566
|
+
source: "error_type",
|
|
567
|
+
status: "wired",
|
|
568
|
+
httpStatus: 422,
|
|
569
|
+
retryable: false,
|
|
570
|
+
sdkClass: "GislAuthRejectionError",
|
|
571
|
+
description: "422 on profile PATCH — the submitted email matches the current one, so there is nothing to change. Same envelope and sdkClass as unprocessable_entity; separate row because the wire value differs.",
|
|
572
|
+
metadataSchema: Object.freeze({
|
|
573
|
+
"errorType": "string",
|
|
574
|
+
}),
|
|
575
|
+
}),
|
|
576
|
+
"config_error": Object.freeze({
|
|
577
|
+
code: "config_error",
|
|
578
|
+
category: "config",
|
|
579
|
+
source: "SDK_local",
|
|
580
|
+
status: "wired",
|
|
581
|
+
httpStatus: null,
|
|
582
|
+
retryable: false,
|
|
583
|
+
sdkClass: "GislConfigError",
|
|
584
|
+
description: "Client-side configuration rejected before any request — thrown directly (preset resolver, output() gates), not only as a base class. `reason` carries the discriminator: `unknown_field` and `type_mismatch` are values of THIS field and deliberately have no rows of their own.",
|
|
585
|
+
metadataSchema: Object.freeze({
|
|
586
|
+
"reason": "string",
|
|
587
|
+
"conflictingFields": "array",
|
|
588
|
+
"resolvedSnapshot": "object",
|
|
589
|
+
"suggestion": "string",
|
|
590
|
+
}),
|
|
591
|
+
}),
|
|
592
|
+
"bundle_already_archived": Object.freeze({
|
|
593
|
+
code: "bundle_already_archived",
|
|
594
|
+
category: "config",
|
|
595
|
+
source: "SDK_local",
|
|
596
|
+
status: "planned",
|
|
597
|
+
httpStatus: null,
|
|
598
|
+
retryable: false,
|
|
599
|
+
sdkClass: "GislBundleAlreadyArchivedError",
|
|
600
|
+
description: "PLANNED — not reachable in any shipped build. Raised when a bundle operation targets an already-archived bundle, once `.bundle()` ships. Do not write handler code against this yet.",
|
|
601
|
+
metadataSchema: Object.freeze({}),
|
|
602
|
+
}),
|
|
603
|
+
"fan_out_timeout": Object.freeze({
|
|
604
|
+
code: "fan_out_timeout",
|
|
605
|
+
category: "network",
|
|
606
|
+
source: "SDK_local",
|
|
607
|
+
status: "wired",
|
|
608
|
+
httpStatus: null,
|
|
609
|
+
retryable: true,
|
|
610
|
+
sdkClass: "GislFanOutTimeoutError",
|
|
611
|
+
description: "A fan-out deadline elapsed before all children finished. The three metadata fields are the reason this is declared separately from GislTimeoutError: completedWorkflowIds are the children that DID finish, parentWorkflowId ran to completion before the fan-out began, and the inherited workflowId is the in-flight child — absent on a clean between-children timeout.",
|
|
612
|
+
metadataSchema: Object.freeze({
|
|
613
|
+
"completedWorkflowIds": "array",
|
|
614
|
+
"parentWorkflowId": "string",
|
|
615
|
+
"workflowId": "string",
|
|
616
|
+
}),
|
|
617
|
+
}),
|
|
618
|
+
"no_such_key": Object.freeze({
|
|
619
|
+
code: "no_such_key",
|
|
620
|
+
category: "chain",
|
|
621
|
+
source: "SDK_local",
|
|
622
|
+
status: "wired",
|
|
623
|
+
httpStatus: null,
|
|
624
|
+
retryable: false,
|
|
625
|
+
sdkClass: "GislNoSuchKeyError",
|
|
626
|
+
description: "The caller asked for a mapEach result key that does not exist in the output. NOTE the key itself is NOT captured in metadata today — it is interpolated into the message only, so consumers cannot branch on which key was missing.",
|
|
627
|
+
metadataSchema: Object.freeze({}),
|
|
628
|
+
}),
|
|
629
|
+
"result_not_ready": Object.freeze({
|
|
630
|
+
code: "result_not_ready",
|
|
631
|
+
category: "validation",
|
|
632
|
+
source: "SDK_local",
|
|
633
|
+
status: "wired",
|
|
634
|
+
httpStatus: null,
|
|
635
|
+
retryable: true,
|
|
636
|
+
sdkClass: "GislResultNotReadyError",
|
|
637
|
+
description: "The caller read a result before the workflow reached a terminal state. `state` carries the non-terminal status observed.",
|
|
638
|
+
metadataSchema: Object.freeze({
|
|
639
|
+
"workflowId": "string",
|
|
640
|
+
"state": "string",
|
|
641
|
+
}),
|
|
642
|
+
}),
|
|
643
|
+
"sink_error": Object.freeze({
|
|
644
|
+
code: "sink_error",
|
|
645
|
+
category: "config",
|
|
646
|
+
source: "SDK_local",
|
|
647
|
+
status: "wired",
|
|
648
|
+
httpStatus: null,
|
|
649
|
+
retryable: false,
|
|
650
|
+
sdkClass: "GislSinkError",
|
|
651
|
+
description: "A caller-supplied output sink could not be used. Branch on `reason` — the values span both caller setup (invalid_directory, duplicate_filename) and runtime write outcomes (write_failed, partial_failure), so the category is a best fit rather than an exact one.",
|
|
652
|
+
metadataSchema: Object.freeze({
|
|
653
|
+
"reason": "string",
|
|
654
|
+
}),
|
|
655
|
+
}),
|
|
656
|
+
"item_failed": Object.freeze({
|
|
657
|
+
code: "item_failed",
|
|
658
|
+
category: "api",
|
|
659
|
+
source: "SDK_local",
|
|
660
|
+
status: "wired",
|
|
661
|
+
httpStatus: null,
|
|
662
|
+
retryable: false,
|
|
663
|
+
sdkClass: "GislItemFailedError",
|
|
664
|
+
description: "CARRIED, not thrown — an entry in a per-item failed[] collection describing one item's server-side failure. `errorCode` carries the server's own code where present; prefer branching on that over this entry.",
|
|
665
|
+
metadataSchema: Object.freeze({
|
|
666
|
+
"key": "string",
|
|
667
|
+
"state": "string",
|
|
668
|
+
"errorMessage": "string",
|
|
669
|
+
"errorCode": "string",
|
|
670
|
+
}),
|
|
671
|
+
}),
|
|
509
672
|
});
|
|
510
673
|
export const ERROR_CATEGORIES = Object.freeze({
|
|
511
674
|
api: Object.freeze([
|
|
@@ -522,20 +685,30 @@ export const ERROR_CATEGORIES = Object.freeze({
|
|
|
522
685
|
"requires_reencode",
|
|
523
686
|
"image_dimensions_too_large",
|
|
524
687
|
"workflow_failed",
|
|
688
|
+
"sse_connection_limit_exceeded",
|
|
689
|
+
"sse_capacity_exhausted",
|
|
690
|
+
"long_form_concurrency_limit_exceeded",
|
|
691
|
+
"item_failed",
|
|
525
692
|
]),
|
|
526
693
|
config: Object.freeze([
|
|
527
694
|
"missing_credentials",
|
|
528
695
|
"feature_requires_auth",
|
|
696
|
+
"config_error",
|
|
697
|
+
"bundle_already_archived",
|
|
698
|
+
"sink_error",
|
|
529
699
|
]),
|
|
530
700
|
network: Object.freeze([
|
|
531
701
|
"timeout",
|
|
532
702
|
"aborted",
|
|
533
703
|
"upload_failed",
|
|
704
|
+
"fan_out_timeout",
|
|
534
705
|
]),
|
|
535
706
|
auth: Object.freeze([
|
|
536
707
|
"auth_failed",
|
|
537
708
|
"multipart_session_ownership",
|
|
538
709
|
"multipart_session_auth_required",
|
|
710
|
+
"unprocessable_entity",
|
|
711
|
+
"email_same",
|
|
539
712
|
]),
|
|
540
713
|
validation: Object.freeze([
|
|
541
714
|
"multipart_part_invalid",
|
|
@@ -551,11 +724,13 @@ export const ERROR_CATEGORIES = Object.freeze({
|
|
|
551
724
|
"missing_dependency",
|
|
552
725
|
"unsupported_value",
|
|
553
726
|
"type_mismatch",
|
|
727
|
+
"result_not_ready",
|
|
554
728
|
]),
|
|
555
729
|
chain: Object.freeze([
|
|
556
730
|
"undeclared_asset",
|
|
557
731
|
"unused_asset",
|
|
558
732
|
"per_input_options_not_supported",
|
|
559
733
|
"chain_cardinality_mismatch",
|
|
734
|
+
"no_such_key",
|
|
560
735
|
]),
|
|
561
736
|
});
|
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
|
}
|