@kya-os/mcp-i-core 1.2.3-canary.7 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -0
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test$colon$coverage.log +4514 -0
- package/.turbo/turbo-test.log +2973 -0
- package/COMPLIANCE_IMPROVEMENT_REPORT.md +483 -0
- package/Composer 3.md +615 -0
- package/GPT-5.md +1169 -0
- package/OPUS-plan.md +352 -0
- package/PHASE_3_AND_4.1_SUMMARY.md +585 -0
- package/PHASE_3_SUMMARY.md +317 -0
- package/PHASE_4.1.3_SUMMARY.md +428 -0
- package/PHASE_4.1_COMPLETE.md +525 -0
- package/PHASE_4_USER_DID_IDENTITY_LINKING_PLAN.md +1240 -0
- package/SCHEMA_COMPLIANCE_REPORT.md +275 -0
- package/TEST_PLAN.md +571 -0
- package/coverage/coverage-final.json +57 -0
- package/dist/__tests__/utils/mock-providers.d.ts +1 -2
- package/dist/__tests__/utils/mock-providers.d.ts.map +1 -1
- package/dist/__tests__/utils/mock-providers.js.map +1 -1
- package/dist/cache/oauth-config-cache.d.ts +69 -0
- package/dist/cache/oauth-config-cache.d.ts.map +1 -0
- package/dist/cache/oauth-config-cache.js +76 -0
- package/dist/cache/oauth-config-cache.js.map +1 -0
- package/dist/identity/idp-token-resolver.d.ts +53 -0
- package/dist/identity/idp-token-resolver.d.ts.map +1 -0
- package/dist/identity/idp-token-resolver.js +108 -0
- package/dist/identity/idp-token-resolver.js.map +1 -0
- package/dist/identity/idp-token-storage.interface.d.ts +42 -0
- package/dist/identity/idp-token-storage.interface.d.ts.map +1 -0
- package/dist/identity/idp-token-storage.interface.js +12 -0
- package/dist/identity/idp-token-storage.interface.js.map +1 -0
- package/dist/identity/user-did-manager.d.ts +39 -1
- package/dist/identity/user-did-manager.d.ts.map +1 -1
- package/dist/identity/user-did-manager.js +69 -3
- package/dist/identity/user-did-manager.js.map +1 -1
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +39 -1
- package/dist/index.js.map +1 -1
- package/dist/runtime/audit-logger.d.ts +37 -0
- package/dist/runtime/audit-logger.d.ts.map +1 -0
- package/dist/runtime/audit-logger.js +9 -0
- package/dist/runtime/audit-logger.js.map +1 -0
- package/dist/runtime/base.d.ts +58 -2
- package/dist/runtime/base.d.ts.map +1 -1
- package/dist/runtime/base.js +266 -11
- package/dist/runtime/base.js.map +1 -1
- package/dist/services/access-control.service.d.ts.map +1 -1
- package/dist/services/access-control.service.js +200 -35
- package/dist/services/access-control.service.js.map +1 -1
- package/dist/services/authorization/authorization-registry.d.ts +29 -0
- package/dist/services/authorization/authorization-registry.d.ts.map +1 -0
- package/dist/services/authorization/authorization-registry.js +57 -0
- package/dist/services/authorization/authorization-registry.js.map +1 -0
- package/dist/services/authorization/types.d.ts +53 -0
- package/dist/services/authorization/types.d.ts.map +1 -0
- package/dist/services/authorization/types.js +10 -0
- package/dist/services/authorization/types.js.map +1 -0
- package/dist/services/batch-delegation.service.d.ts +53 -0
- package/dist/services/batch-delegation.service.d.ts.map +1 -0
- package/dist/services/batch-delegation.service.js +95 -0
- package/dist/services/batch-delegation.service.js.map +1 -0
- package/dist/services/oauth-config.service.d.ts +53 -0
- package/dist/services/oauth-config.service.d.ts.map +1 -0
- package/dist/services/oauth-config.service.js +117 -0
- package/dist/services/oauth-config.service.js.map +1 -0
- package/dist/services/oauth-provider-registry.d.ts +77 -0
- package/dist/services/oauth-provider-registry.d.ts.map +1 -0
- package/dist/services/oauth-provider-registry.js +112 -0
- package/dist/services/oauth-provider-registry.js.map +1 -0
- package/dist/services/oauth-service.d.ts +77 -0
- package/dist/services/oauth-service.d.ts.map +1 -0
- package/dist/services/oauth-service.js +348 -0
- package/dist/services/oauth-service.js.map +1 -0
- package/dist/services/oauth-token-retrieval.service.d.ts +49 -0
- package/dist/services/oauth-token-retrieval.service.d.ts.map +1 -0
- package/dist/services/oauth-token-retrieval.service.js +150 -0
- package/dist/services/oauth-token-retrieval.service.js.map +1 -0
- package/dist/services/provider-resolver.d.ts +48 -0
- package/dist/services/provider-resolver.d.ts.map +1 -0
- package/dist/services/provider-resolver.js +120 -0
- package/dist/services/provider-resolver.js.map +1 -0
- package/dist/services/provider-validator.d.ts +55 -0
- package/dist/services/provider-validator.d.ts.map +1 -0
- package/dist/services/provider-validator.js +135 -0
- package/dist/services/provider-validator.js.map +1 -0
- package/dist/services/tool-context-builder.d.ts +57 -0
- package/dist/services/tool-context-builder.d.ts.map +1 -0
- package/dist/services/tool-context-builder.js +125 -0
- package/dist/services/tool-context-builder.js.map +1 -0
- package/dist/services/tool-protection.service.d.ts +87 -10
- package/dist/services/tool-protection.service.d.ts.map +1 -1
- package/dist/services/tool-protection.service.js +282 -112
- package/dist/services/tool-protection.service.js.map +1 -1
- package/dist/types/oauth-required-error.d.ts +40 -0
- package/dist/types/oauth-required-error.d.ts.map +1 -0
- package/dist/types/oauth-required-error.js +40 -0
- package/dist/types/oauth-required-error.js.map +1 -0
- package/dist/utils/did-helpers.d.ts +33 -0
- package/dist/utils/did-helpers.d.ts.map +1 -1
- package/dist/utils/did-helpers.js +40 -0
- package/dist/utils/did-helpers.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/docs/API_REFERENCE.md +1362 -0
- package/docs/COMPLIANCE_MATRIX.md +691 -0
- package/docs/STATUSLIST2021_GUIDE.md +696 -0
- package/docs/W3C_VC_DELEGATION_GUIDE.md +710 -0
- package/package.json +24 -50
- package/scripts/audit-compliance.ts +724 -0
- package/src/__tests__/cache/tool-protection-cache.test.ts +640 -0
- package/src/__tests__/config/provider-runtime-config.test.ts +309 -0
- package/src/__tests__/delegation-e2e.test.ts +690 -0
- package/src/__tests__/identity/user-did-manager.test.ts +213 -0
- package/src/__tests__/index.test.ts +56 -0
- package/src/__tests__/integration/full-flow.test.ts +776 -0
- package/src/__tests__/integration.test.ts +281 -0
- package/src/__tests__/providers/base.test.ts +173 -0
- package/src/__tests__/providers/memory.test.ts +319 -0
- package/src/__tests__/regression/phase2-regression.test.ts +427 -0
- package/src/__tests__/runtime/audit-logger.test.ts +154 -0
- package/src/__tests__/runtime/base-extensions.test.ts +593 -0
- package/src/__tests__/runtime/base.test.ts +869 -0
- package/src/__tests__/runtime/delegation-flow.test.ts +164 -0
- package/src/__tests__/runtime/proof-client-did.test.ts +375 -0
- package/src/__tests__/runtime/route-interception.test.ts +686 -0
- package/src/__tests__/runtime/tool-protection-enforcement.test.ts +908 -0
- package/src/__tests__/services/agentshield-integration.test.ts +784 -0
- package/src/__tests__/services/provider-resolver-edge-cases.test.ts +487 -0
- package/src/__tests__/services/tool-protection-oauth-provider.test.ts +480 -0
- package/src/__tests__/services/tool-protection.service.test.ts +1366 -0
- package/src/__tests__/utils/mock-providers.ts +340 -0
- package/src/cache/oauth-config-cache.d.ts +69 -0
- package/src/cache/oauth-config-cache.d.ts.map +1 -0
- package/src/cache/oauth-config-cache.js +71 -0
- package/src/cache/oauth-config-cache.js.map +1 -0
- package/src/cache/oauth-config-cache.ts +123 -0
- package/src/cache/tool-protection-cache.ts +171 -0
- package/src/compliance/EXAMPLE.md +412 -0
- package/src/compliance/__tests__/schema-verifier.test.ts +797 -0
- package/src/compliance/index.ts +8 -0
- package/src/compliance/schema-registry.ts +460 -0
- package/src/compliance/schema-verifier.ts +708 -0
- package/src/config/__tests__/remote-config.spec.ts +268 -0
- package/src/config/remote-config.ts +174 -0
- package/src/config.ts +309 -0
- package/src/delegation/__tests__/audience-validator.test.ts +112 -0
- package/src/delegation/__tests__/bitstring.test.ts +346 -0
- package/src/delegation/__tests__/cascading-revocation.test.ts +628 -0
- package/src/delegation/__tests__/delegation-graph.test.ts +584 -0
- package/src/delegation/__tests__/utils.test.ts +152 -0
- package/src/delegation/__tests__/vc-issuer.test.ts +442 -0
- package/src/delegation/__tests__/vc-verifier.test.ts +922 -0
- package/src/delegation/audience-validator.ts +52 -0
- package/src/delegation/bitstring.ts +278 -0
- package/src/delegation/cascading-revocation.ts +370 -0
- package/src/delegation/delegation-graph.ts +299 -0
- package/src/delegation/index.ts +14 -0
- package/src/delegation/statuslist-manager.ts +353 -0
- package/src/delegation/storage/__tests__/memory-graph-storage.test.ts +366 -0
- package/src/delegation/storage/__tests__/memory-statuslist-storage.test.ts +228 -0
- package/src/delegation/storage/index.ts +9 -0
- package/src/delegation/storage/memory-graph-storage.ts +178 -0
- package/src/delegation/storage/memory-statuslist-storage.ts +77 -0
- package/src/delegation/utils.ts +42 -0
- package/src/delegation/vc-issuer.ts +232 -0
- package/src/delegation/vc-verifier.ts +568 -0
- package/src/identity/idp-token-resolver.ts +147 -0
- package/src/identity/idp-token-storage.interface.ts +59 -0
- package/src/identity/user-did-manager.ts +370 -0
- package/src/index.ts +260 -0
- package/src/providers/base.d.ts +91 -0
- package/src/providers/base.d.ts.map +1 -0
- package/src/providers/base.js +38 -0
- package/src/providers/base.js.map +1 -0
- package/src/providers/base.ts +96 -0
- package/src/providers/memory.ts +142 -0
- package/src/runtime/audit-logger.ts +39 -0
- package/src/runtime/base.ts +1329 -0
- package/src/services/__tests__/access-control.integration.test.ts +443 -0
- package/src/services/__tests__/access-control.proof-response-validation.test.ts +578 -0
- package/src/services/__tests__/access-control.service.test.ts +970 -0
- package/src/services/__tests__/batch-delegation.service.test.ts +351 -0
- package/src/services/__tests__/crypto.service.test.ts +531 -0
- package/src/services/__tests__/oauth-provider-registry.test.ts +142 -0
- package/src/services/__tests__/proof-verifier.integration.test.ts +485 -0
- package/src/services/__tests__/proof-verifier.test.ts +489 -0
- package/src/services/__tests__/provider-resolution.integration.test.ts +198 -0
- package/src/services/__tests__/provider-resolver.test.ts +217 -0
- package/src/services/__tests__/storage.service.test.ts +358 -0
- package/src/services/access-control.service.ts +990 -0
- package/src/services/authorization/authorization-registry.ts +66 -0
- package/src/services/authorization/types.ts +71 -0
- package/src/services/batch-delegation.service.ts +137 -0
- package/src/services/crypto.service.ts +302 -0
- package/src/services/errors.ts +76 -0
- package/src/services/index.ts +9 -0
- package/src/services/oauth-config.service.d.ts +53 -0
- package/src/services/oauth-config.service.d.ts.map +1 -0
- package/src/services/oauth-config.service.js +113 -0
- package/src/services/oauth-config.service.js.map +1 -0
- package/src/services/oauth-config.service.ts +166 -0
- package/src/services/oauth-provider-registry.d.ts +57 -0
- package/src/services/oauth-provider-registry.d.ts.map +1 -0
- package/src/services/oauth-provider-registry.js +73 -0
- package/src/services/oauth-provider-registry.js.map +1 -0
- package/src/services/oauth-provider-registry.ts +123 -0
- package/src/services/oauth-service.ts +510 -0
- package/src/services/oauth-token-retrieval.service.ts +245 -0
- package/src/services/proof-verifier.ts +478 -0
- package/src/services/provider-resolver.d.ts +48 -0
- package/src/services/provider-resolver.d.ts.map +1 -0
- package/src/services/provider-resolver.js +106 -0
- package/src/services/provider-resolver.js.map +1 -0
- package/src/services/provider-resolver.ts +144 -0
- package/src/services/provider-validator.ts +170 -0
- package/src/services/storage.service.ts +566 -0
- package/src/services/tool-context-builder.ts +172 -0
- package/src/services/tool-protection.service.ts +958 -0
- package/src/types/oauth-required-error.ts +63 -0
- package/src/types/tool-protection.ts +155 -0
- package/src/utils/__tests__/did-helpers.test.ts +101 -0
- package/src/utils/base64.ts +148 -0
- package/src/utils/cors.ts +83 -0
- package/src/utils/did-helpers.ts +150 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/storage-keys.ts +278 -0
- package/tsconfig.json +21 -0
- package/vitest.config.ts +56 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Providers for Testing
|
|
3
|
+
*
|
|
4
|
+
* These mock implementations allow controlled testing of the runtime
|
|
5
|
+
* and other components that depend on providers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { vi } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
CryptoProvider,
|
|
11
|
+
ClockProvider,
|
|
12
|
+
FetchProvider,
|
|
13
|
+
StorageProvider,
|
|
14
|
+
NonceCacheProvider,
|
|
15
|
+
IdentityProvider,
|
|
16
|
+
AgentIdentity
|
|
17
|
+
} from '../../providers/base';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Mock Crypto Provider
|
|
21
|
+
*/
|
|
22
|
+
export class MockCryptoProvider extends CryptoProvider {
|
|
23
|
+
async sign(data: Uint8Array, privateKey: string): Promise<Uint8Array> {
|
|
24
|
+
// Simple mock signature
|
|
25
|
+
return new Uint8Array([1, 2, 3, 4, 5]);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async verify(data: Uint8Array, signature: Uint8Array, publicKey: string): Promise<boolean> {
|
|
29
|
+
// Simple verification - check signature matches expected pattern
|
|
30
|
+
// In real implementation, this would verify cryptographic signature
|
|
31
|
+
return signature.length === 5 && signature[0] === 1 && signature[1] === 2;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async generateKeyPair(): Promise<{ privateKey: string; publicKey: string }> {
|
|
35
|
+
// Generate valid base64-encoded keys for testing
|
|
36
|
+
// Ed25519 keys are 32 bytes, so we generate 32 random bytes and encode them
|
|
37
|
+
const randomBytes = new Uint8Array(32);
|
|
38
|
+
for (let i = 0; i < 32; i++) {
|
|
39
|
+
randomBytes[i] = Math.floor(Math.random() * 256);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Convert to base64 using Node.js Buffer (works in Node.js test environment)
|
|
43
|
+
const base64PrivateKey = Buffer.from(randomBytes).toString('base64');
|
|
44
|
+
|
|
45
|
+
// Generate a different public key (Ed25519 public key is also 32 bytes)
|
|
46
|
+
const publicBytes = new Uint8Array(32);
|
|
47
|
+
for (let i = 0; i < 32; i++) {
|
|
48
|
+
publicBytes[i] = Math.floor(Math.random() * 256);
|
|
49
|
+
}
|
|
50
|
+
const base64PublicKey = Buffer.from(publicBytes).toString('base64');
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
privateKey: base64PrivateKey,
|
|
54
|
+
publicKey: base64PublicKey
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async hash(data: Uint8Array): Promise<Uint8Array> {
|
|
59
|
+
// Simple mock hash
|
|
60
|
+
return new Uint8Array([9, 8, 7, 6, 5]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async randomBytes(length: number): Promise<Uint8Array> {
|
|
64
|
+
const bytes = new Uint8Array(length);
|
|
65
|
+
for (let i = 0; i < length; i++) {
|
|
66
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
67
|
+
}
|
|
68
|
+
return bytes;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Mock Clock Provider
|
|
74
|
+
*/
|
|
75
|
+
export class MockClockProvider extends ClockProvider {
|
|
76
|
+
private currentTime: number = Date.now();
|
|
77
|
+
|
|
78
|
+
setTime(timestamp: number): void {
|
|
79
|
+
this.currentTime = timestamp;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
now(): number {
|
|
83
|
+
return this.currentTime;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
isWithinSkew(timestamp: number, skewSeconds: number): boolean {
|
|
87
|
+
const diff = Math.abs(this.currentTime - timestamp);
|
|
88
|
+
return diff <= skewSeconds * 1000;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
hasExpired(expiresAt: number): boolean {
|
|
92
|
+
return this.currentTime > expiresAt;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
calculateExpiry(ttlSeconds: number): number {
|
|
96
|
+
return this.currentTime + (ttlSeconds * 1000);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
format(timestamp: number): string {
|
|
100
|
+
return new Date(timestamp).toISOString();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Mock Fetch Provider
|
|
106
|
+
*/
|
|
107
|
+
export class MockFetchProvider extends FetchProvider {
|
|
108
|
+
private didDocuments: Map<string, any> = new Map();
|
|
109
|
+
private statusLists: Map<string, any> = new Map();
|
|
110
|
+
private delegationChains: Map<string, any[]> = new Map();
|
|
111
|
+
public fetch: (url: string, options?: any) => Promise<Response>;
|
|
112
|
+
|
|
113
|
+
constructor() {
|
|
114
|
+
super();
|
|
115
|
+
// Initialize fetch as a vi.fn() in constructor to ensure it's always mockable
|
|
116
|
+
this.fetch = vi.fn(async (url: string, options?: any): Promise<Response> => {
|
|
117
|
+
// Simple mock Response
|
|
118
|
+
return new Response(JSON.stringify({ url, options }), {
|
|
119
|
+
status: 200,
|
|
120
|
+
headers: { 'Content-Type': 'application/json' }
|
|
121
|
+
});
|
|
122
|
+
}) as any;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Test helpers
|
|
126
|
+
setDIDDocument(did: string, doc: any): void {
|
|
127
|
+
this.didDocuments.set(did, doc);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
setStatusList(url: string, list: any): void {
|
|
131
|
+
this.statusLists.set(url, list);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setDelegationChain(id: string, chain: any[]): void {
|
|
135
|
+
this.delegationChains.set(id, chain);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async resolveDID(did: string): Promise<any> {
|
|
139
|
+
const doc = this.didDocuments.get(did);
|
|
140
|
+
if (!doc) {
|
|
141
|
+
throw new Error(`DID ${did} not found`);
|
|
142
|
+
}
|
|
143
|
+
return doc;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async fetchStatusList(url: string): Promise<any> {
|
|
147
|
+
const list = this.statusLists.get(url);
|
|
148
|
+
if (!list) {
|
|
149
|
+
throw new Error(`Status list ${url} not found`);
|
|
150
|
+
}
|
|
151
|
+
return list;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async fetchDelegationChain(id: string): Promise<any[]> {
|
|
155
|
+
const chain = this.delegationChains.get(id);
|
|
156
|
+
if (!chain) {
|
|
157
|
+
throw new Error(`Delegation chain ${id} not found`);
|
|
158
|
+
}
|
|
159
|
+
return chain;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Mock Storage Provider
|
|
165
|
+
*/
|
|
166
|
+
export class MockStorageProvider extends StorageProvider {
|
|
167
|
+
private store: Map<string, string> = new Map();
|
|
168
|
+
|
|
169
|
+
async get(key: string): Promise<string | null> {
|
|
170
|
+
return this.store.get(key) || null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async set(key: string, value: string): Promise<void> {
|
|
174
|
+
this.store.set(key, value);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async delete(key: string): Promise<void> {
|
|
178
|
+
this.store.delete(key);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async exists(key: string): Promise<boolean> {
|
|
182
|
+
return this.store.has(key);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async list(prefix?: string): Promise<string[]> {
|
|
186
|
+
const keys = Array.from(this.store.keys());
|
|
187
|
+
if (prefix) {
|
|
188
|
+
return keys.filter(k => k.startsWith(prefix));
|
|
189
|
+
}
|
|
190
|
+
return keys;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Test helper
|
|
194
|
+
clear(): void {
|
|
195
|
+
this.store.clear();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Mock Nonce Cache Provider
|
|
201
|
+
*/
|
|
202
|
+
export class MockNonceCacheProvider extends NonceCacheProvider {
|
|
203
|
+
private nonces: Map<string, number> = new Map();
|
|
204
|
+
public cleanupCalled = false;
|
|
205
|
+
public destroyCalled = false;
|
|
206
|
+
private clock?: ClockProvider;
|
|
207
|
+
|
|
208
|
+
setClock(clock: ClockProvider): void {
|
|
209
|
+
this.clock = clock;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async has(nonce: string, agentDid?: string): Promise<boolean> {
|
|
213
|
+
const key = agentDid ? `nonce:${agentDid}:${nonce}` : `nonce:${nonce}`;
|
|
214
|
+
const expiry = this.nonces.get(key);
|
|
215
|
+
if (!expiry) return false;
|
|
216
|
+
|
|
217
|
+
const now = this.clock ? this.clock.now() : Date.now();
|
|
218
|
+
if (now > expiry) {
|
|
219
|
+
this.nonces.delete(key);
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async add(nonce: string, ttlSeconds: number, agentDid?: string): Promise<void> {
|
|
227
|
+
const key = agentDid ? `nonce:${agentDid}:${nonce}` : `nonce:${nonce}`;
|
|
228
|
+
// Convert TTL seconds to absolute expiration timestamp for storage
|
|
229
|
+
const now = this.clock ? this.clock.now() : Date.now();
|
|
230
|
+
const expiresAt = now + (ttlSeconds * 1000);
|
|
231
|
+
this.nonces.set(key, expiresAt);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async cleanup(): Promise<void> {
|
|
235
|
+
this.cleanupCalled = true;
|
|
236
|
+
const now = this.clock ? this.clock.now() : Date.now();
|
|
237
|
+
for (const [nonce, expiry] of this.nonces) {
|
|
238
|
+
if (now > expiry) {
|
|
239
|
+
this.nonces.delete(nonce);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async destroy(): Promise<void> {
|
|
245
|
+
this.destroyCalled = true;
|
|
246
|
+
this.nonces.clear();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Test helper
|
|
250
|
+
clear(): void {
|
|
251
|
+
this.nonces.clear();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
size(): number {
|
|
255
|
+
return this.nonces.size;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Mock Identity Provider
|
|
261
|
+
*/
|
|
262
|
+
export class MockIdentityProvider extends IdentityProvider {
|
|
263
|
+
private identity?: AgentIdentity;
|
|
264
|
+
public rotateKeysCalled = false;
|
|
265
|
+
public deleteIdentityCalled = false;
|
|
266
|
+
private rotateCount = 0;
|
|
267
|
+
|
|
268
|
+
constructor(identity?: AgentIdentity) {
|
|
269
|
+
super();
|
|
270
|
+
this.identity = identity;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async getIdentity(): Promise<AgentIdentity> {
|
|
274
|
+
if (!this.identity) {
|
|
275
|
+
this.identity = {
|
|
276
|
+
did: 'did:key:zmock123',
|
|
277
|
+
kid: 'did:key:zmock123#key-1',
|
|
278
|
+
privateKey: 'mock-private-key',
|
|
279
|
+
publicKey: 'mock-public-key',
|
|
280
|
+
createdAt: new Date().toISOString(),
|
|
281
|
+
type: 'development',
|
|
282
|
+
metadata: { mock: true }
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return this.identity;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async saveIdentity(identity: AgentIdentity): Promise<void> {
|
|
289
|
+
this.identity = identity;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async rotateKeys(): Promise<AgentIdentity> {
|
|
293
|
+
this.rotateKeysCalled = true;
|
|
294
|
+
this.rotateCount++;
|
|
295
|
+
this.identity = {
|
|
296
|
+
did: `did:key:zmock456-${this.rotateCount}`,
|
|
297
|
+
kid: `did:key:zmock456-${this.rotateCount}#key-1`,
|
|
298
|
+
privateKey: `mock-private-key-rotated-${this.rotateCount}`,
|
|
299
|
+
publicKey: `mock-public-key-rotated-${this.rotateCount}`,
|
|
300
|
+
createdAt: new Date().toISOString(),
|
|
301
|
+
type: 'development',
|
|
302
|
+
metadata: { mock: true, rotated: true, rotateCount: this.rotateCount }
|
|
303
|
+
};
|
|
304
|
+
return this.identity;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async deleteIdentity(): Promise<void> {
|
|
308
|
+
this.deleteIdentityCalled = true;
|
|
309
|
+
this.identity = undefined;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Test helper
|
|
313
|
+
setIdentity(identity: AgentIdentity): void {
|
|
314
|
+
this.identity = identity;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Create a full set of mock providers for testing
|
|
320
|
+
*/
|
|
321
|
+
export function createMockProviders() {
|
|
322
|
+
const cryptoProvider = new MockCryptoProvider();
|
|
323
|
+
const clockProvider = new MockClockProvider();
|
|
324
|
+
const fetchProvider = new MockFetchProvider();
|
|
325
|
+
const storageProvider = new MockStorageProvider();
|
|
326
|
+
const nonceCacheProvider = new MockNonceCacheProvider();
|
|
327
|
+
const identityProvider = new MockIdentityProvider();
|
|
328
|
+
|
|
329
|
+
// Connect nonce cache to clock for consistent time
|
|
330
|
+
nonceCacheProvider.setClock(clockProvider);
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
cryptoProvider,
|
|
334
|
+
clockProvider,
|
|
335
|
+
fetchProvider,
|
|
336
|
+
storageProvider,
|
|
337
|
+
nonceCacheProvider,
|
|
338
|
+
identityProvider
|
|
339
|
+
};
|
|
340
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-agnostic cache interface for OAuth provider configurations
|
|
3
|
+
*
|
|
4
|
+
* This interface allows different runtime adapters to provide their own
|
|
5
|
+
* caching implementations (e.g., in-memory for Node.js, KV for Cloudflare)
|
|
6
|
+
*
|
|
7
|
+
* @package @kya-os/mcp-i-core
|
|
8
|
+
*/
|
|
9
|
+
import type { OAuthConfig } from "@kya-os/contracts/config";
|
|
10
|
+
/**
|
|
11
|
+
* Cache interface for storing and retrieving OAuth provider configurations
|
|
12
|
+
*/
|
|
13
|
+
export interface OAuthConfigCache {
|
|
14
|
+
/**
|
|
15
|
+
* Retrieve a cached OAuth configuration
|
|
16
|
+
* @param key Cache key (typically projectId)
|
|
17
|
+
* @returns Cached config or null if not found/expired
|
|
18
|
+
*/
|
|
19
|
+
get(key: string): Promise<OAuthConfig | null>;
|
|
20
|
+
/**
|
|
21
|
+
* Store an OAuth configuration in cache
|
|
22
|
+
* @param key Cache key (typically projectId)
|
|
23
|
+
* @param value OAuth configuration to cache
|
|
24
|
+
* @param ttl Time-to-live in milliseconds
|
|
25
|
+
*/
|
|
26
|
+
set(key: string, value: OAuthConfig, ttl: number): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Clear all cached entries
|
|
29
|
+
*/
|
|
30
|
+
clear(): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Remove a specific cache entry
|
|
33
|
+
* @param key Cache key to remove
|
|
34
|
+
*/
|
|
35
|
+
delete(key: string): Promise<void>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* In-memory cache implementation
|
|
39
|
+
*
|
|
40
|
+
* Suitable for:
|
|
41
|
+
* - Node.js runtimes
|
|
42
|
+
* - Development/testing
|
|
43
|
+
* - Single-instance deployments
|
|
44
|
+
*
|
|
45
|
+
* NOT suitable for:
|
|
46
|
+
* - Multi-instance deployments (cache not shared)
|
|
47
|
+
* - Serverless environments (state not persisted)
|
|
48
|
+
*/
|
|
49
|
+
export declare class InMemoryOAuthConfigCache implements OAuthConfigCache {
|
|
50
|
+
private cache;
|
|
51
|
+
get(key: string): Promise<OAuthConfig | null>;
|
|
52
|
+
set(key: string, value: OAuthConfig, ttl: number): Promise<void>;
|
|
53
|
+
clear(): Promise<void>;
|
|
54
|
+
delete(key: string): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* No-op cache implementation (disables caching)
|
|
58
|
+
*
|
|
59
|
+
* Use when:
|
|
60
|
+
* - You want to disable caching entirely
|
|
61
|
+
* - Testing scenarios that require fresh data
|
|
62
|
+
*/
|
|
63
|
+
export declare class NoOpOAuthConfigCache implements OAuthConfigCache {
|
|
64
|
+
get(_key: string): Promise<OAuthConfig | null>;
|
|
65
|
+
set(_key: string, _value: OAuthConfig, _ttl: number): Promise<void>;
|
|
66
|
+
clear(): Promise<void>;
|
|
67
|
+
delete(_key: string): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=oauth-config-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-config-cache.d.ts","sourceRoot":"","sources":["oauth-config-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAE5D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;OAIG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE9C;;;;;OAKG;IACH,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEjE;;OAEG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvB;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpC;AAED;;;;;;;;;;;GAWG;AACH,qBAAa,wBAAyB,YAAW,gBAAgB;IAC/D,OAAO,CAAC,KAAK,CAGT;IAEE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAgB7C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAShE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAGzC;AAED;;;;;;GAMG;AACH,qBAAa,oBAAqB,YAAW,gBAAgB;IACrD,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAI9C,GAAG,CACP,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC;IAIV,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAG1C"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-agnostic cache interface for OAuth provider configurations
|
|
3
|
+
*
|
|
4
|
+
* This interface allows different runtime adapters to provide their own
|
|
5
|
+
* caching implementations (e.g., in-memory for Node.js, KV for Cloudflare)
|
|
6
|
+
*
|
|
7
|
+
* @package @kya-os/mcp-i-core
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* In-memory cache implementation
|
|
11
|
+
*
|
|
12
|
+
* Suitable for:
|
|
13
|
+
* - Node.js runtimes
|
|
14
|
+
* - Development/testing
|
|
15
|
+
* - Single-instance deployments
|
|
16
|
+
*
|
|
17
|
+
* NOT suitable for:
|
|
18
|
+
* - Multi-instance deployments (cache not shared)
|
|
19
|
+
* - Serverless environments (state not persisted)
|
|
20
|
+
*/
|
|
21
|
+
export class InMemoryOAuthConfigCache {
|
|
22
|
+
cache = new Map();
|
|
23
|
+
async get(key) {
|
|
24
|
+
const entry = this.cache.get(key);
|
|
25
|
+
if (!entry) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
// Check if expired
|
|
29
|
+
if (Date.now() > entry.expiresAt) {
|
|
30
|
+
this.cache.delete(key);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return entry.value;
|
|
34
|
+
}
|
|
35
|
+
async set(key, value, ttl) {
|
|
36
|
+
// If TTL is <= 0, don't store (entry would be immediately expired)
|
|
37
|
+
if (ttl <= 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const expiresAt = Date.now() + ttl;
|
|
41
|
+
this.cache.set(key, { value, expiresAt });
|
|
42
|
+
}
|
|
43
|
+
async clear() {
|
|
44
|
+
this.cache.clear();
|
|
45
|
+
}
|
|
46
|
+
async delete(key) {
|
|
47
|
+
this.cache.delete(key);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* No-op cache implementation (disables caching)
|
|
52
|
+
*
|
|
53
|
+
* Use when:
|
|
54
|
+
* - You want to disable caching entirely
|
|
55
|
+
* - Testing scenarios that require fresh data
|
|
56
|
+
*/
|
|
57
|
+
export class NoOpOAuthConfigCache {
|
|
58
|
+
async get(_key) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
async set(_key, _value, _ttl) {
|
|
62
|
+
// No-op
|
|
63
|
+
}
|
|
64
|
+
async clear() {
|
|
65
|
+
// No-op
|
|
66
|
+
}
|
|
67
|
+
async delete(_key) {
|
|
68
|
+
// No-op
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=oauth-config-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth-config-cache.js","sourceRoot":"","sources":["oauth-config-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAmCH;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,wBAAwB;IAC3B,KAAK,GAAG,IAAI,GAAG,EAGpB,CAAC;IAEJ,KAAK,CAAC,GAAG,CAAC,GAAW;QACnB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAElC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,IAAI,CAAC;QACd,CAAC;QAED,mBAAmB;QACnB,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;YACjC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACvB,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,KAAK,CAAC,KAAK,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,GAAW,EAAE,KAAkB,EAAE,GAAW;QACpD,mEAAmE;QACnE,IAAI,GAAG,IAAI,CAAC,EAAE,CAAC;YACb,OAAO;QACT,CAAC;QACD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC;QACnC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW;QACtB,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,oBAAoB;IAC/B,KAAK,CAAC,GAAG,CAAC,IAAY;QACpB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,GAAG,CACP,IAAY,EACZ,MAAmB,EACnB,IAAY;QAEZ,QAAQ;IACV,CAAC;IAED,KAAK,CAAC,KAAK;QACT,QAAQ;IACV,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,IAAY;QACvB,QAAQ;IACV,CAAC;CACF"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-agnostic cache interface for OAuth provider configurations
|
|
3
|
+
*
|
|
4
|
+
* This interface allows different runtime adapters to provide their own
|
|
5
|
+
* caching implementations (e.g., in-memory for Node.js, KV for Cloudflare)
|
|
6
|
+
*
|
|
7
|
+
* @package @kya-os/mcp-i-core
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { OAuthConfig } from "@kya-os/contracts/config";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Cache interface for storing and retrieving OAuth provider configurations
|
|
14
|
+
*/
|
|
15
|
+
export interface OAuthConfigCache {
|
|
16
|
+
/**
|
|
17
|
+
* Retrieve a cached OAuth configuration
|
|
18
|
+
* @param key Cache key (typically projectId)
|
|
19
|
+
* @returns Cached config or null if not found/expired
|
|
20
|
+
*/
|
|
21
|
+
get(key: string): Promise<OAuthConfig | null>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Store an OAuth configuration in cache
|
|
25
|
+
* @param key Cache key (typically projectId)
|
|
26
|
+
* @param value OAuth configuration to cache
|
|
27
|
+
* @param ttl Time-to-live in milliseconds
|
|
28
|
+
*/
|
|
29
|
+
set(key: string, value: OAuthConfig, ttl: number): Promise<void>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Clear all cached entries
|
|
33
|
+
*/
|
|
34
|
+
clear(): Promise<void>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Remove a specific cache entry
|
|
38
|
+
* @param key Cache key to remove
|
|
39
|
+
*/
|
|
40
|
+
delete(key: string): Promise<void>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* In-memory cache implementation
|
|
45
|
+
*
|
|
46
|
+
* Suitable for:
|
|
47
|
+
* - Node.js runtimes
|
|
48
|
+
* - Development/testing
|
|
49
|
+
* - Single-instance deployments
|
|
50
|
+
*
|
|
51
|
+
* NOT suitable for:
|
|
52
|
+
* - Multi-instance deployments (cache not shared)
|
|
53
|
+
* - Serverless environments (state not persisted)
|
|
54
|
+
*/
|
|
55
|
+
export class InMemoryOAuthConfigCache implements OAuthConfigCache {
|
|
56
|
+
private cache = new Map<
|
|
57
|
+
string,
|
|
58
|
+
{ value: OAuthConfig; expiresAt: number }
|
|
59
|
+
>();
|
|
60
|
+
|
|
61
|
+
async get(key: string): Promise<OAuthConfig | null> {
|
|
62
|
+
const entry = this.cache.get(key);
|
|
63
|
+
|
|
64
|
+
if (!entry) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Check if expired
|
|
69
|
+
if (Date.now() > entry.expiresAt) {
|
|
70
|
+
this.cache.delete(key);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return entry.value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async set(key: string, value: OAuthConfig, ttl: number): Promise<void> {
|
|
78
|
+
// If TTL is <= 0, don't store (entry would be immediately expired)
|
|
79
|
+
if (ttl <= 0) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const expiresAt = Date.now() + ttl;
|
|
83
|
+
this.cache.set(key, { value, expiresAt });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async clear(): Promise<void> {
|
|
87
|
+
this.cache.clear();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async delete(key: string): Promise<void> {
|
|
91
|
+
this.cache.delete(key);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* No-op cache implementation (disables caching)
|
|
97
|
+
*
|
|
98
|
+
* Use when:
|
|
99
|
+
* - You want to disable caching entirely
|
|
100
|
+
* - Testing scenarios that require fresh data
|
|
101
|
+
*/
|
|
102
|
+
export class NoOpOAuthConfigCache implements OAuthConfigCache {
|
|
103
|
+
async get(_key: string): Promise<OAuthConfig | null> {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async set(
|
|
108
|
+
_key: string,
|
|
109
|
+
_value: OAuthConfig,
|
|
110
|
+
_ttl: number
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
// No-op
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async clear(): Promise<void> {
|
|
116
|
+
// No-op
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async delete(_key: string): Promise<void> {
|
|
120
|
+
// No-op
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|