@nominalso/vibe-host 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/README.md +17 -33
- package/dist/index.cjs +121 -56
- package/dist/index.d.cts +135 -46
- package/dist/index.d.ts +135 -46
- package/dist/index.js +121 -56
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -29,9 +29,9 @@ const host = new VibeAppHost({
|
|
|
29
29
|
clientConfig: { baseUrl: '/api/proxy', stripApiPrefix: true },
|
|
30
30
|
})
|
|
31
31
|
|
|
32
|
-
// Start listening
|
|
32
|
+
// Start listening. The iframe identifies itself via its CONNECT handshake, and
|
|
33
|
+
// the host replies with its context — no iframe window needed up front.
|
|
33
34
|
const unmount = host.mount()
|
|
34
|
-
// once the iframe has loaded: host.pushContextTo(iframe.contentWindow)
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
## API
|
|
@@ -58,56 +58,40 @@ const trustedOrigins =
|
|
|
58
58
|
: ['https://app.nominal.so', 'https://*.vercel.app', 'https://*.lovable.app']
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
Because you cannot `postMessage` to a pattern, proactive
|
|
61
|
+
Because you cannot `postMessage` to a pattern, proactive pushes target the exact allowlist entries plus the connected iframe's learned origin; a pattern-matched iframe is reached via the concrete origin learned from its `CONNECT` handshake.
|
|
62
62
|
|
|
63
|
-
### `mount(
|
|
63
|
+
### `mount(): () => void`
|
|
64
64
|
|
|
65
|
-
Starts listening for bridge messages and sets up browser back/forward sync. Returns a cleanup function.
|
|
65
|
+
Starts listening for bridge messages and sets up browser back/forward sync. Returns a cleanup function. Takes no iframe window — the iframe identifies itself via its `CONNECT` handshake (and re-identifies on every reload), and the host replies with its context.
|
|
66
66
|
|
|
67
|
-
### `
|
|
67
|
+
### `refreshContext(): void`
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
Re-reads `getContext()` and pushes it to the connected iframe (e.g. after a tenant/subsidiary switch), so the app reacts via `bridge.onContextChange`. No-op until the iframe has connected.
|
|
70
|
+
|
|
71
|
+
### `notifyAuthChange(auth): void`
|
|
72
|
+
|
|
73
|
+
Pushes a Nominal auth change (logout, or identity/tenant switch) to the connected iframe, so the app reacts via `bridge.onAuthChange`. No-op until the iframe has connected.
|
|
70
74
|
|
|
71
75
|
### Exports
|
|
72
76
|
|
|
73
|
-
`VibeAppHost`, and the types `VibeAppHostOptions`, `HostContext`, `HostHandlers`, `RequestHandlers`, `ContextPayload`, `VibeApiClientOptions`.
|
|
77
|
+
`VibeAppHost`, and the types `VibeAppHostOptions`, `HostContext`, `HostHandlers`, `RequestHandlers`, `ContextPayload`, `AuthPayload`, `VibeApiClientOptions`.
|
|
74
78
|
|
|
75
79
|
## Mounting inside Next.js (nom-ui)
|
|
76
80
|
|
|
77
|
-
The iframe is server-rendered
|
|
81
|
+
Just `mount()` in an effect — no iframe-window handling, no `load`-event dance. The iframe is server-rendered and may load before React hydrates, but that's fine: its bridge re-sends `CONNECT` until the host (now listening) answers, so there's no race.
|
|
78
82
|
|
|
79
83
|
```tsx
|
|
80
84
|
'use client'
|
|
81
|
-
import { useEffect
|
|
85
|
+
import { useEffect } from 'react'
|
|
82
86
|
import { VibeAppHost } from '@nominalso/vibe-host'
|
|
83
87
|
|
|
84
88
|
function VibeAppFrame({ host, src }: { host: VibeAppHost; src: string }) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const iframe = iframeRef.current
|
|
89
|
-
if (!iframe) return
|
|
90
|
-
const unmount = host.mount() // listen before the iframe loads
|
|
91
|
-
const onLoad = () => iframe.contentWindow && host.pushContextTo(iframe.contentWindow)
|
|
92
|
-
iframe.addEventListener('load', onLoad)
|
|
93
|
-
return () => {
|
|
94
|
-
iframe.removeEventListener('load', onLoad)
|
|
95
|
-
unmount()
|
|
96
|
-
}
|
|
97
|
-
}, [host])
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<iframe
|
|
101
|
-
ref={iframeRef}
|
|
102
|
-
src={src}
|
|
103
|
-
sandbox="allow-scripts allow-forms allow-same-origin allow-popups"
|
|
104
|
-
/>
|
|
105
|
-
)
|
|
89
|
+
useEffect(() => host.mount(), [host]) // returns the unmount cleanup
|
|
90
|
+
|
|
91
|
+
return <iframe src={src} sandbox="allow-scripts allow-forms allow-same-origin allow-popups" />
|
|
106
92
|
}
|
|
107
93
|
```
|
|
108
94
|
|
|
109
|
-
Calling `contentWindow.postMessage` while the iframe is still at `about:blank` throws a `DOMException` — that's why context is pushed from the `load` event, not on effect setup.
|
|
110
|
-
|
|
111
95
|
## How it fits together
|
|
112
96
|
|
|
113
97
|
The iframe side is [`@nominalso/vibe-bridge`](https://www.npmjs.com/package/@nominalso/vibe-bridge). See the [repository](https://github.com/nominalso/vibe-apps-sdk#readme) for the full protocol and architecture.
|
package/dist/index.cjs
CHANGED
|
@@ -27,7 +27,7 @@ __export(index_exports, {
|
|
|
27
27
|
module.exports = __toCommonJS(index_exports);
|
|
28
28
|
|
|
29
29
|
// package.json
|
|
30
|
-
var version = "0.
|
|
30
|
+
var version = "0.5.0";
|
|
31
31
|
|
|
32
32
|
// ../protocol-types/dist/index.js
|
|
33
33
|
function normalizeSubroute(subroute) {
|
|
@@ -43,7 +43,7 @@ function normalizeSubroute(subroute) {
|
|
|
43
43
|
return normalized;
|
|
44
44
|
}
|
|
45
45
|
var PROTOCOL_ID = "nominal-vibe-bridge";
|
|
46
|
-
var PROTOCOL_VERSION =
|
|
46
|
+
var PROTOCOL_VERSION = 2;
|
|
47
47
|
var BRIDGE_KINDS = ["request", "response", "push", "command", "progress"];
|
|
48
48
|
var CORRELATED_KINDS = ["request", "response", "progress"];
|
|
49
49
|
var BRIDGE_KINDS_SET = new Set(BRIDGE_KINDS);
|
|
@@ -657,18 +657,20 @@ function rejectUnwired(op) {
|
|
|
657
657
|
var VibeAppHost = class {
|
|
658
658
|
constructor(opts) {
|
|
659
659
|
/**
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
660
|
+
* The connected iframe, as two facets of one thing: `connectedWindow` is its
|
|
661
|
+
* `Window` (identity + the single push target) and `connectedOrigin` is its
|
|
662
|
+
* origin (what you `postMessage` to — you cannot post to a `Window` ref, nor to
|
|
663
|
+
* a glob). Both are written together by {@link handleConnect} (the sole writer)
|
|
664
|
+
* and cleared together on unmount, so they can never diverge.
|
|
665
|
+
*
|
|
666
|
+
* There is deliberately no other window reference. The connected window can
|
|
667
|
+
* only be learned from each `CONNECT` handshake's `event.source`, never at
|
|
668
|
+
* mount: a reloaded/remounted iframe is a *new* `Window` with the same origin,
|
|
669
|
+
* so a mount-time window would be stale. The gate blocks every non-`CONNECT`
|
|
670
|
+
* message whose source isn't `connectedWindow`; every proactive push targets it.
|
|
663
671
|
*/
|
|
664
|
-
this.
|
|
665
|
-
this.
|
|
666
|
-
/**
|
|
667
|
-
* Source windows we've already logged a successful handshake for. The bridge
|
|
668
|
-
* polls `GET_CONTEXT` on an interval until it connects, so without this the
|
|
669
|
-
* "Vibe App connected" line repeats on every poll (SSOT §13 F3).
|
|
670
|
-
*/
|
|
671
|
-
this.handshakeLoggedWindows = /* @__PURE__ */ new WeakSet();
|
|
672
|
+
this.connectedWindow = null;
|
|
673
|
+
this.connectedOrigin = null;
|
|
672
674
|
this.kindHandlers = {
|
|
673
675
|
request: (msg, source, origin) => this.handleRequest(msg, source, origin),
|
|
674
676
|
command: (msg) => this.dispatchCommand(msg)
|
|
@@ -677,6 +679,7 @@ var VibeAppHost = class {
|
|
|
677
679
|
this.trustedOrigins = new Set(trustedOrigins);
|
|
678
680
|
this.exactOrigins = trustedOrigins.filter((entry) => !isOriginPattern(entry));
|
|
679
681
|
this.getContext = getContext;
|
|
682
|
+
this.getPostHog = opts.getPostHog;
|
|
680
683
|
this.appBasePath = appBasePath;
|
|
681
684
|
this.onRequestComplete = opts.onRequestComplete;
|
|
682
685
|
this.rateLimiter = createRateLimiter(opts.rateLimit ?? DEFAULT_RATE_LIMIT);
|
|
@@ -689,7 +692,8 @@ var VibeAppHost = class {
|
|
|
689
692
|
this.commandHandlers = this.buildCommandHandlers(opts);
|
|
690
693
|
}
|
|
691
694
|
dispatchCommand(msg) {
|
|
692
|
-
const
|
|
695
|
+
const handlers = this.commandHandlers;
|
|
696
|
+
const handler = handlers[msg.type];
|
|
693
697
|
if (!handler) {
|
|
694
698
|
console.warn(`[vibe-host] Received unknown command "${msg.type}" \u2014 ignoring.`);
|
|
695
699
|
return;
|
|
@@ -735,31 +739,41 @@ var VibeAppHost = class {
|
|
|
735
739
|
* Starts listening for bridge messages and sets up browser back/forward sync.
|
|
736
740
|
* Returns a cleanup function to call on unmount.
|
|
737
741
|
*
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
*
|
|
741
|
-
* loaded to trigger the proactive push.
|
|
742
|
+
* Takes no iframe window: the host learns the iframe from its `CONNECT`
|
|
743
|
+
* handshake (and re-learns it on every reload), then delivers context as the
|
|
744
|
+
* reply to that handshake. Just call `mount()` once the host exists.
|
|
742
745
|
*/
|
|
743
|
-
mount(
|
|
744
|
-
if (iframeWindow) this.iframeWindow = iframeWindow;
|
|
746
|
+
mount() {
|
|
745
747
|
window.addEventListener("message", this.boundHandleMessage);
|
|
746
748
|
window.addEventListener("popstate", this.boundHandlePopState);
|
|
747
|
-
if (iframeWindow) this.pushContext(iframeWindow);
|
|
748
749
|
return () => {
|
|
749
750
|
window.removeEventListener("message", this.boundHandleMessage);
|
|
750
751
|
window.removeEventListener("popstate", this.boundHandlePopState);
|
|
751
|
-
this.
|
|
752
|
-
this.
|
|
752
|
+
this.connectedWindow = null;
|
|
753
|
+
this.connectedOrigin = null;
|
|
753
754
|
};
|
|
754
755
|
}
|
|
755
756
|
/**
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
757
|
+
* Re-reads `getContext()` and re-pushes it to the connected iframe, so an open
|
|
758
|
+
* app reacts to a tenant/subsidiary switch or period close without reloading.
|
|
759
|
+
* PostHog is not re-sent (it rides only the connect handshake). No-op until an
|
|
760
|
+
* iframe is mounted.
|
|
761
|
+
*/
|
|
762
|
+
refreshContext() {
|
|
763
|
+
if (!this.connectedWindow) return;
|
|
764
|
+
this.pushContext(this.connectedWindow);
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Notifies the connected iframe of a Nominal auth change — a logout
|
|
768
|
+
* (`authenticated: false`) or an identity/tenant switch (`authenticated: true`
|
|
769
|
+
* with a new `userId`/`tenant`) — via `AUTH_CHANGED`, so the app re-authenticates
|
|
770
|
+
* or signs its own backend out. No-op until the iframe has connected. When auth
|
|
771
|
+
* and context change together, call this *before* {@link refreshContext} ("auth
|
|
772
|
+
* change, then context").
|
|
759
773
|
*/
|
|
760
|
-
|
|
761
|
-
this.
|
|
762
|
-
this.
|
|
774
|
+
notifyAuthChange(auth) {
|
|
775
|
+
if (!this.connectedWindow) return;
|
|
776
|
+
this.pushToIframe("AUTH_CHANGED", auth);
|
|
763
777
|
}
|
|
764
778
|
pushContext(target) {
|
|
765
779
|
const message = {
|
|
@@ -774,14 +788,14 @@ var VibeAppHost = class {
|
|
|
774
788
|
}
|
|
775
789
|
}
|
|
776
790
|
handlePopState() {
|
|
777
|
-
if (!this.
|
|
791
|
+
if (!this.connectedWindow) return;
|
|
778
792
|
const currentPath = window.location.pathname;
|
|
779
793
|
if (!currentPath.startsWith(this.appBasePath)) return;
|
|
780
794
|
const subroute = currentPath.slice(this.appBasePath.length) || "/";
|
|
781
795
|
this.pushToIframe("SUBROUTE_PUSH", { subroute });
|
|
782
796
|
}
|
|
783
797
|
pushToIframe(type, payload) {
|
|
784
|
-
if (!this.
|
|
798
|
+
if (!this.connectedWindow) return;
|
|
785
799
|
const message = {
|
|
786
800
|
__protocol: PROTOCOL_ID,
|
|
787
801
|
__version: PROTOCOL_VERSION,
|
|
@@ -790,44 +804,95 @@ var VibeAppHost = class {
|
|
|
790
804
|
payload
|
|
791
805
|
};
|
|
792
806
|
for (const origin of this.concreteTargets()) {
|
|
793
|
-
this.
|
|
807
|
+
this.connectedWindow.postMessage(message, origin);
|
|
794
808
|
}
|
|
795
809
|
}
|
|
810
|
+
/**
|
|
811
|
+
* Sends the host's PostHog config to a freshly connected iframe as a one-time
|
|
812
|
+
* POSTHOG_PUSH. No-op when no `getPostHog` source is configured or it returns
|
|
813
|
+
* nothing (recording disabled). Targets the exact handshaking window/origin
|
|
814
|
+
* (no `concreteTargets` fan-out) since it fires from a validated inbound poll.
|
|
815
|
+
*/
|
|
816
|
+
pushPostHog(target, origin) {
|
|
817
|
+
const payload = this.getPostHog?.();
|
|
818
|
+
if (!payload) return;
|
|
819
|
+
const message = {
|
|
820
|
+
__protocol: PROTOCOL_ID,
|
|
821
|
+
__version: PROTOCOL_VERSION,
|
|
822
|
+
kind: "push",
|
|
823
|
+
type: "POSTHOG_PUSH",
|
|
824
|
+
payload
|
|
825
|
+
};
|
|
826
|
+
target.postMessage(message, origin);
|
|
827
|
+
}
|
|
796
828
|
async handleMessage(event) {
|
|
797
829
|
if (!isOriginAllowed(event.origin, this.trustedOrigins, { allowPatterns: true })) return;
|
|
798
830
|
if (!isBridgeMessage(event.data)) return;
|
|
799
|
-
|
|
800
|
-
|
|
831
|
+
const msg = event.data;
|
|
832
|
+
const source = event.source;
|
|
833
|
+
if (msg.kind === "command" && msg.type === "CONNECT") {
|
|
834
|
+
this.handleConnect(source, event.origin);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (source !== this.connectedWindow) {
|
|
838
|
+
if (msg.kind === "request") {
|
|
839
|
+
this.respond(source, event.origin, {
|
|
840
|
+
kind: "response",
|
|
841
|
+
type: msg.type,
|
|
842
|
+
requestId: msg.requestId,
|
|
843
|
+
ok: false,
|
|
844
|
+
error: "Vibe App is not connected \u2014 call connect() first.",
|
|
845
|
+
code: "NOT_CONNECTED"
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
await this.kindHandlers[msg.kind]?.(msg, source, event.origin);
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Handles the CONNECT handshake: records the connecting window/origin (which
|
|
854
|
+
* opens the connection gate for it) and pushes the initial context + one-time
|
|
855
|
+
* PostHog config. Only the first CONNECT per origin pushes — later retries are
|
|
856
|
+
* ignored, so the bridge receives exactly one initial CONTEXT_PUSH (no
|
|
857
|
+
* spurious onContextChange). A reloaded iframe is a new window and re-connects
|
|
858
|
+
* normally. If `getContext()` throws, the window is left unconnected so the
|
|
859
|
+
* bridge's retry can recover.
|
|
860
|
+
*/
|
|
861
|
+
handleConnect(source, origin) {
|
|
862
|
+
if (source === this.connectedWindow) return;
|
|
863
|
+
try {
|
|
864
|
+
const ctx = { ...this.getContext(), hostVersion: version };
|
|
865
|
+
this.connectedWindow = source;
|
|
866
|
+
this.connectedOrigin = origin;
|
|
867
|
+
console.log(
|
|
868
|
+
`[vibe-host] Vibe App connected \u2014 host: ${version}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
869
|
+
);
|
|
870
|
+
const message = {
|
|
871
|
+
__protocol: PROTOCOL_ID,
|
|
872
|
+
__version: PROTOCOL_VERSION,
|
|
873
|
+
kind: "push",
|
|
874
|
+
type: "CONTEXT_PUSH",
|
|
875
|
+
payload: ctx
|
|
876
|
+
};
|
|
877
|
+
source.postMessage(message, origin);
|
|
878
|
+
this.pushPostHog(source, origin);
|
|
879
|
+
} catch (err) {
|
|
880
|
+
console.error("[vibe-host] getContext threw during connect \u2014 context not delivered.", err);
|
|
881
|
+
}
|
|
801
882
|
}
|
|
802
883
|
/**
|
|
803
884
|
* Concrete origins a proactive push may target: the exact allowlist entries
|
|
804
|
-
* plus
|
|
805
|
-
*
|
|
806
|
-
*
|
|
885
|
+
* plus the connected iframe's learned origin. Never includes glob patterns
|
|
886
|
+
* (you cannot `postMessage` to one). When only exact origins are configured
|
|
887
|
+
* this equals the configured set, so behaviour is unchanged.
|
|
807
888
|
*/
|
|
808
889
|
concreteTargets() {
|
|
809
|
-
|
|
890
|
+
const targets = new Set(this.exactOrigins);
|
|
891
|
+
if (this.connectedOrigin) targets.add(this.connectedOrigin);
|
|
892
|
+
return targets;
|
|
810
893
|
}
|
|
811
894
|
async handleRequest(msg, source, origin) {
|
|
812
895
|
const { requestId, type } = msg;
|
|
813
|
-
if (type === "GET_CONTEXT") {
|
|
814
|
-
try {
|
|
815
|
-
const ctx = { ...this.getContext(), hostVersion: version };
|
|
816
|
-
if (!this.handshakeLoggedWindows.has(source)) {
|
|
817
|
-
this.handshakeLoggedWindows.add(source);
|
|
818
|
-
const bridgeVersion = msg.payload?.bridgeVersion ?? "unknown";
|
|
819
|
-
console.log(
|
|
820
|
-
`[vibe-host] Vibe App connected \u2014 bridge: ${bridgeVersion}, host: ${version}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
821
|
-
);
|
|
822
|
-
}
|
|
823
|
-
this.respond(source, origin, { kind: "response", type, requestId, ok: true, data: ctx });
|
|
824
|
-
} catch (err) {
|
|
825
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
826
|
-
const code = err instanceof BridgeError ? err.code : "CONTEXT_ERROR";
|
|
827
|
-
this.respond(source, origin, { kind: "response", type, requestId, ok: false, error, code });
|
|
828
|
-
}
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
896
|
if (!checkRateLimit(this.rateLimiter)) {
|
|
832
897
|
this.respond(source, origin, {
|
|
833
898
|
kind: "response",
|
package/dist/index.d.cts
CHANGED
|
@@ -1390,6 +1390,7 @@ type ActivityProviderOperationErrorType = 'Connection Error' | 'Authentication E
|
|
|
1390
1390
|
type ActivityReconciliationDefinitionData = {
|
|
1391
1391
|
type: ActivityReconciliationType;
|
|
1392
1392
|
task_definition_id: string;
|
|
1393
|
+
currency?: ActivityCurrency | null;
|
|
1393
1394
|
side_a_entity_type: ActivityReconciliationEntityType;
|
|
1394
1395
|
side_a_entity_id: string;
|
|
1395
1396
|
side_b_entity_type: ActivityReconciliationEntityType;
|
|
@@ -1403,7 +1404,6 @@ type ActivityReconciliationInstanceDataInput = {
|
|
|
1403
1404
|
task_instance_id: string;
|
|
1404
1405
|
start_date: string;
|
|
1405
1406
|
end_date: string;
|
|
1406
|
-
currency?: ActivityCurrency | null;
|
|
1407
1407
|
side_a_value?: number | string | null;
|
|
1408
1408
|
side_b_value?: number | string | null;
|
|
1409
1409
|
reconciling_items_balance?: number | string | null;
|
|
@@ -1417,7 +1417,6 @@ type ActivityReconciliationInstanceDataOutput = {
|
|
|
1417
1417
|
task_instance_id: string;
|
|
1418
1418
|
start_date: string;
|
|
1419
1419
|
end_date: string;
|
|
1420
|
-
currency?: ActivityCurrency | null;
|
|
1421
1420
|
side_a_value?: string | null;
|
|
1422
1421
|
side_b_value?: string | null;
|
|
1423
1422
|
reconciling_items_balance?: string | null;
|
|
@@ -3180,6 +3179,76 @@ interface BridgeSubsidiary {
|
|
|
3180
3179
|
id: number;
|
|
3181
3180
|
displayName: string;
|
|
3182
3181
|
}
|
|
3182
|
+
/**
|
|
3183
|
+
* PostHog configuration the host supplies so the iframe can record a session
|
|
3184
|
+
* replay (stitched into the host's recording via `recordCrossOriginIframes`)
|
|
3185
|
+
* and emit custom events, all attributed to the **same PostHog person** as
|
|
3186
|
+
* nom-ui.
|
|
3187
|
+
*
|
|
3188
|
+
* The host (nom-ui) is the single source of truth for these values — it knows
|
|
3189
|
+
* the project key, the env-aware ingestion host, and the resolved person — and
|
|
3190
|
+
* delivers it over the bridge (as a one-time `POSTHOG_PUSH`, separate from
|
|
3191
|
+
* {@link ContextPayload}) so the iframe never hardcodes any of it. When the host
|
|
3192
|
+
* has no PostHog config (recording disabled for this env/app), it sends no
|
|
3193
|
+
* `POSTHOG_PUSH` and the bridge initializes nothing.
|
|
3194
|
+
*/
|
|
3195
|
+
interface PostHogContext {
|
|
3196
|
+
/** Project API key — the same PostHog project the host (nom-ui) uses. */
|
|
3197
|
+
token: string;
|
|
3198
|
+
/**
|
|
3199
|
+
* **Absolute** ingestion host, e.g. `"https://us.i.posthog.com"`.
|
|
3200
|
+
*
|
|
3201
|
+
* Must NOT be the host's relative `/ingest` reverse-proxy path: a relative
|
|
3202
|
+
* path resolves against the iframe's *own* (cross-origin) host and 404s.
|
|
3203
|
+
*/
|
|
3204
|
+
apiHost: string;
|
|
3205
|
+
/**
|
|
3206
|
+
* Host-resolved PostHog distinct id. The bridge calls `identify()` with it so
|
|
3207
|
+
* the iframe's recording and events attribute to the same person as the host.
|
|
3208
|
+
*/
|
|
3209
|
+
distinctId: string;
|
|
3210
|
+
/**
|
|
3211
|
+
* **Optional.** The host's current PostHog session id (from
|
|
3212
|
+
* `posthog.get_session_id()`). When provided, the bridge seeds it as the
|
|
3213
|
+
* iframe instance's session via `bootstrap.sessionID`, so custom events from
|
|
3214
|
+
* `bridge.track()` share the **same `$session_id`** as the host — letting them
|
|
3215
|
+
* line up with the stitched session replay on one timeline.
|
|
3216
|
+
*
|
|
3217
|
+
* Not needed for *replay* stitching itself (that works via
|
|
3218
|
+
* `recordCrossOriginIframes` on both sides regardless). Must be a valid UUID
|
|
3219
|
+
* v7 — PostHog session ids already are. It is a one-time seed at connect, so
|
|
3220
|
+
* if the host's session later rotates the two can diverge; alignment is
|
|
3221
|
+
* best-effort for the session in progress.
|
|
3222
|
+
*/
|
|
3223
|
+
sessionId?: string;
|
|
3224
|
+
/**
|
|
3225
|
+
* Master switch, env- and/or app-gated by the host. The bridge only calls
|
|
3226
|
+
* `posthog.init()` when this is `true`; it stays the kill-switch even though
|
|
3227
|
+
* `posthog-js` is bundled into the bridge.
|
|
3228
|
+
*/
|
|
3229
|
+
enabled: boolean;
|
|
3230
|
+
}
|
|
3231
|
+
/**
|
|
3232
|
+
* Authentication-state change the host pushes to the iframe via `AUTH_CHANGED`.
|
|
3233
|
+
* It carries the security principal the embedded app authenticates and scopes
|
|
3234
|
+
* its own data by. Sent when the Nominal session changes: a logout
|
|
3235
|
+
* (`authenticated: false`) or an identity/tenant switch (`authenticated: true`
|
|
3236
|
+
* with a new `userId`/`tenant`). The initial state is implicit at connect — the
|
|
3237
|
+
* iframe only loads when authenticated, and the current identity/tenant are
|
|
3238
|
+
* {@link ContextPayload.user} / {@link ContextPayload.tenant}, so apps baseline
|
|
3239
|
+
* off those and react to this push.
|
|
3240
|
+
*
|
|
3241
|
+
* Subsidiary/period and other view-level state are NOT here — they ride the
|
|
3242
|
+
* follow-up `CONTEXT_PUSH` (`onContextChange`), per "auth change, then context".
|
|
3243
|
+
*/
|
|
3244
|
+
interface AuthPayload {
|
|
3245
|
+
/** Whether the Nominal user is still authenticated. */
|
|
3246
|
+
authenticated: boolean;
|
|
3247
|
+
/** The effective Nominal user id when authenticated; omitted on logout. */
|
|
3248
|
+
userId?: string;
|
|
3249
|
+
/** The tenant the user is acting in — the app's data-scoping key (RLS). */
|
|
3250
|
+
tenant?: string;
|
|
3251
|
+
}
|
|
3183
3252
|
interface ContextPayload {
|
|
3184
3253
|
tenant: string;
|
|
3185
3254
|
subsidiaryId: number;
|
|
@@ -3195,11 +3264,6 @@ interface ContextPayload {
|
|
|
3195
3264
|
/** Version of the host SDK (@nominalso/vibe-host) running in nom-ui. */
|
|
3196
3265
|
hostVersion: string;
|
|
3197
3266
|
}
|
|
3198
|
-
/** Payload sent by the bridge when requesting context from the host. */
|
|
3199
|
-
interface GetContextPayload {
|
|
3200
|
-
/** Version of the bridge SDK (@nominalso/vibe-bridge) running in the iframe. */
|
|
3201
|
-
bridgeVersion: string;
|
|
3202
|
-
}
|
|
3203
3267
|
|
|
3204
3268
|
interface UploadPayload {
|
|
3205
3269
|
buffer: ArrayBuffer;
|
|
@@ -3451,10 +3515,6 @@ interface RequestRegistry {
|
|
|
3451
3515
|
payload: InvalidateCachePayload;
|
|
3452
3516
|
data: InvalidateResult;
|
|
3453
3517
|
};
|
|
3454
|
-
GET_CONTEXT: {
|
|
3455
|
-
payload: GetContextPayload;
|
|
3456
|
-
data: ContextPayload;
|
|
3457
|
-
};
|
|
3458
3518
|
UPLOAD_FILE: {
|
|
3459
3519
|
payload: UploadPayload;
|
|
3460
3520
|
data: UploadResponse;
|
|
@@ -3568,6 +3628,13 @@ interface VibeAppHostOptions {
|
|
|
3568
3628
|
trustedOrigins: string[];
|
|
3569
3629
|
/** Returns the current context to send to the iframe on connect. */
|
|
3570
3630
|
getContext: () => HostContext;
|
|
3631
|
+
/**
|
|
3632
|
+
* Optional PostHog config source. Read **once per iframe** at connect and
|
|
3633
|
+
* delivered via a one-time `POSTHOG_PUSH`, kept separate from `getContext` so
|
|
3634
|
+
* it never re-sends when context or auth later change. Return `undefined` (or
|
|
3635
|
+
* a config with `enabled: false`) to disable in-iframe recording.
|
|
3636
|
+
*/
|
|
3637
|
+
getPostHog?: () => PostHogContext | undefined;
|
|
3571
3638
|
/**
|
|
3572
3639
|
* The base URL path of this Vibe App in nom-ui, e.g. `/acme/1/apps/fixed-assets`.
|
|
3573
3640
|
* Used to sync the browser URL with the iframe's internal subroute.
|
|
@@ -3620,18 +3687,11 @@ interface VibeAppHostOptions {
|
|
|
3620
3687
|
* onUpload: (p, sendProgress) => uploadToS3(p, sendProgress),
|
|
3621
3688
|
* })
|
|
3622
3689
|
*
|
|
3623
|
-
* useEffect(() =>
|
|
3624
|
-
* const iframe = iframeRef.current
|
|
3625
|
-
* if (!iframe) return
|
|
3626
|
-
* const unmount = host.mount() // listen before the iframe loads
|
|
3627
|
-
* const onLoad = () => iframe.contentWindow && host.pushContextTo(iframe.contentWindow)
|
|
3628
|
-
* iframe.addEventListener('load', onLoad)
|
|
3629
|
-
* return () => {
|
|
3630
|
-
* iframe.removeEventListener('load', onLoad)
|
|
3631
|
-
* unmount()
|
|
3632
|
-
* }
|
|
3633
|
-
* }, [host])
|
|
3690
|
+
* useEffect(() => host.mount(), [host]) // returns the unmount cleanup
|
|
3634
3691
|
* ```
|
|
3692
|
+
*
|
|
3693
|
+
* `mount()` just starts listening; the iframe identifies itself via its
|
|
3694
|
+
* `CONNECT` handshake, so the host never needs the iframe's `Window` up front.
|
|
3635
3695
|
*/
|
|
3636
3696
|
declare class VibeAppHost {
|
|
3637
3697
|
/**
|
|
@@ -3644,26 +3704,29 @@ declare class VibeAppHost {
|
|
|
3644
3704
|
/** The exact (non-pattern) subset of {@link trustedOrigins} — see {@link concreteTargets}. */
|
|
3645
3705
|
private readonly exactOrigins;
|
|
3646
3706
|
/**
|
|
3647
|
-
*
|
|
3648
|
-
*
|
|
3649
|
-
*
|
|
3707
|
+
* The connected iframe, as two facets of one thing: `connectedWindow` is its
|
|
3708
|
+
* `Window` (identity + the single push target) and `connectedOrigin` is its
|
|
3709
|
+
* origin (what you `postMessage` to — you cannot post to a `Window` ref, nor to
|
|
3710
|
+
* a glob). Both are written together by {@link handleConnect} (the sole writer)
|
|
3711
|
+
* and cleared together on unmount, so they can never diverge.
|
|
3712
|
+
*
|
|
3713
|
+
* There is deliberately no other window reference. The connected window can
|
|
3714
|
+
* only be learned from each `CONNECT` handshake's `event.source`, never at
|
|
3715
|
+
* mount: a reloaded/remounted iframe is a *new* `Window` with the same origin,
|
|
3716
|
+
* so a mount-time window would be stale. The gate blocks every non-`CONNECT`
|
|
3717
|
+
* message whose source isn't `connectedWindow`; every proactive push targets it.
|
|
3650
3718
|
*/
|
|
3651
|
-
private
|
|
3719
|
+
private connectedWindow;
|
|
3720
|
+
private connectedOrigin;
|
|
3652
3721
|
private readonly handlers;
|
|
3653
3722
|
private readonly commandHandlers;
|
|
3654
3723
|
private readonly getContext;
|
|
3724
|
+
private readonly getPostHog?;
|
|
3655
3725
|
private readonly appBasePath;
|
|
3656
3726
|
private readonly onRequestComplete?;
|
|
3657
3727
|
private readonly rateLimiter;
|
|
3658
3728
|
private readonly boundHandleMessage;
|
|
3659
3729
|
private readonly boundHandlePopState;
|
|
3660
|
-
private iframeWindow;
|
|
3661
|
-
/**
|
|
3662
|
-
* Source windows we've already logged a successful handshake for. The bridge
|
|
3663
|
-
* polls `GET_CONTEXT` on an interval until it connects, so without this the
|
|
3664
|
-
* "Vibe App connected" line repeats on every poll (SSOT §13 F3).
|
|
3665
|
-
*/
|
|
3666
|
-
private readonly handshakeLoggedWindows;
|
|
3667
3730
|
private readonly kindHandlers;
|
|
3668
3731
|
private dispatchCommand;
|
|
3669
3732
|
/**
|
|
@@ -3683,31 +3746,57 @@ declare class VibeAppHost {
|
|
|
3683
3746
|
* Starts listening for bridge messages and sets up browser back/forward sync.
|
|
3684
3747
|
* Returns a cleanup function to call on unmount.
|
|
3685
3748
|
*
|
|
3686
|
-
*
|
|
3687
|
-
*
|
|
3688
|
-
*
|
|
3689
|
-
|
|
3749
|
+
* Takes no iframe window: the host learns the iframe from its `CONNECT`
|
|
3750
|
+
* handshake (and re-learns it on every reload), then delivers context as the
|
|
3751
|
+
* reply to that handshake. Just call `mount()` once the host exists.
|
|
3752
|
+
*/
|
|
3753
|
+
mount(): () => void;
|
|
3754
|
+
/**
|
|
3755
|
+
* Re-reads `getContext()` and re-pushes it to the connected iframe, so an open
|
|
3756
|
+
* app reacts to a tenant/subsidiary switch or period close without reloading.
|
|
3757
|
+
* PostHog is not re-sent (it rides only the connect handshake). No-op until an
|
|
3758
|
+
* iframe is mounted.
|
|
3690
3759
|
*/
|
|
3691
|
-
|
|
3760
|
+
refreshContext(): void;
|
|
3692
3761
|
/**
|
|
3693
|
-
*
|
|
3694
|
-
*
|
|
3695
|
-
*
|
|
3762
|
+
* Notifies the connected iframe of a Nominal auth change — a logout
|
|
3763
|
+
* (`authenticated: false`) or an identity/tenant switch (`authenticated: true`
|
|
3764
|
+
* with a new `userId`/`tenant`) — via `AUTH_CHANGED`, so the app re-authenticates
|
|
3765
|
+
* or signs its own backend out. No-op until the iframe has connected. When auth
|
|
3766
|
+
* and context change together, call this *before* {@link refreshContext} ("auth
|
|
3767
|
+
* change, then context").
|
|
3696
3768
|
*/
|
|
3697
|
-
|
|
3769
|
+
notifyAuthChange(auth: AuthPayload): void;
|
|
3698
3770
|
private pushContext;
|
|
3699
3771
|
private handlePopState;
|
|
3700
3772
|
private pushToIframe;
|
|
3773
|
+
/**
|
|
3774
|
+
* Sends the host's PostHog config to a freshly connected iframe as a one-time
|
|
3775
|
+
* POSTHOG_PUSH. No-op when no `getPostHog` source is configured or it returns
|
|
3776
|
+
* nothing (recording disabled). Targets the exact handshaking window/origin
|
|
3777
|
+
* (no `concreteTargets` fan-out) since it fires from a validated inbound poll.
|
|
3778
|
+
*/
|
|
3779
|
+
private pushPostHog;
|
|
3701
3780
|
private handleMessage;
|
|
3781
|
+
/**
|
|
3782
|
+
* Handles the CONNECT handshake: records the connecting window/origin (which
|
|
3783
|
+
* opens the connection gate for it) and pushes the initial context + one-time
|
|
3784
|
+
* PostHog config. Only the first CONNECT per origin pushes — later retries are
|
|
3785
|
+
* ignored, so the bridge receives exactly one initial CONTEXT_PUSH (no
|
|
3786
|
+
* spurious onContextChange). A reloaded iframe is a new window and re-connects
|
|
3787
|
+
* normally. If `getContext()` throws, the window is left unconnected so the
|
|
3788
|
+
* bridge's retry can recover.
|
|
3789
|
+
*/
|
|
3790
|
+
private handleConnect;
|
|
3702
3791
|
/**
|
|
3703
3792
|
* Concrete origins a proactive push may target: the exact allowlist entries
|
|
3704
|
-
* plus
|
|
3705
|
-
*
|
|
3706
|
-
*
|
|
3793
|
+
* plus the connected iframe's learned origin. Never includes glob patterns
|
|
3794
|
+
* (you cannot `postMessage` to one). When only exact origins are configured
|
|
3795
|
+
* this equals the configured set, so behaviour is unchanged.
|
|
3707
3796
|
*/
|
|
3708
3797
|
private concreteTargets;
|
|
3709
3798
|
private handleRequest;
|
|
3710
3799
|
private respond;
|
|
3711
3800
|
}
|
|
3712
3801
|
|
|
3713
|
-
export { BridgeError, type ContextPayload, type HostContext, type HostHandlers, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type NavigatePayload, type RequestCompleteInfo, type RequestHandlers, type SetSideNavPayload, type SwitchSubsidiaryPayload, type UploadPayload, type UploadProgress, type UploadResponse, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };
|
|
3802
|
+
export { type AuthPayload, BridgeError, type ContextPayload, type HostContext, type HostHandlers, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type NavigatePayload, type PostHogContext, type RequestCompleteInfo, type RequestHandlers, type SetSideNavPayload, type SwitchSubsidiaryPayload, type UploadPayload, type UploadProgress, type UploadResponse, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -1390,6 +1390,7 @@ type ActivityProviderOperationErrorType = 'Connection Error' | 'Authentication E
|
|
|
1390
1390
|
type ActivityReconciliationDefinitionData = {
|
|
1391
1391
|
type: ActivityReconciliationType;
|
|
1392
1392
|
task_definition_id: string;
|
|
1393
|
+
currency?: ActivityCurrency | null;
|
|
1393
1394
|
side_a_entity_type: ActivityReconciliationEntityType;
|
|
1394
1395
|
side_a_entity_id: string;
|
|
1395
1396
|
side_b_entity_type: ActivityReconciliationEntityType;
|
|
@@ -1403,7 +1404,6 @@ type ActivityReconciliationInstanceDataInput = {
|
|
|
1403
1404
|
task_instance_id: string;
|
|
1404
1405
|
start_date: string;
|
|
1405
1406
|
end_date: string;
|
|
1406
|
-
currency?: ActivityCurrency | null;
|
|
1407
1407
|
side_a_value?: number | string | null;
|
|
1408
1408
|
side_b_value?: number | string | null;
|
|
1409
1409
|
reconciling_items_balance?: number | string | null;
|
|
@@ -1417,7 +1417,6 @@ type ActivityReconciliationInstanceDataOutput = {
|
|
|
1417
1417
|
task_instance_id: string;
|
|
1418
1418
|
start_date: string;
|
|
1419
1419
|
end_date: string;
|
|
1420
|
-
currency?: ActivityCurrency | null;
|
|
1421
1420
|
side_a_value?: string | null;
|
|
1422
1421
|
side_b_value?: string | null;
|
|
1423
1422
|
reconciling_items_balance?: string | null;
|
|
@@ -3180,6 +3179,76 @@ interface BridgeSubsidiary {
|
|
|
3180
3179
|
id: number;
|
|
3181
3180
|
displayName: string;
|
|
3182
3181
|
}
|
|
3182
|
+
/**
|
|
3183
|
+
* PostHog configuration the host supplies so the iframe can record a session
|
|
3184
|
+
* replay (stitched into the host's recording via `recordCrossOriginIframes`)
|
|
3185
|
+
* and emit custom events, all attributed to the **same PostHog person** as
|
|
3186
|
+
* nom-ui.
|
|
3187
|
+
*
|
|
3188
|
+
* The host (nom-ui) is the single source of truth for these values — it knows
|
|
3189
|
+
* the project key, the env-aware ingestion host, and the resolved person — and
|
|
3190
|
+
* delivers it over the bridge (as a one-time `POSTHOG_PUSH`, separate from
|
|
3191
|
+
* {@link ContextPayload}) so the iframe never hardcodes any of it. When the host
|
|
3192
|
+
* has no PostHog config (recording disabled for this env/app), it sends no
|
|
3193
|
+
* `POSTHOG_PUSH` and the bridge initializes nothing.
|
|
3194
|
+
*/
|
|
3195
|
+
interface PostHogContext {
|
|
3196
|
+
/** Project API key — the same PostHog project the host (nom-ui) uses. */
|
|
3197
|
+
token: string;
|
|
3198
|
+
/**
|
|
3199
|
+
* **Absolute** ingestion host, e.g. `"https://us.i.posthog.com"`.
|
|
3200
|
+
*
|
|
3201
|
+
* Must NOT be the host's relative `/ingest` reverse-proxy path: a relative
|
|
3202
|
+
* path resolves against the iframe's *own* (cross-origin) host and 404s.
|
|
3203
|
+
*/
|
|
3204
|
+
apiHost: string;
|
|
3205
|
+
/**
|
|
3206
|
+
* Host-resolved PostHog distinct id. The bridge calls `identify()` with it so
|
|
3207
|
+
* the iframe's recording and events attribute to the same person as the host.
|
|
3208
|
+
*/
|
|
3209
|
+
distinctId: string;
|
|
3210
|
+
/**
|
|
3211
|
+
* **Optional.** The host's current PostHog session id (from
|
|
3212
|
+
* `posthog.get_session_id()`). When provided, the bridge seeds it as the
|
|
3213
|
+
* iframe instance's session via `bootstrap.sessionID`, so custom events from
|
|
3214
|
+
* `bridge.track()` share the **same `$session_id`** as the host — letting them
|
|
3215
|
+
* line up with the stitched session replay on one timeline.
|
|
3216
|
+
*
|
|
3217
|
+
* Not needed for *replay* stitching itself (that works via
|
|
3218
|
+
* `recordCrossOriginIframes` on both sides regardless). Must be a valid UUID
|
|
3219
|
+
* v7 — PostHog session ids already are. It is a one-time seed at connect, so
|
|
3220
|
+
* if the host's session later rotates the two can diverge; alignment is
|
|
3221
|
+
* best-effort for the session in progress.
|
|
3222
|
+
*/
|
|
3223
|
+
sessionId?: string;
|
|
3224
|
+
/**
|
|
3225
|
+
* Master switch, env- and/or app-gated by the host. The bridge only calls
|
|
3226
|
+
* `posthog.init()` when this is `true`; it stays the kill-switch even though
|
|
3227
|
+
* `posthog-js` is bundled into the bridge.
|
|
3228
|
+
*/
|
|
3229
|
+
enabled: boolean;
|
|
3230
|
+
}
|
|
3231
|
+
/**
|
|
3232
|
+
* Authentication-state change the host pushes to the iframe via `AUTH_CHANGED`.
|
|
3233
|
+
* It carries the security principal the embedded app authenticates and scopes
|
|
3234
|
+
* its own data by. Sent when the Nominal session changes: a logout
|
|
3235
|
+
* (`authenticated: false`) or an identity/tenant switch (`authenticated: true`
|
|
3236
|
+
* with a new `userId`/`tenant`). The initial state is implicit at connect — the
|
|
3237
|
+
* iframe only loads when authenticated, and the current identity/tenant are
|
|
3238
|
+
* {@link ContextPayload.user} / {@link ContextPayload.tenant}, so apps baseline
|
|
3239
|
+
* off those and react to this push.
|
|
3240
|
+
*
|
|
3241
|
+
* Subsidiary/period and other view-level state are NOT here — they ride the
|
|
3242
|
+
* follow-up `CONTEXT_PUSH` (`onContextChange`), per "auth change, then context".
|
|
3243
|
+
*/
|
|
3244
|
+
interface AuthPayload {
|
|
3245
|
+
/** Whether the Nominal user is still authenticated. */
|
|
3246
|
+
authenticated: boolean;
|
|
3247
|
+
/** The effective Nominal user id when authenticated; omitted on logout. */
|
|
3248
|
+
userId?: string;
|
|
3249
|
+
/** The tenant the user is acting in — the app's data-scoping key (RLS). */
|
|
3250
|
+
tenant?: string;
|
|
3251
|
+
}
|
|
3183
3252
|
interface ContextPayload {
|
|
3184
3253
|
tenant: string;
|
|
3185
3254
|
subsidiaryId: number;
|
|
@@ -3195,11 +3264,6 @@ interface ContextPayload {
|
|
|
3195
3264
|
/** Version of the host SDK (@nominalso/vibe-host) running in nom-ui. */
|
|
3196
3265
|
hostVersion: string;
|
|
3197
3266
|
}
|
|
3198
|
-
/** Payload sent by the bridge when requesting context from the host. */
|
|
3199
|
-
interface GetContextPayload {
|
|
3200
|
-
/** Version of the bridge SDK (@nominalso/vibe-bridge) running in the iframe. */
|
|
3201
|
-
bridgeVersion: string;
|
|
3202
|
-
}
|
|
3203
3267
|
|
|
3204
3268
|
interface UploadPayload {
|
|
3205
3269
|
buffer: ArrayBuffer;
|
|
@@ -3451,10 +3515,6 @@ interface RequestRegistry {
|
|
|
3451
3515
|
payload: InvalidateCachePayload;
|
|
3452
3516
|
data: InvalidateResult;
|
|
3453
3517
|
};
|
|
3454
|
-
GET_CONTEXT: {
|
|
3455
|
-
payload: GetContextPayload;
|
|
3456
|
-
data: ContextPayload;
|
|
3457
|
-
};
|
|
3458
3518
|
UPLOAD_FILE: {
|
|
3459
3519
|
payload: UploadPayload;
|
|
3460
3520
|
data: UploadResponse;
|
|
@@ -3568,6 +3628,13 @@ interface VibeAppHostOptions {
|
|
|
3568
3628
|
trustedOrigins: string[];
|
|
3569
3629
|
/** Returns the current context to send to the iframe on connect. */
|
|
3570
3630
|
getContext: () => HostContext;
|
|
3631
|
+
/**
|
|
3632
|
+
* Optional PostHog config source. Read **once per iframe** at connect and
|
|
3633
|
+
* delivered via a one-time `POSTHOG_PUSH`, kept separate from `getContext` so
|
|
3634
|
+
* it never re-sends when context or auth later change. Return `undefined` (or
|
|
3635
|
+
* a config with `enabled: false`) to disable in-iframe recording.
|
|
3636
|
+
*/
|
|
3637
|
+
getPostHog?: () => PostHogContext | undefined;
|
|
3571
3638
|
/**
|
|
3572
3639
|
* The base URL path of this Vibe App in nom-ui, e.g. `/acme/1/apps/fixed-assets`.
|
|
3573
3640
|
* Used to sync the browser URL with the iframe's internal subroute.
|
|
@@ -3620,18 +3687,11 @@ interface VibeAppHostOptions {
|
|
|
3620
3687
|
* onUpload: (p, sendProgress) => uploadToS3(p, sendProgress),
|
|
3621
3688
|
* })
|
|
3622
3689
|
*
|
|
3623
|
-
* useEffect(() =>
|
|
3624
|
-
* const iframe = iframeRef.current
|
|
3625
|
-
* if (!iframe) return
|
|
3626
|
-
* const unmount = host.mount() // listen before the iframe loads
|
|
3627
|
-
* const onLoad = () => iframe.contentWindow && host.pushContextTo(iframe.contentWindow)
|
|
3628
|
-
* iframe.addEventListener('load', onLoad)
|
|
3629
|
-
* return () => {
|
|
3630
|
-
* iframe.removeEventListener('load', onLoad)
|
|
3631
|
-
* unmount()
|
|
3632
|
-
* }
|
|
3633
|
-
* }, [host])
|
|
3690
|
+
* useEffect(() => host.mount(), [host]) // returns the unmount cleanup
|
|
3634
3691
|
* ```
|
|
3692
|
+
*
|
|
3693
|
+
* `mount()` just starts listening; the iframe identifies itself via its
|
|
3694
|
+
* `CONNECT` handshake, so the host never needs the iframe's `Window` up front.
|
|
3635
3695
|
*/
|
|
3636
3696
|
declare class VibeAppHost {
|
|
3637
3697
|
/**
|
|
@@ -3644,26 +3704,29 @@ declare class VibeAppHost {
|
|
|
3644
3704
|
/** The exact (non-pattern) subset of {@link trustedOrigins} — see {@link concreteTargets}. */
|
|
3645
3705
|
private readonly exactOrigins;
|
|
3646
3706
|
/**
|
|
3647
|
-
*
|
|
3648
|
-
*
|
|
3649
|
-
*
|
|
3707
|
+
* The connected iframe, as two facets of one thing: `connectedWindow` is its
|
|
3708
|
+
* `Window` (identity + the single push target) and `connectedOrigin` is its
|
|
3709
|
+
* origin (what you `postMessage` to — you cannot post to a `Window` ref, nor to
|
|
3710
|
+
* a glob). Both are written together by {@link handleConnect} (the sole writer)
|
|
3711
|
+
* and cleared together on unmount, so they can never diverge.
|
|
3712
|
+
*
|
|
3713
|
+
* There is deliberately no other window reference. The connected window can
|
|
3714
|
+
* only be learned from each `CONNECT` handshake's `event.source`, never at
|
|
3715
|
+
* mount: a reloaded/remounted iframe is a *new* `Window` with the same origin,
|
|
3716
|
+
* so a mount-time window would be stale. The gate blocks every non-`CONNECT`
|
|
3717
|
+
* message whose source isn't `connectedWindow`; every proactive push targets it.
|
|
3650
3718
|
*/
|
|
3651
|
-
private
|
|
3719
|
+
private connectedWindow;
|
|
3720
|
+
private connectedOrigin;
|
|
3652
3721
|
private readonly handlers;
|
|
3653
3722
|
private readonly commandHandlers;
|
|
3654
3723
|
private readonly getContext;
|
|
3724
|
+
private readonly getPostHog?;
|
|
3655
3725
|
private readonly appBasePath;
|
|
3656
3726
|
private readonly onRequestComplete?;
|
|
3657
3727
|
private readonly rateLimiter;
|
|
3658
3728
|
private readonly boundHandleMessage;
|
|
3659
3729
|
private readonly boundHandlePopState;
|
|
3660
|
-
private iframeWindow;
|
|
3661
|
-
/**
|
|
3662
|
-
* Source windows we've already logged a successful handshake for. The bridge
|
|
3663
|
-
* polls `GET_CONTEXT` on an interval until it connects, so without this the
|
|
3664
|
-
* "Vibe App connected" line repeats on every poll (SSOT §13 F3).
|
|
3665
|
-
*/
|
|
3666
|
-
private readonly handshakeLoggedWindows;
|
|
3667
3730
|
private readonly kindHandlers;
|
|
3668
3731
|
private dispatchCommand;
|
|
3669
3732
|
/**
|
|
@@ -3683,31 +3746,57 @@ declare class VibeAppHost {
|
|
|
3683
3746
|
* Starts listening for bridge messages and sets up browser back/forward sync.
|
|
3684
3747
|
* Returns a cleanup function to call on unmount.
|
|
3685
3748
|
*
|
|
3686
|
-
*
|
|
3687
|
-
*
|
|
3688
|
-
*
|
|
3689
|
-
|
|
3749
|
+
* Takes no iframe window: the host learns the iframe from its `CONNECT`
|
|
3750
|
+
* handshake (and re-learns it on every reload), then delivers context as the
|
|
3751
|
+
* reply to that handshake. Just call `mount()` once the host exists.
|
|
3752
|
+
*/
|
|
3753
|
+
mount(): () => void;
|
|
3754
|
+
/**
|
|
3755
|
+
* Re-reads `getContext()` and re-pushes it to the connected iframe, so an open
|
|
3756
|
+
* app reacts to a tenant/subsidiary switch or period close without reloading.
|
|
3757
|
+
* PostHog is not re-sent (it rides only the connect handshake). No-op until an
|
|
3758
|
+
* iframe is mounted.
|
|
3690
3759
|
*/
|
|
3691
|
-
|
|
3760
|
+
refreshContext(): void;
|
|
3692
3761
|
/**
|
|
3693
|
-
*
|
|
3694
|
-
*
|
|
3695
|
-
*
|
|
3762
|
+
* Notifies the connected iframe of a Nominal auth change — a logout
|
|
3763
|
+
* (`authenticated: false`) or an identity/tenant switch (`authenticated: true`
|
|
3764
|
+
* with a new `userId`/`tenant`) — via `AUTH_CHANGED`, so the app re-authenticates
|
|
3765
|
+
* or signs its own backend out. No-op until the iframe has connected. When auth
|
|
3766
|
+
* and context change together, call this *before* {@link refreshContext} ("auth
|
|
3767
|
+
* change, then context").
|
|
3696
3768
|
*/
|
|
3697
|
-
|
|
3769
|
+
notifyAuthChange(auth: AuthPayload): void;
|
|
3698
3770
|
private pushContext;
|
|
3699
3771
|
private handlePopState;
|
|
3700
3772
|
private pushToIframe;
|
|
3773
|
+
/**
|
|
3774
|
+
* Sends the host's PostHog config to a freshly connected iframe as a one-time
|
|
3775
|
+
* POSTHOG_PUSH. No-op when no `getPostHog` source is configured or it returns
|
|
3776
|
+
* nothing (recording disabled). Targets the exact handshaking window/origin
|
|
3777
|
+
* (no `concreteTargets` fan-out) since it fires from a validated inbound poll.
|
|
3778
|
+
*/
|
|
3779
|
+
private pushPostHog;
|
|
3701
3780
|
private handleMessage;
|
|
3781
|
+
/**
|
|
3782
|
+
* Handles the CONNECT handshake: records the connecting window/origin (which
|
|
3783
|
+
* opens the connection gate for it) and pushes the initial context + one-time
|
|
3784
|
+
* PostHog config. Only the first CONNECT per origin pushes — later retries are
|
|
3785
|
+
* ignored, so the bridge receives exactly one initial CONTEXT_PUSH (no
|
|
3786
|
+
* spurious onContextChange). A reloaded iframe is a new window and re-connects
|
|
3787
|
+
* normally. If `getContext()` throws, the window is left unconnected so the
|
|
3788
|
+
* bridge's retry can recover.
|
|
3789
|
+
*/
|
|
3790
|
+
private handleConnect;
|
|
3702
3791
|
/**
|
|
3703
3792
|
* Concrete origins a proactive push may target: the exact allowlist entries
|
|
3704
|
-
* plus
|
|
3705
|
-
*
|
|
3706
|
-
*
|
|
3793
|
+
* plus the connected iframe's learned origin. Never includes glob patterns
|
|
3794
|
+
* (you cannot `postMessage` to one). When only exact origins are configured
|
|
3795
|
+
* this equals the configured set, so behaviour is unchanged.
|
|
3707
3796
|
*/
|
|
3708
3797
|
private concreteTargets;
|
|
3709
3798
|
private handleRequest;
|
|
3710
3799
|
private respond;
|
|
3711
3800
|
}
|
|
3712
3801
|
|
|
3713
|
-
export { BridgeError, type ContextPayload, type HostContext, type HostHandlers, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type NavigatePayload, type RequestCompleteInfo, type RequestHandlers, type SetSideNavPayload, type SwitchSubsidiaryPayload, type UploadPayload, type UploadProgress, type UploadResponse, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };
|
|
3802
|
+
export { type AuthPayload, BridgeError, type ContextPayload, type HostContext, type HostHandlers, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type NavigatePayload, type PostHogContext, type RequestCompleteInfo, type RequestHandlers, type SetSideNavPayload, type SwitchSubsidiaryPayload, type UploadPayload, type UploadProgress, type UploadResponse, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };
|
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);
|
|
@@ -632,18 +632,20 @@ function rejectUnwired(op) {
|
|
|
632
632
|
var VibeAppHost = class {
|
|
633
633
|
constructor(opts) {
|
|
634
634
|
/**
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
*
|
|
635
|
+
* The connected iframe, as two facets of one thing: `connectedWindow` is its
|
|
636
|
+
* `Window` (identity + the single push target) and `connectedOrigin` is its
|
|
637
|
+
* origin (what you `postMessage` to — you cannot post to a `Window` ref, nor to
|
|
638
|
+
* a glob). Both are written together by {@link handleConnect} (the sole writer)
|
|
639
|
+
* and cleared together on unmount, so they can never diverge.
|
|
640
|
+
*
|
|
641
|
+
* There is deliberately no other window reference. The connected window can
|
|
642
|
+
* only be learned from each `CONNECT` handshake's `event.source`, never at
|
|
643
|
+
* mount: a reloaded/remounted iframe is a *new* `Window` with the same origin,
|
|
644
|
+
* so a mount-time window would be stale. The gate blocks every non-`CONNECT`
|
|
645
|
+
* message whose source isn't `connectedWindow`; every proactive push targets it.
|
|
638
646
|
*/
|
|
639
|
-
this.
|
|
640
|
-
this.
|
|
641
|
-
/**
|
|
642
|
-
* Source windows we've already logged a successful handshake for. The bridge
|
|
643
|
-
* polls `GET_CONTEXT` on an interval until it connects, so without this the
|
|
644
|
-
* "Vibe App connected" line repeats on every poll (SSOT §13 F3).
|
|
645
|
-
*/
|
|
646
|
-
this.handshakeLoggedWindows = /* @__PURE__ */ new WeakSet();
|
|
647
|
+
this.connectedWindow = null;
|
|
648
|
+
this.connectedOrigin = null;
|
|
647
649
|
this.kindHandlers = {
|
|
648
650
|
request: (msg, source, origin) => this.handleRequest(msg, source, origin),
|
|
649
651
|
command: (msg) => this.dispatchCommand(msg)
|
|
@@ -652,6 +654,7 @@ var VibeAppHost = class {
|
|
|
652
654
|
this.trustedOrigins = new Set(trustedOrigins);
|
|
653
655
|
this.exactOrigins = trustedOrigins.filter((entry) => !isOriginPattern(entry));
|
|
654
656
|
this.getContext = getContext;
|
|
657
|
+
this.getPostHog = opts.getPostHog;
|
|
655
658
|
this.appBasePath = appBasePath;
|
|
656
659
|
this.onRequestComplete = opts.onRequestComplete;
|
|
657
660
|
this.rateLimiter = createRateLimiter(opts.rateLimit ?? DEFAULT_RATE_LIMIT);
|
|
@@ -664,7 +667,8 @@ var VibeAppHost = class {
|
|
|
664
667
|
this.commandHandlers = this.buildCommandHandlers(opts);
|
|
665
668
|
}
|
|
666
669
|
dispatchCommand(msg) {
|
|
667
|
-
const
|
|
670
|
+
const handlers = this.commandHandlers;
|
|
671
|
+
const handler = handlers[msg.type];
|
|
668
672
|
if (!handler) {
|
|
669
673
|
console.warn(`[vibe-host] Received unknown command "${msg.type}" \u2014 ignoring.`);
|
|
670
674
|
return;
|
|
@@ -710,31 +714,41 @@ var VibeAppHost = class {
|
|
|
710
714
|
* Starts listening for bridge messages and sets up browser back/forward sync.
|
|
711
715
|
* Returns a cleanup function to call on unmount.
|
|
712
716
|
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
*
|
|
716
|
-
* loaded to trigger the proactive push.
|
|
717
|
+
* Takes no iframe window: the host learns the iframe from its `CONNECT`
|
|
718
|
+
* handshake (and re-learns it on every reload), then delivers context as the
|
|
719
|
+
* reply to that handshake. Just call `mount()` once the host exists.
|
|
717
720
|
*/
|
|
718
|
-
mount(
|
|
719
|
-
if (iframeWindow) this.iframeWindow = iframeWindow;
|
|
721
|
+
mount() {
|
|
720
722
|
window.addEventListener("message", this.boundHandleMessage);
|
|
721
723
|
window.addEventListener("popstate", this.boundHandlePopState);
|
|
722
|
-
if (iframeWindow) this.pushContext(iframeWindow);
|
|
723
724
|
return () => {
|
|
724
725
|
window.removeEventListener("message", this.boundHandleMessage);
|
|
725
726
|
window.removeEventListener("popstate", this.boundHandlePopState);
|
|
726
|
-
this.
|
|
727
|
-
this.
|
|
727
|
+
this.connectedWindow = null;
|
|
728
|
+
this.connectedOrigin = null;
|
|
728
729
|
};
|
|
729
730
|
}
|
|
730
731
|
/**
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
*
|
|
732
|
+
* Re-reads `getContext()` and re-pushes it to the connected iframe, so an open
|
|
733
|
+
* app reacts to a tenant/subsidiary switch or period close without reloading.
|
|
734
|
+
* PostHog is not re-sent (it rides only the connect handshake). No-op until an
|
|
735
|
+
* iframe is mounted.
|
|
736
|
+
*/
|
|
737
|
+
refreshContext() {
|
|
738
|
+
if (!this.connectedWindow) return;
|
|
739
|
+
this.pushContext(this.connectedWindow);
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Notifies the connected iframe of a Nominal auth change — a logout
|
|
743
|
+
* (`authenticated: false`) or an identity/tenant switch (`authenticated: true`
|
|
744
|
+
* with a new `userId`/`tenant`) — via `AUTH_CHANGED`, so the app re-authenticates
|
|
745
|
+
* or signs its own backend out. No-op until the iframe has connected. When auth
|
|
746
|
+
* and context change together, call this *before* {@link refreshContext} ("auth
|
|
747
|
+
* change, then context").
|
|
734
748
|
*/
|
|
735
|
-
|
|
736
|
-
this.
|
|
737
|
-
this.
|
|
749
|
+
notifyAuthChange(auth) {
|
|
750
|
+
if (!this.connectedWindow) return;
|
|
751
|
+
this.pushToIframe("AUTH_CHANGED", auth);
|
|
738
752
|
}
|
|
739
753
|
pushContext(target) {
|
|
740
754
|
const message = {
|
|
@@ -749,14 +763,14 @@ var VibeAppHost = class {
|
|
|
749
763
|
}
|
|
750
764
|
}
|
|
751
765
|
handlePopState() {
|
|
752
|
-
if (!this.
|
|
766
|
+
if (!this.connectedWindow) return;
|
|
753
767
|
const currentPath = window.location.pathname;
|
|
754
768
|
if (!currentPath.startsWith(this.appBasePath)) return;
|
|
755
769
|
const subroute = currentPath.slice(this.appBasePath.length) || "/";
|
|
756
770
|
this.pushToIframe("SUBROUTE_PUSH", { subroute });
|
|
757
771
|
}
|
|
758
772
|
pushToIframe(type, payload) {
|
|
759
|
-
if (!this.
|
|
773
|
+
if (!this.connectedWindow) return;
|
|
760
774
|
const message = {
|
|
761
775
|
__protocol: PROTOCOL_ID,
|
|
762
776
|
__version: PROTOCOL_VERSION,
|
|
@@ -765,44 +779,95 @@ var VibeAppHost = class {
|
|
|
765
779
|
payload
|
|
766
780
|
};
|
|
767
781
|
for (const origin of this.concreteTargets()) {
|
|
768
|
-
this.
|
|
782
|
+
this.connectedWindow.postMessage(message, origin);
|
|
769
783
|
}
|
|
770
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* Sends the host's PostHog config to a freshly connected iframe as a one-time
|
|
787
|
+
* POSTHOG_PUSH. No-op when no `getPostHog` source is configured or it returns
|
|
788
|
+
* nothing (recording disabled). Targets the exact handshaking window/origin
|
|
789
|
+
* (no `concreteTargets` fan-out) since it fires from a validated inbound poll.
|
|
790
|
+
*/
|
|
791
|
+
pushPostHog(target, origin) {
|
|
792
|
+
const payload = this.getPostHog?.();
|
|
793
|
+
if (!payload) return;
|
|
794
|
+
const message = {
|
|
795
|
+
__protocol: PROTOCOL_ID,
|
|
796
|
+
__version: PROTOCOL_VERSION,
|
|
797
|
+
kind: "push",
|
|
798
|
+
type: "POSTHOG_PUSH",
|
|
799
|
+
payload
|
|
800
|
+
};
|
|
801
|
+
target.postMessage(message, origin);
|
|
802
|
+
}
|
|
771
803
|
async handleMessage(event) {
|
|
772
804
|
if (!isOriginAllowed(event.origin, this.trustedOrigins, { allowPatterns: true })) return;
|
|
773
805
|
if (!isBridgeMessage(event.data)) return;
|
|
774
|
-
|
|
775
|
-
|
|
806
|
+
const msg = event.data;
|
|
807
|
+
const source = event.source;
|
|
808
|
+
if (msg.kind === "command" && msg.type === "CONNECT") {
|
|
809
|
+
this.handleConnect(source, event.origin);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
if (source !== this.connectedWindow) {
|
|
813
|
+
if (msg.kind === "request") {
|
|
814
|
+
this.respond(source, event.origin, {
|
|
815
|
+
kind: "response",
|
|
816
|
+
type: msg.type,
|
|
817
|
+
requestId: msg.requestId,
|
|
818
|
+
ok: false,
|
|
819
|
+
error: "Vibe App is not connected \u2014 call connect() first.",
|
|
820
|
+
code: "NOT_CONNECTED"
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
await this.kindHandlers[msg.kind]?.(msg, source, event.origin);
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Handles the CONNECT handshake: records the connecting window/origin (which
|
|
829
|
+
* opens the connection gate for it) and pushes the initial context + one-time
|
|
830
|
+
* PostHog config. Only the first CONNECT per origin pushes — later retries are
|
|
831
|
+
* ignored, so the bridge receives exactly one initial CONTEXT_PUSH (no
|
|
832
|
+
* spurious onContextChange). A reloaded iframe is a new window and re-connects
|
|
833
|
+
* normally. If `getContext()` throws, the window is left unconnected so the
|
|
834
|
+
* bridge's retry can recover.
|
|
835
|
+
*/
|
|
836
|
+
handleConnect(source, origin) {
|
|
837
|
+
if (source === this.connectedWindow) return;
|
|
838
|
+
try {
|
|
839
|
+
const ctx = { ...this.getContext(), hostVersion: version };
|
|
840
|
+
this.connectedWindow = source;
|
|
841
|
+
this.connectedOrigin = origin;
|
|
842
|
+
console.log(
|
|
843
|
+
`[vibe-host] Vibe App connected \u2014 host: ${version}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
844
|
+
);
|
|
845
|
+
const message = {
|
|
846
|
+
__protocol: PROTOCOL_ID,
|
|
847
|
+
__version: PROTOCOL_VERSION,
|
|
848
|
+
kind: "push",
|
|
849
|
+
type: "CONTEXT_PUSH",
|
|
850
|
+
payload: ctx
|
|
851
|
+
};
|
|
852
|
+
source.postMessage(message, origin);
|
|
853
|
+
this.pushPostHog(source, origin);
|
|
854
|
+
} catch (err) {
|
|
855
|
+
console.error("[vibe-host] getContext threw during connect \u2014 context not delivered.", err);
|
|
856
|
+
}
|
|
776
857
|
}
|
|
777
858
|
/**
|
|
778
859
|
* Concrete origins a proactive push may target: the exact allowlist entries
|
|
779
|
-
* plus
|
|
780
|
-
*
|
|
781
|
-
*
|
|
860
|
+
* plus the connected iframe's learned origin. Never includes glob patterns
|
|
861
|
+
* (you cannot `postMessage` to one). When only exact origins are configured
|
|
862
|
+
* this equals the configured set, so behaviour is unchanged.
|
|
782
863
|
*/
|
|
783
864
|
concreteTargets() {
|
|
784
|
-
|
|
865
|
+
const targets = new Set(this.exactOrigins);
|
|
866
|
+
if (this.connectedOrigin) targets.add(this.connectedOrigin);
|
|
867
|
+
return targets;
|
|
785
868
|
}
|
|
786
869
|
async handleRequest(msg, source, origin) {
|
|
787
870
|
const { requestId, type } = msg;
|
|
788
|
-
if (type === "GET_CONTEXT") {
|
|
789
|
-
try {
|
|
790
|
-
const ctx = { ...this.getContext(), hostVersion: version };
|
|
791
|
-
if (!this.handshakeLoggedWindows.has(source)) {
|
|
792
|
-
this.handshakeLoggedWindows.add(source);
|
|
793
|
-
const bridgeVersion = msg.payload?.bridgeVersion ?? "unknown";
|
|
794
|
-
console.log(
|
|
795
|
-
`[vibe-host] Vibe App connected \u2014 bridge: ${bridgeVersion}, host: ${version}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
796
|
-
);
|
|
797
|
-
}
|
|
798
|
-
this.respond(source, origin, { kind: "response", type, requestId, ok: true, data: ctx });
|
|
799
|
-
} catch (err) {
|
|
800
|
-
const error = err instanceof Error ? err.message : String(err);
|
|
801
|
-
const code = err instanceof BridgeError ? err.code : "CONTEXT_ERROR";
|
|
802
|
-
this.respond(source, origin, { kind: "response", type, requestId, ok: false, error, code });
|
|
803
|
-
}
|
|
804
|
-
return;
|
|
805
|
-
}
|
|
806
871
|
if (!checkRateLimit(this.rateLimiter)) {
|
|
807
872
|
this.respond(source, origin, {
|
|
808
873
|
kind: "response",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nominalso/vibe-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Host-side SDK for embedding Nominal Vibe Apps — receives bridge requests over a typed postMessage protocol and dispatches them to Nominal APIs (used by nom-ui).",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://github.com/nominalso/vibe-apps-sdk#readme",
|