mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
Require multipart package publishes so public clients cannot provide trusted file or artifact metadata. Preserve released CLI multipart field names (`files` and `clawpack`), derive stored file and npm-pack artifact metadata from uploaded bytes, and keep large ClawPack tarballs working through ticketed staged uploads. Tests: - bunx vitest run convex/uploads.test.ts --reporter=dot - bunx vitest run convex/httpApi.handlers.test.ts convex/httpApiV1.handlers.test.ts --testNamePattern "package publish|multipart package publish|multipart ClawPack|staged ClawPack|browser session auth|cliUploadUrl|upload tickets" --reporter=dot - bun run --cwd packages/clawhub test:src -- src/cli/commands/packages.test.ts --testNamePattern "ClawPack tarballs over|stages ClawPack|publishes a ClawPack|cleans generated ClawPack" - bun run format:check - bun run lint - bunx tsc -p packages/schema/tsconfig.json --noEmit --pretty false - bunx tsc -p packages/clawhub/tsconfig.json --noEmit --pretty false - bunx tsc --noEmit --pretty false - bun run ci:unit - git diff --check origin/main
93 lines
3.2 KiB
TypeScript
93 lines
3.2 KiB
TypeScript
import { v } from "convex/values";
|
|
import { internalMutation, mutation } from "./functions";
|
|
import { requireUser } from "./lib/access";
|
|
|
|
const PACKAGE_PUBLISH_UPLOAD_TICKET_TTL_MS = 15 * 60_000;
|
|
|
|
export const generateUploadUrl = mutation({
|
|
args: {},
|
|
handler: async (ctx) => {
|
|
await requireUser(ctx);
|
|
return ctx.storage.generateUploadUrl();
|
|
},
|
|
});
|
|
|
|
export const createPackagePublishUploadForUserInternal = internalMutation({
|
|
args: { userId: v.id("users") },
|
|
handler: async (ctx, args) => {
|
|
const user = await ctx.db.get(args.userId);
|
|
if (!user || user.deletedAt || user.deactivatedAt) throw new Error("User not found");
|
|
const now = Date.now();
|
|
const uploadTicket = await ctx.db.insert("packagePublishUploadTickets", {
|
|
kind: "user",
|
|
userId: args.userId,
|
|
createdAt: now,
|
|
expiresAt: now + PACKAGE_PUBLISH_UPLOAD_TICKET_TTL_MS,
|
|
});
|
|
const uploadUrl = await ctx.storage.generateUploadUrl();
|
|
return { uploadUrl, uploadTicket };
|
|
},
|
|
});
|
|
|
|
export const createPackagePublishUploadForTokenInternal = internalMutation({
|
|
args: { publishTokenId: v.id("packagePublishTokens") },
|
|
handler: async (ctx, args) => {
|
|
const publishToken = await ctx.db.get(args.publishTokenId);
|
|
const now = Date.now();
|
|
if (!publishToken || publishToken.revokedAt || publishToken.expiresAt <= now) {
|
|
throw new Error("Trusted publish token is missing or expired");
|
|
}
|
|
const uploadTicket = await ctx.db.insert("packagePublishUploadTickets", {
|
|
kind: "github-actions",
|
|
publishTokenId: args.publishTokenId,
|
|
createdAt: now,
|
|
expiresAt: now + PACKAGE_PUBLISH_UPLOAD_TICKET_TTL_MS,
|
|
});
|
|
const uploadUrl = await ctx.storage.generateUploadUrl();
|
|
return { uploadUrl, uploadTicket };
|
|
},
|
|
});
|
|
|
|
export const consumePackagePublishUploadTicketInternal = internalMutation({
|
|
args: {
|
|
uploadTicket: v.id("packagePublishUploadTickets"),
|
|
storageId: v.id("_storage"),
|
|
auth: v.union(
|
|
v.object({ kind: v.literal("user"), userId: v.id("users") }),
|
|
v.object({
|
|
kind: v.literal("github-actions"),
|
|
publishTokenId: v.id("packagePublishTokens"),
|
|
}),
|
|
),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
const ticket = await ctx.db.get(args.uploadTicket);
|
|
const now = Date.now();
|
|
if (!ticket || ticket.expiresAt <= now) {
|
|
throw new Error("Package tarball upload ticket is missing or expired");
|
|
}
|
|
if (
|
|
args.auth.kind === "user"
|
|
? ticket.kind !== "user" || ticket.userId !== args.auth.userId
|
|
: ticket.kind !== "github-actions" || ticket.publishTokenId !== args.auth.publishTokenId
|
|
) {
|
|
throw new Error("Package tarball upload ticket does not match this publish token");
|
|
}
|
|
if (ticket.usedAt) {
|
|
if (ticket.storageId === args.storageId) return;
|
|
throw new Error("Package tarball upload ticket was already used");
|
|
}
|
|
|
|
const metadata = await ctx.db.system.get("_storage", args.storageId);
|
|
if (!metadata) throw new Error("Package tarball upload no longer exists");
|
|
if (metadata._creationTime < ticket.createdAt) {
|
|
throw new Error("Package tarball upload must be created after its upload ticket");
|
|
}
|
|
|
|
await ctx.db.patch(ticket._id, {
|
|
usedAt: now,
|
|
storageId: args.storageId,
|
|
});
|
|
},
|
|
});
|