Separate Zapier, web, API, and leaf access planes with NATS authz
Some checks are pending
offline / test (push) Waiting to run

Zapier is one ingress. Direct web, customer API, and S2S leaf nodes are their own services. Every hop to an internal subject must pass verae.access.authz.check (default deny by plane).
This commit is contained in:
George Lambert 2026-09-11 16:05:05 -04:00
parent ac38676645
commit 1b199ca4d4
117 changed files with 2640 additions and 105 deletions

View file

@ -126,3 +126,12 @@ One MD file per source module.
- [zappier-sales-pricing/server](zappier-sales-pricing/server.md)
- [zappier-accounting-export/nats-billing](zappier-accounting-export/nats-billing.md)
- [zappier-accounting-export/server](zappier-accounting-export/server.md)
- [verae-access-authz/client](verae-access-authz/client.md)
- [verae-access-authz/policy](verae-access-authz/policy.md)
- [verae-access-authz/server](verae-access-authz/server.md)
- [verae-access-authz/subjects](verae-access-authz/subjects.md)
- [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/server](verae-access-leaf/server.md)
- [verae-access-zapier/server](verae-access-zapier/server.md)

View file

@ -0,0 +1,41 @@
# `verae-access-api/server`
**Package:** `verae-access-api`
**Source:** `packages/verae-access-api/src/server.js`
**Lines:** 60
## What this module is
Implementation module in `verae-access-api`. 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, principal, extra = {}` | — | `unknown` | see Call graph |
| `json` | `code, obj` | — | `unknown` | see Call graph |
## What it imports / requires
- `node:http`
## Call graph (identifiers invoked)
`replace`, `check`, `fetch`, `stringify`, `json`, `createServer`, `async`, `writeHead`, `end`, `match`, `push`, `concat`, `listen`, `write`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,42 @@
# `verae-access-authz/client`
**Package:** `verae-access-authz`
**Source:** `packages/verae-access-authz/src/client.js`
**Lines:** 39
## What this module is
Implementation module in `verae-access-authz`. The tables below are extracted from the source (signatures + JSDoc).
## Exports
`authorizeThenRequest`, `authorizeThenPublish`
## Types / interfaces / classes
_None extracted._
## Functions
| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) |
|------|------------|---------------------|---------|-----------------------------|
| `authorizeThenRequest` | `nc, sc, { plane, subject, principal, payload, timeout = 2000 }` | — | `unknown` | see Call graph |
| | _Call verae.access.authz.check then the internal subject. Fail closed._ | | | |
| `authorizeThenPublish` | `nc, sc, { plane, subject, principal, payload, timeout = 2000 }` | — | `unknown` | see Call graph |
## What it imports / requires
- `./subjects.js`
## Call graph (identifiers invoked)
`authorizeThenRequest`, `request`, `encode`, `stringify`, `parse`, `decode`, `authorizeThenPublish`, `publish`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,42 @@
# `verae-access-authz/policy`
**Package:** `verae-access-authz`
**Source:** `packages/verae-access-authz/src/policy.js`
**Lines:** 99
## What this module is
Implementation module in `verae-access-authz`. The tables below are extracted from the source (signatures + JSDoc).
## Exports
`POLICY`, `authorize`
## Types / interfaces / classes
_None extracted._
## Functions
| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) |
|------|------------|---------------------|---------|-----------------------------|
| `prefixAllowed` | `allow, subject` | — | `unknown` | see Call graph |
| `authorize` | `req` | — | `unknown` | see Call graph |
| `deny` | `plane, subject, reason` | — | `unknown` | see Call graph |
## What it imports / requires
- `./subjects.js`
## Call graph (identifiers invoked)
`workers`, `freeze`, `web`, `prefixAllowed`, `some`, `endsWith`, `startsWith`, `authorize`, `parseAddress`, `deny`, `includes`, `mismatch`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,44 @@
# `verae-access-authz/server`
**Package:** `verae-access-authz`
**Source:** `packages/verae-access-authz/src/server.js`
**Lines:** 84
## What this module is
Implementation module in `verae-access-authz`. 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 |
| `readBody` | `req` | — | `unknown` | see Call graph |
| `json` | `code, obj` | — | `unknown` | see Call graph |
## What it imports / requires
- `node:http`
- `./policy.js`
- `./subjects.js`
## Call graph (identifiers invoked)
`startNats`, `import`, `connect`, `split`, `subscribe`, `async`, `parse`, `decode`, `authorize`, `publish`, `encode`, `stringify`, `toISOString`, `respond`, `write`, `readBody`, `on`, `push`, `resolve`, `concat`, `toString`, `createServer`, `writeHead`, `end`, `json`, `listen`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,43 @@
# `verae-access-authz/subjects`
**Package:** `verae-access-authz`
**Source:** `packages/verae-access-authz/src/subjects.js`
**Lines:** 37
## What this module is
Implementation module in `verae-access-authz`. The tables below are extracted from the source (signatures + JSDoc).
## Exports
`PLANES`, `SUBJECTS`, `accessAddress`, `parseAddress`
## Types / interfaces / classes
_None extracted._
## Functions
| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) |
|------|------------|---------------------|---------|-----------------------------|
| `accessAddress` | `plane, internalSubject` | — | `unknown` | see Call graph |
| | _verae.billing.statement.get + plane web → verae.access.web.billing.statement.get_ | | | |
| `parseAddress` | `subject, planeHint` | — | `unknown` | see Call graph |
| | _Parse an access or internal subject into { plane, internal }._ | | | |
## What it imports / requires
_No imports detected._
## Call graph (identifiers invoked)
`ingress`, `internal`, `freeze`, `accessAddress`, `includes`, `startsWith`, `slice`, `parseAddress`, `match`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,44 @@
# `verae-access-leaf/server`
**Package:** `verae-access-leaf`
**Source:** `packages/verae-access-leaf/src/server.js`
**Lines:** 95
## What this module is
Implementation module in `verae-access-leaf`. 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, principal` | — | `unknown` | see Call graph |
| `authorizedToken` | `got` | — | `unknown` | see Call graph |
| `forward` | `body` | — | `unknown` | see Call graph |
| `startNats` | `(none)` | — | `unknown` | see Call graph |
| `json` | `code, obj` | — | `unknown` | see Call graph |
## What it imports / requires
- `node:http`
## 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`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,42 @@
# `verae-access-web/gate`
**Package:** `verae-access-web`
**Source:** `packages/verae-access-web/src/gate.js`
**Lines:** 51
## What this module is
Implementation module in `verae-access-web`. The tables below are extracted from the source (signatures + JSDoc).
## Exports
`PLANE`, `AUTHZ_CHECK`, `check`, `statement`, `reload`
## Types / interfaces / classes
_None extracted._
## Functions
| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) |
|------|------------|---------------------|---------|-----------------------------|
| `check` | `subject, { principal, kind, payload } = {}` | — | `unknown` | see Call graph |
| `statement` | `customerId, principal` | — | `unknown` | see Call graph |
| `reload` | `customerId, cents, principal` | — | `unknown` | see Call graph |
## What it imports / requires
_No imports detected._
## Call graph (identifiers invoked)
`replace`, `check`, `fetch`, `stringify`, `json`, `statement`, `import`, `connect`, `split`, `request`, `encode`, `parse`, `decode`, `close`, `reload`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,41 @@
# `verae-access-web/server`
**Package:** `verae-access-web`
**Source:** `packages/verae-access-web/src/server.js`
**Lines:** 39
## What this module is
Implementation module in `verae-access-web`. 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) |
|------|------------|---------------------|---------|-----------------------------|
| `json` | `code, obj` | — | `unknown` | see Call graph |
## What it imports / requires
- `node:http`
- `./gate.js`
## Call graph (identifiers invoked)
`createServer`, `async`, `writeHead`, `end`, `stringify`, `json`, `match`, `statement`, `push`, `parse`, `concat`, `toString`, `reload`, `listen`, `write`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,41 @@
# `verae-access-zapier/server`
**Package:** `verae-access-zapier`
**Source:** `packages/verae-access-zapier/src/server.js`
**Lines:** 56
## What this module is
Implementation module in `verae-access-zapier`. 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, principal` | — | `unknown` | see Call graph |
| `json` | `code, obj` | — | `unknown` | see Call graph |
## What it imports / requires
- `node:http`
## Call graph (identifiers invoked)
`replace`, `check`, `fetch`, `stringify`, `json`, `createServer`, `async`, `writeHead`, `end`, `startsWith`, `push`, `concat`, `listen`, `write`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -2,7 +2,7 @@
**Package:** `zappier-customer-service`
**Source:** `packages/zappier-customer-service/src/nats-billing.js`
**Lines:** 21
**Lines:** 45
## What this module is
@ -20,6 +20,7 @@ _None extracted._
| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) |
|------|------------|---------------------|---------|-----------------------------|
| `authz` | `subject, payload` | — | `unknown` | see Call graph |
| `billingRequest` | `subject, payload` | — | `unknown` | see Call graph |
## What it imports / requires
@ -28,7 +29,7 @@ _No imports detected._
## Call graph (identifiers invoked)
`billingRequest`, `import`, `connect`, `split`, `request`, `encode`, `stringify`, `parse`, `decode`, `close`
`authz`, `fetch`, `replace`, `stringify`, `json`, `billingRequest`, `import`, `connect`, `split`, `request`, `encode`, `parse`, `decode`, `close`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier-sales-pricing`
**Source:** `packages/zappier-sales-pricing/src/nats-billing.js`
**Lines:** 18
**Lines:** 37
## What this module is
@ -20,6 +20,7 @@ _None extracted._
| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) |
|------|------------|---------------------|---------|-----------------------------|
| `authz` | `subject` | — | `unknown` | see Call graph |
| `billingRequest` | `subject, payload` | — | `unknown` | see Call graph |
## What it imports / requires
@ -28,7 +29,7 @@ _No imports detected._
## Call graph (identifiers invoked)
`billingRequest`, `import`, `connect`, `split`, `request`, `encode`, `stringify`, `parse`, `decode`, `close`
`authz`, `fetch`, `replace`, `stringify`, `json`, `billingRequest`, `import`, `connect`, `split`, `request`, `encode`, `parse`, `decode`, `close`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier`
**Source:** `packages/zappier/src/admin.ts`
**Lines:** 588
**Lines:** 591
## What this module is

View file

@ -2,7 +2,7 @@
**Package:** `zappier`
**Source:** `packages/zappier/src/billing-nats.ts`
**Lines:** 74
**Lines:** 130
## What this module is
@ -10,12 +10,13 @@ Implementation module in `zappier`. The tables below are extracted from the sour
## Exports
`BILLING_SUBJECTS`, `BillingStatement`, `natsStatement`, `natsAdjust`, `natsPublish`
`BILLING_SUBJECTS`, `AUTHZ_CHECK`, `AccessPlane`, `BillingStatement`, `natsStatement`, `natsAdjust`, `natsPublish`
## Types / interfaces / classes
| Kind | Name |
|------|------|
| type | `AccessPlane` |
| type | `BillingStatement` |
| type | `Nc` |
@ -26,15 +27,26 @@ Implementation module in `zappier`. The tables below are extracted from the sour
| `nc` | `(none)` | — | `unknown` | see Call graph |
| `encode` | `obj: unknown` | — | `unknown` | see Call graph |
| `decode` | `buf: Uint8Array` | — | `unknown` | see Call graph |
| `natsStatement` | `customerId: string` | — | `unknown` | see Call graph |
| `authzAllow` | `plane: AccessPlane,
subject: string,
extra: { principal?: string; kind?: string } = {},` | — | `unknown` | see Call graph |
| `natsStatement` | `customerId: string,
plane: AccessPlane = 'web',` | — | `unknown` | see Call graph |
| `natsAdjust` | `payload: {
customerId: string;
cents: number;
reason: string;
agent: string;
kind?: string;
}` | — | `unknown` | see Call graph |
| `natsPublish` | `subject: string, payload: unknown` | — | `unknown` | see Call graph |
customerId: string;
cents: number;
reason: string;
agent: string;
kind?: string;
},
plane: AccessPlane = 'staff',` | — | `unknown` | see Call graph |
| `natsPublish` | `subject: string, payload: unknown, plane: AccessPlane = 'api'` | — | `unknown` | see Call graph |
## Methods (class / object)
| Name | Parameters |
|------|------------|
| `void` | `async (` |
## What it imports / requires
@ -42,7 +54,7 @@ _No imports detected._
## Call graph (identifiers invoked)
`unset`, `nc`, `async`, `import`, `connect`, `split`, `encode`, `stringify`, `decode`, `parse`, `natsStatement`, `request`, `natsAdjust`, `natsPublish`, `then`, `publish`
`unset`, `nc`, `async`, `import`, `connect`, `split`, `encode`, `stringify`, `decode`, `parse`, `authzAllow`, `request`, `fetch`, `replace`, `json`, `natsStatement`, `natsAdjust`, `natsPublish`, `void`, `publish`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier`
**Source:** `packages/zappier/src/portal.ts`
**Lines:** 321
**Lines:** 328
## What this module is

View file

@ -28,6 +28,11 @@ Markdown indexes: https://zapier.georgelambert.org/index-md.html
repos/zappier-customer-service
repos/zappier-sales-pricing
repos/zappier-accounting-export
repos/verae-access-authz
repos/verae-access-web
repos/verae-access-api
repos/verae-access-leaf
repos/verae-access-zapier
.. toctree::
:maxdepth: 2
@ -157,3 +162,12 @@ Markdown indexes: https://zapier.georgelambert.org/index-md.html
modules/zappier-sales-pricing/server
modules/zappier-accounting-export/nats-billing
modules/zappier-accounting-export/server
modules/verae-access-authz/client
modules/verae-access-authz/policy
modules/verae-access-authz/server
modules/verae-access-authz/subjects
modules/verae-access-web/gate
modules/verae-access-web/server
modules/verae-access-api/server
modules/verae-access-leaf/server
modules/verae-access-zapier/server

View file

@ -0,0 +1,41 @@
# `verae-access-api/server`
**Package:** `verae-access-api`
**Source:** `packages/verae-access-api/src/server.js`
**Lines:** 60
## What this module is
Implementation module in `verae-access-api`. 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, principal, extra = {}` | — | `unknown` | see Call graph |
| `json` | `code, obj` | — | `unknown` | see Call graph |
## What it imports / requires
- `node:http`
## Call graph (identifiers invoked)
`replace`, `check`, `fetch`, `stringify`, `json`, `createServer`, `async`, `writeHead`, `end`, `match`, `push`, `concat`, `listen`, `write`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,7 @@
verae-access-api.server
=======================
Generated API sheet for ``verae-access-api/server``.
.. include:: server.md
:parser: myst_parser.sphinx_

