#!/usr/bin/env node
// Generate corrosion-nats authorization config from the licenses table.
//
// Per-license isolation without auth-callout: each license maps to a NATS user
// (user = license UUID, password = HMAC-SHA256(license_id, NATS_TOKEN_SECRET))
// whose publish/subscribe is restricted to corrosion.{license_id}.> (+ _INBOX
// for request-reply). The backend uses a privileged internal user.
//
// STAGING (NATS_AUTH_STAGE env) — defaults to "enforce" (secure by default):
//   "enforce" (default) — no anonymous; unauthenticated connections rejected.
//   "open"              — EXPLICIT opt-in for a brief migration window. Maps
//                         anonymous to a HARMLESS namespace (corrosion.unclaimed.>),
//                         NEVER full access, so a stale "open" deploy cannot
//                         read or forge real tenant (corrosion.{uuid}.>) traffic.
//
// REPLY SUBJECTS: per-license users are scoped to corrosion.{license}.> ONLY —
// no _INBOX grant (that would let one license read another's request-reply
// responses). Backend→agent request-reply MUST therefore use a reply subject
// inside the license namespace, e.g. corrosion.{license}.reply.<id>, not the
// default global _INBOX. The agent simply responds to msg.reply, so no agent
// change is needed — the constraint is on the requester (the internal user has
// full > and is unaffected).
//
// Usage:
//   DATABASE_URL=... NATS_INTERNAL_USER=... NATS_INTERNAL_PASSWORD=... \
//   NATS_TOKEN_SECRET=... NATS_AUTH_STAGE=open node scripts/generate-nats-auth.mjs > docker/nats-auth.conf
//
// Re-run and reload NATS (`docker exec corrosion-nats nats-server --signal reload`)
// whenever licenses change.

import { createRequire } from 'node:module';
import { createHmac } from 'node:crypto';

const require = createRequire(new URL('../backend-nest/x.js', import.meta.url));
const { Client } = require('pg');

const {
  DATABASE_URL,
  NATS_INTERNAL_USER,
  NATS_INTERNAL_PASSWORD,
  NATS_TOKEN_SECRET,
  NATS_AUTH_STAGE = 'enforce',
} = process.env;

for (const [k, v] of Object.entries({ DATABASE_URL, NATS_INTERNAL_USER, NATS_INTERNAL_PASSWORD, NATS_TOKEN_SECRET })) {
  if (!v) { console.error(`Missing required env: ${k}`); process.exit(2); }
}

/** Per-license agent password — must match the backend's derivation. */
export function licensePassword(licenseId, secret) {
  return createHmac('sha256', secret).update(licenseId).digest('hex');
}

const esc = (s) => String(s).replace(/\\/g, '\\\\').replace(/"/g, '\\"');

const main = async () => {
  const pg = new Client({ connectionString: DATABASE_URL });
  await pg.connect();
  const { rows } = await pg.query('SELECT id FROM licenses ORDER BY created_at');
  await pg.end();

  const lines = [];
  lines.push('# GENERATED by scripts/generate-nats-auth.mjs — do not edit by hand.');
  lines.push(`# stage=${NATS_AUTH_STAGE}  licenses=${rows.length}`);
  lines.push('authorization {');
  lines.push('  users: [');
  // Privileged internal user — the backend (full corrosion.> + _INBOX + _SYS).
  lines.push(`    { user: "${esc(NATS_INTERNAL_USER)}", password: "${esc(NATS_INTERNAL_PASSWORD)}", permissions: { publish: ">", subscribe: ">" } }`);

  // Per-license scoped users — corrosion.{id}.> ONLY. No _INBOX grant:
  // replies ride the license namespace (see header). This is the whole
  // point — one license can never touch another's subjects.
  for (const { id } of rows) {
    const pw = licensePassword(id, NATS_TOKEN_SECRET);
    const scope = `corrosion.${id}.>`;
    lines.push(
      `    { user: "${esc(id)}", password: "${esc(pw)}", permissions: { ` +
        `publish: { allow: ["${scope}"] }, ` +
        `subscribe: { allow: ["${scope}"] } } }`,
    );
  }

  if (NATS_AUTH_STAGE === 'open') {
    // EXPLICIT migration opt-in only. Anonymous gets a HARMLESS namespace —
    // never real tenant subjects — so a stale "open" deploy leaks nothing.
    lines.push('    { user: "anonymous", password: "", permissions: { publish: { allow: ["corrosion.unclaimed.>"] }, subscribe: { allow: ["corrosion.unclaimed.>"] } } }');
  }
  lines.push('  ]');
  lines.push('}');
  // no_auth_user is a TOP-LEVEL field, NOT inside authorization { } — nesting
  // it makes nats-server reject the whole config ("unknown field no_auth_user").
  if (NATS_AUTH_STAGE === 'open') {
    lines.push('no_auth_user: "anonymous"');
  }
  process.stdout.write(lines.join('\n') + '\n');
};

main().catch((e) => { console.error(e); process.exit(1); });
