Skip to content

Commit 95d4d76

Browse files
committed
feat(sdk/js): browser compatible
1 parent efb4df6 commit 95d4d76

File tree

8 files changed

+521
-21
lines changed

8 files changed

+521
-21
lines changed

sdk/js/package.json

Lines changed: 69 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,97 @@
22
"name": "@phala/dstack-sdk",
33
"version": "0.5.3",
44
"description": "dstack SDK",
5-
"main": "dist/index.js",
6-
"types": "dist/index.d.ts",
5+
"main": "dist/node/index.js",
6+
"types": "dist/node/index.d.ts",
7+
"browser": {
8+
"crypto": "crypto-browserify"
9+
},
710
"exports": {
8-
".": "./dist/index.js",
11+
".": {
12+
"import": "./dist/node/index.js",
13+
"require": "./dist/node/index.js",
14+
"types": "./dist/node/index.d.ts"
15+
},
916
"./viem": {
10-
"import": "./dist/viem.js",
11-
"require": "./dist/viem.js",
12-
"types": "./dist/viem.d.ts"
17+
"import": "./dist/node/viem.js",
18+
"require": "./dist/node/viem.js",
19+
"types": "./dist/node/viem.d.ts"
1320
},
1421
"./encrypt-env-vars": {
15-
"import": "./dist/encrypt-env-vars.js",
16-
"require": "./dist/encrypt-env-vars.js",
17-
"types": "./dist/encrypt-env-vars.d.ts"
22+
"node": {
23+
"import": "./dist/node/encrypt-env-vars.js",
24+
"require": "./dist/node/encrypt-env-vars.js",
25+
"types": "./dist/node/encrypt-env-vars.d.ts"
26+
},
27+
"browser": {
28+
"import": "./dist/browser/encrypt-env-vars.browser.js",
29+
"require": "./dist/browser/encrypt-env-vars.browser.js",
30+
"types": "./dist/browser/encrypt-env-vars.browser.d.ts"
31+
},
32+
"default": {
33+
"import": "./dist/browser/encrypt-env-vars.browser.js",
34+
"require": "./dist/browser/encrypt-env-vars.browser.js",
35+
"types": "./dist/browser/encrypt-env-vars.browser.d.ts"
36+
}
1837
},
1938
"./solana": {
20-
"import": "./dist/solana.js",
21-
"require": "./dist/solana.js",
22-
"types": "./dist/solana.d.ts"
39+
"import": "./dist/node/solana.js",
40+
"require": "./dist/node/solana.js",
41+
"types": "./dist/node/solana.d.ts"
2342
},
2443
"./get-compose-hash": {
25-
"import": "./dist/get-compose-hash.js",
26-
"require": "./dist/get-compose-hash.js",
27-
"types": "./dist/get-compose-hash.d.ts"
44+
"node": {
45+
"import": "./dist/node/get-compose-hash.js",
46+
"require": "./dist/node/get-compose-hash.js",
47+
"types": "./dist/node/get-compose-hash.d.ts"
48+
},
49+
"browser": {
50+
"import": "./dist/browser/get-compose-hash.browser.js",
51+
"require": "./dist/browser/get-compose-hash.browser.js",
52+
"types": "./dist/browser/get-compose-hash.browser.d.ts"
53+
},
54+
"default": {
55+
"import": "./dist/browser/get-compose-hash.browser.js",
56+
"require": "./dist/browser/get-compose-hash.browser.js",
57+
"types": "./dist/browser/get-compose-hash.browser.d.ts"
58+
}
2859
},
2960
"./verify-env-encrypt-public-key": {
30-
"import": "./dist/verify-env-encrypt-public-key.js",
31-
"require": "./dist/verify-env-encrypt-public-key.js",
32-
"types": "./dist/verify-env-encrypt-public-key.d.ts"
61+
"node": {
62+
"import": "./dist/node/verify-env-encrypt-public-key.js",
63+
"require": "./dist/node/verify-env-encrypt-public-key.js",
64+
"types": "./dist/node/verify-env-encrypt-public-key.d.ts"
65+
},
66+
"browser": {
67+
"import": "./dist/browser/verify-env-encrypt-public-key.browser.js",
68+
"require": "./dist/browser/verify-env-encrypt-public-key.browser.js",
69+
"types": "./dist/browser/verify-env-encrypt-public-key.browser.d.ts"
70+
},
71+
"default": {
72+
"import": "./dist/browser/verify-env-encrypt-public-key.browser.js",
73+
"require": "./dist/browser/verify-env-encrypt-public-key.browser.js",
74+
"types": "./dist/browser/verify-env-encrypt-public-key.browser.d.ts"
75+
}
3376
}
3477
},
3578
"engines": {
3679
"node": ">=18.0.0"
3780
},
3881
"scripts": {
39-
"build": "tsc",
82+
"build": "npm run build:node && npm run build:browser",
83+
"build:node": "tsc -p tsconfig.node.json",
84+
"build:browser": "tsc -p tsconfig.browser.json",
85+
"clean": "rm -rf dist",
4086
"test": "vitest",
4187
"test:ci": "vitest --run",
42-
"release": "npm run build && npm publish --access public"
88+
"release": "npm run clean && npm run build && npm publish --access public"
4389
},
4490
"keywords": ["sdk", "dstack", "Phala"],
4591
"author": "Leechael Yim",
4692
"license": "Apache-2.0",
93+
"dependencies": {
94+
"crypto-browserify": "^3.12.0"
95+
},
4796
"devDependencies": {
4897
"@types/node": "latest",
4998
"typescript": "latest",
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
/**
2+
* Browser Compatibility Tests
3+
*
4+
* These tests ensure that browser versions have the same interfaces and produce
5+
* compatible outputs as their Node.js counterparts
6+
*/
7+
8+
import { describe, it, expect } from 'vitest'
9+
10+
// Import Node.js versions
11+
import * as nodeEncryptEnvVars from '../encrypt-env-vars'
12+
import * as nodeGetComposeHash from '../get-compose-hash'
13+
import * as nodeVerifyEnvEncryptPublicKey from '../verify-env-encrypt-public-key'
14+
15+
// Import browser versions
16+
import * as browserEncryptEnvVars from '../encrypt-env-vars.browser'
17+
import * as browserGetComposeHash from '../get-compose-hash.browser'
18+
import * as browserVerifyEnvEncryptPublicKey from '../verify-env-encrypt-public-key.browser'
19+
20+
describe('Browser Compatibility Tests', () => {
21+
22+
describe('Interface Compatibility', () => {
23+
it('should have matching exports - encrypt-env-vars', () => {
24+
// Check that both versions export the same interface
25+
expect(typeof browserEncryptEnvVars.encryptEnvVars).toBe('function')
26+
expect(typeof nodeEncryptEnvVars.encryptEnvVars).toBe('function')
27+
28+
// Check EnvVar interface exists (TypeScript will catch this at compile time)
29+
const testEnvVar: nodeEncryptEnvVars.EnvVar = { key: 'test', value: 'value' }
30+
const testEnvVarBrowser: browserEncryptEnvVars.EnvVar = { key: 'test', value: 'value' }
31+
32+
expect(testEnvVar).toEqual(testEnvVarBrowser)
33+
})
34+
35+
it('should have matching exports - get-compose-hash', () => {
36+
expect(typeof browserGetComposeHash.getComposeHash).toBe('function')
37+
expect(typeof nodeGetComposeHash.getComposeHash).toBe('function')
38+
})
39+
40+
it('should have matching exports - verify-env-encrypt-public-key', () => {
41+
expect(typeof browserVerifyEnvEncryptPublicKey.verifyEnvEncryptPublicKey).toBe('function')
42+
expect(typeof nodeVerifyEnvEncryptPublicKey.verifyEnvEncryptPublicKey).toBe('function')
43+
})
44+
})
45+
46+
describe('get-compose-hash Compatibility', () => {
47+
const testCases = [
48+
{ input: { services: { app: { image: 'nginx' } } } },
49+
{ input: { version: '3.8', services: { db: { image: 'postgres', environment: { POSTGRES_PASSWORD: 'secret' } } } } },
50+
{ input: { a: 1, b: 2, c: { nested: true, array: [1, 2, 3] } } },
51+
{ input: {} },
52+
{ input: { nullValue: null, undefinedValue: undefined, booleanValue: true, numberValue: 42 } }
53+
]
54+
55+
testCases.forEach((testCase, index) => {
56+
it(`should produce identical hash for test case ${index + 1}`, async () => {
57+
const nodeResult = await nodeGetComposeHash.getComposeHash(testCase.input)
58+
59+
try {
60+
const browserResult = await browserGetComposeHash.getComposeHash(testCase.input)
61+
expect(browserResult).toBe(nodeResult)
62+
expect(typeof browserResult).toBe('string')
63+
expect(browserResult).toMatch(/^[a-f0-9]{64}$/) // SHA-256 hex string
64+
} catch (error) {
65+
// Browser version may fail in Node.js test environment due to Web Crypto API
66+
console.log(`Browser version test skipped (Web Crypto API not available): ${error}`)
67+
expect(typeof nodeResult).toBe('string')
68+
expect(nodeResult).toMatch(/^[a-f0-9]{64}$/)
69+
}
70+
})
71+
})
72+
73+
it('should handle key ordering consistently', async () => {
74+
const obj1 = { z: 1, a: 2, m: 3 }
75+
const obj2 = { a: 2, m: 3, z: 1 }
76+
77+
const nodeResult1 = await nodeGetComposeHash.getComposeHash(obj1)
78+
const nodeResult2 = await nodeGetComposeHash.getComposeHash(obj2)
79+
80+
// Results should be identical regardless of input order
81+
expect(nodeResult1).toBe(nodeResult2)
82+
83+
try {
84+
const browserResult1 = await browserGetComposeHash.getComposeHash(obj1)
85+
const browserResult2 = await browserGetComposeHash.getComposeHash(obj2)
86+
87+
expect(browserResult1).toBe(browserResult2)
88+
expect(nodeResult1).toBe(browserResult1)
89+
} catch (error) {
90+
console.log(`Browser version test skipped: ${error}`)
91+
}
92+
})
93+
})
94+
95+
describe('encrypt-env-vars Interface Compatibility', () => {
96+
const testEnvVars: nodeEncryptEnvVars.EnvVar[] = [
97+
{ key: 'TEST_KEY', value: 'test_value' },
98+
{ key: 'ANOTHER_KEY', value: 'another_value' }
99+
]
100+
const testPublicKey = '1234567890abcdef'.repeat(4) // 64 char hex string
101+
102+
it('should accept the same input parameters', async () => {
103+
// Both should accept the same parameters without throwing
104+
expect(async () => {
105+
await nodeEncryptEnvVars.encryptEnvVars(testEnvVars, testPublicKey)
106+
}).not.toThrow()
107+
108+
expect(async () => {
109+
await browserEncryptEnvVars.encryptEnvVars(testEnvVars, testPublicKey)
110+
}).not.toThrow()
111+
})
112+
113+
it('should return hex-encoded strings', async () => {
114+
try {
115+
const nodeResult = await nodeEncryptEnvVars.encryptEnvVars(testEnvVars, testPublicKey)
116+
expect(typeof nodeResult).toBe('string')
117+
expect(nodeResult).toMatch(/^[a-f0-9]+$/) // Hex string
118+
} catch (error) {
119+
// Node version might fail if simulator not available, that's ok for interface test
120+
console.log('Node version failed (expected in test environment):', error)
121+
}
122+
123+
try {
124+
const browserResult = await browserEncryptEnvVars.encryptEnvVars(testEnvVars, testPublicKey)
125+
expect(typeof browserResult).toBe('string')
126+
expect(browserResult).toMatch(/^[a-f0-9]+$/) // Hex string
127+
} catch (error) {
128+
// Browser version might fail if X25519 not supported, that's ok for interface test
129+
console.log('Browser version failed (might not support X25519):', error)
130+
}
131+
})
132+
133+
it('should validate input parameters consistently', async () => {
134+
const emptyEnvVars: nodeEncryptEnvVars.EnvVar[] = []
135+
136+
// Both should handle empty input arrays
137+
try {
138+
await nodeEncryptEnvVars.encryptEnvVars(emptyEnvVars, testPublicKey)
139+
// If Node version doesn't throw, that's ok
140+
} catch (error) {
141+
// Node version may throw, which is fine
142+
}
143+
144+
try {
145+
await browserEncryptEnvVars.encryptEnvVars(emptyEnvVars, testPublicKey)
146+
// If browser version doesn't throw, that's ok
147+
} catch (error) {
148+
// Browser version may throw due to Web Crypto API availability
149+
}
150+
151+
// Just ensure both functions exist and can be called
152+
expect(typeof nodeEncryptEnvVars.encryptEnvVars).toBe('function')
153+
expect(typeof browserEncryptEnvVars.encryptEnvVars).toBe('function')
154+
})
155+
})
156+
157+
describe('verify-env-encrypt-public-key Interface Compatibility', () => {
158+
const testPublicKey = new Uint8Array(32).fill(1) // 32 bytes
159+
const testSignature = new Uint8Array(65).fill(2) // 65 bytes
160+
const testAppId = 'test-app-id'
161+
162+
it('should accept the same input parameters', async () => {
163+
const nodeResult = await nodeVerifyEnvEncryptPublicKey.verifyEnvEncryptPublicKey(
164+
testPublicKey, testSignature, testAppId
165+
)
166+
const browserResult = await browserVerifyEnvEncryptPublicKey.verifyEnvEncryptPublicKey(
167+
testPublicKey, testSignature, testAppId
168+
)
169+
170+
// Both should return string or null
171+
expect(nodeResult === null || typeof nodeResult === 'string').toBeTruthy()
172+
expect(browserResult === null || typeof browserResult === 'string').toBeTruthy()
173+
})
174+
175+
it('should validate input parameters consistently', async () => {
176+
const invalidPublicKey = new Uint8Array(16) // Wrong size
177+
const invalidSignature = new Uint8Array(32) // Wrong size
178+
179+
// Both should handle invalid inputs similarly
180+
const nodeResult1 = await nodeVerifyEnvEncryptPublicKey.verifyEnvEncryptPublicKey(
181+
invalidPublicKey, testSignature, testAppId
182+
)
183+
const browserResult1 = await browserVerifyEnvEncryptPublicKey.verifyEnvEncryptPublicKey(
184+
invalidPublicKey, testSignature, testAppId
185+
)
186+
187+
const nodeResult2 = await nodeVerifyEnvEncryptPublicKey.verifyEnvEncryptPublicKey(
188+
testPublicKey, invalidSignature, testAppId
189+
)
190+
const browserResult2 = await browserVerifyEnvEncryptPublicKey.verifyEnvEncryptPublicKey(
191+
testPublicKey, invalidSignature, testAppId
192+
)
193+
194+
// Both should return null for invalid inputs (or handle errors consistently)
195+
expect(nodeResult1).toBeNull()
196+
expect(browserResult1).toBeNull()
197+
expect(nodeResult2).toBeNull()
198+
expect(browserResult2).toBeNull()
199+
})
200+
201+
it('should handle empty/invalid app ID consistently', async () => {
202+
const nodeResult = await nodeVerifyEnvEncryptPublicKey.verifyEnvEncryptPublicKey(
203+
testPublicKey, testSignature, ''
204+
)
205+
const browserResult = await browserVerifyEnvEncryptPublicKey.verifyEnvEncryptPublicKey(
206+
testPublicKey, testSignature, ''
207+
)
208+
209+
expect(nodeResult).toBeNull()
210+
expect(browserResult).toBeNull()
211+
})
212+
})
213+
214+
describe('Function Signatures', () => {
215+
it('should have matching function signatures', () => {
216+
// These checks ensure TypeScript compatibility
217+
const nodeEncryptFn: typeof nodeEncryptEnvVars.encryptEnvVars = browserEncryptEnvVars.encryptEnvVars
218+
const nodeHashFn: typeof nodeGetComposeHash.getComposeHash = browserGetComposeHash.getComposeHash
219+
const nodeVerifyFn: typeof nodeVerifyEnvEncryptPublicKey.verifyEnvEncryptPublicKey = browserVerifyEnvEncryptPublicKey.verifyEnvEncryptPublicKey
220+
221+
expect(typeof nodeEncryptFn).toBe('function')
222+
expect(typeof nodeHashFn).toBe('function')
223+
expect(typeof nodeVerifyFn).toBe('function')
224+
})
225+
})
226+
})

0 commit comments

Comments
 (0)