teleportation-cli 1.0.0 → 1.0.1

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.
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Vault Error Utilities
3
+ *
4
+ * Custom error classes for Vault operations with actionable suggestions
5
+ */
6
+
7
+ /**
8
+ * Base class for all Vault-related errors
9
+ */
10
+ export class VaultError extends Error {
11
+ constructor(message, options = {}) {
12
+ super(message);
13
+ this.name = this.constructor.name;
14
+ this.code = options.code;
15
+ this.statusCode = options.statusCode;
16
+ this.suggestion = options.suggestion;
17
+ this.context = options.context || {};
18
+
19
+ // Maintain proper stack trace for where error was thrown (V8 only)
20
+ if (Error.captureStackTrace) {
21
+ Error.captureStackTrace(this, this.constructor);
22
+ }
23
+ }
24
+
25
+ toJSON() {
26
+ return {
27
+ name: this.name,
28
+ message: this.message,
29
+ code: this.code,
30
+ statusCode: this.statusCode,
31
+ suggestion: this.suggestion,
32
+ context: this.context,
33
+ };
34
+ }
35
+
36
+ toString() {
37
+ let str = `${this.name}: ${this.message}`;
38
+ if (this.suggestion) {
39
+ str += `\n\nSuggestion: ${this.suggestion}`;
40
+ }
41
+ return str;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Authentication failed error
47
+ * Thrown when API key or app ID is invalid or missing
48
+ */
49
+ export class VaultAuthError extends VaultError {
50
+ constructor(message, options = {}) {
51
+ const defaultMessage = 'Authentication failed: Invalid API key or credentials';
52
+ const defaultSuggestion = `
53
+ Please verify your credentials:
54
+ 1. Check that MECH_API_KEY is set correctly
55
+ 2. Check that MECH_APP_ID is set correctly
56
+ 3. Ensure your API key has not expired
57
+ 4. Run 'teleportation login' to refresh credentials
58
+ `.trim();
59
+
60
+ super(message || defaultMessage, {
61
+ code: 'VAULT_AUTH_FAILED',
62
+ statusCode: 401,
63
+ suggestion: options.suggestion || defaultSuggestion,
64
+ ...options,
65
+ });
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Access denied error
71
+ * Thrown when user doesn't have permission to access resource
72
+ */
73
+ export class VaultAccessDeniedError extends VaultError {
74
+ constructor(message, options = {}) {
75
+ const defaultMessage = 'Access denied: Insufficient permissions';
76
+ const defaultSuggestion = `
77
+ This operation requires additional permissions:
78
+ 1. Verify your app has the required scopes in Mech Vault dashboard
79
+ 2. Check if the namespace belongs to your app
80
+ 3. Contact your administrator if you need elevated permissions
81
+ `.trim();
82
+
83
+ super(message || defaultMessage, {
84
+ code: 'VAULT_ACCESS_DENIED',
85
+ statusCode: 403,
86
+ suggestion: options.suggestion || defaultSuggestion,
87
+ ...options,
88
+ });
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Secret not found error
94
+ * Thrown when requested secret doesn't exist
95
+ */
96
+ export class SecretNotFoundError extends VaultError {
97
+ constructor(secretId, options = {}) {
98
+ const message = options.message || `Secret not found: ${secretId || 'unknown'}`;
99
+ const suggestion = options.suggestion || `
100
+ The requested secret does not exist or has been deleted:
101
+ 1. Verify the secret ID is correct
102
+ 2. Check if the secret has expired
103
+ 3. Ensure you're using the correct namespace
104
+ 4. The secret may have been cleaned up automatically
105
+ `.trim();
106
+
107
+ super(message, {
108
+ code: 'SECRET_NOT_FOUND',
109
+ statusCode: 404,
110
+ suggestion,
111
+ context: { secretId, ...options.context },
112
+ ...options,
113
+ });
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Network error
119
+ * Thrown when network request fails (timeout, connection refused, etc.)
120
+ */
121
+ export class VaultNetworkError extends VaultError {
122
+ constructor(message, options = {}) {
123
+ const defaultMessage = 'Network error: Unable to reach Vault API';
124
+ const defaultSuggestion = `
125
+ Failed to connect to Vault API:
126
+ 1. Check your internet connection
127
+ 2. Verify MECH_VAULT_URL is correct (${options.context?.vaultUrl || 'https://vault.mechdna.net/api'})
128
+ 3. Check if Vault service is operational (https://status.mechdna.net)
129
+ 4. Ensure no firewall is blocking the connection
130
+ 5. Try again in a few moments
131
+ `.trim();
132
+
133
+ super(message || defaultMessage, {
134
+ code: 'VAULT_NETWORK_ERROR',
135
+ suggestion: options.suggestion || defaultSuggestion,
136
+ ...options,
137
+ });
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Server error
143
+ * Thrown when Vault API returns 5xx error
144
+ */
145
+ export class VaultServerError extends VaultError {
146
+ constructor(message, options = {}) {
147
+ const defaultMessage = 'Vault server error: Service temporarily unavailable';
148
+ const defaultSuggestion = `
149
+ The Vault service encountered an error:
150
+ 1. The service may be temporarily unavailable
151
+ 2. Check Vault status at https://status.mechdna.net
152
+ 3. Retry your request in a few moments
153
+ 4. If the problem persists, contact support with the error details
154
+ `.trim();
155
+
156
+ super(message || defaultMessage, {
157
+ code: 'VAULT_SERVER_ERROR',
158
+ statusCode: options.statusCode || 500,
159
+ suggestion: options.suggestion || defaultSuggestion,
160
+ ...options,
161
+ });
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Validation error
167
+ * Thrown when input validation fails
168
+ */
169
+ export class VaultValidationError extends VaultError {
170
+ constructor(message, options = {}) {
171
+ const defaultSuggestion = `
172
+ Input validation failed:
173
+ 1. Check that all required fields are provided
174
+ 2. Verify field formats match requirements
175
+ 3. Ensure values are not empty or whitespace-only
176
+ `.trim();
177
+
178
+ super(message, {
179
+ code: 'VAULT_VALIDATION_ERROR',
180
+ statusCode: 400,
181
+ suggestion: options.suggestion || defaultSuggestion,
182
+ ...options,
183
+ });
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Retry exhausted error
189
+ * Thrown when all retry attempts fail
190
+ */
191
+ export class VaultRetryExhaustedError extends VaultError {
192
+ constructor(originalError, retryCount, options = {}) {
193
+ const message = `Failed after ${retryCount} retries: ${originalError.message}`;
194
+ const suggestion = options.suggestion || `
195
+ All retry attempts have been exhausted:
196
+ 1. Check the underlying error for details
197
+ 2. Verify network connectivity
198
+ 3. Ensure Vault service is operational
199
+ 4. Consider increasing retry limits if transient failures are expected
200
+ `.trim();
201
+
202
+ super(message, {
203
+ code: 'VAULT_RETRY_EXHAUSTED',
204
+ suggestion,
205
+ context: {
206
+ originalError: originalError.message,
207
+ retryCount,
208
+ ...options.context,
209
+ },
210
+ ...options,
211
+ });
212
+
213
+ this.originalError = originalError;
214
+ this.retryCount = retryCount;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Format error for display to user
220
+ * @param {Error} error - Error to format
221
+ * @returns {string} Formatted error message
222
+ */
223
+ export function formatVaultError(error) {
224
+ if (error instanceof VaultError) {
225
+ return error.toString();
226
+ }
227
+
228
+ // Handle generic errors
229
+ return `Error: ${error.message}`;
230
+ }
231
+
232
+ /**
233
+ * Create appropriate VaultError from HTTP response
234
+ * @param {Response} response - Fetch response object
235
+ * @param {Object} context - Additional context
236
+ * @returns {VaultError} Appropriate error instance
237
+ */
238
+ export function createVaultErrorFromResponse(response, context = {}) {
239
+ const status = response.status;
240
+ const statusText = response.statusText;
241
+
242
+ let detailsText = '';
243
+ const responseBody = context && typeof context === 'object' ? context.responseBody : undefined;
244
+ if (responseBody && typeof responseBody === 'string') {
245
+ try {
246
+ const parsed = JSON.parse(responseBody);
247
+ const msg = parsed?.error?.message || parsed?.message;
248
+ const details = parsed?.error?.details;
249
+ const combined = details || msg;
250
+ if (combined && typeof combined === 'string') {
251
+ detailsText = `: ${combined}`;
252
+ }
253
+ } catch {
254
+ const trimmed = responseBody.trim();
255
+ if (trimmed) {
256
+ detailsText = `: ${trimmed.slice(0, 200)}`;
257
+ }
258
+ }
259
+ }
260
+
261
+ if (status === 401) {
262
+ return new VaultAuthError(
263
+ `Authentication failed: ${statusText}${detailsText}`,
264
+ { statusCode: status, context }
265
+ );
266
+ }
267
+
268
+ if (status === 403) {
269
+ return new VaultAccessDeniedError(
270
+ `Access denied: ${statusText}${detailsText}`,
271
+ { statusCode: status, context }
272
+ );
273
+ }
274
+
275
+ if (status === 404) {
276
+ return new SecretNotFoundError(
277
+ context.secretId || 'unknown',
278
+ { statusCode: status, context }
279
+ );
280
+ }
281
+
282
+ if (status >= 500) {
283
+ return new VaultServerError(
284
+ `Server error: ${statusText}${detailsText}`,
285
+ { statusCode: status, context }
286
+ );
287
+ }
288
+
289
+ if (status >= 400 && status < 500) {
290
+ return new VaultValidationError(
291
+ `Request failed: ${statusText}${detailsText}`,
292
+ { statusCode: status, context }
293
+ );
294
+ }
295
+
296
+ return new VaultError(
297
+ `Unexpected error: ${status} ${statusText}`,
298
+ { statusCode: status, context }
299
+ );
300
+ }
301
+
302
+ /**
303
+ * Wrap network errors with VaultNetworkError
304
+ * @param {Error} error - Original network error
305
+ * @param {Object} context - Additional context
306
+ * @returns {VaultNetworkError} Network error with context
307
+ */
308
+ export function wrapNetworkError(error, context = {}) {
309
+ return new VaultNetworkError(
310
+ `Network error: ${error.message}`,
311
+ { context: { ...context, originalError: error.message } }
312
+ );
313
+ }
314
+
315
+ /**
316
+ * Check if error is retryable
317
+ * @param {Error} error - Error to check
318
+ * @returns {boolean} True if error should be retried
319
+ */
320
+ export function isRetryableError(error) {
321
+ // Network errors are retryable
322
+ if (error instanceof VaultNetworkError) {
323
+ return true;
324
+ }
325
+
326
+ // Server errors (5xx) are retryable
327
+ if (error instanceof VaultServerError) {
328
+ return true;
329
+ }
330
+
331
+ // VaultError with 5xx status code
332
+ if (error instanceof VaultError && error.statusCode >= 500) {
333
+ return true;
334
+ }
335
+
336
+ // Generic network errors
337
+ if (error.message && (
338
+ error.message.includes('timeout') ||
339
+ error.message.includes('ECONNREFUSED') ||
340
+ error.message.includes('ETIMEDOUT') ||
341
+ error.message.includes('ENOTFOUND') ||
342
+ error.message.includes('Network')
343
+ )) {
344
+ return true;
345
+ }
346
+
347
+ // Client errors (4xx) are not retryable
348
+ if (error instanceof VaultError && error.statusCode >= 400 && error.statusCode < 500) {
349
+ return false;
350
+ }
351
+
352
+ return false;
353
+ }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "teleportation-cli",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Remote approval system for Claude Code - approve AI coding changes from your phone",
5
5
  "type": "module",
6
6
  "main": "teleportation-cli.cjs",
7
7
  "bin": {
8
- "teleportation": "./teleportation-cli.cjs"
8
+ "teleportation": "teleportation-cli.cjs"
9
9
  },
10
10
  "engines": {
11
11
  "bun": "^1.3.0"
@@ -13,7 +13,7 @@
13
13
  "license": "MIT",
14
14
  "repository": {
15
15
  "type": "git",
16
- "url": "https://github.com/dundas/teleportation-private.git"
16
+ "url": "git+https://github.com/dundas/teleportation-private.git"
17
17
  },
18
18
  "homepage": "https://github.com/dundas/teleportation-private#readme",
19
19
  "bugs": {
@@ -31,7 +31,7 @@
31
31
  ],
32
32
  "author": "Dundas",
33
33
  "scripts": {
34
- "test": "bun --bun vitest run 'tests/**/*.test.js' 'lib/**/*.test.js'",
34
+ "test": "bun --bun vitest run",
35
35
  "test:watch": "bun --bun vitest",
36
36
  "test:coverage": "bun --bun vitest --coverage",
37
37
  "test:e2e": "bun --bun vitest run tests/e2e",
@@ -40,7 +40,7 @@
40
40
  "dev:relay": "cd relay && bun run dev",
41
41
  "dev:mobile": "cd mobile-ui && bun run dev",
42
42
  "dev:all": "bun run dev:relay & bun run dev:mobile",
43
- "prepublishOnly": "echo 'Skipping tests for now - will fix test command post-publish'"
43
+ "prepublishOnly": "bun --bun vitest run"
44
44
  },
45
45
  "files": [
46
46
  "lib/**/*.js",