@shipstatic/types 2.2.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 +8 -8
- package/dist/index.js +67 -28
- package/package.json +6 -3
- package/src/index.ts +99 -55
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
|
*/
|
|
@@ -536,7 +536,7 @@ export declare const AuthMethod: {
|
|
|
536
536
|
readonly WEBHOOK: "webhook";
|
|
537
537
|
readonly SYSTEM: "system";
|
|
538
538
|
};
|
|
539
|
-
export type AuthMethodType = typeof AuthMethod[keyof typeof AuthMethod];
|
|
539
|
+
export type AuthMethodType = (typeof AuthMethod)[keyof typeof AuthMethod];
|
|
540
540
|
/**
|
|
541
541
|
* Shape constants for API keys (`ship-{64 hex chars}`).
|
|
542
542
|
* Single source of truth used by validation utilities and auth middleware.
|
|
@@ -595,7 +595,7 @@ export declare const TokenKind: {
|
|
|
595
595
|
readonly DEPLOY_TOKEN: "token";
|
|
596
596
|
readonly OPAQUE: "opaque";
|
|
597
597
|
};
|
|
598
|
-
export type TokenKindType = typeof TokenKind[keyof typeof TokenKind];
|
|
598
|
+
export type TokenKindType = (typeof TokenKind)[keyof typeof TokenKind];
|
|
599
599
|
/**
|
|
600
600
|
* Classify a client token by shape. The single dispatch used by both sides
|
|
601
601
|
* of the wire: API auth middleware (which population is this credential?)
|
|
@@ -621,7 +621,7 @@ export declare const OAuthScope: {
|
|
|
621
621
|
readonly DOMAINS_READ: "domains:read";
|
|
622
622
|
readonly DOMAINS_WRITE: "domains:write";
|
|
623
623
|
};
|
|
624
|
-
export type OAuthScopeType = typeof OAuthScope[keyof typeof OAuthScope];
|
|
624
|
+
export type OAuthScopeType = (typeof OAuthScope)[keyof typeof OAuthScope];
|
|
625
625
|
export declare const DEPLOYMENT_CONFIG_FILENAME = "ship.json";
|
|
626
626
|
/** Default ship.json config for SPA routing. Single source of truth — used by both API and SDK. */
|
|
627
627
|
export declare const SPA_DEFAULT_CONFIG: {
|
|
@@ -924,7 +924,7 @@ export declare const FileValidationStatus: {
|
|
|
924
924
|
/** File passed validation and is ready for deployment */
|
|
925
925
|
readonly READY: "ready";
|
|
926
926
|
};
|
|
927
|
-
export type FileValidationStatusType = typeof FileValidationStatus[keyof typeof FileValidationStatus];
|
|
927
|
+
export type FileValidationStatusType = (typeof FileValidationStatus)[keyof typeof FileValidationStatus];
|
|
928
928
|
/**
|
|
929
929
|
* A validation issue with a display-ready message
|
|
930
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([
|
|
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,7 +450,7 @@ 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
|
|
@@ -440,7 +475,7 @@ export const AuthMethod = {
|
|
|
440
475
|
AGENT: 'agent',
|
|
441
476
|
OAUTH: 'oauth',
|
|
442
477
|
WEBHOOK: 'webhook',
|
|
443
|
-
SYSTEM: 'system'
|
|
478
|
+
SYSTEM: 'system',
|
|
444
479
|
};
|
|
445
480
|
/**
|
|
446
481
|
* Shape constants for API keys (`ship-{64 hex chars}`).
|
|
@@ -536,7 +571,9 @@ export const OAuthScope = {
|
|
|
536
571
|
// =============================================================================
|
|
537
572
|
export const DEPLOYMENT_CONFIG_FILENAME = 'ship.json';
|
|
538
573
|
/** Default ship.json config for SPA routing. Single source of truth — used by both API and SDK. */
|
|
539
|
-
export const SPA_DEFAULT_CONFIG = {
|
|
574
|
+
export const SPA_DEFAULT_CONFIG = {
|
|
575
|
+
rewrites: [{ source: '/(.*)', destination: '/index.html' }],
|
|
576
|
+
};
|
|
540
577
|
// =============================================================================
|
|
541
578
|
// VALIDATION UTILITIES
|
|
542
579
|
// =============================================================================
|
|
@@ -578,9 +615,11 @@ export function validateDeployToken(deployToken) {
|
|
|
578
615
|
export function validateToken(token) {
|
|
579
616
|
switch (classifyToken(token)) {
|
|
580
617
|
case TokenKind.API_KEY:
|
|
581
|
-
|
|
618
|
+
validateApiKey(token);
|
|
619
|
+
return;
|
|
582
620
|
case TokenKind.DEPLOY_TOKEN:
|
|
583
|
-
|
|
621
|
+
validateDeployToken(token);
|
|
622
|
+
return;
|
|
584
623
|
case TokenKind.OPAQUE:
|
|
585
624
|
if (!token)
|
|
586
625
|
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.2.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
|
|
@@ -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
|
// =============================================================================
|
|
@@ -824,10 +855,10 @@ export const AuthMethod = {
|
|
|
824
855
|
AGENT: 'agent',
|
|
825
856
|
OAUTH: 'oauth',
|
|
826
857
|
WEBHOOK: 'webhook',
|
|
827
|
-
SYSTEM: 'system'
|
|
858
|
+
SYSTEM: 'system',
|
|
828
859
|
} as const;
|
|
829
860
|
|
|
830
|
-
export type AuthMethodType = typeof AuthMethod[keyof typeof AuthMethod];
|
|
861
|
+
export type AuthMethodType = (typeof AuthMethod)[keyof typeof AuthMethod];
|
|
831
862
|
|
|
832
863
|
/**
|
|
833
864
|
* Shape constants for API keys (`ship-{64 hex chars}`).
|
|
@@ -891,7 +922,7 @@ export const TokenKind = {
|
|
|
891
922
|
OPAQUE: 'opaque',
|
|
892
923
|
} as const;
|
|
893
924
|
|
|
894
|
-
export type TokenKindType = typeof TokenKind[keyof typeof TokenKind];
|
|
925
|
+
export type TokenKindType = (typeof TokenKind)[keyof typeof TokenKind];
|
|
895
926
|
|
|
896
927
|
/**
|
|
897
928
|
* Classify a client token by shape. The single dispatch used by both sides
|
|
@@ -924,7 +955,7 @@ export const OAuthScope = {
|
|
|
924
955
|
DOMAINS_WRITE: 'domains:write',
|
|
925
956
|
} as const;
|
|
926
957
|
|
|
927
|
-
export type OAuthScopeType = typeof OAuthScope[keyof typeof OAuthScope];
|
|
958
|
+
export type OAuthScopeType = (typeof OAuthScope)[keyof typeof OAuthScope];
|
|
928
959
|
|
|
929
960
|
// =============================================================================
|
|
930
961
|
// DEPLOYMENT CONFIGURATION CONSTANTS
|
|
@@ -933,7 +964,9 @@ export type OAuthScopeType = typeof OAuthScope[keyof typeof OAuthScope];
|
|
|
933
964
|
export const DEPLOYMENT_CONFIG_FILENAME = 'ship.json';
|
|
934
965
|
|
|
935
966
|
/** Default ship.json config for SPA routing. Single source of truth — used by both API and SDK. */
|
|
936
|
-
export const SPA_DEFAULT_CONFIG = {
|
|
967
|
+
export const SPA_DEFAULT_CONFIG = {
|
|
968
|
+
rewrites: [{ source: '/(.*)', destination: '/index.html' }],
|
|
969
|
+
} as const;
|
|
937
970
|
|
|
938
971
|
// =============================================================================
|
|
939
972
|
// VALIDATION UTILITIES
|
|
@@ -947,19 +980,23 @@ export const SPA_DEFAULT_CONFIG = { rewrites: [{ source: '/(.*)', destination: '
|
|
|
947
980
|
function validatePrefixedCredential(
|
|
948
981
|
value: string,
|
|
949
982
|
shape: { PREFIX: string; HEX_LENGTH: number; TOTAL_LENGTH: number },
|
|
950
|
-
label: string
|
|
983
|
+
label: string,
|
|
951
984
|
): void {
|
|
952
985
|
if (!value.startsWith(shape.PREFIX)) {
|
|
953
986
|
throw ShipError.validation(`${label} must start with "${shape.PREFIX}"`);
|
|
954
987
|
}
|
|
955
988
|
|
|
956
989
|
if (value.length !== shape.TOTAL_LENGTH) {
|
|
957
|
-
throw ShipError.validation(
|
|
990
|
+
throw ShipError.validation(
|
|
991
|
+
`${label} must be ${shape.TOTAL_LENGTH} characters total (${shape.PREFIX} + ${shape.HEX_LENGTH} hex chars)`,
|
|
992
|
+
);
|
|
958
993
|
}
|
|
959
994
|
|
|
960
995
|
const hexPart = value.slice(shape.PREFIX.length);
|
|
961
996
|
if (!new RegExp(`^[a-f0-9]{${shape.HEX_LENGTH}}$`, 'i').test(hexPart)) {
|
|
962
|
-
throw ShipError.validation(
|
|
997
|
+
throw ShipError.validation(
|
|
998
|
+
`${label} must contain ${shape.HEX_LENGTH} hexadecimal characters after "${shape.PREFIX}" prefix`,
|
|
999
|
+
);
|
|
963
1000
|
}
|
|
964
1001
|
}
|
|
965
1002
|
|
|
@@ -986,9 +1023,11 @@ export function validateDeployToken(deployToken: string): void {
|
|
|
986
1023
|
export function validateToken(token: string): void {
|
|
987
1024
|
switch (classifyToken(token)) {
|
|
988
1025
|
case TokenKind.API_KEY:
|
|
989
|
-
|
|
1026
|
+
validateApiKey(token);
|
|
1027
|
+
return;
|
|
990
1028
|
case TokenKind.DEPLOY_TOKEN:
|
|
991
|
-
|
|
1029
|
+
validateDeployToken(token);
|
|
1030
|
+
return;
|
|
992
1031
|
case TokenKind.OPAQUE:
|
|
993
1032
|
if (!token) throw ShipError.validation('Token must be a non-empty string');
|
|
994
1033
|
}
|
|
@@ -1002,7 +1041,7 @@ export function validateToken(token: string): void {
|
|
|
1002
1041
|
export function validateCaller(caller: string): void {
|
|
1003
1042
|
if (!caller || caller.length > CALLER.MAX_LENGTH || !CALLER.PATTERN.test(caller)) {
|
|
1004
1043
|
throw ShipError.validation(
|
|
1005
|
-
`Caller must be 1-${CALLER.MAX_LENGTH} characters: letters, digits, dots, underscores, or hyphens
|
|
1044
|
+
`Caller must be 1-${CALLER.MAX_LENGTH} characters: letters, digits, dots, underscores, or hyphens`,
|
|
1006
1045
|
);
|
|
1007
1046
|
}
|
|
1008
1047
|
}
|
|
@@ -1180,7 +1219,10 @@ export interface DeploymentUploadOptions {
|
|
|
1180
1219
|
* Deployment resource interface - the contract all implementations must follow
|
|
1181
1220
|
*/
|
|
1182
1221
|
export interface DeploymentResource {
|
|
1183
|
-
upload: (
|
|
1222
|
+
upload: (
|
|
1223
|
+
input: DeployInput,
|
|
1224
|
+
options?: DeploymentUploadOptions,
|
|
1225
|
+
) => Promise<DeploymentCreateResponse>;
|
|
1184
1226
|
list: () => Promise<DeploymentListResponse>;
|
|
1185
1227
|
get: (id: string) => Promise<Deployment>;
|
|
1186
1228
|
set: (id: string, options: { labels: string[] }) => Promise<Deployment>;
|
|
@@ -1191,7 +1233,10 @@ export interface DeploymentResource {
|
|
|
1191
1233
|
* Domain resource interface - the contract all implementations must follow
|
|
1192
1234
|
*/
|
|
1193
1235
|
export interface DomainResource {
|
|
1194
|
-
set: (
|
|
1236
|
+
set: (
|
|
1237
|
+
name: string,
|
|
1238
|
+
options?: { deployment?: string; labels?: string[] },
|
|
1239
|
+
) => Promise<DomainSetResult>;
|
|
1195
1240
|
list: () => Promise<DomainListResponse>;
|
|
1196
1241
|
get: (name: string) => Promise<Domain>;
|
|
1197
1242
|
remove: (name: string) => Promise<void>;
|
|
@@ -1241,7 +1286,6 @@ export interface BillingStatus {
|
|
|
1241
1286
|
portal: string | null;
|
|
1242
1287
|
}
|
|
1243
1288
|
|
|
1244
|
-
|
|
1245
1289
|
/**
|
|
1246
1290
|
* Checkout session response from POST /billing/checkout
|
|
1247
1291
|
*/
|
|
@@ -1305,9 +1349,9 @@ export type ActivityEvent =
|
|
|
1305
1349
|
| 'refund.created'
|
|
1306
1350
|
| 'dispute.created'
|
|
1307
1351
|
// Billing operational events (admin/debug only, not user-visible)
|
|
1308
|
-
| 'billing.sync'
|
|
1309
|
-
| 'billing.stale'
|
|
1310
|
-
| 'billing.race';
|
|
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
|
|
1311
1355
|
|
|
1312
1356
|
/**
|
|
1313
1357
|
* Activity events visible to users in the dashboard
|
|
@@ -1416,8 +1460,8 @@ export const FileValidationStatus = {
|
|
|
1416
1460
|
READY: 'ready',
|
|
1417
1461
|
} as const;
|
|
1418
1462
|
|
|
1419
|
-
export type FileValidationStatusType =
|
|
1420
|
-
|
|
1463
|
+
export type FileValidationStatusType =
|
|
1464
|
+
(typeof FileValidationStatus)[keyof typeof FileValidationStatus];
|
|
1421
1465
|
|
|
1422
1466
|
/**
|
|
1423
1467
|
* A validation issue with a display-ready message
|
|
@@ -1660,4 +1704,4 @@ export function validatePassword(value: unknown): string | undefined {
|
|
|
1660
1704
|
);
|
|
1661
1705
|
}
|
|
1662
1706
|
return trimmed;
|
|
1663
|
-
}
|
|
1707
|
+
}
|