@atbash/sdk 0.3.6 → 0.3.7

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/dist/index.cjs CHANGED
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  DEFAULT_CHROMIA_NODE_URLS: () => DEFAULT_CHROMIA_NODE_URLS,
35
35
  DEFAULT_ENDPOINT: () => DEFAULT_ENDPOINT,
36
36
  checkAgentExists: () => checkAgentExists,
37
+ createAtbashClient: () => createAtbashClient,
37
38
  derivePublicKey: () => derivePublicKey,
38
39
  generateKeyPair: () => generateKeyPair,
39
40
  getAgentDetail: () => getAgentDetail,
@@ -51,15 +52,47 @@ __export(index_exports, {
51
52
  isValidPrivateKey: () => isValidPrivateKey,
52
53
  judgeAction: () => judgeAction,
53
54
  loadAgent: () => loadAgent,
55
+ loadAgentFromFile: () => loadAgentFromFile,
54
56
  logToolCall: () => logToolCall,
55
- toPubkeyHex: () => toPubkeyHex
57
+ resolveKeyPath: () => resolveKeyPath,
58
+ toPubkeyHex: () => toPubkeyHex,
59
+ validateJudgeEndpoint: () => validateJudgeEndpoint,
60
+ verifyJudgeResponseSignature: () => verifyJudgeResponseSignature
56
61
  });
57
62
  module.exports = __toCommonJS(index_exports);
58
63
 
59
64
  // src/client.ts
60
65
  var import_crypto = require("crypto");
66
+ var import_postchain_client2 = __toESM(require("postchain-client"), 1);
67
+
68
+ // src/signature.ts
61
69
  var import_postchain_client = __toESM(require("postchain-client"), 1);
62
- var { createClient, encryption, newSignatureProvider } = import_postchain_client.default;
70
+ var { encryption } = import_postchain_client.default;
71
+ function verifyJudgeResponseSignature(bodyBytes, signatureHex, pubKeyHex) {
72
+ if (!signatureHex) {
73
+ return { ok: false, reason: "missing X-Atbash-Signature header" };
74
+ }
75
+ const sigClean = signatureHex.trim().toLowerCase().replace(/^0x/, "");
76
+ if (!/^[0-9a-f]+$/.test(sigClean) || sigClean.length < 64 || sigClean.length > 256) {
77
+ return { ok: false, reason: "malformed signature header" };
78
+ }
79
+ let isValid = false;
80
+ try {
81
+ const digest = encryption.sha256(Buffer.from(bodyBytes));
82
+ const pubKeyBytes = Buffer.from(pubKeyHex.replace(/^0x/, ""), "hex");
83
+ const sigBytes = Buffer.from(sigClean, "hex");
84
+ isValid = encryption.checkDigestSignature(digest, pubKeyBytes, sigBytes);
85
+ } catch (err) {
86
+ const message = String(
87
+ err?.message ?? err ?? ""
88
+ );
89
+ return { ok: false, reason: `signature verification threw: ${message}` };
90
+ }
91
+ return isValid ? { ok: true } : { ok: false, reason: "signature does not verify against configured verifyPubKey" };
92
+ }
93
+
94
+ // src/client.ts
95
+ var { createClient, encryption: encryption2, newSignatureProvider } = import_postchain_client2.default;
63
96
  var DEFAULT_ENDPOINT = "https://atbash.ai";
