@rubytech/create-maxy-code 0.1.58 → 0.1.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +30 -19
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +35 -90
- package/payload/platform/plugins/docs/references/admin-session.md +17 -16
- package/payload/platform/plugins/docs/references/platform.md +3 -3
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/loop/PLUGIN.md +13 -1
- package/payload/platform/plugins/loop/mcp/dist/index.js +8 -7
- package/payload/platform/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/lib/file-crypto.d.ts +3 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/file-crypto.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/file-crypto.js +48 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/file-crypto.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/key-store.d.ts +22 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/key-store.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/key-store.js +165 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/key-store.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/lib/loop-api.d.ts +1 -1
- package/payload/platform/plugins/loop/mcp/dist/lib/loop-api.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/lib/loop-api.js +4 -39
- package/payload/platform/plugins/loop/mcp/dist/lib/loop-api.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/key-deregister.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/key-deregister.js +5 -15
- package/payload/platform/plugins/loop/mcp/dist/tools/key-deregister.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/key-register.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/key-register.js +11 -28
- package/payload/platform/plugins/loop/mcp/dist/tools/key-register.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/src/index.ts +10 -7
- package/payload/platform/plugins/loop/mcp/src/lib/file-crypto.ts +54 -0
- package/payload/platform/plugins/loop/mcp/src/lib/key-store.ts +221 -0
- package/payload/platform/plugins/loop/mcp/src/lib/loop-api.ts +5 -42
- package/payload/platform/plugins/loop/mcp/src/tools/key-deregister.ts +5 -20
- package/payload/platform/plugins/loop/mcp/src/tools/key-register.ts +14 -39
- package/payload/platform/plugins/preval/.claude-plugin/plugin.json +8 -0
- package/payload/platform/plugins/preval/PLUGIN.md +39 -0
- package/payload/platform/plugins/preval/skills/property-preval/SKILL.md +78 -0
- package/payload/platform/plugins/preval/skills/property-preval/references/render.py +265 -0
- package/payload/platform/plugins/preval/skills/property-preval/references/template-inputs.schema.json +143 -0
- package/payload/platform/plugins/preval/skills/property-preval/references/template.html +253 -0
- package/payload/platform/plugins/property-data/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/property-data/PLUGIN.md +60 -2
- package/payload/platform/plugins/property-data/mcp/dist/index.js +95 -12
- package/payload/platform/plugins/property-data/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/lib/file-crypto.d.ts +3 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/file-crypto.d.ts.map +1 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/file-crypto.js +49 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/file-crypto.js.map +1 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/key-store.d.ts +15 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/key-store.d.ts.map +1 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/key-store.js +135 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/key-store.js.map +1 -0
- package/payload/platform/plugins/property-data/mcp/dist/lib/propertydata-api.d.ts +0 -1
- package/payload/platform/plugins/property-data/mcp/dist/lib/propertydata-api.d.ts.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/lib/propertydata-api.js +11 -22
- package/payload/platform/plugins/property-data/mcp/dist/lib/propertydata-api.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.d.ts +10 -0
- package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.d.ts.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.js +15 -2
- package/payload/platform/plugins/property-data/mcp/dist/tools/endpoints.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-deregister.d.ts.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-deregister.js +5 -15
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-deregister.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-register.d.ts.map +1 -1
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-register.js +30 -23
- package/payload/platform/plugins/property-data/mcp/dist/tools/key-register.js.map +1 -1
- package/payload/platform/plugins/property-data/mcp/src/__tests__/endpoints.test.ts +252 -0
- package/payload/platform/plugins/property-data/mcp/src/__tests__/schema-parity.test.ts +16 -2
- package/payload/platform/plugins/property-data/mcp/src/index.ts +171 -11
- package/payload/platform/plugins/property-data/mcp/src/lib/file-crypto.ts +55 -0
- package/payload/platform/plugins/property-data/mcp/src/lib/key-store.ts +181 -0
- package/payload/platform/plugins/property-data/mcp/src/lib/propertydata-api.ts +9 -24
- package/payload/platform/plugins/property-data/mcp/src/tools/endpoints.ts +22 -2
- package/payload/platform/plugins/property-data/mcp/src/tools/key-deregister.ts +5 -17
- package/payload/platform/plugins/property-data/mcp/src/tools/key-register.ts +29 -33
- package/payload/platform/plugins/property-data/skills/property-data/SKILL.md +19 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +25 -260
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts +3 -7
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js +6 -32
- package/payload/platform/services/claude-session-manager/dist/jsonl-enumerator.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +6 -24
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +18 -91
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/reaper.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/reaper.js +0 -11
- package/payload/platform/services/claude-session-manager/dist/reaper.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-store.d.ts +0 -21
- package/payload/platform/services/claude-session-manager/dist/session-store.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-store.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +1 -1
- package/payload/platform/templates/agents/admin/SOUL.md +1 -1
- package/payload/premium-plugins/real-agent/BUNDLE.md +5 -3
- package/payload/premium-plugins/real-agent/agents/negotiator.md +1 -1
- package/payload/premium-plugins/real-agent/agents/valuer.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/.claude-plugin/marketplace.json +5 -0
- package/payload/premium-plugins/real-agent/plugins/loop/PLUGIN.md +13 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js +8 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/file-crypto.d.ts +3 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/file-crypto.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/file-crypto.js +48 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/file-crypto.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/key-store.d.ts +22 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/key-store.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/key-store.js +165 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/key-store.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.d.ts +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.js +4 -39
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/lib/loop-api.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-deregister.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-deregister.js +5 -15
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-deregister.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-register.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-register.js +11 -28
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/key-register.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/index.ts +10 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/lib/file-crypto.ts +54 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/lib/key-store.ts +221 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/lib/loop-api.ts +5 -42
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/key-deregister.ts +5 -20
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/key-register.ts +14 -39
- package/payload/premium-plugins/real-agent/plugins/preval/.claude-plugin/plugin.json +8 -0
- package/payload/premium-plugins/real-agent/plugins/preval/PLUGIN.md +39 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/SKILL.md +78 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/render.py +265 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template-inputs.schema.json +143 -0
- package/payload/premium-plugins/real-agent/plugins/preval/skills/property-preval/references/template.html +253 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/.claude-plugin/plugin.json +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/PLUGIN.md +60 -2
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/index.js +95 -12
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/file-crypto.d.ts +3 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/file-crypto.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/file-crypto.js +49 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/file-crypto.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/key-store.d.ts +15 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/key-store.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/key-store.js +135 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/key-store.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/propertydata-api.d.ts +0 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/propertydata-api.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/propertydata-api.js +11 -22
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/lib/propertydata-api.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.d.ts +10 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.js +15 -2
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/endpoints.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-deregister.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-deregister.js +5 -15
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-deregister.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-register.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-register.js +30 -23
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/dist/tools/key-register.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/__tests__/endpoints.test.ts +252 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/__tests__/schema-parity.test.ts +16 -2
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/index.ts +171 -11
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/lib/file-crypto.ts +55 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/lib/key-store.ts +181 -0
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/lib/propertydata-api.ts +9 -24
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/tools/endpoints.ts +22 -2
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/tools/key-deregister.ts +5 -17
- package/payload/premium-plugins/real-agent/plugins/property-data/mcp/src/tools/key-register.ts +29 -33
- package/payload/premium-plugins/real-agent/plugins/property-data/skills/property-data/SKILL.md +19 -0
- package/payload/server/public/assets/{admin-2ZJmzJ_g.js → admin-Bp-BjBCX.js} +51 -51
- package/payload/server/public/assets/{data-DlmoKxmN.js → data-BGUAGVkV.js} +1 -1
- package/payload/server/public/assets/{graph-BVvcFSEQ.js → graph-MvYxZOBF.js} +1 -1
- package/payload/server/public/assets/{graph-labels-cCvgu38Y.js → graph-labels-D865qb3K.js} +1 -1
- package/payload/server/public/assets/{page-BnAxPkmg.js → page--hOVRrgN.js} +1 -1
- package/payload/server/public/assets/{page-BnaN3u86.js → page-C2b1nlOc.js} +1 -1
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/server.js +18 -83
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.d.ts +0 -51
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.d.ts.map +0 -1
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.js +0 -196
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/dist/index.js.map +0 -1
- package/payload/premium-plugins/real-agent/lib/mcp-stderr-tee/package.json +0 -7
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task 144 — per-endpoint coverage for the 10 new PropertyData adapters.
|
|
3
|
+
* For each new endpoint, assert:
|
|
4
|
+
* 1. happy path — JSON parses and round-trips through the adapter
|
|
5
|
+
* 2. 4xx upstream → PropertyDataError with status preserved
|
|
6
|
+
* 3. non-JSON body → BadResponseError
|
|
7
|
+
* 4. missing key → KeyNotRegisteredError
|
|
8
|
+
*
|
|
9
|
+
* The existing propertydata-api.test.ts proves the four paths fire from
|
|
10
|
+
* `propertyDataCall`; this file proves each of the 10 adapter exports
|
|
11
|
+
* threads its own endpoint path through that same plumbing so the
|
|
12
|
+
* MCP tool actually hits the right URL.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
16
|
+
|
|
17
|
+
// Mock the Neo4j layer keyed by accountId so each test can choose an
|
|
18
|
+
// already-registered account (happy / 4xx / bad-response) or a brand-new
|
|
19
|
+
// "ghost" account (missing-key) without ordering coupling.
|
|
20
|
+
vi.mock("../lib/neo4j.js", () => {
|
|
21
|
+
const stored = new Map<string, string>();
|
|
22
|
+
return {
|
|
23
|
+
getSession: () => ({
|
|
24
|
+
run: vi.fn(async (cypher: string, params: Record<string, unknown>) => {
|
|
25
|
+
const accountId = String(params.accountId ?? "");
|
|
26
|
+
if (/MATCH \(k:PropertyDataKey/.test(cypher) && /RETURN/.test(cypher)) {
|
|
27
|
+
const encryptedKey = stored.get(accountId);
|
|
28
|
+
return {
|
|
29
|
+
records: encryptedKey
|
|
30
|
+
? [
|
|
31
|
+
{
|
|
32
|
+
get: (k: string) => {
|
|
33
|
+
if (k === "encryptedKey") return encryptedKey;
|
|
34
|
+
if (k === "createdAt") return "2026-01-01T00:00:00Z";
|
|
35
|
+
if (k === "updatedAt") return "2026-01-01T00:00:00Z";
|
|
36
|
+
return null;
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
]
|
|
40
|
+
: [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (/MERGE \(k:PropertyDataKey/.test(cypher)) {
|
|
44
|
+
stored.set(accountId, String(params.encryptedKey));
|
|
45
|
+
return { records: [] };
|
|
46
|
+
}
|
|
47
|
+
if (/DELETE k/.test(cypher)) {
|
|
48
|
+
const had = stored.delete(accountId);
|
|
49
|
+
return {
|
|
50
|
+
records: [
|
|
51
|
+
{
|
|
52
|
+
get: (k: string) =>
|
|
53
|
+
k === "deleted" ? { toNumber: () => (had ? 1 : 0) } : null,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return { records: [] };
|
|
59
|
+
}),
|
|
60
|
+
close: vi.fn(async () => {}),
|
|
61
|
+
}),
|
|
62
|
+
closeDriver: vi.fn(async () => {}),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
vi.mock("../lib/crypto.js", () => ({
|
|
67
|
+
encrypt: (s: string) => `enc:${s}`,
|
|
68
|
+
decrypt: (s: string) => s.replace(/^enc:/, ""),
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
import {
|
|
72
|
+
pricesPerSqf,
|
|
73
|
+
soldPricesPerSqf,
|
|
74
|
+
crime,
|
|
75
|
+
floodRisk,
|
|
76
|
+
councilTax,
|
|
77
|
+
planningApplications,
|
|
78
|
+
addressMatchUprn,
|
|
79
|
+
growthPsf,
|
|
80
|
+
demandRent,
|
|
81
|
+
propertyTypes,
|
|
82
|
+
} from "../tools/endpoints.js";
|
|
83
|
+
import {
|
|
84
|
+
KeyNotRegisteredError,
|
|
85
|
+
PropertyDataError,
|
|
86
|
+
BadResponseError,
|
|
87
|
+
} from "../lib/propertydata-api.js";
|
|
88
|
+
import { keyRegister } from "../tools/key-register.js";
|
|
89
|
+
|
|
90
|
+
const originalFetch = globalThis.fetch;
|
|
91
|
+
|
|
92
|
+
function mockFetch(impl: (url: string) => Response | Promise<Response>) {
|
|
93
|
+
const fn = vi.fn(async (url: string | URL | Request) => impl(String(url))) as unknown as typeof fetch;
|
|
94
|
+
globalThis.fetch = fn;
|
|
95
|
+
return fn as unknown as ReturnType<typeof vi.fn>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function registerTestKey(accountId: string) {
|
|
99
|
+
mockFetch(() => new Response(JSON.stringify({ status: "success", uk_hpi: 100 }), { status: 200 }));
|
|
100
|
+
await keyRegister({ apiKey: "test-key-12345", accountId });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
// no-op
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
globalThis.fetch = originalFetch;
|
|
109
|
+
vi.restoreAllMocks();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
interface EndpointCase {
|
|
113
|
+
toolName: string;
|
|
114
|
+
endpointPath: string;
|
|
115
|
+
adapter: (args: { accountId: string } & Record<string, unknown>) => Promise<string>;
|
|
116
|
+
sampleParams: Record<string, unknown>;
|
|
117
|
+
happyPayload: Record<string, unknown>;
|
|
118
|
+
happyAssertField: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const cases: EndpointCase[] = [
|
|
122
|
+
{
|
|
123
|
+
toolName: "property-data-prices-per-sqf",
|
|
124
|
+
endpointPath: "prices-per-sqf",
|
|
125
|
+
adapter: pricesPerSqf,
|
|
126
|
+
sampleParams: { postcode: "CM23 2UJ" },
|
|
127
|
+
happyPayload: { status: "success", data: { avg: 450 } },
|
|
128
|
+
happyAssertField: "avg",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
toolName: "property-data-sold-prices-per-sqf",
|
|
132
|
+
endpointPath: "sold-prices-per-sqf",
|
|
133
|
+
adapter: soldPricesPerSqf,
|
|
134
|
+
sampleParams: { postcode: "CM23 2UJ", max_age: 24 },
|
|
135
|
+
happyPayload: { status: "success", data: { avg: 425, sales: [] } },
|
|
136
|
+
happyAssertField: "avg",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
toolName: "property-data-crime",
|
|
140
|
+
endpointPath: "crime",
|
|
141
|
+
adapter: crime,
|
|
142
|
+
sampleParams: { postcode: "CM23" },
|
|
143
|
+
happyPayload: { status: "success", data: { rate_per_1000: 42 } },
|
|
144
|
+
happyAssertField: "rate_per_1000",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
toolName: "property-data-flood-risk",
|
|
148
|
+
endpointPath: "flood-risk",
|
|
149
|
+
adapter: floodRisk,
|
|
150
|
+
sampleParams: { postcode: "CM23 2UJ" },
|
|
151
|
+
happyPayload: { status: "success", data: { river: "very-low" } },
|
|
152
|
+
happyAssertField: "river",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
toolName: "property-data-council-tax",
|
|
156
|
+
endpointPath: "council-tax",
|
|
157
|
+
adapter: councilTax,
|
|
158
|
+
sampleParams: { postcode: "CM23 2UJ" },
|
|
159
|
+
happyPayload: { status: "success", data: { council: "East Herts", band_d: 2100 } },
|
|
160
|
+
happyAssertField: "council",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
toolName: "property-data-planning-applications",
|
|
164
|
+
endpointPath: "planning-applications",
|
|
165
|
+
adapter: planningApplications,
|
|
166
|
+
sampleParams: { postcode: "CM23", results: 10 },
|
|
167
|
+
happyPayload: { status: "success", data: { applications: [] } },
|
|
168
|
+
happyAssertField: "applications",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
toolName: "property-data-address-match-uprn",
|
|
172
|
+
endpointPath: "address-match-uprn",
|
|
173
|
+
adapter: addressMatchUprn,
|
|
174
|
+
sampleParams: { address: "1 The High Street, Bishop's Stortford, CM23 2UJ" },
|
|
175
|
+
happyPayload: { status: "success", data: { uprn: "10001234567" } },
|
|
176
|
+
happyAssertField: "uprn",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
toolName: "property-data-growth-psf",
|
|
180
|
+
endpointPath: "growth-psf",
|
|
181
|
+
adapter: growthPsf,
|
|
182
|
+
sampleParams: { postcode: "CM23" },
|
|
183
|
+
happyPayload: { status: "success", data: { series: [{ year: 2019, psf: 400 }] } },
|
|
184
|
+
happyAssertField: "series",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
toolName: "property-data-demand-rent",
|
|
188
|
+
endpointPath: "demand-rent",
|
|
189
|
+
adapter: demandRent,
|
|
190
|
+
sampleParams: { postcode: "CM23", bedrooms: 3 },
|
|
191
|
+
happyPayload: { status: "success", data: { for_rent: 12, lets_per_month: 4 } },
|
|
192
|
+
happyAssertField: "for_rent",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
toolName: "property-data-property-types",
|
|
196
|
+
endpointPath: "property-types",
|
|
197
|
+
adapter: propertyTypes,
|
|
198
|
+
sampleParams: { postcode: "CM23" },
|
|
199
|
+
happyPayload: { status: "success", data: { detached: 28, flat: 12 } },
|
|
200
|
+
happyAssertField: "detached",
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
describe("Task 144 endpoint adapters", () => {
|
|
205
|
+
for (const c of cases) {
|
|
206
|
+
describe(c.toolName, () => {
|
|
207
|
+
const acct = `acct-${c.endpointPath}`;
|
|
208
|
+
|
|
209
|
+
it("happy path returns the parsed JSON payload", async () => {
|
|
210
|
+
await registerTestKey(acct);
|
|
211
|
+
let capturedUrl = "";
|
|
212
|
+
mockFetch((url) => {
|
|
213
|
+
capturedUrl = url;
|
|
214
|
+
return new Response(JSON.stringify(c.happyPayload), { status: 200 });
|
|
215
|
+
});
|
|
216
|
+
const text = await c.adapter({ accountId: acct, ...c.sampleParams });
|
|
217
|
+
expect(capturedUrl).toContain(`/${c.endpointPath}?`);
|
|
218
|
+
const parsed = JSON.parse(text);
|
|
219
|
+
// payload has shape { status, data: { ... } } — assert the field
|
|
220
|
+
// exists somewhere in the round-tripped output.
|
|
221
|
+
expect(JSON.stringify(parsed)).toContain(c.happyAssertField);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("maps HTTP 4xx to PropertyDataError preserving the status", async () => {
|
|
225
|
+
await registerTestKey(acct);
|
|
226
|
+
mockFetch(() => new Response("bad request", { status: 400 }));
|
|
227
|
+
try {
|
|
228
|
+
await c.adapter({ accountId: acct, ...c.sampleParams });
|
|
229
|
+
throw new Error("expected PropertyDataError");
|
|
230
|
+
} catch (err) {
|
|
231
|
+
expect(err).toBeInstanceOf(PropertyDataError);
|
|
232
|
+
expect((err as PropertyDataError).status).toBe(400);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("maps non-JSON body to BadResponseError", async () => {
|
|
237
|
+
await registerTestKey(acct);
|
|
238
|
+
mockFetch(() => new Response("<html>maintenance</html>", { status: 200 }));
|
|
239
|
+
await expect(
|
|
240
|
+
c.adapter({ accountId: acct, ...c.sampleParams }),
|
|
241
|
+
).rejects.toBeInstanceOf(BadResponseError);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("throws KeyNotRegisteredError when no key is stored", async () => {
|
|
245
|
+
// brand-new account with nothing registered
|
|
246
|
+
await expect(
|
|
247
|
+
c.adapter({ accountId: `${acct}-ghost`, ...c.sampleParams }),
|
|
248
|
+
).rejects.toBeInstanceOf(KeyNotRegisteredError);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
});
|
|
@@ -38,11 +38,25 @@ describe("schema parity", () => {
|
|
|
38
38
|
expect(registered).toEqual(declared);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
it("declares all
|
|
41
|
+
it("declares all 22 tools (3 key-mgmt + 19 endpoints)", () => {
|
|
42
42
|
const declared = readPluginMdTools();
|
|
43
|
-
expect(declared).toHaveLength(
|
|
43
|
+
expect(declared).toHaveLength(22);
|
|
44
44
|
expect(declared).toContain("property-data-key-register");
|
|
45
45
|
expect(declared).toContain("property-data-valuation-sale");
|
|
46
46
|
expect(declared).toContain("property-data-national-hpi");
|
|
47
|
+
for (const name of [
|
|
48
|
+
"property-data-prices-per-sqf",
|
|
49
|
+
"property-data-sold-prices-per-sqf",
|
|
50
|
+
"property-data-crime",
|
|
51
|
+
"property-data-flood-risk",
|
|
52
|
+
"property-data-council-tax",
|
|
53
|
+
"property-data-planning-applications",
|
|
54
|
+
"property-data-address-match-uprn",
|
|
55
|
+
"property-data-growth-psf",
|
|
56
|
+
"property-data-demand-rent",
|
|
57
|
+
"property-data-property-types",
|
|
58
|
+
]) {
|
|
59
|
+
expect(declared).toContain(name);
|
|
60
|
+
}
|
|
47
61
|
});
|
|
48
62
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// Logging-tee was a platform-wrapper concern: the platform's mcp-spawn-tee
|
|
2
|
+
// pipes our stderr where it wants. In standalone mode (Claude Code native
|
|
3
|
+
// spawn) the host owns the stderr pipe. Either way, we just write to stderr.
|
|
3
4
|
|
|
4
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -17,6 +18,16 @@ import {
|
|
|
17
18
|
yields,
|
|
18
19
|
growth,
|
|
19
20
|
nationalHpi,
|
|
21
|
+
pricesPerSqf,
|
|
22
|
+
soldPricesPerSqf,
|
|
23
|
+
crime,
|
|
24
|
+
floodRisk,
|
|
25
|
+
councilTax,
|
|
26
|
+
planningApplications,
|
|
27
|
+
addressMatchUprn,
|
|
28
|
+
growthPsf,
|
|
29
|
+
demandRent,
|
|
30
|
+
propertyTypes,
|
|
20
31
|
} from "./tools/endpoints.js";
|
|
21
32
|
import { closeDriver } from "./lib/neo4j.js";
|
|
22
33
|
import {
|
|
@@ -24,18 +35,22 @@ import {
|
|
|
24
35
|
PropertyDataError,
|
|
25
36
|
BadResponseError,
|
|
26
37
|
} from "./lib/propertydata-api.js";
|
|
38
|
+
import { selectKeyStore } from "./lib/key-store.js";
|
|
27
39
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
// ACCOUNT_ID is the platform's tenant id; in standalone mode (Claude Code
|
|
41
|
+
// native spawn) there is no platform tenant, so "local" is the single-tenant
|
|
42
|
+
// stand-in. Same MCP body, env-selected backend.
|
|
43
|
+
const accountId = process.env.ACCOUNT_ID ?? "local";
|
|
32
44
|
|
|
33
45
|
const server = new McpServer({
|
|
34
46
|
name: "maxy-property-data",
|
|
35
47
|
version: "0.1.0",
|
|
36
48
|
});
|
|
37
49
|
|
|
38
|
-
|
|
50
|
+
const store = selectKeyStore();
|
|
51
|
+
console.error(
|
|
52
|
+
`[property-data] server started, account=${accountId} backend=${store.backendName} path=${store.backendPath}`,
|
|
53
|
+
);
|
|
39
54
|
|
|
40
55
|
function errorEnvelope(err: unknown): { content: { type: "text"; text: string }[]; isError: true } {
|
|
41
56
|
let text: string;
|
|
@@ -57,7 +72,7 @@ function endpointHandler<P extends Record<string, unknown>>(
|
|
|
57
72
|
) {
|
|
58
73
|
return async (params: P) => {
|
|
59
74
|
try {
|
|
60
|
-
const text = await fn({ ...params, accountId
|
|
75
|
+
const text = await fn({ ...params, accountId });
|
|
61
76
|
return { content: [{ type: "text" as const, text }] };
|
|
62
77
|
} catch (err) {
|
|
63
78
|
return errorEnvelope(err);
|
|
@@ -77,7 +92,7 @@ server.tool(
|
|
|
77
92
|
},
|
|
78
93
|
async ({ apiKey }) => {
|
|
79
94
|
try {
|
|
80
|
-
await keyRegister({ apiKey, accountId
|
|
95
|
+
await keyRegister({ apiKey, accountId });
|
|
81
96
|
return {
|
|
82
97
|
content: [
|
|
83
98
|
{
|
|
@@ -98,7 +113,7 @@ server.tool(
|
|
|
98
113
|
{},
|
|
99
114
|
async () => {
|
|
100
115
|
try {
|
|
101
|
-
await keyDeregister({ accountId
|
|
116
|
+
await keyDeregister({ accountId });
|
|
102
117
|
return {
|
|
103
118
|
content: [{ type: "text" as const, text: "PropertyData API key deregistered." }],
|
|
104
119
|
};
|
|
@@ -114,7 +129,7 @@ server.tool(
|
|
|
114
129
|
{},
|
|
115
130
|
async () => {
|
|
116
131
|
try {
|
|
117
|
-
const text = await keyList({ accountId
|
|
132
|
+
const text = await keyList({ accountId });
|
|
118
133
|
return { content: [{ type: "text" as const, text }] };
|
|
119
134
|
} catch (err) {
|
|
120
135
|
return errorEnvelope(err);
|
|
@@ -236,6 +251,151 @@ server.tool(
|
|
|
236
251
|
endpointHandler(nationalHpi),
|
|
237
252
|
);
|
|
238
253
|
|
|
254
|
+
// ─────────────────────────────────────────────────────────────────
|
|
255
|
+
// Task 144 — £/sqft baselines and growth
|
|
256
|
+
// ─────────────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
const pricesPerSqfSchema = {
|
|
259
|
+
postcode: z.string().min(2).describe("UK postcode (outcode or full)"),
|
|
260
|
+
type: z
|
|
261
|
+
.enum(["detached", "semi-detached", "terraced", "flat"])
|
|
262
|
+
.optional()
|
|
263
|
+
.describe("Filter by property type"),
|
|
264
|
+
bedrooms: z.number().int().min(0).optional().describe("Filter by bedroom count"),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
server.tool(
|
|
268
|
+
"property-data-prices-per-sqf",
|
|
269
|
+
"Asking £/sqft distribution for sale listings in a postcode — avg plus 70/80/90/100% percentile ranges, with the raw listings that have square-foot data. Use for the asking £/sqft KPI on a market report. Costs 1 credit per call.",
|
|
270
|
+
pricesPerSqfSchema,
|
|
271
|
+
endpointHandler(pricesPerSqf),
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
server.tool(
|
|
275
|
+
"property-data-sold-prices-per-sqf",
|
|
276
|
+
"Land Registry sold £/sqft distribution for a postcode — avg plus percentile ranges, up to 20 raw sales with square-foot data and a sales-window date range. Use for the achieved £/sqft KPI. Costs 1 credit per call.",
|
|
277
|
+
{
|
|
278
|
+
postcode: z.string().min(2).describe("UK postcode (outcode or full)"),
|
|
279
|
+
type: z
|
|
280
|
+
.enum(["detached", "semi-detached", "terraced", "flat"])
|
|
281
|
+
.optional()
|
|
282
|
+
.describe("Filter by property type"),
|
|
283
|
+
bedrooms: z.number().int().min(0).optional().describe("Filter by bedroom count"),
|
|
284
|
+
max_age: z.number().int().min(1).optional().describe("Max transaction age in months"),
|
|
285
|
+
},
|
|
286
|
+
endpointHandler(soldPricesPerSqf),
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
server.tool(
|
|
290
|
+
"property-data-growth-psf",
|
|
291
|
+
"7-year £/sqft series with year-on-year percent change for a postcode. Use for the £/sqft growth chart on a market report. Costs 1 credit per call.",
|
|
292
|
+
{
|
|
293
|
+
postcode: z.string().min(2).describe("UK postcode (outcode or full)"),
|
|
294
|
+
type: z
|
|
295
|
+
.enum(["detached", "semi-detached", "terraced", "flat"])
|
|
296
|
+
.optional()
|
|
297
|
+
.describe("Filter by property type"),
|
|
298
|
+
},
|
|
299
|
+
endpointHandler(growthPsf),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// ─────────────────────────────────────────────────────────────────
|
|
303
|
+
// Task 144 — Area risk and cost of occupancy
|
|
304
|
+
// ─────────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
server.tool(
|
|
307
|
+
"property-data-crime",
|
|
308
|
+
"Recent crime statistics for a postcode (or location / town) — per-1,000 rate, 12-month total, breakdown by crime type, and per-month observations. Use for the area-risk pillar. Costs 1 credit per call.",
|
|
309
|
+
{
|
|
310
|
+
postcode: z.string().min(2).optional().describe("UK postcode (outcode or full)"),
|
|
311
|
+
location: z
|
|
312
|
+
.string()
|
|
313
|
+
.optional()
|
|
314
|
+
.describe("Latitude,longitude pair (e.g. '51.5074,-0.1278'). Alternative to postcode."),
|
|
315
|
+
town: z.string().optional().describe("UK town name. Alternative to postcode."),
|
|
316
|
+
},
|
|
317
|
+
endpointHandler(crime),
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
server.tool(
|
|
321
|
+
"property-data-flood-risk",
|
|
322
|
+
"River and sea flood-risk rating for a full UK postcode. Use for area-risk reporting and lender-ready disclosures. Requires a full postcode (not outcode). Costs 1 credit per call.",
|
|
323
|
+
{
|
|
324
|
+
postcode: z.string().min(5).describe("Full UK postcode (not outcode)"),
|
|
325
|
+
},
|
|
326
|
+
endpointHandler(floodRisk),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
server.tool(
|
|
330
|
+
"property-data-council-tax",
|
|
331
|
+
"Council-tax authority, rating, and per-band charges (A–H) for a full UK postcode, plus any per-property bands the API returns. Use for cost-of-occupancy disclosures. Requires a full postcode. Costs 1 credit per call.",
|
|
332
|
+
{
|
|
333
|
+
postcode: z.string().min(5).describe("Full UK postcode (not outcode)"),
|
|
334
|
+
},
|
|
335
|
+
endpointHandler(councilTax),
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
// ─────────────────────────────────────────────────────────────────
|
|
339
|
+
// Task 144 — Planning precedent and address resolution
|
|
340
|
+
// ─────────────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
server.tool(
|
|
343
|
+
"property-data-planning-applications",
|
|
344
|
+
"Planning applications within a radius of a postcode — references, proposals, decisions, appeals, distance. Use for development precedent and pre-purchase risk. Costs 2 credits per call. Throttled to ~4 calls per 10 seconds upstream — wrap in the preval skill for fan-out, do not loop directly.",
|
|
345
|
+
{
|
|
346
|
+
postcode: z.string().min(2).describe("UK postcode (outcode or full)"),
|
|
347
|
+
decision_rating: z
|
|
348
|
+
.enum(["positive", "neutral", "negative"])
|
|
349
|
+
.optional()
|
|
350
|
+
.describe("Filter by decision sentiment"),
|
|
351
|
+
category: z.string().optional().describe("PropertyData planning category filter"),
|
|
352
|
+
max_age_decision: z
|
|
353
|
+
.number()
|
|
354
|
+
.int()
|
|
355
|
+
.min(1)
|
|
356
|
+
.optional()
|
|
357
|
+
.describe("Max age of decision in months"),
|
|
358
|
+
results: z.number().int().min(1).max(100).optional().describe("Max results to return"),
|
|
359
|
+
},
|
|
360
|
+
endpointHandler(planningApplications),
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
server.tool(
|
|
364
|
+
"property-data-address-match-uprn",
|
|
365
|
+
"Resolve a full UK address string to its UPRN, address classification, and lat/lng. Use for CRM hygiene and unique-property identification. Costs 10 credits per call — do not fan out blindly; call once per address you actually need to resolve.",
|
|
366
|
+
{
|
|
367
|
+
address: z.string().min(5).describe("Full UK address (line one + postcode)"),
|
|
368
|
+
},
|
|
369
|
+
endpointHandler(addressMatchUprn),
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// ─────────────────────────────────────────────────────────────────
|
|
373
|
+
// Task 144 — Rental demand and property-type mix
|
|
374
|
+
// ─────────────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
server.tool(
|
|
377
|
+
"property-data-demand-rent",
|
|
378
|
+
"Rental demand signals for a postcode — for-rent count, lets per month, turnover percent, months of inventory, days on market, and a rating. Use for the rental pillar on a market report. Costs 1 credit per call.",
|
|
379
|
+
{
|
|
380
|
+
postcode: z.string().min(2).describe("UK postcode (outcode or full)"),
|
|
381
|
+
bedrooms: z.number().int().min(0).optional().describe("Filter by bedroom count"),
|
|
382
|
+
type: z
|
|
383
|
+
.enum(["detached", "semi-detached", "terraced", "flat"])
|
|
384
|
+
.optional()
|
|
385
|
+
.describe("Filter by property type"),
|
|
386
|
+
},
|
|
387
|
+
endpointHandler(demandRent),
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
server.tool(
|
|
391
|
+
"property-data-property-types",
|
|
392
|
+
"Output-area property-type distribution (percent by class — detached, semi-detached, terraced, flat) for a postcode. Use for the property-type mix doughnut on a market report. Costs 1 credit per call.",
|
|
393
|
+
{
|
|
394
|
+
postcode: z.string().min(2).describe("UK postcode (outcode or full)"),
|
|
395
|
+
},
|
|
396
|
+
endpointHandler(propertyTypes),
|
|
397
|
+
);
|
|
398
|
+
|
|
239
399
|
// ─────────────────────────────────────────────────────────────────
|
|
240
400
|
// Lifecycle
|
|
241
401
|
// ─────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { randomBytes, createCipheriv, createDecipheriv } from "node:crypto";
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
// Standalone-mode crypto. Same AES-256-GCM envelope as crypto.ts, but the
|
|
5
|
+
// encryption-key file path is passed in (not derived from PLATFORM_ROOT /
|
|
6
|
+
// brand.json), so it can live under ~/.claude/ when the MCP is spawned by
|
|
7
|
+
// Claude Code natively.
|
|
8
|
+
|
|
9
|
+
const ALGORITHM = "aes-256-gcm";
|
|
10
|
+
const KEY_LENGTH = 32;
|
|
11
|
+
const IV_LENGTH = 12;
|
|
12
|
+
const AUTH_TAG_LENGTH = 16;
|
|
13
|
+
|
|
14
|
+
function loadOrGenerateKey(keyFilePath: string, label: string): Buffer {
|
|
15
|
+
if (existsSync(keyFilePath)) {
|
|
16
|
+
const hex = readFileSync(keyFilePath, "utf-8").trim();
|
|
17
|
+
const buf = Buffer.from(hex, "hex");
|
|
18
|
+
if (buf.length !== KEY_LENGTH) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`[${label}] encryption key at ${keyFilePath} is ${buf.length} bytes, expected ${KEY_LENGTH}.`,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return buf;
|
|
24
|
+
}
|
|
25
|
+
const key = randomBytes(KEY_LENGTH);
|
|
26
|
+
writeFileSync(keyFilePath, key.toString("hex"), { mode: 0o600 });
|
|
27
|
+
console.error(`[${label}] encryption key generated at ${keyFilePath}`);
|
|
28
|
+
return key;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function encryptWith(plaintext: string, keyFilePath: string, label: string): string {
|
|
32
|
+
const key = loadOrGenerateKey(keyFilePath, label);
|
|
33
|
+
const iv = randomBytes(IV_LENGTH);
|
|
34
|
+
const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
35
|
+
let encrypted = cipher.update(plaintext, "utf-8", "hex");
|
|
36
|
+
encrypted += cipher.final("hex");
|
|
37
|
+
const authTag = cipher.getAuthTag().toString("hex");
|
|
38
|
+
return `${iv.toString("hex")}:${encrypted}:${authTag}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function decryptWith(encryptedValue: string, keyFilePath: string, label: string): string {
|
|
42
|
+
const parts = encryptedValue.split(":");
|
|
43
|
+
if (parts.length !== 3) {
|
|
44
|
+
throw new Error(`[${label}] bad encrypted envelope (expected iv:ct:tag)`);
|
|
45
|
+
}
|
|
46
|
+
const key = loadOrGenerateKey(keyFilePath, label);
|
|
47
|
+
const iv = Buffer.from(parts[0], "hex");
|
|
48
|
+
const ciphertext = parts[1];
|
|
49
|
+
const authTag = Buffer.from(parts[2], "hex");
|
|
50
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
51
|
+
decipher.setAuthTag(authTag);
|
|
52
|
+
let plaintext = decipher.update(ciphertext, "hex", "utf-8");
|
|
53
|
+
plaintext += decipher.final("utf-8");
|
|
54
|
+
return plaintext;
|
|
55
|
+
}
|