feat: add c0mpute plugin and hire-us request flow

This commit is contained in:
Anthony Ettinger 2026-06-06 19:42:00 +00:00
parent 1a87c2f26d
commit b1d8fe475a
20 changed files with 1271 additions and 59 deletions

View file

@ -7,8 +7,17 @@ LOGICSRC_SCHEMA_VERSION=0.1
COINPAY_API_URL= COINPAY_API_URL=
COINPAY_API_KEY= COINPAY_API_KEY=
COINPAY_MERCHANT_ID= COINPAY_MERCHANT_ID=
COINPAY_BUSINESS_ID=
COINPAY_ELIGIBILITY_MERCHANT_ID=
COINPAY_ELIGIBILITY_API_KEY=
COINPAY_HIRE_US_BLOCKCHAIN=USDC_POL COINPAY_HIRE_US_BLOCKCHAIN=USDC_POL
COINPAY_WEBHOOK_SECRET= COINPAY_WEBHOOK_SECRET=
COINPAY_OAUTH_ISSUER=https://coinpayportal.com
COINPAY_OAUTH_CLIENT_ID=
COINPAY_OAUTH_CLIENT_SECRET=
COINPAY_OAUTH_REDIRECT_URI=https://logicsrc.com/api/oauth/coinpay/callback
COINPAY_OAUTH_SCOPES=openid profile email
LOGICSRC_SESSION_SECRET=
UGIG_API_URL= UGIG_API_URL=
UGIG_API_KEY= UGIG_API_KEY=

View file

@ -23,6 +23,7 @@ packages/
plugins/ plugins/
coinpay default DID, wallet, payment, and escrow plugin coinpay default DID, wallet, payment, and escrow plugin
ugig default jobs and gigs marketplace plugin ugig default jobs and gigs marketplace plugin
c0mpute work-in-progress compute jobs and worker pools plugin
docs/ docs/
specs, CLI conventions, permissions, and roadmap notes specs, CLI conventions, permissions, and roadmap notes
scripts/ scripts/
@ -59,4 +60,5 @@ It provides read-only resources for docs and schemas, validation/example tools,
- Credential Sharing OpenSpec for .env, Doppler, Railway variables, and GitHub Secrets. - Credential Sharing OpenSpec for .env, Doppler, Railway variables, and GitHub Secrets.
- CoinPay as the default payment, DID, wallet, and escrow plugin. - CoinPay as the default payment, DID, wallet, and escrow plugin.
- uGig as the default jobs and gigs marketplace plugin. - uGig as the default jobs and gigs marketplace plugin.
- c0mpute as a work-in-progress compute jobs and worker pools plugin.
- Installer, update/upgrade, remove/uninstall workflows. - Installer, update/upgrade, remove/uninstall workflows.

View file

