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,344 @@
1
+ /**
2
+ * Known devices registry for unotoken device signatures.
3
+ *
4
+ * Manages a SQLite database of approved devices. Each device is identified
5
+ * by its fingerprint (see fingerprint.ts). Devices must be approved before
6
+ * they can access the vault.
7
+ *
8
+ * The registry is stored at ~/.yokotoken/devices.db (separate from the
9
+ * encrypted vault) because device checks happen BEFORE vault unlock.
10
+ *
11
+ * Table: known_devices
12
+ * - device_id: UUID primary key
13
+ * - fingerprint: SHA-256 hex string (unique)
14
+ * - name: human-friendly device name (e.g., "stefan@macbook")
15
+ * - approved_at: ISO 8601 timestamp
16
+ * - last_seen_at: ISO 8601 timestamp (updated on each vault operation)
17
+ * - approval_method: 'email_code' | 'initial_setup'
18
+ *
19
+ * @module
20
+ */
21
+ import initSqlJs from 'sql.js';
22
+ import { randomUUID } from 'node:crypto';
23
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
24
+ import path from 'node:path';
25
+ import { getDeviceDir } from '../oauth/device-secret.js';
26
+ // ─── Constants ──────────────────────────────────────────────────────
27
+ const DEVICES_DB_FILENAME = 'devices.db';
28
+ // ─── SQL.js Initialization ──────────────────────────────────────────
29
+ /** Cached sql.js module (loaded once). */
30
+ let sqlJsModule = null;
31
+ async function getSqlJs() {
32
+ if (!sqlJsModule) {
33
+ sqlJsModule = await initSqlJs();
34
+ }
35
+ return sqlJsModule;
36
+ }
37
+ // ─── DevicesDatabase ────────────────────────────────────────────────
38
+ /**
39
+ * Manages the known_devices SQLite database.
40
+ *
41
+ * Uses sql.js (SQLite compiled to WASM) for portable, single-file access.
42
+ * The database lives in memory and is persisted to disk after mutations.
43
+ */
44
+ export class DevicesDatabase {
45
+ db;
46
+ dbPath;
47
+ constructor(db, dbPath) {
48
+ this.db = db;
49
+ this.dbPath = dbPath;
50
+ }
51
+ /**
52
+ * Open or create the devices database at the given path.
53
+ *
54
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
55
+ * @returns A ready-to-use DevicesDatabase instance
56
+ */
57
+ static async open(baseDir) {
58
+ const dir = baseDir ?? getDeviceDir();
59
+ const dbPath = path.join(dir, DEVICES_DB_FILENAME);
60
+ if (!existsSync(dir)) {
61
+ mkdirSync(dir, { recursive: true });
62
+ }
63
+ const SQL = await getSqlJs();
64
+ let db;
65
+ if (existsSync(dbPath)) {
66
+ const buffer = readFileSync(dbPath);
67
+ db = new SQL.Database(buffer);
68
+ }
69
+ else {
70
+ db = new SQL.Database();
71
+ }
72
+ const instance = new DevicesDatabase(db, dbPath);
73
+ instance.initSchema();
74
+ return instance;
75
+ }
76
+ /**
77
+ * Initialize the known_devices table if it does not exist.
78
+ */
79
+ initSchema() {
80
+ this.db.run(`
81
+ CREATE TABLE IF NOT EXISTS known_devices (
82
+ device_id TEXT PRIMARY KEY,
83
+ fingerprint TEXT NOT NULL UNIQUE,
84
+ name TEXT NOT NULL,
85
+ approved_at TEXT NOT NULL,
86
+ last_seen_at TEXT NOT NULL,
87
+ approval_method TEXT NOT NULL CHECK (approval_method IN ('email_code', 'initial_setup'))
88
+ )
89
+ `);
90
+ this.save();
91
+ }
92
+ /**
93
+ * Persist the in-memory database to disk.
94
+ */
95
+ save() {
96
+ const dir = path.dirname(this.dbPath);
97
+ if (!existsSync(dir)) {
98
+ mkdirSync(dir, { recursive: true });
99
+ }
100
+ const data = this.db.export();
101
+ writeFileSync(this.dbPath, Buffer.from(data));
102
+ }
103
+ /**
104
+ * Check whether a device with the given fingerprint is registered.
105
+ *
106
+ * @param fingerprint - The device fingerprint (SHA-256 hex)
107
+ * @returns true if the device is in the known_devices table
108
+ */
109
+ isDeviceKnown(fingerprint) {
110
+ const stmt = this.db.prepare('SELECT COUNT(*) as cnt FROM known_devices WHERE fingerprint = ?');
111
+ stmt.bind([fingerprint]);
112
+ let count = 0;
113
+ if (stmt.step()) {
114
+ const row = stmt.getAsObject();
115
+ count = row.cnt;
116
+ }
117
+ stmt.free();
118
+ return count > 0;
119
+ }
120
+ /**
121
+ * Register a new device in the known_devices table.
122
+ *
123
+ * @param fingerprint - The device fingerprint (SHA-256 hex)
124
+ * @param name - Human-friendly device name
125
+ * @param method - How the device was approved
126
+ * @returns The newly created KnownDevice record
127
+ * @throws Error if the fingerprint is already registered
128
+ */
129
+ registerDevice(fingerprint, name, method) {
130
+ const now = new Date().toISOString();
131
+ const deviceId = randomUUID();
132
+ this.db.run(`INSERT INTO known_devices (device_id, fingerprint, name, approved_at, last_seen_at, approval_method)
133
+ VALUES (?, ?, ?, ?, ?, ?)`, [deviceId, fingerprint, name, now, now, method]);
134
+ this.save();
135
+ return {
136
+ device_id: deviceId,
137
+ fingerprint,
138
+ name,
139
+ approved_at: now,
140
+ last_seen_at: now,
141
+ approval_method: method,
142
+ };
143
+ }
144
+ /**
145
+ * Get a known device by fingerprint.
146
+ *
147
+ * @param fingerprint - The device fingerprint (SHA-256 hex)
148
+ * @returns The device record, or null if not found
149
+ */
150
+ getDevice(fingerprint) {
151
+ const stmt = this.db.prepare('SELECT device_id, fingerprint, name, approved_at, last_seen_at, approval_method FROM known_devices WHERE fingerprint = ?');
152
+ stmt.bind([fingerprint]);
153
+ let device = null;
154
+ if (stmt.step()) {
155
+ const row = stmt.getAsObject();
156
+ device = {
157
+ device_id: row.device_id,
158
+ fingerprint: row.fingerprint,
159
+ name: row.name,
160
+ approved_at: row.approved_at,
161
+ last_seen_at: row.last_seen_at,
162
+ approval_method: row.approval_method,
163
+ };
164
+ }
165
+ stmt.free();
166
+ return device;
167
+ }
168
+ /**
169
+ * Update the last_seen_at timestamp for a device.
170
+ *
171
+ * Called on each vault operation from a known device.
172
+ *
173
+ * @param fingerprint - The device fingerprint
174
+ */
175
+ updateLastSeen(fingerprint) {
176
+ const now = new Date().toISOString();
177
+ this.db.run('UPDATE known_devices SET last_seen_at = ? WHERE fingerprint = ?', [now, fingerprint]);
178
+ this.save();
179
+ }
180
+ /**
181
+ * List all known (approved) devices.
182
+ *
183
+ * @returns Array of KnownDevice records, ordered by approved_at ascending
184
+ */
185
+ listDevices() {
186
+ const devices = [];
187
+ const stmt = this.db.prepare('SELECT device_id, fingerprint, name, approved_at, last_seen_at, approval_method FROM known_devices ORDER BY approved_at ASC');
188
+ while (stmt.step()) {
189
+ const row = stmt.getAsObject();
190
+ devices.push({
191
+ device_id: row.device_id,
192
+ fingerprint: row.fingerprint,
193
+ name: row.name,
194
+ approved_at: row.approved_at,
195
+ last_seen_at: row.last_seen_at,
196
+ approval_method: row.approval_method,
197
+ });
198
+ }
199
+ stmt.free();
200
+ return devices;
201
+ }
202
+ /**
203
+ * Remove a device by fingerprint.
204
+ *
205
+ * @param fingerprint - The device fingerprint
206
+ * @returns true if a device was removed, false if not found
207
+ */
208
+ removeDevice(fingerprint) {
209
+ const before = this.isDeviceKnown(fingerprint);
210
+ if (!before)
211
+ return false;
212
+ this.db.run('DELETE FROM known_devices WHERE fingerprint = ?', [fingerprint]);
213
+ this.save();
214
+ return true;
215
+ }
216
+ /**
217
+ * Rename a device by fingerprint.
218
+ *
219
+ * @param fingerprint - The device fingerprint
220
+ * @param newName - The new human-friendly name
221
+ * @returns true if the device was renamed, false if not found
222
+ */
223
+ renameDevice(fingerprint, newName) {
224
+ if (!this.isDeviceKnown(fingerprint))
225
+ return false;
226
+ this.db.run('UPDATE known_devices SET name = ? WHERE fingerprint = ?', [newName, fingerprint]);
227
+ this.save();
228
+ return true;
229
+ }
230
+ /**
231
+ * Find devices whose fingerprint starts with the given prefix.
232
+ *
233
+ * Works like git SHA prefix matching -- the user provides the first N
234
+ * characters of the fingerprint and we find matching devices.
235
+ *
236
+ * @param prefix - The fingerprint prefix (minimum 8 characters recommended)
237
+ * @returns Array of matching KnownDevice records
238
+ */
239
+ findByFingerprintPrefix(prefix) {
240
+ const devices = [];
241
+ const pattern = prefix + '%';
242
+ const stmt = this.db.prepare(`SELECT device_id, fingerprint, name, approved_at, last_seen_at, approval_method
243
+ FROM known_devices
244
+ WHERE fingerprint LIKE ?
245
+ ORDER BY approved_at ASC`);
246
+ stmt.bind([pattern]);
247
+ while (stmt.step()) {
248
+ const row = stmt.getAsObject();
249
+ devices.push({
250
+ device_id: row.device_id,
251
+ fingerprint: row.fingerprint,
252
+ name: row.name,
253
+ approved_at: row.approved_at,
254
+ last_seen_at: row.last_seen_at,
255
+ approval_method: row.approval_method,
256
+ });
257
+ }
258
+ stmt.free();
259
+ return devices;
260
+ }
261
+ /**
262
+ * Remove all devices except the one with the given fingerprint.
263
+ *
264
+ * Used as a nuclear option for security incidents -- removes all
265
+ * approved devices except the current one.
266
+ *
267
+ * @param keepFingerprint - The fingerprint to keep
268
+ * @returns The number of devices removed
269
+ */
270
+ removeAllExcept(keepFingerprint) {
271
+ const before = this.countDevices();
272
+ this.db.run('DELETE FROM known_devices WHERE fingerprint != ?', [keepFingerprint]);
273
+ this.save();
274
+ const after = this.countDevices();
275
+ return before - after;
276
+ }
277
+ /**
278
+ * Count total known devices.
279
+ *
280
+ * @returns The number of registered devices
281
+ */
282
+ countDevices() {
283
+ const stmt = this.db.prepare('SELECT COUNT(*) as cnt FROM known_devices');
284
+ let count = 0;
285
+ if (stmt.step()) {
286
+ const row = stmt.getAsObject();
287
+ count = row.cnt;
288
+ }
289
+ stmt.free();
290
+ return count;
291
+ }
292
+ /**
293
+ * Check if this is the very first device (no devices registered yet).
294
+ *
295
+ * Used during initial vault setup to auto-approve the first device
296
+ * without requiring an email code.
297
+ *
298
+ * @returns true if zero devices are registered
299
+ */
300
+ isFirstDevice() {
301
+ return this.countDevices() === 0;
302
+ }
303
+ /**
304
+ * Close the database connection.
305
+ */
306
+ close() {
307
+ this.db.close();
308
+ }
309
+ }
310
+ // ─── High-level convenience functions ───────────────────────────────
311
+ /**
312
+ * Check if the current device is known and auto-approve the first device.
313
+ *
314
+ * This is the main entry point for device checks:
315
+ * 1. If no devices exist yet (first setup), auto-approves current device
316
+ * 2. If devices exist, checks if current fingerprint is registered
317
+ *
318
+ * @param fingerprint - The current device's fingerprint
319
+ * @param deviceName - Human-friendly name for this device
320
+ * @param baseDir - Optional base directory for the devices database
321
+ * @returns Object with `known` boolean and `autoApproved` boolean
322
+ */
323
+ export async function checkDevice(fingerprint, deviceName, baseDir) {
324
+ const db = await DevicesDatabase.open(baseDir);
325
+ try {
326
+ // First device ever — auto-approve
327
+ if (db.isFirstDevice()) {
328
+ const device = db.registerDevice(fingerprint, deviceName, 'initial_setup');
329
+ return { known: true, autoApproved: true, device };
330
+ }
331
+ // Check if this device is already known
332
+ if (db.isDeviceKnown(fingerprint)) {
333
+ db.updateLastSeen(fingerprint);
334
+ const device = db.getDevice(fingerprint);
335
+ return { known: true, autoApproved: false, device };
336
+ }
337
+ // Unknown device — needs approval
338
+ return { known: false, autoApproved: false, device: null };
339
+ }
340
+ finally {
341
+ db.close();
342
+ }
343
+ }
344
+ //# sourceMappingURL=devices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devices.js","sourceRoot":"","sources":["../../src/signatures/devices.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Email configuration for unotoken device signatures.
3
+ *
4
+ * Stores the user's verified email address at ~/.yokotoken/email-config.json.
5
+ * This email is used to send device approval codes when a new/unknown device
6
+ * attempts to access the vault.
7
+ *
8
+ * The config file contains:
9
+ * - email: the verified email address
10
+ * - verified: boolean indicating verification status
11
+ * - verifiedAt: ISO 8601 timestamp of when verification completed
12
+ * - resendApiKey: optional custom Resend API key (overrides built-in)
13
+ *
14
+ * @module
15
+ */
16
+ export interface EmailConfig {
17
+ /** The user's email address */
18
+ email: string;
19
+ /** Whether the email has been verified via code entry */
20
+ verified: boolean;
21
+ /** ISO 8601 timestamp of when verification was completed */
22
+ verifiedAt: string;
23
+ /** Optional custom Resend API key (overrides the built-in key) */
24
+ resendApiKey?: string;
25
+ }
26
+ /**
27
+ * Get the path to the email config file.
28
+ *
29
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
30
+ * @returns Absolute path to email-config.json
31
+ */
32
+ export declare function getEmailConfigPath(baseDir?: string): string;
33
+ /**
34
+ * Validate an email address format.
35
+ *
36
+ * @param email - The email address to validate
37
+ * @returns true if the email has a valid format
38
+ */
39
+ export declare function isValidEmail(email: string): boolean;
40
+ /**
41
+ * Load the email configuration from disk.
42
+ *
43
+ * Returns null if no config file exists or if the file is corrupted.
44
+ *
45
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
46
+ * @returns The email config, or null if not configured
47
+ */
48
+ export declare function loadEmailConfig(baseDir?: string): EmailConfig | null;
49
+ /**
50
+ * Save the email configuration to disk.
51
+ *
52
+ * Creates the directory if it does not exist.
53
+ *
54
+ * @param config - The email config to save
55
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
56
+ */
57
+ export declare function saveEmailConfig(config: EmailConfig, baseDir?: string): void;
58
+ /**
59
+ * Clear (delete) the email configuration file.
60
+ *
61
+ * After clearing, device approval codes cannot be sent until a new
62
+ * email is configured and verified.
63
+ *
64
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
65
+ * @returns true if a config file was deleted, false if none existed
66
+ */
67
+ export declare function clearEmailConfig(baseDir?: string): boolean;
68
+ /**
69
+ * Check if a verified email is configured.
70
+ *
71
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
72
+ * @returns true if a verified email config exists
73
+ */
74
+ export declare function hasVerifiedEmail(baseDir?: string): boolean;
75
+ /**
76
+ * Get the configured email address (masked for display).
77
+ *
78
+ * Masks the local part: first 2 chars + asterisks + last char before @.
79
+ * Example: stefan@example.com -> st***n@example.com
80
+ *
81
+ * @param email - The email address to mask
82
+ * @returns The masked email string
83
+ */
84
+ export declare function maskEmail(email: string): string;
85
+ /**
86
+ * Format the email config for display.
87
+ *
88
+ * @param config - The email config to format (or null if not configured)
89
+ * @returns Human-readable string showing email config
90
+ */
91
+ export declare function formatEmailConfigForDisplay(config: EmailConfig | null): string;
92
+ /**
93
+ * Update the Resend API key in the email configuration.
94
+ *
95
+ * If no email config exists, creates a placeholder that requires verification.
96
+ *
97
+ * @param resendApiKey - The custom Resend API key to set
98
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
99
+ * @returns The updated config
100
+ */
101
+ export declare function setResendApiKey(resendApiKey: string, baseDir?: string): EmailConfig;
102
+ //# sourceMappingURL=email-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-config.d.ts","sourceRoot":"","sources":["../../src/signatures/email-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,MAAM,WAAW,WAAW;IAC1B,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,QAAQ,EAAE,OAAO,CAAC;IAClB,4DAA4D;IAC5D,UAAU,EAAE,MAAM,CAAC;IACnB,kEAAkE;IAClE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAcD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3D;AAID;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAInD;AAID;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAqBpE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAS3E;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAS1D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAG1D;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAW/C;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,MAAM,CAqB9E;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,WAAW,CAgBnF"}
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Email configuration for unotoken device signatures.
3
+ *
4
+ * Stores the user's verified email address at ~/.yokotoken/email-config.json.
5
+ * This email is used to send device approval codes when a new/unknown device
6
+ * attempts to access the vault.
7
+ *
8
+ * The config file contains:
9
+ * - email: the verified email address
10
+ * - verified: boolean indicating verification status
11
+ * - verifiedAt: ISO 8601 timestamp of when verification completed
12
+ * - resendApiKey: optional custom Resend API key (overrides built-in)
13
+ *
14
+ * @module
15
+ */
16
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync } from 'node:fs';
17
+ import path from 'node:path';
18
+ import { getDeviceDir } from '../oauth/device-secret.js';
19
+ // ─── Constants ──────────────────────────────────────────────────────
20
+ const EMAIL_CONFIG_FILENAME = 'email-config.json';
21
+ /**
22
+ * Basic email validation regex.
23
+ * Checks for: something@something.something
24
+ */
25
+ const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
26
+ // ─── Path Resolution ────────────────────────────────────────────────
27
+ /**
28
+ * Get the path to the email config file.
29
+ *
30
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
31
+ * @returns Absolute path to email-config.json
32
+ */
33
+ export function getEmailConfigPath(baseDir) {
34
+ return path.join(baseDir ?? getDeviceDir(), EMAIL_CONFIG_FILENAME);
35
+ }
36
+ // ─── Validation ─────────────────────────────────────────────────────
37
+ /**
38
+ * Validate an email address format.
39
+ *
40
+ * @param email - The email address to validate
41
+ * @returns true if the email has a valid format
42
+ */
43
+ export function isValidEmail(email) {
44
+ if (!email || typeof email !== 'string')
45
+ return false;
46
+ if (email.length > 254)
47
+ return false;
48
+ return EMAIL_REGEX.test(email);
49
+ }
50
+ // ─── Load / Save / Clear ────────────────────────────────────────────
51
+ /**
52
+ * Load the email configuration from disk.
53
+ *
54
+ * Returns null if no config file exists or if the file is corrupted.
55
+ *
56
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
57
+ * @returns The email config, or null if not configured
58
+ */
59
+ export function loadEmailConfig(baseDir) {
60
+ const configPath = getEmailConfigPath(baseDir);
61
+ if (!existsSync(configPath)) {
62
+ return null;
63
+ }
64
+ try {
65
+ const raw = readFileSync(configPath, 'utf-8');
66
+ const parsed = JSON.parse(raw);
67
+ // Validate essential fields
68
+ if (!parsed.email || typeof parsed.verified !== 'boolean') {
69
+ return null;
70
+ }
71
+ return parsed;
72
+ }
73
+ catch {
74
+ // Corrupted config -- treat as not configured
75
+ return null;
76
+ }
77
+ }
78
+ /**
79
+ * Save the email configuration to disk.
80
+ *
81
+ * Creates the directory if it does not exist.
82
+ *
83
+ * @param config - The email config to save
84
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
85
+ */
86
+ export function saveEmailConfig(config, baseDir) {
87
+ const dir = baseDir ?? getDeviceDir();
88
+ const configPath = getEmailConfigPath(dir);
89
+ if (!existsSync(dir)) {
90
+ mkdirSync(dir, { recursive: true });
91
+ }
92
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
93
+ }
94
+ /**
95
+ * Clear (delete) the email configuration file.
96
+ *
97
+ * After clearing, device approval codes cannot be sent until a new
98
+ * email is configured and verified.
99
+ *
100
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
101
+ * @returns true if a config file was deleted, false if none existed
102
+ */
103
+ export function clearEmailConfig(baseDir) {
104
+ const configPath = getEmailConfigPath(baseDir);
105
+ if (!existsSync(configPath)) {
106
+ return false;
107
+ }
108
+ unlinkSync(configPath);
109
+ return true;
110
+ }
111
+ /**
112
+ * Check if a verified email is configured.
113
+ *
114
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
115
+ * @returns true if a verified email config exists
116
+ */
117
+ export function hasVerifiedEmail(baseDir) {
118
+ const config = loadEmailConfig(baseDir);
119
+ return config !== null && config.verified === true;
120
+ }
121
+ /**
122
+ * Get the configured email address (masked for display).
123
+ *
124
+ * Masks the local part: first 2 chars + asterisks + last char before @.
125
+ * Example: stefan@example.com -> st***n@example.com
126
+ *
127
+ * @param email - The email address to mask
128
+ * @returns The masked email string
129
+ */
130
+ export function maskEmail(email) {
131
+ const [local, domain] = email.split('@');
132
+ if (!local || !domain)
133
+ return '***@***';
134
+ if (local.length <= 2) {
135
+ return `${local[0]}***@${domain}`;
136
+ }
137
+ const first = local.slice(0, 2);
138
+ const last = local.slice(-1);
139
+ return `${first}${'*'.repeat(Math.max(3, local.length - 3))}${last}@${domain}`;
140
+ }
141
+ /**
142
+ * Format the email config for display.
143
+ *
144
+ * @param config - The email config to format (or null if not configured)
145
+ * @returns Human-readable string showing email config
146
+ */
147
+ export function formatEmailConfigForDisplay(config) {
148
+ if (!config) {
149
+ return ' No email configured.\n Set one with: unotoken config email <address>';
150
+ }
151
+ const lines = [];
152
+ lines.push(` Email: ${maskEmail(config.email)}`);
153
+ lines.push(` Verified: ${config.verified ? 'Yes' : 'No'}`);
154
+ if (config.verifiedAt) {
155
+ try {
156
+ const d = new Date(config.verifiedAt);
157
+ lines.push(` Verified at: ${d.toLocaleString()}`);
158
+ }
159
+ catch {
160
+ lines.push(` Verified at: ${config.verifiedAt}`);
161
+ }
162
+ }
163
+ lines.push(` Resend key: ${config.resendApiKey ? 'Custom (user-provided)' : 'Built-in (Indigo-hosted)'}`);
164
+ return lines.join('\n');
165
+ }
166
+ /**
167
+ * Update the Resend API key in the email configuration.
168
+ *
169
+ * If no email config exists, creates a placeholder that requires verification.
170
+ *
171
+ * @param resendApiKey - The custom Resend API key to set
172
+ * @param baseDir - Optional base directory (default: ~/.yokotoken)
173
+ * @returns The updated config
174
+ */
175
+ export function setResendApiKey(resendApiKey, baseDir) {
176
+ const existing = loadEmailConfig(baseDir);
177
+ if (existing) {
178
+ const updated = {
179
+ ...existing,
180
+ resendApiKey,
181
+ };
182
+ saveEmailConfig(updated, baseDir);
183
+ return updated;
184
+ }
185
+ // No config exists -- cannot just set a key without an email
186
+ throw new Error('No email configured. Set your email first with: unotoken config email <address>');
187
+ }
188
+ //# sourceMappingURL=email-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email-config.js","sourceRoot":"","sources":["../../src/signatures/email-config.ts"],"names":[],"mappings":"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"}