View file

@ -0,0 +1,42 @@
# `verae-access-authz/client`
**Package:** `verae-access-authz`
**Source:** `packages/verae-access-authz/src/client.js`
**Lines:** 39
## What this module is
Implementation module in `verae-access-authz`. The tables below are extracted from the source (signatures + JSDoc).
## Exports
`authorizeThenRequest`, `authorizeThenPublish`
## Types / interfaces / classes
_None extracted._
## Functions
| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) |
|------|------------|---------------------|---------|-----------------------------|
| `authorizeThenRequest` | `nc, sc, { plane, subject, principal, payload, timeout = 2000 }` | — | `unknown` | see Call graph |
| | _Call verae.access.authz.check then the internal subject. Fail closed._ | | | |
| `authorizeThenPublish` | `nc, sc, { plane, subject, principal, payload, timeout = 2000 }` | — | `unknown` | see Call graph |
## What it imports / requires
- `./subjects.js`
## Call graph (identifiers invoked)
`authorizeThenRequest`, `request`, `encode`, `stringify`, `parse`, `decode`, `authorizeThenPublish`, `publish`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,7 @@
verae-access-authz.client
=========================
Generated API sheet for ``verae-access-authz/client``.
.. include:: client.md
:parser: myst_parser.sphinx_

View file

@ -0,0 +1,42 @@
# `verae-access-authz/policy`
**Package:** `verae-access-authz`
**Source:** `packages/verae-access-authz/src/policy.js`
**Lines:** 99
## What this module is
Implementation module in `verae-access-authz`. The tables below are extracted from the source (signatures + JSDoc).
## Exports
`POLICY`, `authorize`
## Types / interfaces / classes
_None extracted._
## Functions
| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) |
|------|------------|---------------------|---------|-----------------------------|
| `prefixAllowed` | `allow, subject` | — | `unknown` | see Call graph |
| `authorize` | `req` | — | `unknown` | see Call graph |
| `deny` | `plane, subject, reason` | — | `unknown` | see Call graph |
## What it imports / requires
- `./subjects.js`
## Call graph (identifiers invoked)
`workers`, `freeze`, `web`, `prefixAllowed`, `some`, `endsWith`, `startsWith`, `authorize`, `parseAddress`, `deny`, `includes`, `mismatch`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,7 @@
verae-access-authz.policy
=========================
Generated API sheet for ``verae-access-authz/policy``.
.. include:: policy.md
:parser: myst_parser.sphinx_

View file

@ -0,0 +1,44 @@
# `verae-access-authz/server`
**Package:** `verae-access-authz`
**Source:** `packages/verae-access-authz/src/server.js`
**Lines:** 84
## What this module is
Implementation module in `verae-access-authz`. 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 |
| `readBody` | `req` | — | `unknown` | see Call graph |
| `json` | `code, obj` | — | `unknown` | see Call graph |
## What it imports / requires
- `node:http`
- `./policy.js`
- `./subjects.js`
## Call graph (identifiers invoked)
`startNats`, `import`, `connect`, `split`, `subscribe`, `async`, `parse`, `decode`, `authorize`, `publish`, `encode`, `stringify`, `toISOString`, `respond`, `write`, `readBody`, `on`, `push`, `resolve`, `concat`, `toString`, `createServer`, `writeHead`, `end`, `json`, `listen`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,7 @@
verae-access-authz.server
=========================
Generated API sheet for ``verae-access-authz/server``.
.. include:: server.md
:parser: myst_parser.sphinx_

View file

@ -0,0 +1,43 @@
# `verae-access-authz/subjects`
**Package:** `verae-access-authz`
**Source:** `packages/verae-access-authz/src/subjects.js`
**Lines:** 37
## What this module is
Implementation module in `verae-access-authz`. The tables below are extracted from the source (signatures + JSDoc).
## Exports
`PLANES`, `SUBJECTS`, `accessAddress`, `parseAddress`
## Types / interfaces / classes
_None extracted._
## Functions
| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) |
|------|------------|---------------------|---------|-----------------------------|
| `accessAddress` | `plane, internalSubject` | — | `unknown` | see Call graph |
| | _verae.billing.statement.get + plane web → verae.access.web.billing.statement.get_ | | | |
| `parseAddress` | `subject, planeHint` | — | `unknown` | see Call graph |
| | _Parse an access or internal subject into { plane, internal }._ | | | |
## What it imports / requires
_No imports detected._
## Call graph (identifiers invoked)
`ingress`, `internal`, `freeze`, `accessAddress`, `includes`, `startsWith`, `slice`, `parseAddress`, `match`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,7 @@
verae-access-authz.subjects
===========================
Generated API sheet for ``verae-access-authz/subjects``.
.. include:: subjects.md
:parser: myst_parser.sphinx_

View file

@ -0,0 +1,44 @@
# `verae-access-leaf/server`
**Package:** `verae-access-leaf`
**Source:** `packages/verae-access-leaf/src/server.js`
**Lines:** 95
## What this module is
Implementation module in `verae-access-leaf`. 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, principal` | — | `unknown` | see Call graph |
| `authorizedToken` | `got` | — | `unknown` | see Call graph |
| `forward` | `body` | — | `unknown` | see Call graph |
| `startNats` | `(none)` | — | `unknown` | see Call graph |
| `json` | `code, obj` | — | `unknown` | see Call graph |
## What it imports / requires
- `node:http`
## 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`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,7 @@
verae-access-leaf.server
========================
Generated API sheet for ``verae-access-leaf/server``.
.. include:: server.md
:parser: myst_parser.sphinx_

View file

