blockintel-gate-sdk 0.3.6 → 0.3.7
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 +24 -0
- package/dist/index.cjs +144 -146
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +143 -145
- package/dist/index.js.map +1 -1
- package/package.json +3 -7
package/README.md
CHANGED
|
@@ -13,6 +13,12 @@ npm install @blockintel/gate-sdk
|
|
|
13
13
|
- Node.js >= 18.0.0 (uses global `fetch` API)
|
|
14
14
|
- TypeScript >= 5.0.0 (optional, for type definitions)
|
|
15
15
|
|
|
16
|
+
### Hot Path compatibility
|
|
17
|
+
|
|
18
|
+
- **Mode**: Default is `SHADOW` (Hot Path returns ALLOW with reason codes for would-block decisions). Set `mode: 'ENFORCE'` or `GATE_MODE=ENFORCE` for real BLOCK responses.
|
|
19
|
+
- **signingContext**: Hot Path requires `actorPrincipal` and `signerId`. The SDK defaults them when missing (`gate-sdk-client` or from `signingContext.signerId`).
|
|
20
|
+
- **ESM**: HMAC and SHA-256 use `node:crypto` (no `require('crypto')`), so the SDK works in ESM (`"type": "module"`) and in bundled canary apps.
|
|
21
|
+
|
|
16
22
|
## Quick Start
|
|
17
23
|
|
|
18
24
|
### HMAC Authentication
|
|
@@ -203,6 +209,9 @@ When step-up is disabled, the SDK treats `REQUIRE_STEP_UP` as `BLOCK` by default
|
|
|
203
209
|
GATE_BASE_URL=https://gate.blockintelai.com
|
|
204
210
|
GATE_TENANT_ID=your-tenant-id
|
|
205
211
|
|
|
212
|
+
# Heartbeat (required when not using local mode; parity with Python GATE_HEARTBEAT_KEY)
|
|
213
|
+
GATE_HEARTBEAT_KEY=your-heartbeat-key
|
|
214
|
+
|
|
206
215
|
# HMAC Authentication
|
|
207
216
|
GATE_KEY_ID=your-key-id
|
|
208
217
|
GATE_HMAC_SECRET=your-secret
|
|
@@ -376,6 +385,21 @@ The SDK automatically retries failed requests:
|
|
|
376
385
|
- Same `requestId` is used across all retries
|
|
377
386
|
- Ensures idempotency on Gate server
|
|
378
387
|
|
|
388
|
+
## Degraded Mode / X-BlockIntel-Degraded
|
|
389
|
+
|
|
390
|
+
When the SDK is in a degraded situation, it logs `X-BlockIntel-Degraded: true` with a `reason` for **logs and telemetry only**. This is **never sent as an HTTP request header** to the Gate server.
|
|
391
|
+
|
|
392
|
+
**Reasons:** `retry`, `429`, `fail_open`, `fail_safe_allow`.
|
|
393
|
+
|
|
394
|
+
**Example (one line):**
|
|
395
|
+
`[GATE SDK] X-BlockIntel-Degraded: true (reason=retry) attempt=1/3 status=503 err=RATE_LIMITED`
|
|
396
|
+
|
|
397
|
+
**How to observe:**
|
|
398
|
+
- **Logs:** `[GATE SDK] X-BlockIntel-Degraded: true (reason: <reason>)` via `console.warn`. Pipe stderr to your log aggregator.
|
|
399
|
+
- **Metrics:** Use `onMetrics`; metrics include `timeouts`, `errors`, `failOpen`, etc. Correlate with log lines if you ship both.
|
|
400
|
+
|
|
401
|
+
**Manual check (retry):** Point the SDK at an endpoint that returns 5xx; confirm one degraded log per retry attempt including `attempt`, `max`, and `status`/`err`.
|
|
402
|
+
|
|
379
403
|
## Heartbeat System
|
|
380
404
|
|
|
381
405
|
The SDK includes a **Heartbeat Manager** that automatically acquires and refreshes heartbeat tokens from the Gate Control Plane. Heartbeat tokens are required for all signing operations and ensure that Gate is alive and enforcing policy.
|
package/dist/index.cjs
CHANGED
|
@@ -2,18 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
+
var crypto = require('crypto');
|
|
5
6
|
var uuid = require('uuid');
|
|
6
7
|
var clientKms = require('@aws-sdk/client-kms');
|
|
7
|
-
var crypto$1 = require('crypto');
|
|
8
8
|
|
|
9
9
|
var __defProp = Object.defineProperty;
|
|
10
10
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
11
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
12
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
13
|
-
}) : x)(function(x) {
|
|
14
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
15
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
16
|
-
});
|
|
17
11
|
var __esm = (fn, res) => function __init() {
|
|
18
12
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
19
13
|
};
|
|
@@ -50,56 +44,24 @@ function canonicalizeJson(obj) {
|
|
|
50
44
|
return JSON.stringify(sorted);
|
|
51
45
|
}
|
|
52
46
|
async function sha256Hex(input) {
|
|
53
|
-
|
|
54
|
-
const encoder = new TextEncoder();
|
|
55
|
-
const data = encoder.encode(input);
|
|
56
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
57
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
58
|
-
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
59
|
-
}
|
|
60
|
-
if (typeof __require !== "undefined") {
|
|
61
|
-
const crypto2 = __require("crypto");
|
|
62
|
-
return crypto2.createHash("sha256").update(input, "utf8").digest("hex");
|
|
63
|
-
}
|
|
64
|
-
throw new Error("SHA-256 not available in this environment");
|
|
47
|
+
return crypto.createHash("sha256").update(input, "utf8").digest("hex");
|
|
65
48
|
}
|
|
66
49
|
var init_canonicalJson = __esm({
|
|
67
50
|
"src/utils/canonicalJson.ts"() {
|
|
68
51
|
}
|
|
69
52
|
});
|
|
70
|
-
|
|
71
|
-
// src/utils/crypto.ts
|
|
72
53
|
async function hmacSha256(secret, message) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}, null, 2));
|
|
85
|
-
return signatureHex;
|
|
86
|
-
}
|
|
87
|
-
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
88
|
-
const encoder = new TextEncoder();
|
|
89
|
-
const keyData = encoder.encode(secret);
|
|
90
|
-
const messageData = encoder.encode(message);
|
|
91
|
-
const key = await crypto.subtle.importKey(
|
|
92
|
-
"raw",
|
|
93
|
-
keyData,
|
|
94
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
95
|
-
false,
|
|
96
|
-
["sign"]
|
|
97
|
-
);
|
|
98
|
-
const signature = await crypto.subtle.sign("HMAC", key, messageData);
|
|
99
|
-
const hashArray = Array.from(new Uint8Array(signature));
|
|
100
|
-
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
101
|
-
}
|
|
102
|
-
throw new Error("HMAC-SHA256 not available in this environment");
|
|
54
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
55
|
+
hmac.update(message, "utf8");
|
|
56
|
+
const signatureHex = hmac.digest("hex");
|
|
57
|
+
console.error("[HMAC CRYPTO DEBUG] Signature computation:", JSON.stringify({
|
|
58
|
+
secretLength: secret.length,
|
|
59
|
+
messageLength: message.length,
|
|
60
|
+
messagePreview: message.substring(0, 200) + "...",
|
|
61
|
+
signatureLength: signatureHex.length,
|
|
62
|
+
signaturePreview: signatureHex.substring(0, 16) + "..."
|
|
63
|
+
}, null, 2));
|
|
64
|
+
return signatureHex;
|
|
103
65
|
}
|
|
104
66
|
|
|
105
67
|
// src/auth/HmacSigner.ts
|
|
@@ -132,26 +94,7 @@ var HmacSigner = class {
|
|
|
132
94
|
// Used as nonce in canonical string
|
|
133
95
|
bodyHash
|
|
134
96
|
].join("\n");
|
|
135
|
-
console.error("[HMAC SIGNER DEBUG] Canonical request string:", JSON.stringify({
|
|
136
|
-
method: method.toUpperCase(),
|
|
137
|
-
path,
|
|
138
|
-
tenantId,
|
|
139
|
-
keyId: this.keyId,
|
|
140
|
-
timestampMs: String(timestampMs),
|
|
141
|
-
requestId,
|
|
142
|
-
bodyHash,
|
|
143
|
-
signingStringLength: signingString.length,
|
|
144
|
-
signingStringPreview: signingString.substring(0, 200) + "...",
|
|
145
|
-
bodyJsonLength: bodyJson.length,
|
|
146
|
-
bodyJsonPreview: bodyJson.substring(0, 200) + "..."
|
|
147
|
-
}, null, 2));
|
|
148
97
|
const signature = await hmacSha256(this.secret, signingString);
|
|
149
|
-
console.error("[HMAC SIGNER DEBUG] Signature computed:", JSON.stringify({
|
|
150
|
-
signatureLength: signature.length,
|
|
151
|
-
signaturePreview: signature.substring(0, 16) + "...",
|
|
152
|
-
secretLength: this.secret.length,
|
|
153
|
-
secretPreview: this.secret.substring(0, 4) + "..." + this.secret.substring(this.secret.length - 4)
|
|
154
|
-
}, null, 2));
|
|
155
98
|
return {
|
|
156
99
|
"X-GATE-TENANT-ID": tenantId,
|
|
157
100
|
"X-GATE-KEY-ID": this.keyId,
|
|
@@ -350,6 +293,10 @@ async function retryWithBackoff(fn, options = {}) {
|
|
|
350
293
|
if (!isRetryable) {
|
|
351
294
|
throw error;
|
|
352
295
|
}
|
|
296
|
+
const status = error instanceof Response && error.status || error && typeof error === "object" && "status" in error && error.status || error && typeof error === "object" && "statusCode" in error && error.statusCode;
|
|
297
|
+
const errName = error instanceof Error ? error.name : error && typeof error === "object" && "code" in error ? error.code : "Unknown";
|
|
298
|
+
const extra = ` attempt=${attempt}/${opts.maxAttempts} status=${status ?? "n/a"} err=${errName}`;
|
|
299
|
+
console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason=retry)" + extra);
|
|
353
300
|
const delay = calculateBackoffDelay(attempt, opts);
|
|
354
301
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
355
302
|
}
|
|
@@ -357,17 +304,73 @@ async function retryWithBackoff(fn, options = {}) {
|
|
|
357
304
|
throw lastError;
|
|
358
305
|
}
|
|
359
306
|
|
|
307
|
+
// src/utils/sanitize.ts
|
|
308
|
+
var SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
309
|
+
"authorization",
|
|
310
|
+
"x-api-key",
|
|
311
|
+
"x-gate-heartbeat-key",
|
|
312
|
+
"x-gate-signature",
|
|
313
|
+
"cookie"
|
|
314
|
+
]);
|
|
315
|
+
var MAX_STRING_LENGTH = 80;
|
|
316
|
+
function sanitizeHeaders(headers) {
|
|
317
|
+
const out = {};
|
|
318
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
319
|
+
const lower = key.toLowerCase();
|
|
320
|
+
if (SENSITIVE_HEADER_NAMES.has(lower) || lower.includes("signature") || lower.includes("secret") || lower.includes("token")) {
|
|
321
|
+
out[key] = value ? "[REDACTED]" : "[empty]";
|
|
322
|
+
} else {
|
|
323
|
+
out[key] = truncate(String(value), MAX_STRING_LENGTH);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
function sanitizeBodyShape(body) {
|
|
329
|
+
if (body === null || body === void 0) {
|
|
330
|
+
return {};
|
|
331
|
+
}
|
|
332
|
+
if (typeof body !== "object") {
|
|
333
|
+
return { _: typeof body };
|
|
334
|
+
}
|
|
335
|
+
if (Array.isArray(body)) {
|
|
336
|
+
return { _: "array", length: String(body.length) };
|
|
337
|
+
}
|
|
338
|
+
const out = {};
|
|
339
|
+
for (const key of Object.keys(body).sort()) {
|
|
340
|
+
const val = body[key];
|
|
341
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
342
|
+
out[key] = "object";
|
|
343
|
+
} else if (Array.isArray(val)) {
|
|
344
|
+
out[key] = "array";
|
|
345
|
+
} else {
|
|
346
|
+
out[key] = typeof val;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return out;
|
|
350
|
+
}
|
|
351
|
+
function truncate(s, max) {
|
|
352
|
+
if (s.length <= max) return s;
|
|
353
|
+
return s.slice(0, max) + "...";
|
|
354
|
+
}
|
|
355
|
+
function isDebugEnabled(debugOption) {
|
|
356
|
+
if (debugOption === true) return true;
|
|
357
|
+
if (typeof process !== "undefined" && process.env.GATE_SDK_DEBUG === "1") return true;
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
|
|
360
361
|
// src/http/HttpClient.ts
|
|
361
362
|
var HttpClient = class {
|
|
362
363
|
baseUrl;
|
|
363
364
|
timeoutMs;
|
|
364
365
|
userAgent;
|
|
365
366
|
retryOptions;
|
|
367
|
+
debug;
|
|
366
368
|
constructor(config) {
|
|
367
369
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
368
370
|
this.timeoutMs = config.timeoutMs ?? 15e3;
|
|
369
371
|
this.userAgent = config.userAgent ?? "blockintel-gate-sdk/0.1.0";
|
|
370
372
|
this.retryOptions = config.retryOptions;
|
|
373
|
+
this.debug = isDebugEnabled(config.debug);
|
|
371
374
|
if (!this.baseUrl) {
|
|
372
375
|
throw new Error("baseUrl is required");
|
|
373
376
|
}
|
|
@@ -386,7 +389,6 @@ var HttpClient = class {
|
|
|
386
389
|
const controller = new AbortController();
|
|
387
390
|
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
388
391
|
let requestDetailsForLogging = null;
|
|
389
|
-
let requestDetailsSet = false;
|
|
390
392
|
try {
|
|
391
393
|
const response = await retryWithBackoff(
|
|
392
394
|
async () => {
|
|
@@ -409,31 +411,22 @@ var HttpClient = class {
|
|
|
409
411
|
fetchOptions.body = JSON.stringify(body);
|
|
410
412
|
}
|
|
411
413
|
}
|
|
412
|
-
const logHeaders = {};
|
|
413
|
-
if (fetchOptions.headers) {
|
|
414
|
-
Object.entries(fetchOptions.headers).forEach(([key, value]) => {
|
|
415
|
-
if (key.toLowerCase().includes("signature") || key.toLowerCase().includes("secret")) {
|
|
416
|
-
logHeaders[key] = String(value).substring(0, 8) + "...";
|
|
417
|
-
} else {
|
|
418
|
-
logHeaders[key] = String(value);
|
|
419
|
-
}
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
414
|
const bodyStr = typeof fetchOptions.body === "string" ? fetchOptions.body : null;
|
|
423
|
-
|
|
424
|
-
headers:
|
|
425
|
-
bodyLength: bodyStr ? bodyStr.length : 0
|
|
426
|
-
bodyPreview: bodyStr ? bodyStr.substring(0, 300) : null
|
|
415
|
+
requestDetailsForLogging = {
|
|
416
|
+
headers: this.debug ? sanitizeHeaders(requestHeaders) : {},
|
|
417
|
+
bodyLength: bodyStr ? bodyStr.length : 0
|
|
427
418
|
};
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
419
|
+
if (this.debug) {
|
|
420
|
+
const bodyShape = body && typeof body === "object" ? sanitizeBodyShape(body) : {};
|
|
421
|
+
console.error("[GATE SDK] Request:", JSON.stringify({
|
|
422
|
+
url,
|
|
423
|
+
method,
|
|
424
|
+
headerNames: Object.keys(requestHeaders),
|
|
425
|
+
headersRedacted: requestDetailsForLogging.headers,
|
|
426
|
+
bodyLength: requestDetailsForLogging.bodyLength,
|
|
427
|
+
bodyKeysAndTypes: bodyShape
|
|
428
|
+
}, null, 2));
|
|
429
|
+
}
|
|
437
430
|
const res = await fetch(url, fetchOptions);
|
|
438
431
|
if (!res.ok && isRetryableStatus(res.status)) {
|
|
439
432
|
throw res;
|
|
@@ -451,26 +444,24 @@ var HttpClient = class {
|
|
|
451
444
|
clearTimeout(timeoutId);
|
|
452
445
|
let data;
|
|
453
446
|
const contentType = response.headers.get("content-type");
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
447
|
+
if (this.debug) {
|
|
448
|
+
console.error("[GATE SDK] Response:", JSON.stringify({
|
|
449
|
+
status: response.status,
|
|
450
|
+
ok: response.ok,
|
|
451
|
+
url: response.url
|
|
452
|
+
}, null, 2));
|
|
453
|
+
}
|
|
461
454
|
if (contentType && contentType.includes("application/json")) {
|
|
462
455
|
try {
|
|
463
456
|
const jsonText = await response.text();
|
|
464
|
-
console.error("[HTTP CLIENT DEBUG] Response body (first 500 chars):", jsonText.substring(0, 500));
|
|
465
457
|
data = JSON.parse(jsonText);
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
hasData: typeof data?.data !== "undefined",
|
|
470
|
-
hasError: typeof data?.error !== "undefined"
|
|
471
|
-
}, null, 2));
|
|
458
|
+
if (this.debug && data && typeof data === "object") {
|
|
459
|
+
console.error("[GATE SDK] Response keys:", Object.keys(data));
|
|
460
|
+
}
|
|
472
461
|
} catch (parseError) {
|
|
473
|
-
|
|
462
|
+
if (this.debug) {
|
|
463
|
+
console.error("[GATE SDK] JSON parse error:", parseError instanceof Error ? parseError.message : String(parseError));
|
|
464
|
+
}
|
|
474
465
|
throw new GateError(
|
|
475
466
|
"INVALID_RESPONSE" /* INVALID_RESPONSE */,
|
|
476
467
|
"Failed to parse JSON response",
|
|
@@ -498,26 +489,12 @@ var HttpClient = class {
|
|
|
498
489
|
response.headers.forEach((value, key) => {
|
|
499
490
|
responseHeaders[key] = value;
|
|
500
491
|
});
|
|
501
|
-
if (
|
|
502
|
-
console.error("[
|
|
492
|
+
if (this.debug) {
|
|
493
|
+
console.error("[GATE SDK] Error response:", JSON.stringify({
|
|
503
494
|
status: response.status,
|
|
504
|
-
statusText: response.statusText,
|
|
505
495
|
url: response.url,
|
|
506
|
-
requestMethod: method,
|
|
507
496
|
requestPath: path,
|
|
508
|
-
|
|
509
|
-
responseHeaders,
|
|
510
|
-
responseData: data,
|
|
511
|
-
bodyLength: requestDetailsForLogging ? requestDetailsForLogging.bodyLength : 0,
|
|
512
|
-
bodyPreview: requestDetailsForLogging ? requestDetailsForLogging.bodyPreview : null
|
|
513
|
-
}, null, 2));
|
|
514
|
-
} else {
|
|
515
|
-
console.error("[HTTP CLIENT DEBUG] Response not OK:", JSON.stringify({
|
|
516
|
-
status: response.status,
|
|
517
|
-
statusText: response.statusText,
|
|
518
|
-
url: response.url,
|
|
519
|
-
headers: responseHeaders,
|
|
520
|
-
data
|
|
497
|
+
responseKeys: data && typeof data === "object" ? Object.keys(data) : []
|
|
521
498
|
}, null, 2));
|
|
522
499
|
}
|
|
523
500
|
const errorCode = this.statusToErrorCode(response.status);
|
|
@@ -529,7 +506,6 @@ var HttpClient = class {
|
|
|
529
506
|
details: data
|
|
530
507
|
});
|
|
531
508
|
}
|
|
532
|
-
console.error("[HTTP CLIENT DEBUG] Response OK, returning data");
|
|
533
509
|
return data;
|
|
534
510
|
} catch (error) {
|
|
535
511
|
clearTimeout(timeoutId);
|
|
@@ -982,7 +958,7 @@ function defaultExtractTxIntent(command) {
|
|
|
982
958
|
throw new Error("SignCommand missing required Message property");
|
|
983
959
|
}
|
|
984
960
|
const messageBuffer = message instanceof Buffer ? message : Buffer.from(message);
|
|
985
|
-
const messageHash = crypto
|
|
961
|
+
const messageHash = crypto.createHash("sha256").update(messageBuffer).digest("hex");
|
|
986
962
|
return {
|
|
987
963
|
networkFamily: "OTHER",
|
|
988
964
|
toAddress: void 0,
|
|
@@ -1088,6 +1064,8 @@ var HeartbeatManager = class {
|
|
|
1088
1064
|
// Unique per process
|
|
1089
1065
|
sdkVersion;
|
|
1090
1066
|
// SDK version for tracking
|
|
1067
|
+
apiKey;
|
|
1068
|
+
// x-gate-heartbeat-key for Control Plane auth
|
|
1091
1069
|
currentToken = null;
|
|
1092
1070
|
refreshTimer = null;
|
|
1093
1071
|
started = false;
|
|
@@ -1100,19 +1078,22 @@ var HeartbeatManager = class {
|
|
|
1100
1078
|
this.signerId = options.signerId;
|
|
1101
1079
|
this.environment = options.environment ?? "prod";
|
|
1102
1080
|
this.baseRefreshIntervalSeconds = options.refreshIntervalSeconds ?? 10;
|
|
1081
|
+
this.apiKey = options.apiKey;
|
|
1103
1082
|
this.clientInstanceId = options.clientInstanceId || uuid.v4();
|
|
1104
1083
|
this.sdkVersion = options.sdkVersion || "1.0.0";
|
|
1084
|
+
this.apiKey = options.apiKey;
|
|
1105
1085
|
}
|
|
1106
1086
|
/**
|
|
1107
|
-
* Start background heartbeat refresher
|
|
1087
|
+
* Start background heartbeat refresher.
|
|
1088
|
+
* Optionally wait for initial token (first evaluate() will otherwise wait up to 2s for token).
|
|
1108
1089
|
*/
|
|
1109
|
-
start() {
|
|
1090
|
+
start(options) {
|
|
1110
1091
|
if (this.started) {
|
|
1111
1092
|
return;
|
|
1112
1093
|
}
|
|
1113
1094
|
this.started = true;
|
|
1114
1095
|
this.acquireHeartbeat().catch((error) => {
|
|
1115
|
-
console.error("[HEARTBEAT] Failed to acquire initial heartbeat:", error);
|
|
1096
|
+
console.error("[HEARTBEAT] Failed to acquire initial heartbeat:", error instanceof Error ? error.message : error);
|
|
1116
1097
|
});
|
|
1117
1098
|
this.scheduleNextRefresh();
|
|
1118
1099
|
}
|
|
@@ -1195,12 +1176,23 @@ var HeartbeatManager = class {
|
|
|
1195
1176
|
/**
|
|
1196
1177
|
* Acquire a new heartbeat token from Control Plane
|
|
1197
1178
|
* NEVER logs token value (security)
|
|
1179
|
+
* Requires x-gate-heartbeat-key header (apiKey) for authentication.
|
|
1198
1180
|
*/
|
|
1199
1181
|
async acquireHeartbeat() {
|
|
1182
|
+
if (!this.apiKey || this.apiKey.length === 0) {
|
|
1183
|
+
throw new GateError(
|
|
1184
|
+
"UNAUTHORIZED" /* UNAUTHORIZED */,
|
|
1185
|
+
"Heartbeat API key is required. Set GATE_HEARTBEAT_KEY in environment or pass heartbeatApiKey in GateClientConfig.",
|
|
1186
|
+
{}
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1200
1189
|
try {
|
|
1201
1190
|
const response = await this.httpClient.request({
|
|
1202
1191
|
method: "POST",
|
|
1203
1192
|
path: "/api/v1/gate/heartbeat",
|
|
1193
|
+
headers: {
|
|
1194
|
+
"x-gate-heartbeat-key": this.apiKey
|
|
1195
|
+
},
|
|
1204
1196
|
body: {
|
|
1205
1197
|
tenantId: this.tenantId,
|
|
1206
1198
|
signerId: this.signerId,
|
|
@@ -1528,7 +1520,8 @@ var GateClient = class {
|
|
|
1528
1520
|
this.httpClient = new HttpClient({
|
|
1529
1521
|
baseUrl: config.baseUrl,
|
|
1530
1522
|
timeoutMs: config.timeoutMs,
|
|
1531
|
-
userAgent: config.userAgent
|
|
1523
|
+
userAgent: config.userAgent,
|
|
1524
|
+
debug: config.debug
|
|
1532
1525
|
});
|
|
1533
1526
|
if (config.enableStepUp) {
|
|
1534
1527
|
this.stepUpPoller = new StepUpPoller({
|
|
@@ -1549,6 +1542,12 @@ var GateClient = class {
|
|
|
1549
1542
|
console.warn("[GATE CLIENT] LOCAL MODE ENABLED - Auth, heartbeat, and break-glass are disabled");
|
|
1550
1543
|
this.heartbeatManager = null;
|
|
1551
1544
|
} else {
|
|
1545
|
+
const heartbeatApiKey = config.heartbeatApiKey ?? (typeof process !== "undefined" ? process.env.GATE_HEARTBEAT_KEY : void 0);
|
|
1546
|
+
if (!heartbeatApiKey || heartbeatApiKey.length === 0) {
|
|
1547
|
+
throw new Error(
|
|
1548
|
+
"GATE_HEARTBEAT_KEY environment variable or heartbeatApiKey in config is required for heartbeat authentication. Set GATE_HEARTBEAT_KEY in your environment or pass heartbeatApiKey in GateClientConfig."
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1552
1551
|
let controlPlaneUrl = config.baseUrl;
|
|
1553
1552
|
if (controlPlaneUrl.includes("/defense")) {
|
|
1554
1553
|
controlPlaneUrl = controlPlaneUrl.split("/defense")[0];
|
|
@@ -1568,7 +1567,8 @@ var GateClient = class {
|
|
|
1568
1567
|
tenantId: config.tenantId,
|
|
1569
1568
|
signerId: initialSignerId,
|
|
1570
1569
|
environment: config.environment ?? "prod",
|
|
1571
|
-
refreshIntervalSeconds: config.heartbeatRefreshIntervalSeconds ?? 10
|
|
1570
|
+
refreshIntervalSeconds: config.heartbeatRefreshIntervalSeconds ?? 10,
|
|
1571
|
+
apiKey: heartbeatApiKey
|
|
1572
1572
|
});
|
|
1573
1573
|
this.heartbeatManager.start();
|
|
1574
1574
|
}
|
|
@@ -1657,7 +1657,9 @@ var GateClient = class {
|
|
|
1657
1657
|
delete txIntent.from;
|
|
1658
1658
|
}
|
|
1659
1659
|
const signingContext = {
|
|
1660
|
-
...req.signingContext
|
|
1660
|
+
...req.signingContext,
|
|
1661
|
+
actorPrincipal: req.signingContext?.actorPrincipal ?? req.signingContext?.signerId ?? "gate-sdk-client",
|
|
1662
|
+
signerId: req.signingContext?.signerId ?? req.signingContext?.actorPrincipal ?? "gate-sdk-client"
|
|
1661
1663
|
};
|
|
1662
1664
|
if (heartbeatToken) {
|
|
1663
1665
|
signingContext.heartbeatToken = heartbeatToken;
|
|
@@ -1673,17 +1675,16 @@ var GateClient = class {
|
|
|
1673
1675
|
};
|
|
1674
1676
|
}
|
|
1675
1677
|
let body = {
|
|
1678
|
+
tenantId: this.config.tenantId,
|
|
1676
1679
|
requestId,
|
|
1677
1680
|
timestampMs,
|
|
1678
1681
|
txIntent,
|
|
1679
1682
|
signingContext,
|
|
1680
1683
|
// Add SDK info (required by Hot Path validation)
|
|
1681
|
-
// Note: Must match Python SDK name for consistent canonical JSON
|
|
1682
1684
|
sdk: {
|
|
1683
1685
|
name: "gate-sdk",
|
|
1684
1686
|
version: "0.1.0"
|
|
1685
1687
|
},
|
|
1686
|
-
// Add mode and connection failure strategy
|
|
1687
1688
|
mode: requestMode,
|
|
1688
1689
|
onConnectionFailure: this.onConnectionFailure
|
|
1689
1690
|
};
|
|
@@ -1713,15 +1714,6 @@ var GateClient = class {
|
|
|
1713
1714
|
});
|
|
1714
1715
|
headers = { ...hmacHeaders };
|
|
1715
1716
|
body.__canonicalJson = canonicalBodyJson;
|
|
1716
|
-
const debugHeaders = {};
|
|
1717
|
-
Object.entries(headers).forEach(([key, value]) => {
|
|
1718
|
-
if (key.toLowerCase().includes("signature")) {
|
|
1719
|
-
debugHeaders[key] = value.substring(0, 8) + "...";
|
|
1720
|
-
} else {
|
|
1721
|
-
debugHeaders[key] = value;
|
|
1722
|
-
}
|
|
1723
|
-
});
|
|
1724
|
-
console.error("[GATE CLIENT DEBUG] HMAC headers prepared:", JSON.stringify(debugHeaders, null, 2));
|
|
1725
1717
|
} else if (this.apiKeyAuth) {
|
|
1726
1718
|
const apiKeyHeaders = this.apiKeyAuth.createHeaders({
|
|
1727
1719
|
tenantId: this.config.tenantId,
|
|
@@ -1729,7 +1721,6 @@ var GateClient = class {
|
|
|
1729
1721
|
requestId
|
|
1730
1722
|
});
|
|
1731
1723
|
headers = { ...apiKeyHeaders };
|
|
1732
|
-
console.error("[GATE CLIENT DEBUG] API key headers prepared:", JSON.stringify(headers, null, 2));
|
|
1733
1724
|
} else {
|
|
1734
1725
|
throw new Error("No authentication configured");
|
|
1735
1726
|
}
|
|
@@ -1863,6 +1854,7 @@ var GateClient = class {
|
|
|
1863
1854
|
tenantId: this.config.tenantId,
|
|
1864
1855
|
mode: requestMode
|
|
1865
1856
|
});
|
|
1857
|
+
console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_open)");
|
|
1866
1858
|
this.metrics.recordRequest("FAIL_OPEN", Date.now() - startTime);
|
|
1867
1859
|
return {
|
|
1868
1860
|
decision: "ALLOW",
|
|
@@ -1894,6 +1886,10 @@ var GateClient = class {
|
|
|
1894
1886
|
}
|
|
1895
1887
|
throw error;
|
|
1896
1888
|
}
|
|
1889
|
+
if (error instanceof GateError && error.code === "RATE_LIMITED" /* RATE_LIMITED */) {
|
|
1890
|
+
console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: 429)");
|
|
1891
|
+
throw error;
|
|
1892
|
+
}
|
|
1897
1893
|
if (error instanceof BlockIntelBlockedError || error instanceof BlockIntelStepUpRequiredError) {
|
|
1898
1894
|
throw error;
|
|
1899
1895
|
}
|
|
@@ -1906,6 +1902,7 @@ var GateClient = class {
|
|
|
1906
1902
|
*/
|
|
1907
1903
|
handleFailSafe(mode, error, requestId) {
|
|
1908
1904
|
if (mode === "ALLOW_ON_TIMEOUT") {
|
|
1905
|
+
console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_safe_allow)");
|
|
1909
1906
|
return {
|
|
1910
1907
|
decision: "ALLOW",
|
|
1911
1908
|
reasonCodes: ["FAIL_SAFE_ALLOW"],
|
|
@@ -1916,6 +1913,7 @@ var GateClient = class {
|
|
|
1916
1913
|
return null;
|
|
1917
1914
|
}
|
|
1918
1915
|
if (mode === "BLOCK_ON_ANOMALY") {
|
|
1916
|
+
console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_safe_allow)");
|
|
1919
1917
|
return {
|
|
1920
1918
|
decision: "ALLOW",
|
|
1921
1919
|
reasonCodes: ["FAIL_SAFE_ALLOW"],
|