blockintel-gate-sdk 0.3.7 → 0.3.9
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 +2 -0
- package/dist/contracts-KKk945Ox.d.cts +380 -0
- package/dist/contracts-KKk945Ox.d.ts +380 -0
- package/dist/index.cjs +1020 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +392 -298
- package/dist/index.d.ts +392 -298
- package/dist/index.js +1015 -4
- package/dist/index.js.map +1 -1
- package/dist/pilot/index.cjs +2401 -0
- package/dist/pilot/index.cjs.map +1 -0
- package/dist/pilot/index.d.cts +38 -0
- package/dist/pilot/index.d.ts +38 -0
- package/dist/pilot/index.js +2397 -0
- package/dist/pilot/index.js.map +1 -0
- package/package.json +14 -1
package/dist/index.cjs
CHANGED
|
@@ -5,7 +5,9 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
var crypto = require('crypto');
|
|
6
6
|
var uuid = require('uuid');
|
|
7
7
|
var clientKms = require('@aws-sdk/client-kms');
|
|
8
|
+
var module$1 = require('module');
|
|
8
9
|
|
|
10
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
9
11
|
var __defProp = Object.defineProperty;
|
|
10
12
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
11
13
|
var __esm = (fn, res) => function __init() {
|
|
@@ -50,6 +52,55 @@ var init_canonicalJson = __esm({
|
|
|
50
52
|
"src/utils/canonicalJson.ts"() {
|
|
51
53
|
}
|
|
52
54
|
});
|
|
55
|
+
|
|
56
|
+
// src/utils/decisionTokenVerify.ts
|
|
57
|
+
var decisionTokenVerify_exports = {};
|
|
58
|
+
__export(decisionTokenVerify_exports, {
|
|
59
|
+
decodeJwtUnsafe: () => decodeJwtUnsafe,
|
|
60
|
+
verifyDecisionTokenRs256: () => verifyDecisionTokenRs256
|
|
61
|
+
});
|
|
62
|
+
function decodeJwtUnsafe(token) {
|
|
63
|
+
try {
|
|
64
|
+
const parts = token.split(".");
|
|
65
|
+
if (parts.length !== 3) return null;
|
|
66
|
+
const header = JSON.parse(
|
|
67
|
+
Buffer.from(parts[0], "base64url").toString("utf8")
|
|
68
|
+
);
|
|
69
|
+
const payload = JSON.parse(
|
|
70
|
+
Buffer.from(parts[1], "base64url").toString("utf8")
|
|
71
|
+
);
|
|
72
|
+
return { header, payload };
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function verifyDecisionTokenRs256(token, publicKeyPem) {
|
|
78
|
+
const decoded = decodeJwtUnsafe(token);
|
|
79
|
+
if (!decoded || (decoded.header.alg || "").toUpperCase() !== "RS256") return null;
|
|
80
|
+
const { payload } = decoded;
|
|
81
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
82
|
+
if (payload.iss !== ISS || payload.aud !== AUD) return null;
|
|
83
|
+
if (payload.exp != null && payload.exp < now - 5) return null;
|
|
84
|
+
try {
|
|
85
|
+
const parts = token.split(".");
|
|
86
|
+
const signingInput = `${parts[0]}.${parts[1]}`;
|
|
87
|
+
const signature = Buffer.from(parts[2], "base64url");
|
|
88
|
+
const verify = crypto.createVerify("RSA-SHA256");
|
|
89
|
+
verify.update(signingInput);
|
|
90
|
+
verify.end();
|
|
91
|
+
const ok = verify.verify(publicKeyPem, signature);
|
|
92
|
+
return ok ? payload : null;
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
var ISS, AUD;
|
|
98
|
+
var init_decisionTokenVerify = __esm({
|
|
99
|
+
"src/utils/decisionTokenVerify.ts"() {
|
|
100
|
+
ISS = "blockintel-gate";
|
|
101
|
+
AUD = "gate-decision";
|
|
102
|
+
}
|
|
103
|
+
});
|
|
53
104
|
async function hmacSha256(secret, message) {
|
|
54
105
|
const hmac = crypto.createHmac("sha256", secret);
|
|
55
106
|
hmac.update(message, "utf8");
|
|
@@ -869,6 +920,12 @@ var MetricsCollector = class {
|
|
|
869
920
|
this.circuitBreakerOpenTotal++;
|
|
870
921
|
this.emitMetrics();
|
|
871
922
|
}
|
|
923
|
+
/**
|
|
924
|
+
* Record soft-enforce override (app chose to sign despite BLOCK decision)
|
|
925
|
+
*/
|
|
926
|
+
recordSoftBlockOverride(decision) {
|
|
927
|
+
this.emitMetrics();
|
|
928
|
+
}
|
|
872
929
|
/**
|
|
873
930
|
* Get current metrics snapshot
|
|
874
931
|
*/
|
|
@@ -922,6 +979,75 @@ var MetricsCollector = class {
|
|
|
922
979
|
this.latencyMs = [];
|
|
923
980
|
}
|
|
924
981
|
};
|
|
982
|
+
function canonicalJsonBinding(obj) {
|
|
983
|
+
if (obj === null || obj === void 0) return "null";
|
|
984
|
+
if (typeof obj === "string") return JSON.stringify(obj);
|
|
985
|
+
if (typeof obj === "number") return obj.toString();
|
|
986
|
+
if (typeof obj === "boolean") return obj ? "true" : "false";
|
|
987
|
+
if (Array.isArray(obj)) {
|
|
988
|
+
const items = obj.map((item) => canonicalJsonBinding(item));
|
|
989
|
+
return "[" + items.join(",") + "]";
|
|
990
|
+
}
|
|
991
|
+
if (typeof obj === "object") {
|
|
992
|
+
const keys = Object.keys(obj).sort();
|
|
993
|
+
const pairs = [];
|
|
994
|
+
for (const key of keys) {
|
|
995
|
+
const value = obj[key];
|
|
996
|
+
if (value !== void 0) {
|
|
997
|
+
pairs.push(JSON.stringify(key) + ":" + canonicalJsonBinding(value));
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
return "{" + pairs.join(",") + "}";
|
|
1001
|
+
}
|
|
1002
|
+
return JSON.stringify(obj);
|
|
1003
|
+
}
|
|
1004
|
+
function normalizeAddress(addr) {
|
|
1005
|
+
if (addr == null || addr === "") return "";
|
|
1006
|
+
const s = String(addr).trim();
|
|
1007
|
+
if (s.startsWith("0x")) return s.toLowerCase();
|
|
1008
|
+
return "0x" + s.toLowerCase();
|
|
1009
|
+
}
|
|
1010
|
+
function normalizeData(data) {
|
|
1011
|
+
if (data == null || data === "") return "";
|
|
1012
|
+
const s = String(data).trim().toLowerCase();
|
|
1013
|
+
return s.startsWith("0x") ? s : "0x" + s;
|
|
1014
|
+
}
|
|
1015
|
+
function buildTxBindingObject(txIntent, signerId, decodedRecipient, decodedFields, fromAddress) {
|
|
1016
|
+
const toAddr = txIntent.toAddress ?? txIntent.to ?? "";
|
|
1017
|
+
const value = (txIntent.valueAtomic ?? txIntent.valueDecimal ?? txIntent.value ?? "0").toString();
|
|
1018
|
+
const data = normalizeData(
|
|
1019
|
+
txIntent.data ?? txIntent.payloadHash ?? txIntent.dataHash ?? ""
|
|
1020
|
+
);
|
|
1021
|
+
const chainId = (txIntent.chainId ?? txIntent.chain ?? "").toString();
|
|
1022
|
+
const toAddress = normalizeAddress(toAddr);
|
|
1023
|
+
const nonce = txIntent.nonce != null ? String(txIntent.nonce) : "";
|
|
1024
|
+
const decoded = {};
|
|
1025
|
+
if (decodedFields && typeof decodedFields === "object") {
|
|
1026
|
+
for (const [k, v] of Object.entries(decodedFields)) {
|
|
1027
|
+
if (v !== void 0) decoded[k] = v;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
const out = {
|
|
1031
|
+
chainId,
|
|
1032
|
+
toAddress,
|
|
1033
|
+
value,
|
|
1034
|
+
data,
|
|
1035
|
+
nonce
|
|
1036
|
+
};
|
|
1037
|
+
if (fromAddress) out.fromAddress = normalizeAddress(fromAddress);
|
|
1038
|
+
if (decodedRecipient != null)
|
|
1039
|
+
out.decodedRecipient = decodedRecipient ? normalizeAddress(decodedRecipient) : null;
|
|
1040
|
+
if (Object.keys(decoded).length > 0) out.decoded = decoded;
|
|
1041
|
+
if (signerId) out.signerId = signerId;
|
|
1042
|
+
if (txIntent.networkFamily) out.networkFamily = txIntent.networkFamily;
|
|
1043
|
+
return out;
|
|
1044
|
+
}
|
|
1045
|
+
function computeTxDigest(binding) {
|
|
1046
|
+
const canonical = canonicalJsonBinding(binding);
|
|
1047
|
+
return crypto.createHash("sha256").update(canonical, "utf8").digest("hex");
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// src/kms/wrapAwsSdkV3KmsClient.ts
|
|
925
1051
|
function wrapKmsClient(kmsClient, gateClient, options = {}) {
|
|
926
1052
|
const defaultOptions = {
|
|
927
1053
|
mode: options.mode || "enforce",
|
|
@@ -997,6 +1123,34 @@ async function handleSignCommand(command, originalClient, gateClient, options) {
|
|
|
997
1123
|
// Type assertion - txIntent may have extra fields
|
|
998
1124
|
signingContext
|
|
999
1125
|
});
|
|
1126
|
+
if (decision.decision === "ALLOW" && gateClient.getRequireDecisionToken() && decision.txDigest != null) {
|
|
1127
|
+
const binding = buildTxBindingObject(
|
|
1128
|
+
txIntent,
|
|
1129
|
+
signerId,
|
|
1130
|
+
void 0,
|
|
1131
|
+
void 0,
|
|
1132
|
+
signingContext.actorPrincipal
|
|
1133
|
+
);
|
|
1134
|
+
const computedDigest = computeTxDigest(binding);
|
|
1135
|
+
if (computedDigest !== decision.txDigest) {
|
|
1136
|
+
options.onDecision("BLOCK", {
|
|
1137
|
+
error: new BlockIntelBlockedError(
|
|
1138
|
+
"DECISION_TOKEN_TX_MISMATCH",
|
|
1139
|
+
decision.decisionId,
|
|
1140
|
+
decision.correlationId,
|
|
1141
|
+
void 0
|
|
1142
|
+
),
|
|
1143
|
+
signerId,
|
|
1144
|
+
command
|
|
1145
|
+
});
|
|
1146
|
+
throw new BlockIntelBlockedError(
|
|
1147
|
+
"DECISION_TOKEN_TX_MISMATCH",
|
|
1148
|
+
decision.decisionId,
|
|
1149
|
+
decision.correlationId,
|
|
1150
|
+
void 0
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1000
1154
|
options.onDecision("ALLOW", { decision, signerId, command });
|
|
1001
1155
|
if (options.mode === "dry-run") {
|
|
1002
1156
|
return await originalClient.send(new clientKms.SignCommand(command));
|
|
@@ -1593,6 +1747,16 @@ var GateClient = class {
|
|
|
1593
1747
|
});
|
|
1594
1748
|
}
|
|
1595
1749
|
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Whether the SDK requires a decision token for ALLOW before sign (ENFORCE/HARD).
|
|
1752
|
+
* Env GATE_REQUIRE_DECISION_TOKEN overrides config.
|
|
1753
|
+
*/
|
|
1754
|
+
getRequireDecisionToken() {
|
|
1755
|
+
if (typeof process !== "undefined" && process.env.GATE_REQUIRE_DECISION_TOKEN !== void 0) {
|
|
1756
|
+
return process.env.GATE_REQUIRE_DECISION_TOKEN === "true" || process.env.GATE_REQUIRE_DECISION_TOKEN === "1";
|
|
1757
|
+
}
|
|
1758
|
+
return this.config.requireDecisionToken ?? (this.mode === "ENFORCE" || this.config.enforcementMode === "HARD");
|
|
1759
|
+
}
|
|
1596
1760
|
/**
|
|
1597
1761
|
* Perform async IAM permission risk check (non-blocking)
|
|
1598
1762
|
*
|
|
@@ -1622,7 +1786,9 @@ var GateClient = class {
|
|
|
1622
1786
|
const timestampMs = req.timestampMs ?? nowMs();
|
|
1623
1787
|
const startTime = Date.now();
|
|
1624
1788
|
const failSafeMode = this.config.failSafeMode ?? "ALLOW_ON_TIMEOUT";
|
|
1789
|
+
const evaluationMode = this.config.evaluationMode ?? "BLOCKING";
|
|
1625
1790
|
const requestMode = req.mode || this.mode;
|
|
1791
|
+
const requireToken = this.getRequireDecisionToken();
|
|
1626
1792
|
const executeRequest = async () => {
|
|
1627
1793
|
if (!this.config.local && this.heartbeatManager && req.signingContext?.signerId) {
|
|
1628
1794
|
this.heartbeatManager.updateSignerId(req.signingContext.signerId);
|
|
@@ -1765,6 +1931,10 @@ var GateClient = class {
|
|
|
1765
1931
|
reasonCodes: responseData.reason_codes ?? responseData.reasonCodes ?? [],
|
|
1766
1932
|
policyVersion: responseData.policy_version ?? responseData.policyVersion,
|
|
1767
1933
|
correlationId: responseData.correlation_id ?? responseData.correlationId,
|
|
1934
|
+
decisionId: responseData.decision_id ?? responseData.decisionId,
|
|
1935
|
+
decisionToken: responseData.decision_token ?? responseData.decisionToken,
|
|
1936
|
+
expiresAt: responseData.expires_at ?? responseData.expiresAt,
|
|
1937
|
+
txDigest: responseData.tx_digest ?? responseData.txDigest,
|
|
1768
1938
|
stepUp: responseData.step_up ? {
|
|
1769
1939
|
requestId: responseData.step_up.request_id ?? (responseData.stepUp?.requestId ?? ""),
|
|
1770
1940
|
ttlSeconds: responseData.step_up.ttl_seconds ?? responseData.stepUp?.ttlSeconds
|
|
@@ -1780,10 +1950,115 @@ var GateClient = class {
|
|
|
1780
1950
|
errorReason: simulationData.errorReason ?? simulationData.error_reason
|
|
1781
1951
|
},
|
|
1782
1952
|
simulationLatencyMs: metadata.simulationLatencyMs ?? metadata.simulation_latency_ms
|
|
1783
|
-
} : {}
|
|
1953
|
+
} : {},
|
|
1954
|
+
metadata: {
|
|
1955
|
+
evaluationLatencyMs: metadata.evaluationLatencyMs ?? metadata.evaluation_latency_ms,
|
|
1956
|
+
policyHash: metadata.policyHash ?? metadata.policy_hash,
|
|
1957
|
+
snapshotVersion: metadata.snapshotVersion ?? metadata.snapshot_version
|
|
1958
|
+
}
|
|
1784
1959
|
};
|
|
1785
1960
|
const latencyMs = Date.now() - startTime;
|
|
1961
|
+
const expectedPolicyHash = this.config.expectedPolicyHash;
|
|
1962
|
+
const expectedSnapshotVersion = this.config.expectedSnapshotVersion;
|
|
1963
|
+
if (expectedPolicyHash != null && result.metadata?.policyHash !== expectedPolicyHash) {
|
|
1964
|
+
if (this.config.debug) {
|
|
1965
|
+
console.warn("[GATE SDK] Policy hash mismatch (pinning)", {
|
|
1966
|
+
expected: expectedPolicyHash,
|
|
1967
|
+
received: result.metadata?.policyHash,
|
|
1968
|
+
requestId
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
this.metrics.recordRequest("BLOCK", latencyMs);
|
|
1972
|
+
throw new BlockIntelBlockedError(
|
|
1973
|
+
"POLICY_HASH_MISMATCH",
|
|
1974
|
+
result.decisionId ?? requestId,
|
|
1975
|
+
result.correlationId,
|
|
1976
|
+
requestId
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
if (expectedSnapshotVersion != null && result.metadata?.snapshotVersion !== void 0 && result.metadata.snapshotVersion !== expectedSnapshotVersion) {
|
|
1980
|
+
if (this.config.debug) {
|
|
1981
|
+
console.warn("[GATE SDK] Snapshot version mismatch (pinning)", {
|
|
1982
|
+
expected: expectedSnapshotVersion,
|
|
1983
|
+
received: result.metadata?.snapshotVersion,
|
|
1984
|
+
requestId
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
this.metrics.recordRequest("BLOCK", latencyMs);
|
|
1988
|
+
throw new BlockIntelBlockedError(
|
|
1989
|
+
"SNAPSHOT_VERSION_MISMATCH",
|
|
1990
|
+
result.decisionId ?? requestId,
|
|
1991
|
+
result.correlationId,
|
|
1992
|
+
requestId
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
if (requireToken && requestMode === "ENFORCE" && result.decision === "ALLOW" && !this.config.local) {
|
|
1996
|
+
if (!result.decisionToken || !result.txDigest) {
|
|
1997
|
+
this.metrics.recordRequest("BLOCK", latencyMs);
|
|
1998
|
+
throw new BlockIntelBlockedError(
|
|
1999
|
+
"DECISION_TOKEN_MISSING",
|
|
2000
|
+
result.decisionId ?? requestId,
|
|
2001
|
+
result.correlationId,
|
|
2002
|
+
requestId
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
2006
|
+
if (result.expiresAt != null && result.expiresAt < nowSec - 5) {
|
|
2007
|
+
this.metrics.recordRequest("BLOCK", latencyMs);
|
|
2008
|
+
throw new BlockIntelBlockedError(
|
|
2009
|
+
"DECISION_TOKEN_EXPIRED",
|
|
2010
|
+
result.decisionId ?? requestId,
|
|
2011
|
+
result.correlationId,
|
|
2012
|
+
requestId
|
|
2013
|
+
);
|
|
2014
|
+
}
|
|
2015
|
+
const publicKeyPem = this.config.decisionTokenPublicKey;
|
|
2016
|
+
if (publicKeyPem && result.decisionToken) {
|
|
2017
|
+
const { decodeJwtUnsafe: decodeJwtUnsafe2, verifyDecisionTokenRs256: verifyDecisionTokenRs2562 } = await Promise.resolve().then(() => (init_decisionTokenVerify(), decisionTokenVerify_exports));
|
|
2018
|
+
const decoded = decodeJwtUnsafe2(result.decisionToken);
|
|
2019
|
+
if (decoded && (decoded.header.alg || "").toUpperCase() === "RS256") {
|
|
2020
|
+
const resolvedPem = publicKeyPem.startsWith("-----") ? publicKeyPem : Buffer.from(publicKeyPem, "base64").toString("utf8");
|
|
2021
|
+
const verified = verifyDecisionTokenRs2562(result.decisionToken, resolvedPem);
|
|
2022
|
+
if (verified === null) {
|
|
2023
|
+
this.metrics.recordRequest("BLOCK", latencyMs);
|
|
2024
|
+
throw new BlockIntelBlockedError(
|
|
2025
|
+
"DECISION_TOKEN_INVALID",
|
|
2026
|
+
result.decisionId ?? requestId,
|
|
2027
|
+
result.correlationId,
|
|
2028
|
+
requestId
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
const signerId = signingContext?.signerId ?? req.signingContext?.signerId;
|
|
2034
|
+
const fromAddress = txIntent.fromAddress ?? txIntent.from;
|
|
2035
|
+
const binding = buildTxBindingObject(txIntent, signerId, void 0, void 0, fromAddress);
|
|
2036
|
+
const computedDigest = computeTxDigest(binding);
|
|
2037
|
+
if (computedDigest !== result.txDigest) {
|
|
2038
|
+
this.metrics.recordRequest("BLOCK", latencyMs);
|
|
2039
|
+
throw new BlockIntelBlockedError(
|
|
2040
|
+
"DECISION_TOKEN_DIGEST_MISMATCH",
|
|
2041
|
+
result.decisionId ?? requestId,
|
|
2042
|
+
result.correlationId,
|
|
2043
|
+
requestId
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
1786
2047
|
if (result.decision === "BLOCK") {
|
|
2048
|
+
if (requestMode === "SOFT_ENFORCE") {
|
|
2049
|
+
console.warn("[SOFT ENFORCE] Policy violation detected - app can override", {
|
|
2050
|
+
requestId,
|
|
2051
|
+
reasonCodes: result.reasonCodes
|
|
2052
|
+
});
|
|
2053
|
+
this.metrics.recordRequest("BLOCK", latencyMs);
|
|
2054
|
+
return {
|
|
2055
|
+
...result,
|
|
2056
|
+
decision: "BLOCK",
|
|
2057
|
+
enforced: false,
|
|
2058
|
+
mode: "SOFT_ENFORCE",
|
|
2059
|
+
warning: "Policy violation detected. Override at your own risk."
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
1787
2062
|
if (requestMode === "SHADOW") {
|
|
1788
2063
|
console.warn("[GATE SHADOW MODE] Would have blocked transaction", {
|
|
1789
2064
|
requestId,
|
|
@@ -1822,6 +2097,26 @@ var GateClient = class {
|
|
|
1822
2097
|
this.metrics.recordRequest("ALLOW", latencyMs);
|
|
1823
2098
|
return result;
|
|
1824
2099
|
};
|
|
2100
|
+
if (evaluationMode === "FIRE_AND_FORGET") {
|
|
2101
|
+
executeRequest().then((res) => {
|
|
2102
|
+
if (res.decision === "BLOCK" || res.shadowWouldBlock) {
|
|
2103
|
+
console.warn("[FIRE-AND-FORGET] Would have blocked:", res.reasonCodes);
|
|
2104
|
+
}
|
|
2105
|
+
this.metrics.recordRequest(res.decision === "ALLOW" ? "ALLOW" : "WOULD_BLOCK", Date.now() - startTime);
|
|
2106
|
+
}).catch((err) => {
|
|
2107
|
+
console.error("[FIRE-AND-FORGET] Attestation failed:", err);
|
|
2108
|
+
this.metrics.recordError();
|
|
2109
|
+
});
|
|
2110
|
+
return {
|
|
2111
|
+
decision: "ALLOW",
|
|
2112
|
+
decisionId: requestId,
|
|
2113
|
+
correlationId: requestId,
|
|
2114
|
+
reasonCodes: [],
|
|
2115
|
+
enforced: false,
|
|
2116
|
+
mode: requestMode,
|
|
2117
|
+
fireAndForget: true
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
1825
2120
|
try {
|
|
1826
2121
|
if (this.circuitBreaker) {
|
|
1827
2122
|
return await this.circuitBreaker.execute(executeRequest);
|
|
@@ -1962,6 +2257,102 @@ var GateClient = class {
|
|
|
1962
2257
|
intervalMs: args.intervalMs ?? this.config.stepUp?.pollingIntervalMs
|
|
1963
2258
|
});
|
|
1964
2259
|
}
|
|
2260
|
+
/**
|
|
2261
|
+
* Evaluate policy and sign in one call when decision is ALLOW.
|
|
2262
|
+
* Convenience for: evaluate → if ALLOW then sign → return { decision, signature }.
|
|
2263
|
+
*/
|
|
2264
|
+
async evaluateAndSign(params) {
|
|
2265
|
+
const decision = await this.evaluate({
|
|
2266
|
+
txIntent: params.txIntent,
|
|
2267
|
+
signingContext: params.signingContext
|
|
2268
|
+
});
|
|
2269
|
+
if (decision.decision === "ALLOW") {
|
|
2270
|
+
const signature = await params.signer.sign({
|
|
2271
|
+
keyId: params.keyId,
|
|
2272
|
+
message: params.message,
|
|
2273
|
+
algorithm: params.algorithm ?? "ECDSA_SHA_256"
|
|
2274
|
+
});
|
|
2275
|
+
return { decision, signature };
|
|
2276
|
+
}
|
|
2277
|
+
return { decision };
|
|
2278
|
+
}
|
|
2279
|
+
/**
|
|
2280
|
+
* Attest a completed signature (post-sign). Use when you want zero latency impact on signing
|
|
2281
|
+
* but still want an audit trail. Policy is evaluated against txIntent; returns ALLOW or
|
|
2282
|
+
* POLICY_VIOLATION_DETECTED. Cannot be used for enforcement (signature already created).
|
|
2283
|
+
*/
|
|
2284
|
+
async attestCompleted(req) {
|
|
2285
|
+
const requestId = uuid.v4();
|
|
2286
|
+
const timestampMs = nowMs();
|
|
2287
|
+
const txIntent = { ...req.txIntent };
|
|
2288
|
+
if (txIntent.to && !txIntent.toAddress) {
|
|
2289
|
+
txIntent.toAddress = txIntent.to;
|
|
2290
|
+
delete txIntent.to;
|
|
2291
|
+
}
|
|
2292
|
+
if (!txIntent.networkFamily && txIntent.chainId) txIntent.networkFamily = "EVM";
|
|
2293
|
+
const signingContext = {
|
|
2294
|
+
...req.signingContext,
|
|
2295
|
+
signerId: req.signingContext?.signerId ?? req.signature.signerId
|
|
2296
|
+
};
|
|
2297
|
+
const body = {
|
|
2298
|
+
tenantId: this.config.tenantId,
|
|
2299
|
+
requestId,
|
|
2300
|
+
timestampMs,
|
|
2301
|
+
txIntent,
|
|
2302
|
+
signature: req.signature,
|
|
2303
|
+
signingContext
|
|
2304
|
+
};
|
|
2305
|
+
let headers = { "Content-Type": "application/json" };
|
|
2306
|
+
if (this.config.local) ; else if (this.hmacSigner) {
|
|
2307
|
+
const { canonicalizeJson: canonicalizeJson2 } = await Promise.resolve().then(() => (init_canonicalJson(), canonicalJson_exports));
|
|
2308
|
+
const canonicalBodyJson = canonicalizeJson2(body);
|
|
2309
|
+
const hmacHeaders = await this.hmacSigner.signRequest({
|
|
2310
|
+
method: "POST",
|
|
2311
|
+
path: "/defense/attest-completed",
|
|
2312
|
+
tenantId: this.config.tenantId,
|
|
2313
|
+
timestampMs,
|
|
2314
|
+
requestId,
|
|
2315
|
+
body
|
|
2316
|
+
});
|
|
2317
|
+
headers = { ...hmacHeaders };
|
|
2318
|
+
body.__canonicalJson = canonicalBodyJson;
|
|
2319
|
+
} else if (this.apiKeyAuth) {
|
|
2320
|
+
const apiKeyHeaders = this.apiKeyAuth.createHeaders({
|
|
2321
|
+
tenantId: this.config.tenantId,
|
|
2322
|
+
timestampMs,
|
|
2323
|
+
requestId
|
|
2324
|
+
});
|
|
2325
|
+
headers = { ...apiKeyHeaders };
|
|
2326
|
+
} else {
|
|
2327
|
+
throw new Error("No authentication configured");
|
|
2328
|
+
}
|
|
2329
|
+
const apiResponse = await this.httpClient.request({
|
|
2330
|
+
method: "POST",
|
|
2331
|
+
path: "/defense/attest-completed",
|
|
2332
|
+
headers,
|
|
2333
|
+
body,
|
|
2334
|
+
requestId
|
|
2335
|
+
});
|
|
2336
|
+
if (apiResponse.success === true && apiResponse.data) {
|
|
2337
|
+
const data = apiResponse.data;
|
|
2338
|
+
if (data.decision === "POLICY_VIOLATION_DETECTED") {
|
|
2339
|
+
console.warn("[POST-SIGN ATTESTATION] Policy violation detected after signing", {
|
|
2340
|
+
requestId,
|
|
2341
|
+
reasonCodes: data.reasonCodes
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
return data;
|
|
2345
|
+
}
|
|
2346
|
+
if (apiResponse.error) {
|
|
2347
|
+
const err = apiResponse.error;
|
|
2348
|
+
throw new GateError(err.code || "SERVER_ERROR", err.message || "Request failed", {
|
|
2349
|
+
status: err.status,
|
|
2350
|
+
correlationId: err.correlationId,
|
|
2351
|
+
requestId
|
|
2352
|
+
});
|
|
2353
|
+
}
|
|
2354
|
+
throw new GateError("INVALID_RESPONSE" /* INVALID_RESPONSE */, "Invalid response from attest-completed", { requestId });
|
|
2355
|
+
}
|
|
1965
2356
|
/**
|
|
1966
2357
|
* Wrap AWS SDK v3 KMS client to intercept SignCommand calls
|
|
1967
2358
|
*
|
|
@@ -1994,6 +2385,39 @@ var Gate = class {
|
|
|
1994
2385
|
constructor(opts) {
|
|
1995
2386
|
this.apiKey = opts?.apiKey ?? process.env.BLOCKINTEL_API_KEY;
|
|
1996
2387
|
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Create a GateClient from environment variables (5-line integration).
|
|
2390
|
+
*
|
|
2391
|
+
* Reads: GATE_BASE_URL, GATE_TENANT_ID, GATE_API_KEY (or GATE_KEY_ID + GATE_HMAC_SECRET), GATE_MODE.
|
|
2392
|
+
*/
|
|
2393
|
+
static fromEnv(overrides) {
|
|
2394
|
+
const baseUrl = process.env.GATE_BASE_URL;
|
|
2395
|
+
const tenantId = process.env.GATE_TENANT_ID;
|
|
2396
|
+
const apiKey = process.env.GATE_API_KEY;
|
|
2397
|
+
const keyId = process.env.GATE_KEY_ID;
|
|
2398
|
+
const hmacSecret = process.env.GATE_HMAC_SECRET;
|
|
2399
|
+
const mode = process.env.GATE_MODE ?? "SHADOW";
|
|
2400
|
+
if (!baseUrl || !tenantId) {
|
|
2401
|
+
throw new Error("GATE_BASE_URL and GATE_TENANT_ID environment variables are required");
|
|
2402
|
+
}
|
|
2403
|
+
let auth;
|
|
2404
|
+
if (apiKey) {
|
|
2405
|
+
auth = { mode: "apiKey", apiKey };
|
|
2406
|
+
} else if (keyId && hmacSecret) {
|
|
2407
|
+
auth = { mode: "hmac", keyId, secret: hmacSecret };
|
|
2408
|
+
} else {
|
|
2409
|
+
throw new Error(
|
|
2410
|
+
"Either GATE_API_KEY or (GATE_KEY_ID and GATE_HMAC_SECRET) environment variables are required"
|
|
2411
|
+
);
|
|
2412
|
+
}
|
|
2413
|
+
return new GateClient({
|
|
2414
|
+
baseUrl,
|
|
2415
|
+
tenantId,
|
|
2416
|
+
auth,
|
|
2417
|
+
mode,
|
|
2418
|
+
...overrides
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
1997
2421
|
/**
|
|
1998
2422
|
* Guard a signing operation. In passthrough mode, executes the callback.
|
|
1999
2423
|
* For full Gate integration, use GateClient with evaluate() before sending.
|
|
@@ -2002,18 +2426,613 @@ var Gate = class {
|
|
|
2002
2426
|
return cb();
|
|
2003
2427
|
}
|
|
2004
2428
|
};
|
|
2429
|
+
var AwsKmsSigner = class {
|
|
2430
|
+
config;
|
|
2431
|
+
constructor(config) {
|
|
2432
|
+
this.config = config;
|
|
2433
|
+
}
|
|
2434
|
+
getName() {
|
|
2435
|
+
return "AWS KMS";
|
|
2436
|
+
}
|
|
2437
|
+
isAvailable() {
|
|
2438
|
+
return !!this.config.kmsClient;
|
|
2439
|
+
}
|
|
2440
|
+
async sign(request) {
|
|
2441
|
+
if (!this.isAvailable()) {
|
|
2442
|
+
throw new Error("AWS KMS client not configured");
|
|
2443
|
+
}
|
|
2444
|
+
const algorithm = this.mapAlgorithm(request.algorithm || this.config.defaultAlgorithm || "ECDSA_SHA_256");
|
|
2445
|
+
const signInput = {
|
|
2446
|
+
KeyId: request.keyId,
|
|
2447
|
+
Message: Buffer.from(request.message),
|
|
2448
|
+
MessageType: request.messageType || this.config.defaultMessageType || "RAW",
|
|
2449
|
+
SigningAlgorithm: algorithm
|
|
2450
|
+
};
|
|
2451
|
+
const command = new clientKms.SignCommand(signInput);
|
|
2452
|
+
const response = await this.config.kmsClient.send(command);
|
|
2453
|
+
if (!response.Signature) {
|
|
2454
|
+
throw new Error("AWS KMS sign response missing signature");
|
|
2455
|
+
}
|
|
2456
|
+
return {
|
|
2457
|
+
signature: Buffer.from(response.Signature),
|
|
2458
|
+
keyId: response.KeyId || request.keyId,
|
|
2459
|
+
algorithm: response.SigningAlgorithm || algorithm,
|
|
2460
|
+
metadata: {
|
|
2461
|
+
keyId: response.KeyId,
|
|
2462
|
+
signingAlgorithm: response.SigningAlgorithm
|
|
2463
|
+
}
|
|
2464
|
+
};
|
|
2465
|
+
}
|
|
2466
|
+
/**
|
|
2467
|
+
* Map algorithm string to AWS KMS SigningAlgorithmSpec
|
|
2468
|
+
*/
|
|
2469
|
+
mapAlgorithm(algorithm) {
|
|
2470
|
+
if (Object.values(clientKms.SigningAlgorithmSpec).includes(algorithm)) {
|
|
2471
|
+
return algorithm;
|
|
2472
|
+
}
|
|
2473
|
+
const algorithmMap = {
|
|
2474
|
+
"ECDSA_SHA_256": clientKms.SigningAlgorithmSpec.ECDSA_SHA_256,
|
|
2475
|
+
"ECDSA_SHA_384": clientKms.SigningAlgorithmSpec.ECDSA_SHA_384,
|
|
2476
|
+
"ECDSA_SHA_512": clientKms.SigningAlgorithmSpec.ECDSA_SHA_512,
|
|
2477
|
+
"RSASSA_PSS_SHA_256": clientKms.SigningAlgorithmSpec.RSASSA_PSS_SHA_256,
|
|
2478
|
+
"RSASSA_PSS_SHA_384": clientKms.SigningAlgorithmSpec.RSASSA_PSS_SHA_384,
|
|
2479
|
+
"RSASSA_PSS_SHA_512": clientKms.SigningAlgorithmSpec.RSASSA_PSS_SHA_512,
|
|
2480
|
+
"RSASSA_PKCS1_V1_5_SHA_256": clientKms.SigningAlgorithmSpec.RSASSA_PKCS1_V1_5_SHA_256,
|
|
2481
|
+
"RSASSA_PKCS1_V1_5_SHA_384": clientKms.SigningAlgorithmSpec.RSASSA_PKCS1_V1_5_SHA_384,
|
|
2482
|
+
"RSASSA_PKCS1_V1_5_SHA_512": clientKms.SigningAlgorithmSpec.RSASSA_PKCS1_V1_5_SHA_512
|
|
2483
|
+
};
|
|
2484
|
+
return algorithmMap[algorithm.toUpperCase()] || clientKms.SigningAlgorithmSpec.ECDSA_SHA_256;
|
|
2485
|
+
}
|
|
2486
|
+
};
|
|
2487
|
+
|
|
2488
|
+
// src/signer/VaultSigner.ts
|
|
2489
|
+
var VaultSigner = class {
|
|
2490
|
+
config;
|
|
2491
|
+
authToken = null;
|
|
2492
|
+
constructor(config) {
|
|
2493
|
+
this.config = {
|
|
2494
|
+
mountPath: "transit",
|
|
2495
|
+
...config
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
getName() {
|
|
2499
|
+
return "HashiCorp Vault";
|
|
2500
|
+
}
|
|
2501
|
+
isAvailable() {
|
|
2502
|
+
return !!this.config.vaultUrl && (!!this.config.token || !!this.config.appRole);
|
|
2503
|
+
}
|
|
2504
|
+
async sign(request) {
|
|
2505
|
+
if (!this.isAvailable()) {
|
|
2506
|
+
throw new Error("Vault signer not configured");
|
|
2507
|
+
}
|
|
2508
|
+
if (!this.authToken && this.config.appRole) {
|
|
2509
|
+
await this.authenticateAppRole();
|
|
2510
|
+
}
|
|
2511
|
+
const token = this.config.token || this.authToken;
|
|
2512
|
+
if (!token) {
|
|
2513
|
+
throw new Error("Vault authentication token not available");
|
|
2514
|
+
}
|
|
2515
|
+
const algorithm = this.mapAlgorithm(request.algorithm || this.config.defaultAlgorithm || "ecdsa-sha2-256");
|
|
2516
|
+
const url = `${this.config.vaultUrl}/v1/${this.config.mountPath}/sign/${request.keyId}`;
|
|
2517
|
+
const messageBase64 = Buffer.from(request.message).toString("base64");
|
|
2518
|
+
const requestBody = {
|
|
2519
|
+
input: messageBase64,
|
|
2520
|
+
...algorithm && { algorithm },
|
|
2521
|
+
...request.options || {}
|
|
2522
|
+
};
|
|
2523
|
+
const timeout = this.config.httpOptions?.timeout || 5e3;
|
|
2524
|
+
const controller = new AbortController();
|
|
2525
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
2526
|
+
try {
|
|
2527
|
+
const response = await fetch(url, {
|
|
2528
|
+
method: "POST",
|
|
2529
|
+
headers: {
|
|
2530
|
+
"Content-Type": "application/json",
|
|
2531
|
+
"X-Vault-Token": token
|
|
2532
|
+
},
|
|
2533
|
+
body: JSON.stringify(requestBody),
|
|
2534
|
+
signal: controller.signal
|
|
2535
|
+
});
|
|
2536
|
+
clearTimeout(timeoutId);
|
|
2537
|
+
if (!response.ok) {
|
|
2538
|
+
const errorText = await response.text();
|
|
2539
|
+
throw new Error(`Vault sign failed: ${response.status} ${errorText}`);
|
|
2540
|
+
}
|
|
2541
|
+
const data = await response.json();
|
|
2542
|
+
if (!data.data || !data.data.signature) {
|
|
2543
|
+
throw new Error("Vault sign response missing signature");
|
|
2544
|
+
}
|
|
2545
|
+
const signatureParts = data.data.signature.split(":");
|
|
2546
|
+
const signatureBase64 = signatureParts[signatureParts.length - 1];
|
|
2547
|
+
const signature = Buffer.from(signatureBase64, "base64");
|
|
2548
|
+
return {
|
|
2549
|
+
signature,
|
|
2550
|
+
keyId: request.keyId,
|
|
2551
|
+
algorithm,
|
|
2552
|
+
metadata: {
|
|
2553
|
+
vaultSignature: data.data.signature,
|
|
2554
|
+
keyVersion: data.data.key_version
|
|
2555
|
+
}
|
|
2556
|
+
};
|
|
2557
|
+
} catch (error) {
|
|
2558
|
+
clearTimeout(timeoutId);
|
|
2559
|
+
if (error.name === "AbortError") {
|
|
2560
|
+
throw new Error("Vault sign request timeout");
|
|
2561
|
+
}
|
|
2562
|
+
throw error;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
/**
|
|
2566
|
+
* Authenticate using AppRole
|
|
2567
|
+
*/
|
|
2568
|
+
async authenticateAppRole() {
|
|
2569
|
+
if (!this.config.appRole) {
|
|
2570
|
+
throw new Error("AppRole not configured");
|
|
2571
|
+
}
|
|
2572
|
+
const url = `${this.config.vaultUrl}/v1/auth/approle/login`;
|
|
2573
|
+
const response = await fetch(url, {
|
|
2574
|
+
method: "POST",
|
|
2575
|
+
headers: {
|
|
2576
|
+
"Content-Type": "application/json"
|
|
2577
|
+
},
|
|
2578
|
+
body: JSON.stringify({
|
|
2579
|
+
role_id: this.config.appRole.roleId,
|
|
2580
|
+
secret_id: this.config.appRole.secretId
|
|
2581
|
+
})
|
|
2582
|
+
});
|
|
2583
|
+
if (!response.ok) {
|
|
2584
|
+
const errorText = await response.text();
|
|
2585
|
+
throw new Error(`Vault AppRole authentication failed: ${response.status} ${errorText}`);
|
|
2586
|
+
}
|
|
2587
|
+
const data = await response.json();
|
|
2588
|
+
if (!data.auth || !data.auth.client_token) {
|
|
2589
|
+
throw new Error("Vault AppRole authentication response missing token");
|
|
2590
|
+
}
|
|
2591
|
+
this.authToken = data.auth.client_token;
|
|
2592
|
+
}
|
|
2593
|
+
/**
|
|
2594
|
+
* Map algorithm string to Vault format
|
|
2595
|
+
*/
|
|
2596
|
+
mapAlgorithm(algorithm) {
|
|
2597
|
+
const algorithmMap = {
|
|
2598
|
+
"ECDSA_SHA_256": "ecdsa-sha2-256",
|
|
2599
|
+
"ECDSA_SHA_384": "ecdsa-sha2-384",
|
|
2600
|
+
"ECDSA_SHA_512": "ecdsa-sha2-512",
|
|
2601
|
+
"RSASSA_PSS_SHA_256": "rsa-sha2-256",
|
|
2602
|
+
"RSASSA_PSS_SHA_384": "rsa-sha2-384",
|
|
2603
|
+
"RSASSA_PSS_SHA_512": "rsa-sha2-512"
|
|
2604
|
+
};
|
|
2605
|
+
if (algorithm.startsWith("ecdsa-") || algorithm.startsWith("rsa-")) {
|
|
2606
|
+
return algorithm;
|
|
2607
|
+
}
|
|
2608
|
+
return algorithmMap[algorithm.toUpperCase()] || "ecdsa-sha2-256";
|
|
2609
|
+
}
|
|
2610
|
+
};
|
|
2611
|
+
|
|
2612
|
+
// src/signer/GcpKmsSigner.ts
|
|
2613
|
+
var GcpKmsSigner = class {
|
|
2614
|
+
config;
|
|
2615
|
+
accessToken = null;
|
|
2616
|
+
tokenExpiry = 0;
|
|
2617
|
+
constructor(config) {
|
|
2618
|
+
this.config = {
|
|
2619
|
+
useWorkloadIdentity: false,
|
|
2620
|
+
...config
|
|
2621
|
+
};
|
|
2622
|
+
}
|
|
2623
|
+
getName() {
|
|
2624
|
+
return "Google Cloud KMS";
|
|
2625
|
+
}
|
|
2626
|
+
isAvailable() {
|
|
2627
|
+
if (this.config.useWorkloadIdentity) {
|
|
2628
|
+
return true;
|
|
2629
|
+
}
|
|
2630
|
+
return !!this.config.credentials && !!this.config.projectId;
|
|
2631
|
+
}
|
|
2632
|
+
async sign(request) {
|
|
2633
|
+
if (!this.isAvailable()) {
|
|
2634
|
+
throw new Error("GCP KMS signer not configured");
|
|
2635
|
+
}
|
|
2636
|
+
const accessToken = await this.getAccessToken();
|
|
2637
|
+
const algorithm = this.mapAlgorithm(request.algorithm || this.config.defaultAlgorithm || "EC_SIGN_P256_SHA256");
|
|
2638
|
+
const keyName = request.keyId.includes("/") ? request.keyId : `projects/${this.config.projectId}/locations/${this.config.location}/keyRings/${this.config.keyRing}/cryptoKeys/${request.keyId}`;
|
|
2639
|
+
const url = `https://cloudkms.googleapis.com/v1/${keyName}:asymmetricSign`;
|
|
2640
|
+
const messageBase64 = Buffer.from(request.message).toString("base64");
|
|
2641
|
+
const requestBody = {
|
|
2642
|
+
digest: {
|
|
2643
|
+
sha256: messageBase64
|
|
2644
|
+
// GCP expects digest, not raw message
|
|
2645
|
+
}
|
|
2646
|
+
};
|
|
2647
|
+
const timeout = this.config.httpOptions?.timeout || 5e3;
|
|
2648
|
+
const controller = new AbortController();
|
|
2649
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
2650
|
+
try {
|
|
2651
|
+
const response = await fetch(url, {
|
|
2652
|
+
method: "POST",
|
|
2653
|
+
headers: {
|
|
2654
|
+
"Content-Type": "application/json",
|
|
2655
|
+
"Authorization": `Bearer ${accessToken}`
|
|
2656
|
+
},
|
|
2657
|
+
body: JSON.stringify(requestBody),
|
|
2658
|
+
signal: controller.signal
|
|
2659
|
+
});
|
|
2660
|
+
clearTimeout(timeoutId);
|
|
2661
|
+
if (!response.ok) {
|
|
2662
|
+
const errorText = await response.text();
|
|
2663
|
+
throw new Error(`GCP KMS sign failed: ${response.status} ${errorText}`);
|
|
2664
|
+
}
|
|
2665
|
+
const data = await response.json();
|
|
2666
|
+
if (!data.signature) {
|
|
2667
|
+
throw new Error("GCP KMS sign response missing signature");
|
|
2668
|
+
}
|
|
2669
|
+
const signature = Buffer.from(data.signature, "base64");
|
|
2670
|
+
return {
|
|
2671
|
+
signature,
|
|
2672
|
+
keyId: request.keyId,
|
|
2673
|
+
algorithm,
|
|
2674
|
+
metadata: {
|
|
2675
|
+
name: data.name,
|
|
2676
|
+
verifiedDigestCrc32c: data.verifiedDigestCrc32c
|
|
2677
|
+
}
|
|
2678
|
+
};
|
|
2679
|
+
} catch (error) {
|
|
2680
|
+
clearTimeout(timeoutId);
|
|
2681
|
+
if (error.name === "AbortError") {
|
|
2682
|
+
throw new Error("GCP KMS sign request timeout");
|
|
2683
|
+
}
|
|
2684
|
+
throw error;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
/**
|
|
2688
|
+
* Get GCP access token
|
|
2689
|
+
*/
|
|
2690
|
+
async getAccessToken() {
|
|
2691
|
+
if (this.accessToken && Date.now() < this.tokenExpiry - 5 * 60 * 1e3) {
|
|
2692
|
+
return this.accessToken;
|
|
2693
|
+
}
|
|
2694
|
+
if (this.config.useWorkloadIdentity) {
|
|
2695
|
+
const metadataUrl = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token";
|
|
2696
|
+
const response = await fetch(metadataUrl, {
|
|
2697
|
+
method: "GET",
|
|
2698
|
+
headers: {
|
|
2699
|
+
"Metadata-Flavor": "Google"
|
|
2700
|
+
}
|
|
2701
|
+
});
|
|
2702
|
+
if (!response.ok) {
|
|
2703
|
+
throw new Error(`GCP metadata service authentication failed: ${response.status}`);
|
|
2704
|
+
}
|
|
2705
|
+
const data = await response.json();
|
|
2706
|
+
this.accessToken = data.access_token;
|
|
2707
|
+
this.tokenExpiry = Date.now() + data.expires_in * 1e3;
|
|
2708
|
+
return data.access_token;
|
|
2709
|
+
} else {
|
|
2710
|
+
if (!this.config.credentials) {
|
|
2711
|
+
throw new Error("GCP credentials not configured");
|
|
2712
|
+
}
|
|
2713
|
+
throw new Error("Service account authentication requires @google-cloud/kms SDK. Install it with: npm install @google-cloud/kms. Alternatively, use workload identity (recommended for GCP environments).");
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
/**
|
|
2717
|
+
* Map algorithm string to GCP format
|
|
2718
|
+
*/
|
|
2719
|
+
mapAlgorithm(algorithm) {
|
|
2720
|
+
const algorithmMap = {
|
|
2721
|
+
"ECDSA_SHA_256": "EC_SIGN_P256_SHA256",
|
|
2722
|
+
"ECDSA_SHA_384": "EC_SIGN_P384_SHA384",
|
|
2723
|
+
"ECDSA_SHA_512": "EC_SIGN_P512_SHA512",
|
|
2724
|
+
"RSASSA_PSS_SHA_256": "RSA_SIGN_PSS_2048_SHA256",
|
|
2725
|
+
"RSASSA_PSS_SHA_384": "RSA_SIGN_PSS_3072_SHA256",
|
|
2726
|
+
"RSASSA_PSS_SHA_512": "RSA_SIGN_PSS_4096_SHA256",
|
|
2727
|
+
"RSASSA_PKCS1_V1_5_SHA_256": "RSA_SIGN_PKCS1_2048_SHA256",
|
|
2728
|
+
"RSASSA_PKCS1_V1_5_SHA_384": "RSA_SIGN_PKCS1_3072_SHA256",
|
|
2729
|
+
"RSASSA_PKCS1_V1_5_SHA_512": "RSA_SIGN_PKCS1_4096_SHA256"
|
|
2730
|
+
};
|
|
2731
|
+
if (algorithm.startsWith("EC_SIGN_") || algorithm.startsWith("RSA_SIGN_")) {
|
|
2732
|
+
return algorithm;
|
|
2733
|
+
}
|
|
2734
|
+
return algorithmMap[algorithm.toUpperCase()] || "EC_SIGN_P256_SHA256";
|
|
2735
|
+
}
|
|
2736
|
+
};
|
|
2737
|
+
var FireblocksSigner = class {
|
|
2738
|
+
config;
|
|
2739
|
+
apiBaseUrl;
|
|
2740
|
+
constructor(config) {
|
|
2741
|
+
this.config = config;
|
|
2742
|
+
this.apiBaseUrl = config.apiBaseUrl ?? "https://api.fireblocks.io";
|
|
2743
|
+
}
|
|
2744
|
+
getName() {
|
|
2745
|
+
return "Fireblocks";
|
|
2746
|
+
}
|
|
2747
|
+
isAvailable() {
|
|
2748
|
+
return !!this.config.apiKey && !!this.config.apiSecret;
|
|
2749
|
+
}
|
|
2750
|
+
async sign(request) {
|
|
2751
|
+
if (!this.isAvailable()) {
|
|
2752
|
+
throw new Error("Fireblocks API key and secret required");
|
|
2753
|
+
}
|
|
2754
|
+
const keyIdMatch = request.keyId.match(/^fireblocks:\/\/([^/]+)\/(.+)$/);
|
|
2755
|
+
if (!keyIdMatch) {
|
|
2756
|
+
throw new Error(
|
|
2757
|
+
"Invalid Fireblocks keyId format. Expected: fireblocks://vaultAccountId/assetId"
|
|
2758
|
+
);
|
|
2759
|
+
}
|
|
2760
|
+
const [, vaultAccountId, assetId] = keyIdMatch;
|
|
2761
|
+
const messageHex = request.message instanceof Buffer ? request.message.toString("hex") : Buffer.from(request.message).toString("hex");
|
|
2762
|
+
const requestId = request.options?.requestId || request.requestId;
|
|
2763
|
+
const txRequest = {
|
|
2764
|
+
operation: "RAW",
|
|
2765
|
+
source: { type: "VAULT_ACCOUNT", id: vaultAccountId },
|
|
2766
|
+
assetId,
|
|
2767
|
+
note: `Gate signing request: ${requestId ?? "unknown"}`,
|
|
2768
|
+
extraParameters: {
|
|
2769
|
+
rawMessageData: {
|
|
2770
|
+
messages: [{ content: messageHex }]
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
};
|
|
2774
|
+
const token = this.createAuthToken("/v1/transactions", JSON.stringify(txRequest));
|
|
2775
|
+
const response = await fetch(`${this.apiBaseUrl}/v1/transactions`, {
|
|
2776
|
+
method: "POST",
|
|
2777
|
+
headers: {
|
|
2778
|
+
"Content-Type": "application/json",
|
|
2779
|
+
"X-API-Key": this.config.apiKey,
|
|
2780
|
+
Authorization: `Bearer ${token}`
|
|
2781
|
+
},
|
|
2782
|
+
body: JSON.stringify(txRequest)
|
|
2783
|
+
});
|
|
2784
|
+
if (!response.ok) {
|
|
2785
|
+
const error = await response.text();
|
|
2786
|
+
throw new Error(`Fireblocks API error: ${response.status} ${error}`);
|
|
2787
|
+
}
|
|
2788
|
+
const result = await response.json();
|
|
2789
|
+
const txId = result.id;
|
|
2790
|
+
if (!txId) {
|
|
2791
|
+
throw new Error("Fireblocks API did not return transaction id");
|
|
2792
|
+
}
|
|
2793
|
+
const signed = await this.pollTransaction(txId);
|
|
2794
|
+
const sigHex = signed?.signature ?? signed?.signedMessages?.[0]?.signature;
|
|
2795
|
+
if (!sigHex) {
|
|
2796
|
+
throw new Error(`Fireblocks transaction ${txId} did not return signature`);
|
|
2797
|
+
}
|
|
2798
|
+
return {
|
|
2799
|
+
signature: Buffer.from(sigHex, "hex"),
|
|
2800
|
+
keyId: request.keyId,
|
|
2801
|
+
algorithm: request.algorithm ?? "ECDSA_SHA_256"
|
|
2802
|
+
};
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* Create JWT for Fireblocks API (RS256, uri + bodyHash in payload).
|
|
2806
|
+
*/
|
|
2807
|
+
createAuthToken(uri, bodyJson) {
|
|
2808
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
2809
|
+
const nonce = crypto.randomBytes(16).toString("hex");
|
|
2810
|
+
const bodyHash = bodyJson ? crypto.createHash("sha256").update(bodyJson, "utf8").digest("hex") : "";
|
|
2811
|
+
const payload = {
|
|
2812
|
+
uri,
|
|
2813
|
+
nonce,
|
|
2814
|
+
iat: now,
|
|
2815
|
+
exp: now + 30,
|
|
2816
|
+
sub: this.config.apiKey,
|
|
2817
|
+
bodyHash
|
|
2818
|
+
};
|
|
2819
|
+
const header = { alg: "RS256", typ: "JWT" };
|
|
2820
|
+
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
2821
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
2822
|
+
const signingInput = `${encodedHeader}.${encodedPayload}`;
|
|
2823
|
+
const sign = crypto.createSign("RSA-SHA256");
|
|
2824
|
+
sign.update(signingInput);
|
|
2825
|
+
const signature = sign.sign(this.config.apiSecret);
|
|
2826
|
+
const encodedSig = base64UrlEncode(signature);
|
|
2827
|
+
return `${signingInput}.${encodedSig}`;
|
|
2828
|
+
}
|
|
2829
|
+
async pollTransaction(txId, maxAttempts = 30) {
|
|
2830
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
2831
|
+
const token = this.createAuthToken(`/v1/transactions/${txId}`);
|
|
2832
|
+
const response = await fetch(`${this.apiBaseUrl}/v1/transactions/${txId}`, {
|
|
2833
|
+
headers: {
|
|
2834
|
+
"X-API-Key": this.config.apiKey,
|
|
2835
|
+
Authorization: `Bearer ${token}`
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2838
|
+
if (!response.ok) {
|
|
2839
|
+
throw new Error(`Failed to fetch transaction status: ${await response.text()}`);
|
|
2840
|
+
}
|
|
2841
|
+
const tx = await response.json();
|
|
2842
|
+
if (tx.status === "COMPLETED") {
|
|
2843
|
+
return tx.signedMessages?.[0] ? { signature: tx.signedMessages[0].signature } : tx;
|
|
2844
|
+
}
|
|
2845
|
+
if (tx.status === "FAILED" || tx.status === "REJECTED") {
|
|
2846
|
+
throw new Error(`Fireblocks transaction ${txId} failed: ${tx.status}`);
|
|
2847
|
+
}
|
|
2848
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
2849
|
+
}
|
|
2850
|
+
throw new Error(
|
|
2851
|
+
`Fireblocks transaction ${txId} did not complete within ${maxAttempts} seconds`
|
|
2852
|
+
);
|
|
2853
|
+
}
|
|
2854
|
+
};
|
|
2855
|
+
function base64UrlEncode(input) {
|
|
2856
|
+
const raw = typeof input === "string" ? Buffer.from(input, "utf8").toString("base64") : input.toString("base64");
|
|
2857
|
+
return raw.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2858
|
+
}
|
|
2859
|
+
var require2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
2860
|
+
var NOT_LINKED = "PKCS#11 runtime not linked. Install pkcs11js (npm install pkcs11js) and ensure the HSM library path is correct, or provide a custom pkcs11Session to GenericHsmSigner.";
|
|
2861
|
+
function mechanismToPkcs11(mechanism) {
|
|
2862
|
+
switch (mechanism) {
|
|
2863
|
+
case "CKM_ECDSA_SHA256":
|
|
2864
|
+
return getPkcs11().CKM_ECDSA_SHA256;
|
|
2865
|
+
case "CKM_RSA_PKCS":
|
|
2866
|
+
return getPkcs11().CKM_SHA256_RSA_PKCS;
|
|
2867
|
+
default:
|
|
2868
|
+
throw new Error(`Unsupported PKCS#11 mechanism: ${mechanism}`);
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
var pkcs11Module = void 0;
|
|
2872
|
+
function getPkcs11() {
|
|
2873
|
+
if (pkcs11Module !== void 0) {
|
|
2874
|
+
if (pkcs11Module === null) throw new Error(NOT_LINKED);
|
|
2875
|
+
return pkcs11Module;
|
|
2876
|
+
}
|
|
2877
|
+
try {
|
|
2878
|
+
pkcs11Module = require2("pkcs11js");
|
|
2879
|
+
return pkcs11Module;
|
|
2880
|
+
} catch {
|
|
2881
|
+
pkcs11Module = null;
|
|
2882
|
+
throw new Error(NOT_LINKED);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
var Pkcs11SessionImpl = class {
|
|
2886
|
+
libPath = "";
|
|
2887
|
+
pin = "";
|
|
2888
|
+
pkcs11 = null;
|
|
2889
|
+
session = null;
|
|
2890
|
+
initialized = false;
|
|
2891
|
+
async initialize(libraryPath, pin, options) {
|
|
2892
|
+
const p = getPkcs11();
|
|
2893
|
+
this.libPath = libraryPath;
|
|
2894
|
+
this.pin = pin;
|
|
2895
|
+
this.pkcs11 = new p.PKCS11();
|
|
2896
|
+
this.pkcs11.load(libraryPath);
|
|
2897
|
+
this.pkcs11.C_Initialize();
|
|
2898
|
+
this.initialized = true;
|
|
2899
|
+
const slots = this.pkcs11.C_GetSlotList(true);
|
|
2900
|
+
if (!slots || slots.length === 0) {
|
|
2901
|
+
await this.close();
|
|
2902
|
+
throw new Error("PKCS#11: no token present in any slot");
|
|
2903
|
+
}
|
|
2904
|
+
const slotIndex = options?.slotId ?? 0;
|
|
2905
|
+
if (slotIndex < 0 || slotIndex >= slots.length) {
|
|
2906
|
+
await this.close();
|
|
2907
|
+
throw new Error(`PKCS#11: slotId ${slotIndex} out of range (0..${slots.length - 1})`);
|
|
2908
|
+
}
|
|
2909
|
+
const slot = slots[slotIndex];
|
|
2910
|
+
const flags = p.CKF_SERIAL_SESSION | p.CKF_RW_SESSION;
|
|
2911
|
+
this.session = this.pkcs11.C_OpenSession(slot, flags);
|
|
2912
|
+
this.pkcs11.C_Login(this.session, p.CKU_USER, pin);
|
|
2913
|
+
}
|
|
2914
|
+
async sign(keyHandle, mechanism, data) {
|
|
2915
|
+
if (!this.pkcs11 || !this.session) {
|
|
2916
|
+
throw new Error("PKCS#11 session not initialized. Call initialize() first.");
|
|
2917
|
+
}
|
|
2918
|
+
getPkcs11();
|
|
2919
|
+
const mechCode = mechanismToPkcs11(mechanism);
|
|
2920
|
+
this.pkcs11.C_SignInit(this.session, { mechanism: mechCode }, keyHandle);
|
|
2921
|
+
const maxSigLen = 512;
|
|
2922
|
+
const outData = Buffer.alloc(maxSigLen);
|
|
2923
|
+
const signature = this.pkcs11.C_Sign(this.session, data, outData);
|
|
2924
|
+
return Buffer.from(signature);
|
|
2925
|
+
}
|
|
2926
|
+
async close() {
|
|
2927
|
+
if (!this.initialized) return;
|
|
2928
|
+
this.initialized = false;
|
|
2929
|
+
try {
|
|
2930
|
+
if (this.pkcs11 && this.session) {
|
|
2931
|
+
try {
|
|
2932
|
+
this.pkcs11.C_Logout(this.session);
|
|
2933
|
+
} catch {
|
|
2934
|
+
}
|
|
2935
|
+
try {
|
|
2936
|
+
this.pkcs11.C_CloseSession(this.session);
|
|
2937
|
+
} catch {
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2940
|
+
if (this.pkcs11) {
|
|
2941
|
+
try {
|
|
2942
|
+
this.pkcs11.C_Finalize();
|
|
2943
|
+
} catch {
|
|
2944
|
+
}
|
|
2945
|
+
try {
|
|
2946
|
+
this.pkcs11.close();
|
|
2947
|
+
} catch {
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
} finally {
|
|
2951
|
+
this.pkcs11 = null;
|
|
2952
|
+
this.session = null;
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
};
|
|
2956
|
+
|
|
2957
|
+
// src/signer/GenericHsmSigner.ts
|
|
2958
|
+
var GenericHsmSigner = class {
|
|
2959
|
+
config;
|
|
2960
|
+
session = null;
|
|
2961
|
+
constructor(config) {
|
|
2962
|
+
this.config = config;
|
|
2963
|
+
}
|
|
2964
|
+
getName() {
|
|
2965
|
+
return "Generic HSM (PKCS#11)";
|
|
2966
|
+
}
|
|
2967
|
+
isAvailable() {
|
|
2968
|
+
return !!this.config.pkcs11LibraryPath && !!this.config.pin;
|
|
2969
|
+
}
|
|
2970
|
+
async sign(request) {
|
|
2971
|
+
if (!this.session) {
|
|
2972
|
+
this.session = this.config.pkcs11Session ?? await this.initializePkcs11Session();
|
|
2973
|
+
}
|
|
2974
|
+
const keyIdMatch = request.keyId.match(/^hsm:\/\/(.+)$/);
|
|
2975
|
+
if (!keyIdMatch) {
|
|
2976
|
+
throw new Error(
|
|
2977
|
+
"Invalid HSM keyId format. Expected: hsm://keyHandle (hex-encoded) or hsm://keyLabel"
|
|
2978
|
+
);
|
|
2979
|
+
}
|
|
2980
|
+
const keyHandle = Buffer.from(keyIdMatch[1], "hex");
|
|
2981
|
+
const mechanism = this.mapAlgorithmToMechanism(
|
|
2982
|
+
request.algorithm ?? "ECDSA_SHA_256"
|
|
2983
|
+
);
|
|
2984
|
+
const message = request.message instanceof Buffer ? request.message : Buffer.from(request.message);
|
|
2985
|
+
const signature = await this.session.sign(keyHandle, mechanism, message);
|
|
2986
|
+
return {
|
|
2987
|
+
signature,
|
|
2988
|
+
keyId: request.keyId,
|
|
2989
|
+
algorithm: request.algorithm ?? "ECDSA_SHA_256"
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
async initializePkcs11Session() {
|
|
2993
|
+
const session = new Pkcs11SessionImpl();
|
|
2994
|
+
await session.initialize(this.config.pkcs11LibraryPath, this.config.pin, {
|
|
2995
|
+
slotId: this.config.slotId
|
|
2996
|
+
});
|
|
2997
|
+
return session;
|
|
2998
|
+
}
|
|
2999
|
+
mapAlgorithmToMechanism(algorithm) {
|
|
3000
|
+
switch (algorithm) {
|
|
3001
|
+
case "ECDSA_SHA_256":
|
|
3002
|
+
return "CKM_ECDSA_SHA256";
|
|
3003
|
+
case "RSASSA_PKCS1_V1_5_SHA_256":
|
|
3004
|
+
return "CKM_RSA_PKCS";
|
|
3005
|
+
default:
|
|
3006
|
+
throw new Error(`Unsupported algorithm for HSM: ${algorithm}`);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
/** Release the PKCS#11 session. Call when done to free resources. */
|
|
3010
|
+
async close() {
|
|
3011
|
+
if (this.session) {
|
|
3012
|
+
await this.session.close();
|
|
3013
|
+
this.session = null;
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
};
|
|
2005
3017
|
|
|
3018
|
+
exports.AwsKmsSigner = AwsKmsSigner;
|
|
2006
3019
|
exports.BlockIntelAuthError = BlockIntelAuthError;
|
|
2007
3020
|
exports.BlockIntelBlockedError = BlockIntelBlockedError;
|
|
2008
3021
|
exports.BlockIntelStepUpRequiredError = BlockIntelStepUpRequiredError;
|
|
2009
3022
|
exports.BlockIntelUnavailableError = BlockIntelUnavailableError;
|
|
3023
|
+
exports.FireblocksSigner = FireblocksSigner;
|
|
2010
3024
|
exports.Gate = Gate;
|
|
2011
3025
|
exports.GateClient = GateClient;
|
|
2012
3026
|
exports.GateError = GateError;
|
|
2013
3027
|
exports.GateErrorCode = GateErrorCode;
|
|
3028
|
+
exports.GcpKmsSigner = GcpKmsSigner;
|
|
3029
|
+
exports.GenericHsmSigner = GenericHsmSigner;
|
|
2014
3030
|
exports.HeartbeatManager = HeartbeatManager;
|
|
2015
3031
|
exports.ProvenanceProvider = ProvenanceProvider;
|
|
2016
3032
|
exports.StepUpNotConfiguredError = StepUpNotConfiguredError;
|
|
3033
|
+
exports.VaultSigner = VaultSigner;
|
|
3034
|
+
exports.buildTxBindingObject = buildTxBindingObject;
|
|
3035
|
+
exports.computeTxDigest = computeTxDigest;
|
|
2017
3036
|
exports.createGateClient = createGateClient;
|
|
2018
3037
|
exports.default = GateClient;
|
|
2019
3038
|
exports.wrapKmsClient = wrapKmsClient;
|