@nominalso/vibe-bridge 0.2.0 → 0.4.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 +11 -10
- package/README.md +63 -21
- package/dist/index.cjs +234 -10
- package/dist/index.d.cts +141 -10
- package/dist/index.d.ts +141 -10
- package/dist/index.js +224 -10
- package/docs/connect-lifecycle.md +11 -1
- package/docs/getting-started.md +13 -11
- package/llms.txt +3 -3
- 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.4.0";
|
|
3
3
|
|
|
4
4
|
// ../protocol-types/dist/index.js
|
|
5
5
|
function normalizeSubroute(subroute) {
|
|
@@ -44,6 +44,36 @@ var HttpBridgeError = class extends BridgeError {
|
|
|
44
44
|
this.name = "HttpBridgeError";
|
|
45
45
|
}
|
|
46
46
|
};
|
|
47
|
+
function isOriginPattern(entry) {
|
|
48
|
+
return entry.includes("*");
|
|
49
|
+
}
|
|
50
|
+
var patternCache = /* @__PURE__ */ new Map();
|
|
51
|
+
function patternToRegExp(pattern) {
|
|
52
|
+
const cached = patternCache.get(pattern);
|
|
53
|
+
if (cached) return cached;
|
|
54
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
55
|
+
const body = escaped.replace(/\\\*/g, "[^./:@?#\\s]+");
|
|
56
|
+
const compiled = new RegExp(`^${body}$`);
|
|
57
|
+
patternCache.set(pattern, compiled);
|
|
58
|
+
return compiled;
|
|
59
|
+
}
|
|
60
|
+
function matchesOrigin(origin, entry) {
|
|
61
|
+
if (!isOriginPattern(entry)) return origin === entry;
|
|
62
|
+
return patternToRegExp(entry).test(origin);
|
|
63
|
+
}
|
|
64
|
+
function isOriginAllowed(origin, allowlist, { allowPatterns }) {
|
|
65
|
+
for (const entry of allowlist) {
|
|
66
|
+
if (isOriginPattern(entry)) {
|
|
67
|
+
if (allowPatterns && matchesOrigin(origin, entry)) return true;
|
|
68
|
+
} else if (origin === entry) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/VibeAppBridge.ts
|
|
76
|
+
import posthogDefault from "posthog-js";
|
|
47
77
|
|
|
48
78
|
// src/data-methods.ts
|
|
49
79
|
var BridgeDataMethods = class {
|
|
@@ -362,13 +392,86 @@ function resolveRequestTimeout(type, requestTimeout) {
|
|
|
362
392
|
}
|
|
363
393
|
|
|
364
394
|
// src/VibeAppBridge.ts
|
|
395
|
+
function resolvePosthog(mod) {
|
|
396
|
+
let candidate = mod;
|
|
397
|
+
while (candidate && typeof candidate.init !== "function" && candidate.default) {
|
|
398
|
+
candidate = candidate.default;
|
|
399
|
+
}
|
|
400
|
+
return candidate;
|
|
401
|
+
}
|
|
402
|
+
var posthog = resolvePosthog(posthogDefault);
|
|
403
|
+
var PARENT_ORIGIN_OVERRIDE_KEY = "__NOM_VIBE_PARENT_ORIGIN__";
|
|
404
|
+
var PREVIEW_HOST_RE = /(?:^|\.)(?:vercel\.app|lovable\.app|lovable\.dev|lovableproject\.com)$|^(?:localhost|127\.0\.0\.1)$/;
|
|
405
|
+
function isPreviewSelfHost(hostname) {
|
|
406
|
+
return PREVIEW_HOST_RE.test(hostname);
|
|
407
|
+
}
|
|
408
|
+
function resolveActualParentOrigin() {
|
|
409
|
+
if (typeof window === "undefined") return null;
|
|
410
|
+
try {
|
|
411
|
+
const ancestors = window.location.ancestorOrigins;
|
|
412
|
+
if (ancestors && ancestors.length > 0 && ancestors[0]) return ancestors[0];
|
|
413
|
+
} catch {
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
if (document.referrer) return new URL(document.referrer).origin;
|
|
417
|
+
} catch {
|
|
418
|
+
}
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
function resolveOriginPolicy(configured) {
|
|
422
|
+
if (typeof window !== "undefined") {
|
|
423
|
+
const override = window[PARENT_ORIGIN_OVERRIDE_KEY];
|
|
424
|
+
if (typeof override === "string" && override.length > 0) {
|
|
425
|
+
console.log(`[vibe-bridge] Using dev-bridge parentOrigin override: ${override}`);
|
|
426
|
+
return { targetOrigin: override, isTrusted: (o) => o === override };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const rawList = configured === void 0 ? [] : Array.isArray(configured) ? configured : [configured];
|
|
430
|
+
const allowlist = rawList.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
431
|
+
if (allowlist.length === 0) {
|
|
432
|
+
const actual2 = resolveActualParentOrigin();
|
|
433
|
+
if (actual2) return { targetOrigin: actual2, isTrusted: (o) => o === actual2 };
|
|
434
|
+
console.warn(
|
|
435
|
+
"[vibe-bridge] No `parentOrigin` set and the embedding host could not be auto-detected (no ancestorOrigins/referrer). Pass `parentOrigin` explicitly."
|
|
436
|
+
);
|
|
437
|
+
return { targetOrigin: null, isTrusted: () => false };
|
|
438
|
+
}
|
|
439
|
+
const exactEntries = allowlist.filter((entry) => !isOriginPattern(entry));
|
|
440
|
+
const hasPatterns = exactEntries.length !== allowlist.length;
|
|
441
|
+
if (allowlist.length === 1 && !hasPatterns) {
|
|
442
|
+
const only = allowlist[0];
|
|
443
|
+
return { targetOrigin: only, isTrusted: (o) => o === only };
|
|
444
|
+
}
|
|
445
|
+
const allowPatterns = hasPatterns && typeof window !== "undefined" && isPreviewSelfHost(window.location.hostname);
|
|
446
|
+
const isTrusted = (o) => isOriginAllowed(o, allowlist, { allowPatterns });
|
|
447
|
+
const actual = resolveActualParentOrigin();
|
|
448
|
+
let targetOrigin = null;
|
|
449
|
+
if (actual && isTrusted(actual)) {
|
|
450
|
+
targetOrigin = actual;
|
|
451
|
+
} else if (actual === null && !allowPatterns && exactEntries.length === 1) {
|
|
452
|
+
targetOrigin = exactEntries[0];
|
|
453
|
+
}
|
|
454
|
+
if (targetOrigin === null) {
|
|
455
|
+
console.warn(
|
|
456
|
+
"[vibe-bridge] Could not resolve a parent origin to post to. Check `parentOrigin` against the embedding host" + (hasPatterns && !allowPatterns ? " \u2014 pattern entries are ignored because this page is not served from a recognised preview host." : ".")
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
return { targetOrigin, isTrusted };
|
|
460
|
+
}
|
|
365
461
|
var VibeAppBridge = class extends BridgeDataMethods {
|
|
366
|
-
constructor({ parentOrigin, requestTimeout }) {
|
|
462
|
+
constructor({ parentOrigin, requestTimeout } = {}) {
|
|
367
463
|
super();
|
|
368
464
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
369
465
|
this.context = null;
|
|
466
|
+
/**
|
|
467
|
+
* Set once {@link initPostHog} has successfully called `posthog.init()`.
|
|
468
|
+
* Guards against a double-init and makes {@link track} a no-op until
|
|
469
|
+
* recording is actually live.
|
|
470
|
+
*/
|
|
471
|
+
this.posthogInitialized = false;
|
|
370
472
|
this.connectPromise = null;
|
|
371
473
|
this.connectPollInterval = null;
|
|
474
|
+
this.connectTimeout = null;
|
|
372
475
|
this.onContextReceived = null;
|
|
373
476
|
this.onContextError = null;
|
|
374
477
|
/**
|
|
@@ -423,7 +526,9 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
423
526
|
this.sendCommand("SWITCH_SUBSIDIARY", { subsidiaryId });
|
|
424
527
|
}
|
|
425
528
|
};
|
|
426
|
-
|
|
529
|
+
const policy = resolveOriginPolicy(parentOrigin);
|
|
530
|
+
this.targetOrigin = policy.targetOrigin;
|
|
531
|
+
this.isTrustedOrigin = policy.isTrusted;
|
|
427
532
|
this.requestTimeout = requestTimeout;
|
|
428
533
|
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
429
534
|
window.addEventListener("message", this.boundHandleMessage);
|
|
@@ -459,6 +564,14 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
459
564
|
connect() {
|
|
460
565
|
if (this.context) return Promise.resolve(this.context);
|
|
461
566
|
if (this.connectPromise) return this.connectPromise;
|
|
567
|
+
if (this.targetOrigin === null) {
|
|
568
|
+
return Promise.reject(
|
|
569
|
+
new BridgeError(
|
|
570
|
+
"PARENT_ORIGIN_UNRESOLVED",
|
|
571
|
+
"Could not resolve a parent origin to connect to. Check `parentOrigin` against the embedding host."
|
|
572
|
+
)
|
|
573
|
+
);
|
|
574
|
+
}
|
|
462
575
|
this.connectPromise = new Promise((resolve, reject) => {
|
|
463
576
|
const timer = setTimeout(() => {
|
|
464
577
|
this.stopConnectPolling();
|
|
@@ -467,6 +580,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
467
580
|
this.onContextError = null;
|
|
468
581
|
reject(new BridgeError("TIMEOUT", "Bridge connect timed out"));
|
|
469
582
|
}, CONNECT_TIMEOUT_MS);
|
|
583
|
+
this.connectTimeout = timer;
|
|
470
584
|
this.onContextError = (error) => {
|
|
471
585
|
clearTimeout(timer);
|
|
472
586
|
this.stopConnectPolling();
|
|
@@ -485,6 +599,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
485
599
|
console.log(
|
|
486
600
|
`[vibe-bridge] Connected \u2014 bridge: ${version}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
487
601
|
);
|
|
602
|
+
this.initPostHog(ctx);
|
|
488
603
|
const initialSubroute = ctx.subroute ? normalizeSubroute(ctx.subroute) : null;
|
|
489
604
|
if (initialSubroute && initialSubroute !== "/") {
|
|
490
605
|
this.withSubrouteSuppressed(() => {
|
|
@@ -509,7 +624,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
509
624
|
requestId,
|
|
510
625
|
payload: { bridgeVersion: version }
|
|
511
626
|
};
|
|
512
|
-
|
|
627
|
+
this.postToParent(message);
|
|
513
628
|
};
|
|
514
629
|
poll();
|
|
515
630
|
this.connectPollInterval = setInterval(poll, CONNECT_POLL_MS);
|
|
@@ -589,8 +704,37 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
589
704
|
invalidateCache(payload) {
|
|
590
705
|
return this.request("INVALIDATE_CACHE", payload);
|
|
591
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Captures a custom product event from inside the Vibe App, attributed to the
|
|
709
|
+
* same PostHog person as the host (via the `distinctId` the host supplied at
|
|
710
|
+
* connect).
|
|
711
|
+
*
|
|
712
|
+
* **No-op until {@link connect} has resolved with PostHog enabled by the
|
|
713
|
+
* host** — if the host didn't send a `posthog` block (older host, or recording
|
|
714
|
+
* disabled for this env/app), nothing is sent. App authors don't import
|
|
715
|
+
* `posthog-js` themselves; this is the supported event surface.
|
|
716
|
+
*
|
|
717
|
+
* The event is sent **directly** from the iframe's own PostHog instance (only
|
|
718
|
+
* the session *recording* is forwarded to the parent), so it carries the
|
|
719
|
+
* iframe instance's `$session_id` rather than the host's — events tie to the
|
|
720
|
+
* same person, but cross-boundary event↔replay timeline correlation is a known
|
|
721
|
+
* limitation.
|
|
722
|
+
*
|
|
723
|
+
* @param event Event name. Namespacing app events (e.g. `'vibe:<slug>:<action>'`)
|
|
724
|
+
* keeps them filterable from the host's own events.
|
|
725
|
+
* @param properties Optional event properties.
|
|
726
|
+
* @example
|
|
727
|
+
* ```ts
|
|
728
|
+
* bridge.track('vibe:fixed-assets:report_exported', { format: 'csv', rows: 128 })
|
|
729
|
+
* ```
|
|
730
|
+
*/
|
|
731
|
+
track(event, properties) {
|
|
732
|
+
if (!this.posthogInitialized) return;
|
|
733
|
+
posthog.capture(event, properties);
|
|
734
|
+
}
|
|
592
735
|
destroy() {
|
|
593
736
|
this.teardownNavigationTracking();
|
|
737
|
+
this.onContextError?.(new BridgeError("DESTROYED", "Bridge destroyed"));
|
|
594
738
|
this.stopConnectPolling();
|
|
595
739
|
window.removeEventListener("message", this.boundHandleMessage);
|
|
596
740
|
for (const pending of this.pendingRequests.values()) {
|
|
@@ -598,6 +742,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
598
742
|
}
|
|
599
743
|
this.pendingRequests.clear();
|
|
600
744
|
this.context = null;
|
|
745
|
+
this.posthogInitialized = false;
|
|
601
746
|
this.connectPromise = null;
|
|
602
747
|
this.onContextReceived = null;
|
|
603
748
|
this.onContextError = null;
|
|
@@ -609,8 +754,65 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
609
754
|
clearInterval(this.connectPollInterval);
|
|
610
755
|
this.connectPollInterval = null;
|
|
611
756
|
}
|
|
757
|
+
if (this.connectTimeout !== null) {
|
|
758
|
+
clearTimeout(this.connectTimeout);
|
|
759
|
+
this.connectTimeout = null;
|
|
760
|
+
}
|
|
612
761
|
this.connectPollIds.clear();
|
|
613
762
|
}
|
|
763
|
+
/**
|
|
764
|
+
* Initializes PostHog session recording + analytics from the host-supplied
|
|
765
|
+
* config, **only when the host opted in** (`ctx.posthog?.enabled`).
|
|
766
|
+
*
|
|
767
|
+
* The host is the single source of truth: when it sends no `posthog` block
|
|
768
|
+
* (older nom-ui, or recording disabled for this env/app) or `enabled: false`,
|
|
769
|
+
* this does nothing — `enabled` is the kill-switch even though `posthog-js` is
|
|
770
|
+
* bundled into the bridge. Cross-origin replay needs `recordCrossOriginIframes`
|
|
771
|
+
* on both sides so the parent receives the iframe's rrweb frames into one
|
|
772
|
+
* stitched recording, and `identify()` ties the iframe's recording + events to
|
|
773
|
+
* the same person as the host. Wrapped in try/catch so a PostHog failure is
|
|
774
|
+
* best-effort and never blocks `connect()`.
|
|
775
|
+
*/
|
|
776
|
+
initPostHog(ctx) {
|
|
777
|
+
const config = ctx.posthog;
|
|
778
|
+
if (!config?.enabled) return;
|
|
779
|
+
if (this.posthogInitialized) return;
|
|
780
|
+
if (typeof window === "undefined") return;
|
|
781
|
+
if (!config.token || !config.apiHost || !config.distinctId) {
|
|
782
|
+
console.warn(
|
|
783
|
+
"[vibe-bridge] PostHog enabled but token/apiHost/distinctId missing \u2014 skipping init."
|
|
784
|
+
);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
posthog.init(config.token, {
|
|
789
|
+
api_host: config.apiHost,
|
|
790
|
+
// absolute ingestion host, NOT a relative /ingest proxy
|
|
791
|
+
person_profiles: "identified_only",
|
|
792
|
+
capture_pageview: false,
|
|
793
|
+
// the host owns pageviews; don't double-count
|
|
794
|
+
autocapture: false,
|
|
795
|
+
// opt-in events only, via track()
|
|
796
|
+
// Seed the host's session id so track() events share the host's
|
|
797
|
+
// $session_id and line up with the stitched replay. Replay stitching
|
|
798
|
+
// itself does not depend on this (recordCrossOriginIframes handles it).
|
|
799
|
+
...config.sessionId ? { bootstrap: { sessionID: config.sessionId } } : {},
|
|
800
|
+
session_recording: {
|
|
801
|
+
recordCrossOriginIframes: true,
|
|
802
|
+
// REQUIRED so the parent receives our rrweb frames
|
|
803
|
+
// Mirror nom-ui's posture (PostHogProvider.tsx): unmask inputs for
|
|
804
|
+
// richer insight, but keep password fields masked.
|
|
805
|
+
maskAllInputs: false,
|
|
806
|
+
maskInputOptions: { password: true }
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
posthog.identify(config.distinctId);
|
|
810
|
+
this.posthogInitialized = true;
|
|
811
|
+
console.log("[vibe-bridge] PostHog session recording initialized");
|
|
812
|
+
} catch (err) {
|
|
813
|
+
console.warn("[vibe-bridge] PostHog init failed \u2014 continuing without recording.", err);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
614
816
|
/**
|
|
615
817
|
* Monkey-patches `history.pushState` and `history.replaceState` to
|
|
616
818
|
* auto-detect SPA navigation and report it to the host. Called after
|
|
@@ -665,10 +867,13 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
665
867
|
this.sendCommand("REPORT_SUBROUTE", { subroute, replace });
|
|
666
868
|
}
|
|
667
869
|
sendCommand(type, payload) {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
870
|
+
this.postToParent({
|
|
871
|
+
__protocol: PROTOCOL_ID,
|
|
872
|
+
__version: PROTOCOL_VERSION,
|
|
873
|
+
kind: "command",
|
|
874
|
+
type,
|
|
875
|
+
payload
|
|
876
|
+
});
|
|
672
877
|
}
|
|
673
878
|
/**
|
|
674
879
|
* Low-level typed request for **any** operation in {@link RequestRegistry}.
|
|
@@ -711,11 +916,20 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
711
916
|
requestId,
|
|
712
917
|
payload
|
|
713
918
|
};
|
|
714
|
-
|
|
919
|
+
this.postToParent(message);
|
|
715
920
|
});
|
|
716
921
|
}
|
|
922
|
+
/**
|
|
923
|
+
* Posts to the embedding host. No-op when no concrete target origin could be
|
|
924
|
+
* resolved (a config/embedding error) — `connect()` rejects fast in that case
|
|
925
|
+
* so calls never silently hang waiting on a timeout.
|
|
926
|
+
*/
|
|
927
|
+
postToParent(message) {
|
|
928
|
+
if (this.targetOrigin === null) return;
|
|
929
|
+
window.parent.postMessage(message, this.targetOrigin);
|
|
930
|
+
}
|
|
717
931
|
handleMessage(event) {
|
|
718
|
-
if (event.origin
|
|
932
|
+
if (!this.isTrustedOrigin(event.origin)) return;
|
|
719
933
|
if (!isBridgeMessage(event.data)) return;
|
|
720
934
|
this.kindHandlers[event.data.kind]?.(event.data);
|
|
721
935
|
}
|
|
@@ -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:
|
|
@@ -34,7 +44,7 @@ The host pushes context to the iframe on load, but the iframe's listener may not
|
|
|
34
44
|
4. The bridge begins auto-tracking SPA navigation (see below).
|
|
35
45
|
5. If nothing arrives within **10 seconds**, `connect()` rejects with a `BridgeError` (code `'TIMEOUT'`, message `Bridge connect timed out`).
|
|
36
46
|
|
|
37
|
-
A timeout almost always means a `parentOrigin` mismatch or the host never mounted.
|
|
47
|
+
A timeout almost always means a `parentOrigin` mismatch or the host never mounted. A separate, immediate rejection — `BridgeError` code `PARENT_ORIGIN_UNRESOLVED` — means no concrete parent origin could be resolved at all (e.g. a pattern-only `parentOrigin` on a non-preview host); fix `parentOrigin` rather than waiting on the timeout.
|
|
38
48
|
|
|
39
49
|
## Initial deep link
|
|
40
50
|
|
package/docs/getting-started.md
CHANGED
|
@@ -8,16 +8,14 @@
|
|
|
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
|
|
|
15
15
|
```ts
|
|
16
16
|
import { VibeAppBridge, type ContextPayload } from '@nominalso/vibe-bridge'
|
|
17
17
|
|
|
18
|
-
const bridge = new VibeAppBridge(
|
|
19
|
-
parentOrigin: import.meta.env.VITE_PARENT_ORIGIN,
|
|
20
|
-
})
|
|
18
|
+
const bridge = new VibeAppBridge() // auto-detects the embedding Nominal host
|
|
21
19
|
|
|
22
20
|
async function main() {
|
|
23
21
|
const ctx: ContextPayload = await bridge.connect()
|
|
@@ -32,20 +30,24 @@ async function main() {
|
|
|
32
30
|
main().catch(console.error)
|
|
33
31
|
```
|
|
34
32
|
|
|
35
|
-
## `parentOrigin`
|
|
33
|
+
## `parentOrigin` (optional)
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
By default you pass **nothing** — the bridge auto-detects the Nominal app that embeds your iframe and talks only to it. This is safe because Nominal serves Vibe Apps behind a `frame-ancestors` CSP, so only nom-ui can frame the app. Most apps need nothing here.
|
|
38
36
|
|
|
39
|
-
Drive it from an
|
|
37
|
+
Pass `parentOrigin` to **pin the host origin** explicitly (defense in depth, or when running outside Nominal's edge). It's an **origin** — scheme + host + port, no path, no trailing slash. Drive it from an env var, or use a list / **glob patterns** for dynamic preview deployments:
|
|
40
38
|
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
```ts
|
|
40
|
+
new VibeAppBridge({ parentOrigin: import.meta.env.VITE_PARENT_ORIGIN })
|
|
41
|
+
new VibeAppBridge({ parentOrigin: ['https://app.nominal.so', 'https://*.vercel.app'] })
|
|
42
|
+
```
|
|
44
43
|
|
|
45
|
-
|
|
44
|
+
```sh
|
|
45
|
+
# .env.local — when pinning explicitly while testing inside nom-ui locally
|
|
46
46
|
VITE_PARENT_ORIGIN=http://localhost:3000
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
A pattern's `*` matches exactly one DNS label or port (anchored, scheme literal). Patterns are honoured **only when the app's own page is served from a recognised preview host** (`*.vercel.app`, `*.lovable.app`, `localhost`, …); on a production custom domain they are ignored and only exact origins match. Keep at least one exact origin in the list.
|
|
50
|
+
|
|
49
51
|
## Next
|
|
50
52
|
|
|
51
53
|
- [`connect-lifecycle.md`](./connect-lifecycle.md) — how `connect()` works and the connection lifecycle.
|
package/llms.txt
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
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:
|
|
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
|
|
|
9
9
|
- [README](./README.md): install, quickstart, full API, common mistakes, and the operation catalog.
|
|
10
|
-
- [Getting started](./docs/getting-started.md): install + minimal app + `parentOrigin`
|
|
10
|
+
- [Getting started](./docs/getting-started.md): install + minimal app + optional `parentOrigin` pinning.
|
|
11
11
|
- [Connection lifecycle](./docs/connect-lifecycle.md): `connect()` semantics, `ContextPayload`, timeout, subroute sync, teardown.
|
|
12
12
|
- [Fetching data](./docs/data-fetching.md): named methods vs the typed `request()` escape hatch, payload shapes, errors/timeouts, operation list.
|
|
13
13
|
- [File upload](./docs/file-upload.md): `upload(file, options)`, progress events, constraints.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nominalso/vibe-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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"
|