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