(
jwt: { verify: (token: string) => Promise<unknown> },
cookieValue: unknown
)
| 95 | * the two guards interpret differently. |
| 96 | */ |
| 97 | const verifyAuthCookie = async ( |
| 98 | jwt: { verify: (token: string) => Promise<unknown> }, |
| 99 | cookieValue: unknown |
| 100 | ): Promise<{ user: IUser; accountId: string } | null> => { |
| 101 | if (cookieValue === undefined) { |
| 102 | return null; |
| 103 | } |
| 104 | |
| 105 | if (typeof cookieValue !== "string") { |
| 106 | throw ApiErrors.unauthorized("Invalid authentication cookie"); |
| 107 | } |
| 108 | |
| 109 | try { |
| 110 | const verified = await jwt.verify(cookieValue); |
| 111 | const parsed = parseAuthJWTPayload(verified); |
| 112 | |
| 113 | if (parsed.kind !== "ok") { |
| 114 | throw ApiErrors.unauthorized("Invalid token payload"); |
| 115 | } |
| 116 | |
| 117 | await assertNotRevoked(parsed); |
| 118 | |
| 119 | const user = await db.query.users.findFirst({ |
| 120 | where: eq(users.id, parsed.userId), |
| 121 | }); |
| 122 | |
| 123 | if (!user) { |
| 124 | throw ApiErrors.unauthorized("User not found"); |
| 125 | } |
| 126 | |
| 127 | /* |
| 128 | * Tag the active Sentry scope with the user so any error captured |
| 129 | * for the rest of this request carries `user.id` + `user.email`. |
| 130 | * The Pino mixin also reads this scope to inject `userId` on every |
| 131 | * log record, so a Grafana log line and a GlitchTip error event can |
| 132 | * be correlated by the same id. No-op when SENTRY_DSN is unset. |
| 133 | */ |
| 134 | Sentry.setUser({ id: user.id, email: user.email }); |
| 135 | |
| 136 | return { user, accountId: parsed.accountId }; |
| 137 | } catch (err: unknown) { |
| 138 | return translateJwtError(err); |
| 139 | } |
| 140 | }; |
| 141 | |
| 142 | /** |
| 143 | * Required-auth guard. Use on every endpoint where an anonymous caller |
no test coverage detected