@kya-os/mcp-i 1.5.6-canary.3 → 1.5.8-canary.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.
@@ -1,4 +1,20 @@
1
1
  import { NonceCache } from "@kya-os/contracts/handshake";
2
+ /**
3
+ * Cloudflare KV namespace interface
4
+ */
5
+ interface KVNamespace {
6
+ get(key: string, options?: {
7
+ type?: string;
8
+ }): Promise<string | null>;
9
+ getWithMetadata?(key: string): Promise<{
10
+ value: string | null;
11
+ metadata?: unknown;
12
+ }>;
13
+ put(key: string, value: string, options?: {
14
+ expirationTtl?: number;
15
+ }): Promise<void>;
16
+ delete(key: string): Promise<void>;
17
+ }
2
18
  /**
3
19
  * Cloudflare KV-based nonce cache implementation
4
20
  * Suitable for Cloudflare Workers deployments
@@ -6,9 +22,10 @@ import { NonceCache } from "@kya-os/contracts/handshake";
6
22
  export declare class CloudflareKVNonceCache implements NonceCache {
7
23
  private kv;
8
24
  private keyPrefix;
9
- constructor(kv: any, keyPrefix?: string);
25
+ constructor(kv: KVNamespace, keyPrefix?: string);
10
26
  private getKey;
11
- has(nonce: string): Promise<boolean>;
12
- add(nonce: string, ttl: number): Promise<void>;
27
+ has(nonce: string, agentDid?: string): Promise<boolean>;
28
+ add(nonce: string, ttl: number, agentDid?: string): Promise<void>;
13
29
  cleanup(): Promise<void>;
14
30
  }
31
+ export {};
@@ -6,17 +6,20 @@ exports.CloudflareKVNonceCache = void 0;
6
6
  * Suitable for Cloudflare Workers deployments
7
7
  */
