@ursalock/server 0.3.0 → 0.4.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 +733 -50
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/app.ts
|
|
2
|
-
import { Hono as
|
|
2
|
+
import { Hono as Hono6 } from "hono";
|
|
3
3
|
import { cors } from "hono/cors";
|
|
4
4
|
import { logger } from "hono/logger";
|
|
5
5
|
import { bodyLimit } from "hono/body-limit";
|
|
@@ -88,6 +88,13 @@ function getAllowedOrigins() {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
// src/db/schema.ts
|
|
91
|
+
function parseApiKeyScopes(key) {
|
|
92
|
+
return {
|
|
93
|
+
permissions: JSON.parse(key.permissions),
|
|
94
|
+
vaultUids: key.vaultUids ? JSON.parse(key.vaultUids) : null,
|
|
95
|
+
collections: key.collections ? JSON.parse(key.collections) : null
|
|
96
|
+
};
|
|
97
|
+
}
|
|
91
98
|
var CREATE_TABLES_SQL = `
|
|
92
99
|
-- Users table
|
|
93
100
|
CREATE TABLE IF NOT EXISTS users (
|
|
@@ -149,6 +156,47 @@ CREATE TABLE IF NOT EXISTS vaults (
|
|
|
149
156
|
CREATE INDEX IF NOT EXISTS idx_vaults_uid ON vaults(uid);
|
|
150
157
|
CREATE INDEX IF NOT EXISTS idx_vaults_user_id ON vaults(user_id);
|
|
151
158
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_vaults_user_name ON vaults(user_id, name);
|
|
159
|
+
|
|
160
|
+
-- Documents table (individually encrypted items within vaults)
|
|
161
|
+
CREATE TABLE IF NOT EXISTS documents (
|
|
162
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
163
|
+
uid TEXT NOT NULL UNIQUE DEFAULT (lower(hex(randomblob(16)))),
|
|
164
|
+
vault_uid TEXT NOT NULL,
|
|
165
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
166
|
+
collection TEXT NOT NULL,
|
|
167
|
+
data TEXT NOT NULL,
|
|
168
|
+
hmac TEXT,
|
|
169
|
+
version INTEGER NOT NULL DEFAULT 1,
|
|
170
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
171
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
172
|
+
deleted_at INTEGER
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
CREATE INDEX IF NOT EXISTS idx_documents_vault_uid ON documents(vault_uid);
|
|
176
|
+
CREATE INDEX IF NOT EXISTS idx_documents_user_id ON documents(user_id);
|
|
177
|
+
CREATE INDEX IF NOT EXISTS idx_documents_vault_collection ON documents(vault_uid, collection);
|
|
178
|
+
CREATE INDEX IF NOT EXISTS idx_documents_vault_updated ON documents(vault_uid, updated_at);
|
|
179
|
+
CREATE INDEX IF NOT EXISTS idx_documents_vault_collection_deleted ON documents(vault_uid, collection, deleted_at);
|
|
180
|
+
|
|
181
|
+
-- API keys table (for agent/service access)
|
|
182
|
+
CREATE TABLE IF NOT EXISTS api_keys (
|
|
183
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
184
|
+
uid TEXT NOT NULL UNIQUE DEFAULT (lower(hex(randomblob(16)))),
|
|
185
|
+
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
186
|
+
name TEXT NOT NULL,
|
|
187
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
188
|
+
key_prefix TEXT NOT NULL,
|
|
189
|
+
permissions TEXT NOT NULL DEFAULT '["read","write"]',
|
|
190
|
+
vault_uids TEXT,
|
|
191
|
+
collections TEXT,
|
|
192
|
+
expires_at INTEGER,
|
|
193
|
+
last_used_at INTEGER,
|
|
194
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
195
|
+
revoked_at INTEGER
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_key_hash ON api_keys(key_hash);
|
|
199
|
+
CREATE INDEX IF NOT EXISTS idx_api_keys_user_id ON api_keys(user_id);
|
|
152
200
|
`;
|
|
153
201
|
|
|
154
202
|
// src/db/client.ts
|
|
@@ -400,6 +448,177 @@ function deleteVault(uid, userId) {
|
|
|
400
448
|
const stmt = db.prepare(`DELETE FROM vaults WHERE uid = ? AND user_id = ?`);
|
|
401
449
|
return stmt.run(uid, userId).changes > 0;
|
|
402
450
|
}
|
|
451
|
+
var DOCUMENT_COLUMNS = `id, uid, vault_uid as vaultUid, user_id as userId, collection,
|
|
452
|
+
data, hmac, version, created_at as createdAt, updated_at as updatedAt, deleted_at as deletedAt`;
|
|
453
|
+
function createDocument(input) {
|
|
454
|
+
const db = getDb();
|
|
455
|
+
const stmt = db.prepare(`
|
|
456
|
+
INSERT INTO documents (vault_uid, user_id, collection, data, hmac)
|
|
457
|
+
VALUES (?, ?, ?, ?, ?)
|
|
458
|
+
RETURNING ${DOCUMENT_COLUMNS}
|
|
459
|
+
`);
|
|
460
|
+
return stmt.get(
|
|
461
|
+
input.vaultUid,
|
|
462
|
+
input.userId,
|
|
463
|
+
input.collection,
|
|
464
|
+
input.data,
|
|
465
|
+
input.hmac ?? null
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
function getDocumentByUid(uid, vaultUid, userId) {
|
|
469
|
+
const db = getDb();
|
|
470
|
+
const stmt = db.prepare(`
|
|
471
|
+
SELECT ${DOCUMENT_COLUMNS}
|
|
472
|
+
FROM documents WHERE uid = ? AND vault_uid = ? AND user_id = ?
|
|
473
|
+
`);
|
|
474
|
+
return stmt.get(uid, vaultUid, userId);
|
|
475
|
+
}
|
|
476
|
+
function listDocuments(vaultUid, userId, opts) {
|
|
477
|
+
const db = getDb();
|
|
478
|
+
const conditions = ["vault_uid = ?", "user_id = ?"];
|
|
479
|
+
const params = [vaultUid, userId];
|
|
480
|
+
if (opts?.collection) {
|
|
481
|
+
conditions.push("collection = ?");
|
|
482
|
+
params.push(opts.collection);
|
|
483
|
+
}
|
|
484
|
+
if (opts?.since != null) {
|
|
485
|
+
conditions.push("updated_at >= ?");
|
|
486
|
+
params.push(opts.since);
|
|
487
|
+
}
|
|
488
|
+
if (!opts?.includeDeleted) {
|
|
489
|
+
conditions.push("deleted_at IS NULL");
|
|
490
|
+
}
|
|
491
|
+
let query = `SELECT ${DOCUMENT_COLUMNS} FROM documents WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC`;
|
|
492
|
+
if (opts?.limit != null) {
|
|
493
|
+
query += ` LIMIT ?`;
|
|
494
|
+
params.push(opts.limit);
|
|
495
|
+
}
|
|
496
|
+
if (opts?.offset != null) {
|
|
497
|
+
query += ` OFFSET ?`;
|
|
498
|
+
params.push(opts.offset);
|
|
499
|
+
}
|
|
500
|
+
const stmt = db.prepare(query);
|
|
501
|
+
return stmt.all(...params);
|
|
502
|
+
}
|
|
503
|
+
function updateDocument(uid, vaultUid, userId, input) {
|
|
504
|
+
const db = getDb();
|
|
505
|
+
if (input.version != null) {
|
|
506
|
+
const stmt2 = db.prepare(`
|
|
507
|
+
UPDATE documents SET data = ?, hmac = ?, version = ? + 1, updated_at = unixepoch()
|
|
508
|
+
WHERE uid = ? AND vault_uid = ? AND user_id = ? AND version = ?
|
|
509
|
+
RETURNING ${DOCUMENT_COLUMNS}
|
|
510
|
+
`);
|
|
511
|
+
return stmt2.get(input.data, input.hmac ?? null, input.version, uid, vaultUid, userId, input.version);
|
|
512
|
+
}
|
|
513
|
+
const stmt = db.prepare(`
|
|
514
|
+
UPDATE documents SET data = ?, hmac = ?, version = version + 1, updated_at = unixepoch()
|
|
515
|
+
WHERE uid = ? AND vault_uid = ? AND user_id = ?
|
|
516
|
+
RETURNING ${DOCUMENT_COLUMNS}
|
|
517
|
+
`);
|
|
518
|
+
return stmt.get(input.data, input.hmac ?? null, uid, vaultUid, userId);
|
|
519
|
+
}
|
|
520
|
+
function softDeleteDocument(uid, vaultUid, userId) {
|
|
521
|
+
const db = getDb();
|
|
522
|
+
const stmt = db.prepare(`
|
|
523
|
+
UPDATE documents SET deleted_at = unixepoch(), updated_at = unixepoch()
|
|
524
|
+
WHERE uid = ? AND vault_uid = ? AND user_id = ? AND deleted_at IS NULL
|
|
525
|
+
RETURNING ${DOCUMENT_COLUMNS}
|
|
526
|
+
`);
|
|
527
|
+
return stmt.get(uid, vaultUid, userId);
|
|
528
|
+
}
|
|
529
|
+
function getDocumentsSince(vaultUid, userId, since) {
|
|
530
|
+
const db = getDb();
|
|
531
|
+
const stmt = db.prepare(`
|
|
532
|
+
SELECT ${DOCUMENT_COLUMNS}
|
|
533
|
+
FROM documents WHERE vault_uid = ? AND user_id = ? AND updated_at >= ?
|
|
534
|
+
ORDER BY updated_at DESC
|
|
535
|
+
`);
|
|
536
|
+
return stmt.all(vaultUid, userId, since);
|
|
537
|
+
}
|
|
538
|
+
var API_KEY_COLUMNS = `id, uid, user_id as userId, name, key_hash as keyHash, key_prefix as keyPrefix,
|
|
539
|
+
permissions, vault_uids as vaultUids, collections, expires_at as expiresAt,
|
|
540
|
+
last_used_at as lastUsedAt, created_at as createdAt, revoked_at as revokedAt`;
|
|
541
|
+
function createApiKey(input) {
|
|
542
|
+
const db = getDb();
|
|
543
|
+
const stmt = db.prepare(`
|
|
544
|
+
INSERT INTO api_keys (user_id, name, key_hash, key_prefix, permissions, vault_uids, collections, expires_at)
|
|
545
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
546
|
+
RETURNING ${API_KEY_COLUMNS}
|
|
547
|
+
`);
|
|
548
|
+
const permissions = input.permissions ? JSON.stringify(input.permissions) : '["read","write"]';
|
|
549
|
+
const vaultUids = input.vaultUids ? JSON.stringify(input.vaultUids) : null;
|
|
550
|
+
const collections = input.collections ? JSON.stringify(input.collections) : null;
|
|
551
|
+
return stmt.get(
|
|
552
|
+
input.userId,
|
|
553
|
+
input.name,
|
|
554
|
+
input.keyHash,
|
|
555
|
+
input.keyPrefix,
|
|
556
|
+
permissions,
|
|
557
|
+
vaultUids,
|
|
558
|
+
collections,
|
|
559
|
+
input.expiresAt ?? null
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
function getApiKeyByHash(keyHash) {
|
|
563
|
+
const db = getDb();
|
|
564
|
+
const stmt = db.prepare(`
|
|
565
|
+
SELECT
|
|
566
|
+
k.id, k.uid, k.user_id as userId, k.name, k.key_hash as keyHash, k.key_prefix as keyPrefix,
|
|
567
|
+
k.permissions, k.vault_uids as vaultUids, k.collections, k.expires_at as expiresAt,
|
|
568
|
+
k.last_used_at as lastUsedAt, k.created_at as createdAt, k.revoked_at as revokedAt,
|
|
569
|
+
${USER_JOIN_COLUMNS}
|
|
570
|
+
FROM api_keys k
|
|
571
|
+
JOIN users u ON k.user_id = u.id
|
|
572
|
+
WHERE k.key_hash = ?
|
|
573
|
+
`);
|
|
574
|
+
const row = stmt.get(keyHash);
|
|
575
|
+
if (!row) return void 0;
|
|
576
|
+
return {
|
|
577
|
+
id: row["id"],
|
|
578
|
+
uid: row["uid"],
|
|
579
|
+
userId: row["userId"],
|
|
580
|
+
name: row["name"],
|
|
581
|
+
keyHash: row["keyHash"],
|
|
582
|
+
keyPrefix: row["keyPrefix"],
|
|
583
|
+
permissions: row["permissions"],
|
|
584
|
+
vaultUids: row["vaultUids"] ?? null,
|
|
585
|
+
collections: row["collections"] ?? null,
|
|
586
|
+
expiresAt: row["expiresAt"] ?? null,
|
|
587
|
+
lastUsedAt: row["lastUsedAt"] ?? null,
|
|
588
|
+
createdAt: row["createdAt"],
|
|
589
|
+
revokedAt: row["revokedAt"] ?? null,
|
|
590
|
+
user: userFromRow(row)
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function listApiKeysByUserId(userId) {
|
|
594
|
+
const db = getDb();
|
|
595
|
+
const stmt = db.prepare(`
|
|
596
|
+
SELECT id, uid, user_id as userId, name, key_prefix as keyPrefix,
|
|
597
|
+
permissions, vault_uids as vaultUids, collections, expires_at as expiresAt,
|
|
598
|
+
last_used_at as lastUsedAt, created_at as createdAt, revoked_at as revokedAt
|
|
599
|
+
FROM api_keys WHERE user_id = ?
|
|
600
|
+
ORDER BY created_at DESC
|
|
601
|
+
`);
|
|
602
|
+
return stmt.all(userId);
|
|
603
|
+
}
|
|
604
|
+
function revokeApiKey(uid, userId) {
|
|
605
|
+
const db = getDb();
|
|
606
|
+
const stmt = db.prepare(`
|
|
607
|
+
UPDATE api_keys SET revoked_at = unixepoch()
|
|
608
|
+
WHERE uid = ? AND user_id = ? AND revoked_at IS NULL
|
|
609
|
+
`);
|
|
610
|
+
return stmt.run(uid, userId).changes > 0;
|
|
611
|
+
}
|
|
612
|
+
function updateApiKeyLastUsed(uid) {
|
|
613
|
+
const db = getDb();
|
|
614
|
+
const stmt = db.prepare(`UPDATE api_keys SET last_used_at = unixepoch() WHERE uid = ?`);
|
|
615
|
+
stmt.run(uid);
|
|
616
|
+
}
|
|
617
|
+
function deleteExpiredApiKeys() {
|
|
618
|
+
const db = getDb();
|
|
619
|
+
const stmt = db.prepare(`DELETE FROM api_keys WHERE expires_at IS NOT NULL AND expires_at <= unixepoch()`);
|
|
620
|
+
return stmt.run().changes;
|
|
621
|
+
}
|
|
403
622
|
|
|
404
623
|
// src/features/auth/jwt.ts
|
|
405
624
|
import { createHash } from "crypto";
|
|
@@ -460,11 +679,18 @@ var ErrorCode = z2.enum([
|
|
|
460
679
|
"passkey_not_found",
|
|
461
680
|
"session_expired",
|
|
462
681
|
"invalid_origin",
|
|
682
|
+
"api_key_not_found",
|
|
683
|
+
"api_key_revoked",
|
|
684
|
+
"insufficient_permissions",
|
|
463
685
|
// Vault errors
|
|
464
686
|
"vault_not_found",
|
|
465
687
|
"vault_already_exists",
|
|
466
688
|
"vault_conflict",
|
|
467
689
|
"invalid_vault_data",
|
|
690
|
+
// Document errors
|
|
691
|
+
"document_not_found",
|
|
692
|
+
"document_conflict",
|
|
693
|
+
"document_already_exists",
|
|
468
694
|
// Validation errors
|
|
469
695
|
"validation_error",
|
|
470
696
|
"invalid_request",
|
|
@@ -488,6 +714,9 @@ var errors = {
|
|
|
488
714
|
passkey_not_found: { code: "passkey_not_found", message: "Passkey not found" },
|
|
489
715
|
session_expired: { code: "session_expired", message: "Session expired" },
|
|
490
716
|
invalid_origin: { code: "invalid_origin", message: "Origin not allowed" },
|
|
717
|
+
api_key_not_found: { code: "api_key_not_found", message: "API key not found" },
|
|
718
|
+
api_key_revoked: { code: "api_key_revoked", message: "API key has been revoked" },
|
|
719
|
+
insufficient_permissions: { code: "insufficient_permissions", message: "Insufficient permissions" },
|
|
491
720
|
// Vault errors
|
|
492
721
|
vault_not_found: { code: "vault_not_found", message: "Vault not found" },
|
|
493
722
|
vault_already_exists: (name) => ({
|
|
@@ -496,6 +725,10 @@ var errors = {
|
|
|
496
725
|
}),
|
|
497
726
|
vault_conflict: { code: "vault_conflict", message: "Version conflict - vault has been modified. Please refresh and retry." },
|
|
498
727
|
invalid_vault_data: { code: "invalid_vault_data", message: "Invalid vault data" },
|
|
728
|
+
// Document errors
|
|
729
|
+
document_not_found: { code: "document_not_found", message: "Document not found" },
|
|
730
|
+
document_conflict: { code: "document_conflict", message: "Version conflict - document has been modified. Please refresh and retry." },
|
|
731
|
+
document_already_exists: { code: "document_already_exists", message: "Document already exists" },
|
|
499
732
|
// Validation errors
|
|
500
733
|
validation_error: (details) => ({
|
|
501
734
|
code: "validation_error",
|
|
@@ -538,7 +771,8 @@ var optionalAuthMiddleware = createMiddleware(async (c, next) => {
|
|
|
538
771
|
uid: session.user.uid,
|
|
539
772
|
email: session.user.email
|
|
540
773
|
},
|
|
541
|
-
sessionId: session.id
|
|
774
|
+
sessionId: session.id,
|
|
775
|
+
authType: "jwt"
|
|
542
776
|
});
|
|
543
777
|
return next();
|
|
544
778
|
});
|
|
@@ -548,6 +782,44 @@ var requireAuthMiddleware = createMiddleware(async (c, next) => {
|
|
|
548
782
|
throw new ApiException(getError("unauthorized"), 401);
|
|
549
783
|
}
|
|
550
784
|
const token = authHeader.slice(7);
|
|
785
|
+
if (token.startsWith("ulk_")) {
|
|
786
|
+
if (token.length !== 52) {
|
|
787
|
+
throw new ApiException(getError("unauthorized"), 401);
|
|
788
|
+
}
|
|
789
|
+
const keyHash = hashToken(token);
|
|
790
|
+
const keyRecord = getApiKeyByHash(keyHash);
|
|
791
|
+
if (!keyRecord) {
|
|
792
|
+
throw new ApiException(getError("unauthorized"), 401);
|
|
793
|
+
}
|
|
794
|
+
if (keyRecord.revokedAt !== null) {
|
|
795
|
+
throw new ApiException(getError("api_key_revoked"), 401);
|
|
796
|
+
}
|
|
797
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
798
|
+
if (keyRecord.expiresAt !== null && keyRecord.expiresAt <= now) {
|
|
799
|
+
throw new ApiException(getError("unauthorized"), 401);
|
|
800
|
+
}
|
|
801
|
+
setImmediate(() => {
|
|
802
|
+
try {
|
|
803
|
+
updateApiKeyLastUsed(keyRecord.uid);
|
|
804
|
+
} catch {
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
const scopes = parseApiKeyScopes(keyRecord);
|
|
808
|
+
c.set("session", {
|
|
809
|
+
user: {
|
|
810
|
+
id: keyRecord.user.id,
|
|
811
|
+
uid: keyRecord.user.uid,
|
|
812
|
+
email: keyRecord.user.email
|
|
813
|
+
},
|
|
814
|
+
sessionId: keyRecord.id,
|
|
815
|
+
authType: "apiKey",
|
|
816
|
+
apiKey: {
|
|
817
|
+
uid: keyRecord.uid,
|
|
818
|
+
...scopes
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
return next();
|
|
822
|
+
}
|
|
551
823
|
const payload = await verifyToken(token);
|
|
552
824
|
if (!payload?.sub) {
|
|
553
825
|
throw new ApiException(getError("unauthorized"), 401);
|
|
@@ -563,10 +835,39 @@ var requireAuthMiddleware = createMiddleware(async (c, next) => {
|
|
|
563
835
|
uid: session.user.uid,
|
|
564
836
|
email: session.user.email
|
|
565
837
|
},
|
|
566
|
-
sessionId: session.id
|
|
838
|
+
sessionId: session.id,
|
|
839
|
+
authType: "jwt"
|
|
567
840
|
});
|
|
568
841
|
return next();
|
|
569
842
|
});
|
|
843
|
+
var requirePermission = (permission) => {
|
|
844
|
+
return createMiddleware(async (c, next) => {
|
|
845
|
+
const session = c.get("session");
|
|
846
|
+
if (!session.apiKey) return next();
|
|
847
|
+
if (!session.apiKey.permissions.includes(permission)) {
|
|
848
|
+
throw new ApiException(getError("insufficient_permissions"), 403);
|
|
849
|
+
}
|
|
850
|
+
return next();
|
|
851
|
+
});
|
|
852
|
+
};
|
|
853
|
+
var requireVaultAccess = createMiddleware(async (c, next) => {
|
|
854
|
+
const session = c.get("session");
|
|
855
|
+
if (!session.apiKey) return next();
|
|
856
|
+
if (session.apiKey.vaultUids === null) return next();
|
|
857
|
+
const vaultUid = c.req.param("vaultUid") ?? c.req.param("uid");
|
|
858
|
+
if (!vaultUid) return next();
|
|
859
|
+
if (!session.apiKey.vaultUids.includes(vaultUid)) {
|
|
860
|
+
throw new ApiException(getError("vault_not_found"), 404);
|
|
861
|
+
}
|
|
862
|
+
return next();
|
|
863
|
+
});
|
|
864
|
+
function assertCollectionAccess(session, collection) {
|
|
865
|
+
if (!session.apiKey) return;
|
|
866
|
+
if (session.apiKey.collections === null) return;
|
|
867
|
+
if (!session.apiKey.collections.includes(collection)) {
|
|
868
|
+
throw new ApiException(getError("insufficient_permissions"), 403);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
570
871
|
|
|
571
872
|
// src/api/schemas.ts
|
|
572
873
|
import { z as z3 } from "zod";
|
|
@@ -633,6 +934,70 @@ var VaultResponse = z3.object({
|
|
|
633
934
|
var VaultsListResponse = z3.object({
|
|
634
935
|
vaults: z3.array(VaultResponse)
|
|
635
936
|
});
|
|
937
|
+
var COLLECTION_NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
938
|
+
var HMAC_HEX_RE = /^[0-9a-f]{64}$/;
|
|
939
|
+
var CreateDocumentRequest = z3.object({
|
|
940
|
+
collection: z3.string().min(1).max(255).regex(COLLECTION_NAME_RE, "Collection name must be alphanumeric (hyphens and underscores allowed)"),
|
|
941
|
+
data: z3.string().max(MAX_DATA_SIZE, `Data must not exceed ${MAX_DATA_SIZE} bytes`).regex(BASE64_RE, "Data must be valid base64"),
|
|
942
|
+
hmac: z3.string().regex(HMAC_HEX_RE, "HMAC must be a valid SHA-256 hex string (64 characters)").optional()
|
|
943
|
+
});
|
|
944
|
+
var UpdateDocumentRequest = z3.object({
|
|
945
|
+
data: z3.string().max(MAX_DATA_SIZE, `Data must not exceed ${MAX_DATA_SIZE} bytes`).regex(BASE64_RE, "Data must be valid base64"),
|
|
946
|
+
hmac: z3.string().regex(HMAC_HEX_RE, "HMAC must be a valid SHA-256 hex string (64 characters)").optional(),
|
|
947
|
+
version: z3.number().optional()
|
|
948
|
+
});
|
|
949
|
+
var DocumentResponse = z3.object({
|
|
950
|
+
uid: z3.string(),
|
|
951
|
+
collection: z3.string(),
|
|
952
|
+
data: z3.string(),
|
|
953
|
+
hmac: z3.string().nullable(),
|
|
954
|
+
version: z3.number(),
|
|
955
|
+
createdAt: z3.number(),
|
|
956
|
+
updatedAt: z3.number(),
|
|
957
|
+
deletedAt: z3.number().nullable()
|
|
958
|
+
});
|
|
959
|
+
var DocumentListResponse = z3.object({
|
|
960
|
+
documents: z3.array(DocumentResponse)
|
|
961
|
+
});
|
|
962
|
+
var DocumentSyncResponse = z3.object({
|
|
963
|
+
documents: z3.array(DocumentResponse),
|
|
964
|
+
syncedAt: z3.number()
|
|
965
|
+
});
|
|
966
|
+
var CreateApiKeyRequest = z3.object({
|
|
967
|
+
name: z3.string().min(1).max(255),
|
|
968
|
+
permissions: z3.array(z3.enum(["read", "write", "delete"])).optional(),
|
|
969
|
+
vaultUids: z3.array(z3.string()).optional(),
|
|
970
|
+
collections: z3.array(z3.string()).optional(),
|
|
971
|
+
expiresAt: z3.number().optional()
|
|
972
|
+
});
|
|
973
|
+
var ApiKeyResponse = z3.object({
|
|
974
|
+
uid: z3.string(),
|
|
975
|
+
name: z3.string(),
|
|
976
|
+
keyPrefix: z3.string(),
|
|
977
|
+
permissions: z3.array(z3.string()),
|
|
978
|
+
vaultUids: z3.array(z3.string()).nullable(),
|
|
979
|
+
collections: z3.array(z3.string()).nullable(),
|
|
980
|
+
expiresAt: z3.number().nullable(),
|
|
981
|
+
lastUsedAt: z3.number().nullable(),
|
|
982
|
+
createdAt: z3.number(),
|
|
983
|
+
revokedAt: z3.number().nullable()
|
|
984
|
+
});
|
|
985
|
+
var ApiKeyCreatedResponse = ApiKeyResponse.extend({
|
|
986
|
+
key: z3.string()
|
|
987
|
+
});
|
|
988
|
+
var ApiKeysListResponse = z3.object({
|
|
989
|
+
apiKeys: z3.array(ApiKeyResponse)
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
// src/features/auth/key-gen.ts
|
|
993
|
+
import { randomBytes } from "crypto";
|
|
994
|
+
function generateApiKey() {
|
|
995
|
+
const randomPart = randomBytes(24).toString("hex");
|
|
996
|
+
return `ulk_${randomPart}`;
|
|
997
|
+
}
|
|
998
|
+
function getKeyPrefix(key) {
|
|
999
|
+
return key.substring(0, 8);
|
|
1000
|
+
}
|
|
636
1001
|
|
|
637
1002
|
// src/api/auth/passkey.ts
|
|
638
1003
|
import { Hono } from "hono";
|
|
@@ -1058,6 +1423,12 @@ var zkcRouter = new Hono2().post(
|
|
|
1058
1423
|
);
|
|
1059
1424
|
|
|
1060
1425
|
// src/api/auth/router.ts
|
|
1426
|
+
var MAX_API_KEYS_PER_USER = 50;
|
|
1427
|
+
function requireJwtSession(session) {
|
|
1428
|
+
if (session.apiKey) {
|
|
1429
|
+
throw new ApiException(errors.insufficient_permissions, 403);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1061
1432
|
var authRouter = new Hono3().post(
|
|
1062
1433
|
"/email/register",
|
|
1063
1434
|
zValidator3("json", EmailRegisterRequest),
|
|
@@ -1154,6 +1525,82 @@ var authRouter = new Hono3().post(
|
|
|
1154
1525
|
}
|
|
1155
1526
|
return c.json({ success: true });
|
|
1156
1527
|
}
|
|
1528
|
+
).post(
|
|
1529
|
+
"/api-keys",
|
|
1530
|
+
requireAuthMiddleware,
|
|
1531
|
+
zValidator3("json", CreateApiKeyRequest),
|
|
1532
|
+
async (c) => {
|
|
1533
|
+
const session = c.get("session");
|
|
1534
|
+
requireJwtSession(session);
|
|
1535
|
+
const existing = listApiKeysByUserId(session.user.id);
|
|
1536
|
+
if (existing.length >= MAX_API_KEYS_PER_USER) {
|
|
1537
|
+
throw new ApiException(
|
|
1538
|
+
{ code: "rate_limited", message: `Maximum ${MAX_API_KEYS_PER_USER} API keys per user` },
|
|
1539
|
+
429
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
const input = c.req.valid("json");
|
|
1543
|
+
const key = generateApiKey();
|
|
1544
|
+
const keyHash = hashToken(key);
|
|
1545
|
+
const apiKey = createApiKey({
|
|
1546
|
+
userId: session.user.id,
|
|
1547
|
+
name: input.name,
|
|
1548
|
+
keyHash,
|
|
1549
|
+
keyPrefix: getKeyPrefix(key),
|
|
1550
|
+
permissions: input.permissions,
|
|
1551
|
+
vaultUids: input.vaultUids,
|
|
1552
|
+
collections: input.collections,
|
|
1553
|
+
expiresAt: input.expiresAt
|
|
1554
|
+
});
|
|
1555
|
+
const scopes = parseApiKeyScopes(apiKey);
|
|
1556
|
+
return c.json({
|
|
1557
|
+
uid: apiKey.uid,
|
|
1558
|
+
name: apiKey.name,
|
|
1559
|
+
key,
|
|
1560
|
+
// Only returned on creation!
|
|
1561
|
+
keyPrefix: apiKey.keyPrefix,
|
|
1562
|
+
...scopes,
|
|
1563
|
+
expiresAt: apiKey.expiresAt,
|
|
1564
|
+
lastUsedAt: apiKey.lastUsedAt,
|
|
1565
|
+
createdAt: apiKey.createdAt,
|
|
1566
|
+
revokedAt: apiKey.revokedAt
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
).get(
|
|
1570
|
+
"/api-keys",
|
|
1571
|
+
requireAuthMiddleware,
|
|
1572
|
+
(c) => {
|
|
1573
|
+
const session = c.get("session");
|
|
1574
|
+
requireJwtSession(session);
|
|
1575
|
+
const apiKeys = listApiKeysByUserId(session.user.id);
|
|
1576
|
+
return c.json({
|
|
1577
|
+
apiKeys: apiKeys.map((key) => {
|
|
1578
|
+
const scopes = parseApiKeyScopes(key);
|
|
1579
|
+
return {
|
|
1580
|
+
uid: key.uid,
|
|
1581
|
+
name: key.name,
|
|
1582
|
+
keyPrefix: key.keyPrefix,
|
|
1583
|
+
...scopes,
|
|
1584
|
+
expiresAt: key.expiresAt,
|
|
1585
|
+
lastUsedAt: key.lastUsedAt,
|
|
1586
|
+
createdAt: key.createdAt,
|
|
1587
|
+
revokedAt: key.revokedAt
|
|
1588
|
+
};
|
|
1589
|
+
})
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
).delete(
|
|
1593
|
+
"/api-keys/:uid",
|
|
1594
|
+
requireAuthMiddleware,
|
|
1595
|
+
(c) => {
|
|
1596
|
+
const session = c.get("session");
|
|
1597
|
+
requireJwtSession(session);
|
|
1598
|
+
const uid = c.req.param("uid");
|
|
1599
|
+
if (!revokeApiKey(uid, session.user.id)) {
|
|
1600
|
+
throw new ApiException(errors.api_key_not_found, 404);
|
|
1601
|
+
}
|
|
1602
|
+
return c.json({ success: true });
|
|
1603
|
+
}
|
|
1157
1604
|
).route("/passkey", passkeyRouter).route("/zkc", zkcRouter);
|
|
1158
1605
|
|
|
1159
1606
|
// src/api/vault/router.ts
|
|
@@ -1172,8 +1619,8 @@ function toVaultResponse(vault) {
|
|
|
1172
1619
|
};
|
|
1173
1620
|
}
|
|
1174
1621
|
var VaultService = class {
|
|
1175
|
-
constructor(
|
|
1176
|
-
this.vaultRepo =
|
|
1622
|
+
constructor(vaultRepo3) {
|
|
1623
|
+
this.vaultRepo = vaultRepo3;
|
|
1177
1624
|
}
|
|
1178
1625
|
/**
|
|
1179
1626
|
* List all vaults for a user
|
|
@@ -1275,19 +1722,32 @@ var vaultRepo = new VaultRepository();
|
|
|
1275
1722
|
var vaultService = new VaultService(vaultRepo);
|
|
1276
1723
|
var vaultRouter = new Hono4().use("/*", requireAuthMiddleware).get(
|
|
1277
1724
|
"/",
|
|
1725
|
+
requirePermission("read"),
|
|
1278
1726
|
(c) => {
|
|
1279
1727
|
const session = c.get("session");
|
|
1280
|
-
|
|
1728
|
+
const result = vaultService.listVaults(session.user.id);
|
|
1729
|
+
if (session.apiKey?.vaultUids) {
|
|
1730
|
+
const allowed = new Set(session.apiKey.vaultUids);
|
|
1731
|
+
result.vaults = result.vaults.filter((v) => allowed.has(v.uid));
|
|
1732
|
+
}
|
|
1733
|
+
return c.json(result);
|
|
1281
1734
|
}
|
|
1282
1735
|
).get(
|
|
1283
1736
|
"/by-name/:name",
|
|
1737
|
+
requirePermission("read"),
|
|
1284
1738
|
(c) => {
|
|
1285
1739
|
const session = c.get("session");
|
|
1286
1740
|
const { name } = c.req.param();
|
|
1287
|
-
|
|
1741
|
+
const vault = vaultService.getVaultByName(name, session.user.id);
|
|
1742
|
+
if (session.apiKey?.vaultUids && !session.apiKey.vaultUids.includes(vault.uid)) {
|
|
1743
|
+
throw new ApiException(errors.vault_not_found, 404);
|
|
1744
|
+
}
|
|
1745
|
+
return c.json(vault);
|
|
1288
1746
|
}
|
|
1289
1747
|
).get(
|
|
1290
1748
|
"/:uid",
|
|
1749
|
+
requirePermission("read"),
|
|
1750
|
+
requireVaultAccess,
|
|
1291
1751
|
(c) => {
|
|
1292
1752
|
const session = c.get("session");
|
|
1293
1753
|
const { uid } = c.req.param();
|
|
@@ -1295,6 +1755,7 @@ var vaultRouter = new Hono4().use("/*", requireAuthMiddleware).get(
|
|
|
1295
1755
|
}
|
|
1296
1756
|
).post(
|
|
1297
1757
|
"/",
|
|
1758
|
+
requirePermission("write"),
|
|
1298
1759
|
zValidator4("json", CreateVaultRequest),
|
|
1299
1760
|
(c) => {
|
|
1300
1761
|
const session = c.get("session");
|
|
@@ -1306,6 +1767,8 @@ var vaultRouter = new Hono4().use("/*", requireAuthMiddleware).get(
|
|
|
1306
1767
|
}
|
|
1307
1768
|
).put(
|
|
1308
1769
|
"/:uid",
|
|
1770
|
+
requirePermission("write"),
|
|
1771
|
+
requireVaultAccess,
|
|
1309
1772
|
zValidator4("json", UpdateVaultRequest),
|
|
1310
1773
|
(c) => {
|
|
1311
1774
|
const session = c.get("session");
|
|
@@ -1317,6 +1780,8 @@ var vaultRouter = new Hono4().use("/*", requireAuthMiddleware).get(
|
|
|
1317
1780
|
}
|
|
1318
1781
|
).delete(
|
|
1319
1782
|
"/:uid",
|
|
1783
|
+
requirePermission("delete"),
|
|
1784
|
+
requireVaultAccess,
|
|
1320
1785
|
(c) => {
|
|
1321
1786
|
const session = c.get("session");
|
|
1322
1787
|
const { uid } = c.req.param();
|
|
@@ -1324,6 +1789,252 @@ var vaultRouter = new Hono4().use("/*", requireAuthMiddleware).get(
|
|
|
1324
1789
|
}
|
|
1325
1790
|
);
|
|
1326
1791
|
|
|
1792
|
+
// src/api/document/router.ts
|
|
1793
|
+
import { Hono as Hono5 } from "hono";
|
|
1794
|
+
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
1795
|
+
import { z as z6 } from "zod";
|
|
1796
|
+
|
|
1797
|
+
// src/services/document-service.ts
|
|
1798
|
+
function toDocumentResponse(document) {
|
|
1799
|
+
return {
|
|
1800
|
+
uid: document.uid,
|
|
1801
|
+
collection: document.collection,
|
|
1802
|
+
data: document.data,
|
|
1803
|
+
hmac: document.hmac,
|
|
1804
|
+
version: document.version,
|
|
1805
|
+
createdAt: document.createdAt,
|
|
1806
|
+
updatedAt: document.updatedAt,
|
|
1807
|
+
deletedAt: document.deletedAt
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
var DocumentService = class {
|
|
1811
|
+
constructor(documentRepo2, vaultRepo3) {
|
|
1812
|
+
this.documentRepo = documentRepo2;
|
|
1813
|
+
this.vaultRepo = vaultRepo3;
|
|
1814
|
+
}
|
|
1815
|
+
/**
|
|
1816
|
+
* Verify vault ownership before any operation
|
|
1817
|
+
* Throws 404 if vault doesn't exist or doesn't belong to user
|
|
1818
|
+
*/
|
|
1819
|
+
verifyVaultOwnership(vaultUid, userId) {
|
|
1820
|
+
const vault = this.vaultRepo.findByUid(vaultUid, userId);
|
|
1821
|
+
if (!vault) {
|
|
1822
|
+
throw new ApiException(errors.vault_not_found, 404);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Create a new document
|
|
1827
|
+
*/
|
|
1828
|
+
createDocument(userId, vaultUid, data) {
|
|
1829
|
+
this.verifyVaultOwnership(vaultUid, userId);
|
|
1830
|
+
const document = this.documentRepo.create({
|
|
1831
|
+
vaultUid,
|
|
1832
|
+
userId,
|
|
1833
|
+
collection: data.collection,
|
|
1834
|
+
data: data.data,
|
|
1835
|
+
hmac: data.hmac
|
|
1836
|
+
});
|
|
1837
|
+
return toDocumentResponse(document);
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Get document by UID
|
|
1841
|
+
*/
|
|
1842
|
+
getDocument(uid, vaultUid, userId) {
|
|
1843
|
+
this.verifyVaultOwnership(vaultUid, userId);
|
|
1844
|
+
const document = this.documentRepo.findByUid(uid, vaultUid, userId);
|
|
1845
|
+
if (!document) {
|
|
1846
|
+
throw new ApiException(errors.document_not_found, 404);
|
|
1847
|
+
}
|
|
1848
|
+
return toDocumentResponse(document);
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* List documents in a vault
|
|
1852
|
+
*/
|
|
1853
|
+
listDocuments(vaultUid, userId, opts) {
|
|
1854
|
+
this.verifyVaultOwnership(vaultUid, userId);
|
|
1855
|
+
const documents = this.documentRepo.list(vaultUid, userId, opts);
|
|
1856
|
+
return {
|
|
1857
|
+
documents: documents.map(toDocumentResponse)
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Update a document
|
|
1862
|
+
*/
|
|
1863
|
+
updateDocument(uid, vaultUid, userId, data) {
|
|
1864
|
+
this.verifyVaultOwnership(vaultUid, userId);
|
|
1865
|
+
const document = this.documentRepo.update(uid, vaultUid, userId, data);
|
|
1866
|
+
if (!document) {
|
|
1867
|
+
if (data.version != null) {
|
|
1868
|
+
const existing = this.documentRepo.findByUid(uid, vaultUid, userId);
|
|
1869
|
+
if (existing) {
|
|
1870
|
+
throw new ApiException(errors.document_conflict, 409);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
throw new ApiException(errors.document_not_found, 404);
|
|
1874
|
+
}
|
|
1875
|
+
return toDocumentResponse(document);
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Soft delete a document
|
|
1879
|
+
*/
|
|
1880
|
+
deleteDocument(uid, vaultUid, userId) {
|
|
1881
|
+
this.verifyVaultOwnership(vaultUid, userId);
|
|
1882
|
+
const document = this.documentRepo.softDelete(uid, vaultUid, userId);
|
|
1883
|
+
if (!document) {
|
|
1884
|
+
throw new ApiException(errors.document_not_found, 404);
|
|
1885
|
+
}
|
|
1886
|
+
return { success: true };
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Delta sync - get documents modified since timestamp
|
|
1890
|
+
*/
|
|
1891
|
+
syncDocuments(vaultUid, userId, since) {
|
|
1892
|
+
this.verifyVaultOwnership(vaultUid, userId);
|
|
1893
|
+
const documents = this.documentRepo.getSince(vaultUid, userId, since);
|
|
1894
|
+
return {
|
|
1895
|
+
documents: documents.map(toDocumentResponse),
|
|
1896
|
+
syncedAt: Math.floor(Date.now() / 1e3)
|
|
1897
|
+
};
|
|
1898
|
+
}
|
|
1899
|
+
};
|
|
1900
|
+
|
|
1901
|
+
// src/repositories/document-repository.ts
|
|
1902
|
+
var DocumentRepository = class {
|
|
1903
|
+
create(document) {
|
|
1904
|
+
return createDocument(document);
|
|
1905
|
+
}
|
|
1906
|
+
findByUid(uid, vaultUid, userId) {
|
|
1907
|
+
return getDocumentByUid(uid, vaultUid, userId);
|
|
1908
|
+
}
|
|
1909
|
+
list(vaultUid, userId, opts) {
|
|
1910
|
+
return listDocuments(vaultUid, userId, opts);
|
|
1911
|
+
}
|
|
1912
|
+
update(uid, vaultUid, userId, data) {
|
|
1913
|
+
return updateDocument(uid, vaultUid, userId, data);
|
|
1914
|
+
}
|
|
1915
|
+
softDelete(uid, vaultUid, userId) {
|
|
1916
|
+
return softDeleteDocument(uid, vaultUid, userId);
|
|
1917
|
+
}
|
|
1918
|
+
getSince(vaultUid, userId, since) {
|
|
1919
|
+
return getDocumentsSince(vaultUid, userId, since);
|
|
1920
|
+
}
|
|
1921
|
+
};
|
|
1922
|
+
|
|
1923
|
+
// src/api/document/router.ts
|
|
1924
|
+
var documentRepo = new DocumentRepository();
|
|
1925
|
+
var vaultRepo2 = new VaultRepository();
|
|
1926
|
+
var documentService = new DocumentService(documentRepo, vaultRepo2);
|
|
1927
|
+
var ListQuerySchema = z6.object({
|
|
1928
|
+
collection: z6.string().optional(),
|
|
1929
|
+
since: z6.coerce.number().optional(),
|
|
1930
|
+
includeDeleted: z6.enum(["true", "false"]).optional().transform((val) => val === "true"),
|
|
1931
|
+
limit: z6.coerce.number().optional(),
|
|
1932
|
+
offset: z6.coerce.number().optional()
|
|
1933
|
+
});
|
|
1934
|
+
var SyncQuerySchema = z6.object({
|
|
1935
|
+
since: z6.coerce.number()
|
|
1936
|
+
});
|
|
1937
|
+
var documentRouter = new Hono5().use("/*", requireAuthMiddleware).get(
|
|
1938
|
+
"/vault/:vaultUid/documents",
|
|
1939
|
+
requirePermission("read"),
|
|
1940
|
+
requireVaultAccess,
|
|
1941
|
+
zValidator5("query", ListQuerySchema),
|
|
1942
|
+
(c) => {
|
|
1943
|
+
const session = c.get("session");
|
|
1944
|
+
const { vaultUid } = c.req.param();
|
|
1945
|
+
const query = c.req.valid("query");
|
|
1946
|
+
if (query.collection) {
|
|
1947
|
+
assertCollectionAccess(session, query.collection);
|
|
1948
|
+
}
|
|
1949
|
+
const result = documentService.listDocuments(vaultUid, session.user.id, {
|
|
1950
|
+
collection: query.collection,
|
|
1951
|
+
since: query.since,
|
|
1952
|
+
includeDeleted: query.includeDeleted,
|
|
1953
|
+
limit: query.limit,
|
|
1954
|
+
offset: query.offset
|
|
1955
|
+
});
|
|
1956
|
+
if (!query.collection && session.apiKey?.collections) {
|
|
1957
|
+
const allowed = new Set(session.apiKey.collections);
|
|
1958
|
+
result.documents = result.documents.filter((doc) => allowed.has(doc.collection));
|
|
1959
|
+
}
|
|
1960
|
+
return c.json(result);
|
|
1961
|
+
}
|
|
1962
|
+
).get(
|
|
1963
|
+
"/vault/:vaultUid/documents/sync",
|
|
1964
|
+
requirePermission("read"),
|
|
1965
|
+
requireVaultAccess,
|
|
1966
|
+
zValidator5("query", SyncQuerySchema),
|
|
1967
|
+
(c) => {
|
|
1968
|
+
const session = c.get("session");
|
|
1969
|
+
const { vaultUid } = c.req.param();
|
|
1970
|
+
const { since } = c.req.valid("query");
|
|
1971
|
+
const result = documentService.syncDocuments(vaultUid, session.user.id, since);
|
|
1972
|
+
if (session.apiKey?.collections) {
|
|
1973
|
+
const allowed = new Set(session.apiKey.collections);
|
|
1974
|
+
result.documents = result.documents.filter((doc) => allowed.has(doc.collection));
|
|
1975
|
+
}
|
|
1976
|
+
return c.json(result);
|
|
1977
|
+
}
|
|
1978
|
+
).get(
|
|
1979
|
+
"/vault/:vaultUid/documents/:uid",
|
|
1980
|
+
requirePermission("read"),
|
|
1981
|
+
requireVaultAccess,
|
|
1982
|
+
(c) => {
|
|
1983
|
+
const session = c.get("session");
|
|
1984
|
+
const { vaultUid, uid } = c.req.param();
|
|
1985
|
+
return c.json(
|
|
1986
|
+
documentService.getDocument(uid, vaultUid, session.user.id)
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
).post(
|
|
1990
|
+
"/vault/:vaultUid/documents",
|
|
1991
|
+
requirePermission("write"),
|
|
1992
|
+
requireVaultAccess,
|
|
1993
|
+
zValidator5("json", CreateDocumentRequest),
|
|
1994
|
+
(c) => {
|
|
1995
|
+
const session = c.get("session");
|
|
1996
|
+
const { vaultUid } = c.req.param();
|
|
1997
|
+
const { collection, data, hmac } = c.req.valid("json");
|
|
1998
|
+
assertCollectionAccess(session, collection);
|
|
1999
|
+
return c.json(
|
|
2000
|
+
documentService.createDocument(session.user.id, vaultUid, {
|
|
2001
|
+
collection,
|
|
2002
|
+
data,
|
|
2003
|
+
hmac
|
|
2004
|
+
}),
|
|
2005
|
+
201
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
).put(
|
|
2009
|
+
"/vault/:vaultUid/documents/:uid",
|
|
2010
|
+
requirePermission("write"),
|
|
2011
|
+
requireVaultAccess,
|
|
2012
|
+
zValidator5("json", UpdateDocumentRequest),
|
|
2013
|
+
(c) => {
|
|
2014
|
+
const session = c.get("session");
|
|
2015
|
+
const { vaultUid, uid } = c.req.param();
|
|
2016
|
+
const { data, hmac, version } = c.req.valid("json");
|
|
2017
|
+
return c.json(
|
|
2018
|
+
documentService.updateDocument(uid, vaultUid, session.user.id, {
|
|
2019
|
+
data,
|
|
2020
|
+
hmac,
|
|
2021
|
+
version
|
|
2022
|
+
})
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
).delete(
|
|
2026
|
+
"/vault/:vaultUid/documents/:uid",
|
|
2027
|
+
requirePermission("delete"),
|
|
2028
|
+
requireVaultAccess,
|
|
2029
|
+
(c) => {
|
|
2030
|
+
const session = c.get("session");
|
|
2031
|
+
const { vaultUid, uid } = c.req.param();
|
|
2032
|
+
return c.json(
|
|
2033
|
+
documentService.deleteDocument(uid, vaultUid, session.user.id)
|
|
2034
|
+
);
|
|
2035
|
+
}
|
|
2036
|
+
);
|
|
2037
|
+
|
|
1327
2038
|
// src/features/auth/rate-limit.ts
|
|
1328
2039
|
import { createMiddleware as createMiddleware2 } from "hono/factory";
|
|
1329
2040
|
var DEFAULT_CONFIG = { max: 100, windowMs: 6e4 };
|
|
@@ -1386,47 +2097,6 @@ function rateLimit(config = {}) {
|
|
|
1386
2097
|
});
|
|
1387
2098
|
}
|
|
1388
2099
|
|
|
1389
|
-
// src/features/auth/csrf.ts
|
|
1390
|
-
import { createMiddleware as createMiddleware3 } from "hono/factory";
|
|
1391
|
-
import { getCookie, setCookie } from "hono/cookie";
|
|
1392
|
-
var CSRF_COOKIE_NAME = "__csrf";
|
|
1393
|
-
var CSRF_HEADER_NAME = "x-csrf-token";
|
|
1394
|
-
var TOKEN_BYTES = 32;
|
|
1395
|
-
var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
|
|
1396
|
-
function csrfCookieOptions() {
|
|
1397
|
-
return {
|
|
1398
|
-
path: "/",
|
|
1399
|
-
httpOnly: false,
|
|
1400
|
-
// Client JS must read this value
|
|
1401
|
-
sameSite: "Strict",
|
|
1402
|
-
secure: env.NODE_ENV === "production"
|
|
1403
|
-
};
|
|
1404
|
-
}
|
|
1405
|
-
function generateToken() {
|
|
1406
|
-
const bytes = new Uint8Array(TOKEN_BYTES);
|
|
1407
|
-
crypto.getRandomValues(bytes);
|
|
1408
|
-
return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
1409
|
-
}
|
|
1410
|
-
var csrfProtection = createMiddleware3(async (c, next) => {
|
|
1411
|
-
if (SAFE_METHODS.has(c.req.method)) {
|
|
1412
|
-
const existing = getCookie(c, CSRF_COOKIE_NAME);
|
|
1413
|
-
if (!existing) {
|
|
1414
|
-
setCookie(c, CSRF_COOKIE_NAME, generateToken(), csrfCookieOptions());
|
|
1415
|
-
}
|
|
1416
|
-
return next();
|
|
1417
|
-
}
|
|
1418
|
-
const cookieToken = getCookie(c, CSRF_COOKIE_NAME);
|
|
1419
|
-
const headerToken = c.req.header(CSRF_HEADER_NAME);
|
|
1420
|
-
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
|
|
1421
|
-
throw new ApiException(
|
|
1422
|
-
{ code: "invalid_request", message: "Invalid or missing CSRF token" },
|
|
1423
|
-
403
|
|
1424
|
-
);
|
|
1425
|
-
}
|
|
1426
|
-
setCookie(c, CSRF_COOKIE_NAME, generateToken(), csrfCookieOptions());
|
|
1427
|
-
await next();
|
|
1428
|
-
});
|
|
1429
|
-
|
|
1430
2100
|
// src/app.ts
|
|
1431
2101
|
var errorHandler = (error, c) => {
|
|
1432
2102
|
const requestId = c.req.header("x-request-id") ?? crypto.randomUUID();
|
|
@@ -1457,7 +2127,7 @@ var errorHandler = (error, c) => {
|
|
|
1457
2127
|
);
|
|
1458
2128
|
};
|
|
1459
2129
|
function createApp() {
|
|
1460
|
-
const app = new
|
|
2130
|
+
const app = new Hono6();
|
|
1461
2131
|
app.use("*", bodyLimit({ maxSize: 11 * 1024 * 1024 }));
|
|
1462
2132
|
app.use(
|
|
1463
2133
|
"*",
|
|
@@ -1489,11 +2159,15 @@ function createApp() {
|
|
|
1489
2159
|
})
|
|
1490
2160
|
);
|
|
1491
2161
|
app.use("*", rateLimit({ max: 100, windowMs: 6e4 }));
|
|
1492
|
-
app.use("*", csrfProtection);
|
|
1493
2162
|
app.get("/health", (c) => c.json({ status: "ok", timestamp: Date.now() }));
|
|
2163
|
+
try {
|
|
2164
|
+
deleteExpiredApiKeys();
|
|
2165
|
+
} catch {
|
|
2166
|
+
}
|
|
1494
2167
|
app.use("/auth/*", rateLimit({ max: 10, windowMs: 6e4 }));
|
|
1495
2168
|
app.route("/auth", authRouter);
|
|
1496
2169
|
app.route("/vault", vaultRouter);
|
|
2170
|
+
app.route("/", documentRouter);
|
|
1497
2171
|
app.onError(errorHandler);
|
|
1498
2172
|
app.notFound((c) => {
|
|
1499
2173
|
const requestId = c.req.header("x-request-id") ?? crypto.randomUUID();
|
|
@@ -1506,8 +2180,16 @@ function createApp() {
|
|
|
1506
2180
|
}
|
|
1507
2181
|
export {
|
|
1508
2182
|
ApiException,
|
|
2183
|
+
ApiKeyCreatedResponse,
|
|
2184
|
+
ApiKeyResponse,
|
|
2185
|
+
ApiKeysListResponse,
|
|
1509
2186
|
AuthResponse,
|
|
2187
|
+
CreateApiKeyRequest,
|
|
2188
|
+
CreateDocumentRequest,
|
|
1510
2189
|
CreateVaultRequest,
|
|
2190
|
+
DocumentListResponse,
|
|
2191
|
+
DocumentResponse,
|
|
2192
|
+
DocumentSyncResponse,
|
|
1511
2193
|
EmailLoginRequest,
|
|
1512
2194
|
EmailRegisterRequest,
|
|
1513
2195
|
MeResponse,
|
|
@@ -1516,6 +2198,7 @@ export {
|
|
|
1516
2198
|
PasskeyRegisterOptionsRequest,
|
|
1517
2199
|
PasskeyRegisterVerifyRequest,
|
|
1518
2200
|
RefreshResponse,
|
|
2201
|
+
UpdateDocumentRequest,
|
|
1519
2202
|
UpdateVaultRequest,
|
|
1520
2203
|
UserResponse,
|
|
1521
2204
|
VaultResponse,
|