@ursalock/server 0.2.0 → 0.3.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/dist/index.js +389 -107
- package/package.json +3 -1
- package/dist/chunk-BYTMLM3Q.js +0 -1048
- package/dist/server.js +0 -45
package/dist/chunk-BYTMLM3Q.js
DELETED
|
@@ -1,1048 +0,0 @@
|
|
|
1
|
-
// src/env.ts
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
var numeric = z.string().transform((val) => Number.parseInt(val, 10));
|
|
4
|
-
var envSchema = {
|
|
5
|
-
NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
|
|
6
|
-
PORT: numeric.default("3456"),
|
|
7
|
-
/** SQLite database file path */
|
|
8
|
-
DATABASE_PATH: z.string().default("./data/vault.db"),
|
|
9
|
-
/** JWT secret for signing tokens (32+ bytes recommended) */
|
|
10
|
-
JWT_SECRET: z.string().min(32),
|
|
11
|
-
/** JWT token expiry in seconds (default: 7 days) */
|
|
12
|
-
JWT_EXPIRY: numeric.default("604800"),
|
|
13
|
-
/** Relying Party ID for WebAuthn (your domain) */
|
|
14
|
-
RP_ID: z.string().default("localhost"),
|
|
15
|
-
/** Relying Party name shown during passkey registration */
|
|
16
|
-
RP_NAME: z.string().default("zod-vault"),
|
|
17
|
-
/** Expected origin for WebAuthn (e.g., https://app.example.com) */
|
|
18
|
-
RP_ORIGIN: z.string().url().default("http://localhost:5173")
|
|
19
|
-
};
|
|
20
|
-
var skipEnvValidation = process.env["NODE_ENV"] === "test";
|
|
21
|
-
var env = (() => {
|
|
22
|
-
if (skipEnvValidation) {
|
|
23
|
-
return Object.fromEntries(
|
|
24
|
-
Object.entries(envSchema).map(([key, schema]) => {
|
|
25
|
-
const result = schema.safeParse(process.env[key]);
|
|
26
|
-
return [key, result.success ? result.data : void 0];
|
|
27
|
-
})
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
const parsed = z.object(envSchema).safeParse(process.env);
|
|
31
|
-
if (!parsed.success) {
|
|
32
|
-
const errors2 = parsed.error.errors.map((e) => ` ${e.path.join(".")}: ${e.message}`).join("\n");
|
|
33
|
-
throw new Error(`Invalid environment variables:
|
|
34
|
-
${errors2}`);
|
|
35
|
-
}
|
|
36
|
-
return parsed.data;
|
|
37
|
-
})();
|
|
38
|
-
|
|
39
|
-
// src/db/client.ts
|
|
40
|
-
import Database from "better-sqlite3";
|
|
41
|
-
|
|
42
|
-
// src/db/schema.ts
|
|
43
|
-
var CREATE_TABLES_SQL = `
|
|
44
|
-
-- Users table
|
|
45
|
-
CREATE TABLE IF NOT EXISTS users (
|
|
46
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
47
|
-
uid TEXT NOT NULL UNIQUE DEFAULT (lower(hex(randomblob(16)))),
|
|
48
|
-
email TEXT UNIQUE,
|
|
49
|
-
password_hash TEXT,
|
|
50
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
51
|
-
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
55
|
-
CREATE INDEX IF NOT EXISTS idx_users_uid ON users(uid);
|
|
56
|
-
|
|
57
|
-
-- Passkeys table (WebAuthn credentials)
|
|
58
|
-
CREATE TABLE IF NOT EXISTS passkeys (
|
|
59
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
60
|
-
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
61
|
-
credential_id TEXT NOT NULL UNIQUE,
|
|
62
|
-
public_key TEXT NOT NULL,
|
|
63
|
-
counter INTEGER NOT NULL DEFAULT 0,
|
|
64
|
-
device_type TEXT NOT NULL DEFAULT 'singleDevice',
|
|
65
|
-
backed_up INTEGER NOT NULL DEFAULT 0,
|
|
66
|
-
transports TEXT,
|
|
67
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
CREATE INDEX IF NOT EXISTS idx_passkeys_user_id ON passkeys(user_id);
|
|
71
|
-
CREATE INDEX IF NOT EXISTS idx_passkeys_credential_id ON passkeys(credential_id);
|
|
72
|
-
|
|
73
|
-
-- Sessions table
|
|
74
|
-
CREATE TABLE IF NOT EXISTS sessions (
|
|
75
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
76
|
-
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
77
|
-
token_hash TEXT NOT NULL UNIQUE,
|
|
78
|
-
expires_at INTEGER NOT NULL,
|
|
79
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_token_hash ON sessions(token_hash);
|
|
83
|
-
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
|
84
|
-
|
|
85
|
-
-- Vaults table (encrypted blobs)
|
|
86
|
-
CREATE TABLE IF NOT EXISTS vaults (
|
|
87
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
88
|
-
uid TEXT NOT NULL UNIQUE DEFAULT (lower(hex(randomblob(16)))),
|
|
89
|
-
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
90
|
-
name TEXT NOT NULL,
|
|
91
|
-
data TEXT NOT NULL,
|
|
92
|
-
salt TEXT NOT NULL,
|
|
93
|
-
version INTEGER NOT NULL DEFAULT 1,
|
|
94
|
-
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
95
|
-
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
96
|
-
);
|
|
97
|
-
|
|
98
|
-
CREATE INDEX IF NOT EXISTS idx_vaults_uid ON vaults(uid);
|
|
99
|
-
CREATE INDEX IF NOT EXISTS idx_vaults_user_id ON vaults(user_id);
|
|
100
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_vaults_user_name ON vaults(user_id, name);
|
|
101
|
-
`;
|
|
102
|
-
|
|
103
|
-
// src/db/client.ts
|
|
104
|
-
var _db = null;
|
|
105
|
-
function getDb() {
|
|
106
|
-
if (!_db) {
|
|
107
|
-
_db = new Database(env.DATABASE_PATH);
|
|
108
|
-
_db.pragma("journal_mode = WAL");
|
|
109
|
-
_db.pragma("foreign_keys = ON");
|
|
110
|
-
_db.exec(CREATE_TABLES_SQL);
|
|
111
|
-
}
|
|
112
|
-
return _db;
|
|
113
|
-
}
|
|
114
|
-
function closeDb() {
|
|
115
|
-
if (_db) {
|
|
116
|
-
_db.close();
|
|
117
|
-
_db = null;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
function createUser(input) {
|
|
121
|
-
const db = getDb();
|
|
122
|
-
const stmt = db.prepare(`
|
|
123
|
-
INSERT INTO users (email, password_hash)
|
|
124
|
-
VALUES (?, ?)
|
|
125
|
-
RETURNING id, uid, email, password_hash as passwordHash, created_at as createdAt, updated_at as updatedAt
|
|
126
|
-
`);
|
|
127
|
-
return stmt.get(input.email ?? null, input.passwordHash ?? null);
|
|
128
|
-
}
|
|
129
|
-
function getUserByEmail(email) {
|
|
130
|
-
const db = getDb();
|
|
131
|
-
const stmt = db.prepare(`
|
|
132
|
-
SELECT id, uid, email, password_hash as passwordHash, created_at as createdAt, updated_at as updatedAt
|
|
133
|
-
FROM users WHERE email = ?
|
|
134
|
-
`);
|
|
135
|
-
return stmt.get(email);
|
|
136
|
-
}
|
|
137
|
-
function createPasskey(input) {
|
|
138
|
-
const db = getDb();
|
|
139
|
-
const stmt = db.prepare(`
|
|
140
|
-
INSERT INTO passkeys (user_id, credential_id, public_key, counter, device_type, backed_up, transports)
|
|
141
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
142
|
-
RETURNING id, user_id as userId, credential_id as credentialId, public_key as publicKey,
|
|
143
|
-
counter, device_type as deviceType, backed_up as backedUp, transports, created_at as createdAt
|
|
144
|
-
`);
|
|
145
|
-
const result = stmt.get(
|
|
146
|
-
input.userId,
|
|
147
|
-
input.credentialId,
|
|
148
|
-
input.publicKey,
|
|
149
|
-
input.counter,
|
|
150
|
-
input.deviceType,
|
|
151
|
-
input.backedUp ? 1 : 0,
|
|
152
|
-
input.transports?.join(",") ?? null
|
|
153
|
-
);
|
|
154
|
-
return { ...result, backedUp: Boolean(result.backedUp) };
|
|
155
|
-
}
|
|
156
|
-
function getPasskeyByCredentialId(credentialId) {
|
|
157
|
-
const db = getDb();
|
|
158
|
-
const stmt = db.prepare(`
|
|
159
|
-
SELECT
|
|
160
|
-
p.id, p.user_id as userId, p.credential_id as credentialId, p.public_key as publicKey,
|
|
161
|
-
p.counter, p.device_type as deviceType, p.backed_up as backedUp, p.transports, p.created_at as createdAt,
|
|
162
|
-
u.id as "user.id", u.uid as "user.uid", u.email as "user.email"
|
|
163
|
-
FROM passkeys p
|
|
164
|
-
JOIN users u ON p.user_id = u.id
|
|
165
|
-
WHERE p.credential_id = ?
|
|
166
|
-
`);
|
|
167
|
-
const row = stmt.get(credentialId);
|
|
168
|
-
if (!row) return void 0;
|
|
169
|
-
return {
|
|
170
|
-
id: row["id"],
|
|
171
|
-
userId: row["userId"],
|
|
172
|
-
credentialId: row["credentialId"],
|
|
173
|
-
publicKey: row["publicKey"],
|
|
174
|
-
counter: row["counter"],
|
|
175
|
-
deviceType: row["deviceType"],
|
|
176
|
-
backedUp: Boolean(row["backedUp"]),
|
|
177
|
-
transports: row["transports"],
|
|
178
|
-
createdAt: row["createdAt"],
|
|
179
|
-
user: {
|
|
180
|
-
id: row["user.id"],
|
|
181
|
-
uid: row["user.uid"],
|
|
182
|
-
email: row["user.email"],
|
|
183
|
-
passwordHash: null,
|
|
184
|
-
createdAt: 0,
|
|
185
|
-
updatedAt: 0
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
function getPasskeysByUserId(userId) {
|
|
190
|
-
const db = getDb();
|
|
191
|
-
const stmt = db.prepare(`
|
|
192
|
-
SELECT id, user_id as userId, credential_id as credentialId, public_key as publicKey,
|
|
193
|
-
counter, device_type as deviceType, backed_up as backedUp, transports, created_at as createdAt
|
|
194
|
-
FROM passkeys WHERE user_id = ?
|
|
195
|
-
`);
|
|
196
|
-
return stmt.all(userId).map((p) => ({ ...p, backedUp: Boolean(p.backedUp) }));
|
|
197
|
-
}
|
|
198
|
-
function updatePasskeyCounter(credentialId, counter) {
|
|
199
|
-
const db = getDb();
|
|
200
|
-
const stmt = db.prepare(`UPDATE passkeys SET counter = ? WHERE credential_id = ?`);
|
|
201
|
-
stmt.run(counter, credentialId);
|
|
202
|
-
}
|
|
203
|
-
function createSession(input) {
|
|
204
|
-
const db = getDb();
|
|
205
|
-
const stmt = db.prepare(`
|
|
206
|
-
INSERT INTO sessions (user_id, token_hash, expires_at)
|
|
207
|
-
VALUES (?, ?, ?)
|
|
208
|
-
RETURNING id, user_id as userId, token_hash as tokenHash, expires_at as expiresAt, created_at as createdAt
|
|
209
|
-
`);
|
|
210
|
-
return stmt.get(input.userId, input.tokenHash, input.expiresAt);
|
|
211
|
-
}
|
|
212
|
-
function getSessionByTokenHash(tokenHash) {
|
|
213
|
-
const db = getDb();
|
|
214
|
-
const stmt = db.prepare(`
|
|
215
|
-
SELECT
|
|
216
|
-
s.id, s.user_id as userId, s.token_hash as tokenHash, s.expires_at as expiresAt, s.created_at as createdAt,
|
|
217
|
-
u.id as "user.id", u.uid as "user.uid", u.email as "user.email"
|
|
218
|
-
FROM sessions s
|
|
219
|
-
JOIN users u ON s.user_id = u.id
|
|
220
|
-
WHERE s.token_hash = ? AND s.expires_at > unixepoch()
|
|
221
|
-
`);
|
|
222
|
-
const row = stmt.get(tokenHash);
|
|
223
|
-
if (!row) return void 0;
|
|
224
|
-
return {
|
|
225
|
-
id: row["id"],
|
|
226
|
-
userId: row["userId"],
|
|
227
|
-
tokenHash: row["tokenHash"],
|
|
228
|
-
expiresAt: row["expiresAt"],
|
|
229
|
-
createdAt: row["createdAt"],
|
|
230
|
-
user: {
|
|
231
|
-
id: row["user.id"],
|
|
232
|
-
uid: row["user.uid"],
|
|
233
|
-
email: row["user.email"],
|
|
234
|
-
passwordHash: null,
|
|
235
|
-
createdAt: 0,
|
|
236
|
-
updatedAt: 0
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
function deleteSession(tokenHash) {
|
|
241
|
-
const db = getDb();
|
|
242
|
-
const stmt = db.prepare(`DELETE FROM sessions WHERE token_hash = ?`);
|
|
243
|
-
stmt.run(tokenHash);
|
|
244
|
-
}
|
|
245
|
-
function createVault(input) {
|
|
246
|
-
const db = getDb();
|
|
247
|
-
const stmt = db.prepare(`
|
|
248
|
-
INSERT INTO vaults (user_id, name, data, salt, version)
|
|
249
|
-
VALUES (?, ?, ?, ?, ?)
|
|
250
|
-
RETURNING id, uid, user_id as userId, name, data, salt, version, created_at as createdAt, updated_at as updatedAt
|
|
251
|
-
`);
|
|
252
|
-
return stmt.get(input.userId, input.name, input.data, input.salt, input.version ?? 1);
|
|
253
|
-
}
|
|
254
|
-
function getVaultByUid(uid, userId) {
|
|
255
|
-
const db = getDb();
|
|
256
|
-
const stmt = db.prepare(`
|
|
257
|
-
SELECT id, uid, user_id as userId, name, data, salt, version, created_at as createdAt, updated_at as updatedAt
|
|
258
|
-
FROM vaults WHERE uid = ? AND user_id = ?
|
|
259
|
-
`);
|
|
260
|
-
return stmt.get(uid, userId);
|
|
261
|
-
}
|
|
262
|
-
function getVaultByName(name, userId) {
|
|
263
|
-
const db = getDb();
|
|
264
|
-
const stmt = db.prepare(`
|
|
265
|
-
SELECT id, uid, user_id as userId, name, data, salt, version, created_at as createdAt, updated_at as updatedAt
|
|
266
|
-
FROM vaults WHERE name = ? AND user_id = ?
|
|
267
|
-
`);
|
|
268
|
-
return stmt.get(name, userId);
|
|
269
|
-
}
|
|
270
|
-
function getVaultsByUserId(userId) {
|
|
271
|
-
const db = getDb();
|
|
272
|
-
const stmt = db.prepare(`
|
|
273
|
-
SELECT id, uid, user_id as userId, name, data, salt, version, created_at as createdAt, updated_at as updatedAt
|
|
274
|
-
FROM vaults WHERE user_id = ?
|
|
275
|
-
ORDER BY updated_at DESC
|
|
276
|
-
`);
|
|
277
|
-
return stmt.all(userId);
|
|
278
|
-
}
|
|
279
|
-
function updateVault(uid, userId, input) {
|
|
280
|
-
const db = getDb();
|
|
281
|
-
const stmt = db.prepare(`
|
|
282
|
-
UPDATE vaults SET data = ?, salt = ?, version = COALESCE(?, version), updated_at = unixepoch()
|
|
283
|
-
WHERE uid = ? AND user_id = ?
|
|
284
|
-
RETURNING id, uid, user_id as userId, name, data, salt, version, created_at as createdAt, updated_at as updatedAt
|
|
285
|
-
`);
|
|
286
|
-
return stmt.get(input.data, input.salt, input.version ?? null, uid, userId);
|
|
287
|
-
}
|
|
288
|
-
function deleteVault(uid, userId) {
|
|
289
|
-
const db = getDb();
|
|
290
|
-
const stmt = db.prepare(`DELETE FROM vaults WHERE uid = ? AND user_id = ?`);
|
|
291
|
-
return stmt.run(uid, userId).changes > 0;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// src/features/auth/jwt.ts
|
|
295
|
-
import { createHash } from "crypto";
|
|
296
|
-
import { SignJWT, jwtVerify } from "jose";
|
|
297
|
-
function getSecretKey() {
|
|
298
|
-
return new TextEncoder().encode(env.JWT_SECRET);
|
|
299
|
-
}
|
|
300
|
-
async function createToken(payload) {
|
|
301
|
-
const jti = crypto.randomUUID();
|
|
302
|
-
const jwt = new SignJWT({ email: payload.email }).setSubject(payload.userId).setJti(jti).setIssuedAt().setExpirationTime(`${env.JWT_EXPIRY}s`).setProtectedHeader({ alg: "HS256" });
|
|
303
|
-
return jwt.sign(getSecretKey());
|
|
304
|
-
}
|
|
305
|
-
async function verifyToken(token) {
|
|
306
|
-
try {
|
|
307
|
-
const { payload } = await jwtVerify(token, getSecretKey());
|
|
308
|
-
if (!payload.sub) {
|
|
309
|
-
return null;
|
|
310
|
-
}
|
|
311
|
-
return payload;
|
|
312
|
-
} catch {
|
|
313
|
-
return null;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
function hashToken(token) {
|
|
317
|
-
return createHash("sha256").update(token).digest("hex");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// src/errors.ts
|
|
321
|
-
import { z as z2 } from "zod";
|
|
322
|
-
var ErrorCode = z2.enum([
|
|
323
|
-
// Auth errors
|
|
324
|
-
"unauthorized",
|
|
325
|
-
"invalid_credentials",
|
|
326
|
-
"email_already_exists",
|
|
327
|
-
"passkey_not_found",
|
|
328
|
-
"session_expired",
|
|
329
|
-
// Vault errors
|
|
330
|
-
"vault_not_found",
|
|
331
|
-
"vault_already_exists",
|
|
332
|
-
"invalid_vault_data",
|
|
333
|
-
// Validation errors
|
|
334
|
-
"validation_error",
|
|
335
|
-
"invalid_request",
|
|
336
|
-
// Server errors
|
|
337
|
-
"internal_error"
|
|
338
|
-
]);
|
|
339
|
-
var ApiException = class extends Error {
|
|
340
|
-
constructor(error, status = 400) {
|
|
341
|
-
super(error.message);
|
|
342
|
-
this.error = error;
|
|
343
|
-
this.status = status;
|
|
344
|
-
this.name = "ApiException";
|
|
345
|
-
}
|
|
346
|
-
};
|
|
347
|
-
var errors = {
|
|
348
|
-
// Auth errors
|
|
349
|
-
unauthorized: { code: "unauthorized", message: "Unauthorized" },
|
|
350
|
-
invalid_credentials: { code: "invalid_credentials", message: "Invalid email or password" },
|
|
351
|
-
email_already_exists: { code: "email_already_exists", message: "Email already registered" },
|
|
352
|
-
passkey_not_found: { code: "passkey_not_found", message: "Passkey not found" },
|
|
353
|
-
session_expired: { code: "session_expired", message: "Session expired" },
|
|
354
|
-
// Vault errors
|
|
355
|
-
vault_not_found: { code: "vault_not_found", message: "Vault not found" },
|
|
356
|
-
vault_already_exists: (name) => ({
|
|
357
|
-
code: "vault_already_exists",
|
|
358
|
-
message: `Vault "${name}" already exists`
|
|
359
|
-
}),
|
|
360
|
-
invalid_vault_data: { code: "invalid_vault_data", message: "Invalid vault data" },
|
|
361
|
-
// Validation errors
|
|
362
|
-
validation_error: (details) => ({
|
|
363
|
-
code: "validation_error",
|
|
364
|
-
message: details
|
|
365
|
-
}),
|
|
366
|
-
invalid_request: { code: "invalid_request", message: "Invalid request" },
|
|
367
|
-
// Server errors
|
|
368
|
-
internal_error: { code: "internal_error", message: "Internal server error" }
|
|
369
|
-
};
|
|
370
|
-
function getError(code, arg) {
|
|
371
|
-
const factory = errors[code];
|
|
372
|
-
if (typeof factory === "function") {
|
|
373
|
-
return factory(arg ?? "");
|
|
374
|
-
}
|
|
375
|
-
return factory;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// src/features/auth/middleware.ts
|
|
379
|
-
import { createMiddleware } from "hono/factory";
|
|
380
|
-
var optionalAuthMiddleware = createMiddleware(async (c, next) => {
|
|
381
|
-
const authHeader = c.req.header("Authorization");
|
|
382
|
-
if (!authHeader?.startsWith("Bearer ")) {
|
|
383
|
-
c.set("session", null);
|
|
384
|
-
return next();
|
|
385
|
-
}
|
|
386
|
-
const token = authHeader.slice(7);
|
|
387
|
-
const payload = await verifyToken(token);
|
|
388
|
-
if (!payload?.sub) {
|
|
389
|
-
c.set("session", null);
|
|
390
|
-
return next();
|
|
391
|
-
}
|
|
392
|
-
const tokenHash = hashToken(token);
|
|
393
|
-
const session = getSessionByTokenHash(tokenHash);
|
|
394
|
-
if (!session) {
|
|
395
|
-
c.set("session", null);
|
|
396
|
-
return next();
|
|
397
|
-
}
|
|
398
|
-
c.set("session", {
|
|
399
|
-
user: {
|
|
400
|
-
id: session.user.id,
|
|
401
|
-
uid: session.user.uid,
|
|
402
|
-
email: session.user.email
|
|
403
|
-
},
|
|
404
|
-
sessionId: session.id
|
|
405
|
-
});
|
|
406
|
-
return next();
|
|
407
|
-
});
|
|
408
|
-
var requireAuthMiddleware = createMiddleware(async (c, next) => {
|
|
409
|
-
const authHeader = c.req.header("Authorization");
|
|
410
|
-
if (!authHeader?.startsWith("Bearer ")) {
|
|
411
|
-
throw new ApiException(getError("unauthorized"), 401);
|
|
412
|
-
}
|
|
413
|
-
const token = authHeader.slice(7);
|
|
414
|
-
const payload = await verifyToken(token);
|
|
415
|
-
if (!payload?.sub) {
|
|
416
|
-
throw new ApiException(getError("unauthorized"), 401);
|
|
417
|
-
}
|
|
418
|
-
const tokenHash = hashToken(token);
|
|
419
|
-
const session = getSessionByTokenHash(tokenHash);
|
|
420
|
-
if (!session) {
|
|
421
|
-
throw new ApiException(getError("session_expired"), 401);
|
|
422
|
-
}
|
|
423
|
-
c.set("session", {
|
|
424
|
-
user: {
|
|
425
|
-
id: session.user.id,
|
|
426
|
-
uid: session.user.uid,
|
|
427
|
-
email: session.user.email
|
|
428
|
-
},
|
|
429
|
-
sessionId: session.id
|
|
430
|
-
});
|
|
431
|
-
return next();
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
// src/api/schemas.ts
|
|
435
|
-
import { z as z3 } from "zod";
|
|
436
|
-
var UserResponse = z3.object({
|
|
437
|
-
id: z3.string(),
|
|
438
|
-
email: z3.string().email().nullable(),
|
|
439
|
-
createdAt: z3.number()
|
|
440
|
-
});
|
|
441
|
-
var EmailRegisterRequest = z3.object({
|
|
442
|
-
email: z3.string().email(),
|
|
443
|
-
password: z3.string().min(8, "Password must be at least 8 characters")
|
|
444
|
-
});
|
|
445
|
-
var EmailLoginRequest = z3.object({
|
|
446
|
-
email: z3.string().email(),
|
|
447
|
-
password: z3.string().min(1)
|
|
448
|
-
});
|
|
449
|
-
var AuthResponse = z3.object({
|
|
450
|
-
user: UserResponse,
|
|
451
|
-
token: z3.string(),
|
|
452
|
-
recoveryKey: z3.string().optional()
|
|
453
|
-
});
|
|
454
|
-
var RefreshResponse = z3.object({
|
|
455
|
-
token: z3.string(),
|
|
456
|
-
expiresIn: z3.number()
|
|
457
|
-
});
|
|
458
|
-
var MeResponse = z3.object({
|
|
459
|
-
user: UserResponse
|
|
460
|
-
});
|
|
461
|
-
var PasskeyRegisterOptionsRequest = z3.object({
|
|
462
|
-
email: z3.string().email().optional()
|
|
463
|
-
});
|
|
464
|
-
var PasskeyRegisterVerifyRequest = z3.object({
|
|
465
|
-
email: z3.string().email().optional(),
|
|
466
|
-
credential: z3.record(z3.unknown())
|
|
467
|
-
});
|
|
468
|
-
var PasskeyLoginOptionsRequest = z3.object({
|
|
469
|
-
email: z3.string().email().optional()
|
|
470
|
-
});
|
|
471
|
-
var PasskeyLoginVerifyRequest = z3.object({
|
|
472
|
-
credential: z3.record(z3.unknown())
|
|
473
|
-
});
|
|
474
|
-
var PasskeyCheckResponse = z3.object({
|
|
475
|
-
hasPasskey: z3.boolean()
|
|
476
|
-
});
|
|
477
|
-
var CreateVaultRequest = z3.object({
|
|
478
|
-
name: z3.string().min(1).max(255),
|
|
479
|
-
data: z3.string(),
|
|
480
|
-
// Encrypted blob (base64)
|
|
481
|
-
salt: z3.string()
|
|
482
|
-
// Salt (base64)
|
|
483
|
-
});
|
|
484
|
-
var UpdateVaultRequest = z3.object({
|
|
485
|
-
data: z3.string(),
|
|
486
|
-
salt: z3.string(),
|
|
487
|
-
version: z3.number().optional()
|
|
488
|
-
});
|
|
489
|
-
var VaultResponse = z3.object({
|
|
490
|
-
uid: z3.string(),
|
|
491
|
-
name: z3.string(),
|
|
492
|
-
data: z3.string(),
|
|
493
|
-
salt: z3.string(),
|
|
494
|
-
version: z3.number(),
|
|
495
|
-
updatedAt: z3.number()
|
|
496
|
-
});
|
|
497
|
-
var VaultsListResponse = z3.object({
|
|
498
|
-
vaults: z3.array(VaultResponse)
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
// src/app.ts
|
|
502
|
-
import { Hono as Hono4 } from "hono";
|
|
503
|
-
import { cors } from "hono/cors";
|
|
504
|
-
import { logger } from "hono/logger";
|
|
505
|
-
import { ZodError } from "zod";
|
|
506
|
-
|
|
507
|
-
// src/api/auth/router.ts
|
|
508
|
-
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
509
|
-
import { Hono as Hono2 } from "hono";
|
|
510
|
-
import { zValidator as zValidator2 } from "@hono/zod-validator";
|
|
511
|
-
|
|
512
|
-
// src/api/auth/passkey.ts
|
|
513
|
-
import { Hono } from "hono";
|
|
514
|
-
import { zValidator } from "@hono/zod-validator";
|
|
515
|
-
import { z as z4 } from "zod";
|
|
516
|
-
import {
|
|
517
|
-
generateRegistrationOptions,
|
|
518
|
-
verifyRegistrationResponse,
|
|
519
|
-
generateAuthenticationOptions,
|
|
520
|
-
verifyAuthenticationResponse
|
|
521
|
-
} from "@simplewebauthn/server";
|
|
522
|
-
|
|
523
|
-
// src/features/crypto/recovery.ts
|
|
524
|
-
import { randomBytes } from "crypto";
|
|
525
|
-
var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
526
|
-
function generateRecoveryKey() {
|
|
527
|
-
const bytes = randomBytes(32);
|
|
528
|
-
const raw = base32Encode(bytes);
|
|
529
|
-
return formatWithDashes(raw);
|
|
530
|
-
}
|
|
531
|
-
function base32Encode(bytes) {
|
|
532
|
-
let result = "";
|
|
533
|
-
let bits = 0;
|
|
534
|
-
let value = 0;
|
|
535
|
-
for (const byte of bytes) {
|
|
536
|
-
value = value << 8 | byte;
|
|
537
|
-
bits += 8;
|
|
538
|
-
while (bits >= 5) {
|
|
539
|
-
bits -= 5;
|
|
540
|
-
result += BASE32_ALPHABET[value >> bits & 31];
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
if (bits > 0) {
|
|
544
|
-
result += BASE32_ALPHABET[value << 5 - bits & 31];
|
|
545
|
-
}
|
|
546
|
-
return result;
|
|
547
|
-
}
|
|
548
|
-
function formatWithDashes(raw) {
|
|
549
|
-
const chunks = [];
|
|
550
|
-
for (let i = 0; i < raw.length; i += 4) {
|
|
551
|
-
chunks.push(raw.slice(i, i + 4));
|
|
552
|
-
}
|
|
553
|
-
return chunks.join("-");
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// src/api/auth/passkey.ts
|
|
557
|
-
var challengeStore = /* @__PURE__ */ new Map();
|
|
558
|
-
setInterval(() => {
|
|
559
|
-
const now = Date.now();
|
|
560
|
-
for (const [key, value] of challengeStore) {
|
|
561
|
-
if (value.expiresAt < now) {
|
|
562
|
-
challengeStore.delete(key);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
}, 6e4);
|
|
566
|
-
var PasskeyRegisterOptionsRequest2 = z4.object({
|
|
567
|
-
email: z4.string().email().optional()
|
|
568
|
-
});
|
|
569
|
-
var PasskeyRegisterVerifyRequest2 = z4.object({
|
|
570
|
-
email: z4.string().email().optional(),
|
|
571
|
-
credential: z4.any()
|
|
572
|
-
// RegistrationResponseJSON
|
|
573
|
-
});
|
|
574
|
-
var PasskeyLoginOptionsRequest2 = z4.object({
|
|
575
|
-
email: z4.string().email().optional()
|
|
576
|
-
});
|
|
577
|
-
var PasskeyLoginVerifyRequest2 = z4.object({
|
|
578
|
-
credential: z4.any()
|
|
579
|
-
// AuthenticationResponseJSON
|
|
580
|
-
});
|
|
581
|
-
var PasskeyCheckRequest = z4.object({
|
|
582
|
-
email: z4.string().email()
|
|
583
|
-
});
|
|
584
|
-
var passkeyRouter = new Hono().post(
|
|
585
|
-
"/register/options",
|
|
586
|
-
zValidator("json", PasskeyRegisterOptionsRequest2),
|
|
587
|
-
async (c) => {
|
|
588
|
-
const { email } = c.req.valid("json");
|
|
589
|
-
if (email) {
|
|
590
|
-
const existing = getUserByEmail(email);
|
|
591
|
-
if (existing) {
|
|
592
|
-
throw new ApiException(errors.email_already_exists, 409);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
const tempUserId = crypto.randomUUID();
|
|
596
|
-
const options = await generateRegistrationOptions({
|
|
597
|
-
rpName: env.RP_NAME,
|
|
598
|
-
rpID: env.RP_ID,
|
|
599
|
-
userName: email ?? tempUserId,
|
|
600
|
-
userDisplayName: email ?? "Anonymous User",
|
|
601
|
-
attestationType: "none",
|
|
602
|
-
authenticatorSelection: {
|
|
603
|
-
residentKey: "preferred",
|
|
604
|
-
userVerification: "preferred",
|
|
605
|
-
authenticatorAttachment: "platform"
|
|
606
|
-
},
|
|
607
|
-
timeout: 6e4
|
|
608
|
-
});
|
|
609
|
-
const challengeKey = email ?? tempUserId;
|
|
610
|
-
challengeStore.set(challengeKey, {
|
|
611
|
-
challenge: options.challenge,
|
|
612
|
-
expiresAt: Date.now() + 12e4
|
|
613
|
-
// 2 min expiry
|
|
614
|
-
});
|
|
615
|
-
return c.json(options);
|
|
616
|
-
}
|
|
617
|
-
).post(
|
|
618
|
-
"/register/verify",
|
|
619
|
-
zValidator("json", PasskeyRegisterVerifyRequest2),
|
|
620
|
-
async (c) => {
|
|
621
|
-
const { email, credential } = c.req.valid("json");
|
|
622
|
-
const response = credential;
|
|
623
|
-
const challengeKey = email ?? response.id;
|
|
624
|
-
const stored = challengeStore.get(challengeKey);
|
|
625
|
-
if (!stored) {
|
|
626
|
-
throw new ApiException(errors.invalid_credentials, 401);
|
|
627
|
-
}
|
|
628
|
-
if (email) {
|
|
629
|
-
const existing = getUserByEmail(email);
|
|
630
|
-
if (existing) {
|
|
631
|
-
throw new ApiException(errors.email_already_exists, 409);
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
try {
|
|
635
|
-
const verification = await verifyRegistrationResponse({
|
|
636
|
-
response,
|
|
637
|
-
expectedChallenge: stored.challenge,
|
|
638
|
-
expectedOrigin: env.RP_ORIGIN,
|
|
639
|
-
expectedRPID: env.RP_ID
|
|
640
|
-
});
|
|
641
|
-
if (!verification.verified || !verification.registrationInfo) {
|
|
642
|
-
throw new ApiException(errors.invalid_credentials, 401);
|
|
643
|
-
}
|
|
644
|
-
const { credentialID, credentialPublicKey, counter, credentialDeviceType, credentialBackedUp } = verification.registrationInfo;
|
|
645
|
-
const user = createUser({ email: email ?? void 0, passwordHash: void 0 });
|
|
646
|
-
createPasskey({
|
|
647
|
-
userId: user.id,
|
|
648
|
-
credentialId: credentialID,
|
|
649
|
-
publicKey: Buffer.from(credentialPublicKey).toString("base64"),
|
|
650
|
-
counter,
|
|
651
|
-
deviceType: credentialDeviceType,
|
|
652
|
-
backedUp: credentialBackedUp,
|
|
653
|
-
transports: response.response.transports
|
|
654
|
-
});
|
|
655
|
-
const recoveryKey = generateRecoveryKey();
|
|
656
|
-
const token = await createToken({ userId: user.uid, email: email ?? void 0 });
|
|
657
|
-
const tokenHash = hashToken(token);
|
|
658
|
-
const expiresAt = Math.floor(Date.now() / 1e3) + env.JWT_EXPIRY;
|
|
659
|
-
createSession({ userId: user.id, tokenHash, expiresAt });
|
|
660
|
-
challengeStore.delete(challengeKey);
|
|
661
|
-
return c.json({
|
|
662
|
-
user: {
|
|
663
|
-
id: user.uid,
|
|
664
|
-
email: user.email,
|
|
665
|
-
createdAt: user.createdAt
|
|
666
|
-
},
|
|
667
|
-
token,
|
|
668
|
-
recoveryKey
|
|
669
|
-
});
|
|
670
|
-
} catch (error) {
|
|
671
|
-
if (error instanceof ApiException) throw error;
|
|
672
|
-
console.error("Passkey registration error:", error);
|
|
673
|
-
throw new ApiException(errors.invalid_credentials, 401);
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
).post(
|
|
677
|
-
"/login/options",
|
|
678
|
-
zValidator("json", PasskeyLoginOptionsRequest2),
|
|
679
|
-
async (c) => {
|
|
680
|
-
const { email } = c.req.valid("json");
|
|
681
|
-
let allowCredentials;
|
|
682
|
-
if (email) {
|
|
683
|
-
const user = getUserByEmail(email);
|
|
684
|
-
if (user) {
|
|
685
|
-
const passkeys = getPasskeysByUserId(user.id);
|
|
686
|
-
allowCredentials = passkeys.map((p) => ({
|
|
687
|
-
id: p.credentialId,
|
|
688
|
-
transports: p.transports?.split(",")
|
|
689
|
-
}));
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
const options = await generateAuthenticationOptions({
|
|
693
|
-
rpID: env.RP_ID,
|
|
694
|
-
allowCredentials,
|
|
695
|
-
userVerification: "preferred",
|
|
696
|
-
timeout: 6e4
|
|
697
|
-
});
|
|
698
|
-
const challengeKey = `auth:${options.challenge}`;
|
|
699
|
-
challengeStore.set(challengeKey, {
|
|
700
|
-
challenge: options.challenge,
|
|
701
|
-
expiresAt: Date.now() + 12e4
|
|
702
|
-
});
|
|
703
|
-
return c.json(options);
|
|
704
|
-
}
|
|
705
|
-
).post(
|
|
706
|
-
"/login/verify",
|
|
707
|
-
zValidator("json", PasskeyLoginVerifyRequest2),
|
|
708
|
-
async (c) => {
|
|
709
|
-
const { credential } = c.req.valid("json");
|
|
710
|
-
const response = credential;
|
|
711
|
-
const passkey = getPasskeyByCredentialId(response.id);
|
|
712
|
-
if (!passkey) {
|
|
713
|
-
throw new ApiException(errors.passkey_not_found, 401);
|
|
714
|
-
}
|
|
715
|
-
let storedChallenge = null;
|
|
716
|
-
for (const [key, value] of challengeStore) {
|
|
717
|
-
if (key.startsWith("auth:") && value.expiresAt > Date.now()) {
|
|
718
|
-
storedChallenge = value.challenge;
|
|
719
|
-
break;
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
if (!storedChallenge) {
|
|
723
|
-
throw new ApiException(errors.invalid_credentials, 401);
|
|
724
|
-
}
|
|
725
|
-
try {
|
|
726
|
-
const verification = await verifyAuthenticationResponse({
|
|
727
|
-
response,
|
|
728
|
-
expectedChallenge: storedChallenge,
|
|
729
|
-
expectedOrigin: env.RP_ORIGIN,
|
|
730
|
-
expectedRPID: env.RP_ID,
|
|
731
|
-
authenticator: {
|
|
732
|
-
credentialID: passkey.credentialId,
|
|
733
|
-
credentialPublicKey: Buffer.from(passkey.publicKey, "base64"),
|
|
734
|
-
counter: passkey.counter,
|
|
735
|
-
transports: passkey.transports?.split(",")
|
|
736
|
-
}
|
|
737
|
-
});
|
|
738
|
-
if (!verification.verified) {
|
|
739
|
-
throw new ApiException(errors.invalid_credentials, 401);
|
|
740
|
-
}
|
|
741
|
-
updatePasskeyCounter(passkey.credentialId, verification.authenticationInfo.newCounter);
|
|
742
|
-
const token = await createToken({
|
|
743
|
-
userId: passkey.user.uid,
|
|
744
|
-
email: passkey.user.email ?? void 0
|
|
745
|
-
});
|
|
746
|
-
const tokenHash = hashToken(token);
|
|
747
|
-
const expiresAt = Math.floor(Date.now() / 1e3) + env.JWT_EXPIRY;
|
|
748
|
-
createSession({ userId: passkey.user.id, tokenHash, expiresAt });
|
|
749
|
-
challengeStore.delete(`auth:${storedChallenge}`);
|
|
750
|
-
return c.json({
|
|
751
|
-
user: {
|
|
752
|
-
id: passkey.user.uid,
|
|
753
|
-
email: passkey.user.email,
|
|
754
|
-
createdAt: passkey.user.createdAt
|
|
755
|
-
},
|
|
756
|
-
token
|
|
757
|
-
});
|
|
758
|
-
} catch (error) {
|
|
759
|
-
if (error instanceof ApiException) throw error;
|
|
760
|
-
console.error("Passkey authentication error:", error);
|
|
761
|
-
throw new ApiException(errors.invalid_credentials, 401);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
).post(
|
|
765
|
-
"/check",
|
|
766
|
-
zValidator("json", PasskeyCheckRequest),
|
|
767
|
-
(c) => {
|
|
768
|
-
const { email } = c.req.valid("json");
|
|
769
|
-
const user = getUserByEmail(email);
|
|
770
|
-
if (!user) {
|
|
771
|
-
return c.json({ hasPasskey: false });
|
|
772
|
-
}
|
|
773
|
-
const passkeys = getPasskeysByUserId(user.id);
|
|
774
|
-
return c.json({ hasPasskey: passkeys.length > 0 });
|
|
775
|
-
}
|
|
776
|
-
);
|
|
777
|
-
|
|
778
|
-
// src/api/auth/router.ts
|
|
779
|
-
function hashPassword(password, salt) {
|
|
780
|
-
const crypto2 = createHash2("sha256");
|
|
781
|
-
return crypto2.update(password + salt).digest("hex");
|
|
782
|
-
}
|
|
783
|
-
function generateSalt() {
|
|
784
|
-
return randomBytes2(16).toString("hex");
|
|
785
|
-
}
|
|
786
|
-
var authRouter = new Hono2().post(
|
|
787
|
-
"/email/register",
|
|
788
|
-
zValidator2("json", EmailRegisterRequest),
|
|
789
|
-
async (c) => {
|
|
790
|
-
const { email, password } = c.req.valid("json");
|
|
791
|
-
const existing = getUserByEmail(email);
|
|
792
|
-
if (existing) {
|
|
793
|
-
throw new ApiException(errors.email_already_exists, 409);
|
|
794
|
-
}
|
|
795
|
-
const salt = generateSalt();
|
|
796
|
-
const passwordHash = hashPassword(password, salt) + ":" + salt;
|
|
797
|
-
const user = createUser({ email, passwordHash });
|
|
798
|
-
const token = await createToken({ userId: user.uid, email });
|
|
799
|
-
const tokenHash = hashToken(token);
|
|
800
|
-
const expiresAt = Math.floor(Date.now() / 1e3) + env.JWT_EXPIRY;
|
|
801
|
-
createSession({ userId: user.id, tokenHash, expiresAt });
|
|
802
|
-
return c.json({
|
|
803
|
-
user: {
|
|
804
|
-
id: user.uid,
|
|
805
|
-
email: user.email,
|
|
806
|
-
createdAt: user.createdAt
|
|
807
|
-
},
|
|
808
|
-
token
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
).post(
|
|
812
|
-
"/email/login",
|
|
813
|
-
zValidator2("json", EmailLoginRequest),
|
|
814
|
-
async (c) => {
|
|
815
|
-
const { email, password } = c.req.valid("json");
|
|
816
|
-
const user = getUserByEmail(email);
|
|
817
|
-
if (!user || !user.passwordHash) {
|
|
818
|
-
throw new ApiException(errors.invalid_credentials, 401);
|
|
819
|
-
}
|
|
820
|
-
const [storedHash, salt] = user.passwordHash.split(":");
|
|
821
|
-
if (!salt) {
|
|
822
|
-
throw new ApiException(errors.invalid_credentials, 401);
|
|
823
|
-
}
|
|
824
|
-
const inputHash = hashPassword(password, salt);
|
|
825
|
-
if (inputHash !== storedHash) {
|
|
826
|
-
throw new ApiException(errors.invalid_credentials, 401);
|
|
827
|
-
}
|
|
828
|
-
const token = await createToken({ userId: user.uid, email: user.email ?? void 0 });
|
|
829
|
-
const tokenHash = hashToken(token);
|
|
830
|
-
const expiresAt = Math.floor(Date.now() / 1e3) + env.JWT_EXPIRY;
|
|
831
|
-
createSession({ userId: user.id, tokenHash, expiresAt });
|
|
832
|
-
return c.json({
|
|
833
|
-
user: {
|
|
834
|
-
id: user.uid,
|
|
835
|
-
email: user.email,
|
|
836
|
-
createdAt: user.createdAt
|
|
837
|
-
},
|
|
838
|
-
token
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
).get(
|
|
842
|
-
"/me",
|
|
843
|
-
requireAuthMiddleware,
|
|
844
|
-
(c) => {
|
|
845
|
-
const session = c.get("session");
|
|
846
|
-
return c.json({
|
|
847
|
-
user: {
|
|
848
|
-
id: session.user.uid,
|
|
849
|
-
email: session.user.email,
|
|
850
|
-
createdAt: 0
|
|
851
|
-
// Not needed for /me
|
|
852
|
-
}
|
|
853
|
-
});
|
|
854
|
-
}
|
|
855
|
-
).post(
|
|
856
|
-
"/refresh",
|
|
857
|
-
requireAuthMiddleware,
|
|
858
|
-
async (c) => {
|
|
859
|
-
const session = c.get("session");
|
|
860
|
-
const token = await createToken({
|
|
861
|
-
userId: session.user.uid,
|
|
862
|
-
email: session.user.email ?? void 0
|
|
863
|
-
});
|
|
864
|
-
const tokenHash = hashToken(token);
|
|
865
|
-
const expiresAt = Math.floor(Date.now() / 1e3) + env.JWT_EXPIRY;
|
|
866
|
-
const oldToken = c.req.header("Authorization")?.slice(7);
|
|
867
|
-
if (oldToken) {
|
|
868
|
-
deleteSession(hashToken(oldToken));
|
|
869
|
-
}
|
|
870
|
-
createSession({ userId: session.user.id, tokenHash, expiresAt });
|
|
871
|
-
return c.json({
|
|
872
|
-
token,
|
|
873
|
-
expiresIn: env.JWT_EXPIRY
|
|
874
|
-
});
|
|
875
|
-
}
|
|
876
|
-
).post(
|
|
877
|
-
"/logout",
|
|
878
|
-
requireAuthMiddleware,
|
|
879
|
-
(c) => {
|
|
880
|
-
const token = c.req.header("Authorization")?.slice(7);
|
|
881
|
-
if (token) {
|
|
882
|
-
deleteSession(hashToken(token));
|
|
883
|
-
}
|
|
884
|
-
return c.json({ success: true });
|
|
885
|
-
}
|
|
886
|
-
).route("/passkey", passkeyRouter);
|
|
887
|
-
|
|
888
|
-
// src/api/vault/router.ts
|
|
889
|
-
import { Hono as Hono3 } from "hono";
|
|
890
|
-
import { zValidator as zValidator3 } from "@hono/zod-validator";
|
|
891
|
-
function toVaultResponse(vault) {
|
|
892
|
-
return {
|
|
893
|
-
uid: vault.uid,
|
|
894
|
-
name: vault.name,
|
|
895
|
-
data: vault.data,
|
|
896
|
-
salt: vault.salt,
|
|
897
|
-
version: vault.version,
|
|
898
|
-
updatedAt: vault.updatedAt
|
|
899
|
-
};
|
|
900
|
-
}
|
|
901
|
-
var vaultRouter = new Hono3().use("/*", requireAuthMiddleware).get(
|
|
902
|
-
"/",
|
|
903
|
-
(c) => {
|
|
904
|
-
const session = c.get("session");
|
|
905
|
-
const vaults = getVaultsByUserId(session.user.id);
|
|
906
|
-
return c.json({
|
|
907
|
-
vaults: vaults.map(toVaultResponse)
|
|
908
|
-
});
|
|
909
|
-
}
|
|
910
|
-
).get(
|
|
911
|
-
"/:uid",
|
|
912
|
-
(c) => {
|
|
913
|
-
const session = c.get("session");
|
|
914
|
-
const { uid } = c.req.param();
|
|
915
|
-
const vault = getVaultByUid(uid, session.user.id);
|
|
916
|
-
if (!vault) {
|
|
917
|
-
throw new ApiException(errors.vault_not_found, 404);
|
|
918
|
-
}
|
|
919
|
-
return c.json(toVaultResponse(vault));
|
|
920
|
-
}
|
|
921
|
-
).post(
|
|
922
|
-
"/",
|
|
923
|
-
zValidator3("json", CreateVaultRequest),
|
|
924
|
-
(c) => {
|
|
925
|
-
const session = c.get("session");
|
|
926
|
-
const { name, data, salt } = c.req.valid("json");
|
|
927
|
-
const existing = getVaultByName(name, session.user.id);
|
|
928
|
-
if (existing) {
|
|
929
|
-
throw new ApiException(errors.vault_already_exists(name), 409);
|
|
930
|
-
}
|
|
931
|
-
const vault = createVault({
|
|
932
|
-
userId: session.user.id,
|
|
933
|
-
name,
|
|
934
|
-
data,
|
|
935
|
-
salt
|
|
936
|
-
});
|
|
937
|
-
return c.json(toVaultResponse(vault), 201);
|
|
938
|
-
}
|
|
939
|
-
).put(
|
|
940
|
-
"/:uid",
|
|
941
|
-
zValidator3("json", UpdateVaultRequest),
|
|
942
|
-
(c) => {
|
|
943
|
-
const session = c.get("session");
|
|
944
|
-
const { uid } = c.req.param();
|
|
945
|
-
const { data, salt, version } = c.req.valid("json");
|
|
946
|
-
const vault = updateVault(uid, session.user.id, { data, salt, version });
|
|
947
|
-
if (!vault) {
|
|
948
|
-
throw new ApiException(errors.vault_not_found, 404);
|
|
949
|
-
}
|
|
950
|
-
return c.json(toVaultResponse(vault));
|
|
951
|
-
}
|
|
952
|
-
).delete(
|
|
953
|
-
"/:uid",
|
|
954
|
-
(c) => {
|
|
955
|
-
const session = c.get("session");
|
|
956
|
-
const { uid } = c.req.param();
|
|
957
|
-
const deleted = deleteVault(uid, session.user.id);
|
|
958
|
-
if (!deleted) {
|
|
959
|
-
throw new ApiException(errors.vault_not_found, 404);
|
|
960
|
-
}
|
|
961
|
-
return c.json({ success: true });
|
|
962
|
-
}
|
|
963
|
-
);
|
|
964
|
-
|
|
965
|
-
// src/app.ts
|
|
966
|
-
var errorHandler = (error, c) => {
|
|
967
|
-
const requestId = c.req.header("x-request-id") ?? crypto.randomUUID();
|
|
968
|
-
if (error instanceof ApiException) {
|
|
969
|
-
return c.json(
|
|
970
|
-
{ error: error.error, requestId },
|
|
971
|
-
error.status
|
|
972
|
-
);
|
|
973
|
-
}
|
|
974
|
-
if (error instanceof ZodError) {
|
|
975
|
-
const message = error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join(", ");
|
|
976
|
-
return c.json(
|
|
977
|
-
{
|
|
978
|
-
error: {
|
|
979
|
-
code: "validation_error",
|
|
980
|
-
message,
|
|
981
|
-
details: { errors: error.errors }
|
|
982
|
-
},
|
|
983
|
-
requestId
|
|
984
|
-
},
|
|
985
|
-
400
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
console.error(`[${requestId}] Unhandled error:`, error);
|
|
989
|
-
return c.json(
|
|
990
|
-
{ error: errors.internal_error, requestId },
|
|
991
|
-
500
|
|
992
|
-
);
|
|
993
|
-
};
|
|
994
|
-
function createApp() {
|
|
995
|
-
const app = new Hono4();
|
|
996
|
-
app.use("*", logger());
|
|
997
|
-
app.use(
|
|
998
|
-
"*",
|
|
999
|
-
cors({
|
|
1000
|
-
origin: "*",
|
|
1001
|
-
// Configure for production
|
|
1002
|
-
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
1003
|
-
allowHeaders: ["Content-Type", "Authorization"],
|
|
1004
|
-
exposeHeaders: ["X-Request-Id"]
|
|
1005
|
-
})
|
|
1006
|
-
);
|
|
1007
|
-
app.get("/health", (c) => c.json({ status: "ok", timestamp: Date.now() }));
|
|
1008
|
-
app.route("/auth", authRouter);
|
|
1009
|
-
app.route("/vault", vaultRouter);
|
|
1010
|
-
app.onError(errorHandler);
|
|
1011
|
-
app.notFound((c) => {
|
|
1012
|
-
return c.json(
|
|
1013
|
-
{ error: { code: "not_found", message: "Endpoint not found" } },
|
|
1014
|
-
404
|
|
1015
|
-
);
|
|
1016
|
-
});
|
|
1017
|
-
return app;
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
export {
|
|
1021
|
-
env,
|
|
1022
|
-
getDb,
|
|
1023
|
-
closeDb,
|
|
1024
|
-
createToken,
|
|
1025
|
-
verifyToken,
|
|
1026
|
-
hashToken,
|
|
1027
|
-
ApiException,
|
|
1028
|
-
errors,
|
|
1029
|
-
getError,
|
|
1030
|
-
optionalAuthMiddleware,
|
|
1031
|
-
requireAuthMiddleware,
|
|
1032
|
-
UserResponse,
|
|
1033
|
-
EmailRegisterRequest,
|
|
1034
|
-
EmailLoginRequest,
|
|
1035
|
-
AuthResponse,
|
|
1036
|
-
RefreshResponse,
|
|
1037
|
-
MeResponse,
|
|
1038
|
-
PasskeyRegisterOptionsRequest,
|
|
1039
|
-
PasskeyRegisterVerifyRequest,
|
|
1040
|
-
PasskeyLoginOptionsRequest,
|
|
1041
|
-
PasskeyLoginVerifyRequest,
|
|
1042
|
-
PasskeyCheckResponse,
|
|
1043
|
-
CreateVaultRequest,
|
|
1044
|
-
UpdateVaultRequest,
|
|
1045
|
-
VaultResponse,
|
|
1046
|
-
VaultsListResponse,
|
|
1047
|
-
createApp
|
|
1048
|
-
};
|