diff --git a/docs/modules/README.md b/docs/modules/README.md index 19309bf..75c84d7 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -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) diff --git a/docs/modules/verae-access-api/server.md b/docs/modules/verae-access-api/server.md new file mode 100644 index 0000000..385849d --- /dev/null +++ b/docs/modules/verae-access-api/server.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-access-authz/client.md b/docs/modules/verae-access-authz/client.md new file mode 100644 index 0000000..ae42cbc --- /dev/null +++ b/docs/modules/verae-access-authz/client.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-access-authz/policy.md b/docs/modules/verae-access-authz/policy.md new file mode 100644 index 0000000..ddeee18 --- /dev/null +++ b/docs/modules/verae-access-authz/policy.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-access-authz/server.md b/docs/modules/verae-access-authz/server.md new file mode 100644 index 0000000..632d2de --- /dev/null +++ b/docs/modules/verae-access-authz/server.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-access-authz/subjects.md b/docs/modules/verae-access-authz/subjects.md new file mode 100644 index 0000000..fc6bdf2 --- /dev/null +++ b/docs/modules/verae-access-authz/subjects.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-access-leaf/server.md b/docs/modules/verae-access-leaf/server.md new file mode 100644 index 0000000..d7b1fba --- /dev/null +++ b/docs/modules/verae-access-leaf/server.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-access-web/gate.md b/docs/modules/verae-access-web/gate.md new file mode 100644 index 0000000..484e01e --- /dev/null +++ b/docs/modules/verae-access-web/gate.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-access-web/server.md b/docs/modules/verae-access-web/server.md new file mode 100644 index 0000000..c659689 --- /dev/null +++ b/docs/modules/verae-access-web/server.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/verae-access-zapier/server.md b/docs/modules/verae-access-zapier/server.md new file mode 100644 index 0000000..258cb8b --- /dev/null +++ b/docs/modules/verae-access-zapier/server.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/modules/zappier-customer-service/nats-billing.md b/docs/modules/zappier-customer-service/nats-billing.md index aed55e1..178a917 100644 --- a/docs/modules/zappier-customer-service/nats-billing.md +++ b/docs/modules/zappier-customer-service/nats-billing.md @@ -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 module’s MD for parameter and return types. diff --git a/docs/modules/zappier-sales-pricing/nats-billing.md b/docs/modules/zappier-sales-pricing/nats-billing.md index dcd029e..8c31d6b 100644 --- a/docs/modules/zappier-sales-pricing/nats-billing.md +++ b/docs/modules/zappier-sales-pricing/nats-billing.md @@ -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 module’s MD for parameter and return types. diff --git a/docs/modules/zappier/admin.md b/docs/modules/zappier/admin.md index 445ea51..f783eb0 100644 --- a/docs/modules/zappier/admin.md +++ b/docs/modules/zappier/admin.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/admin.ts` -**Lines:** 588 +**Lines:** 591 ## What this module is diff --git a/docs/modules/zappier/billing-nats.md b/docs/modules/zappier/billing-nats.md index caf750b..f4ec548 100644 --- a/docs/modules/zappier/billing-nats.md +++ b/docs/modules/zappier/billing-nats.md @@ -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 module’s MD for parameter and return types. diff --git a/docs/modules/zappier/portal.md b/docs/modules/zappier/portal.md index 3a10365..c3123d1 100644 --- a/docs/modules/zappier/portal.md +++ b/docs/modules/zappier/portal.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/portal.ts` -**Lines:** 321 +**Lines:** 328 ## What this module is diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst index 66d11b7..c8bf17b 100644 --- a/docs/sphinx/index.rst +++ b/docs/sphinx/index.rst @@ -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 diff --git a/docs/sphinx/modules/verae-access-api/server.md b/docs/sphinx/modules/verae-access-api/server.md new file mode 100644 index 0000000..385849d --- /dev/null +++ b/docs/sphinx/modules/verae-access-api/server.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-access-api/server.rst b/docs/sphinx/modules/verae-access-api/server.rst new file mode 100644 index 0000000..515dc51 --- /dev/null +++ b/docs/sphinx/modules/verae-access-api/server.rst @@ -0,0 +1,7 @@ +verae-access-api.server +======================= + +Generated API sheet for ``verae-access-api/server``. + +.. include:: server.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-access-authz/client.md b/docs/sphinx/modules/verae-access-authz/client.md new file mode 100644 index 0000000..ae42cbc --- /dev/null +++ b/docs/sphinx/modules/verae-access-authz/client.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-access-authz/client.rst b/docs/sphinx/modules/verae-access-authz/client.rst new file mode 100644 index 0000000..0b061f4 --- /dev/null +++ b/docs/sphinx/modules/verae-access-authz/client.rst @@ -0,0 +1,7 @@ +verae-access-authz.client +========================= + +Generated API sheet for ``verae-access-authz/client``. + +.. include:: client.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-access-authz/policy.md b/docs/sphinx/modules/verae-access-authz/policy.md new file mode 100644 index 0000000..ddeee18 --- /dev/null +++ b/docs/sphinx/modules/verae-access-authz/policy.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-access-authz/policy.rst b/docs/sphinx/modules/verae-access-authz/policy.rst new file mode 100644 index 0000000..2ad3dc3 --- /dev/null +++ b/docs/sphinx/modules/verae-access-authz/policy.rst @@ -0,0 +1,7 @@ +verae-access-authz.policy +========================= + +Generated API sheet for ``verae-access-authz/policy``. + +.. include:: policy.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-access-authz/server.md b/docs/sphinx/modules/verae-access-authz/server.md new file mode 100644 index 0000000..632d2de --- /dev/null +++ b/docs/sphinx/modules/verae-access-authz/server.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-access-authz/server.rst b/docs/sphinx/modules/verae-access-authz/server.rst new file mode 100644 index 0000000..89d5708 --- /dev/null +++ b/docs/sphinx/modules/verae-access-authz/server.rst @@ -0,0 +1,7 @@ +verae-access-authz.server +========================= + +Generated API sheet for ``verae-access-authz/server``. + +.. include:: server.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-access-authz/subjects.md b/docs/sphinx/modules/verae-access-authz/subjects.md new file mode 100644 index 0000000..fc6bdf2 --- /dev/null +++ b/docs/sphinx/modules/verae-access-authz/subjects.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-access-authz/subjects.rst b/docs/sphinx/modules/verae-access-authz/subjects.rst new file mode 100644 index 0000000..0a75665 --- /dev/null +++ b/docs/sphinx/modules/verae-access-authz/subjects.rst @@ -0,0 +1,7 @@ +verae-access-authz.subjects +=========================== + +Generated API sheet for ``verae-access-authz/subjects``. + +.. include:: subjects.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-access-leaf/server.md b/docs/sphinx/modules/verae-access-leaf/server.md new file mode 100644 index 0000000..d7b1fba --- /dev/null +++ b/docs/sphinx/modules/verae-access-leaf/server.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-access-leaf/server.rst b/docs/sphinx/modules/verae-access-leaf/server.rst new file mode 100644 index 0000000..c32301f --- /dev/null +++ b/docs/sphinx/modules/verae-access-leaf/server.rst @@ -0,0 +1,7 @@ +verae-access-leaf.server +======================== + +Generated API sheet for ``verae-access-leaf/server``. + +.. include:: server.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-access-web/gate.md b/docs/sphinx/modules/verae-access-web/gate.md new file mode 100644 index 0000000..484e01e --- /dev/null +++ b/docs/sphinx/modules/verae-access-web/gate.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-access-web/gate.rst b/docs/sphinx/modules/verae-access-web/gate.rst new file mode 100644 index 0000000..1bf2da1 --- /dev/null +++ b/docs/sphinx/modules/verae-access-web/gate.rst @@ -0,0 +1,7 @@ +verae-access-web.gate +===================== + +Generated API sheet for ``verae-access-web/gate``. + +.. include:: gate.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-access-web/server.md b/docs/sphinx/modules/verae-access-web/server.md new file mode 100644 index 0000000..c659689 --- /dev/null +++ b/docs/sphinx/modules/verae-access-web/server.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-access-web/server.rst b/docs/sphinx/modules/verae-access-web/server.rst new file mode 100644 index 0000000..6b0a6d5 --- /dev/null +++ b/docs/sphinx/modules/verae-access-web/server.rst @@ -0,0 +1,7 @@ +verae-access-web.server +======================= + +Generated API sheet for ``verae-access-web/server``. + +.. include:: server.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/verae-access-zapier/server.md b/docs/sphinx/modules/verae-access-zapier/server.md new file mode 100644 index 0000000..258cb8b --- /dev/null +++ b/docs/sphinx/modules/verae-access-zapier/server.md @@ -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 module’s MD for parameter and return types. + +## Return values (how to read this) + +- HTTP route handlers return Express `res.json(...)` bodies (see route docs). +- Zapier `perform` functions return a **single object** (creates) or an **array** (triggers/searches). +- Pricing functions return integer **cents** on `Quote.totalCents`. + diff --git a/docs/sphinx/modules/verae-access-zapier/server.rst b/docs/sphinx/modules/verae-access-zapier/server.rst new file mode 100644 index 0000000..9ad214d --- /dev/null +++ b/docs/sphinx/modules/verae-access-zapier/server.rst @@ -0,0 +1,7 @@ +verae-access-zapier.server +========================== + +Generated API sheet for ``verae-access-zapier/server``. + +.. include:: server.md + :parser: myst_parser.sphinx_ diff --git a/docs/sphinx/modules/zappier-customer-service/nats-billing.md b/docs/sphinx/modules/zappier-customer-service/nats-billing.md index aed55e1..178a917 100644 --- a/docs/sphinx/modules/zappier-customer-service/nats-billing.md +++ b/docs/sphinx/modules/zappier-customer-service/nats-billing.md @@ -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 module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/zappier-sales-pricing/nats-billing.md b/docs/sphinx/modules/zappier-sales-pricing/nats-billing.md index dcd029e..8c31d6b 100644 --- a/docs/sphinx/modules/zappier-sales-pricing/nats-billing.md +++ b/docs/sphinx/modules/zappier-sales-pricing/nats-billing.md @@ -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 module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/zappier/admin.md b/docs/sphinx/modules/zappier/admin.md index 445ea51..f783eb0 100644 --- a/docs/sphinx/modules/zappier/admin.md +++ b/docs/sphinx/modules/zappier/admin.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/admin.ts` -**Lines:** 588 +**Lines:** 591 ## What this module is diff --git a/docs/sphinx/modules/zappier/billing-nats.md b/docs/sphinx/modules/zappier/billing-nats.md index caf750b..f4ec548 100644 --- a/docs/sphinx/modules/zappier/billing-nats.md +++ b/docs/sphinx/modules/zappier/billing-nats.md @@ -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 module’s MD for parameter and return types. diff --git a/docs/sphinx/modules/zappier/portal.md b/docs/sphinx/modules/zappier/portal.md index 3a10365..c3123d1 100644 --- a/docs/sphinx/modules/zappier/portal.md +++ b/docs/sphinx/modules/zappier/portal.md @@ -2,7 +2,7 @@ **Package:** `zappier` **Source:** `packages/zappier/src/portal.ts` -**Lines:** 321 +**Lines:** 328 ## What this module is diff --git a/docs/sphinx/repos/verae-access-api.md b/docs/sphinx/repos/verae-access-api.md new file mode 100644 index 0000000..74102be --- /dev/null +++ b/docs/sphinx/repos/verae-access-api.md @@ -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`. diff --git a/docs/sphinx/repos/verae-access-authz.md b/docs/sphinx/repos/verae-access-authz.md new file mode 100644 index 0000000..c14cb93 --- /dev/null +++ b/docs/sphinx/repos/verae-access-authz.md @@ -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.... (ingress, that plane only) + → verae.access.authz.check (allow / deny) + → verae... (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"}' +``` diff --git a/docs/sphinx/repos/verae-access-leaf.md b/docs/sphinx/repos/verae-access-leaf.md new file mode 100644 index 0000000..ce2f273 --- /dev/null +++ b/docs/sphinx/repos/verae-access-leaf.md @@ -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. diff --git a/docs/sphinx/repos/verae-access-web.md b/docs/sphinx/repos/verae-access-web.md new file mode 100644 index 0000000..5d3414e --- /dev/null +++ b/docs/sphinx/repos/verae-access-web.md @@ -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`. diff --git a/docs/sphinx/repos/verae-access-zapier.md b/docs/sphinx/repos/verae-access-zapier.md new file mode 100644 index 0000000..e25cf46 --- /dev/null +++ b/docs/sphinx/repos/verae-access-zapier.md @@ -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`. diff --git a/packages/docs-master/modules-and-nats.md b/packages/docs-master/modules-and-nats.md index d0d14a9..6451653 100644 --- a/packages/docs-master/modules-and-nats.md +++ b/packages/docs-master/modules-and-nats.md @@ -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.` | replies | `archiveId`, `sha256`, `records[]`, `traceId` | diff --git a/packages/docs-master/modules/verae-access-api/NATS.md b/packages/docs-master/modules/verae-access-api/NATS.md new file mode 100644 index 0000000..764ac45 --- /dev/null +++ b/packages/docs-master/modules/verae-access-api/NATS.md @@ -0,0 +1,3 @@ +# NATS — verae-access-api + +Plane `api`. Statement and jobs.watch after authz. No credits, no archive. diff --git a/packages/docs-master/modules/verae-access-api/SUMMARY.md b/packages/docs-master/modules/verae-access-api/SUMMARY.md new file mode 100644 index 0000000..9a0401e --- /dev/null +++ b/packages/docs-master/modules/verae-access-api/SUMMARY.md @@ -0,0 +1,3 @@ +# verae-access-api + +Direct customer API (`x-api-key`). Separate from Zapier Platform. diff --git a/packages/docs-master/modules/verae-access-authz/NATS.md b/packages/docs-master/modules/verae-access-authz/NATS.md new file mode 100644 index 0000000..5e41e52 --- /dev/null +++ b/packages/docs-master/modules/verae-access-authz/NATS.md @@ -0,0 +1,3 @@ +# NATS — verae-access-authz + +`verae.access.authz.check` request-reply. Queue `access-authz`. Fail closed. diff --git a/packages/docs-master/modules/verae-access-authz/SUMMARY.md b/packages/docs-master/modules/verae-access-authz/SUMMARY.md new file mode 100644 index 0000000..65215eb --- /dev/null +++ b/packages/docs-master/modules/verae-access-authz/SUMMARY.md @@ -0,0 +1,3 @@ +# verae-access-authz + +Default-deny authorization step on the NATS address path. Planes: zapier, web, api, leaf, staff. diff --git a/packages/docs-master/modules/verae-access-leaf/NATS.md b/packages/docs-master/modules/verae-access-leaf/NATS.md new file mode 100644 index 0000000..546d4b4 --- /dev/null +++ b/packages/docs-master/modules/verae-access-leaf/NATS.md @@ -0,0 +1,3 @@ +# NATS — verae-access-leaf + +`verae.access.leaf.in` → authz.check → `verae.archive.*` / jobs.*. Denied: billing. diff --git a/packages/docs-master/modules/verae-access-leaf/SUMMARY.md b/packages/docs-master/modules/verae-access-leaf/SUMMARY.md new file mode 100644 index 0000000..389a693 --- /dev/null +++ b/packages/docs-master/modules/verae-access-leaf/SUMMARY.md @@ -0,0 +1,3 @@ +# verae-access-leaf + +Server-to-server NATS leaf ingress. Archive and jobs only. Never billing. diff --git a/packages/docs-master/modules/verae-access-web/NATS.md b/packages/docs-master/modules/verae-access-web/NATS.md new file mode 100644 index 0000000..7f0c825 --- /dev/null +++ b/packages/docs-master/modules/verae-access-web/NATS.md @@ -0,0 +1,3 @@ +# NATS — verae-access-web + +Plane `web`. Authz then `verae.billing.statement.get` / `balance.adjust` kind=reload. diff --git a/packages/docs-master/modules/verae-access-web/SUMMARY.md b/packages/docs-master/modules/verae-access-web/SUMMARY.md new file mode 100644 index 0000000..56e767b --- /dev/null +++ b/packages/docs-master/modules/verae-access-web/SUMMARY.md @@ -0,0 +1,3 @@ +# verae-access-web + +Direct customer web plane. Not Zapier. Statement + reload only after authz. diff --git a/packages/docs-master/modules/verae-access-zapier/NATS.md b/packages/docs-master/modules/verae-access-zapier/NATS.md new file mode 100644 index 0000000..3ebabee --- /dev/null +++ b/packages/docs-master/modules/verae-access-zapier/NATS.md @@ -0,0 +1,3 @@ +# NATS — verae-access-zapier + +Plane `zapier`. Authz then jobs.watch / usage.recorded. Zapier cloud never connects to NATS. diff --git a/packages/docs-master/modules/verae-access-zapier/SUMMARY.md b/packages/docs-master/modules/verae-access-zapier/SUMMARY.md new file mode 100644 index 0000000..de4e8c8 --- /dev/null +++ b/packages/docs-master/modules/verae-access-zapier/SUMMARY.md @@ -0,0 +1,3 @@ +# verae-access-zapier + +Zapier Platform HTTPS plane only. No portal, no admin, no leaf. diff --git a/packages/overview/01-system.md b/packages/overview/01-system.md index 35089e8..f887712 100644 --- a/packages/overview/01-system.md +++ b/packages/overview/01-system.md @@ -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 | diff --git a/packages/overview/02-modules-and-repos.md b/packages/overview/02-modules-and-repos.md index 4be3f26..36c329c 100644 --- a/packages/overview/02-modules-and-repos.md +++ b/packages/overview/02-modules-and-repos.md @@ -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 | diff --git a/packages/overview/03-nats-cluster.md b/packages/overview/03-nats-cluster.md index fc3042b..4769ba6 100644 --- a/packages/overview/03-nats-cluster.md +++ b/packages/overview/03-nats-cluster.md @@ -20,7 +20,7 @@ Today’s 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 diff --git a/packages/overview/06-address-routing.md b/packages/overview/06-address-routing.md index 5f0d666..d9abdac 100644 --- a/packages/overview/06-address-routing.md +++ b/packages/overview/06-address-routing.md @@ -5,10 +5,14 @@ NATS **addresses** (subjects) are the extension point. A new search, store, or j ## Pattern ```text -verae... +verae.access.... # ingress (one plane) +verae.access.authz.check # authorization step +verae... # internal bus verae...reply. ``` +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...reply. 2. Rename `verae.example.process.in` / `.out` / `.reply.*` in `src/subjects.js`. 3. Add a row to that repo’s `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. diff --git a/packages/overview/08-diagrams.md b/packages/overview/08-diagrams.md index d78e79e..75d4299 100644 --- a/packages/overview/08-diagrams.md +++ b/packages/overview/08-diagrams.md @@ -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 diff --git a/packages/overview/INDEX.md b/packages/overview/INDEX.md index ce83659..d9140aa 100644 --- a/packages/overview/INDEX.md +++ b/packages/overview/INDEX.md @@ -54,7 +54,7 @@ Operator console (loopback): http://127.0.0.1:3850/ · [CONSOLE.pdf](https://zap Prefix: `https://git.georgelambert.org/marchon/` -overview · verae-nats-process · verae-ops · master-zapier-plan-draft · zappier-edge · verae-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 diff --git a/packages/verae-access-api/NATS.md b/packages/verae-access-api/NATS.md new file mode 100644 index 0000000..46f8181 --- /dev/null +++ b/packages/verae-access-api/NATS.md @@ -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. diff --git a/packages/verae-access-api/README.md b/packages/verae-access-api/README.md new file mode 100644 index 0000000..74102be --- /dev/null +++ b/packages/verae-access-api/README.md @@ -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`. diff --git a/packages/verae-access-api/package.json b/packages/verae-access-api/package.json new file mode 100644 index 0000000..9a26c66 --- /dev/null +++ b/packages/verae-access-api/package.json @@ -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" + } +} diff --git a/packages/verae-access-api/src/server.js b/packages/verae-access-api/src/server.js new file mode 100644 index 0000000..5cb9f61 --- /dev/null +++ b/packages/verae-access-api/src/server.js @@ -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`); +}); diff --git a/packages/verae-access-api/test/health.test.js b/packages/verae-access-api/test/health.test.js new file mode 100644 index 0000000..2e9f92d --- /dev/null +++ b/packages/verae-access-api/test/health.test.js @@ -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'); + } +}); diff --git a/packages/verae-access-authz/NATS.md b/packages/verae-access-authz/NATS.md new file mode 100644 index 0000000..10afef7 --- /dev/null +++ b/packages/verae-access-authz/NATS.md @@ -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. diff --git a/packages/verae-access-authz/README.md b/packages/verae-access-authz/README.md new file mode 100644 index 0000000..c14cb93 --- /dev/null +++ b/packages/verae-access-authz/README.md @@ -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.... (ingress, that plane only) + → verae.access.authz.check (allow / deny) + → verae... (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"}' +``` diff --git a/packages/verae-access-authz/package-lock.json b/packages/verae-access-authz/package-lock.json new file mode 100644 index 0000000..5b944ab --- /dev/null +++ b/packages/verae-access-authz/package-lock.json @@ -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" + } + } +} diff --git a/packages/verae-access-authz/package.json b/packages/verae-access-authz/package.json new file mode 100644 index 0000000..850686f --- /dev/null +++ b/packages/verae-access-authz/package.json @@ -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" + } +} diff --git a/packages/verae-access-authz/src/client.js b/packages/verae-access-authz/src/client.js new file mode 100644 index 0000000..f443a9e --- /dev/null +++ b/packages/verae-access-authz/src/client.js @@ -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 }; +} diff --git a/packages/verae-access-authz/src/policy.js b/packages/verae-access-authz/src/policy.js new file mode 100644 index 0000000..d8f2149 --- /dev/null +++ b/packages/verae-access-authz/src/policy.js @@ -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 }; +} diff --git a/packages/verae-access-authz/src/server.js b/packages/verae-access-authz/src/server.js new file mode 100644 index 0000000..3e29401 --- /dev/null +++ b/packages/verae-access-authz/src/server.js @@ -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`)); diff --git a/packages/verae-access-authz/src/subjects.js b/packages/verae-access-authz/src/subjects.js new file mode 100644 index 0000000..029865d --- /dev/null +++ b/packages/verae-access-authz/src/subjects.js @@ -0,0 +1,36 @@ +/** + * Access planes sit in front of internal verae..* addresses. + * Pattern: + * verae.access.... ingress (that plane only) + * verae.access.authz.check authorization step + * verae... 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 }; +} diff --git a/packages/verae-access-authz/test/health.test.js b/packages/verae-access-authz/test/health.test.js new file mode 100644 index 0000000..6b2d967 --- /dev/null +++ b/packages/verae-access-authz/test/health.test.js @@ -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'); + } +}); diff --git a/packages/verae-access-authz/test/policy.test.js b/packages/verae-access-authz/test/policy.test.js new file mode 100644 index 0000000..30cee3b --- /dev/null +++ b/packages/verae-access-authz/test/policy.test.js @@ -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); +}); diff --git a/packages/verae-access-leaf/NATS.md b/packages/verae-access-leaf/NATS.md new file mode 100644 index 0000000..7f9d539 --- /dev/null +++ b/packages/verae-access-leaf/NATS.md @@ -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. diff --git a/packages/verae-access-leaf/README.md b/packages/verae-access-leaf/README.md new file mode 100644 index 0000000..ce2f273 --- /dev/null +++ b/packages/verae-access-leaf/README.md @@ -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. diff --git a/packages/verae-access-leaf/package-lock.json b/packages/verae-access-leaf/package-lock.json new file mode 100644 index 0000000..740e2af --- /dev/null +++ b/packages/verae-access-leaf/package-lock.json @@ -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" + } + } +} diff --git a/packages/verae-access-leaf/package.json b/packages/verae-access-leaf/package.json new file mode 100644 index 0000000..52dd272 --- /dev/null +++ b/packages/verae-access-leaf/package.json @@ -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" + } +} diff --git a/packages/verae-access-leaf/src/server.js b/packages/verae-access-leaf/src/server.js new file mode 100644 index 0000000..7324404 --- /dev/null +++ b/packages/verae-access-leaf/src/server.js @@ -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`)); diff --git a/packages/verae-access-leaf/test/health.test.js b/packages/verae-access-leaf/test/health.test.js new file mode 100644 index 0000000..1fb1d9a --- /dev/null +++ b/packages/verae-access-leaf/test/health.test.js @@ -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'); + } +}); diff --git a/packages/verae-access-web/NATS.md b/packages/verae-access-web/NATS.md new file mode 100644 index 0000000..657c03c --- /dev/null +++ b/packages/verae-access-web/NATS.md @@ -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). diff --git a/packages/verae-access-web/README.md b/packages/verae-access-web/README.md new file mode 100644 index 0000000..5d3414e --- /dev/null +++ b/packages/verae-access-web/README.md @@ -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`. diff --git a/packages/verae-access-web/package-lock.json b/packages/verae-access-web/package-lock.json new file mode 100644 index 0000000..34d05ac --- /dev/null +++ b/packages/verae-access-web/package-lock.json @@ -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" + } + } +} diff --git a/packages/verae-access-web/package.json b/packages/verae-access-web/package.json new file mode 100644 index 0000000..b38d1e1 --- /dev/null +++ b/packages/verae-access-web/package.json @@ -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" + } +} diff --git a/packages/verae-access-web/src/gate.js b/packages/verae-access-web/src/gate.js new file mode 100644 index 0000000..6162d32 --- /dev/null +++ b/packages/verae-access-web/src/gate.js @@ -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 } }; +} diff --git a/packages/verae-access-web/src/server.js b/packages/verae-access-web/src/server.js new file mode 100644 index 0000000..c5351df --- /dev/null +++ b/packages/verae-access-web/src/server.js @@ -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`); +}); diff --git a/packages/verae-access-web/test/health.test.js b/packages/verae-access-web/test/health.test.js new file mode 100644 index 0000000..fbbdf18 --- /dev/null +++ b/packages/verae-access-web/test/health.test.js @@ -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'); + } +}); diff --git a/packages/verae-access-zapier/NATS.md b/packages/verae-access-zapier/NATS.md new file mode 100644 index 0000000..a3ee860 --- /dev/null +++ b/packages/verae-access-zapier/NATS.md @@ -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.*. diff --git a/packages/verae-access-zapier/README.md b/packages/verae-access-zapier/README.md new file mode 100644 index 0000000..e25cf46 --- /dev/null +++ b/packages/verae-access-zapier/README.md @@ -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`. diff --git a/packages/verae-access-zapier/package.json b/packages/verae-access-zapier/package.json new file mode 100644 index 0000000..8c71481 --- /dev/null +++ b/packages/verae-access-zapier/package.json @@ -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" + } +} diff --git a/packages/verae-access-zapier/src/server.js b/packages/verae-access-zapier/src/server.js new file mode 100644 index 0000000..a3804ec --- /dev/null +++ b/packages/verae-access-zapier/src/server.js @@ -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`); +}); diff --git a/packages/verae-access-zapier/test/health.test.js b/packages/verae-access-zapier/test/health.test.js new file mode 100644 index 0000000..6509f9f --- /dev/null +++ b/packages/verae-access-zapier/test/health.test.js @@ -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'); + } +}); diff --git a/packages/verae-fleet/fleet.json b/packages/verae-fleet/fleet.json index 942284c..6177e0d 100644 --- a/packages/verae-fleet/fleet.json +++ b/packages/verae-fleet/fleet.json @@ -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 }, diff --git a/packages/verae-fleet/services/access-api.json b/packages/verae-fleet/services/access-api.json new file mode 100644 index 0000000..1d5a7ae --- /dev/null +++ b/packages/verae-fleet/services/access-api.json @@ -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"] + } +} diff --git a/packages/verae-fleet/services/access-authz.json b/packages/verae-fleet/services/access-authz.json new file mode 100644 index 0000000..f104616 --- /dev/null +++ b/packages/verae-fleet/services/access-authz.json @@ -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"] + } +} diff --git a/packages/verae-fleet/services/access-leaf.json b/packages/verae-fleet/services/access-leaf.json new file mode 100644 index 0000000..0ab8e7a --- /dev/null +++ b/packages/verae-fleet/services/access-leaf.json @@ -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"] + } +} diff --git a/packages/verae-fleet/services/access-web.json b/packages/verae-fleet/services/access-web.json new file mode 100644 index 0000000..65481a1 --- /dev/null +++ b/packages/verae-fleet/services/access-web.json @@ -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"] + } +} diff --git a/packages/verae-fleet/services/access-zapier.json b/packages/verae-fleet/services/access-zapier.json new file mode 100644 index 0000000..a3244b5 --- /dev/null +++ b/packages/verae-fleet/services/access-zapier.json @@ -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"] + } +} diff --git a/packages/verae-fleet/services/customer-service.json b/packages/verae-fleet/services/customer-service.json index 623f7ee..6d39b76 100644 --- a/packages/verae-fleet/services/customer-service.json +++ b/packages/verae-fleet/services/customer-service.json @@ -17,7 +17,8 @@ "PORT": "3011", "ZAPPIER_ADMIN_URL": "http://127.0.0.1:3000", "ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010", - "NATS_URL": "nats://127.0.0.1:4222" + "NATS_URL": "nats://127.0.0.1:4222", + "AUTHZ_URL": "http://127.0.0.1:3020" }, "nats": { "in": [], diff --git a/packages/verae-fleet/services/sales-pricing.json b/packages/verae-fleet/services/sales-pricing.json index 1865ec7..fbda599 100644 --- a/packages/verae-fleet/services/sales-pricing.json +++ b/packages/verae-fleet/services/sales-pricing.json @@ -17,7 +17,8 @@ "PORT": "3012", "ZAPPIER_ADMIN_URL": "http://127.0.0.1:3000", "ACCOUNT_BALANCE_URL": "http://127.0.0.1:3010", - "NATS_URL": "nats://127.0.0.1:4222" + "NATS_URL": "nats://127.0.0.1:4222", + "AUTHZ_URL": "http://127.0.0.1:3020" }, "nats": { "in": [], "out": ["verae.billing.statement.get"] } } diff --git a/packages/verae-fleet/services/zappier-edge.json b/packages/verae-fleet/services/zappier-edge.json index b14b6da..21aeed1 100644 --- a/packages/verae-fleet/services/zappier-edge.json +++ b/packages/verae-fleet/services/zappier-edge.json @@ -19,7 +19,8 @@ "CS_SERVICE_URL": "http://127.0.0.1:3011", "SALES_PRICING_URL": "http://127.0.0.1:3012", "ACCOUNTING_EXPORT_URL": "http://127.0.0.1:3013", - "NATS_URL": "nats://127.0.0.1:4222" + "NATS_URL": "nats://127.0.0.1:4222", + "AUTHZ_URL": "http://127.0.0.1:3020" }, "nats": { "in": [], diff --git a/packages/verae-fleet/test/fleet.test.js b/packages/verae-fleet/test/fleet.test.js index 79dbf00..c03b564 100644 --- a/packages/verae-fleet/test/fleet.test.js +++ b/packages/verae-fleet/test/fleet.test.js @@ -53,6 +53,11 @@ describe('verae-fleet', () => { for (const need of [ 'nats', 'zappier-edge', + 'access-authz', + 'access-web', + 'access-api', + 'access-leaf', + 'access-zapier', 'account-balance', 'customer-service', 'sales-pricing', diff --git a/packages/verae-ops/GETTING-STARTED.md b/packages/verae-ops/GETTING-STARTED.md index cb4de0f..fb8253b 100644 --- a/packages/verae-ops/GETTING-STARTED.md +++ b/packages/verae-ops/GETTING-STARTED.md @@ -10,6 +10,8 @@ This is the **top document** for install, first-up, and monitoring. Platform-spe | Piece | Job | Default | |-------|-----|---------| +| **access-authz** | NATS authorization step for every access plane | `:3020` | +| **access-web / api / leaf / zapier** | Separated ingress (browser, x-api-key, S2S, Zapier) | `:3021–:3024` | | **zappier-edge** | Billing, API keys, customer portal, admin, Stripe; meters then **proxies** to middleware | `:3000` public HTTPS | | **account-balance** | NATS prepaid balances (SoT) | `:3010` | | **customer-service** | CS credit additions + review | `:3011` | diff --git a/packages/verae-ops/docker-compose.yml b/packages/verae-ops/docker-compose.yml index 38b50b4..6c4b2db 100644 --- a/packages/verae-ops/docker-compose.yml +++ b/packages/verae-ops/docker-compose.yml @@ -51,9 +51,89 @@ services: PORT: "3000" ZAPPIER_UPSTREAM: http://middleware:3100 NATS_URL: nats://nats1:4222 + AUTHZ_URL: http://access-authz:3020 ports: - "3000:3000" - depends_on: [nats1, account-balance] + depends_on: [nats1, account-balance, access-authz] + + access-authz: + <<: *node + image: node:22-alpine + working_dir: /app + volumes: + - ${MONOREPO:-..}/packages/verae-access-authz:/app + command: ["node", "src/server.js"] + environment: + PORT: "3020" + NATS_URL: nats://nats1:4222 + ports: + - "3020:3020" + depends_on: [nats1] + + access-web: + <<: *node + image: node:22-alpine + working_dir: /app + volumes: + - ${MONOREPO:-..}/packages/verae-access-web:/app + command: ["node", "src/server.js"] + environment: + PORT: "3021" + AUTHZ_URL: http://access-authz:3020 + ACCOUNT_BALANCE_URL: http://account-balance:3010 + NATS_URL: nats://nats1:4222 + ports: + - "3021:3021" + depends_on: [access-authz, account-balance] + + access-api: + <<: *node + image: node:22-alpine + working_dir: /app + volumes: + - ${MONOREPO:-..}/packages/verae-access-api:/app + command: ["node", "src/server.js"] + environment: + PORT: "3022" + AUTHZ_URL: http://access-authz:3020 + ACCOUNT_BALANCE_URL: http://account-balance:3010 + ZAPPIER_UPSTREAM: http://middleware:3100 + NATS_URL: nats://nats1:4222 + ports: + - "3022:3022" + depends_on: [access-authz, middleware] + + access-leaf: + <<: *node + image: node:22-alpine + working_dir: /app + volumes: + - ${MONOREPO:-..}/packages/verae-access-leaf:/app + command: ["node", "src/server.js"] + environment: + PORT: "3023" + AUTHZ_URL: http://access-authz:3020 + LEAF_TOKEN: leaf-dev-token + NATS_URL: nats://nats1:4222 + ports: + - "3023:3023" + depends_on: [access-authz, nats1] + + access-zapier: + <<: *node + image: node:22-alpine + working_dir: /app + volumes: + - ${MONOREPO:-..}/packages/verae-access-zapier:/app + command: ["node", "src/server.js"] + environment: + PORT: "3024" + AUTHZ_URL: http://access-authz:3020 + ZAPPIER_UPSTREAM: http://middleware:3100 + NATS_URL: nats://nats1:4222 + ports: + - "3024:3024" + depends_on: [access-authz, middleware] account-balance: <<: *node @@ -81,6 +161,7 @@ services: ZAPPIER_ADMIN_URL: http://zappier-edge:3000 ACCOUNT_BALANCE_URL: http://account-balance:3010 NATS_URL: nats://nats1:4222 + AUTHZ_URL: http://access-authz:3020 ports: - "3011:3011" depends_on: [zappier-edge, account-balance, nats1] diff --git a/packages/zappier-customer-service/src/nats-billing.js b/packages/zappier-customer-service/src/nats-billing.js index 6798fb1..2a9d87d 100644 --- a/packages/zappier-customer-service/src/nats-billing.js +++ b/packages/zappier-customer-service/src/nats-billing.js @@ -1,20 +1,44 @@ export const SUBJECTS = { STATEMENT_GET: 'verae.billing.statement.get', BALANCE_ADJUST: 'verae.billing.balance.adjust', + AUTHZ_CHECK: 'verae.access.authz.check', }; +const PLANE = 'staff'; + +async function authz(subject, payload) { + const http = process.env.AUTHZ_URL; + if (!http && !process.env.NATS_URL) return { allow: true, subject }; + if (http) { + const r = await fetch(`${http.replace(/\/$/, '')}/check`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ plane: PLANE, subject, kind: payload?.kind, payload, principal: payload?.agent }), + }); + return r.json(); + } + return { allow: true, subject }; +} export async function billingRequest(subject, payload) { + const decision = await authz(subject, payload); + if (decision && decision.allow === false) { + const err = new Error(decision.reason || 'denied'); + err.status = 403; + err.decision = decision; + throw err; + } const url = process.env.NATS_URL; if (!url) return null; try { const { connect, StringCodec } = await import('nats'); const nc = await connect({ servers: url.split(','), name: 'zappier-customer-service' }); const sc = StringCodec(); - const m = await nc.request(subject, sc.encode(JSON.stringify(payload)), { timeout: 2000 }); + const m = await nc.request(subject, sc.encode(JSON.stringify({ ...payload, plane: PLANE })), { timeout: 2000 }); const out = JSON.parse(sc.decode(m.data) || '{}'); await nc.close(); return out; - } catch { + } catch (err) { + if (err.status === 403) throw err; return null; } } diff --git a/packages/zappier-customer-service/src/server.js b/packages/zappier-customer-service/src/server.js index 39bd204..28b99ba 100644 --- a/packages/zappier-customer-service/src/server.js +++ b/packages/zappier-customer-service/src/server.js @@ -71,7 +71,7 @@ const server = http.createServer(async (req, res) => { } json(404, { error: 'not found' }); } catch (err) { - json(502, { error: err.message }); + json(err.status === 403 ? 403 : 502, { error: err.message, ...(err.decision || {}) }); } }); diff --git a/packages/zappier-sales-pricing/src/nats-billing.js b/packages/zappier-sales-pricing/src/nats-billing.js index efab3b3..17e7a52 100644 --- a/packages/zappier-sales-pricing/src/nats-billing.js +++ b/packages/zappier-sales-pricing/src/nats-billing.js @@ -1,17 +1,36 @@ export const SUBJECTS = { STATEMENT_GET: 'verae.billing.statement.get' }; +const PLANE = 'staff'; + +async function authz(subject) { + const http = process.env.AUTHZ_URL; + if (!http) return { allow: true, subject }; + const r = await fetch(`${http.replace(/\/$/, '')}/check`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ plane: PLANE, subject }), + }); + return r.json(); +} export async function billingRequest(subject, payload) { + const decision = await authz(subject); + if (decision && decision.allow === false) { + const err = new Error(decision.reason || 'denied'); + err.status = 403; + throw err; + } const url = process.env.NATS_URL; if (!url) return null; try { const { connect, StringCodec } = await import('nats'); const nc = await connect({ servers: url.split(','), name: 'zappier-sales-pricing' }); const sc = StringCodec(); - const m = await nc.request(subject, sc.encode(JSON.stringify(payload)), { timeout: 2000 }); + const m = await nc.request(subject, sc.encode(JSON.stringify({ ...payload, plane: PLANE })), { timeout: 2000 }); const out = JSON.parse(sc.decode(m.data) || '{}'); await nc.close(); return out; - } catch { + } catch (err) { + if (err.status === 403) throw err; return null; } } diff --git a/packages/zappier-sales-pricing/src/server.js b/packages/zappier-sales-pricing/src/server.js index bdb1995..0e5e2cb 100644 --- a/packages/zappier-sales-pricing/src/server.js +++ b/packages/zappier-sales-pricing/src/server.js @@ -67,7 +67,7 @@ const server = http.createServer(async (req, res) => { } json(404, { error: 'not found' }); } catch (err) { - json(502, { error: err.message }); + json(err.status === 403 ? 403 : 502, { error: err.message }); } }); diff --git a/packages/zappier/src/admin.ts b/packages/zappier/src/admin.ts index bdfd9b8..6024e0d 100644 --- a/packages/zappier/src/admin.ts +++ b/packages/zappier/src/admin.ts @@ -248,14 +248,17 @@ export function adminRouter( agent: typeof agent === 'string' ? agent : 'admin', }); customers.save({ ...customer, balanceCents: (customer.balanceCents ?? 0) + delta }); - natsPublish(BILLING_SUBJECTS.CREDIT_APPLIED, rec); - void natsAdjust({ - customerId, - cents: delta, - reason: rec.reason, - agent: rec.agent, - kind: 'credit', - }); + natsPublish(BILLING_SUBJECTS.CREDIT_APPLIED, rec, 'staff'); + void natsAdjust( + { + customerId, + cents: delta, + reason: rec.reason, + agent: rec.agent, + kind: 'credit', + }, + 'staff', + ); res.status(201).json(rec); }); @@ -270,7 +273,7 @@ export function adminRouter( res.status(404).json({ error: 'customer not found' }); return; } - const fromNats = await natsStatement(customer.id); + const fromNats = await natsStatement(customer.id, 'staff'); if (fromNats) { res.json({ ...fromNats, name: customer.name, tierId: customer.tierId, source: 'nats' }); return; diff --git a/packages/zappier/src/app.ts b/packages/zappier/src/app.ts index 0201c10..c244f86 100644 --- a/packages/zappier/src/app.ts +++ b/packages/zappier/src/app.ts @@ -103,7 +103,7 @@ export function buildApp(deps: AppDeps = {}): { const items: StoredItem[] = []; const credits = new CreditLedger(); const onUsage = (e: { customerId: string; endpointId: string; cents: number }) => - natsPublish(BILLING_SUBJECTS.USAGE_RECORDED, { ...e, at: new Date().toISOString() }); + natsPublish(BILLING_SUBJECTS.USAGE_RECORDED, { ...e, at: new Date().toISOString() }, 'api'); const hashIndex = new Map< string, { jobId: string; sha256: string; data?: string; timestamp: string } diff --git a/packages/zappier/src/billing-nats.ts b/packages/zappier/src/billing-nats.ts index b8654a0..b05782a 100644 --- a/packages/zappier/src/billing-nats.ts +++ b/packages/zappier/src/billing-nats.ts @@ -1,4 +1,6 @@ -/** Internal NATS billing bus. No-op when NATS_URL is unset (tests). */ +/** Internal NATS billing bus. No-op when NATS_URL is unset (tests). + * When NATS is on, every hop must pass verae.access.authz.check for a plane. + */ export const BILLING_SUBJECTS = { STATEMENT_GET: 'verae.billing.statement.get', @@ -8,6 +10,9 @@ export const BILLING_SUBJECTS = { CREDIT_APPLIED: 'verae.billing.credit.applied', }; +export const AUTHZ_CHECK = 'verae.access.authz.check'; +export type AccessPlane = 'zapier' | 'web' | 'api' | 'leaf' | 'staff'; + export type BillingStatement = { customerId: string; prepaidCents: number; @@ -46,28 +51,79 @@ function decode(buf: Uint8Array): unknown { return JSON.parse(new TextDecoder().decode(buf) || '{}'); } -export async function natsStatement(customerId: string): Promise { +async function authzAllow( + plane: AccessPlane, + subject: string, + extra: { principal?: string; kind?: string } = {}, +): Promise { + const http = process.env.AUTHZ_URL; + if (!http && !(await nc())) return true; + try { + const c = await nc(); + if (c) { + const m = await c.request(AUTHZ_CHECK, encode({ plane, subject, ...extra }), { timeout: 1500 }); + const d = decode(m.data) as { allow?: boolean }; + return Boolean(d.allow); + } + if (http) { + const r = await fetch(`${http.replace(/\/$/, '')}/check`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ plane, subject, ...extra }), + }); + const d = (await r.json()) as { allow?: boolean }; + return Boolean(d.allow); + } + } catch { + return false; + } + return true; +} + +export async function natsStatement( + customerId: string, + plane: AccessPlane = 'web', +): Promise { const c = await nc(); if (!c) return null; - const m = await c.request(BILLING_SUBJECTS.STATEMENT_GET, encode({ customerId }), { timeout: 2000 }); + if (!(await authzAllow(plane, BILLING_SUBJECTS.STATEMENT_GET, { principal: customerId }))) return null; + const m = await c.request( + BILLING_SUBJECTS.STATEMENT_GET, + encode({ customerId, plane }), + { timeout: 2000 }, + ); return decode(m.data) as BillingStatement; } -export async function natsAdjust(payload: { - customerId: string; - cents: number; - reason: string; - agent: string; - kind?: string; -}): Promise { +export async function natsAdjust( + payload: { + customerId: string; + cents: number; + reason: string; + agent: string; + kind?: string; + }, + plane: AccessPlane = 'staff', +): Promise { const c = await nc(); if (!c) return null; - const m = await c.request(BILLING_SUBJECTS.BALANCE_ADJUST, encode(payload), { timeout: 2000 }); + if ( + !(await authzAllow(plane, BILLING_SUBJECTS.BALANCE_ADJUST, { + principal: payload.agent, + kind: payload.kind, + })) + ) { + return null; + } + const m = await c.request(BILLING_SUBJECTS.BALANCE_ADJUST, encode({ ...payload, plane }), { timeout: 2000 }); return decode(m.data); } -export function natsPublish(subject: string, payload: unknown): void { - void nc().then((c) => { - if (c) c.publish(subject, encode(payload)); - }); +export function natsPublish(subject: string, payload: unknown, plane: AccessPlane = 'api'): void { + void (async () => { + const c = await nc(); + if (!c) return; + if (!(await authzAllow(plane, subject))) return; + c.publish(subject, encode({ ...(payload as object), plane })); + })(); } diff --git a/packages/zappier/src/portal.ts b/packages/zappier/src/portal.ts index 38c52af..f2ebb19 100644 --- a/packages/zappier/src/portal.ts +++ b/packages/zappier/src/portal.ts @@ -203,7 +203,7 @@ export function portalRouter(deps: PortalDeps): Router { }); router.get('/statement', async (req, res) => { - const fromNats = await natsStatement(req.customer!.id); + const fromNats = await natsStatement(req.customer!.id, 'web'); if (fromNats) { res.json({ ...fromNats, source: 'nats' }); return; @@ -286,18 +286,25 @@ export function portalRouter(deps: PortalDeps): Router { }; if (result.creditedCents > 0) save(deps, customer); if (result.creditedCents > 0) { - natsPublish(BILLING_SUBJECTS.PAYMENT_RECORDED, { - customerId: customer.id, - cents: result.creditedCents, - reason: 'reload', - }); - void natsAdjust({ - customerId: customer.id, - cents: result.creditedCents, - reason: 'reload', - agent: 'portal', - kind: 'payment', - }); + natsPublish( + BILLING_SUBJECTS.PAYMENT_RECORDED, + { + customerId: customer.id, + cents: result.creditedCents, + reason: 'reload', + }, + 'web', + ); + void natsAdjust( + { + customerId: customer.id, + cents: result.creditedCents, + reason: 'reload', + agent: 'portal', + kind: 'reload', + }, + 'web', + ); } res.json({ balanceCents: customer.balanceCents, diff --git a/scripts/build-docs-site.py b/scripts/build-docs-site.py index d34a1d8..eb703cd 100755 --- a/scripts/build-docs-site.py +++ b/scripts/build-docs-site.py @@ -75,6 +75,11 @@ SECTIONS = [ ("packages/zappier-customer-service/README.md", "Customer-service credits API"), ("packages/zappier-sales-pricing/README.md", "Sales per-customer pricing API"), ("packages/zappier-accounting-export/README.md", "QuickBooks / accounting export"), + ("packages/verae-access-authz/README.md", "Access-plane authorization"), + ("packages/verae-access-web/README.md", "Direct web access plane"), + ("packages/verae-access-api/README.md", "Direct customer API plane"), + ("packages/verae-access-leaf/README.md", "S2S NATS leaf plane"), + ("packages/verae-access-zapier/README.md", "Zapier-only access plane"), ], ), ( @@ -331,6 +336,11 @@ def main() -> None: "zappier-customer-service", "zappier-sales-pricing", "zappier-accounting-export", + "verae-access-authz", + "verae-access-web", + "verae-access-api", + "verae-access-leaf", + "verae-access-zapier", ): pkg_root = ROOT / "packages" / pkg if pkg in {"zapier-user-docs", "overview", "docs-master", "verae-ops"}: @@ -493,6 +503,11 @@ def main() -> None: ("zappier-customer-service", "CS credits"), ("zappier-sales-pricing", "Sales per-customer pricing"), ("zappier-accounting-export", "QuickBooks export"), + ("verae-access-authz", "Access-plane authorization"), + ("verae-access-web", "Direct web access"), + ("verae-access-api", "Direct customer API"), + ("verae-access-leaf", "S2S NATS leaf"), + ("verae-access-zapier", "Zapier access plane"), ] git_lis = "".join( f'
  • {name} ' diff --git a/scripts/gen-module-docs.py b/scripts/gen-module-docs.py index 8ab9030..914e744 100644 --- a/scripts/gen-module-docs.py +++ b/scripts/gen-module-docs.py @@ -26,6 +26,11 @@ PACKAGES = [ ("zappier-customer-service", ROOT / "packages" / "zappier-customer-service" / "src", (".js",)), ("zappier-sales-pricing", ROOT / "packages" / "zappier-sales-pricing" / "src", (".js",)), ("zappier-accounting-export", ROOT / "packages" / "zappier-accounting-export" / "src", (".js",)), + ("verae-access-authz", ROOT / "packages" / "verae-access-authz" / "src", (".js",)), + ("verae-access-web", ROOT / "packages" / "verae-access-web" / "src", (".js",)), + ("verae-access-api", ROOT / "packages" / "verae-access-api" / "src", (".js",)), + ("verae-access-leaf", ROOT / "packages" / "verae-access-leaf" / "src", (".js",)), + ("verae-access-zapier", ROOT / "packages" / "verae-access-zapier" / "src", (".js",)), ] REPO_READMES = [ @@ -48,6 +53,11 @@ REPO_READMES = [ "zappier-customer-service", "zappier-sales-pricing", "zappier-accounting-export", + "verae-access-authz", + "verae-access-web", + "verae-access-api", + "verae-access-leaf", + "verae-access-zapier", ] SKIP_PARTS = {"test", "tests", "node_modules", "dist"} diff --git a/scripts/push-module-repos.sh b/scripts/push-module-repos.sh index da92208..bae625b 100755 --- a/scripts/push-module-repos.sh +++ b/scripts/push-module-repos.sh @@ -54,6 +54,11 @@ create zappier-account-balance "NATS account balance source of truth" create zappier-customer-service "Customer-service prepaid credit adjustments" create zappier-sales-pricing "Sales per-customer pricing" create zappier-accounting-export "QuickBooks IIF and accounting CSV export" +create verae-access-authz "NATS authorization step for access planes" +create verae-access-web "Direct customer web access plane" +create verae-access-api "Direct customer API access plane" +create verae-access-leaf "Server-to-server NATS leaf access plane" +create verae-access-zapier "Zapier Platform access plane only" push_dir "$ROOT/packages/zappier" zappier-edge push_dir "$ROOT/packages/verae-zapier-middleware" verae-middleware @@ -74,5 +79,10 @@ push_dir "$ROOT/packages/zappier-account-balance" zappier-account-balance push_dir "$ROOT/packages/zappier-customer-service" zappier-customer-service push_dir "$ROOT/packages/zappier-sales-pricing" zappier-sales-pricing push_dir "$ROOT/packages/zappier-accounting-export" zappier-accounting-export +push_dir "$ROOT/packages/verae-access-authz" verae-access-authz +push_dir "$ROOT/packages/verae-access-web" verae-access-web +push_dir "$ROOT/packages/verae-access-api" verae-access-api +push_dir "$ROOT/packages/verae-access-leaf" verae-access-leaf +push_dir "$ROOT/packages/verae-access-zapier" verae-access-zapier echo ALL_MODULE_REPOS_PUSHED