@shipstatic/types 2.1.0 → 2.2.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
@@ -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
  /**
@@ -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
@@ -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
@@ -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
package/dist/index.js CHANGED
@@ -417,6 +417,31 @@ export function hasUnbuiltMarker(filePath) {
417
417
  const segments = filePath.replace(/\\/g, '/').split('/').filter(Boolean);
418
418
  return segments.some(s => UNBUILT_PROJECT_MARKERS.has(s));
419
419
  }
420
+ // =============================================================================
421
+ // CREDENTIAL SHAPES
422
+ // =============================================================================
423
+ // The one address for credential vocabulary: how a request is authorized
424
+ // (AuthMethod), the shapes that distinguish populations on the wire
425
+ // (API_KEY, DEPLOY_TOKEN, CALLER), the single dispatch over them (TokenKind,
426
+ // classifyToken), and the delegated-access scopes (OAuthScope).
427
+ /**
428
+ * How a request (or recorded activity) was authorized.
429
+ *
430
+ * Client populations: `SESSION` (first-party cookie), `API_KEY` (`ship-`
431
+ * key), `TOKEN` (`deploy-` deploy token), `AGENT` (anonymous public deploy —
432
+ * no credential; the platform grants the public-account identity per
433
+ * request), `OAUTH` (delegated access token). Server populations: `WEBHOOK`
434
+ * (signed webhook processing), `SYSTEM` (scheduled/background jobs).
435
+ */
436
+ export const AuthMethod = {
437
+ SESSION: 'session',
438
+ API_KEY: 'apiKey',
439
+ TOKEN: 'token',
440
+ AGENT: 'agent',
441
+ OAUTH: 'oauth',
442
+ WEBHOOK: 'webhook',
443
+ SYSTEM: 'system'
444
+ };
420
445
  /**
421
446
  * Shape constants for API keys (`ship-{64 hex chars}`).
422
447
  * Single source of truth used by validation utilities and auth middleware.
@@ -432,26 +457,62 @@ export const API_KEY = {
432
457
  HINT_LENGTH: 4,
433
458
  };
434
459
  /**
435
- * Shape constants for deploy tokens (`token-{64 hex chars}`).
460
+ * Shape constants for deploy tokens (`deploy-{64 hex chars}`).
436
461
  * Single source of truth used by validation utilities and auth middleware.
437
462
  */
438
463
  export const DEPLOY_TOKEN = {
439
464
  /** Prefix that identifies a deploy token. */
440
- PREFIX: 'token-',
465
+ PREFIX: 'deploy-',
441
466
  /** Number of hex characters following the prefix. */
442
467
  HEX_LENGTH: 64,
443
- /** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 70`). */
444
- TOTAL_LENGTH: 70,
468
+ /** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 71`). */
469
+ TOTAL_LENGTH: 71,
445
470
  };
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'
471
+ /**
472
+ * Shape constants for caller identifiers (the `X-Caller` instance-identity
473
+ * header — rate-limit bucketing for multi-tenant orchestrators). The API
474
+ * normalizes case and silently ignores malformed values (the header is
475
+ * unauthenticated); clients validate at the boundary via `validateCaller`,
476
+ * so a value the server would drop fails fast instead.
477
+ */
478
+ export const CALLER = {
479
+ /** HTTP header name. */
480
+ HEADER: 'X-Caller',
481
+ /** Maximum identifier length. */
482
+ MAX_LENGTH: 128,
483
+ /** Allowed characters: alphanumeric, dot, underscore, hyphen. */
484
+ PATTERN: /^[a-zA-Z0-9._-]+$/,
485
+ };
486
+ /**
487
+ * Token populations distinguishable by shape. The platform carries every
488
+ * client token in one wire slot (`Authorization: Bearer <value>`) and
489
+ * classifies by value, never by a side channel — this is the classifier.
490
+ *
491
+ * `API_KEY` and `DEPLOY_TOKEN` *are* `AuthMethod.API_KEY` and
492
+ * `AuthMethod.TOKEN` — the equality is structural, so a classification flows
493
+ * straight into an auth method and the pair can never drift. `OPAQUE` is any
494
+ * other value — shape says nothing about it, so only a lookup can. Today the
495
+ * server refuses every opaque bearer; the OAuth access-token population
496
+ * resolves there when the authorization server ships.
497
+ */
498
+ export const TokenKind = {
499
+ API_KEY: AuthMethod.API_KEY,
500
+ DEPLOY_TOKEN: AuthMethod.TOKEN,
501
+ OPAQUE: 'opaque',
454
502
  };
