@nominalso/vibe-bridge 0.3.0 → 0.5.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 +6 -3
- package/README.md +24 -2
- package/dist/index.cjs +206 -47
- package/dist/index.d.cts +168 -26
- package/dist/index.d.ts +168 -26
- package/dist/index.js +196 -47
- package/docs/connect-lifecycle.md +10 -0
- package/docs/getting-started.md +1 -1
- package/llms.txt +2 -2
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// package.json
|
|
2
|
-
var version = "0.
|
|
2
|
+
var version = "0.5.0";
|
|
3
3
|
|
|
4
4
|
// ../protocol-types/dist/index.js
|
|
5
5
|
function normalizeSubroute(subroute) {
|
|
@@ -15,7 +15,7 @@ function normalizeSubroute(subroute) {
|
|
|
15
15
|
return normalized;
|
|
16
16
|
}
|
|
17
17
|
var PROTOCOL_ID = "nominal-vibe-bridge";
|
|
18
|
-
var PROTOCOL_VERSION =
|
|
18
|
+
var PROTOCOL_VERSION = 2;
|
|
19
19
|
var BRIDGE_KINDS = ["request", "response", "push", "command", "progress"];
|
|
20
20
|
var CORRELATED_KINDS = ["request", "response", "progress"];
|
|
21
21
|
var BRIDGE_KINDS_SET = new Set(BRIDGE_KINDS);
|
|
@@ -72,6 +72,9 @@ function isOriginAllowed(origin, allowlist, { allowPatterns }) {
|
|
|
72
72
|
return false;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// src/VibeAppBridge.ts
|
|
76
|
+
import posthogDefault from "posthog-js";
|
|
77
|
+
|
|
75
78
|
// src/data-methods.ts
|
|
76
79
|
var BridgeDataMethods = class {
|
|
77
80
|
// ---------------------------------------------------------------------------
|
|
@@ -367,8 +370,6 @@ var CONNECT_POLL_MS = 500;
|
|
|
367
370
|
var DEFAULT_TIMEOUTS = {
|
|
368
371
|
/** Typed Nominal API reads/writes (`GET_*` / `POST_*` / `CREATE_*` / ...). */
|
|
369
372
|
api: 3e4,
|
|
370
|
-
/** `GET_CONTEXT` re-fetch (the initial fetch is governed by `connect()`). */
|
|
371
|
-
context: 5e3,
|
|
372
373
|
/** `INVALIDATE_CACHE`. */
|
|
373
374
|
invalidate: 1e4,
|
|
374
375
|
/** `UPLOAD_FILE` — large payloads + presigned S3 PUT take a while. */
|
|
@@ -381,14 +382,20 @@ function resolveRequestTimeout(type, requestTimeout) {
|
|
|
381
382
|
return DEFAULT_TIMEOUTS.upload;
|
|
382
383
|
case "INVALIDATE_CACHE":
|
|
383
384
|
return DEFAULT_TIMEOUTS.invalidate;
|
|
384
|
-
case "GET_CONTEXT":
|
|
385
|
-
return DEFAULT_TIMEOUTS.context;
|
|
386
385
|
default:
|
|
387
386
|
return DEFAULT_TIMEOUTS.api;
|
|
388
387
|
}
|
|
389
388
|
}
|
|
390
389
|
|
|
391
390
|
// src/VibeAppBridge.ts
|
|
391
|
+
function resolvePosthog(mod) {
|
|
392
|
+
let candidate = mod;
|
|
393
|
+
while (candidate && typeof candidate.init !== "function" && candidate.default) {
|
|
394
|
+
candidate = candidate.default;
|
|
395
|
+
}
|
|
396
|
+
return candidate;
|
|
397
|
+
}
|
|
398
|
+
var posthog = resolvePosthog(posthogDefault);
|
|
392
399
|
var PARENT_ORIGIN_OVERRIDE_KEY = "__NOM_VIBE_PARENT_ORIGIN__";
|
|
393
400
|
var PREVIEW_HOST_RE = /(?:^|\.)(?:vercel\.app|lovable\.app|lovable\.dev|lovableproject\.com)$|^(?:localhost|127\.0\.0\.1)$/;
|
|
394
401
|
function isPreviewSelfHost(hostname) {
|
|
@@ -452,17 +459,30 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
452
459
|
super();
|
|
453
460
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
454
461
|
this.context = null;
|
|
462
|
+
/**
|
|
463
|
+
* Set once {@link initPostHog} has successfully called `posthog.init()`.
|
|
464
|
+
* Guards against a double-init and makes {@link track} a no-op until
|
|
465
|
+
* recording is actually live.
|
|
466
|
+
*/
|
|
467
|
+
this.posthogInitialized = false;
|
|
455
468
|
this.connectPromise = null;
|
|
456
469
|
this.connectPollInterval = null;
|
|
457
470
|
this.connectTimeout = null;
|
|
458
471
|
this.onContextReceived = null;
|
|
459
|
-
|
|
472
|
+
/** Rejects an in-flight connect() (used by destroy()); `null` once settled. */
|
|
473
|
+
this.rejectConnect = null;
|
|
460
474
|
/**
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
475
|
+
* Persistent subscriber for post-connect context changes (tenant/subsidiary
|
|
476
|
+
* switch, period close, …). Distinct from {@link onContextReceived}, which only
|
|
477
|
+
* resolves the connect handshake and is cleared afterward. `null` until
|
|
478
|
+
* {@link onContextChange} is called.
|
|
464
479
|
*/
|
|
465
|
-
this.
|
|
480
|
+
this.contextChangeCallback = null;
|
|
481
|
+
/**
|
|
482
|
+
* Persistent subscriber for host auth changes (Nominal logout or
|
|
483
|
+
* identity/tenant switch). `null` until {@link onAuthChange} is called.
|
|
484
|
+
*/
|
|
485
|
+
this.authChangeCallback = null;
|
|
466
486
|
this.lastReportedSubroute = null;
|
|
467
487
|
this.suppressSubrouteReport = false;
|
|
468
488
|
this.subrouteCallback = null;
|
|
@@ -475,7 +495,21 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
475
495
|
progress: (msg) => this.handleProgress(msg)
|
|
476
496
|
};
|
|
477
497
|
this.pushHandlers = {
|
|
478
|
-
CONTEXT_PUSH: (payload) =>
|
|
498
|
+
CONTEXT_PUSH: (payload) => {
|
|
499
|
+
if (this.onContextReceived) {
|
|
500
|
+
this.onContextReceived(payload);
|
|
501
|
+
} else if (this.context) {
|
|
502
|
+
this.context = payload;
|
|
503
|
+
this.contextChangeCallback?.(payload);
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
// Persistent (not nulled after connect): the host delivers PostHog once at
|
|
507
|
+
// connect via its own push, and initPostHog is idempotent, so this can run
|
|
508
|
+
// whenever the message arrives, independent of how connect() resolved.
|
|
509
|
+
POSTHOG_PUSH: (payload) => this.initPostHog(payload),
|
|
510
|
+
// Persistent: the host pushes auth changes (logout, identity/tenant
|
|
511
|
+
// switch) at any time after connect.
|
|
512
|
+
AUTH_CHANGED: (payload) => this.authChangeCallback?.(payload),
|
|
479
513
|
SUBROUTE_PUSH: (payload) => {
|
|
480
514
|
const safe = normalizeSubroute(payload.subroute);
|
|
481
515
|
if (!safe) return;
|
|
@@ -526,11 +560,11 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
526
560
|
* methods, `upload()`, and subroute reporting all require an established
|
|
527
561
|
* connection. Concurrent calls return the same promise.
|
|
528
562
|
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
* after 10 seconds if no context arrives (usually a
|
|
533
|
-
* or the host hasn't mounted).
|
|
563
|
+
* Sends a `CONNECT` handshake, re-sending every 500ms until the host replies
|
|
564
|
+
* by pushing the context (so it works even if the host mounts after the
|
|
565
|
+
* iframe loads). Context is only ever delivered by push. Rejects with `Bridge
|
|
566
|
+
* connect timed out` after 10 seconds if no context arrives (usually a
|
|
567
|
+
* `parentOrigin` mismatch or the host hasn't mounted).
|
|
534
568
|
*
|
|
535
569
|
* After connecting, if the context includes an initial subroute (e.g. from
|
|
536
570
|
* a deep link), the bridge navigates to it and starts auto-tracking
|
|
@@ -560,16 +594,16 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
560
594
|
this.stopConnectPolling();
|
|
561
595
|
this.connectPromise = null;
|
|
562
596
|
this.onContextReceived = null;
|
|
563
|
-
this.
|
|
597
|
+
this.rejectConnect = null;
|
|
564
598
|
reject(new BridgeError("TIMEOUT", "Bridge connect timed out"));
|
|
565
599
|
}, CONNECT_TIMEOUT_MS);
|
|
566
600
|
this.connectTimeout = timer;
|
|
567
|
-
this.
|
|
601
|
+
this.rejectConnect = (error) => {
|
|
568
602
|
clearTimeout(timer);
|
|
569
603
|
this.stopConnectPolling();
|
|
570
604
|
this.connectPromise = null;
|
|
571
605
|
this.onContextReceived = null;
|
|
572
|
-
this.
|
|
606
|
+
this.rejectConnect = null;
|
|
573
607
|
reject(error);
|
|
574
608
|
};
|
|
575
609
|
this.onContextReceived = (ctx) => {
|
|
@@ -578,7 +612,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
578
612
|
this.context = ctx;
|
|
579
613
|
this.connectPromise = null;
|
|
580
614
|
this.onContextReceived = null;
|
|
581
|
-
this.
|
|
615
|
+
this.rejectConnect = null;
|
|
582
616
|
console.log(
|
|
583
617
|
`[vibe-bridge] Connected \u2014 bridge: ${version}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
584
618
|
);
|
|
@@ -593,23 +627,14 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
593
627
|
this.lastReportedSubroute = window.location.pathname + window.location.search;
|
|
594
628
|
}
|
|
595
629
|
this.setupNavigationTracking();
|
|
630
|
+
this.warnMissingListeners();
|
|
596
631
|
resolve(ctx);
|
|
597
632
|
};
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
this.connectPollIds.add(requestId);
|
|
601
|
-
const message = {
|
|
602
|
-
__protocol: PROTOCOL_ID,
|
|
603
|
-
__version: PROTOCOL_VERSION,
|
|
604
|
-
kind: "request",
|
|
605
|
-
type: "GET_CONTEXT",
|
|
606
|
-
requestId,
|
|
607
|
-
payload: { bridgeVersion: version }
|
|
608
|
-
};
|
|
609
|
-
this.postToParent(message);
|
|
633
|
+
const sendConnect = () => {
|
|
634
|
+
this.sendCommand("CONNECT", { bridgeVersion: version });
|
|
610
635
|
};
|
|
611
|
-
|
|
612
|
-
this.connectPollInterval = setInterval(
|
|
636
|
+
sendConnect();
|
|
637
|
+
this.connectPollInterval = setInterval(sendConnect, CONNECT_POLL_MS);
|
|
613
638
|
});
|
|
614
639
|
return this.connectPromise;
|
|
615
640
|
}
|
|
@@ -636,6 +661,56 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
636
661
|
this.subrouteCallback = null;
|
|
637
662
|
};
|
|
638
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Subscribes to post-connect context changes (tenant/subsidiary switch, period
|
|
666
|
+
* close, …). The callback fires for each {@link ContextPayload} the host pushes
|
|
667
|
+
* *after* connect — not for the initial context, which {@link connect} already
|
|
668
|
+
* returns. Returns an unsubscribe function. Single subscriber: a second call
|
|
669
|
+
* replaces the first. Wire it **before {@link connect}** (the bridge warns at
|
|
670
|
+
* connect if it isn't).
|
|
671
|
+
*/
|
|
672
|
+
onContextChange(callback) {
|
|
673
|
+
this.contextChangeCallback = callback;
|
|
674
|
+
return () => {
|
|
675
|
+
this.contextChangeCallback = null;
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Subscribes to host auth changes: a Nominal logout (`authenticated: false` —
|
|
680
|
+
* sign your backend out) or an identity/tenant switch (`authenticated: true`
|
|
681
|
+
* with a new `userId`/`tenant` — re-authenticate). Fires for each change the
|
|
682
|
+
* host pushes after connect. Returns an unsubscribe function. Single
|
|
683
|
+
* subscriber: a second call replaces the first.
|
|
684
|
+
*
|
|
685
|
+
* **Wire this before {@link connect}** — without it the embedded app won't
|
|
686
|
+
* sign out when the user logs out of Nominal (the bridge warns at connect if
|
|
687
|
+
* it isn't wired).
|
|
688
|
+
*/
|
|
689
|
+
onAuthChange(callback) {
|
|
690
|
+
this.authChangeCallback = callback;
|
|
691
|
+
return () => {
|
|
692
|
+
this.authChangeCallback = null;
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* At connect, nudge the app if it hasn't wired the change listeners: every
|
|
697
|
+
* embedded app should react to a Nominal logout ({@link onAuthChange}) and a
|
|
698
|
+
* tenant/subsidiary switch ({@link onContextChange}). Fires once during the
|
|
699
|
+
* connect flow (not speculatively later), so wire both **before** `connect()`.
|
|
700
|
+
* The skill/codegen does this by default; this catches hand-wired apps.
|
|
701
|
+
*/
|
|
702
|
+
warnMissingListeners() {
|
|
703
|
+
if (!this.authChangeCallback) {
|
|
704
|
+
console.warn(
|
|
705
|
+
"[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()."
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
if (!this.contextChangeCallback) {
|
|
709
|
+
console.warn(
|
|
710
|
+
"[vibe-bridge] connected without onContextChange \u2014 the app will not react to a tenant/subsidiary/period switch. Call bridge.onContextChange() before connect()."
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
639
714
|
// ---------------------------------------------------------------------------
|
|
640
715
|
// Host navigation & UI commands
|
|
641
716
|
//
|
|
@@ -686,9 +761,37 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
686
761
|
invalidateCache(payload) {
|
|
687
762
|
return this.request("INVALIDATE_CACHE", payload);
|
|
688
763
|
}
|
|
764
|
+
/**
|
|
765
|
+
* Captures a custom product event from inside the Vibe App, attributed to the
|
|
766
|
+
* same PostHog person as the host (via the `distinctId` the host supplied at
|
|
767
|
+
* connect).
|
|
768
|
+
*
|
|
769
|
+
* **No-op until {@link connect} has resolved with PostHog enabled by the
|
|
770
|
+
* host** — if the host didn't send a `posthog` block (older host, or recording
|
|
771
|
+
* disabled for this env/app), nothing is sent. App authors don't import
|
|
772
|
+
* `posthog-js` themselves; this is the supported event surface.
|
|
773
|
+
*
|
|
774
|
+
* The event is sent **directly** from the iframe's own PostHog instance (only
|
|
775
|
+
* the session *recording* is forwarded to the parent), so it carries the
|
|
776
|
+
* iframe instance's `$session_id` rather than the host's — events tie to the
|
|
777
|
+
* same person, but cross-boundary event↔replay timeline correlation is a known
|
|
778
|
+
* limitation.
|
|
779
|
+
*
|
|
780
|
+
* @param event Event name. Namespacing app events (e.g. `'vibe:<slug>:<action>'`)
|
|
781
|
+
* keeps them filterable from the host's own events.
|
|
782
|
+
* @param properties Optional event properties.
|
|
783
|
+
* @example
|
|
784
|
+
* ```ts
|
|
785
|
+
* bridge.track('vibe:fixed-assets:report_exported', { format: 'csv', rows: 128 })
|
|
786
|
+
* ```
|
|
787
|
+
*/
|
|
788
|
+
track(event, properties) {
|
|
789
|
+
if (!this.posthogInitialized) return;
|
|
790
|
+
posthog.capture(event, properties);
|
|
791
|
+
}
|
|
689
792
|
destroy() {
|
|
690
793
|
this.teardownNavigationTracking();
|
|
691
|
-
this.
|
|
794
|
+
this.rejectConnect?.(new BridgeError("DESTROYED", "Bridge destroyed"));
|
|
692
795
|
this.stopConnectPolling();
|
|
693
796
|
window.removeEventListener("message", this.boundHandleMessage);
|
|
694
797
|
for (const pending of this.pendingRequests.values()) {
|
|
@@ -696,9 +799,12 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
696
799
|
}
|
|
697
800
|
this.pendingRequests.clear();
|
|
698
801
|
this.context = null;
|
|
802
|
+
this.posthogInitialized = false;
|
|
699
803
|
this.connectPromise = null;
|
|
700
804
|
this.onContextReceived = null;
|
|
701
|
-
this.
|
|
805
|
+
this.rejectConnect = null;
|
|
806
|
+
this.contextChangeCallback = null;
|
|
807
|
+
this.authChangeCallback = null;
|
|
702
808
|
this.subrouteCallback = null;
|
|
703
809
|
this.lastReportedSubroute = null;
|
|
704
810
|
}
|
|
@@ -711,7 +817,59 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
711
817
|
clearTimeout(this.connectTimeout);
|
|
712
818
|
this.connectTimeout = null;
|
|
713
819
|
}
|
|
714
|
-
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Initializes PostHog session recording + analytics from the host-supplied
|
|
823
|
+
* config carried by a one-time `POSTHOG_PUSH`, **only when the host opted in**
|
|
824
|
+
* (`config.enabled`).
|
|
825
|
+
*
|
|
826
|
+
* The host is the single source of truth: when it sends no `POSTHOG_PUSH`
|
|
827
|
+
* (recording disabled for this env/app) or `enabled: false`, this does
|
|
828
|
+
* nothing — `enabled` is the kill-switch even though `posthog-js` is bundled
|
|
829
|
+
* into the bridge. Cross-origin replay needs `recordCrossOriginIframes` on both
|
|
830
|
+
* sides so the parent receives the iframe's rrweb frames into one stitched
|
|
831
|
+
* recording, and `identify()` ties the iframe's recording + events to the same
|
|
832
|
+
* person as the host. Idempotent (the `posthogInitialized` guard) and wrapped
|
|
833
|
+
* in try/catch so a duplicate push or a PostHog failure is harmless.
|
|
834
|
+
*/
|
|
835
|
+
initPostHog(config) {
|
|
836
|
+
if (!config.enabled) return;
|
|
837
|
+
if (this.posthogInitialized) return;
|
|
838
|
+
if (typeof window === "undefined") return;
|
|
839
|
+
if (!config.token || !config.apiHost || !config.distinctId) {
|
|
840
|
+
console.warn(
|
|
841
|
+
"[vibe-bridge] PostHog enabled but token/apiHost/distinctId missing \u2014 skipping init."
|
|
842
|
+
);
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
posthog.init(config.token, {
|
|
847
|
+
api_host: config.apiHost,
|
|
848
|
+
// absolute ingestion host, NOT a relative /ingest proxy
|
|
849
|
+
person_profiles: "identified_only",
|
|
850
|
+
capture_pageview: false,
|
|
851
|
+
// the host owns pageviews; don't double-count
|
|
852
|
+
autocapture: false,
|
|
853
|
+
// opt-in events only, via track()
|
|
854
|
+
// Seed the host's session id so track() events share the host's
|
|
855
|
+
// $session_id and line up with the stitched replay. Replay stitching
|
|
856
|
+
// itself does not depend on this (recordCrossOriginIframes handles it).
|
|
857
|
+
...config.sessionId ? { bootstrap: { sessionID: config.sessionId } } : {},
|
|
858
|
+
session_recording: {
|
|
859
|
+
recordCrossOriginIframes: true,
|
|
860
|
+
// REQUIRED so the parent receives our rrweb frames
|
|
861
|
+
// Mirror nom-ui's posture (PostHogProvider.tsx): unmask inputs for
|
|
862
|
+
// richer insight, but keep password fields masked.
|
|
863
|
+
maskAllInputs: false,
|
|
864
|
+
maskInputOptions: { password: true }
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
posthog.identify(config.distinctId);
|
|
868
|
+
this.posthogInitialized = true;
|
|
869
|
+
console.log("[vibe-bridge] PostHog session recording initialized");
|
|
870
|
+
} catch (err) {
|
|
871
|
+
console.warn("[vibe-bridge] PostHog init failed \u2014 continuing without recording.", err);
|
|
872
|
+
}
|
|
715
873
|
}
|
|
716
874
|
/**
|
|
717
875
|
* Monkey-patches `history.pushState` and `history.replaceState` to
|
|
@@ -834,15 +992,6 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
834
992
|
this.kindHandlers[event.data.kind]?.(event.data);
|
|
835
993
|
}
|
|
836
994
|
handleResponse(msg) {
|
|
837
|
-
if (msg.type === "GET_CONTEXT") {
|
|
838
|
-
if (!this.connectPollIds.has(msg.requestId)) return;
|
|
839
|
-
if (msg.ok) {
|
|
840
|
-
this.onContextReceived?.(msg.data);
|
|
841
|
-
} else {
|
|
842
|
-
this.onContextError?.(new BridgeError(msg.code ?? "CONTEXT_ERROR", msg.error));
|
|
843
|
-
}
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
995
|
const pending = this.pendingRequests.get(msg.requestId);
|
|
847
996
|
if (!pending) return;
|
|
848
997
|
this.pendingRequests.delete(msg.requestId);
|
|
@@ -20,10 +20,20 @@ interface ContextPayload {
|
|
|
20
20
|
user: { id: string; displayName: string }
|
|
21
21
|
subroute?: string // initial deep-link path, if any
|
|
22
22
|
lastClosedPeriodSlug?: string // e.g. "mar-2026"
|
|
23
|
+
posthog?: {
|
|
24
|
+
// present only when the host enables in-iframe recording
|
|
25
|
+
token: string
|
|
26
|
+
apiHost: string // absolute ingestion host, e.g. "https://us.i.posthog.com"
|
|
27
|
+
distinctId: string // host-resolved person id (so events tie to the same person)
|
|
28
|
+
sessionId?: string // host's posthog.get_session_id(); seeds bootstrap so track() events share the host session
|
|
29
|
+
enabled: boolean // master switch; false/absent → bridge initializes nothing
|
|
30
|
+
}
|
|
23
31
|
hostVersion: string // version of @nominalso/vibe-host running in the host
|
|
24
32
|
}
|
|
25
33
|
```
|
|
26
34
|
|
|
35
|
+
When the resolved context carries `posthog.enabled === true`, the bridge auto-initializes `posthog-js` for cross-origin session recording (stitched into the host's recording) and exposes `bridge.track(event, properties?)` for custom events — see [Session recording & custom events](../README.md#session-recording--custom-events). If the host omits the `posthog` block, recording stays off.
|
|
36
|
+
|
|
27
37
|
## How it works
|
|
28
38
|
|
|
29
39
|
The host pushes context to the iframe on load, but the iframe's listener may not be ready when that push arrives. `connect()` solves the race:
|
package/docs/getting-started.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
npm install @nominalso/vibe-bridge
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Bundles `posthog-js` (its only runtime dependency) for host-enabled in-iframe session recording. Ships ESM + CJS with self-contained TypeScript types.
|
|
12
12
|
|
|
13
13
|
## Minimal app
|
|
14
14
|
|
package/llms.txt
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @nominalso/vibe-bridge
|
|
2
2
|
|
|
3
|
-
> Iframe-side TypeScript SDK for building Nominal Vibe Apps — standalone browser apps (often built with Lovable) embedded in the Nominal platform via a cross-origin iframe. Connects the app to its Nominal host over a typed postMessage protocol: read Nominal data, submit Close-Management task outputs, upload files,
|
|
3
|
+
> Iframe-side TypeScript SDK for building Nominal Vibe Apps — standalone browser apps (often built with Lovable) embedded in the Nominal platform via a cross-origin iframe. Connects the app to its Nominal host over a typed postMessage protocol: read Nominal data, submit Close-Management task outputs, upload files, keep deep-link routing in sync, and (when the host enables it) record the in-iframe session to PostHog. Browser-only; bundles posthog-js as its only runtime dependency.
|
|
4
4
|
|
|
5
|
-
The integration is always the same: `new VibeAppBridge()` (no config — `parentOrigin` is optional and the bridge auto-detects the embedding Nominal host; pass it only to pin explicitly or to allow preview origins), `await bridge.connect()` once (it returns the tenant/user `ContextPayload`), then call any of the ~46 typed data methods, `upload(file, options)`, or `postTaskOutput(...)`. Tear down with `bridge.destroy()`. The only write path into Nominal is `postTaskOutput`.
|
|
5
|
+
The integration is always the same: `new VibeAppBridge()` (no config — `parentOrigin` is optional and the bridge auto-detects the embedding Nominal host; pass it only to pin explicitly or to allow preview origins), `await bridge.connect()` once (it returns the tenant/user `ContextPayload`), then call any of the ~46 typed data methods, `upload(file, options)`, or `postTaskOutput(...)`. Tear down with `bridge.destroy()`. The only write path into Nominal is `postTaskOutput`. Session recording auto-initializes on `connect()` only when the host sends a `posthog` block with `enabled: true`; emit custom events with `bridge.track(event, properties?)` (a no-op until then).
|
|
6
6
|
|
|
7
7
|
## Docs
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nominalso/vibe-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Iframe-side SDK for building Nominal Vibe Apps — connects an embedded app to its Nominal host over a typed postMessage bridge (context, data fetch, file upload, subroute deep-linking).",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://github.com/nominalso/vibe-apps-sdk#readme",
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
"registry": "https://registry.npmjs.org/",
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"posthog-js": "^1.393.5"
|
|
38
|
+
},
|
|
36
39
|
"devDependencies": {
|
|
37
40
|
"@nominalso/vibe-api-types": "0.0.1",
|
|
38
41
|
"@nominalso/vibe-protocol-types": "0.0.1"
|