@metamynd/agentsafe-guard 0.3.0 → 0.4.0

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/cli.mjs ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ // cli.mjs — the package's only executable. Its whole job is to make the offline
3
+ // demo reachable without cloning anything:
4
+ //
5
+ // npx @metamynd/agentsafe-guard demo
6
+ //
7
+ // Kept deliberately thin: no argument parser, no dependencies, no network.
8
+ const [, , cmd] = process.argv;
9
+
10
+ if (cmd === 'demo') {
11
+ await import('./demo.mjs');
12
+ } else if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') {
13
+ console.log(`
14
+ @metamynd/agentsafe-guard — runtime governance for Node AI agents
15
+
16
+ Usage
17
+ npx @metamynd/agentsafe-guard demo Run the offline policy demo
18
+ (no account, no API key, no network)
19
+
20
+ In your agent
21
+ import { createGuard } from '@metamynd/agentsafe-guard';
22
+
23
+ Docs https://www.npmjs.com/package/@metamynd/agentsafe-guard
24
+ Hosted https://metamynd.ai
25
+ `);
26
+ } else {
27
+ console.error(`Unknown command: ${cmd}\nTry: npx @metamynd/agentsafe-guard demo`);
28
+ process.exit(1);
29
+ }
package/demo.mjs ADDED
@@ -0,0 +1,161 @@
1
+ // demo.mjs — the no-account, no-network tour of the guard.
2
+ //
3
+ // npx @metamynd/agentsafe-guard demo
4
+ //
5
+ // Everything here runs locally: an ephemeral Ed25519 key, an inline policy bundle,
6
+ // and `policy-core` — the same evaluator bytes the hosted gate runs. No MetaMynd
7
+ // account, no API key, no Hedera, no outbound request (the `api` URL below is
8
+ // deliberately unroutable, and nothing ever calls it).
9
+ //
10
+ // Exits 0 when every verdict matches what the policy says it should be, 1 otherwise —
11
+ // so this doubles as a smoke test of the shipped package.
12
+ import crypto from 'node:crypto';
13
+ import { createGuard } from './agentsafe-guard.mjs';
14
+
15
+ const c = (code, s) => (process.stdout.isTTY ? `\u001b[${code}m${s}\u001b[0m` : s);
16
+ const bold = (s) => c('1', s);
17
+ const dim = (s) => c('2', s);
18
+ const head = (s) => console.log(`\n${bold(s)}\n${dim('─'.repeat(s.length))}`);
19
+
20
+ // Colour per decision — but never colour ALONE: the verdict is always spelled out.
21
+ const paint = { allow: '32', observe: '36', escalate: '33', block: '31', quarantine: '35' };
22
+ const verdict = (d) => c(paint[d] ?? '0', d.toUpperCase().padEnd(10));
23
+
24
+ const { privateKey } = crypto.generateKeyPairSync('ed25519');
25
+ const guard = createGuard({
26
+ api: 'http://unused.local/api/v1', // never contacted — this demo is entirely local
27
+ agentDid: 'did:hedera:testnet:z6MkDemo_0.0.1',
28
+ agentKey: privateKey.export({ format: 'der', type: 'pkcs8' }).toString('hex'),
29
+ });
30
+
31
+ // The policy an owner would author in the dashboard, as the agent receives it:
32
+ // one enforced Standard, one SOP, and an ODRL mandate.
33
+ const bundle = {
34
+ standards: [
35
+ {
36
+ standardKey: 'eu-ai-act',
37
+ document: {
38
+ molecules: [
39
+ {
40
+ id: 'risk',
41
+ combinator: 'any',
42
+ atoms: [{ id: 'r', predicate: 'risk-at-or-above', config: { level: 'high' } }],
43
+ decision: 'escalate',
44
+ reasonCode: 'RISK_REVIEW',
45
+ },
46
+ ],
47
+ },
48
+ },
49
+ ],
50
+ sops: [
51
+ {
52
+ standardKey: 'sop:travel',
53
+ document: {
54
+ molecules: [
55
+ {
56
+ id: 'watch',
57
+ combinator: 'any',
58
+ atoms: [{ id: 'w', predicate: 'amount-over', config: { limit: 200 } }],
59
+ decision: 'observe',
60
+ reasonCode: 'WATCH_LARGE',
61
+ },
62
+ {
63
+ id: 'cap',
64
+ combinator: 'any',
65
+ atoms: [{ id: 'a', predicate: 'amount-over', config: { limit: 500 } }],
66
+ decision: 'block',
67
+ reasonCode: 'SOP_SPEND_CAP',
68
+ },
69
+ {
70
+ id: 'contain',
71
+ combinator: 'any',
72
+ atoms: [{ id: 'q', predicate: 'amount-over', config: { limit: 5000 } }],
73
+ decision: 'quarantine',
74
+ reasonCode: 'GROSS_OVERSPEND',
75
+ },
76
+ ],
77
+ },
78
+ },
79
+ ],
80
+ mandate: {
81
+ permission: [
82
+ {
83
+ target: 'flight-purchase',
84
+ constraint: [
85
+ { leftOperand: 'mm:payAmount', operator: 'lteq', rightOperand: 1000 },
86
+ { leftOperand: 'mm:cumulativeSpend', operator: 'lteq', rightOperand: 1000 },
87
+ { leftOperand: 'mm:merchant', operator: 'isAnyOf', rightOperand: ['amadeus'] },
88
+ ],
89
+ },
90
+ ],
91
+ },
92
+ };
93
+
94
+ let failed = 0;
95
+ const check = (ok) => { if (!ok) failed++; return ok ? dim('ok') : c('31', 'FAIL'); };
96
+
97
+ const run = (label, request, expect, extra = {}) => {
98
+ const v = guard.evaluateLocally({ ...bundle, ...extra, request });
99
+ const ok = v.decision === expect[0] && v.reasonCode === expect[1];
100
+ console.log(` ${verdict(v.decision)} ${String(v.reasonCode).padEnd(22)} ${label} ${check(ok)}`);
101
+ };
102
+
103
+ console.log(bold('\nAgentSafe Guard — local policy evaluation'));
104
+ console.log(dim('No account, no API key, no network. Same policy-core the hosted gate runs.'));
105
+
106
+ head('The policy this agent is under');
107
+ console.log(` Standard ${dim('eu-ai-act')} risk ≥ high → escalate`);
108
+ console.log(` SOP ${dim('sop:travel')} amount > 200 → observe ${dim('(permit, but flag)')}`);
109
+ console.log(` amount > 500 → block`);
110
+ console.log(` amount > 5000 → quarantine ${dim('(contain the agent)')}`);
111
+ console.log(` Mandate ${dim('ODRL')} merchant ∈ [amadeus], ≤ 1000 per action and in total`);
112
+
113
+ head('A tool call, evaluated against it');
114
+ run('$100 to amadeus, low risk', { action: 'flight-purchase', amount: 100, merchant: 'amadeus', context: { riskLevel: 'low' } }, ['allow', 'AUTHORIZED']);
115
+ run('$300 — over the watch line', { action: 'flight-purchase', amount: 300, merchant: 'amadeus', context: { riskLevel: 'low' } }, ['observe', 'WATCH_LARGE']);
116
+ run('$600 — over the SOP cap', { action: 'flight-purchase', amount: 600, merchant: 'amadeus', context: { riskLevel: 'low' } }, ['block', 'SOP_SPEND_CAP']);
117
+ run('$100 but flagged high risk', { action: 'flight-purchase', amount: 100, merchant: 'amadeus', context: { riskLevel: 'high' } }, ['escalate', 'RISK_REVIEW']);
118
+ run('$100 to a merchant off the mandate', { action: 'flight-purchase', amount: 100, merchant: 'sabre', context: { riskLevel: 'low' } }, ['block', 'MERCHANT_NOT_ALLOWED']);
119
+ run('$6000 — gross overspend', { action: 'flight-purchase', amount: 6000, merchant: 'amadeus', context: { riskLevel: 'low' } }, ['quarantine', 'GROSS_OVERSPEND']);
120
+
121
+ head('The agent cannot argue its way out');
122
+ console.log(dim(' Unsigned context is untrusted input — it never shadows the signed amount.'));
123
+ run('$600 claiming "payAmount: 1"', { action: 'flight-purchase', amount: 600, merchant: 'amadeus', context: { 'mm:payAmount': 1, riskLevel: 'low' } }, ['block', 'SOP_SPEND_CAP']);
124
+ console.log(dim(' A quarantined agent is refused at the edge, before any rule is read.'));
125
+ run('$1 while quarantined', { action: 'flight-purchase', amount: 1, merchant: 'amadeus', context: { riskLevel: 'low' } }, ['quarantine', 'AGENT_QUARANTINED'], { contained: { status: 'quarantined', reason: 'GROSS_OVERSPEND' } });
126
+
127
+ head('Operating mode narrows the ladder further');
128
+ const mode = (m) => ({ operatingMode: { mode: m } });
129
+ run('read_only — value action refused', { action: 'flight-purchase', amount: 100, merchant: 'amadeus', context: { riskLevel: 'low' } }, ['block', 'MODE_READ_ONLY'], mode('read_only'));
130
+ run('read_only — a $0 read still passes', { action: 'flight-purchase', amount: 0, merchant: 'amadeus', context: { riskLevel: 'low' } }, ['allow', 'AUTHORIZED'], mode('read_only'));
131
+ run('restricted — spend needs a human', { action: 'flight-purchase', amount: 100, merchant: 'amadeus', context: { riskLevel: 'low' } }, ['escalate', 'MODE_RESTRICTED_REVIEW'], mode('restricted'));
132
+ run('restricted — but a block stays a block', { action: 'flight-purchase', amount: 600, merchant: 'amadeus', context: { riskLevel: 'low' } }, ['block', 'SOP_SPEND_CAP'], mode('restricted'));
133
+
134
+ head('What that means for your tool');
135
+ {
136
+ let ran = false;
137
+ const book = guard.guardToolLocal('flight-purchase', async () => { ran = true; return 'booked'; }, (a) => a, bundle);
138
+ const out = await book({ amount: 300, merchant: 'amadeus', context: { riskLevel: 'low' } });
139
+ console.log(` observe → handler RAN, returned ${JSON.stringify(out)} ${check(ran === true && out === 'booked')}`);
140
+ }
141
+ {
142
+ let ran = false;
143
+ const book = guard.guardToolLocal('flight-purchase', async () => { ran = true; return 'booked'; }, (a) => a, bundle);
144
+ let name = null;
145
+ try { await book({ amount: 600, merchant: 'amadeus', context: { riskLevel: 'low' } }); } catch (e) { name = e?.name; }
146
+ console.log(` block → threw ${name}, handler never ran ${check(name === 'GovernanceBlocked' && ran === false)}`);
147
+ }
148
+
149
+ if (failed) {
150
+ console.error(c('31', `\n${failed} case(s) FAILED — that is a bug, please report it.`));
151
+ console.error(dim('Please report it — https://metamynd.ai\n'));
152
+ process.exit(1);
153
+ }
154
+
155
+ console.log(bold('\nPASS') + dim(' — every verdict matched the policy, decided locally in-process.\n'));
156
+ console.log('Wire it into your own agent:');
157
+ console.log(dim(" import { createGuard } from '@metamynd/agentsafe-guard';"));
158
+ console.log(dim(' const safeBooking = guard.guardToolLocal(\'flight-purchase\', bookFlight, mapArgs, bundle);'));
159
+ console.log(`\n${dim('Local evaluation needs nothing from us. The hosted platform adds live policy')}`);
160
+ console.log(`${dim('editing, cumulative spend caps, human escalation, and anchored evidence:')}`);
161
+ console.log(`${dim('https://metamynd.ai')}\n`);
@@ -1,47 +1,47 @@
1
- // example-openclaw-agent.mjs — a runnable demo of the guard against a seeded bound agent.
2
- //
3
- // Simulates how an OpenClaw agent's TOOL is wrapped: the raw tool only runs if the
4
- // AgentSafe gate returns `allow`. Run it after seeding an agent with
5
- // backend/scripts/demo-seed-governance.ts:
6
- //
7
- // $env:AGENTSAFE_API="http://localhost:9926/api/v1"
8
- // $env:AGENT_DID="did:hedera:testnet:..."
9
- // $env:AGENT_KEY="302e0201..."
10
- // node example-openclaw-agent.mjs
11
- import { createGuard } from './agentsafe-guard.mjs';
12
-
13
- const guard = createGuard({
14
- api: process.env.AGENTSAFE_API ?? 'http://localhost:9926/api/v1',
15
- agentDid: process.env.AGENT_DID,
16
- agentKey: process.env.AGENT_KEY,
17
- });
18
-
19
- // --- The agent's tool. In OpenClaw you register this handler for the tool; here we
20
- // wrap it with guard.guardTool so every call is gated first. ---
21
- const bookFlight = guard.guardTool(
22
- 'flight-purchase', // the governed action (matches the mandate scope)
23
- async (args, decision) => {
24
- // Only reached when the gate ALLOWED. Real booking would go here.
25
- return { booked: true, pnr: 'PNR-DEMO', remaining: decision.remaining, ...args };
26
- },
27
- // Map the tool args → gate inputs. `context` carries what the Standard/SOP rules need.
28
- (a) => ({ amount: a.amount, currency: 'USD', merchant: a.merchant, context: { tool: a.tool ?? 'book-flight', riskLevel: a.riskLevel ?? 'low' } }),
29
- );
30
-
31
- async function ask(label, args) {
32
- try {
33
- const r = await bookFlight(args);
34
- console.log(` \x1b[32m✅ ALLOW\x1b[0m ${label.padEnd(30)} → booked ${r.pnr} (remaining $${r.remaining})`);
35
- } catch (e) {
36
- const g = e.governance ?? {};
37
- const tag = g.decision === 'escalate' ? '\x1b[33m⚠ ESCALATE\x1b[0m' : '\x1b[31m⛔ BLOCK\x1b[0m';
38
- console.log(` ${tag} ${label.padEnd(30)} → ${g.reasonCode ?? e.message}`);
39
- }
40
- }
41
-
42
- console.log(`\n OpenClaw agent — every booking passes through the AgentSafe gate\n ${'─'.repeat(60)}`);
43
- await ask('$150 book-flight, low risk', { amount: 150, merchant: 'skyward-air', tool: 'book-flight', riskLevel: 'low' });
44
- await ask('$600 book-flight', { amount: 600, merchant: 'skyward-air', tool: 'book-flight', riskLevel: 'low' });
45
- await ask('$100 wire-transfer tool', { amount: 100, merchant: 'skyward-air', tool: 'wire-transfer', riskLevel: 'low' });
46
- await ask('$100 high-risk decision', { amount: 100, merchant: 'skyward-air', tool: 'book-flight', riskLevel: 'high' });
47
- console.log(`\n The agent refuses blocked/escalated actions itself — governance decided, not the LLM.\n`);
1
+ // example-openclaw-agent.mjs — a runnable demo of the guard against a seeded bound agent.
2
+ //
3
+ // Simulates how an OpenClaw agent's TOOL is wrapped: the raw tool only runs if the
4
+ // AgentSafe gate returns `allow`. Run it after seeding an agent with
5
+ // backend/scripts/demo-seed-governance.ts:
6
+ //
7
+ // $env:AGENTSAFE_API="http://localhost:9926/api/v1"
8
+ // $env:AGENT_DID="did:hedera:testnet:..."
9
+ // $env:AGENT_KEY="302e0201..."
10
+ // node example-openclaw-agent.mjs
11
+ import { createGuard } from './agentsafe-guard.mjs';
12
+
13
+ const guard = createGuard({
14
+ api: process.env.AGENTSAFE_API ?? 'http://localhost:9926/api/v1',
15
+ agentDid: process.env.AGENT_DID,
16
+ agentKey: process.env.AGENT_KEY,
17
+ });
18
+
19
+ // --- The agent's tool. In OpenClaw you register this handler for the tool; here we
20
+ // wrap it with guard.guardTool so every call is gated first. ---
21
+ const bookFlight = guard.guardTool(
22
+ 'flight-purchase', // the governed action (matches the mandate scope)
23
+ async (args, decision) => {
24
+ // Only reached when the gate ALLOWED. Real booking would go here.
25
+ return { booked: true, pnr: 'PNR-DEMO', remaining: decision.remaining, ...args };
26
+ },
27
+ // Map the tool args → gate inputs. `context` carries what the Standard/SOP rules need.
28
+ (a) => ({ amount: a.amount, currency: 'USD', merchant: a.merchant, context: { tool: a.tool ?? 'book-flight', riskLevel: a.riskLevel ?? 'low' } }),
29
+ );
30
+
31
+ async function ask(label, args) {
32
+ try {
33
+ const r = await bookFlight(args);
34
+ console.log(` \x1b[32m✅ ALLOW\x1b[0m ${label.padEnd(30)} → booked ${r.pnr} (remaining $${r.remaining})`);
35
+ } catch (e) {
36
+ const g = e.governance ?? {};
37
+ const tag = g.decision === 'escalate' ? '\x1b[33m⚠ ESCALATE\x1b[0m' : '\x1b[31m⛔ BLOCK\x1b[0m';
38
+ console.log(` ${tag} ${label.padEnd(30)} → ${g.reasonCode ?? e.message}`);
39
+ }
40
+ }
41
+
42
+ console.log(`\n OpenClaw agent — every booking passes through the AgentSafe gate\n ${'─'.repeat(60)}`);
43
+ await ask('$150 book-flight, low risk', { amount: 150, merchant: 'skyward-air', tool: 'book-flight', riskLevel: 'low' });
44
+ await ask('$600 book-flight', { amount: 600, merchant: 'skyward-air', tool: 'book-flight', riskLevel: 'low' });
45
+ await ask('$100 wire-transfer tool', { amount: 100, merchant: 'skyward-air', tool: 'wire-transfer', riskLevel: 'low' });
46
+ await ask('$100 high-risk decision', { amount: 100, merchant: 'skyward-air', tool: 'book-flight', riskLevel: 'high' });
47
+ console.log(`\n The agent refuses blocked/escalated actions itself — governance decided, not the LLM.\n`);
package/magp-did.mjs CHANGED
@@ -1,157 +1,157 @@
1
- // GENERATED from backend/src/features/magp/did.ts — do not edit. Regenerate: npm run build:guard-core
2
-
3
- // src/features/magp/did.ts
4
- import crypto from "node:crypto";
5
-
6
- // src/features/agent-identity/did.util.ts
7
- var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
8
- function base58(bytes) {
9
- let zeros = 0;
10
- while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
11
- const digits = [];
12
- for (let i = zeros; i < bytes.length; i++) {
13
- let carry = bytes[i];
14
- for (let j = 0; j < digits.length; j++) {
15
- carry += digits[j] << 8;
16
- digits[j] = carry % 58;
17
- carry = carry / 58 | 0;
18
- }
19
- while (carry > 0) {
20
- digits.push(carry % 58);
21
- carry = carry / 58 | 0;
22
- }
23
- }
24
- let out = "";
25
- for (let k = 0; k < zeros; k++) out += BASE58_ALPHABET[0];
26
- for (let q = digits.length - 1; q >= 0; q--) out += BASE58_ALPHABET[digits[q]];
27
- return out;
28
- }
29
- function multibaseBase58btc(bytes) {
30
- return "z" + base58(bytes);
31
- }
32
- function buildHederaDid(network, publicKeyBytes, topicId) {
33
- return `did:hedera:${network}:${multibaseBase58btc(publicKeyBytes)}_${topicId}`;
34
- }
35
- function base58Decode(str) {
36
- const bytes = [0];
37
- for (const ch of str) {
38
- const value = BASE58_ALPHABET.indexOf(ch);
39
- if (value === -1) throw new Error(`invalid base58 character '${ch}'`);
40
- let carry = value;
41
- for (let j = 0; j < bytes.length; j++) {
42
- carry += bytes[j] * 58;
43
- bytes[j] = carry & 255;
44
- carry >>= 8;
45
- }
46
- while (carry > 0) {
47
- bytes.push(carry & 255);
48
- carry >>= 8;
49
- }
50
- }
51
- let zeros = 0;
52
- for (let k = 0; k < str.length && str[k] === BASE58_ALPHABET[0]; k++) zeros++;
53
- const out = new Uint8Array(zeros + bytes.length);
54
- for (let i = 0; i < bytes.length; i++) out[zeros + i] = bytes[bytes.length - 1 - i];
55
- return out;
56
- }
57
- function parseHederaDid(did) {
58
- const m = /^did:hedera:(mainnet|testnet|previewnet|devnet):(z[1-9A-HJ-NP-Za-km-z]+)_(\d+\.\d+\.\d+)$/.exec(did ?? "");
59
- if (!m) return null;
60
- const [, network, publicKeyMultibase, topicId] = m;
61
- let publicKeyBytes;
62
- try {
63
- publicKeyBytes = base58Decode(publicKeyMultibase.slice(1));
64
- } catch {
65
- return null;
66
- }
67
- if (publicKeyBytes.length !== 32) return null;
68
- return { network, publicKeyMultibase, publicKeyBytes, topicId };
69
- }
70
- var ED25519_MULTICODEC = Uint8Array.of(237, 1);
71
- function buildDidKey(publicKeyBytes) {
72
- const prefixed = new Uint8Array(ED25519_MULTICODEC.length + publicKeyBytes.length);
73
- prefixed.set(ED25519_MULTICODEC, 0);
74
- prefixed.set(publicKeyBytes, ED25519_MULTICODEC.length);
75
- return `did:key:${multibaseBase58btc(prefixed)}`;
76
- }
77
- function parseDidKey(did) {
78
- const m = /^did:key:(z[1-9A-HJ-NP-Za-km-z]+)$/.exec(did ?? "");
79
- if (!m) return null;
80
- const multibase = m[1];
81
- let decoded;
82
- try {
83
- decoded = base58Decode(multibase.slice(1));
84
- } catch {
85
- return null;
86
- }
87
- if (decoded.length !== ED25519_MULTICODEC.length + 32) return null;
88
- if (decoded[0] !== ED25519_MULTICODEC[0] || decoded[1] !== ED25519_MULTICODEC[1]) return null;
89
- return { method: "key", publicKeyMultibase: multibase, publicKeyBytes: decoded.slice(ED25519_MULTICODEC.length) };
90
- }
91
- function didPublicKeyBytes(did) {
92
- return parseHederaDid(did)?.publicKeyBytes ?? parseDidKey(did)?.publicKeyBytes ?? null;
93
- }
94
-
95
- // src/features/magp/did.ts
96
- var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
97
- function ed25519KeyFromRaw(raw) {
98
- const der = Buffer.concat([ED25519_SPKI_PREFIX, Buffer.from(raw)]);
99
- return crypto.createPublicKey({ key: der, format: "der", type: "spki" });
100
- }
101
- function verifyDidSignature(did, message, signatureHex) {
102
- const publicKeyBytes = didPublicKeyBytes(did);
103
- if (!publicKeyBytes) return false;
104
- try {
105
- const key = ed25519KeyFromRaw(publicKeyBytes);
106
- return crypto.verify(null, Buffer.from(message, "utf8"), key, Buffer.from(signatureHex, "hex"));
107
- } catch {
108
- return false;
109
- }
110
- }
111
- function buildDidDocument(did, service) {
112
- const hedera = parseHederaDid(did);
113
- const key = hedera ? null : parseDidKey(did);
114
- if (!hedera && !key) return null;
115
- const publicKeyMultibase = hedera ? hedera.publicKeyMultibase : key.publicKeyMultibase;
116
- const fragment = hedera ? "#did-root-key" : `#${key.publicKeyMultibase}`;
117
- const vmId = `${did}${fragment}`;
118
- const doc = {
119
- "@context": ["https://www.w3.org/ns/did/v1"],
120
- id: did,
121
- controller: did,
122
- verificationMethod: [
123
- {
124
- id: vmId,
125
- type: "Ed25519VerificationKey2020",
126
- controller: did,
127
- publicKeyMultibase
128
- }
129
- ],
130
- authentication: [vmId]
131
- };
132
- if (service) {
133
- doc.service = [
134
- {
135
- id: `${did}#magp`,
136
- type: "MAGPEndpoint",
137
- serviceEndpoint: service.serviceEndpoint,
138
- channels: service.channels,
139
- protoVersions: service.protoVersions
140
- }
141
- ];
142
- }
143
- return doc;
144
- }
145
- export {
146
- base58,
147
- base58Decode,
148
- buildDidDocument,
149
- buildDidKey,
150
- buildHederaDid,
151
- didPublicKeyBytes,
152
- ed25519KeyFromRaw,
153
- multibaseBase58btc,
154
- parseDidKey,
155
- parseHederaDid,
156
- verifyDidSignature
157
- };
1
+ // GENERATED from backend/src/features/magp/did.ts — do not edit. Regenerate: npm run build:guard-core
2
+
3
+ // src/features/magp/did.ts
4
+ import crypto from "node:crypto";
5
+
6
+ // src/features/agent-identity/did.util.ts
7
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
8
+ function base58(bytes) {
9
+ let zeros = 0;
10
+ while (zeros < bytes.length && bytes[zeros] === 0) zeros++;
11
+ const digits = [];
12
+ for (let i = zeros; i < bytes.length; i++) {
13
+ let carry = bytes[i];
14
+ for (let j = 0; j < digits.length; j++) {
15
+ carry += digits[j] << 8;
16
+ digits[j] = carry % 58;
17
+ carry = carry / 58 | 0;
18
+ }
19
+ while (carry > 0) {
20
+ digits.push(carry % 58);
21
+ carry = carry / 58 | 0;
22
+ }
23
+ }
24
+ let out = "";
25
+ for (let k = 0; k < zeros; k++) out += BASE58_ALPHABET[0];
26
+ for (let q = digits.length - 1; q >= 0; q--) out += BASE58_ALPHABET[digits[q]];
27
+ return out;
28
+ }
29
+ function multibaseBase58btc(bytes) {
30
+ return "z" + base58(bytes);
31
+ }
32
+ function buildHederaDid(network, publicKeyBytes, topicId) {
33
+ return `did:hedera:${network}:${multibaseBase58btc(publicKeyBytes)}_${topicId}`;
34
+ }
35
+ function base58Decode(str) {
36
+ const bytes = [0];
37
+ for (const ch of str) {
38
+ const value = BASE58_ALPHABET.indexOf(ch);
39
+ if (value === -1) throw new Error(`invalid base58 character '${ch}'`);
40
+ let carry = value;
41
+ for (let j = 0; j < bytes.length; j++) {
42
+ carry += bytes[j] * 58;
43
+ bytes[j] = carry & 255;
44
+ carry >>= 8;
45
+ }
46
+ while (carry > 0) {
47
+ bytes.push(carry & 255);
48
+ carry >>= 8;
49
+ }
50
+ }
51
+ let zeros = 0;
52
+ for (let k = 0; k < str.length && str[k] === BASE58_ALPHABET[0]; k++) zeros++;
53
+ const out = new Uint8Array(zeros + bytes.length);
54
+ for (let i = 0; i < bytes.length; i++) out[zeros + i] = bytes[bytes.length - 1 - i];
55
+ return out;
56
+ }
57
+ function parseHederaDid(did) {
58
+ const m = /^did:hedera:(mainnet|testnet|previewnet|devnet):(z[1-9A-HJ-NP-Za-km-z]+)_(\d+\.\d+\.\d+)$/.exec(did ?? "");
59
+ if (!m) return null;
60
+ const [, network, publicKeyMultibase, topicId] = m;
61
+ let publicKeyBytes;
62
+ try {
63
+ publicKeyBytes = base58Decode(publicKeyMultibase.slice(1));
64
+ } catch {
65
+ return null;
66
+ }
67
+ if (publicKeyBytes.length !== 32) return null;
68
+ return { network, publicKeyMultibase, publicKeyBytes, topicId };
69
+ }
70
+ var ED25519_MULTICODEC = Uint8Array.of(237, 1);
71
+ function buildDidKey(publicKeyBytes) {
72
+ const prefixed = new Uint8Array(ED25519_MULTICODEC.length + publicKeyBytes.length);
73
+ prefixed.set(ED25519_MULTICODEC, 0);
74
+ prefixed.set(publicKeyBytes, ED25519_MULTICODEC.length);
75
+ return `did:key:${multibaseBase58btc(prefixed)}`;
76
+ }
77
+ function parseDidKey(did) {
78
+ const m = /^did:key:(z[1-9A-HJ-NP-Za-km-z]+)$/.exec(did ?? "");
79
+ if (!m) return null;
80
+ const multibase = m[1];
81
+ let decoded;
82
+ try {
83
+ decoded = base58Decode(multibase.slice(1));
84
+ } catch {
85
+ return null;
86
+ }
87
+ if (decoded.length !== ED25519_MULTICODEC.length + 32) return null;
88
+ if (decoded[0] !== ED25519_MULTICODEC[0] || decoded[1] !== ED25519_MULTICODEC[1]) return null;
89
+ return { method: "key", publicKeyMultibase: multibase, publicKeyBytes: decoded.slice(ED25519_MULTICODEC.length) };
90
+ }
91
+ function didPublicKeyBytes(did) {
92
+ return parseHederaDid(did)?.publicKeyBytes ?? parseDidKey(did)?.publicKeyBytes ?? null;
93
+ }
94
+
95
+ // src/features/magp/did.ts
96
+ var ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
97
+ function ed25519KeyFromRaw(raw) {
98
+ const der = Buffer.concat([ED25519_SPKI_PREFIX, Buffer.from(raw)]);
99
+ return crypto.createPublicKey({ key: der, format: "der", type: "spki" });
100
+ }
101
+ function verifyDidSignature(did, message, signatureHex) {
102
+ const publicKeyBytes = didPublicKeyBytes(did);
103
+ if (!publicKeyBytes) return false;
104
+ try {
105
+ const key = ed25519KeyFromRaw(publicKeyBytes);
106
+ return crypto.verify(null, Buffer.from(message, "utf8"), key, Buffer.from(signatureHex, "hex"));
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+ function buildDidDocument(did, service) {
112
+ const hedera = parseHederaDid(did);
113
+ const key = hedera ? null : parseDidKey(did);
114
+ if (!hedera && !key) return null;
115
+ const publicKeyMultibase = hedera ? hedera.publicKeyMultibase : key.publicKeyMultibase;
116
+ const fragment = hedera ? "#did-root-key" : `#${key.publicKeyMultibase}`;
117
+ const vmId = `${did}${fragment}`;
118
+ const doc = {
119
+ "@context": ["https://www.w3.org/ns/did/v1"],
120
+ id: did,
121
+ controller: did,
122
+ verificationMethod: [
123
+ {
124
+ id: vmId,
125
+ type: "Ed25519VerificationKey2020",
126
+ controller: did,
127
+ publicKeyMultibase
128
+ }
129
+ ],
130
+ authentication: [vmId]
131
+ };
132
+ if (service) {
133
+ doc.service = [
134
+ {
135
+ id: `${did}#magp`,
136
+ type: "MAGPEndpoint",
137
+ serviceEndpoint: service.serviceEndpoint,
138
+ channels: service.channels,
139
+ protoVersions: service.protoVersions
140
+ }
141
+ ];
142
+ }
143
+ return doc;
144
+ }
145
+ export {
146
+ base58,
147
+ base58Decode,
148
+ buildDidDocument,
149
+ buildDidKey,
150
+ buildHederaDid,
151
+ didPublicKeyBytes,
152
+ ed25519KeyFromRaw,
153
+ multibaseBase58btc,
154
+ parseDidKey,
155
+ parseHederaDid,
156
+ verifyDidSignature
157
+ };