blockintel-gate-sdk 0.3.10 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{contracts-KKk945Ox.d.cts → contracts-Dxb9vt_M.d.cts} +12 -0
- package/dist/{contracts-KKk945Ox.d.ts → contracts-Dxb9vt_M.d.ts} +12 -0
- package/dist/index.cjs +270 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +69 -20
- package/dist/index.d.ts +69 -20
- package/dist/index.js +270 -105
- package/dist/index.js.map +1 -1
- package/dist/pilot/index.cjs +269 -104
- package/dist/pilot/index.cjs.map +1 -1
- package/dist/pilot/index.d.cts +1 -1
- package/dist/pilot/index.d.ts +1 -1
- package/dist/pilot/index.js +269 -104
- package/dist/pilot/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -169,6 +169,18 @@ interface DefenseEvaluateResponseV2 {
|
|
|
169
169
|
* Gate mode used for this evaluation
|
|
170
170
|
*/
|
|
171
171
|
mode?: GateMode;
|
|
172
|
+
/**
|
|
173
|
+
* Gate receipt (when HARD_KMS_GATEWAY (or HARD_KMS_ATTESTED, deprecated) or receipt requested). Required for KMS signing in receipt-required mode.
|
|
174
|
+
*/
|
|
175
|
+
receipt?: Record<string, unknown>;
|
|
176
|
+
/**
|
|
177
|
+
* Decision hash from receipt (SHA256 of canonical receipt payload). Used for receipt-required KMS flow.
|
|
178
|
+
*/
|
|
179
|
+
decisionHash?: string;
|
|
180
|
+
/**
|
|
181
|
+
* Receipt signature (HS256:base64). Used for receipt-required KMS flow.
|
|
182
|
+
*/
|
|
183
|
+
receiptSignature?: string;
|
|
172
184
|
/**
|
|
173
185
|
* Metadata (evaluation latency, simulation, policy hash for pinning)
|
|
174
186
|
*/
|
|
@@ -169,6 +169,18 @@ interface DefenseEvaluateResponseV2 {
|
|
|
169
169
|
* Gate mode used for this evaluation
|
|
170
170
|
*/
|
|
171
171
|
mode?: GateMode;
|
|
172
|
+
/**
|
|
173
|
+
* Gate receipt (when HARD_KMS_GATEWAY (or HARD_KMS_ATTESTED, deprecated) or receipt requested). Required for KMS signing in receipt-required mode.
|
|
174
|
+
*/
|
|
175
|
+
receipt?: Record<string, unknown>;
|
|
176
|
+
/**
|
|
177
|
+
* Decision hash from receipt (SHA256 of canonical receipt payload). Used for receipt-required KMS flow.
|
|
178
|
+
*/
|
|
179
|
+
decisionHash?: string;
|
|
180
|
+
/**
|
|
181
|
+
* Receipt signature (HS256:base64). Used for receipt-required KMS flow.
|
|
182
|
+
*/
|
|
183
|
+
receiptSignature?: string;
|
|
172
184
|
/**
|
|
173
185
|
* Metadata (evaluation latency, simulation, policy hash for pinning)
|
|
174
186
|
*/
|
package/dist/index.cjs
CHANGED
|
@@ -1047,13 +1047,21 @@ function computeTxDigest(binding) {
|
|
|
1047
1047
|
return crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
1048
1048
|
}
|
|
1049
1049
|
|
|
1050
|
+
// src/metrics/GateMetricsSink.ts
|
|
1051
|
+
var noOpMetricsSink = {
|
|
1052
|
+
emit() {
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1050
1056
|
// src/kms/wrapAwsSdkV3KmsClient.ts
|
|
1051
1057
|
function wrapKmsClient(kmsClient, gateClient, options = {}) {
|
|
1052
1058
|
const defaultOptions = {
|
|
1053
1059
|
mode: options.mode || "enforce",
|
|
1060
|
+
requireReceiptForSign: options.requireReceiptForSign ?? false,
|
|
1054
1061
|
onDecision: options.onDecision || (() => {
|
|
1055
1062
|
}),
|
|
1056
|
-
extractTxIntent: options.extractTxIntent || defaultExtractTxIntent
|
|
1063
|
+
extractTxIntent: options.extractTxIntent || defaultExtractTxIntent,
|
|
1064
|
+
metricsSink: options.metricsSink ?? noOpMetricsSink
|
|
1057
1065
|
};
|
|
1058
1066
|
const wrapped = new Proxy(kmsClient, {
|
|
1059
1067
|
get(target, prop, receiver) {
|
|
@@ -1094,12 +1102,39 @@ function defaultExtractTxIntent(command) {
|
|
|
1094
1102
|
// Backward compatibility
|
|
1095
1103
|
};
|
|
1096
1104
|
}
|
|
1105
|
+
function buildMetricLabels(gateClient, command, signerId, txIntent) {
|
|
1106
|
+
const config = gateClient.config;
|
|
1107
|
+
const keyId = command.input?.KeyId ?? command.KeyId;
|
|
1108
|
+
return {
|
|
1109
|
+
tenantId: config?.tenantId,
|
|
1110
|
+
signerId: signerId || void 0,
|
|
1111
|
+
adoptionStage: config?.adoptionStage ?? process.env.GATE_ADOPTION_STAGE,
|
|
1112
|
+
env: config?.env ?? process.env.GATE_ENV ?? process.env.NODE_ENV,
|
|
1113
|
+
chain: txIntent.chainId != null ? String(txIntent.chainId) : txIntent.networkFamily,
|
|
1114
|
+
kmsKeyId: keyId,
|
|
1115
|
+
region: process.env.AWS_REGION
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function emitMetric(sink, name, labels) {
|
|
1119
|
+
const event = { name, labels, timestampMs: Date.now() };
|
|
1120
|
+
try {
|
|
1121
|
+
const result = sink.emit(event);
|
|
1122
|
+
if (result && typeof result.catch === "function") {
|
|
1123
|
+
result.catch(() => {
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
} catch {
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1097
1129
|
async function handleSignCommand(command, originalClient, gateClient, options) {
|
|
1098
1130
|
const txIntent = options.extractTxIntent(command);
|
|
1099
1131
|
const signerId = command.input?.KeyId ?? command.KeyId ?? "unknown";
|
|
1100
|
-
gateClient
|
|
1101
|
-
|
|
1102
|
-
|
|
1132
|
+
const labels = buildMetricLabels(gateClient, command, signerId, txIntent);
|
|
1133
|
+
emitMetric(options.metricsSink, "sign_attempt_total", labels);
|
|
1134
|
+
let heartbeatToken;
|
|
1135
|
+
try {
|
|
1136
|
+
heartbeatToken = await gateClient.heartbeatManager.getTokenForSigner(signerId, 2e3);
|
|
1137
|
+
} catch {
|
|
1103
1138
|
throw new BlockIntelBlockedError(
|
|
1104
1139
|
"HEARTBEAT_MISSING",
|
|
1105
1140
|
void 0,
|
|
@@ -1123,6 +1158,28 @@ async function handleSignCommand(command, originalClient, gateClient, options) {
|
|
|
1123
1158
|
// Type assertion - txIntent may have extra fields
|
|
1124
1159
|
signingContext
|
|
1125
1160
|
});
|
|
1161
|
+
if (decision.decision === "ALLOW" && options.requireReceiptForSign) {
|
|
1162
|
+
const hasReceipt2 = decision.receipt != null || decision.decisionHash != null && decision.receiptSignature != null;
|
|
1163
|
+
if (!hasReceipt2) {
|
|
1164
|
+
emitMetric(options.metricsSink, "sign_blocked_missing_receipt_total", labels);
|
|
1165
|
+
options.onDecision("BLOCK", {
|
|
1166
|
+
error: new BlockIntelBlockedError(
|
|
1167
|
+
"RECEIPT_REQUIRED",
|
|
1168
|
+
decision.decisionId,
|
|
1169
|
+
decision.correlationId,
|
|
1170
|
+
void 0
|
|
1171
|
+
),
|
|
1172
|
+
signerId,
|
|
1173
|
+
command
|
|
1174
|
+
});
|
|
1175
|
+
throw new BlockIntelBlockedError(
|
|
1176
|
+
"RECEIPT_REQUIRED",
|
|
1177
|
+
decision.decisionId,
|
|
1178
|
+
decision.correlationId,
|
|
1179
|
+
void 0
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1126
1183
|
if (decision.decision === "ALLOW" && gateClient.getRequireDecisionToken() && decision.txDigest != null) {
|
|
1127
1184
|
const binding = buildTxBindingObject(
|
|
1128
1185
|
txIntent,
|
|
@@ -1151,6 +1208,11 @@ async function handleSignCommand(command, originalClient, gateClient, options) {
|
|
|
1151
1208
|
);
|
|
1152
1209
|
}
|
|
1153
1210
|
}
|
|
1211
|
+
const hasReceipt = decision.receipt != null || decision.decisionHash != null && decision.receiptSignature != null;
|
|
1212
|
+
if (hasReceipt) {
|
|
1213
|
+
emitMetric(options.metricsSink, "sign_success_with_receipt_total", labels);
|
|
1214
|
+
}
|
|
1215
|
+
emitMetric(options.metricsSink, "sign_success_total", labels);
|
|
1154
1216
|
options.onDecision("ALLOW", { decision, signerId, command });
|
|
1155
1217
|
if (options.mode === "dry-run") {
|
|
1156
1218
|
return await originalClient.send(new clientKms.SignCommand(command));
|
|
@@ -1211,7 +1273,7 @@ var ProvenanceProvider = class {
|
|
|
1211
1273
|
var HeartbeatManager = class {
|
|
1212
1274
|
httpClient;
|
|
1213
1275
|
tenantId;
|
|
1214
|
-
|
|
1276
|
+
defaultSignerId;
|
|
1215
1277
|
environment;
|
|
1216
1278
|
baseRefreshIntervalSeconds;
|
|
1217
1279
|
clientInstanceId;
|
|
@@ -1220,24 +1282,27 @@ var HeartbeatManager = class {
|
|
|
1220
1282
|
// SDK version for tracking
|
|
1221
1283
|
apiKey;
|
|
1222
1284
|
// x-gate-heartbeat-key for Control Plane auth
|
|
1223
|
-
|
|
1224
|
-
|
|
1285
|
+
signerEntries = /* @__PURE__ */ new Map();
|
|
1286
|
+
evictionTimer = null;
|
|
1225
1287
|
started = false;
|
|
1226
|
-
consecutiveFailures = 0;
|
|
1227
1288
|
maxBackoffSeconds = 30;
|
|
1228
1289
|
// Maximum backoff interval
|
|
1229
|
-
|
|
1230
|
-
|
|
1290
|
+
maxSigners;
|
|
1291
|
+
signerIdleTtlMs;
|
|
1292
|
+
localRateLimitMs;
|
|
1231
1293
|
constructor(options) {
|
|
1232
1294
|
this.httpClient = options.httpClient;
|
|
1233
1295
|
this.tenantId = options.tenantId;
|
|
1234
|
-
this.
|
|
1296
|
+
this.defaultSignerId = options.signerId;
|
|
1235
1297
|
this.environment = options.environment ?? "prod";
|
|
1236
1298
|
this.baseRefreshIntervalSeconds = options.refreshIntervalSeconds ?? 10;
|
|
1237
1299
|
this.apiKey = options.apiKey;
|
|
1238
1300
|
this.clientInstanceId = options.clientInstanceId || uuid.v4();
|
|
1239
1301
|
this.sdkVersion = options.sdkVersion || "1.0.0";
|
|
1240
1302
|
this.apiKey = options.apiKey;
|
|
1303
|
+
this.maxSigners = options.maxSigners ?? 20;
|
|
1304
|
+
this.signerIdleTtlMs = options.signerIdleTtlMs ?? 3e5;
|
|
1305
|
+
this.localRateLimitMs = options.localRateLimitMs ?? 2100;
|
|
1241
1306
|
}
|
|
1242
1307
|
/**
|
|
1243
1308
|
* Start background heartbeat refresher.
|
|
@@ -1248,46 +1313,56 @@ var HeartbeatManager = class {
|
|
|
1248
1313
|
return;
|
|
1249
1314
|
}
|
|
1250
1315
|
this.started = true;
|
|
1251
|
-
this.
|
|
1252
|
-
|
|
1316
|
+
this.startEvictionTimer();
|
|
1317
|
+
this.getTokenForSigner(this.defaultSignerId, 0).catch((error) => {
|
|
1318
|
+
console.warn("[HEARTBEAT] Failed to acquire initial heartbeat:", error instanceof Error ? error.message : error);
|
|
1253
1319
|
});
|
|
1254
|
-
|
|
1320
|
+
}
|
|
1321
|
+
startEvictionTimer() {
|
|
1322
|
+
if (this.evictionTimer) clearInterval(this.evictionTimer);
|
|
1323
|
+
this.evictionTimer = setInterval(() => {
|
|
1324
|
+
const now = Date.now();
|
|
1325
|
+
for (const [signerId, entry] of this.signerEntries) {
|
|
1326
|
+
if (now - entry.lastUsedMs > this.signerIdleTtlMs) {
|
|
1327
|
+
if (entry.refreshTimer) clearTimeout(entry.refreshTimer);
|
|
1328
|
+
this.signerEntries.delete(signerId);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}, 6e4);
|
|
1255
1332
|
}
|
|
1256
1333
|
/**
|
|
1257
|
-
* Schedule next refresh with jitter and backoff
|
|
1334
|
+
* Schedule next refresh with jitter and backoff for a specific signer
|
|
1258
1335
|
*/
|
|
1259
|
-
|
|
1260
|
-
if (!this.started) {
|
|
1336
|
+
scheduleRefreshForSigner(signerId, entry) {
|
|
1337
|
+
if (!this.started || !this.signerEntries.has(signerId)) {
|
|
1261
1338
|
return;
|
|
1262
1339
|
}
|
|
1340
|
+
if (entry.refreshTimer) {
|
|
1341
|
+
clearTimeout(entry.refreshTimer);
|
|
1342
|
+
entry.refreshTimer = null;
|
|
1343
|
+
}
|
|
1263
1344
|
const baseInterval = this.baseRefreshIntervalSeconds * 1e3;
|
|
1264
1345
|
const jitter = Math.random() * 2e3;
|
|
1265
|
-
const backoff =
|
|
1346
|
+
const backoff = Math.min(
|
|
1347
|
+
Math.pow(2, entry.consecutiveFailures) * 1e3,
|
|
1348
|
+
this.maxBackoffSeconds * 1e3
|
|
1349
|
+
);
|
|
1266
1350
|
const interval = baseInterval + jitter + backoff;
|
|
1267
|
-
|
|
1268
|
-
this.
|
|
1269
|
-
|
|
1270
|
-
|
|
1351
|
+
entry.refreshTimer = setTimeout(() => {
|
|
1352
|
+
if (!this.signerEntries.has(signerId)) return;
|
|
1353
|
+
entry.acquiring = true;
|
|
1354
|
+
entry.acquirePromise = this.acquireHeartbeatForSigner(signerId, entry).then(() => {
|
|
1355
|
+
this.scheduleRefreshForSigner(signerId, entry);
|
|
1271
1356
|
}).catch((error) => {
|
|
1272
|
-
|
|
1273
|
-
console.error(
|
|
1274
|
-
this.
|
|
1357
|
+
entry.consecutiveFailures++;
|
|
1358
|
+
console.error(`[HEARTBEAT] Refresh failed for signer ${signerId} (will retry):`, error.message || error);
|
|
1359
|
+
this.scheduleRefreshForSigner(signerId, entry);
|
|
1360
|
+
}).finally(() => {
|
|
1361
|
+
entry.acquiring = false;
|
|
1362
|
+
entry.acquirePromise = null;
|
|
1275
1363
|
});
|
|
1276
1364
|
}, interval);
|
|
1277
1365
|
}
|
|
1278
|
-
/**
|
|
1279
|
-
* Calculate exponential backoff (capped at maxBackoffSeconds)
|
|
1280
|
-
*/
|
|
1281
|
-
calculateBackoff() {
|
|
1282
|
-
if (this.consecutiveFailures === 0) {
|
|
1283
|
-
return 0;
|
|
1284
|
-
}
|
|
1285
|
-
const backoffSeconds = Math.min(
|
|
1286
|
-
Math.pow(2, this.consecutiveFailures) * 1e3,
|
|
1287
|
-
this.maxBackoffSeconds * 1e3
|
|
1288
|
-
);
|
|
1289
|
-
return backoffSeconds;
|
|
1290
|
-
}
|
|
1291
1366
|
/**
|
|
1292
1367
|
* Stop background heartbeat refresher
|
|
1293
1368
|
*/
|
|
@@ -1296,53 +1371,153 @@ var HeartbeatManager = class {
|
|
|
1296
1371
|
return;
|
|
1297
1372
|
}
|
|
1298
1373
|
this.started = false;
|
|
1299
|
-
if (this.
|
|
1300
|
-
|
|
1301
|
-
this.
|
|
1374
|
+
if (this.evictionTimer) {
|
|
1375
|
+
clearInterval(this.evictionTimer);
|
|
1376
|
+
this.evictionTimer = null;
|
|
1377
|
+
}
|
|
1378
|
+
for (const [signerId, entry] of this.signerEntries) {
|
|
1379
|
+
if (entry.refreshTimer) {
|
|
1380
|
+
clearTimeout(entry.refreshTimer);
|
|
1381
|
+
entry.refreshTimer = null;
|
|
1382
|
+
}
|
|
1302
1383
|
}
|
|
1384
|
+
this.signerEntries.clear();
|
|
1303
1385
|
}
|
|
1304
1386
|
/**
|
|
1305
|
-
* Get current heartbeat token if valid
|
|
1387
|
+
* Get current heartbeat token if valid for the default signer
|
|
1388
|
+
* @deprecated Use getTokenForSigner() instead.
|
|
1306
1389
|
*/
|
|
1307
1390
|
getToken() {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
if (this.currentToken.expiresAt <= now + 2) {
|
|
1313
|
-
return null;
|
|
1391
|
+
const entry = this.signerEntries.get(this.defaultSignerId);
|
|
1392
|
+
if (entry && entry.token && entry.token.expiresAt > Math.floor(Date.now() / 1e3) + 2) {
|
|
1393
|
+
entry.lastUsedMs = Date.now();
|
|
1394
|
+
return entry.token.token;
|
|
1314
1395
|
}
|
|
1315
|
-
return
|
|
1396
|
+
return null;
|
|
1316
1397
|
}
|
|
1317
1398
|
/**
|
|
1318
|
-
* Check if current heartbeat token is valid
|
|
1399
|
+
* Check if current heartbeat token is valid for the default signer
|
|
1400
|
+
* @deprecated Use getTokenForSigner() instead.
|
|
1319
1401
|
*/
|
|
1320
1402
|
isValid() {
|
|
1321
1403
|
return this.getToken() !== null;
|
|
1322
1404
|
}
|
|
1323
1405
|
/**
|
|
1324
1406
|
* Update signer ID (called when signer is known).
|
|
1325
|
-
*
|
|
1407
|
+
* @deprecated Use getTokenForSigner() — signerId changes are handled automatically by the per-signer cache.
|
|
1326
1408
|
*/
|
|
1327
1409
|
updateSignerId(signerId) {
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1410
|
+
this.defaultSignerId = signerId;
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Get a valid heartbeat token for a specific signer.
|
|
1414
|
+
* Returns immediately if a cached valid token exists.
|
|
1415
|
+
* If no token, triggers acquisition and returns a Promise that resolves
|
|
1416
|
+
* when the token is available (or rejects after maxWaitMs).
|
|
1417
|
+
*/
|
|
1418
|
+
async getTokenForSigner(signerId, maxWaitMs = 2e3) {
|
|
1419
|
+
if (!this.started) {
|
|
1420
|
+
throw new GateError("HEARTBEAT_MISSING" /* HEARTBEAT_MISSING */, "HeartbeatManager not started");
|
|
1421
|
+
}
|
|
1422
|
+
const startTime = Date.now();
|
|
1423
|
+
let entry = this.signerEntries.get(signerId);
|
|
1424
|
+
const now = Date.now();
|
|
1425
|
+
const getValidToken = (e) => {
|
|
1426
|
+
if (e.token && e.token.expiresAt > Math.floor(Date.now() / 1e3) + 2) {
|
|
1427
|
+
return e.token.token;
|
|
1337
1428
|
}
|
|
1429
|
+
return null;
|
|
1430
|
+
};
|
|
1431
|
+
if (entry) {
|
|
1432
|
+
entry.lastUsedMs = now;
|
|
1433
|
+
const t2 = getValidToken(entry);
|
|
1434
|
+
if (t2) return t2;
|
|
1435
|
+
} else {
|
|
1436
|
+
if (this.signerEntries.size >= this.maxSigners) {
|
|
1437
|
+
let oldestSignerId = null;
|
|
1438
|
+
let oldestUsedMs = Infinity;
|
|
1439
|
+
for (const [sId, e] of this.signerEntries) {
|
|
1440
|
+
if (e.lastUsedMs < oldestUsedMs) {
|
|
1441
|
+
oldestUsedMs = e.lastUsedMs;
|
|
1442
|
+
oldestSignerId = sId;
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (oldestSignerId) {
|
|
1446
|
+
const oldestEntry = this.signerEntries.get(oldestSignerId);
|
|
1447
|
+
if (oldestEntry?.refreshTimer) clearTimeout(oldestEntry.refreshTimer);
|
|
1448
|
+
this.signerEntries.delete(oldestSignerId);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
entry = {
|
|
1452
|
+
token: null,
|
|
1453
|
+
refreshTimer: null,
|
|
1454
|
+
consecutiveFailures: 0,
|
|
1455
|
+
lastAcquireAttemptMs: 0,
|
|
1456
|
+
lastUsedMs: now,
|
|
1457
|
+
acquiring: false,
|
|
1458
|
+
acquirePromise: null
|
|
1459
|
+
};
|
|
1460
|
+
this.signerEntries.set(signerId, entry);
|
|
1461
|
+
}
|
|
1462
|
+
if (entry.acquiring && entry.acquirePromise) {
|
|
1463
|
+
const remainingWait = Math.max(0, maxWaitMs - (Date.now() - startTime));
|
|
1464
|
+
try {
|
|
1465
|
+
await Promise.race([
|
|
1466
|
+
entry.acquirePromise,
|
|
1467
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), remainingWait))
|
|
1468
|
+
]);
|
|
1469
|
+
} catch (e) {
|
|
1470
|
+
}
|
|
1471
|
+
const t2 = getValidToken(entry);
|
|
1472
|
+
if (t2) return t2;
|
|
1473
|
+
}
|
|
1474
|
+
const timeSinceLastAttempt = Date.now() - entry.lastAcquireAttemptMs;
|
|
1475
|
+
let timeToWaitBeforeFetch = 0;
|
|
1476
|
+
if (timeSinceLastAttempt < this.localRateLimitMs) {
|
|
1477
|
+
timeToWaitBeforeFetch = this.localRateLimitMs - timeSinceLastAttempt;
|
|
1478
|
+
}
|
|
1479
|
+
const remainingWait2 = Math.max(0, maxWaitMs - (Date.now() - startTime));
|
|
1480
|
+
if (timeToWaitBeforeFetch >= remainingWait2) {
|
|
1481
|
+
throw new GateError(
|
|
1482
|
+
"HEARTBEAT_MISSING" /* HEARTBEAT_MISSING */,
|
|
1483
|
+
"Signing blocked: Heartbeat token is missing or expired. Gate must be alive and enforcing policy."
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
if (timeToWaitBeforeFetch > 0) {
|
|
1487
|
+
await new Promise((resolve) => setTimeout(resolve, timeToWaitBeforeFetch));
|
|
1488
|
+
}
|
|
1489
|
+
if (!entry.acquiring) {
|
|
1490
|
+
entry.acquiring = true;
|
|
1491
|
+
entry.acquirePromise = this.acquireHeartbeatForSigner(signerId, entry).finally(() => {
|
|
1492
|
+
if (entry) {
|
|
1493
|
+
entry.acquiring = false;
|
|
1494
|
+
entry.acquirePromise = null;
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1338
1497
|
}
|
|
1498
|
+
const remainingWait3 = Math.max(0, maxWaitMs - (Date.now() - startTime));
|
|
1499
|
+
try {
|
|
1500
|
+
if (entry.acquirePromise) {
|
|
1501
|
+
await Promise.race([
|
|
1502
|
+
entry.acquirePromise,
|
|
1503
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), remainingWait3))
|
|
1504
|
+
]);
|
|
1505
|
+
}
|
|
1506
|
+
} catch (e) {
|
|
1507
|
+
}
|
|
1508
|
+
const t = getValidToken(entry);
|
|
1509
|
+
if (t) return t;
|
|
1510
|
+
throw new GateError(
|
|
1511
|
+
"HEARTBEAT_MISSING" /* HEARTBEAT_MISSING */,
|
|
1512
|
+
"Signing blocked: Heartbeat token is missing or expired. Gate must be alive and enforcing policy."
|
|
1513
|
+
);
|
|
1339
1514
|
}
|
|
1340
1515
|
/**
|
|
1341
|
-
* Acquire a new heartbeat token from Control Plane
|
|
1516
|
+
* Acquire a new heartbeat token from Control Plane for a specific signer
|
|
1342
1517
|
* NEVER logs token value (security)
|
|
1343
1518
|
* Requires x-gate-heartbeat-key header (apiKey) for authentication.
|
|
1344
1519
|
*/
|
|
1345
|
-
async
|
|
1520
|
+
async acquireHeartbeatForSigner(signerId, entry) {
|
|
1346
1521
|
if (!this.apiKey || this.apiKey.length === 0) {
|
|
1347
1522
|
throw new GateError(
|
|
1348
1523
|
"UNAUTHORIZED" /* UNAUTHORIZED */,
|
|
@@ -1350,8 +1525,7 @@ var HeartbeatManager = class {
|
|
|
1350
1525
|
{}
|
|
1351
1526
|
);
|
|
1352
1527
|
}
|
|
1353
|
-
|
|
1354
|
-
this.acquiringForSignerId = signerIdAtRequest;
|
|
1528
|
+
entry.lastAcquireAttemptMs = Date.now();
|
|
1355
1529
|
try {
|
|
1356
1530
|
const response = await this.httpClient.request({
|
|
1357
1531
|
method: "POST",
|
|
@@ -1361,12 +1535,15 @@ var HeartbeatManager = class {
|
|
|
1361
1535
|
},
|
|
1362
1536
|
body: {
|
|
1363
1537
|
tenantId: this.tenantId,
|
|
1364
|
-
signerId
|
|
1538
|
+
signerId,
|
|
1365
1539
|
environment: this.environment,
|
|
1366
1540
|
clientInstanceId: this.clientInstanceId,
|
|
1367
1541
|
sdkVersion: this.sdkVersion
|
|
1368
1542
|
}
|
|
1369
1543
|
});
|
|
1544
|
+
if (!this.signerEntries.has(signerId)) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1370
1547
|
if (response.success && response.data) {
|
|
1371
1548
|
const token = response.data.heartbeatToken;
|
|
1372
1549
|
const expiresAt = response.data.expiresAt;
|
|
@@ -1376,19 +1553,22 @@ var HeartbeatManager = class {
|
|
|
1376
1553
|
"Invalid heartbeat response: missing token or expiresAt"
|
|
1377
1554
|
);
|
|
1378
1555
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1556
|
+
entry.token = {
|
|
1557
|
+
token,
|
|
1558
|
+
expiresAt,
|
|
1559
|
+
jti: response.data.jti,
|
|
1560
|
+
policyHash: response.data.policyHash
|
|
1561
|
+
};
|
|
1562
|
+
entry.consecutiveFailures = 0;
|
|
1563
|
+
console.log("[HEARTBEAT] Acquired heartbeat token", {
|
|
1564
|
+
expiresAt,
|
|
1565
|
+
signerId,
|
|
1566
|
+
jti: response.data.jti,
|
|
1567
|
+
policyHash: response.data.policyHash?.substring(0, 8) + "..."
|
|
1568
|
+
// DO NOT log token value
|
|
1569
|
+
});
|
|
1570
|
+
if (!entry.refreshTimer) {
|
|
1571
|
+
this.scheduleRefreshForSigner(signerId, entry);
|
|
1392
1572
|
}
|
|
1393
1573
|
} else {
|
|
1394
1574
|
const error = response.error || {};
|
|
@@ -1398,12 +1578,8 @@ var HeartbeatManager = class {
|
|
|
1398
1578
|
);
|
|
1399
1579
|
}
|
|
1400
1580
|
} catch (error) {
|
|
1401
|
-
console.error(
|
|
1581
|
+
console.error(`[HEARTBEAT] Failed to acquire heartbeat for signer ${signerId}:`, error.message || error);
|
|
1402
1582
|
throw error;
|
|
1403
|
-
} finally {
|
|
1404
|
-
if (this.acquiringForSignerId === signerIdAtRequest) {
|
|
1405
|
-
this.acquiringForSignerId = null;
|
|
1406
|
-
}
|
|
1407
1583
|
}
|
|
1408
1584
|
}
|
|
1409
1585
|
/**
|
|
@@ -1659,6 +1835,7 @@ var IamPermissionRiskChecker = class {
|
|
|
1659
1835
|
};
|
|
1660
1836
|
|
|
1661
1837
|
// src/client/GateClient.ts
|
|
1838
|
+
var DEFAULT_SIGNER_ID = "gate-sdk-client";
|
|
1662
1839
|
var GateClient = class {
|
|
1663
1840
|
config;
|
|
1664
1841
|
httpClient;
|
|
@@ -1733,7 +1910,7 @@ var GateClient = class {
|
|
|
1733
1910
|
// 5s timeout for heartbeat
|
|
1734
1911
|
userAgent: config.userAgent
|
|
1735
1912
|
});
|
|
1736
|
-
const initialSignerId = config.signerId ??
|
|
1913
|
+
const initialSignerId = config.signerId ?? DEFAULT_SIGNER_ID;
|
|
1737
1914
|
this.heartbeatManager = new HeartbeatManager({
|
|
1738
1915
|
httpClient: heartbeatHttpClient,
|
|
1739
1916
|
tenantId: config.tenantId,
|
|
@@ -1808,26 +1985,10 @@ var GateClient = class {
|
|
|
1808
1985
|
const requestMode = req.mode || this.mode;
|
|
1809
1986
|
const requireToken = this.getRequireDecisionToken();
|
|
1810
1987
|
const executeRequest = async () => {
|
|
1811
|
-
if (!this.config.local && this.heartbeatManager && req.signingContext?.signerId) {
|
|
1812
|
-
this.heartbeatManager.updateSignerId(req.signingContext.signerId);
|
|
1813
|
-
}
|
|
1814
1988
|
let heartbeatToken = null;
|
|
1815
1989
|
if (!this.config.local && this.heartbeatManager) {
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
const maxWaitMs = 2e3;
|
|
1819
|
-
const startTime2 = Date.now();
|
|
1820
|
-
while (!heartbeatToken && Date.now() - startTime2 < maxWaitMs) {
|
|
1821
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1822
|
-
heartbeatToken = this.heartbeatManager.getToken();
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
if (!heartbeatToken) {
|
|
1826
|
-
throw new GateError(
|
|
1827
|
-
"HEARTBEAT_MISSING" /* HEARTBEAT_MISSING */,
|
|
1828
|
-
"Signing blocked: Heartbeat token is missing or expired. Gate must be alive and enforcing policy."
|
|
1829
|
-
);
|
|
1830
|
-
}
|
|
1990
|
+
const effectiveSignerId2 = req.signingContext?.signerId ?? req.signingContext?.actorPrincipal ?? DEFAULT_SIGNER_ID;
|
|
1991
|
+
heartbeatToken = await this.heartbeatManager.getTokenForSigner(effectiveSignerId2, 2e3);
|
|
1831
1992
|
}
|
|
1832
1993
|
const txIntent = { ...req.txIntent };
|
|
1833
1994
|
if (txIntent.to && !txIntent.toAddress) {
|
|
@@ -1840,10 +2001,11 @@ var GateClient = class {
|
|
|
1840
2001
|
if (txIntent.from && !txIntent.fromAddress) {
|
|
1841
2002
|
delete txIntent.from;
|
|
1842
2003
|
}
|
|
2004
|
+
const effectiveSignerId = req.signingContext?.signerId ?? req.signingContext?.actorPrincipal ?? DEFAULT_SIGNER_ID;
|
|
1843
2005
|
const signingContext = {
|
|
1844
2006
|
...req.signingContext,
|
|
1845
|
-
actorPrincipal: req.signingContext?.actorPrincipal ?? req.signingContext?.signerId ??
|
|
1846
|
-
signerId:
|
|
2007
|
+
actorPrincipal: req.signingContext?.actorPrincipal ?? req.signingContext?.signerId ?? DEFAULT_SIGNER_ID,
|
|
2008
|
+
signerId: effectiveSignerId
|
|
1847
2009
|
};
|
|
1848
2010
|
if (heartbeatToken) {
|
|
1849
2011
|
signingContext.heartbeatToken = heartbeatToken;
|
|
@@ -1960,6 +2122,9 @@ var GateClient = class {
|
|
|
1960
2122
|
enforced: responseData.enforced ?? requestMode === "ENFORCE",
|
|
1961
2123
|
shadowWouldBlock: responseData.shadow_would_block ?? responseData.shadowWouldBlock ?? false,
|
|
1962
2124
|
mode: responseData.mode ?? requestMode,
|
|
2125
|
+
receipt: responseData.receipt,
|
|
2126
|
+
decisionHash: responseData.decision_hash ?? responseData.decisionHash,
|
|
2127
|
+
receiptSignature: responseData.receipt_signature ?? responseData.receiptSignature,
|
|
1963
2128
|
...simulationData ? {
|
|
1964
2129
|
simulation: {
|
|
1965
2130
|
willRevert: simulationData.willRevert ?? simulationData.will_revert ?? false,
|
|
@@ -3053,6 +3218,7 @@ exports.buildTxBindingObject = buildTxBindingObject;
|
|
|
3053
3218
|
exports.computeTxDigest = computeTxDigest;
|
|
3054
3219
|
exports.createGateClient = createGateClient;
|
|
3055
3220
|
exports.default = GateClient;
|
|
3221
|
+
exports.noOpMetricsSink = noOpMetricsSink;
|
|
3056
3222
|
exports.wrapKmsClient = wrapKmsClient;
|
|
3057
3223
|
//# sourceMappingURL=index.cjs.map
|
|
3058
3224
|
//# sourceMappingURL=index.cjs.map
|