@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 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.network(...)` — a deadline expired, so
793
- * nothing was exchanged; the message names the timeout
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 `Network` (which `isNetworkError` owns)
305
- * belongs here.
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
- network: new Set([ErrorType.Network]),
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 accepted trade is unchanged: a caller's `TokenProvider` that throws a
386
- * coded error (`ENOENT` from a keychain read) is typed `Network` rather than
387
- * `Api`. Both are wrong for it; `Network` is the cheaper wrong.
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.network(...)` — a deadline expired, so
535
- * nothing was exchanged; the message names the timeout
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, not a fault and not a cancellation: nothing was exchanged,
572
- // which is exactly what `Network` claims. The runtime's own sentence is
573
- // dropped here rather than relayed "The operation was aborted due to
574
- // timeout" is the mechanism, not the news.
575
- return ShipError.network(`${op} timed out`, { cause });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/types",
3
- "version": "2.7.0-beta.2",
3
+ "version": "2.7.0-beta.4",
4
4
  "description": "Shared types for ShipStatic platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
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 `Network` (which `isNetworkError` owns)
853
- * belongs here.
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
- network: new Set<ErrorType>([ErrorType.Network]),
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 accepted trade is unchanged: a caller's `TokenProvider` that throws a
939
- * coded error (`ENOENT` from a keychain read) is typed `Network` rather than
940
- * `Api`. Both are wrong for it; `Network` is the cheaper wrong.
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.network(...)` — a deadline expired, so
1111
- * nothing was exchanged; the message names the timeout
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, not a fault and not a cancellation: nothing was exchanged,
1149
- // which is exactly what `Network` claims. The runtime's own sentence is
1150
- // dropped here rather than relayed "The operation was aborted due to
1151
- // timeout" is the mechanism, not the news.
1152
- return ShipError.network(`${op} timed out`, { cause });
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
  *