blockintel-gate-sdk 0.3.6 → 0.3.8

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
@@ -2,18 +2,12 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var crypto = require('crypto');
5
6
  var uuid = require('uuid');
6
7
  var clientKms = require('@aws-sdk/client-kms');
7
- var crypto$1 = require('crypto');
8
8
 
9
9
  var __defProp = Object.defineProperty;
10
10
  var __getOwnPropNames = Object.getOwnPropertyNames;
11
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
12
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
13
- }) : x)(function(x) {
14
- if (typeof require !== "undefined") return require.apply(this, arguments);
15
- throw Error('Dynamic require of "' + x + '" is not supported');
16
- });
17
11
  var __esm = (fn, res) => function __init() {
18
12
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
19
13
  };
@@ -50,56 +44,73 @@ function canonicalizeJson(obj) {
50
44
  return JSON.stringify(sorted);
51
45
  }
52
46
  async function sha256Hex(input) {
53
- if (typeof crypto !== "undefined" && crypto.subtle) {
54
- const encoder = new TextEncoder();
55
- const data = encoder.encode(input);
56
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
57
- const hashArray = Array.from(new Uint8Array(hashBuffer));
58
- return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
59
- }
60
- if (typeof __require !== "undefined") {
61
- const crypto2 = __require("crypto");
62
- return crypto2.createHash("sha256").update(input, "utf8").digest("hex");
63
- }
64
- throw new Error("SHA-256 not available in this environment");
47
+ return crypto.createHash("sha256").update(input, "utf8").digest("hex");
65
48
  }
66
49
  var init_canonicalJson = __esm({
67
50
  "src/utils/canonicalJson.ts"() {
68
51
  }
69
52
  });
70
53
 
