@shipstatic/types 2.7.0-beta.2 → 2.7.0-beta.4
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 +2 -2
- package/dist/index.d.ts +97 -3
- package/dist/index.js +136 -14
- package/package.json +1 -1
- package/src/index.ts +161 -14
package/README.md
CHANGED
|
@@ -78,9 +78,9 @@ try { response = await fetch(url); }
|
|
|
78
78
|
catch (cause) { throw ShipError.fromFetchError(cause, 'Get account'); }
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
`fromHttpResponse` trusts the body's `error` field when it's a known server-producible `ErrorType` — so a server's `ShipError.validation(...)` round-trips back to `ErrorType.Validation` on the client. For non-API responses (CDN errors, intermediaries) or malformed bodies it falls back to status-derived (401 → `Authentication`, 403 → `Forbidden`, 429 → `RateLimit`, else → `Api`). Client-only types (`Network`, `Cancelled`, `File`, `Config`) are filtered out of the trusted set. Body's `message` and `details` are preserved best-effort.
|
|
81
|
+
`fromHttpResponse` trusts the body's `error` field when it's a known server-producible `ErrorType` — so a server's `ShipError.validation(...)` round-trips back to `ErrorType.Validation` on the client. For non-API responses (CDN errors, intermediaries) or malformed bodies it falls back to status-derived (401 → `Authentication`, 403 → `Forbidden`, 429 → `RateLimit`, else → `Api`). Client-only types (`Network`, `Timeout`, `Cancelled`, `File`, `Config`) are filtered out of the trusted set. Body's `message` and `details` are preserved best-effort.
|
|
82
82
|
|
|
83
|
-
`fromFetchError` routes by the thrown cause: an existing `ShipError` is returned unchanged, `AbortError` becomes `Cancelled`, a fetch `TypeError` becomes `Network`, anything else becomes `Api` (with no HTTP status — the request never reached the server).
|
|
83
|
+
`fromFetchError` routes by the thrown cause: an existing `ShipError` is returned unchanged, `AbortError` becomes `Cancelled`, `TimeoutError` becomes `Timeout`, a fetch `TypeError` becomes `Network`, anything else becomes `Api` (with no HTTP status — the request never reached the server). `Timeout` is a distinct type inside the network category — `isNetworkError()` is true for it — so a surface can retry it like any transport failure while still saying "timed out" rather than "check your connection".
|
|
84
84
|
|
|
85
85
|
Both helpers accept an optional operation-name string for contextual messages (`"Get account was cancelled"`, `"Get account failed: ..."`).
|
|
86
86
|
|
package/dist/index.d.ts
CHANGED
|
@@ -671,6 +671,12 @@ export declare const DEPLOY_FIELDS: {
|
|
|
671
671
|
readonly VIA: "via";
|
|
672
672
|
/** Plaintext password — the API hashes it server-side. */
|
|
673
673
|
readonly PASSWORD: "password";
|
|
674
|
+
/**
|
|
675
|
+
* Requested lifetime in SECONDS — a duration, never an instant. The API
|
|
676
|
+
* computes and stores the expiry, so the wire carries no client clock.
|
|
677
|
+
* See {@link validateTtl}.
|
|
678
|
+
*/
|
|
679
|
+
readonly TTL: "ttl";
|
|
674
680
|
/** @internal Server-processing flag — first-party `/upload` only. */
|
|
675
681
|
readonly BUILD: "build";
|
|
676
682
|
/** @internal Server-processing flag — first-party `/upload` only. */
|
|
@@ -725,6 +731,23 @@ export declare const ErrorType: {
|
|
|
725
731
|
readonly Maintenance: "maintenance";
|
|
726
732
|
/** Network/connection error. Client-side only — set by HTTP clients on fetch failure; never produced server-side. */
|
|
727
733
|
readonly Network: "network_error";
|
|
734
|
+
/**
|
|
735
|
+
* A deadline expired before the exchange completed. Client-side only — set
|
|
736
|
+
* by HTTP clients when a timeout signal fires; never produced server-side.
|
|
737
|
+
*
|
|
738
|
+
* A member of the NETWORK category rather than a sibling of it:
|
|
739
|
+
* `isNetworkError()` answers "nothing was exchanged", which is true of a
|
|
740
|
+
* deadline exactly as it is of a refused connection, so every consumer that
|
|
741
|
+
* retries, declines to report, or declines to relay a wire message on that
|
|
742
|
+
* category is already right about a timeout. The distinct TYPE exists for
|
|
743
|
+
* the one decision the category cannot make — what to SAY. "Check your
|
|
744
|
+
* internet connection" is the wrong sentence for a five-minute deploy
|
|
745
|
+
* ceiling, and a surface can only tell the two apart by type.
|
|
746
|
+
*
|
|
747
|
+
* The same relationship every comparable SDK ships:
|
|
748
|
+
* `APIConnectionTimeoutError extends APIConnectionError`.
|
|
749
|
+
*/
|
|
750
|
+
readonly Timeout: "timeout_error";
|
|
728
751
|
/** Operation was cancelled. Client-side only — set on `AbortSignal` abort; never produced server-side. */
|
|
729
752
|
readonly Cancelled: "operation_cancelled";
|
|
730
753
|
/** File operation error. Client-side only — set by SDK during local file processing; never produced server-side. */
|
|
@@ -769,7 +792,7 @@ export declare class ShipError extends Error {
|
|
|
769
792
|
* on the client). Falls back to status-derived (401 → Authentication,
|
|
770
793
|
* 403 → Forbidden, 429 → RateLimit, else → Api) for non-API responses
|
|
771
794
|
* (CDN errors, intermediaries) or malformed bodies. Client-only types
|
|
772
|
-
* (`Network`, `Cancelled`, `File`, `Config`) are filtered out of the
|
|
795
|
+
* (`Network`, `Timeout`, `Cancelled`, `File`, `Config`) are filtered out of the
|
|
773
796
|
* trusted set — a misbehaving server claiming one of those is ignored.
|
|
774
797
|
*
|
|
775
798
|
* `operationName` (e.g. `"Get account"`) is used to compose the fallback
|
|
@@ -789,8 +812,9 @@ export declare class ShipError extends Error {
|
|
|
789
812
|
* Routing:
|
|
790
813
|
* - Already a `ShipError` → returned as-is (caller's intent preserved)
|
|
791
814
|
* - `AbortError` → `ShipError.cancelled(...)` — someone stopped it on purpose
|
|
792
|
-
* - `TimeoutError` → `ShipError.
|
|
793
|
-
*
|
|
815
|
+
* - `TimeoutError` → `ShipError.timeout(...)` — a deadline expired; the
|
|
816
|
+
* message names the timeout, and the type is in the network CATEGORY
|
|
817
|
+
* because nothing was exchanged
|
|
794
818
|
* - A transport failure → `ShipError.network(...)` — see `isTransportFailure`
|
|
795
819
|
* for what each runtime offers as evidence
|
|
796
820
|
* - Any other `Error` → `ShipError(Api, ...)` (no HTTP status — fetch never reached the server)
|
|
@@ -837,6 +861,14 @@ export declare class ShipError extends Error {
|
|
|
837
861
|
static authentication(message?: string, details?: unknown): ShipError;
|
|
838
862
|
static business(message: string, status?: number, details?: unknown): ShipError;
|
|
839
863
|
static network(message: string, details?: unknown): ShipError;
|
|
864
|
+
/**
|
|
865
|
+
* A deadline expired before the exchange completed.
|
|
866
|
+
*
|
|
867
|
+
* Statusless like its four client-only siblings: no exchange completed, so
|
|
868
|
+
* there is no HTTP status to report. `isNetworkError()` is true — see
|
|
869
|
+
* `ErrorType.Timeout` for why the category is shared and the type is not.
|
|
870
|
+
*/
|
|
871
|
+
static timeout(message: string, details?: unknown): ShipError;
|
|
840
872
|
static cancelled(message: string, details?: unknown): ShipError;
|
|
841
873
|
static file(message: string, details?: unknown): ShipError;
|
|
842
874
|
static config(message: string, details?: unknown): ShipError;
|
|
@@ -1233,6 +1265,47 @@ export declare function validateApiUrl(apiUrl: string): void;
|
|
|
1233
1265
|
* Example: "happy-cat-abc1234.shipstatic.com"
|
|
1234
1266
|
*/
|
|
1235
1267
|
export declare function isDeployment(input: string): boolean;
|
|
1268
|
+
/**
|
|
1269
|
+
* The envelope a requested lifetime must fit — one word, one grammar, wherever
|
|
1270
|
+
* the platform lets a caller choose how long something lives.
|
|
1271
|
+
*
|
|
1272
|
+
* Two resources wear it: `TokenCreateOptions.ttl` and
|
|
1273
|
+
* `DeploymentUploadOptions.ttl`. It lives here rather than on the server by
|
|
1274
|
+
* the format-vs-policy rule — a client can decide offline whether a duration
|
|
1275
|
+
* is well-formed, and the API rejects the same value the same way. What is
|
|
1276
|
+
* NOT here is any per-plan ceiling: no such policy exists, and one delivered
|
|
1277
|
+
* speculatively through `/limits` would be an owner for a decision nobody has
|
|
1278
|
+
* made.
|
|
1279
|
+
*/
|
|
1280
|
+
export declare const TTL_CONSTRAINTS: {
|
|
1281
|
+
/**
|
|
1282
|
+
* Shortest requestable lifetime, in seconds. One rather than zero: a
|
|
1283
|
+
* deployment that expires the instant it is created is not a shorter lease,
|
|
1284
|
+
* it is a deploy that was never live, and `0` is how an unset variable
|
|
1285
|
+
* arrives.
|
|
1286
|
+
*/
|
|
1287
|
+
readonly MIN_SECONDS: 1;
|
|
1288
|
+
/** Longest requestable lifetime, in seconds — one year. */
|
|
1289
|
+
readonly MAX_SECONDS: number;
|
|
1290
|
+
};
|
|
1291
|
+
/**
|
|
1292
|
+
* Validate a requested lifetime in SECONDS and return it, or `undefined` when
|
|
1293
|
+
* none was asked for.
|
|
1294
|
+
*
|
|
1295
|
+
* **A duration, never an instant.** The caller says how long; the server owns
|
|
1296
|
+
* what time it is and stamps the expiry — so a client's clock, however wrong,
|
|
1297
|
+
* cannot shorten or extend a lease. That is the tokens precedent, and it is
|
|
1298
|
+
* why this rule measures a count of seconds rather than checking a timestamp
|
|
1299
|
+
* against `now`.
|
|
1300
|
+
*
|
|
1301
|
+
* Fractions are refused rather than rounded: a caller who wrote `1.5` meant
|
|
1302
|
+
* something the wire cannot carry, and silently choosing `1` or `2` for them
|
|
1303
|
+
* is a decision the platform has no standing to make.
|
|
1304
|
+
*
|
|
1305
|
+
* Single source of truth shared by the API (the tokens route and the deploy
|
|
1306
|
+
* schema), the SDK's request boundary, and the CLI's parser.
|
|
1307
|
+
*/
|
|
1308
|
+
export declare function validateTtl(value: unknown): number | undefined;
|
|
1236
1309
|
/**
|
|
1237
1310
|
* Request payload for SPA check endpoint
|
|
1238
1311
|
*/
|
|
@@ -1365,6 +1438,27 @@ export interface DeploymentUploadOptions {
|
|
|
1365
1438
|
* into missing analytics rather than an error. See {@link DeploymentVia}.
|
|
1366
1439
|
*/
|
|
1367
1440
|
via?: DeploymentViaType;
|
|
1441
|
+
/**
|
|
1442
|
+
* Seconds until this deployment expires; omit for one that never does.
|
|
1443
|
+
*
|
|
1444
|
+
* The platform reclaims it when the time is up — an ephemeral deployment,
|
|
1445
|
+
* chosen by the deployer rather than by the identity. The same word and the
|
|
1446
|
+
* same grammar as {@link TokenCreateOptions.ttl}, bounded by
|
|
1447
|
+
* {@link TTL_CONSTRAINTS}.
|
|
1448
|
+
*
|
|
1449
|
+
* **Requires a credential.** An anonymous deploy has no deployer, and the
|
|
1450
|
+
* platform owns anonymous lifetime as policy
|
|
1451
|
+
* ({@link PUBLIC_DEPLOYMENT_TTL_SECONDS}) — so a ttl on one is refused
|
|
1452
|
+
* rather than honoured or ignored.
|
|
1453
|
+
*
|
|
1454
|
+
* **A deployment carrying one cannot be linked to a domain.** A domain is a
|
|
1455
|
+
* commitment and a deadline is its opposite; the API refuses the link, which
|
|
1456
|
+
* is what keeps the reaper from tearing a live domain's target away.
|
|
1457
|
+
*
|
|
1458
|
+
* Immutable, like every other field of a deployment: to keep something
|
|
1459
|
+
* longer, redeploy.
|
|
1460
|
+
*/
|
|
1461
|
+
ttl?: number;
|
|
1368
1462
|
/**
|
|
1369
1463
|
* Optional password that protects this deployment.
|
|
1370
1464
|
*
|
package/dist/index.js
CHANGED
|
@@ -214,6 +214,12 @@ export const DEPLOY_FIELDS = {
|
|
|
214
214
|
VIA: 'via',
|
|
215
215
|
/** Plaintext password — the API hashes it server-side. */
|
|
216
216
|
PASSWORD: 'password',
|
|
217
|
+
/**
|
|
218
|
+
* Requested lifetime in SECONDS — a duration, never an instant. The API
|
|
219
|
+
* computes and stores the expiry, so the wire carries no client clock.
|
|
220
|
+
* See {@link validateTtl}.
|
|
221
|
+
*/
|
|
222
|
+
TTL: 'ttl',
|
|
217
223
|
/** @internal Server-processing flag — first-party `/upload` only. */
|
|
218
224
|
BUILD: 'build',
|
|
219
225
|
/** @internal Server-processing flag — first-party `/upload` only. */
|
|
@@ -271,6 +277,23 @@ export const ErrorType = {
|
|
|
271
277
|
Maintenance: 'maintenance',
|
|
272
278
|
/** Network/connection error. Client-side only — set by HTTP clients on fetch failure; never produced server-side. */
|
|
273
279
|
Network: 'network_error',
|
|
280
|
+
/**
|
|
281
|
+
* A deadline expired before the exchange completed. Client-side only — set
|
|
282
|
+
* by HTTP clients when a timeout signal fires; never produced server-side.
|
|
283
|
+
*
|
|
284
|
+
* A member of the NETWORK category rather than a sibling of it:
|
|
285
|
+
* `isNetworkError()` answers "nothing was exchanged", which is true of a
|
|
286
|
+
* deadline exactly as it is of a refused connection, so every consumer that
|
|
287
|
+
* retries, declines to report, or declines to relay a wire message on that
|
|
288
|
+
* category is already right about a timeout. The distinct TYPE exists for
|
|
289
|
+
* the one decision the category cannot make — what to SAY. "Check your
|
|
290
|
+
* internet connection" is the wrong sentence for a five-minute deploy
|
|
291
|
+
* ceiling, and a surface can only tell the two apart by type.
|
|
292
|
+
*
|
|
293
|
+
* The same relationship every comparable SDK ships:
|
|
294
|
+
* `APIConnectionTimeoutError extends APIConnectionError`.
|
|
295
|
+
*/
|
|
296
|
+
Timeout: 'timeout_error',
|
|
274
297
|
/** Operation was cancelled. Client-side only — set on `AbortSignal` abort; never produced server-side. */
|
|
275
298
|
Cancelled: 'operation_cancelled',
|
|
276
299
|
/** File operation error. Client-side only — set by SDK during local file processing; never produced server-side. */
|
|
@@ -286,6 +309,7 @@ export const ErrorType = {
|
|
|
286
309
|
*/
|
|
287
310
|
const CLIENT_ONLY_ERROR_TYPES = new Set([
|
|
288
311
|
ErrorType.Network,
|
|
312
|
+
ErrorType.Timeout,
|
|
289
313
|
ErrorType.Cancelled,
|
|
290
314
|
ErrorType.File,
|
|
291
315
|
ErrorType.Config,
|
|
@@ -301,12 +325,20 @@ const ERROR_CATEGORIES = {
|
|
|
301
325
|
* over the statusless ones too — those are raised locally and have no
|
|
302
326
|
* status for `isClientError`'s second arm to read, so omitting one makes it
|
|
303
327
|
* read as a server fault. The rule is the membership test: every type in
|
|
304
|
-
* `CLIENT_ONLY_ERROR_TYPES` except
|
|
305
|
-
*
|
|
328
|
+
* `CLIENT_ONLY_ERROR_TYPES` except the two `isNetworkError` owns belongs
|
|
329
|
+
* here.
|
|
306
330
|
*
|
|
307
331
|
* `Cancelled` was missing until 2026-07-29, which is exactly that failure:
|
|
308
332
|
* a caller who aborted their own deploy was told "server error: please try
|
|
309
333
|
* again" — the CLI's fallback for everything this set does not claim.
|
|
334
|
+
*
|
|
335
|
+
* `Timeout` is deliberately NOT here, and it is the sharper case, because
|
|
336
|
+
* it is the one client-only type that is not the client's fault: the
|
|
337
|
+
* caller set a ceiling, but what exhausted it was the network or the
|
|
338
|
+
* server. Reading it as client-attributable would say the caller erred,
|
|
339
|
+
* and it would silently disarm every consumer whose retry predicate
|
|
340
|
+
* declines `isClientError()` — a deadline is precisely the failure worth
|
|
341
|
+
* a second attempt.
|
|
310
342
|
*/
|
|
311
343
|
client: new Set([
|
|
312
344
|
ErrorType.Business,
|
|
@@ -318,7 +350,14 @@ const ERROR_CATEGORIES = {
|
|
|
318
350
|
ErrorType.RateLimit,
|
|
319
351
|
ErrorType.Validation,
|
|
320
352
|
]),
|
|
321
|
-
|
|
353
|
+
/**
|
|
354
|
+
* The exchange never happened. Two types, one category: a refused
|
|
355
|
+
* connection and an expired deadline differ in what a surface should SAY
|
|
356
|
+
* and in nothing else a consumer decides on — both are retryable, neither
|
|
357
|
+
* carries a wire message to relay, neither is worth reporting as an
|
|
358
|
+
* incident. See `ErrorType.Timeout` for why the type is distinct anyway.
|
|
359
|
+
*/
|
|
360
|
+
network: new Set([ErrorType.Network, ErrorType.Timeout]),
|
|
322
361
|
auth: new Set([ErrorType.Authentication]),
|
|
323
362
|
};
|
|
324
363
|
/**
|
|
@@ -382,9 +421,24 @@ const MAX_FOREIGN_MESSAGE_LENGTH = 200;
|
|
|
382
421
|
* that runtime (`cloudflare/mcp`) reaches the API through a service BINDING,
|
|
383
422
|
* which is in-process and does not produce transport rejections at all.
|
|
384
423
|
*
|
|
385
|
-
* The
|
|
386
|
-
*
|
|
387
|
-
*
|
|
424
|
+
* **The `TokenProvider` case stopped being a trade when clients gained
|
|
425
|
+
* retries.** A caller's provider that throws a coded error is typed `Network`
|
|
426
|
+
* here, which was recorded as "both are wrong for it; `Network` is the cheaper
|
|
427
|
+
* wrong" — written when the classification decided only what a surface would
|
|
428
|
+
* SAY. It now also decides whether the call is retried, and that turns the
|
|
429
|
+
* cheaper wrong into the right answer: a `TokenProvider` is where minting and
|
|
430
|
+
* refresh live, so the common one is an OAuth refresh over the network, and a
|
|
431
|
+
* transient failure there is precisely what another attempt repairs.
|
|
432
|
+
*
|
|
433
|
+
* The residual cost is a deterministic provider fault — a genuinely missing
|
|
434
|
+
* keychain entry — invoking the provider three times over a few hundred
|
|
435
|
+
* milliseconds before failing with the same error. No request leaves the
|
|
436
|
+
* process on any of them. That is the cheap direction of a bet whose other
|
|
437
|
+
* side is a refused deploy, and suppressing it would need a way to mark
|
|
438
|
+
* credential faults non-retryable: machinery with one holder, refused by the
|
|
439
|
+
* estate's stopping rule. Provider failures that carry no code are `Api` and
|
|
440
|
+
* are not retried at all, and a provider yielding nothing is `Authentication`
|
|
441
|
+
* by the fail-closed invariant, which is likewise terminal.
|
|
388
442
|
*/
|
|
389
443
|
function isTransportFailure(cause) {
|
|
390
444
|
const code = cause.code;
|
|
@@ -447,7 +501,7 @@ export class ShipError extends Error {
|
|
|
447
501
|
* on the client). Falls back to status-derived (401 → Authentication,
|
|
448
502
|
* 403 → Forbidden, 429 → RateLimit, else → Api) for non-API responses
|
|
449
503
|
* (CDN errors, intermediaries) or malformed bodies. Client-only types
|
|
450
|
-
* (`Network`, `Cancelled`, `File`, `Config`) are filtered out of the
|
|
504
|
+
* (`Network`, `Timeout`, `Cancelled`, `File`, `Config`) are filtered out of the
|
|
451
505
|
* trusted set — a misbehaving server claiming one of those is ignored.
|
|
452
506
|
*
|
|
453
507
|
* `operationName` (e.g. `"Get account"`) is used to compose the fallback
|
|
@@ -531,8 +585,9 @@ export class ShipError extends Error {
|
|
|
531
585
|
* Routing:
|
|
532
586
|
* - Already a `ShipError` → returned as-is (caller's intent preserved)
|
|
533
587
|
* - `AbortError` → `ShipError.cancelled(...)` — someone stopped it on purpose
|
|
534
|
-
* - `TimeoutError` → `ShipError.
|
|
535
|
-
*
|
|
588
|
+
* - `TimeoutError` → `ShipError.timeout(...)` — a deadline expired; the
|
|
589
|
+
* message names the timeout, and the type is in the network CATEGORY
|
|
590
|
+
* because nothing was exchanged
|
|
536
591
|
* - A transport failure → `ShipError.network(...)` — see `isTransportFailure`
|
|
537
592
|
* for what each runtime offers as evidence
|
|
538
593
|
* - Any other `Error` → `ShipError(Api, ...)` (no HTTP status — fetch never reached the server)
|
|
@@ -568,11 +623,14 @@ export class ShipError extends Error {
|
|
|
568
623
|
return ShipError.cancelled(`${op} was cancelled`);
|
|
569
624
|
}
|
|
570
625
|
if (name === 'TimeoutError') {
|
|
571
|
-
// A deadline
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
|
|
626
|
+
// A deadline: not a fault, not a cancellation, and — since it has its
|
|
627
|
+
// own type — no longer merely "network". Nothing was exchanged, which
|
|
628
|
+
// is what keeps it in the network CATEGORY and therefore retryable; the
|
|
629
|
+
// type is what lets a surface say "timed out" instead of sending
|
|
630
|
+
// someone to check their Wi-Fi. The runtime's own sentence is dropped
|
|
631
|
+
// rather than relayed: "The operation was aborted due to timeout" is
|
|
632
|
+
// the mechanism, not the news.
|
|
633
|
+
return ShipError.timeout(`${op} timed out`, { cause });
|
|
576
634
|
}
|
|
577
635
|
if (cause instanceof Error) {
|
|
578
636
|
if (isTransportFailure(cause)) {
|
|
@@ -621,6 +679,16 @@ export class ShipError extends Error {
|
|
|
621
679
|
static network(message, details) {
|
|
622
680
|
return new ShipError(ErrorType.Network, message, undefined, details);
|
|
623
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* A deadline expired before the exchange completed.
|
|
684
|
+
*
|
|
685
|
+
* Statusless like its four client-only siblings: no exchange completed, so
|
|
686
|
+
* there is no HTTP status to report. `isNetworkError()` is true — see
|
|
687
|
+
* `ErrorType.Timeout` for why the category is shared and the type is not.
|
|
688
|
+
*/
|
|
689
|
+
static timeout(message, details) {
|
|
690
|
+
return new ShipError(ErrorType.Timeout, message, undefined, details);
|
|
691
|
+
}
|
|
624
692
|
static cancelled(message, details) {
|
|
625
693
|
return new ShipError(ErrorType.Cancelled, message, undefined, details);
|
|
626
694
|
}
|
|
@@ -1264,6 +1332,60 @@ export function validateApiUrl(apiUrl) {
|
|
|
1264
1332
|
export function isDeployment(input) {
|
|
1265
1333
|
return /^[a-z]+-[a-z]+-[a-z0-9]{7}(\.[a-z0-9.-]+)?$/i.test(input);
|
|
1266
1334
|
}
|
|
1335
|
+
/**
|
|
1336
|
+
* The envelope a requested lifetime must fit — one word, one grammar, wherever
|
|
1337
|
+
* the platform lets a caller choose how long something lives.
|
|
1338
|
+
*
|
|
1339
|
+
* Two resources wear it: `TokenCreateOptions.ttl` and
|
|
1340
|
+
* `DeploymentUploadOptions.ttl`. It lives here rather than on the server by
|
|
1341
|
+
* the format-vs-policy rule — a client can decide offline whether a duration
|
|
1342
|
+
* is well-formed, and the API rejects the same value the same way. What is
|
|
1343
|
+
* NOT here is any per-plan ceiling: no such policy exists, and one delivered
|
|
1344
|
+
* speculatively through `/limits` would be an owner for a decision nobody has
|
|
1345
|
+
* made.
|
|
1346
|
+
*/
|
|
1347
|
+
export const TTL_CONSTRAINTS = {
|
|
1348
|
+
/**
|
|
1349
|
+
* Shortest requestable lifetime, in seconds. One rather than zero: a
|
|
1350
|
+
* deployment that expires the instant it is created is not a shorter lease,
|
|
1351
|
+
* it is a deploy that was never live, and `0` is how an unset variable
|
|
1352
|
+
* arrives.
|
|
1353
|
+
*/
|
|
1354
|
+
MIN_SECONDS: 1,
|
|
1355
|
+
/** Longest requestable lifetime, in seconds — one year. */
|
|
1356
|
+
MAX_SECONDS: 365 * 24 * 60 * 60,
|
|
1357
|
+
};
|
|
1358
|
+
/**
|
|
1359
|
+
* Validate a requested lifetime in SECONDS and return it, or `undefined` when
|
|
1360
|
+
* none was asked for.
|
|
1361
|
+
*
|
|
1362
|
+
* **A duration, never an instant.** The caller says how long; the server owns
|
|
1363
|
+
* what time it is and stamps the expiry — so a client's clock, however wrong,
|
|
1364
|
+
* cannot shorten or extend a lease. That is the tokens precedent, and it is
|
|
1365
|
+
* why this rule measures a count of seconds rather than checking a timestamp
|
|
1366
|
+
* against `now`.
|
|
1367
|
+
*
|
|
1368
|
+
* Fractions are refused rather than rounded: a caller who wrote `1.5` meant
|
|
1369
|
+
* something the wire cannot carry, and silently choosing `1` or `2` for them
|
|
1370
|
+
* is a decision the platform has no standing to make.
|
|
1371
|
+
*
|
|
1372
|
+
* Single source of truth shared by the API (the tokens route and the deploy
|
|
1373
|
+
* schema), the SDK's request boundary, and the CLI's parser.
|
|
1374
|
+
*/
|
|
1375
|
+
export function validateTtl(value) {
|
|
1376
|
+
if (value === undefined || value === null)
|
|
1377
|
+
return undefined;
|
|
1378
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
1379
|
+
throw ShipError.validation('TTL must be a number of seconds');
|
|
1380
|
+
}
|
|
1381
|
+
if (!Number.isInteger(value)) {
|
|
1382
|
+
throw ShipError.validation('TTL must be a whole number of seconds');
|
|
1383
|
+
}
|
|
1384
|
+
if (value < TTL_CONSTRAINTS.MIN_SECONDS || value > TTL_CONSTRAINTS.MAX_SECONDS) {
|
|
1385
|
+
throw ShipError.validation(`TTL must be between ${TTL_CONSTRAINTS.MIN_SECONDS} and ${TTL_CONSTRAINTS.MAX_SECONDS} seconds`);
|
|
1386
|
+
}
|
|
1387
|
+
return value;
|
|
1388
|
+
}
|
|
1267
1389
|
// =============================================================================
|
|
1268
1390
|
// PLATFORM CONSTANTS
|
|
1269
1391
|
// =============================================================================
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -756,6 +756,12 @@ export const DEPLOY_FIELDS = {
|
|
|
756
756
|
VIA: 'via',
|
|
757
757
|
/** Plaintext password — the API hashes it server-side. */
|
|
758
758
|
PASSWORD: 'password',
|
|
759
|
+
/**
|
|
760
|
+
* Requested lifetime in SECONDS — a duration, never an instant. The API
|
|
761
|
+
* computes and stores the expiry, so the wire carries no client clock.
|
|
762
|
+
* See {@link validateTtl}.
|
|
763
|
+
*/
|
|
764
|
+
TTL: 'ttl',
|
|
759
765
|
/** @internal Server-processing flag — first-party `/upload` only. */
|
|
760
766
|
BUILD: 'build',
|
|
761
767
|
/** @internal Server-processing flag — first-party `/upload` only. */
|
|
@@ -815,6 +821,23 @@ export const ErrorType = {
|
|
|
815
821
|
Maintenance: 'maintenance',
|
|
816
822
|
/** Network/connection error. Client-side only — set by HTTP clients on fetch failure; never produced server-side. */
|
|
817
823
|
Network: 'network_error',
|
|
824
|
+
/**
|
|
825
|
+
* A deadline expired before the exchange completed. Client-side only — set
|
|
826
|
+
* by HTTP clients when a timeout signal fires; never produced server-side.
|
|
827
|
+
*
|
|
828
|
+
* A member of the NETWORK category rather than a sibling of it:
|
|
829
|
+
* `isNetworkError()` answers "nothing was exchanged", which is true of a
|
|
830
|
+
* deadline exactly as it is of a refused connection, so every consumer that
|
|
831
|
+
* retries, declines to report, or declines to relay a wire message on that
|
|
832
|
+
* category is already right about a timeout. The distinct TYPE exists for
|
|
833
|
+
* the one decision the category cannot make — what to SAY. "Check your
|
|
834
|
+
* internet connection" is the wrong sentence for a five-minute deploy
|
|
835
|
+
* ceiling, and a surface can only tell the two apart by type.
|
|
836
|
+
*
|
|
837
|
+
* The same relationship every comparable SDK ships:
|
|
838
|
+
* `APIConnectionTimeoutError extends APIConnectionError`.
|
|
839
|
+
*/
|
|
840
|
+
Timeout: 'timeout_error',
|
|
818
841
|
/** Operation was cancelled. Client-side only — set on `AbortSignal` abort; never produced server-side. */
|
|
819
842
|
Cancelled: 'operation_cancelled',
|
|
820
843
|
/** File operation error. Client-side only — set by SDK during local file processing; never produced server-side. */
|
|
@@ -833,6 +856,7 @@ export type ErrorType = (typeof ErrorType)[keyof typeof ErrorType];
|
|
|
833
856
|
*/
|
|
834
857
|
const CLIENT_ONLY_ERROR_TYPES = new Set<string>([
|
|
835
858
|
ErrorType.Network,
|
|
859
|
+
ErrorType.Timeout,
|
|
836
860
|
ErrorType.Cancelled,
|
|
837
861
|
ErrorType.File,
|
|
838
862
|
ErrorType.Config,
|
|
@@ -849,12 +873,20 @@ const ERROR_CATEGORIES = {
|
|
|
849
873
|
* over the statusless ones too — those are raised locally and have no
|
|
850
874
|
* status for `isClientError`'s second arm to read, so omitting one makes it
|
|
851
875
|
* read as a server fault. The rule is the membership test: every type in
|
|
852
|
-
* `CLIENT_ONLY_ERROR_TYPES` except
|
|
853
|
-
*
|
|
876
|
+
* `CLIENT_ONLY_ERROR_TYPES` except the two `isNetworkError` owns belongs
|
|
877
|
+
* here.
|
|
854
878
|
*
|
|
855
879
|
* `Cancelled` was missing until 2026-07-29, which is exactly that failure:
|
|
856
880
|
* a caller who aborted their own deploy was told "server error: please try
|
|
857
881
|
* again" — the CLI's fallback for everything this set does not claim.
|
|
882
|
+
*
|
|
883
|
+
* `Timeout` is deliberately NOT here, and it is the sharper case, because
|
|
884
|
+
* it is the one client-only type that is not the client's fault: the
|
|
885
|
+
* caller set a ceiling, but what exhausted it was the network or the
|
|
886
|
+
* server. Reading it as client-attributable would say the caller erred,
|
|
887
|
+
* and it would silently disarm every consumer whose retry predicate
|
|
888
|
+
* declines `isClientError()` — a deadline is precisely the failure worth
|
|
889
|
+
* a second attempt.
|
|
858
890
|
*/
|
|
859
891
|
client: new Set<ErrorType>([
|
|
860
892
|
ErrorType.Business,
|
|
@@ -866,7 +898,14 @@ const ERROR_CATEGORIES = {
|
|
|
866
898
|
ErrorType.RateLimit,
|
|
867
899
|
ErrorType.Validation,
|
|
868
900
|
]),
|
|
869
|
-
|
|
901
|
+
/**
|
|
902
|
+
* The exchange never happened. Two types, one category: a refused
|
|
903
|
+
* connection and an expired deadline differ in what a surface should SAY
|
|
904
|
+
* and in nothing else a consumer decides on — both are retryable, neither
|
|
905
|
+
* carries a wire message to relay, neither is worth reporting as an
|
|
906
|
+
* incident. See `ErrorType.Timeout` for why the type is distinct anyway.
|
|
907
|
+
*/
|
|
908
|
+
network: new Set<ErrorType>([ErrorType.Network, ErrorType.Timeout]),
|
|
870
909
|
auth: new Set<ErrorType>([ErrorType.Authentication]),
|
|
871
910
|
} as const;
|
|
872
911
|
|
|
@@ -935,9 +974,24 @@ const MAX_FOREIGN_MESSAGE_LENGTH = 200;
|
|
|
935
974
|
* that runtime (`cloudflare/mcp`) reaches the API through a service BINDING,
|
|
936
975
|
* which is in-process and does not produce transport rejections at all.
|
|
937
976
|
*
|
|
938
|
-
* The
|
|
939
|
-
*
|
|
940
|
-
*
|
|
977
|
+
* **The `TokenProvider` case stopped being a trade when clients gained
|
|
978
|
+
* retries.** A caller's provider that throws a coded error is typed `Network`
|
|
979
|
+
* here, which was recorded as "both are wrong for it; `Network` is the cheaper
|
|
980
|
+
* wrong" — written when the classification decided only what a surface would
|
|
981
|
+
* SAY. It now also decides whether the call is retried, and that turns the
|
|
982
|
+
* cheaper wrong into the right answer: a `TokenProvider` is where minting and
|
|
983
|
+
* refresh live, so the common one is an OAuth refresh over the network, and a
|
|
984
|
+
* transient failure there is precisely what another attempt repairs.
|
|
985
|
+
*
|
|
986
|
+
* The residual cost is a deterministic provider fault — a genuinely missing
|
|
987
|
+
* keychain entry — invoking the provider three times over a few hundred
|
|
988
|
+
* milliseconds before failing with the same error. No request leaves the
|
|
989
|
+
* process on any of them. That is the cheap direction of a bet whose other
|
|
990
|
+
* side is a refused deploy, and suppressing it would need a way to mark
|
|
991
|
+
* credential faults non-retryable: machinery with one holder, refused by the
|
|
992
|
+
* estate's stopping rule. Provider failures that carry no code are `Api` and
|
|
993
|
+
* are not retried at all, and a provider yielding nothing is `Authentication`
|
|
994
|
+
* by the fail-closed invariant, which is likewise terminal.
|
|
941
995
|
*/
|
|
942
996
|
function isTransportFailure(cause: Error): boolean {
|
|
943
997
|
const code = (cause as { code?: unknown }).code;
|
|
@@ -1019,7 +1073,7 @@ export class ShipError extends Error {
|
|
|
1019
1073
|
* on the client). Falls back to status-derived (401 → Authentication,
|
|
1020
1074
|
* 403 → Forbidden, 429 → RateLimit, else → Api) for non-API responses
|
|
1021
1075
|
* (CDN errors, intermediaries) or malformed bodies. Client-only types
|
|
1022
|
-
* (`Network`, `Cancelled`, `File`, `Config`) are filtered out of the
|
|
1076
|
+
* (`Network`, `Timeout`, `Cancelled`, `File`, `Config`) are filtered out of the
|
|
1023
1077
|
* trusted set — a misbehaving server claiming one of those is ignored.
|
|
1024
1078
|
*
|
|
1025
1079
|
* `operationName` (e.g. `"Get account"`) is used to compose the fallback
|
|
@@ -1107,8 +1161,9 @@ export class ShipError extends Error {
|
|
|
1107
1161
|
* Routing:
|
|
1108
1162
|
* - Already a `ShipError` → returned as-is (caller's intent preserved)
|
|
1109
1163
|
* - `AbortError` → `ShipError.cancelled(...)` — someone stopped it on purpose
|
|
1110
|
-
* - `TimeoutError` → `ShipError.
|
|
1111
|
-
*
|
|
1164
|
+
* - `TimeoutError` → `ShipError.timeout(...)` — a deadline expired; the
|
|
1165
|
+
* message names the timeout, and the type is in the network CATEGORY
|
|
1166
|
+
* because nothing was exchanged
|
|
1112
1167
|
* - A transport failure → `ShipError.network(...)` — see `isTransportFailure`
|
|
1113
1168
|
* for what each runtime offers as evidence
|
|
1114
1169
|
* - Any other `Error` → `ShipError(Api, ...)` (no HTTP status — fetch never reached the server)
|
|
@@ -1145,11 +1200,14 @@ export class ShipError extends Error {
|
|
|
1145
1200
|
return ShipError.cancelled(`${op} was cancelled`);
|
|
1146
1201
|
}
|
|
1147
1202
|
if (name === 'TimeoutError') {
|
|
1148
|
-
// A deadline
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
1151
|
-
//
|
|
1152
|
-
|
|
1203
|
+
// A deadline: not a fault, not a cancellation, and — since it has its
|
|
1204
|
+
// own type — no longer merely "network". Nothing was exchanged, which
|
|
1205
|
+
// is what keeps it in the network CATEGORY and therefore retryable; the
|
|
1206
|
+
// type is what lets a surface say "timed out" instead of sending
|
|
1207
|
+
// someone to check their Wi-Fi. The runtime's own sentence is dropped
|
|
1208
|
+
// rather than relayed: "The operation was aborted due to timeout" is
|
|
1209
|
+
// the mechanism, not the news.
|
|
1210
|
+
return ShipError.timeout(`${op} timed out`, { cause });
|
|
1153
1211
|
}
|
|
1154
1212
|
|
|
1155
1213
|
if (cause instanceof Error) {
|
|
@@ -1209,6 +1267,17 @@ export class ShipError extends Error {
|
|
|
1209
1267
|
return new ShipError(ErrorType.Network, message, undefined, details);
|
|
1210
1268
|
}
|
|
1211
1269
|
|
|
1270
|
+
/**
|
|
1271
|
+
* A deadline expired before the exchange completed.
|
|
1272
|
+
*
|
|
1273
|
+
* Statusless like its four client-only siblings: no exchange completed, so
|
|
1274
|
+
* there is no HTTP status to report. `isNetworkError()` is true — see
|
|
1275
|
+
* `ErrorType.Timeout` for why the category is shared and the type is not.
|
|
1276
|
+
*/
|
|
1277
|
+
static timeout(message: string, details?: unknown): ShipError {
|
|
1278
|
+
return new ShipError(ErrorType.Timeout, message, undefined, details);
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1212
1281
|
static cancelled(message: string, details?: unknown): ShipError {
|
|
1213
1282
|
return new ShipError(ErrorType.Cancelled, message, undefined, details);
|
|
1214
1283
|
}
|
|
@@ -1985,6 +2054,63 @@ export function isDeployment(input: string): boolean {
|
|
|
1985
2054
|
return /^[a-z]+-[a-z]+-[a-z0-9]{7}(\.[a-z0-9.-]+)?$/i.test(input);
|
|
1986
2055
|
}
|
|
1987
2056
|
|
|
2057
|
+
/**
|
|
2058
|
+
* The envelope a requested lifetime must fit — one word, one grammar, wherever
|
|
2059
|
+
* the platform lets a caller choose how long something lives.
|
|
2060
|
+
*
|
|
2061
|
+
* Two resources wear it: `TokenCreateOptions.ttl` and
|
|
2062
|
+
* `DeploymentUploadOptions.ttl`. It lives here rather than on the server by
|
|
2063
|
+
* the format-vs-policy rule — a client can decide offline whether a duration
|
|
2064
|
+
* is well-formed, and the API rejects the same value the same way. What is
|
|
2065
|
+
* NOT here is any per-plan ceiling: no such policy exists, and one delivered
|
|
2066
|
+
* speculatively through `/limits` would be an owner for a decision nobody has
|
|
2067
|
+
* made.
|
|
2068
|
+
*/
|
|
2069
|
+
export const TTL_CONSTRAINTS = {
|
|
2070
|
+
/**
|
|
2071
|
+
* Shortest requestable lifetime, in seconds. One rather than zero: a
|
|
2072
|
+
* deployment that expires the instant it is created is not a shorter lease,
|
|
2073
|
+
* it is a deploy that was never live, and `0` is how an unset variable
|
|
2074
|
+
* arrives.
|
|
2075
|
+
*/
|
|
2076
|
+
MIN_SECONDS: 1,
|
|
2077
|
+
/** Longest requestable lifetime, in seconds — one year. */
|
|
2078
|
+
MAX_SECONDS: 365 * 24 * 60 * 60,
|
|
2079
|
+
} as const;
|
|
2080
|
+
|
|
2081
|
+
/**
|
|
2082
|
+
* Validate a requested lifetime in SECONDS and return it, or `undefined` when
|
|
2083
|
+
* none was asked for.
|
|
2084
|
+
*
|
|
2085
|
+
* **A duration, never an instant.** The caller says how long; the server owns
|
|
2086
|
+
* what time it is and stamps the expiry — so a client's clock, however wrong,
|
|
2087
|
+
* cannot shorten or extend a lease. That is the tokens precedent, and it is
|
|
2088
|
+
* why this rule measures a count of seconds rather than checking a timestamp
|
|
2089
|
+
* against `now`.
|
|
2090
|
+
*
|
|
2091
|
+
* Fractions are refused rather than rounded: a caller who wrote `1.5` meant
|
|
2092
|
+
* something the wire cannot carry, and silently choosing `1` or `2` for them
|
|
2093
|
+
* is a decision the platform has no standing to make.
|
|
2094
|
+
*
|
|
2095
|
+
* Single source of truth shared by the API (the tokens route and the deploy
|
|
2096
|
+
* schema), the SDK's request boundary, and the CLI's parser.
|
|
2097
|
+
*/
|
|
2098
|
+
export function validateTtl(value: unknown): number | undefined {
|
|
2099
|
+
if (value === undefined || value === null) return undefined;
|
|
2100
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
2101
|
+
throw ShipError.validation('TTL must be a number of seconds');
|
|
2102
|
+
}
|
|
2103
|
+
if (!Number.isInteger(value)) {
|
|
2104
|
+
throw ShipError.validation('TTL must be a whole number of seconds');
|
|
2105
|
+
}
|
|
2106
|
+
if (value < TTL_CONSTRAINTS.MIN_SECONDS || value > TTL_CONSTRAINTS.MAX_SECONDS) {
|
|
2107
|
+
throw ShipError.validation(
|
|
2108
|
+
`TTL must be between ${TTL_CONSTRAINTS.MIN_SECONDS} and ${TTL_CONSTRAINTS.MAX_SECONDS} seconds`,
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
return value;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
1988
2114
|
// =============================================================================
|
|
1989
2115
|
// SPA CHECK TYPES
|
|
1990
2116
|
// =============================================================================
|
|
@@ -2142,6 +2268,27 @@ export interface DeploymentUploadOptions {
|
|
|
2142
2268
|
* into missing analytics rather than an error. See {@link DeploymentVia}.
|
|
2143
2269
|
*/
|
|
2144
2270
|
via?: DeploymentViaType;
|
|
2271
|
+
/**
|
|
2272
|
+
* Seconds until this deployment expires; omit for one that never does.
|
|
2273
|
+
*
|
|
2274
|
+
* The platform reclaims it when the time is up — an ephemeral deployment,
|
|
2275
|
+
* chosen by the deployer rather than by the identity. The same word and the
|
|
2276
|
+
* same grammar as {@link TokenCreateOptions.ttl}, bounded by
|
|
2277
|
+
* {@link TTL_CONSTRAINTS}.
|
|
2278
|
+
*
|
|
2279
|
+
* **Requires a credential.** An anonymous deploy has no deployer, and the
|
|
2280
|
+
* platform owns anonymous lifetime as policy
|
|
2281
|
+
* ({@link PUBLIC_DEPLOYMENT_TTL_SECONDS}) — so a ttl on one is refused
|
|
2282
|
+
* rather than honoured or ignored.
|
|
2283
|
+
*
|
|
2284
|
+
* **A deployment carrying one cannot be linked to a domain.** A domain is a
|
|
2285
|
+
* commitment and a deadline is its opposite; the API refuses the link, which
|
|
2286
|
+
* is what keeps the reaper from tearing a live domain's target away.
|
|
2287
|
+
*
|
|
2288
|
+
* Immutable, like every other field of a deployment: to keep something
|
|
2289
|
+
* longer, redeploy.
|
|
2290
|
+
*/
|
|
2291
|
+
ttl?: number;
|
|
2145
2292
|
/**
|
|
2146
2293
|
* Optional password that protects this deployment.
|
|
2147
2294
|
*
|