mirror of
https://github.com/cita-777/metapi.git
synced 2026-08-31 02:06:09 +08:00
Restore New API session compatibility across user-id headers
New API-compatible deployments disagree on which user-id header session-cookie calls must send, so Metapi should emit the compatible header set wherever it has a resolved platform user id. Constraint: Some New API deployments require X-User-Id while existing integrations already depend on New-Api-User variants. Rejected: Replacing the existing header variants | it would regress deployments that already accept the current names. Confidence: high Scope-risk: narrow Directive: Keep New API session-cookie auth header changes centralized in the shared helper. Tested: GitHub CI for PR #567 passed Test Core, Build Web, Build Server, Build Desktop, Typecheck, schema checks for SQLite/MySQL/Postgres, CodeQL, and audit. Not-tested: No fresh post-merge local runtime smoke test was run.
This commit is contained in:
@@ -14,6 +14,7 @@ interface RequestSnapshot {
|
||||
|
||||
const COOKIE_SESSION_TOKEN = 'cookie-session-token';
|
||||
const COOKIE_REQUIRES_USER_TOKEN = 'cookie-requires-user';
|
||||
const COOKIE_REQUIRES_X_USER_ID_TOKEN = 'cookie-requires-x-user-id';
|
||||
const CHECKIN_ALREADY_TOKEN = 'checkin-already-token';
|
||||
const CHECKIN_INVALID_URL_TOKEN = 'checkin-invalid-url-token';
|
||||
const CHECKIN_INVALID_URL_EXPIRED_SESSION_TOKEN = 'checkin-invalid-url-expired-session-token';
|
||||
@@ -222,6 +223,11 @@ describe('NewApiAdapter', () => {
|
||||
res.end(JSON.stringify({ success: false, message: 'unauthorized' }));
|
||||
return;
|
||||
}
|
||||
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${COOKIE_REQUIRES_X_USER_ID_TOKEN}`) {
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'unauthorized' }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${COOKIE_SESSION_TOKEN}`)) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
@@ -248,6 +254,21 @@ describe('NewApiAdapter', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${COOKIE_REQUIRES_X_USER_ID_TOKEN}`)) {
|
||||
if (req.headers['x-user-id'] !== '448') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'missing X-User-Id' }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
data: {
|
||||
items: [{ key: 'cookie-x-user-id-key' }],
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
data: {
|
||||
@@ -340,6 +361,11 @@ describe('NewApiAdapter', () => {
|
||||
res.end(JSON.stringify({ success: false, message: 'invalid token' }));
|
||||
return;
|
||||
}
|
||||
if (typeof req.headers.authorization === 'string' && req.headers.authorization === `Bearer ${COOKIE_REQUIRES_X_USER_ID_TOKEN}`) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'invalid token' }));
|
||||
return;
|
||||
}
|
||||
if (typeof req.headers.authorization === 'string') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'invalid token' }));
|
||||
@@ -369,6 +395,20 @@ describe('NewApiAdapter', () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${COOKIE_REQUIRES_X_USER_ID_TOKEN}`)) {
|
||||
if (req.headers['x-user-id'] !== '448') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'missing X-User-Id' }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
data: { id: 448, username: 'x-user-id-cookie-user', quota: 1500000, used_quota: 100000 },
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof req.headers.cookie === 'string' && req.headers.cookie.includes(`session=${COOKIE_GOB_USER_TOKEN}`)) {
|
||||
if (req.headers['new-api-user'] !== '144408') {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
@@ -625,6 +665,21 @@ describe('NewApiAdapter', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('sends X-User-Id for cookie sessions when the site requires that New API variant', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
const result = await adapter.verifyToken(baseUrl, COOKIE_REQUIRES_X_USER_ID_TOKEN, 448);
|
||||
|
||||
expect(result.tokenType).toBe('session');
|
||||
expect(result.userInfo?.username).toBe('x-user-id-cookie-user');
|
||||
expect(result.apiToken).toBe('cookie-x-user-id-key');
|
||||
expect(
|
||||
requests.some((r) => r.url === '/api/user/self' && r.headers['x-user-id'] === '448'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
requests.some((r) => r.url?.startsWith('/api/token/') && r.headers['x-user-id'] === '448'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('solves anyrouter acw challenge and probes user id from session payload', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
const result = await adapter.verifyToken(baseUrl, COOKIE_SHIELDED_TOKEN);
|
||||
|
||||
@@ -51,13 +51,21 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
}
|
||||
|
||||
private authHeaders(accessToken: string, userId?: number): Record<string, string> {
|
||||
const headers: Record<string, string> = { Authorization: `Bearer ${accessToken}` };
|
||||
return {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
...this.userIdHeaders(userId),
|
||||
};
|
||||
}
|
||||
|
||||
private userIdHeaders(userId?: number | null): Record<string, string> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (userId) {
|
||||
const value = String(userId);
|
||||
headers['New-API-User'] = value;
|
||||
headers['Veloera-User'] = value;
|
||||
headers['voapi-user'] = value;
|
||||
headers['User-id'] = value;
|
||||
headers['X-User-Id'] = value;
|
||||
headers['Rix-Api-User'] = value;
|
||||
headers['neo-api-user'] = value;
|
||||
}
|
||||
@@ -769,7 +777,7 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
for (const cookie of this.buildCookieCandidates(token)) {
|
||||
try {
|
||||
const headers: Record<string, string> = { Cookie: cookie };
|
||||
if (platformUserId) headers['New-Api-User'] = String(platformUserId);
|
||||
Object.assign(headers, this.userIdHeaders(platformUserId));
|
||||
const res = await this.fetchJsonRaw<any>(`${baseUrl}/api/user/self`, { headers });
|
||||
if (res?.success && res?.data) return res;
|
||||
if (typeof res?.message === 'string' && res.message.trim()) {
|
||||
@@ -786,7 +794,7 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
for (const id of candidates) {
|
||||
try {
|
||||
const res = await this.fetchJsonRaw<any>(`${baseUrl}/api/user/self`, {
|
||||
headers: { Cookie: cookie, 'New-Api-User': String(id) },
|
||||
headers: { Cookie: cookie, ...this.userIdHeaders(id) },
|
||||
});
|
||||
if (res?.success && res?.data) return id;
|
||||
} catch {}
|
||||
@@ -812,7 +820,7 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
for (const cookie of this.buildCookieCandidates(token)) {
|
||||
try {
|
||||
const headers: Record<string, string> = { Cookie: cookie };
|
||||
if (userId) headers['New-Api-User'] = String(userId);
|
||||
Object.assign(headers, this.userIdHeaders(userId));
|
||||
const res = await this.fetchJsonRaw<any>(`${baseUrl}/api/token/?p=0&size=100`, { headers });
|
||||
const normalized = this.normalizeTokenItems(this.parseTokenItems(res));
|
||||
if (normalized.length > 0) return normalized;
|
||||
@@ -825,7 +833,7 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
for (const cookie of this.buildCookieCandidates(token)) {
|
||||
try {
|
||||
const headers: Record<string, string> = { Cookie: cookie };
|
||||
if (userId) headers['New-Api-User'] = String(userId);
|
||||
Object.assign(headers, this.userIdHeaders(userId));
|
||||
const res = await this.fetchJsonRaw<any>(`${baseUrl}/api/user/models`, { headers });
|
||||
if (Array.isArray(res?.data) && res.data.length > 0) return res.data.filter(Boolean);
|
||||
if (res?.data && typeof res.data === 'object') {
|
||||
@@ -1125,7 +1133,7 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = { Cookie: cookie };
|
||||
if (cookieUserId) headers['New-Api-User'] = String(cookieUserId);
|
||||
Object.assign(headers, this.userIdHeaders(cookieUserId));
|
||||
const res = await this.fetchJsonRaw<any>(`${baseUrl}/api/user/checkin`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
@@ -1284,7 +1292,7 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
for (const cookie of this.buildCookieCandidates(accessToken)) {
|
||||
try {
|
||||
const headers: Record<string, string> = { Cookie: cookie };
|
||||
if (cookieUserId) headers['New-Api-User'] = String(cookieUserId);
|
||||
Object.assign(headers, this.userIdHeaders(cookieUserId));
|
||||
const res = await this.fetchJsonRaw<any>(`${baseUrl}/api/token/`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
@@ -1327,7 +1335,7 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
const cookieUserId = resolvedUserId || await this.probeUserIdByCookie(baseUrl, accessToken);
|
||||
for (const cookie of this.buildCookieCandidates(accessToken)) {
|
||||
const headers: Record<string, string> = { Cookie: cookie };
|
||||
if (cookieUserId) headers['New-Api-User'] = String(cookieUserId);
|
||||
Object.assign(headers, this.userIdHeaders(cookieUserId));
|
||||
|
||||
try {
|
||||
const res = await this.fetchJsonRaw<any>(`${baseUrl}/api/user/self/groups`, { headers });
|
||||
@@ -1395,7 +1403,7 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
const cookieUserId = resolvedUserId || await this.probeUserIdByCookie(baseUrl, accessToken);
|
||||
for (const cookie of this.buildCookieCandidates(accessToken)) {
|
||||
const headers: Record<string, string> = { Cookie: cookie };
|
||||
if (cookieUserId) headers['New-Api-User'] = String(cookieUserId);
|
||||
Object.assign(headers, this.userIdHeaders(cookieUserId));
|
||||
|
||||
try {
|
||||
if (!tokenId) {
|
||||
|
||||
Reference in New Issue
Block a user