@solongate/sdk 0.1.1

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,927 @@
1
+ import { DEFAULT_INPUT_GUARD_CONFIG, UNSAFE_CONFIGURATION_WARNINGS, createSecurityContext, Permission, RateLimitError, createDeniedToolResult, sanitizeInput, SchemaValidationError, PolicyDeniedError, MIN_SECRET_LENGTH, DEFAULT_TOKEN_TTL_SECONDS, TOKEN_ALGORITHM, RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX_ENTRIES, TrustLevel } from '@solongate/core';
2
+ export { RateLimitError as CoreRateLimitError, InputGuardError, NetworkError, Permission, PolicyDeniedError, PolicyEffect, SchemaValidationError, SolonGateError, TrustLevel, checkEntropyLimits, checkLengthLimits, createDeniedToolResult, createSecurityContext, detectPathTraversal, detectSQLInjection, detectSSRF, detectShellInjection, detectWildcardAbuse, sanitizeInput, validateToolInput } from '@solongate/core';
3
+ import { PolicyStore, PolicyEngine } from '@solongate/policy-engine';
4
+ export { PolicyEngine, PolicyStore, createDefaultDenyPolicySet, createReadOnlyPolicySet } from '@solongate/policy-engine';
5
+ import { randomUUID, createHmac } from 'crypto';
6
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
+
8
+ // src/solongate.ts
9
+ var DEFAULT_CONFIG = Object.freeze({
10
+ validateSchemas: true,
11
+ enableLogging: true,
12
+ logLevel: "info",
13
+ evaluationTimeoutMs: 100,
14
+ verboseErrors: false,
15
+ globalRateLimitPerMinute: 600,
16
+ rateLimitPerTool: 60,
17
+ tokenTtlSeconds: 30,
18
+ inputGuardConfig: DEFAULT_INPUT_GUARD_CONFIG,
19
+ enableVersionedPolicies: true
20
+ });
21
+ function resolveConfig(userConfig) {
22
+ const warnings = [];
23
+ const config = { ...DEFAULT_CONFIG, ...userConfig };
24
+ if (!config.validateSchemas) {
25
+ warnings.push(UNSAFE_CONFIGURATION_WARNINGS.DISABLED_VALIDATION);
26
+ }
27
+ if (config.globalRateLimitPerMinute === 0) {
28
+ warnings.push(UNSAFE_CONFIGURATION_WARNINGS.RATE_LIMIT_ZERO);
29
+ }
30
+ if (config.verboseErrors) {
31
+ warnings.push(
32
+ "Verbose errors enabled: internal error details will be sent to the LLM."
33
+ );
34
+ }
35
+ if (config.tokenSecret && config.tokenSecret.length < 32) {
36
+ warnings.push(
37
+ "Token secret is shorter than 32 characters. Use a longer secret for production."
38
+ );
39
+ }
40
+ return { config, warnings };
41
+ }
42
+ async function interceptToolCall(params, upstreamCall, options) {
43
+ const requestId = randomUUID();
44
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
45
+ const context = createSecurityContext({ requestId });
46
+ const request = {
47
+ context,
48
+ toolName: params.name,
49
+ serverName: "default",
50
+ arguments: params.arguments ?? {},
51
+ requiredPermission: Permission.EXECUTE,
52
+ timestamp
53
+ };
54
+ if (options.rateLimiter) {
55
+ if (options.rateLimitPerTool) {
56
+ const toolLimit = options.rateLimiter.checkLimit(
57
+ params.name,
58
+ options.rateLimitPerTool
59
+ );
60
+ if (!toolLimit.allowed) {
61
+ const result = {
62
+ status: "ERROR",
63
+ request,
64
+ error: new RateLimitError(params.name, options.rateLimitPerTool),
65
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
66
+ };
67
+ options.onDecision?.(result);
68
+ return createDeniedToolResult(
69
+ `Rate limit exceeded for tool "${params.name}"`
70
+ );
71
+ }
72
+ }
73
+ if (options.globalRateLimitPerMinute) {
74
+ const globalLimit = options.rateLimiter.checkGlobalLimit(
75
+ options.globalRateLimitPerMinute
76
+ );
77
+ if (!globalLimit.allowed) {
78
+ const result = {
79
+ status: "ERROR",
80
+ request,
81
+ error: new RateLimitError("*", options.globalRateLimitPerMinute),
82
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
83
+ };
84
+ options.onDecision?.(result);
85
+ return createDeniedToolResult("Global rate limit exceeded");
86
+ }
87
+ }
88
+ }
89
+ if (options.validateSchemas && params.arguments) {
90
+ const guardConfig = options.inputGuardConfig ?? DEFAULT_INPUT_GUARD_CONFIG;
91
+ const sanitization = sanitizeInput("arguments", params.arguments, guardConfig);
92
+ if (!sanitization.safe) {
93
+ const threatDescriptions = sanitization.threats.map(
94
+ (t) => `${t.type}: ${t.description} (field: ${t.field})`
95
+ );
96
+ const result = {
97
+ status: "ERROR",
98
+ request,
99
+ error: new SchemaValidationError(params.name, threatDescriptions),
100
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
101
+ };
102
+ options.onDecision?.(result);
103
+ const reason = options.verboseErrors ? `Input validation failed: ${sanitization.threats.length} threat(s) detected` : "Input validation failed.";
104
+ return createDeniedToolResult(reason);
105
+ }
106
+ }
107
+ const decision = options.policyEngine.evaluate(request);
108
+ if (decision.effect === "DENY") {
109
+ const result = {
110
+ status: "DENIED",
111
+ request,
112
+ decision,
113
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
114
+ };
115
+ options.onDecision?.(result);
116
+ const reason = options.verboseErrors ? decision.reason : "Tool execution denied by security policy.";
117
+ return createDeniedToolResult(reason);
118
+ }
119
+ let capabilityToken;
120
+ if (options.tokenIssuer) {
121
+ capabilityToken = options.tokenIssuer.issue(
122
+ requestId,
123
+ [Permission.EXECUTE],
124
+ [params.name]
125
+ );
126
+ }
127
+ if (options.serverVerifier && capabilityToken) {
128
+ options.serverVerifier.createSignedRequest(params, capabilityToken);
129
+ }
130
+ try {
131
+ const startTime = performance.now();
132
+ const toolResult = await upstreamCall(params);
133
+ const durationMs = performance.now() - startTime;
134
+ if (options.rateLimiter) {
135
+ options.rateLimiter.recordCall(params.name);
136
+ }
137
+ const result = {
138
+ status: "ALLOWED",
139
+ request,
140
+ decision,
141
+ toolResult,
142
+ durationMs,
143
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
144
+ };
145
+ options.onDecision?.(result);
146
+ return toolResult;
147
+ } catch (error) {
148
+ const result = {
149
+ status: "ERROR",
150
+ request,
151
+ error: error instanceof Error ? new PolicyDeniedError(params.name, error.message) : new PolicyDeniedError(params.name, "Unknown upstream error"),
152
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
153
+ };
154
+ options.onDecision?.(result);
155
+ throw error;
156
+ }
157
+ }
158
+
159
+ // src/logger.ts
160
+ var LOG_LEVEL_ORDER = {
161
+ debug: 0,
162
+ info: 1,
163
+ warn: 2,
164
+ error: 3
165
+ };
166
+ var SecurityLogger = class {
167
+ minLevel;
168
+ enabled;
169
+ constructor(options) {
170
+ this.minLevel = options.level;
171
+ this.enabled = options.enabled;
172
+ }
173
+ logDecision(result) {
174
+ if (!this.enabled) return;
175
+ const entry = {
176
+ type: "security_decision",
177
+ status: result.status,
178
+ toolName: result.request.toolName,
179
+ permission: result.request.requiredPermission,
180
+ trustLevel: result.request.context.trustLevel,
181
+ requestId: result.request.context.requestId,
182
+ timestamp: result.timestamp,
183
+ ...result.status === "ALLOWED" && { durationMs: result.durationMs },
184
+ ...result.status === "DENIED" && { reason: result.decision.reason },
185
+ ...result.status === "ERROR" && { error: result.error.code }
186
+ };
187
+ if (result.status === "DENIED" || result.status === "ERROR") {
188
+ this.log("warn", entry);
189
+ } else {
190
+ this.log("info", entry);
191
+ }
192
+ }
193
+ log(level, data) {
194
+ if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[this.minLevel]) return;
195
+ const output = JSON.stringify({ level, ...data });
196
+ switch (level) {
197
+ case "error":
198
+ console.error(`[SolonGate] ${output}`);
199
+ break;
200
+ case "warn":
201
+ console.warn(`[SolonGate] ${output}`);
202
+ break;
203
+ case "debug":
204
+ console.debug(`[SolonGate] ${output}`);
205
+ break;
206
+ default:
207
+ console.info(`[SolonGate] ${output}`);
208
+ }
209
+ }
210
+ };
211
+ var TokenIssuer = class {
212
+ secret;
213
+ ttlSeconds;
214
+ issuer;
215
+ usedNonces = /* @__PURE__ */ new Set();
216
+ revokedTokens = /* @__PURE__ */ new Set();
217
+ constructor(config) {
218
+ if (config.secret.length < MIN_SECRET_LENGTH) {
219
+ throw new Error(
220
+ `Token secret must be at least ${MIN_SECRET_LENGTH} characters`
221
+ );
222
+ }
223
+ this.secret = config.secret;
224
+ this.ttlSeconds = config.ttlSeconds || DEFAULT_TOKEN_TTL_SECONDS;
225
+ this.issuer = config.issuer;
226
+ }
227
+ /**
228
+ * Issues a signed capability token.
229
+ */
230
+ issue(requestId, permissions, toolScope, serverScope = ["*"], pathScope) {
231
+ const now = Math.floor(Date.now() / 1e3);
232
+ const jti = randomUUID();
233
+ const payload = {
234
+ jti,
235
+ iss: this.issuer,
236
+ sub: requestId,
237
+ iat: now,
238
+ exp: now + this.ttlSeconds,
239
+ permissions: [...permissions],
240
+ toolScope: [...toolScope],
241
+ serverScope: [...serverScope],
242
+ ...pathScope && { pathScope: [...pathScope] }
243
+ };
244
+ return this.sign(payload);
245
+ }
246
+ /**
247
+ * Verifies a capability token and consumes the nonce (single-use).
248
+ */
249
+ verify(token) {
250
+ const parsed = this.parseAndVerify(token);
251
+ if (!parsed.valid || !parsed.payload) {
252
+ return parsed;
253
+ }
254
+ const payload = parsed.payload;
255
+ const now = Math.floor(Date.now() / 1e3);
256
+ if (payload.exp <= now) {
257
+ return { valid: false, reason: "Token expired" };
258
+ }
259
+ if (this.revokedTokens.has(payload.jti)) {
260
+ return { valid: false, reason: "Token has been revoked" };
261
+ }
262
+ if (this.usedNonces.has(payload.jti)) {
263
+ return { valid: false, reason: "Token already used (replay detected)" };
264
+ }
265
+ this.usedNonces.add(payload.jti);
266
+ return { valid: true, payload };
267
+ }
268
+ /**
269
+ * Revokes a token by its ID.
270
+ */
271
+ revoke(jti) {
272
+ this.revokedTokens.add(jti);
273
+ }
274
+ /**
275
+ * Checks if a token ID has been revoked.
276
+ */
277
+ isRevoked(jti) {
278
+ return this.revokedTokens.has(jti);
279
+ }
280
+ // --- Internal helpers ---
281
+ sign(payload) {
282
+ const header = base64UrlEncode(JSON.stringify({ alg: TOKEN_ALGORITHM, typ: "JWT" }));
283
+ const body = base64UrlEncode(JSON.stringify(payload));
284
+ const signature = this.computeSignature(`${header}.${body}`);
285
+ return `${header}.${body}.${signature}`;
286
+ }
287
+ parseAndVerify(token) {
288
+ const parts = token.split(".");
289
+ if (parts.length !== 3) {
290
+ return { valid: false, reason: "Invalid token format" };
291
+ }
292
+ const [header, body, signature] = parts;
293
+ const expectedSignature = this.computeSignature(`${header}.${body}`);
294
+ if (signature !== expectedSignature) {
295
+ return { valid: false, reason: "Invalid token signature" };
296
+ }
297
+ try {
298
+ const payload = JSON.parse(base64UrlDecode(body));
299
+ return { valid: true, payload };
300
+ } catch {
301
+ return { valid: false, reason: "Invalid token payload" };
302
+ }
303
+ }
304
+ computeSignature(data) {
305
+ return base64UrlEncode(
306
+ createHmac("sha256", this.secret).update(data).digest("base64")
307
+ );
308
+ }
309
+ };
310
+ function base64UrlEncode(str) {
311
+ return Buffer.from(str).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
312
+ }
313
+ function base64UrlDecode(str) {
314
+ const padded = str + "=".repeat((4 - str.length % 4) % 4);
315
+ return Buffer.from(padded.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString();
316
+ }
317
+ var ServerVerifier = class {
318
+ gatewaySecret;
319
+ maxAgeMs;
320
+ usedNonces = /* @__PURE__ */ new Set();
321
+ constructor(config) {
322
+ if (config.gatewaySecret.length < 32) {
323
+ throw new Error("Gateway secret must be at least 32 characters");
324
+ }
325
+ this.gatewaySecret = config.gatewaySecret;
326
+ this.maxAgeMs = config.maxAgeMs ?? 6e4;
327
+ }
328
+ /**
329
+ * Computes HMAC signature for request data.
330
+ */
331
+ signRequest(params, capabilityToken) {
332
+ const data = JSON.stringify({ params, capabilityToken });
333
+ return createHmac("sha256", this.gatewaySecret).update(data).digest("hex");
334
+ }
335
+ /**
336
+ * Verifies the HMAC signature of request data.
337
+ */
338
+ verifySignature(params, capabilityToken, signature) {
339
+ const expected = this.signRequest(params, capabilityToken);
340
+ if (expected.length !== signature.length) return false;
341
+ let result = 0;
342
+ for (let i = 0; i < expected.length; i++) {
343
+ result |= expected.charCodeAt(i) ^ signature.charCodeAt(i);
344
+ }
345
+ return result === 0;
346
+ }
347
+ /**
348
+ * Creates a complete signed request including timestamp and nonce.
349
+ */
350
+ createSignedRequest(params, capabilityToken) {
351
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
352
+ const nonce = randomUUID();
353
+ const signature = this.signRequest(params, capabilityToken);
354
+ return {
355
+ params,
356
+ capabilityToken,
357
+ signature,
358
+ timestamp,
359
+ nonce
360
+ };
361
+ }
362
+ /**
363
+ * Validates a complete signed request including timestamp, nonce, and signature.
364
+ */
365
+ validateSignedRequest(request) {
366
+ const requestTime = new Date(request.timestamp).getTime();
367
+ const now = Date.now();
368
+ if (isNaN(requestTime)) {
369
+ return { valid: false, reason: "Invalid timestamp" };
370
+ }
371
+ if (now - requestTime > this.maxAgeMs) {
372
+ return { valid: false, reason: "Request too old" };
373
+ }
374
+ if (requestTime > now + 3e4) {
375
+ return { valid: false, reason: "Request timestamp in the future" };
376
+ }
377
+ if (this.usedNonces.has(request.nonce)) {
378
+ return { valid: false, reason: "Duplicate nonce (replay detected)" };
379
+ }
380
+ if (!this.verifySignature(request.params, request.capabilityToken, request.signature)) {
381
+ return { valid: false, reason: "Invalid signature" };
382
+ }
383
+ this.usedNonces.add(request.nonce);
384
+ return { valid: true };
385
+ }
386
+ };
387
+ var RateLimiter = class {
388
+ windowMs;
389
+ records = /* @__PURE__ */ new Map();
390
+ globalRecords = [];
391
+ constructor(options) {
392
+ this.windowMs = options?.windowMs ?? RATE_LIMIT_WINDOW_MS;
393
+ }
394
+ /**
395
+ * Checks if a tool call is within the rate limit.
396
+ * Does NOT record the call - use recordCall() after successful execution.
397
+ */
398
+ checkLimit(toolName, limitPerWindow) {
399
+ const now = Date.now();
400
+ const windowStart = now - this.windowMs;
401
+ const records = this.getActiveRecords(toolName, windowStart);
402
+ const count = records.length;
403
+ const allowed = count < limitPerWindow;
404
+ const remaining = Math.max(0, limitPerWindow - count);
405
+ const resetAt = records.length > 0 ? records[0].timestamp + this.windowMs : now + this.windowMs;
406
+ return { allowed, remaining, resetAt };
407
+ }
408
+ /**
409
+ * Checks the global rate limit across all tools.
410
+ */
411
+ checkGlobalLimit(limitPerWindow) {
412
+ const now = Date.now();
413
+ const windowStart = now - this.windowMs;
414
+ this.globalRecords = this.globalRecords.filter(
415
+ (r) => r.timestamp > windowStart
416
+ );
417
+ const count = this.globalRecords.length;
418
+ const allowed = count < limitPerWindow;
419
+ const remaining = Math.max(0, limitPerWindow - count);
420
+ const resetAt = this.globalRecords.length > 0 ? this.globalRecords[0].timestamp + this.windowMs : now + this.windowMs;
421
+ return { allowed, remaining, resetAt };
422
+ }
423
+ /**
424
+ * Atomically checks and records a tool call.
425
+ * Prevents TOCTOU race conditions between check and record.
426
+ * Returns the rate limit result; if allowed, the call is already recorded.
427
+ */
428
+ checkAndRecord(toolName, limitPerWindow, globalLimit) {
429
+ const result = this.checkLimit(toolName, limitPerWindow);
430
+ if (!result.allowed) {
431
+ return result;
432
+ }
433
+ if (globalLimit !== void 0) {
434
+ const globalResult = this.checkGlobalLimit(globalLimit);
435
+ if (!globalResult.allowed) {
436
+ return globalResult;
437
+ }
438
+ }
439
+ this.recordCall(toolName);
440
+ return result;
441
+ }
442
+ /**
443
+ * Records a tool call for rate limiting.
444
+ * Call this after successful execution.
445
+ */
446
+ recordCall(toolName) {
447
+ const now = Date.now();
448
+ const record = { timestamp: now };
449
+ const records = this.records.get(toolName) ?? [];
450
+ records.push(record);
451
+ if (records.length > RATE_LIMIT_MAX_ENTRIES) {
452
+ const windowStart = now - this.windowMs;
453
+ const cleaned = records.filter((r) => r.timestamp > windowStart);
454
+ this.records.set(toolName, cleaned);
455
+ } else {
456
+ this.records.set(toolName, records);
457
+ }
458
+ this.globalRecords.push(record);
459
+ if (this.globalRecords.length > RATE_LIMIT_MAX_ENTRIES) {
460
+ const windowStart = now - this.windowMs;
461
+ this.globalRecords = this.globalRecords.filter(
462
+ (r) => r.timestamp > windowStart
463
+ );
464
+ }
465
+ }
466
+ /**
467
+ * Gets usage stats for a tool.
468
+ */
469
+ getUsage(toolName) {
470
+ const now = Date.now();
471
+ const windowStart = now - this.windowMs;
472
+ const records = this.getActiveRecords(toolName, windowStart);
473
+ return { count: records.length, windowStart };
474
+ }
475
+ /**
476
+ * Resets rate tracking for a specific tool.
477
+ */
478
+ resetTool(toolName) {
479
+ this.records.delete(toolName);
480
+ }
481
+ /**
482
+ * Resets all rate tracking.
483
+ */
484
+ resetAll() {
485
+ this.records.clear();
486
+ this.globalRecords = [];
487
+ }
488
+ getActiveRecords(toolName, windowStart) {
489
+ const records = this.records.get(toolName) ?? [];
490
+ const active = records.filter((r) => r.timestamp > windowStart);
491
+ if (active.length !== records.length) {
492
+ this.records.set(toolName, active);
493
+ }
494
+ return active;
495
+ }
496
+ };
497
+
498
+ // src/solongate.ts
499
+ var LicenseError = class extends Error {
500
+ constructor(message) {
501
+ super(
502
+ `${message}
503
+ Get your API key at https://solongate.com
504
+ Usage: new SolonGate({ name: '...', apiKey: 'sg_live_xxx' })`
505
+ );
506
+ this.name = "LicenseError";
507
+ }
508
+ };
509
+ var SolonGate = class {
510
+ policyEngine;
511
+ config;
512
+ logger;
513
+ configWarnings;
514
+ tokenIssuer;
515
+ serverVerifier;
516
+ rateLimiter;
517
+ apiKey;
518
+ licenseValidated = false;
519
+ constructor(options) {
520
+ const apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
521
+ if (!apiKey) {
522
+ throw new LicenseError("A valid SolonGate API key is required.");
523
+ }
524
+ if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
525
+ throw new LicenseError(
526
+ "Invalid API key format. Keys must start with 'sg_live_' or 'sg_test_'."
527
+ );
528
+ }
529
+ this.apiKey = apiKey;
530
+ const { config, warnings } = resolveConfig(options.config);
531
+ this.config = config;
532
+ this.configWarnings = warnings;
533
+ this.logger = new SecurityLogger({
534
+ level: config.logLevel,
535
+ enabled: config.enableLogging
536
+ });
537
+ for (const warning of warnings) {
538
+ console.warn(`[SolonGate] WARNING: ${warning}`);
539
+ }
540
+ const store = config.enableVersionedPolicies ? new PolicyStore() : void 0;
541
+ this.policyEngine = new PolicyEngine({
542
+ policySet: options.policySet ?? config.policySet,
543
+ timeoutMs: config.evaluationTimeoutMs,
544
+ store
545
+ });
546
+ this.tokenIssuer = config.tokenSecret ? new TokenIssuer({
547
+ secret: config.tokenSecret,
548
+ ttlSeconds: config.tokenTtlSeconds,
549
+ algorithm: TOKEN_ALGORITHM,
550
+ issuer: config.tokenIssuer ?? options.name
551
+ }) : null;
552
+ this.serverVerifier = config.gatewaySecret ? new ServerVerifier({ gatewaySecret: config.gatewaySecret }) : null;
553
+ this.rateLimiter = new RateLimiter();
554
+ }
555
+ /**
556
+ * Validate the API key against the SolonGate cloud API.
557
+ * Called once on first executeToolCall. Throws LicenseError if invalid.
558
+ * Test keys (sg_test_) skip online validation.
559
+ */
560
+ async validateLicense() {
561
+ if (this.licenseValidated) return;
562
+ if (this.apiKey.startsWith("sg_test_")) {
563
+ this.licenseValidated = true;
564
+ return;
565
+ }
566
+ const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
567
+ try {
568
+ const res = await fetch(`${apiUrl}/api/v1/auth/me`, {
569
+ headers: {
570
+ "X-API-Key": this.apiKey,
571
+ "Authorization": `Bearer ${this.apiKey}`
572
+ },
573
+ signal: AbortSignal.timeout(1e4)
574
+ });
575
+ if (res.status === 401) {
576
+ throw new LicenseError("Invalid or expired API key.");
577
+ }
578
+ if (res.status === 403) {
579
+ throw new LicenseError("Your subscription is inactive. Renew at https://solongate.com");
580
+ }
581
+ this.licenseValidated = true;
582
+ } catch (err) {
583
+ if (err instanceof LicenseError) throw err;
584
+ throw new LicenseError(
585
+ "Unable to reach SolonGate license server. Check your internet connection."
586
+ );
587
+ }
588
+ }
589
+ /**
590
+ * Intercept and evaluate a tool call against the full security pipeline.
591
+ * If denied at any stage, returns an error result without calling upstream.
592
+ * If allowed, calls upstream and returns the result.
593
+ */
594
+ async executeToolCall(params, upstreamCall) {
595
+ await this.validateLicense();
596
+ return interceptToolCall(params, upstreamCall, {
597
+ policyEngine: this.policyEngine,
598
+ validateSchemas: this.config.validateSchemas,
599
+ verboseErrors: this.config.verboseErrors,
600
+ onDecision: (result) => this.logger.logDecision(result),
601
+ tokenIssuer: this.tokenIssuer ?? void 0,
602
+ serverVerifier: this.serverVerifier ?? void 0,
603
+ rateLimiter: this.rateLimiter,
604
+ inputGuardConfig: this.config.inputGuardConfig,
605
+ rateLimitPerTool: this.config.rateLimitPerTool,
606
+ globalRateLimitPerMinute: this.config.globalRateLimitPerMinute
607
+ });
608
+ }
609
+ /** Load a new policy set at runtime. */
610
+ loadPolicy(policySet, options) {
611
+ return this.policyEngine.loadPolicySet(policySet, options);
612
+ }
613
+ /** Get current security warnings. */
614
+ getWarnings() {
615
+ return [
616
+ ...this.configWarnings,
617
+ ...this.policyEngine.getSecurityWarnings().map((w) => `[${w.level}] ${w.message}`)
618
+ ];
619
+ }
620
+ /** Get the policy engine for direct access. */
621
+ getPolicyEngine() {
622
+ return this.policyEngine;
623
+ }
624
+ /** Get the rate limiter for direct access. */
625
+ getRateLimiter() {
626
+ return this.rateLimiter;
627
+ }
628
+ /** Get the token issuer (null if not configured). */
629
+ getTokenIssuer() {
630
+ return this.tokenIssuer;
631
+ }
632
+ };
633
+ var SecureMcpServer = class extends McpServer {
634
+ gate;
635
+ /**
636
+ * Create a secure MCP server.
637
+ *
638
+ * @param serverInfo - MCP server info (name, version)
639
+ * @param solongateOptions - SolonGate security options
640
+ * @param mcpOptions - Standard McpServer options (capabilities, etc.)
641
+ */
642
+ constructor(serverInfo, solongateOptions, mcpOptions) {
643
+ super(serverInfo, mcpOptions);
644
+ this.gate = new SolonGate({
645
+ name: serverInfo.name,
646
+ version: serverInfo.version,
647
+ apiKey: solongateOptions?.apiKey,
648
+ policySet: solongateOptions?.policySet,
649
+ config: solongateOptions?.config
650
+ });
651
+ const warnings = this.gate.getWarnings();
652
+ for (const w of warnings) {
653
+ console.warn(`[SolonGate] ${w}`);
654
+ }
655
+ }
656
+ /**
657
+ * Override tool() to auto-wrap handlers with SolonGate security pipeline.
658
+ *
659
+ * Supports all McpServer.tool() overloads — the handler (always the last
660
+ * argument) is transparently wrapped. Tool name, description, schema, and
661
+ * annotations pass through unchanged.
662
+ */
663
+ tool(name, ...rest) {
664
+ const handler = rest[rest.length - 1];
665
+ if (typeof handler !== "function") {
666
+ return super.tool.call(this, name, ...rest);
667
+ }
668
+ const toolName = name;
669
+ const gate = this.gate;
670
+ rest[rest.length - 1] = async (...callArgs) => {
671
+ const toolArgs = callArgs.length > 1 && typeof callArgs[0] === "object" && callArgs[0] !== null ? callArgs[0] : {};
672
+ const result = await gate.executeToolCall(
673
+ { name: toolName, arguments: toolArgs },
674
+ async () => handler(...callArgs)
675
+ );
676
+ return { ...result, content: [...result.content] };
677
+ };
678
+ return super.tool.call(this, name, ...rest);
679
+ }
680
+ /**
681
+ * Override registerTool() to auto-wrap handlers with SolonGate security pipeline.
682
+ *
683
+ * This is the modern (non-deprecated) API for registering tools.
684
+ */
685
+ registerTool(name, config, cb) {
686
+ if (typeof cb !== "function") {
687
+ return super.registerTool.call(this, name, config, cb);
688
+ }
689
+ const toolName = name;
690
+ const gate = this.gate;
691
+ const wrappedCb = async (...callArgs) => {
692
+ const toolArgs = callArgs.length > 1 && typeof callArgs[0] === "object" && callArgs[0] !== null ? callArgs[0] : {};
693
+ const result = await gate.executeToolCall(
694
+ { name: toolName, arguments: toolArgs },
695
+ async () => cb(...callArgs)
696
+ );
697
+ return { ...result, content: [...result.content] };
698
+ };
699
+ return super.registerTool.call(this, name, config, wrappedCb);
700
+ }
701
+ /** Get the underlying SolonGate instance for direct access. */
702
+ getSolonGate() {
703
+ return this.gate;
704
+ }
705
+ };
706
+ var DEFAULT_API_URL = "https://api.solongate.com";
707
+ var API_VERSION = "v1";
708
+ var SDK_VERSION = "0.2.0";
709
+ var APIError = class extends Error {
710
+ constructor(message, statusCode, requestId, code = "API_ERROR") {
711
+ super(message);
712
+ this.statusCode = statusCode;
713
+ this.requestId = requestId;
714
+ this.code = code;
715
+ this.name = "APIError";
716
+ }
717
+ };
718
+ var AuthenticationError = class extends APIError {
719
+ constructor(message = "Invalid API key") {
720
+ super(message, 401, void 0, "AUTHENTICATION_ERROR");
721
+ this.name = "AuthenticationError";
722
+ }
723
+ };
724
+ var RateLimitError2 = class extends APIError {
725
+ constructor(message, retryAfter) {
726
+ super(message, 429, void 0, "RATE_LIMIT_ERROR");
727
+ this.retryAfter = retryAfter;
728
+ this.name = "RateLimitError";
729
+ }
730
+ };
731
+ var PoliciesResource = class {
732
+ constructor(client) {
733
+ this.client = client;
734
+ }
735
+ async get(policyId = "default", version) {
736
+ const params = version ? `?version=${version}` : "";
737
+ return this.client.request("GET", `/policies/${policyId}${params}`);
738
+ }
739
+ async list() {
740
+ return this.client.request("GET", "/policies");
741
+ }
742
+ async create(policy) {
743
+ return this.client.request("POST", "/policies", policy);
744
+ }
745
+ async update(policyId, policy) {
746
+ return this.client.request("PUT", `/policies/${policyId}`, policy);
747
+ }
748
+ };
749
+ var TokensResource = class {
750
+ constructor(client) {
751
+ this.client = client;
752
+ }
753
+ async create(tool, scope, ttlSeconds = 30) {
754
+ const response = await this.client.request("POST", "/tokens", {
755
+ tool,
756
+ scope: scope || `EXECUTE:${tool}`,
757
+ ttl_seconds: ttlSeconds
758
+ });
759
+ return {
760
+ token: response.token,
761
+ tool: response.tool,
762
+ scope: response.scope,
763
+ expiresAt: response.expires_at,
764
+ nonce: response.nonce
765
+ };
766
+ }
767
+ async verify(token) {
768
+ return this.client.request("POST", "/tokens/verify", { token });
769
+ }
770
+ };
771
+ var ToolsResource = class {
772
+ constructor(client) {
773
+ this.client = client;
774
+ }
775
+ async list() {
776
+ return this.client.request("GET", "/tools");
777
+ }
778
+ async get(name) {
779
+ return this.client.request("GET", `/tools/${name}`);
780
+ }
781
+ async register(name, description, inputSchema, permissions = ["READ"]) {
782
+ return this.client.request("POST", "/tools", {
783
+ name,
784
+ description,
785
+ input_schema: inputSchema,
786
+ permissions
787
+ });
788
+ }
789
+ async update(name, data) {
790
+ return this.client.request("PUT", `/tools/${name}`, data);
791
+ }
792
+ async delete(name) {
793
+ return this.client.request("DELETE", `/tools/${name}`);
794
+ }
795
+ };
796
+ var SolonGateAPI = class {
797
+ apiKey;
798
+ apiUrl;
799
+ timeout;
800
+ maxRetries;
801
+ policies;
802
+ tokens;
803
+ tools;
804
+ constructor(config) {
805
+ if (typeof config === "string") {
806
+ config = { apiKey: config };
807
+ }
808
+ this.apiKey = config.apiKey || (typeof process !== "undefined" ? process.env.SOLONGATE_API_KEY : "") || "";
809
+ if (!this.apiKey) {
810
+ throw new AuthenticationError(
811
+ "API key is required. Provide apiKey in config or set SOLONGATE_API_KEY environment variable."
812
+ );
813
+ }
814
+ if (!this.apiKey.startsWith("sg_live_") && !this.apiKey.startsWith("sg_test_")) {
815
+ throw new AuthenticationError(
816
+ "Invalid API key format. Keys should start with 'sg_live_' or 'sg_test_'"
817
+ );
818
+ }
819
+ this.apiUrl = config.apiUrl || DEFAULT_API_URL;
820
+ this.timeout = config.timeout || 3e4;
821
+ this.maxRetries = config.maxRetries || 3;
822
+ this.policies = new PoliciesResource(this);
823
+ this.tokens = new TokensResource(this);
824
+ this.tools = new ToolsResource(this);
825
+ }
826
+ /**
827
+ * Make an API request.
828
+ * @internal
829
+ */
830
+ async request(method, path, body) {
831
+ const url = `${this.apiUrl}/api/${API_VERSION}${path}`;
832
+ let lastError;
833
+ for (let attempt = 0; attempt < this.maxRetries; attempt++) {
834
+ try {
835
+ const controller = new AbortController();
836
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
837
+ const response = await fetch(url, {
838
+ method,
839
+ headers: {
840
+ "X-API-Key": this.apiKey,
841
+ "Authorization": `Bearer ${this.apiKey}`,
842
+ "Content-Type": "application/json",
843
+ "User-Agent": `solongate-js/${SDK_VERSION}`
844
+ },
845
+ body: body ? JSON.stringify(body) : void 0,
846
+ signal: controller.signal
847
+ });
848
+ clearTimeout(timeoutId);
849
+ if (response.status === 429) {
850
+ const retryAfter = parseInt(response.headers.get("Retry-After") || "1");
851
+ await new Promise((resolve) => setTimeout(resolve, retryAfter * 1e3));
852
+ continue;
853
+ }
854
+ if (response.status === 401) {
855
+ throw new AuthenticationError("Invalid API key");
856
+ }
857
+ if (!response.ok) {
858
+ const errorData = await response.json().catch(() => ({}));
859
+ throw new APIError(
860
+ errorData.error?.message || "Unknown error",
861
+ response.status,
862
+ response.headers.get("X-Request-Id") || void 0
863
+ );
864
+ }
865
+ return await response.json();
866
+ } catch (error) {
867
+ if (error instanceof APIError || error instanceof AuthenticationError) {
868
+ throw error;
869
+ }
870
+ lastError = error;
871
+ }
872
+ }
873
+ throw new APIError(lastError?.message || "Request failed");
874
+ }
875
+ /**
876
+ * Validate a tool call against policies.
877
+ *
878
+ * @example
879
+ * ```typescript
880
+ * const result = await api.validate('file.read', { path: '/home/user/doc.txt' });
881
+ * if (result.allowed) {
882
+ * // Proceed with the tool call
883
+ * }
884
+ * ```
885
+ */
886
+ async validate(tool, args, options = {}) {
887
+ const startTime = performance.now();
888
+ const response = await this.request("POST", "/validate", {
889
+ tool,
890
+ arguments: args,
891
+ trust_level: options.trustLevel || TrustLevel.VERIFIED,
892
+ include_token: options.includeToken !== false
893
+ });
894
+ const latencyMs = performance.now() - startTime;
895
+ return {
896
+ allowed: response.allowed,
897
+ tool,
898
+ decision: response.decision ? {
899
+ effect: response.decision.effect,
900
+ matchedRule: response.decision.matched_rule,
901
+ reason: response.decision.reason,
902
+ timestamp: response.decision.evaluated_at,
903
+ evaluationTimeMs: 0
904
+ } : void 0,
905
+ token: response.token,
906
+ tokenExpiresAt: response.token_expires_at,
907
+ requestId: response.request_id,
908
+ latencyMs
909
+ };
910
+ }
911
+ /**
912
+ * Check if using live (production) API key.
913
+ */
914
+ isLiveMode() {
915
+ return this.apiKey.startsWith("sg_live_");
916
+ }
917
+ /**
918
+ * Check if using test (development) API key.
919
+ */
920
+ isTestMode() {
921
+ return this.apiKey.startsWith("sg_test_");
922
+ }
923
+ };
924
+
925
+ export { APIError, AuthenticationError, DEFAULT_CONFIG, LicenseError, RateLimitError2 as RateLimitError, RateLimiter, SecureMcpServer, SecurityLogger, ServerVerifier, SolonGate, SolonGateAPI, TokenIssuer, interceptToolCall, resolveConfig };
926
+ //# sourceMappingURL=index.js.map
927
+ //# sourceMappingURL=index.js.map