@thor-commerce/app-bridge-react 0.7.3 → 0.8.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.
@@ -0,0 +1,971 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/browser.ts
21
+ var browser_exports = {};
22
+ __export(browser_exports, {
23
+ ThorAppBridgeBrowser: () => api
24
+ });
25
+ module.exports = __toCommonJS(browser_exports);
26
+
27
+ // src/navigation.ts
28
+ var EMBEDDED_LAUNCH_PARAMS = [
29
+ "appLoadId",
30
+ "embedded",
31
+ "hmac",
32
+ "host",
33
+ "id_token",
34
+ "link_source",
35
+ "locale",
36
+ "project",
37
+ "protocol",
38
+ "session",
39
+ "shop",
40
+ "tenant",
41
+ "timestamp"
42
+ ];
43
+ var NAVIGATION_BASE_URL = "https://embedded-app.local";
44
+ function normalizeSearch(search) {
45
+ if (!search) {
46
+ return "";
47
+ }
48
+ return search.startsWith("?") ? search : `?${search}`;
49
+ }
50
+ function normalizeHash(hash) {
51
+ if (!hash) {
52
+ return "";
53
+ }
54
+ return hash.startsWith("#") ? hash : `#${hash}`;
55
+ }
56
+ function buildNavigationUpdatePayload(path) {
57
+ const sanitizedPath = sanitizeEmbeddedAppPath(path) ?? path;
58
+ let pathname = sanitizedPath;
59
+ let search = "";
60
+ let hash = "";
61
+ const hashIndex = pathname.indexOf("#");
62
+ if (hashIndex >= 0) {
63
+ hash = pathname.slice(hashIndex);
64
+ pathname = pathname.slice(0, hashIndex);
65
+ }
66
+ const searchIndex = pathname.indexOf("?");
67
+ if (searchIndex >= 0) {
68
+ search = pathname.slice(searchIndex);
69
+ pathname = pathname.slice(0, searchIndex);
70
+ }
71
+ return {
72
+ path: `${pathname || "/"}${search}${hash}`,
73
+ pathname: pathname || "/",
74
+ search,
75
+ hash
76
+ };
77
+ }
78
+ function sanitizeEmbeddedAppPath(path) {
79
+ if (!path) {
80
+ return path;
81
+ }
82
+ const trimmedPath = path.trim();
83
+ if (!trimmedPath) {
84
+ return trimmedPath;
85
+ }
86
+ const url = new URL(trimmedPath, NAVIGATION_BASE_URL);
87
+ let changed = false;
88
+ for (const key of EMBEDDED_LAUNCH_PARAMS) {
89
+ if (url.searchParams.has(key)) {
90
+ url.searchParams.delete(key);
91
+ changed = true;
92
+ }
93
+ }
94
+ if (!changed) {
95
+ return trimmedPath;
96
+ }
97
+ if (/^https?:\/\//i.test(trimmedPath)) {
98
+ return url.toString();
99
+ }
100
+ return `${url.pathname}${url.search}${url.hash}`;
101
+ }
102
+ function preserveEmbeddedAppLaunchParams(path, currentHref) {
103
+ if (!path || !currentHref) {
104
+ return path;
105
+ }
106
+ try {
107
+ const currentUrl = new URL(currentHref, NAVIGATION_BASE_URL);
108
+ const nextUrl = new URL(path, currentUrl.href);
109
+ if (nextUrl.origin !== currentUrl.origin) {
110
+ return path;
111
+ }
112
+ let changed = false;
113
+ for (const key of EMBEDDED_LAUNCH_PARAMS) {
114
+ if (!nextUrl.searchParams.has(key) && currentUrl.searchParams.has(key)) {
115
+ const value = currentUrl.searchParams.get(key);
116
+ if (value != null) {
117
+ nextUrl.searchParams.set(key, value);
118
+ changed = true;
119
+ }
120
+ }
121
+ }
122
+ if (!changed) {
123
+ return path;
124
+ }
125
+ if (/^https?:\/\//i.test(path)) {
126
+ return nextUrl.toString();
127
+ }
128
+ return `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`;
129
+ } catch {
130
+ return path;
131
+ }
132
+ }
133
+ function resolveLocalNavigationPath(href, currentOrigin) {
134
+ if (!href || href.startsWith("#")) {
135
+ return null;
136
+ }
137
+ let destination;
138
+ try {
139
+ destination = new URL(href, currentOrigin);
140
+ } catch {
141
+ return null;
142
+ }
143
+ if (destination.origin !== currentOrigin) {
144
+ return null;
145
+ }
146
+ return sanitizeEmbeddedAppPath(
147
+ `${destination.pathname}${destination.search}${destination.hash}`
148
+ ) ?? null;
149
+ }
150
+ function resolveNavigationDestination(payload) {
151
+ if (typeof payload === "string") {
152
+ return sanitizeEmbeddedAppPath(payload) ?? null;
153
+ }
154
+ if (!payload || typeof payload !== "object") {
155
+ return null;
156
+ }
157
+ const value = payload;
158
+ if (typeof value.path === "string" && value.path) {
159
+ return sanitizeEmbeddedAppPath(value.path) ?? null;
160
+ }
161
+ if (typeof value.href === "string" && value.href) {
162
+ return sanitizeEmbeddedAppPath(value.href) ?? null;
163
+ }
164
+ if (typeof value.pathname !== "string" || !value.pathname) {
165
+ return null;
166
+ }
167
+ return sanitizeEmbeddedAppPath(
168
+ `${value.pathname}${normalizeSearch(value.search ?? "")}${normalizeHash(value.hash ?? "")}`
169
+ ) ?? null;
170
+ }
171
+ function normalizeNavigationItemPath(path) {
172
+ if (!path) {
173
+ return null;
174
+ }
175
+ const trimmedPath = path.trim();
176
+ if (!trimmedPath || /^https?:\/\//i.test(trimmedPath)) {
177
+ return null;
178
+ }
179
+ const normalizedPath = trimmedPath.startsWith("/") ? trimmedPath : "/" + trimmedPath;
180
+ return sanitizeEmbeddedAppPath(normalizedPath) ?? normalizedPath;
181
+ }
182
+ function normalizeBridgeNavigationItem(item) {
183
+ const label = item.label?.trim();
184
+ const path = normalizeNavigationItemPath(item.path);
185
+ if (!label || !path) {
186
+ return null;
187
+ }
188
+ const matchPattern = normalizeNavigationItemPath(item.matchPattern ?? item.path);
189
+ return {
190
+ label,
191
+ path,
192
+ ...matchPattern ? { matchPattern } : {},
193
+ ...item.exactMatch !== void 0 ? { exactMatch: item.exactMatch } : {}
194
+ };
195
+ }
196
+ function normalizeBridgeNavigationItems(items) {
197
+ if (!items?.length) {
198
+ return [];
199
+ }
200
+ return items.flatMap((item) => {
201
+ const normalizedItem = normalizeBridgeNavigationItem(item);
202
+ return normalizedItem ? [normalizedItem] : [];
203
+ });
204
+ }
205
+
206
+ // src/core.ts
207
+ var DEFAULT_NAMESPACE = "thorcommerce:app-bridge";
208
+ var DEFAULT_TIMEOUT_MS = 1e4;
209
+ var DEFAULT_REDIRECT_EVENT_TYPE = "navigation:redirect";
210
+ function createMessageId() {
211
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
212
+ return crypto.randomUUID();
213
+ }
214
+ return `msg_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
215
+ }
216
+ function resolveSelfWindow(explicitWindow) {
217
+ if (explicitWindow) {
218
+ return explicitWindow;
219
+ }
220
+ if (typeof window !== "undefined") {
221
+ return window;
222
+ }
223
+ return void 0;
224
+ }
225
+ function resolveTargetWindow(selfWindow, targetWindow) {
226
+ if (targetWindow) {
227
+ return targetWindow;
228
+ }
229
+ if (!selfWindow) {
230
+ return void 0;
231
+ }
232
+ if (selfWindow.parent && selfWindow.parent !== selfWindow) {
233
+ return selfWindow.parent;
234
+ }
235
+ return void 0;
236
+ }
237
+ function isDevelopmentEnvironment() {
238
+ const nodeEnv = typeof globalThis !== "undefined" && "process" in globalThis ? globalThis.process?.env?.NODE_ENV : void 0;
239
+ if (nodeEnv) {
240
+ return nodeEnv !== "production";
241
+ }
242
+ return true;
243
+ }
244
+ function hasAllowedOrigin(origin, allowedOrigins) {
245
+ if (!allowedOrigins || allowedOrigins.length === 0) {
246
+ return true;
247
+ }
248
+ return allowedOrigins.includes(origin);
249
+ }
250
+ function isMessageTarget(value) {
251
+ return !!value && typeof value.postMessage === "function";
252
+ }
253
+ function omitUndefinedFields(value) {
254
+ return Object.fromEntries(
255
+ Object.entries(value).filter(([, fieldValue]) => fieldValue !== void 0)
256
+ );
257
+ }
258
+ function isBridgeMessage(value, namespace = DEFAULT_NAMESPACE) {
259
+ if (!value || typeof value !== "object") {
260
+ return false;
261
+ }
262
+ const message = value;
263
+ return message.namespace === namespace && message.version === "1.0" && typeof message.kind === "string" && typeof message.id === "string" && typeof message.type === "string" && typeof message.source === "string";
264
+ }
265
+ var AppBridge = class {
266
+ constructor(options = {}) {
267
+ this.eventHandlers = /* @__PURE__ */ new Map();
268
+ this.requestHandlers = /* @__PURE__ */ new Map();
269
+ this.pendingRequests = /* @__PURE__ */ new Map();
270
+ this.namespace = options.namespace ?? DEFAULT_NAMESPACE;
271
+ this.clientId = options.clientId;
272
+ this.source = options.source ?? "embedded-app";
273
+ this.target = options.target;
274
+ this.targetOrigin = options.targetOrigin ?? "*";
275
+ this.allowedOrigins = options.allowedOrigins;
276
+ this.defaultTimeoutMs = options.requestTimeoutMs ?? DEFAULT_TIMEOUT_MS;
277
+ this.selfWindow = resolveSelfWindow(options.selfWindow);
278
+ this.targetWindow = resolveTargetWindow(this.selfWindow, options.targetWindow);
279
+ this.messageListener = (event) => {
280
+ this.handleMessage(event);
281
+ };
282
+ if (!this.selfWindow) {
283
+ throw new Error(
284
+ "AppBridge requires a browser window. Pass selfWindow explicitly when constructing it outside global window scope."
285
+ );
286
+ }
287
+ if (this.targetOrigin === "*" && isDevelopmentEnvironment()) {
288
+ console.warn(
289
+ 'AppBridge is using "*" as targetOrigin. Set targetOrigin explicitly for both the dashboard and embedded app in production.'
290
+ );
291
+ }
292
+ this.selfWindow.addEventListener("message", this.messageListener);
293
+ }
294
+ setTargetWindow(targetWindow) {
295
+ this.targetWindow = targetWindow ?? void 0;
296
+ }
297
+ hasTargetWindow() {
298
+ return this.targetWindow !== void 0;
299
+ }
300
+ redirect(payload) {
301
+ const destination = resolveNavigationDestination(payload);
302
+ if (!destination) {
303
+ throw new Error("AppBridge redirect requires a valid destination.");
304
+ }
305
+ if (!this.targetWindow) {
306
+ this.navigateSelf(destination);
307
+ return;
308
+ }
309
+ this.postMessage({
310
+ kind: "event",
311
+ type: DEFAULT_REDIRECT_EVENT_TYPE,
312
+ payload: typeof payload === "string" ? { href: payload } : payload
313
+ });
314
+ }
315
+ redirectToRemote(href) {
316
+ this.redirect({ href });
317
+ }
318
+ redirectToApp(path) {
319
+ this.redirect(typeof path === "string" ? { path } : path);
320
+ }
321
+ send(type, payload) {
322
+ this.postMessage({
323
+ kind: "event",
324
+ type,
325
+ payload
326
+ });
327
+ }
328
+ request(type, payload, options = {}) {
329
+ const messageId = createMessageId();
330
+ const timeoutMs = options.timeoutMs ?? this.defaultTimeoutMs;
331
+ return new Promise((resolve, reject) => {
332
+ const timeoutId = setTimeout(() => {
333
+ this.pendingRequests.delete(messageId);
334
+ reject(new Error(`Bridge request timed out for "${type}" after ${timeoutMs}ms.`));
335
+ }, timeoutMs);
336
+ this.pendingRequests.set(messageId, {
337
+ resolve: (value) => resolve(value),
338
+ reject,
339
+ timeoutId
340
+ });
341
+ try {
342
+ this.postMessage({
343
+ id: messageId,
344
+ kind: "request",
345
+ type,
346
+ payload
347
+ });
348
+ } catch (error) {
349
+ clearTimeout(timeoutId);
350
+ this.pendingRequests.delete(messageId);
351
+ reject(error instanceof Error ? error : new Error("Failed to send bridge request."));
352
+ }
353
+ });
354
+ }
355
+ getSessionToken(request = {}, options = {}) {
356
+ const resolvedRequest = request.clientId || this.clientId ? {
357
+ ...request,
358
+ clientId: request.clientId ?? this.clientId
359
+ } : request;
360
+ return this.request(
361
+ "thor:session-token:get",
362
+ resolvedRequest,
363
+ options
364
+ );
365
+ }
366
+ on(type, handler) {
367
+ const handlers = this.eventHandlers.get(type) ?? /* @__PURE__ */ new Set();
368
+ handlers.add(handler);
369
+ this.eventHandlers.set(type, handlers);
370
+ return () => {
371
+ handlers.delete(handler);
372
+ if (handlers.size === 0) {
373
+ this.eventHandlers.delete(type);
374
+ }
375
+ };
376
+ }
377
+ onRequest(type, handler) {
378
+ this.requestHandlers.set(type, handler);
379
+ return () => {
380
+ const registeredHandler = this.requestHandlers.get(type);
381
+ if (registeredHandler === handler) {
382
+ this.requestHandlers.delete(type);
383
+ }
384
+ };
385
+ }
386
+ destroy() {
387
+ if (this.selfWindow) {
388
+ this.selfWindow.removeEventListener("message", this.messageListener);
389
+ }
390
+ for (const pendingRequest of this.pendingRequests.values()) {
391
+ clearTimeout(pendingRequest.timeoutId);
392
+ pendingRequest.reject(new Error("AppBridge destroyed before a response was received."));
393
+ }
394
+ this.pendingRequests.clear();
395
+ this.eventHandlers.clear();
396
+ this.requestHandlers.clear();
397
+ }
398
+ handleMessage(event) {
399
+ if (!hasAllowedOrigin(event.origin, this.allowedOrigins)) {
400
+ return;
401
+ }
402
+ if (!isBridgeMessage(event.data, this.namespace)) {
403
+ return;
404
+ }
405
+ const message = event.data;
406
+ if (this.target && message.target && message.target !== this.source) {
407
+ return;
408
+ }
409
+ const receivedMessage = {
410
+ ...message,
411
+ origin: event.origin,
412
+ rawEvent: event
413
+ };
414
+ if (message.kind === "event" && message.type === DEFAULT_REDIRECT_EVENT_TYPE && this.handleRedirectMessage(receivedMessage)) {
415
+ return;
416
+ }
417
+ if (message.kind === "response" && message.replyTo) {
418
+ this.resolvePendingRequest(message.replyTo, message);
419
+ return;
420
+ }
421
+ this.emitHandlers(message.type, receivedMessage);
422
+ if (message.kind === "request") {
423
+ void this.handleRequest(receivedMessage);
424
+ }
425
+ }
426
+ emitHandlers(type, message) {
427
+ const typeHandlers = this.eventHandlers.get(type);
428
+ if (typeHandlers) {
429
+ for (const handler of typeHandlers) {
430
+ handler(message);
431
+ }
432
+ }
433
+ if (type === "*") {
434
+ return;
435
+ }
436
+ const wildcardHandlers = this.eventHandlers.get("*");
437
+ if (wildcardHandlers) {
438
+ for (const handler of wildcardHandlers) {
439
+ handler(message);
440
+ }
441
+ }
442
+ }
443
+ async handleRequest(message) {
444
+ const handler = this.requestHandlers.get(message.type);
445
+ if (!handler) {
446
+ return;
447
+ }
448
+ const replyTarget = isMessageTarget(message.rawEvent.source) ? message.rawEvent.source : this.targetWindow;
449
+ try {
450
+ const payload = await handler(message.payload, message);
451
+ this.postMessage(
452
+ {
453
+ kind: "response",
454
+ type: message.type,
455
+ payload,
456
+ replyTo: message.id
457
+ },
458
+ replyTarget
459
+ );
460
+ } catch (error) {
461
+ const bridgeError = error instanceof Error ? { code: "request_handler_error", message: error.message } : { code: "request_handler_error", message: "Unknown request handler error." };
462
+ this.postMessage(
463
+ {
464
+ kind: "response",
465
+ type: message.type,
466
+ error: bridgeError,
467
+ replyTo: message.id
468
+ },
469
+ replyTarget
470
+ );
471
+ }
472
+ }
473
+ resolvePendingRequest(messageId, message) {
474
+ const pendingRequest = this.pendingRequests.get(messageId);
475
+ if (!pendingRequest) {
476
+ return;
477
+ }
478
+ clearTimeout(pendingRequest.timeoutId);
479
+ this.pendingRequests.delete(messageId);
480
+ if (message.error) {
481
+ pendingRequest.reject(new Error(`${message.error.code}: ${message.error.message}`));
482
+ return;
483
+ }
484
+ pendingRequest.resolve(message.payload);
485
+ }
486
+ postMessage(partialMessage, targetWindowOverride) {
487
+ const targetWindow = targetWindowOverride ?? this.targetWindow;
488
+ if (!targetWindow) {
489
+ throw new Error(
490
+ "AppBridge could not resolve a target window. Pass targetWindow explicitly or call setTargetWindow()."
491
+ );
492
+ }
493
+ const message = omitUndefinedFields({
494
+ namespace: this.namespace,
495
+ version: "1.0",
496
+ id: partialMessage.id ?? createMessageId(),
497
+ kind: partialMessage.kind,
498
+ type: partialMessage.type,
499
+ source: this.source,
500
+ target: partialMessage.target ?? this.target,
501
+ payload: partialMessage.payload,
502
+ replyTo: partialMessage.replyTo,
503
+ error: partialMessage.error
504
+ });
505
+ targetWindow.postMessage(message, this.targetOrigin);
506
+ }
507
+ handleRedirectMessage(message) {
508
+ const destination = resolveNavigationDestination(message.payload);
509
+ if (!destination) {
510
+ return false;
511
+ }
512
+ this.navigateSelf(destination);
513
+ return true;
514
+ }
515
+ navigateSelf(destination) {
516
+ if (!this.selfWindow) {
517
+ throw new Error("AppBridge could not resolve a browser window for redirect.");
518
+ }
519
+ this.selfWindow.location.assign(
520
+ preserveEmbeddedAppLaunchParams(destination, this.selfWindow.location.href) ?? destination
521
+ );
522
+ }
523
+ };
524
+ function createAppBridge(options = {}) {
525
+ return new AppBridge(options);
526
+ }
527
+
528
+ // src/runtime.ts
529
+ var GLOBAL_RUNTIME_KEY = "__thorEmbeddedAppRuntime__";
530
+ var THOR_NAVIGATE_EVENT = "thor:navigate";
531
+ var EmbeddedAppRuntime = class {
532
+ constructor(config) {
533
+ this.currentPath = null;
534
+ this.navigationItems = [];
535
+ this.navigationEventType = "navigation:go";
536
+ this.navigationUpdateEventType = "navigation:update";
537
+ this.readyEventType = "app:ready";
538
+ this.sessionTokenCache = null;
539
+ this.pendingSessionToken = null;
540
+ const resolvedWindow = config.selfWindow ?? (typeof window !== "undefined" ? window : void 0);
541
+ if (!resolvedWindow) {
542
+ throw new Error("EmbeddedAppRuntime requires a browser window.");
543
+ }
544
+ this.selfWindow = resolvedWindow;
545
+ this.targetOrigin = config.targetOrigin ?? getReferrerOrigin(resolvedWindow);
546
+ this.clientId = config.clientId;
547
+ this.project = readInitialProject(resolvedWindow);
548
+ this.readyPayload = config.readyPayload ?? { clientId: config.clientId };
549
+ this.bridge = createAppBridge({
550
+ allowedOrigins: this.targetOrigin ? [this.targetOrigin] : void 0,
551
+ namespace: config.namespace,
552
+ requestTimeoutMs: config.requestTimeoutMs,
553
+ selfWindow: resolvedWindow,
554
+ source: "embedded-app",
555
+ target: "dashboard",
556
+ targetOrigin: this.targetOrigin,
557
+ targetWindow: config.targetWindow
558
+ });
559
+ this.removeNavigationRequestHandler = this.bridge.on(this.navigationEventType, (message) => {
560
+ const destination = resolveNavigationDestination(message.payload);
561
+ if (!destination) {
562
+ return;
563
+ }
564
+ const nextPath = preserveEmbeddedAppLaunchParams(
565
+ destination,
566
+ this.selfWindow.location.href
567
+ );
568
+ this.navigate(nextPath ?? destination, message);
569
+ });
570
+ this.removeNavigationInterceptors = installNavigationInterceptors({
571
+ bridge: this.bridge,
572
+ selfWindow: this.selfWindow,
573
+ navigate: (path) => this.navigate(path)
574
+ });
575
+ this.restoreFetch = installFetchInterceptor({
576
+ bridge: this.bridge,
577
+ getClientId: () => this.clientId,
578
+ getProject: () => this.project,
579
+ setProject: (project) => {
580
+ this.project = project;
581
+ },
582
+ readCachedToken: () => this.sessionTokenCache,
583
+ writeCachedToken: (token) => {
584
+ this.sessionTokenCache = token;
585
+ },
586
+ readPendingToken: () => this.pendingSessionToken,
587
+ writePendingToken: (token) => {
588
+ this.pendingSessionToken = token;
589
+ }
590
+ });
591
+ this.update(config);
592
+ }
593
+ update(config) {
594
+ if (config.clientId) {
595
+ this.clientId = config.clientId;
596
+ if (!this.readyPayload || typeof this.readyPayload === "object" && this.readyPayload !== null && "clientId" in this.readyPayload) {
597
+ this.readyPayload = config.readyPayload ?? { clientId: config.clientId };
598
+ }
599
+ }
600
+ if (config.currentPath !== void 0) {
601
+ this.currentPath = config.currentPath;
602
+ }
603
+ if (config.navigationItems !== void 0) {
604
+ this.navigationItems = normalizeBridgeNavigationItems(config.navigationItems);
605
+ }
606
+ if (config.onNavigate !== void 0) {
607
+ this.onNavigate = config.onNavigate;
608
+ }
609
+ if (config.readyEventType) {
610
+ this.readyEventType = config.readyEventType;
611
+ }
612
+ if (config.readyPayload !== void 0) {
613
+ this.readyPayload = config.readyPayload;
614
+ }
615
+ if (config.targetWindow !== void 0) {
616
+ this.bridge.setTargetWindow(config.targetWindow ?? null);
617
+ }
618
+ this.syncBridgeState();
619
+ }
620
+ clearNavigationHandler() {
621
+ this.onNavigate = void 0;
622
+ }
623
+ destroy() {
624
+ this.removeNavigationRequestHandler?.();
625
+ this.removeNavigationInterceptors();
626
+ this.restoreFetch();
627
+ this.bridge.destroy();
628
+ }
629
+ syncBridgeState() {
630
+ if (!this.bridge.hasTargetWindow()) {
631
+ return;
632
+ }
633
+ this.bridge.send(this.readyEventType, this.readyPayload);
634
+ this.bridge.send("navigation:items:update", {
635
+ items: this.navigationItems
636
+ });
637
+ if (this.currentPath) {
638
+ this.bridge.send(
639
+ this.navigationUpdateEventType,
640
+ buildNavigationUpdatePayload(this.currentPath)
641
+ );
642
+ }
643
+ }
644
+ navigate(path, message) {
645
+ if (this.onNavigate) {
646
+ this.onNavigate(path, message);
647
+ return;
648
+ }
649
+ if (dispatchNavigateEvent(this.selfWindow.document, path)) {
650
+ return;
651
+ }
652
+ this.selfWindow.location.assign(path);
653
+ }
654
+ };
655
+ function getOrCreateEmbeddedAppRuntime(config) {
656
+ const globalScope = globalThis;
657
+ const existingRuntime = globalScope[GLOBAL_RUNTIME_KEY];
658
+ if (existingRuntime) {
659
+ existingRuntime.update(config);
660
+ return existingRuntime;
661
+ }
662
+ const runtime = new EmbeddedAppRuntime(config);
663
+ globalScope[GLOBAL_RUNTIME_KEY] = runtime;
664
+ return runtime;
665
+ }
666
+ function getEmbeddedAppRuntime() {
667
+ return globalThis[GLOBAL_RUNTIME_KEY] ?? null;
668
+ }
669
+ function dispatchNavigateEvent(document2, path) {
670
+ const event = new CustomEvent(THOR_NAVIGATE_EVENT, {
671
+ cancelable: true,
672
+ detail: { path }
673
+ });
674
+ document2.dispatchEvent(event);
675
+ return event.defaultPrevented;
676
+ }
677
+ function getReferrerOrigin(explicitWindow) {
678
+ const resolvedWindow = explicitWindow ?? (typeof window !== "undefined" ? window : void 0);
679
+ const referrer = resolvedWindow?.document?.referrer;
680
+ if (!referrer) {
681
+ return void 0;
682
+ }
683
+ try {
684
+ return new URL(referrer).origin;
685
+ } catch {
686
+ return void 0;
687
+ }
688
+ }
689
+ function installNavigationInterceptors({
690
+ bridge,
691
+ selfWindow,
692
+ navigate
693
+ }) {
694
+ const document2 = selfWindow.document;
695
+ const handleLocalNavigation = (path) => {
696
+ const sanitizedPath = sanitizeEmbeddedAppPath(path);
697
+ if (!sanitizedPath) {
698
+ return;
699
+ }
700
+ const nextPath = preserveEmbeddedAppLaunchParams(
701
+ sanitizedPath,
702
+ selfWindow.location.href
703
+ );
704
+ navigate(nextPath ?? sanitizedPath);
705
+ };
706
+ const handleDocumentClick = (event) => {
707
+ if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) {
708
+ return;
709
+ }
710
+ const target = event.target;
711
+ if (!(target instanceof Element)) {
712
+ return;
713
+ }
714
+ const anchor = target.closest("a[href]");
715
+ if (!(anchor instanceof HTMLAnchorElement)) {
716
+ return;
717
+ }
718
+ if (anchor.hasAttribute("download")) {
719
+ return;
720
+ }
721
+ const targetWindow = anchor.target.toLowerCase();
722
+ const href = anchor.getAttribute("href");
723
+ if (!href) {
724
+ return;
725
+ }
726
+ if (targetWindow === "_top" || targetWindow === "_parent") {
727
+ event.preventDefault();
728
+ bridge.redirectToRemote(anchor.href);
729
+ return;
730
+ }
731
+ if (targetWindow && targetWindow !== "_self") {
732
+ return;
733
+ }
734
+ const nextPath = resolveLocalNavigationPath(href, selfWindow.location.origin);
735
+ if (!nextPath) {
736
+ return;
737
+ }
738
+ event.preventDefault();
739
+ handleLocalNavigation(nextPath);
740
+ };
741
+ const originalOpen = selfWindow.open.bind(selfWindow);
742
+ selfWindow.open = (url, target, features) => {
743
+ if (url == null) {
744
+ return originalOpen(url, target, features);
745
+ }
746
+ const href = typeof url === "string" ? url : url.toString();
747
+ const targetName = (target ?? "").toLowerCase();
748
+ if (targetName === "_top" || targetName === "_parent") {
749
+ bridge.redirectToRemote(new URL(href, selfWindow.location.href).toString());
750
+ return null;
751
+ }
752
+ if (!targetName || targetName === "_self") {
753
+ const nextPath = resolveLocalNavigationPath(
754
+ href,
755
+ selfWindow.location.origin
756
+ );
757
+ if (nextPath) {
758
+ handleLocalNavigation(nextPath);
759
+ return selfWindow;
760
+ }
761
+ }
762
+ return originalOpen(url, target, features);
763
+ };
764
+ document2.addEventListener("click", handleDocumentClick, true);
765
+ return () => {
766
+ document2.removeEventListener("click", handleDocumentClick, true);
767
+ selfWindow.open = originalOpen;
768
+ };
769
+ }
770
+ function installFetchInterceptor({
771
+ bridge,
772
+ getClientId,
773
+ getProject,
774
+ setProject,
775
+ readCachedToken,
776
+ writeCachedToken,
777
+ readPendingToken,
778
+ writePendingToken
779
+ }) {
780
+ if (typeof window === "undefined") {
781
+ return () => {
782
+ };
783
+ }
784
+ const originalFetch = globalThis.fetch.bind(globalThis);
785
+ const interceptedFetch = async (input, init) => {
786
+ if (!shouldAttachSessionToken(input)) {
787
+ return originalFetch(input, init);
788
+ }
789
+ const existingAuthorization = getExistingAuthorization(input, init);
790
+ if (existingAuthorization) {
791
+ return originalFetch(input, init);
792
+ }
793
+ const nextHeaders = new Headers(
794
+ input instanceof Request ? input.headers : init?.headers
795
+ );
796
+ try {
797
+ const sessionToken = await getSessionToken({
798
+ bridge,
799
+ clientId: getClientId(),
800
+ project: getProject,
801
+ setProject,
802
+ readCachedToken,
803
+ writeCachedToken,
804
+ readPendingToken,
805
+ writePendingToken
806
+ });
807
+ nextHeaders.set("Authorization", `Bearer ${sessionToken}`);
808
+ } catch {
809
+ if (!getProject()) {
810
+ throw new Error("Missing Thor embedded session token");
811
+ }
812
+ }
813
+ if (!nextHeaders.has("X-Requested-With")) {
814
+ nextHeaders.set("X-Requested-With", "XMLHttpRequest");
815
+ }
816
+ if (input instanceof Request) {
817
+ return originalFetch(
818
+ new Request(input, {
819
+ headers: nextHeaders
820
+ })
821
+ );
822
+ }
823
+ return originalFetch(input, {
824
+ ...init,
825
+ headers: nextHeaders
826
+ });
827
+ };
828
+ globalThis.fetch = interceptedFetch;
829
+ window.fetch = interceptedFetch;
830
+ return () => {
831
+ globalThis.fetch = originalFetch;
832
+ window.fetch = originalFetch;
833
+ };
834
+ }
835
+ async function getSessionToken({
836
+ bridge,
837
+ clientId,
838
+ project,
839
+ setProject,
840
+ readCachedToken,
841
+ writeCachedToken,
842
+ readPendingToken,
843
+ writePendingToken
844
+ }) {
845
+ const cachedToken = readCachedToken();
846
+ if (cachedToken && !isExpired(cachedToken.expiresAt)) {
847
+ return cachedToken.token;
848
+ }
849
+ const pendingToken = readPendingToken();
850
+ if (pendingToken) {
851
+ return pendingToken;
852
+ }
853
+ const nextPendingToken = bridge.getSessionToken({ clientId }).then((response) => {
854
+ const token = response.sessionToken ?? response.idToken;
855
+ if (!token) {
856
+ throw new Error("Missing Thor embedded session token");
857
+ }
858
+ if (response.project) {
859
+ setProject(response.project);
860
+ }
861
+ writeCachedToken({
862
+ token,
863
+ expiresAt: normalizeTokenExpiry(token, response.exp)
864
+ });
865
+ writePendingToken(null);
866
+ return token;
867
+ }).catch((error) => {
868
+ writePendingToken(null);
869
+ throw error;
870
+ });
871
+ writePendingToken(nextPendingToken);
872
+ return nextPendingToken;
873
+ }
874
+ function shouldAttachSessionToken(input) {
875
+ if (typeof window === "undefined") {
876
+ return false;
877
+ }
878
+ const requestUrl = resolveRequestUrl(input);
879
+ return requestUrl.origin === window.location.origin;
880
+ }
881
+ function resolveRequestUrl(input) {
882
+ if (input instanceof Request) {
883
+ return new URL(input.url);
884
+ }
885
+ if (input instanceof URL) {
886
+ return input;
887
+ }
888
+ return new URL(input, window.location.href);
889
+ }
890
+ function getExistingAuthorization(input, init) {
891
+ if (input instanceof Request && input.headers.has("Authorization")) {
892
+ return input.headers.get("Authorization");
893
+ }
894
+ if (!init?.headers) {
895
+ return null;
896
+ }
897
+ return new Headers(init.headers).get("Authorization");
898
+ }
899
+ function normalizeTokenExpiry(token, explicitExp) {
900
+ const tokenExp = explicitExp ?? decodeJwtExpiry(token);
901
+ return tokenExp ? tokenExp * 1e3 : void 0;
902
+ }
903
+ function decodeJwtExpiry(token) {
904
+ const [, payload] = token.split(".");
905
+ if (!payload || typeof window === "undefined") {
906
+ return void 0;
907
+ }
908
+ try {
909
+ const normalized = payload.replace(/-/g, "+").replace(/_/g, "/");
910
+ const padded = normalized.padEnd(
911
+ normalized.length + (4 - normalized.length % 4) % 4,
912
+ "="
913
+ );
914
+ const json = JSON.parse(window.atob(padded));
915
+ return typeof json.exp === "number" ? json.exp : void 0;
916
+ } catch {
917
+ return void 0;
918
+ }
919
+ }
920
+ function isExpired(expiresAt) {
921
+ if (!expiresAt) {
922
+ return false;
923
+ }
924
+ return Date.now() >= expiresAt - 5e3;
925
+ }
926
+ function readInitialProject(explicitWindow) {
927
+ const resolvedWindow = explicitWindow ?? (typeof window !== "undefined" ? window : void 0);
928
+ if (!resolvedWindow) {
929
+ return null;
930
+ }
931
+ try {
932
+ const url = new URL(resolvedWindow.location.href);
933
+ return url.searchParams.get("project");
934
+ } catch {
935
+ return null;
936
+ }
937
+ }
938
+
939
+ // src/browser.ts
940
+ var api = {
941
+ init(config) {
942
+ return getOrCreateEmbeddedAppRuntime(config);
943
+ },
944
+ getRuntime: getEmbeddedAppRuntime
945
+ };
946
+ if (typeof window !== "undefined") {
947
+ window.ThorAppBridge = api;
948
+ const script = document.currentScript;
949
+ const clientId = script?.dataset.clientId?.trim() ?? script?.dataset.apiKey?.trim();
950
+ if (clientId) {
951
+ getOrCreateEmbeddedAppRuntime({
952
+ clientId,
953
+ namespace: script?.dataset.namespace,
954
+ readyEventType: script?.dataset.readyEventType,
955
+ requestTimeoutMs: parseTimeout(script?.dataset.requestTimeoutMs),
956
+ targetOrigin: script?.dataset.targetOrigin
957
+ });
958
+ }
959
+ }
960
+ function parseTimeout(value) {
961
+ if (!value) {
962
+ return void 0;
963
+ }
964
+ const timeout = Number(value);
965
+ return Number.isFinite(timeout) ? timeout : void 0;
966
+ }
967
+ // Annotate the CommonJS export names for ESM import in node:
968
+ 0 && (module.exports = {
969
+ ThorAppBridgeBrowser
970
+ });
971
+ //# sourceMappingURL=browser.cjs.map