Merge branch 'main' into codex/run-release.yml-to-simulate-github-actions-ohq25x
This commit is contained in:
@@ -114,75 +114,6 @@ jobs:
|
||||
name: metapi-dist-${{ github.sha }}
|
||||
path: dist
|
||||
|
||||
|
||||
- name: Normalize downloaded dist layout (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
if (Test-Path 'dist/desktop/main.js') {
|
||||
exit 0
|
||||
}
|
||||
if (Test-Path 'dist/dist/desktop/main.js') {
|
||||
Copy-Item 'dist/dist/*' 'dist/' -Recurse -Force
|
||||
Remove-Item 'dist/dist' -Recurse -Force
|
||||
exit 0
|
||||
}
|
||||
if ((Test-Path 'desktop/main.js') -or (Test-Path 'web/index.html') -or (Test-Path 'server/index.js')) {
|
||||
New-Item -ItemType Directory -Path 'dist' -Force | Out-Null
|
||||
if (Test-Path 'desktop') { Move-Item 'desktop' 'dist/' -Force }
|
||||
if (Test-Path 'web') { Move-Item 'web' 'dist/' -Force }
|
||||
if (Test-Path 'server') { Move-Item 'server' 'dist/' -Force }
|
||||
}
|
||||
if (-not (Test-Path 'dist/desktop/main.js')) {
|
||||
throw 'Missing dist/desktop/main.js after downloading dist artifact'
|
||||
}
|
||||
|
||||
- name: Normalize downloaded dist layout (Linux/macOS)
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -f dist/desktop/main.js ]; then
|
||||
exit 0
|
||||
fi
|
||||
if [ -f dist/dist/desktop/main.js ]; then
|
||||
cp -R dist/dist/. dist/
|
||||
rm -rf dist/dist
|
||||
elif [ -f desktop/main.js ] || [ -f web/index.html ] || [ -f server/index.js ]; then
|
||||
mkdir -p dist
|
||||
[ -d desktop ] && mv desktop dist/
|
||||
[ -d web ] && mv web dist/
|
||||
[ -d server ] && mv server dist/
|
||||
fi
|
||||
test -f dist/desktop/main.js
|
||||
|
||||
- name: Ensure desktop entry exists (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
if (Test-Path 'dist/desktop/main.js') {
|
||||
Write-Host 'Found dist/desktop/main.js from downloaded artifact.'
|
||||
exit 0
|
||||
}
|
||||
Write-Host 'dist/desktop/main.js missing, rebuilding dist locally...'
|
||||
npm run build
|
||||
if (-not (Test-Path 'dist/desktop/main.js')) {
|
||||
throw 'dist/desktop/main.js is still missing after npm run build'
|
||||
}
|
||||
|
||||
- name: Ensure desktop entry exists (Linux/macOS)
|
||||
if: runner.os != 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -f dist/desktop/main.js ]; then
|
||||
echo 'Found dist/desktop/main.js from downloaded artifact.'
|
||||
exit 0
|
||||
fi
|
||||
echo 'dist/desktop/main.js missing, rebuilding dist locally...'
|
||||
npm run build
|
||||
test -f dist/desktop/main.js
|
||||
|
||||
- name: Build desktop artifacts (Windows)
|
||||
if: runner.os == 'Windows'
|
||||
shell: pwsh
|
||||
|
||||
@@ -26,6 +26,12 @@ const COOKIE_ONLY_LOGIN_SESSION = 'cookie-only-session';
|
||||
const COOKIE_SHIELDED_TOKEN = Buffer.from(
|
||||
`1771864970|${Buffer.from('username=linuxdo_131936').toString('base64')}|sig`,
|
||||
).toString('base64');
|
||||
const COOKIE_GOB_USER_TOKEN = Buffer.from(
|
||||
`1772806887|${Buffer.from(
|
||||
'0d7f040102ff8000011001100000ff93ff80000506737472696e670c060004726f6c6503696e740402000206737472696e670c08000673746174757303696e740402000206737472696e670c07000567726f757006737472696e670c09000764656661756c7406737472696e670c040002696403696e74040500fd04683006737472696e670c0a0008757365726e616d6506737472696e670c09000773756974313539',
|
||||
'hex',
|
||||
).toString('base64')}|sig`,
|
||||
).toString('base64');
|
||||
const ANYROUTER_CHALLENGE_HTML = readFileSync(
|
||||
new URL('./__fixtures__/anyrouter-challenge.html', import.meta.url),
|
||||
'utf8',
|
||||
@@ -293,6 +299,20 @@ describe('NewApiAdapter', () => {
|
||||
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' });
|
||||
res.end(JSON.stringify({ success: false, message: 'missing New-Api-User' }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
data: { id: 144408, username: 'suit159', quota: 50000000, used_quota: 0 },
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ success: false, message: 'unauthorized' }));
|
||||
return;
|
||||
@@ -467,6 +487,29 @@ describe('NewApiAdapter', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('extracts gob-encoded user id from anyrouter session cookie when reading balance', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
const balance = await adapter.getBalance(baseUrl, COOKIE_GOB_USER_TOKEN);
|
||||
|
||||
expect(balance.balance).toBe(100);
|
||||
expect(
|
||||
requests.some((r) => r.url === '/api/user/self' && r.headers['new-api-user'] === '144408'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('recovers from mismatched provided user id by probing gob-encoded session payload', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
const balance = await adapter.getBalance(baseUrl, COOKIE_GOB_USER_TOKEN, 159);
|
||||
|
||||
expect(balance.balance).toBe(100);
|
||||
expect(
|
||||
requests.some((r) => r.url === '/api/user/self' && r.headers['new-api-user'] === '159'),
|
||||
).toBe(true);
|
||||
expect(
|
||||
requests.some((r) => r.url === '/api/user/self' && r.headers['new-api-user'] === '144408'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('uses shielded cookie flow for balance and checkin', async () => {
|
||||
const adapter = new NewApiAdapter();
|
||||
const balance = await adapter.getBalance(baseUrl, COOKIE_SHIELDED_TOKEN);
|
||||
|
||||
@@ -72,6 +72,76 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
return null;
|
||||
}
|
||||
|
||||
private decodeBase64BufferLoose(value: string): Buffer | null {
|
||||
if (!value) return null;
|
||||
try {
|
||||
return Buffer.from(value, 'base64');
|
||||
} catch {}
|
||||
try {
|
||||
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
|
||||
return Buffer.from(normalized, 'base64');
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
private decodeGobSignedInt(encoded: Buffer): number | null {
|
||||
if (!encoded.length) return null;
|
||||
|
||||
let unsigned = 0n;
|
||||
if (encoded[0] < 0x80) {
|
||||
unsigned = BigInt(encoded[0]);
|
||||
} else {
|
||||
const width = 0x100 - encoded[0];
|
||||
if (width <= 0 || encoded.length !== width + 1) return null;
|
||||
for (let i = 1; i < encoded.length; i += 1) {
|
||||
unsigned = (unsigned << 8n) | BigInt(encoded[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const signed = (unsigned & 1n) === 0n
|
||||
? unsigned >> 1n
|
||||
: -((unsigned >> 1n) + 1n);
|
||||
if (signed <= 0n || signed > BigInt(Number.MAX_SAFE_INTEGER)) return null;
|
||||
return Number(signed);
|
||||
}
|
||||
|
||||
private extractGobFieldInts(payload: Buffer, fieldName: string): number[] {
|
||||
const ids: number[] = [];
|
||||
const push = (value: number | null) => {
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) return;
|
||||
if (value <= 0 || value > 10_000_000) return;
|
||||
if (!ids.includes(value)) ids.push(value);
|
||||
};
|
||||
|
||||
const marker = Buffer.concat([
|
||||
Buffer.from(fieldName, 'utf8'),
|
||||
Buffer.from([0x03]),
|
||||
Buffer.from('int', 'utf8'),
|
||||
Buffer.from([0x04]),
|
||||
]);
|
||||
|
||||
let start = 0;
|
||||
while (start < payload.length) {
|
||||
const position = payload.indexOf(marker, start);
|
||||
if (position < 0) break;
|
||||
|
||||
const encodedLength = payload[position + marker.length];
|
||||
const delimiter = payload[position + marker.length + 1];
|
||||
if (typeof encodedLength === 'number' && delimiter === 0x00) {
|
||||
const byteLength = encodedLength - 1;
|
||||
const valueStart = position + marker.length + 2;
|
||||
const valueEnd = valueStart + byteLength;
|
||||
if (byteLength > 0 && valueEnd <= payload.length) {
|
||||
push(this.decodeGobSignedInt(payload.subarray(valueStart, valueEnd)));
|
||||
}
|
||||
}
|
||||
|
||||
start = position + marker.length;
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private extractLikelyUserIds(token: string): number[] {
|
||||
const ids: number[] = [];
|
||||
const push = (value: unknown) => {
|
||||
@@ -96,14 +166,20 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
}
|
||||
|
||||
for (const sessionValue of sessionValues) {
|
||||
const decoded = this.decodeBase64Loose(sessionValue);
|
||||
if (!decoded) continue;
|
||||
const decodedBuffer = this.decodeBase64BufferLoose(sessionValue);
|
||||
if (!decodedBuffer) continue;
|
||||
|
||||
const decoded = decodedBuffer.toString('utf8');
|
||||
|
||||
const payloadCandidates: string[] = [decoded];
|
||||
const payloadBuffers: Buffer[] = [decodedBuffer];
|
||||
const parts = decoded.split('|');
|
||||
if (parts.length >= 2) {
|
||||
const middlePayload = this.decodeBase64Loose(parts[1]);
|
||||
if (middlePayload) payloadCandidates.push(middlePayload);
|
||||
const middlePayloadBuffer = this.decodeBase64BufferLoose(parts[1]);
|
||||
if (middlePayloadBuffer) {
|
||||
payloadCandidates.push(middlePayloadBuffer.toString('utf8'));
|
||||
payloadBuffers.push(middlePayloadBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
for (const payload of payloadCandidates) {
|
||||
@@ -114,6 +190,12 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
push(m[1]);
|
||||
}
|
||||
}
|
||||
|
||||
for (const payload of payloadBuffers) {
|
||||
for (const value of this.extractGobFieldInts(payload, 'id')) {
|
||||
push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
@@ -584,6 +666,19 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async probeAlternateUserIdByCookie(
|
||||
baseUrl: string,
|
||||
token: string,
|
||||
currentUserId?: number | null,
|
||||
): Promise<number | null> {
|
||||
const probed = await this.probeUserIdByCookie(baseUrl, token);
|
||||
if (!probed) return null;
|
||||
if (typeof currentUserId === 'number' && currentUserId > 0 && probed === currentUserId) {
|
||||
return null;
|
||||
}
|
||||
return probed;
|
||||
}
|
||||
|
||||
private async getApiTokensByCookie(baseUrl: string, token: string, userId?: number | null): Promise<ApiTokenInfo[]> {
|
||||
for (const cookie of this.buildCookieCandidates(token)) {
|
||||
try {
|
||||
@@ -670,6 +765,16 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const fallbackUserId = await this.probeAlternateUserIdByCookie(baseUrl, accessToken, platformUserId);
|
||||
if (fallbackUserId) {
|
||||
const cookieRetry = await this.fetchUserSelfByCookie(baseUrl, accessToken, fallbackUserId);
|
||||
if (cookieRetry?.success && cookieRetry?.data) {
|
||||
return this.parseUserInfo(cookieRetry.data);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -771,17 +876,15 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
return { tokenType: 'session', userInfo, balance, apiToken };
|
||||
}
|
||||
|
||||
if (!platformUserId) {
|
||||
const cookieUserId = await this.probeUserIdByCookie(baseUrl, token);
|
||||
if (cookieUserId) {
|
||||
const cookieRetry = await this.fetchUserSelfByCookie(baseUrl, token, cookieUserId);
|
||||
if (cookieRetry?.success && cookieRetry?.data) {
|
||||
const userInfo = this.parseUserInfo(cookieRetry.data);
|
||||
const balance = this.parseBalance(cookieRetry.data);
|
||||
let apiToken: string | null = null;
|
||||
try { apiToken = await this.getApiTokenWithUser(baseUrl, token, cookieUserId); } catch {}
|
||||
return { tokenType: 'session', userInfo, balance, apiToken };
|
||||
}
|
||||
const cookieUserId = await this.probeAlternateUserIdByCookie(baseUrl, token, platformUserId);
|
||||
if (cookieUserId) {
|
||||
const cookieRetry = await this.fetchUserSelfByCookie(baseUrl, token, cookieUserId);
|
||||
if (cookieRetry?.success && cookieRetry?.data) {
|
||||
const userInfo = this.parseUserInfo(cookieRetry.data);
|
||||
const balance = this.parseBalance(cookieRetry.data);
|
||||
let apiToken: string | null = null;
|
||||
try { apiToken = await this.getApiTokenWithUser(baseUrl, token, cookieUserId); } catch {}
|
||||
return { tokenType: 'session', userInfo, balance, apiToken };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -839,47 +942,59 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
return { success: false, message: firstFailureMessage };
|
||||
}
|
||||
|
||||
const cookieUserId = resolvedUserId || await this.probeUserIdByCookie(baseUrl, accessToken);
|
||||
for (const cookie of this.buildCookieCandidates(accessToken)) {
|
||||
try {
|
||||
const signInRes = await this.fetchJsonRaw<any>(`${baseUrl}/api/user/sign_in`, {
|
||||
method: 'POST',
|
||||
body: '{}',
|
||||
headers: {
|
||||
Cookie: cookie,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
});
|
||||
if (signInRes?.success) {
|
||||
return {
|
||||
success: true,
|
||||
message: signInRes.message || 'checked in',
|
||||
reward: signInRes.data?.reward?.toString(),
|
||||
};
|
||||
const tryCookieCheckin = async (cookieUserId?: number | null): Promise<CheckinResult | null> => {
|
||||
for (const cookie of this.buildCookieCandidates(accessToken)) {
|
||||
try {
|
||||
const signInRes = await this.fetchJsonRaw<any>(`${baseUrl}/api/user/sign_in`, {
|
||||
method: 'POST',
|
||||
body: '{}',
|
||||
headers: {
|
||||
Cookie: cookie,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
});
|
||||
if (signInRes?.success) {
|
||||
return {
|
||||
success: true,
|
||||
message: signInRes.message || 'checked in',
|
||||
reward: signInRes.data?.reward?.toString(),
|
||||
};
|
||||
}
|
||||
const signInMessage = this.extractResponseMessage(signInRes);
|
||||
if (!firstFailureMessage && signInMessage) firstFailureMessage = signInMessage;
|
||||
} catch (err) {
|
||||
const parsed = this.formatRequestErrorMessage(err);
|
||||
if (!firstFailureMessage && parsed) firstFailureMessage = parsed;
|
||||
}
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = { Cookie: cookie };
|
||||
if (cookieUserId) headers['New-Api-User'] = String(cookieUserId);
|
||||
const res = await this.fetchJsonRaw<any>(`${baseUrl}/api/user/checkin`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
});
|
||||
if (res?.success) {
|
||||
return { success: true, message: res.message || 'checkin success', reward: res.data?.reward?.toString() };
|
||||
}
|
||||
const cookieMessage = this.extractResponseMessage(res);
|
||||
if (cookieMessage) firstFailureMessage = cookieMessage;
|
||||
} catch (err) {
|
||||
const parsed = this.formatRequestErrorMessage(err);
|
||||
if (parsed) firstFailureMessage = parsed;
|
||||
}
|
||||
const signInMessage = this.extractResponseMessage(signInRes);
|
||||
if (!firstFailureMessage && signInMessage) firstFailureMessage = signInMessage;
|
||||
} catch (err) {
|
||||
const parsed = this.formatRequestErrorMessage(err);
|
||||
if (!firstFailureMessage && parsed) firstFailureMessage = parsed;
|
||||
}
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = { Cookie: cookie };
|
||||
if (cookieUserId) headers['New-Api-User'] = String(cookieUserId);
|
||||
const res = await this.fetchJsonRaw<any>(`${baseUrl}/api/user/checkin`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
});
|
||||
if (res?.success) {
|
||||
return { success: true, message: res.message || 'checkin success', reward: res.data?.reward?.toString() };
|
||||
}
|
||||
const cookieMessage = this.extractResponseMessage(res);
|
||||
if (cookieMessage) firstFailureMessage = cookieMessage;
|
||||
} catch (err) {
|
||||
const parsed = this.formatRequestErrorMessage(err);
|
||||
if (parsed) firstFailureMessage = parsed;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const initialCookieResult = await tryCookieCheckin(resolvedUserId);
|
||||
if (initialCookieResult) return initialCookieResult;
|
||||
|
||||
const alternateCookieUserId = await this.probeAlternateUserIdByCookie(baseUrl, accessToken, resolvedUserId);
|
||||
if (alternateCookieUserId) {
|
||||
const retriedCookieResult = await tryCookieCheckin(alternateCookieUserId);
|
||||
if (retriedCookieResult) return retriedCookieResult;
|
||||
}
|
||||
|
||||
return { success: false, message: firstFailureMessage || 'checkin failed' };
|
||||
@@ -917,13 +1032,11 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
return this.parseBalance(cookieRes.data);
|
||||
}
|
||||
|
||||
if (!resolvedUserId) {
|
||||
const cookieUserId = await this.probeUserIdByCookie(baseUrl, accessToken);
|
||||
if (cookieUserId) {
|
||||
const cookieRetry = await this.fetchUserSelfByCookie(baseUrl, accessToken, cookieUserId, rememberFailure);
|
||||
if (cookieRetry?.success && cookieRetry?.data) {
|
||||
return this.parseBalance(cookieRetry.data);
|
||||
}
|
||||
const cookieUserId = await this.probeAlternateUserIdByCookie(baseUrl, accessToken, resolvedUserId);
|
||||
if (cookieUserId) {
|
||||
const cookieRetry = await this.fetchUserSelfByCookie(baseUrl, accessToken, cookieUserId, rememberFailure);
|
||||
if (cookieRetry?.success && cookieRetry?.data) {
|
||||
return this.parseBalance(cookieRetry.data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -952,6 +1065,12 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
const cookieModels = await this.getSessionModelsByCookie(baseUrl, token, userId || platformUserId);
|
||||
if (cookieModels.length > 0) return cookieModels;
|
||||
|
||||
const alternateCookieUserId = await this.probeAlternateUserIdByCookie(baseUrl, token, userId || platformUserId);
|
||||
if (alternateCookieUserId) {
|
||||
const fallbackModels = await this.getSessionModelsByCookie(baseUrl, token, alternateCookieUserId);
|
||||
if (fallbackModels.length > 0) return fallbackModels;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1119,6 +1238,15 @@ export class NewApiAdapter extends BasePlatformAdapter {
|
||||
if (normalized.length > 0) return normalized;
|
||||
} catch {}
|
||||
|
||||
return this.getApiTokensByCookie(baseUrl, accessToken, userId);
|
||||
const cookieTokens = await this.getApiTokensByCookie(baseUrl, accessToken, userId);
|
||||
if (cookieTokens.length > 0) return cookieTokens;
|
||||
|
||||
const alternateCookieUserId = await this.probeAlternateUserIdByCookie(baseUrl, accessToken, userId);
|
||||
if (alternateCookieUserId) {
|
||||
const fallbackTokens = await this.getApiTokensByCookie(baseUrl, accessToken, alternateCookieUserId);
|
||||
if (fallbackTokens.length > 0) return fallbackTokens;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user