@shipstatic/types 2.5.0-beta.2 → 2.5.0-beta.20

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.js CHANGED
@@ -14,6 +14,83 @@ export const DeploymentStatus = {
14
14
  FAILED: 'failed',
15
15
  DELETING: 'deleting',
16
16
  };
17
+ /**
18
+ * Which client made a deployment — the origin-tracking vocabulary.
19
+ *
20
+ * A closed set with many authors: the CLI, the SDK, the dashboard, both MCP
21
+ * transports, the GitHub Action, the n8n node and the VS Code extension each
22
+ * name themselves here. It lived in the API's config until 2026-08-06, where
23
+ * being server-side made it unenforceable in the one direction that matters —
24
+ * every client wrote a bare string, and a value outside the set was **silently
25
+ * dropped** by the server, so a typo did not fail anywhere. It stopped
26
+ * recording where deploys came from and said nothing.
27
+ */
28
+ export const DeploymentVia = {
29
+ WEB: 'web',
30
+ SDK: 'sdk',
31
+ CLI: 'cli',
32
+ MCP: 'mcp',
33
+ GIT: 'git',
34
+ N8N: 'n8n',
35
+ GPT: 'gpt',
36
+ VSC: 'vsc',
37
+ };
38
+ /**
39
+ * Every path the public API answers on, declared once.
40
+ *
41
+ * The URL surface was written out in four places — the API's mounts, the
42
+ * SDK's client, the dashboard's client, and the post-deploy smoke — so a
43
+ * rename meant finding all four. The first three now read this table.
44
+ *
45
+ * The smoke (`cloudflare/api/smoke.mjs`) deliberately still spells its own:
46
+ * five of its nine paths are `/admin/*`, which this table excludes by
47
+ * design, and splitting one list between a registry and literals reads worse
48
+ * than keeping it uniform.
49
+ *
50
+ * **What this guarantees, exactly.** Collection paths are mounted from here,
51
+ * so producer and consumer cannot diverge. Item paths are declared here and
52
+ * consumed by clients, but the API spells them relative to their mount
53
+ * (`/:deployment/config`), so the table does not *generate* them — it is
54
+ * held to them by `api/tests/architecture/api-paths.test.ts`, which fails if
55
+ * any entry names a path no route answers. Some entries have no client yet
56
+ * (`DEPLOYMENT_CONFIG`, `DOMAIN_PROPAGATION` — endpoints the SDK
57
+ * deliberately does not reach); the fence is what keeps those honest rather
58
+ * than merely asserted.
59
+ *
60
+ * **The operator surface is deliberately absent.** `/admin/*` paths belong
61
+ * to `web/my`, for the same reason its row types do: this package is
62
+ * published, and the operator surface is not public (see `CLAUDE.md`, "Admin
63
+ * types"). A path here is a promise to every npm consumer; `/admin` is a
64
+ * promise to one dashboard.
65
+ *
66
+ * Item paths are functions rather than templates so the key is interpolated
67
+ * in one place, encoded the same way by every caller.
68
+ */
69
+ export const API_PATHS = {
70
+ DEPLOYMENTS: '/deployments',
71
+ DEPLOYMENT: (deployment) => `/deployments/${deployment}`,
72
+ DEPLOYMENT_CONFIG: (deployment) => `/deployments/${deployment}/config`,
73
+ DOMAINS: '/domains',
74
+ DOMAIN: (domain) => `/domains/${domain}`,
75
+ DOMAIN_VERIFY: (domain) => `/domains/${domain}/verify`,
76
+ DOMAIN_DNS: (domain) => `/domains/${domain}/dns`,
77
+ DOMAIN_RECORDS: (domain) => `/domains/${domain}/records`,
78
+ DOMAIN_SHARE: (domain) => `/domains/${domain}/share`,
79
+ DOMAIN_PROPAGATION: (domain) => `/domains/${domain}/propagation`,
80
+ DOMAINS_VALIDATE: '/domains/validate',
81
+ TOKENS: '/tokens',
82
+ TOKEN: (token) => `/tokens/${token}`,
83
+ ACCOUNT: '/account',
84
+ ACCOUNT_KEY: '/account/key',
85
+ ACCOUNT_CLAIM: '/account/claim',
86
+ ACTIVITIES: '/activities',
87
+ LABELS: '/labels',
88
+ LIMITS: '/limits',
89
+ PING: '/ping',
90
+ SETUP: '/setup',
91
+ SPA_CHECK: '/spa-check',
92
+ UPLOAD: '/upload',
93
+ };
17
94
  // =============================================================================
