@@ -72,6 +72,7 @@ type ClawHubPublishablePluginPackageFilters = {
|
72 | 72 | }; |
73 | 73 | |
74 | 74 | const CLAWHUB_DEFAULT_REGISTRY = "https://clawhub.ai"; |
| 75 | +const CLAWHUB_RESPONSE_BODY_MAX_BYTES = 1024 * 1024; |
75 | 76 | const SAFE_EXTENSION_ID_RE = /^[a-z0-9][a-z0-9._-]*$/; |
76 | 77 | const CLAWHUB_SHARED_RELEASE_INPUT_PATHS = [ |
77 | 78 | ".github/workflows/plugin-clawhub-release.yml", |
@@ -98,6 +99,63 @@ function getRegistryBaseUrl(explicit?: string) {
|
98 | 99 | ); |
99 | 100 | } |
100 | 101 | |
| 102 | +async function readBoundedResponseText( |
| 103 | +response: Response, |
| 104 | +label: string, |
| 105 | +maxBytes = CLAWHUB_RESPONSE_BODY_MAX_BYTES, |
| 106 | +): Promise<string> { |
| 107 | +const contentLength = Number.parseInt(response.headers.get("content-length") ?? "", 10); |
| 108 | +if (Number.isFinite(contentLength) && contentLength > maxBytes) { |
| 109 | +throw new Error(`${label} response body exceeded ${maxBytes} bytes.`); |
| 110 | +} |
| 111 | + |
| 112 | +if (!response.body) { |
| 113 | +return ""; |
| 114 | +} |
| 115 | + |
| 116 | +const reader = response.body.getReader(); |
| 117 | +const decoder = new TextDecoder(); |
| 118 | +const chunks: string[] = []; |
| 119 | +let totalBytes = 0; |
| 120 | +let canceled = false; |
| 121 | + |
| 122 | +try { |
| 123 | +for (;;) { |
| 124 | +const { done, value } = await reader.read(); |
| 125 | +if (done) { |
| 126 | +const tail = decoder.decode(); |
| 127 | +if (tail) { |
| 128 | +chunks.push(tail); |
| 129 | +} |
| 130 | +break; |
| 131 | +} |
| 132 | + |
| 133 | +totalBytes += value.byteLength; |
| 134 | +if (totalBytes > maxBytes) { |
| 135 | +canceled = true; |
| 136 | +await reader.cancel().catch(() => undefined); |
| 137 | +throw new Error(`${label} response body exceeded ${maxBytes} bytes.`); |
| 138 | +} |
| 139 | +chunks.push(decoder.decode(value, { stream: true })); |
| 140 | +} |
| 141 | +} finally { |
| 142 | +if (!canceled) { |
| 143 | +reader.releaseLock(); |
| 144 | +} |
| 145 | +} |
| 146 | + |
| 147 | +return chunks.join(""); |
| 148 | +} |
| 149 | + |
| 150 | +async function readClawHubPackageOwnerDetail( |
| 151 | +response: Response, |
| 152 | +packageName: string, |
| 153 | +): Promise<ClawHubPackageOwnerDetail> { |
| 154 | +return JSON.parse( |
| 155 | +await readBoundedResponseText(response, `ClawHub package owner response for ${packageName}`), |
| 156 | +) as ClawHubPackageOwnerDetail; |
| 157 | +} |
| 158 | + |
101 | 159 | export function collectClawHubPublishablePluginPackages( |
102 | 160 | rootDir = resolve("."), |
103 | 161 | filters: ClawHubPublishablePluginPackageFilters = {}, |
@@ -390,7 +448,7 @@ export async function collectClawHubOpenClawOwnerErrors(params: {
|
390 | 448 | return; |
391 | 449 | } |
392 | 450 | |
393 | | -const detail = (await response.json()) as ClawHubPackageOwnerDetail; |
| 451 | +const detail = await readClawHubPackageOwnerDetail(response, plugin.packageName); |
394 | 452 | const ownerHandle = typeof detail.owner?.handle === "string" ? detail.owner.handle : null; |
395 | 453 | if (ownerHandle !== requiredOwnerHandle) { |
396 | 454 | errors.push( |
|