fix: close marketplace client review findings

This commit is contained in:
2026-08-28 21:01:51 +08:00
parent 8dfa542860
commit 1614f7efc1
25 changed files with 1224 additions and 143 deletions

View File

@@ -6,7 +6,7 @@ import {
getWorksSquareAccountBinding,
subscribeWorksSquareSession,
} from '../services/works-square-session';
import { proxyAwareFetch, fetchWithDeadline } from '../utils/proxy-fetch';
import { proxyAwareFetch, runWithDeadline } from '../utils/proxy-fetch';
import {
AccountPluginCache,
type AccountBinding,
@@ -202,7 +202,17 @@ export type MarketplaceErrorCode =
| 'marketplace_response_invalid'
| 'marketplace_response_too_large'
| 'marketplace_download_invalid'
| 'marketplace_beta_selection_required';
| 'marketplace_beta_selection_required'
| 'plugin_auth_required'
| 'plugin_account_changed'
| 'plugin_library_required'
| 'plugin_release_not_ready'
| 'plugin_release_yanked'
| 'plugin_incompatible_client'
| 'plugin_signature_invalid'
| 'plugin_artifact_invalid'
| 'plugin_runtime_suspended'
| 'plugin_backend_unavailable';
export class MarketplaceClientError extends Error {
constructor(
@@ -236,6 +246,19 @@ function fail(code: MarketplaceErrorCode, message: string = code, status = 0): n
throw new MarketplaceClientError(code, status, message);
}
const SERVER_ERROR_CODES = new Set<MarketplaceErrorCode>([
'plugin_auth_required',
'plugin_account_changed',
'plugin_library_required',
'plugin_release_not_ready',
'plugin_release_yanked',
'plugin_incompatible_client',
'plugin_signature_invalid',
'plugin_artifact_invalid',
'plugin_runtime_suspended',
'plugin_backend_unavailable',
]);
function exactKeys(
value: UnknownRecord,
required: readonly string[],
@@ -586,8 +609,7 @@ async function readBoundedBytes(response: Response, maximum: number): Promise<Ui
return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
}
async function readJson(response: Response, maximum: number): Promise<unknown> {
const bytes = await readBoundedBytes(response, maximum);
function parseJsonBytes(bytes: Uint8Array): unknown {
let source: string;
try {
source = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
@@ -602,6 +624,48 @@ async function readJson(response: Response, maximum: number): Promise<unknown> {
}
}
function failForResponse(
response: Response,
bytes: Uint8Array | null,
fallbackMessage: string,
): never {
let code: MarketplaceErrorCode | null = null;
let responseMessage = fallbackMessage;
if (bytes && bytes.byteLength > 0) {
try {
const payload = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes)) as unknown;
if (isRecord(payload)) {
if (typeof payload.code === 'string' && SERVER_ERROR_CODES.has(payload.code as MarketplaceErrorCode)) {
code = payload.code as MarketplaceErrorCode;
}
if (typeof payload.error === 'string' && payload.error.length > 0 && payload.error.length <= 256) {
responseMessage = payload.error;
}
}
} catch {
// A malformed error body must stay a bounded generic client failure.
}
}
fail(code ?? 'marketplace_request_failed', responseMessage, response.status);
}
async function fetchResponseWithBodyDeadline(
fetchImpl: FetchImplementation,
input: string | URL,
init: RequestInit,
timeoutMs: number,
maximum: number,
): Promise<{ response: Response; bytes: Uint8Array | null }> {
return runWithDeadline(async (signal) => {
const response = await fetchImpl(input, { ...init, signal });
if (response.status >= 300 && response.status < 400) {
await response.body?.cancel().catch(() => undefined);
return { response, bytes: null };
}
return { response, bytes: await readBoundedBytes(response, maximum) };
}, timeoutMs, init.signal);
}
function normalizedBase(value: string): string {
try {
const url = new URL(value);
@@ -955,7 +1019,7 @@ class MarketplaceClientImpl implements MarketplaceClient {
let url = localUrl.toString();
let sendAuthorization = true;
for (let redirect = 0; redirect <= 3; redirect += 1) {
const response = await fetchWithDeadline(
const { response, bytes } = await fetchResponseWithBodyDeadline(
this.fetchImpl as typeof fetch,
url,
{
@@ -967,6 +1031,7 @@ class MarketplaceClientImpl implements MarketplaceClient {
redirect: 'manual',
},
this.requestTimeoutMs,
Math.min(this.maxArtifactBytes, grant.sizeBytes),
);
if (response.status === 401 && sendAuthorization && !refreshed) {
await response.body?.cancel().catch(() => undefined);
@@ -995,10 +1060,9 @@ class MarketplaceClientImpl implements MarketplaceClient {
continue;
}
if (!response.ok) {
await response.body?.cancel().catch(() => undefined);
fail(response.status === 401 ? 'marketplace_auth_required' : 'marketplace_request_failed', 'Marketplace download failed', response.status);
failForResponse(response, bytes, 'Marketplace download failed');
}
const bytes = await readBoundedBytes(response, Math.min(this.maxArtifactBytes, grant.sizeBytes));
if (!bytes) fail('marketplace_download_invalid', 'Marketplace response body is unavailable');
if (bytes.byteLength !== grant.sizeBytes) fail('marketplace_download_invalid', 'Marketplace artifact size does not match its grant');
this.assertBinding(binding);
return bytes;
@@ -1083,11 +1147,12 @@ class MarketplaceClientImpl implements MarketplaceClient {
...(token ? { Authorization: `Bearer ${token}` } : {}),
...(options.etag ? { 'If-None-Match': options.etag } : {}),
};
const response = await fetchWithDeadline(
const { response, bytes } = await fetchResponseWithBodyDeadline(
this.fetchImpl as typeof fetch,
url,
{ method, headers, body, redirect: 'manual' },
this.requestTimeoutMs,
this.maxResponseBytes,
);
if (response.status === 401 && !refreshed && token) {
await response.body?.cancel().catch(() => undefined);
@@ -1102,10 +1167,10 @@ class MarketplaceClientImpl implements MarketplaceClient {
return { status: response.status, headers: response.headers, value: null, notModified: true };
}
if (!response.ok) {
await response.body?.cancel().catch(() => undefined);
fail(response.status === 401 ? 'marketplace_auth_required' : 'marketplace_request_failed', 'Marketplace request failed', response.status);
failForResponse(response, bytes, 'Marketplace request failed');
}
const value = parser(await readJson(response, this.maxResponseBytes));
if (!bytes) fail('marketplace_response_invalid', 'Marketplace response body is unavailable');
const value = parser(parseJsonBytes(bytes));
if (binding) this.assertBinding(binding);
return { status: response.status, headers: response.headers, value, notModified: false };
}