503
+ /**
504
+ * Classify a client token by shape. The single dispatch used by both sides
505
+ * of the wire: API auth middleware (which population is this credential?)
506
+ * and SDK validation (which format rules apply before sending?). Sharing it
507
+ * is what guarantees client and server can never disagree on dispatch.
508
+ */
509
+ export function classifyToken(token) {
510
+ if (token.startsWith(API_KEY.PREFIX))
511
+ return TokenKind.API_KEY;
512
+ if (token.startsWith(DEPLOY_TOKEN.PREFIX))
513
+ return TokenKind.DEPLOY_TOKEN;
514
+ return TokenKind.OPAQUE;
515
+ }
455
516
  /**
456
517
  * OAuth scope vocabulary for delegated third-party access tokens.
457
518
  * Single source of truth used by the authorization server (advertised in
@@ -470,7 +531,9 @@ export const OAuthScope = {
470
531
  DOMAINS_READ: 'domains:read',
471
532
  DOMAINS_WRITE: 'domains:write',
472
533
  };
473
- // Deployment Configuration
534
+ // =============================================================================
535
+ // DEPLOYMENT CONFIGURATION CONSTANTS
536
+ // =============================================================================
474
537
  export const DEPLOYMENT_CONFIG_FILENAME = 'ship.json';
475
538
  /** Default ship.json config for SPA routing. Single source of truth — used by both API and SDK. */
476
539
  export const SPA_DEFAULT_CONFIG = { rewrites: [{ source: '/(.*)', destination: '/index.html' }] };
@@ -478,33 +541,59 @@ export const SPA_DEFAULT_CONFIG = { rewrites: [{ source: '/(.*)', destination: '
478
541
  // VALIDATION UTILITIES
479
542
  // =============================================================================
480
543
  /**
481
- * Validate API key format
544
+ * Shared rule for prefixed credentials: `{PREFIX}{HEX_LENGTH hex chars}`.
545
+ * The regex derives from the shape constants, so the validators can never
546
+ * drift from the shapes `classifyToken` dispatches on.
482
547
  */
483
- export function validateApiKey(apiKey) {
484
- if (!apiKey.startsWith(API_KEY.PREFIX)) {
485
- throw ShipError.validation(`API key must start with "${API_KEY.PREFIX}"`);
548
+ function validatePrefixedCredential(value, shape, label) {
549
+ if (!value.startsWith(shape.PREFIX)) {
550
+ throw ShipError.validation(`${label} must start with "${shape.PREFIX}"`);
486
551
  }
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)`);
552
+ if (value.length !== shape.TOTAL_LENGTH) {
553
+ throw ShipError.validation(`${label} must be ${shape.TOTAL_LENGTH} characters total (${shape.PREFIX} + ${shape.HEX_LENGTH} hex chars)`);
489
554
  }
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`);
555
+ const hexPart = value.slice(shape.PREFIX.length);
556
+ if (!new RegExp(`^[a-f0-9]{${shape.HEX_LENGTH}}$`, 'i').test(hexPart)) {
557
+ throw ShipError.validation(`${label} must contain ${shape.HEX_LENGTH} hexadecimal characters after "${shape.PREFIX}" prefix`);
493
558
  }
494
559
  }
560
+ /**
561
+ * Validate API key format
562
+ */
563
+ export function validateApiKey(apiKey) {
564
+ validatePrefixedCredential(apiKey, API_KEY, 'API key');
565
+ }
495
566
  /**
496
567
  * Validate deploy token format
497
568
  */
498
569
  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)`);
570
+ validatePrefixedCredential(deployToken, DEPLOY_TOKEN, 'Deploy token');
571
+ }
572
+ /**
573
+ * Validate a client token of any population. Classifies by shape and applies
574
+ * the matching format rules: `ship-` keys and `deploy-` deploy tokens are
575
+ * validated strictly; opaque tokens (OAuth access tokens, future populations)
576
+ * only need to be non-empty — their validity is the server's to decide.
577
+ */
578
+ export function validateToken(token) {
579
+ switch (classifyToken(token)) {
580
+ case TokenKind.API_KEY:
581
+ return validateApiKey(token);
582
+ case TokenKind.DEPLOY_TOKEN:
583
+ return validateDeployToken(token);
584
+ case TokenKind.OPAQUE:
585
+ if (!token)
586
+ throw ShipError.validation('Token must be a non-empty string');
504
587
  }
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`);
588
+ }
589
+ /**
590
+ * Validate a caller identifier against the `CALLER` shape. The server
591
+ * silently ignores malformed values (the header is unauthenticated); clients
592
+ * call this at configuration time so the drop never silently happens.
593
+ */
594
+ export function validateCaller(caller) {
595
+ if (!caller || caller.length > CALLER.MAX_LENGTH || !CALLER.PATTERN.test(caller)) {
596
+ throw ShipError.validation(`Caller must be 1-${CALLER.MAX_LENGTH} characters: letters, digits, dots, underscores, or hyphens`);
508
597
  }
509
598
  }
