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
+53 -24
View File
@@ -1,4 +1,19 @@
import { openDB } from 'idb';
import { Auth } from './plugins/auth';
function authEndpointForUrl(url: string | URL) {
try {
const parsed = new URL(url, globalThis.location.origin);
return parsed.protocol === 'http:' || parsed.protocol === 'https:'
? parsed.origin
: null;
} catch {
return null;
}
}
function canonicalEndpoint(endpoint: string) {
return authEndpointForUrl(endpoint) ?? endpoint;
}
/**
* the below code includes the custom fetch and xmlhttprequest implementation for ios webview.
@@ -8,9 +23,11 @@ const rawFetch = globalThis.fetch;
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
const request = new Request(input, init);
const origin = new URL(request.url, globalThis.location.origin).origin;
const origin = authEndpointForUrl(request.url);
const token = await readEndpointToken(origin);
const token = origin
? await readEndpointToken(origin).catch(() => null)
: null;
if (token) {
request.headers.set('Authorization', `Bearer ${token}`);
}
@@ -19,11 +36,30 @@ globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
};
const rawXMLHttpRequest = globalThis.XMLHttpRequest;
const xhrRequestUrls = new WeakMap<XMLHttpRequest, string>();
globalThis.XMLHttpRequest = class extends rawXMLHttpRequest {
override send(body?: Document | XMLHttpRequestBodyInit | null): void {
const origin = new URL(this.responseURL, globalThis.location.origin).origin;
override open(
method: string,
url: string | URL,
async: boolean = true,
username?: string | null,
password?: string | null
): void {
xhrRequestUrls.set(this, url.toString());
return super.open(
method,
url,
async,
username ?? undefined,
password ?? undefined
);
}
readEndpointToken(origin).then(
override send(body?: Document | XMLHttpRequestBodyInit | null): void {
const requestUrl = xhrRequestUrls.get(this);
const origin = authEndpointForUrl(requestUrl ?? globalThis.location.href);
(origin ? readEndpointToken(origin) : Promise.resolve(null)).then(
token => {
if (token) {
this.setRequestHeader('Authorization', `Bearer ${token}`);
@@ -31,7 +67,7 @@ globalThis.XMLHttpRequest = class extends rawXMLHttpRequest {
return super.send(body);
},
() => {
throw new Error('Failed to read token');
return super.send(body);
}
);
}
@@ -40,26 +76,19 @@ globalThis.XMLHttpRequest = class extends rawXMLHttpRequest {
export async function readEndpointToken(
endpoint: string
): Promise<string | null> {
const idb = await openDB('affine-token', 1, {
upgrade(db) {
if (!db.objectStoreNames.contains('tokens')) {
db.createObjectStore('tokens', { keyPath: 'endpoint' });
}
},
const { token } = await Auth.readEndpointToken({
endpoint: canonicalEndpoint(endpoint),
});
const token = await idb.get('tokens', endpoint);
return token ? token.token : null;
return token ?? null;
}
export async function writeEndpointToken(endpoint: string, token: string) {
const db = await openDB('affine-token', 1, {
upgrade(db) {
if (!db.objectStoreNames.contains('tokens')) {
db.createObjectStore('tokens', { keyPath: 'endpoint' });
}
},
await Auth.writeEndpointToken({
endpoint: canonicalEndpoint(endpoint),
token,
});
await db.put('tokens', { endpoint, token });
}
export async function deleteEndpointToken(endpoint: string) {
await Auth.deleteEndpointToken({ endpoint: canonicalEndpoint(endpoint) });
}