@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.
- package/README.md +171 -3
- package/dist/chunk-2KVLFGLM.cjs +260 -0
- package/dist/chunk-2KVLFGLM.cjs.map +1 -0
- package/dist/{chunk-GFLV5EJV.js → chunk-RECO5F6T.js} +102 -1
- package/dist/chunk-RECO5F6T.js.map +1 -0
- package/dist/index.cjs +18 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/index.cjs +18 -2
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.cts +202 -1
- package/dist/utils/index.d.ts +202 -1
- package/dist/utils/index.js +19 -3
- package/package.json +1 -1
- package/src/utils/__tests__/signed-profile-urls.spec.ts +276 -0
- package/src/utils/index.ts +10 -1
- package/src/utils/signed-profile-urls.ts +385 -0
- package/dist/chunk-GFLV5EJV.js.map +0 -1
- package/dist/chunk-QU6NUTY6.cjs +0 -159
- package/dist/chunk-QU6NUTY6.cjs.map +0 -1
package/dist/utils/index.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
+
})
|
package/src/utils/index.ts
CHANGED
|
@@ -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'
|