8
8
  class CloudflareKVNonceCache {
9
- kv; // KV namespace - type depends on Cloudflare Workers runtime
9
+ kv;
10
10
  keyPrefix;
11
11
  constructor(kv, keyPrefix = "nonce:") {
12
12
  this.kv = kv;
13
13
  this.keyPrefix = keyPrefix;
14
14
  }
15
- getKey(nonce) {
15
+ getKey(nonce, agentDid) {
16
+ if (agentDid) {
17
+ return `${this.keyPrefix}${agentDid}:${nonce}`;
18
+ }
16
19
  return `${this.keyPrefix}${nonce}`;
17
20
  }
18
- async has(nonce) {
19
- const key = this.getKey(nonce);
21
+ async has(nonce, agentDid) {
22
+ const key = this.getKey(nonce, agentDid);
20
23
  const value = await this.kv.get(key);
21
24
  if (!value) {
22
25
  return false;
@@ -37,8 +40,8 @@ class CloudflareKVNonceCache {
37
40
  return false;
38
41
  }
39
42
  }
40
- async add(nonce, ttl) {
41
- const key = this.getKey(nonce);
43
+ async add(nonce, ttl, agentDid) {
44
+ const key = this.getKey(nonce, agentDid);
42
45
  const expiresAt = Date.now() + ttl * 1000;
43
46
  const data = {
44
47
  nonce,
@@ -48,8 +51,10 @@ class CloudflareKVNonceCache {
48
51
  // Cloudflare KV doesn't have native atomic add-if-absent
49
52
  // We implement a best-effort approach with metadata checking
50
53
  try {
51
- // First, try to get the existing value with metadata
52
- const existingWithMetadata = await this.kv.getWithMetadata(key);
54
+ // First, try to get the existing value with metadata (if available)
55
+ const existingWithMetadata = this.kv.getWithMetadata
56
+ ? await this.kv.getWithMetadata(key)
57
+ : null;
53
58
  if (existingWithMetadata && existingWithMetadata.value !== null) {
54
59
  // Key exists, check if it's still valid
55
60
  const existingData = JSON.parse(existingWithMetadata.value);
@@ -65,13 +70,13 @@ class CloudflareKVNonceCache {
65
70
  }
66
71
  catch (error) {
67
72
  // If this is a replay attack error, always rethrow it
68
- if (error.message?.includes("potential replay attack")) {
73
+ if (error instanceof Error && error.message?.includes("potential replay attack")) {
69
74
  throw error;
70
75
  }
71
76
  // If getWithMetadata is not available, fall back to basic approach
72
- if (error.message?.includes("getWithMetadata")) {
77
+ if (error instanceof Error && error.message?.includes("getWithMetadata")) {
73
78
  // Check if already exists first (less atomic but still functional)
74
- if (await this.has(nonce)) {
79
+ if (await this.has(nonce, agentDid)) {
75
80
  throw new Error(`Nonce ${nonce} already exists - potential replay attack`);
76
81
  }
77
82
  // Store with KV TTL as backup (convert to seconds)
@@ -11,7 +11,7 @@ import type { NonceCache } from "@kya-os/contracts/handshake";
11
11
  export interface KVNamespace {
12
12
  get(key: string, options?: {
13
13
  type?: "text" | "json" | "arrayBuffer" | "stream";
14
- }): Promise<any>;
14
+ }): Promise<string | null>;
15
15
  put(key: string, value: string | ArrayBuffer | ReadableStream, options?: {
16
16
  expiration?: number;
17
17
  expirationTtl?: number;
@@ -71,17 +71,19 @@ export declare class CloudflareKVNonceCache implements NonceCache {
71
71
  * Check if a nonce exists in the cache
72
72
  *
73
73
  * @param nonce - The nonce to check
74
+ * @param agentDid - Optional agent DID for agent-scoped nonces (prevents cross-agent replay attacks)
74
75
  * @returns Promise<boolean> - true if exists, false if not
75
76
  */
76
- has(nonce: string): Promise<boolean>;
77
+ has(nonce: string, agentDid?: string): Promise<boolean>;
77
78
  /**
78
79
  * Add a nonce to the cache with TTL
79
80
  * Implements atomic add-if-absent semantics for replay prevention
80
81
  *
81
82
  * @param nonce - The nonce to add
82
83
  * @param ttl - Time-to-live in seconds
84
+ * @param agentDid - Optional agent DID for agent-scoped nonces (prevents cross-agent replay attacks)
83
85
  */
84
- add(nonce: string, ttl: number): Promise<void>;
86
+ add(nonce: string, ttl: number, agentDid?: string): Promise<void>;
85
87
  /**
86
88
  * Cleanup expired nonces
87
89
  * Note: Cloudflare KV handles expiration automatically via TTL
@@ -44,10 +44,11 @@ class CloudflareKVNonceCache {
44
44
  * Check if a nonce exists in the cache
45
45
  *
46
46
  * @param nonce - The nonce to check
47
+ * @param agentDid - Optional agent DID for agent-scoped nonces (prevents cross-agent replay attacks)
47
48
  * @returns Promise<boolean> - true if exists, false if not
48
49
  */
49
- async has(nonce) {
50
- const key = this.keyPrefix + nonce;
50
+ async has(nonce, agentDid) {
51
+ const key = agentDid ? `${this.keyPrefix}${agentDid}:${nonce}` : this.keyPrefix + nonce;
51
52
  try {
52
53
  const existing = await this.kv.get(key, { type: "text" });
53
54
  return existing !== null;
@@ -64,9 +65,10 @@ class CloudflareKVNonceCache {
64
65
  *
65
66
  * @param nonce - The nonce to add
66
67
  * @param ttl - Time-to-live in seconds
68
+ * @param agentDid - Optional agent DID for agent-scoped nonces (prevents cross-agent replay attacks)
67
69
  */
68
- async add(nonce, ttl) {
69
- const key = this.keyPrefix + nonce;
70
+ async add(nonce, ttl, agentDid) {
71
+ const key = agentDid ? `${this.keyPrefix}${agentDid}:${nonce}` : this.keyPrefix + nonce;
70
72
  try {
71
73
  // Check if nonce already exists
72
74
  const existing = await this.kv.get(key, { type: "text" });
@@ -1,4 +1,9 @@
1
1
  import { NonceCache } from "@kya-os/contracts/handshake";
2
+ /**
3
+ * DynamoDB client interface for SDK v3
4
+ * Using 'unknown' for the client type to work with any AWS SDK version
5
+ */
6
+ type DynamoDBClient = any;
2
7
  /**
3
8
  * DynamoDB-based nonce cache implementation
4
9
  * Suitable for AWS Lambda deployments
@@ -8,8 +13,10 @@ export declare class DynamoNonceCache implements NonceCache {
8
13
  private tableName;
9
14
  private keyAttribute;
10
15
  private ttlAttribute;
11
- constructor(dynamodb: any, tableName: string, keyAttribute?: string, ttlAttribute?: string);
12
- has(nonce: string): Promise<boolean>;
13
- add(nonce: string, ttl: number): Promise<void>;
16
+ constructor(dynamodb: DynamoDBClient, tableName: string, keyAttribute?: string, ttlAttribute?: string);
17
+ private getKey;
18
+ has(nonce: string, agentDid?: string): Promise<boolean>;
19
+ add(nonce: string, ttl: number, agentDid?: string): Promise<void>;
14
20
  cleanup(): Promise<void>;
15
21
  }
22
+ export {};
@@ -6,7 +6,7 @@ exports.DynamoNonceCache = void 0;
6
6
  * Suitable for AWS Lambda deployments
7
7
  */
8
8
  class DynamoNonceCache {
9
- dynamodb; // DynamoDB client - type depends on AWS SDK version
9
+ dynamodb;
10
10
  tableName;
11
11
  keyAttribute;
12
12
  ttlAttribute;
@@ -16,13 +16,20 @@ class DynamoNonceCache {
16
16
  this.keyAttribute = keyAttribute;
17
17
  this.ttlAttribute = ttlAttribute;
18
18
  }
19
- async has(nonce) {
19
+ getKey(nonce, agentDid) {
20
+ if (agentDid) {
21
+ return `nonce:${agentDid}:${nonce}`;
22
+ }
23
+ return nonce;
24
+ }
25
+ async has(nonce, agentDid) {
20
26
  try {
27
+ const key = this.getKey(nonce, agentDid);
21
28
  const result = await this.dynamodb
22
29
  .getItem({
23
30
  TableName: this.tableName,
24
31
  Key: {
25
- [this.keyAttribute]: { S: nonce },
32
+ [this.keyAttribute]: { S: key },
26
33
  },
27
34
  ConsistentRead: true, // Ensure we get the latest data
28
35
  })
@@ -31,7 +38,11 @@ class DynamoNonceCache {
31
38
  return false;
32
39
  }
33
40
  // Check if expired (DynamoDB TTL might not have cleaned up yet)
34
- const expiresAt = parseInt(result.Item[this.ttlAttribute].N);
41
+ const ttlValue = result.Item[this.ttlAttribute]?.N;
42
+ if (!ttlValue) {
43
+ return false;
44
+ }
45
+ const expiresAt = parseInt(ttlValue);
35
46
  if (Date.now() / 1000 > expiresAt) {
36
47
  return false;
37
48
  }
@@ -40,17 +51,20 @@ class DynamoNonceCache {
40
51
  catch (error) {
41
52
  // Log error but don't expose internal details
42
53
  console.error("DynamoDB has() operation failed:", error);
54
+ // Type guard for AWS SDK errors
55
+ const awsError = error;
43
56
  // For certain errors, we can safely return false
44
- if (error.code === "ResourceNotFoundException" ||
45
- error.code === "ItemCollectionSizeLimitExceededException") {
57
+ if (awsError.code === "ResourceNotFoundException" ||
58
+ awsError.code === "ItemCollectionSizeLimitExceededException") {
46
59
  return false;
47
60
  }
48
61
  // For other errors, we should throw to avoid security issues
49
- throw new Error(`Failed to check nonce existence: ${error.code || error.message}`);
62
+ throw new Error(`Failed to check nonce existence: ${awsError.code || awsError.message || "Unknown error"}`);
50
63
  }
51
64
  }
52
- async add(nonce, ttl) {
65
+ async add(nonce, ttl, agentDid) {
53
66
  const expiresAt = Math.floor(Date.now() / 1000) + ttl;
67
+ const key = this.getKey(nonce, agentDid);
54
68
  try {
55
69
  // Use conditional write to ensure atomic add-if-absent
56
70
  // This is truly atomic in DynamoDB
@@ -58,7 +72,7 @@ class DynamoNonceCache {
58
72
  .putItem({
59
73
  TableName: this.tableName,
60
74
  Item: {
61
- [this.keyAttribute]: { S: nonce },
75
+ [this.keyAttribute]: { S: key },
62
76
  [this.ttlAttribute]: { N: expiresAt.toString() },
63
77
  createdAt: { N: Math.floor(Date.now() / 1000).toString() },
64
78
  },
@@ -67,21 +81,23 @@ class DynamoNonceCache {
67
81
  .promise();
68
82
  }
69
83
  catch (error) {
70
- if (error.code === "ConditionalCheckFailedException") {
84
+ // Type guard for AWS SDK errors
85
+ const awsError = error;
86
+ if (awsError.code === "ConditionalCheckFailedException") {
71
87
  throw new Error(`Nonce ${nonce} already exists - potential replay attack`);
72
88
  }
73
89
  console.error("DynamoDB add() operation failed:", error);
74
90
  // Provide more specific error messages for common issues
75
- if (error.code === "ResourceNotFoundException") {
91
+ if (awsError.code === "ResourceNotFoundException") {
76
92
  throw new Error(`DynamoDB table ${this.tableName} not found`);
77
93
  }
78
- else if (error.code === "ValidationException") {
79
- throw new Error(`Invalid DynamoDB operation: ${error.message}`);
94
+ else if (awsError.code === "ValidationException") {
95
+ throw new Error(`Invalid DynamoDB operation: ${awsError.message || "Unknown validation error"}`);
80
96
  }
81
- else if (error.code === "ProvisionedThroughputExceededException") {
97
+ else if (awsError.code === "ProvisionedThroughputExceededException") {
82
98
  throw new Error("DynamoDB throughput exceeded - consider increasing capacity");
83
99
  }
84
- throw new Error(`Failed to add nonce to cache: ${error.code || error.message}`);
100
+ throw new Error(`Failed to add nonce to cache: ${awsError.code || awsError.message || "Unknown error"}`);
85
101
  }
86
102
  }
87
103
  async cleanup() {
@@ -15,12 +15,12 @@ export declare class MemoryNonceCache implements NonceCache {
15
15
  /**
16
16
  * Check if a nonce exists in the cache
17
17
  */
18
- has(nonce: string): Promise<boolean>;
18
+ has(nonce: string, agentDid?: string): Promise<boolean>;
19
19
  /**
20
20
  * Add a nonce to the cache with TTL
21
21
  * MUST ensure atomic add-if-absent semantics for replay prevention
22
22
  */
23
- add(nonce: string, ttlSeconds: number): Promise<void>;
23
+ add(nonce: string, ttlSeconds: number, agentDid?: string): Promise<void>;
24
24
  /**
25
25
  * Clean up expired entries
26
26
  * Safe to call frequently and should be no-op for backends that auto-expire
@@ -25,14 +25,15 @@ class MemoryNonceCache {
25
25
  /**
26
26
  * Check if a nonce exists in the cache
27
27
  */
28
- async has(nonce) {
29
- const entry = this.cache.get(nonce);
28
+ async has(nonce, agentDid) {
29
+ const key = agentDid ? `nonce:${agentDid}:${nonce}` : nonce;
30
+ const entry = this.cache.get(key);
30
31
  if (!entry) {
31
32
  return false;
32
33
  }
33
34
  // Check if expired
34
35
  if (Date.now() > entry.expiresAt) {
35
- this.cache.delete(nonce);
36
+ this.cache.delete(key);
36
37
  return false;
37
38
  }
38
39
  return true;
@@ -41,9 +42,10 @@ class MemoryNonceCache {
41
42
  * Add a nonce to the cache with TTL
42
43
  * MUST ensure atomic add-if-absent semantics for replay prevention
43
44
  */
44
- async add(nonce, ttlSeconds) {
45
+ async add(nonce, ttlSeconds, agentDid) {
46
+ const key = agentDid ? `nonce:${agentDid}:${nonce}` : nonce;
45
47
  // Check if nonce already exists (atomic check-and-set)
46
- const existing = this.cache.get(nonce);
48
+ const existing = this.cache.get(key);
47
49
  if (existing && Date.now() <= existing.expiresAt) {
48
50
  throw new Error(`Nonce ${nonce} already exists - potential replay attack`);
49
51
  }
@@ -53,7 +55,7 @@ class MemoryNonceCache {
53
55
  sessionId: `mem_${Date.now()}_${Math.random().toString(36).substring(2)}`,
54
56
  expiresAt,
55
57
  };
56
- this.cache.set(nonce, entry);
58
+ this.cache.set(key, entry);
57
59
  }
58
60
  /**
59
61
  * Clean up expired entries
@@ -123,11 +123,13 @@ async function createNonceCache(config, options) {
123
123
  try {
124
124
  // Dynamic import to avoid bundling AWS SDK in environments that don't need it
125
125
  const awsSdk = await import("@aws-sdk/client-dynamodb");
126
- // Extract types from dynamic import - handle both ESM and CJS exports
127
- const DynamoDBClient = awsSdk.DynamoDBClient ||
128
- awsSdk.default?.DynamoDBClient;
129
- const DescribeTableCommand = awsSdk.DescribeTableCommand ||
130
- awsSdk.default?.DescribeTableCommand;
126
+ const sdkModule = awsSdk;
127
+ const DynamoDBClient = "DynamoDBClient" in sdkModule
128
+ ? sdkModule.DynamoDBClient
129
+ : sdkModule.default.DynamoDBClient;
130
+ const DescribeTableCommand = "DescribeTableCommand" in sdkModule
131
+ ? sdkModule.DescribeTableCommand
132
+ : sdkModule.default.DescribeTableCommand;
131
133
  const region = config?.dynamodb?.region ||
132
134
  process.env.AWS_REGION ||
133
135
  process.env.AWS_DEFAULT_REGION ||
@@ -179,13 +181,14 @@ async function createNonceCache(config, options) {
179
181
  // Verify KV namespace has required methods
180
182
  const kvInterface = kv;
181
183
  if (typeof kvInterface.get !== "function" ||
182
- typeof kvInterface.put !== "function") {
184
+ typeof kvInterface.put !== "function" ||
185
+ typeof kvInterface.delete !== "function") {
183
186
  throw new Error(`KV namespace '${namespace}' does not have required methods`);
184
187
  }
185
188
  // Test basic KV operation (this will be very fast in Workers)
186
189
  try {
187
- const kvInterface = kv;
188
- await kvInterface.get("__mcpi_test_key__");
190
+ const kvTest = kv;
191
+ await kvTest.get("__mcpi_test_key__");
189
192
  }
190
193
  catch (kvError) {
191
194
  const error = kvError;
@@ -8,7 +8,7 @@ export declare class RedisNonceCache implements NonceCache {
8
8
  private keyPrefix;
9
9
  constructor(redis: any, keyPrefix?: string);
10
10
  private getKey;
11
- has(nonce: string): Promise<boolean>;
12
- add(nonce: string, ttl: number): Promise<void>;
11
+ has(nonce: string, agentDid?: string): Promise<boolean>;
12
+ add(nonce: string, ttl: number, agentDid?: string): Promise<void>;
13
13
  cleanup(): Promise<void>;
14
14
  }
@@ -12,12 +12,15 @@ class RedisNonceCache {
12
12
  this.redis = redis;
13
13
  this.keyPrefix = keyPrefix;
14
14
  }
15
- getKey(nonce) {
15
+ getKey(nonce, agentDid) {
16
+ if (agentDid) {
17
+ return `${this.keyPrefix}${agentDid}:${nonce}`;
18
+ }
16
19
  return `${this.keyPrefix}${nonce}`;
17
20
  }
18
- async has(nonce) {
21
+ async has(nonce, agentDid) {
19
22
  try {
20
- const key = this.getKey(nonce);
23
+ const key = this.getKey(nonce, agentDid);
21
24
  const exists = await this.redis.exists(key);
22
25
  return exists === 1;
23
26
  }
@@ -26,9 +29,9 @@ class RedisNonceCache {
26
29
  throw new Error(`Failed to check nonce existence: ${error}`);
27
30
  }
28
31
  }
29
- async add(nonce, ttl) {
32
+ async add(nonce, ttl, agentDid) {
30
33
  try {
31
- const key = this.getKey(nonce);
34
+ const key = this.getKey(nonce, agentDid);
32
35
  // Use SET with NX (not exists) and EX (expiry) for atomic add-if-absent
33
36
  // This is truly atomic in Redis
34
37
  const result = await this.redis.set(key, "1", "EX", ttl, "NX");