Stand up mesh-sdk — the stable spine a module builds against
Per novox/hq ADR 0044/0045: the sdk holds only what rarely changes and is shared across modules; per-module code (a client, tool impls, a create-a-resource adapter) lives in the module. Five areas, real and tested: - contracts: the runtime shapes module code touches (grant, credential, a mesh Interface, tool + envelope types) — not the manifest schema, which the control plane owns. - provisioner: the reconcile harness every provider shares (watch grants, create via the module's adapter, seal + write the credential, remove on withdrawal). A module writes only the adapter. - tools: registerModuleTools + collectTools — the serving harness; tools and their client live in the module. - messaging: the Broker/Envelope/event contract over the mesh broker; the concrete binding is provided by the hosting runtime. - primitives: AES-256-GCM seal/unseal, semver, resolved-env access. Compiles (tsc, NodeNext) and passes tests: sealing round-trip + wrong-key rejection, semver, tool registration (a thrower is skipped not fatal), and the provisioner creating then removing a sealed grant. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, writeFile, readFile, readdir } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { seal, unseal, compareVersions } from "../dist/primitives/index.js";
|
||||
import { registerModuleTools, collectTools, resetTools } from "../dist/tools/index.js";
|
||||
import { runProvisioner, type Grant } from "../dist/provisioner/index.js";
|
||||
|
||||
test("seal round-trips and rejects the wrong key", () => {
|
||||
const sealed = seal("hunter2", "node-key");
|
||||
assert.equal(unseal(sealed, "node-key"), "hunter2");
|
||||
assert.notEqual(sealed, "hunter2");
|
||||
assert.throws(() => unseal(sealed, "wrong-key"));
|
||||
});
|
||||
|
||||
test("semver orders releases", () => {
|
||||
assert.equal(compareVersions("1.2.3", "1.2.10"), -1);
|
||||
assert.equal(compareVersions("2.0.0", "1.9.9"), 1);
|
||||
assert.equal(compareVersions("v1.0.0", "1.0.0"), 0);
|
||||
});
|
||||
|
||||
test("module tools register and collect, a thrower is skipped not fatal", () => {
|
||||
resetTools();
|
||||
registerModuleTools("umami", () => [
|
||||
{ name: "umami_stats", description: "d", input: {}, run: async () => 1 },
|
||||
]);
|
||||
registerModuleTools("broken", () => {
|
||||
throw new Error("no token");
|
||||
});
|
||||
const collected = collectTools({});
|
||||
const umami = collected.find((c) => c.module === "umami");
|
||||
const broken = collected.find((c) => c.module === "broken");
|
||||
assert.equal(umami?.tools.length, 1);
|
||||
assert.equal(broken?.tools.length, 0);
|
||||
});
|
||||
|
||||
test("provisioner creates a sealed credential for a grant, then removes on withdrawal", async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), "prov-"));
|
||||
const created: string[] = [];
|
||||
const removed: string[] = [];
|
||||
|
||||
const grant: Grant = { resource: "analytics", consumer: "webapp", node: "anchor", values: { name: "webapp" } };
|
||||
await writeFile(join(dir, "webapp.grant.json"), JSON.stringify(grant));
|
||||
|
||||
const stop = runProvisioner(
|
||||
"analytics",
|
||||
{
|
||||
async create(g) {
|
||||
created.push(g.consumer);
|
||||
return { fields: { siteId: "abc", snippet: "<script>", dashboard: "https://x/webapp" } };
|
||||
},
|
||||
async remove(g) {
|
||||
removed.push(g.consumer);
|
||||
},
|
||||
},
|
||||
{ grants: dir, sealKey: "k", everyMs: 20 },
|
||||
);
|
||||
|
||||
await waitFor(() => created.length === 1, 2000);
|
||||
const files = await readdir(dir);
|
||||
const credFile = files.find((f) => f.endsWith(".credential"));
|
||||
assert.ok(credFile, "a credential file was written");
|
||||
const sealed = await readFile(join(dir, credFile!), "utf8");
|
||||
const body = JSON.parse(unseal(sealed, "k")) as { fields: Record<string, string> };
|
||||
assert.equal(body.fields.siteId, "abc");
|
||||
|
||||
// Withdraw the grant → the harness removes via the adapter and deletes the credential.
|
||||
await (await import("node:fs/promises")).rm(join(dir, "webapp.grant.json"));
|
||||
await waitFor(() => removed.length === 1, 2000);
|
||||
stop();
|
||||
});
|
||||
|
||||
async function waitFor(cond: () => boolean, ms: number): Promise<void> {
|
||||
const start = Date.now();
|
||||
while (!cond()) {
|
||||
if (Date.now() - start > ms) throw new Error("timed out waiting");
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user