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.
- package/README.md +360 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1207 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +15 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +15 -0
- package/dist/client.js.map +1 -0
- package/dist/db.d.ts +52 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +97 -0
- package/dist/db.js.map +1 -0
- package/dist/dotenv.d.ts +69 -0
- package/dist/dotenv.d.ts.map +1 -0
- package/dist/dotenv.js +115 -0
- package/dist/dotenv.js.map +1 -0
- package/dist/env-mapper.d.ts +55 -0
- package/dist/env-mapper.d.ts.map +1 -0
- package/dist/env-mapper.js +97 -0
- package/dist/env-mapper.js.map +1 -0
- package/dist/exec.d.ts +80 -0
- package/dist/exec.d.ts.map +1 -0
- package/dist/exec.js +214 -0
- package/dist/exec.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/oauth/commands.d.ts +151 -0
- package/dist/oauth/commands.d.ts.map +1 -0
- package/dist/oauth/commands.js +322 -0
- package/dist/oauth/commands.js.map +1 -0
- package/dist/oauth/config.d.ts +84 -0
- package/dist/oauth/config.d.ts.map +1 -0
- package/dist/oauth/config.js +156 -0
- package/dist/oauth/config.js.map +1 -0
- package/dist/oauth/crypto-helpers.d.ts +44 -0
- package/dist/oauth/crypto-helpers.d.ts.map +1 -0
- package/dist/oauth/crypto-helpers.js +94 -0
- package/dist/oauth/crypto-helpers.js.map +1 -0
- package/dist/oauth/device-secret.d.ts +57 -0
- package/dist/oauth/device-secret.d.ts.map +1 -0
- package/dist/oauth/device-secret.js +106 -0
- package/dist/oauth/device-secret.js.map +1 -0
- package/dist/oauth/flow.d.ts +112 -0
- package/dist/oauth/flow.d.ts.map +1 -0
- package/dist/oauth/flow.js +255 -0
- package/dist/oauth/flow.js.map +1 -0
- package/dist/oauth/index.d.ts +18 -0
- package/dist/oauth/index.d.ts.map +1 -0
- package/dist/oauth/index.js +24 -0
- package/dist/oauth/index.js.map +1 -0
- package/dist/oauth/key-wrap.d.ts +146 -0
- package/dist/oauth/key-wrap.d.ts.map +1 -0
- package/dist/oauth/key-wrap.js +275 -0
- package/dist/oauth/key-wrap.js.map +1 -0
- package/dist/oauth/pkce.d.ts +29 -0
- package/dist/oauth/pkce.d.ts.map +1 -0
- package/dist/oauth/pkce.js +34 -0
- package/dist/oauth/pkce.js.map +1 -0
- package/dist/oauth/provider.d.ts +79 -0
- package/dist/oauth/provider.d.ts.map +1 -0
- package/dist/oauth/provider.js +10 -0
- package/dist/oauth/provider.js.map +1 -0
- package/dist/oauth/providers/github.d.ts +75 -0
- package/dist/oauth/providers/github.d.ts.map +1 -0
- package/dist/oauth/providers/github.js +119 -0
- package/dist/oauth/providers/github.js.map +1 -0
- package/dist/oauth/providers/google.d.ts +115 -0
- package/dist/oauth/providers/google.d.ts.map +1 -0
- package/dist/oauth/providers/google.js +285 -0
- package/dist/oauth/providers/google.js.map +1 -0
- package/dist/sdk.d.ts +8 -0
- package/dist/sdk.d.ts.map +1 -0
- package/dist/sdk.js +8 -0
- package/dist/sdk.js.map +1 -0
- package/dist/server.d.ts +33 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +287 -0
- package/dist/server.js.map +1 -0
- package/dist/signatures/approval-codes.d.ts +192 -0
- package/dist/signatures/approval-codes.d.ts.map +1 -0
- package/dist/signatures/approval-codes.js +407 -0
- package/dist/signatures/approval-codes.js.map +1 -0
- package/dist/signatures/commands.d.ts +108 -0
- package/dist/signatures/commands.d.ts.map +1 -0
- package/dist/signatures/commands.js +270 -0
- package/dist/signatures/commands.js.map +1 -0
- package/dist/signatures/devices.d.ts +165 -0
- package/dist/signatures/devices.d.ts.map +1 -0
- package/dist/signatures/devices.js +344 -0
- package/dist/signatures/devices.js.map +1 -0
- package/dist/signatures/email-config.d.ts +102 -0
- package/dist/signatures/email-config.d.ts.map +1 -0
- package/dist/signatures/email-config.js +188 -0
- package/dist/signatures/email-config.js.map +1 -0
- package/dist/signatures/email.d.ts +106 -0
- package/dist/signatures/email.d.ts.map +1 -0
- package/dist/signatures/email.js +180 -0
- package/dist/signatures/email.js.map +1 -0
- package/dist/signatures/fingerprint.d.ts +70 -0
- package/dist/signatures/fingerprint.d.ts.map +1 -0
- package/dist/signatures/fingerprint.js +123 -0
- package/dist/signatures/fingerprint.js.map +1 -0
- package/dist/signatures/guard.d.ts +118 -0
- package/dist/signatures/guard.d.ts.map +1 -0
- package/dist/signatures/guard.js +310 -0
- package/dist/signatures/guard.js.map +1 -0
- package/dist/signatures/resend.d.ts +84 -0
- package/dist/signatures/resend.d.ts.map +1 -0
- package/dist/signatures/resend.js +248 -0
- package/dist/signatures/resend.js.map +1 -0
- package/dist/token-requests.d.ts +80 -0
- package/dist/token-requests.d.ts.map +1 -0
- package/dist/token-requests.js +201 -0
- package/dist/token-requests.js.map +1 -0
- package/dist/tokens.d.ts +80 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +150 -0
- package/dist/tokens.js.map +1 -0
- 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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"}
|