@lukso/core 1.2.10 → 1.2.11-dev.6cff3c3

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.
@@ -0,0 +1,385 @@
1
+ /**
2
+ * @service-checkin/signed-qr-code
3
+ *
4
+ * Create and verify signed QR codes for Universal Profile check-ins.
5
+ * Pure TypeScript - compatible with React Native, Node.js, and browsers.
6
+ *
7
+ * URI Format: ethereum:<address>@<chainId>?ts=<unixTimestamp>&sig=<signature>
8
+ *
9
+ * The signed message is everything before &sig= (the URI without the signature).
10
+ * Uses EIP-191 prefixed signing (signMessage) for safety - this prevents
11
+ * signed messages from being confused with transaction hashes.
12
+ */
13
+
14
+ import { type Hex, hashMessage, recoverMessageAddress } from 'viem'
15
+ import { privateKeyToAccount } from 'viem/accounts'
16
+
17
+ // ============================================================================
18
+ // Types
19
+ // ============================================================================
20
+
21
+ export interface SignedQRData {
22
+ /** The Universal Profile address */
23
+ profileAddress: string
24
+ /** The chain ID (42 for LUKSO mainnet, 4201 for testnet) */
25
+ chainId: number
26
+ /** Unix timestamp (seconds since epoch) */
27
+ timestamp: number
28
+ /** The signature over the message */
29
+ signature: Hex
30
+ }
31
+
32
+ /**
33
+ * Custom signer function type.
34
+ * Takes the message string and returns an EIP-191 signature.
35
+ * The signer should use `signMessage` (which adds the EIP-191 prefix).
36
+ * Compatible with hardware wallets, secure enclaves, WalletConnect, etc.
37
+ *
38
+ * The EIP-191 prefix prevents signed messages from being confused with
39
+ * transaction hashes, making this safe for identity verification.
40
+ */
41
+ export type SignerFunction = (message: string) => Promise<Hex>
42
+
43
+ export interface CreateSignedQROptions {
44
+ /**
45
+ * Seconds to add to current time to account for QR generation/display latency.
46
+ * The QR timestamp will be set to (now + generationOffsetSeconds).
47
+ * Capped at 5 seconds maximum to prevent abuse.
48
+ * Default: 0
49
+ */
50
+ generationOffsetSeconds?: number
51
+
52
+ /**
53
+ * Custom signer function for signing the message.
54
+ * If provided, the privateKey parameter can be omitted or set to null.
55
+ * Useful for hardware wallets, secure enclaves, or WalletConnect.
56
+ *
57
+ * The signer receives the message string and should use `signMessage`
58
+ * (EIP-191 prefixed signing) to return the signature.
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * // Using a wallet that signs messages
63
+ * const uri = await createSignedQR(null, profileAddress, 42, {
64
+ * signer: async (message) => {
65
+ * return await wallet.signMessage(message)
66
+ * }
67
+ * })
68
+ * ```
69
+ */
70
+ signer?: SignerFunction
71
+ }
72
+
73
+ export interface VerifySignedQROptions {
74
+ /** Maximum age in seconds before QR is considered expired (default: 60) */
75
+ maxAgeSeconds?: number
76
+ /** Skip timestamp validation (useful for testing) */
77
+ skipTimestampValidation?: boolean
78
+ }
79
+
80
+ export interface VerificationResult {
81
+ /** Whether the QR code is valid and not expired */
82
+ isValid: boolean
83
+ /** The Universal Profile address from the QR */
84
+ profileAddress: string
85
+ /** The chain ID from the QR */
86
+ chainId: number
87
+ /** The timestamp from the QR (unix seconds) */
88
+ timestamp: number
89
+ /** The address recovered from the signature (the controller) */
90
+ recoveredAddress: string
91
+ /** Whether the QR has expired based on maxAgeSeconds */
92
+ isExpired: boolean
93
+ /** The original signed message (for EIP-1271 verification) */
94
+ message: string
95
+ /** The hash of the message (for EIP-1271 verification) */
96
+ messageHash: Hex
97
+ }
98
+
99
+ export interface ParsedQRData {
100
+ profileAddress: string
101
+ chainId: number
102
+ timestamp: number
103
+ signature: Hex
104
+ message: string
105
+ }
106
+
107
+ // ============================================================================
108
+ // Constants
109
+ // ============================================================================
110
+
111
+ /** URI scheme for signed QR codes */
112
+ export const URI_SCHEME = 'ethereum'
113
+
114
+ /** Regex pattern for parsing signed QR URIs */
115
+ const URI_PATTERN =
116
+ /^ethereum:(0x[a-fA-F0-9]{40})@(\d+)\?ts=(\d+)&sig=(0x[a-fA-F0-9]+)$/
117
+
118
+ // ============================================================================
119
+ // Core Functions
120
+ // ============================================================================
121
+
122
+ /**
123
+ * Create the message that will be signed.
124
+ * This is the URI without the signature parameter.
125
+ */
126
+ export function createMessage(
127
+ profileAddress: string,
128
+ chainId: number,
129
+ timestamp: number
130
+ ): string {
131
+ return `${URI_SCHEME}:${profileAddress.toLowerCase()}@${chainId}?ts=${timestamp}`
132
+ }
133
+
134
+ /** Maximum allowed generation offset to prevent abuse */
135
+ const MAX_GENERATION_OFFSET_SECONDS = 5
136
+
137
+ /**
138
+ * Create a signed QR URI string.
139
+ *
140
+ * @param privateKey - The controller's private key (hex string with 0x prefix).
141
+ * Can be '0x' or omitted if using options.signer.
142
+ * @param profileAddress - The Universal Profile address to sign for
143
+ * @param chainId - The chain ID (42 for LUKSO mainnet, 4201 for testnet)
144
+ * @param options - Optional configuration including custom signer
145
+ * @returns The complete signed URI string ready to encode as QR
146
+ *
147
+ * @example
148
+ * ```ts
149
+ * // Using a private key directly
150
+ * const uri = await createSignedQR(
151
+ * '0x1234...', // controller private key
152
+ * '0xabcd...', // profile address
153
+ * 42, // LUKSO mainnet
154
+ * { generationOffsetSeconds: 2 }
155
+ * )
156
+ *
157
+ * // Using a custom signer (WalletConnect, hardware wallet, etc.)
158
+ * const uri = await createSignedQR(
159
+ * null, // not needed when signer is provided
160
+ * '0xabcd...', // profile address
161
+ * 42, // LUKSO mainnet
162
+ * {
163
+ * signer: async (message) => await wallet.signMessage(message)
164
+ * }
165
+ * )
166
+ * // Returns: ethereum:0xabcd...@42?ts=1706345678&sig=0x...
167
+ * ```
168
+ */
169
+ export async function createSignedQR(
170
+ privateKey: Hex | null,
171
+ profileAddress: string,
172
+ chainId: number,
173
+ options?: CreateSignedQROptions
174
+ ): Promise<string> {
175
+ // Cap the offset at MAX_GENERATION_OFFSET_SECONDS to prevent abuse
176
+ const requestedOffset = options?.generationOffsetSeconds ?? 0
177
+ const generationOffset = Math.min(
178
+ Math.max(0, requestedOffset),
179
+ MAX_GENERATION_OFFSET_SECONDS
180
+ )
181
+ const timestamp = Math.floor(Date.now() / 1000) + generationOffset
182
+
183
+ const message = createMessage(profileAddress, chainId, timestamp)
184
+
185
+ let signature: Hex
186
+
187
+ if (options?.signer) {
188
+ // Use custom signer (hardware wallet, secure enclave, WalletConnect, etc.)
189
+ // Pass the message string - signer should use signMessage (EIP-191)
190
+ signature = await options.signer(message)
191
+ } else if (privateKey && privateKey !== '0x') {
192
+ // Use private key directly with EIP-191 prefix (signMessage)
193
+ const account = privateKeyToAccount(privateKey)
194
+ signature = await account.signMessage({ message })
195
+ } else {
196
+ throw new Error(
197
+ 'Either a valid privateKey or options.signer must be provided'
198
+ )
199
+ }
200
+
201
+ return `${message}&sig=${signature}`
202
+ }
203
+
204
+ /**
205
+ * Parse a signed QR URI without verification.
206
+ * Use this when you need to extract data before full verification.
207
+ *
208
+ * @param uri - The signed QR URI string
209
+ * @returns Parsed data or null if format is invalid
210
+ */
211
+ export function parseSignedQR(uri: string): ParsedQRData | null {
212
+ const match = uri.match(URI_PATTERN)
213
+
214
+ if (!match) {
215
+ return null
216
+ }
217
+
218
+ const [, profileAddress, chainIdStr, timestampStr, signature] = match
219
+
220
+ return {
221
+ profileAddress: profileAddress.toLowerCase(),
222
+ chainId: Number.parseInt(chainIdStr, 10),
223
+ timestamp: Number.parseInt(timestampStr, 10),
224
+ signature: signature as Hex,
225
+ message: createMessage(
226
+ profileAddress,
227
+ Number.parseInt(chainIdStr, 10),
228
+ Number.parseInt(timestampStr, 10)
229
+ ),
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Verify a signed QR URI and recover the signer.
235
+ *
236
+ * This performs:
237
+ * 1. URI format validation
238
+ * 2. Signature recovery (gets the controller address that signed)
239
+ * 3. Timestamp validation (checks if expired)
240
+ *
241
+ * Note: This does NOT verify that the recovered address is an authorized
242
+ * controller of the profile. That check should be done separately using
243
+ * EIP-1271 isValidSignature() or by checking controller permissions on-chain.
244
+ *
245
+ * @param uri - The signed QR URI string
246
+ * @param options - Optional configuration
247
+ * @returns Verification result with recovered address
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * const result = await verifySignedQR(uri)
252
+ *
253
+ * if (result.isValid) {
254
+ * console.log('Profile:', result.profileAddress)
255
+ * console.log('Signed by:', result.recoveredAddress)
256
+ *
257
+ * // Now verify the signer is an authorized controller
258
+ * // Option 1: Check if recoveredAddress === profileAddress (EOA case)
259
+ * // Option 2: Call isValidSignature() on the profile contract
260
+ * }
261
+ * ```
262
+ */
263
+ export async function verifySignedQR(
264
+ uri: string,
265
+ options?: VerifySignedQROptions
266
+ ): Promise<VerificationResult> {
267
+ const maxAgeSeconds = options?.maxAgeSeconds ?? 60
268
+ const skipTimestampValidation = options?.skipTimestampValidation ?? false
269
+
270
+ // Parse the URI
271
+ const parsed = parseSignedQR(uri)
272
+
273
+ if (!parsed) {
274
+ throw new Error('Invalid QR format: does not match expected URI pattern')
275
+ }
276
+
277
+ const { profileAddress, chainId, timestamp, signature, message } = parsed
278
+
279
+ // Compute the EIP-191 message hash for storage
280
+ const messageHash = hashMessage(message)
281
+
282
+ // Recover the signer address using EIP-191 message recovery
283
+ const recoveredAddress = await recoverMessageAddress({
284
+ message,
285
+ signature,
286
+ })
287
+
288
+ // Check if expired
289
+ const now = Math.floor(Date.now() / 1000)
290
+ const isExpired = !skipTimestampValidation && timestamp + maxAgeSeconds < now
291
+
292
+ return {
293
+ isValid: !isExpired,
294
+ profileAddress,
295
+ chainId,
296
+ timestamp,
297
+ recoveredAddress: recoveredAddress.toLowerCase(),
298
+ isExpired,
299
+ message,
300
+ messageHash,
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Check if a string looks like a valid signed QR URI.
306
+ * This is a quick format check without full verification.
307
+ */
308
+ export function isSignedQRFormat(uri: string): boolean {
309
+ return URI_PATTERN.test(uri)
310
+ }
311
+
312
+ /**
313
+ * Get the controller address from a private key.
314
+ * Useful for displaying which address will sign the QR codes.
315
+ */
316
+ export function getControllerAddress(privateKey: Hex): string {
317
+ const account = privateKeyToAccount(privateKey)
318
+ return account.address
319
+ }
320
+
321
+ // ============================================================================
322
+ // EIP-1271 Helpers
323
+ // ============================================================================
324
+
325
+ /**
326
+ * EIP-1271 magic value returned for valid signatures
327
+ */
328
+ export const EIP1271_MAGIC_VALUE = '0x1626ba7e' as const
329
+
330
+ /**
331
+ * ABI for EIP-1271 isValidSignature function.
332
+ * Use with viem's readContract to verify signatures on Universal Profiles.
333
+ */
334
+ export const EIP1271_ABI = [
335
+ {
336
+ name: 'isValidSignature',
337
+ type: 'function',
338
+ stateMutability: 'view',
339
+ inputs: [
340
+ { name: 'dataHash', type: 'bytes32' },
341
+ { name: 'signature', type: 'bytes' },
342
+ ],
343
+ outputs: [{ name: '', type: 'bytes4' }],
344
+ },
345
+ ] as const
346
+
347
+ /**
348
+ * Prepare data for EIP-1271 verification.
349
+ * Returns the hash and signature needed to call isValidSignature().
350
+ *
351
+ * @param uri - The original signed QR URI (needed to extract the signature)
352
+ * @param verificationResult - The result from verifySignedQR()
353
+ *
354
+ * @example
355
+ * ```ts
356
+ * const result = await verifySignedQR(uri)
357
+ * const { hash, signature } = getEIP1271Data(uri, result)
358
+ *
359
+ * const magicValue = await client.readContract({
360
+ * address: result.profileAddress,
361
+ * abi: EIP1271_ABI,
362
+ * functionName: 'isValidSignature',
363
+ * args: [hash, signature],
364
+ * })
365
+ *
366
+ * const isAuthorizedController = magicValue === EIP1271_MAGIC_VALUE
367
+ * ```
368
+ */
369
+ export function getEIP1271Data(
370
+ uri: string,
371
+ verificationResult: VerificationResult
372
+ ): {
373
+ hash: Hex
374
+ signature: Hex
375
+ } {
376
+ const parsed = parseSignedQR(uri)
377
+ if (!parsed) {
378
+ throw new Error('Invalid QR format')
379
+ }
380
+
381
+ return {
382
+ hash: verificationResult.messageHash,
383
+ signature: parsed.signature,
384
+ }
385
+ }
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/utils/browserInfo.ts","../src/utils/slug.ts","../src/utils/url-resolver.ts"],"sourcesContent":["import type { DeviceService } from '../services'\n\nexport type BrowserName =\n | 'chrome'\n | 'safari'\n | 'firefox'\n | 'edge'\n | 'opera'\n | 'brave'\n\nexport type BrowserInfo = {\n id: BrowserName\n name: string\n icon: string\n storeLink: string\n}\n\n// extension store links (all webkit based browsers use chrome web store installation)\nexport const EXTENSION_STORE_LINKS = {\n chrome:\n 'https://chrome.google.com/webstore/detail/universal-profiles-testin/abpickdkkbnbcoepogfhkhennhfhehfn',\n brave:\n 'https://chrome.google.com/webstore/detail/universal-profiles-testin/abpickdkkbnbcoepogfhkhennhfhehfn',\n edge: 'https://chrome.google.com/webstore/detail/universal-profiles-testin/abpickdkkbnbcoepogfhkhennhfhehfn',\n opera: '',\n safari: '',\n firefox: '',\n}\n\n/**\n * Expose browser info to the app\n */\nexport const browserInfo = (deviceService: DeviceService): BrowserInfo => {\n const browserInfoDefaults = {\n id: 'chrome',\n name: '',\n icon: '',\n } as BrowserInfo\n\n const detectBrowser = (): BrowserInfo | undefined => {\n const { isChrome, isBrave, isFirefox, isSafari, isEdge, isOpera } =\n deviceService\n\n if (isBrave) {\n return {\n id: 'brave',\n name: 'Brave',\n icon: 'logo-brave',\n storeLink: EXTENSION_STORE_LINKS.brave,\n }\n }\n\n if (isEdge) {\n return {\n id: 'edge',\n name: 'Edge',\n icon: 'logo-edge',\n storeLink: EXTENSION_STORE_LINKS.edge,\n }\n }\n\n if (isOpera) {\n return {\n id: 'opera',\n name: 'Opera',\n icon: 'logo-opera',\n storeLink: EXTENSION_STORE_LINKS.opera,\n }\n }\n\n if (isChrome) {\n return {\n id: 'chrome',\n name: 'Chrome',\n icon: 'logo-chrome',\n storeLink: EXTENSION_STORE_LINKS.chrome,\n }\n }\n\n if (isFirefox) {\n return {\n id: 'firefox',\n name: 'Firefox',\n icon: 'logo-firefox',\n storeLink: EXTENSION_STORE_LINKS.firefox,\n }\n }\n\n if (isSafari) {\n return {\n id: 'safari',\n name: 'Safari',\n icon: 'logo-safari',\n storeLink: EXTENSION_STORE_LINKS.safari,\n }\n }\n }\n\n const browserInfo = { ...browserInfoDefaults, ...detectBrowser() }\n\n return browserInfo\n}\n","/**\n * Make slug from text\n *\n * @param value\n * @returns\n */\nexport const slug = (value?: string) => {\n if (!value) {\n return ''\n }\n\n return value.toLowerCase().replace(/\\s+/g, '-') // convert spaces to hyphens\n}\n","export class UrlConverter {\n private destination: URL\n /**\n * It will relatively append pathname or hostname to the destination URL\n *\n * For example:\n * destination=https://some.api.gateway/something/ipfs\n * url=ipfs://QmSomeHash\n * output=https://some.api.gateway/something/ipfs/QmSomeHash\n *\n * destination=https://some.api.gateway/something/ipfs\n * url=https://something.com/somewhere\n * output=https://some.api.gateway/something/ipfs/somewhere\n *\n * @param destination destination string | URL\n */\n constructor(destination: string | URL) {\n this.destination = new URL(destination)\n if (this.destination.pathname.at(-1) !== '/') {\n this.destination.pathname += '/'\n }\n }\n\n resolveUrl(url: string): string {\n // Parse and convert to javascript URL objects\n // this will manage / and relative paths for us.\n const source = new URL(url)\n // extract the relative path. For URLs with a pathname prepend \".\" to make it ./ (i.e. relative)\n // for anything that only has a hostname we prepend ./ to make it relative\n // the pathname is at least slash for https urls, but '' for ipfs for example\n const relativePath = source.pathname\n ? `./${source.hostname}${source.pathname}` // pathname always starts with at least a slash\n : `./${source.hostname}`\n // Construct relative URL on destination using the relative pathname.\n const out = new URL(relativePath, this.destination)\n out.pathname = out.pathname.replaceAll(/\\/\\/+/g, '/')\n return out.toString()\n }\n}\n\nexport class UrlResolver {\n private converters: Array<{\n match: string | RegExp\n converter: UrlConverter\n }> = []\n constructor(converters: Array<[string | RegExp, UrlConverter | string]>) {\n for (const item of converters) {\n const [match, _converter] = item\n if (match == null) {\n throw new TypeError('Match criteria not defined')\n }\n const converter =\n typeof _converter === 'string'\n ? new UrlConverter(_converter)\n : _converter\n if (!(converter instanceof UrlConverter)) {\n throw new TypeError('Invalid converter')\n }\n this.converters.push({ match, converter })\n }\n }\n\n /**\n * Resolves a URL to a gateway URL.\n * Supports possible multiple converters transforming the URL\n * in sequence until no converter matches.\n *\n * @param {string} url to resolve\n * @returns {string} resolved url (if resolver is found, otherwise the parameter url is returned)\n */\n resolveUrl(url_: string): string {\n let url = url_\n const current = new Set<{\n match: string | RegExp\n converter: UrlConverter\n }>(this.converters)\n let found = true\n while (found) {\n found = false\n for (const entry of current) {\n const { match, converter } = entry\n if (match instanceof RegExp ? match.test(url) : url.startsWith(match)) {\n url = converter.resolveUrl(url)\n // This converter matches, so don't use it again.\n current.delete(entry)\n found = true\n break\n }\n }\n }\n return url\n }\n}\n"],"mappings":"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","names":["browserInfo"]}
@@ -1,159 +0,0 @@
1
- "use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/utils/browserInfo.ts
2
- var EXTENSION_STORE_LINKS = {
3
- chrome: "https://chrome.google.com/webstore/detail/universal-profiles-testin/abpickdkkbnbcoepogfhkhennhfhehfn",
4
- brave: "https://chrome.google.com/webstore/detail/universal-profiles-testin/abpickdkkbnbcoepogfhkhennhfhehfn",
5
- edge: "https://chrome.google.com/webstore/detail/universal-profiles-testin/abpickdkkbnbcoepogfhkhennhfhehfn",
6
- opera: "",
7
- safari: "",
8
- firefox: ""
9
- };
10
- var browserInfo = (deviceService) => {
11
- const browserInfoDefaults = {
12
- id: "chrome",
13
- name: "",
14
- icon: ""
15
- };
16
- const detectBrowser = () => {
17
- const { isChrome, isBrave, isFirefox, isSafari, isEdge, isOpera } = deviceService;
18
- if (isBrave) {
19
- return {
20
- id: "brave",
21
- name: "Brave",
22
- icon: "logo-brave",
23
- storeLink: EXTENSION_STORE_LINKS.brave
24
- };
25
- }
26
- if (isEdge) {
27
- return {
28
- id: "edge",
29
- name: "Edge",
30
- icon: "logo-edge",
31
- storeLink: EXTENSION_STORE_LINKS.edge
32
- };
33
- }
34
- if (isOpera) {
35
- return {
36
- id: "opera",
37
- name: "Opera",
38
- icon: "logo-opera",
39
- storeLink: EXTENSION_STORE_LINKS.opera
40
- };
41
- }
42
- if (isChrome) {
43
- return {
44
- id: "chrome",
45
- name: "Chrome",
46
- icon: "logo-chrome",
47
- storeLink: EXTENSION_STORE_LINKS.chrome
48
- };
49
- }
50
- if (isFirefox) {
51
- return {
52
- id: "firefox",
53
- name: "Firefox",
54
- icon: "logo-firefox",
55
- storeLink: EXTENSION_STORE_LINKS.firefox
56
- };
57
- }
58
- if (isSafari) {
59
- return {
60
- id: "safari",
61
- name: "Safari",
62
- icon: "logo-safari",
63
- storeLink: EXTENSION_STORE_LINKS.safari
64
- };
65
- }
66
- };
67
- const browserInfo2 = { ...browserInfoDefaults, ...detectBrowser() };
68
- return browserInfo2;
69
- };
70
-
71
- // src/utils/slug.ts
72
- var slug = (value) => {
73
- if (!value) {
74
- return "";
75
- }
76
- return value.toLowerCase().replace(/\s+/g, "-");
77
- };
78
-
79
- // src/utils/url-resolver.ts
80
- var UrlConverter = class {
81
- /**
82
- * It will relatively append pathname or hostname to the destination URL
83
- *
84
- * For example:
85
- * destination=https://some.api.gateway/something/ipfs
86
- * url=ipfs://QmSomeHash
87
- * output=https://some.api.gateway/something/ipfs/QmSomeHash
88
- *
89
- * destination=https://some.api.gateway/something/ipfs
90
- * url=https://something.com/somewhere
91
- * output=https://some.api.gateway/something/ipfs/somewhere
92
- *
93
- * @param destination destination string | URL
94
- */
95
- constructor(destination) {
96
- this.destination = new URL(destination);
97
- if (this.destination.pathname.at(-1) !== "/") {
98
- this.destination.pathname += "/";
99
- }
100
- }
101
- resolveUrl(url) {
102
- const source = new URL(url);
103
- const relativePath = source.pathname ? `./${source.hostname}${source.pathname}` : `./${source.hostname}`;
104
- const out = new URL(relativePath, this.destination);
105
- out.pathname = out.pathname.replaceAll(/\/\/+/g, "/");
106
- return out.toString();
107
- }
108
- };
109
- var UrlResolver = class {
110
- constructor(converters) {
111
- this.converters = [];
112
- for (const item of converters) {
113
- const [match, _converter] = item;
114
- if (match == null) {
115
- throw new TypeError("Match criteria not defined");
116
- }
117
- const converter = typeof _converter === "string" ? new UrlConverter(_converter) : _converter;
118
- if (!(converter instanceof UrlConverter)) {
119
- throw new TypeError("Invalid converter");
120
- }
121
- this.converters.push({ match, converter });
122
- }
123
- }
124
- /**
125
- * Resolves a URL to a gateway URL.
126
- * Supports possible multiple converters transforming the URL
127
- * in sequence until no converter matches.
128
- *
129
- * @param {string} url to resolve
130
- * @returns {string} resolved url (if resolver is found, otherwise the parameter url is returned)
131
- */
132
- resolveUrl(url_) {
133
- let url = url_;
134
- const current = new Set(this.converters);
135
- let found = true;
136
- while (found) {
137
- found = false;
138
- for (const entry of current) {
139
- const { match, converter } = entry;
140
- if (match instanceof RegExp ? match.test(url) : url.startsWith(match)) {
141
- url = converter.resolveUrl(url);
142
- current.delete(entry);
143
- found = true;
144
- break;
145
- }
146
- }
147
- }
148
- return url;
149
- }
150
- };
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
- exports.EXTENSION_STORE_LINKS = EXTENSION_STORE_LINKS; exports.browserInfo = browserInfo; exports.slug = slug; exports.UrlConverter = UrlConverter; exports.UrlResolver = UrlResolver;
159
- //# sourceMappingURL=chunk-QU6NUTY6.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["/home/runner/work/service-auth-simple/service-auth-simple/packages/core/dist/chunk-QU6NUTY6.cjs","../src/utils/browserInfo.ts","../src/utils/slug.ts","../src/utils/url-resolver.ts"],"names":["browserInfo"],"mappings":"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","file":"/home/runner/work/service-auth-simple/service-auth-simple/packages/core/dist/chunk-QU6NUTY6.cjs","sourcesContent":[null,"import type { DeviceService } from '../services'\n\nexport type BrowserName =\n | 'chrome'\n | 'safari'\n | 'firefox'\n | 'edge'\n | 'opera'\n | 'brave'\n\nexport type BrowserInfo = {\n id: BrowserName\n name: string\n icon: string\n storeLink: string\n}\n\n// extension store links (all webkit based browsers use chrome web store installation)\nexport const EXTENSION_STORE_LINKS = {\n chrome:\n 'https://chrome.google.com/webstore/detail/universal-profiles-testin/abpickdkkbnbcoepogfhkhennhfhehfn',\n brave:\n 'https://chrome.google.com/webstore/detail/universal-profiles-testin/abpickdkkbnbcoepogfhkhennhfhehfn',\n edge: 'https://chrome.google.com/webstore/detail/universal-profiles-testin/abpickdkkbnbcoepogfhkhennhfhehfn',\n opera: '',\n safari: '',\n firefox: '',\n}\n\n/**\n * Expose browser info to the app\n */\nexport const browserInfo = (deviceService: DeviceService): BrowserInfo => {\n const browserInfoDefaults = {\n id: 'chrome',\n name: '',\n icon: '',\n } as BrowserInfo\n\n const detectBrowser = (): BrowserInfo | undefined => {\n const { isChrome, isBrave, isFirefox, isSafari, isEdge, isOpera } =\n deviceService\n\n if (isBrave) {\n return {\n id: 'brave',\n name: 'Brave',\n icon: 'logo-brave',\n storeLink: EXTENSION_STORE_LINKS.brave,\n }\n }\n\n if (isEdge) {\n return {\n id: 'edge',\n name: 'Edge',\n icon: 'logo-edge',\n storeLink: EXTENSION_STORE_LINKS.edge,\n }\n }\n\n if (isOpera) {\n return {\n id: 'opera',\n name: 'Opera',\n icon: 'logo-opera',\n storeLink: EXTENSION_STORE_LINKS.opera,\n }\n }\n\n if (isChrome) {\n return {\n id: 'chrome',\n name: 'Chrome',\n icon: 'logo-chrome',\n storeLink: EXTENSION_STORE_LINKS.chrome,\n }\n }\n\n if (isFirefox) {\n return {\n id: 'firefox',\n name: 'Firefox',\n icon: 'logo-firefox',\n storeLink: EXTENSION_STORE_LINKS.firefox,\n }\n }\n\n if (isSafari) {\n return {\n id: 'safari',\n name: 'Safari',\n icon: 'logo-safari',\n storeLink: EXTENSION_STORE_LINKS.safari,\n }\n }\n }\n\n const browserInfo = { ...browserInfoDefaults, ...detectBrowser() }\n\n return browserInfo\n}\n","/**\n * Make slug from text\n *\n * @param value\n * @returns\n */\nexport const slug = (value?: string) => {\n if (!value) {\n return ''\n }\n\n return value.toLowerCase().replace(/\\s+/g, '-') // convert spaces to hyphens\n}\n","export class UrlConverter {\n private destination: URL\n /**\n * It will relatively append pathname or hostname to the destination URL\n *\n * For example:\n * destination=https://some.api.gateway/something/ipfs\n * url=ipfs://QmSomeHash\n * output=https://some.api.gateway/something/ipfs/QmSomeHash\n *\n * destination=https://some.api.gateway/something/ipfs\n * url=https://something.com/somewhere\n * output=https://some.api.gateway/something/ipfs/somewhere\n *\n * @param destination destination string | URL\n */\n constructor(destination: string | URL) {\n this.destination = new URL(destination)\n if (this.destination.pathname.at(-1) !== '/') {\n this.destination.pathname += '/'\n }\n }\n\n resolveUrl(url: string): string {\n // Parse and convert to javascript URL objects\n // this will manage / and relative paths for us.\n const source = new URL(url)\n // extract the relative path. For URLs with a pathname prepend \".\" to make it ./ (i.e. relative)\n // for anything that only has a hostname we prepend ./ to make it relative\n // the pathname is at least slash for https urls, but '' for ipfs for example\n const relativePath = source.pathname\n ? `./${source.hostname}${source.pathname}` // pathname always starts with at least a slash\n : `./${source.hostname}`\n // Construct relative URL on destination using the relative pathname.\n const out = new URL(relativePath, this.destination)\n out.pathname = out.pathname.replaceAll(/\\/\\/+/g, '/')\n return out.toString()\n }\n}\n\nexport class UrlResolver {\n private converters: Array<{\n match: string | RegExp\n converter: UrlConverter\n }> = []\n constructor(converters: Array<[string | RegExp, UrlConverter | string]>) {\n for (const item of converters) {\n const [match, _converter] = item\n if (match == null) {\n throw new TypeError('Match criteria not defined')\n }\n const converter =\n typeof _converter === 'string'\n ? new UrlConverter(_converter)\n : _converter\n if (!(converter instanceof UrlConverter)) {\n throw new TypeError('Invalid converter')\n }\n this.converters.push({ match, converter })\n }\n }\n\n /**\n * Resolves a URL to a gateway URL.\n * Supports possible multiple converters transforming the URL\n * in sequence until no converter matches.\n *\n * @param {string} url to resolve\n * @returns {string} resolved url (if resolver is found, otherwise the parameter url is returned)\n */\n resolveUrl(url_: string): string {\n let url = url_\n const current = new Set<{\n match: string | RegExp\n converter: UrlConverter\n }>(this.converters)\n let found = true\n while (found) {\n found = false\n for (const entry of current) {\n const { match, converter } = entry\n if (match instanceof RegExp ? match.test(url) : url.startsWith(match)) {\n url = converter.resolveUrl(url)\n // This converter matches, so don't use it again.\n current.delete(entry)\n found = true\n break\n }\n }\n }\n return url\n }\n}\n"]}