@ -0,0 +1,42 @@
# `verae-access-web/gate`
**Package:** `verae-access-web`
**Source:** `packages/verae-access-web/src/gate.js`
**Lines:** 51
## What this module is
Implementation module in `verae-access-web`. The tables below are extracted from the source (signatures + JSDoc).
## Exports
`PLANE`, `AUTHZ_CHECK`, `check`, `statement`, `reload`
## Types / interfaces / classes
_None extracted._
## Functions
| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) |
|------|------------|---------------------|---------|-----------------------------|
| `check` | `subject, { principal, kind, payload } = {}` | — | `unknown` | see Call graph |
| `statement` | `customerId, principal` | — | `unknown` | see Call graph |
| `reload` | `customerId, cents, principal` | — | `unknown` | see Call graph |
## What it imports / requires
_No imports detected._
## Call graph (identifiers invoked)
`replace`, `check`, `fetch`, `stringify`, `json`, `statement`, `import`, `connect`, `split`, `request`, `encode`, `parse`, `decode`, `close`, `reload`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,7 @@
verae-access-web.gate
=====================
Generated API sheet for ``verae-access-web/gate``.
.. include:: gate.md
:parser: myst_parser.sphinx_

View file

@ -0,0 +1,41 @@
# `verae-access-web/server`
**Package:** `verae-access-web`
**Source:** `packages/verae-access-web/src/server.js`
**Lines:** 39
## What this module is
Implementation module in `verae-access-web`. 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) |
|------|------------|---------------------|---------|-----------------------------|
| `json` | `code, obj` | — | `unknown` | see Call graph |
## What it imports / requires
- `node:http`
- `./gate.js`
## Call graph (identifiers invoked)
`createServer`, `async`, `writeHead`, `end`, `stringify`, `json`, `match`, `statement`, `push`, `parse`, `concat`, `toString`, `reload`, `listen`, `write`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,7 @@
verae-access-web.server
=======================
Generated API sheet for ``verae-access-web/server``.
.. include:: server.md
:parser: myst_parser.sphinx_

View file

@ -0,0 +1,41 @@
# `verae-access-zapier/server`
**Package:** `verae-access-zapier`
**Source:** `packages/verae-access-zapier/src/server.js`
**Lines:** 56
## What this module is
Implementation module in `verae-access-zapier`. 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, principal` | — | `unknown` | see Call graph |
| `json` | `code, obj` | — | `unknown` | see Call graph |
## What it imports / requires
- `node:http`
## Call graph (identifiers invoked)
`replace`, `check`, `fetch`, `stringify`, `json`, `createServer`, `async`, `writeHead`, `end`, `startsWith`, `push`, `concat`, `listen`, `write`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules 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`.

View file

@ -0,0 +1,7 @@
verae-access-zapier.server
==========================
Generated API sheet for ``verae-access-zapier/server``.
.. include:: server.md
:parser: myst_parser.sphinx_

View file

@ -2,7 +2,7 @@
**Package:** `zappier-customer-service`
**Source:** `packages/zappier-customer-service/src/nats-billing.js`
**Lines:** 21
**Lines:** 45
## What this module is
@ -20,6 +20,7 @@ _None extracted._
| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) |
|------|------------|---------------------|---------|-----------------------------|
| `authz` | `subject, payload` | — | `unknown` | see Call graph |
| `billingRequest` | `subject, payload` | — | `unknown` | see Call graph |
## What it imports / requires
@ -28,7 +29,7 @@ _No imports detected._
## Call graph (identifiers invoked)
`billingRequest`, `import`, `connect`, `split`, `request`, `encode`, `stringify`, `parse`, `decode`, `close`
`authz`, `fetch`, `replace`, `stringify`, `json`, `billingRequest`, `import`, `connect`, `split`, `request`, `encode`, `parse`, `decode`, `close`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier-sales-pricing`
**Source:** `packages/zappier-sales-pricing/src/nats-billing.js`
**Lines:** 18
**Lines:** 37
## What this module is
@ -20,6 +20,7 @@ _None extracted._
| Name | Parameters | Param types (JSDoc) | Returns | Calls (same file / helpers) |
|------|------------|---------------------|---------|-----------------------------|
| `authz` | `subject` | — | `unknown` | see Call graph |
| `billingRequest` | `subject, payload` | — | `unknown` | see Call graph |
## What it imports / requires
@ -28,7 +29,7 @@ _No imports detected._
## Call graph (identifiers invoked)
`billingRequest`, `import`, `connect`, `split`, `request`, `encode`, `stringify`, `parse`, `decode`, `close`
`authz`, `fetch`, `replace`, `stringify`, `json`, `billingRequest`, `import`, `connect`, `split`, `request`, `encode`, `parse`, `decode`, `close`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier`
**Source:** `packages/zappier/src/admin.ts`
**Lines:** 588
**Lines:** 591
## What this module is

View file

