UNPKG

run402-mcp

Version:

MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.

133 lines • 6.12 kB
/** * v1.50 client-side validators for asset metadata, EXIF policy, and the * `Assets.list` (`assets.ls`) sort + filter surface. Every validator * throws {@link LocalError} with the same `code` the gateway would return * for the equivalent server-side rejection so consumers can branch on * `e.code` regardless of whether validation happened locally or remotely. * * - `INVALID_ASSET_METADATA` — nested objects, non-allowed leaf types, * undefined leaves, or serialized size > 4 KB. * - `INVALID_EXIF_POLICY` — value other than `"keep"` | `"strip"`. * - `INVALID_SORT` — sort value not in {@link ASSET_SORT_KEYS}. * - `INVALID_FILTER_KEY` — unknown key on the filter bag. */ import { LocalError } from "../errors.js"; import { ASSET_FILTER_KEYS, ASSET_SORT_KEYS, } from "./assets.types.js"; /** Maximum serialized size (bytes) for the caller-provided metadata bag. */ export const ASSET_METADATA_MAX_BYTES = 4096; /** v1.50: validate caller-supplied metadata bag. Throws {@link LocalError} * with `code === "INVALID_ASSET_METADATA"` for any structural violation. */ export function assertAssetMetadata(value, context) { if (value === null || typeof value !== "object" || Array.isArray(value)) { throw new LocalError("metadata must be a plain object with string / number / boolean / string[] leaves.", context, { code: "INVALID_ASSET_METADATA" }); } const obj = value; for (const [k, leaf] of Object.entries(obj)) { if (typeof k !== "string" || k.length === 0) { throw new LocalError("metadata keys must be non-empty strings.", context, { code: "INVALID_ASSET_METADATA", details: { key: k } }); } if (!isAssetMetadataValue(leaf)) { throw new LocalError(`metadata.${k} must be a string, number, boolean, or string[] — got ${describe(leaf)}.`, context, { code: "INVALID_ASSET_METADATA", details: { key: k } }); } } let serialized; try { serialized = JSON.stringify(obj); } catch (err) { throw new LocalError("metadata must be JSON-serializable.", context, { code: "INVALID_ASSET_METADATA", details: { cause: String(err) } }); } const bytes = byteLength(serialized); if (bytes > ASSET_METADATA_MAX_BYTES) { throw new LocalError(`metadata exceeds the 4 KB serialized limit (${bytes} bytes).`, context, { code: "INVALID_ASSET_METADATA", details: { size_bytes: bytes, limit_bytes: ASSET_METADATA_MAX_BYTES }, }); } } /** v1.50: validate the EXIF policy enum. */ export function assertExifPolicy(value, context) { if (value !== "keep" && value !== "strip") { throw new LocalError(`exifPolicy must be "keep" or "strip" — got ${describe(value)}.`, context, { code: "INVALID_EXIF_POLICY", details: { value } }); } } /** v1.50: validate the sort key enum. */ export function assertAssetSortKey(value, context) { if (typeof value !== "string" || !ASSET_SORT_KEYS.includes(value)) { throw new LocalError(`sort must be one of: ${ASSET_SORT_KEYS.join(", ")} — got ${describe(value)}.`, context, { code: "INVALID_SORT", details: { value } }); } } /** v1.50: reject unknown filter keys; type-check each known key. */ export function assertAssetFilter(value, context) { if (value === null || typeof value !== "object" || Array.isArray(value)) { throw new LocalError("filter must be a plain object.", context, { code: "INVALID_FILTER_KEY" }); } const obj = value; for (const [k, v] of Object.entries(obj)) { if (!ASSET_FILTER_KEYS.has(k)) { throw new LocalError(`filter.${k} is not a known filter key — allowed: ${[...ASSET_FILTER_KEYS].join(", ")}.`, context, { code: "INVALID_FILTER_KEY", details: { key: k } }); } if (v === undefined) continue; switch (k) { case "is_image": if (typeof v !== "boolean") { throw new LocalError(`filter.${k} must be a boolean — got ${describe(v)}.`, context, { code: "INVALID_FILTER_KEY", details: { key: k, value: v } }); } break; case "min_width": case "max_width": case "min_height": case "max_height": if (!Number.isSafeInteger(v) || v < 0) { throw new LocalError(`filter.${k} must be a non-negative integer — got ${describe(v)}.`, context, { code: "INVALID_FILTER_KEY", details: { key: k, value: v } }); } break; default: if (typeof v !== "string" || v.length === 0) { throw new LocalError(`filter.${k} must be a non-empty string — got ${describe(v)}.`, context, { code: "INVALID_FILTER_KEY", details: { key: k, value: v } }); } } } } /** * v1.50: serialize a validated filter bag as query parameters. The wire * convention is `filter[<key>]=<value>` per design D-filter (see private * gateway docs). Booleans render as `"true"` / `"false"`; numbers * render verbatim; string arrays are not allowed on filter values. */ export function appendAssetFilterTo(qs, filter) { for (const [k, v] of Object.entries(filter)) { if (v === undefined) continue; if (typeof v === "boolean") qs.set(`filter[${k}]`, v ? "true" : "false"); else qs.set(`filter[${k}]`, String(v)); } } function isAssetMetadataValue(v) { if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") { return true; } if (Array.isArray(v)) { for (const item of v) if (typeof item !== "string") return false; return true; } return false; } function describe(v) { if (v === null) return "null"; if (Array.isArray(v)) return "array"; return typeof v; } function byteLength(s) { // Node's TextEncoder is universally available in our supported runtimes. return new TextEncoder().encode(s).byteLength; } //# sourceMappingURL=assets-validation.js.map