blockintel-gate-sdk 0.3.1 → 0.3.3

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
@@ -6,64 +6,48 @@ var uuid = require('uuid');
6
6
  var clientKms = require('@aws-sdk/client-kms');
7
7
  var crypto$1 = require('crypto');
8
8
 
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
9
11
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
10
12
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
11
13
  }) : x)(function(x) {
12
14
  if (typeof require !== "undefined") return require.apply(this, arguments);
13
15
  throw Error('Dynamic require of "' + x + '" is not supported');
14
16
  });
15
-
16
- // src/utils/crypto.ts
17
- async function hmacSha256(secret, message) {
18
- if (typeof crypto !== "undefined" && crypto.subtle) {
19
- const encoder = new TextEncoder();
20
- const keyData = encoder.encode(secret);
21
- const messageData = encoder.encode(message);
22
- const key = await crypto.subtle.importKey(
23
- "raw",
24
- keyData,
25
- { name: "HMAC", hash: "SHA-256" },
26
- false,
27
- ["sign"]
28
- );
29
- const signature = await crypto.subtle.sign("HMAC", key, messageData);
30
- const hashArray = Array.from(new Uint8Array(signature));
31
- return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
32
- }
33
- if (typeof __require !== "undefined") {
34
- const crypto2 = __require("crypto");
35
- const hmac = crypto2.createHmac("sha256", secret);
36
- hmac.update(message, "utf8");
37
- return hmac.digest("hex");
38
- }
39
- throw new Error("HMAC-SHA256 not available in this environment");
40
- }
17
+ var __esm = (fn, res) => function __init() {
18
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
19
+ };
20
+ var __export = (target, all) => {
21
+ for (var name in all)
22
+ __defProp(target, name, { get: all[name], enumerable: true });
23
+ };
41
24
 
42
25
  // src/utils/canonicalJson.ts
26
+ var canonicalJson_exports = {};
27
+ __export(canonicalJson_exports, {
28
+ canonicalizeJson: () => canonicalizeJson,
29
+ sha256Hex: () => sha256Hex
30
+ });
43
31
  function canonicalizeJson(obj) {
44
32
  if (obj === null || obj === void 0) {
45
33
  return "null";
46
34
  }
47
- if (typeof obj === "string") {
48
- return JSON.stringify(obj);
49
- }
50
- if (typeof obj === "number" || typeof obj === "boolean") {
51
- return String(obj);
52
- }
53
- if (Array.isArray(obj)) {
54
- const items = obj.map((item) => canonicalizeJson(item));
55
- return `[${items.join(",")}]`;
56
- }
57
- if (typeof obj === "object") {
58
- const keys = Object.keys(obj).sort();
59
- const pairs = keys.map((key) => {
60
- const value = obj[key];
61
- const canonicalValue = canonicalizeJson(value);
62
- return `${JSON.stringify(key)}:${canonicalValue}`;
63
- });
64
- return `{${pairs.join(",")}}`;
35
+ const cloned = JSON.parse(JSON.stringify(obj));
36
+ function sortKeys(item) {
37
+ if (Array.isArray(item)) {
38
+ return item.map(sortKeys);
39
+ }
40
+ if (item !== null && typeof item === "object") {
41
+ const sorted2 = {};
42
+ Object.keys(item).sort().forEach((key) => {
43
+ sorted2[key] = sortKeys(item[key]);
44
+ });
45
+ return sorted2;
46
+ }
47
+ return item;
65
48
  }
66
- return JSON.stringify(obj);
49
+ const sorted = sortKeys(cloned);
50
+ return JSON.stringify(sorted);
67
51
  }
68
52
  async function sha256Hex(input) {
69
53
  if (typeof crypto !== "undefined" && crypto.subtle) {
@@ -79,14 +63,53 @@ async function sha256Hex(input) {
79
63
  }
80
64
  throw new Error("SHA-256 not available in this environment");
81
65
  }
66
+ var init_canonicalJson = __esm({
67
+ "src/utils/canonicalJson.ts"() {
68
+ }
69
+ });
70
+
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"]
97
+ );
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("");
101
+ }
102
+ throw new Error("HMAC-SHA256 not available in this environment");
103
+ }
82
104
 
83
105
  // src/auth/HmacSigner.ts
106
+ init_canonicalJson();
84
107
  var HmacSigner = class {
85
108
  keyId;
86
109
  secret;
87
110
  constructor(config) {
88
111
  this.keyId = config.keyId;
89
- this.secret = config.secret;
112
+ this.secret = config.secret.trim();
90
113
  if (!this.secret || this.secret.length === 0) {
91
114
  throw new Error("HMAC secret cannot be empty");
92
115
  }
@@ -109,7 +132,26 @@ var HmacSigner = class {
109
132
  // Used as nonce in canonical string
110
133
  bodyHash
111
134
  ].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));
112
148
  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));
