mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
* chore: update plugin inspector to 0.3.20 * feat: gate plugin compatibility emails on hard errors
259 lines
9.5 KiB
TypeScript
259 lines
9.5 KiB
TypeScript
import { internal } from "./_generated/api";
|
|
import type { Id } from "./_generated/dataModel";
|
|
import { httpAction } from "./_generated/server";
|
|
import type { ActionCtx } from "./_generated/server";
|
|
import { json, parseJsonPayload, text } from "./httpApiV1/shared";
|
|
import { buildLegacyPackageScanZip } from "./lib/skillZip";
|
|
|
|
const internalRefs = internal as unknown as {
|
|
packages: {
|
|
claimPackageInspectorScanBatchInternal: unknown;
|
|
acknowledgePackageInspectorScanBatchInternal: unknown;
|
|
previewPackageInspectorScanBatchInternal: unknown;
|
|
getPackageInspectorArtifactInternal: unknown;
|
|
ingestPackageInspectorScanResultsInternal: unknown;
|
|
sendPackageInspectorFindingsEmailInternal: unknown;
|
|
markPackageInspectorNotificationCompletedInternal: unknown;
|
|
};
|
|
};
|
|
|
|
function readBearerToken(request: Request) {
|
|
return (
|
|
request.headers
|
|
.get("authorization")
|
|
?.match(/^Bearer\s+(.+)$/i)?.[1]
|
|
?.trim() ?? ""
|
|
);
|
|
}
|
|
|
|
function requireWorkerToken(request: Request) {
|
|
const expected = process.env.CLAWHUB_PLUGIN_INSPECTOR_WORKER_TOKEN?.trim() || "";
|
|
if (!expected) return { ok: false as const, response: text("Worker unavailable", 503) };
|
|
if (readBearerToken(request) !== expected) {
|
|
return { ok: false as const, response: text("Unauthorized", 401) };
|
|
}
|
|
return { ok: true as const };
|
|
}
|
|
|
|
export function absolutePackageArtifactUrl(request: Request, releaseId: string) {
|
|
const url = new URL("/api/v1/package-inspector/artifact", request.url);
|
|
url.searchParams.set("releaseId", releaseId);
|
|
return url.toString();
|
|
}
|
|
|
|
function isTruthyParam(value: string | null) {
|
|
if (!value) return false;
|
|
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
}
|
|
|
|
async function runMutationRef<T>(ctx: Pick<ActionCtx, "runMutation">, ref: unknown, args: unknown) {
|
|
return (await ctx.runMutation(ref as never, args as never)) as T;
|
|
}
|
|
|
|
async function runQueryRef<T>(ctx: Pick<ActionCtx, "runQuery">, ref: unknown, args: unknown) {
|
|
return (await ctx.runQuery(ref as never, args as never)) as T;
|
|
}
|
|
|
|
async function runActionRef<T>(ctx: Pick<ActionCtx, "runAction">, ref: unknown, args: unknown) {
|
|
return (await ctx.runAction(ref as never, args as never)) as T;
|
|
}
|
|
|
|
export const packageInspectorClaimHttp = httpAction(async (ctx, request) => {
|
|
const auth = requireWorkerToken(request);
|
|
if (!auth.ok) return auth.response;
|
|
const url = new URL(request.url);
|
|
const batchSize = Number(url.searchParams.get("batchSize") ?? "25");
|
|
const cursor = url.searchParams.get("cursor");
|
|
const runId = url.searchParams.get("runId")?.trim() || undefined;
|
|
const dryRun = isTruthyParam(url.searchParams.get("dryRun"));
|
|
const inspectorVersion = url.searchParams.get("inspectorVersion")?.trim() || undefined;
|
|
const targetOpenClawVersion = url.searchParams.get("targetOpenClawVersion")?.trim() || undefined;
|
|
const notifyOwners = isTruthyParam(url.searchParams.get("notifyOwners"));
|
|
if (!dryRun && !runId) return text("Missing runId", 400);
|
|
type ClaimResult = {
|
|
ok: true;
|
|
leased: boolean;
|
|
nextCursor: string | null;
|
|
items: Array<{
|
|
packageId: string;
|
|
releaseId: string;
|
|
ownerUserId?: string;
|
|
ownerPublisherId?: string;
|
|
packageName: string;
|
|
version: string;
|
|
artifactKind: string;
|
|
}>;
|
|
};
|
|
const claimArgs = {
|
|
batchSize: Number.isFinite(batchSize) ? batchSize : undefined,
|
|
...(dryRun || cursor ? { cursor } : {}),
|
|
...(runId ? { runId } : {}),
|
|
...(inspectorVersion ? { inspectorVersion } : {}),
|
|
...(targetOpenClawVersion ? { targetOpenClawVersion } : {}),
|
|
...(notifyOwners ? { notifyOwners: true } : {}),
|
|
};
|
|
const result = dryRun
|
|
? await runQueryRef<ClaimResult>(
|
|
ctx,
|
|
internalRefs.packages.previewPackageInspectorScanBatchInternal,
|
|
claimArgs,
|
|
)
|
|
: await runMutationRef<ClaimResult>(
|
|
ctx,
|
|
internalRefs.packages.claimPackageInspectorScanBatchInternal,
|
|
claimArgs,
|
|
);
|
|
return json({
|
|
...result,
|
|
dryRun,
|
|
items: result.items.map((item) => ({
|
|
...item,
|
|
downloadUrl: absolutePackageArtifactUrl(request, item.releaseId),
|
|
})),
|
|
});
|
|
});
|
|
|
|
export const packageInspectorAcknowledgeHttp = httpAction(async (ctx, request) => {
|
|
const auth = requireWorkerToken(request);
|
|
if (!auth.ok) return auth.response;
|
|
const url = new URL(request.url);
|
|
const runId = url.searchParams.get("runId")?.trim();
|
|
if (!runId) return text("Missing runId", 400);
|
|
const cursor = url.searchParams.get("cursor")?.trim() || null;
|
|
const result = await runMutationRef<{
|
|
ok: true;
|
|
cursor: string | null;
|
|
completed: boolean;
|
|
}>(ctx, internalRefs.packages.acknowledgePackageInspectorScanBatchInternal, {
|
|
runId,
|
|
cursor,
|
|
});
|
|
return json(result);
|
|
});
|
|
|
|
export const packageInspectorArtifactHttp = httpAction(async (ctx, request) => {
|
|
const auth = requireWorkerToken(request);
|
|
if (!auth.ok) return auth.response;
|
|
const releaseId = new URL(request.url).searchParams.get("releaseId")?.trim();
|
|
if (!releaseId) return text("Missing releaseId", 400);
|
|
const artifact = await runQueryRef<{
|
|
packageName: string;
|
|
version: string;
|
|
artifactKind: "legacy-zip" | "npm-pack";
|
|
clawpackStorageId?: string;
|
|
clawpackSha256?: string;
|
|
npmIntegrity?: string;
|
|
npmShasum?: string;
|
|
npmTarballName?: string;
|
|
files: Array<{ path: string; storageId: string }>;
|
|
} | null>(ctx, internalRefs.packages.getPackageInspectorArtifactInternal, {
|
|
releaseId,
|
|
});
|
|
if (!artifact) return text("Artifact not found", 404);
|
|
|
|
if (artifact.artifactKind === "npm-pack") {
|
|
if (!artifact.clawpackStorageId) return text("Artifact not found", 404);
|
|
const blob = await ctx.storage.get(artifact.clawpackStorageId as Id<"_storage">);
|
|
if (!blob) return text("Artifact not found", 404);
|
|
const headers: Record<string, string> = {
|
|
"Content-Type": "application/octet-stream",
|
|
"Content-Disposition": `attachment; filename="${artifact.npmTarballName ?? `${artifact.packageName.replaceAll("/", "-")}-${artifact.version}.tgz`}"`,
|
|
"X-ClawHub-Artifact-Type": "npm-pack-tarball",
|
|
};
|
|
if (artifact.clawpackSha256) {
|
|
headers.ETag = `"sha256:${artifact.clawpackSha256}"`;
|
|
headers["X-ClawHub-Artifact-Sha256"] = artifact.clawpackSha256;
|
|
}
|
|
if (artifact.npmIntegrity) headers["X-ClawHub-Npm-Integrity"] = artifact.npmIntegrity;
|
|
if (artifact.npmShasum) headers["X-ClawHub-Npm-Shasum"] = artifact.npmShasum;
|
|
return new Response(blob, { status: 200, headers });
|
|
}
|
|
|
|
const entries: Array<{ path: string; bytes: Uint8Array }> = [];
|
|
for (const file of artifact.files) {
|
|
const blob = await ctx.storage.get(file.storageId as Id<"_storage">);
|
|
if (!blob) return text(`Missing stored file: ${file.path}`, 500);
|
|
entries.push({
|
|
path: file.path,
|
|
bytes: new Uint8Array(await blob.arrayBuffer()),
|
|
});
|
|
}
|
|
const zip = buildLegacyPackageScanZip(entries);
|
|
return new Response(new Blob([zip], { type: "application/zip" }), {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/zip",
|
|
"Content-Disposition": `attachment; filename="${artifact.packageName.replaceAll("/", "-")}-${artifact.version}.zip"`,
|
|
"X-ClawHub-Artifact-Type": "legacy-plugin-zip",
|
|
},
|
|
});
|
|
});
|
|
|
|
export const packageInspectorResultsHttp = httpAction(async (ctx, request) => {
|
|
const auth = requireWorkerToken(request);
|
|
if (!auth.ok) return auth.response;
|
|
const parsed = await parseJsonPayload(request, {});
|
|
if (!parsed.ok) return parsed.response;
|
|
const payload = parsed.payload;
|
|
const result = await runMutationRef<{
|
|
ok: true;
|
|
inserted: number;
|
|
shouldEmailOwner: boolean;
|
|
}>(ctx, internalRefs.packages.ingestPackageInspectorScanResultsInternal, {
|
|
packageId: payload.packageId,
|
|
releaseId: payload.releaseId,
|
|
inspectorVersion: payload.inspectorVersion,
|
|
targetOpenClawVersion: payload.targetOpenClawVersion,
|
|
notifyOwners: payload.notifyOwners === true,
|
|
findings: Array.isArray(payload.findings) ? payload.findings : [],
|
|
});
|
|
if (payload.notifyOwners === true && result.shouldEmailOwner) {
|
|
try {
|
|
await runActionRef(ctx, internalRefs.packages.sendPackageInspectorFindingsEmailInternal, {
|
|
packageId: payload.packageId,
|
|
releaseId: payload.releaseId,
|
|
inspectorVersion: payload.inspectorVersion,
|
|
targetOpenClawVersion: payload.targetOpenClawVersion,
|
|
});
|
|
} catch (error) {
|
|
console.error("Package Inspector findings email failed", error);
|
|
}
|
|
}
|
|
return json(result);
|
|
});
|
|
|
|
export const packageInspectorNotifyHttp = httpAction(async (ctx, request) => {
|
|
const auth = requireWorkerToken(request);
|
|
if (!auth.ok) return auth.response;
|
|
const parsed = await parseJsonPayload(request, {});
|
|
if (!parsed.ok) return parsed.response;
|
|
const payload = parsed.payload;
|
|
if (
|
|
typeof payload.inspectorVersion !== "string" ||
|
|
!payload.inspectorVersion.trim() ||
|
|
typeof payload.targetOpenClawVersion !== "string" ||
|
|
!payload.targetOpenClawVersion.trim()
|
|
) {
|
|
return text("Missing exact scan identity", 400);
|
|
}
|
|
const args = {
|
|
packageId: payload.packageId,
|
|
releaseId: payload.releaseId,
|
|
inspectorVersion: payload.inspectorVersion,
|
|
targetOpenClawVersion: payload.targetOpenClawVersion,
|
|
};
|
|
const result = await runActionRef<{
|
|
ok: true;
|
|
sent: boolean;
|
|
reason?: "no-context";
|
|
}>(ctx, internalRefs.packages.sendPackageInspectorFindingsEmailInternal, args);
|
|
if (!result.sent && result.reason === "no-context") {
|
|
await runMutationRef(
|
|
ctx,
|
|
internalRefs.packages.markPackageInspectorNotificationCompletedInternal,
|
|
args,
|
|
);
|
|
}
|
|
return json(result);
|
|
});
|