@nominalso/vibe-bridge 0.5.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/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 index_exports = {};
32
- __export(index_exports, {
33
- BRIDGE_VERSION: () => 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(index_exports);
39
-
40
- // package.json
41
- var version = "0.5.0";
38
+ module.exports = __toCommonJS(src_exports);
42
39
 
43
40
  // ../protocol-types/dist/index.js
44
41
  function normalizeSubroute(subroute) {
@@ -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
- // src/VibeAppBridge.ts
115
- var import_posthog_js = __toESM(require("posthog-js"), 1);
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 {
@@ -428,13 +454,23 @@ function resolveRequestTimeout(type, requestTimeout) {
428
454
 
429
455
  // src/VibeAppBridge.ts
430
456
  function resolvePosthog(mod) {
431
- let candidate = mod;
432
- while (candidate && typeof candidate.init !== "function" && candidate.default) {
433
- candidate = candidate.default;
434
- }
435
- return candidate;
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);
436
473
  }
437
- var posthog = resolvePosthog(import_posthog_js.default);
438
474
  var PARENT_ORIGIN_OVERRIDE_KEY = "__NOM_VIBE_PARENT_ORIGIN__";
439
475
  var PREVIEW_HOST_RE = /(?:^|\.)(?:vercel\.app|lovable\.app|lovable\.dev|lovableproject\.com)$|^(?:localhost|127\.0\.0\.1)$/;
440
476
  function isPreviewSelfHost(hostname) {
@@ -494,9 +530,22 @@ function resolveOriginPolicy(configured) {
494
530
  return { targetOrigin, isTrusted };
495
531
  }
496
532
  var VibeAppBridge = class extends BridgeDataMethods {
497
- constructor({ parentOrigin, requestTimeout } = {}) {
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 = {}) {
498
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;
499
546
  this.pendingRequests = /* @__PURE__ */ new Map();
547
+ /** Whether {@link attach} has installed the message listener + resolved origin. */
548
+ this.attached = false;
500
549
  this.context = null;
501
550
  /**
502
551
  * Set once {@link initPostHog} has successfully called `posthog.init()`.
@@ -504,6 +553,16 @@ var VibeAppBridge = class extends BridgeDataMethods {
504
553
  * recording is actually live.
505
554
  */
506
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;
507
566
  this.connectPromise = null;
508
567
  this.connectPollInterval = null;
509
568
  this.connectTimeout = null;
@@ -544,8 +603,12 @@ var VibeAppBridge = class extends BridgeDataMethods {
544
603
  },
545
604
  // Persistent (not nulled after connect): the host delivers PostHog once at
546
605
  // connect via its own push, and initPostHog is idempotent, so this can run
547
- // whenever the message arrives, independent of how connect() resolved.
548
- POSTHOG_PUSH: (payload) => this.initPostHog(payload),
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
+ },
549
612
  // Persistent: the host pushes auth changes (logout, identity/tenant
550
613
  // switch) at any time after connect.
551
614
  AUTH_CHANGED: (payload) => this.authChangeCallback?.(payload),
@@ -575,42 +638,72 @@ var VibeAppBridge = class extends BridgeDataMethods {
575
638
  this.ui = {
576
639
  /** Collapse or expand the host's side navigation. */
577
640
  setSideNav: (payload) => {
641
+ assertBrowser("ui.setSideNav()");
578
642
  this.sendCommand("SET_SIDE_NAV", payload);
579
643
  },
580
644
  /** Switch the host to a different subsidiary. */
581
645
  switchSubsidiary: (subsidiaryId) => {
646
+ assertBrowser("ui.switchSubsidiary()");
582
647
  this.sendCommand("SWITCH_SUBSIDIARY", { subsidiaryId });
583
648
  }
584
649
  };
585
- const policy = resolveOriginPolicy(parentOrigin);
586
- this.targetOrigin = policy.targetOrigin;
587
- this.isTrustedOrigin = policy.isTrusted;
588
- this.requestTimeout = requestTimeout;
650
+ this.parentOriginOption = options.parentOrigin;
651
+ this.requestTimeout = options.requestTimeout;
589
652
  this.boundHandleMessage = this.handleMessage.bind(this);
590
- window.addEventListener("message", this.boundHandleMessage);
591
653
  }
592
654
  dispatchPush(msg) {
593
655
  const handler = this.pushHandlers[msg.type];
594
656
  handler(msg.payload);
595
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
+ }
596
686
  /**
597
687
  * Connects to the host and returns the tenant/user {@link ContextPayload}.
598
688
  * **Call this once on app init and await it before any other method** — data
599
689
  * methods, `upload()`, and subroute reporting all require an established
600
690
  * connection. Concurrent calls return the same promise.
601
691
  *
602
- * Sends a `CONNECT` handshake, re-sending every 500ms until the host replies
603
- * by pushing the context (so it works even if the host mounts after the
604
- * iframe loads). Context is only ever delivered by push. Rejects with `Bridge
605
- * connect timed out` after 10 seconds if no context arrives (usually a
606
- * `parentOrigin` mismatch or the host hasn't mounted).
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).
607
698
  *
608
699
  * After connecting, if the context includes an initial subroute (e.g. from
609
700
  * a deep link), the bridge navigates to it and starts auto-tracking
610
701
  * internal navigation via the history API.
611
702
  *
612
703
  * @returns The tenant/user/subsidiary context for this app session.
613
- * @throws If no context is received within 10 seconds.
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.
614
707
  * @example
615
708
  * ```ts
616
709
  * const ctx = await bridge.connect()
@@ -618,8 +711,10 @@ var VibeAppBridge = class extends BridgeDataMethods {
618
711
  * ```
619
712
  */
620
713
  connect() {
714
+ if (typeof window === "undefined") return Promise.reject(serverEnvError("connect()"));
621
715
  if (this.context) return Promise.resolve(this.context);
622
716
  if (this.connectPromise) return this.connectPromise;
717
+ this.attach();
623
718
  if (this.targetOrigin === null) {
624
719
  return Promise.reject(
625
720
  new BridgeError(
@@ -653,7 +748,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
653
748
  this.onContextReceived = null;
654
749
  this.rejectConnect = null;
655
750
  console.log(
656
- `[vibe-bridge] Connected \u2014 bridge: ${version}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
751
+ `[vibe-bridge] Connected \u2014 bridge: ${BRIDGE_VERSION}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
657
752
  );
658
753
  const initialSubroute = ctx.subroute ? normalizeSubroute(ctx.subroute) : null;
659
754
  if (initialSubroute && initialSubroute !== "/") {
@@ -670,7 +765,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
670
765
  resolve(ctx);
671
766
  };
672
767
  const sendConnect = () => {
673
- this.sendCommand("CONNECT", { bridgeVersion: version });
768
+ this.sendCommand("CONNECT", { bridgeVersion: BRIDGE_VERSION });
674
769
  };
675
770
  sendConnect();
676
771
  this.connectPollInterval = setInterval(sendConnect, CONNECT_POLL_MS);
@@ -683,6 +778,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
683
778
  * Use this for hash-based routers or non-standard navigation patterns.
684
779
  */
685
780
  reportSubroute(subroute, options) {
781
+ assertBrowser("reportSubroute()");
686
782
  const normalized = normalizeSubroute(subroute);
687
783
  if (normalized === null) return;
688
784
  this.lastReportedSubroute = normalized;
@@ -693,6 +789,8 @@ var VibeAppBridge = class extends BridgeDataMethods {
693
789
  * If no callback is registered, the SDK uses `history.pushState` + a
694
790
  * `popstate` event, which works for most SPA routers automatically.
695
791
  * Returns an unsubscribe function.
792
+ *
793
+ * Pure — only stores the callback, so it is safe to call before {@link connect}.
696
794
  */
697
795
  onSubrouteRequest(callback) {
698
796
  this.subrouteCallback = callback;
@@ -707,6 +805,8 @@ var VibeAppBridge = class extends BridgeDataMethods {
707
805
  * returns. Returns an unsubscribe function. Single subscriber: a second call
708
806
  * replaces the first. Wire it **before {@link connect}** (the bridge warns at
709
807
  * connect if it isn't).
808
+ *
809
+ * Pure — only stores the callback, so it is safe to call before {@link connect}.
710
810
  */
711
811
  onContextChange(callback) {
712
812
  this.contextChangeCallback = callback;
@@ -723,7 +823,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
723
823
  *
724
824
  * **Wire this before {@link connect}** — without it the embedded app won't
725
825
  * sign out when the user logs out of Nominal (the bridge warns at connect if
726
- * it isn't wired).
826
+ * it isn't wired). Pure — only stores the callback.
727
827
  */
728
828
  onAuthChange(callback) {
729
829
  this.authChangeCallback = callback;
@@ -769,6 +869,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
769
869
  * ```
770
870
  */
771
871
  navigate(path, action = "push") {
872
+ assertBrowser("navigate()");
772
873
  this.sendCommand("NAVIGATE", { path, action });
773
874
  }
774
875
  /**
@@ -782,10 +883,12 @@ var VibeAppBridge = class extends BridgeDataMethods {
782
883
  * ```
783
884
  */
784
885
  navigateTo(route, routeParams, action = "push") {
886
+ assertBrowser("navigateTo()");
785
887
  this.sendCommand("NAVIGATE", { route, routeParams, action });
786
888
  }
787
889
  /** Refreshes the host's current route (re-fetches its server data). Fire-and-forget. */
788
890
  refresh() {
891
+ assertBrowser("refresh()");
789
892
  this.sendCommand("NAVIGATE", { action: "refresh" });
790
893
  }
791
894
  /**
@@ -807,8 +910,9 @@ var VibeAppBridge = class extends BridgeDataMethods {
807
910
  *
808
911
  * **No-op until {@link connect} has resolved with PostHog enabled by the
809
912
  * host** — if the host didn't send a `posthog` block (older host, or recording
810
- * disabled for this env/app), nothing is sent. App authors don't import
811
- * `posthog-js` themselves; this is the supported event surface.
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.
812
916
  *
813
917
  * The event is sent **directly** from the iframe's own PostHog instance (only
814
918
  * the session *recording* is forwarded to the parent), so it carries the
@@ -825,20 +929,33 @@ var VibeAppBridge = class extends BridgeDataMethods {
825
929
  * ```
826
930
  */
827
931
  track(event, properties) {
828
- if (!this.posthogInitialized) return;
829
- posthog.capture(event, properties);
932
+ assertBrowser("track()");
933
+ if (!this.posthogInitialized || !this.posthogInstance) return;
934
+ this.posthogInstance.capture(event, properties);
830
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
+ */
831
943
  destroy() {
832
944
  this.teardownNavigationTracking();
833
945
  this.rejectConnect?.(new BridgeError("DESTROYED", "Bridge destroyed"));
834
946
  this.stopConnectPolling();
835
- window.removeEventListener("message", this.boundHandleMessage);
947
+ if (this.attached && typeof window !== "undefined") {
948
+ window.removeEventListener("message", this.boundHandleMessage);
949
+ }
950
+ this.attached = false;
836
951
  for (const pending of this.pendingRequests.values()) {
837
952
  pending.reject(new BridgeError("DESTROYED", "Bridge destroyed"));
838
953
  }
839
954
  this.pendingRequests.clear();
840
955
  this.context = null;
841
956
  this.posthogInitialized = false;
957
+ this.posthogInstance = null;
958
+ this.posthogIniting = false;
842
959
  this.connectPromise = null;
843
960
  this.onContextReceived = null;
844
961
  this.rejectConnect = null;
@@ -857,23 +974,34 @@ var VibeAppBridge = class extends BridgeDataMethods {
857
974
  this.connectTimeout = null;
858
975
  }
859
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
+ );
983
+ }
860
984
  /**
861
985
  * Initializes PostHog session recording + analytics from the host-supplied
862
986
  * config carried by a one-time `POSTHOG_PUSH`, **only when the host opted in**
863
987
  * (`config.enabled`).
864
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).
993
+ *
865
994
  * The host is the single source of truth: when it sends no `POSTHOG_PUSH`
866
995
  * (recording disabled for this env/app) or `enabled: false`, this does
867
- * nothing — `enabled` is the kill-switch even though `posthog-js` is bundled
868
- * into the bridge. Cross-origin replay needs `recordCrossOriginIframes` on both
869
- * sides so the parent receives the iframe's rrweb frames into one stitched
870
- * recording, and `identify()` ties the iframe's recording + events to the same
871
- * person as the host. Idempotent (the `posthogInitialized` guard) and wrapped
872
- * in try/catch so a duplicate push or a PostHog failure is harmless.
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.
873
1001
  */
874
- initPostHog(config) {
1002
+ async initPostHog(config) {
875
1003
  if (!config.enabled) return;
876
- if (this.posthogInitialized) return;
1004
+ if (this.posthogInitialized || this.posthogIniting) return;
877
1005
  if (typeof window === "undefined") return;
878
1006
  if (!config.token || !config.apiHost || !config.distinctId) {
879
1007
  console.warn(
@@ -881,7 +1009,20 @@ var VibeAppBridge = class extends BridgeDataMethods {
881
1009
  );
882
1010
  return;
883
1011
  }
1012
+ this.posthogIniting = true;
884
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;
885
1026
  posthog.init(config.token, {
886
1027
  api_host: config.apiHost,
887
1028
  // absolute ingestion host, NOT a relative /ingest proxy
@@ -904,16 +1045,19 @@ var VibeAppBridge = class extends BridgeDataMethods {
904
1045
  }
905
1046
  });
906
1047
  posthog.identify(config.distinctId);
1048
+ this.posthogInstance = posthog;
907
1049
  this.posthogInitialized = true;
908
1050
  console.log("[vibe-bridge] PostHog session recording initialized");
909
1051
  } catch (err) {
910
1052
  console.warn("[vibe-bridge] PostHog init failed \u2014 continuing without recording.", err);
1053
+ } finally {
1054
+ this.posthogIniting = false;
911
1055
  }
912
1056
  }
913
1057
  /**
914
1058
  * Monkey-patches `history.pushState` and `history.replaceState` to
915
1059
  * auto-detect SPA navigation and report it to the host. Called after
916
- * connect() resolves.
1060
+ * connect() resolves (so `window`/`history` are guaranteed present).
917
1061
  */
918
1062
  setupNavigationTracking() {
919
1063
  if (this.origPushState) return;
@@ -964,6 +1108,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
964
1108
  this.sendCommand("REPORT_SUBROUTE", { subroute, replace });
965
1109
  }
966
1110
  sendCommand(type, payload) {
1111
+ this.ensureAttached();
967
1112
  this.postToParent({
968
1113
  __protocol: PROTOCOL_ID,
969
1114
  __version: PROTOCOL_VERSION,
@@ -979,12 +1124,18 @@ var VibeAppBridge = class extends BridgeDataMethods {
979
1124
  * `onProgress` callback. `type` autocompletes to every operation name and
980
1125
  * narrows `payload`/return to that operation's types.
981
1126
  *
1127
+ * @throws Rejects with `BridgeError` code `'SERVER_ENVIRONMENT'` if called
1128
+ * where there is no `window`.
982
1129
  * @example
983
1130
  * ```ts
984
1131
  * const accounts = await bridge.request('GET_ACCOUNTS', { query: { account_ids: ['acc-1'] } })
985
1132
  * ```
986
1133
  */
987
1134
  request(type, payload, onProgress) {
1135
+ if (typeof window === "undefined") {
1136
+ return Promise.reject(serverEnvError(`request(${String(type)})`));
1137
+ }
1138
+ this.ensureAttached();
988
1139
  return new Promise((resolve, reject) => {
989
1140
  const requestId = crypto.randomUUID();
990
1141
  const timer = setTimeout(
@@ -1056,3 +1207,4 @@ var VibeAppBridge = class extends BridgeDataMethods {
1056
1207
  HttpBridgeError,
1057
1208
  VibeAppBridge
1058
1209
  });
1210
+ //# sourceMappingURL=index.cjs.map