@relayerfi/widget-kit-core 0.1.0

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/dist/index.js ADDED
@@ -0,0 +1,610 @@
1
+ // src/utils/client-key.ts
2
+ var globalClientKey = "";
3
+ var EXTENSION_STORAGE_KEY = "relayer_client_key";
4
+ function getStorageAPI() {
5
+ if (typeof chrome !== "undefined" && chrome.storage) {
6
+ return chrome.storage;
7
+ }
8
+ if (typeof browser !== "undefined" && browser.storage) {
9
+ return browser.storage;
10
+ }
11
+ return null;
12
+ }
13
+ async function setClientKey(key) {
14
+ if (!key || key.trim() === "") {
15
+ throw new Error("Client key is required and cannot be empty");
16
+ }
17
+ globalClientKey = key;
18
+ const storage = getStorageAPI();
19
+ if (storage) {
20
+ try {
21
+ await storage.local.set({ [EXTENSION_STORAGE_KEY]: key });
22
+ } catch (error) {
23
+ throw new Error("Failed to store client key in storage", { cause: error });
24
+ }
25
+ }
26
+ }
27
+ async function getClientKey() {
28
+ if (globalClientKey && globalClientKey.trim() !== "") {
29
+ return globalClientKey;
30
+ }
31
+ const storage = getStorageAPI();
32
+ if (storage) {
33
+ try {
34
+ const result = await storage.local.get(EXTENSION_STORAGE_KEY);
35
+ const storedKey = result[EXTENSION_STORAGE_KEY];
36
+ if (storedKey) {
37
+ globalClientKey = storedKey;
38
+ return globalClientKey;
39
+ }
40
+ } catch (error) {
41
+ throw new Error("Failed to get client key from storage", { cause: error });
42
+ }
43
+ }
44
+ throw new Error("Client key not found");
45
+ }
46
+ function clearClientKey() {
47
+ globalClientKey = "";
48
+ return globalClientKey;
49
+ }
50
+
51
+ // src/utils/format.ts
52
+ function removeHexPrefix(address) {
53
+ return address.startsWith("0x") ? address.slice(2) : address;
54
+ }
55
+ function addHexPrefix(address) {
56
+ return address.startsWith("0x") ? address : `0x${address}`;
57
+ }
58
+ function formatNumber(value, decimals = 2) {
59
+ const num = typeof value === "string" ? parseFloat(value) : value;
60
+ return num.toLocaleString(void 0, {
61
+ minimumFractionDigits: decimals,
62
+ maximumFractionDigits: decimals
63
+ });
64
+ }
65
+ function truncateString(str, length = 4) {
66
+ if (str.length <= length * 2 + 3) return str;
67
+ return `${str.substring(0, length)}...${str.substring(str.length - length)}`;
68
+ }
69
+ function truncateAddress(address) {
70
+ return truncateString(removeHexPrefix(address), 4);
71
+ }
72
+
73
+ // src/utils/constants.ts
74
+ import {
75
+ SDK_TO_PROXY_HEADERS,
76
+ PROXY_TO_DEV_HEADERS,
77
+ RELAYER_VALUES,
78
+ VALID_OPERATIONS
79
+ } from "@relayerfi/action-kit";
80
+ var PROXY_URL = "https://proxy.relayer.fi";
81
+ var API_REPOSITORY_URL = "https://api.relayer.fi/v1/directory/v1";
82
+
83
+ // src/utils/proxify.ts
84
+ import {
85
+ createDynamicExecutor,
86
+ WidgetExecutor
87
+ } from "@relayerfi/action-kit";
88
+ async function proxify(url, isExtension = false) {
89
+ const clientKey = await getClientKey();
90
+ if (!clientKey) {
91
+ throw new Error("Client key not found", { cause: new Error("Client key not found") });
92
+ }
93
+ const proxyEndpoint = `${PROXY_URL}/proxy`;
94
+ const queryParams = new URLSearchParams();
95
+ queryParams.set("url", url);
96
+ const proxifiedUrl = `${proxyEndpoint}?${queryParams.toString()}`;
97
+ const headers = {
98
+ Accept: "application/json",
99
+ [SDK_TO_PROXY_HEADERS.USER_AGENT]: RELAYER_VALUES.USER_AGENT,
100
+ [SDK_TO_PROXY_HEADERS.CONTENT_TYPE]: RELAYER_VALUES.CONTENT_TYPE_JSON,
101
+ [SDK_TO_PROXY_HEADERS.CLIENT_KEY]: clientKey
102
+ };
103
+ return { url: proxifiedUrl, headers };
104
+ }
105
+ async function getDynamicExecutor(key) {
106
+ if (!key) {
107
+ return await createDynamicExecutor();
108
+ } else {
109
+ return createDynamicExecutor(key);
110
+ }
111
+ }
112
+ async function execute(action, inputs, context, options) {
113
+ const clientKey = await getClientKey();
114
+ if (!clientKey) {
115
+ const executor = await getDynamicExecutor();
116
+ return await executor.execute(action, inputs, context, options);
117
+ } else {
118
+ const executor = await getDynamicExecutor(clientKey);
119
+ return await executor.execute(action, inputs, context, options);
120
+ }
121
+ }
122
+ async function getMetadata(url) {
123
+ const clientKey = await getClientKey();
124
+ const executor = clientKey ? new WidgetExecutor(clientKey) : new WidgetExecutor();
125
+ return await executor.getMetadata(url);
126
+ }
127
+
128
+ // src/errors/Errors.ts
129
+ var RelayerError = class extends Error {
130
+ code;
131
+ httpStatus;
132
+ originalError;
133
+ context;
134
+ constructor({
135
+ message,
136
+ code = "UNKNOWN_ERROR",
137
+ httpStatus,
138
+ originalError,
139
+ context
140
+ }) {
141
+ super(message);
142
+ this.name = this.constructor.name;
143
+ this.code = code;
144
+ this.httpStatus = httpStatus;
145
+ this.originalError = originalError;
146
+ this.context = context;
147
+ if (Error.captureStackTrace) {
148
+ Error.captureStackTrace(this, this.constructor);
149
+ }
150
+ }
151
+ /**
152
+ * Convert error to a plain object for logging or serialization
153
+ */
154
+ toJSON() {
155
+ return {
156
+ name: this.name,
157
+ message: this.message,
158
+ code: this.code,
159
+ httpStatus: this.httpStatus,
160
+ stack: this.stack,
161
+ context: this.context,
162
+ originalError: this.originalError ? {
163
+ message: this.originalError.message,
164
+ name: this.originalError.name,
165
+ stack: this.originalError.stack
166
+ } : void 0
167
+ };
168
+ }
169
+ };
170
+ var NetworkError = class _NetworkError extends RelayerError {
171
+ constructor({
172
+ message,
173
+ code = "NETWORK_ERROR",
174
+ httpStatus,
175
+ originalError,
176
+ context
177
+ }) {
178
+ super({ message, code, httpStatus, originalError, context });
179
+ }
180
+ static fromFetchError(url, error, httpStatus) {
181
+ return new _NetworkError({
182
+ message: `Network request failed for ${url}`,
183
+ code: "FETCH_ERROR",
184
+ httpStatus,
185
+ originalError: error,
186
+ context: { url }
187
+ });
188
+ }
189
+ };
190
+ var ValidationError = class _ValidationError extends RelayerError {
191
+ invalidFields;
192
+ constructor({
193
+ message,
194
+ code = "VALIDATION_ERROR",
195
+ invalidFields,
196
+ context
197
+ }) {
198
+ super({ message, code, context });
199
+ this.invalidFields = invalidFields;
200
+ }
201
+ static missingField(fieldName) {
202
+ return new _ValidationError({
203
+ message: `Missing required field: ${fieldName}`,
204
+ code: "MISSING_FIELD",
205
+ invalidFields: { [fieldName]: "Field is required" }
206
+ });
207
+ }
208
+ static invalidFormat(fieldName, reason) {
209
+ return new _ValidationError({
210
+ message: `Invalid format for ${fieldName}: ${reason}`,
211
+ code: "INVALID_FORMAT",
212
+ invalidFields: { [fieldName]: reason }
213
+ });
214
+ }
215
+ };
216
+ var SecurityError = class extends RelayerError {
217
+ constructor({
218
+ message,
219
+ code = "SECURITY_ERROR",
220
+ context
221
+ }) {
222
+ super({ message, code, context });
223
+ }
224
+ };
225
+ var BlockchainError = class _BlockchainError extends RelayerError {
226
+ constructor({
227
+ message,
228
+ code = "BLOCKCHAIN_ERROR",
229
+ originalError,
230
+ context
231
+ }) {
232
+ super({ message, code, originalError, context });
233
+ }
234
+ static transactionFailed(reason, context) {
235
+ return new _BlockchainError({
236
+ message: `Transaction failed: ${reason}`,
237
+ code: "TRANSACTION_FAILED",
238
+ context
239
+ });
240
+ }
241
+ static walletConnectionFailed(reason) {
242
+ return new _BlockchainError({
243
+ message: `Failed to connect to wallet: ${reason}`,
244
+ code: "WALLET_CONNECTION_ERROR"
245
+ });
246
+ }
247
+ };
248
+ var NotFoundError = class _NotFoundError extends RelayerError {
249
+ constructor({
250
+ message,
251
+ code = "NOT_FOUND",
252
+ httpStatus = 404,
253
+ context
254
+ }) {
255
+ super({ message, code, httpStatus, context });
256
+ }
257
+ static resourceNotFound(resourceType, identifier) {
258
+ return new _NotFoundError({
259
+ message: `${resourceType} not found: ${identifier}`,
260
+ context: { resourceType, identifier }
261
+ });
262
+ }
263
+ };
264
+ var InvalidOperationError = class _InvalidOperationError extends RelayerError {
265
+ constructor({
266
+ message,
267
+ code = "INVALID_OPERATION",
268
+ context
269
+ }) {
270
+ super({ message, code, context });
271
+ }
272
+ static actionNotPermitted(action, reason) {
273
+ return new _InvalidOperationError({
274
+ message: `Operation not permitted: ${action}. Reason: ${reason}`,
275
+ context: { action, reason }
276
+ });
277
+ }
278
+ };
279
+ var WidgetError = class _WidgetError extends RelayerError {
280
+ constructor({
281
+ message,
282
+ code = "WIDGET_ERROR",
283
+ httpStatus,
284
+ originalError,
285
+ context
286
+ }) {
287
+ super({ message, code, httpStatus, originalError, context });
288
+ }
289
+ static metadataFetchFailed(url, httpStatus) {
290
+ return new _WidgetError({
291
+ message: `Failed to fetch widget metadata from ${url}`,
292
+ code: "METADATA_FETCH_FAILED",
293
+ httpStatus,
294
+ context: { url }
295
+ });
296
+ }
297
+ static invalidUrl(url, reason) {
298
+ return new _WidgetError({
299
+ message: `Invalid widget URL: ${url}. Reason: ${reason}`,
300
+ code: "INVALID_URL",
301
+ context: { url, reason }
302
+ });
303
+ }
304
+ static invalidMetadata(reason, originalError) {
305
+ return new _WidgetError({
306
+ message: `Invalid widget metadata: ${reason}`,
307
+ code: "INVALID_METADATA",
308
+ originalError,
309
+ context: { reason }
310
+ });
311
+ }
312
+ static actionError(actionIndex, reason) {
313
+ return new _WidgetError({
314
+ message: `Failed to execute widget action at index ${actionIndex}: ${reason}`,
315
+ code: "ACTION_EXECUTION_ERROR",
316
+ context: { actionIndex, reason }
317
+ });
318
+ }
319
+ /**
320
+ * Error when fetching the widget directory fails.
321
+ * @param url The URL that was fetched.
322
+ * @param httpStatus The HTTP status code received.
323
+ * @param statusText Optional status text from the response.
324
+ */
325
+ static directoryFetchFailed(url, httpStatus, statusText) {
326
+ return new _WidgetError({
327
+ message: `Failed to fetch widget directory from ${url}. Status: ${httpStatus}${statusText ? ` (${statusText})` : ""}`,
328
+ code: "REGISTRY_FETCH_FAILED",
329
+ httpStatus,
330
+ context: { url }
331
+ });
332
+ }
333
+ /**
334
+ * Error when the fetched directory data has an invalid format.
335
+ * @param reason Description of why the format is invalid.
336
+ */
337
+ static invalidDirectoryFormat(reason) {
338
+ return new _WidgetError({
339
+ message: `Invalid widget directory format: ${reason}`,
340
+ code: "INVALID_REGISTRY_FORMAT",
341
+ context: { reason }
342
+ });
343
+ }
344
+ /**
345
+ * Generic error during the directory refresh process.
346
+ * @param reason Description of the error.
347
+ * @param originalError The original error caught, if any.
348
+ */
349
+ static directoryRefreshError(reason, originalError) {
350
+ return new _WidgetError({
351
+ message: `Error during widget directory refresh: ${reason}`,
352
+ code: "REGISTRY_REFRESH_ERROR",
353
+ originalError,
354
+ context: { reason }
355
+ });
356
+ }
357
+ };
358
+ var ErrorUtils = {
359
+ /**
360
+ * Safely wrap async operations with consistent error handling
361
+ */
362
+ async safeAsync(promise, errorMapper) {
363
+ try {
364
+ return await promise;
365
+ } catch (error) {
366
+ if (errorMapper) {
367
+ throw errorMapper(error);
368
+ }
369
+ if (error instanceof RelayerError) {
370
+ throw error;
371
+ }
372
+ throw new RelayerError({
373
+ message: error instanceof Error ? error.message : String(error),
374
+ originalError: error instanceof Error ? error : void 0
375
+ });
376
+ }
377
+ }
378
+ };
379
+
380
+ // src/directory/WidgetDirectory.ts
381
+ var WidgetDirectory = class _WidgetDirectory {
382
+ static instance = null;
383
+ widgetsByHostAndPath = /* @__PURE__ */ new Map();
384
+ widgetsByHost = /* @__PURE__ */ new Map();
385
+ lastUpdated = /* @__PURE__ */ new Date(0);
386
+ isRefreshing = false;
387
+ isExtension = false;
388
+ defaultDirectoryUrl;
389
+ initializationPromise = null;
390
+ isInitialized = false;
391
+ refreshIntervalId = null;
392
+ // Para almacenar el ID del intervalo
393
+ constructor(config = {}) {
394
+ this.isExtension = typeof chrome !== "undefined" && chrome.runtime && !!chrome.runtime.id;
395
+ this.defaultDirectoryUrl = API_REPOSITORY_URL;
396
+ }
397
+ static getInstance(config = {}) {
398
+ if (!this.instance) {
399
+ this.instance = new _WidgetDirectory(config);
400
+ }
401
+ return this.instance;
402
+ }
403
+ async init(config = {}) {
404
+ if (this.isInitialized && !config.forceRefresh) {
405
+ return;
406
+ }
407
+ if (this.initializationPromise && !config.forceRefresh) {
408
+ return this.initializationPromise;
409
+ }
410
+ if (config.forceRefresh) {
411
+ this.isInitialized = false;
412
+ }
413
+ this.initializationPromise = (async () => {
414
+ try {
415
+ const urlToRefresh = this.defaultDirectoryUrl;
416
+ await this.refresh(urlToRefresh);
417
+ if (this.isExtension && config?.refreshInterval) {
418
+ if (this.refreshIntervalId) {
419
+ clearInterval(this.refreshIntervalId);
420
+ }
421
+ this.refreshIntervalId = setInterval(() => {
422
+ this.refresh(urlToRefresh).catch((error) => {
423
+ });
424
+ }, config.refreshInterval);
425
+ }
426
+ this.isInitialized = true;
427
+ } catch (error) {
428
+ this.isInitialized = false;
429
+ throw error;
430
+ } finally {
431
+ this.initializationPromise = null;
432
+ }
433
+ })();
434
+ return this.initializationPromise;
435
+ }
436
+ createKey(host, pathname) {
437
+ return `${host}${pathname}`;
438
+ }
439
+ normalizeHost(hostInput) {
440
+ let host = hostInput;
441
+ if (host.startsWith("http://") || host.startsWith("https://")) {
442
+ try {
443
+ const url = new URL(hostInput);
444
+ host = url.host;
445
+ } catch (e) {
446
+ const match = hostInput.match(/^https?:\/\/([^/?#]+)/i);
447
+ if (match && match[1]) {
448
+ host = match[1];
449
+ } else {
450
+ host = hostInput.split("/")[0] || hostInput;
451
+ }
452
+ }
453
+ }
454
+ if (host.startsWith("www.")) {
455
+ host = host.substring(4);
456
+ }
457
+ return host;
458
+ }
459
+ lookup(url) {
460
+ if (!this.isInitialized) {
461
+ return { state: "unknown" };
462
+ }
463
+ try {
464
+ const urlObj = typeof url === "string" ? new URL(url) : url;
465
+ const host = this.normalizeHost(urlObj.host);
466
+ const pathname = urlObj.pathname;
467
+ const exactKey = this.createKey(host, pathname);
468
+ const exactMatch = this.widgetsByHostAndPath.get(exactKey);
469
+ if (exactMatch) {
470
+ return { state: exactMatch.state, id: exactMatch.id };
471
+ }
472
+ const hostApps = this.widgetsByHost.get(host);
473
+ if (hostApps && hostApps.length > 0) {
474
+ for (const app of hostApps) {
475
+ const appPath = app.path || "/";
476
+ if (pathname.startsWith(appPath)) {
477
+ return { state: app.state, id: app.id };
478
+ }
479
+ }
480
+ const maliciousApp = hostApps.find((app) => app.state === "malicious");
481
+ if (maliciousApp) {
482
+ return { state: "malicious", id: maliciousApp.id };
483
+ }
484
+ const rootPathApp = hostApps.find((app) => (app.path || "/") === "/");
485
+ if (rootPathApp) {
486
+ return { state: rootPathApp.state, id: rootPathApp.id };
487
+ }
488
+ return { state: "unknown" };
489
+ }
490
+ return { state: "unknown" };
491
+ } catch (error) {
492
+ return { state: "unknown" };
493
+ }
494
+ }
495
+ async refresh(directoryUrl) {
496
+ if (this.isRefreshing) {
497
+ return;
498
+ }
499
+ const urlToFetch = directoryUrl || this.defaultDirectoryUrl;
500
+ this.isRefreshing = true;
501
+ try {
502
+ const response = await fetch(urlToFetch, {
503
+ headers: { "Cache-Control": "no-cache, no-store", Pragma: "no-cache" }
504
+ });
505
+ if (!response.ok) {
506
+ throw WidgetError.directoryFetchFailed(
507
+ urlToFetch,
508
+ response.status,
509
+ response.statusText
510
+ );
511
+ }
512
+ const data = await response.json();
513
+ if (!data || typeof data !== "object" || !Array.isArray(data.mini_apps)) {
514
+ throw WidgetError.invalidDirectoryFormat("Missing expected fields (mini_apps)");
515
+ }
516
+ const newWidgetsByHostAndPath = /* @__PURE__ */ new Map();
517
+ const newWidgetsByHost = /* @__PURE__ */ new Map();
518
+ for (const app of data.mini_apps) {
519
+ if (app && typeof app === "object" && app.host && app.state) {
520
+ const originalAppHost = app.host;
521
+ const normalizedHost = this.normalizeHost(app.host);
522
+ const path = app.path || "/";
523
+ const normalizedApp = { ...app, host: normalizedHost, path };
524
+ const key = this.createKey(normalizedHost, path);
525
+ newWidgetsByHostAndPath.set(key, normalizedApp);
526
+ if (!newWidgetsByHost.has(normalizedHost)) {
527
+ newWidgetsByHost.set(normalizedHost, []);
528
+ }
529
+ newWidgetsByHost.get(normalizedHost).push(normalizedApp);
530
+ } else {
531
+ console.warn(
532
+ "[widget-kit] Skipping invalid WidgetEntry during refresh:",
533
+ app
534
+ );
535
+ }
536
+ }
537
+ for (const [host, apps] of newWidgetsByHost) {
538
+ apps.sort((a, b) => b.path.length - a.path.length);
539
+ newWidgetsByHost.set(host, apps);
540
+ }
541
+ this.widgetsByHostAndPath = newWidgetsByHostAndPath;
542
+ this.widgetsByHost = newWidgetsByHost;
543
+ this.lastUpdated = new Date(data.lastUpdated);
544
+ } catch (error) {
545
+ if (error instanceof WidgetError) throw error;
546
+ const errorMessage = error instanceof Error ? error.message : String(error);
547
+ throw WidgetError.directoryRefreshError(
548
+ errorMessage,
549
+ error instanceof Error ? error : void 0
550
+ );
551
+ } finally {
552
+ this.isRefreshing = false;
553
+ }
554
+ }
555
+ getLastUpdated() {
556
+ return this.lastUpdated;
557
+ }
558
+ getCurrentDirectory() {
559
+ return this.widgetsByHostAndPath;
560
+ }
561
+ debug() {
562
+ console.log("=== Directory Debug ===");
563
+ console.log(`Is Initialized: ${this.isInitialized}`);
564
+ console.log(`Last Updated: ${this.lastUpdated.toISOString()}`);
565
+ console.log("By Host + Path:");
566
+ for (const [key, app] of this.widgetsByHostAndPath) {
567
+ console.log(` ${key} -> State: ${app.state}, Path in App: ${app.path}`);
568
+ }
569
+ console.log("By Host (Sorted by path specificity):");
570
+ for (const [host, apps] of this.widgetsByHost) {
571
+ console.log(` ${host}:`);
572
+ apps.forEach((app) => console.log(` Path: ${app.path} -> State: ${app.state}`));
573
+ }
574
+ console.log("=======================");
575
+ }
576
+ };
577
+
578
+ // src/index.ts
579
+ var VERSION = "0.1.2";
580
+ export {
581
+ API_REPOSITORY_URL,
582
+ BlockchainError,
583
+ ErrorUtils,
584
+ InvalidOperationError,
585
+ NetworkError,
586
+ NotFoundError,
587
+ PROXY_TO_DEV_HEADERS,
588
+ PROXY_URL,
589
+ RELAYER_VALUES,
590
+ RelayerError,
591
+ SDK_TO_PROXY_HEADERS,
592
+ SecurityError,
593
+ VALID_OPERATIONS,
594
+ VERSION,
595
+ ValidationError,
596
+ WidgetDirectory,
597
+ WidgetError,
598
+ addHexPrefix,
599
+ clearClientKey,
600
+ execute,
601
+ formatNumber,
602
+ getClientKey,
603
+ getMetadata,
604
+ proxify,
605
+ removeHexPrefix,
606
+ setClientKey,
607
+ truncateAddress,
608
+ truncateString
609
+ };
610
+ //# sourceMappingURL=index.js.map