71
- // src/utils/crypto.ts
72
- async function hmacSha256(secret, message) {
73
- if (typeof __require !== "undefined") {
74
- const crypto2 = __require("crypto");
75
- const hmac = crypto2.createHmac("sha256", secret);
76
- hmac.update(message, "utf8");
77
- const signatureHex = hmac.digest("hex");
78
- console.error("[HMAC CRYPTO DEBUG] Signature computation:", JSON.stringify({
79
- secretLength: secret.length,
80
- messageLength: message.length,
81
- messagePreview: message.substring(0, 200) + "...",
82
- signatureLength: signatureHex.length,
83
- signaturePreview: signatureHex.substring(0, 16) + "..."
84
- }, null, 2));
85
- return signatureHex;
86
- }
87
- if (typeof crypto !== "undefined" && crypto.subtle) {
88
- const encoder = new TextEncoder();
89
- const keyData = encoder.encode(secret);
90
- const messageData = encoder.encode(message);
91
- const key = await crypto.subtle.importKey(
92
- "raw",
93
- keyData,
94
- { name: "HMAC", hash: "SHA-256" },
95
- false,
96
- ["sign"]
54
+ // src/utils/decisionTokenVerify.ts
55
+ var decisionTokenVerify_exports = {};
56
+ __export(decisionTokenVerify_exports, {
57
+ decodeJwtUnsafe: () => decodeJwtUnsafe,
58
+ verifyDecisionTokenRs256: () => verifyDecisionTokenRs256
59
+ });
60
+ function decodeJwtUnsafe(token) {
61
+ try {
62
+ const parts = token.split(".");
63
+ if (parts.length !== 3) return null;
64
+ const header = JSON.parse(
65
+ Buffer.from(parts[0], "base64url").toString("utf8")
97
66
  );
98
- const signature = await crypto.subtle.sign("HMAC", key, messageData);
99
- const hashArray = Array.from(new Uint8Array(signature));
100
- return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
67
+ const payload = JSON.parse(
68
+ Buffer.from(parts[1], "base64url").toString("utf8")
69
+ );
70
+ return { header, payload };
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+ function verifyDecisionTokenRs256(token, publicKeyPem) {
76
+ const decoded = decodeJwtUnsafe(token);
77
+ if (!decoded || (decoded.header.alg || "").toUpperCase() !== "RS256") return null;
78
+ const { payload } = decoded;
79
+ const now = Math.floor(Date.now() / 1e3);
80
+ if (payload.iss !== ISS || payload.aud !== AUD) return null;
81
+ if (payload.exp != null && payload.exp < now - 5) return null;
82
+ try {
83
+ const parts = token.split(".");
84
+ const signingInput = `${parts[0]}.${parts[1]}`;
85
+ const signature = Buffer.from(parts[2], "base64url");
86
+ const verify = crypto.createVerify("RSA-SHA256");
87
+ verify.update(signingInput);
88
+ verify.end();
89
+ const ok = verify.verify(publicKeyPem, signature);
90
+ return ok ? payload : null;
91
+ } catch {
92
+ return null;
101
93
  }
102
- throw new Error("HMAC-SHA256 not available in this environment");
94
+ }
95
+ var ISS, AUD;
96
+ var init_decisionTokenVerify = __esm({
97
+ "src/utils/decisionTokenVerify.ts"() {
98
+ ISS = "blockintel-gate";
99
+ AUD = "gate-decision";
100
+ }
101
+ });
102
+ async function hmacSha256(secret, message) {
103
+ const hmac = crypto.createHmac("sha256", secret);
104
+ hmac.update(message, "utf8");
105
+ const signatureHex = hmac.digest("hex");
106
+ console.error("[HMAC CRYPTO DEBUG] Signature computation:", JSON.stringify({
107
+ secretLength: secret.length,
108
+ messageLength: message.length,
109
+ messagePreview: message.substring(0, 200) + "...",
110
+ signatureLength: signatureHex.length,
111
+ signaturePreview: signatureHex.substring(0, 16) + "..."
112
+ }, null, 2));
113
+ return signatureHex;
103
114
  }
104
115
 
105
116
  // src/auth/HmacSigner.ts
@@ -132,26 +143,7 @@ var HmacSigner = class {
132
143
  // Used as nonce in canonical string
133
144
  bodyHash
134
145
  ].join("\n");
135
- console.error("[HMAC SIGNER DEBUG] Canonical request string:", JSON.stringify({
136
- method: method.toUpperCase(),
137
- path,
138
- tenantId,
139
- keyId: this.keyId,
140
- timestampMs: String(timestampMs),
141
- requestId,
142
- bodyHash,
143
- signingStringLength: signingString.length,
144
- signingStringPreview: signingString.substring(0, 200) + "...",
145
- bodyJsonLength: bodyJson.length,
146
- bodyJsonPreview: bodyJson.substring(0, 200) + "..."
147
- }, null, 2));
148
146
  const signature = await hmacSha256(this.secret, signingString);
149
- console.error("[HMAC SIGNER DEBUG] Signature computed:", JSON.stringify({
150
- signatureLength: signature.length,
151
- signaturePreview: signature.substring(0, 16) + "...",
152
- secretLength: this.secret.length,
153
- secretPreview: this.secret.substring(0, 4) + "..." + this.secret.substring(this.secret.length - 4)
154
- }, null, 2));
155
147
  return {
156
148
  "X-GATE-TENANT-ID": tenantId,
157
149
  "X-GATE-KEY-ID": this.keyId,
@@ -350,6 +342,10 @@ async function retryWithBackoff(fn, options = {}) {
350
342
  if (!isRetryable) {
351
343
  throw error;
352
344
  }
345
+ const status = error instanceof Response && error.status || error && typeof error === "object" && "status" in error && error.status || error && typeof error === "object" && "statusCode" in error && error.statusCode;
346
+ const errName = error instanceof Error ? error.name : error && typeof error === "object" && "code" in error ? error.code : "Unknown";
347
+ const extra = ` attempt=${attempt}/${opts.maxAttempts} status=${status ?? "n/a"} err=${errName}`;
348
+ console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason=retry)" + extra);
353
349
  const delay = calculateBackoffDelay(attempt, opts);
354
350
  await new Promise((resolve) => setTimeout(resolve, delay));
355
351
  }
@@ -357,17 +353,73 @@ async function retryWithBackoff(fn, options = {}) {
357
353
  throw lastError;
358
354
  }
359
355
 
356
+ // src/utils/sanitize.ts
357
+ var SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
358
+ "authorization",
359
+ "x-api-key",
360
+ "x-gate-heartbeat-key",
361
+ "x-gate-signature",
362
+ "cookie"
363
+ ]);
364
+ var MAX_STRING_LENGTH = 80;
365
+ function sanitizeHeaders(headers) {
366
+ const out = {};
367
+ for (const [key, value] of Object.entries(headers)) {
368
+ const lower = key.toLowerCase();
369
+ if (SENSITIVE_HEADER_NAMES.has(lower) || lower.includes("signature") || lower.includes("secret") || lower.includes("token")) {
370
+ out[key] = value ? "[REDACTED]" : "[empty]";
371
+ } else {
372
+ out[key] = truncate(String(value), MAX_STRING_LENGTH);
373
+ }
374
+ }
375
+ return out;
376
+ }
377
+ function sanitizeBodyShape(body) {
378
+ if (body === null || body === void 0) {
379
+ return {};
380
+ }
381
+ if (typeof body !== "object") {
382
+ return { _: typeof body };
383
+ }
384
+ if (Array.isArray(body)) {
385
+ return { _: "array", length: String(body.length) };
386
+ }
387
+ const out = {};
388
+ for (const key of Object.keys(body).sort()) {
389
+ const val = body[key];
390
+ if (val !== null && typeof val === "object" && !Array.isArray(val)) {
391
+ out[key] = "object";
392
+ } else if (Array.isArray(val)) {
393
+ out[key] = "array";
394
+ } else {
395
+ out[key] = typeof val;
396
+ }
397
+ }
398
+ return out;
399
+ }
400
+ function truncate(s, max) {
401
+ if (s.length <= max) return s;
402
+ return s.slice(0, max) + "...";
403
+ }
404
+ function isDebugEnabled(debugOption) {
405
+ if (debugOption === true) return true;
406
+ if (typeof process !== "undefined" && process.env.GATE_SDK_DEBUG === "1") return true;
407
+ return false;
408
+ }
409
+
360
410
  // src/http/HttpClient.ts
361
411
  var HttpClient = class {
362
412
  baseUrl;
363
413
  timeoutMs;
364
414
  userAgent;
365
415
  retryOptions;
416
+ debug;
366
417
  constructor(config) {
367
418
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
368
419
  this.timeoutMs = config.timeoutMs ?? 15e3;
369
420
  this.userAgent = config.userAgent ?? "blockintel-gate-sdk/0.1.0";
370
421
  this.retryOptions = config.retryOptions;
422
+ this.debug = isDebugEnabled(config.debug);
371
423
  if (!this.baseUrl) {
372
424
  throw new Error("baseUrl is required");
373
425
  }
@@ -386,7 +438,6 @@ var HttpClient = class {
386
438
  const controller = new AbortController();
387
439
  const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
388
440
  let requestDetailsForLogging = null;
389
- let requestDetailsSet = false;
390
441
  try {
391
442
  const response = await retryWithBackoff(
392
443
  async () => {
@@ -409,31 +460,22 @@ var HttpClient = class {
409
460
  fetchOptions.body = JSON.stringify(body);
410
461
  }
411
462
  }
412
- const logHeaders = {};
413
- if (fetchOptions.headers) {
414
- Object.entries(fetchOptions.headers).forEach(([key, value]) => {
415
- if (key.toLowerCase().includes("signature") || key.toLowerCase().includes("secret")) {
416
- logHeaders[key] = String(value).substring(0, 8) + "...";
417
- } else {
418
- logHeaders[key] = String(value);
419
- }
420
- });
421
- }
422
463
  const bodyStr = typeof fetchOptions.body === "string" ? fetchOptions.body : null;
423
- const details = {
424
- headers: logHeaders,
425
- bodyLength: bodyStr ? bodyStr.length : 0,
426
- bodyPreview: bodyStr ? bodyStr.substring(0, 300) : null
464
+ requestDetailsForLogging = {
465
+ headers: this.debug ? sanitizeHeaders(requestHeaders) : {},
466
+ bodyLength: bodyStr ? bodyStr.length : 0
427
467
  };
428
- requestDetailsForLogging = details;
429
- requestDetailsSet = true;
430
- console.error("[HTTP CLIENT DEBUG] Sending request:", JSON.stringify({
431
- url,
432
- method,
433
- headers: logHeaders,
434
- bodyLength: requestDetailsForLogging.bodyLength,
435
- bodyPreview: requestDetailsForLogging.bodyPreview
436
- }, null, 2));
468
+ if (this.debug) {
469
+ const bodyShape = body && typeof body === "object" ? sanitizeBodyShape(body) : {};
470
+ console.error("[GATE SDK] Request:", JSON.stringify({
471
+ url,
472
+ method,
473
+ headerNames: Object.keys(requestHeaders),
474
+ headersRedacted: requestDetailsForLogging.headers,
475
+ bodyLength: requestDetailsForLogging.bodyLength,
476
+ bodyKeysAndTypes: bodyShape
477
+ }, null, 2));
478
+ }
437
479
  const res = await fetch(url, fetchOptions);
438
480
  if (!res.ok && isRetryableStatus(res.status)) {
439
481
  throw res;
@@ -451,26 +493,24 @@ var HttpClient = class {
451
493
  clearTimeout(timeoutId);
452
494
  let data;
453
495
  const contentType = response.headers.get("content-type");
454
- console.error("[HTTP CLIENT DEBUG] Response received:", JSON.stringify({
455
- status: response.status,
456
- ok: response.ok,
457
- statusText: response.statusText,
458
- contentType,
459
- url: response.url
460
- }, null, 2));
496
+ if (this.debug) {
497
+ console.error("[GATE SDK] Response:", JSON.stringify({
498
+ status: response.status,
499
+ ok: response.ok,
500
+ url: response.url
501
+ }, null, 2));
502
+ }
461
503
  if (contentType && contentType.includes("application/json")) {
462
504
  try {
463
505
  const jsonText = await response.text();
464
- console.error("[HTTP CLIENT DEBUG] Response body (first 500 chars):", jsonText.substring(0, 500));
465
506
  data = JSON.parse(jsonText);
466
- console.error("[HTTP CLIENT DEBUG] Parsed JSON:", JSON.stringify({
467
- hasSuccess: typeof data?.success !== "undefined",
468
- success: data?.success,
469
- hasData: typeof data?.data !== "undefined",
470
- hasError: typeof data?.error !== "undefined"
471
- }, null, 2));
507
+ if (this.debug && data && typeof data === "object") {
508
+ console.error("[GATE SDK] Response keys:", Object.keys(data));
509
+ }
472
510
  } catch (parseError) {
473
- console.error("[HTTP CLIENT DEBUG] JSON parse error:", parseError);
511
+ if (this.debug) {
512
+ console.error("[GATE SDK] JSON parse error:", parseError instanceof Error ? parseError.message : String(parseError));
513
+ }
474
514
  throw new GateError(
475
515
  "INVALID_RESPONSE" /* INVALID_RESPONSE */,
476
516
  "Failed to parse JSON response",
@@ -498,26 +538,12 @@ var HttpClient = class {
498
538
  response.headers.forEach((value, key) => {
499
539
  responseHeaders[key] = value;
500
540
  });
501
- if (response.status === 401) {
502
- console.error("[HTTP CLIENT DEBUG] 401 UNAUTHORIZED - Full request details:", JSON.stringify({
541
+ if (this.debug) {
542
+ console.error("[GATE SDK] Error response:", JSON.stringify({
503
543
  status: response.status,
504
- statusText: response.statusText,
505
544
  url: response.url,
506
- requestMethod: method,
507
545
  requestPath: path,
508
- requestHeaders: requestDetailsForLogging ? requestDetailsForLogging.headers : {},
509
- responseHeaders,
510
- responseData: data,
511
- bodyLength: requestDetailsForLogging ? requestDetailsForLogging.bodyLength : 0,
512
- bodyPreview: requestDetailsForLogging ? requestDetailsForLogging.bodyPreview : null
513
- }, null, 2));
514
- } else {
515
- console.error("[HTTP CLIENT DEBUG] Response not OK:", JSON.stringify({
516
- status: response.status,
517
- statusText: response.statusText,
518
- url: response.url,
519
- headers: responseHeaders,
520
- data
546
+ responseKeys: data && typeof data === "object" ? Object.keys(data) : []
521
547
  }, null, 2));
522
548
  }
523
549
  const errorCode = this.statusToErrorCode(response.status);
@@ -529,7 +555,6 @@ var HttpClient = class {
529
555
  details: data
530
556
  });
531
557
  }
532
- console.error("[HTTP CLIENT DEBUG] Response OK, returning data");
533
558
  return data;
534
559
  } catch (error) {
535
560
  clearTimeout(timeoutId);
@@ -946,6 +971,75 @@ var MetricsCollector = class {
946
971
  this.latencyMs = [];
947
972
  }
948
973
  };
974
+ function canonicalJsonBinding(obj) {
975
+ if (obj === null || obj === void 0) return "null";
976
+ if (typeof obj === "string") return JSON.stringify(obj);
977
+ if (typeof obj === "number") return obj.toString();
978
+ if (typeof obj === "boolean") return obj ? "true" : "false";
979
+ if (Array.isArray(obj)) {
980
+ const items = obj.map((item) => canonicalJsonBinding(item));
981
+ return "[" + items.join(",") + "]";
982
+ }
983
+ if (typeof obj === "object") {
984
+ const keys = Object.keys(obj).sort();
985
+ const pairs = [];
986
+ for (const key of keys) {
987
+ const value = obj[key];
988
+ if (value !== void 0) {
989
+ pairs.push(JSON.stringify(key) + ":" + canonicalJsonBinding(value));
990
+ }
991
+ }
992
+ return "{" + pairs.join(",") + "}";
993
+ }
994
+ return JSON.stringify(obj);
995
+ }
996
+ function normalizeAddress(addr) {
997
+ if (addr == null || addr === "") return "";
998
+ const s = String(addr).trim();
999
+ if (s.startsWith("0x")) return s.toLowerCase();
1000
+ return "0x" + s.toLowerCase();
1001
+ }
1002
+ function normalizeData(data) {
1003
+ if (data == null || data === "") return "";
1004
+ const s = String(data).trim().toLowerCase();
1005
+ return s.startsWith("0x") ? s : "0x" + s;
1006
+ }
1007
+ function buildTxBindingObject(txIntent, signerId, decodedRecipient, decodedFields, fromAddress) {
1008
+ const toAddr = txIntent.toAddress ?? txIntent.to ?? "";
1009
+ const value = (txIntent.valueAtomic ?? txIntent.valueDecimal ?? txIntent.value ?? "0").toString();
1010
+ const data = normalizeData(
1011
+ txIntent.data ?? txIntent.payloadHash ?? txIntent.dataHash ?? ""
1012
+ );
1013
+ const chainId = (txIntent.chainId ?? txIntent.chain ?? "").toString();
1014
+ const toAddress = normalizeAddress(toAddr);
1015
+ const nonce = txIntent.nonce != null ? String(txIntent.nonce) : "";
1016
+ const decoded = {};
1017
+ if (decodedFields && typeof decodedFields === "object") {
1018
+ for (const [k, v] of Object.entries(decodedFields)) {
1019
+ if (v !== void 0) decoded[k] = v;
1020
+ }
1021
+ }
1022
+ const out = {
1023
+ chainId,
1024
+ toAddress,
1025
+ value,
1026
+ data,
1027
+ nonce
1028
+ };
1029
+ if (fromAddress) out.fromAddress = normalizeAddress(fromAddress);
1030
+ if (decodedRecipient != null)
1031
+ out.decodedRecipient = decodedRecipient ? normalizeAddress(decodedRecipient) : null;
1032
+ if (Object.keys(decoded).length > 0) out.decoded = decoded;
1033
+ if (signerId) out.signerId = signerId;
1034
+ if (txIntent.networkFamily) out.networkFamily = txIntent.networkFamily;
1035
+ return out;
1036
+ }
1037
+ function computeTxDigest(binding) {
1038
+ const canonical = canonicalJsonBinding(binding);
1039
+ return crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
1040
+ }
1041
+
1042
+ // src/kms/wrapAwsSdkV3KmsClient.ts
949
1043
  function wrapKmsClient(kmsClient, gateClient, options = {}) {
950
1044
  const defaultOptions = {
951
1045
  mode: options.mode || "enforce",
@@ -982,7 +1076,7 @@ function defaultExtractTxIntent(command) {
982
1076
  throw new Error("SignCommand missing required Message property");
983
1077
  }
984
1078
  const messageBuffer = message instanceof Buffer ? message : Buffer.from(message);
985
- const messageHash = crypto$1.createHash("sha256").update(messageBuffer).digest("hex");
1079
+ const messageHash = crypto.createHash("sha256").update(messageBuffer).digest("hex");
986
1080
  return {
987
1081
  networkFamily: "OTHER",
988
1082
  toAddress: void 0,
@@ -1021,6 +1115,34 @@ async function handleSignCommand(command, originalClient, gateClient, options) {
1021
1115
  // Type assertion - txIntent may have extra fields
1022
1116
  signingContext
1023
1117
  });
1118
+ if (decision.decision === "ALLOW" && gateClient.getRequireDecisionToken() && decision.txDigest != null) {
1119
+ const binding = buildTxBindingObject(
1120
+ txIntent,
1121
+ signerId,
1122
+ void 0,
1123
+ void 0,
1124
+ signingContext.actorPrincipal
1125
+ );
1126
+ const computedDigest = computeTxDigest(binding);
1127
+ if (computedDigest !== decision.txDigest) {
1128
+ options.onDecision("BLOCK", {
1129
+ error: new BlockIntelBlockedError(
1130
+ "DECISION_TOKEN_TX_MISMATCH",
1131
+ decision.decisionId,
1132
+ decision.correlationId,
1133
+ void 0
1134
+ ),
1135
+ signerId,
1136
+ command
1137
+ });
1138
+ throw new BlockIntelBlockedError(
1139
+ "DECISION_TOKEN_TX_MISMATCH",
1140
+ decision.decisionId,
1141
+ decision.correlationId,
1142
+ void 0
1143
+ );
1144
+ }
1145
+ }
1024
1146
  options.onDecision("ALLOW", { decision, signerId, command });
1025
1147
  if (options.mode === "dry-run") {
1026
1148
  return await originalClient.send(new clientKms.SignCommand(command));
@@ -1088,6 +1210,8 @@ var HeartbeatManager = class {
1088
1210
  // Unique per process
1089
1211
  sdkVersion;
1090
1212
  // SDK version for tracking
1213
+ apiKey;
1214
+ // x-gate-heartbeat-key for Control Plane auth
1091
1215
  currentToken = null;
1092
1216
  refreshTimer = null;
1093
1217
  started = false;
@@ -1100,19 +1224,22 @@ var HeartbeatManager = class {
1100
1224
  this.signerId = options.signerId;
1101
1225
  this.environment = options.environment ?? "prod";
1102
1226
  this.baseRefreshIntervalSeconds = options.refreshIntervalSeconds ?? 10;
1227
+ this.apiKey = options.apiKey;
1103
1228
  this.clientInstanceId = options.clientInstanceId || uuid.v4();
1104
1229
  this.sdkVersion = options.sdkVersion || "1.0.0";
1230
+ this.apiKey = options.apiKey;
1105
1231
  }
1106
1232
  /**
1107
- * Start background heartbeat refresher
1233
+ * Start background heartbeat refresher.
1234
+ * Optionally wait for initial token (first evaluate() will otherwise wait up to 2s for token).
1108
1235
  */
1109
- start() {
1236
+ start(options) {
1110
1237
  if (this.started) {
1111
1238
  return;
1112
1239
  }
1113
1240
  this.started = true;
1114
1241
  this.acquireHeartbeat().catch((error) => {
1115
- console.error("[HEARTBEAT] Failed to acquire initial heartbeat:", error);
1242
+ console.error("[HEARTBEAT] Failed to acquire initial heartbeat:", error instanceof Error ? error.message : error);
1116
1243
  });
1117
1244
  this.scheduleNextRefresh();
1118
1245
  }
@@ -1195,12 +1322,23 @@ var HeartbeatManager = class {
1195
1322
  /**
1196
1323
  * Acquire a new heartbeat token from Control Plane
1197
1324
  * NEVER logs token value (security)
1325
+ * Requires x-gate-heartbeat-key header (apiKey) for authentication.
1198
1326
  */
1199
1327
  async acquireHeartbeat() {
1328
+ if (!this.apiKey || this.apiKey.length === 0) {
1329
+ throw new GateError(
1330
+ "UNAUTHORIZED" /* UNAUTHORIZED */,
1331
+ "Heartbeat API key is required. Set GATE_HEARTBEAT_KEY in environment or pass heartbeatApiKey in GateClientConfig.",
1332
+ {}
1333
+ );
1334
+ }
1200
1335
  try {
1201
1336
  const response = await this.httpClient.request({
1202
1337
  method: "POST",
1203
1338
  path: "/api/v1/gate/heartbeat",
1339
+ headers: {
1340
+ "x-gate-heartbeat-key": this.apiKey
1341
+ },
1204
1342
  body: {
1205
1343
  tenantId: this.tenantId,
1206
1344
  signerId: this.signerId,
@@ -1528,7 +1666,8 @@ var GateClient = class {
1528
1666
  this.httpClient = new HttpClient({
1529
1667
  baseUrl: config.baseUrl,
1530
1668
  timeoutMs: config.timeoutMs,
1531
- userAgent: config.userAgent
1669
+ userAgent: config.userAgent,
1670
+ debug: config.debug
1532
1671
  });
1533
1672
  if (config.enableStepUp) {
1534
1673
  this.stepUpPoller = new StepUpPoller({
@@ -1549,6 +1688,12 @@ var GateClient = class {
1549
1688
  console.warn("[GATE CLIENT] LOCAL MODE ENABLED - Auth, heartbeat, and break-glass are disabled");
1550
1689
  this.heartbeatManager = null;
1551
1690
  } else {
1691
+ const heartbeatApiKey = config.heartbeatApiKey ?? (typeof process !== "undefined" ? process.env.GATE_HEARTBEAT_KEY : void 0);
1692
+ if (!heartbeatApiKey || heartbeatApiKey.length === 0) {
1693
+ throw new Error(
1694
+ "GATE_HEARTBEAT_KEY environment variable or heartbeatApiKey in config is required for heartbeat authentication. Set GATE_HEARTBEAT_KEY in your environment or pass heartbeatApiKey in GateClientConfig."
1695
+ );
1696
+ }
1552
1697
  let controlPlaneUrl = config.baseUrl;
1553
1698
  if (controlPlaneUrl.includes("/defense")) {
1554
1699
  controlPlaneUrl = controlPlaneUrl.split("/defense")[0];
@@ -1568,7 +1713,8 @@ var GateClient = class {
1568
1713
  tenantId: config.tenantId,
1569
1714
  signerId: initialSignerId,
1570
1715
  environment: config.environment ?? "prod",
1571
- refreshIntervalSeconds: config.heartbeatRefreshIntervalSeconds ?? 10
1716
+ refreshIntervalSeconds: config.heartbeatRefreshIntervalSeconds ?? 10,
1717
+ apiKey: heartbeatApiKey
1572
1718
  });
1573
1719
  this.heartbeatManager.start();
1574
1720
  }
@@ -1593,6 +1739,16 @@ var GateClient = class {
1593
1739
  });
1594
1740
  }
1595
1741
  }
1742
+ /**
1743
+ * Whether the SDK requires a decision token for ALLOW before sign (ENFORCE/HARD).
1744
+ * Env GATE_REQUIRE_DECISION_TOKEN overrides config.
1745
+ */
1746
+ getRequireDecisionToken() {
1747
+ if (typeof process !== "undefined" && process.env.GATE_REQUIRE_DECISION_TOKEN !== void 0) {
1748
+ return process.env.GATE_REQUIRE_DECISION_TOKEN === "true" || process.env.GATE_REQUIRE_DECISION_TOKEN === "1";
1749
+ }
1750
+ return this.config.requireDecisionToken ?? (this.mode === "ENFORCE" || this.config.enforcementMode === "HARD");
1751
+ }
1596
1752
  /**
1597
1753
  * Perform async IAM permission risk check (non-blocking)
1598
1754
  *
@@ -1623,6 +1779,7 @@ var GateClient = class {
1623
1779
  const startTime = Date.now();
1624
1780
  const failSafeMode = this.config.failSafeMode ?? "ALLOW_ON_TIMEOUT";
1625
1781
  const requestMode = req.mode || this.mode;
1782
+ const requireToken = this.getRequireDecisionToken();
1626
1783
  const executeRequest = async () => {
1627
1784
  if (!this.config.local && this.heartbeatManager && req.signingContext?.signerId) {
1628
1785
  this.heartbeatManager.updateSignerId(req.signingContext.signerId);
@@ -1657,7 +1814,9 @@ var GateClient = class {
1657
1814
  delete txIntent.from;
1658
1815
  }
1659
1816
  const signingContext = {
1660
- ...req.signingContext
1817
+ ...req.signingContext,
1818
+ actorPrincipal: req.signingContext?.actorPrincipal ?? req.signingContext?.signerId ?? "gate-sdk-client",
1819
+ signerId: req.signingContext?.signerId ?? req.signingContext?.actorPrincipal ?? "gate-sdk-client"
1661
1820
  };
1662
1821
  if (heartbeatToken) {
1663
1822
  signingContext.heartbeatToken = heartbeatToken;
@@ -1673,17 +1832,16 @@ var GateClient = class {
1673
1832
  };
1674
1833
  }
1675
1834
  let body = {
1835
+ tenantId: this.config.tenantId,
1676
1836
  requestId,
1677
1837
  timestampMs,
1678
1838
  txIntent,
1679
1839
  signingContext,
1680
1840
  // Add SDK info (required by Hot Path validation)
1681
- // Note: Must match Python SDK name for consistent canonical JSON
1682
1841
  sdk: {
1683
1842
  name: "gate-sdk",
1684
1843
  version: "0.1.0"
1685
1844
  },
1686
- // Add mode and connection failure strategy
1687
1845
  mode: requestMode,
1688
1846
  onConnectionFailure: this.onConnectionFailure
1689
1847
  };
@@ -1713,15 +1871,6 @@ var GateClient = class {
1713
1871
  });
1714
1872
  headers = { ...hmacHeaders };
1715
1873
  body.__canonicalJson = canonicalBodyJson;
1716
- const debugHeaders = {};
1717
- Object.entries(headers).forEach(([key, value]) => {
1718
- if (key.toLowerCase().includes("signature")) {
1719
- debugHeaders[key] = value.substring(0, 8) + "...";
1720
- } else {
1721
- debugHeaders[key] = value;
1722
- }
1723
- });
1724
- console.error("[GATE CLIENT DEBUG] HMAC headers prepared:", JSON.stringify(debugHeaders, null, 2));
1725
1874
  } else if (this.apiKeyAuth) {
1726
1875
  const apiKeyHeaders = this.apiKeyAuth.createHeaders({
1727
1876
  tenantId: this.config.tenantId,
@@ -1729,7 +1878,6 @@ var GateClient = class {
1729
1878
  requestId
1730
1879
  });
1731
1880
  headers = { ...apiKeyHeaders };
1732
- console.error("[GATE CLIENT DEBUG] API key headers prepared:", JSON.stringify(headers, null, 2));
1733
1881
  } else {
1734
1882
  throw new Error("No authentication configured");
1735
1883
  }
@@ -1774,6 +1922,10 @@ var GateClient = class {
1774
1922
  reasonCodes: responseData.reason_codes ?? responseData.reasonCodes ?? [],
1775
1923
  policyVersion: responseData.policy_version ?? responseData.policyVersion,
1776
1924
  correlationId: responseData.correlation_id ?? responseData.correlationId,
1925
+ decisionId: responseData.decision_id ?? responseData.decisionId,
1926
+ decisionToken: responseData.decision_token ?? responseData.decisionToken,
1927
+ expiresAt: responseData.expires_at ?? responseData.expiresAt,
1928
+ txDigest: responseData.tx_digest ?? responseData.txDigest,
1777
1929
  stepUp: responseData.step_up ? {
1778
1930
  requestId: responseData.step_up.request_id ?? (responseData.stepUp?.requestId ?? ""),
1779
1931
  ttlSeconds: responseData.step_up.ttl_seconds ?? responseData.stepUp?.ttlSeconds
@@ -1789,9 +1941,100 @@ var GateClient = class {
1789
1941
  errorReason: simulationData.errorReason ?? simulationData.error_reason
1790
1942
  },
1791
1943
  simulationLatencyMs: metadata.simulationLatencyMs ?? metadata.simulation_latency_ms
1792
- } : {}
1944
+ } : {},
1945
+ metadata: {
1946
+ evaluationLatencyMs: metadata.evaluationLatencyMs ?? metadata.evaluation_latency_ms,
1947
+ policyHash: metadata.policyHash ?? metadata.policy_hash,
1948
+ snapshotVersion: metadata.snapshotVersion ?? metadata.snapshot_version
1949
+ }
1793
1950
  };
1794
1951
  const latencyMs = Date.now() - startTime;
1952
+ const expectedPolicyHash = this.config.expectedPolicyHash;
1953
+ const expectedSnapshotVersion = this.config.expectedSnapshotVersion;
1954
+ if (expectedPolicyHash != null && result.metadata?.policyHash !== expectedPolicyHash) {
1955
+ if (this.config.debug) {
1956
+ console.warn("[GATE SDK] Policy hash mismatch (pinning)", {
1957
+ expected: expectedPolicyHash,
1958
+ received: result.metadata?.policyHash,
1959
+ requestId
1960
+ });
1961
+ }
1962
+ this.metrics.recordRequest("BLOCK", latencyMs);
1963
+ throw new BlockIntelBlockedError(
1964
+ "POLICY_HASH_MISMATCH",
1965
+ result.decisionId ?? requestId,
1966
+ result.correlationId,
1967
+ requestId
1968
+ );
1969
+ }
1970
+ if (expectedSnapshotVersion != null && result.metadata?.snapshotVersion !== void 0 && result.metadata.snapshotVersion !== expectedSnapshotVersion) {
1971
+ if (this.config.debug) {
1972
+ console.warn("[GATE SDK] Snapshot version mismatch (pinning)", {
1973
+ expected: expectedSnapshotVersion,
1974
+ received: result.metadata?.snapshotVersion,
1975
+ requestId
1976
+ });
1977
+ }
1978
+ this.metrics.recordRequest("BLOCK", latencyMs);
1979
+ throw new BlockIntelBlockedError(
1980
+ "SNAPSHOT_VERSION_MISMATCH",
1981
+ result.decisionId ?? requestId,
1982
+ result.correlationId,
1983
+ requestId
1984
+ );
1985
+ }
1986
+ if (requireToken && requestMode === "ENFORCE" && result.decision === "ALLOW" && !this.config.local) {
1987
+ if (!result.decisionToken || !result.txDigest) {
1988
+ this.metrics.recordRequest("BLOCK", latencyMs);
1989
+ throw new BlockIntelBlockedError(
1990
+ "DECISION_TOKEN_MISSING",
1991
+ result.decisionId ?? requestId,
1992
+ result.correlationId,
1993
+ requestId
1994
+ );
1995
+ }
1996
+ const nowSec = Math.floor(Date.now() / 1e3);
1997
+ if (result.expiresAt != null && result.expiresAt < nowSec - 5) {
1998
+ this.metrics.recordRequest("BLOCK", latencyMs);
1999
+ throw new BlockIntelBlockedError(
2000
+ "DECISION_TOKEN_EXPIRED",
2001
+ result.decisionId ?? requestId,
2002
+ result.correlationId,
2003
+ requestId
2004
+ );
2005
+ }
2006
+ const publicKeyPem = this.config.decisionTokenPublicKey;
2007
+ if (publicKeyPem && result.decisionToken) {
2008
+ const { decodeJwtUnsafe: decodeJwtUnsafe2, verifyDecisionTokenRs256: verifyDecisionTokenRs2562 } = await Promise.resolve().then(() => (init_decisionTokenVerify(), decisionTokenVerify_exports));
2009
+ const decoded = decodeJwtUnsafe2(result.decisionToken);
2010
+ if (decoded && (decoded.header.alg || "").toUpperCase() === "RS256") {
2011
+ const resolvedPem = publicKeyPem.startsWith("-----") ? publicKeyPem : Buffer.from(publicKeyPem, "base64").toString("utf8");
2012
+ const verified = verifyDecisionTokenRs2562(result.decisionToken, resolvedPem);
2013
+ if (verified === null) {
2014
+ this.metrics.recordRequest("BLOCK", latencyMs);
2015
+ throw new BlockIntelBlockedError(
2016
+ "DECISION_TOKEN_INVALID",
2017
+ result.decisionId ?? requestId,
2018
+ result.correlationId,
2019
+ requestId
2020
+ );
2021
+ }
2022
+ }
2023
+ }
2024
+ const signerId = signingContext?.signerId ?? req.signingContext?.signerId;
2025
+ const fromAddress = txIntent.fromAddress ?? txIntent.from;
2026
+ const binding = buildTxBindingObject(txIntent, signerId, void 0, void 0, fromAddress);
2027
+ const computedDigest = computeTxDigest(binding);
2028
+ if (computedDigest !== result.txDigest) {
2029
+ this.metrics.recordRequest("BLOCK", latencyMs);
2030
+ throw new BlockIntelBlockedError(
2031
+ "DECISION_TOKEN_DIGEST_MISMATCH",
2032
+ result.decisionId ?? requestId,
2033
+ result.correlationId,
2034
+ requestId
2035
+ );
2036
+ }
2037
+ }
1795
2038
  if (result.decision === "BLOCK") {
1796
2039
  if (requestMode === "SHADOW") {
1797
2040
  console.warn("[GATE SHADOW MODE] Would have blocked transaction", {
@@ -1863,6 +2106,7 @@ var GateClient = class {
1863
2106
  tenantId: this.config.tenantId,
1864
2107
  mode: requestMode
1865
2108
  });
2109
+ console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_open)");
1866
2110
  this.metrics.recordRequest("FAIL_OPEN", Date.now() - startTime);
1867
2111
  return {
1868
2112
  decision: "ALLOW",
@@ -1894,6 +2138,10 @@ var GateClient = class {
1894
2138
  }
1895
2139
  throw error;
1896
2140
  }
2141
+ if (error instanceof GateError && error.code === "RATE_LIMITED" /* RATE_LIMITED */) {
2142
+ console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: 429)");
2143
+ throw error;
2144
+ }
1897
2145
  if (error instanceof BlockIntelBlockedError || error instanceof BlockIntelStepUpRequiredError) {
1898
2146
  throw error;
1899
2147
  }
@@ -1906,6 +2154,7 @@ var GateClient = class {
1906
2154
  */
1907
2155
  handleFailSafe(mode, error, requestId) {
1908
2156
  if (mode === "ALLOW_ON_TIMEOUT") {
2157
+ console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_safe_allow)");
1909
2158
  return {
1910
2159
  decision: "ALLOW",
1911
2160
  reasonCodes: ["FAIL_SAFE_ALLOW"],
@@ -1916,6 +2165,7 @@ var GateClient = class {
1916
2165
  return null;
1917
2166
  }
1918
2167
  if (mode === "BLOCK_ON_ANOMALY") {
2168
+ console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_safe_allow)");
1919
2169
  return {
1920
2170
  decision: "ALLOW",
1921
2171
  reasonCodes: ["FAIL_SAFE_ALLOW"],
@@ -2016,6 +2266,8 @@ exports.GateErrorCode = GateErrorCode;
2016
2266
  exports.HeartbeatManager = HeartbeatManager;
2017
2267
  exports.ProvenanceProvider = ProvenanceProvider;
2018
2268
  exports.StepUpNotConfiguredError = StepUpNotConfiguredError;
2269
+ exports.buildTxBindingObject = buildTxBindingObject;
2270
+ exports.computeTxDigest = computeTxDigest;
2019
2271
  exports.createGateClient = createGateClient;
2020
2272
  exports.default = GateClient;
2021
2273
  exports.wrapKmsClient = wrapKmsClient;