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 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;