blockintel-gate-sdk 0.3.9 → 0.4.0
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 +30 -17
- 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 +261 -77
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +70 -18
- package/dist/index.d.ts +70 -18
- package/dist/index.js +261 -78
- package/dist/index.js.map +1 -1
- package/dist/pilot/index.cjs +260 -77
- 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 +260 -77
- package/dist/pilot/index.js.map +1 -1
- package/package.json +1 -1
package/dist/pilot/index.cjs
CHANGED
|
@@ -1016,13 +1016,21 @@ function computeTxDigest(binding) {
|
|
|
1016
1016
|
return crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
1017
1017
|
}
|
|
1018
1018
|
|
|
1019
|
+
// src/metrics/GateMetricsSink.ts
|
|
1020
|
+
var noOpMetricsSink = {
|
|
1021
|
+
emit() {
|
|
1022
|
+
}
|
|
1023
|
+
};
|
|
1024
|
+
|
|
1019
1025
|
// src/kms/wrapAwsSdkV3KmsClient.ts
|
|
1020
1026
|
function wrapKmsClient(kmsClient, gateClient, options = {}) {
|
|
1021
1027
|
const defaultOptions = {
|
|
1022
1028
|
mode: options.mode || "enforce",
|
|
1029
|
+
requireReceiptForSign: options.requireReceiptForSign ?? false,
|
|
1023
1030
|
onDecision: options.onDecision || (() => {
|
|
1024
1031
|
}),
|
|
1025
|
-
extractTxIntent: options.extractTxIntent || defaultExtractTxIntent
|
|
1032
|
+
extractTxIntent: options.extractTxIntent || defaultExtractTxIntent,
|
|
1033
|
+
metricsSink: options.metricsSink ?? noOpMetricsSink
|
|
1026
1034
|
};
|
|
1027
1035
|
const wrapped = new Proxy(kmsClient, {
|
|
1028
1036
|
get(target, prop, receiver) {
|
|
@@ -1063,12 +1071,39 @@ function defaultExtractTxIntent(command) {
|
|
|
1063
1071
|
// Backward compatibility
|
|
1064
1072
|
};
|
|
1065
1073
|
}
|
|
1074
|
+
function buildMetricLabels(gateClient, command, signerId, txIntent) {
|
|
1075
|
+
const config = gateClient.config;
|
|
1076
|
+
const keyId = command.input?.KeyId ?? command.KeyId;
|
|
1077
|
+
return {
|
|
1078
|
+
tenantId: config?.tenantId,
|
|
1079
|
+
signerId: signerId || void 0,
|
|
1080
|
+
adoptionStage: config?.adoptionStage ?? process.env.GATE_ADOPTION_STAGE,
|
|
1081
|
+
env: config?.env ?? process.env.GATE_ENV ?? process.env.NODE_ENV,
|
|
1082
|
+
chain: txIntent.chainId != null ? String(txIntent.chainId) : txIntent.networkFamily,
|
|
1083
|
+
kmsKeyId: keyId,
|
|
1084
|
+
region: process.env.AWS_REGION
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
function emitMetric(sink, name, labels) {
|
|
1088
|
+
const event = { name, labels, timestampMs: Date.now() };
|
|
1089
|
+
try {
|
|
1090
|
+
const result = sink.emit(event);
|
|
1091
|
+
if (result && typeof result.catch === "function") {
|
|
1092
|
+
result.catch(() => {
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
} catch {
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1066
1098
|
async function handleSignCommand(command, originalClient, gateClient, options) {
|
|
1067
1099
|
const txIntent = options.extractTxIntent(command);
|
|
1068
1100
|
const signerId = command.input?.KeyId ?? command.KeyId ?? "unknown";
|
|
1069
|
-
gateClient
|
|
1070
|
-
|
|
1071
|
-
|
|
1101
|
+
const labels = buildMetricLabels(gateClient, command, signerId, txIntent);
|
|
1102
|
+
emitMetric(options.metricsSink, "sign_attempt_total", labels);
|
|
1103
|
+
let heartbeatToken;
|
|
1104
|
+
try {
|
|
1105
|
+
heartbeatToken = await gateClient.heartbeatManager.getTokenForSigner(signerId, 2e3);
|
|
1106
|
+
} catch {
|
|
1072
1107
|
throw new BlockIntelBlockedError(
|
|
1073
1108
|
"HEARTBEAT_MISSING",
|
|
1074
1109
|
void 0,
|
|
@@ -1092,6 +1127,28 @@ async function handleSignCommand(command, originalClient, gateClient, options) {
|
|
|
1092
1127
|
// Type assertion - txIntent may have extra fields
|
|
1093
1128
|
signingContext
|
|
1094
1129
|
});
|
|
1130
|
+
if (decision.decision === "ALLOW" && options.requireReceiptForSign) {
|
|
1131
|
+
const hasReceipt2 = decision.receipt != null || decision.decisionHash != null && decision.receiptSignature != null;
|
|
1132
|
+
if (!hasReceipt2) {
|
|
1133
|
+
emitMetric(options.metricsSink, "sign_blocked_missing_receipt_total", labels);
|
|
1134
|
+
options.onDecision("BLOCK", {
|
|
1135
|
+
error: new BlockIntelBlockedError(
|
|
1136
|
+
"RECEIPT_REQUIRED",
|
|
1137
|
+
decision.decisionId,
|
|
1138
|
+
decision.correlationId,
|
|
1139
|
+
void 0
|
|
1140
|
+
),
|
|
1141
|
+
signerId,
|
|
1142
|
+
command
|
|
1143
|
+
});
|
|
1144
|
+
throw new BlockIntelBlockedError(
|
|
1145
|
+
"RECEIPT_REQUIRED",
|
|
1146
|
+
decision.decisionId,
|
|
1147
|
+
decision.correlationId,
|
|
1148
|
+
void 0
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1095
1152
|
if (decision.decision === "ALLOW" && gateClient.getRequireDecisionToken() && decision.txDigest != null) {
|
|
1096
1153
|
const binding = buildTxBindingObject(
|
|
1097
1154
|
txIntent,
|
|
@@ -1120,6 +1177,11 @@ async function handleSignCommand(command, originalClient, gateClient, options) {
|
|
|
1120
1177
|
);
|
|
1121
1178
|
}
|
|
1122
1179
|
}
|
|
1180
|
+
const hasReceipt = decision.receipt != null || decision.decisionHash != null && decision.receiptSignature != null;
|
|
1181
|
+
if (hasReceipt) {
|
|
1182
|
+
emitMetric(options.metricsSink, "sign_success_with_receipt_total", labels);
|
|
1183
|
+
}
|
|
1184
|
+
emitMetric(options.metricsSink, "sign_success_total", labels);
|
|
1123
1185
|
options.onDecision("ALLOW", { decision, signerId, command });
|
|
1124
1186
|
if (options.mode === "dry-run") {
|
|
1125
1187
|
return await originalClient.send(new clientKms.SignCommand(command));
|
|
@@ -1180,7 +1242,7 @@ var ProvenanceProvider = class {
|
|
|
1180
1242
|
var HeartbeatManager = class {
|
|
1181
1243
|
httpClient;
|
|
1182
1244
|
tenantId;
|
|
1183
|
-
|
|
1245
|
+
defaultSignerId;
|
|
1184
1246
|
environment;
|
|
1185
1247
|
baseRefreshIntervalSeconds;
|
|
1186
1248
|
clientInstanceId;
|
|
@@ -1189,22 +1251,27 @@ var HeartbeatManager = class {
|
|
|
1189
1251
|
// SDK version for tracking
|
|
1190
1252
|
apiKey;
|
|
1191
1253
|
// x-gate-heartbeat-key for Control Plane auth
|
|
1192
|
-
|
|
1193
|
-
|
|
1254
|
+
signerEntries = /* @__PURE__ */ new Map();
|
|
1255
|
+
evictionTimer = null;
|
|
1194
1256
|
started = false;
|
|
1195
|
-
consecutiveFailures = 0;
|
|
1196
1257
|
maxBackoffSeconds = 30;
|
|
1197
1258
|
// Maximum backoff interval
|
|
1259
|
+
maxSigners;
|
|
1260
|
+
signerIdleTtlMs;
|
|
1261
|
+
localRateLimitMs;
|
|
1198
1262
|
constructor(options) {
|
|
1199
1263
|
this.httpClient = options.httpClient;
|
|
1200
1264
|
this.tenantId = options.tenantId;
|
|
1201
|
-
this.
|
|
1265
|
+
this.defaultSignerId = options.signerId;
|
|
1202
1266
|
this.environment = options.environment ?? "prod";
|
|
1203
1267
|
this.baseRefreshIntervalSeconds = options.refreshIntervalSeconds ?? 10;
|
|
1204
1268
|
this.apiKey = options.apiKey;
|
|
1205
1269
|
this.clientInstanceId = options.clientInstanceId || uuid.v4();
|
|
1206
1270
|
this.sdkVersion = options.sdkVersion || "1.0.0";
|
|
1207
1271
|
this.apiKey = options.apiKey;
|
|
1272
|
+
this.maxSigners = options.maxSigners ?? 20;
|
|
1273
|
+
this.signerIdleTtlMs = options.signerIdleTtlMs ?? 3e5;
|
|
1274
|
+
this.localRateLimitMs = options.localRateLimitMs ?? 2100;
|
|
1208
1275
|
}
|
|
1209
1276
|
/**
|
|
1210
1277
|
* Start background heartbeat refresher.
|
|
@@ -1215,46 +1282,56 @@ var HeartbeatManager = class {
|
|
|
1215
1282
|
return;
|
|
1216
1283
|
}
|
|
1217
1284
|
this.started = true;
|
|
1218
|
-
this.
|
|
1285
|
+
this.startEvictionTimer();
|
|
1286
|
+
this.getTokenForSigner(this.defaultSignerId, 0).catch((error) => {
|
|
1219
1287
|
console.error("[HEARTBEAT] Failed to acquire initial heartbeat:", error instanceof Error ? error.message : error);
|
|
1220
1288
|
});
|
|
1221
|
-
|
|
1289
|
+
}
|
|
1290
|
+
startEvictionTimer() {
|
|
1291
|
+
if (this.evictionTimer) clearInterval(this.evictionTimer);
|
|
1292
|
+
this.evictionTimer = setInterval(() => {
|
|
1293
|
+
const now = Date.now();
|
|
1294
|
+
for (const [signerId, entry] of this.signerEntries) {
|
|
1295
|
+
if (now - entry.lastUsedMs > this.signerIdleTtlMs) {
|
|
1296
|
+
if (entry.refreshTimer) clearTimeout(entry.refreshTimer);
|
|
1297
|
+
this.signerEntries.delete(signerId);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}, 6e4);
|
|
1222
1301
|
}
|
|
1223
1302
|
/**
|
|
1224
|
-
* Schedule next refresh with jitter and backoff
|
|
1303
|
+
* Schedule next refresh with jitter and backoff for a specific signer
|
|
1225
1304
|
*/
|
|
1226
|
-
|
|
1227
|
-
if (!this.started) {
|
|
1305
|
+
scheduleRefreshForSigner(signerId, entry) {
|
|
1306
|
+
if (!this.started || !this.signerEntries.has(signerId)) {
|
|
1228
1307
|
return;
|
|
1229
1308
|
}
|
|
1309
|
+
if (entry.refreshTimer) {
|
|
1310
|
+
clearTimeout(entry.refreshTimer);
|
|
1311
|
+
entry.refreshTimer = null;
|
|
1312
|
+
}
|
|
1230
1313
|
const baseInterval = this.baseRefreshIntervalSeconds * 1e3;
|
|
1231
1314
|
const jitter = Math.random() * 2e3;
|
|
1232
|
-
const backoff =
|
|
1315
|
+
const backoff = Math.min(
|
|
1316
|
+
Math.pow(2, entry.consecutiveFailures) * 1e3,
|
|
1317
|
+
this.maxBackoffSeconds * 1e3
|
|
1318
|
+
);
|
|
1233
1319
|
const interval = baseInterval + jitter + backoff;
|
|
1234
|
-
|
|
1235
|
-
this.
|
|
1236
|
-
|
|
1237
|
-
|
|
1320
|
+
entry.refreshTimer = setTimeout(() => {
|
|
1321
|
+
if (!this.signerEntries.has(signerId)) return;
|
|
1322
|
+
entry.acquiring = true;
|
|
1323
|
+
entry.acquirePromise = this.acquireHeartbeatForSigner(signerId, entry).then(() => {
|
|
1324
|
+
this.scheduleRefreshForSigner(signerId, entry);
|
|
1238
1325
|
}).catch((error) => {
|
|
1239
|
-
|
|
1240
|
-
console.error(
|
|
1241
|
-
this.
|
|
1326
|
+
entry.consecutiveFailures++;
|
|
1327
|
+
console.error(`[HEARTBEAT] Refresh failed for signer ${signerId} (will retry):`, error.message || error);
|
|
1328
|
+
this.scheduleRefreshForSigner(signerId, entry);
|
|
1329
|
+
}).finally(() => {
|
|
1330
|
+
entry.acquiring = false;
|
|
1331
|
+
entry.acquirePromise = null;
|
|
1242
1332
|
});
|
|
1243
1333
|
}, interval);
|
|
1244
1334
|
}
|
|
1245
|
-
/**
|
|
1246
|
-
* Calculate exponential backoff (capped at maxBackoffSeconds)
|
|
1247
|
-
*/
|
|
1248
|
-
calculateBackoff() {
|
|
1249
|
-
if (this.consecutiveFailures === 0) {
|
|
1250
|
-
return 0;
|
|
1251
|
-
}
|
|
1252
|
-
const backoffSeconds = Math.min(
|
|
1253
|
-
Math.pow(2, this.consecutiveFailures) * 1e3,
|
|
1254
|
-
this.maxBackoffSeconds * 1e3
|
|
1255
|
-
);
|
|
1256
|
-
return backoffSeconds;
|
|
1257
|
-
}
|
|
1258
1335
|
/**
|
|
1259
1336
|
* Stop background heartbeat refresher
|
|
1260
1337
|
*/
|
|
@@ -1263,45 +1340,153 @@ var HeartbeatManager = class {
|
|
|
1263
1340
|
return;
|
|
1264
1341
|
}
|
|
1265
1342
|
this.started = false;
|
|
1266
|
-
if (this.
|
|
1267
|
-
|
|
1268
|
-
this.
|
|
1343
|
+
if (this.evictionTimer) {
|
|
1344
|
+
clearInterval(this.evictionTimer);
|
|
1345
|
+
this.evictionTimer = null;
|
|
1346
|
+
}
|
|
1347
|
+
for (const [signerId, entry] of this.signerEntries) {
|
|
1348
|
+
if (entry.refreshTimer) {
|
|
1349
|
+
clearTimeout(entry.refreshTimer);
|
|
1350
|
+
entry.refreshTimer = null;
|
|
1351
|
+
}
|
|
1269
1352
|
}
|
|
1353
|
+
this.signerEntries.clear();
|
|
1270
1354
|
}
|
|
1271
1355
|
/**
|
|
1272
|
-
* Get current heartbeat token if valid
|
|
1356
|
+
* Get current heartbeat token if valid for the default signer
|
|
1357
|
+
* @deprecated Use getTokenForSigner() instead.
|
|
1273
1358
|
*/
|
|
1274
1359
|
getToken() {
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
if (this.currentToken.expiresAt <= now + 2) {
|
|
1280
|
-
return null;
|
|
1360
|
+
const entry = this.signerEntries.get(this.defaultSignerId);
|
|
1361
|
+
if (entry && entry.token && entry.token.expiresAt > Math.floor(Date.now() / 1e3) + 2) {
|
|
1362
|
+
entry.lastUsedMs = Date.now();
|
|
1363
|
+
return entry.token.token;
|
|
1281
1364
|
}
|
|
1282
|
-
return
|
|
1365
|
+
return null;
|
|
1283
1366
|
}
|
|
1284
1367
|
/**
|
|
1285
|
-
* Check if current heartbeat token is valid
|
|
1368
|
+
* Check if current heartbeat token is valid for the default signer
|
|
1369
|
+
* @deprecated Use getTokenForSigner() instead.
|
|
1286
1370
|
*/
|
|
1287
1371
|
isValid() {
|
|
1288
1372
|
return this.getToken() !== null;
|
|
1289
1373
|
}
|
|
1290
1374
|
/**
|
|
1291
|
-
* Update signer ID (called when signer is known)
|
|
1375
|
+
* Update signer ID (called when signer is known).
|
|
1376
|
+
* @deprecated Use getTokenForSigner() — signerId changes are handled automatically by the per-signer cache.
|
|
1292
1377
|
*/
|
|
1293
1378
|
updateSignerId(signerId) {
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1379
|
+
this.defaultSignerId = signerId;
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Get a valid heartbeat token for a specific signer.
|
|
1383
|
+
* Returns immediately if a cached valid token exists.
|
|
1384
|
+
* If no token, triggers acquisition and returns a Promise that resolves
|
|
1385
|
+
* when the token is available (or rejects after maxWaitMs).
|
|
1386
|
+
*/
|
|
1387
|
+
async getTokenForSigner(signerId, maxWaitMs = 2e3) {
|
|
1388
|
+
if (!this.started) {
|
|
1389
|
+
throw new GateError("HEARTBEAT_MISSING" /* HEARTBEAT_MISSING */, "HeartbeatManager not started");
|
|
1390
|
+
}
|
|
1391
|
+
const startTime = Date.now();
|
|
1392
|
+
let entry = this.signerEntries.get(signerId);
|
|
1393
|
+
const now = Date.now();
|
|
1394
|
+
const getValidToken = (e) => {
|
|
1395
|
+
if (e.token && e.token.expiresAt > Math.floor(Date.now() / 1e3) + 2) {
|
|
1396
|
+
return e.token.token;
|
|
1397
|
+
}
|
|
1398
|
+
return null;
|
|
1399
|
+
};
|
|
1400
|
+
if (entry) {
|
|
1401
|
+
entry.lastUsedMs = now;
|
|
1402
|
+
const t2 = getValidToken(entry);
|
|
1403
|
+
if (t2) return t2;
|
|
1404
|
+
} else {
|
|
1405
|
+
if (this.signerEntries.size >= this.maxSigners) {
|
|
1406
|
+
let oldestSignerId = null;
|
|
1407
|
+
let oldestUsedMs = Infinity;
|
|
1408
|
+
for (const [sId, e] of this.signerEntries) {
|
|
1409
|
+
if (e.lastUsedMs < oldestUsedMs) {
|
|
1410
|
+
oldestUsedMs = e.lastUsedMs;
|
|
1411
|
+
oldestSignerId = sId;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (oldestSignerId) {
|
|
1415
|
+
const oldestEntry = this.signerEntries.get(oldestSignerId);
|
|
1416
|
+
if (oldestEntry?.refreshTimer) clearTimeout(oldestEntry.refreshTimer);
|
|
1417
|
+
this.signerEntries.delete(oldestSignerId);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
entry = {
|
|
1421
|
+
token: null,
|
|
1422
|
+
refreshTimer: null,
|
|
1423
|
+
consecutiveFailures: 0,
|
|
1424
|
+
lastAcquireAttemptMs: 0,
|
|
1425
|
+
lastUsedMs: now,
|
|
1426
|
+
acquiring: false,
|
|
1427
|
+
acquirePromise: null
|
|
1428
|
+
};
|
|
1429
|
+
this.signerEntries.set(signerId, entry);
|
|
1430
|
+
}
|
|
1431
|
+
if (entry.acquiring && entry.acquirePromise) {
|
|
1432
|
+
const remainingWait = Math.max(0, maxWaitMs - (Date.now() - startTime));
|
|
1433
|
+
try {
|
|
1434
|
+
await Promise.race([
|
|
1435
|
+
entry.acquirePromise,
|
|
1436
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), remainingWait))
|
|
1437
|
+
]);
|
|
1438
|
+
} catch (e) {
|
|
1439
|
+
}
|
|
1440
|
+
const t2 = getValidToken(entry);
|
|
1441
|
+
if (t2) return t2;
|
|
1297
1442
|
}
|
|
1443
|
+
const timeSinceLastAttempt = Date.now() - entry.lastAcquireAttemptMs;
|
|
1444
|
+
let timeToWaitBeforeFetch = 0;
|
|
1445
|
+
if (timeSinceLastAttempt < this.localRateLimitMs) {
|
|
1446
|
+
timeToWaitBeforeFetch = this.localRateLimitMs - timeSinceLastAttempt;
|
|
1447
|
+
}
|
|
1448
|
+
const remainingWait2 = Math.max(0, maxWaitMs - (Date.now() - startTime));
|
|
1449
|
+
if (timeToWaitBeforeFetch >= remainingWait2) {
|
|
1450
|
+
throw new GateError(
|
|
1451
|
+
"HEARTBEAT_MISSING" /* HEARTBEAT_MISSING */,
|
|
1452
|
+
"Signing blocked: Heartbeat token is missing or expired. Gate must be alive and enforcing policy."
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
if (timeToWaitBeforeFetch > 0) {
|
|
1456
|
+
await new Promise((resolve) => setTimeout(resolve, timeToWaitBeforeFetch));
|
|
1457
|
+
}
|
|
1458
|
+
if (!entry.acquiring) {
|
|
1459
|
+
entry.acquiring = true;
|
|
1460
|
+
entry.acquirePromise = this.acquireHeartbeatForSigner(signerId, entry).finally(() => {
|
|
1461
|
+
if (entry) {
|
|
1462
|
+
entry.acquiring = false;
|
|
1463
|
+
entry.acquirePromise = null;
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
const remainingWait3 = Math.max(0, maxWaitMs - (Date.now() - startTime));
|
|
1468
|
+
try {
|
|
1469
|
+
if (entry.acquirePromise) {
|
|
1470
|
+
await Promise.race([
|
|
1471
|
+
entry.acquirePromise,
|
|
1472
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), remainingWait3))
|
|
1473
|
+
]);
|
|
1474
|
+
}
|
|
1475
|
+
} catch (e) {
|
|
1476
|
+
}
|
|
1477
|
+
const t = getValidToken(entry);
|
|
1478
|
+
if (t) return t;
|
|
1479
|
+
throw new GateError(
|
|
1480
|
+
"HEARTBEAT_MISSING" /* HEARTBEAT_MISSING */,
|
|
1481
|
+
"Signing blocked: Heartbeat token is missing or expired. Gate must be alive and enforcing policy."
|
|
1482
|
+
);
|
|
1298
1483
|
}
|
|
1299
1484
|
/**
|
|
1300
|
-
* Acquire a new heartbeat token from Control Plane
|
|
1485
|
+
* Acquire a new heartbeat token from Control Plane for a specific signer
|
|
1301
1486
|
* NEVER logs token value (security)
|
|
1302
1487
|
* Requires x-gate-heartbeat-key header (apiKey) for authentication.
|
|
1303
1488
|
*/
|
|
1304
|
-
async
|
|
1489
|
+
async acquireHeartbeatForSigner(signerId, entry) {
|
|
1305
1490
|
if (!this.apiKey || this.apiKey.length === 0) {
|
|
1306
1491
|
throw new GateError(
|
|
1307
1492
|
"UNAUTHORIZED" /* UNAUTHORIZED */,
|
|
@@ -1309,6 +1494,7 @@ var HeartbeatManager = class {
|
|
|
1309
1494
|
{}
|
|
1310
1495
|
);
|
|
1311
1496
|
}
|
|
1497
|
+
entry.lastAcquireAttemptMs = Date.now();
|
|
1312
1498
|
try {
|
|
1313
1499
|
const response = await this.httpClient.request({
|
|
1314
1500
|
method: "POST",
|
|
@@ -1318,12 +1504,15 @@ var HeartbeatManager = class {
|
|
|
1318
1504
|
},
|
|
1319
1505
|
body: {
|
|
1320
1506
|
tenantId: this.tenantId,
|
|
1321
|
-
signerId
|
|
1507
|
+
signerId,
|
|
1322
1508
|
environment: this.environment,
|
|
1323
1509
|
clientInstanceId: this.clientInstanceId,
|
|
1324
1510
|
sdkVersion: this.sdkVersion
|
|
1325
1511
|
}
|
|
1326
1512
|
});
|
|
1513
|
+
if (!this.signerEntries.has(signerId)) {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1327
1516
|
if (response.success && response.data) {
|
|
1328
1517
|
const token = response.data.heartbeatToken;
|
|
1329
1518
|
const expiresAt = response.data.expiresAt;
|
|
@@ -1333,18 +1522,23 @@ var HeartbeatManager = class {
|
|
|
1333
1522
|
"Invalid heartbeat response: missing token or expiresAt"
|
|
1334
1523
|
);
|
|
1335
1524
|
}
|
|
1336
|
-
|
|
1525
|
+
entry.token = {
|
|
1337
1526
|
token,
|
|
1338
1527
|
expiresAt,
|
|
1339
1528
|
jti: response.data.jti,
|
|
1340
1529
|
policyHash: response.data.policyHash
|
|
1341
1530
|
};
|
|
1531
|
+
entry.consecutiveFailures = 0;
|
|
1342
1532
|
console.log("[HEARTBEAT] Acquired heartbeat token", {
|
|
1343
1533
|
expiresAt,
|
|
1534
|
+
signerId,
|
|
1344
1535
|
jti: response.data.jti,
|
|
1345
1536
|
policyHash: response.data.policyHash?.substring(0, 8) + "..."
|
|
1346
1537
|
// DO NOT log token value
|
|
1347
1538
|
});
|
|
1539
|
+
if (!entry.refreshTimer) {
|
|
1540
|
+
this.scheduleRefreshForSigner(signerId, entry);
|
|
1541
|
+
}
|
|
1348
1542
|
} else {
|
|
1349
1543
|
const error = response.error || {};
|
|
1350
1544
|
throw new GateError(
|
|
@@ -1353,7 +1547,7 @@ var HeartbeatManager = class {
|
|
|
1353
1547
|
);
|
|
1354
1548
|
}
|
|
1355
1549
|
} catch (error) {
|
|
1356
|
-
console.error(
|
|
1550
|
+
console.error(`[HEARTBEAT] Failed to acquire heartbeat for signer ${signerId}:`, error.message || error);
|
|
1357
1551
|
throw error;
|
|
1358
1552
|
}
|
|
1359
1553
|
}
|
|
@@ -1610,6 +1804,7 @@ var IamPermissionRiskChecker = class {
|
|
|
1610
1804
|
};
|
|
1611
1805
|
|
|
1612
1806
|
// src/client/GateClient.ts
|
|
1807
|
+
var DEFAULT_SIGNER_ID = "gate-sdk-client";
|
|
1613
1808
|
var GateClient = class {
|
|
1614
1809
|
config;
|
|
1615
1810
|
httpClient;
|
|
@@ -1684,7 +1879,7 @@ var GateClient = class {
|
|
|
1684
1879
|
// 5s timeout for heartbeat
|
|
1685
1880
|
userAgent: config.userAgent
|
|
1686
1881
|
});
|
|
1687
|
-
const initialSignerId = config.signerId ??
|
|
1882
|
+
const initialSignerId = config.signerId ?? DEFAULT_SIGNER_ID;
|
|
1688
1883
|
this.heartbeatManager = new HeartbeatManager({
|
|
1689
1884
|
httpClient: heartbeatHttpClient,
|
|
1690
1885
|
tenantId: config.tenantId,
|
|
@@ -1759,26 +1954,10 @@ var GateClient = class {
|
|
|
1759
1954
|
const requestMode = req.mode || this.mode;
|
|
1760
1955
|
const requireToken = this.getRequireDecisionToken();
|
|
1761
1956
|
const executeRequest = async () => {
|
|
1762
|
-
if (!this.config.local && this.heartbeatManager && req.signingContext?.signerId) {
|
|
1763
|
-
this.heartbeatManager.updateSignerId(req.signingContext.signerId);
|
|
1764
|
-
}
|
|
1765
1957
|
let heartbeatToken = null;
|
|
1766
1958
|
if (!this.config.local && this.heartbeatManager) {
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
const maxWaitMs = 2e3;
|
|
1770
|
-
const startTime2 = Date.now();
|
|
1771
|
-
while (!heartbeatToken && Date.now() - startTime2 < maxWaitMs) {
|
|
1772
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
1773
|
-
heartbeatToken = this.heartbeatManager.getToken();
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
if (!heartbeatToken) {
|
|
1777
|
-
throw new GateError(
|
|
1778
|
-
"HEARTBEAT_MISSING" /* HEARTBEAT_MISSING */,
|
|
1779
|
-
"Signing blocked: Heartbeat token is missing or expired. Gate must be alive and enforcing policy."
|
|
1780
|
-
);
|
|
1781
|
-
}
|
|
1959
|
+
const effectiveSignerId2 = req.signingContext?.signerId ?? req.signingContext?.actorPrincipal ?? DEFAULT_SIGNER_ID;
|
|
1960
|
+
heartbeatToken = await this.heartbeatManager.getTokenForSigner(effectiveSignerId2, 2e3);
|
|
1782
1961
|
}
|
|
1783
1962
|
const txIntent = { ...req.txIntent };
|
|
1784
1963
|
if (txIntent.to && !txIntent.toAddress) {
|
|
@@ -1791,10 +1970,11 @@ var GateClient = class {
|
|
|
1791
1970
|
if (txIntent.from && !txIntent.fromAddress) {
|
|
1792
1971
|
delete txIntent.from;
|
|
1793
1972
|
}
|
|
1973
|
+
const effectiveSignerId = req.signingContext?.signerId ?? req.signingContext?.actorPrincipal ?? DEFAULT_SIGNER_ID;
|
|
1794
1974
|
const signingContext = {
|
|
1795
1975
|
...req.signingContext,
|
|
1796
|
-
actorPrincipal: req.signingContext?.actorPrincipal ?? req.signingContext?.signerId ??
|
|
1797
|
-
signerId:
|
|
1976
|
+
actorPrincipal: req.signingContext?.actorPrincipal ?? req.signingContext?.signerId ?? DEFAULT_SIGNER_ID,
|
|
1977
|
+
signerId: effectiveSignerId
|
|
1798
1978
|
};
|
|
1799
1979
|
if (heartbeatToken) {
|
|
1800
1980
|
signingContext.heartbeatToken = heartbeatToken;
|
|
@@ -1911,6 +2091,9 @@ var GateClient = class {
|
|
|
1911
2091
|
enforced: responseData.enforced ?? requestMode === "ENFORCE",
|
|
1912
2092
|
shadowWouldBlock: responseData.shadow_would_block ?? responseData.shadowWouldBlock ?? false,
|
|
1913
2093
|
mode: responseData.mode ?? requestMode,
|
|
2094
|
+
receipt: responseData.receipt,
|
|
2095
|
+
decisionHash: responseData.decision_hash ?? responseData.decisionHash,
|
|
2096
|
+
receiptSignature: responseData.receipt_signature ?? responseData.receiptSignature,
|
|
1914
2097
|
...simulationData ? {
|
|
1915
2098
|
simulation: {
|
|
1916
2099
|
willRevert: simulationData.willRevert ?? simulationData.will_revert ?? false,
|