unotoken 0.1.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.
Files changed (122) hide show
  1. package/README.md +360 -0
  2. package/dist/cli.d.ts +17 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +1207 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/client.d.ts +15 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +15 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/db.d.ts +52 -0
  11. package/dist/db.d.ts.map +1 -0
  12. package/dist/db.js +97 -0
  13. package/dist/db.js.map +1 -0
  14. package/dist/dotenv.d.ts +69 -0
  15. package/dist/dotenv.d.ts.map +1 -0
  16. package/dist/dotenv.js +115 -0
  17. package/dist/dotenv.js.map +1 -0
  18. package/dist/env-mapper.d.ts +55 -0
  19. package/dist/env-mapper.d.ts.map +1 -0
  20. package/dist/env-mapper.js +97 -0
  21. package/dist/env-mapper.js.map +1 -0
  22. package/dist/exec.d.ts +80 -0
  23. package/dist/exec.d.ts.map +1 -0
  24. package/dist/exec.js +214 -0
  25. package/dist/exec.js.map +1 -0
  26. package/dist/index.d.ts +12 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +43 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/oauth/commands.d.ts +151 -0
  31. package/dist/oauth/commands.d.ts.map +1 -0
  32. package/dist/oauth/commands.js +322 -0
  33. package/dist/oauth/commands.js.map +1 -0
  34. package/dist/oauth/config.d.ts +84 -0
  35. package/dist/oauth/config.d.ts.map +1 -0
  36. package/dist/oauth/config.js +156 -0
  37. package/dist/oauth/config.js.map +1 -0
  38. package/dist/oauth/crypto-helpers.d.ts +44 -0
  39. package/dist/oauth/crypto-helpers.d.ts.map +1 -0
  40. package/dist/oauth/crypto-helpers.js +94 -0
  41. package/dist/oauth/crypto-helpers.js.map +1 -0
  42. package/dist/oauth/device-secret.d.ts +57 -0
  43. package/dist/oauth/device-secret.d.ts.map +1 -0
  44. package/dist/oauth/device-secret.js +106 -0
  45. package/dist/oauth/device-secret.js.map +1 -0
  46. package/dist/oauth/flow.d.ts +112 -0
  47. package/dist/oauth/flow.d.ts.map +1 -0
  48. package/dist/oauth/flow.js +255 -0
  49. package/dist/oauth/flow.js.map +1 -0
  50. package/dist/oauth/index.d.ts +18 -0
  51. package/dist/oauth/index.d.ts.map +1 -0
  52. package/dist/oauth/index.js +24 -0
  53. package/dist/oauth/index.js.map +1 -0
  54. package/dist/oauth/key-wrap.d.ts +146 -0
  55. package/dist/oauth/key-wrap.d.ts.map +1 -0
  56. package/dist/oauth/key-wrap.js +275 -0
  57. package/dist/oauth/key-wrap.js.map +1 -0
  58. package/dist/oauth/pkce.d.ts +29 -0
  59. package/dist/oauth/pkce.d.ts.map +1 -0
  60. package/dist/oauth/pkce.js +34 -0
  61. package/dist/oauth/pkce.js.map +1 -0
  62. package/dist/oauth/provider.d.ts +79 -0
  63. package/dist/oauth/provider.d.ts.map +1 -0
  64. package/dist/oauth/provider.js +10 -0
  65. package/dist/oauth/provider.js.map +1 -0
  66. package/dist/oauth/providers/github.d.ts +75 -0
  67. package/dist/oauth/providers/github.d.ts.map +1 -0
  68. package/dist/oauth/providers/github.js +119 -0
  69. package/dist/oauth/providers/github.js.map +1 -0
  70. package/dist/oauth/providers/google.d.ts +115 -0
  71. package/dist/oauth/providers/google.d.ts.map +1 -0
  72. package/dist/oauth/providers/google.js +285 -0
  73. package/dist/oauth/providers/google.js.map +1 -0
  74. package/dist/sdk.d.ts +8 -0
  75. package/dist/sdk.d.ts.map +1 -0
  76. package/dist/sdk.js +8 -0
  77. package/dist/sdk.js.map +1 -0
  78. package/dist/server.d.ts +33 -0
  79. package/dist/server.d.ts.map +1 -0
  80. package/dist/server.js +287 -0
  81. package/dist/server.js.map +1 -0
  82. package/dist/signatures/approval-codes.d.ts +192 -0
  83. package/dist/signatures/approval-codes.d.ts.map +1 -0
  84. package/dist/signatures/approval-codes.js +407 -0
  85. package/dist/signatures/approval-codes.js.map +1 -0
  86. package/dist/signatures/commands.d.ts +108 -0
  87. package/dist/signatures/commands.d.ts.map +1 -0
  88. package/dist/signatures/commands.js +270 -0
  89. package/dist/signatures/commands.js.map +1 -0
  90. package/dist/signatures/devices.d.ts +165 -0
  91. package/dist/signatures/devices.d.ts.map +1 -0
  92. package/dist/signatures/devices.js +344 -0
  93. package/dist/signatures/devices.js.map +1 -0
  94. package/dist/signatures/email-config.d.ts +102 -0
  95. package/dist/signatures/email-config.d.ts.map +1 -0
  96. package/dist/signatures/email-config.js +188 -0
  97. package/dist/signatures/email-config.js.map +1 -0
  98. package/dist/signatures/email.d.ts +106 -0
  99. package/dist/signatures/email.d.ts.map +1 -0
  100. package/dist/signatures/email.js +180 -0
  101. package/dist/signatures/email.js.map +1 -0
  102. package/dist/signatures/fingerprint.d.ts +70 -0
  103. package/dist/signatures/fingerprint.d.ts.map +1 -0
  104. package/dist/signatures/fingerprint.js +123 -0
  105. package/dist/signatures/fingerprint.js.map +1 -0
  106. package/dist/signatures/guard.d.ts +118 -0
  107. package/dist/signatures/guard.d.ts.map +1 -0
  108. package/dist/signatures/guard.js +310 -0
  109. package/dist/signatures/guard.js.map +1 -0
  110. package/dist/signatures/resend.d.ts +84 -0
  111. package/dist/signatures/resend.d.ts.map +1 -0
  112. package/dist/signatures/resend.js +248 -0
  113. package/dist/signatures/resend.js.map +1 -0
  114. package/dist/token-requests.d.ts +80 -0
  115. package/dist/token-requests.d.ts.map +1 -0
  116. package/dist/token-requests.js +201 -0
  117. package/dist/token-requests.js.map +1 -0
  118. package/dist/tokens.d.ts +80 -0
  119. package/dist/tokens.d.ts.map +1 -0
  120. package/dist/tokens.js +150 -0
  121. package/dist/tokens.js.map +1 -0
  122. package/package.json +62 -0
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Device approval guard for unotoken.
3
+ *
4
+ * This module provides a guard that checks whether the current device is
5
+ * approved before allowing vault operations. It integrates:
6
+ * - Device fingerprinting (fingerprint.ts)
7
+ * - Known devices registry (devices.ts)
8
+ * - Email configuration (email-config.ts)
9
+ * - Approval codes (approval-codes.ts)
10
+ * - Email sending (resend.ts)
11
+ *
12
+ * The guard runs BEFORE vault unlock -- a new device cannot even attempt
13
+ * to unlock without approval.
14
+ *
15
+ * Usage patterns:
16
+ * 1. CLI (TTY): Prompts interactively for the 6-digit code
17
+ * 2. SDK (programmatic): Throws DeviceApprovalRequired with instructions
18
+ *
19
+ * Graceful degradation: If no email is configured, the guard allows the
20
+ * operation to proceed (signatures are opt-in via email config).
21
+ *
22
+ * @module
23
+ */
24
+ import { generateDeviceFingerprint, getDefaultDeviceName } from './fingerprint.js';
25
+ import { checkDevice, DevicesDatabase } from './devices.js';
26
+ import { loadEmailConfig, maskEmail } from './email-config.js';
27
+ import { createApprovalCode, verifyApprovalCode } from './approval-codes.js';
28
+ import { sendApprovalEmail } from './resend.js';
29
+ // ─── Error ──────────────────────────────────────────────────────────
30
+ /**
31
+ * Error thrown when a device requires approval but is used programmatically
32
+ * (no TTY to prompt the user).
33
+ *
34
+ * The caller (app/cloud service) is responsible for:
35
+ * 1. Sending the code via approveDevice()
36
+ * 2. Collecting the code from the user
37
+ * 3. Calling completeApproval() with the code
38
+ */
39
+ export class DeviceApprovalRequired extends Error {
40
+ /** The device fingerprint that needs approval */
41
+ fingerprint;
42
+ /** The masked email where the code was sent (or null if not sent) */
43
+ maskedEmail;
44
+ /** Whether a code was actually sent */
45
+ codeSent;
46
+ constructor(fingerprint, maskedEmail, codeSent) {
47
+ const emailPart = maskedEmail
48
+ ? ` A code has been sent to ${maskedEmail}.`
49
+ : '';
50
+ super(`Device not approved. This device (${fingerprint.slice(0, 12)}...) must be approved before accessing the vault.${emailPart}` +
51
+ ` Use approveDevice() to complete the approval flow.`);
52
+ this.name = 'DeviceApprovalRequired';
53
+ this.fingerprint = fingerprint;
54
+ this.maskedEmail = maskedEmail;
55
+ this.codeSent = codeSent;
56
+ }
57
+ }
58
+ // ─── Guard Implementation ───────────────────────────────────────────
59
+ /**
60
+ * Check if the current device is approved and handle the approval flow.
61
+ *
62
+ * This is the main entry point for device checks. It should be called
63
+ * before any vault operation (unlock, get, set, list).
64
+ *
65
+ * Flow:
66
+ * 1. Generate device fingerprint
67
+ * 2. Check if device is known (auto-approves first device)
68
+ * 3. If known: allow operation, update last_seen
69
+ * 4. If unknown:
70
+ * a. If no email configured: allow (graceful degradation)
71
+ * b. If TTY: send code, prompt user, verify, register device
72
+ * c. If no TTY: throw DeviceApprovalRequired
73
+ *
74
+ * @param options - Guard configuration
75
+ * @returns Result indicating whether the operation should proceed
76
+ * @throws DeviceApprovalRequired when used programmatically and device is unknown
77
+ */
78
+ export async function deviceGuard(options = {}) {
79
+ const { baseDir, readLine, writeOutput = (msg) => process.stderr.write(msg), writeError = (msg) => process.stderr.write(msg), } = options;
80
+ const isTTY = options.isTTY ?? (process.stdin.isTTY === true);
81
+ // 1. Generate fingerprint and device name
82
+ const fingerprint = generateDeviceFingerprint(baseDir);
83
+ const deviceName = getDefaultDeviceName();
84
+ // 2. Check device status
85
+ const deviceCheck = await checkDevice(fingerprint, deviceName, baseDir);
86
+ // 3. Device is known -- allow operation
87
+ if (deviceCheck.known) {
88
+ return {
89
+ allowed: true,
90
+ newlyApproved: false,
91
+ autoApproved: deviceCheck.autoApproved,
92
+ deviceName,
93
+ fingerprint,
94
+ };
95
+ }
96
+ // 4. Device is unknown -- check email config
97
+ const emailConfig = loadEmailConfig(baseDir);
98
+ const emailConfigured = emailConfig !== null && emailConfig.verified === true;
99
+ // 4a. No email configured -- graceful degradation
100
+ if (!emailConfigured) {
101
+ writeOutput('No email configured. Run `unotoken config email <address>` to enable device signatures.\n');
102
+ return {
103
+ allowed: true,
104
+ newlyApproved: false,
105
+ autoApproved: false,
106
+ deviceName,
107
+ fingerprint,
108
+ reason: 'no_email_configured',
109
+ };
110
+ }
111
+ const maskedAddr = maskEmail(emailConfig.email);
112
+ // 4b. TTY mode -- interactive approval
113
+ if (isTTY && readLine) {
114
+ return interactiveApproval({
115
+ fingerprint,
116
+ deviceName,
117
+ email: emailConfig.email,
118
+ maskedEmail: maskedAddr,
119
+ baseDir,
120
+ readLine,
121
+ writeOutput,
122
+ writeError,
123
+ });
124
+ }
125
+ // 4c. Non-TTY / SDK mode -- throw error
126
+ // Try to send the code proactively so the caller can collect it
127
+ let codeSent = false;
128
+ try {
129
+ const codeResult = await createApprovalCode(fingerprint, baseDir);
130
+ if (codeResult) {
131
+ // Send the email
132
+ const context = {
133
+ deviceName,
134
+ timestamp: new Date().toISOString(),
135
+ };
136
+ const sendResult = await sendApprovalEmail(emailConfig.email, codeResult.code, context, fingerprint, baseDir);
137
+ codeSent = sendResult.success;
138
+ }
139
+ }
140
+ catch {
141
+ // Best-effort -- if sending fails, the caller will need to retry
142
+ }
143
+ throw new DeviceApprovalRequired(fingerprint, maskedAddr, codeSent);
144
+ }
145
+ /**
146
+ * Run the interactive device approval flow (TTY mode).
147
+ *
148
+ * Sends an approval code to the user's email and prompts for entry.
149
+ */
150
+ async function interactiveApproval(opts) {
151
+ const { fingerprint, deviceName, email, maskedEmail, baseDir, readLine, writeOutput, writeError, } = opts;
152
+ // Create an approval code
153
+ const codeResult = await createApprovalCode(fingerprint, baseDir);
154
+ if (!codeResult) {
155
+ writeError('Rate limit exceeded. Too many approval codes requested. Please wait before trying again.\n');
156
+ return {
157
+ allowed: false,
158
+ newlyApproved: false,
159
+ autoApproved: false,
160
+ deviceName,
161
+ fingerprint,
162
+ reason: 'rate_limited',
163
+ };
164
+ }
165
+ // Send the email
166
+ const context = {
167
+ deviceName,
168
+ timestamp: new Date().toISOString(),
169
+ };
170
+ const sendResult = await sendApprovalEmail(email, codeResult.code, context, fingerprint, baseDir);
171
+ if (!sendResult.success) {
172
+ writeError(`Failed to send approval code: ${sendResult.error ?? 'Unknown error'}\n`);
173
+ return {
174
+ allowed: false,
175
+ newlyApproved: false,
176
+ autoApproved: false,
177
+ deviceName,
178
+ fingerprint,
179
+ reason: 'email_send_failed',
180
+ };
181
+ }
182
+ writeOutput(`New device detected. Sending approval code to ${maskedEmail}...\n`);
183
+ // Prompt for code (up to 3 attempts handled by approval-codes engine)
184
+ const MAX_PROMPT_ATTEMPTS = 3;
185
+ for (let i = 0; i < MAX_PROMPT_ATTEMPTS; i++) {
186
+ const userInput = await readLine('Enter approval code: ');
187
+ const trimmed = userInput.trim();
188
+ const verifyResult = await verifyApprovalCode(fingerprint, trimmed, baseDir);
189
+ if (verifyResult.valid) {
190
+ // Register the device
191
+ const db = await DevicesDatabase.open(baseDir);
192
+ try {
193
+ db.registerDevice(fingerprint, deviceName, 'email_code');
194
+ }
195
+ finally {
196
+ db.close();
197
+ }
198
+ writeOutput(`Device approved: ${deviceName}\n`);
199
+ return {
200
+ allowed: true,
201
+ newlyApproved: true,
202
+ autoApproved: false,
203
+ deviceName,
204
+ fingerprint,
205
+ };
206
+ }
207
+ // Check if we should give more attempts
208
+ if (verifyResult.reason?.includes('Maximum attempts') || verifyResult.reason?.includes('No active')) {
209
+ writeError('Code expired. Run the command again to get a new code.\n');
210
+ return {
211
+ allowed: false,
212
+ newlyApproved: false,
213
+ autoApproved: false,
214
+ deviceName,
215
+ fingerprint,
216
+ reason: 'max_attempts_or_expired',
217
+ };
218
+ }
219
+ // Extract remaining attempts from reason
220
+ const remainingMatch = verifyResult.reason?.match(/(\d+) attempt/);
221
+ const remaining = remainingMatch ? parseInt(remainingMatch[1], 10) : MAX_PROMPT_ATTEMPTS - i - 1;
222
+ writeError(`Invalid code. ${remaining} attempt${remaining === 1 ? '' : 's'} remaining.\n`);
223
+ }
224
+ // Should not reach here normally (approval-codes engine handles attempt limits)
225
+ writeError('Code expired. Run the command again to get a new code.\n');
226
+ return {
227
+ allowed: false,
228
+ newlyApproved: false,
229
+ autoApproved: false,
230
+ deviceName,
231
+ fingerprint,
232
+ reason: 'max_attempts_or_expired',
233
+ };
234
+ }
235
+ // ─── Programmatic Approval ──────────────────────────────────────────
236
+ /**
237
+ * Programmatically approve a device with an approval code.
238
+ *
239
+ * For use by SDK consumers who caught DeviceApprovalRequired and collected
240
+ * the code from the user via their own UI.
241
+ *
242
+ * @param fingerprint - The device fingerprint from the error
243
+ * @param code - The 6-digit code entered by the user
244
+ * @param baseDir - Optional base directory for config/database files
245
+ * @returns Object with `approved` boolean and optional `reason`
246
+ */
247
+ export async function approveDevice(fingerprint, code, baseDir) {
248
+ const deviceName = getDefaultDeviceName();
249
+ const verifyResult = await verifyApprovalCode(fingerprint, code, baseDir);
250
+ if (!verifyResult.valid) {
251
+ return {
252
+ approved: false,
253
+ deviceName,
254
+ reason: verifyResult.reason,
255
+ };
256
+ }
257
+ // Register the device
258
+ const db = await DevicesDatabase.open(baseDir);
259
+ try {
260
+ db.registerDevice(fingerprint, deviceName, 'email_code');
261
+ }
262
+ finally {
263
+ db.close();
264
+ }
265
+ return { approved: true, deviceName };
266
+ }
267
+ /**
268
+ * Send an approval code to the user's configured email for a specific device.
269
+ *
270
+ * For use by SDK consumers who need to initiate the approval flow programmatically.
271
+ *
272
+ * @param fingerprint - The device fingerprint
273
+ * @param baseDir - Optional base directory for config/database files
274
+ * @returns Object with `sent` boolean, `maskedEmail`, and optional `error`
275
+ */
276
+ export async function sendDeviceApprovalCode(fingerprint, baseDir) {
277
+ const emailConfig = loadEmailConfig(baseDir);
278
+ if (!emailConfig || !emailConfig.verified) {
279
+ return {
280
+ sent: false,
281
+ error: 'No verified email configured. Run `unotoken config email <address>` first.',
282
+ };
283
+ }
284
+ const codeResult = await createApprovalCode(fingerprint, baseDir);
285
+ if (!codeResult) {
286
+ return {
287
+ sent: false,
288
+ maskedEmail: maskEmail(emailConfig.email),
289
+ error: 'Rate limit exceeded. Too many approval codes requested.',
290
+ };
291
+ }
292
+ const deviceName = getDefaultDeviceName();
293
+ const context = {
294
+ deviceName,
295
+ timestamp: new Date().toISOString(),
296
+ };
297
+ const sendResult = await sendApprovalEmail(emailConfig.email, codeResult.code, context, fingerprint, baseDir);
298
+ if (!sendResult.success) {
299
+ return {
300
+ sent: false,
301
+ maskedEmail: maskEmail(emailConfig.email),
302
+ error: sendResult.error,
303
+ };
304
+ }
305
+ return {
306
+ sent: true,
307
+ maskedEmail: maskEmail(emailConfig.email),
308
+ };
309
+ }
310
+ //# sourceMappingURL=guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guard.js","sourceRoot":"","sources":["../../src/signatures/guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Resend API integration for unotoken device approval emails.
3
+ *
4
+ * Uses the Resend REST API directly (no SDK dependency) to send
5
+ * branded 6-digit approval codes for device verification.
6
+ *
7
+ * Features:
8
+ * - Built-in Resend API key (Indigo-hosted, rate-limited) for zero-config setup
9
+ * - Configurable override for users who want their own Resend account
10
+ * - Rate limiting: max 3 emails per device per hour to prevent abuse
11
+ *
12
+ * @module
13
+ */
14
+ export interface ApprovalEmailContext {
15
+ /** Human-friendly device name (e.g., "stefan@macbook") */
16
+ deviceName: string;
17
+ /** ISO 8601 timestamp of when the code was generated */
18
+ timestamp: string;
19
+ /** IP address hint (best-effort, may be "unknown") */
20
+ ipHint?: string;
21
+ }
22
+ export interface SendEmailResult {
23
+ /** Whether the email was sent successfully */
24
+ success: boolean;
25
+ /** Resend message ID (if successful) */
26
+ messageId?: string;
27
+ /** Error message (if failed) */
28
+ error?: string;
29
+ }
30
+ /**
31
+ * Check if sending another email for this device fingerprint is allowed.
32
+ *
33
+ * @param deviceFingerprint - The device fingerprint to check
34
+ * @returns true if under the rate limit
35
+ */
36
+ export declare function isRateLimited(deviceFingerprint: string): boolean;
37
+ /**
38
+ * Get remaining sends in the current rate limit window.
39
+ *
40
+ * @param deviceFingerprint - The device fingerprint
41
+ * @returns Number of remaining sends allowed
42
+ */
43
+ export declare function remainingSends(deviceFingerprint: string): number;
44
+ /**
45
+ * Reset rate limit state (for testing).
46
+ */
47
+ export declare function resetRateLimits(): void;
48
+ /**
49
+ * Get the effective Resend API key.
50
+ *
51
+ * Checks for a user-configured custom key first, falls back to built-in.
52
+ *
53
+ * @param baseDir - Optional base directory for email config
54
+ * @returns The Resend API key to use
55
+ */
56
+ export declare function getResendApiKey(baseDir?: string): string;
57
+ /**
58
+ * Build the HTML email body for a device approval code.
59
+ *
60
+ * @param code - The 6-digit approval code
61
+ * @param context - Additional context about the request
62
+ * @returns HTML string for the email body
63
+ */
64
+ export declare function buildApprovalEmailHtml(code: string, context: ApprovalEmailContext): string;
65
+ /**
66
+ * Build the plain text email body for a device approval code.
67
+ *
68
+ * @param code - The 6-digit approval code
69
+ * @param context - Additional context about the request
70
+ * @returns Plain text string for the email body
71
+ */
72
+ export declare function buildApprovalEmailText(code: string, context: ApprovalEmailContext): string;
73
+ /**
74
+ * Send a device approval email via the Resend API.
75
+ *
76
+ * @param to - Recipient email address
77
+ * @param code - The 6-digit approval code
78
+ * @param context - Additional context (device name, timestamp, IP)
79
+ * @param deviceFingerprint - Device fingerprint for rate limiting
80
+ * @param baseDir - Optional base directory for email config (API key lookup)
81
+ * @returns Result object with success status and optional message ID
82
+ */
83
+ export declare function sendApprovalEmail(to: string, code: string, context: ApprovalEmailContext, deviceFingerprint: string, baseDir?: string): Promise<SendEmailResult>;
84
+ //# sourceMappingURL=resend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resend.d.ts","sourceRoot":"","sources":["../../src/signatures/resend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAqBH,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,UAAU,EAAE,MAAM,CAAC;IACnB,wDAAwD;IACxD,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAeD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,iBAAiB,EAAE,MAAM,GAAG,OAAO,CAQhE;AAeD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED;;GAEG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAEtC;AAID;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAGxD;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,MAAM,CAmC1F;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,GAAG,MAAM,CA4B1F;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,oBAAoB,EAC7B,iBAAiB,EAAE,MAAM,EACzB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,eAAe,CAAC,CA4D1B"}
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Resend API integration for unotoken device approval emails.
3
+ *
4
+ * Uses the Resend REST API directly (no SDK dependency) to send
5
+ * branded 6-digit approval codes for device verification.
6
+ *
7
+ * Features:
8
+ * - Built-in Resend API key (Indigo-hosted, rate-limited) for zero-config setup
9
+ * - Configurable override for users who want their own Resend account
10
+ * - Rate limiting: max 3 emails per device per hour to prevent abuse
11
+ *
12
+ * @module
13
+ */
14
+ import { loadEmailConfig } from './email-config.js';
15
+ // ─── Constants ──────────────────────────────────────────────────────
16
+ /**
17
+ * Built-in Resend API key for zero-config device approval emails.
18
+ *
19
+ * This is an Indigo-hosted key with rate limits applied. Users who need
20
+ * higher throughput should configure their own key with:
21
+ * unotoken config email --resend-key <key>
22
+ */
23
+ const BUILT_IN_RESEND_API_KEY = 're_placeholder_builtin_key';
24
+ const RESEND_API_URL = 'https://api.resend.com/emails';
25
+ const FROM_ADDRESS = 'unotoken <noreply@getindigo.ai>';
26
+ // ─── Rate Limiting ──────────────────────────────────────────────────
27
+ /**
28
+ * In-memory rate limit tracker.
29
+ *
30
+ * Tracks email send timestamps per device fingerprint. Resets on process
31
+ * restart (acceptable since this is a CLI tool, not a long-running server).
32
+ */
33
+ const rateLimitMap = new Map();
34
+ const MAX_EMAILS_PER_HOUR = 3;
35
+ const RATE_LIMIT_WINDOW_MS = 60 * 60 * 1000; // 1 hour
36
+ /**
37
+ * Check if sending another email for this device fingerprint is allowed.
38
+ *
39
+ * @param deviceFingerprint - The device fingerprint to check
40
+ * @returns true if under the rate limit
41
+ */
42
+ export function isRateLimited(deviceFingerprint) {
43
+ const now = Date.now();
44
+ const timestamps = rateLimitMap.get(deviceFingerprint) ?? [];
45
+ // Filter to only timestamps within the window
46
+ const recent = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
47
+ return recent.length >= MAX_EMAILS_PER_HOUR;
48
+ }
49
+ /**
50
+ * Record a sent email for rate limiting.
51
+ *
52
+ * @param deviceFingerprint - The device fingerprint
53
+ */
54
+ function recordSend(deviceFingerprint) {
55
+ const now = Date.now();
56
+ const timestamps = rateLimitMap.get(deviceFingerprint) ?? [];
57
+ const recent = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
58
+ recent.push(now);
59
+ rateLimitMap.set(deviceFingerprint, recent);
60
+ }
61
+ /**
62
+ * Get remaining sends in the current rate limit window.
63
+ *
64
+ * @param deviceFingerprint - The device fingerprint
65
+ * @returns Number of remaining sends allowed
66
+ */
67
+ export function remainingSends(deviceFingerprint) {
68
+ const now = Date.now();
69
+ const timestamps = rateLimitMap.get(deviceFingerprint) ?? [];
70
+ const recent = timestamps.filter((t) => now - t < RATE_LIMIT_WINDOW_MS);
71
+ return Math.max(0, MAX_EMAILS_PER_HOUR - recent.length);
72
+ }
73
+ /**
74
+ * Reset rate limit state (for testing).
75
+ */
76
+ export function resetRateLimits() {
77
+ rateLimitMap.clear();
78
+ }
79
+ // ─── Email Sending ──────────────────────────────────────────────────
80
+ /**
81
+ * Get the effective Resend API key.
82
+ *
83
+ * Checks for a user-configured custom key first, falls back to built-in.
84
+ *
85
+ * @param baseDir - Optional base directory for email config
86
+ * @returns The Resend API key to use
87
+ */
88
+ export function getResendApiKey(baseDir) {
89
+ const config = loadEmailConfig(baseDir);
90
+ return config?.resendApiKey ?? BUILT_IN_RESEND_API_KEY;
91
+ }
92
+ /**
93
+ * Build the HTML email body for a device approval code.
94
+ *
95
+ * @param code - The 6-digit approval code
96
+ * @param context - Additional context about the request
97
+ * @returns HTML string for the email body
98
+ */
99
+ export function buildApprovalEmailHtml(code, context) {
100
+ const timestamp = new Date(context.timestamp).toLocaleString();
101
+ const ipLine = context.ipHint && context.ipHint !== 'unknown'
102
+ ? `<p style="color:#666;font-size:13px;margin:4px 0;">IP: ${escapeHtml(context.ipHint)}</p>`
103
+ : '';
104
+ return `
105
+ <!DOCTYPE html>
106
+ <html>
107
+ <head><meta charset="utf-8"></head>
108
+ <body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:480px;margin:0 auto;padding:20px;">
109
+ <h2 style="color:#1a1a1a;margin-bottom:4px;">Device Approval Code</h2>
110
+ <p style="color:#666;margin-top:0;">A new device is requesting access to your unotoken vault.</p>
111
+
112
+ <div style="background:#f4f4f5;border-radius:8px;padding:24px;text-align:center;margin:24px 0;">
113
+ <p style="color:#666;font-size:13px;margin:0 0 8px 0;">Your approval code:</p>
114
+ <div style="font-size:36px;font-weight:700;letter-spacing:8px;color:#1a1a1a;font-family:monospace;">${escapeHtml(code)}</div>
115
+ <p style="color:#999;font-size:12px;margin:8px 0 0 0;">Expires in 5 minutes</p>
116
+ </div>
117
+
118
+ <div style="background:#fafafa;border-radius:6px;padding:12px 16px;margin:16px 0;">
119
+ <p style="color:#666;font-size:13px;margin:4px 0;"><strong>Device:</strong> ${escapeHtml(context.deviceName)}</p>
120
+ <p style="color:#666;font-size:13px;margin:4px 0;"><strong>Time:</strong> ${escapeHtml(timestamp)}</p>
121
+ ${ipLine}
122
+ </div>
123
+
124
+ <p style="color:#999;font-size:12px;margin-top:24px;">
125
+ If you did not request this, someone may be trying to access your vault from a new device.
126
+ You can safely ignore this email -- the code will expire automatically.
127
+ </p>
128
+
129
+ <hr style="border:none;border-top:1px solid #eee;margin:24px 0;">
130
+ <p style="color:#bbb;font-size:11px;">Sent by unotoken -- secure secret vault</p>
131
+ </body>
132
+ </html>`.trim();
133
+ }
134
+ /**
135
+ * Build the plain text email body for a device approval code.
136
+ *
137
+ * @param code - The 6-digit approval code
138
+ * @param context - Additional context about the request
139
+ * @returns Plain text string for the email body
140
+ */
141
+ export function buildApprovalEmailText(code, context) {
142
+ const timestamp = new Date(context.timestamp).toLocaleString();
143
+ const ipLine = context.ipHint && context.ipHint !== 'unknown'
144
+ ? `IP: ${context.ipHint}\n`
145
+ : '';
146
+ return [
147
+ 'Device Approval Code',
148
+ '====================',
149
+ '',
150
+ 'A new device is requesting access to your unotoken vault.',
151
+ '',
152
+ `Your approval code: ${code}`,
153
+ '',
154
+ 'This code expires in 5 minutes.',
155
+ '',
156
+ `Device: ${context.deviceName}`,
157
+ `Time: ${timestamp}`,
158
+ ipLine ? ipLine.trim() : null,
159
+ '',
160
+ 'If you did not request this, someone may be trying to access your',
161
+ 'vault from a new device. You can safely ignore this email.',
162
+ '',
163
+ '---',
164
+ 'Sent by unotoken',
165
+ ]
166
+ .filter((line) => line !== null)
167
+ .join('\n');
168
+ }
169
+ /**
170
+ * Send a device approval email via the Resend API.
171
+ *
172
+ * @param to - Recipient email address
173
+ * @param code - The 6-digit approval code
174
+ * @param context - Additional context (device name, timestamp, IP)
175
+ * @param deviceFingerprint - Device fingerprint for rate limiting
176
+ * @param baseDir - Optional base directory for email config (API key lookup)
177
+ * @returns Result object with success status and optional message ID
178
+ */
179
+ export async function sendApprovalEmail(to, code, context, deviceFingerprint, baseDir) {
180
+ // Rate limit check
181
+ if (isRateLimited(deviceFingerprint)) {
182
+ return {
183
+ success: false,
184
+ error: `Rate limit exceeded: maximum ${MAX_EMAILS_PER_HOUR} approval emails per device per hour. Please wait before requesting a new code.`,
185
+ };
186
+ }
187
+ const apiKey = getResendApiKey(baseDir);
188
+ const htmlBody = buildApprovalEmailHtml(code, context);
189
+ const textBody = buildApprovalEmailText(code, context);
190
+ try {
191
+ const response = await fetch(RESEND_API_URL, {
192
+ method: 'POST',
193
+ headers: {
194
+ 'Authorization': `Bearer ${apiKey}`,
195
+ 'Content-Type': 'application/json',
196
+ },
197
+ body: JSON.stringify({
198
+ from: FROM_ADDRESS,
199
+ to: [to],
200
+ subject: `${code} is your unotoken device approval code`,
201
+ html: htmlBody,
202
+ text: textBody,
203
+ }),
204
+ });
205
+ if (!response.ok) {
206
+ const errorBody = await response.text();
207
+ let errorMsg;
208
+ try {
209
+ const parsed = JSON.parse(errorBody);
210
+ errorMsg = parsed.message ?? parsed.error ?? errorBody;
211
+ }
212
+ catch {
213
+ errorMsg = errorBody;
214
+ }
215
+ return {
216
+ success: false,
217
+ error: `Resend API error (${response.status}): ${errorMsg}`,
218
+ };
219
+ }
220
+ const data = (await response.json());
221
+ // Record successful send for rate limiting
222
+ recordSend(deviceFingerprint);
223
+ return {
224
+ success: true,
225
+ messageId: data.id,
226
+ };
227
+ }
228
+ catch (err) {
229
+ const msg = err instanceof Error ? err.message : String(err);
230
+ return {
231
+ success: false,
232
+ error: `Failed to send email: ${msg}`,
233
+ };
234
+ }
235
+ }
236
+ // ─── Helpers ────────────────────────────────────────────────────────
237
+ /**
238
+ * Escape HTML entities to prevent XSS in email body.
239
+ */
240
+ function escapeHtml(str) {
241
+ return str
242
+ .replace(/&/g, '&amp;')
243
+ .replace(/</g, '&lt;')
244
+ .replace(/>/g, '&gt;')
245
+ .replace(/"/g, '&quot;')
246
+ .replace(/'/g, '&#39;');
247
+ }
248
+ //# sourceMappingURL=resend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resend.js","sourceRoot":"","sources":["../../src/signatures/resend.ts"],"names":[],"mappings":"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"}