feat(server): passkey pre-refactor (#15060)

#### PR Dependency Tree


* **PR #15060** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* OpenApp native sign-in and native session exchange (JWT) for mobile &
desktop.
  * Centralized short-lived auth challenge store for one-time tokens.
* Encrypted per-endpoint token storage and native token handlers
(Android, iOS, Electron).

* **Improvements**
* Richer auth-method reporting (password, magic link, OAuth, passkey)
and improved sign-in flows.
* Hardened magic-link, OAuth, and session issuance; JWT-backed sessions
and websocket JWT support.
* UX tweaks: form-based password submit, OTP autocomplete, adjusted
captcha flow.

* **Bug Fixes**
  * Expanded tests and auth-state resets to avoid cross-test leakage.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-06-01 17:11:15 +08:00
committed by GitHub
parent 5b9d51b41b
commit ce9841df9d
74 changed files with 3719 additions and 939 deletions
+23 -2
View File
@@ -7,6 +7,14 @@ export interface CacheSetOptions {
ttl?: number;
}
const GET_AND_DELETE_LUA = `
local value = redis.call("GET", KEYS[1])
if value then
redis.call("DEL", KEYS[1])
end
return value
`;
export function isValidCacheTtl(ttl: unknown): ttl is number {
return typeof ttl === 'number' && Number.isSafeInteger(ttl) && ttl > 0;
}
@@ -32,12 +40,15 @@ export class CacheProvider {
value: T,
opts: CacheSetOptions = {}
): Promise<boolean> {
if (opts.ttl) {
if (isValidCacheTtl(opts.ttl)) {
return this.redis
.set(key, JSON.stringify(value), 'PX', opts.ttl)
.then(() => true)
.catch(() => false);
}
if (opts.ttl !== undefined) {
return false;
}
return this.redis
.set(key, JSON.stringify(value))
@@ -58,12 +69,15 @@ export class CacheProvider {
value: T,
opts: CacheSetOptions = {}
): Promise<boolean> {
if (opts.ttl) {
if (isValidCacheTtl(opts.ttl)) {
return this.redis
.set(key, JSON.stringify(value), 'PX', opts.ttl, 'NX')
.then(v => !!v)
.catch(() => false);
}
if (opts.ttl !== undefined) {
return false;
}
return this.redis
.set(key, JSON.stringify(value), 'NX')
@@ -78,6 +92,13 @@ export class CacheProvider {
.catch(() => false);
}
async getAndDelete<T = unknown>(key: string): Promise<T | undefined> {
return this.redis
.eval(GET_AND_DELETE_LUA, 1, key)
.then(v => (typeof v === 'string' ? JSON.parse(v) : undefined))
.catch(() => undefined);
}
async has(key: string): Promise<boolean> {
return this.redis
.exists(key)
@@ -7,12 +7,16 @@ import { GqlArgumentsHost } from '@nestjs/graphql';
import type { Request, Response } from 'express';
import { ClsServiceManager } from 'nestjs-cls';
import type { Socket } from 'socket.io';
import { z } from 'zod';
type RequestResponse = {
req: Request;
res?: Response;
};
const RequestCookieValueSchema = z.string().min(1);
const RequestHeaderValueSchema = z.string().min(1);
export function getRequestResponseFromHost(
host: ArgumentsHost
): RequestResponse {
@@ -68,9 +72,7 @@ export function getRequestResponseFromContext(
export function parseCookies(
req: IncomingMessage & { cookies?: Record<string, string> }
) {
if (req.cookies) {
return;
}
if (req.cookies) return;
const cookieStr = req.headers.cookie ?? '';
req.cookies = cookieStr.split(';').reduce(
@@ -103,6 +105,25 @@ export function parseCookies(
);
}
export function getRequestCookie(
req: IncomingMessage & { cookies?: Record<string, unknown> },
name: string
) {
parseCookies(req as IncomingMessage & { cookies?: Record<string, string> });
const value = req.cookies?.[name];
const parsed = RequestCookieValueSchema.safeParse(value);
return parsed.success ? parsed.data : undefined;
}
export function getRequestHeader(req: IncomingMessage, name: string) {
const value = req.headers[name.toLowerCase()];
const parsed = RequestHeaderValueSchema.safeParse(value);
return parsed.success ? parsed.data : undefined;
}
/**
* Request type
*