@lastshotlabs/bunshot 0.0.25 → 0.0.27

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 (108) hide show
  1. package/dist/adapters/localStorage.js +20 -5
  2. package/dist/adapters/memoryAuth.d.ts +6 -0
  3. package/dist/adapters/memoryAuth.js +117 -2
  4. package/dist/adapters/mongoAuth.js +97 -1
  5. package/dist/adapters/sqliteAuth.d.ts +23 -0
  6. package/dist/adapters/sqliteAuth.js +153 -2
  7. package/dist/app.d.ts +105 -2
  8. package/dist/app.js +112 -9
  9. package/dist/index.d.ts +23 -4
  10. package/dist/index.js +13 -2
  11. package/dist/lib/HttpError.d.ts +2 -1
  12. package/dist/lib/HttpError.js +3 -1
  13. package/dist/lib/appConfig.d.ts +113 -0
  14. package/dist/lib/appConfig.js +38 -0
  15. package/dist/lib/auditLog.d.ts +6 -0
  16. package/dist/lib/auditLog.js +17 -0
  17. package/dist/lib/authAdapter.d.ts +71 -1
  18. package/dist/lib/authRateLimit.js +36 -0
  19. package/dist/lib/breachedPassword.d.ts +13 -0
  20. package/dist/lib/breachedPassword.js +48 -0
  21. package/dist/lib/captcha.d.ts +25 -0
  22. package/dist/lib/captcha.js +37 -0
  23. package/dist/lib/context.d.ts +5 -0
  24. package/dist/lib/credentialStuffing.d.ts +31 -0
  25. package/dist/lib/credentialStuffing.js +77 -0
  26. package/dist/lib/emailVerification.d.ts +6 -0
  27. package/dist/lib/emailVerification.js +46 -3
  28. package/dist/lib/jwks.d.ts +25 -0
  29. package/dist/lib/jwks.js +51 -0
  30. package/dist/lib/jwt.d.ts +15 -2
  31. package/dist/lib/jwt.js +92 -5
  32. package/dist/lib/logger.d.ts +2 -0
  33. package/dist/lib/logger.js +6 -0
  34. package/dist/lib/m2m.d.ts +29 -0
  35. package/dist/lib/m2m.js +48 -0
  36. package/dist/lib/mfaChallenge.d.ts +14 -1
  37. package/dist/lib/mfaChallenge.js +111 -6
  38. package/dist/lib/mongo.js +1 -1
  39. package/dist/lib/oauthCode.js +23 -18
  40. package/dist/lib/resetPassword.js +3 -1
  41. package/dist/lib/saml.d.ts +25 -0
  42. package/dist/lib/saml.js +64 -0
  43. package/dist/lib/scim.d.ts +44 -0
  44. package/dist/lib/scim.js +54 -0
  45. package/dist/lib/securityEvents.d.ts +28 -0
  46. package/dist/lib/securityEvents.js +26 -0
  47. package/dist/lib/session.d.ts +10 -0
  48. package/dist/lib/session.js +67 -5
  49. package/dist/lib/signing.js +5 -2
  50. package/dist/lib/suspension.d.ts +13 -0
  51. package/dist/lib/suspension.js +23 -0
  52. package/dist/lib/upload.d.ts +4 -0
  53. package/dist/lib/upload.js +26 -1
  54. package/dist/lib/uploadRegistry.d.ts +18 -0
  55. package/dist/lib/uploadRegistry.js +83 -0
  56. package/dist/lib/ws.js +7 -0
  57. package/dist/middleware/bearerAuth.js +1 -1
  58. package/dist/middleware/captcha.d.ts +10 -0
  59. package/dist/middleware/captcha.js +36 -0
  60. package/dist/middleware/csrf.js +8 -4
  61. package/dist/middleware/errorHandler.js +4 -1
  62. package/dist/middleware/identify.js +40 -13
  63. package/dist/middleware/requestSigning.js +6 -5
  64. package/dist/middleware/requireMfaSetup.js +2 -1
  65. package/dist/middleware/requireScope.d.ts +10 -0
  66. package/dist/middleware/requireScope.js +25 -0
  67. package/dist/middleware/requireStepUp.d.ts +18 -0
  68. package/dist/middleware/requireStepUp.js +29 -0
  69. package/dist/middleware/scimAuth.d.ts +8 -0
  70. package/dist/middleware/scimAuth.js +29 -0
  71. package/dist/middleware/webhookAuth.d.ts +1 -1
  72. package/dist/middleware/webhookAuth.js +6 -5
  73. package/dist/models/AuthUser.d.ts +7 -0
  74. package/dist/models/AuthUser.js +7 -0
  75. package/dist/models/M2MClient.d.ts +18 -0
  76. package/dist/models/M2MClient.js +18 -0
  77. package/dist/routes/auth.d.ts +3 -2
  78. package/dist/routes/auth.js +155 -16
  79. package/dist/routes/jobs.js +21 -3
  80. package/dist/routes/m2m.d.ts +2 -0
  81. package/dist/routes/m2m.js +72 -0
  82. package/dist/routes/metrics.d.ts +1 -0
  83. package/dist/routes/metrics.js +3 -0
  84. package/dist/routes/mfa.js +9 -1
  85. package/dist/routes/oauth.js +6 -0
  86. package/dist/routes/oidc.d.ts +2 -0
  87. package/dist/routes/oidc.js +29 -0
  88. package/dist/routes/passkey.d.ts +1 -0
  89. package/dist/routes/passkey.js +157 -0
  90. package/dist/routes/saml.d.ts +2 -0
  91. package/dist/routes/saml.js +86 -0
  92. package/dist/routes/scim.d.ts +2 -0
  93. package/dist/routes/scim.js +255 -0
  94. package/dist/routes/uploads.d.ts +13 -1
  95. package/dist/routes/uploads.js +98 -6
  96. package/dist/services/auth.d.ts +2 -0
  97. package/dist/services/auth.js +101 -22
  98. package/dist/services/mfa.js +2 -2
  99. package/dist/ws/index.js +2 -1
  100. package/docs/sections/auth-flow/full.md +790 -779
  101. package/docs/sections/auth-security-examples/full.md +23 -0
  102. package/docs/sections/metrics/full.md +6 -2
  103. package/docs/sections/passkey-login/full.md +90 -0
  104. package/docs/sections/passkey-login/overview.md +1 -0
  105. package/docs/sections/uploads/full.md +11 -2
  106. package/docs/sections/webhook-auth/full.md +1 -1
  107. package/docs/sections/websocket/full.md +12 -0
  108. package/package.json +3 -2
