@ursalock/server 0.3.1 → 0.4.2
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 +750 -64
- 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,186 @@ 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 result = db.transaction(() => {
|
|
507
|
+
const stmt2 = db.prepare(`
|
|
508
|
+
UPDATE documents SET data = ?, hmac = ?, version = ? + 1, updated_at = unixepoch()
|
|
509
|
+
WHERE uid = ? AND vault_uid = ? AND user_id = ? AND version = ?
|
|
510
|
+
RETURNING ${DOCUMENT_COLUMNS}
|
|
511
|
+
`);
|
|
512
|
+
const doc2 = stmt2.get(input.data, input.hmac ?? null, input.version, uid, vaultUid, userId, input.version);
|
|
513
|
+
if (doc2) return { document: doc2, conflict: false };
|
|
514
|
+
const exists = db.prepare(
|
|
515
|
+
`SELECT 1 FROM documents WHERE uid = ? AND vault_uid = ? AND user_id = ?`
|
|
516
|
+
).get(uid, vaultUid, userId);
|
|
517
|
+
return { document: void 0, conflict: !!exists };
|
|
518
|
+
})();
|
|
519
|
+
return result;
|
|
520
|
+
}
|
|
521
|
+
const stmt = db.prepare(`
|
|
522
|
+
UPDATE documents SET data = ?, hmac = ?, version = version + 1, updated_at = unixepoch()
|
|
523
|
+
WHERE uid = ? AND vault_uid = ? AND user_id = ?
|
|
524
|
+
RETURNING ${DOCUMENT_COLUMNS}
|
|
525
|
+
`);
|
|
526
|
+
const doc = stmt.get(input.data, input.hmac ?? null, uid, vaultUid, userId);
|
|
527
|
+
return { document: doc, conflict: false };
|
|
528
|
+
}
|
|
529
|
+
function softDeleteDocument(uid, vaultUid, userId) {
|
|
530
|
+
const db = getDb();
|
|
531
|
+
const stmt = db.prepare(`
|
|
532
|
+
UPDATE documents SET deleted_at = unixepoch(), updated_at = unixepoch()
|
|
533
|
+
WHERE uid = ? AND vault_uid = ? AND user_id = ? AND deleted_at IS NULL
|
|
534
|
+
RETURNING ${DOCUMENT_COLUMNS}
|
|
535
|
+
`);
|
|
536
|
+
return stmt.get(uid, vaultUid, userId);
|
|
537
|
+
}
|
|
538
|
+
function getDocumentsSince(vaultUid, userId, since) {
|
|
539
|
+
const db = getDb();
|
|
540
|
+
const stmt = db.prepare(`
|
|
541
|
+
SELECT ${DOCUMENT_COLUMNS}
|
|
542
|
+
FROM documents WHERE vault_uid = ? AND user_id = ? AND updated_at >= ?
|
|
543
|
+
ORDER BY updated_at DESC
|
|
544
|
+
`);
|
|
545
|
+
return stmt.all(vaultUid, userId, since);
|
|
546
|
+
}
|
|
547
|
+
var API_KEY_COLUMNS = `id, uid, user_id as userId, name, key_hash as keyHash, key_prefix as keyPrefix,
|
|
548
|
+
permissions, vault_uids as vaultUids, collections, expires_at as expiresAt,
|
|
549
|
+
last_used_at as lastUsedAt, created_at as createdAt, revoked_at as revokedAt`;
|
|
550
|
+
function createApiKey(input) {
|
|
551
|
+
const db = getDb();
|
|
552
|
+
const stmt = db.prepare(`
|
|
553
|
+
INSERT INTO api_keys (user_id, name, key_hash, key_prefix, permissions, vault_uids, collections, expires_at)
|
|
554
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
555
|
+
RETURNING ${API_KEY_COLUMNS}
|
|
556
|
+
`);
|
|
557
|
+
const permissions = input.permissions ? JSON.stringify(input.permissions) : '["read","write"]';
|
|
558
|
+
const vaultUids = input.vaultUids ? JSON.stringify(input.vaultUids) : null;
|
|
559
|
+
const collections = input.collections ? JSON.stringify(input.collections) : null;
|
|
560
|
+
return stmt.get(
|
|
561
|
+
input.userId,
|
|
562
|
+
input.name,
|
|
563
|
+
input.keyHash,
|
|
564
|
+
input.keyPrefix,
|
|
565
|
+
permissions,
|
|
566
|
+
vaultUids,
|
|
567
|
+
collections,
|
|
568
|
+
input.expiresAt ?? null
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
function getApiKeyByHash(keyHash) {
|
|
572
|
+
const db = getDb();
|
|
573
|
+
const stmt = db.prepare(`
|
|
574
|
+
SELECT
|
|
575
|
+
k.id, k.uid, k.user_id as userId, k.name, k.key_hash as keyHash, k.key_prefix as keyPrefix,
|
|
576
|
+
k.permissions, k.vault_uids as vaultUids, k.collections, k.expires_at as expiresAt,
|
|
577
|
+
k.last_used_at as lastUsedAt, k.created_at as createdAt, k.revoked_at as revokedAt,
|
|
578
|
+
${USER_JOIN_COLUMNS}
|
|
579
|
+
FROM api_keys k
|
|
580
|
+
JOIN users u ON k.user_id = u.id
|
|
581
|
+
WHERE k.key_hash = ?
|
|
582
|
+
`);
|
|
583
|
+
const row = stmt.get(keyHash);
|
|
584
|
+
if (!row) return void 0;
|
|
585
|
+
return {
|
|
586
|
+
id: row["id"],
|
|
587
|
+
uid: row["uid"],
|
|
588
|
+
userId: row["userId"],
|
|
589
|
+
name: row["name"],
|
|
590
|
+
keyHash: row["keyHash"],
|
|
591
|
+
keyPrefix: row["keyPrefix"],
|
|
592
|
+
permissions: row["permissions"],
|
|
593
|
+
vaultUids: row["vaultUids"] ?? null,
|
|
594
|
+
collections: row["collections"] ?? null,
|
|
595
|
+
expiresAt: row["expiresAt"] ?? null,
|
|
596
|
+
lastUsedAt: row["lastUsedAt"] ?? null,
|
|
597
|
+
createdAt: row["createdAt"],
|
|
598
|
+
revokedAt: row["revokedAt"] ?? null,
|
|
599
|
+
user: userFromRow(row)
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function listApiKeysByUserId(userId) {
|
|
603
|
+
const db = getDb();
|
|
604
|
+
const stmt = db.prepare(`
|
|
605
|
+
SELECT id, uid, user_id as userId, name, key_prefix as keyPrefix,
|
|
606
|
+
permissions, vault_uids as vaultUids, collections, expires_at as expiresAt,
|
|
607
|
+
last_used_at as lastUsedAt, created_at as createdAt, revoked_at as revokedAt
|
|
608
|
+
FROM api_keys WHERE user_id = ?
|
|
609
|
+
ORDER BY created_at DESC
|
|
610
|
+
`);
|
|
611
|
+
return stmt.all(userId);
|
|
612
|
+
}
|
|
613
|
+
function revokeApiKey(uid, userId) {
|
|
614
|
+
const db = getDb();
|
|
615
|
+
const stmt = db.prepare(`
|
|
616
|
+
UPDATE api_keys SET revoked_at = unixepoch()
|
|
617
|
+
WHERE uid = ? AND user_id = ? AND revoked_at IS NULL
|
|
618
|
+
`);
|
|
619
|
+
return stmt.run(uid, userId).changes > 0;
|
|
620
|
+
}
|
|
621
|
+
function updateApiKeyLastUsed(uid) {
|
|
622
|
+
const db = getDb();
|
|
623
|
+
const stmt = db.prepare(`UPDATE api_keys SET last_used_at = unixepoch() WHERE uid = ?`);
|
|
624
|
+
stmt.run(uid);
|
|
625
|
+
}
|
|
626
|
+
function deleteExpiredApiKeys() {
|
|
627
|
+
const db = getDb();
|
|
628
|
+
const stmt = db.prepare(`DELETE FROM api_keys WHERE expires_at IS NOT NULL AND expires_at <= unixepoch()`);
|
|
629
|
+
return stmt.run().changes;
|
|
630
|
+
}
|
|
403
631
|
|
|
404
632
|
// src/features/auth/jwt.ts
|
|
405
633
|
import { createHash } from "crypto";
|
|
@@ -460,11 +688,18 @@ var ErrorCode = z2.enum([
|
|
|
460
688
|
"passkey_not_found",
|
|
461
689
|
"session_expired",
|
|
462
690
|
"invalid_origin",
|
|
691
|
+
"api_key_not_found",
|
|
692
|
+
"api_key_revoked",
|
|
693
|
+
"insufficient_permissions",
|
|
463
694
|
// Vault errors
|
|
464
695
|
"vault_not_found",
|
|
465
696
|
"vault_already_exists",
|
|
466
697
|
"vault_conflict",
|
|
467
698
|
"invalid_vault_data",
|
|
699
|
+
// Document errors
|
|
700
|
+
"document_not_found",
|
|
701
|
+
"document_conflict",
|
|
702
|
+
"document_already_exists",
|
|
468
703
|
// Validation errors
|
|
469
704
|
"validation_error",
|
|
470
705
|
"invalid_request",
|
|
@@ -488,29 +723,37 @@ var errors = {
|
|
|
488
723
|
passkey_not_found: { code: "passkey_not_found", message: "Passkey not found" },
|
|
489
724
|
session_expired: { code: "session_expired", message: "Session expired" },
|
|
490
725
|
invalid_origin: { code: "invalid_origin", message: "Origin not allowed" },
|
|
726
|
+
api_key_not_found: { code: "api_key_not_found", message: "API key not found" },
|
|
727
|
+
api_key_revoked: { code: "api_key_revoked", message: "API key has been revoked" },
|
|
728
|
+
insufficient_permissions: { code: "insufficient_permissions", message: "Insufficient permissions" },
|
|
491
729
|
// Vault errors
|
|
492
730
|
vault_not_found: { code: "vault_not_found", message: "Vault not found" },
|
|
493
|
-
vault_already_exists: (name) => ({
|
|
494
|
-
code: "vault_already_exists",
|
|
495
|
-
message: `Vault "${name}" already exists`
|
|
496
|
-
}),
|
|
497
731
|
vault_conflict: { code: "vault_conflict", message: "Version conflict - vault has been modified. Please refresh and retry." },
|
|
498
732
|
invalid_vault_data: { code: "invalid_vault_data", message: "Invalid vault data" },
|
|
733
|
+
// Document errors
|
|
734
|
+
document_not_found: { code: "document_not_found", message: "Document not found" },
|
|
735
|
+
document_conflict: { code: "document_conflict", message: "Version conflict - document has been modified. Please refresh and retry." },
|
|
736
|
+
document_already_exists: { code: "document_already_exists", message: "Document already exists" },
|
|
499
737
|
// Validation errors
|
|
500
|
-
validation_error: (details) => ({
|
|
501
|
-
code: "validation_error",
|
|
502
|
-
message: details
|
|
503
|
-
}),
|
|
504
738
|
invalid_request: { code: "invalid_request", message: "Invalid request" },
|
|
505
739
|
// Server errors
|
|
506
740
|
internal_error: { code: "internal_error", message: "Internal server error" }
|
|
507
741
|
};
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
742
|
+
var errorBuilders = {
|
|
743
|
+
vaultAlreadyExists: (name) => ({
|
|
744
|
+
code: "vault_already_exists",
|
|
745
|
+
message: `Vault "${name}" already exists`
|
|
746
|
+
}),
|
|
747
|
+
validationError: (details) => ({
|
|
748
|
+
code: "validation_error",
|
|
749
|
+
message: details
|
|
750
|
+
})
|
|
751
|
+
};
|
|
752
|
+
function getError(code) {
|
|
753
|
+
if (code in errors) {
|
|
754
|
+
return errors[code];
|
|
512
755
|
}
|
|
513
|
-
return
|
|
756
|
+
return { code, message: code.replace(/_/g, " ") };
|
|
514
757
|
}
|
|
515
758
|
|
|
516
759
|
// src/features/auth/middleware.ts
|
|
@@ -538,7 +781,8 @@ var optionalAuthMiddleware = createMiddleware(async (c, next) => {
|
|
|
538
781
|
uid: session.user.uid,
|
|
539
782
|
email: session.user.email
|
|
540
783
|
},
|
|
541
|
-
sessionId: session.id
|
|
784
|
+
sessionId: session.id,
|
|
785
|
+
authType: "jwt"
|
|
542
786
|
});
|
|
543
787
|
return next();
|
|
544
788
|
});
|
|
@@ -548,6 +792,44 @@ var requireAuthMiddleware = createMiddleware(async (c, next) => {
|
|
|
548
792
|
throw new ApiException(getError("unauthorized"), 401);
|
|
549
793
|
}
|
|
550
794
|
const token = authHeader.slice(7);
|
|
795
|
+
if (token.startsWith("ulk_")) {
|
|
796
|
+
if (token.length !== 52) {
|
|
797
|
+
throw new ApiException(getError("unauthorized"), 401);
|
|
798
|
+
}
|
|
799
|
+
const keyHash = hashToken(token);
|
|
800
|
+
const keyRecord = getApiKeyByHash(keyHash);
|
|
801
|
+
if (!keyRecord) {
|
|
802
|
+
throw new ApiException(getError("unauthorized"), 401);
|
|
803
|
+
}
|
|
804
|
+
if (keyRecord.revokedAt !== null) {
|
|
805
|
+
throw new ApiException(getError("api_key_revoked"), 401);
|
|
806
|
+
}
|
|
807
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
808
|
+
if (keyRecord.expiresAt !== null && keyRecord.expiresAt <= now) {
|
|
809
|
+
throw new ApiException(getError("unauthorized"), 401);
|
|
810
|
+
}
|
|
811
|
+
setImmediate(() => {
|
|
812
|
+
try {
|
|
813
|
+
updateApiKeyLastUsed(keyRecord.uid);
|
|
814
|
+
} catch {
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
const scopes = parseApiKeyScopes(keyRecord);
|
|
818
|
+
c.set("session", {
|
|
819
|
+
user: {
|
|
820
|
+
id: keyRecord.user.id,
|
|
821
|
+
uid: keyRecord.user.uid,
|
|
822
|
+
email: keyRecord.user.email
|
|
823
|
+
},
|
|
824
|
+
sessionId: keyRecord.id,
|
|
825
|
+
authType: "apiKey",
|
|
826
|
+
apiKey: {
|
|
827
|
+
uid: keyRecord.uid,
|
|
828
|
+
...scopes
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
return next();
|
|
832
|
+
}
|
|
551
833
|
const payload = await verifyToken(token);
|
|
552
834
|
if (!payload?.sub) {
|
|
553
835
|
throw new ApiException(getError("unauthorized"), 401);
|
|
@@ -563,10 +845,39 @@ var requireAuthMiddleware = createMiddleware(async (c, next) => {
|
|
|
563
845
|
uid: session.user.uid,
|
|
564
846
|
email: session.user.email
|
|
565
847
|
},
|
|
566
|
-
sessionId: session.id
|
|
848
|
+
sessionId: session.id,
|
|
849
|
+
authType: "jwt"
|
|
850
|
+
});
|
|
851
|
+
return next();
|
|
852
|
+
});
|
|
853
|
+
var requirePermission = (permission) => {
|
|
854
|
+
return createMiddleware(async (c, next) => {
|
|
855
|
+
const session = c.get("session");
|
|
856
|
+
if (!session.apiKey) return next();
|
|
857
|
+
if (!session.apiKey.permissions.includes(permission)) {
|
|
858
|
+
throw new ApiException(getError("insufficient_permissions"), 403);
|
|
859
|
+
}
|
|
860
|
+
return next();
|
|
567
861
|
});
|
|
862
|
+
};
|
|
863
|
+
var requireVaultAccess = createMiddleware(async (c, next) => {
|
|
864
|
+
const session = c.get("session");
|
|
865
|
+
if (!session.apiKey) return next();
|
|
866
|
+
if (session.apiKey.vaultUids === null) return next();
|
|
867
|
+
const vaultUid = c.req.param("vaultUid") ?? c.req.param("uid");
|
|
868
|
+
if (!vaultUid) return next();
|
|
869
|
+
if (!session.apiKey.vaultUids.includes(vaultUid)) {
|
|
870
|
+
throw new ApiException(getError("vault_not_found"), 404);
|
|
871
|
+
}
|
|
568
872
|
return next();
|
|
569
873
|
});
|
|
874
|
+
function assertCollectionAccess(session, collection) {
|
|
875
|
+
if (!session.apiKey) return;
|
|
876
|
+
if (session.apiKey.collections === null) return;
|
|
877
|
+
if (!session.apiKey.collections.includes(collection)) {
|
|
878
|
+
throw new ApiException(getError("insufficient_permissions"), 403);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
570
881
|
|
|
571
882
|
// src/api/schemas.ts
|
|
572
883
|
import { z as z3 } from "zod";
|
|
@@ -633,6 +944,70 @@ var VaultResponse = z3.object({
|
|
|
633
944
|
var VaultsListResponse = z3.object({
|
|
634
945
|
vaults: z3.array(VaultResponse)
|
|
635
946
|
});
|
|
947
|
+
var COLLECTION_NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
948
|
+
var HMAC_HEX_RE = /^[0-9a-f]{64}$/;
|
|
949
|
+
var CreateDocumentRequest = z3.object({
|
|
950
|
+
collection: z3.string().min(1).max(255).regex(COLLECTION_NAME_RE, "Collection name must be alphanumeric (hyphens and underscores allowed)"),
|
|
951
|
+
data: z3.string().max(MAX_DATA_SIZE, `Data must not exceed ${MAX_DATA_SIZE} bytes`).regex(BASE64_RE, "Data must be valid base64"),
|
|
952
|
+
hmac: z3.string().regex(HMAC_HEX_RE, "HMAC must be a valid SHA-256 hex string (64 characters)").optional()
|
|
953
|
+
});
|
|
954
|
+
var UpdateDocumentRequest = z3.object({
|
|
955
|
+
data: z3.string().max(MAX_DATA_SIZE, `Data must not exceed ${MAX_DATA_SIZE} bytes`).regex(BASE64_RE, "Data must be valid base64"),
|
|
956
|
+
hmac: z3.string().regex(HMAC_HEX_RE, "HMAC must be a valid SHA-256 hex string (64 characters)").optional(),
|
|
957
|
+
version: z3.number().optional()
|
|
958
|
+
});
|
|
959
|
+
var DocumentResponse = z3.object({
|
|
960
|
+
uid: z3.string(),
|
|
961
|
+
collection: z3.string(),
|
|
962
|
+
data: z3.string(),
|
|
963
|
+
hmac: z3.string().nullable(),
|
|
964
|
+
version: z3.number(),
|
|
965
|
+
createdAt: z3.number(),
|
|
966
|
+
updatedAt: z3.number(),
|
|
967
|
+
deletedAt: z3.number().nullable()
|
|
968
|
+
});
|
|
969
|
+
var DocumentListResponse = z3.object({
|
|
970
|
+
documents: z3.array(DocumentResponse)
|
|
971
|
+
});
|
|
972
|
+
var DocumentSyncResponse = z3.object({
|
|
973
|
+
documents: z3.array(DocumentResponse),
|
|
974
|
+
syncedAt: z3.number()
|
|
975
|
+
});
|
|
976
|
+
var CreateApiKeyRequest = z3.object({
|
|
977
|
+
name: z3.string().min(1).max(255),
|
|
978
|
+
permissions: z3.array(z3.enum(["read", "write", "delete"])).optional(),
|
|
979
|
+
vaultUids: z3.array(z3.string()).optional(),
|
|
980
|
+
collections: z3.array(z3.string()).optional(),
|
|
981
|
+
expiresAt: z3.number().optional()
|
|
982
|
+
});
|
|
983
|
+
var ApiKeyResponse = z3.object({
|
|
984
|
+
uid: z3.string(),
|
|
985
|
+
name: z3.string(),
|
|
986
|
+
keyPrefix: z3.string(),
|
|
987
|
+
permissions: z3.array(z3.string()),
|
|
988
|
+
vaultUids: z3.array(z3.string()).nullable(),
|
|
989
|
+
collections: z3.array(z3.string()).nullable(),
|
|
990
|
+
expiresAt: z3.number().nullable(),
|
|
991
|
+
lastUsedAt: z3.number().nullable(),
|
|
992
|
+
createdAt: z3.number(),
|
|
993
|
+
revokedAt: z3.number().nullable()
|
|
994
|
+
});
|
|
995
|
+
var ApiKeyCreatedResponse = ApiKeyResponse.extend({
|
|
996
|
+
key: z3.string()
|
|
997
|
+
});
|
|
998
|
+
var ApiKeysListResponse = z3.object({
|
|
999
|
+
apiKeys: z3.array(ApiKeyResponse)
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
// src/features/auth/key-gen.ts
|
|
1003
|
+
import { randomBytes } from "crypto";
|
|
1004
|
+
function generateApiKey() {
|
|
1005
|
+
const randomPart = randomBytes(24).toString("hex");
|
|
1006
|
+
return `ulk_${randomPart}`;
|
|
1007
|
+
}
|
|
1008
|
+
function getKeyPrefix(key) {
|
|
1009
|
+
return key.substring(0, 8);
|
|
1010
|
+
}
|
|
636
1011
|
|
|
637
1012
|
// src/api/auth/passkey.ts
|
|
638
1013
|
import { Hono } from "hono";
|
|
@@ -1058,6 +1433,12 @@ var zkcRouter = new Hono2().post(
|
|
|
1058
1433
|
);
|
|
1059
1434
|
|
|
1060
1435
|
// src/api/auth/router.ts
|
|
1436
|
+
var MAX_API_KEYS_PER_USER = 50;
|
|
1437
|
+
function requireJwtSession(session) {
|
|
1438
|
+
if (session.apiKey) {
|
|
1439
|
+
throw new ApiException(errors.insufficient_permissions, 403);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1061
1442
|
var authRouter = new Hono3().post(
|
|
1062
1443
|
"/email/register",
|
|
1063
1444
|
zValidator3("json", EmailRegisterRequest),
|
|
@@ -1154,6 +1535,82 @@ var authRouter = new Hono3().post(
|
|
|
1154
1535
|
}
|
|
1155
1536
|
return c.json({ success: true });
|
|
1156
1537
|
}
|
|
1538
|
+
).post(
|
|
1539
|
+
"/api-keys",
|
|
1540
|
+
requireAuthMiddleware,
|
|
1541
|
+
zValidator3("json", CreateApiKeyRequest),
|
|
1542
|
+
async (c) => {
|
|
1543
|
+
const session = c.get("session");
|
|
1544
|
+
requireJwtSession(session);
|
|
1545
|
+
const existing = listApiKeysByUserId(session.user.id);
|
|
1546
|
+
if (existing.length >= MAX_API_KEYS_PER_USER) {
|
|
1547
|
+
throw new ApiException(
|
|
1548
|
+
{ code: "rate_limited", message: `Maximum ${MAX_API_KEYS_PER_USER} API keys per user` },
|
|
1549
|
+
429
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
const input = c.req.valid("json");
|
|
1553
|
+
const key = generateApiKey();
|
|
1554
|
+
const keyHash = hashToken(key);
|
|
1555
|
+
const apiKey = createApiKey({
|
|
1556
|
+
userId: session.user.id,
|
|
1557
|
+
name: input.name,
|
|
1558
|
+
keyHash,
|
|
1559
|
+
keyPrefix: getKeyPrefix(key),
|
|
1560
|
+
permissions: input.permissions,
|
|
1561
|
+
vaultUids: input.vaultUids,
|
|
1562
|
+
collections: input.collections,
|
|
1563
|
+
expiresAt: input.expiresAt
|
|
1564
|
+
});
|
|
1565
|
+
const scopes = parseApiKeyScopes(apiKey);
|
|
1566
|
+
return c.json({
|
|
1567
|
+
uid: apiKey.uid,
|
|
1568
|
+
name: apiKey.name,
|
|
1569
|
+
key,
|
|
1570
|
+
// Only returned on creation!
|
|
1571
|
+
keyPrefix: apiKey.keyPrefix,
|
|
1572
|
+
...scopes,
|
|
1573
|
+
expiresAt: apiKey.expiresAt,
|
|
1574
|
+
lastUsedAt: apiKey.lastUsedAt,
|
|
1575
|
+
createdAt: apiKey.createdAt,
|
|
1576
|
+
revokedAt: apiKey.revokedAt
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
).get(
|
|
1580
|
+
"/api-keys",
|
|
1581
|
+
requireAuthMiddleware,
|
|
1582
|
+
(c) => {
|
|
1583
|
+
const session = c.get("session");
|
|
1584
|
+
requireJwtSession(session);
|
|
1585
|
+
const apiKeys = listApiKeysByUserId(session.user.id);
|
|
1586
|
+
return c.json({
|
|
1587
|
+
apiKeys: apiKeys.map((key) => {
|
|
1588
|
+
const scopes = parseApiKeyScopes(key);
|
|
1589
|
+
return {
|
|
1590
|
+
uid: key.uid,
|
|
1591
|
+
name: key.name,
|
|
1592
|
+
keyPrefix: key.keyPrefix,
|
|
1593
|
+
...scopes,
|
|
1594
|
+
expiresAt: key.expiresAt,
|
|
1595
|
+
lastUsedAt: key.lastUsedAt,
|
|
1596
|
+
createdAt: key.createdAt,
|
|
1597
|
+
revokedAt: key.revokedAt
|
|
1598
|
+
};
|
|
1599
|
+
})
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
).delete(
|
|
1603
|
+
"/api-keys/:uid",
|
|
1604
|
+
requireAuthMiddleware,
|
|
1605
|
+
(c) => {
|
|
1606
|
+
const session = c.get("session");
|
|
1607
|
+
requireJwtSession(session);
|
|
1608
|
+
const uid = c.req.param("uid");
|
|
1609
|
+
if (!revokeApiKey(uid, session.user.id)) {
|
|
1610
|
+
throw new ApiException(errors.api_key_not_found, 404);
|
|
1611
|
+
}
|
|
1612
|
+
return c.json({ success: true });
|
|
1613
|
+
}
|
|
1157
1614
|
).route("/passkey", passkeyRouter).route("/zkc", zkcRouter);
|
|
1158
1615
|
|
|
1159
1616
|
// src/api/vault/router.ts
|
|
@@ -1172,8 +1629,8 @@ function toVaultResponse(vault) {
|
|
|
1172
1629
|
};
|
|
1173
1630
|
}
|
|
1174
1631
|
var VaultService = class {
|
|
1175
|
-
constructor(
|
|
1176
|
-
this.vaultRepo =
|
|
1632
|
+
constructor(vaultRepo3) {
|
|
1633
|
+
this.vaultRepo = vaultRepo3;
|
|
1177
1634
|
}
|
|
1178
1635
|
/**
|
|
1179
1636
|
* List all vaults for a user
|
|
@@ -1210,7 +1667,7 @@ var VaultService = class {
|
|
|
1210
1667
|
createVault(userId, data) {
|
|
1211
1668
|
const existing = this.vaultRepo.findByName(data.name, userId);
|
|
1212
1669
|
if (existing) {
|
|
1213
|
-
throw new ApiException(
|
|
1670
|
+
throw new ApiException(errorBuilders.vaultAlreadyExists(data.name), 409);
|
|
1214
1671
|
}
|
|
1215
1672
|
const vault = this.vaultRepo.create({
|
|
1216
1673
|
userId,
|
|
@@ -1275,19 +1732,32 @@ var vaultRepo = new VaultRepository();
|
|
|
1275
1732
|
var vaultService = new VaultService(vaultRepo);
|
|
1276
1733
|
var vaultRouter = new Hono4().use("/*", requireAuthMiddleware).get(
|
|
1277
1734
|
"/",
|
|
1735
|
+
requirePermission("read"),
|
|
1278
1736
|
(c) => {
|
|
1279
1737
|
const session = c.get("session");
|
|
1280
|
-
|
|
1738
|
+
const result = vaultService.listVaults(session.user.id);
|
|
1739
|
+
if (session.apiKey?.vaultUids) {
|
|
1740
|
+
const allowed = new Set(session.apiKey.vaultUids);
|
|
1741
|
+
result.vaults = result.vaults.filter((v) => allowed.has(v.uid));
|
|
1742
|
+
}
|
|
1743
|
+
return c.json(result);
|
|
1281
1744
|
}
|
|
1282
1745
|
).get(
|
|
1283
1746
|
"/by-name/:name",
|
|
1747
|
+
requirePermission("read"),
|
|
1284
1748
|
(c) => {
|
|
1285
1749
|
const session = c.get("session");
|
|
1286
1750
|
const { name } = c.req.param();
|
|
1287
|
-
|
|
1751
|
+
const vault = vaultService.getVaultByName(name, session.user.id);
|
|
1752
|
+
if (session.apiKey?.vaultUids && !session.apiKey.vaultUids.includes(vault.uid)) {
|
|
1753
|
+
throw new ApiException(errors.vault_not_found, 404);
|
|
1754
|
+
}
|
|
1755
|
+
return c.json(vault);
|
|
1288
1756
|
}
|
|
1289
1757
|
).get(
|
|
1290
1758
|
"/:uid",
|
|
1759
|
+
requirePermission("read"),
|
|
1760
|
+
requireVaultAccess,
|
|
1291
1761
|
(c) => {
|
|
1292
1762
|
const session = c.get("session");
|
|
1293
1763
|
const { uid } = c.req.param();
|
|
@@ -1295,6 +1765,7 @@ var vaultRouter = new Hono4().use("/*", requireAuthMiddleware).get(
|
|
|
1295
1765
|
}
|
|
1296
1766
|
).post(
|
|
1297
1767
|
"/",
|
|
1768
|
+
requirePermission("write"),
|
|
1298
1769
|
zValidator4("json", CreateVaultRequest),
|
|
1299
1770
|
(c) => {
|
|
1300
1771
|
const session = c.get("session");
|
|
@@ -1306,6 +1777,8 @@ var vaultRouter = new Hono4().use("/*", requireAuthMiddleware).get(
|
|
|
1306
1777
|
}
|
|
1307
1778
|
).put(
|
|
1308
1779
|
"/:uid",
|
|
1780
|
+
requirePermission("write"),
|
|
1781
|
+
requireVaultAccess,
|
|
1309
1782
|
zValidator4("json", UpdateVaultRequest),
|
|
1310
1783
|
(c) => {
|
|
1311
1784
|
const session = c.get("session");
|
|
@@ -1317,6 +1790,8 @@ var vaultRouter = new Hono4().use("/*", requireAuthMiddleware).get(
|
|
|
1317
1790
|
}
|
|
1318
1791
|
).delete(
|
|
1319
1792
|
"/:uid",
|
|
1793
|
+
requirePermission("delete"),
|
|
1794
|
+
requireVaultAccess,
|
|
1320
1795
|
(c) => {
|
|
1321
1796
|
const session = c.get("session");
|
|
1322
1797
|
const { uid } = c.req.param();
|
|
@@ -1324,6 +1799,245 @@ var vaultRouter = new Hono4().use("/*", requireAuthMiddleware).get(
|
|
|
1324
1799
|
}
|
|
1325
1800
|
);
|
|
1326
1801
|
|
|
1802
|
+
// src/api/document/router.ts
|
|
1803
|
+
import { Hono as Hono5 } from "hono";
|
|
1804
|
+
import { zValidator as zValidator5 } from "@hono/zod-validator";
|
|
1805
|
+
import { z as z6 } from "zod";
|
|
1806
|
+
|
|
1807
|
+
// src/services/document-service.ts
|
|
1808
|
+
function toDocumentResponse(document) {
|
|
1809
|
+
return {
|
|
1810
|
+
uid: document.uid,
|
|
1811
|
+
collection: document.collection,
|
|
1812
|
+
data: document.data,
|
|
1813
|
+
hmac: document.hmac,
|
|
1814
|
+
version: document.version,
|
|
1815
|
+
createdAt: document.createdAt,
|
|
1816
|
+
updatedAt: document.updatedAt,
|
|
1817
|
+
deletedAt: document.deletedAt
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
var DocumentService = class {
|
|
1821
|
+
constructor(documentRepo2, vaultRepo3) {
|
|
1822
|
+
this.documentRepo = documentRepo2;
|
|
1823
|
+
this.vaultRepo = vaultRepo3;
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Verify vault exists and belongs to user.
|
|
1827
|
+
* Only needed for create (to prevent orphan documents referencing non-existent vaults).
|
|
1828
|
+
* Read/update/delete queries already filter by userId — no vault exists = empty result.
|
|
1829
|
+
*/
|
|
1830
|
+
verifyVaultOwnership(vaultUid, userId) {
|
|
1831
|
+
const vault = this.vaultRepo.findByUid(vaultUid, userId);
|
|
1832
|
+
if (!vault) {
|
|
1833
|
+
throw new ApiException(errors.vault_not_found, 404);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
/**
|
|
1837
|
+
* Create a new document
|
|
1838
|
+
*/
|
|
1839
|
+
createDocument(userId, vaultUid, data) {
|
|
1840
|
+
this.verifyVaultOwnership(vaultUid, userId);
|
|
1841
|
+
const document = this.documentRepo.create({
|
|
1842
|
+
vaultUid,
|
|
1843
|
+
userId,
|
|
1844
|
+
collection: data.collection,
|
|
1845
|
+
data: data.data,
|
|
1846
|
+
hmac: data.hmac
|
|
1847
|
+
});
|
|
1848
|
+
return toDocumentResponse(document);
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Get document by UID
|
|
1852
|
+
*/
|
|
1853
|
+
getDocument(uid, vaultUid, userId) {
|
|
1854
|
+
const document = this.documentRepo.findByUid(uid, vaultUid, userId);
|
|
1855
|
+
if (!document) {
|
|
1856
|
+
throw new ApiException(errors.document_not_found, 404);
|
|
1857
|
+
}
|
|
1858
|
+
return toDocumentResponse(document);
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* List documents in a vault
|
|
1862
|
+
*/
|
|
1863
|
+
listDocuments(vaultUid, userId, opts) {
|
|
1864
|
+
const documents = this.documentRepo.list(vaultUid, userId, opts);
|
|
1865
|
+
return {
|
|
1866
|
+
documents: documents.map(toDocumentResponse)
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Update a document
|
|
1871
|
+
*/
|
|
1872
|
+
updateDocument(uid, vaultUid, userId, data) {
|
|
1873
|
+
const { document, conflict } = this.documentRepo.update(uid, vaultUid, userId, data);
|
|
1874
|
+
if (!document) {
|
|
1875
|
+
if (conflict) {
|
|
1876
|
+
throw new ApiException(errors.document_conflict, 409);
|
|
1877
|
+
}
|
|
1878
|
+
throw new ApiException(errors.document_not_found, 404);
|
|
1879
|
+
}
|
|
1880
|
+
return toDocumentResponse(document);
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Soft delete a document
|
|
1884
|
+
*/
|
|
1885
|
+
deleteDocument(uid, vaultUid, userId) {
|
|
1886
|
+
const document = this.documentRepo.softDelete(uid, vaultUid, userId);
|
|
1887
|
+
if (!document) {
|
|
1888
|
+
throw new ApiException(errors.document_not_found, 404);
|
|
1889
|
+
}
|
|
1890
|
+
return { success: true };
|
|
1891
|
+
}
|
|
1892
|
+
/**
|
|
1893
|
+
* Delta sync - get documents modified since timestamp
|
|
1894
|
+
*/
|
|
1895
|
+
syncDocuments(vaultUid, userId, since) {
|
|
1896
|
+
const documents = this.documentRepo.getSince(vaultUid, userId, since);
|
|
1897
|
+
return {
|
|
1898
|
+
documents: documents.map(toDocumentResponse),
|
|
1899
|
+
syncedAt: Math.floor(Date.now() / 1e3)
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
|
|
1904
|
+
// src/repositories/document-repository.ts
|
|
1905
|
+
var DocumentRepository = class {
|
|
1906
|
+
create(document) {
|
|
1907
|
+
return createDocument(document);
|
|
1908
|
+
}
|
|
1909
|
+
findByUid(uid, vaultUid, userId) {
|
|
1910
|
+
return getDocumentByUid(uid, vaultUid, userId);
|
|
1911
|
+
}
|
|
1912
|
+
list(vaultUid, userId, opts) {
|
|
1913
|
+
return listDocuments(vaultUid, userId, opts);
|
|
1914
|
+
}
|
|
1915
|
+
update(uid, vaultUid, userId, data) {
|
|
1916
|
+
return updateDocument(uid, vaultUid, userId, data);
|
|
1917
|
+
}
|
|
1918
|
+
softDelete(uid, vaultUid, userId) {
|
|
1919
|
+
return softDeleteDocument(uid, vaultUid, userId);
|
|
1920
|
+
}
|
|
1921
|
+
getSince(vaultUid, userId, since) {
|
|
1922
|
+
return getDocumentsSince(vaultUid, userId, since);
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
|
|
1926
|
+
// src/api/document/router.ts
|
|
1927
|
+
var documentRepo = new DocumentRepository();
|
|
1928
|
+
var vaultRepo2 = new VaultRepository();
|
|
1929
|
+
var documentService = new DocumentService(documentRepo, vaultRepo2);
|
|
1930
|
+
var ListQuerySchema = z6.object({
|
|
1931
|
+
collection: z6.string().optional(),
|
|
1932
|
+
since: z6.coerce.number().optional(),
|
|
1933
|
+
includeDeleted: z6.enum(["true", "false"]).optional().transform((val) => val === "true"),
|
|
1934
|
+
limit: z6.coerce.number().optional(),
|
|
1935
|
+
offset: z6.coerce.number().optional()
|
|
1936
|
+
});
|
|
1937
|
+
var SyncQuerySchema = z6.object({
|
|
1938
|
+
since: z6.coerce.number()
|
|
1939
|
+
});
|
|
1940
|
+
var documentRouter = new Hono5().use("/*", requireAuthMiddleware).get(
|
|
1941
|
+
"/vault/:vaultUid/documents",
|
|
1942
|
+
requirePermission("read"),
|
|
1943
|
+
requireVaultAccess,
|
|
1944
|
+
zValidator5("query", ListQuerySchema),
|
|
1945
|
+
(c) => {
|
|
1946
|
+
const session = c.get("session");
|
|
1947
|
+
const { vaultUid } = c.req.param();
|
|
1948
|
+
const query = c.req.valid("query");
|
|
1949
|
+
if (query.collection) {
|
|
1950
|
+
assertCollectionAccess(session, query.collection);
|
|
1951
|
+
}
|
|
1952
|
+
const result = documentService.listDocuments(vaultUid, session.user.id, {
|
|
1953
|
+
collection: query.collection,
|
|
1954
|
+
since: query.since,
|
|
1955
|
+
includeDeleted: query.includeDeleted,
|
|
1956
|
+
limit: query.limit,
|
|
1957
|
+
offset: query.offset
|
|
1958
|
+
});
|
|
1959
|
+
if (!query.collection && session.apiKey?.collections) {
|
|
1960
|
+
const allowed = new Set(session.apiKey.collections);
|
|
1961
|
+
result.documents = result.documents.filter((doc) => allowed.has(doc.collection));
|
|
1962
|
+
}
|
|
1963
|
+
return c.json(result);
|
|
1964
|
+
}
|
|
1965
|
+
).get(
|
|
1966
|
+
"/vault/:vaultUid/documents/sync",
|
|
1967
|
+
requirePermission("read"),
|
|
1968
|
+
requireVaultAccess,
|
|
1969
|
+
zValidator5("query", SyncQuerySchema),
|
|
1970
|
+
(c) => {
|
|
1971
|
+
const session = c.get("session");
|
|
1972
|
+
const { vaultUid } = c.req.param();
|
|
1973
|
+
const { since } = c.req.valid("query");
|
|
1974
|
+
const result = documentService.syncDocuments(vaultUid, session.user.id, since);
|
|
1975
|
+
if (session.apiKey?.collections) {
|
|
1976
|
+
const allowed = new Set(session.apiKey.collections);
|
|
1977
|
+
result.documents = result.documents.filter((doc) => allowed.has(doc.collection));
|
|
1978
|
+
}
|
|
1979
|
+
return c.json(result);
|
|
1980
|
+
}
|
|
1981
|
+
).get(
|
|
1982
|
+
"/vault/:vaultUid/documents/:uid",
|
|
1983
|
+
requirePermission("read"),
|
|
1984
|
+
requireVaultAccess,
|
|
1985
|
+
(c) => {
|
|
1986
|
+
const session = c.get("session");
|
|
1987
|
+
const { vaultUid, uid } = c.req.param();
|
|
1988
|
+
return c.json(
|
|
1989
|
+
documentService.getDocument(uid, vaultUid, session.user.id)
|
|
1990
|
+
);
|
|
1991
|
+
}
|
|
1992
|
+
).post(
|
|
1993
|
+
"/vault/:vaultUid/documents",
|
|
1994
|
+
requirePermission("write"),
|
|
1995
|
+
requireVaultAccess,
|
|
1996
|
+
zValidator5("json", CreateDocumentRequest),
|
|
1997
|
+
(c) => {
|
|
1998
|
+
const session = c.get("session");
|
|
1999
|
+
const { vaultUid } = c.req.param();
|
|
2000
|
+
const { collection, data, hmac } = c.req.valid("json");
|
|
2001
|
+
assertCollectionAccess(session, collection);
|
|
2002
|
+
return c.json(
|
|
2003
|
+
documentService.createDocument(session.user.id, vaultUid, {
|
|
2004
|
+
collection,
|
|
2005
|
+
data,
|
|
2006
|
+
hmac
|
|
2007
|
+
}),
|
|
2008
|
+
201
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
).put(
|
|
2012
|
+
"/vault/:vaultUid/documents/:uid",
|
|
2013
|
+
requirePermission("write"),
|
|
2014
|
+
requireVaultAccess,
|
|
2015
|
+
zValidator5("json", UpdateDocumentRequest),
|
|
2016
|
+
(c) => {
|
|
2017
|
+
const session = c.get("session");
|
|
2018
|
+
const { vaultUid, uid } = c.req.param();
|
|
2019
|
+
const { data, hmac, version } = c.req.valid("json");
|
|
2020
|
+
return c.json(
|
|
2021
|
+
documentService.updateDocument(uid, vaultUid, session.user.id, {
|
|
2022
|
+
data,
|
|
2023
|
+
hmac,
|
|
2024
|
+
version
|
|
2025
|
+
})
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
).delete(
|
|
2029
|
+
"/vault/:vaultUid/documents/:uid",
|
|
2030
|
+
requirePermission("delete"),
|
|
2031
|
+
requireVaultAccess,
|
|
2032
|
+
(c) => {
|
|
2033
|
+
const session = c.get("session");
|
|
2034
|
+
const { vaultUid, uid } = c.req.param();
|
|
2035
|
+
return c.json(
|
|
2036
|
+
documentService.deleteDocument(uid, vaultUid, session.user.id)
|
|
2037
|
+
);
|
|
2038
|
+
}
|
|
2039
|
+
);
|
|
2040
|
+
|
|
1327
2041
|
// src/features/auth/rate-limit.ts
|
|
1328
2042
|
import { createMiddleware as createMiddleware2 } from "hono/factory";
|
|
1329
2043
|
var DEFAULT_CONFIG = { max: 100, windowMs: 6e4 };
|
|
@@ -1386,47 +2100,6 @@ function rateLimit(config = {}) {
|
|
|
1386
2100
|
});
|
|
1387
2101
|
}
|
|
1388
2102
|
|
|
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
2103
|
// src/app.ts
|
|
1431
2104
|
var errorHandler = (error, c) => {
|
|
1432
2105
|
const requestId = c.req.header("x-request-id") ?? crypto.randomUUID();
|
|
@@ -1457,7 +2130,7 @@ var errorHandler = (error, c) => {
|
|
|
1457
2130
|
);
|
|
1458
2131
|
};
|
|
1459
2132
|
function createApp() {
|
|
1460
|
-
const app = new
|
|
2133
|
+
const app = new Hono6();
|
|
1461
2134
|
app.use("*", bodyLimit({ maxSize: 11 * 1024 * 1024 }));
|
|
1462
2135
|
app.use(
|
|
1463
2136
|
"*",
|
|
@@ -1489,11 +2162,15 @@ function createApp() {
|
|
|
1489
2162
|
})
|
|
1490
2163
|
);
|
|
1491
2164
|
app.use("*", rateLimit({ max: 100, windowMs: 6e4 }));
|
|
1492
|
-
app.use("*", csrfProtection);
|
|
1493
2165
|
app.get("/health", (c) => c.json({ status: "ok", timestamp: Date.now() }));
|
|
2166
|
+
try {
|
|
2167
|
+
deleteExpiredApiKeys();
|
|
2168
|
+
} catch {
|
|
2169
|
+
}
|
|
1494
2170
|
app.use("/auth/*", rateLimit({ max: 10, windowMs: 6e4 }));
|
|
1495
2171
|
app.route("/auth", authRouter);
|
|
1496
2172
|
app.route("/vault", vaultRouter);
|
|
2173
|
+
app.route("/", documentRouter);
|
|
1497
2174
|
app.onError(errorHandler);
|
|
1498
2175
|
app.notFound((c) => {
|
|
1499
2176
|
const requestId = c.req.header("x-request-id") ?? crypto.randomUUID();
|
|
@@ -1506,8 +2183,16 @@ function createApp() {
|
|
|
1506
2183
|
}
|
|
1507
2184
|
export {
|
|
1508
2185
|
ApiException,
|
|
2186
|
+
ApiKeyCreatedResponse,
|
|
2187
|
+
ApiKeyResponse,
|
|
2188
|
+
ApiKeysListResponse,
|
|
1509
2189
|
AuthResponse,
|
|
2190
|
+
CreateApiKeyRequest,
|
|
2191
|
+
CreateDocumentRequest,
|
|
1510
2192
|
CreateVaultRequest,
|
|
2193
|
+
DocumentListResponse,
|
|
2194
|
+
DocumentResponse,
|
|
2195
|
+
DocumentSyncResponse,
|
|
1511
2196
|
EmailLoginRequest,
|
|
1512
2197
|
EmailRegisterRequest,
|
|
1513
2198
|
MeResponse,
|
|
@@ -1516,6 +2201,7 @@ export {
|
|
|
1516
2201
|
PasskeyRegisterOptionsRequest,
|
|
1517
2202
|
PasskeyRegisterVerifyRequest,
|
|
1518
2203
|
RefreshResponse,
|
|
2204
|
+
UpdateDocumentRequest,
|
|
1519
2205
|
UpdateVaultRequest,
|
|
1520
2206
|
UserResponse,
|
|
1521
2207
|
VaultResponse,
|