@shipstatic/types 0.9.1 → 0.9.3

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 CHANGED
@@ -46,6 +46,20 @@ if (error.isClientError()) { /* Business | Config | File | Validation */ }
46
46
  if (error.isAuthError()) { /* handle auth */ }
47
47
  ```
48
48
 
49
+ **HTTP client integration.** Both producer and consumer sides of the wire have first-class helpers, so every HTTP client across the platform reconstructs the same `ShipError` shape:
50
+
51
+ ```typescript
52
+ // Producer side (API workers): serialize a ShipError to JSON
53
+ return c.json(error.toResponse(), error.status ?? 500);
54
+
55
+ // Consumer side (SDK, web app): rehydrate from any error Response
56
+ if (!response.ok) {
57
+ throw await ShipError.fromHttpResponse(response, 'Get account failed');
58
+ }
59
+ ```
60
+
61
+ `fromHttpResponse` trusts the body's `error` field when it's a known `ErrorType` — so a server's `ShipError.validation(...)` round-trips back to `ErrorType.Validation` on the client. For non-API responses (CDN errors, intermediaries) or malformed bodies it falls back to status-derived (401 → `Authentication`, 429 → `RateLimit`, else → `Api`). Body's `message` and `details` are preserved best-effort. The optional second arg is a fallback message used when the body has nothing usable.
62
+
49
63
  ### Status Constants
50
64
 
51
65
  ```typescript
