@giveitsmaller/sdk 0.22.0 → 0.26.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 +47 -11
- package/dist/builder.js +17 -6
- 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/presets/video_compress.d.ts +9 -3
- package/dist/errors.d.ts +130 -7
- package/dist/errors.js +149 -7
- package/dist/file-first.d.ts +1 -1
- package/dist/file-first.js +13 -3
- package/dist/generated/sdk_spec/errors.d.ts +1 -1
- package/dist/generated/sdk_spec/errors.js +29 -0
- 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 +15 -2
- package/dist/merge.d.ts +8 -2
- package/dist/merge.js +12 -2
- package/dist/sse.d.ts +23 -1
- package/dist/sse.js +27 -2
- package/dist/types.d.ts +37 -6
- package/package.json +10 -2
package/README.md
CHANGED
|
@@ -74,23 +74,26 @@ development against staging** — which is the one most likely to bite first, be
|
|
|
74
74
|
bug in your code.
|
|
75
75
|
|
|
76
76
|
⚠️ **Local dev against staging is the sharp edge, and the reason is worth stating:** the two hosts
|
|
77
|
-
have *different* CORS policies
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
which reads like a mistake in your own application rather than a platform limitation.
|
|
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.
|
|
83
82
|
|
|
84
83
|
**Production allows only the production web app on both hosts**, and always has.
|
|
85
84
|
|
|
86
85
|
**Workaround:** pass `useSSE: false` to `run()`. The SDK falls back to polling, which goes to the
|
|
87
86
|
main API host and is unaffected. Everything else about the call is identical.
|
|
88
87
|
|
|
89
|
-
**Why it
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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.
|
|
94
97
|
|
|
95
98
|
**On authentication:** prefer an API key (`bearerAuth`) or the anonymous capability token on the
|
|
96
99
|
stream host. Cookie/session auth is accepted by the endpoint but a *credentialed cross-origin*
|
|
@@ -104,6 +107,39 @@ same reason.
|
|
|
104
107
|
> test and canary we run passes while a consumer on another origin fails. It is written here because
|
|
105
108
|
> nothing else would tell you.
|
|
106
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
|
+
|
|
107
143
|
## Documentation
|
|
108
144
|
|
|
109
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, GislFanOutTimeoutError, 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.
|
|
@@ -592,7 +602,7 @@ export async function _consumeSseToTerminal(client, args) {
|
|
|
592
602
|
// GislNetworkError so the await-terminal callers poll-fallback on it
|
|
593
603
|
// (and ONLY on it / a clean stream-end), never on an onProgress throw.
|
|
594
604
|
if (err instanceof TypeError) {
|
|
595
|
-
throw new
|
|
605
|
+
throw new GislTransportError(`SSE connect to workflow ${args.workflowId} events failed: ${err.message}`);
|
|
596
606
|
}
|
|
597
607
|
throw err;
|
|
598
608
|
}
|
|
@@ -674,11 +684,12 @@ export async function _consumeSseToTerminal(client, args) {
|
|
|
674
684
|
throw new GislTimeoutError(`Workflow ${args.workflowId} did not complete before maxWait deadline`, args.workflowId);
|
|
675
685
|
}
|
|
676
686
|
// A genuine mid-stream TRANSPORT failure (reader disconnect) surfaces as a
|
|
677
|
-
// raw `TypeError` from the iterator — wrap as
|
|
687
|
+
// raw `TypeError` from the iterator — wrap as GislTransportError so callers
|
|
678
688
|
// poll-fallback. (An onProgress throw was already handled above, so a
|
|
679
|
-
// 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.
|
|
680
691
|
if (innerErr instanceof TypeError) {
|
|
681
|
-
throw new
|
|
692
|
+
throw new GislTransportError(`SSE stream for workflow ${args.workflowId} failed mid-stream: ${innerErr.message}`);
|
|
682
693
|
}
|
|
683
694
|
throw innerErr;
|
|
684
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
|
// ---------------------------------------------------------------------------
|
|
@@ -11,9 +11,15 @@ export interface VideoCompressPresetOptionsInput {
|
|
|
11
11
|
* single-pass-CRF by construction and two-pass target-size is unbuilt. The request
|
|
12
12
|
* fails during execution, and the SDK cannot warn earlier — routing is decided
|
|
13
13
|
* server-side at create-plan time, so there is nothing here to check it against.
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* The same limit applies to {@link MergeOptions.targetSize}.
|
|
15
|
+
* Short-form compresses honour it normally.
|
|
16
|
+
*
|
|
17
|
+
* The contract CAN now express this — `per_class_availability` scopes an option to
|
|
18
|
+
* a processing class, vendored at v2.195.0 and pinned by
|
|
19
|
+
* `tests/unit/per-class-availability-conformance.test.ts`. That buys an honest 422
|
|
20
|
+
* from the API at CREATE rather than a job dying mid-execution; it does NOT become
|
|
21
|
+
* a client-side gate, because routing is still decided server-side and a duration
|
|
22
|
+
* heuristic here would be wrong at the boundary. Tracked by `zJN6XIi5`.
|
|
17
23
|
*/
|
|
18
24
|
readonly targetSize?: string | number;
|
|
19
25
|
readonly crf?: number;
|