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