@shipstatic/types 2.5.0-beta.1 → 2.5.0-beta.11
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/README.md +11 -6
- package/dist/index.d.ts +198 -38
- package/dist/index.js +70 -2
- package/package.json +14 -12
- package/src/index.ts +235 -38
package/README.md
CHANGED
|
@@ -20,14 +20,20 @@ npm install @shipstatic/types
|
|
|
20
20
|
|
|
21
21
|
```typescript
|
|
22
22
|
import type {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
ListResponse,
|
|
24
|
+
Deployment, DeploymentListResponse, DeploymentDeleteResponse,
|
|
25
|
+
Domain, DomainSetResult, DomainListResponse, DnsRecord, DomainDnsResponse, DomainRecordsResponse, DomainValidateResponse, DomainDeleteResponse, DomainVerifyResponse,
|
|
26
|
+
Token, TokenListResponse, TokenCreateResponse, TokenDeleteResponse,
|
|
27
|
+
Account, AccountUsage, AccountOverrides, AccountDeleteResponse, AccountKeyResponse,
|
|
27
28
|
StaticFile
|
|
28
29
|
} from '@shipstatic/types';
|
|
29
30
|
```
|
|
30
31
|
|
|
32
|
+
A mutation answers with the resource it affected — the entity when it
|
|
33
|
+
survives, otherwise the `*DeleteResponse` shape: the resource noun carrying
|
|
34
|
+
the canonical key, plus the resource's own state field where the resource is
|
|
35
|
+
mid-transition. No `message`, no `success`, no constant flags.
|
|
36
|
+
|
|
31
37
|
### Error System
|
|
32
38
|
|
|
33
39
|
```typescript
|
|
@@ -82,7 +88,7 @@ import {
|
|
|
82
88
|
DomainStatus, // pending | partial | success | paused
|
|
83
89
|
AccountPlan, // free | standard | sponsored | enterprise | suspended | terminating | terminated
|
|
84
90
|
FileValidationStatus, // pending | processing_error | excluded | validation_failed | ready
|
|
85
|
-
AuthMethod, //
|
|
91
|
+
AuthMethod, // session | apiKey | token | agent | oauth | webhook | system
|
|
86
92
|
} from '@shipstatic/types';
|
|
87
93
|
```
|
|
88
94
|
|
|
@@ -132,7 +138,6 @@ import type {
|
|
|
132
138
|
FileValidationResult,
|
|
133
139
|
ValidationIssue,
|
|
134
140
|
UploadedFile,
|
|
135
|
-
ProgressInfo,
|
|
136
141
|
} from '@shipstatic/types';
|
|
137
142
|
```
|
|
138
143
|
|
package/dist/index.d.ts
CHANGED
|
@@ -49,16 +49,55 @@ 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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Acknowledgement of `DELETE /deployments/:deployment` — and the shape every
|
|
79
|
+
* mutation with no entity left to return follows.
|
|
80
|
+
*
|
|
81
|
+
* **The law:** a mutation answers with the resource it affected. If the
|
|
82
|
+
* resource still exists, that means the entity itself (`Deployment`,
|
|
83
|
+
* `Domain`, …). Otherwise it means this: the resource noun carrying the
|
|
84
|
+
* item's canonical key, plus the resource's own state field — and ONLY when
|
|
85
|
+
* the resource survived in a transitional state, as an async deletion's does.
|
|
86
|
+
* Where the resource is simply gone, the key alone is the whole answer
|
|
87
|
+
* ({@link DomainDeleteResponse}, {@link TokenDeleteResponse}).
|
|
88
|
+
*
|
|
89
|
+
* Nothing else rides along. No prose (`message`), because an acknowledgement
|
|
90
|
+
* is data and each surface composes its own copy; and no constant
|
|
91
|
+
* (`changed: true`, `queued: true`, `success: true`), because a field whose
|
|
92
|
+
* value the type already fixes tells a caller nothing it did not know before
|
|
93
|
+
* it made the request. Sync versus accepted is the HTTP status code's job —
|
|
94
|
+
* 200 versus 202 — not a boolean's.
|
|
95
|
+
*/
|
|
96
|
+
export interface DeploymentDeleteResponse {
|
|
97
|
+
/** The deployment hostname that was marked for removal */
|
|
98
|
+
readonly deployment: string;
|
|
99
|
+
/** The state the deployment is in while background cleanup runs */
|
|
100
|
+
readonly status: DeploymentStatusType;
|
|
62
101
|
}
|
|
63
102
|
/**
|
|
64
103
|
* Domain status constants
|
|
@@ -91,7 +130,7 @@ export interface Domain {
|
|
|
91
130
|
labels: string[];
|
|
92
131
|
/** Unix timestamp (seconds) when domain was created */
|
|
93
132
|
readonly created: number;
|
|
94
|
-
/**
|
|
133
|
+
/** Unix timestamp (seconds) when deployment was last linked, null if never linked */
|
|
95
134
|
linked: number | null;
|
|
96
135
|
/** Total deployment links */
|
|
97
136
|
links: number;
|
|
@@ -113,13 +152,28 @@ export interface DomainSetResult extends Domain {
|
|
|
113
152
|
/**
|
|
114
153
|
* Response for listing domains
|
|
115
154
|
*/
|
|
116
|
-
export interface DomainListResponse {
|
|
155
|
+
export interface DomainListResponse extends ListResponse {
|
|
117
156
|
/** Array of domains */
|
|
118
157
|
domains: Domain[];
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Acknowledgement of `DELETE /domains/:domain`. The row is gone, so there is
|
|
161
|
+
* no state to state — the canonical domain name is the whole answer. See
|
|
162
|
+
* {@link DeploymentDeleteResponse} for the law.
|
|
163
|
+
*/
|
|
164
|
+
export interface DomainDeleteResponse {
|
|
165
|
+
/** The domain name that was removed, normalized */
|
|
166
|
+
readonly domain: string;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Acknowledgement of `POST /domains/:domain/verify` (202). The DNS check is
|
|
170
|
+
* queued, not performed — the accepted status code says so, and the domain's
|
|
171
|
+
* own status is unchanged until the check runs, which is why none is stated
|
|
172
|
+
* here. See {@link DeploymentDeleteResponse} for the law.
|
|
173
|
+
*/
|
|
174
|
+
export interface DomainVerifyResponse {
|
|
175
|
+
/** The domain whose DNS verification was queued, normalized */
|
|
176
|
+
readonly domain: string;
|
|
123
177
|
}
|
|
124
178
|
/**
|
|
125
179
|
* DNS record types supported for domain configuration
|
|
@@ -179,11 +233,13 @@ export interface DomainValidateResponse {
|
|
|
179
233
|
error: string | null;
|
|
180
234
|
}
|
|
181
235
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
236
|
+
* Core deploy token object - used in both API responses and SDK.
|
|
237
|
+
*
|
|
238
|
+
* The secret is never here: it is shown once at creation
|
|
239
|
+
* ({@link TokenCreateResponse.secret}) and never again, so an entity read
|
|
240
|
+
* carries only the management identifier and lifecycle metadata.
|
|
185
241
|
*/
|
|
186
|
-
export interface
|
|
242
|
+
export interface Token {
|
|
187
243
|
/** 7-char management identifier (e.g., "a1b2c3d") */
|
|
188
244
|
readonly token: string;
|
|
189
245
|
/** Labels for categorization and filtering. Always present, empty array when none. */
|
|
@@ -198,24 +254,28 @@ export interface TokenListItem {
|
|
|
198
254
|
/**
|
|
199
255
|
* Response for listing tokens
|
|
200
256
|
*/
|
|
201
|
-
export interface TokenListResponse {
|
|
202
|
-
/** Array of tokens (
|
|
203
|
-
tokens:
|
|
204
|
-
/** Total number of tokens */
|
|
205
|
-
total: number;
|
|
257
|
+
export interface TokenListResponse extends ListResponse {
|
|
258
|
+
/** Array of tokens (the secret is never among them) */
|
|
259
|
+
tokens: Token[];
|
|
206
260
|
}
|
|
207
261
|
/**
|
|
208
|
-
* Response
|
|
262
|
+
* Response from token creation. Extends Token with the one field that
|
|
263
|
+
* exists only on creation — the same shape as
|
|
264
|
+
* {@link DeploymentCreateResponse}, because a 201 returns the resource it
|
|
265
|
+
* created plus whatever is knowable only once.
|
|
209
266
|
*/
|
|
210
|
-
export interface TokenCreateResponse {
|
|
211
|
-
/** 7-char management identifier */
|
|
212
|
-
token: string;
|
|
267
|
+
export interface TokenCreateResponse extends Token {
|
|
213
268
|
/** The raw credential value (shown once at creation, then never again) */
|
|
214
|
-
secret: string;
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
269
|
+
readonly secret: string;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Acknowledgement of `DELETE /tokens/:token`. The credential is revoked and
|
|
273
|
+
* its row is gone, so the management identifier is the whole answer. See
|
|
274
|
+
* {@link DeploymentDeleteResponse} for the law.
|
|
275
|
+
*/
|
|
276
|
+
export interface TokenDeleteResponse {
|
|
277
|
+
/** The 7-char management identifier that was revoked */
|
|
278
|
+
readonly token: string;
|
|
219
279
|
}
|
|
220
280
|
/**
|
|
221
281
|
* Account plan constants
|
|
@@ -232,10 +292,34 @@ export declare const AccountPlan: {
|
|
|
232
292
|
export type AccountPlanType = (typeof AccountPlan)[keyof typeof AccountPlan];
|
|
233
293
|
/**
|
|
234
294
|
* Account usage metrics — always available regardless of billing provider.
|
|
295
|
+
*
|
|
296
|
+
* This is where a caller's own totals live. Lists answer pages and carry no
|
|
297
|
+
* `total` (see {@link ListOptions}); a count is an aggregate over a
|
|
298
|
+
* collection, so it belongs to the summary resource that owns the
|
|
299
|
+
* collection. `GET /account` is that resource for one caller, `GET
|
|
300
|
+
* /admin/stats` for the platform.
|
|
301
|
+
*
|
|
302
|
+
* The counted dimensions are the ones the plan caps — deployments and
|
|
303
|
+
* domains (`PlatformLimits`) — plus the billable custom-domain subset, so a
|
|
304
|
+
* surface can render "3 of 10" without a second request.
|
|
235
305
|
*/
|
|
236
306
|
export interface AccountUsage {
|
|
237
307
|
/** Number of active custom domains (excludes paused) */
|
|
238
308
|
customDomains: number;
|
|
309
|
+
/**
|
|
310
|
+
* Deployments counted against the plan's deployment cap — every row
|
|
311
|
+
* whatever its status, because that is what the cap counts, so a surface
|
|
312
|
+
* renders "3 of 10" against the denominator the 403 divides by. (`GET
|
|
313
|
+
* /deployments` lists successful ones only; that is a different question
|
|
314
|
+
* asked of a different resource.) Optional by the additive-evolution law:
|
|
315
|
+
* an API predating this field omits it.
|
|
316
|
+
*/
|
|
317
|
+
deployments?: number;
|
|
318
|
+
/**
|
|
319
|
+
* Domains counted against the plan's domain cap — every domain, platform
|
|
320
|
+
* and custom alike, unlike `customDomains`. Optional for the same reason.
|
|
321
|
+
*/
|
|
322
|
+
domains?: number;
|
|
239
323
|
}
|
|
240
324
|
/**
|
|
241
325
|
* Core account object - used in both API responses and SDK
|
|
@@ -282,6 +366,32 @@ export interface AccountGetResponse extends Account {
|
|
|
282
366
|
/** Present only during read-only admin impersonation: the operator's account id. */
|
|
283
367
|
readonly impersonatedBy?: string;
|
|
284
368
|
}
|
|
369
|
+
/**
|
|
370
|
+
* Acknowledgement of `DELETE /account` (202). Termination is asynchronous —
|
|
371
|
+
* a cleanup consumer finishes the job — so the account survives long enough
|
|
372
|
+
* to state the plan it is transitioning through. `plan` is the account's
|
|
373
|
+
* state field, the way `status` is a deployment's. See
|
|
374
|
+
* {@link DeploymentDeleteResponse} for the law.
|
|
375
|
+
*/
|
|
376
|
+
export interface AccountDeleteResponse {
|
|
377
|
+
/** The account that was marked for termination */
|
|
378
|
+
readonly account: string;
|
|
379
|
+
/** The plan the account is in while cleanup runs */
|
|
380
|
+
readonly plan: AccountPlanType;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Response from `PUT /account/key` — the account's single API key, minted in
|
|
384
|
+
* place of whatever was there before.
|
|
385
|
+
*
|
|
386
|
+
* There is no entity to return: only the key's last-4 `hint` is durable
|
|
387
|
+
* (`Account.hint`), and the plaintext exists exactly once, in this response.
|
|
388
|
+
* The raw credential is `secret` on every surface that mints one — the same
|
|
389
|
+
* field `TokenCreateResponse` carries — because one concept gets one name.
|
|
390
|
+
*/
|
|
391
|
+
export interface AccountKeyResponse {
|
|
392
|
+
/** The raw API key (shown once at mint, then never again) */
|
|
393
|
+
readonly secret: string;
|
|
394
|
+
}
|
|
285
395
|
/**
|
|
286
396
|
* Account-specific configuration overrides
|
|
287
397
|
* Allows per-account customization of limits without changing plan
|
|
@@ -421,6 +531,18 @@ export declare class ShipError extends Error {
|
|
|
421
531
|
static file(message: string, details?: unknown): ShipError;
|
|
422
532
|
static config(message: string, details?: unknown): ShipError;
|
|
423
533
|
static api(message: string, status?: number, details?: unknown): ShipError;
|
|
534
|
+
/**
|
|
535
|
+
* The caller is at fault — by HTTP's own definition of a 4xx, or by a type
|
|
536
|
+
* that is client-attributable without ever having a status (`Config`,
|
|
537
|
+
* `File`, raised locally by the SDK).
|
|
538
|
+
*
|
|
539
|
+
* Both arms are load-bearing, because type and status are independent
|
|
540
|
+
* axes. `fromHttpResponse` trusts `body.error` only when it names a
|
|
541
|
+
* server-producible type; a non-OK response without one is status-derived,
|
|
542
|
+
* so a CDN 404 or any intermediary error arrives as `Api` — a server-fault
|
|
543
|
+
* *type* carrying a client *status*. Judging by type alone would report it
|
|
544
|
+
* as a platform failure and bury the server's own message.
|
|
545
|
+
*/
|
|
424
546
|
isClientError(): boolean;
|
|
425
547
|
isNetworkError(): boolean;
|
|
426
548
|
isAuthError(): boolean;
|
|
@@ -645,6 +767,36 @@ export declare const SPA_DEFAULT_CONFIG: {
|
|
|
645
767
|
readonly destination: "/index.html";
|
|
646
768
|
}];
|
|
647
769
|
};
|
|
770
|
+
/**
|
|
771
|
+
* Assert that a ship.json file is *syntactically* loadable. Syntax only —
|
|
772
|
+
* never schema.
|
|
773
|
+
*
|
|
774
|
+
* ship.json is validated and compiled on the server, deliberately: the schema
|
|
775
|
+
* and the compiler evolve, and a client that judged them would reject configs
|
|
776
|
+
* a newer platform accepts. That reasoning bounds what a client may check to
|
|
777
|
+
* the properties which are true of *every* past and future schema:
|
|
778
|
+
*
|
|
779
|
+
* 1. it parses as JSON — JSON syntax is frozen (RFC 8259), so text that
|
|
780
|
+
* does not parse can never be a valid config;
|
|
781
|
+
* 2. its top level is an object — ship.json is `{ ... }` in every version.
|
|
782
|
+
*
|
|
783
|
+
* Both are monotonic: neither can ever reject something the server would
|
|
784
|
+
* accept. Everything beyond them (field names, types, rule semantics, which
|
|
785
|
+
* keys are permitted) stays server-side, where it can change.
|
|
786
|
+
*
|
|
787
|
+
* The payoff is the common case. Hand-edited JSON fails on a trailing comma,
|
|
788
|
+
* a `//` comment, single quotes, unquoted keys, or smart quotes pasted from
|
|
789
|
+
* documentation — mistakes that otherwise cost a full upload round-trip to
|
|
790
|
+
* discover. A UTF-8 BOM (Windows editors, PowerShell redirects) is stripped
|
|
791
|
+
* before parsing rather than rejected, because the server accepts it too;
|
|
792
|
+
* diverging there would reintroduce exactly the false rejection this
|
|
793
|
+
* function exists to avoid.
|
|
794
|
+
*
|
|
795
|
+
* @throws {ShipError} `ErrorType.Config` — the same type the server's own
|
|
796
|
+
* config rejection carries, so the error contract is identical wherever the
|
|
797
|
+
* failure is detected.
|
|
798
|
+
*/
|
|
799
|
+
export declare function assertShipJsonSyntax(text: string): void;
|
|
648
800
|
/**
|
|
649
801
|
* Validate API key format
|
|
650
802
|
*/
|
|
@@ -770,10 +922,20 @@ export interface DeploymentUploadOptions {
|
|
|
770
922
|
captcha?: string;
|
|
771
923
|
}
|
|
772
924
|
/**
|
|
773
|
-
* Pagination options for
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
925
|
+
* Pagination options for every list endpoint. The response's `cursor` feeds
|
|
926
|
+
* the next request; a `null` cursor means the last page. Omitting both
|
|
927
|
+
* returns the server's default first page.
|
|
928
|
+
*
|
|
929
|
+
* A list answers `{ <collection>, cursor }` and nothing else — `cursor`
|
|
930
|
+
* carries the entire has-more signal, so no redundant boolean, and no
|
|
931
|
+
* `total`. **A count is an aggregate over a collection, not a property of a
|
|
932
|
+
* page:** including one makes every read pay for a full scan it did not ask
|
|
933
|
+
* for, which is precisely the cost keyset pagination exists to avoid.
|
|
934
|
+
*
|
|
935
|
+
* Counts therefore live on the summary resource that owns them —
|
|
936
|
+
* `GET /account` (`usage`) for a caller's own totals, `GET /admin/stats` for
|
|
937
|
+
* platform-wide ones. Ask for a count when you want a count; ask for a page
|
|
938
|
+
* when you want a page.
|
|
777
939
|
*/
|
|
778
940
|
export interface ListOptions {
|
|
779
941
|
/** Maximum number of items to return in one page. */
|
|
@@ -809,9 +971,7 @@ export interface DomainResource {
|
|
|
809
971
|
list: (options?: ListOptions) => Promise<DomainListResponse>;
|
|
810
972
|
get: (name: string) => Promise<Domain>;
|
|
811
973
|
remove: (name: string) => Promise<void>;
|
|
812
|
-
verify: (name: string) => Promise<
|
|
813
|
-
message: string;
|
|
814
|
-
}>;
|
|
974
|
+
verify: (name: string) => Promise<DomainVerifyResponse>;
|
|
815
975
|
validate: (name: string) => Promise<DomainValidateResponse>;
|
|
816
976
|
dns: (name: string) => Promise<DomainDnsResponse>;
|
|
817
977
|
records: (name: string) => Promise<DomainRecordsResponse>;
|
|
@@ -834,7 +994,7 @@ export interface TokenResource {
|
|
|
834
994
|
ttl?: number;
|
|
835
995
|
labels?: string[];
|
|
836
996
|
}) => Promise<TokenCreateResponse>;
|
|
837
|
-
list: () => Promise<TokenListResponse>;
|
|
997
|
+
list: (options?: ListOptions) => Promise<TokenListResponse>;
|
|
838
998
|
remove: (token: string) => Promise<void>;
|
|
839
999
|
}
|
|
840
1000
|
/**
|
|
@@ -929,7 +1089,7 @@ export interface ActivityMeta {
|
|
|
929
1089
|
/**
|
|
930
1090
|
* Response from GET /activities endpoint
|
|
931
1091
|
*/
|
|
932
|
-
export interface ActivityListResponse {
|
|
1092
|
+
export interface ActivityListResponse extends ListResponse {
|
|
933
1093
|
/** Array of activities */
|
|
934
1094
|
activities: Activity[];
|
|
935
1095
|
}
|
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.11",
|
|
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,57 @@ 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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Acknowledgement of `DELETE /deployments/:deployment` — and the shape every
|
|
90
|
+
* mutation with no entity left to return follows.
|
|
91
|
+
*
|
|
92
|
+
* **The law:** a mutation answers with the resource it affected. If the
|
|
93
|
+
* resource still exists, that means the entity itself (`Deployment`,
|
|
94
|
+
* `Domain`, …). Otherwise it means this: the resource noun carrying the
|
|
95
|
+
* item's canonical key, plus the resource's own state field — and ONLY when
|
|
96
|
+
* the resource survived in a transitional state, as an async deletion's does.
|
|
97
|
+
* Where the resource is simply gone, the key alone is the whole answer
|
|
98
|
+
* ({@link DomainDeleteResponse}, {@link TokenDeleteResponse}).
|
|
99
|
+
*
|
|
100
|
+
* Nothing else rides along. No prose (`message`), because an acknowledgement
|
|
101
|
+
* is data and each surface composes its own copy; and no constant
|
|
102
|
+
* (`changed: true`, `queued: true`, `success: true`), because a field whose
|
|
103
|
+
* value the type already fixes tells a caller nothing it did not know before
|
|
104
|
+
* it made the request. Sync versus accepted is the HTTP status code's job —
|
|
105
|
+
* 200 versus 202 — not a boolean's.
|
|
106
|
+
*/
|
|
107
|
+
export interface DeploymentDeleteResponse {
|
|
108
|
+
/** The deployment hostname that was marked for removal */
|
|
109
|
+
readonly deployment: string;
|
|
110
|
+
/** The state the deployment is in while background cleanup runs */
|
|
111
|
+
readonly status: DeploymentStatusType;
|
|
71
112
|
}
|
|
72
113
|
|
|
73
114
|
// =============================================================================
|
|
@@ -107,7 +148,7 @@ export interface Domain {
|
|
|
107
148
|
labels: string[];
|
|
108
149
|
/** Unix timestamp (seconds) when domain was created */
|
|
109
150
|
readonly created: number;
|
|
110
|
-
/**
|
|
151
|
+
/** Unix timestamp (seconds) when deployment was last linked, null if never linked */
|
|
111
152
|
linked: number | null;
|
|
112
153
|
/** Total deployment links */
|
|
113
154
|
links: number;
|
|
@@ -131,13 +172,30 @@ export interface DomainSetResult extends Domain {
|
|
|
131
172
|
/**
|
|
132
173
|
* Response for listing domains
|
|
133
174
|
*/
|
|
134
|
-
export interface DomainListResponse {
|
|
175
|
+
export interface DomainListResponse extends ListResponse {
|
|
135
176
|
/** Array of domains */
|
|
136
177
|
domains: Domain[];
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Acknowledgement of `DELETE /domains/:domain`. The row is gone, so there is
|
|
182
|
+
* no state to state — the canonical domain name is the whole answer. See
|
|
183
|
+
* {@link DeploymentDeleteResponse} for the law.
|
|
184
|
+
*/
|
|
185
|
+
export interface DomainDeleteResponse {
|
|
186
|
+
/** The domain name that was removed, normalized */
|
|
187
|
+
readonly domain: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Acknowledgement of `POST /domains/:domain/verify` (202). The DNS check is
|
|
192
|
+
* queued, not performed — the accepted status code says so, and the domain's
|
|
193
|
+
* own status is unchanged until the check runs, which is why none is stated
|
|
194
|
+
* here. See {@link DeploymentDeleteResponse} for the law.
|
|
195
|
+
*/
|
|
196
|
+
export interface DomainVerifyResponse {
|
|
197
|
+
/** The domain whose DNS verification was queued, normalized */
|
|
198
|
+
readonly domain: string;
|
|
141
199
|
}
|
|
142
200
|
|
|
143
201
|
/**
|
|
@@ -206,11 +264,13 @@ export interface DomainValidateResponse {
|
|
|
206
264
|
// =============================================================================
|
|
207
265
|
|
|
208
266
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
267
|
+
* Core deploy token object - used in both API responses and SDK.
|
|
268
|
+
*
|
|
269
|
+
* The secret is never here: it is shown once at creation
|
|
270
|
+
* ({@link TokenCreateResponse.secret}) and never again, so an entity read
|
|
271
|
+
* carries only the management identifier and lifecycle metadata.
|
|
212
272
|
*/
|
|
213
|
-
export interface
|
|
273
|
+
export interface Token {
|
|
214
274
|
/** 7-char management identifier (e.g., "a1b2c3d") */
|
|
215
275
|
readonly token: string;
|
|
216
276
|
/** Labels for categorization and filtering. Always present, empty array when none. */
|
|
@@ -226,25 +286,30 @@ export interface TokenListItem {
|
|
|
226
286
|
/**
|
|
227
287
|
* Response for listing tokens
|
|
228
288
|
*/
|
|
229
|
-
export interface TokenListResponse {
|
|
230
|
-
/** Array of tokens (
|
|
231
|
-
tokens:
|
|
232
|
-
/** Total number of tokens */
|
|
233
|
-
total: number;
|
|
289
|
+
export interface TokenListResponse extends ListResponse {
|
|
290
|
+
/** Array of tokens (the secret is never among them) */
|
|
291
|
+
tokens: Token[];
|
|
234
292
|
}
|
|
235
293
|
|
|
236
294
|
/**
|
|
237
|
-
* Response
|
|
295
|
+
* Response from token creation. Extends Token with the one field that
|
|
296
|
+
* exists only on creation — the same shape as
|
|
297
|
+
* {@link DeploymentCreateResponse}, because a 201 returns the resource it
|
|
298
|
+
* created plus whatever is knowable only once.
|
|
238
299
|
*/
|
|
239
|
-
export interface TokenCreateResponse {
|
|
240
|
-
/** 7-char management identifier */
|
|
241
|
-
token: string;
|
|
300
|
+
export interface TokenCreateResponse extends Token {
|
|
242
301
|
/** The raw credential value (shown once at creation, then never again) */
|
|
243
|
-
secret: string;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
302
|
+
readonly secret: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Acknowledgement of `DELETE /tokens/:token`. The credential is revoked and
|
|
307
|
+
* its row is gone, so the management identifier is the whole answer. See
|
|
308
|
+
* {@link DeploymentDeleteResponse} for the law.
|
|
309
|
+
*/
|
|
310
|
+
export interface TokenDeleteResponse {
|
|
311
|
+
/** The 7-char management identifier that was revoked */
|
|
312
|
+
readonly token: string;
|
|
248
313
|
}
|
|
249
314
|
|
|
250
315
|
// =============================================================================
|
|
@@ -268,10 +333,34 @@ export type AccountPlanType = (typeof AccountPlan)[keyof typeof AccountPlan];
|
|
|
268
333
|
|
|
269
334
|
/**
|
|
270
335
|
* Account usage metrics — always available regardless of billing provider.
|
|
336
|
+
*
|
|
337
|
+
* This is where a caller's own totals live. Lists answer pages and carry no
|
|
338
|
+
* `total` (see {@link ListOptions}); a count is an aggregate over a
|
|
339
|
+
* collection, so it belongs to the summary resource that owns the
|
|
340
|
+
* collection. `GET /account` is that resource for one caller, `GET
|
|
341
|
+
* /admin/stats` for the platform.
|
|
342
|
+
*
|
|
343
|
+
* The counted dimensions are the ones the plan caps — deployments and
|
|
344
|
+
* domains (`PlatformLimits`) — plus the billable custom-domain subset, so a
|
|
345
|
+
* surface can render "3 of 10" without a second request.
|
|
271
346
|
*/
|
|
272
347
|
export interface AccountUsage {
|
|
273
348
|
/** Number of active custom domains (excludes paused) */
|
|
274
349
|
customDomains: number;
|
|
350
|
+
/**
|
|
351
|
+
* Deployments counted against the plan's deployment cap — every row
|
|
352
|
+
* whatever its status, because that is what the cap counts, so a surface
|
|
353
|
+
* renders "3 of 10" against the denominator the 403 divides by. (`GET
|
|
354
|
+
* /deployments` lists successful ones only; that is a different question
|
|
355
|
+
* asked of a different resource.) Optional by the additive-evolution law:
|
|
356
|
+
* an API predating this field omits it.
|
|
357
|
+
*/
|
|
358
|
+
deployments?: number;
|
|
359
|
+
/**
|
|
360
|
+
* Domains counted against the plan's domain cap — every domain, platform
|
|
361
|
+
* and custom alike, unlike `customDomains`. Optional for the same reason.
|
|
362
|
+
*/
|
|
363
|
+
domains?: number;
|
|
275
364
|
}
|
|
276
365
|
|
|
277
366
|
/**
|
|
@@ -321,6 +410,34 @@ export interface AccountGetResponse extends Account {
|
|
|
321
410
|
readonly impersonatedBy?: string;
|
|
322
411
|
}
|
|
323
412
|
|
|
413
|
+
/**
|
|
414
|
+
* Acknowledgement of `DELETE /account` (202). Termination is asynchronous —
|
|
415
|
+
* a cleanup consumer finishes the job — so the account survives long enough
|
|
416
|
+
* to state the plan it is transitioning through. `plan` is the account's
|
|
417
|
+
* state field, the way `status` is a deployment's. See
|
|
418
|
+
* {@link DeploymentDeleteResponse} for the law.
|
|
419
|
+
*/
|
|
420
|
+
export interface AccountDeleteResponse {
|
|
421
|
+
/** The account that was marked for termination */
|
|
422
|
+
readonly account: string;
|
|
423
|
+
/** The plan the account is in while cleanup runs */
|
|
424
|
+
readonly plan: AccountPlanType;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Response from `PUT /account/key` — the account's single API key, minted in
|
|
429
|
+
* place of whatever was there before.
|
|
430
|
+
*
|
|
431
|
+
* There is no entity to return: only the key's last-4 `hint` is durable
|
|
432
|
+
* (`Account.hint`), and the plaintext exists exactly once, in this response.
|
|
433
|
+
* The raw credential is `secret` on every surface that mints one — the same
|
|
434
|
+
* field `TokenCreateResponse` carries — because one concept gets one name.
|
|
435
|
+
*/
|
|
436
|
+
export interface AccountKeyResponse {
|
|
437
|
+
/** The raw API key (shown once at mint, then never again) */
|
|
438
|
+
readonly secret: string;
|
|
439
|
+
}
|
|
440
|
+
|
|
324
441
|
/**
|
|
325
442
|
* Account-specific configuration overrides
|
|
326
443
|
* Allows per-account customization of limits without changing plan
|
|
@@ -397,11 +514,19 @@ const CLIENT_ONLY_ERROR_TYPES = new Set<string>([
|
|
|
397
514
|
* union so `.has(error.type)` accepts any value from the union.
|
|
398
515
|
*/
|
|
399
516
|
const ERROR_CATEGORIES = {
|
|
517
|
+
/**
|
|
518
|
+
* Client-attributable types. Exhaustive over the 4xx-carrying types, and
|
|
519
|
+
* it must include the statusless ones (`Config`, `File`) — those are
|
|
520
|
+
* raised locally by the SDK and have no status for `isClientError`'s
|
|
521
|
+
* second arm to read.
|
|
522
|
+
*/
|
|
400
523
|
client: new Set<ErrorType>([
|
|
401
524
|
ErrorType.Business,
|
|
402
525
|
ErrorType.Config,
|
|
403
526
|
ErrorType.File,
|
|
404
527
|
ErrorType.Forbidden,
|
|
528
|
+
ErrorType.NotFound,
|
|
529
|
+
ErrorType.RateLimit,
|
|
405
530
|
ErrorType.Validation,
|
|
406
531
|
]),
|
|
407
532
|
network: new Set<ErrorType>([ErrorType.Network]),
|
|
@@ -645,10 +770,24 @@ export class ShipError extends Error {
|
|
|
645
770
|
return new ShipError(ErrorType.Api, message, status, details);
|
|
646
771
|
}
|
|
647
772
|
|
|
648
|
-
// Semantic-category
|
|
773
|
+
// Semantic-category guards. For specific-type checks, use
|
|
649
774
|
// `error.type === ErrorType.X` directly or the generic `isType(t)`.
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* The caller is at fault — by HTTP's own definition of a 4xx, or by a type
|
|
778
|
+
* that is client-attributable without ever having a status (`Config`,
|
|
779
|
+
* `File`, raised locally by the SDK).
|
|
780
|
+
*
|
|
781
|
+
* Both arms are load-bearing, because type and status are independent
|
|
782
|
+
* axes. `fromHttpResponse` trusts `body.error` only when it names a
|
|
783
|
+
* server-producible type; a non-OK response without one is status-derived,
|
|
784
|
+
* so a CDN 404 or any intermediary error arrives as `Api` — a server-fault
|
|
785
|
+
* *type* carrying a client *status*. Judging by type alone would report it
|
|
786
|
+
* as a platform failure and bury the server's own message.
|
|
787
|
+
*/
|
|
650
788
|
isClientError(): boolean {
|
|
651
|
-
|
|
789
|
+
if (ERROR_CATEGORIES.client.has(this.type)) return true;
|
|
790
|
+
return this.status !== undefined && this.status >= 400 && this.status < 500;
|
|
652
791
|
}
|
|
653
792
|
|
|
654
793
|
isNetworkError(): boolean {
|
|
@@ -1004,6 +1143,54 @@ export const SPA_DEFAULT_CONFIG = {
|
|
|
1004
1143
|
rewrites: [{ source: '/(.*)', destination: '/index.html' }],
|
|
1005
1144
|
} as const;
|
|
1006
1145
|
|
|
1146
|
+
/**
|
|
1147
|
+
* Assert that a ship.json file is *syntactically* loadable. Syntax only —
|
|
1148
|
+
* never schema.
|
|
1149
|
+
*
|
|
1150
|
+
* ship.json is validated and compiled on the server, deliberately: the schema
|
|
1151
|
+
* and the compiler evolve, and a client that judged them would reject configs
|
|
1152
|
+
* a newer platform accepts. That reasoning bounds what a client may check to
|
|
1153
|
+
* the properties which are true of *every* past and future schema:
|
|
1154
|
+
*
|
|
1155
|
+
* 1. it parses as JSON — JSON syntax is frozen (RFC 8259), so text that
|
|
1156
|
+
* does not parse can never be a valid config;
|
|
1157
|
+
* 2. its top level is an object — ship.json is `{ ... }` in every version.
|
|
1158
|
+
*
|
|
1159
|
+
* Both are monotonic: neither can ever reject something the server would
|
|
1160
|
+
* accept. Everything beyond them (field names, types, rule semantics, which
|
|
1161
|
+
* keys are permitted) stays server-side, where it can change.
|
|
1162
|
+
*
|
|
1163
|
+
* The payoff is the common case. Hand-edited JSON fails on a trailing comma,
|
|
1164
|
+
* a `//` comment, single quotes, unquoted keys, or smart quotes pasted from
|
|
1165
|
+
* documentation — mistakes that otherwise cost a full upload round-trip to
|
|
1166
|
+
* discover. A UTF-8 BOM (Windows editors, PowerShell redirects) is stripped
|
|
1167
|
+
* before parsing rather than rejected, because the server accepts it too;
|
|
1168
|
+
* diverging there would reintroduce exactly the false rejection this
|
|
1169
|
+
* function exists to avoid.
|
|
1170
|
+
*
|
|
1171
|
+
* @throws {ShipError} `ErrorType.Config` — the same type the server's own
|
|
1172
|
+
* config rejection carries, so the error contract is identical wherever the
|
|
1173
|
+
* failure is detected.
|
|
1174
|
+
*/
|
|
1175
|
+
export function assertShipJsonSyntax(text: string): void {
|
|
1176
|
+
const withoutBom = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
1177
|
+
|
|
1178
|
+
let parsed: unknown;
|
|
1179
|
+
try {
|
|
1180
|
+
parsed = JSON.parse(withoutBom);
|
|
1181
|
+
} catch (error) {
|
|
1182
|
+
throw ShipError.config(`invalid JSON format in config: ${(error as Error).message}`, {
|
|
1183
|
+
filePath: DEPLOYMENT_CONFIG_FILENAME,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
1188
|
+
throw ShipError.config(`${DEPLOYMENT_CONFIG_FILENAME} must contain a JSON object`, {
|
|
1189
|
+
filePath: DEPLOYMENT_CONFIG_FILENAME,
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1007
1194
|
// =============================================================================
|
|
1008
1195
|
// VALIDATION UTILITIES
|
|
1009
1196
|
// =============================================================================
|
|
@@ -1233,10 +1420,20 @@ export interface DeploymentUploadOptions {
|
|
|
1233
1420
|
}
|
|
1234
1421
|
|
|
1235
1422
|
/**
|
|
1236
|
-
* Pagination options for
|
|
1237
|
-
*
|
|
1238
|
-
*
|
|
1239
|
-
*
|
|
1423
|
+
* Pagination options for every list endpoint. The response's `cursor` feeds
|
|
1424
|
+
* the next request; a `null` cursor means the last page. Omitting both
|
|
1425
|
+
* returns the server's default first page.
|
|
1426
|
+
*
|
|
1427
|
+
* A list answers `{ <collection>, cursor }` and nothing else — `cursor`
|
|
1428
|
+
* carries the entire has-more signal, so no redundant boolean, and no
|
|
1429
|
+
* `total`. **A count is an aggregate over a collection, not a property of a
|
|
1430
|
+
* page:** including one makes every read pay for a full scan it did not ask
|
|
1431
|
+
* for, which is precisely the cost keyset pagination exists to avoid.
|
|
1432
|
+
*
|
|
1433
|
+
* Counts therefore live on the summary resource that owns them —
|
|
1434
|
+
* `GET /account` (`usage`) for a caller's own totals, `GET /admin/stats` for
|
|
1435
|
+
* platform-wide ones. Ask for a count when you want a count; ask for a page
|
|
1436
|
+
* when you want a page.
|
|
1240
1437
|
*/
|
|
1241
1438
|
export interface ListOptions {
|
|
1242
1439
|
/** Maximum number of items to return in one page. */
|
|
@@ -1274,7 +1471,7 @@ export interface DomainResource {
|
|
|
1274
1471
|
list: (options?: ListOptions) => Promise<DomainListResponse>;
|
|
1275
1472
|
get: (name: string) => Promise<Domain>;
|
|
1276
1473
|
remove: (name: string) => Promise<void>;
|
|
1277
|
-
verify: (name: string) => Promise<
|
|
1474
|
+
verify: (name: string) => Promise<DomainVerifyResponse>;
|
|
1278
1475
|
validate: (name: string) => Promise<DomainValidateResponse>;
|
|
1279
1476
|
dns: (name: string) => Promise<DomainDnsResponse>;
|
|
1280
1477
|
records: (name: string) => Promise<DomainRecordsResponse>;
|
|
@@ -1293,7 +1490,7 @@ export interface AccountResource {
|
|
|
1293
1490
|
*/
|
|
1294
1491
|
export interface TokenResource {
|
|
1295
1492
|
create: (options?: { ttl?: number; labels?: string[] }) => Promise<TokenCreateResponse>;
|
|
1296
|
-
list: () => Promise<TokenListResponse>;
|
|
1493
|
+
list: (options?: ListOptions) => Promise<TokenListResponse>;
|
|
1297
1494
|
remove: (token: string) => Promise<void>;
|
|
1298
1495
|
}
|
|
1299
1496
|
|
|
@@ -1477,7 +1674,7 @@ export interface ActivityMeta {
|
|
|
1477
1674
|
/**
|
|
1478
1675
|
* Response from GET /activities endpoint
|
|
1479
1676
|
*/
|
|
1480
|
-
export interface ActivityListResponse {
|
|
1677
|
+
export interface ActivityListResponse extends ListResponse {
|
|
1481
1678
|
/** Array of activities */
|
|
1482
1679
|
activities: Activity[];
|
|
1483
1680
|
}
|