@ -2,7 +2,7 @@
**Package:** `zappier`
**Source:** `packages/zappier/src/billing-nats.ts`
**Lines:** 74
**Lines:** 130
## What this module is
@ -10,12 +10,13 @@ Implementation module in `zappier`. The tables below are extracted from the sour
## Exports
`BILLING_SUBJECTS`, `BillingStatement`, `natsStatement`, `natsAdjust`, `natsPublish`
`BILLING_SUBJECTS`, `AUTHZ_CHECK`, `AccessPlane`, `BillingStatement`, `natsStatement`, `natsAdjust`, `natsPublish`
## Types / interfaces / classes
| Kind | Name |
|------|------|
| type | `AccessPlane` |
| type | `BillingStatement` |
| type | `Nc` |
@ -26,15 +27,26 @@ Implementation module in `zappier`. The tables below are extracted from the sour
| `nc` | `(none)` | — | `unknown` | see Call graph |
| `encode` | `obj: unknown` | — | `unknown` | see Call graph |
| `decode` | `buf: Uint8Array` | — | `unknown` | see Call graph |
| `natsStatement` | `customerId: string` | — | `unknown` | see Call graph |
| `authzAllow` | `plane: AccessPlane,
subject: string,
extra: { principal?: string; kind?: string } = {},` | — | `unknown` | see Call graph |
| `natsStatement` | `customerId: string,
plane: AccessPlane = 'web',` | — | `unknown` | see Call graph |
| `natsAdjust` | `payload: {
customerId: string;
cents: number;
reason: string;
agent: string;
kind?: string;
}` | — | `unknown` | see Call graph |
| `natsPublish` | `subject: string, payload: unknown` | — | `unknown` | see Call graph |
customerId: string;
cents: number;
reason: string;
agent: string;
kind?: string;
},
plane: AccessPlane = 'staff',` | — | `unknown` | see Call graph |
| `natsPublish` | `subject: string, payload: unknown, plane: AccessPlane = 'api'` | — | `unknown` | see Call graph |
## Methods (class / object)
| Name | Parameters |
|------|------------|
| `void` | `async (` |
## What it imports / requires
@ -42,7 +54,7 @@ _No imports detected._
## Call graph (identifiers invoked)
`unset`, `nc`, `async`, `import`, `connect`, `split`, `encode`, `stringify`, `decode`, `parse`, `natsStatement`, `request`, `natsAdjust`, `natsPublish`, `then`, `publish`
`unset`, `nc`, `async`, `import`, `connect`, `split`, `encode`, `stringify`, `decode`, `parse`, `authzAllow`, `request`, `fetch`, `replace`, `json`, `natsStatement`, `natsAdjust`, `natsPublish`, `void`, `publish`
Each identifier is a call site in this file. Follow the import list to see the defining module; open that modules MD for parameter and return types.

View file

@ -2,7 +2,7 @@
**Package:** `zappier`
**Source:** `packages/zappier/src/portal.ts`
**Lines:** 321
**Lines:** 328
## What this module is

View file

@ -0,0 +1,11 @@
# verae-access-api
**Direct customer API** plane (`x-api-key`). Separate from the Zapier Platform app.
**Forgejo:** https://git.georgelambert.org/marchon/verae-access-api
Address path: `verae.access.api.*``verae.access.authz.check``verae.billing.statement.get` / `verae.zapier.jobs.watch` (HTTPS to middleware, not a Zapier NATS client).
Cannot `balance.adjust` or `archive.put`.
Port `:3022`.

View file

@ -0,0 +1,34 @@
# verae-access-authz
Authorization step on the NATS **address path**. Zapier is only **one** access plane. Direct web, customer API, and server-to-server leaf nodes are separate, and each is default-deny against internal subjects.
**Forgejo:** https://git.georgelambert.org/marchon/verae-access-authz
**Catalog:** https://zapier.georgelambert.org/packages/verae-access-authz/README.pdf
## Address path
```text
client
→ verae.access.<plane>.<area>.<resource>.<action> (ingress, that plane only)
→ verae.access.authz.check (allow / deny)
→ verae.<area>.<resource>.<action> (internal bus)
```
Internal subjects (`verae.billing.*`, `verae.archive.*`, `verae.zapier.jobs.*`) stay stable. Planes never publish them until `authz.check` returns `allow`.
| Plane | Who | May reach |
|-------|-----|-----------|
| `zapier` | Zapier Platform HTTPS | jobs.*, webhooks.deliver, billing.usage.recorded |
| `web` | Customer browser portal | statement.get, balance.adjust kind=reload\|payment |
| `api` | Customer `x-api-key` (not Zapier) | statement.get, usage.recorded, jobs.watch |
| `leaf` | S2S NATS leaf / mTLS | archive.*, jobs.*, webhooks — **not billing** |
| `staff` | CS / sales / admin | statement, balance.adjust (credits) |
A leaf node cannot credit an account. A Zapier hop cannot read a customer statement. A browser cannot `archive.put`.
```bash
NATS_URL=nats://127.0.0.1:4222 PORT=3020 npm start
curl -s http://127.0.0.1:3020/policy
curl -s -X POST http://127.0.0.1:3020/check -H 'content-type: application/json' \
-d '{"plane":"leaf","subject":"verae.billing.balance.adjust"}'
```

View file

@ -0,0 +1,13 @@
# verae-access-leaf
**Server-to-server leaf node** access plane. Remote machines connect as NATS leaf nodes (or POST `/forward` with a leaf token). They never get the core billing subjects.
**Forgejo:** https://git.georgelambert.org/marchon/verae-access-leaf
```text
remote leaf → verae.access.leaf.in → authz.check → verae.archive.put | jobs.*
```
Denied: `verae.billing.balance.adjust`, `verae.billing.statement.get`. A compromised WORM/leaf cannot credit customers.
`LEAF_TOKEN` required. Port `:3023`. Do not publish NATS `4222` to the internet; leaf nodes use a private leafnode port / tunnel.

View file

@ -0,0 +1,15 @@
# verae-access-web
**Direct web** access plane. Customer browsers hit this process, not Zapier and not the NATS port.
**Forgejo:** https://git.georgelambert.org/marchon/verae-access-web
Every NATS hop is `verae.access.web.*``verae.access.authz.check` → internal `verae.billing.statement.get` (reload only, not CS credits).
| Route | Job |
|-------|-----|
| `GET /health` | `{ plane: "web" }` |
| `GET /statement/:id` | Authz then statement |
| `POST /reload` | Authz then `balance.adjust` kind=reload |
Port `:3021`.

View file

@ -0,0 +1,9 @@
# verae-access-zapier
**Zapier is one access plane**, not the only one. This process is the Zapier HTTPS front door: timestamp/wait/hash into middleware. No customer portal, no admin, no leaf.
**Forgejo:** https://git.georgelambert.org/marchon/verae-access-zapier
Zapier cloud never opens NATS. This process may request `verae.access.authz.check` for `verae.zapier.jobs.watch` and `verae.billing.usage.recorded` only.
Port `:3024`.

View file

@ -30,7 +30,12 @@ Zapier cloud **never** connects to NATS. Internal services do: middleware worker
| Module | Repo (Forgejo) | Runtime | Listens | Sends |
|--------|----------------|---------|---------|-------|
| zapier-platform-app | `verae-zapier-app` | Zapier cloud | User Zap steps | HTTPS to zappier-edge |
| zappier-edge | `zappier-edge` | Public HTTPS :3000 | Zapier, portal, admin | HTTPS to middleware; NATS billing pub |
| zappier-edge | `zappier-edge` | Public HTTPS :3000 | Zapier, portal, admin | HTTPS to middleware; NATS billing after authz |
| access-authz | `verae-access-authz` | :3020 + NATS | `verae.access.authz.check` | allow/deny |
| access-web | `verae-access-web` | :3021 | customer browser | billing statement/reload |
| access-api | `verae-access-api` | :3022 | x-api-key | statement; HTTPS to middleware |
| access-leaf | `verae-access-leaf` | :3023 | S2S leaf | archive/jobs only |
| access-zapier | `verae-access-zapier` | :3024 | Zapier HTTPS | jobs.watch after authz |
| account-balance | `zappier-account-balance` | :3010 + NATS | `verae.billing.*` | statement/adjust replies |
| customer-service | `zappier-customer-service` | :3011 | CS staff HTTP | NATS balance.adjust / statement.get |
| sales-pricing | `zappier-sales-pricing` | :3012 | Sales HTTP | NATS statement.get; HTTPS to edge for multipliers |
@ -63,6 +68,11 @@ Zapier cloud **never** connects to NATS. Internal services do: middleware worker
| `verae.billing.usage.recorded` | pub | meter events from zappier-edge |
| `verae.billing.payment.recorded` | pub | portal reload / invoice paid |
| `verae.billing.credit.applied` | pub | CS goodwill |
| `verae.access.authz.check` | request-reply | `{ plane, subject, principal }``{ allow, reason }` |
| `verae.access.web.billing.statement.get` | ingress | web plane only; mapped after authz |
| `verae.access.api.*` | ingress | customer API plane |
| `verae.access.zapier.*` | ingress | Zapier plane |
| `verae.access.leaf.in` | ingress | S2S leaf; never billing |
| `verae.archive.put` | JetStream | `sha256`, `tenantId`, `kind` (`publicMeta\|privateMeta\|file\|tree`), `record`, `traceId` |
| `verae.archive.query` | pub to **all** archives | `correlationId`, `sha256`, `tenantId`, `kinds[]`, `traceId` |
| `verae.archive.reply.<correlationId>` | replies | `archiveId`, `sha256`, `records[]`, `traceId` |

View file

@ -0,0 +1,3 @@
# NATS — verae-access-api
Plane `api`. Statement and jobs.watch after authz. No credits, no archive.

View file

@ -0,0 +1,3 @@
# verae-access-api
Direct customer API (`x-api-key`). Separate from Zapier Platform.

View file

@ -0,0 +1,3 @@
# NATS — verae-access-authz
`verae.access.authz.check` request-reply. Queue `access-authz`. Fail closed.

View file

@ -0,0 +1,3 @@
# verae-access-authz
Default-deny authorization step on the NATS address path. Planes: zapier, web, api, leaf, staff.

View file

@ -0,0 +1,3 @@
# NATS — verae-access-leaf
`verae.access.leaf.in` → authz.check → `verae.archive.*` / jobs.*. Denied: billing.

View file

@ -0,0 +1,3 @@
# verae-access-leaf
Server-to-server NATS leaf ingress. Archive and jobs only. Never billing.

View file

@ -0,0 +1,3 @@
# NATS — verae-access-web
Plane `web`. Authz then `verae.billing.statement.get` / `balance.adjust` kind=reload.

View file

@ -0,0 +1,3 @@
# verae-access-web
Direct customer web plane. Not Zapier. Statement + reload only after authz.

View file

@ -0,0 +1,3 @@
# NATS — verae-access-zapier
Plane `zapier`. Authz then jobs.watch / usage.recorded. Zapier cloud never connects to NATS.

View file

@ -0,0 +1,3 @@
# verae-access-zapier
Zapier Platform HTTPS plane only. No portal, no admin, no leaf.

View file

@ -10,8 +10,12 @@ Verae Time proves a SHA-256 existed at a given time. Zapier lets customers regis
| Plane | What | Network |
|-------|------|---------|
| Zapier cloud | `verae-zapier-app`, `verae-activate` | HTTPS to zappier-edge |
| Commercial edge | `zappier-edge` portal, admin, `x-api-key`, Stripe meter | Public HTTPS; NATS billing pubs/requests |
| Zapier cloud | `verae-zapier-app` via **access-zapier** | HTTPS to `:3024` only (not NATS) |
| Direct web | `verae-access-web` customer browser | HTTPS `:3021` |
| 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 |
| 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 |

View file

@ -8,6 +8,11 @@ Each runtime piece is its **own git repo** on Forgejo (`git.georgelambert.org`,
| **verae-nats-process** | `packages/verae-nats-process` | **Template** for a new addressed NATS process |
| **master-zapier-plan-draft** | workspace root | Combined snapshot (`main` and `master`) |
| **zappier-edge** | `packages/zappier` | Metered HTTPS, portal, admin, Stripe; **proxies Verae calls to middleware** |
| **verae-access-authz** | `packages/verae-access-authz` | NATS authorization step (`authz.check`) |
| **verae-access-web** | `packages/verae-access-web` | Direct customer web plane |
| **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 |
| **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 |

View file

@ -20,7 +20,7 @@ Todays NS1 box (`NS1.GEORGELAMBERT.ORG`, `70.88.205.138`) already runs JetStr
| Allowed | Forbidden |
|---------|-----------|
| verae-middleware, fleet workers, WORM, tree nodes, account-balance, zappier-edge billing, CS/sales/accounting, `verae-nats-process` clones | Zapier cloud, customer browsers (they use HTTPS portal/CS/sales UIs) |
| access-authz, access-leaf (after token), middleware, fleet workers, WORM, tree, account-balance, access planes, CS/sales | Zapier cloud, customer browsers (HTTPS to access-web / access-api / access-zapier only) |
## Address families already in use

View file

@ -5,10 +5,14 @@ NATS **addresses** (subjects) are the extension point. A new search, store, or j
## Pattern
```text
verae.<area>.<resource>.<action>
verae.access.<plane>.<area>.<resource>.<action> # ingress (one plane)
verae.access.authz.check # authorization step
verae.<area>.<resource>.<action> # internal bus
verae.<area>.<resource>.reply.<correlationId>
```
Planes: `zapier` | `web` | `api` | `leaf` | `staff`. Default deny. A leaf cannot `balance.adjust`; Zapier cannot `statement.get`; a browser cannot `archive.put`.
| Piece | Example | Meaning |
|-------|---------|---------|
| `verae` | — | Verae bus (not Zapier) |
@ -26,7 +30,7 @@ verae.<area>.<resource>.reply.<correlationId>
2. Rename `verae.example.process.in` / `.out` / `.reply.*` in `src/subjects.js`.
3. Add a row to that repos `ROUTING.md` and to [INDEX.md](INDEX.md).
4. Register the process in `verae-fleet` (`min`/`max`, machines, roles).
5. If Zapier must call it, add **one HTTPS route** on middleware — Zapier still never sees NATS.
5. If an access plane must call it, add the subject to **verae-access-authz** policy for that plane only. Zapier still never sees NATS.
Do **not** invent a public NATS URL for Zapier. Do **not** reuse `verae.archive.query` as a queue group.

View file

@ -8,23 +8,24 @@ SVG files in [`diagrams/`](diagrams/). The same shapes are repeated below in mer
```mermaid
flowchart LR
Z[Zapier cloud apps] -->|HTTPS x-api-key| E[zappier-edge]
Cust[customer portal] -->|HTTPS statement| E
CS[customer-service] -->|NATS statement/adjust| N[NATS 3-node cluster]
SA[sales-pricing] -->|NATS statement| N
AC[accounting-export] -->|NATS statement| N
E -->|NATS billing| N
BAL[account-balance] -->|reply verae.billing.*| N
E -->|HTTPS metered| M[verae-middleware]
M -->|HTTPS| C[Verae chain]
M -->|JetStream| N
Z[Zapier cloud] -->|HTTPS plane=zapier| AZ[access-zapier]
Web[customer browser] -->|HTTPS plane=web| AW[access-web]
API[customer API] -->|HTTPS plane=api| AA[access-api]
Leaf[S2S leaf] -->|verae.access.leaf.in| AL[access-leaf]
AZ --> Authz[access-authz]
AW --> Authz
AA --> Authz
AL --> Authz
CS[staff CS/sales] --> Authz
Authz -->|allow| N[NATS cluster]
BAL[account-balance] --> N
AZ -->|HTTPS| M[verae-middleware]
M --> N
N --> P[job-poller]
N --> W[webhook-deliver]
N --> A[archive-aggregator]
N --> R[WORM x N]
N --> T[tree-node x N]
P -->|HTTPS status| C
W -->|HTTPS REST Hook| Z
N --> R[WORM]
N --> T[tree-node]
P --> C[Verae chain]
M --> C
```
## 8.2 NATS.IO 3-server cluster

View file

@ -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-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-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

View file

@ -0,0 +1,3 @@
# NATS — verae-access-api
Plane `api`. Ingress `verae.access.api.*`. Authz then statement.get / usage.recorded / jobs.watch. Never archive or credits.

View file

@ -0,0 +1,11 @@
# verae-access-api
**Direct customer API** plane (`x-api-key`). Separate from the Zapier Platform app.
**Forgejo:** https://git.georgelambert.org/marchon/verae-access-api
Address path: `verae.access.api.*``verae.access.authz.check``verae.billing.statement.get` / `verae.zapier.jobs.watch` (HTTPS to middleware, not a Zapier NATS client).
Cannot `balance.adjust` or `archive.put`.
Port `:3022`.

View file

@ -0,0 +1,11 @@
{
"name": "verae-access-api",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Direct customer API access plane (x-api-key, not Zapier)",
"scripts": {
"start": "node src/server.js",
"test": "node --test test/*.test.js"
}
}

View file

@ -0,0 +1,59 @@
#!/usr/bin/env node
/** Direct customer API (x-api-key). Not Zapier Platform. */
import http from 'node:http';
const PORT = Number(process.env.PORT || 3022);
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 MW = (process.env.ZAPPIER_UPSTREAM || 'http://127.0.0.1:3100').replace(/\/$/, '');
const PLANE = 'api';
async function check(subject, principal, extra = {}) {
const r = await fetch(`${AUTHZ}/check`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ plane: PLANE, subject, principal, ...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-api', plane: PLANE });
}
const key = req.headers['x-api-key'];
const principal = typeof key === 'string' ? key : 'anonymous';
const st = url.pathname.match(/^\/v1\/statement\/([^/]+)$/);
if (req.method === 'GET' && st) {
const gate = await check('verae.billing.statement.get', principal);
if (!gate.allow) return json(403, gate);
const r = await fetch(`${BOOKS}/statement/${st[1]}`);
return json(r.status, { ...(await r.json()), plane: PLANE, source: 'account-balance' });
}
if (req.method === 'POST' && url.pathname === '/v1/timestamp') {
const gate = await check('verae.zapier.jobs.watch', principal);
if (!gate.allow) return json(403, gate);
const chunks = [];
for await (const c of req) chunks.push(c);
const r = await fetch(`${MW}/zapier/v1/timestamp`, {
method: 'POST',
headers: { 'content-type': 'application/json', 'x-api-key': principal },
body: Buffer.concat(chunks),
});
return json(r.status, { ...(await r.json().catch(() => ({}))), 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-api http://0.0.0.0:${PORT}/ plane=${PLANE}\n`);
});

