@lukso/core 1.2.10-dev.84ae4e5 → 1.2.11-dev.c409ee8

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,16 +1,32 @@
1
1
  import {
2
+ EIP1271_MAGIC_VALUE,
2
3
  EXTENSION_STORE_LINKS,
3
4
  UrlConverter,
4
5
  UrlResolver,
5
6
  browserInfo,
6
- slug
7
- } from "../chunk-GFLV5EJV.js";
7
+ createMessage,
8
+ createSignedQR,
9
+ getControllerAddress,
10
+ getEIP1271Data,
11
+ isSignedQRFormat,
12
+ parseSignedQR,
13
+ slug,
14
+ verifySignedQR
15
+ } from "../chunk-RECO5F6T.js";
8
16
  import "../chunk-ET7EYHRY.js";
9
17
  export {
18
+ EIP1271_MAGIC_VALUE,
10
19
  EXTENSION_STORE_LINKS,
11
20
  UrlConverter,
12
21
  UrlResolver,
13
22
  browserInfo,
14
- slug
23
+ createMessage,
24
+ createSignedQR,
25
+ getControllerAddress,
26
+ getEIP1271Data,
27
+ isSignedQRFormat,
28
+ parseSignedQR,
29
+ slug,
30
+ verifySignedQR
15
31
  };
16
32
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukso/core",
3
- "version": "1.2.10-dev.84ae4e5",
3
+ "version": "1.2.11-dev.c409ee8",
4
4
  "description": "Core utilities, services, and mixins for LUKSO web components and applications",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -0,0 +1,276 @@
