@shipstatic/types 2.5.0-beta.9 → 2.6.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/README.md +16 -6
- package/dist/index.d.ts +595 -77
- package/dist/index.js +495 -17
- package/package.json +6 -1
- package/src/index.ts +840 -73
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* This package is the single source of truth for all shared data structures.
|
|
4
4
|
*/
|
|
5
5
|
// =============================================================================
|
|
6
|
-
//
|
|
6
|
+
// DEPLOYMENT TYPES
|
|
7
7
|
// =============================================================================
|
|
8
8
|
/**
|
|
9
9
|
* Deployment status constants
|
|
@@ -14,6 +14,27 @@ export const DeploymentStatus = {
|
|
|
14
14
|
FAILED: 'failed',
|
|
15
15
|
DELETING: 'deleting',
|
|
16
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* Which client made a deployment — the origin-tracking vocabulary.
|
|
19
|
+
*
|
|
20
|
+
* A closed set with many authors: the CLI, the SDK, the dashboard, both MCP
|
|
21
|
+
* transports, the GitHub Action, the n8n node and the VS Code extension each
|
|
22
|
+
* name themselves here. It lived in the API's config until 2026-08-06, where
|
|
23
|
+
* being server-side made it unenforceable in the one direction that matters —
|
|
24
|
+
* every client wrote a bare string, and a value outside the set was **silently
|
|
25
|
+
* dropped** by the server, so a typo did not fail anywhere. It stopped
|
|
26
|
+
* recording where deploys came from and said nothing.
|
|
27
|
+
*/
|
|
28
|
+
export const DeploymentVia = {
|
|
29
|
+
WEB: 'web',
|
|
30
|
+
SDK: 'sdk',
|
|
31
|
+
CLI: 'cli',
|
|
32
|
+
MCP: 'mcp',
|
|
33
|
+
GIT: 'git',
|
|
34
|
+
N8N: 'n8n',
|
|
35
|
+
GPT: 'gpt',
|
|
36
|
+
VSC: 'vsc',
|
|
37
|
+
};
|
|
17
38
|
// =============================================================================
|
|
18
39
|
// DOMAIN TYPES
|
|
19
40
|
// =============================================================================
|
|
@@ -31,6 +52,68 @@ export const DomainStatus = {
|
|
|
31
52
|
SUCCESS: 'success',
|
|
32
53
|
PAUSED: 'paused',
|
|
33
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* The envelope an `Idempotency-Key` must fit, and how long a replay lasts.
|
|
57
|
+
*
|
|
58
|
+
* Format lives here rather than on the server alone by the format-vs-policy
|
|
59
|
+
* rule: a client can decide offline whether a key is well-formed, and the
|
|
60
|
+
* API would reject the same value the same way.
|
|
61
|
+
*/
|
|
62
|
+
export const IDEMPOTENCY_KEY_CONSTRAINTS = {
|
|
63
|
+
/**
|
|
64
|
+
* HTTP header name. Here for the same reason {@link CALLER.HEADER} is: a
|
|
65
|
+
* wire header has two ends, and the package that owns the value's format
|
|
66
|
+
* is the only place both ends can read its name from.
|
|
67
|
+
*/
|
|
68
|
+
HEADER: 'Idempotency-Key',
|
|
69
|
+
MAX_LENGTH: 256,
|
|
70
|
+
/** How long a stored 201 stays replayable. */
|
|
71
|
+
WINDOW_SECONDS: 24 * 60 * 60,
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Normalize a `via` value from any transport — trimmed, lowercased, and a
|
|
75
|
+
* member of {@link DeploymentVia}, or `undefined`.
|
|
76
|
+
*
|
|
77
|
+
* A format rule by this package's own test: a client can decide offline
|
|
78
|
+
* whether a value is well-formed, and the API reaches the same verdict on the
|
|
79
|
+
* same input. It lived server-side until 2026-08-06, which meant clients could
|
|
80
|
+
* only learn their label was unusable by noticing analytics had gone quiet.
|
|
81
|
+
*
|
|
82
|
+
* **Not knowing your `via` is not an error** — an unrecognized value yields
|
|
83
|
+
* `undefined` rather than throwing, because origin tracking is telemetry and a
|
|
84
|
+
* deploy must never fail over it. A caller that has an honest default should
|
|
85
|
+
* prefer it (`normalizeVia(process.env.SHIP_VIA) ?? DeploymentVia.CLI`): the
|
|
86
|
+
* deploy really did come from the CLI, so recording that beats recording
|
|
87
|
+
* nothing.
|
|
88
|
+
*/
|
|
89
|
+
export function normalizeVia(value) {
|
|
90
|
+
if (!value || typeof value !== 'string')
|
|
91
|
+
return undefined;
|
|
92
|
+
const via = value.trim().toLowerCase();
|
|
93
|
+
return Object.values(DeploymentVia).includes(via)
|
|
94
|
+
? via
|
|
95
|
+
: undefined;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Validate an idempotency key, returning the trimmed value or `undefined`
|
|
99
|
+
* when none was supplied. Throws {@link ShipError.validation} when the value
|
|
100
|
+
* cannot be sent — the same verdict the API would reach, reached earlier.
|
|
101
|
+
*/
|
|
102
|
+
export function validateIdempotencyKey(value) {
|
|
103
|
+
if (value === undefined || value === null)
|
|
104
|
+
return undefined;
|
|
105
|
+
if (typeof value !== 'string') {
|
|
106
|
+
throw ShipError.validation('Idempotency key must be a string.');
|
|
107
|
+
}
|
|
108
|
+
const key = value.trim();
|
|
109
|
+
if (!key) {
|
|
110
|
+
throw ShipError.validation('Idempotency key must not be empty.');
|
|
111
|
+
}
|
|
112
|
+
if (key.length > IDEMPOTENCY_KEY_CONSTRAINTS.MAX_LENGTH) {
|
|
113
|
+
throw ShipError.validation(`Idempotency key must be at most ${IDEMPOTENCY_KEY_CONSTRAINTS.MAX_LENGTH} characters.`);
|
|
114
|
+
}
|
|
115
|
+
return key;
|
|
116
|
+
}
|
|
34
117
|
// =============================================================================
|
|
35
118
|
// ACCOUNT TYPES
|
|
36
119
|
// =============================================================================
|
|
@@ -47,6 +130,100 @@ export const AccountPlan = {
|
|
|
47
130
|
TERMINATED: 'terminated',
|
|
48
131
|
};
|
|
49
132
|
// =============================================================================
|
|
133
|
+
// WIRE SURFACE
|
|
134
|
+
// =============================================================================
|
|
135
|
+
/**
|
|
136
|
+
* Every path the public API answers on, declared once.
|
|
137
|
+
*
|
|
138
|
+
* The URL surface was written out in four places — the API's mounts, the
|
|
139
|
+
* SDK's client, the dashboard's client, and the post-deploy smoke — so a
|
|
140
|
+
* rename meant finding all four. The first three now read this table.
|
|
141
|
+
*
|
|
142
|
+
* The smoke (`cloudflare/api/smoke.mjs`) deliberately still spells its own:
|
|
143
|
+
* five of its nine paths are `/admin/*`, which this table excludes by
|
|
144
|
+
* design, and splitting one list between a registry and literals reads worse
|
|
145
|
+
* than keeping it uniform.
|
|
146
|
+
*
|
|
147
|
+
* **What this guarantees, exactly.** Collection paths are mounted from here,
|
|
148
|
+
* so producer and consumer cannot diverge. Item paths are declared here and
|
|
149
|
+
* consumed by clients, but the API spells them relative to their mount
|
|
150
|
+
* (`/:deployment/config`), so the table does not *generate* them — it is
|
|
151
|
+
* held to them by `api/tests/architecture/api-paths.test.ts`, which fails if
|
|
152
|
+
* any entry names a path no route answers. Some entries have no client yet
|
|
153
|
+
* (`DEPLOYMENT_CONFIG`, `DOMAIN_PROPAGATION` — endpoints the SDK
|
|
154
|
+
* deliberately does not reach); the fence is what keeps those honest rather
|
|
155
|
+
* than merely asserted.
|
|
156
|
+
*
|
|
157
|
+
* **The operator surface is deliberately absent.** `/admin/*` paths belong
|
|
158
|
+
* to `web/my`, for the same reason its row types do: this package is
|
|
159
|
+
* published, and the operator surface is not public (see `CLAUDE.md`, "Admin
|
|
160
|
+
* types"). A path here is a promise to every npm consumer; `/admin` is a
|
|
161
|
+
* promise to one dashboard.
|
|
162
|
+
*
|
|
163
|
+
* Item paths are functions rather than templates so the key is interpolated
|
|
164
|
+
* in one place, encoded the same way by every caller.
|
|
165
|
+
*/
|
|
166
|
+
export const API_PATHS = {
|
|
167
|
+
DEPLOYMENTS: '/deployments',
|
|
168
|
+
DEPLOYMENT: (deployment) => `/deployments/${deployment}`,
|
|
169
|
+
DEPLOYMENT_CONFIG: (deployment) => `/deployments/${deployment}/config`,
|
|
170
|
+
DOMAINS: '/domains',
|
|
171
|
+
DOMAIN: (domain) => `/domains/${domain}`,
|
|
172
|
+
DOMAIN_VERIFY: (domain) => `/domains/${domain}/verify`,
|
|
173
|
+
DOMAIN_DNS: (domain) => `/domains/${domain}/dns`,
|
|
174
|
+
DOMAIN_RECORDS: (domain) => `/domains/${domain}/records`,
|
|
175
|
+
DOMAIN_SHARE: (domain) => `/domains/${domain}/share`,
|
|
176
|
+
DOMAIN_PROPAGATION: (domain) => `/domains/${domain}/propagation`,
|
|
177
|
+
DOMAINS_VALIDATE: '/domains/validate',
|
|
178
|
+
TOKENS: '/tokens',
|
|
179
|
+
TOKEN: (token) => `/tokens/${token}`,
|
|
180
|
+
ACCOUNT: '/account',
|
|
181
|
+
ACCOUNT_KEY: '/account/key',
|
|
182
|
+
ACCOUNT_CLAIM: '/account/claim',
|
|
183
|
+
ACTIVITIES: '/activities',
|
|
184
|
+
LABELS: '/labels',
|
|
185
|
+
LIMITS: '/limits',
|
|
186
|
+
PING: '/ping',
|
|
187
|
+
SETUP: '/setup',
|
|
188
|
+
SPA_CHECK: '/spa-check',
|
|
189
|
+
UPLOAD: '/upload',
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* The deploy request's multipart field names — the other half of the wire
|
|
193
|
+
* surface beside {@link API_PATHS}. `POST /deployments` (and the first-party
|
|
194
|
+
* `/upload`) is multipart/form-data, and these are the names the API reads.
|
|
195
|
+
*
|
|
196
|
+
* Declared once because the body has three independent WRITERS — the SDK's
|
|
197
|
+
* Node and browser body builders, and the n8n community node's hand-rolled
|
|
198
|
+
* client (which cannot import this under n8n Cloud's zero-dependency rule,
|
|
199
|
+
* and fences its restated copy instead) — and until this export every writer
|
|
200
|
+
* restated the strings the API parses, with nothing comparing them.
|
|
201
|
+
*
|
|
202
|
+
* `FILES` carries one entry per file (the API reads it with `getAll`); every
|
|
203
|
+
* other field is single. The `@internal` flags are serialized as the literal
|
|
204
|
+
* string `'true'` and belong to first-party surfaces only.
|
|
205
|
+
*/
|
|
206
|
+
export const DEPLOY_FIELDS = {
|
|
207
|
+
/** One entry per file — read with `getAll`. */
|
|
208
|
+
FILES: 'files[]',
|
|
209
|
+
/** JSON array of MD5 hex digests, index-aligned with `FILES`. */
|
|
210
|
+
CHECKSUMS: 'checksums',
|
|
211
|
+
/** JSON array of label strings. */
|
|
212
|
+
LABELS: 'labels',
|
|
213
|
+
/** The deploying surface's {@link DeploymentVia} member. */
|
|
214
|
+
VIA: 'via',
|
|
215
|
+
/** Plaintext password — the API hashes it server-side. */
|
|
216
|
+
PASSWORD: 'password',
|
|
217
|
+
/** @internal Server-processing flag — first-party `/upload` only. */
|
|
218
|
+
BUILD: 'build',
|
|
219
|
+
/** @internal Server-processing flag — first-party `/upload` only. */
|
|
220
|
+
PRERENDER: 'prerender',
|
|
221
|
+
/** @internal Server-processing flag — first-party `/upload` only. */
|
|
222
|
+
SPA: 'spa',
|
|
223
|
+
/** @internal reCAPTCHA proof — `web/www`'s public uploader only. */
|
|
224
|
+
CAPTCHA: 'captcha',
|
|
225
|
+
};
|
|
226
|
+
// =============================================================================
|
|
50
227
|
// ERROR SYSTEM
|
|
51
228
|
// =============================================================================
|
|
52
229
|
/**
|
|
@@ -59,7 +236,15 @@ export const AccountPlan = {
|
|
|
59
236
|
* (`DeploymentStatus`, `DomainStatus`, `AccountPlan`, `AuthMethod`) follow.
|
|
60
237
|
*/
|
|
61
238
|
export const ErrorType = {
|
|
62
|
-
/**
|
|
239
|
+
/**
|
|
240
|
+
* Validation failed. Input shape is wrong.
|
|
241
|
+
*
|
|
242
|
+
* Carries 400 when an API judged it — including a client-side pre-check of a
|
|
243
|
+
* rule the server enforces too, which keeps the error identical wherever it
|
|
244
|
+
* was caught. **Statusless** when a client rejects something no API judges,
|
|
245
|
+
* such as a CLI's own command grammar: `status` is documented "(API
|
|
246
|
+
* contexts)" on `ErrorResponse`, so there is none to report.
|
|
247
|
+
*/
|
|
63
248
|
Validation: 'validation_failed',
|
|
64
249
|
/** Resource not found (404). */
|
|
65
250
|
NotFound: 'not_found',
|
|
@@ -73,6 +258,17 @@ export const ErrorType = {
|
|
|
73
258
|
Business: 'business_logic_error',
|
|
74
259
|
/** API server error (500). Generic server-side fault. */
|
|
75
260
|
Api: 'internal_server_error',
|
|
261
|
+
/**
|
|
262
|
+
* The platform is closed for maintenance (503). A deliberate operator
|
|
263
|
+
* state, not a fault — nothing errored; the API is refusing work on
|
|
264
|
+
* purpose, and deployed sites keep serving throughout.
|
|
265
|
+
*
|
|
266
|
+
* Distinct from `Api` at 503, which the platform already uses for a
|
|
267
|
+
* dependency that failed (moderation unavailable). A consumer has to tell
|
|
268
|
+
* "we closed the door" from "something broke": the two get opposite words
|
|
269
|
+
* and opposite retry behaviour.
|
|
270
|
+
*/
|
|
271
|
+
Maintenance: 'maintenance',
|
|
76
272
|
/** Network/connection error. Client-side only — set by HTTP clients on fetch failure; never produced server-side. */
|
|
77
273
|
Network: 'network_error',
|
|
78
274
|
/** Operation was cancelled. Client-side only — set on `AbortSignal` abort; never produced server-side. */
|
|
@@ -102,12 +298,19 @@ const CLIENT_ONLY_ERROR_TYPES = new Set([
|
|
|
102
298
|
const ERROR_CATEGORIES = {
|
|
103
299
|
/**
|
|
104
300
|
* Client-attributable types. Exhaustive over the 4xx-carrying types, and
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
301
|
+
* over the statusless ones too — those are raised locally and have no
|
|
302
|
+
* status for `isClientError`'s second arm to read, so omitting one makes it
|
|
303
|
+
* read as a server fault. The rule is the membership test: every type in
|
|
304
|
+
* `CLIENT_ONLY_ERROR_TYPES` except `Network` (which `isNetworkError` owns)
|
|
305
|
+
* belongs here.
|
|
306
|
+
*
|
|
307
|
+
* `Cancelled` was missing until 2026-07-29, which is exactly that failure:
|
|
308
|
+
* a caller who aborted their own deploy was told "server error: please try
|
|
309
|
+
* again" — the CLI's fallback for everything this set does not claim.
|
|
108
310
|
*/
|
|
109
311
|
client: new Set([
|
|
110
312
|
ErrorType.Business,
|
|
313
|
+
ErrorType.Cancelled,
|
|
111
314
|
ErrorType.Config,
|
|
112
315
|
ErrorType.File,
|
|
113
316
|
ErrorType.Forbidden,
|
|
@@ -126,6 +329,50 @@ const ERROR_CATEGORIES = {
|
|
|
126
329
|
* `ErrorType` is automatically picked up.
|
|
127
330
|
*/
|
|
128
331
|
const SERVER_PRODUCIBLE_ERROR_TYPES = new Set(Object.values(ErrorType).filter((t) => !CLIENT_ONLY_ERROR_TYPES.has(t)));
|
|
332
|
+
/**
|
|
333
|
+
* Ceiling on a message adopted from a **non-JSON** error body — a foreign
|
|
334
|
+
* responder's, never this platform's. Generous for the plain-text one-liners
|
|
335
|
+
* intermediaries actually send (`error code: 1015`), far below a document.
|
|
336
|
+
* Our own messages are never measured against it: a JSON body is the API's
|
|
337
|
+
* contract, and truncating a long validation message would be the bug.
|
|
338
|
+
*/
|
|
339
|
+
const MAX_FOREIGN_MESSAGE_LENGTH = 200;
|
|
340
|
+
/**
|
|
341
|
+
* Did the runtime say the exchange never completed?
|
|
342
|
+
*
|
|
343
|
+
* WHATWG has `fetch` reject with a **TypeError** on network error, and undici,
|
|
344
|
+
* Chromium and Firefox comply. Bun does not: it rejects with a plain `Error`
|
|
345
|
+
* carrying a system `code` string. Captured 2026-08-05 (the capture script is
|
|
346
|
+
* in `tests/errors.test.ts`, "runtime failure shapes"):
|
|
347
|
+
*
|
|
348
|
+
* | failure | Node 22 / undici | Bun 1.3.14 |
|
|
349
|
+
* |---------------|---------------------------|----------------------------------------------|
|
|
350
|
+
* | refused | `TypeError: fetch failed` | `Error` `code: 'ConnectionRefused'` |
|
|
351
|
+
* | DNS failure | `TypeError: fetch failed` | `Error` `code: 'ConnectionRefused'` |
|
|
352
|
+
* | reset | `TypeError: fetch failed` | `Error` `code: 'ECONNRESET'` |
|
|
353
|
+
* | TLS rejected | `TypeError: fetch failed` | `Error` `code: 'UNKNOWN_CERTIFICATE_…ERROR'` |
|
|
354
|
+
*
|
|
355
|
+
* So the test is the **evidence, not a list of dialect strings**: a string
|
|
356
|
+
* `code` is a runtime naming a transport-level failure. An allowlist of codes
|
|
357
|
+
* was written first and rejected — the TLS row alone would mean enumerating
|
|
358
|
+
* BoringSSL's certificate table, and a code nobody guessed is precisely the bug
|
|
359
|
+
* this closes. Two kinds of error are deliberately NOT caught: ordinary JS
|
|
360
|
+
* faults carry no `code` at all, and a `DOMException`'s is a **number**, so
|
|
361
|
+
* aborts and timeouts fall through to their own arms.
|
|
362
|
+
*
|
|
363
|
+
* The accepted trade: a caller's `TokenProvider` that throws a coded error
|
|
364
|
+
* (`ENOENT` from a keychain read) is typed `Network` rather than `Api`. Both
|
|
365
|
+
* are wrong for it, `Network` is the cheaper wrong — it says "nothing was
|
|
366
|
+
* exchanged", which is true, where `Api` claims a server answered.
|
|
367
|
+
*/
|
|
368
|
+
function isTransportFailure(cause) {
|
|
369
|
+
if (typeof cause.code === 'string')
|
|
370
|
+
return true;
|
|
371
|
+
// Spec runtimes put no code on the rejection itself. The message test is what
|
|
372
|
+
// keeps fetch's ARGUMENT errors out — `Failed to parse URL from …` is a
|
|
373
|
+
// caller's config mistake, not a transport failure.
|
|
374
|
+
return cause instanceof TypeError && cause.message.includes('fetch');
|
|
375
|
+
}
|
|
129
376
|
/**
|
|
130
377
|
* Simple unified error class for both API and SDK
|
|
131
378
|
*/
|
|
@@ -197,9 +444,17 @@ export class ShipError extends Error {
|
|
|
197
444
|
}
|
|
198
445
|
}
|
|
199
446
|
else {
|
|
200
|
-
|
|
201
|
-
|
|
447
|
+
// A non-JSON body did not come from this platform — every API error
|
|
448
|
+
// is `ErrorResponse` JSON — so it is an intermediary's output, and
|
|
449
|
+
// the two kinds it produces need opposite treatment. A CDN's plain
|
|
450
|
+
// `error code: 1015` is the most useful thing there is to say. A
|
|
451
|
+
// proxy's HTML error page is a *document*, not a message: adopting it
|
|
452
|
+
// verbatim made a misconfigured `apiUrl` print 2,059 characters of
|
|
453
|
+
// markup as the error. Trust it only when it reads as a message.
|
|
454
|
+
const text = (await response.text()).trim();
|
|
455
|
+
if (text && !text.startsWith('<') && text.length <= MAX_FOREIGN_MESSAGE_LENGTH) {
|
|
202
456
|
message = text;
|
|
457
|
+
}
|
|
203
458
|
}
|
|
204
459
|
}
|
|
205
460
|
catch {
|
|
@@ -243,7 +498,8 @@ export class ShipError extends Error {
|
|
|
243
498
|
* Routing:
|
|
244
499
|
* - Already a `ShipError` → returned as-is (caller's intent preserved)
|
|
245
500
|
* - `AbortError` → `ShipError.cancelled(...)`
|
|
246
|
-
* -
|
|
501
|
+
* - A transport failure → `ShipError.network(...)` — see `isTransportFailure`
|
|
502
|
+
* for what each runtime offers as evidence
|
|
247
503
|
* - Any other `Error` → `ShipError(Api, ...)` (no HTTP status — fetch never reached the server)
|
|
248
504
|
* - Anything else (string, undefined, etc.) → `ShipError(Api, ...)`
|
|
249
505
|
*
|
|
@@ -259,7 +515,7 @@ export class ShipError extends Error {
|
|
|
259
515
|
if (cause.name === 'AbortError') {
|
|
260
516
|
return ShipError.cancelled(`${op} was cancelled`);
|
|
261
517
|
}
|
|
262
|
-
if (cause
|
|
518
|
+
if (isTransportFailure(cause)) {
|
|
263
519
|
return ShipError.network(`${op} failed: ${cause.message}`, { cause });
|
|
264
520
|
}
|
|
265
521
|
return new ShipError(ErrorType.Api, `${op} failed: ${cause.message}`);
|
|
@@ -317,6 +573,18 @@ export class ShipError extends Error {
|
|
|
317
573
|
static api(message, status = 500, details) {
|
|
318
574
|
return new ShipError(ErrorType.Api, message, status, details);
|
|
319
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* The platform is closed for maintenance (503).
|
|
578
|
+
*
|
|
579
|
+
* `message` is REQUIRED and has no default here. The API is the only
|
|
580
|
+
* producer of that sentence, and a default in this file would be a second
|
|
581
|
+
* owner of one fact — see CLAUDE.md, "The Constellation Law" (stopping
|
|
582
|
+
* rule). It is also the one factory whose status is fixed rather than
|
|
583
|
+
* defaulted: a maintenance refusal is 503 or it is not this error.
|
|
584
|
+
*/
|
|
585
|
+
static maintenance(message, details) {
|
|
586
|
+
return new ShipError(ErrorType.Maintenance, message, 503, details);
|
|
587
|
+
}
|
|
320
588
|
// Semantic-category guards. For specific-type checks, use
|
|
321
589
|
// `error.type === ErrorType.X` directly or the generic `isType(t)`.
|
|
322
590
|
/**
|
|
@@ -441,6 +709,125 @@ export function isBlockedExtension(filename) {
|
|
|
441
709
|
return BLOCKED_EXTENSIONS.has(ext);
|
|
442
710
|
}
|
|
443
711
|
// =============================================================================
|
|
712
|
+
// PICKER ACCEPT HINT
|
|
713
|
+
// =============================================================================
|
|
714
|
+
/**
|
|
715
|
+
* The extensions a browser file picker offers by default, grouped by role.
|
|
716
|
+
*
|
|
717
|
+
* Private on purpose: the only published form is `WEB_FILE_ACCEPT`, the
|
|
718
|
+
* attribute value itself. A published set would invite a call site to ask it
|
|
719
|
+
* whether a file is allowed — which is the one thing this list must never
|
|
720
|
+
* answer. See `WEB_FILE_ACCEPT`.
|
|
721
|
+
*
|
|
722
|
+
* Extensionless files (`LICENSE`, most `.well-known` entries) are inexpressible
|
|
723
|
+
* in `accept`, and reach a deployment by folder pick, ZIP, or drag-and-drop.
|
|
724
|
+
*/
|
|
725
|
+
const WEB_FILE_EXTENSIONS = [
|
|
726
|
+
// Markup & documents
|
|
727
|
+
'html',
|
|
728
|
+
'htm',
|
|
729
|
+
'xhtml',
|
|
730
|
+
'xml',
|
|
731
|
+
'txt',
|
|
732
|
+
'md',
|
|
733
|
+
'markdown',
|
|
734
|
+
'pdf',
|
|
735
|
+
'csv',
|
|
736
|
+
// Data & config
|
|
737
|
+
'json',
|
|
738
|
+
'jsonc',
|
|
739
|
+
'webmanifest',
|
|
740
|
+
'map',
|
|
741
|
+
'toml',
|
|
742
|
+
'yaml',
|
|
743
|
+
'yml',
|
|
744
|
+
'rss',
|
|
745
|
+
'atom',
|
|
746
|
+
// Styles
|
|
747
|
+
'css',
|
|
748
|
+
'scss',
|
|
749
|
+
'sass',
|
|
750
|
+
'less',
|
|
751
|
+
// Scripts & modules
|
|
752
|
+
'js',
|
|
753
|
+
'mjs',
|
|
754
|
+
'cjs',
|
|
755
|
+
'jsx',
|
|
756
|
+
'ts',
|
|
757
|
+
'tsx',
|
|
758
|
+
'wasm',
|
|
759
|
+
'vue',
|
|
760
|
+
'svelte',
|
|
761
|
+
// Images
|
|
762
|
+
'png',
|
|
763
|
+
'jpg',
|
|
764
|
+
'jpeg',
|
|
765
|
+
'gif',
|
|
766
|
+
'webp',
|
|
767
|
+
'avif',
|
|
768
|
+
'svg',
|
|
769
|
+
'ico',
|
|
770
|
+
'bmp',
|
|
771
|
+
'tif',
|
|
772
|
+
'tiff',
|
|
773
|
+
'heic',
|
|
774
|
+
'heif',
|
|
775
|
+
// Fonts
|
|
776
|
+
'woff',
|
|
777
|
+
'woff2',
|
|
778
|
+
'ttf',
|
|
779
|
+
'otf',
|
|
780
|
+
'eot',
|
|
781
|
+
// Audio
|
|
782
|
+
'mp3',
|
|
783
|
+
'wav',
|
|
784
|
+
'ogg',
|
|
785
|
+
'oga',
|
|
786
|
+
'opus',
|
|
787
|
+
'm4a',
|
|
788
|
+
'aac',
|
|
789
|
+
'flac',
|
|
790
|
+
'weba',
|
|
791
|
+
// Video
|
|
792
|
+
'mp4',
|
|
793
|
+
'webm',
|
|
794
|
+
'ogv',
|
|
795
|
+
'mov',
|
|
796
|
+
'm4v',
|
|
797
|
+
'avi',
|
|
798
|
+
// 3D models
|
|
799
|
+
'glb',
|
|
800
|
+
'gltf',
|
|
801
|
+
'usdz',
|
|
802
|
+
// Text tracks
|
|
803
|
+
'vtt',
|
|
804
|
+
'srt',
|
|
805
|
+
// Archive — a whole site in one file
|
|
806
|
+
'zip',
|
|
807
|
+
];
|
|
808
|
+
/**
|
|
809
|
+
* The `accept` attribute value for a browser file picker offering web files.
|
|
810
|
+
*
|
|
811
|
+
* **This is a hint, never a rule.** `BLOCKED_EXTENSIONS` is the platform's
|
|
812
|
+
* gate and the only thing that decides what may be hosted; this constant
|
|
813
|
+
* decides what a *file dialog* shows first. The two are not two halves of one
|
|
814
|
+
* policy, and this one must never be consulted to accept or reject a file.
|
|
815
|
+
*
|
|
816
|
+
* The distinction is structural, not stylistic. `accept` can express only an
|
|
817
|
+
* allowlist, while the platform's rule is a blocklist — so this list is
|
|
818
|
+
* necessarily *narrower* than what the platform hosts, and reading it as
|
|
819
|
+
* authority would reject files the platform serves happily. It is also not
|
|
820
|
+
* enforcement in the browser's own terms: every file dialog offers an
|
|
821
|
+
* all-files escape, and **drag-and-drop ignores `accept` entirely**. The
|
|
822
|
+
* dropzone and the picker must reach the same verdict on the same files, and
|
|
823
|
+
* they do — because the verdict is `validateFiles`, downstream of both.
|
|
824
|
+
*
|
|
825
|
+
* Kept beside `BLOCKED_EXTENSIONS` so one file holds both, which is what lets
|
|
826
|
+
* `tests/validation-constants.test.ts` fence the invariant that matters: the
|
|
827
|
+
* picker must never offer a file the platform will refuse.
|
|
828
|
+
*/
|
|
829
|
+
export const WEB_FILE_ACCEPT = WEB_FILE_EXTENSIONS.map((ext) => `.${ext}`).join(',');
|
|
830
|
+
// =============================================================================
|
|
444
831
|
// FILENAME CHARACTER VALIDATION
|
|
445
832
|
// =============================================================================
|
|
446
833
|
/**
|
|
@@ -499,6 +886,28 @@ export function hasUnbuiltMarker(filePath) {
|
|
|
499
886
|
// that distinguish populations on the wire (API_KEY, DEPLOY_TOKEN, CALLER),
|
|
500
887
|
// the single dispatch over them (TokenKind, classifyToken), and the
|
|
501
888
|
// delegated-access scopes (OAuthScope).
|
|
889
|
+
//
|
|
890
|
+
// THE SHAPE LAW, in three clauses. Every secret the platform mints obeys it,
|
|
891
|
+
// and `tests/validation-constants.test.ts` holds all three mechanically.
|
|
892
|
+
//
|
|
893
|
+
// 1. ONE ENTROPY STANDARD. Every minted random secret is `HEX_LENGTH` hex
|
|
894
|
+
// characters — one width for the whole platform, so "how long is a
|
|
895
|
+
// credential" has a single answer rather than one per population.
|
|
896
|
+
//
|
|
897
|
+
// 2. A PREFIX MARKS A SHARED SLOT, AND NOTHING ELSE. API keys and deploy
|
|
898
|
+
// tokens both arrive as `Authorization: Bearer`, so something must say
|
|
899
|
+
// which population a value belongs to: that is what the prefix IS, and
|
|
900
|
+
// `classifyToken` below is its only reader. Secrets that arrive somewhere
|
|
901
|
+
// unambiguous carry none — the deployment claim code reaches its own
|
|
902
|
+
// route in its own field, inside a URL whose path already says `/claim/`,
|
|
903
|
+
// so a prefix there would be a second name for what the route states.
|
|
904
|
+
//
|
|
905
|
+
// 3. NO PREFIX IS A PREFIX OF ANOTHER. This is what makes the dispatch
|
|
906
|
+
// order-independent, and it is the reason the populations are named on
|
|
907
|
+
// different axes (`ship-` for the product, `deploy-` for the capability)
|
|
908
|
+
// rather than sharing a stem. A `ship-` / `ship-deploy-` pair reads tidier
|
|
909
|
+
// and is a trap: every deploy token would also match the API-key branch,
|
|
910
|
+
// leaving correctness resting on the order of two `if`s.
|
|
502
911
|
/**
|
|
503
912
|
* Where human identity is mounted on the API host. The API mounts Better
|
|
504
913
|
* Auth at this path (sign-in, sign-out, session reads, admin impersonation)
|
|
@@ -526,30 +935,35 @@ export const AuthMethod = {
|
|
|
526
935
|
SYSTEM: 'system',
|
|
527
936
|
};
|
|
528
937
|
/**
|
|
529
|
-
* Shape constants for API keys (`ship-{
|
|
938
|
+
* Shape constants for API keys (`ship-{32 hex chars}`).
|
|
530
939
|
* Single source of truth used by validation utilities and auth middleware.
|
|
531
940
|
*/
|
|
532
941
|
export const API_KEY = {
|
|
533
942
|
/** Prefix that identifies an API key. */
|
|
534
943
|
PREFIX: 'ship-',
|
|
535
944
|
/** Number of hex characters following the prefix. */
|
|
536
|
-
HEX_LENGTH:
|
|
537
|
-
/** Total length of an API key including prefix (`PREFIX.length + HEX_LENGTH =
|
|
538
|
-
TOTAL_LENGTH:
|
|
945
|
+
HEX_LENGTH: 32,
|
|
946
|
+
/** Total length of an API key including prefix (`PREFIX.length + HEX_LENGTH = 37`). */
|
|
947
|
+
TOTAL_LENGTH: 37,
|
|
539
948
|
/** Number of trailing characters used to display a redacted hint (e.g. last 4). */
|
|
540
949
|
HINT_LENGTH: 4,
|
|
541
950
|
};
|
|
542
951
|
/**
|
|
543
|
-
* Shape constants for deploy tokens (`deploy-{
|
|
952
|
+
* Shape constants for deploy tokens (`deploy-{32 hex chars}`).
|
|
544
953
|
* Single source of truth used by validation utilities and auth middleware.
|
|
954
|
+
*
|
|
955
|
+
* Deliberately the same width as `API_KEY`: both are minted by one generator
|
|
956
|
+
* and classified by prefix alone, so a length that differed between them
|
|
957
|
+
* would be a second thing to know about a credential whose prefix already
|
|
958
|
+
* says what it is.
|
|
545
959
|
*/
|
|
546
960
|
export const DEPLOY_TOKEN = {
|
|
547
961
|
/** Prefix that identifies a deploy token. */
|
|
548
962
|
PREFIX: 'deploy-',
|
|
549
963
|
/** Number of hex characters following the prefix. */
|
|
550
|
-
HEX_LENGTH:
|
|
551
|
-
/** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH =
|
|
552
|
-
TOTAL_LENGTH:
|
|
964
|
+
HEX_LENGTH: 32,
|
|
965
|
+
/** Total length of a deploy token including prefix (`PREFIX.length + HEX_LENGTH = 39`). */
|
|
966
|
+
TOTAL_LENGTH: 39,
|
|
553
967
|
};
|
|
554
968
|
/**
|
|
555
969
|
* Shape constants for caller identifiers (the `X-Caller` instance-identity
|
|
@@ -622,6 +1036,26 @@ export const DEPLOYMENT_CONFIG_FILENAME = 'ship.json';
|
|
|
622
1036
|
export const SPA_DEFAULT_CONFIG = {
|
|
623
1037
|
rewrites: [{ source: '/(.*)', destination: '/index.html' }],
|
|
624
1038
|
};
|
|
1039
|
+
/**
|
|
1040
|
+
* The `/spa-check` pre-flight's client-side envelope: which file is the
|
|
1041
|
+
* check's subject, and how large it may be before a client skips the call.
|
|
1042
|
+
*
|
|
1043
|
+
* One fact with three holders until this export — the API's config declared
|
|
1044
|
+
* the cap, the SDK's `checkSPA` hardcoded `100 * 1024`, and prose restated
|
|
1045
|
+
* "100KB". `INDEX_FILE` is the selection rule (the file whose content rides
|
|
1046
|
+
* `SPACheckRequest.index`), restated by every client that builds the request.
|
|
1047
|
+
*
|
|
1048
|
+
* Neither member is a validation boundary: a client over the cap simply
|
|
1049
|
+
* skips the pre-flight, because the server answers an oversized index
|
|
1050
|
+
* `isSPA: false` anyway. A consumer that cannot import this (n8n) needs no
|
|
1051
|
+
* size copy at all — outcome parity is the server's, not the client's.
|
|
1052
|
+
*/
|
|
1053
|
+
export const SPA_CHECK_CONSTRAINTS = {
|
|
1054
|
+
/** The file whose content is the check's subject. */
|
|
1055
|
+
INDEX_FILE: 'index.html',
|
|
1056
|
+
/** Skip the pre-flight above this size — the server would answer false. */
|
|
1057
|
+
MAX_INDEX_BYTES: 100 * 1024,
|
|
1058
|
+
};
|
|
625
1059
|
/**
|
|
626
1060
|
* Assert that a ship.json file is *syntactically* loadable. Syntax only —
|
|
627
1061
|
* never schema.
|
|
@@ -764,6 +1198,50 @@ export function isDeployment(input) {
|
|
|
764
1198
|
// =============================================================================
|
|
765
1199
|
/** Default API URL if not otherwise configured. */
|
|
766
1200
|
export const DEFAULT_API = 'https://api.shipstatic.com';
|
|
1201
|
+
/**
|
|
1202
|
+
* The Node SDK's ambient configuration pair — the ONLY environment variables
|
|
1203
|
+
* the SDK reads, and therefore the COMPLETE list an embedding host must
|
|
1204
|
+
* scrub (per `npm/ship`'s strict-isolation contract, scrubbing is the host's
|
|
1205
|
+
* job, not the SDK's). A host that derives its scrub from this object's
|
|
1206
|
+
* values — as the VS Code extension's child-process env block does — picks
|
|
1207
|
+
* up a grown contract at the next pin bump instead of by remembered prose.
|
|
1208
|
+
*
|
|
1209
|
+
* Browser builds read no environment at all, and the CLI-only variables
|
|
1210
|
+
* (`SHIP_PASSWORD`, `SHIP_VIA`) are deliberately NOT here: they are the
|
|
1211
|
+
* CLI's operational levers, not the SDK's ambient contract — see
|
|
1212
|
+
* `npm/ship/CLAUDE.md`, "CLI-only env vars".
|
|
1213
|
+
*/
|
|
1214
|
+
export const SHIP_ENV = {
|
|
1215
|
+
/** The one credential slot — any platform token. */
|
|
1216
|
+
TOKEN: 'SHIP_TOKEN',
|
|
1217
|
+
/** The API endpoint override. */
|
|
1218
|
+
API_URL: 'SHIP_API_URL',
|
|
1219
|
+
};
|
|
1220
|
+
/**
|
|
1221
|
+
* Where a human creates an API key — the console deep link quoted by every
|
|
1222
|
+
* surface that teaches authentication (the CLI's config wizard, the VS Code
|
|
1223
|
+
* and n8n listings, the n8n rate-limit hint and credential copy). Written
|
|
1224
|
+
* out in five files across three repos until this export.
|
|
1225
|
+
*
|
|
1226
|
+
* Production-branded by design: published artifacts name the product, never
|
|
1227
|
+
* an environment (root `CLAUDE.md`, "Environment-Aware URLs").
|
|
1228
|
+
*/
|
|
1229
|
+
export const MY_API_KEY_URL = 'https://my.shipstatic.com/api-key';
|
|
1230
|
+
/**
|
|
1231
|
+
* How long an anonymous deployment lives before it expires.
|
|
1232
|
+
*
|
|
1233
|
+
* The lifetime of the public tier, and one fact with several readers. The API
|
|
1234
|
+
* stamps a deployment's `expires` from it and gives a claim code exactly the
|
|
1235
|
+
* same window — a live site with a dead claim link is a coherence bug, so the
|
|
1236
|
+
* two are one constant rather than two that agree. Both MCP transports quote
|
|
1237
|
+
* the duration in prose an agent reads, and derive it from here rather than
|
|
1238
|
+
* writing it out, which they did in eight places until this export existed.
|
|
1239
|
+
*
|
|
1240
|
+
* Seconds, spelled in the name: this platform has both second- and
|
|
1241
|
+
* millisecond-valued durations, and the pair is only safe when each says which
|
|
1242
|
+
* it is.
|
|
1243
|
+
*/
|
|
1244
|
+
export const PUBLIC_DEPLOYMENT_TTL_SECONDS = 3 * 24 * 60 * 60;
|
|
767
1245
|
// =============================================================================
|
|
768
1246
|
// FILE UPLOAD TYPES
|
|
769
1247
|
// =============================================================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipstatic/types",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Shared types for ShipStatic platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc",
|
|
16
|
+
"prepack": "pnpm run build",
|
|
16
17
|
"clean": "rm -rf dist",
|
|
17
18
|
"test": "vitest",
|
|
18
19
|
"lint": "biome check .",
|
|
@@ -47,5 +48,9 @@
|
|
|
47
48
|
"@types/node": "^24.13.3",
|
|
48
49
|
"typescript": "^5.9.3",
|
|
49
50
|
"vitest": "^2.1.9"
|
|
51
|
+
},
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public",
|
|
54
|
+
"provenance": true
|
|
50
55
|
}
|
|
51
56
|
}
|