feat: add audited package hard delete (#3282)

This commit is contained in:
Patrick Erichsen
2026-07-27 14:45:02 -05:00
committed by GitHub
parent 34b6774848
commit 7713313fa5
15 changed files with 627 additions and 6 deletions
@@ -148,6 +148,7 @@ status|moderation-status <name>
queue|moderation-queue
reports
triage-report <report-id>
hard-delete <name>
transfer <name>
repair-name <name>
migrations
@@ -159,6 +160,8 @@ Examples:
```sh
bun run admin -- packages status <name>
bun run admin -- packages hard-delete <name> --owner <handle> --reason "<reason>" # dry-run
bun run admin -- packages hard-delete <name> --owner <handle> --reason "<reason>" --apply --confirm "<token>" --yes
bun run admin -- packages transfer <name> --to <owner> --reason "<reason>" # dry-run
bun run admin -- packages transfer <name> --to <owner> --reason "<reason>" --apply
bun run admin -- packages repair-name <name> --next-name <name> --reason "<reason>"
@@ -228,5 +231,8 @@ only after admin auth succeeds.
moderation hold, restores skills hidden by that hold, and writes an audit log.
- `packages transfer` preserves the package row, stats, releases, and history;
it changes the owner publisher.
- `packages hard-delete` is admin-only, requires an already-soft-deleted package,
exact owner handle, reason, and dry-run token, and permanently removes all
package releases and related history.
- `org delete` soft-deletes an empty org publisher and retains member rows for
history; it refuses orgs with active skills or packages.
+76 -1
View File
@@ -8161,6 +8161,81 @@ describe("httpApiV1 handlers", () => {
expect(runMutation).toHaveBeenCalledTimes(1);
});
it("package hard-delete dry-runs through the admin-only API", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:admin",
user: { _id: "users:admin", role: "admin" },
} as never);
const token = "hard-delete-package:@hxy91819/openclaw-tencent-provider:packages:1";
const runMutation = vi.fn(async (_mutation: unknown, args: Record<string, unknown>) => {
if (isRateLimitArgs(args)) return okRate();
return {
ok: true,
packageId: "packages:1",
name: "openclaw-tencent-provider",
ownerHandle: "hxy91819",
displayName: "Tencent Cloud",
runtimeId: "tencent",
dryRun: true,
deleted: false,
confirmationToken: token,
};
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/openclaw-tencent-provider/hard-delete", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({
ownerHandle: "hxy91819",
reason: "Owner-requested cleanup",
dryRun: true,
}),
}),
);
expect(response.status).toBe(200);
await expect(response.json()).resolves.toMatchObject({
dryRun: true,
deleted: false,
confirmationToken: token,
});
expect(runMutation).toHaveBeenCalledWith(
(internal as unknown as { packages: Record<string, unknown> }).packages
.hardDeleteForAdminInternal,
{
actorUserId: "users:admin",
name: "openclaw-tencent-provider",
ownerHandle: "hxy91819",
reason: "Owner-requested cleanup",
dryRun: true,
},
);
});
it("package hard-delete forbids non-admin API tokens", 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();
throw new Error("should not hard-delete");
});
const response = await __handlers.packagesPostRouterV1Handler(
makeCtx({ runMutation }),
new Request("https://example.com/api/v1/packages/demo/hard-delete", {
method: "POST",
headers: { Authorization: "Bearer clh_test" },
body: JSON.stringify({ ownerHandle: "openclaw", reason: "Cleanup" }),
}),
);
expect(response.status).toBe(403);
await expect(response.text()).resolves.toBe("Admin role required.");
expect(runMutation).toHaveBeenCalledTimes(1);
});
it("skill rescan enqueues owner-authorized ClawScan jobs", async () => {
vi.mocked(requireApiTokenUser).mockResolvedValue({
userId: "users:moderator",
@@ -15251,7 +15326,7 @@ describe("httpApiV1 handlers", () => {
}),
);
expect(response.status, await response.clone().text()).toBe(200);
expect(response.status).toBe(200);
expect(storageStore).toHaveBeenCalledTimes(3);
expect(runAction).toHaveBeenCalledWith(
expect.anything(),
+36
View File
@@ -4,6 +4,7 @@ import {
ApiV1PackageOfficialMigrationResponseSchema,
ApiV1PackageModerationStatusResponseSchema,
ApiV1PackageSecurityResponseSchema,
PackageHardDeleteRequestSchema,
PackageAppealResolveRequestSchema,
PackageAppealRequestSchema,
PackageOfficialMigrationUpsertRequestSchema,
@@ -116,6 +117,7 @@ const internalRefs = internal as unknown as {
searchForViewerInternal: unknown;
listVersionsForViewerInternal: unknown;
getPackageByNameInternal: unknown;
hardDeleteForAdminInternal: unknown;
getTrustedPublisherByPackageIdInternal: unknown;
getVersionByNameForViewerInternal: unknown;
getVersionSecurityByNameForViewerInternal: unknown;
@@ -2663,6 +2665,40 @@ export async function packagesPostRouterV1Handler(ctx: ActionCtx, request: Reque
const packageName = packageRoute.packageName;
const packageSegments = packageRoute.rest;
if (packageSegments[0] === "hard-delete" && packageSegments.length === 1) {
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;
const admin = requireAdminOrResponse(auth.user, rate.headers);
if (!admin.ok) return admin.response;
try {
const body = parseArk(
PackageHardDeleteRequestSchema,
await request.json(),
"Package hard-delete payload",
) as {
ownerHandle: string;
reason: string;
dryRun?: boolean;
confirmationToken?: string;
};
const result = await runMutationRef(ctx, internalRefs.packages.hardDeleteForAdminInternal, {
actorUserId: auth.userId,
name: packageName,
ownerHandle: body.ownerHandle,
reason: body.reason,
...(body.dryRun !== undefined ? { dryRun: body.dryRun } : {}),
...(body.confirmationToken ? { confirmationToken: body.confirmationToken } : {}),
});
return json(result, 200, rate.headers);
} catch (error) {
if (error instanceof SyntaxError) return text("Invalid JSON", 400, rate.headers);
return packageOperationErrorToResponse(error, rate.headers, "Package hard delete failed");
}
}
if (packageSegments[0] === "rescan" && packageSegments.length === 1) {
const rate = await applyRateLimit(ctx, request, "write");
if (!rate.ok) return rate.response;
+123
View File
@@ -0,0 +1,123 @@
/* @vitest-environment node */
import { describe, expect, it, vi } from "vitest";
import { hardDeleteForAdminInternal } from "./packages";
type Args = {
actorUserId: string;
name: string;
ownerHandle: string;
reason: string;
dryRun?: boolean;
confirmationToken?: string;
};
type Handler = { _handler: (ctx: unknown, args: Args) => Promise<Record<string, unknown>> };
const handler = (hardDeleteForAdminInternal as unknown as Handler)._handler;
function makeCtx(options: { softDeleted?: boolean; ownerHandle?: string } = {}) {
const pkg = {
_id: "packages:tencent",
name: "openclaw-tencent-provider",
normalizedName: "openclaw-tencent-provider",
displayName: "Tencent Cloud",
runtimeId: "tencent",
ownerUserId: "users:owner",
ownerPublisherId: "publishers:hxy91819",
softDeletedAt: options.softDeleted === false ? undefined : 1_000,
};
const publisher = {
_id: "publishers:hxy91819",
kind: "user",
handle: options.ownerHandle ?? "hxy91819",
linkedUserId: "users:owner",
};
const insert = vi.fn(async () => "auditLogs:1");
const remove = vi.fn();
const query = vi.fn((table: string) => ({
withIndex: () => ({
unique: async () => (table === "packages" ? pkg : null),
collect: async () => [],
}),
}));
const ctx = {
db: {
get: vi.fn(async (id: string) => {
if (id === "users:admin") return { _id: id, role: "admin" };
if (id === "publishers:hxy91819") return publisher;
return null;
}),
query,
insert,
delete: remove,
patch: vi.fn(),
replace: vi.fn(),
normalizeId: vi.fn(),
},
} as never;
return { ctx, insert, remove };
}
const baseArgs = {
actorUserId: "users:admin",
name: "openclaw-tencent-provider",
ownerHandle: "hxy91819",
reason: "Free the stale package name for Tencent externalization",
};
describe("hardDeleteForAdminInternal", () => {
it("returns an exact token without mutating during dry-run", async () => {
const { ctx, insert, remove } = makeCtx();
await expect(handler(ctx, baseArgs)).resolves.toEqual({
ok: true,
packageId: "packages:tencent",
name: "openclaw-tencent-provider",
ownerHandle: "hxy91819",
displayName: "Tencent Cloud",
runtimeId: "tencent",
dryRun: true,
deleted: false,
confirmationToken: "hard-delete-package:@hxy91819/openclaw-tencent-provider:packages:tencent",
});
expect(insert).not.toHaveBeenCalled();
expect(remove).not.toHaveBeenCalled();
});
it("requires the package to be soft-deleted and owner-qualified", async () => {
await expect(handler(makeCtx({ softDeleted: false }).ctx, baseArgs)).rejects.toThrow(
/must be soft-deleted/i,
);
await expect(handler(makeCtx().ctx, { ...baseArgs, ownerHandle: "tencent" })).rejects.toThrow(
/owner does not match/i,
);
});
it("requires the exact token before deleting and auditing", async () => {
const { ctx, insert, remove } = makeCtx();
await expect(
handler(ctx, { ...baseArgs, dryRun: false, confirmationToken: "wrong" }),
).rejects.toThrow(/confirmation token must be/i);
expect(remove).not.toHaveBeenCalled();
const token = "hard-delete-package:@hxy91819/openclaw-tencent-provider:packages:tencent";
const result = await handler(ctx, {
...baseArgs,
dryRun: false,
confirmationToken: token,
});
expect(result).toMatchObject({ dryRun: false, deleted: true });
expect(remove).toHaveBeenCalledWith("packages:tencent");
expect(insert).toHaveBeenCalledWith(
"auditLogs",
expect.objectContaining({
action: "package.hard_delete.requested",
targetId: "packages:tencent",
metadata: expect.objectContaining({
ownerHandle: "hxy91819",
reason: baseArgs.reason,
source: "clawhub-admin",
}),
}),
);
});
});
+76 -1
View File
@@ -617,6 +617,7 @@ const ownedPackageScanScopeValidator = v.optional(
v.union(v.literal("ownerUserId"), v.literal("personalPublisher")),
);
const hardDeletePackageSourceValidator = v.union(
v.literal("admin"),
v.literal("account.delete"),
v.literal("publisher.delete"),
);
@@ -5491,7 +5492,8 @@ async function hardDeletePackageDoc(
params: {
actorUserId: Id<"users">;
deletedAt: number;
source: "account.delete" | "publisher.delete";
source: "admin" | "account.delete" | "publisher.delete";
reason?: string;
},
) {
const releases = await ctx.db
@@ -5574,6 +5576,7 @@ async function hardDeletePackageDoc(
ownerUserId: pkg.ownerUserId,
ownerPublisherId: pkg.ownerPublisherId,
source: params.source,
reason: params.reason,
releases: releases.length,
reports: reports.length,
appeals: appeals.length,
@@ -5609,6 +5612,78 @@ export const hardDeletePackageInternal = internalMutation({
},
});
export const hardDeleteForAdminInternal = internalMutation({
args: {
actorUserId: v.id("users"),
name: v.string(),
ownerHandle: v.string(),
reason: v.string(),
dryRun: v.optional(v.boolean()),
confirmationToken: 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");
assertAdmin(actor);
const name = normalizePackageName(args.name);
const ownerHandle = normalizePublisherHandle(args.ownerHandle);
const reason = args.reason.trim();
if (!name) throw new ConvexError("Package name required");
if (!ownerHandle) throw new ConvexError("Owner handle required");
if (!reason) throw new ConvexError("Reason is required");
if (reason.length > 500) throw new ConvexError("Reason too long (max 500 chars)");
const pkg = await getPackageByNormalizedName(ctx, name);
if (!pkg) throw new ConvexError("Package not found");
if (!pkg.softDeletedAt) throw new ConvexError("Package must be soft-deleted first");
const owner = await getOwnerPublisher(ctx, {
ownerPublisherId: pkg.ownerPublisherId,
ownerUserId: pkg.ownerUserId,
});
if (normalizePublisherHandle(owner?.handle) !== ownerHandle) {
throw new ConvexError("Package owner does not match --owner");
}
const confirmationToken = `hard-delete-package:@${ownerHandle}/${pkg.normalizedName}:${pkg._id}`;
const baseResult = {
ok: true as const,
packageId: pkg._id,
name: pkg.normalizedName,
ownerHandle,
displayName: pkg.displayName,
runtimeId: pkg.runtimeId ?? null,
confirmationToken,
};
if (args.dryRun !== false) return { ...baseResult, dryRun: true, deleted: false };
if (args.confirmationToken !== confirmationToken) {
throw new ConvexError(`Confirmation token must be "${confirmationToken}"`);
}
const now = Date.now();
await ctx.db.insert("auditLogs", {
actorUserId: args.actorUserId,
action: "package.hard_delete.requested",
targetType: "package",
targetId: pkg._id,
metadata: {
name: pkg.normalizedName,
ownerHandle,
reason,
source: "clawhub-admin",
},
createdAt: now,
});
await hardDeletePackageDoc(ctx, pkg, {
actorUserId: args.actorUserId,
deletedAt: now,
source: "admin",
reason,
});
return { ...baseResult, dryRun: false, deleted: true };
},
});
function comparePackageRestoreLatestCandidates(
family: Doc<"packages">["family"],
a: Doc<"packageReleases">,
+1
View File
@@ -131,6 +131,7 @@ bun run admin -- plugins triage-report <report-id> --status open|confirmed|dismi
bun run admin -- plugins migrations [--phase <phase>]
bun run admin -- plugins set-migration <bundled-plugin-id> --package <name>
bun run admin -- plugins hard-delete <name> --owner <handle> --reason <text> [--apply --confirm <token> --yes] [--json]
bun run admin -- plugins repair-name <name> --next-name <name> --reason <text> [--retire-target] [--owner <handle>] [--apply]
bun run admin -- plugins trusted-publisher get <name>
bun run admin -- plugins trusted-publisher set <name> --repository <owner/repo> --workflow-filename <file>
+16
View File
@@ -52,6 +52,7 @@ import {
} from "./commands/orgs.js";
import {
cmdDeletePackageTrustedPublisher,
cmdHardDeletePackage,
cmdListPackageMigrations,
cmdListPackageReports,
cmdModeratePackageRelease,
@@ -594,6 +595,21 @@ function registerOrgCommands(command: Command) {
}
function registerPluginGovernanceCommands(command: Command) {
command
.command("hard-delete")
.description("Permanently delete one soft-deleted plugin package and all related history")
.argument("<name>", "Plugin package name")
.requiredOption("--owner <handle>", "Current owner publisher handle")
.requiredOption("--reason <reason>", "Audit reason")
.option("--apply", "Delete permanently; defaults to dry-run")
.option("--confirm <token>", "Confirmation token returned by the dry-run")
.option("--yes", "Skip confirmation for --apply")
.option("--json", "Output JSON")
.action(async (name, options) => {
const opts = await resolveGlobalOpts();
await cmdHardDeletePackage(opts, name, options, isInputAllowed());
});
command
.command("transfer")
.description("Transfer a plugin package to another publisher without changing package stats")
@@ -19,13 +19,126 @@ vi.mock("../../../clawhub/src/cli/registry.js", () => registryMocks.moduleFactor
vi.mock("../../../clawhub/src/http.js", () => httpMocks.moduleFactory());
vi.mock("../../../clawhub/src/cli/ui.js", () => uiMocks.moduleFactory());
const { cmdRepairPackageName, cmdRepairPackageRuntimeId, cmdTransferPackageOwner } =
await import("./packages");
const {
cmdHardDeletePackage,
cmdRepairPackageName,
cmdRepairPackageRuntimeId,
cmdTransferPackageOwner,
} = await import("./packages");
afterEach(() => {
vi.clearAllMocks();
});
describe("cmdHardDeletePackage", () => {
it("dry-runs an owner-qualified package by default", async () => {
const confirmationToken =
"hard-delete-package:@hxy91819/openclaw-tencent-provider:packages:tencent";
httpMocks.apiRequest.mockResolvedValueOnce({
ok: true,
packageId: "packages:tencent",
name: "openclaw-tencent-provider",
ownerHandle: "hxy91819",
displayName: "Tencent Cloud",
runtimeId: "tencent",
dryRun: true,
deleted: false,
confirmationToken,
});
const result = await cmdHardDeletePackage(
makeGlobalOpts(),
"openclaw-tencent-provider",
{
owner: "HXY91819",
reason: "Free the stale package name for Tencent externalization",
json: true,
},
false,
);
expect(result).toMatchObject({
dryRun: true,
deleted: false,
ownerHandle: "hxy91819",
name: "openclaw-tencent-provider",
});
expect(httpMocks.apiRequest).toHaveBeenCalledWith(
"https://clawhub.ai",
expect.objectContaining({
method: "POST",
path: "/api/v1/packages/openclaw-tencent-provider/hard-delete",
token: "tkn",
body: {
ownerHandle: "hxy91819",
reason: "Free the stale package name for Tencent externalization",
dryRun: true,
},
}),
expect.anything(),
);
});
it("requires owner, reason, and an apply confirmation token", async () => {
await expect(
cmdHardDeletePackage(makeGlobalOpts(), "demo", { reason: "Cleanup" }, false),
).rejects.toThrow(/--owner required/i);
await expect(
cmdHardDeletePackage(makeGlobalOpts(), "demo", { owner: "openclaw" }, false),
).rejects.toThrow(/--reason required/i);
await expect(
cmdHardDeletePackage(
makeGlobalOpts(),
"demo",
{ owner: "openclaw", reason: "Cleanup", apply: true, yes: true },
false,
),
).rejects.toThrow(/--confirm required/i);
expect(httpMocks.apiRequest).not.toHaveBeenCalled();
});
it("applies with the exact token and disables retries", async () => {
const confirmationToken =
"hard-delete-package:@hxy91819/openclaw-tencent-provider:packages:tencent";
httpMocks.apiRequest.mockResolvedValueOnce({
ok: true,
packageId: "packages:tencent",
name: "openclaw-tencent-provider",
ownerHandle: "hxy91819",
displayName: "Tencent Cloud",
runtimeId: "tencent",
dryRun: false,
deleted: true,
confirmationToken,
});
await cmdHardDeletePackage(
makeGlobalOpts(),
"openclaw-tencent-provider",
{
owner: "hxy91819",
reason: "Cleanup",
apply: true,
confirm: confirmationToken,
yes: true,
},
false,
);
expect(httpMocks.apiRequest).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
retryCount: 0,
body: {
ownerHandle: "hxy91819",
reason: "Cleanup",
dryRun: false,
confirmationToken,
},
}),
expect.anything(),
);
});
});
describe("cmdRepairPackageName", () => {
it("defaults to a dry run", async () => {
httpMocks.apiRequest.mockResolvedValueOnce({
@@ -5,10 +5,17 @@ import {
} from "../../../clawhub/src/cli/commands/moderationPlan.js";
import { getRegistry } from "../../../clawhub/src/cli/registry.js";
import type { GlobalOpts } from "../../../clawhub/src/cli/types.js";
import { createCrabLoader, fail, formatError } from "../../../clawhub/src/cli/ui.js";
import {
createCrabLoader,
fail,
formatError,
isInteractive,
promptConfirm,
} from "../../../clawhub/src/cli/ui.js";
import { apiRequest, registryUrl } from "../../../clawhub/src/http.js";
import {
ApiRoutes,
ApiV1PackageHardDeleteResponseSchema,
ApiV1PackageModerationQueueResponseSchema,
ApiV1PackageOfficialMigrationListResponseSchema,
ApiV1PackageOfficialMigrationResponseSchema,
@@ -114,6 +121,74 @@ type PackageTransferOwnerOptions = {
json?: boolean;
};
type PackageHardDeleteOptions = {
owner?: string;
reason?: string;
apply?: boolean;
confirm?: string;
json?: boolean;
yes?: boolean;
};
export async function cmdHardDeletePackage(
opts: GlobalOpts,
packageName: string,
options: PackageHardDeleteOptions,
inputAllowed: boolean,
) {
const name = normalizePackageNameOrFail(packageName).toLowerCase();
const ownerHandle = options.owner?.trim().replace(/^@+/, "").toLowerCase();
const reason = options.reason?.trim();
if (!ownerHandle) fail("--owner required");
if (!reason) fail("--reason required");
const dryRun = options.apply !== true;
const confirmationToken = options.confirm?.trim();
if (!dryRun && !confirmationToken) fail("--confirm required when using --apply");
if (!dryRun && !options.yes) {
const allowPrompt = isInteractive() && inputAllowed !== false;
if (!allowPrompt) fail("Pass --yes (no input)");
const confirmed = await promptConfirm(
`Permanently hard-delete @${ownerHandle}/${name} and all related history? (cannot be undone)`,
);
if (!confirmed) return undefined;
}
const token = await requireAuthToken();
const registry = await getRegistry(opts, { cache: true });
const spinner = options.json
? null
: createCrabLoader(`${dryRun ? "Planning hard delete for" : "Hard-deleting"} ${name}`);
try {
const result = await apiRequest(
registry,
{
method: "POST",
path: `${ApiRoutes.packages}/${encodeURIComponent(name)}/hard-delete`,
token,
...(dryRun ? {} : { retryCount: 0 }),
body: {
ownerHandle,
reason,
dryRun,
...(confirmationToken ? { confirmationToken } : {}),
},
},
ApiV1PackageHardDeleteResponseSchema,
);
spinner?.succeed(
result.deleted
? `Hard-deleted @${result.ownerHandle}/${result.name}`
: `Dry run OK for @${result.ownerHandle}/${result.name}: pass --apply --confirm ${result.confirmationToken}`,
);
if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
return result;
} catch (error) {
spinner?.fail(formatError(error));
throw error;
}
}
export async function cmdSetPackageTrustedPublisher(
opts: GlobalOpts,
packageName: string,
+22
View File
@@ -702,6 +702,28 @@ export const ApiV1PackageTransferResponseSchema = type({
});
export type ApiV1PackageTransferResponse = (typeof ApiV1PackageTransferResponseSchema)[inferred];
export const PackageHardDeleteRequestSchema = type({
ownerHandle: "string",
reason: "string",
dryRun: "boolean?",
confirmationToken: "string?",
});
export type PackageHardDeleteRequest = (typeof PackageHardDeleteRequestSchema)[inferred];
export const ApiV1PackageHardDeleteResponseSchema = type({
ok: "true",
packageId: "string",
name: "string",
ownerHandle: "string",
displayName: "string",
runtimeId: "string|null?",
dryRun: "boolean",
deleted: "boolean",
confirmationToken: "string",
});
export type ApiV1PackageHardDeleteResponse =
(typeof ApiV1PackageHardDeleteResponseSchema)[inferred];
export const PackageRepairNameRequestSchema = type({
nextName: "string",
retireTarget: "boolean?",
+19
View File
@@ -913,6 +913,25 @@ export declare const ApiV1PackageTransferResponseSchema: import("arktype/interna
isOfficial: boolean;
}, {}>;
export type ApiV1PackageTransferResponse = (typeof ApiV1PackageTransferResponseSchema)[inferred];
export declare const PackageHardDeleteRequestSchema: import("arktype/internal/variants/object.ts").ObjectType<{
ownerHandle: string;
reason: string;
dryRun?: boolean | undefined;
confirmationToken?: string | undefined;
}, {}>;
export type PackageHardDeleteRequest = (typeof PackageHardDeleteRequestSchema)[inferred];
export declare const ApiV1PackageHardDeleteResponseSchema: import("arktype/internal/variants/object.ts").ObjectType<{
ok: true;
packageId: string;
name: string;
ownerHandle: string;
displayName: string;
runtimeId?: string | null | undefined;
dryRun: boolean;
deleted: boolean;
confirmationToken: string;
}, {}>;
export type ApiV1PackageHardDeleteResponse = (typeof ApiV1PackageHardDeleteResponseSchema)[inferred];
export declare const PackageRepairNameRequestSchema: import("arktype/internal/variants/object.ts").ObjectType<{
nextName: string;
retireTarget?: boolean | undefined;
+17
View File
@@ -584,6 +584,23 @@ export const ApiV1PackageTransferResponseSchema = type({
channel: PackageChannelSchema,
isOfficial: "boolean",
});
export const PackageHardDeleteRequestSchema = type({
ownerHandle: "string",
reason: "string",
dryRun: "boolean?",
confirmationToken: "string?",
});
export const ApiV1PackageHardDeleteResponseSchema = type({
ok: "true",
packageId: "string",
name: "string",
ownerHandle: "string",
displayName: "string",
runtimeId: "string|null?",
dryRun: "boolean",
deleted: "boolean",
confirmationToken: "string",
});
export const PackageRepairNameRequestSchema = type({
nextName: "string",
retireTarget: "boolean?",
File diff suppressed because one or more lines are too long
+22
View File
@@ -738,6 +738,28 @@ export const ApiV1PackageTransferResponseSchema = type({
});
export type ApiV1PackageTransferResponse = (typeof ApiV1PackageTransferResponseSchema)[inferred];
export const PackageHardDeleteRequestSchema = type({
ownerHandle: "string",
reason: "string",
dryRun: "boolean?",
confirmationToken: "string?",
});
export type PackageHardDeleteRequest = (typeof PackageHardDeleteRequestSchema)[inferred];
export const ApiV1PackageHardDeleteResponseSchema = type({
ok: "true",
packageId: "string",
name: "string",
ownerHandle: "string",
displayName: "string",
runtimeId: "string|null?",
dryRun: "boolean",
deleted: "boolean",
confirmationToken: "string",
});
export type ApiV1PackageHardDeleteResponse =
(typeof ApiV1PackageHardDeleteResponseSchema)[inferred];
export const PackageRepairNameRequestSchema = type({
nextName: "string",
retireTarget: "boolean?",
+21
View File
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
import { parseArk } from "./ark";
import { DocsLinks, openClawDocsUrl } from "./docsLinks";
import {
ApiV1PackageHardDeleteResponseSchema,
ApiV1PackageVersionResponseSchema,
ApiV1PackagePublishResponseSchema,
getPackageScopeOwnerMismatch,
@@ -24,6 +25,26 @@ import {
} from "./schemas";
describe("clawhub-schema", () => {
it("parses package hard-delete responses", () => {
const result = parseArk(
ApiV1PackageHardDeleteResponseSchema,
{
ok: true,
packageId: "packages:tencent",
name: "openclaw-tencent-provider",
ownerHandle: "hxy91819",
displayName: "Tencent Cloud",
runtimeId: "tencent",
dryRun: true,
deleted: false,
confirmationToken:
"hard-delete-package:@hxy91819/openclaw-tencent-provider:packages:tencent",
},
"Package hard-delete response",
);
expect(result.deleted).toBe(false);
});
it("parses skill hard-delete responses", () => {
const generated_token_reference = "hard-delete-skill:@openclaw/demo:skills:demo";
const response = parseArk(