@swimmingkiim/trust-sdk 0.1.32 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,25 +1,139 @@
1
- import { initAgent } from '../agent'
1
+ import crypto from 'crypto'
2
+ import { createResolver } from '../resolver'
3
+ import { Resolver } from 'did-resolver'
4
+
5
+ export interface EphemeralIdentity {
6
+ did: string
7
+ keyPair: {
8
+ publicKey: Uint8Array
9
+ secretKey: Uint8Array
10
+ }
11
+ }
12
+
13
+ // Ed25519 multicodec prefix (0xed01)
14
+ const ED25519_MULTICODEC_PREFIX = new Uint8Array([0xed, 0x01])
15
+
16
+ // Base58btc alphabet (Bitcoin alphabet)
17
+ const BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
18
+
19
+ /**
20
+ * Encodes a Uint8Array to base58btc string.
21
+ * Pure implementation with no external dependencies.
22
+ */
23
+ function base58btcEncode(bytes: Uint8Array): string {
24
+ // Count leading zeros
25
+ let leadingZeros = 0
26
+ for (let i = 0; i < bytes.length && bytes[i] === 0; i++) {
27
+ leadingZeros++
28
+ }
29
+
30
+ // Convert to BigInt for base conversion
31
+ let num = BigInt('0x' + Buffer.from(bytes).toString('hex'))
32
+ const chars: string[] = []
33
+
34
+ while (num > 0n) {
35
+ const remainder = Number(num % 58n)
36
+ chars.unshift(BASE58_ALPHABET[remainder])
37
+ num = num / 58n
38
+ }
39
+
40
+ // Add '1' for each leading zero byte
41
+ for (let i = 0; i < leadingZeros; i++) {
42
+ chars.unshift('1')
43
+ }
44
+
45
+ return chars.join('')
46
+ }
47
+
48
+ /**
49
+ * Encodes an Ed25519 public key as a did:key identifier.
50
+ * Format: did:key:z + base58btc(multicodec_prefix + public_key)
51
+ */
52
+ function publicKeyToDidKey(publicKey: Uint8Array): string {
53
+ const multicodecKey = new Uint8Array(ED25519_MULTICODEC_PREFIX.length + publicKey.length)
54
+ multicodecKey.set(ED25519_MULTICODEC_PREFIX)
55
+ multicodecKey.set(publicKey, ED25519_MULTICODEC_PREFIX.length)
56
+ return `did:key:z${base58btcEncode(multicodecKey)}`
57
+ }
2
58
 
