@nominalso/vibe-host 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/README.md +21 -7
- package/dist/index.cjs +49 -4
- package/dist/index.d.cts +92 -3
- package/dist/index.d.ts +92 -3
- package/dist/index.js +49 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,7 +18,8 @@ Externalizes only `@hey-api/client-fetch`; TypeScript types are self-contained.
|
|
|
18
18
|
import { VibeAppHost } from '@nominalso/vibe-host'
|
|
19
19
|
|
|
20
20
|
const host = new VibeAppHost({
|
|
21
|
-
// Iframe origin(s) allowed to talk to this host
|
|
21
|
+
// Iframe origin(s) allowed to talk to this host. Exact origins, or glob
|
|
22
|
+
// patterns (e.g. 'https://*.vercel.app') — add patterns only in non-prod.
|
|
22
23
|
trustedOrigins: ['https://my-vibe-app.lovable.app'],
|
|
23
24
|
// This Vibe App's base path in nom-ui (used to sync the browser URL).
|
|
24
25
|
appBasePath: `/${tenant}/${subsidiary}/apps/${slug}`,
|
|
@@ -37,15 +38,28 @@ const unmount = host.mount()
|
|
|
37
38
|
|
|
38
39
|
### `new VibeAppHost(options)`
|
|
39
40
|
|
|
40
|
-
| Option | Type | Description
|
|
41
|
-
| ---------------- | ----------------------- |
|
|
42
|
-
| `trustedOrigins` | `string[]` | Iframe origins allowed to talk to this host. Messages from other origins are ignored.
|
|
43
|
-
| `getContext` | `() => HostContext` | Returns the current context to send to the iframe (`hostVersion` is injected automatically).
|
|
44
|
-
| `appBasePath` | `string` | Base URL path of this Vibe App in nom-ui, e.g. `/acme/1/apps/fixed-assets`.
|
|
45
|
-
| `clientConfig` | `VibeApiClientOptions?` | Points the built-in handler map at the Nominal API. Defaults to `{ baseUrl: '', stripApiPrefix: false }`.
|
|
41
|
+
| Option | Type | Description |
|
|
42
|
+
| ---------------- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| `trustedOrigins` | `string[]` | Iframe origins allowed to talk to this host — exact origins or glob patterns (`https://*.vercel.app`, honoured unconditionally, so add only in non-prod). Messages from other origins are ignored. |
|
|
44
|
+
| `getContext` | `() => HostContext` | Returns the current context to send to the iframe (`hostVersion` is injected automatically). |
|
|
45
|
+
| `appBasePath` | `string` | Base URL path of this Vibe App in nom-ui, e.g. `/acme/1/apps/fixed-assets`. |
|
|
46
|
+
| `clientConfig` | `VibeApiClientOptions?` | Points the built-in handler map at the Nominal API. Defaults to `{ baseUrl: '', stripApiPrefix: false }`. |
|
|
46
47
|
|
|
47
48
|
`VibeApiClientOptions` is `{ baseUrl: string; stripApiPrefix?: boolean }`. Set `stripApiPrefix: true` when routing through a proxy whose convention expects paths without the `/api` prefix.
|
|
48
49
|
|
|
50
|
+
#### Testing preview deployments with `trustedOrigins` patterns
|
|
51
|
+
|
|
52
|
+
A pattern entry's `*` matches exactly one DNS label or port — anchored, scheme literal — so `https://*.vercel.app` matches `https://pr-7.vercel.app` but not `https://a.b.vercel.app` or `https://pr-7.vercel.app.evil.com`. Patterns are honoured unconditionally, so **gate the list on your own deploy environment** — keep production exact-only:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const trustedOrigins =
|
|
56
|
+
process.env.NOM_ENV === 'production'
|
|
57
|
+
? ['https://app.nominal.so']
|
|
58
|
+
: ['https://app.nominal.so', 'https://*.vercel.app', 'https://*.lovable.app']
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Because you cannot `postMessage` to a pattern, proactive context/subroute pushes target the exact entries plus any origin seen on a validated inbound message; a pattern-matched iframe still receives context via its `connect()` poll response.
|
|
62
|
+
|
|
49
63
|
### `mount(iframeWindow?): () => void`
|
|
50
64
|
|
|
51
65
|
Starts listening for bridge messages and sets up browser back/forward sync. Returns a cleanup function. Pass `iframeWindow` to immediately push context; if omitted, the host answers `GET_CONTEXT` polls but won't proactively push until you call `pushContextTo`.
|
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.4.0";
|
|
31
31
|
|
|
32
32
|
// ../protocol-types/dist/index.js
|
|
33
33
|
function normalizeSubroute(subroute) {
|
|
@@ -72,6 +72,33 @@ var HttpBridgeError = class extends BridgeError {
|
|
|
72
72
|
this.name = "HttpBridgeError";
|
|
73
73
|
}
|
|
74
74
|
};
|
|
75
|
+
function isOriginPattern(entry) {
|
|
76
|
+
return entry.includes("*");
|
|
77
|
+
}
|
|
78
|
+
var patternCache = /* @__PURE__ */ new Map();
|
|
79
|
+
function patternToRegExp(pattern) {
|
|
80
|
+
const cached = patternCache.get(pattern);
|
|
81
|
+
if (cached) return cached;
|
|
82
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
83
|
+
const body = escaped.replace(/\\\*/g, "[^./:@?#\\s]+");
|
|
84
|
+
const compiled = new RegExp(`^${body}$`);
|
|
85
|
+
patternCache.set(pattern, compiled);
|
|
86
|
+
return compiled;
|
|
87
|
+
}
|
|
88
|
+
function matchesOrigin(origin, entry) {
|
|
89
|
+
if (!isOriginPattern(entry)) return origin === entry;
|
|
90
|
+
return patternToRegExp(entry).test(origin);
|
|
91
|
+
}
|
|
92
|
+
function isOriginAllowed(origin, allowlist, { allowPatterns }) {
|
|
93
|
+
for (const entry of allowlist) {
|
|
94
|
+
if (isOriginPattern(entry)) {
|
|
95
|
+
if (allowPatterns && matchesOrigin(origin, entry)) return true;
|
|
96
|
+
} else if (origin === entry) {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
75
102
|
|
|
76
103
|
// ../api-client/dist/index.js
|
|
77
104
|
var import_client_fetch = require("@hey-api/client-fetch");
|
|
@@ -629,6 +656,12 @@ function rejectUnwired(op) {
|
|
|
629
656
|
// src/VibeAppHost.ts
|
|
630
657
|
var VibeAppHost = class {
|
|
631
658
|
constructor(opts) {
|
|
659
|
+
/**
|
|
660
|
+
* Concrete iframe origins observed on validated inbound messages. Because you
|
|
661
|
+
* cannot `postMessage` to a glob pattern, proactive pushes target these
|
|
662
|
+
* learned origins (plus the exact allowlist entries) rather than the patterns.
|
|
663
|
+
*/
|
|
664
|
+
this.connectedOrigins = /* @__PURE__ */ new Set();
|
|
632
665
|
this.iframeWindow = null;
|
|
633
666
|
/**
|
|
634
667
|
* Source windows we've already logged a successful handshake for. The bridge
|
|
@@ -642,6 +675,7 @@ var VibeAppHost = class {
|
|
|
642
675
|
};
|
|
643
676
|
const { trustedOrigins, getContext, appBasePath, clientConfig } = opts;
|
|
644
677
|
this.trustedOrigins = new Set(trustedOrigins);
|
|
678
|
+
this.exactOrigins = trustedOrigins.filter((entry) => !isOriginPattern(entry));
|
|
645
679
|
this.getContext = getContext;
|
|
646
680
|
this.appBasePath = appBasePath;
|
|
647
681
|
this.onRequestComplete = opts.onRequestComplete;
|
|
@@ -715,6 +749,7 @@ var VibeAppHost = class {
|
|
|
715
749
|
window.removeEventListener("message", this.boundHandleMessage);
|
|
716
750
|
window.removeEventListener("popstate", this.boundHandlePopState);
|
|
717
751
|
this.iframeWindow = null;
|
|
752
|
+
this.connectedOrigins.clear();
|
|
718
753
|
};
|
|
719
754
|
}
|
|
720
755
|
/**
|
|
@@ -734,7 +769,7 @@ var VibeAppHost = class {
|
|
|
734
769
|
type: "CONTEXT_PUSH",
|
|
735
770
|
payload: { ...this.getContext(), hostVersion: version }
|
|
736
771
|
};
|
|
737
|
-
for (const origin of this.
|
|
772
|
+
for (const origin of this.concreteTargets()) {
|
|
738
773
|
target.postMessage(message, origin);
|
|
739
774
|
}
|
|
740
775
|
}
|
|
@@ -754,15 +789,25 @@ var VibeAppHost = class {
|
|
|
754
789
|
type,
|
|
755
790
|
payload
|
|
756
791
|
};
|
|
757
|
-
for (const origin of this.
|
|
792
|
+
for (const origin of this.concreteTargets()) {
|
|
758
793
|
this.iframeWindow.postMessage(message, origin);
|
|
759
794
|
}
|
|
760
795
|
}
|
|
761
796
|
async handleMessage(event) {
|
|
762
|
-
if (!
|
|
797
|
+
if (!isOriginAllowed(event.origin, this.trustedOrigins, { allowPatterns: true })) return;
|
|
763
798
|
if (!isBridgeMessage(event.data)) return;
|
|
799
|
+
this.connectedOrigins.add(event.origin);
|
|
764
800
|
await this.kindHandlers[event.data.kind]?.(event.data, event.source, event.origin);
|
|
765
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* Concrete origins a proactive push may target: the exact allowlist entries
|
|
804
|
+
* plus any origins seen on validated inbound messages. Never includes glob
|
|
805
|
+
* patterns (you cannot `postMessage` to one). When only exact origins are
|
|
806
|
+
* configured this equals the configured set, so behaviour is unchanged.
|
|
807
|
+
*/
|
|
808
|
+
concreteTargets() {
|
|
809
|
+
return /* @__PURE__ */ new Set([...this.exactOrigins, ...this.connectedOrigins]);
|
|
810
|
+
}
|
|
766
811
|
async handleRequest(msg, source, origin) {
|
|
767
812
|
const { requestId, type } = msg;
|
|
768
813
|
if (type === "GET_CONTEXT") {
|
package/dist/index.d.cts
CHANGED
|
@@ -3180,6 +3180,54 @@ interface BridgeSubsidiary {
|
|
|
3180
3180
|
id: number;
|
|
3181
3181
|
displayName: string;
|
|
3182
3182
|
}
|
|
3183
|
+
/**
|
|
3184
|
+
* PostHog configuration the host supplies so the iframe can record a session
|
|
3185
|
+
* replay (stitched into the host's recording via `recordCrossOriginIframes`)
|
|
3186
|
+
* and emit custom events, all attributed to the **same PostHog person** as
|
|
3187
|
+
* nom-ui.
|
|
3188
|
+
*
|
|
3189
|
+
* The host (nom-ui) is the single source of truth for these values — it knows
|
|
3190
|
+
* the project key, the env-aware ingestion host, and the resolved person — and
|
|
3191
|
+
* delivers them over the bridge so the iframe never hardcodes any of it. When
|
|
3192
|
+
* the host omits the {@link ContextPayload.posthog} block entirely (older host,
|
|
3193
|
+
* or recording disabled for this env/app), 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
|
+
}
|
|
3183
3231
|
interface ContextPayload {
|
|
3184
3232
|
tenant: string;
|
|
3185
3233
|
subsidiaryId: number;
|
|
@@ -3192,6 +3240,13 @@ interface ContextPayload {
|
|
|
3192
3240
|
subroute?: string;
|
|
3193
3241
|
/** Slug of the last closed accounting period, e.g. "mar-2026". */
|
|
3194
3242
|
lastClosedPeriodSlug?: string;
|
|
3243
|
+
/**
|
|
3244
|
+
* PostHog config for in-iframe session recording + custom events. **Absent
|
|
3245
|
+
* when recording is off** (older host, or disabled for this env/app), in which
|
|
3246
|
+
* case the bridge initializes no analytics. Optional so old hosts/bridges stay
|
|
3247
|
+
* compatible.
|
|
3248
|
+
*/
|
|
3249
|
+
posthog?: PostHogContext;
|
|
3195
3250
|
/** Version of the host SDK (@nominalso/vibe-host) running in nom-ui. */
|
|
3196
3251
|
hostVersion: string;
|
|
3197
3252
|
}
|
|
@@ -3549,8 +3604,21 @@ interface RequestCompleteInfo {
|
|
|
3549
3604
|
interface VibeAppHostOptions {
|
|
3550
3605
|
/**
|
|
3551
3606
|
* Iframe origins allowed to talk to this host. Messages from any other origin
|
|
3552
|
-
* are ignored, and context/pushes are only sent to
|
|
3553
|
-
*
|
|
3607
|
+
* are ignored, and context/pushes are only sent to allowed origins.
|
|
3608
|
+
*
|
|
3609
|
+
* Each entry is either an exact origin (`'https://my-vibe-app.lovable.app'`)
|
|
3610
|
+
* or a **glob pattern** containing `*` (`'https://*.vercel.app'`), where `*`
|
|
3611
|
+
* matches exactly one DNS label or port — anchored, scheme literal — so
|
|
3612
|
+
* `https://*.vercel.app` matches `https://pr-7.vercel.app` but not
|
|
3613
|
+
* `https://a.b.vercel.app` or `https://pr-7.vercel.app.evil.com`.
|
|
3614
|
+
*
|
|
3615
|
+
* Patterns are honoured unconditionally, so **only add them for non-production
|
|
3616
|
+
* environments** — gate this list on your own deploy env (e.g. pass exact
|
|
3617
|
+
* origins only in production, exact + `*.vercel.app`/`*.lovable.app` in
|
|
3618
|
+
* staging/dev) to test against dynamic preview deployments. Because you cannot
|
|
3619
|
+
* `postMessage` to a pattern, proactive context/subroute pushes target the
|
|
3620
|
+
* exact entries plus any origin seen on a validated inbound message; pattern-
|
|
3621
|
+
* matched iframes still receive context via their `connect()` poll response.
|
|
3554
3622
|
*/
|
|
3555
3623
|
trustedOrigins: string[];
|
|
3556
3624
|
/** Returns the current context to send to the iframe on connect. */
|
|
@@ -3621,7 +3689,21 @@ interface VibeAppHostOptions {
|
|
|
3621
3689
|
* ```
|
|
3622
3690
|
*/
|
|
3623
3691
|
declare class VibeAppHost {
|
|
3692
|
+
/**
|
|
3693
|
+
* Configured allowlist. Entries may be exact origins or glob patterns
|
|
3694
|
+
* (`https://*.vercel.app`); inbound messages are validated against it with
|
|
3695
|
+
* {@link isOriginAllowed}. Pattern entries are honoured — nom-ui is expected
|
|
3696
|
+
* to supply patterns only in non-production environments.
|
|
3697
|
+
*/
|
|
3624
3698
|
private readonly trustedOrigins;
|
|
3699
|
+
/** The exact (non-pattern) subset of {@link trustedOrigins} — see {@link concreteTargets}. */
|
|
3700
|
+
private readonly exactOrigins;
|
|
3701
|
+
/**
|
|
3702
|
+
* Concrete iframe origins observed on validated inbound messages. Because you
|
|
3703
|
+
* cannot `postMessage` to a glob pattern, proactive pushes target these
|
|
3704
|
+
* learned origins (plus the exact allowlist entries) rather than the patterns.
|
|
3705
|
+
*/
|
|
3706
|
+
private readonly connectedOrigins;
|
|
3625
3707
|
private readonly handlers;
|
|
3626
3708
|
private readonly commandHandlers;
|
|
3627
3709
|
private readonly getContext;
|
|
@@ -3672,8 +3754,15 @@ declare class VibeAppHost {
|
|
|
3672
3754
|
private handlePopState;
|
|
3673
3755
|
private pushToIframe;
|
|
3674
3756
|
private handleMessage;
|
|
3757
|
+
/**
|
|
3758
|
+
* Concrete origins a proactive push may target: the exact allowlist entries
|
|
3759
|
+
* plus any origins seen on validated inbound messages. Never includes glob
|
|
3760
|
+
* patterns (you cannot `postMessage` to one). When only exact origins are
|
|
3761
|
+
* configured this equals the configured set, so behaviour is unchanged.
|
|
3762
|
+
*/
|
|
3763
|
+
private concreteTargets;
|
|
3675
3764
|
private handleRequest;
|
|
3676
3765
|
private respond;
|
|
3677
3766
|
}
|
|
3678
3767
|
|
|
3679
|
-
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 };
|
|
3768
|
+
export { 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
|
@@ -3180,6 +3180,54 @@ interface BridgeSubsidiary {
|
|
|
3180
3180
|
id: number;
|
|
3181
3181
|
displayName: string;
|
|
3182
3182
|
}
|
|
3183
|
+
/**
|
|
3184
|
+
* PostHog configuration the host supplies so the iframe can record a session
|
|
3185
|
+
* replay (stitched into the host's recording via `recordCrossOriginIframes`)
|
|
3186
|
+
* and emit custom events, all attributed to the **same PostHog person** as
|
|
3187
|
+
* nom-ui.
|
|
3188
|
+
*
|
|
3189
|
+
* The host (nom-ui) is the single source of truth for these values — it knows
|
|
3190
|
+
* the project key, the env-aware ingestion host, and the resolved person — and
|
|
3191
|
+
* delivers them over the bridge so the iframe never hardcodes any of it. When
|
|
3192
|
+
* the host omits the {@link ContextPayload.posthog} block entirely (older host,
|
|
3193
|
+
* or recording disabled for this env/app), 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
|
+
}
|
|
3183
3231
|
interface ContextPayload {
|
|
3184
3232
|
tenant: string;
|
|
3185
3233
|
subsidiaryId: number;
|
|
@@ -3192,6 +3240,13 @@ interface ContextPayload {
|
|
|
3192
3240
|
subroute?: string;
|
|
3193
3241
|
/** Slug of the last closed accounting period, e.g. "mar-2026". */
|
|
3194
3242
|
lastClosedPeriodSlug?: string;
|
|
3243
|
+
/**
|
|
3244
|
+
* PostHog config for in-iframe session recording + custom events. **Absent
|
|
3245
|
+
* when recording is off** (older host, or disabled for this env/app), in which
|
|
3246
|
+
* case the bridge initializes no analytics. Optional so old hosts/bridges stay
|
|
3247
|
+
* compatible.
|
|
3248
|
+
*/
|
|
3249
|
+
posthog?: PostHogContext;
|
|
3195
3250
|
/** Version of the host SDK (@nominalso/vibe-host) running in nom-ui. */
|
|
3196
3251
|
hostVersion: string;
|
|
3197
3252
|
}
|
|
@@ -3549,8 +3604,21 @@ interface RequestCompleteInfo {
|
|
|
3549
3604
|
interface VibeAppHostOptions {
|
|
3550
3605
|
/**
|
|
3551
3606
|
* Iframe origins allowed to talk to this host. Messages from any other origin
|
|
3552
|
-
* are ignored, and context/pushes are only sent to
|
|
3553
|
-
*
|
|
3607
|
+
* are ignored, and context/pushes are only sent to allowed origins.
|
|
3608
|
+
*
|
|
3609
|
+
* Each entry is either an exact origin (`'https://my-vibe-app.lovable.app'`)
|
|
3610
|
+
* or a **glob pattern** containing `*` (`'https://*.vercel.app'`), where `*`
|
|
3611
|
+
* matches exactly one DNS label or port — anchored, scheme literal — so
|
|
3612
|
+
* `https://*.vercel.app` matches `https://pr-7.vercel.app` but not
|
|
3613
|
+
* `https://a.b.vercel.app` or `https://pr-7.vercel.app.evil.com`.
|
|
3614
|
+
*
|
|
3615
|
+
* Patterns are honoured unconditionally, so **only add them for non-production
|
|
3616
|
+
* environments** — gate this list on your own deploy env (e.g. pass exact
|
|
3617
|
+
* origins only in production, exact + `*.vercel.app`/`*.lovable.app` in
|
|
3618
|
+
* staging/dev) to test against dynamic preview deployments. Because you cannot
|
|
3619
|
+
* `postMessage` to a pattern, proactive context/subroute pushes target the
|
|
3620
|
+
* exact entries plus any origin seen on a validated inbound message; pattern-
|
|
3621
|
+
* matched iframes still receive context via their `connect()` poll response.
|
|
3554
3622
|
*/
|
|
3555
3623
|
trustedOrigins: string[];
|
|
3556
3624
|
/** Returns the current context to send to the iframe on connect. */
|
|
@@ -3621,7 +3689,21 @@ interface VibeAppHostOptions {
|
|
|
3621
3689
|
* ```
|
|
3622
3690
|
*/
|
|
3623
3691
|
declare class VibeAppHost {
|
|
3692
|
+
/**
|
|
3693
|
+
* Configured allowlist. Entries may be exact origins or glob patterns
|
|
3694
|
+
* (`https://*.vercel.app`); inbound messages are validated against it with
|
|
3695
|
+
* {@link isOriginAllowed}. Pattern entries are honoured — nom-ui is expected
|
|
3696
|
+
* to supply patterns only in non-production environments.
|
|
3697
|
+
*/
|
|
3624
3698
|
private readonly trustedOrigins;
|
|
3699
|
+
/** The exact (non-pattern) subset of {@link trustedOrigins} — see {@link concreteTargets}. */
|
|
3700
|
+
private readonly exactOrigins;
|
|
3701
|
+
/**
|
|
3702
|
+
* Concrete iframe origins observed on validated inbound messages. Because you
|
|
3703
|
+
* cannot `postMessage` to a glob pattern, proactive pushes target these
|
|
3704
|
+
* learned origins (plus the exact allowlist entries) rather than the patterns.
|
|
3705
|
+
*/
|
|
3706
|
+
private readonly connectedOrigins;
|
|
3625
3707
|
private readonly handlers;
|
|
3626
3708
|
private readonly commandHandlers;
|
|
3627
3709
|
private readonly getContext;
|
|
@@ -3672,8 +3754,15 @@ declare class VibeAppHost {
|
|
|
3672
3754
|
private handlePopState;
|
|
3673
3755
|
private pushToIframe;
|
|
3674
3756
|
private handleMessage;
|
|
3757
|
+
/**
|
|
3758
|
+
* Concrete origins a proactive push may target: the exact allowlist entries
|
|
3759
|
+
* plus any origins seen on validated inbound messages. Never includes glob
|
|
3760
|
+
* patterns (you cannot `postMessage` to one). When only exact origins are
|
|
3761
|
+
* configured this equals the configured set, so behaviour is unchanged.
|
|
3762
|
+
*/
|
|
3763
|
+
private concreteTargets;
|
|
3675
3764
|
private handleRequest;
|
|
3676
3765
|
private respond;
|
|
3677
3766
|
}
|
|
3678
3767
|
|
|
3679
|
-
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 };
|
|
3768
|
+
export { 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.4.0";
|
|
3
3
|
|
|
4
4
|
// ../protocol-types/dist/index.js
|
|
5
5
|
function normalizeSubroute(subroute) {
|
|
@@ -44,6 +44,33 @@ 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
|
+
}
|
|
47
74
|
|
|
48
75
|
// ../api-client/dist/index.js
|
|
49
76
|
import {
|
|
@@ -604,6 +631,12 @@ function rejectUnwired(op) {
|
|
|
604
631
|
// src/VibeAppHost.ts
|
|
605
632
|
var VibeAppHost = class {
|
|
606
633
|
constructor(opts) {
|
|
634
|
+
/**
|
|
635
|
+
* Concrete iframe origins observed on validated inbound messages. Because you
|
|
636
|
+
* cannot `postMessage` to a glob pattern, proactive pushes target these
|
|
637
|
+
* learned origins (plus the exact allowlist entries) rather than the patterns.
|
|
638
|
+
*/
|
|
639
|
+
this.connectedOrigins = /* @__PURE__ */ new Set();
|
|
607
640
|
this.iframeWindow = null;
|
|
608
641
|
/**
|
|
609
642
|
* Source windows we've already logged a successful handshake for. The bridge
|
|
@@ -617,6 +650,7 @@ var VibeAppHost = class {
|
|
|
617
650
|
};
|
|
618
651
|
const { trustedOrigins, getContext, appBasePath, clientConfig } = opts;
|
|
619
652
|
this.trustedOrigins = new Set(trustedOrigins);
|
|
653
|
+
this.exactOrigins = trustedOrigins.filter((entry) => !isOriginPattern(entry));
|
|
620
654
|
this.getContext = getContext;
|
|
621
655
|
this.appBasePath = appBasePath;
|
|
622
656
|
this.onRequestComplete = opts.onRequestComplete;
|
|
@@ -690,6 +724,7 @@ var VibeAppHost = class {
|
|
|
690
724
|
window.removeEventListener("message", this.boundHandleMessage);
|
|
691
725
|
window.removeEventListener("popstate", this.boundHandlePopState);
|
|
692
726
|
this.iframeWindow = null;
|
|
727
|
+
this.connectedOrigins.clear();
|
|
693
728
|
};
|
|
694
729
|
}
|
|
695
730
|
/**
|
|
@@ -709,7 +744,7 @@ var VibeAppHost = class {
|
|
|
709
744
|
type: "CONTEXT_PUSH",
|
|
710
745
|
payload: { ...this.getContext(), hostVersion: version }
|
|
711
746
|
};
|
|
712
|
-
for (const origin of this.
|
|
747
|
+
for (const origin of this.concreteTargets()) {
|
|
713
748
|
target.postMessage(message, origin);
|
|
714
749
|
}
|
|
715
750
|
}
|
|
@@ -729,15 +764,25 @@ var VibeAppHost = class {
|
|
|
729
764
|
type,
|
|
730
765
|
payload
|
|
731
766
|
};
|
|
732
|
-
for (const origin of this.
|
|
767
|
+
for (const origin of this.concreteTargets()) {
|
|
733
768
|
this.iframeWindow.postMessage(message, origin);
|
|
734
769
|
}
|
|
735
770
|
}
|
|
736
771
|
async handleMessage(event) {
|
|
737
|
-
if (!
|
|
772
|
+
if (!isOriginAllowed(event.origin, this.trustedOrigins, { allowPatterns: true })) return;
|
|
738
773
|
if (!isBridgeMessage(event.data)) return;
|
|
774
|
+
this.connectedOrigins.add(event.origin);
|
|
739
775
|
await this.kindHandlers[event.data.kind]?.(event.data, event.source, event.origin);
|
|
740
776
|
}
|
|
777
|
+
/**
|
|
778
|
+
* Concrete origins a proactive push may target: the exact allowlist entries
|
|
779
|
+
* plus any origins seen on validated inbound messages. Never includes glob
|
|
780
|
+
* patterns (you cannot `postMessage` to one). When only exact origins are
|
|
781
|
+
* configured this equals the configured set, so behaviour is unchanged.
|
|
782
|
+
*/
|
|
783
|
+
concreteTargets() {
|
|
784
|
+
return /* @__PURE__ */ new Set([...this.exactOrigins, ...this.connectedOrigins]);
|
|
785
|
+
}
|
|
741
786
|
async handleRequest(msg, source, origin) {
|
|
742
787
|
const { requestId, type } = msg;
|
|
743
788
|
if (type === "GET_CONTEXT") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nominalso/vibe-host",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|