View file

@ -0,0 +1,37 @@
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');
test('api plane health and authz deny on credits', async () => {
const authzPort = 18024;
const apiPort = 18025;
const authz = spawn(process.execPath, ['src/server.js'], {
cwd: authzRoot,
env: { ...process.env, PORT: String(authzPort) },
stdio: ['ignore', 'pipe', 'pipe'],
});
const api = spawn(process.execPath, ['src/server.js'], {
cwd: root,
env: { ...process.env, PORT: String(apiPort), AUTHZ_URL: `http://127.0.0.1:${authzPort}` },
stdio: ['ignore', 'pipe', 'pipe'],
});
await new Promise((r) => setTimeout(r, 500));
try {
const h = await (await fetch(`http://127.0.0.1:${apiPort}/health`)).json();
assert.equal(h.plane, 'api');
const deny = await fetch(`http://127.0.0.1:${authzPort}/check`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ plane: 'api', subject: 'verae.billing.balance.adjust' }),
});
assert.equal(deny.status, 403);
} finally {
api.kill('SIGTERM');
authz.kill('SIGTERM');
}
});

View file

@ -0,0 +1,11 @@
# NATS — verae-access-authz
| Direction | Address | Kind |
|-----------|---------|------|
| IN | `verae.access.authz.check` | request-reply, queue `access-authz` |
| OUT | `verae.access.authz.deny` | pub on deny (audit) |
Body in: `{ plane, subject, principal, kind, payload }`.
Body out: `{ allow, plane, subject, reason }`.
Fail closed: if this process is down, access gateways must not forward.

View file

@ -0,0 +1,34 @@
# verae-access-authz
Authorization step on the NATS **address path**. Zapier is only **one** access plane. Direct web, customer API, and server-to-server leaf nodes are separate, and each is default-deny against internal subjects.
**Forgejo:** https://git.georgelambert.org/marchon/verae-access-authz
**Catalog:** https://zapier.georgelambert.org/packages/verae-access-authz/README.pdf
## Address path
```text
client
→ verae.access.<plane>.<area>.<resource>.<action> (ingress, that plane only)
→ verae.access.authz.check (allow / deny)
→ verae.<area>.<resource>.<action> (internal bus)
```
Internal subjects (`verae.billing.*`, `verae.archive.*`, `verae.zapier.jobs.*`) stay stable. Planes never publish them until `authz.check` returns `allow`.
| Plane | Who | May reach |
|-------|-----|-----------|
| `zapier` | Zapier Platform HTTPS | jobs.*, webhooks.deliver, billing.usage.recorded |
| `web` | Customer browser portal | statement.get, balance.adjust kind=reload\|payment |
| `api` | Customer `x-api-key` (not Zapier) | statement.get, usage.recorded, jobs.watch |
| `leaf` | S2S NATS leaf / mTLS | archive.*, jobs.*, webhooks — **not billing** |
| `staff` | CS / sales / admin | statement, balance.adjust (credits) |
A leaf node cannot credit an account. A Zapier hop cannot read a customer statement. A browser cannot `archive.put`.
```bash
NATS_URL=nats://127.0.0.1:4222 PORT=3020 npm start
curl -s http://127.0.0.1:3020/policy
curl -s -X POST http://127.0.0.1:3020/check -H 'content-type: application/json' \
-d '{"plane":"leaf","subject":"verae.billing.balance.adjust"}'
```

View file

@ -0,0 +1,46 @@
{
"name": "verae-access-authz",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "verae-access-authz",
"version": "0.1.0",
"dependencies": {
"nats": "^2.28.2"
}
},
"node_modules/nats": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/nats/-/nats-2.29.3.tgz",
"integrity": "sha512-tOQCRCwC74DgBTk4pWZ9V45sk4d7peoE2njVprMRCBXrhJ5q5cYM7i6W+Uvw2qUrcfOSnuisrX7bEx3b3Wx4QA==",
"deprecated": "Package moved. Use @nats-io/transport-node from https://github.com/nats-io/nats.js",
"license": "Apache-2.0",
"dependencies": {
"nkeys.js": "1.1.0"
},
"engines": {
"node": ">= 14.0.0"
}
},
"node_modules/nkeys.js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/nkeys.js/-/nkeys.js-1.1.0.tgz",
"integrity": "sha512-tB/a0shZL5UZWSwsoeyqfTszONTt4k2YS0tuQioMOD180+MbombYVgzDUYHlx+gejYK6rgf08n/2Df99WY0Sxg==",
"license": "Apache-2.0",
"dependencies": {
"tweetnacl": "1.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"license": "Unlicense"
}
}
}

View file

@ -0,0 +1,14 @@
{
"name": "verae-access-authz",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "NATS authorization step for access planes (web, api, zapier, leaf, staff)",
"scripts": {
"start": "node src/server.js",
"test": "node --test test/*.test.js"
},
"dependencies": {
"nats": "^2.28.2"
}
}

View file

@ -0,0 +1,38 @@
/** Call verae.access.authz.check then the internal subject. Fail closed. */
import { SUBJECTS } from './subjects.js';
export async function authorizeThenRequest(nc, sc, { plane, subject, principal, payload, timeout = 2000 }) {
const kind = payload?.kind;
const checkMsg = await nc.request(
SUBJECTS.AUTHZ_CHECK,
sc.encode(JSON.stringify({ plane, subject, principal, kind, payload })),
{ timeout },
);
const decision = JSON.parse(sc.decode(checkMsg.data) || '{}');
if (!decision.allow) {
const err = new Error(decision.reason || 'denied');
err.decision = decision;
throw err;
}
const internal = decision.subject;
const body = { ...payload, plane, principal, traceId: payload?.traceId };
const m = await nc.request(internal, sc.encode(JSON.stringify(body)), { timeout });
return { decision, body: JSON.parse(sc.decode(m.data) || '{}') };
}
export async function authorizeThenPublish(nc, sc, { plane, subject, principal, payload, timeout = 2000 }) {
const kind = payload?.kind;
const checkMsg = await nc.request(
SUBJECTS.AUTHZ_CHECK,
sc.encode(JSON.stringify({ plane, subject, principal, kind, payload })),
{ timeout },
);
const decision = JSON.parse(sc.decode(checkMsg.data) || '{}');
if (!decision.allow) {
const err = new Error(decision.reason || 'denied');
err.decision = decision;
throw err;
}
nc.publish(decision.subject, sc.encode(JSON.stringify({ ...payload, plane, principal })));
return { decision };
}

View file

