blockintel-gate-sdk 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +255 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +91 -1
- package/dist/index.d.ts +91 -1
- package/dist/index.js +255 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -50,6 +50,55 @@ var init_canonicalJson = __esm({
|
|
|
50
50
|
"src/utils/canonicalJson.ts"() {
|
|
51
51
|
}
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
// src/utils/decisionTokenVerify.ts
|
|
55
|
+
var decisionTokenVerify_exports = {};
|
|
56
|
+
__export(decisionTokenVerify_exports, {
|
|
57
|
+
decodeJwtUnsafe: () => decodeJwtUnsafe,
|
|
58
|
+
verifyDecisionTokenRs256: () => verifyDecisionTokenRs256
|
|
59
|
+
});
|
|
60
|
+
function decodeJwtUnsafe(token) {
|
|
61
|
+
try {
|
|
62
|
+
const parts = token.split(".");
|
|
63
|
+
if (parts.length !== 3) return null;
|
|
64
|
+
const header = JSON.parse(
|
|
65
|
+
Buffer.from(parts[0], "base64url").toString("utf8")
|
|
66
|
+
);
|
|
67
|
+
const payload = JSON.parse(
|
|
68
|
+
Buffer.from(parts[1], "base64url").toString("utf8")
|
|
69
|
+
);
|
|
70
|
+
return { header, payload };
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function verifyDecisionTokenRs256(token, publicKeyPem) {
|
|
76
|
+
const decoded = decodeJwtUnsafe(token);
|
|
77
|
+
if (!decoded || (decoded.header.alg || "").toUpperCase() !== "RS256") return null;
|
|
78
|
+
const { payload } = decoded;
|
|
79
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
80
|
+
if (payload.iss !== ISS || payload.aud !== AUD) return null;
|
|
81
|
+
if (payload.exp != null && payload.exp < now - 5) return null;
|
|
82
|
+
try {
|
|
83
|
+
const parts = token.split(".");
|
|
84
|
+
const signingInput = `${parts[0]}.${parts[1]}`;
|
|
85
|
+
const signature = Buffer.from(parts[2], "base64url");
|
|
86
|
+
const verify = crypto.createVerify("RSA-SHA256");
|
|
87
|
+
verify.update(signingInput);
|
|
88
|
+
verify.end();
|
|
89
|
+
const ok = verify.verify(publicKeyPem, signature);
|
|
90
|
+
return ok ? payload : null;
|
|
91
|
+
} catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
var ISS, AUD;
|
|
96
|
+
var init_decisionTokenVerify = __esm({
|
|
97
|
+
"src/utils/decisionTokenVerify.ts"() {
|
|
98
|
+
ISS = "blockintel-gate";
|
|
99
|
+
AUD = "gate-decision";
|
|
100
|
+
}
|
|
101
|
+
});
|
|
53
102
|
async function hmacSha256(secret, message) {
|
|
54
103
|
const hmac = crypto.createHmac("sha256", secret);
|
|
55
104
|
hmac.update(message, "utf8");
|
|
@@ -922,6 +971,75 @@ var MetricsCollector = class {
|
|
|
922
971
|
this.latencyMs = [];
|
|
923
972
|
}
|
|
924
973
|
};
|
|
974
|
+
function canonicalJsonBinding(obj) {
|
|
975
|
+
if (obj === null || obj === void 0) return "null";
|
|
976
|
+
if (typeof obj === "string") return JSON.stringify(obj);
|
|
977
|
+
if (typeof obj === "number") return obj.toString();
|
|
978
|
+
if (typeof obj === "boolean") return obj ? "true" : "false";
|
|
979
|
+
if (Array.isArray(obj)) {
|
|
980
|
+
const items = obj.map((item) => canonicalJsonBinding(item));
|
|
981
|
+
return "[" + items.join(",") + "]";
|
|
982
|
+
}
|
|
983
|
+
if (typeof obj === "object") {
|
|
984
|
+
const keys = Object.keys(obj).sort();
|
|
985
|
+
const pairs = [];
|
|
986
|
+
for (const key of keys) {
|
|
987
|
+
const value = obj[key];
|
|
988
|
+
if (value !== void 0) {
|
|
989
|
+
pairs.push(JSON.stringify(key) + ":" + canonicalJsonBinding(value));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return "{" + pairs.join(",") + "}";
|
|
993
|
+
}
|
|
994
|
+
return JSON.stringify(obj);
|
|
995
|
+
}
|
|
996
|
+
function normalizeAddress(addr) {
|
|
997
|
+
if (addr == null || addr === "") return "";
|
|
998
|
+
const s = String(addr).trim();
|
|
999
|
+
if (s.startsWith("0x")) return s.toLowerCase();
|
|
1000
|
+
return "0x" + s.toLowerCase();
|
|
1001
|
+
}
|
|
1002
|
+
function normalizeData(data) {
|
|
1003
|
+
if (data == null || data === "") return "";
|
|
1004
|
+
const s = String(data).trim().toLowerCase();
|
|
1005
|
+
return s.startsWith("0x") ? s : "0x" + s;
|
|
1006
|
+
}
|
|
1007
|
+
function buildTxBindingObject(txIntent, signerId, decodedRecipient, decodedFields, fromAddress) {
|
|
1008
|
+
const toAddr = txIntent.toAddress ?? txIntent.to ?? "";
|
|
1009
|
+
const value = (txIntent.valueAtomic ?? txIntent.valueDecimal ?? txIntent.value ?? "0").toString();
|
|
1010
|
+
const data = normalizeData(
|
|
1011
|
+
txIntent.data ?? txIntent.payloadHash ?? txIntent.dataHash ?? ""
|
|
1012
|
+
);
|
|
1013
|
+
const chainId = (txIntent.chainId ?? txIntent.chain ?? "").toString();
|
|
1014
|
+
const toAddress = normalizeAddress(toAddr);
|
|
1015
|
+
const nonce = txIntent.nonce != null ? String(txIntent.nonce) : "";
|
|
1016
|
+
const decoded = {};
|
|
1017
|
+
if (decodedFields && typeof decodedFields === "object") {
|
|
1018
|
+
for (const [k, v] of Object.entries(decodedFields)) {
|
|
1019
|
+
if (v !== void 0) decoded[k] = v;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
const out = {
|
|
1023
|
+
chainId,
|
|
1024
|
+
toAddress,
|
|
1025
|
+
value,
|
|
1026
|
+
data,
|
|
1027
|
+
nonce
|
|
1028
|
+
};
|
|
1029
|
+
if (fromAddress) out.fromAddress = normalizeAddress(fromAddress);
|
|
1030
|
+
if (decodedRecipient != null)
|
|
1031
|
+
out.decodedRecipient = decodedRecipient ? normalizeAddress(decodedRecipient) : null;
|
|
1032
|
+
if (Object.keys(decoded).length > 0) out.decoded = decoded;
|
|
1033
|
+
if (signerId) out.signerId = signerId;
|
|
1034
|
+
if (txIntent.networkFamily) out.networkFamily = txIntent.networkFamily;
|
|
1035
|
+
return out;
|
|
1036
|
+
}
|
|
1037
|
+
function computeTxDigest(binding) {
|
|
1038
|
+
const canonical = canonicalJsonBinding(binding);
|
|
1039
|
+
return crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/kms/wrapAwsSdkV3KmsClient.ts
|
|
925
1043
|
function wrapKmsClient(kmsClient, gateClient, options = {}) {
|
|
926
1044
|
const defaultOptions = {
|
|
927
1045
|
mode: options.mode || "enforce",
|
|
@@ -997,6 +1115,34 @@ async function handleSignCommand(command, originalClient, gateClient, options) {
|
|
|
997
1115
|
// Type assertion - txIntent may have extra fields
|
|
998
1116
|
signingContext
|
|
999
1117
|
});
|
|
1118
|
+
if (decision.decision === "ALLOW" && gateClient.getRequireDecisionToken() && decision.txDigest != null) {
|
|
1119
|
+
const binding = buildTxBindingObject(
|
|
1120
|
+
txIntent,
|
|
1121
|
+
signerId,
|
|
1122
|
+
void 0,
|
|
1123
|
+
void 0,
|
|
1124
|
+
signingContext.actorPrincipal
|
|
1125
|
+
);
|
|
1126
|
+
const computedDigest = computeTxDigest(binding);
|
|
1127
|
+
if (computedDigest !== decision.txDigest) {
|
|
1128
|
+
options.onDecision("BLOCK", {
|
|
1129
|
+
error: new BlockIntelBlockedError(
|
|
1130
|
+
"DECISION_TOKEN_TX_MISMATCH",
|
|
1131
|
+
decision.decisionId,
|
|
1132
|
+
decision.correlationId,
|
|
1133
|
+
void 0
|
|
1134
|
+
),
|
|
1135
|
+
signerId,
|
|
1136
|
+
command
|
|
1137
|
+
});
|
|
1138
|
+
throw new BlockIntelBlockedError(
|
|
1139
|
+
"DECISION_TOKEN_TX_MISMATCH",
|
|
1140
|
+
decision.decisionId,
|
|
1141
|
+
decision.correlationId,
|
|
1142
|
+
void 0
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1000
1146
|
options.onDecision("ALLOW", { decision, signerId, command });
|
|
1001
1147
|
if (options.mode === "dry-run") {
|
|
1002
1148
|
return await originalClient.send(new clientKms.SignCommand(command));
|
|
@@ -1593,6 +1739,16 @@ var GateClient = class {
|
|
|
1593
1739
|
});
|
|
1594
1740
|
}
|
|
1595
1741
|
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Whether the SDK requires a decision token for ALLOW before sign (ENFORCE/HARD).
|
|
1744
|
+
* Env GATE_REQUIRE_DECISION_TOKEN overrides config.
|
|
1745
|
+
*/
|
|
1746
|
+
getRequireDecisionToken() {
|
|
1747
|
+
if (typeof process !== "undefined" && process.env.GATE_REQUIRE_DECISION_TOKEN !== void 0) {
|
|
1748
|
+
return process.env.GATE_REQUIRE_DECISION_TOKEN === "true" || process.env.GATE_REQUIRE_DECISION_TOKEN === "1";
|
|
1749
|
+
}
|
|
1750
|
+
return this.config.requireDecisionToken ?? (this.mode === "ENFORCE" || this.config.enforcementMode === "HARD");
|
|
1751
|
+
}
|
|
1596
1752
|
/**
|
|
1597
1753
|
* Perform async IAM permission risk check (non-blocking)
|
|
1598
1754
|
*
|
|
@@ -1623,6 +1779,7 @@ var GateClient = class {
|
|
|
1623
1779
|
const startTime = Date.now();
|
|
1624
1780
|
const failSafeMode = this.config.failSafeMode ?? "ALLOW_ON_TIMEOUT";
|
|
1625
1781
|
const requestMode = req.mode || this.mode;
|
|
1782
|
+
const requireToken = this.getRequireDecisionToken();
|
|
1626
1783
|
const executeRequest = async () => {
|
|
1627
1784
|
if (!this.config.local && this.heartbeatManager && req.signingContext?.signerId) {
|
|
1628
1785
|
this.heartbeatManager.updateSignerId(req.signingContext.signerId);
|
|
@@ -1765,6 +1922,10 @@ var GateClient = class {
|
|
|
1765
1922
|
reasonCodes: responseData.reason_codes ?? responseData.reasonCodes ?? [],
|
|
1766
1923
|
policyVersion: responseData.policy_version ?? responseData.policyVersion,
|
|
1767
1924
|
correlationId: responseData.correlation_id ?? responseData.correlationId,
|
|
1925
|
+
decisionId: responseData.decision_id ?? responseData.decisionId,
|
|
1926
|
+
decisionToken: responseData.decision_token ?? responseData.decisionToken,
|
|
1927
|
+
expiresAt: responseData.expires_at ?? responseData.expiresAt,
|
|
1928
|
+
txDigest: responseData.tx_digest ?? responseData.txDigest,
|
|
1768
1929
|
stepUp: responseData.step_up ? {
|
|
1769
1930
|
requestId: responseData.step_up.request_id ?? (responseData.stepUp?.requestId ?? ""),
|
|
1770
1931
|
ttlSeconds: responseData.step_up.ttl_seconds ?? responseData.stepUp?.ttlSeconds
|
|
@@ -1780,9 +1941,100 @@ var GateClient = class {
|
|
|
1780
1941
|
errorReason: simulationData.errorReason ?? simulationData.error_reason
|
|
1781
1942
|
},
|
|
1782
1943
|
simulationLatencyMs: metadata.simulationLatencyMs ?? metadata.simulation_latency_ms
|
|
1783
|
-
} : {}
|
|
1944
|
+
} : {},
|
|
1945
|
+
metadata: {
|
|
1946
|
+
evaluationLatencyMs: metadata.evaluationLatencyMs ?? metadata.evaluation_latency_ms,
|
|
1947
|
+
policyHash: metadata.policyHash ?? metadata.policy_hash,
|
|
1948
|
+
snapshotVersion: metadata.snapshotVersion ?? metadata.snapshot_version
|
|
1949
|
+
}
|
|
1784
1950
|
};
|
|
1785
1951
|
const latencyMs = Date.now() - startTime;
|
|
1952
|
+
const expectedPolicyHash = this.config.expectedPolicyHash;
|
|
1953
|
+
const expectedSnapshotVersion = this.config.expectedSnapshotVersion;
|
|
1954
|
+
if (expectedPolicyHash != null && result.metadata?.policyHash !== expectedPolicyHash) {
|
|
1955
|
+
if (this.config.debug) {
|
|
1956
|
+
console.warn("[GATE SDK] Policy hash mismatch (pinning)", {
|
|
1957
|
+
expected: expectedPolicyHash,
|
|
1958
|
+
received: result.metadata?.policyHash,
|
|
1959
|
+
requestId
|
|
1960
|
+
});
|
|
1961
|
+
}
|
|
1962
|
+
this.metrics.recordRequest("BLOCK", latencyMs);
|
|
1963
|
+
throw new BlockIntelBlockedError(
|
|
1964
|
+
"POLICY_HASH_MISMATCH",
|
|
1965
|
+
result.decisionId ?? requestId,
|
|
1966
|
+
result.correlationId,
|
|
1967
|
+
requestId
|
|
1968
|
+
);
|
|
1969
|
+
}
|
|
1970
|
+
if (expectedSnapshotVersion != null && result.metadata?.snapshotVersion !== void 0 && result.metadata.snapshotVersion !== expectedSnapshotVersion) {
|
|
1971
|
+
if (this.config.debug) {
|
|
1972
|
+
console.warn("[GATE SDK] Snapshot version mismatch (pinning)", {
|
|
1973
|
+
expected: expectedSnapshotVersion,
|
|
1974
|
+
received: result.metadata?.snapshotVersion,
|
|
1975
|
+
requestId
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
this.metrics.recordRequest("BLOCK", latencyMs);
|
|
1979
|
+
throw new BlockIntelBlockedError(
|
|
1980
|
+
"SNAPSHOT_VERSION_MISMATCH",
|
|
1981
|
+
result.decisionId ?? requestId,
|
|
1982
|
+
result.correlationId,
|
|
1983
|
+
requestId
|
|
1984
|
+
);
|
|
1985
|
+
}
|
|
1986
|
+
if (requireToken && requestMode === "ENFORCE" && result.decision === "ALLOW" && !this.config.local) {
|
|
1987
|
+
if (!result.decisionToken || !result.txDigest) {
|
|
1988
|
+
this.metrics.recordRequest("BLOCK", latencyMs);
|
|
1989
|
+
throw new BlockIntelBlockedError(
|
|
1990
|
+
"DECISION_TOKEN_MISSING",
|
|
1991
|
+
result.decisionId ?? requestId,
|
|
1992
|
+
result.correlationId,
|
|
1993
|
+
requestId
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
1997
|
+
if (result.expiresAt != null && result.expiresAt < nowSec - 5) {
|
|
1998
|
+
this.metrics.recordRequest("BLOCK", latencyMs);
|
|
1999
|
+
throw new BlockIntelBlockedError(
|
|
2000
|
+
"DECISION_TOKEN_EXPIRED",
|
|
2001
|
+
result.decisionId ?? requestId,
|
|
2002
|
+
result.correlationId,
|
|
2003
|
+
requestId
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
const publicKeyPem = this.config.decisionTokenPublicKey;
|
|
2007
|
+
if (publicKeyPem && result.decisionToken) {
|
|
2008
|
+
const { decodeJwtUnsafe: decodeJwtUnsafe2, verifyDecisionTokenRs256: verifyDecisionTokenRs2562 } = await Promise.resolve().then(() => (init_decisionTokenVerify(), decisionTokenVerify_exports));
|
|
2009
|
+
const decoded = decodeJwtUnsafe2(result.decisionToken);
|
|
2010
|
+
if (decoded && (decoded.header.alg || "").toUpperCase() === "RS256") {
|
|
2011
|
+
const resolvedPem = publicKeyPem.startsWith("-----") ? publicKeyPem : Buffer.from(publicKeyPem, "base64").toString("utf8");
|
|
2012
|
+
const verified = verifyDecisionTokenRs2562(result.decisionToken, resolvedPem);
|
|
2013
|
+
if (verified === null) {
|
|
2014
|
+
this.metrics.recordRequest("BLOCK", latencyMs);
|
|
2015
|
+
throw new BlockIntelBlockedError(
|
|
2016
|
+
"DECISION_TOKEN_INVALID",
|
|
2017
|
+
result.decisionId ?? requestId,
|
|
2018
|
+
result.correlationId,
|
|
2019
|
+
requestId
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
const signerId = signingContext?.signerId ?? req.signingContext?.signerId;
|
|
2025
|
+
const fromAddress = txIntent.fromAddress ?? txIntent.from;
|
|
2026
|
+
const binding = buildTxBindingObject(txIntent, signerId, void 0, void 0, fromAddress);
|
|
2027
|
+
const computedDigest = computeTxDigest(binding);
|
|
2028
|
+
if (computedDigest !== result.txDigest) {
|
|
2029
|
+
this.metrics.recordRequest("BLOCK", latencyMs);
|
|
2030
|
+
throw new BlockIntelBlockedError(
|
|
2031
|
+
"DECISION_TOKEN_DIGEST_MISMATCH",
|
|
2032
|
+
result.decisionId ?? requestId,
|
|
2033
|
+
result.correlationId,
|
|
2034
|
+
requestId
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
1786
2038
|
if (result.decision === "BLOCK") {
|
|
1787
2039
|
if (requestMode === "SHADOW") {
|
|
1788
2040
|
console.warn("[GATE SHADOW MODE] Would have blocked transaction", {
|
|
@@ -2014,6 +2266,8 @@ exports.GateErrorCode = GateErrorCode;
|
|
|
2014
2266
|
exports.HeartbeatManager = HeartbeatManager;
|
|
2015
2267
|
exports.ProvenanceProvider = ProvenanceProvider;
|
|
2016
2268
|
exports.StepUpNotConfiguredError = StepUpNotConfiguredError;
|
|
2269
|
+
exports.buildTxBindingObject = buildTxBindingObject;
|
|
2270
|
+
exports.computeTxDigest = computeTxDigest;
|
|
2017
2271
|
exports.createGateClient = createGateClient;
|
|
2018
2272
|
exports.default = GateClient;
|
|
2019
2273
|
exports.wrapKmsClient = wrapKmsClient;
|