1import { isValidName, VAULT_SLOTS } from "../../../shared/mod.ts";2import type { Ttl, VaultLifecycle } from "../../../shared/mod.ts";3import {4 createVault,5 defaultVaults,6 listVaults,7 setDefaultVault,8 updateVault,9 vaultRowToInfo,10} from "../db.ts";11import { type Env, json } from "../env.ts";12import { ApiError, readJson } from "./errors.ts";1314const DESCRIPTION_MAX_LENGTH = 500;1516function descriptionValue(value: unknown): string | null {17 if (value === null) return null;18 if (typeof value !== "string") {19 throw new ApiError(400, "description must be a string or null");20 }21 const description = value.trim();22 if (description.length > DESCRIPTION_MAX_LENGTH) {23 throw new ApiError(24 400,25 `description must be at most ${DESCRIPTION_MAX_LENGTH} characters`,26 );27 }28 // Descriptions are rendered raw into CLI tables; keep them single-line.29 if (/[\x00-\x1f\x7f-\x9f]/.test(description)) {30 throw new ApiError(400, "description must not contain control characters");31 }32 return description || null;33}3435function defaultTtlValue(value: unknown): Ttl | null {36 if (value === null || value === "forever") return value;37 if (38 typeof value !== "number" || !Number.isFinite(value) || value <= 0 ||39 value > 365040 ) {41 throw new ApiError(42 400,43 'defaultTtl must be null, 1-3650 days, or "forever"',44 );45 }46 return value;47}4849function lifecycleValue(value: unknown): VaultLifecycle | null {50 if (value === null || value === "temporary" || value === "permanent") {51 return value;52 }53 throw new ApiError(54 400,55 'defaultFor must be null, "temporary", or "permanent"',56 );57}5859export async function handleVaults(60 request: Request,61 env: Env,62): Promise<Response> {63 if (request.method === "GET") return json(await listVaults(env));64 const body = await readJson<{65 name: string;66 slot?: number;67 description?: unknown;68 defaultTtl?: unknown;69 defaultFor?: unknown;70 }>(request);71 if (!isValidName(body.name)) throw new ApiError(400, "invalid vault name");72 if (body.slot !== undefined && (body.slot < 0 || body.slot >= VAULT_SLOTS)) {73 throw new ApiError(400, "slot must be 0-15");74 }75 const description = body.description === undefined76 ? null77 : descriptionValue(body.description);78 const defaultTtl = body.defaultTtl === undefined79 ? null80 : defaultTtlValue(body.defaultTtl);81 const defaultFor = body.defaultFor === undefined82 ? null83 : lifecycleValue(body.defaultFor);84 try {85 const vault = await createVault(86 env,87 body.name,88 body.slot,89 description,90 defaultTtl,91 defaultFor,92 );93 return json(94 vaultRowToInfo(vault, 0, defaultFor === null ? [] : [defaultFor]),95 201,96 );97 } catch (cause) {98 const message = cause instanceof Error ? cause.message : "vault conflict";99 throw new ApiError(409, message, { cause });100 }101}102103export async function handleVault(104 request: Request,105 env: Env,106 encodedName: string,107): Promise<Response> {108 if (request.method !== "PATCH") throw new ApiError(405, "method not allowed");109 let currentName: string;110 try {111 currentName = decodeURIComponent(encodedName);112 } catch {113 throw new ApiError(400, "invalid vault name");114 }115 const body = await readJson<{116 name?: unknown;117 description?: unknown;118 defaultTtl?: unknown;119 defaultFor?: unknown;120 }>(request);121 if (122 body.name === undefined && body.description === undefined &&123 body.defaultTtl === undefined && body.defaultFor === undefined124 ) {125 throw new ApiError(126 400,127 "provide name, description, defaultTtl, and/or defaultFor",128 );129 }130 if (131 body.name !== undefined &&132 (typeof body.name !== "string" || !isValidName(body.name))133 ) {134 throw new ApiError(400, "invalid vault name");135 }136 const patch = {137 ...(typeof body.name === "string" ? { name: body.name } : {}),138 ...(body.description !== undefined139 ? { description: descriptionValue(body.description) }140 : {}),141 ...(body.defaultTtl !== undefined142 ? { defaultTtl: defaultTtlValue(body.defaultTtl) }143 : {}),144 ...(body.defaultFor !== undefined145 ? { defaultFor: lifecycleValue(body.defaultFor) }146 : {}),147 };148 try {149 const vault = await updateVault(env, currentName, patch);150 if (!vault) throw new ApiError(404, `unknown vault: ${currentName}`);151 return json(vault);152 } catch (cause) {153 if (cause instanceof ApiError) throw cause;154 const message = cause instanceof Error ? cause.message : "vault conflict";155 throw new ApiError(409, message, { cause });156 }157}158159export async function handleDefaultVault(160 request: Request,161 env: Env,162 lifecycleSegment: string,163): Promise<Response> {164 const lifecycle = lifecycleValue(lifecycleSegment);165 if (lifecycle === null) throw new ApiError(400, "lifecycle is required");166 const body = await readJson<{ name?: unknown }>(request);167 if (typeof body.name !== "string" || !isValidName(body.name)) {168 throw new ApiError(400, "valid vault name required");169 }170 const vault = await setDefaultVault(env, lifecycle, body.name);171 if (!vault) throw new ApiError(404, `unknown vault: ${body.name}`);172 return json({ defaultVaults: await defaultVaults(env), vault });173}