blockintel-gate-sdk 0.4.4 → 0.4.6

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
@@ -1217,6 +1217,12 @@ async function handleSignCommand(command, originalClient, gateClient, options) {
1217
1217
  if (options.mode === "dry-run") {
1218
1218
  return await originalClient.send(new clientKms.SignCommand(command));
1219
1219
  }
1220
+ const GATEWAY_STAGES = ["HARD_KMS_GATEWAY", "HARD_GCP_GATEWAY", "HARD_HSM_GATEWAY"];
1221
+ const currentStage = gateClient.heartbeatManager?.getAdoptionStage?.();
1222
+ if (currentStage && GATEWAY_STAGES.includes(currentStage)) {
1223
+ emitMetric(options.metricsSink, "sign_success_total", labels);
1224
+ return await signViaProxy(gateClient, decision, command, signerId);
1225
+ }
1220
1226
  return await originalClient.send(new clientKms.SignCommand(command));
1221
1227
  } catch (error) {
1222
1228
  if (error instanceof BlockIntelBlockedError) {
@@ -1230,6 +1236,72 @@ async function handleSignCommand(command, originalClient, gateClient, options) {
1230
1236
  throw error;
1231
1237
  }
1232
1238
  }
1239
+ async function signViaProxy(gateClient, decision, command, signerId) {
1240
+ const config = gateClient.config;
1241
+ const baseUrl = config?.baseUrl || config?.controlPlaneUrl;
1242
+ const tenantId = config?.tenantId;
1243
+ if (!baseUrl || !tenantId) {
1244
+ throw new Error("[Gate SDK] Cannot use signing proxy: baseUrl or tenantId not configured on GateClient");
1245
+ }
1246
+ const message = command.input?.Message ?? command.Message;
1247
+ if (!message) {
1248
+ throw new Error("[Gate SDK] SignCommand missing Message for proxy signing");
1249
+ }
1250
+ const messageBuffer = message instanceof Buffer ? message : Buffer.from(message);
1251
+ const messageBase64 = messageBuffer.toString("base64");
1252
+ const keyId = command.input?.KeyId ?? command.KeyId;
1253
+ if (!keyId) {
1254
+ throw new Error("[Gate SDK] SignCommand missing KeyId for proxy signing");
1255
+ }
1256
+ const signingAlgorithm = command.input?.SigningAlgorithm ?? command.SigningAlgorithm ?? "ECDSA_SHA_256";
1257
+ const messageType = command.input?.MessageType ?? command.MessageType ?? "RAW";
1258
+ const proxyUrl = `${baseUrl.replace("/defense", "")}/tenants/${tenantId}/defense/sign`;
1259
+ const headers = {
1260
+ "Content-Type": "application/json"
1261
+ };
1262
+ const authHeaders = gateClient.getAuthHeaders?.();
1263
+ if (authHeaders) {
1264
+ Object.assign(headers, authHeaders);
1265
+ } else {
1266
+ const auth = config?.auth;
1267
+ if (auth?.mode === "api_key" && auth?.apiKey) {
1268
+ headers["x-api-key"] = auth.apiKey;
1269
+ }
1270
+ }
1271
+ const jwt = gateClient.jwt || gateClient.config?.jwt;
1272
+ if (jwt) {
1273
+ headers["Authorization"] = `Bearer ${jwt}`;
1274
+ }
1275
+ const response = await fetch(proxyUrl, {
1276
+ method: "POST",
1277
+ headers,
1278
+ body: JSON.stringify({
1279
+ requestId: decision.decisionId || decision.requestId,
1280
+ decisionToken: decision.decisionToken,
1281
+ keyId,
1282
+ message: messageBase64,
1283
+ signingAlgorithm,
1284
+ messageType
1285
+ })
1286
+ });
1287
+ if (!response.ok) {
1288
+ const errorBody = await response.json().catch(() => ({}));
1289
+ const code = errorBody?.error?.code || "SIGN_PROXY_FAILED";
1290
+ const msg = errorBody?.error?.message || `Signing proxy returned ${response.status}`;
1291
+ throw new Error(`[Gate SDK] ${code}: ${msg}`);
1292
+ }
1293
+ const result = await response.json();
1294
+ const data = result?.data;
1295
+ if (!data?.signature) {
1296
+ throw new Error("[Gate SDK] Signing proxy returned no signature");
1297
+ }
1298
+ return {
1299
+ Signature: Buffer.from(data.signature, "base64"),
1300
+ KeyId: data.keyId || keyId,
1301
+ SigningAlgorithm: data.signingAlgorithm || signingAlgorithm,
1302
+ $metadata: { httpStatusCode: 200 }
1303
+ };
1304
+ }
1233
1305
 
1234
1306
  // src/provenance/ProvenanceProvider.ts
1235
1307
  var ProvenanceProvider = class {
@@ -1287,6 +1359,8 @@ var HeartbeatManager = class {
1287
1359
  started = false;
1288
1360
  maxBackoffSeconds = 30;
1289
1361
  // Maximum backoff interval
1362
+ /** Server's current adoption stage for this tenant (cached from heartbeat response) */
1363
+ adoptionStage = null;
1290
1364
  maxSigners;
1291
1365
  signerIdleTtlMs;
1292
1366
  localRateLimitMs;
@@ -1560,6 +1634,9 @@ var HeartbeatManager = class {
1560
1634
  policyHash: response.data.policyHash
1561
1635
  };
1562
1636
  entry.consecutiveFailures = 0;
1637
+ if (response.data.adoptionStage != null) {
1638
+ this.adoptionStage = response.data.adoptionStage;
1639
+ }
1563
1640
  console.log("[HEARTBEAT] Acquired heartbeat token", {
1564
1641
  expiresAt,
1565
1642
  signerId,
@@ -1588,6 +1665,14 @@ var HeartbeatManager = class {
1588
1665
  getClientInstanceId() {
1589
1666
  return this.clientInstanceId;
1590
1667
  }
1668
+ /**
1669
+ * Get the server's current adoption stage for this tenant.
1670
+ * Populated after the first successful heartbeat response.
1671
+ * Returns null if not yet received.
1672
+ */
1673
+ getAdoptionStage() {
1674
+ return this.adoptionStage;
1675
+ }
1591
1676
  };
1592
1677
 
1593
1678
  // src/security/IamPermissionRiskChecker.ts
@@ -1920,6 +2005,8 @@ var GateClient = class {
1920
2005
  apiKey: heartbeatApiKey
1921
2006
  });
1922
2007
  this.heartbeatManager.start();
2008
+ this.checkAdoptionStageMismatch().catch(() => {
2009
+ });
1923
2010
  }
1924
2011
  if (!config.local) {
1925
2012
  const enforcementMode = config.enforcementMode || "SOFT";
@@ -1965,9 +2052,38 @@ var GateClient = class {
1965
2052
  console.warn("[GATE CLIENT] Async IAM risk check warning:", error instanceof Error ? error.message : String(error));
1966
2053
  }
1967
2054
  }
2055
+ /**
2056
+ * Warn if the local SDK mode is SHADOW but the server's adoption stage is enforcing.
2057
+ * Runs non-blocking after heartbeat startup; never throws.
2058
+ */
2059
+ async checkAdoptionStageMismatch() {
2060
+ if (!this.heartbeatManager) return;
2061
+ const signerId = this.config.signerId ?? DEFAULT_SIGNER_ID;
2062
+ try {
2063
+ await this.heartbeatManager.getTokenForSigner(signerId, 5e3);
2064
+ } catch {
2065
+ return;
2066
+ }
2067
+ const adoptionStage = this.heartbeatManager.getAdoptionStage();
2068
+ if (!adoptionStage) return;
2069
+ const ENFORCING_STAGES = [
2070
+ "SOFT_ENFORCE",
2071
+ "HARD_ENFORCE",
2072
+ "PROVENANCE",
2073
+ "HARD_KMS_GATEWAY",
2074
+ "HARD_KMS_ATTESTED",
2075
+ "HARD_KMS_ATTESTED_ENCLAVE",
2076
+ "HARD_GCP_CONFIDENTIAL_VM"
2077
+ ];
2078
+ if (this.mode === "SHADOW" && ENFORCING_STAGES.includes(adoptionStage)) {
2079
+ console.warn(
2080
+ `[GATE SDK] Server adoption stage is ${adoptionStage} but SDK mode is SHADOW. Consider updating mode to 'ENFORCE' so your application handles blocks correctly. Until updated, the SDK will allow transactions the server would block.`
2081
+ );
2082
+ }
2083
+ }
1968
2084
  /**
1969
2085
  * Evaluate a transaction defense request
1970
- *
2086
+ *
1971
2087
  * Implements:
1972
2088
  * - Shadow Mode (SHADOW: monitor-only, ENFORCE: enforce decisions)
1973
2089
  * - Connection failure strategy (FAIL_OPEN vs FAIL_CLOSED)
@@ -2228,7 +2344,8 @@ var GateClient = class {
2228
2344
  }
2229
2345
  }
2230
2346
  if (result.decision === "BLOCK") {
2231
- if (requestMode === "SOFT_ENFORCE") {
2347
+ const effectiveMode = result.mode ?? requestMode;
2348
+ if (effectiveMode === "SOFT_ENFORCE") {
2232
2349
  console.warn("[SOFT ENFORCE] Policy violation detected - app can override", {
2233
2350
  requestId,
2234
2351
  reasonCodes: result.reasonCodes
@@ -2242,7 +2359,7 @@ var GateClient = class {
2242
2359
  warning: "Policy violation detected. Override at your own risk."
2243
2360
  };
2244
2361
  }
2245
- if (requestMode === "SHADOW") {
2362
+ if (effectiveMode === "SHADOW") {
2246
2363
  console.warn("[GATE SHADOW MODE] Would have blocked transaction", {
2247
2364
  requestId,
2248
2365
  reasonCodes: result.reasonCodes,