@lastshotlabs/bunshot 0.0.21 → 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.
Files changed (122) hide show
  1. package/README.md +3035 -1249
  2. package/dist/adapters/localStorage.d.ts +6 -0
  3. package/dist/adapters/localStorage.js +44 -0
  4. package/dist/adapters/memoryAuth.d.ts +7 -0
  5. package/dist/adapters/memoryAuth.js +144 -0
  6. package/dist/adapters/memoryStorage.d.ts +3 -0
  7. package/dist/adapters/memoryStorage.js +44 -0
  8. package/dist/adapters/mongoAuth.js +120 -0
  9. package/dist/adapters/s3Storage.d.ts +14 -0
  10. package/dist/adapters/s3Storage.js +126 -0
  11. package/dist/adapters/sqliteAuth.d.ts +7 -0
  12. package/dist/adapters/sqliteAuth.js +199 -0
  13. package/dist/app.d.ts +100 -3
  14. package/dist/app.js +247 -46
  15. package/dist/cli.js +118 -38
  16. package/dist/index.d.ts +49 -7
  17. package/dist/index.js +35 -5
  18. package/dist/lib/HttpError.d.ts +5 -0
  19. package/dist/lib/HttpError.js +7 -0
  20. package/dist/lib/appConfig.d.ts +44 -0
  21. package/dist/lib/appConfig.js +16 -0
  22. package/dist/lib/auditLog.d.ts +52 -0
  23. package/dist/lib/auditLog.js +201 -0
  24. package/dist/lib/authAdapter.d.ts +69 -0
  25. package/dist/lib/constants.d.ts +4 -0
  26. package/dist/lib/constants.js +4 -0
  27. package/dist/lib/context.d.ts +19 -1
  28. package/dist/lib/context.js +17 -3
  29. package/dist/lib/createRoute.d.ts +28 -2
  30. package/dist/lib/createRoute.js +54 -3
  31. package/dist/lib/deletionCancelToken.d.ts +12 -0
  32. package/dist/lib/deletionCancelToken.js +88 -0
  33. package/dist/lib/groups.d.ts +113 -0
  34. package/dist/lib/groups.js +133 -0
  35. package/dist/lib/idempotency.d.ts +22 -0
  36. package/dist/lib/idempotency.js +182 -0
  37. package/dist/lib/metrics.d.ts +14 -0
  38. package/dist/lib/metrics.js +158 -0
  39. package/dist/lib/pagination.d.ts +119 -0
  40. package/dist/lib/pagination.js +166 -0
  41. package/dist/lib/session.d.ts +4 -0
  42. package/dist/lib/session.js +56 -2
  43. package/dist/lib/signing.d.ts +52 -0
  44. package/dist/lib/signing.js +180 -0
  45. package/dist/lib/storageAdapter.d.ts +30 -0
  46. package/dist/lib/storageAdapter.js +1 -0
  47. package/dist/lib/stripUnreferencedSchemas.d.ts +11 -0
  48. package/dist/lib/stripUnreferencedSchemas.js +79 -0
  49. package/dist/lib/tenant.js +2 -2
  50. package/dist/lib/upload.d.ts +35 -0
  51. package/dist/lib/upload.js +87 -0
  52. package/dist/lib/validate.js +2 -2
  53. package/dist/lib/ws.d.ts +1 -0
  54. package/dist/lib/ws.js +21 -0
  55. package/dist/lib/wsHeartbeat.d.ts +12 -0
  56. package/dist/lib/wsHeartbeat.js +57 -0
  57. package/dist/lib/wsMessages.d.ts +40 -0
  58. package/dist/lib/wsMessages.js +330 -0
  59. package/dist/lib/wsPresence.d.ts +25 -0
  60. package/dist/lib/wsPresence.js +99 -0
  61. package/dist/middleware/auditLog.d.ts +22 -0
  62. package/dist/middleware/auditLog.js +39 -0
  63. package/dist/middleware/cacheResponse.js +5 -1
  64. package/dist/middleware/csrf.js +10 -0
  65. package/dist/middleware/identify.js +57 -9
  66. package/dist/middleware/metrics.d.ts +9 -0
  67. package/dist/middleware/metrics.js +26 -0
  68. package/dist/middleware/requestId.d.ts +3 -0
  69. package/dist/middleware/requestId.js +7 -0
  70. package/dist/middleware/requestLogger.d.ts +38 -0
  71. package/dist/middleware/requestLogger.js +68 -0
  72. package/dist/middleware/requestSigning.d.ts +20 -0
  73. package/dist/middleware/requestSigning.js +99 -0
  74. package/dist/middleware/requireMfaSetup.d.ts +16 -0
  75. package/dist/middleware/requireMfaSetup.js +36 -0
  76. package/dist/middleware/requireRole.d.ts +9 -3
  77. package/dist/middleware/requireRole.js +23 -36
  78. package/dist/middleware/upload.d.ts +5 -0
  79. package/dist/middleware/upload.js +27 -0
  80. package/dist/middleware/webhookAuth.d.ts +30 -0
  81. package/dist/middleware/webhookAuth.js +57 -0
  82. package/dist/models/AuditLog.d.ts +30 -0
  83. package/dist/models/AuditLog.js +39 -0
  84. package/dist/models/Group.d.ts +21 -0
  85. package/dist/models/Group.js +28 -0
  86. package/dist/models/GroupMembership.d.ts +21 -0
  87. package/dist/models/GroupMembership.js +25 -0
  88. package/dist/routes/auth.js +84 -6
  89. package/dist/routes/groups.d.ts +21 -0
  90. package/dist/routes/groups.js +346 -0
  91. package/dist/routes/jobs.js +47 -45
  92. package/dist/routes/metrics.d.ts +7 -0
  93. package/dist/routes/metrics.js +52 -0
  94. package/dist/routes/mfa.js +4 -0
  95. package/dist/routes/uploads.d.ts +2 -0
  96. package/dist/routes/uploads.js +135 -0
  97. package/dist/server.d.ts +26 -0
  98. package/dist/server.js +46 -3
  99. package/dist/ws/index.js +3 -0
  100. package/docs/sections/auth-flow/full.md +779 -634
  101. package/docs/sections/auth-flow/overview.md +2 -2
  102. package/docs/sections/auth-security-examples/full.md +365 -0
  103. package/docs/sections/authentication/full.md +130 -0
  104. package/docs/sections/authentication/overview.md +5 -0
  105. package/docs/sections/cli/full.md +13 -1
  106. package/docs/sections/configuration/full.md +17 -0
  107. package/docs/sections/configuration/overview.md +1 -0
  108. package/docs/sections/exports/full.md +34 -3
  109. package/docs/sections/logging/full.md +83 -0
  110. package/docs/sections/metrics/full.md +127 -0
  111. package/docs/sections/oauth/full.md +189 -189
  112. package/docs/sections/oauth/overview.md +1 -1
  113. package/docs/sections/pagination/full.md +93 -0
  114. package/docs/sections/roles/full.md +224 -135
  115. package/docs/sections/roles/overview.md +3 -1
  116. package/docs/sections/signing/full.md +203 -0
  117. package/docs/sections/uploads/full.md +199 -0
  118. package/docs/sections/versioning/full.md +85 -0
  119. package/docs/sections/webhook-auth/full.md +100 -0
  120. package/docs/sections/websocket/full.md +83 -0
  121. package/docs/sections/websocket-rooms/full.md +6 -1
  122. 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 { PrimaryField, EmailVerificationConfig, PasswordResetConfig, PasswordPolicyConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, MfaWebAuthnConfig } from "./lib/appConfig";
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>>;