@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 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 must match exactly.
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.2.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.trustedOrigins) {
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.trustedOrigins) {
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 (!this.trustedOrigins.has(event.origin)) return;
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 these origins. Must match
3553
- * the Vibe App's origin exactly (e.g. `'https://my-vibe-app.lovable.app'`).
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 these origins. Must match
3553
- * the Vibe App's origin exactly (e.g. `'https://my-vibe-app.lovable.app'`).
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.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.trustedOrigins) {
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.trustedOrigins) {
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 (!this.trustedOrigins.has(event.origin)) return;
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.2.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",