feat: drop outdated session (#14373)

#### PR Dependency Tree


* **PR #14373** 👈

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**
* Added client version tracking and validation to ensure application
compatibility across authentication flows and sessions.
* Enhanced OAuth authentication with improved version handling during
sign-in and refresh operations.

* **Bug Fixes**
* Improved payment callback URL handling with safer defaults for
redirect links.

* **Tests**
* Expanded test coverage for client version enforcement and session
management.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-05 21:35:36 +08:00
committed by GitHub
parent 161eb302fd
commit 944fab36ac
29 changed files with 845 additions and 125 deletions
@@ -86,6 +86,29 @@ test('can create link', t => {
);
});
test('addSimpleQuery should not double encode', t => {
t.is(
t.context.url.addSimpleQuery(
'https://app.affine.local/path',
'redirect_uri',
'/path'
),
'https://app.affine.local/path?redirect_uri=%2Fpath'
);
});
test('addSimpleQuery should allow unescaped value when escape=false', t => {
t.is(
t.context.url.addSimpleQuery(
'https://app.affine.local/path',
'session_id',
'{CHECKOUT_SESSION_ID}',
false
),
'https://app.affine.local/path?session_id={CHECKOUT_SESSION_ID}'
);
});
test('can validate callbackUrl allowlist', t => {
t.true(t.context.url.isAllowedCallbackUrl('/magic-link'));
t.true(
@@ -109,7 +109,7 @@ export class URLHelper {
) {
const urlObj = new URL(url);
if (escape) {
urlObj.searchParams.set(key, encodeURIComponent(value));
urlObj.searchParams.set(key, String(value));
return urlObj.toString();
} else {
const query =
@@ -16,6 +16,7 @@ import type { Request, Response } from 'express';
import { Config } from '../config';
import { getRequestResponseFromContext } from '../utils/request';
import { getRequestTrackerId } from '../utils/request-tracker';
import type { ThrottlerType } from './config';
import { THROTTLER_PROTECTED, Throttlers } from './decorators';
@@ -63,11 +64,8 @@ export class CloudThrottlerGuard extends ThrottlerGuard {
}
override getTracker(req: Request): Promise<string> {
return Promise.resolve(
// ↓ prefer session id if available
`throttler:${req.session?.sessionId ?? req.get('CF-Connecting-IP') ?? req.get('CF-ray') ?? req.ip}`
// ^ throttler prefix make the key in store recognizable
);
// throttler prefix make the key in store recognizable
return Promise.resolve(`throttler:${getRequestTrackerId(req)}`);
}
override generateKey(
@@ -1,6 +1,7 @@
export * from './duration';
export * from './promise';
export * from './request';
export * from './request-tracker';
export * from './ssrf';
export * from './stream';
export * from './types';
@@ -0,0 +1,44 @@
import type { Request } from 'express';
function firstForwardedForIp(value?: string) {
if (!value) {
return;
}
const [first] = value.split(',', 1);
const ip = first?.trim();
return ip || undefined;
}
function firstNonEmpty(...values: Array<string | undefined>) {
for (const value of values) {
const trimmed = value?.trim();
if (trimmed) {
return trimmed;
}
}
return;
}
export function getRequestClientIp(req: Request) {
return firstNonEmpty(
req.get('CF-Connecting-IP'),
firstForwardedForIp(req.get('X-Forwarded-For')),
req.get('X-Real-IP'),
req.ip
)!;
}
export function getRequestTrackerId(req: Request) {
return (
req.session?.sessionId ??
firstNonEmpty(
req.get('CF-Connecting-IP'),
firstForwardedForIp(req.get('X-Forwarded-For')),
req.get('X-Real-IP'),
req.get('CF-Ray'),
req.ip
)!
);
}