File size: 7,743 Bytes
c09f67c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
/**
 * @fileoverview Structured error classes for inbox sync operations.
 *
 * This module provides type-safe error handling for email provider integrations.
 * Errors are classified into two categories:
 * - {@link InboxAuthError} - Authentication/authorization failures
 * - {@link InboxSyncError} - Transient sync issues (network, rate limits, etc.)
 *
 * @example
 * ```typescript
 * import { InboxAuthError, isInboxAuthError } from "@midday/inbox/errors";
 *
 * try {
 *   await connector.getAttachments(options);
 * } catch (error) {
 *   if (isInboxAuthError(error)) {
 *     if (error.requiresReauth) {
 *       // User must reconnect their account
 *     }
 *   }
 * }
 * ```
 */

/**
 * Error codes for authentication-related errors.
 *
 * | Code | Description |
 * |------|-------------|
 * | `token_expired` | Access token has expired |
 * | `token_invalid` | Access token is malformed or invalid |
 * | `refresh_token_expired` | Refresh token has expired (typically 90 days) |
 * | `refresh_token_invalid` | Refresh token is missing or invalid |
 * | `unauthorized` | General 401 unauthorized response |
 * | `forbidden` | Permission denied (403) |
 * | `consent_required` | User must re-consent to permissions (Outlook) |
 * | `mfa_required` | Multi-factor authentication required (Outlook) |
 */
export type InboxAuthErrorCode =
  | "token_expired"
  | "token_invalid"
  | "refresh_token_expired"
  | "refresh_token_invalid"
  | "unauthorized"
  | "forbidden"
  | "consent_required"
  | "mfa_required";

/**
 * Error codes for sync-related errors.
 *
 * | Code | Description |
 * |------|-------------|
 * | `fetch_failed` | General failure fetching data from provider |
 * | `rate_limited` | API rate limit exceeded (429) |
 * | `network_error` | Network connectivity issue |
 * | `provider_error` | Provider-specific error |
 */
export type InboxSyncErrorCode =
  | "fetch_failed"
  | "rate_limited"
  | "network_error"
  | "provider_error";

/**
 * Supported email providers.
 */
export type InboxProvider = "gmail" | "outlook";

/**
 * Options for constructing an {@link InboxAuthError}.
 */
interface InboxAuthErrorOptions {
  /** The specific error code identifying the auth failure type */
  code: InboxAuthErrorCode;
  /** The email provider that generated this error */
  provider: InboxProvider;
  /** Human-readable error message */
  message: string;
  /** Whether user intervention is required to resolve this error */
  requiresReauth: boolean;
  /** The original error that caused this error, if any */
  cause?: Error;
}

/**
 * Structured error for authentication and authorization issues.
 *
 * Use the `requiresReauth` property to determine if user intervention is needed:
 * - `true`: User must reconnect their account (token revoked, expired, etc.)
 * - `false`: Error may be transient, retry might succeed
 *
 * @example
 * ```typescript
 * throw new InboxAuthError({
 *   code: "token_expired",
 *   provider: "gmail",
 *   message: "Access token has expired",
 *   requiresReauth: true,
 * });
 * ```
 */
export class InboxAuthError extends Error {
  /** The specific error code */
  readonly code: InboxAuthErrorCode;
  /** The email provider that generated this error */
  readonly provider: InboxProvider;
  /** Whether the user needs to re-authenticate */
  readonly requiresReauth: boolean;

  constructor(options: InboxAuthErrorOptions) {
    super(options.message);
    this.name = "InboxAuthError";
    this.code = options.code;
    this.provider = options.provider;
    this.requiresReauth = options.requiresReauth;

    // Preserve the original error stack if available
    if (options.cause) {
      this.cause = options.cause;
    }

    // Ensure proper prototype chain for instanceof checks
    Object.setPrototypeOf(this, InboxAuthError.prototype);
  }

