Initial import of zappier-identity from zapier monorepo

This commit is contained in:
George Lambert 2026-09-11 17:17:12 -04:00
commit d422bb2639
8 changed files with 204 additions and 0 deletions

3
NATS.md Normal file
View file

@ -0,0 +1,3 @@
# NATS — zappier-identity
Queue `identity`. Subjects `verae.identity.bind`, `verae.identity.lookup`.

12
README.md Normal file
View file

@ -0,0 +1,12 @@
# zappier-identity
Mailbox for **Verae user id bind/lookup**. Public credentials stay zappier API keys. JWTs never live here.
**Forgejo:** https://git.georgelambert.org/marchon/zappier-identity
| Address | Kind |
|---------|------|
| `verae.identity.bind` | request-reply `{ email, customerId }` |
| `verae.identity.lookup` | request-reply `{ customerId }` or `{ veraeUserId }` |
HTTP `:3026` `POST /bind` `GET /lookup/:customerId`.

14
package.json Normal file
View file

@ -0,0 +1,14 @@
{
"name": "zappier-identity",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Mailbox: bind and lookup veraeUserId (not JWTs)",
"scripts": {
"start": "node src/server.js",
"test": "node --test test/*.test.js"
},
"dependencies": {
"nats": "^2.28.2"
}
}

15
src/ids.js Normal file
View file

@ -0,0 +1,15 @@
import { createHash } from 'node:crypto';
export function stableVeraeUserId(username) {
const n = String(username || '')
.trim()
.toLowerCase();
return `vu_${createHash('sha256').update(n).digest('hex').slice(0, 16)}`;
}
export function bindEmail(email) {
const veraeUsername = String(email || '')
.trim()
.toLowerCase();
return { veraeUserId: stableVeraeUserId(veraeUsername), veraeUsername, bound: true };
}

89
src/server.js Normal file
View file

@ -0,0 +1,89 @@
#!/usr/bin/env node
/** Mailbox: bind/lookup veraeUserId. JWT never stored here. */
import http from 'node:http';
import { bindEmail } from './ids.js';
import { load, put } from './store.js';
import { SUBJECTS } from './subjects.js';
const PORT = Number(process.env.PORT || 3026);
const BIND = process.env.FLEET_HEALTH_BIND || '0.0.0.0';
const db = load();
function handleBind(p) {
const email = p.email || p.veraeUsername;
const bound = bindEmail(email);
const customerId = p.customerId || bound.veraeUsername;
put(db, { customerId, ...bound });
return { ...bound, customerId };
}
function handleLookup(p) {
if (p.customerId && db.byCustomer[p.customerId]) return db.byCustomer[p.customerId];
if (p.veraeUserId && db.byVerae[p.veraeUserId]) return db.byVerae[p.veraeUserId];
if (p.email) return bindEmail(p.email);
return { error: 'not found' };
}
async function startNats() {
const url = process.env.NATS_URL;
if (!url) return;
const { connect, StringCodec } = await import('nats');
const nc = await connect({ servers: url.split(','), name: 'zappier-identity' });
const sc = StringCodec();
for (const subj of [SUBJECTS.BIND, SUBJECTS.LOOKUP]) {
(async () => {
for await (const m of nc.subscribe(subj, { queue: SUBJECTS.QUEUE })) {
let p = {};
try {
p = JSON.parse(sc.decode(m.data) || '{}');
} catch {
p = {};
}
const out = subj === SUBJECTS.BIND ? handleBind(p) : handleLookup(p);
if (m.reply) m.respond(sc.encode(JSON.stringify(out)));
}
})();
}
}
function readBody(req) {
return new Promise((resolve) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'));
} catch {
resolve({});
}
});
});
}
const server = http.createServer(async (req, res) => {
const url = new URL(req.url || '/', `http://127.0.0.1:${PORT}`);
const json = (code, obj) => {
res.writeHead(code, { 'content-type': 'application/json' });
res.end(JSON.stringify(obj));
};
try {
if (req.method === 'GET' && url.pathname === '/health') {
return json(200, { ok: true, role: 'zappier-identity', subjects: SUBJECTS });
}
if (req.method === 'POST' && url.pathname === '/bind') {
return json(200, handleBind(await readBody(req)));
}
const who = url.pathname.match(/^\/lookup\/([^/]+)$/);
if (req.method === 'GET' && who) {
return json(200, handleLookup({ customerId: who[1] }));
}
json(404, { error: 'not found' });
} catch (err) {
json(500, { error: err.message });
}
});
server.listen(PORT, BIND, () => {
process.stdout.write(`zappier-identity http://${BIND}:${PORT}/\n`);
});
startNats().catch((err) => process.stderr.write(`nats optional: ${err.message}\n`));

30
src/store.js Normal file
View file

@ -0,0 +1,30 @@
import fs from 'node:fs';
import path from 'node:path';
const file = () =>
process.env.IDENTITY_PATH ||
path.join(process.env.FLEET_STATE_DIR || process.cwd(), 'data', 'identity.json');
export function load() {
const p = file();
if (!fs.existsSync(p)) return { byCustomer: {}, byVerae: {} };
try {
return { byCustomer: {}, byVerae: {}, ...JSON.parse(fs.readFileSync(p, 'utf8')) };
} catch {
return { byCustomer: {}, byVerae: {} };
}
}
export function save(db) {
const p = file();
fs.mkdirSync(path.dirname(p), { recursive: true });
fs.writeFileSync(p, JSON.stringify(db, null, 2));
}
export function put(db, { customerId, veraeUserId, veraeUsername }) {
if (!customerId || !veraeUserId) return db;
db.byCustomer[customerId] = { veraeUserId, veraeUsername };
db.byVerae[veraeUserId] = { customerId, veraeUsername };
save(db);
return db;
}

5
src/subjects.js Normal file
View file

@ -0,0 +1,5 @@
export const SUBJECTS = {
BIND: 'verae.identity.bind',
LOOKUP: 'verae.identity.lookup',
QUEUE: 'identity',
};

36
test/health.test.js Normal file
View file

@ -0,0 +1,36 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { stableVeraeUserId } from '../src/ids.js';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
test('identity bind is stable and not a JWT', async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'id-'));
const port = 18026;
const child = spawn(process.execPath, ['src/server.js'], {
cwd: root,
env: { ...process.env, PORT: String(port), IDENTITY_PATH: path.join(dir, 'id.json') },
stdio: ['ignore', 'pipe', 'pipe'],
});
await new Promise((r) => setTimeout(r, 400));
try {
const h = await (await fetch(`http://127.0.0.1:${port}/health`)).json();
assert.equal(h.role, 'zappier-identity');
const b = await (
await fetch(`http://127.0.0.1:${port}/bind`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ email: 'Ada@Example.com', customerId: 'cust_x' }),
})
).json();
assert.equal(b.veraeUserId, stableVeraeUserId('ada@example.com'));
assert.doesNotMatch(JSON.stringify(b), /eyJ|mock-jwt/);
} finally {
child.kill('SIGTERM');
}
});