@ -0,0 +1,98 @@
import { PLANES, parseAddress } from './subjects.js';
/**
* Default-deny. Each access plane may only touch listed internal prefixes.
* Internal workers (poller, WORM, account-balance) are not planes; they
* already sit on the private bus. This policy gates *ingress*.
*/
export const POLICY = Object.freeze({
zapier: {
title: 'Zapier Platform (HTTPS only; never a NATS client)',
allow: [
'verae.zapier.jobs.watch',
'verae.zapier.jobs.events',
'verae.zapier.webhooks.deliver',
'verae.billing.usage.recorded',
],
},
web: {
title: 'Direct customer web (portal browser)',
allow: [
'verae.billing.statement.get',
'verae.billing.balance.adjust',
'verae.billing.payment.recorded',
],
adjustKinds: ['payment', 'reload'],
},
api: {
title: 'Direct customer API (x-api-key, not Zapier)',
allow: ['verae.billing.statement.get', 'verae.billing.usage.recorded', 'verae.zapier.jobs.watch'],
},
leaf: {
title: 'Server-to-server NATS leaf / mTLS',
allow: [
'verae.archive.put',
'verae.archive.query',
'verae.archive.reply.',
'verae.zapier.jobs.watch',
'verae.zapier.jobs.events',
'verae.zapier.webhooks.deliver',
],
},
staff: {
title: 'CS / sales / admin / accounting web',
allow: [
'verae.billing.statement.get',
'verae.billing.balance.get',
'verae.billing.balance.adjust',
'verae.billing.credit.applied',
'verae.billing.payment.recorded',
],
},
});
function prefixAllowed(allow, subject) {
return allow.some((p) => (p.endsWith('.') ? subject.startsWith(p) : subject === p || subject.startsWith(`${p}.`)));
}
/**
* @param {{ plane: string, subject: string, kind?: string, principal?: string }} req
*/
export function authorize(req) {
const plane = String(req?.plane || '');
const parsed = parseAddress(req?.subject, plane);
if (parsed.authz) {
return deny(plane, req?.subject, 'authz subjects are not forwardable');
}
if (!PLANES.includes(plane)) {
return deny(plane, parsed.internal, `unknown access plane`);
}
if (parsed.plane && parsed.plane !== plane) {
return deny(plane, parsed.internal, `plane mismatch (address is ${parsed.plane})`);
}
const internal = parsed.internal;
if (!internal.startsWith('verae.') || internal.startsWith('verae.access.')) {
return deny(plane, internal, 'not an internal verae.* subject');
}
const rule = POLICY[plane];
if (!prefixAllowed(rule.allow, internal)) {
return deny(plane, internal, `${plane} cannot reach ${internal}`);
}
if (internal === 'verae.billing.balance.adjust' && rule.adjustKinds) {
const kind = req?.kind || req?.payload?.kind;
if (kind && !rule.adjustKinds.includes(kind)) {
return deny(plane, internal, `${plane} cannot adjust kind=${kind}`);
}
}
return {
allow: true,
plane,
subject: internal,
principal: req?.principal || null,
reason: 'ok',
};
}
function deny(plane, subject, reason) {
return { allow: false, plane: plane || null, subject: subject || null, reason };
}

View file

@ -0,0 +1,83 @@
#!/usr/bin/env node
import http from 'node:http';
import { authorize, POLICY } from './policy.js';
import { PLANES, SUBJECTS } from './subjects.js';
const PORT = Number(process.env.PORT || process.env.FLEET_HEALTH_PORT || 3020);
const BIND = process.env.FLEET_HEALTH_BIND || '0.0.0.0';
let natsOk = false;
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-access-authz' });
const sc = StringCodec();
const sub = nc.subscribe(SUBJECTS.AUTHZ_CHECK, { queue: SUBJECTS.QUEUE });
(async () => {
for await (const m of sub) {
let payload = {};
try {
payload = JSON.parse(sc.decode(m.data) || '{}');
} catch {
payload = {};
}
const out = authorize(payload);
if (!out.allow && m.reply) {
nc.publish(SUBJECTS.AUTHZ_DENY, sc.encode(JSON.stringify({ ...out, at: new Date().toISOString() })));
}
if (m.reply) m.respond(sc.encode(JSON.stringify(out)));
}
})();
natsOk = true;
process.stdout.write(`access-authz nats ${url}\n`);
}
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: 'verae-access-authz',
nats: natsOk,
planes: PLANES,
subject: SUBJECTS.AUTHZ_CHECK,
});
}
if (req.method === 'GET' && url.pathname === '/policy') {
return json(200, { planes: PLANES, policy: POLICY, subjects: SUBJECTS });
}
if (req.method === 'POST' && url.pathname === '/check') {
const body = await readBody(req);
const out = authorize(body);
return json(out.allow ? 200 : 403, out);
}
json(404, { error: 'not found' });
} catch (err) {
json(500, { error: err.message });
}
});
server.listen(PORT, BIND, () => {
process.stdout.write(`verae-access-authz http://${BIND}:${PORT}/\n`);
});
startNats().catch((err) => process.stderr.write(`nats optional: ${err.message}\n`));

View file

@ -0,0 +1,36 @@
/**
* Access planes sit in front of internal verae.<area>.* addresses.
* Pattern:
* verae.access.<plane>.<area>.<resource>.<action> ingress (that plane only)
* verae.access.authz.check authorization step
* verae.<area>.<resource>.<action> internal (unchanged)
*/
export const PLANES = Object.freeze(['zapier', 'web', 'api', 'leaf', 'staff']);
export const SUBJECTS = Object.freeze({
AUTHZ_CHECK: 'verae.access.authz.check',
AUTHZ_DENY: 'verae.access.authz.deny',
QUEUE: 'access-authz',
ingress: (plane) => `verae.access.${plane}.in`,
});
/** verae.billing.statement.get + plane web → verae.access.web.billing.statement.get */
export function accessAddress(plane, internalSubject) {
if (!PLANES.includes(plane)) throw new Error(`unknown plane ${plane}`);
if (!internalSubject.startsWith('verae.') || internalSubject.startsWith('verae.access.')) {
throw new Error(`not an internal verae.* subject: ${internalSubject}`);
}
return `verae.access.${plane}.${internalSubject.slice('verae.'.length)}`;
}
/** Parse an access or internal subject into { plane, internal }. */
export function parseAddress(subject, planeHint) {
const s = String(subject || '');
const m = s.match(/^verae\.access\.([a-z]+)\.(.+)$/);
if (m) {
const plane = m[1];
if (plane === 'authz') return { plane: planeHint || null, internal: s, authz: true };
return { plane, internal: `verae.${m[2]}`, authz: false };
}
return { plane: planeHint || null, internal: s, authz: false };
}

View file

@ -0,0 +1,35 @@
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('authz health policy and check', async () => {
const port = 18020;
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-access-authz');
const ok = await fetch(`http://127.0.0.1:${port}/check`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ plane: 'web', subject: 'verae.billing.statement.get' }),
});
assert.equal(ok.status, 200);
const no = await fetch(`http://127.0.0.1:${port}/check`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ plane: 'leaf', subject: 'verae.billing.balance.adjust' }),
});
assert.equal(no.status, 403);
} finally {
child.kill('SIGTERM');
}
});

View file

@ -0,0 +1,66 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { authorize } from '../src/policy.js';
import { accessAddress, parseAddress, PLANES } from '../src/subjects.js';
test('five access planes', () => {
assert.deepEqual([...PLANES], ['zapier', 'web', 'api', 'leaf', 'staff']);
});
test('web may read statement and reload, not archive or jobs', () => {
assert.equal(authorize({ plane: 'web', subject: 'verae.billing.statement.get' }).allow, true);
assert.equal(
authorize({ plane: 'web', subject: 'verae.billing.balance.adjust', kind: 'reload' }).allow,
true,
);
assert.equal(
authorize({ plane: 'web', subject: 'verae.billing.balance.adjust', kind: 'credit' }).allow,
false,
);
assert.equal(authorize({ plane: 'web', subject: 'verae.archive.put' }).allow, false);
assert.equal(authorize({ plane: 'web', subject: 'verae.zapier.jobs.watch' }).allow, false);
});
test('leaf may archive and jobs, not billing adjust', () => {
assert.equal(authorize({ plane: 'leaf', subject: 'verae.archive.put' }).allow, true);
assert.equal(authorize({ plane: 'leaf', subject: 'verae.archive.reply.abc' }).allow, true);
assert.equal(authorize({ plane: 'leaf', subject: 'verae.zapier.jobs.watch' }).allow, true);
assert.equal(authorize({ plane: 'leaf', subject: 'verae.billing.balance.adjust' }).allow, false);
assert.equal(authorize({ plane: 'leaf', subject: 'verae.billing.statement.get' }).allow, false);
});
test('zapier may meter and jobs, not customer statement or credits', () => {
assert.equal(authorize({ plane: 'zapier', subject: 'verae.zapier.jobs.watch' }).allow, true);
assert.equal(authorize({ plane: 'zapier', subject: 'verae.billing.usage.recorded' }).allow, true);
assert.equal(authorize({ plane: 'zapier', subject: 'verae.billing.statement.get' }).allow, false);
assert.equal(authorize({ plane: 'zapier', subject: 'verae.billing.balance.adjust' }).allow, false);
assert.equal(authorize({ plane: 'zapier', subject: 'verae.archive.put' }).allow, false);
});
test('api may statement and usage, not credits or archive', () => {
assert.equal(authorize({ plane: 'api', subject: 'verae.billing.statement.get' }).allow, true);
assert.equal(authorize({ plane: 'api', subject: 'verae.billing.usage.recorded' }).allow, true);
assert.equal(authorize({ plane: 'api', subject: 'verae.billing.balance.adjust' }).allow, false);
assert.equal(authorize({ plane: 'api', subject: 'verae.archive.query' }).allow, false);
});
test('staff may credit and statement, not archive', () => {
assert.equal(authorize({ plane: 'staff', subject: 'verae.billing.balance.adjust', kind: 'credit' }).allow, true);
assert.equal(authorize({ plane: 'staff', subject: 'verae.billing.statement.get' }).allow, true);
assert.equal(authorize({ plane: 'staff', subject: 'verae.archive.put' }).allow, false);
});
test('access-prefixed address is mapped to internal', () => {
const addr = accessAddress('leaf', 'verae.archive.put');
assert.equal(addr, 'verae.access.leaf.archive.put');
const parsed = parseAddress(addr);
assert.equal(parsed.plane, 'leaf');
assert.equal(parsed.internal, 'verae.archive.put');
assert.equal(authorize({ plane: 'leaf', subject: addr }).allow, true);
assert.equal(authorize({ plane: 'web', subject: addr }).allow, false);
});
test('unknown plane and authz subjects denied', () => {
assert.equal(authorize({ plane: 'partner', subject: 'verae.archive.put' }).allow, false);
assert.equal(authorize({ plane: 'web', subject: 'verae.access.authz.check' }).allow, false);
});

View file

@ -0,0 +1,9 @@
# NATS — verae-access-leaf
| Direction | Address | Kind |
|-----------|---------|------|
| IN | `verae.access.leaf.in` | queue `access-leaf` `{ token, target, payload, principal }` |
| OUT | `verae.access.authz.check` | request-reply |
| OUT | allowed `target` only | pub after allow |
Do not export `verae.billing.*` to the leaf account.

View file

