@shipstatic/types 2.1.0 → 2.2.1-beta.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
@@ -11,7 +11,7 @@ export declare const DeploymentStatus: {
11
11
  readonly FAILED: "failed";
12
12
  readonly DELETING: "deleting";
13
13
  };
14
- export type DeploymentStatusType = typeof DeploymentStatus[keyof typeof DeploymentStatus];
14
+ export type DeploymentStatusType = (typeof DeploymentStatus)[keyof typeof DeploymentStatus];
15
15
  /**
16
16
  * Core deployment object - used in both API responses and SDK
17
17
  */
@@ -74,7 +74,7 @@ export declare const DomainStatus: {
74
74
  readonly SUCCESS: "success";
75
75
  readonly PAUSED: "paused";
76
76
  };
77
- export type DomainStatusType = typeof DomainStatus[keyof typeof DomainStatus];
77
+ export type DomainStatusType = (typeof DomainStatus)[keyof typeof DomainStatus];
78
78
  /**
79
79
  * Core domain object - used in both API responses and SDK
80
80
  */
@@ -178,30 +178,10 @@ export interface DomainValidateResponse {
178
178
  /** Error message, null when valid */
179
179
  error: string | null;
180
180
  }
181
- /**
182
- * Deployment token for automated deployments
183
- */
184
- export interface Token {
185
- /** 7-char management identifier */
186
- readonly token: string;
187
- /** The account this token belongs to */
188
- readonly account: string;
189
- /** SHA256 hash of the raw credential (auth lookups only, never exposed to users) */
190
- readonly hash: string;
191
- /** IP-lock. Non-null = single-use (claimable only from this IP, once); null = multi-use. */
192
- readonly ip: string | null;
193
- /** Labels for categorization and filtering (lowercase, alphanumeric with separators). Always present, empty array when none. */
194
- labels: string[];
195
- /** Unix timestamp (seconds) when token was created */
196
- readonly created: number;
197
- /** Unix timestamp (seconds) when token expires, null for never */
198
- readonly expires: number | null;
199
- /** Consumption timestamp. Single-use: gates re-use (set once). Multi-use: refreshed on every claim. Null = never claimed. */
200
- readonly used: number | null;
201
- }
202
181
  /**
203
182
  * Token as returned by the list endpoint.
204
- * Shows 7-char management ID, omits account and hash.
183
+ * The secret is shown once at creation and never again — listings carry
184
+ * only the management identifier and lifecycle metadata.
205
185
  */
206
186
  export interface TokenListItem {
207
187
  /** 7-char management identifier (e.g., "a1b2c3d") */
@@ -212,7 +192,7 @@ export interface TokenListItem {
212
192
  readonly created: number;
213
193
  /** Unix timestamp (seconds) when token expires, null for never */
214
194
  readonly expires: number | null;
215
- /** Consumption timestamp. Single-use: gates re-use (set once). Multi-use: refreshed on every claim. Null = never claimed. */
195
+ /** Unix timestamp (seconds) of the last request authenticated with this token, null if never used */
216
196
  readonly used: number | null;
217
197
  }
218
198
  /**
@@ -249,7 +229,7 @@ export declare const AccountPlan: {
249
229
  readonly TERMINATING: "terminating";
250
230
  readonly TERMINATED: "terminated";
251
231
  };
252
- export type AccountPlanType = typeof AccountPlan[keyof typeof AccountPlan];
232
+ export type AccountPlanType = (typeof AccountPlan)[keyof typeof AccountPlan];
253
233
  /**
254
234
  * Account usage metrics — always available regardless of billing provider.
255
235
  */
@@ -281,6 +261,20 @@ export interface Account {
281
261
  /** Grace period expiration (unix seconds), null if no grace period active */
282
262
  readonly grace: number | null;
283
263
  }
264
+ /**
265
+ * Account as returned by `GET /account` — the entity plus how the request
266
+ * was authorized, so `whoami` can answer "what credential am I holding?".
267
+ * Request-scoped fields live on the response type, never on the entity
268
+ * (the `DeploymentCreateResponse` pattern).
269
+ */
270
+ export interface AccountGetResponse extends Account {
271
+ /** How the request that produced this response was authorized. */
272
+ readonly authMethod: AuthMethodType;
273
+ /** Present (and true) only when the caller is an operator acting as themselves. */
274
+ readonly isAdmin?: true;
275
+ /** Present only during read-only admin impersonation: the operator's account id. */
276
+ readonly impersonatedBy?: string;
277
+ }
284
278
  /**
285
279
  * Account-specific configuration overrides
286
280
  * Allows per-account customization of limits without changing plan
@@ -330,7 +324,7 @@ export declare const ErrorType: {
330
324
  /** Configuration error. Client-side only — set by SDK during config parsing/validation; never produced server-side. */
331
325
  readonly Config: "config_error";
332
326
  };
333
- export type ErrorType = typeof ErrorType[keyof typeof ErrorType];
327
+ export type ErrorType = (typeof ErrorType)[keyof typeof ErrorType];
334
328
  /**
335
329
  * Standard error response format used everywhere
336
330
  */