@@ -62,7 +76,7 @@ import {
62
76
 
63
77
  ```typescript
64
78
  import type {
65
- PlatformLimits, // plan-based caps from /config (file size, file count, total size)
79
+ PlatformLimits, // plan-based caps from /limits (file size, file count, total size)
66
80
  BillingStatus,
67
81
  CheckoutSession,
68
82
  ActivityListResponse,
package/dist/index.d.ts CHANGED
@@ -350,8 +350,23 @@ export declare class ShipError extends Error {
350
350
  constructor(type: ErrorType, message: string, status?: number | undefined, details?: any | undefined);
351
351
  /** Convert to wire format */
352
352
  toResponse(): ErrorResponse;
353
- /** Create from wire format */
354
- static fromResponse(response: ErrorResponse): ShipError;
353
+ /**
354
+ * Construct a `ShipError` from an HTTP error response.
355
+ *
356
+ * Best-effort body parse for `{ message, error?, details? }`. Message
357
+ * resolution: `body.message` → `body.error` → `fallbackMessage` →
358
+ * `Request failed with status N`.
359
+ *
360
+ * Type resolution: trusts `body.error` when it's a known `ErrorType`
361
+ * (preserves the wire's intent — server's `ShipError.validation(...)`
362
+ * round-trips back to `ErrorType.Validation` on the client). Falls back to
363
+ * status-derived (401 → Authentication, 429 → RateLimit, else → Api) for
364
+ * non-API responses (CDN errors, intermediaries) or malformed bodies.
365
+ *
366
+ * Async because it reads the response body. Returns rather than throws so
367
+ * callers can compose; most will `throw await ShipError.fromHttpResponse(...)`.
368
+ */
369
+ static fromHttpResponse(response: Response, fallbackMessage?: string): Promise<ShipError>;
355
370
  static validation(message: string, details?: any): ShipError;
356
371
  static notFound(resource: string, id?: string): ShipError;
357
372
  static rateLimit(message?: string): ShipError;
@@ -362,8 +377,6 @@ export declare class ShipError extends Error {
362
377
  static file(message: string, filePath?: string): ShipError;
363
378
  static config(message: string, details?: any): ShipError;
364
379
  static api(message: string, status?: number): ShipError;
365
- static database(message: string, status?: number): ShipError;
366
- static storage(message: string, status?: number): ShipError;
367
380
  get filePath(): string | undefined;
368
381
  isClientError(): boolean;
369
382
  isNetworkError(): boolean;
package/dist/index.js CHANGED
@@ -90,6 +90,13 @@ const ERROR_CATEGORIES = {
90
90
  network: new Set([ErrorType.Network]),
91
91
  auth: new Set([ErrorType.Authentication]),
92
92
  };
93
+ /**
94
+ * Lookup set of known wire-format error type strings. Used by
95
+ * `ShipError.fromHttpResponse` to validate the body's `error` field before
96
+ * trusting it as an `ErrorType`. Defensive against malformed/unknown values
97
+ * that could otherwise leak into the typed `ShipError.type` field.
98
+ */
99
+ const KNOWN_ERROR_TYPES = new Set(Object.values(ErrorType));
93
100
  /**
94
101
  * Simple unified error class for both API and SDK
95
102
  */
@@ -117,9 +124,56 @@ export class ShipError extends Error {
117
124
  details
118
125
  };
119
126
  }
120
- /** Create from wire format */
121
- static fromResponse(response) {
122
- return new ShipError(response.error, response.message, response.status, response.details);
127
+ /**
128
+ * Construct a `ShipError` from an HTTP error response.
129
+ *
130
+ * Best-effort body parse for `{ message, error?, details? }`. Message
131
+ * resolution: `body.message` → `body.error` → `fallbackMessage` →
132
+ * `Request failed with status N`.
133
+ *
134
+ * Type resolution: trusts `body.error` when it's a known `ErrorType`
135
+ * (preserves the wire's intent — server's `ShipError.validation(...)`
136
+ * round-trips back to `ErrorType.Validation` on the client). Falls back to
137
+ * status-derived (401 → Authentication, 429 → RateLimit, else → Api) for
138
+ * non-API responses (CDN errors, intermediaries) or malformed bodies.
139
+ *
140
+ * Async because it reads the response body. Returns rather than throws so
141
+ * callers can compose; most will `throw await ShipError.fromHttpResponse(...)`.
142
+ */
143
+ static async fromHttpResponse(response, fallbackMessage) {
144
+ let message;
145
+ let details;
146
+ let bodyType;
147
+ try {
148
+ const contentType = response.headers.get('content-type');
149
+ if (contentType?.includes('application/json')) {
150
+ const json = await response.json();
151
+ if (json && typeof json === 'object') {
152
+ const obj = json;
153
+ if (typeof obj.message === 'string')
154
+ message = obj.message;
155
+ else if (typeof obj.error === 'string')
156
+ message = obj.error;
157
+ details = obj.details;
158
+ if (typeof obj.error === 'string' && KNOWN_ERROR_TYPES.has(obj.error)) {
159
+ bodyType = obj.error;
160
+ }
161
+ }
162
+ }
163
+ else {
164
+ const text = await response.text();
165
+ if (text)
166
+ message = text;
167
+ }
168
+ }
169
+ catch {
170
+ // Body unreadable; fall through to fallback.
171
+ }
172
+ message = message || fallbackMessage || `Request failed with status ${response.status}`;
173
+ const type = bodyType ?? (response.status === 401 ? ErrorType.Authentication :
174
+ response.status === 429 ? ErrorType.RateLimit :
175
+ ErrorType.Api);
176
+ return new ShipError(type, message, response.status, details);
123
177
  }
124
178
  // Factory methods for common errors
125
179
  static validation(message, details) {
@@ -153,12 +207,6 @@ export class ShipError extends Error {
153
207
  static api(message, status = 500) {
154
208
  return new ShipError(ErrorType.Api, message, status);
155
209
  }
156
- static database(message, status = 500) {
157
- return new ShipError(ErrorType.Api, message, status);
158
- }
159
- static storage(message, status = 500) {
160
- return new ShipError(ErrorType.Api, message, status);
161
- }
162
210
  // Helper getter for accessing file path from details
163
211
  get filePath() {
164
212
  return this.details?.filePath;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/types",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "Shared types for ShipStatic platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -385,6 +385,14 @@ const ERROR_CATEGORIES = {
385
385
  auth: new Set<ErrorType>([ErrorType.Authentication]),
386
386
  } as const;
387
387
 
388
+ /**
389
+ * Lookup set of known wire-format error type strings. Used by
390
+ * `ShipError.fromHttpResponse` to validate the body's `error` field before
391
+ * trusting it as an `ErrorType`. Defensive against malformed/unknown values
392
+ * that could otherwise leak into the typed `ShipError.type` field.
393
+ */
394
+ const KNOWN_ERROR_TYPES = new Set<string>(Object.values(ErrorType));
395
+
388
396
  /**
389
397
  * Standard error response format used everywhere
390
398
  */
@@ -428,9 +436,60 @@ export class ShipError extends Error {
428
436
  };
429
437
  }
430
438
 
431
- /** Create from wire format */
432
- static fromResponse(response: ErrorResponse): ShipError {
433
- return new ShipError(response.error, response.message, response.status, response.details);
439
+ /**
440
+ * Construct a `ShipError` from an HTTP error response.
441
+ *
442
+ * Best-effort body parse for `{ message, error?, details? }`. Message
443
+ * resolution: `body.message` → `body.error` → `fallbackMessage` →
444
+ * `Request failed with status N`.
445
+ *
446
+ * Type resolution: trusts `body.error` when it's a known `ErrorType`
447
+ * (preserves the wire's intent — server's `ShipError.validation(...)`
448
+ * round-trips back to `ErrorType.Validation` on the client). Falls back to
449
+ * status-derived (401 → Authentication, 429 → RateLimit, else → Api) for
450
+ * non-API responses (CDN errors, intermediaries) or malformed bodies.
451
+ *
452
+ * Async because it reads the response body. Returns rather than throws so
453
+ * callers can compose; most will `throw await ShipError.fromHttpResponse(...)`.
454
+ */
455
+ static async fromHttpResponse(
456
+ response: Response,
457
+ fallbackMessage?: string,
458
+ ): Promise<ShipError> {
459
+ let message: string | undefined;
460
+ let details: unknown;
461
+ let bodyType: ErrorType | undefined;
462
+
463
+ try {
464
+ const contentType = response.headers.get('content-type');
465
+ if (contentType?.includes('application/json')) {
466
+ const json: unknown = await response.json();
467
+ if (json && typeof json === 'object') {
468
+ const obj = json as Record<string, unknown>;
469
+ if (typeof obj.message === 'string') message = obj.message;
470
+ else if (typeof obj.error === 'string') message = obj.error;
471
+ details = obj.details;
472
+ if (typeof obj.error === 'string' && KNOWN_ERROR_TYPES.has(obj.error)) {
473
+ bodyType = obj.error as ErrorType;
474
+ }
475
+ }
476
+ } else {
477
+ const text = await response.text();
478
+ if (text) message = text;
479
+ }
480
+ } catch {
481
+ // Body unreadable; fall through to fallback.
482
+ }
483
+
484
+ message = message || fallbackMessage || `Request failed with status ${response.status}`;
485
+
486
+ const type = bodyType ?? (
487
+ response.status === 401 ? ErrorType.Authentication :
488
+ response.status === 429 ? ErrorType.RateLimit :
489
+ ErrorType.Api
490
+ );
491
+
492
+ return new ShipError(type, message, response.status, details);
434
493
  }
435
494
 
436
495
  // Factory methods for common errors
@@ -475,14 +534,6 @@ export class ShipError extends Error {
475
534
  return new ShipError(ErrorType.Api, message, status);
476
535
  }
477
536
 
478
- static database(message: string, status: number = 500): ShipError {
479
- return new ShipError(ErrorType.Api, message, status);
480
- }
481
-
482
- static storage(message: string, status: number = 500): ShipError {
483
- return new ShipError(ErrorType.Api, message, status);
484
- }
485
-
486
537
  // Helper getter for accessing file path from details
487
538
  get filePath(): string | undefined {
488
539
  return this.details?.filePath;