blockintel-gate-sdk 0.3.5 → 0.3.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/README.md +0 -24
- package/dist/index.cjs +162 -144
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -17
- package/dist/index.d.ts +29 -17
- package/dist/index.js +161 -144
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -13,12 +13,6 @@ 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
|
-
|
|
22
16
|
## Quick Start
|
|
23
17
|
|
|
24
18
|
### HMAC Authentication
|
|
@@ -209,9 +203,6 @@ When step-up is disabled, the SDK treats `REQUIRE_STEP_UP` as `BLOCK` by default
|
|
|
209
203
|
GATE_BASE_URL=https://gate.blockintelai.com
|
|
210
204
|
GATE_TENANT_ID=your-tenant-id
|
|
211
205
|
|
|
212
|
-
# Heartbeat (required when not using local mode; parity with Python GATE_HEARTBEAT_KEY)
|
|
213
|
-
GATE_HEARTBEAT_KEY=your-heartbeat-key
|
|
214
|
-
|
|
215
206
|
# HMAC Authentication
|
|
216
207
|
GATE_KEY_ID=your-key-id
|
|
217
208
|
GATE_HMAC_SECRET=your-secret
|
|
@@ -385,21 +376,6 @@ The SDK automatically retries failed requests:
|
|
|
385
376
|
- Same `requestId` is used across all retries
|
|
386
377
|
- Ensures idempotency on Gate server
|
|
387
378
|
|
|
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
|
-
|
|
403
379
|
## Heartbeat System
|
|
404
380
|
|
|
405
381
|
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,12 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
var crypto = require('crypto');
|
|
6
5
|
var uuid = require('uuid');
|
|
7
6
|
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
|
+
});
|
|
11
17
|
var __esm = (fn, res) => function __init() {
|
|
12
18
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
13
19
|
};
|
|
@@ -44,24 +50,56 @@ function canonicalizeJson(obj) {
|
|
|
44
50
|
return JSON.stringify(sorted);
|
|
45
51
|
}
|
|
46
52
|
async function sha256Hex(input) {
|
|
47
|
-
|
|
53
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
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");
|
|
48
65
|
}
|
|
49
66
|
var init_canonicalJson = __esm({
|
|
50
67
|
"src/utils/canonicalJson.ts"() {
|
|
51
68
|
}
|
|
52
69
|
});
|
|
70
|
+
|
|
71
|
+
// src/utils/crypto.ts
|
|
53
72
|
async function hmacSha256(secret, message) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
if (typeof __require !== "undefined") {
|
|
74
|
+
const crypto2 = __require("crypto");
|
|
75
|
+
const hmac = crypto2.createHmac("sha256", secret);
|
|
76
|
+
hmac.update(message, "utf8");
|
|
77
|
+
const signatureHex = hmac.digest("hex");
|
|
78
|
+
console.error("[HMAC CRYPTO DEBUG] Signature computation:", JSON.stringify({
|
|
79
|
+
secretLength: secret.length,
|
|
80
|
+
messageLength: message.length,
|
|
81
|
+
messagePreview: message.substring(0, 200) + "...",
|
|
82
|
+
signatureLength: signatureHex.length,
|
|
83
|
+
signaturePreview: signatureHex.substring(0, 16) + "..."
|
|
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");
|
|
65
103
|
}
|
|
66
104
|
|
|
67
105
|
// src/auth/HmacSigner.ts
|
|
@@ -94,7 +132,26 @@ var HmacSigner = class {
|
|
|
94
132
|
// Used as nonce in canonical string
|
|
95
133
|
bodyHash
|
|
96
134
|
].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));
|
|
97
148
|
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));
|
|
98
155
|
return {
|
|
99
156
|
"X-GATE-TENANT-ID": tenantId,
|
|
100
157
|
"X-GATE-KEY-ID": this.keyId,
|
|
@@ -293,10 +350,6 @@ async function retryWithBackoff(fn, options = {}) {
|
|
|
293
350
|
if (!isRetryable) {
|
|
294
351
|
throw error;
|
|
295
352
|
}
|
|
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);
|
|
300
353
|
const delay = calculateBackoffDelay(attempt, opts);
|
|
301
354
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
302
355
|
}
|
|
@@ -304,73 +357,17 @@ async function retryWithBackoff(fn, options = {}) {
|
|
|
304
357
|
throw lastError;
|
|
305
358
|
}
|
|
306
359
|
|
|
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
|
-
|
|
361
360
|
// src/http/HttpClient.ts
|
|
362
361
|
var HttpClient = class {
|
|
363
362
|
baseUrl;
|
|
364
363
|
timeoutMs;
|
|
365
364
|
userAgent;
|
|
366
365
|
retryOptions;
|
|
367
|
-
debug;
|
|
368
366
|
constructor(config) {
|
|
369
367
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
370
368
|
this.timeoutMs = config.timeoutMs ?? 15e3;
|
|
371
369
|
this.userAgent = config.userAgent ?? "blockintel-gate-sdk/0.1.0";
|
|
372
370
|
this.retryOptions = config.retryOptions;
|
|
373
|
-
this.debug = isDebugEnabled(config.debug);
|
|
374
371
|
if (!this.baseUrl) {
|
|
375
372
|
throw new Error("baseUrl is required");
|
|
376
373
|
}
|
|
@@ -389,6 +386,7 @@ var HttpClient = class {
|
|
|
389
386
|
const controller = new AbortController();
|
|
390
387
|
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
391
388
|
let requestDetailsForLogging = null;
|
|
389
|
+
let requestDetailsSet = false;
|
|
392
390
|
try {
|
|
393
391
|
const response = await retryWithBackoff(
|
|
394
392
|
async () => {
|
|
@@ -411,22 +409,31 @@ var HttpClient = class {
|
|
|
411
409
|
fetchOptions.body = JSON.stringify(body);
|
|
412
410
|
}
|
|
413
411
|
}
|
|
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
|
+
}
|
|
414
422
|
const bodyStr = typeof fetchOptions.body === "string" ? fetchOptions.body : null;
|
|
415
|
-
|
|
416
|
-
headers:
|
|
417
|
-
bodyLength: bodyStr ? bodyStr.length : 0
|
|
423
|
+
const details = {
|
|
424
|
+
headers: logHeaders,
|
|
425
|
+
bodyLength: bodyStr ? bodyStr.length : 0,
|
|
426
|
+
bodyPreview: bodyStr ? bodyStr.substring(0, 300) : null
|
|
418
427
|
};
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}, null, 2));
|
|
429
|
-
}
|
|
428
|
+
requestDetailsForLogging = details;
|
|
429
|
+
requestDetailsSet = true;
|
|
430
|
+
console.error("[HTTP CLIENT DEBUG] Sending request:", JSON.stringify({
|
|
431
|
+
url,
|
|
432
|
+
method,
|
|
433
|
+
headers: logHeaders,
|
|
434
|
+
bodyLength: requestDetailsForLogging.bodyLength,
|
|
435
|
+
bodyPreview: requestDetailsForLogging.bodyPreview
|
|
436
|
+
}, null, 2));
|
|
430
437
|
const res = await fetch(url, fetchOptions);
|
|
431
438
|
if (!res.ok && isRetryableStatus(res.status)) {
|
|
432
439
|
throw res;
|
|
@@ -444,24 +451,26 @@ var HttpClient = class {
|
|
|
444
451
|
clearTimeout(timeoutId);
|
|
445
452
|
let data;
|
|
446
453
|
const contentType = response.headers.get("content-type");
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
}
|
|
454
|
+
console.error("[HTTP CLIENT DEBUG] Response received:", JSON.stringify({
|
|
455
|
+
status: response.status,
|
|
456
|
+
ok: response.ok,
|
|
457
|
+
statusText: response.statusText,
|
|
458
|
+
contentType,
|
|
459
|
+
url: response.url
|
|
460
|
+
}, null, 2));
|
|
454
461
|
if (contentType && contentType.includes("application/json")) {
|
|
455
462
|
try {
|
|
456
463
|
const jsonText = await response.text();
|
|
464
|
+
console.error("[HTTP CLIENT DEBUG] Response body (first 500 chars):", jsonText.substring(0, 500));
|
|
457
465
|
data = JSON.parse(jsonText);
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
466
|
+
console.error("[HTTP CLIENT DEBUG] Parsed JSON:", JSON.stringify({
|
|
467
|
+
hasSuccess: typeof data?.success !== "undefined",
|
|
468
|
+
success: data?.success,
|
|
469
|
+
hasData: typeof data?.data !== "undefined",
|
|
470
|
+
hasError: typeof data?.error !== "undefined"
|
|
471
|
+
}, null, 2));
|
|
461
472
|
} catch (parseError) {
|
|
462
|
-
|
|
463
|
-
console.error("[GATE SDK] JSON parse error:", parseError instanceof Error ? parseError.message : String(parseError));
|
|
464
|
-
}
|
|
473
|
+
console.error("[HTTP CLIENT DEBUG] JSON parse error:", parseError);
|
|
465
474
|
throw new GateError(
|
|
466
475
|
"INVALID_RESPONSE" /* INVALID_RESPONSE */,
|
|
467
476
|
"Failed to parse JSON response",
|
|
@@ -489,12 +498,26 @@ var HttpClient = class {
|
|
|
489
498
|
response.headers.forEach((value, key) => {
|
|
490
499
|
responseHeaders[key] = value;
|
|
491
500
|
});
|
|
492
|
-
if (
|
|
493
|
-
console.error("[
|
|
501
|
+
if (response.status === 401) {
|
|
502
|
+
console.error("[HTTP CLIENT DEBUG] 401 UNAUTHORIZED - Full request details:", JSON.stringify({
|
|
494
503
|
status: response.status,
|
|
504
|
+
statusText: response.statusText,
|
|
495
505
|
url: response.url,
|
|
506
|
+
requestMethod: method,
|
|
496
507
|
requestPath: path,
|
|
497
|
-
|
|
508
|
+
requestHeaders: requestDetailsForLogging ? requestDetailsForLogging.headers : {},
|
|
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
|
|
498
521
|
}, null, 2));
|
|
499
522
|
}
|
|
500
523
|
const errorCode = this.statusToErrorCode(response.status);
|
|
@@ -506,6 +529,7 @@ var HttpClient = class {
|
|
|
506
529
|
details: data
|
|
507
530
|
});
|
|
508
531
|
}
|
|
532
|
+
console.error("[HTTP CLIENT DEBUG] Response OK, returning data");
|
|
509
533
|
return data;
|
|
510
534
|
} catch (error) {
|
|
511
535
|
clearTimeout(timeoutId);
|
|
@@ -958,7 +982,7 @@ function defaultExtractTxIntent(command) {
|
|
|
958
982
|
throw new Error("SignCommand missing required Message property");
|
|
959
983
|
}
|
|
960
984
|
const messageBuffer = message instanceof Buffer ? message : Buffer.from(message);
|
|
961
|
-
const messageHash = crypto.createHash("sha256").update(messageBuffer).digest("hex");
|
|
985
|
+
const messageHash = crypto$1.createHash("sha256").update(messageBuffer).digest("hex");
|
|
962
986
|
return {
|
|
963
987
|
networkFamily: "OTHER",
|
|
964
988
|
toAddress: void 0,
|
|
@@ -1064,8 +1088,6 @@ var HeartbeatManager = class {
|
|
|
1064
1088
|
// Unique per process
|
|
1065
1089
|
sdkVersion;
|
|
1066
1090
|
// SDK version for tracking
|
|
1067
|
-
apiKey;
|
|
1068
|
-
// x-gate-heartbeat-key for Control Plane auth
|
|
1069
1091
|
currentToken = null;
|
|
1070
1092
|
refreshTimer = null;
|
|
1071
1093
|
started = false;
|
|
@@ -1078,22 +1100,19 @@ var HeartbeatManager = class {
|
|
|
1078
1100
|
this.signerId = options.signerId;
|
|
1079
1101
|
this.environment = options.environment ?? "prod";
|
|
1080
1102
|
this.baseRefreshIntervalSeconds = options.refreshIntervalSeconds ?? 10;
|
|
1081
|
-
this.apiKey = options.apiKey;
|
|
1082
1103
|
this.clientInstanceId = options.clientInstanceId || uuid.v4();
|
|
1083
1104
|
this.sdkVersion = options.sdkVersion || "1.0.0";
|
|
1084
|
-
this.apiKey = options.apiKey;
|
|
1085
1105
|
}
|
|
1086
1106
|
/**
|
|
1087
|
-
* Start background heartbeat refresher
|
|
1088
|
-
* Optionally wait for initial token (first evaluate() will otherwise wait up to 2s for token).
|
|
1107
|
+
* Start background heartbeat refresher
|
|
1089
1108
|
*/
|
|
1090
|
-
start(
|
|
1109
|
+
start() {
|
|
1091
1110
|
if (this.started) {
|
|
1092
1111
|
return;
|
|
1093
1112
|
}
|
|
1094
1113
|
this.started = true;
|
|
1095
1114
|
this.acquireHeartbeat().catch((error) => {
|
|
1096
|
-
console.error("[HEARTBEAT] Failed to acquire initial heartbeat:", error
|
|
1115
|
+
console.error("[HEARTBEAT] Failed to acquire initial heartbeat:", error);
|
|
1097
1116
|
});
|
|
1098
1117
|
this.scheduleNextRefresh();
|
|
1099
1118
|
}
|
|
@@ -1176,23 +1195,12 @@ var HeartbeatManager = class {
|
|
|
1176
1195
|
/**
|
|
1177
1196
|
* Acquire a new heartbeat token from Control Plane
|
|
1178
1197
|
* NEVER logs token value (security)
|
|
1179
|
-
* Requires x-gate-heartbeat-key header (apiKey) for authentication.
|
|
1180
1198
|
*/
|
|
1181
1199
|
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
|
-
}
|
|
1189
1200
|
try {
|
|
1190
1201
|
const response = await this.httpClient.request({
|
|
1191
1202
|
method: "POST",
|
|
1192
1203
|
path: "/api/v1/gate/heartbeat",
|
|
1193
|
-
headers: {
|
|
1194
|
-
"x-gate-heartbeat-key": this.apiKey
|
|
1195
|
-
},
|
|
1196
1204
|
body: {
|
|
1197
1205
|
tenantId: this.tenantId,
|
|
1198
1206
|
signerId: this.signerId,
|
|
@@ -1520,8 +1528,7 @@ var GateClient = class {
|
|
|
1520
1528
|
this.httpClient = new HttpClient({
|
|
1521
1529
|
baseUrl: config.baseUrl,
|
|
1522
1530
|
timeoutMs: config.timeoutMs,
|
|
1523
|
-
userAgent: config.userAgent
|
|
1524
|
-
debug: config.debug
|
|
1531
|
+
userAgent: config.userAgent
|
|
1525
1532
|
});
|
|
1526
1533
|
if (config.enableStepUp) {
|
|
1527
1534
|
this.stepUpPoller = new StepUpPoller({
|
|
@@ -1542,12 +1549,6 @@ var GateClient = class {
|
|
|
1542
1549
|
console.warn("[GATE CLIENT] LOCAL MODE ENABLED - Auth, heartbeat, and break-glass are disabled");
|
|
1543
1550
|
this.heartbeatManager = null;
|
|
1544
1551
|
} 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
|
-
}
|
|
1551
1552
|
let controlPlaneUrl = config.baseUrl;
|
|
1552
1553
|
if (controlPlaneUrl.includes("/defense")) {
|
|
1553
1554
|
controlPlaneUrl = controlPlaneUrl.split("/defense")[0];
|
|
@@ -1567,8 +1568,7 @@ var GateClient = class {
|
|
|
1567
1568
|
tenantId: config.tenantId,
|
|
1568
1569
|
signerId: initialSignerId,
|
|
1569
1570
|
environment: config.environment ?? "prod",
|
|
1570
|
-
refreshIntervalSeconds: config.heartbeatRefreshIntervalSeconds ?? 10
|
|
1571
|
-
apiKey: heartbeatApiKey
|
|
1571
|
+
refreshIntervalSeconds: config.heartbeatRefreshIntervalSeconds ?? 10
|
|
1572
1572
|
});
|
|
1573
1573
|
this.heartbeatManager.start();
|
|
1574
1574
|
}
|
|
@@ -1657,9 +1657,7 @@ var GateClient = class {
|
|
|
1657
1657
|
delete txIntent.from;
|
|
1658
1658
|
}
|
|
1659
1659
|
const 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"
|
|
1660
|
+
...req.signingContext
|
|
1663
1661
|
};
|
|
1664
1662
|
if (heartbeatToken) {
|
|
1665
1663
|
signingContext.heartbeatToken = heartbeatToken;
|
|
@@ -1675,16 +1673,17 @@ var GateClient = class {
|
|
|
1675
1673
|
};
|
|
1676
1674
|
}
|
|
1677
1675
|
let body = {
|
|
1678
|
-
tenantId: this.config.tenantId,
|
|
1679
1676
|
requestId,
|
|
1680
1677
|
timestampMs,
|
|
1681
1678
|
txIntent,
|
|
1682
1679
|
signingContext,
|
|
1683
1680
|
// Add SDK info (required by Hot Path validation)
|
|
1681
|
+
// Note: Must match Python SDK name for consistent canonical JSON
|
|
1684
1682
|
sdk: {
|
|
1685
1683
|
name: "gate-sdk",
|
|
1686
1684
|
version: "0.1.0"
|
|
1687
1685
|
},
|
|
1686
|
+
// Add mode and connection failure strategy
|
|
1688
1687
|
mode: requestMode,
|
|
1689
1688
|
onConnectionFailure: this.onConnectionFailure
|
|
1690
1689
|
};
|
|
@@ -1714,6 +1713,15 @@ var GateClient = class {
|
|
|
1714
1713
|
});
|
|
1715
1714
|
headers = { ...hmacHeaders };
|
|
1716
1715
|
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));
|
|
1717
1725
|
} else if (this.apiKeyAuth) {
|
|
1718
1726
|
const apiKeyHeaders = this.apiKeyAuth.createHeaders({
|
|
1719
1727
|
tenantId: this.config.tenantId,
|
|
@@ -1721,6 +1729,7 @@ var GateClient = class {
|
|
|
1721
1729
|
requestId
|
|
1722
1730
|
});
|
|
1723
1731
|
headers = { ...apiKeyHeaders };
|
|
1732
|
+
console.error("[GATE CLIENT DEBUG] API key headers prepared:", JSON.stringify(headers, null, 2));
|
|
1724
1733
|
} else {
|
|
1725
1734
|
throw new Error("No authentication configured");
|
|
1726
1735
|
}
|
|
@@ -1854,7 +1863,6 @@ var GateClient = class {
|
|
|
1854
1863
|
tenantId: this.config.tenantId,
|
|
1855
1864
|
mode: requestMode
|
|
1856
1865
|
});
|
|
1857
|
-
console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_open)");
|
|
1858
1866
|
this.metrics.recordRequest("FAIL_OPEN", Date.now() - startTime);
|
|
1859
1867
|
return {
|
|
1860
1868
|
decision: "ALLOW",
|
|
@@ -1886,10 +1894,6 @@ var GateClient = class {
|
|
|
1886
1894
|
}
|
|
1887
1895
|
throw error;
|
|
1888
1896
|
}
|
|
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
|
-
}
|
|
1893
1897
|
if (error instanceof BlockIntelBlockedError || error instanceof BlockIntelStepUpRequiredError) {
|
|
1894
1898
|
throw error;
|
|
1895
1899
|
}
|
|
@@ -1902,7 +1906,6 @@ var GateClient = class {
|
|
|
1902
1906
|
*/
|
|
1903
1907
|
handleFailSafe(mode, error, requestId) {
|
|
1904
1908
|
if (mode === "ALLOW_ON_TIMEOUT") {
|
|
1905
|
-
console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_safe_allow)");
|
|
1906
1909
|
return {
|
|
1907
1910
|
decision: "ALLOW",
|
|
1908
1911
|
reasonCodes: ["FAIL_SAFE_ALLOW"],
|
|
@@ -1913,7 +1916,6 @@ var GateClient = class {
|
|
|
1913
1916
|
return null;
|
|
1914
1917
|
}
|
|
1915
1918
|
if (mode === "BLOCK_ON_ANOMALY") {
|
|
1916
|
-
console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_safe_allow)");
|
|
1917
1919
|
return {
|
|
1918
1920
|
decision: "ALLOW",
|
|
1919
1921
|
reasonCodes: ["FAIL_SAFE_ALLOW"],
|
|
@@ -1988,10 +1990,26 @@ function createGateClient(config) {
|
|
|
1988
1990
|
return new GateClient(config);
|
|
1989
1991
|
}
|
|
1990
1992
|
|
|
1993
|
+
// src/client/Gate.ts
|
|
1994
|
+
var Gate = class {
|
|
1995
|
+
apiKey;
|
|
1996
|
+
constructor(opts) {
|
|
1997
|
+
this.apiKey = opts?.apiKey ?? process.env.BLOCKINTEL_API_KEY;
|
|
1998
|
+
}
|
|
1999
|
+
/**
|
|
2000
|
+
* Guard a signing operation. In passthrough mode, executes the callback.
|
|
2001
|
+
* For full Gate integration, use GateClient with evaluate() before sending.
|
|
2002
|
+
*/
|
|
2003
|
+
async guard(_ctx, cb) {
|
|
2004
|
+
return cb();
|
|
2005
|
+
}
|
|
2006
|
+
};
|
|
2007
|
+
|
|
1991
2008
|
exports.BlockIntelAuthError = BlockIntelAuthError;
|
|
1992
2009
|
exports.BlockIntelBlockedError = BlockIntelBlockedError;
|
|
1993
2010
|
exports.BlockIntelStepUpRequiredError = BlockIntelStepUpRequiredError;
|
|
1994
2011
|
exports.BlockIntelUnavailableError = BlockIntelUnavailableError;
|
|
2012
|
+
exports.Gate = Gate;
|
|
1995
2013
|
exports.GateClient = GateClient;
|
|
1996
2014
|
exports.GateError = GateError;
|
|
1997
2015
|
exports.GateErrorCode = GateErrorCode;
|