dev-overview/02-endpoint-io.md

154 lines
6.8 KiB
Markdown

# Endpoint I/O
Each product NATS address: **what it is for**, **who sends**, **who receives**, **how to attach**.
Connect with a **long-lived** client (reconnect-per-message is the anti-pattern):
```text
NATS_URL=nats://10.10.10.21:4222,nats://10.10.10.22:4222,nats://10.10.10.23:4222
```
Public edge (token in `/etc/nats/public-edge.token` on NS1): `nats://<token>@70.88.205.138:4222`. Do not put that URL in Zapier or in Verae.
---
## `verae.zapier.jobs.watch`
| | |
|--|--|
| For | Work queue: “poll Verae until this `jobId` is terminal.” |
| In from | middleware `enqueueWatch` after `POST /api/timestamp` (or mock) returns `jobId` |
| Out to | job-poller worker (queue `job-poller`, durable on `ZAPIER_JOBS`) |
| Then | poller calls Verae `GET /api/status/{jobId}` over **HTTPS**, not NATS |
Payload: `tenantId`, `veraeUserId`, `jobId`, `tokenRef`, `enqueuedAt`, `attempt`, `maxAttempts`, `intervalMs`, `traceId`. JWT stays in middleware (`tokenRef`); never on Zapier.
**Connect Verae-side:** do **not** subscribe this from `api.veraetime.net`. The poller is the Verae HTTP client. To add another watcher, start another `job-poller` consumer in the same queue group.
---
## `verae.zapier.jobs.events`
| | |
|--|--|
| For | Terminal outcomes: `timestamp.completed` / `failed` / `timeout` |
| In from | job-poller after Verae status is not `pending` |
| Out to | (1) middleware `waitForJobEvent` (wait HTTP); (2) webhook router → `webhooks.deliver`; (3) optional `verae-jobs-events` mailbox `:3030` |
If `JOBS_EVENTS_EXCLUSIVE=1`, only `verae-jobs-events` is the durable consumer and republishes `verae.internal.jobs.events`. Default lab: middleware wait + webhook worker share the stream.
Payload: `event`, `tenantId`, `jobId`, `status`, `traceId`, `emittedAt`.
---
## `verae.zapier.webhooks.deliver`
| | |
|--|--|
| For | HTTPS POST to `hooks.zapier.com` (REST Hook) |
| In from | webhook worker on completed/failed events |
| Out to | Zapier cloud **HTTPS**, never NATS |
Payload: `hookId`, `tenantId`, `targetUrl`, `event`, `payload`, `attempt`, `traceId`. Slow consumer: keep replica floors on the worker if you split it out of middleware.
---
## `verae.zapier.usage`
| | |
|--|--|
| For | Optional meter of Zap actions on NATS (JetStream) |
| In from | designed: middleware / edge |
| Out to | designed: `USAGE_WRITER` → books |
| Running | **stream exists; no writer; middleware uses local usage files** |
Prepaid meter that **is** wired uses `verae.billing.usage.recorded` (core pub), not this stream. Finish: either publish here from `recordUsage` or drop the stream.
---
## Billing (`verae.billing.*`) — account-balance source of truth
Request-reply unless noted. Queue `account-balance`. **Not** JetStream.
| Address | For | In from | Out to |
|---------|-----|---------|--------|
| `statement.get` | prepaid, credits, usage, payments | portal, CS, sales, accounting, access-web/api | reply to requester |
| `balance.get` | cents only | internal | reply |
| `balance.adjust` | CS credit or portal reload | CS, access-web (reload kind only) | books + reply |
| `usage.recorded` | meter pub | zappier-edge `meter()` `onUsage` when `NATS_URL` set | account-balance subscriber (debits prepaid) |
| `payment.recorded` | reload/invoice pub | portal | books |
| `credit.applied` | goodwill pub | CS/admin | books |
| `customer.put` | upsert customer | edge ledger | books |
Authz **must** pass `verae.access.authz.check` before these from a public plane. Leaf plane **cannot** call billing (policy).
Middleware `recordUsage` still writes **local tenant files** and does **not** publish `usage.recorded`. Two meters until that is unified.
---
## Access planes
| Address | For | In | Out |
|---------|-----|----|-----|
| `verae.access.authz.check` | allow/deny `{ plane, subject, principal }` | every plane + edge `billing-nats.ts` | `{ allow, reason }`; deny also pubs `authz.deny` |
| `verae.access.authz.deny` | audit | authz on deny | **nowhere yet** (no SIEM) |
| `verae.access.web.in` / `verae.access.web.billing.*` | browser portal | access-web HTTP | `statement.get` / `balance.adjust` kind=reload after authz |
| `verae.access.api.*` | customer `x-api-key` | access-api | statement; HTTPS to middleware for jobs |
| `verae.access.zapier.*` | Zapier HTTPS only | access-zapier | jobs.watch **via HTTP to middleware**, not Zapier NATS; usage.recorded allowed by policy |
| `verae.access.leaf.in` | S2S `{ token, target, payload }` | remote leaf | archive + jobs after authz; **never billing** |
| `verae.access.staff.*` | CS/sales/accounting | access-staff | billing after authz |
Ingress mapping: `accessAddress(plane, 'verae.billing.statement.get')` → `verae.access.<plane>.billing.statement.get`.
---
## Identity
| Address | For | In | Out |
|---------|-----|----|-----|
| `verae.identity.bind` | bind email/customerId → `veraeUserId` | portal / middleware | reply |
| `verae.identity.lookup` | lookup by customerId or veraeUserId | same | reply |
Queue `identity`. Fleet keepFloor.
---
## Archive / tree
| Address | For | In | Out |
|---------|-----|----|-----|
| `verae.archive.put` | WORM/tree write (`kind`: publicMeta / privateMeta / file / tree) | splitter (in-process from middleware today) | every worm/tree (JS) |
| `verae.archive.query` | broadcast lookup | aggregator | **all** worm/tree, **no** queue group |
| `verae.archive.reply.<correlationId>` | bloom **hit only** | worm/tree | aggregator |
Silence = miss. Chain seal does not wait on archive. Bytes never go to the chain.
---
## Template / unused
| Address | For | In | Out | Running |
|---------|-----|----|-----|---------|
| `verae.splitter.in` | future split-out splitter process | middleware | splitter | **no** |
| `verae.example.process.in` | template worker | any | `example-process` Q | **no** |
| `verae.example.process.out` | template fan-out | template | peers | **no** |
| `verae.example.process.reply.<id>` | template reply | template | requester | **no** |
| `verae.internal.jobs.events` | exclusive mailbox republish | jobs-events | wait/webhook | **no** unless env |
---
## How to connect **Verae API** to NATS
`api.veraetime.net` is **HTTPS**. It does not subscribe. The glue is middleware:
1. Set `VERAE_API_BASE_URL=https://api.veraetime.net` and `MOCK_VERAE=false`.
2. Middleware login (username/password) → JWT stored as `tokenRef`.
3. `POST /api/timestamp` → 202 `{ jobId }`.
4. Publish `verae.zapier.jobs.watch`.
5. Poller `GET /api/status/{jobId}` until `completed` / `failed`.
6. Publish `verae.zapier.jobs.events`.
7. Wait HTTP and/or REST Hook.
Do **not** point Verae at `NATS_URL`. Do **not** put NATS in Zapier env. A new worker that must see seals uses `jobs.events` or HTTP `GET /zapier/v1/status/{jobId}`.
Template for a new addressed process: clone `verae-nats-process`, rename `verae.example.process.*`, add the subject to `verae-access-authz` policy for the plane that may call it.