@shipstatic/types 2.7.0-beta.3 → 2.7.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/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. */
@@ -1259,6 +1265,47 @@ export declare function validateApiUrl(apiUrl: string): void;
1259
1265
  * Example: "happy-cat-abc1234.shipstatic.com"
1260
1266
  */
1261
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;
1262
1309
  /**
1263
1310
  * Request payload for SPA check endpoint
1264
1311
  */
@@ -1391,6 +1438,27 @@ export interface DeploymentUploadOptions {
1391
1438
  * into missing analytics rather than an error. See {@link DeploymentVia}.
1392
1439
  */
1393
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;
1394
1462
  /**
1395
1463
  * Optional password that protects this deployment.
1396
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. */
@@ -415,9 +421,24 @@ const MAX_FOREIGN_MESSAGE_LENGTH = 200;
415
421
  * that runtime (`cloudflare/mcp`) reaches the API through a service BINDING,
416
422
  * which is in-process and does not produce transport rejections at all.
417
423
  *
418
- * The accepted trade is unchanged: a caller's `TokenProvider` that throws a
419
- * coded error (`ENOENT` from a keychain read) is typed `Network` rather than
420
- * `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.
421
442
  */
422
443
  function isTransportFailure(cause) {
423
444
  const code = cause.code;
@@ -1311,6 +1332,60 @@ export function validateApiUrl(apiUrl) {
1311
1332
  export function isDeployment(input) {
1312
1333
  return /^[a-z]+-[a-z]+-[a-z0-9]{7}(\.[a-z0-9.-]+)?$/i.test(input);
1313
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
+ }
1314
1389
  // =============================================================================
1315
1390
  // PLATFORM CONSTANTS
1316
1391
  // =============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/types",
3
- "version": "2.7.0-beta.3",
3
+ "version": "2.7.0",
4
4
  "description": "Shared types for ShipStatic platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -41,7 +41,7 @@
41
41
  "url": "https://github.com/shipstatic/types/issues"
42
42
  },
43
43
  "engines": {
44
- "node": ">=20.0.0"
44
+ "node": ">=20.19.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@biomejs/biome": "2.5.5",
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. */
@@ -968,9 +974,24 @@ const MAX_FOREIGN_MESSAGE_LENGTH = 200;
968
974
  * that runtime (`cloudflare/mcp`) reaches the API through a service BINDING,
969
975
  * which is in-process and does not produce transport rejections at all.
970
976
  *
971
- * The accepted trade is unchanged: a caller's `TokenProvider` that throws a
972
- * coded error (`ENOENT` from a keychain read) is typed `Network` rather than
973
- * `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.
974
995
  */
975
996
  function isTransportFailure(cause: Error): boolean {
976
997
  const code = (cause as { code?: unknown }).code;
@@ -2033,6 +2054,63 @@ export function isDeployment(input: string): boolean {
2033
2054
  return /^[a-z]+-[a-z]+-[a-z0-9]{7}(\.[a-z0-9.-]+)?$/i.test(input);
2034
2055
  }
2035
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
+
2036
2114
  // =============================================================================
2037
2115
  // SPA CHECK TYPES
2038
2116
  // =============================================================================
@@ -2190,6 +2268,27 @@ export interface DeploymentUploadOptions {
2190
2268
  * into missing analytics rather than an error. See {@link DeploymentVia}.
2191
2269
  */
2192
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;
2193
2292
  /**
2194
2293
  * Optional password that protects this deployment.
2195
2294
  *