@sellable/install
Version:
One-command installer for Sellable MCP in Claude Code, Codex, and Hermes
329 lines (313 loc) • 10.7 kB
JavaScript
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;
});
}