  /**
   * Check if this error indicates the user needs to reconnect their account.
   * @returns `true` if user must re-authenticate
   */
  isReauthRequired(): boolean {
    return this.requiresReauth;
  }
}

/**
 * Options for constructing an {@link InboxSyncError}.
 */
interface InboxSyncErrorOptions {
  /** The specific error code identifying the sync failure type */
  code: InboxSyncErrorCode;
  /** The email provider that generated this error */
  provider: InboxProvider;
  /** Human-readable error message */
  message: string;
  /** The original error that caused this error, if any */
  cause?: Error;
}

/**
 * Structured error for sync-related issues (non-authentication).
 *
 * These errors are typically transient and may resolve on retry.
 * Use the `isRetryable()` method to check if retrying is recommended.
 *
 * @example
 * ```typescript
 * throw new InboxSyncError({
 *   code: "rate_limited",
 *   provider: "outlook",
 *   message: "API rate limit exceeded",
 * });
 * ```
 */
export class InboxSyncError extends Error {
  /** The specific error code */
  readonly code: InboxSyncErrorCode;
  /** The email provider that generated this error */
  readonly provider: InboxProvider;

  constructor(options: InboxSyncErrorOptions) {
    super(options.message);
    this.name = "InboxSyncError";
    this.code = options.code;
    this.provider = options.provider;

    if (options.cause) {
      this.cause = options.cause;
    }

    Object.setPrototypeOf(this, InboxSyncError.prototype);
  }

  /**
   * Check if this error is likely transient and worth retrying.
   * @returns `true` for network errors and rate limits
   */
  isRetryable(): boolean {
    return this.code === "network_error" || this.code === "rate_limited";
  }
}

/**
 * Type guard to check if an error is an {@link InboxAuthError}.
 *
 * @param error - The error to check
 * @returns `true` if the error is an InboxAuthError
 *
 * @example
 * ```typescript
 * if (isInboxAuthError(error)) {
 *   console.log(error.code); // TypeScript knows this is InboxAuthErrorCode
 * }
 * ```
 */
export function isInboxAuthError(error: unknown): error is InboxAuthError {
  return error instanceof InboxAuthError;
}

/**
 * Type guard to check if an error is an {@link InboxSyncError}.
 *
 * @param error - The error to check
 * @returns `true` if the error is an InboxSyncError
 *
 * @example
 * ```typescript
 * if (isInboxSyncError(error)) {
 *   console.log(error.isRetryable());
 * }
 * ```
 */
export function isInboxSyncError(error: unknown): error is InboxSyncError {
  return error instanceof InboxSyncError;
}

/**
 * Assertion function that narrows an error to {@link InboxAuthError}.
 *
 * Use after a type guard check to avoid manual type casting.
 *
 * @param error - The error to assert
 * @throws {TypeError} If the error is not an InboxAuthError
 *
 * @example
 * ```typescript
 * if (isInboxAuthError(error)) {
 *   assertInboxAuthError(error);
 *   // error is now typed as InboxAuthError without casting
 *   console.log(error.requiresReauth);
 * }
 * ```
 */
export function assertInboxAuthError(
  error: unknown,
): asserts error is InboxAuthError {
  if (!isInboxAuthError(error)) {
    throw new TypeError(`Expected InboxAuthError, got ${typeof error}`);
  }
}

/**
 * Assertion function that narrows an error to {@link InboxSyncError}.
 *
 * Use after a type guard check to avoid manual type casting.
 *
 * @param error - The error to assert
 * @throws {TypeError} If the error is not an InboxSyncError
 *
 * @example
 * ```typescript
 * if (isInboxSyncError(error)) {
 *   assertInboxSyncError(error);
 *   // error is now typed as InboxSyncError without casting
 *   console.log(error.isRetryable());
 * }
 * ```
 */
export function assertInboxSyncError(
  error: unknown,
): asserts error is InboxSyncError {
  if (!isInboxSyncError(error)) {
    throw new TypeError(`Expected InboxSyncError, got ${typeof error}`);
  }
}