fix(api): expose legacy zip artifact aliases
Some checks are pending
CI / types-build (push) Waiting to run
CI / static (push) Waiting to run
CI / unit (push) Waiting to run
CI / packages (push) Waiting to run
CI / e2e-http (push) Waiting to run
CI / playwright-smoke (push) Waiting to run
Security Gate: Secret Scanning / Scan for Verified Secrets (push) Waiting to run

Expose legacy ZIP resolver compatibility aliases without confusing publish-time content hashes for downloaded archive integrity.
This commit is contained in:
Vincent Koc 2026-05-03 10:34:37 -07:00 committed by GitHub
parent 59fc54ff64
commit 199e6a0cdf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 198 additions and 6 deletions

View File

@ -3775,6 +3775,142 @@ describe("httpApiV1 handlers", () => {
});
});
it("package artifact endpoint exposes legacy zip resolver compatibility aliases", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("version" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "publishers:demo", handle: "demo" },
};
}
if ("name" in args && "version" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
},
version: {
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "legacy-zip",
integritySha256: "a".repeat(64),
sha256hash: "b".repeat(64),
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/versions/1.0.0/artifact"),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
package: { name: "demo-plugin" },
version: "1.0.0",
artifact: {
kind: "legacy-zip",
sha256: "b".repeat(64),
format: "zip",
source: "clawhub",
artifactKind: "legacy-zip",
artifactSha256: "b".repeat(64),
packageName: "demo-plugin",
version: "1.0.0",
downloadUrl: "https://example.com/api/v1/packages/demo-plugin/download?version=1.0.0",
legacyDownloadUrl: "https://example.com/api/v1/packages/demo-plugin/download?version=1.0.0",
},
});
});
it("package artifact endpoint omits legacy zip archive aliases when archive hash is missing", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("version" in args)) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
tags: { latest: "packageReleases:1" },
latestReleaseId: "packageReleases:1",
channel: "community",
isOfficial: false,
createdAt: 1,
updatedAt: 1,
},
latestRelease: null,
owner: { _id: "publishers:demo", handle: "demo" },
};
}
if ("name" in args && "version" in args) {
return {
package: {
_id: "packages:demo-plugin",
name: "demo-plugin",
displayName: "Demo Plugin",
family: "code-plugin",
},
version: {
_id: "packageReleases:1",
packageId: "packages:demo-plugin",
version: "1.0.0",
createdAt: 1,
changelog: "Initial release",
distTags: ["latest"],
files: [],
artifactKind: "legacy-zip",
integritySha256: "a".repeat(64),
},
};
}
return null;
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/demo-plugin/versions/1.0.0/artifact"),
);
expect(response.status).toBe(200);
const body = await response.json();
expect(body).toMatchObject({
artifact: {
kind: "legacy-zip",
format: "zip",
source: "clawhub",
artifactKind: "legacy-zip",
packageName: "demo-plugin",
version: "1.0.0",
},
});
expect(body.artifact).not.toHaveProperty("sha256");
expect(body.artifact).not.toHaveProperty("artifactSha256");
});
it("package artifact endpoint accepts split scoped package paths", async () => {
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if ("name" in args && !("version" in args)) {

View File

@ -364,7 +364,7 @@ function getReleaseSecurityBlock(release: ReleaseLike) {
return getPackageDownloadSecurityBlock(release);
}
function toReleaseArtifact(release: ReleaseLike) {
function toReleaseArtifact(release: ReleaseLike, packageName?: string) {
if (release.artifactKind === "npm-pack") {
return {
kind: "npm-pack" as const,
@ -378,10 +378,16 @@ function toReleaseArtifact(release: ReleaseLike) {
npmFileCount: release.npmFileCount,
};
}
const sha256 = release.sha256hash;
return {
kind: "legacy-zip" as const,
sha256: release.integritySha256 ?? release.sha256hash,
...(sha256 ? { sha256 } : {}),
format: "zip",
source: "clawhub" as const,
artifactKind: "legacy-zip" as const,
...(sha256 ? { artifactSha256: sha256 } : {}),
packageName,
version: release.version,
};
}
@ -2347,7 +2353,7 @@ export async function packagesGetRouterV1Handler(ctx: ActionCtx, request: Reques
},
version: release.version,
artifact: {
...toReleaseArtifact(release),
...toReleaseArtifact(release, publicPackage!.name),
...releaseArtifactUrls(request, publicPackage!.name, release),
},
},
@ -2427,7 +2433,7 @@ export async function packagesGetRouterV1Handler(ctx: ActionCtx, request: Reques
compatibility: result.version.compatibility ?? null,
capabilities: result.version.capabilities ?? null,
verification: result.version.verification ?? null,
artifact: toReleaseArtifact(result.version),
artifact: toReleaseArtifact(result.version, result.package.name),
sha256hash: result.version.sha256hash ?? null,
vtAnalysis: result.version.vtAnalysis ?? null,
llmAnalysis: result.version.llmAnalysis ?? null,
@ -2669,7 +2675,7 @@ export async function npmMirrorGetHandler(ctx: ActionCtx, request: Request) {
const versions = Object.fromEntries(
releases.map((release) => {
const artifact = toReleaseArtifact(release);
const artifact = toReleaseArtifact(release, detail.package!.name);
const urls = releaseArtifactUrls(request, detail.package!.name, release);
return [
release.version,

View File

@ -103,6 +103,11 @@ export const PackageArtifactSummarySchema = type({
npmTarballName: "string?",
npmUnpackedSize: "number?",
npmFileCount: "number?",
source: '"clawhub"?',
artifactKind: PackageArtifactKindSchema.optional(),
artifactSha256: "string?",
packageName: "string?",
version: "string?",
});
export type PackageArtifactSummary = (typeof PackageArtifactSummarySchema)[inferred];
@ -315,6 +320,11 @@ export const ApiV1PackageArtifactResponseSchema = type({
downloadUrl: "string",
tarballUrl: "string?",
legacyDownloadUrl: "string?",
source: '"clawhub"?',
artifactKind: PackageArtifactKindSchema.optional(),
artifactSha256: "string?",
packageName: "string?",
version: "string?",
}),
});
export type ApiV1PackageArtifactResponse = (typeof ApiV1PackageArtifactResponseSchema)[inferred];

View File

@ -79,6 +79,11 @@ export declare const PackageArtifactSummarySchema: import("arktype/internal/vari
npmTarballName?: string | undefined;
npmUnpackedSize?: number | undefined;
npmFileCount?: number | undefined;
source?: "clawhub" | undefined;
artifactKind?: "legacy-zip" | "npm-pack" | undefined;
artifactSha256?: string | undefined;
packageName?: string | undefined;
version?: string | undefined;
}, {}>;
export type PackageArtifactSummary = (typeof PackageArtifactSummarySchema)[inferred];
export declare const PackagePublishArtifactSchema: import("arktype/internal/variants/object.ts").ObjectType<{
@ -331,6 +336,11 @@ export declare const ApiV1PackageResponseSchema: import("arktype/internal/varian
npmTarballName?: string | undefined;
npmUnpackedSize?: number | undefined;
npmFileCount?: number | undefined;
source?: "clawhub" | undefined;
artifactKind?: "legacy-zip" | "npm-pack" | undefined;
artifactSha256?: string | undefined;
packageName?: string | undefined;
version?: string | undefined;
} | null | undefined;
stats?: {
downloads: number;
@ -414,6 +424,11 @@ export declare const ApiV1PackageVersionResponseSchema: import("arktype/internal
npmTarballName?: string | undefined;
npmUnpackedSize?: number | undefined;
npmFileCount?: number | undefined;
source?: "clawhub" | undefined;
artifactKind?: "legacy-zip" | "npm-pack" | undefined;
artifactSha256?: string | undefined;
packageName?: string | undefined;
version?: string | undefined;
} | null | undefined;
sha256hash?: string | null | undefined;
vtAnalysis?: {
@ -477,6 +492,11 @@ export declare const ApiV1PackageArtifactResponseSchema: import("arktype/interna
npmFileCount?: number | undefined;
tarballUrl?: string | undefined;
legacyDownloadUrl?: string | undefined;
source?: "clawhub" | undefined;
artifactKind?: "legacy-zip" | "npm-pack" | undefined;
artifactSha256?: string | undefined;
packageName?: string | undefined;
version?: string | undefined;
};
}, {}>;
export type ApiV1PackageArtifactResponse = (typeof ApiV1PackageArtifactResponseSchema)[inferred];

View File

@ -64,6 +64,11 @@ export const PackageArtifactSummarySchema = type({
npmTarballName: "string?",
npmUnpackedSize: "number?",
npmFileCount: "number?",
source: '"clawhub"?',
artifactKind: PackageArtifactKindSchema.optional(),
artifactSha256: "string?",
packageName: "string?",
version: "string?",
});
export const PackagePublishArtifactSchema = type({
kind: '"npm-pack"',
@ -249,6 +254,11 @@ export const ApiV1PackageArtifactResponseSchema = type({
downloadUrl: "string",
tarballUrl: "string?",
legacyDownloadUrl: "string?",
source: '"clawhub"?',
artifactKind: PackageArtifactKindSchema.optional(),
artifactSha256: "string?",
packageName: "string?",
version: "string?",
}),
});
export const PackageReleaseModerationRequestSchema = type({

File diff suppressed because one or more lines are too long

View File

@ -103,6 +103,11 @@ export const PackageArtifactSummarySchema = type({
npmTarballName: "string?",
npmUnpackedSize: "number?",
npmFileCount: "number?",
source: '"clawhub"?',
artifactKind: PackageArtifactKindSchema.optional(),
artifactSha256: "string?",
packageName: "string?",
version: "string?",
});
export type PackageArtifactSummary = (typeof PackageArtifactSummarySchema)[inferred];
@ -321,6 +326,11 @@ export const ApiV1PackageArtifactResponseSchema = type({
downloadUrl: "string",
tarballUrl: "string?",
legacyDownloadUrl: "string?",
source: '"clawhub"?',
artifactKind: PackageArtifactKindSchema.optional(),
artifactSha256: "string?",
packageName: "string?",
version: "string?",
}),
});
export type ApiV1PackageArtifactResponse = (typeof ApiV1PackageArtifactResponseSchema)[inferred];