64
97
  var DEFAULT_CHROMIA_NODE_URLS = [
65
98
  "https://node6.testnet.chromia.com:7740",
@@ -115,7 +148,7 @@ async function buildSignedTx(opName, args, auth, chainOpts) {
115
148
  const blockchainRid = chainOpts?.blockchainRid ?? DEFAULT_BLOCKCHAIN_RID;
116
149
  const client = await createClient({ nodeUrlPool: nodeUrls, blockchainRid });
117
150
  const privKeyBuf = Buffer.from(auth.privkey, "hex");
118
- const keyPair = encryption.makeKeyPair(privKeyBuf);
151
+ const keyPair = encryption2.makeKeyPair(privKeyBuf);
119
152
  const sigProvider = newSignatureProvider({
120
153
  privKey: keyPair.privKey,
121
154
  pubKey: keyPair.pubKey
@@ -226,6 +259,31 @@ async function getJson(url, opts) {
226
259
  }
227
260
  return resp.json();
228
261
  }
262
+ async function postJudgeRequest(url, body, opts) {
263
+ if (!opts?.verifyPubKey) {
264
+ return postJson(url, body, opts);
265
+ }
266
+ const resp = await fetch(url, {
267
+ method: "POST",
268
+ headers: { "Content-Type": "application/json" },
269
+ body: JSON.stringify(body),
270
+ signal: opts?.timeout ? AbortSignal.timeout(opts.timeout) : void 0
271
+ });
272
+ if (!resp.ok) {
273
+ const text = await resp.text().catch(() => "");
274
+ throw enrichError(resp.status, text, resp.statusText, opts);
275
+ }
276
+ const buf = new Uint8Array(await resp.arrayBuffer());
277
+ const verdict = verifyJudgeResponseSignature(
278
+ buf,
279
+ resp.headers.get("X-Atbash-Signature"),
280
+ opts.verifyPubKey
281
+ );
282
+ if (!verdict.ok) {
283
+ throw new Error(`signature verification failed: ${verdict.reason}`);
284
+ }
285
+ return JSON.parse(new TextDecoder().decode(buf));
286
+ }
229
287
  async function judgeAction(action, context = "", auth, opts) {
230
288
  if (!action || !action.trim()) {
231
289
  throw new Error("action is required and cannot be empty.");
@@ -263,7 +321,7 @@ async function judgeAction(action, context = "", auth, opts) {
263
321
  ...opts?.toolName && { tool_name: opts.toolName },
264
322
  ...opts?.model && { model: opts.model }
265
323
  };
266
- const data = await postJson(url, body, opts);
324
+ const data = await postJudgeRequest(url, body, opts);
267
325
  return {
268
326
  verdict: normalizeVerdict(data.verdict),
269
327
  action_type: String(data.action_type || ""),
@@ -390,12 +448,213 @@ async function getSafetyStats(opts) {
390
448
  const result = await getJson(url, opts);
391
449
  return result?.data || result;
392
450
  }
451
+
452
+ // src/config.ts
453
+ var ALLOWED_JUDGE_HOSTS = /* @__PURE__ */ new Set([
454
+ "atbash.ai",
455
+ "www.atbash.ai"
456
+ ]);
457
+ function validateJudgeEndpoint(judge) {
458
+ const policy = judge?.policy === "self-hosted" ? "self-hosted" : "default";
459
+ const candidate = judge?.endpoint?.trim() || DEFAULT_ENDPOINT;
460
+ let parsed;
461
+ try {
462
+ parsed = new URL(candidate);
463
+ } catch {
464
+ throw new Error(
465
+ `[atbash] invalid judge endpoint URL: ${candidate}. Refusing to load \u2014 fix the URL or omit it to use the default (${DEFAULT_ENDPOINT}).`
466
+ );
467
+ }
468
+ if (parsed.protocol !== "https:") {
469
+ throw new Error(
470
+ `[atbash] judge endpoint must use https:// (got "${parsed.protocol}"). Refusing to load \u2014 plaintext endpoints leak verdicts and enable trivial MITM bypass.`
471
+ );
472
+ }
473
+ if (parsed.username || parsed.password) {
474
+ throw new Error(
475
+ `[atbash] judge endpoint must not contain credentials (user:pass@host). Refusing to load \u2014 credentials embedded in URLs leak to logs and process listings.`
476
+ );
477
+ }
478
+ const normalisedUrl = parsed.origin;
479
+ if (policy === "self-hosted") {
480
+ const verifyPubKey = judge?.verifyPubKey;
481
+ const key = verifyPubKey?.trim().toLowerCase();
482
+ if (!key || !/^[0-9a-f]{66}$/.test(key)) {
483
+ throw new Error(
484
+ `[atbash] judge endpoint policy "self-hosted" requires verifyPubKey to be a 66-hex-char compressed secp256k1 pubkey. Refusing to load \u2014 self-hosted judges must produce signed responses so the SDK can detect a malicious or compromised judge.`
485
+ );
486
+ }
487
+ return { url: normalisedUrl, policy, verifyPubKey: key };
488
+ }
489
+ if (!ALLOWED_JUDGE_HOSTS.has(parsed.hostname.toLowerCase())) {
490
+ throw new Error(
491
+ `[atbash] judge endpoint hostname "${parsed.hostname}" is not in the trusted allowlist. Allowed: ${[...ALLOWED_JUDGE_HOSTS].join(", ")}. To use a self-hosted judge, set BOTH policy="self-hosted" AND verifyPubKey to the 66-hex pubkey of your judge's response-signing key. Refusing to load \u2014 silent endpoint redirection is a known attack vector (F-003).`
492
+ );
493
+ }
494
+ return { url: normalisedUrl, policy, verifyPubKey: null };
495
+ }
496
+
497
+ // src/key-loader.ts
498
+ var import_node_fs = require("fs");
499
+ var import_node_os = require("os");
500
+ var import_node_path = require("path");
501
+ var DEFAULT_KEY_PATH_REL = ".config/atbash/guard-client-key";
502
+ function resolveKeyPath(input) {
503
+ if (input) return expandHome(input);
504
+ const home = process.env.HOME || (0, import_node_os.homedir)() || "";
505
+ return (0, import_node_path.join)(home, DEFAULT_KEY_PATH_REL);
506
+ }
507
+ function expandHome(p) {
508
+ if (!p.startsWith("~/")) return p;
509
+ const home = process.env.HOME || (0, import_node_os.homedir)() || "";
510
+ return (0, import_node_path.join)(home, p.slice(2));
511
+ }
512
+ function readKeyFile(keyPath) {
513
+ const content = String((0, import_node_fs.readFileSync)(keyPath, "utf8") || "").trim();
514
+ let privKey = "";
515
+ let pubKey = "";
516
+ if (content.startsWith("{")) {
517
+ const creds = JSON.parse(content);
518
+ privKey = String(
519
+ creds.privKey || creds.privkey || creds.privateKey || ""
520
+ ).trim();
521
+ pubKey = String(
522
+ creds.pubKey || creds.pubkey || creds.publicKey || ""
523
+ ).trim();
524
+ } else {
525
+ const lines = content.split(/\r?\n/);
526
+ for (const line of lines) {
527
+ if (line.startsWith("privkey=")) privKey = line.slice("privkey=".length).trim();
528
+ if (line.startsWith("pubkey=")) pubKey = line.slice("pubkey=".length).trim();
529
+ }
530
+ }
531
+ if (!privKey || !pubKey) {
532
+ throw new Error(`atbash key file missing priv/pub key fields: ${keyPath}`);
533
+ }
534
+ privKey = privKey.replace(/^0x/, "");
535
+ return { privKey, pubKey };
536
+ }
537
+ function loadAgentFromFile(keyPath) {
538
+ const resolved = resolveKeyPath(keyPath);
539
+ const { privKey } = readKeyFile(resolved);
540
+ return loadAgent(privKey);
541
+ }
542
+
543
+ // src/factory.ts
544
+ function createAtbashClient(config = {}) {
545
+ const validated = validateJudgeEndpoint(config.judge);
546
+ const failClosed = config.failClosed !== false;
547
+ const logger = config.logger ?? {};
548
+ const inlineKeyPair = config.keyPair;
549
+ const keyPath = inlineKeyPair ? null : config.keyPath;
550
+ if (validated.url !== DEFAULT_ENDPOINT) {
551
+ logger.warn?.("[atbash] running on non-default judge endpoint", {
552
+ endpoint: validated.url,
553
+ policy: validated.policy,
554
+ verifying: validated.verifyPubKey ? "with response-signature pubkey configured" : "without signature verification"
555
+ });
556
+ }
557
+ let cachedAgent = inlineKeyPair ? loadAgent(inlineKeyPair.privKey) : null;
558
+ function loadAgentOnce() {
559
+ if (cachedAgent) return cachedAgent;
560
+ cachedAgent = loadAgentFromFile(keyPath ?? void 0);
561
+ return cachedAgent;
562
+ }
563
+ function fail(reason, toolCallId) {
564
+ return { allow: !failClosed, verdict: "ERROR", reason, toolCallId };
565
+ }
566
+ return {
567
+ async auditToolCall(input) {
568
+ let agent;
569
+ try {
570
+ agent = loadAgentOnce();
571
+ } catch (err) {
572
+ const message = String(err?.message ?? err ?? "");
573
+ logger.warn?.("[atbash] failed to load key pair, blocking for safety", {
574
+ error: message
575
+ });
576
+ return fail("key load failed, blocking for safety");
577
+ }
578
+ const toolName = input.toolName || "unknown";
579
+ const argsJson = stringifyArgs(input.args);
580
+ const actionText = truncate(argsJson);
581
+ const contextText = input.context ?? toolName;
582
+ try {
583
+ logger.info?.("[atbash] judge API called", { tool: toolName });
584
+ const result = await judgeAction(actionText, contextText, agent, {
585
+ endpoint: validated.url,
586
+ verifyPubKey: validated.verifyPubKey ?? void 0,
587
+ toolName,
588
+ toolArgsJson: argsJson,
589
+ chainOpts: {
590
+ nodeUrls: config.nodeUrls,
591
+ blockchainRid: config.blockchainRid
592
+ }
593
+ });
594
+ if (result.verdict === "No verdict") {
595
+ return {
596
+ allow: true,
597
+ verdict: "ALLOW",
598
+ reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
599
+ toolCallId: result.tool_call_id
600
+ };
601
+ }
602
+ const action = result.action_type;
603
+ if (action === "block") {
604
+ return {
605
+ allow: false,
606
+ verdict: "BLOCK",
607
+ reason: result.reason,
608
+ toolCallId: result.tool_call_id
609
+ };
610
+ }
611
+ if (action === "hold_for_user_confirm") {
612
+ return {
613
+ allow: false,
614
+ verdict: "HOLD",
615
+ reason: result.reason || "held for human confirmation",
616
+ toolCallId: result.tool_call_id
617
+ };
618
+ }
619
+ if (action === "allow") {
620
+ const surfacedVerdict = result.verdict === "ALLOW" || result.verdict === "HOLD" || result.verdict === "BLOCK" ? result.verdict : "ALLOW";
621
+ return {
622
+ allow: true,
623
+ verdict: surfacedVerdict,
624
+ reason: result.reason,
625
+ toolCallId: result.tool_call_id
626
+ };
627
+ }
628
+ return fail("unrecognized action_type from judge", result.tool_call_id);
629
+ } catch (err) {
630
+ const message = String(err?.message ?? err ?? "");
631
+ logger.warn?.("[atbash] judge API failed", { reason: message });
632
+ return fail(message);
633
+ }
634
+ }
635
+ };
636
+ }
637
+ function stringifyArgs(args) {
638
+ if (args == null) return "";
639
+ if (typeof args === "string") return args;
640
+ try {
641
+ return JSON.stringify(args);
642
+ } catch {
643
+ return String(args);
644
+ }
645
+ }
646
+ var MAX_ACTION_LEN = 4e3;
647
+ function truncate(text) {
648
+ if (text.length <= MAX_ACTION_LEN) return text;
649
+ return text.slice(0, MAX_ACTION_LEN) + "\u2026";
650
+ }
393
651
  // Annotate the CommonJS export names for ESM import in node:
394
652
  0 && (module.exports = {
395
653
  DEFAULT_BLOCKCHAIN_RID,
396
654
  DEFAULT_CHROMIA_NODE_URLS,
397
655
  DEFAULT_ENDPOINT,
398
656
  checkAgentExists,
657
+ createAtbashClient,
399
658
  derivePublicKey,
400
659
  generateKeyPair,
401
660
  getAgentDetail,
@@ -413,6 +672,10 @@ async function getSafetyStats(opts) {
413
672
  isValidPrivateKey,
414
673
  judgeAction,
415
674
  loadAgent,
675
+ loadAgentFromFile,
416
676
  logToolCall,
417
- toPubkeyHex
677
+ resolveKeyPath,
678
+ toPubkeyHex,
679
+ validateJudgeEndpoint,
680
+ verifyJudgeResponseSignature
418
681
  });
package/dist/index.d.cts CHANGED
@@ -40,6 +40,7 @@ interface JudgeOptions extends ClientOpts {
40
40
  toolName?: string;
41
41
  toolArgsJson?: string;
42
42
  chainOpts?: ChainOpts;
43
+ verifyPubKey?: string;
43
44
  }
44
45
  interface JudgmentStatus {
45
46
  status: JudgmentStatusState;
@@ -106,6 +107,46 @@ interface AgentPolicy {
106
107
  is_custom: boolean;
107
108
  default_policy: string;
108
109
  }
110
+ type DecisionVerdict = "ALLOW" | "HOLD" | "BLOCK" | "ERROR";
111
+ interface Decision {
112
+ allow: boolean;
113
+ verdict: DecisionVerdict;
114
+ reason?: string;
115
+ toolCallId?: string;
116
+ }
117
+ interface ToolCallInput {
118
+ toolName: string;
119
+ args?: unknown;
120
+ context?: string;
121
+ }
122
+ type JudgeEndpointConfig = {
123
+ policy?: "default";
124
+ endpoint?: string;
125
+ } | {
126
+ policy: "self-hosted";
127
+ endpoint: string;
128
+ verifyPubKey: string;
129
+ };
130
+ interface ValidatedEndpoint {
131
+ url: string;
132
+ policy: "default" | "self-hosted";
133
+ verifyPubKey: string | null;
134
+ }
135
+ interface AtbashClientConfig {
136
+ judge?: JudgeEndpointConfig;
137
+ nodeUrls?: string[];
138
+ blockchainRid?: string;
139
+ keyPath?: string;
140
+ keyPair?: {
141
+ privKey: string;
142
+ pubKey: string;
143
+ };
144
+ failClosed?: boolean;
145
+ logger?: {
146
+ info?(...a: unknown[]): void;
147
+ warn?(...a: unknown[]): void;
148
+ };
149
+ }
109
150
 
110
151
  declare const DEFAULT_ENDPOINT = "https://atbash.ai";
111
152
  declare const DEFAULT_CHROMIA_NODE_URLS: string[];
@@ -148,4 +189,19 @@ declare function getAgentDetail(agentPubkey: string, opts?: ClientOpts): Promise
148
189
  declare function getAgentPolicy(agentPubkey: string, opts?: ClientOpts): Promise<AgentPolicy>;
149
190
  declare function getSafetyStats(opts?: ClientOpts): Promise<Record<string, unknown>>;
150
191
 
151
- export { type ActionType, type AgentAuth, type AgentPolicy, type ChainOpts, type ClientOpts, DEFAULT_BLOCKCHAIN_RID, DEFAULT_CHROMIA_NODE_URLS, DEFAULT_ENDPOINT, type HeldAction, type HeldActionReview, type JudgeOptions, type JudgeResult, type JudgmentStatus, type JudgmentStatusState, type LogToolCallResult, type Provider, type PubkeyValue, type Tier, type TierInfo, type ToolCallFull, type ToolCallRecord, type Verdict, checkAgentExists, derivePublicKey, generateKeyPair, getAgentDetail, getAgentPolicy, getAgentToolCalls, getHeldActionReviews, getJudgmentStatus, getOrgTierInfo, getOrgToolCalls, getPendingHeldActions, getSafetyStats, getToolCallCount, getToolCallFull, getToolCalls, isValidPrivateKey, judgeAction, loadAgent, logToolCall, toPubkeyHex };
192
+ interface AtbashClient {
193
+ auditToolCall(input: ToolCallInput): Promise<Decision>;
194
+ }
195
+ declare function createAtbashClient(config?: AtbashClientConfig): AtbashClient;
196
+
197
+ declare function validateJudgeEndpoint(judge?: JudgeEndpointConfig): ValidatedEndpoint;
198
+
199
+ declare function resolveKeyPath(input?: string): string;
200
+ declare function loadAgentFromFile(keyPath?: string): AgentAuth;
201
+
202
+ declare function verifyJudgeResponseSignature(bodyBytes: Uint8Array, signatureHex: string | null, pubKeyHex: string): {
203
+ ok: boolean;
204
+ reason?: string;
205
+ };
206
+
207
+ export { type ActionType, type AgentAuth, type AgentPolicy, type AtbashClient, type AtbashClientConfig, type ChainOpts, type ClientOpts, DEFAULT_BLOCKCHAIN_RID, DEFAULT_CHROMIA_NODE_URLS, DEFAULT_ENDPOINT, type Decision, type DecisionVerdict, type HeldAction, type HeldActionReview, type JudgeEndpointConfig, type JudgeOptions, type JudgeResult, type JudgmentStatus, type JudgmentStatusState, type LogToolCallResult, type Provider, type PubkeyValue, type Tier, type TierInfo, type ToolCallFull, type ToolCallInput, type ToolCallRecord, type ValidatedEndpoint, type Verdict, checkAgentExists, createAtbashClient, derivePublicKey, generateKeyPair, getAgentDetail, getAgentPolicy, getAgentToolCalls, getHeldActionReviews, getJudgmentStatus, getOrgTierInfo, getOrgToolCalls, getPendingHeldActions, getSafetyStats, getToolCallCount, getToolCallFull, getToolCalls, isValidPrivateKey, judgeAction, loadAgent, loadAgentFromFile, logToolCall, resolveKeyPath, toPubkeyHex, validateJudgeEndpoint, verifyJudgeResponseSignature };
package/dist/index.d.ts CHANGED
@@ -40,6 +40,7 @@ interface JudgeOptions extends ClientOpts {
40
40
  toolName?: string;
41
41
  toolArgsJson?: string;
42
42
  chainOpts?: ChainOpts;
43
+ verifyPubKey?: string;
43
44
  }
44
45
  interface JudgmentStatus {
45
46
  status: JudgmentStatusState;
@@ -106,6 +107,46 @@ interface AgentPolicy {
106
107
  is_custom: boolean;
107
108
  default_policy: string;
108
109
  }
110
+ type DecisionVerdict = "ALLOW" | "HOLD" | "BLOCK" | "ERROR";
111
+ interface Decision {
112
+ allow: boolean;
113
+ verdict: DecisionVerdict;
114
+ reason?: string;
115
+ toolCallId?: string;
116
+ }
117
+ interface ToolCallInput {
118
+ toolName: string;
119
+ args?: unknown;
120
+ context?: string;
121
+ }
122
+ type JudgeEndpointConfig = {
123
+ policy?: "default";
124
+ endpoint?: string;
125
+ } | {
126
+ policy: "self-hosted";
127
+ endpoint: string;
128
+ verifyPubKey: string;
129
+ };
130
+ interface ValidatedEndpoint {
131
+ url: string;
132
+ policy: "default" | "self-hosted";
133
+ verifyPubKey: string | null;
134
+ }
135
+ interface AtbashClientConfig {
136
+ judge?: JudgeEndpointConfig;
137
+ nodeUrls?: string[];
138
+ blockchainRid?: string;
139
+ keyPath?: string;
140
+ keyPair?: {
141
+ privKey: string;
142
+ pubKey: string;
143
+ };
144
+ failClosed?: boolean;
145
+ logger?: {
146
+ info?(...a: unknown[]): void;
147
+ warn?(...a: unknown[]): void;
148
+ };
149
+ }
109
150
 
110
151
  declare const DEFAULT_ENDPOINT = "https://atbash.ai";
111
152
  declare const DEFAULT_CHROMIA_NODE_URLS: string[];
@@ -148,4 +189,19 @@ declare function getAgentDetail(agentPubkey: string, opts?: ClientOpts): Promise
148
189
  declare function getAgentPolicy(agentPubkey: string, opts?: ClientOpts): Promise<AgentPolicy>;
149
190
  declare function getSafetyStats(opts?: ClientOpts): Promise<Record<string, unknown>>;
150
191
 
151
- export { type ActionType, type AgentAuth, type AgentPolicy, type ChainOpts, type ClientOpts, DEFAULT_BLOCKCHAIN_RID, DEFAULT_CHROMIA_NODE_URLS, DEFAULT_ENDPOINT, type HeldAction, type HeldActionReview, type JudgeOptions, type JudgeResult, type JudgmentStatus, type JudgmentStatusState, type LogToolCallResult, type Provider, type PubkeyValue, type Tier, type TierInfo, type ToolCallFull, type ToolCallRecord, type Verdict, checkAgentExists, derivePublicKey, generateKeyPair, getAgentDetail, getAgentPolicy, getAgentToolCalls, getHeldActionReviews, getJudgmentStatus, getOrgTierInfo, getOrgToolCalls, getPendingHeldActions, getSafetyStats, getToolCallCount, getToolCallFull, getToolCalls, isValidPrivateKey, judgeAction, loadAgent, logToolCall, toPubkeyHex };
192
+ interface AtbashClient {
193
+ auditToolCall(input: ToolCallInput): Promise<Decision>;
194
+ }
195
+ declare function createAtbashClient(config?: AtbashClientConfig): AtbashClient;
196
+
197
+ declare function validateJudgeEndpoint(judge?: JudgeEndpointConfig): ValidatedEndpoint;
198
+
199
+ declare function resolveKeyPath(input?: string): string;
200
+ declare function loadAgentFromFile(keyPath?: string): AgentAuth;
201
+
202
+ declare function verifyJudgeResponseSignature(bodyBytes: Uint8Array, signatureHex: string | null, pubKeyHex: string): {
203
+ ok: boolean;
204
+ reason?: string;
205
+ };
206
+
207
+ export { type ActionType, type AgentAuth, type AgentPolicy, type AtbashClient, type AtbashClientConfig, type ChainOpts, type ClientOpts, DEFAULT_BLOCKCHAIN_RID, DEFAULT_CHROMIA_NODE_URLS, DEFAULT_ENDPOINT, type Decision, type DecisionVerdict, type HeldAction, type HeldActionReview, type JudgeEndpointConfig, type JudgeOptions, type JudgeResult, type JudgmentStatus, type JudgmentStatusState, type LogToolCallResult, type Provider, type PubkeyValue, type Tier, type TierInfo, type ToolCallFull, type ToolCallInput, type ToolCallRecord, type ValidatedEndpoint, type Verdict, checkAgentExists, createAtbashClient, derivePublicKey, generateKeyPair, getAgentDetail, getAgentPolicy, getAgentToolCalls, getHeldActionReviews, getJudgmentStatus, getOrgTierInfo, getOrgToolCalls, getPendingHeldActions, getSafetyStats, getToolCallCount, getToolCallFull, getToolCalls, isValidPrivateKey, judgeAction, loadAgent, loadAgentFromFile, logToolCall, resolveKeyPath, toPubkeyHex, validateJudgeEndpoint, verifyJudgeResponseSignature };
package/dist/index.js CHANGED
@@ -1,7 +1,35 @@
1
1
  // src/client.ts
2
2
  import { createECDH, randomBytes } from "crypto";
3
+ import postchain2 from "postchain-client";
4
+
5
+ // src/signature.ts
3
6
  import postchain from "postchain-client";
4
- var { createClient, encryption, newSignatureProvider } = postchain;
7
+ var { encryption } = postchain;
8
+ function verifyJudgeResponseSignature(bodyBytes, signatureHex, pubKeyHex) {
9
+ if (!signatureHex) {
10
+ return { ok: false, reason: "missing X-Atbash-Signature header" };
11
+ }
12
+ const sigClean = signatureHex.trim().toLowerCase().replace(/^0x/, "");
13
+ if (!/^[0-9a-f]+$/.test(sigClean) || sigClean.length < 64 || sigClean.length > 256) {
14
+ return { ok: false, reason: "malformed signature header" };
15
+ }
16
+ let isValid = false;
17
+ try {
18
+ const digest = encryption.sha256(Buffer.from(bodyBytes));
19
+ const pubKeyBytes = Buffer.from(pubKeyHex.replace(/^0x/, ""), "hex");
20
+ const sigBytes = Buffer.from(sigClean, "hex");
21
+ isValid = encryption.checkDigestSignature(digest, pubKeyBytes, sigBytes);
22
+ } catch (err) {
23
+ const message = String(
24
+ err?.message ?? err ?? ""
25
+ );
26
+ return { ok: false, reason: `signature verification threw: ${message}` };
27
+ }
28
+ return isValid ? { ok: true } : { ok: false, reason: "signature does not verify against configured verifyPubKey" };
29
+ }
30
+
31
+ // src/client.ts
32
+ var { createClient, encryption: encryption2, newSignatureProvider } = postchain2;
5
33
  var DEFAULT_ENDPOINT = "https://atbash.ai";
6
34
  var DEFAULT_CHROMIA_NODE_URLS = [
7
35
  "https://node6.testnet.chromia.com:7740",
@@ -57,7 +85,7 @@ async function buildSignedTx(opName, args, auth, chainOpts) {
57
85
  const blockchainRid = chainOpts?.blockchainRid ?? DEFAULT_BLOCKCHAIN_RID;
58
86
  const client = await createClient({ nodeUrlPool: nodeUrls, blockchainRid });
59
87
  const privKeyBuf = Buffer.from(auth.privkey, "hex");
60
- const keyPair = encryption.makeKeyPair(privKeyBuf);
88
+ const keyPair = encryption2.makeKeyPair(privKeyBuf);
61
89
  const sigProvider = newSignatureProvider({
62
90
  privKey: keyPair.privKey,
63
91
  pubKey: keyPair.pubKey
@@ -168,6 +196,31 @@ async function getJson(url, opts) {
168
196
  }
169
197
  return resp.json();
170
198
  }
199
+ async function postJudgeRequest(url, body, opts) {
200
+ if (!opts?.verifyPubKey) {
201
+ return postJson(url, body, opts);
202
+ }
203
+ const resp = await fetch(url, {
204
+ method: "POST",
205
+ headers: { "Content-Type": "application/json" },
206
+ body: JSON.stringify(body),
207
+ signal: opts?.timeout ? AbortSignal.timeout(opts.timeout) : void 0
208
+ });
209
+ if (!resp.ok) {
210
+ const text = await resp.text().catch(() => "");
211
+ throw enrichError(resp.status, text, resp.statusText, opts);
212
+ }
213
+ const buf = new Uint8Array(await resp.arrayBuffer());
214
+ const verdict = verifyJudgeResponseSignature(
215
+ buf,
216
+ resp.headers.get("X-Atbash-Signature"),
217
+ opts.verifyPubKey
218
+ );
219
+ if (!verdict.ok) {
220
+ throw new Error(`signature verification failed: ${verdict.reason}`);
221
+ }
222
+ return JSON.parse(new TextDecoder().decode(buf));
223
+ }
171
224
  async function judgeAction(action, context = "", auth, opts) {
172
225
  if (!action || !action.trim()) {
173
226
  throw new Error("action is required and cannot be empty.");
@@ -205,7 +258,7 @@ async function judgeAction(action, context = "", auth, opts) {
205
258
  ...opts?.toolName && { tool_name: opts.toolName },
206
259
  ...opts?.model && { model: opts.model }
207
260
  };
208
- const data = await postJson(url, body, opts);
261
+ const data = await postJudgeRequest(url, body, opts);
209
262
  return {
210
263
  verdict: normalizeVerdict(data.verdict),
211
264
  action_type: String(data.action_type || ""),
@@ -332,11 +385,212 @@ async function getSafetyStats(opts) {
332
385
  const result = await getJson(url, opts);
333
386
  return result?.data || result;
334
387
  }
388
+
389
+ // src/config.ts
390
+ var ALLOWED_JUDGE_HOSTS = /* @__PURE__ */ new Set([
391
+ "atbash.ai",
392
+ "www.atbash.ai"
393
+ ]);
394
+ function validateJudgeEndpoint(judge) {
395
+ const policy = judge?.policy === "self-hosted" ? "self-hosted" : "default";
396
+ const candidate = judge?.endpoint?.trim() || DEFAULT_ENDPOINT;
397
+ let parsed;
398
+ try {
399
+ parsed = new URL(candidate);
400
+ } catch {
401
+ throw new Error(
402
+ `[atbash] invalid judge endpoint URL: ${candidate}. Refusing to load \u2014 fix the URL or omit it to use the default (${DEFAULT_ENDPOINT}).`
403
+ );
404
+ }
405
+ if (parsed.protocol !== "https:") {
406
+ throw new Error(
407
+ `[atbash] judge endpoint must use https:// (got "${parsed.protocol}"). Refusing to load \u2014 plaintext endpoints leak verdicts and enable trivial MITM bypass.`
408
+ );
409
+ }
410
+ if (parsed.username || parsed.password) {
411
+ throw new Error(
412
+ `[atbash] judge endpoint must not contain credentials (user:pass@host). Refusing to load \u2014 credentials embedded in URLs leak to logs and process listings.`
413
+ );
414
+ }
415
+ const normalisedUrl = parsed.origin;
416
+ if (policy === "self-hosted") {
417
+ const verifyPubKey = judge?.verifyPubKey;
418
+ const key = verifyPubKey?.trim().toLowerCase();
419
+ if (!key || !/^[0-9a-f]{66}$/.test(key)) {
420
+ throw new Error(
421
+ `[atbash] judge endpoint policy "self-hosted" requires verifyPubKey to be a 66-hex-char compressed secp256k1 pubkey. Refusing to load \u2014 self-hosted judges must produce signed responses so the SDK can detect a malicious or compromised judge.`
422
+ );
423
+ }
424
+ return { url: normalisedUrl, policy, verifyPubKey: key };
425
+ }
426
+ if (!ALLOWED_JUDGE_HOSTS.has(parsed.hostname.toLowerCase())) {
427
+ throw new Error(
428
+ `[atbash] judge endpoint hostname "${parsed.hostname}" is not in the trusted allowlist. Allowed: ${[...ALLOWED_JUDGE_HOSTS].join(", ")}. To use a self-hosted judge, set BOTH policy="self-hosted" AND verifyPubKey to the 66-hex pubkey of your judge's response-signing key. Refusing to load \u2014 silent endpoint redirection is a known attack vector (F-003).`
429
+ );
430
+ }
431
+ return { url: normalisedUrl, policy, verifyPubKey: null };
432
+ }
433
+
434
+ // src/key-loader.ts
435
+ import { readFileSync } from "fs";
436
+ import { homedir } from "os";
437
+ import { join } from "path";
438
+ var DEFAULT_KEY_PATH_REL = ".config/atbash/guard-client-key";
439
+ function resolveKeyPath(input) {
440
+ if (input) return expandHome(input);
441
+ const home = process.env.HOME || homedir() || "";
442
+ return join(home, DEFAULT_KEY_PATH_REL);
443
+ }
444
+ function expandHome(p) {
445
+ if (!p.startsWith("~/")) return p;
446
+ const home = process.env.HOME || homedir() || "";
447
+ return join(home, p.slice(2));
448
+ }
449
+ function readKeyFile(keyPath) {
450
+ const content = String(readFileSync(keyPath, "utf8") || "").trim();
451
+ let privKey = "";
452
+ let pubKey = "";
453
+ if (content.startsWith("{")) {
454
+ const creds = JSON.parse(content);
455
+ privKey = String(
456
+ creds.privKey || creds.privkey || creds.privateKey || ""
457
+ ).trim();
458
+ pubKey = String(
459
+ creds.pubKey || creds.pubkey || creds.publicKey || ""
460
+ ).trim();
461
+ } else {
462
+ const lines = content.split(/\r?\n/);
463
+ for (const line of lines) {
464
+ if (line.startsWith("privkey=")) privKey = line.slice("privkey=".length).trim();
465
+ if (line.startsWith("pubkey=")) pubKey = line.slice("pubkey=".length).trim();
466
+ }
467
+ }
468
+ if (!privKey || !pubKey) {
469
+ throw new Error(`atbash key file missing priv/pub key fields: ${keyPath}`);
470
+ }
471
+ privKey = privKey.replace(/^0x/, "");
472
+ return { privKey, pubKey };
473
+ }
474
+ function loadAgentFromFile(keyPath) {
475
+ const resolved = resolveKeyPath(keyPath);
476
+ const { privKey } = readKeyFile(resolved);
477
+ return loadAgent(privKey);
478
+ }
479
+
480
+ // src/factory.ts
481
+ function createAtbashClient(config = {}) {
482
+ const validated = validateJudgeEndpoint(config.judge);
483
+ const failClosed = config.failClosed !== false;
484
+ const logger = config.logger ?? {};
485
+ const inlineKeyPair = config.keyPair;
486
+ const keyPath = inlineKeyPair ? null : config.keyPath;
487
+ if (validated.url !== DEFAULT_ENDPOINT) {
488
+ logger.warn?.("[atbash] running on non-default judge endpoint", {
489
+ endpoint: validated.url,
490
+ policy: validated.policy,
491
+ verifying: validated.verifyPubKey ? "with response-signature pubkey configured" : "without signature verification"
492
+ });
493
+ }
494
+ let cachedAgent = inlineKeyPair ? loadAgent(inlineKeyPair.privKey) : null;
495
+ function loadAgentOnce() {
496
+ if (cachedAgent) return cachedAgent;
497
+ cachedAgent = loadAgentFromFile(keyPath ?? void 0);
498
+ return cachedAgent;
499
+ }
500
+ function fail(reason, toolCallId) {
501
+ return { allow: !failClosed, verdict: "ERROR", reason, toolCallId };
502
+ }
503
+ return {
504
+ async auditToolCall(input) {
505
+ let agent;
506
+ try {
507
+ agent = loadAgentOnce();
508
+ } catch (err) {
509
+ const message = String(err?.message ?? err ?? "");
510
+ logger.warn?.("[atbash] failed to load key pair, blocking for safety", {
511
+ error: message
512
+ });
513
+ return fail("key load failed, blocking for safety");
514
+ }
515
+ const toolName = input.toolName || "unknown";
516
+ const argsJson = stringifyArgs(input.args);
517
+ const actionText = truncate(argsJson);
518
+ const contextText = input.context ?? toolName;
519
+ try {
520
+ logger.info?.("[atbash] judge API called", { tool: toolName });
521
+ const result = await judgeAction(actionText, contextText, agent, {
522
+ endpoint: validated.url,
523
+ verifyPubKey: validated.verifyPubKey ?? void 0,
524
+ toolName,
525
+ toolArgsJson: argsJson,
526
+ chainOpts: {
527
+ nodeUrls: config.nodeUrls,
528
+ blockchainRid: config.blockchainRid
529
+ }
530
+ });
531
+ if (result.verdict === "No verdict") {
532
+ return {
533
+ allow: true,
534
+ verdict: "ALLOW",
535
+ reason: result.reason || "audit tier \u2014 request logged on-chain, no AI enforcement",
536
+ toolCallId: result.tool_call_id
537
+ };
538
+ }
539
+ const action = result.action_type;
540
+ if (action === "block") {
541
+ return {
542
+ allow: false,
543
+ verdict: "BLOCK",
544
+ reason: result.reason,
545
+ toolCallId: result.tool_call_id
546
+ };
547
+ }
548
+ if (action === "hold_for_user_confirm") {
549
+ return {
550
+ allow: false,
551
+ verdict: "HOLD",
552
+ reason: result.reason || "held for human confirmation",
553
+ toolCallId: result.tool_call_id
554
+ };
555
+ }
556
+ if (action === "allow") {
557
+ const surfacedVerdict = result.verdict === "ALLOW" || result.verdict === "HOLD" || result.verdict === "BLOCK" ? result.verdict : "ALLOW";
558
+ return {
559
+ allow: true,
560
+ verdict: surfacedVerdict,
561
+ reason: result.reason,
562
+ toolCallId: result.tool_call_id
563
+ };
564
+ }
565
+ return fail("unrecognized action_type from judge", result.tool_call_id);
566
+ } catch (err) {
567
+ const message = String(err?.message ?? err ?? "");
568
+ logger.warn?.("[atbash] judge API failed", { reason: message });
569
+ return fail(message);
570
+ }
571
+ }
572
+ };
573
+ }
574
+ function stringifyArgs(args) {
575
+ if (args == null) return "";
576
+ if (typeof args === "string") return args;
577
+ try {
578
+ return JSON.stringify(args);
579
+ } catch {
580
+ return String(args);
581
+ }
582
+ }
583
+ var MAX_ACTION_LEN = 4e3;
584
+ function truncate(text) {
585
+ if (text.length <= MAX_ACTION_LEN) return text;
586
+ return text.slice(0, MAX_ACTION_LEN) + "\u2026";
587
+ }
335
588
  export {
336
589
  DEFAULT_BLOCKCHAIN_RID,
337
590
  DEFAULT_CHROMIA_NODE_URLS,
338
591
  DEFAULT_ENDPOINT,
339
592
  checkAgentExists,
593
+ createAtbashClient,
340
594
  derivePublicKey,
341
595
  generateKeyPair,
342
596
  getAgentDetail,
@@ -354,6 +608,10 @@ export {
354
608
  isValidPrivateKey,
355
609
  judgeAction,
356
610
  loadAgent,
611
+ loadAgentFromFile,
357
612
  logToolCall,
358
- toPubkeyHex
613
+ resolveKeyPath,
614
+ toPubkeyHex,
615
+ validateJudgeEndpoint,
616
+ verifyJudgeResponseSignature
359
617
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atbash/sdk",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Atbash SDK — control boundary before the last irreversible step in an agent workflow",
5
5
  "homepage": "https://atbash.ai",
6
6
  "author": "Atbash",