UNPKG

@receiz/mcp-server

Version:

MCP server for agents building and verifying Receiz proof-native applications through the canonical Receiz SDK rails.

112 lines • 6 kB
import { createHash } from "node:crypto"; import { verifyReceizOfflineSealedFile } from "@receiz/sdk/offline"; export class ArtifactInputError extends Error { code; stage; constructor(code, stage) { super(code); this.code = code; this.stage = stage; } } export const RECEIZ_VERIFY_UPLOADED_ARTIFACT_TOOL = { name: "receiz_verify_uploaded_artifact", description: "Use this when verifying an original user-selected ChatGPT attachment. Retrieve its authorized original bytes and run the canonical Receiz verifier locally. No signing, claiming, resealing, appending or publishing. A ChatGPT /mnt/data path is not a device workspace path. MCP is not authority. The download reference is not proof authority.", inputSchema: { type: "object", properties: { file: { type: "object", properties: { download_url: { type: "string" }, file_id: { type: "string" }, mime_type: { type: "string" }, file_name: { type: "string" }, }, required: ["download_url", "file_id"], additionalProperties: false } }, required: ["file"], additionalProperties: false }, annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, idempotentHint: true }, _meta: { "openai/fileParams": ["file"] }, }; const MAX_BYTES = 32 * 1024 * 1024; const DOWNLOAD_TIMEOUT_MS = 15_000; const record = (v) => !!v && typeof v === "object" && !Array.isArray(v); /** Host configuration only; never accept permitted origins through a tool call. * The enclosing relay authenticates the request owner and retains exact attempts. * A selected file's capability URL grants retrieval only, never artifact authority. */ export async function readAuthorizedUploadedArtifact(raw, options = {}) { const deny = () => new ArtifactInputError("attachment_authorization_failed", "attachment_authorization"); if (!record(raw) || Object.keys(raw).some(k => k !== "file") || !record(raw.file)) throw deny(); const file = raw.file; if (Object.keys(file).some(k => !["download_url", "file_id", "mime_type", "file_name"].includes(k)) || typeof file.download_url !== "string" || file.download_url.length > 16_384 || typeof file.file_id !== "string" || !file.file_id.trim() || file.file_id.length > 512 || ["mime_type", "file_name"].some(k => file[k] !== undefined && (typeof file[k] !== "string" || file[k].length > 512))) throw deny(); let url; try { url = new URL(file.download_url); } catch { throw deny(); } const origins = options.allowedOrigins ?? (process.env.RECEIZ_ATTACHMENT_DOWNLOAD_ORIGINS ?? "").split(",").map(v => v.trim()).filter(Boolean); // Exact HTTPS origins, not suffix matching or model-supplied allowlists. No // redirects, cookies, OAuth headers, content conversion or custody access. if (url.protocol !== "https:" || url.username || url.password || url.hash || !origins.includes(url.origin)) throw deny(); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS); let bytes; try { const response = await (options.fetch ?? globalThis.fetch)(url.href, { signal: controller.signal, redirect: "error", credentials: "omit", cache: "no-store", }); if ([401, 403, 404, 410].includes(response.status)) throw new ArtifactInputError("attachment_unavailable", "download"); if (!response.ok || response.redirected || (response.url && response.url !== url.href)) throw new ArtifactInputError("attachment_download_failed", "download"); const length = response.headers.get("content-length"); if (length && Number(length) > MAX_BYTES) throw new ArtifactInputError("attachment_too_large", "download"); const reader = response.body?.getReader(); if (!reader) throw new ArtifactInputError("attachment_unavailable", "download"); const chunks = []; let total = 0; try { for (;;) { const next = await reader.read(); if (next.done) break; total += next.value.byteLength; if (total > MAX_BYTES) throw new ArtifactInputError("attachment_too_large", "download"); chunks.push(next.value); } if (!total) throw new ArtifactInputError("attachment_unavailable", "download"); bytes = Buffer.concat(chunks, total); } finally { await reader.cancel().catch(() => { }); reader.releaseLock(); } } catch (error) { if (error instanceof ArtifactInputError) throw error; throw new ArtifactInputError(controller.signal.aborted ? "attachment_download_timeout" : "attachment_download_failed", "download"); } finally { clearTimeout(timer); controller.abort(); } const transport = { byteLength: bytes.byteLength, sha256: createHash("sha256").update(bytes).digest("hex"), authority: false }; // Filename is a parser hint, never a path. MIME claims do not establish validity. const filename = typeof file.file_name === "string" ? file.file_name.split(/[\\/]/).pop() || "attachment" : "attachment"; return { bytes, filename, mimeType: typeof file.mime_type === "string" ? file.mime_type : undefined, transport }; } export async function verifyUploadedArtifact(raw, options = {}) { const { bytes, filename, transport } = await readAuthorizedUploadedArtifact(raw, options); try { const verification = await verifyReceizOfflineSealedFile({ bytes, filename }); return { verification, verdictObtained: true, transport, mcpAuthority: false }; } catch { throw new ArtifactInputError("artifact_verification_unavailable", "canonical_verification"); } } //# sourceMappingURL=uploadedArtifact.js.map