@shipstatic/types 2.5.0-beta.1 → 2.5.0-beta.10
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 +118 -35
- package/dist/index.js +70 -2
- package/package.json +14 -12
- package/src/index.ts +149 -37
package/dist/index.d.ts
CHANGED
|
@@ -49,16 +49,30 @@ export interface DeploymentCreateResponse extends Deployment {
|
|
|
49
49
|
/** Claim URL for public deployments. Present when deployed without credentials. */
|
|
50
50
|
readonly claim?: string;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The half of a list response that is identical on every list.
|
|
54
|
+
*
|
|
55
|
+
* `GET /<collection>` answers exactly two fields — the collection under its
|
|
56
|
+
* own plural noun, and this cursor — so the cursor is declared once here and
|
|
57
|
+
* each response below adds only its noun. `cursor: null` means last page and
|
|
58
|
+
* is the ENTIRE has-more signal, which is why there is no `has_more`.
|
|
59
|
+
*
|
|
60
|
+
* There is deliberately no `total`. A count is an aggregate over a
|
|
61
|
+
* collection, not a property of a page; producing one would cost a COUNT
|
|
62
|
+
* beside every page read, which is precisely what keyset pagination exists
|
|
63
|
+
* to avoid. Counts live on the resource that summarises the collection —
|
|
64
|
+
* `GET /account`'s `usage` for one caller, `GET /admin/stats` platform-wide.
|
|
65
|
+
*/
|
|
66
|
+
export interface ListResponse {
|
|
67
|
+
/** Opaque cursor from this page; `null` on the last page. */
|
|
68
|
+
cursor: string | null;
|
|
69
|
+
}
|
|
52
70
|
/**
|
|
53
71
|
* Response for listing deployments
|
|
54
72
|
*/
|
|
55
|
-
export interface DeploymentListResponse {
|
|
73
|
+
export interface DeploymentListResponse extends ListResponse {
|
|
56
74
|
/** Array of deployments */
|
|
57
75
|
deployments: Deployment[];
|
|
58
|
-
/** Cursor for pagination, null if no more pages */
|
|
59
|
-
cursor: string | null;
|
|
60
|
-
/** Total number of deployments */
|
|
61
|
-
total: number;
|
|
62
76
|
}
|
|
63
77
|
/**
|
|
64
78
|
* Domain status constants
|
|
@@ -91,7 +105,7 @@ export interface Domain {
|
|
|
91
105
|
labels: string[];
|
|
92
106
|
/** Unix timestamp (seconds) when domain was created */
|
|
93
107
|
readonly created: number;
|
|
94
|
-
/**
|
|
108
|
+
/** Unix timestamp (seconds) when deployment was last linked, null if never linked */
|
|
95
109
|
linked: number | null;
|
|
96
110
|
/** Total deployment links */
|
|
97
111
|
links: number;
|
|
@@ -113,13 +127,9 @@ export interface DomainSetResult extends Domain {
|
|
|
113
127
|
/**
|
|
114
128
|
* Response for listing domains
|
|
115
129
|
*/
|
|
116
|
-
export interface DomainListResponse {
|
|
130
|
+
export interface DomainListResponse extends ListResponse {
|
|
117
131
|
/** Array of domains */
|
|
118
132
|
domains: Domain[];
|
|
119
|
-
/** Cursor for pagination, null if no more pages */
|
|
120
|
-
cursor: string | null;
|
|
121
|
-
/** Total number of domains */
|
|
122
|
-
total: number;
|
|
123
133
|
}
|
|
124
134
|
/**
|
|
125
135
|
* DNS record types supported for domain configuration
|
|
@@ -179,11 +189,13 @@ export interface DomainValidateResponse {
|
|
|
179
189
|
error: string | null;
|
|
180
190
|
}
|
|
181
191
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
192
|
+
* Core deploy token object - used in both API responses and SDK.
|
|
193
|
+
*
|
|
194
|
+
* The secret is never here: it is shown once at creation
|
|
195
|
+
* ({@link TokenCreateResponse.secret}) and never again, so an entity read
|
|
196
|
+
* carries only the management identifier and lifecycle metadata.
|
|
185
197
|
*/
|
|
186
|
-
export interface
|
|
198
|
+
export interface Token {
|
|
187
199
|
/** 7-char management identifier (e.g., "a1b2c3d") */
|
|
188
200
|
readonly token: string;
|
|
189
201
|
/** Labels for categorization and filtering. Always present, empty array when none. */
|
|
@@ -198,24 +210,19 @@ export interface TokenListItem {
|
|
|
198
210
|
/**
|
|
199
211
|
* Response for listing tokens
|
|
200
212
|
*/
|
|
201
|
-
export interface TokenListResponse {
|
|
202
|
-
/** Array of tokens (
|
|
203
|
-
tokens:
|
|
204
|
-
/** Total number of tokens */
|
|
205
|
-
total: number;
|
|
213
|
+
export interface TokenListResponse extends ListResponse {
|
|
214
|
+
/** Array of tokens (the secret is never among them) */
|
|
215
|
+
tokens: Token[];
|
|
206
216
|
}
|
|
207
217
|
/**
|
|
208
|
-
* Response
|
|
218
|
+
* Response from token creation. Extends Token with the one field that
|
|
219
|
+
* exists only on creation — the same shape as
|
|
220
|
+
* {@link DeploymentCreateResponse}, because a 201 returns the resource it
|
|
221
|
+
* created plus whatever is knowable only once.
|
|
209
222
|
*/
|
|
210
|
-
export interface TokenCreateResponse {
|
|
211
|
-
/** 7-char management identifier */
|
|
212
|
-
token: string;
|
|
223
|
+
export interface TokenCreateResponse extends Token {
|
|
213
224
|
/** The raw credential value (shown once at creation, then never again) */
|
|
214
|
-
secret: string;
|
|
215
|
-
/** Labels for categorization and filtering. Always present, empty array when none. */
|
|
216
|
-
labels: string[];
|
|
217
|
-
/** Unix timestamp (seconds) when token expires, null for never */
|
|
218
|
-
expires: number | null;
|
|
225
|
+
readonly secret: string;
|
|
219
226
|
}
|
|
220
227
|
/**
|
|
221
228
|
* Account plan constants
|
|
@@ -232,10 +239,34 @@ export declare const AccountPlan: {
|
|
|
232
239
|
export type AccountPlanType = (typeof AccountPlan)[keyof typeof AccountPlan];
|
|
233
240
|
/**
|
|
234
241
|
* Account usage metrics — always available regardless of billing provider.
|
|
242
|
+
*
|
|
243
|
+
* This is where a caller's own totals live. Lists answer pages and carry no
|
|
244
|
+
* `total` (see {@link ListOptions}); a count is an aggregate over a
|
|
245
|
+
* collection, so it belongs to the summary resource that owns the
|
|
246
|
+
* collection. `GET /account` is that resource for one caller, `GET
|
|
247
|
+
* /admin/stats` for the platform.
|
|
248
|
+
*
|
|
249
|
+
* The counted dimensions are the ones the plan caps — deployments and
|
|
250
|
+
* domains (`PlatformLimits`) — plus the billable custom-domain subset, so a
|
|
251
|
+
* surface can render "3 of 10" without a second request.
|
|
235
252
|
*/
|
|
236
253
|
export interface AccountUsage {
|
|
237
254
|
/** Number of active custom domains (excludes paused) */
|
|
238
255
|
customDomains: number;
|
|
256
|
+
/**
|
|
257
|
+
* Deployments counted against the plan's deployment cap — every row
|
|
258
|
+
* whatever its status, because that is what the cap counts, so a surface
|
|
259
|
+
* renders "3 of 10" against the denominator the 403 divides by. (`GET
|
|
260
|
+
* /deployments` lists successful ones only; that is a different question
|
|
261
|
+
* asked of a different resource.) Optional by the additive-evolution law:
|
|
262
|
+
* an API predating this field omits it.
|
|
263
|
+
*/
|
|
264
|
+
deployments?: number;
|
|
265
|
+
/**
|
|
266
|
+
* Domains counted against the plan's domain cap — every domain, platform
|
|
267
|
+
* and custom alike, unlike `customDomains`. Optional for the same reason.
|
|
268
|
+
*/
|
|
269
|
+
domains?: number;
|
|
239
270
|
}
|
|
240
271
|
/**
|
|
241
272
|
* Core account object - used in both API responses and SDK
|
|
@@ -421,6 +452,18 @@ export declare class ShipError extends Error {
|
|
|
421
452
|
static file(message: string, details?: unknown): ShipError;
|
|
422
453
|
static config(message: string, details?: unknown): ShipError;
|
|
423
454
|
static api(message: string, status?: number, details?: unknown): ShipError;
|
|
455
|
+
/**
|
|
456
|
+
* The caller is at fault — by HTTP's own definition of a 4xx, or by a type
|
|
457
|
+
* that is client-attributable without ever having a status (`Config`,
|
|
458
|
+
* `File`, raised locally by the SDK).
|
|
459
|
+
*
|
|
460
|
+
* Both arms are load-bearing, because type and status are independent
|
|
461
|
+
* axes. `fromHttpResponse` trusts `body.error` only when it names a
|
|
462
|
+
* server-producible type; a non-OK response without one is status-derived,
|
|
463
|
+
* so a CDN 404 or any intermediary error arrives as `Api` — a server-fault
|
|
464
|
+
* *type* carrying a client *status*. Judging by type alone would report it
|
|
465
|
+
* as a platform failure and bury the server's own message.
|
|
466
|
+
*/
|
|
424
467
|
isClientError(): boolean;
|
|
425
468
|
isNetworkError(): boolean;
|
|
426
469
|
isAuthError(): boolean;
|
|
@@ -645,6 +688,36 @@ export declare const SPA_DEFAULT_CONFIG: {
|
|
|
645
688
|
readonly destination: "/index.html";
|
|
646
689
|
}];
|
|
647
690
|
};
|
|
691
|
+
/**
|
|
692
|
+
* Assert that a ship.json file is *syntactically* loadable. Syntax only —
|
|
693
|
+
* never schema.
|
|
694
|
+
*
|
|
695
|
+
* ship.json is validated and compiled on the server, deliberately: the schema
|
|
696
|
+
* and the compiler evolve, and a client that judged them would reject configs
|
|
697
|
+
* a newer platform accepts. That reasoning bounds what a client may check to
|
|
698
|
+
* the properties which are true of *every* past and future schema:
|
|
699
|
+
*
|
|
700
|
+
* 1. it parses as JSON — JSON syntax is frozen (RFC 8259), so text that
|
|
701
|
+
* does not parse can never be a valid config;
|
|
702
|
+
* 2. its top level is an object — ship.json is `{ ... }` in every version.
|
|
703
|
+
*
|
|
704
|
+
* Both are monotonic: neither can ever reject something the server would
|
|
705
|
+
* accept. Everything beyond them (field names, types, rule semantics, which
|
|
706
|
+
* keys are permitted) stays server-side, where it can change.
|
|
707
|
+
*
|
|
708
|
+
* The payoff is the common case. Hand-edited JSON fails on a trailing comma,
|
|
709
|
+
* a `//` comment, single quotes, unquoted keys, or smart quotes pasted from
|
|
710
|
+
* documentation — mistakes that otherwise cost a full upload round-trip to
|
|
711
|
+
* discover. A UTF-8 BOM (Windows editors, PowerShell redirects) is stripped
|
|
712
|
+
* before parsing rather than rejected, because the server accepts it too;
|
|
713
|
+
* diverging there would reintroduce exactly the false rejection this
|
|
714
|
+
* function exists to avoid.
|
|
715
|
+
*
|
|
716
|
+
* @throws {ShipError} `ErrorType.Config` — the same type the server's own
|
|
717
|
+
* config rejection carries, so the error contract is identical wherever the
|
|
718
|
+
* failure is detected.
|
|
719
|
+
*/
|
|
720
|
+
export declare function assertShipJsonSyntax(text: string): void;
|
|
648
721
|
/**
|
|
649
722
|
* Validate API key format
|
|
650
723
|
*/
|
|
@@ -770,10 +843,20 @@ export interface DeploymentUploadOptions {
|
|
|
770
843
|
captcha?: string;
|
|
771
844
|
}
|
|
772
845
|
/**
|
|
773
|
-
* Pagination options for
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
846
|
+
* Pagination options for every list endpoint. The response's `cursor` feeds
|
|
847
|
+
* the next request; a `null` cursor means the last page. Omitting both
|
|
848
|
+
* returns the server's default first page.
|
|
849
|
+
*
|
|
850
|
+
* A list answers `{ <collection>, cursor }` and nothing else — `cursor`
|
|
851
|
+
* carries the entire has-more signal, so no redundant boolean, and no
|
|
852
|
+
* `total`. **A count is an aggregate over a collection, not a property of a
|
|
853
|
+
* page:** including one makes every read pay for a full scan it did not ask
|
|
854
|
+
* for, which is precisely the cost keyset pagination exists to avoid.
|
|
855
|
+
*
|
|
856
|
+
* Counts therefore live on the summary resource that owns them —
|
|
857
|
+
* `GET /account` (`usage`) for a caller's own totals, `GET /admin/stats` for
|
|
858
|
+
* platform-wide ones. Ask for a count when you want a count; ask for a page
|
|
859
|
+
* when you want a page.
|
|
777
860
|
*/
|
|
778
861
|
export interface ListOptions {
|
|
779
862
|
/** Maximum number of items to return in one page. */
|
|
@@ -834,7 +917,7 @@ export interface TokenResource {
|
|
|
834
917
|
ttl?: number;
|
|
835
918
|
labels?: string[];
|
|
836
919
|
}) => Promise<TokenCreateResponse>;
|
|
837
|
-
list: () => Promise<TokenListResponse>;
|
|
920
|
+
list: (options?: ListOptions) => Promise<TokenListResponse>;
|
|
838
921
|
remove: (token: string) => Promise<void>;
|
|
839
922
|
}
|
|
840
923
|
/**
|
|
@@ -929,7 +1012,7 @@ export interface ActivityMeta {
|
|
|
929
1012
|
/**
|
|
930
1013
|
* Response from GET /activities endpoint
|
|
931
1014
|
*/
|
|
932
|
-
export interface ActivityListResponse {
|
|
1015
|
+
export interface ActivityListResponse extends ListResponse {
|
|
933
1016
|
/** Array of activities */
|
|
934
1017
|
activities: Activity[];
|
|
935
1018
|
}
|
package/dist/index.js
CHANGED
|
@@ -100,11 +100,19 @@ 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
|
+
/**
|
|
104
|
+
* Client-attributable types. Exhaustive over the 4xx-carrying types, and
|
|
105
|
+
* it must include the statusless ones (`Config`, `File`) — those are
|
|
106
|
+
* raised locally by the SDK and have no status for `isClientError`'s
|
|
107
|
+
* second arm to read.
|
|
108
|
+
*/
|
|
103
109
|
client: new Set([
|
|
104
110
|
ErrorType.Business,
|
|
105
111
|
ErrorType.Config,
|
|
106
112
|
ErrorType.File,
|
|
107
113
|
ErrorType.Forbidden,
|
|
114
|
+
ErrorType.NotFound,
|
|
115
|
+
ErrorType.RateLimit,
|
|
108
116
|
ErrorType.Validation,
|
|
109
117
|
]),
|
|
110
118
|
network: new Set([ErrorType.Network]),
|
|
@@ -309,10 +317,24 @@ export class ShipError extends Error {
|
|
|
309
317
|
static api(message, status = 500, details) {
|
|
310
318
|
return new ShipError(ErrorType.Api, message, status, details);
|
|
311
319
|
}
|
|
312
|
-
// Semantic-category
|
|
320
|
+
// Semantic-category guards. For specific-type checks, use
|
|
313
321
|
// `error.type === ErrorType.X` directly or the generic `isType(t)`.
|
|
322
|
+
/**
|
|
323
|
+
* The caller is at fault — by HTTP's own definition of a 4xx, or by a type
|
|
324
|
+
* that is client-attributable without ever having a status (`Config`,
|
|
325
|
+
* `File`, raised locally by the SDK).
|
|
326
|
+
*
|
|
327
|
+
* Both arms are load-bearing, because type and status are independent
|
|
328
|
+
* axes. `fromHttpResponse` trusts `body.error` only when it names a
|
|
329
|
+
* server-producible type; a non-OK response without one is status-derived,
|
|
330
|
+
* so a CDN 404 or any intermediary error arrives as `Api` — a server-fault
|
|
331
|
+
* *type* carrying a client *status*. Judging by type alone would report it
|
|
332
|
+
* as a platform failure and bury the server's own message.
|
|
333
|
+
*/
|
|
314
334
|
isClientError() {
|
|
315
|
-
|
|
335
|
+
if (ERROR_CATEGORIES.client.has(this.type))
|
|
336
|
+
return true;
|
|
337
|
+
return this.status !== undefined && this.status >= 400 && this.status < 500;
|
|
316
338
|
}
|
|
317
339
|
isNetworkError() {
|
|
318
340
|
return ERROR_CATEGORIES.network.has(this.type);
|
|
@@ -600,6 +622,52 @@ export const DEPLOYMENT_CONFIG_FILENAME = 'ship.json';
|
|
|
600
622
|
export const SPA_DEFAULT_CONFIG = {
|
|
601
623
|
rewrites: [{ source: '/(.*)', destination: '/index.html' }],
|
|
602
624
|
};
|
|
625
|
+
/**
|
|
626
|
+
* Assert that a ship.json file is *syntactically* loadable. Syntax only —
|
|
627
|
+
* never schema.
|
|
628
|
+
*
|
|
629
|
+
* ship.json is validated and compiled on the server, deliberately: the schema
|
|
630
|
+
* and the compiler evolve, and a client that judged them would reject configs
|
|
631
|
+
* a newer platform accepts. That reasoning bounds what a client may check to
|
|
632
|
+
* the properties which are true of *every* past and future schema:
|
|
633
|
+
*
|
|
634
|
+
* 1. it parses as JSON — JSON syntax is frozen (RFC 8259), so text that
|
|
635
|
+
* does not parse can never be a valid config;
|
|
636
|
+
* 2. its top level is an object — ship.json is `{ ... }` in every version.
|
|
637
|
+
*
|
|
638
|
+
* Both are monotonic: neither can ever reject something the server would
|
|
639
|
+
* accept. Everything beyond them (field names, types, rule semantics, which
|
|
640
|
+
* keys are permitted) stays server-side, where it can change.
|
|
641
|
+
*
|
|
642
|
+
* The payoff is the common case. Hand-edited JSON fails on a trailing comma,
|
|
643
|
+
* a `//` comment, single quotes, unquoted keys, or smart quotes pasted from
|
|
644
|
+
* documentation — mistakes that otherwise cost a full upload round-trip to
|
|
645
|
+
* discover. A UTF-8 BOM (Windows editors, PowerShell redirects) is stripped
|
|
646
|
+
* before parsing rather than rejected, because the server accepts it too;
|
|
647
|
+
* diverging there would reintroduce exactly the false rejection this
|
|
648
|
+
* function exists to avoid.
|
|
649
|
+
*
|
|
650
|
+
* @throws {ShipError} `ErrorType.Config` — the same type the server's own
|
|
651
|
+
* config rejection carries, so the error contract is identical wherever the
|
|
652
|
+
* failure is detected.
|
|
653
|
+
*/
|
|
654
|
+
export function assertShipJsonSyntax(text) {
|
|
655
|
+
const withoutBom = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
656
|
+
let parsed;
|
|
657
|
+
try {
|
|
658
|
+
parsed = JSON.parse(withoutBom);
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
throw ShipError.config(`invalid JSON format in config: ${error.message}`, {
|
|
662
|
+
filePath: DEPLOYMENT_CONFIG_FILENAME,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
666
|
+
throw ShipError.config(`${DEPLOYMENT_CONFIG_FILENAME} must contain a JSON object`, {
|
|
667
|
+
filePath: DEPLOYMENT_CONFIG_FILENAME,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
603
671
|
// =============================================================================
|
|
604
672
|
// VALIDATION UTILITIES
|
|
605
673
|
// =============================================================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipstatic/types",
|
|
3
|
-
"version": "2.5.0-beta.
|
|
3
|
+
"version": "2.5.0-beta.10",
|
|
4
4
|
"description": "Shared types for ShipStatic platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -11,6 +11,16 @@
|
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"clean": "rm -rf dist",
|
|
17
|
+
"test": "vitest",
|
|
18
|
+
"lint": "biome check .",
|
|
19
|
+
"format": "biome format --write .",
|
|
20
|
+
"prepare": "git config core.hooksPath scripts/githooks",
|
|
21
|
+
"typecheck": "tsc -p tsconfig.check.json --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"packageManager": "pnpm@10.12.4",
|
|
14
24
|
"files": [
|
|
15
25
|
"dist",
|
|
16
26
|
"src"
|
|
@@ -34,16 +44,8 @@
|
|
|
34
44
|
},
|
|
35
45
|
"devDependencies": {
|
|
36
46
|
"@biomejs/biome": "2.5.5",
|
|
37
|
-
"@types/node": "^24.
|
|
47
|
+
"@types/node": "^24.13.3",
|
|
38
48
|
"typescript": "^5.9.3",
|
|
39
|
-
"vitest": "^2.1.
|
|
40
|
-
},
|
|
41
|
-
"scripts": {
|
|
42
|
-
"build": "tsc",
|
|
43
|
-
"clean": "rm -rf dist",
|
|
44
|
-
"test": "vitest",
|
|
45
|
-
"lint": "biome check .",
|
|
46
|
-
"format": "biome format --write .",
|
|
47
|
-
"typecheck": "tsc --noEmit"
|
|
49
|
+
"vitest": "^2.1.9"
|
|
48
50
|
}
|
|
49
|
-
}
|
|
51
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -58,16 +58,31 @@ export interface DeploymentCreateResponse extends Deployment {
|
|
|
58
58
|
readonly claim?: string;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The half of a list response that is identical on every list.
|
|
63
|
+
*
|
|
64
|
+
* `GET /<collection>` answers exactly two fields — the collection under its
|
|
65
|
+
* own plural noun, and this cursor — so the cursor is declared once here and
|
|
66
|
+
* each response below adds only its noun. `cursor: null` means last page and
|
|
67
|
+
* is the ENTIRE has-more signal, which is why there is no `has_more`.
|
|
68
|
+
*
|
|
69
|
+
* There is deliberately no `total`. A count is an aggregate over a
|
|
70
|
+
* collection, not a property of a page; producing one would cost a COUNT
|
|
71
|
+
* beside every page read, which is precisely what keyset pagination exists
|
|
72
|
+
* to avoid. Counts live on the resource that summarises the collection —
|
|
73
|
+
* `GET /account`'s `usage` for one caller, `GET /admin/stats` platform-wide.
|
|
74
|
+
*/
|
|
75
|
+
export interface ListResponse {
|
|
76
|
+
/** Opaque cursor from this page; `null` on the last page. */
|
|
77
|
+
cursor: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
/**
|
|
62
81
|
* Response for listing deployments
|
|
63
82
|
*/
|
|
64
|
-
export interface DeploymentListResponse {
|
|
83
|
+
export interface DeploymentListResponse extends ListResponse {
|
|
65
84
|
/** Array of deployments */
|
|
66
85
|
deployments: Deployment[];
|
|
67
|
-
/** Cursor for pagination, null if no more pages */
|
|
68
|
-
cursor: string | null;
|
|
69
|
-
/** Total number of deployments */
|
|
70
|
-
total: number;
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
// =============================================================================
|
|
@@ -107,7 +122,7 @@ export interface Domain {
|
|
|
107
122
|
labels: string[];
|
|
108
123
|
/** Unix timestamp (seconds) when domain was created */
|
|
109
124
|
readonly created: number;
|
|
110
|
-
/**
|
|
125
|
+
/** Unix timestamp (seconds) when deployment was last linked, null if never linked */
|
|
111
126
|
linked: number | null;
|
|
112
127
|
/** Total deployment links */
|
|
113
128
|
links: number;
|
|
@@ -131,13 +146,9 @@ export interface DomainSetResult extends Domain {
|
|
|
131
146
|
/**
|
|
132
147
|
* Response for listing domains
|
|
133
148
|
*/
|
|
134
|
-
export interface DomainListResponse {
|
|
149
|
+
export interface DomainListResponse extends ListResponse {
|
|
135
150
|
/** Array of domains */
|
|
136
151
|
domains: Domain[];
|
|
137
|
-
/** Cursor for pagination, null if no more pages */
|
|
138
|
-
cursor: string | null;
|
|
139
|
-
/** Total number of domains */
|
|
140
|
-
total: number;
|
|
141
152
|
}
|
|
142
153
|
|
|
143
154
|
/**
|
|
@@ -206,11 +217,13 @@ export interface DomainValidateResponse {
|
|
|
206
217
|
// =============================================================================
|
|
207
218
|
|
|
208
219
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
220
|
+
* Core deploy token object - used in both API responses and SDK.
|
|
221
|
+
*
|
|
222
|
+
* The secret is never here: it is shown once at creation
|
|
223
|
+
* ({@link TokenCreateResponse.secret}) and never again, so an entity read
|
|
224
|
+
* carries only the management identifier and lifecycle metadata.
|
|
212
225
|
*/
|
|
213
|
-
export interface
|
|
226
|
+
export interface Token {
|
|
214
227
|
/** 7-char management identifier (e.g., "a1b2c3d") */
|
|
215
228
|
readonly token: string;
|
|
216
229
|
/** Labels for categorization and filtering. Always present, empty array when none. */
|
|
@@ -226,25 +239,20 @@ export interface TokenListItem {
|
|
|
226
239
|
/**
|
|
227
240
|
* Response for listing tokens
|
|
228
241
|
*/
|
|
229
|
-
export interface TokenListResponse {
|
|
230
|
-
/** Array of tokens (
|
|
231
|
-
tokens:
|
|
232
|
-
/** Total number of tokens */
|
|
233
|
-
total: number;
|
|
242
|
+
export interface TokenListResponse extends ListResponse {
|
|
243
|
+
/** Array of tokens (the secret is never among them) */
|
|
244
|
+
tokens: Token[];
|
|
234
245
|
}
|
|
235
246
|
|
|
236
247
|
/**
|
|
237
|
-
* Response
|
|
248
|
+
* Response from token creation. Extends Token with the one field that
|
|
249
|
+
* exists only on creation — the same shape as
|
|
250
|
+
* {@link DeploymentCreateResponse}, because a 201 returns the resource it
|
|
251
|
+
* created plus whatever is knowable only once.
|
|
238
252
|
*/
|
|
239
|
-
export interface TokenCreateResponse {
|
|
240
|
-
/** 7-char management identifier */
|
|
241
|
-
token: string;
|
|
253
|
+
export interface TokenCreateResponse extends Token {
|
|
242
254
|
/** The raw credential value (shown once at creation, then never again) */
|
|
243
|
-
secret: string;
|
|
244
|
-
/** Labels for categorization and filtering. Always present, empty array when none. */
|
|
245
|
-
labels: string[];
|
|
246
|
-
/** Unix timestamp (seconds) when token expires, null for never */
|
|
247
|
-
expires: number | null;
|
|
255
|
+
readonly secret: string;
|
|
248
256
|
}
|
|
249
257
|
|
|
250
258
|
// =============================================================================
|
|
@@ -268,10 +276,34 @@ export type AccountPlanType = (typeof AccountPlan)[keyof typeof AccountPlan];
|
|
|
268
276
|
|
|
269
277
|
/**
|
|
270
278
|
* Account usage metrics — always available regardless of billing provider.
|
|
279
|
+
*
|
|
280
|
+
* This is where a caller's own totals live. Lists answer pages and carry no
|
|
281
|
+
* `total` (see {@link ListOptions}); a count is an aggregate over a
|
|
282
|
+
* collection, so it belongs to the summary resource that owns the
|
|
283
|
+
* collection. `GET /account` is that resource for one caller, `GET
|
|
284
|
+
* /admin/stats` for the platform.
|
|
285
|
+
*
|
|
286
|
+
* The counted dimensions are the ones the plan caps — deployments and
|
|
287
|
+
* domains (`PlatformLimits`) — plus the billable custom-domain subset, so a
|
|
288
|
+
* surface can render "3 of 10" without a second request.
|
|
271
289
|
*/
|
|
272
290
|
export interface AccountUsage {
|
|
273
291
|
/** Number of active custom domains (excludes paused) */
|
|
274
292
|
customDomains: number;
|
|
293
|
+
/**
|
|
294
|
+
* Deployments counted against the plan's deployment cap — every row
|
|
295
|
+
* whatever its status, because that is what the cap counts, so a surface
|
|
296
|
+
* renders "3 of 10" against the denominator the 403 divides by. (`GET
|
|
297
|
+
* /deployments` lists successful ones only; that is a different question
|
|
298
|
+
* asked of a different resource.) Optional by the additive-evolution law:
|
|
299
|
+
* an API predating this field omits it.
|
|
300
|
+
*/
|
|
301
|
+
deployments?: number;
|
|
302
|
+
/**
|
|
303
|
+
* Domains counted against the plan's domain cap — every domain, platform
|
|
304
|
+
* and custom alike, unlike `customDomains`. Optional for the same reason.
|
|
305
|
+
*/
|
|
306
|
+
domains?: number;
|
|
275
307
|
}
|
|
276
308
|
|
|
277
309
|
/**
|
|
@@ -397,11 +429,19 @@ const CLIENT_ONLY_ERROR_TYPES = new Set<string>([
|
|
|
397
429
|
* union so `.has(error.type)` accepts any value from the union.
|
|
398
430
|
*/
|
|
399
431
|
const ERROR_CATEGORIES = {
|
|
432
|
+
/**
|
|
433
|
+
* Client-attributable types. Exhaustive over the 4xx-carrying types, and
|
|
434
|
+
* it must include the statusless ones (`Config`, `File`) — those are
|
|
435
|
+
* raised locally by the SDK and have no status for `isClientError`'s
|
|
436
|
+
* second arm to read.
|
|
437
|
+
*/
|
|
400
438
|
client: new Set<ErrorType>([
|
|
401
439
|
ErrorType.Business,
|
|
402
440
|
ErrorType.Config,
|
|
403
441
|
ErrorType.File,
|
|
404
442
|
ErrorType.Forbidden,
|
|
443
|
+
ErrorType.NotFound,
|
|
444
|
+
ErrorType.RateLimit,
|
|
405
445
|
ErrorType.Validation,
|
|
406
446
|
]),
|
|
407
447
|
network: new Set<ErrorType>([ErrorType.Network]),
|
|
@@ -645,10 +685,24 @@ export class ShipError extends Error {
|
|
|
645
685
|
return new ShipError(ErrorType.Api, message, status, details);
|
|
646
686
|
}
|
|
647
687
|
|
|
648
|
-
// Semantic-category
|
|
688
|
+
// Semantic-category guards. For specific-type checks, use
|
|
649
689
|
// `error.type === ErrorType.X` directly or the generic `isType(t)`.
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* The caller is at fault — by HTTP's own definition of a 4xx, or by a type
|
|
693
|
+
* that is client-attributable without ever having a status (`Config`,
|
|
694
|
+
* `File`, raised locally by the SDK).
|
|
695
|
+
*
|
|
696
|
+
* Both arms are load-bearing, because type and status are independent
|
|
697
|
+
* axes. `fromHttpResponse` trusts `body.error` only when it names a
|
|
698
|
+
* server-producible type; a non-OK response without one is status-derived,
|
|
699
|
+
* so a CDN 404 or any intermediary error arrives as `Api` — a server-fault
|
|
700
|
+
* *type* carrying a client *status*. Judging by type alone would report it
|
|
701
|
+
* as a platform failure and bury the server's own message.
|
|
702
|
+
*/
|
|
650
703
|
isClientError(): boolean {
|
|
651
|
-
|
|
704
|
+
if (ERROR_CATEGORIES.client.has(this.type)) return true;
|
|
705
|
+
return this.status !== undefined && this.status >= 400 && this.status < 500;
|
|
652
706
|
}
|
|
653
707
|
|
|
654
708
|
isNetworkError(): boolean {
|
|
@@ -1004,6 +1058,54 @@ export const SPA_DEFAULT_CONFIG = {
|
|
|
1004
1058
|
rewrites: [{ source: '/(.*)', destination: '/index.html' }],
|
|
1005
1059
|
} as const;
|
|
1006
1060
|
|
|
1061
|
+
/**
|
|
1062
|
+
* Assert that a ship.json file is *syntactically* loadable. Syntax only —
|
|
1063
|
+
* never schema.
|
|
1064
|
+
*
|
|
1065
|
+
* ship.json is validated and compiled on the server, deliberately: the schema
|
|
1066
|
+
* and the compiler evolve, and a client that judged them would reject configs
|
|
1067
|
+
* a newer platform accepts. That reasoning bounds what a client may check to
|
|
1068
|
+
* the properties which are true of *every* past and future schema:
|
|
1069
|
+
*
|
|
1070
|
+
* 1. it parses as JSON — JSON syntax is frozen (RFC 8259), so text that
|
|
1071
|
+
* does not parse can never be a valid config;
|
|
1072
|
+
* 2. its top level is an object — ship.json is `{ ... }` in every version.
|
|
1073
|
+
*
|
|
1074
|
+
* Both are monotonic: neither can ever reject something the server would
|
|
1075
|
+
* accept. Everything beyond them (field names, types, rule semantics, which
|
|
1076
|
+
* keys are permitted) stays server-side, where it can change.
|
|
1077
|
+
*
|
|
1078
|
+
* The payoff is the common case. Hand-edited JSON fails on a trailing comma,
|
|
1079
|
+
* a `//` comment, single quotes, unquoted keys, or smart quotes pasted from
|
|
1080
|
+
* documentation — mistakes that otherwise cost a full upload round-trip to
|
|
1081
|
+
* discover. A UTF-8 BOM (Windows editors, PowerShell redirects) is stripped
|
|
1082
|
+
* before parsing rather than rejected, because the server accepts it too;
|
|
1083
|
+
* diverging there would reintroduce exactly the false rejection this
|
|
1084
|
+
* function exists to avoid.
|
|
1085
|
+
*
|
|
1086
|
+
* @throws {ShipError} `ErrorType.Config` — the same type the server's own
|
|
1087
|
+
* config rejection carries, so the error contract is identical wherever the
|
|
1088
|
+
* failure is detected.
|
|
1089
|
+
*/
|
|
1090
|
+
export function assertShipJsonSyntax(text: string): void {
|
|
1091
|
+
const withoutBom = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
1092
|
+
|
|
1093
|
+
let parsed: unknown;
|
|
1094
|
+
try {
|
|
1095
|
+
parsed = JSON.parse(withoutBom);
|
|
1096
|
+
} catch (error) {
|
|
1097
|
+
throw ShipError.config(`invalid JSON format in config: ${(error as Error).message}`, {
|
|
1098
|
+
filePath: DEPLOYMENT_CONFIG_FILENAME,
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1103
|
+
throw ShipError.config(`${DEPLOYMENT_CONFIG_FILENAME} must contain a JSON object`, {
|
|
1104
|
+
filePath: DEPLOYMENT_CONFIG_FILENAME,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1007
1109
|
// =============================================================================
|
|
1008
1110
|
// VALIDATION UTILITIES
|
|
1009
1111
|
// =============================================================================
|
|
@@ -1233,10 +1335,20 @@ export interface DeploymentUploadOptions {
|
|
|
1233
1335
|
}
|
|
1234
1336
|
|
|
1235
1337
|
/**
|
|
1236
|
-
* Pagination options for
|
|
1237
|
-
*
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1338
|
+
* Pagination options for every list endpoint. The response's `cursor` feeds
|
|
1339
|
+
* the next request; a `null` cursor means the last page. Omitting both
|
|
1340
|
+
* returns the server's default first page.
|
|
1341
|
+
*
|
|
1342
|
+
* A list answers `{ <collection>, cursor }` and nothing else — `cursor`
|
|
1343
|
+
* carries the entire has-more signal, so no redundant boolean, and no
|
|
1344
|
+
* `total`. **A count is an aggregate over a collection, not a property of a
|
|
1345
|
+
* page:** including one makes every read pay for a full scan it did not ask
|
|
1346
|
+
* for, which is precisely the cost keyset pagination exists to avoid.
|
|
1347
|
+
*
|
|
1348
|
+
* Counts therefore live on the summary resource that owns them —
|
|
1349
|
+
* `GET /account` (`usage`) for a caller's own totals, `GET /admin/stats` for
|
|
1350
|
+
* platform-wide ones. Ask for a count when you want a count; ask for a page
|
|
1351
|
+
* when you want a page.
|
|
1240
1352
|
*/
|
|
1241
1353
|
export interface ListOptions {
|
|
1242
1354
|
/** Maximum number of items to return in one page. */
|
|
@@ -1293,7 +1405,7 @@ export interface AccountResource {
|
|
|
1293
1405
|
*/
|
|
1294
1406
|
export interface TokenResource {
|
|
1295
1407
|
create: (options?: { ttl?: number; labels?: string[] }) => Promise<TokenCreateResponse>;
|
|
1296
|
-
list: () => Promise<TokenListResponse>;
|
|
1408
|
+
list: (options?: ListOptions) => Promise<TokenListResponse>;
|
|
1297
1409
|
remove: (token: string) => Promise<void>;
|
|
1298
1410
|
}
|
|
1299
1411
|
|
|
@@ -1477,7 +1589,7 @@ export interface ActivityMeta {
|
|
|
1477
1589
|
/**
|
|
1478
1590
|
* Response from GET /activities endpoint
|
|
1479
1591
|
*/
|
|
1480
|
-
export interface ActivityListResponse {
|
|
1592
|
+
export interface ActivityListResponse extends ListResponse {
|
|
1481
1593
|
/** Array of activities */
|
|
1482
1594
|
activities: Activity[];
|
|
1483
1595
|
}
|