Skip to content

Commit 6e4cf3f

Browse files
🔄 synced file(s) with circlefin/modularwallets-web-sdk-internal (#18)
synced local file(s) with [circlefin/modularwallets-web-sdk-internal](https://github.com/circlefin/modularwallets-web-sdk-internal). <details> <summary>Changed files</summary> <ul> <li>synced local directory <code>packages/</code> with remote directory <code>packages/</code></li> </ul> </details> --- This PR was created automatically by the [repo-file-sync-action](https://github.com/BetaHuhn/repo-file-sync-action) workflow run [#16598764014](https://github.com/circlefin/modularwallets-web-sdk-internal/actions/runs/16598764014)
1 parent ed28920 commit 6e4cf3f

File tree

9 files changed

+467
-5
lines changed

9 files changed

+467
-5
lines changed

‎packages/w3s-web-core-sdk/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@circle-fin/modular-wallets-core",
33
"description": "Serverless Typescript SDK",
4-
"version": "1.0.10",
4+
"version": "1.0.11",
55
"main": "./dist/index.js",
66
"module": "./dist/index.mjs",
77
"types": "./dist/index.d.ts",

‎packages/w3s-web-core-sdk/src/__tests__/utils/rpc/fetchFromApi.test.ts‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,16 @@ import { EIP1193ProviderRpcError } from 'viem'
2121
import { ResponseError } from 'web3'
2222

2323
import { fetchFromApi } from '../../../utils'
24+
import { isChromeExtension } from '../../../utils/rpc/isChromeExtension'
2425

2526
jest.mock('uuid', () => ({
2627
v4: jest.fn().mockReturnValue('mocked-uuid'),
2728
}))
2829

30+
jest.mock('../../../utils/rpc/isChromeExtension', () => ({
31+
isChromeExtension: jest.fn(),
32+
}))
33+
2934
beforeAll(() => {
3035
fetchMock.enableMocks()
3136
})
@@ -133,4 +138,85 @@ describe('Utils > rpc > fetchFromApi', () => {
133138

134139
jest.restoreAllMocks()
135140
})
141+
142+
describe('Chrome extension support', () => {
143+
const mockIsChromeExtension = isChromeExtension as jest.MockedFunction<
144+
typeof isChromeExtension
145+
>
146+
const originalWindow = global.window
147+
148+
beforeEach(() => {
149+
Object.defineProperty(global, 'window', {
150+
value: {
151+
location: {
152+
hostname: 'test-extension-id',
153+
},
154+
},
155+
writable: true,
156+
})
157+
})
158+
159+
afterEach(() => {
160+
global.window = originalWindow
161+
mockIsChromeExtension.mockReset()
162+
})
163+
164+
it('should use chrome-extension URI format when in Chrome extension context', async () => {
165+
mockIsChromeExtension.mockReturnValue(true)
166+
167+
const mockResponse = { jsonrpc: '2.0', id: 1, result: '0x1' }
168+
fetchMock.mockResponseOnce(JSON.stringify(mockResponse), { status: 200 })
169+
170+
await fetchFromApi(mockClientUrl, mockClientKey, mockPayload)
171+
172+
const callArgs = fetchMock.mock.calls[0]
173+
const requestOptions = callArgs?.[1]
174+
const headers = requestOptions?.headers as Record<string, string>
175+
176+
expect(headers['X-AppInfo']).toContain(
177+
'chrome-extension://test-extension-id',
178+
)
179+
})
180+
181+
it('should use regular hostname when not in Chrome extension context', async () => {
182+
mockIsChromeExtension.mockReturnValue(false)
183+
184+
const mockResponse = { jsonrpc: '2.0', id: 1, result: '0x1' }
185+
fetchMock.mockResponseOnce(JSON.stringify(mockResponse), { status: 200 })
186+
187+
await fetchFromApi(mockClientUrl, mockClientKey, mockPayload)
188+
189+
const callArgs = fetchMock.mock.calls[0]
190+
const requestOptions = callArgs?.[1]
191+
const headers = requestOptions?.headers as Record<string, string>
192+
const appInfo = headers['X-AppInfo']
193+
194+
expect(appInfo).toContain('uri=test-extension-id')
195+
expect(appInfo).not.toContain('chrome-extension://')
196+
})
197+
198+
it('should handle unknown hostname gracefully', async () => {
199+
mockIsChromeExtension.mockReturnValue(false)
200+
201+
Object.defineProperty(global, 'window', {
202+
value: {
203+
location: {
204+
hostname: undefined,
205+
},
206+
},
207+
writable: true,
208+
})
209+
210+
const mockResponse = { jsonrpc: '2.0', id: 1, result: '0x1' }
211+
fetchMock.mockResponseOnce(JSON.stringify(mockResponse), { status: 200 })
212+
213+
await fetchFromApi(mockClientUrl, mockClientKey, mockPayload)
214+
215+
const callArgs = fetchMock.mock.calls[0]
216+
const requestOptions = callArgs?.[1]
217+
const headers = requestOptions?.headers as Record<string, string>
218+
219+
expect(headers['X-AppInfo']).toContain('uri=unknown')
220+
})
221+
})
136222
})
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* Copyright 2025 Circle Internet Group, Inc. All rights reserved.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at.
9+
*
10+
* Http://www.apache.org/licenses/LICENSE-2.0.
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import { isChromeExtension } from '../../../utils'
20+
21+
describe('Utils > rpc > isChromeExtension', () => {
22+
const originalWindow = global.window
23+
const originalLocation = global.location
24+
25+
afterEach(() => {
26+
global.window = originalWindow
27+
global.location = originalLocation
28+
})
29+
30+
it('should return true when in Chrome extension context', () => {
31+
Object.defineProperty(global, 'window', {
32+
value: {
33+
location: {
34+
protocol: 'chrome-extension:',
35+
},
36+
},
37+
writable: true,
38+
})
39+
40+
expect(isChromeExtension()).toBe(true)
41+
})
42+
43+
it('should return false when in regular web context', () => {
44+
Object.defineProperty(global, 'window', {
45+
value: {
46+
location: {
47+
protocol: 'https:',
48+
},
49+
},
50+
writable: true,
51+
})
52+
53+
expect(isChromeExtension()).toBe(false)
54+
})
55+
56+
it('should return false when in http context', () => {
57+
Object.defineProperty(global, 'window', {
58+
value: {
59+
location: {
60+
protocol: 'http:',
61+
},
62+
},
63+
writable: true,
64+
})
65+
66+
expect(isChromeExtension()).toBe(false)
67+
})
68+
69+
it('should return false when window is undefined', () => {
70+
Object.defineProperty(global, 'window', {
71+
value: undefined,
72+
writable: true,
73+
})
74+
75+
expect(isChromeExtension()).toBe(false)
76+
})
77+
78+
it('should return false when window.location is undefined', () => {
79+
Object.defineProperty(global, 'window', {
80+
value: {},
81+
writable: true,
82+
})
83+
84+
expect(isChromeExtension()).toBe(false)
85+
})
86+
87+
it('should return false when protocol is undefined', () => {
88+
Object.defineProperty(global, 'window', {
89+
value: {
90+
location: {},
91+
},
92+
writable: true,
93+
})
94+
95+
expect(isChromeExtension()).toBe(false)
96+
})
97+
})
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* Copyright 2025 Circle Internet Group, Inc. All rights reserved.
3+
*
4+
* SPDX-License-Identifier: Apache-2.0.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at.
9+
*
10+
* Http://www.apache.org/licenses/LICENSE-2.0.
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
import {
20+
adaptCredentialCreationOptions,
21+
adaptCredentialRequestOptions,
22+
} from '../../../utils'
23+
import { isChromeExtension } from '../../../utils/rpc/isChromeExtension'
24+
25+
jest.mock('../../../utils/rpc/isChromeExtension', () => ({
26+
isChromeExtension: jest.fn(),
27+
}))
28+
29+
describe('Utils > rpc > webAuthnHelpers', () => {
30+
const mockIsChromeExtension = isChromeExtension as jest.MockedFunction<
31+
typeof isChromeExtension
32+
>
33+
34+
afterEach(() => {
35+
mockIsChromeExtension.mockReset()
36+
})
37+
38+
describe('adaptCredentialCreationOptions', () => {
39+
const mockCreationOptions: PublicKeyCredentialCreationOptions = {
40+
challenge: new Uint8Array([1, 2, 3]),
41+
rp: {
42+
id: 'example.com',
43+
name: 'Example Corp',
44+
},
45+
user: {
46+
id: new Uint8Array([4, 5, 6]),
47+
name: 'user@example.com',
48+
displayName: 'John Doe',
49+
},
50+
pubKeyCredParams: [{ alg: -7, type: 'public-key' }],
51+
timeout: 60000,
52+
attestation: 'direct',
53+
}
54+
55+
it('should return original options when not in Chrome extension', () => {
56+
mockIsChromeExtension.mockReturnValue(false)
57+
58+
const result = adaptCredentialCreationOptions(mockCreationOptions)
59+
60+
expect(result).toEqual(mockCreationOptions)
61+
expect(result.rp.id).toBe('example.com')
62+
})
63+
64+
it('should remove rp.id when in Chrome extension context', () => {
65+
mockIsChromeExtension.mockReturnValue(true)
66+
67+
const result = adaptCredentialCreationOptions(mockCreationOptions)
68+
69+
expect(result.rp).toEqual({
70+
name: 'Example Corp',
71+
})
72+
expect(result.rp).not.toHaveProperty('id')
73+
expect(result.challenge).toEqual(mockCreationOptions.challenge)
74+
expect(result.user).toEqual(mockCreationOptions.user)
75+
})
76+
77+
it('should preserve all other properties when removing rp.id', () => {
78+
mockIsChromeExtension.mockReturnValue(true)
79+
80+
const result = adaptCredentialCreationOptions(mockCreationOptions)
81+
82+
expect(result.challenge).toEqual(mockCreationOptions.challenge)
83+
expect(result.user).toEqual(mockCreationOptions.user)
84+
expect(result.pubKeyCredParams).toEqual(
85+
mockCreationOptions.pubKeyCredParams,
86+
)
87+
expect(result.timeout).toEqual(mockCreationOptions.timeout)
88+
expect(result.attestation).toEqual(mockCreationOptions.attestation)
89+
})
90+
})
91+
92+
describe('adaptCredentialRequestOptions', () => {
93+
const mockRequestOptions: PublicKeyCredentialRequestOptions = {
94+
challenge: new Uint8Array([7, 8, 9]),
95+
rpId: 'example.com',
96+
allowCredentials: [
97+
{
98+
id: new Uint8Array([10, 11, 12]),
99+
type: 'public-key',
100+
transports: ['usb', 'nfc'],
101+
},
102+
],
103+
timeout: 60000,
104+
userVerification: 'preferred',
105+
}
106+
107+
it('should return original options when not in Chrome extension', () => {
108+
mockIsChromeExtension.mockReturnValue(false)
109+
110+
const result = adaptCredentialRequestOptions(mockRequestOptions)
111+
112+
expect(result).toEqual(mockRequestOptions)
113+
expect(result.rpId).toBe('example.com')
114+
})
115+
116+
it('should remove rpId when in Chrome extension context', () => {
117+
mockIsChromeExtension.mockReturnValue(true)
118+
119+
const result = adaptCredentialRequestOptions(mockRequestOptions)
120+
121+
expect(result).not.toHaveProperty('rpId')
122+
expect(result.challenge).toEqual(mockRequestOptions.challenge)
123+
expect(result.allowCredentials).toEqual(
124+
mockRequestOptions.allowCredentials,
125+
)
126+
})
127+
128+
it('should preserve all other properties when removing rpId', () => {
129+
mockIsChromeExtension.mockReturnValue(true)
130+
131+
const result = adaptCredentialRequestOptions(mockRequestOptions)
132+
133+
expect(result.challenge).toEqual(mockRequestOptions.challenge)
134+
expect(result.allowCredentials).toEqual(
135+
mockRequestOptions.allowCredentials,
136+
)
137+
expect(result.timeout).toEqual(mockRequestOptions.timeout)
138+
expect(result.userVerification).toEqual(
139+
mockRequestOptions.userVerification,
140+
)
141+
})
142+
143+
it('should handle options without rpId gracefully', () => {
144+
mockIsChromeExtension.mockReturnValue(true)
145+
146+
const optionsWithoutRpId = {
147+
challenge: new Uint8Array([7, 8, 9]),
148+
timeout: 60000,
149+
} as PublicKeyCredentialRequestOptions
150+
151+
const result = adaptCredentialRequestOptions(optionsWithoutRpId)
152+
153+
expect(result).toEqual(optionsWithoutRpId)
154+
expect(result).not.toHaveProperty('rpId')
155+
})
156+
})
157+
})

0 commit comments

Comments
 (0)