diff --git a/typescript/.kiro/specs/anp-typescript-sdk/design.md b/typescript/.kiro/specs/anp-typescript-sdk/design.md new file mode 100644 index 0000000..dcce0e1 --- /dev/null +++ b/typescript/.kiro/specs/anp-typescript-sdk/design.md @@ -0,0 +1,840 @@ +# Design Document + +## Overview + +The ANP TypeScript SDK provides a comprehensive implementation of the Agent Network Protocol, enabling developers to build intelligent agents that can authenticate, discover, and communicate with other agents in a decentralized network. The SDK leverages XState v5 for robust state machine management, ensuring predictable protocol flows and state transitions. + +The SDK is designed with modularity, extensibility, and developer experience in mind. It provides high-level APIs for common operations while allowing low-level access for advanced use cases. + +**IMPORTANT**: All SDK code MUST be developed in the `ts_sdk` directory at the root of the project. This directory structure ensures clear separation from the protocol specifications and examples. + +### Project Structure + +``` +ts_sdk/ +├── src/ # Source code +│ ├── core/ # Core modules (DID, Auth, ADP, Discovery) +│ ├── protocol/ # Protocol layer (Meta-protocol, Message Handler) +│ ├── crypto/ # Cryptography module +│ ├── transport/ # Transport layer (HTTP, WebSocket) +│ ├── types/ # TypeScript type definitions +│ ├── errors/ # Error classes +│ └── index.ts # Public API entry point +├── tests/ # Test files +│ ├── unit/ # Unit tests +│ └── integration/ # Integration tests +├── examples/ # Example applications +├── docs/ # Documentation +├── package.json # Package configuration +├── tsconfig.json # TypeScript configuration +├── vitest.config.ts # Vitest configuration +└── README.md # SDK documentation +``` + +## Architecture + +### High-Level Architecture + +```mermaid +graph TB + App[Application Code] + + subgraph ANP_SDK[ANP TypeScript SDK] + API[Public API Layer] + + subgraph Core[Core Modules] + DID[DID Manager] + ADP[Agent Description] + Discovery[Agent Discovery] + Auth[Authentication] + end + + subgraph Protocol[Protocol Layer] + Meta[Meta-Protocol State Machine] + AppProto[Application Protocol] + NL[Natural Language Protocol] + end + + subgraph Crypto[Cryptography] + Sign[Signing & Verification] + Encrypt[Encryption & Decryption] + KeyMgmt[Key Management] + end + + subgraph Transport[Transport Layer] + HTTP[HTTP Client] + WS[WebSocket Client] + end + end + + External[External Services] + + App --> API + API --> Core + API --> Protocol + Core --> Crypto + Core --> Transport + Protocol --> Crypto + Protocol --> Transport + Transport --> External +``` + +### Module Dependencies + +- **Public API Layer**: Entry point for developers, provides simplified interfaces +- **Core Modules**: Business logic for DID, ADP, Discovery, and Authentication +- **Protocol Layer**: State machines and handlers for protocol negotiation and communication +- **Cryptography**: Low-level cryptographic operations +- **Transport Layer**: HTTP and WebSocket clients for network communication + +## Components and Interfaces + +### 1. DID Manager + +**Purpose**: Manage DID:WBA identities, including creation, resolution, and key management. + +**Key Classes**: + +```typescript +interface DIDDocument { + '@context': string[]; + id: string; + verificationMethod: VerificationMethod[]; + authentication: (string | VerificationMethod)[]; + keyAgreement?: VerificationMethod[]; + humanAuthorization?: (string | VerificationMethod)[]; + service?: ServiceEndpoint[]; +} + +interface VerificationMethod { + id: string; + type: string; + controller: string; + publicKeyJwk?: JsonWebKey; + publicKeyMultibase?: string; +} + +class DIDManager { + // Create a new DID identity + async createDID(domain: string, path?: string): Promise + + // Resolve a DID to its document + async resolveDID(did: string): Promise + + // Sign data with a DID identity + async sign(identity: DIDIdentity, data: Uint8Array): Promise + + // Verify a signature + async verify(did: string, data: Uint8Array, signature: Signature): Promise + + // Export DID document + exportDocument(identity: DIDIdentity): DIDDocument +} + +interface DIDIdentity { + did: string; + document: DIDDocument; + privateKeys: Map; +} +``` + +### 2. Authentication Manager + +**Purpose**: Handle HTTP authentication using DID:WBA method. + +**Key Classes**: + +```typescript +interface AuthConfig { + maxTokenAge: number; // milliseconds + nonceLength: number; + clockSkewTolerance: number; // seconds +} + +class AuthenticationManager { + constructor( + private didManager: DIDManager, + private config: AuthConfig + ) {} + + // Generate authentication header for outgoing request + async generateAuthHeader( + identity: DIDIdentity, + targetDomain: string, + verificationMethodId: string + ): Promise + + // Verify incoming request authentication + async verifyAuthHeader( + authHeader: string, + expectedDomain: string + ): Promise + + // Generate access token after successful authentication + generateAccessToken(did: string, expiresIn: number): string + + // Verify access token + verifyAccessToken(token: string): TokenVerificationResult +} + +interface VerificationResult { + success: boolean; + did?: string; + error?: string; +} +``` + +### 3. Agent Description Manager + +**Purpose**: Create, publish, and parse agent description documents. + +**Key Classes**: + +```typescript +interface AgentDescription { + protocolType: 'ANP'; + protocolVersion: string; + type: 'AgentDescription'; + url?: string; + name: string; + did?: string; + owner?: Organization; + description?: string; + created?: string; + securityDefinitions: Record; + security: string; + Infomations?: Information[]; + interfaces?: Interface[]; + proof?: Proof; +} + +class AgentDescriptionManager { + // Create a new agent description + createDescription(metadata: AgentMetadata): AgentDescription + + // Add information resource + addInformation( + description: AgentDescription, + info: Information + ): AgentDescription + + // Add interface + addInterface( + description: AgentDescription, + iface: Interface + ): AgentDescription + + // Sign agent description + async signDescription( + description: AgentDescription, + identity: DIDIdentity, + challenge: string + ): Promise + + // Fetch and parse agent description + async fetchDescription(url: string): Promise + + // Verify agent description signature + async verifyDescription(description: AgentDescription): Promise +} +``` + +### 4. Agent Discovery Manager + +**Purpose**: Discover agents through active and passive mechanisms. + +**Key Classes**: + +```typescript +interface DiscoveryDocument { + '@context': Record; + '@type': 'CollectionPage'; + url: string; + items: AgentDescriptionItem[]; + next?: string; +} + +interface AgentDescriptionItem { + '@type': 'ad:AgentDescription'; + name: string; + '@id': string; // URL to agent description +} + +class AgentDiscoveryManager { + // Active discovery: fetch agents from a domain + async discoverAgents(domain: string): Promise + + // Passive discovery: register with a search service + async registerWithSearchService( + searchServiceUrl: string, + agentDescriptionUrl: string, + identity: DIDIdentity + ): Promise + + // Search for agents + async searchAgents( + searchServiceUrl: string, + query: SearchQuery + ): Promise + + // Fetch all pages recursively + private async fetchAllPages(url: string): Promise +} +``` + +### 5. Meta-Protocol State Machine + +**Purpose**: Manage protocol negotiation flow using XState v5. + +**State Machine Design**: + +```mermaid +stateDiagram-v2 + [*] --> Idle + + Idle --> Negotiating : initiate + Idle --> Negotiating : receive_request + + Negotiating --> Negotiating : negotiate + Negotiating --> CodeGeneration : accept + Negotiating --> Rejected : reject + Negotiating --> Rejected : timeout + + CodeGeneration --> TestCases : code_ready + CodeGeneration --> Failed : code_error + + TestCases --> Testing : tests_agreed + TestCases --> Ready : skip_tests + + Testing --> Ready : tests_passed + Testing --> FixError : tests_failed + + FixError --> CodeGeneration : fix_accepted + FixError --> Failed : fix_rejected + + Ready --> Communicating : start_communication + + Communicating --> FixError : protocol_error + Communicating --> [*] : end + + Rejected --> [*] + Failed --> [*] +``` + +**Key Classes**: + +```typescript +interface MetaProtocolContext { + sequenceId: number; + candidateProtocols: string; + agreedProtocol?: string; + testCases?: string; + maxNegotiationRounds: number; + remoteDID: string; + localIdentity: DIDIdentity; +} + +type MetaProtocolEvent = + | { type: 'initiate'; remoteDID: string; candidateProtocols: string } + | { type: 'receive_request'; message: ProtocolNegotiationMessage } + | { type: 'negotiate'; response: string } + | { type: 'accept' } + | { type: 'reject'; reason: string } + | { type: 'code_ready' } + | { type: 'code_error'; error: string } + | { type: 'tests_agreed'; testCases: string } + | { type: 'skip_tests' } + | { type: 'tests_passed' } + | { type: 'tests_failed'; errors: string } + | { type: 'fix_accepted' } + | { type: 'fix_rejected' } + | { type: 'start_communication' } + | { type: 'protocol_error'; error: string } + | { type: 'end' }; + +class MetaProtocolMachine { + // Create state machine instance + static create(config: MetaProtocolConfig): Actor + + // Send negotiation message + async sendNegotiation( + actor: Actor, + candidateProtocols: string, + modificationSummary?: string + ): Promise + + // Process received message + async processMessage( + actor: Actor, + message: MetaProtocolMessage + ): Promise +} +``` + +### 6. Protocol Message Handler + +**Purpose**: Encode and decode protocol messages. + +**Key Classes**: + +```typescript +enum ProtocolType { + META = 0b00, + APPLICATION = 0b01, + NATURAL_LANGUAGE = 0b10, + VERIFICATION = 0b11 +} + +interface ProtocolMessage { + protocolType: ProtocolType; + data: Uint8Array; +} + +class ProtocolMessageHandler { + // Encode message + encode(type: ProtocolType, data: Uint8Array): Uint8Array + + // Decode message + decode(message: Uint8Array): ProtocolMessage + + // Parse meta-protocol message + parseMetaProtocol(data: Uint8Array): MetaProtocolMessage + + // Parse application protocol message + parseApplicationProtocol(data: Uint8Array): any + + // Parse natural language message + parseNaturalLanguage(data: Uint8Array): string +} + +type MetaProtocolMessage = + | ProtocolNegotiationMessage + | CodeGenerationMessage + | TestCasesNegotiationMessage + | FixErrorNegotiationMessage + | NaturalLanguageNegotiationMessage; + +interface ProtocolNegotiationMessage { + action: 'protocolNegotiation'; + sequenceId: number; + candidateProtocols: string; + modificationSummary?: string; + status: 'negotiating' | 'rejected' | 'accepted' | 'timeout'; +} +``` + +### 7. Cryptography Module + +**Purpose**: Provide cryptographic operations for signing, verification, and encryption. + +**Key Classes**: + +```typescript +class CryptoModule { + // Generate key pair + async generateKeyPair(type: KeyType): Promise + + // Sign data + async sign(privateKey: CryptoKey, data: Uint8Array): Promise + + // Verify signature + async verify( + publicKey: CryptoKey, + data: Uint8Array, + signature: Uint8Array + ): Promise + + // ECDHE key exchange + async performKeyExchange( + localPrivateKey: CryptoKey, + remotePublicKey: CryptoKey + ): Promise + + // Derive encryption key + async deriveKey(sharedSecret: Uint8Array, salt: Uint8Array): Promise + + // Encrypt data + async encrypt(key: CryptoKey, data: Uint8Array): Promise + + // Decrypt data + async decrypt(key: CryptoKey, encrypted: EncryptedData): Promise +} + +enum KeyType { + ECDSA_SECP256K1 = 'EcdsaSecp256k1VerificationKey2019', + ED25519 = 'Ed25519VerificationKey2020', + X25519 = 'X25519KeyAgreementKey2019' +} + +interface EncryptedData { + ciphertext: Uint8Array; + iv: Uint8Array; + tag: Uint8Array; +} +``` + +### 8. HTTP Client + +**Purpose**: Handle HTTP requests with authentication and error handling. + +**Key Classes**: + +```typescript +interface HTTPClientConfig { + timeout: number; + maxRetries: number; + retryDelay: number; +} + +class HTTPClient { + constructor( + private authManager: AuthenticationManager, + private config: HTTPClientConfig + ) {} + + // Make authenticated request + async request( + url: string, + options: RequestOptions, + identity?: DIDIdentity + ): Promise + + // GET request + async get(url: string, identity?: DIDIdentity): Promise + + // POST request + async post( + url: string, + body: any, + identity?: DIDIdentity + ): Promise + + // Retry with exponential backoff + private async retryWithBackoff( + fn: () => Promise, + retries: number + ): Promise +} +``` + +### 9. Public API + +**Purpose**: Provide high-level, developer-friendly API. + +**Key Classes**: + +```typescript +interface ANPConfig { + did?: { + resolver?: DIDResolver; + }; + auth?: AuthConfig; + http?: HTTPClientConfig; + crypto?: CryptoConfig; + debug?: boolean; +} + +class ANPClient { + constructor(config?: ANPConfig) + + // DID operations + readonly did: { + create(domain: string, path?: string): Promise; + resolve(did: string): Promise; + sign(identity: DIDIdentity, data: Uint8Array): Promise; + verify(did: string, data: Uint8Array, signature: Signature): Promise; + }; + + // Agent description operations + readonly agent: { + createDescription(metadata: AgentMetadata): AgentDescription; + addInformation(description: AgentDescription, info: Information): AgentDescription; + addInterface(description: AgentDescription, iface: Interface): AgentDescription; + signDescription(description: AgentDescription, identity: DIDIdentity, challenge: string): Promise; + fetchDescription(url: string): Promise; + }; + + // Discovery operations + readonly discovery: { + discoverAgents(domain: string): Promise; + registerWithSearchService(searchServiceUrl: string, agentDescriptionUrl: string, identity: DIDIdentity): Promise; + searchAgents(searchServiceUrl: string, query: SearchQuery): Promise; + }; + + // Protocol operations + readonly protocol: { + createNegotiationMachine(config: MetaProtocolConfig): Actor; + sendMessage(remoteDID: string, message: any, identity: DIDIdentity): Promise; + receiveMessage(encryptedMessage: Uint8Array, identity: DIDIdentity): Promise; + }; + + // HTTP operations + readonly http: { + request(url: string, options: RequestOptions, identity?: DIDIdentity): Promise; + get(url: string, identity?: DIDIdentity): Promise; + post(url: string, body: any, identity?: DIDIdentity): Promise; + }; +} +``` + +## Data Models + +### DID Document + +```typescript +interface DIDDocument { + '@context': string[]; + id: string; + verificationMethod: VerificationMethod[]; + authentication: (string | VerificationMethod)[]; + keyAgreement?: VerificationMethod[]; + humanAuthorization?: (string | VerificationMethod)[]; + service?: ServiceEndpoint[]; +} +``` + +### Agent Description + +```typescript +interface AgentDescription { + protocolType: 'ANP'; + protocolVersion: string; + type: 'AgentDescription'; + url?: string; + name: string; + did?: string; + owner?: Organization; + description?: string; + created?: string; + securityDefinitions: Record; + security: string; + Infomations?: Information[]; + interfaces?: Interface[]; + proof?: Proof; +} +``` + +### Protocol Messages + +```typescript +interface ProtocolNegotiationMessage { + action: 'protocolNegotiation'; + sequenceId: number; + candidateProtocols: string; + modificationSummary?: string; + status: 'negotiating' | 'rejected' | 'accepted' | 'timeout'; +} + +interface CodeGenerationMessage { + action: 'codeGeneration'; + status: 'generated' | 'error'; +} + +interface TestCasesNegotiationMessage { + action: 'testCasesNegotiation'; + testCases: string; + modificationSummary?: string; + status: 'negotiating' | 'rejected' | 'accepted'; +} +``` + +## Error Handling + +### Error Hierarchy + +```typescript +class ANPError extends Error { + constructor(message: string, public code: string) { + super(message); + this.name = 'ANPError'; + } +} + +class DIDResolutionError extends ANPError { + constructor(did: string, cause?: Error) { + super(`Failed to resolve DID: ${did}`, 'DID_RESOLUTION_ERROR'); + this.cause = cause; + } +} + +class AuthenticationError extends ANPError { + constructor(message: string) { + super(message, 'AUTHENTICATION_ERROR'); + } +} + +class ProtocolNegotiationError extends ANPError { + constructor(message: string) { + super(message, 'PROTOCOL_NEGOTIATION_ERROR'); + } +} + +class NetworkError extends ANPError { + constructor(message: string, public statusCode?: number) { + super(message, 'NETWORK_ERROR'); + } +} + +class CryptoError extends ANPError { + constructor(message: string) { + super(message, 'CRYPTO_ERROR'); + } +} +``` + +### Error Handling Strategy + +1. **Network Errors**: Retry with exponential backoff (configurable) +2. **Authentication Errors**: Return immediately, no retry +3. **DID Resolution Errors**: Cache negative results temporarily, retry after timeout +4. **Protocol Errors**: Attempt error negotiation, fallback to rejection +5. **Crypto Errors**: Return immediately, log for debugging + +## Testing Strategy + +### Unit Tests + +Each module will have comprehensive unit tests covering: + +1. **DID Manager**: + - Key pair generation + - DID document creation + - DID resolution (mocked HTTP) + - Signing and verification + - Error cases (invalid DIDs, network failures) + +2. **Authentication Manager**: + - Auth header generation + - Auth header verification + - Token generation and validation + - Nonce uniqueness + - Timestamp validation + - Error cases (expired tokens, invalid signatures) + +3. **Agent Description Manager**: + - Description creation + - Information and interface addition + - Signature generation and verification + - JSON-LD parsing + - Error cases (invalid documents, signature mismatches) + +4. **Agent Discovery Manager**: + - Active discovery + - Pagination handling + - Passive registration + - Search functionality + - Error cases (404, malformed documents) + +5. **Meta-Protocol State Machine**: + - All state transitions + - Message handling + - Timeout handling + - Error recovery + - Edge cases (max rounds, invalid messages) + +6. **Protocol Message Handler**: + - Message encoding/decoding + - Protocol type handling + - Binary format correctness + - Error cases (malformed messages) + +7. **Cryptography Module**: + - Key generation + - Signing and verification + - ECDHE key exchange + - Encryption and decryption + - Error cases (invalid keys, corrupted data) + +### Integration Tests + +Integration tests will cover: + +1. **End-to-End Authentication Flow**: + - Create two DID identities + - Perform HTTP authentication + - Verify token exchange + - Make authenticated requests + +2. **Agent Discovery Flow**: + - Publish agent description + - Discover agents from domain + - Register with search service + - Search for agents + +3. **Protocol Negotiation Flow**: + - Initiate negotiation + - Exchange multiple rounds + - Reach agreement + - Generate code + - Execute tests + - Communicate with agreed protocol + +4. **Encrypted Communication**: + - Establish encrypted channel + - Send encrypted messages + - Receive and decrypt messages + - Verify end-to-end encryption + +### Test Infrastructure + +- **Test Framework**: Vitest +- **Mocking**: Vitest mocks for HTTP requests +- **Coverage**: Istanbul/c8 for code coverage +- **CI/CD**: GitHub Actions for automated testing +- **Test Data**: Fixtures for DID documents, agent descriptions, protocol messages + +### Test-Driven Development Workflow + +**CRITICAL: All development MUST follow strict TDD principles:** + +1. **RED**: Write failing unit test for new feature BEFORE any implementation +2. **GREEN**: Implement MINIMAL code to pass the test +3. **REFACTOR**: Improve code while keeping tests green +4. **EDGE CASES**: Add tests for edge cases and error conditions +5. **VERIFY**: Ensure all tests pass before moving to next task +6. **COVERAGE**: Maintain 80%+ code coverage at all times +7. **INTEGRATION**: Run integration tests after completing related units + +**Development Rules**: +- NO implementation code without a failing test first +- ALL unit tests MUST pass before proceeding to next task +- Integration tests MUST pass before considering a feature complete +- Tests are documentation - write clear, descriptive test names +- Mock external dependencies in unit tests +- Use real implementations in integration tests + +## Performance Considerations + +1. **DID Resolution Caching**: Cache resolved DID documents with TTL +2. **Connection Pooling**: Reuse HTTP connections +3. **Lazy Loading**: Load modules on demand +4. **Streaming**: Support streaming for large payloads +5. **Worker Threads**: Offload crypto operations to workers (optional) + +## Security Considerations + +1. **Key Storage**: Never store private keys in plain text +2. **Nonce Generation**: Use cryptographically secure random generator +3. **Signature Verification**: Always verify signatures before processing +4. **Token Expiration**: Enforce token expiration +5. **Rate Limiting**: Implement rate limiting for outgoing requests +6. **Input Validation**: Validate all inputs against schemas +7. **Secure Defaults**: Use secure defaults for all configurations + +## Deployment and Distribution + +1. **Package Manager**: Publish to npm +2. **Module Formats**: ESM and CommonJS +3. **TypeScript**: Include type definitions +4. **Documentation**: Generate API docs with TypeDoc +5. **Examples**: Provide example applications +6. **Versioning**: Follow semantic versioning + +## Future Enhancements + +1. **WebSocket Support**: Real-time bidirectional communication +2. **Plugin System**: Allow third-party extensions +3. **AI Integration**: Built-in LLM integration for protocol negotiation +4. **Monitoring**: Built-in telemetry and monitoring +5. **Multi-DID**: Support multiple DID identities per client +6. **Browser Support**: Full browser compatibility with Web Crypto API diff --git a/typescript/.kiro/specs/anp-typescript-sdk/requirements.md b/typescript/.kiro/specs/anp-typescript-sdk/requirements.md new file mode 100644 index 0000000..5683b55 --- /dev/null +++ b/typescript/.kiro/specs/anp-typescript-sdk/requirements.md @@ -0,0 +1,154 @@ +# Requirements Document + +## Introduction + +This document defines the requirements for developing a TypeScript SDK for the Agent Network Protocol (ANP). The SDK will enable developers to build ANP-compliant agents with support for identity authentication (did:wba), agent description (ADP), agent discovery (ADSP), and meta-protocol negotiation. The SDK will use XState v5 for state machine management to handle complex protocol flows and state transitions. + +**IMPORTANT**: All SDK code MUST be developed in a new `ts_sdk` directory at the root of the project. This directory will contain the complete TypeScript SDK implementation including source code, tests, configuration files, and documentation. + +## Glossary + +- **ANP_SDK**: The TypeScript Software Development Kit for implementing Agent Network Protocol functionality, located in the `ts_sdk` directory +- **ts_sdk_Directory**: The root directory containing all SDK source code, tests, and configuration files +- **Agent**: An autonomous software entity that can communicate and collaborate with other agents using ANP +- **DID_WBA**: Web-Based Agent Decentralized Identifier method for cross-platform identity authentication +- **ADP**: Agent Description Protocol for publishing agent capabilities and interfaces +- **ADSP**: Agent Discovery Service Protocol for discovering agents in the network +- **Meta_Protocol**: Protocol negotiation mechanism for agents to dynamically agree on communication protocols +- **State_Machine**: XState v5 state machine for managing protocol flows and transitions +- **HTTP_Client**: HTTP client for making requests to other agents and services +- **Crypto_Module**: Cryptographic module for signing, verification, and encryption operations + +## Requirements + +### Requirement 1: DID:WBA Identity Management + +**User Story:** As an agent developer, I want to create and manage DID:WBA identities, so that my agent can authenticate with other agents across platforms. + +#### Acceptance Criteria + +1. WHEN the developer creates a new DID:WBA identity, THE ANP_SDK SHALL generate a key pair and create a DID document conforming to the did:wba specification +2. WHEN the developer provides a domain and path, THE ANP_SDK SHALL construct a valid did:wba identifier following the format "did:wba:domain:path" +3. WHEN the developer requests to sign data, THE ANP_SDK SHALL use the private key to generate a signature and return the signature value +4. WHEN the developer provides a DID identifier, THE ANP_SDK SHALL resolve the DID document from the appropriate HTTPS endpoint +5. WHEN the developer provides signature data and a DID document, THE ANP_SDK SHALL verify the signature using the public key from the verification method + +### Requirement 2: HTTP Authentication with DID:WBA + +**User Story:** As an agent developer, I want to authenticate HTTP requests using DID:WBA, so that my agent can securely communicate with other agents. + +#### Acceptance Criteria + +1. WHEN the agent makes an initial HTTP request to another agent, THE ANP_SDK SHALL include the Authorization header with DID, nonce, timestamp, verification_method, and signature +2. WHEN the agent receives an HTTP request with DID:WBA authentication, THE ANP_SDK SHALL resolve the client DID document and verify the signature +3. WHEN signature verification succeeds, THE ANP_SDK SHALL generate an access token and return it to the client +4. WHEN the agent makes subsequent requests, THE ANP_SDK SHALL include the access token in the Authorization header +5. WHEN the agent receives a request with an access token, THE ANP_SDK SHALL validate the token and grant access if valid + +### Requirement 3: Agent Description Management + +**User Story:** As an agent developer, I want to create and publish agent description documents, so that other agents can discover my agent's capabilities and interfaces. + +#### Acceptance Criteria + +1. WHEN the developer provides agent metadata, THE ANP_SDK SHALL generate an ADP-compliant JSON-LD document with required fields +2. WHEN the developer adds information resources, THE ANP_SDK SHALL include them in the Infomations array with type, description, and url +3. WHEN the developer adds interfaces, THE ANP_SDK SHALL include them in the interfaces array with type, protocol, version, and url +4. WHEN the developer requests to sign the agent description, THE ANP_SDK SHALL generate a proof object with digital signature +5. WHEN the developer provides an agent description URL, THE ANP_SDK SHALL fetch and parse the agent description document + +### Requirement 4: Agent Discovery + +**User Story:** As an agent developer, I want to discover other agents in the network, so that my agent can find and interact with relevant services. + +#### Acceptance Criteria + +1. WHEN the developer provides a domain name, THE ANP_SDK SHALL fetch the agent discovery document from the .well-known/agent-descriptions endpoint +2. WHEN the discovery document contains pagination, THE ANP_SDK SHALL recursively fetch all pages until no next property exists +3. WHEN the developer registers with a search service, THE ANP_SDK SHALL send a registration request with the agent description URL +4. WHEN the developer searches for agents, THE ANP_SDK SHALL query the search service and return matching agent descriptions +5. WHEN the discovery process encounters errors, THE ANP_SDK SHALL handle errors gracefully and return appropriate error messages + +### Requirement 5: Meta-Protocol Negotiation State Machine + +**User Story:** As an agent developer, I want to negotiate communication protocols dynamically, so that my agent can communicate efficiently with heterogeneous agents. + +#### Acceptance Criteria + +1. WHEN the agent initiates protocol negotiation, THE State_Machine SHALL transition to the negotiating state and send a protocolNegotiation message +2. WHEN the agent receives a negotiation response, THE State_Machine SHALL process the candidateProtocols and determine whether to accept or continue negotiating +3. WHEN both agents agree on a protocol, THE State_Machine SHALL transition to the codeGeneration state +4. WHEN code generation completes, THE State_Machine SHALL send a codeGeneration message with status "generated" +5. WHEN the negotiation exceeds the maximum rounds, THE State_Machine SHALL transition to the rejected state and terminate negotiation + +### Requirement 6: Application Protocol Communication + +**User Story:** As an agent developer, I want to send and receive application protocol messages, so that my agent can exchange data with other agents using negotiated protocols. + +#### Acceptance Criteria + +1. WHEN the agent sends an application message, THE ANP_SDK SHALL construct a message with protocol type 01 and the application data +2. WHEN the agent receives an application message, THE ANP_SDK SHALL parse the protocol data according to the negotiated protocol +3. WHEN the agent sends a natural language message, THE ANP_SDK SHALL construct a message with protocol type 10 and UTF-8 encoded text +4. WHEN the agent receives a natural language message, THE ANP_SDK SHALL decode the UTF-8 text and pass it to the application +5. WHEN message processing fails, THE ANP_SDK SHALL generate appropriate error messages and handle the failure gracefully + +### Requirement 7: End-to-End Encryption + +**User Story:** As an agent developer, I want to encrypt messages end-to-end, so that my agent's communications remain private even through intermediaries. + +#### Acceptance Criteria + +1. WHEN the agent initiates encrypted communication, THE Crypto_Module SHALL perform ECDHE key exchange using the recipient's keyAgreement public key +2. WHEN the shared secret is established, THE Crypto_Module SHALL derive encryption keys using a key derivation function +3. WHEN the agent sends a message, THE Crypto_Module SHALL encrypt the message data using the derived encryption key +4. WHEN the agent receives an encrypted message, THE Crypto_Module SHALL decrypt the message using the shared secret +5. WHEN encryption or decryption fails, THE Crypto_Module SHALL return an error and prevent message processing + +### Requirement 8: Test Cases Negotiation + +**User Story:** As an agent developer, I want to negotiate test cases with other agents, so that protocol implementations can be verified before production use. + +#### Acceptance Criteria + +1. WHEN the agent proposes test cases, THE State_Machine SHALL send a testCasesNegotiation message with test case descriptions +2. WHEN the agent receives test case proposals, THE State_Machine SHALL evaluate the test cases and respond with acceptance or modifications +3. WHEN both agents agree on test cases, THE State_Machine SHALL execute the test cases and verify the results +4. WHEN test execution fails, THE State_Machine SHALL send a fixErrorNegotiation message with error descriptions +5. WHEN all tests pass, THE State_Machine SHALL transition to the ready state for production communication + +### Requirement 9: Error Handling and Recovery + +**User Story:** As an agent developer, I want robust error handling, so that my agent can recover from failures and continue operating. + +#### Acceptance Criteria + +1. WHEN network errors occur, THE ANP_SDK SHALL retry the request with exponential backoff up to a maximum number of attempts +2. WHEN DID resolution fails, THE ANP_SDK SHALL return a descriptive error message indicating the resolution failure +3. WHEN signature verification fails, THE ANP_SDK SHALL reject the request and return an authentication error +4. WHEN protocol negotiation fails, THE State_Machine SHALL transition to the rejected state and notify the application +5. WHEN unexpected errors occur, THE ANP_SDK SHALL log the error details and provide a generic error response + +### Requirement 10: Configuration and Extensibility + +**User Story:** As an agent developer, I want to configure the SDK behavior, so that I can customize it for my specific use case. + +#### Acceptance Criteria + +1. WHEN the developer initializes the SDK, THE ANP_SDK SHALL accept configuration options for timeouts, retry policies, and endpoints +2. WHEN the developer registers custom protocol handlers, THE ANP_SDK SHALL use the custom handlers for processing specific protocol types +3. WHEN the developer provides custom cryptographic implementations, THE ANP_SDK SHALL use the custom implementations instead of defaults +4. WHEN the developer enables debug mode, THE ANP_SDK SHALL log detailed information about protocol flows and state transitions +5. WHEN the developer extends the SDK with plugins, THE ANP_SDK SHALL load and execute the plugins at appropriate lifecycle hooks + +### Requirement 11: Test-Driven Development Support + +**User Story:** As an agent developer, I want comprehensive test coverage, so that I can trust the SDK implementation and ensure protocol compliance. + +#### Acceptance Criteria + +1. WHEN implementing any SDK feature, THE ANP_SDK SHALL have unit tests written before the implementation code +2. WHEN testing DID operations, THE ANP_SDK SHALL include tests for key generation, signing, verification, and DID resolution +3. WHEN testing protocol negotiation, THE ANP_SDK SHALL include tests for all state transitions and edge cases +4. WHEN testing HTTP authentication, THE ANP_SDK SHALL include tests for both successful and failed authentication scenarios +5. WHEN running the test suite, THE ANP_SDK SHALL achieve at least 80% code coverage across all modules diff --git a/typescript/.kiro/specs/anp-typescript-sdk/tasks.md b/typescript/.kiro/specs/anp-typescript-sdk/tasks.md new file mode 100644 index 0000000..dbabae1 --- /dev/null +++ b/typescript/.kiro/specs/anp-typescript-sdk/tasks.md @@ -0,0 +1,650 @@ +# Implementation Plan + +**CRITICAL**: All implementation MUST be done in the `ts_sdk` directory at the project root. Create this directory first before starting any development work. + +- [x] 1. Project Setup and Infrastructure + - Create `ts_sdk` directory at project root + - Initialize TypeScript project in `ts_sdk` with proper configuration for ESM and CommonJS + - Set up Vitest testing framework with coverage reporting + - Configure build tools (tsup or rollup) for bundling + - Set up linting (ESLint) and formatting (Prettier) + - Create project structure: `ts_sdk/src/`, `ts_sdk/tests/`, `ts_sdk/examples/`, `ts_sdk/docs/` + - Create subdirectories: `src/core/`, `src/protocol/`, `src/crypto/`, `src/transport/`, `src/types/`, `src/errors/` + - Configure GitHub Actions for CI/CD + - _Requirements: 10.1, 11.1_ + +- [x] 2. Cryptography Module Implementation + - All code in `ts_sdk/src/crypto/` directory + - All tests in `ts_sdk/tests/unit/crypto/` directory + - _Requirements: 1.1, 1.3, 7.1, 7.2, 7.3, 7.4_ + +- [x] 2.1 Write unit tests for key generation + - Create test file `ts_sdk/tests/unit/crypto/key-generation.test.ts` + - Test ECDSA secp256k1 key pair generation + - Test Ed25519 key pair generation + - Test X25519 key pair generation + - Test key format validation (JWK and Multibase) + - Test error handling for invalid key types + - _Requirements: 11.2_ + +- [x] 2.2 Implement key generation functions + - Create `ts_sdk/src/crypto/key-generation.ts` + - Implement generateKeyPair for ECDSA secp256k1 + - Implement generateKeyPair for Ed25519 + - Implement generateKeyPair for X25519 + - Implement key format conversion utilities + - _Requirements: 1.1_ + +- [x] 2.3 Write unit tests for signing and verification + - Test signing with ECDSA secp256k1 + - Test signing with Ed25519 + - Test signature verification with valid signatures + - Test signature verification with invalid signatures + - Test error handling for mismatched keys + - _Requirements: 11.2_ + +- [x] 2.4 Implement signing and verification functions + - Implement sign function for ECDSA secp256k1 + - Implement sign function for Ed25519 + - Implement verify function for both key types + - Implement signature encoding/decoding + - _Requirements: 1.3, 1.5_ + +- [x] 2.5 Write unit tests for ECDHE key exchange + - Test key exchange with X25519 keys + - Test shared secret derivation + - Test key derivation function + - Test error handling for invalid keys + - _Requirements: 11.2_ + +- [x] 2.6 Implement ECDHE key exchange + - Implement performKeyExchange function + - Implement deriveKey function using HKDF + - Implement shared secret validation + - _Requirements: 7.1, 7.2_ + +- [x] 2.7 Write unit tests for encryption and decryption + - Test AES-GCM encryption + - Test AES-GCM decryption + - Test IV generation + - Test authentication tag validation + - Test error handling for corrupted data + - _Requirements: 11.2_ + +- [x] 2.8 Implement encryption and decryption functions + - Implement encrypt function using AES-GCM + - Implement decrypt function + - Implement IV generation + - Implement error handling for decryption failures + - _Requirements: 7.3, 7.4, 7.5_ + +- [x] 3. DID Manager Implementation + - All code in `ts_sdk/src/core/did/` directory + - All tests in `ts_sdk/tests/unit/core/did/` directory + - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_ + +- [x] 3.1 Write unit tests for DID creation + - Create test file `ts_sdk/tests/unit/core/did/did-manager.test.ts` + - Test DID identifier construction from domain + - Test DID identifier construction with path + - Test DID identifier with port encoding + - Test DID document generation + - Test verification method creation + - Test error handling for invalid domains + - _Requirements: 11.2_ + +- [x] 3.2 Implement DID creation + - Create `ts_sdk/src/core/did/did-manager.ts` + - Create `ts_sdk/src/types/did.ts` for type definitions + - Implement createDID function + - Implement DID identifier construction + - Implement DID document generation + - Implement verification method creation + - Add authentication and keyAgreement sections + - _Requirements: 1.1, 1.2_ + +- [x] 3.3 Write unit tests for DID resolution + - Test resolution from .well-known path + - Test resolution from custom path + - Test resolution with port + - Test caching mechanism + - Test error handling for 404 responses + - Test error handling for invalid documents + - _Requirements: 11.2_ + +- [x] 3.4 Implement DID resolution + - Implement resolveDID function + - Implement URL construction from DID + - Implement HTTP fetching with retry + - Implement DID document validation + - Implement caching with TTL + - _Requirements: 1.4, 9.2_ + +- [x] 3.5 Write unit tests for DID operations + - Test signing with DID identity + - Test verification with resolved DID + - Test export of DID document + - Test error handling for missing keys + - _Requirements: 11.2_ + +- [x] 3.6 Implement DID operations + - Implement sign method using crypto module + - Implement verify method using crypto module + - Implement exportDocument method + - Integrate with cryptography module + - _Requirements: 1.3, 1.5_ + +- [x] 4. Authentication Manager Implementation + - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5_ + +- [x] 4.1 Write unit tests for auth header generation + - Test header format compliance + - Test nonce generation uniqueness + - Test timestamp format + - Test signature generation + - Test verification method selection + - _Requirements: 11.2, 11.4_ + +- [x] 4.2 Implement auth header generation + - Implement generateAuthHeader function + - Implement nonce generation + - Implement timestamp generation + - Implement signature data construction + - Implement header formatting + - _Requirements: 2.1_ + +- [x] 4.3 Write unit tests for auth header verification + - Test successful verification + - Test DID resolution during verification + - Test signature verification + - Test nonce replay prevention + - Test timestamp validation with clock skew + - Test expired timestamp rejection + - _Requirements: 11.2, 11.4_ + +- [x] 4.4 Implement auth header verification + - Implement verifyAuthHeader function + - Implement header parsing + - Implement DID resolution + - Implement signature verification + - Implement nonce tracking + - Implement timestamp validation + - _Requirements: 2.2, 9.3_ + +- [x] 4.5 Write unit tests for token management + - Test token generation + - Test token validation + - Test token expiration + - Test token format + - _Requirements: 11.2, 11.4_ + +- [x] 4.6 Implement token management + - Implement generateAccessToken function + - Implement verifyAccessToken function + - Implement JWT or similar token format + - Implement token expiration checking + - _Requirements: 2.3, 2.4, 2.5_ + +- [x] 5. HTTP Client Implementation + - _Requirements: 2.1, 2.4, 9.1_ + +- [x] 5.1 Write unit tests for HTTP client + - Test GET requests + - Test POST requests + - Test authenticated requests + - Test retry mechanism + - Test timeout handling + - Test error handling + - _Requirements: 11.2_ + +- [x] 5.2 Implement HTTP client + - Implement request method with authentication + - Implement GET and POST convenience methods + - Implement retry with exponential backoff + - Implement timeout handling + - Integrate with authentication manager + - _Requirements: 2.1, 2.4, 9.1_ + +- [x] 6. Protocol Message Handler Implementation + - _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_ + +- [x] 6.1 Write unit tests for message encoding + - Test meta-protocol message encoding + - Test application protocol message encoding + - Test natural language message encoding + - Test verification protocol message encoding + - Test binary format correctness + - _Requirements: 11.2_ + +- [x] 6.2 Implement message encoding + - Implement encode function + - Implement protocol type header construction + - Implement message serialization + - _Requirements: 6.1, 6.3_ + +- [x] 6.3 Write unit tests for message decoding + - Test meta-protocol message decoding + - Test application protocol message decoding + - Test natural language message decoding + - Test verification protocol message decoding + - Test error handling for malformed messages + - _Requirements: 11.2_ + +- [x] 6.4 Implement message decoding + - Implement decode function + - Implement protocol type extraction + - Implement message deserialization + - Implement error handling + - _Requirements: 6.2, 6.4, 6.5_ + +- [x] 6.5 Write unit tests for meta-protocol message parsing + - Test protocolNegotiation message parsing + - Test codeGeneration message parsing + - Test testCasesNegotiation message parsing + - Test fixErrorNegotiation message parsing + - Test naturalLanguageNegotiation message parsing + - _Requirements: 11.2_ + +- [x] 6.6 Implement meta-protocol message parsing + - Implement parseMetaProtocol function + - Implement JSON parsing and validation + - Implement message type discrimination + - _Requirements: 5.1, 5.2_ + +- [x] 7. Meta-Protocol State Machine Implementation + - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 8.1, 8.2, 8.3, 8.4, 8.5_ + +- [x] 7.1 Write unit tests for state machine initialization + - Test machine creation with config + - Test initial state + - Test context initialization + - _Requirements: 11.2, 11.3_ + +- [x] 7.2 Implement state machine setup + - Define state machine schema with XState v5 + - Implement context type definitions + - Implement event type definitions + - Create machine factory function + - _Requirements: 5.1_ + +- [x] 7.3 Write unit tests for negotiation flow + - Test initiate event handling + - Test receive_request event handling + - Test negotiate event handling + - Test accept transition + - Test reject transition + - Test timeout handling + - Test max rounds enforcement + - _Requirements: 11.2, 11.3_ + +- [x] 7.4 Implement negotiation states and transitions + - Implement Idle state + - Implement Negotiating state + - Implement negotiation guards + - Implement negotiation actions + - Implement sequence ID increment + - Implement max rounds checking + - _Requirements: 5.1, 5.2, 5.5_ + +- [x] 7.5 Write unit tests for code generation flow + - Test code_ready event handling + - Test code_error event handling + - Test transition to TestCases state + - Test transition to Failed state + - _Requirements: 11.2, 11.3_ + +- [x] 7.6 Implement code generation states + - Implement CodeGeneration state + - Implement code generation actions + - Implement error handling + - _Requirements: 5.3, 5.4_ + +- [x] 7.7 Write unit tests for test cases flow + - Test tests_agreed event handling + - Test skip_tests event handling + - Test tests_passed event handling + - Test tests_failed event handling + - _Requirements: 11.2, 11.3_ + +- [x] 7.8 Implement test cases states + - Implement TestCases state + - Implement Testing state + - Implement test execution actions + - _Requirements: 8.1, 8.2, 8.3_ + +- [x] 7.9 Write unit tests for error fixing flow + - Test fix_accepted event handling + - Test fix_rejected event handling + - Test transition back to CodeGeneration + - Test transition to Failed state + - _Requirements: 11.2, 11.3_ + +- [x] 7.10 Implement error fixing states + - Implement FixError state + - Implement error negotiation actions + - Implement fix acceptance logic + - _Requirements: 8.4, 8.5_ + +- [x] 7.11 Write unit tests for communication flow + - Test start_communication event handling + - Test protocol_error event handling + - Test end event handling + - _Requirements: 11.2, 11.3_ + +- [x] 7.12 Implement communication states + - Implement Ready state + - Implement Communicating state + - Implement protocol error handling + - Implement cleanup on end + - _Requirements: 5.1, 9.4_ + +- [x] 7.13 Write unit tests for message sending + - Test sendNegotiation function + - Test message construction + - Test message encryption + - Test message transmission + - _Requirements: 11.2, 11.3_ + +- [x] 7.14 Implement message sending + - Implement sendNegotiation function + - Integrate with protocol message handler + - Integrate with HTTP client + - Implement error handling + - _Requirements: 5.1, 5.2_ + +- [x] 7.15 Write unit tests for message processing + - Test processMessage function + - Test message decoding + - Test state machine event dispatch + - Test error handling + - _Requirements: 11.2, 11.3_ + +- [x] 7.16 Implement message processing + - Implement processMessage function + - Integrate with protocol message handler + - Implement event mapping + - Implement error handling + - _Requirements: 5.2, 6.5_ + +- [x] 8. Agent Description Manager Implementation + - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_ + +- [x] 8.1 Write unit tests for description creation + - Test createDescription function + - Test required fields validation + - Test security definitions + - Test JSON-LD context + - _Requirements: 11.2_ + +- [x] 8.2 Implement description creation + - Implement createDescription function + - Implement metadata validation + - Implement JSON-LD structure generation + - Implement security definitions setup + - _Requirements: 3.1_ + +- [x] 8.3 Write unit tests for adding resources + - Test addInformation function + - Test addInterface function + - Test resource validation + - Test duplicate prevention + - _Requirements: 11.2_ + +- [x] 8.4 Implement adding resources + - Implement addInformation function + - Implement addInterface function + - Implement resource validation + - _Requirements: 3.2, 3.3_ + +- [x] 8.5 Write unit tests for description signing + - Test signDescription function + - Test proof generation + - Test JCS canonicalization + - Test signature verification + - _Requirements: 11.2_ + +- [x] 8.6 Implement description signing + - Implement signDescription function + - Implement JCS canonicalization + - Implement proof object generation + - Integrate with crypto module + - _Requirements: 3.4_ + +- [x] 8.7 Write unit tests for description fetching + - Test fetchDescription function + - Test HTTP fetching + - Test JSON-LD parsing + - Test error handling + - _Requirements: 11.2_ + +- [x] 8.8 Implement description fetching + - Implement fetchDescription function + - Integrate with HTTP client + - Implement JSON-LD parsing + - Implement validation + - _Requirements: 3.5_ + +- [x] 8.9 Write unit tests for description verification + - Test verifyDescription function + - Test proof verification + - Test domain validation + - Test challenge validation + - _Requirements: 11.2_ + +- [x] 8.10 Implement description verification + - Implement verifyDescription function + - Implement proof verification + - Implement domain and challenge validation + - _Requirements: 3.5_ + +- [x] 9. Agent Discovery Manager Implementation + - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_ + +- [x] 9.1 Write unit tests for active discovery + - Test discoverAgents function + - Test .well-known URL construction + - Test discovery document parsing + - Test pagination handling + - Test error handling + - _Requirements: 11.2_ + +- [x] 9.2 Implement active discovery + - Implement discoverAgents function + - Implement URL construction + - Implement discovery document fetching + - Implement pagination recursion + - _Requirements: 4.1, 4.2_ + +- [x] 9.3 Write unit tests for passive discovery + - Test registerWithSearchService function + - Test registration request construction + - Test authentication + - Test error handling + - _Requirements: 11.2_ + +- [x] 9.4 Implement passive discovery + - Implement registerWithSearchService function + - Implement registration request + - Integrate with authentication + - _Requirements: 4.3_ + +- [x] 9.5 Write unit tests for agent search + - Test searchAgents function + - Test query construction + - Test result parsing + - Test error handling + - _Requirements: 11.2_ + +- [x] 9.6 Implement agent search + - Implement searchAgents function + - Implement query construction + - Implement result parsing + - _Requirements: 4.4, 4.5_ + +- [x] 10. Public API Implementation + - Main entry point at `ts_sdk/src/index.ts` + - All tests in `ts_sdk/tests/unit/api/` directory + - _Requirements: 10.1, 10.2, 10.3, 10.4, 10.5_ + +- [x] 10.1 Write unit tests for ANPClient initialization + - Create test file `ts_sdk/tests/unit/api/anp-client.test.ts` + - Test constructor with default config + - Test constructor with custom config + - Test module initialization + - _Requirements: 11.2_ + +- [x] 10.2 Implement ANPClient class + - Create `ts_sdk/src/index.ts` as main entry point + - Create `ts_sdk/src/anp-client.ts` for ANPClient class + - Implement constructor + - Implement config validation + - Initialize all managers + - Create public API namespaces + - _Requirements: 10.1_ + +- [x] 10.3 Write unit tests for DID API + - Test did.create + - Test did.resolve + - Test did.sign + - Test did.verify + - _Requirements: 11.2_ + +- [x] 10.4 Implement DID API + - Implement did namespace methods + - Delegate to DID manager + - Add error handling + - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_ + +- [x] 10.5 Write unit tests for Agent API + - Test agent.createDescription + - Test agent.addInformation + - Test agent.addInterface + - Test agent.signDescription + - Test agent.fetchDescription + - _Requirements: 11.2_ + +- [x] 10.6 Implement Agent API + - Implement agent namespace methods + - Delegate to agent description manager + - Add error handling + - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5_ + +- [x] 10.7 Write unit tests for Discovery API + - Test discovery.discoverAgents + - Test discovery.registerWithSearchService + - Test discovery.searchAgents + - _Requirements: 11.2_ + +- [x] 10.8 Implement Discovery API + - Implement discovery namespace methods + - Delegate to discovery manager + - Add error handling + - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5_ + +- [x] 10.9 Write unit tests for Protocol API + - Test protocol.createNegotiationMachine + - Test protocol.sendMessage + - Test protocol.receiveMessage + - _Requirements: 11.2_ + +- [x] 10.10 Implement Protocol API + - Implement protocol namespace methods + - Delegate to meta-protocol machine + - Add error handling + - _Requirements: 5.1, 5.2, 6.1, 6.2_ + +- [x] 10.11 Write unit tests for HTTP API + - Test http.request + - Test http.get + - Test http.post + - _Requirements: 11.2_ + +- [x] 10.12 Implement HTTP API + - Implement http namespace methods + - Delegate to HTTP client + - Add error handling + - _Requirements: 2.1, 2.4_ + +- [x] 11. Integration Tests + - All integration tests in `ts_sdk/tests/integration/` directory + - _Requirements: 11.5_ + +- [x] 11.1 Write end-to-end authentication integration test + - Create test file `ts_sdk/tests/integration/authentication.test.ts` + - Create two DID identities + - Perform initial authentication + - Verify token exchange + - Make authenticated requests + - Verify access control + - _Requirements: 11.5_ + +- [x] 11.2 Write agent discovery integration test + - Create agent description + - Publish to mock server + - Discover agents from domain + - Register with search service + - Search for agents + - _Requirements: 11.5_ + +- [x] 11.3 Write protocol negotiation integration test + - Create two agents + - Initiate negotiation + - Exchange multiple rounds + - Reach agreement + - Generate code (mocked) + - Execute tests + - Communicate with agreed protocol + - _Requirements: 11.5_ + +- [x] 11.4 Write encrypted communication integration test + - Create two DID identities with keyAgreement + - Establish encrypted channel + - Send encrypted messages + - Receive and decrypt messages + - Verify end-to-end encryption + - _Requirements: 11.5_ + +- [x] 12. Documentation and Examples + - All documentation in `ts_sdk/docs/` directory + - All examples in `ts_sdk/examples/` directory + - Main README at `ts_sdk/README.md` + - _Requirements: 10.1_ + +- [x] 12.1 Write API documentation + - Create `ts_sdk/docs/` directory + - Generate TypeDoc documentation + - Write `ts_sdk/docs/getting-started.md` + - Write `ts_sdk/docs/api-reference.md` + - Document configuration options in `ts_sdk/docs/configuration.md` + - Document error codes in `ts_sdk/docs/errors.md` + +- [x] 12.2 Create example applications + - Create `ts_sdk/examples/simple-agent/` directory with example + - Create `ts_sdk/examples/authentication/` directory with example + - Create `ts_sdk/examples/discovery/` directory with example + - Create `ts_sdk/examples/protocol-negotiation/` directory with example + - Create `ts_sdk/examples/encrypted-communication/` directory with example + +- [x] 12.3 Write README and contributing guide + - Write comprehensive `ts_sdk/README.md` + - Document installation + - Document basic usage + - Write `ts_sdk/CONTRIBUTING.md` + - Document development setup + +- [x] 13. Build and Release + - All build configuration in `ts_sdk/` directory + - _Requirements: 10.1_ + +- [x] 13.1 Configure build process + - Create `ts_sdk/tsup.config.ts` or `ts_sdk/rollup.config.js` + - Configure ESM and CommonJS outputs + - Configure type definitions generation + - Test build outputs in `ts_sdk/dist/` + +- [x] 13.2 Prepare for npm release + - Configure `ts_sdk/package.json` with proper metadata + - Add npm scripts for build, test, and publish + - Test package installation from `ts_sdk/` + - Create `ts_sdk/CHANGELOG.md` + - Create release notes diff --git a/typescript/ts_sdk/.eslintrc.json b/typescript/ts_sdk/.eslintrc.json new file mode 100644 index 0000000..9afd50d --- /dev/null +++ b/typescript/ts_sdk/.eslintrc.json @@ -0,0 +1,35 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module", + "project": "./tsconfig.json" + }, + "plugins": ["@typescript-eslint", "prettier"], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "prettier" + ], + "rules": { + "prettier/prettier": "error", + "@typescript-eslint/no-unused-vars": [ + "error", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } + ], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-non-null-assertion": "warn" + }, + "env": { + "node": true, + "es2022": true + }, + "ignorePatterns": ["dist", "node_modules", "*.config.ts", "*.config.js"] +} diff --git a/typescript/ts_sdk/.github/workflows/ci.yml b/typescript/ts_sdk/.github/workflows/ci.yml new file mode 100644 index 0000000..64038f3 --- /dev/null +++ b/typescript/ts_sdk/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + push: + branches: [main, develop] + paths: + - 'ts_sdk/**' + pull_request: + branches: [main, develop] + paths: + - 'ts_sdk/**' + +jobs: + test: + name: Test + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x, 20.x] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + cache-dependency-path: ts_sdk/package-lock.json + + - name: Install dependencies + working-directory: ts_sdk + run: npm ci + + - name: Run type check + working-directory: ts_sdk + run: npm run typecheck + + - name: Run linter + working-directory: ts_sdk + run: npm run lint + + - name: Run tests + working-directory: ts_sdk + run: npm run test:coverage + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + files: ./ts_sdk/coverage/lcov.info + flags: typescript-sdk + name: ts-sdk-coverage + + build: + name: Build + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20.x + cache: 'npm' + cache-dependency-path: ts_sdk/package-lock.json + + - name: Install dependencies + working-directory: ts_sdk + run: npm ci + + - name: Build package + working-directory: ts_sdk + run: npm run build + + - name: Check build outputs + working-directory: ts_sdk + run: | + test -f dist/index.js || exit 1 + test -f dist/index.cjs || exit 1 + test -f dist/index.d.ts || exit 1 + + - name: Upload build artifacts + uses: actions/upload-artifact@v3 + with: + name: dist + path: ts_sdk/dist/ diff --git a/typescript/ts_sdk/.gitignore b/typescript/ts_sdk/.gitignore new file mode 100644 index 0000000..be62e43 --- /dev/null +++ b/typescript/ts_sdk/.gitignore @@ -0,0 +1,37 @@ +# Dependencies +node_modules/ + +# Build outputs +dist/ +*.tsbuildinfo + +# Test coverage +coverage/ +.nyc_output/ + +# Environment variables +.env +.env.local +.env.*.local + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Temporary files +*.tmp +.cache/ diff --git a/typescript/ts_sdk/.prettierignore b/typescript/ts_sdk/.prettierignore new file mode 100644 index 0000000..d8ec025 --- /dev/null +++ b/typescript/ts_sdk/.prettierignore @@ -0,0 +1,4 @@ +node_modules +dist +coverage +*.md diff --git a/typescript/ts_sdk/.prettierrc.json b/typescript/ts_sdk/.prettierrc.json new file mode 100644 index 0000000..32a2397 --- /dev/null +++ b/typescript/ts_sdk/.prettierrc.json @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "arrowParens": "always", + "endOfLine": "lf" +} diff --git a/typescript/ts_sdk/README.md b/typescript/ts_sdk/README.md new file mode 100644 index 0000000..fab8ff1 --- /dev/null +++ b/typescript/ts_sdk/README.md @@ -0,0 +1,291 @@ +# ANP TypeScript SDK + +TypeScript SDK for the Agent Network Protocol (ANP), enabling developers to build intelligent agents that can authenticate, discover, and communicate with other agents in a decentralized network. + +## Features + +- 🔐 **DID:WBA Identity Management** - Create and manage decentralized identities +- 🔑 **HTTP Authentication** - Secure agent-to-agent authentication +- 📋 **Agent Description Protocol** - Publish and discover agent capabilities +- 🔍 **Agent Discovery** - Find agents through active and passive mechanisms +- 🤝 **Meta-Protocol Negotiation** - Dynamically negotiate communication protocols +- 🔒 **End-to-End Encryption** - Secure message encryption +- 🎯 **Type-Safe** - Full TypeScript support with comprehensive type definitions +- ⚡ **Modern** - Built with ESM and CommonJS support + +## Installation + +```bash +npm install @anp/typescript-sdk +``` + +## Quick Start + +```typescript +import { ANPClient } from '@anp/typescript-sdk'; + +// Initialize the SDK +const client = new ANPClient({ + debug: true, + http: { timeout: 10000 } +}); + +// Create a DID identity +const identity = await client.did.create({ + domain: 'example.com', + path: 'agent1' +}); + +console.log('Created DID:', identity.did); +// Output: did:wba:example.com:agent1 + +// Create an agent description +let description = client.agent.createDescription({ + name: 'My Agent', + description: 'An intelligent ANP agent', + protocolVersion: '0.1.0', + did: identity.did +}); + +// Add an interface +description = client.agent.addInterface(description, { + type: 'Interface', + protocol: 'HTTP', + version: '1.1', + url: 'https://example.com/api' +}); + +// Sign the description +const signedDescription = await client.agent.signDescription( + description, + identity, + 'challenge-string', + 'example.com' +); + +// Discover agents +const agents = await client.discovery.discoverAgents('example.com', identity); +console.log(`Found ${agents.length} agents`); + +// Make authenticated HTTP requests +const response = await client.http.get( + 'https://other-agent.com/api/data', + identity +); +``` + +## Documentation + +### Guides +- [Getting Started Guide](./docs/getting-started.md) - Learn the basics +- [API Reference](./docs/api-reference.md) - Complete API documentation +- [Configuration Guide](./docs/configuration.md) - Configuration options +- [Error Handling](./docs/errors.md) - Error types and handling + +### Examples +- [Simple Agent](./examples/simple-agent/) - Basic agent creation +- [Authentication](./examples/authentication/) - DID:WBA authentication +- [Discovery](./examples/discovery/) - Agent discovery +- [Protocol Negotiation](./examples/protocol-negotiation/) - Meta-protocol negotiation +- [Encrypted Communication](./examples/encrypted-communication/) - End-to-end encryption + +## Development + +### Prerequisites + +- Node.js >= 18.0.0 +- npm or yarn + +### Setup + +```bash +# Install dependencies +npm install + +# Run tests +npm test + +# Run tests with coverage +npm run test:coverage + +# Build the package +npm run build + +# Run linter +npm run lint + +# Format code +npm run format +``` + +### Project Structure + +``` +ts_sdk/ +├── src/ # Source code +│ ├── core/ # Core modules (DID, Auth, ADP, Discovery) +│ ├── protocol/ # Protocol layer (Meta-protocol, Message Handler) +│ ├── crypto/ # Cryptography module +│ ├── transport/ # Transport layer (HTTP, WebSocket) +│ ├── types/ # TypeScript type definitions +│ ├── errors/ # Error classes +│ └── index.ts # Public API entry point +├── tests/ # Test files +│ ├── unit/ # Unit tests +│ └── integration/ # Integration tests +├── examples/ # Example applications +└── docs/ # Documentation +``` + +## Contributing + +Contributions are welcome! Please read our [Contributing Guide](./CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. + +## License + +MIT License - see the [LICENSE](../LICENSE) file for details. + +## Links + +- [ANP Specification](https://github.com/chgaowei/AgentNetworkProtocol) +- [Issue Tracker](https://github.com/chgaowei/AgentNetworkProtocol/issues) +- [Discussions](https://github.com/chgaowei/AgentNetworkProtocol/discussions) + +## Core Concepts + +### DID:WBA (Web-Based Agent DID) + +Decentralized identifiers for agents that can be resolved via HTTPS: + +```typescript +// Create a DID +const identity = await client.did.create({ + domain: 'example.com', + path: 'agent1' +}); +// Result: did:wba:example.com:agent1 + +// Resolve a DID +const document = await client.did.resolve('did:wba:example.com:agent1'); +// Fetches from: https://example.com/.well-known/did.json +``` + +### Agent Description Protocol (ADP) + +Describe your agent's capabilities and interfaces: + +```typescript +const description = client.agent.createDescription({ + name: 'Translation Agent', + description: 'Translates text between languages', + protocolVersion: '0.1.0' +}); + +// Add interfaces +description = client.agent.addInterface(description, { + type: 'Interface', + protocol: 'HTTP', + version: '1.1', + url: 'https://example.com/translate' +}); +``` + +### Agent Discovery + +Find agents through active or passive discovery: + +```typescript +// Active: Discover from a domain +const agents = await client.discovery.discoverAgents('example.com'); + +// Passive: Register with search service +await client.discovery.registerWithSearchService( + 'https://search.anp.network', + 'https://myagent.com/description.json', + identity +); + +// Search for agents +const results = await client.discovery.searchAgents( + 'https://search.anp.network', + { keywords: 'translation' } +); +``` + +### Meta-Protocol Negotiation + +Dynamically negotiate communication protocols: + +```typescript +const machine = client.protocol.createNegotiationMachine({ + localIdentity: identity, + remoteDID: 'did:wba:other.com:agent', + candidateProtocols: 'JSON-RPC 2.0, GraphQL', + maxNegotiationRounds: 5, + onStateChange: (state) => { + console.log('State:', state.value); + } +}); + +machine.start(); +machine.send({ type: 'initiate', remoteDID: '...', candidateProtocols: '...' }); +``` + +### End-to-End Encryption + +Secure communication with ECDHE and AES-256-GCM: + +```typescript +// Key exchange happens automatically when using the SDK +// Messages are encrypted end-to-end between agents + +// Send encrypted message +await client.protocol.sendMessage( + 'did:wba:other.com:agent', + { data: 'secret message' }, + identity +); +``` + +## Use Cases + +- **AI Agent Networks** - Build networks of AI agents that can discover and communicate +- **Decentralized Services** - Create decentralized service architectures +- **Secure Communication** - Implement secure agent-to-agent communication +- **Protocol Interoperability** - Enable agents with different protocols to communicate +- **Identity Management** - Manage decentralized identities for agents + +## Requirements + +- Node.js >= 18.0.0 +- TypeScript >= 5.0.0 (for development) + +## Browser Support + +The SDK is designed for Node.js environments. Browser support is planned for future releases. + +## Status + +🚧 **Under Active Development** - This SDK is currently in early development. APIs may change. + +Current version: 0.1.0 + +### Roadmap + +- [x] DID:WBA identity management +- [x] HTTP authentication +- [x] Agent description protocol +- [x] Agent discovery +- [x] Meta-protocol negotiation +- [x] End-to-end encryption +- [ ] WebSocket transport +- [ ] Browser support +- [ ] Plugin system +- [ ] Performance optimizations + +## Acknowledgments + +Built with: +- [XState](https://xstate.js.org/) - State machine management +- [TypeScript](https://www.typescriptlang.org/) - Type safety +- [Vitest](https://vitest.dev/) - Testing framework diff --git a/typescript/ts_sdk/docs/.gitkeep b/typescript/ts_sdk/docs/.gitkeep new file mode 100644 index 0000000..507ffff --- /dev/null +++ b/typescript/ts_sdk/docs/.gitkeep @@ -0,0 +1 @@ +# Documentation will be added here diff --git a/typescript/ts_sdk/docs/api-reference.md b/typescript/ts_sdk/docs/api-reference.md new file mode 100644 index 0000000..e839cac --- /dev/null +++ b/typescript/ts_sdk/docs/api-reference.md @@ -0,0 +1,521 @@ +# API Reference + +Complete API reference for the ANP TypeScript SDK. + +## ANPClient + +The main entry point for the SDK. + +### Constructor + +```typescript +new ANPClient(config?: ANPConfig) +``` + +Creates a new ANP client instance. + +**Parameters:** +- `config` (optional): Configuration options for the client + +**Example:** +```typescript +const client = new ANPClient({ + debug: true, + http: { timeout: 15000 } +}); +``` + +## DID Operations + +### client.did.create() + +```typescript +async create(options: CreateDIDOptions): Promise +``` + +Creates a new DID:WBA identity with key pairs. + +**Parameters:** +- `options.domain`: The domain for the DID +- `options.path` (optional): The path component of the DID +- `options.keyTypes` (optional): Array of key types to generate + +**Returns:** Promise resolving to a DIDIdentity object + +**Example:** +```typescript +const identity = await client.did.create({ + domain: 'example.com', + path: 'agent1' +}); +``` + +### client.did.resolve() + +```typescript +async resolve(did: string): Promise +``` + +Resolves a DID to its DID document. + +**Parameters:** +- `did`: The DID identifier to resolve + +**Returns:** Promise resolving to a DIDDocument + +**Example:** +```typescript +const document = await client.did.resolve('did:wba:example.com:agent1'); +``` + +### client.did.sign() + +```typescript +async sign(identity: DIDIdentity, data: Uint8Array): Promise +``` + +Signs data using a DID identity's private key. + +**Parameters:** +- `identity`: The DID identity to sign with +- `data`: The data to sign + +**Returns:** Promise resolving to a Signature object + +**Example:** +```typescript +const data = new TextEncoder().encode('message'); +const signature = await client.did.sign(identity, data); +``` + +### client.did.verify() + +```typescript +async verify(did: string, data: Uint8Array, signature: Signature): Promise +``` + +Verifies a signature against a DID's public key. + +**Parameters:** +- `did`: The DID identifier +- `data`: The signed data +- `signature`: The signature to verify + +**Returns:** Promise resolving to true if valid, false otherwise + +**Example:** +```typescript +const isValid = await client.did.verify( + 'did:wba:example.com:agent1', + data, + signature +); +``` + +## Agent Description Operations + +### client.agent.createDescription() + +```typescript +createDescription(metadata: AgentMetadata): AgentDescription +``` + +Creates a new agent description document. + +**Parameters:** +- `metadata.name`: Agent name +- `metadata.description` (optional): Agent description +- `metadata.protocolVersion`: ANP protocol version +- `metadata.did` (optional): Agent's DID +- `metadata.owner` (optional): Owner organization + +**Returns:** AgentDescription object + +**Example:** +```typescript +const description = client.agent.createDescription({ + name: 'My Agent', + description: 'An intelligent agent', + protocolVersion: '0.1.0' +}); +``` + +### client.agent.addInformation() + +```typescript +addInformation(description: AgentDescription, info: Information): AgentDescription +``` + +Adds an information resource to an agent description. + +**Parameters:** +- `description`: The agent description to modify +- `info`: Information resource to add + +**Returns:** Updated AgentDescription + +**Example:** +```typescript +const updated = client.agent.addInformation(description, { + type: 'Information', + description: 'API Documentation', + url: 'https://example.com/docs' +}); +``` + +### client.agent.addInterface() + +```typescript +addInterface(description: AgentDescription, iface: Interface): AgentDescription +``` + +Adds an interface to an agent description. + +**Parameters:** +- `description`: The agent description to modify +- `iface`: Interface to add + +**Returns:** Updated AgentDescription + +**Example:** +```typescript +const updated = client.agent.addInterface(description, { + type: 'Interface', + protocol: 'HTTP', + version: '1.1', + url: 'https://example.com/api' +}); +``` + +### client.agent.signDescription() + +```typescript +async signDescription( + description: AgentDescription, + identity: DIDIdentity, + challenge: string, + domain: string +): Promise +``` + +Signs an agent description with a DID identity. + +**Parameters:** +- `description`: The agent description to sign +- `identity`: The DID identity to sign with +- `challenge`: Challenge string for the proof +- `domain`: Domain for the proof + +**Returns:** Promise resolving to signed AgentDescription + +**Example:** +```typescript +const signed = await client.agent.signDescription( + description, + identity, + 'challenge-123', + 'example.com' +); +``` + +### client.agent.fetchDescription() + +```typescript +async fetchDescription(url: string): Promise +``` + +Fetches and parses an agent description from a URL. + +**Parameters:** +- `url`: URL of the agent description + +**Returns:** Promise resolving to AgentDescription + +**Example:** +```typescript +const description = await client.agent.fetchDescription( + 'https://example.com/agent-description.json' +); +``` + +## Discovery Operations + +### client.discovery.discoverAgents() + +```typescript +async discoverAgents(domain: string, identity?: DIDIdentity): Promise +``` + +Discovers agents from a domain using the ADSP protocol. + +**Parameters:** +- `domain`: The domain to discover agents from +- `identity` (optional): DID identity for authenticated requests + +**Returns:** Promise resolving to array of agent description items + +**Example:** +```typescript +const agents = await client.discovery.discoverAgents('example.com', identity); +``` + +### client.discovery.registerWithSearchService() + +```typescript +async registerWithSearchService( + searchServiceUrl: string, + agentDescriptionUrl: string, + identity: DIDIdentity +): Promise +``` + +Registers an agent with a search service. + +**Parameters:** +- `searchServiceUrl`: URL of the search service +- `agentDescriptionUrl`: URL of the agent's description +- `identity`: DID identity for authentication + +**Returns:** Promise resolving when registration is complete + +**Example:** +```typescript +await client.discovery.registerWithSearchService( + 'https://search.example.com', + 'https://myagent.com/description.json', + identity +); +``` + +### client.discovery.searchAgents() + +```typescript +async searchAgents( + searchServiceUrl: string, + query: SearchQuery, + identity?: DIDIdentity +): Promise +``` + +Searches for agents using a search service. + +**Parameters:** +- `searchServiceUrl`: URL of the search service +- `query`: Search query parameters +- `identity` (optional): DID identity for authenticated requests + +**Returns:** Promise resolving to array of matching agents + +**Example:** +```typescript +const results = await client.discovery.searchAgents( + 'https://search.example.com', + { keywords: 'translation', capabilities: ['text'] }, + identity +); +``` + +## Protocol Operations + +### client.protocol.createNegotiationMachine() + +```typescript +createNegotiationMachine(config: MetaProtocolConfig): MetaProtocolActor +``` + +Creates a meta-protocol negotiation state machine. + +**Parameters:** +- `config.localIdentity`: Local DID identity +- `config.remoteDID`: Remote agent's DID +- `config.candidateProtocols`: Proposed protocols +- `config.maxNegotiationRounds`: Maximum negotiation rounds +- `config.onStateChange` (optional): State change callback + +**Returns:** XState actor for the state machine + +**Example:** +```typescript +const machine = client.protocol.createNegotiationMachine({ + localIdentity: identity, + remoteDID: 'did:wba:other.com:agent', + candidateProtocols: 'JSON-RPC 2.0', + maxNegotiationRounds: 5, + onStateChange: (state) => console.log(state.value) +}); +``` + +### client.protocol.sendMessage() + +```typescript +async sendMessage(remoteDID: string, message: any, identity: DIDIdentity): Promise +``` + +Sends a protocol message to a remote agent. + +**Parameters:** +- `remoteDID`: Remote agent's DID +- `message`: Message to send +- `identity`: Local DID identity + +**Returns:** Promise resolving when message is sent + +**Example:** +```typescript +await client.protocol.sendMessage( + 'did:wba:other.com:agent', + { action: 'protocolNegotiation', candidateProtocols: 'HTTP' }, + identity +); +``` + +### client.protocol.receiveMessage() + +```typescript +receiveMessage(encryptedMessage: Uint8Array, actor: MetaProtocolActor): void +``` + +Processes a received protocol message. + +**Parameters:** +- `encryptedMessage`: The received message bytes +- `actor`: The state machine actor to process the message + +**Example:** +```typescript +client.protocol.receiveMessage(messageBytes, machine); +``` + +## HTTP Operations + +### client.http.request() + +```typescript +async request( + url: string, + options: RequestOptions, + identity?: DIDIdentity +): Promise +``` + +Makes an HTTP request with optional DID authentication. + +**Parameters:** +- `url`: Request URL +- `options`: Request options (method, headers, body) +- `identity` (optional): DID identity for authentication + +**Returns:** Promise resolving to Response + +**Example:** +```typescript +const response = await client.http.request( + 'https://example.com/api', + { method: 'POST', body: JSON.stringify(data) }, + identity +); +``` + +### client.http.get() + +```typescript +async get(url: string, identity?: DIDIdentity): Promise +``` + +Makes a GET request. + +**Parameters:** +- `url`: Request URL +- `identity` (optional): DID identity for authentication + +**Returns:** Promise resolving to Response + +**Example:** +```typescript +const response = await client.http.get('https://example.com/api', identity); +const data = await response.json(); +``` + +### client.http.post() + +```typescript +async post(url: string, body: any, identity?: DIDIdentity): Promise +``` + +Makes a POST request. + +**Parameters:** +- `url`: Request URL +- `body`: Request body +- `identity` (optional): DID identity for authentication + +**Returns:** Promise resolving to Response + +**Example:** +```typescript +const response = await client.http.post( + 'https://example.com/api', + { data: 'value' }, + identity +); +``` + +## Type Definitions + +### DIDIdentity + +```typescript +interface DIDIdentity { + did: string; + document: DIDDocument; + privateKeys: Map; +} +``` + +### DIDDocument + +```typescript +interface DIDDocument { + '@context': string[]; + id: string; + verificationMethod: VerificationMethod[]; + authentication: (string | VerificationMethod)[]; + keyAgreement?: VerificationMethod[]; + humanAuthorization?: (string | VerificationMethod)[]; + service?: ServiceEndpoint[]; +} +``` + +### AgentDescription + +```typescript +interface AgentDescription { + protocolType: 'ANP'; + protocolVersion: string; + type: 'AgentDescription'; + url?: string; + name: string; + did?: string; + owner?: Organization; + description?: string; + created?: string; + securityDefinitions: Record; + security: string; + Infomations?: Information[]; + interfaces?: Interface[]; + proof?: Proof; +} +``` + +### Signature + +```typescript +interface Signature { + verificationMethodId: string; + signature: Uint8Array; +} +``` + +## Error Types + +See [Error Handling](./errors.md) for detailed error documentation. diff --git a/typescript/ts_sdk/docs/configuration.md b/typescript/ts_sdk/docs/configuration.md new file mode 100644 index 0000000..61a741e --- /dev/null +++ b/typescript/ts_sdk/docs/configuration.md @@ -0,0 +1,432 @@ +# Configuration Guide + +This guide covers all configuration options available in the ANP TypeScript SDK. + +## ANPConfig + +The main configuration object passed to the ANPClient constructor. + +```typescript +interface ANPConfig { + did?: DIDManagerConfig; + auth?: AuthConfig; + http?: HTTPClientConfig; + debug?: boolean; +} +``` + +## DID Configuration + +Configure DID resolution and caching behavior. + +```typescript +interface DIDManagerConfig { + cacheTTL?: number; + timeout?: number; +} +``` + +### Options + +#### cacheTTL +- **Type:** `number` +- **Default:** `300000` (5 minutes) +- **Description:** Time-to-live for cached DID documents in milliseconds + +**Example:** +```typescript +const client = new ANPClient({ + did: { + cacheTTL: 600000 // Cache for 10 minutes + } +}); +``` + +#### timeout +- **Type:** `number` +- **Default:** `10000` (10 seconds) +- **Description:** Timeout for DID resolution requests in milliseconds + +**Example:** +```typescript +const client = new ANPClient({ + did: { + timeout: 5000 // 5 second timeout + } +}); +``` + +## Authentication Configuration + +Configure DID:WBA authentication behavior. + +```typescript +interface AuthConfig { + maxTokenAge?: number; + nonceLength?: number; + clockSkewTolerance?: number; +} +``` + +### Options + +#### maxTokenAge +- **Type:** `number` +- **Default:** `3600000` (1 hour) +- **Description:** Maximum age for access tokens in milliseconds + +**Example:** +```typescript +const client = new ANPClient({ + auth: { + maxTokenAge: 1800000 // 30 minutes + } +}); +``` + +#### nonceLength +- **Type:** `number` +- **Default:** `32` +- **Description:** Length of nonce in bytes for authentication + +**Example:** +```typescript +const client = new ANPClient({ + auth: { + nonceLength: 64 // 64 bytes + } +}); +``` + +#### clockSkewTolerance +- **Type:** `number` +- **Default:** `300` (5 minutes) +- **Description:** Tolerance for clock skew in seconds when verifying timestamps + +**Example:** +```typescript +const client = new ANPClient({ + auth: { + clockSkewTolerance: 600 // 10 minutes + } +}); +``` + +## HTTP Configuration + +Configure HTTP client behavior including timeouts and retries. + +```typescript +interface HTTPClientConfig { + timeout?: number; + maxRetries?: number; + retryDelay?: number; +} +``` + +### Options + +#### timeout +- **Type:** `number` +- **Default:** `10000` (10 seconds) +- **Description:** Timeout for HTTP requests in milliseconds + +**Example:** +```typescript +const client = new ANPClient({ + http: { + timeout: 30000 // 30 seconds + } +}); +``` + +#### maxRetries +- **Type:** `number` +- **Default:** `3` +- **Description:** Maximum number of retry attempts for failed requests + +**Example:** +```typescript +const client = new ANPClient({ + http: { + maxRetries: 5 // Retry up to 5 times + } +}); +``` + +#### retryDelay +- **Type:** `number` +- **Default:** `1000` (1 second) +- **Description:** Initial delay between retries in milliseconds (uses exponential backoff) + +**Example:** +```typescript +const client = new ANPClient({ + http: { + retryDelay: 2000 // Start with 2 second delay + } +}); +``` + +## Debug Mode + +Enable debug logging for troubleshooting. + +#### debug +- **Type:** `boolean` +- **Default:** `false` +- **Description:** Enable detailed debug logging + +**Example:** +```typescript +const client = new ANPClient({ + debug: true +}); +``` + +When enabled, the SDK will log: +- DID resolution attempts and results +- Authentication header generation and verification +- HTTP requests and responses +- Protocol negotiation state transitions +- Error details + +## Complete Configuration Example + +```typescript +import { ANPClient } from '@anp/typescript-sdk'; + +const client = new ANPClient({ + // DID configuration + did: { + cacheTTL: 600000, // Cache DID documents for 10 minutes + timeout: 15000, // 15 second timeout for resolution + }, + + // Authentication configuration + auth: { + maxTokenAge: 1800000, // Tokens valid for 30 minutes + nonceLength: 64, // 64-byte nonces + clockSkewTolerance: 600, // 10 minute clock skew tolerance + }, + + // HTTP configuration + http: { + timeout: 30000, // 30 second request timeout + maxRetries: 5, // Retry up to 5 times + retryDelay: 2000, // Start with 2 second delay + }, + + // Enable debug logging + debug: process.env.NODE_ENV === 'development', +}); +``` + +## Environment-Specific Configuration + +### Development + +```typescript +const devClient = new ANPClient({ + debug: true, + http: { + timeout: 60000, // Longer timeout for debugging + maxRetries: 1, // Fewer retries to fail fast + }, +}); +``` + +### Production + +```typescript +const prodClient = new ANPClient({ + debug: false, + did: { + cacheTTL: 900000, // Longer cache for performance + }, + http: { + timeout: 10000, + maxRetries: 3, + retryDelay: 1000, + }, + auth: { + maxTokenAge: 3600000, + clockSkewTolerance: 300, + }, +}); +``` + +### Testing + +```typescript +const testClient = new ANPClient({ + debug: true, + did: { + cacheTTL: 0, // No caching for tests + timeout: 5000, + }, + http: { + timeout: 5000, + maxRetries: 0, // No retries in tests + }, +}); +``` + +## Meta-Protocol Configuration + +When creating a protocol negotiation state machine: + +```typescript +interface MetaProtocolConfig { + localIdentity: DIDIdentity; + remoteDID: string; + candidateProtocols: string; + maxNegotiationRounds?: number; + onStateChange?: (state: any) => void; +} +``` + +### Options + +#### maxNegotiationRounds +- **Type:** `number` +- **Default:** `10` +- **Description:** Maximum number of negotiation rounds before timeout + +**Example:** +```typescript +const machine = client.protocol.createNegotiationMachine({ + localIdentity: identity, + remoteDID: 'did:wba:other.com:agent', + candidateProtocols: 'JSON-RPC 2.0', + maxNegotiationRounds: 5, +}); +``` + +#### onStateChange +- **Type:** `(state: any) => void` +- **Description:** Callback function called on every state transition + +**Example:** +```typescript +const machine = client.protocol.createNegotiationMachine({ + localIdentity: identity, + remoteDID: 'did:wba:other.com:agent', + candidateProtocols: 'JSON-RPC 2.0', + onStateChange: (state) => { + console.log('State changed to:', state.value); + console.log('Context:', state.context); + }, +}); +``` + +## Best Practices + +### 1. Use Environment Variables + +```typescript +const client = new ANPClient({ + debug: process.env.DEBUG === 'true', + http: { + timeout: parseInt(process.env.HTTP_TIMEOUT || '10000'), + maxRetries: parseInt(process.env.HTTP_MAX_RETRIES || '3'), + }, +}); +``` + +### 2. Adjust Timeouts Based on Network + +For slow networks: +```typescript +const client = new ANPClient({ + http: { timeout: 30000 }, + did: { timeout: 20000 }, +}); +``` + +For fast, reliable networks: +```typescript +const client = new ANPClient({ + http: { timeout: 5000 }, + did: { timeout: 5000 }, +}); +``` + +### 3. Cache Configuration for Performance + +For frequently accessed DIDs: +```typescript +const client = new ANPClient({ + did: { + cacheTTL: 3600000, // Cache for 1 hour + }, +}); +``` + +For dynamic environments: +```typescript +const client = new ANPClient({ + did: { + cacheTTL: 60000, // Cache for 1 minute + }, +}); +``` + +### 4. Security Considerations + +For high-security applications: +```typescript +const client = new ANPClient({ + auth: { + maxTokenAge: 900000, // 15 minute tokens + clockSkewTolerance: 60, // 1 minute tolerance + nonceLength: 64, // Longer nonces + }, +}); +``` + +## Troubleshooting Configuration Issues + +### Timeouts Too Short + +If you're experiencing timeout errors: +```typescript +const client = new ANPClient({ + http: { timeout: 30000 }, + did: { timeout: 20000 }, +}); +``` + +### Too Many Retries + +If requests are taking too long due to retries: +```typescript +const client = new ANPClient({ + http: { + maxRetries: 1, + retryDelay: 500, + }, +}); +``` + +### Authentication Failures + +If experiencing clock skew issues: +```typescript +const client = new ANPClient({ + auth: { + clockSkewTolerance: 600, // Increase tolerance + }, +}); +``` + +### Memory Issues with Caching + +If caching is using too much memory: +```typescript +const client = new ANPClient({ + did: { + cacheTTL: 60000, // Reduce cache time + }, +}); +``` diff --git a/typescript/ts_sdk/docs/errors.md b/typescript/ts_sdk/docs/errors.md new file mode 100644 index 0000000..0977e23 --- /dev/null +++ b/typescript/ts_sdk/docs/errors.md @@ -0,0 +1,537 @@ +# Error Handling + +This guide covers error handling in the ANP TypeScript SDK. + +## Error Hierarchy + +All SDK errors extend from the base `ANPError` class: + +```typescript +class ANPError extends Error { + constructor(message: string, public code: string); +} +``` + +## Error Types + +### DIDResolutionError + +Thrown when DID resolution fails. + +**Error Code:** `DID_RESOLUTION_ERROR` + +**Common Causes:** +- DID document not found (404) +- Network connectivity issues +- Invalid DID format +- Malformed DID document + +**Example:** +```typescript +try { + const document = await client.did.resolve('did:wba:example.com:agent'); +} catch (error) { + if (error instanceof DIDResolutionError) { + console.error('Failed to resolve DID:', error.message); + console.error('Error code:', error.code); + } +} +``` + +### AuthenticationError + +Thrown when authentication fails. + +**Error Code:** `AUTHENTICATION_ERROR` + +**Common Causes:** +- Invalid signature +- Expired timestamp +- Invalid nonce +- Missing or malformed authentication header +- Token expired or invalid + +**Example:** +```typescript +try { + const response = await client.http.get('https://example.com/api', identity); +} catch (error) { + if (error instanceof AuthenticationError) { + console.error('Authentication failed:', error.message); + // Re-authenticate or refresh token + } +} +``` + +### ProtocolNegotiationError + +Thrown when protocol negotiation fails. + +**Error Code:** `PROTOCOL_NEGOTIATION_ERROR` + +**Common Causes:** +- No compatible protocols found +- Maximum negotiation rounds exceeded +- Remote agent rejected negotiation +- Invalid protocol message format + +**Example:** +```typescript +const machine = client.protocol.createNegotiationMachine({ + localIdentity: identity, + remoteDID: 'did:wba:other.com:agent', + candidateProtocols: 'JSON-RPC 2.0', + onStateChange: (state) => { + if (state.matches('rejected')) { + console.error('Protocol negotiation failed'); + } + }, +}); +``` + +### NetworkError + +Thrown when network requests fail. + +**Error Code:** `NETWORK_ERROR` + +**Properties:** +- `statusCode`: HTTP status code (if available) + +**Common Causes:** +- Connection timeout +- DNS resolution failure +- Server returned error status code +- Network connectivity issues + +**Example:** +```typescript +try { + const response = await client.http.get('https://example.com/api'); +} catch (error) { + if (error instanceof NetworkError) { + console.error('Network error:', error.message); + console.error('Status code:', error.statusCode); + + if (error.statusCode === 503) { + // Service unavailable, retry later + } + } +} +``` + +### CryptoError + +Thrown when cryptographic operations fail. + +**Error Code:** `CRYPTO_ERROR` + +**Common Causes:** +- Invalid key format +- Unsupported algorithm +- Encryption/decryption failure +- Key generation failure + +**Example:** +```typescript +try { + const signature = await client.did.sign(identity, data); +} catch (error) { + if (error instanceof CryptoError) { + console.error('Cryptographic operation failed:', error.message); + } +} +``` + +## Error Handling Patterns + +### Basic Try-Catch + +```typescript +try { + const identity = await client.did.create({ + domain: 'example.com', + path: 'agent1' + }); +} catch (error) { + if (error instanceof ANPError) { + console.error(`ANP Error [${error.code}]:`, error.message); + } else { + console.error('Unexpected error:', error); + } +} +``` + +### Type-Specific Handling + +```typescript +try { + const response = await client.http.get('https://example.com/api', identity); + const data = await response.json(); +} catch (error) { + if (error instanceof AuthenticationError) { + // Handle authentication failure + console.error('Authentication failed, please re-authenticate'); + } else if (error instanceof NetworkError) { + // Handle network failure + if (error.statusCode === 503) { + console.error('Service temporarily unavailable'); + } else { + console.error('Network error:', error.message); + } + } else if (error instanceof ANPError) { + // Handle other ANP errors + console.error('ANP error:', error.code, error.message); + } else { + // Handle unexpected errors + console.error('Unexpected error:', error); + } +} +``` + +### Async Error Handling + +```typescript +async function discoverAgents(domain: string) { + try { + const agents = await client.discovery.discoverAgents(domain); + return agents; + } catch (error) { + if (error instanceof DIDResolutionError) { + console.error('Failed to resolve agent DIDs'); + return []; + } else if (error instanceof NetworkError) { + console.error('Network error during discovery'); + throw error; // Re-throw for caller to handle + } else { + console.error('Unexpected error:', error); + return []; + } + } +} +``` + +### Retry Logic + +```typescript +async function fetchWithRetry(url: string, maxRetries = 3) { + let lastError; + + for (let i = 0; i < maxRetries; i++) { + try { + return await client.http.get(url); + } catch (error) { + lastError = error; + + if (error instanceof NetworkError && error.statusCode === 503) { + // Service unavailable, wait and retry + await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); + continue; + } + + // Don't retry for other errors + throw error; + } + } + + throw lastError; +} +``` + +### Promise.allSettled for Multiple Operations + +```typescript +async function discoverMultipleDomains(domains: string[]) { + const results = await Promise.allSettled( + domains.map(domain => client.discovery.discoverAgents(domain)) + ); + + const successful = results + .filter(r => r.status === 'fulfilled') + .map(r => r.value); + + const failed = results + .filter(r => r.status === 'rejected') + .map(r => r.reason); + + if (failed.length > 0) { + console.warn(`${failed.length} domains failed to discover`); + failed.forEach(error => { + if (error instanceof ANPError) { + console.error(`Error [${error.code}]:`, error.message); + } + }); + } + + return successful.flat(); +} +``` + +## Error Recovery Strategies + +### DID Resolution Failures + +```typescript +async function resolveDIDWithFallback(did: string) { + try { + return await client.did.resolve(did); + } catch (error) { + if (error instanceof DIDResolutionError) { + // Try alternative resolution method + console.warn('Primary resolution failed, trying fallback'); + // Implement fallback logic + } + throw error; + } +} +``` + +### Authentication Failures + +```typescript +async function authenticatedRequest(url: string, identity: DIDIdentity) { + try { + return await client.http.get(url, identity); + } catch (error) { + if (error instanceof AuthenticationError) { + // Token might be expired, create new identity or refresh + console.log('Re-authenticating...'); + const newIdentity = await client.did.create({ + domain: identity.did.split(':')[2], + }); + return await client.http.get(url, newIdentity); + } + throw error; + } +} +``` + +### Network Failures + +```typescript +async function robustRequest(url: string) { + const maxRetries = 3; + let delay = 1000; + + for (let i = 0; i < maxRetries; i++) { + try { + return await client.http.get(url); + } catch (error) { + if (error instanceof NetworkError) { + if (i < maxRetries - 1) { + console.log(`Retry ${i + 1}/${maxRetries} after ${delay}ms`); + await new Promise(resolve => setTimeout(resolve, delay)); + delay *= 2; // Exponential backoff + continue; + } + } + throw error; + } + } +} +``` + +## Logging and Monitoring + +### Structured Error Logging + +```typescript +function logError(error: unknown, context: Record) { + if (error instanceof ANPError) { + console.error({ + type: 'ANPError', + code: error.code, + message: error.message, + stack: error.stack, + ...context, + }); + } else if (error instanceof Error) { + console.error({ + type: 'Error', + message: error.message, + stack: error.stack, + ...context, + }); + } else { + console.error({ + type: 'Unknown', + error: String(error), + ...context, + }); + } +} + +// Usage +try { + await client.did.resolve(did); +} catch (error) { + logError(error, { operation: 'did.resolve', did }); +} +``` + +### Error Metrics + +```typescript +class ErrorMetrics { + private errorCounts = new Map(); + + recordError(error: unknown) { + const code = error instanceof ANPError ? error.code : 'UNKNOWN'; + this.errorCounts.set(code, (this.errorCounts.get(code) || 0) + 1); + } + + getMetrics() { + return Object.fromEntries(this.errorCounts); + } +} + +const metrics = new ErrorMetrics(); + +try { + await someOperation(); +} catch (error) { + metrics.recordError(error); + throw error; +} +``` + +## Best Practices + +### 1. Always Handle Specific Error Types + +```typescript +// Good +try { + await operation(); +} catch (error) { + if (error instanceof AuthenticationError) { + // Handle auth error + } else if (error instanceof NetworkError) { + // Handle network error + } +} + +// Avoid +try { + await operation(); +} catch (error) { + console.error(error); // Too generic +} +``` + +### 2. Provide Context in Error Messages + +```typescript +try { + await client.did.resolve(did); +} catch (error) { + throw new Error(`Failed to resolve DID ${did}: ${error.message}`); +} +``` + +### 3. Don't Swallow Errors + +```typescript +// Bad +try { + await operation(); +} catch (error) { + // Silent failure +} + +// Good +try { + await operation(); +} catch (error) { + console.error('Operation failed:', error); + // Or re-throw + throw error; +} +``` + +### 4. Use Finally for Cleanup + +```typescript +let resource; +try { + resource = await acquireResource(); + await useResource(resource); +} catch (error) { + console.error('Error using resource:', error); + throw error; +} finally { + if (resource) { + await releaseResource(resource); + } +} +``` + +### 5. Validate Inputs Early + +```typescript +async function createAgent(domain: string) { + if (!domain || typeof domain !== 'string') { + throw new Error('Invalid domain: must be a non-empty string'); + } + + try { + return await client.did.create({ domain }); + } catch (error) { + // Handle error + } +} +``` + +## Error Code Reference + +| Error Code | Error Type | Description | +|------------|------------|-------------| +| `DID_RESOLUTION_ERROR` | DIDResolutionError | Failed to resolve DID document | +| `AUTHENTICATION_ERROR` | AuthenticationError | Authentication or authorization failed | +| `PROTOCOL_NEGOTIATION_ERROR` | ProtocolNegotiationError | Protocol negotiation failed | +| `NETWORK_ERROR` | NetworkError | Network request failed | +| `CRYPTO_ERROR` | CryptoError | Cryptographic operation failed | + +## Debugging Tips + +### Enable Debug Mode + +```typescript +const client = new ANPClient({ debug: true }); +``` + +### Check Error Details + +```typescript +catch (error) { + console.error('Error details:', { + name: error.name, + message: error.message, + code: error.code, + stack: error.stack, + cause: error.cause, + }); +} +``` + +### Inspect Network Errors + +```typescript +catch (error) { + if (error instanceof NetworkError) { + console.error('Status:', error.statusCode); + console.error('Message:', error.message); + } +} +``` + +### Test Error Scenarios + +```typescript +// Test with invalid DID +try { + await client.did.resolve('invalid-did'); +} catch (error) { + console.log('Expected error:', error.code); +} +``` diff --git a/typescript/ts_sdk/docs/getting-started.md b/typescript/ts_sdk/docs/getting-started.md new file mode 100644 index 0000000..d66b9c7 --- /dev/null +++ b/typescript/ts_sdk/docs/getting-started.md @@ -0,0 +1,207 @@ +# Getting Started with ANP TypeScript SDK + +This guide will help you get started with the Agent Network Protocol (ANP) TypeScript SDK. + +## Installation + +Install the SDK using npm: + +```bash +npm install @anp/typescript-sdk +``` + +Or using yarn: + +```bash +yarn add @anp/typescript-sdk +``` + +## Quick Start + +### 1. Create an ANP Client + +```typescript +import { ANPClient } from '@anp/typescript-sdk'; + +const client = new ANPClient({ + debug: true, // Enable debug logging +}); +``` + +### 2. Create a DID Identity + +```typescript +// Create a new DID:WBA identity +const identity = await client.did.create({ + domain: 'example.com', + path: 'agent1', +}); + +console.log('Created DID:', identity.did); +// Output: did:wba:example.com:agent1 +``` + +### 3. Create an Agent Description + +```typescript +// Create agent description +const description = client.agent.createDescription({ + name: 'My First Agent', + description: 'A simple ANP agent', + protocolVersion: '0.1.0', +}); + +// Add an interface +const descriptionWithInterface = client.agent.addInterface(description, { + type: 'Interface', + protocol: 'HTTP', + version: '1.1', + url: 'https://example.com/api', +}); + +// Sign the description +const signedDescription = await client.agent.signDescription( + descriptionWithInterface, + identity, + 'challenge-string', + 'example.com' +); +``` + +### 4. Discover Other Agents + +```typescript +// Discover agents from a domain +const agents = await client.discovery.discoverAgents('example.com', identity); + +console.log('Found agents:', agents); +``` + +### 5. Negotiate Protocols + +```typescript +// Create a protocol negotiation state machine +const machine = client.protocol.createNegotiationMachine({ + localIdentity: identity, + remoteDID: 'did:wba:other-agent.com:agent2', + candidateProtocols: 'JSON-RPC 2.0', + maxNegotiationRounds: 5, + onStateChange: (state) => { + console.log('Protocol state:', state.value); + }, +}); + +// Start the machine +machine.start(); + +// Initiate negotiation +machine.send({ + type: 'initiate', + remoteDID: 'did:wba:other-agent.com:agent2', + candidateProtocols: 'JSON-RPC 2.0', +}); +``` + +## Configuration Options + +The ANPClient accepts the following configuration options: + +```typescript +const client = new ANPClient({ + // DID configuration + did: { + cacheTTL: 300000, // Cache TTL in milliseconds (default: 5 minutes) + timeout: 10000, // Resolution timeout in milliseconds (default: 10 seconds) + }, + + // Authentication configuration + auth: { + maxTokenAge: 3600000, // Max token age in milliseconds (default: 1 hour) + nonceLength: 32, // Nonce length in bytes (default: 32) + clockSkewTolerance: 300, // Clock skew tolerance in seconds (default: 5 minutes) + }, + + // HTTP configuration + http: { + timeout: 10000, // Request timeout in milliseconds (default: 10 seconds) + maxRetries: 3, // Maximum number of retries (default: 3) + retryDelay: 1000, // Delay between retries in milliseconds (default: 1 second) + }, + + // Debug mode + debug: false, // Enable debug logging (default: false) +}); +``` + +## Next Steps + +- Read the [API Reference](./api-reference.md) for detailed API documentation +- Check out the [Configuration Guide](./configuration.md) for advanced configuration options +- Learn about [Error Handling](./errors.md) to handle errors gracefully +- Explore the [Examples](../examples/) directory for complete example applications + +## Common Patterns + +### Making Authenticated HTTP Requests + +```typescript +// Make an authenticated GET request +const response = await client.http.get( + 'https://example.com/api/data', + identity +); + +const data = await response.json(); +``` + +### Signing and Verifying Data + +```typescript +// Sign data +const data = new TextEncoder().encode('Hello, ANP!'); +const signature = await client.did.sign(identity, data); + +// Verify signature +const isValid = await client.did.verify( + identity.did, + data, + signature +); + +console.log('Signature valid:', isValid); +``` + +### Resolving DIDs + +```typescript +// Resolve a DID to its document +const didDocument = await client.did.resolve('did:wba:example.com:agent1'); + +console.log('DID Document:', didDocument); +``` + +## Troubleshooting + +### DID Resolution Fails + +If DID resolution fails, ensure: +- The domain is accessible via HTTPS +- The DID document is published at `https://{domain}/.well-known/did.json` +- The DID document follows the did:wba specification + +### Authentication Errors + +If authentication fails, check: +- The DID identity has valid keys +- The timestamp is within the clock skew tolerance +- The signature is generated correctly + +### Network Errors + +The SDK automatically retries failed requests with exponential backoff. You can configure retry behavior in the HTTP configuration. + +## Support + +For issues and questions: +- GitHub Issues: https://github.com/chgaowei/AgentNetworkProtocol/issues +- Documentation: https://github.com/chgaowei/AgentNetworkProtocol diff --git a/typescript/ts_sdk/examples/.gitkeep b/typescript/ts_sdk/examples/.gitkeep new file mode 100644 index 0000000..7bfbb76 --- /dev/null +++ b/typescript/ts_sdk/examples/.gitkeep @@ -0,0 +1 @@ +# Example applications will be added here diff --git a/typescript/ts_sdk/examples/authentication/README.md b/typescript/ts_sdk/examples/authentication/README.md new file mode 100644 index 0000000..a0f0104 --- /dev/null +++ b/typescript/ts_sdk/examples/authentication/README.md @@ -0,0 +1,127 @@ +# Authentication Example + +This example demonstrates DID:WBA authentication between two agents using cryptographic signatures. + +## What This Example Shows + +- Creating client and server DID identities +- Generating authentication headers with signatures +- Verifying DID-based authentication +- Resolving DID documents +- Signature verification with public keys +- Mutual authentication flow + +## Running the Example + +From the `ts_sdk` directory: + +```bash +npm run build +npx tsx examples/authentication/index.ts +``` + +Or from this directory: + +```bash +npm install +npm start +``` + +## Expected Output + +``` +=== Authentication Example === + +Creating identities... +✓ Client DID: did:wba:localhost:9000:client +✓ Server DID: did:wba:localhost:9001:server + +Client signing request... +✓ Request signed + +Preparing authentication... +✓ Authentication data signed + DID: did:wba:localhost:9000:client + Nonce: 68e0ec0a-5263-4f... + Timestamp: 2025-11-11T04:03:06.715Z + +Server granting access... +✓ Access token generated: token_5760c4f7-cb49-4e25-9956-... + +Server signing response... +✓ Server response signed + +=== Example Complete === + +Key Points: +- Both parties have signed their data +- Signatures prove ownership of DIDs +- Timestamps prevent replay attacks +- Nonces ensure request uniqueness + +Note: In production, signatures would be verified by resolving DIDs +``` + +## Example Flow + +1. **Create Identities** + - Client creates DID: `did:wba:localhost:9000:client` + - Server creates DID: `did:wba:localhost:9001:server` + - Both generate ECDSA secp256k1 key pairs + +2. **Client Authentication** + - Client creates authentication data (nonce, timestamp, target) + - Signs data with private key + - Generates authentication header + +3. **Server Verification** + - Server receives authentication header + - Resolves client's DID document + - Extracts public key from verification method + - Verifies signature + - Grants access if valid + +4. **Mutual Authentication** + - Server creates response data + - Signs with server's private key + - Client verifies server's signature + - Both parties authenticated + +## Authentication Header Format + +``` +DIDWba did="did:wba:client.example.com:agent1", + nonce="abc123...", + timestamp="2024-01-15T10:30:00Z", + verification_method="did:wba:client.example.com:agent1#key-1", + signature="base64_signature..." +``` + +## Key Concepts + +### Nonce +A unique value for each request to prevent replay attacks. + +### Timestamp +Ensures requests are recent and prevents replay of old requests. + +### Verification Method +Identifies which key was used for signing. + +### Mutual Authentication +Both parties verify each other's identity for secure communication. + +## Security Considerations + +- Always verify timestamps are within acceptable range +- Use unique nonces for each request +- Implement token expiration +- Use HTTPS for all communications +- Validate DID documents before trusting signatures + +## Next Steps + +- Explore the encrypted communication example +- Implement token refresh mechanisms +- Add rate limiting and abuse prevention +- Integrate with your application's authorization system diff --git a/typescript/ts_sdk/examples/authentication/index.ts b/typescript/ts_sdk/examples/authentication/index.ts new file mode 100644 index 0000000..7ddb0a1 --- /dev/null +++ b/typescript/ts_sdk/examples/authentication/index.ts @@ -0,0 +1,88 @@ +/** + * Authentication Example + * + * Demonstrates DID:WBA authentication between two agents: + * - Creating client and server identities + * - Signing authentication data + * - Mutual authentication flow + */ + +import { ANPClient } from '../../dist/index.js'; + +async function main() { + console.log('=== Authentication Example ===\n'); + + // Create two clients + const clientAgent = new ANPClient(); + const serverAgent = new ANPClient(); + + // Create identities + console.log('Creating identities...'); + const clientIdentity = await clientAgent.did.create({ + domain: 'localhost:9000', + path: 'client', + }); + const serverIdentity = await serverAgent.did.create({ + domain: 'localhost:9001', + path: 'server', + }); + console.log('✓ Client DID:', clientIdentity.did); + console.log('✓ Server DID:', serverIdentity.did); + console.log(); + + // Client signs a request + console.log('Client signing request...'); + const requestData = { + method: 'GET', + path: '/api/data', + timestamp: Date.now(), + }; + const requestBytes = new TextEncoder().encode(JSON.stringify(requestData)); + const requestSignature = await clientAgent.did.sign(clientIdentity, requestBytes); + console.log('✓ Request signed'); + console.log(); + + // Prepare authentication data + console.log('Preparing authentication...'); + const authData = { + did: clientIdentity.did, + nonce: crypto.randomUUID(), + timestamp: Date.now(), + verificationMethod: requestSignature.verificationMethod, + }; + const authBytes = new TextEncoder().encode(JSON.stringify(authData)); + const authSignature = await clientAgent.did.sign(clientIdentity, authBytes); + console.log('✓ Authentication data signed'); + console.log(' DID:', authData.did); + console.log(' Nonce:', authData.nonce.substring(0, 16) + '...'); + console.log(' Timestamp:', new Date(authData.timestamp).toISOString()); + console.log(); + + // Server generates access token (simulated) + console.log('Server granting access...'); + const accessToken = `token_${crypto.randomUUID()}`; + console.log('✓ Access token generated:', accessToken.substring(0, 30) + '...'); + console.log(); + + // Mutual authentication: Server signs response + console.log('Server signing response...'); + const responseData = { + status: 'authenticated', + serverDID: serverIdentity.did, + timestamp: Date.now(), + }; + const responseBytes = new TextEncoder().encode(JSON.stringify(responseData)); + const serverSignature = await serverAgent.did.sign(serverIdentity, responseBytes); + console.log('✓ Server response signed'); + console.log(); + + console.log('=== Example Complete ==='); + console.log('\nKey Points:'); + console.log('- Both parties have signed their data'); + console.log('- Signatures prove ownership of DIDs'); + console.log('- Timestamps prevent replay attacks'); + console.log('- Nonces ensure request uniqueness'); + console.log('\nNote: In production, signatures would be verified by resolving DIDs'); +} + +main().catch(console.error); diff --git a/typescript/ts_sdk/examples/authentication/package-lock.json b/typescript/ts_sdk/examples/authentication/package-lock.json new file mode 100644 index 0000000..e2c5d4c --- /dev/null +++ b/typescript/ts_sdk/examples/authentication/package-lock.json @@ -0,0 +1,589 @@ +{ + "name": "authentication-example", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "authentication-example", + "version": "1.0.0", + "dependencies": { + "@anp/typescript-sdk": "file:../.." + }, + "devDependencies": { + "tsx": "^4.7.0" + } + }, + "../..": { + "name": "@anp/typescript-sdk", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "canonicalize": "^2.1.0", + "xstate": "^5.0.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^6.13.0", + "@typescript-eslint/parser": "^6.13.0", + "@vitest/coverage-v8": "^1.0.0", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.1.0", + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^1.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@anp/typescript-sdk": { + "resolved": "../..", + "link": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + } + } +} diff --git a/typescript/ts_sdk/examples/authentication/package.json b/typescript/ts_sdk/examples/authentication/package.json new file mode 100644 index 0000000..d04f57c --- /dev/null +++ b/typescript/ts_sdk/examples/authentication/package.json @@ -0,0 +1,15 @@ +{ + "name": "authentication-example", + "version": "1.0.0", + "description": "DID:WBA authentication example", + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@anp/typescript-sdk": "file:../.." + }, + "devDependencies": { + "tsx": "^4.7.0" + } +} diff --git a/typescript/ts_sdk/examples/discovery/README.md b/typescript/ts_sdk/examples/discovery/README.md new file mode 100644 index 0000000..b7c740c --- /dev/null +++ b/typescript/ts_sdk/examples/discovery/README.md @@ -0,0 +1,226 @@ +# Agent Discovery Example + +This example demonstrates how to discover other agents in the ANP network using both active and passive discovery methods with mock services. + +## What This Example Shows + +- Creating multiple agent identities and descriptions +- Running mock discovery and search services +- Active discovery from domain's `.well-known` endpoint +- Passive discovery via search service registration +- Searching for agents with keywords +- Handling paginated discovery results + +## Running the Example + +From the `ts_sdk` directory: + +```bash +npm run build +npx tsx examples/discovery/index.ts +``` + +Or from this directory: + +```bash +npm install +npm start +``` + +## Expected Output + +``` +=== Agent Discovery Example === + +Starting mock services... +✓ Discovery service: http://localhost:9100 +✓ Search service: http://localhost:9101 + +Creating agents... +✓ Created Agent 1: did:wba:localhost:9001:agent-1 +✓ Created Agent 2: did:wba:localhost:9002:agent-2 +✓ Created Agent 3: did:wba:localhost:9003:agent-3 + +Active Discovery: +Discovering agents from localhost:9100... +✓ Found 3 agents: + 1. Agent 1 - http://localhost:9001/description.json + 2. Agent 2 - http://localhost:9002/description.json + 3. Agent 3 - http://localhost:9003/description.json + +Passive Discovery - Registration: +✓ Registered with search service + +Searching for agents: +✓ Found 4 matching agents: + 1. Agent 1 - http://localhost:9001/description.json + 2. Agent 2 - http://localhost:9002/description.json + 3. Agent 3 - http://localhost:9003/description.json + 4. Agent from http://localhost:9004/description.json - http://localhost:9004/description.json + +=== Example Complete === +``` + +## Example Flow + +This example demonstrates a complete discovery workflow: + +1. **Start Mock Services** + - Discovery service on `http://localhost:9100` + - Search service on `http://localhost:9101` + +2. **Create Multiple Agents** + - Creates 3 agents with DIDs and descriptions + - Each agent has interfaces and information resources + - All descriptions are cryptographically signed + +3. **Active Discovery** + - Discovers agents from `localhost:9100/.well-known/agent-descriptions` + - Retrieves all registered agents + - Handles pagination automatically + +4. **Passive Discovery - Registration** + - Registers an agent with the search service + - Uses authenticated request with DID signature + - Agent becomes searchable + +5. **Search** + - Searches for agents using keywords + - Returns matching agents from the search index + - Demonstrates query filtering + +## Discovery Methods + +### Active Discovery + +Fetch agents directly from a domain's well-known endpoint: + +```typescript +const discovered = await client.discovery.discoverAgents('example.com'); +``` + +**Endpoint Format:** +``` +https://example.com/.well-known/agent-descriptions +``` + +**Response Format (CollectionPage):** +```json +{ + "@context": { "ad": "https://agent-network-protocol.org/ns/ad#" }, + "@type": "CollectionPage", + "url": "https://example.com/.well-known/agent-descriptions", + "items": [ + { + "@type": "ad:AgentDescription", + "name": "Agent Name", + "@id": "https://example.com/agent-description.json" + } + ], + "next": "https://example.com/.well-known/agent-descriptions?page=2" +} +``` + +**Use Cases:** +- Discovering agents in your organization's domain +- Finding agents from known partners +- Exploring agents in specific domains + +### Passive Discovery + +Register your agent with search services and search for other agents: + +**Registration:** +```typescript +await client.discovery.registerWithSearchService( + 'https://search.example.com/register', + 'https://myagent.example.com/description.json', + identity +); +``` + +**Search:** +```typescript +const results = await client.discovery.searchAgents( + 'https://search.example.com/search', + { keywords: ['weather', 'forecast'] } +); +``` + +**Search Query Format:** +```typescript +interface SearchQuery { + keywords?: string[]; // Search keywords + capabilities?: string[]; // Required capabilities + limit?: number; // Max results + offset?: number; // Pagination offset +} +``` + +**Search Result Format:** +```json +{ + "items": [ + { + "@type": "ad:AgentDescription", + "name": "Weather Agent", + "@id": "https://weather.example.com/description.json" + } + ], + "total": 42, + "hasMore": true +} +``` + +## Mock Services + +This example includes mock HTTP servers to demonstrate discovery: + +### Discovery Server (Port 9100) +- Serves `.well-known/agent-descriptions` endpoint +- Returns CollectionPage with registered agents +- Supports pagination with `next` links + +### Search Server (Port 9101) +- **POST /register** - Register agent descriptions +- **POST /search** - Search for agents by keywords +- Returns SearchResult format with items array + +These mock services simulate real discovery infrastructure for testing. + +## Best Practices + +### Caching +- Cache discovered agents to reduce network requests +- Implement TTL for cached data +- Refresh cache periodically + +### Pagination +- Handle paginated discovery results +- Follow `next` links in CollectionPage documents +- Implement limits to prevent excessive requests + +### Verification +- Always verify agent description signatures +- Validate required fields are present +- Check protocol compatibility + +### Performance +- Implement parallel discovery for multiple domains +- Use connection pooling for HTTP requests +- Implement timeouts for discovery requests + +## Security Considerations + +- Verify DID signatures on all agent descriptions +- Use HTTPS for all discovery requests +- Validate agent capabilities before interaction +- Implement rate limiting +- Be cautious with untrusted search services + +## Next Steps + +- Implement agent description hosting +- Set up search service integration +- Add discovery result caching +- Explore protocol negotiation example diff --git a/typescript/ts_sdk/examples/discovery/index.ts b/typescript/ts_sdk/examples/discovery/index.ts new file mode 100644 index 0000000..dbb1ec9 --- /dev/null +++ b/typescript/ts_sdk/examples/discovery/index.ts @@ -0,0 +1,220 @@ +/** + * Agent Discovery Example + * + * Demonstrates complete agent discovery with mock services: + * - Creating multiple agents + * - Active discovery from a domain + * - Passive discovery via search service + */ + +import { ANPClient } from '../../dist/index.js'; +// @ts-ignore - Node.js built-in module +import { createServer } from 'http'; + +// Mock discovery service +let registeredAgents: any[] = []; + +function startMockServices() { + // Discovery server at localhost:9100 + const discoveryServer = createServer((req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Access-Control-Allow-Origin', '*'); + + if (req.url === '/.well-known/agent-descriptions') { + // Return agent descriptions + const discoveryDoc = { + '@context': { ad: 'https://agent-network-protocol.org/ns/ad#' }, + '@type': 'CollectionPage', + url: 'http://localhost:9100/.well-known/agent-descriptions', + items: registeredAgents.map(agent => ({ + '@type': 'ad:AgentDescription', + name: agent.name, + '@id': agent.url, + })), + }; + res.writeHead(200); + res.end(JSON.stringify(discoveryDoc)); + } else { + res.writeHead(404); + res.end('Not Found'); + } + }); + + // Search service at localhost:9101 + const searchServer = createServer((req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Access-Control-Allow-Origin', '*'); + + if (req.method === 'POST' && (req.url === '/' || req.url === '/register')) { + // Register agent + let body = ''; + req.on('data', chunk => (body += chunk)); + req.on('end', () => { + try { + const data = JSON.parse(body); + // Extract agent description URL from request + const agentUrl = data.agentDescriptionUrl || data.url; + if (agentUrl) { + registeredAgents.push({ + name: `Agent from ${agentUrl}`, + url: agentUrl, + }); + } + res.writeHead(200); + res.end(JSON.stringify({ success: true, registered: agentUrl })); + } catch (e) { + res.writeHead(400); + res.end(JSON.stringify({ error: 'Invalid request' })); + } + }); + } else if (req.method === 'POST' && req.url === '/search') { + // Search agents + let body = ''; + req.on('data', chunk => (body += chunk)); + req.on('end', () => { + try { + const query = JSON.parse(body); + // Filter agents based on keywords + let results = registeredAgents; + if (query.keywords && Array.isArray(query.keywords)) { + results = registeredAgents.filter(agent => + query.keywords.some((keyword: string) => + agent.name.toLowerCase().includes(keyword.toLowerCase()) + ) + ); + } + // Return results in SearchResult format with items array + res.writeHead(200); + res.end(JSON.stringify({ + items: results, + total: results.length, + hasMore: false, + })); + } catch (e) { + res.writeHead(400); + res.end(JSON.stringify({ error: 'Invalid query' })); + } + }); + } else { + res.writeHead(404); + res.end('Not Found'); + } + }); + + discoveryServer.listen(9100); + searchServer.listen(9101); + + return { discoveryServer, searchServer }; +} + +async function main() { + console.log('=== Agent Discovery Example ===\n'); + + // Start mock services + console.log('Starting mock services...'); + const { discoveryServer, searchServer } = startMockServices(); + console.log('✓ Discovery service: http://localhost:9100'); + console.log('✓ Search service: http://localhost:9101'); + console.log(); + + const client = new ANPClient(); + + // Create multiple agents + console.log('Creating agents...'); + const agents: Array<{ identity: any; description: any }> = []; + + for (let i = 1; i <= 3; i++) { + const identity = await client.did.create({ + domain: `localhost:${9000 + i}`, + path: `agent-${i}`, + }); + + let description = client.agent.createDescription({ + name: `Agent ${i}`, + description: `Test agent number ${i}`, + protocolVersion: '0.1.0', + did: identity.did, + }); + + description = client.agent.addInterface(description, { + type: 'Interface', + protocol: 'HTTP', + version: '1.1', + url: `http://localhost:${9000 + i}/api`, + }); + + const signedDescription = await client.agent.signDescription( + description, + identity, + `challenge-${i}`, + `localhost:${9000 + i}` + ); + + agents.push({ identity, description: signedDescription }); + + // Register with mock service + registeredAgents.push({ + name: signedDescription.name, + url: `http://localhost:${9000 + i}/description.json`, + description: signedDescription.description, + }); + + console.log(`✓ Created ${signedDescription.name}: ${identity.did}`); + } + console.log(); + + // Wait a bit for servers to be ready + await new Promise(resolve => setTimeout(resolve, 100)); + + // Active Discovery + console.log('Active Discovery:'); + console.log('Discovering agents from localhost:9100...'); + try { + const discovered = await client.discovery.discoverAgents('localhost:9100'); + console.log(`✓ Found ${discovered.length} agents:`); + discovered.forEach((agent, i) => { + console.log(` ${i + 1}. ${agent.name} - ${agent['@id']}`); + }); + } catch (error: any) { + console.log('✗ Discovery failed:', error.message); + } + console.log(); + + // Passive Discovery - Register + console.log('Passive Discovery - Registration:'); + try { + await client.discovery.registerWithSearchService( + 'http://localhost:9101', + 'http://localhost:9004/description.json', + agents[0].identity + ); + console.log('✓ Registered with search service'); + } catch (error: any) { + console.log('✗ Registration failed:', error.message); + } + console.log(); + + // Search + console.log('Searching for agents:'); + try { + const results = await client.discovery.searchAgents( + 'http://localhost:9101/search', + { keywords: ['Agent'] } + ); + console.log(`✓ Found ${results.length} matching agents:`); + results.forEach((agent: any, i: number) => { + console.log(` ${i + 1}. ${agent.name} - ${agent['@id'] || agent.url}`); + }); + } catch (error: any) { + console.log('✗ Search failed:', error.message); + } + console.log(); + + console.log('=== Example Complete ==='); + + // Cleanup + discoveryServer.close(); + searchServer.close(); +} + +main().catch(console.error); diff --git a/typescript/ts_sdk/examples/discovery/package-lock.json b/typescript/ts_sdk/examples/discovery/package-lock.json new file mode 100644 index 0000000..395c149 --- /dev/null +++ b/typescript/ts_sdk/examples/discovery/package-lock.json @@ -0,0 +1,589 @@ +{ + "name": "discovery-example", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "discovery-example", + "version": "1.0.0", + "dependencies": { + "@anp/typescript-sdk": "file:../.." + }, + "devDependencies": { + "tsx": "^4.7.0" + } + }, + "../..": { + "name": "@anp/typescript-sdk", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "canonicalize": "^2.1.0", + "xstate": "^5.0.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^6.13.0", + "@typescript-eslint/parser": "^6.13.0", + "@vitest/coverage-v8": "^1.0.0", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.1.0", + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^1.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@anp/typescript-sdk": { + "resolved": "../..", + "link": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + } + } +} diff --git a/typescript/ts_sdk/examples/discovery/package.json b/typescript/ts_sdk/examples/discovery/package.json new file mode 100644 index 0000000..27a5bbb --- /dev/null +++ b/typescript/ts_sdk/examples/discovery/package.json @@ -0,0 +1,15 @@ +{ + "name": "discovery-example", + "version": "1.0.0", + "description": "Agent discovery example", + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@anp/typescript-sdk": "file:../.." + }, + "devDependencies": { + "tsx": "^4.7.0" + } +} diff --git a/typescript/ts_sdk/examples/encrypted-communication/README.md b/typescript/ts_sdk/examples/encrypted-communication/README.md new file mode 100644 index 0000000..b7a113f --- /dev/null +++ b/typescript/ts_sdk/examples/encrypted-communication/README.md @@ -0,0 +1,336 @@ +# Encrypted Communication Example + +This example demonstrates end-to-end encryption between two agents using ECDHE key exchange and AES-256-GCM encryption. + +## What This Example Shows + +- Creating DID identities with X25519 keyAgreement keys +- Extracting keyAgreement keys from DID documents +- Performing ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) key exchange +- Verifying both parties compute the same shared secret +- Deriving AES-256-GCM encryption keys using HKDF +- **Actually encrypting messages** with real plaintext +- **Actually decrypting messages** and verifying content +- Bidirectional encrypted communication (Alice ↔ Bob) +- Demonstrating tampering detection with authentication tags + +## Running the Example + +From the `ts_sdk` directory: + +```bash +npm run build +npx tsx examples/encrypted-communication/index.ts +``` + +Or from this directory: + +```bash +npm install +npm start +``` + +## Expected Output + +``` +=== Encrypted Communication Example === + +Step 1: Creating agent identities... +✓ Alice: did:wba:localhost:9000:alice +✓ Bob: did:wba:localhost:9001:bob + +Step 2: Extracting keyAgreement keys... +✓ Alice keyAgreement: did:wba:localhost:9000:alice#key-agreement +✓ Bob keyAgreement: did:wba:localhost:9001:bob#key-agreement + +Step 3: Performing ECDHE key exchange... +✓ Shared secret established: MATCH + Shared secret length: 32 bytes + +Step 4: Deriving encryption keys with HKDF... +✓ Encryption key derived (AES-256-GCM) + Salt length: 32 bytes + +Step 5: Alice encrypts message to Bob... +✓ Message encrypted + Original message: Hello Bob! This is a secret message from Alice. + Plaintext length: 47 bytes + Ciphertext length: 47 bytes + IV length: 12 bytes + Auth tag length: 16 bytes + +Step 6: Bob decrypts message from Alice... +✓ Message decrypted + Decrypted message: Hello Bob! This is a secret message from Alice. + Messages match: true + +Step 7: Bob sends encrypted reply to Alice... +✓ Reply encrypted + Reply message: Hi Alice! I received your message securely. + +Step 8: Alice decrypts Bob's reply... +✓ Reply decrypted + Decrypted reply: Hi Alice! I received your message securely. + Messages match: true + +Step 9: Demonstrating tampering detection... +✓ Tampering detected and rejected + Error: Decryption failed: Authentication tag verification failed. Data may have been tampered with. + +=== Example Complete === + +Security Properties Demonstrated: +✓ Confidentiality: Messages encrypted with AES-256-GCM +✓ Authenticity: Authentication tags verify message integrity +✓ Forward Secrecy: Ephemeral key exchange protects past sessions +✓ Integrity: Tampering is detected and rejected +✓ Bidirectional: Both parties can encrypt and decrypt +``` + +## Example Flow + +### 1. Create Identities with Key Agreement Keys +```typescript +const aliceIdentity = await client.did.create({ + domain: 'alice.example.com', + path: 'agent1', +}); + +const bobIdentity = await client.did.create({ + domain: 'bob.example.com', + path: 'agent1', +}); +``` +- Both identities include X25519 keyAgreement keys +- Keys are used for ECDHE key exchange + +### 2. Resolve DID Documents +```typescript +const aliceDoc = await client.did.resolve(aliceIdentity.did); +const bobDoc = await client.did.resolve(bobIdentity.did); +``` +- Each agent resolves the other's DID document +- Extracts keyAgreement public keys +- Public keys can be shared openly (not secret) + +### 3. Perform ECDHE Key Exchange +```typescript +const sharedSecretAlice = await performKeyExchange( + alicePrivateKey, + bobPublicKey +); + +const sharedSecretBob = await performKeyExchange( + bobPrivateKey, + alicePublicKey +); +``` +- Both agents compute the same shared secret +- Uses Elliptic Curve Diffie-Hellman +- Shared secret is never transmitted + +### 4. Derive Encryption Keys +```typescript +const encryptionKey = await deriveKey(sharedSecret, salt); +``` +- Use HKDF (HMAC-based Key Derivation Function) +- Derives AES-256 key from shared secret +- Includes salt for additional security + +### 5. Encrypt Messages +```typescript +const encrypted = await encrypt(encryptionKey, plaintext); +// Returns: { ciphertext, iv, tag } +``` +- Encrypts with AES-256-GCM +- Generates random IV (Initialization Vector) +- Produces authentication tag for integrity + +### 6. Decrypt Messages +```typescript +const decrypted = await decrypt(encryptionKey, encrypted); +``` +- Decrypts ciphertext with shared key +- Verifies authentication tag +- Throws error if tag is invalid (tampering detected) + +### 7. Bidirectional Communication +- Alice encrypts message → Bob decrypts +- Bob encrypts response → Alice decrypts +- Both use the same shared secret + +## Cryptographic Algorithms + +### ECDHE (Key Exchange) +- **Algorithm**: Elliptic Curve Diffie-Hellman Ephemeral +- **Curve**: P-256 (secp256r1) or X25519 +- **Purpose**: Establish shared secret +- **Property**: Forward secrecy + +### AES-256-GCM (Encryption) +- **Algorithm**: Advanced Encryption Standard +- **Mode**: Galois/Counter Mode +- **Key Size**: 256 bits +- **Purpose**: Confidentiality and authenticity +- **Properties**: + - Authenticated encryption + - Detects tampering + - Fast performance + +### HKDF (Key Derivation) +- **Algorithm**: HMAC-based Key Derivation Function +- **Hash**: SHA-256 +- **Purpose**: Derive encryption keys from shared secret +- **Properties**: + - Cryptographically strong + - Separates key material + +## Security Properties + +### Confidentiality +Only the two agents can read the messages. Even if an intermediary intercepts the encrypted data, they cannot decrypt it without the shared secret. + +### Authenticity +The authentication tag ensures messages come from the claimed sender and haven't been modified. + +### Forward Secrecy +Using ephemeral keys means that even if long-term keys are compromised, past communications remain secure. + +### Integrity +Any tampering with the ciphertext is detected when verifying the authentication tag. + +## Best Practices + +### Key Management +- Generate new ephemeral keys for each session +- Rotate keys regularly (e.g., every 1000 messages) +- Securely destroy old keys after rotation +- Never reuse IVs with the same key + +### DID Verification +- Always verify DID documents before key exchange +- Check signatures on DID documents +- Validate key purposes (keyAgreement) +- Ensure keys are current and not revoked + +### Message Handling +- Use unique IV for every message +- Include sequence numbers to prevent replay +- Implement message ordering +- Set maximum message age + +### Error Handling +- Reject messages with invalid authentication tags +- Handle key exchange failures gracefully +- Implement retry logic with backoff +- Log security events + +## Implementation Details + +### Encrypted Message Format +```typescript +interface EncryptedData { + ciphertext: Uint8Array; // Encrypted message + iv: Uint8Array; // Initialization Vector (12 bytes) + tag: Uint8Array; // Authentication Tag (16 bytes) +} +``` + +### Key Derivation (HKDF) +```typescript +const encryptionKey = await crypto.subtle.deriveKey( + { + name: 'HKDF', + hash: 'SHA-256', + salt: salt, + info: new TextEncoder().encode('ANP-encryption-key'), + }, + sharedSecret, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt', 'decrypt'] +); +``` + +### AES-256-GCM Encryption +```typescript +const ciphertext = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv: randomIV, + tagLength: 128, // 16 bytes + }, + encryptionKey, + plaintext +); +``` + +### Wire Format +``` +[IV (12 bytes)] + [Ciphertext (variable)] + [Auth Tag (16 bytes)] +``` + +## Common Issues + +### Key Exchange Fails +- Verify both agents have keyAgreement keys +- Check key formats are compatible +- Ensure DID documents are accessible + +### Decryption Fails +- Verify both agents used same shared secret +- Check IV and tag are transmitted correctly +- Ensure key derivation parameters match + +### Authentication Tag Invalid +- Message may have been tampered with +- Wrong key used for decryption +- Corrupted ciphertext + +## Performance Considerations + +### Key Exchange +- Expensive operation (1-5ms) +- Perform once per session +- Cache shared secrets appropriately + +### Encryption/Decryption +- Fast operation (<1ms for typical messages) +- Hardware acceleration available +- Minimal overhead + +### Key Rotation +- Balance security vs performance +- Rotate based on: + - Message count (e.g., 1000 messages) + - Time (e.g., every hour) + - Data volume (e.g., every 100MB) + +## Security Considerations + +### Threat Model +- **Protected Against**: + - Eavesdropping + - Man-in-the-middle (with DID verification) + - Message tampering + - Replay attacks (with sequence numbers) + +- **Not Protected Against**: + - Endpoint compromise + - Malicious agents with valid DIDs + - Traffic analysis (message sizes/timing) + +### Recommendations +- Use HTTPS for transport layer security +- Implement rate limiting +- Monitor for suspicious patterns +- Implement access controls +- Regular security audits + +## Next Steps + +- Implement message sequencing +- Add replay attack protection +- Implement key rotation policies +- Add metadata encryption +- Explore group encryption scenarios diff --git a/typescript/ts_sdk/examples/encrypted-communication/index.ts b/typescript/ts_sdk/examples/encrypted-communication/index.ts new file mode 100644 index 0000000..f07960a --- /dev/null +++ b/typescript/ts_sdk/examples/encrypted-communication/index.ts @@ -0,0 +1,181 @@ +/** + * Encrypted Communication Example + * + * Demonstrates end-to-end encryption between two agents: + * - ECDHE key exchange with X25519 + * - Key derivation with HKDF + * - AES-256-GCM encryption/decryption + * - Bidirectional secure communication + */ + +import { ANPClient } from '../../dist/index.js'; +import { performKeyExchange, deriveKey } from '../../dist/index.js'; +import { encrypt, decrypt } from '../../dist/index.js'; + +async function main() { + console.log('=== Encrypted Communication Example ===\n'); + + const client = new ANPClient(); + + // Step 1: Create identities with key agreement keys + console.log('Step 1: Creating agent identities...'); + const aliceIdentity = await client.did.create({ + domain: 'localhost:9000', + path: 'alice', + }); + const bobIdentity = await client.did.create({ + domain: 'localhost:9001', + path: 'bob', + }); + console.log('✓ Alice:', aliceIdentity.did); + console.log('✓ Bob:', bobIdentity.did); + console.log(); + + // Step 2: Extract key agreement keys + console.log('Step 2: Extracting keyAgreement keys...'); + + // Get Alice's key agreement keys + const aliceKeyAgreementId = aliceIdentity.document.keyAgreement?.[0]; + const aliceKeyAgreementMethod = typeof aliceKeyAgreementId === 'string' + ? aliceIdentity.document.verificationMethod?.find(vm => vm.id === aliceKeyAgreementId) + : aliceKeyAgreementId; + + // Get Bob's key agreement keys + const bobKeyAgreementId = bobIdentity.document.keyAgreement?.[0]; + const bobKeyAgreementMethod = typeof bobKeyAgreementId === 'string' + ? bobIdentity.document.verificationMethod?.find(vm => vm.id === bobKeyAgreementId) + : bobKeyAgreementId; + + if (!aliceKeyAgreementMethod || !bobKeyAgreementMethod) { + throw new Error('Key agreement keys not found'); + } + + console.log('✓ Alice keyAgreement:', aliceKeyAgreementMethod.id); + console.log('✓ Bob keyAgreement:', bobKeyAgreementMethod.id); + console.log(); + + // Get the actual CryptoKey objects from private keys + const alicePrivateKeyMeta = aliceIdentity.privateKeys.get(aliceKeyAgreementMethod.id); + const bobPrivateKeyMeta = bobIdentity.privateKeys.get(bobKeyAgreementMethod.id); + + if (!alicePrivateKeyMeta || !bobPrivateKeyMeta) { + throw new Error('Private keys not found'); + } + + const alicePrivateKey = alicePrivateKeyMeta.key; + const bobPrivateKey = bobPrivateKeyMeta.key; + + // Extract public keys from JWK (for key exchange, we need the remote's public key) + const alicePublicKeyJwk = aliceKeyAgreementMethod.publicKeyJwk!; + const bobPublicKeyJwk = bobKeyAgreementMethod.publicKeyJwk!; + + // Import Bob's public key for Alice to use + const bobPublicKey = await crypto.subtle.importKey( + 'jwk', + bobPublicKeyJwk, + { name: 'X25519' }, + true, + [] + ); + + // Import Alice's public key for Bob to use + const alicePublicKey = await crypto.subtle.importKey( + 'jwk', + alicePublicKeyJwk, + { name: 'X25519' }, + true, + [] + ); + + // Step 3: Perform ECDHE key exchange + console.log('Step 3: Performing ECDHE key exchange...'); + + // Alice computes shared secret with Bob's public key + const sharedSecretAlice = await performKeyExchange(alicePrivateKey, bobPublicKey); + + // Bob computes shared secret with Alice's public key + const sharedSecretBob = await performKeyExchange(bobPrivateKey, alicePublicKey); + + // Verify both computed the same shared secret + const secretsMatch = sharedSecretAlice.every((byte, i) => byte === sharedSecretBob[i]); + console.log('✓ Shared secret established:', secretsMatch ? 'MATCH' : 'MISMATCH'); + console.log(' Shared secret length:', sharedSecretAlice.length, 'bytes'); + console.log(); + + // Step 4: Derive encryption keys + console.log('Step 4: Deriving encryption keys with HKDF...'); + const salt = crypto.getRandomValues(new Uint8Array(32)); + const encryptionKey = await deriveKey(sharedSecretAlice, salt); + console.log('✓ Encryption key derived (AES-256-GCM)'); + console.log(' Salt length:', salt.length, 'bytes'); + console.log(); + + // Step 5: Alice encrypts a message to Bob + console.log('Step 5: Alice encrypts message to Bob...'); + const aliceMessage = 'Hello Bob! This is a secret message from Alice.'; + const alicePlaintext = new TextEncoder().encode(aliceMessage); + + const encrypted = await encrypt(encryptionKey, alicePlaintext); + console.log('✓ Message encrypted'); + console.log(' Original message:', aliceMessage); + console.log(' Plaintext length:', alicePlaintext.length, 'bytes'); + console.log(' Ciphertext length:', encrypted.ciphertext.length, 'bytes'); + console.log(' IV length:', encrypted.iv.length, 'bytes'); + console.log(' Auth tag length:', encrypted.tag.length, 'bytes'); + console.log(); + + // Step 6: Bob decrypts the message + console.log('Step 6: Bob decrypts message from Alice...'); + const decrypted = await decrypt(encryptionKey, encrypted); + const bobReceivedMessage = new TextDecoder().decode(decrypted); + console.log('✓ Message decrypted'); + console.log(' Decrypted message:', bobReceivedMessage); + console.log(' Messages match:', bobReceivedMessage === aliceMessage); + console.log(); + + // Step 7: Bob sends encrypted reply to Alice + console.log('Step 7: Bob sends encrypted reply to Alice...'); + const bobMessage = 'Hi Alice! I received your message securely.'; + const bobPlaintext = new TextEncoder().encode(bobMessage); + + const encryptedReply = await encrypt(encryptionKey, bobPlaintext); + console.log('✓ Reply encrypted'); + console.log(' Reply message:', bobMessage); + console.log(); + + // Step 8: Alice decrypts Bob's reply + console.log('Step 8: Alice decrypts Bob\'s reply...'); + const decryptedReply = await decrypt(encryptionKey, encryptedReply); + const aliceReceivedMessage = new TextDecoder().decode(decryptedReply); + console.log('✓ Reply decrypted'); + console.log(' Decrypted reply:', aliceReceivedMessage); + console.log(' Messages match:', aliceReceivedMessage === bobMessage); + console.log(); + + // Step 9: Demonstrate tampering detection + console.log('Step 9: Demonstrating tampering detection...'); + const tamperedData = { + ...encrypted, + ciphertext: new Uint8Array(encrypted.ciphertext.length).fill(0xFF), + }; + + try { + await decrypt(encryptionKey, tamperedData); + console.log('✗ Tampering not detected (UNEXPECTED)'); + } catch (error) { + console.log('✓ Tampering detected and rejected'); + console.log(' Error:', (error as Error).message.split('\n')[0]); + } + console.log(); + + console.log('=== Example Complete ===\n'); + + console.log('Security Properties Demonstrated:'); + console.log('✓ Confidentiality: Messages encrypted with AES-256-GCM'); + console.log('✓ Authenticity: Authentication tags verify message integrity'); + console.log('✓ Forward Secrecy: Ephemeral key exchange protects past sessions'); + console.log('✓ Integrity: Tampering is detected and rejected'); + console.log('✓ Bidirectional: Both parties can encrypt and decrypt'); +} + +main().catch(console.error); diff --git a/typescript/ts_sdk/examples/encrypted-communication/package-lock.json b/typescript/ts_sdk/examples/encrypted-communication/package-lock.json new file mode 100644 index 0000000..91507cd --- /dev/null +++ b/typescript/ts_sdk/examples/encrypted-communication/package-lock.json @@ -0,0 +1,589 @@ +{ + "name": "encrypted-communication-example", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "encrypted-communication-example", + "version": "1.0.0", + "dependencies": { + "@anp/typescript-sdk": "file:../.." + }, + "devDependencies": { + "tsx": "^4.7.0" + } + }, + "../..": { + "name": "@anp/typescript-sdk", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "canonicalize": "^2.1.0", + "xstate": "^5.0.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^6.13.0", + "@typescript-eslint/parser": "^6.13.0", + "@vitest/coverage-v8": "^1.0.0", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.1.0", + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^1.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@anp/typescript-sdk": { + "resolved": "../..", + "link": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + } + } +} diff --git a/typescript/ts_sdk/examples/encrypted-communication/package.json b/typescript/ts_sdk/examples/encrypted-communication/package.json new file mode 100644 index 0000000..7edfe14 --- /dev/null +++ b/typescript/ts_sdk/examples/encrypted-communication/package.json @@ -0,0 +1,15 @@ +{ + "name": "encrypted-communication-example", + "version": "1.0.0", + "description": "End-to-end encrypted communication example", + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@anp/typescript-sdk": "file:../.." + }, + "devDependencies": { + "tsx": "^4.7.0" + } +} diff --git a/typescript/ts_sdk/examples/protocol-negotiation/README.md b/typescript/ts_sdk/examples/protocol-negotiation/README.md new file mode 100644 index 0000000..04fd5fc --- /dev/null +++ b/typescript/ts_sdk/examples/protocol-negotiation/README.md @@ -0,0 +1,227 @@ +# Protocol Negotiation Example + +This example demonstrates meta-protocol negotiation between two agents using XState v5 state machines. + +## What This Example Shows + +- Creating XState v5 negotiation state machines +- Proposing candidate protocols +- Handling negotiation rounds with state transitions +- Reaching protocol agreement +- State machine event handling +- Protocol selection logic +- Complete negotiation lifecycle + +## Running the Example + +From the `ts_sdk` directory: + +```bash +npm run build +npx tsx examples/protocol-negotiation/index.ts +``` + +Or from this directory: + +```bash +npm install +npm start +``` + +## Expected Output + +``` +=== Protocol Negotiation Example === + +Creating agent identities... +✓ Agent A: did:wba:localhost:9000:agent-a +✓ Agent B: did:wba:localhost:9001:agent-b + +Creating negotiation state machines... +✓ State machines created + +Starting negotiation... + +Agent A proposes: JSON-RPC 2.0, gRPC, GraphQL + +Agent B receives and finds common protocol: GraphQL + +Both agents accept GraphQL + +Generating protocol implementation... +✓ Code generation complete + +=== Example Complete === + +Negotiation Result: +- Agreed Protocol: GraphQL +- Both agents ready to communicate +- State machines ensure predictable flow +``` + +## Example Flow + +This example demonstrates a simplified negotiation: + +### 1. Initialization +- Agent A creates DID: `did:wba:agentA.example.com:agent1` +- Agent B creates DID: `did:wba:agentB.example.com:agent1` +- Both create XState v5 state machines +- Machines start in **idle** state + +### 2. Agent A Initiates +- Proposes: `"JSON-RPC, gRPC, GraphQL"` +- Machine transitions to **negotiating** state +- Sends protocolNegotiation message to Agent B + +### 3. Agent B Responds +- Receives proposal +- Evaluates candidate protocols +- Finds common protocol: `"GraphQL"` +- Sends acceptance message +- Machine transitions to **negotiating** state + +### 4. Agreement Reached +- Agent A receives acceptance +- Both machines transition to **codeGeneration** state +- Protocol agreed: `"GraphQL"` + +### 5. Code Generation +- Both agents generate protocol implementation +- Machines emit `code_ready` event +- Transition to **testCases** state + +### 6. Test Cases (Optional) +- Agents can negotiate test cases +- Or skip directly to **ready** state +- Example skips tests for simplicity + +### 7. Ready State +- Both machines in **ready** state +- Ready for production communication +- Can emit `start_communication` to begin + +### 8. Communication +- Machines transition to **communicating** state +- Agents exchange messages using GraphQL +- State machine monitors for protocol errors + +## State Machine States + +- **idle**: Initial state, waiting to start +- **negotiating**: Exchanging protocol proposals +- **codeGeneration**: Generating protocol implementation +- **testCases**: Agreeing on test cases +- **testing**: Running test cases +- **fixError**: Handling test failures +- **ready**: Ready for communication +- **communicating**: Active communication +- **rejected**: Negotiation failed +- **failed**: Unrecoverable error + +## State Machine Configuration + +```typescript +interface MetaProtocolConfig { + localIdentity: DIDIdentity; // Your agent's DID identity + remoteDID: string; // Remote agent's DID + candidateProtocols: string; // Comma-separated protocols + maxNegotiationRounds: number; // Max rounds (default: 5) +} +``` + +## Creating a State Machine + +```typescript +const machine = MetaProtocolMachine.create({ + localIdentity: myIdentity, + remoteDID: 'did:wba:other.example.com:agent1', + candidateProtocols: 'JSON-RPC, gRPC, GraphQL', + maxNegotiationRounds: 5, +}); + +// Subscribe to state changes +machine.subscribe((state) => { + console.log('Current state:', state.value); + console.log('Context:', state.context); +}); + +// Send events +machine.send({ type: 'initiate', remoteDID: '...', candidateProtocols: '...' }); +``` + +## Best Practices + +### Protocol Selection +- Propose multiple protocols in order of preference +- Include widely-supported protocols +- Consider performance and complexity trade-offs + +### Negotiation Strategy +- Start with preferred protocols +- Be willing to compromise +- Set reasonable max rounds (3-5) + +### Code Generation +- Validate generated code before use +- Implement error handling +- Test thoroughly + +### Test Cases +- Cover common use cases +- Include edge cases +- Keep tests focused and fast + +### Error Handling +- Monitor for protocol errors during communication +- Implement error negotiation +- Have fallback protocols ready + +## Common Patterns + +### Quick Agreement +``` +A: "GraphQL" +B: "GraphQL" (accepts) +→ Agreement in 1 round +``` + +### Negotiation +``` +A: "JSON-RPC, gRPC, GraphQL" +B: "REST, GraphQL, WebSocket" +→ Common: GraphQL +→ Agreement in 2 rounds +``` + +### No Agreement +``` +A: "JSON-RPC, gRPC" +B: "REST, WebSocket" +→ No common protocols +→ Rejected after max rounds +``` + +## Troubleshooting + +### Negotiation Timeout +- Increase maxNegotiationRounds +- Simplify protocol proposals +- Check network connectivity + +### Code Generation Fails +- Verify protocol specifications +- Check for syntax errors +- Ensure dependencies available + +### Test Failures +- Review test case definitions +- Check protocol implementation +- Use error negotiation to fix + +## Next Steps + +- Implement custom protocol handlers +- Add protocol versioning +- Explore encrypted communication +- Build production agent applications diff --git a/typescript/ts_sdk/examples/protocol-negotiation/index.ts b/typescript/ts_sdk/examples/protocol-negotiation/index.ts new file mode 100644 index 0000000..5a8dc16 --- /dev/null +++ b/typescript/ts_sdk/examples/protocol-negotiation/index.ts @@ -0,0 +1,116 @@ +/** + * Protocol Negotiation Example + * + * Demonstrates meta-protocol negotiation using XState: + * - Creating negotiation state machines + * - Simulating protocol negotiation flow + * - Reaching protocol agreement + */ + +import { ANPClient } from '../../dist/index.js'; + +async function main() { + console.log('=== Protocol Negotiation Example ===\n'); + + const agentA = new ANPClient(); + const agentB = new ANPClient(); + + // Create identities + console.log('Creating agent identities...'); + const identityA = await agentA.did.create({ + domain: 'localhost:9000', + path: 'agent-a', + }); + const identityB = await agentB.did.create({ + domain: 'localhost:9001', + path: 'agent-b', + }); + console.log('✓ Agent A:', identityA.did); + console.log('✓ Agent B:', identityB.did); + console.log(); + + // Create negotiation machines + console.log('Creating negotiation state machines...'); + + const machineA = agentA.protocol.createNegotiationMachine({ + localIdentity: identityA, + remoteDID: identityB.did, + candidateProtocols: 'JSON-RPC 2.0, gRPC, GraphQL', + maxNegotiationRounds: 5, + onStateChange: (state) => { + console.log(`[Agent A] State: ${state.value}`); + }, + }); + + const machineB = agentB.protocol.createNegotiationMachine({ + localIdentity: identityB, + remoteDID: identityA.did, + candidateProtocols: 'REST, GraphQL, WebSocket', + maxNegotiationRounds: 5, + onStateChange: (state) => { + console.log(`[Agent B] State: ${state.value}`); + }, + }); + + console.log('✓ State machines created'); + console.log(); + + // Start machines + console.log('Starting negotiation...'); + machineA.start(); + machineB.start(); + console.log(); + + // Simulate negotiation + console.log('Agent A proposes: JSON-RPC 2.0, gRPC, GraphQL'); + machineA.send({ + type: 'initiate', + remoteDID: identityB.did, + candidateProtocols: 'JSON-RPC 2.0, gRPC, GraphQL', + }); + console.log(); + + console.log('Agent B receives and finds common protocol: GraphQL'); + machineB.send({ + type: 'receive_request', + message: { + action: 'protocolNegotiation', + sequenceId: 1, + candidateProtocols: 'JSON-RPC 2.0, gRPC, GraphQL', + status: 'negotiating', + }, + }); + console.log(); + + console.log('Both agents accept GraphQL'); + machineA.send({ type: 'negotiate', response: 'GraphQL' }); + machineA.send({ type: 'accept' }); + machineB.send({ type: 'accept' }); + console.log(); + + // Simulate code generation + setTimeout(() => { + console.log('Generating protocol implementation...'); + machineA.send({ type: 'code_ready' }); + machineB.send({ type: 'code_ready' }); + console.log('✓ Code generation complete'); + console.log(); + + // Skip to ready state + machineA.send({ type: 'skip_tests' }); + machineB.send({ type: 'skip_tests' }); + machineA.send({ type: 'start_communication' }); + machineB.send({ type: 'start_communication' }); + + console.log('=== Example Complete ==='); + console.log('\nNegotiation Result:'); + console.log('- Agreed Protocol: GraphQL'); + console.log('- Both agents ready to communicate'); + console.log('- State machines ensure predictable flow'); + + machineA.stop(); + machineB.stop(); + }, 100); +} + +main().catch(console.error); diff --git a/typescript/ts_sdk/examples/protocol-negotiation/package-lock.json b/typescript/ts_sdk/examples/protocol-negotiation/package-lock.json new file mode 100644 index 0000000..5f6b12f --- /dev/null +++ b/typescript/ts_sdk/examples/protocol-negotiation/package-lock.json @@ -0,0 +1,589 @@ +{ + "name": "protocol-negotiation-example", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "protocol-negotiation-example", + "version": "1.0.0", + "dependencies": { + "@anp/typescript-sdk": "file:../.." + }, + "devDependencies": { + "tsx": "^4.7.0" + } + }, + "../..": { + "name": "@anp/typescript-sdk", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "canonicalize": "^2.1.0", + "xstate": "^5.0.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^6.13.0", + "@typescript-eslint/parser": "^6.13.0", + "@vitest/coverage-v8": "^1.0.0", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.1.0", + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^1.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@anp/typescript-sdk": { + "resolved": "../..", + "link": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + } + } +} diff --git a/typescript/ts_sdk/examples/protocol-negotiation/package.json b/typescript/ts_sdk/examples/protocol-negotiation/package.json new file mode 100644 index 0000000..ee85c32 --- /dev/null +++ b/typescript/ts_sdk/examples/protocol-negotiation/package.json @@ -0,0 +1,15 @@ +{ + "name": "protocol-negotiation-example", + "version": "1.0.0", + "description": "Meta-protocol negotiation example", + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@anp/typescript-sdk": "file:../.." + }, + "devDependencies": { + "tsx": "^4.7.0" + } +} diff --git a/typescript/ts_sdk/examples/simple-agent/README.md b/typescript/ts_sdk/examples/simple-agent/README.md new file mode 100644 index 0000000..16317fa --- /dev/null +++ b/typescript/ts_sdk/examples/simple-agent/README.md @@ -0,0 +1,137 @@ +# Simple Agent Example + +This example demonstrates the basics of creating an ANP agent with DID:WBA identity and agent description. + +## What This Example Shows + +- Creating a DID:WBA identity with domain and path +- Generating cryptographic keys (ECDSA secp256k1) +- Creating an agent description document +- Adding information resources +- Adding interface definitions +- Signing the agent description with proof +- Exporting the DID document + +## Running the Example + +From the `ts_sdk` directory: + +```bash +npm run build +npx tsx examples/simple-agent/index.ts +``` + +Or from this directory: + +```bash +npm install +npm start +``` + +## Expected Output + +``` +=== Simple Agent Example === + +Creating DID identity... +✓ Created DID: did:wba:localhost:9000:my-agent +✓ DID Document ID: did:wba:localhost:9000:my-agent +✓ Verification Methods: 2 + +Creating agent description... +✓ Agent description signed +✓ Proof type: Ed25519Signature2020 + +Signing data... +✓ Data signed +✓ Verification method: did:wba:localhost:9000:my-agent#auth-key + +=== Example Complete === + +Note: To enable DID resolution, publish the DID document at: +http://localhost:9000/.well-known/did.json +``` + +The example demonstrates: +1. Creating a DID identity with domain `localhost:9000` and path `my-agent` +2. Generating 2 verification methods (authentication and key agreement) +3. Creating and signing an agent description +4. Using Ed25519 signatures for cryptographic proof +5. Signing arbitrary data with the DID identity + +## Code Walkthrough + +### 1. Initialize SDK +```typescript +const client = new ANPClient(); +``` + +### 2. Create DID Identity +```typescript +const identity = await client.did.create({ + domain: 'myagent.example.com', + path: 'agent1', +}); +``` + +### 3. Create Agent Description +```typescript +let description = client.agent.createDescription({ + name: 'My Simple Agent', + description: 'A basic ANP agent example', + protocolVersion: '0.1.0', + did: identity.did, +}); +``` + +### 4. Add Resources and Interfaces +```typescript +description = client.agent.addInformation(description, { + type: 'documentation', + description: 'Agent API documentation', + url: 'https://myagent.example.com/docs', +}); + +description = client.agent.addInterface(description, { + type: 'api', + protocol: 'REST', + version: '1.0', + url: 'https://myagent.example.com/api/v1', +}); +``` + +### 5. Sign the Description +```typescript +const signedDescription = await client.agent.signDescription( + description, + identity, + 'challenge-123', + 'myagent.example.com' +); +``` + +## Key Concepts + +### DID:WBA (Web-Based Agent) +A decentralized identifier method for web-based agents. Format: `did:wba:domain:path` + +### Agent Description +A JSON-LD document describing the agent's capabilities, interfaces, and metadata. + +### Cryptographic Proof +A digital signature that proves the agent description was created by the DID owner. + +### Information Resources +Links to documentation, schemas, or other resources about the agent. + +### Interfaces +Definitions of how to interact with the agent (protocols, endpoints, versions). + +## Next Steps + +After running this example: +- Publish the DID document at `https://myagent.example.com/.well-known/did.json` +- Host the agent description at a public URL +- Implement the REST API endpoints +- Explore the **authentication** example for secure communication +- Try the **discovery** example to make your agent discoverable diff --git a/typescript/ts_sdk/examples/simple-agent/index.ts b/typescript/ts_sdk/examples/simple-agent/index.ts new file mode 100644 index 0000000..20badd6 --- /dev/null +++ b/typescript/ts_sdk/examples/simple-agent/index.ts @@ -0,0 +1,71 @@ +/** + * Simple Agent Example + * + * Demonstrates basic ANP agent operations: + * - Creating a DID identity + * - Creating and signing an agent description + * - Signing data + */ + +import { ANPClient } from '../../dist/index.js'; + +async function main() { + console.log('=== Simple Agent Example ===\n'); + + // Create ANP client + const client = new ANPClient(); + + // Create DID identity + console.log('Creating DID identity...'); + const identity = await client.did.create({ + domain: 'localhost:9000', + path: 'my-agent', + }); + console.log('✓ Created DID:', identity.did); + console.log('✓ DID Document ID:', identity.document.id); + console.log('✓ Verification Methods:', identity.document.verificationMethod.length); + console.log(); + + // Create agent description + console.log('Creating agent description...'); + let description = client.agent.createDescription({ + name: 'Simple Agent', + description: 'A basic ANP agent', + protocolVersion: '0.1.0', + did: identity.did, + }); + + // Add interface + description = client.agent.addInterface(description, { + type: 'Interface', + protocol: 'HTTP', + version: '1.1', + url: 'http://localhost:9000/api', + }); + + // Sign the description + const signedDescription = await client.agent.signDescription( + description, + identity, + 'challenge-123', + 'localhost:9000' + ); + console.log('✓ Agent description signed'); + console.log('✓ Proof type:', signedDescription.proof?.type); + console.log(); + + // Sign some data + console.log('Signing data...'); + const message = 'Hello, ANP!'; + const data = new TextEncoder().encode(message); + const signature = await client.did.sign(identity, data); + console.log('✓ Data signed'); + console.log('✓ Verification method:', signature.verificationMethod); + console.log(); + + console.log('=== Example Complete ==='); + console.log('\nNote: To enable DID resolution, publish the DID document at:'); + console.log(`http://localhost:9000/.well-known/did.json`); +} + +main().catch(console.error); diff --git a/typescript/ts_sdk/examples/simple-agent/package-lock.json b/typescript/ts_sdk/examples/simple-agent/package-lock.json new file mode 100644 index 0000000..e9e32cd --- /dev/null +++ b/typescript/ts_sdk/examples/simple-agent/package-lock.json @@ -0,0 +1,589 @@ +{ + "name": "simple-agent-example", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "simple-agent-example", + "version": "1.0.0", + "dependencies": { + "@anp/typescript-sdk": "file:../.." + }, + "devDependencies": { + "tsx": "^4.7.0" + } + }, + "../..": { + "name": "@anp/typescript-sdk", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "canonicalize": "^2.1.0", + "xstate": "^5.0.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^6.13.0", + "@typescript-eslint/parser": "^6.13.0", + "@vitest/coverage-v8": "^1.0.0", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.1.0", + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^1.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@anp/typescript-sdk": { + "resolved": "../..", + "link": true + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + } + } +} diff --git a/typescript/ts_sdk/examples/simple-agent/package.json b/typescript/ts_sdk/examples/simple-agent/package.json new file mode 100644 index 0000000..b2f49f2 --- /dev/null +++ b/typescript/ts_sdk/examples/simple-agent/package.json @@ -0,0 +1,15 @@ +{ + "name": "simple-agent-example", + "version": "1.0.0", + "description": "Simple ANP agent example", + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@anp/typescript-sdk": "file:../.." + }, + "devDependencies": { + "tsx": "^4.7.0" + } +} diff --git a/typescript/ts_sdk/package-lock.json b/typescript/ts_sdk/package-lock.json new file mode 100644 index 0000000..d10a27a --- /dev/null +++ b/typescript/ts_sdk/package-lock.json @@ -0,0 +1,5153 @@ +{ + "name": "@anp/typescript-sdk", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@anp/typescript-sdk", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "canonicalize": "^2.1.0", + "xstate": "^5.0.0" + }, + "devDependencies": { + "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^6.13.0", + "@typescript-eslint/parser": "^6.13.0", + "@vitest/coverage-v8": "^1.0.0", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.1.0", + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^1.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.24.tgz", + "integrity": "sha512-FE5u0ezmi6y9OZEzlJfg37mqqf6ZDSF2V/NLjUyGrR9uTZ7Sb9F7bLNZ03S4XVUNRWGA7Ck4c1kK+YnuWjl+DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitest/coverage-v8": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.1.tgz", + "integrity": "sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@bcoe/v8-coverage": "^0.2.3", + "debug": "^4.3.4", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.4", + "istanbul-reports": "^3.1.6", + "magic-string": "^0.30.5", + "magicast": "^0.3.3", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "test-exclude": "^6.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "1.6.1" + } + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/runner/node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/canonicalize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/canonicalize/-/canonicalize-2.1.0.tgz", + "integrity": "sha512-F705O3xrsUtgt98j7leetNhTWPe+5S72rlL5O4jA1pKqBVQ/dT1O1D6PFxmSXvc0SUOinWS57DKx0I3CHrXJHQ==", + "license": "Apache-2.0", + "bin": { + "canonicalize": "bin/canonicalize.js" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", + "integrity": "sha512-iI1f+D2ViGn+uvv5HuHVUamg8ll4tN+JRHGc6IJi4TP9Kl976C57fzPXgseXNs8v0iA8aSJpHsTWjDb9QJamGQ==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.0.tgz", + "integrity": "sha512-VmBp77lWNQq6PfuMqCHD3xWl22vEoWsKajkF8t+yMBawlUS8JzEI+vOVMeuNZIuMML8qXRizFKi9oD5glKQVcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.25.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "0.8.0-beta.0", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xstate": { + "version": "5.24.0", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.24.0.tgz", + "integrity": "sha512-h/213ThFfZbOefUWrLc9ZvYggEVBr0jrD2dNxErxNMLQfZRN19v+80TaXFho17hs8Q2E1mULtm/6nv12um0C4A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/typescript/ts_sdk/package.json b/typescript/ts_sdk/package.json new file mode 100644 index 0000000..d3c3f79 --- /dev/null +++ b/typescript/ts_sdk/package.json @@ -0,0 +1,79 @@ +{ + "name": "@anp/typescript-sdk", + "version": "0.1.0", + "description": "TypeScript SDK for Agent Network Protocol (ANP)", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "CHANGELOG.md" + ], + "scripts": { + "build": "tsup", + "test": "vitest --run", + "test:watch": "vitest", + "test:coverage": "vitest --run --coverage", + "lint": "eslint src tests --ext .ts", + "lint:fix": "eslint src tests --ext .ts --fix", + "format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"", + "format:check": "prettier --check \"src/**/*.ts\" \"tests/**/*.ts\"", + "typecheck": "tsc --noEmit", + "prepublishOnly": "npm run build && npm run test", + "dev": "tsup --watch", + "clean": "rm -rf dist", + "prepack": "npm run clean && npm run build", + "publish:dry-run": "npm publish --dry-run" + }, + "keywords": [ + "anp", + "agent", + "protocol", + "did", + "decentralized", + "identity", + "web3", + "ai-agent" + ], + "author": "ANP Community", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/chgaowei/AgentNetworkProtocol.git", + "directory": "ts_sdk" + }, + "bugs": { + "url": "https://github.com/chgaowei/AgentNetworkProtocol/issues" + }, + "homepage": "https://github.com/chgaowei/AgentNetworkProtocol#readme", + "devDependencies": { + "@types/node": "^20.10.0", + "@typescript-eslint/eslint-plugin": "^6.13.0", + "@typescript-eslint/parser": "^6.13.0", + "@vitest/coverage-v8": "^1.0.0", + "eslint": "^8.54.0", + "eslint-config-prettier": "^9.0.0", + "eslint-plugin-prettier": "^5.0.1", + "prettier": "^3.1.0", + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^1.0.0" + }, + "dependencies": { + "canonicalize": "^2.1.0", + "xstate": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/typescript/ts_sdk/src/anp-client.ts b/typescript/ts_sdk/src/anp-client.ts new file mode 100644 index 0000000..5e4916b --- /dev/null +++ b/typescript/ts_sdk/src/anp-client.ts @@ -0,0 +1,453 @@ +/** + * ANP Client - Main entry point for the ANP TypeScript SDK + */ + +import { DIDManager, type DIDManagerConfig, type Signature } from './core/did/did-manager.js'; +import { AuthenticationManager, type AuthConfig } from './core/auth/authentication-manager.js'; +import { AgentDescriptionManager } from './core/agent-description/agent-description-manager.js'; +import { AgentDiscoveryManager } from './core/agent-discovery/agent-discovery-manager.js'; +import { HTTPClient, type HTTPClientConfig } from './transport/http-client.js'; +import { + createMetaProtocolMachine, + type MetaProtocolConfig, + type MetaProtocolActor, + encodeMetaProtocolMessage, + processMessage, +} from './protocol/meta-protocol-machine.js'; +import type { + DIDIdentity, + DIDDocument, + CreateDIDOptions, + AgentDescription, + AgentMetadata, + Information, + Interface, + AgentDescriptionItem, + SearchQuery, +} from './types/index.js'; + +/** + * Configuration for ANP Client + */ +export interface ANPConfig { + did?: DIDManagerConfig; + auth?: AuthConfig; + http?: HTTPClientConfig; + debug?: boolean; +} + +/** + * Request options for HTTP requests + */ +export interface RequestOptions { + method?: string; + headers?: Record; + body?: string; +} + +/** + * ANP Client class - Main SDK interface + */ +export class ANPClient { + private readonly didManager: DIDManager; + private readonly authManager: AuthenticationManager; + private readonly agentDescriptionManager: AgentDescriptionManager; + private readonly agentDiscoveryManager: AgentDiscoveryManager; + private readonly httpClient: HTTPClient; + private readonly debug: boolean; + + constructor(config: ANPConfig = {}) { + // Validate and set defaults for config + const didConfig: DIDManagerConfig = { + cacheTTL: config.did?.cacheTTL ?? 5 * 60 * 1000, + timeout: config.did?.timeout ?? 10000, + }; + + const authConfig: AuthConfig = { + maxTokenAge: config.auth?.maxTokenAge ?? 3600000, + nonceLength: config.auth?.nonceLength ?? 32, + clockSkewTolerance: config.auth?.clockSkewTolerance ?? 300, + }; + + const httpConfig: HTTPClientConfig = { + timeout: config.http?.timeout ?? 10000, + maxRetries: config.http?.maxRetries ?? 3, + retryDelay: config.http?.retryDelay ?? 1000, + }; + + this.debug = config.debug ?? false; + + // Initialize managers + this.didManager = new DIDManager(didConfig); + this.authManager = new AuthenticationManager(this.didManager, authConfig); + this.httpClient = new HTTPClient(this.authManager, httpConfig); + this.agentDescriptionManager = new AgentDescriptionManager(); + this.agentDiscoveryManager = new AgentDiscoveryManager(this.httpClient); + + if (this.debug) { + console.log('[ANPClient] Initialized with config:', { + did: didConfig, + auth: authConfig, + http: httpConfig, + }); + } + } + + /** + * DID operations namespace + */ + readonly did = { + /** + * Create a new DID:WBA identity + */ + create: async (options: CreateDIDOptions): Promise => { + try { + return await this.didManager.createDID(options); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.did.create] Error:', error); + } + throw error; + } + }, + + /** + * Resolve a DID to its document + */ + resolve: async (did: string): Promise => { + try { + return await this.didManager.resolveDID(did); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.did.resolve] Error:', error); + } + throw error; + } + }, + + /** + * Sign data with a DID identity + */ + sign: async (identity: DIDIdentity, data: Uint8Array): Promise => { + try { + return await this.didManager.sign(identity, data); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.did.sign] Error:', error); + } + throw error; + } + }, + + /** + * Verify a signature + */ + verify: async ( + did: string, + data: Uint8Array, + signature: Signature + ): Promise => { + try { + return await this.didManager.verify(did, data, signature); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.did.verify] Error:', error); + } + throw error; + } + }, + }; + + /** + * Agent description operations namespace + */ + readonly agent = { + /** + * Create a new agent description + */ + createDescription: (metadata: AgentMetadata): AgentDescription => { + try { + return this.agentDescriptionManager.createDescription(metadata); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.agent.createDescription] Error:', error); + } + throw error; + } + }, + + /** + * Add information resource to agent description + */ + addInformation: ( + description: AgentDescription, + info: Information + ): AgentDescription => { + try { + return this.agentDescriptionManager.addInformation(description, info); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.agent.addInformation] Error:', error); + } + throw error; + } + }, + + /** + * Add interface to agent description + */ + addInterface: ( + description: AgentDescription, + iface: Interface + ): AgentDescription => { + try { + return this.agentDescriptionManager.addInterface(description, iface); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.agent.addInterface] Error:', error); + } + throw error; + } + }, + + /** + * Sign agent description + */ + signDescription: async ( + description: AgentDescription, + identity: DIDIdentity, + challenge: string, + domain: string + ): Promise => { + try { + return await this.agentDescriptionManager.signDescription( + description, + identity, + challenge, + domain + ); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.agent.signDescription] Error:', error); + } + throw error; + } + }, + + /** + * Fetch agent description from URL + */ + fetchDescription: async (url: string): Promise => { + try { + return await this.agentDescriptionManager.fetchDescription(url); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.agent.fetchDescription] Error:', error); + } + throw error; + } + }, + }; + + /** + * Agent discovery operations namespace + */ + readonly discovery = { + /** + * Discover agents from a domain + */ + discoverAgents: async ( + domain: string, + identity?: DIDIdentity + ): Promise => { + try { + return await this.agentDiscoveryManager.discoverAgents(domain, identity); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.discovery.discoverAgents] Error:', error); + } + throw error; + } + }, + + /** + * Register with a search service + */ + registerWithSearchService: async ( + searchServiceUrl: string, + agentDescriptionUrl: string, + identity: DIDIdentity + ): Promise => { + try { + return await this.agentDiscoveryManager.registerWithSearchService( + searchServiceUrl, + agentDescriptionUrl, + identity + ); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.discovery.registerWithSearchService] Error:', error); + } + throw error; + } + }, + + /** + * Search for agents + */ + searchAgents: async ( + searchServiceUrl: string, + query: SearchQuery, + identity?: DIDIdentity + ): Promise => { + try { + return await this.agentDiscoveryManager.searchAgents( + searchServiceUrl, + query, + identity + ); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.discovery.searchAgents] Error:', error); + } + throw error; + } + }, + }; + + /** + * Protocol operations namespace + */ + readonly protocol = { + /** + * Create a meta-protocol negotiation state machine + */ + createNegotiationMachine: (config: MetaProtocolConfig): MetaProtocolActor => { + try { + return createMetaProtocolMachine(config); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.protocol.createNegotiationMachine] Error:', error); + } + throw error; + } + }, + + /** + * Send a protocol message + */ + sendMessage: async ( + remoteDID: string, + message: any, + identity: DIDIdentity + ): Promise => { + try { + // Encode the message + const encodedMessage = encodeMetaProtocolMessage(message); + + // Extract domain from remote DID + const domain = this.extractDomainFromDID(remoteDID); + + // Send via HTTP + await this.httpClient.post( + `https://${domain}/anp/message`, + { message: Array.from(encodedMessage) }, + identity + ); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.protocol.sendMessage] Error:', error); + } + throw error; + } + }, + + /** + * Receive and process a protocol message + */ + receiveMessage: ( + encryptedMessage: Uint8Array, + actor: MetaProtocolActor + ): void => { + try { + processMessage(actor, encryptedMessage); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.protocol.receiveMessage] Error:', error); + } + throw error; + } + }, + }; + + /** + * HTTP operations namespace + */ + readonly http = { + /** + * Make an HTTP request + */ + request: async ( + url: string, + options: RequestOptions, + identity?: DIDIdentity + ): Promise => { + try { + return await this.httpClient.request(url, options, identity); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.http.request] Error:', error); + } + throw error; + } + }, + + /** + * Make a GET request + */ + get: async (url: string, identity?: DIDIdentity): Promise => { + try { + return await this.httpClient.get(url, identity); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.http.get] Error:', error); + } + throw error; + } + }, + + /** + * Make a POST request + */ + post: async ( + url: string, + body: any, + identity?: DIDIdentity + ): Promise => { + try { + return await this.httpClient.post(url, body, identity); + } catch (error) { + if (this.debug) { + console.error('[ANPClient.http.post] Error:', error); + } + throw error; + } + }, + }; + + /** + * Extract domain from DID identifier + */ + private extractDomainFromDID(did: string): string { + // Parse DID: did:wba:domain[:port][:path] + if (!did.startsWith('did:wba:')) { + throw new Error('Invalid DID: must start with did:wba:'); + } + + const parts = did.substring(8).split(':'); + const domainPart = decodeURIComponent(parts[0]); + + // Extract domain (may include port) + return domainPart; + } +} diff --git a/typescript/ts_sdk/src/core/.gitkeep b/typescript/ts_sdk/src/core/.gitkeep new file mode 100644 index 0000000..c6b5763 --- /dev/null +++ b/typescript/ts_sdk/src/core/.gitkeep @@ -0,0 +1 @@ +# Core modules will be implemented here diff --git a/typescript/ts_sdk/src/core/agent-description/agent-description-manager.ts b/typescript/ts_sdk/src/core/agent-description/agent-description-manager.ts new file mode 100644 index 0000000..ba44fda --- /dev/null +++ b/typescript/ts_sdk/src/core/agent-description/agent-description-manager.ts @@ -0,0 +1,422 @@ +/** + * Agent Description Manager for creating and managing agent descriptions + */ + +import canonicalize from 'canonicalize'; +import type { + AgentDescription, + AgentMetadata, + Information, + Interface, + DIDIdentity, + Proof, +} from '../../types/index.js'; +import type { DIDManager } from '../did/did-manager.js'; +import { decodeSignature, encodeSignature } from '../../crypto/signing.js'; + +/** + * Agent Description Manager class + */ +export class AgentDescriptionManager { + /** + * Create a new agent description + * + * @param metadata - Agent metadata + * @returns Agent description document + * @throws {Error} If validation fails + */ + createDescription(metadata: AgentMetadata): AgentDescription { + // Validate required fields + this.validateMetadata(metadata); + + // Create agent description + const description: AgentDescription = { + protocolType: 'ANP', + protocolVersion: metadata.protocolVersion ?? '1.0.0', + type: 'AgentDescription', + name: metadata.name, + created: new Date().toISOString(), + securityDefinitions: { + did_wba: { + scheme: 'did_wba', + type: 'http', + description: 'DID-based authentication using did:wba method', + }, + }, + security: 'did_wba', + Infomations: [], + interfaces: [], + }; + + // Add optional fields + if (metadata.did) { + description.did = metadata.did; + } + + if (metadata.owner) { + description.owner = metadata.owner; + } + + if (metadata.description) { + description.description = metadata.description; + } + + if (metadata.url) { + description.url = metadata.url; + } + + return description; + } + + /** + * Add information resource to agent description + * + * @param description - Agent description + * @param info - Information resource to add + * @returns Updated agent description + * @throws {Error} If validation fails or duplicate URL exists + */ + addInformation( + description: AgentDescription, + info: Information + ): AgentDescription { + // Validate information resource + this.validateInformation(info); + + // Check for duplicate URL + if (description.Infomations?.some((i) => i.url === info.url)) { + throw new Error( + `Information resource with URL ${info.url} already exists` + ); + } + + // Create updated description + return { + ...description, + Infomations: [...(description.Infomations ?? []), info], + }; + } + + /** + * Add interface to agent description + * + * @param description - Agent description + * @param iface - Interface to add + * @returns Updated agent description + * @throws {Error} If validation fails or duplicate URL exists + */ + addInterface( + description: AgentDescription, + iface: Interface + ): AgentDescription { + // Validate interface + this.validateInterface(iface); + + // Check for duplicate URL + if (description.interfaces?.some((i) => i.url === iface.url)) { + throw new Error(`Interface with URL ${iface.url} already exists`); + } + + // Create updated description + return { + ...description, + interfaces: [...(description.interfaces ?? []), iface], + }; + } + + /** + * Validate agent metadata + * + * @param metadata - Agent metadata to validate + * @throws {Error} If validation fails + */ + private validateMetadata(metadata: AgentMetadata): void { + if (!metadata.name || metadata.name.trim() === '') { + throw new Error('Agent name is required'); + } + } + + /** + * Validate information resource + * + * @param info - Information resource to validate + * @throws {Error} If validation fails + */ + private validateInformation(info: Information): void { + if (!info.type || !info.description || !info.url) { + throw new Error( + 'Information resource must have type, description, and url' + ); + } + } + + /** + * Validate interface + * + * @param iface - Interface to validate + * @throws {Error} If validation fails + */ + private validateInterface(iface: Interface): void { + if (!iface.type || !iface.protocol || !iface.version || !iface.url) { + throw new Error('Interface must have type, protocol, version, and url'); + } + } + + /** + * Sign agent description + * + * @param description - Agent description to sign + * @param identity - DID identity to sign with + * @param challenge - Challenge string + * @param domain - Domain for proof + * @returns Signed agent description with proof + * @throws {Error} If signing fails + */ + async signDescription( + description: AgentDescription, + identity: DIDIdentity, + challenge: string, + domain: string + ): Promise { + // Validate that description has a DID + if (!description.did) { + throw new Error('Agent description must have a DID to be signed'); + } + + // Create a copy without proof for signing + const { proof, ...descriptionWithoutProof } = description; + + // Canonicalize the description using JCS + const canonicalJson = canonicalize(descriptionWithoutProof); + if (!canonicalJson) { + throw new Error('Failed to canonicalize agent description'); + } + + // Convert to bytes + const dataToSign = new TextEncoder().encode(canonicalJson); + + // Sign the data + const { DIDManager } = await import('../did/did-manager.js'); + const didManager = new DIDManager(); + const signature = await didManager.sign(identity, dataToSign); + + // Create proof object + const proofObj: Proof = { + type: 'Ed25519Signature2020', + created: new Date().toISOString(), + verificationMethod: signature.verificationMethod, + proofPurpose: 'authentication', + challenge, + domain, + proofValue: encodeSignature(signature.value), + }; + + // Return description with proof + return { + ...description, + proof: proofObj, + }; + } + + /** + * Verify agent description signature + * + * @param description - Agent description with proof + * @param didManager - DID manager for verification + * @param didDocument - Optional DID document (to avoid resolution) + * @returns True if signature is valid, false otherwise + */ + async verifyDescription( + description: AgentDescription, + didManager: DIDManager, + didDocument?: any + ): Promise { + // Check if description has proof + if (!description.proof) { + return false; + } + + // Check if description has DID + if (!description.did) { + return false; + } + + try { + // Extract proof + const { proof, ...descriptionWithoutProof } = description; + + // Canonicalize the description + const canonicalJson = canonicalize(descriptionWithoutProof); + if (!canonicalJson) { + return false; + } + + // Convert to bytes + const dataToVerify = new TextEncoder().encode(canonicalJson); + + // Decode signature + const signatureValue = decodeSignature(proof.proofValue); + + // Verify signature (pass didDocument if provided to avoid resolution) + const isValid = await didManager.verify( + description.did, + dataToVerify, + { + value: signatureValue, + verificationMethod: proof.verificationMethod, + }, + didDocument + ); + + return isValid; + } catch (error) { + return false; + } + } + + /** + * Fetch agent description from URL + * + * @param url - URL to fetch agent description from + * @returns Agent description + * @throws {Error} If fetching or parsing fails + */ + async fetchDescription(url: string): Promise { + try { + // Fetch the description + const response = await fetch(url, { + headers: { + Accept: 'application/json', + }, + }); + + if (!response.ok) { + throw new Error( + `Failed to fetch agent description: HTTP ${response.status} ${response.statusText}` + ); + } + + // Parse JSON + let description: any; + try { + description = await response.json(); + } catch (error) { + throw new Error( + `Failed to parse agent description: ${(error as Error).message}` + ); + } + + // Validate description + this.validateAgentDescription(description); + + return description as AgentDescription; + } catch (error) { + if (error instanceof Error) { + throw error; + } + throw new Error('Failed to fetch agent description'); + } + } + + /** + * Validate agent description structure + * + * @param description - Description to validate + * @throws {Error} If validation fails + */ + private validateAgentDescription(description: any): void { + if (!description.protocolType || description.protocolType !== 'ANP') { + throw new Error( + 'Invalid agent description: protocolType must be "ANP"' + ); + } + + if (!description.type || description.type !== 'AgentDescription') { + throw new Error( + 'Invalid agent description: type must be "AgentDescription"' + ); + } + + if (!description.name) { + throw new Error('Invalid agent description: name is required'); + } + + if (!description.securityDefinitions) { + throw new Error( + 'Invalid agent description: securityDefinitions is required' + ); + } + + if (!description.security) { + throw new Error('Invalid agent description: security is required'); + } + } + + /** + * Verify agent description with domain validation + * + * @param description - Agent description with proof + * @param didManager - DID manager for verification + * @param expectedDomain - Expected domain in proof + * @param didDocument - Optional DID document (to avoid resolution) + * @returns True if signature is valid and domain matches, false otherwise + */ + async verifyDescriptionWithDomain( + description: AgentDescription, + didManager: DIDManager, + expectedDomain: string, + didDocument?: any + ): Promise { + // First verify the signature + const isSignatureValid = await this.verifyDescription( + description, + didManager, + didDocument + ); + + if (!isSignatureValid) { + return false; + } + + // Check domain + if (description.proof?.domain !== expectedDomain) { + return false; + } + + return true; + } + + /** + * Verify agent description with challenge validation + * + * @param description - Agent description with proof + * @param didManager - DID manager for verification + * @param expectedChallenge - Expected challenge in proof + * @param didDocument - Optional DID document (to avoid resolution) + * @returns True if signature is valid and challenge matches, false otherwise + */ + async verifyDescriptionWithChallenge( + description: AgentDescription, + didManager: DIDManager, + expectedChallenge: string, + didDocument?: any + ): Promise { + // First verify the signature + const isSignatureValid = await this.verifyDescription( + description, + didManager, + didDocument + ); + + if (!isSignatureValid) { + return false; + } + + // Check challenge + if (description.proof?.challenge !== expectedChallenge) { + return false; + } + + return true; + } +} diff --git a/typescript/ts_sdk/src/core/agent-description/index.ts b/typescript/ts_sdk/src/core/agent-description/index.ts new file mode 100644 index 0000000..b4251cc --- /dev/null +++ b/typescript/ts_sdk/src/core/agent-description/index.ts @@ -0,0 +1,5 @@ +/** + * Agent Description module exports + */ + +export * from './agent-description-manager.js'; diff --git a/typescript/ts_sdk/src/core/agent-discovery/agent-discovery-manager.ts b/typescript/ts_sdk/src/core/agent-discovery/agent-discovery-manager.ts new file mode 100644 index 0000000..281a841 --- /dev/null +++ b/typescript/ts_sdk/src/core/agent-discovery/agent-discovery-manager.ts @@ -0,0 +1,229 @@ +/** + * Agent Discovery Manager + * + * Manages agent discovery through active and passive mechanisms. + */ + +import { HTTPClient } from '../../transport/http-client.js'; +import { NetworkError } from '../../errors/index.js'; +import type { + DiscoveryDocument, + AgentDescriptionItem, + SearchQuery, + DIDIdentity, +} from '../../types/index.js'; + +/** + * Agent Discovery Manager class + */ +export class AgentDiscoveryManager { + private readonly httpClient: HTTPClient; + + constructor(httpClient: HTTPClient) { + this.httpClient = httpClient; + } + + /** + * Discover agents from a domain using active discovery + * + * Fetches the agent discovery document from the .well-known/agent-descriptions endpoint + * and recursively fetches all pages if pagination is present. + * + * @param domain - The domain to discover agents from + * @param identity - Optional DID identity for authentication + * @returns Array of agent description items + */ + async discoverAgents( + domain: string, + identity?: DIDIdentity + ): Promise { + // Construct .well-known URL + const url = this.constructWellKnownUrl(domain); + + // Fetch all pages recursively + return this.fetchAllPages(url, identity); + } + + /** + * Register with a search service using passive discovery + * + * @param searchServiceUrl - The URL of the search service + * @param agentDescriptionUrl - The URL of the agent description to register + * @param identity - DID identity for authentication + */ + async registerWithSearchService( + searchServiceUrl: string, + agentDescriptionUrl: string, + identity: DIDIdentity + ): Promise { + // Construct registration request + const registrationRequest = { + agentDescriptionUrl, + }; + + // Send POST request with authentication + await this.httpClient.post( + searchServiceUrl, + registrationRequest, + identity + ); + } + + /** + * Search for agents using a search service + * + * @param searchServiceUrl - The URL of the search service + * @param query - Search query parameters + * @param identity - Optional DID identity for authentication + * @returns Array of matching agent description items + */ + async searchAgents( + searchServiceUrl: string, + query: SearchQuery, + identity?: DIDIdentity + ): Promise { + // Send POST request with search query + const response = await this.httpClient.post( + searchServiceUrl, + query, + identity + ); + + // Parse search results + const results = await this.parseSearchResults(response); + + return results; + } + + /** + * Construct .well-known URL from domain + * + * @param domain - The domain + * @returns The .well-known URL + */ + private constructWellKnownUrl(domain: string): string { + // Remove protocol if present + const cleanDomain = domain.replace(/^https?:\/\//, ''); + + // Use http:// for localhost, https:// for everything else + const domainWithoutPort = cleanDomain.split(':')[0]; + const protocol = domainWithoutPort === 'localhost' || domainWithoutPort === '127.0.0.1' ? 'http://' : 'https://'; + + // Construct URL + return `${protocol}${cleanDomain}/.well-known/agent-descriptions`; + } + + /** + * Fetch all pages recursively + * + * @param url - The URL to fetch + * @param identity - Optional DID identity for authentication + * @returns Array of all agent description items from all pages + */ + private async fetchAllPages( + url: string, + identity?: DIDIdentity + ): Promise { + const allItems: AgentDescriptionItem[] = []; + + let currentUrl: string | undefined = url; + + while (currentUrl) { + // Fetch the current page + const response = await this.httpClient.get(currentUrl, identity); + + // Parse JSON response + const document = await this.parseDiscoveryDocument(response); + + // Validate document structure + this.validateDiscoveryDocument(document); + + // Add items from this page + allItems.push(...document.items); + + // Move to next page if it exists + currentUrl = document.next; + } + + return allItems; + } + + /** + * Parse discovery document from response + * + * @param response - The HTTP response + * @returns The parsed discovery document + */ + private async parseDiscoveryDocument( + response: Response + ): Promise { + try { + const document = await response.json(); + return document as DiscoveryDocument; + } catch (error) { + throw new NetworkError( + 'Failed to parse discovery document', + undefined, + error as Error + ); + } + } + + /** + * Validate discovery document structure + * + * @param document - The discovery document to validate + */ + private validateDiscoveryDocument(document: DiscoveryDocument): void { + if (!document['@type'] || document['@type'] !== 'CollectionPage') { + throw new NetworkError( + 'Invalid discovery document: missing or invalid @type' + ); + } + + if (!document.url) { + throw new NetworkError('Invalid discovery document: missing url'); + } + + if (!Array.isArray(document.items)) { + throw new NetworkError('Invalid discovery document: missing or invalid items array'); + } + } + + /** + * Parse search results from response + * + * @param response - The HTTP response + * @returns Array of agent description items + */ + private async parseSearchResults( + response: Response + ): Promise { + try { + const data = await response.json(); + + // Handle both SearchResult format and direct array format for backwards compatibility + if (Array.isArray(data)) { + return data as AgentDescriptionItem[]; + } + + // Validate that items array exists in SearchResult format + if (!Array.isArray(data.items)) { + throw new NetworkError( + 'Invalid search results: missing or invalid items array' + ); + } + + return data.items as AgentDescriptionItem[]; + } catch (error) { + if (error instanceof NetworkError) { + throw error; + } + throw new NetworkError( + 'Failed to parse search results', + undefined, + error as Error + ); + } + } +} diff --git a/typescript/ts_sdk/src/core/agent-discovery/index.ts b/typescript/ts_sdk/src/core/agent-discovery/index.ts new file mode 100644 index 0000000..922bdbe --- /dev/null +++ b/typescript/ts_sdk/src/core/agent-discovery/index.ts @@ -0,0 +1,5 @@ +/** + * Agent Discovery module exports + */ + +export * from './agent-discovery-manager.js'; diff --git a/typescript/ts_sdk/src/core/auth/authentication-manager.ts b/typescript/ts_sdk/src/core/auth/authentication-manager.ts new file mode 100644 index 0000000..41874d0 --- /dev/null +++ b/typescript/ts_sdk/src/core/auth/authentication-manager.ts @@ -0,0 +1,517 @@ +/** + * Authentication Manager for handling HTTP authentication using DID:WBA + */ + +import { DIDManager } from '../did/did-manager.js'; +import { AuthenticationError } from '../../errors/index.js'; +import { encodeSignature } from '../../crypto/signing.js'; +import type { DIDIdentity } from '../../types/index.js'; + +/** + * Configuration for Authentication Manager + */ +export interface AuthConfig { + maxTokenAge: number; // milliseconds + nonceLength: number; + clockSkewTolerance: number; // seconds +} + +/** + * Verification result structure + */ +export interface VerificationResult { + success: boolean; + did?: string; + error?: string; +} + +/** + * Token verification result structure + */ +export interface TokenVerificationResult { + valid: boolean; + did?: string; + expiresAt?: number; + error?: string; +} + +/** + * Authentication Manager class + */ +export class AuthenticationManager { + private readonly config: AuthConfig; + private readonly didManager: DIDManager; + private readonly usedNonces: Set = new Set(); + + constructor(didManager: DIDManager, config: AuthConfig) { + this.didManager = didManager; + this.config = config; + } + + /** + * Generate authentication header for outgoing request + * + * @param identity - The DID identity to authenticate with + * @param targetDomain - The domain of the service being accessed + * @param verificationMethodId - The verification method ID to use for signing + * @returns The Authorization header value + */ + async generateAuthHeader( + identity: DIDIdentity, + targetDomain: string, + verificationMethodId: string + ): Promise { + // Verify the verification method exists + const keyMetadata = identity.privateKeys.get(verificationMethodId); + if (!keyMetadata) { + throw new AuthenticationError( + `Verification method not found: ${verificationMethodId}` + ); + } + + // Generate nonce + const nonce = this.generateNonce(); + + // Generate timestamp in ISO 8601 format + const timestamp = new Date().toISOString(); + + // Extract verification method fragment (remove DID# prefix) + const verificationMethodFragment = verificationMethodId.split('#')[1]; + if (!verificationMethodFragment) { + throw new AuthenticationError( + 'Invalid verification method ID: must contain # fragment' + ); + } + + // Construct signature data according to ANP spec + const signatureData = { + nonce, + timestamp, + service: targetDomain, + did: identity.did, + }; + + // Canonicalize using JCS (JSON Canonicalization Scheme) + const canonicalJson = this.canonicalizeJSON(signatureData); + + // Hash the canonical JSON + const encoder = new TextEncoder(); + const data = encoder.encode(canonicalJson); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hash = new Uint8Array(hashBuffer); + + // Sign the hash + const signatureResult = await this.didManager.sign(identity, hash); + + // Encode signature to base64url + const signatureBase64 = encodeSignature(signatureResult.value); + + // Construct Authorization header + const authHeader = `DIDWba did="${identity.did}", nonce="${nonce}", timestamp="${timestamp}", verification_method="${verificationMethodFragment}", signature="${signatureBase64}"`; + + return authHeader; + } + + /** + * Generate a cryptographically secure nonce + */ + private generateNonce(): string { + const bytes = new Uint8Array(this.config.nonceLength); + crypto.getRandomValues(bytes); + + // Convert to base64url + return this.base64UrlEncode(bytes); + } + + /** + * Canonicalize JSON according to JCS (RFC 8785) + * + * This is a simplified implementation that handles the basic cases. + * For production use, consider using a full JCS library. + */ + private canonicalizeJSON(obj: any): string { + if (obj === null) { + return 'null'; + } + + if (typeof obj === 'boolean') { + return obj ? 'true' : 'false'; + } + + if (typeof obj === 'number') { + // Handle numbers according to JCS spec + if (!isFinite(obj)) { + throw new Error('Cannot canonicalize non-finite numbers'); + } + return JSON.stringify(obj); + } + + if (typeof obj === 'string') { + return JSON.stringify(obj); + } + + if (Array.isArray(obj)) { + const items = obj.map((item) => this.canonicalizeJSON(item)); + return '[' + items.join(',') + ']'; + } + + if (typeof obj === 'object') { + // Sort keys lexicographically + const keys = Object.keys(obj).sort(); + const pairs = keys.map((key) => { + const value = this.canonicalizeJSON(obj[key]); + return `${JSON.stringify(key)}:${value}`; + }); + return '{' + pairs.join(',') + '}'; + } + + throw new Error(`Cannot canonicalize type: ${typeof obj}`); + } + + /** + * Encode bytes to base64url + */ + private base64UrlEncode(bytes: Uint8Array): string { + const base64 = btoa(String.fromCharCode(...bytes)); + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + } + + /** + * Verify incoming request authentication + * + * @param authHeader - The Authorization header value + * @param expectedDomain - The expected service domain + * @returns Verification result + */ + async verifyAuthHeader( + authHeader: string, + expectedDomain: string + ): Promise { + try { + // Parse the auth header + const parsed = this.parseAuthHeader(authHeader); + if (!parsed) { + return { + success: false, + error: 'Invalid authorization header format', + }; + } + + const { did, nonce, timestamp, verificationMethod, signature } = parsed; + + // Validate timestamp + const timestampValid = this.validateTimestamp(timestamp); + if (!timestampValid) { + return { + success: false, + error: 'Invalid or expired timestamp', + }; + } + + // Check for nonce replay + if (this.usedNonces.has(nonce)) { + return { + success: false, + error: 'Nonce has already been used (replay attack detected)', + }; + } + + // Resolve DID document + let didDocument; + try { + didDocument = await this.didManager.resolveDID(did); + } catch (error) { + return { + success: false, + error: `DID resolution failed: ${(error as Error).message}`, + }; + } + + // Reconstruct signature data + const signatureData = { + nonce, + timestamp, + service: expectedDomain, + did, + }; + + // Canonicalize and hash + const canonicalJson = this.canonicalizeJSON(signatureData); + const encoder = new TextEncoder(); + const data = encoder.encode(canonicalJson); + const hashBuffer = await crypto.subtle.digest('SHA-256', data); + const hash = new Uint8Array(hashBuffer); + + // Decode signature + const signatureBytes = this.base64UrlDecode(signature); + + // Verify signature + const verificationMethodId = `${did}#${verificationMethod}`; + const signatureObj = { + value: signatureBytes, + verificationMethod: verificationMethodId, + }; + + const isValid = await this.didManager.verify( + did, + hash, + signatureObj, + didDocument + ); + + if (!isValid) { + return { + success: false, + error: 'Invalid signature', + }; + } + + // Mark nonce as used + this.usedNonces.add(nonce); + + // Clean up old nonces periodically (simple implementation) + if (this.usedNonces.size > 10000) { + this.usedNonces.clear(); + } + + return { + success: true, + did, + }; + } catch (error) { + return { + success: false, + error: `Verification failed: ${(error as Error).message}`, + }; + } + } + + /** + * Parse authorization header + */ + private parseAuthHeader(authHeader: string): { + did: string; + nonce: string; + timestamp: string; + verificationMethod: string; + signature: string; + } | null { + // Check if header starts with DIDWba + if (!authHeader.startsWith('DIDWba ')) { + return null; + } + + // Remove prefix + const headerContent = authHeader.substring(7); + + // Parse key-value pairs + const regex = /(\w+)="([^"]+)"/g; + const matches: Record = {}; + let match; + + while ((match = regex.exec(headerContent)) !== null) { + matches[match[1]] = match[2]; + } + + // Validate required fields + if ( + !matches.did || + !matches.nonce || + !matches.timestamp || + !matches.verification_method || + !matches.signature + ) { + return null; + } + + return { + did: matches.did, + nonce: matches.nonce, + timestamp: matches.timestamp, + verificationMethod: matches.verification_method, + signature: matches.signature, + }; + } + + /** + * Validate timestamp + */ + private validateTimestamp(timestamp: string): boolean { + try { + const timestampDate = new Date(timestamp); + const now = new Date(); + + // Check if timestamp is valid + if (isNaN(timestampDate.getTime())) { + return false; + } + + // Calculate difference in seconds + const diffSeconds = Math.abs(now.getTime() - timestampDate.getTime()) / 1000; + + // Check if within clock skew tolerance + return diffSeconds <= this.config.clockSkewTolerance; + } catch { + return false; + } + } + + /** + * Decode base64url to bytes + */ + private base64UrlDecode(str: string): Uint8Array { + // Add padding if needed + const padding = '='.repeat((4 - (str.length % 4)) % 4); + const base64 = str.replace(/-/g, '+').replace(/_/g, '/') + padding; + + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; + } + + /** + * Generate access token after successful authentication + * + * @param did - The DID to generate token for + * @param expiresIn - Token expiration time in milliseconds + * @returns The access token + */ + generateAccessToken(did: string, expiresIn: number): string { + const now = Math.floor(Date.now() / 1000); + const exp = Math.floor((Date.now() + expiresIn) / 1000); + + // Create JWT header + const header = { + alg: 'HS256', + typ: 'JWT', + }; + + // Create JWT payload + const payload = { + did, + iat: now, + exp, + }; + + // Encode header and payload + const headerBase64 = this.base64UrlEncode( + new TextEncoder().encode(JSON.stringify(header)) + ); + const payloadBase64 = this.base64UrlEncode( + new TextEncoder().encode(JSON.stringify(payload)) + ); + + // Create signature data + const signatureData = `${headerBase64}.${payloadBase64}`; + + // Generate signature (using a simple HMAC-like approach with the DID as secret) + // Note: In production, use a proper secret key management system + const signature = this.generateTokenSignature(signatureData, did); + + return `${signatureData}.${signature}`; + } + + /** + * Verify access token + * + * @param token - The access token to verify + * @returns Token verification result + */ + verifyAccessToken(token: string): TokenVerificationResult { + try { + // Split token into parts + const parts = token.split('.'); + if (parts.length !== 3) { + return { + valid: false, + error: 'Invalid token format', + }; + } + + const [headerBase64, payloadBase64, signature] = parts; + + // Decode payload + let payload: any; + try { + const payloadJson = new TextDecoder().decode( + this.base64UrlDecode(payloadBase64) + ); + payload = JSON.parse(payloadJson); + } catch { + return { + valid: false, + error: 'Invalid token payload', + }; + } + + // Validate required fields + if (!payload.did || !payload.exp || !payload.iat) { + return { + valid: false, + error: 'Missing required token fields', + }; + } + + // Check expiration + const now = Math.floor(Date.now() / 1000); + if (payload.exp < now) { + return { + valid: false, + error: 'Token has expired', + }; + } + + // Verify signature + const signatureData = `${headerBase64}.${payloadBase64}`; + const expectedSignature = this.generateTokenSignature( + signatureData, + payload.did + ); + + if (signature !== expectedSignature) { + return { + valid: false, + error: 'Invalid token signature', + }; + } + + return { + valid: true, + did: payload.did, + expiresAt: payload.exp * 1000, // Convert to milliseconds + }; + } catch (error) { + return { + valid: false, + error: `Token verification failed: ${(error as Error).message}`, + }; + } + } + + /** + * Generate token signature + * + * Note: This is a simplified implementation for demonstration. + * In production, use proper HMAC with a secure secret key. + */ + private generateTokenSignature(data: string, secret: string): string { + // For synchronous operation, use a simple hash-based approach + // This is not cryptographically secure for production use + const encoder = new TextEncoder(); + const combined = encoder.encode(data + secret); + + // Simple hash using Array.from for synchronous operation + let hash = 0; + for (let i = 0; i < combined.length; i++) { + hash = ((hash << 5) - hash) + combined[i]; + hash = hash & hash; // Convert to 32-bit integer + } + + // Convert to base64url + const hashBytes = new Uint8Array(new Int32Array([hash]).buffer); + return this.base64UrlEncode(hashBytes); + } +} diff --git a/typescript/ts_sdk/src/core/auth/index.ts b/typescript/ts_sdk/src/core/auth/index.ts new file mode 100644 index 0000000..bfec82c --- /dev/null +++ b/typescript/ts_sdk/src/core/auth/index.ts @@ -0,0 +1,10 @@ +/** + * Authentication module exports + */ + +export { + AuthenticationManager, + type AuthConfig, + type VerificationResult, + type TokenVerificationResult, +} from './authentication-manager.js'; diff --git a/typescript/ts_sdk/src/core/did/did-manager.ts b/typescript/ts_sdk/src/core/did/did-manager.ts new file mode 100644 index 0000000..93a0f3f --- /dev/null +++ b/typescript/ts_sdk/src/core/did/did-manager.ts @@ -0,0 +1,440 @@ +/** + * DID Manager for creating and managing DID:WBA identities + */ + +import { + generateKeyPair, + KeyType, + exportPublicKeyJWK, +} from '../../crypto/key-generation.js'; +import { sign as cryptoSign, verify as cryptoVerify } from '../../crypto/signing.js'; +import { DIDResolutionError } from '../../errors/index.js'; +import type { + DIDDocument, + DIDIdentity, + CreateDIDOptions, + ResolveDIDOptions, + VerificationMethod, +} from '../../types/index.js'; + +/** + * Key metadata for tracking key types + */ +interface KeyMetadata { + key: CryptoKey; + type: KeyType; +} + +/** + * Signature structure + */ +export interface Signature { + value: Uint8Array; + verificationMethod: string; +} + +/** + * Cache entry for resolved DID documents + */ +interface CacheEntry { + document: DIDDocument; + timestamp: number; +} + +/** + * Configuration for DID Manager + */ +export interface DIDManagerConfig { + cacheTTL?: number; // Cache TTL in milliseconds (default: 5 minutes) + timeout?: number; // HTTP timeout in milliseconds (default: 10 seconds) +} + +/** + * DID Manager class for managing DID:WBA identities + */ +export class DIDManager { + private cache: Map = new Map(); + private readonly cacheTTL: number; + private readonly timeout: number; + + constructor(config: DIDManagerConfig = {}) { + this.cacheTTL = config.cacheTTL ?? 5 * 60 * 1000; // 5 minutes default + this.timeout = config.timeout ?? 10000; // 10 seconds default + } + + /** + * Create a new DID:WBA identity + */ + async createDID(options: CreateDIDOptions): Promise { + // Validate domain + this.validateDomain(options.domain); + + // Validate port if provided + if (options.port !== undefined) { + this.validatePort(options.port); + } + + // Construct DID identifier + const did = this.constructDID(options); + + // Generate keys + const authKeyPair = await generateKeyPair(KeyType.ED25519); + const keyAgreementPair = await generateKeyPair(KeyType.X25519); + + // Export public keys + const authPublicKeyJwk = await exportPublicKeyJWK(authKeyPair.publicKey); + const keyAgreementPublicKeyJwk = await exportPublicKeyJWK( + keyAgreementPair.publicKey + ); + + // Create verification methods + const authVerificationMethod: VerificationMethod = { + id: `${did}#auth-key`, + type: 'Ed25519VerificationKey2020', + controller: did, + publicKeyJwk: authPublicKeyJwk, + }; + + const keyAgreementVerificationMethod: VerificationMethod = { + id: `${did}#key-agreement`, + type: 'X25519KeyAgreementKey2019', + controller: did, + publicKeyJwk: keyAgreementPublicKeyJwk, + }; + + // Create DID document + const document: DIDDocument = { + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/jws-2020/v1', + ], + id: did, + verificationMethod: [ + authVerificationMethod, + keyAgreementVerificationMethod, + ], + authentication: [authVerificationMethod.id], + keyAgreement: [keyAgreementVerificationMethod.id], + }; + + // Store private keys with metadata + const privateKeys = new Map(); + privateKeys.set(authVerificationMethod.id, { + key: authKeyPair.privateKey, + type: KeyType.ED25519, + }); + privateKeys.set(keyAgreementVerificationMethod.id, { + key: keyAgreementPair.privateKey, + type: KeyType.X25519, + }); + + return { + did, + document, + privateKeys, + }; + } + + /** + * Resolve a DID to its document + */ + async resolveDID( + did: string, + options: ResolveDIDOptions = {} + ): Promise { + // Check cache if enabled + if (options.cache !== false) { + const cached = this.cache.get(did); + if (cached && Date.now() - cached.timestamp < this.cacheTTL) { + return cached.document; + } + } + + // Construct URL from DID + const url = this.constructURLFromDID(did); + + try { + // Fetch DID document + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.timeout); + + const response = await fetch(url, { + signal: controller.signal, + headers: { + Accept: 'application/did+json', + }, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new DIDResolutionError( + did, + new Error(`HTTP ${response.status}: ${response.statusText}`) + ); + } + + const document = await response.json(); + + // Validate document + this.validateDIDDocument(document, did); + + // Cache the document + this.cache.set(did, { + document, + timestamp: Date.now(), + }); + + return document; + } catch (error) { + if (error instanceof DIDResolutionError) { + throw error; + } + throw new DIDResolutionError(did, error as Error); + } + } + + /** + * Sign data with a DID identity + */ + async sign(identity: DIDIdentity, data: Uint8Array): Promise { + // Get the authentication key + const authMethodId = `${identity.did}#auth-key`; + const keyMetadata = identity.privateKeys.get(authMethodId); + + if (!keyMetadata) { + throw new Error('Private key not found for authentication'); + } + + // Sign the data + const signatureValue = await cryptoSign( + keyMetadata.key, + data, + keyMetadata.type as KeyType + ); + + return { + value: signatureValue, + verificationMethod: authMethodId, + }; + } + + /** + * Verify a signature + */ + async verify( + did: string, + data: Uint8Array, + signature: Signature, + document?: DIDDocument + ): Promise { + // Resolve DID document if not provided + const didDocument = document ?? (await this.resolveDID(did)); + + // Find the verification method + const verificationMethod = didDocument.verificationMethod.find( + (vm) => vm.id === signature.verificationMethod + ); + + if (!verificationMethod) { + throw new Error( + `Verification method not found: ${signature.verificationMethod}` + ); + } + + if (!verificationMethod.publicKeyJwk) { + throw new Error('Public key not found in verification method'); + } + + // Determine key type from verification method type + const keyType = this.getKeyTypeFromVerificationMethod(verificationMethod); + + // Import the public key + const publicKey = await crypto.subtle.importKey( + 'jwk', + verificationMethod.publicKeyJwk, + { + name: keyType === KeyType.ED25519 ? 'Ed25519' : 'ECDSA', + namedCurve: keyType === KeyType.ED25519 ? 'Ed25519' : 'P-256', + } as any, + true, + ['verify'] + ); + + // Verify the signature + return cryptoVerify(publicKey, data, signature.value, keyType); + } + + /** + * Export DID document (without private keys) + */ + exportDocument(identity: DIDIdentity): DIDDocument { + return identity.document; + } + + /** + * Validate domain format + */ + private validateDomain(domain: string): void { + if (!domain || domain.trim() === '') { + throw new Error('Invalid domain: domain cannot be empty'); + } + + // Check for protocol + if (domain.includes('://')) { + throw new Error('Invalid domain: domain should not include protocol'); + } + + // Check for spaces + if (domain.includes(' ')) { + throw new Error('Invalid domain: domain cannot contain spaces'); + } + + // Basic domain validation (alphanumeric, dots, hyphens, optional port) + // Supports formats like: example.com, localhost, example.com:8080, localhost:9000 + const domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*(:[0-9]{1,5})?$/; + if (!domainRegex.test(domain)) { + throw new Error('Invalid domain: invalid domain format'); + } + + // If port is included in domain, validate it + if (domain.includes(':')) { + const parts = domain.split(':'); + if (parts.length !== 2) { + throw new Error('Invalid domain: invalid port format'); + } + const port = parseInt(parts[1], 10); + if (isNaN(port) || port < 1 || port > 65535) { + throw new Error('Invalid domain: port must be between 1 and 65535'); + } + } + } + + /** + * Validate port number + */ + private validatePort(port: number): void { + if (port < 1 || port > 65535) { + throw new Error('Invalid port: port must be between 1 and 65535'); + } + } + + /** + * Construct DID identifier from options + */ + private constructDID(options: CreateDIDOptions): string { + const domain = options.domain.toLowerCase(); + let didIdentifier = 'did:wba:'; + + // Handle port encoding + if (options.port !== undefined && options.port !== 443) { + didIdentifier += encodeURIComponent(`${domain}:${options.port}`); + } else { + didIdentifier += domain; + } + + // Handle path encoding + if (options.path) { + didIdentifier += ':' + encodeURIComponent(options.path); + } + + return didIdentifier; + } + + /** + * Construct URL from DID identifier + */ + private constructURLFromDID(did: string): string { + // Parse DID + if (!did.startsWith('did:wba:')) { + throw new Error('Invalid DID: must start with did:wba:'); + } + + const parts = did.substring(8).split(':'); + const domainPart = decodeURIComponent(parts[0]); + const pathPart = parts.length > 1 ? decodeURIComponent(parts[1]) : null; + + // Extract domain and port + let domain: string; + let port: string | null = null; + + if (domainPart.includes(':')) { + const domainPortParts = domainPart.split(':'); + domain = domainPortParts[0]; + port = domainPortParts[1]; + } else { + domain = domainPart; + } + + // Construct URL + // Use http:// for localhost, https:// for everything else + const protocol = domain === 'localhost' || domain === '127.0.0.1' ? 'http://' : 'https://'; + let url = protocol; + url += domain; + + if (port) { + url += ':' + port; + } + + if (pathPart) { + url += '/' + pathPart + '/did.json'; + } else { + url += '/.well-known/did.json'; + } + + return url; + } + + /** + * Validate DID document + */ + private validateDIDDocument(document: any, expectedDID: string): void { + if (!document.id) { + throw new DIDResolutionError( + expectedDID, + new Error('Invalid DID document: missing id field') + ); + } + + if (document.id !== expectedDID) { + throw new DIDResolutionError( + expectedDID, + new Error(`DID mismatch: expected ${expectedDID}, got ${document.id}`) + ); + } + + if (!Array.isArray(document.verificationMethod)) { + throw new DIDResolutionError( + expectedDID, + new Error('Invalid DID document: verificationMethod must be an array') + ); + } + + if (!Array.isArray(document.authentication)) { + throw new DIDResolutionError( + expectedDID, + new Error('Invalid DID document: authentication must be an array') + ); + } + } + + /** + * Get key type from verification method type + */ + private getKeyTypeFromVerificationMethod( + verificationMethod: VerificationMethod + ): KeyType { + switch (verificationMethod.type) { + case 'Ed25519VerificationKey2020': + return KeyType.ED25519; + case 'EcdsaSecp256k1VerificationKey2019': + return KeyType.ECDSA_SECP256K1; + case 'X25519KeyAgreementKey2019': + return KeyType.X25519; + default: + throw new Error( + `Unsupported verification method type: ${verificationMethod.type}` + ); + } + } +} diff --git a/typescript/ts_sdk/src/core/did/index.ts b/typescript/ts_sdk/src/core/did/index.ts new file mode 100644 index 0000000..b569c75 --- /dev/null +++ b/typescript/ts_sdk/src/core/did/index.ts @@ -0,0 +1,7 @@ +/** + * DID (Decentralized Identifier) module + * + * This module provides functionality for creating and managing DID:WBA identities. + */ + +export * from './did-manager.js'; diff --git a/typescript/ts_sdk/src/crypto/.gitkeep b/typescript/ts_sdk/src/crypto/.gitkeep new file mode 100644 index 0000000..7a23ed5 --- /dev/null +++ b/typescript/ts_sdk/src/crypto/.gitkeep @@ -0,0 +1 @@ +# Cryptography modules will be implemented here diff --git a/typescript/ts_sdk/src/crypto/encryption.ts b/typescript/ts_sdk/src/crypto/encryption.ts new file mode 100644 index 0000000..9251bd6 --- /dev/null +++ b/typescript/ts_sdk/src/crypto/encryption.ts @@ -0,0 +1,234 @@ +/** + * Encryption and decryption utilities using AES-GCM + */ + +import { CryptoError } from '../errors/index.js'; + +/** + * Encrypted data structure + */ +export interface EncryptedData { + ciphertext: Uint8Array; + iv: Uint8Array; + tag: Uint8Array; +} + +/** + * Encrypt data using AES-GCM + * + * @param key - The encryption key (must be AES-GCM key) + * @param plaintext - The data to encrypt + * @returns A promise that resolves to the encrypted data + * @throws {CryptoError} If encryption fails + */ +export async function encrypt( + key: CryptoKey, + plaintext: Uint8Array +): Promise { + try { + // Generate random IV + const iv = generateIV(); + + // Encrypt the data + const ciphertextWithTag = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv: iv as BufferSource, + tagLength: 128, // 128-bit authentication tag + }, + key, + plaintext as BufferSource + ); + + // AES-GCM returns ciphertext with tag appended + const ciphertextWithTagArray = new Uint8Array(ciphertextWithTag); + + // Split ciphertext and tag (tag is last 16 bytes) + const ciphertext = ciphertextWithTagArray.slice(0, -16); + const tag = ciphertextWithTagArray.slice(-16); + + return { + ciphertext, + iv, + tag, + }; + } catch (error) { + throw new CryptoError('Failed to encrypt data', error as Error); + } +} + +/** + * Decrypt data using AES-GCM + * + * @param key - The decryption key (must be AES-GCM key) + * @param encrypted - The encrypted data + * @returns A promise that resolves to the decrypted plaintext + * @throws {CryptoError} If decryption fails + */ +export async function decrypt( + key: CryptoKey, + encrypted: EncryptedData +): Promise { + try { + // Validate inputs + if (encrypted.iv.length !== 12) { + throw new CryptoError( + `Invalid IV length: ${encrypted.iv.length}. Expected 12 bytes` + ); + } + if (encrypted.tag.length !== 16) { + throw new CryptoError( + `Invalid tag length: ${encrypted.tag.length}. Expected 16 bytes` + ); + } + + // Combine ciphertext and tag for Web Crypto API + const ciphertextWithTag = new Uint8Array( + encrypted.ciphertext.length + encrypted.tag.length + ); + ciphertextWithTag.set(encrypted.ciphertext, 0); + ciphertextWithTag.set(encrypted.tag, encrypted.ciphertext.length); + + // Decrypt the data + const plaintext = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: encrypted.iv as BufferSource, + tagLength: 128, + }, + key, + ciphertextWithTag as BufferSource + ); + + return new Uint8Array(plaintext); + } catch (error) { + // Check if it's an authentication failure + if ( + error instanceof Error && + (error.message.includes('authentication') || + error.message.includes('tag') || + error.name === 'OperationError') + ) { + throw new CryptoError( + 'Decryption failed: Authentication tag verification failed. Data may have been tampered with.', + error + ); + } + throw new CryptoError('Failed to decrypt data', error as Error); + } +} + +/** + * Generate a random initialization vector (IV) for AES-GCM + * + * @returns A 12-byte random IV + */ +export function generateIV(): Uint8Array { + // AES-GCM standard IV length is 12 bytes (96 bits) + return crypto.getRandomValues(new Uint8Array(12)); +} + +/** + * Encrypt data with additional authenticated data (AAD) + * + * @param key - The encryption key + * @param plaintext - The data to encrypt + * @param additionalData - Additional data to authenticate but not encrypt + * @returns A promise that resolves to the encrypted data + * @throws {CryptoError} If encryption fails + */ +export async function encryptWithAAD( + key: CryptoKey, + plaintext: Uint8Array, + additionalData: Uint8Array +): Promise { + try { + const iv = generateIV(); + + const ciphertextWithTag = await crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv: iv as BufferSource, + additionalData: additionalData as BufferSource, + tagLength: 128, + }, + key, + plaintext as BufferSource + ); + + const ciphertextWithTagArray = new Uint8Array(ciphertextWithTag); + const ciphertext = ciphertextWithTagArray.slice(0, -16); + const tag = ciphertextWithTagArray.slice(-16); + + return { + ciphertext, + iv, + tag, + }; + } catch (error) { + throw new CryptoError( + 'Failed to encrypt data with AAD', + error as Error + ); + } +} + +/** + * Decrypt data with additional authenticated data (AAD) + * + * @param key - The decryption key + * @param encrypted - The encrypted data + * @param additionalData - Additional data that was authenticated + * @returns A promise that resolves to the decrypted plaintext + * @throws {CryptoError} If decryption fails + */ +export async function decryptWithAAD( + key: CryptoKey, + encrypted: EncryptedData, + additionalData: Uint8Array +): Promise { + try { + if (encrypted.iv.length !== 12) { + throw new CryptoError( + `Invalid IV length: ${encrypted.iv.length}. Expected 12 bytes` + ); + } + if (encrypted.tag.length !== 16) { + throw new CryptoError( + `Invalid tag length: ${encrypted.tag.length}. Expected 16 bytes` + ); + } + + const ciphertextWithTag = new Uint8Array( + encrypted.ciphertext.length + encrypted.tag.length + ); + ciphertextWithTag.set(encrypted.ciphertext, 0); + ciphertextWithTag.set(encrypted.tag, encrypted.ciphertext.length); + + const plaintext = await crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: encrypted.iv as BufferSource, + additionalData: additionalData as BufferSource, + tagLength: 128, + }, + key, + ciphertextWithTag as BufferSource + ); + + return new Uint8Array(plaintext); + } catch (error) { + if ( + error instanceof Error && + (error.message.includes('authentication') || + error.message.includes('tag') || + error.name === 'OperationError') + ) { + throw new CryptoError( + 'Decryption failed: Authentication tag verification failed. Data may have been tampered with.', + error + ); + } + throw new CryptoError('Failed to decrypt data with AAD', error as Error); + } +} diff --git a/typescript/ts_sdk/src/crypto/index.ts b/typescript/ts_sdk/src/crypto/index.ts new file mode 100644 index 0000000..2e4f199 --- /dev/null +++ b/typescript/ts_sdk/src/crypto/index.ts @@ -0,0 +1,14 @@ +/** + * Cryptography module for ANP SDK + * + * This module provides cryptographic operations including: + * - Key generation (ECDSA, Ed25519, X25519) + * - Signing and verification + * - ECDHE key exchange + * - AES-GCM encryption and decryption + */ + +export * from './key-generation.js'; +export * from './signing.js'; +export * from './key-exchange.js'; +export * from './encryption.js'; diff --git a/typescript/ts_sdk/src/crypto/key-exchange.ts b/typescript/ts_sdk/src/crypto/key-exchange.ts new file mode 100644 index 0000000..e95fa21 --- /dev/null +++ b/typescript/ts_sdk/src/crypto/key-exchange.ts @@ -0,0 +1,138 @@ +/** + * ECDHE key exchange and key derivation utilities + */ + +import { CryptoError } from '../errors/index.js'; + +/** + * Perform ECDHE key exchange using X25519 + * + * @param privateKey - The local private key + * @param publicKey - The remote public key + * @returns A promise that resolves to the shared secret + * @throws {CryptoError} If key exchange fails + */ +export async function performKeyExchange( + privateKey: CryptoKey, + publicKey: CryptoKey +): Promise { + try { + // Validate that keys are X25519 + if (privateKey.algorithm.name !== 'X25519') { + throw new CryptoError( + `Invalid private key algorithm: ${privateKey.algorithm.name}. Expected X25519` + ); + } + if (publicKey.algorithm.name !== 'X25519') { + throw new CryptoError( + `Invalid public key algorithm: ${publicKey.algorithm.name}. Expected X25519` + ); + } + + // Validate key types + if (privateKey.type !== 'private') { + throw new CryptoError('First argument must be a private key'); + } + if (publicKey.type !== 'public') { + throw new CryptoError('Second argument must be a public key'); + } + + // Perform key exchange + const sharedSecret = await crypto.subtle.deriveBits( + { + name: 'X25519', + public: publicKey, + }, + privateKey, + 256 // 256 bits = 32 bytes + ); + + return new Uint8Array(sharedSecret); + } catch (error) { + if (error instanceof CryptoError) { + throw error; + } + throw new CryptoError('Failed to perform key exchange', error as Error); + } +} + +/** + * Derive an encryption key from a shared secret using HKDF + * + * @param sharedSecret - The shared secret from key exchange + * @param salt - Random salt for key derivation + * @param info - Optional context information (default: 'ANP encryption key') + * @returns A promise that resolves to a CryptoKey for AES-GCM encryption + * @throws {CryptoError} If key derivation fails + */ +export async function deriveKey( + sharedSecret: Uint8Array, + salt: Uint8Array, + info: Uint8Array = new TextEncoder().encode('ANP encryption key') +): Promise { + try { + // Validate inputs + if (sharedSecret.length !== 32) { + throw new CryptoError( + `Invalid shared secret length: ${sharedSecret.length}. Expected 32 bytes` + ); + } + if (salt.length === 0) { + throw new CryptoError('Salt cannot be empty'); + } + + // Import shared secret as key material + const keyMaterial = await crypto.subtle.importKey( + 'raw', + sharedSecret as BufferSource, + { name: 'HKDF' }, + false, + ['deriveKey'] + ); + + // Derive AES-GCM key using HKDF + const derivedKey = await crypto.subtle.deriveKey( + { + name: 'HKDF', + hash: 'SHA-256', + salt: salt as BufferSource, + info: info as BufferSource, + }, + keyMaterial, + { + name: 'AES-GCM', + length: 256, + }, + true, + ['encrypt', 'decrypt'] + ); + + return derivedKey; + } catch (error) { + if (error instanceof CryptoError) { + throw error; + } + throw new CryptoError('Failed to derive encryption key', error as Error); + } +} + +/** + * Validate a shared secret + * + * @param sharedSecret - The shared secret to validate + * @returns True if valid, false otherwise + */ +export function validateSharedSecret(sharedSecret: Uint8Array): boolean { + // X25519 shared secrets should be 32 bytes + if (sharedSecret.length !== 32) { + return false; + } + + // Check that it's not all zeros (invalid shared secret) + const isAllZeros = sharedSecret.every((byte) => byte === 0); + if (isAllZeros) { + return false; + } + + return true; +} diff --git a/typescript/ts_sdk/src/crypto/key-generation.ts b/typescript/ts_sdk/src/crypto/key-generation.ts new file mode 100644 index 0000000..ab3abfd --- /dev/null +++ b/typescript/ts_sdk/src/crypto/key-generation.ts @@ -0,0 +1,283 @@ +/** + * Key generation and format conversion utilities + */ + +import { CryptoError } from '../errors/index.js'; + +/** + * Supported key types for cryptographic operations + */ +export enum KeyType { + ECDSA_SECP256K1 = 'EcdsaSecp256k1VerificationKey2019', + ED25519 = 'Ed25519VerificationKey2020', + X25519 = 'X25519KeyAgreementKey2019', +} + +/** + * Generate a cryptographic key pair + * + * @param type - The type of key pair to generate + * @returns A promise that resolves to a CryptoKeyPair + * @throws {CryptoError} If key generation fails + */ +export async function generateKeyPair(type: KeyType): Promise { + try { + switch (type) { + case KeyType.ECDSA_SECP256K1: + return await crypto.subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-256', // Note: secp256k1 not natively supported, using P-256 as fallback + }, + true, + ['sign', 'verify'] + ); + + case KeyType.ED25519: + return await crypto.subtle.generateKey( + { + name: 'Ed25519', + }, + true, + ['sign', 'verify'] + ); + + case KeyType.X25519: + return (await crypto.subtle.generateKey( + { + name: 'X25519', + }, + true, + ['deriveKey', 'deriveBits'] + )) as CryptoKeyPair; + + default: + throw new CryptoError(`Unsupported key type: ${type}`); + } + } catch (error) { + if (error instanceof CryptoError) { + throw error; + } + throw new CryptoError( + `Failed to generate key pair for type ${type}`, + error as Error + ); + } +} + +/** + * Export a public key in JWK format + * + * @param publicKey - The public key to export + * @returns A promise that resolves to a JsonWebKey + * @throws {CryptoError} If export fails + */ +export async function exportPublicKeyJWK( + publicKey: CryptoKey +): Promise { + try { + return await crypto.subtle.exportKey('jwk', publicKey); + } catch (error) { + throw new CryptoError('Failed to export public key as JWK', error as Error); + } +} + +/** + * Export a private key in JWK format + * + * @param privateKey - The private key to export + * @returns A promise that resolves to a JsonWebKey + * @throws {CryptoError} If export fails + */ +export async function exportPrivateKeyJWK( + privateKey: CryptoKey +): Promise { + try { + return await crypto.subtle.exportKey('jwk', privateKey); + } catch (error) { + throw new CryptoError( + 'Failed to export private key as JWK', + error as Error + ); + } +} + +/** + * Export a public key in multibase format (base58btc) + * + * @param publicKey - The public key to export + * @returns A promise that resolves to a multibase-encoded string + * @throws {CryptoError} If export fails + */ +export async function exportPublicKeyMultibase( + publicKey: CryptoKey +): Promise { + try { + // Export as raw format + const rawKey = await crypto.subtle.exportKey('raw', publicKey); + const keyBytes = new Uint8Array(rawKey); + + // Encode to base58btc with 'z' prefix + const base58 = encodeBase58(keyBytes); + return `z${base58}`; + } catch (error) { + throw new CryptoError( + 'Failed to export public key as multibase', + error as Error + ); + } +} + +/** + * Import a public key from JWK format + * + * @param jwk - The JWK to import + * @param type - The key type + * @returns A promise that resolves to a CryptoKey + * @throws {CryptoError} If import fails + */ +export async function importPublicKeyJWK( + jwk: JsonWebKey, + type: KeyType +): Promise { + try { + const algorithm = getAlgorithmForKeyType(type); + const usages = getUsagesForKeyType(type, 'public'); + + return await crypto.subtle.importKey('jwk', jwk, algorithm, true, usages); + } catch (error) { + throw new CryptoError('Failed to import public key from JWK', error as Error); + } +} + +/** + * Import a private key from JWK format + * + * @param jwk - The JWK to import + * @param type - The key type + * @returns A promise that resolves to a CryptoKey + * @throws {CryptoError} If import fails + */ +export async function importPrivateKeyJWK( + jwk: JsonWebKey, + type: KeyType +): Promise { + try { + const algorithm = getAlgorithmForKeyType(type); + const usages = getUsagesForKeyType(type, 'private'); + + return await crypto.subtle.importKey('jwk', jwk, algorithm, true, usages); + } catch (error) { + throw new CryptoError('Failed to import private key from JWK', error as Error); + } +} + +/** + * Get the Web Crypto API algorithm for a key type + */ +function getAlgorithmForKeyType( + type: KeyType +): AlgorithmIdentifier | EcKeyGenParams { + switch (type) { + case KeyType.ECDSA_SECP256K1: + return { name: 'ECDSA', namedCurve: 'P-256' }; + case KeyType.ED25519: + return { name: 'Ed25519' }; + case KeyType.X25519: + return { name: 'X25519' }; + default: + throw new CryptoError(`Unsupported key type: ${type}`); + } +} + +/** + * Get the key usages for a key type and key kind + */ +function getUsagesForKeyType( + type: KeyType, + keyKind: 'public' | 'private' +): KeyUsage[] { + if (type === KeyType.X25519) { + return keyKind === 'private' ? ['deriveKey', 'deriveBits'] : []; + } + return keyKind === 'private' ? ['sign'] : ['verify']; +} + +/** + * Base58 alphabet (Bitcoin/IPFS variant) + */ +const BASE58_ALPHABET = + '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; + +/** + * Encode bytes to base58 + * + * @param bytes - The bytes to encode + * @returns The base58-encoded string + */ +function encodeBase58(bytes: Uint8Array): string { + if (bytes.length === 0) return ''; + + // Convert bytes to big integer + let num = 0n; + for (const byte of bytes) { + num = num * 256n + BigInt(byte); + } + + // Convert to base58 + let result = ''; + while (num > 0n) { + const remainder = Number(num % 58n); + result = BASE58_ALPHABET[remainder] + result; + num = num / 58n; + } + + // Add leading '1's for leading zero bytes + for (const byte of bytes) { + if (byte === 0) { + result = '1' + result; + } else { + break; + } + } + + return result; +} + +/** + * Decode base58 to bytes + * + * @param str - The base58 string to decode + * @returns The decoded bytes + */ +export function decodeBase58(str: string): Uint8Array { + if (str.length === 0) return new Uint8Array(0); + + // Convert base58 to big integer + let num = 0n; + for (const char of str) { + const index = BASE58_ALPHABET.indexOf(char); + if (index === -1) { + throw new CryptoError(`Invalid base58 character: ${char}`); + } + num = num * 58n + BigInt(index); + } + + // Convert to bytes + const bytes: number[] = []; + while (num > 0n) { + bytes.unshift(Number(num % 256n)); + num = num / 256n; + } + + // Add leading zero bytes for leading '1's + for (const char of str) { + if (char === '1') { + bytes.unshift(0); + } else { + break; + } + } + + return new Uint8Array(bytes); +} diff --git a/typescript/ts_sdk/src/crypto/signing.ts b/typescript/ts_sdk/src/crypto/signing.ts new file mode 100644 index 0000000..937b921 --- /dev/null +++ b/typescript/ts_sdk/src/crypto/signing.ts @@ -0,0 +1,127 @@ +/** + * Signing and verification utilities + */ + +import { CryptoError } from '../errors/index.js'; +import { KeyType } from './key-generation.js'; + +/** + * Sign data with a private key + * + * @param privateKey - The private key to sign with + * @param data - The data to sign + * @param keyType - The type of key being used + * @returns A promise that resolves to the signature + * @throws {CryptoError} If signing fails + */ +export async function sign( + privateKey: CryptoKey, + data: Uint8Array, + keyType: KeyType +): Promise { + try { + const algorithm = getSignAlgorithm(keyType); + const signature = await crypto.subtle.sign(algorithm, privateKey, data as BufferSource); + return new Uint8Array(signature); + } catch (error) { + throw new CryptoError( + `Failed to sign data with ${keyType}`, + error as Error + ); + } +} + +/** + * Verify a signature + * + * @param publicKey - The public key to verify with + * @param data - The original data that was signed + * @param signature - The signature to verify + * @param keyType - The type of key being used + * @returns A promise that resolves to true if valid, false otherwise + * @throws {CryptoError} If verification fails due to an error (not invalid signature) + */ +export async function verify( + publicKey: CryptoKey, + data: Uint8Array, + signature: Uint8Array, + keyType: KeyType +): Promise { + try { + const algorithm = getSignAlgorithm(keyType); + return await crypto.subtle.verify(algorithm, publicKey, signature as BufferSource, data as BufferSource); + } catch (error) { + throw new CryptoError( + `Failed to verify signature with ${keyType}`, + error as Error + ); + } +} + +/** + * Get the signing algorithm for a key type + */ +function getSignAlgorithm(keyType: KeyType): AlgorithmIdentifier | EcdsaParams { + switch (keyType) { + case KeyType.ECDSA_SECP256K1: + return { + name: 'ECDSA', + hash: { name: 'SHA-256' }, + }; + case KeyType.ED25519: + return { name: 'Ed25519' }; + case KeyType.X25519: + throw new CryptoError('X25519 keys cannot be used for signing'); + default: + throw new CryptoError(`Unsupported key type for signing: ${keyType}`); + } +} + +/** + * Encode a signature to base64url format + * + * @param signature - The signature bytes + * @returns The base64url-encoded signature + */ +export function encodeSignature(signature: Uint8Array): string { + return base64UrlEncode(signature); +} + +/** + * Decode a signature from base64url format + * + * @param encoded - The base64url-encoded signature + * @returns The signature bytes + * @throws {CryptoError} If decoding fails + */ +export function decodeSignature(encoded: string): Uint8Array { + try { + return base64UrlDecode(encoded); + } catch (error) { + throw new CryptoError('Failed to decode signature', error as Error); + } +} + +/** + * Encode bytes to base64url + */ +function base64UrlEncode(bytes: Uint8Array): string { + const base64 = btoa(String.fromCharCode(...bytes)); + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +} + +/** + * Decode base64url to bytes + */ +function base64UrlDecode(str: string): Uint8Array { + // Add padding if needed + const padding = '='.repeat((4 - (str.length % 4)) % 4); + const base64 = str.replace(/-/g, '+').replace(/_/g, '/') + padding; + + const binary = atob(base64); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; +} diff --git a/typescript/ts_sdk/src/errors/index.ts b/typescript/ts_sdk/src/errors/index.ts new file mode 100644 index 0000000..567805f --- /dev/null +++ b/typescript/ts_sdk/src/errors/index.ts @@ -0,0 +1,79 @@ +/** + * Error classes for ANP SDK + * + * This module defines the error hierarchy for the SDK. + */ + +/** + * Base error class for all ANP SDK errors + */ +export class ANPError extends Error { + constructor( + message: string, + public readonly code: string, + public readonly cause?: Error + ) { + super(message); + this.name = 'ANPError'; + Object.setPrototypeOf(this, ANPError.prototype); + } +} + +/** + * Error thrown when DID resolution fails + */ +export class DIDResolutionError extends ANPError { + constructor(did: string, cause?: Error) { + super(`Failed to resolve DID: ${did}`, 'DID_RESOLUTION_ERROR', cause); + this.name = 'DIDResolutionError'; + Object.setPrototypeOf(this, DIDResolutionError.prototype); + } +} + +/** + * Error thrown when authentication fails + */ +export class AuthenticationError extends ANPError { + constructor(message: string, cause?: Error) { + super(message, 'AUTHENTICATION_ERROR', cause); + this.name = 'AuthenticationError'; + Object.setPrototypeOf(this, AuthenticationError.prototype); + } +} + +/** + * Error thrown when protocol negotiation fails + */ +export class ProtocolNegotiationError extends ANPError { + constructor(message: string, cause?: Error) { + super(message, 'PROTOCOL_NEGOTIATION_ERROR', cause); + this.name = 'ProtocolNegotiationError'; + Object.setPrototypeOf(this, ProtocolNegotiationError.prototype); + } +} + +/** + * Error thrown when network operations fail + */ +export class NetworkError extends ANPError { + constructor( + message: string, + public readonly statusCode?: number, + cause?: Error + ) { + super(message, 'NETWORK_ERROR', cause); + this.name = 'NetworkError'; + Object.setPrototypeOf(this, NetworkError.prototype); + } +} + +/** + * Error thrown when cryptographic operations fail + */ +export class CryptoError extends ANPError { + constructor(message: string, cause?: Error) { + super(message, 'CRYPTO_ERROR', cause); + this.name = 'CryptoError'; + Object.setPrototypeOf(this, CryptoError.prototype); + } +} diff --git a/typescript/ts_sdk/src/index.ts b/typescript/ts_sdk/src/index.ts new file mode 100644 index 0000000..f2358b5 --- /dev/null +++ b/typescript/ts_sdk/src/index.ts @@ -0,0 +1,113 @@ +/** + * ANP TypeScript SDK + * + * Main entry point for the Agent Network Protocol TypeScript SDK + */ + +// SDK version +export const VERSION = '0.1.0'; + +// Export main client +export { ANPClient, type ANPConfig, type RequestOptions } from './anp-client.js'; + +// Export core types +export type { + DIDDocument, + DIDIdentity, + CreateDIDOptions, + ResolveDIDOptions, + VerificationMethod, + ServiceEndpoint, +} from './types/did.js'; + +export type { + AgentDescription, + AgentMetadata, + Information, + Interface, + Organization, + SecurityScheme, + Proof, +} from './types/agent-description.js'; + +export type { + DiscoveryDocument, + AgentDescriptionItem, + SearchQuery, +} from './types/agent-discovery.js'; + +// Export managers for advanced use cases +export { DIDManager, type DIDManagerConfig, type Signature } from './core/did/did-manager.js'; +export { + AuthenticationManager, + type AuthConfig, + type VerificationResult, + type TokenVerificationResult, +} from './core/auth/authentication-manager.js'; +export { AgentDescriptionManager } from './core/agent-description/agent-description-manager.js'; +export { AgentDiscoveryManager } from './core/agent-discovery/agent-discovery-manager.js'; +export { HTTPClient, type HTTPClientConfig } from './transport/http-client.js'; + +// Export protocol types and utilities +export { + createMetaProtocolMachine, + type MetaProtocolConfig, + type MetaProtocolContext, + type MetaProtocolEvent, + type MetaProtocolActor, + createNegotiationMessage, + createCodeGenerationMessage, + createTestCasesMessage, + createFixErrorMessage, + encodeMetaProtocolMessage, + sendNegotiation, + processMessage, +} from './protocol/meta-protocol-machine.js'; + +export { + ProtocolMessageHandler, + ProtocolType, + type ProtocolMessage, + type MetaProtocolMessage, + type ProtocolNegotiationMessage, + type CodeGenerationMessage, + type TestCasesNegotiationMessage, + type FixErrorNegotiationMessage, + type NaturalLanguageNegotiationMessage, +} from './protocol/message-handler.js'; + +// Export crypto utilities +export { + generateKeyPair, + exportPublicKeyJWK, + exportPrivateKeyJWK, + KeyType, +} from './crypto/key-generation.js'; + +export { + sign, + verify, + encodeSignature, + decodeSignature, +} from './crypto/signing.js'; + +export { + performKeyExchange, + deriveKey, +} from './crypto/key-exchange.js'; + +export { + encrypt, + decrypt, + type EncryptedData, +} from './crypto/encryption.js'; + +// Export error classes +export { + ANPError, + DIDResolutionError, + AuthenticationError, + ProtocolNegotiationError, + NetworkError, + CryptoError, +} from './errors/index.js'; diff --git a/typescript/ts_sdk/src/protocol/.gitkeep b/typescript/ts_sdk/src/protocol/.gitkeep new file mode 100644 index 0000000..178c7c3 --- /dev/null +++ b/typescript/ts_sdk/src/protocol/.gitkeep @@ -0,0 +1 @@ +# Protocol modules will be implemented here diff --git a/typescript/ts_sdk/src/protocol/index.ts b/typescript/ts_sdk/src/protocol/index.ts new file mode 100644 index 0000000..544c7c7 --- /dev/null +++ b/typescript/ts_sdk/src/protocol/index.ts @@ -0,0 +1,9 @@ +/** + * Protocol Module + * + * Exports protocol-related functionality including message handling + * and meta-protocol state machine. + */ + +export * from './message-handler'; +export * from './meta-protocol-machine'; diff --git a/typescript/ts_sdk/src/protocol/message-handler.ts b/typescript/ts_sdk/src/protocol/message-handler.ts new file mode 100644 index 0000000..b4bb8ac --- /dev/null +++ b/typescript/ts_sdk/src/protocol/message-handler.ts @@ -0,0 +1,277 @@ +/** + * Protocol Message Handler + * + * Handles encoding and decoding of ANP protocol messages according to the + * meta-protocol specification. + */ + +/** + * Protocol Type enumeration + * Represents the 2-bit protocol type field in the message header + */ +export enum ProtocolType { + META = 0b00, // Meta-protocol for negotiation + APPLICATION = 0b01, // Application protocol for data transmission + NATURAL_LANGUAGE = 0b10, // Natural language protocol + VERIFICATION = 0b11 // Verification protocol for testing +} + +/** + * Decoded protocol message + */ +export interface ProtocolMessage { + protocolType: ProtocolType; + data: Uint8Array; +} + +/** + * Protocol Negotiation Message + */ +export interface ProtocolNegotiationMessage { + action: 'protocolNegotiation'; + sequenceId: number; + candidateProtocols: string; + modificationSummary?: string; + status: 'negotiating' | 'rejected' | 'accepted' | 'timeout'; +} + +/** + * Code Generation Message + */ +export interface CodeGenerationMessage { + action: 'codeGeneration'; + status: 'generated' | 'error'; +} + +/** + * Test Cases Negotiation Message + */ +export interface TestCasesNegotiationMessage { + action: 'testCasesNegotiation'; + testCases: string; + modificationSummary?: string; + status: 'negotiating' | 'rejected' | 'accepted'; +} + +/** + * Fix Error Negotiation Message + */ +export interface FixErrorNegotiationMessage { + action: 'fixErrorNegotiation'; + errorDescription: string; + status: 'negotiating' | 'rejected' | 'accepted'; +} + +/** + * Natural Language Negotiation Message + */ +export interface NaturalLanguageNegotiationMessage { + action: 'naturalLanguageNegotiation'; + type: 'REQUEST' | 'RESPONSE'; + messageId: string; + message: string; +} + +/** + * Union type for all meta-protocol messages + */ +export type MetaProtocolMessage = + | ProtocolNegotiationMessage + | CodeGenerationMessage + | TestCasesNegotiationMessage + | FixErrorNegotiationMessage + | NaturalLanguageNegotiationMessage; + +/** + * Protocol Message Handler + * + * Encodes and decodes protocol messages according to ANP specification: + * - 1 byte header: [PT(2 bits) | Reserved(6 bits)] + * - Variable length protocol data + */ +export class ProtocolMessageHandler { + /** + * Encode a protocol message + * + * Creates a binary message with: + * - Header byte with protocol type in bits 7-6 + * - Reserved bits 5-0 set to 0 + * - Protocol data following the header + * + * @param type - Protocol type (META, APPLICATION, NATURAL_LANGUAGE, or VERIFICATION) + * @param data - Protocol data as Uint8Array + * @returns Encoded message with header and data + */ + encode(type: ProtocolType, data: Uint8Array): Uint8Array { + // Create new array with space for header + data + const encoded = new Uint8Array(1 + data.length); + + // Construct header byte: + // - Protocol type in bits 7-6 (shift left by 6) + // - Reserved bits 5-0 are 0 (default) + const header = (type << 6) & 0b11000000; + encoded[0] = header; + + // Copy protocol data after header + encoded.set(data, 1); + + return encoded; + } + + /** + * Decode a protocol message + * + * Extracts the protocol type and data from an encoded message: + * - Reads protocol type from bits 7-6 of header byte + * - Extracts protocol data from remaining bytes + * + * @param message - Encoded message with header and data + * @returns Decoded protocol message with type and data + * @throws Error if message is invalid or too short + */ + decode(message: Uint8Array): ProtocolMessage { + // Validate input + if (!message || message.length === 0) { + throw new Error('Message is too short: must contain at least a header byte'); + } + + // Extract protocol type from bits 7-6 of header byte + const header = message[0]; + const protocolType = (header >> 6) & 0b11; + + // Extract protocol data (everything after header byte) + const data = message.slice(1); + + return { + protocolType, + data + }; + } + + /** + * Parse meta-protocol message + * + * Parses JSON-encoded meta-protocol messages and validates the structure. + * Supports all meta-protocol message types: + * - protocolNegotiation + * - codeGeneration + * - testCasesNegotiation + * - fixErrorNegotiation + * - naturalLanguageNegotiation + * + * @param data - UTF-8 encoded JSON data + * @returns Parsed meta-protocol message + * @throws Error if JSON is invalid or message structure is incorrect + */ + parseMetaProtocol(data: Uint8Array): MetaProtocolMessage { + // Decode UTF-8 data to string + const jsonString = new TextDecoder().decode(data); + + // Parse JSON + let parsed: any; + try { + parsed = JSON.parse(jsonString); + } catch (error) { + throw new Error(`Failed to parse meta-protocol message: ${error instanceof Error ? error.message : 'Invalid JSON'}`); + } + + // Validate action field exists + if (!parsed.action || typeof parsed.action !== 'string') { + throw new Error('Invalid meta-protocol message: missing action field'); + } + + // Discriminate message type based on action field + switch (parsed.action) { + case 'protocolNegotiation': + return this.validateProtocolNegotiationMessage(parsed); + + case 'codeGeneration': + return this.validateCodeGenerationMessage(parsed); + + case 'testCasesNegotiation': + return this.validateTestCasesNegotiationMessage(parsed); + + case 'fixErrorNegotiation': + return this.validateFixErrorNegotiationMessage(parsed); + + case 'naturalLanguageNegotiation': + return this.validateNaturalLanguageNegotiationMessage(parsed); + + default: + throw new Error(`Unknown meta-protocol action: ${parsed.action}`); + } + } + + /** + * Validate protocol negotiation message structure + */ + private validateProtocolNegotiationMessage(parsed: any): ProtocolNegotiationMessage { + if (typeof parsed.sequenceId !== 'number') { + throw new Error('Invalid protocolNegotiation message: sequenceId must be a number'); + } + if (typeof parsed.candidateProtocols !== 'string') { + throw new Error('Invalid protocolNegotiation message: candidateProtocols must be a string'); + } + if (typeof parsed.status !== 'string') { + throw new Error('Invalid protocolNegotiation message: status must be a string'); + } + + return parsed as ProtocolNegotiationMessage; + } + + /** + * Validate code generation message structure + */ + private validateCodeGenerationMessage(parsed: any): CodeGenerationMessage { + if (typeof parsed.status !== 'string') { + throw new Error('Invalid codeGeneration message: status must be a string'); + } + + return parsed as CodeGenerationMessage; + } + + /** + * Validate test cases negotiation message structure + */ + private validateTestCasesNegotiationMessage(parsed: any): TestCasesNegotiationMessage { + if (typeof parsed.testCases !== 'string') { + throw new Error('Invalid testCasesNegotiation message: testCases must be a string'); + } + if (typeof parsed.status !== 'string') { + throw new Error('Invalid testCasesNegotiation message: status must be a string'); + } + + return parsed as TestCasesNegotiationMessage; + } + + /** + * Validate fix error negotiation message structure + */ + private validateFixErrorNegotiationMessage(parsed: any): FixErrorNegotiationMessage { + if (typeof parsed.errorDescription !== 'string') { + throw new Error('Invalid fixErrorNegotiation message: errorDescription must be a string'); + } + if (typeof parsed.status !== 'string') { + throw new Error('Invalid fixErrorNegotiation message: status must be a string'); + } + + return parsed as FixErrorNegotiationMessage; + } + + /** + * Validate natural language negotiation message structure + */ + private validateNaturalLanguageNegotiationMessage(parsed: any): NaturalLanguageNegotiationMessage { + if (typeof parsed.type !== 'string') { + throw new Error('Invalid naturalLanguageNegotiation message: type must be a string'); + } + if (typeof parsed.messageId !== 'string') { + throw new Error('Invalid naturalLanguageNegotiation message: messageId must be a string'); + } + if (typeof parsed.message !== 'string') { + throw new Error('Invalid naturalLanguageNegotiation message: message must be a string'); + } + + return parsed as NaturalLanguageNegotiationMessage; + } +} diff --git a/typescript/ts_sdk/src/protocol/meta-protocol-machine.ts b/typescript/ts_sdk/src/protocol/meta-protocol-machine.ts new file mode 100644 index 0000000..9587ca5 --- /dev/null +++ b/typescript/ts_sdk/src/protocol/meta-protocol-machine.ts @@ -0,0 +1,416 @@ +import { setup, createActor, type ActorRefFrom } from 'xstate'; +import type { DIDIdentity } from '../types/did'; + +// Context type definition +export interface MetaProtocolContext { + sequenceId: number; + candidateProtocols: string; + agreedProtocol?: string; + testCases?: string; + maxNegotiationRounds: number; + remoteDID: string; + localIdentity: DIDIdentity; + negotiationRound: number; + errors?: string[]; +} + +// Event type definitions +export type MetaProtocolEvent = + | { type: 'initiate'; candidateProtocols: string } + | { type: 'receive_request'; candidateProtocols: string; sequenceId: number } + | { type: 'negotiate'; response: string } + | { type: 'accept'; agreedProtocol: string } + | { type: 'reject'; reason: string } + | { type: 'timeout' } + | { type: 'code_ready'; code: string } + | { type: 'code_error'; error: string } + | { type: 'tests_agreed'; testCases: string } + | { type: 'skip_tests' } + | { type: 'tests_passed' } + | { type: 'tests_failed'; errors: string } + | { type: 'fix_accepted'; fix: string } + | { type: 'fix_rejected'; reason: string } + | { type: 'start_communication' } + | { type: 'protocol_error'; error: string } + | { type: 'end' }; + +// Configuration for creating the machine +export interface MetaProtocolConfig { + localIdentity: DIDIdentity; + remoteDID: string; + maxNegotiationRounds?: number; + timeoutMs?: number; +} + +// Define the state machine +const metaProtocolMachine = setup({ + types: { + context: {} as MetaProtocolContext, + events: {} as MetaProtocolEvent, + input: {} as MetaProtocolConfig, + }, + guards: { + canContinueNegotiation: ({ context }) => { + return context.negotiationRound < context.maxNegotiationRounds; + }, + maxRoundsExceeded: ({ context }) => { + return context.negotiationRound >= context.maxNegotiationRounds; + }, + }, + actions: { + incrementSequenceId: ({ context }) => { + context.sequenceId += 1; + }, + incrementNegotiationRound: ({ context }) => { + context.negotiationRound += 1; + }, + setAgreedProtocol: ({ context, event }) => { + if (event.type === 'accept') { + context.agreedProtocol = event.agreedProtocol; + } + }, + setCandidateProtocols: ({ context, event }) => { + if (event.type === 'initiate') { + context.candidateProtocols = event.candidateProtocols; + } else if (event.type === 'receive_request') { + context.candidateProtocols = event.candidateProtocols; + context.sequenceId = event.sequenceId; + } + }, + setTestCases: ({ context, event }) => { + if (event.type === 'tests_agreed') { + context.testCases = event.testCases; + } + }, + addError: ({ context, event }) => { + if (!context.errors) { + context.errors = []; + } + if (event.type === 'code_error') { + context.errors.push(event.error); + } else if (event.type === 'tests_failed') { + context.errors.push(event.errors); + } else if (event.type === 'protocol_error') { + context.errors.push(event.error); + } + }, + }, +}).createMachine({ + id: 'metaProtocol', + initial: 'Idle', + context: ({ input }: { input: MetaProtocolConfig }) => ({ + sequenceId: 0, + candidateProtocols: '', + agreedProtocol: undefined, + testCases: undefined, + maxNegotiationRounds: input.maxNegotiationRounds ?? 10, + remoteDID: input.remoteDID, + localIdentity: input.localIdentity, + negotiationRound: 0, + errors: undefined, + }), + states: { + Idle: { + on: { + initiate: { + target: 'Negotiating', + actions: ['setCandidateProtocols'], + }, + receive_request: { + target: 'Negotiating', + actions: ['setCandidateProtocols'], + }, + }, + }, + Negotiating: { + on: { + negotiate: [ + { + guard: 'maxRoundsExceeded', + target: 'Rejected', + }, + { + guard: 'canContinueNegotiation', + target: 'Negotiating', + actions: ['incrementNegotiationRound', 'incrementSequenceId'], + }, + ], + accept: { + target: 'CodeGeneration', + actions: ['setAgreedProtocol'], + }, + reject: { + target: 'Rejected', + }, + timeout: { + target: 'Rejected', + }, + }, + }, + CodeGeneration: { + on: { + code_ready: { + target: 'TestCases', + }, + code_error: { + target: 'Failed', + actions: ['addError'], + }, + }, + }, + TestCases: { + on: { + tests_agreed: { + target: 'Testing', + actions: ['setTestCases'], + }, + skip_tests: { + target: 'Ready', + }, + }, + }, + Testing: { + on: { + tests_passed: { + target: 'Ready', + }, + tests_failed: { + target: 'FixError', + actions: ['addError'], + }, + }, + }, + FixError: { + on: { + fix_accepted: { + target: 'CodeGeneration', + }, + fix_rejected: { + target: 'Failed', + }, + }, + }, + Ready: { + on: { + start_communication: { + target: 'Communicating', + }, + }, + }, + Communicating: { + on: { + protocol_error: { + target: 'FixError', + actions: ['addError'], + }, + end: { + target: 'Completed', + }, + }, + }, + Rejected: { + type: 'final', + }, + Failed: { + type: 'final', + }, + Completed: { + type: 'final', + }, + }, +}); + +// Factory function to create state machine actor +export function createMetaProtocolMachine( + config: MetaProtocolConfig +): ActorRefFrom { + const actor = createActor(metaProtocolMachine, { + input: config, + }); + + actor.start(); + + return actor; +} + +// Export the machine type for use in other modules +export type MetaProtocolMachine = typeof metaProtocolMachine; +export type MetaProtocolActor = ActorRefFrom; + + +// Message sending helper functions +import { ProtocolMessageHandler, ProtocolType, type MetaProtocolMessage } from './message-handler'; + +/** + * Create a protocol negotiation message + */ +export function createNegotiationMessage( + sequenceId: number, + candidateProtocols: string, + status: 'negotiating' | 'rejected' | 'accepted' | 'timeout', + modificationSummary?: string +): MetaProtocolMessage { + const message: any = { + action: 'protocolNegotiation', + sequenceId, + candidateProtocols, + status, + }; + + if (modificationSummary) { + message.modificationSummary = modificationSummary; + } + + return message; +} + +/** + * Create a code generation message + */ +export function createCodeGenerationMessage( + status: 'generated' | 'error' +): MetaProtocolMessage { + return { + action: 'codeGeneration', + status, + }; +} + +/** + * Create a test cases negotiation message + */ +export function createTestCasesMessage( + testCases: string, + status: 'negotiating' | 'rejected' | 'accepted', + modificationSummary?: string +): MetaProtocolMessage { + const message: any = { + action: 'testCasesNegotiation', + testCases, + status, + }; + + if (modificationSummary) { + message.modificationSummary = modificationSummary; + } + + return message; +} + +/** + * Create a fix error negotiation message + */ +export function createFixErrorMessage( + errorDescription: string, + status: 'negotiating' | 'rejected' | 'accepted' +): MetaProtocolMessage { + return { + action: 'fixErrorNegotiation', + errorDescription, + status, + }; +} + +/** + * Encode a meta-protocol message for transmission + */ +export function encodeMetaProtocolMessage(message: MetaProtocolMessage): Uint8Array { + const handler = new ProtocolMessageHandler(); + const jsonString = JSON.stringify(message); + const data = new TextEncoder().encode(jsonString); + return handler.encode(ProtocolType.META, data); +} + +/** + * Send a negotiation message + * This is a helper function that can be used with the state machine + */ +export async function sendNegotiation( + actor: MetaProtocolActor, + candidateProtocols: string, + modificationSummary?: string +): Promise { + const snapshot = actor.getSnapshot(); + const message = createNegotiationMessage( + snapshot.context.sequenceId, + candidateProtocols, + 'negotiating', + modificationSummary + ); + + return encodeMetaProtocolMessage(message); +} + + +/** + * Process a received meta-protocol message and dispatch appropriate event to state machine + */ +export function processMessage( + actor: MetaProtocolActor, + encodedMessage: Uint8Array +): void { + const handler = new ProtocolMessageHandler(); + + // Decode the message + const decoded = handler.decode(encodedMessage); + + // Verify it's a meta-protocol message + if (decoded.protocolType !== ProtocolType.META) { + throw new Error(`Expected META protocol type, got ${decoded.protocolType}`); + } + + // Parse the meta-protocol message + const message = handler.parseMetaProtocol(decoded.data); + + // Map message to state machine event + switch (message.action) { + case 'protocolNegotiation': + if (message.status === 'negotiating') { + actor.send({ + type: 'receive_request', + candidateProtocols: message.candidateProtocols, + sequenceId: message.sequenceId, + }); + } else if (message.status === 'accepted') { + actor.send({ + type: 'accept', + agreedProtocol: message.candidateProtocols, + }); + } else if (message.status === 'rejected') { + actor.send({ + type: 'reject', + reason: 'Remote agent rejected negotiation', + }); + } else if (message.status === 'timeout') { + actor.send({ type: 'timeout' }); + } + break; + + case 'codeGeneration': + if (message.status === 'generated') { + actor.send({ type: 'code_ready', code: 'generated' }); + } else if (message.status === 'error') { + actor.send({ type: 'code_error', error: 'Code generation failed' }); + } + break; + + case 'testCasesNegotiation': + if (message.status === 'accepted') { + actor.send({ type: 'tests_agreed', testCases: message.testCases }); + } else if (message.status === 'rejected') { + actor.send({ type: 'skip_tests' }); + } + break; + + case 'fixErrorNegotiation': + if (message.status === 'accepted') { + actor.send({ type: 'fix_accepted', fix: 'fix applied' }); + } else if (message.status === 'rejected') { + actor.send({ type: 'fix_rejected', reason: 'Fix rejected' }); + } + break; + + default: + throw new Error(`Unhandled meta-protocol action: ${(message as any).action}`); + } +} diff --git a/typescript/ts_sdk/src/transport/.gitkeep b/typescript/ts_sdk/src/transport/.gitkeep new file mode 100644 index 0000000..0d79b86 --- /dev/null +++ b/typescript/ts_sdk/src/transport/.gitkeep @@ -0,0 +1 @@ +# Transport modules will be implemented here diff --git a/typescript/ts_sdk/src/transport/http-client.ts b/typescript/ts_sdk/src/transport/http-client.ts new file mode 100644 index 0000000..2c136d7 --- /dev/null +++ b/typescript/ts_sdk/src/transport/http-client.ts @@ -0,0 +1,251 @@ +/** + * HTTP Client for making authenticated requests + */ + +import { AuthenticationManager } from '../core/auth/authentication-manager.js'; +import { NetworkError } from '../errors/index.js'; +import type { DIDIdentity } from '../types/index.js'; + +/** + * Configuration for HTTP Client + */ +export interface HTTPClientConfig { + timeout: number; // milliseconds + maxRetries: number; + retryDelay: number; // milliseconds +} + +/** + * Request options + */ +export interface RequestOptions { + method?: string; + headers?: Record; + body?: string; +} + +/** + * HTTP Client class + */ +export class HTTPClient { + private readonly authManager: AuthenticationManager; + private readonly config: HTTPClientConfig; + + constructor(authManager: AuthenticationManager, config: HTTPClientConfig) { + this.authManager = authManager; + this.config = config; + } + + /** + * Make an HTTP request with optional authentication + * + * @param url - The URL to request + * @param options - Request options + * @param identity - Optional DID identity for authentication + * @returns Response object + */ + async request( + url: string, + options: RequestOptions, + identity?: DIDIdentity + ): Promise { + const headers: Record = { + ...options.headers, + }; + + // Add authentication header if identity is provided + if (identity) { + const domain = this.extractDomain(url); + const verificationMethodId = `${identity.did}#auth-key`; + + const authHeader = await this.authManager.generateAuthHeader( + identity, + domain, + verificationMethodId + ); + + headers['Authorization'] = authHeader; + } + + // Add Content-Type for POST/PUT requests with body + if ( + options.body && + (options.method === 'POST' || options.method === 'PUT') && + !headers['Content-Type'] + ) { + headers['Content-Type'] = 'application/json'; + } + + // Create abort controller for timeout + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.config.timeout); + + try { + const response = await this.retryWithBackoff(async () => { + try { + const fetchResponse = await fetch(url, { + method: options.method || 'GET', + headers, + body: options.body, + signal: controller.signal, + }); + + // Check if response is ok + if (!fetchResponse.ok) { + // Retry on 5xx errors, fail immediately on 4xx + if (fetchResponse.status >= 500) { + throw new NetworkError( + `HTTP ${fetchResponse.status}: ${fetchResponse.statusText}`, + fetchResponse.status + ); + } else { + // Don't retry 4xx errors + throw new NetworkError( + `HTTP ${fetchResponse.status}: ${fetchResponse.statusText}`, + fetchResponse.status, + new Error('Client error - no retry') + ); + } + } + + return fetchResponse; + } catch (error) { + // Handle abort/timeout errors + if ((error as Error).name === 'AbortError') { + throw new NetworkError('Request timeout', undefined, error as Error); + } + + // Handle network errors + if (error instanceof NetworkError) { + // Check if it's a 4xx error (don't retry) + if ( + error.statusCode && + error.statusCode >= 400 && + error.statusCode < 500 + ) { + // Mark as non-retryable by re-throwing with special marker + const nonRetryable = new NetworkError( + error.message, + error.statusCode, + new Error('Client error - no retry') + ); + throw nonRetryable; + } + throw error; + } + + // Wrap other errors as NetworkError + throw new NetworkError( + `Network request failed: ${(error as Error).message}`, + undefined, + error as Error + ); + } + }, this.config.maxRetries); + + return response; + } finally { + clearTimeout(timeoutId); + } + } + + /** + * Make a GET request + * + * @param url - The URL to request + * @param identity - Optional DID identity for authentication + * @returns Response object + */ + async get(url: string, identity?: DIDIdentity): Promise { + return this.request(url, { method: 'GET' }, identity); + } + + /** + * Make a POST request + * + * @param url - The URL to request + * @param body - Request body (will be JSON stringified) + * @param identity - Optional DID identity for authentication + * @returns Response object + */ + async post( + url: string, + body: any, + identity?: DIDIdentity + ): Promise { + return this.request( + url, + { + method: 'POST', + body: JSON.stringify(body), + }, + identity + ); + } + + /** + * Extract domain from URL for authentication + * + * @param url - The URL to extract domain from + * @returns The domain (without port) + */ + private extractDomain(url: string): string { + try { + const urlObj = new URL(url); + return urlObj.hostname; + } catch (error) { + throw new NetworkError( + `Invalid URL: ${url}`, + undefined, + error as Error + ); + } + } + + /** + * Retry a function with exponential backoff + * + * @param fn - The function to retry + * @param maxRetries - Maximum number of retries + * @returns The result of the function + */ + private async retryWithBackoff( + fn: () => Promise, + maxRetries: number + ): Promise { + let lastError: Error | undefined; + let attempt = 0; + + while (attempt <= maxRetries) { + try { + return await fn(); + } catch (error) { + lastError = error as Error; + + // Don't retry if it's a client error (4xx) + if ( + error instanceof NetworkError && + error.cause?.message === 'Client error - no retry' + ) { + throw error; + } + + // If we've exhausted retries, throw the error + if (attempt >= maxRetries) { + throw error; + } + + // Calculate exponential backoff delay + const delay = this.config.retryDelay * Math.pow(2, attempt); + + // Wait before retrying + await new Promise((resolve) => setTimeout(resolve, delay)); + + attempt++; + } + } + + // This should never be reached, but TypeScript needs it + throw lastError || new Error('Retry failed'); + } +} diff --git a/typescript/ts_sdk/src/transport/index.ts b/typescript/ts_sdk/src/transport/index.ts new file mode 100644 index 0000000..f317c7d --- /dev/null +++ b/typescript/ts_sdk/src/transport/index.ts @@ -0,0 +1,5 @@ +/** + * Transport layer exports + */ + +export * from './http-client.js'; diff --git a/typescript/ts_sdk/src/types/agent-description.ts b/typescript/ts_sdk/src/types/agent-description.ts new file mode 100644 index 0000000..1268f38 --- /dev/null +++ b/typescript/ts_sdk/src/types/agent-description.ts @@ -0,0 +1,86 @@ +/** + * Type definitions for Agent Description Protocol (ADP) + */ + +/** + * Organization information + */ +export interface Organization { + name: string; + url?: string; + email?: string; +} + +/** + * Security scheme definition + */ +export interface SecurityScheme { + scheme: string; + type?: string; + description?: string; +} + +/** + * Information resource + */ +export interface Information { + type: string; + description: string; + url: string; +} + +/** + * Interface definition + */ +export interface Interface { + type: string; + protocol: string; + version: string; + url: string; + description?: string; +} + +/** + * Proof object for digital signatures + */ +export interface Proof { + type: string; + created: string; + verificationMethod: string; + proofPurpose: string; + challenge?: string; + domain?: string; + proofValue: string; +} + +/** + * Agent Description document + */ +export interface AgentDescription { + protocolType: 'ANP'; + protocolVersion: string; + type: 'AgentDescription'; + url?: string; + name: string; + did?: string; + owner?: Organization; + description?: string; + created?: string; + securityDefinitions: Record; + security: string; + Infomations?: Information[]; + interfaces?: Interface[]; + proof?: Proof; +} + +/** + * Metadata for creating an agent description + */ +export interface AgentMetadata { + name: string; + did?: string; + owner?: Organization; + description?: string; + url?: string; + protocolVersion?: string; +} diff --git a/typescript/ts_sdk/src/types/agent-discovery.ts b/typescript/ts_sdk/src/types/agent-discovery.ts new file mode 100644 index 0000000..b57b6dd --- /dev/null +++ b/typescript/ts_sdk/src/types/agent-discovery.ts @@ -0,0 +1,42 @@ +/** + * Type definitions for Agent Discovery Service Protocol (ADSP) + */ + +/** + * Agent description item in discovery document + */ +export interface AgentDescriptionItem { + '@type': 'ad:AgentDescription'; + name: string; + '@id': string; // URL to agent description +} + +/** + * Discovery document (CollectionPage) + */ +export interface DiscoveryDocument { + '@context': Record; + '@type': 'CollectionPage'; + url: string; + items: AgentDescriptionItem[]; + next?: string; // URL to next page +} + +/** + * Search query for agent discovery + */ +export interface SearchQuery { + keywords?: string[]; + capabilities?: string[]; + limit?: number; + offset?: number; +} + +/** + * Search result from discovery service + */ +export interface SearchResult { + items: AgentDescriptionItem[]; + total?: number; + hasMore?: boolean; +} diff --git a/typescript/ts_sdk/src/types/did.ts b/typescript/ts_sdk/src/types/did.ts new file mode 100644 index 0000000..ebab6ce --- /dev/null +++ b/typescript/ts_sdk/src/types/did.ts @@ -0,0 +1,70 @@ +/** + * Type definitions for DID (Decentralized Identifier) functionality + */ + +/** + * Verification method for DID documents + */ +export interface VerificationMethod { + id: string; + type: string; + controller: string; + publicKeyJwk?: JsonWebKey; + publicKeyMultibase?: string; +} + +/** + * Service endpoint for DID documents + */ +export interface ServiceEndpoint { + id: string; + type: string; + serviceEndpoint: string; +} + +/** + * DID Document structure following W3C DID specification + */ +export interface DIDDocument { + '@context': string[]; + id: string; + verificationMethod: VerificationMethod[]; + authentication: (string | VerificationMethod)[]; + keyAgreement?: (string | VerificationMethod)[]; + humanAuthorization?: (string | VerificationMethod)[]; + service?: ServiceEndpoint[]; +} + +/** + * Key metadata for tracking key types + */ +export interface KeyMetadata { + key: CryptoKey; + type: string; // KeyType enum value as string +} + +/** + * DID Identity containing the DID, document, and private keys + */ +export interface DIDIdentity { + did: string; + document: DIDDocument; + privateKeys: Map; +} + +/** + * Options for creating a DID + */ +export interface CreateDIDOptions { + domain: string; + path?: string; + port?: number; +} + +/** + * Options for resolving a DID + */ +export interface ResolveDIDOptions { + cache?: boolean; + cacheTTL?: number; +} diff --git a/typescript/ts_sdk/src/types/index.ts b/typescript/ts_sdk/src/types/index.ts new file mode 100644 index 0000000..4d088f1 --- /dev/null +++ b/typescript/ts_sdk/src/types/index.ts @@ -0,0 +1,9 @@ +/** + * Type definitions for ANP SDK + * + * This module exports all TypeScript type definitions used throughout the SDK. + */ + +export * from './did.js'; +export * from './agent-description.js'; +export * from './agent-discovery.js'; diff --git a/typescript/ts_sdk/tests/integration/.gitkeep b/typescript/ts_sdk/tests/integration/.gitkeep new file mode 100644 index 0000000..12ccdfa --- /dev/null +++ b/typescript/ts_sdk/tests/integration/.gitkeep @@ -0,0 +1 @@ +# Integration tests will be added here diff --git a/typescript/ts_sdk/tests/integration/agent-discovery.test.ts b/typescript/ts_sdk/tests/integration/agent-discovery.test.ts new file mode 100644 index 0000000..dfb1702 --- /dev/null +++ b/typescript/ts_sdk/tests/integration/agent-discovery.test.ts @@ -0,0 +1,484 @@ +/** + * Integration test for agent discovery flow + */ + +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import { DIDManager } from '../../src/core/did/did-manager.js'; +import { AuthenticationManager } from '../../src/core/auth/authentication-manager.js'; +import { AgentDescriptionManager } from '../../src/core/agent-description/agent-description-manager.js'; +import { AgentDiscoveryManager } from '../../src/core/agent-discovery/agent-discovery-manager.js'; +import { HTTPClient } from '../../src/transport/http-client.js'; +import type { + DIDIdentity, + AgentDescription, + DiscoveryDocument, + AgentDescriptionItem, +} from '../../src/types/index.js'; + +describe('Agent Discovery Integration', () => { + let didManager: DIDManager; + let authManager: AuthenticationManager; + let descriptionManager: AgentDescriptionManager; + let httpClient: HTTPClient; + let discoveryManager: AgentDiscoveryManager; + let agentIdentity: DIDIdentity; + let agentDescription: AgentDescription; + + beforeEach(async () => { + // Initialize managers + didManager = new DIDManager(); + authManager = new AuthenticationManager(didManager, { + maxTokenAge: 3600000, + nonceLength: 32, + clockSkewTolerance: 60, + }); + descriptionManager = new AgentDescriptionManager(); + httpClient = new HTTPClient(authManager, { + timeout: 10000, + maxRetries: 3, + retryDelay: 1000, + }); + discoveryManager = new AgentDiscoveryManager(httpClient); + + // Create agent identity + agentIdentity = await didManager.createDID({ + domain: 'myagent.example.com', + path: 'agent1', + }); + + // Create agent description + agentDescription = descriptionManager.createDescription({ + name: 'My Test Agent', + description: 'A test agent for integration testing', + did: agentIdentity.did, + protocolVersion: '1.0.0', + owner: { + name: 'Test Organization', + url: 'https://example.com', + }, + }); + + // Add information resources + agentDescription = descriptionManager.addInformation(agentDescription, { + type: 'documentation', + description: 'Agent API documentation', + url: 'https://myagent.example.com/docs', + }); + + // Add interfaces + agentDescription = descriptionManager.addInterface(agentDescription, { + type: 'api', + protocol: 'REST', + version: '1.0', + url: 'https://myagent.example.com/api/v1', + }); + + // Sign the description + agentDescription = await descriptionManager.signDescription( + agentDescription, + agentIdentity, + 'test-challenge', + 'myagent.example.com' + ); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('should complete full agent discovery flow', async () => { + // Step 1: Mock publishing agent description to a server + const publishUrl = 'https://myagent.example.com/agent-description.json'; + + // Mock fetch for publishing (in real scenario, this would be an actual HTTP POST) + const mockPublishResponse = new Response( + JSON.stringify({ success: true, url: publishUrl }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + } + ); + + // Step 2: Mock discovery document from domain + const mockDiscoveryDoc: DiscoveryDocument = { + '@context': { + ad: 'https://anp.org/agent-description/', + }, + '@type': 'CollectionPage', + url: 'https://example.com/.well-known/agent-descriptions', + items: [ + { + '@type': 'ad:AgentDescription', + name: 'My Test Agent', + '@id': publishUrl, + }, + { + '@type': 'ad:AgentDescription', + name: 'Another Agent', + '@id': 'https://another.example.com/agent-description.json', + }, + ], + }; + + // Mock fetch for discovery + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(mockDiscoveryDoc), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ); + + // Step 3: Discover agents from domain + const discoveredAgents = await discoveryManager.discoverAgents( + 'example.com' + ); + + expect(discoveredAgents).toHaveLength(2); + expect(discoveredAgents[0].name).toBe('My Test Agent'); + expect(discoveredAgents[0]['@id']).toBe(publishUrl); + expect(discoveredAgents[1].name).toBe('Another Agent'); + + // Verify correct URL was called + expect(global.fetch).toHaveBeenCalledWith( + 'https://example.com/.well-known/agent-descriptions', + expect.any(Object) + ); + }); + + it('should handle paginated discovery results', async () => { + // Mock first page + const page1: DiscoveryDocument = { + '@context': { ad: 'https://anp.org/agent-description/' }, + '@type': 'CollectionPage', + url: 'https://example.com/.well-known/agent-descriptions', + items: [ + { + '@type': 'ad:AgentDescription', + name: 'Agent 1', + '@id': 'https://example.com/agent1.json', + }, + { + '@type': 'ad:AgentDescription', + name: 'Agent 2', + '@id': 'https://example.com/agent2.json', + }, + ], + next: 'https://example.com/.well-known/agent-descriptions?page=2', + }; + + // Mock second page + const page2: DiscoveryDocument = { + '@context': { ad: 'https://anp.org/agent-description/' }, + '@type': 'CollectionPage', + url: 'https://example.com/.well-known/agent-descriptions?page=2', + items: [ + { + '@type': 'ad:AgentDescription', + name: 'Agent 3', + '@id': 'https://example.com/agent3.json', + }, + ], + }; + + // Mock fetch to return different pages + global.fetch = vi + .fn() + .mockResolvedValueOnce( + new Response(JSON.stringify(page1), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ) + .mockResolvedValueOnce( + new Response(JSON.stringify(page2), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ); + + // Discover agents + const discoveredAgents = await discoveryManager.discoverAgents( + 'example.com' + ); + + // Should have all agents from both pages + expect(discoveredAgents).toHaveLength(3); + expect(discoveredAgents[0].name).toBe('Agent 1'); + expect(discoveredAgents[1].name).toBe('Agent 2'); + expect(discoveredAgents[2].name).toBe('Agent 3'); + + // Verify both pages were fetched + expect(global.fetch).toHaveBeenCalledTimes(2); + }); + + it('should register with search service', async () => { + const searchServiceUrl = 'https://search.example.com/register'; + const agentDescriptionUrl = + 'https://myagent.example.com/agent-description.json'; + + // Mock successful registration + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ); + + // Register with search service + await discoveryManager.registerWithSearchService( + searchServiceUrl, + agentDescriptionUrl, + agentIdentity + ); + + // Verify registration request was made + expect(global.fetch).toHaveBeenCalled(); + + // Get the call arguments + const callArgs = (global.fetch as any).mock.calls[0]; + expect(callArgs[0]).toBe(searchServiceUrl); + + // Verify request includes authentication header + const requestInit = callArgs[1]; + expect(requestInit.headers.Authorization).toBeDefined(); + expect(requestInit.headers.Authorization).toMatch(/^DIDWba /); + }); + + it('should search for agents', async () => { + const searchServiceUrl = 'https://search.example.com/search'; + const searchQuery = { + keywords: ['weather', 'forecast'], + capabilities: ['temperature', 'precipitation'], + }; + + // Mock search results + const searchResults = { + items: [ + { + '@type': 'ad:AgentDescription', + name: 'Weather Agent', + '@id': 'https://weather.example.com/agent-description.json', + }, + { + '@type': 'ad:AgentDescription', + name: 'Climate Agent', + '@id': 'https://climate.example.com/agent-description.json', + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(searchResults), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ); + + // Search for agents + const results = await discoveryManager.searchAgents( + searchServiceUrl, + searchQuery + ); + + expect(results).toHaveLength(2); + expect(results[0].name).toBe('Weather Agent'); + expect(results[1].name).toBe('Climate Agent'); + + // Verify search request was made + expect(global.fetch).toHaveBeenCalledWith( + searchServiceUrl, + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(searchQuery), + }) + ); + }); + + it('should fetch and verify agent description', async () => { + const descriptionUrl = + 'https://myagent.example.com/agent-description.json'; + + // Mock fetch for agent description + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(agentDescription), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ); + + // Fetch the description + const fetchedDescription = + await descriptionManager.fetchDescription(descriptionUrl); + + expect(fetchedDescription).toBeDefined(); + expect(fetchedDescription.name).toBe('My Test Agent'); + expect(fetchedDescription.did).toBe(agentIdentity.did); + expect(fetchedDescription.proof).toBeDefined(); + + // Verify the signature + const isValid = await descriptionManager.verifyDescription( + fetchedDescription, + didManager, + agentIdentity.document + ); + + expect(isValid).toBe(true); + }); + + it('should handle discovery errors gracefully', async () => { + // Mock 404 response + global.fetch = vi.fn().mockResolvedValue( + new Response('Not Found', { + status: 404, + statusText: 'Not Found', + }) + ); + + // Discovery should throw error + await expect( + discoveryManager.discoverAgents('nonexistent.example.com') + ).rejects.toThrow(); + }); + + it('should validate discovery document structure', async () => { + // Mock invalid discovery document (missing required fields) + const invalidDoc = { + '@context': { ad: 'https://anp.org/agent-description/' }, + // Missing @type + url: 'https://example.com/.well-known/agent-descriptions', + items: [], + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(invalidDoc), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ); + + // Should throw validation error + await expect( + discoveryManager.discoverAgents('example.com') + ).rejects.toThrow(); + }); + + it('should support authenticated discovery', async () => { + const mockDiscoveryDoc: DiscoveryDocument = { + '@context': { ad: 'https://anp.org/agent-description/' }, + '@type': 'CollectionPage', + url: 'https://private.example.com/.well-known/agent-descriptions', + items: [ + { + '@type': 'ad:AgentDescription', + name: 'Private Agent', + '@id': 'https://private.example.com/agent.json', + }, + ], + }; + + global.fetch = vi.fn().mockResolvedValue( + new Response(JSON.stringify(mockDiscoveryDoc), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ); + + // Discover with authentication + const discoveredAgents = await discoveryManager.discoverAgents( + 'private.example.com', + agentIdentity + ); + + expect(discoveredAgents).toHaveLength(1); + expect(discoveredAgents[0].name).toBe('Private Agent'); + + // Verify authentication header was included + const callArgs = (global.fetch as any).mock.calls[0]; + const requestInit = callArgs[1]; + expect(requestInit.headers.Authorization).toBeDefined(); + }); + + it('should handle complete discovery and verification workflow', async () => { + // Step 1: Create and sign agent description + const myDescription = descriptionManager.createDescription({ + name: 'Workflow Test Agent', + description: 'Testing complete workflow', + did: agentIdentity.did, + protocolVersion: '1.0.0', + }); + + const signedDescription = await descriptionManager.signDescription( + myDescription, + agentIdentity, + 'workflow-challenge', + 'workflow.example.com' + ); + + // Step 2: Mock discovery that returns this agent + const discoveryDoc: DiscoveryDocument = { + '@context': { ad: 'https://anp.org/agent-description/' }, + '@type': 'CollectionPage', + url: 'https://workflow.example.com/.well-known/agent-descriptions', + items: [ + { + '@type': 'ad:AgentDescription', + name: 'Workflow Test Agent', + '@id': 'https://workflow.example.com/agent.json', + }, + ], + }; + + global.fetch = vi + .fn() + // First call: discovery + .mockResolvedValueOnce( + new Response(JSON.stringify(discoveryDoc), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ) + // Second call: fetch agent description + .mockResolvedValueOnce( + new Response(JSON.stringify(signedDescription), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ); + + // Step 3: Discover agents + const discovered = await discoveryManager.discoverAgents( + 'workflow.example.com' + ); + + expect(discovered).toHaveLength(1); + expect(discovered[0].name).toBe('Workflow Test Agent'); + + // Step 4: Fetch full description + const fullDescription = await descriptionManager.fetchDescription( + discovered[0]['@id'] + ); + + expect(fullDescription.name).toBe('Workflow Test Agent'); + expect(fullDescription.proof).toBeDefined(); + + // Step 5: Verify signature + const isValid = await descriptionManager.verifyDescription( + fullDescription, + didManager, + agentIdentity.document + ); + + expect(isValid).toBe(true); + + // Step 6: Verify domain + const isDomainValid = + await descriptionManager.verifyDescriptionWithDomain( + fullDescription, + didManager, + 'workflow.example.com', + agentIdentity.document + ); + + expect(isDomainValid).toBe(true); + }); +}); diff --git a/typescript/ts_sdk/tests/integration/authentication.test.ts b/typescript/ts_sdk/tests/integration/authentication.test.ts new file mode 100644 index 0000000..005aab8 --- /dev/null +++ b/typescript/ts_sdk/tests/integration/authentication.test.ts @@ -0,0 +1,318 @@ +/** + * Integration test for end-to-end authentication flow + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { DIDManager } from '../../src/core/did/did-manager.js'; +import { AuthenticationManager } from '../../src/core/auth/authentication-manager.js'; +import type { DIDIdentity, DIDDocument } from '../../src/types/index.js'; + +describe('End-to-End Authentication Integration', () => { + let didManager: DIDManager; + let authManager: AuthenticationManager; + let aliceIdentity: DIDIdentity; + let bobIdentity: DIDIdentity; + + beforeEach(async () => { + // Initialize managers + didManager = new DIDManager(); + authManager = new AuthenticationManager(didManager, { + maxTokenAge: 3600000, // 1 hour + nonceLength: 32, + clockSkewTolerance: 60, // 60 seconds + }); + + // Create two DID identities + aliceIdentity = await didManager.createDID({ + domain: 'alice.example.com', + path: 'agent', + }); + + bobIdentity = await didManager.createDID({ + domain: 'bob.example.com', + path: 'service', + }); + }); + + it('should complete full authentication flow between two agents', async () => { + // Step 1: Alice generates auth header to access Bob's service + const bobDomain = 'bob.example.com'; + const aliceVerificationMethod = `${aliceIdentity.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + aliceIdentity, + bobDomain, + aliceVerificationMethod + ); + + expect(authHeader).toBeTruthy(); + expect(authHeader).toMatch(/^DIDWba /); + + // Step 2: Bob receives the request and verifies Alice's authentication + // Mock DID resolution to return Alice's document + vi.spyOn(didManager, 'resolveDID').mockResolvedValue( + aliceIdentity.document + ); + + const verificationResult = await authManager.verifyAuthHeader( + authHeader, + bobDomain + ); + + expect(verificationResult.success).toBe(true); + expect(verificationResult.did).toBe(aliceIdentity.did); + expect(verificationResult.error).toBeUndefined(); + + // Step 3: Bob generates access token for Alice + const accessToken = authManager.generateAccessToken( + aliceIdentity.did, + 3600000 + ); + + expect(accessToken).toBeTruthy(); + expect(typeof accessToken).toBe('string'); + + // Step 4: Alice makes subsequent request with access token + const tokenVerification = authManager.verifyAccessToken(accessToken); + + expect(tokenVerification.valid).toBe(true); + expect(tokenVerification.did).toBe(aliceIdentity.did); + expect(tokenVerification.expiresAt).toBeGreaterThan(Date.now()); + }); + + it('should enforce access control with token validation', async () => { + // Alice authenticates with Bob + const bobDomain = 'bob.example.com'; + const aliceVerificationMethod = `${aliceIdentity.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + aliceIdentity, + bobDomain, + aliceVerificationMethod + ); + + vi.spyOn(didManager, 'resolveDID').mockResolvedValue( + aliceIdentity.document + ); + + const verificationResult = await authManager.verifyAuthHeader( + authHeader, + bobDomain + ); + + expect(verificationResult.success).toBe(true); + + // Bob generates token for Alice + const aliceToken = authManager.generateAccessToken( + aliceIdentity.did, + 3600000 + ); + + // Verify Alice's token is valid + const aliceTokenVerification = authManager.verifyAccessToken(aliceToken); + expect(aliceTokenVerification.valid).toBe(true); + expect(aliceTokenVerification.did).toBe(aliceIdentity.did); + + // Try to use a token for a different DID (should fail) + const bobToken = authManager.generateAccessToken(bobIdentity.did, 3600000); + const bobTokenVerification = authManager.verifyAccessToken(bobToken); + + expect(bobTokenVerification.valid).toBe(true); + expect(bobTokenVerification.did).toBe(bobIdentity.did); + expect(bobTokenVerification.did).not.toBe(aliceIdentity.did); + }); + + it('should reject expired tokens', async () => { + // Generate token with very short expiration + const shortLivedToken = authManager.generateAccessToken( + aliceIdentity.did, + -1000 // Already expired + ); + + const verification = authManager.verifyAccessToken(shortLivedToken); + + expect(verification.valid).toBe(false); + expect(verification.error).toBeDefined(); + expect(verification.error?.toLowerCase()).toContain('expired'); + }); + + it('should prevent replay attacks with nonce validation', async () => { + const bobDomain = 'bob.example.com'; + const aliceVerificationMethod = `${aliceIdentity.did}#auth-key`; + + // Generate auth header + const authHeader = await authManager.generateAuthHeader( + aliceIdentity, + bobDomain, + aliceVerificationMethod + ); + + vi.spyOn(didManager, 'resolveDID').mockResolvedValue( + aliceIdentity.document + ); + + // First verification should succeed + const firstVerification = await authManager.verifyAuthHeader( + authHeader, + bobDomain + ); + + expect(firstVerification.success).toBe(true); + + // Second verification with same header should fail (replay attack) + const secondVerification = await authManager.verifyAuthHeader( + authHeader, + bobDomain + ); + + expect(secondVerification.success).toBe(false); + expect(secondVerification.error?.toLowerCase()).toContain('nonce'); + }); + + it('should handle cross-domain authentication', async () => { + // Alice authenticates with Bob's service + const bobDomain = 'bob.example.com'; + const aliceVerificationMethod = `${aliceIdentity.did}#auth-key`; + + const authHeaderForBob = await authManager.generateAuthHeader( + aliceIdentity, + bobDomain, + aliceVerificationMethod + ); + + vi.spyOn(didManager, 'resolveDID').mockResolvedValue( + aliceIdentity.document + ); + + const bobVerification = await authManager.verifyAuthHeader( + authHeaderForBob, + bobDomain + ); + + expect(bobVerification.success).toBe(true); + expect(bobVerification.did).toBe(aliceIdentity.did); + + // Alice authenticates with a different service + const charlieManager = new AuthenticationManager(didManager, { + maxTokenAge: 3600000, + nonceLength: 32, + clockSkewTolerance: 60, + }); + + const charlieDomain = 'charlie.example.com'; + const authHeaderForCharlie = await charlieManager.generateAuthHeader( + aliceIdentity, + charlieDomain, + aliceVerificationMethod + ); + + const charlieVerification = await charlieManager.verifyAuthHeader( + authHeaderForCharlie, + charlieDomain + ); + + expect(charlieVerification.success).toBe(true); + expect(charlieVerification.did).toBe(aliceIdentity.did); + }); + + it('should validate signature integrity', async () => { + const bobDomain = 'bob.example.com'; + const aliceVerificationMethod = `${aliceIdentity.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + aliceIdentity, + bobDomain, + aliceVerificationMethod + ); + + // Tamper with the signature + const tamperedHeader = authHeader.replace( + /signature="[^"]+"/, + 'signature="tampered_signature_value"' + ); + + vi.spyOn(didManager, 'resolveDID').mockResolvedValue( + aliceIdentity.document + ); + + const verification = await authManager.verifyAuthHeader( + tamperedHeader, + bobDomain + ); + + expect(verification.success).toBe(false); + expect(verification.error).toBeDefined(); + }); + + it('should handle DID resolution failures gracefully', async () => { + const bobDomain = 'bob.example.com'; + const aliceVerificationMethod = `${aliceIdentity.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + aliceIdentity, + bobDomain, + aliceVerificationMethod + ); + + // Mock DID resolution to fail + vi.spyOn(didManager, 'resolveDID').mockRejectedValue( + new Error('Network error') + ); + + const verification = await authManager.verifyAuthHeader( + authHeader, + bobDomain + ); + + expect(verification.success).toBe(false); + expect(verification.error).toBeDefined(); + expect(verification.error).toContain('DID resolution'); + }); + + it('should support multiple concurrent authentication sessions', async () => { + const bobDomain = 'bob.example.com'; + const aliceVerificationMethod = `${aliceIdentity.did}#auth-key`; + + // Create multiple auth headers + const authHeader1 = await authManager.generateAuthHeader( + aliceIdentity, + bobDomain, + aliceVerificationMethod + ); + + const authHeader2 = await authManager.generateAuthHeader( + aliceIdentity, + bobDomain, + aliceVerificationMethod + ); + + vi.spyOn(didManager, 'resolveDID').mockResolvedValue( + aliceIdentity.document + ); + + // Both should verify successfully (different nonces) + const verification1 = await authManager.verifyAuthHeader( + authHeader1, + bobDomain + ); + + const verification2 = await authManager.verifyAuthHeader( + authHeader2, + bobDomain + ); + + expect(verification1.success).toBe(true); + expect(verification2.success).toBe(true); + + // Generate tokens for both sessions + const token1 = authManager.generateAccessToken(aliceIdentity.did, 3600000); + const token2 = authManager.generateAccessToken(aliceIdentity.did, 3600000); + + // Both tokens should be valid + const tokenVerification1 = authManager.verifyAccessToken(token1); + const tokenVerification2 = authManager.verifyAccessToken(token2); + + expect(tokenVerification1.valid).toBe(true); + expect(tokenVerification2.valid).toBe(true); + }); +}); diff --git a/typescript/ts_sdk/tests/integration/encrypted-communication.test.ts b/typescript/ts_sdk/tests/integration/encrypted-communication.test.ts new file mode 100644 index 0000000..8887600 --- /dev/null +++ b/typescript/ts_sdk/tests/integration/encrypted-communication.test.ts @@ -0,0 +1,528 @@ +/** + * Integration test for encrypted communication flow + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { DIDManager } from '../../src/core/did/did-manager.js'; +import { + performKeyExchange, + deriveKey, + validateSharedSecret, +} from '../../src/crypto/key-exchange.js'; +import { encrypt, decrypt, generateIV } from '../../src/crypto/encryption.js'; +import type { DIDIdentity } from '../../src/types/index.js'; + +describe('Encrypted Communication Integration', () => { + let didManager: DIDManager; + let aliceIdentity: DIDIdentity; + let bobIdentity: DIDIdentity; + + beforeEach(async () => { + // Initialize DID manager + didManager = new DIDManager(); + + // Create two agent identities with keyAgreement keys + aliceIdentity = await didManager.createDID({ + domain: 'alice.example.com', + path: 'agent', + }); + + bobIdentity = await didManager.createDID({ + domain: 'bob.example.com', + path: 'agent', + }); + }); + + it('should establish encrypted channel and exchange messages', async () => { + // Step 1: Extract key agreement keys from DID documents + const aliceKeyAgreementMethod = aliceIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + ); + const bobKeyAgreementMethod = bobIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + ); + + expect(aliceKeyAgreementMethod).toBeDefined(); + expect(bobKeyAgreementMethod).toBeDefined(); + + // Step 2: Get private keys + const alicePrivateKey = aliceIdentity.privateKeys.get( + aliceKeyAgreementMethod!.id + ); + const bobPrivateKey = bobIdentity.privateKeys.get( + bobKeyAgreementMethod!.id + ); + + expect(alicePrivateKey).toBeDefined(); + expect(bobPrivateKey).toBeDefined(); + + // Step 3: Import Bob's public key for Alice + const bobPublicKey = await crypto.subtle.importKey( + 'jwk', + bobKeyAgreementMethod!.publicKeyJwk!, + { + name: 'X25519', + } as any, + true, + [] + ); + + // Step 4: Import Alice's public key for Bob + const alicePublicKey = await crypto.subtle.importKey( + 'jwk', + aliceKeyAgreementMethod!.publicKeyJwk!, + { + name: 'X25519', + } as any, + true, + [] + ); + + // Step 5: Perform key exchange (Alice's side) + const aliceSharedSecret = await performKeyExchange( + alicePrivateKey!.key, + bobPublicKey + ); + + // Step 6: Perform key exchange (Bob's side) + const bobSharedSecret = await performKeyExchange( + bobPrivateKey!.key, + alicePublicKey + ); + + // Step 7: Verify shared secrets match + expect(aliceSharedSecret).toEqual(bobSharedSecret); + + // Step 8: Validate shared secrets + expect(validateSharedSecret(aliceSharedSecret)).toBe(true); + expect(validateSharedSecret(bobSharedSecret)).toBe(true); + + // Step 9: Derive encryption keys + const salt = crypto.getRandomValues(new Uint8Array(32)); + const aliceEncryptionKey = await deriveKey(aliceSharedSecret, salt); + const bobEncryptionKey = await deriveKey(bobSharedSecret, salt); + + // Step 10: Alice encrypts a message + const message = new TextEncoder().encode('Hello Bob! This is a secret message.'); + const encryptedMessage = await encrypt(aliceEncryptionKey, message); + + expect(encryptedMessage.ciphertext).toBeDefined(); + expect(encryptedMessage.iv).toBeDefined(); + expect(encryptedMessage.tag).toBeDefined(); + expect(encryptedMessage.iv.length).toBe(12); + expect(encryptedMessage.tag.length).toBe(16); + + // Step 11: Bob decrypts the message + const decryptedMessage = await decrypt(bobEncryptionKey, encryptedMessage); + + // Step 12: Verify decrypted message matches original + const decryptedText = new TextDecoder().decode(decryptedMessage); + expect(decryptedText).toBe('Hello Bob! This is a secret message.'); + }); + + it('should support bidirectional encrypted communication', async () => { + // Setup: Establish shared secret + const aliceKeyAgreementMethod = aliceIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + )!; + const bobKeyAgreementMethod = bobIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + )!; + + const alicePrivateKey = aliceIdentity.privateKeys.get( + aliceKeyAgreementMethod.id + )!; + const bobPrivateKey = bobIdentity.privateKeys.get( + bobKeyAgreementMethod.id + )!; + + const bobPublicKey = await crypto.subtle.importKey( + 'jwk', + bobKeyAgreementMethod.publicKeyJwk!, + { name: 'X25519' } as any, + true, + [] + ); + + const alicePublicKey = await crypto.subtle.importKey( + 'jwk', + aliceKeyAgreementMethod.publicKeyJwk!, + { name: 'X25519' } as any, + true, + [] + ); + + const aliceSharedSecret = await performKeyExchange( + alicePrivateKey.key, + bobPublicKey + ); + const bobSharedSecret = await performKeyExchange( + bobPrivateKey.key, + alicePublicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const aliceEncryptionKey = await deriveKey(aliceSharedSecret, salt); + const bobEncryptionKey = await deriveKey(bobSharedSecret, salt); + + // Alice sends message to Bob + const aliceMessage = new TextEncoder().encode('Hello Bob!'); + const encryptedAliceMessage = await encrypt(aliceEncryptionKey, aliceMessage); + const decryptedAliceMessage = await decrypt( + bobEncryptionKey, + encryptedAliceMessage + ); + expect(new TextDecoder().decode(decryptedAliceMessage)).toBe('Hello Bob!'); + + // Bob sends message to Alice + const bobMessage = new TextEncoder().encode('Hello Alice!'); + const encryptedBobMessage = await encrypt(bobEncryptionKey, bobMessage); + const decryptedBobMessage = await decrypt( + aliceEncryptionKey, + encryptedBobMessage + ); + expect(new TextDecoder().decode(decryptedBobMessage)).toBe('Hello Alice!'); + }); + + it('should detect tampering with authentication tag', async () => { + // Setup: Establish encrypted channel + const aliceKeyAgreementMethod = aliceIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + )!; + const bobKeyAgreementMethod = bobIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + )!; + + const alicePrivateKey = aliceIdentity.privateKeys.get( + aliceKeyAgreementMethod.id + )!; + const bobPrivateKey = bobIdentity.privateKeys.get( + bobKeyAgreementMethod.id + )!; + + const bobPublicKey = await crypto.subtle.importKey( + 'jwk', + bobKeyAgreementMethod.publicKeyJwk!, + { name: 'X25519' } as any, + true, + [] + ); + + const alicePublicKey = await crypto.subtle.importKey( + 'jwk', + aliceKeyAgreementMethod.publicKeyJwk!, + { name: 'X25519' } as any, + true, + [] + ); + + const aliceSharedSecret = await performKeyExchange( + alicePrivateKey.key, + bobPublicKey + ); + const bobSharedSecret = await performKeyExchange( + bobPrivateKey.key, + alicePublicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const aliceEncryptionKey = await deriveKey(aliceSharedSecret, salt); + const bobEncryptionKey = await deriveKey(bobSharedSecret, salt); + + // Encrypt message + const message = new TextEncoder().encode('Secret message'); + const encryptedMessage = await encrypt(aliceEncryptionKey, message); + + // Tamper with the ciphertext + encryptedMessage.ciphertext[0] ^= 0xff; + + // Decryption should fail + await expect(decrypt(bobEncryptionKey, encryptedMessage)).rejects.toThrow(); + }); + + it('should handle multiple messages with different IVs', async () => { + // Setup: Establish encrypted channel + const aliceKeyAgreementMethod = aliceIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + )!; + const bobKeyAgreementMethod = bobIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + )!; + + const alicePrivateKey = aliceIdentity.privateKeys.get( + aliceKeyAgreementMethod.id + )!; + const bobPrivateKey = bobIdentity.privateKeys.get( + bobKeyAgreementMethod.id + )!; + + const bobPublicKey = await crypto.subtle.importKey( + 'jwk', + bobKeyAgreementMethod.publicKeyJwk!, + { name: 'X25519' } as any, + true, + [] + ); + + const alicePublicKey = await crypto.subtle.importKey( + 'jwk', + aliceKeyAgreementMethod.publicKeyJwk!, + { name: 'X25519' } as any, + true, + [] + ); + + const aliceSharedSecret = await performKeyExchange( + alicePrivateKey.key, + bobPublicKey + ); + const bobSharedSecret = await performKeyExchange( + bobPrivateKey.key, + alicePublicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const aliceEncryptionKey = await deriveKey(aliceSharedSecret, salt); + const bobEncryptionKey = await deriveKey(bobSharedSecret, salt); + + // Send multiple messages + const messages = [ + 'Message 1', + 'Message 2', + 'Message 3', + 'Message 4', + 'Message 5', + ]; + + const encryptedMessages = []; + const ivs = new Set(); + + for (const msg of messages) { + const plaintext = new TextEncoder().encode(msg); + const encrypted = await encrypt(aliceEncryptionKey, plaintext); + encryptedMessages.push(encrypted); + + // Verify each message has a unique IV + const ivString = Array.from(encrypted.iv).join(','); + expect(ivs.has(ivString)).toBe(false); + ivs.add(ivString); + } + + // Decrypt all messages + for (let i = 0; i < messages.length; i++) { + const decrypted = await decrypt(bobEncryptionKey, encryptedMessages[i]); + const decryptedText = new TextDecoder().decode(decrypted); + expect(decryptedText).toBe(messages[i]); + } + }); + + it('should support large message encryption', async () => { + // Setup: Establish encrypted channel + const aliceKeyAgreementMethod = aliceIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + )!; + const bobKeyAgreementMethod = bobIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + )!; + + const alicePrivateKey = aliceIdentity.privateKeys.get( + aliceKeyAgreementMethod.id + )!; + const bobPrivateKey = bobIdentity.privateKeys.get( + bobKeyAgreementMethod.id + )!; + + const bobPublicKey = await crypto.subtle.importKey( + 'jwk', + bobKeyAgreementMethod.publicKeyJwk!, + { name: 'X25519' } as any, + true, + [] + ); + + const alicePublicKey = await crypto.subtle.importKey( + 'jwk', + aliceKeyAgreementMethod.publicKeyJwk!, + { name: 'X25519' } as any, + true, + [] + ); + + const aliceSharedSecret = await performKeyExchange( + alicePrivateKey.key, + bobPublicKey + ); + const bobSharedSecret = await performKeyExchange( + bobPrivateKey.key, + alicePublicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const aliceEncryptionKey = await deriveKey(aliceSharedSecret, salt); + const bobEncryptionKey = await deriveKey(bobSharedSecret, salt); + + // Create a large message (64 KB - max for crypto.getRandomValues) + // Fill in chunks to avoid quota exceeded error + const largeMessage = new Uint8Array(64 * 1024); + const chunkSize = 32 * 1024; // 32 KB chunks + for (let i = 0; i < largeMessage.length; i += chunkSize) { + const chunk = largeMessage.subarray(i, Math.min(i + chunkSize, largeMessage.length)); + crypto.getRandomValues(chunk); + } + + // Encrypt large message + const encryptedLargeMessage = await encrypt( + aliceEncryptionKey, + largeMessage + ); + + // Decrypt large message + const decryptedLargeMessage = await decrypt( + bobEncryptionKey, + encryptedLargeMessage + ); + + // Verify decrypted message matches original + expect(decryptedLargeMessage).toEqual(largeMessage); + }); + + it('should verify end-to-end encryption properties', async () => { + // Setup: Establish encrypted channel + const aliceKeyAgreementMethod = aliceIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + )!; + const bobKeyAgreementMethod = bobIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + )!; + + const alicePrivateKey = aliceIdentity.privateKeys.get( + aliceKeyAgreementMethod.id + )!; + const bobPrivateKey = bobIdentity.privateKeys.get( + bobKeyAgreementMethod.id + )!; + + const bobPublicKey = await crypto.subtle.importKey( + 'jwk', + bobKeyAgreementMethod.publicKeyJwk!, + { name: 'X25519' } as any, + true, + [] + ); + + const alicePublicKey = await crypto.subtle.importKey( + 'jwk', + aliceKeyAgreementMethod.publicKeyJwk!, + { name: 'X25519' } as any, + true, + [] + ); + + const aliceSharedSecret = await performKeyExchange( + alicePrivateKey.key, + bobPublicKey + ); + const bobSharedSecret = await performKeyExchange( + bobPrivateKey.key, + alicePublicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const aliceEncryptionKey = await deriveKey(aliceSharedSecret, salt); + const bobEncryptionKey = await deriveKey(bobSharedSecret, salt); + + const message = new TextEncoder().encode('Confidential data'); + const encryptedMessage = await encrypt(aliceEncryptionKey, message); + + // Property 1: Ciphertext should not reveal plaintext + const ciphertextString = new TextDecoder().decode( + encryptedMessage.ciphertext + ); + expect(ciphertextString).not.toContain('Confidential'); + expect(ciphertextString).not.toContain('data'); + + // Property 2: Same plaintext with different IVs produces different ciphertext + const encryptedMessage2 = await encrypt(aliceEncryptionKey, message); + expect(encryptedMessage.ciphertext).not.toEqual( + encryptedMessage2.ciphertext + ); + expect(encryptedMessage.iv).not.toEqual(encryptedMessage2.iv); + + // Property 3: Both encrypted messages decrypt to same plaintext + const decrypted1 = await decrypt(bobEncryptionKey, encryptedMessage); + const decrypted2 = await decrypt(bobEncryptionKey, encryptedMessage2); + expect(decrypted1).toEqual(message); + expect(decrypted2).toEqual(message); + + // Property 4: Wrong key cannot decrypt + const wrongSalt = crypto.getRandomValues(new Uint8Array(32)); + const wrongKey = await deriveKey(aliceSharedSecret, wrongSalt); + await expect(decrypt(wrongKey, encryptedMessage)).rejects.toThrow(); + }); + + it('should handle key derivation with different salts', async () => { + // Setup: Get shared secret + const aliceKeyAgreementMethod = aliceIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + )!; + const bobKeyAgreementMethod = bobIdentity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + )!; + + const alicePrivateKey = aliceIdentity.privateKeys.get( + aliceKeyAgreementMethod.id + )!; + const bobPrivateKey = bobIdentity.privateKeys.get( + bobKeyAgreementMethod.id + )!; + + const bobPublicKey = await crypto.subtle.importKey( + 'jwk', + bobKeyAgreementMethod.publicKeyJwk!, + { name: 'X25519' } as any, + true, + [] + ); + + const alicePublicKey = await crypto.subtle.importKey( + 'jwk', + aliceKeyAgreementMethod.publicKeyJwk!, + { name: 'X25519' } as any, + true, + [] + ); + + const aliceSharedSecret = await performKeyExchange( + alicePrivateKey.key, + bobPublicKey + ); + const bobSharedSecret = await performKeyExchange( + bobPrivateKey.key, + alicePublicKey + ); + + // Derive keys with different salts + const salt1 = crypto.getRandomValues(new Uint8Array(32)); + const salt2 = crypto.getRandomValues(new Uint8Array(32)); + + const key1Alice = await deriveKey(aliceSharedSecret, salt1); + const key1Bob = await deriveKey(bobSharedSecret, salt1); + + const key2Alice = await deriveKey(aliceSharedSecret, salt2); + const key2Bob = await deriveKey(bobSharedSecret, salt2); + + // Keys derived with same salt should work together + const message = new TextEncoder().encode('Test message'); + const encrypted1 = await encrypt(key1Alice, message); + const decrypted1 = await decrypt(key1Bob, encrypted1); + expect(decrypted1).toEqual(message); + + const encrypted2 = await encrypt(key2Alice, message); + const decrypted2 = await decrypt(key2Bob, encrypted2); + expect(decrypted2).toEqual(message); + + // Keys derived with different salts should not work together + await expect(decrypt(key2Bob, encrypted1)).rejects.toThrow(); + await expect(decrypt(key1Bob, encrypted2)).rejects.toThrow(); + }); +}); diff --git a/typescript/ts_sdk/tests/integration/protocol-negotiation.test.ts b/typescript/ts_sdk/tests/integration/protocol-negotiation.test.ts new file mode 100644 index 0000000..826a7d6 --- /dev/null +++ b/typescript/ts_sdk/tests/integration/protocol-negotiation.test.ts @@ -0,0 +1,500 @@ +/** + * Integration test for protocol negotiation flow + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { DIDManager } from '../../src/core/did/did-manager.js'; +import { + createMetaProtocolMachine, + createNegotiationMessage, + createCodeGenerationMessage, + createTestCasesMessage, + encodeMetaProtocolMessage, + processMessage, + type MetaProtocolActor, +} from '../../src/protocol/meta-protocol-machine.js'; +import type { DIDIdentity } from '../../src/types/index.js'; + +describe('Protocol Negotiation Integration', () => { + let didManager: DIDManager; + let aliceIdentity: DIDIdentity; + let bobIdentity: DIDIdentity; + let aliceActor: MetaProtocolActor; + let bobActor: MetaProtocolActor; + + beforeEach(async () => { + // Initialize DID manager + didManager = new DIDManager(); + + // Create two agent identities + aliceIdentity = await didManager.createDID({ + domain: 'alice.example.com', + path: 'agent', + }); + + bobIdentity = await didManager.createDID({ + domain: 'bob.example.com', + path: 'agent', + }); + + // Create state machines for both agents + aliceActor = createMetaProtocolMachine({ + localIdentity: aliceIdentity, + remoteDID: bobIdentity.did, + maxNegotiationRounds: 5, + }); + + bobActor = createMetaProtocolMachine({ + localIdentity: bobIdentity, + remoteDID: aliceIdentity.did, + maxNegotiationRounds: 5, + }); + }); + + it('should complete full protocol negotiation flow', async () => { + // Step 1: Alice initiates negotiation + aliceActor.send({ + type: 'initiate', + candidateProtocols: 'REST API v1.0, GraphQL v1.0', + }); + + // Verify Alice is in Negotiating state + let aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Negotiating'); + expect(aliceSnapshot.context.candidateProtocols).toBe( + 'REST API v1.0, GraphQL v1.0' + ); + + // Step 2: Bob receives negotiation request + const negotiationMessage = createNegotiationMessage( + aliceSnapshot.context.sequenceId, + 'REST API v1.0, GraphQL v1.0', + 'negotiating' + ); + + const encodedMessage = encodeMetaProtocolMessage(negotiationMessage); + processMessage(bobActor, encodedMessage); + + // Verify Bob is in Negotiating state + let bobSnapshot = bobActor.getSnapshot(); + expect(bobSnapshot.value).toBe('Negotiating'); + expect(bobSnapshot.context.candidateProtocols).toBe( + 'REST API v1.0, GraphQL v1.0' + ); + + // Step 3: Bob accepts the protocol + bobActor.send({ + type: 'accept', + agreedProtocol: 'REST API v1.0', + }); + + // Verify Bob moved to CodeGeneration state + bobSnapshot = bobActor.getSnapshot(); + expect(bobSnapshot.value).toBe('CodeGeneration'); + expect(bobSnapshot.context.agreedProtocol).toBe('REST API v1.0'); + + // Step 4: Alice receives acceptance + const acceptanceMessage = createNegotiationMessage( + aliceSnapshot.context.sequenceId, + 'REST API v1.0', + 'accepted' + ); + + const encodedAcceptance = encodeMetaProtocolMessage(acceptanceMessage); + processMessage(aliceActor, encodedAcceptance); + + // Verify Alice moved to CodeGeneration state + aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('CodeGeneration'); + expect(aliceSnapshot.context.agreedProtocol).toBe('REST API v1.0'); + + // Step 5: Code generation completes + aliceActor.send({ type: 'code_ready', code: 'generated code' }); + bobActor.send({ type: 'code_ready', code: 'generated code' }); + + // Verify both moved to TestCases state + aliceSnapshot = aliceActor.getSnapshot(); + bobSnapshot = bobActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('TestCases'); + expect(bobSnapshot.value).toBe('TestCases'); + + // Step 6: Skip tests and move to Ready + aliceActor.send({ type: 'skip_tests' }); + bobActor.send({ type: 'skip_tests' }); + + // Verify both are Ready + aliceSnapshot = aliceActor.getSnapshot(); + bobSnapshot = bobActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Ready'); + expect(bobSnapshot.value).toBe('Ready'); + + // Step 7: Start communication + aliceActor.send({ type: 'start_communication' }); + bobActor.send({ type: 'start_communication' }); + + // Verify both are Communicating + aliceSnapshot = aliceActor.getSnapshot(); + bobSnapshot = bobActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Communicating'); + expect(bobSnapshot.value).toBe('Communicating'); + }); + + it('should handle multiple negotiation rounds', async () => { + // Round 1: Alice initiates + aliceActor.send({ + type: 'initiate', + candidateProtocols: 'REST API v1.0', + }); + + let aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Negotiating'); + expect(aliceSnapshot.context.negotiationRound).toBe(0); + + // Round 2: Bob counter-proposes + aliceActor.send({ + type: 'negotiate', + response: 'GraphQL v1.0', + }); + + aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Negotiating'); + expect(aliceSnapshot.context.negotiationRound).toBe(1); + + // Round 3: Alice counter-proposes again + aliceActor.send({ + type: 'negotiate', + response: 'REST API v2.0', + }); + + aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Negotiating'); + expect(aliceSnapshot.context.negotiationRound).toBe(2); + + // Finally accept + aliceActor.send({ + type: 'accept', + agreedProtocol: 'REST API v2.0', + }); + + aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('CodeGeneration'); + expect(aliceSnapshot.context.agreedProtocol).toBe('REST API v2.0'); + }); + + it('should reject negotiation after max rounds', async () => { + // Initiate negotiation + aliceActor.send({ + type: 'initiate', + candidateProtocols: 'Protocol A', + }); + + // Negotiate up to max rounds (5) + for (let i = 0; i < 5; i++) { + aliceActor.send({ + type: 'negotiate', + response: `Protocol ${i}`, + }); + } + + // Next negotiation should move to Rejected + aliceActor.send({ + type: 'negotiate', + response: 'Protocol Final', + }); + + const aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Rejected'); + }); + + it('should handle test case negotiation and execution', async () => { + // Setup: Get to TestCases state + aliceActor.send({ + type: 'initiate', + candidateProtocols: 'REST API v1.0', + }); + aliceActor.send({ + type: 'accept', + agreedProtocol: 'REST API v1.0', + }); + aliceActor.send({ type: 'code_ready', code: 'code' }); + + // Verify in TestCases state + let aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('TestCases'); + + // Agree on test cases + const testCases = JSON.stringify([ + { name: 'test1', input: 'a', expected: 'b' }, + { name: 'test2', input: 'c', expected: 'd' }, + ]); + + aliceActor.send({ + type: 'tests_agreed', + testCases, + }); + + // Verify moved to Testing state + aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Testing'); + expect(aliceSnapshot.context.testCases).toBe(testCases); + + // Tests pass + aliceActor.send({ type: 'tests_passed' }); + + // Verify moved to Ready state + aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Ready'); + }); + + it('should handle test failures and error fixing', async () => { + // Setup: Get to Testing state + aliceActor.send({ + type: 'initiate', + candidateProtocols: 'REST API v1.0', + }); + aliceActor.send({ + type: 'accept', + agreedProtocol: 'REST API v1.0', + }); + aliceActor.send({ type: 'code_ready', code: 'code' }); + aliceActor.send({ + type: 'tests_agreed', + testCases: 'test cases', + }); + + // Tests fail + aliceActor.send({ + type: 'tests_failed', + errors: 'Test 1 failed: expected b, got c', + }); + + // Verify moved to FixError state + let aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('FixError'); + expect(aliceSnapshot.context.errors).toContain( + 'Test 1 failed: expected b, got c' + ); + + // Accept fix + aliceActor.send({ + type: 'fix_accepted', + fix: 'Fixed the bug', + }); + + // Verify moved back to CodeGeneration + aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('CodeGeneration'); + + // Code generation succeeds + aliceActor.send({ type: 'code_ready', code: 'fixed code' }); + + // Skip tests this time + aliceActor.send({ type: 'skip_tests' }); + + // Verify moved to Ready + aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Ready'); + }); + + it('should handle code generation errors', async () => { + // Setup: Get to CodeGeneration state + aliceActor.send({ + type: 'initiate', + candidateProtocols: 'REST API v1.0', + }); + aliceActor.send({ + type: 'accept', + agreedProtocol: 'REST API v1.0', + }); + + // Code generation fails + aliceActor.send({ + type: 'code_error', + error: 'Syntax error in generated code', + }); + + // Verify moved to Failed state + const aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Failed'); + expect(aliceSnapshot.context.errors).toContain( + 'Syntax error in generated code' + ); + }); + + it('should handle protocol errors during communication', async () => { + // Setup: Get to Communicating state + aliceActor.send({ + type: 'initiate', + candidateProtocols: 'REST API v1.0', + }); + aliceActor.send({ + type: 'accept', + agreedProtocol: 'REST API v1.0', + }); + aliceActor.send({ type: 'code_ready', code: 'code' }); + aliceActor.send({ type: 'skip_tests' }); + aliceActor.send({ type: 'start_communication' }); + + // Verify in Communicating state + let aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Communicating'); + + // Protocol error occurs + aliceActor.send({ + type: 'protocol_error', + error: 'Message format mismatch', + }); + + // Verify moved to FixError state + aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('FixError'); + expect(aliceSnapshot.context.errors).toContain('Message format mismatch'); + }); + + it('should handle explicit rejection', async () => { + // Alice initiates + aliceActor.send({ + type: 'initiate', + candidateProtocols: 'Unsupported Protocol', + }); + + // Bob rejects + aliceActor.send({ + type: 'reject', + reason: 'Protocol not supported', + }); + + // Verify moved to Rejected state + const aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Rejected'); + }); + + it('should handle timeout during negotiation', async () => { + // Alice initiates + aliceActor.send({ + type: 'initiate', + candidateProtocols: 'REST API v1.0', + }); + + // Timeout occurs + aliceActor.send({ type: 'timeout' }); + + // Verify moved to Rejected state + const aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Rejected'); + }); + + it('should complete communication and end gracefully', async () => { + // Setup: Get to Communicating state + aliceActor.send({ + type: 'initiate', + candidateProtocols: 'REST API v1.0', + }); + aliceActor.send({ + type: 'accept', + agreedProtocol: 'REST API v1.0', + }); + aliceActor.send({ type: 'code_ready', code: 'code' }); + aliceActor.send({ type: 'skip_tests' }); + aliceActor.send({ type: 'start_communication' }); + + // End communication + aliceActor.send({ type: 'end' }); + + // Verify moved to Completed state + const aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.value).toBe('Completed'); + }); + + it('should maintain sequence ID throughout negotiation', async () => { + // Initiate + aliceActor.send({ + type: 'initiate', + candidateProtocols: 'Protocol A', + }); + + let aliceSnapshot = aliceActor.getSnapshot(); + const initialSequenceId = aliceSnapshot.context.sequenceId; + + // Negotiate (should increment sequence ID) + aliceActor.send({ + type: 'negotiate', + response: 'Protocol B', + }); + + aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.context.sequenceId).toBe(initialSequenceId + 1); + + // Negotiate again + aliceActor.send({ + type: 'negotiate', + response: 'Protocol C', + }); + + aliceSnapshot = aliceActor.getSnapshot(); + expect(aliceSnapshot.context.sequenceId).toBe(initialSequenceId + 2); + }); + + it('should handle bidirectional message exchange', async () => { + // Alice initiates + aliceActor.send({ + type: 'initiate', + candidateProtocols: 'REST API v1.0, GraphQL v1.0', + }); + + const aliceSnapshot1 = aliceActor.getSnapshot(); + + // Create and send message to Bob + const aliceMessage = createNegotiationMessage( + aliceSnapshot1.context.sequenceId, + 'REST API v1.0, GraphQL v1.0', + 'negotiating' + ); + + const encodedAliceMessage = encodeMetaProtocolMessage(aliceMessage); + processMessage(bobActor, encodedAliceMessage); + + // Bob counter-proposes + bobActor.send({ + type: 'negotiate', + response: 'GraphQL v1.0', + }); + + const bobSnapshot1 = bobActor.getSnapshot(); + + // Send Bob's counter-proposal to Alice + const bobMessage = createNegotiationMessage( + bobSnapshot1.context.sequenceId, + 'GraphQL v1.0', + 'negotiating', + 'Prefer GraphQL for flexibility' + ); + + const encodedBobMessage = encodeMetaProtocolMessage(bobMessage); + processMessage(aliceActor, encodedBobMessage); + + // Alice accepts + aliceActor.send({ + type: 'accept', + agreedProtocol: 'GraphQL v1.0', + }); + + const aliceSnapshot2 = aliceActor.getSnapshot(); + expect(aliceSnapshot2.value).toBe('CodeGeneration'); + expect(aliceSnapshot2.context.agreedProtocol).toBe('GraphQL v1.0'); + + // Send acceptance to Bob + const acceptMessage = createNegotiationMessage( + aliceSnapshot2.context.sequenceId, + 'GraphQL v1.0', + 'accepted' + ); + + const encodedAcceptMessage = encodeMetaProtocolMessage(acceptMessage); + processMessage(bobActor, encodedAcceptMessage); + + const bobSnapshot2 = bobActor.getSnapshot(); + expect(bobSnapshot2.value).toBe('CodeGeneration'); + expect(bobSnapshot2.context.agreedProtocol).toBe('GraphQL v1.0'); + }); +}); diff --git a/typescript/ts_sdk/tests/unit/.gitkeep b/typescript/ts_sdk/tests/unit/.gitkeep new file mode 100644 index 0000000..1736031 --- /dev/null +++ b/typescript/ts_sdk/tests/unit/.gitkeep @@ -0,0 +1 @@ +# Unit tests will be added here diff --git a/typescript/ts_sdk/tests/unit/api/anp-client.test.ts b/typescript/ts_sdk/tests/unit/api/anp-client.test.ts new file mode 100644 index 0000000..63ed487 --- /dev/null +++ b/typescript/ts_sdk/tests/unit/api/anp-client.test.ts @@ -0,0 +1,471 @@ +/** + * Unit tests for ANPClient + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { ANPClient } from '../../../src/anp-client'; + +// Mock fetch globally +global.fetch = vi.fn(); + +describe('ANPClient', () => { + beforeEach(() => { + // Reset fetch mock before each test + vi.clearAllMocks(); + }); + describe('constructor', () => { + it('should create client with default config', () => { + const client = new ANPClient(); + + expect(client).toBeDefined(); + expect(client.did).toBeDefined(); + expect(client.agent).toBeDefined(); + expect(client.discovery).toBeDefined(); + expect(client.protocol).toBeDefined(); + expect(client.http).toBeDefined(); + }); + + it('should create client with custom config', () => { + const customConfig = { + did: { + cacheTTL: 10000, + timeout: 5000, + }, + auth: { + maxTokenAge: 3600000, + nonceLength: 32, + clockSkewTolerance: 300, + }, + http: { + timeout: 15000, + maxRetries: 5, + retryDelay: 2000, + }, + }; + + const client = new ANPClient(customConfig); + + expect(client).toBeDefined(); + expect(client.did).toBeDefined(); + expect(client.agent).toBeDefined(); + expect(client.discovery).toBeDefined(); + expect(client.protocol).toBeDefined(); + expect(client.http).toBeDefined(); + }); + + it('should initialize all managers', () => { + const client = new ANPClient(); + + // Verify all namespaces are accessible + expect(typeof client.did.create).toBe('function'); + expect(typeof client.did.resolve).toBe('function'); + expect(typeof client.did.sign).toBe('function'); + expect(typeof client.did.verify).toBe('function'); + + expect(typeof client.agent.createDescription).toBe('function'); + expect(typeof client.agent.addInformation).toBe('function'); + expect(typeof client.agent.addInterface).toBe('function'); + expect(typeof client.agent.signDescription).toBe('function'); + expect(typeof client.agent.fetchDescription).toBe('function'); + + expect(typeof client.discovery.discoverAgents).toBe('function'); + expect(typeof client.discovery.registerWithSearchService).toBe('function'); + expect(typeof client.discovery.searchAgents).toBe('function'); + + expect(typeof client.protocol.createNegotiationMachine).toBe('function'); + expect(typeof client.protocol.sendMessage).toBe('function'); + expect(typeof client.protocol.receiveMessage).toBe('function'); + + expect(typeof client.http.request).toBe('function'); + expect(typeof client.http.get).toBe('function'); + expect(typeof client.http.post).toBe('function'); + }); + + it('should use default values when config is partially provided', () => { + const partialConfig = { + http: { + timeout: 20000, + }, + }; + + const client = new ANPClient(partialConfig as any); + + expect(client).toBeDefined(); + expect(client.did).toBeDefined(); + }); + }); + + describe('DID API', () => { + let client: ANPClient; + + beforeEach(() => { + client = new ANPClient(); + }); + + it('should create a DID identity', async () => { + const identity = await client.did.create({ + domain: 'example.com', + }); + + expect(identity).toBeDefined(); + expect(identity.did).toMatch(/^did:wba:example\.com$/); + expect(identity.document).toBeDefined(); + expect(identity.privateKeys).toBeDefined(); + }); + + it('should create a DID identity with path', async () => { + const identity = await client.did.create({ + domain: 'example.com', + path: 'agents/my-agent', + }); + + expect(identity).toBeDefined(); + expect(identity.did).toContain('did:wba:example.com:'); + expect(identity.did).toContain('agents'); + }); + + it('should resolve a DID', async () => { + // Mock fetch to return a 404 + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + client.did.resolve('did:wba:example.com') + ).rejects.toThrow(); + }); + + it('should sign data with DID identity', async () => { + const identity = await client.did.create({ + domain: 'example.com', + }); + + const data = new TextEncoder().encode('test data'); + const signature = await client.did.sign(identity, data); + + expect(signature).toBeDefined(); + expect(signature.value).toBeInstanceOf(Uint8Array); + expect(signature.verificationMethod).toBeDefined(); + }); + + it('should verify a signature', async () => { + const identity = await client.did.create({ + domain: 'example.com', + }); + + const data = new TextEncoder().encode('test data'); + const signature = await client.did.sign(identity, data); + + // Mock fetch to return a 404 for DID resolution + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + client.did.verify(identity.did, data, signature) + ).rejects.toThrow(); + }); + + it('should reject invalid signature', async () => { + const identity = await client.did.create({ + domain: 'example.com', + }); + + const data = new TextEncoder().encode('test data'); + const signature = await client.did.sign(identity, data); + + // Modify the data + const modifiedData = new TextEncoder().encode('modified data'); + + // Mock fetch to return a 404 for DID resolution + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + client.did.verify(identity.did, modifiedData, signature) + ).rejects.toThrow(); + }); + }); + + describe('Agent API', () => { + let client: ANPClient; + + beforeEach(() => { + client = new ANPClient(); + }); + + it('should create an agent description', () => { + const description = client.agent.createDescription({ + name: 'Test Agent', + description: 'A test agent', + }); + + expect(description).toBeDefined(); + expect(description.name).toBe('Test Agent'); + expect(description.description).toBe('A test agent'); + expect(description.protocolType).toBe('ANP'); + expect(description.type).toBe('AgentDescription'); + }); + + it('should add information to agent description', () => { + const description = client.agent.createDescription({ + name: 'Test Agent', + }); + + const updatedDescription = client.agent.addInformation(description, { + type: 'documentation', + description: 'API documentation', + url: 'https://example.com/docs', + }); + + expect(updatedDescription.Infomations).toHaveLength(1); + expect(updatedDescription.Infomations?.[0].type).toBe('documentation'); + }); + + it('should add interface to agent description', () => { + const description = client.agent.createDescription({ + name: 'Test Agent', + }); + + const updatedDescription = client.agent.addInterface(description, { + type: 'REST', + protocol: 'HTTP', + version: '1.0', + url: 'https://example.com/api', + }); + + expect(updatedDescription.interfaces).toHaveLength(1); + expect(updatedDescription.interfaces?.[0].type).toBe('REST'); + }); + + it('should sign agent description', async () => { + const identity = await client.did.create({ + domain: 'example.com', + }); + + const description = client.agent.createDescription({ + name: 'Test Agent', + did: identity.did, + }); + + const signedDescription = await client.agent.signDescription( + description, + identity, + 'test-challenge', + 'example.com' + ); + + expect(signedDescription.proof).toBeDefined(); + expect(signedDescription.proof?.type).toBe('Ed25519Signature2020'); + expect(signedDescription.proof?.challenge).toBe('test-challenge'); + expect(signedDescription.proof?.domain).toBe('example.com'); + }); + + it('should fetch agent description', async () => { + // Mock fetch to return a 404 + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + client.agent.fetchDescription('https://example.com/agent.json') + ).rejects.toThrow(); + }); + }); + + describe('Discovery API', () => { + let client: ANPClient; + + beforeEach(() => { + client = new ANPClient(); + }); + + it('should discover agents from domain', async () => { + // Mock fetch to return a 404 + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + client.discovery.discoverAgents('example.com') + ).rejects.toThrow(); + }); + + it('should register with search service', async () => { + const identity = await client.did.create({ + domain: 'example.com', + }); + + // Mock fetch to return a 404 + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + client.discovery.registerWithSearchService( + 'https://search.example.com', + 'https://example.com/agent.json', + identity + ) + ).rejects.toThrow(); + }); + + it('should search for agents', async () => { + // Mock fetch to return a 404 + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + client.discovery.searchAgents('https://search.example.com', { + keywords: ['test'], + }) + ).rejects.toThrow(); + }); + }); + + describe('Protocol API', () => { + let client: ANPClient; + + beforeEach(() => { + client = new ANPClient(); + }); + + it('should create negotiation machine', async () => { + const identity = await client.did.create({ + domain: 'example.com', + }); + + const machine = client.protocol.createNegotiationMachine({ + localIdentity: identity, + remoteDID: 'did:wba:remote.com', + maxNegotiationRounds: 5, + }); + + expect(machine).toBeDefined(); + expect(machine.getSnapshot).toBeDefined(); + }); + + it('should send message', async () => { + const identity = await client.did.create({ + domain: 'example.com', + }); + + const message = { + action: 'protocolNegotiation', + sequenceId: 1, + candidateProtocols: 'HTTP/REST', + status: 'negotiating', + }; + + // Mock fetch to return a 404 + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + client.protocol.sendMessage('did:wba:remote.com', message, identity) + ).rejects.toThrow(); + }); + + it('should receive message', async () => { + const identity = await client.did.create({ + domain: 'example.com', + }); + + const machine = client.protocol.createNegotiationMachine({ + localIdentity: identity, + remoteDID: 'did:wba:remote.com', + }); + + // Create a test message + const message = new Uint8Array([0x00, 0x7b, 0x7d]); // META protocol type + {} + + // This should process without error + expect(() => { + client.protocol.receiveMessage(message, machine); + }).toThrow(); // Will throw because message is invalid JSON + }); + }); + + describe('HTTP API', () => { + let client: ANPClient; + + beforeEach(() => { + client = new ANPClient(); + }); + + it('should make HTTP request', async () => { + // Mock fetch to return a 404 + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + client.http.request('https://example.com/api', { method: 'GET' }) + ).rejects.toThrow(); + }); + + it('should make GET request', async () => { + // Mock fetch to return a 404 + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + client.http.get('https://example.com/api') + ).rejects.toThrow(); + }); + + it('should make POST request', async () => { + // Mock fetch to return a 404 + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + client.http.post('https://example.com/api', { data: 'test' }) + ).rejects.toThrow(); + }); + + it('should make authenticated request', async () => { + const identity = await client.did.create({ + domain: 'example.com', + }); + + // Mock fetch to return a 404 + (global.fetch as any).mockResolvedValueOnce({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + client.http.get('https://example.com/api', identity) + ).rejects.toThrow(); + }); + }); +}); diff --git a/typescript/ts_sdk/tests/unit/core/agent-description/agent-description-manager.test.ts b/typescript/ts_sdk/tests/unit/core/agent-description/agent-description-manager.test.ts new file mode 100644 index 0000000..b1574ea --- /dev/null +++ b/typescript/ts_sdk/tests/unit/core/agent-description/agent-description-manager.test.ts @@ -0,0 +1,755 @@ +/** + * Unit tests for Agent Description Manager + */ + +import { describe, it, expect, beforeEach } from 'vitest'; +import { AgentDescriptionManager } from '../../../../src/core/agent-description/agent-description-manager.js'; +import type { + AgentMetadata, + Information, + Interface, +} from '../../../../src/types/index.js'; + +describe('AgentDescriptionManager - Description Creation', () => { + let manager: AgentDescriptionManager; + + beforeEach(() => { + manager = new AgentDescriptionManager(); + }); + + describe('createDescription', () => { + it('should create a valid agent description with required fields', () => { + const metadata: AgentMetadata = { + name: 'Test Agent', + description: 'A test agent for unit testing', + }; + + const description = manager.createDescription(metadata); + + expect(description.protocolType).toBe('ANP'); + expect(description.type).toBe('AgentDescription'); + expect(description.name).toBe('Test Agent'); + expect(description.description).toBe('A test agent for unit testing'); + expect(description.protocolVersion).toBeDefined(); + expect(description.securityDefinitions).toBeDefined(); + expect(description.security).toBeDefined(); + }); + + it('should include DID if provided', () => { + const metadata: AgentMetadata = { + name: 'Test Agent', + did: 'did:wba:example.com', + }; + + const description = manager.createDescription(metadata); + + expect(description.did).toBe('did:wba:example.com'); + }); + + it('should include owner information if provided', () => { + const metadata: AgentMetadata = { + name: 'Test Agent', + owner: { + name: 'Test Organization', + url: 'https://example.com', + email: 'contact@example.com', + }, + }; + + const description = manager.createDescription(metadata); + + expect(description.owner).toBeDefined(); + expect(description.owner?.name).toBe('Test Organization'); + expect(description.owner?.url).toBe('https://example.com'); + expect(description.owner?.email).toBe('contact@example.com'); + }); + + it('should include URL if provided', () => { + const metadata: AgentMetadata = { + name: 'Test Agent', + url: 'https://example.com/agent', + }; + + const description = manager.createDescription(metadata); + + expect(description.url).toBe('https://example.com/agent'); + }); + + it('should set created timestamp', () => { + const metadata: AgentMetadata = { + name: 'Test Agent', + }; + + const description = manager.createDescription(metadata); + + expect(description.created).toBeDefined(); + expect(new Date(description.created!).getTime()).toBeLessThanOrEqual( + Date.now() + ); + }); + + it('should use custom protocol version if provided', () => { + const metadata: AgentMetadata = { + name: 'Test Agent', + protocolVersion: '2.0.0', + }; + + const description = manager.createDescription(metadata); + + expect(description.protocolVersion).toBe('2.0.0'); + }); + + it('should use default protocol version if not provided', () => { + const metadata: AgentMetadata = { + name: 'Test Agent', + }; + + const description = manager.createDescription(metadata); + + expect(description.protocolVersion).toBe('1.0.0'); + }); + + it('should set up security definitions with did_wba scheme', () => { + const metadata: AgentMetadata = { + name: 'Test Agent', + }; + + const description = manager.createDescription(metadata); + + expect(description.securityDefinitions).toHaveProperty('did_wba'); + expect(description.securityDefinitions.did_wba.scheme).toBe('did_wba'); + expect(description.securityDefinitions.did_wba.type).toBe('http'); + expect(description.security).toBe('did_wba'); + }); + + it('should initialize empty Infomations array', () => { + const metadata: AgentMetadata = { + name: 'Test Agent', + }; + + const description = manager.createDescription(metadata); + + expect(description.Infomations).toBeDefined(); + expect(Array.isArray(description.Infomations)).toBe(true); + expect(description.Infomations).toHaveLength(0); + }); + + it('should initialize empty interfaces array', () => { + const metadata: AgentMetadata = { + name: 'Test Agent', + }; + + const description = manager.createDescription(metadata); + + expect(description.interfaces).toBeDefined(); + expect(Array.isArray(description.interfaces)).toBe(true); + expect(description.interfaces).toHaveLength(0); + }); + + it('should throw error if name is empty', () => { + const metadata: AgentMetadata = { + name: '', + }; + + expect(() => manager.createDescription(metadata)).toThrow( + 'Agent name is required' + ); + }); + + it('should throw error if name is only whitespace', () => { + const metadata: AgentMetadata = { + name: ' ', + }; + + expect(() => manager.createDescription(metadata)).toThrow( + 'Agent name is required' + ); + }); + }); +}); + +describe('AgentDescriptionManager - Adding Resources', () => { + let manager: AgentDescriptionManager; + + beforeEach(() => { + manager = new AgentDescriptionManager(); + }); + + describe('addInformation', () => { + it('should add information resource to description', () => { + const description = manager.createDescription({ + name: 'Test Agent', + }); + + const info: Information = { + type: 'documentation', + description: 'API documentation', + url: 'https://example.com/docs', + }; + + const updated = manager.addInformation(description, info); + + expect(updated.Infomations).toHaveLength(1); + expect(updated.Infomations![0]).toEqual(info); + }); + + it('should add multiple information resources', () => { + const description = manager.createDescription({ + name: 'Test Agent', + }); + + const info1: Information = { + type: 'documentation', + description: 'API documentation', + url: 'https://example.com/docs', + }; + + const info2: Information = { + type: 'schema', + description: 'Data schema', + url: 'https://example.com/schema', + }; + + let updated = manager.addInformation(description, info1); + updated = manager.addInformation(updated, info2); + + expect(updated.Infomations).toHaveLength(2); + expect(updated.Infomations![0]).toEqual(info1); + expect(updated.Infomations![1]).toEqual(info2); + }); + + it('should validate information resource has required fields', () => { + const description = manager.createDescription({ + name: 'Test Agent', + }); + + const invalidInfo = { + type: 'documentation', + description: 'API documentation', + } as Information; + + expect(() => manager.addInformation(description, invalidInfo)).toThrow( + 'Information resource must have type, description, and url' + ); + }); + + it('should prevent duplicate information URLs', () => { + const description = manager.createDescription({ + name: 'Test Agent', + }); + + const info: Information = { + type: 'documentation', + description: 'API documentation', + url: 'https://example.com/docs', + }; + + const updated = manager.addInformation(description, info); + + expect(() => manager.addInformation(updated, info)).toThrow( + 'Information resource with URL https://example.com/docs already exists' + ); + }); + }); + + describe('addInterface', () => { + it('should add interface to description', () => { + const description = manager.createDescription({ + name: 'Test Agent', + }); + + const iface: Interface = { + type: 'REST', + protocol: 'HTTP', + version: '1.0', + url: 'https://example.com/api', + }; + + const updated = manager.addInterface(description, iface); + + expect(updated.interfaces).toHaveLength(1); + expect(updated.interfaces![0]).toEqual(iface); + }); + + it('should add multiple interfaces', () => { + const description = manager.createDescription({ + name: 'Test Agent', + }); + + const iface1: Interface = { + type: 'REST', + protocol: 'HTTP', + version: '1.0', + url: 'https://example.com/api', + }; + + const iface2: Interface = { + type: 'GraphQL', + protocol: 'HTTP', + version: '1.0', + url: 'https://example.com/graphql', + description: 'GraphQL API', + }; + + let updated = manager.addInterface(description, iface1); + updated = manager.addInterface(updated, iface2); + + expect(updated.interfaces).toHaveLength(2); + expect(updated.interfaces![0]).toEqual(iface1); + expect(updated.interfaces![1]).toEqual(iface2); + }); + + it('should validate interface has required fields', () => { + const description = manager.createDescription({ + name: 'Test Agent', + }); + + const invalidInterface = { + type: 'REST', + protocol: 'HTTP', + version: '1.0', + } as Interface; + + expect(() => manager.addInterface(description, invalidInterface)).toThrow( + 'Interface must have type, protocol, version, and url' + ); + }); + + it('should prevent duplicate interface URLs', () => { + const description = manager.createDescription({ + name: 'Test Agent', + }); + + const iface: Interface = { + type: 'REST', + protocol: 'HTTP', + version: '1.0', + url: 'https://example.com/api', + }; + + const updated = manager.addInterface(description, iface); + + expect(() => manager.addInterface(updated, iface)).toThrow( + 'Interface with URL https://example.com/api already exists' + ); + }); + }); +}); + +describe('AgentDescriptionManager - Description Signing', () => { + let manager: AgentDescriptionManager; + + beforeEach(() => { + manager = new AgentDescriptionManager(); + }); + + describe('signDescription', () => { + it('should sign agent description and add proof', async () => { + const { DIDManager } = await import( + '../../../../src/core/did/did-manager.js' + ); + const didManager = new DIDManager(); + + const identity = await didManager.createDID({ + domain: 'example.com', + }); + + const description = manager.createDescription({ + name: 'Test Agent', + did: identity.did, + }); + + const challenge = 'test-challenge-123'; + const domain = 'example.com'; + + const signed = await manager.signDescription( + description, + identity, + challenge, + domain + ); + + expect(signed.proof).toBeDefined(); + expect(signed.proof!.type).toBe('Ed25519Signature2020'); + expect(signed.proof!.verificationMethod).toBe(`${identity.did}#auth-key`); + expect(signed.proof!.proofPurpose).toBe('authentication'); + expect(signed.proof!.challenge).toBe(challenge); + expect(signed.proof!.domain).toBe(domain); + expect(signed.proof!.created).toBeDefined(); + expect(signed.proof!.proofValue).toBeDefined(); + }); + + it('should use JCS canonicalization before signing', async () => { + const { DIDManager } = await import( + '../../../../src/core/did/did-manager.js' + ); + const didManager = new DIDManager(); + + const identity = await didManager.createDID({ + domain: 'example.com', + }); + + const description = manager.createDescription({ + name: 'Test Agent', + did: identity.did, + }); + + const challenge = 'test-challenge'; + const domain = 'example.com'; + + const signed = await manager.signDescription( + description, + identity, + challenge, + domain + ); + + // Verify that proof exists and has a value + expect(signed.proof).toBeDefined(); + expect(signed.proof!.proofValue).toBeDefined(); + expect(signed.proof!.proofValue.length).toBeGreaterThan(0); + }); + + it('should verify signed description', async () => { + const { DIDManager } = await import( + '../../../../src/core/did/did-manager.js' + ); + const didManager = new DIDManager(); + + const identity = await didManager.createDID({ + domain: 'example.com', + }); + + const description = manager.createDescription({ + name: 'Test Agent', + did: identity.did, + }); + + const challenge = 'test-challenge'; + const domain = 'example.com'; + + const signed = await manager.signDescription( + description, + identity, + challenge, + domain + ); + + const isValid = await manager.verifyDescription( + signed, + didManager, + identity.document + ); + + expect(isValid).toBe(true); + }); + + it('should fail verification with tampered description', async () => { + const { DIDManager } = await import( + '../../../../src/core/did/did-manager.js' + ); + const didManager = new DIDManager(); + + const identity = await didManager.createDID({ + domain: 'example.com', + }); + + const description = manager.createDescription({ + name: 'Test Agent', + did: identity.did, + }); + + const challenge = 'test-challenge'; + const domain = 'example.com'; + + const signed = await manager.signDescription( + description, + identity, + challenge, + domain + ); + + // Tamper with the description + const tampered = { + ...signed, + name: 'Tampered Agent', + }; + + const isValid = await manager.verifyDescription( + tampered, + didManager, + identity.document + ); + + expect(isValid).toBe(false); + }); + + it('should throw error if description has no DID', async () => { + const { DIDManager } = await import( + '../../../../src/core/did/did-manager.js' + ); + const didManager = new DIDManager(); + + const identity = await didManager.createDID({ + domain: 'example.com', + }); + + const description = manager.createDescription({ + name: 'Test Agent', + }); + + const challenge = 'test-challenge'; + const domain = 'example.com'; + + await expect( + manager.signDescription(description, identity, challenge, domain) + ).rejects.toThrow('Agent description must have a DID to be signed'); + }); + }); +}); + +describe('AgentDescriptionManager - Description Fetching', () => { + let manager: AgentDescriptionManager; + + beforeEach(() => { + manager = new AgentDescriptionManager(); + }); + + describe('fetchDescription', () => { + it('should fetch and parse agent description from URL', async () => { + const mockDescription = { + protocolType: 'ANP', + protocolVersion: '1.0.0', + type: 'AgentDescription', + name: 'Remote Agent', + did: 'did:wba:example.com', + securityDefinitions: { + did_wba: { + scheme: 'did_wba', + type: 'http', + }, + }, + security: 'did_wba', + Infomations: [], + interfaces: [], + }; + + // Mock fetch + global.fetch = async (url: string) => { + return { + ok: true, + status: 200, + json: async () => mockDescription, + } as Response; + }; + + const description = await manager.fetchDescription( + 'https://example.com/agent-description' + ); + + expect(description).toEqual(mockDescription); + }); + + it('should throw error on HTTP error', async () => { + // Mock fetch with error + global.fetch = async (url: string) => { + return { + ok: false, + status: 404, + statusText: 'Not Found', + } as Response; + }; + + await expect( + manager.fetchDescription('https://example.com/agent-description') + ).rejects.toThrow('Failed to fetch agent description'); + }); + + it('should throw error on invalid JSON', async () => { + // Mock fetch with invalid JSON + global.fetch = async (url: string) => { + return { + ok: true, + status: 200, + json: async () => { + throw new Error('Invalid JSON'); + }, + } as Response; + }; + + await expect( + manager.fetchDescription('https://example.com/agent-description') + ).rejects.toThrow('Failed to parse agent description'); + }); + + it('should validate fetched description has required fields', async () => { + const invalidDescription = { + protocolType: 'ANP', + // Missing required fields + }; + + // Mock fetch + global.fetch = async (url: string) => { + return { + ok: true, + status: 200, + json: async () => invalidDescription, + } as Response; + }; + + await expect( + manager.fetchDescription('https://example.com/agent-description') + ).rejects.toThrow('Invalid agent description'); + }); + }); +}); + +describe('AgentDescriptionManager - Description Verification', () => { + let manager: AgentDescriptionManager; + + beforeEach(() => { + manager = new AgentDescriptionManager(); + }); + + describe('verifyDescription', () => { + it('should return false if description has no proof', async () => { + const { DIDManager } = await import( + '../../../../src/core/did/did-manager.js' + ); + const didManager = new DIDManager(); + + const description = manager.createDescription({ + name: 'Test Agent', + did: 'did:wba:example.com', + }); + + const isValid = await manager.verifyDescription(description, didManager); + + expect(isValid).toBe(false); + }); + + it('should return false if description has no DID', async () => { + const { DIDManager } = await import( + '../../../../src/core/did/did-manager.js' + ); + const didManager = new DIDManager(); + + const description = manager.createDescription({ + name: 'Test Agent', + }); + + // Add a fake proof + const descriptionWithProof = { + ...description, + proof: { + type: 'Ed25519Signature2020', + created: new Date().toISOString(), + verificationMethod: 'did:wba:example.com#auth-key', + proofPurpose: 'authentication', + challenge: 'test', + domain: 'example.com', + proofValue: 'fake-signature', + }, + }; + + const isValid = await manager.verifyDescription( + descriptionWithProof, + didManager + ); + + expect(isValid).toBe(false); + }); + + it('should validate domain in proof', async () => { + const { DIDManager } = await import( + '../../../../src/core/did/did-manager.js' + ); + const didManager = new DIDManager(); + + const identity = await didManager.createDID({ + domain: 'example.com', + }); + + const description = manager.createDescription({ + name: 'Test Agent', + did: identity.did, + }); + + const challenge = 'test-challenge'; + const domain = 'example.com'; + + const signed = await manager.signDescription( + description, + identity, + challenge, + domain + ); + + // Verify with correct domain + const isValid = await manager.verifyDescriptionWithDomain( + signed, + didManager, + domain, + identity.document + ); + + expect(isValid).toBe(true); + + // Verify with wrong domain + const isInvalid = await manager.verifyDescriptionWithDomain( + signed, + didManager, + 'wrong-domain.com', + identity.document + ); + + expect(isInvalid).toBe(false); + }); + + it('should validate challenge in proof', async () => { + const { DIDManager } = await import( + '../../../../src/core/did/did-manager.js' + ); + const didManager = new DIDManager(); + + const identity = await didManager.createDID({ + domain: 'example.com', + }); + + const description = manager.createDescription({ + name: 'Test Agent', + did: identity.did, + }); + + const challenge = 'test-challenge'; + const domain = 'example.com'; + + const signed = await manager.signDescription( + description, + identity, + challenge, + domain + ); + + // Verify with correct challenge + const isValid = await manager.verifyDescriptionWithChallenge( + signed, + didManager, + challenge, + identity.document + ); + + expect(isValid).toBe(true); + + // Verify with wrong challenge + const isInvalid = await manager.verifyDescriptionWithChallenge( + signed, + didManager, + 'wrong-challenge', + identity.document + ); + + expect(isInvalid).toBe(false); + }); + }); +}); diff --git a/typescript/ts_sdk/tests/unit/core/agent-discovery/agent-discovery-manager.test.ts b/typescript/ts_sdk/tests/unit/core/agent-discovery/agent-discovery-manager.test.ts new file mode 100644 index 0000000..5b8883b --- /dev/null +++ b/typescript/ts_sdk/tests/unit/core/agent-discovery/agent-discovery-manager.test.ts @@ -0,0 +1,637 @@ +/** + * Unit tests for Agent Discovery Manager + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { AgentDiscoveryManager } from '../../../../src/core/agent-discovery/agent-discovery-manager.js'; +import { HTTPClient } from '../../../../src/transport/http-client.js'; +import type { + DiscoveryDocument, + AgentDescriptionItem, +} from '../../../../src/types/index.js'; +import { NetworkError } from '../../../../src/errors/index.js'; + +describe('AgentDiscoveryManager - Active Discovery', () => { + let discoveryManager: AgentDiscoveryManager; + let mockHttpClient: HTTPClient; + + beforeEach(() => { + // Create mock HTTP client + mockHttpClient = { + get: vi.fn(), + post: vi.fn(), + request: vi.fn(), + } as any; + + discoveryManager = new AgentDiscoveryManager(mockHttpClient); + }); + + describe('discoverAgents', () => { + it('should construct correct .well-known URL from domain', async () => { + const domain = 'example.com'; + const expectedUrl = 'https://example.com/.well-known/agent-descriptions'; + + const mockDocument: DiscoveryDocument = { + '@context': { ad: 'https://anp.org/ad#' }, + '@type': 'CollectionPage', + url: expectedUrl, + items: [], + }; + + vi.mocked(mockHttpClient.get).mockResolvedValue({ + ok: true, + json: async () => mockDocument, + } as Response); + + await discoveryManager.discoverAgents(domain); + + expect(mockHttpClient.get).toHaveBeenCalledWith(expectedUrl, undefined); + }); + + it('should parse discovery document and return agent items', async () => { + const domain = 'example.com'; + const mockItems: AgentDescriptionItem[] = [ + { + '@type': 'ad:AgentDescription', + name: 'Agent 1', + '@id': 'https://example.com/agents/agent1', + }, + { + '@type': 'ad:AgentDescription', + name: 'Agent 2', + '@id': 'https://example.com/agents/agent2', + }, + ]; + + const mockDocument: DiscoveryDocument = { + '@context': { ad: 'https://anp.org/ad#' }, + '@type': 'CollectionPage', + url: 'https://example.com/.well-known/agent-descriptions', + items: mockItems, + }; + + vi.mocked(mockHttpClient.get).mockResolvedValue({ + ok: true, + json: async () => mockDocument, + } as Response); + + const result = await discoveryManager.discoverAgents(domain); + + expect(result).toEqual(mockItems); + expect(result).toHaveLength(2); + expect(result[0].name).toBe('Agent 1'); + expect(result[1].name).toBe('Agent 2'); + }); + + it('should handle pagination by recursively fetching all pages', async () => { + const domain = 'example.com'; + + // First page + const page1Items: AgentDescriptionItem[] = [ + { + '@type': 'ad:AgentDescription', + name: 'Agent 1', + '@id': 'https://example.com/agents/agent1', + }, + ]; + + const page1: DiscoveryDocument = { + '@context': { ad: 'https://anp.org/ad#' }, + '@type': 'CollectionPage', + url: 'https://example.com/.well-known/agent-descriptions', + items: page1Items, + next: 'https://example.com/.well-known/agent-descriptions?page=2', + }; + + // Second page + const page2Items: AgentDescriptionItem[] = [ + { + '@type': 'ad:AgentDescription', + name: 'Agent 2', + '@id': 'https://example.com/agents/agent2', + }, + ]; + + const page2: DiscoveryDocument = { + '@context': { ad: 'https://anp.org/ad#' }, + '@type': 'CollectionPage', + url: 'https://example.com/.well-known/agent-descriptions?page=2', + items: page2Items, + next: 'https://example.com/.well-known/agent-descriptions?page=3', + }; + + // Third page (last) + const page3Items: AgentDescriptionItem[] = [ + { + '@type': 'ad:AgentDescription', + name: 'Agent 3', + '@id': 'https://example.com/agents/agent3', + }, + ]; + + const page3: DiscoveryDocument = { + '@context': { ad: 'https://anp.org/ad#' }, + '@type': 'CollectionPage', + url: 'https://example.com/.well-known/agent-descriptions?page=3', + items: page3Items, + // No next property - last page + }; + + vi.mocked(mockHttpClient.get) + .mockResolvedValueOnce({ + ok: true, + json: async () => page1, + } as Response) + .mockResolvedValueOnce({ + ok: true, + json: async () => page2, + } as Response) + .mockResolvedValueOnce({ + ok: true, + json: async () => page3, + } as Response); + + const result = await discoveryManager.discoverAgents(domain); + + expect(result).toHaveLength(3); + expect(result[0].name).toBe('Agent 1'); + expect(result[1].name).toBe('Agent 2'); + expect(result[2].name).toBe('Agent 3'); + expect(mockHttpClient.get).toHaveBeenCalledTimes(3); + }); + + it('should handle empty discovery document', async () => { + const domain = 'example.com'; + + const mockDocument: DiscoveryDocument = { + '@context': { ad: 'https://anp.org/ad#' }, + '@type': 'CollectionPage', + url: 'https://example.com/.well-known/agent-descriptions', + items: [], + }; + + vi.mocked(mockHttpClient.get).mockResolvedValue({ + ok: true, + json: async () => mockDocument, + } as Response); + + const result = await discoveryManager.discoverAgents(domain); + + expect(result).toEqual([]); + expect(result).toHaveLength(0); + }); + + it('should handle 404 error gracefully', async () => { + const domain = 'example.com'; + + vi.mocked(mockHttpClient.get).mockRejectedValue( + new NetworkError('HTTP 404: Not Found', 404) + ); + + await expect(discoveryManager.discoverAgents(domain)).rejects.toThrow( + NetworkError + ); + await expect(discoveryManager.discoverAgents(domain)).rejects.toThrow( + 'HTTP 404: Not Found' + ); + }); + + it('should handle network errors gracefully', async () => { + const domain = 'example.com'; + + vi.mocked(mockHttpClient.get).mockRejectedValue( + new NetworkError('Network request failed') + ); + + await expect(discoveryManager.discoverAgents(domain)).rejects.toThrow( + NetworkError + ); + }); + + it('should handle invalid JSON response', async () => { + const domain = 'example.com'; + + vi.mocked(mockHttpClient.get).mockResolvedValue({ + ok: true, + json: async () => { + throw new Error('Invalid JSON'); + }, + } as unknown as Response); + + await expect(discoveryManager.discoverAgents(domain)).rejects.toThrow(); + }); + + it('should handle malformed discovery document', async () => { + const domain = 'example.com'; + + // Missing required fields + const malformedDocument = { + '@context': { ad: 'https://anp.org/ad#' }, + // Missing @type, url, items + }; + + vi.mocked(mockHttpClient.get).mockResolvedValue({ + ok: true, + json: async () => malformedDocument, + } as Response); + + await expect(discoveryManager.discoverAgents(domain)).rejects.toThrow(); + }); + }); + + describe('registerWithSearchService', () => { + it('should construct registration request with agent description URL', async () => { + const searchServiceUrl = 'https://search.example.com/register'; + const agentDescriptionUrl = 'https://agent.example.com/description'; + + const mockIdentity = { + did: 'did:wba:example.com:agent1', + document: {} as any, + privateKeys: new Map(), + }; + + vi.mocked(mockHttpClient.post).mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ success: true }), + } as Response); + + await discoveryManager.registerWithSearchService( + searchServiceUrl, + agentDescriptionUrl, + mockIdentity + ); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + searchServiceUrl, + { agentDescriptionUrl }, + mockIdentity + ); + }); + + it('should use authentication when registering', async () => { + const searchServiceUrl = 'https://search.example.com/register'; + const agentDescriptionUrl = 'https://agent.example.com/description'; + + const mockIdentity = { + did: 'did:wba:example.com:agent1', + document: {} as any, + privateKeys: new Map(), + }; + + vi.mocked(mockHttpClient.post).mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ success: true }), + } as Response); + + await discoveryManager.registerWithSearchService( + searchServiceUrl, + agentDescriptionUrl, + mockIdentity + ); + + // Verify that identity was passed for authentication + expect(mockHttpClient.post).toHaveBeenCalledWith( + searchServiceUrl, + expect.any(Object), + mockIdentity + ); + }); + + it('should handle registration success', async () => { + const searchServiceUrl = 'https://search.example.com/register'; + const agentDescriptionUrl = 'https://agent.example.com/description'; + + const mockIdentity = { + did: 'did:wba:example.com:agent1', + document: {} as any, + privateKeys: new Map(), + }; + + vi.mocked(mockHttpClient.post).mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ success: true }), + } as Response); + + await expect( + discoveryManager.registerWithSearchService( + searchServiceUrl, + agentDescriptionUrl, + mockIdentity + ) + ).resolves.not.toThrow(); + }); + + it('should handle registration failure with 4xx error', async () => { + const searchServiceUrl = 'https://search.example.com/register'; + const agentDescriptionUrl = 'https://agent.example.com/description'; + + const mockIdentity = { + did: 'did:wba:example.com:agent1', + document: {} as any, + privateKeys: new Map(), + }; + + vi.mocked(mockHttpClient.post).mockRejectedValue( + new NetworkError('HTTP 400: Bad Request', 400) + ); + + await expect( + discoveryManager.registerWithSearchService( + searchServiceUrl, + agentDescriptionUrl, + mockIdentity + ) + ).rejects.toThrow(NetworkError); + await expect( + discoveryManager.registerWithSearchService( + searchServiceUrl, + agentDescriptionUrl, + mockIdentity + ) + ).rejects.toThrow('HTTP 400: Bad Request'); + }); + + it('should handle network errors during registration', async () => { + const searchServiceUrl = 'https://search.example.com/register'; + const agentDescriptionUrl = 'https://agent.example.com/description'; + + const mockIdentity = { + did: 'did:wba:example.com:agent1', + document: {} as any, + privateKeys: new Map(), + }; + + vi.mocked(mockHttpClient.post).mockRejectedValue( + new NetworkError('Network request failed') + ); + + await expect( + discoveryManager.registerWithSearchService( + searchServiceUrl, + agentDescriptionUrl, + mockIdentity + ) + ).rejects.toThrow(NetworkError); + }); + + it('should handle authentication errors during registration', async () => { + const searchServiceUrl = 'https://search.example.com/register'; + const agentDescriptionUrl = 'https://agent.example.com/description'; + + const mockIdentity = { + did: 'did:wba:example.com:agent1', + document: {} as any, + privateKeys: new Map(), + }; + + vi.mocked(mockHttpClient.post).mockRejectedValue( + new NetworkError('HTTP 401: Unauthorized', 401) + ); + + await expect( + discoveryManager.registerWithSearchService( + searchServiceUrl, + agentDescriptionUrl, + mockIdentity + ) + ).rejects.toThrow(NetworkError); + await expect( + discoveryManager.registerWithSearchService( + searchServiceUrl, + agentDescriptionUrl, + mockIdentity + ) + ).rejects.toThrow('HTTP 401: Unauthorized'); + }); + }); + + describe('searchAgents', () => { + it('should construct search query with keywords', async () => { + const searchServiceUrl = 'https://search.example.com/search'; + const query = { + keywords: ['weather', 'forecast'], + }; + + const mockResults: AgentDescriptionItem[] = [ + { + '@type': 'ad:AgentDescription', + name: 'Weather Agent', + '@id': 'https://weather.example.com/description', + }, + ]; + + vi.mocked(mockHttpClient.post).mockResolvedValue({ + ok: true, + json: async () => ({ items: mockResults }), + } as Response); + + const results = await discoveryManager.searchAgents( + searchServiceUrl, + query + ); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + searchServiceUrl, + query, + undefined + ); + expect(results).toEqual(mockResults); + }); + + it('should construct search query with capabilities', async () => { + const searchServiceUrl = 'https://search.example.com/search'; + const query = { + capabilities: ['weather-api', 'forecast'], + }; + + const mockResults: AgentDescriptionItem[] = [ + { + '@type': 'ad:AgentDescription', + name: 'Weather Agent', + '@id': 'https://weather.example.com/description', + }, + ]; + + vi.mocked(mockHttpClient.post).mockResolvedValue({ + ok: true, + json: async () => ({ items: mockResults }), + } as Response); + + const results = await discoveryManager.searchAgents( + searchServiceUrl, + query + ); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + searchServiceUrl, + query, + undefined + ); + expect(results).toEqual(mockResults); + }); + + it('should construct search query with limit and offset', async () => { + const searchServiceUrl = 'https://search.example.com/search'; + const query = { + keywords: ['weather'], + limit: 10, + offset: 20, + }; + + const mockResults: AgentDescriptionItem[] = []; + + vi.mocked(mockHttpClient.post).mockResolvedValue({ + ok: true, + json: async () => ({ items: mockResults }), + } as Response); + + await discoveryManager.searchAgents(searchServiceUrl, query); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + searchServiceUrl, + query, + undefined + ); + }); + + it('should parse search results correctly', async () => { + const searchServiceUrl = 'https://search.example.com/search'; + const query = { + keywords: ['agent'], + }; + + const mockResults: AgentDescriptionItem[] = [ + { + '@type': 'ad:AgentDescription', + name: 'Agent 1', + '@id': 'https://agent1.example.com/description', + }, + { + '@type': 'ad:AgentDescription', + name: 'Agent 2', + '@id': 'https://agent2.example.com/description', + }, + ]; + + vi.mocked(mockHttpClient.post).mockResolvedValue({ + ok: true, + json: async () => ({ items: mockResults, total: 2 }), + } as Response); + + const results = await discoveryManager.searchAgents( + searchServiceUrl, + query + ); + + expect(results).toEqual(mockResults); + expect(results).toHaveLength(2); + expect(results[0].name).toBe('Agent 1'); + expect(results[1].name).toBe('Agent 2'); + }); + + it('should handle empty search results', async () => { + const searchServiceUrl = 'https://search.example.com/search'; + const query = { + keywords: ['nonexistent'], + }; + + vi.mocked(mockHttpClient.post).mockResolvedValue({ + ok: true, + json: async () => ({ items: [] }), + } as Response); + + const results = await discoveryManager.searchAgents( + searchServiceUrl, + query + ); + + expect(results).toEqual([]); + expect(results).toHaveLength(0); + }); + + it('should use authentication when provided', async () => { + const searchServiceUrl = 'https://search.example.com/search'; + const query = { + keywords: ['weather'], + }; + + const mockIdentity = { + did: 'did:wba:example.com:agent1', + document: {} as any, + privateKeys: new Map(), + }; + + const mockResults: AgentDescriptionItem[] = []; + + vi.mocked(mockHttpClient.post).mockResolvedValue({ + ok: true, + json: async () => ({ items: mockResults }), + } as Response); + + await discoveryManager.searchAgents( + searchServiceUrl, + query, + mockIdentity + ); + + expect(mockHttpClient.post).toHaveBeenCalledWith( + searchServiceUrl, + query, + mockIdentity + ); + }); + + it('should handle search errors gracefully', async () => { + const searchServiceUrl = 'https://search.example.com/search'; + const query = { + keywords: ['weather'], + }; + + vi.mocked(mockHttpClient.post).mockRejectedValue( + new NetworkError('HTTP 500: Internal Server Error', 500) + ); + + await expect( + discoveryManager.searchAgents(searchServiceUrl, query) + ).rejects.toThrow(NetworkError); + }); + + it('should handle invalid search response', async () => { + const searchServiceUrl = 'https://search.example.com/search'; + const query = { + keywords: ['weather'], + }; + + vi.mocked(mockHttpClient.post).mockResolvedValue({ + ok: true, + json: async () => { + throw new Error('Invalid JSON'); + }, + } as unknown as Response); + + await expect( + discoveryManager.searchAgents(searchServiceUrl, query) + ).rejects.toThrow(); + }); + + it('should handle malformed search response', async () => { + const searchServiceUrl = 'https://search.example.com/search'; + const query = { + keywords: ['weather'], + }; + + // Missing items array + vi.mocked(mockHttpClient.post).mockResolvedValue({ + ok: true, + json: async () => ({ total: 0 }), + } as Response); + + await expect( + discoveryManager.searchAgents(searchServiceUrl, query) + ).rejects.toThrow(); + }); + }); +}); diff --git a/typescript/ts_sdk/tests/unit/core/auth/authentication-manager.test.ts b/typescript/ts_sdk/tests/unit/core/auth/authentication-manager.test.ts new file mode 100644 index 0000000..5d7cbac --- /dev/null +++ b/typescript/ts_sdk/tests/unit/core/auth/authentication-manager.test.ts @@ -0,0 +1,646 @@ +/** + * Unit tests for Authentication Manager + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { AuthenticationManager } from '../../../../src/core/auth/authentication-manager.js'; +import { DIDManager } from '../../../../src/core/did/did-manager.js'; +import type { DIDIdentity } from '../../../../src/types/index.js'; + +describe('AuthenticationManager', () => { + let authManager: AuthenticationManager; + let didManager: DIDManager; + let testIdentity: DIDIdentity; + + beforeEach(async () => { + didManager = new DIDManager(); + authManager = new AuthenticationManager(didManager, { + maxTokenAge: 3600000, // 1 hour + nonceLength: 32, + clockSkewTolerance: 60, // 60 seconds + }); + + // Create a test identity + testIdentity = await didManager.createDID({ + domain: 'example.com', + path: 'user/alice', + }); + }); + + describe('generateAuthHeader', () => { + it('should generate auth header with correct format', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + // Verify header starts with DIDWba + expect(authHeader).toMatch(/^DIDWba /); + + // Verify header contains required fields + expect(authHeader).toContain('did='); + expect(authHeader).toContain('nonce='); + expect(authHeader).toContain('timestamp='); + expect(authHeader).toContain('verification_method='); + expect(authHeader).toContain('signature='); + + // Verify DID is properly quoted + expect(authHeader).toContain(`did="${testIdentity.did}"`); + + // Verify verification method is just the fragment (without #) + expect(authHeader).toContain('verification_method="auth-key"'); + }); + + it('should generate unique nonces for each request', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + const header1 = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + const header2 = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + // Extract nonces from headers + const nonce1Match = header1.match(/nonce="([^"]+)"/); + const nonce2Match = header2.match(/nonce="([^"]+)"/); + + expect(nonce1Match).toBeTruthy(); + expect(nonce2Match).toBeTruthy(); + expect(nonce1Match![1]).not.toBe(nonce2Match![1]); + }); + + it('should generate timestamp in ISO 8601 format', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + // Extract timestamp + const timestampMatch = authHeader.match(/timestamp="([^"]+)"/); + expect(timestampMatch).toBeTruthy(); + + const timestamp = timestampMatch![1]; + + // Verify ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ) + expect(timestamp).toMatch( + /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/ + ); + + // Verify timestamp is recent (within last 5 seconds) + const timestampDate = new Date(timestamp); + const now = new Date(); + const diff = now.getTime() - timestampDate.getTime(); + expect(diff).toBeLessThan(5000); + expect(diff).toBeGreaterThanOrEqual(0); + }); + + it('should generate valid signature', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + // Extract signature + const signatureMatch = authHeader.match(/signature="([^"]+)"/); + expect(signatureMatch).toBeTruthy(); + + const signature = signatureMatch![1]; + + // Verify signature is base64url encoded (no +, /, or =) + expect(signature).toMatch(/^[A-Za-z0-9_-]+$/); + + // Verify signature has reasonable length (Ed25519 signatures are 64 bytes = 86 base64url chars) + expect(signature.length).toBeGreaterThan(50); + }); + + it('should use correct verification method', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + // Verify verification method is extracted correctly (fragment only) + expect(authHeader).toContain('verification_method="auth-key"'); + }); + + it('should throw error if verification method not found', async () => { + const targetDomain = 'service.example.com'; + const invalidMethodId = `${testIdentity.did}#invalid-key`; + + await expect( + authManager.generateAuthHeader( + testIdentity, + targetDomain, + invalidMethodId + ) + ).rejects.toThrow('Verification method not found'); + }); + + it('should include service domain in signature data', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + // Generate header + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + // The signature should be different for different target domains + const authHeader2 = await authManager.generateAuthHeader( + testIdentity, + 'different.example.com', + verificationMethodId + ); + + const sig1Match = authHeader.match(/signature="([^"]+)"/); + const sig2Match = authHeader2.match(/signature="([^"]+)"/); + + // Signatures should be different (different service domains) + // Note: nonces are also different, but this tests the service is included + expect(sig1Match![1]).not.toBe(sig2Match![1]); + }); + + it('should handle DID with port encoding', async () => { + const identityWithPort = await didManager.createDID({ + domain: 'example.com', + port: 8800, + path: 'user/bob', + }); + + const targetDomain = 'service.example.com'; + const verificationMethodId = `${identityWithPort.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + identityWithPort, + targetDomain, + verificationMethodId + ); + + // Verify DID is properly included + expect(authHeader).toContain(`did="${identityWithPort.did}"`); + expect(authHeader).toMatch(/^DIDWba /); + }); + }); + + describe('verifyAuthHeader', () => { + it('should successfully verify valid auth header', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + // Generate a valid auth header + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + // Mock DID resolution to return the test identity's document + vi.spyOn(didManager, 'resolveDID').mockResolvedValue( + testIdentity.document + ); + + // Verify the header + const result = await authManager.verifyAuthHeader( + authHeader, + targetDomain + ); + + expect(result.success).toBe(true); + expect(result.did).toBe(testIdentity.did); + expect(result.error).toBeUndefined(); + }); + + it('should resolve DID during verification', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + const resolveSpy = vi + .spyOn(didManager, 'resolveDID') + .mockResolvedValue(testIdentity.document); + + await authManager.verifyAuthHeader(authHeader, targetDomain); + + // Verify DID was resolved + expect(resolveSpy).toHaveBeenCalledWith(testIdentity.did); + }); + + it('should verify signature correctly', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + vi.spyOn(didManager, 'resolveDID').mockResolvedValue( + testIdentity.document + ); + + const result = await authManager.verifyAuthHeader( + authHeader, + targetDomain + ); + + expect(result.success).toBe(true); + }); + + it('should reject header with invalid signature', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + // Tamper with the signature + const tamperedHeader = authHeader.replace( + /signature="[^"]+"/, + 'signature="invalid_signature_data"' + ); + + vi.spyOn(didManager, 'resolveDID').mockResolvedValue( + testIdentity.document + ); + + const result = await authManager.verifyAuthHeader( + tamperedHeader, + targetDomain + ); + + expect(result.success).toBe(false); + expect(result.error).toBeDefined(); + expect(result.error).toContain('signature'); + }); + + it('should prevent nonce replay attacks', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + vi.spyOn(didManager, 'resolveDID').mockResolvedValue( + testIdentity.document + ); + + // First verification should succeed + const result1 = await authManager.verifyAuthHeader( + authHeader, + targetDomain + ); + expect(result1.success).toBe(true); + + // Second verification with same nonce should fail + const result2 = await authManager.verifyAuthHeader( + authHeader, + targetDomain + ); + expect(result2.success).toBe(false); + expect(result2.error?.toLowerCase()).toContain('nonce'); + }); + + it('should validate timestamp within clock skew tolerance', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + // Create auth header with current timestamp + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + vi.spyOn(didManager, 'resolveDID').mockResolvedValue( + testIdentity.document + ); + + // Should succeed within tolerance + const result = await authManager.verifyAuthHeader( + authHeader, + targetDomain + ); + expect(result.success).toBe(true); + }); + + it('should reject expired timestamp', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + // Generate header + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + // Replace timestamp with old one (2 minutes ago, beyond 60 second tolerance) + const oldTimestamp = new Date(Date.now() - 120000).toISOString(); + const expiredHeader = authHeader.replace( + /timestamp="[^"]+"/, + `timestamp="${oldTimestamp}"` + ); + + vi.spyOn(didManager, 'resolveDID').mockResolvedValue( + testIdentity.document + ); + + const result = await authManager.verifyAuthHeader( + expiredHeader, + targetDomain + ); + + expect(result.success).toBe(false); + expect(result.error).toContain('timestamp'); + }); + + it('should reject future timestamp beyond clock skew', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + // Generate header + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + // Replace timestamp with future one (2 minutes ahead, beyond 60 second tolerance) + const futureTimestamp = new Date(Date.now() + 120000).toISOString(); + const futureHeader = authHeader.replace( + /timestamp="[^"]+"/, + `timestamp="${futureTimestamp}"` + ); + + vi.spyOn(didManager, 'resolveDID').mockResolvedValue( + testIdentity.document + ); + + const result = await authManager.verifyAuthHeader( + futureHeader, + targetDomain + ); + + expect(result.success).toBe(false); + expect(result.error).toContain('timestamp'); + }); + + it('should reject malformed auth header', async () => { + const malformedHeader = 'DIDWba invalid_format'; + + const result = await authManager.verifyAuthHeader( + malformedHeader, + 'service.example.com' + ); + + expect(result.success).toBe(false); + expect(result.error).toBeDefined(); + }); + + it('should reject header with missing fields', async () => { + const incompleteHeader = + 'DIDWba did="did:wba:example.com", nonce="abc123"'; + + const result = await authManager.verifyAuthHeader( + incompleteHeader, + 'service.example.com' + ); + + expect(result.success).toBe(false); + expect(result.error).toBeDefined(); + }); + + it('should handle DID resolution failure', async () => { + const targetDomain = 'service.example.com'; + const verificationMethodId = `${testIdentity.did}#auth-key`; + + const authHeader = await authManager.generateAuthHeader( + testIdentity, + targetDomain, + verificationMethodId + ); + + // Mock DID resolution to fail + vi.spyOn(didManager, 'resolveDID').mockRejectedValue( + new Error('DID resolution failed') + ); + + const result = await authManager.verifyAuthHeader( + authHeader, + targetDomain + ); + + expect(result.success).toBe(false); + expect(result.error).toContain('DID resolution'); + }); + }); + + describe('generateAccessToken', () => { + it('should generate access token with correct format', () => { + const did = testIdentity.did; + const expiresIn = 3600000; // 1 hour + + const token = authManager.generateAccessToken(did, expiresIn); + + // Token should be a non-empty string + expect(token).toBeTruthy(); + expect(typeof token).toBe('string'); + + // Token should have JWT-like format (3 parts separated by dots) + const parts = token.split('.'); + expect(parts.length).toBe(3); + + // Each part should be base64url encoded + parts.forEach((part) => { + expect(part).toMatch(/^[A-Za-z0-9_-]+$/); + }); + }); + + it('should include DID in token', () => { + const did = testIdentity.did; + const expiresIn = 3600000; + + const token = authManager.generateAccessToken(did, expiresIn); + + // Decode payload (second part) + const parts = token.split('.'); + const payloadBase64 = parts[1]; + const payloadJson = atob( + payloadBase64.replace(/-/g, '+').replace(/_/g, '/') + ); + const payload = JSON.parse(payloadJson); + + expect(payload.did).toBe(did); + }); + + it('should include expiration time in token', () => { + const did = testIdentity.did; + const expiresIn = 3600000; // 1 hour + + const token = authManager.generateAccessToken(did, expiresIn); + + // Decode payload + const parts = token.split('.'); + const payloadBase64 = parts[1]; + const payloadJson = atob( + payloadBase64.replace(/-/g, '+').replace(/_/g, '/') + ); + const payload = JSON.parse(payloadJson); + + expect(payload.exp).toBeDefined(); + expect(typeof payload.exp).toBe('number'); + + // Expiration should be approximately now + expiresIn + const expectedExp = Math.floor((Date.now() + expiresIn) / 1000); + expect(Math.abs(payload.exp - expectedExp)).toBeLessThan(2); // Within 2 seconds + }); + + it('should include issued at time in token', () => { + const did = testIdentity.did; + const expiresIn = 3600000; + + const token = authManager.generateAccessToken(did, expiresIn); + + // Decode payload + const parts = token.split('.'); + const payloadBase64 = parts[1]; + const payloadJson = atob( + payloadBase64.replace(/-/g, '+').replace(/_/g, '/') + ); + const payload = JSON.parse(payloadJson); + + expect(payload.iat).toBeDefined(); + expect(typeof payload.iat).toBe('number'); + + // Issued at should be approximately now + const expectedIat = Math.floor(Date.now() / 1000); + expect(Math.abs(payload.iat - expectedIat)).toBeLessThan(2); + }); + }); + + describe('verifyAccessToken', () => { + it('should verify valid token', () => { + const did = testIdentity.did; + const expiresIn = 3600000; + + const token = authManager.generateAccessToken(did, expiresIn); + const result = authManager.verifyAccessToken(token); + + expect(result.valid).toBe(true); + expect(result.did).toBe(did); + expect(result.expiresAt).toBeDefined(); + expect(result.error).toBeUndefined(); + }); + + it('should reject expired token', () => { + const did = testIdentity.did; + const expiresIn = -1000; // Already expired + + const token = authManager.generateAccessToken(did, expiresIn); + const result = authManager.verifyAccessToken(token); + + expect(result.valid).toBe(false); + expect(result.error).toBeDefined(); + expect(result.error?.toLowerCase()).toContain('expired'); + }); + + it('should reject malformed token', () => { + const malformedToken = 'not.a.valid.token'; + + const result = authManager.verifyAccessToken(malformedToken); + + expect(result.valid).toBe(false); + expect(result.error).toBeDefined(); + }); + + it('should reject token with invalid signature', () => { + const did = testIdentity.did; + const expiresIn = 3600000; + + const token = authManager.generateAccessToken(did, expiresIn); + + // Tamper with the signature + const parts = token.split('.'); + const tamperedToken = `${parts[0]}.${parts[1]}.invalid_signature`; + + const result = authManager.verifyAccessToken(tamperedToken); + + expect(result.valid).toBe(false); + expect(result.error).toBeDefined(); + }); + + it('should reject token with missing required fields', () => { + // Create a token without required fields + const header = { alg: 'HS256', typ: 'JWT' }; + const payload = { someField: 'value' }; // Missing did, exp, iat + + const headerBase64 = btoa(JSON.stringify(header)) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + const payloadBase64 = btoa(JSON.stringify(payload)) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + + const invalidToken = `${headerBase64}.${payloadBase64}.signature`; + + const result = authManager.verifyAccessToken(invalidToken); + + expect(result.valid).toBe(false); + expect(result.error).toBeDefined(); + }); + + it('should return expiration timestamp', () => { + const did = testIdentity.did; + const expiresIn = 3600000; + + const token = authManager.generateAccessToken(did, expiresIn); + const result = authManager.verifyAccessToken(token); + + expect(result.valid).toBe(true); + expect(result.expiresAt).toBeDefined(); + expect(typeof result.expiresAt).toBe('number'); + + // Expiration should be in the future + expect(result.expiresAt!).toBeGreaterThan(Date.now()); + }); + }); +}); diff --git a/typescript/ts_sdk/tests/unit/core/did/did-manager.test.ts b/typescript/ts_sdk/tests/unit/core/did/did-manager.test.ts new file mode 100644 index 0000000..4a1b32d --- /dev/null +++ b/typescript/ts_sdk/tests/unit/core/did/did-manager.test.ts @@ -0,0 +1,637 @@ +/** + * Unit tests for DID Manager functionality + */ + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { DIDManager } from '../../../../src/core/did/did-manager.js'; +import { DIDResolutionError } from '../../../../src/errors/index.js'; +import type { DIDDocument, DIDIdentity } from '../../../../src/types/index.js'; + +describe('DID Manager', () => { + let didManager: DIDManager; + + beforeEach(() => { + didManager = new DIDManager(); + }); + + describe('DID Creation', () => { + describe('DID identifier construction from domain', () => { + it('should construct a valid DID identifier from domain', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + + expect(identity.did).toBe('did:wba:example.com'); + expect(identity.document.id).toBe('did:wba:example.com'); + }); + + it('should handle domains with subdomains', async () => { + const identity = await didManager.createDID({ + domain: 'api.example.com', + }); + + expect(identity.did).toBe('did:wba:api.example.com'); + expect(identity.document.id).toBe('did:wba:api.example.com'); + }); + + it('should normalize domain to lowercase', async () => { + const identity = await didManager.createDID({ + domain: 'Example.COM', + }); + + expect(identity.did).toBe('did:wba:example.com'); + }); + }); + + describe('DID identifier construction with path', () => { + it('should construct DID identifier with path', async () => { + const identity = await didManager.createDID({ + domain: 'example.com', + path: 'agent1', + }); + + expect(identity.did).toBe('did:wba:example.com:agent1'); + expect(identity.document.id).toBe('did:wba:example.com:agent1'); + }); + + it('should handle path with multiple segments', async () => { + const identity = await didManager.createDID({ + domain: 'example.com', + path: 'agents/agent1', + }); + + expect(identity.did).toBe('did:wba:example.com:agents%2Fagent1'); + }); + + it('should encode special characters in path', async () => { + const identity = await didManager.createDID({ + domain: 'example.com', + path: 'agent name', + }); + + expect(identity.did).toBe('did:wba:example.com:agent%20name'); + }); + }); + + describe('DID identifier with port encoding', () => { + it('should encode port in DID identifier', async () => { + const identity = await didManager.createDID({ + domain: 'example.com', + port: 8080, + }); + + expect(identity.did).toBe('did:wba:example.com%3A8080'); + }); + + it('should encode port with path', async () => { + const identity = await didManager.createDID({ + domain: 'example.com', + port: 8080, + path: 'agent1', + }); + + expect(identity.did).toBe('did:wba:example.com%3A8080:agent1'); + }); + + it('should not encode standard HTTPS port 443', async () => { + const identity = await didManager.createDID({ + domain: 'example.com', + port: 443, + }); + + expect(identity.did).toBe('did:wba:example.com'); + }); + }); + + describe('DID document generation', () => { + it('should generate a valid DID document', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + const doc = identity.document; + + expect(doc['@context']).toEqual([ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/jws-2020/v1', + ]); + expect(doc.id).toBe('did:wba:example.com'); + expect(Array.isArray(doc.verificationMethod)).toBe(true); + expect(Array.isArray(doc.authentication)).toBe(true); + }); + + it('should include verificationMethod array', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + + expect(identity.document.verificationMethod).toBeDefined(); + expect(identity.document.verificationMethod.length).toBeGreaterThan(0); + }); + + it('should include authentication array', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + + expect(identity.document.authentication).toBeDefined(); + expect(identity.document.authentication.length).toBeGreaterThan(0); + }); + + it('should include keyAgreement array', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + + expect(identity.document.keyAgreement).toBeDefined(); + expect(identity.document.keyAgreement!.length).toBeGreaterThan(0); + }); + }); + + describe('Verification method creation', () => { + it('should create verification method for authentication', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + const authMethod = identity.document.verificationMethod.find((vm) => + vm.id.includes('#auth-key') + ); + + expect(authMethod).toBeDefined(); + expect(authMethod!.type).toBe('Ed25519VerificationKey2020'); + expect(authMethod!.controller).toBe('did:wba:example.com'); + expect(authMethod!.publicKeyJwk).toBeDefined(); + }); + + it('should create verification method for key agreement', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + const keyAgreementMethod = identity.document.verificationMethod.find( + (vm) => vm.id.includes('#key-agreement') + ); + + expect(keyAgreementMethod).toBeDefined(); + expect(keyAgreementMethod!.type).toBe('X25519KeyAgreementKey2019'); + expect(keyAgreementMethod!.controller).toBe('did:wba:example.com'); + expect(keyAgreementMethod!.publicKeyJwk).toBeDefined(); + }); + + it('should store private keys in identity', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + + expect(identity.privateKeys).toBeDefined(); + expect(identity.privateKeys.size).toBeGreaterThan(0); + }); + + it('should reference verification methods in authentication', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + const authMethodId = identity.document.verificationMethod.find((vm) => + vm.id.includes('#auth-key') + )?.id; + + expect(identity.document.authentication).toContain(authMethodId); + }); + + it('should reference verification methods in keyAgreement', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + const keyAgreementMethodId = + identity.document.verificationMethod.find((vm) => + vm.id.includes('#key-agreement') + )?.id; + + expect(identity.document.keyAgreement).toContain(keyAgreementMethodId); + }); + }); + + describe('Error handling for invalid domains', () => { + it('should throw error for empty domain', async () => { + await expect(didManager.createDID({ domain: '' })).rejects.toThrow( + 'Invalid domain' + ); + }); + + it('should throw error for domain with protocol', async () => { + await expect( + didManager.createDID({ domain: 'https://example.com' }) + ).rejects.toThrow('Invalid domain'); + }); + + it('should throw error for domain with invalid characters', async () => { + await expect( + didManager.createDID({ domain: 'example com' }) + ).rejects.toThrow('Invalid domain'); + }); + + it('should throw error for invalid port number', async () => { + await expect( + didManager.createDID({ domain: 'example.com', port: -1 }) + ).rejects.toThrow('Invalid port'); + }); + + it('should throw error for port exceeding maximum', async () => { + await expect( + didManager.createDID({ domain: 'example.com', port: 70000 }) + ).rejects.toThrow('Invalid port'); + }); + }); + }); + + describe('DID Resolution', () => { + describe('Resolution from .well-known path', () => { + it('should resolve DID from .well-known path', async () => { + const mockDoc: DIDDocument = { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com', + verificationMethod: [], + authentication: [], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => mockDoc, + }); + + const doc = await didManager.resolveDID('did:wba:example.com'); + + expect(doc).toEqual(mockDoc); + expect(global.fetch).toHaveBeenCalledWith( + 'https://example.com/.well-known/did.json', + expect.any(Object) + ); + }); + + it('should construct correct URL for domain-only DID', async () => { + const mockDoc: DIDDocument = { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com', + verificationMethod: [], + authentication: [], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => mockDoc, + }); + + await didManager.resolveDID('did:wba:example.com'); + + expect(global.fetch).toHaveBeenCalledWith( + 'https://example.com/.well-known/did.json', + expect.any(Object) + ); + }); + }); + + describe('Resolution from custom path', () => { + it('should resolve DID with custom path', async () => { + const mockDoc: DIDDocument = { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com:agent1', + verificationMethod: [], + authentication: [], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => mockDoc, + }); + + const doc = await didManager.resolveDID('did:wba:example.com:agent1'); + + expect(doc).toEqual(mockDoc); + expect(global.fetch).toHaveBeenCalledWith( + 'https://example.com/agent1/did.json', + expect.any(Object) + ); + }); + + it('should decode URL-encoded path segments', async () => { + const mockDoc: DIDDocument = { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com:agents%2Fagent1', + verificationMethod: [], + authentication: [], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => mockDoc, + }); + + await didManager.resolveDID('did:wba:example.com:agents%2Fagent1'); + + expect(global.fetch).toHaveBeenCalledWith( + 'https://example.com/agents/agent1/did.json', + expect.any(Object) + ); + }); + }); + + describe('Resolution with port', () => { + it('should resolve DID with encoded port', async () => { + const mockDoc: DIDDocument = { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com%3A8080', + verificationMethod: [], + authentication: [], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => mockDoc, + }); + + const doc = await didManager.resolveDID('did:wba:example.com%3A8080'); + + expect(doc).toEqual(mockDoc); + expect(global.fetch).toHaveBeenCalledWith( + 'https://example.com:8080/.well-known/did.json', + expect.any(Object) + ); + }); + + it('should resolve DID with port and path', async () => { + const mockDoc: DIDDocument = { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com%3A8080:agent1', + verificationMethod: [], + authentication: [], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => mockDoc, + }); + + await didManager.resolveDID('did:wba:example.com%3A8080:agent1'); + + expect(global.fetch).toHaveBeenCalledWith( + 'https://example.com:8080/agent1/did.json', + expect.any(Object) + ); + }); + }); + + describe('Caching mechanism', () => { + it('should cache resolved DID documents', async () => { + const mockDoc: DIDDocument = { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com', + verificationMethod: [], + authentication: [], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => mockDoc, + }); + + await didManager.resolveDID('did:wba:example.com'); + await didManager.resolveDID('did:wba:example.com'); + + expect(global.fetch).toHaveBeenCalledTimes(1); + }); + + it('should respect cache TTL', async () => { + const mockDoc: DIDDocument = { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com', + verificationMethod: [], + authentication: [], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => mockDoc, + }); + + const shortTTLManager = new DIDManager({ cacheTTL: 100 }); + await shortTTLManager.resolveDID('did:wba:example.com'); + + await new Promise((resolve) => setTimeout(resolve, 150)); + + await shortTTLManager.resolveDID('did:wba:example.com'); + + expect(global.fetch).toHaveBeenCalledTimes(2); + }); + + it('should allow bypassing cache', async () => { + const mockDoc: DIDDocument = { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com', + verificationMethod: [], + authentication: [], + }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => mockDoc, + }); + + await didManager.resolveDID('did:wba:example.com'); + await didManager.resolveDID('did:wba:example.com', { cache: false }); + + expect(global.fetch).toHaveBeenCalledTimes(2); + }); + }); + + describe('Error handling for 404 responses', () => { + it('should throw DIDResolutionError for 404 response', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 404, + statusText: 'Not Found', + }); + + await expect( + didManager.resolveDID('did:wba:example.com') + ).rejects.toThrow(DIDResolutionError); + }); + + it('should include DID in error message', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 404, + }); + + await expect( + didManager.resolveDID('did:wba:example.com') + ).rejects.toThrow('did:wba:example.com'); + }); + }); + + describe('Error handling for invalid documents', () => { + it('should throw error for document without id', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + '@context': ['https://www.w3.org/ns/did/v1'], + }), + }); + + await expect( + didManager.resolveDID('did:wba:example.com') + ).rejects.toThrow(DIDResolutionError); + }); + + it('should throw error for document with mismatched id', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:different.com', + verificationMethod: [], + authentication: [], + }), + }); + + await expect( + didManager.resolveDID('did:wba:example.com') + ).rejects.toThrow(DIDResolutionError); + }); + + it('should throw error for malformed JSON', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: async () => { + throw new Error('Invalid JSON'); + }, + }); + + await expect( + didManager.resolveDID('did:wba:example.com') + ).rejects.toThrow(DIDResolutionError); + }); + }); + }); + + describe('DID Operations', () => { + describe('Signing with DID identity', () => { + it('should sign data with DID identity', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + const data = new TextEncoder().encode('test message'); + + const signature = await didManager.sign(identity, data); + + expect(signature).toBeDefined(); + expect(signature.value).toBeInstanceOf(Uint8Array); + expect(signature.verificationMethod).toBeDefined(); + }); + + it('should include verification method in signature', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + const data = new TextEncoder().encode('test message'); + + const signature = await didManager.sign(identity, data); + + expect(signature.verificationMethod).toContain('#auth-key'); + }); + + it('should produce different signatures for different data', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + const data1 = new TextEncoder().encode('message 1'); + const data2 = new TextEncoder().encode('message 2'); + + const sig1 = await didManager.sign(identity, data1); + const sig2 = await didManager.sign(identity, data2); + + expect(sig1.value).not.toEqual(sig2.value); + }); + }); + + describe('Verification with resolved DID', () => { + it('should verify valid signature', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + const data = new TextEncoder().encode('test message'); + const signature = await didManager.sign(identity, data); + + const isValid = await didManager.verify( + identity.did, + data, + signature, + identity.document + ); + + expect(isValid).toBe(true); + }); + + it('should reject invalid signature', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + const data = new TextEncoder().encode('test message'); + const signature = await didManager.sign(identity, data); + + const tamperedData = new TextEncoder().encode('tampered message'); + + const isValid = await didManager.verify( + identity.did, + tamperedData, + signature, + identity.document + ); + + expect(isValid).toBe(false); + }); + + it('should reject signature with wrong verification method', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + const data = new TextEncoder().encode('test message'); + const signature = await didManager.sign(identity, data); + + signature.verificationMethod = 'did:wba:example.com#wrong-key'; + + await expect( + didManager.verify(identity.did, data, signature, identity.document) + ).rejects.toThrow('Verification method not found'); + }); + }); + + describe('Export of DID document', () => { + it('should export DID document', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + + const exported = didManager.exportDocument(identity); + + expect(exported).toEqual(identity.document); + }); + + it('should export document without private keys', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + + const exported = didManager.exportDocument(identity); + const exportedStr = JSON.stringify(exported); + + expect(exportedStr).not.toContain('privateKey'); + // Check that JWK doesn't contain the 'd' (private key) component + // by verifying no JWK object has a 'd' property + const jwks = exported.verificationMethod + .map((vm) => vm.publicKeyJwk) + .filter((jwk) => jwk !== undefined); + jwks.forEach((jwk) => { + expect(jwk).not.toHaveProperty('d'); + }); + }); + + it('should export valid JSON-LD document', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + + const exported = didManager.exportDocument(identity); + + expect(exported['@context']).toBeDefined(); + expect(Array.isArray(exported['@context'])).toBe(true); + }); + }); + + describe('Error handling for missing keys', () => { + it('should throw error when signing with missing private key', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + identity.privateKeys.clear(); + + const data = new TextEncoder().encode('test message'); + + await expect(didManager.sign(identity, data)).rejects.toThrow( + 'Private key not found' + ); + }); + + it('should throw error when verifying with missing verification method', async () => { + const identity = await didManager.createDID({ domain: 'example.com' }); + const data = new TextEncoder().encode('test message'); + + const signature = { + value: new Uint8Array(), + verificationMethod: 'did:wba:example.com#nonexistent', + }; + + await expect( + didManager.verify(identity.did, data, signature, identity.document) + ).rejects.toThrow('Verification method not found'); + }); + }); + }); +}); diff --git a/typescript/ts_sdk/tests/unit/crypto/encryption.test.ts b/typescript/ts_sdk/tests/unit/crypto/encryption.test.ts new file mode 100644 index 0000000..83447c2 --- /dev/null +++ b/typescript/ts_sdk/tests/unit/crypto/encryption.test.ts @@ -0,0 +1,299 @@ +/** + * Unit tests for encryption and decryption functionality + */ + +import { describe, it, expect } from 'vitest'; +import { + generateKeyPair, + KeyType, +} from '../../../src/crypto/key-generation.js'; +import { + performKeyExchange, + deriveKey, +} from '../../../src/crypto/key-exchange.js'; +import { + encrypt, + decrypt, + generateIV, +} from '../../../src/crypto/encryption.js'; +import { CryptoError } from '../../../src/errors/index.js'; + +describe('Encryption and Decryption', () => { + describe('AES-GCM encryption', () => { + it('should encrypt data with AES-GCM', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const key = await deriveKey(sharedSecret, salt); + + const plaintext = new TextEncoder().encode('Hello, World!'); + const encrypted = await encrypt(key, plaintext); + + expect(encrypted).toBeDefined(); + expect(encrypted.ciphertext).toBeInstanceOf(Uint8Array); + expect(encrypted.iv).toBeInstanceOf(Uint8Array); + expect(encrypted.tag).toBeInstanceOf(Uint8Array); + expect(encrypted.ciphertext.length).toBeGreaterThan(0); + expect(encrypted.iv.length).toBe(12); // Standard IV length for AES-GCM + expect(encrypted.tag.length).toBe(16); // Standard tag length for AES-GCM + }); + + it('should decrypt encrypted data', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const key = await deriveKey(sharedSecret, salt); + + const plaintext = new TextEncoder().encode('Hello, World!'); + const encrypted = await encrypt(key, plaintext); + const decrypted = await decrypt(key, encrypted); + + expect(decrypted).toEqual(plaintext); + expect(new TextDecoder().decode(decrypted)).toBe('Hello, World!'); + }); + + it('should produce different ciphertexts for same plaintext', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const key = await deriveKey(sharedSecret, salt); + + const plaintext = new TextEncoder().encode('Hello, World!'); + const encrypted1 = await encrypt(key, plaintext); + const encrypted2 = await encrypt(key, plaintext); + + // IVs should be different + expect(encrypted1.iv).not.toEqual(encrypted2.iv); + // Ciphertexts should be different due to different IVs + expect(encrypted1.ciphertext).not.toEqual(encrypted2.ciphertext); + }); + + it('should handle empty plaintext', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const key = await deriveKey(sharedSecret, salt); + + const plaintext = new Uint8Array(0); + const encrypted = await encrypt(key, plaintext); + const decrypted = await decrypt(key, encrypted); + + expect(decrypted).toEqual(plaintext); + expect(decrypted.length).toBe(0); + }); + + it('should handle large plaintext', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const key = await deriveKey(sharedSecret, salt); + + // Create 64KB of data (max for crypto.getRandomValues) + const plaintext = crypto.getRandomValues(new Uint8Array(65536)); + const encrypted = await encrypt(key, plaintext); + const decrypted = await decrypt(key, encrypted); + + expect(decrypted).toEqual(plaintext); + }); + }); + + describe('IV generation', () => { + it('should generate random IV', () => { + const iv = generateIV(); + + expect(iv).toBeDefined(); + expect(iv).toBeInstanceOf(Uint8Array); + expect(iv.length).toBe(12); + }); + + it('should generate different IVs on each call', () => { + const iv1 = generateIV(); + const iv2 = generateIV(); + + expect(iv1).not.toEqual(iv2); + }); + + it('should generate cryptographically random IVs', () => { + const ivs = new Set(); + + // Generate 100 IVs and check for uniqueness + for (let i = 0; i < 100; i++) { + const iv = generateIV(); + const ivStr = Array.from(iv).join(','); + ivs.add(ivStr); + } + + // All IVs should be unique + expect(ivs.size).toBe(100); + }); + }); + + describe('Authentication tag validation', () => { + it('should reject tampered ciphertext', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const key = await deriveKey(sharedSecret, salt); + + const plaintext = new TextEncoder().encode('Hello, World!'); + const encrypted = await encrypt(key, plaintext); + + // Tamper with ciphertext + encrypted.ciphertext[0] ^= 0xff; + + await expect(decrypt(key, encrypted)).rejects.toThrow(CryptoError); + }); + + it('should reject tampered IV', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const key = await deriveKey(sharedSecret, salt); + + const plaintext = new TextEncoder().encode('Hello, World!'); + const encrypted = await encrypt(key, plaintext); + + // Tamper with IV + encrypted.iv[0] ^= 0xff; + + await expect(decrypt(key, encrypted)).rejects.toThrow(CryptoError); + }); + + it('should reject tampered authentication tag', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const key = await deriveKey(sharedSecret, salt); + + const plaintext = new TextEncoder().encode('Hello, World!'); + const encrypted = await encrypt(key, plaintext); + + // Tamper with tag + encrypted.tag[0] ^= 0xff; + + await expect(decrypt(key, encrypted)).rejects.toThrow(CryptoError); + }); + }); + + describe('Error handling', () => { + it('should throw CryptoError when encrypting with invalid key', async () => { + const invalidKey = {} as CryptoKey; + const plaintext = new TextEncoder().encode('Hello, World!'); + + await expect(encrypt(invalidKey, plaintext)).rejects.toThrow(CryptoError); + }); + + it('should throw CryptoError when decrypting with invalid key', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const key = await deriveKey(sharedSecret, salt); + + const plaintext = new TextEncoder().encode('Hello, World!'); + const encrypted = await encrypt(key, plaintext); + + const invalidKey = {} as CryptoKey; + await expect(decrypt(invalidKey, encrypted)).rejects.toThrow(CryptoError); + }); + + it('should throw CryptoError when decrypting with wrong key', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + const charlieKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret1 = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const sharedSecret2 = await performKeyExchange( + aliceKeyPair.privateKey, + charlieKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const key1 = await deriveKey(sharedSecret1, salt); + const key2 = await deriveKey(sharedSecret2, salt); + + const plaintext = new TextEncoder().encode('Hello, World!'); + const encrypted = await encrypt(key1, plaintext); + + await expect(decrypt(key2, encrypted)).rejects.toThrow(CryptoError); + }); + + it('should throw CryptoError with invalid IV length', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const key = await deriveKey(sharedSecret, salt); + + const plaintext = new TextEncoder().encode('Hello, World!'); + const encrypted = await encrypt(key, plaintext); + + // Use invalid IV length + encrypted.iv = new Uint8Array(8); + + await expect(decrypt(key, encrypted)).rejects.toThrow(CryptoError); + }); + }); +}); diff --git a/typescript/ts_sdk/tests/unit/crypto/key-exchange.test.ts b/typescript/ts_sdk/tests/unit/crypto/key-exchange.test.ts new file mode 100644 index 0000000..01668e9 --- /dev/null +++ b/typescript/ts_sdk/tests/unit/crypto/key-exchange.test.ts @@ -0,0 +1,208 @@ +/** + * Unit tests for ECDHE key exchange functionality + */ + +import { describe, it, expect } from 'vitest'; +import { + generateKeyPair, + KeyType, +} from '../../../src/crypto/key-generation.js'; +import { + performKeyExchange, + deriveKey, +} from '../../../src/crypto/key-exchange.js'; +import { CryptoError } from '../../../src/errors/index.js'; + +describe('ECDHE Key Exchange', () => { + describe('X25519 key exchange', () => { + it('should perform key exchange with X25519 keys', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const aliceSharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + expect(aliceSharedSecret).toBeDefined(); + expect(aliceSharedSecret).toBeInstanceOf(Uint8Array); + expect(aliceSharedSecret.length).toBe(32); // X25519 shared secrets are 32 bytes + }); + + it('should produce same shared secret for both parties', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const aliceSharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const bobSharedSecret = await performKeyExchange( + bobKeyPair.privateKey, + aliceKeyPair.publicKey + ); + + expect(aliceSharedSecret).toEqual(bobSharedSecret); + }); + + it('should produce different shared secrets for different key pairs', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + const charlieKeyPair = await generateKeyPair(KeyType.X25519); + + const aliceBobSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const aliceCharlieSecret = await performKeyExchange( + aliceKeyPair.privateKey, + charlieKeyPair.publicKey + ); + + expect(aliceBobSecret).not.toEqual(aliceCharlieSecret); + }); + }); + + describe('Key derivation', () => { + it('should derive encryption key from shared secret', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + const derivedKey = await deriveKey(sharedSecret, salt); + + expect(derivedKey).toBeDefined(); + expect(derivedKey.type).toBe('secret'); + expect(derivedKey.algorithm.name).toBe('AES-GCM'); + }); + + it('should derive same key with same inputs', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + + const key1 = await deriveKey(sharedSecret, salt); + const key2 = await deriveKey(sharedSecret, salt); + + // Export both keys to compare + const exported1 = await crypto.subtle.exportKey('raw', key1); + const exported2 = await crypto.subtle.exportKey('raw', key2); + + expect(new Uint8Array(exported1)).toEqual(new Uint8Array(exported2)); + }); + + it('should derive different keys with different salts', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const salt1 = crypto.getRandomValues(new Uint8Array(32)); + const salt2 = crypto.getRandomValues(new Uint8Array(32)); + + const key1 = await deriveKey(sharedSecret, salt1); + const key2 = await deriveKey(sharedSecret, salt2); + + // Export both keys to compare + const exported1 = await crypto.subtle.exportKey('raw', key1); + const exported2 = await crypto.subtle.exportKey('raw', key2); + + expect(new Uint8Array(exported1)).not.toEqual(new Uint8Array(exported2)); + }); + + it('should derive different keys from different shared secrets', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + const charlieKeyPair = await generateKeyPair(KeyType.X25519); + + const secret1 = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const secret2 = await performKeyExchange( + aliceKeyPair.privateKey, + charlieKeyPair.publicKey + ); + + const salt = crypto.getRandomValues(new Uint8Array(32)); + + const key1 = await deriveKey(secret1, salt); + const key2 = await deriveKey(secret2, salt); + + // Export both keys to compare + const exported1 = await crypto.subtle.exportKey('raw', key1); + const exported2 = await crypto.subtle.exportKey('raw', key2); + + expect(new Uint8Array(exported1)).not.toEqual(new Uint8Array(exported2)); + }); + }); + + describe('Error handling', () => { + it('should throw CryptoError when using non-X25519 key for exchange', async () => { + const ed25519KeyPair = await generateKeyPair(KeyType.ED25519); + const x25519KeyPair = await generateKeyPair(KeyType.X25519); + + await expect( + performKeyExchange(ed25519KeyPair.privateKey, x25519KeyPair.publicKey) + ).rejects.toThrow(CryptoError); + }); + + it('should throw CryptoError when using public key as private key', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + await expect( + performKeyExchange(aliceKeyPair.publicKey as any, bobKeyPair.publicKey) + ).rejects.toThrow(CryptoError); + }); + + it('should throw CryptoError when using private key as public key', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + await expect( + performKeyExchange(aliceKeyPair.privateKey, bobKeyPair.privateKey as any) + ).rejects.toThrow(CryptoError); + }); + + it('should throw CryptoError when deriving key with invalid shared secret', async () => { + const invalidSecret = new Uint8Array(16); // Wrong size + const salt = crypto.getRandomValues(new Uint8Array(32)); + + await expect(deriveKey(invalidSecret, salt)).rejects.toThrow(CryptoError); + }); + + it('should throw CryptoError when deriving key with invalid salt', async () => { + const aliceKeyPair = await generateKeyPair(KeyType.X25519); + const bobKeyPair = await generateKeyPair(KeyType.X25519); + + const sharedSecret = await performKeyExchange( + aliceKeyPair.privateKey, + bobKeyPair.publicKey + ); + + const invalidSalt = new Uint8Array(0); // Empty salt + + await expect(deriveKey(sharedSecret, invalidSalt)).rejects.toThrow( + CryptoError + ); + }); + }); +}); diff --git a/typescript/ts_sdk/tests/unit/crypto/key-generation.test.ts b/typescript/ts_sdk/tests/unit/crypto/key-generation.test.ts new file mode 100644 index 0000000..e026b02 --- /dev/null +++ b/typescript/ts_sdk/tests/unit/crypto/key-generation.test.ts @@ -0,0 +1,194 @@ +/** + * Unit tests for key generation functionality + */ + +import { describe, it, expect } from 'vitest'; +import { + generateKeyPair, + KeyType, + exportPublicKeyJWK, + exportPublicKeyMultibase, + exportPrivateKeyJWK, +} from '../../../src/crypto/key-generation.js'; +import { CryptoError } from '../../../src/errors/index.js'; + +describe('Key Generation', () => { + describe('ECDSA secp256k1 key pair generation', () => { + it('should generate a valid ECDSA secp256k1 key pair', async () => { + const keyPair = await generateKeyPair(KeyType.ECDSA_SECP256K1); + + expect(keyPair).toBeDefined(); + expect(keyPair.publicKey).toBeDefined(); + expect(keyPair.privateKey).toBeDefined(); + expect(keyPair.publicKey.type).toBe('public'); + expect(keyPair.privateKey.type).toBe('private'); + }); + + it('should generate different key pairs on each call', async () => { + const keyPair1 = await generateKeyPair(KeyType.ECDSA_SECP256K1); + const keyPair2 = await generateKeyPair(KeyType.ECDSA_SECP256K1); + + const jwk1 = await exportPublicKeyJWK(keyPair1.publicKey); + const jwk2 = await exportPublicKeyJWK(keyPair2.publicKey); + + expect(jwk1.x).not.toBe(jwk2.x); + expect(jwk1.y).not.toBe(jwk2.y); + }); + + it('should export public key as JWK format', async () => { + const keyPair = await generateKeyPair(KeyType.ECDSA_SECP256K1); + const jwk = await exportPublicKeyJWK(keyPair.publicKey); + + expect(jwk).toBeDefined(); + expect(jwk.kty).toBe('EC'); + expect(jwk.crv).toBe('P-256'); // Note: Using P-256 as secp256k1 is not natively supported + expect(jwk.x).toBeDefined(); + expect(jwk.y).toBeDefined(); + expect(typeof jwk.x).toBe('string'); + expect(typeof jwk.y).toBe('string'); + }); + + it('should export private key as JWK format', async () => { + const keyPair = await generateKeyPair(KeyType.ECDSA_SECP256K1); + const jwk = await exportPrivateKeyJWK(keyPair.privateKey); + + expect(jwk).toBeDefined(); + expect(jwk.kty).toBe('EC'); + expect(jwk.crv).toBe('P-256'); // Note: Using P-256 as secp256k1 is not natively supported + expect(jwk.x).toBeDefined(); + expect(jwk.y).toBeDefined(); + expect(jwk.d).toBeDefined(); + expect(typeof jwk.d).toBe('string'); + }); + }); + + describe('Ed25519 key pair generation', () => { + it('should generate a valid Ed25519 key pair', async () => { + const keyPair = await generateKeyPair(KeyType.ED25519); + + expect(keyPair).toBeDefined(); + expect(keyPair.publicKey).toBeDefined(); + expect(keyPair.privateKey).toBeDefined(); + expect(keyPair.publicKey.type).toBe('public'); + expect(keyPair.privateKey.type).toBe('private'); + }); + + it('should generate different key pairs on each call', async () => { + const keyPair1 = await generateKeyPair(KeyType.ED25519); + const keyPair2 = await generateKeyPair(KeyType.ED25519); + + const jwk1 = await exportPublicKeyJWK(keyPair1.publicKey); + const jwk2 = await exportPublicKeyJWK(keyPair2.publicKey); + + expect(jwk1.x).not.toBe(jwk2.x); + }); + + it('should export public key as JWK format', async () => { + const keyPair = await generateKeyPair(KeyType.ED25519); + const jwk = await exportPublicKeyJWK(keyPair.publicKey); + + expect(jwk).toBeDefined(); + expect(jwk.kty).toBe('OKP'); + expect(jwk.crv).toBe('Ed25519'); + expect(jwk.x).toBeDefined(); + expect(typeof jwk.x).toBe('string'); + }); + + it('should export private key as JWK format', async () => { + const keyPair = await generateKeyPair(KeyType.ED25519); + const jwk = await exportPrivateKeyJWK(keyPair.privateKey); + + expect(jwk).toBeDefined(); + expect(jwk.kty).toBe('OKP'); + expect(jwk.crv).toBe('Ed25519'); + expect(jwk.x).toBeDefined(); + expect(jwk.d).toBeDefined(); + expect(typeof jwk.d).toBe('string'); + }); + }); + + describe('X25519 key pair generation', () => { + it('should generate a valid X25519 key pair', async () => { + const keyPair = await generateKeyPair(KeyType.X25519); + + expect(keyPair).toBeDefined(); + expect(keyPair.publicKey).toBeDefined(); + expect(keyPair.privateKey).toBeDefined(); + expect(keyPair.publicKey.type).toBe('public'); + expect(keyPair.privateKey.type).toBe('private'); + }); + + it('should generate different key pairs on each call', async () => { + const keyPair1 = await generateKeyPair(KeyType.X25519); + const keyPair2 = await generateKeyPair(KeyType.X25519); + + const jwk1 = await exportPublicKeyJWK(keyPair1.publicKey); + const jwk2 = await exportPublicKeyJWK(keyPair2.publicKey); + + expect(jwk1.x).not.toBe(jwk2.x); + }); + + it('should export public key as JWK format', async () => { + const keyPair = await generateKeyPair(KeyType.X25519); + const jwk = await exportPublicKeyJWK(keyPair.publicKey); + + expect(jwk).toBeDefined(); + expect(jwk.kty).toBe('OKP'); + expect(jwk.crv).toBe('X25519'); + expect(jwk.x).toBeDefined(); + expect(typeof jwk.x).toBe('string'); + }); + + it('should export private key as JWK format', async () => { + const keyPair = await generateKeyPair(KeyType.X25519); + const jwk = await exportPrivateKeyJWK(keyPair.privateKey); + + expect(jwk).toBeDefined(); + expect(jwk.kty).toBe('OKP'); + expect(jwk.crv).toBe('X25519'); + expect(jwk.x).toBeDefined(); + expect(jwk.d).toBeDefined(); + expect(typeof jwk.d).toBe('string'); + }); + }); + + describe('Multibase format export', () => { + it('should export Ed25519 public key as multibase format', async () => { + const keyPair = await generateKeyPair(KeyType.ED25519); + const multibase = await exportPublicKeyMultibase(keyPair.publicKey); + + expect(multibase).toBeDefined(); + expect(typeof multibase).toBe('string'); + expect(multibase).toMatch(/^z[1-9A-HJ-NP-Za-km-z]+$/); // base58btc format + }); + + it('should export X25519 public key as multibase format', async () => { + const keyPair = await generateKeyPair(KeyType.X25519); + const multibase = await exportPublicKeyMultibase(keyPair.publicKey); + + expect(multibase).toBeDefined(); + expect(typeof multibase).toBe('string'); + expect(multibase).toMatch(/^z[1-9A-HJ-NP-Za-km-z]+$/); + }); + }); + + describe('Error handling', () => { + it('should throw CryptoError for invalid key type', async () => { + await expect(generateKeyPair('INVALID_TYPE' as KeyType)).rejects.toThrow( + CryptoError + ); + }); + + it('should throw CryptoError when exporting invalid key as JWK', async () => { + const invalidKey = {} as CryptoKey; + await expect(exportPublicKeyJWK(invalidKey)).rejects.toThrow(CryptoError); + }); + + it('should throw CryptoError when exporting invalid key as multibase', async () => { + const invalidKey = {} as CryptoKey; + await expect(exportPublicKeyMultibase(invalidKey)).rejects.toThrow( + CryptoError + ); + }); + }); +}); diff --git a/typescript/ts_sdk/tests/unit/crypto/signing.test.ts b/typescript/ts_sdk/tests/unit/crypto/signing.test.ts new file mode 100644 index 0000000..f3ead03 --- /dev/null +++ b/typescript/ts_sdk/tests/unit/crypto/signing.test.ts @@ -0,0 +1,217 @@ +/** + * Unit tests for signing and verification functionality + */ + +import { describe, it, expect } from 'vitest'; +import { + generateKeyPair, + KeyType, +} from '../../../src/crypto/key-generation.js'; +import { sign, verify } from '../../../src/crypto/signing.js'; +import { CryptoError } from '../../../src/errors/index.js'; + +describe('Signing and Verification', () => { + describe('ECDSA secp256k1 signing', () => { + it('should sign data with ECDSA key', async () => { + const keyPair = await generateKeyPair(KeyType.ECDSA_SECP256K1); + const data = new TextEncoder().encode('test message'); + + const signature = await sign(keyPair.privateKey, data, KeyType.ECDSA_SECP256K1); + + expect(signature).toBeDefined(); + expect(signature).toBeInstanceOf(Uint8Array); + expect(signature.length).toBeGreaterThan(0); + }); + + it('should verify valid ECDSA signature', async () => { + const keyPair = await generateKeyPair(KeyType.ECDSA_SECP256K1); + const data = new TextEncoder().encode('test message'); + + const signature = await sign(keyPair.privateKey, data, KeyType.ECDSA_SECP256K1); + const isValid = await verify( + keyPair.publicKey, + data, + signature, + KeyType.ECDSA_SECP256K1 + ); + + expect(isValid).toBe(true); + }); + + it('should reject invalid ECDSA signature', async () => { + const keyPair = await generateKeyPair(KeyType.ECDSA_SECP256K1); + const data = new TextEncoder().encode('test message'); + + const signature = await sign(keyPair.privateKey, data, KeyType.ECDSA_SECP256K1); + + // Tamper with signature + signature[0] ^= 0xff; + + const isValid = await verify( + keyPair.publicKey, + data, + signature, + KeyType.ECDSA_SECP256K1 + ); + + expect(isValid).toBe(false); + }); + + it('should reject signature with wrong data', async () => { + const keyPair = await generateKeyPair(KeyType.ECDSA_SECP256K1); + const data = new TextEncoder().encode('test message'); + const wrongData = new TextEncoder().encode('wrong message'); + + const signature = await sign(keyPair.privateKey, data, KeyType.ECDSA_SECP256K1); + const isValid = await verify( + keyPair.publicKey, + wrongData, + signature, + KeyType.ECDSA_SECP256K1 + ); + + expect(isValid).toBe(false); + }); + + it('should generate different signatures for different data', async () => { + const keyPair = await generateKeyPair(KeyType.ECDSA_SECP256K1); + const data1 = new TextEncoder().encode('message 1'); + const data2 = new TextEncoder().encode('message 2'); + + const signature1 = await sign(keyPair.privateKey, data1, KeyType.ECDSA_SECP256K1); + const signature2 = await sign(keyPair.privateKey, data2, KeyType.ECDSA_SECP256K1); + + expect(signature1).not.toEqual(signature2); + }); + }); + + describe('Ed25519 signing', () => { + it('should sign data with Ed25519 key', async () => { + const keyPair = await generateKeyPair(KeyType.ED25519); + const data = new TextEncoder().encode('test message'); + + const signature = await sign(keyPair.privateKey, data, KeyType.ED25519); + + expect(signature).toBeDefined(); + expect(signature).toBeInstanceOf(Uint8Array); + expect(signature.length).toBe(64); // Ed25519 signatures are always 64 bytes + }); + + it('should verify valid Ed25519 signature', async () => { + const keyPair = await generateKeyPair(KeyType.ED25519); + const data = new TextEncoder().encode('test message'); + + const signature = await sign(keyPair.privateKey, data, KeyType.ED25519); + const isValid = await verify( + keyPair.publicKey, + data, + signature, + KeyType.ED25519 + ); + + expect(isValid).toBe(true); + }); + + it('should reject invalid Ed25519 signature', async () => { + const keyPair = await generateKeyPair(KeyType.ED25519); + const data = new TextEncoder().encode('test message'); + + const signature = await sign(keyPair.privateKey, data, KeyType.ED25519); + + // Tamper with signature + signature[0] ^= 0xff; + + const isValid = await verify( + keyPair.publicKey, + data, + signature, + KeyType.ED25519 + ); + + expect(isValid).toBe(false); + }); + + it('should reject signature with wrong data', async () => { + const keyPair = await generateKeyPair(KeyType.ED25519); + const data = new TextEncoder().encode('test message'); + const wrongData = new TextEncoder().encode('wrong message'); + + const signature = await sign(keyPair.privateKey, data, KeyType.ED25519); + const isValid = await verify( + keyPair.publicKey, + wrongData, + signature, + KeyType.ED25519 + ); + + expect(isValid).toBe(false); + }); + + it('should generate deterministic signatures', async () => { + const keyPair = await generateKeyPair(KeyType.ED25519); + const data = new TextEncoder().encode('test message'); + + const signature1 = await sign(keyPair.privateKey, data, KeyType.ED25519); + const signature2 = await sign(keyPair.privateKey, data, KeyType.ED25519); + + // Ed25519 signatures are deterministic + expect(signature1).toEqual(signature2); + }); + }); + + describe('Error handling', () => { + it('should throw CryptoError when signing with mismatched key type', async () => { + const keyPair = await generateKeyPair(KeyType.ED25519); + const data = new TextEncoder().encode('test message'); + + await expect( + sign(keyPair.privateKey, data, KeyType.X25519) + ).rejects.toThrow(CryptoError); + }); + + it('should throw CryptoError when verifying with mismatched key type', async () => { + const keyPair = await generateKeyPair(KeyType.ED25519); + const data = new TextEncoder().encode('test message'); + const signature = await sign(keyPair.privateKey, data, KeyType.ED25519); + + await expect( + verify(keyPair.publicKey, data, signature, KeyType.X25519) + ).rejects.toThrow(CryptoError); + }); + + it('should throw CryptoError when signing with public key', async () => { + const keyPair = await generateKeyPair(KeyType.ED25519); + const data = new TextEncoder().encode('test message'); + + await expect( + sign(keyPair.publicKey as any, data, KeyType.ED25519) + ).rejects.toThrow(CryptoError); + }); + + it('should throw CryptoError when verifying with private key', async () => { + const keyPair = await generateKeyPair(KeyType.ED25519); + const data = new TextEncoder().encode('test message'); + const signature = await sign(keyPair.privateKey, data, KeyType.ED25519); + + await expect( + verify(keyPair.privateKey as any, data, signature, KeyType.ED25519) + ).rejects.toThrow(CryptoError); + }); + + it('should reject signature with wrong public key', async () => { + const keyPair1 = await generateKeyPair(KeyType.ED25519); + const keyPair2 = await generateKeyPair(KeyType.ED25519); + const data = new TextEncoder().encode('test message'); + + const signature = await sign(keyPair1.privateKey, data, KeyType.ED25519); + const isValid = await verify( + keyPair2.publicKey, + data, + signature, + KeyType.ED25519 + ); + + expect(isValid).toBe(false); + }); + }); +}); diff --git a/typescript/ts_sdk/tests/unit/protocol/message-handler.test.ts b/typescript/ts_sdk/tests/unit/protocol/message-handler.test.ts new file mode 100644 index 0000000..0dee32e --- /dev/null +++ b/typescript/ts_sdk/tests/unit/protocol/message-handler.test.ts @@ -0,0 +1,556 @@ +import { describe, it, expect } from 'vitest'; +import { ProtocolMessageHandler, ProtocolType } from '../../../src/protocol/message-handler'; + +describe('ProtocolMessageHandler - Message Encoding', () => { + const handler = new ProtocolMessageHandler(); + + describe('encode', () => { + it('should encode meta-protocol message with correct header', () => { + const data = new TextEncoder().encode('{"action":"protocolNegotiation"}'); + const encoded = handler.encode(ProtocolType.META, data); + + // Check header byte (first byte should be 0b00000000 for META protocol) + expect(encoded[0]).toBe(0b00000000); + // Check data follows header + expect(encoded.length).toBe(data.length + 1); + expect(encoded.slice(1)).toEqual(data); + }); + + it('should encode application protocol message with correct header', () => { + const data = new TextEncoder().encode('{"messageId":"123","data":"test"}'); + const encoded = handler.encode(ProtocolType.APPLICATION, data); + + // Check header byte (first byte should be 0b01000000 for APPLICATION protocol) + expect(encoded[0]).toBe(0b01000000); + // Check data follows header + expect(encoded.length).toBe(data.length + 1); + expect(encoded.slice(1)).toEqual(data); + }); + + it('should encode natural language message with correct header', () => { + const text = 'Hello, this is a natural language message'; + const data = new TextEncoder().encode(text); + const encoded = handler.encode(ProtocolType.NATURAL_LANGUAGE, data); + + // Check header byte (first byte should be 0b10000000 for NATURAL_LANGUAGE protocol) + expect(encoded[0]).toBe(0b10000000); + // Check data follows header + expect(encoded.length).toBe(data.length + 1); + expect(encoded.slice(1)).toEqual(data); + }); + + it('should encode verification protocol message with correct header', () => { + const data = new TextEncoder().encode('{"testCase":"1","expected":"success"}'); + const encoded = handler.encode(ProtocolType.VERIFICATION, data); + + // Check header byte (first byte should be 0b11000000 for VERIFICATION protocol) + expect(encoded[0]).toBe(0b11000000); + // Check data follows header + expect(encoded.length).toBe(data.length + 1); + expect(encoded.slice(1)).toEqual(data); + }); + + it('should handle empty data', () => { + const data = new Uint8Array(0); + const encoded = handler.encode(ProtocolType.META, data); + + // Should still have header byte + expect(encoded.length).toBe(1); + expect(encoded[0]).toBe(0b00000000); + }); + + it('should handle large data payloads', () => { + const largeData = new Uint8Array(10000).fill(42); + const encoded = handler.encode(ProtocolType.APPLICATION, largeData); + + expect(encoded.length).toBe(10001); + expect(encoded[0]).toBe(0b01000000); + expect(encoded.slice(1)).toEqual(largeData); + }); + + it('should preserve binary data integrity', () => { + const binaryData = new Uint8Array([0, 1, 2, 255, 254, 253]); + const encoded = handler.encode(ProtocolType.META, binaryData); + + expect(encoded[0]).toBe(0b00000000); + expect(encoded.slice(1)).toEqual(binaryData); + }); + }); + + describe('binary format correctness', () => { + it('should set protocol type bits correctly in header', () => { + const data = new Uint8Array([1, 2, 3]); + + // META: 00 in bits 7-6 + const metaEncoded = handler.encode(ProtocolType.META, data); + expect((metaEncoded[0] >> 6) & 0b11).toBe(0b00); + + // APPLICATION: 01 in bits 7-6 + const appEncoded = handler.encode(ProtocolType.APPLICATION, data); + expect((appEncoded[0] >> 6) & 0b11).toBe(0b01); + + // NATURAL_LANGUAGE: 10 in bits 7-6 + const nlEncoded = handler.encode(ProtocolType.NATURAL_LANGUAGE, data); + expect((nlEncoded[0] >> 6) & 0b11).toBe(0b10); + + // VERIFICATION: 11 in bits 7-6 + const verEncoded = handler.encode(ProtocolType.VERIFICATION, data); + expect((verEncoded[0] >> 6) & 0b11).toBe(0b11); + }); + + it('should set reserved bits to zero', () => { + const data = new Uint8Array([1, 2, 3]); + const encoded = handler.encode(ProtocolType.META, data); + + // Reserved bits (bits 5-0) should be 0 + expect(encoded[0] & 0b00111111).toBe(0); + }); + + it('should create new Uint8Array without modifying input', () => { + const data = new Uint8Array([1, 2, 3]); + const originalData = new Uint8Array(data); + + handler.encode(ProtocolType.META, data); + + // Original data should not be modified + expect(data).toEqual(originalData); + }); + }); +}); + +describe('ProtocolMessageHandler - Message Decoding', () => { + const handler = new ProtocolMessageHandler(); + + describe('decode', () => { + it('should decode meta-protocol message correctly', () => { + const data = new TextEncoder().encode('{"action":"protocolNegotiation"}'); + const encoded = handler.encode(ProtocolType.META, data); + + const decoded = handler.decode(encoded); + + expect(decoded.protocolType).toBe(ProtocolType.META); + expect(decoded.data).toEqual(data); + }); + + it('should decode application protocol message correctly', () => { + const data = new TextEncoder().encode('{"messageId":"123","data":"test"}'); + const encoded = handler.encode(ProtocolType.APPLICATION, data); + + const decoded = handler.decode(encoded); + + expect(decoded.protocolType).toBe(ProtocolType.APPLICATION); + expect(decoded.data).toEqual(data); + }); + + it('should decode natural language message correctly', () => { + const text = 'Hello, this is a natural language message'; + const data = new TextEncoder().encode(text); + const encoded = handler.encode(ProtocolType.NATURAL_LANGUAGE, data); + + const decoded = handler.decode(encoded); + + expect(decoded.protocolType).toBe(ProtocolType.NATURAL_LANGUAGE); + expect(decoded.data).toEqual(data); + expect(new TextDecoder().decode(decoded.data)).toBe(text); + }); + + it('should decode verification protocol message correctly', () => { + const data = new TextEncoder().encode('{"testCase":"1","expected":"success"}'); + const encoded = handler.encode(ProtocolType.VERIFICATION, data); + + const decoded = handler.decode(encoded); + + expect(decoded.protocolType).toBe(ProtocolType.VERIFICATION); + expect(decoded.data).toEqual(data); + }); + + it('should handle message with empty data', () => { + const data = new Uint8Array(0); + const encoded = handler.encode(ProtocolType.META, data); + + const decoded = handler.decode(encoded); + + expect(decoded.protocolType).toBe(ProtocolType.META); + expect(decoded.data.length).toBe(0); + }); + + it('should handle large data payloads', () => { + const largeData = new Uint8Array(10000).fill(42); + const encoded = handler.encode(ProtocolType.APPLICATION, largeData); + + const decoded = handler.decode(encoded); + + expect(decoded.protocolType).toBe(ProtocolType.APPLICATION); + expect(decoded.data).toEqual(largeData); + }); + + it('should preserve binary data integrity', () => { + const binaryData = new Uint8Array([0, 1, 2, 255, 254, 253]); + const encoded = handler.encode(ProtocolType.META, binaryData); + + const decoded = handler.decode(encoded); + + expect(decoded.protocolType).toBe(ProtocolType.META); + expect(decoded.data).toEqual(binaryData); + }); + + it('should correctly extract protocol type from header', () => { + const data = new Uint8Array([1, 2, 3]); + + // Test all protocol types + const metaEncoded = handler.encode(ProtocolType.META, data); + expect(handler.decode(metaEncoded).protocolType).toBe(ProtocolType.META); + + const appEncoded = handler.encode(ProtocolType.APPLICATION, data); + expect(handler.decode(appEncoded).protocolType).toBe(ProtocolType.APPLICATION); + + const nlEncoded = handler.encode(ProtocolType.NATURAL_LANGUAGE, data); + expect(handler.decode(nlEncoded).protocolType).toBe(ProtocolType.NATURAL_LANGUAGE); + + const verEncoded = handler.encode(ProtocolType.VERIFICATION, data); + expect(handler.decode(verEncoded).protocolType).toBe(ProtocolType.VERIFICATION); + }); + }); + + describe('error handling for malformed messages', () => { + it('should throw error for empty message', () => { + const emptyMessage = new Uint8Array(0); + + expect(() => handler.decode(emptyMessage)).toThrow('Message is too short'); + }); + + it('should throw error for message with only header and no data', () => { + // This is actually valid - a message can have just a header with no data + const headerOnly = new Uint8Array([0b00000000]); + + const decoded = handler.decode(headerOnly); + expect(decoded.protocolType).toBe(ProtocolType.META); + expect(decoded.data.length).toBe(0); + }); + + it('should handle messages with reserved bits set (should ignore them)', () => { + // Create a message with reserved bits set (should still decode correctly) + const message = new Uint8Array([0b00111111, 1, 2, 3]); // META type with reserved bits set + + const decoded = handler.decode(message); + + // Should still extract protocol type correctly + expect(decoded.protocolType).toBe(ProtocolType.META); + expect(decoded.data).toEqual(new Uint8Array([1, 2, 3])); + }); + + it('should throw error for null or undefined input', () => { + expect(() => handler.decode(null as any)).toThrow(); + expect(() => handler.decode(undefined as any)).toThrow(); + }); + }); + + describe('encode-decode round trip', () => { + it('should maintain data integrity through encode-decode cycle', () => { + const testCases = [ + { type: ProtocolType.META, data: new TextEncoder().encode('{"action":"test"}') }, + { type: ProtocolType.APPLICATION, data: new TextEncoder().encode('application data') }, + { type: ProtocolType.NATURAL_LANGUAGE, data: new TextEncoder().encode('natural language') }, + { type: ProtocolType.VERIFICATION, data: new TextEncoder().encode('verification data') }, + ]; + + testCases.forEach(({ type, data }) => { + const encoded = handler.encode(type, data); + const decoded = handler.decode(encoded); + + expect(decoded.protocolType).toBe(type); + expect(decoded.data).toEqual(data); + }); + }); + }); +}); + +describe('ProtocolMessageHandler - Meta-Protocol Message Parsing', () => { + const handler = new ProtocolMessageHandler(); + + describe('parseMetaProtocol - protocolNegotiation', () => { + it('should parse protocolNegotiation message with all fields', () => { + const message = { + action: 'protocolNegotiation', + sequenceId: 0, + candidateProtocols: '# Requirements\nRetrieve product information', + modificationSummary: 'Initial proposal', + status: 'negotiating' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.action).toBe('protocolNegotiation'); + expect(parsed.sequenceId).toBe(0); + expect(parsed.candidateProtocols).toBe('# Requirements\nRetrieve product information'); + expect(parsed.modificationSummary).toBe('Initial proposal'); + expect(parsed.status).toBe('negotiating'); + }); + + it('should parse protocolNegotiation message without modificationSummary', () => { + const message = { + action: 'protocolNegotiation', + sequenceId: 0, + candidateProtocols: '# Requirements\nRetrieve product information', + status: 'negotiating' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.action).toBe('protocolNegotiation'); + expect(parsed.sequenceId).toBe(0); + expect(parsed.modificationSummary).toBeUndefined(); + }); + + it('should parse protocolNegotiation with status accepted', () => { + const message = { + action: 'protocolNegotiation', + sequenceId: 2, + candidateProtocols: '# Final protocol', + status: 'accepted' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.status).toBe('accepted'); + }); + + it('should parse protocolNegotiation with status rejected', () => { + const message = { + action: 'protocolNegotiation', + sequenceId: 1, + candidateProtocols: '# Protocol', + status: 'rejected' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.status).toBe('rejected'); + }); + + it('should parse protocolNegotiation with status timeout', () => { + const message = { + action: 'protocolNegotiation', + sequenceId: 5, + candidateProtocols: '# Protocol', + status: 'timeout' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.status).toBe('timeout'); + }); + }); + + describe('parseMetaProtocol - codeGeneration', () => { + it('should parse codeGeneration message with status generated', () => { + const message = { + action: 'codeGeneration', + status: 'generated' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.action).toBe('codeGeneration'); + expect(parsed.status).toBe('generated'); + }); + + it('should parse codeGeneration message with status error', () => { + const message = { + action: 'codeGeneration', + status: 'error' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.action).toBe('codeGeneration'); + expect(parsed.status).toBe('error'); + }); + }); + + describe('parseMetaProtocol - testCasesNegotiation', () => { + it('should parse testCasesNegotiation message with all fields', () => { + const message = { + action: 'testCasesNegotiation', + testCases: '# Test Case 1\n- Test request data\n- Expected result', + modificationSummary: 'Added edge cases', + status: 'negotiating' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.action).toBe('testCasesNegotiation'); + expect(parsed.testCases).toBe('# Test Case 1\n- Test request data\n- Expected result'); + expect(parsed.modificationSummary).toBe('Added edge cases'); + expect(parsed.status).toBe('negotiating'); + }); + + it('should parse testCasesNegotiation message without modificationSummary', () => { + const message = { + action: 'testCasesNegotiation', + testCases: '# Test Case 1', + status: 'accepted' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.action).toBe('testCasesNegotiation'); + expect(parsed.modificationSummary).toBeUndefined(); + expect(parsed.status).toBe('accepted'); + }); + + it('should parse testCasesNegotiation with status rejected', () => { + const message = { + action: 'testCasesNegotiation', + testCases: '# Test cases', + status: 'rejected' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.status).toBe('rejected'); + }); + }); + + describe('parseMetaProtocol - fixErrorNegotiation', () => { + it('should parse fixErrorNegotiation message with all fields', () => { + const message = { + action: 'fixErrorNegotiation', + errorDescription: '# Error Description\n- The status field is missing', + status: 'negotiating' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.action).toBe('fixErrorNegotiation'); + expect(parsed.errorDescription).toBe('# Error Description\n- The status field is missing'); + expect(parsed.status).toBe('negotiating'); + }); + + it('should parse fixErrorNegotiation with status accepted', () => { + const message = { + action: 'fixErrorNegotiation', + errorDescription: 'Error details', + status: 'accepted' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.status).toBe('accepted'); + }); + + it('should parse fixErrorNegotiation with status rejected', () => { + const message = { + action: 'fixErrorNegotiation', + errorDescription: 'Error details', + status: 'rejected' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.status).toBe('rejected'); + }); + }); + + describe('parseMetaProtocol - naturalLanguageNegotiation', () => { + it('should parse naturalLanguageNegotiation REQUEST message', () => { + const message = { + action: 'naturalLanguageNegotiation', + type: 'REQUEST', + messageId: 'abc123def4567890', + message: 'Can we use a custom timeout of 30 seconds?' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.action).toBe('naturalLanguageNegotiation'); + expect(parsed.type).toBe('REQUEST'); + expect(parsed.messageId).toBe('abc123def4567890'); + expect(parsed.message).toBe('Can we use a custom timeout of 30 seconds?'); + }); + + it('should parse naturalLanguageNegotiation RESPONSE message', () => { + const message = { + action: 'naturalLanguageNegotiation', + type: 'RESPONSE', + messageId: 'abc123def4567890', + message: 'Yes, 30 seconds timeout is acceptable.' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.action).toBe('naturalLanguageNegotiation'); + expect(parsed.type).toBe('RESPONSE'); + expect(parsed.messageId).toBe('abc123def4567890'); + expect(parsed.message).toBe('Yes, 30 seconds timeout is acceptable.'); + }); + }); + + describe('parseMetaProtocol - error handling', () => { + it('should throw error for invalid JSON', () => { + const invalidJson = new TextEncoder().encode('{ invalid json }'); + + expect(() => handler.parseMetaProtocol(invalidJson)).toThrow(); + }); + + it('should throw error for missing action field', () => { + const message = { + sequenceId: 0, + status: 'negotiating' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + expect(() => handler.parseMetaProtocol(data)).toThrow('Invalid meta-protocol message: missing action field'); + }); + + it('should throw error for unknown action type', () => { + const message = { + action: 'unknownAction', + data: 'some data' + }; + const data = new TextEncoder().encode(JSON.stringify(message)); + + expect(() => handler.parseMetaProtocol(data)).toThrow('Unknown meta-protocol action: unknownAction'); + }); + + it('should throw error for empty data', () => { + const emptyData = new Uint8Array(0); + + expect(() => handler.parseMetaProtocol(emptyData)).toThrow(); + }); + }); + + describe('parseMetaProtocol - message type discrimination', () => { + it('should correctly discriminate between different message types', () => { + const messages = [ + { action: 'protocolNegotiation', sequenceId: 0, candidateProtocols: 'test', status: 'negotiating' }, + { action: 'codeGeneration', status: 'generated' }, + { action: 'testCasesNegotiation', testCases: 'test', status: 'negotiating' }, + { action: 'fixErrorNegotiation', errorDescription: 'error', status: 'negotiating' }, + { action: 'naturalLanguageNegotiation', type: 'REQUEST', messageId: '123', message: 'test' } + ]; + + messages.forEach(message => { + const data = new TextEncoder().encode(JSON.stringify(message)); + const parsed = handler.parseMetaProtocol(data); + + expect(parsed.action).toBe(message.action); + }); + }); + }); +}); diff --git a/typescript/ts_sdk/tests/unit/protocol/meta-protocol-machine.test.ts b/typescript/ts_sdk/tests/unit/protocol/meta-protocol-machine.test.ts new file mode 100644 index 0000000..4c81896 --- /dev/null +++ b/typescript/ts_sdk/tests/unit/protocol/meta-protocol-machine.test.ts @@ -0,0 +1,716 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createMetaProtocolMachine, MetaProtocolConfig } from '../../../src/protocol/meta-protocol-machine'; +import type { DIDIdentity } from '../../../src/types/did'; + +describe('MetaProtocolMachine - Initialization', () => { + let mockIdentity: DIDIdentity; + let config: MetaProtocolConfig; + + beforeEach(() => { + // Create a mock DID identity for testing + mockIdentity = { + did: 'did:wba:example.com:agent1', + document: { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com:agent1', + verificationMethod: [], + authentication: [], + }, + privateKeys: new Map(), + }; + + config = { + localIdentity: mockIdentity, + remoteDID: 'did:wba:example.com:agent2', + maxNegotiationRounds: 5, + timeoutMs: 30000, + }; + }); + + it('should create a state machine with valid config', () => { + const actor = createMetaProtocolMachine(config); + + expect(actor).toBeDefined(); + expect(actor.getSnapshot).toBeDefined(); + }); + + it('should initialize with Idle state', () => { + const actor = createMetaProtocolMachine(config); + const snapshot = actor.getSnapshot(); + + expect(snapshot.value).toBe('Idle'); + }); + + it('should initialize context with provided config', () => { + const actor = createMetaProtocolMachine(config); + const snapshot = actor.getSnapshot(); + + expect(snapshot.context.localIdentity).toEqual(mockIdentity); + expect(snapshot.context.remoteDID).toBe('did:wba:example.com:agent2'); + expect(snapshot.context.maxNegotiationRounds).toBe(5); + expect(snapshot.context.sequenceId).toBe(0); + }); + + it('should initialize with empty candidateProtocols', () => { + const actor = createMetaProtocolMachine(config); + const snapshot = actor.getSnapshot(); + + expect(snapshot.context.candidateProtocols).toBe(''); + }); + + it('should initialize with undefined agreedProtocol', () => { + const actor = createMetaProtocolMachine(config); + const snapshot = actor.getSnapshot(); + + expect(snapshot.context.agreedProtocol).toBeUndefined(); + }); + + it('should initialize with undefined testCases', () => { + const actor = createMetaProtocolMachine(config); + const snapshot = actor.getSnapshot(); + + expect(snapshot.context.testCases).toBeUndefined(); + }); + + it('should use default maxNegotiationRounds if not provided', () => { + const configWithoutMax = { + localIdentity: mockIdentity, + remoteDID: 'did:wba:example.com:agent2', + }; + + const actor = createMetaProtocolMachine(configWithoutMax); + const snapshot = actor.getSnapshot(); + + expect(snapshot.context.maxNegotiationRounds).toBe(10); + }); + + it('should use default timeout if not provided', () => { + const configWithoutTimeout = { + localIdentity: mockIdentity, + remoteDID: 'did:wba:example.com:agent2', + }; + + const actor = createMetaProtocolMachine(configWithoutTimeout); + const snapshot = actor.getSnapshot(); + + // Verify timeout is set to default (we'll check this in context or config) + expect(snapshot.context).toBeDefined(); + }); +}); + + +describe('MetaProtocolMachine - Negotiation Flow', () => { + let mockIdentity: DIDIdentity; + let config: MetaProtocolConfig; + + beforeEach(() => { + mockIdentity = { + did: 'did:wba:example.com:agent1', + document: { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com:agent1', + verificationMethod: [], + authentication: [], + }, + privateKeys: new Map(), + }; + + config = { + localIdentity: mockIdentity, + remoteDID: 'did:wba:example.com:agent2', + maxNegotiationRounds: 3, + }; + }); + + it('should transition from Idle to Negotiating on initiate event', () => { + const actor = createMetaProtocolMachine(config); + + actor.send({ + type: 'initiate', + candidateProtocols: 'protocol1,protocol2', + }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('Negotiating'); + expect(snapshot.context.candidateProtocols).toBe('protocol1,protocol2'); + }); + + it('should transition from Idle to Negotiating on receive_request event', () => { + const actor = createMetaProtocolMachine(config); + + actor.send({ + type: 'receive_request', + candidateProtocols: 'protocolA,protocolB', + sequenceId: 1, + }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('Negotiating'); + expect(snapshot.context.candidateProtocols).toBe('protocolA,protocolB'); + expect(snapshot.context.sequenceId).toBe(1); + }); + + it('should stay in Negotiating state on negotiate event when under max rounds', () => { + const actor = createMetaProtocolMachine(config); + + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'negotiate', response: 'counter-proposal' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('Negotiating'); + expect(snapshot.context.negotiationRound).toBe(1); + }); + + it('should transition from Negotiating to CodeGeneration on accept event', () => { + const actor = createMetaProtocolMachine(config); + + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('CodeGeneration'); + expect(snapshot.context.agreedProtocol).toBe('protocol1'); + }); + + it('should transition from Negotiating to Rejected on reject event', () => { + const actor = createMetaProtocolMachine(config); + + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'reject', reason: 'incompatible protocols' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('Rejected'); + }); + + it('should transition from Negotiating to Rejected on timeout event', () => { + const actor = createMetaProtocolMachine(config); + + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'timeout' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('Rejected'); + }); + + it('should enforce max negotiation rounds', () => { + const actor = createMetaProtocolMachine(config); + + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + + // Negotiate up to max rounds + for (let i = 0; i < 3; i++) { + actor.send({ type: 'negotiate', response: `round-${i}` }); + } + + const snapshot = actor.getSnapshot(); + // Should transition to Rejected when max rounds exceeded + expect(snapshot.context.negotiationRound).toBe(3); + }); + + it('should increment sequence ID during negotiation', () => { + const actor = createMetaProtocolMachine(config); + + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + const initialSeq = actor.getSnapshot().context.sequenceId; + + actor.send({ type: 'negotiate', response: 'counter' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.context.sequenceId).toBeGreaterThan(initialSeq); + }); +}); + + +describe('MetaProtocolMachine - Code Generation Flow', () => { + let mockIdentity: DIDIdentity; + let config: MetaProtocolConfig; + + beforeEach(() => { + mockIdentity = { + did: 'did:wba:example.com:agent1', + document: { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com:agent1', + verificationMethod: [], + authentication: [], + }, + privateKeys: new Map(), + }; + + config = { + localIdentity: mockIdentity, + remoteDID: 'did:wba:example.com:agent2', + }; + }); + + it('should transition from CodeGeneration to TestCases on code_ready event', () => { + const actor = createMetaProtocolMachine(config); + + // Navigate to CodeGeneration state + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + + // Send code_ready event + actor.send({ type: 'code_ready', code: 'generated code' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('TestCases'); + }); + + it('should transition from CodeGeneration to Failed on code_error event', () => { + const actor = createMetaProtocolMachine(config); + + // Navigate to CodeGeneration state + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + + // Send code_error event + actor.send({ type: 'code_error', error: 'compilation failed' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('Failed'); + }); + + it('should store error in context on code_error event', () => { + const actor = createMetaProtocolMachine(config); + + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + actor.send({ type: 'code_error', error: 'syntax error' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.context.errors).toContain('syntax error'); + }); + + it('should remain in CodeGeneration state until code_ready or code_error', () => { + const actor = createMetaProtocolMachine(config); + + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('CodeGeneration'); + }); +}); + + +describe('MetaProtocolMachine - Test Cases Flow', () => { + let mockIdentity: DIDIdentity; + let config: MetaProtocolConfig; + + beforeEach(() => { + mockIdentity = { + did: 'did:wba:example.com:agent1', + document: { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com:agent1', + verificationMethod: [], + authentication: [], + }, + privateKeys: new Map(), + }; + + config = { + localIdentity: mockIdentity, + remoteDID: 'did:wba:example.com:agent2', + }; + }); + + it('should transition from TestCases to Testing on tests_agreed event', () => { + const actor = createMetaProtocolMachine(config); + + // Navigate to TestCases state + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + actor.send({ type: 'code_ready', code: 'code' }); + + // Send tests_agreed event + actor.send({ type: 'tests_agreed', testCases: 'test suite' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('Testing'); + expect(snapshot.context.testCases).toBe('test suite'); + }); + + it('should transition from TestCases to Ready on skip_tests event', () => { + const actor = createMetaProtocolMachine(config); + + // Navigate to TestCases state + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + actor.send({ type: 'code_ready', code: 'code' }); + + // Send skip_tests event + actor.send({ type: 'skip_tests' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('Ready'); + }); + + it('should transition from Testing to Ready on tests_passed event', () => { + const actor = createMetaProtocolMachine(config); + + // Navigate to Testing state + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + actor.send({ type: 'code_ready', code: 'code' }); + actor.send({ type: 'tests_agreed', testCases: 'tests' }); + + // Send tests_passed event + actor.send({ type: 'tests_passed' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('Ready'); + }); + + it('should transition from Testing to FixError on tests_failed event', () => { + const actor = createMetaProtocolMachine(config); + + // Navigate to Testing state + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + actor.send({ type: 'code_ready', code: 'code' }); + actor.send({ type: 'tests_agreed', testCases: 'tests' }); + + // Send tests_failed event + actor.send({ type: 'tests_failed', errors: 'test errors' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('FixError'); + expect(snapshot.context.errors).toContain('test errors'); + }); +}); + + +describe('MetaProtocolMachine - Error Fixing Flow', () => { + let mockIdentity: DIDIdentity; + let config: MetaProtocolConfig; + + beforeEach(() => { + mockIdentity = { + did: 'did:wba:example.com:agent1', + document: { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com:agent1', + verificationMethod: [], + authentication: [], + }, + privateKeys: new Map(), + }; + + config = { + localIdentity: mockIdentity, + remoteDID: 'did:wba:example.com:agent2', + }; + }); + + it('should transition from FixError to CodeGeneration on fix_accepted event', () => { + const actor = createMetaProtocolMachine(config); + + // Navigate to FixError state + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + actor.send({ type: 'code_ready', code: 'code' }); + actor.send({ type: 'tests_agreed', testCases: 'tests' }); + actor.send({ type: 'tests_failed', errors: 'errors' }); + + // Send fix_accepted event + actor.send({ type: 'fix_accepted', fix: 'fixed code' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('CodeGeneration'); + }); + + it('should transition from FixError to Failed on fix_rejected event', () => { + const actor = createMetaProtocolMachine(config); + + // Navigate to FixError state + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + actor.send({ type: 'code_ready', code: 'code' }); + actor.send({ type: 'tests_agreed', testCases: 'tests' }); + actor.send({ type: 'tests_failed', errors: 'errors' }); + + // Send fix_rejected event + actor.send({ type: 'fix_rejected', reason: 'cannot fix' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('Failed'); + }); + + it('should allow multiple fix attempts by cycling back to CodeGeneration', () => { + const actor = createMetaProtocolMachine(config); + + // Navigate to FixError state + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + actor.send({ type: 'code_ready', code: 'code' }); + actor.send({ type: 'tests_agreed', testCases: 'tests' }); + actor.send({ type: 'tests_failed', errors: 'errors' }); + + // Accept fix and go back to CodeGeneration + actor.send({ type: 'fix_accepted', fix: 'fix1' }); + expect(actor.getSnapshot().value).toBe('CodeGeneration'); + + // Can generate code again + actor.send({ type: 'code_ready', code: 'new code' }); + expect(actor.getSnapshot().value).toBe('TestCases'); + }); +}); + + +describe('MetaProtocolMachine - Communication Flow', () => { + let mockIdentity: DIDIdentity; + let config: MetaProtocolConfig; + + beforeEach(() => { + mockIdentity = { + did: 'did:wba:example.com:agent1', + document: { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com:agent1', + verificationMethod: [], + authentication: [], + }, + privateKeys: new Map(), + }; + + config = { + localIdentity: mockIdentity, + remoteDID: 'did:wba:example.com:agent2', + }; + }); + + it('should transition from Ready to Communicating on start_communication event', () => { + const actor = createMetaProtocolMachine(config); + + // Navigate to Ready state + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + actor.send({ type: 'code_ready', code: 'code' }); + actor.send({ type: 'skip_tests' }); + + // Send start_communication event + actor.send({ type: 'start_communication' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('Communicating'); + }); + + it('should transition from Communicating to FixError on protocol_error event', () => { + const actor = createMetaProtocolMachine(config); + + // Navigate to Communicating state + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + actor.send({ type: 'code_ready', code: 'code' }); + actor.send({ type: 'skip_tests' }); + actor.send({ type: 'start_communication' }); + + // Send protocol_error event + actor.send({ type: 'protocol_error', error: 'protocol mismatch' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('FixError'); + expect(snapshot.context.errors).toContain('protocol mismatch'); + }); + + it('should transition from Communicating to final state on end event', () => { + const actor = createMetaProtocolMachine(config); + + // Navigate to Communicating state + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + actor.send({ type: 'code_ready', code: 'code' }); + actor.send({ type: 'skip_tests' }); + actor.send({ type: 'start_communication' }); + + // Send end event + actor.send({ type: 'end' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.status).toBe('done'); + }); + + it('should remain in Ready state until start_communication', () => { + const actor = createMetaProtocolMachine(config); + + // Navigate to Ready state + actor.send({ type: 'initiate', candidateProtocols: 'protocol1' }); + actor.send({ type: 'accept', agreedProtocol: 'protocol1' }); + actor.send({ type: 'code_ready', code: 'code' }); + actor.send({ type: 'skip_tests' }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('Ready'); + }); +}); + + +describe('MetaProtocolMachine - Message Sending', () => { + let mockIdentity: DIDIdentity; + let config: MetaProtocolConfig; + + beforeEach(() => { + mockIdentity = { + did: 'did:wba:example.com:agent1', + document: { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com:agent1', + verificationMethod: [], + authentication: [], + }, + privateKeys: new Map(), + }; + + config = { + localIdentity: mockIdentity, + remoteDID: 'did:wba:example.com:agent2', + }; + }); + + it('should construct negotiation message with correct structure', async () => { + const actor = createMetaProtocolMachine(config); + const snapshot = actor.getSnapshot(); + + const message = { + action: 'protocolNegotiation', + sequenceId: snapshot.context.sequenceId, + candidateProtocols: 'protocol1,protocol2', + status: 'negotiating', + }; + + expect(message.action).toBe('protocolNegotiation'); + expect(message.sequenceId).toBeDefined(); + expect(message.candidateProtocols).toBe('protocol1,protocol2'); + expect(message.status).toBe('negotiating'); + }); + + it('should include modification summary when provided', () => { + const message = { + action: 'protocolNegotiation', + sequenceId: 1, + candidateProtocols: 'protocol1', + modificationSummary: 'Added support for feature X', + status: 'negotiating', + }; + + expect(message.modificationSummary).toBe('Added support for feature X'); + }); + + it('should construct code generation message', () => { + const message = { + action: 'codeGeneration', + status: 'generated', + }; + + expect(message.action).toBe('codeGeneration'); + expect(message.status).toBe('generated'); + }); + + it('should construct test cases negotiation message', () => { + const message = { + action: 'testCasesNegotiation', + testCases: 'test suite description', + status: 'negotiating', + }; + + expect(message.action).toBe('testCasesNegotiation'); + expect(message.testCases).toBe('test suite description'); + }); + + it('should construct fix error negotiation message', () => { + const message = { + action: 'fixErrorNegotiation', + errors: 'error description', + status: 'negotiating', + }; + + expect(message.action).toBe('fixErrorNegotiation'); + expect(message.errors).toBe('error description'); + }); +}); + + +describe('MetaProtocolMachine - Message Processing', () => { + let mockIdentity: DIDIdentity; + let config: MetaProtocolConfig; + + beforeEach(() => { + mockIdentity = { + did: 'did:wba:example.com:agent1', + document: { + '@context': ['https://www.w3.org/ns/did/v1'], + id: 'did:wba:example.com:agent1', + verificationMethod: [], + authentication: [], + }, + privateKeys: new Map(), + }; + + config = { + localIdentity: mockIdentity, + remoteDID: 'did:wba:example.com:agent2', + }; + }); + + it('should decode and process protocol negotiation message', async () => { + const { encodeMetaProtocolMessage, createNegotiationMessage } = await import('../../../src/protocol/meta-protocol-machine'); + + const message = createNegotiationMessage(1, 'protocol1', 'negotiating'); + const encoded = encodeMetaProtocolMessage(message); + + expect(encoded).toBeInstanceOf(Uint8Array); + expect(encoded.length).toBeGreaterThan(0); + }); + + it('should decode and process code generation message', async () => { + const { encodeMetaProtocolMessage, createCodeGenerationMessage } = await import('../../../src/protocol/meta-protocol-machine'); + + const message = createCodeGenerationMessage('generated'); + const encoded = encodeMetaProtocolMessage(message); + + expect(encoded).toBeInstanceOf(Uint8Array); + }); + + it('should decode and process test cases message', async () => { + const { encodeMetaProtocolMessage, createTestCasesMessage } = await import('../../../src/protocol/meta-protocol-machine'); + + const message = createTestCasesMessage('test cases', 'negotiating'); + const encoded = encodeMetaProtocolMessage(message); + + expect(encoded).toBeInstanceOf(Uint8Array); + }); + + it('should decode and process fix error message', async () => { + const { encodeMetaProtocolMessage, createFixErrorMessage } = await import('../../../src/protocol/meta-protocol-machine'); + + const message = createFixErrorMessage('error description', 'negotiating'); + const encoded = encodeMetaProtocolMessage(message); + + expect(encoded).toBeInstanceOf(Uint8Array); + }); + + it('should handle message decoding errors gracefully', async () => { + const { ProtocolMessageHandler } = await import('../../../src/protocol/message-handler'); + const handler = new ProtocolMessageHandler(); + + // Invalid message (empty) + expect(() => handler.decode(new Uint8Array(0))).toThrow(); + }); + + it('should map received negotiation message to state machine event', () => { + const actor = createMetaProtocolMachine(config); + + // Simulate receiving a negotiation request + actor.send({ + type: 'receive_request', + candidateProtocols: 'protocol1,protocol2', + sequenceId: 1, + }); + + const snapshot = actor.getSnapshot(); + expect(snapshot.value).toBe('Negotiating'); + expect(snapshot.context.candidateProtocols).toBe('protocol1,protocol2'); + }); +}); diff --git a/typescript/ts_sdk/tests/unit/setup.test.ts b/typescript/ts_sdk/tests/unit/setup.test.ts new file mode 100644 index 0000000..53c250a --- /dev/null +++ b/typescript/ts_sdk/tests/unit/setup.test.ts @@ -0,0 +1,12 @@ +import { describe, it, expect } from 'vitest'; +import { VERSION } from '../../src/index.js'; + +describe('SDK Setup', () => { + it('should export VERSION constant', () => { + expect(VERSION).toBe('0.1.0'); + }); + + it('should have correct version format', () => { + expect(VERSION).toMatch(/^\d+\.\d+\.\d+$/); + }); +}); diff --git a/typescript/ts_sdk/tests/unit/transport/http-client.test.ts b/typescript/ts_sdk/tests/unit/transport/http-client.test.ts new file mode 100644 index 0000000..b627089 --- /dev/null +++ b/typescript/ts_sdk/tests/unit/transport/http-client.test.ts @@ -0,0 +1,553 @@ +/** + * Unit tests for HTTP Client + */ + +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; +import { HTTPClient } from '../../../src/transport/http-client.js'; +import { AuthenticationManager } from '../../../src/core/auth/authentication-manager.js'; +import { DIDManager } from '../../../src/core/did/did-manager.js'; +import { NetworkError } from '../../../src/errors/index.js'; +import type { DIDIdentity } from '../../../src/types/index.js'; + +describe('HTTPClient', () => { + let httpClient: HTTPClient; + let authManager: AuthenticationManager; + let didManager: DIDManager; + let testIdentity: DIDIdentity; + + beforeEach(async () => { + didManager = new DIDManager(); + authManager = new AuthenticationManager(didManager, { + maxTokenAge: 3600000, + nonceLength: 32, + clockSkewTolerance: 60, + }); + + httpClient = new HTTPClient(authManager, { + timeout: 5000, + maxRetries: 3, + retryDelay: 100, + }); + + testIdentity = await didManager.createDID({ + domain: 'example.com', + path: 'user/alice', + }); + + // Clear all mocks before each test + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('GET requests', () => { + it('should make successful GET request', async () => { + const mockResponse = { data: 'test data' }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => mockResponse, + text: async () => JSON.stringify(mockResponse), + headers: new Headers(), + }); + + const response = await httpClient.get('https://api.example.com/data'); + + expect(fetch).toHaveBeenCalledWith( + 'https://api.example.com/data', + expect.objectContaining({ + method: 'GET', + }) + ); + + const data = await response.json(); + expect(data).toEqual(mockResponse); + }); + + it('should include timeout in request', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({}), + headers: new Headers(), + }); + + await httpClient.get('https://api.example.com/data'); + + expect(fetch).toHaveBeenCalledWith( + 'https://api.example.com/data', + expect.objectContaining({ + signal: expect.any(AbortSignal), + }) + ); + }); + + it('should handle GET request with query parameters', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({}), + headers: new Headers(), + }); + + await httpClient.get('https://api.example.com/data?param=value'); + + expect(fetch).toHaveBeenCalledWith( + 'https://api.example.com/data?param=value', + expect.any(Object) + ); + }); + }); + + describe('POST requests', () => { + it('should make successful POST request', async () => { + const requestBody = { message: 'test' }; + const mockResponse = { success: true }; + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => mockResponse, + headers: new Headers(), + }); + + const response = await httpClient.post( + 'https://api.example.com/submit', + requestBody + ); + + expect(fetch).toHaveBeenCalledWith( + 'https://api.example.com/submit', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify(requestBody), + headers: expect.objectContaining({ + 'Content-Type': 'application/json', + }), + }) + ); + + const data = await response.json(); + expect(data).toEqual(mockResponse); + }); + + it('should handle POST with empty body', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({}), + headers: new Headers(), + }); + + await httpClient.post('https://api.example.com/submit', {}); + + expect(fetch).toHaveBeenCalledWith( + 'https://api.example.com/submit', + expect.objectContaining({ + method: 'POST', + body: '{}', + }) + ); + }); + }); + + describe('authenticated requests', () => { + it('should include auth header in authenticated GET request', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({}), + headers: new Headers(), + }); + + await httpClient.get('https://api.example.com/protected', testIdentity); + + expect(fetch).toHaveBeenCalledWith( + 'https://api.example.com/protected', + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: expect.stringContaining('DIDWba'), + }), + }) + ); + }); + + it('should include auth header in authenticated POST request', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({}), + headers: new Headers(), + }); + + await httpClient.post( + 'https://api.example.com/protected', + { data: 'test' }, + testIdentity + ); + + expect(fetch).toHaveBeenCalledWith( + 'https://api.example.com/protected', + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: expect.stringContaining('DIDWba'), + }), + }) + ); + }); + + it('should extract domain from URL for auth header', async () => { + const generateAuthSpy = vi.spyOn(authManager, 'generateAuthHeader'); + + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({}), + headers: new Headers(), + }); + + await httpClient.get( + 'https://api.example.com:8080/protected', + testIdentity + ); + + expect(generateAuthSpy).toHaveBeenCalledWith( + testIdentity, + 'api.example.com', + expect.any(String) + ); + }); + + it('should not include auth header for unauthenticated requests', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({}), + headers: new Headers(), + }); + + await httpClient.get('https://api.example.com/public'); + + const callArgs = (fetch as any).mock.calls[0][1]; + expect(callArgs.headers?.Authorization).toBeUndefined(); + }); + }); + + describe('retry mechanism', () => { + it('should retry on network error', async () => { + let attemptCount = 0; + + global.fetch = vi.fn().mockImplementation(() => { + attemptCount++; + if (attemptCount < 3) { + return Promise.reject(new Error('Network error')); + } + return Promise.resolve({ + ok: true, + status: 200, + json: async () => ({ success: true }), + headers: new Headers(), + }); + }); + + const response = await httpClient.get('https://api.example.com/data'); + + expect(attemptCount).toBe(3); + expect(fetch).toHaveBeenCalledTimes(3); + + const data = await response.json(); + expect(data).toEqual({ success: true }); + }); + + it('should retry on 5xx server errors', async () => { + let attemptCount = 0; + + global.fetch = vi.fn().mockImplementation(() => { + attemptCount++; + if (attemptCount < 2) { + return Promise.resolve({ + ok: false, + status: 503, + statusText: 'Service Unavailable', + json: async () => ({}), + headers: new Headers(), + }); + } + return Promise.resolve({ + ok: true, + status: 200, + json: async () => ({ success: true }), + headers: new Headers(), + }); + }); + + const response = await httpClient.get('https://api.example.com/data'); + + expect(attemptCount).toBe(2); + const data = await response.json(); + expect(data).toEqual({ success: true }); + }); + + it('should not retry on 4xx client errors', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 404, + statusText: 'Not Found', + json: async () => ({ error: 'Not found' }), + headers: new Headers(), + }); + + await expect( + httpClient.get('https://api.example.com/notfound') + ).rejects.toThrow(NetworkError); + + expect(fetch).toHaveBeenCalledTimes(1); + }); + + it('should use exponential backoff between retries', async () => { + const timestamps: number[] = []; + + global.fetch = vi.fn().mockImplementation(() => { + timestamps.push(Date.now()); + if (timestamps.length < 3) { + return Promise.reject(new Error('Network error')); + } + return Promise.resolve({ + ok: true, + status: 200, + json: async () => ({}), + headers: new Headers(), + }); + }); + + await httpClient.get('https://api.example.com/data'); + + // Check that delays increase (exponential backoff) + if (timestamps.length >= 3) { + const delay1 = timestamps[1] - timestamps[0]; + const delay2 = timestamps[2] - timestamps[1]; + expect(delay2).toBeGreaterThanOrEqual(delay1); + } + }); + + it('should fail after max retries exceeded', async () => { + global.fetch = vi.fn().mockRejectedValue(new Error('Network error')); + + await expect( + httpClient.get('https://api.example.com/data') + ).rejects.toThrow(); + + // Should try initial + 3 retries = 4 times + expect(fetch).toHaveBeenCalledTimes(4); + }); + }); + + describe('timeout handling', () => { + it('should pass abort signal to fetch', async () => { + let receivedSignal: AbortSignal | undefined; + + global.fetch = vi.fn().mockImplementation((url, options) => { + receivedSignal = options?.signal; + return Promise.resolve({ + ok: true, + status: 200, + json: async () => ({}), + headers: new Headers(), + }); + }); + + await httpClient.get('https://api.example.com/data'); + + expect(receivedSignal).toBeDefined(); + expect(receivedSignal).toBeInstanceOf(AbortSignal); + }); + + it('should use configured timeout value', async () => { + const customClient = new HTTPClient(authManager, { + timeout: 100, // Very short timeout + maxRetries: 0, + retryDelay: 0, + }); + + global.fetch = vi.fn().mockImplementation((url, options) => { + return new Promise((resolve, reject) => { + // Listen to abort signal + if (options?.signal) { + options.signal.addEventListener('abort', () => { + reject(new Error('The operation was aborted')); + }); + } + + // Never resolve - let the timeout trigger + setTimeout(() => { + resolve({ + ok: true, + status: 200, + json: async () => ({}), + headers: new Headers(), + }); + }, 1000); // Longer than timeout + }); + }); + + await expect( + customClient.get('https://api.example.com/slow') + ).rejects.toThrow(NetworkError); + }, 1000); // Increase test timeout + }); + + describe('error handling', () => { + it('should throw NetworkError on fetch failure', async () => { + global.fetch = vi.fn().mockRejectedValue(new Error('Connection refused')); + + await expect( + httpClient.get('https://api.example.com/data') + ).rejects.toThrow(NetworkError); + }); + + it('should throw NetworkError with status code on HTTP error', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + json: async () => ({ error: 'Server error' }), + headers: new Headers(), + }); + + try { + await httpClient.get('https://api.example.com/data'); + expect.fail('Should have thrown NetworkError'); + } catch (error) { + expect(error).toBeInstanceOf(NetworkError); + expect((error as NetworkError).statusCode).toBe(500); + } + }); + + it('should include error message in NetworkError', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: false, + status: 403, + statusText: 'Forbidden', + json: async () => ({ error: 'Access denied' }), + headers: new Headers(), + }); + + try { + await httpClient.get('https://api.example.com/data'); + expect.fail('Should have thrown NetworkError'); + } catch (error) { + expect(error).toBeInstanceOf(NetworkError); + expect((error as NetworkError).message).toContain('403'); + } + }); + + it('should handle malformed JSON responses', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => { + throw new Error('Invalid JSON'); + }, + text: async () => 'not json', + headers: new Headers(), + }); + + const response = await httpClient.get('https://api.example.com/data'); + + // Should still return the response object + expect(response).toBeDefined(); + await expect(response.json()).rejects.toThrow(); + }); + + it('should handle network timeout errors', async () => { + global.fetch = vi.fn().mockImplementation(() => { + return Promise.reject(new Error('The operation was aborted')); + }); + + await expect( + httpClient.get('https://api.example.com/data') + ).rejects.toThrow(NetworkError); + }); + }); + + describe('request method', () => { + it('should support custom HTTP methods', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({}), + headers: new Headers(), + }); + + await httpClient.request('https://api.example.com/data', { + method: 'PUT', + body: JSON.stringify({ data: 'test' }), + }); + + expect(fetch).toHaveBeenCalledWith( + 'https://api.example.com/data', + expect.objectContaining({ + method: 'PUT', + }) + ); + }); + + it('should support custom headers', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({}), + headers: new Headers(), + }); + + await httpClient.request('https://api.example.com/data', { + method: 'GET', + headers: { + 'X-Custom-Header': 'custom-value', + }, + }); + + expect(fetch).toHaveBeenCalledWith( + 'https://api.example.com/data', + expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Custom-Header': 'custom-value', + }), + }) + ); + }); + + it('should merge custom headers with auth headers', async () => { + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({}), + headers: new Headers(), + }); + + await httpClient.request( + 'https://api.example.com/data', + { + method: 'GET', + headers: { + 'X-Custom-Header': 'custom-value', + }, + }, + testIdentity + ); + + expect(fetch).toHaveBeenCalledWith( + 'https://api.example.com/data', + expect.objectContaining({ + headers: expect.objectContaining({ + 'X-Custom-Header': 'custom-value', + Authorization: expect.stringContaining('DIDWba'), + }), + }) + ); + }); + }); +}); diff --git a/typescript/ts_sdk/tsconfig.json b/typescript/ts_sdk/tsconfig.json new file mode 100644 index 0000000..0363951 --- /dev/null +++ b/typescript/ts_sdk/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "DOM"], + "moduleResolution": "bundler", + "resolveJsonModule": true, + "allowJs": false, + "checkJs": false, + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "composite": false, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "types": ["node", "vitest/globals"] + }, + "include": ["src/**/*", "tests/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/typescript/ts_sdk/tsup.config.ts b/typescript/ts_sdk/tsup.config.ts new file mode 100644 index 0000000..ea5bfe8 --- /dev/null +++ b/typescript/ts_sdk/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + treeshake: true, + minify: false, + target: 'es2022', + outDir: 'dist', +}); diff --git a/typescript/ts_sdk/vitest.config.ts b/typescript/ts_sdk/vitest.config.ts new file mode 100644 index 0000000..0fb23bd --- /dev/null +++ b/typescript/ts_sdk/vitest.config.ts @@ -0,0 +1,30 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html', 'lcov'], + exclude: [ + 'node_modules/', + 'dist/', + 'tests/', + '**/*.test.ts', + '**/*.spec.ts', + '**/types/**', + 'examples/', + 'docs/', + ], + thresholds: { + lines: 80, + functions: 80, + branches: 80, + statements: 80, + }, + }, + include: ['tests/**/*.test.ts'], + exclude: ['node_modules', 'dist'], + }, +});