Initial import of zappier-identity from zapier monorepo
This commit is contained in:
commit
3f136070b6
8 changed files with 204 additions and 0 deletions
15
src/ids.js
Normal file
15
src/ids.js
Normal 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
89
src/server.js
Normal 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
30
src/store.js
Normal 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
5
src/subjects.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export const SUBJECTS = {
|
||||
BIND: 'verae.identity.bind',
|
||||
LOOKUP: 'verae.identity.lookup',
|
||||
QUEUE: 'identity',
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue