@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/README.md
CHANGED
|
@@ -56,6 +56,90 @@ for (const artifact of many.artifacts) console.log(artifact.url);
|
|
|
56
56
|
> only under the same auth that created it; the upload-then-create flow above is
|
|
57
57
|
> consistent by construction. Anonymous-intake uploads are unaffected.
|
|
58
58
|
|
|
59
|
+
## Known limitation — browser SSE is restricted to one origin
|
|
60
|
+
|
|
61
|
+
**Applies to the browser build only** (`@giveitsmaller/sdk/browser`), and **only to live progress
|
|
62
|
+
streaming** — `streamEvents()`, and the `run({ useSSE: true })` default that uses it.
|
|
63
|
+
|
|
64
|
+
Live progress is served from a **separate host** to the rest of the API, and that host allows
|
|
65
|
+
**exactly one browser origin**: the Give It Smaller web app. So from a browser on any other origin:
|
|
66
|
+
|
|
67
|
+
| what you are doing | works? |
|
|
68
|
+
|---|---|
|
|
69
|
+
| Uploads, workflow create, status, downloads | **Yes** — these stay on the main API host, which allows a **list** of origins (per environment) |
|
|
70
|
+
| `streamEvents()` / SSE progress | **No** — blocked at the CORS preflight |
|
|
71
|
+
|
|
72
|
+
**Affected consumers:** third-party sites embedding the SDK, embedded/iframe use, and **local
|
|
73
|
+
development against staging** — which is the one most likely to bite first, because it looks like a
|
|
74
|
+
bug in your code.
|
|
75
|
+
|
|
76
|
+
⚠️ **Local dev against staging is the sharp edge, and the reason is worth stating:** the two hosts
|
|
77
|
+
have *different* CORS policies today. The main API host allows a **list** of origins — and the
|
|
78
|
+
**staging** list includes the usual localhost dev ports — so a browser on `localhost` works against
|
|
79
|
+
staging today. The stream host is configured with exactly **one** origin. So everything keeps
|
|
80
|
+
working right up until live progress, and then fails with a CORS error — which reads like a mistake
|
|
81
|
+
in your own application rather than a deployment setting.
|
|
82
|
+
|
|
83
|
+
**Production allows only the production web app on both hosts**, and always has.
|
|
84
|
+
|
|
85
|
+
**Workaround:** pass `useSSE: false` to `run()`. The SDK falls back to polling, which goes to the
|
|
86
|
+
main API host and is unaffected. Everything else about the call is identical.
|
|
87
|
+
|
|
88
|
+
**Why it is not a one-line config change — and why it is NOT impossible.** An earlier version of
|
|
89
|
+
this section said multi-origin support could not be done on this host. **That was wrong, and it is
|
|
90
|
+
corrected here.** What is true: `Access-Control-Allow-Origin` accepts exactly one origin (a
|
|
91
|
+
comma-separated list is not valid), and because the stream can be cookie-credentialed, the CORS
|
|
92
|
+
specification also forbids answering `*` alongside `Access-Control-Allow-Credentials: true`. What
|
|
93
|
+
does **not** follow is that more origins are unreachable. The server can validate the request's
|
|
94
|
+
`Origin` and echo it back, which is how every multi-origin credentialed endpoint works. That is a
|
|
95
|
+
**change somebody has to build and get right**, not a platform prohibition — so treat this as
|
|
96
|
+
unshipped work with its own correctness risk, not as a closed door.
|
|
97
|
+
|
|
98
|
+
**On authentication:** prefer an API key (`bearerAuth`) or the anonymous capability token on the
|
|
99
|
+
stream host. Cookie/session auth is accepted by the endpoint but a *credentialed cross-origin*
|
|
100
|
+
request additionally needs the browser to opt in and the server to answer with matching credential
|
|
101
|
+
headers — cookie domain scope alone is not sufficient, and this path is not verified.
|
|
102
|
+
|
|
103
|
+
**Node consumers are unaffected** — CORS is a browser mechanism. The PHP SDK is unaffected for the
|
|
104
|
+
same reason.
|
|
105
|
+
|
|
106
|
+
> ⚠️ This limitation is invisible to automated testing: our own app's origin is allowed, so every
|
|
107
|
+
> test and canary we run passes while a consumer on another origin fails. It is written here because
|
|
108
|
+
> nothing else would tell you.
|
|
109
|
+
|
|
110
|
+
## Pointing the event stream at its own host
|
|
111
|
+
|
|
112
|
+
Live progress is served from a **second host**, separate from `baseUrl`. The SDK reads that host
|
|
113
|
+
from the contract's declaration — it **never derives** `stream.*` from `api.*`.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// Resolved from the environment's declared stream host.
|
|
117
|
+
const client = await gisl.create({ apiKey, environment: 'staging' });
|
|
118
|
+
|
|
119
|
+
// Or point it explicitly. This moves the STREAM only — uploads, workflow-create
|
|
120
|
+
// and downloads still go to baseUrl.
|
|
121
|
+
const client = await gisl.create({
|
|
122
|
+
apiKey,
|
|
123
|
+
environment: 'staging',
|
|
124
|
+
streamBaseUrl: 'https://stream.example.com',
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`GISL_STREAM_BASE_URL` does the same thing from the environment. Precedence matches `baseUrl`:
|
|
129
|
+
explicit argument, then `environment`, then the env var.
|
|
130
|
+
|
|
131
|
+
⚠️ **If nothing declares a stream host, the SDK does not fall back to `baseUrl`.** `streamEvents()`
|
|
132
|
+
throws `GislStreamHostNotDeclaredError` (a `GislConfigError`), and `run()` silently uses polling
|
|
133
|
+
instead, which is a working transport. **This is deliberate.** Guessing the stream host from the API
|
|
134
|
+
host is a convention, and the last time a client did that it streamed into a gateway that cannot
|
|
135
|
+
stream, invisibly — a silent fallback looks exactly like a working one.
|
|
136
|
+
|
|
137
|
+
Both `prod` and `staging` resolve to their contract-declared hosts (production landed with
|
|
138
|
+
contracts `v2.195.0`), and an **unconfigured** client resolves production for the stream just as it
|
|
139
|
+
already did for the API. What still fails closed is a **custom** host: pass your own `baseUrl` (or
|
|
140
|
+
set `GISL_BASE_URL`) with no `streamBaseUrl` and `streamEvents()` raises rather than guessing that
|
|
141
|
+
your proxy's stream lives at production.
|
|
142
|
+
|
|
59
143
|
## Documentation
|
|
60
144
|
|
|
61
145
|
Full documentation — getting started and concepts, the `GislClient` reference and operation
|
package/dist/builder.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { SseEventType, SseOperationProgressDataFromJSON, } from '@giveitsmaller/contracts/openapi';
|
|
28
28
|
import { uploadSource } from './types.js';
|
|
29
|
-
import { GislTimeoutError, GislNetworkError, SseEndedWithoutTerminal } from './errors.js';
|
|
29
|
+
import { GislTimeoutError, GislFanOutTimeoutError, GislNetworkError, GislStreamHostNotDeclaredError, GislTransportError, SseEndedWithoutTerminal } from './errors.js';
|
|
30
30
|
// Deferred-usage-only import: `Handle` is constructed inside submit() at call
|
|
31
31
|
// time, not at module load, so the builder.ts <-> handle.ts cycle is safe
|
|
32
32
|
// under ESM (handle.ts imports the await-primitives from this module).
|
|
@@ -403,7 +403,17 @@ export class OperationBuilder {
|
|
|
403
403
|
// Everything else — timeout, abort, API error, an onProgress callback
|
|
404
404
|
// throw, anything unexpected — MUST propagate; re-issuing the same doomed
|
|
405
405
|
// request via poll would mask the real failure.
|
|
406
|
-
if (!(err instanceof SseEndedWithoutTerminal ||
|
|
406
|
+
if (!(err instanceof SseEndedWithoutTerminal ||
|
|
407
|
+
err instanceof GislNetworkError ||
|
|
408
|
+
// VUozk5Bc: no stream host is DECLARED for this configuration (a
|
|
409
|
+
// configuration nothing declares; both named environments resolve as of
|
|
410
|
+
// contracts v2.195.0). That is not a failure to recover from,
|
|
411
|
+
// it is SSE being unavailable here, and polling is a working
|
|
412
|
+
// transport. Failing hard instead would strand every caller on a host
|
|
413
|
+
// nobody has declared yet. A DIRECT `streamEvents` caller still gets
|
|
414
|
+
// the hard error — they asked for the stream specifically; a `run()`
|
|
415
|
+
// caller asked for a result.
|
|
416
|
+
err instanceof GislStreamHostNotDeclaredError)) {
|
|
407
417
|
throw err;
|
|
408
418
|
}
|
|
409
419
|
// Genuine SSE stream-end / transport error — fall through to poll fallback.
|
|
@@ -446,13 +456,38 @@ export class MapEachBuilder {
|
|
|
446
456
|
_checkAborted(options.signal);
|
|
447
457
|
const remaining = deadline - Date.now();
|
|
448
458
|
if (remaining <= 0) {
|
|
449
|
-
|
|
459
|
+
// Clean timeout BETWEEN children (no child in flight): the parent + the
|
|
460
|
+
// children completed so far are recoverable — carry their ids so the
|
|
461
|
+
// caller polls them and re-runs ONLY the never-created children (4G4FaA9X).
|
|
462
|
+
throw new GislFanOutTimeoutError(`maxWait elapsed during fan-out (after ${collectedChildResults.length} child runs)`, {
|
|
463
|
+
completedWorkflowIds: collectedChildResults.map((r) => r.workflowId),
|
|
464
|
+
parentWorkflowId: parentResult.workflowId,
|
|
465
|
+
});
|
|
450
466
|
}
|
|
451
467
|
const childBuilder = this.fn(art);
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
468
|
+
let childResult;
|
|
469
|
+
try {
|
|
470
|
+
childResult = await childBuilder.run({
|
|
471
|
+
...options,
|
|
472
|
+
maxWait: remaining,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
catch (err) {
|
|
476
|
+
// A CHILD's own deadline elapsed mid-run — the COMMON fan-out timeout
|
|
477
|
+
// path. Re-throw as a fan-out timeout so the parent + already-completed
|
|
478
|
+
// children + this in-flight child are ALL recoverable, instead of losing
|
|
479
|
+
// them behind the child's bare GislTimeoutError (4G4FaA9X). Other errors
|
|
480
|
+
// (config / API / item failure) propagate unchanged.
|
|
481
|
+
if (err instanceof GislTimeoutError) {
|
|
482
|
+
throw new GislFanOutTimeoutError(`maxWait elapsed during fan-out while a child was running (${collectedChildResults.length} completed)`, {
|
|
483
|
+
completedWorkflowIds: collectedChildResults.map((r) => r.workflowId),
|
|
484
|
+
parentWorkflowId: parentResult.workflowId,
|
|
485
|
+
workflowId: err.workflowId,
|
|
486
|
+
cause: err,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
throw err;
|
|
490
|
+
}
|
|
456
491
|
collectedChildResults.push(childResult);
|
|
457
492
|
for (const childArt of childResult.artifacts)
|
|
458
493
|
collectedArtifacts.push(childArt);
|
|
@@ -567,7 +602,7 @@ export async function _consumeSseToTerminal(client, args) {
|
|
|
567
602
|
// GislNetworkError so the await-terminal callers poll-fallback on it
|
|
568
603
|
// (and ONLY on it / a clean stream-end), never on an onProgress throw.
|
|
569
604
|
if (err instanceof TypeError) {
|
|
570
|
-
throw new
|
|
605
|
+
throw new GislTransportError(`SSE connect to workflow ${args.workflowId} events failed: ${err.message}`);
|
|
571
606
|
}
|
|
572
607
|
throw err;
|
|
573
608
|
}
|
|
@@ -649,11 +684,12 @@ export async function _consumeSseToTerminal(client, args) {
|
|
|
649
684
|
throw new GislTimeoutError(`Workflow ${args.workflowId} did not complete before maxWait deadline`, args.workflowId);
|
|
650
685
|
}
|
|
651
686
|
// A genuine mid-stream TRANSPORT failure (reader disconnect) surfaces as a
|
|
652
|
-
// raw `TypeError` from the iterator — wrap as
|
|
687
|
+
// raw `TypeError` from the iterator — wrap as GislTransportError so callers
|
|
653
688
|
// poll-fallback. (An onProgress throw was already handled above, so a
|
|
654
|
-
// TypeError here is unambiguously transport.)
|
|
689
|
+
// TypeError here is unambiguously transport.) It stays a GislNetworkError
|
|
690
|
+
// by inheritance, so the poll-fallback gates below are unchanged.
|
|
655
691
|
if (innerErr instanceof TypeError) {
|
|
656
|
-
throw new
|
|
692
|
+
throw new GislTransportError(`SSE stream for workflow ${args.workflowId} failed mid-stream: ${innerErr.message}`);
|
|
657
693
|
}
|
|
658
694
|
throw innerErr;
|
|
659
695
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -13,6 +13,13 @@ export interface ValidationDetail {
|
|
|
13
13
|
}
|
|
14
14
|
export declare class GislClient {
|
|
15
15
|
private readonly baseUrl;
|
|
16
|
+
/**
|
|
17
|
+
* Declared SSE stream host, or `null` when nothing declares one for this
|
|
18
|
+
* configuration. `null` is a legitimate state, not a misconfiguration —
|
|
19
|
+
* see `streamEvents`, which fails closed on it rather than falling back to
|
|
20
|
+
* `baseUrl`.
|
|
21
|
+
*/
|
|
22
|
+
private readonly streamBaseUrl;
|
|
16
23
|
private readonly headers;
|
|
17
24
|
private readonly timeoutMs;
|
|
18
25
|
private readonly multipartThreshold;
|
package/dist/client.js
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
// dynamic one) so `vi.mock('node:fs/promises')` still intercepts it in tests.
|
|
6
6
|
import { open, stat, basename } from './node-fs.js';
|
|
7
7
|
import { AudioWatermarkDecodeRequestToJSON, AudioWatermarkDecodeResponseFromJSON, ExternalImportCreatedResponseFromJSON, ExternalImportRequestToJSON, LoginUser200ResponseDataFromJSON, AccountLimitsFromJSON, CreditsBalanceResponseFromJSON, CreditsUsageResponseFromJSON, UploadResponseFromJSON, UploadProbeResponseFromJSON, MultipartInitiateResponseFromJSON, MultipartInitiateRequestMetadataHintToJSON, MultipartCompleteResponseFromJSON, MultipartCompleteRequestToJSON, WorkflowCancelResponseFromJSON, WorkflowCreateResponseFromJSON, WorkflowResumeResponseFromJSON, WorkflowStatusResponseFromJSON, WorkflowListResponseFromJSON, WorkflowDownloadResponseFromJSON, MetadataResponseFromJSON, OperationsSchemaResponseFromJSON, RetryResponseFromJSON, WorkflowStatus, AuthErrorResponseFromJSON, AuthErrorType, AuthRejectionEnvelopeFromJSON, AuthRejectionEnvelopeErrorTypeEnum, BalanceExhaustedResponseFromJSON, BalanceExhaustedResponseRequiredActionEnum, FeatureNotAvailableResponseFromJSON, FeatureTierRestrictedResponseFromJSON, LongFormConcurrencyLimitResponseFromJSON, TierRestrictionKind, TierRestrictionResponseFromJSON, UserTier, WorkflowExpiredResponseFromJSON, ProbePendingResponseFromJSON, UploadSizeExceedsTierResponseFromJSON, UploadDurationExceedsTierResponseFromJSON, UploadConstraintsAppliedProcessingClassPreAssignmentEnum, UploadThresholdsSingleShotMaxBytesEnum, UploadThresholdsMultipartChunkSizeEnum, UploadThresholdsMultipartConcurrencyDefaultEnum, } from '@giveitsmaller/contracts/openapi';
|
|
8
|
-
import { GislAbortError, GislApiError, GislAuthError, GislAuthRejectionError, GislBalanceExhaustedError, GislError, GislFeatureNotAvailableError, GislFeatureTierRestrictedError, GislLongFormConcurrencyError, GislMultipartPartCountError, GislMultipartPartError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTierRestrictedError, GislTimeoutError, GislProbePendingError, GislUploadCapExceededError, GislValidationError, GislWorkflowExpiredError, } from './errors.js';
|
|
8
|
+
import { GislAbortError, GislApiError, GislAuthError, GislAuthRejectionError, GislBalanceExhaustedError, GislConfigError, GislError, GislFeatureNotAvailableError, GislFeatureTierRestrictedError, GislLongFormConcurrencyError, GislMultipartPartCountError, GislMultipartPartError, GislMultipartSessionNotFoundError, GislMultipartSessionOwnershipError, GislMultipartSessionAuthRequiredError, GislTierRestrictedError, GislTimeoutError, GislProbePendingError, GislStreamHostNotDeclaredError, GislUploadCapExceededError, GislValidationError, GislWorkflowExpiredError, } from './errors.js';
|
|
9
|
+
// Stream-host vocabulary for the fail-closed `streamEvents` guard. The
|
|
10
|
+
// resolver itself runs in `gisl.create()`; the client only reports what a
|
|
11
|
+
// caller can do about an absent host.
|
|
12
|
+
import { declaredStreamEnvironments, GISL_STREAM_BASE_URL_ENV, } from './credentials.js';
|
|
9
13
|
// The `Retry-After` millisecond parser lives in the shared retry-metadata
|
|
10
14
|
// module (extracted to break the client ↔ errors circular import); re-imported
|
|
11
15
|
// here so the retry-loop timing stays byte-identical.
|
|
@@ -350,8 +354,60 @@ function fileByteSource(path, size) {
|
|
|
350
354
|
},
|
|
351
355
|
};
|
|
352
356
|
}
|
|
357
|
+
/**
|
|
358
|
+
* Normalise a configured stream host to an absolute origin, or `null` when
|
|
359
|
+
* none was supplied. Trailing slashes are stripped so path concatenation does
|
|
360
|
+
* not double-separate.
|
|
361
|
+
*
|
|
362
|
+
* ⚠️ **A PRESENT-BUT-MALFORMED VALUE THROWS RATHER THAN DEGRADING TO `null`,
|
|
363
|
+
* and the distinction is deliberate.** Absent means "nobody declared one" — a
|
|
364
|
+
* legitimate state that `run()` handles by polling. A caller who passed
|
|
365
|
+
* `'/'` or `'stream.example.com'` did declare one, and got it wrong.
|
|
366
|
+
* Quietly converting that to "absent" would send their stream somewhere they
|
|
367
|
+
* did not choose (a bare `'/'` normalises to `''`, which concatenates into a
|
|
368
|
+
* RELATIVE url) and hand them a poll they never asked for — the silent
|
|
369
|
+
* degradation this whole mechanism exists to refuse, one layer further down.
|
|
370
|
+
*
|
|
371
|
+
* An empty OR WHITESPACE-ONLY string is treated as unset — the two are
|
|
372
|
+
* indistinguishable in intent — matching how `locale` handles `''` elsewhere in
|
|
373
|
+
* this config.
|
|
374
|
+
*/
|
|
375
|
+
function normaliseStreamBaseUrl(value) {
|
|
376
|
+
if (value === undefined)
|
|
377
|
+
return null;
|
|
378
|
+
const trimmed = value.trim();
|
|
379
|
+
if (trimmed === '')
|
|
380
|
+
return null;
|
|
381
|
+
let parsed;
|
|
382
|
+
try {
|
|
383
|
+
parsed = new URL(trimmed);
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
throw new GislConfigError(`streamBaseUrl must be an absolute http(s) URL (e.g. https://stream.example.com); got '${value}'.`);
|
|
387
|
+
}
|
|
388
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
389
|
+
throw new GislConfigError(`streamBaseUrl must use http or https; got protocol '${parsed.protocol}' in '${value}'.`);
|
|
390
|
+
}
|
|
391
|
+
// A query or fragment cannot survive path concatenation: the events path
|
|
392
|
+
// is appended as a STRING, so `https://host?token=x` would request `/`
|
|
393
|
+
// with the whole events path buried inside the query value. Rejecting is
|
|
394
|
+
// right rather than stripping — a caller who put a token there meant it to
|
|
395
|
+
// be sent, and silently dropping it would fail later and further away.
|
|
396
|
+
// codex 5793a3be0f7b.
|
|
397
|
+
if (parsed.search !== '' || parsed.hash !== '') {
|
|
398
|
+
throw new GislConfigError(`streamBaseUrl must not carry a query or fragment (the events path is appended to it); got '${value}'.`);
|
|
399
|
+
}
|
|
400
|
+
return trimmed.replace(/\/+$/, '');
|
|
401
|
+
}
|
|
353
402
|
export class GislClient {
|
|
354
403
|
baseUrl;
|
|
404
|
+
/**
|
|
405
|
+
* Declared SSE stream host, or `null` when nothing declares one for this
|
|
406
|
+
* configuration. `null` is a legitimate state, not a misconfiguration —
|
|
407
|
+
* see `streamEvents`, which fails closed on it rather than falling back to
|
|
408
|
+
* `baseUrl`.
|
|
409
|
+
*/
|
|
410
|
+
streamBaseUrl;
|
|
355
411
|
headers;
|
|
356
412
|
timeoutMs;
|
|
357
413
|
multipartThreshold;
|
|
@@ -361,6 +417,11 @@ export class GislClient {
|
|
|
361
417
|
useSessionCookie;
|
|
362
418
|
constructor(config) {
|
|
363
419
|
this.baseUrl = config.baseUrl.replace(/\/+$/, '');
|
|
420
|
+
// NOT defaulted to `baseUrl`. An absent stream host stays absent so
|
|
421
|
+
// `streamEvents` can fail closed and name the missing declaration; a
|
|
422
|
+
// default here would be the silent derivation this whole mechanism exists
|
|
423
|
+
// to prevent, hidden one layer deeper than the resolver.
|
|
424
|
+
this.streamBaseUrl = normaliseStreamBaseUrl(config.streamBaseUrl);
|
|
364
425
|
this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
365
426
|
this.useSessionCookie = config.useSessionCookie ?? false;
|
|
366
427
|
// Floor the threshold at the first-chunk size: the multipart initiate
|
|
@@ -399,7 +460,7 @@ export class GislClient {
|
|
|
399
460
|
if (opts.signal?.aborted) {
|
|
400
461
|
throw new GislAbortError(`Request to ${method} ${path} aborted`);
|
|
401
462
|
}
|
|
402
|
-
const url = `${this.baseUrl}${path}`;
|
|
463
|
+
const url = `${opts.baseUrl ?? this.baseUrl}${path}`;
|
|
403
464
|
const headers = { ...this.headers, ...opts.headers };
|
|
404
465
|
let body;
|
|
405
466
|
if (opts.json !== false && opts.body && !(opts.body instanceof FormData)) {
|
|
@@ -1748,6 +1809,24 @@ export class GislClient {
|
|
|
1748
1809
|
*/
|
|
1749
1810
|
async streamEvents(workflowId, opts = {}) {
|
|
1750
1811
|
const eventsPath = `/api/workflows/${encodeURIComponent(workflowId)}/events`;
|
|
1812
|
+
// FAIL CLOSED. The stream lives on a second host and this SDK will not
|
|
1813
|
+
// guess it. Falling back to `this.baseUrl` here would be the one line that
|
|
1814
|
+
// re-creates, inside a published SDK, the failure this mechanism exists to
|
|
1815
|
+
// prevent: production had no stream host configured, fell back to the API
|
|
1816
|
+
// host by convention, and streamed into a gateway that cannot stream —
|
|
1817
|
+
// invisibly, because a silent fallback looks exactly like a working one.
|
|
1818
|
+
// `run()` handles this case by polling; a direct `streamEvents` caller
|
|
1819
|
+
// asked for the stream specifically and is told plainly that there isn't
|
|
1820
|
+
// one.
|
|
1821
|
+
if (this.streamBaseUrl === null) {
|
|
1822
|
+
const declared = declaredStreamEnvironments();
|
|
1823
|
+
throw new GislStreamHostNotDeclaredError('No SSE stream host is declared for this configuration, and the SDK does not derive one ' +
|
|
1824
|
+
`from baseUrl. ${declared.length > 0
|
|
1825
|
+
? `Environments that declare a stream host: ${declared.join(', ')}.`
|
|
1826
|
+
: 'No environment currently declares a stream host.'} Pass {streamBaseUrl} to gisl.create() / new GislClient(), set ` +
|
|
1827
|
+
`${GISL_STREAM_BASE_URL_ENV}, or construct with an {environment} that declares one. ` +
|
|
1828
|
+
'Hosts are read from the contract declaration and are never derived from baseUrl.');
|
|
1829
|
+
}
|
|
1751
1830
|
// SSE-lifetime AbortController. `request()` builds its own controller
|
|
1752
1831
|
// and tears it down (`clearTimeout(timer); unbind()`) in its `finally`
|
|
1753
1832
|
// the instant the response headers arrive — BEFORE the SSE body
|
|
@@ -1771,6 +1850,8 @@ export class GislClient {
|
|
|
1771
1850
|
rawResponse: true,
|
|
1772
1851
|
signal: controller.signal,
|
|
1773
1852
|
headers: workflowCapabilityHeaders(opts.capability),
|
|
1853
|
+
// The one call in the SDK that does NOT go to `baseUrl`.
|
|
1854
|
+
baseUrl: this.streamBaseUrl,
|
|
1774
1855
|
});
|
|
1775
1856
|
}
|
|
1776
1857
|
catch (err) {
|
package/dist/credentials.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
export declare const GISL_API_KEY_ENV = "GISL_API_KEY";
|
|
17
17
|
export declare const GISL_BASE_URL_ENV = "GISL_BASE_URL";
|
|
18
18
|
export declare const GISL_ENVIRONMENT_ENV = "GISL_ENVIRONMENT";
|
|
19
|
+
export declare const GISL_STREAM_BASE_URL_ENV = "GISL_STREAM_BASE_URL";
|
|
19
20
|
/**
|
|
20
21
|
* Named environments → base URLs. Kept colocated with the resolver so the
|
|
21
22
|
* mapping table doesn't leak into `gisl.ts`.
|
|
@@ -26,6 +27,42 @@ export declare const ENVIRONMENT_ENDPOINTS: {
|
|
|
26
27
|
};
|
|
27
28
|
export type Environment = keyof typeof ENVIRONMENT_ENDPOINTS;
|
|
28
29
|
export declare const DEFAULT_ENDPOINT: "https://api.giveitsmaller.com";
|
|
30
|
+
/**
|
|
31
|
+
* Named environments → **SSE stream host**. A SECOND host, deliberately
|
|
32
|
+
* separate from {@link ENVIRONMENT_ENDPOINTS}: the API host fronts an
|
|
33
|
+
* integration with no response-streaming mode, so the event stream lives on
|
|
34
|
+
* its own public entry point.
|
|
35
|
+
*
|
|
36
|
+
* ⚠️ **DECLARED, NEVER DERIVED.** This table exists because the alternative —
|
|
37
|
+
* transforming `api.*` into `stream.*` by string surgery — is a *convention*,
|
|
38
|
+
* and a convention is exactly what put production on the gateway path: the
|
|
39
|
+
* frontend's prod build had no `VITE_SSE_BASE_URL`, silently fell back to the
|
|
40
|
+
* API host, and nobody could see it. A host is a fact somebody states, not a
|
|
41
|
+
* pattern somebody guesses.
|
|
42
|
+
*
|
|
43
|
+
* PINNED to the generated `availability.json`
|
|
44
|
+
* `endpoints['GET /api/workflows/{id}/events'].servers` by
|
|
45
|
+
* `tests/unit/stream-host-conformance.test.ts`, which fails **closed**: if the
|
|
46
|
+
* contract declares a host this table does not carry (or vice versa), the
|
|
47
|
+
* build breaks. Hand-maintained rather than read at runtime because the SDK
|
|
48
|
+
* ships a browser entry point and does not load `availability.json` — the same
|
|
49
|
+
* table+conformance shape used by `COMPRESS_OPTION_VALUES`,
|
|
50
|
+
* `OUTPUT_OPTION_DEPENDS_ON`, the preset planned gate and the watermark gate.
|
|
51
|
+
*
|
|
52
|
+
* `prod` landed with contracts `v2.195.0` (#410), which declared the production
|
|
53
|
+
* stream host. It is here because the CONTRACT declares it — the entry and the
|
|
54
|
+
* vendored declaration moved in the same change, never ahead of it.
|
|
55
|
+
*
|
|
56
|
+
* ⚠️ **A CONFIGURATION WITH NO DECLARED HOST STILL FAILS CLOSED.** Both entries
|
|
57
|
+
* being present does not soften the rule: {@link resolveStreamEndpoint} returns
|
|
58
|
+
* `null` for anything it cannot resolve from a declaration, and
|
|
59
|
+
* `GislClient.streamEvents` raises rather than quietly reusing `baseUrl`.
|
|
60
|
+
*
|
|
61
|
+
* `localhost` is intentionally absent too: it is declared in the contract as a
|
|
62
|
+
* development server, but there is no `localhost` *environment* name to key it
|
|
63
|
+
* off. Local callers pass `{streamBaseUrl}` or set `GISL_STREAM_BASE_URL`.
|
|
64
|
+
*/
|
|
65
|
+
export declare const ENVIRONMENT_STREAM_ENDPOINTS: Partial<Record<Environment, string>>;
|
|
29
66
|
export interface ResolveCredentialsOptions {
|
|
30
67
|
/** Explicit API key — highest precedence. */
|
|
31
68
|
readonly apiKey?: string;
|
|
@@ -44,6 +81,12 @@ export interface ResolveCredentialsOptions {
|
|
|
44
81
|
export interface ResolveEndpointOptions {
|
|
45
82
|
readonly baseUrl?: string;
|
|
46
83
|
readonly environment?: Environment;
|
|
84
|
+
/**
|
|
85
|
+
* Explicit SSE stream host. Highest precedence for stream resolution, and
|
|
86
|
+
* the ONLY knob that moves the stream **without** moving every other call —
|
|
87
|
+
* overriding `baseUrl` moves uploads, workflow-create and downloads too.
|
|
88
|
+
*/
|
|
89
|
+
readonly streamBaseUrl?: string;
|
|
47
90
|
}
|
|
48
91
|
/**
|
|
49
92
|
* Resolve the API key via the credential chain. Returns the resolved key,
|
|
@@ -59,3 +102,34 @@ export declare function resolveApiKey(opts?: ResolveCredentialsOptions): Promise
|
|
|
59
102
|
* resolves to a usable URL.
|
|
60
103
|
*/
|
|
61
104
|
export declare function resolveEndpoint(opts?: ResolveEndpointOptions): string;
|
|
105
|
+
/**
|
|
106
|
+
* Resolve the **SSE stream host**, or `null` when no host is declared for this
|
|
107
|
+
* configuration. Explicit `streamBaseUrl` wins; otherwise an explicit
|
|
108
|
+
* `environment` name; otherwise `GISL_STREAM_BASE_URL`; otherwise the
|
|
109
|
+
* `GISL_ENVIRONMENT` env var.
|
|
110
|
+
*
|
|
111
|
+
* ⚠️ **RETURNS `null` RATHER THAN FALLING BACK TO `baseUrl`, AND THAT IS THE
|
|
112
|
+
* WHOLE POINT OF THIS FUNCTION.** Deriving the stream host from the API host
|
|
113
|
+
* would reproduce, inside a published SDK, the exact failure this resolver
|
|
114
|
+
* exists to prevent: prod had no stream host configured, fell back to the API
|
|
115
|
+
* host by convention, and landed on the gateway path where the stream cannot
|
|
116
|
+
* work. A silent fallback is not a lenient control — it is the absence of one
|
|
117
|
+
* wearing the control's name. Callers decide what `null` means; see
|
|
118
|
+
* `GislClient.streamEvents`, which fails closed and names the missing
|
|
119
|
+
* declaration.
|
|
120
|
+
*
|
|
121
|
+
* Unlike {@link resolveEndpoint}, there is no default: prod has no declared
|
|
122
|
+
* stream host yet (see {@link ENVIRONMENT_STREAM_ENDPOINTS}), so a default
|
|
123
|
+
* could only be a guess.
|
|
124
|
+
*
|
|
125
|
+
* Throws `GislConfigError` on an unknown explicit `environment` name — the
|
|
126
|
+
* same fail-closed behaviour as {@link resolveEndpoint}, for the same reason
|
|
127
|
+
* (a typo must not silently re-route a stream).
|
|
128
|
+
*/
|
|
129
|
+
export declare function resolveStreamEndpoint(opts?: ResolveEndpointOptions): string | null;
|
|
130
|
+
/**
|
|
131
|
+
* Human-readable list of the environments that currently declare a stream
|
|
132
|
+
* host. Used in the fail-closed error message so the caller is told what IS
|
|
133
|
+
* available rather than only what is missing.
|
|
134
|
+
*/
|
|
135
|
+
export declare function declaredStreamEnvironments(): readonly string[];
|
package/dist/credentials.js
CHANGED
|
@@ -20,6 +20,7 @@ import { GislConfigError } from './errors.js';
|
|
|
20
20
|
export const GISL_API_KEY_ENV = 'GISL_API_KEY';
|
|
21
21
|
export const GISL_BASE_URL_ENV = 'GISL_BASE_URL';
|
|
22
22
|
export const GISL_ENVIRONMENT_ENV = 'GISL_ENVIRONMENT';
|
|
23
|
+
export const GISL_STREAM_BASE_URL_ENV = 'GISL_STREAM_BASE_URL';
|
|
23
24
|
/**
|
|
24
25
|
* Named environments → base URLs. Kept colocated with the resolver so the
|
|
25
26
|
* mapping table doesn't leak into `gisl.ts`.
|
|
@@ -29,6 +30,45 @@ export const ENVIRONMENT_ENDPOINTS = {
|
|
|
29
30
|
staging: 'https://api.staging.giveitsmaller.com',
|
|
30
31
|
};
|
|
31
32
|
export const DEFAULT_ENDPOINT = ENVIRONMENT_ENDPOINTS.prod;
|
|
33
|
+
/**
|
|
34
|
+
* Named environments → **SSE stream host**. A SECOND host, deliberately
|
|
35
|
+
* separate from {@link ENVIRONMENT_ENDPOINTS}: the API host fronts an
|
|
36
|
+
* integration with no response-streaming mode, so the event stream lives on
|
|
37
|
+
* its own public entry point.
|
|
38
|
+
*
|
|
39
|
+
* ⚠️ **DECLARED, NEVER DERIVED.** This table exists because the alternative —
|
|
40
|
+
* transforming `api.*` into `stream.*` by string surgery — is a *convention*,
|
|
41
|
+
* and a convention is exactly what put production on the gateway path: the
|
|
42
|
+
* frontend's prod build had no `VITE_SSE_BASE_URL`, silently fell back to the
|
|
43
|
+
* API host, and nobody could see it. A host is a fact somebody states, not a
|
|
44
|
+
* pattern somebody guesses.
|
|
45
|
+
*
|
|
46
|
+
* PINNED to the generated `availability.json`
|
|
47
|
+
* `endpoints['GET /api/workflows/{id}/events'].servers` by
|
|
48
|
+
* `tests/unit/stream-host-conformance.test.ts`, which fails **closed**: if the
|
|
49
|
+
* contract declares a host this table does not carry (or vice versa), the
|
|
50
|
+
* build breaks. Hand-maintained rather than read at runtime because the SDK
|
|
51
|
+
* ships a browser entry point and does not load `availability.json` — the same
|
|
52
|
+
* table+conformance shape used by `COMPRESS_OPTION_VALUES`,
|
|
53
|
+
* `OUTPUT_OPTION_DEPENDS_ON`, the preset planned gate and the watermark gate.
|
|
54
|
+
*
|
|
55
|
+
* `prod` landed with contracts `v2.195.0` (#410), which declared the production
|
|
56
|
+
* stream host. It is here because the CONTRACT declares it — the entry and the
|
|
57
|
+
* vendored declaration moved in the same change, never ahead of it.
|
|
58
|
+
*
|
|
59
|
+
* ⚠️ **A CONFIGURATION WITH NO DECLARED HOST STILL FAILS CLOSED.** Both entries
|
|
60
|
+
* being present does not soften the rule: {@link resolveStreamEndpoint} returns
|
|
61
|
+
* `null` for anything it cannot resolve from a declaration, and
|
|
62
|
+
* `GislClient.streamEvents` raises rather than quietly reusing `baseUrl`.
|
|
63
|
+
*
|
|
64
|
+
* `localhost` is intentionally absent too: it is declared in the contract as a
|
|
65
|
+
* development server, but there is no `localhost` *environment* name to key it
|
|
66
|
+
* off. Local callers pass `{streamBaseUrl}` or set `GISL_STREAM_BASE_URL`.
|
|
67
|
+
*/
|
|
68
|
+
export const ENVIRONMENT_STREAM_ENDPOINTS = {
|
|
69
|
+
prod: 'https://stream.giveitsmaller.com',
|
|
70
|
+
staging: 'https://stream.staging.giveitsmaller.com',
|
|
71
|
+
};
|
|
32
72
|
// ---------------------------------------------------------------------------
|
|
33
73
|
// Public resolvers
|
|
34
74
|
// ---------------------------------------------------------------------------
|
|
@@ -105,6 +145,89 @@ export function resolveEndpoint(opts = {}) {
|
|
|
105
145
|
}
|
|
106
146
|
return DEFAULT_ENDPOINT;
|
|
107
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Resolve the **SSE stream host**, or `null` when no host is declared for this
|
|
150
|
+
* configuration. Explicit `streamBaseUrl` wins; otherwise an explicit
|
|
151
|
+
* `environment` name; otherwise `GISL_STREAM_BASE_URL`; otherwise the
|
|
152
|
+
* `GISL_ENVIRONMENT` env var.
|
|
153
|
+
*
|
|
154
|
+
* ⚠️ **RETURNS `null` RATHER THAN FALLING BACK TO `baseUrl`, AND THAT IS THE
|
|
155
|
+
* WHOLE POINT OF THIS FUNCTION.** Deriving the stream host from the API host
|
|
156
|
+
* would reproduce, inside a published SDK, the exact failure this resolver
|
|
157
|
+
* exists to prevent: prod had no stream host configured, fell back to the API
|
|
158
|
+
* host by convention, and landed on the gateway path where the stream cannot
|
|
159
|
+
* work. A silent fallback is not a lenient control — it is the absence of one
|
|
160
|
+
* wearing the control's name. Callers decide what `null` means; see
|
|
161
|
+
* `GislClient.streamEvents`, which fails closed and names the missing
|
|
162
|
+
* declaration.
|
|
163
|
+
*
|
|
164
|
+
* Unlike {@link resolveEndpoint}, there is no default: prod has no declared
|
|
165
|
+
* stream host yet (see {@link ENVIRONMENT_STREAM_ENDPOINTS}), so a default
|
|
166
|
+
* could only be a guess.
|
|
167
|
+
*
|
|
168
|
+
* Throws `GislConfigError` on an unknown explicit `environment` name — the
|
|
169
|
+
* same fail-closed behaviour as {@link resolveEndpoint}, for the same reason
|
|
170
|
+
* (a typo must not silently re-route a stream).
|
|
171
|
+
*/
|
|
172
|
+
export function resolveStreamEndpoint(opts = {}) {
|
|
173
|
+
// TRIM BEFORE THE PRESENCE CHECK. A whitespace-only value is unset (the
|
|
174
|
+
// client normaliser treats it that way too), and if it were allowed to
|
|
175
|
+
// count as "supplied" here it would SUPPRESS the environment's declared
|
|
176
|
+
// host and then normalise to nothing — silently disabling a stream that
|
|
177
|
+
// was perfectly well declared. codex a7f5ec9f0d32.
|
|
178
|
+
const explicit = opts.streamBaseUrl?.trim() ?? '';
|
|
179
|
+
if (explicit !== '') {
|
|
180
|
+
return explicit;
|
|
181
|
+
}
|
|
182
|
+
if (typeof opts.environment === 'string') {
|
|
183
|
+
if (!(opts.environment in ENVIRONMENT_ENDPOINTS)) {
|
|
184
|
+
throw new GislConfigError(`Unknown environment '${opts.environment}'. Valid values: ${Object.keys(ENVIRONMENT_ENDPOINTS).join(', ')}.`);
|
|
185
|
+
}
|
|
186
|
+
// A KNOWN environment with no declared stream host resolves to `null`, not
|
|
187
|
+
// to an error and not to `baseUrl`: the config is valid, the declaration is
|
|
188
|
+
// simply missing upstream. Both current environments declare one.
|
|
189
|
+
return ENVIRONMENT_STREAM_ENDPOINTS[opts.environment] ?? null;
|
|
190
|
+
}
|
|
191
|
+
const envStreamBaseUrl = readEnv(GISL_STREAM_BASE_URL_ENV);
|
|
192
|
+
if (envStreamBaseUrl !== null && envStreamBaseUrl.length > 0) {
|
|
193
|
+
return envStreamBaseUrl;
|
|
194
|
+
}
|
|
195
|
+
const envEnvironment = readEnv(GISL_ENVIRONMENT_ENV);
|
|
196
|
+
if (envEnvironment !== null) {
|
|
197
|
+
const envMapped = ENVIRONMENT_STREAM_ENDPOINTS[envEnvironment];
|
|
198
|
+
if (envMapped !== undefined) {
|
|
199
|
+
return envMapped;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// SYMMETRY WITH `resolveEndpoint`, and a correctness fix rather than a
|
|
203
|
+
// convenience (codex 480e8b865b90). `resolveEndpoint` FALLS THROUGH to the
|
|
204
|
+
// production API host when nothing is configured — so an unconfigured
|
|
205
|
+
// `gisl.create({apiKey})` already talks to production, while its stream
|
|
206
|
+
// resolved to `null`. That made THE DEFAULT CONFIGURATION the one that could
|
|
207
|
+
// not stream: `streamEvents()` threw and `run()` silently polled, against a
|
|
208
|
+
// production host whose stream IS declared. The two resolvers have to agree
|
|
209
|
+
// about what "unconfigured" means.
|
|
210
|
+
//
|
|
211
|
+
// ⚠️ ONLY when the API host ALSO defaulted. An explicit `baseUrl` (or
|
|
212
|
+
// `GISL_BASE_URL`) names a host we were told about and cannot reason about —
|
|
213
|
+
// a proxy, a self-host, a test double — so we still refuse rather than assume
|
|
214
|
+
// production's stream host. Assuming there would be deriving one host from
|
|
215
|
+
// another, which is precisely what this mechanism exists to refuse.
|
|
216
|
+
const apiHostWasConfigured = (typeof opts.baseUrl === 'string' && opts.baseUrl.trim() !== '') ||
|
|
217
|
+
readEnv(GISL_BASE_URL_ENV) !== null;
|
|
218
|
+
if (!apiHostWasConfigured) {
|
|
219
|
+
return ENVIRONMENT_STREAM_ENDPOINTS.prod ?? null;
|
|
220
|
+
}
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Human-readable list of the environments that currently declare a stream
|
|
225
|
+
* host. Used in the fail-closed error message so the caller is told what IS
|
|
226
|
+
* available rather than only what is missing.
|
|
227
|
+
*/
|
|
228
|
+
export function declaredStreamEnvironments() {
|
|
229
|
+
return Object.keys(ENVIRONMENT_STREAM_ENDPOINTS);
|
|
230
|
+
}
|
|
108
231
|
// ---------------------------------------------------------------------------
|
|
109
232
|
// Internals
|
|
110
233
|
// ---------------------------------------------------------------------------
|
|
@@ -85,6 +85,24 @@ export declare function resolveOutputRoute(inputToken: string, outputFormat: str
|
|
|
85
85
|
* `compressMetadata` `per_value_availability`; same_format only (the only route
|
|
86
86
|
* where value-level options like `metadata` are honored). Returns false when the
|
|
87
87
|
* option / value / group is unknown (no gate).
|
|
88
|
+
*
|
|
89
|
+
* PURELY ADDITIVE (SB1wmTJz): planned if ANY consulted group marks this value planned.
|
|
90
|
+
* The historical group is still consulted, so **every verdict this returned before still
|
|
91
|
+
* holds** — the change can only turn a missed gate into a gate, never a gate into a
|
|
92
|
+
* pass. That direction matters: a new false ACCEPT would send a request the server
|
|
93
|
+
* rejects, which is the failure this function exists to prevent.
|
|
94
|
+
*
|
|
95
|
+
* Why not "most specific wins", which reads cleaner: it would flip `webp` +
|
|
96
|
+
* `color_profile: 'srgb'` from gated to un-gated, because `image_webp` defines
|
|
97
|
+
* `color_profile` with an empty `per_value_availability`. `RecipeOutputTest`
|
|
98
|
+
* deliberately pins webp srgb as GATED (v2.134 added `srgb: planned` to the generic
|
|
99
|
+
* group), and whether webp srgb actually works on the server is not something this
|
|
100
|
+
* layer can know. Un-gating it on an inference would be exactly the "confident answer
|
|
101
|
+
* from a check that could not tell you otherwise" pattern. Raised as a question instead.
|
|
102
|
+
*
|
|
103
|
+
* What this DOES fix: `image_svg` marks `output_format: 'original'` planned and the
|
|
104
|
+
* generic group does not, so an SVG input previously sailed through the one marker that
|
|
105
|
+
* mattered for it — on this gate and on the `output()` gate that shares it.
|
|
88
106
|
*/
|
|
89
107
|
export declare function isPlannedValue(inputToken: string, optionKey: string, value: unknown): boolean;
|
|
90
108
|
/**
|
|
@@ -117,4 +135,53 @@ export declare const COMPRESS_OPTION_VALUES: Readonly<Record<string, Readonly<Re
|
|
|
117
135
|
* treated as unknown rather than coerced to a match.
|
|
118
136
|
*/
|
|
119
137
|
export declare function isUnknownEnumValue(inputToken: string, optionKey: string, value: unknown): boolean;
|
|
138
|
+
/** A contract `depends_on` rule for a compress-image output option (ehHU08Hu). */
|
|
139
|
+
type OutputDependsOnRule = {
|
|
140
|
+
readonly requiresKey: string;
|
|
141
|
+
readonly requiresValue: string;
|
|
142
|
+
} | {
|
|
143
|
+
readonly requiresAnyOf: readonly string[];
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Contract `depends_on` per compress-image output option, mirroring
|
|
147
|
+
* `availability.json` `operations.compress.mime_groups.<group>.options.<opt>.depends_on`
|
|
148
|
+
* (ehHU08Hu). The rule is option-consistent across every image group that carries
|
|
149
|
+
* the option, so this is a FLAT table (validated group-by-group by
|
|
150
|
+
* `output-route-conformance.test.ts` / PHP `ImageOutputRouteConformanceTest`).
|
|
151
|
+
*
|
|
152
|
+
* Kept as a hand table — NOT a runtime read of the ~238KB availability sidecar —
|
|
153
|
+
* so the gate stays browser-safe with no contracts-version coupling, exactly like
|
|
154
|
+
* {@link COMPRESS_OPTION_VALUES}. Mirrored by PHP
|
|
155
|
+
* `ImageOutputRoutes::OUTPUT_OPTION_DEPENDS_ON`.
|
|
156
|
+
*
|
|
157
|
+
* Generalises the 86gAu5Tr auto_quality gate: every option's dependency is
|
|
158
|
+
* checked uniformly, so quality/lossless/target_size_bytes under `auto_quality`,
|
|
159
|
+
* `target_size_bytes` without `target_size`, `fit` without width/height, etc. are
|
|
160
|
+
* all rejected pre-upload instead of only the one hand-coded case.
|
|
161
|
+
*/
|
|
162
|
+
export declare const OUTPUT_OPTION_DEPENDS_ON: Readonly<Record<string, OutputDependsOnRule>>;
|
|
163
|
+
/**
|
|
164
|
+
* Default of each depended-on key — an ABSENT key resolves to this before the
|
|
165
|
+
* dependency check (the server applies the same default). `encoding_mode`
|
|
166
|
+
* defaults to `quality`, so `quality`/`lossless` are valid with no explicit mode,
|
|
167
|
+
* but `target_size_bytes` / `quality_preset` are not. Pinned to `availability.json`
|
|
168
|
+
* defaults by the conformance suite.
|
|
169
|
+
*/
|
|
170
|
+
export declare const DEPENDS_ON_KEY_DEFAULTS: Readonly<Record<string, string>>;
|
|
171
|
+
/**
|
|
172
|
+
* The first contract `depends_on` an already-lowered compress-image wire-option
|
|
173
|
+
* set violates for the resolved `route`, or `undefined` when every dependency is
|
|
174
|
+
* satisfied (ehHU08Hu). The caller ({@link Recipe} output lowering) throws
|
|
175
|
+
* `invalid_option_combination` with the returned message + conflictingFields.
|
|
176
|
+
* Only options PRESENT in `wireOptions` are checked; a scalar dependency reads
|
|
177
|
+
* the depended-on key's effective value ({@link DEPENDS_ON_KEY_DEFAULTS} when
|
|
178
|
+
* absent). A scalar (encoding_mode) dependency is skipped on a `format_change`
|
|
179
|
+
* (convert has its own deps); universal deps (e.g. `fit → width|height`, identical
|
|
180
|
+
* in compress + convert) run on BOTH routes. Mirrored by PHP
|
|
181
|
+
* `ImageOutputRoutes::dependsOnViolation`.
|
|
182
|
+
*/
|
|
183
|
+
export declare function dependsOnViolation(wireOptions: Readonly<Record<string, unknown>>, route: 'same_format' | 'format_change'): {
|
|
184
|
+
readonly message: string;
|
|
185
|
+
readonly conflictingFields: readonly string[];
|
|
186
|
+
} | undefined;
|
|
120
187
|
export {};
|