1
+ import type { Hex } from 'viem'
2
+ import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
+ import {
5
+ createMessage,
6
+ createSignedQR,
7
+ EIP1271_MAGIC_VALUE,
8
+ getControllerAddress,
9
+ getEIP1271Data,
10
+ isSignedQRFormat,
11
+ parseSignedQR,
12
+ verifySignedQR,
13
+ } from '../signed-profile-urls.js'
14
+
15
+ describe('signed-qr-code', () => {
16
+ let privateKey: Hex
17
+ const profileAddress = '0x1234567890123456789012345678901234567890'
18
+ const chainId = 42
19
+
20
+ beforeEach(() => {
21
+ privateKey = generatePrivateKey()
22
+ // Mock Date.now for consistent timestamps in tests
23
+ vi.useFakeTimers()
24
+ vi.setSystemTime(new Date('2024-01-27T12:00:00Z'))
25
+ })
26
+
27
+ afterEach(() => {
28
+ vi.useRealTimers()
29
+ })
30
+
31
+ describe('createMessage', () => {
32
+ it('creates a deterministic message', () => {
33
+ const message = createMessage(profileAddress, chainId, 1706356800)
34
+ expect(message).toBe(
35
+ 'ethereum:0x1234567890123456789012345678901234567890@42?ts=1706356800'
36
+ )
37
+ })
38
+
39
+ it('lowercases the address', () => {
40
+ const message = createMessage(
41
+ '0xABCDEF1234567890123456789012345678901234',
42
+ chainId,
43
+ 1706356800
44
+ )
45
+ expect(message).toContain('0xabcdef1234567890123456789012345678901234')
46
+ })
47
+ })
48
+
49
+ describe('createSignedQR', () => {
50
+ it('creates a valid signed URI', async () => {
51
+ const uri = await createSignedQR(privateKey, profileAddress, chainId)
52
+
53
+ expect(uri).toMatch(
54
+ /^ethereum:0x[a-f0-9]{40}@\d+\?ts=\d+&sig=0x[a-f0-9]+$/
55
+ )
56
+ })
57
+
58
+ it('uses current timestamp by default (no offset)', async () => {
59
+ const uri = await createSignedQR(privateKey, profileAddress, chainId)
60
+ const parsed = parseSignedQR(uri)
61
+
62
+ const now = Math.floor(Date.now() / 1000)
63
+ expect(parsed?.timestamp).toBe(now) // default offset is 0
64
+ })
65
+
66
+ it('respects generationOffsetSeconds option', async () => {
67
+ const uri = await createSignedQR(privateKey, profileAddress, chainId, {
68
+ generationOffsetSeconds: 3,
69
+ })
70
+ const parsed = parseSignedQR(uri)
71
+
72
+ const now = Math.floor(Date.now() / 1000)
73
+ expect(parsed?.timestamp).toBe(now + 3)
74
+ })
75
+
76
+ it('caps generationOffsetSeconds at 5 seconds', async () => {
77
+ const uri = await createSignedQR(privateKey, profileAddress, chainId, {
78
+ generationOffsetSeconds: 100, // way over the limit
79
+ })
80
+ const parsed = parseSignedQR(uri)
81
+
82
+ const now = Math.floor(Date.now() / 1000)
83
+ expect(parsed?.timestamp).toBe(now + 5) // capped at 5
84
+ })
85
+
86
+ it('does not allow negative generationOffsetSeconds', async () => {
87
+ const uri = await createSignedQR(privateKey, profileAddress, chainId, {
88
+ generationOffsetSeconds: -10,
89
+ })
90
+ const parsed = parseSignedQR(uri)
91
+
92
+ const now = Math.floor(Date.now() / 1000)
93
+ expect(parsed?.timestamp).toBe(now) // clamped to 0
94
+ })
95
+
96
+ it('uses custom signer when provided', async () => {
97
+ const controllerAddress = getControllerAddress(privateKey)
98
+
99
+ // Create a custom signer that uses signMessage (EIP-191)
100
+ const customSigner = async (message: string) => {
101
+ const account = privateKeyToAccount(privateKey)
102
+ return account.signMessage({ message })
103
+ }
104
+
105
+ const uri = await createSignedQR(null, profileAddress, chainId, {
106
+ signer: customSigner,
107
+ })
108
+
109
+ const result = await verifySignedQR(uri)
110
+ expect(result.isValid).toBe(true)
111
+ expect(result.recoveredAddress).toBe(controllerAddress.toLowerCase())
112
+ })
113
+
114
+ it('throws when neither privateKey nor signer is provided', async () => {
115
+ await expect(
116
+ createSignedQR(null, profileAddress, chainId)
117
+ ).rejects.toThrow(
118
+ 'Either a valid privateKey or options.signer must be provided'
119
+ )
120
+ })
121
+
122
+ it('throws when privateKey is 0x and no signer provided', async () => {
123
+ await expect(
124
+ createSignedQR('0x', profileAddress, chainId)
125
+ ).rejects.toThrow(
126
+ 'Either a valid privateKey or options.signer must be provided'
127
+ )
128
+ })
129
+ })
130
+
131
+ describe('parseSignedQR', () => {
132
+ it('parses a valid URI', async () => {
133
+ const uri = await createSignedQR(privateKey, profileAddress, chainId)
134
+ const parsed = parseSignedQR(uri)
135
+
136
+ expect(parsed).not.toBeNull()
137
+ expect(parsed?.profileAddress).toBe(profileAddress.toLowerCase())
138
+ expect(parsed?.chainId).toBe(chainId)
139
+ expect(parsed?.signature).toMatch(/^0x[a-f0-9]+$/)
140
+ })
141
+
142
+ it('returns null for invalid format', () => {
143
+ expect(parseSignedQR('invalid')).toBeNull()
144
+ expect(parseSignedQR('ethereum:invalid')).toBeNull()
145
+ expect(parseSignedQR('http://example.com')).toBeNull()
146
+ })
147
+
148
+ it('returns null for missing signature', () => {
149
+ expect(
150
+ parseSignedQR(
151
+ 'ethereum:0x1234567890123456789012345678901234567890@42?ts=123'
152
+ )
153
+ ).toBeNull()
154
+ })
155
+ })
156
+
157
+ describe('verifySignedQR', () => {
158
+ it('verifies a freshly created QR', async () => {
159
+ const uri = await createSignedQR(privateKey, profileAddress, chainId, {
160
+ generationOffsetSeconds: 0,
161
+ })
162
+ const result = await verifySignedQR(uri)
163
+
164
+ expect(result.isValid).toBe(true)
165
+ expect(result.isExpired).toBe(false)
166
+ expect(result.profileAddress).toBe(profileAddress.toLowerCase())
167
+ expect(result.chainId).toBe(chainId)
168
+ })
169
+
170
+ it('recovers the correct controller address', async () => {
171
+ const controllerAddress = getControllerAddress(privateKey)
172
+ const uri = await createSignedQR(privateKey, profileAddress, chainId)
173
+ const result = await verifySignedQR(uri)
174
+
175
+ expect(result.recoveredAddress).toBe(controllerAddress.toLowerCase())
176
+ })
177
+
178
+ it('marks expired QR as invalid', async () => {
179
+ // Create a QR at current time
180
+ const uri = await createSignedQR(privateKey, profileAddress, chainId)
181
+
182
+ // Advance time by 2 minutes (past the 60 second maxAge)
183
+ vi.advanceTimersByTime(120 * 1000)
184
+
185
+ const result = await verifySignedQR(uri, { maxAgeSeconds: 60 })
186
+
187
+ expect(result.isValid).toBe(false)
188
+ expect(result.isExpired).toBe(true)
189
+ })
190
+
191
+ it('allows skipping timestamp validation', async () => {
192
+ // Create a QR at current time
193
+ const uri = await createSignedQR(privateKey, profileAddress, chainId)
194
+
195
+ // Advance time by 2 minutes (would normally be expired)
196
+ vi.advanceTimersByTime(120 * 1000)
197
+
198
+ const result = await verifySignedQR(uri, {
199
+ skipTimestampValidation: true,
200
+ })
201
+
202
+ expect(result.isValid).toBe(true)
203
+ expect(result.isExpired).toBe(false)
204
+ })
205
+
206
+ it('throws for invalid URI format', async () => {
207
+ await expect(verifySignedQR('invalid')).rejects.toThrow(
208
+ 'Invalid QR format'
209
+ )
210
+ })
211
+ })
212
+
213
+ describe('isSignedQRFormat', () => {
214
+ it('returns true for valid format', async () => {
215
+ const uri = await createSignedQR(privateKey, profileAddress, chainId)
216
+ expect(isSignedQRFormat(uri)).toBe(true)
217
+ })
218
+
219
+ it('returns false for invalid formats', () => {
220
+ expect(isSignedQRFormat('invalid')).toBe(false)
221
+ expect(isSignedQRFormat('ethereum:0x123')).toBe(false)
222
+ expect(isSignedQRFormat('http://example.com')).toBe(false)
223
+ })
224
+ })
225
+
226
+ describe('getControllerAddress', () => {
227
+ it('returns the address for a private key', () => {
228
+ const address = getControllerAddress(privateKey)
229
+ expect(address).toMatch(/^0x[a-fA-F0-9]{40}$/)
230
+ })
231
+ })
232
+
233
+ describe('getEIP1271Data', () => {
234
+ it('returns hash and signature for EIP-1271 verification', async () => {
235
+ const uri = await createSignedQR(privateKey, profileAddress, chainId)
236
+ const result = await verifySignedQR(uri)
237
+ const { hash, signature } = getEIP1271Data(uri, result)
238
+
239
+ expect(hash).toMatch(/^0x[a-f0-9]{64}$/)
240
+ expect(signature).toMatch(/^0x[a-f0-9]+$/)
241
+ expect(hash).toBe(result.messageHash)
242
+ })
243
+ })
244
+
245
+ describe('EIP1271_MAGIC_VALUE', () => {
246
+ it('is the correct magic value', () => {
247
+ expect(EIP1271_MAGIC_VALUE).toBe('0x1626ba7e')
248
+ })
249
+ })
250
+
251
+ describe('round-trip verification', () => {
252
+ it('creates and verifies QR for different chain IDs', async () => {
253
+ for (const testChainId of [42, 4201, 1]) {
254
+ const uri = await createSignedQR(
255
+ privateKey,
256
+ profileAddress,
257
+ testChainId,
258
+ { generationOffsetSeconds: 0 }
259
+ )
260
+ const result = await verifySignedQR(uri)
261
+
262
+ expect(result.isValid).toBe(true)
263
+ expect(result.chainId).toBe(testChainId)
264
+ }
265
+ })
266
+
267
+ it('signature is deterministic for same inputs', async () => {
268
+ // With the same timestamp, the signature should be the same
269
+ const timestamp = Math.floor(Date.now() / 1000)
270
+ const message1 = createMessage(profileAddress, chainId, timestamp)
271
+ const message2 = createMessage(profileAddress, chainId, timestamp)
272
+
273
+ expect(message1).toBe(message2)
274
+ })
275
+ })
276
+ })
@@ -1,5 +1,14 @@
1
1
  export type { BrowserInfo, BrowserName } from './browserInfo.js'
2
2
  export { browserInfo, EXTENSION_STORE_LINKS } from './browserInfo.js'
3
-
3
+ export {
4
+ createMessage,
5
+ createSignedQR,
6
+ EIP1271_MAGIC_VALUE,
7
+ getControllerAddress,
8
+ getEIP1271Data,
9
+ isSignedQRFormat,
10
+ parseSignedQR,
11
+ verifySignedQR,
12
+ } from './signed-profile-urls.js'
4
13
  export { slug } from './slug.js'
5
14
  export { UrlConverter, UrlResolver } from './url-resolver.js'