@shipstatic/types 2.2.0 → 2.3.0-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 +16 -8
- package/dist/index.js +80 -32
- package/package.json +6 -3
- package/src/index.ts +113 -59
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
|
*/
|
|
@@ -229,7 +229,7 @@ export declare const AccountPlan: {
|
|
|
229
229
|
readonly TERMINATING: "terminating";
|
|
230
230
|
readonly TERMINATED: "terminated";
|
|
231
231
|
};
|
|
232
|
-
export type AccountPlanType = typeof AccountPlan[keyof typeof AccountPlan];
|
|
232
|
+
export type AccountPlanType = (typeof AccountPlan)[keyof typeof AccountPlan];
|
|
233
233
|
/**
|
|
234
234
|
* Account usage metrics — always available regardless of billing provider.
|
|
235
235
|
*/
|
|
@@ -324,7 +324,7 @@ export declare const ErrorType: {
|
|
|
324
324
|
/** Configuration error. Client-side only — set by SDK during config parsing/validation; never produced server-side. */
|
|
325
325
|
readonly Config: "config_error";
|
|
326
326
|
};
|
|
327
|
-
export type ErrorType = typeof ErrorType[keyof typeof ErrorType];
|
|
327
|
+
export type ErrorType = (typeof ErrorType)[keyof typeof ErrorType];
|
|
328
328
|
/**
|
|
329
329
|
* Standard error response format used everywhere
|
|
330
330
|
*/
|
|
@@ -518,6 +518,14 @@ export interface PingResponse {
|
|
|
518
518
|
/** Optional timestamp */
|
|
519
519
|
timestamp?: number;
|
|
520
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* Where human identity is mounted on the API host. The API mounts Better
|
|
523
|
+
* Auth at this path (sign-in, sign-out, session reads, admin impersonation)
|
|
524
|
+
* and the web console's auth client posts to it — shared here so the two
|
|
525
|
+
* halves of the auth pair agree by construction, the same way both sides
|
|
526
|
+
* already share the credential prefixes below.
|
|
527
|
+
*/
|
|
528
|
+
export declare const AUTH_BASE_PATH = "/auth";
|
|
521
529
|
/**
|
|
522
530
|
* How a request (or recorded activity) was authorized.
|
|
523
531
|
*
|
|
@@ -536,7 +544,7 @@ export declare const AuthMethod: {
|
|
|
536
544
|
readonly WEBHOOK: "webhook";
|
|
537
545
|
readonly SYSTEM: "system";
|
|
538
546
|
};
|
|
539
|
-
export type AuthMethodType = typeof AuthMethod[keyof typeof AuthMethod];
|
|
547
|
+
export type AuthMethodType = (typeof AuthMethod)[keyof typeof AuthMethod];
|
|
540
548
|
/**
|
|
541
549
|
* Shape constants for API keys (`ship-{64 hex chars}`).
|
|
542
550
|
* Single source of truth used by validation utilities and auth middleware.
|
|
@@ -595,7 +603,7 @@ export declare const TokenKind: {
|
|
|
595
603
|
readonly DEPLOY_TOKEN: "token";
|
|
596
604
|
readonly OPAQUE: "opaque";
|
|
597
605
|
};
|
|
598
|
-
export type TokenKindType = typeof TokenKind[keyof typeof TokenKind];
|
|
606
|
+
export type TokenKindType = (typeof TokenKind)[keyof typeof TokenKind];
|
|
599
607
|
/**
|
|
600
608
|
* Classify a client token by shape. The single dispatch used by both sides
|
|
601
609
|
* of the wire: API auth middleware (which population is this credential?)
|
|
@@ -621,7 +629,7 @@ export declare const OAuthScope: {
|
|
|
621
629
|
readonly DOMAINS_READ: "domains:read";
|
|
622
630
|
readonly DOMAINS_WRITE: "domains:write";
|
|
623
631
|
};
|
|
624
|
-
export type OAuthScopeType = typeof OAuthScope[keyof typeof OAuthScope];
|
|
632
|
+
export type OAuthScopeType = (typeof OAuthScope)[keyof typeof OAuthScope];
|
|
625
633
|
export declare const DEPLOYMENT_CONFIG_FILENAME = "ship.json";
|
|
626
634
|
/** Default ship.json config for SPA routing. Single source of truth — used by both API and SDK. */
|
|
627
635
|
export declare const SPA_DEFAULT_CONFIG: {
|
|
@@ -924,7 +932,7 @@ export declare const FileValidationStatus: {
|
|
|
924
932
|
/** File passed validation and is ready for deployment */
|
|
925
933
|
readonly READY: "ready";
|
|
926
934
|
};
|
|
927
|
-
export type FileValidationStatusType = typeof FileValidationStatus[keyof typeof FileValidationStatus];
|
|
935
|
+
export type FileValidationStatusType = (typeof FileValidationStatus)[keyof typeof FileValidationStatus];
|
|
928
936
|
/**
|
|
929
937
|
* A validation issue with a display-ready message
|
|
930
938
|
*
|
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([
|
|
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 ??
|
|
198
|
-
response.status ===
|
|
199
|
-
|
|
200
|
-
|
|
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 =
|
|
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 =
|
|
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',
|
|
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',
|
|
355
|
+
'pkg',
|
|
356
|
+
'mpkg',
|
|
338
357
|
// Disk images
|
|
339
|
-
'dmg',
|
|
358
|
+
'dmg',
|
|
359
|
+
'iso',
|
|
360
|
+
'img',
|
|
340
361
|
// Malware vectors
|
|
341
|
-
'cab',
|
|
362
|
+
'cab',
|
|
363
|
+
'cpl',
|
|
364
|
+
'chm',
|
|
342
365
|
// Dangerous scripts
|
|
343
|
-
'ps1',
|
|
366
|
+
'ps1',
|
|
367
|
+
'vbs',
|
|
368
|
+
'vbe',
|
|
369
|
+
'ws',
|
|
370
|
+
'wsf',
|
|
371
|
+
'wsc',
|
|
372
|
+
'wsh',
|
|
373
|
+
'reg',
|
|
344
374
|
// Java
|
|
345
|
-
'jar',
|
|
375
|
+
'jar',
|
|
376
|
+
'jnlp',
|
|
346
377
|
// Mobile/browser packages
|
|
347
|
-
'apk',
|
|
378
|
+
'apk',
|
|
379
|
+
'crx',
|
|
348
380
|
// Shortcut/link
|
|
349
|
-
'lnk',
|
|
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,15 +450,24 @@ 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
|
}
|
|
420
455
|
// =============================================================================
|
|
421
456
|
// CREDENTIAL SHAPES
|
|
422
457
|
// =============================================================================
|
|
423
|
-
// The one address for credential vocabulary:
|
|
424
|
-
// (
|
|
425
|
-
// (API_KEY, DEPLOY_TOKEN, CALLER),
|
|
426
|
-
// classifyToken), and the
|
|
458
|
+
// The one address for credential vocabulary: where human identity lives
|
|
459
|
+
// (AUTH_BASE_PATH), how a request is authorized (AuthMethod), the shapes
|
|
460
|
+
// that distinguish populations on the wire (API_KEY, DEPLOY_TOKEN, CALLER),
|
|
461
|
+
// the single dispatch over them (TokenKind, classifyToken), and the
|
|
462
|
+
// delegated-access scopes (OAuthScope).
|
|
463
|
+
/**
|
|
464
|
+
* Where human identity is mounted on the API host. The API mounts Better
|
|
465
|
+
* Auth at this path (sign-in, sign-out, session reads, admin impersonation)
|
|
466
|
+
* and the web console's auth client posts to it — shared here so the two
|
|
467
|
+
* halves of the auth pair agree by construction, the same way both sides
|
|
468
|
+
* already share the credential prefixes below.
|
|
469
|
+
*/
|
|
470
|
+
export const AUTH_BASE_PATH = '/auth';
|
|
427
471
|
/**
|
|
428
472
|
* How a request (or recorded activity) was authorized.
|
|
429
473
|
*
|
|
@@ -440,7 +484,7 @@ export const AuthMethod = {
|
|
|
440
484
|
AGENT: 'agent',
|
|
441
485
|
OAUTH: 'oauth',
|
|
442
486
|
WEBHOOK: 'webhook',
|
|
443
|
-
SYSTEM: 'system'
|
|
487
|
+
SYSTEM: 'system',
|
|
444
488
|
};
|
|
445
489
|
/**
|
|
446
490
|
* Shape constants for API keys (`ship-{64 hex chars}`).
|
|
@@ -536,7 +580,9 @@ export const OAuthScope = {
|
|
|
536
580
|
// =============================================================================
|
|
537
581
|
export const DEPLOYMENT_CONFIG_FILENAME = 'ship.json';
|
|
538
582
|
/** Default ship.json config for SPA routing. Single source of truth — used by both API and SDK. */
|
|
539
|
-
export const SPA_DEFAULT_CONFIG = {
|
|
583
|
+
export const SPA_DEFAULT_CONFIG = {
|
|
584
|
+
rewrites: [{ source: '/(.*)', destination: '/index.html' }],
|
|
585
|
+
};
|
|
540
586
|
// =============================================================================
|
|
541
587
|
// VALIDATION UTILITIES
|
|
542
588
|
// =============================================================================
|
|
@@ -578,9 +624,11 @@ export function validateDeployToken(deployToken) {
|
|
|
578
624
|
export function validateToken(token) {
|
|
579
625
|
switch (classifyToken(token)) {
|
|
580
626
|
case TokenKind.API_KEY:
|
|
581
|
-
|
|
627
|
+
validateApiKey(token);
|
|
628
|
+
return;
|
|
582
629
|
case TokenKind.DEPLOY_TOKEN:
|
|
583
|
-
|
|
630
|
+
validateDeployToken(token);
|
|
631
|
+
return;
|
|
584
632
|
case TokenKind.OPAQUE:
|
|
585
633
|
if (!token)
|
|
586
634
|
throw ShipError.validation('Token must be a non-empty string');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipstatic/types",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0-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
|
|
@@ -262,10 +261,10 @@ export const AccountPlan = {
|
|
|
262
261
|
ENTERPRISE: 'enterprise',
|
|
263
262
|
SUSPENDED: 'suspended',
|
|
264
263
|
TERMINATING: 'terminating',
|
|
265
|
-
TERMINATED: 'terminated'
|
|
264
|
+
TERMINATED: 'terminated',
|
|
266
265
|
} as const;
|
|
267
266
|
|
|
268
|
-
export type AccountPlanType = typeof AccountPlan[keyof typeof AccountPlan];
|
|
267
|
+
export type AccountPlanType = (typeof AccountPlan)[keyof typeof AccountPlan];
|
|
269
268
|
|
|
270
269
|
/**
|
|
271
270
|
* Account usage metrics — always available regardless of billing provider.
|
|
@@ -370,7 +369,7 @@ export const ErrorType = {
|
|
|
370
369
|
Config: 'config_error',
|
|
371
370
|
} as const;
|
|
372
371
|
|
|
373
|
-
export type ErrorType = typeof ErrorType[keyof typeof ErrorType];
|
|
372
|
+
export type ErrorType = (typeof ErrorType)[keyof typeof ErrorType];
|
|
374
373
|
|
|
375
374
|
/**
|
|
376
375
|
* Error types that originate exclusively on the client (HTTP clients, SDK
|
|
@@ -391,7 +390,13 @@ const CLIENT_ONLY_ERROR_TYPES = new Set<string>([
|
|
|
391
390
|
* union so `.has(error.type)` accepts any value from the union.
|
|
392
391
|
*/
|
|
393
392
|
const ERROR_CATEGORIES = {
|
|
394
|
-
client: new Set<ErrorType>([
|
|
393
|
+
client: new Set<ErrorType>([
|
|
394
|
+
ErrorType.Business,
|
|
395
|
+
ErrorType.Config,
|
|
396
|
+
ErrorType.File,
|
|
397
|
+
ErrorType.Forbidden,
|
|
398
|
+
ErrorType.Validation,
|
|
399
|
+
]),
|
|
395
400
|
network: new Set<ErrorType>([ErrorType.Network]),
|
|
396
401
|
auth: new Set<ErrorType>([ErrorType.Authentication]),
|
|
397
402
|
} as const;
|
|
@@ -404,7 +409,7 @@ const ERROR_CATEGORIES = {
|
|
|
404
409
|
* `ErrorType` is automatically picked up.
|
|
405
410
|
*/
|
|
406
411
|
const SERVER_PRODUCIBLE_ERROR_TYPES = new Set<string>(
|
|
407
|
-
Object.values(ErrorType).filter(t => !CLIENT_ONLY_ERROR_TYPES.has(t)),
|
|
412
|
+
Object.values(ErrorType).filter((t) => !CLIENT_ONLY_ERROR_TYPES.has(t)),
|
|
408
413
|
);
|
|
409
414
|
|
|
410
415
|
/**
|
|
@@ -441,15 +446,14 @@ export class ShipError extends Error {
|
|
|
441
446
|
// tag (see `ShipError.authentication` JSDoc) — these are server-side
|
|
442
447
|
// diagnostics like 'session_invalid' that must not leak to clients.
|
|
443
448
|
const authDetails = this.details as { internal?: unknown } | undefined;
|
|
444
|
-
const details =
|
|
445
|
-
? undefined
|
|
446
|
-
: this.details;
|
|
449
|
+
const details =
|
|
450
|
+
this.type === ErrorType.Authentication && authDetails?.internal ? undefined : this.details;
|
|
447
451
|
|
|
448
452
|
return {
|
|
449
453
|
error: this.type,
|
|
450
454
|
message: this.message,
|
|
451
455
|
status: this.status,
|
|
452
|
-
details
|
|
456
|
+
details,
|
|
453
457
|
};
|
|
454
458
|
}
|
|
455
459
|
|
|
@@ -475,10 +479,7 @@ export class ShipError extends Error {
|
|
|
475
479
|
* Async because it reads the response body. Returns rather than throws so
|
|
476
480
|
* callers can compose; most will `throw await ShipError.fromHttpResponse(...)`.
|
|
477
481
|
*/
|
|
478
|
-
static async fromHttpResponse(
|
|
479
|
-
response: Response,
|
|
480
|
-
operationName?: string,
|
|
481
|
-
): Promise<ShipError> {
|
|
482
|
+
static async fromHttpResponse(response: Response, operationName?: string): Promise<ShipError> {
|
|
482
483
|
let message: string | undefined;
|
|
483
484
|
let details: unknown;
|
|
484
485
|
let bodyType: ErrorType | undefined;
|
|
@@ -506,12 +507,15 @@ export class ShipError extends Error {
|
|
|
506
507
|
|
|
507
508
|
message = message || `${operationName || 'Request'} failed with status ${response.status}`;
|
|
508
509
|
|
|
509
|
-
const type =
|
|
510
|
-
|
|
511
|
-
response.status ===
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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);
|
|
515
519
|
|
|
516
520
|
return new ShipError(type, message, response.status, details);
|
|
517
521
|
}
|
|
@@ -570,7 +574,7 @@ export class ShipError extends Error {
|
|
|
570
574
|
return new ShipError(ErrorType.Forbidden, message, 403, details);
|
|
571
575
|
}
|
|
572
576
|
|
|
573
|
-
static rateLimit(message: string =
|
|
577
|
+
static rateLimit(message: string = 'Too many requests', details?: unknown): ShipError {
|
|
574
578
|
return new ShipError(ErrorType.RateLimit, message, 429, details);
|
|
575
579
|
}
|
|
576
580
|
|
|
@@ -587,7 +591,7 @@ export class ShipError extends Error {
|
|
|
587
591
|
* Use this pattern in API auth code; do not put client-visible info under
|
|
588
592
|
* `internal`. Other `details` keys round-trip normally.
|
|
589
593
|
*/
|
|
590
|
-
static authentication(message: string =
|
|
594
|
+
static authentication(message: string = 'Authentication required', details?: unknown): ShipError {
|
|
591
595
|
return new ShipError(ErrorType.Authentication, message, 401, details);
|
|
592
596
|
}
|
|
593
597
|
|
|
@@ -694,21 +698,47 @@ export interface PlatformLimits {
|
|
|
694
698
|
*/
|
|
695
699
|
export const BLOCKED_EXTENSIONS: ReadonlySet<string> = new Set([
|
|
696
700
|
// Executables
|
|
697
|
-
'exe',
|
|
701
|
+
'exe',
|
|
702
|
+
'msi',
|
|
703
|
+
'dll',
|
|
704
|
+
'scr',
|
|
705
|
+
'bat',
|
|
706
|
+
'cmd',
|
|
707
|
+
'com',
|
|
708
|
+
'pif',
|
|
709
|
+
'app',
|
|
710
|
+
'deb',
|
|
711
|
+
'rpm',
|
|
698
712
|
// Installers
|
|
699
|
-
'pkg',
|
|
713
|
+
'pkg',
|
|
714
|
+
'mpkg',
|
|
700
715
|
// Disk images
|
|
701
|
-
'dmg',
|
|
716
|
+
'dmg',
|
|
717
|
+
'iso',
|
|
718
|
+
'img',
|
|
702
719
|
// Malware vectors
|
|
703
|
-
'cab',
|
|
720
|
+
'cab',
|
|
721
|
+
'cpl',
|
|
722
|
+
'chm',
|
|
704
723
|
// Dangerous scripts
|
|
705
|
-
'ps1',
|
|
724
|
+
'ps1',
|
|
725
|
+
'vbs',
|
|
726
|
+
'vbe',
|
|
727
|
+
'ws',
|
|
728
|
+
'wsf',
|
|
729
|
+
'wsc',
|
|
730
|
+
'wsh',
|
|
731
|
+
'reg',
|
|
706
732
|
// Java
|
|
707
|
-
'jar',
|
|
733
|
+
'jar',
|
|
734
|
+
'jnlp',
|
|
708
735
|
// Mobile/browser packages
|
|
709
|
-
'apk',
|
|
736
|
+
'apk',
|
|
737
|
+
'crx',
|
|
710
738
|
// Shortcut/link
|
|
711
|
-
'lnk',
|
|
739
|
+
'lnk',
|
|
740
|
+
'inf',
|
|
741
|
+
'hta',
|
|
712
742
|
]);
|
|
713
743
|
|
|
714
744
|
/**
|
|
@@ -745,6 +775,7 @@ export function isBlockedExtension(filename: string): boolean {
|
|
|
745
775
|
*
|
|
746
776
|
* Everything else is allowed — browser percent-encodes, Worker decodes, R2 matches.
|
|
747
777
|
*/
|
|
778
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: blocking control characters is this regex's purpose
|
|
748
779
|
export const UNSAFE_FILENAME_CHARS = /[\x00-\x1f\x7f#?%\\<>"]/;
|
|
749
780
|
|
|
750
781
|
/**
|
|
@@ -783,7 +814,7 @@ export const UNBUILT_PROJECT_MARKERS: ReadonlySet<string> = new Set([
|
|
|
783
814
|
*/
|
|
784
815
|
export function hasUnbuiltMarker(filePath: string): boolean {
|
|
785
816
|
const segments = filePath.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
786
|
-
return segments.some(s => UNBUILT_PROJECT_MARKERS.has(s));
|
|
817
|
+
return segments.some((s) => UNBUILT_PROJECT_MARKERS.has(s));
|
|
787
818
|
}
|
|
788
819
|
|
|
789
820
|
// =============================================================================
|
|
@@ -803,10 +834,20 @@ export interface PingResponse {
|
|
|
803
834
|
// =============================================================================
|
|
804
835
|
// CREDENTIAL SHAPES
|
|
805
836
|
// =============================================================================
|
|
806
|
-
// The one address for credential vocabulary:
|
|
807
|
-
// (
|
|
808
|
-
// (API_KEY, DEPLOY_TOKEN, CALLER),
|
|
809
|
-
// classifyToken), and the
|
|
837
|
+
// The one address for credential vocabulary: where human identity lives
|
|
838
|
+
// (AUTH_BASE_PATH), how a request is authorized (AuthMethod), the shapes
|
|
839
|
+
// that distinguish populations on the wire (API_KEY, DEPLOY_TOKEN, CALLER),
|
|
840
|
+
// the single dispatch over them (TokenKind, classifyToken), and the
|
|
841
|
+
// delegated-access scopes (OAuthScope).
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Where human identity is mounted on the API host. The API mounts Better
|
|
845
|
+
* Auth at this path (sign-in, sign-out, session reads, admin impersonation)
|
|
846
|
+
* and the web console's auth client posts to it — shared here so the two
|
|
847
|
+
* halves of the auth pair agree by construction, the same way both sides
|
|
848
|
+
* already share the credential prefixes below.
|
|
849
|
+
*/
|
|
850
|
+
export const AUTH_BASE_PATH = '/auth';
|
|
810
851
|
|
|
811
852
|
/**
|
|
812
853
|
* How a request (or recorded activity) was authorized.
|
|
@@ -824,10 +865,10 @@ export const AuthMethod = {
|
|
|
824
865
|
AGENT: 'agent',
|
|
825
866
|
OAUTH: 'oauth',
|
|
826
867
|
WEBHOOK: 'webhook',
|
|
827
|
-
SYSTEM: 'system'
|
|
868
|
+
SYSTEM: 'system',
|
|
828
869
|
} as const;
|
|
829
870
|
|
|
830
|
-
export type AuthMethodType = typeof AuthMethod[keyof typeof AuthMethod];
|
|
871
|
+
export type AuthMethodType = (typeof AuthMethod)[keyof typeof AuthMethod];
|
|
831
872
|
|
|
832
873
|
/**
|
|
833
874
|
* Shape constants for API keys (`ship-{64 hex chars}`).
|
|
@@ -891,7 +932,7 @@ export const TokenKind = {
|
|
|
891
932
|
OPAQUE: 'opaque',
|
|
892
933
|
} as const;
|
|
893
934
|
|
|
894
|
-
export type TokenKindType = typeof TokenKind[keyof typeof TokenKind];
|
|
935
|
+
export type TokenKindType = (typeof TokenKind)[keyof typeof TokenKind];
|
|
895
936
|
|
|
896
937
|
/**
|
|
897
938
|
* Classify a client token by shape. The single dispatch used by both sides
|
|
@@ -924,7 +965,7 @@ export const OAuthScope = {
|
|
|
924
965
|
DOMAINS_WRITE: 'domains:write',
|
|
925
966
|
} as const;
|
|
926
967
|
|
|
927
|
-
export type OAuthScopeType = typeof OAuthScope[keyof typeof OAuthScope];
|
|
968
|
+
export type OAuthScopeType = (typeof OAuthScope)[keyof typeof OAuthScope];
|
|
928
969
|
|
|
929
970
|
// =============================================================================
|
|
930
971
|
// DEPLOYMENT CONFIGURATION CONSTANTS
|
|
@@ -933,7 +974,9 @@ export type OAuthScopeType = typeof OAuthScope[keyof typeof OAuthScope];
|
|
|
933
974
|
export const DEPLOYMENT_CONFIG_FILENAME = 'ship.json';
|
|
934
975
|
|
|
935
976
|
/** Default ship.json config for SPA routing. Single source of truth — used by both API and SDK. */
|
|
936
|
-
export const SPA_DEFAULT_CONFIG = {
|
|
977
|
+
export const SPA_DEFAULT_CONFIG = {
|
|
978
|
+
rewrites: [{ source: '/(.*)', destination: '/index.html' }],
|
|
979
|
+
} as const;
|
|
937
980
|
|
|
938
981
|
// =============================================================================
|
|
939
982
|
// VALIDATION UTILITIES
|
|
@@ -947,19 +990,23 @@ export const SPA_DEFAULT_CONFIG = { rewrites: [{ source: '/(.*)', destination: '
|
|
|
947
990
|
function validatePrefixedCredential(
|
|
948
991
|
value: string,
|
|
949
992
|
shape: { PREFIX: string; HEX_LENGTH: number; TOTAL_LENGTH: number },
|
|
950
|
-
label: string
|
|
993
|
+
label: string,
|
|
951
994
|
): void {
|
|
952
995
|
if (!value.startsWith(shape.PREFIX)) {
|
|
953
996
|
throw ShipError.validation(`${label} must start with "${shape.PREFIX}"`);
|
|
954
997
|
}
|
|
955
998
|
|
|
956
999
|
if (value.length !== shape.TOTAL_LENGTH) {
|
|
957
|
-
throw ShipError.validation(
|
|
1000
|
+
throw ShipError.validation(
|
|
1001
|
+
`${label} must be ${shape.TOTAL_LENGTH} characters total (${shape.PREFIX} + ${shape.HEX_LENGTH} hex chars)`,
|
|
1002
|
+
);
|
|
958
1003
|
}
|
|
959
1004
|
|
|
960
1005
|
const hexPart = value.slice(shape.PREFIX.length);
|
|
961
1006
|
if (!new RegExp(`^[a-f0-9]{${shape.HEX_LENGTH}}$`, 'i').test(hexPart)) {
|
|
962
|
-
throw ShipError.validation(
|
|
1007
|
+
throw ShipError.validation(
|
|
1008
|
+
`${label} must contain ${shape.HEX_LENGTH} hexadecimal characters after "${shape.PREFIX}" prefix`,
|
|
1009
|
+
);
|
|
963
1010
|
}
|
|
964
1011
|
}
|
|
965
1012
|
|
|
@@ -986,9 +1033,11 @@ export function validateDeployToken(deployToken: string): void {
|
|
|
986
1033
|
export function validateToken(token: string): void {
|
|
987
1034
|
switch (classifyToken(token)) {
|
|
988
1035
|
case TokenKind.API_KEY:
|
|
989
|
-
|
|
1036
|
+
validateApiKey(token);
|
|
1037
|
+
return;
|
|
990
1038
|
case TokenKind.DEPLOY_TOKEN:
|
|
991
|
-
|
|
1039
|
+
validateDeployToken(token);
|
|
1040
|
+
return;
|
|
992
1041
|
case TokenKind.OPAQUE:
|
|
993
1042
|
if (!token) throw ShipError.validation('Token must be a non-empty string');
|
|
994
1043
|
}
|
|
@@ -1002,7 +1051,7 @@ export function validateToken(token: string): void {
|
|
|
1002
1051
|
export function validateCaller(caller: string): void {
|
|
1003
1052
|
if (!caller || caller.length > CALLER.MAX_LENGTH || !CALLER.PATTERN.test(caller)) {
|
|
1004
1053
|
throw ShipError.validation(
|
|
1005
|
-
`Caller must be 1-${CALLER.MAX_LENGTH} characters: letters, digits, dots, underscores, or hyphens
|
|
1054
|
+
`Caller must be 1-${CALLER.MAX_LENGTH} characters: letters, digits, dots, underscores, or hyphens`,
|
|
1006
1055
|
);
|
|
1007
1056
|
}
|
|
1008
1057
|
}
|
|
@@ -1180,7 +1229,10 @@ export interface DeploymentUploadOptions {
|
|
|
1180
1229
|
* Deployment resource interface - the contract all implementations must follow
|
|
1181
1230
|
*/
|
|
1182
1231
|
export interface DeploymentResource {
|
|
1183
|
-
upload: (
|
|
1232
|
+
upload: (
|
|
1233
|
+
input: DeployInput,
|
|
1234
|
+
options?: DeploymentUploadOptions,
|
|
1235
|
+
) => Promise<DeploymentCreateResponse>;
|
|
1184
1236
|
list: () => Promise<DeploymentListResponse>;
|
|
1185
1237
|
get: (id: string) => Promise<Deployment>;
|
|
1186
1238
|
set: (id: string, options: { labels: string[] }) => Promise<Deployment>;
|
|
@@ -1191,7 +1243,10 @@ export interface DeploymentResource {
|
|
|
1191
1243
|
* Domain resource interface - the contract all implementations must follow
|
|
1192
1244
|
*/
|
|
1193
1245
|
export interface DomainResource {
|
|
1194
|
-
set: (
|
|
1246
|
+
set: (
|
|
1247
|
+
name: string,
|
|
1248
|
+
options?: { deployment?: string; labels?: string[] },
|
|
1249
|
+
) => Promise<DomainSetResult>;
|
|
1195
1250
|
list: () => Promise<DomainListResponse>;
|
|
1196
1251
|
get: (name: string) => Promise<Domain>;
|
|
1197
1252
|
remove: (name: string) => Promise<void>;
|
|
@@ -1241,7 +1296,6 @@ export interface BillingStatus {
|
|
|
1241
1296
|
portal: string | null;
|
|
1242
1297
|
}
|
|
1243
1298
|
|
|
1244
|
-
|
|
1245
1299
|
/**
|
|
1246
1300
|
* Checkout session response from POST /billing/checkout
|
|
1247
1301
|
*/
|
|
@@ -1305,9 +1359,9 @@ export type ActivityEvent =
|
|
|
1305
1359
|
| 'refund.created'
|
|
1306
1360
|
| 'dispute.created'
|
|
1307
1361
|
// Billing operational events (admin/debug only, not user-visible)
|
|
1308
|
-
| 'billing.sync'
|
|
1309
|
-
| 'billing.stale'
|
|
1310
|
-
| 'billing.race';
|
|
1362
|
+
| 'billing.sync' // Outbound: unit count pushed to payment provider
|
|
1363
|
+
| 'billing.stale' // Dropped: webhook predates last known state
|
|
1364
|
+
| 'billing.race'; // Dropped: concurrent webhook already updated state
|
|
1311
1365
|
|
|
1312
1366
|
/**
|
|
1313
1367
|
* Activity events visible to users in the dashboard
|
|
@@ -1416,8 +1470,8 @@ export const FileValidationStatus = {
|
|
|
1416
1470
|
READY: 'ready',
|
|
1417
1471
|
} as const;
|
|
1418
1472
|
|
|
1419
|
-
export type FileValidationStatusType =
|
|
1420
|
-
|
|
1473
|
+
export type FileValidationStatusType =
|
|
1474
|
+
(typeof FileValidationStatus)[keyof typeof FileValidationStatus];
|
|
1421
1475
|
|
|
1422
1476
|
/**
|
|
1423
1477
|
* A validation issue with a display-ready message
|
|
@@ -1660,4 +1714,4 @@ export function validatePassword(value: unknown): string | undefined {
|
|
|
1660
1714
|
);
|
|
1661
1715
|
}
|
|
1662
1716
|
return trimmed;
|
|
1663
|
-
}
|
|
1717
|
+
}
|