510
599
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/types",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Shared types for ShipStatic platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -206,31 +206,10 @@ export interface DomainValidateResponse {
206
206
  // TOKEN TYPES
207
207
  // =============================================================================
208
208
 
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
209
  /**
232
210
  * Token as returned by the list endpoint.
233
- * Shows 7-char management ID, omits account and hash.
211
+ * The secret is shown once at creation and never again — listings carry
212
+ * only the management identifier and lifecycle metadata.
234
213
  */
235
214
  export interface TokenListItem {
236
215
  /** 7-char management identifier (e.g., "a1b2c3d") */
@@ -241,7 +220,7 @@ export interface TokenListItem {
241
220
  readonly created: number;
242
221
  /** Unix timestamp (seconds) when token expires, null for never */
243
222
  readonly expires: number | null;
244
- /** Consumption timestamp. Single-use: gates re-use (set once). Multi-use: refreshed on every claim. Null = never claimed. */
223
+ /** Unix timestamp (seconds) of the last request authenticated with this token, null if never used */
245
224
  readonly used: number | null;
246
225
  }
247
226
 
@@ -321,6 +300,21 @@ export interface Account {
321
300
  readonly grace: number | null;
322
301
  }
323
302
 
303
+ /**
304
+ * Account as returned by `GET /account` — the entity plus how the request
305
+ * was authorized, so `whoami` can answer "what credential am I holding?".
306
+ * Request-scoped fields live on the response type, never on the entity
307
+ * (the `DeploymentCreateResponse` pattern).
308
+ */
309
+ export interface AccountGetResponse extends Account {
310
+ /** How the request that produced this response was authorized. */
311
+ readonly authMethod: AuthMethodType;
312
+ /** Present (and true) only when the caller is an operator acting as themselves. */
313
+ readonly isAdmin?: true;
314
+ /** Present only during read-only admin impersonation: the operator's account id. */
315
+ readonly impersonatedBy?: string;
316
+ }
317
+
324
318
  /**
325
319
  * Account-specific configuration overrides
326
320
  * Allows per-account customization of limits without changing plan
@@ -666,15 +660,14 @@ export function isShipError(error: unknown): error is ShipError {
666
660
  // =============================================================================
667
661
 
668
662
  /**
669
- * Plan-based platform limits returned by the `/config` endpoint.
663
+ * Plan-based platform limits returned by the `/limits` endpoint.
670
664
  *
671
665
  * The SDK fetches these once on first API call to drive client-side
672
666
  * file-size / file-count / total-size validation that mirrors what the API
673
667
  * would enforce server-side. Limits vary by account plan.
674
668
  *
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.
669
+ * These are the *platform's* posted caps for the current account — server
670
+ * truth delivered at runtime, never hard-coded on the client.
678
671
  */
679
672
  export interface PlatformLimits {
680
673
  /** Maximum size in bytes for a single file. */
@@ -807,6 +800,35 @@ export interface PingResponse {
807
800
  timestamp?: number;
808
801
  }
809
802
 
803
+ // =============================================================================
804
+ // CREDENTIAL SHAPES
805
+ // =============================================================================
806
+ // The one address for credential vocabulary: how a request is authorized
807
+ // (AuthMethod), the shapes that distinguish populations on the wire
808
+ // (API_KEY, DEPLOY_TOKEN, CALLER), the single dispatch over them (TokenKind,
809
+ // classifyToken), and the delegated-access scopes (OAuthScope).
810
+
811
+ /**
812
+ * How a request (or recorded activity) was authorized.
813
+ *
814
+ * Client populations: `SESSION` (first-party cookie), `API_KEY` (`ship-`
815
+ * key), `TOKEN` (`deploy-` deploy token), `AGENT` (anonymous public deploy —
816
+ * no credential; the platform grants the public-account identity per
817
+ * request), `OAUTH` (delegated access token). Server populations: `WEBHOOK`
818
+ * (signed webhook processing), `SYSTEM` (scheduled/background jobs).
819
+ */
820
+ export const AuthMethod = {
821
+ SESSION: 'session',
822
+ API_KEY: 'apiKey',
823
+ TOKEN: 'token',
824
+ AGENT: 'agent',
825
+ OAUTH: 'oauth',
826
+ WEBHOOK: 'webhook',
827
+ SYSTEM: 'system'
828
+ } as const;
829
+
830
+ export type AuthMethodType = typeof AuthMethod[keyof typeof AuthMethod];
831
+
810
832
  /**
811
833
  * Shape constants for API keys (`ship-{64 hex chars}`).
812
834
  * Single source of truth used by validation utilities and auth middleware.
@@ -823,29 +845,65 @@ export const API_KEY = {
823
845
  } as const;
824
846
 
825
847
  /**
826
- * Shape constants for deploy tokens (`token-{64 hex chars}`).
848
+ * Shape constants for deploy tokens (`deploy-{64 hex chars}`).
827
849
  * Single source of truth used by validation utilities and auth middleware.
828
850
  */
829
851
  export const DEPLOY_TOKEN = {
830
852
  /** Prefix that identifies a deploy token. */
831
- PREFIX: 'token-',
853
+ PREFIX: 'deploy-',
832
854
  /** Number of hex characters following the prefix. */
833
855
  HEX_LENGTH: 64,
834
- /** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 70`). */
835
- TOTAL_LENGTH: 70,
856
+ /** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 71`). */
857
+ TOTAL_LENGTH: 71,
836
858
  } as const;
837
859
 
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'
860
+ /**
861
+ * Shape constants for caller identifiers (the `X-Caller` instance-identity
862
+ * header — rate-limit bucketing for multi-tenant orchestrators). The API
863
+ * normalizes case and silently ignores malformed values (the header is
864
+ * unauthenticated); clients validate at the boundary via `validateCaller`,
865
+ * so a value the server would drop fails fast instead.
866
+ */
867
+ export const CALLER = {
868
+ /** HTTP header name. */
869
+ HEADER: 'X-Caller',
870
+ /** Maximum identifier length. */
871
+ MAX_LENGTH: 128,
872
+ /** Allowed characters: alphanumeric, dot, underscore, hyphen. */
873
+ PATTERN: /^[a-zA-Z0-9._-]+$/,
846
874
  } as const;
847
875
 
848
- export type AuthMethodType = typeof AuthMethod[keyof typeof AuthMethod];
876
+ /**
877
+ * Token populations distinguishable by shape. The platform carries every
878
+ * client token in one wire slot (`Authorization: Bearer <value>`) and
879
+ * classifies by value, never by a side channel — this is the classifier.
880
+ *
881
+ * `API_KEY` and `DEPLOY_TOKEN` *are* `AuthMethod.API_KEY` and
882
+ * `AuthMethod.TOKEN` — the equality is structural, so a classification flows
883
+ * straight into an auth method and the pair can never drift. `OPAQUE` is any
884
+ * other value — shape says nothing about it, so only a lookup can. Today the
885
+ * server refuses every opaque bearer; the OAuth access-token population
886
+ * resolves there when the authorization server ships.
887
+ */
888
+ export const TokenKind = {
889
+ API_KEY: AuthMethod.API_KEY,
890
+ DEPLOY_TOKEN: AuthMethod.TOKEN,
891
+ OPAQUE: 'opaque',
892
+ } as const;
893
+
894
+ export type TokenKindType = typeof TokenKind[keyof typeof TokenKind];
895
+
896
+ /**
897
+ * Classify a client token by shape. The single dispatch used by both sides
898
+ * of the wire: API auth middleware (which population is this credential?)
899
+ * and SDK validation (which format rules apply before sending?). Sharing it
900
+ * is what guarantees client and server can never disagree on dispatch.
901
+ */
902
+ export function classifyToken(token: string): TokenKindType {
903
+ if (token.startsWith(API_KEY.PREFIX)) return TokenKind.API_KEY;
904
+ if (token.startsWith(DEPLOY_TOKEN.PREFIX)) return TokenKind.DEPLOY_TOKEN;
905
+ return TokenKind.OPAQUE;
906
+ }
849
907
 
850
908
  /**
851
909
  * OAuth scope vocabulary for delegated third-party access tokens.
@@ -868,7 +926,10 @@ export const OAuthScope = {
868
926
 
869
927
  export type OAuthScopeType = typeof OAuthScope[keyof typeof OAuthScope];
870
928
 
871
- // Deployment Configuration
929
+ // =============================================================================
930
+ // DEPLOYMENT CONFIGURATION CONSTANTS
931
+ // =============================================================================
932
+
872
933
  export const DEPLOYMENT_CONFIG_FILENAME = 'ship.json';
873
934
 
874
935
  /** Default ship.json config for SPA routing. Single source of truth — used by both API and SDK. */
@@ -879,38 +940,70 @@ export const SPA_DEFAULT_CONFIG = { rewrites: [{ source: '/(.*)', destination: '
879
940
  // =============================================================================
880
941
 
881
942
  /**
882
- * Validate API key format
943
+ * Shared rule for prefixed credentials: `{PREFIX}{HEX_LENGTH hex chars}`.
944
+ * The regex derives from the shape constants, so the validators can never
945
+ * drift from the shapes `classifyToken` dispatches on.
883
946
  */
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}"`);
947
+ function validatePrefixedCredential(
948
+ value: string,
949
+ shape: { PREFIX: string; HEX_LENGTH: number; TOTAL_LENGTH: number },
950
+ label: string
951
+ ): void {
952
+ if (!value.startsWith(shape.PREFIX)) {
953
+ throw ShipError.validation(`${label} must start with "${shape.PREFIX}"`);
887
954
  }
888
955
 
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)`);
956
+ if (value.length !== shape.TOTAL_LENGTH) {
957
+ throw ShipError.validation(`${label} must be ${shape.TOTAL_LENGTH} characters total (${shape.PREFIX} + ${shape.HEX_LENGTH} hex chars)`);
891
958
  }
892
959
 
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`);
960
+ const hexPart = value.slice(shape.PREFIX.length);
961
+ if (!new RegExp(`^[a-f0-9]{${shape.HEX_LENGTH}}$`, 'i').test(hexPart)) {
962
+ throw ShipError.validation(`${label} must contain ${shape.HEX_LENGTH} hexadecimal characters after "${shape.PREFIX}" prefix`);
896
963
  }
897
964
  }
898
965
 
966
+ /**
967
+ * Validate API key format
968
+ */
969
+ export function validateApiKey(apiKey: string): void {
970
+ validatePrefixedCredential(apiKey, API_KEY, 'API key');
971
+ }
972
+
899
973
  /**
900
974
  * Validate deploy token format
901
975
  */
902
976
  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
- }
977
+ validatePrefixedCredential(deployToken, DEPLOY_TOKEN, 'Deploy token');
978
+ }
906
979
 
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)`);
980
+ /**
981
+ * Validate a client token of any population. Classifies by shape and applies
982
+ * the matching format rules: `ship-` keys and `deploy-` deploy tokens are
983
+ * validated strictly; opaque tokens (OAuth access tokens, future populations)
984
+ * only need to be non-empty — their validity is the server's to decide.
985
+ */
986
+ export function validateToken(token: string): void {
987
+ switch (classifyToken(token)) {
988
+ case TokenKind.API_KEY:
989
+ return validateApiKey(token);
990
+ case TokenKind.DEPLOY_TOKEN:
991
+ return validateDeployToken(token);
992
+ case TokenKind.OPAQUE:
993
+ if (!token) throw ShipError.validation('Token must be a non-empty string');
909
994
  }
995
+ }
910
996
 
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`);
997
+ /**
998
+ * Validate a caller identifier against the `CALLER` shape. The server
999
+ * silently ignores malformed values (the header is unauthenticated); clients
1000
+ * call this at configuration time so the drop never silently happens.
1001
+ */
1002
+ export function validateCaller(caller: string): void {
1003
+ if (!caller || caller.length > CALLER.MAX_LENGTH || !CALLER.PATTERN.test(caller)) {
1004
+ throw ShipError.validation(
1005
+ `Caller must be 1-${CALLER.MAX_LENGTH} characters: letters, digits, dots, underscores, or hyphens`
1006
+ );
914
1007
  }
915
1008
  }
916
1009
 
@@ -1011,29 +1104,6 @@ export interface StaticFile {
1011
1104
  size: number;
1012
1105
  }
1013
1106
 
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
1107
  // =============================================================================
1038
1108
  // PROGRESS TRACKING
1039
1109
  // =============================================================================
@@ -1102,6 +1172,8 @@ export interface DeploymentUploadOptions {
1102
1172
  prerender?: boolean;
1103
1173
  /** @internal Trigger server-side SPA detection. Only available via /upload endpoint. */
1104
1174
  spa?: boolean;
1175
+ /** @internal reCAPTCHA proof for the anonymous human deploy channel. Only available via /upload endpoint. */
1176
+ captcha?: string;
1105
1177
  }
1106
1178
 
1107
1179
  /**
@@ -1134,7 +1206,7 @@ export interface DomainResource {
1134
1206
  * Account resource interface - the contract all implementations must follow
1135
1207
  */
1136
1208
  export interface AccountResource {
1137
- get: () => Promise<Account>;
1209
+ get: () => Promise<AccountGetResponse>;
1138
1210
  }
1139
1211
 
1140
1212
  /**