@lastshotlabs/bunshot 0.0.20 → 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3035 -1249
- package/dist/adapters/localStorage.d.ts +6 -0
- package/dist/adapters/localStorage.js +44 -0
- package/dist/adapters/memoryAuth.d.ts +7 -0
- package/dist/adapters/memoryAuth.js +144 -0
- package/dist/adapters/memoryStorage.d.ts +3 -0
- package/dist/adapters/memoryStorage.js +44 -0
- package/dist/adapters/mongoAuth.js +120 -0
- package/dist/adapters/s3Storage.d.ts +14 -0
- package/dist/adapters/s3Storage.js +126 -0
- package/dist/adapters/sqliteAuth.d.ts +7 -0
- package/dist/adapters/sqliteAuth.js +199 -0
- package/dist/app.d.ts +100 -3
- package/dist/app.js +248 -47
- package/dist/cli.js +118 -38
- package/dist/index.d.ts +49 -7
- package/dist/index.js +35 -5
- package/dist/lib/HttpError.d.ts +5 -0
- package/dist/lib/HttpError.js +7 -0
- package/dist/lib/appConfig.d.ts +44 -0
- package/dist/lib/appConfig.js +16 -0
- package/dist/lib/auditLog.d.ts +52 -0
- package/dist/lib/auditLog.js +201 -0
- package/dist/lib/authAdapter.d.ts +69 -0
- package/dist/lib/constants.d.ts +4 -0
- package/dist/lib/constants.js +4 -0
- package/dist/lib/context.d.ts +19 -1
- package/dist/lib/context.js +17 -3
- package/dist/lib/createRoute.d.ts +28 -2
- package/dist/lib/createRoute.js +54 -3
- package/dist/lib/deletionCancelToken.d.ts +12 -0
- package/dist/lib/deletionCancelToken.js +88 -0
- package/dist/lib/groups.d.ts +113 -0
- package/dist/lib/groups.js +133 -0
- package/dist/lib/idempotency.d.ts +22 -0
- package/dist/lib/idempotency.js +182 -0
- package/dist/lib/metrics.d.ts +14 -0
- package/dist/lib/metrics.js +158 -0
- package/dist/lib/pagination.d.ts +119 -0
- package/dist/lib/pagination.js +166 -0
- package/dist/lib/session.d.ts +4 -0
- package/dist/lib/session.js +56 -2
- package/dist/lib/signing.d.ts +52 -0
- package/dist/lib/signing.js +180 -0
- package/dist/lib/storageAdapter.d.ts +30 -0
- package/dist/lib/storageAdapter.js +1 -0
- package/dist/lib/stripUnreferencedSchemas.d.ts +11 -0
- package/dist/lib/stripUnreferencedSchemas.js +79 -0
- package/dist/lib/tenant.js +2 -2
- package/dist/lib/upload.d.ts +35 -0
- package/dist/lib/upload.js +87 -0
- package/dist/lib/validate.js +2 -2
- package/dist/lib/ws.d.ts +1 -0
- package/dist/lib/ws.js +21 -0
- package/dist/lib/wsHeartbeat.d.ts +12 -0
- package/dist/lib/wsHeartbeat.js +57 -0
- package/dist/lib/wsMessages.d.ts +40 -0
- package/dist/lib/wsMessages.js +330 -0
- package/dist/lib/wsPresence.d.ts +25 -0
- package/dist/lib/wsPresence.js +99 -0
- package/dist/middleware/auditLog.d.ts +22 -0
- package/dist/middleware/auditLog.js +39 -0
- package/dist/middleware/cacheResponse.js +5 -1
- package/dist/middleware/csrf.js +10 -0
- package/dist/middleware/identify.js +57 -9
- package/dist/middleware/metrics.d.ts +9 -0
- package/dist/middleware/metrics.js +26 -0
- package/dist/middleware/requestId.d.ts +3 -0
- package/dist/middleware/requestId.js +7 -0
- package/dist/middleware/requestLogger.d.ts +38 -0
- package/dist/middleware/requestLogger.js +68 -0
- package/dist/middleware/requestSigning.d.ts +20 -0
- package/dist/middleware/requestSigning.js +99 -0
- package/dist/middleware/requireMfaSetup.d.ts +16 -0
- package/dist/middleware/requireMfaSetup.js +36 -0
- package/dist/middleware/requireRole.d.ts +9 -3
- package/dist/middleware/requireRole.js +23 -36
- package/dist/middleware/upload.d.ts +5 -0
- package/dist/middleware/upload.js +27 -0
- package/dist/middleware/webhookAuth.d.ts +30 -0
- package/dist/middleware/webhookAuth.js +57 -0
- package/dist/models/AuditLog.d.ts +30 -0
- package/dist/models/AuditLog.js +39 -0
- package/dist/models/Group.d.ts +21 -0
- package/dist/models/Group.js +28 -0
- package/dist/models/GroupMembership.d.ts +21 -0
- package/dist/models/GroupMembership.js +25 -0
- package/dist/routes/auth.js +84 -6
- package/dist/routes/groups.d.ts +21 -0
- package/dist/routes/groups.js +346 -0
- package/dist/routes/jobs.js +47 -45
- package/dist/routes/metrics.d.ts +7 -0
- package/dist/routes/metrics.js +52 -0
- package/dist/routes/mfa.js +4 -0
- package/dist/routes/uploads.d.ts +2 -0
- package/dist/routes/uploads.js +135 -0
- package/dist/server.d.ts +26 -0
- package/dist/server.js +46 -3
- package/dist/ws/index.js +3 -0
- package/docs/sections/auth-flow/full.md +779 -634
- package/docs/sections/auth-flow/overview.md +2 -2
- package/docs/sections/auth-security-examples/full.md +365 -0
- package/docs/sections/authentication/full.md +130 -0
- package/docs/sections/authentication/overview.md +5 -0
- package/docs/sections/cli/full.md +13 -1
- package/docs/sections/configuration/full.md +17 -0
- package/docs/sections/configuration/overview.md +1 -0
- package/docs/sections/exports/full.md +34 -3
- package/docs/sections/logging/full.md +83 -0
- package/docs/sections/metrics/full.md +127 -0
- package/docs/sections/oauth/full.md +189 -189
- package/docs/sections/oauth/overview.md +1 -1
- package/docs/sections/pagination/full.md +93 -0
- package/docs/sections/roles/full.md +224 -135
- package/docs/sections/roles/overview.md +3 -1
- package/docs/sections/signing/full.md +203 -0
- package/docs/sections/uploads/full.md +199 -0
- package/docs/sections/versioning/full.md +85 -0
- package/docs/sections/webhook-auth/full.md +100 -0
- package/docs/sections/websocket/full.md +83 -0
- package/docs/sections/websocket-rooms/full.md +6 -1
- package/package.json +16 -4
|
@@ -78,6 +78,10 @@ function initSchema(db) {
|
|
|
78
78
|
db.run("ALTER TABLE sessions ADD COLUMN prevTokenExpiresAt INTEGER");
|
|
79
79
|
}
|
|
80
80
|
catch { /* already exists */ }
|
|
81
|
+
try {
|
|
82
|
+
db.run("ALTER TABLE sessions ADD COLUMN fingerprint TEXT");
|
|
83
|
+
}
|
|
84
|
+
catch { /* already exists */ }
|
|
81
85
|
db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_refreshToken ON sessions(refreshToken) WHERE refreshToken IS NOT NULL");
|
|
82
86
|
db.run(`CREATE TABLE IF NOT EXISTS oauth_states (
|
|
83
87
|
state TEXT PRIMARY KEY,
|
|
@@ -119,6 +123,35 @@ function initSchema(db) {
|
|
|
119
123
|
createdAt INTEGER NOT NULL
|
|
120
124
|
)`);
|
|
121
125
|
db.run("CREATE INDEX IF NOT EXISTS idx_webauthn_userId ON webauthn_credentials(userId)");
|
|
126
|
+
db.run(`CREATE TABLE IF NOT EXISTS groups (
|
|
127
|
+
id TEXT PRIMARY KEY,
|
|
128
|
+
name TEXT NOT NULL,
|
|
129
|
+
displayName TEXT,
|
|
130
|
+
description TEXT,
|
|
131
|
+
roles TEXT NOT NULL DEFAULT '[]',
|
|
132
|
+
tenantId TEXT,
|
|
133
|
+
createdAt INTEGER NOT NULL,
|
|
134
|
+
updatedAt INTEGER NOT NULL
|
|
135
|
+
)`);
|
|
136
|
+
// SQLite UNIQUE treats each NULL as distinct, so we use partial indexes instead of
|
|
137
|
+
// a simple UNIQUE constraint on (name, tenantId). This correctly enforces name
|
|
138
|
+
// uniqueness within app-wide scope and within each tenant scope separately.
|
|
139
|
+
db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_appwide ON groups(name) WHERE tenantId IS NULL");
|
|
140
|
+
db.run("CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_tenant ON groups(name, tenantId) WHERE tenantId IS NOT NULL");
|
|
141
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_groups_tenantId ON groups(tenantId)");
|
|
142
|
+
db.run(`CREATE TABLE IF NOT EXISTS group_memberships (
|
|
143
|
+
userId TEXT NOT NULL,
|
|
144
|
+
groupId TEXT NOT NULL REFERENCES groups(id) ON DELETE CASCADE,
|
|
145
|
+
roles TEXT NOT NULL DEFAULT '[]',
|
|
146
|
+
tenantId TEXT,
|
|
147
|
+
createdAt INTEGER NOT NULL,
|
|
148
|
+
PRIMARY KEY (userId, groupId)
|
|
149
|
+
)`);
|
|
150
|
+
// NOTE: PRAGMA foreign_keys = ON is set in setSqliteDb() and must run per-connection.
|
|
151
|
+
// If any code path opens SQLite without going through setSqliteDb, ON DELETE CASCADE
|
|
152
|
+
// for group_memberships will silently not fire. All SQLite access must use setSqliteDb.
|
|
153
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_gm_groupId ON group_memberships(groupId)");
|
|
154
|
+
db.run("CREATE INDEX IF NOT EXISTS idx_gm_userId_tenantId ON group_memberships(userId, tenantId)");
|
|
122
155
|
db.run(`CREATE TABLE IF NOT EXISTS oauth_codes (
|
|
123
156
|
codeHash TEXT PRIMARY KEY,
|
|
124
157
|
token TEXT NOT NULL,
|
|
@@ -127,6 +160,12 @@ function initSchema(db) {
|
|
|
127
160
|
refreshToken TEXT,
|
|
128
161
|
expiresAt INTEGER NOT NULL
|
|
129
162
|
)`);
|
|
163
|
+
db.run(`CREATE TABLE IF NOT EXISTS deletion_cancel_tokens (
|
|
164
|
+
token TEXT PRIMARY KEY,
|
|
165
|
+
userId TEXT NOT NULL,
|
|
166
|
+
jobId TEXT NOT NULL,
|
|
167
|
+
expiresAt INTEGER NOT NULL
|
|
168
|
+
)`);
|
|
130
169
|
}
|
|
131
170
|
// ---------------------------------------------------------------------------
|
|
132
171
|
// Auth adapter
|
|
@@ -325,6 +364,148 @@ export const sqliteAuthAdapter = {
|
|
|
325
364
|
async removeTenantRole(userId, tenantId, role) {
|
|
326
365
|
getDb().run("DELETE FROM tenant_roles WHERE userId = ? AND tenantId = ? AND role = ?", [userId, tenantId, role]);
|
|
327
366
|
},
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
// Groups
|
|
369
|
+
// ---------------------------------------------------------------------------
|
|
370
|
+
async createGroup(group) {
|
|
371
|
+
const id = crypto.randomUUID();
|
|
372
|
+
const now = Date.now();
|
|
373
|
+
try {
|
|
374
|
+
getDb().run("INSERT INTO groups (id, name, displayName, description, roles, tenantId, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [id, group.name, group.displayName ?? null, group.description ?? null, JSON.stringify(group.roles ?? []), group.tenantId ?? null, now, now]);
|
|
375
|
+
}
|
|
376
|
+
catch (err) {
|
|
377
|
+
if (err?.code === "SQLITE_CONSTRAINT_UNIQUE" || err?.code === "SQLITE_CONSTRAINT_PRIMARYKEY") {
|
|
378
|
+
throw new HttpError(409, "A group with this name already exists in this scope");
|
|
379
|
+
}
|
|
380
|
+
throw err;
|
|
381
|
+
}
|
|
382
|
+
return { id };
|
|
383
|
+
},
|
|
384
|
+
async deleteGroup(groupId) {
|
|
385
|
+
// group_memberships are cascade-deleted via ON DELETE CASCADE (requires PRAGMA foreign_keys = ON)
|
|
386
|
+
getDb().run("DELETE FROM groups WHERE id = ?", [groupId]);
|
|
387
|
+
},
|
|
388
|
+
async getGroup(groupId) {
|
|
389
|
+
const row = getDb().query("SELECT id, name, displayName, description, roles, tenantId, createdAt, updatedAt FROM groups WHERE id = ?").get(groupId);
|
|
390
|
+
if (!row)
|
|
391
|
+
return null;
|
|
392
|
+
return { ...row, displayName: row.displayName ?? undefined, description: row.description ?? undefined, roles: JSON.parse(row.roles) };
|
|
393
|
+
},
|
|
394
|
+
async listGroups(tenantId, opts) {
|
|
395
|
+
const limit = Math.min(opts?.limit ?? 50, 200);
|
|
396
|
+
const offset = opts?.offset ?? 0;
|
|
397
|
+
const db = getDb();
|
|
398
|
+
const cols = "id, name, displayName, description, roles, tenantId, createdAt, updatedAt";
|
|
399
|
+
let rows;
|
|
400
|
+
let total;
|
|
401
|
+
if (tenantId === null) {
|
|
402
|
+
rows = db.query(`SELECT ${cols} FROM groups WHERE tenantId IS NULL LIMIT ? OFFSET ?`).all(limit, offset);
|
|
403
|
+
total = (db.query("SELECT COUNT(*) as c FROM groups WHERE tenantId IS NULL").get()?.c ?? 0);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
rows = db.query(`SELECT ${cols} FROM groups WHERE tenantId = ? LIMIT ? OFFSET ?`).all(tenantId, limit, offset);
|
|
407
|
+
total = (db.query("SELECT COUNT(*) as c FROM groups WHERE tenantId = ?").get(tenantId)?.c ?? 0);
|
|
408
|
+
}
|
|
409
|
+
const items = rows.map((r) => ({ ...r, displayName: r.displayName ?? undefined, description: r.description ?? undefined, roles: JSON.parse(r.roles) }));
|
|
410
|
+
return { items, total, limit, offset };
|
|
411
|
+
},
|
|
412
|
+
async updateGroup(groupId, updates) {
|
|
413
|
+
const db = getDb();
|
|
414
|
+
const now = Date.now();
|
|
415
|
+
const sets = ["updatedAt = ?"];
|
|
416
|
+
const params = [now];
|
|
417
|
+
if (updates.name !== undefined) {
|
|
418
|
+
sets.push("name = ?");
|
|
419
|
+
params.push(updates.name);
|
|
420
|
+
}
|
|
421
|
+
if ("displayName" in updates) {
|
|
422
|
+
sets.push("displayName = ?");
|
|
423
|
+
params.push(updates.displayName ?? null);
|
|
424
|
+
}
|
|
425
|
+
if ("description" in updates) {
|
|
426
|
+
sets.push("description = ?");
|
|
427
|
+
params.push(updates.description ?? null);
|
|
428
|
+
}
|
|
429
|
+
if (updates.roles !== undefined) {
|
|
430
|
+
sets.push("roles = ?");
|
|
431
|
+
params.push(JSON.stringify(updates.roles));
|
|
432
|
+
}
|
|
433
|
+
params.push(groupId);
|
|
434
|
+
db.run(`UPDATE groups SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
435
|
+
},
|
|
436
|
+
async addGroupMember(groupId, userId, roles = []) {
|
|
437
|
+
const group = getDb().query("SELECT tenantId FROM groups WHERE id = ?").get(groupId);
|
|
438
|
+
if (!group)
|
|
439
|
+
throw new HttpError(404, "Group not found");
|
|
440
|
+
try {
|
|
441
|
+
getDb().run("INSERT INTO group_memberships (userId, groupId, roles, tenantId, createdAt) VALUES (?, ?, ?, ?, ?)", [userId, groupId, JSON.stringify(roles), group.tenantId ?? null, Date.now()]);
|
|
442
|
+
}
|
|
443
|
+
catch (err) {
|
|
444
|
+
if (err?.code === "SQLITE_CONSTRAINT_PRIMARYKEY" || err?.code === "SQLITE_CONSTRAINT_UNIQUE") {
|
|
445
|
+
throw new HttpError(409, "User is already a member of this group");
|
|
446
|
+
}
|
|
447
|
+
throw err;
|
|
448
|
+
}
|
|
449
|
+
},
|
|
450
|
+
async updateGroupMembership(groupId, userId, roles) {
|
|
451
|
+
getDb().run("UPDATE group_memberships SET roles = ? WHERE userId = ? AND groupId = ?", [JSON.stringify(roles), userId, groupId]);
|
|
452
|
+
},
|
|
453
|
+
async removeGroupMember(groupId, userId) {
|
|
454
|
+
getDb().run("DELETE FROM group_memberships WHERE userId = ? AND groupId = ?", [userId, groupId]);
|
|
455
|
+
},
|
|
456
|
+
async getGroupMembers(groupId, opts) {
|
|
457
|
+
const limit = Math.min(opts?.limit ?? 50, 200);
|
|
458
|
+
const offset = opts?.offset ?? 0;
|
|
459
|
+
const db = getDb();
|
|
460
|
+
const rows = db.query("SELECT userId, roles FROM group_memberships WHERE groupId = ? LIMIT ? OFFSET ?").all(groupId, limit, offset);
|
|
461
|
+
const total = db.query("SELECT COUNT(*) as c FROM group_memberships WHERE groupId = ?").get(groupId)?.c ?? 0;
|
|
462
|
+
return { items: rows.map((r) => ({ userId: r.userId, roles: JSON.parse(r.roles) })), total, limit, offset };
|
|
463
|
+
},
|
|
464
|
+
async getUserGroups(userId, tenantId) {
|
|
465
|
+
const db = getDb();
|
|
466
|
+
let memberRows;
|
|
467
|
+
if (tenantId === null) {
|
|
468
|
+
memberRows = db.query("SELECT groupId, roles as memberRoles FROM group_memberships WHERE userId = ? AND tenantId IS NULL").all(userId);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
memberRows = db.query("SELECT groupId, roles as memberRoles FROM group_memberships WHERE userId = ? AND tenantId = ?").all(userId, tenantId);
|
|
472
|
+
}
|
|
473
|
+
if (memberRows.length === 0)
|
|
474
|
+
return [];
|
|
475
|
+
const result = [];
|
|
476
|
+
for (const m of memberRows) {
|
|
477
|
+
const row = db.query("SELECT id, name, displayName, description, roles, tenantId, createdAt, updatedAt FROM groups WHERE id = ?").get(m.groupId);
|
|
478
|
+
if (row) {
|
|
479
|
+
result.push({
|
|
480
|
+
group: { ...row, displayName: row.displayName ?? undefined, description: row.description ?? undefined, roles: JSON.parse(row.roles) },
|
|
481
|
+
membershipRoles: JSON.parse(m.memberRoles),
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return result;
|
|
486
|
+
},
|
|
487
|
+
async getEffectiveRoles(userId, tenantId) {
|
|
488
|
+
const db = getDb();
|
|
489
|
+
// Direct roles
|
|
490
|
+
let direct = [];
|
|
491
|
+
if (tenantId) {
|
|
492
|
+
const rows = db.query("SELECT role FROM tenant_roles WHERE userId = ? AND tenantId = ?").all(userId, tenantId);
|
|
493
|
+
direct = rows.map((r) => r.role);
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
const row = db.query("SELECT roles FROM users WHERE id = ?").get(userId);
|
|
497
|
+
direct = row ? JSON.parse(row.roles) : [];
|
|
498
|
+
}
|
|
499
|
+
let memberRows;
|
|
500
|
+
if (tenantId === null) {
|
|
501
|
+
memberRows = db.query("SELECT g.roles as groupRoles, gm.roles as memberRoles FROM group_memberships gm JOIN groups g ON g.id = gm.groupId WHERE gm.userId = ? AND gm.tenantId IS NULL").all(userId);
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
memberRows = db.query("SELECT g.roles as groupRoles, gm.roles as memberRoles FROM group_memberships gm JOIN groups g ON g.id = gm.groupId WHERE gm.userId = ? AND gm.tenantId = ?").all(userId, tenantId);
|
|
505
|
+
}
|
|
506
|
+
const groupRoles = memberRows.flatMap((r) => [...JSON.parse(r.groupRoles), ...JSON.parse(r.memberRoles)]);
|
|
507
|
+
return [...new Set([...direct, ...groupRoles])];
|
|
508
|
+
},
|
|
328
509
|
};
|
|
329
510
|
import { getPersistSessionMetadata, getIncludeInactiveSessions } from "../lib/appConfig";
|
|
330
511
|
const SESSION_TTL_MS = 60 * 60 * 24 * 7 * 1000; // 7 days
|
|
@@ -413,6 +594,13 @@ export const sqliteRotateRefreshToken = (sessionId, newRefreshToken, newAccessTo
|
|
|
413
594
|
const prevTokenExpiresAt = Date.now() + graceSeconds * 1000;
|
|
414
595
|
getDb().run("UPDATE sessions SET prevRefreshToken = refreshToken, prevTokenExpiresAt = ?, refreshToken = ?, token = ? WHERE sessionId = ?", [prevTokenExpiresAt, newRefreshToken, newAccessToken, sessionId]);
|
|
415
596
|
};
|
|
597
|
+
export const sqliteGetSessionFingerprint = (sessionId) => {
|
|
598
|
+
const row = getDb().query("SELECT fingerprint FROM sessions WHERE sessionId = ?").get(sessionId);
|
|
599
|
+
return row?.fingerprint ?? null;
|
|
600
|
+
};
|
|
601
|
+
export const sqliteSetSessionFingerprint = (sessionId, fingerprint) => {
|
|
602
|
+
getDb().run("UPDATE sessions SET fingerprint = ? WHERE sessionId = ?", [fingerprint, sessionId]);
|
|
603
|
+
};
|
|
416
604
|
// ---------------------------------------------------------------------------
|
|
417
605
|
// OAuth state helpers (used by src/lib/oauth.ts)
|
|
418
606
|
// ---------------------------------------------------------------------------
|
|
@@ -475,6 +663,17 @@ export const sqliteConsumeResetToken = (hash) => {
|
|
|
475
663
|
const row = getDb().query("DELETE FROM password_resets WHERE token = ? AND expiresAt > ? RETURNING userId, email").get(hash, Date.now());
|
|
476
664
|
return row ?? null;
|
|
477
665
|
};
|
|
666
|
+
// ---------------------------------------------------------------------------
|
|
667
|
+
// Account deletion cancel token helpers (used by src/lib/deletionCancelToken.ts)
|
|
668
|
+
// ---------------------------------------------------------------------------
|
|
669
|
+
export const sqliteCreateDeletionCancelToken = (token, userId, jobId, ttlSeconds) => {
|
|
670
|
+
const expiresAt = Date.now() + ttlSeconds * 1000;
|
|
671
|
+
getDb().run("INSERT INTO deletion_cancel_tokens (token, userId, jobId, expiresAt) VALUES (?, ?, ?, ?)", [token, userId, jobId, expiresAt]);
|
|
672
|
+
};
|
|
673
|
+
export const sqliteConsumeDeletionCancelToken = (hash) => {
|
|
674
|
+
const row = getDb().query("DELETE FROM deletion_cancel_tokens WHERE token = ? AND expiresAt > ? RETURNING userId, jobId").get(hash, Date.now());
|
|
675
|
+
return row ?? null;
|
|
676
|
+
};
|
|
478
677
|
export const sqliteStoreOAuthCode = (hash, payload, ttlSeconds) => {
|
|
479
678
|
const expiresAt = Date.now() + ttlSeconds * 1000;
|
|
480
679
|
getDb().run("INSERT INTO oauth_codes (codeHash, token, userId, email, refreshToken, expiresAt) VALUES (?, ?, ?, ?, ?, ?)", [hash, payload.token, payload.userId, payload.email ?? null, payload.refreshToken ?? null, expiresAt]);
|
package/dist/app.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { OpenAPIHono } from "@hono/zod-openapi";
|
|
2
2
|
import type { MiddlewareHandler } from "hono";
|
|
3
|
-
import type { AppEnv } from "./lib/context";
|
|
4
|
-
import type {
|
|
3
|
+
import type { AppEnv, ValidationErrorFormatter } from "./lib/context";
|
|
4
|
+
import type { RequestLogEntry, LogLevel } from "./middleware/requestLogger";
|
|
5
|
+
import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, PasswordPolicyConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, MfaWebAuthnConfig, SigningConfig } from "./lib/appConfig";
|
|
5
6
|
import type { AuthAdapter } from "./lib/authAdapter";
|
|
6
7
|
import type { OAuthProviderConfig } from "./lib/oauth";
|
|
7
8
|
type StoreType = "redis" | "mongo" | "sqlite" | "memory";
|
|
@@ -204,7 +205,7 @@ export interface AuthSessionPolicyConfig {
|
|
|
204
205
|
*/
|
|
205
206
|
trackLastActive?: boolean;
|
|
206
207
|
}
|
|
207
|
-
export type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, MfaWebAuthnConfig };
|
|
208
|
+
export type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, MfaWebAuthnConfig, SigningConfig };
|
|
208
209
|
export interface BotProtectionConfig {
|
|
209
210
|
/**
|
|
210
211
|
* List of IPv4 CIDRs (e.g. "198.51.100.0/24"), IPv4 addresses, or IPv6 addresses to block outright.
|
|
@@ -267,6 +268,11 @@ export interface SecurityConfig {
|
|
|
267
268
|
* Only validates when the auth cookie is present on state-changing requests.
|
|
268
269
|
*/
|
|
269
270
|
csrf?: CsrfConfig;
|
|
271
|
+
/**
|
|
272
|
+
* Unified HMAC signing for cookies, cursors, presigned URLs, request signing,
|
|
273
|
+
* idempotency key hashing, and session binding. All features are opt-in.
|
|
274
|
+
*/
|
|
275
|
+
signing?: SigningConfig;
|
|
270
276
|
}
|
|
271
277
|
export interface ModelSchemasConfig {
|
|
272
278
|
/**
|
|
@@ -331,6 +337,77 @@ export interface TenancyConfig {
|
|
|
331
337
|
/** HTTP status when onResolve returns null. Default: 403. */
|
|
332
338
|
rejectionStatus?: 403 | 404;
|
|
333
339
|
}
|
|
340
|
+
export interface LoggingConfig {
|
|
341
|
+
/** Enable structured request logging. Default: true. When false, no logger is registered at all. */
|
|
342
|
+
enabled?: boolean;
|
|
343
|
+
/** Custom log handler. Default: `console.log(JSON.stringify(entry))`. */
|
|
344
|
+
onLog?: (entry: RequestLogEntry) => void | Promise<void>;
|
|
345
|
+
/** Minimum log level to emit. Entries below this level are dropped. */
|
|
346
|
+
level?: LogLevel;
|
|
347
|
+
/**
|
|
348
|
+
* Paths to exclude from logging. Strings use **prefix matching**.
|
|
349
|
+
* Default: `["/health", "/docs", "/openapi.json"]`.
|
|
350
|
+
*/
|
|
351
|
+
excludePaths?: (string | RegExp)[];
|
|
352
|
+
/** HTTP methods to exclude from logging (e.g. `["OPTIONS"]`). */
|
|
353
|
+
excludeMethods?: string[];
|
|
354
|
+
}
|
|
355
|
+
export interface MetricsConfig {
|
|
356
|
+
/** Enable the /metrics endpoint. Default: false (must be explicitly enabled). */
|
|
357
|
+
enabled?: boolean;
|
|
358
|
+
/**
|
|
359
|
+
* Auth protection for the /metrics endpoint.
|
|
360
|
+
* - `"userAuth"` — requires authenticated user session.
|
|
361
|
+
* - `"none"` — no auth (default — logs a production warning).
|
|
362
|
+
* - `MiddlewareHandler[]` — custom middleware stack.
|
|
363
|
+
*/
|
|
364
|
+
auth?: "userAuth" | "none" | MiddlewareHandler<AppEnv>[];
|
|
365
|
+
/** Paths to exclude from metrics collection. Strings use prefix matching. */
|
|
366
|
+
excludePaths?: (string | RegExp)[];
|
|
367
|
+
/** Custom path normalizer to prevent high-cardinality labels. */
|
|
368
|
+
normalizePath?: (path: string) => string;
|
|
369
|
+
/** BullMQ queue names to report depth gauges for. */
|
|
370
|
+
queues?: string[];
|
|
371
|
+
}
|
|
372
|
+
export interface ValidationConfig {
|
|
373
|
+
/** Custom formatter for Zod validation errors. Receives issues + requestId, returns the JSON body. */
|
|
374
|
+
formatError?: ValidationErrorFormatter;
|
|
375
|
+
}
|
|
376
|
+
export interface VersioningConfig {
|
|
377
|
+
/**
|
|
378
|
+
* Version identifiers in ascending order, e.g. `["v1", "v2"]`.
|
|
379
|
+
* Each version needs a matching subdirectory under `routesDir` (e.g. `routes/v1/`).
|
|
380
|
+
*/
|
|
381
|
+
versions: string[];
|
|
382
|
+
/**
|
|
383
|
+
* Subdirectory name for routes shared across all versions. Shared route schemas
|
|
384
|
+
* receive unprefixed names since they are version-agnostic. Default: `"shared"`.
|
|
385
|
+
* Set `false` to disable shared route discovery.
|
|
386
|
+
*/
|
|
387
|
+
sharedDir?: string | false;
|
|
388
|
+
/**
|
|
389
|
+
* Which version `/docs` and `/openapi.json` redirect to.
|
|
390
|
+
* Defaults to the last version in the array (i.e. the latest).
|
|
391
|
+
*/
|
|
392
|
+
defaultVersion?: string;
|
|
393
|
+
}
|
|
394
|
+
export interface PresignedUrlConfig {
|
|
395
|
+
expirySeconds?: number;
|
|
396
|
+
path?: string;
|
|
397
|
+
}
|
|
398
|
+
export interface UploadConfig {
|
|
399
|
+
storage: import("./lib/storageAdapter").StorageAdapter;
|
|
400
|
+
maxFileSize?: number;
|
|
401
|
+
maxFiles?: number;
|
|
402
|
+
allowedMimeTypes?: string[];
|
|
403
|
+
keyPrefix?: string;
|
|
404
|
+
generateKey?: (file: File, ctx: {
|
|
405
|
+
userId?: string;
|
|
406
|
+
tenantId?: string;
|
|
407
|
+
}) => string;
|
|
408
|
+
tenantScopedKeys?: boolean;
|
|
409
|
+
presignedUrls?: boolean | PresignedUrlConfig;
|
|
410
|
+
}
|
|
334
411
|
export interface CreateAppConfig {
|
|
335
412
|
/** Absolute path to the service's routes directory (use import.meta.dir + "/routes") */
|
|
336
413
|
routesDir: string;
|
|
@@ -355,5 +432,25 @@ export interface CreateAppConfig {
|
|
|
355
432
|
jobs?: JobsConfig;
|
|
356
433
|
/** Multi-tenancy configuration. When set, tenant middleware resolves tenant on each request. */
|
|
357
434
|
tenancy?: TenancyConfig;
|
|
435
|
+
/**
|
|
436
|
+
* Groups feature configuration. When set, the groups lib is available.
|
|
437
|
+
* Set managementRoutes to mount built-in CRUD routes for groups and memberships.
|
|
438
|
+
*/
|
|
439
|
+
groups?: import("./routes/groups").GroupsConfig;
|
|
440
|
+
/** Structured request logging configuration. Replaces Hono's built-in text logger. */
|
|
441
|
+
logging?: LoggingConfig;
|
|
442
|
+
/** Prometheus-compatible /metrics endpoint. Opt-in. */
|
|
443
|
+
metrics?: MetricsConfig;
|
|
444
|
+
/** Zod validation error formatting configuration. */
|
|
445
|
+
validation?: ValidationConfig;
|
|
446
|
+
/** File upload configuration. When set, registers storage adapter and upload settings. */
|
|
447
|
+
upload?: UploadConfig;
|
|
448
|
+
/**
|
|
449
|
+
* API versioning configuration. When set, routes are discovered per-version from
|
|
450
|
+
* subdirectories of `routesDir` (e.g. `routes/v1/`, `routes/v2/`). Each version
|
|
451
|
+
* gets its own OpenAPI spec at `/{version}/openapi.json` and Scalar docs at
|
|
452
|
+
* `/{version}/docs`. Root `/docs` becomes a version selector.
|
|
453
|
+
*/
|
|
454
|
+
versioning?: VersioningConfig;
|
|
358
455
|
}
|
|
359
456
|
export declare const createApp: (config: CreateAppConfig) => Promise<OpenAPIHono<AppEnv>>;
|