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