UNPKG

@sellable/install

Version:

One-command installer for Sellable MCP in Claude Code, Codex, and Hermes

329 lines (313 loc) • 10.7 kB
#!/usr/bin/env node import { createHash } from "node:crypto"; import { existsSync, lstatSync, readFileSync } from "node:fs"; import { basename, dirname, isAbsolute, resolve } from "node:path"; import { pathToFileURL } from "node:url"; // Use the canonical application origin directly. The apex host redirects to // app.sellable.dev, and Fetch intentionally strips Authorization when a // redirect crosses origins; following that redirect would turn every valid // endpoint invocation key into an unauthenticated admission request. const ORIGIN = "https://app.sellable.dev"; const SCHEMA_V1 = "sellable-agent-endpoint-cron-binding/v1"; const SCHEMA_V2 = "sellable-agent-endpoint-cron-binding/v2"; const SCHEMA_V3 = "sellable-agent-endpoint-cron-binding/v3"; const DEFINITIVE_REPORT_RETRY = "definitive-report-failure-v1"; const TERMINAL = new Set([ "SUCCEEDED", "OUTPUT_CONTRACT_FAILED", "FAILED", "UNCERTAIN", "TIMED_OUT", "TIMED_OUT_UNCERTAIN", "CANCELLED_PRE_EFFECT", "SUPERSEDED", "LATE_COMPLETION", ]); const REPORT_RETRYABLE_TERMINALS = new Set([ "OUTPUT_CONTRACT_FAILED", "FAILED", "CANCELLED_PRE_EFFECT", "SUPERSEDED", ]); const MAX_REPORT_ATTEMPTS = 3; const SAFE_NAME = /^[a-z0-9][a-z0-9-]{0,62}$/; const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/; const ENDPOINT_KEY = /^sek_[A-Za-z0-9_-]{16,512}$/; const sha256 = (value) => createHash("sha256").update(value).digest("hex"); function privateRegularFile(path) { if (!existsSync(path)) throw new Error("endpoint_cron_binding_missing"); const stat = lstatSync(path); if ( stat.isSymbolicLink() || !stat.isFile() || (stat.mode & 0o077) !== 0 || stat.uid !== process.getuid() ) { throw new Error("endpoint_cron_binding_rejected"); } } export function endpointCronBindingPath( name, hermesHome = process.env.HERMES_HOME ) { if (!SAFE_NAME.test(name ?? "") || !isAbsolute(hermesHome ?? "")) { throw new Error("endpoint_cron_binding_identity_rejected"); } const root = resolve(hermesHome, ".sellable-agent", "cron-endpoints"); const path = resolve(root, `${name}.json`); if (dirname(path) !== root || basename(path) !== `${name}.json`) { throw new Error("endpoint_cron_binding_identity_rejected"); } return path; } export function readEndpointCronBinding(name, hermesHome) { const path = endpointCronBindingPath(name, hermesHome); privateRegularFile(path); let value; try { value = JSON.parse(readFileSync(path, "utf8")); } catch { throw new Error("endpoint_cron_binding_invalid"); } const keys = Object.keys(value ?? {}).sort(); const commonKeys = [ "endpointId", "input", "invocationKey", "schemaVersion", "timeoutSeconds", ]; const expectedKeys = value?.schemaVersion === SCHEMA_V3 ? [...commonKeys, "outputMode", "retryMode"].sort() : value?.schemaVersion === SCHEMA_V2 ? [...commonKeys, "outputMode"].sort() : commonKeys; const outputMode = value?.schemaVersion === SCHEMA_V1 ? "passthrough" : value?.outputMode; const retryMode = value?.schemaVersion === SCHEMA_V3 ? value?.retryMode : "none"; if ( JSON.stringify(keys) !== JSON.stringify(expectedKeys) || ![SCHEMA_V1, SCHEMA_V2, SCHEMA_V3].includes(value.schemaVersion) || !["passthrough", "report-v1"].includes(outputMode) || ([SCHEMA_V2, SCHEMA_V3].includes(value.schemaVersion) && outputMode === "passthrough") || (value.schemaVersion === SCHEMA_V3 && retryMode !== DEFINITIVE_REPORT_RETRY) || !SAFE_ID.test(value.endpointId ?? "") || !ENDPOINT_KEY.test(value.invocationKey ?? "") || typeof value.input !== "string" || value.input.length < 1 || Buffer.byteLength(value.input) > 64 * 1024 || !Number.isSafeInteger(value.timeoutSeconds) || value.timeoutSeconds < 30 || value.timeoutSeconds > 3_600 ) { throw new Error("endpoint_cron_binding_invalid"); } return Object.freeze({ ...value, outputMode, retryMode, name, path }); } function projectEndpointCronOutput(output, outputMode) { if (outputMode === "passthrough") return output; let projected = output; if ( typeof projected === "string" && Buffer.byteLength(projected) <= 64 * 1024 ) { try { projected = JSON.parse(projected); } catch { // The exact report contract below remains fail-closed. } } const keys = Object.keys(projected ?? {}).sort(); if ( !projected || typeof projected !== "object" || Array.isArray(projected) || JSON.stringify(keys) !== JSON.stringify(["report", "status"]) || projected.status !== "ok" || typeof projected.report !== "string" || projected.report.trim().length < 20 || Buffer.byteLength(projected.report) > 64 * 1024 ) { throw new Error("endpoint_cron_output_rejected"); } return projected.report; } async function jsonResponse(response, code) { let body; try { body = await response.json(); } catch { throw new Error(code); } if (!response.ok || !body?.data) throw new Error(code); return body.data; } function assertStatusUrl(value, runId) { let url; try { url = new URL(value); } catch { throw new Error("endpoint_cron_status_url_rejected"); } if ( url.origin !== ORIGIN || url.username || url.password || url.search || url.hash || url.pathname !== `/api/v3/agent-endpoint-runs/${runId}` ) { throw new Error("endpoint_cron_status_url_rejected"); } return url.href; } async function invokeEndpointRun({ binding, deadline, fetchImpl, idempotencyKey, now, sleep, }) { const invocationUrl = `${ORIGIN}/api/v3/agent-endpoints/${binding.endpointId}`; const accepted = await jsonResponse( await fetchImpl(invocationUrl, { method: "POST", headers: { authorization: `Bearer ${binding.invocationKey}`, "content-type": "application/json", "idempotency-key": idempotencyKey, }, body: JSON.stringify({ mode: "async", input: binding.input }), signal: AbortSignal.timeout(30_000), }), "endpoint_cron_admission_failed" ); if ( !SAFE_ID.test(accepted.runId ?? "") || typeof accepted.accessCapability !== "string" || accepted.accessCapability.length < 16 ) { throw new Error("endpoint_cron_admission_rejected"); } const statusUrl = assertStatusUrl(accepted.statusUrl, accepted.runId); let projection = accepted; while (!TERMINAL.has(projection.status)) { if (now().getTime() >= deadline) { throw new Error("endpoint_cron_wait_timed_out"); } await sleep(2_000); projection = await jsonResponse( await fetchImpl(statusUrl, { method: "GET", headers: { authorization: `Bearer ${accepted.accessCapability}` }, signal: AbortSignal.timeout(30_000), }), "endpoint_cron_status_failed" ); // Admission names the identifier `runId`; the public status projection // intentionally uses its resource field name, `id`. Accept either closed // representation, but always require an exact match to the admitted run. const projectionRunId = projection.runId ?? projection.id; if (projectionRunId !== accepted.runId) { throw new Error("endpoint_cron_status_rejected"); } } return { accepted, projection }; } function reportRetryKey(baseKey, attemptNumber, runId, reason) { return `${baseKey}-r${attemptNumber}-${sha256(`${runId}:${reason}`).slice(0, 12)}`; } export async function invokeEndpointCronBinding(binding, options = {}) { const fetchImpl = options.fetchImpl ?? fetch; const now = options.now ?? (() => new Date()); const sleep = options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms))); const deadline = now().getTime() + binding.timeoutSeconds * 1_000; const utcDay = now().toISOString().slice(0, 10); const baseKey = `hermes-cron-${binding.name}-${utcDay}-${sha256(binding.input).slice(0, 12)}`; const maxAttempts = binding.retryMode === DEFINITIVE_REPORT_RETRY ? MAX_REPORT_ATTEMPTS : 1; let idempotencyKey = baseKey; let terminalError; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { const { accepted, projection } = await invokeEndpointRun({ binding, deadline, fetchImpl, idempotencyKey, now, sleep, }); if (projection.status === "SUCCEEDED") { try { return projectEndpointCronOutput(projection.output, binding.outputMode); } catch (error) { terminalError = error; if ( binding.retryMode !== DEFINITIVE_REPORT_RETRY || attempt === maxAttempts || !(error instanceof Error) || error.message !== "endpoint_cron_output_rejected" ) { throw error; } // The private v3 binding is the operator's explicit assertion that // this endpoint is a retry-safe report. A terminal run that did not // produce its output contract must not poison every later invocation // through the daily base key. Deriving the retry key from the rejected // run makes concurrent/restarted callers converge on the same retry. idempotencyKey = reportRetryKey( baseKey, attempt, accepted.runId, "output_rejected" ); continue; } } terminalError = new Error( `endpoint_cron_terminal_${String(projection.status).toLowerCase()}` ); if ( binding.retryMode !== DEFINITIVE_REPORT_RETRY || attempt === maxAttempts || !REPORT_RETRYABLE_TERMINALS.has(projection.status) ) { throw terminalError; } // Never retry UNCERTAIN or timeout states: their effect disposition is not // known. Definitive report failures can advance using a run-bound key. idempotencyKey = reportRetryKey( baseKey, attempt, accepted.runId, projection.status.toLowerCase() ); } throw terminalError ?? new Error("endpoint_cron_failed"); } async function main() { const [name, ...extra] = process.argv.slice(2); if (!name || extra.length) throw new Error("endpoint_cron_usage_invalid"); const output = await invokeEndpointCronBinding(readEndpointCronBinding(name)); if (output === null || output === undefined || output === "") return; process.stdout.write( typeof output === "string" ? `${output}\n` : `${JSON.stringify(output)}\n` ); } if ( process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href ) { main().catch((error) => { process.stderr.write( `${error instanceof Error ? error.message : "endpoint_cron_failed"}\n` ); process.exitCode = 1; }); }