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