@@ -438,15 +432,14 @@ export declare class ShipError extends Error {
438
432
  */
439
433
  export declare function isShipError(error: unknown): error is ShipError;
440
434
  /**
441
- * Plan-based platform limits returned by the `/config` endpoint.
435
+ * Plan-based platform limits returned by the `/limits` endpoint.
442
436
  *
443
437
  * The SDK fetches these once on first API call to drive client-side
444
438
  * file-size / file-count / total-size validation that mirrors what the API
445
439
  * would enforce server-side. Limits vary by account plan.
446
440
  *
447
- * Distinct from `ResolvedConfig` (which carries the *client's* credentials
448
- * and API URL after defaulting); this one carries the *platform's* posted
449
- * caps for the current account.
441
+ * These are the *platform's* posted caps for the current account — server
442
+ * truth delivered at runtime, never hard-coded on the client.
450
443
  */
451
444
  export interface PlatformLimits {
452
445
  /** Maximum size in bytes for a single file. */
@@ -525,6 +518,25 @@ export interface PingResponse {
525
518
  /** Optional timestamp */
526
519
  timestamp?: number;
527
520
  }
521
+ /**
522
+ * How a request (or recorded activity) was authorized.
523
+ *
524
+ * Client populations: `SESSION` (first-party cookie), `API_KEY` (`ship-`
525
+ * key), `TOKEN` (`deploy-` deploy token), `AGENT` (anonymous public deploy —
526
+ * no credential; the platform grants the public-account identity per
527
+ * request), `OAUTH` (delegated access token). Server populations: `WEBHOOK`
528
+ * (signed webhook processing), `SYSTEM` (scheduled/background jobs).
529
+ */
530
+ export declare const AuthMethod: {
531
+ readonly SESSION: "session";
532
+ readonly API_KEY: "apiKey";
533
+ readonly TOKEN: "token";
534
+ readonly AGENT: "agent";
535
+ readonly OAUTH: "oauth";
536
+ readonly WEBHOOK: "webhook";
537
+ readonly SYSTEM: "system";
538
+ };
539
+ export type AuthMethodType = (typeof AuthMethod)[keyof typeof AuthMethod];
528
540
  /**
529
541
  * Shape constants for API keys (`ship-{64 hex chars}`).
530
542
  * Single source of truth used by validation utilities and auth middleware.
@@ -540,26 +552,57 @@ export declare const API_KEY: {
540
552
  readonly HINT_LENGTH: 4;
541
553
  };
542
554
  /**
543
- * Shape constants for deploy tokens (`token-{64 hex chars}`).
555
+ * Shape constants for deploy tokens (`deploy-{64 hex chars}`).
544
556
  * Single source of truth used by validation utilities and auth middleware.
545
557
  */
546
558
  export declare const DEPLOY_TOKEN: {
547
559
  /** Prefix that identifies a deploy token. */
548
- readonly PREFIX: "token-";
560
+ readonly PREFIX: "deploy-";
549
561
  /** Number of hex characters following the prefix. */
550
562
  readonly HEX_LENGTH: 64;
551
- /** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 70`). */
552
- readonly TOTAL_LENGTH: 70;
563
+ /** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 71`). */
564
+ readonly TOTAL_LENGTH: 71;
553
565
  };
554
- export declare const AuthMethod: {
555
- readonly SESSION: "session";
566
+ /**
567
+ * Shape constants for caller identifiers (the `X-Caller` instance-identity
568
+ * header — rate-limit bucketing for multi-tenant orchestrators). The API
569
+ * normalizes case and silently ignores malformed values (the header is
570
+ * unauthenticated); clients validate at the boundary via `validateCaller`,
571
+ * so a value the server would drop fails fast instead.
572
+ */
573
+ export declare const CALLER: {
574
+ /** HTTP header name. */
575
+ readonly HEADER: "X-Caller";
576
+ /** Maximum identifier length. */
577
+ readonly MAX_LENGTH: 128;
578
+ /** Allowed characters: alphanumeric, dot, underscore, hyphen. */
579
+ readonly PATTERN: RegExp;
580
+ };
581
+ /**
582
+ * Token populations distinguishable by shape. The platform carries every
583
+ * client token in one wire slot (`Authorization: Bearer <value>`) and
584
+ * classifies by value, never by a side channel — this is the classifier.
585
+ *
586
+ * `API_KEY` and `DEPLOY_TOKEN` *are* `AuthMethod.API_KEY` and
587
+ * `AuthMethod.TOKEN` — the equality is structural, so a classification flows
588
+ * straight into an auth method and the pair can never drift. `OPAQUE` is any
589
+ * other value — shape says nothing about it, so only a lookup can. Today the
590
+ * server refuses every opaque bearer; the OAuth access-token population
591
+ * resolves there when the authorization server ships.
592
+ */
593
+ export declare const TokenKind: {
556
594
  readonly API_KEY: "apiKey";
557
- readonly TOKEN: "token";
558
- readonly OAUTH: "oauth";
559
- readonly WEBHOOK: "webhook";
560
- readonly SYSTEM: "system";
595
+ readonly DEPLOY_TOKEN: "token";
596
+ readonly OPAQUE: "opaque";
561
597
  };
562
- export type AuthMethodType = typeof AuthMethod[keyof typeof AuthMethod];
598
+ export type TokenKindType = (typeof TokenKind)[keyof typeof TokenKind];
599
+ /**
600
+ * Classify a client token by shape. The single dispatch used by both sides
601
+ * of the wire: API auth middleware (which population is this credential?)
602
+ * and SDK validation (which format rules apply before sending?). Sharing it
603
+ * is what guarantees client and server can never disagree on dispatch.
604
+ */
605
+ export declare function classifyToken(token: string): TokenKindType;
563
606
  /**
564
607
  * OAuth scope vocabulary for delegated third-party access tokens.
565
608
  * Single source of truth used by the authorization server (advertised in
@@ -578,7 +621,7 @@ export declare const OAuthScope: {
578
621
  readonly DOMAINS_READ: "domains:read";
579
622
  readonly DOMAINS_WRITE: "domains:write";
580
623
  };
581
- export type OAuthScopeType = typeof OAuthScope[keyof typeof OAuthScope];
624
+ export type OAuthScopeType = (typeof OAuthScope)[keyof typeof OAuthScope];
582
625
  export declare const DEPLOYMENT_CONFIG_FILENAME = "ship.json";
583
626
  /** Default ship.json config for SPA routing. Single source of truth — used by both API and SDK. */
584
627
  export declare const SPA_DEFAULT_CONFIG: {
@@ -595,6 +638,19 @@ export declare function validateApiKey(apiKey: string): void;
595
638
  * Validate deploy token format
596
639
  */
597
640
  export declare function validateDeployToken(deployToken: string): void;
641
+ /**
642
+ * Validate a client token of any population. Classifies by shape and applies
643
+ * the matching format rules: `ship-` keys and `deploy-` deploy tokens are
644
+ * validated strictly; opaque tokens (OAuth access tokens, future populations)
645
+ * only need to be non-empty — their validity is the server's to decide.
646
+ */
647
+ export declare function validateToken(token: string): void;
648
+ /**
649
+ * Validate a caller identifier against the `CALLER` shape. The server
650
+ * silently ignores malformed values (the header is unauthenticated); clients
651
+ * call this at configuration time so the drop never silently happens.
652
+ */
653
+ export declare function validateCaller(caller: string): void;
598
654
  /**
599
655
  * Validate API URL format
600
656
  */
@@ -656,24 +712,6 @@ export interface StaticFile {
656
712
  /** The size of the file in bytes. */
657
713
  size: number;
658
714
  }
659
- /**
660
- * Resolved client configuration with `apiUrl` defaulted.
661
- *
662
- * Produced by the SDK after layering its credential sources (constructor
663
- * options on top of `SHIP_*` env vars in Node; constructor options only in
664
- * Browser) and applying the `DEFAULT_API` fallback. File-based sources
665
- * (`.shiprc`, `package.json` `"ship"` key) are the CLI's responsibility and
666
- * are merged in *before* construction — by the time a `ResolvedConfig`
667
- * exists, every source has already collapsed into the constructor argument.
668
- */
669
- export interface ResolvedConfig {
670
- /** API URL — always present after resolution, defaults to `DEFAULT_API`. */
671
- apiUrl: string;
672
- /** API key for authenticated deployments. */
673
- apiKey?: string;
674
- /** Deploy token used as the request credential (`token-{64-hex}`). */
675
- deployToken?: string;
676
- }
677
715
  /**
678
716
  * Progress information for deploy/upload operations.
679
717
  * Provides consistent percentage-based progress with byte-level details.
@@ -727,6 +765,8 @@ export interface DeploymentUploadOptions {
727
765
  prerender?: boolean;
728
766
  /** @internal Trigger server-side SPA detection. Only available via /upload endpoint. */
729
767
  spa?: boolean;
768
+ /** @internal reCAPTCHA proof for the anonymous human deploy channel. Only available via /upload endpoint. */
769
+ captcha?: string;
730
770
  }
731
771
  /**
732
772
  * Deployment resource interface - the contract all implementations must follow
@@ -766,7 +806,7 @@ export interface DomainResource {
766
806
  * Account resource interface - the contract all implementations must follow
767
807
  */
768
808
  export interface AccountResource {
769
- get: () => Promise<Account>;
809
+ get: () => Promise<AccountGetResponse>;
770
810
  }
771
811
  /**
772
812
  * Token resource interface - the contract all implementations must follow
@@ -884,7 +924,7 @@ export declare const FileValidationStatus: {
884
924
  /** File passed validation and is ready for deployment */
885
925
  readonly READY: "ready";
886
926
  };
887
- export type FileValidationStatusType = typeof FileValidationStatus[keyof typeof FileValidationStatus];
927
+ export type FileValidationStatusType = (typeof FileValidationStatus)[keyof typeof FileValidationStatus];
888
928
  /**
889
929
  * A validation issue with a display-ready message
890
930
  *
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ export const DeploymentStatus = {
12
12
  PENDING: 'pending',
13
13
  SUCCESS: 'success',
14
14
  FAILED: 'failed',
15
- DELETING: 'deleting'
15
+ DELETING: 'deleting',
16
16
  };
17
17
  // =============================================================================
18
18
  // DOMAIN TYPES
@@ -29,7 +29,7 @@ export const DomainStatus = {
29
29
  PENDING: 'pending',
30
30
  PARTIAL: 'partial',
31
31
  SUCCESS: 'success',
32
- PAUSED: 'paused'
32
+ PAUSED: 'paused',
33
33
  };
34
34
  // =============================================================================
35
35
  // ACCOUNT TYPES
@@ -44,7 +44,7 @@ export const AccountPlan = {
44
44
  ENTERPRISE: 'enterprise',
45
45
  SUSPENDED: 'suspended',
46
46
  TERMINATING: 'terminating',
47
- TERMINATED: 'terminated'
47
+ TERMINATED: 'terminated',
48
48
  };
49
49
  // =============================================================================
50
50
  // ERROR SYSTEM
@@ -100,7 +100,13 @@ const CLIENT_ONLY_ERROR_TYPES = new Set([
100
100
  * union so `.has(error.type)` accepts any value from the union.
101
101
  */
102
102
  const ERROR_CATEGORIES = {
103
- client: new Set([ErrorType.Business, ErrorType.Config, ErrorType.File, ErrorType.Forbidden, ErrorType.Validation]),
103
+ client: new Set([
104
+ ErrorType.Business,
105
+ ErrorType.Config,
106
+ ErrorType.File,
107
+ ErrorType.Forbidden,
108
+ ErrorType.Validation,
109
+ ]),
104
110
  network: new Set([ErrorType.Network]),
105
111
  auth: new Set([ErrorType.Authentication]),
106
112
  };
@@ -111,7 +117,7 @@ const ERROR_CATEGORIES = {
111
117
  * `CLIENT_ONLY_ERROR_TYPES` so adding a new server-producible type to
112
118
  * `ErrorType` is automatically picked up.
113
119
  */
114
- const SERVER_PRODUCIBLE_ERROR_TYPES = new Set(Object.values(ErrorType).filter(t => !CLIENT_ONLY_ERROR_TYPES.has(t)));
120
+ const SERVER_PRODUCIBLE_ERROR_TYPES = new Set(Object.values(ErrorType).filter((t) => !CLIENT_ONLY_ERROR_TYPES.has(t)));
115
121
  /**
116
122
  * Simple unified error class for both API and SDK
117
123
  */
@@ -132,14 +138,12 @@ export class ShipError extends Error {
132
138
  // tag (see `ShipError.authentication` JSDoc) — these are server-side
133
139
  // diagnostics like 'session_invalid' that must not leak to clients.
134
140
  const authDetails = this.details;
135
- const details = this.type === ErrorType.Authentication && authDetails?.internal
136
- ? undefined
137
- : this.details;
141
+ const details = this.type === ErrorType.Authentication && authDetails?.internal ? undefined : this.details;
138
142
  return {
139
143
  error: this.type,
140
144
  message: this.message,
141
145
  status: this.status,
142
- details
146
+ details,
143
147
  };
144
148
  }
145
149
  /**
@@ -194,10 +198,14 @@ export class ShipError extends Error {
194
198
  // Body unreadable; fall through to operationName-derived message.
195
199
  }
196
200
  message = message || `${operationName || 'Request'} failed with status ${response.status}`;
197
- const type = bodyType ?? (response.status === 401 ? ErrorType.Authentication :
198
- response.status === 403 ? ErrorType.Forbidden :
199
- response.status === 429 ? ErrorType.RateLimit :
200
- ErrorType.Api);
201
+ const type = bodyType ??
202
+ (response.status === 401
203
+ ? ErrorType.Authentication
204
+ : response.status === 403
205
+ ? ErrorType.Forbidden
206
+ : response.status === 429
207
+ ? ErrorType.RateLimit
208
+ : ErrorType.Api);
201
209
  return new ShipError(type, message, response.status, details);
202
210
  }
203
211
  /**
@@ -247,7 +255,7 @@ export class ShipError extends Error {
247
255
  static forbidden(message, details) {
248
256
  return new ShipError(ErrorType.Forbidden, message, 403, details);
249
257
  }
250
- static rateLimit(message = "Too many requests", details) {
258
+ static rateLimit(message = 'Too many requests', details) {
251
259
  return new ShipError(ErrorType.RateLimit, message, 429, details);
252
260
  }
253
261
  /**
@@ -263,7 +271,7 @@ export class ShipError extends Error {
263
271
  * Use this pattern in API auth code; do not put client-visible info under
264
272
  * `internal`. Other `details` keys round-trip normally.
265
273
  */
266
- static authentication(message = "Authentication required", details) {
274
+ static authentication(message = 'Authentication required', details) {
267
275
  return new ShipError(ErrorType.Authentication, message, 401, details);
268
276
  }
269
277
  static business(message, status = 400, details) {
@@ -332,21 +340,47 @@ export function isShipError(error) {
332
340
  */
333
341
  export const BLOCKED_EXTENSIONS = new Set([
334
342
  // Executables
335
- 'exe', 'msi', 'dll', 'scr', 'bat', 'cmd', 'com', 'pif', 'app', 'deb', 'rpm',
343
+ 'exe',
344
+ 'msi',
345
+ 'dll',
346
+ 'scr',
347
+ 'bat',
348
+ 'cmd',
349
+ 'com',
350
+ 'pif',
351
+ 'app',
352
+ 'deb',
353
+ 'rpm',
336
354
  // Installers
337
- 'pkg', 'mpkg',
355
+ 'pkg',
356
+ 'mpkg',
338
357
  // Disk images
339
- 'dmg', 'iso', 'img',
358
+ 'dmg',
359
+ 'iso',
360
+ 'img',
340
361
  // Malware vectors
341
- 'cab', 'cpl', 'chm',
362
+ 'cab',
363
+ 'cpl',
364
+ 'chm',
342
365
  // Dangerous scripts
343
- 'ps1', 'vbs', 'vbe', 'ws', 'wsf', 'wsc', 'wsh', 'reg',
366
+ 'ps1',
367
+ 'vbs',
368
+ 'vbe',
369
+ 'ws',
370
+ 'wsf',
371
+ 'wsc',
372
+ 'wsh',
373
+ 'reg',
344
374
  // Java
345
- 'jar', 'jnlp',
375
+ 'jar',
376
+ 'jnlp',
346
377
  // Mobile/browser packages
347
- 'apk', 'crx',
378
+ 'apk',
379
+ 'crx',
348
380
  // Shortcut/link
349
- 'lnk', 'inf', 'hta',
381
+ 'lnk',
382
+ 'inf',
383
+ 'hta',
350
384
  ]);
351
385
  /**
352
386
  * Check if a filename has a blocked extension.
@@ -381,6 +415,7 @@ export function isBlockedExtension(filename) {
381
415
  *
382
416
  * Everything else is allowed — browser percent-encodes, Worker decodes, R2 matches.
383
417
  */
418
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: blocking control characters is this regex's purpose
384
419
  export const UNSAFE_FILENAME_CHARS = /[\x00-\x1f\x7f#?%\\<>"]/;
385
420
  /**
386
421
  * Check if a filename contains unsafe characters.
@@ -415,8 +450,33 @@ export const UNBUILT_PROJECT_MARKERS = new Set([
415
450
  */
416
451
  export function hasUnbuiltMarker(filePath) {
417
452
  const segments = filePath.replace(/\\/g, '/').split('/').filter(Boolean);
418
- return segments.some(s => UNBUILT_PROJECT_MARKERS.has(s));
453
+ return segments.some((s) => UNBUILT_PROJECT_MARKERS.has(s));
419
454
  }
455
+ // =============================================================================
456
+ // CREDENTIAL SHAPES
457
+ // =============================================================================
458
+ // The one address for credential vocabulary: how a request is authorized
459
+ // (AuthMethod), the shapes that distinguish populations on the wire
460
+ // (API_KEY, DEPLOY_TOKEN, CALLER), the single dispatch over them (TokenKind,
461
+ // classifyToken), and the delegated-access scopes (OAuthScope).
462
+ /**
463
+ * How a request (or recorded activity) was authorized.
464
+ *
465
+ * Client populations: `SESSION` (first-party cookie), `API_KEY` (`ship-`
466
+ * key), `TOKEN` (`deploy-` deploy token), `AGENT` (anonymous public deploy —
467
+ * no credential; the platform grants the public-account identity per
468
+ * request), `OAUTH` (delegated access token). Server populations: `WEBHOOK`
469
+ * (signed webhook processing), `SYSTEM` (scheduled/background jobs).
470
+ */
471
+ export const AuthMethod = {
472
+ SESSION: 'session',
473
+ API_KEY: 'apiKey',
474
+ TOKEN: 'token',
475
+ AGENT: 'agent',
476
+ OAUTH: 'oauth',
477
+ WEBHOOK: 'webhook',
478
+ SYSTEM: 'system',
479
+ };
420
480
  /**
421
481
  * Shape constants for API keys (`ship-{64 hex chars}`).
422
482
  * Single source of truth used by validation utilities and auth middleware.
@@ -432,26 +492,62 @@ export const API_KEY = {
432
492
  HINT_LENGTH: 4,
433
493
  };
434
494
  /**
435
- * Shape constants for deploy tokens (`token-{64 hex chars}`).
495
+ * Shape constants for deploy tokens (`deploy-{64 hex chars}`).
436
496
  * Single source of truth used by validation utilities and auth middleware.
437
497
  */
438
498
  export const DEPLOY_TOKEN = {
439
499
  /** Prefix that identifies a deploy token. */
440
- PREFIX: 'token-',
500
+ PREFIX: 'deploy-',
441
501
  /** Number of hex characters following the prefix. */
442
502
  HEX_LENGTH: 64,
443
- /** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 70`). */
444
- TOTAL_LENGTH: 70,
503
+ /** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 71`). */
504
+ TOTAL_LENGTH: 71,
445
505
  };
446
- // Authentication Method Constants
447
- export const AuthMethod = {
448
- SESSION: 'session',
449
- API_KEY: 'apiKey',
450
- TOKEN: 'token',
451
- OAUTH: 'oauth',
452
- WEBHOOK: 'webhook',
453
- SYSTEM: 'system'
506
+ /**
507
+ * Shape constants for caller identifiers (the `X-Caller` instance-identity
508
+ * header — rate-limit bucketing for multi-tenant orchestrators). The API
509
+ * normalizes case and silently ignores malformed values (the header is
510
+ * unauthenticated); clients validate at the boundary via `validateCaller`,
511
+ * so a value the server would drop fails fast instead.
512
+ */
513
+ export const CALLER = {
514
+ /** HTTP header name. */
515
+ HEADER: 'X-Caller',
516
+ /** Maximum identifier length. */
517
+ MAX_LENGTH: 128,
518
+ /** Allowed characters: alphanumeric, dot, underscore, hyphen. */
519
+ PATTERN: /^[a-zA-Z0-9._-]+$/,
454
520
  };
521
+ /**
522
+ * Token populations distinguishable by shape. The platform carries every
523
+ * client token in one wire slot (`Authorization: Bearer <value>`) and
524
+ * classifies by value, never by a side channel — this is the classifier.
525
+ *
526
+ * `API_KEY` and `DEPLOY_TOKEN` *are* `AuthMethod.API_KEY` and
527
+ * `AuthMethod.TOKEN` — the equality is structural, so a classification flows
528
+ * straight into an auth method and the pair can never drift. `OPAQUE` is any
529
+ * other value — shape says nothing about it, so only a lookup can. Today the
530
+ * server refuses every opaque bearer; the OAuth access-token population
531
+ * resolves there when the authorization server ships.
532
+ */
533
+ export const TokenKind = {
534
+ API_KEY: AuthMethod.API_KEY,
535
+ DEPLOY_TOKEN: AuthMethod.TOKEN,
536
+ OPAQUE: 'opaque',
537
+ };
538
+ /**
539
+ * Classify a client token by shape. The single dispatch used by both sides
540
+ * of the wire: API auth middleware (which population is this credential?)
541
+ * and SDK validation (which format rules apply before sending?). Sharing it
542
+ * is what guarantees client and server can never disagree on dispatch.
543
+ */
544
+ export function classifyToken(token) {
545
+ if (token.startsWith(API_KEY.PREFIX))
546
+ return TokenKind.API_KEY;
547
+ if (token.startsWith(DEPLOY_TOKEN.PREFIX))
548
+ return TokenKind.DEPLOY_TOKEN;
549
+ return TokenKind.OPAQUE;
550
+ }
455
551
  /**
456
552
  * OAuth scope vocabulary for delegated third-party access tokens.
457
553
  * Single source of truth used by the authorization server (advertised in
@@ -470,41 +566,73 @@ export const OAuthScope = {
470
566
  DOMAINS_READ: 'domains:read',
471
567
  DOMAINS_WRITE: 'domains:write',
472
568
  };
473
- // Deployment Configuration
569
+ // =============================================================================
570
+ // DEPLOYMENT CONFIGURATION CONSTANTS
571
+ // =============================================================================
474
572
  export const DEPLOYMENT_CONFIG_FILENAME = 'ship.json';
475
573
  /** Default ship.json config for SPA routing. Single source of truth — used by both API and SDK. */
476
- export const SPA_DEFAULT_CONFIG = { rewrites: [{ source: '/(.*)', destination: '/index.html' }] };
574
+ export const SPA_DEFAULT_CONFIG = {
575
+ rewrites: [{ source: '/(.*)', destination: '/index.html' }],
576
+ };
477
577
  // =============================================================================
478
578
  // VALIDATION UTILITIES
479
579
  // =============================================================================
480
580
  /**
481
- * Validate API key format
581
+ * Shared rule for prefixed credentials: `{PREFIX}{HEX_LENGTH hex chars}`.
582
+ * The regex derives from the shape constants, so the validators can never
583
+ * drift from the shapes `classifyToken` dispatches on.
482
584
  */
483
- export function validateApiKey(apiKey) {
484
- if (!apiKey.startsWith(API_KEY.PREFIX)) {
485
- throw ShipError.validation(`API key must start with "${API_KEY.PREFIX}"`);
585
+ function validatePrefixedCredential(value, shape, label) {
586
+ if (!value.startsWith(shape.PREFIX)) {
587
+ throw ShipError.validation(`${label} must start with "${shape.PREFIX}"`);
486
588
  }
487
- if (apiKey.length !== API_KEY.TOTAL_LENGTH) {
488
- throw ShipError.validation(`API key must be ${API_KEY.TOTAL_LENGTH} characters total (${API_KEY.PREFIX} + ${API_KEY.HEX_LENGTH} hex chars)`);
589
+ if (value.length !== shape.TOTAL_LENGTH) {
590
+ throw ShipError.validation(`${label} must be ${shape.TOTAL_LENGTH} characters total (${shape.PREFIX} + ${shape.HEX_LENGTH} hex chars)`);
489
591
  }
490
- const hexPart = apiKey.slice(API_KEY.PREFIX.length);
491
- if (!/^[a-f0-9]{64}$/i.test(hexPart)) {
492
- throw ShipError.validation(`API key must contain ${API_KEY.HEX_LENGTH} hexadecimal characters after "${API_KEY.PREFIX}" prefix`);
592
+ const hexPart = value.slice(shape.PREFIX.length);
593
+ if (!new RegExp(`^[a-f0-9]{${shape.HEX_LENGTH}}$`, 'i').test(hexPart)) {
594
+ throw ShipError.validation(`${label} must contain ${shape.HEX_LENGTH} hexadecimal characters after "${shape.PREFIX}" prefix`);
493
595
  }
494
596
  }
597
+ /**
598
+ * Validate API key format
599
+ */
600
+ export function validateApiKey(apiKey) {
601
+ validatePrefixedCredential(apiKey, API_KEY, 'API key');
602
+ }
495
603
  /**
496
604
  * Validate deploy token format
497
605
  */
498
606
  export function validateDeployToken(deployToken) {
499
- if (!deployToken.startsWith(DEPLOY_TOKEN.PREFIX)) {
500
- throw ShipError.validation(`Deploy token must start with "${DEPLOY_TOKEN.PREFIX}"`);
501
- }
502
- if (deployToken.length !== DEPLOY_TOKEN.TOTAL_LENGTH) {
503
- throw ShipError.validation(`Deploy token must be ${DEPLOY_TOKEN.TOTAL_LENGTH} characters total (${DEPLOY_TOKEN.PREFIX} + ${DEPLOY_TOKEN.HEX_LENGTH} hex chars)`);
607
+ validatePrefixedCredential(deployToken, DEPLOY_TOKEN, 'Deploy token');
608
+ }
609
+ /**
610
+ * Validate a client token of any population. Classifies by shape and applies
611
+ * the matching format rules: `ship-` keys and `deploy-` deploy tokens are
612
+ * validated strictly; opaque tokens (OAuth access tokens, future populations)
613
+ * only need to be non-empty — their validity is the server's to decide.
614
+ */
615
+ export function validateToken(token) {
616
+ switch (classifyToken(token)) {
617
+ case TokenKind.API_KEY:
618
+ validateApiKey(token);
619
+ return;
620
+ case TokenKind.DEPLOY_TOKEN:
621
+ validateDeployToken(token);
622
+ return;
623
+ case TokenKind.OPAQUE:
624
+ if (!token)
625
+ throw ShipError.validation('Token must be a non-empty string');
504
626
  }
505
- const hexPart = deployToken.slice(DEPLOY_TOKEN.PREFIX.length);
506
- if (!/^[a-f0-9]{64}$/i.test(hexPart)) {
507
- throw ShipError.validation(`Deploy token must contain ${DEPLOY_TOKEN.HEX_LENGTH} hexadecimal characters after "${DEPLOY_TOKEN.PREFIX}" prefix`);
627
+ }
628
+ /**
629
+ * Validate a caller identifier against the `CALLER` shape. The server
630
+ * silently ignores malformed values (the header is unauthenticated); clients
631
+ * call this at configuration time so the drop never silently happens.
632
+ */
633
+ export function validateCaller(caller) {
634
+ if (!caller || caller.length > CALLER.MAX_LENGTH || !CALLER.PATTERN.test(caller)) {
635
+ throw ShipError.validation(`Caller must be 1-${CALLER.MAX_LENGTH} characters: letters, digits, dots, underscores, or hyphens`);
508
636
  }
509
637
  }
510
638
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/types",
3
- "version": "2.1.0",
3
+ "version": "2.2.1-beta.0",
4
4
  "description": "Shared types for ShipStatic platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,14 +33,17 @@
33
33
  "node": ">=20.0.0"
34
34
  },
35
35
  "devDependencies": {
36
+ "@biomejs/biome": "2.5.5",
36
37
  "@types/node": "^24.10.9",
37
- "husky": "^9.1.7",
38
38
  "typescript": "^5.9.3",
39
39
  "vitest": "^2.1.8"
40
40
  },
41
41
  "scripts": {
42
42
  "build": "tsc",
43
43
  "clean": "rm -rf dist",
44
- "test": "vitest"
44
+ "test": "vitest",
45
+ "lint": "biome check .",
46
+ "format": "biome format --write .",
47
+ "typecheck": "tsc --noEmit"
45
48
  }
46
49
  }
package/src/index.ts CHANGED
@@ -14,10 +14,10 @@ export const DeploymentStatus = {
14
14
  PENDING: 'pending',
15
15
  SUCCESS: 'success',
16
16
  FAILED: 'failed',
17
- DELETING: 'deleting'
17
+ DELETING: 'deleting',
18
18
  } as const;
19
19
 
20
- export type DeploymentStatusType = typeof DeploymentStatus[keyof typeof DeploymentStatus];
20
+ export type DeploymentStatusType = (typeof DeploymentStatus)[keyof typeof DeploymentStatus];
21
21
 
22
22
  /**
23
23
  * Core deployment object - used in both API responses and SDK
@@ -49,7 +49,6 @@ export interface Deployment {
49
49
  readonly screenshot: string;
50
50
  }
51
51
 
52
-
53
52
  /**
54
53
  * Response from deployment creation. Extends Deployment with one-time fields
55
54
  * only present on creation (not on subsequent GET requests).
@@ -87,10 +86,10 @@ export const DomainStatus = {
87
86
  PENDING: 'pending',
88
87
  PARTIAL: 'partial',
89
88
  SUCCESS: 'success',
90
- PAUSED: 'paused'
89
+ PAUSED: 'paused',
91
90
  } as const;
92
91
 
93
- export type DomainStatusType = typeof DomainStatus[keyof typeof DomainStatus];
92
+ export type DomainStatusType = (typeof DomainStatus)[keyof typeof DomainStatus];
94
93
 
95
94
  /**
96
95
  * Core domain object - used in both API responses and SDK
@@ -206,31 +205,10 @@ export interface DomainValidateResponse {
206
205
  // TOKEN TYPES
207
206
  // =============================================================================
208
207
 
209
- /**
210
- * Deployment token for automated deployments
211
- */
212
- export interface Token {
213
- /** 7-char management identifier */
214
- readonly token: string;
215
- /** The account this token belongs to */
216
- readonly account: string;
217
- /** SHA256 hash of the raw credential (auth lookups only, never exposed to users) */
218
- readonly hash: string;
219
- /** IP-lock. Non-null = single-use (claimable only from this IP, once); null = multi-use. */
220
- readonly ip: string | null;
221
- /** Labels for categorization and filtering (lowercase, alphanumeric with separators). Always present, empty array when none. */
222
- labels: string[];
223
- /** Unix timestamp (seconds) when token was created */
224
- readonly created: number;
225
- /** Unix timestamp (seconds) when token expires, null for never */
226
- readonly expires: number | null;
227
- /** Consumption timestamp. Single-use: gates re-use (set once). Multi-use: refreshed on every claim. Null = never claimed. */
228
- readonly used: number | null;
229
- }
230
-
231
208
  /**
232
209
  * Token as returned by the list endpoint.
233
- * Shows 7-char management ID, omits account and hash.
210
+ * The secret is shown once at creation and never again — listings carry
211
+ * only the management identifier and lifecycle metadata.
234
212
  */
235
213
  export interface TokenListItem {
236
214
  /** 7-char management identifier (e.g., "a1b2c3d") */
@@ -241,7 +219,7 @@ export interface TokenListItem {
241
219
  readonly created: number;
242
220
  /** Unix timestamp (seconds) when token expires, null for never */
243
221
  readonly expires: number | null;
244
- /** Consumption timestamp. Single-use: gates re-use (set once). Multi-use: refreshed on every claim. Null = never claimed. */
222
+ /** Unix timestamp (seconds) of the last request authenticated with this token, null if never used */
245
223
  readonly used: number | null;
246
224
  }
247
225
 
@@ -283,10 +261,10 @@ export const AccountPlan = {
283
261
  ENTERPRISE: 'enterprise',
284
262
  SUSPENDED: 'suspended',
285
263
  TERMINATING: 'terminating',
286
- TERMINATED: 'terminated'
264
+ TERMINATED: 'terminated',
287
265
  } as const;
288
266
 
289
- export type AccountPlanType = typeof AccountPlan[keyof typeof AccountPlan];
267
+ export type AccountPlanType = (typeof AccountPlan)[keyof typeof AccountPlan];
290
268
 
291
269
  /**
292
270
  * Account usage metrics — always available regardless of billing provider.
@@ -321,6 +299,21 @@ export interface Account {
321
299
  readonly grace: number | null;
322
300
  }
323
301
 
302
+ /**
303
+ * Account as returned by `GET /account` — the entity plus how the request
304
+ * was authorized, so `whoami` can answer "what credential am I holding?".
305
+ * Request-scoped fields live on the response type, never on the entity
306
+ * (the `DeploymentCreateResponse` pattern).
307
+ */
308
+ export interface AccountGetResponse extends Account {
309
+ /** How the request that produced this response was authorized. */
310
+ readonly authMethod: AuthMethodType;
311
+ /** Present (and true) only when the caller is an operator acting as themselves. */
312
+ readonly isAdmin?: true;
313
+ /** Present only during read-only admin impersonation: the operator's account id. */
314
+ readonly impersonatedBy?: string;
315
+ }
316
+
324
317
  /**
325
318
  * Account-specific configuration overrides
326
319
  * Allows per-account customization of limits without changing plan
@@ -376,7 +369,7 @@ export const ErrorType = {
376
369
  Config: 'config_error',
377
370
  } as const;
378
371
 
379
- export type ErrorType = typeof ErrorType[keyof typeof ErrorType];
372
+ export type ErrorType = (typeof ErrorType)[keyof typeof ErrorType];
380
373
 
381
374
  /**
382
375
  * Error types that originate exclusively on the client (HTTP clients, SDK
@@ -397,7 +390,13 @@ const CLIENT_ONLY_ERROR_TYPES = new Set<string>([
397
390
  * union so `.has(error.type)` accepts any value from the union.
398
391
  */
399
392
  const ERROR_CATEGORIES = {
400
- client: new Set<ErrorType>([ErrorType.Business, ErrorType.Config, ErrorType.File, ErrorType.Forbidden, ErrorType.Validation]),
393
+ client: new Set<ErrorType>([
394
+ ErrorType.Business,
395
+ ErrorType.Config,
396
+ ErrorType.File,
397
+ ErrorType.Forbidden,
398
+ ErrorType.Validation,
399
+ ]),
401
400
  network: new Set<ErrorType>([ErrorType.Network]),
402
401
  auth: new Set<ErrorType>([ErrorType.Authentication]),
403
402
  } as const;
@@ -410,7 +409,7 @@ const ERROR_CATEGORIES = {
410
409
  * `ErrorType` is automatically picked up.
411
410
  */
412
411
  const SERVER_PRODUCIBLE_ERROR_TYPES = new Set<string>(
413
- Object.values(ErrorType).filter(t => !CLIENT_ONLY_ERROR_TYPES.has(t)),
412
+ Object.values(ErrorType).filter((t) => !CLIENT_ONLY_ERROR_TYPES.has(t)),
414
413
  );
415
414
 
416
415
  /**
@@ -447,15 +446,14 @@ export class ShipError extends Error {
447
446
  // tag (see `ShipError.authentication` JSDoc) — these are server-side
448
447
  // diagnostics like 'session_invalid' that must not leak to clients.
449
448
  const authDetails = this.details as { internal?: unknown } | undefined;
450
- const details = this.type === ErrorType.Authentication && authDetails?.internal
451
- ? undefined
452
- : this.details;
449
+ const details =
450
+ this.type === ErrorType.Authentication && authDetails?.internal ? undefined : this.details;
453
451
 
454
452
  return {
455
453
  error: this.type,
456
454
  message: this.message,
457
455
  status: this.status,
458
- details
456
+ details,
459
457
  };
460
458
  }
461
459
 
@@ -481,10 +479,7 @@ export class ShipError extends Error {
481
479
  * Async because it reads the response body. Returns rather than throws so
482
480
  * callers can compose; most will `throw await ShipError.fromHttpResponse(...)`.
483
481
  */
484
- static async fromHttpResponse(
485
- response: Response,
486
- operationName?: string,
487
- ): Promise<ShipError> {
482
+ static async fromHttpResponse(response: Response, operationName?: string): Promise<ShipError> {
488
483
  let message: string | undefined;
489
484
  let details: unknown;
490
485
  let bodyType: ErrorType | undefined;
@@ -512,12 +507,15 @@ export class ShipError extends Error {
512
507
 
513
508
  message = message || `${operationName || 'Request'} failed with status ${response.status}`;
514
509
 
515
- const type = bodyType ?? (
516
- response.status === 401 ? ErrorType.Authentication :
517
- response.status === 403 ? ErrorType.Forbidden :
518
- response.status === 429 ? ErrorType.RateLimit :
519
- ErrorType.Api
520
- );
510
+ const type =
511
+ bodyType ??
512
+ (response.status === 401
513
+ ? ErrorType.Authentication
514
+ : response.status === 403
515
+ ? ErrorType.Forbidden
516
+ : response.status === 429
517
+ ? ErrorType.RateLimit
518
+ : ErrorType.Api);
521
519
 
522
520
  return new ShipError(type, message, response.status, details);
523
521
  }
@@ -576,7 +574,7 @@ export class ShipError extends Error {
576
574
  return new ShipError(ErrorType.Forbidden, message, 403, details);
577
575
  }
578
576
 
579
- static rateLimit(message: string = "Too many requests", details?: unknown): ShipError {
577
+ static rateLimit(message: string = 'Too many requests', details?: unknown): ShipError {
580
578
  return new ShipError(ErrorType.RateLimit, message, 429, details);
581
579
  }
582
580
 
@@ -593,7 +591,7 @@ export class ShipError extends Error {
593
591
  * Use this pattern in API auth code; do not put client-visible info under
594
592
  * `internal`. Other `details` keys round-trip normally.
595
593
  */
596
- static authentication(message: string = "Authentication required", details?: unknown): ShipError {
594
+ static authentication(message: string = 'Authentication required', details?: unknown): ShipError {
597
595
  return new ShipError(ErrorType.Authentication, message, 401, details);
598
596
  }
599
597
 
@@ -666,15 +664,14 @@ export function isShipError(error: unknown): error is ShipError {
666
664
  // =============================================================================
667
665
 
668
666
  /**
669
- * Plan-based platform limits returned by the `/config` endpoint.
667
+ * Plan-based platform limits returned by the `/limits` endpoint.
670
668
  *
671
669
  * The SDK fetches these once on first API call to drive client-side
672
670
  * file-size / file-count / total-size validation that mirrors what the API
673
671
  * would enforce server-side. Limits vary by account plan.
674
672
  *
675
- * Distinct from `ResolvedConfig` (which carries the *client's* credentials
676
- * and API URL after defaulting); this one carries the *platform's* posted
677
- * caps for the current account.
673
+ * These are the *platform's* posted caps for the current account — server
674
+ * truth delivered at runtime, never hard-coded on the client.
678
675
  */
679
676
  export interface PlatformLimits {
680
677
  /** Maximum size in bytes for a single file. */
@@ -701,21 +698,47 @@ export interface PlatformLimits {
701
698
  */
702
699
  export const BLOCKED_EXTENSIONS: ReadonlySet<string> = new Set([
703
700
  // Executables
704
- 'exe', 'msi', 'dll', 'scr', 'bat', 'cmd', 'com', 'pif', 'app', 'deb', 'rpm',
701
+ 'exe',
702
+ 'msi',
703
+ 'dll',
704
+ 'scr',
705
+ 'bat',
706
+ 'cmd',
707
+ 'com',
708
+ 'pif',
709
+ 'app',
710
+ 'deb',
711
+ 'rpm',
705
712
  // Installers
706
- 'pkg', 'mpkg',
713
+ 'pkg',
714
+ 'mpkg',
707
715
  // Disk images
708
- 'dmg', 'iso', 'img',
716
+ 'dmg',
717
+ 'iso',
718
+ 'img',
709
719
  // Malware vectors
710
- 'cab', 'cpl', 'chm',
720
+ 'cab',
721
+ 'cpl',
722
+ 'chm',
711
723
  // Dangerous scripts
712
- 'ps1', 'vbs', 'vbe', 'ws', 'wsf', 'wsc', 'wsh', 'reg',
724
+ 'ps1',
725
+ 'vbs',
726
+ 'vbe',
727
+ 'ws',
728
+ 'wsf',
729
+ 'wsc',
730
+ 'wsh',
731
+ 'reg',
713
732
  // Java
714
- 'jar', 'jnlp',
733
+ 'jar',
734
+ 'jnlp',
715
735
  // Mobile/browser packages
716
- 'apk', 'crx',
736
+ 'apk',
737
+ 'crx',
717
738
  // Shortcut/link
718
- 'lnk', 'inf', 'hta',
739
+ 'lnk',
740
+ 'inf',
741
+ 'hta',
719
742
  ]);
720
743
 
721
744
  /**
@@ -752,6 +775,7 @@ export function isBlockedExtension(filename: string): boolean {
752
775
  *
753
776
  * Everything else is allowed — browser percent-encodes, Worker decodes, R2 matches.
754
777
  */
778
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: blocking control characters is this regex's purpose
755
779
  export const UNSAFE_FILENAME_CHARS = /[\x00-\x1f\x7f#?%\\<>"]/;
756
780
 
757
781
  /**
@@ -790,7 +814,7 @@ export const UNBUILT_PROJECT_MARKERS: ReadonlySet<string> = new Set([
790
814
  */
791
815
  export function hasUnbuiltMarker(filePath: string): boolean {
792
816
  const segments = filePath.replace(/\\/g, '/').split('/').filter(Boolean);
793
- return segments.some(s => UNBUILT_PROJECT_MARKERS.has(s));
817
+ return segments.some((s) => UNBUILT_PROJECT_MARKERS.has(s));
794
818
  }
795
819
 
796
820
  // =============================================================================
@@ -807,6 +831,35 @@ export interface PingResponse {
807
831
  timestamp?: number;
808
832
  }
809
833
 
834
+ // =============================================================================
835
+ // CREDENTIAL SHAPES
836
+ // =============================================================================
837
+ // The one address for credential vocabulary: how a request is authorized
838
+ // (AuthMethod), the shapes that distinguish populations on the wire
839
+ // (API_KEY, DEPLOY_TOKEN, CALLER), the single dispatch over them (TokenKind,
840
+ // classifyToken), and the delegated-access scopes (OAuthScope).
841
+
842
+ /**
843
+ * How a request (or recorded activity) was authorized.
844
+ *
845
+ * Client populations: `SESSION` (first-party cookie), `API_KEY` (`ship-`
846
+ * key), `TOKEN` (`deploy-` deploy token), `AGENT` (anonymous public deploy —
847
+ * no credential; the platform grants the public-account identity per
848
+ * request), `OAUTH` (delegated access token). Server populations: `WEBHOOK`
849
+ * (signed webhook processing), `SYSTEM` (scheduled/background jobs).
850
+ */
851
+ export const AuthMethod = {
852
+ SESSION: 'session',
853
+ API_KEY: 'apiKey',
854
+ TOKEN: 'token',
855
+ AGENT: 'agent',
856
+ OAUTH: 'oauth',
857
+ WEBHOOK: 'webhook',
858
+ SYSTEM: 'system',
859
+ } as const;
860
+
861
+ export type AuthMethodType = (typeof AuthMethod)[keyof typeof AuthMethod];
862
+
810
863
  /**
811
864
  * Shape constants for API keys (`ship-{64 hex chars}`).
812
865
  * Single source of truth used by validation utilities and auth middleware.
@@ -823,29 +876,65 @@ export const API_KEY = {
823
876
  } as const;
824
877
 
825
878
  /**
826
- * Shape constants for deploy tokens (`token-{64 hex chars}`).
879
+ * Shape constants for deploy tokens (`deploy-{64 hex chars}`).
827
880
  * Single source of truth used by validation utilities and auth middleware.
828
881
  */
829
882
  export const DEPLOY_TOKEN = {
830
883
  /** Prefix that identifies a deploy token. */
831
- PREFIX: 'token-',
884
+ PREFIX: 'deploy-',
832
885
  /** Number of hex characters following the prefix. */
833
886
  HEX_LENGTH: 64,
834
- /** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 70`). */
835
- TOTAL_LENGTH: 70,
887
+ /** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 71`). */
888
+ TOTAL_LENGTH: 71,
836
889
  } as const;
837
890
 
838
- // Authentication Method Constants
839
- export const AuthMethod = {
840
- SESSION: 'session',
841
- API_KEY: 'apiKey',
842
- TOKEN: 'token',
843
- OAUTH: 'oauth',
844
- WEBHOOK: 'webhook',
845
- SYSTEM: 'system'
891
+ /**
892
+ * Shape constants for caller identifiers (the `X-Caller` instance-identity
893
+ * header — rate-limit bucketing for multi-tenant orchestrators). The API
894
+ * normalizes case and silently ignores malformed values (the header is
895
+ * unauthenticated); clients validate at the boundary via `validateCaller`,
896
+ * so a value the server would drop fails fast instead.
897
+ */
898
+ export const CALLER = {
899
+ /** HTTP header name. */
900
+ HEADER: 'X-Caller',
901
+ /** Maximum identifier length. */
902
+ MAX_LENGTH: 128,
903
+ /** Allowed characters: alphanumeric, dot, underscore, hyphen. */
904
+ PATTERN: /^[a-zA-Z0-9._-]+$/,
846
905
  } as const;
847
906
 
848
- export type AuthMethodType = typeof AuthMethod[keyof typeof AuthMethod];
907
+ /**
908
+ * Token populations distinguishable by shape. The platform carries every
909
+ * client token in one wire slot (`Authorization: Bearer <value>`) and
910
+ * classifies by value, never by a side channel — this is the classifier.
911
+ *
912
+ * `API_KEY` and `DEPLOY_TOKEN` *are* `AuthMethod.API_KEY` and
913
+ * `AuthMethod.TOKEN` — the equality is structural, so a classification flows
914
+ * straight into an auth method and the pair can never drift. `OPAQUE` is any
915
+ * other value — shape says nothing about it, so only a lookup can. Today the
916
+ * server refuses every opaque bearer; the OAuth access-token population
917
+ * resolves there when the authorization server ships.
918
+ */
919
+ export const TokenKind = {
920
+ API_KEY: AuthMethod.API_KEY,
921
+ DEPLOY_TOKEN: AuthMethod.TOKEN,
922
+ OPAQUE: 'opaque',
923
+ } as const;
924
+
925
+ export type TokenKindType = (typeof TokenKind)[keyof typeof TokenKind];
926
+
927
+ /**
928
+ * Classify a client token by shape. The single dispatch used by both sides
929
+ * of the wire: API auth middleware (which population is this credential?)
930
+ * and SDK validation (which format rules apply before sending?). Sharing it
931
+ * is what guarantees client and server can never disagree on dispatch.
932
+ */
933
+ export function classifyToken(token: string): TokenKindType {
934
+ if (token.startsWith(API_KEY.PREFIX)) return TokenKind.API_KEY;
935
+ if (token.startsWith(DEPLOY_TOKEN.PREFIX)) return TokenKind.DEPLOY_TOKEN;
936
+ return TokenKind.OPAQUE;
937
+ }
849
938
 
850
939
  /**
851
940
  * OAuth scope vocabulary for delegated third-party access tokens.
@@ -866,51 +955,94 @@ export const OAuthScope = {
866
955
  DOMAINS_WRITE: 'domains:write',
867
956
  } as const;
868
957
 
869
- export type OAuthScopeType = typeof OAuthScope[keyof typeof OAuthScope];
958
+ export type OAuthScopeType = (typeof OAuthScope)[keyof typeof OAuthScope];
959
+
960
+ // =============================================================================
961
+ // DEPLOYMENT CONFIGURATION CONSTANTS
962
+ // =============================================================================
870
963
 
871
- // Deployment Configuration
872
964
  export const DEPLOYMENT_CONFIG_FILENAME = 'ship.json';
873
965
 
874
966
  /** Default ship.json config for SPA routing. Single source of truth — used by both API and SDK. */
875
- export const SPA_DEFAULT_CONFIG = { rewrites: [{ source: '/(.*)', destination: '/index.html' }] } as const;
967
+ export const SPA_DEFAULT_CONFIG = {
968
+ rewrites: [{ source: '/(.*)', destination: '/index.html' }],
969
+ } as const;
876
970
 
877
971
  // =============================================================================
878
972
  // VALIDATION UTILITIES
879
973
  // =============================================================================
880
974
 
881
975
  /**
882
- * Validate API key format
976
+ * Shared rule for prefixed credentials: `{PREFIX}{HEX_LENGTH hex chars}`.
977
+ * The regex derives from the shape constants, so the validators can never
978
+ * drift from the shapes `classifyToken` dispatches on.
883
979
  */
884
- export function validateApiKey(apiKey: string): void {
885
- if (!apiKey.startsWith(API_KEY.PREFIX)) {
886
- throw ShipError.validation(`API key must start with "${API_KEY.PREFIX}"`);
980
+ function validatePrefixedCredential(
981
+ value: string,
982
+ shape: { PREFIX: string; HEX_LENGTH: number; TOTAL_LENGTH: number },
983
+ label: string,
984
+ ): void {
985
+ if (!value.startsWith(shape.PREFIX)) {
986
+ throw ShipError.validation(`${label} must start with "${shape.PREFIX}"`);
887
987
  }
888
988
 
889
- if (apiKey.length !== API_KEY.TOTAL_LENGTH) {
890
- throw ShipError.validation(`API key must be ${API_KEY.TOTAL_LENGTH} characters total (${API_KEY.PREFIX} + ${API_KEY.HEX_LENGTH} hex chars)`);
989
+ if (value.length !== shape.TOTAL_LENGTH) {
990
+ throw ShipError.validation(
991
+ `${label} must be ${shape.TOTAL_LENGTH} characters total (${shape.PREFIX} + ${shape.HEX_LENGTH} hex chars)`,
992
+ );
891
993
  }
892
994
 
893
- const hexPart = apiKey.slice(API_KEY.PREFIX.length);
894
- if (!/^[a-f0-9]{64}$/i.test(hexPart)) {
895
- throw ShipError.validation(`API key must contain ${API_KEY.HEX_LENGTH} hexadecimal characters after "${API_KEY.PREFIX}" prefix`);
995
+ const hexPart = value.slice(shape.PREFIX.length);
996
+ if (!new RegExp(`^[a-f0-9]{${shape.HEX_LENGTH}}$`, 'i').test(hexPart)) {
997
+ throw ShipError.validation(
998
+ `${label} must contain ${shape.HEX_LENGTH} hexadecimal characters after "${shape.PREFIX}" prefix`,
999
+ );
896
1000
  }
897
1001
  }
898
1002
 
1003
+ /**
1004
+ * Validate API key format
1005
+ */
1006
+ export function validateApiKey(apiKey: string): void {
1007
+ validatePrefixedCredential(apiKey, API_KEY, 'API key');
1008
+ }
1009
+
899
1010
  /**
900
1011
  * Validate deploy token format
901
1012
  */
902
1013
  export function validateDeployToken(deployToken: string): void {
903
- if (!deployToken.startsWith(DEPLOY_TOKEN.PREFIX)) {
904
- throw ShipError.validation(`Deploy token must start with "${DEPLOY_TOKEN.PREFIX}"`);
905
- }
1014
+ validatePrefixedCredential(deployToken, DEPLOY_TOKEN, 'Deploy token');
1015
+ }
906
1016
 
907
- if (deployToken.length !== DEPLOY_TOKEN.TOTAL_LENGTH) {
908
- throw ShipError.validation(`Deploy token must be ${DEPLOY_TOKEN.TOTAL_LENGTH} characters total (${DEPLOY_TOKEN.PREFIX} + ${DEPLOY_TOKEN.HEX_LENGTH} hex chars)`);
1017
+ /**
1018
+ * Validate a client token of any population. Classifies by shape and applies
1019
+ * the matching format rules: `ship-` keys and `deploy-` deploy tokens are
1020
+ * validated strictly; opaque tokens (OAuth access tokens, future populations)
1021
+ * only need to be non-empty — their validity is the server's to decide.
1022
+ */
1023
+ export function validateToken(token: string): void {
1024
+ switch (classifyToken(token)) {
1025
+ case TokenKind.API_KEY:
1026
+ validateApiKey(token);
1027
+ return;
1028
+ case TokenKind.DEPLOY_TOKEN:
1029
+ validateDeployToken(token);
1030
+ return;
1031
+ case TokenKind.OPAQUE:
1032
+ if (!token) throw ShipError.validation('Token must be a non-empty string');
909
1033
  }
1034
+ }
910
1035
 
911
- const hexPart = deployToken.slice(DEPLOY_TOKEN.PREFIX.length);
912
- if (!/^[a-f0-9]{64}$/i.test(hexPart)) {
913
- throw ShipError.validation(`Deploy token must contain ${DEPLOY_TOKEN.HEX_LENGTH} hexadecimal characters after "${DEPLOY_TOKEN.PREFIX}" prefix`);
1036
+ /**
1037
+ * Validate a caller identifier against the `CALLER` shape. The server
1038
+ * silently ignores malformed values (the header is unauthenticated); clients
1039
+ * call this at configuration time so the drop never silently happens.
1040
+ */
1041
+ export function validateCaller(caller: string): void {
1042
+ if (!caller || caller.length > CALLER.MAX_LENGTH || !CALLER.PATTERN.test(caller)) {
1043
+ throw ShipError.validation(
1044
+ `Caller must be 1-${CALLER.MAX_LENGTH} characters: letters, digits, dots, underscores, or hyphens`,
1045
+ );
914
1046
  }
915
1047
  }
916
1048
 
@@ -1011,29 +1143,6 @@ export interface StaticFile {
1011
1143
  size: number;
1012
1144
  }
1013
1145
 
1014
- // =============================================================================
1015
- // PLATFORM CONFIGURATION
1016
- // =============================================================================
1017
-
1018
- /**
1019
- * Resolved client configuration with `apiUrl` defaulted.
1020
- *
1021
- * Produced by the SDK after layering its credential sources (constructor
1022
- * options on top of `SHIP_*` env vars in Node; constructor options only in
1023
- * Browser) and applying the `DEFAULT_API` fallback. File-based sources
1024
- * (`.shiprc`, `package.json` `"ship"` key) are the CLI's responsibility and
1025
- * are merged in *before* construction — by the time a `ResolvedConfig`
1026
- * exists, every source has already collapsed into the constructor argument.
1027
- */
1028
- export interface ResolvedConfig {
1029
- /** API URL — always present after resolution, defaults to `DEFAULT_API`. */
1030
- apiUrl: string;
1031
- /** API key for authenticated deployments. */
1032
- apiKey?: string;
1033
- /** Deploy token used as the request credential (`token-{64-hex}`). */
1034
- deployToken?: string;
1035
- }
1036
-
1037
1146
  // =============================================================================
1038
1147
  // PROGRESS TRACKING
1039
1148
  // =============================================================================
@@ -1102,13 +1211,18 @@ export interface DeploymentUploadOptions {
1102
1211
  prerender?: boolean;
1103
1212
  /** @internal Trigger server-side SPA detection. Only available via /upload endpoint. */
1104
1213
  spa?: boolean;
1214
+ /** @internal reCAPTCHA proof for the anonymous human deploy channel. Only available via /upload endpoint. */
1215
+ captcha?: string;
1105
1216
  }
1106
1217
 
1107
1218
  /**
1108
1219
  * Deployment resource interface - the contract all implementations must follow
1109
1220
  */
1110
1221
  export interface DeploymentResource {
1111
- upload: (input: DeployInput, options?: DeploymentUploadOptions) => Promise<DeploymentCreateResponse>;
1222
+ upload: (
1223
+ input: DeployInput,
1224
+ options?: DeploymentUploadOptions,
1225
+ ) => Promise<DeploymentCreateResponse>;
1112
1226
  list: () => Promise<DeploymentListResponse>;
1113
1227
  get: (id: string) => Promise<Deployment>;
1114
1228
  set: (id: string, options: { labels: string[] }) => Promise<Deployment>;
@@ -1119,7 +1233,10 @@ export interface DeploymentResource {
1119
1233
  * Domain resource interface - the contract all implementations must follow
1120
1234
  */
1121
1235
  export interface DomainResource {
1122
- set: (name: string, options?: { deployment?: string; labels?: string[] }) => Promise<DomainSetResult>;
1236
+ set: (
1237
+ name: string,
1238
+ options?: { deployment?: string; labels?: string[] },
1239
+ ) => Promise<DomainSetResult>;
1123
1240
  list: () => Promise<DomainListResponse>;
1124
1241
  get: (name: string) => Promise<Domain>;
1125
1242
  remove: (name: string) => Promise<void>;
@@ -1134,7 +1251,7 @@ export interface DomainResource {
1134
1251
  * Account resource interface - the contract all implementations must follow
1135
1252
  */
1136
1253
  export interface AccountResource {
1137
- get: () => Promise<Account>;
1254
+ get: () => Promise<AccountGetResponse>;
1138
1255
  }
1139
1256
 
1140
1257
  /**
@@ -1169,7 +1286,6 @@ export interface BillingStatus {
1169
1286
  portal: string | null;
1170
1287
  }
1171
1288
 
1172
-
1173
1289
  /**
1174
1290
  * Checkout session response from POST /billing/checkout
1175
1291
  */
@@ -1233,9 +1349,9 @@ export type ActivityEvent =
1233
1349
  | 'refund.created'
1234
1350
  | 'dispute.created'
1235
1351
  // Billing operational events (admin/debug only, not user-visible)
1236
- | 'billing.sync' // Outbound: unit count pushed to payment provider
1237
- | 'billing.stale' // Dropped: webhook predates last known state
1238
- | 'billing.race'; // Dropped: concurrent webhook already updated state
1352
+ | 'billing.sync' // Outbound: unit count pushed to payment provider
1353
+ | 'billing.stale' // Dropped: webhook predates last known state
1354
+ | 'billing.race'; // Dropped: concurrent webhook already updated state
1239
1355
 
1240
1356
  /**
1241
1357
  * Activity events visible to users in the dashboard
@@ -1344,8 +1460,8 @@ export const FileValidationStatus = {
1344
1460
  READY: 'ready',
1345
1461
  } as const;
1346
1462
 
1347
- export type FileValidationStatusType = typeof FileValidationStatus[keyof typeof FileValidationStatus];
1348
-
1463
+ export type FileValidationStatusType =
1464
+ (typeof FileValidationStatus)[keyof typeof FileValidationStatus];
1349
1465
 
1350
1466
  /**
1351
1467
  * A validation issue with a display-ready message
@@ -1588,4 +1704,4 @@ export function validatePassword(value: unknown): string | undefined {
1588
1704
  );
1589
1705
  }
1590
1706
  return trimmed;
1591
- }
1707
+ }