18
95
  // DOMAIN TYPES
19
96
  // =============================================================================
@@ -31,6 +108,68 @@ export const DomainStatus = {
31
108
  SUCCESS: 'success',
32
109
  PAUSED: 'paused',
33
110
  };
111
+ /**
112
+ * The envelope an `Idempotency-Key` must fit, and how long a replay lasts.
113
+ *
114
+ * Format lives here rather than on the server alone by the format-vs-policy
115
+ * rule: a client can decide offline whether a key is well-formed, and the
116
+ * API would reject the same value the same way.
117
+ */
118
+ export const IDEMPOTENCY_KEY_CONSTRAINTS = {
119
+ /**
120
+ * HTTP header name. Here for the same reason {@link CALLER.HEADER} is: a
121
+ * wire header has two ends, and the package that owns the value's format
122
+ * is the only place both ends can read its name from.
123
+ */
124
+ HEADER: 'Idempotency-Key',
125
+ MAX_LENGTH: 256,
126
+ /** How long a stored 201 stays replayable. */
127
+ WINDOW_SECONDS: 24 * 60 * 60,
128
+ };
129
+ /**
130
+ * Normalize a `via` value from any transport — trimmed, lowercased, and a
131
+ * member of {@link DeploymentVia}, or `undefined`.
132
+ *
133
+ * A format rule by this package's own test: a client can decide offline
134
+ * whether a value is well-formed, and the API reaches the same verdict on the
135
+ * same input. It lived server-side until 2026-08-06, which meant clients could
136
+ * only learn their label was unusable by noticing analytics had gone quiet.
137
+ *
138
+ * **Not knowing your `via` is not an error** — an unrecognized value yields
139
+ * `undefined` rather than throwing, because origin tracking is telemetry and a
140
+ * deploy must never fail over it. A caller that has an honest default should
141
+ * prefer it (`normalizeVia(process.env.SHIP_VIA) ?? DeploymentVia.CLI`): the
142
+ * deploy really did come from the CLI, so recording that beats recording
143
+ * nothing.
144
+ */
145
+ export function normalizeVia(value) {
146
+ if (!value || typeof value !== 'string')
147
+ return undefined;
148
+ const via = value.trim().toLowerCase();
149
+ return Object.values(DeploymentVia).includes(via)
150
+ ? via
151
+ : undefined;
152
+ }
153
+ /**
154
+ * Validate an idempotency key, returning the trimmed value or `undefined`
155
+ * when none was supplied. Throws {@link ShipError.validation} when the value
156
+ * cannot be sent — the same verdict the API would reach, reached earlier.
157
+ */
158
+ export function validateIdempotencyKey(value) {
159
+ if (value === undefined || value === null)
160
+ return undefined;
161
+ if (typeof value !== 'string') {
162
+ throw ShipError.validation('Idempotency key must be a string.');
163
+ }
164
+ const key = value.trim();
165
+ if (!key) {
166
+ throw ShipError.validation('Idempotency key must not be empty.');
167
+ }
168
+ if (key.length > IDEMPOTENCY_KEY_CONSTRAINTS.MAX_LENGTH) {
169
+ throw ShipError.validation(`Idempotency key must be at most ${IDEMPOTENCY_KEY_CONSTRAINTS.MAX_LENGTH} characters.`);
170
+ }
171
+ return key;
172
+ }
34
173
  // =============================================================================
35
174
  // ACCOUNT TYPES
36
175
  // =============================================================================
