blockintel-gate-sdk 0.1.0

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 ADDED
@@ -0,0 +1,1193 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var uuid = require('uuid');
6
+ var clientKms = require('@aws-sdk/client-kms');
7
+ var crypto$1 = require('crypto');
8
+
9
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
10
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
11
+ }) : x)(function(x) {
12
+ if (typeof require !== "undefined") return require.apply(this, arguments);
13
+ throw Error('Dynamic require of "' + x + '" is not supported');
14
+ });
15
+
16
+ // src/utils/crypto.ts
17
+ async function hmacSha256(secret, message) {
18
+ if (typeof crypto !== "undefined" && crypto.subtle) {
19
+ const encoder = new TextEncoder();
20
+ const keyData = encoder.encode(secret);
21
+ const messageData = encoder.encode(message);
22
+ const key = await crypto.subtle.importKey(
23
+ "raw",
24
+ keyData,
25
+ { name: "HMAC", hash: "SHA-256" },
26
+ false,
27
+ ["sign"]
28
+ );
29
+ const signature = await crypto.subtle.sign("HMAC", key, messageData);
30
+ const hashArray = Array.from(new Uint8Array(signature));
31
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
32
+ }
33
+ if (typeof __require !== "undefined") {
34
+ const crypto2 = __require("crypto");
35
+ const hmac = crypto2.createHmac("sha256", secret);
36
+ hmac.update(message, "utf8");
37
+ return hmac.digest("hex");
38
+ }
39
+ throw new Error("HMAC-SHA256 not available in this environment");
40
+ }
41
+
42
+ // src/utils/canonicalJson.ts
43
+ function canonicalizeJson(obj) {
44
+ if (obj === null || obj === void 0) {
45
+ return "null";
46
+ }
47
+ if (typeof obj === "string") {
48
+ return JSON.stringify(obj);
49
+ }
50
+ if (typeof obj === "number" || typeof obj === "boolean") {
51
+ return String(obj);
52
+ }
53
+ if (Array.isArray(obj)) {
54
+ const items = obj.map((item) => canonicalizeJson(item));
55
+ return `[${items.join(",")}]`;
56
+ }
57
+ if (typeof obj === "object") {
58
+ const keys = Object.keys(obj).sort();
59
+ const pairs = keys.map((key) => {
60
+ const value = obj[key];
61
+ const canonicalValue = canonicalizeJson(value);
62
+ return `${JSON.stringify(key)}:${canonicalValue}`;
63
+ });
64
+ return `{${pairs.join(",")}}`;
65
+ }
66
+ return JSON.stringify(obj);
67
+ }
68
+ async function sha256Hex(input) {
69
+ if (typeof crypto !== "undefined" && crypto.subtle) {
70
+ const encoder = new TextEncoder();
71
+ const data = encoder.encode(input);
72
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
73
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
74
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
75
+ }
76
+ if (typeof __require !== "undefined") {
77
+ const crypto2 = __require("crypto");
78
+ return crypto2.createHash("sha256").update(input, "utf8").digest("hex");
79
+ }
80
+ throw new Error("SHA-256 not available in this environment");
81
+ }
82
+
83
+ // src/auth/HmacSigner.ts
84
+ var HmacSigner = class {
85
+ keyId;
86
+ secret;
87
+ constructor(config) {
88
+ this.keyId = config.keyId;
89
+ this.secret = config.secret;
90
+ if (!this.secret || this.secret.length === 0) {
91
+ throw new Error("HMAC secret cannot be empty");
92
+ }
93
+ }
94
+ /**
95
+ * Sign a request and return headers
96
+ */
97
+ async signRequest(params) {
98
+ const { method, path, tenantId, timestampMs, requestId, body } = params;
99
+ const bodyJson = body ? canonicalizeJson(body) : "";
100
+ const bodyHash = await sha256Hex(bodyJson);
101
+ const signingString = [
102
+ "v1",
103
+ method.toUpperCase(),
104
+ path,
105
+ tenantId,
106
+ this.keyId,
107
+ String(timestampMs),
108
+ requestId,
109
+ // Used as nonce in canonical string
110
+ bodyHash
111
+ ].join("\n");
112
+ const signature = await hmacSha256(this.secret, signingString);
113
+ return {
114
+ "X-GATE-TENANT-ID": tenantId,
115
+ "X-GATE-KEY-ID": this.keyId,
116
+ "X-GATE-TIMESTAMP-MS": String(timestampMs),
117
+ "X-GATE-REQUEST-ID": requestId,
118
+ "X-GATE-SIGNATURE": signature
119
+ };
120
+ }
121
+ };
122
+
123
+ // src/auth/ApiKeyAuth.ts
124
+ var ApiKeyAuth = class {
125
+ apiKey;
126
+ constructor(config) {
127
+ this.apiKey = config.apiKey;
128
+ if (!this.apiKey || this.apiKey.length === 0) {
129
+ throw new Error("API key cannot be empty");
130
+ }
131
+ }
132
+ /**
133
+ * Create headers for API key authentication
134
+ */
135
+ createHeaders(params) {
136
+ const { tenantId, timestampMs, requestId } = params;
137
+ return {
138
+ "X-API-KEY": this.apiKey,
139
+ "X-GATE-TENANT-ID": tenantId,
140
+ "X-GATE-REQUEST-ID": requestId,
141
+ "X-GATE-TIMESTAMP-MS": String(timestampMs)
142
+ };
143
+ }
144
+ };
145
+
146
+ // src/types/errors.ts
147
+ var GateErrorCode = /* @__PURE__ */ ((GateErrorCode2) => {
148
+ GateErrorCode2["NETWORK_ERROR"] = "NETWORK_ERROR";
149
+ GateErrorCode2["TIMEOUT"] = "TIMEOUT";
150
+ GateErrorCode2["NOT_FOUND"] = "NOT_FOUND";
151
+ GateErrorCode2["UNAUTHORIZED"] = "UNAUTHORIZED";
152
+ GateErrorCode2["FORBIDDEN"] = "FORBIDDEN";
153
+ GateErrorCode2["RATE_LIMITED"] = "RATE_LIMITED";
154
+ GateErrorCode2["SERVER_ERROR"] = "SERVER_ERROR";
155
+ GateErrorCode2["INVALID_RESPONSE"] = "INVALID_RESPONSE";
156
+ GateErrorCode2["STEP_UP_NOT_CONFIGURED"] = "STEP_UP_NOT_CONFIGURED";
157
+ GateErrorCode2["STEP_UP_TIMEOUT"] = "STEP_UP_TIMEOUT";
158
+ GateErrorCode2["BLOCKED"] = "BLOCKED";
159
+ GateErrorCode2["SERVICE_UNAVAILABLE"] = "SERVICE_UNAVAILABLE";
160
+ GateErrorCode2["AUTH_ERROR"] = "AUTH_ERROR";
161
+ return GateErrorCode2;
162
+ })(GateErrorCode || {});
163
+ var GateError = class extends Error {
164
+ code;
165
+ status;
166
+ details;
167
+ requestId;
168
+ correlationId;
169
+ constructor(code, message, options) {
170
+ super(message);
171
+ this.name = "GateError";
172
+ this.code = code;
173
+ this.status = options?.status;
174
+ this.details = options?.details;
175
+ this.requestId = options?.requestId;
176
+ this.correlationId = options?.correlationId;
177
+ if (options?.cause) {
178
+ this.cause = options.cause;
179
+ }
180
+ Error.captureStackTrace(this, this.constructor);
181
+ }
182
+ toJSON() {
183
+ return {
184
+ name: this.name,
185
+ code: this.code,
186
+ message: this.message,
187
+ status: this.status,
188
+ details: this.details,
189
+ requestId: this.requestId,
190
+ correlationId: this.correlationId
191
+ };
192
+ }
193
+ };
194
+ var StepUpNotConfiguredError = class extends GateError {
195
+ constructor(requestId) {
196
+ super(
197
+ "STEP_UP_NOT_CONFIGURED" /* STEP_UP_NOT_CONFIGURED */,
198
+ "Step-up is required but not configured in SDK. Enable step-up in client config or treat REQUIRE_STEP_UP as BLOCK.",
199
+ { requestId }
200
+ );
201
+ this.name = "StepUpNotConfiguredError";
202
+ }
203
+ };
204
+ var BlockIntelBlockedError = class extends GateError {
205
+ receiptId;
206
+ reasonCode;
207
+ constructor(reasonCode, receiptId, correlationId, requestId) {
208
+ super(
209
+ "BLOCKED" /* BLOCKED */,
210
+ `Transaction blocked: ${reasonCode}`,
211
+ { correlationId, requestId, details: { reasonCode, receiptId } }
212
+ );
213
+ this.name = "BlockIntelBlockedError";
214
+ this.receiptId = receiptId;
215
+ this.reasonCode = reasonCode;
216
+ }
217
+ };
218
+ var BlockIntelUnavailableError = class extends GateError {
219
+ constructor(message, requestId) {
220
+ super("SERVICE_UNAVAILABLE" /* SERVICE_UNAVAILABLE */, message, { requestId });
221
+ this.name = "BlockIntelUnavailableError";
222
+ }
223
+ };
224
+ var BlockIntelAuthError = class extends GateError {
225
+ constructor(message, status, requestId) {
226
+ super(
227
+ status === 401 ? "UNAUTHORIZED" /* UNAUTHORIZED */ : "FORBIDDEN" /* FORBIDDEN */,
228
+ message,
229
+ { status, requestId }
230
+ );
231
+ this.name = "BlockIntelAuthError";
232
+ }
233
+ };
234
+ var BlockIntelStepUpRequiredError = class extends GateError {
235
+ stepUpRequestId;
236
+ statusUrl;
237
+ expiresAtMs;
238
+ constructor(stepUpRequestId, statusUrl, expiresAtMs, requestId) {
239
+ super(
240
+ "STEP_UP_NOT_CONFIGURED" /* STEP_UP_NOT_CONFIGURED */,
241
+ "Step-up approval required",
242
+ {
243
+ requestId,
244
+ details: { stepUpRequestId, statusUrl, expiresAtMs }
245
+ }
246
+ );
247
+ this.name = "BlockIntelStepUpRequiredError";
248
+ this.stepUpRequestId = stepUpRequestId;
249
+ this.statusUrl = statusUrl;
250
+ this.expiresAtMs = expiresAtMs;
251
+ }
252
+ };
253
+
254
+ // src/http/retry.ts
255
+ var DEFAULT_RETRY_OPTIONS = {
256
+ maxAttempts: 3,
257
+ baseDelayMs: 100,
258
+ maxDelayMs: 800,
259
+ factor: 2
260
+ };
261
+ function isRetryableStatus(status) {
262
+ return status === 429 || status >= 500 && status < 600;
263
+ }
264
+ function isRetryableError(error) {
265
+ if (error instanceof Error) {
266
+ const message = error.message.toLowerCase();
267
+ return message.includes("network") || message.includes("timeout") || message.includes("connection") || message.includes("econnrefused") || message.includes("enotfound") || message.includes("econnreset");
268
+ }
269
+ return false;
270
+ }
271
+ function calculateBackoffDelay(attempt, options) {
272
+ const exponentialDelay = options.baseDelayMs * Math.pow(options.factor, attempt - 1);
273
+ const jitter = Math.random() * 0.3 * exponentialDelay;
274
+ const delay = exponentialDelay + jitter;
275
+ return Math.min(delay, options.maxDelayMs);
276
+ }
277
+ function isRetryableGateError(error) {
278
+ if (error && typeof error === "object" && "code" in error) {
279
+ const gateError = error;
280
+ if (gateError.code === "SERVER_ERROR" || gateError.code === "RATE_LIMITED") {
281
+ return true;
282
+ }
283
+ if (gateError.status && isRetryableStatus(gateError.status)) {
284
+ return true;
285
+ }
286
+ }
287
+ return false;
288
+ }
289
+ async function retryWithBackoff(fn, options = {}) {
290
+ const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
291
+ let lastError;
292
+ for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
293
+ try {
294
+ return await fn();
295
+ } catch (error) {
296
+ lastError = error;
297
+ if (attempt >= opts.maxAttempts) {
298
+ break;
299
+ }
300
+ if (error instanceof Response && !isRetryableStatus(error.status)) {
301
+ throw error;
302
+ }
303
+ const isRetryable = error instanceof Response && isRetryableStatus(error.status) || isRetryableError(error) || isRetryableGateError(error);
304
+ if (!isRetryable) {
305
+ throw error;
306
+ }
307
+ const delay = calculateBackoffDelay(attempt, opts);
308
+ await new Promise((resolve) => setTimeout(resolve, delay));
309
+ }
310
+ }
311
+ throw lastError;
312
+ }
313
+
314
+ // src/http/HttpClient.ts
315
+ var HttpClient = class {
316
+ baseUrl;
317
+ timeoutMs;
318
+ userAgent;
319
+ retryOptions;
320
+ constructor(config) {
321
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
322
+ this.timeoutMs = config.timeoutMs ?? 15e3;
323
+ this.userAgent = config.userAgent ?? "blockintel-gate-sdk/0.1.0";
324
+ this.retryOptions = config.retryOptions;
325
+ if (!this.baseUrl) {
326
+ throw new Error("baseUrl is required");
327
+ }
328
+ if (typeof process !== "undefined" && process.env.NODE_ENV === "production") {
329
+ if (!this.baseUrl.startsWith("https://") && !this.baseUrl.includes("localhost")) {
330
+ throw new Error("baseUrl must use HTTPS in production (except localhost)");
331
+ }
332
+ }
333
+ }
334
+ /**
335
+ * Make an HTTP request with retry and timeout
336
+ */
337
+ async request(options) {
338
+ const { method, path, headers = {}, body, requestId } = options;
339
+ const url = `${this.baseUrl}${path}`;
340
+ const controller = new AbortController();
341
+ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
342
+ try {
343
+ const response = await retryWithBackoff(
344
+ async () => {
345
+ const fetchOptions = {
346
+ method,
347
+ headers: {
348
+ ...headers,
349
+ "User-Agent": this.userAgent,
350
+ "Content-Type": "application/json"
351
+ },
352
+ signal: controller.signal
353
+ };
354
+ if (body) {
355
+ fetchOptions.body = JSON.stringify(body);
356
+ }
357
+ const res = await fetch(url, fetchOptions);
358
+ if (!res.ok && isRetryableStatus(res.status)) {
359
+ throw res;
360
+ }
361
+ if (!res.ok && !isRetryableStatus(res.status)) {
362
+ throw res;
363
+ }
364
+ return res;
365
+ },
366
+ {
367
+ ...this.retryOptions
368
+ // Custom retry logic that handles Response objects
369
+ }
370
+ );
371
+ clearTimeout(timeoutId);
372
+ let data;
373
+ const contentType = response.headers.get("content-type");
374
+ if (contentType && contentType.includes("application/json")) {
375
+ try {
376
+ data = await response.json();
377
+ } catch (parseError) {
378
+ throw new GateError(
379
+ "INVALID_RESPONSE" /* INVALID_RESPONSE */,
380
+ "Failed to parse JSON response",
381
+ {
382
+ status: response.status,
383
+ requestId,
384
+ cause: parseError instanceof Error ? parseError : void 0
385
+ }
386
+ );
387
+ }
388
+ } else {
389
+ const text = await response.text();
390
+ throw new GateError(
391
+ "INVALID_RESPONSE" /* INVALID_RESPONSE */,
392
+ `Unexpected content type: ${contentType}`,
393
+ {
394
+ status: response.status,
395
+ details: { body: text.substring(0, 200) },
396
+ requestId
397
+ }
398
+ );
399
+ }
400
+ if (!response.ok) {
401
+ const errorCode = this.statusToErrorCode(response.status);
402
+ const correlationId = response.headers.get("X-Correlation-ID") ?? void 0;
403
+ throw new GateError(errorCode, `HTTP ${response.status}: ${response.statusText}`, {
404
+ status: response.status,
405
+ correlationId,
406
+ requestId,
407
+ details: data
408
+ });
409
+ }
410
+ return data;
411
+ } catch (error) {
412
+ clearTimeout(timeoutId);
413
+ if (error instanceof Error && error.name === "AbortError") {
414
+ throw new GateError("TIMEOUT" /* TIMEOUT */, `Request timeout after ${this.timeoutMs}ms`, {
415
+ requestId
416
+ });
417
+ }
418
+ if (error instanceof Response) {
419
+ const errorCode = this.statusToErrorCode(error.status);
420
+ const correlationId = error.headers.get("X-Correlation-ID") ?? void 0;
421
+ let details;
422
+ try {
423
+ const text = await error.text();
424
+ try {
425
+ details = JSON.parse(text);
426
+ } catch {
427
+ details = { body: text.substring(0, 200) };
428
+ }
429
+ } catch {
430
+ }
431
+ throw new GateError(errorCode, `HTTP ${error.status}: ${error.statusText}`, {
432
+ status: error.status,
433
+ correlationId,
434
+ requestId,
435
+ details
436
+ });
437
+ }
438
+ if (isRetryableError(error)) {
439
+ throw new GateError(
440
+ "NETWORK_ERROR" /* NETWORK_ERROR */,
441
+ `Network error: ${error instanceof Error ? error.message : String(error)}`,
442
+ {
443
+ requestId,
444
+ cause: error instanceof Error ? error : void 0
445
+ }
446
+ );
447
+ }
448
+ if (error instanceof GateError) {
449
+ throw error;
450
+ }
451
+ throw new GateError(
452
+ "NETWORK_ERROR" /* NETWORK_ERROR */,
453
+ `Unexpected error: ${error instanceof Error ? error.message : String(error)}`,
454
+ {
455
+ requestId,
456
+ cause: error instanceof Error ? error : void 0
457
+ }
458
+ );
459
+ }
460
+ }
461
+ /**
462
+ * Map HTTP status code to GateErrorCode
463
+ */
464
+ statusToErrorCode(status) {
465
+ if (status === 401) return "UNAUTHORIZED" /* UNAUTHORIZED */;
466
+ if (status === 403) return "FORBIDDEN" /* FORBIDDEN */;
467
+ if (status === 404) return "NOT_FOUND" /* NOT_FOUND */;
468
+ if (status === 429) return "RATE_LIMITED" /* RATE_LIMITED */;
469
+ if (status >= 500 && status < 600) return "SERVER_ERROR" /* SERVER_ERROR */;
470
+ return "NETWORK_ERROR" /* NETWORK_ERROR */;
471
+ }
472
+ };
473
+
474
+ // src/utils/time.ts
475
+ function nowMs() {
476
+ return Date.now();
477
+ }
478
+ function nowEpochSeconds() {
479
+ return Math.floor(Date.now() / 1e3);
480
+ }
481
+ function clamp(value, min, max) {
482
+ return Math.max(min, Math.min(max, value));
483
+ }
484
+ function sleep(ms) {
485
+ return new Promise((resolve) => setTimeout(resolve, ms));
486
+ }
487
+
488
+ // src/stepup/stepup.ts
489
+ var DEFAULT_POLLING_INTERVAL_MS = 250;
490
+ var DEFAULT_MAX_WAIT_MS = 15e3;
491
+ var DEFAULT_TTL_MIN_SECONDS = 300;
492
+ var DEFAULT_TTL_MAX_SECONDS = 900;
493
+ var DEFAULT_TTL_DEFAULT_SECONDS = 600;
494
+ var StepUpPoller = class {
495
+ httpClient;
496
+ tenantId;
497
+ pollingIntervalMs;
498
+ maxWaitMs;
499
+ ttlMinSeconds;
500
+ ttlMaxSeconds;
501
+ ttlDefaultSeconds;
502
+ constructor(config) {
503
+ this.httpClient = config.httpClient;
504
+ this.tenantId = config.tenantId;
505
+ this.pollingIntervalMs = config.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS;
506
+ this.maxWaitMs = config.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
507
+ this.ttlMinSeconds = config.ttlMinSeconds ?? DEFAULT_TTL_MIN_SECONDS;
508
+ this.ttlMaxSeconds = config.ttlMaxSeconds ?? DEFAULT_TTL_MAX_SECONDS;
509
+ this.ttlDefaultSeconds = config.ttlDefaultSeconds ?? DEFAULT_TTL_DEFAULT_SECONDS;
510
+ }
511
+ /**
512
+ * Get current step-up status
513
+ */
514
+ async getStatus(requestId) {
515
+ const path = `/defense/stepup/status?tenantId=${encodeURIComponent(this.tenantId)}&requestId=${encodeURIComponent(requestId)}`;
516
+ try {
517
+ const apiResponse = await this.httpClient.request({
518
+ method: "GET",
519
+ path,
520
+ requestId
521
+ });
522
+ const response = {
523
+ status: apiResponse.status,
524
+ tenantId: apiResponse.tenant_id ?? apiResponse.tenantId,
525
+ requestId: apiResponse.request_id ?? apiResponse.requestId,
526
+ decision: apiResponse.decision,
527
+ reasonCodes: apiResponse.reason_codes ?? apiResponse.reasonCodes,
528
+ correlationId: apiResponse.correlation_id ?? apiResponse.correlationId,
529
+ expiresAtMs: apiResponse.expires_at_ms ?? apiResponse.expiresAtMs,
530
+ ttl: apiResponse.ttl
531
+ };
532
+ const now = nowEpochSeconds();
533
+ if (response.ttl !== void 0 && response.ttl <= now) {
534
+ return {
535
+ ...response,
536
+ status: "EXPIRED"
537
+ };
538
+ }
539
+ return response;
540
+ } catch (error) {
541
+ if (error instanceof GateError && error.code === "NOT_FOUND" /* NOT_FOUND */) {
542
+ throw new GateError(
543
+ "NOT_FOUND" /* NOT_FOUND */,
544
+ `Step-up request not found: ${requestId}`,
545
+ { requestId }
546
+ );
547
+ }
548
+ throw error;
549
+ }
550
+ }
551
+ /**
552
+ * Wait for step-up decision with polling
553
+ *
554
+ * Polls until status is APPROVED, DENIED, or EXPIRED, or timeout is reached.
555
+ */
556
+ async awaitDecision(requestId, options) {
557
+ const startTime = Date.now();
558
+ const maxWaitMs = options?.maxWaitMs ?? this.maxWaitMs;
559
+ const intervalMs = options?.intervalMs ?? this.pollingIntervalMs;
560
+ while (true) {
561
+ const elapsedMs = Date.now() - startTime;
562
+ if (elapsedMs >= maxWaitMs) {
563
+ throw new GateError(
564
+ "STEP_UP_TIMEOUT" /* STEP_UP_TIMEOUT */,
565
+ `Step-up decision timeout after ${maxWaitMs}ms`,
566
+ { requestId }
567
+ );
568
+ }
569
+ try {
570
+ const status = await this.getStatus(requestId);
571
+ const now = nowEpochSeconds();
572
+ if (status.ttl !== void 0 && status.ttl <= now) {
573
+ return {
574
+ status: "EXPIRED",
575
+ requestId,
576
+ elapsedMs,
577
+ correlationId: status.correlationId
578
+ };
579
+ }
580
+ if (status.status === "APPROVED" || status.status === "DENIED" || status.status === "EXPIRED") {
581
+ return {
582
+ status: status.status,
583
+ requestId,
584
+ elapsedMs,
585
+ decision: status.decision,
586
+ reasonCodes: status.reasonCodes,
587
+ correlationId: status.correlationId
588
+ };
589
+ }
590
+ await sleep(intervalMs);
591
+ } catch (error) {
592
+ if (error instanceof GateError && error.code === "NOT_FOUND" /* NOT_FOUND */) {
593
+ throw error;
594
+ }
595
+ const remainingMs = maxWaitMs - (Date.now() - startTime);
596
+ if (remainingMs <= 0) {
597
+ throw new GateError(
598
+ "STEP_UP_TIMEOUT" /* STEP_UP_TIMEOUT */,
599
+ `Step-up decision timeout after ${maxWaitMs}ms`,
600
+ { requestId, cause: error instanceof Error ? error : void 0 }
601
+ );
602
+ }
603
+ await sleep(Math.min(intervalMs, remainingMs));
604
+ }
605
+ }
606
+ }
607
+ /**
608
+ * Clamp TTL to guardrails
609
+ */
610
+ clampTtl(ttlSeconds) {
611
+ if (ttlSeconds === void 0) {
612
+ return this.ttlDefaultSeconds;
613
+ }
614
+ return clamp(ttlSeconds, this.ttlMinSeconds, this.ttlMaxSeconds);
615
+ }
616
+ };
617
+
618
+ // src/circuit/CircuitBreaker.ts
619
+ var CircuitBreaker = class {
620
+ state = "CLOSED";
621
+ failures = 0;
622
+ successes = 0;
623
+ lastFailureTime;
624
+ lastSuccessTime;
625
+ tripsToOpen = 0;
626
+ tripThreshold;
627
+ coolDownMs;
628
+ constructor(config = {}) {
629
+ this.tripThreshold = config.tripAfterConsecutiveFailures ?? 5;
630
+ this.coolDownMs = config.coolDownMs ?? 3e4;
631
+ }
632
+ /**
633
+ * Execute function with circuit breaker protection
634
+ */
635
+ async execute(fn) {
636
+ if (this.state === "OPEN") {
637
+ const now = Date.now();
638
+ const timeSinceLastFailure = this.lastFailureTime ? now - this.lastFailureTime : Infinity;
639
+ if (timeSinceLastFailure >= this.coolDownMs) {
640
+ this.state = "HALF_OPEN";
641
+ this.failures = 0;
642
+ } else {
643
+ throw new CircuitBreakerOpenError(
644
+ `Circuit breaker is OPEN. Will retry after ${this.coolDownMs - timeSinceLastFailure}ms`
645
+ );
646
+ }
647
+ }
648
+ try {
649
+ const result = await fn();
650
+ this.onSuccess();
651
+ return result;
652
+ } catch (error) {
653
+ this.onFailure();
654
+ throw error;
655
+ }
656
+ }
657
+ onSuccess() {
658
+ this.successes++;
659
+ this.lastSuccessTime = Date.now();
660
+ if (this.state === "HALF_OPEN") {
661
+ this.state = "CLOSED";
662
+ this.failures = 0;
663
+ } else if (this.state === "CLOSED") {
664
+ this.failures = 0;
665
+ }
666
+ }
667
+ onFailure() {
668
+ this.failures++;
669
+ this.lastFailureTime = Date.now();
670
+ if (this.state === "HALF_OPEN") {
671
+ this.state = "OPEN";
672
+ this.tripsToOpen++;
673
+ } else if (this.state === "CLOSED" && this.failures >= this.tripThreshold) {
674
+ this.state = "OPEN";
675
+ this.tripsToOpen++;
676
+ }
677
+ }
678
+ /**
679
+ * Get current metrics
680
+ */
681
+ getMetrics() {
682
+ return {
683
+ failures: this.failures,
684
+ successes: this.successes,
685
+ state: this.state,
686
+ lastFailureTime: this.lastFailureTime,
687
+ lastSuccessTime: this.lastSuccessTime,
688
+ tripsToOpen: this.tripsToOpen
689
+ };
690
+ }
691
+ /**
692
+ * Reset circuit breaker to CLOSED state
693
+ */
694
+ reset() {
695
+ this.state = "CLOSED";
696
+ this.failures = 0;
697
+ this.successes = 0;
698
+ this.lastFailureTime = void 0;
699
+ this.lastSuccessTime = void 0;
700
+ this.tripsToOpen = 0;
701
+ }
702
+ };
703
+ var CircuitBreakerOpenError = class extends Error {
704
+ constructor(message) {
705
+ super(message);
706
+ this.name = "CircuitBreakerOpenError";
707
+ }
708
+ };
709
+
710
+ // src/metrics/MetricsCollector.ts
711
+ var MetricsCollector = class {
712
+ requestsTotal = 0;
713
+ allowedTotal = 0;
714
+ blockedTotal = 0;
715
+ stepupTotal = 0;
716
+ timeoutsTotal = 0;
717
+ errorsTotal = 0;
718
+ circuitBreakerOpenTotal = 0;
719
+ latencyMs = [];
720
+ maxSamples = 1e3;
721
+ // Keep last 1000 samples
722
+ hooks = [];
723
+ /**
724
+ * Record a request
725
+ */
726
+ recordRequest(decision, latencyMs) {
727
+ this.requestsTotal++;
728
+ if (decision === "ALLOW") {
729
+ this.allowedTotal++;
730
+ } else if (decision === "BLOCK") {
731
+ this.blockedTotal++;
732
+ } else if (decision === "REQUIRE_STEP_UP") {
733
+ this.stepupTotal++;
734
+ }
735
+ this.latencyMs.push(latencyMs);
736
+ if (this.latencyMs.length > this.maxSamples) {
737
+ this.latencyMs.shift();
738
+ }
739
+ this.emitMetrics();
740
+ }
741
+ /**
742
+ * Record a timeout
743
+ */
744
+ recordTimeout() {
745
+ this.timeoutsTotal++;
746
+ this.errorsTotal++;
747
+ this.emitMetrics();
748
+ }
749
+ /**
750
+ * Record an error
751
+ */
752
+ recordError() {
753
+ this.errorsTotal++;
754
+ this.emitMetrics();
755
+ }
756
+ /**
757
+ * Record circuit breaker open
758
+ */
759
+ recordCircuitBreakerOpen() {
760
+ this.circuitBreakerOpenTotal++;
761
+ this.emitMetrics();
762
+ }
763
+ /**
764
+ * Get current metrics snapshot
765
+ */
766
+ getMetrics() {
767
+ return {
768
+ requestsTotal: this.requestsTotal,
769
+ allowedTotal: this.allowedTotal,
770
+ blockedTotal: this.blockedTotal,
771
+ stepupTotal: this.stepupTotal,
772
+ timeoutsTotal: this.timeoutsTotal,
773
+ errorsTotal: this.errorsTotal,
774
+ circuitBreakerOpenTotal: this.circuitBreakerOpenTotal,
775
+ latencyMs: [...this.latencyMs]
776
+ // Copy array
777
+ };
778
+ }
779
+ /**
780
+ * Register a metrics hook (e.g., for Prometheus/OpenTelemetry export)
781
+ */
782
+ registerHook(hook) {
783
+ this.hooks.push(hook);
784
+ }
785
+ /**
786
+ * Emit metrics to all registered hooks
787
+ */
788
+ emitMetrics() {
789
+ const metrics = this.getMetrics();
790
+ for (const hook of this.hooks) {
791
+ try {
792
+ hook(metrics);
793
+ } catch (error) {
794
+ console.error("Error in metrics hook:", error);
795
+ }
796
+ }
797
+ }
798
+ /**
799
+ * Reset all metrics
800
+ */
801
+ reset() {
802
+ this.requestsTotal = 0;
803
+ this.allowedTotal = 0;
804
+ this.blockedTotal = 0;
805
+ this.stepupTotal = 0;
806
+ this.timeoutsTotal = 0;
807
+ this.errorsTotal = 0;
808
+ this.circuitBreakerOpenTotal = 0;
809
+ this.latencyMs = [];
810
+ }
811
+ };
812
+ function wrapKmsClient(kmsClient, gateClient, options = {}) {
813
+ const defaultOptions = {
814
+ mode: options.mode || "enforce",
815
+ onDecision: options.onDecision || (() => {
816
+ }),
817
+ extractTxIntent: options.extractTxIntent || defaultExtractTxIntent
818
+ };
819
+ const wrapped = new Proxy(kmsClient, {
820
+ get(target, prop, receiver) {
821
+ if (prop === "send") {
822
+ return async function(command) {
823
+ if (command && command.constructor && command.constructor.name === "SignCommand") {
824
+ return await handleSignCommand(
825
+ command,
826
+ target,
827
+ gateClient,
828
+ defaultOptions
829
+ );
830
+ }
831
+ return await target.send(command);
832
+ };
833
+ }
834
+ return Reflect.get(target, prop, receiver);
835
+ }
836
+ });
837
+ wrapped._originalClient = kmsClient;
838
+ wrapped._gateClient = gateClient;
839
+ wrapped._wrapperOptions = defaultOptions;
840
+ return wrapped;
841
+ }
842
+ function defaultExtractTxIntent(command) {
843
+ const message = command.input?.Message ?? command.Message;
844
+ if (!message) {
845
+ throw new Error("SignCommand missing required Message property");
846
+ }
847
+ const messageBuffer = message instanceof Buffer ? message : Buffer.from(message);
848
+ const messageHash = crypto$1.createHash("sha256").update(messageBuffer).digest("hex");
849
+ return {
850
+ networkFamily: "OTHER",
851
+ toAddress: void 0,
852
+ // Unknown from KMS message alone
853
+ payloadHash: messageHash,
854
+ dataHash: messageHash
855
+ // Backward compatibility
856
+ };
857
+ }
858
+ async function handleSignCommand(command, originalClient, gateClient, options) {
859
+ const txIntent = options.extractTxIntent(command);
860
+ const signerId = command.input?.KeyId ?? command.KeyId ?? "unknown";
861
+ const signingContext = {
862
+ signerId,
863
+ actorPrincipal: "kms-signer"
864
+ // Default - can be customized via extractTxIntent
865
+ };
866
+ try {
867
+ const decision = await gateClient.evaluate({
868
+ txIntent,
869
+ // Type assertion - txIntent may have extra fields
870
+ signingContext
871
+ });
872
+ options.onDecision("ALLOW", { decision, signerId, command });
873
+ if (options.mode === "dry-run") {
874
+ return await originalClient.send(new clientKms.SignCommand(command));
875
+ }
876
+ return await originalClient.send(new clientKms.SignCommand(command));
877
+ } catch (error) {
878
+ if (error instanceof BlockIntelBlockedError) {
879
+ options.onDecision("BLOCK", { error, signerId, command });
880
+ throw error;
881
+ }
882
+ if (error instanceof BlockIntelStepUpRequiredError) {
883
+ options.onDecision("REQUIRE_STEP_UP", { error, signerId, command });
884
+ throw error;
885
+ }
886
+ throw error;
887
+ }
888
+ }
889
+
890
+ // src/client/GateClient.ts
891
+ var GateClient = class {
892
+ config;
893
+ httpClient;
894
+ hmacSigner;
895
+ apiKeyAuth;
896
+ stepUpPoller;
897
+ circuitBreaker;
898
+ metrics;
899
+ constructor(config) {
900
+ this.config = config;
901
+ if (config.auth.mode === "hmac") {
902
+ this.hmacSigner = new HmacSigner({
903
+ keyId: config.auth.keyId,
904
+ secret: config.auth.secret
905
+ });
906
+ } else {
907
+ this.apiKeyAuth = new ApiKeyAuth({
908
+ apiKey: config.auth.apiKey
909
+ });
910
+ }
911
+ this.httpClient = new HttpClient({
912
+ baseUrl: config.baseUrl,
913
+ timeoutMs: config.timeoutMs,
914
+ userAgent: config.userAgent
915
+ });
916
+ if (config.enableStepUp) {
917
+ this.stepUpPoller = new StepUpPoller({
918
+ httpClient: this.httpClient,
919
+ tenantId: config.tenantId,
920
+ pollingIntervalMs: config.stepUp?.pollingIntervalMs,
921
+ maxWaitMs: config.stepUp?.maxWaitMs
922
+ });
923
+ }
924
+ if (config.circuitBreaker) {
925
+ this.circuitBreaker = new CircuitBreaker(config.circuitBreaker);
926
+ }
927
+ this.metrics = new MetricsCollector();
928
+ if (config.onMetrics) {
929
+ this.metrics.registerHook(config.onMetrics);
930
+ }
931
+ }
932
+ /**
933
+ * Evaluate a transaction defense request
934
+ *
935
+ * Implements:
936
+ * - Circuit breaker protection
937
+ * - Fail-safe modes (ALLOW_ON_TIMEOUT, BLOCK_ON_TIMEOUT, BLOCK_ON_ANOMALY)
938
+ * - Metrics collection
939
+ * - Error handling (BLOCK → BlockIntelBlockedError, REQUIRE_STEP_UP → BlockIntelStepUpRequiredError)
940
+ */
941
+ async evaluate(req, opts) {
942
+ const requestId = opts?.requestId ?? uuid.v4();
943
+ const timestampMs = req.timestampMs ?? nowMs();
944
+ const startTime = Date.now();
945
+ const failSafeMode = this.config.failSafeMode ?? "ALLOW_ON_TIMEOUT";
946
+ const executeRequest = async () => {
947
+ const txIntent = { ...req.txIntent };
948
+ if (txIntent.to && !txIntent.toAddress) {
949
+ txIntent.toAddress = txIntent.to;
950
+ delete txIntent.to;
951
+ }
952
+ if (!txIntent.networkFamily && txIntent.chainId) {
953
+ txIntent.networkFamily = "EVM";
954
+ }
955
+ if (txIntent.from && !txIntent.fromAddress) {
956
+ delete txIntent.from;
957
+ }
958
+ const signingContext = {
959
+ ...req.signingContext,
960
+ actorPrincipal: req.signingContext?.actorPrincipal || req.signingContext?.signerId || "unknown"
961
+ };
962
+ const body = {
963
+ requestId,
964
+ tenantId: this.config.tenantId,
965
+ timestampMs,
966
+ txIntent,
967
+ signingContext,
968
+ // Add SDK info (required by Hot Path validation)
969
+ sdk: {
970
+ name: "blockintel-gate-sdk",
971
+ version: "0.1.0"
972
+ }
973
+ };
974
+ let headers;
975
+ if (this.hmacSigner) {
976
+ const hmacHeaders = await this.hmacSigner.signRequest({
977
+ method: "POST",
978
+ path: "/defense/evaluate",
979
+ tenantId: this.config.tenantId,
980
+ timestampMs,
981
+ requestId,
982
+ body
983
+ });
984
+ headers = { ...hmacHeaders };
985
+ } else if (this.apiKeyAuth) {
986
+ const apiKeyHeaders = this.apiKeyAuth.createHeaders({
987
+ tenantId: this.config.tenantId,
988
+ timestampMs,
989
+ requestId
990
+ });
991
+ headers = { ...apiKeyHeaders };
992
+ } else {
993
+ throw new Error("No authentication configured");
994
+ }
995
+ const apiResponse = await this.httpClient.request({
996
+ method: "POST",
997
+ path: "/defense/evaluate",
998
+ headers,
999
+ body,
1000
+ requestId
1001
+ });
1002
+ if (!apiResponse.success || !apiResponse.data) {
1003
+ throw new GateError(
1004
+ "INVALID_RESPONSE" /* INVALID_RESPONSE */,
1005
+ "Invalid response format: expected { success: true, data: { ... } }",
1006
+ {
1007
+ requestId,
1008
+ details: apiResponse
1009
+ }
1010
+ );
1011
+ }
1012
+ const responseData = apiResponse.data;
1013
+ const result = {
1014
+ decision: responseData.decision,
1015
+ reasonCodes: responseData.reason_codes ?? responseData.reasonCodes ?? [],
1016
+ policyVersion: responseData.policy_version ?? responseData.policyVersion,
1017
+ correlationId: responseData.correlation_id ?? responseData.correlationId,
1018
+ stepUp: responseData.step_up ? {
1019
+ requestId: responseData.step_up.request_id ?? (responseData.stepUp?.requestId ?? ""),
1020
+ ttlSeconds: responseData.step_up.ttl_seconds ?? responseData.stepUp?.ttlSeconds
1021
+ } : responseData.stepUp
1022
+ };
1023
+ const latencyMs = Date.now() - startTime;
1024
+ if (result.decision === "BLOCK") {
1025
+ const receiptId = responseData.decision_id || requestId;
1026
+ const reasonCode = result.reasonCodes[0] || "POLICY_VIOLATION";
1027
+ this.metrics.recordRequest("BLOCK", latencyMs);
1028
+ throw new BlockIntelBlockedError(reasonCode, receiptId, result.correlationId, requestId);
1029
+ }
1030
+ if (result.decision === "REQUIRE_STEP_UP") {
1031
+ if (this.config.enableStepUp && this.stepUpPoller && result.stepUp) {
1032
+ const stepUpRequestId = result.stepUp.requestId || requestId;
1033
+ const expiresAtMs = responseData.step_up?.expires_at_ms;
1034
+ const statusUrl = `/defense/stepup/status?tenantId=${this.config.tenantId}&requestId=${stepUpRequestId}`;
1035
+ this.metrics.recordRequest("REQUIRE_STEP_UP", latencyMs);
1036
+ throw new BlockIntelStepUpRequiredError(stepUpRequestId, statusUrl, expiresAtMs, requestId);
1037
+ } else {
1038
+ const receiptId = responseData.decision_id || requestId;
1039
+ const reasonCode = "STEPUP_REQUIRED";
1040
+ this.metrics.recordRequest("BLOCK", latencyMs);
1041
+ throw new BlockIntelBlockedError(reasonCode, receiptId, result.correlationId, requestId);
1042
+ }
1043
+ }
1044
+ this.metrics.recordRequest("ALLOW", latencyMs);
1045
+ return result;
1046
+ };
1047
+ try {
1048
+ if (this.circuitBreaker) {
1049
+ return await this.circuitBreaker.execute(executeRequest);
1050
+ }
1051
+ return await executeRequest();
1052
+ } catch (error) {
1053
+ if (error instanceof CircuitBreakerOpenError) {
1054
+ this.metrics.recordCircuitBreakerOpen();
1055
+ const failSafeResult = this.handleFailSafe(failSafeMode, error, requestId);
1056
+ if (failSafeResult) {
1057
+ return failSafeResult;
1058
+ }
1059
+ throw error;
1060
+ }
1061
+ if (error instanceof GateError && (error.code === "UNAUTHORIZED" /* UNAUTHORIZED */ || error.code === "FORBIDDEN" /* FORBIDDEN */)) {
1062
+ this.metrics.recordError();
1063
+ throw new BlockIntelAuthError(
1064
+ error.message,
1065
+ error.status || 401,
1066
+ requestId
1067
+ );
1068
+ }
1069
+ if (error instanceof GateError && error.code === "TIMEOUT" /* TIMEOUT */) {
1070
+ this.metrics.recordTimeout();
1071
+ const failSafeResult = this.handleFailSafe(failSafeMode, error, requestId);
1072
+ if (failSafeResult) {
1073
+ return failSafeResult;
1074
+ }
1075
+ throw new BlockIntelUnavailableError(`Service timeout: ${error.message}`, requestId);
1076
+ }
1077
+ if (error instanceof GateError && error.code === "SERVER_ERROR" /* SERVER_ERROR */) {
1078
+ this.metrics.recordError();
1079
+ const failSafeResult = this.handleFailSafe(failSafeMode, error, requestId);
1080
+ if (failSafeResult) {
1081
+ return failSafeResult;
1082
+ }
1083
+ throw error;
1084
+ }
1085
+ if (error instanceof BlockIntelBlockedError || error instanceof BlockIntelStepUpRequiredError) {
1086
+ throw error;
1087
+ }
1088
+ this.metrics.recordError();
1089
+ throw error;
1090
+ }
1091
+ }
1092
+ /**
1093
+ * Handle fail-safe modes for timeouts/errors
1094
+ */
1095
+ handleFailSafe(mode, error, requestId) {
1096
+ if (mode === "ALLOW_ON_TIMEOUT") {
1097
+ return {
1098
+ decision: "ALLOW",
1099
+ reasonCodes: ["FAIL_SAFE_ALLOW"],
1100
+ correlationId: requestId
1101
+ };
1102
+ }
1103
+ if (mode === "BLOCK_ON_TIMEOUT") {
1104
+ return null;
1105
+ }
1106
+ if (mode === "BLOCK_ON_ANOMALY") {
1107
+ return {
1108
+ decision: "ALLOW",
1109
+ reasonCodes: ["FAIL_SAFE_ALLOW"],
1110
+ correlationId: requestId
1111
+ };
1112
+ }
1113
+ return null;
1114
+ }
1115
+ /**
1116
+ * Get current metrics
1117
+ */
1118
+ getMetrics() {
1119
+ return this.metrics.getMetrics();
1120
+ }
1121
+ /**
1122
+ * Get circuit breaker metrics (if enabled)
1123
+ */
1124
+ getCircuitBreakerMetrics() {
1125
+ return this.circuitBreaker?.getMetrics() || null;
1126
+ }
1127
+ /**
1128
+ * Get step-up status
1129
+ */
1130
+ async getStepUpStatus(args) {
1131
+ if (!this.stepUpPoller) {
1132
+ throw new StepUpNotConfiguredError(args.requestId);
1133
+ }
1134
+ const tenantId = args.tenantId ?? this.config.tenantId;
1135
+ const poller = new StepUpPoller({
1136
+ httpClient: this.httpClient,
1137
+ tenantId,
1138
+ pollingIntervalMs: this.config.stepUp?.pollingIntervalMs,
1139
+ maxWaitMs: this.config.stepUp?.maxWaitMs
1140
+ });
1141
+ return poller.getStatus(args.requestId);
1142
+ }
1143
+ /**
1144
+ * Wait for step-up decision with polling
1145
+ */
1146
+ async awaitStepUpDecision(args) {
1147
+ if (!this.stepUpPoller) {
1148
+ throw new StepUpNotConfiguredError(args.requestId);
1149
+ }
1150
+ return this.stepUpPoller.awaitDecision(args.requestId, {
1151
+ maxWaitMs: args.maxWaitMs ?? this.config.stepUp?.maxWaitMs,
1152
+ intervalMs: args.intervalMs ?? this.config.stepUp?.pollingIntervalMs
1153
+ });
1154
+ }
1155
+ /**
1156
+ * Wrap AWS SDK v3 KMS client to intercept SignCommand calls
1157
+ *
1158
+ * @param kmsClient - AWS SDK v3 KMSClient instance
1159
+ * @param options - Wrapper options
1160
+ * @returns Wrapped KMS client that enforces Gate policies
1161
+ *
1162
+ * @example
1163
+ * ```typescript
1164
+ * import { KMSClient } from '@aws-sdk/client-kms';
1165
+ *
1166
+ * const kms = new KMSClient({});
1167
+ * const protectedKms = gateClient.wrapKmsClient(kms);
1168
+ *
1169
+ * // Now SignCommand calls will be intercepted and evaluated by Gate
1170
+ * const result = await protectedKms.send(new SignCommand({ ... }));
1171
+ * ```
1172
+ */
1173
+ wrapKmsClient(kmsClient, options) {
1174
+ return wrapKmsClient(kmsClient, this, options);
1175
+ }
1176
+ };
1177
+ function createGateClient(config) {
1178
+ return new GateClient(config);
1179
+ }
1180
+
1181
+ exports.BlockIntelAuthError = BlockIntelAuthError;
1182
+ exports.BlockIntelBlockedError = BlockIntelBlockedError;
1183
+ exports.BlockIntelStepUpRequiredError = BlockIntelStepUpRequiredError;
1184
+ exports.BlockIntelUnavailableError = BlockIntelUnavailableError;
1185
+ exports.GateClient = GateClient;
1186
+ exports.GateError = GateError;
1187
+ exports.GateErrorCode = GateErrorCode;
1188
+ exports.StepUpNotConfiguredError = StepUpNotConfiguredError;
1189
+ exports.createGateClient = createGateClient;
1190
+ exports.default = GateClient;
1191
+ exports.wrapKmsClient = wrapKmsClient;
1192
+ //# sourceMappingURL=index.cjs.map
1193
+ //# sourceMappingURL=index.cjs.map