3
59
  export class IdentityManager {
4
- async createEphemeralDID() {
5
- const agent = await initAgent()
6
- const identifier = await agent.didManagerCreate({
7
- provider: 'did:key'
8
- })
9
- return identifier
60
+ private resolver: Resolver
61
+
62
+ constructor(resolver?: Resolver) {
63
+ this.resolver = resolver ?? createResolver()
10
64
  }
11
65
 
12
- async createPersistentDID(alias?: string) {
13
- const agent = await initAgent()
14
- const identifier = await agent.didManagerCreate({
15
- provider: 'did:ethr',
16
- alias
17
- })
18
- return identifier
66
+ /**
67
+ * Creates an ephemeral (in-memory) did:key identity.
68
+ * Uses Node.js built-in crypto — no external dependencies, no database.
69
+ */
70
+ async createEphemeralDID(): Promise<EphemeralIdentity> {
71
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519')
72
+
73
+ // Export raw key bytes from DER format
74
+ const pubDer = publicKey.export({ type: 'spki', format: 'der' })
75
+ const privDer = privateKey.export({ type: 'pkcs8', format: 'der' })
76
+
77
+ // Ed25519 SPKI DER: 12-byte header + 32-byte key
78
+ const rawPublicKey = new Uint8Array(pubDer.subarray(pubDer.length - 32))
79
+ // Ed25519 PKCS8 DER: 16-byte header + 32-byte key
80
+ const rawSecretKey = new Uint8Array(privDer.subarray(privDer.length - 32))
81
+
82
+ const did = publicKeyToDidKey(rawPublicKey)
83
+
84
+ return {
85
+ did,
86
+ keyPair: { publicKey: rawPublicKey, secretKey: rawSecretKey },
87
+ }
19
88
  }
20
89
 
21
- async resolveDID(did: string) {
22
- const agent = await initAgent()
23
- return await agent.resolveDid({ didUrl: did })
90
+ /**
91
+ * Resolves a DID to its DID Document.
92
+ * Stateless: uses blockchain (did:ethr) or derivation (did:key).
93
+ */
94
+ async resolveDID(did: string): Promise<any> {
95
+ return await this.resolver.resolve(did)
96
+ }
97
+
98
+ /**
99
+ * Imports an existing Ed25519 secret key and derives the DID.
100
+ * Use this when you already have a fixed key (e.g., from did_keys.json).
101
+ *
102
+ * @param secretKey - 32-byte Ed25519 secret key as hex string or Uint8Array
103
+ * @returns EphemeralIdentity with the derived DID and key pair
104
+ */
105
+ async fromSecretKey(secretKey: string | Uint8Array): Promise<EphemeralIdentity> {
106
+ const rawSecretKey = typeof secretKey === 'string'
107
+ ? new Uint8Array(Buffer.from(secretKey.replace(/^0x/, ''), 'hex'))
108
+ : secretKey
109
+
110
+ if (rawSecretKey.length !== 32) {
111
+ throw new Error(`Invalid Ed25519 secret key length: expected 32 bytes, got ${rawSecretKey.length}`)
112
+ }
113
+
114
+ // Reconstruct Node.js KeyObject from raw bytes
115
+ // Ed25519 PKCS8 DER = fixed 16-byte header + 32-byte raw key
116
+ const pkcs8Header = Buffer.from([
117
+ 0x30, 0x2e, 0x02, 0x01, 0x00, 0x30, 0x05, 0x06,
118
+ 0x03, 0x2b, 0x65, 0x70, 0x04, 0x22, 0x04, 0x20,
119
+ ])
120
+ const pkcs8Der = Buffer.concat([pkcs8Header, Buffer.from(rawSecretKey)])
121
+ const privateKeyObject = crypto.createPrivateKey({
122
+ key: pkcs8Der,
123
+ format: 'der',
124
+ type: 'pkcs8',
125
+ })
126
+
127
+ // Derive public key
128
+ const publicKeyObject = crypto.createPublicKey(privateKeyObject)
129
+ const pubDer = publicKeyObject.export({ type: 'spki', format: 'der' })
130
+ const rawPublicKey = new Uint8Array(pubDer.subarray(pubDer.length - 32))
131
+
132
+ const did = publicKeyToDidKey(rawPublicKey)
133
+
134
+ return {
135
+ did,
136
+ keyPair: { publicKey: rawPublicKey, secretKey: rawSecretKey },
137
+ }
24
138
  }
25
139
  }
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
- export * from './identity/did-manager'
2
- export * from './credentials/vc-handler.service'
3
- export * from './agent'
1
+ export { IdentityManager } from './identity/did-manager'
2
+ export type { EphemeralIdentity } from './identity/did-manager'
3
+ export { VCHandler } from './credentials/vc-handler.service'
4
+ export { createResolver } from './resolver'
5
+ export type { ResolverOptions } from './resolver'
@@ -0,0 +1,26 @@
1
+ import { Resolver } from 'did-resolver'
2
+ import { getResolver as getKeyResolver } from 'key-did-resolver'
3
+ import { getResolver as getEthrResolver } from 'ethr-did-resolver'
4
+
5
+ export interface ResolverOptions {
6
+ /** Ethereum network name for did:ethr (default: 'base') */
7
+ ethNetwork?: string
8
+ /** RPC URL for did:ethr resolution */
9
+ rpcUrl?: string
10
+ }
11
+
12
+ /**
13
+ * Creates a stateless DID Resolver.
14
+ * No database, no state — only blockchain/key-based resolution.
15
+ */
16
+ export function createResolver(options?: ResolverOptions): Resolver {
17
+ const ethNetwork = options?.ethNetwork || 'base'
18
+ const rpcUrl = options?.rpcUrl || 'https://mainnet.base.org'
19
+
20
+ return new Resolver({
21
+ ...getKeyResolver(),
22
+ ...getEthrResolver({
23
+ networks: [{ name: ethNetwork, rpcUrl }],
24
+ }),
25
+ })
26
+ }
@@ -1,68 +1,58 @@
1
- import { describe, it, expect, beforeAll, vi } from 'vitest'
1
+ import { describe, it, expect } from 'vitest'
2
+ import { IdentityManager } from '../src/identity/did-manager'
2
3
  import { VCHandler } from '../src/credentials/vc-handler.service'
3
4
 
4
- // Mocking agent to simulate verification failure on tampered data
5
- vi.mock('../src/agent', () => {
6
- const mockAgent = {
7
- verifyCredential: vi.fn().mockImplementation(async ({ credential }) => {
8
- // Check for expiration FIRST
9
- if (credential.expirationDate === '1999-01-01T00:00:00Z') {
10
- return { verified: false, error: 'Credential expired' }
11
- }
12
- // Then check for signature
13
- if (credential.proof && credential.proof.jwt === 'valid_signature') {
14
- return { verified: true }
15
- }
16
- return { verified: false, error: 'Invalid signature' }
17
- }),
18
- createVerifiableCredential: vi.fn().mockResolvedValue({
19
- proof: { jwt: 'valid_signature' },
20
- credentialSubject: { id: 'did:example:123', name: 'Test' },
21
- issuanceDate: new Date().toISOString()
22
- })
23
- }
24
- return {
25
- agent: mockAgent,
26
- // Ensure initAgent returns the object containing verify/create methods
27
- initAgent: vi.fn().mockResolvedValue(mockAgent)
28
- }
29
- })
30
-
5
+ /**
6
+ * Security Tests (No Mocks — Real JWT Verification)
7
+ */
31
8
  describe('a2trust: Security Tests', () => {
9
+ let idManager: IdentityManager
32
10
  let vcHandler: VCHandler
33
11
 
34
- beforeAll(async () => {
35
- const { agent: mockAgent } = await import('../src/agent')
36
- console.error('DEBUG: Security Test mockAgent keys:', Object.keys(mockAgent))
37
- vcHandler = new VCHandler(mockAgent as any)
38
- })
12
+ idManager = new IdentityManager()
13
+ vcHandler = new VCHandler()
39
14
 
40
- it('Security: Should reject tampered VC', async () => {
41
- // 1. Create a "valid" VC (MOCKED)
42
- const validVC = await vcHandler.createCredential('issuer', 'subject', {})
15
+ it('should reject a tampered JWT', async () => {
16
+ const identity = await idManager.createEphemeralDID()
43
17
 
44
- // 2. Tamper with the VC (e.g., change the signature)
45
- const tamperedVC = { ...validVC, proof: { jwt: 'malicious_signature' } }
18
+ const validJwt = await vcHandler.createCredential(
19
+ identity.did,
20
+ identity.did,
21
+ { walletAddress: '0x123' },
22
+ identity.keyPair
23
+ )
46
24
 
47
- // 3. Verify -> Should rely on our mock logic simulating failure
48
- // Import mocked agent
49
- const { agent } = await import('../src/agent')
50
- const result = await agent.verifyCredential({ credential: tamperedVC as any })
25
+ // Tamper with the JWT payload
26
+ const parts = validJwt.split('.')
27
+ const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString())
28
+ payload.vc.credentialSubject.walletAddress = '0xHACKED'
29
+ parts[1] = Buffer.from(JSON.stringify(payload)).toString('base64url')
30
+ const tamperedJwt = parts.join('.')
51
31
 
52
- expect(result.verified).toBe(false)
53
- expect(result.error).toBeDefined()
32
+ const result = await vcHandler.verifyCredential(tamperedJwt)
33
+ expect(result).toBe(false)
54
34
  })
55
35
 
56
- it('Security: Should reject expired VC', async () => {
57
- const { agent } = await import('../src/agent')
58
-
59
- const expiredVC = {
60
- proof: { jwt: 'valid_signature' },
61
- expirationDate: '1999-01-01T00:00:00Z'
62
- }
36
+ it('should reject a completely invalid JWT string', async () => {
37
+ const result = await vcHandler.verifyCredential('not.a.valid.jwt')
38
+ expect(result).toBe(false)
39
+ })
63
40
 
64
- const result = await agent.verifyCredential({ credential: expiredVC as any })
65
- expect(result.verified).toBe(false)
66
- expect(result.error).toMatch(/expired/)
41
+ it('should reject a JWT signed by unknown issuer', async () => {
42
+ // Create a VC signed by identity A
43
+ const identityA = await idManager.createEphemeralDID()
44
+ const identityB = await idManager.createEphemeralDID()
45
+
46
+ // Sign with A's key but claim issuer is B
47
+ // This should fail because B's DID document won't match A's key
48
+ const vcJwt = await vcHandler.createCredential(
49
+ identityB.did, // claim to be B
50
+ identityB.did,
51
+ { walletAddress: '0x123' },
52
+ identityA.keyPair // but sign with A's key
53
+ )
54
+
55
+ const result = await vcHandler.verifyCredential(vcJwt)
56
+ expect(result).toBe(false)
67
57
  })
68
58
  })
@@ -1,69 +1,81 @@
1
- import { describe, it, expect, beforeAll, vi } from 'vitest'
1
+ import { describe, it, expect } from 'vitest'
2
2
  import { IdentityManager } from '../src/identity/did-manager'
3
3
  import { VCHandler } from '../src/credentials/vc-handler.service'
4
- import { agent } from '../src/agent'
5
-
6
- // Mock the agent module to avoid loading Veramo dependencies which cause syntax errors in Node 22
7
- vi.mock('../src/agent', () => {
8
- const mockAgent = {
9
- didManagerCreate: vi.fn().mockResolvedValue({ did: 'did:key:mocked123' }),
10
- createVerifiableCredential: vi.fn().mockResolvedValue({
11
- proof: { jwt: 'mock_jwt' },
12
- credentialSubject: { id: 'did:key:mocked123', name: 'Test Agent' }
13
- }),
14
- verifyCredential: vi.fn().mockResolvedValue({ verified: true })
15
- }
16
- return {
17
- agent: mockAgent,
18
- initAgent: vi.fn().mockResolvedValue(mockAgent)
19
- }
20
- })
21
4
 
5
+ /**
6
+ * Identity & Credentials Tests (No Mocks)
7
+ *
8
+ * These test REAL in-memory behavior — no vi.mock, no DB.
9
+ */
22
10
  describe('a2trust: Identity & Credentials', () => {
23
11
  let idManager: IdentityManager
24
12
  let vcHandler: VCHandler
25
- let issuerDid: string
26
- let subjectDid: string
27
-
28
- beforeAll(async () => {
29
- // Since we mocked the module, imports of ../src/agent return the mock
30
- const { agent: mockAgent } = await import('../src/agent')
31
13
 
32
- idManager = new IdentityManager()
33
- // Inject the mocked agent into VCHandler
34
- vcHandler = new VCHandler(mockAgent as any)
35
- })
14
+ idManager = new IdentityManager()
15
+ vcHandler = new VCHandler()
36
16
 
37
17
  it('should create an ephemeral did:key', async () => {
38
- // This calls idManager -> initAgent (mocked) -> agent.didManagerCreate (mocked)
39
- const did = await idManager.createEphemeralDID()
40
- expect(did).toBeDefined()
41
- expect(did.did).toMatch(/^did:key:/)
42
- issuerDid = did.did
18
+ const result = await idManager.createEphemeralDID()
19
+ expect(result).toBeDefined()
20
+ expect(result.did).toMatch(/^did:key:/)
43
21
  })
44
22
 
45
- it('should create a persistent did:ethr', async () => {
46
- // Mock returns same structure
47
- const did = await idManager.createEphemeralDID()
48
- subjectDid = did.did
49
- expect(subjectDid).toBeDefined()
23
+ it('should resolve a created DID', async () => {
24
+ const created = await idManager.createEphemeralDID()
25
+ const resolved = await idManager.resolveDID(created.did)
26
+
27
+ expect(resolved).toBeDefined()
28
+ expect(resolved.didDocument).toBeDefined()
29
+ expect(resolved.didDocument?.id).toBe(created.did)
50
30
  })
51
31
 
52
- it('should issue a Verifiable Credential', async () => {
32
+ it('should issue a Verifiable Credential as JWT', async () => {
33
+ const identity = await idManager.createEphemeralDID()
53
34
  const claims = { name: 'Test Agent', role: 'Tester' }
54
- const vc = await vcHandler.createCredential(issuerDid, subjectDid, claims)
35
+
36
+ const vc = await vcHandler.createCredential(
37
+ identity.did,
38
+ identity.did,
39
+ claims,
40
+ identity.keyPair
41
+ )
55
42
 
56
43
  expect(vc).toBeDefined()
57
- expect(vc.proof).toBeDefined()
58
- expect(vc.credentialSubject.name).toBe('Test Agent')
44
+ expect(typeof vc).toBe('string')
45
+
46
+ // Should be a valid JWT (3 parts separated by dots)
47
+ const parts = vc.split('.')
48
+ expect(parts.length).toBe(3)
59
49
  })
60
50
 
61
- it('should verify a Verifiable Credential', async () => {
51
+ it('should verify a valid Verifiable Credential', async () => {
52
+ const identity = await idManager.createEphemeralDID()
62
53
  const claims = { name: 'Verified Agent' }
63
- const vc = await vcHandler.createCredential(issuerDid, subjectDid, claims)
64
54
 
65
- // Use the imported (mocked) agent to verify
66
- const result = await agent.verifyCredential({ credential: vc as any })
67
- expect(result.verified).toBe(true)
55
+ const vcJwt = await vcHandler.createCredential(
56
+ identity.did,
57
+ identity.did,
58
+ claims,
59
+ identity.keyPair
60
+ )
61
+
62
+ const result = await vcHandler.verifyCredential(vcJwt)
63
+ expect(result).toBe(true)
64
+ })
65
+
66
+ it('createCredential should include claims in the VC payload', async () => {
67
+ const identity = await idManager.createEphemeralDID()
68
+ const claims = { walletAddress: '0xABCDEF1234567890' }
69
+
70
+ const vcJwt = await vcHandler.createCredential(
71
+ identity.did,
72
+ identity.did,
73
+ claims,
74
+ identity.keyPair
75
+ )
76
+
77
+ // Decode the JWT payload
78
+ const payload = JSON.parse(Buffer.from(vcJwt.split('.')[1], 'base64').toString())
79
+ expect(payload.vc.credentialSubject.walletAddress).toBe('0xABCDEF1234567890')
68
80
  })
69
81
  })
@@ -0,0 +1,133 @@
1
+ import { describe, it, expect } from 'vitest'
2
+
3
+ /**
4
+ * Stateless Property Tests
5
+ *
6
+ * These tests verify that trust-sdk operates without ANY database dependency.
7
+ * No environment variables, no DB connections, no external state.
8
+ */
9
+ describe('trust-sdk: Stateless Properties', () => {
10
+ it('should create an IdentityManager without any env vars or DB', async () => {
11
+ // Must import without AGENT_SECRET_KEY or DB env vars set
12
+ const { IdentityManager } = await import('../src/identity/did-manager')
13
+ const idManager = new IdentityManager()
14
+ expect(idManager).toBeDefined()
15
+ })
16
+
17
+ it('should create a VCHandler without any env vars or DB', async () => {
18
+ const { VCHandler } = await import('../src/credentials/vc-handler.service')
19
+ const vcHandler = new VCHandler()
20
+ expect(vcHandler).toBeDefined()
21
+ })
22
+
23
+ it('should create a DID without any database', async () => {
24
+ const { IdentityManager } = await import('../src/identity/did-manager')
25
+ const idManager = new IdentityManager()
26
+
27
+ const result = await idManager.createEphemeralDID()
28
+ expect(result).toBeDefined()
29
+ expect(result.did).toMatch(/^did:key:/)
30
+ // Must return a key pair (or at least the DID string)
31
+ expect(typeof result.did).toBe('string')
32
+ })
33
+
34
+ it('should resolve a did:key without any database', async () => {
35
+ const { IdentityManager } = await import('../src/identity/did-manager')
36
+ const idManager = new IdentityManager()
37
+
38
+ const created = await idManager.createEphemeralDID()
39
+ const resolved = await idManager.resolveDID(created.did)
40
+
41
+ expect(resolved).toBeDefined()
42
+ expect(resolved.didDocument).toBeDefined()
43
+ expect(resolved.didDocument?.id).toBe(created.did)
44
+ })
45
+
46
+ it('should issue and verify a VC E2E without any database', async () => {
47
+ const { IdentityManager } = await import('../src/identity/did-manager')
48
+ const { VCHandler } = await import('../src/credentials/vc-handler.service')
49
+
50
+ const idManager = new IdentityManager()
51
+ const vcHandler = new VCHandler()
52
+
53
+ // Create a DID
54
+ const identity = await idManager.createEphemeralDID()
55
+
56
+ // Issue a self-signed VC
57
+ const vc = await vcHandler.createCredential(
58
+ identity.did,
59
+ identity.did,
60
+ { walletAddress: '0x1234567890abcdef' },
61
+ identity.keyPair // pass the signing key
62
+ )
63
+
64
+ expect(vc).toBeDefined()
65
+ expect(typeof vc).toBe('string') // JWT string
66
+
67
+ // Verify the VC
68
+ const isValid = await vcHandler.verifyCredential(vc)
69
+ expect(isValid).toBe(true)
70
+ })
71
+
72
+ it('should export createResolver from the SDK', async () => {
73
+ const sdk = await import('../src/index')
74
+ expect(sdk.createResolver).toBeDefined()
75
+ expect(typeof sdk.createResolver).toBe('function')
76
+ })
77
+
78
+ it('should NOT export agent or initAgent', async () => {
79
+ const sdk = await import('../src/index')
80
+ expect((sdk as any).agent).toBeUndefined()
81
+ expect((sdk as any).initAgent).toBeUndefined()
82
+ })
83
+
84
+ it('should import an existing secret key via fromSecretKey (hex string)', async () => {
85
+ const { IdentityManager } = await import('../src/identity/did-manager')
86
+ const idManager = new IdentityManager()
87
+
88
+ // Generate a key first, then re-import it
89
+ const original = await idManager.createEphemeralDID()
90
+ const hexKey = Buffer.from(original.keyPair.secretKey).toString('hex')
91
+
92
+ const imported = await idManager.fromSecretKey(hexKey)
93
+ expect(imported.did).toBe(original.did) // same key = same DID
94
+ expect(imported.keyPair.publicKey).toEqual(original.keyPair.publicKey)
95
+ })
96
+
97
+ it('should import a 0x-prefixed hex secret key', async () => {
98
+ const { IdentityManager } = await import('../src/identity/did-manager')
99
+ const idManager = new IdentityManager()
100
+
101
+ const original = await idManager.createEphemeralDID()
102
+ const hexKey = '0x' + Buffer.from(original.keyPair.secretKey).toString('hex')
103
+
104
+ const imported = await idManager.fromSecretKey(hexKey)
105
+ expect(imported.did).toBe(original.did)
106
+ })
107
+
108
+ it('should issue and verify a VC using imported key', async () => {
109
+ const { IdentityManager } = await import('../src/identity/did-manager')
110
+ const { VCHandler } = await import('../src/credentials/vc-handler.service')
111
+
112
+ const idManager = new IdentityManager()
113
+ const vcHandler = new VCHandler()
114
+
115
+ // Simulate a bot with a fixed key
116
+ const original = await idManager.createEphemeralDID()
117
+ const hexKey = Buffer.from(original.keyPair.secretKey).toString('hex')
118
+
119
+ // Import the key (as a bot would)
120
+ const identity = await idManager.fromSecretKey(hexKey)
121
+
122
+ // Create and verify VC
123
+ const vc = await vcHandler.createCredential(
124
+ identity.did,
125
+ identity.did,
126
+ { walletAddress: '0xBotWallet123' },
127
+ identity.keyPair
128
+ )
129
+
130
+ const isValid = await vcHandler.verifyCredential(vc)
131
+ expect(isValid).toBe(true)
132
+ })
133
+ })