import { and, eq, isNotNull } from "drizzle-orm"; import { z } from "zod"; import { createUserAccessKey, listUserAccessKeys, } from "@/lib/access-keys/service"; import { MIN_PROVISIONING_SECRET_LENGTH, parseProvisioningBearerSecret, verifyProvisioningSecret, } from "@/lib/access-keys/provisioning-secret"; import { db } from "@/lib/db"; import { user } from "@/lib/db/schema"; const provisionAccessKeySchema = z.object({ dingTalkUsername: z.string().trim().min(1).max(100), dingTalkUserId: z.string().trim().min(1).max(128).optional(), keyName: z.string().trim().min(1).max(100).optional(), scopes: z .array(z.enum(["canvas:read", "canvas:write"])) .min(1) .optional(), expiresInDays: z.number().int().min(1).max(365).optional(), }); function jsonError(error: string, code: string, status: number) { return Response.json( { success: false, error, code }, { status, headers: { "Cache-Control": "no-store" } } ); } export async function POST(request: Request) { const expectedSecret = String( process.env.MCP_KEY_PROVISIONING_SECRET || "" ).trim(); if (expectedSecret.length < MIN_PROVISIONING_SECRET_LENGTH) { return jsonError( "Access Key provisioning is not configured", "PROVISIONING_NOT_CONFIGURED", 503 ); } const providedSecret = parseProvisioningBearerSecret( request.headers.get("authorization") ); if (!verifyProvisioningSecret(providedSecret, expectedSecret)) { return jsonError( "Missing or invalid provisioning credential", "PROVISIONING_UNAUTHORIZED", 401 ); } const parsedBody = provisionAccessKeySchema.safeParse( await request.json().catch(() => null) ); if (!parsedBody.success) { return jsonError( "Invalid provisioning request", "PROVISIONING_REQUEST_INVALID", 400 ); } const body = parsedBody.data; const conditions = [ eq(user.name, body.dingTalkUsername), eq(user.isActive, true), isNotNull(user.dingtalkUserId), ]; if (body.dingTalkUserId) { conditions.push(eq(user.dingtalkUserId, body.dingTalkUserId)); } const matches = await db .select({ id: user.id, name: user.name, dingtalkUserId: user.dingtalkUserId, }) .from(user) .where(and(...conditions)) .limit(2); if (matches.length === 0) { return jsonError( "No active DingTalk user matched the supplied identity", "DINGTALK_USER_NOT_FOUND", 404 ); } if (matches.length > 1) { return jsonError( "DingTalk username is not unique; provide dingTalkUserId", "DINGTALK_USERNAME_AMBIGUOUS", 409 ); } const matchedUser = matches[0]; const existingKeys = await listUserAccessKeys(matchedUser.id); const activeKeyCount = existingKeys.filter( (item) => !item.revokedAt && (!item.expiresAt || new Date(item.expiresAt).getTime() > Date.now()) ).length; if (activeKeyCount >= 10) { return jsonError( "The user already has 10 active Access Keys", "ACCESS_KEY_LIMIT_REACHED", 409 ); } try { const accessKey = await createUserAccessKey({ userId: matchedUser.id, name: body.keyName || "第三方 OpenClaw", scopes: body.scopes, expiresInDays: body.expiresInDays, }); return Response.json( { success: true, data: { user: matchedUser, accessKey, }, }, { status: 201, headers: { "Cache-Control": "no-store", Pragma: "no-cache", }, } ); } catch { return jsonError( "Failed to provision Access Key", "ACCESS_KEY_PROVISION_FAILED", 500 ); } }