Wire zappier-edge into the live stack and add billing department APIs
Some checks are pending
offline / test (push) Waiting to run

Fleet now spawns the real zappier and middleware processes. Metered
timestamp/receipt/hash calls proxy to middleware when ZAPPIER_UPSTREAM
is set. CS credits, sales per-customer pricing, and QuickBooks export
are separate repos plugged into zappier-edge admin.
This commit is contained in:
George Lambert 2026-09-11 15:15:55 -04:00
parent 65bfa544b2
commit a1a5b957fd
44 changed files with 822 additions and 18 deletions

View file

@ -15,6 +15,9 @@
"services": {
"nats": { "min": 1, "max": 1, "keepFloor": false, "enabled": true, "managed": false },
"zappier-edge": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"customer-service": { "min": 0, "max": 1, "keepFloor": false, "enabled": true },
"sales-pricing": { "min": 0, "max": 1, "keepFloor": false, "enabled": true },
"accounting-export": { "min": 0, "max": 1, "keepFloor": false, "enabled": true },
"middleware-http": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"job-poller": { "min": 1, "max": 2, "keepFloor": true, "enabled": true },
"webhook-deliver": { "min": 1, "max": 2, "keepFloor": true, "enabled": true },

View file

@ -0,0 +1,21 @@
{
"id": "accounting-export",
"title": "Accounting export (QuickBooks)",
"kind": "http",
"package": "zappier-accounting-export",
"role": "accounting-export",
"managed": true,
"runtime": "HTTP :3013 IIF/CSV → zappier-edge invoices",
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
"ports": { "healthBase": 3013 },
"spawn": {
"cwd": "../zappier-accounting-export",
"command": "node",
"args": ["src/server.js"]
},
"env": {
"PORT": "3013",
"ZAPPIER_ADMIN_URL": "http://127.0.0.1:3000"
},
"nats": { "in": [], "out": [] }
}

View file

@ -0,0 +1,22 @@
{
"id": "customer-service",
"title": "Customer-service credits",
"kind": "http",
"package": "zappier-customer-service",
"role": "customer-service",
"managed": true,
"runtime": "HTTP :3011 CS credits → zappier-edge",
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
"ports": { "healthBase": 3011 },
"spawn": {
"cwd": "../zappier-customer-service",
"command": "node",
"args": ["src/server.js"]
},
"env": {
"PORT": "3011",
"ZAPPIER_ADMIN_URL": "http://127.0.0.1:3000"
},
"nats": { "in": [], "out": [] },
"notes": "Department API. Prepaid balances live on zappier-edge."
}

View file

@ -6,9 +6,19 @@
"role": "middleware-http",
"managed": true,
"runtime": "HTTPS :3100 /zapier/v1",
"health": { "type": "http", "path": "/health", "timeoutMs": 800 },
"ports": { "healthBase": 13100 },
"env": { "PORT": "3100", "NATS_URL": "nats://127.0.0.1:4222" },
"health": { "type": "http", "path": "/health", "timeoutMs": 4000 },
"ports": { "healthBase": 3100 },
"spawn": {
"cwd": "../verae-zapier-middleware",
"command": "node",
"args": ["src/index.js"]
},
"env": {
"PORT": "3100",
"NATS_URL": "nats://127.0.0.1:4222",
"NATS_ENABLED": "true",
"MOCK_VERAE": "true"
},
"nats": {
"in": ["verae.zapier.jobs.events"],
"out": ["verae.zapier.jobs.watch", "verae.zapier.webhooks.deliver"]

View file

@ -0,0 +1,21 @@
{
"id": "sales-pricing",
"title": "Sales per-customer pricing",
"kind": "http",
"package": "zappier-sales-pricing",
"role": "sales-pricing",
"managed": true,
"runtime": "HTTP :3012 quotes → zappier-edge",
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
"ports": { "healthBase": 3012 },
"spawn": {
"cwd": "../zappier-sales-pricing",
"command": "node",
"args": ["src/server.js"]
},
"env": {
"PORT": "3012",
"ZAPPIER_ADMIN_URL": "http://127.0.0.1:3000"
},
"nats": { "in": [], "out": [] }
}

View file

@ -5,10 +5,21 @@
"package": "zappier",
"role": "zappier-edge",
"managed": true,
"runtime": "HTTPS :3000 (portal, admin, meter)",
"health": { "type": "http", "path": "/health", "timeoutMs": 800 },
"ports": { "healthBase": 13000 },
"env": { "PORT": "3000" },
"runtime": "HTTPS :3000 (portal, admin, meter, proxy to middleware)",
"health": { "type": "http", "path": "/health", "timeoutMs": 4000 },
"ports": { "healthBase": 3000 },
"spawn": {
"cwd": "../zappier",
"command": "npx",
"args": ["ts-node", "--transpile-only", "src/index.ts"]
},
"env": {
"PORT": "3000",
"ZAPPIER_UPSTREAM": "http://127.0.0.1:3100",
"CS_SERVICE_URL": "http://127.0.0.1:3011",
"SALES_PRICING_URL": "http://127.0.0.1:3012",
"ACCOUNTING_EXPORT_URL": "http://127.0.0.1:3013"
},
"nats": { "in": [], "out": [] },
"notes": "Zapier x-api-key lands here. HTTPS to middleware only."
}

View file

@ -16,6 +16,23 @@ import { sshSpawnWorker, sshKillWorker, sshHttp, sshCheck } from './ssh.js';
const WORKER = path.join(path.dirname(fileURLToPath(import.meta.url)), 'worker.js');
/**
* Real HTTP apps (zappier-edge, middleware) set `spawn` in services/*.json.
* Everything else is the fleet health worker.
*/
export function spawnLocal(root, spec, env, healthPort) {
const merged = { ...env, ...(spec.env || {}), PORT: String(healthPort) };
if (spec.spawn?.command) {
const cwd = path.resolve(root, spec.spawn.cwd || '.');
return spawn(spec.spawn.command, spec.spawn.args || [], {
env: merged,
cwd,
stdio: ['ignore', 'pipe', 'pipe'],
});
}
return spawn(process.execPath, [WORKER], { env: merged, stdio: ['ignore', 'pipe', 'pipe'] });
}
export class Supervisor {
/**
* @param {object} [opts]
@ -184,7 +201,7 @@ export class Supervisor {
return null;
}
} else {
child = spawn(process.execPath, [WORKER], { env, stdio: ['ignore', 'pipe', 'pipe'] });
child = spawnLocal(this.loaded.root, spec, env, healthPort);
pid = child.pid;
}
const rec = {

View file

@ -1,7 +1,7 @@
import { describe, it, after } from 'node:test';
import assert from 'node:assert/strict';
import { loadFleet, listServices, FLEET_ROOT } from '../src/load.js';
import { Supervisor } from '../src/supervisor.js';
import { Supervisor, spawnLocal } from '../src/supervisor.js';
import { Monitor } from '../src/monitor.js';
function treeOnly(healthBase) {
@ -32,6 +32,20 @@ describe('verae-fleet', () => {
for (const s of supervisors) await s.stopAll();
});
it('spawnLocal runs spawn.command instead of the stub worker', async () => {
const child = spawnLocal(
FLEET_ROOT,
{ spawn: { command: process.execPath, args: ['-e', 'process.stdout.write("ok"); process.exit(0)'] } },
{ ...process.env },
9,
);
const out = [];
child.stdout.on('data', (c) => out.push(c));
const code = await new Promise((resolve) => child.on('exit', resolve));
assert.equal(code, 0);
assert.equal(Buffer.concat(out).toString(), 'ok');
});
it('lists every service with a config file and central replica spec', () => {
const loaded = loadFleet();
const rows = listServices(loaded);
@ -39,6 +53,9 @@ describe('verae-fleet', () => {
for (const need of [
'nats',
'zappier-edge',
'customer-service',
'sales-pricing',
'accounting-export',
'middleware-http',
'job-poller',
'webhook-deliver',