mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
feat(packages): resolve package appeals
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -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",
|
||||
|
||||
Vendored
+40
@@ -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;
|
||||
|
||||
Vendored
+36
@@ -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",
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user