@zerohash-sdk/travel-rule-js 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/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # @zerohash-sdk/travel-rule-js
2
+
3
+ This library was generated with [Nx](https://nx.dev).
4
+
5
+ ## Running unit tests
6
+
7
+ Run `nx test @zerohash-sdk/travel-rule-js` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,513 @@
1
+ /**
2
+ * Zerohash Travel Rule JavaScript SDK
3
+ *
4
+ * A programmatic JavaScript SDK for integrating the Travel Rule widget
5
+ * into web applications without requiring HTML custom elements.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+
10
+ /**
11
+ * Generic app event structure
12
+ * @template TType - Event type string
13
+ * @template TData - Event data payload
14
+ */
15
+ declare type AppEvent<TType extends string = string, TData = Record<string, unknown>> = {
16
+ /** The type of event that occurred */
17
+ type: TType;
18
+ /** Data associated with the event */
19
+ data: TData;
20
+ };
21
+
22
+ /**
23
+ * Configuration options for the SDK
24
+ */
25
+ declare interface BaseConfig<TEvent = AppEvent> extends CommonCallbacks<TEvent> {
26
+ /**
27
+ * JWT token used for authentication
28
+ */
29
+ jwt: string;
30
+
31
+ /**
32
+ * Target environment
33
+ * @default 'production'
34
+ */
35
+ env?: Environment;
36
+
37
+ /**
38
+ * Theme mode
39
+ * @default 'light'
40
+ *
41
+ * Available themes:
42
+ * - `'auto'` - Automatically detect system preference (light/dark mode)
43
+ * - `'light'` - Force light theme
44
+ * - `'dark'` - Force dark theme
45
+ */
46
+ theme?: Theme;
47
+ }
48
+
49
+ declare type BaseErrorMessageKeys =
50
+ | 'ALREADY_RENDERED'
51
+ | 'NOT_RENDERED'
52
+ | 'INVALID_CONTAINER'
53
+ | 'SCRIPT_LOAD_FAILED'
54
+ | 'WEB_COMPONENT_NOT_DEFINED';
55
+
56
+ declare abstract class BaseJsSdk<Config extends BaseConfig<never> = BaseConfig> {
57
+ private config: Config;
58
+ private state: SdkState;
59
+ private scriptLoadingPromise?: Promise<void>;
60
+
61
+ protected abstract errorMessages: Record<BaseErrorMessageKeys, string>;
62
+ protected abstract scriptUrls: Record<string, string>;
63
+ protected abstract webComponentTag: string;
64
+
65
+ constructor(config: Config) {
66
+ if (!config.jwt || typeof config.jwt !== 'string') {
67
+ throw new Error(INVALID_JWT_ERROR_MESSAGE);
68
+ }
69
+
70
+ this.config = {
71
+ ...config,
72
+ env: config.env || DEFAULT_ENVIRONMENT,
73
+ theme: config.theme,
74
+ };
75
+
76
+ this.state = {
77
+ initialized: false,
78
+ scriptLoaded: false,
79
+ container: null,
80
+ element: null,
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Render the widget to a container element
86
+ * @param container - The container element to render the widget into
87
+ * @returns Promise that resolves when the widget is rendered
88
+ */
89
+ async render(container: HTMLElement): Promise<void> {
90
+ // Validate container
91
+ if (!container || !(container instanceof HTMLElement)) {
92
+ throw new Error(this.errorMessages.INVALID_CONTAINER);
93
+ }
94
+
95
+ // Check if already rendered
96
+ if (this.state.initialized) {
97
+ throw new Error(this.errorMessages.ALREADY_RENDERED);
98
+ }
99
+
100
+ try {
101
+ // Ensure script is loaded
102
+ await this.ensureScriptLoaded();
103
+
104
+ // Create and append the web component
105
+ const element = this.createWebComponent();
106
+
107
+ // Clear the container and append the element
108
+ container.innerHTML = '';
109
+ container.appendChild(element);
110
+
111
+ // Update state
112
+ this.state.container = container;
113
+ this.state.element = element;
114
+ this.state.initialized = true;
115
+ } catch (error) {
116
+ console.error('Failed to render widget:', error);
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Update the configuration of the widget
123
+ * @param config - Partial configuration to update
124
+ * @returns void
125
+ */
126
+ updateConfig(config: Partial<Config>): void {
127
+ if (!this.state.initialized || !this.state.element) {
128
+ throw new Error(this.errorMessages.NOT_RENDERED);
129
+ }
130
+
131
+ const element = this.state.element as SdkElement<Config>;
132
+
133
+ Object.entries(config).forEach(([key, value]) => {
134
+ if (value) {
135
+ this.config[key as keyof Config] = value;
136
+ element[key as keyof Config] = value;
137
+ }
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Destroy the widget and clean up resources
143
+ */
144
+ destroy(): void {
145
+ if (!this.state.initialized) {
146
+ return; // Nothing to destroy
147
+ }
148
+
149
+ // Remove the element from the container
150
+ if (this.state.element && this.state.element.parentNode) {
151
+ this.state.element.parentNode.removeChild(this.state.element);
152
+ }
153
+
154
+ // Clear the container
155
+ if (this.state.container) {
156
+ this.state.container.innerHTML = '';
157
+ }
158
+
159
+ // Reset state
160
+ this.state.container = null;
161
+ this.state.element = null;
162
+ this.state.initialized = false;
163
+ }
164
+
165
+ /**
166
+ * Check if the widget is currently rendered
167
+ * @returns True if the widget is rendered, false otherwise
168
+ */
169
+ isRendered(): boolean {
170
+ return this.state.initialized;
171
+ }
172
+
173
+ /**
174
+ * Get the current configuration
175
+ * @returns The current configuration object
176
+ */
177
+ getConfig(): Readonly<Config> {
178
+ return { ...this.config };
179
+ }
180
+
181
+ private getEnvironment(): Environment {
182
+ return this.config.env || DEFAULT_ENVIRONMENT;
183
+ }
184
+
185
+ private getScriptId() {
186
+ return `${this.webComponentTag}-script-${this.getEnvironment()}`;
187
+ }
188
+
189
+ private isScriptLoaded() {
190
+ return !!document.getElementById(this.getScriptId());
191
+ }
192
+
193
+ private getWebComponent() {
194
+ return customElements.get(this.webComponentTag);
195
+ }
196
+
197
+ private getEffectiveScriptUrls(): Record<string, string> {
198
+ // Support local development with Vite: an internal build pins the script
199
+ // URL for the configured env, bypassing the CDN maps entirely.
200
+ if (typeof import.meta !== 'undefined' && import.meta.env?.['VITE_INTERNAL_BUILD'] === 'true') {
201
+ const override = import.meta.env['VITE_SCRIPT_URL'];
202
+ if (override) return { [this.getEnvironment()]: override };
203
+ }
204
+
205
+ return this.scriptUrls;
206
+ }
207
+
208
+ private async loadScript() {
209
+ // When the element is already defined or another instance's script tag is
210
+ // in flight, defer to waitForWebComponent — the shared loader would no-op
211
+ // for those cases and never settle this promise.
212
+ if (this.getWebComponent() || this.isScriptLoaded()) {
213
+ return;
214
+ }
215
+
216
+ if (this.scriptLoadingPromise) {
217
+ return this.scriptLoadingPromise;
218
+ }
219
+
220
+ // The shared loader also reports failures to Faro (with JWT claims for
221
+ // partner/participant context) and removes a failed tag so a later
222
+ // render() call can retry cleanly.
223
+ this.scriptLoadingPromise = new Promise<void>((resolve, reject) => {
224
+ loadWebComponentScript({
225
+ webComponentTag: this.webComponentTag,
226
+ appName: this.webComponentTag,
227
+ scriptUrls: this.getEffectiveScriptUrls(),
228
+ collectorUrls: FARO_COLLECTOR_URLS,
229
+ env: this.getEnvironment(),
230
+ jwt: this.config.jwt,
231
+ onLoad: resolve,
232
+ onError: (info) => {
233
+ reject(
234
+ new Error(
235
+ info.reason === 'not-defined'
236
+ ? this.errorMessages.WEB_COMPONENT_NOT_DEFINED
237
+ : `${this.errorMessages.SCRIPT_LOAD_FAILED} (${this.getEnvironment()})`
238
+ )
239
+ );
240
+ },
241
+ });
242
+ });
243
+
244
+ try {
245
+ await this.scriptLoadingPromise;
246
+ } catch (error) {
247
+ this.scriptLoadingPromise = undefined;
248
+ throw error;
249
+ }
250
+
251
+ return this.scriptLoadingPromise;
252
+ }
253
+
254
+ private async waitForWebComponent(timeout = 5000) {
255
+ if (this.getWebComponent()) {
256
+ return;
257
+ }
258
+
259
+ return new Promise<void>((resolve, reject) => {
260
+ const timeoutId = setTimeout(() => {
261
+ reject(new Error(`Timeout waiting for ${this.webComponentTag} to be defined`));
262
+ }, timeout);
263
+
264
+ customElements
265
+ .whenDefined(this.webComponentTag)
266
+ .then(() => {
267
+ clearTimeout(timeoutId);
268
+ resolve();
269
+ })
270
+ .catch((error) => {
271
+ clearTimeout(timeoutId);
272
+ reject(error);
273
+ });
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Load the web component script if not already loaded
279
+ */
280
+ protected async ensureScriptLoaded(): Promise<void> {
281
+ if (this.state.scriptLoaded) {
282
+ return;
283
+ }
284
+
285
+ try {
286
+ await this.loadScript();
287
+ await this.waitForWebComponent();
288
+ this.state.scriptLoaded = true;
289
+ } catch (error) {
290
+ console.error('Failed to load Connect script:', error);
291
+ throw error;
292
+ }
293
+ }
294
+
295
+ private createWebComponent() {
296
+ const element = document.createElement(this.webComponentTag) as SdkElement<Config>;
297
+
298
+ Object.entries(this.config).forEach(([key, value]) => {
299
+ if (value) {
300
+ element[key as keyof Config] = value;
301
+ }
302
+ });
303
+
304
+ return element;
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Common callback function types shared across all Connect apps
310
+ * @template TEvent - The event type for onEvent callback
311
+ */
312
+ declare type CommonCallbacks<TEvent = AppEvent> = {
313
+ /** Called when an error occurs */
314
+ onError?: (error: ErrorPayload) => void;
315
+ /** Called when the component is closed */
316
+ onClose?: () => void;
317
+ /** Called when a general event occurs */
318
+ onEvent?: (event: TEvent) => void;
319
+ /** Called when the widget has loaded and is ready */
320
+ onLoaded?: () => void;
321
+ /**
322
+ * Called when a deposit reaches a terminal state (success/failure/verifying).
323
+ * Optional — only SDKs that drive the integrations deposit flow emit it.
324
+ */
325
+ onDeposit?: (deposit: DepositCompletedPayload) => void;
326
+ };
327
+
328
+ /**
329
+ * Deposit completed payload — emitted by SDKs that drive a deposit through
330
+ * the integrations flow. Lives on `CommonCallbacks` because both Auth and
331
+ * any SDK embedding `@zerohash/integrations-flow` (e.g. fund with `useAuth`)
332
+ * resolve through the same deposit-status hook.
333
+ */
334
+ declare type DepositCompletedPayload = {
335
+ data: {
336
+ depositId: string;
337
+ status: DepositStatus;
338
+ assetId: string;
339
+ networkId: string;
340
+ amount?: string;
341
+ accountMatchingValidation?: {
342
+ status: 'PENDING' | 'VALID' | 'INVALID' | 'ERROR';
343
+ reason?: string;
344
+ };
345
+ };
346
+ };
347
+
348
+ /**
349
+ * Deposit status object — shared across SDKs that surface deposit completion.
350
+ */
351
+ declare type DepositStatus = {
352
+ value: string;
353
+ details: string;
354
+ occurredAt: string;
355
+ };
356
+
357
+ /**
358
+ * Environment configuration for the SDK
359
+ */
360
+ export declare type Environment = 'local' | 'dev' | 'cert' | 'prod' | 'sandbox' | 'production';
361
+
362
+ /**
363
+ * Generic error codes for all Connect applications
364
+ */
365
+ export declare enum ErrorCode {
366
+ /** Network connectivity error */
367
+ NETWORK_ERROR = 'network_error',
368
+ /** Authentication or session expired error */
369
+ AUTH_ERROR = 'auth_error',
370
+ /** Resource not found error */
371
+ NOT_FOUND_ERROR = 'not_found_error',
372
+ /** Validation error from user input */
373
+ VALIDATION_ERROR = 'validation_error',
374
+ /** Server error (5xx) */
375
+ SERVER_ERROR = 'server_error',
376
+ /** Client error (4xx) */
377
+ CLIENT_ERROR = 'client_error',
378
+ /** Unknown or unexpected error */
379
+ UNKNOWN_ERROR = 'unknown_error',
380
+ }
381
+
382
+ /**
383
+ * Generic error payload structure for error callbacks
384
+ */
385
+ declare type ErrorPayload = {
386
+ /** Error code indicating the type of error */
387
+ errorCode: ErrorCode;
388
+ /** Human-readable reason for the error */
389
+ reason: string;
390
+ };
391
+
392
+ /**
393
+ * Generic HTMLElement representing a web component with custom config
394
+ */
395
+ declare type SdkElement<Config extends BaseConfig<never>> = HTMLElement & Config;
396
+
397
+ /**
398
+ * Internal state of the SDK
399
+ */
400
+ declare interface SdkState {
401
+ /** Whether the SDK is initialized */
402
+ initialized: boolean;
403
+ /** Whether the web component script is loaded */
404
+ scriptLoaded: boolean;
405
+ /** The container element where the widget is rendered */
406
+ container: HTMLElement | null;
407
+ /** The web component element */
408
+ element: HTMLElement | null;
409
+ }
410
+
411
+ /**
412
+ * Theme configuration for the SDK
413
+ *
414
+ * - `"auto"` - Automatically detect system preference (light/dark mode)
415
+ * - `"light"` - Force light theme
416
+ * - `"dark"` - Force dark theme
417
+ */
418
+ declare type Theme = 'auto' | 'light' | 'dark';
419
+
420
+ /**
421
+ * TravelRule SDK class for programmatic control of the Zerohash Travel Rule widget.
422
+ *
423
+ * Renders the `zerohash-travel-rule` web component, which handles the iframe
424
+ * and postMessage communication internally.
425
+ *
426
+ * @example
427
+ * ```javascript
428
+ * const travelRule = new TravelRule({
429
+ * jwt: 'your-jwt-token',
430
+ * depositId: 'deposit-abc123',
431
+ * env: 'prod',
432
+ * theme: 'light',
433
+ * onCompleted: ({ deposit_id, submitted_at }) => console.log('Submitted', deposit_id, submitted_at),
434
+ * onClose: () => console.log('Closed'),
435
+ * onError: ({ errorCode, reason }) => console.error(errorCode, reason),
436
+ * });
437
+ *
438
+ * await travelRule.render(document.getElementById('travel-rule-container'));
439
+ * ```
440
+ */
441
+ declare class TravelRule extends BaseJsSdk<TravelRuleConfig> {
442
+ protected errorMessages: {
443
+ ALREADY_RENDERED: string;
444
+ NOT_RENDERED: string;
445
+ INVALID_CONTAINER: string;
446
+ SCRIPT_LOAD_FAILED: string;
447
+ WEB_COMPONENT_NOT_DEFINED: string;
448
+ };
449
+ protected scriptUrls: Record<string, string>;
450
+ protected webComponentTag: string;
451
+ /**
452
+ * Render the TravelRule widget to a container element
453
+ * @param container - The container element to render the widget into
454
+ * @returns Promise that resolves when the widget is rendered
455
+ */
456
+ render(container: HTMLElement): Promise<void>;
457
+ /**
458
+ * Update the configuration of the TravelRule widget
459
+ * @param config - Partial configuration to update
460
+ */
461
+ updateConfig(config: Partial<TravelRuleConfig>): void;
462
+ /**
463
+ * Get the current configuration
464
+ * @returns The current configuration object
465
+ */
466
+ getConfig(): Readonly<TravelRuleConfig>;
467
+ /**
468
+ * Check if the TravelRule widget is currently rendered
469
+ * @returns True if the widget is rendered, false otherwise
470
+ */
471
+ isRendered(): boolean;
472
+ /**
473
+ * Destroy the TravelRule widget and clean up resources
474
+ */
475
+ destroy(): void;
476
+ }
477
+ export { TravelRule }
478
+ export default TravelRule;
479
+
480
+ declare type TravelRuleCallbacks = CommonCallbacks<TravelRuleEvent> & {
481
+ /** Called when the travel-rule flow is successfully completed */
482
+ onCompleted?: (data: TravelRuleCompletedData) => void;
483
+ };
484
+
485
+ declare type TravelRuleCompletedData = {
486
+ /** Deposit id the travel-rule submission is tied to */
487
+ deposit_id: string;
488
+ /** ISO-8601 timestamp of the travel-rule submission */
489
+ submitted_at: string;
490
+ };
491
+
492
+ export declare interface TravelRuleConfig extends BaseConfig<TravelRuleEvent>, TravelRuleCallbacks {
493
+ /**
494
+ * Deposit id the travel-rule submission is tied to. Required — every
495
+ * travel-rule submission is scoped to a specific deposit (TRD §4.1 host
496
+ * handoff contract). `BaseJsSdk.render` forwards it to the web component.
497
+ */
498
+ depositId: string;
499
+ }
500
+
501
+ /**
502
+ * Travel Rule event structure
503
+ */
504
+ declare type TravelRuleEvent = AppEvent<TravelRuleEventType>;
505
+
506
+ /**
507
+ * Travel Rule event type literals
508
+ */
509
+ declare type TravelRuleEventType = string;
510
+
511
+ export declare type ZerohashTravelRuleElement = SdkElement<TravelRuleConfig>;
512
+
513
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,346 @@
1
+ const C = "production", M = "JWT token is required and must be a string.", W = {
2
+ dev: "https://grafana-faro-collector.dev.0hash.com/collect",
3
+ cert: "https://grafana-faro-collector.cert.zerohash.com/collect",
4
+ prod: "https://grafana-faro-collector.zerohash.com/collect",
5
+ "eu-cert": "https://grafana-faro-collector.cert.zerohash.eu/collect",
6
+ "eu-prod": "https://grafana-faro-collector.zerohash.eu/collect",
7
+ sandbox: "https://grafana-faro-collector.cert.zerohash.com/collect",
8
+ production: "https://grafana-faro-collector.zerohash.com/collect"
9
+ }, P = (e) => {
10
+ if (!e || typeof e != "string")
11
+ return null;
12
+ const t = e.split(".");
13
+ if (t.length < 2)
14
+ return null;
15
+ try {
16
+ const r = t[1].replace(/-/g, "+").replace(/_/g, "/"), o = r + "===".slice(0, (4 - r.length % 4) % 4), n = typeof atob < "u" ? atob(o) : Buffer.from(o, "base64").toString("utf-8"), i = JSON.parse(n)?.payload?.region;
17
+ if (typeof i != "string")
18
+ return null;
19
+ const s = i.toLowerCase();
20
+ return s === "us" || s === "eu" ? s : null;
21
+ } catch {
22
+ return null;
23
+ }
24
+ }, S = (e, t) => P(t) !== "eu" ? e : e === "cert" ? "eu-cert" : e === "prod" ? "eu-prod" : e, U = (e) => {
25
+ if (!e || typeof e != "string")
26
+ return {};
27
+ const t = e.split(".");
28
+ if (t.length < 2)
29
+ return {};
30
+ try {
31
+ const r = t[1].replace(/-/g, "+").replace(/_/g, "/"), o = r + "===".slice(0, (4 - r.length % 4) % 4), n = typeof atob < "u" ? atob(o) : Buffer.from(o, "base64").toString("utf-8"), a = JSON.parse(n), i = a?.payload ?? {}, s = (c) => typeof c == "string" && c.length > 0 ? c : void 0;
32
+ return {
33
+ participantCode: s(i.participant_code),
34
+ platformName: s(i.platform_name),
35
+ platformCode: s(a.platform_code),
36
+ region: s(i.region)
37
+ };
38
+ } catch {
39
+ return {};
40
+ }
41
+ }, N = (e, t, r) => {
42
+ const o = S(t, r);
43
+ return e[o] ?? e[t] ?? e.prod;
44
+ }, k = (e, t, r) => {
45
+ const o = S(t, r);
46
+ return e[o] ?? e[t];
47
+ }, w = () => {
48
+ }, F = () => {
49
+ const e = new Uint8Array(8);
50
+ return globalThis.crypto.getRandomValues(e), Array.from(e, (t) => t.toString(16).padStart(2, "0")).join("");
51
+ }, $ = (e, t) => {
52
+ const r = F(), o = globalThis.__ZH_WEB_SDK_VERSION__, n = {};
53
+ e.claims.participantCode && (n.participant_code = e.claims.participantCode), e.claims.platformName && (n.platform_name = e.claims.platformName), e.claims.platformCode && (n.platform_code = e.claims.platformCode), e.claims.region && (n.region = e.claims.region), o && (n.zh_web_sdk_version = o);
54
+ const a = {
55
+ meta: {
56
+ app: { name: e.appName, version: t ?? "unknown", environment: e.env },
57
+ session: { id: r, attributes: n },
58
+ browser: typeof navigator < "u" ? { userAgent: navigator.userAgent } : void 0,
59
+ page: typeof window < "u" ? { url: window.location.origin } : void 0
60
+ },
61
+ logs: [
62
+ {
63
+ message: `Failed to load the script for ${e.webComponentTag} from ${e.env} environment.`,
64
+ level: "error",
65
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
66
+ context: {
67
+ web_component: e.webComponentTag,
68
+ script_url: e.scriptUrl,
69
+ reason: e.reason,
70
+ tried_fallback: String(e.triedFallback),
71
+ elapsed_ms: String(e.elapsedMs)
72
+ }
73
+ }
74
+ ]
75
+ };
76
+ return { sessionId: r, payload: a };
77
+ }, B = (e, t, r) => {
78
+ if (!e || typeof fetch > "u")
79
+ return;
80
+ const { sessionId: o, payload: n } = $(t, r);
81
+ try {
82
+ fetch(e, {
83
+ method: "POST",
84
+ headers: { "Content-Type": "application/json", "x-faro-session-id": o },
85
+ body: JSON.stringify(n),
86
+ keepalive: !0
87
+ }).catch(() => {
88
+ });
89
+ } catch {
90
+ }
91
+ }, V = (e) => {
92
+ const { webComponentTag: t, appName: r, appVersion: o, scriptUrls: n, fallbackScriptUrls: a, collectorUrls: i, env: s, jwt: c, timeoutMs: O = 15e3, onLoad: L, onError: I } = e;
93
+ if (typeof document > "u")
94
+ return w;
95
+ const h = `${t}-script-${s}`;
96
+ if (customElements.get(t) || document.getElementById(h))
97
+ return w;
98
+ const A = U(c), D = e.report ?? ((l) => B(i && k(i, s, c), l, o));
99
+ let m = !1, R = 0, g;
100
+ const f = () => {
101
+ g !== void 0 && clearTimeout(g);
102
+ }, E = (l, u, d) => {
103
+ if (m)
104
+ return;
105
+ const z = Math.round(performance.now() - R), y = {
106
+ webComponentTag: t,
107
+ appName: r,
108
+ env: s,
109
+ scriptUrl: u,
110
+ reason: l,
111
+ triedFallback: d,
112
+ elapsedMs: z,
113
+ claims: A
114
+ };
115
+ D(y);
116
+ const _ = d ? void 0 : N(a ?? {}, s, c);
117
+ if (_ && _ !== u) {
118
+ b(_, !0);
119
+ return;
120
+ }
121
+ m = !0, f(), document.getElementById(h)?.remove(), I?.(y);
122
+ }, b = (l, u) => {
123
+ f(), R = performance.now();
124
+ const d = document.createElement("script");
125
+ d.id = h, d.src = l, d.type = "module", d.async = !0, d.onload = () => {
126
+ setTimeout(() => {
127
+ m || (customElements.get(t) ? (m = !0, f(), L?.()) : E("not-defined", l, u));
128
+ }, 0);
129
+ }, d.onerror = () => E("network", l, u), g = setTimeout(() => E("timeout", l, u), O), document.getElementById(h)?.remove(), document.head.appendChild(d);
130
+ }, T = N(n, s, c);
131
+ return T ? (b(T, !1), () => {
132
+ m = !0, f();
133
+ }) : w;
134
+ };
135
+ class j {
136
+ config;
137
+ state;
138
+ scriptLoadingPromise;
139
+ constructor(t) {
140
+ if (!t.jwt || typeof t.jwt != "string")
141
+ throw new Error(M);
142
+ this.config = {
143
+ ...t,
144
+ env: t.env || C,
145
+ theme: t.theme
146
+ }, this.state = {
147
+ initialized: !1,
148
+ scriptLoaded: !1,
149
+ container: null,
150
+ element: null
151
+ };
152
+ }
153
+ /**
154
+ * Render the widget to a container element
155
+ * @param container - The container element to render the widget into
156
+ * @returns Promise that resolves when the widget is rendered
157
+ */
158
+ async render(t) {
159
+ if (!t || !(t instanceof HTMLElement))
160
+ throw new Error(this.errorMessages.INVALID_CONTAINER);
161
+ if (this.state.initialized)
162
+ throw new Error(this.errorMessages.ALREADY_RENDERED);
163
+ try {
164
+ await this.ensureScriptLoaded();
165
+ const r = this.createWebComponent();
166
+ t.innerHTML = "", t.appendChild(r), this.state.container = t, this.state.element = r, this.state.initialized = !0;
167
+ } catch (r) {
168
+ throw console.error("Failed to render widget:", r), r;
169
+ }
170
+ }
171
+ /**
172
+ * Update the configuration of the widget
173
+ * @param config - Partial configuration to update
174
+ * @returns void
175
+ */
176
+ updateConfig(t) {
177
+ if (!this.state.initialized || !this.state.element)
178
+ throw new Error(this.errorMessages.NOT_RENDERED);
179
+ const r = this.state.element;
180
+ Object.entries(t).forEach(([o, n]) => {
181
+ n && (this.config[o] = n, r[o] = n);
182
+ });
183
+ }
184
+ /**
185
+ * Destroy the widget and clean up resources
186
+ */
187
+ destroy() {
188
+ this.state.initialized && (this.state.element && this.state.element.parentNode && this.state.element.parentNode.removeChild(this.state.element), this.state.container && (this.state.container.innerHTML = ""), this.state.container = null, this.state.element = null, this.state.initialized = !1);
189
+ }
190
+ /**
191
+ * Check if the widget is currently rendered
192
+ * @returns True if the widget is rendered, false otherwise
193
+ */
194
+ isRendered() {
195
+ return this.state.initialized;
196
+ }
197
+ /**
198
+ * Get the current configuration
199
+ * @returns The current configuration object
200
+ */
201
+ getConfig() {
202
+ return { ...this.config };
203
+ }
204
+ getEnvironment() {
205
+ return this.config.env || C;
206
+ }
207
+ getScriptId() {
208
+ return `${this.webComponentTag}-script-${this.getEnvironment()}`;
209
+ }
210
+ isScriptLoaded() {
211
+ return !!document.getElementById(this.getScriptId());
212
+ }
213
+ getWebComponent() {
214
+ return customElements.get(this.webComponentTag);
215
+ }
216
+ getEffectiveScriptUrls() {
217
+ return this.scriptUrls;
218
+ }
219
+ async loadScript() {
220
+ if (!(this.getWebComponent() || this.isScriptLoaded())) {
221
+ if (this.scriptLoadingPromise)
222
+ return this.scriptLoadingPromise;
223
+ this.scriptLoadingPromise = new Promise((t, r) => {
224
+ V({
225
+ webComponentTag: this.webComponentTag,
226
+ appName: this.webComponentTag,
227
+ scriptUrls: this.getEffectiveScriptUrls(),
228
+ collectorUrls: W,
229
+ env: this.getEnvironment(),
230
+ jwt: this.config.jwt,
231
+ onLoad: t,
232
+ onError: (o) => {
233
+ r(new Error(o.reason === "not-defined" ? this.errorMessages.WEB_COMPONENT_NOT_DEFINED : `${this.errorMessages.SCRIPT_LOAD_FAILED} (${this.getEnvironment()})`));
234
+ }
235
+ });
236
+ });
237
+ try {
238
+ await this.scriptLoadingPromise;
239
+ } catch (t) {
240
+ throw this.scriptLoadingPromise = void 0, t;
241
+ }
242
+ return this.scriptLoadingPromise;
243
+ }
244
+ }
245
+ async waitForWebComponent(t = 5e3) {
246
+ if (!this.getWebComponent())
247
+ return new Promise((r, o) => {
248
+ const n = setTimeout(() => {
249
+ o(new Error(`Timeout waiting for ${this.webComponentTag} to be defined`));
250
+ }, t);
251
+ customElements.whenDefined(this.webComponentTag).then(() => {
252
+ clearTimeout(n), r();
253
+ }).catch((a) => {
254
+ clearTimeout(n), o(a);
255
+ });
256
+ });
257
+ }
258
+ /**
259
+ * Load the web component script if not already loaded
260
+ */
261
+ async ensureScriptLoaded() {
262
+ if (!this.state.scriptLoaded)
263
+ try {
264
+ await this.loadScript(), await this.waitForWebComponent(), this.state.scriptLoaded = !0;
265
+ } catch (t) {
266
+ throw console.error("Failed to load Connect script:", t), t;
267
+ }
268
+ }
269
+ createWebComponent() {
270
+ const t = document.createElement(this.webComponentTag);
271
+ return Object.entries(this.config).forEach(([r, o]) => {
272
+ o && (t[r] = o);
273
+ }), t;
274
+ }
275
+ }
276
+ const p = {
277
+ local: "http://localhost:5173",
278
+ dev: "https://sdk-cdn.dev.0hash.com",
279
+ cert: "https://sdk-cdn.cert.zerohash.com",
280
+ prod: "https://sdk-cdn.zerohash.com",
281
+ "eu-cert": "https://sdk-cdn.cert.zerohash.eu",
282
+ "eu-prod": "https://sdk-cdn.zerohash.eu"
283
+ }, J = (e, t, r = {}) => {
284
+ const { eu: o = !1, legacyAliases: n = !1, local: a = p.local } = r, i = (c) => `${c}/${e}/${t}`, s = {
285
+ local: i(a),
286
+ dev: i(p.dev),
287
+ cert: i(p.cert),
288
+ prod: i(p.prod)
289
+ };
290
+ return n && (s.sandbox = i(p.cert), s.production = i(p.prod)), o && (s["eu-cert"] = i(p["eu-cert"]), s["eu-prod"] = i(p["eu-prod"])), s;
291
+ };
292
+ var v;
293
+ (function(e) {
294
+ e.NETWORK_ERROR = "network_error", e.AUTH_ERROR = "auth_error", e.NOT_FOUND_ERROR = "not_found_error", e.VALIDATION_ERROR = "validation_error", e.SERVER_ERROR = "server_error", e.CLIENT_ERROR = "client_error", e.UNKNOWN_ERROR = "unknown_error";
295
+ })(v || (v = {}));
296
+ class x extends j {
297
+ errorMessages = {
298
+ ALREADY_RENDERED: "TravelRule widget is already rendered. Call destroy() before rendering again.",
299
+ NOT_RENDERED: "TravelRule widget is not rendered. Call render() first.",
300
+ INVALID_CONTAINER: "Invalid container element provided.",
301
+ SCRIPT_LOAD_FAILED: "Failed to load the Zerohash TravelRule script.",
302
+ WEB_COMPONENT_NOT_DEFINED: "Web component is not defined. Script may not be loaded."
303
+ };
304
+ scriptUrls = J("travel-rule-web", "index.js", { legacyAliases: !0, eu: !0 });
305
+ webComponentTag = "zerohash-travel-rule";
306
+ /**
307
+ * Render the TravelRule widget to a container element
308
+ * @param container - The container element to render the widget into
309
+ * @returns Promise that resolves when the widget is rendered
310
+ */
311
+ render(t) {
312
+ return super.render(t);
313
+ }
314
+ /**
315
+ * Update the configuration of the TravelRule widget
316
+ * @param config - Partial configuration to update
317
+ */
318
+ updateConfig(t) {
319
+ return super.updateConfig(t);
320
+ }
321
+ /**
322
+ * Get the current configuration
323
+ * @returns The current configuration object
324
+ */
325
+ getConfig() {
326
+ return super.getConfig();
327
+ }
328
+ /**
329
+ * Check if the TravelRule widget is currently rendered
330
+ * @returns True if the widget is rendered, false otherwise
331
+ */
332
+ isRendered() {
333
+ return super.isRendered();
334
+ }
335
+ /**
336
+ * Destroy the TravelRule widget and clean up resources
337
+ */
338
+ destroy() {
339
+ return super.destroy();
340
+ }
341
+ }
342
+ export {
343
+ v as ErrorCode,
344
+ x as TravelRule,
345
+ x as default
346
+ };
@@ -0,0 +1 @@
1
+ (function(c,m){typeof exports=="object"&&typeof module<"u"?m(exports):typeof define=="function"&&define.amd?define(["exports"],m):(c=typeof globalThis<"u"?globalThis:c||self,m(c.TravelRule={}))})(this,(function(c){"use strict";const m="production",L="JWT token is required and must be a string.",I={dev:"https://grafana-faro-collector.dev.0hash.com/collect",cert:"https://grafana-faro-collector.cert.zerohash.com/collect",prod:"https://grafana-faro-collector.zerohash.com/collect","eu-cert":"https://grafana-faro-collector.cert.zerohash.eu/collect","eu-prod":"https://grafana-faro-collector.zerohash.eu/collect",sandbox:"https://grafana-faro-collector.cert.zerohash.com/collect",production:"https://grafana-faro-collector.zerohash.com/collect"},A=e=>{if(!e||typeof e!="string")return null;const t=e.split(".");if(t.length<2)return null;try{const r=t[1].replace(/-/g,"+").replace(/_/g,"/"),o=r+"===".slice(0,(4-r.length%4)%4),n=typeof atob<"u"?atob(o):Buffer.from(o,"base64").toString("utf-8"),i=JSON.parse(n)?.payload?.region;if(typeof i!="string")return null;const s=i.toLowerCase();return s==="us"||s==="eu"?s:null}catch{return null}},b=(e,t)=>A(t)!=="eu"?e:e==="cert"?"eu-cert":e==="prod"?"eu-prod":e,D=e=>{if(!e||typeof e!="string")return{};const t=e.split(".");if(t.length<2)return{};try{const r=t[1].replace(/-/g,"+").replace(/_/g,"/"),o=r+"===".slice(0,(4-r.length%4)%4),n=typeof atob<"u"?atob(o):Buffer.from(o,"base64").toString("utf-8"),a=JSON.parse(n),i=a?.payload??{},s=d=>typeof d=="string"&&d.length>0?d:void 0;return{participantCode:s(i.participant_code),platformName:s(i.platform_name),platformCode:s(a.platform_code),region:s(i.region)}}catch{return{}}},y=(e,t,r)=>{const o=b(t,r);return e[o]??e[t]??e.prod},z=(e,t,r)=>{const o=b(t,r);return e[o]??e[t]},_=()=>{},M=()=>{const e=new Uint8Array(8);return globalThis.crypto.getRandomValues(e),Array.from(e,t=>t.toString(16).padStart(2,"0")).join("")},W=(e,t)=>{const r=M(),o=globalThis.__ZH_WEB_SDK_VERSION__,n={};e.claims.participantCode&&(n.participant_code=e.claims.participantCode),e.claims.platformName&&(n.platform_name=e.claims.platformName),e.claims.platformCode&&(n.platform_code=e.claims.platformCode),e.claims.region&&(n.region=e.claims.region),o&&(n.zh_web_sdk_version=o);const a={meta:{app:{name:e.appName,version:t??"unknown",environment:e.env},session:{id:r,attributes:n},browser:typeof navigator<"u"?{userAgent:navigator.userAgent}:void 0,page:typeof window<"u"?{url:window.location.origin}:void 0},logs:[{message:`Failed to load the script for ${e.webComponentTag} from ${e.env} environment.`,level:"error",timestamp:new Date().toISOString(),context:{web_component:e.webComponentTag,script_url:e.scriptUrl,reason:e.reason,tried_fallback:String(e.triedFallback),elapsed_ms:String(e.elapsedMs)}}]};return{sessionId:r,payload:a}},P=(e,t,r)=>{if(!e||typeof fetch>"u")return;const{sessionId:o,payload:n}=W(t,r);try{fetch(e,{method:"POST",headers:{"Content-Type":"application/json","x-faro-session-id":o},body:JSON.stringify(n),keepalive:!0}).catch(()=>{})}catch{}},U=e=>{const{webComponentTag:t,appName:r,appVersion:o,scriptUrls:n,fallbackScriptUrls:a,collectorUrls:i,env:s,jwt:d,timeoutMs:$=15e3,onLoad:j,onError:B}=e;if(typeof document>"u")return _;const g=`${t}-script-${s}`;if(customElements.get(t)||document.getElementById(g))return _;const V=D(d),J=e.report??(u=>P(i&&z(i,s,d),u,o));let f=!1,N=0,R;const E=()=>{R!==void 0&&clearTimeout(R)},w=(u,h,l)=>{if(f)return;const H=Math.round(performance.now()-N),O={webComponentTag:t,appName:r,env:s,scriptUrl:h,reason:u,triedFallback:l,elapsedMs:H,claims:V};J(O);const T=l?void 0:y(a??{},s,d);if(T&&T!==h){v(T,!0);return}f=!0,E(),document.getElementById(g)?.remove(),B?.(O)},v=(u,h)=>{E(),N=performance.now();const l=document.createElement("script");l.id=g,l.src=u,l.type="module",l.async=!0,l.onload=()=>{setTimeout(()=>{f||(customElements.get(t)?(f=!0,E(),j?.()):w("not-defined",u,h))},0)},l.onerror=()=>w("network",u,h),R=setTimeout(()=>w("timeout",u,h),$),document.getElementById(g)?.remove(),document.head.appendChild(l)},S=y(n,s,d);return S?(v(S,!1),()=>{f=!0,E()}):_};class k{config;state;scriptLoadingPromise;constructor(t){if(!t.jwt||typeof t.jwt!="string")throw new Error(L);this.config={...t,env:t.env||m,theme:t.theme},this.state={initialized:!1,scriptLoaded:!1,container:null,element:null}}async render(t){if(!t||!(t instanceof HTMLElement))throw new Error(this.errorMessages.INVALID_CONTAINER);if(this.state.initialized)throw new Error(this.errorMessages.ALREADY_RENDERED);try{await this.ensureScriptLoaded();const r=this.createWebComponent();t.innerHTML="",t.appendChild(r),this.state.container=t,this.state.element=r,this.state.initialized=!0}catch(r){throw console.error("Failed to render widget:",r),r}}updateConfig(t){if(!this.state.initialized||!this.state.element)throw new Error(this.errorMessages.NOT_RENDERED);const r=this.state.element;Object.entries(t).forEach(([o,n])=>{n&&(this.config[o]=n,r[o]=n)})}destroy(){this.state.initialized&&(this.state.element&&this.state.element.parentNode&&this.state.element.parentNode.removeChild(this.state.element),this.state.container&&(this.state.container.innerHTML=""),this.state.container=null,this.state.element=null,this.state.initialized=!1)}isRendered(){return this.state.initialized}getConfig(){return{...this.config}}getEnvironment(){return this.config.env||m}getScriptId(){return`${this.webComponentTag}-script-${this.getEnvironment()}`}isScriptLoaded(){return!!document.getElementById(this.getScriptId())}getWebComponent(){return customElements.get(this.webComponentTag)}getEffectiveScriptUrls(){return this.scriptUrls}async loadScript(){if(!(this.getWebComponent()||this.isScriptLoaded())){if(this.scriptLoadingPromise)return this.scriptLoadingPromise;this.scriptLoadingPromise=new Promise((t,r)=>{U({webComponentTag:this.webComponentTag,appName:this.webComponentTag,scriptUrls:this.getEffectiveScriptUrls(),collectorUrls:I,env:this.getEnvironment(),jwt:this.config.jwt,onLoad:t,onError:o=>{r(new Error(o.reason==="not-defined"?this.errorMessages.WEB_COMPONENT_NOT_DEFINED:`${this.errorMessages.SCRIPT_LOAD_FAILED} (${this.getEnvironment()})`))}})});try{await this.scriptLoadingPromise}catch(t){throw this.scriptLoadingPromise=void 0,t}return this.scriptLoadingPromise}}async waitForWebComponent(t=5e3){if(!this.getWebComponent())return new Promise((r,o)=>{const n=setTimeout(()=>{o(new Error(`Timeout waiting for ${this.webComponentTag} to be defined`))},t);customElements.whenDefined(this.webComponentTag).then(()=>{clearTimeout(n),r()}).catch(a=>{clearTimeout(n),o(a)})})}async ensureScriptLoaded(){if(!this.state.scriptLoaded)try{await this.loadScript(),await this.waitForWebComponent(),this.state.scriptLoaded=!0}catch(t){throw console.error("Failed to load Connect script:",t),t}}createWebComponent(){const t=document.createElement(this.webComponentTag);return Object.entries(this.config).forEach(([r,o])=>{o&&(t[r]=o)}),t}}const p={local:"http://localhost:5173",dev:"https://sdk-cdn.dev.0hash.com",cert:"https://sdk-cdn.cert.zerohash.com",prod:"https://sdk-cdn.zerohash.com","eu-cert":"https://sdk-cdn.cert.zerohash.eu","eu-prod":"https://sdk-cdn.zerohash.eu"},F=(e,t,r={})=>{const{eu:o=!1,legacyAliases:n=!1,local:a=p.local}=r,i=d=>`${d}/${e}/${t}`,s={local:i(a),dev:i(p.dev),cert:i(p.cert),prod:i(p.prod)};return n&&(s.sandbox=i(p.cert),s.production=i(p.prod)),o&&(s["eu-cert"]=i(p["eu-cert"]),s["eu-prod"]=i(p["eu-prod"])),s};c.ErrorCode=void 0,(function(e){e.NETWORK_ERROR="network_error",e.AUTH_ERROR="auth_error",e.NOT_FOUND_ERROR="not_found_error",e.VALIDATION_ERROR="validation_error",e.SERVER_ERROR="server_error",e.CLIENT_ERROR="client_error",e.UNKNOWN_ERROR="unknown_error"})(c.ErrorCode||(c.ErrorCode={}));class C extends k{errorMessages={ALREADY_RENDERED:"TravelRule widget is already rendered. Call destroy() before rendering again.",NOT_RENDERED:"TravelRule widget is not rendered. Call render() first.",INVALID_CONTAINER:"Invalid container element provided.",SCRIPT_LOAD_FAILED:"Failed to load the Zerohash TravelRule script.",WEB_COMPONENT_NOT_DEFINED:"Web component is not defined. Script may not be loaded."};scriptUrls=F("travel-rule-web","index.js",{legacyAliases:!0,eu:!0});webComponentTag="zerohash-travel-rule";render(t){return super.render(t)}updateConfig(t){return super.updateConfig(t)}getConfig(){return super.getConfig()}isRendered(){return super.isRendered()}destroy(){return super.destroy()}}c.TravelRule=C,c.default=C,Object.defineProperties(c,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@zerohash-sdk/travel-rule-js",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "license": "MIT",
10
+ "exports": {
11
+ "./package.json": "./package.json",
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.umd.js",
16
+ "default": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "!**/*.tsbuildinfo"
22
+ ],
23
+ "nx": {
24
+ "name": "travel-rule-js"
25
+ }
26
+ }