113
155
  return {
114
156
  "X-GATE-TENANT-ID": tenantId,
115
157
  "X-GATE-KEY-ID": this.keyId,
@@ -158,6 +200,10 @@ var GateErrorCode = /* @__PURE__ */ ((GateErrorCode2) => {
158
200
  GateErrorCode2["BLOCKED"] = "BLOCKED";
159
201
  GateErrorCode2["SERVICE_UNAVAILABLE"] = "SERVICE_UNAVAILABLE";
160
202
  GateErrorCode2["AUTH_ERROR"] = "AUTH_ERROR";
203
+ GateErrorCode2["HEARTBEAT_MISSING"] = "HEARTBEAT_MISSING";
204
+ GateErrorCode2["HEARTBEAT_EXPIRED"] = "HEARTBEAT_EXPIRED";
205
+ GateErrorCode2["HEARTBEAT_INVALID"] = "HEARTBEAT_INVALID";
206
+ GateErrorCode2["HEARTBEAT_MISMATCH"] = "HEARTBEAT_MISMATCH";
161
207
  return GateErrorCode2;
162
208
  })(GateErrorCode || {});
163
209
  var GateError = class extends Error {
@@ -339,21 +385,55 @@ var HttpClient = class {
339
385
  const url = `${this.baseUrl}${path}`;
340
386
  const controller = new AbortController();
341
387
  const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
388
+ let requestDetailsForLogging = null;
389
+ let requestDetailsSet = false;
342
390
  try {
343
391
  const response = await retryWithBackoff(
344
392
  async () => {
393
+ const requestHeaders = {};
394
+ for (const [key, value] of Object.entries(headers)) {
395
+ requestHeaders[key] = String(value);
396
+ }
397
+ requestHeaders["User-Agent"] = this.userAgent;
398
+ requestHeaders["Content-Type"] = "application/json";
345
399
  const fetchOptions = {
346
400
  method,
347
- headers: {
348
- ...headers,
349
- "User-Agent": this.userAgent,
350
- "Content-Type": "application/json"
351
- },
401
+ headers: requestHeaders,
352
402
  signal: controller.signal
353
403
  };
354
404
  if (body) {
355
- fetchOptions.body = JSON.stringify(body);
405
+ if (body.__canonicalJson) {
406
+ fetchOptions.body = body.__canonicalJson;
407
+ delete body.__canonicalJson;
408
+ } else {
409
+ fetchOptions.body = JSON.stringify(body);
410
+ }
411
+ }
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
+ });
356
421
  }
422
+ 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
427
+ };
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));
357
437
  const res = await fetch(url, fetchOptions);
358
438
  if (!res.ok && isRetryableStatus(res.status)) {
359
439
  throw res;
@@ -371,10 +451,26 @@ var HttpClient = class {
371
451
  clearTimeout(timeoutId);
372
452
  let data;
373
453
  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));
