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/dist/index.d.cts
CHANGED
|
@@ -252,6 +252,10 @@ interface GateClientConfig {
|
|
|
252
252
|
onMetrics?: (metrics: Metrics) => void | Promise<void>;
|
|
253
253
|
signerId?: string;
|
|
254
254
|
heartbeatRefreshIntervalSeconds?: number;
|
|
255
|
+
/** API key for Control Plane heartbeat endpoint (x-gate-heartbeat-key). Required when not local. Fallback: GATE_HEARTBEAT_KEY env. */
|
|
256
|
+
heartbeatApiKey?: string;
|
|
257
|
+
/** When true or GATE_SDK_DEBUG=1, log sanitized request/response (no secrets, no body values). */
|
|
258
|
+
debug?: boolean;
|
|
255
259
|
/**
|
|
256
260
|
* Break-glass token (optional, for emergency override)
|
|
257
261
|
*
|
|
@@ -671,6 +675,8 @@ interface HttpClientConfig {
|
|
|
671
675
|
baseUrl: string;
|
|
672
676
|
timeoutMs?: number;
|
|
673
677
|
userAgent?: string;
|
|
678
|
+
/** When true or GATE_SDK_DEBUG=1, log sanitized request/response (no secrets, no body values). */
|
|
679
|
+
debug?: boolean;
|
|
674
680
|
retryOptions?: {
|
|
675
681
|
maxAttempts?: number;
|
|
676
682
|
baseDelayMs?: number;
|
|
@@ -693,6 +699,7 @@ declare class HttpClient {
|
|
|
693
699
|
private readonly timeoutMs;
|
|
694
700
|
private readonly userAgent;
|
|
695
701
|
private readonly retryOptions;
|
|
702
|
+
private readonly debug;
|
|
696
703
|
constructor(config: HttpClientConfig);
|
|
697
704
|
/**
|
|
698
705
|
* Make an HTTP request with retry and timeout
|
|
@@ -731,6 +738,7 @@ declare class HeartbeatManager {
|
|
|
731
738
|
private readonly baseRefreshIntervalSeconds;
|
|
732
739
|
private readonly clientInstanceId;
|
|
733
740
|
private readonly sdkVersion;
|
|
741
|
+
private readonly apiKey;
|
|
734
742
|
private currentToken;
|
|
735
743
|
private refreshTimer;
|
|
736
744
|
private started;
|
|
@@ -744,11 +752,16 @@ declare class HeartbeatManager {
|
|
|
744
752
|
refreshIntervalSeconds?: number;
|
|
745
753
|
clientInstanceId?: string;
|
|
746
754
|
sdkVersion?: string;
|
|
755
|
+
/** API key for heartbeat endpoint auth (x-gate-heartbeat-key). Required unless local mode. */
|
|
756
|
+
apiKey?: string;
|
|
747
757
|
});
|
|
748
758
|
/**
|
|
749
|
-
* Start background heartbeat refresher
|
|
759
|
+
* Start background heartbeat refresher.
|
|
760
|
+
* Optionally wait for initial token (first evaluate() will otherwise wait up to 2s for token).
|
|
750
761
|
*/
|
|
751
|
-
start(
|
|
762
|
+
start(options?: {
|
|
763
|
+
waitForInitial?: boolean;
|
|
764
|
+
}): void;
|
|
752
765
|
/**
|
|
753
766
|
* Schedule next refresh with jitter and backoff
|
|
754
767
|
*/
|
|
@@ -776,6 +789,7 @@ declare class HeartbeatManager {
|
|
|
776
789
|
/**
|
|
777
790
|
* Acquire a new heartbeat token from Control Plane
|
|
778
791
|
* NEVER logs token value (security)
|
|
792
|
+
* Requires x-gate-heartbeat-key header (apiKey) for authentication.
|
|
779
793
|
*/
|
|
780
794
|
private acquireHeartbeat;
|
|
781
795
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -252,6 +252,10 @@ interface GateClientConfig {
|
|
|
252
252
|
onMetrics?: (metrics: Metrics) => void | Promise<void>;
|
|
253
253
|
signerId?: string;
|
|
254
254
|
heartbeatRefreshIntervalSeconds?: number;
|
|
255
|
+
/** API key for Control Plane heartbeat endpoint (x-gate-heartbeat-key). Required when not local. Fallback: GATE_HEARTBEAT_KEY env. */
|
|
256
|
+
heartbeatApiKey?: string;
|
|
257
|
+
/** When true or GATE_SDK_DEBUG=1, log sanitized request/response (no secrets, no body values). */
|
|
258
|
+
debug?: boolean;
|
|
255
259
|
/**
|
|
256
260
|
* Break-glass token (optional, for emergency override)
|
|
257
261
|
*
|
|
@@ -671,6 +675,8 @@ interface HttpClientConfig {
|
|
|
671
675
|
baseUrl: string;
|
|
672
676
|
timeoutMs?: number;
|
|
673
677
|
userAgent?: string;
|
|
678
|
+
/** When true or GATE_SDK_DEBUG=1, log sanitized request/response (no secrets, no body values). */
|
|
679
|
+
debug?: boolean;
|
|
674
680
|
retryOptions?: {
|
|
675
681
|
maxAttempts?: number;
|
|
676
682
|
baseDelayMs?: number;
|
|
@@ -693,6 +699,7 @@ declare class HttpClient {
|
|
|
693
699
|
private readonly timeoutMs;
|
|
694
700
|
private readonly userAgent;
|
|
695
701
|
private readonly retryOptions;
|
|
702
|
+
private readonly debug;
|
|
696
703
|
constructor(config: HttpClientConfig);
|
|
697
704
|
/**
|
|
698
705
|
* Make an HTTP request with retry and timeout
|
|
@@ -731,6 +738,7 @@ declare class HeartbeatManager {
|
|
|
731
738
|
private readonly baseRefreshIntervalSeconds;
|
|
732
739
|
private readonly clientInstanceId;
|
|
733
740
|
private readonly sdkVersion;
|
|
741
|
+
private readonly apiKey;
|
|
734
742
|
private currentToken;
|
|
735
743
|
private refreshTimer;
|
|
736
744
|
private started;
|
|
@@ -744,11 +752,16 @@ declare class HeartbeatManager {
|
|
|
744
752
|
refreshIntervalSeconds?: number;
|
|
745
753
|
clientInstanceId?: string;
|
|
746
754
|
sdkVersion?: string;
|
|
755
|
+
/** API key for heartbeat endpoint auth (x-gate-heartbeat-key). Required unless local mode. */
|
|
756
|
+
apiKey?: string;
|
|
747
757
|
});
|
|
748
758
|
/**
|
|
749
|
-
* Start background heartbeat refresher
|
|
759
|
+
* Start background heartbeat refresher.
|
|
760
|
+
* Optionally wait for initial token (first evaluate() will otherwise wait up to 2s for token).
|
|
750
761
|
*/
|
|
751
|
-
start(
|
|
762
|
+
start(options?: {
|
|
763
|
+
waitForInitial?: boolean;
|
|
764
|
+
}): void;
|
|
752
765
|
/**
|
|
753
766
|
* Schedule next refresh with jitter and backoff
|
|
754
767
|
*/
|
|
@@ -776,6 +789,7 @@ declare class HeartbeatManager {
|
|
|
776
789
|
/**
|
|
777
790
|
* Acquire a new heartbeat token from Control Plane
|
|
778
791
|
* NEVER logs token value (security)
|
|
792
|
+
* Requires x-gate-heartbeat-key header (apiKey) for authentication.
|
|
779
793
|
*/
|
|
780
794
|
private acquireHeartbeat;
|
|
781
795
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
+
import { createHash, createHmac } from 'crypto';
|
|
1
2
|
import { v4 } from 'uuid';
|
|
2
3
|
import { SignCommand } from '@aws-sdk/client-kms';
|
|
3
|
-
import { createHash } from 'crypto';
|
|
4
4
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
-
}) : x)(function(x) {
|
|
10
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
-
});
|
|
13
7
|
var __esm = (fn, res) => function __init() {
|
|
14
8
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
9
|
};
|
|
@@ -46,56 +40,24 @@ function canonicalizeJson(obj) {
|
|
|
46
40
|
return JSON.stringify(sorted);
|
|
47
41
|
}
|
|
48
42
|
async function sha256Hex(input) {
|
|
49
|
-
|
|
50
|
-
const encoder = new TextEncoder();
|
|
51
|
-
const data = encoder.encode(input);
|
|
52
|
-
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
53
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
54
|
-
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
55
|
-
}
|
|
56
|
-
if (typeof __require !== "undefined") {
|
|
57
|
-
const crypto2 = __require("crypto");
|
|
58
|
-
return crypto2.createHash("sha256").update(input, "utf8").digest("hex");
|
|
59
|
-
}
|
|
60
|
-
throw new Error("SHA-256 not available in this environment");
|
|
43
|
+
return createHash("sha256").update(input, "utf8").digest("hex");
|
|
61
44
|
}
|
|
62
45
|
var init_canonicalJson = __esm({
|
|
63
46
|
"src/utils/canonicalJson.ts"() {
|
|
64
47
|
}
|
|
65
48
|
});
|
|
66
|
-
|
|
67
|
-
// src/utils/crypto.ts
|
|
68
49
|
async function hmacSha256(secret, message) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}, null, 2));
|
|
81
|
-
return signatureHex;
|
|
82
|
-
}
|
|
83
|
-
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
84
|
-
const encoder = new TextEncoder();
|
|
85
|
-
const keyData = encoder.encode(secret);
|
|
86
|
-
const messageData = encoder.encode(message);
|
|
87
|
-
const key = await crypto.subtle.importKey(
|
|
88
|
-
"raw",
|
|
89
|
-
keyData,
|
|
90
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
91
|
-
false,
|
|
92
|
-
["sign"]
|
|
93
|
-
);
|
|
94
|
-
const signature = await crypto.subtle.sign("HMAC", key, messageData);
|
|
95
|
-
const hashArray = Array.from(new Uint8Array(signature));
|
|
96
|
-
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
97
|
-
}
|
|
98
|
-
throw new Error("HMAC-SHA256 not available in this environment");
|
|
50
|
+
const hmac = createHmac("sha256", secret);
|
|
51
|
+
hmac.update(message, "utf8");
|
|
52
|
+
const signatureHex = hmac.digest("hex");
|
|
53
|
+
console.error("[HMAC CRYPTO DEBUG] Signature computation:", JSON.stringify({
|
|
54
|
+
secretLength: secret.length,
|
|
55
|
+
messageLength: message.length,
|
|
56
|
+
messagePreview: message.substring(0, 200) + "...",
|
|
57
|
+
signatureLength: signatureHex.length,
|
|
58
|
+
signaturePreview: signatureHex.substring(0, 16) + "..."
|
|
59
|
+
}, null, 2));
|
|
60
|
+
return signatureHex;
|
|
99
61
|
}
|
|
100
62
|
|
|
101
63
|
// src/auth/HmacSigner.ts
|
|
@@ -128,26 +90,7 @@ var HmacSigner = class {
|
|
|
128
90
|
// Used as nonce in canonical string
|
|
129
91
|
bodyHash
|
|
130
92
|
].join("\n");
|
|
131
|
-
console.error("[HMAC SIGNER DEBUG] Canonical request string:", JSON.stringify({
|
|
132
|
-
method: method.toUpperCase(),
|
|
133
|
-
path,
|
|
134
|
-
tenantId,
|
|
135
|
-
keyId: this.keyId,
|
|
136
|
-
timestampMs: String(timestampMs),
|
|
137
|
-
requestId,
|
|
138
|
-
bodyHash,
|
|
139
|
-
signingStringLength: signingString.length,
|
|
140
|
-
signingStringPreview: signingString.substring(0, 200) + "...",
|
|
141
|
-
bodyJsonLength: bodyJson.length,
|
|
142
|
-
bodyJsonPreview: bodyJson.substring(0, 200) + "..."
|
|
143
|
-
}, null, 2));
|
|
144
93
|
const signature = await hmacSha256(this.secret, signingString);
|
|
145
|
-
console.error("[HMAC SIGNER DEBUG] Signature computed:", JSON.stringify({
|
|
146
|
-
signatureLength: signature.length,
|
|
147
|
-
signaturePreview: signature.substring(0, 16) + "...",
|
|
148
|
-
secretLength: this.secret.length,
|
|
149
|
-
secretPreview: this.secret.substring(0, 4) + "..." + this.secret.substring(this.secret.length - 4)
|
|
150
|
-
}, null, 2));
|
|
151
94
|
return {
|
|
152
95
|
"X-GATE-TENANT-ID": tenantId,
|
|
153
96
|
"X-GATE-KEY-ID": this.keyId,
|
|
@@ -346,6 +289,10 @@ async function retryWithBackoff(fn, options = {}) {
|
|
|
346
289
|
if (!isRetryable) {
|
|
347
290
|
throw error;
|
|
348
291
|
}
|
|
292
|
+
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;
|
|
293
|
+
const errName = error instanceof Error ? error.name : error && typeof error === "object" && "code" in error ? error.code : "Unknown";
|
|
294
|
+
const extra = ` attempt=${attempt}/${opts.maxAttempts} status=${status ?? "n/a"} err=${errName}`;
|
|
295
|
+
console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason=retry)" + extra);
|
|
349
296
|
const delay = calculateBackoffDelay(attempt, opts);
|
|
350
297
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
351
298
|
}
|
|
@@ -353,17 +300,73 @@ async function retryWithBackoff(fn, options = {}) {
|
|
|
353
300
|
throw lastError;
|
|
354
301
|
}
|
|
355
302
|
|
|
303
|
+
// src/utils/sanitize.ts
|
|
304
|
+
var SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
|
|
305
|
+
"authorization",
|
|
306
|
+
"x-api-key",
|
|
307
|
+
"x-gate-heartbeat-key",
|
|
308
|
+
"x-gate-signature",
|
|
309
|
+
"cookie"
|
|
310
|
+
]);
|
|
311
|
+
var MAX_STRING_LENGTH = 80;
|
|
312
|
+
function sanitizeHeaders(headers) {
|
|
313
|
+
const out = {};
|
|
314
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
315
|
+
const lower = key.toLowerCase();
|
|
316
|
+
if (SENSITIVE_HEADER_NAMES.has(lower) || lower.includes("signature") || lower.includes("secret") || lower.includes("token")) {
|
|
317
|
+
out[key] = value ? "[REDACTED]" : "[empty]";
|
|
318
|
+
} else {
|
|
319
|
+
out[key] = truncate(String(value), MAX_STRING_LENGTH);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
function sanitizeBodyShape(body) {
|
|
325
|
+
if (body === null || body === void 0) {
|
|
326
|
+
return {};
|
|
327
|
+
}
|
|
328
|
+
if (typeof body !== "object") {
|
|
329
|
+
return { _: typeof body };
|
|
330
|
+
}
|
|
331
|
+
if (Array.isArray(body)) {
|
|
332
|
+
return { _: "array", length: String(body.length) };
|
|
333
|
+
}
|
|
334
|
+
const out = {};
|
|
335
|
+
for (const key of Object.keys(body).sort()) {
|
|
336
|
+
const val = body[key];
|
|
337
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val)) {
|
|
338
|
+
out[key] = "object";
|
|
339
|
+
} else if (Array.isArray(val)) {
|
|
340
|
+
out[key] = "array";
|
|
341
|
+
} else {
|
|
342
|
+
out[key] = typeof val;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
function truncate(s, max) {
|
|
348
|
+
if (s.length <= max) return s;
|
|
349
|
+
return s.slice(0, max) + "...";
|
|
350
|
+
}
|
|
351
|
+
function isDebugEnabled(debugOption) {
|
|
352
|
+
if (debugOption === true) return true;
|
|
353
|
+
if (typeof process !== "undefined" && process.env.GATE_SDK_DEBUG === "1") return true;
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
356
357
|
// src/http/HttpClient.ts
|
|
357
358
|
var HttpClient = class {
|
|
358
359
|
baseUrl;
|
|
359
360
|
timeoutMs;
|
|
360
361
|
userAgent;
|
|
361
362
|
retryOptions;
|
|
363
|
+
debug;
|
|
362
364
|
constructor(config) {
|
|
363
365
|
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
364
366
|
this.timeoutMs = config.timeoutMs ?? 15e3;
|
|
365
367
|
this.userAgent = config.userAgent ?? "blockintel-gate-sdk/0.1.0";
|
|
366
368
|
this.retryOptions = config.retryOptions;
|
|
369
|
+
this.debug = isDebugEnabled(config.debug);
|
|
367
370
|
if (!this.baseUrl) {
|
|
368
371
|
throw new Error("baseUrl is required");
|
|
369
372
|
}
|
|
@@ -382,7 +385,6 @@ var HttpClient = class {
|
|
|
382
385
|
const controller = new AbortController();
|
|
383
386
|
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
384
387
|
let requestDetailsForLogging = null;
|
|
385
|
-
let requestDetailsSet = false;
|
|
386
388
|
try {
|
|
387
389
|
const response = await retryWithBackoff(
|
|
388
390
|
async () => {
|
|
@@ -405,31 +407,22 @@ var HttpClient = class {
|
|
|
405
407
|
fetchOptions.body = JSON.stringify(body);
|
|
406
408
|
}
|
|
407
409
|
}
|
|
408
|
-
const logHeaders = {};
|
|
409
|
-
if (fetchOptions.headers) {
|
|
410
|
-
Object.entries(fetchOptions.headers).forEach(([key, value]) => {
|
|
411
|
-
if (key.toLowerCase().includes("signature") || key.toLowerCase().includes("secret")) {
|
|
412
|
-
logHeaders[key] = String(value).substring(0, 8) + "...";
|
|
413
|
-
} else {
|
|
414
|
-
logHeaders[key] = String(value);
|
|
415
|
-
}
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
410
|
const bodyStr = typeof fetchOptions.body === "string" ? fetchOptions.body : null;
|
|
419
|
-
|
|
420
|
-
headers:
|
|
421
|
-
bodyLength: bodyStr ? bodyStr.length : 0
|
|
422
|
-
bodyPreview: bodyStr ? bodyStr.substring(0, 300) : null
|
|
411
|
+
requestDetailsForLogging = {
|
|
412
|
+
headers: this.debug ? sanitizeHeaders(requestHeaders) : {},
|
|
413
|
+
bodyLength: bodyStr ? bodyStr.length : 0
|
|
423
414
|
};
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
415
|
+
if (this.debug) {
|
|
416
|
+
const bodyShape = body && typeof body === "object" ? sanitizeBodyShape(body) : {};
|
|
417
|
+
console.error("[GATE SDK] Request:", JSON.stringify({
|
|
418
|
+
url,
|
|
419
|
+
method,
|
|
420
|
+
headerNames: Object.keys(requestHeaders),
|
|
421
|
+
headersRedacted: requestDetailsForLogging.headers,
|
|
422
|
+
bodyLength: requestDetailsForLogging.bodyLength,
|
|
423
|
+
bodyKeysAndTypes: bodyShape
|
|
424
|
+
}, null, 2));
|
|
425
|
+
}
|
|
433
426
|
const res = await fetch(url, fetchOptions);
|
|
434
427
|
if (!res.ok && isRetryableStatus(res.status)) {
|
|
435
428
|
throw res;
|
|
@@ -447,26 +440,24 @@ var HttpClient = class {
|
|
|
447
440
|
clearTimeout(timeoutId);
|
|
448
441
|
let data;
|
|
449
442
|
const contentType = response.headers.get("content-type");
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
443
|
+
if (this.debug) {
|
|
444
|
+
console.error("[GATE SDK] Response:", JSON.stringify({
|
|
445
|
+
status: response.status,
|
|
446
|
+
ok: response.ok,
|
|
447
|
+
url: response.url
|
|
448
|
+
}, null, 2));
|
|
449
|
+
}
|
|
457
450
|
if (contentType && contentType.includes("application/json")) {
|
|
458
451
|
try {
|
|
459
452
|
const jsonText = await response.text();
|
|
460
|
-
console.error("[HTTP CLIENT DEBUG] Response body (first 500 chars):", jsonText.substring(0, 500));
|
|
461
453
|
data = JSON.parse(jsonText);
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
hasData: typeof data?.data !== "undefined",
|
|
466
|
-
hasError: typeof data?.error !== "undefined"
|
|
467
|
-
}, null, 2));
|
|
454
|
+
if (this.debug && data && typeof data === "object") {
|
|
455
|
+
console.error("[GATE SDK] Response keys:", Object.keys(data));
|
|
456
|
+
}
|
|
468
457
|
} catch (parseError) {
|
|
469
|
-
|
|
458
|
+
if (this.debug) {
|
|
459
|
+
console.error("[GATE SDK] JSON parse error:", parseError instanceof Error ? parseError.message : String(parseError));
|
|
460
|
+
}
|
|
470
461
|
throw new GateError(
|
|
471
462
|
"INVALID_RESPONSE" /* INVALID_RESPONSE */,
|
|
472
463
|
"Failed to parse JSON response",
|
|
@@ -494,26 +485,12 @@ var HttpClient = class {
|
|
|
494
485
|
response.headers.forEach((value, key) => {
|
|
495
486
|
responseHeaders[key] = value;
|
|
496
487
|
});
|
|
497
|
-
if (
|
|
498
|
-
console.error("[
|
|
488
|
+
if (this.debug) {
|
|
489
|
+
console.error("[GATE SDK] Error response:", JSON.stringify({
|
|
499
490
|
status: response.status,
|
|
500
|
-
statusText: response.statusText,
|
|
501
491
|
url: response.url,
|
|
502
|
-
requestMethod: method,
|
|
503
492
|
requestPath: path,
|
|
504
|
-
|
|
505
|
-
responseHeaders,
|
|
506
|
-
responseData: data,
|
|
507
|
-
bodyLength: requestDetailsForLogging ? requestDetailsForLogging.bodyLength : 0,
|
|
508
|
-
bodyPreview: requestDetailsForLogging ? requestDetailsForLogging.bodyPreview : null
|
|
509
|
-
}, null, 2));
|
|
510
|
-
} else {
|
|
511
|
-
console.error("[HTTP CLIENT DEBUG] Response not OK:", JSON.stringify({
|
|
512
|
-
status: response.status,
|
|
513
|
-
statusText: response.statusText,
|
|
514
|
-
url: response.url,
|
|
515
|
-
headers: responseHeaders,
|
|
516
|
-
data
|
|
493
|
+
responseKeys: data && typeof data === "object" ? Object.keys(data) : []
|
|
517
494
|
}, null, 2));
|
|
518
495
|
}
|
|
519
496
|
const errorCode = this.statusToErrorCode(response.status);
|
|
@@ -525,7 +502,6 @@ var HttpClient = class {
|
|
|
525
502
|
details: data
|
|
526
503
|
});
|
|
527
504
|
}
|
|
528
|
-
console.error("[HTTP CLIENT DEBUG] Response OK, returning data");
|
|
529
505
|
return data;
|
|
530
506
|
} catch (error) {
|
|
531
507
|
clearTimeout(timeoutId);
|
|
@@ -1084,6 +1060,8 @@ var HeartbeatManager = class {
|
|
|
1084
1060
|
// Unique per process
|
|
1085
1061
|
sdkVersion;
|
|
1086
1062
|
// SDK version for tracking
|
|
1063
|
+
apiKey;
|
|
1064
|
+
// x-gate-heartbeat-key for Control Plane auth
|
|
1087
1065
|
currentToken = null;
|
|
1088
1066
|
refreshTimer = null;
|
|
1089
1067
|
started = false;
|
|
@@ -1096,19 +1074,22 @@ var HeartbeatManager = class {
|
|
|
1096
1074
|
this.signerId = options.signerId;
|
|
1097
1075
|
this.environment = options.environment ?? "prod";
|
|
1098
1076
|
this.baseRefreshIntervalSeconds = options.refreshIntervalSeconds ?? 10;
|
|
1077
|
+
this.apiKey = options.apiKey;
|
|
1099
1078
|
this.clientInstanceId = options.clientInstanceId || v4();
|
|
1100
1079
|
this.sdkVersion = options.sdkVersion || "1.0.0";
|
|
1080
|
+
this.apiKey = options.apiKey;
|
|
1101
1081
|
}
|
|
1102
1082
|
/**
|
|
1103
|
-
* Start background heartbeat refresher
|
|
1083
|
+
* Start background heartbeat refresher.
|
|
1084
|
+
* Optionally wait for initial token (first evaluate() will otherwise wait up to 2s for token).
|
|
1104
1085
|
*/
|
|
1105
|
-
start() {
|
|
1086
|
+
start(options) {
|
|
1106
1087
|
if (this.started) {
|
|
1107
1088
|
return;
|
|
1108
1089
|
}
|
|
1109
1090
|
this.started = true;
|
|
1110
1091
|
this.acquireHeartbeat().catch((error) => {
|
|
1111
|
-
console.error("[HEARTBEAT] Failed to acquire initial heartbeat:", error);
|
|
1092
|
+
console.error("[HEARTBEAT] Failed to acquire initial heartbeat:", error instanceof Error ? error.message : error);
|
|
1112
1093
|
});
|
|
1113
1094
|
this.scheduleNextRefresh();
|
|
1114
1095
|
}
|
|
@@ -1191,12 +1172,23 @@ var HeartbeatManager = class {
|
|
|
1191
1172
|
/**
|
|
1192
1173
|
* Acquire a new heartbeat token from Control Plane
|
|
1193
1174
|
* NEVER logs token value (security)
|
|
1175
|
+
* Requires x-gate-heartbeat-key header (apiKey) for authentication.
|
|
1194
1176
|
*/
|
|
1195
1177
|
async acquireHeartbeat() {
|
|
1178
|
+
if (!this.apiKey || this.apiKey.length === 0) {
|
|
1179
|
+
throw new GateError(
|
|
1180
|
+
"UNAUTHORIZED" /* UNAUTHORIZED */,
|
|
1181
|
+
"Heartbeat API key is required. Set GATE_HEARTBEAT_KEY in environment or pass heartbeatApiKey in GateClientConfig.",
|
|
1182
|
+
{}
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1196
1185
|
try {
|
|
1197
1186
|
const response = await this.httpClient.request({
|
|
1198
1187
|
method: "POST",
|
|
1199
1188
|
path: "/api/v1/gate/heartbeat",
|
|
1189
|
+
headers: {
|
|
1190
|
+
"x-gate-heartbeat-key": this.apiKey
|
|
1191
|
+
},
|
|
1200
1192
|
body: {
|
|
1201
1193
|
tenantId: this.tenantId,
|
|
1202
1194
|
signerId: this.signerId,
|
|
@@ -1524,7 +1516,8 @@ var GateClient = class {
|
|
|
1524
1516
|
this.httpClient = new HttpClient({
|
|
1525
1517
|
baseUrl: config.baseUrl,
|
|
1526
1518
|
timeoutMs: config.timeoutMs,
|
|
1527
|
-
userAgent: config.userAgent
|
|
1519
|
+
userAgent: config.userAgent,
|
|
1520
|
+
debug: config.debug
|
|
1528
1521
|
});
|
|
1529
1522
|
if (config.enableStepUp) {
|
|
1530
1523
|
this.stepUpPoller = new StepUpPoller({
|
|
@@ -1545,6 +1538,12 @@ var GateClient = class {
|
|
|
1545
1538
|
console.warn("[GATE CLIENT] LOCAL MODE ENABLED - Auth, heartbeat, and break-glass are disabled");
|
|
1546
1539
|
this.heartbeatManager = null;
|
|
1547
1540
|
} else {
|
|
1541
|
+
const heartbeatApiKey = config.heartbeatApiKey ?? (typeof process !== "undefined" ? process.env.GATE_HEARTBEAT_KEY : void 0);
|
|
1542
|
+
if (!heartbeatApiKey || heartbeatApiKey.length === 0) {
|
|
1543
|
+
throw new Error(
|
|
1544
|
+
"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."
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1548
1547
|
let controlPlaneUrl = config.baseUrl;
|
|
1549
1548
|
if (controlPlaneUrl.includes("/defense")) {
|
|
1550
1549
|
controlPlaneUrl = controlPlaneUrl.split("/defense")[0];
|
|
@@ -1564,7 +1563,8 @@ var GateClient = class {
|
|
|
1564
1563
|
tenantId: config.tenantId,
|
|
1565
1564
|
signerId: initialSignerId,
|
|
1566
1565
|
environment: config.environment ?? "prod",
|
|
1567
|
-
refreshIntervalSeconds: config.heartbeatRefreshIntervalSeconds ?? 10
|
|
1566
|
+
refreshIntervalSeconds: config.heartbeatRefreshIntervalSeconds ?? 10,
|
|
1567
|
+
apiKey: heartbeatApiKey
|
|
1568
1568
|
});
|
|
1569
1569
|
this.heartbeatManager.start();
|
|
1570
1570
|
}
|
|
@@ -1653,7 +1653,9 @@ var GateClient = class {
|
|
|
1653
1653
|
delete txIntent.from;
|
|
1654
1654
|
}
|
|
1655
1655
|
const signingContext = {
|
|
1656
|
-
...req.signingContext
|
|
1656
|
+
...req.signingContext,
|
|
1657
|
+
actorPrincipal: req.signingContext?.actorPrincipal ?? req.signingContext?.signerId ?? "gate-sdk-client",
|
|
1658
|
+
signerId: req.signingContext?.signerId ?? req.signingContext?.actorPrincipal ?? "gate-sdk-client"
|
|
1657
1659
|
};
|
|
1658
1660
|
if (heartbeatToken) {
|
|
1659
1661
|
signingContext.heartbeatToken = heartbeatToken;
|
|
@@ -1669,17 +1671,16 @@ var GateClient = class {
|
|
|
1669
1671
|
};
|
|
1670
1672
|
}
|
|
1671
1673
|
let body = {
|
|
1674
|
+
tenantId: this.config.tenantId,
|
|
1672
1675
|
requestId,
|
|
1673
1676
|
timestampMs,
|
|
1674
1677
|
txIntent,
|
|
1675
1678
|
signingContext,
|
|
1676
1679
|
// Add SDK info (required by Hot Path validation)
|
|
1677
|
-
// Note: Must match Python SDK name for consistent canonical JSON
|
|
1678
1680
|
sdk: {
|
|
1679
1681
|
name: "gate-sdk",
|
|
1680
1682
|
version: "0.1.0"
|
|
1681
1683
|
},
|
|
1682
|
-
// Add mode and connection failure strategy
|
|
1683
1684
|
mode: requestMode,
|
|
1684
1685
|
onConnectionFailure: this.onConnectionFailure
|
|
1685
1686
|
};
|
|
@@ -1709,15 +1710,6 @@ var GateClient = class {
|
|
|
1709
1710
|
});
|
|
1710
1711
|
headers = { ...hmacHeaders };
|
|
1711
1712
|
body.__canonicalJson = canonicalBodyJson;
|
|
1712
|
-
const debugHeaders = {};
|
|
1713
|
-
Object.entries(headers).forEach(([key, value]) => {
|
|
1714
|
-
if (key.toLowerCase().includes("signature")) {
|
|
1715
|
-
debugHeaders[key] = value.substring(0, 8) + "...";
|
|
1716
|
-
} else {
|
|
1717
|
-
debugHeaders[key] = value;
|
|
1718
|
-
}
|
|
1719
|
-
});
|
|
1720
|
-
console.error("[GATE CLIENT DEBUG] HMAC headers prepared:", JSON.stringify(debugHeaders, null, 2));
|
|
1721
1713
|
} else if (this.apiKeyAuth) {
|
|
1722
1714
|
const apiKeyHeaders = this.apiKeyAuth.createHeaders({
|
|
1723
1715
|
tenantId: this.config.tenantId,
|
|
@@ -1725,7 +1717,6 @@ var GateClient = class {
|
|
|
1725
1717
|
requestId
|
|
1726
1718
|
});
|
|
1727
1719
|
headers = { ...apiKeyHeaders };
|
|
1728
|
-
console.error("[GATE CLIENT DEBUG] API key headers prepared:", JSON.stringify(headers, null, 2));
|
|
1729
1720
|
} else {
|
|
1730
1721
|
throw new Error("No authentication configured");
|
|
1731
1722
|
}
|
|
@@ -1859,6 +1850,7 @@ var GateClient = class {
|
|
|
1859
1850
|
tenantId: this.config.tenantId,
|
|
1860
1851
|
mode: requestMode
|
|
1861
1852
|
});
|
|
1853
|
+
console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_open)");
|
|
1862
1854
|
this.metrics.recordRequest("FAIL_OPEN", Date.now() - startTime);
|
|
1863
1855
|
return {
|
|
1864
1856
|
decision: "ALLOW",
|
|
@@ -1890,6 +1882,10 @@ var GateClient = class {
|
|
|
1890
1882
|
}
|
|
1891
1883
|
throw error;
|
|
1892
1884
|
}
|
|
1885
|
+
if (error instanceof GateError && error.code === "RATE_LIMITED" /* RATE_LIMITED */) {
|
|
1886
|
+
console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: 429)");
|
|
1887
|
+
throw error;
|
|
1888
|
+
}
|
|
1893
1889
|
if (error instanceof BlockIntelBlockedError || error instanceof BlockIntelStepUpRequiredError) {
|
|
1894
1890
|
throw error;
|
|
1895
1891
|
}
|
|
@@ -1902,6 +1898,7 @@ var GateClient = class {
|
|
|
1902
1898
|
*/
|
|
1903
1899
|
handleFailSafe(mode, error, requestId) {
|
|
1904
1900
|
if (mode === "ALLOW_ON_TIMEOUT") {
|
|
1901
|
+
console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_safe_allow)");
|
|
1905
1902
|
return {
|
|
1906
1903
|
decision: "ALLOW",
|
|
1907
1904
|
reasonCodes: ["FAIL_SAFE_ALLOW"],
|
|
@@ -1912,6 +1909,7 @@ var GateClient = class {
|
|
|
1912
1909
|
return null;
|
|
1913
1910
|
}
|
|
1914
1911
|
if (mode === "BLOCK_ON_ANOMALY") {
|
|
1912
|
+
console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_safe_allow)");
|
|
1915
1913
|
return {
|
|
1916
1914
|
decision: "ALLOW",
|
|
1917
1915
|
reasonCodes: ["FAIL_SAFE_ALLOW"],
|