@ -0,0 +1,13 @@
# verae-access-leaf
**Server-to-server leaf node** access plane. Remote machines connect as NATS leaf nodes (or POST `/forward` with a leaf token). They never get the core billing subjects.
**Forgejo:** https://git.georgelambert.org/marchon/verae-access-leaf
```text
remote leaf → verae.access.leaf.in → authz.check → verae.archive.put | jobs.*
```
Denied: `verae.billing.balance.adjust`, `verae.billing.statement.get`. A compromised WORM/leaf cannot credit customers.
`LEAF_TOKEN` required. Port `:3023`. Do not publish NATS `4222` to the internet; leaf nodes use a private leafnode port / tunnel.

View file

@ -0,0 +1,46 @@
{
"name": "verae-access-leaf",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "verae-access-leaf",
"version": "0.1.0",
"dependencies": {
"nats": "^2.28.2"
}
},
"node_modules/nats": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/nats/-/nats-2.29.3.tgz",
"integrity": "sha512-tOQCRCwC74DgBTk4pWZ9V45sk4d7peoE2njVprMRCBXrhJ5q5cYM7i6W+Uvw2qUrcfOSnuisrX7bEx3b3Wx4QA==",
"deprecated": "Package moved. Use @nats-io/transport-node from https://github.com/nats-io/nats.js",
"license": "Apache-2.0",
"dependencies": {
"nkeys.js": "1.1.0"
},
"engines": {
"node": ">= 14.0.0"
}
},
"node_modules/nkeys.js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/nkeys.js/-/nkeys.js-1.1.0.tgz",
"integrity": "sha512-tB/a0shZL5UZWSwsoeyqfTszONTt4k2YS0tuQioMOD180+MbombYVgzDUYHlx+gejYK6rgf08n/2Df99WY0Sxg==",
"license": "Apache-2.0",
"dependencies": {
"tweetnacl": "1.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"license": "Unlicense"
}
}
}

View file

@ -0,0 +1,14 @@
{
"name": "verae-access-leaf",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Server-to-server NATS leaf-node access plane",
"scripts": {
"start": "node src/server.js",
"test": "node --test test/*.test.js"
},
"dependencies": {
"nats": "^2.28.2"
}
}

View file

@ -0,0 +1,94 @@
#!/usr/bin/env node
/**
* Server-to-server / NATS leaf-node ingress.
* Remote servers publish verae.access.leaf.in; this process authz-checks
* then forwards only allowed internal subjects (archive + jobs, never billing).
*/
import http from 'node:http';
const PORT = Number(process.env.PORT || 3023);
const AUTHZ = (process.env.AUTHZ_URL || 'http://127.0.0.1:3020').replace(/\/$/, '');
const TOKEN = process.env.LEAF_TOKEN || 'leaf-dev-token';
const PLANE = 'leaf';
const IN = 'verae.access.leaf.in';
async function check(subject, principal) {
const r = await fetch(`${AUTHZ}/check`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ plane: PLANE, subject, principal }),
});
return r.json();
}
function authorizedToken(got) {
return got && got === TOKEN;
}
async function forward(body) {
if (!authorizedToken(body.token)) {
return { status: 401, body: { allow: false, reason: 'bad leaf token' } };
}
const target = body.target || body.subject;
const gate = await check(target, body.principal || 'leaf');
if (!gate.allow) return { status: 403, body: gate };
const url = process.env.NATS_URL;
if (url) {
const { connect, StringCodec } = await import('nats');
const nc = await connect({ servers: url.split(','), name: 'verae-access-leaf' });
const sc = StringCodec();
nc.publish(gate.subject, sc.encode(JSON.stringify({ ...(body.payload || {}), plane: PLANE })));
await nc.flush();
await nc.close();
}
return { status: 200, body: { forwarded: gate.subject, plane: PLANE, nats: Boolean(url) } };
}
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-access-leaf' });
const sc = StringCodec();
(async () => {
for await (const m of nc.subscribe(IN, { queue: 'access-leaf' })) {
let payload = {};
try {
payload = JSON.parse(sc.decode(m.data) || '{}');
} catch {
payload = {};
}
const out = await forward(payload);
if (m.reply) m.respond(sc.encode(JSON.stringify(out.body)));
}
})();
process.stdout.write(`access-leaf nats ${IN}\n`);
}
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-leaf', plane: PLANE, ingress: IN });
}
if (req.method === 'POST' && url.pathname === '/forward') {
const chunks = [];
for await (const c of req) chunks.push(c);
const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
const out = await forward(body);
return json(out.status, out.body);
}
json(404, { error: 'not found' });
} catch (err) {
json(502, { error: err.message });
}
});
server.listen(PORT, '0.0.0.0', () => {
process.stdout.write(`verae-access-leaf http://0.0.0.0:${PORT}/ plane=${PLANE}\n`);
});
startNats().catch((err) => process.stderr.write(`nats optional: ${err.message}\n`));

View file

@ -0,0 +1,63 @@
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');
test('leaf forwards archive.put and refuses billing adjust', async () => {
const authzPort = 18026;
const leafPort = 18027;
const authz = spawn(process.execPath, ['src/server.js'], {
cwd: authzRoot,
env: { ...process.env, PORT: String(authzPort) },
stdio: ['ignore', 'pipe', 'pipe'],
});
const leaf = spawn(process.execPath, ['src/server.js'], {
cwd: root,
env: {
...process.env,
PORT: String(leafPort),
AUTHZ_URL: `http://127.0.0.1:${authzPort}`,
LEAF_TOKEN: 'leaf-dev-token',
},
stdio: ['ignore', 'pipe', 'pipe'],
});
await new Promise((r) => setTimeout(r, 500));
try {
const h = await (await fetch(`http://127.0.0.1:${leafPort}/health`)).json();
assert.equal(h.plane, 'leaf');
const ok = await fetch(`http://127.0.0.1:${leafPort}/forward`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
token: 'leaf-dev-token',
target: 'verae.archive.put',
principal: 'ns2',
payload: { sha256: 'abc' },
}),
});
assert.equal(ok.status, 200);
const no = await fetch(`http://127.0.0.1:${leafPort}/forward`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
token: 'leaf-dev-token',
target: 'verae.billing.balance.adjust',
payload: { customerId: 'c1', cents: 9999 },
}),
});
assert.equal(no.status, 403);
const bad = await fetch(`http://127.0.0.1:${leafPort}/forward`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ token: 'wrong', target: 'verae.archive.put' }),
});
assert.equal(bad.status, 401);
} finally {
leaf.kill('SIGTERM');
authz.kill('SIGTERM');
}
});

View file

@ -0,0 +1,3 @@
# NATS — verae-access-web
Plane `web`. Ingress `verae.access.web.in` / `verae.access.web.billing.*`. Must pass `verae.access.authz.check`. Internal: `verae.billing.statement.get`, `verae.billing.balance.adjust` (reload/payment only).

View file

@ -0,0 +1,15 @@
# verae-access-web
**Direct web** access plane. Customer browsers hit this process, not Zapier and not the NATS port.
**Forgejo:** https://git.georgelambert.org/marchon/verae-access-web
Every NATS hop is `verae.access.web.*``verae.access.authz.check` → internal `verae.billing.statement.get` (reload only, not CS credits).
| Route | Job |
|-------|-----|
| `GET /health` | `{ plane: "web" }` |
| `GET /statement/:id` | Authz then statement |
| `POST /reload` | Authz then `balance.adjust` kind=reload |
Port `:3021`.

View file

@ -0,0 +1,46 @@
{
"name": "verae-access-web",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "verae-access-web",
"version": "0.1.0",
"dependencies": {
"nats": "^2.28.2"
}
},
"node_modules/nats": {
"version": "2.29.3",
"resolved": "https://registry.npmjs.org/nats/-/nats-2.29.3.tgz",
"integrity": "sha512-tOQCRCwC74DgBTk4pWZ9V45sk4d7peoE2njVprMRCBXrhJ5q5cYM7i6W+Uvw2qUrcfOSnuisrX7bEx3b3Wx4QA==",
"deprecated": "Package moved. Use @nats-io/transport-node from https://github.com/nats-io/nats.js",
"license": "Apache-2.0",
"dependencies": {
"nkeys.js": "1.1.0"
},
"engines": {
"node": ">= 14.0.0"
}
},
"node_modules/nkeys.js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/nkeys.js/-/nkeys.js-1.1.0.tgz",
"integrity": "sha512-tB/a0shZL5UZWSwsoeyqfTszONTt4k2YS0tuQioMOD180+MbombYVgzDUYHlx+gejYK6rgf08n/2Df99WY0Sxg==",
"license": "Apache-2.0",
"dependencies": {
"tweetnacl": "1.0.3"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/tweetnacl": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
"license": "Unlicense"
}
}
}

View file

@ -0,0 +1,14 @@
{
"name": "verae-access-web",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Direct customer web access plane (not Zapier)",
"scripts": {
"start": "node src/server.js",
"test": "node --test test/*.test.js"
},
"dependencies": {
"nats": "^2.28.2"
}
}

View file

@ -0,0 +1,50 @@
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(/\/$/, '');
export const PLANE = 'web';
export const AUTHZ_CHECK = 'verae.access.authz.check';
export async function check(subject, { principal, kind, payload } = {}) {
const r = await fetch(`${AUTHZ}/check`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ plane: PLANE, subject, principal, kind, payload }),
});
return { status: r.status, body: await r.json().catch(() => ({ allow: false })) };
}
export async function statement(customerId, principal) {
const gate = await check('verae.billing.statement.get', { principal });
if (!gate.body.allow) return { status: 403, body: gate.body };
const url = process.env.NATS_URL;
if (url) {
try {
const { connect, StringCodec } = await import('nats');
const nc = await connect({ servers: url.split(','), name: 'verae-access-web' });
const sc = StringCodec();
const m = await nc.request(
'verae.billing.statement.get',
sc.encode(JSON.stringify({ customerId, plane: PLANE, principal })),
{ timeout: 2000 },
);
const out = JSON.parse(sc.decode(m.data) || '{}');
await nc.close();
return { status: 200, body: { ...out, source: 'nats', plane: PLANE } };
} catch {
/* HTTP fallback */
}
}
const r = await fetch(`${BOOKS}/statement/${customerId}`);
return { status: r.status, body: { ...(await r.json().catch(() => ({}))), source: 'account-balance', plane: PLANE } };
}
export async function reload(customerId, cents, principal) {
const gate = await check('verae.billing.balance.adjust', { principal, kind: 'reload' });
if (!gate.body.allow) return { status: 403, body: gate.body };
const r = await fetch(`${BOOKS}/adjust`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ customerId, cents, reason: 'reload', agent: principal || 'web', kind: 'reload' }),
});
return { status: r.status, body: { ...(await r.json().catch(() => ({}))), plane: PLANE } };
}

