blockintel-gate-sdk 0.3.1 → 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.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
+ }
352
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
+ });
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,177 @@ 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
+ };
925
1248
 
926
1249
  // src/client/GateClient.ts
927
1250
  var GateClient = class {
@@ -932,8 +1255,18 @@ var GateClient = class {
932
1255
  stepUpPoller;
933
1256
  circuitBreaker;
934
1257
  metrics;
1258
+ heartbeatManager;
1259
+ mode;
1260
+ onConnectionFailure;
935
1261
  constructor(config) {
936
1262
  this.config = config;
1263
+ const envMode = process.env.GATE_MODE;
1264
+ this.mode = envMode || config.mode || "SHADOW";
1265
+ if (config.onConnectionFailure) {
1266
+ this.onConnectionFailure = config.onConnectionFailure;
1267
+ } else {
1268
+ this.onConnectionFailure = this.mode === "SHADOW" ? "FAIL_OPEN" : "FAIL_CLOSED";
1269
+ }
937
1270
  if (config.auth.mode === "hmac") {
938
1271
  this.hmacSigner = new HmacSigner({
939
1272
  keyId: config.auth.keyId,
@@ -964,11 +1297,40 @@ var GateClient = class {
964
1297
  if (config.onMetrics) {
965
1298
  this.metrics.registerHook(config.onMetrics);
966
1299
  }
1300
+ if (config.local) {
1301
+ console.warn("[GATE CLIENT] LOCAL MODE ENABLED - Auth, heartbeat, and break-glass are disabled");
1302
+ this.heartbeatManager = null;
1303
+ } else {
1304
+ let controlPlaneUrl = config.baseUrl;
1305
+ if (controlPlaneUrl.includes("/defense")) {
1306
+ controlPlaneUrl = controlPlaneUrl.split("/defense")[0];
1307
+ }
1308
+ if (config.controlPlaneUrl) {
1309
+ controlPlaneUrl = config.controlPlaneUrl;
1310
+ }
1311
+ const heartbeatHttpClient = new HttpClient({
1312
+ baseUrl: controlPlaneUrl,
1313
+ timeoutMs: 5e3,
1314
+ // 5s timeout for heartbeat
1315
+ userAgent: config.userAgent
1316
+ });
1317
+ const initialSignerId = config.signerId ?? "trading-bot-signer";
1318
+ this.heartbeatManager = new HeartbeatManager({
1319
+ httpClient: heartbeatHttpClient,
1320
+ tenantId: config.tenantId,
1321
+ signerId: initialSignerId,
1322
+ environment: config.environment ?? "prod",
1323
+ refreshIntervalSeconds: config.heartbeatRefreshIntervalSeconds ?? 10
1324
+ });
1325
+ this.heartbeatManager.start();
1326
+ }
967
1327
  }
968
1328
  /**
969
1329
  * Evaluate a transaction defense request
970
1330
  *
971
1331
  * Implements:
1332
+ * - Shadow Mode (SHADOW: monitor-only, ENFORCE: enforce decisions)
1333
+ * - Connection failure strategy (FAIL_OPEN vs FAIL_CLOSED)
972
1334
  * - Circuit breaker protection
973
1335
  * - Fail-safe modes (ALLOW_ON_TIMEOUT, BLOCK_ON_TIMEOUT, BLOCK_ON_ANOMALY)
974
1336
  * - Metrics collection
@@ -979,7 +1341,29 @@ var GateClient = class {
979
1341
  const timestampMs = req.timestampMs ?? nowMs();
980
1342
  const startTime = Date.now();
981
1343
  const failSafeMode = this.config.failSafeMode ?? "ALLOW_ON_TIMEOUT";
1344
+ const requestMode = req.mode || this.mode;
982
1345
  const executeRequest = async () => {
1346
+ if (!this.config.local && this.heartbeatManager && req.signingContext?.signerId) {
1347
+ this.heartbeatManager.updateSignerId(req.signingContext.signerId);
1348
+ }
1349
+ let heartbeatToken = null;
1350
+ if (!this.config.local && this.heartbeatManager) {
1351
+ heartbeatToken = this.heartbeatManager.getToken();
1352
+ if (!heartbeatToken) {
1353
+ const maxWaitMs = 2e3;
1354
+ const startTime2 = Date.now();
1355
+ while (!heartbeatToken && Date.now() - startTime2 < maxWaitMs) {
1356
+ await new Promise((resolve) => setTimeout(resolve, 50));
1357
+ heartbeatToken = this.heartbeatManager.getToken();
1358
+ }
1359
+ }
1360
+ if (!heartbeatToken) {
1361
+ throw new GateError(
1362
+ "HEARTBEAT_MISSING" /* HEARTBEAT_MISSING */,
1363
+ "Signing blocked: Heartbeat token is missing or expired. Gate must be alive and enforcing policy."
1364
+ );
1365
+ }
1366
+ }
983
1367
  const txIntent = { ...req.txIntent };
984
1368
  if (txIntent.to && !txIntent.toAddress) {
985
1369
  txIntent.toAddress = txIntent.to;
@@ -992,9 +1376,11 @@ var GateClient = class {
992
1376
  delete txIntent.from;
993
1377
  }
994
1378
  const signingContext = {
995
- ...req.signingContext,
996
- actorPrincipal: req.signingContext?.actorPrincipal || req.signingContext?.signerId || "unknown"
1379
+ ...req.signingContext
997
1380
  };
1381
+ if (heartbeatToken) {
1382
+ signingContext.heartbeatToken = heartbeatToken;
1383
+ }
998
1384
  const provenance = ProvenanceProvider.getProvenance();
999
1385
  if (provenance) {
1000
1386
  signingContext.caller = {
@@ -1005,20 +1391,36 @@ var GateClient = class {
1005
1391
  attestation: provenance.attestation
1006
1392
  };
1007
1393
  }
1008
- const body = {
1394
+ let body = {
1009
1395
  requestId,
1010
- tenantId: this.config.tenantId,
1011
1396
  timestampMs,
1012
1397
  txIntent,
1013
1398
  signingContext,
1014
1399
  // Add SDK info (required by Hot Path validation)
1400
+ // Note: Must match Python SDK name for consistent canonical JSON
1015
1401
  sdk: {
1016
- name: "blockintel-gate-sdk",
1402
+ name: "gate-sdk",
1017
1403
  version: "0.1.0"
1018
- }
1404
+ },
1405
+ // Add mode and connection failure strategy
1406
+ mode: requestMode,
1407
+ onConnectionFailure: this.onConnectionFailure
1019
1408
  };
1020
- let headers;
1021
- if (this.hmacSigner) {
1409
+ if (req.simulate === true) {
1410
+ body.simulate = true;
1411
+ }
1412
+ if (!this.config.local && this.config.breakglassToken) {
1413
+ signingContext.breakglassToken = this.config.breakglassToken;
1414
+ }
1415
+ let headers = {};
1416
+ if (this.config.local) {
1417
+ headers = {
1418
+ "Content-Type": "application/json"
1419
+ };
1420
+ console.log("[GATE CLIENT] LOCAL MODE - Skipping authentication");
1421
+ } else if (this.hmacSigner) {
1422
+ const { canonicalizeJson: canonicalizeJson2 } = await Promise.resolve().then(() => (init_canonicalJson(), canonicalJson_exports));
1423
+ const canonicalBodyJson = canonicalizeJson2(body);
1022
1424
  const hmacHeaders = await this.hmacSigner.signRequest({
1023
1425
  method: "POST",
1024
1426
  path: "/defense/evaluate",
@@ -1026,8 +1428,19 @@ var GateClient = class {
1026
1428
  timestampMs,
1027
1429
  requestId,
1028
1430
  body
1431
+ // Pass original body - HmacSigner will canonicalize it internally
1029
1432
  });
1030
1433
  headers = { ...hmacHeaders };
1434
+ body.__canonicalJson = canonicalBodyJson;
1435
+ const debugHeaders = {};
1436
+ Object.entries(headers).forEach(([key, value]) => {
1437
+ if (key.toLowerCase().includes("signature")) {
1438
+ debugHeaders[key] = value.substring(0, 8) + "...";
1439
+ } else {
1440
+ debugHeaders[key] = value;
1441
+ }
1442
+ });
1443
+ console.error("[GATE CLIENT DEBUG] HMAC headers prepared:", JSON.stringify(debugHeaders, null, 2));
1031
1444
  } else if (this.apiKeyAuth) {
1032
1445
  const apiKeyHeaders = this.apiKeyAuth.createHeaders({
1033
1446
  tenantId: this.config.tenantId,
@@ -1035,6 +1448,7 @@ var GateClient = class {
1035
1448
  requestId
1036
1449
  });
1037
1450
  headers = { ...apiKeyHeaders };
1451
+ console.error("[GATE CLIENT DEBUG] API key headers prepared:", JSON.stringify(headers, null, 2));
1038
1452
  } else {
1039
1453
  throw new Error("No authentication configured");
1040
1454
  }
@@ -1045,17 +1459,35 @@ var GateClient = class {
1045
1459
  body,
1046
1460
  requestId
1047
1461
  });
1048
- if (!apiResponse.success || !apiResponse.data) {
1462
+ let responseData;
1463
+ if (apiResponse.success === true && apiResponse.data) {
1464
+ responseData = apiResponse.data;
1465
+ } else if (apiResponse.success === false && apiResponse.error) {
1466
+ const error = apiResponse.error;
1467
+ throw new GateError(
1468
+ error.code || "SERVER_ERROR" /* SERVER_ERROR */,
1469
+ error.message || "Request failed",
1470
+ {
1471
+ status: error.status,
1472
+ correlationId: error.correlationId,
1473
+ requestId,
1474
+ details: error
1475
+ }
1476
+ );
1477
+ } else if (apiResponse.decision) {
1478
+ responseData = apiResponse;
1479
+ } else {
1049
1480
  throw new GateError(
1050
1481
  "INVALID_RESPONSE" /* INVALID_RESPONSE */,
1051
- "Invalid response format: expected { success: true, data: { ... } }",
1482
+ "Invalid response format: expected { success: true, data: { ... } } or unwrapped response",
1052
1483
  {
1053
1484
  requestId,
1054
1485
  details: apiResponse
1055
1486
  }
1056
1487
  );
1057
1488
  }
1058
- const responseData = apiResponse.data;
1489
+ const metadata = responseData.metadata || {};
1490
+ const simulationData = metadata.simulation;
1059
1491
  const result = {
1060
1492
  decision: responseData.decision,
1061
1493
  reasonCodes: responseData.reason_codes ?? responseData.reasonCodes ?? [],
@@ -1064,10 +1496,38 @@ var GateClient = class {
1064
1496
  stepUp: responseData.step_up ? {
1065
1497
  requestId: responseData.step_up.request_id ?? (responseData.stepUp?.requestId ?? ""),
1066
1498
  ttlSeconds: responseData.step_up.ttl_seconds ?? responseData.stepUp?.ttlSeconds
1067
- } : responseData.stepUp
1499
+ } : responseData.stepUp,
1500
+ enforced: responseData.enforced ?? requestMode === "ENFORCE",
1501
+ shadowWouldBlock: responseData.shadow_would_block ?? responseData.shadowWouldBlock ?? false,
1502
+ mode: responseData.mode ?? requestMode,
1503
+ ...simulationData ? {
1504
+ simulation: {
1505
+ willRevert: simulationData.willRevert ?? simulationData.will_revert ?? false,
1506
+ gasUsed: simulationData.gasUsed ?? simulationData.gas_used,
1507
+ balanceChanges: simulationData.balanceChanges ?? simulationData.balance_changes,
1508
+ errorReason: simulationData.errorReason ?? simulationData.error_reason
1509
+ },
1510
+ simulationLatencyMs: metadata.simulationLatencyMs ?? metadata.simulation_latency_ms
1511
+ } : {}
1068
1512
  };
1069
1513
  const latencyMs = Date.now() - startTime;
1070
1514
  if (result.decision === "BLOCK") {
1515
+ if (requestMode === "SHADOW") {
1516
+ console.warn("[GATE SHADOW MODE] Would have blocked transaction", {
1517
+ requestId,
1518
+ reasonCodes: result.reasonCodes,
1519
+ correlationId: result.correlationId,
1520
+ tenantId: this.config.tenantId,
1521
+ signerId: req.signingContext?.signerId
1522
+ });
1523
+ this.metrics.recordRequest("WOULD_BLOCK", latencyMs);
1524
+ return {
1525
+ ...result,
1526
+ decision: "ALLOW",
1527
+ enforced: false,
1528
+ shadowWouldBlock: true
1529
+ };
1530
+ }
1071
1531
  const receiptId = responseData.decision_id || requestId;
1072
1532
  const reasonCode = result.reasonCodes[0] || "POLICY_VIOLATION";
1073
1533
  this.metrics.recordRequest("BLOCK", latencyMs);
@@ -1112,6 +1572,31 @@ var GateClient = class {
1112
1572
  requestId
1113
1573
  );
1114
1574
  }
1575
+ 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";
1576
+ if (isConnectionFailure) {
1577
+ this.metrics.recordTimeout();
1578
+ if (this.onConnectionFailure === "FAIL_OPEN") {
1579
+ console.error("[GATE CONNECTION FAILURE] FAIL_OPEN mode - allowing transaction", {
1580
+ requestId,
1581
+ error: error.message,
1582
+ tenantId: this.config.tenantId,
1583
+ mode: requestMode
1584
+ });
1585
+ this.metrics.recordRequest("FAIL_OPEN", Date.now() - startTime);
1586
+ return {
1587
+ decision: "ALLOW",
1588
+ reasonCodes: ["GATE_HOTPATH_UNAVAILABLE"],
1589
+ correlationId: requestId,
1590
+ enforced: false,
1591
+ mode: requestMode
1592
+ };
1593
+ } else {
1594
+ throw new BlockIntelUnavailableError(
1595
+ `Signing blocked: Gate hot path unreachable (fail-closed). ${error.message}`,
1596
+ requestId
1597
+ );
1598
+ }
1599
+ }
1115
1600
  if (error instanceof GateError && error.code === "TIMEOUT" /* TIMEOUT */) {
1116
1601
  this.metrics.recordTimeout();
1117
1602
  const failSafeResult = this.handleFailSafe(failSafeMode, error, requestId);
@@ -1224,6 +1709,6 @@ function createGateClient(config) {
1224
1709
  return new GateClient(config);
1225
1710
  }
1226
1711
 
1227
- export { BlockIntelAuthError, BlockIntelBlockedError, BlockIntelStepUpRequiredError, BlockIntelUnavailableError, GateClient, GateError, GateErrorCode, ProvenanceProvider, StepUpNotConfiguredError, createGateClient, GateClient as default, wrapKmsClient };
1712
+ export { BlockIntelAuthError, BlockIntelBlockedError, BlockIntelStepUpRequiredError, BlockIntelUnavailableError, GateClient, GateError, GateErrorCode, HeartbeatManager, ProvenanceProvider, StepUpNotConfiguredError, createGateClient, GateClient as default, wrapKmsClient };
1228
1713
  //# sourceMappingURL=index.js.map
1229
1714
  //# sourceMappingURL=index.js.map