374
461
  if (contentType && contentType.includes("application/json")) {
375
462
  try {
376
- data = await response.json();
463
+ const jsonText = await response.text();
464
+ console.error("[HTTP CLIENT DEBUG] Response body (first 500 chars):", jsonText.substring(0, 500));
465
+ 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));
377
472
  } catch (parseError) {
473
+ console.error("[HTTP CLIENT DEBUG] JSON parse error:", parseError);
378
474
  throw new GateError(
379
475
  "INVALID_RESPONSE" /* INVALID_RESPONSE */,
380
476
  "Failed to parse JSON response",
@@ -398,6 +494,32 @@ var HttpClient = class {
398
494
  );
399
495
  }
400
496
  if (!response.ok) {
497
+ const responseHeaders = {};
498
+ response.headers.forEach((value, key) => {
499
+ responseHeaders[key] = value;
500
+ });
501
+ if (response.status === 401) {
502
+ console.error("[HTTP CLIENT DEBUG] 401 UNAUTHORIZED - Full request details:", JSON.stringify({
503
+ status: response.status,
504
+ statusText: response.statusText,
505
+ url: response.url,
506
+ requestMethod: method,
507
+ 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
521
+ }, null, 2));
522
+ }
401
523
  const errorCode = this.statusToErrorCode(response.status);
402
524
  const correlationId = response.headers.get("X-Correlation-ID") ?? void 0;
403
525
  throw new GateError(errorCode, `HTTP ${response.status}: ${response.statusText}`, {
@@ -407,6 +529,7 @@ var HttpClient = class {
407
529
  details: data
408
530
  });
409
531
  }
532
+ console.error("[HTTP CLIENT DEBUG] Response OK, returning data");
410
533
  return data;
411
534
  } catch (error) {
412
535
  clearTimeout(timeoutId);
@@ -716,6 +839,10 @@ var MetricsCollector = class {
716
839
  timeoutsTotal = 0;
717
840
  errorsTotal = 0;
718
841
  circuitBreakerOpenTotal = 0;
842
+ wouldBlockTotal = 0;
843
+ // Shadow mode would-block count
844
+ failOpenTotal = 0;
845
+ // Fail-open count
719
846
  latencyMs = [];
720
847
  maxSamples = 1e3;
721
848
  // Keep last 1000 samples
@@ -731,6 +858,12 @@ var MetricsCollector = class {
731
858
  this.blockedTotal++;
732
859
  } else if (decision === "REQUIRE_STEP_UP") {
733
860
  this.stepupTotal++;
861
+ } else if (decision === "WOULD_BLOCK") {
862
+ this.wouldBlockTotal++;
863
+ this.allowedTotal++;
864
+ } else if (decision === "FAIL_OPEN") {
865
+ this.failOpenTotal++;
866
+ this.allowedTotal++;
734
867
  }
735
868
  this.latencyMs.push(latencyMs);
736
869
  if (this.latencyMs.length > this.maxSamples) {
@@ -772,6 +905,8 @@ var MetricsCollector = class {
772
905
  timeoutsTotal: this.timeoutsTotal,
773
906
  errorsTotal: this.errorsTotal,
774
907
  circuitBreakerOpenTotal: this.circuitBreakerOpenTotal,
908
+ wouldBlockTotal: this.wouldBlockTotal,
909
+ failOpenTotal: this.failOpenTotal,
775
910
  latencyMs: [...this.latencyMs]
776
911
  // Copy array
777
912
  };
@@ -806,6 +941,8 @@ var MetricsCollector = class {
806
941
  this.timeoutsTotal = 0;
807
942
  this.errorsTotal = 0;
808
943
  this.circuitBreakerOpenTotal = 0;
944
+ this.wouldBlockTotal = 0;
945
+ this.failOpenTotal = 0;
809
946
  this.latencyMs = [];
810
947
  }
811
948
  };
@@ -858,10 +995,25 @@ function defaultExtractTxIntent(command) {
858
995
  async function handleSignCommand(command, originalClient, gateClient, options) {
859
996
  const txIntent = options.extractTxIntent(command);
860
997
  const signerId = command.input?.KeyId ?? command.KeyId ?? "unknown";
998
+ gateClient.heartbeatManager.updateSignerId(signerId);
999
+ const heartbeatToken = gateClient.heartbeatManager.getToken();
1000
+ if (!heartbeatToken) {
1001
+ throw new BlockIntelBlockedError(
1002
+ "HEARTBEAT_MISSING",
1003
+ void 0,
1004
+ // receiptId
1005
+ void 0,
1006
+ // correlationId
1007
+ void 0
1008
+ // requestId
1009
+ );
1010
+ }
861
1011
  const signingContext = {
862
1012
  signerId,
863
- actorPrincipal: "kms-signer"
1013
+ actorPrincipal: "kms-signer",
864
1014
  // Default - can be customized via extractTxIntent
1015
+ heartbeatToken
1016
+ // Attach heartbeat token
865
1017
  };
866
1018
  try {
867
1019
  const decision = await gateClient.evaluate({
@@ -926,6 +1078,421 @@ var ProvenanceProvider = class {
926
1078
  return !!(process.env.GATE_CALLER_REPO || process.env.GATE_CALLER_WORKFLOW || process.env.GATE_ATTESTATION_VALID);
927
1079
  }
928
1080
  };
1081
+ var HeartbeatManager = class {
1082
+ httpClient;
1083
+ tenantId;
1084
+ signerId;
1085
+ environment;
1086
+ baseRefreshIntervalSeconds;
1087
+ clientInstanceId;
1088
+ // Unique per process
1089
+ sdkVersion;
1090
+ // SDK version for tracking
1091
+ currentToken = null;
1092
+ refreshTimer = null;
1093
+ started = false;
1094
+ consecutiveFailures = 0;
1095
+ maxBackoffSeconds = 30;
1096
+ // Maximum backoff interval
1097
+ constructor(options) {
1098
+ this.httpClient = options.httpClient;
1099
+ this.tenantId = options.tenantId;
1100
+ this.signerId = options.signerId;
1101
+ this.environment = options.environment ?? "prod";
1102
+ this.baseRefreshIntervalSeconds = options.refreshIntervalSeconds ?? 10;
1103
+ this.clientInstanceId = options.clientInstanceId || uuid.v4();
1104
+ this.sdkVersion = options.sdkVersion || "1.0.0";
1105
+ }
1106
+ /**
1107
+ * Start background heartbeat refresher
1108
+ */
1109
+ start() {
1110
+ if (this.started) {
1111
+ return;
1112
+ }
1113
+ this.started = true;
1114
+ this.acquireHeartbeat().catch((error) => {
1115
+ console.error("[HEARTBEAT] Failed to acquire initial heartbeat:", error);
1116
+ });
1117
+ this.scheduleNextRefresh();
1118
+ }
1119
+ /**
1120
+ * Schedule next refresh with jitter and backoff
1121
+ */
1122
+ scheduleNextRefresh() {
1123
+ if (!this.started) {
1124
+ return;
1125
+ }
1126
+ const baseInterval = this.baseRefreshIntervalSeconds * 1e3;
1127
+ const jitter = Math.random() * 2e3;
1128
+ const backoff = this.calculateBackoff();
1129
+ const interval = baseInterval + jitter + backoff;
1130
+ this.refreshTimer = setTimeout(() => {
1131
+ this.acquireHeartbeat().then(() => {
1132
+ this.consecutiveFailures = 0;
1133
+ this.scheduleNextRefresh();
1134
+ }).catch((error) => {
1135
+ this.consecutiveFailures++;
1136
+ console.error("[HEARTBEAT] Refresh failed (will retry):", error);
1137
+ this.scheduleNextRefresh();
1138
+ });
1139
+ }, interval);
1140
+ }
1141
+ /**
1142
+ * Calculate exponential backoff (capped at maxBackoffSeconds)
1143
+ */
1144
+ calculateBackoff() {
1145
+ if (this.consecutiveFailures === 0) {
1146
+ return 0;
1147
+ }
1148
+ const backoffSeconds = Math.min(
1149
+ Math.pow(2, this.consecutiveFailures) * 1e3,
1150
+ this.maxBackoffSeconds * 1e3
1151
+ );
1152
+ return backoffSeconds;
1153
+ }
1154
+ /**
1155
+ * Stop background heartbeat refresher
1156
+ */
1157
+ stop() {
1158
+ if (!this.started) {
1159
+ return;
1160
+ }
1161
+ this.started = false;
1162
+ if (this.refreshTimer) {
1163
+ clearTimeout(this.refreshTimer);
1164
+ this.refreshTimer = null;
1165
+ }
1166
+ }
1167
+ /**
1168
+ * Get current heartbeat token if valid
1169
+ */
1170
+ getToken() {
1171
+ if (!this.currentToken) {
1172
+ return null;
1173
+ }
1174
+ const now = Math.floor(Date.now() / 1e3);
1175
+ if (this.currentToken.expiresAt <= now + 2) {
1176
+ return null;
1177
+ }
1178
+ return this.currentToken.token;
1179
+ }
1180
+ /**
1181
+ * Check if current heartbeat token is valid
1182
+ */
1183
+ isValid() {
1184
+ return this.getToken() !== null;
1185
+ }
1186
+ /**
1187
+ * Update signer ID (called when signer is known)
1188
+ */
1189
+ updateSignerId(signerId) {
1190
+ if (this.signerId !== signerId) {
1191
+ this.signerId = signerId;
1192
+ this.currentToken = null;
1193
+ }
1194
+ }
1195
+ /**
1196
+ * Acquire a new heartbeat token from Control Plane
1197
+ * NEVER logs token value (security)
1198
+ */
1199
+ async acquireHeartbeat() {
1200
+ try {
1201
+ const response = await this.httpClient.request({
1202
+ method: "POST",
1203
+ path: "/api/v1/gate/heartbeat",
1204
+ body: {
1205
+ tenantId: this.tenantId,
1206
+ signerId: this.signerId,
1207
+ environment: this.environment,
1208
+ clientInstanceId: this.clientInstanceId,
1209
+ sdkVersion: this.sdkVersion
1210
+ }
1211
+ });
1212
+ if (response.success && response.data) {
1213
+ const token = response.data.heartbeatToken;
1214
+ const expiresAt = response.data.expiresAt;
1215
+ if (!token || !expiresAt) {
1216
+ throw new GateError(
1217
+ "INVALID_RESPONSE" /* INVALID_RESPONSE */,
1218
+ "Invalid heartbeat response: missing token or expiresAt"
1219
+ );
1220
+ }
1221
+ this.currentToken = {
1222
+ token,
1223
+ expiresAt,
1224
+ jti: response.data.jti,
1225
+ policyHash: response.data.policyHash
1226
+ };
1227
+ console.log("[HEARTBEAT] Acquired heartbeat token", {
1228
+ expiresAt,
1229
+ jti: response.data.jti,
1230
+ policyHash: response.data.policyHash?.substring(0, 8) + "..."
1231
+ // DO NOT log token value
1232
+ });
1233
+ } else {
1234
+ const error = response.error || {};
1235
+ throw new GateError(
1236
+ "SERVER_ERROR" /* SERVER_ERROR */,
1237
+ `Heartbeat acquisition failed: ${error.message || "Unknown error"}`
1238
+ );
1239
+ }
1240
+ } catch (error) {
1241
+ console.error("[HEARTBEAT] Failed to acquire heartbeat:", error.message || error);
1242
+ throw error;
1243
+ }
1244
+ }
1245
+ /**
1246
+ * Get client instance ID (for tracking)
1247
+ */
1248
+ getClientInstanceId() {
1249
+ return this.clientInstanceId;
1250
+ }
1251
+ };
1252
+
1253
+ // src/security/IamPermissionRiskChecker.ts
1254
+ var IamPermissionRiskChecker = class {
1255
+ options;
1256
+ constructor(options) {
1257
+ this.options = options;
1258
+ }
1259
+ /**
1260
+ * Perform synchronous IAM permission risk check
1261
+ *
1262
+ * Performs quick checks (credentials, environment markers) synchronously.
1263
+ * In HARD mode, throws error if risk detected and override not set.
1264
+ *
1265
+ * Use this for blocking initialization checks.
1266
+ */
1267
+ checkSync() {
1268
+ const checks = [];
1269
+ const credentialsCheck = this.checkAwsCredentials();
1270
+ if (credentialsCheck.hasRisk) {
1271
+ checks.push(credentialsCheck);
1272
+ }
1273
+ const envCheck = this.checkEnvironmentMarkers();
1274
+ if (envCheck.hasRisk) {
1275
+ checks.push(envCheck);
1276
+ }
1277
+ const highestConfidence = this.getHighestConfidence(checks);
1278
+ const highestRisk = checks.find((c) => c.confidence === highestConfidence);
1279
+ if (!highestRisk || !highestRisk.hasRisk) {
1280
+ return {
1281
+ hasRisk: false,
1282
+ confidence: "LOW",
1283
+ details: "No IAM permission risk detected (synchronous check)"
1284
+ };
1285
+ }
1286
+ if (this.options.enforcementMode === "HARD" && !this.options.allowInsecureKmsSignPermission) {
1287
+ const errorMessage = this.buildErrorMessage(highestRisk);
1288
+ throw new Error(errorMessage);
1289
+ }
1290
+ this.logWarning(highestRisk);
1291
+ return highestRisk;
1292
+ }
1293
+ /**
1294
+ * Perform full IAM permission risk check (including async IAM simulation)
1295
+ *
1296
+ * Returns risk assessment with confidence level.
1297
+ * In HARD mode, throws error if risk detected and override not set.
1298
+ */
1299
+ async check() {
1300
+ const syncResult = this.checkSync();
1301
+ const simulationCheck = await this.checkIamSimulation();
1302
+ if (simulationCheck.hasRisk) {
1303
+ if (this.options.enforcementMode === "HARD" && !this.options.allowInsecureKmsSignPermission) {
1304
+ const errorMessage = this.buildErrorMessage(simulationCheck);
1305
+ throw new Error(errorMessage);
1306
+ }
1307
+ this.logWarning(simulationCheck);
1308
+ return simulationCheck;
1309
+ }
1310
+ return syncResult;
1311
+ }
1312
+ /**
1313
+ * Check if AWS credentials are present
1314
+ */
1315
+ checkAwsCredentials() {
1316
+ const hasEnvVars = !!(process.env.AWS_ACCESS_KEY_ID || process.env.AWS_SECRET_ACCESS_KEY || process.env.AWS_SESSION_TOKEN);
1317
+ const hasRoleCredentials = !!(process.env.AWS_ROLE_ARN || process.env.AWS_WEB_IDENTITY_TOKEN_FILE || process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI);
1318
+ if (hasEnvVars || hasRoleCredentials) {
1319
+ return {
1320
+ hasRisk: true,
1321
+ riskType: "AWS_CREDENTIALS_DETECTED",
1322
+ confidence: "MEDIUM",
1323
+ details: "AWS credentials detected in environment. Application may have direct KMS signing permissions.",
1324
+ remediation: "Remove kms:Sign permission from application role. See https://docs.blockintel.ai/gate/IAM_HARDENING"
1325
+ };
1326
+ }
1327
+ return {
1328
+ hasRisk: false,
1329
+ confidence: "LOW",
1330
+ details: "No AWS credentials detected in environment variables"
1331
+ };
1332
+ }
1333
+ /**
1334
+ * Check IAM permissions using simulation API (if available)
1335
+ */
1336
+ async checkIamSimulation() {
1337
+ try {
1338
+ const iamModule = await import('@aws-sdk/client-iam').catch(() => null);
1339
+ if (!iamModule || !iamModule.IAMClient || !iamModule.SimulatePrincipalPolicyCommand) {
1340
+ return {
1341
+ hasRisk: false,
1342
+ confidence: "LOW",
1343
+ details: "AWS SDK not available for IAM simulation"
1344
+ };
1345
+ }
1346
+ const { IAMClient, SimulatePrincipalPolicyCommand } = iamModule;
1347
+ const principalArn = await this.getCurrentPrincipalArn();
1348
+ if (!principalArn) {
1349
+ return {
1350
+ hasRisk: false,
1351
+ confidence: "LOW",
1352
+ details: "Could not determine current principal ARN for simulation"
1353
+ };
1354
+ }
1355
+ const client = new IAMClient({});
1356
+ const command = new SimulatePrincipalPolicyCommand({
1357
+ PolicySourceArn: principalArn,
1358
+ ActionNames: ["kms:Sign"],
1359
+ ResourceArns: this.options.kmsKeyIds?.map((id) => `arn:aws:kms:*:*:key/${id}`) || ["arn:aws:kms:*:*:key/*"]
1360
+ });
1361
+ const response = await client.send(command).catch(() => null);
1362
+ if (!response) {
1363
+ return {
1364
+ hasRisk: false,
1365
+ confidence: "LOW",
1366
+ details: "IAM simulation not available (may require additional permissions)"
1367
+ };
1368
+ }
1369
+ const allowsSign = response.EvaluationResults?.some(
1370
+ (result) => result.EvalDecision === "allowed" || result.EvalDecision === "explicitAllow"
1371
+ );
1372
+ if (allowsSign) {
1373
+ return {
1374
+ hasRisk: true,
1375
+ riskType: "DIRECT_KMS_SIGN_PERMISSION",
1376
+ confidence: "HIGH",
1377
+ details: `IAM simulation confirms principal ${principalArn} has kms:Sign permission. Direct KMS signing can bypass Gate.`,
1378
+ remediation: "Remove kms:Sign permission from application role. See https://docs.blockintel.ai/gate/IAM_HARDENING"
1379
+ };
1380
+ }
1381
+ return {
1382
+ hasRisk: false,
1383
+ confidence: "HIGH",
1384
+ details: "IAM simulation confirms no kms:Sign permission"
1385
+ };
1386
+ } catch (error) {
1387
+ return {
1388
+ hasRisk: false,
1389
+ confidence: "LOW",
1390
+ details: `IAM simulation failed: ${error instanceof Error ? error.message : "Unknown error"}`
1391
+ };
1392
+ }
1393
+ }
1394
+ /**
1395
+ * Check environment markers that suggest direct KMS usage
1396
+ */
1397
+ checkEnvironmentMarkers() {
1398
+ const markers = [
1399
+ "KMS_KEY_ID",
1400
+ "AWS_KMS_KEY_ID",
1401
+ "KMS_KEY_ARN",
1402
+ "AWS_KMS_KEY_ARN"
1403
+ ];
1404
+ const foundMarkers = markers.filter((marker) => process.env[marker]);
1405
+ if (foundMarkers.length > 0) {
1406
+ return {
1407
+ hasRisk: true,
1408
+ riskType: "ENVIRONMENT_MARKERS",
1409
+ confidence: "LOW",
1410
+ details: `Environment markers suggest direct KMS usage: ${foundMarkers.join(", ")}`,
1411
+ remediation: "Review environment variables and ensure KMS access is gated through Gate SDK"
1412
+ };
1413
+ }
1414
+ return {
1415
+ hasRisk: false,
1416
+ confidence: "LOW",
1417
+ details: "No environment markers suggesting direct KMS usage"
1418
+ };
1419
+ }
1420
+ /**
1421
+ * Get current principal ARN (best-effort)
1422
+ */
1423
+ async getCurrentPrincipalArn() {
1424
+ try {
1425
+ const stsModule = await import('@aws-sdk/client-sts').catch(() => null);
1426
+ if (!stsModule || !stsModule.STSClient || !stsModule.GetCallerIdentityCommand) {
1427
+ return null;
1428
+ }
1429
+ const { STSClient, GetCallerIdentityCommand } = stsModule;
1430
+ const client = new STSClient({});
1431
+ const command = new GetCallerIdentityCommand({});
1432
+ const response = await client.send(command).catch(() => null);
1433
+ if (response?.Arn) {
1434
+ return response.Arn;
1435
+ }
1436
+ } catch (error) {
1437
+ }
1438
+ return null;
1439
+ }
1440
+ /**
1441
+ * Get highest confidence level from checks
1442
+ */
1443
+ getHighestConfidence(checks) {
1444
+ if (checks.some((c) => c.confidence === "HIGH")) {
1445
+ return "HIGH";
1446
+ }
1447
+ if (checks.some((c) => c.confidence === "MEDIUM")) {
1448
+ return "MEDIUM";
1449
+ }
1450
+ return "LOW";
1451
+ }
1452
+ /**
1453
+ * Build error message for HARD mode
1454
+ */
1455
+ buildErrorMessage(result) {
1456
+ const parts = [
1457
+ "[GATE ERROR] Hard enforcement mode blocked initialization:",
1458
+ ` - IAM permission risk: ${result.details}`,
1459
+ ` - Risk type: ${result.riskType}`,
1460
+ ` - Confidence: ${result.confidence}`,
1461
+ ` - Tenant ID: ${this.options.tenantId}`
1462
+ ];
1463
+ if (this.options.signerId) {
1464
+ parts.push(` - Signer ID: ${this.options.signerId}`);
1465
+ }
1466
+ if (this.options.environment) {
1467
+ parts.push(` - Environment: ${this.options.environment}`);
1468
+ }
1469
+ if (result.remediation) {
1470
+ parts.push(` - Remediation: ${result.remediation}`);
1471
+ }
1472
+ parts.push(" - See: https://docs.blockintel.ai/gate/IAM_HARDENING");
1473
+ parts.push(` - Override: Set allowInsecureKmsSignPermission=true (not recommended for production)`);
1474
+ return parts.join("\n");
1475
+ }
1476
+ /**
1477
+ * Log warning (SOFT mode or override set)
1478
+ */
1479
+ logWarning(result) {
1480
+ const logData = {
1481
+ level: "WARN",
1482
+ message: "IAM permission risk detected",
1483
+ tenantId: this.options.tenantId,
1484
+ signerId: this.options.signerId,
1485
+ environment: this.options.environment,
1486
+ enforcementMode: this.options.enforcementMode,
1487
+ riskType: result.riskType,
1488
+ confidence: result.confidence,
1489
+ details: result.details,
1490
+ remediation: result.remediation,
1491
+ documentation: "https://docs.blockintel.ai/gate/IAM_HARDENING"
1492
+ };
1493
+ console.warn("[GATE WARNING]", JSON.stringify(logData, null, 2));
1494
+ }
1495
+ };
929
1496
 
930
1497
  // src/client/GateClient.ts
931
1498
  var GateClient = class {
@@ -936,8 +1503,18 @@ var GateClient = class {
936
1503
  stepUpPoller;
937
1504
  circuitBreaker;
938
1505
  metrics;
1506
+ heartbeatManager;
1507
+ mode;
1508
+ onConnectionFailure;
939
1509
  constructor(config) {
940
1510
  this.config = config;
1511
+ const envMode = process.env.GATE_MODE;
1512
+ this.mode = envMode || config.mode || "SHADOW";
1513
+ if (config.onConnectionFailure) {
1514
+ this.onConnectionFailure = config.onConnectionFailure;
1515
+ } else {
1516
+ this.onConnectionFailure = this.mode === "SHADOW" ? "FAIL_OPEN" : "FAIL_CLOSED";
1517
+ }
941
1518
  if (config.auth.mode === "hmac") {
942
1519
  this.hmacSigner = new HmacSigner({
943
1520
  keyId: config.auth.keyId,
@@ -968,11 +1545,73 @@ var GateClient = class {
968
1545
  if (config.onMetrics) {
969
1546
  this.metrics.registerHook(config.onMetrics);
970
1547
  }
1548
+ if (config.local) {
1549
+ console.warn("[GATE CLIENT] LOCAL MODE ENABLED - Auth, heartbeat, and break-glass are disabled");
1550
+ this.heartbeatManager = null;
1551
+ } else {
1552
+ let controlPlaneUrl = config.baseUrl;
1553
+ if (controlPlaneUrl.includes("/defense")) {
1554
+ controlPlaneUrl = controlPlaneUrl.split("/defense")[0];
1555
+ }
1556
+ if (config.controlPlaneUrl) {
1557
+ controlPlaneUrl = config.controlPlaneUrl;
1558
+ }
1559
+ const heartbeatHttpClient = new HttpClient({
1560
+ baseUrl: controlPlaneUrl,
1561
+ timeoutMs: 5e3,
1562
+ // 5s timeout for heartbeat
1563
+ userAgent: config.userAgent
1564
+ });
1565
+ const initialSignerId = config.signerId ?? "trading-bot-signer";
1566
+ this.heartbeatManager = new HeartbeatManager({
1567
+ httpClient: heartbeatHttpClient,
1568
+ tenantId: config.tenantId,
1569
+ signerId: initialSignerId,
1570
+ environment: config.environment ?? "prod",
1571
+ refreshIntervalSeconds: config.heartbeatRefreshIntervalSeconds ?? 10
1572
+ });
1573
+ this.heartbeatManager.start();
1574
+ }
1575
+ if (!config.local) {
1576
+ const enforcementMode = config.enforcementMode || "SOFT";
1577
+ const allowInsecureKmsSignPermission = config.allowInsecureKmsSignPermission ?? enforcementMode === "SOFT";
1578
+ const riskChecker = new IamPermissionRiskChecker({
1579
+ tenantId: config.tenantId,
1580
+ signerId: config.signerId,
1581
+ environment: config.environment,
1582
+ enforcementMode,
1583
+ allowInsecureKmsSignPermission,
1584
+ kmsKeyIds: config.kmsKeyIds
1585
+ });
1586
+ riskChecker.checkSync();
1587
+ this.performIamRiskCheckAsync(riskChecker, enforcementMode).catch((error) => {
1588
+ if (enforcementMode === "SOFT" || allowInsecureKmsSignPermission) {
1589
+ console.warn("[GATE CLIENT] Async IAM risk check warning:", error instanceof Error ? error.message : String(error));
1590
+ } else {
1591
+ console.error("[GATE CLIENT] Async IAM risk check found risk after initialization:", error);
1592
+ }
1593
+ });
1594
+ }
1595
+ }
1596
+ /**
1597
+ * Perform async IAM permission risk check (non-blocking)
1598
+ *
1599
+ * Performs async IAM simulation check in background.
1600
+ * Logs warnings but doesn't block (initialization already completed).
1601
+ */
1602
+ async performIamRiskCheckAsync(riskChecker, enforcementMode) {
1603
+ try {
1604
+ await riskChecker.check();
1605
+ } catch (error) {
1606
+ console.warn("[GATE CLIENT] Async IAM risk check warning:", error instanceof Error ? error.message : String(error));
1607
+ }
971
1608
  }
972
1609
  /**
973
1610
  * Evaluate a transaction defense request
974
1611
  *
975
1612
  * Implements:
1613
+ * - Shadow Mode (SHADOW: monitor-only, ENFORCE: enforce decisions)
1614
+ * - Connection failure strategy (FAIL_OPEN vs FAIL_CLOSED)
976
1615
  * - Circuit breaker protection
977
1616
  * - Fail-safe modes (ALLOW_ON_TIMEOUT, BLOCK_ON_TIMEOUT, BLOCK_ON_ANOMALY)
978
1617
  * - Metrics collection
@@ -983,7 +1622,29 @@ var GateClient = class {
983
1622
  const timestampMs = req.timestampMs ?? nowMs();
984
1623
  const startTime = Date.now();
985
1624
  const failSafeMode = this.config.failSafeMode ?? "ALLOW_ON_TIMEOUT";
1625
+ const requestMode = req.mode || this.mode;
986
1626
  const executeRequest = async () => {
1627
+ if (!this.config.local && this.heartbeatManager && req.signingContext?.signerId) {
1628
+ this.heartbeatManager.updateSignerId(req.signingContext.signerId);
1629
+ }
1630
+ let heartbeatToken = null;
1631
+ if (!this.config.local && this.heartbeatManager) {
1632
+ heartbeatToken = this.heartbeatManager.getToken();
1633
+ if (!heartbeatToken) {
1634
+ const maxWaitMs = 2e3;
1635
+ const startTime2 = Date.now();
1636
+ while (!heartbeatToken && Date.now() - startTime2 < maxWaitMs) {
1637
+ await new Promise((resolve) => setTimeout(resolve, 50));
1638
+ heartbeatToken = this.heartbeatManager.getToken();
1639
+ }
1640
+ }
1641
+ if (!heartbeatToken) {
1642
+ throw new GateError(
1643
+ "HEARTBEAT_MISSING" /* HEARTBEAT_MISSING */,
1644
+ "Signing blocked: Heartbeat token is missing or expired. Gate must be alive and enforcing policy."
1645
+ );
1646
+ }
1647
+ }
987
1648
  const txIntent = { ...req.txIntent };
988
1649
  if (txIntent.to && !txIntent.toAddress) {
989
1650
  txIntent.toAddress = txIntent.to;
@@ -996,9 +1657,11 @@ var GateClient = class {
996
1657
  delete txIntent.from;
997
1658
  }
998
1659
  const signingContext = {
999
- ...req.signingContext,
1000
- actorPrincipal: req.signingContext?.actorPrincipal || req.signingContext?.signerId || "unknown"
1660
+ ...req.signingContext
1001
1661
  };
1662
+ if (heartbeatToken) {
1663
+ signingContext.heartbeatToken = heartbeatToken;
1664
+ }
1002
1665
  const provenance = ProvenanceProvider.getProvenance();
1003
1666
  if (provenance) {
1004
1667
  signingContext.caller = {
@@ -1009,20 +1672,36 @@ var GateClient = class {
1009
1672
  attestation: provenance.attestation
1010
1673
  };
1011
1674
  }
1012
- const body = {
1675
+ let body = {
1013
1676
  requestId,
1014
- tenantId: this.config.tenantId,
1015
1677
  timestampMs,
1016
1678
  txIntent,
1017
1679
  signingContext,
1018
1680
  // Add SDK info (required by Hot Path validation)
1681
+ // Note: Must match Python SDK name for consistent canonical JSON
1019
1682
  sdk: {
1020
- name: "blockintel-gate-sdk",
1683
+ name: "gate-sdk",
1021
1684
  version: "0.1.0"
1022
- }
1685
+ },
1686
+ // Add mode and connection failure strategy
1687
+ mode: requestMode,
1688
+ onConnectionFailure: this.onConnectionFailure
1023
1689
  };
1024
- let headers;
1025
- if (this.hmacSigner) {
1690
+ if (req.simulate === true) {
1691
+ body.simulate = true;
1692
+ }
1693
+ if (!this.config.local && this.config.breakglassToken) {
1694
+ signingContext.breakglassToken = this.config.breakglassToken;
1695
+ }
1696
+ let headers = {};
1697
+ if (this.config.local) {
1698
+ headers = {
1699
+ "Content-Type": "application/json"
1700
+ };
1701
+ console.log("[GATE CLIENT] LOCAL MODE - Skipping authentication");
1702
+ } else if (this.hmacSigner) {
1703
+ const { canonicalizeJson: canonicalizeJson2 } = await Promise.resolve().then(() => (init_canonicalJson(), canonicalJson_exports));
1704
+ const canonicalBodyJson = canonicalizeJson2(body);
1026
1705
  const hmacHeaders = await this.hmacSigner.signRequest({
1027
1706
  method: "POST",
1028
1707
  path: "/defense/evaluate",
@@ -1030,8 +1709,19 @@ var GateClient = class {
1030
1709
  timestampMs,
1031
1710
  requestId,
1032
1711
  body
1712
+ // Pass original body - HmacSigner will canonicalize it internally
1033
1713
  });
1034
1714
  headers = { ...hmacHeaders };
1715
+ 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));
1035
1725
  } else if (this.apiKeyAuth) {
1036
1726
  const apiKeyHeaders = this.apiKeyAuth.createHeaders({
1037
1727
  tenantId: this.config.tenantId,
@@ -1039,6 +1729,7 @@ var GateClient = class {
1039
1729
  requestId
1040
1730
  });
1041
1731
  headers = { ...apiKeyHeaders };
1732
+ console.error("[GATE CLIENT DEBUG] API key headers prepared:", JSON.stringify(headers, null, 2));
1042
1733
  } else {
1043
1734
  throw new Error("No authentication configured");
1044
1735
  }
@@ -1049,17 +1740,35 @@ var GateClient = class {
1049
1740
  body,
1050
1741
  requestId
1051
1742
  });
1052
- if (!apiResponse.success || !apiResponse.data) {
1743
+ let responseData;
1744
+ if (apiResponse.success === true && apiResponse.data) {
1745
+ responseData = apiResponse.data;
1746
+ } else if (apiResponse.success === false && apiResponse.error) {
1747
+ const error = apiResponse.error;
1748
+ throw new GateError(
1749
+ error.code || "SERVER_ERROR" /* SERVER_ERROR */,
1750
+ error.message || "Request failed",
1751
+ {
1752
+ status: error.status,
1753
+ correlationId: error.correlationId,
1754
+ requestId,
1755
+ details: error
1756
+ }
1757
+ );
1758
+ } else if (apiResponse.decision) {
1759
+ responseData = apiResponse;
1760
+ } else {
1053
1761
  throw new GateError(
1054
1762
  "INVALID_RESPONSE" /* INVALID_RESPONSE */,
1055
- "Invalid response format: expected { success: true, data: { ... } }",
1763
+ "Invalid response format: expected { success: true, data: { ... } } or unwrapped response",
1056
1764
  {
1057
1765
  requestId,
1058
1766
  details: apiResponse
1059
1767
  }
1060
1768
  );
1061
1769
  }
1062
- const responseData = apiResponse.data;
1770
+ const metadata = responseData.metadata || {};
1771
+ const simulationData = metadata.simulation;
1063
1772
  const result = {
1064
1773
  decision: responseData.decision,
1065
1774
  reasonCodes: responseData.reason_codes ?? responseData.reasonCodes ?? [],
@@ -1068,10 +1777,38 @@ var GateClient = class {
1068
1777
  stepUp: responseData.step_up ? {
1069
1778
  requestId: responseData.step_up.request_id ?? (responseData.stepUp?.requestId ?? ""),
1070
1779
  ttlSeconds: responseData.step_up.ttl_seconds ?? responseData.stepUp?.ttlSeconds
1071
- } : responseData.stepUp
1780
+ } : responseData.stepUp,
1781
+ enforced: responseData.enforced ?? requestMode === "ENFORCE",
1782
+ shadowWouldBlock: responseData.shadow_would_block ?? responseData.shadowWouldBlock ?? false,
1783
+ mode: responseData.mode ?? requestMode,
1784
+ ...simulationData ? {
1785
+ simulation: {
1786
+ willRevert: simulationData.willRevert ?? simulationData.will_revert ?? false,
1787
+ gasUsed: simulationData.gasUsed ?? simulationData.gas_used,
1788
+ balanceChanges: simulationData.balanceChanges ?? simulationData.balance_changes,
1789
+ errorReason: simulationData.errorReason ?? simulationData.error_reason
1790
+ },
1791
+ simulationLatencyMs: metadata.simulationLatencyMs ?? metadata.simulation_latency_ms
1792
+ } : {}
1072
1793
  };
1073
1794
  const latencyMs = Date.now() - startTime;
1074
1795
  if (result.decision === "BLOCK") {
1796
+ if (requestMode === "SHADOW") {
1797
+ console.warn("[GATE SHADOW MODE] Would have blocked transaction", {
1798
+ requestId,
1799
+ reasonCodes: result.reasonCodes,
1800
+ correlationId: result.correlationId,
1801
+ tenantId: this.config.tenantId,
1802
+ signerId: req.signingContext?.signerId
1803
+ });
1804
+ this.metrics.recordRequest("WOULD_BLOCK", latencyMs);
1805
+ return {
1806
+ ...result,
1807
+ decision: "ALLOW",
1808
+ enforced: false,
1809
+ shadowWouldBlock: true
1810
+ };
1811
+ }
1075
1812
  const receiptId = responseData.decision_id || requestId;
1076
1813
  const reasonCode = result.reasonCodes[0] || "POLICY_VIOLATION";
1077
1814
  this.metrics.recordRequest("BLOCK", latencyMs);
@@ -1116,6 +1853,31 @@ var GateClient = class {
1116
1853
  requestId
1117
1854
  );
1118
1855
  }
1856
+ const isConnectionFailure = error instanceof GateError && (error.code === "TIMEOUT" /* TIMEOUT */ || error.code === "SERVER_ERROR" /* SERVER_ERROR */) || error instanceof BlockIntelUnavailableError || error?.code === "ECONNREFUSED" || error?.code === "ENOTFOUND" || error?.code === "ETIMEDOUT";
1857
+ if (isConnectionFailure) {
1858
+ this.metrics.recordTimeout();
1859
+ if (this.onConnectionFailure === "FAIL_OPEN") {
1860
+ console.error("[GATE CONNECTION FAILURE] FAIL_OPEN mode - allowing transaction", {
1861
+ requestId,
1862
+ error: error.message,
1863
+ tenantId: this.config.tenantId,
1864
+ mode: requestMode
1865
+ });
1866
+ this.metrics.recordRequest("FAIL_OPEN", Date.now() - startTime);
1867
+ return {
1868
+ decision: "ALLOW",
1869
+ reasonCodes: ["GATE_HOTPATH_UNAVAILABLE"],
1870
+ correlationId: requestId,
1871
+ enforced: false,
1872
+ mode: requestMode
1873
+ };
1874
+ } else {
1875
+ throw new BlockIntelUnavailableError(
1876
+ `Signing blocked: Gate hot path unreachable (fail-closed). ${error.message}`,
1877
+ requestId
1878
+ );
1879
+ }
1880
+ }
1119
1881
  if (error instanceof GateError && error.code === "TIMEOUT" /* TIMEOUT */) {
1120
1882
  this.metrics.recordTimeout();
1121
1883
  const failSafeResult = this.handleFailSafe(failSafeMode, error, requestId);
@@ -1235,6 +1997,7 @@ exports.BlockIntelUnavailableError = BlockIntelUnavailableError;
1235
1997
  exports.GateClient = GateClient;
1236
1998
  exports.GateError = GateError;
1237
1999
  exports.GateErrorCode = GateErrorCode;
2000
+ exports.HeartbeatManager = HeartbeatManager;
1238
2001
  exports.ProvenanceProvider = ProvenanceProvider;
1239
2002
  exports.StepUpNotConfiguredError = StepUpNotConfiguredError;
1240
2003
  exports.createGateClient = createGateClient;