View file

@ -0,0 +1,38 @@
#!/usr/bin/env node
/** Direct customer web access. Not Zapier. NATS only after authz. */
import http from 'node:http';
import { PLANE, statement, reload } from './gate.js';
const PORT = Number(process.env.PORT || 3021);
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-web', plane: PLANE });
}
const st = url.pathname.match(/^\/statement\/([^/]+)$/);
if (req.method === 'GET' && st) {
const out = await statement(st[1], st[1]);
return json(out.status, out.body);
}
if (req.method === 'POST' && url.pathname === '/reload') {
const chunks = [];
for await (const c of req) chunks.push(c);
const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
const out = await reload(body.customerId, body.cents, body.customerId);
return json(out.status, out.body);
}
json(404, { error: 'not found' });
} catch (err) {
json(502, { error: err.message });
}
});
server.listen(PORT, '0.0.0.0', () => {
process.stdout.write(`verae-access-web http://0.0.0.0:${PORT}/ plane=${PLANE}\n`);
});

View file

@ -0,0 +1,52 @@
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('web plane can read statement after authz, cannot skip authz', async () => {
const authzPort = 18021;
const booksPort = 18022;
const webPort = 18023;
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) },
stdio: ['ignore', 'pipe', 'pipe'],
});
const web = spawn(process.execPath, ['src/server.js'], {
cwd: root,
env: {
...process.env,
PORT: String(webPort),
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 {
await fetch(`http://127.0.0.1:${booksPort}/adjust`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ customerId: 'c-web', cents: 400, kind: 'reload' }),
});
const h = await (await fetch(`http://127.0.0.1:${webPort}/health`)).json();
assert.equal(h.plane, 'web');
const st = await (await fetch(`http://127.0.0.1:${webPort}/statement/c-web`)).json();
assert.equal(st.prepaidCents, 400);
assert.equal(st.plane, 'web');
} finally {
web.kill('SIGTERM');
books.kill('SIGTERM');
authz.kill('SIGTERM');
}
});

View file

@ -0,0 +1,3 @@
# NATS — verae-access-zapier
Plane `zapier`. Authz then `verae.zapier.jobs.watch` / `verae.billing.usage.recorded`. Denied: statement.get, balance.adjust, archive.*.

View file

@ -0,0 +1,9 @@
# verae-access-zapier
**Zapier is one access plane**, not the only one. This process is the Zapier HTTPS front door: timestamp/wait/hash into middleware. No customer portal, no admin, no leaf.
**Forgejo:** https://git.georgelambert.org/marchon/verae-access-zapier
Zapier cloud never opens NATS. This process may request `verae.access.authz.check` for `verae.zapier.jobs.watch` and `verae.billing.usage.recorded` only.
Port `:3024`.

View file

@ -0,0 +1,11 @@
{
"name": "verae-access-zapier",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Zapier Platform access plane (HTTPS only, no portal)",
"scripts": {
"start": "node src/server.js",
"test": "node --test test/*.test.js"
}
}

View file

@ -0,0 +1,55 @@
#!/usr/bin/env node
/**
* Zapier Platform access plane only. No portal, no customer API keys, no leaf.
* Zapier cloud still never connects to NATS; this process does after authz.
*/
import http from 'node:http';
const PORT = Number(process.env.PORT || 3024);
const AUTHZ = (process.env.AUTHZ_URL || 'http://127.0.0.1:3020').replace(/\/$/, '');
const MW = (process.env.ZAPPIER_UPSTREAM || 'http://127.0.0.1:3100').replace(/\/$/, '');
const PLANE = 'zapier';
async function check(subject, principal) {
const r = await fetch(`${AUTHZ}/check`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ plane: PLANE, subject, principal }),
});
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-zapier', plane: PLANE });
}
if (url.pathname.startsWith('/portal') || url.pathname.startsWith('/admin')) {
return json(404, { error: 'zapier plane has no portal/admin', plane: PLANE });
}
if (req.method === 'POST' && (url.pathname === '/v1/timestamp' || url.pathname === '/zapier/v1/timestamp')) {
const gate = await check('verae.zapier.jobs.watch', 'zapier-app');
if (!gate.allow) return json(403, gate);
const chunks = [];
for await (const c of req) chunks.push(c);
const r = await fetch(`${MW}/zapier/v1/timestamp`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: Buffer.concat(chunks),
});
return json(r.status, { ...(await r.json().catch(() => ({}))), plane: PLANE });
}
json(404, { error: 'not found', plane: PLANE });
} catch (err) {
json(502, { error: err.message });
}
});
server.listen(PORT, '0.0.0.0', () => {
process.stdout.write(`verae-access-zapier http://0.0.0.0:${PORT}/ plane=${PLANE}\n`);
});

View file

@ -0,0 +1,39 @@
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');
test('zapier plane has no portal and cannot read statements', async () => {
const authzPort = 18028;
const zPort = 18029;
const authz = spawn(process.execPath, ['src/server.js'], {
cwd: authzRoot,
env: { ...process.env, PORT: String(authzPort) },
stdio: ['ignore', 'pipe', 'pipe'],
});
const z = spawn(process.execPath, ['src/server.js'], {
cwd: root,
env: { ...process.env, PORT: String(zPort), AUTHZ_URL: `http://127.0.0.1:${authzPort}` },
stdio: ['ignore', 'pipe', 'pipe'],
});
await new Promise((r) => setTimeout(r, 500));
try {
const h = await (await fetch(`http://127.0.0.1:${zPort}/health`)).json();
assert.equal(h.plane, 'zapier');
const portal = await fetch(`http://127.0.0.1:${zPort}/portal`);
assert.equal(portal.status, 404);
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.statement.get' }),
});
assert.equal(deny.status, 403);
} finally {
z.kill('SIGTERM');
authz.kill('SIGTERM');
}
});

View file

@ -15,6 +15,11 @@
"services": {
"nats": { "min": 1, "max": 1, "keepFloor": false, "enabled": true, "managed": false },
"zappier-edge": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"access-authz": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"access-web": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"access-api": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"access-leaf": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"access-zapier": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"account-balance": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"customer-service": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },
"sales-pricing": { "min": 1, "max": 1, "keepFloor": true, "enabled": true },

View file

@ -0,0 +1,27 @@
{
"id": "access-api",
"title": "Direct customer API access",
"kind": "http",
"package": "verae-access-api",
"role": "access-api",
"managed": true,
"runtime": "HTTP :3022 plane=api",
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
"ports": { "healthBase": 3022 },
"spawn": {
"cwd": "../verae-access-api",
"command": "node",
"args": ["src/server.js"]
},
"env": {
"PORT": "3022",
"AUTHZ_URL": "http://127.0.0.1:3020",
"ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010",
"ZAPPIER_UPSTREAM": "http://127.0.0.1:3100",
"NATS_URL": "nats://127.0.0.1:4222"
},
"nats": {
"in": [],
"out": ["verae.access.authz.check", "verae.billing.statement.get", "verae.zapier.jobs.watch"]
}
}

View file

@ -0,0 +1,24 @@
{
"id": "access-authz",
"title": "Access-plane authorization",
"kind": "http",
"package": "verae-access-authz",
"role": "access-authz",
"managed": true,
"runtime": "HTTP :3020 + NATS verae.access.authz.check",
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
"ports": { "healthBase": 3020 },
"spawn": {
"cwd": "../verae-access-authz",
"command": "node",
"args": ["src/server.js"]
},
"env": {
"PORT": "3020",
"NATS_URL": "nats://127.0.0.1:4222"
},
"nats": {
"in": ["verae.access.authz.check"],
"out": ["verae.access.authz.deny"]
}
}

View file

@ -0,0 +1,26 @@
{
"id": "access-leaf",
"title": "Server-to-server NATS leaf access",
"kind": "http",
"package": "verae-access-leaf",
"role": "access-leaf",
"managed": true,
"runtime": "HTTP :3023 + NATS verae.access.leaf.in",
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
"ports": { "healthBase": 3023 },
"spawn": {
"cwd": "../verae-access-leaf",
"command": "node",
"args": ["src/server.js"]
},
"env": {
"PORT": "3023",
"AUTHZ_URL": "http://127.0.0.1:3020",
"LEAF_TOKEN": "leaf-dev-token",
"NATS_URL": "nats://127.0.0.1:4222"
},
"nats": {
"in": ["verae.access.leaf.in"],
"out": ["verae.access.authz.check", "verae.archive.put", "verae.archive.query"]
}
}

View file

@ -0,0 +1,26 @@
{
"id": "access-web",
"title": "Direct customer web access",
"kind": "http",
"package": "verae-access-web",
"role": "access-web",
"managed": true,
"runtime": "HTTP :3021 plane=web",
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
"ports": { "healthBase": 3021 },
"spawn": {
"cwd": "../verae-access-web",
"command": "node",
"args": ["src/server.js"]
},
"env": {
"PORT": "3021",
"AUTHZ_URL": "http://127.0.0.1:3020",
"ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010",
"NATS_URL": "nats://127.0.0.1:4222"
},
"nats": {
"in": [],
"out": ["verae.access.authz.check", "verae.billing.statement.get", "verae.billing.balance.adjust"]
}
}

View file

@ -0,0 +1,26 @@
{
"id": "access-zapier",
"title": "Zapier Platform access plane",
"kind": "http",
"package": "verae-access-zapier",
"role": "access-zapier",
"managed": true,
"runtime": "HTTP :3024 plane=zapier (no portal)",
"health": { "type": "http", "path": "/health", "timeoutMs": 2000 },
"ports": { "healthBase": 3024 },
"spawn": {
"cwd": "../verae-access-zapier",
"command": "node",
"args": ["src/server.js"]
},
"env": {
"PORT": "3024",
"AUTHZ_URL": "http://127.0.0.1:3020",
"ZAPPIER_UPSTREAM": "http://127.0.0.1:3100",
"NATS_URL": "nats://127.0.0.1:4222"
},
"nats": {
"in": [],
"out": ["verae.access.authz.check", "verae.zapier.jobs.watch"]
}
}

Some files were not shown because too many files have changed in this diff Show more