blockintel-gate-sdk 0.3.7 → 0.3.9

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.
@@ -0,0 +1,2397 @@
1
+ import { createHash, createVerify, createHmac } from 'crypto';
2
+ import { v4 } from 'uuid';
3
+ import { SignCommand } from '@aws-sdk/client-kms';
4
+
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __esm = (fn, res) => function __init() {
8
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
9
+ };
10
+ var __export = (target, all) => {
11
+ for (var name in all)
12
+ __defProp(target, name, { get: all[name], enumerable: true });
13
+ };
14
+
15
+ // src/utils/canonicalJson.ts
16
+ var canonicalJson_exports = {};
17
+ __export(canonicalJson_exports, {
18
+ canonicalizeJson: () => canonicalizeJson,
19
+ sha256Hex: () => sha256Hex
20
+ });
21
+ function canonicalizeJson(obj) {
22
+ if (obj === null || obj === void 0) {
23
+ return "null";
24
+ }
25
+ const cloned = JSON.parse(JSON.stringify(obj));
26
+ function sortKeys(item) {
27
+ if (Array.isArray(item)) {
28
+ return item.map(sortKeys);
29
+ }
30
+ if (item !== null && typeof item === "object") {
31
+ const sorted2 = {};
32
+ Object.keys(item).sort().forEach((key) => {
33
+ sorted2[key] = sortKeys(item[key]);
34
+ });
35
+ return sorted2;
36
+ }
37
+ return item;
38
+ }
39
+ const sorted = sortKeys(cloned);
40
+ return JSON.stringify(sorted);
41
+ }
42
+ async function sha256Hex(input) {
43
+ return createHash("sha256").update(input, "utf8").digest("hex");
44
+ }
45
+ var init_canonicalJson = __esm({
46
+ "src/utils/canonicalJson.ts"() {
47
+ }
48
+ });
49
+
50
+ // src/utils/decisionTokenVerify.ts
51
+ var decisionTokenVerify_exports = {};
52
+ __export(decisionTokenVerify_exports, {
53
+ decodeJwtUnsafe: () => decodeJwtUnsafe,
54
+ verifyDecisionTokenRs256: () => verifyDecisionTokenRs256
55
+ });
56
+ function decodeJwtUnsafe(token) {
57
+ try {
58
+ const parts = token.split(".");
59
+ if (parts.length !== 3) return null;
60
+ const header = JSON.parse(
61
+ Buffer.from(parts[0], "base64url").toString("utf8")
62
+ );
63
+ const payload = JSON.parse(
64
+ Buffer.from(parts[1], "base64url").toString("utf8")
65
+ );
66
+ return { header, payload };
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ function verifyDecisionTokenRs256(token, publicKeyPem) {
72
+ const decoded = decodeJwtUnsafe(token);
73
+ if (!decoded || (decoded.header.alg || "").toUpperCase() !== "RS256") return null;
74
+ const { payload } = decoded;
75
+ const now = Math.floor(Date.now() / 1e3);
76
+ if (payload.iss !== ISS || payload.aud !== AUD) return null;
77
+ if (payload.exp != null && payload.exp < now - 5) return null;
78
+ try {
79
+ const parts = token.split(".");
80
+ const signingInput = `${parts[0]}.${parts[1]}`;
81
+ const signature = Buffer.from(parts[2], "base64url");
82
+ const verify = createVerify("RSA-SHA256");
83
+ verify.update(signingInput);
84
+ verify.end();
85
+ const ok = verify.verify(publicKeyPem, signature);
86
+ return ok ? payload : null;
87
+ } catch {
88
+ return null;
89
+ }
90
+ }
91
+ var ISS, AUD;
92
+ var init_decisionTokenVerify = __esm({
93
+ "src/utils/decisionTokenVerify.ts"() {
94
+ ISS = "blockintel-gate";
95
+ AUD = "gate-decision";
96
+ }
97
+ });
98
+ async function hmacSha256(secret, message) {
99
+ const hmac = createHmac("sha256", secret);
100
+ hmac.update(message, "utf8");
101
+ const signatureHex = hmac.digest("hex");
102
+ console.error("[HMAC CRYPTO DEBUG] Signature computation:", JSON.stringify({
103
+ secretLength: secret.length,
104
+ messageLength: message.length,
105
+ messagePreview: message.substring(0, 200) + "...",
106
+ signatureLength: signatureHex.length,
107
+ signaturePreview: signatureHex.substring(0, 16) + "..."
108
+ }, null, 2));
109
+ return signatureHex;
110
+ }
111
+
112
+ // src/auth/HmacSigner.ts
113
+ init_canonicalJson();
114
+ var HmacSigner = class {
115
+ keyId;
116
+ secret;
117
+ constructor(config) {
118
+ this.keyId = config.keyId;
119
+ this.secret = config.secret.trim();
120
+ if (!this.secret || this.secret.length === 0) {
121
+ throw new Error("HMAC secret cannot be empty");
122
+ }
123
+ }
124
+ /**
125
+ * Sign a request and return headers
126
+ */
127
+ async signRequest(params) {
128
+ const { method, path, tenantId, timestampMs, requestId, body } = params;
129
+ const bodyJson = body ? canonicalizeJson(body) : "";
130
+ const bodyHash = await sha256Hex(bodyJson);
131
+ const signingString = [
132
+ "v1",
133
+ method.toUpperCase(),
134
+ path,
135
+ tenantId,
136
+ this.keyId,
137
+ String(timestampMs),
138
+ requestId,
139
+ // Used as nonce in canonical string
140
+ bodyHash
141
+ ].join("\n");
142
+ const signature = await hmacSha256(this.secret, signingString);
143
+ return {
144
+ "X-GATE-TENANT-ID": tenantId,
145
+ "X-GATE-KEY-ID": this.keyId,
146
+ "X-GATE-TIMESTAMP-MS": String(timestampMs),
147
+ "X-GATE-REQUEST-ID": requestId,
148
+ "X-GATE-SIGNATURE": signature
149
+ };
150
+ }
151
+ };
152
+
153
+ // src/auth/ApiKeyAuth.ts
154
+ var ApiKeyAuth = class {
155
+ apiKey;
156
+ constructor(config) {
157
+ this.apiKey = config.apiKey;
158
+ if (!this.apiKey || this.apiKey.length === 0) {
159
+ throw new Error("API key cannot be empty");
160
+ }
161
+ }
162
+ /**
163
+ * Create headers for API key authentication
164
+ */
165
+ createHeaders(params) {
166
+ const { tenantId, timestampMs, requestId } = params;
167
+ return {
168
+ "X-API-KEY": this.apiKey,
169
+ "X-GATE-TENANT-ID": tenantId,
170
+ "X-GATE-REQUEST-ID": requestId,
171
+ "X-GATE-TIMESTAMP-MS": String(timestampMs)
172
+ };
173
+ }
174
+ };
175
+
176
+ // src/types/errors.ts
177
+ var GateError = class extends Error {
178
+ code;
179
+ status;
180
+ details;
181
+ requestId;
182
+ correlationId;
183
+ constructor(code, message, options) {
184
+ super(message);
185
+ this.name = "GateError";
186
+ this.code = code;
187
+ this.status = options?.status;
188
+ this.details = options?.details;
189
+ this.requestId = options?.requestId;
190
+ this.correlationId = options?.correlationId;
191
+ if (options?.cause) {
192
+ this.cause = options.cause;
193
+ }
194
+ Error.captureStackTrace(this, this.constructor);
195
+ }
196
+ toJSON() {
197
+ return {
198
+ name: this.name,
199
+ code: this.code,
200
+ message: this.message,
201
+ status: this.status,
202
+ details: this.details,
203
+ requestId: this.requestId,
204
+ correlationId: this.correlationId
205
+ };
206
+ }
207
+ };
208
+ var StepUpNotConfiguredError = class extends GateError {
209
+ constructor(requestId) {
210
+ super(
211
+ "STEP_UP_NOT_CONFIGURED" /* STEP_UP_NOT_CONFIGURED */,
212
+ "Step-up is required but not configured in SDK. Enable step-up in client config or treat REQUIRE_STEP_UP as BLOCK.",
213
+ { requestId }
214
+ );
215
+ this.name = "StepUpNotConfiguredError";
216
+ }
217
+ };
218
+ var BlockIntelBlockedError = class extends GateError {
219
+ receiptId;
220
+ reasonCode;
221
+ constructor(reasonCode, receiptId, correlationId, requestId) {
222
+ super(
223
+ "BLOCKED" /* BLOCKED */,
224
+ `Transaction blocked: ${reasonCode}`,
225
+ { correlationId, requestId, details: { reasonCode, receiptId } }
226
+ );
227
+ this.name = "BlockIntelBlockedError";
228
+ this.receiptId = receiptId;
229
+ this.reasonCode = reasonCode;
230
+ }
231
+ };
232
+ var BlockIntelUnavailableError = class extends GateError {
233
+ constructor(message, requestId) {
234
+ super("SERVICE_UNAVAILABLE" /* SERVICE_UNAVAILABLE */, message, { requestId });
235
+ this.name = "BlockIntelUnavailableError";
236
+ }
237
+ };
238
+ var BlockIntelAuthError = class extends GateError {
239
+ constructor(message, status, requestId) {
240
+ super(
241
+ status === 401 ? "UNAUTHORIZED" /* UNAUTHORIZED */ : "FORBIDDEN" /* FORBIDDEN */,
242
+ message,
243
+ { status, requestId }
244
+ );
245
+ this.name = "BlockIntelAuthError";
246
+ }
247
+ };
248
+ var BlockIntelStepUpRequiredError = class extends GateError {
249
+ stepUpRequestId;
250
+ statusUrl;
251
+ expiresAtMs;
252
+ constructor(stepUpRequestId, statusUrl, expiresAtMs, requestId) {
253
+ super(
254
+ "STEP_UP_NOT_CONFIGURED" /* STEP_UP_NOT_CONFIGURED */,
255
+ "Step-up approval required",
256
+ {
257
+ requestId,
258
+ details: { stepUpRequestId, statusUrl, expiresAtMs }
259
+ }
260
+ );
261
+ this.name = "BlockIntelStepUpRequiredError";
262
+ this.stepUpRequestId = stepUpRequestId;
263
+ this.statusUrl = statusUrl;
264
+ this.expiresAtMs = expiresAtMs;
265
+ }
266
+ };
267
+
268
+ // src/http/retry.ts
269
+ var DEFAULT_RETRY_OPTIONS = {
270
+ maxAttempts: 3,
271
+ baseDelayMs: 100,
272
+ maxDelayMs: 800,
273
+ factor: 2
274
+ };
275
+ function isRetryableStatus(status) {
276
+ return status === 429 || status >= 500 && status < 600;
277
+ }
278
+ function isRetryableError(error) {
279
+ if (error instanceof Error) {
280
+ const message = error.message.toLowerCase();
281
+ return message.includes("network") || message.includes("timeout") || message.includes("connection") || message.includes("econnrefused") || message.includes("enotfound") || message.includes("econnreset");
282
+ }
283
+ return false;
284
+ }
285
+ function calculateBackoffDelay(attempt, options) {
286
+ const exponentialDelay = options.baseDelayMs * Math.pow(options.factor, attempt - 1);
287
+ const jitter = Math.random() * 0.3 * exponentialDelay;
288
+ const delay = exponentialDelay + jitter;
289
+ return Math.min(delay, options.maxDelayMs);
290
+ }
291
+ function isRetryableGateError(error) {
292
+ if (error && typeof error === "object" && "code" in error) {
293
+ const gateError = error;
294
+ if (gateError.code === "SERVER_ERROR" || gateError.code === "RATE_LIMITED") {
295
+ return true;
296
+ }
297
+ if (gateError.status && isRetryableStatus(gateError.status)) {
298
+ return true;
299
+ }
300
+ }
301
+ return false;
302
+ }
303
+ async function retryWithBackoff(fn, options = {}) {
304
+ const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
305
+ let lastError;
306
+ for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
307
+ try {
308
+ return await fn();
309
+ } catch (error) {
310
+ lastError = error;
311
+ if (attempt >= opts.maxAttempts) {
312
+ break;
313
+ }
314
+ if (error instanceof Response && !isRetryableStatus(error.status)) {
315
+ throw error;
316
+ }
317
+ const isRetryable = error instanceof Response && isRetryableStatus(error.status) || isRetryableError(error) || isRetryableGateError(error);
318
+ if (!isRetryable) {
319
+ throw error;
320
+ }
321
+ 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;
322
+ const errName = error instanceof Error ? error.name : error && typeof error === "object" && "code" in error ? error.code : "Unknown";
323
+ const extra = ` attempt=${attempt}/${opts.maxAttempts} status=${status ?? "n/a"} err=${errName}`;
324
+ console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason=retry)" + extra);
325
+ const delay = calculateBackoffDelay(attempt, opts);
326
+ await new Promise((resolve) => setTimeout(resolve, delay));
327
+ }
328
+ }
329
+ throw lastError;
330
+ }
331
+
332
+ // src/utils/sanitize.ts
333
+ var SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
334
+ "authorization",
335
+ "x-api-key",
336
+ "x-gate-heartbeat-key",
337
+ "x-gate-signature",
338
+ "cookie"
339
+ ]);
340
+ var MAX_STRING_LENGTH = 80;
341
+ function sanitizeHeaders(headers) {
342
+ const out = {};
343
+ for (const [key, value] of Object.entries(headers)) {
344
+ const lower = key.toLowerCase();
345
+ if (SENSITIVE_HEADER_NAMES.has(lower) || lower.includes("signature") || lower.includes("secret") || lower.includes("token")) {
346
+ out[key] = value ? "[REDACTED]" : "[empty]";
347
+ } else {
348
+ out[key] = truncate(String(value), MAX_STRING_LENGTH);
349
+ }
350
+ }
351
+ return out;
352
+ }
353
+ function sanitizeBodyShape(body) {
354
+ if (body === null || body === void 0) {
355
+ return {};
356
+ }
357
+ if (typeof body !== "object") {
358
+ return { _: typeof body };
359
+ }
360
+ if (Array.isArray(body)) {
361
+ return { _: "array", length: String(body.length) };
362
+ }
363
+ const out = {};
364
+ for (const key of Object.keys(body).sort()) {
365
+ const val = body[key];
366
+ if (val !== null && typeof val === "object" && !Array.isArray(val)) {
367
+ out[key] = "object";
368
+ } else if (Array.isArray(val)) {
369
+ out[key] = "array";
370
+ } else {
371
+ out[key] = typeof val;
372
+ }
373
+ }
374
+ return out;
375
+ }
376
+ function truncate(s, max) {
377
+ if (s.length <= max) return s;
378
+ return s.slice(0, max) + "...";
379
+ }
380
+ function isDebugEnabled(debugOption) {
381
+ if (debugOption === true) return true;
382
+ if (typeof process !== "undefined" && process.env.GATE_SDK_DEBUG === "1") return true;
383
+ return false;
384
+ }
385
+
386
+ // src/http/HttpClient.ts
387
+ var HttpClient = class {
388
+ baseUrl;
389
+ timeoutMs;
390
+ userAgent;
391
+ retryOptions;
392
+ debug;
393
+ constructor(config) {
394
+ this.baseUrl = config.baseUrl.replace(/\/$/, "");
395
+ this.timeoutMs = config.timeoutMs ?? 15e3;
396
+ this.userAgent = config.userAgent ?? "blockintel-gate-sdk/0.1.0";
397
+ this.retryOptions = config.retryOptions;
398
+ this.debug = isDebugEnabled(config.debug);
399
+ if (!this.baseUrl) {
400
+ throw new Error("baseUrl is required");
401
+ }
402
+ if (typeof process !== "undefined" && process.env.NODE_ENV === "production") {
403
+ if (!this.baseUrl.startsWith("https://") && !this.baseUrl.includes("localhost")) {
404
+ throw new Error("baseUrl must use HTTPS in production (except localhost)");
405
+ }
406
+ }
407
+ }
408
+ /**
409
+ * Make an HTTP request with retry and timeout
410
+ */
411
+ async request(options) {
412
+ const { method, path, headers = {}, body, requestId } = options;
413
+ const url = `${this.baseUrl}${path}`;
414
+ const controller = new AbortController();
415
+ const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
416
+ let requestDetailsForLogging = null;
417
+ try {
418
+ const response = await retryWithBackoff(
419
+ async () => {
420
+ const requestHeaders = {};
421
+ for (const [key, value] of Object.entries(headers)) {
422
+ requestHeaders[key] = String(value);
423
+ }
424
+ requestHeaders["User-Agent"] = this.userAgent;
425
+ requestHeaders["Content-Type"] = "application/json";
426
+ const fetchOptions = {
427
+ method,
428
+ headers: requestHeaders,
429
+ signal: controller.signal
430
+ };
431
+ if (body) {
432
+ if (body.__canonicalJson) {
433
+ fetchOptions.body = body.__canonicalJson;
434
+ delete body.__canonicalJson;
435
+ } else {
436
+ fetchOptions.body = JSON.stringify(body);
437
+ }
438
+ }
439
+ const bodyStr = typeof fetchOptions.body === "string" ? fetchOptions.body : null;
440
+ requestDetailsForLogging = {
441
+ headers: this.debug ? sanitizeHeaders(requestHeaders) : {},
442
+ bodyLength: bodyStr ? bodyStr.length : 0
443
+ };
444
+ if (this.debug) {
445
+ const bodyShape = body && typeof body === "object" ? sanitizeBodyShape(body) : {};
446
+ console.error("[GATE SDK] Request:", JSON.stringify({
447
+ url,
448
+ method,
449
+ headerNames: Object.keys(requestHeaders),
450
+ headersRedacted: requestDetailsForLogging.headers,
451
+ bodyLength: requestDetailsForLogging.bodyLength,
452
+ bodyKeysAndTypes: bodyShape
453
+ }, null, 2));
454
+ }
455
+ const res = await fetch(url, fetchOptions);
456
+ if (!res.ok && isRetryableStatus(res.status)) {
457
+ throw res;
458
+ }
459
+ if (!res.ok && !isRetryableStatus(res.status)) {
460
+ throw res;
461
+ }
462
+ return res;
463
+ },
464
+ {
465
+ ...this.retryOptions
466
+ // Custom retry logic that handles Response objects
467
+ }
468
+ );
469
+ clearTimeout(timeoutId);
470
+ let data;
471
+ const contentType = response.headers.get("content-type");
472
+ if (this.debug) {
473
+ console.error("[GATE SDK] Response:", JSON.stringify({
474
+ status: response.status,
475
+ ok: response.ok,
476
+ url: response.url
477
+ }, null, 2));
478
+ }
479
+ if (contentType && contentType.includes("application/json")) {
480
+ try {
481
+ const jsonText = await response.text();
482
+ data = JSON.parse(jsonText);
483
+ if (this.debug && data && typeof data === "object") {
484
+ console.error("[GATE SDK] Response keys:", Object.keys(data));
485
+ }
486
+ } catch (parseError) {
487
+ if (this.debug) {
488
+ console.error("[GATE SDK] JSON parse error:", parseError instanceof Error ? parseError.message : String(parseError));
489
+ }
490
+ throw new GateError(
491
+ "INVALID_RESPONSE" /* INVALID_RESPONSE */,
492
+ "Failed to parse JSON response",
493
+ {
494
+ status: response.status,
495
+ requestId,
496
+ cause: parseError instanceof Error ? parseError : void 0
497
+ }
498
+ );
499
+ }
500
+ } else {
501
+ const text = await response.text();
502
+ throw new GateError(
503
+ "INVALID_RESPONSE" /* INVALID_RESPONSE */,
504
+ `Unexpected content type: ${contentType}`,
505
+ {
506
+ status: response.status,
507
+ details: { body: text.substring(0, 200) },
508
+ requestId
509
+ }
510
+ );
511
+ }
512
+ if (!response.ok) {
513
+ const responseHeaders = {};
514
+ response.headers.forEach((value, key) => {
515
+ responseHeaders[key] = value;
516
+ });
517
+ if (this.debug) {
518
+ console.error("[GATE SDK] Error response:", JSON.stringify({
519
+ status: response.status,
520
+ url: response.url,
521
+ requestPath: path,
522
+ responseKeys: data && typeof data === "object" ? Object.keys(data) : []
523
+ }, null, 2));
524
+ }
525
+ const errorCode = this.statusToErrorCode(response.status);
526
+ const correlationId = response.headers.get("X-Correlation-ID") ?? void 0;
527
+ throw new GateError(errorCode, `HTTP ${response.status}: ${response.statusText}`, {
528
+ status: response.status,
529
+ correlationId,
530
+ requestId,
531
+ details: data
532
+ });
533
+ }
534
+ return data;
535
+ } catch (error) {
536
+ clearTimeout(timeoutId);
537
+ if (error instanceof Error && error.name === "AbortError") {
538
+ throw new GateError("TIMEOUT" /* TIMEOUT */, `Request timeout after ${this.timeoutMs}ms`, {
539
+ requestId
540
+ });
541
+ }
542
+ if (error instanceof Response) {
543
+ const errorCode = this.statusToErrorCode(error.status);
544
+ const correlationId = error.headers.get("X-Correlation-ID") ?? void 0;
545
+ let details;
546
+ try {
547
+ const text = await error.text();
548
+ try {
549
+ details = JSON.parse(text);
550
+ } catch {
551
+ details = { body: text.substring(0, 200) };
552
+ }
553
+ } catch {
554
+ }
555
+ throw new GateError(errorCode, `HTTP ${error.status}: ${error.statusText}`, {
556
+ status: error.status,
557
+ correlationId,
558
+ requestId,
559
+ details
560
+ });
561
+ }
562
+ if (isRetryableError(error)) {
563
+ throw new GateError(
564
+ "NETWORK_ERROR" /* NETWORK_ERROR */,
565
+ `Network error: ${error instanceof Error ? error.message : String(error)}`,
566
+ {
567
+ requestId,
568
+ cause: error instanceof Error ? error : void 0
569
+ }
570
+ );
571
+ }
572
+ if (error instanceof GateError) {
573
+ throw error;
574
+ }
575
+ throw new GateError(
576
+ "NETWORK_ERROR" /* NETWORK_ERROR */,
577
+ `Unexpected error: ${error instanceof Error ? error.message : String(error)}`,
578
+ {
579
+ requestId,
580
+ cause: error instanceof Error ? error : void 0
581
+ }
582
+ );
583
+ }
584
+ }
585
+ /**
586
+ * Map HTTP status code to GateErrorCode
587
+ */
588
+ statusToErrorCode(status) {
589
+ if (status === 401) return "UNAUTHORIZED" /* UNAUTHORIZED */;
590
+ if (status === 403) return "FORBIDDEN" /* FORBIDDEN */;
591
+ if (status === 404) return "NOT_FOUND" /* NOT_FOUND */;
592
+ if (status === 429) return "RATE_LIMITED" /* RATE_LIMITED */;
593
+ if (status >= 500 && status < 600) return "SERVER_ERROR" /* SERVER_ERROR */;
594
+ return "NETWORK_ERROR" /* NETWORK_ERROR */;
595
+ }
596
+ };
597
+
598
+ // src/utils/time.ts
599
+ function nowMs() {
600
+ return Date.now();
601
+ }
602
+ function nowEpochSeconds() {
603
+ return Math.floor(Date.now() / 1e3);
604
+ }
605
+ function clamp(value, min, max) {
606
+ return Math.max(min, Math.min(max, value));
607
+ }
608
+ function sleep(ms) {
609
+ return new Promise((resolve) => setTimeout(resolve, ms));
610
+ }
611
+
612
+ // src/stepup/stepup.ts
613
+ var DEFAULT_POLLING_INTERVAL_MS = 250;
614
+ var DEFAULT_MAX_WAIT_MS = 15e3;
615
+ var DEFAULT_TTL_MIN_SECONDS = 300;
616
+ var DEFAULT_TTL_MAX_SECONDS = 900;
617
+ var DEFAULT_TTL_DEFAULT_SECONDS = 600;
618
+ var StepUpPoller = class {
619
+ httpClient;
620
+ tenantId;
621
+ pollingIntervalMs;
622
+ maxWaitMs;
623
+ ttlMinSeconds;
624
+ ttlMaxSeconds;
625
+ ttlDefaultSeconds;
626
+ constructor(config) {
627
+ this.httpClient = config.httpClient;
628
+ this.tenantId = config.tenantId;
629
+ this.pollingIntervalMs = config.pollingIntervalMs ?? DEFAULT_POLLING_INTERVAL_MS;
630
+ this.maxWaitMs = config.maxWaitMs ?? DEFAULT_MAX_WAIT_MS;
631
+ this.ttlMinSeconds = config.ttlMinSeconds ?? DEFAULT_TTL_MIN_SECONDS;
632
+ this.ttlMaxSeconds = config.ttlMaxSeconds ?? DEFAULT_TTL_MAX_SECONDS;
633
+ this.ttlDefaultSeconds = config.ttlDefaultSeconds ?? DEFAULT_TTL_DEFAULT_SECONDS;
634
+ }
635
+ /**
636
+ * Get current step-up status
637
+ */
638
+ async getStatus(requestId) {
639
+ const path = `/defense/stepup/status?tenantId=${encodeURIComponent(this.tenantId)}&requestId=${encodeURIComponent(requestId)}`;
640
+ try {
641
+ const apiResponse = await this.httpClient.request({
642
+ method: "GET",
643
+ path,
644
+ requestId
645
+ });
646
+ const response = {
647
+ status: apiResponse.status,
648
+ tenantId: apiResponse.tenant_id ?? apiResponse.tenantId,
649
+ requestId: apiResponse.request_id ?? apiResponse.requestId,
650
+ decision: apiResponse.decision,
651
+ reasonCodes: apiResponse.reason_codes ?? apiResponse.reasonCodes,
652
+ correlationId: apiResponse.correlation_id ?? apiResponse.correlationId,
653
+ expiresAtMs: apiResponse.expires_at_ms ?? apiResponse.expiresAtMs,
654
+ ttl: apiResponse.ttl
655
+ };
656
+ const now = nowEpochSeconds();
657
+ if (response.ttl !== void 0 && response.ttl <= now) {
658
+ return {
659
+ ...response,
660
+ status: "EXPIRED"
661
+ };
662
+ }
663
+ return response;
664
+ } catch (error) {
665
+ if (error instanceof GateError && error.code === "NOT_FOUND" /* NOT_FOUND */) {
666
+ throw new GateError(
667
+ "NOT_FOUND" /* NOT_FOUND */,
668
+ `Step-up request not found: ${requestId}`,
669
+ { requestId }
670
+ );
671
+ }
672
+ throw error;
673
+ }
674
+ }
675
+ /**
676
+ * Wait for step-up decision with polling
677
+ *
678
+ * Polls until status is APPROVED, DENIED, or EXPIRED, or timeout is reached.
679
+ */
680
+ async awaitDecision(requestId, options) {
681
+ const startTime = Date.now();
682
+ const maxWaitMs = options?.maxWaitMs ?? this.maxWaitMs;
683
+ const intervalMs = options?.intervalMs ?? this.pollingIntervalMs;
684
+ while (true) {
685
+ const elapsedMs = Date.now() - startTime;
686
+ if (elapsedMs >= maxWaitMs) {
687
+ throw new GateError(
688
+ "STEP_UP_TIMEOUT" /* STEP_UP_TIMEOUT */,
689
+ `Step-up decision timeout after ${maxWaitMs}ms`,
690
+ { requestId }
691
+ );
692
+ }
693
+ try {
694
+ const status = await this.getStatus(requestId);
695
+ const now = nowEpochSeconds();
696
+ if (status.ttl !== void 0 && status.ttl <= now) {
697
+ return {
698
+ status: "EXPIRED",
699
+ requestId,
700
+ elapsedMs,
701
+ correlationId: status.correlationId
702
+ };
703
+ }
704
+ if (status.status === "APPROVED" || status.status === "DENIED" || status.status === "EXPIRED") {
705
+ return {
706
+ status: status.status,
707
+ requestId,
708
+ elapsedMs,
709
+ decision: status.decision,
710
+ reasonCodes: status.reasonCodes,
711
+ correlationId: status.correlationId
712
+ };
713
+ }
714
+ await sleep(intervalMs);
715
+ } catch (error) {
716
+ if (error instanceof GateError && error.code === "NOT_FOUND" /* NOT_FOUND */) {
717
+ throw error;
718
+ }
719
+ const remainingMs = maxWaitMs - (Date.now() - startTime);
720
+ if (remainingMs <= 0) {
721
+ throw new GateError(
722
+ "STEP_UP_TIMEOUT" /* STEP_UP_TIMEOUT */,
723
+ `Step-up decision timeout after ${maxWaitMs}ms`,
724
+ { requestId, cause: error instanceof Error ? error : void 0 }
725
+ );
726
+ }
727
+ await sleep(Math.min(intervalMs, remainingMs));
728
+ }
729
+ }
730
+ }
731
+ /**
732
+ * Clamp TTL to guardrails
733
+ */
734
+ clampTtl(ttlSeconds) {
735
+ if (ttlSeconds === void 0) {
736
+ return this.ttlDefaultSeconds;
737
+ }
738
+ return clamp(ttlSeconds, this.ttlMinSeconds, this.ttlMaxSeconds);
739
+ }
740
+ };
741
+
742
+ // src/circuit/CircuitBreaker.ts
743
+ var CircuitBreaker = class {
744
+ state = "CLOSED";
745
+ failures = 0;
746
+ successes = 0;
747
+ lastFailureTime;
748
+ lastSuccessTime;
749
+ tripsToOpen = 0;
750
+ tripThreshold;
751
+ coolDownMs;
752
+ constructor(config = {}) {
753
+ this.tripThreshold = config.tripAfterConsecutiveFailures ?? 5;
754
+ this.coolDownMs = config.coolDownMs ?? 3e4;
755
+ }
756
+ /**
757
+ * Execute function with circuit breaker protection
758
+ */
759
+ async execute(fn) {
760
+ if (this.state === "OPEN") {
761
+ const now = Date.now();
762
+ const timeSinceLastFailure = this.lastFailureTime ? now - this.lastFailureTime : Infinity;
763
+ if (timeSinceLastFailure >= this.coolDownMs) {
764
+ this.state = "HALF_OPEN";
765
+ this.failures = 0;
766
+ } else {
767
+ throw new CircuitBreakerOpenError(
768
+ `Circuit breaker is OPEN. Will retry after ${this.coolDownMs - timeSinceLastFailure}ms`
769
+ );
770
+ }
771
+ }
772
+ try {
773
+ const result = await fn();
774
+ this.onSuccess();
775
+ return result;
776
+ } catch (error) {
777
+ this.onFailure();
778
+ throw error;
779
+ }
780
+ }
781
+ onSuccess() {
782
+ this.successes++;
783
+ this.lastSuccessTime = Date.now();
784
+ if (this.state === "HALF_OPEN") {
785
+ this.state = "CLOSED";
786
+ this.failures = 0;
787
+ } else if (this.state === "CLOSED") {
788
+ this.failures = 0;
789
+ }
790
+ }
791
+ onFailure() {
792
+ this.failures++;
793
+ this.lastFailureTime = Date.now();
794
+ if (this.state === "HALF_OPEN") {
795
+ this.state = "OPEN";
796
+ this.tripsToOpen++;
797
+ } else if (this.state === "CLOSED" && this.failures >= this.tripThreshold) {
798
+ this.state = "OPEN";
799
+ this.tripsToOpen++;
800
+ }
801
+ }
802
+ /**
803
+ * Get current metrics
804
+ */
805
+ getMetrics() {
806
+ return {
807
+ failures: this.failures,
808
+ successes: this.successes,
809
+ state: this.state,
810
+ lastFailureTime: this.lastFailureTime,
811
+ lastSuccessTime: this.lastSuccessTime,
812
+ tripsToOpen: this.tripsToOpen
813
+ };
814
+ }
815
+ /**
816
+ * Reset circuit breaker to CLOSED state
817
+ */
818
+ reset() {
819
+ this.state = "CLOSED";
820
+ this.failures = 0;
821
+ this.successes = 0;
822
+ this.lastFailureTime = void 0;
823
+ this.lastSuccessTime = void 0;
824
+ this.tripsToOpen = 0;
825
+ }
826
+ };
827
+ var CircuitBreakerOpenError = class extends Error {
828
+ constructor(message) {
829
+ super(message);
830
+ this.name = "CircuitBreakerOpenError";
831
+ }
832
+ };
833
+
834
+ // src/metrics/MetricsCollector.ts
835
+ var MetricsCollector = class {
836
+ requestsTotal = 0;
837
+ allowedTotal = 0;
838
+ blockedTotal = 0;
839
+ stepupTotal = 0;
840
+ timeoutsTotal = 0;
841
+ errorsTotal = 0;
842
+ circuitBreakerOpenTotal = 0;
843
+ wouldBlockTotal = 0;
844
+ // Shadow mode would-block count
845
+ failOpenTotal = 0;
846
+ // Fail-open count
847
+ latencyMs = [];
848
+ maxSamples = 1e3;
849
+ // Keep last 1000 samples
850
+ hooks = [];
851
+ /**
852
+ * Record a request
853
+ */
854
+ recordRequest(decision, latencyMs) {
855
+ this.requestsTotal++;
856
+ if (decision === "ALLOW") {
857
+ this.allowedTotal++;
858
+ } else if (decision === "BLOCK") {
859
+ this.blockedTotal++;
860
+ } else if (decision === "REQUIRE_STEP_UP") {
861
+ this.stepupTotal++;
862
+ } else if (decision === "WOULD_BLOCK") {
863
+ this.wouldBlockTotal++;
864
+ this.allowedTotal++;
865
+ } else if (decision === "FAIL_OPEN") {
866
+ this.failOpenTotal++;
867
+ this.allowedTotal++;
868
+ }
869
+ this.latencyMs.push(latencyMs);
870
+ if (this.latencyMs.length > this.maxSamples) {
871
+ this.latencyMs.shift();
872
+ }
873
+ this.emitMetrics();
874
+ }
875
+ /**
876
+ * Record a timeout
877
+ */
878
+ recordTimeout() {
879
+ this.timeoutsTotal++;
880
+ this.errorsTotal++;
881
+ this.emitMetrics();
882
+ }
883
+ /**
884
+ * Record an error
885
+ */
886
+ recordError() {
887
+ this.errorsTotal++;
888
+ this.emitMetrics();
889
+ }
890
+ /**
891
+ * Record circuit breaker open
892
+ */
893
+ recordCircuitBreakerOpen() {
894
+ this.circuitBreakerOpenTotal++;
895
+ this.emitMetrics();
896
+ }
897
+ /**
898
+ * Record soft-enforce override (app chose to sign despite BLOCK decision)
899
+ */
900
+ recordSoftBlockOverride(decision) {
901
+ this.emitMetrics();
902
+ }
903
+ /**
904
+ * Get current metrics snapshot
905
+ */
906
+ getMetrics() {
907
+ return {
908
+ requestsTotal: this.requestsTotal,
909
+ allowedTotal: this.allowedTotal,
910
+ blockedTotal: this.blockedTotal,
911
+ stepupTotal: this.stepupTotal,
912
+ timeoutsTotal: this.timeoutsTotal,
913
+ errorsTotal: this.errorsTotal,
914
+ circuitBreakerOpenTotal: this.circuitBreakerOpenTotal,
915
+ wouldBlockTotal: this.wouldBlockTotal,
916
+ failOpenTotal: this.failOpenTotal,
917
+ latencyMs: [...this.latencyMs]
918
+ // Copy array
919
+ };
920
+ }
921
+ /**
922
+ * Register a metrics hook (e.g., for Prometheus/OpenTelemetry export)
923
+ */
924
+ registerHook(hook) {
925
+ this.hooks.push(hook);
926
+ }
927
+ /**
928
+ * Emit metrics to all registered hooks
929
+ */
930
+ emitMetrics() {
931
+ const metrics = this.getMetrics();
932
+ for (const hook of this.hooks) {
933
+ try {
934
+ hook(metrics);
935
+ } catch (error) {
936
+ console.error("Error in metrics hook:", error);
937
+ }
938
+ }
939
+ }
940
+ /**
941
+ * Reset all metrics
942
+ */
943
+ reset() {
944
+ this.requestsTotal = 0;
945
+ this.allowedTotal = 0;
946
+ this.blockedTotal = 0;
947
+ this.stepupTotal = 0;
948
+ this.timeoutsTotal = 0;
949
+ this.errorsTotal = 0;
950
+ this.circuitBreakerOpenTotal = 0;
951
+ this.wouldBlockTotal = 0;
952
+ this.failOpenTotal = 0;
953
+ this.latencyMs = [];
954
+ }
955
+ };
956
+ function canonicalJsonBinding(obj) {
957
+ if (obj === null || obj === void 0) return "null";
958
+ if (typeof obj === "string") return JSON.stringify(obj);
959
+ if (typeof obj === "number") return obj.toString();
960
+ if (typeof obj === "boolean") return obj ? "true" : "false";
961
+ if (Array.isArray(obj)) {
962
+ const items = obj.map((item) => canonicalJsonBinding(item));
963
+ return "[" + items.join(",") + "]";
964
+ }
965
+ if (typeof obj === "object") {
966
+ const keys = Object.keys(obj).sort();
967
+ const pairs = [];
968
+ for (const key of keys) {
969
+ const value = obj[key];
970
+ if (value !== void 0) {
971
+ pairs.push(JSON.stringify(key) + ":" + canonicalJsonBinding(value));
972
+ }
973
+ }
974
+ return "{" + pairs.join(",") + "}";
975
+ }
976
+ return JSON.stringify(obj);
977
+ }
978
+ function normalizeAddress(addr) {
979
+ if (addr == null || addr === "") return "";
980
+ const s = String(addr).trim();
981
+ if (s.startsWith("0x")) return s.toLowerCase();
982
+ return "0x" + s.toLowerCase();
983
+ }
984
+ function normalizeData(data) {
985
+ if (data == null || data === "") return "";
986
+ const s = String(data).trim().toLowerCase();
987
+ return s.startsWith("0x") ? s : "0x" + s;
988
+ }
989
+ function buildTxBindingObject(txIntent, signerId, decodedRecipient, decodedFields, fromAddress) {
990
+ const toAddr = txIntent.toAddress ?? txIntent.to ?? "";
991
+ const value = (txIntent.valueAtomic ?? txIntent.valueDecimal ?? txIntent.value ?? "0").toString();
992
+ const data = normalizeData(
993
+ txIntent.data ?? txIntent.payloadHash ?? txIntent.dataHash ?? ""
994
+ );
995
+ const chainId = (txIntent.chainId ?? txIntent.chain ?? "").toString();
996
+ const toAddress = normalizeAddress(toAddr);
997
+ const nonce = txIntent.nonce != null ? String(txIntent.nonce) : "";
998
+ const decoded = {};
999
+ const out = {
1000
+ chainId,
1001
+ toAddress,
1002
+ value,
1003
+ data,
1004
+ nonce
1005
+ };
1006
+ if (fromAddress) out.fromAddress = normalizeAddress(fromAddress);
1007
+ if (Object.keys(decoded).length > 0) out.decoded = decoded;
1008
+ if (signerId) out.signerId = signerId;
1009
+ if (txIntent.networkFamily) out.networkFamily = txIntent.networkFamily;
1010
+ return out;
1011
+ }
1012
+ function computeTxDigest(binding) {
1013
+ const canonical = canonicalJsonBinding(binding);
1014
+ return createHash("sha256").update(canonical, "utf8").digest("hex");
1015
+ }
1016
+
1017
+ // src/kms/wrapAwsSdkV3KmsClient.ts
1018
+ function wrapKmsClient(kmsClient, gateClient, options = {}) {
1019
+ const defaultOptions = {
1020
+ mode: options.mode || "enforce",
1021
+ onDecision: options.onDecision || (() => {
1022
+ }),
1023
+ extractTxIntent: options.extractTxIntent || defaultExtractTxIntent
1024
+ };
1025
+ const wrapped = new Proxy(kmsClient, {
1026
+ get(target, prop, receiver) {
1027
+ if (prop === "send") {
1028
+ return async function(command) {
1029
+ if (command && command.constructor && command.constructor.name === "SignCommand") {
1030
+ return await handleSignCommand(
1031
+ command,
1032
+ target,
1033
+ gateClient,
1034
+ defaultOptions
1035
+ );
1036
+ }
1037
+ return await target.send(command);
1038
+ };
1039
+ }
1040
+ return Reflect.get(target, prop, receiver);
1041
+ }
1042
+ });
1043
+ wrapped._originalClient = kmsClient;
1044
+ wrapped._gateClient = gateClient;
1045
+ wrapped._wrapperOptions = defaultOptions;
1046
+ return wrapped;
1047
+ }
1048
+ function defaultExtractTxIntent(command) {
1049
+ const message = command.input?.Message ?? command.Message;
1050
+ if (!message) {
1051
+ throw new Error("SignCommand missing required Message property");
1052
+ }
1053
+ const messageBuffer = message instanceof Buffer ? message : Buffer.from(message);
1054
+ const messageHash = createHash("sha256").update(messageBuffer).digest("hex");
1055
+ return {
1056
+ networkFamily: "OTHER",
1057
+ toAddress: void 0,
1058
+ // Unknown from KMS message alone
1059
+ payloadHash: messageHash,
1060
+ dataHash: messageHash
1061
+ // Backward compatibility
1062
+ };
1063
+ }
1064
+ async function handleSignCommand(command, originalClient, gateClient, options) {
1065
+ const txIntent = options.extractTxIntent(command);
1066
+ const signerId = command.input?.KeyId ?? command.KeyId ?? "unknown";
1067
+ gateClient.heartbeatManager.updateSignerId(signerId);
1068
+ const heartbeatToken = gateClient.heartbeatManager.getToken();
1069
+ if (!heartbeatToken) {
1070
+ throw new BlockIntelBlockedError(
1071
+ "HEARTBEAT_MISSING",
1072
+ void 0,
1073
+ // receiptId
1074
+ void 0,
1075
+ // correlationId
1076
+ void 0
1077
+ // requestId
1078
+ );
1079
+ }
1080
+ const signingContext = {
1081
+ signerId,
1082
+ actorPrincipal: "kms-signer",
1083
+ // Default - can be customized via extractTxIntent
1084
+ heartbeatToken
1085
+ // Attach heartbeat token
1086
+ };
1087
+ try {
1088
+ const decision = await gateClient.evaluate({
1089
+ txIntent,
1090
+ // Type assertion - txIntent may have extra fields
1091
+ signingContext
1092
+ });
1093
+ if (decision.decision === "ALLOW" && gateClient.getRequireDecisionToken() && decision.txDigest != null) {
1094
+ const binding = buildTxBindingObject(
1095
+ txIntent,
1096
+ signerId,
1097
+ void 0,
1098
+ void 0,
1099
+ signingContext.actorPrincipal
1100
+ );
1101
+ const computedDigest = computeTxDigest(binding);
1102
+ if (computedDigest !== decision.txDigest) {
1103
+ options.onDecision("BLOCK", {
1104
+ error: new BlockIntelBlockedError(
1105
+ "DECISION_TOKEN_TX_MISMATCH",
1106
+ decision.decisionId,
1107
+ decision.correlationId,
1108
+ void 0
1109
+ ),
1110
+ signerId,
1111
+ command
1112
+ });
1113
+ throw new BlockIntelBlockedError(
1114
+ "DECISION_TOKEN_TX_MISMATCH",
1115
+ decision.decisionId,
1116
+ decision.correlationId,
1117
+ void 0
1118
+ );
1119
+ }
1120
+ }
1121
+ options.onDecision("ALLOW", { decision, signerId, command });
1122
+ if (options.mode === "dry-run") {
1123
+ return await originalClient.send(new SignCommand(command));
1124
+ }
1125
+ return await originalClient.send(new SignCommand(command));
1126
+ } catch (error) {
1127
+ if (error instanceof BlockIntelBlockedError) {
1128
+ options.onDecision("BLOCK", { error, signerId, command });
1129
+ throw error;
1130
+ }
1131
+ if (error instanceof BlockIntelStepUpRequiredError) {
1132
+ options.onDecision("REQUIRE_STEP_UP", { error, signerId, command });
1133
+ throw error;
1134
+ }
1135
+ throw error;
1136
+ }
1137
+ }
1138
+
1139
+ // src/provenance/ProvenanceProvider.ts
1140
+ var ProvenanceProvider = class {
1141
+ /**
1142
+ * Get provenance from environment variables
1143
+ */
1144
+ static getProvenance() {
1145
+ const repo = process.env.GATE_CALLER_REPO;
1146
+ const workflow = process.env.GATE_CALLER_WORKFLOW;
1147
+ const ref = process.env.GATE_CALLER_REF;
1148
+ const actor = process.env.GATE_CALLER_ACTOR;
1149
+ const attestationValid = process.env.GATE_ATTESTATION_VALID;
1150
+ const attestationIssuer = process.env.GATE_ATTESTATION_ISSUER;
1151
+ const attestationSubject = process.env.GATE_ATTESTATION_SUBJECT;
1152
+ const attestationSha = process.env.GATE_ATTESTATION_SHA;
1153
+ if (!repo && !workflow && !ref && !actor && !attestationValid) {
1154
+ return null;
1155
+ }
1156
+ const provenance = {};
1157
+ if (repo) provenance.repo = repo;
1158
+ if (workflow) provenance.workflow = workflow;
1159
+ if (ref) provenance.ref = ref;
1160
+ if (actor) provenance.actor = actor;
1161
+ if (attestationValid || attestationIssuer || attestationSubject || attestationSha) {
1162
+ provenance.attestation = {
1163
+ valid: attestationValid === "true" || attestationValid === "1",
1164
+ issuer: attestationIssuer,
1165
+ subject: attestationSubject,
1166
+ sha: attestationSha
1167
+ };
1168
+ }
1169
+ return provenance;
1170
+ }
1171
+ /**
1172
+ * Check if provenance is enabled (env vars present)
1173
+ */
1174
+ static isEnabled() {
1175
+ return !!(process.env.GATE_CALLER_REPO || process.env.GATE_CALLER_WORKFLOW || process.env.GATE_ATTESTATION_VALID);
1176
+ }
1177
+ };
1178
+ var HeartbeatManager = class {
1179
+ httpClient;
1180
+ tenantId;
1181
+ signerId;
1182
+ environment;
1183
+ baseRefreshIntervalSeconds;
1184
+ clientInstanceId;
1185
+ // Unique per process
1186
+ sdkVersion;
1187
+ // SDK version for tracking
1188
+ apiKey;
1189
+ // x-gate-heartbeat-key for Control Plane auth
1190
+ currentToken = null;
1191
+ refreshTimer = null;
1192
+ started = false;
1193
+ consecutiveFailures = 0;
1194
+ maxBackoffSeconds = 30;
1195
+ // Maximum backoff interval
1196
+ constructor(options) {
1197
+ this.httpClient = options.httpClient;
1198
+ this.tenantId = options.tenantId;
1199
+ this.signerId = options.signerId;
1200
+ this.environment = options.environment ?? "prod";
1201
+ this.baseRefreshIntervalSeconds = options.refreshIntervalSeconds ?? 10;
1202
+ this.apiKey = options.apiKey;
1203
+ this.clientInstanceId = options.clientInstanceId || v4();
1204
+ this.sdkVersion = options.sdkVersion || "1.0.0";
1205
+ this.apiKey = options.apiKey;
1206
+ }
1207
+ /**
1208
+ * Start background heartbeat refresher.
1209
+ * Optionally wait for initial token (first evaluate() will otherwise wait up to 2s for token).
1210
+ */
1211
+ start(options) {
1212
+ if (this.started) {
1213
+ return;
1214
+ }
1215
+ this.started = true;
1216
+ this.acquireHeartbeat().catch((error) => {
1217
+ console.error("[HEARTBEAT] Failed to acquire initial heartbeat:", error instanceof Error ? error.message : error);
1218
+ });
1219
+ this.scheduleNextRefresh();
1220
+ }
1221
+ /**
1222
+ * Schedule next refresh with jitter and backoff
1223
+ */
1224
+ scheduleNextRefresh() {
1225
+ if (!this.started) {
1226
+ return;
1227
+ }
1228
+ const baseInterval = this.baseRefreshIntervalSeconds * 1e3;
1229
+ const jitter = Math.random() * 2e3;
1230
+ const backoff = this.calculateBackoff();
1231
+ const interval = baseInterval + jitter + backoff;
1232
+ this.refreshTimer = setTimeout(() => {
1233
+ this.acquireHeartbeat().then(() => {
1234
+ this.consecutiveFailures = 0;
1235
+ this.scheduleNextRefresh();
1236
+ }).catch((error) => {
1237
+ this.consecutiveFailures++;
1238
+ console.error("[HEARTBEAT] Refresh failed (will retry):", error);
1239
+ this.scheduleNextRefresh();
1240
+ });
1241
+ }, interval);
1242
+ }
1243
+ /**
1244
+ * Calculate exponential backoff (capped at maxBackoffSeconds)
1245
+ */
1246
+ calculateBackoff() {
1247
+ if (this.consecutiveFailures === 0) {
1248
+ return 0;
1249
+ }
1250
+ const backoffSeconds = Math.min(
1251
+ Math.pow(2, this.consecutiveFailures) * 1e3,
1252
+ this.maxBackoffSeconds * 1e3
1253
+ );
1254
+ return backoffSeconds;
1255
+ }
1256
+ /**
1257
+ * Stop background heartbeat refresher
1258
+ */
1259
+ stop() {
1260
+ if (!this.started) {
1261
+ return;
1262
+ }
1263
+ this.started = false;
1264
+ if (this.refreshTimer) {
1265
+ clearTimeout(this.refreshTimer);
1266
+ this.refreshTimer = null;
1267
+ }
1268
+ }
1269
+ /**
1270
+ * Get current heartbeat token if valid
1271
+ */
1272
+ getToken() {
1273
+ if (!this.currentToken) {
1274
+ return null;
1275
+ }
1276
+ const now = Math.floor(Date.now() / 1e3);
1277
+ if (this.currentToken.expiresAt <= now + 2) {
1278
+ return null;
1279
+ }
1280
+ return this.currentToken.token;
1281
+ }
1282
+ /**
1283
+ * Check if current heartbeat token is valid
1284
+ */
1285
+ isValid() {
1286
+ return this.getToken() !== null;
1287
+ }
1288
+ /**
1289
+ * Update signer ID (called when signer is known)
1290
+ */
1291
+ updateSignerId(signerId) {
1292
+ if (this.signerId !== signerId) {
1293
+ this.signerId = signerId;
1294
+ this.currentToken = null;
1295
+ }
1296
+ }
1297
+ /**
1298
+ * Acquire a new heartbeat token from Control Plane
1299
+ * NEVER logs token value (security)
1300
+ * Requires x-gate-heartbeat-key header (apiKey) for authentication.
1301
+ */
1302
+ async acquireHeartbeat() {
1303
+ if (!this.apiKey || this.apiKey.length === 0) {
1304
+ throw new GateError(
1305
+ "UNAUTHORIZED" /* UNAUTHORIZED */,
1306
+ "Heartbeat API key is required. Set GATE_HEARTBEAT_KEY in environment or pass heartbeatApiKey in GateClientConfig.",
1307
+ {}
1308
+ );
1309
+ }
1310
+ try {
1311
+ const response = await this.httpClient.request({
1312
+ method: "POST",
1313
+ path: "/api/v1/gate/heartbeat",
1314
+ headers: {
1315
+ "x-gate-heartbeat-key": this.apiKey
1316
+ },
1317
+ body: {
1318
+ tenantId: this.tenantId,
1319
+ signerId: this.signerId,
1320
+ environment: this.environment,
1321
+ clientInstanceId: this.clientInstanceId,
1322
+ sdkVersion: this.sdkVersion
1323
+ }
1324
+ });
1325
+ if (response.success && response.data) {
1326
+ const token = response.data.heartbeatToken;
1327
+ const expiresAt = response.data.expiresAt;
1328
+ if (!token || !expiresAt) {
1329
+ throw new GateError(
1330
+ "INVALID_RESPONSE" /* INVALID_RESPONSE */,
1331
+ "Invalid heartbeat response: missing token or expiresAt"
1332
+ );
1333
+ }
1334
+ this.currentToken = {
1335
+ token,
1336
+ expiresAt,
1337
+ jti: response.data.jti,
1338
+ policyHash: response.data.policyHash
1339
+ };
1340
+ console.log("[HEARTBEAT] Acquired heartbeat token", {
1341
+ expiresAt,
1342
+ jti: response.data.jti,
1343
+ policyHash: response.data.policyHash?.substring(0, 8) + "..."
1344
+ // DO NOT log token value
1345
+ });
1346
+ } else {
1347
+ const error = response.error || {};
1348
+ throw new GateError(
1349
+ "SERVER_ERROR" /* SERVER_ERROR */,
1350
+ `Heartbeat acquisition failed: ${error.message || "Unknown error"}`
1351
+ );
1352
+ }
1353
+ } catch (error) {
1354
+ console.error("[HEARTBEAT] Failed to acquire heartbeat:", error.message || error);
1355
+ throw error;
1356
+ }
1357
+ }
1358
+ /**
1359
+ * Get client instance ID (for tracking)
1360
+ */
1361
+ getClientInstanceId() {
1362
+ return this.clientInstanceId;
1363
+ }
1364
+ };
1365
+
1366
+ // src/security/IamPermissionRiskChecker.ts
1367
+ var IamPermissionRiskChecker = class {
1368
+ options;
1369
+ constructor(options) {
1370
+ this.options = options;
1371
+ }
1372
+ /**
1373
+ * Perform synchronous IAM permission risk check
1374
+ *
1375
+ * Performs quick checks (credentials, environment markers) synchronously.
1376
+ * In HARD mode, throws error if risk detected and override not set.
1377
+ *
1378
+ * Use this for blocking initialization checks.
1379
+ */
1380
+ checkSync() {
1381
+ const checks = [];
1382
+ const credentialsCheck = this.checkAwsCredentials();
1383
+ if (credentialsCheck.hasRisk) {
1384
+ checks.push(credentialsCheck);
1385
+ }
1386
+ const envCheck = this.checkEnvironmentMarkers();
1387
+ if (envCheck.hasRisk) {
1388
+ checks.push(envCheck);
1389
+ }
1390
+ const highestConfidence = this.getHighestConfidence(checks);
1391
+ const highestRisk = checks.find((c) => c.confidence === highestConfidence);
1392
+ if (!highestRisk || !highestRisk.hasRisk) {
1393
+ return {
1394
+ hasRisk: false,
1395
+ confidence: "LOW",
1396
+ details: "No IAM permission risk detected (synchronous check)"
1397
+ };
1398
+ }
1399
+ if (this.options.enforcementMode === "HARD" && !this.options.allowInsecureKmsSignPermission) {
1400
+ const errorMessage = this.buildErrorMessage(highestRisk);
1401
+ throw new Error(errorMessage);
1402
+ }
1403
+ this.logWarning(highestRisk);
1404
+ return highestRisk;
1405
+ }
1406
+ /**
1407
+ * Perform full IAM permission risk check (including async IAM simulation)
1408
+ *
1409
+ * Returns risk assessment with confidence level.
1410
+ * In HARD mode, throws error if risk detected and override not set.
1411
+ */
1412
+ async check() {
1413
+ const syncResult = this.checkSync();
1414
+ const simulationCheck = await this.checkIamSimulation();
1415
+ if (simulationCheck.hasRisk) {
1416
+ if (this.options.enforcementMode === "HARD" && !this.options.allowInsecureKmsSignPermission) {
1417
+ const errorMessage = this.buildErrorMessage(simulationCheck);
1418
+ throw new Error(errorMessage);
1419
+ }
1420
+ this.logWarning(simulationCheck);
1421
+ return simulationCheck;
1422
+ }
1423
+ return syncResult;
1424
+ }
1425
+ /**
1426
+ * Check if AWS credentials are present
1427
+ */
1428
+ checkAwsCredentials() {
1429
+ const hasEnvVars = !!(process.env.AWS_ACCESS_KEY_ID || process.env.AWS_SECRET_ACCESS_KEY || process.env.AWS_SESSION_TOKEN);
1430
+ const hasRoleCredentials = !!(process.env.AWS_ROLE_ARN || process.env.AWS_WEB_IDENTITY_TOKEN_FILE || process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI);
1431
+ if (hasEnvVars || hasRoleCredentials) {
1432
+ return {
1433
+ hasRisk: true,
1434
+ riskType: "AWS_CREDENTIALS_DETECTED",
1435
+ confidence: "MEDIUM",
1436
+ details: "AWS credentials detected in environment. Application may have direct KMS signing permissions.",
1437
+ remediation: "Remove kms:Sign permission from application role. See https://docs.blockintelai.com/gate/IAM_HARDENING"
1438
+ };
1439
+ }
1440
+ return {
1441
+ hasRisk: false,
1442
+ confidence: "LOW",
1443
+ details: "No AWS credentials detected in environment variables"
1444
+ };
1445
+ }
1446
+ /**
1447
+ * Check IAM permissions using simulation API (if available)
1448
+ */
1449
+ async checkIamSimulation() {
1450
+ try {
1451
+ const iamModule = await import('@aws-sdk/client-iam').catch(() => null);
1452
+ if (!iamModule || !iamModule.IAMClient || !iamModule.SimulatePrincipalPolicyCommand) {
1453
+ return {
1454
+ hasRisk: false,
1455
+ confidence: "LOW",
1456
+ details: "AWS SDK not available for IAM simulation"
1457
+ };
1458
+ }
1459
+ const { IAMClient, SimulatePrincipalPolicyCommand } = iamModule;
1460
+ const principalArn = await this.getCurrentPrincipalArn();
1461
+ if (!principalArn) {
1462
+ return {
1463
+ hasRisk: false,
1464
+ confidence: "LOW",
1465
+ details: "Could not determine current principal ARN for simulation"
1466
+ };
1467
+ }
1468
+ const client = new IAMClient({});
1469
+ const command = new SimulatePrincipalPolicyCommand({
1470
+ PolicySourceArn: principalArn,
1471
+ ActionNames: ["kms:Sign"],
1472
+ ResourceArns: this.options.kmsKeyIds?.map((id) => `arn:aws:kms:*:*:key/${id}`) || ["arn:aws:kms:*:*:key/*"]
1473
+ });
1474
+ const response = await client.send(command).catch(() => null);
1475
+ if (!response) {
1476
+ return {
1477
+ hasRisk: false,
1478
+ confidence: "LOW",
1479
+ details: "IAM simulation not available (may require additional permissions)"
1480
+ };
1481
+ }
1482
+ const allowsSign = response.EvaluationResults?.some(
1483
+ (result) => result.EvalDecision === "allowed" || result.EvalDecision === "explicitAllow"
1484
+ );
1485
+ if (allowsSign) {
1486
+ return {
1487
+ hasRisk: true,
1488
+ riskType: "DIRECT_KMS_SIGN_PERMISSION",
1489
+ confidence: "HIGH",
1490
+ details: `IAM simulation confirms principal ${principalArn} has kms:Sign permission. Direct KMS signing can bypass Gate.`,
1491
+ remediation: "Remove kms:Sign permission from application role. See https://docs.blockintelai.com/gate/IAM_HARDENING"
1492
+ };
1493
+ }
1494
+ return {
1495
+ hasRisk: false,
1496
+ confidence: "HIGH",
1497
+ details: "IAM simulation confirms no kms:Sign permission"
1498
+ };
1499
+ } catch (error) {
1500
+ return {
1501
+ hasRisk: false,
1502
+ confidence: "LOW",
1503
+ details: `IAM simulation failed: ${error instanceof Error ? error.message : "Unknown error"}`
1504
+ };
1505
+ }
1506
+ }
1507
+ /**
1508
+ * Check environment markers that suggest direct KMS usage
1509
+ */
1510
+ checkEnvironmentMarkers() {
1511
+ const markers = [
1512
+ "KMS_KEY_ID",
1513
+ "AWS_KMS_KEY_ID",
1514
+ "KMS_KEY_ARN",
1515
+ "AWS_KMS_KEY_ARN"
1516
+ ];
1517
+ const foundMarkers = markers.filter((marker) => process.env[marker]);
1518
+ if (foundMarkers.length > 0) {
1519
+ return {
1520
+ hasRisk: true,
1521
+ riskType: "ENVIRONMENT_MARKERS",
1522
+ confidence: "LOW",
1523
+ details: `Environment markers suggest direct KMS usage: ${foundMarkers.join(", ")}`,
1524
+ remediation: "Review environment variables and ensure KMS access is gated through Gate SDK"
1525
+ };
1526
+ }
1527
+ return {
1528
+ hasRisk: false,
1529
+ confidence: "LOW",
1530
+ details: "No environment markers suggesting direct KMS usage"
1531
+ };
1532
+ }
1533
+ /**
1534
+ * Get current principal ARN (best-effort)
1535
+ */
1536
+ async getCurrentPrincipalArn() {
1537
+ try {
1538
+ const stsModule = await import('@aws-sdk/client-sts').catch(() => null);
1539
+ if (!stsModule || !stsModule.STSClient || !stsModule.GetCallerIdentityCommand) {
1540
+ return null;
1541
+ }
1542
+ const { STSClient, GetCallerIdentityCommand } = stsModule;
1543
+ const client = new STSClient({});
1544
+ const command = new GetCallerIdentityCommand({});
1545
+ const response = await client.send(command).catch(() => null);
1546
+ if (response?.Arn) {
1547
+ return response.Arn;
1548
+ }
1549
+ } catch (error) {
1550
+ }
1551
+ return null;
1552
+ }
1553
+ /**
1554
+ * Get highest confidence level from checks
1555
+ */
1556
+ getHighestConfidence(checks) {
1557
+ if (checks.some((c) => c.confidence === "HIGH")) {
1558
+ return "HIGH";
1559
+ }
1560
+ if (checks.some((c) => c.confidence === "MEDIUM")) {
1561
+ return "MEDIUM";
1562
+ }
1563
+ return "LOW";
1564
+ }
1565
+ /**
1566
+ * Build error message for HARD mode
1567
+ */
1568
+ buildErrorMessage(result) {
1569
+ const parts = [
1570
+ "[GATE ERROR] Hard enforcement mode blocked initialization:",
1571
+ ` - IAM permission risk: ${result.details}`,
1572
+ ` - Risk type: ${result.riskType}`,
1573
+ ` - Confidence: ${result.confidence}`,
1574
+ ` - Tenant ID: ${this.options.tenantId}`
1575
+ ];
1576
+ if (this.options.signerId) {
1577
+ parts.push(` - Signer ID: ${this.options.signerId}`);
1578
+ }
1579
+ if (this.options.environment) {
1580
+ parts.push(` - Environment: ${this.options.environment}`);
1581
+ }
1582
+ if (result.remediation) {
1583
+ parts.push(` - Remediation: ${result.remediation}`);
1584
+ }
1585
+ parts.push(" - See: https://docs.blockintelai.com/gate/IAM_HARDENING");
1586
+ parts.push(` - Override: Set allowInsecureKmsSignPermission=true (not recommended for production)`);
1587
+ return parts.join("\n");
1588
+ }
1589
+ /**
1590
+ * Log warning (SOFT mode or override set)
1591
+ */
1592
+ logWarning(result) {
1593
+ const logData = {
1594
+ level: "WARN",
1595
+ message: "IAM permission risk detected",
1596
+ tenantId: this.options.tenantId,
1597
+ signerId: this.options.signerId,
1598
+ environment: this.options.environment,
1599
+ enforcementMode: this.options.enforcementMode,
1600
+ riskType: result.riskType,
1601
+ confidence: result.confidence,
1602
+ details: result.details,
1603
+ remediation: result.remediation,
1604
+ documentation: "https://docs.blockintelai.com/gate/IAM_HARDENING"
1605
+ };
1606
+ console.warn("[GATE WARNING]", JSON.stringify(logData, null, 2));
1607
+ }
1608
+ };
1609
+
1610
+ // src/client/GateClient.ts
1611
+ var GateClient = class {
1612
+ config;
1613
+ httpClient;
1614
+ hmacSigner;
1615
+ apiKeyAuth;
1616
+ stepUpPoller;
1617
+ circuitBreaker;
1618
+ metrics;
1619
+ heartbeatManager;
1620
+ mode;
1621
+ onConnectionFailure;
1622
+ constructor(config) {
1623
+ this.config = config;
1624
+ const envMode = process.env.GATE_MODE;
1625
+ this.mode = envMode || config.mode || "SHADOW";
1626
+ if (config.onConnectionFailure) {
1627
+ this.onConnectionFailure = config.onConnectionFailure;
1628
+ } else {
1629
+ this.onConnectionFailure = this.mode === "SHADOW" ? "FAIL_OPEN" : "FAIL_CLOSED";
1630
+ }
1631
+ if (config.auth.mode === "hmac") {
1632
+ this.hmacSigner = new HmacSigner({
1633
+ keyId: config.auth.keyId,
1634
+ secret: config.auth.secret
1635
+ });
1636
+ } else {
1637
+ this.apiKeyAuth = new ApiKeyAuth({
1638
+ apiKey: config.auth.apiKey
1639
+ });
1640
+ }
1641
+ this.httpClient = new HttpClient({
1642
+ baseUrl: config.baseUrl,
1643
+ timeoutMs: config.timeoutMs,
1644
+ userAgent: config.userAgent,
1645
+ debug: config.debug
1646
+ });
1647
+ if (config.enableStepUp) {
1648
+ this.stepUpPoller = new StepUpPoller({
1649
+ httpClient: this.httpClient,
1650
+ tenantId: config.tenantId,
1651
+ pollingIntervalMs: config.stepUp?.pollingIntervalMs,
1652
+ maxWaitMs: config.stepUp?.maxWaitMs
1653
+ });
1654
+ }
1655
+ if (config.circuitBreaker) {
1656
+ this.circuitBreaker = new CircuitBreaker(config.circuitBreaker);
1657
+ }
1658
+ this.metrics = new MetricsCollector();
1659
+ if (config.onMetrics) {
1660
+ this.metrics.registerHook(config.onMetrics);
1661
+ }
1662
+ if (config.local) {
1663
+ console.warn("[GATE CLIENT] LOCAL MODE ENABLED - Auth, heartbeat, and break-glass are disabled");
1664
+ this.heartbeatManager = null;
1665
+ } else {
1666
+ const heartbeatApiKey = config.heartbeatApiKey ?? (typeof process !== "undefined" ? process.env.GATE_HEARTBEAT_KEY : void 0);
1667
+ if (!heartbeatApiKey || heartbeatApiKey.length === 0) {
1668
+ throw new Error(
1669
+ "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."
1670
+ );
1671
+ }
1672
+ let controlPlaneUrl = config.baseUrl;
1673
+ if (controlPlaneUrl.includes("/defense")) {
1674
+ controlPlaneUrl = controlPlaneUrl.split("/defense")[0];
1675
+ }
1676
+ if (config.controlPlaneUrl) {
1677
+ controlPlaneUrl = config.controlPlaneUrl;
1678
+ }
1679
+ const heartbeatHttpClient = new HttpClient({
1680
+ baseUrl: controlPlaneUrl,
1681
+ timeoutMs: 5e3,
1682
+ // 5s timeout for heartbeat
1683
+ userAgent: config.userAgent
1684
+ });
1685
+ const initialSignerId = config.signerId ?? "trading-bot-signer";
1686
+ this.heartbeatManager = new HeartbeatManager({
1687
+ httpClient: heartbeatHttpClient,
1688
+ tenantId: config.tenantId,
1689
+ signerId: initialSignerId,
1690
+ environment: config.environment ?? "prod",
1691
+ refreshIntervalSeconds: config.heartbeatRefreshIntervalSeconds ?? 10,
1692
+ apiKey: heartbeatApiKey
1693
+ });
1694
+ this.heartbeatManager.start();
1695
+ }
1696
+ if (!config.local) {
1697
+ const enforcementMode = config.enforcementMode || "SOFT";
1698
+ const allowInsecureKmsSignPermission = config.allowInsecureKmsSignPermission ?? enforcementMode === "SOFT";
1699
+ const riskChecker = new IamPermissionRiskChecker({
1700
+ tenantId: config.tenantId,
1701
+ signerId: config.signerId,
1702
+ environment: config.environment,
1703
+ enforcementMode,
1704
+ allowInsecureKmsSignPermission,
1705
+ kmsKeyIds: config.kmsKeyIds
1706
+ });
1707
+ riskChecker.checkSync();
1708
+ this.performIamRiskCheckAsync(riskChecker, enforcementMode).catch((error) => {
1709
+ if (enforcementMode === "SOFT" || allowInsecureKmsSignPermission) {
1710
+ console.warn("[GATE CLIENT] Async IAM risk check warning:", error instanceof Error ? error.message : String(error));
1711
+ } else {
1712
+ console.error("[GATE CLIENT] Async IAM risk check found risk after initialization:", error);
1713
+ }
1714
+ });
1715
+ }
1716
+ }
1717
+ /**
1718
+ * Whether the SDK requires a decision token for ALLOW before sign (ENFORCE/HARD).
1719
+ * Env GATE_REQUIRE_DECISION_TOKEN overrides config.
1720
+ */
1721
+ getRequireDecisionToken() {
1722
+ if (typeof process !== "undefined" && process.env.GATE_REQUIRE_DECISION_TOKEN !== void 0) {
1723
+ return process.env.GATE_REQUIRE_DECISION_TOKEN === "true" || process.env.GATE_REQUIRE_DECISION_TOKEN === "1";
1724
+ }
1725
+ return this.config.requireDecisionToken ?? (this.mode === "ENFORCE" || this.config.enforcementMode === "HARD");
1726
+ }
1727
+ /**
1728
+ * Perform async IAM permission risk check (non-blocking)
1729
+ *
1730
+ * Performs async IAM simulation check in background.
1731
+ * Logs warnings but doesn't block (initialization already completed).
1732
+ */
1733
+ async performIamRiskCheckAsync(riskChecker, enforcementMode) {
1734
+ try {
1735
+ await riskChecker.check();
1736
+ } catch (error) {
1737
+ console.warn("[GATE CLIENT] Async IAM risk check warning:", error instanceof Error ? error.message : String(error));
1738
+ }
1739
+ }
1740
+ /**
1741
+ * Evaluate a transaction defense request
1742
+ *
1743
+ * Implements:
1744
+ * - Shadow Mode (SHADOW: monitor-only, ENFORCE: enforce decisions)
1745
+ * - Connection failure strategy (FAIL_OPEN vs FAIL_CLOSED)
1746
+ * - Circuit breaker protection
1747
+ * - Fail-safe modes (ALLOW_ON_TIMEOUT, BLOCK_ON_TIMEOUT, BLOCK_ON_ANOMALY)
1748
+ * - Metrics collection
1749
+ * - Error handling (BLOCK → BlockIntelBlockedError, REQUIRE_STEP_UP → BlockIntelStepUpRequiredError)
1750
+ */
1751
+ async evaluate(req, opts) {
1752
+ const requestId = opts?.requestId ?? v4();
1753
+ const timestampMs = req.timestampMs ?? nowMs();
1754
+ const startTime = Date.now();
1755
+ const failSafeMode = this.config.failSafeMode ?? "ALLOW_ON_TIMEOUT";
1756
+ const evaluationMode = this.config.evaluationMode ?? "BLOCKING";
1757
+ const requestMode = req.mode || this.mode;
1758
+ const requireToken = this.getRequireDecisionToken();
1759
+ const executeRequest = async () => {
1760
+ if (!this.config.local && this.heartbeatManager && req.signingContext?.signerId) {
1761
+ this.heartbeatManager.updateSignerId(req.signingContext.signerId);
1762
+ }
1763
+ let heartbeatToken = null;
1764
+ if (!this.config.local && this.heartbeatManager) {
1765
+ heartbeatToken = this.heartbeatManager.getToken();
1766
+ if (!heartbeatToken) {
1767
+ const maxWaitMs = 2e3;
1768
+ const startTime2 = Date.now();
1769
+ while (!heartbeatToken && Date.now() - startTime2 < maxWaitMs) {
1770
+ await new Promise((resolve) => setTimeout(resolve, 50));
1771
+ heartbeatToken = this.heartbeatManager.getToken();
1772
+ }
1773
+ }
1774
+ if (!heartbeatToken) {
1775
+ throw new GateError(
1776
+ "HEARTBEAT_MISSING" /* HEARTBEAT_MISSING */,
1777
+ "Signing blocked: Heartbeat token is missing or expired. Gate must be alive and enforcing policy."
1778
+ );
1779
+ }
1780
+ }
1781
+ const txIntent = { ...req.txIntent };
1782
+ if (txIntent.to && !txIntent.toAddress) {
1783
+ txIntent.toAddress = txIntent.to;
1784
+ delete txIntent.to;
1785
+ }
1786
+ if (!txIntent.networkFamily && txIntent.chainId) {
1787
+ txIntent.networkFamily = "EVM";
1788
+ }
1789
+ if (txIntent.from && !txIntent.fromAddress) {
1790
+ delete txIntent.from;
1791
+ }
1792
+ const signingContext = {
1793
+ ...req.signingContext,
1794
+ actorPrincipal: req.signingContext?.actorPrincipal ?? req.signingContext?.signerId ?? "gate-sdk-client",
1795
+ signerId: req.signingContext?.signerId ?? req.signingContext?.actorPrincipal ?? "gate-sdk-client"
1796
+ };
1797
+ if (heartbeatToken) {
1798
+ signingContext.heartbeatToken = heartbeatToken;
1799
+ }
1800
+ const provenance = ProvenanceProvider.getProvenance();
1801
+ if (provenance) {
1802
+ signingContext.caller = {
1803
+ repo: provenance.repo,
1804
+ workflow: provenance.workflow,
1805
+ ref: provenance.ref,
1806
+ actor: provenance.actor,
1807
+ attestation: provenance.attestation
1808
+ };
1809
+ }
1810
+ let body = {
1811
+ tenantId: this.config.tenantId,
1812
+ requestId,
1813
+ timestampMs,
1814
+ txIntent,
1815
+ signingContext,
1816
+ // Add SDK info (required by Hot Path validation)
1817
+ sdk: {
1818
+ name: "gate-sdk",
1819
+ version: "0.1.0"
1820
+ },
1821
+ mode: requestMode,
1822
+ onConnectionFailure: this.onConnectionFailure
1823
+ };
1824
+ if (req.simulate === true) {
1825
+ body.simulate = true;
1826
+ }
1827
+ if (!this.config.local && this.config.breakglassToken) {
1828
+ signingContext.breakglassToken = this.config.breakglassToken;
1829
+ }
1830
+ let headers = {};
1831
+ if (this.config.local) {
1832
+ headers = {
1833
+ "Content-Type": "application/json"
1834
+ };
1835
+ console.log("[GATE CLIENT] LOCAL MODE - Skipping authentication");
1836
+ } else if (this.hmacSigner) {
1837
+ const { canonicalizeJson: canonicalizeJson2 } = await Promise.resolve().then(() => (init_canonicalJson(), canonicalJson_exports));
1838
+ const canonicalBodyJson = canonicalizeJson2(body);
1839
+ const hmacHeaders = await this.hmacSigner.signRequest({
1840
+ method: "POST",
1841
+ path: "/defense/evaluate",
1842
+ tenantId: this.config.tenantId,
1843
+ timestampMs,
1844
+ requestId,
1845
+ body
1846
+ // Pass original body - HmacSigner will canonicalize it internally
1847
+ });
1848
+ headers = { ...hmacHeaders };
1849
+ body.__canonicalJson = canonicalBodyJson;
1850
+ } else if (this.apiKeyAuth) {
1851
+ const apiKeyHeaders = this.apiKeyAuth.createHeaders({
1852
+ tenantId: this.config.tenantId,
1853
+ timestampMs,
1854
+ requestId
1855
+ });
1856
+ headers = { ...apiKeyHeaders };
1857
+ } else {
1858
+ throw new Error("No authentication configured");
1859
+ }
1860
+ const apiResponse = await this.httpClient.request({
1861
+ method: "POST",
1862
+ path: "/defense/evaluate",
1863
+ headers,
1864
+ body,
1865
+ requestId
1866
+ });
1867
+ let responseData;
1868
+ if (apiResponse.success === true && apiResponse.data) {
1869
+ responseData = apiResponse.data;
1870
+ } else if (apiResponse.success === false && apiResponse.error) {
1871
+ const error = apiResponse.error;
1872
+ throw new GateError(
1873
+ error.code || "SERVER_ERROR" /* SERVER_ERROR */,
1874
+ error.message || "Request failed",
1875
+ {
1876
+ status: error.status,
1877
+ correlationId: error.correlationId,
1878
+ requestId,
1879
+ details: error
1880
+ }
1881
+ );
1882
+ } else if (apiResponse.decision) {
1883
+ responseData = apiResponse;
1884
+ } else {
1885
+ throw new GateError(
1886
+ "INVALID_RESPONSE" /* INVALID_RESPONSE */,
1887
+ "Invalid response format: expected { success: true, data: { ... } } or unwrapped response",
1888
+ {
1889
+ requestId,
1890
+ details: apiResponse
1891
+ }
1892
+ );
1893
+ }
1894
+ const metadata = responseData.metadata || {};
1895
+ const simulationData = metadata.simulation;
1896
+ const result = {
1897
+ decision: responseData.decision,
1898
+ reasonCodes: responseData.reason_codes ?? responseData.reasonCodes ?? [],
1899
+ policyVersion: responseData.policy_version ?? responseData.policyVersion,
1900
+ correlationId: responseData.correlation_id ?? responseData.correlationId,
1901
+ decisionId: responseData.decision_id ?? responseData.decisionId,
1902
+ decisionToken: responseData.decision_token ?? responseData.decisionToken,
1903
+ expiresAt: responseData.expires_at ?? responseData.expiresAt,
1904
+ txDigest: responseData.tx_digest ?? responseData.txDigest,
1905
+ stepUp: responseData.step_up ? {
1906
+ requestId: responseData.step_up.request_id ?? (responseData.stepUp?.requestId ?? ""),
1907
+ ttlSeconds: responseData.step_up.ttl_seconds ?? responseData.stepUp?.ttlSeconds
1908
+ } : responseData.stepUp,
1909
+ enforced: responseData.enforced ?? requestMode === "ENFORCE",
1910
+ shadowWouldBlock: responseData.shadow_would_block ?? responseData.shadowWouldBlock ?? false,
1911
+ mode: responseData.mode ?? requestMode,
1912
+ ...simulationData ? {
1913
+ simulation: {
1914
+ willRevert: simulationData.willRevert ?? simulationData.will_revert ?? false,
1915
+ gasUsed: simulationData.gasUsed ?? simulationData.gas_used,
1916
+ balanceChanges: simulationData.balanceChanges ?? simulationData.balance_changes,
1917
+ errorReason: simulationData.errorReason ?? simulationData.error_reason
1918
+ },
1919
+ simulationLatencyMs: metadata.simulationLatencyMs ?? metadata.simulation_latency_ms
1920
+ } : {},
1921
+ metadata: {
1922
+ evaluationLatencyMs: metadata.evaluationLatencyMs ?? metadata.evaluation_latency_ms,
1923
+ policyHash: metadata.policyHash ?? metadata.policy_hash,
1924
+ snapshotVersion: metadata.snapshotVersion ?? metadata.snapshot_version
1925
+ }
1926
+ };
1927
+ const latencyMs = Date.now() - startTime;
1928
+ const expectedPolicyHash = this.config.expectedPolicyHash;
1929
+ const expectedSnapshotVersion = this.config.expectedSnapshotVersion;
1930
+ if (expectedPolicyHash != null && result.metadata?.policyHash !== expectedPolicyHash) {
1931
+ if (this.config.debug) {
1932
+ console.warn("[GATE SDK] Policy hash mismatch (pinning)", {
1933
+ expected: expectedPolicyHash,
1934
+ received: result.metadata?.policyHash,
1935
+ requestId
1936
+ });
1937
+ }
1938
+ this.metrics.recordRequest("BLOCK", latencyMs);
1939
+ throw new BlockIntelBlockedError(
1940
+ "POLICY_HASH_MISMATCH",
1941
+ result.decisionId ?? requestId,
1942
+ result.correlationId,
1943
+ requestId
1944
+ );
1945
+ }
1946
+ if (expectedSnapshotVersion != null && result.metadata?.snapshotVersion !== void 0 && result.metadata.snapshotVersion !== expectedSnapshotVersion) {
1947
+ if (this.config.debug) {
1948
+ console.warn("[GATE SDK] Snapshot version mismatch (pinning)", {
1949
+ expected: expectedSnapshotVersion,
1950
+ received: result.metadata?.snapshotVersion,
1951
+ requestId
1952
+ });
1953
+ }
1954
+ this.metrics.recordRequest("BLOCK", latencyMs);
1955
+ throw new BlockIntelBlockedError(
1956
+ "SNAPSHOT_VERSION_MISMATCH",
1957
+ result.decisionId ?? requestId,
1958
+ result.correlationId,
1959
+ requestId
1960
+ );
1961
+ }
1962
+ if (requireToken && requestMode === "ENFORCE" && result.decision === "ALLOW" && !this.config.local) {
1963
+ if (!result.decisionToken || !result.txDigest) {
1964
+ this.metrics.recordRequest("BLOCK", latencyMs);
1965
+ throw new BlockIntelBlockedError(
1966
+ "DECISION_TOKEN_MISSING",
1967
+ result.decisionId ?? requestId,
1968
+ result.correlationId,
1969
+ requestId
1970
+ );
1971
+ }
1972
+ const nowSec = Math.floor(Date.now() / 1e3);
1973
+ if (result.expiresAt != null && result.expiresAt < nowSec - 5) {
1974
+ this.metrics.recordRequest("BLOCK", latencyMs);
1975
+ throw new BlockIntelBlockedError(
1976
+ "DECISION_TOKEN_EXPIRED",
1977
+ result.decisionId ?? requestId,
1978
+ result.correlationId,
1979
+ requestId
1980
+ );
1981
+ }
1982
+ const publicKeyPem = this.config.decisionTokenPublicKey;
1983
+ if (publicKeyPem && result.decisionToken) {
1984
+ const { decodeJwtUnsafe: decodeJwtUnsafe2, verifyDecisionTokenRs256: verifyDecisionTokenRs2562 } = await Promise.resolve().then(() => (init_decisionTokenVerify(), decisionTokenVerify_exports));
1985
+ const decoded = decodeJwtUnsafe2(result.decisionToken);
1986
+ if (decoded && (decoded.header.alg || "").toUpperCase() === "RS256") {
1987
+ const resolvedPem = publicKeyPem.startsWith("-----") ? publicKeyPem : Buffer.from(publicKeyPem, "base64").toString("utf8");
1988
+ const verified = verifyDecisionTokenRs2562(result.decisionToken, resolvedPem);
1989
+ if (verified === null) {
1990
+ this.metrics.recordRequest("BLOCK", latencyMs);
1991
+ throw new BlockIntelBlockedError(
1992
+ "DECISION_TOKEN_INVALID",
1993
+ result.decisionId ?? requestId,
1994
+ result.correlationId,
1995
+ requestId
1996
+ );
1997
+ }
1998
+ }
1999
+ }
2000
+ const signerId = signingContext?.signerId ?? req.signingContext?.signerId;
2001
+ const fromAddress = txIntent.fromAddress ?? txIntent.from;
2002
+ const binding = buildTxBindingObject(txIntent, signerId, void 0, void 0, fromAddress);
2003
+ const computedDigest = computeTxDigest(binding);
2004
+ if (computedDigest !== result.txDigest) {
2005
+ this.metrics.recordRequest("BLOCK", latencyMs);
2006
+ throw new BlockIntelBlockedError(
2007
+ "DECISION_TOKEN_DIGEST_MISMATCH",
2008
+ result.decisionId ?? requestId,
2009
+ result.correlationId,
2010
+ requestId
2011
+ );
2012
+ }
2013
+ }
2014
+ if (result.decision === "BLOCK") {
2015
+ if (requestMode === "SOFT_ENFORCE") {
2016
+ console.warn("[SOFT ENFORCE] Policy violation detected - app can override", {
2017
+ requestId,
2018
+ reasonCodes: result.reasonCodes
2019
+ });
2020
+ this.metrics.recordRequest("BLOCK", latencyMs);
2021
+ return {
2022
+ ...result,
2023
+ decision: "BLOCK",
2024
+ enforced: false,
2025
+ mode: "SOFT_ENFORCE",
2026
+ warning: "Policy violation detected. Override at your own risk."
2027
+ };
2028
+ }
2029
+ if (requestMode === "SHADOW") {
2030
+ console.warn("[GATE SHADOW MODE] Would have blocked transaction", {
2031
+ requestId,
2032
+ reasonCodes: result.reasonCodes,
2033
+ correlationId: result.correlationId,
2034
+ tenantId: this.config.tenantId,
2035
+ signerId: req.signingContext?.signerId
2036
+ });
2037
+ this.metrics.recordRequest("WOULD_BLOCK", latencyMs);
2038
+ return {
2039
+ ...result,
2040
+ decision: "ALLOW",
2041
+ enforced: false,
2042
+ shadowWouldBlock: true
2043
+ };
2044
+ }
2045
+ const receiptId = responseData.decision_id || requestId;
2046
+ const reasonCode = result.reasonCodes[0] || "POLICY_VIOLATION";
2047
+ this.metrics.recordRequest("BLOCK", latencyMs);
2048
+ throw new BlockIntelBlockedError(reasonCode, receiptId, result.correlationId, requestId);
2049
+ }
2050
+ if (result.decision === "REQUIRE_STEP_UP") {
2051
+ if (this.config.enableStepUp && this.stepUpPoller && result.stepUp) {
2052
+ const stepUpRequestId = result.stepUp.requestId || requestId;
2053
+ const expiresAtMs = responseData.step_up?.expires_at_ms;
2054
+ const statusUrl = `/defense/stepup/status?tenantId=${this.config.tenantId}&requestId=${stepUpRequestId}`;
2055
+ this.metrics.recordRequest("REQUIRE_STEP_UP", latencyMs);
2056
+ throw new BlockIntelStepUpRequiredError(stepUpRequestId, statusUrl, expiresAtMs, requestId);
2057
+ } else {
2058
+ const receiptId = responseData.decision_id || requestId;
2059
+ const reasonCode = "STEPUP_REQUIRED";
2060
+ this.metrics.recordRequest("BLOCK", latencyMs);
2061
+ throw new BlockIntelBlockedError(reasonCode, receiptId, result.correlationId, requestId);
2062
+ }
2063
+ }
2064
+ this.metrics.recordRequest("ALLOW", latencyMs);
2065
+ return result;
2066
+ };
2067
+ if (evaluationMode === "FIRE_AND_FORGET") {
2068
+ executeRequest().then((res) => {
2069
+ if (res.decision === "BLOCK" || res.shadowWouldBlock) {
2070
+ console.warn("[FIRE-AND-FORGET] Would have blocked:", res.reasonCodes);
2071
+ }
2072
+ this.metrics.recordRequest(res.decision === "ALLOW" ? "ALLOW" : "WOULD_BLOCK", Date.now() - startTime);
2073
+ }).catch((err) => {
2074
+ console.error("[FIRE-AND-FORGET] Attestation failed:", err);
2075
+ this.metrics.recordError();
2076
+ });
2077
+ return {
2078
+ decision: "ALLOW",
2079
+ decisionId: requestId,
2080
+ correlationId: requestId,
2081
+ reasonCodes: [],
2082
+ enforced: false,
2083
+ mode: requestMode,
2084
+ fireAndForget: true
2085
+ };
2086
+ }
2087
+ try {
2088
+ if (this.circuitBreaker) {
2089
+ return await this.circuitBreaker.execute(executeRequest);
2090
+ }
2091
+ return await executeRequest();
2092
+ } catch (error) {
2093
+ if (error instanceof CircuitBreakerOpenError) {
2094
+ this.metrics.recordCircuitBreakerOpen();
2095
+ const failSafeResult = this.handleFailSafe(failSafeMode, error, requestId);
2096
+ if (failSafeResult) {
2097
+ return failSafeResult;
2098
+ }
2099
+ throw error;
2100
+ }
2101
+ if (error instanceof GateError && (error.code === "UNAUTHORIZED" /* UNAUTHORIZED */ || error.code === "FORBIDDEN" /* FORBIDDEN */)) {
2102
+ this.metrics.recordError();
2103
+ throw new BlockIntelAuthError(
2104
+ error.message,
2105
+ error.status || 401,
2106
+ requestId
2107
+ );
2108
+ }
2109
+ const isConnectionFailure = error instanceof GateError && (error.code === "TIMEOUT" /* TIMEOUT */ || error.code === "SERVER_ERROR" /* SERVER_ERROR */) || error instanceof BlockIntelUnavailableError || error?.code === "ECONNREFUSED" || error?.code === "ENOTFOUND" || error?.code === "ETIMEDOUT";
2110
+ if (isConnectionFailure) {
2111
+ this.metrics.recordTimeout();
2112
+ if (this.onConnectionFailure === "FAIL_OPEN") {
2113
+ console.error("[GATE CONNECTION FAILURE] FAIL_OPEN mode - allowing transaction", {
2114
+ requestId,
2115
+ error: error.message,
2116
+ tenantId: this.config.tenantId,
2117
+ mode: requestMode
2118
+ });
2119
+ console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_open)");
2120
+ this.metrics.recordRequest("FAIL_OPEN", Date.now() - startTime);
2121
+ return {
2122
+ decision: "ALLOW",
2123
+ reasonCodes: ["GATE_HOTPATH_UNAVAILABLE"],
2124
+ correlationId: requestId,
2125
+ enforced: false,
2126
+ mode: requestMode
2127
+ };
2128
+ } else {
2129
+ throw new BlockIntelUnavailableError(
2130
+ `Signing blocked: Gate hot path unreachable (fail-closed). ${error.message}`,
2131
+ requestId
2132
+ );
2133
+ }
2134
+ }
2135
+ if (error instanceof GateError && error.code === "TIMEOUT" /* TIMEOUT */) {
2136
+ this.metrics.recordTimeout();
2137
+ const failSafeResult = this.handleFailSafe(failSafeMode, error, requestId);
2138
+ if (failSafeResult) {
2139
+ return failSafeResult;
2140
+ }
2141
+ throw new BlockIntelUnavailableError(`Service timeout: ${error.message}`, requestId);
2142
+ }
2143
+ if (error instanceof GateError && error.code === "SERVER_ERROR" /* SERVER_ERROR */) {
2144
+ this.metrics.recordError();
2145
+ const failSafeResult = this.handleFailSafe(failSafeMode, error, requestId);
2146
+ if (failSafeResult) {
2147
+ return failSafeResult;
2148
+ }
2149
+ throw error;
2150
+ }
2151
+ if (error instanceof GateError && error.code === "RATE_LIMITED" /* RATE_LIMITED */) {
2152
+ console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: 429)");
2153
+ throw error;
2154
+ }
2155
+ if (error instanceof BlockIntelBlockedError || error instanceof BlockIntelStepUpRequiredError) {
2156
+ throw error;
2157
+ }
2158
+ this.metrics.recordError();
2159
+ throw error;
2160
+ }
2161
+ }
2162
+ /**
2163
+ * Handle fail-safe modes for timeouts/errors
2164
+ */
2165
+ handleFailSafe(mode, error, requestId) {
2166
+ if (mode === "ALLOW_ON_TIMEOUT") {
2167
+ console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_safe_allow)");
2168
+ return {
2169
+ decision: "ALLOW",
2170
+ reasonCodes: ["FAIL_SAFE_ALLOW"],
2171
+ correlationId: requestId
2172
+ };
2173
+ }
2174
+ if (mode === "BLOCK_ON_TIMEOUT") {
2175
+ return null;
2176
+ }
2177
+ if (mode === "BLOCK_ON_ANOMALY") {
2178
+ console.warn("[GATE SDK] X-BlockIntel-Degraded: true (reason: fail_safe_allow)");
2179
+ return {
2180
+ decision: "ALLOW",
2181
+ reasonCodes: ["FAIL_SAFE_ALLOW"],
2182
+ correlationId: requestId
2183
+ };
2184
+ }
2185
+ return null;
2186
+ }
2187
+ /**
2188
+ * Get current metrics
2189
+ */
2190
+ getMetrics() {
2191
+ return this.metrics.getMetrics();
2192
+ }
2193
+ /**
2194
+ * Get circuit breaker metrics (if enabled)
2195
+ */
2196
+ getCircuitBreakerMetrics() {
2197
+ return this.circuitBreaker?.getMetrics() || null;
2198
+ }
2199
+ /**
2200
+ * Get step-up status
2201
+ */
2202
+ async getStepUpStatus(args) {
2203
+ if (!this.stepUpPoller) {
2204
+ throw new StepUpNotConfiguredError(args.requestId);
2205
+ }
2206
+ const tenantId = args.tenantId ?? this.config.tenantId;
2207
+ const poller = new StepUpPoller({
2208
+ httpClient: this.httpClient,
2209
+ tenantId,
2210
+ pollingIntervalMs: this.config.stepUp?.pollingIntervalMs,
2211
+ maxWaitMs: this.config.stepUp?.maxWaitMs
2212
+ });
2213
+ return poller.getStatus(args.requestId);
2214
+ }
2215
+ /**
2216
+ * Wait for step-up decision with polling
2217
+ */
2218
+ async awaitStepUpDecision(args) {
2219
+ if (!this.stepUpPoller) {
2220
+ throw new StepUpNotConfiguredError(args.requestId);
2221
+ }
2222
+ return this.stepUpPoller.awaitDecision(args.requestId, {
2223
+ maxWaitMs: args.maxWaitMs ?? this.config.stepUp?.maxWaitMs,
2224
+ intervalMs: args.intervalMs ?? this.config.stepUp?.pollingIntervalMs
2225
+ });
2226
+ }
2227
+ /**
2228
+ * Evaluate policy and sign in one call when decision is ALLOW.
2229
+ * Convenience for: evaluate → if ALLOW then sign → return { decision, signature }.
2230
+ */
2231
+ async evaluateAndSign(params) {
2232
+ const decision = await this.evaluate({
2233
+ txIntent: params.txIntent,
2234
+ signingContext: params.signingContext
2235
+ });
2236
+ if (decision.decision === "ALLOW") {
2237
+ const signature = await params.signer.sign({
2238
+ keyId: params.keyId,
2239
+ message: params.message,
2240
+ algorithm: params.algorithm ?? "ECDSA_SHA_256"
2241
+ });
2242
+ return { decision, signature };
2243
+ }
2244
+ return { decision };
2245
+ }
2246
+ /**
2247
+ * Attest a completed signature (post-sign). Use when you want zero latency impact on signing
2248
+ * but still want an audit trail. Policy is evaluated against txIntent; returns ALLOW or
2249
+ * POLICY_VIOLATION_DETECTED. Cannot be used for enforcement (signature already created).
2250
+ */
2251
+ async attestCompleted(req) {
2252
+ const requestId = v4();
2253
+ const timestampMs = nowMs();
2254
+ const txIntent = { ...req.txIntent };
2255
+ if (txIntent.to && !txIntent.toAddress) {
2256
+ txIntent.toAddress = txIntent.to;
2257
+ delete txIntent.to;
2258
+ }
2259
+ if (!txIntent.networkFamily && txIntent.chainId) txIntent.networkFamily = "EVM";
2260
+ const signingContext = {
2261
+ ...req.signingContext,
2262
+ signerId: req.signingContext?.signerId ?? req.signature.signerId
2263
+ };
2264
+ const body = {
2265
+ tenantId: this.config.tenantId,
2266
+ requestId,
2267
+ timestampMs,
2268
+ txIntent,
2269
+ signature: req.signature,
2270
+ signingContext
2271
+ };
2272
+ let headers = { "Content-Type": "application/json" };
2273
+ if (this.config.local) ; else if (this.hmacSigner) {
2274
+ const { canonicalizeJson: canonicalizeJson2 } = await Promise.resolve().then(() => (init_canonicalJson(), canonicalJson_exports));
2275
+ const canonicalBodyJson = canonicalizeJson2(body);
2276
+ const hmacHeaders = await this.hmacSigner.signRequest({
2277
+ method: "POST",
2278
+ path: "/defense/attest-completed",
2279
+ tenantId: this.config.tenantId,
2280
+ timestampMs,
2281
+ requestId,
2282
+ body
2283
+ });
2284
+ headers = { ...hmacHeaders };
2285
+ body.__canonicalJson = canonicalBodyJson;
2286
+ } else if (this.apiKeyAuth) {
2287
+ const apiKeyHeaders = this.apiKeyAuth.createHeaders({
2288
+ tenantId: this.config.tenantId,
2289
+ timestampMs,
2290
+ requestId
2291
+ });
2292
+ headers = { ...apiKeyHeaders };
2293
+ } else {
2294
+ throw new Error("No authentication configured");
2295
+ }
2296
+ const apiResponse = await this.httpClient.request({
2297
+ method: "POST",
2298
+ path: "/defense/attest-completed",
2299
+ headers,
2300
+ body,
2301
+ requestId
2302
+ });
2303
+ if (apiResponse.success === true && apiResponse.data) {
2304
+ const data = apiResponse.data;
2305
+ if (data.decision === "POLICY_VIOLATION_DETECTED") {
2306
+ console.warn("[POST-SIGN ATTESTATION] Policy violation detected after signing", {
2307
+ requestId,
2308
+ reasonCodes: data.reasonCodes
2309
+ });
2310
+ }
2311
+ return data;
2312
+ }
2313
+ if (apiResponse.error) {
2314
+ const err = apiResponse.error;
2315
+ throw new GateError(err.code || "SERVER_ERROR", err.message || "Request failed", {
2316
+ status: err.status,
2317
+ correlationId: err.correlationId,
2318
+ requestId
2319
+ });
2320
+ }
2321
+ throw new GateError("INVALID_RESPONSE" /* INVALID_RESPONSE */, "Invalid response from attest-completed", { requestId });
2322
+ }
2323
+ /**
2324
+ * Wrap AWS SDK v3 KMS client to intercept SignCommand calls
2325
+ *
2326
+ * @param kmsClient - AWS SDK v3 KMSClient instance
2327
+ * @param options - Wrapper options
2328
+ * @returns Wrapped KMS client that enforces Gate policies
2329
+ *
2330
+ * @example
2331
+ * ```typescript
2332
+ * import { KMSClient } from '@aws-sdk/client-kms';
2333
+ *
2334
+ * const kms = new KMSClient({});
2335
+ * const protectedKms = gateClient.wrapKmsClient(kms);
2336
+ *
2337
+ * // Now SignCommand calls will be intercepted and evaluated by Gate
2338
+ * const result = await protectedKms.send(new SignCommand({ ... }));
2339
+ * ```
2340
+ */
2341
+ wrapKmsClient(kmsClient, options) {
2342
+ return wrapKmsClient(kmsClient, this, options);
2343
+ }
2344
+ };
2345
+
2346
+ // src/pilot/pilotToken.ts
2347
+ function encodePilotToken(config) {
2348
+ const baseUrl = config.baseUrl ?? "https://gate.blockintelai.com";
2349
+ const mode = config.mode ?? "SHADOW";
2350
+ const payload = `${config.tenantId}:${config.keyId}:${config.hmacSecret}:${baseUrl}:${mode}`;
2351
+ return `gp_${Buffer.from(payload, "utf-8").toString("base64")}`;
2352
+ }
2353
+ function decodePilotToken(token) {
2354
+ if (!token || !token.startsWith("gp_")) {
2355
+ throw new Error("Invalid pilot token format. Expected GATE_PILOT_TOKEN=gp_<base64>. Get it from: https://gate.blockintelai.com/pilot/setup");
2356
+ }
2357
+ const decoded = Buffer.from(token.slice(3), "base64").toString("utf-8");
2358
+ const parts = decoded.split(":");
2359
+ if (parts.length < 3) {
2360
+ throw new Error("Invalid pilot token: missing tenantId, keyId, or secret");
2361
+ }
2362
+ const [tenantId, keyId, hmacSecret, baseUrl, mode] = parts;
2363
+ return {
2364
+ baseUrl: baseUrl || "https://gate.blockintelai.com",
2365
+ tenantId,
2366
+ auth: { mode: "hmac", keyId, secret: hmacSecret },
2367
+ mode: mode || "SHADOW"
2368
+ };
2369
+ }
2370
+
2371
+ // src/pilot/attestOnly.ts
2372
+ var cachedClient = null;
2373
+ async function attestOnly(request) {
2374
+ const pilotToken = typeof process !== "undefined" ? process.env?.GATE_PILOT_TOKEN : void 0;
2375
+ if (!pilotToken) {
2376
+ throw new Error(
2377
+ "GATE_PILOT_TOKEN env var not set. Get it from: https://gate.blockintelai.com/pilot/setup"
2378
+ );
2379
+ }
2380
+ if (!cachedClient) {
2381
+ const config = decodePilotToken(pilotToken);
2382
+ cachedClient = new GateClient({
2383
+ ...config,
2384
+ mode: "SHADOW",
2385
+ evaluationMode: "FIRE_AND_FORGET"
2386
+ });
2387
+ }
2388
+ return cachedClient.attestCompleted({
2389
+ signature: request.signature,
2390
+ txIntent: request.txIntent,
2391
+ signingContext: request.signingContext
2392
+ });
2393
+ }
2394
+
2395
+ export { attestOnly, decodePilotToken, encodePilotToken };
2396
+ //# sourceMappingURL=index.js.map
2397
+ //# sourceMappingURL=index.js.map