blockintel-gate-sdk 0.3.0 → 0.3.2

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
+ }
356
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
+ });
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,177 @@ 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
+ };
929
1252
 
930
1253
  // src/client/GateClient.ts
931
1254
  var GateClient = class {
@@ -936,8 +1259,18 @@ var GateClient = class {
936
1259
  stepUpPoller;
937
1260
  circuitBreaker;
938
1261
  metrics;
1262
+ heartbeatManager;
1263
+ mode;
1264
+ onConnectionFailure;
939
1265
  constructor(config) {
940
1266
  this.config = config;
1267
+ const envMode = process.env.GATE_MODE;
1268
+ this.mode = envMode || config.mode || "SHADOW";
1269
+ if (config.onConnectionFailure) {
1270
+ this.onConnectionFailure = config.onConnectionFailure;
1271
+ } else {
1272
+ this.onConnectionFailure = this.mode === "SHADOW" ? "FAIL_OPEN" : "FAIL_CLOSED";
1273
+ }
941
1274
  if (config.auth.mode === "hmac") {
942
1275
  this.hmacSigner = new HmacSigner({
943
1276
  keyId: config.auth.keyId,
@@ -968,11 +1301,40 @@ var GateClient = class {
968
1301
  if (config.onMetrics) {
969
1302
  this.metrics.registerHook(config.onMetrics);
970
1303
  }
1304
+ if (config.local) {
1305
+ console.warn("[GATE CLIENT] LOCAL MODE ENABLED - Auth, heartbeat, and break-glass are disabled");
1306
+ this.heartbeatManager = null;
1307
+ } else {
1308
+ let controlPlaneUrl = config.baseUrl;
1309
+ if (controlPlaneUrl.includes("/defense")) {
1310
+ controlPlaneUrl = controlPlaneUrl.split("/defense")[0];
1311
+ }
1312
+ if (config.controlPlaneUrl) {
1313
+ controlPlaneUrl = config.controlPlaneUrl;
1314
+ }
1315
+ const heartbeatHttpClient = new HttpClient({
1316
+ baseUrl: controlPlaneUrl,
1317
+ timeoutMs: 5e3,
1318
+ // 5s timeout for heartbeat
1319
+ userAgent: config.userAgent
1320
+ });
1321
+ const initialSignerId = config.signerId ?? "trading-bot-signer";
1322
+ this.heartbeatManager = new HeartbeatManager({
1323
+ httpClient: heartbeatHttpClient,
1324
+ tenantId: config.tenantId,
1325
+ signerId: initialSignerId,
1326
+ environment: config.environment ?? "prod",
1327
+ refreshIntervalSeconds: config.heartbeatRefreshIntervalSeconds ?? 10
1328
+ });
1329
+ this.heartbeatManager.start();
1330
+ }
971
1331
  }
972
1332
  /**
973
1333
  * Evaluate a transaction defense request
974
1334
  *
975
1335
  * Implements:
1336
+ * - Shadow Mode (SHADOW: monitor-only, ENFORCE: enforce decisions)
1337
+ * - Connection failure strategy (FAIL_OPEN vs FAIL_CLOSED)
976
1338
  * - Circuit breaker protection
977
1339
  * - Fail-safe modes (ALLOW_ON_TIMEOUT, BLOCK_ON_TIMEOUT, BLOCK_ON_ANOMALY)
978
1340
  * - Metrics collection
@@ -983,7 +1345,29 @@ var GateClient = class {
983
1345
  const timestampMs = req.timestampMs ?? nowMs();
984
1346
  const startTime = Date.now();
985
1347
  const failSafeMode = this.config.failSafeMode ?? "ALLOW_ON_TIMEOUT";
1348
+ const requestMode = req.mode || this.mode;
986
1349
  const executeRequest = async () => {
1350
+ if (!this.config.local && this.heartbeatManager && req.signingContext?.signerId) {
1351
+ this.heartbeatManager.updateSignerId(req.signingContext.signerId);
1352
+ }
1353
+ let heartbeatToken = null;
1354
+ if (!this.config.local && this.heartbeatManager) {
1355
+ heartbeatToken = this.heartbeatManager.getToken();
1356
+ if (!heartbeatToken) {
1357
+ const maxWaitMs = 2e3;
1358
+ const startTime2 = Date.now();
1359
+ while (!heartbeatToken && Date.now() - startTime2 < maxWaitMs) {
1360
+ await new Promise((resolve) => setTimeout(resolve, 50));
1361
+ heartbeatToken = this.heartbeatManager.getToken();
1362
+ }
1363
+ }
1364
+ if (!heartbeatToken) {
1365
+ throw new GateError(
1366
+ "HEARTBEAT_MISSING" /* HEARTBEAT_MISSING */,
1367
+ "Signing blocked: Heartbeat token is missing or expired. Gate must be alive and enforcing policy."
1368
+ );
1369
+ }
1370
+ }
987
1371
  const txIntent = { ...req.txIntent };
988
1372
  if (txIntent.to && !txIntent.toAddress) {
989
1373
  txIntent.toAddress = txIntent.to;
@@ -996,9 +1380,11 @@ var GateClient = class {
996
1380
  delete txIntent.from;
997
1381
  }
998
1382
  const signingContext = {
999
- ...req.signingContext,
1000
- actorPrincipal: req.signingContext?.actorPrincipal || req.signingContext?.signerId || "unknown"
1383
+ ...req.signingContext
1001
1384
  };
1385
+ if (heartbeatToken) {
1386
+ signingContext.heartbeatToken = heartbeatToken;
1387
+ }
1002
1388
  const provenance = ProvenanceProvider.getProvenance();
1003
1389
  if (provenance) {
1004
1390
  signingContext.caller = {
@@ -1009,20 +1395,36 @@ var GateClient = class {
1009
1395
  attestation: provenance.attestation
1010
1396
  };
1011
1397
  }
1012
- const body = {
1398
+ let body = {
1013
1399
  requestId,
1014
- tenantId: this.config.tenantId,
1015
1400
  timestampMs,
1016
1401
  txIntent,
1017
1402
  signingContext,
1018
1403
  // Add SDK info (required by Hot Path validation)
1404
+ // Note: Must match Python SDK name for consistent canonical JSON
1019
1405
  sdk: {
1020
- name: "blockintel-gate-sdk",
1406
+ name: "gate-sdk",
1021
1407
  version: "0.1.0"
1022
- }
1408
+ },
1409
+ // Add mode and connection failure strategy
1410
+ mode: requestMode,
1411
+ onConnectionFailure: this.onConnectionFailure
1023
1412
  };
1024
- let headers;
1025
- if (this.hmacSigner) {
1413
+ if (req.simulate === true) {
1414
+ body.simulate = true;
1415
+ }
1416
+ if (!this.config.local && this.config.breakglassToken) {
1417
+ signingContext.breakglassToken = this.config.breakglassToken;
1418
+ }
1419
+ let headers = {};
1420
+ if (this.config.local) {
1421
+ headers = {
1422
+ "Content-Type": "application/json"
1423
+ };
1424
+ console.log("[GATE CLIENT] LOCAL MODE - Skipping authentication");
1425
+ } else if (this.hmacSigner) {
1426
+ const { canonicalizeJson: canonicalizeJson2 } = await Promise.resolve().then(() => (init_canonicalJson(), canonicalJson_exports));
1427
+ const canonicalBodyJson = canonicalizeJson2(body);
1026
1428
  const hmacHeaders = await this.hmacSigner.signRequest({
1027
1429
  method: "POST",
1028
1430
  path: "/defense/evaluate",
@@ -1030,8 +1432,19 @@ var GateClient = class {
1030
1432
  timestampMs,
1031
1433
  requestId,
1032
1434
  body
1435
+ // Pass original body - HmacSigner will canonicalize it internally
1033
1436
  });
1034
1437
  headers = { ...hmacHeaders };
1438
+ body.__canonicalJson = canonicalBodyJson;
1439
+ const debugHeaders = {};
1440
+ Object.entries(headers).forEach(([key, value]) => {
1441
+ if (key.toLowerCase().includes("signature")) {
1442
+ debugHeaders[key] = value.substring(0, 8) + "...";
1443
+ } else {
1444
+ debugHeaders[key] = value;
1445
+ }
1446
+ });
1447
+ console.error("[GATE CLIENT DEBUG] HMAC headers prepared:", JSON.stringify(debugHeaders, null, 2));
1035
1448
  } else if (this.apiKeyAuth) {
1036
1449
  const apiKeyHeaders = this.apiKeyAuth.createHeaders({
1037
1450
  tenantId: this.config.tenantId,
@@ -1039,6 +1452,7 @@ var GateClient = class {
1039
1452
  requestId
1040
1453
  });
1041
1454
  headers = { ...apiKeyHeaders };
1455
+ console.error("[GATE CLIENT DEBUG] API key headers prepared:", JSON.stringify(headers, null, 2));
1042
1456
  } else {
1043
1457
  throw new Error("No authentication configured");
1044
1458
  }
@@ -1049,17 +1463,35 @@ var GateClient = class {
1049
1463
  body,
1050
1464
  requestId
1051
1465
  });
1052
- if (!apiResponse.success || !apiResponse.data) {
1466
+ let responseData;
1467
+ if (apiResponse.success === true && apiResponse.data) {
1468
+ responseData = apiResponse.data;
1469
+ } else if (apiResponse.success === false && apiResponse.error) {
1470
+ const error = apiResponse.error;
1471
+ throw new GateError(
1472
+ error.code || "SERVER_ERROR" /* SERVER_ERROR */,
1473
+ error.message || "Request failed",
1474
+ {
1475
+ status: error.status,
1476
+ correlationId: error.correlationId,
1477
+ requestId,
1478
+ details: error
1479
+ }
1480
+ );
1481
+ } else if (apiResponse.decision) {
1482
+ responseData = apiResponse;
1483
+ } else {
1053
1484
  throw new GateError(
1054
1485
  "INVALID_RESPONSE" /* INVALID_RESPONSE */,
1055
- "Invalid response format: expected { success: true, data: { ... } }",
1486
+ "Invalid response format: expected { success: true, data: { ... } } or unwrapped response",
1056
1487
  {
1057
1488
  requestId,
1058
1489
  details: apiResponse
1059
1490
  }
1060
1491
  );
1061
1492
  }
1062
- const responseData = apiResponse.data;
1493
+ const metadata = responseData.metadata || {};
1494
+ const simulationData = metadata.simulation;
1063
1495
  const result = {
1064
1496
  decision: responseData.decision,
1065
1497
  reasonCodes: responseData.reason_codes ?? responseData.reasonCodes ?? [],
@@ -1068,10 +1500,38 @@ var GateClient = class {
1068
1500
  stepUp: responseData.step_up ? {
1069
1501
  requestId: responseData.step_up.request_id ?? (responseData.stepUp?.requestId ?? ""),
1070
1502
  ttlSeconds: responseData.step_up.ttl_seconds ?? responseData.stepUp?.ttlSeconds
1071
- } : responseData.stepUp
1503
+ } : responseData.stepUp,
1504
+ enforced: responseData.enforced ?? requestMode === "ENFORCE",
1505
+ shadowWouldBlock: responseData.shadow_would_block ?? responseData.shadowWouldBlock ?? false,
1506
+ mode: responseData.mode ?? requestMode,
1507
+ ...simulationData ? {
1508
+ simulation: {
1509
+ willRevert: simulationData.willRevert ?? simulationData.will_revert ?? false,
1510
+ gasUsed: simulationData.gasUsed ?? simulationData.gas_used,
1511
+ balanceChanges: simulationData.balanceChanges ?? simulationData.balance_changes,
1512
+ errorReason: simulationData.errorReason ?? simulationData.error_reason
1513
+ },
1514
+ simulationLatencyMs: metadata.simulationLatencyMs ?? metadata.simulation_latency_ms
1515
+ } : {}
1072
1516
  };
1073
1517
  const latencyMs = Date.now() - startTime;
1074
1518
  if (result.decision === "BLOCK") {
1519
+ if (requestMode === "SHADOW") {
1520
+ console.warn("[GATE SHADOW MODE] Would have blocked transaction", {
1521
+ requestId,
1522
+ reasonCodes: result.reasonCodes,
1523
+ correlationId: result.correlationId,
1524
+ tenantId: this.config.tenantId,
1525
+ signerId: req.signingContext?.signerId
1526
+ });
1527
+ this.metrics.recordRequest("WOULD_BLOCK", latencyMs);
1528
+ return {
1529
+ ...result,
1530
+ decision: "ALLOW",
1531
+ enforced: false,
1532
+ shadowWouldBlock: true
1533
+ };
1534
+ }
1075
1535
  const receiptId = responseData.decision_id || requestId;
1076
1536
  const reasonCode = result.reasonCodes[0] || "POLICY_VIOLATION";
1077
1537
  this.metrics.recordRequest("BLOCK", latencyMs);
@@ -1116,6 +1576,31 @@ var GateClient = class {
1116
1576
  requestId
1117
1577
  );
1118
1578
  }
1579
+ 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";
1580
+ if (isConnectionFailure) {
1581
+ this.metrics.recordTimeout();
1582
+ if (this.onConnectionFailure === "FAIL_OPEN") {
1583
+ console.error("[GATE CONNECTION FAILURE] FAIL_OPEN mode - allowing transaction", {
1584
+ requestId,
1585
+ error: error.message,
1586
+ tenantId: this.config.tenantId,
1587
+ mode: requestMode
1588
+ });
1589
+ this.metrics.recordRequest("FAIL_OPEN", Date.now() - startTime);
1590
+ return {
1591
+ decision: "ALLOW",
1592
+ reasonCodes: ["GATE_HOTPATH_UNAVAILABLE"],
1593
+ correlationId: requestId,
1594
+ enforced: false,
1595
+ mode: requestMode
1596
+ };
1597
+ } else {
1598
+ throw new BlockIntelUnavailableError(
1599
+ `Signing blocked: Gate hot path unreachable (fail-closed). ${error.message}`,
1600
+ requestId
1601
+ );
1602
+ }
1603
+ }
1119
1604
  if (error instanceof GateError && error.code === "TIMEOUT" /* TIMEOUT */) {
1120
1605
  this.metrics.recordTimeout();
1121
1606
  const failSafeResult = this.handleFailSafe(failSafeMode, error, requestId);
@@ -1235,6 +1720,7 @@ exports.BlockIntelUnavailableError = BlockIntelUnavailableError;
1235
1720
  exports.GateClient = GateClient;
1236
1721
  exports.GateError = GateError;
1237
1722
  exports.GateErrorCode = GateErrorCode;
1723
+ exports.HeartbeatManager = HeartbeatManager;
1238
1724
  exports.ProvenanceProvider = ProvenanceProvider;
1239
1725
  exports.StepUpNotConfiguredError = StepUpNotConfiguredError;
1240
1726
  exports.createGateClient = createGateClient;