@shipstatic/types 0.9.6 → 0.9.7

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
@@ -35,6 +35,7 @@ import { ShipError, ErrorType, isShipError } from '@shipstatic/types';
35
35
 
36
36
  throw ShipError.validation('File too large');
37
37
  throw ShipError.notFound('Deployment', id);
38
+ throw ShipError.forbidden('Account terminated');
38
39
  throw ShipError.authentication();
39
40
  throw ShipError.business('Plan limit reached');
40
41
 
package/dist/index.d.ts CHANGED
@@ -305,25 +305,27 @@ export interface AccountOverrides {
305
305
  * (`DeploymentStatus`, `DomainStatus`, `AccountPlan`, `AuthMethod`) follow.
306
306
  */
307
307
  export declare const ErrorType: {
308
- /** Validation failed (400) */
308
+ /** Validation failed (400). Input shape is wrong. */
309
309
  readonly Validation: "validation_failed";
310
- /** Resource not found (404) */
310
+ /** Resource not found (404). */
311
311
  readonly NotFound: "not_found";
312
- /** Rate limit exceeded (429) */
312
+ /** Authenticated but not allowed (403). User lacks permission for this action. */
313
+ readonly Forbidden: "forbidden";
314
+ /** Rate limit exceeded (429). */
313
315
  readonly RateLimit: "rate_limit_exceeded";
314
- /** Authentication required (401) */
316
+ /** Authentication required or failed (401). Missing/invalid credentials. */
315
317
  readonly Authentication: "authentication_failed";
316
- /** Business logic error (400) */
318
+ /** Business rule violation. Catch-all for 4xx state-rule errors that aren't more specific. */
317
319
  readonly Business: "business_logic_error";
318
- /** API server error (500) */
320
+ /** API server error (500). Generic server-side fault. */
319
321
  readonly Api: "internal_server_error";
320
322
  /** Network/connection error. Client-side only — set by HTTP clients on fetch failure; never produced server-side. */
321
323
  readonly Network: "network_error";
322
324
  /** Operation was cancelled. Client-side only — set on `AbortSignal` abort; never produced server-side. */
323
325
  readonly Cancelled: "operation_cancelled";
324
- /** File operation error */
326
+ /** File operation error. Client-side only — set by SDK during local file processing; never produced server-side. */
325
327
  readonly File: "file_error";
326
- /** Configuration error */
328
+ /** Configuration error. Client-side only — set by SDK during config parsing/validation; never produced server-side. */
327
329
  readonly Config: "config_error";
328
330
  };
329
331
  export type ErrorType = typeof ErrorType[keyof typeof ErrorType];
@@ -337,8 +339,8 @@ export interface ErrorResponse {
337
339
  message: string;
338
340
  /** HTTP status code (API contexts) */
339
341
  status?: number;
340
- /** Optional additional error details */
341
- details?: any;
342
+ /** Optional additional error details. Untyped by design — narrow at the read site. */
343
+ details?: unknown;
342
344
  }
343
345
  /**
344
346
  * Simple unified error class for both API and SDK
@@ -346,8 +348,8 @@ export interface ErrorResponse {
346
348
  export declare class ShipError extends Error {
347
349
  readonly type: ErrorType;
348
350
  readonly status?: number | undefined;
349
- readonly details?: any | undefined;
350
- constructor(type: ErrorType, message: string, status?: number | undefined, details?: any | undefined);
351
+ readonly details?: unknown | undefined;
352
+ constructor(type: ErrorType, message: string, status?: number | undefined, details?: unknown | undefined);
351
353
  /** Convert to wire format */
352
354
  toResponse(): ErrorResponse;
353
355
  /**
@@ -357,11 +359,14 @@ export declare class ShipError extends Error {
357
359
  * resolution: `body.message` → `body.error` → `"<operationName> failed with
358
360
  * status <N>"`.
359
361
  *
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.
362
+ * Type resolution: trusts `body.error` when it's a known server-producible
363
+ * `ErrorType` (preserves the wire's intent — server's
364
+ * `ShipError.validation(...)` round-trips back to `ErrorType.Validation`
365
+ * on the client). Falls back to status-derived (401 → Authentication,
366
+ * 403 Forbidden, 429 → RateLimit, else → Api) for non-API responses
367
+ * (CDN errors, intermediaries) or malformed bodies. Client-only types
368
+ * (`Network`, `Cancelled`, `File`, `Config`) are filtered out of the
369
+ * trusted set — a misbehaving server claiming one of those is ignored.
365
370
  *
366
371
  * `operationName` (e.g. `"Get account"`) is used to compose the fallback
367
372
  * message. Defaults to `"Request"`. Same convention as `fromFetchError`.
@@ -389,15 +394,29 @@ export declare class ShipError extends Error {
389
394
  * `"Request"` when omitted.
390
395
  */
391
396
  static fromFetchError(cause: unknown, operationName?: string): ShipError;
392
- static validation(message: string, details?: any): ShipError;
397
+ static validation(message: string, details?: unknown): ShipError;
393
398
  static notFound(resource: string, id?: string): ShipError;
399
+ static forbidden(message: string, details?: unknown): ShipError;
394
400
  static rateLimit(message?: string): ShipError;
395
- static authentication(message?: string, details?: any): ShipError;
401
+ /**
402
+ * Construct an Authentication (401) error.
403
+ *
404
+ * **Telemetry pattern — `details: { internal: '<tag>' }`.** When the
405
+ * server creates an auth error with an `internal` key in `details`
406
+ * (e.g. `{ internal: 'jwt_missing_subject' }`), `toResponse()` strips the
407
+ * entire `details` object before serialization. This keeps the wire
408
+ * response a clean "Authentication failed" while preserving granular
409
+ * server-side telemetry (which strategy/check failed) for logs and tests.
410
+ *
411
+ * Use this pattern in API auth code; do not put client-visible info under
412
+ * `internal`. Other `details` keys round-trip normally.
413
+ */
414
+ static authentication(message?: string, details?: unknown): ShipError;
396
415
  static business(message: string, status?: number): ShipError;
397
- static network(message: string, details?: any): ShipError;
416
+ static network(message: string, details?: unknown): ShipError;
398
417
  static cancelled(message: string): ShipError;
399
- static file(message: string, details?: any): ShipError;
400
- static config(message: string, details?: any): ShipError;
418
+ static file(message: string, details?: unknown): ShipError;
419
+ static config(message: string, details?: unknown): ShipError;
401
420
  static api(message: string, status?: number): ShipError;
402
421
  isClientError(): boolean;
403
422
  isNetworkError(): boolean;
package/dist/index.js CHANGED
@@ -59,48 +59,59 @@ export const AccountPlan = {
59
59
  * (`DeploymentStatus`, `DomainStatus`, `AccountPlan`, `AuthMethod`) follow.
60
60
  */
61
61
  export const ErrorType = {
62
- /** Validation failed (400) */
62
+ /** Validation failed (400). Input shape is wrong. */
63
63
  Validation: 'validation_failed',
64
- /** Resource not found (404) */
64
+ /** Resource not found (404). */
65
65
  NotFound: 'not_found',
66
- /** Rate limit exceeded (429) */
66
+ /** Authenticated but not allowed (403). User lacks permission for this action. */
67
+ Forbidden: 'forbidden',
68
+ /** Rate limit exceeded (429). */
67
69
  RateLimit: 'rate_limit_exceeded',
68
- /** Authentication required (401) */
70
+ /** Authentication required or failed (401). Missing/invalid credentials. */
69
71
  Authentication: 'authentication_failed',
70
- /** Business logic error (400) */
72
+ /** Business rule violation. Catch-all for 4xx state-rule errors that aren't more specific. */
71
73
  Business: 'business_logic_error',
72
- /** API server error (500) */
74
+ /** API server error (500). Generic server-side fault. */
73
75
  Api: 'internal_server_error',
74
76
  /** Network/connection error. Client-side only — set by HTTP clients on fetch failure; never produced server-side. */
75
77
  Network: 'network_error',
76
78
  /** Operation was cancelled. Client-side only — set on `AbortSignal` abort; never produced server-side. */
77
79
  Cancelled: 'operation_cancelled',
78
- /** File operation error */
80
+ /** File operation error. Client-side only — set by SDK during local file processing; never produced server-side. */
79
81
  File: 'file_error',
80
- /** Configuration error */
82
+ /** Configuration error. Client-side only — set by SDK during config parsing/validation; never produced server-side. */
81
83
  Config: 'config_error',
82
84
  };
85
+ /**
86
+ * Error types that originate exclusively on the client (HTTP clients, SDK
87
+ * file processing, local config parsing). These never appear on the wire
88
+ * from the server, so `fromHttpResponse` will not trust them even if a
89
+ * misbehaving server claims one in `body.error`.
90
+ */
91
+ const CLIENT_ONLY_ERROR_TYPES = new Set([
92
+ ErrorType.Network,
93
+ ErrorType.Cancelled,
94
+ ErrorType.File,
95
+ ErrorType.Config,
96
+ ]);
83
97
  /**
84
98
  * Categorizes error types for the `isClientError` / `isNetworkError` /
85
99
  * `isAuthError` helpers. Each `Set` is typed against the wider `ErrorType`
86
100
  * union so `.has(error.type)` accepts any value from the union.
87
101
  */
88
102
  const ERROR_CATEGORIES = {
89
- client: new Set([ErrorType.Business, ErrorType.Config, ErrorType.File, ErrorType.Validation]),
103
+ client: new Set([ErrorType.Business, ErrorType.Config, ErrorType.File, ErrorType.Forbidden, ErrorType.Validation]),
90
104
  network: new Set([ErrorType.Network]),
91
105
  auth: new Set([ErrorType.Authentication]),
92
106
  };
93
107
  /**
94
- * Lookup set of error types that legitimately appear on the wire i.e.
95
- * server-thrown types. Used by `ShipError.fromHttpResponse` to validate the
96
- * body's `error` field before trusting it as the `ShipError.type`.
97
- *
98
- * Excludes `Network` and `Cancelled`, which are client-side-only by design:
99
- * they originate on the client (fetch failure, abort) and should never be
100
- * reconstructed from a server response, even if a misbehaving server were
101
- * to send them. A defensive omission, not a theoretical concern.
108
+ * Error types the server can legitimately produce on the wire. Used by
109
+ * `ShipError.fromHttpResponse` to validate the body's `error` field before
110
+ * trusting it as `ShipError.type`. Derived by exclusion from
111
+ * `CLIENT_ONLY_ERROR_TYPES` so adding a new server-producible type to
112
+ * `ErrorType` is automatically picked up.
102
113
  */
103
- const SERVER_PRODUCIBLE_ERROR_TYPES = new Set(Object.values(ErrorType).filter(t => t !== ErrorType.Network && t !== ErrorType.Cancelled));
114
+ const SERVER_PRODUCIBLE_ERROR_TYPES = new Set(Object.values(ErrorType).filter(t => !CLIENT_ONLY_ERROR_TYPES.has(t)));
104
115
  /**
105
116
  * Simple unified error class for both API and SDK
106
117
  */
@@ -117,8 +128,11 @@ export class ShipError extends Error {
117
128
  }
118
129
  /** Convert to wire format */
119
130
  toResponse() {
120
- // For security, exclude internal details from authentication errors in API responses
121
- const details = this.type === ErrorType.Authentication && this.details?.internal
131
+ // Strip authentication details when they carry an `internal` telemetry
132
+ // tag (see `ShipError.authentication` JSDoc) these are server-side
133
+ // diagnostics like 'jwt_missing_subject' that must not leak to clients.
134
+ const authDetails = this.details;
135
+ const details = this.type === ErrorType.Authentication && authDetails?.internal
122
136
  ? undefined
123
137
  : this.details;
124
138
  return {
@@ -135,11 +149,14 @@ export class ShipError extends Error {
135
149
  * resolution: `body.message` → `body.error` → `"<operationName> failed with
136
150
  * status <N>"`.
137
151
  *
138
- * Type resolution: trusts `body.error` when it's a known `ErrorType`
139
- * (preserves the wire's intent — server's `ShipError.validation(...)`
140
- * round-trips back to `ErrorType.Validation` on the client). Falls back to
141
- * status-derived (401 → Authentication, 429 → RateLimit, else → Api) for
142
- * non-API responses (CDN errors, intermediaries) or malformed bodies.
152
+ * Type resolution: trusts `body.error` when it's a known server-producible
153
+ * `ErrorType` (preserves the wire's intent — server's
154
+ * `ShipError.validation(...)` round-trips back to `ErrorType.Validation`
155
+ * on the client). Falls back to status-derived (401 → Authentication,
156
+ * 403 Forbidden, 429 → RateLimit, else → Api) for non-API responses
157
+ * (CDN errors, intermediaries) or malformed bodies. Client-only types
158
+ * (`Network`, `Cancelled`, `File`, `Config`) are filtered out of the
159
+ * trusted set — a misbehaving server claiming one of those is ignored.
143
160
  *
144
161
  * `operationName` (e.g. `"Get account"`) is used to compose the fallback
145
162
  * message. Defaults to `"Request"`. Same convention as `fromFetchError`.
@@ -178,8 +195,9 @@ export class ShipError extends Error {
178
195
  }
179
196
  message = message || `${operationName || 'Request'} failed with status ${response.status}`;
180
197
  const type = bodyType ?? (response.status === 401 ? ErrorType.Authentication :
181
- response.status === 429 ? ErrorType.RateLimit :
182
- ErrorType.Api);
198
+ response.status === 403 ? ErrorType.Forbidden :
199
+ response.status === 429 ? ErrorType.RateLimit :
200
+ ErrorType.Api);
183
201
  return new ShipError(type, message, response.status, details);
184
202
  }
185
203
  /**
@@ -215,7 +233,10 @@ export class ShipError extends Error {
215
233
  }
216
234
  return new ShipError(ErrorType.Api, `${op} failed: Unknown error`);
217
235
  }
218
- // Factory methods for common errors
236
+ // Factory methods. Uniform shape `(message, details?)` with two principled
237
+ // exceptions: `notFound` composes its message from (resource, id?), and
238
+ // `business` / `api` accept an optional status because they're the
239
+ // multi-status fallbacks.
219
240
  static validation(message, details) {
220
241
  return new ShipError(ErrorType.Validation, message, 400, details);
221
242
  }
@@ -223,9 +244,25 @@ export class ShipError extends Error {
223
244
  const message = id ? `${resource} ${id} not found` : `${resource} not found`;
224
245
  return new ShipError(ErrorType.NotFound, message, 404);
225
246
  }
247
+ static forbidden(message, details) {
248
+ return new ShipError(ErrorType.Forbidden, message, 403, details);
249
+ }
226
250
  static rateLimit(message = "Too many requests") {
227
251
  return new ShipError(ErrorType.RateLimit, message, 429);
228
252
  }
253
+ /**
254
+ * Construct an Authentication (401) error.
255
+ *
256
+ * **Telemetry pattern — `details: { internal: '<tag>' }`.** When the
257
+ * server creates an auth error with an `internal` key in `details`
258
+ * (e.g. `{ internal: 'jwt_missing_subject' }`), `toResponse()` strips the
259
+ * entire `details` object before serialization. This keeps the wire
260
+ * response a clean "Authentication failed" while preserving granular
261
+ * server-side telemetry (which strategy/check failed) for logs and tests.
262
+ *
263
+ * Use this pattern in API auth code; do not put client-visible info under
264
+ * `internal`. Other `details` keys round-trip normally.
265
+ */
229
266
  static authentication(message = "Authentication required", details) {
230
267
  return new ShipError(ErrorType.Authentication, message, 401, details);
231
268
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/types",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "description": "Shared types for ShipStatic platform",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/index.ts CHANGED
@@ -350,55 +350,65 @@ export interface AccountOverrides {
350
350
  * (`DeploymentStatus`, `DomainStatus`, `AccountPlan`, `AuthMethod`) follow.
351
351
  */
352
352
  export const ErrorType = {
353
- /** Validation failed (400) */
353
+ /** Validation failed (400). Input shape is wrong. */
354
354
  Validation: 'validation_failed',
355
- /** Resource not found (404) */
355
+ /** Resource not found (404). */
356
356
  NotFound: 'not_found',
357
- /** Rate limit exceeded (429) */
357
+ /** Authenticated but not allowed (403). User lacks permission for this action. */
358
+ Forbidden: 'forbidden',
359
+ /** Rate limit exceeded (429). */
358
360
  RateLimit: 'rate_limit_exceeded',
359
- /** Authentication required (401) */
361
+ /** Authentication required or failed (401). Missing/invalid credentials. */
360
362
  Authentication: 'authentication_failed',
361
- /** Business logic error (400) */
363
+ /** Business rule violation. Catch-all for 4xx state-rule errors that aren't more specific. */
362
364
  Business: 'business_logic_error',
363
- /** API server error (500) */
365
+ /** API server error (500). Generic server-side fault. */
364
366
  Api: 'internal_server_error',
365
367
  /** Network/connection error. Client-side only — set by HTTP clients on fetch failure; never produced server-side. */
366
368
  Network: 'network_error',
367
369
  /** Operation was cancelled. Client-side only — set on `AbortSignal` abort; never produced server-side. */
368
370
  Cancelled: 'operation_cancelled',
369
- /** File operation error */
371
+ /** File operation error. Client-side only — set by SDK during local file processing; never produced server-side. */
370
372
  File: 'file_error',
371
- /** Configuration error */
373
+ /** Configuration error. Client-side only — set by SDK during config parsing/validation; never produced server-side. */
372
374
  Config: 'config_error',
373
375
  } as const;
374
376
 
375
377
  export type ErrorType = typeof ErrorType[keyof typeof ErrorType];
376
378
 
379
+ /**
380
+ * Error types that originate exclusively on the client (HTTP clients, SDK
381
+ * file processing, local config parsing). These never appear on the wire
382
+ * from the server, so `fromHttpResponse` will not trust them even if a
383
+ * misbehaving server claims one in `body.error`.
384
+ */
385
+ const CLIENT_ONLY_ERROR_TYPES = new Set<string>([
386
+ ErrorType.Network,
387
+ ErrorType.Cancelled,
388
+ ErrorType.File,
389
+ ErrorType.Config,
390
+ ]);
391
+
377
392
  /**
378
393
  * Categorizes error types for the `isClientError` / `isNetworkError` /
379
394
  * `isAuthError` helpers. Each `Set` is typed against the wider `ErrorType`
380
395
  * union so `.has(error.type)` accepts any value from the union.
381
396
  */
382
397
  const ERROR_CATEGORIES = {
383
- client: new Set<ErrorType>([ErrorType.Business, ErrorType.Config, ErrorType.File, ErrorType.Validation]),
398
+ client: new Set<ErrorType>([ErrorType.Business, ErrorType.Config, ErrorType.File, ErrorType.Forbidden, ErrorType.Validation]),
384
399
  network: new Set<ErrorType>([ErrorType.Network]),
385
400
  auth: new Set<ErrorType>([ErrorType.Authentication]),
386
401
  } as const;
387
402
 
388
403
  /**
389
- * Lookup set of error types that legitimately appear on the wire i.e.
390
- * server-thrown types. Used by `ShipError.fromHttpResponse` to validate the
391
- * body's `error` field before trusting it as the `ShipError.type`.
392
- *
393
- * Excludes `Network` and `Cancelled`, which are client-side-only by design:
394
- * they originate on the client (fetch failure, abort) and should never be
395
- * reconstructed from a server response, even if a misbehaving server were
396
- * to send them. A defensive omission, not a theoretical concern.
404
+ * Error types the server can legitimately produce on the wire. Used by
405
+ * `ShipError.fromHttpResponse` to validate the body's `error` field before
406
+ * trusting it as `ShipError.type`. Derived by exclusion from
407
+ * `CLIENT_ONLY_ERROR_TYPES` so adding a new server-producible type to
408
+ * `ErrorType` is automatically picked up.
397
409
  */
398
410
  const SERVER_PRODUCIBLE_ERROR_TYPES = new Set<string>(
399
- Object.values(ErrorType).filter(
400
- t => t !== ErrorType.Network && t !== ErrorType.Cancelled,
401
- ),
411
+ Object.values(ErrorType).filter(t => !CLIENT_ONLY_ERROR_TYPES.has(t)),
402
412
  );
403
413
 
404
414
  /**
@@ -411,8 +421,8 @@ export interface ErrorResponse {
411
421
  message: string;
412
422
  /** HTTP status code (API contexts) */
413
423
  status?: number;
414
- /** Optional additional error details */
415
- details?: any;
424
+ /** Optional additional error details. Untyped by design — narrow at the read site. */
425
+ details?: unknown;
416
426
  }
417
427
 
418
428
  /**
@@ -423,7 +433,7 @@ export class ShipError extends Error {
423
433
  public readonly type: ErrorType,
424
434
  message: string,
425
435
  public readonly status?: number,
426
- public readonly details?: any
436
+ public readonly details?: unknown,
427
437
  ) {
428
438
  super(message);
429
439
  this.name = 'ShipError';
@@ -431,8 +441,11 @@ export class ShipError extends Error {
431
441
 
432
442
  /** Convert to wire format */
433
443
  toResponse(): ErrorResponse {
434
- // For security, exclude internal details from authentication errors in API responses
435
- const details = this.type === ErrorType.Authentication && this.details?.internal
444
+ // Strip authentication details when they carry an `internal` telemetry
445
+ // tag (see `ShipError.authentication` JSDoc) these are server-side
446
+ // diagnostics like 'jwt_missing_subject' that must not leak to clients.
447
+ const authDetails = this.details as { internal?: unknown } | undefined;
448
+ const details = this.type === ErrorType.Authentication && authDetails?.internal
436
449
  ? undefined
437
450
  : this.details;
438
451
 
@@ -451,11 +464,14 @@ export class ShipError extends Error {
451
464
  * resolution: `body.message` → `body.error` → `"<operationName> failed with
452
465
  * status <N>"`.
453
466
  *
454
- * Type resolution: trusts `body.error` when it's a known `ErrorType`
455
- * (preserves the wire's intent — server's `ShipError.validation(...)`
456
- * round-trips back to `ErrorType.Validation` on the client). Falls back to
457
- * status-derived (401 → Authentication, 429 → RateLimit, else → Api) for
458
- * non-API responses (CDN errors, intermediaries) or malformed bodies.
467
+ * Type resolution: trusts `body.error` when it's a known server-producible
468
+ * `ErrorType` (preserves the wire's intent — server's
469
+ * `ShipError.validation(...)` round-trips back to `ErrorType.Validation`
470
+ * on the client). Falls back to status-derived (401 → Authentication,
471
+ * 403 Forbidden, 429 → RateLimit, else → Api) for non-API responses
472
+ * (CDN errors, intermediaries) or malformed bodies. Client-only types
473
+ * (`Network`, `Cancelled`, `File`, `Config`) are filtered out of the
474
+ * trusted set — a misbehaving server claiming one of those is ignored.
459
475
  *
460
476
  * `operationName` (e.g. `"Get account"`) is used to compose the fallback
461
477
  * message. Defaults to `"Request"`. Same convention as `fromFetchError`.
@@ -496,6 +512,7 @@ export class ShipError extends Error {
496
512
 
497
513
  const type = bodyType ?? (
498
514
  response.status === 401 ? ErrorType.Authentication :
515
+ response.status === 403 ? ErrorType.Forbidden :
499
516
  response.status === 429 ? ErrorType.RateLimit :
500
517
  ErrorType.Api
501
518
  );
@@ -539,8 +556,12 @@ export class ShipError extends Error {
539
556
  return new ShipError(ErrorType.Api, `${op} failed: Unknown error`);
540
557
  }
541
558
 
542
- // Factory methods for common errors
543
- static validation(message: string, details?: any): ShipError {
559
+ // Factory methods. Uniform shape `(message, details?)` with two principled
560
+ // exceptions: `notFound` composes its message from (resource, id?), and
561
+ // `business` / `api` accept an optional status because they're the
562
+ // multi-status fallbacks.
563
+
564
+ static validation(message: string, details?: unknown): ShipError {
544
565
  return new ShipError(ErrorType.Validation, message, 400, details);
545
566
  }
546
567
 
@@ -549,11 +570,28 @@ export class ShipError extends Error {
549
570
  return new ShipError(ErrorType.NotFound, message, 404);
550
571
  }
551
572
 
573
+ static forbidden(message: string, details?: unknown): ShipError {
574
+ return new ShipError(ErrorType.Forbidden, message, 403, details);
575
+ }
576
+
552
577
  static rateLimit(message: string = "Too many requests"): ShipError {
553
578
  return new ShipError(ErrorType.RateLimit, message, 429);
554
579
  }
555
580
 
556
- static authentication(message: string = "Authentication required", details?: any): ShipError {
581
+ /**
582
+ * Construct an Authentication (401) error.
583
+ *
584
+ * **Telemetry pattern — `details: { internal: '<tag>' }`.** When the
585
+ * server creates an auth error with an `internal` key in `details`
586
+ * (e.g. `{ internal: 'jwt_missing_subject' }`), `toResponse()` strips the
587
+ * entire `details` object before serialization. This keeps the wire
588
+ * response a clean "Authentication failed" while preserving granular
589
+ * server-side telemetry (which strategy/check failed) for logs and tests.
590
+ *
591
+ * Use this pattern in API auth code; do not put client-visible info under
592
+ * `internal`. Other `details` keys round-trip normally.
593
+ */
594
+ static authentication(message: string = "Authentication required", details?: unknown): ShipError {
557
595
  return new ShipError(ErrorType.Authentication, message, 401, details);
558
596
  }
559
597
 
@@ -561,7 +599,7 @@ export class ShipError extends Error {
561
599
  return new ShipError(ErrorType.Business, message, status);
562
600
  }
563
601
 
564
- static network(message: string, details?: any): ShipError {
602
+ static network(message: string, details?: unknown): ShipError {
565
603
  return new ShipError(ErrorType.Network, message, undefined, details);
566
604
  }
567
605
 
@@ -569,11 +607,11 @@ export class ShipError extends Error {
569
607
  return new ShipError(ErrorType.Cancelled, message);
570
608
  }
571
609
 
572
- static file(message: string, details?: any): ShipError {
610
+ static file(message: string, details?: unknown): ShipError {
573
611
  return new ShipError(ErrorType.File, message, undefined, details);
574
612
  }
575
613
 
576
- static config(message: string, details?: any): ShipError {
614
+ static config(message: string, details?: unknown): ShipError {
577
615
  return new ShipError(ErrorType.Config, message, undefined, details);
578
616
  }
579
617