@danypops/vehicle-core 0.10.0 → 0.11.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.
@@ -14,6 +14,14 @@ export interface VehicleFailure {
14
14
  readonly recovery?: VehicleRecovery;
15
15
  readonly details?: JsonValue;
16
16
  readonly operationId?: string;
17
+ /**
18
+ * The underlying cause's own message, when this failure wraps an unexpected error (e.g. a
19
+ * handler throwing something other than a VehicleError) -- bounded, never a full stack trace.
20
+ * Without this, a caller sees only a generic template like "x handler failed" and has no way
21
+ * to tell what actually went wrong or whether the underlying operation may have partially
22
+ * applied.
23
+ */
24
+ readonly causeMessage?: string;
17
25
  }
18
26
  export interface VehicleErrorOptions {
19
27
  readonly category: VehicleFailureCategory;
@@ -23,6 +31,15 @@ export interface VehicleErrorOptions {
23
31
  readonly details?: JsonValue;
24
32
  readonly operationId?: string;
25
33
  readonly cause?: unknown;
34
+ /**
35
+ * Secure by default (false): `cause` is always attached to the real in-process Error chain
36
+ * (for server-side logging/observability), but its message crosses the wire via
37
+ * toFailure().causeMessage only when the throw site explicitly opts in here -- an arbitrary
38
+ * cause's message could contain a credential, an internal path, or other detail the thrower
39
+ * never reviewed for wire-safety. Set true only when the cause is known-safe to show (e.g. a
40
+ * validation library's own message intended for the caller).
41
+ */
42
+ readonly exposeCause?: boolean;
26
43
  }
27
44
  export declare class VehicleError extends Error {
28
45
  readonly code: string;
@@ -32,7 +49,10 @@ export declare class VehicleError extends Error {
32
49
  readonly recovery?: VehicleRecovery;
33
50
  readonly details?: JsonValue;
34
51
  readonly operationId?: string;
52
+ private readonly exposeCause;
35
53
  constructor(code: string, message: string, options: VehicleErrorOptions);
36
54
  toFailure(): VehicleFailure;
37
55
  }
56
+ /** Extracts a bounded, wire-safe message from an unknown cause -- never the full stack trace, never an unbounded payload. */
57
+ export declare function boundedCauseMessage(cause: unknown): string | undefined;
38
58
  export declare function boundedValidationDetails(issues: readonly VehicleSchemaIssue[] | undefined): JsonValue | undefined;
@@ -6,6 +6,7 @@ export class VehicleError extends Error {
6
6
  recovery;
7
7
  details;
8
8
  operationId;
9
+ exposeCause;
9
10
  constructor(code, message, options) {
10
11
  super(message, options.cause === undefined ? undefined : { cause: options.cause });
11
12
  this.code = code;
@@ -16,8 +17,10 @@ export class VehicleError extends Error {
16
17
  this.recovery = options.recovery;
17
18
  this.details = options.details;
18
19
  this.operationId = options.operationId;
20
+ this.exposeCause = options.exposeCause ?? false;
19
21
  }
20
22
  toFailure() {
23
+ const causeMessage = this.exposeCause ? boundedCauseMessage(this.cause) : undefined;
21
24
  return {
22
25
  code: this.code,
23
26
  category: this.category,
@@ -27,9 +30,19 @@ export class VehicleError extends Error {
27
30
  ...(this.recovery === undefined ? {} : { recovery: this.recovery }),
28
31
  ...(this.details === undefined ? {} : { details: this.details }),
29
32
  ...(this.operationId === undefined ? {} : { operationId: this.operationId }),
33
+ ...(causeMessage === undefined ? {} : { causeMessage }),
30
34
  };
31
35
  }
32
36
  }
37
+ const MAX_CAUSE_MESSAGE_LENGTH = 500;
38
+ /** Extracts a bounded, wire-safe message from an unknown cause -- never the full stack trace, never an unbounded payload. */
39
+ export function boundedCauseMessage(cause) {
40
+ if (cause instanceof Error && cause.message.length > 0)
41
+ return cause.message.slice(0, MAX_CAUSE_MESSAGE_LENGTH);
42
+ if (typeof cause === "string" && cause.length > 0)
43
+ return cause.slice(0, MAX_CAUSE_MESSAGE_LENGTH);
44
+ return undefined;
45
+ }
33
46
  const MAX_VALIDATION_ISSUES = 10;
34
47
  const MAX_ISSUE_MESSAGE_LENGTH = 500;
35
48
  const MAX_ISSUE_PATH_LENGTH = 20;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/vehicle-core",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Vehicle's runtime-neutral wire contract: operation descriptors, schema codecs, failure shapes. Zero runtime dependencies, zero Bun-specific code -- the one thing every Vehicle client and server package depends on.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -45,6 +45,14 @@ export interface VehicleFailure {
45
45
  readonly recovery?: VehicleRecovery;
46
46
  readonly details?: JsonValue;
47
47
  readonly operationId?: string;
48
+ /**
49
+ * The underlying cause's own message, when this failure wraps an unexpected error (e.g. a
50
+ * handler throwing something other than a VehicleError) -- bounded, never a full stack trace.
51
+ * Without this, a caller sees only a generic template like "x handler failed" and has no way
52
+ * to tell what actually went wrong or whether the underlying operation may have partially
53
+ * applied.
54
+ */
55
+ readonly causeMessage?: string;
48
56
  }
49
57
 
50
58
  export interface VehicleErrorOptions {
@@ -55,6 +63,15 @@ export interface VehicleErrorOptions {
55
63
  readonly details?: JsonValue;
56
64
  readonly operationId?: string;
57
65
  readonly cause?: unknown;
66
+ /**
67
+ * Secure by default (false): `cause` is always attached to the real in-process Error chain
68
+ * (for server-side logging/observability), but its message crosses the wire via
69
+ * toFailure().causeMessage only when the throw site explicitly opts in here -- an arbitrary
70
+ * cause's message could contain a credential, an internal path, or other detail the thrower
71
+ * never reviewed for wire-safety. Set true only when the cause is known-safe to show (e.g. a
72
+ * validation library's own message intended for the caller).
73
+ */
74
+ readonly exposeCause?: boolean;
58
75
  }
59
76
 
60
77
  export class VehicleError extends Error {
@@ -64,6 +81,7 @@ export class VehicleError extends Error {
64
81
  readonly recovery?: VehicleRecovery;
65
82
  readonly details?: JsonValue;
66
83
  readonly operationId?: string;
84
+ private readonly exposeCause: boolean;
67
85
 
68
86
  constructor(
69
87
  readonly code: string,
@@ -78,9 +96,11 @@ export class VehicleError extends Error {
78
96
  this.recovery = options.recovery;
79
97
  this.details = options.details;
80
98
  this.operationId = options.operationId;
99
+ this.exposeCause = options.exposeCause ?? false;
81
100
  }
82
101
 
83
102
  toFailure(): VehicleFailure {
103
+ const causeMessage = this.exposeCause ? boundedCauseMessage(this.cause) : undefined;
84
104
  return {
85
105
  code: this.code,
86
106
  category: this.category,
@@ -90,10 +110,20 @@ export class VehicleError extends Error {
90
110
  ...(this.recovery === undefined ? {} : { recovery: this.recovery }),
91
111
  ...(this.details === undefined ? {} : { details: this.details }),
92
112
  ...(this.operationId === undefined ? {} : { operationId: this.operationId }),
113
+ ...(causeMessage === undefined ? {} : { causeMessage }),
93
114
  };
94
115
  }
95
116
  }
96
117
 
118
+ const MAX_CAUSE_MESSAGE_LENGTH = 500;
119
+
120
+ /** Extracts a bounded, wire-safe message from an unknown cause -- never the full stack trace, never an unbounded payload. */
121
+ export function boundedCauseMessage(cause: unknown): string | undefined {
122
+ if (cause instanceof Error && cause.message.length > 0) return cause.message.slice(0, MAX_CAUSE_MESSAGE_LENGTH);
123
+ if (typeof cause === "string" && cause.length > 0) return cause.slice(0, MAX_CAUSE_MESSAGE_LENGTH);
124
+ return undefined;
125
+ }
126
+
97
127
  const MAX_VALIDATION_ISSUES = 10;
98
128
  const MAX_ISSUE_MESSAGE_LENGTH = 500;
99
129
  const MAX_ISSUE_PATH_LENGTH = 20;