@ -13,6 +13,7 @@
}, },
"dependencies": { "dependencies": {
"@logicsrc/plugin-core": "file:../../packages/plugin-core", "@logicsrc/plugin-core": "file:../../packages/plugin-core",
"@logicsrc/plugin-c0mpute": "file:../../plugins/c0mpute",
"@logicsrc/plugin-coinpay": "file:../../plugins/coinpay", "@logicsrc/plugin-coinpay": "file:../../plugins/coinpay",
"@logicsrc/plugin-sh1pt": "file:../../plugins/sh1pt", "@logicsrc/plugin-sh1pt": "file:../../plugins/sh1pt",
"@logicsrc/plugin-ugig": "file:../../plugins/ugig", "@logicsrc/plugin-ugig": "file:../../plugins/ugig",

View file

@ -39,7 +39,7 @@ describe("CommandBoard API contracts", () => {
expect(body).toEqual({ ok: true, service: "commandboard-api" }); expect(body).toEqual({ ok: true, service: "commandboard-api" });
}); });
it("exposes default plugin contract including sh1pt", async () => { it("exposes default plugin contract including product plugins", async () => {
const response = await fetch(`${baseUrl}/api/plugins`); const response = await fetch(`${baseUrl}/api/plugins`);
const body = await response.json() as { const body = await response.json() as {
plugins: Array<{ id: string; enabled: boolean; capabilities: string[] }>; plugins: Array<{ id: string; enabled: boolean; capabilities: string[] }>;
@ -47,12 +47,17 @@ describe("CommandBoard API contracts", () => {
}; };
expect(response.status).toBe(200); expect(response.status).toBe(200);
expect(body.plugins.map((plugin) => plugin.id)).toEqual(["coinpay", "ugig", "sh1pt"]); expect(body.plugins.map((plugin) => plugin.id)).toEqual(["coinpay", "ugig", "sh1pt", "c0mpute"]);
expect(body.plugins.find((plugin) => plugin.id === "sh1pt")).toMatchObject({ expect(body.plugins.find((plugin) => plugin.id === "sh1pt")).toMatchObject({
enabled: true, enabled: true,
capabilities: expect.arrayContaining(["projects.sync", "actions.publish", "deployments.status"]) capabilities: expect.arrayContaining(["projects.sync", "actions.publish", "deployments.status"])
}); });
expect(body.plugins.find((plugin) => plugin.id === "c0mpute")).toMatchObject({
enabled: true,
capabilities: expect.arrayContaining(["compute.jobs.sync", "compute.jobs.dispatch", "compute.workers.sync"])
});
expect(body.capabilities["actions.publish"]).toEqual(["sh1pt"]); expect(body.capabilities["actions.publish"]).toEqual(["sh1pt"]);
expect(body.capabilities["compute.jobs.dispatch"]).toEqual(["c0mpute"]);
}); });
it("exposes sh1pt project and action contracts", async () => { it("exposes sh1pt project and action contracts", async () => {
@ -83,6 +88,48 @@ describe("CommandBoard API contracts", () => {
}); });
}); });
it("exposes work-in-progress c0mpute jobs and worker contracts", async () => {
const jobsResponse = await fetch(`${baseUrl}/api/plugins/c0mpute/jobs`);
const jobsBody = await jobsResponse.json() as { jobs: Array<{ id: string; board: string; status: string; provider: string }> };
const workersResponse = await fetch(`${baseUrl}/api/plugins/c0mpute/workers`);
const workersBody = await workersResponse.json() as { workers: Array<{ id: string; status: string; capacity: string }> };
expect(jobsResponse.status).toBe(200);
expect(jobsBody.jobs[0]).toMatchObject({ id: "compute_job_1", board: "/projects/c0mpute", status: "draft", provider: "c0mpute.com" });
expect(workersResponse.status).toBe(200);
expect(workersBody.workers[0]).toMatchObject({ id: "worker_pool_1", status: "preview", capacity: "wip" });
});
it("accepts work-in-progress c0mpute dispatch and quote requests", async () => {
const dispatchResponse = await fetch(`${baseUrl}/api/plugins/c0mpute/jobs/dispatch`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ job_id: "compute_job_1" })
});
const dispatchBody = await dispatchResponse.json() as { accepted: boolean; job_id: string; status: string; board: string };
const quoteResponse = await fetch(`${baseUrl}/api/plugins/c0mpute/quotes`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ workload: "agent-run-smoke-test" })
});
const quoteBody = await quoteResponse.json() as { accepted: boolean; workload: string; provider: string; status: string };
expect(dispatchResponse.status).toBe(202);
expect(dispatchBody).toEqual({
accepted: true,
job_id: "compute_job_1",
status: "queued",
board: "/projects/c0mpute"
});
expect(quoteResponse.status).toBe(202);
expect(quoteBody).toMatchObject({
accepted: true,
workload: "agent-run-smoke-test",
provider: "c0mpute.com",
status: "draft"
});
});
it("rejects invalid LogicSRC task payloads", async () => { it("rejects invalid LogicSRC task payloads", async () => {
const response = await fetch(`${baseUrl}/api/tasks`, { const response = await fetch(`${baseUrl}/api/tasks`, {
method: "POST", method: "POST",

View file

@ -1,18 +1,20 @@
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import { createPluginRegistry } from "@logicsrc/plugin-core"; import { createPluginRegistry } from "@logicsrc/plugin-core";
import { c0mputePlugin } from "@logicsrc/plugin-c0mpute";
import { coinPayPlugin } from "@logicsrc/plugin-coinpay"; import { coinPayPlugin } from "@logicsrc/plugin-coinpay";
import { sh1ptPlugin } from "@logicsrc/plugin-sh1pt"; import { sh1ptPlugin } from "@logicsrc/plugin-sh1pt";
import { uGigPlugin } from "@logicsrc/plugin-ugig"; import { uGigPlugin } from "@logicsrc/plugin-ugig";
import { schemas, validate } from "@logicsrc/validators"; import { schemas, validate } from "@logicsrc/validators";
const registry = createPluginRegistry([coinPayPlugin, uGigPlugin, sh1ptPlugin]); const registry = createPluginRegistry([coinPayPlugin, uGigPlugin, sh1ptPlugin, c0mputePlugin]);
const boards = [ const boards = [
{ path: "/general", title: "General", description: "CommandBoard.run general discussion." }, { path: "/general", title: "General", description: "CommandBoard.run general discussion." },
{ path: "/gigs", title: "Gigs", description: "Paid work, uGig imports, and LogicSRC tasks." }, { path: "/gigs", title: "Gigs", description: "Paid work, uGig imports, and LogicSRC tasks." },
{ path: "/agents", title: "Agents", description: "Agent registration, runs, and capabilities." }, { path: "/agents", title: "Agents", description: "Agent registration, runs, and capabilities." },
{ path: "/projects/sh1pt", title: "sh1pt", description: "Project actions, releases, artifacts, and delivery status." } { path: "/projects/sh1pt", title: "sh1pt", description: "Project actions, releases, artifacts, and delivery status." },
{ path: "/projects/c0mpute", title: "c0mpute", description: "Compute jobs, worker pools, usage, and settlement status." }
]; ];
const tasks = [ const tasks = [
@ -41,6 +43,15 @@ const sh1ptActions = [
{ id: "action_deploy_preview", title: "Deploy preview", publishable: true } { id: "action_deploy_preview", title: "Deploy preview", publishable: true }
]; ];
const c0mputeJobs = [
{ id: "compute_job_1", board: "/projects/c0mpute", status: "draft", workload: "agent-run-smoke-test", provider: "c0mpute.com" },
{ id: "compute_job_2", board: "/projects/c0mpute", status: "queued", workload: "openspec-index-build", provider: "c0mpute.com" }
];
const c0mputeWorkers = [
{ id: "worker_pool_1", region: "us-west", status: "preview", capacity: "wip" }
];
export function createCommandBoardServer() { export function createCommandBoardServer() {
return createServer(async (request, response) => { return createServer(async (request, response) => {
try { try {
@ -127,6 +138,49 @@ async function route(request: IncomingMessage, response: ServerResponse) {
return; return;
} }
if (request.method === "GET" && url.pathname === "/api/plugins/c0mpute/jobs") {
json(response, 200, { jobs: c0mputeJobs });
return;
}
if (request.method === "GET" && url.pathname === "/api/plugins/c0mpute/workers") {
json(response, 200, { workers: c0mputeWorkers });
return;
}
if (request.method === "POST" && url.pathname === "/api/plugins/c0mpute/jobs/dispatch") {
const body = await readJson(request);
if (!isRecord(body) || typeof body.job_id !== "string") {
json(response, 422, { error: "Expected job_id" });
return;
}
json(response, 202, {
accepted: true,
job_id: body.job_id,
status: "queued",
board: typeof body.board === "string" ? body.board : "/projects/c0mpute"
});
return;
}
if (request.method === "POST" && url.pathname === "/api/plugins/c0mpute/quotes") {
const body = await readJson(request);
if (!isRecord(body) || typeof body.workload !== "string") {
json(response, 422, { error: "Expected workload" });
return;
}
json(response, 202, {
accepted: true,
quote_id: `quote_${Date.now()}`,
workload: body.workload,
provider: "c0mpute.com",
status: "draft"
});
return;
}
if (request.method === "GET" && url.pathname === "/api/schemas") { if (request.method === "GET" && url.pathname === "/api/schemas") {
json(response, 200, { schemas: Object.keys(schemas) }); json(response, 200, { schemas: Object.keys(schemas) });
return; return;

View file

@ -10,13 +10,15 @@ test.describe("CommandBoard.run PWA", () => {
await expect(page.getByText("QA checkout flow")).toBeVisible(); await expect(page.getByText("QA checkout flow")).toBeVisible();
}); });
test("shows default plugin status including sh1pt", async ({ page }) => { test("shows default plugin status including product plugins", async ({ page }) => {
await page.goto("/"); await page.goto("/");
await expect(page.getByText("CoinPay").last()).toBeVisible(); await expect(page.getByText("CoinPay").last()).toBeVisible();
await expect(page.getByText("uGig").last()).toBeVisible(); await expect(page.getByText("uGig").last()).toBeVisible();
await expect(page.getByText("sh1pt").last()).toBeVisible(); await expect(page.getByText("sh1pt").last()).toBeVisible();
await expect(page.getByText("c0mpute").last()).toBeVisible();
await expect(page.getByText("projects, actions, and releases")).toBeVisible(); await expect(page.getByText("projects, actions, and releases")).toBeVisible();
await expect(page.getByText("c0mpute enabled · compute jobs and worker pools")).toBeVisible();
}); });
test("surfaces sh1pt project activity", async ({ page }) => { test("surfaces sh1pt project activity", async ({ page }) => {
@ -26,4 +28,12 @@ test.describe("CommandBoard.run PWA", () => {
await expect(page.getByText("Release action published")).toBeVisible(); await expect(page.getByText("Release action published")).toBeVisible();
await expect(page.getByText("/projects/sh1pt · deployment ready")).toBeVisible(); await expect(page.getByText("/projects/sh1pt · deployment ready")).toBeVisible();
}); });
test("surfaces c0mpute preview activity", async ({ page }) => {
await page.goto("/");
await expect(page.getByText("/projects/c0mpute", { exact: true })).toBeVisible();
await expect(page.getByText("Compute job queued")).toBeVisible();
await expect(page.getByText("/projects/c0mpute · worker pool preview")).toBeVisible();
});
}); });

View file

@ -4,13 +4,15 @@ const boards = [
{ name: "/gigs", count: 12, label: "Paid tasks and uGig jobs" }, { name: "/gigs", count: 12, label: "Paid tasks and uGig jobs" },
{ name: "/agents", count: 4, label: "Agent registrations and runs" }, { name: "/agents", count: 4, label: "Agent registrations and runs" },
{ name: "/qa", count: 7, label: "Testing, reports, acceptance" }, { name: "/qa", count: 7, label: "Testing, reports, acceptance" },
{ name: "/projects/sh1pt", count: 5, label: "Actions, releases, delivery" } { name: "/projects/sh1pt", count: 5, label: "Actions, releases, delivery" },
{ name: "/projects/c0mpute", count: 2, label: "Compute jobs and worker pools" }
]; ];
const tasks = [ const tasks = [
{ tag: "TASK", title: "QA checkout flow", meta: "25 USDC · submitted · qa-agent-01.coinpay" }, { tag: "TASK", title: "QA checkout flow", meta: "25 USDC · submitted · qa-agent-01.coinpay" },
{ tag: "uGig", title: "Senior AI Engineer remote", meta: "/gigs · synced from uGig" }, { tag: "uGig", title: "Senior AI Engineer remote", meta: "/gigs · synced from uGig" },
{ tag: "sh1pt", title: "Release action published", meta: "/projects/sh1pt · deployment ready" }, { tag: "sh1pt", title: "Release action published", meta: "/projects/sh1pt · deployment ready" },
{ tag: "c0mpute", title: "Compute job queued", meta: "/projects/c0mpute · worker pool preview" },
{ tag: "RUN", title: "crawlproof-bot completed task_123", meta: "logs available" } { tag: "RUN", title: "crawlproof-bot completed task_123", meta: "logs available" }
]; ];
@ -96,6 +98,7 @@ document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
<p><strong>CoinPay</strong> enabled · default payment and DID</p> <p><strong>CoinPay</strong> enabled · default payment and DID</p>
<p><strong>uGig</strong> enabled · default jobs marketplace</p> <p><strong>uGig</strong> enabled · default jobs marketplace</p>
<p><strong>sh1pt</strong> enabled · projects, actions, and releases</p> <p><strong>sh1pt</strong> enabled · projects, actions, and releases</p>
<p><strong>c0mpute</strong> enabled · compute jobs and worker pools</p>
</div> </div>
</section> </section>
</section> </section>

View file

@ -1,11 +1,15 @@
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { createHmac } from "node:crypto";
import { accessSync } from "node:fs"; import { accessSync } from "node:fs";
import { createServer, type Server as HttpServer } from "node:http";
import type { AddressInfo } from "node:net";
import { setTimeout as delay } from "node:timers/promises"; import { setTimeout as delay } from "node:timers/promises";
import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { afterAll, beforeAll, describe, expect, it } from "vitest";
let server: ChildProcessWithoutNullStreams; let server: ChildProcessWithoutNullStreams;
const port = 4291; const port = 4291;
const baseUrl = `http://127.0.0.1:${port}`; const baseUrl = `http://127.0.0.1:${port}`;
let nextCheckoutPort = 4292;
beforeAll(async () => { beforeAll(async () => {
accessSync(new URL("../dist/index.html", import.meta.url)); accessSync(new URL("../dist/index.html", import.meta.url));
@ -19,7 +23,7 @@ beforeAll(async () => {
} }
}); });
await waitForServer(); await waitForServer(baseUrl);
}); });
afterAll(() => { afterAll(() => {
@ -75,13 +79,429 @@ describe("LogicSRC web contracts", () => {
expect(response.headers.get("cache-control")).toBe("no-store"); expect(response.headers.get("cache-control")).toBe("no-store");
expect(body).toEqual({ success: false, error: "CoinPay checkout is not configured" }); expect(body).toEqual({ success: false, error: "CoinPay checkout is not configured" });
}); });
it("creates CoinPay checkout with card and crypto when both rails are available", async () => {
const { response, body, upstreamRequests } = await createCheckout({
eligibility: { accepts_card: true, accepts_crypto: true, chains: ["USDC_POL"] },
payment: { stripe_checkout_url: "https://checkout.stripe.test/session" }
});
const paymentRequest = upstreamRequests.find((request) => request.url === "/api/payments/create");
expect(response.status).toBe(201);
expect(body.payment.checkout_url).toBe("https://checkout.stripe.test/session");
expect(paymentRequest?.method).toBe("POST");
expect(paymentRequest?.authorization).toBe("Bearer cp_test_key");
expect(paymentRequest?.body).toMatchObject({
business_id: "business-123",
amount_usd: 250,
payment_method: "both",
currency: "usdc_pol",
blockchain: "USDC_POL",
description: "LogicSRC Hire Us - $250/week",
success_url: "https://logicsrc.test/hire-us?payment=success",
cancel_url: "https://logicsrc.test/hire-us?payment=cancelled",
redirect_url: "https://logicsrc.test/hire-us?payment=coinpay",
webhook_url: "https://logicsrc.test/api/webhooks/coinpay",
metadata: {
product: "logicsrc-hire-us",
interval: "week",
source: "logicsrc.com/hire-us",
buyer_email: "buyer@example.com"
}
});
});
it("uses card-only checkout when Stripe is available and configured crypto is not", async () => {
const { response, body, upstreamRequests } = await createCheckout({
eligibility: { accepts_card: true, accepts_crypto: false, chains: [] },
payment: { stripe_checkout_url: "https://checkout.stripe.test/card-only" }
});
const paymentRequest = upstreamRequests.find((request) => request.url === "/api/payments/create");
expect(response.status).toBe(201);
expect(body.payment.checkout_url).toBe("https://checkout.stripe.test/card-only");
expect(paymentRequest?.body).toMatchObject({
payment_method: "card",
currency: "card"
});
expect(paymentRequest?.body).not.toHaveProperty("blockchain");
});
it("uses crypto-only checkout when Stripe is not enabled", async () => {
const { response, body, upstreamRequests } = await createCheckout({
eligibility: { accepts_card: false, accepts_crypto: true, chains: ["USDC_POL"] },
payment: {}
});
const paymentRequest = upstreamRequests.find((request) => request.url === "/api/payments/create");
expect(response.status).toBe(201);
expect(body.payment.checkout_url).toBeNull();
expect(body.payment.address).toBe("0xabc");
expect(paymentRequest?.body).toMatchObject({
payment_method: "crypto",
currency: "usdc_pol",
blockchain: "USDC_POL"
});
});
it("does not create CoinPay checkout when no payment rail is available", async () => {
const { response, body, upstreamRequests } = await createCheckout({
eligibility: { accepts_card: false, accepts_crypto: false, chains: [] },
payment: {}
});
expect(response.status).toBe(503);
expect(body).toEqual({
success: false,
error: "CoinPay checkout is not available for this merchant"
});
expect(upstreamRequests.some((request) => request.url === "/api/payments/create")).toBe(false);
});
it("accepts Hire Us project requests before creating a recurring invoice", async () => {
const { appServer, appBaseUrl } = await startApp({});
try {
const response = await fetch(`${appBaseUrl}/api/hire-us/project-request`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
contact: "buyer@example.com",
project: "Build a LogicSRC plugin and API contract for a recurring agent workflow."
})
});
const body = await response.json();
expect(response.status).toBe(202);
expect(body).toMatchObject({
success: true,
request: {
status: "pending_acceptance",
amount_usd: 250,
interval: "week",
invoice: "created_after_acceptance"
}
});
expect(body.request.id).toMatch(/^hire_/);
} finally {
appServer.kill();
}
});
it("accepts signed CoinPay payment webhooks", async () => {
const secret = "whsec_test";
const { appServer, appBaseUrl } = await startApp({
COINPAY_API_KEY: "cp_test_key",
COINPAY_MERCHANT_ID: "business-123",
COINPAY_WEBHOOK_SECRET: secret
});
const payload = JSON.stringify({
id: "evt_1",
type: "payment.forwarded",
data: { payment_id: "pay_123", status: "forwarded" }
});
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = createHmac("sha256", secret).update(`${timestamp}.${payload}`).digest("hex");
try {
const response = await fetch(`${appBaseUrl}/api/webhooks/coinpay`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-coinpay-signature": `t=${timestamp},v1=${signature}`
},
body: payload
});
const body = await response.json();
expect(response.status).toBe(200);
expect(body).toEqual({ received: true, complete: true, payment_id: "pay_123" });
} finally {
appServer.kill();
}
});
it("rejects unsigned CoinPay payment webhooks", async () => {
const { appServer, appBaseUrl } = await startApp({
COINPAY_API_KEY: "cp_test_key",
COINPAY_MERCHANT_ID: "business-123",
COINPAY_WEBHOOK_SECRET: "whsec_test"
});
try {
const response = await fetch(`${appBaseUrl}/api/webhooks/coinpay`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ type: "payment.confirmed", data: { payment_id: "pay_123" } })
});
const body = await response.json();
expect(response.status).toBe(401);
expect(body).toEqual({ success: false, error: "Invalid signature" });
} finally {
appServer.kill();
}
});
it("starts CoinPay OAuth with a registered callback and state cookie", async () => {
const { appServer, appBaseUrl } = await startApp({
COINPAY_OAUTH_ISSUER: "https://coinpayportal.example",
COINPAY_OAUTH_CLIENT_ID: "cp_test_client",
COINPAY_OAUTH_CLIENT_SECRET: "cps_test_secret",
COINPAY_OAUTH_REDIRECT_URI: "https://logicsrc.com/api/oauth/coinpay/callback"
});
try {
const response = await fetch(`${appBaseUrl}/api/oauth/coinpay/start`, { redirect: "manual" });
const location = new URL(response.headers.get("location") ?? "");
expect(response.status).toBe(302);
expect(location.origin).toBe("https://coinpayportal.example");
expect(location.pathname).toBe("/api/oauth/authorize");
expect(location.searchParams.get("response_type")).toBe("code");
expect(location.searchParams.get("client_id")).toBe("cp_test_client");
expect(location.searchParams.get("redirect_uri")).toBe("https://logicsrc.com/api/oauth/coinpay/callback");
expect(location.searchParams.get("scope")).toBe("openid profile email");
expect(location.searchParams.get("state")).toMatch(/^[a-f0-9]{32}$/);
expect(response.headers.get("set-cookie")).toContain("logicsrc_coinpay_oauth_state=");
} finally {
appServer.kill();
}
});
it("exchanges CoinPay OAuth callback codes and stores a signed session", async () => {
const { fakeCoinPay, fakeCoinPayBaseUrl, tokenRequests } = await startFakeCoinPayOAuth();
const { appServer, appBaseUrl } = await startApp({
COINPAY_OAUTH_ISSUER: fakeCoinPayBaseUrl,
COINPAY_OAUTH_CLIENT_ID: "cp_test_client",
COINPAY_OAUTH_CLIENT_SECRET: "cps_test_secret",
COINPAY_OAUTH_REDIRECT_URI: "https://logicsrc.com/api/oauth/coinpay/callback",
LOGICSRC_SESSION_SECRET: "session_secret_for_tests"
});
try {
const response = await fetch(`${appBaseUrl}/api/oauth/coinpay/callback?code=auth_code_123&state=state_123`, {
redirect: "manual",
headers: { cookie: "logicsrc_coinpay_oauth_state=state_123" }
});
const cookieHeader = response.headers.get("set-cookie") ?? "";
const sessionCookie = cookieHeader.match(/logicsrc_coinpay_session=([^;]+)/)?.[0];
expect(response.status).toBe(302);
expect(response.headers.get("location")).toBe("/?coinpay_oauth=connected");
expect(sessionCookie).toBeTruthy();
expect(tokenRequests[0]).toMatchObject({
grant_type: "authorization_code",
code: "auth_code_123",
redirect_uri: "https://logicsrc.com/api/oauth/coinpay/callback",
client_id: "cp_test_client",
client_secret: "cps_test_secret"
});
const sessionResponse = await fetch(`${appBaseUrl}/api/oauth/coinpay/session`, {
headers: { cookie: sessionCookie ?? "" }
});
const sessionBody = await sessionResponse.json();
expect(sessionBody).toMatchObject({
authenticated: true,
user: {
provider: "coinpay",
sub: "merchant-123",
email: "merchant@example.com",
name: "Merchant User",
scope: "openid profile email"
}
});
} finally {
appServer.kill();
await close(fakeCoinPay);
}
});
}); });
async function waitForServer() { async function createCheckout({
eligibility,
payment
}: {
eligibility: { accepts_card: boolean; accepts_crypto: boolean; chains: string[] };
payment: Record<string, unknown>;
}) {
const upstreamRequests: Array<{
method?: string;
url?: string;
authorization?: string;
body?: Record<string, unknown>;
}> = [];
const fakeCoinPay = createServer((request, response) => {
let rawBody = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
rawBody += chunk;
});
request.on("end", () => {
upstreamRequests.push({
method: request.method,
url: request.url,
authorization: request.headers.authorization,
body: rawBody ? JSON.parse(rawBody) : undefined
});
if (request.method === "GET" && request.url?.startsWith("/api/payments/merchant-eligibility")) {
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({
success: true,
merchant_id: "merchant-123",
...eligibility
}));
return;
}
if (request.method === "POST" && request.url === "/api/payments/create") {
response.writeHead(201, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({
success: true,
payment: {
id: "pay_123",
amount_usd: 250,
blockchain: "USDC_POL",
amount_crypto: "499.5",
payment_address: "0xabc",
expires_at: "2030-01-01T00:00:00.000Z",
status: "pending",
...payment
}
}));
return;
}
response.writeHead(404, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({ success: false, error: "not found" }));
});
});
await listen(fakeCoinPay);
const fakeCoinPayPort = (fakeCoinPay.address() as AddressInfo).port;
const appPort = nextCheckoutPort;
nextCheckoutPort += 1;
const { appServer, appBaseUrl } = await startApp({
PORT: String(appPort),
COINPAY_API_KEY: "cp_test_key",
COINPAY_API_URL: `http://127.0.0.1:${fakeCoinPayPort}`,
COINPAY_BUSINESS_ID: "business-123",
COINPAY_ELIGIBILITY_MERCHANT_ID: "merchant-123",
COINPAY_HIRE_US_BLOCKCHAIN: "USDC_POL",
PUBLIC_URL: "https://logicsrc.test"
});
try {
await waitForServer(appBaseUrl);
const response = await fetch(`${appBaseUrl}/api/hire-us/coinpay-checkout`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ email: "buyer@example.com" })
});
const body = await response.json();
return { response, body, upstreamRequests };
} finally {
appServer.kill();
await close(fakeCoinPay);
}
}
async function startApp(env: Record<string, string>) {
const appPort = env.PORT ? Number(env.PORT) : nextCheckoutPort;
if (!env.PORT) {
nextCheckoutPort += 1;
}
const appBaseUrl = `http://127.0.0.1:${appPort}`;
const appServer = spawn(process.execPath, ["server.js"], {
cwd: new URL("..", import.meta.url),
env: {
...process.env,
PORT: String(appPort),
COINPAY_API_URL: "https://coinpayportal.example",
PUBLIC_URL: "https://logicsrc.test",
...env
}
});
await waitForServer(appBaseUrl);
return { appServer, appBaseUrl };
}
async function startFakeCoinPayOAuth() {
const tokenRequests: Array<Record<string, string>> = [];
const fakeCoinPay = createServer((request, response) => {
let rawBody = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
rawBody += chunk;
});
request.on("end", () => {
if (request.method === "POST" && request.url === "/api/oauth/token") {
tokenRequests.push(Object.fromEntries(new URLSearchParams(rawBody)));
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({
access_token: "access_token_123",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "refresh_token_123",
scope: "openid profile email"
}));
return;
}
if (request.method === "GET" && request.url === "/api/oauth/userinfo") {
response.writeHead(200, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({
sub: "merchant-123",
email: "merchant@example.com",
name: "Merchant User"
}));
return;
}
response.writeHead(404, { "content-type": "application/json; charset=utf-8" });
response.end(JSON.stringify({ error: "not found" }));
});
});
await listen(fakeCoinPay);
const fakeCoinPayPort = (fakeCoinPay.address() as AddressInfo).port;
return {
fakeCoinPay,
fakeCoinPayBaseUrl: `http://127.0.0.1:${fakeCoinPayPort}`,
tokenRequests
};
}
function listen(server: HttpServer) {
return new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
}
function close(server: HttpServer) {
return new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
async function waitForServer(serverBaseUrl: string) {
let lastError: unknown; let lastError: unknown;
for (let attempt = 0; attempt < 40; attempt += 1) { for (let attempt = 0; attempt < 40; attempt += 1) {
try { try {
const response = await fetch(`${baseUrl}/health`); const response = await fetch(`${serverBaseUrl}/health`);
if (response.ok) { if (response.ok) {
return; return;
} }

View file

@ -35,16 +35,19 @@ test.describe("LogicSRC PWA", () => {
await expect(page.getByText("/privacy · Privacy")).toBeVisible(); await expect(page.getByText("/privacy · Privacy")).toBeVisible();
}); });
test("renders Hire Us offer and CoinPay payment CTA", async ({ page }) => { test("renders Hire Us project request flow", async ({ page }) => {
await page.goto("/hire-us"); await page.goto("/hire-us");
await expect(page.getByRole("heading", { name: "Hire Us", exact: true })).toBeVisible(); await expect(page.getByRole("heading", { name: "Hire Us", exact: true })).toBeVisible();
await expect(page.locator(".price-row strong", { hasText: "$500" })).toBeVisible(); await expect(page.locator(".price-row strong", { hasText: "$250" })).toBeVisible();
await expect(page.getByText("per week")).toBeVisible(); await expect(page.getByText("per week")).toBeVisible();
await expect(page.getByText("open infrastructure and open specs for AI agent systems")).toBeVisible(); await expect(page.getByText("open infrastructure and open specs for AI agent systems")).toBeVisible();
await expect(page.getByRole("button", { name: "Pay with CoinPay" })).toBeVisible(); await expect(page.getByRole("button", { name: "Request review" })).toBeVisible();
await expect(page.getByPlaceholder("you@example.com")).toBeVisible();
await expect(page.getByPlaceholder("Describe the agent workflow")).toBeVisible();
await expect(page.getByText("without exposing merchant credentials to the browser")).toBeVisible(); await expect(page.getByText("without exposing merchant credentials to the browser")).toBeVisible();
await expect(page.getByText("COINPAY_PRODUCT=logicsrc-hire-us")).toBeVisible(); await expect(page.getByText("COINPAY_PRODUCT=logicsrc-hire-us")).toBeVisible();
await expect(page.getByText("COINPAY_STATUS=pending_acceptance")).toBeVisible();
}); });
test("serves sitemap and RSS XML endpoints", async ({ request }) => { test("serves sitemap and RSS XML endpoints", async ({ request }) => {

View file

@ -1,4 +1,5 @@
import { createReadStream, existsSync, statSync } from "node:fs"; import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
import { createReadStream, existsSync, readFileSync, statSync } from "node:fs";
import { createServer } from "node:http"; import { createServer } from "node:http";
import { extname, join, normalize, resolve } from "node:path"; import { extname, join, normalize, resolve } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
@ -7,8 +8,11 @@ import { createCommandBoardServer } from "../commandboard-api/dist/index.js";
const appDirectory = fileURLToPath(new URL(".", import.meta.url)); const appDirectory = fileURLToPath(new URL(".", import.meta.url));
const distDirectory = resolve(appDirectory, "dist"); const distDirectory = resolve(appDirectory, "dist");
const indexFile = join(distDirectory, "index.html"); const indexFile = join(distDirectory, "index.html");
loadLocalEnv(resolve(appDirectory, "../..", ".env"));
const apiServer = createCommandBoardServer(); const apiServer = createCommandBoardServer();
const port = Number(process.env.PORT ?? 4174); const port = Number(process.env.PORT ?? 4174);
const coinPayOAuthStateCookie = "logicsrc_coinpay_oauth_state";
const coinPaySessionCookie = "logicsrc_coinpay_session";
const mimeTypes = { const mimeTypes = {
".css": "text/css; charset=utf-8", ".css": "text/css; charset=utf-8",
@ -30,6 +34,31 @@ createServer((request, response) => {
return; return;
} }
if (url.pathname === "/api/oauth/coinpay/start") {
handleCoinPayOAuthStart(request, response);
return;
}
if (url.pathname === "/api/oauth/coinpay/callback") {
handleCoinPayOAuthCallback(request, response, url);
return;
}
if (url.pathname === "/api/oauth/coinpay/session") {
handleCoinPayOAuthSession(request, response);
return;
}
if (url.pathname === "/api/hire-us/project-request") {
handleHireUsProjectRequest(request, response);
return;
}
if (url.pathname === "/api/webhooks/coinpay") {
handleCoinPayWebhook(request, response);
return;
}
if (url.pathname === "/health" || url.pathname.startsWith("/api/")) { if (url.pathname === "/health" || url.pathname.startsWith("/api/")) {
apiServer.emit("request", request, response); apiServer.emit("request", request, response);
return; return;
@ -101,11 +130,14 @@ async function handleHireUsCoinPayCheckout(request, response) {
} }
const apiKey = process.env.COINPAY_API_KEY; const apiKey = process.env.COINPAY_API_KEY;
const merchantId = process.env.COINPAY_MERCHANT_ID; const eligibilityApiKey = process.env.COINPAY_ELIGIBILITY_API_KEY || process.env.COINPAY_AGENT_API_KEY || apiKey;
const businessId = process.env.COINPAY_BUSINESS_ID || process.env.COINPAY_MERCHANT_ID;
const eligibilityMerchantId = process.env.COINPAY_ELIGIBILITY_MERCHANT_ID || process.env.COINPAY_MERCHANT_ID;
const apiUrl = process.env.COINPAY_API_URL || "https://coinpayportal.com"; const apiUrl = process.env.COINPAY_API_URL || "https://coinpayportal.com";
const blockchain = process.env.COINPAY_HIRE_US_BLOCKCHAIN || "USDC_POL"; const blockchain = process.env.COINPAY_HIRE_US_BLOCKCHAIN || "USDC_POL";
const publicUrl = process.env.PUBLIC_URL || "https://logicsrc.com";
if (!apiKey) { if (!apiKey || !businessId) {
sendJson(response, 503, { success: false, error: "CoinPay checkout is not configured" }); sendJson(response, 503, { success: false, error: "CoinPay checkout is not configured" });
return; return;
} }
@ -113,6 +145,17 @@ async function handleHireUsCoinPayCheckout(request, response) {
try { try {
const body = await readJson(request); const body = await readJson(request);
const buyerEmail = typeof body.email === "string" ? body.email.trim().slice(0, 160) : ""; const buyerEmail = typeof body.email === "string" ? body.email.trim().slice(0, 160) : "";
const eligibility = await fetchMerchantEligibility(apiUrl, eligibilityApiKey, eligibilityMerchantId);
const paymentRail = choosePaymentRail(eligibility, blockchain);
if (!paymentRail) {
sendJson(response, 503, {
success: false,
error: "CoinPay checkout is not available for this merchant"
});
return;
}
const checkoutResponse = await fetch(new URL("/api/payments/create", apiUrl), { const checkoutResponse = await fetch(new URL("/api/payments/create", apiUrl), {
method: "POST", method: "POST",
headers: { headers: {
@ -120,24 +163,33 @@ async function handleHireUsCoinPayCheckout(request, response) {
"content-type": "application/json" "content-type": "application/json"
}, },
body: JSON.stringify({ body: JSON.stringify({
amount: 500, business_id: businessId,
currency: "USD", amount_usd: 250,
blockchain, payment_method: paymentRail.method,
description: "LogicSRC Hire Us - $500/week", currency: paymentRail.currency,
...(paymentRail.blockchain ? { blockchain: paymentRail.blockchain } : {}),
description: "LogicSRC Hire Us - $250/week",
success_url: `${publicUrl}/hire-us?payment=success`,
cancel_url: `${publicUrl}/hire-us?payment=cancelled`,
redirect_url: `${publicUrl}/hire-us?payment=coinpay`,
webhook_url: `${publicUrl}/api/webhooks/coinpay`,
metadata: { metadata: {
product: "logicsrc-hire-us", product: "logicsrc-hire-us",
interval: "week", interval: "week",
source: "logicsrc.com/hire-us", source: "logicsrc.com/hire-us",
...(merchantId ? { merchant_id: merchantId } : {}),
...(buyerEmail ? { buyer_email: buyerEmail } : {}) ...(buyerEmail ? { buyer_email: buyerEmail } : {})
}, }
redirect_url: `${process.env.PUBLIC_URL || "https://logicsrc.com"}/hire-us?payment=coinpay`
}) })
}); });
const payload = await checkoutResponse.json().catch(() => ({})); const responseText = await checkoutResponse.text();
const payload = parseJson(responseText);
if (!checkoutResponse.ok || !payload.success) { if (!checkoutResponse.ok || !payload.success) {
console.error("[coinpay] checkout create failed", {
status: checkoutResponse.status,
error: payload.error || responseText.slice(0, 300)
});
sendJson(response, checkoutResponse.ok ? 502 : checkoutResponse.status, { sendJson(response, checkoutResponse.ok ? 502 : checkoutResponse.status, {
success: false, success: false,
error: payload.error || "CoinPay checkout failed" error: payload.error || "CoinPay checkout failed"
@ -150,24 +202,464 @@ async function handleHireUsCoinPayCheckout(request, response) {
success: true, success: true,
payment: { payment: {
id: payment.id, id: payment.id,
amount_usd: Number(payment.amount_usd ?? payment.amount ?? 500), amount_usd: Number(payment.amount_usd ?? payment.amount ?? 250),
currency: payment.currency ?? payment.blockchain ?? blockchain, payment_method: payment.stripe_checkout_url ? "card" : paymentRail.method,
currency: payment.currency ?? payment.blockchain ?? paymentRail.blockchain ?? paymentRail.currency,
crypto_amount: payment.amount_crypto ?? payment.crypto_amount ?? null, crypto_amount: payment.amount_crypto ?? payment.crypto_amount ?? null,
address: payment.payment_address ?? null, address: payment.payment_address ?? null,
qr_code: payment.qr_code ?? null, qr_code: payment.qr_code ?? null,
expires_at: payment.expires_at ?? null, expires_at: payment.expires_at ?? null,
status: payment.status ?? "pending", status: payment.status ?? "pending",
checkout_url: payment.stripe_checkout_url ?? null checkout_url: payment.stripe_checkout_url ?? payload.checkout_url ?? payment.checkout_url ?? null
} }
}); });
} catch (error) { } catch (error) {
console.error("[coinpay] checkout request failed", error);
sendJson(response, 500, { sendJson(response, 500, {
success: false, success: false,
error: error instanceof Error ? error.message : "Unable to create CoinPay checkout" error: "Unable to reach CoinPay checkout"
}); });
} }
} }
function handleCoinPayOAuthStart(request, response) {
if (request.method !== "GET") {
sendJson(response, 405, { success: false, error: "Method not allowed" }, { allow: "GET" });
return;
}
const config = getCoinPayOAuthConfig();
if (!config) {
sendJson(response, 503, { success: false, error: "CoinPay OAuth is not configured" });
return;
}
const state = randomBytes(16).toString("hex");
const authorizeUrl = new URL("/api/oauth/authorize", config.issuer);
authorizeUrl.searchParams.set("response_type", "code");
authorizeUrl.searchParams.set("client_id", config.clientId);
authorizeUrl.searchParams.set("redirect_uri", config.redirectUri);
authorizeUrl.searchParams.set("scope", config.scopes);
authorizeUrl.searchParams.set("state", state);
response.writeHead(302, {
location: authorizeUrl.toString(),
"set-cookie": serializeCookie(coinPayOAuthStateCookie, state, {
maxAge: 600,
path: "/api/oauth/coinpay"
})
});
response.end();
}
async function handleCoinPayOAuthCallback(request, response, url) {
if (request.method !== "GET") {
sendJson(response, 405, { success: false, error: "Method not allowed" }, { allow: "GET" });
return;
}
const config = getCoinPayOAuthConfig();
if (!config) {
sendJson(response, 503, { success: false, error: "CoinPay OAuth is not configured" });
return;
}
const callbackError = url.searchParams.get("error");
if (callbackError) {
redirectWithOAuthStatus(response, "error", callbackError);
return;
}
const code = url.searchParams.get("code");
const state = url.searchParams.get("state");
const expectedState = parseCookies(request.headers.cookie)[coinPayOAuthStateCookie];
if (!code) {
redirectWithOAuthStatus(response, "error", "missing_code");
return;
}
if (!state || !expectedState || state !== expectedState) {
redirectWithOAuthStatus(response, "error", "invalid_state");
return;
}
try {
const tokenResponse = await fetch(new URL("/api/oauth/token", config.issuer), {
method: "POST",
headers: { "content-type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: config.redirectUri,
client_id: config.clientId,
client_secret: config.clientSecret
})
});
const tokenText = await tokenResponse.text();
const tokenPayload = parseJson(tokenText);
if (!tokenResponse.ok || typeof tokenPayload.access_token !== "string") {
console.error("[coinpay-oauth] token exchange failed", {
status: tokenResponse.status,
error: tokenPayload.error || tokenText.slice(0, 160)
});
redirectWithOAuthStatus(response, "error", "token_exchange_failed");
return;
}
const userResponse = await fetch(new URL("/api/oauth/userinfo", config.issuer), {
headers: { authorization: `Bearer ${tokenPayload.access_token}` }
});
const userText = await userResponse.text();
const userInfo = userResponse.ok ? parseJson(userText) : {};
const session = signSession({
provider: "coinpay",
sub: typeof userInfo.sub === "string" ? userInfo.sub : null,
email: typeof userInfo.email === "string" ? userInfo.email : null,
name: typeof userInfo.name === "string" ? userInfo.name : null,
scope: typeof tokenPayload.scope === "string" ? tokenPayload.scope : config.scopes,
connected_at: new Date().toISOString()
});
response.writeHead(302, {
location: "/?coinpay_oauth=connected",
"set-cookie": [
serializeCookie(coinPaySessionCookie, session, { maxAge: 60 * 60 * 24 * 30, path: "/" }),
serializeCookie(coinPayOAuthStateCookie, "", { maxAge: 0, path: "/api/oauth/coinpay" })
]
});
response.end();
} catch (error) {
console.error("[coinpay-oauth] callback failed", error);
redirectWithOAuthStatus(response, "error", "callback_failed");
}
}
function handleCoinPayOAuthSession(request, response) {
if (request.method !== "GET") {
sendJson(response, 405, { success: false, error: "Method not allowed" }, { allow: "GET" });
return;
}
const sessionCookie = parseCookies(request.headers.cookie)[coinPaySessionCookie];
const session = sessionCookie ? verifySession(sessionCookie) : null;
sendJson(response, 200, {
authenticated: !!session,
user: session
? {
provider: session.provider,
sub: session.sub,
email: session.email,
name: session.name,
scope: session.scope,
connected_at: session.connected_at
}
: null
});
}
async function handleHireUsProjectRequest(request, response) {
if (request.method !== "POST") {
sendJson(response, 405, { success: false, error: "Method not allowed" }, { allow: "POST" });
return;
}
try {
const body = await readJson(request);
const contact = typeof body.contact === "string" ? body.contact.trim().slice(0, 160) : "";
const project = typeof body.project === "string" ? body.project.trim().slice(0, 4000) : "";
if (!contact || project.length < 20) {
sendJson(response, 422, {
success: false,
error: "Contact and a project description are required"
});
return;
}
const requestId = `hire_${Date.now()}`;
console.log("[hire-us] project request received", {
id: requestId,
contact,
project_length: project.length,
plan: "250/week",
invoice: "pending_acceptance"
});
sendJson(response, 202, {
success: true,
request: {
id: requestId,
status: "pending_acceptance",
amount_usd: 250,
interval: "week",
invoice: "created_after_acceptance"
}
});
} catch (error) {
console.error("[hire-us] project request failed", error);
sendJson(response, 500, { success: false, error: "Unable to submit project request" });
}
}
async function handleCoinPayWebhook(request, response) {
if (request.method !== "POST") {
sendJson(response, 405, { success: false, error: "Method not allowed" }, { allow: "POST" });
return;
}
const webhookSecret = process.env.COINPAY_WEBHOOK_SECRET;
if (!webhookSecret) {
sendJson(response, 503, { success: false, error: "CoinPay webhook is not configured" });
return;
}
const rawBody = await readText(request);
const signature = request.headers["x-coinpay-signature"];
const signatureHeader = Array.isArray(signature) ? signature[0] : signature;
if (!verifyCoinPayWebhook(rawBody, signatureHeader, webhookSecret)) {
sendJson(response, 401, { success: false, error: "Invalid signature" });
return;
}
const payload = parseJson(rawBody);
const paymentId = payload?.data?.payment_id ?? payload?.payment_id ?? null;
const complete = payload?.type === "payment.confirmed" || payload?.type === "payment.forwarded";
console.log("[coinpay] webhook received", {
type: payload?.type ?? null,
payment_id: paymentId,
complete
});
sendJson(response, 200, {
received: true,
complete,
payment_id: paymentId
});
}
async function fetchMerchantEligibility(apiUrl, apiKey, merchantId) {
if (!merchantId) {
return null;
}
try {
const url = new URL("/api/payments/merchant-eligibility", apiUrl);
url.searchParams.set("merchant_id", merchantId);
const response = await fetch(url, {
headers: { authorization: `Bearer ${apiKey}` }
});
const text = await response.text();
const payload = parseJson(text);
if (!response.ok || !payload.success) {
console.warn("[coinpay] merchant eligibility unavailable", {
status: response.status,
error: payload.error || text.slice(0, 120)
});
return null;
}
return {
accepts_card: payload.accepts_card === true,
accepts_crypto: payload.accepts_crypto === true,
chains: Array.isArray(payload.chains) ? payload.chains.filter((chain) => typeof chain === "string") : []
};
} catch (error) {
console.warn("[coinpay] merchant eligibility request failed", error);
return null;
}
}
function choosePaymentRail(eligibility, configuredBlockchain) {
const cryptoCurrency = configuredBlockchain.toLowerCase();
if (!eligibility) {
return { method: "both", currency: cryptoCurrency, blockchain: configuredBlockchain };
}
const configuredChainAvailable = eligibility.chains
.map((chain) => chain.toUpperCase())
.includes(configuredBlockchain.toUpperCase());
const acceptsConfiguredCrypto = eligibility.accepts_crypto && configuredChainAvailable;
if (eligibility.accepts_card && acceptsConfiguredCrypto) {
return { method: "both", currency: cryptoCurrency, blockchain: configuredBlockchain };
}
if (eligibility.accepts_card) {
return { method: "card", currency: "card", blockchain: null };
}
if (acceptsConfiguredCrypto) {
return { method: "crypto", currency: cryptoCurrency, blockchain: configuredBlockchain };
}
return null;
}
function verifyCoinPayWebhook(rawBody, signatureHeader, secret) {
if (!signatureHeader) {
return false;
}
try {
const parts = signatureHeader.split(",");
const timestamp = parts.find((part) => part.startsWith("t="))?.slice(2);
const signature = parts.find((part) => part.startsWith("v1="))?.slice(3);
if (!timestamp || !signature) {
return false;
}
const timestampSeconds = Number.parseInt(timestamp, 10);
if (!Number.isFinite(timestampSeconds) || Math.abs(Math.floor(Date.now() / 1000) - timestampSeconds) > 300) {
return false;
}
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
const actualBuffer = Buffer.from(signature, "hex");
const expectedBuffer = Buffer.from(expected, "hex");
return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
} catch {
return false;
}
}
function getCoinPayOAuthConfig() {
const clientId = process.env.COINPAY_OAUTH_CLIENT_ID;
const clientSecret = process.env.COINPAY_OAUTH_CLIENT_SECRET;
const redirectUri = process.env.COINPAY_OAUTH_REDIRECT_URI;
const issuer = process.env.COINPAY_OAUTH_ISSUER || process.env.COINPAY_API_URL || "https://coinpayportal.com";
const scopes = process.env.COINPAY_OAUTH_SCOPES || "openid profile email";
if (!clientId || !clientSecret || !redirectUri) {
return null;
}
return { clientId, clientSecret, redirectUri, issuer, scopes };
}
function redirectWithOAuthStatus(response, status, error) {
const location = new URL("/", process.env.PUBLIC_URL || "https://logicsrc.com");
location.searchParams.set("coinpay_oauth", status);
if (error) {
location.searchParams.set("error", error);
}
response.writeHead(302, { location: `${location.pathname}${location.search}` });
response.end();
}
function serializeCookie(name, value, options = {}) {
const parts = [
`${name}=${encodeURIComponent(value)}`,
"HttpOnly",
"SameSite=Lax",
`Path=${options.path || "/"}`
];
if (typeof options.maxAge === "number") {
parts.push(`Max-Age=${options.maxAge}`);
}
if ((process.env.PUBLIC_URL || "").startsWith("https://")) {
parts.push("Secure");
}
return parts.join("; ");
}
function parseCookies(header) {
if (!header) return {};
return Object.fromEntries(
header
.split(";")
.map((cookie) => cookie.trim())
.filter(Boolean)
.map((cookie) => {
const separator = cookie.indexOf("=");
if (separator === -1) return [cookie, ""];
return [
cookie.slice(0, separator),
decodeURIComponent(cookie.slice(separator + 1))
];
})
);
}
function signSession(payload) {
const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
const signature = createHmac("sha256", getSessionSecret()).update(encoded).digest("base64url");
return `${encoded}.${signature}`;
}
function verifySession(value) {
const [encoded, signature] = value.split(".");
if (!encoded || !signature) return null;
const expected = createHmac("sha256", getSessionSecret()).update(encoded).digest("base64url");
const actualBuffer = Buffer.from(signature);
const expectedBuffer = Buffer.from(expected);
if (actualBuffer.length !== expectedBuffer.length || !timingSafeEqual(actualBuffer, expectedBuffer)) {
return null;
}
try {
return JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
} catch {
return null;
}
}
function getSessionSecret() {
return process.env.LOGICSRC_SESSION_SECRET || process.env.COINPAY_OAUTH_CLIENT_SECRET || "logicsrc-dev-session-secret";
}
function loadLocalEnv(file) {
if (!existsSync(file)) return;
for (const line of readFileSync(file, "utf8").split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const separator = trimmed.indexOf("=");
if (separator === -1) continue;
const key = trimmed.slice(0, separator).trim();
const rawValue = trimmed.slice(separator + 1).trim();
if (!key || process.env[key] !== undefined) continue;
process.env[key] = rawValue.replace(/^(['"])(.*)\1$/, "$2");
}
}
function parseJson(text) {
try {
return text ? JSON.parse(text) : {};
} catch {
return {};
}
}
function readText(request) {
return new Promise((resolve, reject) => {
let body = "";
request.setEncoding("utf8");
request.on("data", (chunk) => {
body += chunk;
if (body.length > 65536) {
reject(new Error("Request body too large"));
request.destroy();
}
});
request.on("end", () => resolve(body));
request.on("error", reject);
});
}
function readJson(request) { function readJson(request) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let body = ""; let body = "";

View file

@ -64,7 +64,7 @@ const pages = [
{ id: "blog", title: "Blog", detail: "Project notes for LogicSRC, AgentSwarm, AgentByte, OpenSpec workflows, and reference implementations." }, { id: "blog", title: "Blog", detail: "Project notes for LogicSRC, AgentSwarm, AgentByte, OpenSpec workflows, and reference implementations." },
{ id: "openspec", title: "OpenSpec", detail: "Comparison and compatibility notes for OpenSpec.dev-style repo-local specs, proposals, tasks, and deltas." }, { id: "openspec", title: "OpenSpec", detail: "Comparison and compatibility notes for OpenSpec.dev-style repo-local specs, proposals, tasks, and deltas." },
{ id: "credential-sharing", title: "Credential Sharing", detail: "Open replacement architecture for portable secret sync across .env, Doppler, Railway variables, GitHub Secrets, and future providers." }, { id: "credential-sharing", title: "Credential Sharing", detail: "Open replacement architecture for portable secret sync across .env, Doppler, Railway variables, GitHub Secrets, and future providers." },
{ id: "hire-us", title: "Hire Us", detail: "$500/week LogicSRC work on open infrastructure, specs, AI agent workflows, and reference implementations paid through CoinPay." }, { id: "hire-us", title: "Hire Us", detail: "$250/week LogicSRC work on open infrastructure, specs, AI agent workflows, and reference implementations paid through CoinPay after project acceptance." },
{ id: "about", title: "About", detail: "LogicSRC is the Profullstack open specification project for human and AI agent coordination." }, { id: "about", title: "About", detail: "LogicSRC is the Profullstack open specification project for human and AI agent coordination." },
{ id: "terms", title: "Terms", detail: "Draft terms will cover acceptable use, reference implementation boundaries, and hosted-product responsibilities." }, { id: "terms", title: "Terms", detail: "Draft terms will cover acceptable use, reference implementation boundaries, and hosted-product responsibilities." },
{ id: "privacy", title: "Privacy", detail: "Draft privacy notes will cover telemetry, audit events, identity data, and hosted-product data boundaries." } { id: "privacy", title: "Privacy", detail: "Draft privacy notes will cover telemetry, audit events, identity data, and hosted-product data boundaries." }
@ -136,6 +136,9 @@ document.querySelector<HTMLDivElement>("#app")!.innerHTML = `
<p class="eyebrow">Profullstack open spec project</p> <p class="eyebrow">Profullstack open spec project</p>
<h1>LogicSRC</h1> <h1>LogicSRC</h1>
<p class="lede">Open schemas, primitives, and conventions for coordination between humans, AI agents, plugins, payment systems, and hosted products.</p> <p class="lede">Open schemas, primitives, and conventions for coordination between humans, AI agents, plugins, payment systems, and hosted products.</p>
<div class="hero-actions">
<a class="button-primary" href="/api/oauth/coinpay/start">Connect CoinPay</a>
</div>
</div> </div>
<div class="status-grid" aria-label="Project status"> <div class="status-grid" aria-label="Project status">
<span><strong>0.1</strong>draft spec</span> <span><strong>0.1</strong>draft spec</span>
@ -319,7 +322,7 @@ logicsrc credentials plan --from doppler --to github-secrets</code></pre>
<section id="hire-us" class="band hire-us"> <section id="hire-us" class="band hire-us">
<div class="section-head"> <div class="section-head">
<h2>Hire Us</h2> <h2>Hire Us</h2>
<p>$500/week for LogicSRC work using open infrastructure and open specs for AI agent systems.</p> <p>$250/week for accepted LogicSRC work using open infrastructure and open specs for AI agent systems.</p>
</div> </div>
<div class="hire-layout"> <div class="hire-layout">
<article class="hire-panel"> <article class="hire-panel">
@ -327,14 +330,24 @@ logicsrc credentials plan --from doppler --to github-secrets</code></pre>
<h3>Open-spec AI agent implementation help</h3> <h3>Open-spec AI agent implementation help</h3>
<p>Hire us to turn agent ideas into portable LogicSRC specs, CLIs, SDKs, MCP resources, PWAs, APIs, and provider-neutral plugin workflows. We prioritize auditable contracts, repo-local artifacts, and integrations that can move between model providers and infrastructure.</p> <p>Hire us to turn agent ideas into portable LogicSRC specs, CLIs, SDKs, MCP resources, PWAs, APIs, and provider-neutral plugin workflows. We prioritize auditable contracts, repo-local artifacts, and integrations that can move between model providers and infrastructure.</p>
<div class="price-row"> <div class="price-row">
<strong>$500</strong> <strong>$250</strong>
<span>per week</span> <span>per week</span>
</div> </div>
<div class="cta-row"> <form id="project-request-form" class="project-request-form">
<button id="coinpay-checkout-button" class="button-primary" type="button">Pay with CoinPay</button> <label>
<a class="button-secondary" href="/docs">Read specs</a> <span>Contact</span>
</div> <input id="project-contact" name="contact" type="email" autocomplete="email" placeholder="you@example.com" required />
<div id="coinpay-result" class="coinpay-result" aria-live="polite"></div> </label>
<label>
<span>Project</span>
<textarea id="project-description" name="project" rows="6" minlength="20" placeholder="Describe the agent workflow, spec, CLI, plugin, API, or integration you want help with." required></textarea>
</label>
<div class="cta-row">
<button id="project-request-button" class="button-primary" type="submit">Request review</button>
<a class="button-secondary" href="/docs">Read specs</a>
</div>
</form>
<div id="project-request-result" class="coinpay-result" aria-live="polite"></div>
</article> </article>
<div class="hire-stack"> <div class="hire-stack">
<div class="hire-grid"> <div class="hire-grid">
@ -346,12 +359,13 @@ logicsrc credentials plan --from doppler --to github-secrets</code></pre>
`).join("")} `).join("")}
</div> </div>
<article id="coinpay-setup" class="coinpay-panel"> <article id="coinpay-setup" class="coinpay-panel">
<h3>CoinPay checkout hook</h3> <h3>CoinPay recurring invoice</h3>
<p>The primary CTA creates a CoinPay payment request for the weekly plan without exposing merchant credentials to the browser.</p> <p>After we accept the project, we create a recurring CoinPay invoice for the weekly plan without exposing merchant credentials to the browser.</p>
<pre><code>COINPAY_ORG=profullstack <pre><code>COINPAY_ORG=profullstack
COINPAY_PRODUCT=logicsrc-hire-us COINPAY_PRODUCT=logicsrc-hire-us
COINPAY_AMOUNT_USD=500 COINPAY_AMOUNT_USD=250
COINPAY_INTERVAL=week</code></pre> COINPAY_INTERVAL=week
COINPAY_STATUS=pending_acceptance</code></pre>
</article> </article>
</div> </div>
</div> </div>
@ -384,41 +398,38 @@ if ("serviceWorker" in navigator) {
}); });
} }
document.querySelector<HTMLButtonElement>("#coinpay-checkout-button")?.addEventListener("click", async () => { document.querySelector<HTMLFormElement>("#project-request-form")?.addEventListener("submit", async (event) => {
const button = document.querySelector<HTMLButtonElement>("#coinpay-checkout-button"); event.preventDefault();
const result = document.querySelector<HTMLDivElement>("#coinpay-result"); const button = document.querySelector<HTMLButtonElement>("#project-request-button");
if (!button || !result) return; const result = document.querySelector<HTMLDivElement>("#project-request-result");
const contact = document.querySelector<HTMLInputElement>("#project-contact");
const project = document.querySelector<HTMLTextAreaElement>("#project-description");
if (!button || !result || !contact || !project) return;
button.disabled = true; button.disabled = true;
button.textContent = "Creating payment..."; button.textContent = "Submitting...";
result.replaceChildren(buildParagraph("Creating CoinPay payment request.")); result.replaceChildren(buildParagraph("Submitting project request."));
try { try {
const response = await fetch("/api/hire-us/coinpay-checkout", { const response = await fetch("/api/hire-us/project-request", {
method: "POST", method: "POST",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
body: JSON.stringify({}) body: JSON.stringify({ contact: contact.value, project: project.value })
}); });
const payload = await response.json(); const payload = await response.json();
if (!response.ok || !payload.success) { if (!response.ok || !payload.success) {
throw new Error(payload.error || "CoinPay payment could not be created."); throw new Error(payload.error || "Project request could not be submitted.");
} }
const payment = payload.payment; result.replaceChildren(buildParagraph("Request received. If it is a fit, we will send a $250/week recurring CoinPay invoice."));
if (payment.checkout_url) {
window.location.href = payment.checkout_url;
return;
}
result.replaceChildren(buildCoinPayResult(payment));
} catch (error) { } catch (error) {
result.replaceChildren( result.replaceChildren(
buildParagraph(error instanceof Error ? error.message : "CoinPay payment could not be created.") buildParagraph(error instanceof Error ? error.message : "Project request could not be submitted.")
); );
} finally { } finally {
button.disabled = false; button.disabled = false;
button.textContent = "Pay with CoinPay"; button.textContent = "Request review";
} }
}); });
@ -437,7 +448,7 @@ function buildCoinPayResult(payment: {
const details = document.createElement("dl"); const details = document.createElement("dl");
details.append( details.append(
buildDetail("Amount", `$${payment.amount_usd ?? 500} / ${payment.crypto_amount ?? "quoted at checkout"} ${payment.currency ?? "USDC_POL"}`), buildDetail("Amount", `$${payment.amount_usd ?? 250} / ${payment.crypto_amount ?? "quoted at checkout"} ${payment.currency ?? "USDC_POL"}`),
buildDetail("Address", payment.address ?? "Open CoinPay to complete payment", true), buildDetail("Address", payment.address ?? "Open CoinPay to complete payment", true),
buildDetail("Payment ID", payment.id ?? "pending", true) buildDetail("Payment ID", payment.id ?? "pending", true)
); );

View file

@ -139,6 +139,13 @@ h1 {
line-height: 1.55; line-height: 1.55;
} }
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 0.65rem;
margin-top: 1rem;
}
.status-grid { .status-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
@ -477,6 +484,45 @@ pre {
gap: 0.65rem; gap: 0.65rem;
} }
.project-request-form {
display: grid;
gap: 0.75rem;
margin-top: 1rem;
}
.project-request-form label {
display: grid;
gap: 0.35rem;
}
.project-request-form label span {
color: #8ee4c9;
font-size: 0.78rem;
font-weight: 800;
text-transform: uppercase;
}
.project-request-form input,
.project-request-form textarea {
width: 100%;
border: 1px solid #3f5049;
border-radius: 6px;
background: #171d1a;
color: #f6f7f4;
font: inherit;
line-height: 1.45;
padding: 0.7rem 0.75rem;
}
.project-request-form textarea {
resize: vertical;
}
.project-request-form input::placeholder,
.project-request-form textarea::placeholder {
color: #7f8b84;
}
.button-primary, .button-primary,
.button-secondary { .button-secondary {
display: inline-flex; display: inline-flex;

View file

@ -6,6 +6,7 @@ Default plugins:
- CoinPay: DID auth, wallet, payment, escrow, refunds, tips, payment webhooks, and payment reputation. - CoinPay: DID auth, wallet, payment, escrow, refunds, tips, payment webhooks, and payment reputation.
- uGig: job import, gig publishing, candidate/agent linking, bid sync, marketplace publishing, and reputation sync. - uGig: job import, gig publishing, candidate/agent linking, bid sync, marketplace publishing, and reputation sync.
- c0mpute: compute job dispatch, worker pool sync, usage reporting, quote creation, settlement status, and compute reputation events.
- Credential Sharing: provider-neutral secret sync plans, approvals, rollbacks, and audit events. - Credential Sharing: provider-neutral secret sync plans, approvals, rollbacks, and audit events.
Coming soon plugin specs: Coming soon plugin specs:

15
package-lock.json generated
View file

@ -23,6 +23,7 @@
"name": "@logicsrc/commandboard-api", "name": "@logicsrc/commandboard-api",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@logicsrc/plugin-c0mpute": "file:../../plugins/c0mpute",
"@logicsrc/plugin-coinpay": "file:../../plugins/coinpay", "@logicsrc/plugin-coinpay": "file:../../plugins/coinpay",
"@logicsrc/plugin-core": "file:../../packages/plugin-core", "@logicsrc/plugin-core": "file:../../packages/plugin-core",
"@logicsrc/plugin-sh1pt": "file:../../plugins/sh1pt", "@logicsrc/plugin-sh1pt": "file:../../plugins/sh1pt",
@ -987,6 +988,10 @@
"resolved": "apps/commandboard-web", "resolved": "apps/commandboard-web",
"link": true "link": true
}, },
"node_modules/@logicsrc/plugin-c0mpute": {
"resolved": "plugins/c0mpute",
"link": true
},
"node_modules/@logicsrc/plugin-coinpay": { "node_modules/@logicsrc/plugin-coinpay": {
"resolved": "plugins/coinpay", "resolved": "plugins/coinpay",
"link": true "link": true
@ -4728,6 +4733,16 @@
"vitest": "^4.0.8" "vitest": "^4.0.8"
} }
}, },
"plugins/c0mpute": {
"name": "@logicsrc/plugin-c0mpute",
"version": "0.1.0",
"dependencies": {
"@logicsrc/plugin-core": "file:../../packages/plugin-core"
},
"devDependencies": {
"vitest": "^4.0.8"
}
},
"plugins/coinpay": { "plugins/coinpay": {
"name": "@logicsrc/plugin-coinpay", "name": "@logicsrc/plugin-coinpay",
"version": "0.1.0", "version": "0.1.0",

View file

@ -12,7 +12,7 @@
"apps/*" "apps/*"
], ],
"scripts": { "scripts": {
"build": "npm --workspace @logicsrc/schemas run build && npm --workspace @logicsrc/validators run build && npm --workspace @logicsrc/sdk run build && npm --workspace @logicsrc/plugin-core run build && npm --workspace @logicsrc/plugin-coinpay run build && npm --workspace @logicsrc/plugin-ugig run build && npm --workspace @logicsrc/plugin-sh1pt run build && npm --workspace @logicsrc/tui run build && npm --workspace @logicsrc/cli run build && npm --workspace @profullstack/logicsrc-mcp run build && npm --workspace @logicsrc/commandboard-api run build && npm --workspace @logicsrc/commandboard-web run build && npm --workspace @logicsrc/web run build", "build": "npm --workspace @logicsrc/schemas run build && npm --workspace @logicsrc/validators run build && npm --workspace @logicsrc/sdk run build && npm --workspace @logicsrc/plugin-core run build && npm --workspace @logicsrc/plugin-coinpay run build && npm --workspace @logicsrc/plugin-ugig run build && npm --workspace @logicsrc/plugin-sh1pt run build && npm --workspace @logicsrc/plugin-c0mpute run build && npm --workspace @logicsrc/tui run build && npm --workspace @logicsrc/cli run build && npm --workspace @profullstack/logicsrc-mcp run build && npm --workspace @logicsrc/commandboard-api run build && npm --workspace @logicsrc/commandboard-web run build && npm --workspace @logicsrc/web run build",
"start": "npm --workspace @logicsrc/web run start", "start": "npm --workspace @logicsrc/web run start",
"test": "npm run test --workspaces --if-present", "test": "npm run test --workspaces --if-present",
"check": "npm run build && npm run test", "check": "npm run build && npm run test",

11
plugins/c0mpute/README.md Normal file
View file

@ -0,0 +1,11 @@
# c0mpute Plugin
c0mpute is a work-in-progress CommandBoard.run plugin for compute job dispatch, worker pool sync, usage reporting, quote creation, settlement status, and compute-backed reputation events.
Environment variables:
```txt
C0MPUTE_API_URL
C0MPUTE_API_KEY
C0MPUTE_WEBHOOK_SECRET
```

View file

@ -0,0 +1,18 @@
{
"name": "@logicsrc/plugin-c0mpute",
"version": "0.1.0",
"description": "CommandBoard.run compute jobs, worker pools, usage, and settlement plugin for c0mpute.",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "vitest run src --passWithNoTests"
},
"dependencies": {
"@logicsrc/plugin-core": "file:../../packages/plugin-core"
},
"devDependencies": {
"vitest": "^4.0.8"
}
}

View file

@ -0,0 +1,40 @@
import type { PluginDefinition } from "@logicsrc/plugin-core";
import { c0mputeManifest } from "./manifest.js";
export const c0mputePlugin: PluginDefinition = {
manifest: c0mputeManifest,
configDefaults: {
enabled: true,
work_in_progress: true,
default_compute_provider: true,
api_url: "${C0MPUTE_API_URL}",
api_key: "${C0MPUTE_API_KEY}",
webhook_secret: "${C0MPUTE_WEBHOOK_SECRET}",
default_board: "/projects/c0mpute"
},
routes: [
{ method: "GET", path: "/api/plugins/c0mpute/jobs", capability: "compute.jobs.sync" },
{ method: "POST", path: "/api/plugins/c0mpute/jobs/dispatch", capability: "compute.jobs.dispatch" },
{ method: "GET", path: "/api/plugins/c0mpute/workers", capability: "compute.workers.sync" },
{ method: "POST", path: "/api/plugins/c0mpute/quotes", capability: "compute.quotes.create" },
{ method: "POST", path: "/api/plugins/c0mpute/webhooks/compute-status", capability: "webhook.compute_status" }
],
events: [
{ event: "task.created", capability: "compute.quotes.create" },
{ event: "run.requested", capability: "compute.jobs.dispatch" },
{ event: "usage.reported", capability: "compute.usage.report" },
{ event: "settlement.completed", capability: "reputation.compute_event" }
],
permissions: [
"compute:jobs:read",
"compute:jobs:dispatch",
"compute:workers:read",
"compute:quotes:create",
"compute:usage:report",
"compute:settlements:read",
"reputation:sync"
],
tuiPanels: [{ id: "c0mpute-status", title: "c0mpute Jobs" }]
};
export { c0mputeManifest };

View file

@ -0,0 +1,21 @@
import type { PluginManifest } from "@logicsrc/plugin-core";
export const c0mputeManifest: PluginManifest = {
id: "c0mpute",
name: "c0mpute",
version: "0.1.0",
type: ["compute", "jobs", "workers", "usage", "settlement"],
default: true,
capabilities: [
"compute.jobs.sync",
"compute.jobs.dispatch",
"compute.workers.sync",
"compute.quotes.create",
"compute.usage.report",
"compute.settlements.status",
"webhook.compute_status",
"reputation.compute_event"
],
commands: ["c0mpute", "compute", "workers", "quotes"],
env: ["C0MPUTE_API_URL", "C0MPUTE_API_KEY", "C0MPUTE_WEBHOOK_SECRET"]
};

View file

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}