audit-logger: the assigned-module manifest (ADR 0048) #2

Merged
jschoubben merged 28 commits from events/audit-logger-assigned into main 2026-09-05 01:06:59 +00:00
6 changed files with 262 additions and 0 deletions
Showing only changes of commit d2d20a76b6 - Show all commits
+105
View File
@@ -0,0 +1,105 @@
// cloudflare-dns's own code (novox/hq ADR 0044). It provides the mesh `public-dns` interface
// (ADR 0049): a public name that resolves to the mesh's public ingress. Cloudflare is one registrar
// behind the neutral interface — a consumer names `public-dns`, never Cloudflare — so this file is
// the only place Cloudflare's API appears, and swapping registrars swaps only this module.
import { readFileSync } from "node:fs";
export interface PublicRecord {
id: string;
name: string;
type: string;
content: string;
}
export class CloudflareClient {
constructor(
private readonly token: string,
private readonly zoneId: string,
/** The zone this registers under, e.g. "example.com". */
readonly domain: string,
/** What every public name points at — the mesh's public ingress (the reverse proxy). */
readonly ingress: string,
) {}
static fromEnv(env: NodeJS.ProcessEnv = process.env): CloudflareClient {
const token = env.MESH_CLOUDFLARE_TOKEN ?? readSecret(env.MESH_CLOUDFLARE_TOKEN_FILE);
const zoneId = env.MESH_CLOUDFLARE_ZONE_ID;
const domain = env.MESH_PUBLIC_DOMAIN;
const ingress = env.MESH_PUBLIC_INGRESS;
if (!token || !zoneId || !domain || !ingress) {
throw new Error(
"cloudflare-dns needs MESH_CLOUDFLARE_TOKEN (or _FILE), MESH_CLOUDFLARE_ZONE_ID, " +
"MESH_PUBLIC_DOMAIN and MESH_PUBLIC_INGRESS — it cannot register a name without them",
);
}
return new CloudflareClient(token, zoneId, domain, ingress);
}
/**
* The public name a consumer gets: derived from its identity under the mesh's domain. Derived, not
* contributed, for the same reason minio derives a bucket name — the harness hands `remove` only
* the identity, so teardown must recompute exactly what creation made.
*/
nameFor(consumer: string): string {
return `${consumer.replace(/[^A-Za-z0-9-]/g, "-").toLowerCase()}.${this.domain}`;
}
/** An IP points at itself (A/AAAA); a hostname points through a CNAME. */
private recordType(): "A" | "AAAA" | "CNAME" {
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(this.ingress)) return "A";
if (this.ingress.includes(":")) return "AAAA";
return "CNAME";
}
private async api<T>(method: string, path: string, body?: unknown): Promise<T> {
const res = await fetch(`https://api.cloudflare.com/client/v4${path}`, {
method,
headers: { authorization: `Bearer ${this.token}`, "content-type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
const json = (await res.json()) as { success?: boolean; result?: unknown; errors?: unknown };
if (!res.ok || json.success === false) {
throw new Error(`cloudflare ${method} ${path}: ${res.status} ${JSON.stringify(json.errors ?? json)}`);
}
return json.result as T;
}
async findRecord(name: string): Promise<PublicRecord | undefined> {
const records = await this.api<PublicRecord[]>(
"GET",
`/zones/${this.zoneId}/dns_records?name=${encodeURIComponent(name)}`,
);
return records[0];
}
/** Point a public name at the mesh's ingress, idempotently — create it, or update one already there. */
async upsert(name: string): Promise<PublicRecord> {
const body = { type: this.recordType(), name, content: this.ingress, ttl: 300, proxied: false };
const existing = await this.findRecord(name);
if (existing) {
return this.api<PublicRecord>("PUT", `/zones/${this.zoneId}/dns_records/${existing.id}`, body);
}
return this.api<PublicRecord>("POST", `/zones/${this.zoneId}/dns_records`, body);
}
/** Remove a public name, idempotently — a record already gone is not an error on reconcile. */
async remove(name: string): Promise<void> {
const existing = await this.findRecord(name);
if (existing) await this.api("DELETE", `/zones/${this.zoneId}/dns_records/${existing.id}`);
}
/** Every record in the zone, for the diagnostic tool. */
async records(): Promise<PublicRecord[]> {
return this.api<PublicRecord[]>("GET", `/zones/${this.zoneId}/dns_records`);
}
}
function readSecret(path: string | undefined): string | undefined {
if (!path) return undefined;
try {
return readFileSync(path, "utf8").trim();
} catch {
return undefined;
}
}
+61
View File
@@ -0,0 +1,61 @@
{
"module": "cloudflare-dns",
"version": "1",
"provides": [
{
"name": "public-dns",
"scope": "mesh"
}
],
"serves": {
"public-dns": {}
},
"grants": {
"public-dns": "/var/lib/cloudflare-dns/grants"
},
"receives": {
"public-dns": "/var/lib/cloudflare-dns/grants/mesh.json"
},
"own-secrets": {
"token": "/var/lib/cloudflare-dns/token",
"broker": "/var/lib/cloudflare-dns/broker"
},
"emits": [
"module.cloudflare-dns.record.created",
"module.cloudflare-dns.record.removed"
],
"resources": [
{
"id": "state",
"type": "directory",
"path": "/var/lib/cloudflare-dns",
"mode": "0700"
},
{
"id": "grants",
"type": "directory",
"path": "/var/lib/cloudflare-dns/grants",
"mode": "0700"
},
{
"id": "provisioner",
"type": "container",
"name": "mesh-provision-cloudflare-dns",
"image": "mesh-provision-cloudflare-dns@sha256:0000000000000000000000000000000000000000000000000000000000000000",
"network": "host",
"env": {
"GRANTS": "/grants",
"MESH_CLOUDFLARE_TOKEN_FILE": "/run/secrets/token",
"MESH_BROKER_FILE": "/run/secrets/broker",
"MESH_CLOUDFLARE_ZONE_ID": "",
"MESH_PUBLIC_DOMAIN": "",
"MESH_PUBLIC_INGRESS": ""
},
"volumes": [
"/var/lib/cloudflare-dns/grants:/grants",
"/var/lib/cloudflare-dns/token:/run/secrets/token:ro",
"/var/lib/cloudflare-dns/broker:/run/secrets/broker:ro"
]
}
]
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "@novox/module-cloudflare-dns",
"version": "0.1.0",
"description": "cloudflare-dns — a public-dns provider (ADR 0049): registers public names at Cloudflare.
"type": "module",
"private": true,
"dependencies": {
"@novox/mesh-sdk": "^0.1.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"typescript": "^5.6.0"
}
}
@@ -0,0 +1,43 @@
// cloudflare-dns's provisioner — the adapter making it a provider of the mesh `public-dns` interface
// (novox/hq ADR 0049). The reconcile loop, sealing and grant-file handling are the sdk harness's;
// this writes only the per-registrar half: register a consumer's public name at Cloudflare, pointing
// it at the mesh's ingress, and remove it when the grant is withdrawn.
//
// The `public-dns` interface hands a consumer { fqdn, target, ttl } — a name that resolves publicly
// and what it resolves to. It is not a secret (a DNS record is public), so nothing is sealed beyond
// what the harness seals; the only secret is this module's own Cloudflare token, which never leaves.
import { runProvisioner, type Grant, type Credential } from "@novox/mesh-sdk/provisioner";
import { emit } from "@novox/mesh-sdk/events";
import { CloudflareClient } from "../client.js";
const cloudflare = CloudflareClient.fromEnv();
runProvisioner("public-dns", {
async create(grant: Grant): Promise<Credential> {
const fqdn = cloudflare.nameFor(grant.consumer);
await cloudflare.upsert(fqdn);
await announce("module.cloudflare-dns.record.created", {
name: fqdn,
target: cloudflare.ingress,
consumer: grant.consumer,
node: grant.node,
});
return { fields: { fqdn, target: cloudflare.ingress, ttl: "300" } };
},
async remove(grant: Grant): Promise<void> {
const fqdn = cloudflare.nameFor(grant.consumer);
await cloudflare.remove(fqdn);
await announce("module.cloudflare-dns.record.removed", { name: fqdn, consumer: grant.consumer, node: grant.node });
},
});
/** Emit best-effort: a broker hiccup must never fail or reverse a DNS change that already happened. */
async function announce(type: string, body: unknown): Promise<void> {
try {
await emit(type, body);
} catch (err) {
console.error(`[cloudflare-dns] could not emit ${type}: ${err}`);
}
}
+23
View File
@@ -0,0 +1,23 @@
// cloudflare-dns's tool — the diagnostic: what public names the mesh currently publishes here.
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
import { CloudflareClient } from "../client.js";
export function getCloudflareDnsTools(cloudflare: CloudflareClient): ToolDefinition[] {
return [
{
name: "cloudflare_dns_records",
description: "The public DNS records in the mesh's zone — the names it currently publishes.",
input: {},
run: async () => ({ domain: cloudflare.domain, ingress: cloudflare.ingress, records: await cloudflare.records() }),
},
];
}
registerModuleTools("cloudflare-dns", (env) => {
try {
return getCloudflareDnsTools(CloudflareClient.fromEnv(env));
} catch {
return [];
}
});
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
},
"include": [
"client.ts",
"tools/index.ts",
"provisioner/index.ts"
]
}