feat(packages): resolve package appeals

This commit is contained in:
Vincent Koc
2026-05-02 10:50:57 -07:00
parent 6e5578ee6d
commit 28da510571
16 changed files with 946 additions and 1 deletions
+89
View File
@@ -4075,6 +4075,95 @@ describe("httpApiV1 handlers", () => {
});
});
it("package appeals lists moderator appeal intake", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
items: [
{
appealId: "packageAppeals:1",
packageId: "packages:1",
releaseId: "packageReleases:1",
name: "@scope/demo",
displayName: "Demo",
family: "code-plugin",
version: "1.2.3",
message: "please review",
status: "open",
createdAt: 123,
submitter: { userId: "users:owner", handle: "owner", displayName: "Owner" },
resolvedAt: null,
resolvedBy: null,
resolutionNote: null,
},
],
nextCursor: null,
done: true,
};
});
const runMutation = vi.fn().mockResolvedValue(okRate());
const response = await __handlers.packagesGetRouterV1Handler(
makeCtx({ runQuery, runMutation }),
new Request("https://example.com/api/v1/packages/appeals?status=open&limit=10", {
headers: { Authorization: "Bearer clh_test" },
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
items: [{ appealId: "packageAppeals:1", name: "@scope/demo" }],
});
expect(runQuery).toHaveBeenCalledWith(internal.packages.listPackageAppealsInternal, {
actorUserId: "users:moderator",
cursor: null,
limit: 10,
status: "open",
});
});
it("package appeal resolve posts moderator decisions", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
user: { _id: "users:moderator", role: "moderator" },
} as never);
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
appealId: "packageAppeals:1",
packageId: "packages:1",
releaseId: "packageReleases:1",
status: "rejected",
};
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/appeals/packageAppeals%3A1/resolve", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ status: "rejected", note: "scanner finding still applies" }),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({ status: "rejected" });
expect(runMutation).toHaveBeenCalledWith(
internal.packages.resolvePackageAppealForUserInternal,
{
actorUserId: "users:moderator",
appealId: "packageAppeals:1",
status: "rejected",
note: "scanner finding still applies",
},
);
});
it("package artifact backfill posts admin dry-run requests", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
+78
View File
@@ -1,6 +1,7 @@
import {
PackageArtifactBackfillRequestSchema,
ApiV1PackageModerationStatusResponseSchema,
PackageAppealResolveRequestSchema,
PackageAppealRequestSchema,
PackageReportRequestSchema,
PackageReportTriageRequestSchema,
@@ -9,6 +10,7 @@ import {
PackageTrustedPublisherUpsertRequestSchema,
PublishTokenMintRequestSchema,
parseArk,
type PackageAppealListStatus,
type PackageModerationQueueStatus,
type PackageReportListStatus,
} from "clawhub-schema";
@@ -80,6 +82,8 @@ const internalRefs = internal as unknown as {
triagePackageReportForUserInternal: unknown;
getPackageModerationStatusForUserInternal: unknown;
submitPackageAppealForUserInternal: unknown;
listPackageAppealsInternal: unknown;
resolvePackageAppealForUserInternal: unknown;
backfillPackageArtifactKindsInternal: unknown;
listPackageModerationQueueInternal: unknown;
};
@@ -204,6 +208,20 @@ function parsePackageReportListStatus(value: string | null): PackageReportListSt
return null;
}
function parsePackageAppealListStatus(value: string | null): PackageAppealListStatus | null {
if (!value) return "open";
const normalized = value.trim().toLowerCase();
if (
normalized === "open" ||
normalized === "accepted" ||
normalized === "rejected" ||
normalized === "all"
) {
return normalized;
}
return null;
}
type PackageListQueryArgs = {
family?: "skill" | "code-plugin" | "bundle-plugin";
channel?: "official" | "community" | "private";
@@ -1428,6 +1446,46 @@ export async function packagesPostRouterV1Handler(ctx: ActionCtx, request: Reque
}
}
if (
segments[0] === "appeals" &&
segments[1] &&
segments[2] === "resolve" &&
segments.length === 3
) {
const rate = await applyRateLimit(ctx, request, "write");
if (!rate.ok) return rate.response;
const auth = await requireApiTokenUserOrResponse(ctx, request, rate.headers);
if (!auth.ok) return auth.response;
try {
const body = parseArk(
PackageAppealResolveRequestSchema,
await request.json(),
"Package appeal resolve payload",
) as {
status: "open" | "accepted" | "rejected";
note?: string;
};
const result = await runMutationRef(
ctx,
internalRefs.packages.resolvePackageAppealForUserInternal,
{
actorUserId: auth.userId,
appealId: segments[1] as Id<"packageAppeals">,
status: body.status,
...(body.note ? { note: body.note } : {}),
},
);
return json(result, 200, rate.headers);
} catch (error) {
return text(
error instanceof Error ? error.message : "Package appeal resolve failed",
400,
rate.headers,
);
}
}
if (
segments[1] === "versions" &&
segments[2] &&
@@ -1941,6 +1999,26 @@ export async function packagesGetRouterV1Handler(ctx: ActionCtx, request: Reques
return json(result, 200, rate.headers);
}
if (segments[0] === "appeals" && segments.length === 1) {
const rate = await applyRateLimit(ctx, request, "read");
if (!rate.ok) return rate.response;
const auth = await requireApiTokenUserOrResponse(ctx, request, rate.headers);
if (!auth.ok) return auth.response;
const url = new URL(request.url);
const status = parsePackageAppealListStatus(url.searchParams.get("status"));
if (!status) return text("Invalid package appeal status", 400, rate.headers);
const limit = Math.max(1, Math.min(toOptionalNumber(url.searchParams.get("limit")) ?? 25, 100));
const cursor = url.searchParams.get("cursor");
const result = await runQueryRef(ctx, internalRefs.packages.listPackageAppealsInternal, {
actorUserId: auth.userId,
cursor: cursor ?? null,
limit,
status,
});
return json(result, 200, rate.headers);
}
if (segments[1] === "moderation" && segments.length === 2) {
const rate = await applyRateLimit(ctx, request, "read");
if (!rate.ok) return rate.response;
+150
View File
@@ -16,6 +16,8 @@ import {
reportPackageForUserInternal,
triagePackageReportForUserInternal,
submitPackageAppealForUserInternal,
listPackageAppealsInternal,
resolvePackageAppealForUserInternal,
getVersionByName,
insertReleaseInternal,
listPackageModerationQueueInternal,
@@ -310,6 +312,38 @@ const submitPackageAppealForUserInternalHandler = (
}
>
)._handler;
const listPackageAppealsInternalHandler = (
listPackageAppealsInternal as unknown as WrappedHandler<
{
actorUserId: string;
cursor?: string | null;
limit?: number;
status?: "open" | "accepted" | "rejected" | "all";
},
{
items: Array<{ appealId: string; name: string; status: string; message: string }>;
nextCursor: string | null;
done: boolean;
}
>
)._handler;
const resolvePackageAppealForUserInternalHandler = (
resolvePackageAppealForUserInternal as unknown as WrappedHandler<
{
actorUserId: string;
appealId: string;
status: "open" | "accepted" | "rejected";
note?: string;
},
{
ok: true;
appealId: string;
packageId: string;
releaseId: string;
status: string;
}
>
)._handler;
const getPackageReleaseScanBackfillBatchInternalHandler = (
getPackageReleaseScanBackfillBatchInternal as unknown as WrappedHandler<
{
@@ -3823,6 +3857,122 @@ describe("packages public queries", () => {
}),
);
});
it("lists package appeals for moderators", async () => {
const result = await listPackageAppealsInternalHandler(
{
db: {
get: vi.fn(async (id: string) => {
if (id === "users:moderator") return { _id: id, role: "moderator" };
if (id === "users:owner") return { _id: id, handle: "owner" };
if (id === "packages:demo") return makePackageDoc({ name: "@scope/demo" });
return null;
}),
query: vi.fn((table: string) => {
if (table !== "packageAppeals") throw new Error(`Unexpected table ${table}`);
return {
withIndex: vi.fn(() => ({
order: vi.fn(() => ({
paginate: vi.fn().mockResolvedValue({
page: [
{
_id: "packageAppeals:1",
packageId: "packages:demo",
releaseId: "packageReleases:demo-1",
version: "1.2.3",
userId: "users:owner",
message: "please review",
status: "open",
createdAt: 123,
},
],
continueCursor: null,
isDone: true,
}),
})),
})),
};
}),
},
} as never,
{ actorUserId: "users:moderator", status: "open", limit: 10 },
);
expect(result).toEqual({
items: [
expect.objectContaining({
appealId: "packageAppeals:1",
name: "@scope/demo",
version: "1.2.3",
message: "please review",
status: "open",
submitter: expect.objectContaining({ handle: "owner" }),
}),
],
nextCursor: null,
done: true,
});
});
it("resolves package appeals for moderators", async () => {
const patch = vi.fn();
const insert = vi.fn(async () => "auditLogs:1");
const result = await resolvePackageAppealForUserInternalHandler(
{
db: {
get: vi.fn(async (id: string) => {
if (id === "users:moderator") return { _id: id, role: "moderator" };
if (id === "packageAppeals:1") {
return {
_id: id,
packageId: "packages:demo",
releaseId: "packageReleases:demo-1",
version: "1.2.3",
userId: "users:owner",
message: "please review",
status: "open",
createdAt: 123,
};
}
if (id === "packages:demo") return makePackageDoc({ name: "@scope/demo" });
return null;
}),
patch,
insert,
query: vi.fn(),
replace: vi.fn(),
delete: vi.fn(),
normalizeId: vi.fn(),
},
} as never,
{
actorUserId: "users:moderator",
appealId: "packageAppeals:1",
status: "rejected",
note: "scanner finding still applies",
},
);
expect(result).toMatchObject({
ok: true,
appealId: "packageAppeals:1",
status: "rejected",
});
expect(patch).toHaveBeenCalledWith("packageAppeals:1", {
status: "rejected",
resolvedAt: expect.any(Number),
resolvedBy: "users:moderator",
resolutionNote: "scanner finding still applies",
});
expect(insert).toHaveBeenCalledWith(
"auditLogs",
expect.objectContaining({
action: "package.appeal.resolve",
targetType: "packageAppeal",
}),
);
});
});
describe("package scan backfill", () => {
+145
View File
@@ -215,6 +215,27 @@ type PackageReportListItem = {
triagedBy?: Id<"users"> | null;
triageNote?: string | null;
};
type PackageAppealStatus = "open" | "accepted" | "rejected";
type PackageAppealListItem = {
appealId: Id<"packageAppeals">;
packageId: Id<"packages">;
releaseId: Id<"packageReleases">;
name: string;
displayName: string;
family: PackageFamily;
version: string;
message: string;
status: PackageAppealStatus;
createdAt: number;
submitter: {
userId: Id<"users">;
handle?: string | null;
displayName?: string | null;
};
resolvedAt?: number | null;
resolvedBy?: Id<"users"> | null;
resolutionNote?: string | null;
};
type PackageModerationStatus = {
package: {
packageId: Id<"packages">;
@@ -2637,6 +2658,130 @@ export const submitPackageAppealForUserInternal = internalMutation({
},
});
function toPackageAppealListItem(
appeal: Doc<"packageAppeals">,
pkg: Doc<"packages">,
submitter: Doc<"users"> | null,
): PackageAppealListItem {
return {
appealId: appeal._id,
packageId: pkg._id,
releaseId: appeal.releaseId,
name: pkg.name,
displayName: pkg.displayName,
family: pkg.family,
version: appeal.version,
message: appeal.message,
status: appeal.status,
createdAt: appeal.createdAt,
submitter: {
userId: appeal.userId,
handle: submitter?.handle ?? null,
displayName: submitter?.displayName ?? submitter?.name ?? null,
},
resolvedAt: appeal.resolvedAt ?? null,
resolvedBy: appeal.resolvedBy ?? null,
resolutionNote: appeal.resolutionNote ?? null,
};
}
export const listPackageAppealsInternal = internalQuery({
args: {
actorUserId: v.id("users"),
cursor: v.optional(v.union(v.string(), v.null())),
limit: v.optional(v.number()),
status: v.optional(
v.union(v.literal("open"), v.literal("accepted"), v.literal("rejected"), v.literal("all")),
),
},
handler: async (ctx, args) => {
const actor = await ctx.db.get(args.actorUserId);
if (!actor || actor.deletedAt || actor.deactivatedAt) throw new ConvexError("Unauthorized");
assertModerator(actor);
const limit = Math.max(1, Math.min(Math.round(args.limit ?? 25), 100));
const status = args.status ?? "open";
const appealQuery =
status === "all"
? ctx.db.query("packageAppeals").withIndex("by_createdAt", (q) => q)
: ctx.db
.query("packageAppeals")
.withIndex("by_status_createdAt", (q) => q.eq("status", status));
const page = await appealQuery.order("desc").paginate({
cursor: args.cursor ?? null,
numItems: limit,
});
const items: PackageAppealListItem[] = [];
for (const appeal of page.page) {
const pkg = await ctx.db.get(appeal.packageId);
if (!pkg || pkg.softDeletedAt || pkg.family === "skill") continue;
const submitter = await ctx.db.get(appeal.userId);
items.push(toPackageAppealListItem(appeal, pkg, submitter));
}
return {
items,
nextCursor: page.isDone ? null : page.continueCursor,
done: page.isDone,
};
},
});
export const resolvePackageAppealForUserInternal = internalMutation({
args: {
actorUserId: v.id("users"),
appealId: v.id("packageAppeals"),
status: v.union(v.literal("open"), v.literal("accepted"), v.literal("rejected")),
note: v.optional(v.string()),
},
handler: async (ctx, args) => {
const actor = await ctx.db.get(args.actorUserId);
if (!actor || actor.deletedAt || actor.deactivatedAt) throw new ConvexError("Unauthorized");
assertModerator(actor);
const appeal = await ctx.db.get(args.appealId);
if (!appeal) throw new ConvexError("Package appeal not found");
const pkg = await ctx.db.get(appeal.packageId);
if (!pkg || pkg.softDeletedAt) throw new ConvexError("Package appeal not found");
const note = args.note?.trim();
const isOpen = args.status === "open";
if (!isOpen && !note) throw new ConvexError("Resolution note required.");
const now = Date.now();
await ctx.db.patch(appeal._id, {
status: args.status,
resolvedAt: isOpen ? undefined : now,
resolvedBy: isOpen ? undefined : actor._id,
resolutionNote: isOpen ? undefined : note?.slice(0, MAX_APPEAL_MESSAGE_LENGTH),
});
await ctx.db.insert("auditLogs", {
actorUserId: actor._id,
action: "package.appeal.resolve",
targetType: "packageAppeal",
targetId: appeal._id,
metadata: {
packageId: pkg._id,
releaseId: appeal.releaseId,
packageName: pkg.name,
version: appeal.version,
status: args.status,
},
createdAt: now,
});
return {
ok: true as const,
appealId: appeal._id,
packageId: pkg._id,
releaseId: appeal.releaseId,
status: args.status,
};
},
});
export const listPackageModerationQueueInternal = internalQuery({
args: {
actorUserId: v.id("users"),
+1
View File
@@ -1322,6 +1322,7 @@ const packageAppeals = defineTable({
createdAt: v.number(),
})
.index("by_release_status_createdAt", ["releaseId", "status", "createdAt"])
.index("by_createdAt", ["createdAt"])
.index("by_status_createdAt", ["status", "createdAt"])
.index("by_user_createdAt", ["userId", "createdAt"]);
+35
View File
@@ -367,6 +367,41 @@ Example:
clawhub package appeal @openclaw/example-plugin --version 1.2.3 --message "linked source release explains the native binary"
```
### `package appeals`
- Moderator/admin command for listing package appeals.
- Calls `GET /api/v1/packages/appeals`.
- Flags:
- `--status open|accepted|rejected|all`: appeal state filter, default `open`.
- `--cursor <cursor>`: resume cursor from a previous page.
- `--limit <n>`: number of appeals to show, max 100.
- `--json`: machine-readable output.
Examples:
```bash
clawhub package appeals
clawhub package appeals --status all --limit 50
```
### `package resolve-appeal`
- Moderator/admin command for accepting, rejecting, or reopening a package
appeal.
- Calls `POST /api/v1/packages/appeals/{appealId}/resolve`.
- Resolving an appeal does not automatically change release moderation state;
use `package moderate` to approve, quarantine, or revoke the artifact.
- Flags:
- `--status open|accepted|rejected`: required appeal state.
- `--note <text>`: required unless `--status open`.
- `--json`: machine-readable output.
Example:
```bash
clawhub package resolve-appeal packageAppeals:abc --status rejected --note "static finding still applies"
```
### `package reports`
- Moderator/admin command for listing package reports.
+72
View File
@@ -556,6 +556,78 @@ Response:
}
```
### `GET /api/v1/packages/appeals`
Moderator/admin endpoint for package appeal intake.
Auth:
- Requires an API token for a moderator or admin user.
Query params:
- `status` (optional): `open` (default), `accepted`, `rejected`, or `all`
- `limit` (optional): integer (1-100)
- `cursor` (optional): pagination cursor
Response:
```json
{
"items": [
{
"appealId": "packageAppeals:...",
"packageId": "packages:...",
"releaseId": "packageReleases:...",
"name": "@openclaw/example-plugin",
"displayName": "Example Plugin",
"family": "code-plugin",
"version": "1.2.3",
"message": "The native binary is signed.",
"status": "open",
"createdAt": 1730000000000,
"submitter": {
"userId": "users:...",
"handle": "publisher",
"displayName": "Publisher"
},
"resolvedAt": null,
"resolvedBy": null,
"resolutionNote": null
}
],
"nextCursor": null,
"done": true
}
```
### `POST /api/v1/packages/appeals/{appealId}/resolve`
Moderator/admin endpoint for accepting, rejecting, or reopening an appeal.
Request:
```json
{ "status": "rejected", "note": "Static finding still applies." }
```
`note` is required for `accepted` and `rejected`; it may be omitted when
setting `status` back to `open`. Resolving an appeal does not automatically
change release moderation state; use release moderation to approve, quarantine,
or revoke the artifact.
Response:
```json
{
"ok": true,
"appealId": "packageAppeals:...",
"packageId": "packages:...",
"releaseId": "packageReleases:...",
"status": "rejected"
}
```
### `GET /api/v1/packages/reports`
Moderator/admin endpoint for package report intake.
+3
View File
@@ -51,6 +51,9 @@ See also: [acceptable-usage.md](./acceptable-usage.md) for the marketplace polic
- Package owners and publisher members can submit one open appeal per moderated
package release. Appeals are audit-logged and do not automatically approve or
unblock a release.
- Moderators can accept, reject, or reopen appeals with a resolution note.
Appeal resolution is audit-logged and intentionally separate from changing
release moderation state.
- Public queries hide non-active moderation statuses; staff can still access via
staff-only queries and unhide/restore/delete/ban.
- Skills directory supports an optional "Hide suspicious" filter to exclude
+31
View File
@@ -24,12 +24,14 @@ import {
cmdInspectPackage,
cmdDeletePackageTrustedPublisher,
cmdListPackageReports,
cmdListPackageAppeals,
cmdModeratePackageRelease,
cmdPackageModerationStatus,
cmdPackageModerationQueue,
cmdPackageReadiness,
cmdPublishPackage,
cmdReportPackage,
cmdResolvePackageAppeal,
cmdSetPackageTrustedPublisher,
cmdTriagePackageReport,
cmdVerifyPackage,
@@ -493,6 +495,35 @@ packageCmd
await cmdAppealPackage(opts, name, options);
});
packageCmd
.command("appeals")
.description("List package appeals for moderator review")
.option("--status <status>", "open|accepted|rejected|all", "open")
.option("--cursor <cursor>", "Resume cursor")
.option(
"--limit <n>",
"Number of appeals to show (max 100)",
(value) => Number.parseInt(value, 10),
25,
)
.option("--json", "Output JSON")
.action(async (options) => {
const opts = await resolveGlobalOpts();
await cmdListPackageAppeals(opts, options);
});
packageCmd
.command("resolve-appeal")
.description("Resolve or reopen a package appeal")
.argument("<appeal-id>", "Package appeal id")
.requiredOption("--status <status>", "open|accepted|rejected")
.option("--note <text>", "Resolution note; required unless reopening")
.option("--json", "Output JSON")
.action(async (appealId, options) => {
const opts = await resolveGlobalOpts();
await cmdResolvePackageAppeal(opts, appealId, options);
});
packageCmd
.command("reports")
.description("List package reports for moderator review")
@@ -36,12 +36,14 @@ const {
cmdInspectPackage,
cmdBackfillPackageArtifacts,
cmdListPackageReports,
cmdListPackageAppeals,
cmdModeratePackageRelease,
cmdPackageModerationStatus,
cmdPackageModerationQueue,
cmdPackageReadiness,
cmdPublishPackage,
cmdReportPackage,
cmdResolvePackageAppeal,
cmdSetPackageTrustedPublisher,
cmdTriagePackageReport,
cmdVerifyPackage,
@@ -548,6 +550,70 @@ describe("package commands", () => {
expect(mockLog).toHaveBeenCalledWith("OK. Appeal submitted: packageAppeals:1");
});
it("lists package appeals", async () => {
httpMocks.apiRequest.mockResolvedValueOnce({
items: [
{
appealId: "packageAppeals:1",
packageId: "pkg_1",
releaseId: "rel_1",
name: "@scope/demo",
displayName: "Demo",
family: "code-plugin",
version: "1.2.3",
message: "please review",
status: "open",
createdAt: 1,
submitter: { userId: "users:owner", handle: "owner", displayName: "Owner" },
resolvedAt: null,
resolvedBy: null,
resolutionNote: null,
},
],
nextCursor: null,
done: true,
});
await cmdListPackageAppeals(makeOpts(), { status: "open", limit: 10 });
const request = httpMocks.apiRequest.mock.calls[0]?.[1] as { url?: string } | undefined;
const url = new URL(String(request?.url));
expect(url.pathname).toBe("/api/v1/packages/appeals");
expect(url.searchParams.get("status")).toBe("open");
expect(url.searchParams.get("limit")).toBe("10");
expect(mockLog).toHaveBeenCalledWith("packageAppeals:1 open @scope/demo@1.2.3");
});
it("resolves package appeals", async () => {
httpMocks.apiRequest.mockResolvedValueOnce({
ok: true,
appealId: "packageAppeals:1",
packageId: "pkg_1",
releaseId: "rel_1",
status: "rejected",
});
await cmdResolvePackageAppeal(makeOpts(), "packageAppeals:1", {
status: "rejected",
note: "scanner finding still applies",
});
expect(httpMocks.apiRequest).toHaveBeenCalledWith(
"https://clawhub.ai",
{
method: "POST",
path: "/api/v1/packages/appeals/packageAppeals%3A1/resolve",
token: "tkn",
body: {
status: "rejected",
note: "scanner finding still applies",
},
},
expect.anything(),
);
expect(mockLog).toHaveBeenCalledWith("OK. Appeal packageAppeals:1 set to rejected.");
});
it("lists package reports", async () => {
httpMocks.apiRequest.mockResolvedValueOnce({
items: [
@@ -11,6 +11,8 @@ import {
ApiV1PackageArtifactBackfillResponseSchema,
ApiV1PackageArtifactResponseSchema,
ApiV1PackageAppealResponseSchema,
ApiV1PackageAppealListResponseSchema,
ApiV1PackageAppealResolveResponseSchema,
ApiV1PackageListResponseSchema,
ApiV1PackageModerationStatusResponseSchema,
ApiV1PackageModerationQueueResponseSchema,
@@ -28,6 +30,8 @@ import {
ApiV1PublishTokenMintResponseSchema,
normalizeOpenClawExternalPluginCompatibility,
type PackageArtifactSummary,
type PackageAppealListStatus,
type PackageAppealStatus,
type PackageCapabilitySummary,
type PackageCompatibility,
type PackageFamily,
@@ -143,6 +147,19 @@ type PackageAppealOptions = {
json?: boolean;
};
type PackageAppealListOptions = {
status?: PackageAppealListStatus;
cursor?: string;
limit?: number;
json?: boolean;
};
type PackageAppealResolveOptions = {
status?: PackageAppealStatus;
note?: string;
json?: boolean;
};
type PackageReportListOptions = {
status?: PackageReportListStatus;
cursor?: string;
@@ -939,6 +956,96 @@ export async function cmdAppealPackage(
}
}
export async function cmdListPackageAppeals(
opts: GlobalOpts,
options: PackageAppealListOptions = {},
) {
const status = options.status?.trim() || "open";
if (!["open", "accepted", "rejected", "all"].includes(status)) {
fail("--status must be open, accepted, rejected, or all");
}
const token = await requireAuthToken();
const registry = await getRegistry(opts, { cache: true });
const url = registryUrl(`${ApiRoutes.packages}/appeals`, registry);
url.searchParams.set("status", status);
if (options.cursor?.trim()) url.searchParams.set("cursor", options.cursor.trim());
url.searchParams.set("limit", String(clampLimit(options.limit ?? 25, 100)));
const result = await apiRequest(
registry,
{
method: "GET",
url: url.toString(),
token,
},
ApiV1PackageAppealListResponseSchema,
);
if (options.json) {
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return;
}
if (result.items.length === 0) {
console.log("No package appeals found.");
} else {
for (const item of result.items) {
const submitter = item.submitter.handle ?? item.submitter.userId;
console.log(`${item.appealId} ${item.status} ${item.name}@${item.version}`);
console.log(` submitter: ${submitter}`);
console.log(` message: ${item.message}`);
if (item.resolutionNote) console.log(` resolution: ${item.resolutionNote}`);
}
}
if (!result.done && result.nextCursor) {
console.log(`Next cursor: ${result.nextCursor}`);
}
}
export async function cmdResolvePackageAppeal(
opts: GlobalOpts,
appealId: string,
options: PackageAppealResolveOptions = {},
) {
const trimmed = appealId.trim();
if (!trimmed) fail("Appeal id required");
const status = options.status?.trim();
if (!status || !["open", "accepted", "rejected"].includes(status)) {
fail("--status must be open, accepted, or rejected");
}
const note = options.note?.trim();
if (status !== "open" && !note) fail("--note required unless reopening");
const token = await requireAuthToken();
const registry = await getRegistry(opts, { cache: true });
const spinner = options.json ? null : createSpinner(`Updating appeal ${trimmed}`);
try {
const result = await apiRequest(
registry,
{
method: "POST",
path: `${ApiRoutes.packages}/appeals/${encodeURIComponent(trimmed)}/resolve`,
token,
body: {
status,
...(note ? { note } : {}),
},
},
ApiV1PackageAppealResolveResponseSchema,
);
spinner?.stop();
if (options.json) {
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return;
}
console.log(`OK. Appeal ${trimmed} set to ${result.status}.`);
} catch (error) {
spinner?.fail(formatError(error));
throw error;
}
}
export async function cmdListPackageReports(
opts: GlobalOpts,
options: PackageReportListOptions = {},
+46
View File
@@ -80,6 +80,9 @@ export type PackageReportListStatus = (typeof PackageReportListStatusSchema)[inf
export const PackageAppealStatusSchema = type('"open"|"accepted"|"rejected"');
export type PackageAppealStatus = (typeof PackageAppealStatusSchema)[inferred];
export const PackageAppealListStatusSchema = PackageAppealStatusSchema.or('"all"');
export type PackageAppealListStatus = (typeof PackageAppealListStatusSchema)[inferred];
export const PackageArtifactSummarySchema = type({
kind: PackageArtifactKindSchema,
sha256: "string?",
@@ -352,6 +355,49 @@ export const ApiV1PackageAppealResponseSchema = type({
});
export type ApiV1PackageAppealResponse = (typeof ApiV1PackageAppealResponseSchema)[inferred];
export const PackageAppealResolveRequestSchema = type({
status: PackageAppealStatusSchema,
note: "string?",
});
export type PackageAppealResolveRequest = (typeof PackageAppealResolveRequestSchema)[inferred];
export const ApiV1PackageAppealListResponseSchema = type({
items: type({
appealId: "string",
packageId: "string",
releaseId: "string",
name: "string",
displayName: "string",
family: PackageFamilySchema,
version: "string",
message: "string",
status: PackageAppealStatusSchema,
createdAt: "number",
submitter: type({
userId: "string",
handle: "string|null?",
displayName: "string|null?",
}),
resolvedAt: "number|null?",
resolvedBy: "string|null?",
resolutionNote: "string|null?",
}).array(),
nextCursor: "string|null",
done: "boolean",
});
export type ApiV1PackageAppealListResponse =
(typeof ApiV1PackageAppealListResponseSchema)[inferred];
export const ApiV1PackageAppealResolveResponseSchema = type({
ok: "true",
appealId: "string",
packageId: "string",
releaseId: "string",
status: PackageAppealStatusSchema,
});
export type ApiV1PackageAppealResolveResponse =
(typeof ApiV1PackageAppealResolveResponseSchema)[inferred];
export const ApiV1PackageReportListResponseSchema = type({
items: type({
reportId: "string",
+40
View File
@@ -63,6 +63,8 @@ export declare const PackageReportListStatusSchema: import("arktype/internal/var
export type PackageReportListStatus = (typeof PackageReportListStatusSchema)[inferred];
export declare const PackageAppealStatusSchema: import("arktype/internal/variants/string.ts").StringType<"open" | "accepted" | "rejected", {}>;
export type PackageAppealStatus = (typeof PackageAppealStatusSchema)[inferred];
export declare const PackageAppealListStatusSchema: import("arktype/internal/variants/string.ts").StringType<"open" | "all" | "accepted" | "rejected", {}>;
export type PackageAppealListStatus = (typeof PackageAppealListStatusSchema)[inferred];
export declare const PackageArtifactSummarySchema: import("arktype/internal/variants/object.ts").ObjectType<{
kind: "legacy-zip" | "npm-pack";
sha256?: string | undefined;
@@ -513,6 +515,44 @@ export declare const ApiV1PackageAppealResponseSchema: import("arktype/internal/
status: "open" | "accepted" | "rejected";
}, {}>;
export type ApiV1PackageAppealResponse = (typeof ApiV1PackageAppealResponseSchema)[inferred];
export declare const PackageAppealResolveRequestSchema: import("arktype/internal/variants/object.ts").ObjectType<{
status: "open" | "accepted" | "rejected";
note?: string | undefined;
}, {}>;
export type PackageAppealResolveRequest = (typeof PackageAppealResolveRequestSchema)[inferred];
export declare const ApiV1PackageAppealListResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
items: {
appealId: string;
packageId: string;
releaseId: string;
name: string;
displayName: string;
family: "skill" | "code-plugin" | "bundle-plugin";
version: string;
message: string;
status: "open" | "accepted" | "rejected";
createdAt: number;
submitter: {
userId: string;
handle?: string | null | undefined;
displayName?: string | null | undefined;
};
resolvedAt?: number | null | undefined;
resolvedBy?: string | null | undefined;
resolutionNote?: string | null | undefined;
}[];
nextCursor: string | null;
done: boolean;
}, {}>;
export type ApiV1PackageAppealListResponse = (typeof ApiV1PackageAppealListResponseSchema)[inferred];
export declare const ApiV1PackageAppealResolveResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
ok: true;
appealId: string;
packageId: string;
releaseId: string;
status: "open" | "accepted" | "rejected";
}, {}>;
export type ApiV1PackageAppealResolveResponse = (typeof ApiV1PackageAppealResolveResponseSchema)[inferred];
export declare const ApiV1PackageReportListResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
items: {
reportId: string;
+36
View File
@@ -51,6 +51,7 @@ export const PackageReleaseModerationStateSchema = type('"approved"|"quarantined
export const PackageReportStatusSchema = type('"open"|"triaged"|"dismissed"');
export const PackageReportListStatusSchema = PackageReportStatusSchema.or('"all"');
export const PackageAppealStatusSchema = type('"open"|"accepted"|"rejected"');
export const PackageAppealListStatusSchema = PackageAppealStatusSchema.or('"all"');
export const PackageArtifactSummarySchema = type({
kind: PackageArtifactKindSchema,
sha256: "string?",
@@ -281,6 +282,41 @@ export const ApiV1PackageAppealResponseSchema = type({
releaseId: "string",
status: PackageAppealStatusSchema,
});
export const PackageAppealResolveRequestSchema = type({
status: PackageAppealStatusSchema,
note: "string?",
});
export const ApiV1PackageAppealListResponseSchema = type({
items: type({
appealId: "string",
packageId: "string",
releaseId: "string",
name: "string",
displayName: "string",
family: PackageFamilySchema,
version: "string",
message: "string",
status: PackageAppealStatusSchema,
createdAt: "number",
submitter: type({
userId: "string",
handle: "string|null?",
displayName: "string|null?",
}),
resolvedAt: "number|null?",
resolvedBy: "string|null?",
resolutionNote: "string|null?",
}).array(),
nextCursor: "string|null",
done: "boolean",
});
export const ApiV1PackageAppealResolveResponseSchema = type({
ok: "true",
appealId: "string",
packageId: "string",
releaseId: "string",
status: PackageAppealStatusSchema,
});
export const ApiV1PackageReportListResponseSchema = type({
items: type({
reportId: "string",
File diff suppressed because one or more lines are too long
+46
View File
@@ -80,6 +80,9 @@ export type PackageReportListStatus = (typeof PackageReportListStatusSchema)[inf
export const PackageAppealStatusSchema = type('"open"|"accepted"|"rejected"');
export type PackageAppealStatus = (typeof PackageAppealStatusSchema)[inferred];
export const PackageAppealListStatusSchema = PackageAppealStatusSchema.or('"all"');
export type PackageAppealListStatus = (typeof PackageAppealListStatusSchema)[inferred];
export const PackageArtifactSummarySchema = type({
kind: PackageArtifactKindSchema,
sha256: "string?",
@@ -358,6 +361,49 @@ export const ApiV1PackageAppealResponseSchema = type({
});
export type ApiV1PackageAppealResponse = (typeof ApiV1PackageAppealResponseSchema)[inferred];
export const PackageAppealResolveRequestSchema = type({
status: PackageAppealStatusSchema,
note: "string?",
});
export type PackageAppealResolveRequest = (typeof PackageAppealResolveRequestSchema)[inferred];
export const ApiV1PackageAppealListResponseSchema = type({
items: type({
appealId: "string",
packageId: "string",
releaseId: "string",
name: "string",
displayName: "string",
family: PackageFamilySchema,
version: "string",
message: "string",
status: PackageAppealStatusSchema,
createdAt: "number",
submitter: type({
userId: "string",
handle: "string|null?",
displayName: "string|null?",
}),
resolvedAt: "number|null?",
resolvedBy: "string|null?",
resolutionNote: "string|null?",
}).array(),
nextCursor: "string|null",
done: "boolean",
});
export type ApiV1PackageAppealListResponse =
(typeof ApiV1PackageAppealListResponseSchema)[inferred];
export const ApiV1PackageAppealResolveResponseSchema = type({
ok: "true",
appealId: "string",
packageId: "string",
releaseId: "string",
status: PackageAppealStatusSchema,
});
export type ApiV1PackageAppealResolveResponse =
(typeof ApiV1PackageAppealResolveResponseSchema)[inferred];
export const ApiV1PackageReportListResponseSchema = type({
items: type({
reportId: "string",