@@ -59,7 +198,15 @@ export const AccountPlan = {
59
198
  * (`DeploymentStatus`, `DomainStatus`, `AccountPlan`, `AuthMethod`) follow.
60
199
  */
61
200
  export const ErrorType = {
62
- /** Validation failed (400). Input shape is wrong. */
201
+ /**
202
+ * Validation failed. Input shape is wrong.
203
+ *
204
+ * Carries 400 when an API judged it — including a client-side pre-check of a
205
+ * rule the server enforces too, which keeps the error identical wherever it
206
+ * was caught. **Statusless** when a client rejects something no API judges,
207
+ * such as a CLI's own command grammar: `status` is documented "(API
208
+ * contexts)" on `ErrorResponse`, so there is none to report.
209
+ */
63
210
  Validation: 'validation_failed',
64
211
  /** Resource not found (404). */
65
212
  NotFound: 'not_found',
@@ -100,11 +247,26 @@ const CLIENT_ONLY_ERROR_TYPES = new Set([
100
247
  * union so `.has(error.type)` accepts any value from the union.
101
248
  */
102
249
  const ERROR_CATEGORIES = {
250
+ /**
251
+ * Client-attributable types. Exhaustive over the 4xx-carrying types, and
252
+ * over the statusless ones too — those are raised locally and have no
253
+ * status for `isClientError`'s second arm to read, so omitting one makes it
254
+ * read as a server fault. The rule is the membership test: every type in
255
+ * `CLIENT_ONLY_ERROR_TYPES` except `Network` (which `isNetworkError` owns)
256
+ * belongs here.
257
+ *
258
+ * `Cancelled` was missing until 2026-07-29, which is exactly that failure:
259
+ * a caller who aborted their own deploy was told "server error: please try
260
+ * again" — the CLI's fallback for everything this set does not claim.
261
+ */
103
262
  client: new Set([
104
263
  ErrorType.Business,
264
+ ErrorType.Cancelled,
105
265
  ErrorType.Config,
106
266
  ErrorType.File,
107
267
  ErrorType.Forbidden,
268
+ ErrorType.NotFound,
269
+ ErrorType.RateLimit,
108
270
  ErrorType.Validation,
109
271
  ]),
110
272
  network: new Set([ErrorType.Network]),
@@ -118,6 +280,50 @@ const ERROR_CATEGORIES = {
118
280
  * `ErrorType` is automatically picked up.
119
281
  */
120
282
  const SERVER_PRODUCIBLE_ERROR_TYPES = new Set(Object.values(ErrorType).filter((t) => !CLIENT_ONLY_ERROR_TYPES.has(t)));
283
+ /**
284
+ * Ceiling on a message adopted from a **non-JSON** error body — a foreign
285
+ * responder's, never this platform's. Generous for the plain-text one-liners
286
+ * intermediaries actually send (`error code: 1015`), far below a document.
287
+ * Our own messages are never measured against it: a JSON body is the API's
288
+ * contract, and truncating a long validation message would be the bug.
289
+ */
290
+ const MAX_FOREIGN_MESSAGE_LENGTH = 200;
291
+ /**
292
+ * Did the runtime say the exchange never completed?
293
+ *
294
+ * WHATWG has `fetch` reject with a **TypeError** on network error, and undici,
295
+ * Chromium and Firefox comply. Bun does not: it rejects with a plain `Error`
296
+ * carrying a system `code` string. Captured 2026-08-05 (the capture script is
297
+ * in `tests/errors.test.ts`, "runtime failure shapes"):
298
+ *
299
+ * | failure | Node 22 / undici | Bun 1.3.14 |
300
+ * |---------------|---------------------------|----------------------------------------------|
301
+ * | refused | `TypeError: fetch failed` | `Error` `code: 'ConnectionRefused'` |
302
+ * | DNS failure | `TypeError: fetch failed` | `Error` `code: 'ConnectionRefused'` |
303
+ * | reset | `TypeError: fetch failed` | `Error` `code: 'ECONNRESET'` |
304
+ * | TLS rejected | `TypeError: fetch failed` | `Error` `code: 'UNKNOWN_CERTIFICATE_…ERROR'` |
305
+ *
306
+ * So the test is the **evidence, not a list of dialect strings**: a string
307
+ * `code` is a runtime naming a transport-level failure. An allowlist of codes
308
+ * was written first and rejected — the TLS row alone would mean enumerating
309
+ * BoringSSL's certificate table, and a code nobody guessed is precisely the bug
310
+ * this closes. Two kinds of error are deliberately NOT caught: ordinary JS
311
+ * faults carry no `code` at all, and a `DOMException`'s is a **number**, so
312
+ * aborts and timeouts fall through to their own arms.
313
+ *
314
+ * The accepted trade: a caller's `TokenProvider` that throws a coded error
315
+ * (`ENOENT` from a keychain read) is typed `Network` rather than `Api`. Both
316
+ * are wrong for it, `Network` is the cheaper wrong — it says "nothing was
317
+ * exchanged", which is true, where `Api` claims a server answered.
318
+ */
319
+ function isTransportFailure(cause) {
320
+ if (typeof cause.code === 'string')
321
+ return true;
322
+ // Spec runtimes put no code on the rejection itself. The message test is what
323
+ // keeps fetch's ARGUMENT errors out — `Failed to parse URL from …` is a
324
+ // caller's config mistake, not a transport failure.
325
+ return cause instanceof TypeError && cause.message.includes('fetch');
326
+ }
121
327
  /**
122
328
  * Simple unified error class for both API and SDK
123
329
  */
@@ -189,9 +395,17 @@ export class ShipError extends Error {
189
395
  }
190
396
  }
191
397
  else {
192
- const text = await response.text();
193
- if (text)
398
+ // A non-JSON body did not come from this platform — every API error
399
+ // is `ErrorResponse` JSON — so it is an intermediary's output, and
400
+ // the two kinds it produces need opposite treatment. A CDN's plain
401
+ // `error code: 1015` is the most useful thing there is to say. A
402
+ // proxy's HTML error page is a *document*, not a message: adopting it
403
+ // verbatim made a misconfigured `apiUrl` print 2,059 characters of
404
+ // markup as the error. Trust it only when it reads as a message.
405
+ const text = (await response.text()).trim();
406
+ if (text && !text.startsWith('<') && text.length <= MAX_FOREIGN_MESSAGE_LENGTH) {
194
407
  message = text;
408
+ }
195
409
  }
196
410
  }
197
411
  catch {
@@ -235,7 +449,8 @@ export class ShipError extends Error {
235
449
  * Routing:
236
450
  * - Already a `ShipError` → returned as-is (caller's intent preserved)
237
451
  * - `AbortError` → `ShipError.cancelled(...)`
238
- * - `TypeError` whose message mentions "fetch" → `ShipError.network(...)`
452
+ * - A transport failure → `ShipError.network(...)` — see `isTransportFailure`
453
+ * for what each runtime offers as evidence
239
454
  * - Any other `Error` → `ShipError(Api, ...)` (no HTTP status — fetch never reached the server)
240
455
  * - Anything else (string, undefined, etc.) → `ShipError(Api, ...)`
241
456
  *
@@ -251,7 +466,7 @@ export class ShipError extends Error {
251
466
  if (cause.name === 'AbortError') {
252
467
  return ShipError.cancelled(`${op} was cancelled`);
253
468
  }
254
- if (cause instanceof TypeError && cause.message.includes('fetch')) {
469
+ if (isTransportFailure(cause)) {
255
470
  return ShipError.network(`${op} failed: ${cause.message}`, { cause });
256
471
  }
257
472
  return new ShipError(ErrorType.Api, `${op} failed: ${cause.message}`);
@@ -309,10 +524,24 @@ export class ShipError extends Error {
309
524
  static api(message, status = 500, details) {
310
525
  return new ShipError(ErrorType.Api, message, status, details);
311
526
  }
312
- // Semantic-category type guards. For specific-type checks, use
527
+ // Semantic-category guards. For specific-type checks, use
313
528
  // `error.type === ErrorType.X` directly or the generic `isType(t)`.
529
+ /**
530
+ * The caller is at fault — by HTTP's own definition of a 4xx, or by a type
531
+ * that is client-attributable without ever having a status (`Config`,
532
+ * `File`, raised locally by the SDK).
533
+ *
534
+ * Both arms are load-bearing, because type and status are independent
535
+ * axes. `fromHttpResponse` trusts `body.error` only when it names a
536
+ * server-producible type; a non-OK response without one is status-derived,
537
+ * so a CDN 404 or any intermediary error arrives as `Api` — a server-fault
538
+ * *type* carrying a client *status*. Judging by type alone would report it
539
+ * as a platform failure and bury the server's own message.
540
+ */
314
541
  isClientError() {
315
- return ERROR_CATEGORIES.client.has(this.type);
542
+ if (ERROR_CATEGORIES.client.has(this.type))
543
+ return true;
544
+ return this.status !== undefined && this.status >= 400 && this.status < 500;
316
545
  }
317
546
  isNetworkError() {
318
547
  return ERROR_CATEGORIES.network.has(this.type);
@@ -419,6 +648,125 @@ export function isBlockedExtension(filename) {
419
648
  return BLOCKED_EXTENSIONS.has(ext);
420
649
  }
421
650
  // =============================================================================
651
+ // PICKER ACCEPT HINT
652
+ // =============================================================================
653
+ /**
654
+ * The extensions a browser file picker offers by default, grouped by role.
655
+ *
656
+ * Private on purpose: the only published form is `WEB_FILE_ACCEPT`, the
657
+ * attribute value itself. A published set would invite a call site to ask it
658
+ * whether a file is allowed — which is the one thing this list must never
659
+ * answer. See `WEB_FILE_ACCEPT`.
660
+ *
661
+ * Extensionless files (`LICENSE`, most `.well-known` entries) are inexpressible
662
+ * in `accept`, and reach a deployment by folder pick, ZIP, or drag-and-drop.
663
+ */
664
+ const WEB_FILE_EXTENSIONS = [
665
+ // Markup & documents
666
+ 'html',
667
+ 'htm',
668
+ 'xhtml',
669
+ 'xml',
670
+ 'txt',
671
+ 'md',
672
+ 'markdown',
673
+ 'pdf',
674
+ 'csv',
675
+ // Data & config
676
+ 'json',
677
+ 'jsonc',
678
+ 'webmanifest',
679
+ 'map',
680
+ 'toml',
681
+ 'yaml',
682
+ 'yml',
683
+ 'rss',
684
+ 'atom',
685
+ // Styles
686
+ 'css',
687
+ 'scss',
688
+ 'sass',
689
+ 'less',
690
+ // Scripts & modules
691
+ 'js',
692
+ 'mjs',
693
+ 'cjs',
694
+ 'jsx',
695
+ 'ts',
696
+ 'tsx',
697
+ 'wasm',
698
+ 'vue',
699
+ 'svelte',
700
+ // Images
701
+ 'png',
702
+ 'jpg',
703
+ 'jpeg',
704
+ 'gif',
705
+ 'webp',
706
+ 'avif',
707
+ 'svg',
708
+ 'ico',
709
+ 'bmp',
710
+ 'tif',
711
+ 'tiff',
712
+ 'heic',
713
+ 'heif',
714
+ // Fonts
715
+ 'woff',
716
+ 'woff2',
717
+ 'ttf',
718
+ 'otf',
719
+ 'eot',
720
+ // Audio
721
+ 'mp3',
722
+ 'wav',
723
+ 'ogg',
724
+ 'oga',
725
+ 'opus',
726
+ 'm4a',
727
+ 'aac',
728
+ 'flac',
729
+ 'weba',
730
+ // Video
731
+ 'mp4',
732
+ 'webm',
733
+ 'ogv',
734
+ 'mov',
735
+ 'm4v',
736
+ 'avi',
737
+ // 3D models
738
+ 'glb',
739
+ 'gltf',
740
+ 'usdz',
741
+ // Text tracks
742
+ 'vtt',
743
+ 'srt',
744
+ // Archive — a whole site in one file
745
+ 'zip',
746
+ ];
747
+ /**
748
+ * The `accept` attribute value for a browser file picker offering web files.
749
+ *
750
+ * **This is a hint, never a rule.** `BLOCKED_EXTENSIONS` is the platform's
751
+ * gate and the only thing that decides what may be hosted; this constant
752
+ * decides what a *file dialog* shows first. The two are not two halves of one
753
+ * policy, and this one must never be consulted to accept or reject a file.
754
+ *
755
+ * The distinction is structural, not stylistic. `accept` can express only an
756
+ * allowlist, while the platform's rule is a blocklist — so this list is
757
+ * necessarily *narrower* than what the platform hosts, and reading it as
758
+ * authority would reject files the platform serves happily. It is also not
759
+ * enforcement in the browser's own terms: every file dialog offers an
760
+ * all-files escape, and **drag-and-drop ignores `accept` entirely**. The
761
+ * dropzone and the picker must reach the same verdict on the same files, and
762
+ * they do — because the verdict is `validateFiles`, downstream of both.
763
+ *
764
+ * Kept beside `BLOCKED_EXTENSIONS` so one file holds both, which is what lets
765
+ * `tests/validation-constants.test.ts` fence the invariant that matters: the
766
+ * picker must never offer a file the platform will refuse.
767
+ */
768
+ export const WEB_FILE_ACCEPT = WEB_FILE_EXTENSIONS.map((ext) => `.${ext}`).join(',');
769
+ // =============================================================================
422
770
  // FILENAME CHARACTER VALIDATION
423
771
  // =============================================================================
424
772
  /**
@@ -600,6 +948,52 @@ export const DEPLOYMENT_CONFIG_FILENAME = 'ship.json';
600
948
  export const SPA_DEFAULT_CONFIG = {
601
949
  rewrites: [{ source: '/(.*)', destination: '/index.html' }],
602
950
  };
951
+ /**
952
+ * Assert that a ship.json file is *syntactically* loadable. Syntax only —
953
+ * never schema.
954
+ *
955
+ * ship.json is validated and compiled on the server, deliberately: the schema
956
+ * and the compiler evolve, and a client that judged them would reject configs
957
+ * a newer platform accepts. That reasoning bounds what a client may check to
958
+ * the properties which are true of *every* past and future schema:
959
+ *
960
+ * 1. it parses as JSON — JSON syntax is frozen (RFC 8259), so text that
961
+ * does not parse can never be a valid config;
962
+ * 2. its top level is an object — ship.json is `{ ... }` in every version.
963
+ *
964
+ * Both are monotonic: neither can ever reject something the server would
965
+ * accept. Everything beyond them (field names, types, rule semantics, which
966
+ * keys are permitted) stays server-side, where it can change.
967
+ *
968
+ * The payoff is the common case. Hand-edited JSON fails on a trailing comma,
969
+ * a `//` comment, single quotes, unquoted keys, or smart quotes pasted from
970
+ * documentation — mistakes that otherwise cost a full upload round-trip to
971
+ * discover. A UTF-8 BOM (Windows editors, PowerShell redirects) is stripped
972
+ * before parsing rather than rejected, because the server accepts it too;
973
+ * diverging there would reintroduce exactly the false rejection this
974
+ * function exists to avoid.
975
+ *
976
+ * @throws {ShipError} `ErrorType.Config` — the same type the server's own
977
+ * config rejection carries, so the error contract is identical wherever the
978
+ * failure is detected.
979
+ */
980
+ export function assertShipJsonSyntax(text) {
981
+ const withoutBom = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
982
+ let parsed;
983
+ try {
984
+ parsed = JSON.parse(withoutBom);
985
+ }
986
+ catch (error) {
987
+ throw ShipError.config(`invalid JSON format in config: ${error.message}`, {
988
+ filePath: DEPLOYMENT_CONFIG_FILENAME,
989
+ });
990
+ }
991
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
992
+ throw ShipError.config(`${DEPLOYMENT_CONFIG_FILENAME} must contain a JSON object`, {
993
+ filePath: DEPLOYMENT_CONFIG_FILENAME,
994
+ });
995
+ }
996
+ }
603
997
  // =============================================================================
604
998
  // VALIDATION UTILITIES
605
999
  // =============================================================================
@@ -696,6 +1090,21 @@ export function isDeployment(input) {
696
1090
  // =============================================================================
697
1091
  /** Default API URL if not otherwise configured. */
698
1092
  export const DEFAULT_API = 'https://api.shipstatic.com';
1093
+ /**
1094
+ * How long an anonymous deployment lives before it expires.
1095
+ *
1096
+ * The lifetime of the public tier, and one fact with several readers. The API
1097
+ * stamps a deployment's `expires` from it and gives a claim code exactly the
1098
+ * same window — a live site with a dead claim link is a coherence bug, so the
1099
+ * two are one constant rather than two that agree. Both MCP transports quote
1100
+ * the duration in prose an agent reads, and derive it from here rather than
1101
+ * writing it out, which they did in eight places until this export existed.
1102
+ *
1103
+ * Seconds, spelled in the name: this platform has both second- and
1104
+ * millisecond-valued durations, and the pair is only safe when each says which
1105
+ * it is.
1106
+ */
1107
+ export const PUBLIC_DEPLOYMENT_TTL_SECONDS = 3 * 24 * 60 * 60;
699
1108
  // =============================================================================
700
1109
  // FILE UPLOAD TYPES
701
1110
  // =============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/types",
3
- "version": "2.5.0-beta.2",
3
+ "version": "2.5.0-beta.20",
4
4
  "description": "Shared types for ShipStatic platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,7 +18,7 @@
18
18
  "lint": "biome check .",
19
19
  "format": "biome format --write .",
20
20
  "prepare": "git config core.hooksPath scripts/githooks",
21
- "typecheck": "tsc --noEmit"
21
+ "typecheck": "tsc -p tsconfig.check.json --noEmit"
22
22
  },
23
23
  "packageManager": "pnpm@10.12.4",
24
24
  "files": [
@@ -44,8 +44,8 @@
44
44
  },
45
45
  "devDependencies": {
46
46
  "@biomejs/biome": "2.5.5",
47
- "@types/node": "^24.10.9",
47
+ "@types/node": "^24.13.3",
48
48
  "typescript": "^5.9.3",
49
- "vitest": "^2.1.8"
49
+ "vitest": "^2.1.9"
50
50
  }
51
51
  }