@@ -4,11 +4,15 @@ import { COOKIE_TOKEN, COOKIE_CSRF_TOKEN, HEADER_CSRF_TOKEN } from "../lib/const
4
4
  import { createHmac, randomBytes } from "crypto";
5
5
  const isProd = process.env.NODE_ENV === "production";
6
6
  const STATE_CHANGING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
7
- function getJwtSecret() {
7
+ let _csrfSecret = null;
8
+ function getCsrfSecret() {
9
+ if (_csrfSecret)
10
+ return _csrfSecret;
8
11
  const secret = isProd ? process.env.JWT_SECRET_PROD : process.env.JWT_SECRET_DEV;
9
12
  if (!secret)
10
13
  throw new Error("CSRF middleware requires JWT_SECRET_DEV/JWT_SECRET_PROD to be set");
11
- return secret;
14
+ _csrfSecret = secret;
15
+ return _csrfSecret;
12
16
  }
13
17
  function generateCsrfToken(secret) {
14
18
  const token = randomBytes(32).toString("hex");
@@ -36,7 +40,7 @@ const csrfCookieOptions = {
36
40
  * session fixation-adjacent attacks.
37
41
  */
38
42
  export function refreshCsrfToken(c) {
39
- const secret = getJwtSecret();
43
+ const secret = getCsrfSecret();
40
44
  const token = generateCsrfToken(secret);
41
45
  setCookie(c, COOKIE_CSRF_TOKEN, token, csrfCookieOptions);
42
46
  }
@@ -68,7 +72,7 @@ export const csrfProtection = (options = {}) => {
68
72
  "origins to enable origin validation.");
69
73
  }
70
74
  return async (c, next) => {
71
- const secret = getJwtSecret();
75
+ const secret = getCsrfSecret();
72
76
  // Set CSRF cookie on every response if not already present
73
77
  const existingCsrf = getCookie(c, COOKIE_CSRF_TOKEN);
74
78
  if (!existingCsrf) {
@@ -6,7 +6,10 @@ export const errorHandler = async (req, next) => {
6
6
  catch (err) {
7
7
  console.error(err);
8
8
  if (err instanceof HttpError) {
9
- return Response.json({ error: err.message }, { status: err.status });
9
+ const body = { error: err.message };
10
+ if (err.code !== undefined)
11
+ body.code = err.code;
12
+ return Response.json(body, { status: err.status });
10
13
  }
11
14
  return Response.json({ error: "Internal Server Error" }, { status: 500 });
12
15
  }
@@ -2,10 +2,12 @@ import { getCookie } from "hono/cookie";
2
2
  import { verifyToken } from "../lib/jwt";
3
3
  import { getSession, updateSessionLastActive, getSessionFingerprint, setSessionFingerprint } from "../lib/session";
4
4
  import { COOKIE_TOKEN, HEADER_USER_TOKEN } from "../lib/constants";
5
- import { log } from "../lib/logger";
6
- import { getTrackLastActive, getSigningConfig } from "../lib/appConfig";
5
+ import { log, authTrace } from "../lib/logger";
6
+ import { getTrackLastActive, getSigningConfig, getCheckSuspensionOnIdentify } from "../lib/appConfig";
7
+ import { getSuspended } from "../lib/suspension";
7
8
  import { getClientIp } from "../lib/clientIp";
8
- import { sha256 } from "../lib/crypto";
9
+ import { sha256, timingSafeEqual } from "../lib/crypto";
10
+ import { HttpError } from "../lib/HttpError";
9
11
  function computeFingerprint(c, fields) {
10
12
  const parts = fields.map((f) => {
11
13
  if (f === "ip")
@@ -20,20 +22,31 @@ export const identify = async (c, next) => {
20
22
  c.set("authUserId", null);
21
23
  c.set("roles", null);
22
24
  c.set("sessionId", null);
25
+ c.set("authClientId", null);
26
+ c.set("tokenPayload", null);
23
27
  // cookie for browsers, x-user-token header for non-browser clients
24
28
  const token = getCookie(c, COOKIE_TOKEN) ?? c.req.header(HEADER_USER_TOKEN) ?? null;
25
29
  log(`[identify] token=${token ? "present" : "absent"}`);
26
30
  if (token) {
27
31
  try {
28
32
  const payload = await verifyToken(token);
33
+ c.set("tokenPayload", payload);
29
34
  const sessionId = payload.sid;
30
35
  if (!sessionId) {
31
- log("[identify] token missing sid claim — unauthenticated");
36
+ // Check for M2M token (scope present, no sid)
37
+ if (payload.scope && payload.sub) {
38
+ c.set("authClientId", payload.sub);
39
+ log(`[identify] M2M token for clientId=${payload.sub}`);
40
+ }
41
+ else {
42
+ log("[identify] token missing sid claim — unauthenticated");
43
+ }
32
44
  }
33
45
  else {
34
46
  const stored = await getSession(sessionId);
35
- log(`[identify] token for authUserId=${payload.sub} verified, checking session...`);
36
- if (stored === token) {
47
+ log("[identify] token verified, checking session...");
48
+ authTrace(`[identify] authUserId=${payload.sub}`);
49
+ if (timingSafeEqual(stored ?? "", token)) {
37
50
  const signingCfg = getSigningConfig();
38
51
  const bindingCfg = signingCfg?.sessionBinding;
39
52
  if (bindingCfg) {
@@ -45,19 +58,20 @@ export const identify = async (c, next) => {
45
58
  if (storedFp === null) {
46
59
  // First authenticated request — store the fingerprint
47
60
  setSessionFingerprint(sessionId, current).catch(() => {
48
- log(`[identify] failed to store fingerprint for sessionId=${sessionId}`);
61
+ log("[identify] failed to store session fingerprint");
49
62
  });
50
63
  c.set("authUserId", payload.sub);
51
64
  c.set("sessionId", sessionId);
52
65
  }
53
- else if (storedFp === current) {
66
+ else if (timingSafeEqual(storedFp, current)) {
54
67
  c.set("authUserId", payload.sub);
55
68
  c.set("sessionId", sessionId);
56
69
  }
57
70
  else {
58
- log(`[identify] fingerprint mismatch for sessionId=${sessionId} onMismatch=${onMismatch}`);
71
+ log(`[identify] fingerprint mismatch, onMismatch=${onMismatch}`);
72
+ authTrace(`[identify] sessionId=${sessionId}`);
59
73
  if (onMismatch === "reject") {
60
- return c.json({ error: "Unauthorized", code: "FINGERPRINT_MISMATCH" }, 401);
74
+ throw new HttpError(401, "Unauthorized", "FINGERPRINT_MISMATCH");
61
75
  }
62
76
  else if (onMismatch === "log-only") {
63
77
  c.set("authUserId", payload.sub);
@@ -71,10 +85,21 @@ export const identify = async (c, next) => {
71
85
  c.set("sessionId", sessionId);
72
86
  }
73
87
  if (c.get("authUserId")) {
74
- log(`[identify] authUserId=${payload.sub} sessionId=${sessionId}`);
88
+ if (getCheckSuspensionOnIdentify()) {
89
+ const suspensionStatus = await getSuspended(payload.sub).catch(() => ({ suspended: false }));
90
+ if (suspensionStatus.suspended) {
91
+ c.set("authUserId", null);
92
+ c.set("sessionId", null);
93
+ c.set("roles", null);
94
+ log(`[identify] userId=${payload.sub} is suspended — unauthenticated`);
95
+ }
96
+ }
97
+ }
98
+ if (c.get("authUserId")) {
99
+ authTrace(`[identify] authUserId=${payload.sub} sessionId=${sessionId}`);
75
100
  if (getTrackLastActive()) {
76
101
  updateSessionLastActive(sessionId).catch(() => {
77
- log(`[identify] failed to update lastActiveAt for sessionId=${sessionId}`);
102
+ log("[identify] failed to update session lastActiveAt");
78
103
  });
79
104
  }
80
105
  }
@@ -84,7 +109,9 @@ export const identify = async (c, next) => {
84
109
  }
85
110
  }
86
111
  }
87
- catch {
112
+ catch (err) {
113
+ if (err instanceof HttpError)
114
+ throw err;
88
115
  log("[identify] invalid token — unauthenticated");
89
116
  }
90
117
  }
@@ -1,6 +1,7 @@
1
1
  import { getSigningConfig, getSigningSecret } from "../lib/appConfig";
2
2
  import { hmacVerify } from "../lib/signing";
3
3
  import { HEADER_SIGNATURE, HEADER_TIMESTAMP } from "../lib/constants";
4
+ import { HttpError } from "../lib/HttpError";
4
5
  /**
5
6
  * Canonicalize the query string for signing.
6
7
  *
@@ -60,22 +61,22 @@ export const requireSignedRequest = (opts) => async (c, next) => {
60
61
  const rawTs = c.req.header(tsHeader);
61
62
  const tsNum = rawTs !== undefined ? parseInt(rawTs, 10) : NaN;
62
63
  if (isNaN(tsNum)) {
63
- return c.json({ error: "Unauthorized", code: "EXPIRED_TIMESTAMP" }, 401);
64
+ throw new HttpError(401, "Unauthorized", "EXPIRED_TIMESTAMP");
64
65
  }
65
66
  // Auto-detect Unix seconds (< 1e10) vs milliseconds
66
67
  const tsMs = tsNum < 1e10 ? tsNum * 1000 : tsNum;
67
68
  if (Math.abs(Date.now() - tsMs) > tolerance) {
68
- return c.json({ error: "Unauthorized", code: "EXPIRED_TIMESTAMP" }, 401);
69
+ throw new HttpError(401, "Unauthorized", "EXPIRED_TIMESTAMP");
69
70
  }
70
71
  // --- Signature header ---
71
72
  const sig = c.req.header(sigHeader);
72
73
  if (!sig) {
73
- return c.json({ error: "Unauthorized", code: "INVALID_SIGNATURE" }, 401);
74
+ throw new HttpError(401, "Unauthorized", "INVALID_SIGNATURE");
74
75
  }
75
76
  // --- Secret resolution ---
76
77
  const secret = getSigningSecret();
77
78
  if (!secret) {
78
- return c.json({ error: "Internal Server Error", code: "SIGNING_SECRET_MISSING" }, 500);
79
+ throw new HttpError(500, "Internal Server Error", "SIGNING_SECRET_MISSING");
79
80
  }
80
81
  // --- Build canonical string ---
81
82
  const method = c.req.method.toUpperCase();
@@ -93,7 +94,7 @@ export const requireSignedRequest = (opts) => async (c, next) => {
93
94
  valid = false;
94
95
  }
95
96
  if (!valid) {
96
- return c.json({ error: "Unauthorized", code: "INVALID_SIGNATURE" }, 401);
97
+ throw new HttpError(401, "Unauthorized", "INVALID_SIGNATURE");
97
98
  }
98
99
  await next();
99
100
  };
@@ -1,4 +1,5 @@
1
1
  import { getAuthAdapter } from "../lib/authAdapter";
2
+ import { HttpError } from "../lib/HttpError";
2
3
  const EXEMPT_PREFIXES = ["/auth/", "/health", "/docs", "/openapi.json"];
3
4
  /**
4
5
  * Middleware that blocks authenticated users who have not completed MFA setup.
@@ -30,7 +31,7 @@ export const requireMfaSetup = async (c, next) => {
30
31
  }
31
32
  const enabled = await adapter.isMfaEnabled(userId);
32
33
  if (!enabled) {
33
- return c.json({ error: "MFA setup required", code: "MFA_SETUP_REQUIRED" }, 403);
34
+ throw new HttpError(403, "MFA setup required", "MFA_SETUP_REQUIRED");
34
35
  }
35
36
  return next();
36
37
  };
@@ -0,0 +1,10 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+ import type { AppEnv } from "../lib/context";
3
+ /**
4
+ * Middleware that requires the JWT to contain all specified scopes.
5
+ * Reads scope from `tokenPayload.scope` (set by identify middleware).
6
+ *
7
+ * @example
8
+ * router.get("/data", requireScope("read:data"), handler);
9
+ */
10
+ export declare const requireScope: (...requiredScopes: string[]) => MiddlewareHandler<AppEnv>;
@@ -0,0 +1,25 @@
1
+ import { HttpError } from "../lib/HttpError";
2
+ /**
3
+ * Middleware that requires the JWT to contain all specified scopes.
4
+ * Reads scope from `tokenPayload.scope` (set by identify middleware).
5
+ *
6
+ * @example
7
+ * router.get("/data", requireScope("read:data"), handler);
8
+ */
9
+ export const requireScope = (...requiredScopes) => async (c, next) => {
10
+ const payload = c.get("tokenPayload");
11
+ if (!payload) {
12
+ throw new HttpError(401, "Authentication required");
13
+ }
14
+ const scope = payload.scope;
15
+ if (!scope) {
16
+ throw new HttpError(403, "Insufficient scope", "INSUFFICIENT_SCOPE");
17
+ }
18
+ const grantedScopes = scope.split(" ");
19
+ for (const required of requiredScopes) {
20
+ if (!grantedScopes.includes(required)) {
21
+ throw new HttpError(403, "Insufficient scope", "INSUFFICIENT_SCOPE");
22
+ }
23
+ }
24
+ await next();
25
+ };
@@ -0,0 +1,18 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+ import type { AppEnv } from "../lib/context";
3
+ export interface StepUpOptions {
4
+ /** Max age in seconds since last MFA verification. Default: 300 (5 min). */
5
+ maxAge?: number;
6
+ }
7
+ /**
8
+ * Middleware that requires the user to have recently completed step-up MFA.
9
+ *
10
+ * Attach to sensitive routes that require fresh MFA verification:
11
+ * ```
12
+ * router.post("/transfer", userAuth, requireStepUp(), transferHandler);
13
+ * ```
14
+ *
15
+ * The user completes step-up via POST /auth/step-up.
16
+ * After successful step-up, mfaVerifiedAt is stored in their session.
17
+ */
18
+ export declare const requireStepUp: (opts?: StepUpOptions) => MiddlewareHandler<AppEnv>;
@@ -0,0 +1,29 @@
1
+ import { getMfaVerifiedAt } from "../lib/session";
2
+ import { HttpError } from "../lib/HttpError";
3
+ /**
4
+ * Middleware that requires the user to have recently completed step-up MFA.
5
+ *
6
+ * Attach to sensitive routes that require fresh MFA verification:
7
+ * ```
8
+ * router.post("/transfer", userAuth, requireStepUp(), transferHandler);
9
+ * ```
10
+ *
11
+ * The user completes step-up via POST /auth/step-up.
12
+ * After successful step-up, mfaVerifiedAt is stored in their session.
13
+ */
14
+ export const requireStepUp = (opts) => async (c, next) => {
15
+ const sessionId = c.get("sessionId");
16
+ if (!sessionId) {
17
+ throw new HttpError(401, "Authentication required");
18
+ }
19
+ const maxAge = opts?.maxAge ?? 300;
20
+ const verifiedAt = await getMfaVerifiedAt(sessionId);
21
+ if (verifiedAt === null) {
22
+ throw new HttpError(403, "Step-up authentication required", "STEP_UP_REQUIRED");
23
+ }
24
+ const now = Math.floor(Date.now() / 1000);
25
+ if (now - verifiedAt > maxAge) {
26
+ throw new HttpError(403, "Step-up authentication expired", "STEP_UP_REQUIRED");
27
+ }
28
+ await next();
29
+ };
@@ -0,0 +1,8 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+ import type { AppEnv } from "../lib/context";
3
+ export declare function setScimTokens(tokens: string | string[]): void;
4
+ /**
5
+ * Middleware that validates SCIM bearer tokens.
6
+ * Tokens are checked with timingSafeEqual to prevent timing attacks.
7
+ */
8
+ export declare const scimAuth: MiddlewareHandler<AppEnv>;
@@ -0,0 +1,29 @@
1
+ import { timingSafeEqual } from "../lib/crypto";
2
+ import { HttpError } from "../lib/HttpError";
3
+ let _scimTokens = [];
4
+ export function setScimTokens(tokens) {
5
+ _scimTokens = Array.isArray(tokens) ? tokens : [tokens];
6
+ }
7
+ /**
8
+ * Middleware that validates SCIM bearer tokens.
9
+ * Tokens are checked with timingSafeEqual to prevent timing attacks.
10
+ */
11
+ export const scimAuth = async (c, next) => {
12
+ const authHeader = c.req.header("authorization") ?? "";
13
+ if (!authHeader.startsWith("Bearer ")) {
14
+ throw new HttpError(401, "SCIM bearer token required");
15
+ }
16
+ const provided = authHeader.slice(7);
17
+ const valid = _scimTokens.some((token) => {
18
+ try {
19
+ return timingSafeEqual(provided, token);
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ });
25
+ if (!valid) {
26
+ throw new HttpError(401, "Invalid SCIM token");
27
+ }
28
+ await next();
29
+ };
@@ -18,7 +18,7 @@ export interface WebhookAuthOptions {
18
18
  /** Header that carries the signature. Default: `"x-webhook-signature"`. */
19
19
  header?: string;
20
20
  /** HMAC algorithm. Default: `"sha256"`. */
21
- algorithm?: "sha256" | "sha512" | "sha1";
21
+ algorithm?: "sha256" | "sha512";
22
22
  /**
23
23
  * Strip this prefix from the signature header value before comparing.
24
24
  * e.g. `"sha256="` for GitHub-style `X-Hub-Signature-256: sha256=<hex>`.
@@ -1,5 +1,6 @@
1
1
  import { createHmac } from "crypto";
2
2
  import { timingSafeEqual } from "../lib/crypto";
3
+ import { HttpError } from "../lib/HttpError";
3
4
  export const webhookAuth = (options) => async (c, next) => {
4
5
  const algorithm = options.algorithm ?? "sha256";
5
6
  const sigHeader = options.header ?? "x-webhook-signature";
@@ -9,18 +10,18 @@ export const webhookAuth = (options) => async (c, next) => {
9
10
  const rawTs = c.req.header(tsHeader);
10
11
  const tsNum = rawTs !== undefined ? parseInt(rawTs, 10) : NaN;
11
12
  if (isNaN(tsNum)) {
12
- return c.json({ error: "Unauthorized", code: "EXPIRED_TIMESTAMP" }, 401);
13
+ throw new HttpError(401, "Unauthorized", "EXPIRED_TIMESTAMP");
13
14
  }
14
15
  // Auto-detect Unix seconds (< 1e10) vs milliseconds
15
16
  const tsMs = tsNum < 1e10 ? tsNum * 1000 : tsNum;
16
17
  if (Math.abs(Date.now() - tsMs) > tolerance) {
17
- return c.json({ error: "Unauthorized", code: "EXPIRED_TIMESTAMP" }, 401);
18
+ throw new HttpError(401, "Unauthorized", "EXPIRED_TIMESTAMP");
18
19
  }
19
20
  }
20
21
  // --- Signature header ---
21
22
  const rawSig = c.req.header(sigHeader);
22
23
  if (!rawSig) {
23
- return c.json({ error: "Unauthorized", code: "INVALID_SIGNATURE" }, 401);
24
+ throw new HttpError(401, "Unauthorized", "INVALID_SIGNATURE");
24
25
  }
25
26
  const provided = options.prefix && rawSig.startsWith(options.prefix)
26
27
  ? rawSig.slice(options.prefix.length)
@@ -32,7 +33,7 @@ export const webhookAuth = (options) => async (c, next) => {
32
33
  secret = await options.secret(c);
33
34
  }
34
35
  catch {
35
- return c.json({ error: "Internal Server Error", code: "WEBHOOK_SECRET_ERROR" }, 500);
36
+ throw new HttpError(500, "Internal Server Error", "WEBHOOK_SECRET_ERROR");
36
37
  }
37
38
  }
38
39
  else {
@@ -51,7 +52,7 @@ export const webhookAuth = (options) => async (c, next) => {
51
52
  valid = false;
52
53
  }
53
54
  if (!valid) {
54
- return c.json({ error: "Unauthorized", code: "INVALID_SIGNATURE" }, 401);
55
+ throw new HttpError(401, "Unauthorized", "INVALID_SIGNATURE");
55
56
  }
56
57
  await next();
57
58
  };
@@ -25,6 +25,13 @@ interface IAuthUser {
25
25
  name?: string;
26
26
  createdAt: Date;
27
27
  }>;
28
+ displayName?: string;
29
+ firstName?: string;
30
+ lastName?: string;
31
+ externalId?: string;
32
+ suspended: boolean;
33
+ suspendedAt?: Date;
34
+ suspendedReason?: string;
28
35
  }
29
36
  type AuthUserDocument = IAuthUser & Document;
30
37
  export declare const AuthUser: Model<AuthUserDocument, {}, {}, {}, Document<unknown, {}, AuthUserDocument, {}, import("mongoose").DefaultSchemaOptions> & IAuthUser & Document<import("mongoose").Types.ObjectId, any, any, Record<string, any>, {}> & Required<{
@@ -31,6 +31,13 @@ function getAuthUser() {
31
31
  name: { type: String },
32
32
  createdAt: { type: Date, default: Date.now },
33
33
  }],
34
+ displayName: { type: String, default: null },
35
+ firstName: { type: String, default: null },
36
+ lastName: { type: String, default: null },
37
+ externalId: { type: String, default: null, index: true, sparse: true },
38
+ suspended: { type: Boolean, default: false },
39
+ suspendedAt: { type: Date, default: null },
40
+ suspendedReason: { type: String, default: null },
34
41
  }, { timestamps: true });
35
42
  schema.index({ providerIds: 1 });
36
43
  _AuthUser = authConnection.model("AuthUser", schema);
@@ -0,0 +1,18 @@
1
+ import mongoose from "mongoose";
2
+ export interface IM2MClient {
3
+ _id: string;
4
+ clientId: string;
5
+ clientSecretHash: string;
6
+ name: string;
7
+ scopes: string[];
8
+ active: boolean;
9
+ createdAt: Date;
10
+ updatedAt: Date;
11
+ }
12
+ export declare const M2MClient: mongoose.Model<IM2MClient, {}, {}, {}, mongoose.Document<unknown, {}, IM2MClient, {}, mongoose.DefaultSchemaOptions> & IM2MClient & Required<{
13
+ _id: string;
14
+ }> & {
15
+ __v: number;
16
+ } & {
17
+ id: string;
18
+ }, any, IM2MClient>;
@@ -0,0 +1,18 @@
1
+ import mongoose from "mongoose";
2
+ const m2mClientSchema = new mongoose.Schema({
3
+ clientId: { type: String, required: true, unique: true },
4
+ clientSecretHash: { type: String, required: true },
5
+ name: { type: String, required: true },
6
+ scopes: { type: [String], default: [] },
7
+ active: { type: Boolean, default: true },
8
+ }, { timestamps: true });
9
+ // Lazy proxy pattern (same as AuthUser.ts)
10
+ export const M2MClient = new Proxy({}, {
11
+ get(_, prop) {
12
+ const { authConnection } = require("../lib/mongo");
13
+ if (!authConnection)
14
+ throw new Error("authConnection not initialized — call connectAuthMongo() or connectMongo() first");
15
+ const model = authConnection.models["M2MClient"] ?? authConnection.model("M2MClient", m2mClientSchema);
16
+ return model[prop];
17
+ },
18
+ });
@@ -1,4 +1,4 @@
1
- import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig } from "../lib/appConfig";
1
+ import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig, StepUpConfig } from "../lib/appConfig";
2
2
  import type { AuthRateLimitConfig, AccountDeletionConfig } from "../app";
3
3
  export interface AuthRouterOptions {
4
4
  primaryField: PrimaryField;
@@ -7,5 +7,6 @@ export interface AuthRouterOptions {
7
7
  rateLimit?: AuthRateLimitConfig;
8
8
  accountDeletion?: AccountDeletionConfig;
9
9
  refreshTokens?: RefreshTokenConfig;
10
+ stepUp?: StepUpConfig;
10
11
  }
11
- export declare const createAuthRouter: ({ primaryField, emailVerification, passwordReset, rateLimit, accountDeletion, refreshTokens }: AuthRouterOptions) => import("@hono/zod-openapi").OpenAPIHono<import("../lib/context").AppEnv, {}, "/">;
12
+ export declare const createAuthRouter: ({ primaryField, emailVerification, passwordReset, rateLimit, accountDeletion, refreshTokens, stepUp }: AuthRouterOptions) => import("@hono/zod-openapi").OpenAPIHono<import("../lib/context").AppEnv, {}, "/">;