From ddf772454b588f7e056e459f95a292e149dc2f6d Mon Sep 17 00:00:00 2001 From: George Lambert Date: Fri, 11 Sep 2026 17:15:16 -0400 Subject: [PATCH] Clean prepaid SoT, identity mailbox, public access planes, leaf policy, fleet spawn Persist account-balance books; edge caches prepaid from books. Add zappier-identity, verae-nats-accounts, verae-jobs-events, verae-access-staff, zapier-decisions. Edge binds loopback; lan-134 stays off; HTTP services prefer local spawn. --- docs/modules/README.md | 11 +++ docs/modules/verae-access-leaf/allow.md | 41 ++++++++ docs/modules/verae-access-leaf/server.md | 5 +- docs/modules/verae-access-staff/server.md | 41 ++++++++ docs/modules/verae-fleet/cli.md | 2 +- docs/modules/verae-fleet/machines.md | 4 +- docs/modules/verae-fleet/server.md | 2 +- docs/modules/verae-fleet/ssh.md | 2 +- docs/modules/verae-fleet/supervisor.md | 2 +- docs/modules/verae-jobs-events/server.md | 41 ++++++++ docs/modules/verae-jobs-events/subjects.md | 36 +++++++ docs/modules/verae-nats-accounts/policy.md | 43 +++++++++ docs/modules/verae-zapier-simulator/server.md | 2 +- docs/modules/zappier-account-balance/books.md | 6 +- .../zappier-account-balance/persist.md | 50 ++++++++++ .../modules/zappier-account-balance/server.md | 7 +- docs/modules/zappier-identity/ids.md | 41 ++++++++ docs/modules/zappier-identity/server.md | 53 +++++++++++ docs/modules/zappier-identity/store.md | 50 ++++++++++ docs/modules/zappier-identity/subjects.md | 36 +++++++ docs/modules/zappier/admin.md | 5 +- docs/modules/zappier/index.md | 2 +- docs/modules/zappier/ledger.md | 52 ++++++++++ docs/modules/zappier/portal.md | 3 +- docs/modules/zappier/verae-bind.md | 6 +- docs/sphinx/index.rst | 16 ++++ .../sphinx/modules/verae-access-leaf/allow.md | 41 ++++++++ .../modules/verae-access-leaf/allow.rst | 7 ++ .../modules/verae-access-leaf/server.md | 5 +- .../modules/verae-access-staff/server.md | 41 ++++++++ .../modules/verae-access-staff/server.rst | 7 ++ docs/sphinx/modules/verae-fleet/cli.md | 2 +- docs/sphinx/modules/verae-fleet/machines.md | 4 +- docs/sphinx/modules/verae-fleet/server.md | 2 +- docs/sphinx/modules/verae-fleet/ssh.md | 2 +- docs/sphinx/modules/verae-fleet/supervisor.md | 2 +- .../modules/verae-jobs-events/server.md | 41 ++++++++ .../modules/verae-jobs-events/server.rst | 7 ++ .../modules/verae-jobs-events/subjects.md | 36 +++++++ .../modules/verae-jobs-events/subjects.rst | 7 ++ .../modules/verae-nats-accounts/policy.md | 43 +++++++++ .../modules/verae-nats-accounts/policy.rst | 7 ++ .../modules/verae-zapier-simulator/server.md | 2 +- .../modules/zappier-account-balance/books.md | 6 +- .../zappier-account-balance/persist.md | 50 ++++++++++ .../zappier-account-balance/persist.rst | 7 ++ .../modules/zappier-account-balance/server.md | 7 +- docs/sphinx/modules/zappier-identity/ids.md | 41 ++++++++ docs/sphinx/modules/zappier-identity/ids.rst | 7 ++ .../sphinx/modules/zappier-identity/server.md | 53 +++++++++++ .../modules/zappier-identity/server.rst | 7 ++ docs/sphinx/modules/zappier-identity/store.md | 50 ++++++++++ .../sphinx/modules/zappier-identity/store.rst | 7 ++ .../modules/zappier-identity/subjects.md | 36 +++++++ .../modules/zappier-identity/subjects.rst | 7 ++ docs/sphinx/modules/zappier/admin.md | 5 +- docs/sphinx/modules/zappier/index.md | 2 +- docs/sphinx/modules/zappier/ledger.md | 52 ++++++++++ docs/sphinx/modules/zappier/ledger.rst | 7 ++ docs/sphinx/modules/zappier/portal.md | 3 +- docs/sphinx/modules/zappier/verae-bind.md | 6 +- docs/sphinx/repos/verae-access-staff.md | 7 ++ docs/sphinx/repos/verae-fleet.md | 2 +- docs/sphinx/repos/verae-jobs-events.md | 7 ++ docs/sphinx/repos/verae-nats-accounts.md | 8 ++ docs/sphinx/repos/verae-zapier-simulator.md | 2 +- docs/sphinx/repos/zapier-decisions.md | 13 +++ docs/sphinx/repos/zappier-account-balance.md | 2 +- docs/sphinx/repos/zappier-identity.md | 12 +++ .../modules/verae-access-staff/NATS.md | 3 + .../modules/verae-access-staff/SUMMARY.md | 3 + .../modules/verae-jobs-events/NATS.md | 3 + .../modules/verae-jobs-events/SUMMARY.md | 3 + .../modules/verae-nats-accounts/NATS.md | 3 + .../modules/verae-nats-accounts/SUMMARY.md | 3 + .../modules/zapier-decisions/NATS.md | 3 + .../modules/zapier-decisions/SUMMARY.md | 3 + .../modules/zappier-identity/NATS.md | 3 + .../modules/zappier-identity/SUMMARY.md | 3 + packages/overview/01-system.md | 6 +- packages/overview/02-modules-and-repos.md | 5 + packages/overview/INDEX.md | 2 +- packages/verae-access-leaf/src/allow.js | 28 ++++++ packages/verae-access-leaf/src/server.js | 4 + packages/verae-access-staff/NATS.md | 3 + packages/verae-access-staff/README.md | 7 ++ packages/verae-access-staff/package.json | 11 +++ packages/verae-access-staff/src/server.js | 57 +++++++++++ .../verae-access-staff/test/health.test.js | 56 +++++++++++ packages/verae-fleet/README.md | 2 +- packages/verae-fleet/docs/CONSOLE.html | 7 +- packages/verae-fleet/docs/CONSOLE.md | 2 +- packages/verae-fleet/docs/CONSOLE.pdf | Bin 679372 -> 679560 bytes packages/verae-fleet/fleet.json | 5 +- .../verae-fleet/services/access-leaf.json | 1 + .../verae-fleet/services/access-staff.json | 22 +++++ packages/verae-fleet/services/identity.json | 24 +++++ .../verae-fleet/services/jobs-events.json | 24 +++++ .../verae-fleet/services/zappier-edge.json | 9 +- packages/verae-fleet/src/cli.js | 6 +- packages/verae-fleet/src/machines.js | 14 ++- packages/verae-fleet/src/server.js | 4 +- packages/verae-fleet/src/ssh.js | 4 +- packages/verae-fleet/src/supervisor.js | 1 + packages/verae-fleet/test/fleet.test.js | 5 + packages/verae-fleet/test/ssh.test.js | 15 +++ packages/verae-jobs-events/NATS.md | 3 + packages/verae-jobs-events/README.md | 7 ++ packages/verae-jobs-events/package.json | 14 +++ packages/verae-jobs-events/src/server.js | 42 +++++++++ packages/verae-jobs-events/src/subjects.js | 4 + .../verae-jobs-events/test/health.test.js | 23 +++++ packages/verae-nats-accounts/NATS.md | 3 + packages/verae-nats-accounts/README.md | 8 ++ packages/verae-nats-accounts/nats.conf | 21 +++++ packages/verae-nats-accounts/package.json | 10 ++ packages/verae-nats-accounts/policy.json | 35 +++++++ packages/verae-nats-accounts/src/policy.js | 14 +++ .../verae-nats-accounts/test/policy.test.js | 12 +++ packages/verae-ops/GETTING-STARTED.md | 10 +- packages/verae-zapier-simulator/README.md | 2 +- packages/verae-zapier-simulator/src/server.js | 7 +- packages/zapier-decisions/LOG.md | 40 ++++++++ packages/zapier-decisions/NATS.md | 3 + packages/zapier-decisions/README.md | 13 +++ packages/zapier-decisions/TODO.md | 8 ++ .../decisions/D001-prepaid-sot.md | 6 ++ .../decisions/D002-identity.md | 6 ++ .../decisions/D003-public-doors.md | 6 ++ .../decisions/D004-leaf-accounts.md | 6 ++ .../decisions/D005-fleet-local-first.md | 6 ++ packages/zapier-decisions/package.json | 6 ++ packages/zappier-account-balance/README.md | 2 +- packages/zappier-account-balance/src/books.js | 20 ++++ .../zappier-account-balance/src/persist.js | 28 ++++++ .../zappier-account-balance/src/server.js | 20 ++-- .../test/books.test.js | 16 ++++ packages/zappier-identity/NATS.md | 3 + packages/zappier-identity/README.md | 12 +++ packages/zappier-identity/package.json | 14 +++ packages/zappier-identity/src/ids.js | 15 +++ packages/zappier-identity/src/server.js | 89 ++++++++++++++++++ packages/zappier-identity/src/store.js | 30 ++++++ packages/zappier-identity/src/subjects.js | 5 + packages/zappier-identity/test/health.test.js | 36 +++++++ packages/zappier/src/admin.ts | 22 +++-- packages/zappier/src/index.ts | 5 +- packages/zappier/src/ledger.ts | 50 ++++++++++ packages/zappier/src/portal.ts | 46 ++++----- packages/zappier/src/verae-bind.ts | 15 ++- scripts/build-docs-site.py | 15 +++ scripts/gen-module-docs.py | 9 ++ scripts/push-module-repos.sh | 10 ++ 153 files changed, 2236 insertions(+), 116 deletions(-) create mode 100644 docs/modules/verae-access-leaf/allow.md create mode 100644 docs/modules/verae-access-staff/server.md create mode 100644 docs/modules/verae-jobs-events/server.md create mode 100644 docs/modules/verae-jobs-events/subjects.md create mode 100644 docs/modules/verae-nats-accounts/policy.md create mode 100644 docs/modules/zappier-account-balance/persist.md create mode 100644 docs/modules/zappier-identity/ids.md create mode 100644 docs/modules/zappier-identity/server.md create mode 100644 docs/modules/zappier-identity/store.md create mode 100644 docs/modules/zappier-identity/subjects.md create mode 100644 docs/modules/zappier/ledger.md create mode 100644 docs/sphinx/modules/verae-access-leaf/allow.md create mode 100644 docs/sphinx/modules/verae-access-leaf/allow.rst create mode 100644 docs/sphinx/modules/verae-access-staff/server.md create mode 100644 docs/sphinx/modules/verae-access-staff/server.rst create mode 100644 docs/sphinx/modules/verae-jobs-events/server.md create mode 100644 docs/sphinx/modules/verae-jobs-events/server.rst create mode 100644 docs/sphinx/modules/verae-jobs-events/subjects.md create mode 100644 docs/sphinx/modules/verae-jobs-events/subjects.rst create mode 100644 docs/sphinx/modules/verae-nats-accounts/policy.md create mode 100644 docs/sphinx/modules/verae-nats-accounts/policy.rst create mode 100644 docs/sphinx/modules/zappier-account-balance/persist.md create mode 100644 docs/sphinx/modules/zappier-account-balance/persist.rst create mode 100644 docs/sphinx/modules/zappier-identity/ids.md create mode 100644 docs/sphinx/modules/zappier-identity/ids.rst create mode 100644 docs/sphinx/modules/zappier-identity/server.md create mode 100644 docs/sphinx/modules/zappier-identity/server.rst create mode 100644 docs/sphinx/modules/zappier-identity/store.md create mode 100644 docs/sphinx/modules/zappier-identity/store.rst create mode 100644 docs/sphinx/modules/zappier-identity/subjects.md create mode 100644 docs/sphinx/modules/zappier-identity/subjects.rst create mode 100644 docs/sphinx/modules/zappier/ledger.md create mode 100644 docs/sphinx/modules/zappier/ledger.rst create mode 100644 docs/sphinx/repos/verae-access-staff.md create mode 100644 docs/sphinx/repos/verae-jobs-events.md create mode 100644 docs/sphinx/repos/verae-nats-accounts.md create mode 100644 docs/sphinx/repos/zapier-decisions.md create mode 100644 docs/sphinx/repos/zappier-identity.md create mode 100644 packages/docs-master/modules/verae-access-staff/NATS.md create mode 100644 packages/docs-master/modules/verae-access-staff/SUMMARY.md create mode 100644 packages/docs-master/modules/verae-jobs-events/NATS.md create mode 100644 packages/docs-master/modules/verae-jobs-events/SUMMARY.md create mode 100644 packages/docs-master/modules/verae-nats-accounts/NATS.md create mode 100644 packages/docs-master/modules/verae-nats-accounts/SUMMARY.md create mode 100644 packages/docs-master/modules/zapier-decisions/NATS.md create mode 100644 packages/docs-master/modules/zapier-decisions/SUMMARY.md create mode 100644 packages/docs-master/modules/zappier-identity/NATS.md create mode 100644 packages/docs-master/modules/zappier-identity/SUMMARY.md create mode 100644 packages/verae-access-leaf/src/allow.js create mode 100644 packages/verae-access-staff/NATS.md create mode 100644 packages/verae-access-staff/README.md create mode 100644 packages/verae-access-staff/package.json create mode 100644 packages/verae-access-staff/src/server.js create mode 100644 packages/verae-access-staff/test/health.test.js create mode 100644 packages/verae-fleet/services/access-staff.json create mode 100644 packages/verae-fleet/services/identity.json create mode 100644 packages/verae-fleet/services/jobs-events.json create mode 100644 packages/verae-jobs-events/NATS.md create mode 100644 packages/verae-jobs-events/README.md create mode 100644 packages/verae-jobs-events/package.json create mode 100644 packages/verae-jobs-events/src/server.js create mode 100644 packages/verae-jobs-events/src/subjects.js create mode 100644 packages/verae-jobs-events/test/health.test.js create mode 100644 packages/verae-nats-accounts/NATS.md create mode 100644 packages/verae-nats-accounts/README.md create mode 100644 packages/verae-nats-accounts/nats.conf create mode 100644 packages/verae-nats-accounts/package.json create mode 100644 packages/verae-nats-accounts/policy.json create mode 100644 packages/verae-nats-accounts/src/policy.js create mode 100644 packages/verae-nats-accounts/test/policy.test.js create mode 100644 packages/zapier-decisions/LOG.md create mode 100644 packages/zapier-decisions/NATS.md create mode 100644 packages/zapier-decisions/README.md create mode 100644 packages/zapier-decisions/TODO.md create mode 100644 packages/zapier-decisions/decisions/D001-prepaid-sot.md create mode 100644 packages/zapier-decisions/decisions/D002-identity.md create mode 100644 packages/zapier-decisions/decisions/D003-public-doors.md create mode 100644 packages/zapier-decisions/decisions/D004-leaf-accounts.md create mode 100644 packages/zapier-decisions/decisions/D005-fleet-local-first.md create mode 100644 packages/zapier-decisions/package.json create mode 100644 packages/zappier-account-balance/src/persist.js create mode 100644 packages/zappier-identity/NATS.md create mode 100644 packages/zappier-identity/README.md create mode 100644 packages/zappier-identity/package.json create mode 100644 packages/zappier-identity/src/ids.js create mode 100644 packages/zappier-identity/src/server.js create mode 100644 packages/zappier-identity/src/store.js create mode 100644 packages/zappier-identity/src/subjects.js create mode 100644 packages/zappier-identity/test/health.test.js create mode 100644 packages/zappier/src/ledger.ts diff --git a/docs/modules/README.md b/docs/modules/README.md index f0b689e..37c22cc 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -23,6 +23,7 @@ One MD file per source module. - [zappier/index](zappier/index.md) - [zappier/invoicing](zappier/invoicing.md) - [zappier/jobs/report-usage](zappier/jobs/report-usage.md) +- [zappier/ledger](zappier/ledger.md) - [zappier/meter](zappier/meter.md) - [zappier/nats-shim.d](zappier/nats-shim.d.md) - [zappier/paths](zappier/paths.md) @@ -121,6 +122,7 @@ One MD file per source module. - [verae-zapier-simulator/server](verae-zapier-simulator/server.md) - [verae-zapier-simulator/trace](verae-zapier-simulator/trace.md) - [zappier-account-balance/books](zappier-account-balance/books.md) +- [zappier-account-balance/persist](zappier-account-balance/persist.md) - [zappier-account-balance/server](zappier-account-balance/server.md) - [zappier-account-balance/subjects](zappier-account-balance/subjects.md) - [zappier-customer-service/nats-billing](zappier-customer-service/nats-billing.md) @@ -136,5 +138,14 @@ One MD file per source module. - [verae-access-web/gate](verae-access-web/gate.md) - [verae-access-web/server](verae-access-web/server.md) - [verae-access-api/server](verae-access-api/server.md) +- [verae-access-leaf/allow](verae-access-leaf/allow.md) - [verae-access-leaf/server](verae-access-leaf/server.md) - [verae-access-zapier/server](verae-access-zapier/server.md) +- [verae-access-staff/server](verae-access-staff/server.md) +- [zappier-identity/ids](zappier-identity/ids.md) +- [zappier-identity/server](zappier-identity/server.md) +- [zappier-identity/store](zappier-identity/store.md) +- [zappier-identity/subjects](zappier-identity/subjects.md) +- [verae-jobs-events/server](verae-jobs-events/server.md) +- [verae-jobs-events/subjects](verae-jobs-events/subjects.md) +- [verae-nats-accounts/policy](verae-nats-accounts/policy.md) diff --git a/docs/modules/verae-access-leaf/allow.md b/docs/modules/verae-access-leaf/allow.md new file mode 100644 index 0000000..1e1b106 --- /dev/null +++ b/docs/modules/verae-access-leaf/allow.md @@ -0,0 +1,41 @@ +# `verae-access-leaf/allow` + +**Package:** `verae-access-leaf` +**Source:** `packages/verae-access-leaf/src/allow.js` +**Lines:** 29 + +## What this module is + +Implementation module in `verae-access-leaf`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`leafAllowList`, `leafAllowed` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `leafAllowList` | `(none)` | — | `unknown` | see Call graph | +| `leafAllowed` | `subject` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:fs` + +## Call graph (identifiers invoked) + +`leafAllowList`, `existsSync`, `parse`, `readFileSync`, `leafAllowed`, `some`, `endsWith`, `startsWith` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-access-leaf/server.md b/docs/modules/verae-access-leaf/server.md index d7b1fba..ad42867 100644 --- a/docs/modules/verae-access-leaf/server.md +++ b/docs/modules/verae-access-leaf/server.md @@ -2,7 +2,7 @@ **Package:** `verae-access-leaf` **Source:** `packages/verae-access-leaf/src/server.js` -**Lines:** 95 +**Lines:** 99 ## What this module is @@ -29,10 +29,11 @@ _None extracted._ ## What it imports / requires - `node:http` +- `./allow.js` ## Call graph (identifiers invoked) -`subjects`, `replace`, `check`, `fetch`, `stringify`, `json`, `authorizedToken`, `forward`, `import`, `connect`, `split`, `publish`, `encode`, `flush`, `close`, `startNats`, `async`, `subscribe`, `parse`, `decode`, `respond`, `write`, `createServer`, `writeHead`, `end`, `push`, `concat`, `toString`, `listen` +`subjects`, `replace`, `check`, `fetch`, `stringify`, `json`, `authorizedToken`, `forward`, `leafAllowed`, `import`, `connect`, `split`, `publish`, `encode`, `flush`, `close`, `startNats`, `async`, `subscribe`, `parse`, `decode`, `respond`, `write`, `createServer`, `writeHead`, `end`, `push`, `concat`, `toString`, `listen` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/verae-access-staff/server.md b/docs/modules/verae-access-staff/server.md new file mode 100644 index 0000000..2f2291d --- /dev/null +++ b/docs/modules/verae-access-staff/server.md @@ -0,0 +1,41 @@ +# `verae-access-staff/server` + +**Package:** `verae-access-staff` +**Source:** `packages/verae-access-staff/src/server.js` +**Lines:** 58 + +## What this module is + +Implementation module in `verae-access-staff`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +_No named `export` / `module.exports` keys detected._ + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `check` | `subject, extra = {}` | — | `unknown` | see Call graph | +| `json` | `code, obj` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:http` + +## Call graph (identifiers invoked) + +`plane`, `replace`, `check`, `fetch`, `stringify`, `json`, `createServer`, `async`, `writeHead`, `end`, `match`, `push`, `parse`, `concat`, `toString`, `listen`, `write` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-fleet/cli.md b/docs/modules/verae-fleet/cli.md index 3fb56ae..a72ea69 100644 --- a/docs/modules/verae-fleet/cli.md +++ b/docs/modules/verae-fleet/cli.md @@ -50,7 +50,7 @@ _None extracted._ ## Call graph (identifiers invoked) -`loadFleet`, `slice`, `print`, `write`, `stringify`, `help`, `off`, `pause`, `json`, `identityFile`, `login`, `api`, `request`, `on`, `push`, `concat`, `toString`, `resolve`, `parse`, `end`, `main`, `listServices`, `padEnd`, `padStart`, `import`, `find`, `sshCheck`, `keys`, `startService`, `tick`, `start`, `startControlServer`, `async`, `stop`, `stopAll`, `exit`, `test` +`loadFleet`, `slice`, `print`, `write`, `stringify`, `help`, `off`, `pause`, `json`, `identityFile`, `login`, `api`, `request`, `on`, `push`, `concat`, `toString`, `resolve`, `parse`, `end`, `main`, `listServices`, `padEnd`, `padStart`, `import`, `find`, `sshCheck`, `startControlServer`, `keys`, `startService`, `tick`, `start`, `async`, `stop`, `stopAll`, `exit`, `test` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/verae-fleet/machines.md b/docs/modules/verae-fleet/machines.md index 628911d..54ad618 100644 --- a/docs/modules/verae-fleet/machines.md +++ b/docs/modules/verae-fleet/machines.md @@ -2,7 +2,7 @@ **Package:** `verae-fleet` **Source:** `packages/verae-fleet/src/machines.js` -**Lines:** 108 +**Lines:** 116 ## What this module is @@ -25,7 +25,7 @@ _None extracted._ | `normalizeMachine` | `m` | — | `unknown` | see Call graph | | `upsertMachine` | `list, spec` | — | `unknown` | see Call graph | | `canHost` | `machine, serviceId, role` | — | `unknown` | see Call graph | -| `pickMachine` | `machines, instances, serviceId, role, exclude = []` | machines: `object[]`, serviceId: `string`, role: `string` | `unknown` | see Call graph | +| `pickMachine` | `machines, instances, serviceId, role, exclude = [], opts = {}` | machines: `object[]`, serviceId: `string`, role: `string` | `unknown` | see Call graph | | `saveOverlay` | `overlayPath, machines` | — | `unknown` | see Call graph | ## What it imports / requires diff --git a/docs/modules/verae-fleet/server.md b/docs/modules/verae-fleet/server.md index 35986c3..ca86400 100644 --- a/docs/modules/verae-fleet/server.md +++ b/docs/modules/verae-fleet/server.md @@ -34,7 +34,7 @@ _None extracted._ ## Call graph (identifiers invoked) -`join`, `dirname`, `fileURLToPath`, `json`, `writeHead`, `end`, `stringify`, `import`, `startControlServer`, `createServer`, `async`, `snapshot`, `reset`, `push`, `parse`, `concat`, `toString`, `run`, `listServices`, `status`, `addMachine`, `match`, `checkMachine`, `setMachineEnabled`, `reconcile`, `startService`, `stopService`, `pauseService`, `resumeService`, `fn`, `pauseInstance`, `resumeInstance`, `restartInstance`, `stopInstance`, `markUnhealthy`, `createReadStream`, `pipe`, `startsWith`, `normalize`, `existsSync`, `statSync`, `isFile`, `extname`, `toLowerCase`, `listen`, `write` +`bind`, `join`, `dirname`, `fileURLToPath`, `json`, `writeHead`, `end`, `stringify`, `import`, `startControlServer`, `createServer`, `async`, `snapshot`, `reset`, `push`, `parse`, `concat`, `toString`, `run`, `listServices`, `status`, `addMachine`, `match`, `checkMachine`, `setMachineEnabled`, `reconcile`, `startService`, `stopService`, `pauseService`, `resumeService`, `fn`, `pauseInstance`, `resumeInstance`, `restartInstance`, `stopInstance`, `markUnhealthy`, `createReadStream`, `pipe`, `startsWith`, `normalize`, `existsSync`, `statSync`, `isFile`, `extname`, `toLowerCase`, `listen`, `write` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/verae-fleet/ssh.md b/docs/modules/verae-fleet/ssh.md index d6bff8d..3e138cb 100644 --- a/docs/modules/verae-fleet/ssh.md +++ b/docs/modules/verae-fleet/ssh.md @@ -25,7 +25,7 @@ _None extracted._ | `sshTarget` | `machine` | — | `unknown` | see Call graph | | `sshBaseArgs` | `machine` | — | `unknown` | see Call graph | | `scpBaseArgs` | `machine` | — | `unknown` | see Call graph | -| `sshExec` | `machine, remoteCommand, { timeoutMs = 20000 } = {}` | — | `unknown` | see Call graph | +| `sshExec` | `machine, remoteCommand, { timeoutMs = 8000 } = {}` | — | `unknown` | see Call graph | | `scpTo` | `machine, localFiles, remoteDir` | — | `unknown` | see Call graph | | `ensureRemoteRuntime` | `machine` | — | `unknown` | see Call graph | | `sshSpawnWorker` | `machine, { instance, env }` | — | `unknown` | see Call graph | diff --git a/docs/modules/verae-fleet/supervisor.md b/docs/modules/verae-fleet/supervisor.md index e778499..6f86342 100644 --- a/docs/modules/verae-fleet/supervisor.md +++ b/docs/modules/verae-fleet/supervisor.md @@ -2,7 +2,7 @@ **Package:** `verae-fleet` **Source:** `packages/verae-fleet/src/supervisor.js` -**Lines:** 528 +**Lines:** 529 ## What this module is diff --git a/docs/modules/verae-jobs-events/server.md b/docs/modules/verae-jobs-events/server.md new file mode 100644 index 0000000..deab523 --- /dev/null +++ b/docs/modules/verae-jobs-events/server.md @@ -0,0 +1,41 @@ +# `verae-jobs-events/server` + +**Package:** `verae-jobs-events` +**Source:** `packages/verae-jobs-events/src/server.js` +**Lines:** 43 + +## What this module is + +Implementation module in `verae-jobs-events`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +_No named `export` / `module.exports` keys detected._ + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `startNats` | `(none)` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:http` +- `./subjects.js` + +## Call graph (identifiers invoked) + +`startNats`, `import`, `connect`, `split`, `subscribe`, `parse`, `decode`, `respond`, `encode`, `stringify`, `createServer`, `writeHead`, `end`, `listen`, `write` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-jobs-events/subjects.md b/docs/modules/verae-jobs-events/subjects.md new file mode 100644 index 0000000..1083d72 --- /dev/null +++ b/docs/modules/verae-jobs-events/subjects.md @@ -0,0 +1,36 @@ +# `verae-jobs-events/subjects` + +**Package:** `verae-jobs-events` +**Source:** `packages/verae-jobs-events/src/subjects.js` +**Lines:** 5 + +## What this module is + +Implementation module in `verae-jobs-events`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`SUBJECTS` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +_No top-level functions extracted._ + +## What it imports / requires + +_No imports detected._ + +## Call graph (identifiers invoked) + +_No local call identifiers extracted._ + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-nats-accounts/policy.md b/docs/modules/verae-nats-accounts/policy.md new file mode 100644 index 0000000..7248be3 --- /dev/null +++ b/docs/modules/verae-nats-accounts/policy.md @@ -0,0 +1,43 @@ +# `verae-nats-accounts/policy` + +**Package:** `verae-nats-accounts` +**Source:** `packages/verae-nats-accounts/src/policy.js` +**Lines:** 15 + +## What this module is + +Implementation module in `verae-nats-accounts`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`loadPolicy`, `leafAllowed` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `loadPolicy` | `(none)` | — | `unknown` | see Call graph | +| `leafAllowed` | `subject` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:fs` +- `node:path` +- `node:url` + +## Call graph (identifiers invoked) + +`join`, `dirname`, `fileURLToPath`, `loadPolicy`, `parse`, `readFileSync`, `leafAllowed`, `some`, `endsWith`, `startsWith` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-zapier-simulator/server.md b/docs/modules/verae-zapier-simulator/server.md index ab03284..4644bf8 100644 --- a/docs/modules/verae-zapier-simulator/server.md +++ b/docs/modules/verae-zapier-simulator/server.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-simulator` **Source:** `packages/verae-zapier-simulator/src/server.js` -**Lines:** 87 +**Lines:** 88 ## What this module is diff --git a/docs/modules/zappier-account-balance/books.md b/docs/modules/zappier-account-balance/books.md index 49dc16d..5f5e467 100644 --- a/docs/modules/zappier-account-balance/books.md +++ b/docs/modules/zappier-account-balance/books.md @@ -2,7 +2,7 @@ **Package:** `zappier-account-balance` **Source:** `packages/zappier-account-balance/src/books.js` -**Lines:** 100 +**Lines:** 120 ## What this module is @@ -36,6 +36,8 @@ Implementation module in `zappier-account-balance`. The tables below are extract | `recordUsage` | `entry` | | `recordPayment` | `entry` | | `statement` | `customerId` | +| `dump` | `(none)` | +| `load` | `raw` | ## What it imports / requires @@ -43,7 +45,7 @@ _No imports detected._ ## Call graph (identifiers invoked) -`constructor`, `remember`, `prepaidCents`, `adjust`, `trunc`, `now`, `toString`, `random`, `slice`, `toISOString`, `unshift`, `recordUsage`, `recordPayment`, `statement`, `filter`, `match`, `handle`, `endsWith` +`constructor`, `remember`, `prepaidCents`, `adjust`, `trunc`, `now`, `toString`, `random`, `slice`, `toISOString`, `unshift`, `recordUsage`, `recordPayment`, `statement`, `filter`, `match`, `dump`, `load`, `isArray`, `handle`, `endsWith` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/zappier-account-balance/persist.md b/docs/modules/zappier-account-balance/persist.md new file mode 100644 index 0000000..0be5a03 --- /dev/null +++ b/docs/modules/zappier-account-balance/persist.md @@ -0,0 +1,50 @@ +# `zappier-account-balance/persist` + +**Package:** `zappier-account-balance` +**Source:** `packages/zappier-account-balance/src/persist.js` +**Lines:** 29 + +## What this module is + +Implementation module in `zappier-account-balance`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`booksPath`, `loadInto`, `saveFrom` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `booksPath` | `(none)` | — | `unknown` | see Call graph | +| `loadInto` | `books` | — | `unknown` | see Call graph | +| `saveFrom` | `books` | — | `unknown` | see Call graph | + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `return` | `process.env.BOOKS_PATH || + path.join(process.env.FLEET_STATE_DIR || process.cwd(` | + +## What it imports / requires + +- `node:fs` +- `node:path` + +## Call graph (identifiers invoked) + +`booksPath`, `join`, `cwd`, `loadInto`, `existsSync`, `load`, `parse`, `readFileSync`, `saveFrom`, `mkdirSync`, `dirname`, `writeFileSync`, `stringify`, `dump`, `renameSync` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/zappier-account-balance/server.md b/docs/modules/zappier-account-balance/server.md index 6b1858a..00d8a73 100644 --- a/docs/modules/zappier-account-balance/server.md +++ b/docs/modules/zappier-account-balance/server.md @@ -2,7 +2,7 @@ **Package:** `zappier-account-balance` **Source:** `packages/zappier-account-balance/src/server.js` -**Lines:** 93 +**Lines:** 101 ## What this module is @@ -20,6 +20,7 @@ _None extracted._ | Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | |------|------------|---------------------|---------|-----------------------------| +| `apply` | `subject, payload` | — | `unknown` | see Call graph | | `startNats` | `(none)` | — | `unknown` | see Call graph | | `readBody` | `req` | — | `unknown` | see Call graph | | `reply` | `sub, fn` | — | `unknown` | see Call graph | @@ -29,6 +30,7 @@ _None extracted._ | Name | Parameters | |------|------------| +| `saveFrom` | `books` | | `reply` | `nc.subscribe(SUBJECTS.STATEMENT_GET, { queue: SUBJECTS.QUEUE }` | | `reply` | `nc.subscribe(SUBJECTS.BALANCE_GET, { queue: SUBJECTS.QUEUE }` | | `reply` | `nc.subscribe(SUBJECTS.BALANCE_ADJUST, { queue: SUBJECTS.QUEUE }` | @@ -38,10 +40,11 @@ _None extracted._ - `node:http` - `./books.js` - `./subjects.js` +- `./persist.js` ## Call graph (identifiers invoked) -`startNats`, `import`, `connect`, `split`, `async`, `parse`, `decode`, `fn`, `respond`, `encode`, `stringify`, `reply`, `subscribe`, `handle`, `write`, `readBody`, `on`, `push`, `resolve`, `concat`, `toString`, `createServer`, `writeHead`, `end`, `json`, `match`, `statement`, `adjust`, `listen` +`loadInto`, `apply`, `handle`, `saveFrom`, `startNats`, `import`, `connect`, `split`, `async`, `parse`, `decode`, `fn`, `respond`, `encode`, `stringify`, `reply`, `subscribe`, `write`, `readBody`, `on`, `push`, `resolve`, `concat`, `toString`, `createServer`, `writeHead`, `end`, `json`, `match`, `statement`, `listen` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/zappier-identity/ids.md b/docs/modules/zappier-identity/ids.md new file mode 100644 index 0000000..2628937 --- /dev/null +++ b/docs/modules/zappier-identity/ids.md @@ -0,0 +1,41 @@ +# `zappier-identity/ids` + +**Package:** `zappier-identity` +**Source:** `packages/zappier-identity/src/ids.js` +**Lines:** 16 + +## What this module is + +Implementation module in `zappier-identity`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`stableVeraeUserId`, `bindEmail` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `stableVeraeUserId` | `username` | — | `unknown` | see Call graph | +| `bindEmail` | `email` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:crypto` + +## Call graph (identifiers invoked) + +`stableVeraeUserId`, `trim`, `toLowerCase`, `createHash`, `update`, `digest`, `slice`, `bindEmail` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/zappier-identity/server.md b/docs/modules/zappier-identity/server.md new file mode 100644 index 0000000..aec4169 --- /dev/null +++ b/docs/modules/zappier-identity/server.md @@ -0,0 +1,53 @@ +# `zappier-identity/server` + +**Package:** `zappier-identity` +**Source:** `packages/zappier-identity/src/server.js` +**Lines:** 90 + +## What this module is + +Implementation module in `zappier-identity`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +_No named `export` / `module.exports` keys detected._ + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `handleBind` | `p` | — | `unknown` | see Call graph | +| `handleLookup` | `p` | — | `unknown` | see Call graph | +| `startNats` | `(none)` | — | `unknown` | see Call graph | +| `readBody` | `req` | — | `unknown` | see Call graph | +| `json` | `code, obj` | — | `unknown` | see Call graph | + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `put` | `db, { customerId, ...bound }` | + +## What it imports / requires + +- `node:http` +- `./ids.js` +- `./store.js` +- `./subjects.js` + +## Call graph (identifiers invoked) + +`load`, `handleBind`, `bindEmail`, `put`, `handleLookup`, `startNats`, `import`, `connect`, `split`, `async`, `subscribe`, `parse`, `decode`, `respond`, `encode`, `stringify`, `readBody`, `on`, `push`, `resolve`, `concat`, `toString`, `createServer`, `writeHead`, `end`, `json`, `match`, `listen`, `write` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/zappier-identity/store.md b/docs/modules/zappier-identity/store.md new file mode 100644 index 0000000..fcdd798 --- /dev/null +++ b/docs/modules/zappier-identity/store.md @@ -0,0 +1,50 @@ +# `zappier-identity/store` + +**Package:** `zappier-identity` +**Source:** `packages/zappier-identity/src/store.js` +**Lines:** 31 + +## What this module is + +Implementation module in `zappier-identity`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`load`, `save`, `put` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `load` | `(none)` | — | `unknown` | see Call graph | +| `save` | `db` | — | `unknown` | see Call graph | +| `put` | `db, { customerId, veraeUserId, veraeUsername }` | — | `unknown` | see Call graph | +| `file` | `(none)` | — | `unknown` | see Call graph | + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `save` | `db` | + +## What it imports / requires + +- `node:fs` +- `node:path` + +## Call graph (identifiers invoked) + +`join`, `cwd`, `load`, `file`, `existsSync`, `parse`, `readFileSync`, `save`, `mkdirSync`, `dirname`, `writeFileSync`, `stringify`, `put` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/zappier-identity/subjects.md b/docs/modules/zappier-identity/subjects.md new file mode 100644 index 0000000..063e79c --- /dev/null +++ b/docs/modules/zappier-identity/subjects.md @@ -0,0 +1,36 @@ +# `zappier-identity/subjects` + +**Package:** `zappier-identity` +**Source:** `packages/zappier-identity/src/subjects.js` +**Lines:** 6 + +## What this module is + +Implementation module in `zappier-identity`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`SUBJECTS` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +_No top-level functions extracted._ + +## What it imports / requires + +_No imports detected._ + +## Call graph (identifiers invoked) + +_No local call identifiers extracted._ + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/zappier/admin.md b/docs/modules/zappier/admin.md index 96e67cd..6180d66 100644 --- a/docs/modules/zappier/admin.md +++ b/docs/modules/zappier/admin.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/admin.ts` -**Lines:** 593 +**Lines:** 597 ## What this module is @@ -63,10 +63,11 @@ Implementation module in `zappier`. The tables below are extracted from the sour - `./accounting-export` - `./statement` - `./billing-nats` +- `./ledger` ## Call graph (identifiers invoked) -`tokens`, `adminLoginRouter`, `post`, `status`, `json`, `findByUsername`, `verifyPassword`, `randomBytes`, `toString`, `set`, `now`, `adminAuth`, `header`, `next`, `startsWith`, `slice`, `has`, `isValidRule`, `every`, `isValidTier`, `periodWindow`, `split`, `map`, `parseDate`, `trim`, `isNaN`, `getTime`, `escapeHtml`, `replace`, `toFixed`, `renderInvoiceHtml`, `dollars`, `join`, `toISOString`, `toUpperCase`, `adminRouter`, `get`, `getRateCard`, `getTiers`, `put`, `upsertEndpoint`, `delete`, `deleteEndpoint`, `upsertTier`, `deleteTier`, `list`, `randomUUID`, `save`, `find`, `isFinite`, `trunc`, `add`, `natsPublish`, `natsAdjust`, `async`, `natsStatement`, `composeStatement`, `listFor`, `type`, `send` +`tokens`, `adminLoginRouter`, `post`, `status`, `json`, `findByUsername`, `verifyPassword`, `randomBytes`, `toString`, `set`, `now`, `adminAuth`, `header`, `next`, `startsWith`, `slice`, `has`, `isValidRule`, `every`, `isValidTier`, `periodWindow`, `split`, `map`, `parseDate`, `trim`, `isNaN`, `getTime`, `escapeHtml`, `replace`, `toFixed`, `renderInvoiceHtml`, `dollars`, `join`, `toISOString`, `toUpperCase`, `adminRouter`, `get`, `getRateCard`, `getTiers`, `put`, `upsertEndpoint`, `delete`, `deleteEndpoint`, `upsertTier`, `deleteTier`, `list`, `randomUUID`, `save`, `find`, `async`, `isFinite`, `trunc`, `add`, `ledgerAdjust`, `booksConfigured`, `natsPublish`, `ledgerStatement`, `composeStatement`, `listFor`, `type` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/zappier/index.md b/docs/modules/zappier/index.md index 7214325..dab278a 100644 --- a/docs/modules/zappier/index.md +++ b/docs/modules/zappier/index.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/index.ts` -**Lines:** 42 +**Lines:** 43 ## What this module is diff --git a/docs/modules/zappier/ledger.md b/docs/modules/zappier/ledger.md new file mode 100644 index 0000000..4f7e82a --- /dev/null +++ b/docs/modules/zappier/ledger.md @@ -0,0 +1,52 @@ +# `zappier/ledger` + +**Package:** `zappier` +**Source:** `packages/zappier/src/ledger.ts` +**Lines:** 51 + +## What this module is + +Implementation module in `zappier`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`PrepaidRow`, `booksConfigured`, `ledgerAdjust`, `ledgerStatement` + +## Types / interfaces / classes + +| Kind | Name | +|------|------| +| type | `PrepaidRow` | + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `booksUrl` | `(none)` | — | `unknown` | see Call graph | +| `booksConfigured` | `(none)` | — | `unknown` | see Call graph | +| `ledgerAdjust` | `row: PrepaidRow, plane: AccessPlane` | — | `unknown` | see Call graph | +| `ledgerStatement` | `customerId: string, plane: AccessPlane, veraeUserId?: string` | — | `unknown` | see Call graph | + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `return` | `process.env.ACCOUNT_BALANCE_URL || ''` | +| `return` | `await r.json(` | + +## What it imports / requires + +- `./billing-nats` + +## Call graph (identifiers invoked) + +`booksUrl`, `replace`, `booksConfigured`, `ledgerAdjust`, `natsAdjust`, `fetch`, `stringify`, `json`, `ledgerStatement`, `natsStatement`, `encodeURIComponent` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/zappier/portal.md b/docs/modules/zappier/portal.md index 4c31a9c..25d9df7 100644 --- a/docs/modules/zappier/portal.md +++ b/docs/modules/zappier/portal.md @@ -52,10 +52,11 @@ Implementation module in `zappier`. The tables below are extracted from the sour - `./statement` - `./billing-nats` - `./verae-bind` +- `./ledger` ## Call graph (identifiers invoked) -`now`, `reload`, `customer`, `publicProfile`, `sessionAuth`, `header`, `startsWith`, `slice`, `get`, `list`, `find`, `status`, `json`, `next`, `save`, `portalRouter`, `post`, `async`, `trim`, `test`, `hashPassword`, `findByEmail`, `randomBytes`, `toString`, `bindVeraeUser`, `create`, `verifyPassword`, `verifyTotp`, `use`, `delete`, `setUTCDate`, `setUTCHours`, `summaryFor`, `tiers`, `applyMonthlyCredit`, `rateCard`, `natsStatement`, `composeStatement`, `listFor`, `type`, `send`, `renderInvoiceHtml`, `generateTotpSecret`, `totpUri`, `qr`, `isInteger`, `natsPublish`, `natsAdjust`, `put` +`now`, `reload`, `customer`, `publicProfile`, `sessionAuth`, `header`, `startsWith`, `slice`, `get`, `list`, `find`, `status`, `json`, `next`, `save`, `portalRouter`, `post`, `async`, `trim`, `test`, `hashPassword`, `findByEmail`, `randomBytes`, `toString`, `bindVeraeUser`, `create`, `verifyPassword`, `verifyTotp`, `use`, `delete`, `setUTCDate`, `setUTCHours`, `summaryFor`, `tiers`, `applyMonthlyCredit`, `rateCard`, `ledgerStatement`, `composeStatement`, `listFor`, `type`, `send`, `renderInvoiceHtml`, `generateTotpSecret`, `totpUri`, `qr`, `isInteger`, `ledgerAdjust`, `booksConfigured`, `natsPublish`, `put` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/modules/zappier/verae-bind.md b/docs/modules/zappier/verae-bind.md index 30f3320..547bb4c 100644 --- a/docs/modules/zappier/verae-bind.md +++ b/docs/modules/zappier/verae-bind.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/verae-bind.ts` -**Lines:** 77 +**Lines:** 90 ## What this module is @@ -25,7 +25,7 @@ Implementation module in `zappier`. The tables below are extracted from the sour | `normalizeVeraeUsername` | `username: string` | — | `unknown` | see Call graph | | `stableVeraeUserId` | `username: string` | — | `unknown` | see Call graph | | `mockBind` | `email: string` | — | `unknown` | see Call graph | -| `bindVeraeUser` | `email: string` | — | `unknown` | see Call graph | +| `bindVeraeUser` | `email: string, customerId?: string` | — | `unknown` | see Call graph | | `login` | `username: string, pass: string` | — | `unknown` | see Call graph | ## What it imports / requires @@ -34,7 +34,7 @@ Implementation module in `zappier`. The tables below are extracted from the sour ## Call graph (identifiers invoked) -`normalizeVeraeUsername`, `trim`, `toLowerCase`, `stableVeraeUserId`, `createHash`, `update`, `digest`, `slice`, `mockBind`, `bindVeraeUser`, `replace`, `randomBytes`, `toString`, `async`, `fetch`, `stringify`, `json`, `login` +`normalizeVeraeUsername`, `trim`, `toLowerCase`, `stableVeraeUserId`, `createHash`, `update`, `digest`, `slice`, `mockBind`, `bindVeraeUser`, `replace`, `fetch`, `stringify`, `json`, `randomBytes`, `toString`, `async`, `login` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst index 47e4264..d9ebf5e 100644 --- a/docs/sphinx/index.rst +++ b/docs/sphinx/index.rst @@ -33,6 +33,11 @@ Markdown indexes: https://zapier.georgelambert.org/index-md.html repos/verae-access-api repos/verae-access-leaf repos/verae-access-zapier + repos/verae-access-staff + repos/zappier-identity + repos/verae-jobs-events + repos/verae-nats-accounts + repos/zapier-decisions .. toctree:: :maxdepth: 2 @@ -59,6 +64,7 @@ Markdown indexes: https://zapier.georgelambert.org/index-md.html modules/zappier/index modules/zappier/invoicing modules/zappier/jobs/report-usage + modules/zappier/ledger modules/zappier/meter modules/zappier/nats-shim.d modules/zappier/paths @@ -157,6 +163,7 @@ Markdown indexes: https://zapier.georgelambert.org/index-md.html modules/verae-zapier-simulator/server modules/verae-zapier-simulator/trace modules/zappier-account-balance/books + modules/zappier-account-balance/persist modules/zappier-account-balance/server modules/zappier-account-balance/subjects modules/zappier-customer-service/nats-billing @@ -172,5 +179,14 @@ Markdown indexes: https://zapier.georgelambert.org/index-md.html modules/verae-access-web/gate modules/verae-access-web/server modules/verae-access-api/server + modules/verae-access-leaf/allow modules/verae-access-leaf/server modules/verae-access-zapier/server + modules/verae-access-staff/server + modules/zappier-identity/ids + modules/zappier-identity/server + modules/zappier-identity/store + modules/zappier-identity/subjects + modules/verae-jobs-events/server + modules/verae-jobs-events/subjects + modules/verae-nats-accounts/policy diff --git a/docs/sphinx/modules/verae-access-leaf/allow.md b/docs/sphinx/modules/verae-access-leaf/allow.md new file mode 100644 index 0000000..1e1b106 --- /dev/null +++ b/docs/sphinx/modules/verae-access-leaf/allow.md @@ -0,0 +1,41 @@ +# `verae-access-leaf/allow` + +**Package:** `verae-access-leaf` +**Source:** `packages/verae-access-leaf/src/allow.js` +**Lines:** 29 + +## What this module is + +Implementation module in `verae-access-leaf`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`leafAllowList`, `leafAllowed` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `leafAllowList` | `(none)` | — | `unknown` | see Call graph | +| `leafAllowed` | `subject` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:fs` + +## Call graph (identifiers invoked) + +`leafAllowList`, `existsSync`, `parse`, `readFileSync`, `leafAllowed`, `some`, `endsWith`, `startsWith` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-access-leaf/allow.rst b/docs/sphinx/modules/verae-access-leaf/allow.rst new file mode 100644 index 0000000..84f8e3d --- /dev/null +++ b/docs/sphinx/modules/verae-access-leaf/allow.rst @@ -0,0 +1,7 @@ +verae-access-leaf.allow +======================= + +Generated API sheet for ``verae-access-leaf/allow``. + +.. include:: allow.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-access-leaf/server.md b/docs/sphinx/modules/verae-access-leaf/server.md index d7b1fba..ad42867 100644 --- a/docs/sphinx/modules/verae-access-leaf/server.md +++ b/docs/sphinx/modules/verae-access-leaf/server.md @@ -2,7 +2,7 @@ **Package:** `verae-access-leaf` **Source:** `packages/verae-access-leaf/src/server.js` -**Lines:** 95 +**Lines:** 99 ## What this module is @@ -29,10 +29,11 @@ _None extracted._ ## What it imports / requires - `node:http` +- `./allow.js` ## Call graph (identifiers invoked) -`subjects`, `replace`, `check`, `fetch`, `stringify`, `json`, `authorizedToken`, `forward`, `import`, `connect`, `split`, `publish`, `encode`, `flush`, `close`, `startNats`, `async`, `subscribe`, `parse`, `decode`, `respond`, `write`, `createServer`, `writeHead`, `end`, `push`, `concat`, `toString`, `listen` +`subjects`, `replace`, `check`, `fetch`, `stringify`, `json`, `authorizedToken`, `forward`, `leafAllowed`, `import`, `connect`, `split`, `publish`, `encode`, `flush`, `close`, `startNats`, `async`, `subscribe`, `parse`, `decode`, `respond`, `write`, `createServer`, `writeHead`, `end`, `push`, `concat`, `toString`, `listen` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/verae-access-staff/server.md b/docs/sphinx/modules/verae-access-staff/server.md new file mode 100644 index 0000000..2f2291d --- /dev/null +++ b/docs/sphinx/modules/verae-access-staff/server.md @@ -0,0 +1,41 @@ +# `verae-access-staff/server` + +**Package:** `verae-access-staff` +**Source:** `packages/verae-access-staff/src/server.js` +**Lines:** 58 + +## What this module is + +Implementation module in `verae-access-staff`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +_No named `export` / `module.exports` keys detected._ + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `check` | `subject, extra = {}` | — | `unknown` | see Call graph | +| `json` | `code, obj` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:http` + +## Call graph (identifiers invoked) + +`plane`, `replace`, `check`, `fetch`, `stringify`, `json`, `createServer`, `async`, `writeHead`, `end`, `match`, `push`, `parse`, `concat`, `toString`, `listen`, `write` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-access-staff/server.rst b/docs/sphinx/modules/verae-access-staff/server.rst new file mode 100644 index 0000000..ce7df41 --- /dev/null +++ b/docs/sphinx/modules/verae-access-staff/server.rst @@ -0,0 +1,7 @@ +verae-access-staff.server +========================= + +Generated API sheet for ``verae-access-staff/server``. + +.. include:: server.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-fleet/cli.md b/docs/sphinx/modules/verae-fleet/cli.md index 3fb56ae..a72ea69 100644 --- a/docs/sphinx/modules/verae-fleet/cli.md +++ b/docs/sphinx/modules/verae-fleet/cli.md @@ -50,7 +50,7 @@ _None extracted._ ## Call graph (identifiers invoked) -`loadFleet`, `slice`, `print`, `write`, `stringify`, `help`, `off`, `pause`, `json`, `identityFile`, `login`, `api`, `request`, `on`, `push`, `concat`, `toString`, `resolve`, `parse`, `end`, `main`, `listServices`, `padEnd`, `padStart`, `import`, `find`, `sshCheck`, `keys`, `startService`, `tick`, `start`, `startControlServer`, `async`, `stop`, `stopAll`, `exit`, `test` +`loadFleet`, `slice`, `print`, `write`, `stringify`, `help`, `off`, `pause`, `json`, `identityFile`, `login`, `api`, `request`, `on`, `push`, `concat`, `toString`, `resolve`, `parse`, `end`, `main`, `listServices`, `padEnd`, `padStart`, `import`, `find`, `sshCheck`, `startControlServer`, `keys`, `startService`, `tick`, `start`, `async`, `stop`, `stopAll`, `exit`, `test` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/verae-fleet/machines.md b/docs/sphinx/modules/verae-fleet/machines.md index 628911d..54ad618 100644 --- a/docs/sphinx/modules/verae-fleet/machines.md +++ b/docs/sphinx/modules/verae-fleet/machines.md @@ -2,7 +2,7 @@ **Package:** `verae-fleet` **Source:** `packages/verae-fleet/src/machines.js` -**Lines:** 108 +**Lines:** 116 ## What this module is @@ -25,7 +25,7 @@ _None extracted._ | `normalizeMachine` | `m` | — | `unknown` | see Call graph | | `upsertMachine` | `list, spec` | — | `unknown` | see Call graph | | `canHost` | `machine, serviceId, role` | — | `unknown` | see Call graph | -| `pickMachine` | `machines, instances, serviceId, role, exclude = []` | machines: `object[]`, serviceId: `string`, role: `string` | `unknown` | see Call graph | +| `pickMachine` | `machines, instances, serviceId, role, exclude = [], opts = {}` | machines: `object[]`, serviceId: `string`, role: `string` | `unknown` | see Call graph | | `saveOverlay` | `overlayPath, machines` | — | `unknown` | see Call graph | ## What it imports / requires diff --git a/docs/sphinx/modules/verae-fleet/server.md b/docs/sphinx/modules/verae-fleet/server.md index 35986c3..ca86400 100644 --- a/docs/sphinx/modules/verae-fleet/server.md +++ b/docs/sphinx/modules/verae-fleet/server.md @@ -34,7 +34,7 @@ _None extracted._ ## Call graph (identifiers invoked) -`join`, `dirname`, `fileURLToPath`, `json`, `writeHead`, `end`, `stringify`, `import`, `startControlServer`, `createServer`, `async`, `snapshot`, `reset`, `push`, `parse`, `concat`, `toString`, `run`, `listServices`, `status`, `addMachine`, `match`, `checkMachine`, `setMachineEnabled`, `reconcile`, `startService`, `stopService`, `pauseService`, `resumeService`, `fn`, `pauseInstance`, `resumeInstance`, `restartInstance`, `stopInstance`, `markUnhealthy`, `createReadStream`, `pipe`, `startsWith`, `normalize`, `existsSync`, `statSync`, `isFile`, `extname`, `toLowerCase`, `listen`, `write` +`bind`, `join`, `dirname`, `fileURLToPath`, `json`, `writeHead`, `end`, `stringify`, `import`, `startControlServer`, `createServer`, `async`, `snapshot`, `reset`, `push`, `parse`, `concat`, `toString`, `run`, `listServices`, `status`, `addMachine`, `match`, `checkMachine`, `setMachineEnabled`, `reconcile`, `startService`, `stopService`, `pauseService`, `resumeService`, `fn`, `pauseInstance`, `resumeInstance`, `restartInstance`, `stopInstance`, `markUnhealthy`, `createReadStream`, `pipe`, `startsWith`, `normalize`, `existsSync`, `statSync`, `isFile`, `extname`, `toLowerCase`, `listen`, `write` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/verae-fleet/ssh.md b/docs/sphinx/modules/verae-fleet/ssh.md index d6bff8d..3e138cb 100644 --- a/docs/sphinx/modules/verae-fleet/ssh.md +++ b/docs/sphinx/modules/verae-fleet/ssh.md @@ -25,7 +25,7 @@ _None extracted._ | `sshTarget` | `machine` | — | `unknown` | see Call graph | | `sshBaseArgs` | `machine` | — | `unknown` | see Call graph | | `scpBaseArgs` | `machine` | — | `unknown` | see Call graph | -| `sshExec` | `machine, remoteCommand, { timeoutMs = 20000 } = {}` | — | `unknown` | see Call graph | +| `sshExec` | `machine, remoteCommand, { timeoutMs = 8000 } = {}` | — | `unknown` | see Call graph | | `scpTo` | `machine, localFiles, remoteDir` | — | `unknown` | see Call graph | | `ensureRemoteRuntime` | `machine` | — | `unknown` | see Call graph | | `sshSpawnWorker` | `machine, { instance, env }` | — | `unknown` | see Call graph | diff --git a/docs/sphinx/modules/verae-fleet/supervisor.md b/docs/sphinx/modules/verae-fleet/supervisor.md index e778499..6f86342 100644 --- a/docs/sphinx/modules/verae-fleet/supervisor.md +++ b/docs/sphinx/modules/verae-fleet/supervisor.md @@ -2,7 +2,7 @@ **Package:** `verae-fleet` **Source:** `packages/verae-fleet/src/supervisor.js` -**Lines:** 528 +**Lines:** 529 ## What this module is diff --git a/docs/sphinx/modules/verae-jobs-events/server.md b/docs/sphinx/modules/verae-jobs-events/server.md new file mode 100644 index 0000000..deab523 --- /dev/null +++ b/docs/sphinx/modules/verae-jobs-events/server.md @@ -0,0 +1,41 @@ +# `verae-jobs-events/server` + +**Package:** `verae-jobs-events` +**Source:** `packages/verae-jobs-events/src/server.js` +**Lines:** 43 + +## What this module is + +Implementation module in `verae-jobs-events`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +_No named `export` / `module.exports` keys detected._ + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `startNats` | `(none)` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:http` +- `./subjects.js` + +## Call graph (identifiers invoked) + +`startNats`, `import`, `connect`, `split`, `subscribe`, `parse`, `decode`, `respond`, `encode`, `stringify`, `createServer`, `writeHead`, `end`, `listen`, `write` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-jobs-events/server.rst b/docs/sphinx/modules/verae-jobs-events/server.rst new file mode 100644 index 0000000..1eb90e5 --- /dev/null +++ b/docs/sphinx/modules/verae-jobs-events/server.rst @@ -0,0 +1,7 @@ +verae-jobs-events.server +======================== + +Generated API sheet for ``verae-jobs-events/server``. + +.. include:: server.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-jobs-events/subjects.md b/docs/sphinx/modules/verae-jobs-events/subjects.md new file mode 100644 index 0000000..1083d72 --- /dev/null +++ b/docs/sphinx/modules/verae-jobs-events/subjects.md @@ -0,0 +1,36 @@ +# `verae-jobs-events/subjects` + +**Package:** `verae-jobs-events` +**Source:** `packages/verae-jobs-events/src/subjects.js` +**Lines:** 5 + +## What this module is + +Implementation module in `verae-jobs-events`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`SUBJECTS` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +_No top-level functions extracted._ + +## What it imports / requires + +_No imports detected._ + +## Call graph (identifiers invoked) + +_No local call identifiers extracted._ + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-jobs-events/subjects.rst b/docs/sphinx/modules/verae-jobs-events/subjects.rst new file mode 100644 index 0000000..6b2f825 --- /dev/null +++ b/docs/sphinx/modules/verae-jobs-events/subjects.rst @@ -0,0 +1,7 @@ +verae-jobs-events.subjects +========================== + +Generated API sheet for ``verae-jobs-events/subjects``. + +.. include:: subjects.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-nats-accounts/policy.md b/docs/sphinx/modules/verae-nats-accounts/policy.md new file mode 100644 index 0000000..7248be3 --- /dev/null +++ b/docs/sphinx/modules/verae-nats-accounts/policy.md @@ -0,0 +1,43 @@ +# `verae-nats-accounts/policy` + +**Package:** `verae-nats-accounts` +**Source:** `packages/verae-nats-accounts/src/policy.js` +**Lines:** 15 + +## What this module is + +Implementation module in `verae-nats-accounts`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`loadPolicy`, `leafAllowed` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `loadPolicy` | `(none)` | — | `unknown` | see Call graph | +| `leafAllowed` | `subject` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:fs` +- `node:path` +- `node:url` + +## Call graph (identifiers invoked) + +`join`, `dirname`, `fileURLToPath`, `loadPolicy`, `parse`, `readFileSync`, `leafAllowed`, `some`, `endsWith`, `startsWith` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-nats-accounts/policy.rst b/docs/sphinx/modules/verae-nats-accounts/policy.rst new file mode 100644 index 0000000..7264d4d --- /dev/null +++ b/docs/sphinx/modules/verae-nats-accounts/policy.rst @@ -0,0 +1,7 @@ +verae-nats-accounts.policy +========================== + +Generated API sheet for ``verae-nats-accounts/policy``. + +.. include:: policy.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-zapier-simulator/server.md b/docs/sphinx/modules/verae-zapier-simulator/server.md index ab03284..4644bf8 100644 --- a/docs/sphinx/modules/verae-zapier-simulator/server.md +++ b/docs/sphinx/modules/verae-zapier-simulator/server.md @@ -2,7 +2,7 @@ **Package:** `verae-zapier-simulator` **Source:** `packages/verae-zapier-simulator/src/server.js` -**Lines:** 87 +**Lines:** 88 ## What this module is diff --git a/docs/sphinx/modules/zappier-account-balance/books.md b/docs/sphinx/modules/zappier-account-balance/books.md index 49dc16d..5f5e467 100644 --- a/docs/sphinx/modules/zappier-account-balance/books.md +++ b/docs/sphinx/modules/zappier-account-balance/books.md @@ -2,7 +2,7 @@ **Package:** `zappier-account-balance` **Source:** `packages/zappier-account-balance/src/books.js` -**Lines:** 100 +**Lines:** 120 ## What this module is @@ -36,6 +36,8 @@ Implementation module in `zappier-account-balance`. The tables below are extract | `recordUsage` | `entry` | | `recordPayment` | `entry` | | `statement` | `customerId` | +| `dump` | `(none)` | +| `load` | `raw` | ## What it imports / requires @@ -43,7 +45,7 @@ _No imports detected._ ## Call graph (identifiers invoked) -`constructor`, `remember`, `prepaidCents`, `adjust`, `trunc`, `now`, `toString`, `random`, `slice`, `toISOString`, `unshift`, `recordUsage`, `recordPayment`, `statement`, `filter`, `match`, `handle`, `endsWith` +`constructor`, `remember`, `prepaidCents`, `adjust`, `trunc`, `now`, `toString`, `random`, `slice`, `toISOString`, `unshift`, `recordUsage`, `recordPayment`, `statement`, `filter`, `match`, `dump`, `load`, `isArray`, `handle`, `endsWith` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/zappier-account-balance/persist.md b/docs/sphinx/modules/zappier-account-balance/persist.md new file mode 100644 index 0000000..0be5a03 --- /dev/null +++ b/docs/sphinx/modules/zappier-account-balance/persist.md @@ -0,0 +1,50 @@ +# `zappier-account-balance/persist` + +**Package:** `zappier-account-balance` +**Source:** `packages/zappier-account-balance/src/persist.js` +**Lines:** 29 + +## What this module is + +Implementation module in `zappier-account-balance`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`booksPath`, `loadInto`, `saveFrom` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `booksPath` | `(none)` | — | `unknown` | see Call graph | +| `loadInto` | `books` | — | `unknown` | see Call graph | +| `saveFrom` | `books` | — | `unknown` | see Call graph | + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `return` | `process.env.BOOKS_PATH || + path.join(process.env.FLEET_STATE_DIR || process.cwd(` | + +## What it imports / requires + +- `node:fs` +- `node:path` + +## Call graph (identifiers invoked) + +`booksPath`, `join`, `cwd`, `loadInto`, `existsSync`, `load`, `parse`, `readFileSync`, `saveFrom`, `mkdirSync`, `dirname`, `writeFileSync`, `stringify`, `dump`, `renameSync` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/zappier-account-balance/persist.rst b/docs/sphinx/modules/zappier-account-balance/persist.rst new file mode 100644 index 0000000..bebe31f --- /dev/null +++ b/docs/sphinx/modules/zappier-account-balance/persist.rst @@ -0,0 +1,7 @@ +zappier-account-balance.persist +=============================== + +Generated API sheet for ``zappier-account-balance/persist``. + +.. include:: persist.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/zappier-account-balance/server.md b/docs/sphinx/modules/zappier-account-balance/server.md index 6b1858a..00d8a73 100644 --- a/docs/sphinx/modules/zappier-account-balance/server.md +++ b/docs/sphinx/modules/zappier-account-balance/server.md @@ -2,7 +2,7 @@ **Package:** `zappier-account-balance` **Source:** `packages/zappier-account-balance/src/server.js` -**Lines:** 93 +**Lines:** 101 ## What this module is @@ -20,6 +20,7 @@ _None extracted._ | Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | |------|------------|---------------------|---------|-----------------------------| +| `apply` | `subject, payload` | — | `unknown` | see Call graph | | `startNats` | `(none)` | — | `unknown` | see Call graph | | `readBody` | `req` | — | `unknown` | see Call graph | | `reply` | `sub, fn` | — | `unknown` | see Call graph | @@ -29,6 +30,7 @@ _None extracted._ | Name | Parameters | |------|------------| +| `saveFrom` | `books` | | `reply` | `nc.subscribe(SUBJECTS.STATEMENT_GET, { queue: SUBJECTS.QUEUE }` | | `reply` | `nc.subscribe(SUBJECTS.BALANCE_GET, { queue: SUBJECTS.QUEUE }` | | `reply` | `nc.subscribe(SUBJECTS.BALANCE_ADJUST, { queue: SUBJECTS.QUEUE }` | @@ -38,10 +40,11 @@ _None extracted._ - `node:http` - `./books.js` - `./subjects.js` +- `./persist.js` ## Call graph (identifiers invoked) -`startNats`, `import`, `connect`, `split`, `async`, `parse`, `decode`, `fn`, `respond`, `encode`, `stringify`, `reply`, `subscribe`, `handle`, `write`, `readBody`, `on`, `push`, `resolve`, `concat`, `toString`, `createServer`, `writeHead`, `end`, `json`, `match`, `statement`, `adjust`, `listen` +`loadInto`, `apply`, `handle`, `saveFrom`, `startNats`, `import`, `connect`, `split`, `async`, `parse`, `decode`, `fn`, `respond`, `encode`, `stringify`, `reply`, `subscribe`, `write`, `readBody`, `on`, `push`, `resolve`, `concat`, `toString`, `createServer`, `writeHead`, `end`, `json`, `match`, `statement`, `listen` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/zappier-identity/ids.md b/docs/sphinx/modules/zappier-identity/ids.md new file mode 100644 index 0000000..2628937 --- /dev/null +++ b/docs/sphinx/modules/zappier-identity/ids.md @@ -0,0 +1,41 @@ +# `zappier-identity/ids` + +**Package:** `zappier-identity` +**Source:** `packages/zappier-identity/src/ids.js` +**Lines:** 16 + +## What this module is + +Implementation module in `zappier-identity`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`stableVeraeUserId`, `bindEmail` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `stableVeraeUserId` | `username` | — | `unknown` | see Call graph | +| `bindEmail` | `email` | — | `unknown` | see Call graph | + +## What it imports / requires + +- `node:crypto` + +## Call graph (identifiers invoked) + +`stableVeraeUserId`, `trim`, `toLowerCase`, `createHash`, `update`, `digest`, `slice`, `bindEmail` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/zappier-identity/ids.rst b/docs/sphinx/modules/zappier-identity/ids.rst new file mode 100644 index 0000000..54c5eba --- /dev/null +++ b/docs/sphinx/modules/zappier-identity/ids.rst @@ -0,0 +1,7 @@ +zappier-identity.ids +==================== + +Generated API sheet for ``zappier-identity/ids``. + +.. include:: ids.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/zappier-identity/server.md b/docs/sphinx/modules/zappier-identity/server.md new file mode 100644 index 0000000..aec4169 --- /dev/null +++ b/docs/sphinx/modules/zappier-identity/server.md @@ -0,0 +1,53 @@ +# `zappier-identity/server` + +**Package:** `zappier-identity` +**Source:** `packages/zappier-identity/src/server.js` +**Lines:** 90 + +## What this module is + +Implementation module in `zappier-identity`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +_No named `export` / `module.exports` keys detected._ + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `handleBind` | `p` | — | `unknown` | see Call graph | +| `handleLookup` | `p` | — | `unknown` | see Call graph | +| `startNats` | `(none)` | — | `unknown` | see Call graph | +| `readBody` | `req` | — | `unknown` | see Call graph | +| `json` | `code, obj` | — | `unknown` | see Call graph | + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `put` | `db, { customerId, ...bound }` | + +## What it imports / requires + +- `node:http` +- `./ids.js` +- `./store.js` +- `./subjects.js` + +## Call graph (identifiers invoked) + +`load`, `handleBind`, `bindEmail`, `put`, `handleLookup`, `startNats`, `import`, `connect`, `split`, `async`, `subscribe`, `parse`, `decode`, `respond`, `encode`, `stringify`, `readBody`, `on`, `push`, `resolve`, `concat`, `toString`, `createServer`, `writeHead`, `end`, `json`, `match`, `listen`, `write` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/zappier-identity/server.rst b/docs/sphinx/modules/zappier-identity/server.rst new file mode 100644 index 0000000..e58b6f9 --- /dev/null +++ b/docs/sphinx/modules/zappier-identity/server.rst @@ -0,0 +1,7 @@ +zappier-identity.server +======================= + +Generated API sheet for ``zappier-identity/server``. + +.. include:: server.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/zappier-identity/store.md b/docs/sphinx/modules/zappier-identity/store.md new file mode 100644 index 0000000..fcdd798 --- /dev/null +++ b/docs/sphinx/modules/zappier-identity/store.md @@ -0,0 +1,50 @@ +# `zappier-identity/store` + +**Package:** `zappier-identity` +**Source:** `packages/zappier-identity/src/store.js` +**Lines:** 31 + +## What this module is + +Implementation module in `zappier-identity`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`load`, `save`, `put` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `load` | `(none)` | — | `unknown` | see Call graph | +| `save` | `db` | — | `unknown` | see Call graph | +| `put` | `db, { customerId, veraeUserId, veraeUsername }` | — | `unknown` | see Call graph | +| `file` | `(none)` | — | `unknown` | see Call graph | + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `save` | `db` | + +## What it imports / requires + +- `node:fs` +- `node:path` + +## Call graph (identifiers invoked) + +`join`, `cwd`, `load`, `file`, `existsSync`, `parse`, `readFileSync`, `save`, `mkdirSync`, `dirname`, `writeFileSync`, `stringify`, `put` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/zappier-identity/store.rst b/docs/sphinx/modules/zappier-identity/store.rst new file mode 100644 index 0000000..24b4939 --- /dev/null +++ b/docs/sphinx/modules/zappier-identity/store.rst @@ -0,0 +1,7 @@ +zappier-identity.store +====================== + +Generated API sheet for ``zappier-identity/store``. + +.. include:: store.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/zappier-identity/subjects.md b/docs/sphinx/modules/zappier-identity/subjects.md new file mode 100644 index 0000000..063e79c --- /dev/null +++ b/docs/sphinx/modules/zappier-identity/subjects.md @@ -0,0 +1,36 @@ +# `zappier-identity/subjects` + +**Package:** `zappier-identity` +**Source:** `packages/zappier-identity/src/subjects.js` +**Lines:** 6 + +## What this module is + +Implementation module in `zappier-identity`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`SUBJECTS` + +## Types / interfaces / classes + +_None extracted._ + +## Functions + +_No top-level functions extracted._ + +## What it imports / requires + +_No imports detected._ + +## Call graph (identifiers invoked) + +_No local call identifiers extracted._ + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/zappier-identity/subjects.rst b/docs/sphinx/modules/zappier-identity/subjects.rst new file mode 100644 index 0000000..c3bd456 --- /dev/null +++ b/docs/sphinx/modules/zappier-identity/subjects.rst @@ -0,0 +1,7 @@ +zappier-identity.subjects +========================= + +Generated API sheet for ``zappier-identity/subjects``. + +.. include:: subjects.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/zappier/admin.md b/docs/sphinx/modules/zappier/admin.md index 96e67cd..6180d66 100644 --- a/docs/sphinx/modules/zappier/admin.md +++ b/docs/sphinx/modules/zappier/admin.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/admin.ts` -**Lines:** 593 +**Lines:** 597 ## What this module is @@ -63,10 +63,11 @@ Implementation module in `zappier`. The tables below are extracted from the sour - `./accounting-export` - `./statement` - `./billing-nats` +- `./ledger` ## Call graph (identifiers invoked) -`tokens`, `adminLoginRouter`, `post`, `status`, `json`, `findByUsername`, `verifyPassword`, `randomBytes`, `toString`, `set`, `now`, `adminAuth`, `header`, `next`, `startsWith`, `slice`, `has`, `isValidRule`, `every`, `isValidTier`, `periodWindow`, `split`, `map`, `parseDate`, `trim`, `isNaN`, `getTime`, `escapeHtml`, `replace`, `toFixed`, `renderInvoiceHtml`, `dollars`, `join`, `toISOString`, `toUpperCase`, `adminRouter`, `get`, `getRateCard`, `getTiers`, `put`, `upsertEndpoint`, `delete`, `deleteEndpoint`, `upsertTier`, `deleteTier`, `list`, `randomUUID`, `save`, `find`, `isFinite`, `trunc`, `add`, `natsPublish`, `natsAdjust`, `async`, `natsStatement`, `composeStatement`, `listFor`, `type`, `send` +`tokens`, `adminLoginRouter`, `post`, `status`, `json`, `findByUsername`, `verifyPassword`, `randomBytes`, `toString`, `set`, `now`, `adminAuth`, `header`, `next`, `startsWith`, `slice`, `has`, `isValidRule`, `every`, `isValidTier`, `periodWindow`, `split`, `map`, `parseDate`, `trim`, `isNaN`, `getTime`, `escapeHtml`, `replace`, `toFixed`, `renderInvoiceHtml`, `dollars`, `join`, `toISOString`, `toUpperCase`, `adminRouter`, `get`, `getRateCard`, `getTiers`, `put`, `upsertEndpoint`, `delete`, `deleteEndpoint`, `upsertTier`, `deleteTier`, `list`, `randomUUID`, `save`, `find`, `async`, `isFinite`, `trunc`, `add`, `ledgerAdjust`, `booksConfigured`, `natsPublish`, `ledgerStatement`, `composeStatement`, `listFor`, `type` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/zappier/index.md b/docs/sphinx/modules/zappier/index.md index 7214325..dab278a 100644 --- a/docs/sphinx/modules/zappier/index.md +++ b/docs/sphinx/modules/zappier/index.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/index.ts` -**Lines:** 42 +**Lines:** 43 ## What this module is diff --git a/docs/sphinx/modules/zappier/ledger.md b/docs/sphinx/modules/zappier/ledger.md new file mode 100644 index 0000000..4f7e82a --- /dev/null +++ b/docs/sphinx/modules/zappier/ledger.md @@ -0,0 +1,52 @@ +# `zappier/ledger` + +**Package:** `zappier` +**Source:** `packages/zappier/src/ledger.ts` +**Lines:** 51 + +## What this module is + +Implementation module in `zappier`. The tables below are extracted from the source (signatures + JSDoc). + +## Exports + +`PrepaidRow`, `booksConfigured`, `ledgerAdjust`, `ledgerStatement` + +## Types / interfaces / classes + +| Kind | Name | +|------|------| +| type | `PrepaidRow` | + +## Functions + +| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) | +|------|------------|---------------------|---------|-----------------------------| +| `booksUrl` | `(none)` | — | `unknown` | see Call graph | +| `booksConfigured` | `(none)` | — | `unknown` | see Call graph | +| `ledgerAdjust` | `row: PrepaidRow, plane: AccessPlane` | — | `unknown` | see Call graph | +| `ledgerStatement` | `customerId: string, plane: AccessPlane, veraeUserId?: string` | — | `unknown` | see Call graph | + +## Methods (class / object) + +| Name | Parameters | +|------|------------| +| `return` | `process.env.ACCOUNT_BALANCE_URL || ''` | +| `return` | `await r.json(` | + +## What it imports / requires + +- `./billing-nats` + +## Call graph (identifiers invoked) + +`booksUrl`, `replace`, `booksConfigured`, `ledgerAdjust`, `natsAdjust`, `fetch`, `stringify`, `json`, `ledgerStatement`, `natsStatement`, `encodeURIComponent` + +Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/zappier/ledger.rst b/docs/sphinx/modules/zappier/ledger.rst new file mode 100644 index 0000000..19cd8a6 --- /dev/null +++ b/docs/sphinx/modules/zappier/ledger.rst @@ -0,0 +1,7 @@ +zappier.ledger +============== + +Generated API sheet for ``zappier/ledger``. + +.. include:: ledger.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/zappier/portal.md b/docs/sphinx/modules/zappier/portal.md index 4c31a9c..25d9df7 100644 --- a/docs/sphinx/modules/zappier/portal.md +++ b/docs/sphinx/modules/zappier/portal.md @@ -52,10 +52,11 @@ Implementation module in `zappier`. The tables below are extracted from the sour - `./statement` - `./billing-nats` - `./verae-bind` +- `./ledger` ## Call graph (identifiers invoked) -`now`, `reload`, `customer`, `publicProfile`, `sessionAuth`, `header`, `startsWith`, `slice`, `get`, `list`, `find`, `status`, `json`, `next`, `save`, `portalRouter`, `post`, `async`, `trim`, `test`, `hashPassword`, `findByEmail`, `randomBytes`, `toString`, `bindVeraeUser`, `create`, `verifyPassword`, `verifyTotp`, `use`, `delete`, `setUTCDate`, `setUTCHours`, `summaryFor`, `tiers`, `applyMonthlyCredit`, `rateCard`, `natsStatement`, `composeStatement`, `listFor`, `type`, `send`, `renderInvoiceHtml`, `generateTotpSecret`, `totpUri`, `qr`, `isInteger`, `natsPublish`, `natsAdjust`, `put` +`now`, `reload`, `customer`, `publicProfile`, `sessionAuth`, `header`, `startsWith`, `slice`, `get`, `list`, `find`, `status`, `json`, `next`, `save`, `portalRouter`, `post`, `async`, `trim`, `test`, `hashPassword`, `findByEmail`, `randomBytes`, `toString`, `bindVeraeUser`, `create`, `verifyPassword`, `verifyTotp`, `use`, `delete`, `setUTCDate`, `setUTCHours`, `summaryFor`, `tiers`, `applyMonthlyCredit`, `rateCard`, `ledgerStatement`, `composeStatement`, `listFor`, `type`, `send`, `renderInvoiceHtml`, `generateTotpSecret`, `totpUri`, `qr`, `isInteger`, `ledgerAdjust`, `booksConfigured`, `natsPublish`, `put` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/zappier/verae-bind.md b/docs/sphinx/modules/zappier/verae-bind.md index 30f3320..547bb4c 100644 --- a/docs/sphinx/modules/zappier/verae-bind.md +++ b/docs/sphinx/modules/zappier/verae-bind.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/verae-bind.ts` -**Lines:** 77 +**Lines:** 90 ## What this module is @@ -25,7 +25,7 @@ Implementation module in `zappier`. The tables below are extracted from the sour | `normalizeVeraeUsername` | `username: string` | — | `unknown` | see Call graph | | `stableVeraeUserId` | `username: string` | — | `unknown` | see Call graph | | `mockBind` | `email: string` | — | `unknown` | see Call graph | -| `bindVeraeUser` | `email: string` | — | `unknown` | see Call graph | +| `bindVeraeUser` | `email: string, customerId?: string` | — | `unknown` | see Call graph | | `login` | `username: string, pass: string` | — | `unknown` | see Call graph | ## What it imports / requires @@ -34,7 +34,7 @@ Implementation module in `zappier`. The tables below are extracted from the sour ## Call graph (identifiers invoked) -`normalizeVeraeUsername`, `trim`, `toLowerCase`, `stableVeraeUserId`, `createHash`, `update`, `digest`, `slice`, `mockBind`, `bindVeraeUser`, `replace`, `randomBytes`, `toString`, `async`, `fetch`, `stringify`, `json`, `login` +`normalizeVeraeUsername`, `trim`, `toLowerCase`, `stableVeraeUserId`, `createHash`, `update`, `digest`, `slice`, `mockBind`, `bindVeraeUser`, `replace`, `fetch`, `stringify`, `json`, `randomBytes`, `toString`, `async`, `login` Each identifier is a call site in this file. Follow the import list to see the defining module; open that module’s MD for parameter and return types. diff --git a/docs/sphinx/repos/verae-access-staff.md b/docs/sphinx/repos/verae-access-staff.md new file mode 100644 index 0000000..f34039a --- /dev/null +++ b/docs/sphinx/repos/verae-access-staff.md @@ -0,0 +1,7 @@ +# verae-access-staff + +Staff access plane for CS / sales / accounting. Credits and statement review after `verae.access.authz.check` with plane `staff`. + +**Forgejo:** https://git.georgelambert.org/marchon/verae-access-staff + +Port `:3025`. diff --git a/docs/sphinx/repos/verae-fleet.md b/docs/sphinx/repos/verae-fleet.md index aae377a..d3fe246 100644 --- a/docs/sphinx/repos/verae-fleet.md +++ b/docs/sphinx/repos/verae-fleet.md @@ -13,7 +13,7 @@ Operator control plane for Verae Time × Zapier **runtime** services: cd packages/verae-fleet npm test node src/cli.js list -node src/cli.js serve # http://127.0.0.1:3850/ +node src/cli.js serve # http://0.0.0.0:3850/ ``` Against a running daemon: diff --git a/docs/sphinx/repos/verae-jobs-events.md b/docs/sphinx/repos/verae-jobs-events.md new file mode 100644 index 0000000..936ee4d --- /dev/null +++ b/docs/sphinx/repos/verae-jobs-events.md @@ -0,0 +1,7 @@ +# verae-jobs-events + +Mailbox process for `verae.zapier.jobs.events`. Today it counts events (passthrough). Webhook routing still lives in middleware until this process takes the exclusive consumer. + +**Forgejo:** https://git.georgelambert.org/marchon/verae-jobs-events + +Port `:3030`. diff --git a/docs/sphinx/repos/verae-nats-accounts.md b/docs/sphinx/repos/verae-nats-accounts.md new file mode 100644 index 0000000..1284304 --- /dev/null +++ b/docs/sphinx/repos/verae-nats-accounts.md @@ -0,0 +1,8 @@ +# verae-nats-accounts + +NATS **account + leaf allow-list**. Leaf nodes cannot publish `verae.billing.*`. + +**Forgejo:** https://git.georgelambert.org/marchon/verae-nats-accounts + +- `policy.json` — machine-readable allow/deny (used by access-leaf). +- `nats.conf` — sketch for a future accounts-enabled nats-server (loopback). diff --git a/docs/sphinx/repos/verae-zapier-simulator.md b/docs/sphinx/repos/verae-zapier-simulator.md index 7ec71b1..4e174fe 100644 --- a/docs/sphinx/repos/verae-zapier-simulator.md +++ b/docs/sphinx/repos/verae-zapier-simulator.md @@ -7,7 +7,7 @@ Use it to validate messaging and to catch errors, delays, failures, and recoveri ```bash cd packages/verae-zapier-simulator npm test -npm start # http://127.0.0.1:3847/ +npm start # http://0.0.0.0:3847/ ``` ## What it simulates diff --git a/docs/sphinx/repos/zapier-decisions.md b/docs/sphinx/repos/zapier-decisions.md new file mode 100644 index 0000000..58e5adb --- /dev/null +++ b/docs/sphinx/repos/zapier-decisions.md @@ -0,0 +1,13 @@ +# zapier-decisions + +Lab log of architecture decisions, actions taken, and open todos while cleaning the Verae × Zapier stack. + +**Forgejo:** https://git.georgelambert.org/marchon/zapier-decisions + +| File | What | +|------|------| +| [LOG.md](LOG.md) | Chronological actions | +| [TODO.md](TODO.md) | Open items | +| [decisions/](decisions/) | One file per decision | + +No runtime. Not a NATS mailbox. diff --git a/docs/sphinx/repos/zappier-account-balance.md b/docs/sphinx/repos/zappier-account-balance.md index 0c261d6..9b1113b 100644 --- a/docs/sphinx/repos/zappier-account-balance.md +++ b/docs/sphinx/repos/zappier-account-balance.md @@ -1,6 +1,6 @@ # zappier-account-balance -Source of truth for **customer prepaid balances**. Internal traffic is **NATS request-reply**; HTTP is health plus a fallback. +Source of truth for **customer prepaid balances** (persisted `books.json`). Internal traffic is **NATS request-reply**; HTTP is health plus a fallback. zappier-edge `balanceCents` is a cache of this book. **Forgejo:** https://git.georgelambert.org/marchon/zappier-account-balance **Catalog:** https://zapier.georgelambert.org/packages/zappier-account-balance/README.pdf diff --git a/docs/sphinx/repos/zappier-identity.md b/docs/sphinx/repos/zappier-identity.md new file mode 100644 index 0000000..80c438f --- /dev/null +++ b/docs/sphinx/repos/zappier-identity.md @@ -0,0 +1,12 @@ +# zappier-identity + +Mailbox for **Verae user id bind/lookup**. Public credentials stay zappier API keys. JWTs never live here. + +**Forgejo:** https://git.georgelambert.org/marchon/zappier-identity + +| Address | Kind | +|---------|------| +| `verae.identity.bind` | request-reply `{ email, customerId }` | +| `verae.identity.lookup` | request-reply `{ customerId }` or `{ veraeUserId }` | + +HTTP `:3026` `POST /bind` `GET /lookup/:customerId`. diff --git a/packages/docs-master/modules/verae-access-staff/NATS.md b/packages/docs-master/modules/verae-access-staff/NATS.md new file mode 100644 index 0000000..0edb6e8 --- /dev/null +++ b/packages/docs-master/modules/verae-access-staff/NATS.md @@ -0,0 +1,3 @@ +# NATS — verae-access-staff + +Plane `staff`. Authz then billing statement/adjust. diff --git a/packages/docs-master/modules/verae-access-staff/SUMMARY.md b/packages/docs-master/modules/verae-access-staff/SUMMARY.md new file mode 100644 index 0000000..70e7243 --- /dev/null +++ b/packages/docs-master/modules/verae-access-staff/SUMMARY.md @@ -0,0 +1,3 @@ +# verae-access-staff + +Staff plane for credits and statement review. diff --git a/packages/docs-master/modules/verae-jobs-events/NATS.md b/packages/docs-master/modules/verae-jobs-events/NATS.md new file mode 100644 index 0000000..0996e14 --- /dev/null +++ b/packages/docs-master/modules/verae-jobs-events/NATS.md @@ -0,0 +1,3 @@ +# NATS — verae-jobs-events + +IN `verae.zapier.jobs.events` queue `jobs-events`. diff --git a/packages/docs-master/modules/verae-jobs-events/SUMMARY.md b/packages/docs-master/modules/verae-jobs-events/SUMMARY.md new file mode 100644 index 0000000..18675cf --- /dev/null +++ b/packages/docs-master/modules/verae-jobs-events/SUMMARY.md @@ -0,0 +1,3 @@ +# verae-jobs-events + +Mailbox for `verae.zapier.jobs.events` (passthrough + count). diff --git a/packages/docs-master/modules/verae-nats-accounts/NATS.md b/packages/docs-master/modules/verae-nats-accounts/NATS.md new file mode 100644 index 0000000..bfb0eb5 --- /dev/null +++ b/packages/docs-master/modules/verae-nats-accounts/NATS.md @@ -0,0 +1,3 @@ +# NATS — verae-nats-accounts + +Policy only. See `policy.json`. diff --git a/packages/docs-master/modules/verae-nats-accounts/SUMMARY.md b/packages/docs-master/modules/verae-nats-accounts/SUMMARY.md new file mode 100644 index 0000000..8de0c7a --- /dev/null +++ b/packages/docs-master/modules/verae-nats-accounts/SUMMARY.md @@ -0,0 +1,3 @@ +# verae-nats-accounts + +INTERNAL vs LEAF. Leaf deny `verae.billing.>`. diff --git a/packages/docs-master/modules/zapier-decisions/NATS.md b/packages/docs-master/modules/zapier-decisions/NATS.md new file mode 100644 index 0000000..74566e5 --- /dev/null +++ b/packages/docs-master/modules/zapier-decisions/NATS.md @@ -0,0 +1,3 @@ +# NATS — zapier-decisions + +None. diff --git a/packages/docs-master/modules/zapier-decisions/SUMMARY.md b/packages/docs-master/modules/zapier-decisions/SUMMARY.md new file mode 100644 index 0000000..7c4f63c --- /dev/null +++ b/packages/docs-master/modules/zapier-decisions/SUMMARY.md @@ -0,0 +1,3 @@ +# zapier-decisions + +Architecture decisions and action log for the lab cleanup (prepaid SoT, identity, access planes, leaf policy, fleet). diff --git a/packages/docs-master/modules/zappier-identity/NATS.md b/packages/docs-master/modules/zappier-identity/NATS.md new file mode 100644 index 0000000..ad889bb --- /dev/null +++ b/packages/docs-master/modules/zappier-identity/NATS.md @@ -0,0 +1,3 @@ +# NATS — zappier-identity + +`verae.identity.bind` / `verae.identity.lookup` queue `identity`. diff --git a/packages/docs-master/modules/zappier-identity/SUMMARY.md b/packages/docs-master/modules/zappier-identity/SUMMARY.md new file mode 100644 index 0000000..9afb4e6 --- /dev/null +++ b/packages/docs-master/modules/zappier-identity/SUMMARY.md @@ -0,0 +1,3 @@ +# zappier-identity + +Bind and lookup `veraeUserId`. JWTs never stored. diff --git a/packages/overview/01-system.md b/packages/overview/01-system.md index f887712..4874f32 100644 --- a/packages/overview/01-system.md +++ b/packages/overview/01-system.md @@ -15,15 +15,15 @@ Verae Time proves a SHA-256 existed at a given time. Zapier lets customers regis | Direct customer API | `verae-access-api` `x-api-key` | HTTPS `:3022` | | S2S leaf | `verae-access-leaf` | NATS `verae.access.leaf.in` / HTTPS `:3023` | | Authz | `verae-access-authz` | `verae.access.authz.check` before any internal subject | -| Commercial edge | `zappier-edge` portal, admin, Stripe (legacy combined) | Public HTTPS; NATS billing after authz | +| Commercial edge | `zappier-edge` portal, admin, Stripe | Loopback `:3000`; public doors are access-* | | Account balance | `zappier-account-balance` prepaid SoT | NATS `verae.billing.*` + HTTP `:3010` | | CS / sales / accounting | `zappier-customer-service`, `zappier-sales-pricing`, `zappier-accounting-export` | Private HTTP; NATS statement/adjust | -| Middleware HTTP | `/zapier/v1/*` job id + wait | Public HTTPS from edge only | +| Middleware HTTP | `/zapier/v1/*` job id + wait | HTTPS from access-zapier / loopback edge | | NATS cluster | JetStream subjects under `verae.*` | Private; loopback or SSH tunnel | | Workers | poller, webhook-deliver, aggregator | NATS + HTTPS to chain or Zapier hooks | | Archives | WORM + tree nodes | NATS broadcast query; bloom miss = silence | | Chain | Verae timestamping | HTTPS `api.veraetime.net` or MOCK | -| Control | `verae-fleet` | Operator loopback `:3850`; SSH to extra machines | +| Control | `verae-fleet` | Operator HTTP `:3850` on `0.0.0.0`; SSH to extra machines | ## Request in one sentence diff --git a/packages/overview/02-modules-and-repos.md b/packages/overview/02-modules-and-repos.md index 36c329c..c17ac66 100644 --- a/packages/overview/02-modules-and-repos.md +++ b/packages/overview/02-modules-and-repos.md @@ -13,6 +13,11 @@ Each runtime piece is its **own git repo** on Forgejo (`git.georgelambert.org`, | **verae-access-api** | `packages/verae-access-api` | Direct customer API plane (not Zapier) | | **verae-access-leaf** | `packages/verae-access-leaf` | Server-to-server NATS leaf plane | | **verae-access-zapier** | `packages/verae-access-zapier` | Zapier Platform plane only | +| **verae-access-staff** | `packages/verae-access-staff` | Staff CS/sales/accounting plane | +| **zappier-identity** | `packages/zappier-identity` | Bind/lookup `veraeUserId` | +| **verae-jobs-events** | `packages/verae-jobs-events` | Mailbox `verae.zapier.jobs.events` | +| **verae-nats-accounts** | `packages/verae-nats-accounts` | INTERNAL vs LEAF subject policy | +| **zapier-decisions** | `packages/zapier-decisions` | Decision log | | **zappier-account-balance** | `packages/zappier-account-balance` | NATS source of truth for prepaid balances | | **zappier-customer-service** | `packages/zappier-customer-service` | CS goodwill credits onto prepaid balances | | **zappier-sales-pricing** | `packages/zappier-sales-pricing` | Sales per-customer tier / multiplier | diff --git a/packages/overview/INDEX.md b/packages/overview/INDEX.md index d9140aa..0a67911 100644 --- a/packages/overview/INDEX.md +++ b/packages/overview/INDEX.md @@ -54,7 +54,7 @@ Operator console (loopback): http://127.0.0.1:3850/ · [CONSOLE.pdf](https://zap Prefix: `https://git.georgelambert.org/marchon/` -overview · verae-nats-process · verae-ops · master-zapier-plan-draft · zappier-edge · verae-access-authz · verae-access-web · verae-access-api · verae-access-leaf · verae-access-zapier · verae-middleware · verae-zapier-app · verae-activate · verae-request-splitter · verae-archive-worm · verae-archive-aggregator · verae-tree-node · verae-fleet · verae-zapier-simulator · zapier-user-docs · zapier-docs-master +overview · verae-nats-process · verae-ops · master-zapier-plan-draft · zappier-edge · verae-access-authz · verae-access-web · verae-access-api · verae-access-leaf · verae-access-zapier · verae-access-staff · zappier-identity · verae-jobs-events · verae-nats-accounts · zapier-decisions · verae-middleware · verae-zapier-app · verae-activate · verae-request-splitter · verae-archive-worm · verae-archive-aggregator · verae-tree-node · verae-fleet · verae-zapier-simulator · zapier-user-docs · zapier-docs-master ## Per-module contracts diff --git a/packages/verae-access-leaf/src/allow.js b/packages/verae-access-leaf/src/allow.js new file mode 100644 index 0000000..abd2703 --- /dev/null +++ b/packages/verae-access-leaf/src/allow.js @@ -0,0 +1,28 @@ +import fs from 'node:fs'; + +const FALLBACK = [ + 'verae.archive.put', + 'verae.archive.query', + 'verae.archive.reply.', + 'verae.zapier.jobs.watch', + 'verae.zapier.jobs.events', + 'verae.zapier.webhooks.deliver', +]; + +export function leafAllowList() { + const p = process.env.NATS_POLICY_PATH; + if (p && fs.existsSync(p)) { + try { + return JSON.parse(fs.readFileSync(p, 'utf8')).leafAllow || FALLBACK; + } catch { + return FALLBACK; + } + } + return FALLBACK; +} + +export function leafAllowed(subject) { + return leafAllowList().some((p) => + p.endsWith('.') ? subject.startsWith(p) : subject === p || subject.startsWith(`${p}.`), + ); +} diff --git a/packages/verae-access-leaf/src/server.js b/packages/verae-access-leaf/src/server.js index 7324404..82d8798 100644 --- a/packages/verae-access-leaf/src/server.js +++ b/packages/verae-access-leaf/src/server.js @@ -5,6 +5,7 @@ * then forwards only allowed internal subjects (archive + jobs, never billing). */ import http from 'node:http'; +import { leafAllowed } from './allow.js'; const PORT = Number(process.env.PORT || 3023); const AUTHZ = (process.env.AUTHZ_URL || 'http://127.0.0.1:3020').replace(/\/$/, ''); @@ -30,6 +31,9 @@ async function forward(body) { return { status: 401, body: { allow: false, reason: 'bad leaf token' } }; } const target = body.target || body.subject; + if (!leafAllowed(target)) { + return { status: 403, body: { allow: false, reason: `leaf policy denies ${target}` } }; + } const gate = await check(target, body.principal || 'leaf'); if (!gate.allow) return { status: 403, body: gate }; const url = process.env.NATS_URL; diff --git a/packages/verae-access-staff/NATS.md b/packages/verae-access-staff/NATS.md new file mode 100644 index 0000000..f33adff --- /dev/null +++ b/packages/verae-access-staff/NATS.md @@ -0,0 +1,3 @@ +# NATS — verae-access-staff + +Plane `staff`. Authz then `verae.billing.statement.get` / `balance.adjust`. diff --git a/packages/verae-access-staff/README.md b/packages/verae-access-staff/README.md new file mode 100644 index 0000000..f34039a --- /dev/null +++ b/packages/verae-access-staff/README.md @@ -0,0 +1,7 @@ +# verae-access-staff + +Staff access plane for CS / sales / accounting. Credits and statement review after `verae.access.authz.check` with plane `staff`. + +**Forgejo:** https://git.georgelambert.org/marchon/verae-access-staff + +Port `:3025`. diff --git a/packages/verae-access-staff/package.json b/packages/verae-access-staff/package.json new file mode 100644 index 0000000..d306082 --- /dev/null +++ b/packages/verae-access-staff/package.json @@ -0,0 +1,11 @@ +{ + "name": "verae-access-staff", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Staff access plane (CS/sales/accounting)", + "scripts": { + "start": "node src/server.js", + "test": "node --test test/*.test.js" + } +} diff --git a/packages/verae-access-staff/src/server.js b/packages/verae-access-staff/src/server.js new file mode 100644 index 0000000..cf4ef1d --- /dev/null +++ b/packages/verae-access-staff/src/server.js @@ -0,0 +1,57 @@ +#!/usr/bin/env node +/** Staff access plane (CS / sales / accounting). Not Zapier, not customer API. */ +import http from 'node:http'; + +const PORT = Number(process.env.PORT || 3025); +const AUTHZ = (process.env.AUTHZ_URL || 'http://127.0.0.1:3020').replace(/\/$/, ''); +const BOOKS = (process.env.ACCOUNT_BALANCE_URL || 'http://127.0.0.1:3010').replace(/\/$/, ''); +const PLANE = 'staff'; + +async function check(subject, extra = {}) { + const r = await fetch(`${AUTHZ}/check`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ plane: PLANE, subject, ...extra }), + }); + return r.json(); +} + +const server = http.createServer(async (req, res) => { + const url = new URL(req.url || '/', `http://127.0.0.1:${PORT}`); + const json = (code, obj) => { + res.writeHead(code, { 'content-type': 'application/json' }); + res.end(JSON.stringify(obj)); + }; + try { + if (req.method === 'GET' && url.pathname === '/health') { + return json(200, { ok: true, role: 'verae-access-staff', plane: PLANE }); + } + const review = url.pathname.match(/^\/review\/([^/]+)$/); + if (req.method === 'GET' && review) { + const gate = await check('verae.billing.statement.get', { principal: review[1] }); + if (!gate.allow) return json(403, gate); + const r = await fetch(`${BOOKS}/statement/${review[1]}`); + return json(r.status, { ...(await r.json()), plane: PLANE, source: 'account-balance' }); + } + if (req.method === 'POST' && url.pathname === '/credits') { + const chunks = []; + for await (const c of req) chunks.push(c); + const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'); + const gate = await check('verae.billing.balance.adjust', { kind: 'credit', principal: body.agent }); + if (!gate.allow) return json(403, gate); + const r = await fetch(`${BOOKS}/adjust`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ ...body, kind: 'credit' }), + }); + return json(r.status, { ...(await r.json()), plane: PLANE }); + } + json(404, { error: 'not found' }); + } catch (err) { + json(502, { error: err.message }); + } +}); + +server.listen(PORT, '0.0.0.0', () => { + process.stdout.write(`verae-access-staff http://0.0.0.0:${PORT}/ plane=${PLANE}\n`); +}); diff --git a/packages/verae-access-staff/test/health.test.js b/packages/verae-access-staff/test/health.test.js new file mode 100644 index 0000000..0125538 --- /dev/null +++ b/packages/verae-access-staff/test/health.test.js @@ -0,0 +1,56 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); +const authzRoot = path.join(root, '..', 'verae-access-authz'); +const booksRoot = path.join(root, '..', 'zappier-account-balance'); + +test('staff plane can credit after authz; zapier cannot', async () => { + const authzPort = 18031; + const booksPort = 18032; + const staffPort = 18033; + const authz = spawn(process.execPath, ['src/server.js'], { + cwd: authzRoot, + env: { ...process.env, PORT: String(authzPort) }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + const books = spawn(process.execPath, ['src/server.js'], { + cwd: booksRoot, + env: { ...process.env, PORT: String(booksPort), BOOKS_PATH: `/tmp/staff-books-${Date.now()}.json` }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + const staff = spawn(process.execPath, ['src/server.js'], { + cwd: root, + env: { + ...process.env, + PORT: String(staffPort), + AUTHZ_URL: `http://127.0.0.1:${authzPort}`, + ACCOUNT_BALANCE_URL: `http://127.0.0.1:${booksPort}`, + }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + await new Promise((r) => setTimeout(r, 600)); + try { + const h = await (await fetch(`http://127.0.0.1:${staffPort}/health`)).json(); + assert.equal(h.plane, 'staff'); + const add = await fetch(`http://127.0.0.1:${staffPort}/credits`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ customerId: 'c-staff', cents: 50, reason: 'test', agent: 'cs' }), + }); + assert.equal(add.status, 200); + const deny = await fetch(`http://127.0.0.1:${authzPort}/check`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ plane: 'zapier', subject: 'verae.billing.balance.adjust' }), + }); + assert.equal(deny.status, 403); + } finally { + staff.kill('SIGTERM'); + books.kill('SIGTERM'); + authz.kill('SIGTERM'); + } +}); diff --git a/packages/verae-fleet/README.md b/packages/verae-fleet/README.md index aae377a..d3fe246 100644 --- a/packages/verae-fleet/README.md +++ b/packages/verae-fleet/README.md @@ -13,7 +13,7 @@ Operator control plane for Verae Time × Zapier **runtime** services: cd packages/verae-fleet npm test node src/cli.js list -node src/cli.js serve # http://127.0.0.1:3850/ +node src/cli.js serve # http://0.0.0.0:3850/ ``` Against a running daemon: diff --git a/packages/verae-fleet/docs/CONSOLE.html b/packages/verae-fleet/docs/CONSOLE.html index 61778c2..9d47b33 100644 --- a/packages/verae-fleet/docs/CONSOLE.html +++ b/packages/verae-fleet/docs/CONSOLE.html @@ -368,9 +368,10 @@

