From 358f7399c921bdc398d3e32ea626bfbf4daad12b Mon Sep 17 00:00:00 2001 From: George Lambert Date: Fri, 11 Sep 2026 18:13:30 -0400 Subject: [PATCH] Initial import of verae-nats-accounts from zapier monorepo --- NATS.md | 3 +++ README.md | 8 ++++++++ nats.conf | 21 +++++++++++++++++++++ package.json | 10 ++++++++++ policy.json | 35 +++++++++++++++++++++++++++++++++++ src/policy.js | 14 ++++++++++++++ test/policy.test.js | 12 ++++++++++++ 7 files changed, 103 insertions(+) create mode 100644 NATS.md create mode 100644 README.md create mode 100644 nats.conf create mode 100644 package.json create mode 100644 policy.json create mode 100644 src/policy.js create mode 100644 test/policy.test.js diff --git a/NATS.md b/NATS.md new file mode 100644 index 0000000..c782336 --- /dev/null +++ b/NATS.md @@ -0,0 +1,3 @@ +# NATS — verae-nats-accounts + +Not a subscriber. Defines INTERNAL vs LEAF. Leaf deny `verae.billing.>`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1284304 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# verae-nats-accounts + +NATS **account + leaf allow-list**. Leaf nodes cannot publish `verae.billing.*`. + +**Forgejo:** https://git.georgelambert.org/marchon/verae-nats-accounts + +- `policy.json` — machine-readable allow/deny (used by access-leaf). +- `nats.conf` — sketch for a future accounts-enabled nats-server (loopback). diff --git a/nats.conf b/nats.conf new file mode 100644 index 0000000..6e2bede --- /dev/null +++ b/nats.conf @@ -0,0 +1,21 @@ +# Lab accounts sketch. Loopback only. Do not publish 4222. +# INTERNAL: middleware, account-balance, authz, identity. +# LEAF: access-leaf and remote leaf nodes — no billing. + +listen: 127.0.0.1:4222 +http: 127.0.0.1:8222 +jetstream {} + +accounts { + INTERNAL { + users = [ { user: internal, password: internal-dev } ] + jetstream: enabled + } + LEAF { + users = [ { user: leaf, password: leaf-dev } ] + exports = [] + imports = [] + } +} +# Subject restrictions for LEAF are enforced in policy.json + access-leaf +# until this nats-server is started with full auth callout. diff --git a/package.json b/package.json new file mode 100644 index 0000000..0213cab --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "verae-nats-accounts", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "NATS account and leaf subject policy (INTERNAL vs LEAF)", + "scripts": { + "test": "node --test test/*.test.js" + } +} diff --git a/policy.json b/policy.json new file mode 100644 index 0000000..8c5a59c --- /dev/null +++ b/policy.json @@ -0,0 +1,35 @@ +{ + "accounts": { + "INTERNAL": { + "publish": [">"], + "subscribe": [">"] + }, + "LEAF": { + "publish": [ + "verae.access.leaf.in", + "verae.archive.put", + "verae.archive.query", + "verae.archive.reply.*", + "verae.zapier.jobs.watch", + "verae.zapier.jobs.events", + "verae.zapier.webhooks.deliver" + ], + "subscribe": [ + "verae.access.leaf.in", + "verae.archive.>", + "verae.zapier.jobs.>", + "verae.zapier.webhooks.deliver" + ], + "deny_publish": ["verae.billing.>", "verae.access.authz.>"], + "deny_subscribe": ["verae.billing.>"] + } + }, + "leafAllow": [ + "verae.archive.put", + "verae.archive.query", + "verae.archive.reply.", + "verae.zapier.jobs.watch", + "verae.zapier.jobs.events", + "verae.zapier.webhooks.deliver" + ] +} diff --git a/src/policy.js b/src/policy.js new file mode 100644 index 0000000..35bb5af --- /dev/null +++ b/src/policy.js @@ -0,0 +1,14 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const FILE = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'policy.json'); + +export function loadPolicy() { + return JSON.parse(fs.readFileSync(FILE, 'utf8')); +} + +export function leafAllowed(subject) { + const allow = loadPolicy().leafAllow || []; + return allow.some((p) => (p.endsWith('.') ? subject.startsWith(p) : subject === p || subject.startsWith(`${p}.`))); +} diff --git a/test/policy.test.js b/test/policy.test.js new file mode 100644 index 0000000..0cc9ca0 --- /dev/null +++ b/test/policy.test.js @@ -0,0 +1,12 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { leafAllowed, loadPolicy } from '../src/policy.js'; + +test('leaf may archive and jobs, never billing', () => { + assert.equal(leafAllowed('verae.archive.put'), true); + assert.equal(leafAllowed('verae.archive.reply.abc'), true); + assert.equal(leafAllowed('verae.zapier.jobs.watch'), true); + assert.equal(leafAllowed('verae.billing.balance.adjust'), false); + assert.equal(leafAllowed('verae.billing.statement.get'), false); + assert.ok(loadPolicy().accounts.LEAF.deny_publish.includes('verae.billing.>')); +});