@nominalso/vibe-bridge 0.4.0 → 0.6.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/AGENTS.md +13 -4
- package/README.md +45 -7
- package/dist/chunk-AH2BCQVB.js +405 -0
- package/dist/chunk-AH2BCQVB.js.map +1 -0
- package/dist/index.browser.cjs +1211 -0
- package/dist/index.browser.cjs.map +1 -0
- package/dist/index.browser.js +1172 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.cjs +290 -89
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +138 -3784
- package/dist/index.d.ts +138 -3784
- package/dist/index.js +267 -445
- package/dist/index.js.map +1 -0
- package/dist/index.server.cjs +448 -0
- package/dist/index.server.cjs.map +1 -0
- package/dist/index.server.d.cts +51 -0
- package/dist/index.server.d.ts +51 -0
- package/dist/index.server.js +84 -0
- package/dist/index.server.js.map +1 -0
- package/dist/version-MpAmSu0t.d.cts +3819 -0
- package/dist/version-MpAmSu0t.d.ts +3819 -0
- package/docs/getting-started.md +3 -1
- package/llms.txt +3 -1
- package/package.json +27 -30
package/dist/index.cjs
CHANGED
|
@@ -28,17 +28,14 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
|
|
30
30
|
// src/index.ts
|
|
31
|
-
var
|
|
32
|
-
__export(
|
|
33
|
-
BRIDGE_VERSION: () =>
|
|
31
|
+
var src_exports = {};
|
|
32
|
+
__export(src_exports, {
|
|
33
|
+
BRIDGE_VERSION: () => BRIDGE_VERSION,
|
|
34
34
|
BridgeError: () => BridgeError,
|
|
35
35
|
HttpBridgeError: () => HttpBridgeError,
|
|
36
36
|
VibeAppBridge: () => VibeAppBridge
|
|
37
37
|
});
|
|
38
|
-
module.exports = __toCommonJS(
|
|
39
|
-
|
|
40
|
-
// package.json
|
|
41
|
-
var version = "0.4.0";
|
|
38
|
+
module.exports = __toCommonJS(src_exports);
|
|
42
39
|
|
|
43
40
|
// ../protocol-types/dist/index.js
|
|
44
41
|
function normalizeSubroute(subroute) {
|
|
@@ -54,7 +51,7 @@ function normalizeSubroute(subroute) {
|
|
|
54
51
|
return normalized;
|
|
55
52
|
}
|
|
56
53
|
var PROTOCOL_ID = "nominal-vibe-bridge";
|
|
57
|
-
var PROTOCOL_VERSION =
|
|
54
|
+
var PROTOCOL_VERSION = 2;
|
|
58
55
|
var BRIDGE_KINDS = ["request", "response", "push", "command", "progress"];
|
|
59
56
|
var CORRELATED_KINDS = ["request", "response", "progress"];
|
|
60
57
|
var BRIDGE_KINDS_SET = new Set(BRIDGE_KINDS);
|
|
@@ -69,20 +66,46 @@ function isBridgeMessage(data) {
|
|
|
69
66
|
if (CORRELATED_KINDS_SET.has(d.kind) && typeof d.requestId !== "string") return false;
|
|
70
67
|
return true;
|
|
71
68
|
}
|
|
69
|
+
var BRIDGE_ERROR_BRAND = /* @__PURE__ */ Symbol.for("@nominalso/vibe-bridge:BridgeError");
|
|
70
|
+
var HTTP_BRIDGE_ERROR_BRAND = /* @__PURE__ */ Symbol.for("@nominalso/vibe-bridge:HttpBridgeError");
|
|
72
71
|
var BridgeError = class extends Error {
|
|
73
72
|
constructor(code, message) {
|
|
74
73
|
super(message);
|
|
75
74
|
this.code = code;
|
|
76
75
|
this.name = "BridgeError";
|
|
77
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Cross-realm-safe type guard. Prefer this over `instanceof BridgeError` when
|
|
79
|
+
* an error may have been thrown by a differently-built copy of the SDK (e.g.
|
|
80
|
+
* server bundle vs client bundle) — it matches on a process-global brand
|
|
81
|
+
* rather than class identity.
|
|
82
|
+
*/
|
|
83
|
+
static isBridgeError(err) {
|
|
84
|
+
return typeof err === "object" && err !== null && err[BRIDGE_ERROR_BRAND] === true;
|
|
85
|
+
}
|
|
78
86
|
};
|
|
87
|
+
Object.defineProperty(BridgeError.prototype, BRIDGE_ERROR_BRAND, {
|
|
88
|
+
value: true,
|
|
89
|
+
enumerable: false
|
|
90
|
+
});
|
|
79
91
|
var HttpBridgeError = class extends BridgeError {
|
|
80
92
|
constructor(status, message) {
|
|
81
93
|
super("REQUEST_FAILED", message);
|
|
82
94
|
this.status = status;
|
|
83
95
|
this.name = "HttpBridgeError";
|
|
84
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Cross-realm-safe type guard (see {@link BridgeError.isBridgeError}). Returns
|
|
99
|
+
* `true` only for `HttpBridgeError` instances, not plain `BridgeError`s.
|
|
100
|
+
*/
|
|
101
|
+
static isHttpBridgeError(err) {
|
|
102
|
+
return typeof err === "object" && err !== null && err[HTTP_BRIDGE_ERROR_BRAND] === true;
|
|
103
|
+
}
|
|
85
104
|
};
|
|
105
|
+
Object.defineProperty(HttpBridgeError.prototype, HTTP_BRIDGE_ERROR_BRAND, {
|
|
106
|
+
value: true,
|
|
107
|
+
enumerable: false
|
|
108
|
+
});
|
|
86
109
|
function isOriginPattern(entry) {
|
|
87
110
|
return entry.includes("*");
|
|
88
111
|
}
|
|
@@ -111,8 +134,11 @@ function isOriginAllowed(origin, allowlist, { allowPatterns }) {
|
|
|
111
134
|
return false;
|
|
112
135
|
}
|
|
113
136
|
|
|
114
|
-
//
|
|
115
|
-
var
|
|
137
|
+
// package.json
|
|
138
|
+
var version = "0.6.0";
|
|
139
|
+
|
|
140
|
+
// src/version.ts
|
|
141
|
+
var BRIDGE_VERSION = version;
|
|
116
142
|
|
|
117
143
|
// src/data-methods.ts
|
|
118
144
|
var BridgeDataMethods = class {
|
|
@@ -409,8 +435,6 @@ var CONNECT_POLL_MS = 500;
|
|
|
409
435
|
var DEFAULT_TIMEOUTS = {
|
|
410
436
|
/** Typed Nominal API reads/writes (`GET_*` / `POST_*` / `CREATE_*` / ...). */
|
|
411
437
|
api: 3e4,
|
|
412
|
-
/** `GET_CONTEXT` re-fetch (the initial fetch is governed by `connect()`). */
|
|
413
|
-
context: 5e3,
|
|
414
438
|
/** `INVALIDATE_CACHE`. */
|
|
415
439
|
invalidate: 1e4,
|
|
416
440
|
/** `UPLOAD_FILE` — large payloads + presigned S3 PUT take a while. */
|
|
@@ -423,8 +447,6 @@ function resolveRequestTimeout(type, requestTimeout) {
|
|
|
423
447
|
return DEFAULT_TIMEOUTS.upload;
|
|
424
448
|
case "INVALIDATE_CACHE":
|
|
425
449
|
return DEFAULT_TIMEOUTS.invalidate;
|
|
426
|
-
case "GET_CONTEXT":
|
|
427
|
-
return DEFAULT_TIMEOUTS.context;
|
|
428
450
|
default:
|
|
429
451
|
return DEFAULT_TIMEOUTS.api;
|
|
430
452
|
}
|
|
@@ -432,13 +454,23 @@ function resolveRequestTimeout(type, requestTimeout) {
|
|
|
432
454
|
|
|
433
455
|
// src/VibeAppBridge.ts
|
|
434
456
|
function resolvePosthog(mod) {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
return
|
|
457
|
+
const hasInit = (o) => typeof o?.init === "function";
|
|
458
|
+
const d1 = mod?.default;
|
|
459
|
+
if (hasInit(d1)) return d1;
|
|
460
|
+
const d2 = d1?.default;
|
|
461
|
+
if (hasInit(d2)) return d2;
|
|
462
|
+
if (hasInit(mod)) return mod;
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
function serverEnvError(method) {
|
|
466
|
+
return new BridgeError(
|
|
467
|
+
"SERVER_ENVIRONMENT",
|
|
468
|
+
`VibeAppBridge.${method} was called outside a browser (no \`window\`). The bridge only runs in the browser \u2014 call it from a client component or effect, not during SSR, in a React Server Component, or in a Worker.`
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
function assertBrowser(method) {
|
|
472
|
+
if (typeof window === "undefined") throw serverEnvError(method);
|
|
440
473
|
}
|
|
441
|
-
var posthog = resolvePosthog(import_posthog_js.default);
|
|
442
474
|
var PARENT_ORIGIN_OVERRIDE_KEY = "__NOM_VIBE_PARENT_ORIGIN__";
|
|
443
475
|
var PREVIEW_HOST_RE = /(?:^|\.)(?:vercel\.app|lovable\.app|lovable\.dev|lovableproject\.com)$|^(?:localhost|127\.0\.0\.1)$/;
|
|
444
476
|
function isPreviewSelfHost(hostname) {
|
|
@@ -498,9 +530,22 @@ function resolveOriginPolicy(configured) {
|
|
|
498
530
|
return { targetOrigin, isTrusted };
|
|
499
531
|
}
|
|
500
532
|
var VibeAppBridge = class extends BridgeDataMethods {
|
|
501
|
-
|
|
533
|
+
/**
|
|
534
|
+
* Stores options only — **pure and side-effect-free**. No `window`,
|
|
535
|
+
* `document`, `history`, `postMessage`, `crypto`, or `posthog-js` access
|
|
536
|
+
* happens here, so constructing a bridge is safe during SSR / in a Server
|
|
537
|
+
* Component / in a Worker. Browser wiring is deferred to {@link connect} /
|
|
538
|
+
* {@link attach}.
|
|
539
|
+
*/
|
|
540
|
+
constructor(options = {}) {
|
|
502
541
|
super();
|
|
542
|
+
/** Concrete origin to `postMessage` to, or `null` until resolved at {@link attach}. */
|
|
543
|
+
this.targetOrigin = null;
|
|
544
|
+
/** Validates an inbound `event.origin`; rejects everything until {@link attach}. */
|
|
545
|
+
this.isTrustedOrigin = () => false;
|
|
503
546
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
547
|
+
/** Whether {@link attach} has installed the message listener + resolved origin. */
|
|
548
|
+
this.attached = false;
|
|
504
549
|
this.context = null;
|
|
505
550
|
/**
|
|
506
551
|
* Set once {@link initPostHog} has successfully called `posthog.init()`.
|
|
@@ -508,17 +553,34 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
508
553
|
* recording is actually live.
|
|
509
554
|
*/
|
|
510
555
|
this.posthogInitialized = false;
|
|
556
|
+
/** The lazily-imported `posthog-js` singleton, once recording is initialized. */
|
|
557
|
+
this.posthogInstance = null;
|
|
558
|
+
/**
|
|
559
|
+
* Set for the duration of an in-flight {@link initPostHog} (which is async — it
|
|
560
|
+
* awaits `import('posthog-js')`). Serializes initialization so two overlapping
|
|
561
|
+
* `POSTHOG_PUSH`es can't both call `posthog.init()`.
|
|
562
|
+
*/
|
|
563
|
+
this.posthogIniting = false;
|
|
564
|
+
/** So the "posthog-js not installed" warning is logged at most once. */
|
|
565
|
+
this.posthogUnavailableWarned = false;
|
|
511
566
|
this.connectPromise = null;
|
|
512
567
|
this.connectPollInterval = null;
|
|
513
568
|
this.connectTimeout = null;
|
|
514
569
|
this.onContextReceived = null;
|
|
515
|
-
|
|
570
|
+
/** Rejects an in-flight connect() (used by destroy()); `null` once settled. */
|
|
571
|
+
this.rejectConnect = null;
|
|
516
572
|
/**
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
573
|
+
* Persistent subscriber for post-connect context changes (tenant/subsidiary
|
|
574
|
+
* switch, period close, …). Distinct from {@link onContextReceived}, which only
|
|
575
|
+
* resolves the connect handshake and is cleared afterward. `null` until
|
|
576
|
+
* {@link onContextChange} is called.
|
|
520
577
|
*/
|
|
521
|
-
this.
|
|
578
|
+
this.contextChangeCallback = null;
|
|
579
|
+
/**
|
|
580
|
+
* Persistent subscriber for host auth changes (Nominal logout or
|
|
581
|
+
* identity/tenant switch). `null` until {@link onAuthChange} is called.
|
|
582
|
+
*/
|
|
583
|
+
this.authChangeCallback = null;
|
|
522
584
|
this.lastReportedSubroute = null;
|
|
523
585
|
this.suppressSubrouteReport = false;
|
|
524
586
|
this.subrouteCallback = null;
|
|
@@ -531,7 +593,25 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
531
593
|
progress: (msg) => this.handleProgress(msg)
|
|
532
594
|
};
|
|
533
595
|
this.pushHandlers = {
|
|
534
|
-
CONTEXT_PUSH: (payload) =>
|
|
596
|
+
CONTEXT_PUSH: (payload) => {
|
|
597
|
+
if (this.onContextReceived) {
|
|
598
|
+
this.onContextReceived(payload);
|
|
599
|
+
} else if (this.context) {
|
|
600
|
+
this.context = payload;
|
|
601
|
+
this.contextChangeCallback?.(payload);
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
// Persistent (not nulled after connect): the host delivers PostHog once at
|
|
605
|
+
// connect via its own push, and initPostHog is idempotent, so this can run
|
|
606
|
+
// whenever the message arrives, independent of how connect() resolved. Fire
|
|
607
|
+
// and forget — initPostHog is async (lazy-imports posthog-js) and swallows
|
|
608
|
+
// its own errors.
|
|
609
|
+
POSTHOG_PUSH: (payload) => {
|
|
610
|
+
void this.initPostHog(payload);
|
|
611
|
+
},
|
|
612
|
+
// Persistent: the host pushes auth changes (logout, identity/tenant
|
|
613
|
+
// switch) at any time after connect.
|
|
614
|
+
AUTH_CHANGED: (payload) => this.authChangeCallback?.(payload),
|
|
535
615
|
SUBROUTE_PUSH: (payload) => {
|
|
536
616
|
const safe = normalizeSubroute(payload.subroute);
|
|
537
617
|
if (!safe) return;
|
|
@@ -558,42 +638,72 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
558
638
|
this.ui = {
|
|
559
639
|
/** Collapse or expand the host's side navigation. */
|
|
560
640
|
setSideNav: (payload) => {
|
|
641
|
+
assertBrowser("ui.setSideNav()");
|
|
561
642
|
this.sendCommand("SET_SIDE_NAV", payload);
|
|
562
643
|
},
|
|
563
644
|
/** Switch the host to a different subsidiary. */
|
|
564
645
|
switchSubsidiary: (subsidiaryId) => {
|
|
646
|
+
assertBrowser("ui.switchSubsidiary()");
|
|
565
647
|
this.sendCommand("SWITCH_SUBSIDIARY", { subsidiaryId });
|
|
566
648
|
}
|
|
567
649
|
};
|
|
568
|
-
|
|
569
|
-
this.
|
|
570
|
-
this.isTrustedOrigin = policy.isTrusted;
|
|
571
|
-
this.requestTimeout = requestTimeout;
|
|
650
|
+
this.parentOriginOption = options.parentOrigin;
|
|
651
|
+
this.requestTimeout = options.requestTimeout;
|
|
572
652
|
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
573
|
-
window.addEventListener("message", this.boundHandleMessage);
|
|
574
653
|
}
|
|
575
654
|
dispatchPush(msg) {
|
|
576
655
|
const handler = this.pushHandlers[msg.type];
|
|
577
656
|
handler(msg.payload);
|
|
578
657
|
}
|
|
658
|
+
/**
|
|
659
|
+
* Installs the browser wiring: resolves the parent-origin policy and starts
|
|
660
|
+
* listening for host `postMessage`s. **Idempotent** and safe to call before
|
|
661
|
+
* {@link connect} if you want the bridge receiving pushes early; {@link
|
|
662
|
+
* connect} calls it for you, so most apps never call this directly.
|
|
663
|
+
*
|
|
664
|
+
* @throws `BridgeError` code `'SERVER_ENVIRONMENT'` if called where there is no
|
|
665
|
+
* `window`.
|
|
666
|
+
*/
|
|
667
|
+
attach() {
|
|
668
|
+
assertBrowser("attach()");
|
|
669
|
+
if (this.attached) return;
|
|
670
|
+
const policy = resolveOriginPolicy(this.parentOriginOption);
|
|
671
|
+
this.targetOrigin = policy.targetOrigin;
|
|
672
|
+
this.isTrustedOrigin = policy.isTrusted;
|
|
673
|
+
window.addEventListener("message", this.boundHandleMessage);
|
|
674
|
+
this.attached = true;
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Lazily installs the browser wiring on first transport use. Keeps the
|
|
678
|
+
* constructor pure while ensuring that any method reaching the wire (a data
|
|
679
|
+
* `request`, a fire-and-forget command) works without an explicit prior
|
|
680
|
+
* `attach()`/`connect()` — attach stays idempotent, so `connect()` is
|
|
681
|
+
* unaffected.
|
|
682
|
+
*/
|
|
683
|
+
ensureAttached() {
|
|
684
|
+
if (!this.attached) this.attach();
|
|
685
|
+
}
|
|
579
686
|
/**
|
|
580
687
|
* Connects to the host and returns the tenant/user {@link ContextPayload}.
|
|
581
688
|
* **Call this once on app init and await it before any other method** — data
|
|
582
689
|
* methods, `upload()`, and subroute reporting all require an established
|
|
583
690
|
* connection. Concurrent calls return the same promise.
|
|
584
691
|
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
* after
|
|
589
|
-
*
|
|
692
|
+
* First {@link attach | attaches} the browser wiring (message listener +
|
|
693
|
+
* origin resolution), then sends a `CONNECT` handshake, re-sending every 500ms
|
|
694
|
+
* until the host replies by pushing the context (so it works even if the host
|
|
695
|
+
* mounts after the iframe loads). Context is only ever delivered by push.
|
|
696
|
+
* Rejects with `Bridge connect timed out` after 10 seconds if no context
|
|
697
|
+
* arrives (usually a `parentOrigin` mismatch or the host hasn't mounted).
|
|
590
698
|
*
|
|
591
699
|
* After connecting, if the context includes an initial subroute (e.g. from
|
|
592
700
|
* a deep link), the bridge navigates to it and starts auto-tracking
|
|
593
701
|
* internal navigation via the history API.
|
|
594
702
|
*
|
|
595
703
|
* @returns The tenant/user/subsidiary context for this app session.
|
|
596
|
-
* @throws
|
|
704
|
+
* @throws Rejects with `BridgeError` code `'SERVER_ENVIRONMENT'` if called
|
|
705
|
+
* where there is no `window`, `'PARENT_ORIGIN_UNRESOLVED'` if no embedding
|
|
706
|
+
* origin could be resolved, or `'TIMEOUT'` if no context arrives in 10s.
|
|
597
707
|
* @example
|
|
598
708
|
* ```ts
|
|
599
709
|
* const ctx = await bridge.connect()
|
|
@@ -601,8 +711,10 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
601
711
|
* ```
|
|
602
712
|
*/
|
|
603
713
|
connect() {
|
|
714
|
+
if (typeof window === "undefined") return Promise.reject(serverEnvError("connect()"));
|
|
604
715
|
if (this.context) return Promise.resolve(this.context);
|
|
605
716
|
if (this.connectPromise) return this.connectPromise;
|
|
717
|
+
this.attach();
|
|
606
718
|
if (this.targetOrigin === null) {
|
|
607
719
|
return Promise.reject(
|
|
608
720
|
new BridgeError(
|
|
@@ -616,16 +728,16 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
616
728
|
this.stopConnectPolling();
|
|
617
729
|
this.connectPromise = null;
|
|
618
730
|
this.onContextReceived = null;
|
|
619
|
-
this.
|
|
731
|
+
this.rejectConnect = null;
|
|
620
732
|
reject(new BridgeError("TIMEOUT", "Bridge connect timed out"));
|
|
621
733
|
}, CONNECT_TIMEOUT_MS);
|
|
622
734
|
this.connectTimeout = timer;
|
|
623
|
-
this.
|
|
735
|
+
this.rejectConnect = (error) => {
|
|
624
736
|
clearTimeout(timer);
|
|
625
737
|
this.stopConnectPolling();
|
|
626
738
|
this.connectPromise = null;
|
|
627
739
|
this.onContextReceived = null;
|
|
628
|
-
this.
|
|
740
|
+
this.rejectConnect = null;
|
|
629
741
|
reject(error);
|
|
630
742
|
};
|
|
631
743
|
this.onContextReceived = (ctx) => {
|
|
@@ -634,11 +746,10 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
634
746
|
this.context = ctx;
|
|
635
747
|
this.connectPromise = null;
|
|
636
748
|
this.onContextReceived = null;
|
|
637
|
-
this.
|
|
749
|
+
this.rejectConnect = null;
|
|
638
750
|
console.log(
|
|
639
|
-
`[vibe-bridge] Connected \u2014 bridge: ${
|
|
751
|
+
`[vibe-bridge] Connected \u2014 bridge: ${BRIDGE_VERSION}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
640
752
|
);
|
|
641
|
-
this.initPostHog(ctx);
|
|
642
753
|
const initialSubroute = ctx.subroute ? normalizeSubroute(ctx.subroute) : null;
|
|
643
754
|
if (initialSubroute && initialSubroute !== "/") {
|
|
644
755
|
this.withSubrouteSuppressed(() => {
|
|
@@ -650,23 +761,14 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
650
761
|
this.lastReportedSubroute = window.location.pathname + window.location.search;
|
|
651
762
|
}
|
|
652
763
|
this.setupNavigationTracking();
|
|
764
|
+
this.warnMissingListeners();
|
|
653
765
|
resolve(ctx);
|
|
654
766
|
};
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
this.connectPollIds.add(requestId);
|
|
658
|
-
const message = {
|
|
659
|
-
__protocol: PROTOCOL_ID,
|
|
660
|
-
__version: PROTOCOL_VERSION,
|
|
661
|
-
kind: "request",
|
|
662
|
-
type: "GET_CONTEXT",
|
|
663
|
-
requestId,
|
|
664
|
-
payload: { bridgeVersion: version }
|
|
665
|
-
};
|
|
666
|
-
this.postToParent(message);
|
|
767
|
+
const sendConnect = () => {
|
|
768
|
+
this.sendCommand("CONNECT", { bridgeVersion: BRIDGE_VERSION });
|
|
667
769
|
};
|
|
668
|
-
|
|
669
|
-
this.connectPollInterval = setInterval(
|
|
770
|
+
sendConnect();
|
|
771
|
+
this.connectPollInterval = setInterval(sendConnect, CONNECT_POLL_MS);
|
|
670
772
|
});
|
|
671
773
|
return this.connectPromise;
|
|
672
774
|
}
|
|
@@ -676,6 +778,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
676
778
|
* Use this for hash-based routers or non-standard navigation patterns.
|
|
677
779
|
*/
|
|
678
780
|
reportSubroute(subroute, options) {
|
|
781
|
+
assertBrowser("reportSubroute()");
|
|
679
782
|
const normalized = normalizeSubroute(subroute);
|
|
680
783
|
if (normalized === null) return;
|
|
681
784
|
this.lastReportedSubroute = normalized;
|
|
@@ -686,6 +789,8 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
686
789
|
* If no callback is registered, the SDK uses `history.pushState` + a
|
|
687
790
|
* `popstate` event, which works for most SPA routers automatically.
|
|
688
791
|
* Returns an unsubscribe function.
|
|
792
|
+
*
|
|
793
|
+
* Pure — only stores the callback, so it is safe to call before {@link connect}.
|
|
689
794
|
*/
|
|
690
795
|
onSubrouteRequest(callback) {
|
|
691
796
|
this.subrouteCallback = callback;
|
|
@@ -693,6 +798,58 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
693
798
|
this.subrouteCallback = null;
|
|
694
799
|
};
|
|
695
800
|
}
|
|
801
|
+
/**
|
|
802
|
+
* Subscribes to post-connect context changes (tenant/subsidiary switch, period
|
|
803
|
+
* close, …). The callback fires for each {@link ContextPayload} the host pushes
|
|
804
|
+
* *after* connect — not for the initial context, which {@link connect} already
|
|
805
|
+
* returns. Returns an unsubscribe function. Single subscriber: a second call
|
|
806
|
+
* replaces the first. Wire it **before {@link connect}** (the bridge warns at
|
|
807
|
+
* connect if it isn't).
|
|
808
|
+
*
|
|
809
|
+
* Pure — only stores the callback, so it is safe to call before {@link connect}.
|
|
810
|
+
*/
|
|
811
|
+
onContextChange(callback) {
|
|
812
|
+
this.contextChangeCallback = callback;
|
|
813
|
+
return () => {
|
|
814
|
+
this.contextChangeCallback = null;
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* Subscribes to host auth changes: a Nominal logout (`authenticated: false` —
|
|
819
|
+
* sign your backend out) or an identity/tenant switch (`authenticated: true`
|
|
820
|
+
* with a new `userId`/`tenant` — re-authenticate). Fires for each change the
|
|
821
|
+
* host pushes after connect. Returns an unsubscribe function. Single
|
|
822
|
+
* subscriber: a second call replaces the first.
|
|
823
|
+
*
|
|
824
|
+
* **Wire this before {@link connect}** — without it the embedded app won't
|
|
825
|
+
* sign out when the user logs out of Nominal (the bridge warns at connect if
|
|
826
|
+
* it isn't wired). Pure — only stores the callback.
|
|
827
|
+
*/
|
|
828
|
+
onAuthChange(callback) {
|
|
829
|
+
this.authChangeCallback = callback;
|
|
830
|
+
return () => {
|
|
831
|
+
this.authChangeCallback = null;
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* At connect, nudge the app if it hasn't wired the change listeners: every
|
|
836
|
+
* embedded app should react to a Nominal logout ({@link onAuthChange}) and a
|
|
837
|
+
* tenant/subsidiary switch ({@link onContextChange}). Fires once during the
|
|
838
|
+
* connect flow (not speculatively later), so wire both **before** `connect()`.
|
|
839
|
+
* The skill/codegen does this by default; this catches hand-wired apps.
|
|
840
|
+
*/
|
|
841
|
+
warnMissingListeners() {
|
|
842
|
+
if (!this.authChangeCallback) {
|
|
843
|
+
console.warn(
|
|
844
|
+
"[vibe-bridge] connected without onAuthChange \u2014 the app will not sign out on Nominal logout or react to an identity/tenant switch. Call bridge.onAuthChange() before connect()."
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
if (!this.contextChangeCallback) {
|
|
848
|
+
console.warn(
|
|
849
|
+
"[vibe-bridge] connected without onContextChange \u2014 the app will not react to a tenant/subsidiary/period switch. Call bridge.onContextChange() before connect()."
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
696
853
|
// ---------------------------------------------------------------------------
|
|
697
854
|
// Host navigation & UI commands
|
|
698
855
|
//
|
|
@@ -712,6 +869,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
712
869
|
* ```
|
|
713
870
|
*/
|
|
714
871
|
navigate(path, action = "push") {
|
|
872
|
+
assertBrowser("navigate()");
|
|
715
873
|
this.sendCommand("NAVIGATE", { path, action });
|
|
716
874
|
}
|
|
717
875
|
/**
|
|
@@ -725,10 +883,12 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
725
883
|
* ```
|
|
726
884
|
*/
|
|
727
885
|
navigateTo(route, routeParams, action = "push") {
|
|
886
|
+
assertBrowser("navigateTo()");
|
|
728
887
|
this.sendCommand("NAVIGATE", { route, routeParams, action });
|
|
729
888
|
}
|
|
730
889
|
/** Refreshes the host's current route (re-fetches its server data). Fire-and-forget. */
|
|
731
890
|
refresh() {
|
|
891
|
+
assertBrowser("refresh()");
|
|
732
892
|
this.sendCommand("NAVIGATE", { action: "refresh" });
|
|
733
893
|
}
|
|
734
894
|
/**
|
|
@@ -750,8 +910,9 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
750
910
|
*
|
|
751
911
|
* **No-op until {@link connect} has resolved with PostHog enabled by the
|
|
752
912
|
* host** — if the host didn't send a `posthog` block (older host, or recording
|
|
753
|
-
* disabled for this env/app),
|
|
754
|
-
*
|
|
913
|
+
* disabled for this env/app), or `posthog-js` isn't installed (it is an
|
|
914
|
+
* optional dependency), nothing is sent. App authors don't import `posthog-js`
|
|
915
|
+
* themselves; this is the supported event surface.
|
|
755
916
|
*
|
|
756
917
|
* The event is sent **directly** from the iframe's own PostHog instance (only
|
|
757
918
|
* the session *recording* is forwarded to the parent), so it carries the
|
|
@@ -768,23 +929,38 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
768
929
|
* ```
|
|
769
930
|
*/
|
|
770
931
|
track(event, properties) {
|
|
771
|
-
|
|
772
|
-
|
|
932
|
+
assertBrowser("track()");
|
|
933
|
+
if (!this.posthogInitialized || !this.posthogInstance) return;
|
|
934
|
+
this.posthogInstance.capture(event, properties);
|
|
773
935
|
}
|
|
936
|
+
/**
|
|
937
|
+
* Tears down the bridge: stops the connect handshake, removes the message
|
|
938
|
+
* listener, restores patched history methods, and rejects any in-flight calls
|
|
939
|
+
* with a `'DESTROYED'` error. **Idempotent and safe on the server** — calling
|
|
940
|
+
* it when {@link connect}/{@link attach} never ran (or where there is no
|
|
941
|
+
* `window`) is a no-op, so it is safe as a React effect cleanup.
|
|
942
|
+
*/
|
|
774
943
|
destroy() {
|
|
775
944
|
this.teardownNavigationTracking();
|
|
776
|
-
this.
|
|
945
|
+
this.rejectConnect?.(new BridgeError("DESTROYED", "Bridge destroyed"));
|
|
777
946
|
this.stopConnectPolling();
|
|
778
|
-
window
|
|
947
|
+
if (this.attached && typeof window !== "undefined") {
|
|
948
|
+
window.removeEventListener("message", this.boundHandleMessage);
|
|
949
|
+
}
|
|
950
|
+
this.attached = false;
|
|
779
951
|
for (const pending of this.pendingRequests.values()) {
|
|
780
952
|
pending.reject(new BridgeError("DESTROYED", "Bridge destroyed"));
|
|
781
953
|
}
|
|
782
954
|
this.pendingRequests.clear();
|
|
783
955
|
this.context = null;
|
|
784
956
|
this.posthogInitialized = false;
|
|
957
|
+
this.posthogInstance = null;
|
|
958
|
+
this.posthogIniting = false;
|
|
785
959
|
this.connectPromise = null;
|
|
786
960
|
this.onContextReceived = null;
|
|
787
|
-
this.
|
|
961
|
+
this.rejectConnect = null;
|
|
962
|
+
this.contextChangeCallback = null;
|
|
963
|
+
this.authChangeCallback = null;
|
|
788
964
|
this.subrouteCallback = null;
|
|
789
965
|
this.lastReportedSubroute = null;
|
|
790
966
|
}
|
|
@@ -797,25 +973,35 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
797
973
|
clearTimeout(this.connectTimeout);
|
|
798
974
|
this.connectTimeout = null;
|
|
799
975
|
}
|
|
800
|
-
|
|
976
|
+
}
|
|
977
|
+
warnPosthogUnavailable() {
|
|
978
|
+
if (this.posthogUnavailableWarned) return;
|
|
979
|
+
this.posthogUnavailableWarned = true;
|
|
980
|
+
console.warn(
|
|
981
|
+
"[vibe-bridge] The host enabled session recording but `posthog-js` is not installed (it is an optional dependency). Recording and track() are disabled. Run `npm i posthog-js` to enable them."
|
|
982
|
+
);
|
|
801
983
|
}
|
|
802
984
|
/**
|
|
803
985
|
* Initializes PostHog session recording + analytics from the host-supplied
|
|
804
|
-
* config
|
|
986
|
+
* config carried by a one-time `POSTHOG_PUSH`, **only when the host opted in**
|
|
987
|
+
* (`config.enabled`).
|
|
988
|
+
*
|
|
989
|
+
* `posthog-js` is an **optional dependency**, loaded lazily via `await
|
|
990
|
+
* import('posthog-js')` the first time recording is enabled — so it stays out
|
|
991
|
+
* of the import graph (and out of any server bundle) for apps that never
|
|
992
|
+
* record. If it isn't installed, this warns once and no-ops (never throws).
|
|
805
993
|
*
|
|
806
|
-
* The host is the single source of truth: when it sends no `
|
|
807
|
-
* (
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
* best-effort and never blocks `connect()`.
|
|
994
|
+
* The host is the single source of truth: when it sends no `POSTHOG_PUSH`
|
|
995
|
+
* (recording disabled for this env/app) or `enabled: false`, this does
|
|
996
|
+
* nothing. Cross-origin replay needs `recordCrossOriginIframes` on both sides
|
|
997
|
+
* so the parent receives the iframe's rrweb frames into one stitched recording,
|
|
998
|
+
* and `identify()` ties the iframe's recording + events to the same person as
|
|
999
|
+
* the host. Idempotent (the `posthogInitialized` guard) and wrapped in
|
|
1000
|
+
* try/catch so a duplicate push or a PostHog failure is harmless.
|
|
814
1001
|
*/
|
|
815
|
-
initPostHog(
|
|
816
|
-
|
|
817
|
-
if (
|
|
818
|
-
if (this.posthogInitialized) return;
|
|
1002
|
+
async initPostHog(config) {
|
|
1003
|
+
if (!config.enabled) return;
|
|
1004
|
+
if (this.posthogInitialized || this.posthogIniting) return;
|
|
819
1005
|
if (typeof window === "undefined") return;
|
|
820
1006
|
if (!config.token || !config.apiHost || !config.distinctId) {
|
|
821
1007
|
console.warn(
|
|
@@ -823,7 +1009,20 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
823
1009
|
);
|
|
824
1010
|
return;
|
|
825
1011
|
}
|
|
1012
|
+
this.posthogIniting = true;
|
|
826
1013
|
try {
|
|
1014
|
+
let posthog;
|
|
1015
|
+
try {
|
|
1016
|
+
posthog = resolvePosthog(await import("posthog-js"));
|
|
1017
|
+
} catch {
|
|
1018
|
+
this.warnPosthogUnavailable();
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
if (!posthog) {
|
|
1022
|
+
this.warnPosthogUnavailable();
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (!this.attached) return;
|
|
827
1026
|
posthog.init(config.token, {
|
|
828
1027
|
api_host: config.apiHost,
|
|
829
1028
|
// absolute ingestion host, NOT a relative /ingest proxy
|
|
@@ -846,16 +1045,19 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
846
1045
|
}
|
|
847
1046
|
});
|
|
848
1047
|
posthog.identify(config.distinctId);
|
|
1048
|
+
this.posthogInstance = posthog;
|
|
849
1049
|
this.posthogInitialized = true;
|
|
850
1050
|
console.log("[vibe-bridge] PostHog session recording initialized");
|
|
851
1051
|
} catch (err) {
|
|
852
1052
|
console.warn("[vibe-bridge] PostHog init failed \u2014 continuing without recording.", err);
|
|
1053
|
+
} finally {
|
|
1054
|
+
this.posthogIniting = false;
|
|
853
1055
|
}
|
|
854
1056
|
}
|
|
855
1057
|
/**
|
|
856
1058
|
* Monkey-patches `history.pushState` and `history.replaceState` to
|
|
857
1059
|
* auto-detect SPA navigation and report it to the host. Called after
|
|
858
|
-
* connect() resolves.
|
|
1060
|
+
* connect() resolves (so `window`/`history` are guaranteed present).
|
|
859
1061
|
*/
|
|
860
1062
|
setupNavigationTracking() {
|
|
861
1063
|
if (this.origPushState) return;
|
|
@@ -906,6 +1108,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
906
1108
|
this.sendCommand("REPORT_SUBROUTE", { subroute, replace });
|
|
907
1109
|
}
|
|
908
1110
|
sendCommand(type, payload) {
|
|
1111
|
+
this.ensureAttached();
|
|
909
1112
|
this.postToParent({
|
|
910
1113
|
__protocol: PROTOCOL_ID,
|
|
911
1114
|
__version: PROTOCOL_VERSION,
|
|
@@ -921,12 +1124,18 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
921
1124
|
* `onProgress` callback. `type` autocompletes to every operation name and
|
|
922
1125
|
* narrows `payload`/return to that operation's types.
|
|
923
1126
|
*
|
|
1127
|
+
* @throws Rejects with `BridgeError` code `'SERVER_ENVIRONMENT'` if called
|
|
1128
|
+
* where there is no `window`.
|
|
924
1129
|
* @example
|
|
925
1130
|
* ```ts
|
|
926
1131
|
* const accounts = await bridge.request('GET_ACCOUNTS', { query: { account_ids: ['acc-1'] } })
|
|
927
1132
|
* ```
|
|
928
1133
|
*/
|
|
929
1134
|
request(type, payload, onProgress) {
|
|
1135
|
+
if (typeof window === "undefined") {
|
|
1136
|
+
return Promise.reject(serverEnvError(`request(${String(type)})`));
|
|
1137
|
+
}
|
|
1138
|
+
this.ensureAttached();
|
|
930
1139
|
return new Promise((resolve, reject) => {
|
|
931
1140
|
const requestId = crypto.randomUUID();
|
|
932
1141
|
const timer = setTimeout(
|
|
@@ -973,15 +1182,6 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
973
1182
|
this.kindHandlers[event.data.kind]?.(event.data);
|
|
974
1183
|
}
|
|
975
1184
|
handleResponse(msg) {
|
|
976
|
-
if (msg.type === "GET_CONTEXT") {
|
|
977
|
-
if (!this.connectPollIds.has(msg.requestId)) return;
|
|
978
|
-
if (msg.ok) {
|
|
979
|
-
this.onContextReceived?.(msg.data);
|
|
980
|
-
} else {
|
|
981
|
-
this.onContextError?.(new BridgeError(msg.code ?? "CONTEXT_ERROR", msg.error));
|
|
982
|
-
}
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
1185
|
const pending = this.pendingRequests.get(msg.requestId);
|
|
986
1186
|
if (!pending) return;
|
|
987
1187
|
this.pendingRequests.delete(msg.requestId);
|
|
@@ -1007,3 +1207,4 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
1007
1207
|
HttpBridgeError,
|
|
1008
1208
|
VibeAppBridge
|
|
1009
1209
|
});
|
|
1210
|
+
//# sourceMappingURL=index.cjs.map
|