Operator console

Operator console

-

Loopback UI at http://127.0.0.1:3850/ — Fleet, Trace -(simulator), and Docs in one shell. Not a public site. NATS stays -private; SSH identity files stay on disk.

+

Operator UI at http://0.0.0.0:3850/ (all +interfaces). Fleet, Trace (simulator), and Docs in one shell. NATS stays +private (127.0.0.1:4222); SSH identity files stay on +disk.

cd packages/verae-fleet
 node src/cli.js serve
diff --git a/packages/verae-fleet/docs/CONSOLE.md b/packages/verae-fleet/docs/CONSOLE.md index 42816e9..bcb825b 100644 --- a/packages/verae-fleet/docs/CONSOLE.md +++ b/packages/verae-fleet/docs/CONSOLE.md @@ -1,6 +1,6 @@ # Operator console -Loopback UI at **http://127.0.0.1:3850/** — Fleet, Trace (simulator), and Docs in one shell. Not a public site. NATS stays private; SSH identity files stay on disk. +Operator UI at **http://0.0.0.0:3850/** (all interfaces). Fleet, Trace (simulator), and Docs in one shell. NATS stays private (`127.0.0.1:4222`); SSH identity files stay on disk. ```bash cd packages/verae-fleet diff --git a/packages/verae-fleet/docs/CONSOLE.pdf b/packages/verae-fleet/docs/CONSOLE.pdf index f1d1b27c05f6ab96a6b2944d63440d2543759c7b..a6da356788f8427985432bb634eba7d8404323e1 100644 GIT binary patch delta 23309 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
vu+wqL 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 0) await sup.startService(id); } - const mon = new Monitor(sup); await mon.tick(); mon.start(); - startControlServer(sup, mon); - print(`fleet ${cmd} pid=${process.pid}\nUI ${BASE}/\nSIGINT stops all managed replicas`); process.on('SIGINT', async () => { mon.stop(); await sup.stopAll(); diff --git a/packages/verae-fleet/src/machines.js b/packages/verae-fleet/src/machines.js index 551bd55..0b899f7 100644 --- a/packages/verae-fleet/src/machines.js +++ b/packages/verae-fleet/src/machines.js @@ -41,7 +41,11 @@ export function loadMachines(root, overlayPath) { if (extra && typeof extra === 'object') Object.assign(m, extra); } } - return list.map(normalizeMachine); + return list.map((m) => { + const n = normalizeMachine(m); + if (n.id === 'lan-134' && process.env.FLEET_ENABLE_LAN134 !== '1') n.enabled = false; + return n; + }); } export function normalizeMachine(m) { @@ -86,9 +90,13 @@ export function canHost(machine, serviceId, role) { * @param {string} serviceId * @param {string} [role] */ -export function pickMachine(machines, instances, serviceId, role, exclude = []) { +export function pickMachine(machines, instances, serviceId, role, exclude = [], opts = {}) { const skip = new Set(exclude); - const eligible = machines.filter((m) => canHost(m, serviceId, role) && !skip.has(m.id)); + let eligible = machines.filter((m) => canHost(m, serviceId, role) && !skip.has(m.id)); + if (opts.preferLocal) { + const local = eligible.filter((m) => m.kind === 'local'); + if (local.length) eligible = local; + } if (!eligible.length) return null; const scored = eligible.map((m) => { const running = instances.filter((i) => i.machine === m.id && i.pid).length; diff --git a/packages/verae-fleet/src/server.js b/packages/verae-fleet/src/server.js index 311be8b..67843b1 100644 --- a/packages/verae-fleet/src/server.js +++ b/packages/verae-fleet/src/server.js @@ -1,6 +1,6 @@ /** * Operator HTTP: list, status, pause/resume/stop/start/restart. - * Binds 127.0.0.1 only. + * HTTP bind from fleet.json control.bind (default 0.0.0.0). * @module server */ @@ -23,7 +23,7 @@ function json(res, code, obj) { * @param {import('./monitor.js').Monitor} [mon] */ export function startControlServer(sup, mon) { - const bind = sup.loaded.control?.bind || '127.0.0.1'; + const bind = process.env.FLEET_BIND || sup.loaded.control?.bind || '0.0.0.0'; const port = Number(sup.loaded.control?.port || 3850); const sim = new Simulator(); diff --git a/packages/verae-fleet/src/ssh.js b/packages/verae-fleet/src/ssh.js index e8a86c7..d8d2871 100644 --- a/packages/verae-fleet/src/ssh.js +++ b/packages/verae-fleet/src/ssh.js @@ -57,7 +57,7 @@ export function scpBaseArgs(machine) { return args; } -export function sshExec(machine, remoteCommand, { timeoutMs = 20000 } = {}) { +export function sshExec(machine, remoteCommand, { timeoutMs = 8000 } = {}) { return new Promise((resolve, reject) => { const args = [...sshBaseArgs(machine), sshTarget(machine), remoteCommand]; const child = spawn('ssh', args, { stdio: ['ignore', 'pipe', 'pipe'] }); @@ -96,7 +96,7 @@ export function scpTo(machine, localFiles, remoteDir) { const t = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('scp timeout')); - }, 30000); + }, 8000); child.on('close', (code) => { clearTimeout(t); if (code !== 0) reject(new Error(Buffer.concat(err).toString('utf8') || `scp exit ${code}`)); diff --git a/packages/verae-fleet/src/supervisor.js b/packages/verae-fleet/src/supervisor.js index ab51f2b..92937b0 100644 --- a/packages/verae-fleet/src/supervisor.js +++ b/packages/verae-fleet/src/supervisor.js @@ -153,6 +153,7 @@ export class Supervisor { serviceId, spec.role || serviceId, opts.excludeMachines || [], + { preferLocal: Boolean(spec.spawn?.command) }, ); if (!machine) { this.log('skip', { service: serviceId, reason: 'no-machine-capacity' }); diff --git a/packages/verae-fleet/test/fleet.test.js b/packages/verae-fleet/test/fleet.test.js index c03b564..aa717eb 100644 --- a/packages/verae-fleet/test/fleet.test.js +++ b/packages/verae-fleet/test/fleet.test.js @@ -58,6 +58,9 @@ describe('verae-fleet', () => { 'access-api', 'access-leaf', 'access-zapier', + 'access-staff', + 'identity', + 'jobs-events', 'account-balance', 'customer-service', 'sales-pricing', @@ -78,6 +81,8 @@ describe('verae-fleet', () => { assert.equal(tree.keepFloor, true); assert.equal(tree.configPath, 'services/tree-node.json'); assert.equal(loaded.fleetPath, 'fleet.json'); + const lan = loaded.machines.find((m) => m.id === 'lan-134'); + if (lan) assert.equal(lan.enabled, false); }); it('starts the tree-node floor (3 available copies)', async () => { diff --git a/packages/verae-fleet/test/ssh.test.js b/packages/verae-fleet/test/ssh.test.js index 624bd74..7a1adc8 100644 --- a/packages/verae-fleet/test/ssh.test.js +++ b/packages/verae-fleet/test/ssh.test.js @@ -53,4 +53,19 @@ describe('SSH remote machine config', () => { const pick = pickMachine(machines, instances, 'tree-node', 'tree-node'); assert.equal(pick.id, 'ns1'); }); + + it('preferLocal keeps HTTP apps on the control host', () => { + const machines = [ + normalizeMachine({ id: 'local', kind: 'local', host: '127.0.0.1', capacity: 32, roles: ['*'] }), + normalizeMachine({ + id: 'ns1', + kind: 'ssh', + host: '70.88.205.138', + capacity: 24, + roles: ['*'], + }), + ]; + const pick = pickMachine(machines, [], 'account-balance', 'account-balance', [], { preferLocal: true }); + assert.equal(pick.id, 'local'); + }); }); diff --git a/packages/verae-jobs-events/NATS.md b/packages/verae-jobs-events/NATS.md new file mode 100644 index 0000000..0996e14 --- /dev/null +++ b/packages/verae-jobs-events/NATS.md @@ -0,0 +1,3 @@ +# NATS — verae-jobs-events + +IN `verae.zapier.jobs.events` queue `jobs-events`. diff --git a/packages/verae-jobs-events/README.md b/packages/verae-jobs-events/README.md new file mode 100644 index 0000000..936ee4d --- /dev/null +++ b/packages/verae-jobs-events/README.md @@ -0,0 +1,7 @@ +# verae-jobs-events + +Mailbox process for `verae.zapier.jobs.events`. Today it counts events (passthrough). Webhook routing still lives in middleware until this process takes the exclusive consumer. + +**Forgejo:** https://git.georgelambert.org/marchon/verae-jobs-events + +Port `:3030`. diff --git a/packages/verae-jobs-events/package.json b/packages/verae-jobs-events/package.json new file mode 100644 index 0000000..89faf9a --- /dev/null +++ b/packages/verae-jobs-events/package.json @@ -0,0 +1,14 @@ +{ + "name": "verae-jobs-events", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Mailbox for verae.zapier.jobs.events (passthrough)", + "scripts": { + "start": "node src/server.js", + "test": "node --test test/*.test.js" + }, + "dependencies": { + "nats": "^2.28.2" + } +} diff --git a/packages/verae-jobs-events/src/server.js b/packages/verae-jobs-events/src/server.js new file mode 100644 index 0000000..2bbdda6 --- /dev/null +++ b/packages/verae-jobs-events/src/server.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node +/** Mailbox for verae.zapier.jobs.events. Passthrough: count + optional fan-out later. */ +import http from 'node:http'; +import { SUBJECTS } from './subjects.js'; + +const PORT = Number(process.env.PORT || process.env.FLEET_HEALTH_PORT || 3030); +const BIND = process.env.FLEET_HEALTH_BIND || '0.0.0.0'; +let processed = 0; +let last = null; + +async function startNats() { + const url = process.env.NATS_URL; + if (!url) return; + const { connect, StringCodec } = await import('nats'); + const nc = await connect({ servers: url.split(','), name: 'verae-jobs-events' }); + const sc = StringCodec(); + for await (const m of nc.subscribe(SUBJECTS.IN, { queue: SUBJECTS.QUEUE })) { + processed += 1; + try { + last = JSON.parse(sc.decode(m.data) || '{}'); + } catch { + last = {}; + } + if (m.reply) m.respond(sc.encode(JSON.stringify({ ok: true, processed }))); + } +} + +const server = http.createServer((req, res) => { + const url = new URL(req.url || '/', `http://127.0.0.1:${PORT}`); + if (url.pathname === '/health') { + res.writeHead(200, { 'content-type': 'application/json' }); + res.end(JSON.stringify({ ok: true, role: 'verae-jobs-events', processed, subject: SUBJECTS.IN, lastEvent: last?.event || null })); + return; + } + res.writeHead(404, { 'content-type': 'application/json' }); + res.end(JSON.stringify({ error: 'not found' })); +}); + +server.listen(PORT, BIND, () => { + process.stdout.write(`verae-jobs-events http://${BIND}:${PORT}/\n`); +}); +startNats().catch((err) => process.stderr.write(`nats optional: ${err.message}\n`)); diff --git a/packages/verae-jobs-events/src/subjects.js b/packages/verae-jobs-events/src/subjects.js new file mode 100644 index 0000000..b9d826d --- /dev/null +++ b/packages/verae-jobs-events/src/subjects.js @@ -0,0 +1,4 @@ +export const SUBJECTS = { + IN: 'verae.zapier.jobs.events', + QUEUE: 'jobs-events', +}; diff --git a/packages/verae-jobs-events/test/health.test.js b/packages/verae-jobs-events/test/health.test.js new file mode 100644 index 0000000..71dbb2f --- /dev/null +++ b/packages/verae-jobs-events/test/health.test.js @@ -0,0 +1,23 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); + +test('jobs-events health', async () => { + const port = 18030; + const child = spawn(process.execPath, ['src/server.js'], { + cwd: root, + env: { ...process.env, PORT: String(port) }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + await new Promise((r) => setTimeout(r, 400)); + try { + const h = await (await fetch(`http://127.0.0.1:${port}/health`)).json(); + assert.equal(h.role, 'verae-jobs-events'); + } finally { + child.kill('SIGTERM'); + } +}); diff --git a/packages/verae-nats-accounts/NATS.md b/packages/verae-nats-accounts/NATS.md new file mode 100644 index 0000000..c782336 --- /dev/null +++ b/packages/verae-nats-accounts/NATS.md @@ -0,0 +1,3 @@ +# NATS — verae-nats-accounts + +Not a subscriber. Defines INTERNAL vs LEAF. Leaf deny `verae.billing.>`. diff --git a/packages/verae-nats-accounts/README.md b/packages/verae-nats-accounts/README.md new file mode 100644 index 0000000..1284304 --- /dev/null +++ b/packages/verae-nats-accounts/README.md @@ -0,0 +1,8 @@ +# verae-nats-accounts + +NATS **account + leaf allow-list**. Leaf nodes cannot publish `verae.billing.*`. + +**Forgejo:** https://git.georgelambert.org/marchon/verae-nats-accounts + +- `policy.json` — machine-readable allow/deny (used by access-leaf). +- `nats.conf` — sketch for a future accounts-enabled nats-server (loopback). diff --git a/packages/verae-nats-accounts/nats.conf b/packages/verae-nats-accounts/nats.conf new file mode 100644 index 0000000..6e2bede --- /dev/null +++ b/packages/verae-nats-accounts/nats.conf @@ -0,0 +1,21 @@ +# Lab accounts sketch. Loopback only. Do not publish 4222. +# INTERNAL: middleware, account-balance, authz, identity. +# LEAF: access-leaf and remote leaf nodes — no billing. + +listen: 127.0.0.1:4222 +http: 127.0.0.1:8222 +jetstream {} + +accounts { + INTERNAL { + users = [ { user: internal, password: internal-dev } ] + jetstream: enabled + } + LEAF { + users = [ { user: leaf, password: leaf-dev } ] + exports = [] + imports = [] + } +} +# Subject restrictions for LEAF are enforced in policy.json + access-leaf +# until this nats-server is started with full auth callout. diff --git a/packages/verae-nats-accounts/package.json b/packages/verae-nats-accounts/package.json new file mode 100644 index 0000000..0213cab --- /dev/null +++ b/packages/verae-nats-accounts/package.json @@ -0,0 +1,10 @@ +{ + "name": "verae-nats-accounts", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "NATS account and leaf subject policy (INTERNAL vs LEAF)", + "scripts": { + "test": "node --test test/*.test.js" + } +} diff --git a/packages/verae-nats-accounts/policy.json b/packages/verae-nats-accounts/policy.json new file mode 100644 index 0000000..8c5a59c --- /dev/null +++ b/packages/verae-nats-accounts/policy.json @@ -0,0 +1,35 @@ +{ + "accounts": { + "INTERNAL": { + "publish": [">"], + "subscribe": [">"] + }, + "LEAF": { + "publish": [ + "verae.access.leaf.in", + "verae.archive.put", + "verae.archive.query", + "verae.archive.reply.*", + "verae.zapier.jobs.watch", + "verae.zapier.jobs.events", + "verae.zapier.webhooks.deliver" + ], + "subscribe": [ + "verae.access.leaf.in", + "verae.archive.>", + "verae.zapier.jobs.>", + "verae.zapier.webhooks.deliver" + ], + "deny_publish": ["verae.billing.>", "verae.access.authz.>"], + "deny_subscribe": ["verae.billing.>"] + } + }, + "leafAllow": [ + "verae.archive.put", + "verae.archive.query", + "verae.archive.reply.", + "verae.zapier.jobs.watch", + "verae.zapier.jobs.events", + "verae.zapier.webhooks.deliver" + ] +} diff --git a/packages/verae-nats-accounts/src/policy.js b/packages/verae-nats-accounts/src/policy.js new file mode 100644 index 0000000..35bb5af --- /dev/null +++ b/packages/verae-nats-accounts/src/policy.js @@ -0,0 +1,14 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const FILE = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'policy.json'); + +export function loadPolicy() { + return JSON.parse(fs.readFileSync(FILE, 'utf8')); +} + +export function leafAllowed(subject) { + const allow = loadPolicy().leafAllow || []; + return allow.some((p) => (p.endsWith('.') ? subject.startsWith(p) : subject === p || subject.startsWith(`${p}.`))); +} diff --git a/packages/verae-nats-accounts/test/policy.test.js b/packages/verae-nats-accounts/test/policy.test.js new file mode 100644 index 0000000..0cc9ca0 --- /dev/null +++ b/packages/verae-nats-accounts/test/policy.test.js @@ -0,0 +1,12 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { leafAllowed, loadPolicy } from '../src/policy.js'; + +test('leaf may archive and jobs, never billing', () => { + assert.equal(leafAllowed('verae.archive.put'), true); + assert.equal(leafAllowed('verae.archive.reply.abc'), true); + assert.equal(leafAllowed('verae.zapier.jobs.watch'), true); + assert.equal(leafAllowed('verae.billing.balance.adjust'), false); + assert.equal(leafAllowed('verae.billing.statement.get'), false); + assert.ok(loadPolicy().accounts.LEAF.deny_publish.includes('verae.billing.>')); +}); diff --git a/packages/verae-ops/GETTING-STARTED.md b/packages/verae-ops/GETTING-STARTED.md index fb8253b..b142c27 100644 --- a/packages/verae-ops/GETTING-STARTED.md +++ b/packages/verae-ops/GETTING-STARTED.md @@ -11,8 +11,10 @@ This is the **top document** for install, first-up, and monitoring. Platform-spe | Piece | Job | Default | |-------|-----|---------| | **access-authz** | NATS authorization step for every access plane | `:3020` | -| **access-web / api / leaf / zapier** | Separated ingress (browser, x-api-key, S2S, Zapier) | `:3021–:3024` | -| **zappier-edge** | Billing, API keys, customer portal, admin, Stripe; meters then **proxies** to middleware | `:3000` public HTTPS | +| **access-web / api / leaf / zapier / staff** | Public HTTP doors | `:3021–:3025` | +| **identity** | `veraeUserId` bind/lookup | `:3026` | +| **zappier-edge** | Identity cache, Stripe, admin, portal (loopback only) | `127.0.0.1:3000` | +| **jobs-events** | Mailbox `verae.zapier.jobs.events` | `:3030` | | **account-balance** | NATS prepaid balances (SoT) | `:3010` | | **customer-service** | CS credit additions + review | `:3011` | | **sales-pricing** | Per-customer sales quotes | `:3012` | @@ -20,9 +22,9 @@ This is the **top document** for install, first-up, and monitoring. Platform-spe | **verae-middleware** | Zapier `/zapier/v1`, chain, NATS | `:3100` | | **NATS JetStream** | Private jobs / archive | `:4222` **not public** | | **worm ×3, tree-node ×3** | Off-chain files and Merkle leaves | fleet floor | -| **fleet** | Replica floors, SSH hosts, Trace, Docs | `:3850` loopback | +| **fleet** | Replica floors, SSH hosts, Trace, Docs | `:3850` on `0.0.0.0` | -Zapier cloud talks **only** to zappier-edge over HTTPS. +Zapier cloud talks **only** to **access-zapier** (`:3024`) over HTTPS. Browsers use access-web. Customer API keys use access-api. Staff use access-staff. zappier-edge is loopback. The billing and user platform is the standalone **zappier** project (`/Users/marchon/zappier`, Forgejo [zappier-edge](https://git.georgelambert.org/marchon/zappier-edge)): portal signup, TOTP, API keys, rate card, Stripe meter, PO invoices, admin users. Verae middleware does **not** replace it. diff --git a/packages/verae-zapier-simulator/README.md b/packages/verae-zapier-simulator/README.md index 7ec71b1..4e174fe 100644 --- a/packages/verae-zapier-simulator/README.md +++ b/packages/verae-zapier-simulator/README.md @@ -7,7 +7,7 @@ Use it to validate messaging and to catch errors, delays, failures, and recoveri ```bash cd packages/verae-zapier-simulator npm test -npm start # http://127.0.0.1:3847/ +npm start # http://0.0.0.0:3847/ ``` ## What it simulates diff --git a/packages/verae-zapier-simulator/src/server.js b/packages/verae-zapier-simulator/src/server.js index e9c0a43..f2e0886 100644 --- a/packages/verae-zapier-simulator/src/server.js +++ b/packages/verae-zapier-simulator/src/server.js @@ -1,6 +1,6 @@ /** * Local Zapier interface simulator + trace console. - * Default http://127.0.0.1:3847 + * Default http://0.0.0.0:3847 */ import http from 'node:http'; import fs from 'node:fs'; @@ -11,6 +11,7 @@ import { ACTIONS, Simulator } from './pipeline.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const PUBLIC = path.join(__dirname, '..', 'public'); const PORT = Number(process.env.SIM_PORT || 3847); +const BIND = process.env.SIM_BIND || '0.0.0.0'; const world = new Simulator(); const mime = { @@ -81,6 +82,6 @@ const server = http.createServer(async (req, res) => { } }); -server.listen(PORT, '127.0.0.1', () => { - process.stdout.write(`Zapier simulator http://127.0.0.1:${PORT}/\n`); +server.listen(PORT, BIND, () => { + process.stdout.write(`Zapier simulator http://${BIND}:${PORT}/\n`); }); diff --git a/packages/zapier-decisions/LOG.md b/packages/zapier-decisions/LOG.md new file mode 100644 index 0000000..6e65154 --- /dev/null +++ b/packages/zapier-decisions/LOG.md @@ -0,0 +1,40 @@ +# Action log + +## 2026-09-11 — bootstrap + +- Created this repo to record decisions while executing cleanup items 1–5 (and a second pass on 2–5). +- Assumption: single developer, lab only; breaking changes allowed. + +## Step 1 — account-balance is the prepaid source of truth + +- Persist books to `BOOKS_PATH` (atomic JSON). +- Mutations go through `balance.adjust` / `usage.recorded`; edge cache `balanceCents` is updated from the books reply only. +- Local cache mutate only when `NATS_URL` and `ACCOUNT_BALANCE_URL` are both unset (tests). + +## Step 2 — identity join + +- `zappier-identity` mailbox: `verae.identity.bind` / `verae.identity.lookup`. +- Portal + middleware tenants store `veraeUserId`; JWT never public. + +## Step 3 — access planes are the public doors + +- zappier-edge binds loopback (`127.0.0.1:3000`). +- Public: access-web `:3021`, access-api `:3022`, access-leaf `:3023`, access-zapier `:3024`, access-staff `:3025`. + +## Step 4 — leaf hardening + +- `verae-nats-accounts` holds nats.conf + subject policy. +- Leaf token required; billing subjects denied in policy file and access-leaf. + +## Step 5 — fleet spawn + +- Prefer local machines for HTTP `spawn.command` services. +- Disable lan-134 unless `FLEET_ENABLE_LAN134=1`. +- SSH spawn timeout 8s; failed hosts skipped. + +## Second pass (2–5) + +- Identity used from portal when `IDENTITY_URL` set. +- Leaf loads allow-list from nats-accounts `policy.json`. +- Staff plane is a first-class mailbox (`verae-access-staff`). +- jobs.events mailbox process `verae-jobs-events` (passthrough + count). diff --git a/packages/zapier-decisions/NATS.md b/packages/zapier-decisions/NATS.md new file mode 100644 index 0000000..782a476 --- /dev/null +++ b/packages/zapier-decisions/NATS.md @@ -0,0 +1,3 @@ +# NATS — zapier-decisions + +None. Documentation repo. diff --git a/packages/zapier-decisions/README.md b/packages/zapier-decisions/README.md new file mode 100644 index 0000000..58e5adb --- /dev/null +++ b/packages/zapier-decisions/README.md @@ -0,0 +1,13 @@ +# zapier-decisions + +Lab log of architecture decisions, actions taken, and open todos while cleaning the Verae × Zapier stack. + +**Forgejo:** https://git.georgelambert.org/marchon/zapier-decisions + +| File | What | +|------|------| +| [LOG.md](LOG.md) | Chronological actions | +| [TODO.md](TODO.md) | Open items | +| [decisions/](decisions/) | One file per decision | + +No runtime. Not a NATS mailbox. diff --git a/packages/zapier-decisions/TODO.md b/packages/zapier-decisions/TODO.md new file mode 100644 index 0000000..068fd3f --- /dev/null +++ b/packages/zapier-decisions/TODO.md @@ -0,0 +1,8 @@ +# Open todos + +- [ ] Live `api.veraetime.net` with `MOCK_VERAE=false` and admin bind credentials. +- [ ] NATS nkeys/mTLS on a real three-node cluster (accounts file is the lab stand-in). +- [ ] Exclusive JetStream consumer for `verae.zapier.jobs.events` on `verae-jobs-events` (middleware still also listens). +- [ ] Auth on CS/sales/accounting HTML (staff plane is the intended door). +- [ ] Zapier Platform `push` of a private app. +- [ ] Move portal static files fully into `verae-access-web` (today it proxies `/portal` to loopback edge). diff --git a/packages/zapier-decisions/decisions/D001-prepaid-sot.md b/packages/zapier-decisions/decisions/D001-prepaid-sot.md new file mode 100644 index 0000000..9b1bace --- /dev/null +++ b/packages/zapier-decisions/decisions/D001-prepaid-sot.md @@ -0,0 +1,6 @@ +# D001 — Account-balance is the only prepaid writer + +**Status:** accepted +**Step:** 1 + +Prepaid cents, credits, usage, and payments persist in `zappier-account-balance` (`books.json`). zappier-edge `balanceCents` is a cache filled from the books reply. Tests without NATS/HTTP books still mutate the cache so unit tests stay hermetic. diff --git a/packages/zapier-decisions/decisions/D002-identity.md b/packages/zapier-decisions/decisions/D002-identity.md new file mode 100644 index 0000000..7548947 --- /dev/null +++ b/packages/zapier-decisions/decisions/D002-identity.md @@ -0,0 +1,6 @@ +# D002 — veraeUserId is the join key, not the JWT + +**Status:** accepted +**Step:** 2 + +Public token remains the zappier API key. Chain JWT stays server-side (`tokenRef`). `zappier-identity` is the mailbox for bind/lookup so portal and middleware share one map. diff --git a/packages/zapier-decisions/decisions/D003-public-doors.md b/packages/zapier-decisions/decisions/D003-public-doors.md new file mode 100644 index 0000000..9d32527 --- /dev/null +++ b/packages/zapier-decisions/decisions/D003-public-doors.md @@ -0,0 +1,6 @@ +# D003 — Access planes are the only public HTTP doors + +**Status:** accepted +**Step:** 3 + +Zapier → `:3024`, browser → `:3021`, customer API → `:3022`, leaf → `:3023`, staff → `:3025`. zappier-edge listens on `127.0.0.1:3000` for identity, Stripe, admin, and as upstream for those planes. diff --git a/packages/zapier-decisions/decisions/D004-leaf-accounts.md b/packages/zapier-decisions/decisions/D004-leaf-accounts.md new file mode 100644 index 0000000..65de80d --- /dev/null +++ b/packages/zapier-decisions/decisions/D004-leaf-accounts.md @@ -0,0 +1,6 @@ +# D004 — Leaf cannot touch billing, even if authz is skipped + +**Status:** accepted +**Step:** 4 + +`verae-nats-accounts/policy.json` is the allow-list. access-leaf refuses any target not in the leaf set. nats.conf documents INTERNAL vs LEAF accounts for when the lab cluster is replaced with a real accounts-enabled nats-server. diff --git a/packages/zapier-decisions/decisions/D005-fleet-local-first.md b/packages/zapier-decisions/decisions/D005-fleet-local-first.md new file mode 100644 index 0000000..4df6ecd --- /dev/null +++ b/packages/zapier-decisions/decisions/D005-fleet-local-first.md @@ -0,0 +1,6 @@ +# D005 — HTTP services spawn locally; lan-134 stays off + +**Status:** accepted +**Step:** 5 + +SSH to `70.88.205.134` blocked keepFloor. HTTP apps (`spawn.command`) prefer `kind=local`. `lan-134` is disabled unless `FLEET_ENABLE_LAN134=1`. Failed SSH hosts are skipped after 8s. diff --git a/packages/zapier-decisions/package.json b/packages/zapier-decisions/package.json new file mode 100644 index 0000000..a12c852 --- /dev/null +++ b/packages/zapier-decisions/package.json @@ -0,0 +1,6 @@ +{ + "name": "zapier-decisions", + "version": "0.1.0", + "private": true, + "description": "Architecture decisions and action log for the Verae × Zapier stack" +} diff --git a/packages/zappier-account-balance/README.md b/packages/zappier-account-balance/README.md index 0c261d6..9b1113b 100644 --- a/packages/zappier-account-balance/README.md +++ b/packages/zappier-account-balance/README.md @@ -1,6 +1,6 @@ # zappier-account-balance -Source of truth for **customer prepaid balances**. Internal traffic is **NATS request-reply**; HTTP is health plus a fallback. +Source of truth for **customer prepaid balances** (persisted `books.json`). Internal traffic is **NATS request-reply**; HTTP is health plus a fallback. zappier-edge `balanceCents` is a cache of this book. **Forgejo:** https://git.georgelambert.org/marchon/zappier-account-balance **Catalog:** https://zapier.georgelambert.org/packages/zappier-account-balance/README.pdf diff --git a/packages/zappier-account-balance/src/books.js b/packages/zappier-account-balance/src/books.js index e715911..91119f4 100644 --- a/packages/zappier-account-balance/src/books.js +++ b/packages/zappier-account-balance/src/books.js @@ -78,6 +78,26 @@ export class AccountBooks { payments: match(this.payments), }; } + + dump() { + return { + prepaid: this.prepaid, + veraeUserIds: this.veraeUserIds, + credits: this.credits, + usage: this.usage, + payments: this.payments, + }; + } + + load(raw) { + if (!raw || typeof raw !== 'object') return this; + this.prepaid = raw.prepaid || {}; + this.veraeUserIds = raw.veraeUserIds || {}; + this.credits = Array.isArray(raw.credits) ? raw.credits : []; + this.usage = Array.isArray(raw.usage) ? raw.usage : []; + this.payments = Array.isArray(raw.payments) ? raw.payments : []; + return this; + } } export function handle(subject, payload, books) { diff --git a/packages/zappier-account-balance/src/persist.js b/packages/zappier-account-balance/src/persist.js new file mode 100644 index 0000000..9b857ff --- /dev/null +++ b/packages/zappier-account-balance/src/persist.js @@ -0,0 +1,28 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +export function booksPath() { + return ( + process.env.BOOKS_PATH || + path.join(process.env.FLEET_STATE_DIR || process.cwd(), 'data', 'books.json') + ); +} + +export function loadInto(books) { + const p = booksPath(); + if (!fs.existsSync(p)) return books; + try { + books.load(JSON.parse(fs.readFileSync(p, 'utf8'))); + } catch { + /* keep empty */ + } + return books; +} + +export function saveFrom(books) { + const p = booksPath(); + fs.mkdirSync(path.dirname(p), { recursive: true }); + const tmp = `${p}.tmp`; + fs.writeFileSync(tmp, JSON.stringify(books.dump(), null, 2)); + fs.renameSync(tmp, p); +} diff --git a/packages/zappier-account-balance/src/server.js b/packages/zappier-account-balance/src/server.js index 0ec0204..b20d567 100644 --- a/packages/zappier-account-balance/src/server.js +++ b/packages/zappier-account-balance/src/server.js @@ -2,10 +2,17 @@ import http from 'node:http'; import { AccountBooks, handle } from './books.js'; import { SUBJECTS } from './subjects.js'; +import { loadInto, saveFrom } from './persist.js'; const PORT = Number(process.env.PORT || process.env.FLEET_HEALTH_PORT || 3010); const BIND = process.env.FLEET_HEALTH_BIND || '0.0.0.0'; -const books = new AccountBooks(); +const books = loadInto(new AccountBooks()); + +function apply(subject, payload) { + const out = handle(subject, payload, books); + saveFrom(books); + return out; +} let natsOk = false; async function startNats() { @@ -23,21 +30,22 @@ async function startNats() { payload = {}; } const out = fn(payload); + saveFrom(books); if (m.reply) m.respond(sc.encode(JSON.stringify(out))); } }; reply(nc.subscribe(SUBJECTS.STATEMENT_GET, { queue: SUBJECTS.QUEUE }), (p) => - handle(SUBJECTS.STATEMENT_GET, p, books), + apply(SUBJECTS.STATEMENT_GET, p), ); reply(nc.subscribe(SUBJECTS.BALANCE_GET, { queue: SUBJECTS.QUEUE }), (p) => - handle(SUBJECTS.BALANCE_GET, p, books), + apply(SUBJECTS.BALANCE_GET, p), ); reply(nc.subscribe(SUBJECTS.BALANCE_ADJUST, { queue: SUBJECTS.QUEUE }), (p) => - handle(SUBJECTS.BALANCE_ADJUST, p, books), + apply(SUBJECTS.BALANCE_ADJUST, p), ); (async () => { for await (const m of nc.subscribe(SUBJECTS.USAGE_RECORDED)) { - handle(SUBJECTS.USAGE_RECORDED, JSON.parse(sc.decode(m.data) || '{}'), books); + apply(SUBJECTS.USAGE_RECORDED, JSON.parse(sc.decode(m.data) || '{}')); } })(); // payment.recorded / credit.applied are fan-out events. Prepaid mutations @@ -76,7 +84,7 @@ const server = http.createServer(async (req, res) => { } if (req.method === 'POST' && url.pathname === '/adjust') { const body = await readBody(req); - return json(200, books.adjust(body)); + return json(200, apply(SUBJECTS.BALANCE_ADJUST, body)); } json(404, { error: 'not found' }); } catch (err) { diff --git a/packages/zappier-account-balance/test/books.test.js b/packages/zappier-account-balance/test/books.test.js index 48e0583..230fc55 100644 --- a/packages/zappier-account-balance/test/books.test.js +++ b/packages/zappier-account-balance/test/books.test.js @@ -1,7 +1,11 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; import { AccountBooks, handle } from '../src/books.js'; import { SUBJECTS } from '../src/subjects.js'; +import { loadInto, saveFrom, booksPath } from '../src/persist.js'; test('adjust credits prepaid and statement lists credits usage payments', () => { const books = new AccountBooks(); @@ -19,3 +23,15 @@ test('adjust credits prepaid and statement lists credits usage payments', () => assert.equal(st.usage[0].endpointId, 'timestamp'); assert.equal(st.payments[0].kind, 'payment'); }); + +test('persist round-trip keeps prepaid', () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'books-')); + process.env.BOOKS_PATH = path.join(dir, 'books.json'); + const a = new AccountBooks(); + handle(SUBJECTS.BALANCE_ADJUST, { customerId: 'c9', cents: 120, kind: 'reload' }, a); + saveFrom(a); + assert.ok(fs.existsSync(booksPath())); + const b = loadInto(new AccountBooks()); + assert.equal(b.prepaidCents('c9'), 120); + delete process.env.BOOKS_PATH; +}); diff --git a/packages/zappier-identity/NATS.md b/packages/zappier-identity/NATS.md new file mode 100644 index 0000000..d7f3166 --- /dev/null +++ b/packages/zappier-identity/NATS.md @@ -0,0 +1,3 @@ +# NATS — zappier-identity + +Queue `identity`. Subjects `verae.identity.bind`, `verae.identity.lookup`. diff --git a/packages/zappier-identity/README.md b/packages/zappier-identity/README.md new file mode 100644 index 0000000..80c438f --- /dev/null +++ b/packages/zappier-identity/README.md @@ -0,0 +1,12 @@ +# zappier-identity + +Mailbox for **Verae user id bind/lookup**. Public credentials stay zappier API keys. JWTs never live here. + +**Forgejo:** https://git.georgelambert.org/marchon/zappier-identity + +| Address | Kind | +|---------|------| +| `verae.identity.bind` | request-reply `{ email, customerId }` | +| `verae.identity.lookup` | request-reply `{ customerId }` or `{ veraeUserId }` | + +HTTP `:3026` `POST /bind` `GET /lookup/:customerId`. diff --git a/packages/zappier-identity/package.json b/packages/zappier-identity/package.json new file mode 100644 index 0000000..aa4e29a --- /dev/null +++ b/packages/zappier-identity/package.json @@ -0,0 +1,14 @@ +{ + "name": "zappier-identity", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Mailbox: bind and lookup veraeUserId (not JWTs)", + "scripts": { + "start": "node src/server.js", + "test": "node --test test/*.test.js" + }, + "dependencies": { + "nats": "^2.28.2" + } +} diff --git a/packages/zappier-identity/src/ids.js b/packages/zappier-identity/src/ids.js new file mode 100644 index 0000000..bf9d46f --- /dev/null +++ b/packages/zappier-identity/src/ids.js @@ -0,0 +1,15 @@ +import { createHash } from 'node:crypto'; + +export function stableVeraeUserId(username) { + const n = String(username || '') + .trim() + .toLowerCase(); + return `vu_${createHash('sha256').update(n).digest('hex').slice(0, 16)}`; +} + +export function bindEmail(email) { + const veraeUsername = String(email || '') + .trim() + .toLowerCase(); + return { veraeUserId: stableVeraeUserId(veraeUsername), veraeUsername, bound: true }; +} diff --git a/packages/zappier-identity/src/server.js b/packages/zappier-identity/src/server.js new file mode 100644 index 0000000..4f1138e --- /dev/null +++ b/packages/zappier-identity/src/server.js @@ -0,0 +1,89 @@ +#!/usr/bin/env node +/** Mailbox: bind/lookup veraeUserId. JWT never stored here. */ +import http from 'node:http'; +import { bindEmail } from './ids.js'; +import { load, put } from './store.js'; +import { SUBJECTS } from './subjects.js'; + +const PORT = Number(process.env.PORT || 3026); +const BIND = process.env.FLEET_HEALTH_BIND || '0.0.0.0'; +const db = load(); + +function handleBind(p) { + const email = p.email || p.veraeUsername; + const bound = bindEmail(email); + const customerId = p.customerId || bound.veraeUsername; + put(db, { customerId, ...bound }); + return { ...bound, customerId }; +} + +function handleLookup(p) { + if (p.customerId && db.byCustomer[p.customerId]) return db.byCustomer[p.customerId]; + if (p.veraeUserId && db.byVerae[p.veraeUserId]) return db.byVerae[p.veraeUserId]; + if (p.email) return bindEmail(p.email); + return { error: 'not found' }; +} + +async function startNats() { + const url = process.env.NATS_URL; + if (!url) return; + const { connect, StringCodec } = await import('nats'); + const nc = await connect({ servers: url.split(','), name: 'zappier-identity' }); + const sc = StringCodec(); + for (const subj of [SUBJECTS.BIND, SUBJECTS.LOOKUP]) { + (async () => { + for await (const m of nc.subscribe(subj, { queue: SUBJECTS.QUEUE })) { + let p = {}; + try { + p = JSON.parse(sc.decode(m.data) || '{}'); + } catch { + p = {}; + } + const out = subj === SUBJECTS.BIND ? handleBind(p) : handleLookup(p); + if (m.reply) m.respond(sc.encode(JSON.stringify(out))); + } + })(); + } +} + +function readBody(req) { + return new Promise((resolve) => { + const chunks = []; + req.on('data', (c) => chunks.push(c)); + req.on('end', () => { + try { + resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')); + } catch { + resolve({}); + } + }); + }); +} + +const server = http.createServer(async (req, res) => { + const url = new URL(req.url || '/', `http://127.0.0.1:${PORT}`); + const json = (code, obj) => { + res.writeHead(code, { 'content-type': 'application/json' }); + res.end(JSON.stringify(obj)); + }; + try { + if (req.method === 'GET' && url.pathname === '/health') { + return json(200, { ok: true, role: 'zappier-identity', subjects: SUBJECTS }); + } + if (req.method === 'POST' && url.pathname === '/bind') { + return json(200, handleBind(await readBody(req))); + } + const who = url.pathname.match(/^\/lookup\/([^/]+)$/); + if (req.method === 'GET' && who) { + return json(200, handleLookup({ customerId: who[1] })); + } + json(404, { error: 'not found' }); + } catch (err) { + json(500, { error: err.message }); + } +}); + +server.listen(PORT, BIND, () => { + process.stdout.write(`zappier-identity http://${BIND}:${PORT}/\n`); +}); +startNats().catch((err) => process.stderr.write(`nats optional: ${err.message}\n`)); diff --git a/packages/zappier-identity/src/store.js b/packages/zappier-identity/src/store.js new file mode 100644 index 0000000..6371ae8 --- /dev/null +++ b/packages/zappier-identity/src/store.js @@ -0,0 +1,30 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const file = () => + process.env.IDENTITY_PATH || + path.join(process.env.FLEET_STATE_DIR || process.cwd(), 'data', 'identity.json'); + +export function load() { + const p = file(); + if (!fs.existsSync(p)) return { byCustomer: {}, byVerae: {} }; + try { + return { byCustomer: {}, byVerae: {}, ...JSON.parse(fs.readFileSync(p, 'utf8')) }; + } catch { + return { byCustomer: {}, byVerae: {} }; + } +} + +export function save(db) { + const p = file(); + fs.mkdirSync(path.dirname(p), { recursive: true }); + fs.writeFileSync(p, JSON.stringify(db, null, 2)); +} + +export function put(db, { customerId, veraeUserId, veraeUsername }) { + if (!customerId || !veraeUserId) return db; + db.byCustomer[customerId] = { veraeUserId, veraeUsername }; + db.byVerae[veraeUserId] = { customerId, veraeUsername }; + save(db); + return db; +} diff --git a/packages/zappier-identity/src/subjects.js b/packages/zappier-identity/src/subjects.js new file mode 100644 index 0000000..821e7c2 --- /dev/null +++ b/packages/zappier-identity/src/subjects.js @@ -0,0 +1,5 @@ +export const SUBJECTS = { + BIND: 'verae.identity.bind', + LOOKUP: 'verae.identity.lookup', + QUEUE: 'identity', +}; diff --git a/packages/zappier-identity/test/health.test.js b/packages/zappier-identity/test/health.test.js new file mode 100644 index 0000000..2c810f8 --- /dev/null +++ b/packages/zappier-identity/test/health.test.js @@ -0,0 +1,36 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { stableVeraeUserId } from '../src/ids.js'; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); + +test('identity bind is stable and not a JWT', async () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'id-')); + const port = 18026; + const child = spawn(process.execPath, ['src/server.js'], { + cwd: root, + env: { ...process.env, PORT: String(port), IDENTITY_PATH: path.join(dir, 'id.json') }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + await new Promise((r) => setTimeout(r, 400)); + try { + const h = await (await fetch(`http://127.0.0.1:${port}/health`)).json(); + assert.equal(h.role, 'zappier-identity'); + const b = await ( + await fetch(`http://127.0.0.1:${port}/bind`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ email: 'Ada@Example.com', customerId: 'cust_x' }), + }) + ).json(); + assert.equal(b.veraeUserId, stableVeraeUserId('ada@example.com')); + assert.doesNotMatch(JSON.stringify(b), /eyJ|mock-jwt/); + } finally { + child.kill('SIGTERM'); + } +}); diff --git a/packages/zappier/src/admin.ts b/packages/zappier/src/admin.ts index 03264c6..9bf6692 100644 --- a/packages/zappier/src/admin.ts +++ b/packages/zappier/src/admin.ts @@ -13,7 +13,8 @@ import { UsageRepo } from './usage'; import { CreditLedger } from './credits'; import { invoicesToAccountingCsv, invoicesToQuickBooksIif } from './accounting-export'; import { composeStatement } from './statement'; -import { BILLING_SUBJECTS, natsAdjust, natsPublish, natsStatement } from './billing-nats'; +import { BILLING_SUBJECTS, natsPublish } from './billing-nats'; +import { booksConfigured, ledgerAdjust, ledgerStatement } from './ledger'; // Issued login tokens (in-memory; a restart simply requires logging in again). const sessions = new Map(); @@ -229,7 +230,7 @@ export function adminRouter( /* ---------------- customer service: credits ---------------- */ - router.post('/credits', (req, res) => { + router.post('/credits', async (req, res) => { const { customerId, cents, reason, agent } = req.body ?? {}; if (typeof customerId !== 'string' || !Number.isFinite(Number(cents))) { res.status(400).json({ error: 'customerId and cents required' }); @@ -247,9 +248,7 @@ export function adminRouter( reason: typeof reason === 'string' ? reason : 'credit adjustment', agent: typeof agent === 'string' ? agent : 'admin', }); - customers.save({ ...customer, balanceCents: (customer.balanceCents ?? 0) + delta }); - natsPublish(BILLING_SUBJECTS.CREDIT_APPLIED, { ...rec, veraeUserId: customer.veraeUserId }, 'staff'); - void natsAdjust( + const row = await ledgerAdjust( { customerId, veraeUserId: customer.veraeUserId, @@ -260,7 +259,12 @@ export function adminRouter( }, 'staff', ); - res.status(201).json(rec); + let prepaid = customer.balanceCents ?? 0; + if (row && typeof row.prepaidCents === 'number') prepaid = row.prepaidCents; + else if (!booksConfigured()) prepaid += delta; + customers.save({ ...customer, balanceCents: prepaid }); + natsPublish(BILLING_SUBJECTS.CREDIT_APPLIED, { ...rec, veraeUserId: customer.veraeUserId }, 'staff'); + res.status(201).json({ ...rec, prepaidCents: prepaid }); }); router.get('/credits', (req, res) => { @@ -274,9 +278,9 @@ export function adminRouter( res.status(404).json({ error: 'customer not found' }); return; } - const fromNats = await natsStatement(customer.id, 'staff', customer.veraeUserId); - if (fromNats) { - res.json({ ...fromNats, name: customer.name, tierId: customer.tierId, source: 'nats' }); + const fromBooks = await ledgerStatement(customer.id, 'staff', customer.veraeUserId); + if (fromBooks) { + res.json({ ...fromBooks, name: customer.name, tierId: customer.tierId, source: fromBooks.source || 'account-balance' }); return; } res.json({ diff --git a/packages/zappier/src/index.ts b/packages/zappier/src/index.ts index 998bd56..f3429f4 100644 --- a/packages/zappier/src/index.ts +++ b/packages/zappier/src/index.ts @@ -33,8 +33,9 @@ const payments: PaymentClient = hasRealStripeKey(process.env.STRIPE_SECRET_KEY) const { app } = buildApp({ usage, customers, pricingStore, invoices, adminUsers, sessions, payments }); const port = Number(process.env.PORT ?? 3000); -app.listen(port, () => { - console.log(`Zappier API listening on http://localhost:${port}`); +const bind = process.env.BIND ?? '127.0.0.1'; +app.listen(port, bind, () => { + console.log(`Zappier API listening on http://${bind}:${port}`); console.log(`OpenAPI docs at http://localhost:${port}/docs`); console.log(`Customer portal at http://localhost:${port}/portal`); console.log(`Admin console at http://localhost:${port}/admin`); diff --git a/packages/zappier/src/ledger.ts b/packages/zappier/src/ledger.ts new file mode 100644 index 0000000..5905f24 --- /dev/null +++ b/packages/zappier/src/ledger.ts @@ -0,0 +1,50 @@ +/** + * Prepaid mutations. Account-balance is the writer when NATS or HTTP books exist. + * Tests (no NATS_URL, no ACCOUNT_BALANCE_URL) keep a local cache only. + */ +import { natsAdjust, natsStatement } from './billing-nats'; +import type { AccessPlane } from './billing-nats'; + +export type PrepaidRow = { + customerId: string; + veraeUserId?: string; + cents: number; + reason: string; + agent: string; + kind?: string; + prepaidCents?: number; +}; + +function booksUrl(): string { + return (process.env.ACCOUNT_BALANCE_URL || '').replace(/\/$/, ''); +} + +export function booksConfigured(): boolean { + return Boolean(process.env.NATS_URL || booksUrl()); +} + +export async function ledgerAdjust(row: PrepaidRow, plane: AccessPlane): Promise { + const viaNats = await natsAdjust(row, plane); + if (viaNats && typeof viaNats === 'object' && viaNats !== null && 'prepaidCents' in viaNats) { + return viaNats as PrepaidRow; + } + const base = booksUrl(); + if (!base) return null; + const r = await fetch(`${base}/adjust`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(row), + }); + if (!r.ok) return null; + return (await r.json()) as PrepaidRow; +} + +export async function ledgerStatement(customerId: string, plane: AccessPlane, veraeUserId?: string) { + const nats = await natsStatement(customerId, plane, veraeUserId); + if (nats) return nats; + const base = booksUrl(); + if (!base) return null; + const r = await fetch(`${base}/statement/${encodeURIComponent(customerId)}`); + if (!r.ok) return null; + return r.json(); +} diff --git a/packages/zappier/src/portal.ts b/packages/zappier/src/portal.ts index b78bd95..740e256 100644 --- a/packages/zappier/src/portal.ts +++ b/packages/zappier/src/portal.ts @@ -16,8 +16,9 @@ import { RateCard, TierConfig } from './pricing'; import { UsageRepo } from './usage'; import { CreditLedger } from './credits'; import { composeStatement } from './statement'; -import { BILLING_SUBJECTS, natsPublish, natsStatement, natsAdjust } from './billing-nats'; +import { BILLING_SUBJECTS, natsPublish } from './billing-nats'; import { bindVeraeUser } from './verae-bind'; +import { booksConfigured, ledgerAdjust, ledgerStatement } from './ledger'; /** * Customer portal API (/portal/api): signup, login with optional TOTP 2FA, @@ -209,9 +210,9 @@ export function portalRouter(deps: PortalDeps): Router { }); router.get('/statement', async (req, res) => { - const fromNats = await natsStatement(req.customer!.id, 'web', req.customer!.veraeUserId); - if (fromNats) { - res.json({ ...fromNats, source: 'nats' }); + const fromBooks = await ledgerStatement(req.customer!.id, 'web', req.customer!.veraeUserId); + if (fromBooks) { + res.json({ ...fromBooks, source: fromBooks.source || 'account-balance' }); return; } res.json({ @@ -287,26 +288,12 @@ export function portalRouter(deps: PortalDeps): Router { return; } const result = await deps.payments.reload(req.customer!, amountCents); - const customer = { - ...req.customer!, - balanceCents: (req.customer!.balanceCents ?? 0) + result.creditedCents, - }; - if (result.creditedCents > 0) save(deps, customer); + let prepaid = req.customer!.balanceCents ?? 0; if (result.creditedCents > 0) { - natsPublish( - BILLING_SUBJECTS.PAYMENT_RECORDED, + const row = await ledgerAdjust( { - customerId: customer.id, - veraeUserId: customer.veraeUserId, - cents: result.creditedCents, - reason: 'reload', - }, - 'web', - ); - void natsAdjust( - { - customerId: customer.id, - veraeUserId: customer.veraeUserId, + customerId: req.customer!.id, + veraeUserId: req.customer!.veraeUserId, cents: result.creditedCents, reason: 'reload', agent: 'portal', @@ -314,9 +301,22 @@ export function portalRouter(deps: PortalDeps): Router { }, 'web', ); + if (row && typeof row.prepaidCents === 'number') prepaid = row.prepaidCents; + else if (!booksConfigured()) prepaid += result.creditedCents; + save(deps, { ...req.customer!, balanceCents: prepaid }); + natsPublish( + BILLING_SUBJECTS.PAYMENT_RECORDED, + { + customerId: req.customer!.id, + veraeUserId: req.customer!.veraeUserId, + cents: result.creditedCents, + reason: 'reload', + }, + 'web', + ); } res.json({ - balanceCents: customer.balanceCents, + balanceCents: prepaid, mode: result.mode, ...(result.clientSecret ? { clientSecret: result.clientSecret } : {}), }); diff --git a/packages/zappier/src/verae-bind.ts b/packages/zappier/src/verae-bind.ts index 04dffb7..9d07ba3 100644 --- a/packages/zappier/src/verae-bind.ts +++ b/packages/zappier/src/verae-bind.ts @@ -28,7 +28,20 @@ function mockBind(email: string): VeraeBind { * Register or look up the customer on api.veraetime.net. * MOCK_VERAE (default) or missing VERAE_API_BASE_URL → stable id, no network. */ -export async function bindVeraeUser(email: string): Promise { +export async function bindVeraeUser(email: string, customerId?: string): Promise { + const identity = (process.env.IDENTITY_URL || '').replace(/\/$/, ''); + if (identity) { + try { + const r = await fetch(`${identity}/bind`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ email, customerId }), + }); + if (r.ok) return (await r.json()) as VeraeBind; + } catch { + /* fall through */ + } + } const mock = process.env.MOCK_VERAE !== 'false'; const base = (process.env.VERAE_API_BASE_URL || '').replace(/\/$/, ''); if (mock || !base) return mockBind(email); diff --git a/scripts/build-docs-site.py b/scripts/build-docs-site.py index eb703cd..361f920 100755 --- a/scripts/build-docs-site.py +++ b/scripts/build-docs-site.py @@ -80,6 +80,11 @@ SECTIONS = [ ("packages/verae-access-api/README.md", "Direct customer API plane"), ("packages/verae-access-leaf/README.md", "S2S NATS leaf plane"), ("packages/verae-access-zapier/README.md", "Zapier-only access plane"), + ("packages/verae-access-staff/README.md", "Staff access plane"), + ("packages/zappier-identity/README.md", "veraeUserId identity mailbox"), + ("packages/verae-jobs-events/README.md", "jobs.events mailbox"), + ("packages/verae-nats-accounts/README.md", "NATS leaf/internal policy"), + ("packages/zapier-decisions/README.md", "Decisions log"), ], ), ( @@ -341,6 +346,11 @@ def main() -> None: "verae-access-api", "verae-access-leaf", "verae-access-zapier", + "verae-access-staff", + "zappier-identity", + "verae-jobs-events", + "verae-nats-accounts", + "zapier-decisions", ): pkg_root = ROOT / "packages" / pkg if pkg in {"zapier-user-docs", "overview", "docs-master", "verae-ops"}: @@ -508,6 +518,11 @@ def main() -> None: ("verae-access-api", "Direct customer API"), ("verae-access-leaf", "S2S NATS leaf"), ("verae-access-zapier", "Zapier access plane"), + ("verae-access-staff", "Staff access plane"), + ("zappier-identity", "veraeUserId bind/lookup"), + ("verae-jobs-events", "jobs.events mailbox"), + ("verae-nats-accounts", "NATS leaf/internal policy"), + ("zapier-decisions", "Decisions and action log"), ] git_lis = "".join( f'
  • {name} ' diff --git a/scripts/gen-module-docs.py b/scripts/gen-module-docs.py index 914e744..9b06098 100644 --- a/scripts/gen-module-docs.py +++ b/scripts/gen-module-docs.py @@ -31,6 +31,10 @@ PACKAGES = [ ("verae-access-api", ROOT / "packages" / "verae-access-api" / "src", (".js",)), ("verae-access-leaf", ROOT / "packages" / "verae-access-leaf" / "src", (".js",)), ("verae-access-zapier", ROOT / "packages" / "verae-access-zapier" / "src", (".js",)), + ("verae-access-staff", ROOT / "packages" / "verae-access-staff" / "src", (".js",)), + ("zappier-identity", ROOT / "packages" / "zappier-identity" / "src", (".js",)), + ("verae-jobs-events", ROOT / "packages" / "verae-jobs-events" / "src", (".js",)), + ("verae-nats-accounts", ROOT / "packages" / "verae-nats-accounts" / "src", (".js",)), ] REPO_READMES = [ @@ -58,6 +62,11 @@ REPO_READMES = [ "verae-access-api", "verae-access-leaf", "verae-access-zapier", + "verae-access-staff", + "zappier-identity", + "verae-jobs-events", + "verae-nats-accounts", + "zapier-decisions", ] SKIP_PARTS = {"test", "tests", "node_modules", "dist"} diff --git a/scripts/push-module-repos.sh b/scripts/push-module-repos.sh index bae625b..4a7233c 100755 --- a/scripts/push-module-repos.sh +++ b/scripts/push-module-repos.sh @@ -59,6 +59,11 @@ create verae-access-web "Direct customer web access plane" create verae-access-api "Direct customer API access plane" create verae-access-leaf "Server-to-server NATS leaf access plane" create verae-access-zapier "Zapier Platform access plane only" +create verae-access-staff "Staff CS/sales/accounting access plane" +create zappier-identity "Bind and lookup veraeUserId" +create verae-jobs-events "Mailbox for verae.zapier.jobs.events" +create verae-nats-accounts "NATS INTERNAL vs LEAF account policy" +create zapier-decisions "Architecture decisions and action log" push_dir "$ROOT/packages/zappier" zappier-edge push_dir "$ROOT/packages/verae-zapier-middleware" verae-middleware @@ -84,5 +89,10 @@ push_dir "$ROOT/packages/verae-access-web" verae-access-web push_dir "$ROOT/packages/verae-access-api" verae-access-api push_dir "$ROOT/packages/verae-access-leaf" verae-access-leaf push_dir "$ROOT/packages/verae-access-zapier" verae-access-zapier +push_dir "$ROOT/packages/verae-access-staff" verae-access-staff +push_dir "$ROOT/packages/zappier-identity" zappier-identity +push_dir "$ROOT/packages/verae-jobs-events" verae-jobs-events +push_dir "$ROOT/packages/verae-nats-accounts" verae-nats-accounts +push_dir "$ROOT/packages/zapier-decisions" zapier-decisions echo ALL_MODULE_REPOS_PUSHED