fix: close marketplace client review findings
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user