@nominalso/vibe-bridge 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- var version = "0.2.0";
1
+ var version = "0.4.0";
2
2
 
3
3
  type AccountingAccountDimensionValues = {
4
4
  account_id: string;
@@ -3182,6 +3182,54 @@ interface BridgeSubsidiary {
3182
3182
  id: number;
3183
3183
  displayName: string;
3184
3184
  }
3185
+ /**
3186
+ * PostHog configuration the host supplies so the iframe can record a session
3187
+ * replay (stitched into the host's recording via `recordCrossOriginIframes`)
3188
+ * and emit custom events, all attributed to the **same PostHog person** as
3189
+ * nom-ui.
3190
+ *
3191
+ * The host (nom-ui) is the single source of truth for these values — it knows
3192
+ * the project key, the env-aware ingestion host, and the resolved person — and
3193
+ * delivers them over the bridge so the iframe never hardcodes any of it. When
3194
+ * the host omits the {@link ContextPayload.posthog} block entirely (older host,
3195
+ * or recording disabled for this env/app), the bridge initializes nothing.
3196
+ */
3197
+ interface PostHogContext {
3198
+ /** Project API key — the same PostHog project the host (nom-ui) uses. */
3199
+ token: string;
3200
+ /**
3201
+ * **Absolute** ingestion host, e.g. `"https://us.i.posthog.com"`.
3202
+ *
3203
+ * Must NOT be the host's relative `/ingest` reverse-proxy path: a relative
3204
+ * path resolves against the iframe's *own* (cross-origin) host and 404s.
3205
+ */
3206
+ apiHost: string;
3207
+ /**
3208
+ * Host-resolved PostHog distinct id. The bridge calls `identify()` with it so
3209
+ * the iframe's recording and events attribute to the same person as the host.
3210
+ */
3211
+ distinctId: string;
3212
+ /**
3213
+ * **Optional.** The host's current PostHog session id (from
3214
+ * `posthog.get_session_id()`). When provided, the bridge seeds it as the
3215
+ * iframe instance's session via `bootstrap.sessionID`, so custom events from
3216
+ * `bridge.track()` share the **same `$session_id`** as the host — letting them
3217
+ * line up with the stitched session replay on one timeline.
3218
+ *
3219
+ * Not needed for *replay* stitching itself (that works via
3220
+ * `recordCrossOriginIframes` on both sides regardless). Must be a valid UUID
3221
+ * v7 — PostHog session ids already are. It is a one-time seed at connect, so
3222
+ * if the host's session later rotates the two can diverge; alignment is
3223
+ * best-effort for the session in progress.
3224
+ */
3225
+ sessionId?: string;
3226
+ /**
3227
+ * Master switch, env- and/or app-gated by the host. The bridge only calls
3228
+ * `posthog.init()` when this is `true`; it stays the kill-switch even though
3229
+ * `posthog-js` is bundled into the bridge.
3230
+ */
3231
+ enabled: boolean;
3232
+ }
3185
3233
  interface ContextPayload {
3186
3234
  tenant: string;
3187
3235
  subsidiaryId: number;
@@ -3194,6 +3242,13 @@ interface ContextPayload {
3194
3242
  subroute?: string;
3195
3243
  /** Slug of the last closed accounting period, e.g. "mar-2026". */
3196
3244
  lastClosedPeriodSlug?: string;
3245
+ /**
3246
+ * PostHog config for in-iframe session recording + custom events. **Absent
3247
+ * when recording is off** (older host, or disabled for this env/app), in which
3248
+ * case the bridge initializes no analytics. Optional so old hosts/bridges stay
3249
+ * compatible.
3250
+ */
3251
+ posthog?: PostHogContext;
3197
3252
  /** Version of the host SDK (@nominalso/vibe-host) running in nom-ui. */
3198
3253
  hostVersion: string;
3199
3254
  }
@@ -3659,13 +3714,34 @@ declare abstract class BridgeDataMethods {
3659
3714
 
3660
3715
  interface VibeAppBridgeOptions {
3661
3716
  /**
3662
- * Origin of the Nominal app embedding this iframe (e.g.
3663
- * `https://app.nominal.so` or `http://localhost:3000`). Must match the host
3664
- * origin **exactly** messages from any other origin are ignored, and the
3665
- * bridge only `postMessage`s to this origin. Drive it from an env var so the
3666
- * same code works locally and in production.
3717
+ * Origin(s) of the Nominal app embedding this iframe. **Optional.**
3718
+ *
3719
+ * **Omit it** (the recommended default) and the bridge auto-detects the
3720
+ * Nominal page that actually embeds it and talks only to that origin. This is
3721
+ * safe because Nominal serves Vibe Apps behind a `frame-ancestors` CSP, so
3722
+ * only nom-ui can frame the app — a hostile site can't embed it to intercept
3723
+ * what the iframe sends out (`postTaskOutput` bodies, file uploads). Most apps
3724
+ * need nothing here.
3725
+ *
3726
+ * Provide a value to **pin explicitly** — defense in depth, or when deploying
3727
+ * outside Nominal's edge:
3728
+ *
3729
+ * ```ts
3730
+ * parentOrigin: 'https://app.nominal.so' // one exact origin
3731
+ * parentOrigin: ['https://app.nominal.so', 'https://*.vercel.app'] // exact + preview pattern
3732
+ * ```
3733
+ *
3734
+ * A pattern's `*` matches exactly one DNS label or port — anchored, scheme
3735
+ * literal — so `https://*.vercel.app` matches `https://pr-7.vercel.app` but
3736
+ * not `https://a.b.vercel.app` or `https://pr-7.vercel.app.evil.com`. Patterns
3737
+ * are honoured only when this app's own page is served from a recognised
3738
+ * preview host (`*.vercel.app`, `*.lovable.app`, `localhost`, …); on a
3739
+ * production custom domain only exact origins match.
3740
+ *
3741
+ * If neither an explicit value nor an auto-detected parent is available,
3742
+ * `connect()` rejects with `BridgeError` code `PARENT_ORIGIN_UNRESOLVED`.
3667
3743
  */
3668
- parentOrigin: string;
3744
+ parentOrigin?: string | string[];
3669
3745
  /**
3670
3746
  * Global per-request timeout in milliseconds before a call rejects with a
3671
3747
  * `BridgeError` whose `code` is `'TIMEOUT'`.
@@ -3702,13 +3778,23 @@ interface VibeAppBridgeOptions {
3702
3778
  * ```
3703
3779
  */
3704
3780
  declare class VibeAppBridge extends BridgeDataMethods {
3705
- private readonly parentOrigin;
3781
+ /** Concrete origin to `postMessage` to, or `null` if none could be resolved. */
3782
+ private readonly targetOrigin;
3783
+ /** Validates an inbound `event.origin` against the configured allowlist. */
3784
+ private readonly isTrustedOrigin;
3706
3785
  private readonly requestTimeout;
3707
3786
  private readonly pendingRequests;
3708
3787
  private readonly boundHandleMessage;
3709
3788
  private context;
3789
+ /**
3790
+ * Set once {@link initPostHog} has successfully called `posthog.init()`.
3791
+ * Guards against a double-init and makes {@link track} a no-op until
3792
+ * recording is actually live.
3793
+ */
3794
+ private posthogInitialized;
3710
3795
  private connectPromise;
3711
3796
  private connectPollInterval;
3797
+ private connectTimeout;
3712
3798
  private onContextReceived;
3713
3799
  private onContextError;
3714
3800
  /**
@@ -3726,7 +3812,7 @@ declare class VibeAppBridge extends BridgeDataMethods {
3726
3812
  private readonly kindHandlers;
3727
3813
  private dispatchPush;
3728
3814
  private readonly pushHandlers;
3729
- constructor({ parentOrigin, requestTimeout }: VibeAppBridgeOptions);
3815
+ constructor({ parentOrigin, requestTimeout }?: VibeAppBridgeOptions);
3730
3816
  /**
3731
3817
  * Connects to the host and returns the tenant/user {@link ContextPayload}.
3732
3818
  * **Call this once on app init and await it before any other method** — data
@@ -3816,8 +3902,47 @@ declare class VibeAppBridge extends BridgeDataMethods {
3816
3902
  * ```
3817
3903
  */
3818
3904
  invalidateCache(payload: InvalidateCachePayload): Promise<InvalidateResult>;
3905
+ /**
3906
+ * Captures a custom product event from inside the Vibe App, attributed to the
3907
+ * same PostHog person as the host (via the `distinctId` the host supplied at
3908
+ * connect).
3909
+ *
3910
+ * **No-op until {@link connect} has resolved with PostHog enabled by the
3911
+ * host** — if the host didn't send a `posthog` block (older host, or recording
3912
+ * disabled for this env/app), nothing is sent. App authors don't import
3913
+ * `posthog-js` themselves; this is the supported event surface.
3914
+ *
3915
+ * The event is sent **directly** from the iframe's own PostHog instance (only
3916
+ * the session *recording* is forwarded to the parent), so it carries the
3917
+ * iframe instance's `$session_id` rather than the host's — events tie to the
3918
+ * same person, but cross-boundary event↔replay timeline correlation is a known
3919
+ * limitation.
3920
+ *
3921
+ * @param event Event name. Namespacing app events (e.g. `'vibe:<slug>:<action>'`)
3922
+ * keeps them filterable from the host's own events.
3923
+ * @param properties Optional event properties.
3924
+ * @example
3925
+ * ```ts
3926
+ * bridge.track('vibe:fixed-assets:report_exported', { format: 'csv', rows: 128 })
3927
+ * ```
3928
+ */
3929
+ track(event: string, properties?: Record<string, unknown>): void;
3819
3930
  destroy(): void;
3820
3931
  private stopConnectPolling;
3932
+ /**
3933
+ * Initializes PostHog session recording + analytics from the host-supplied
3934
+ * config, **only when the host opted in** (`ctx.posthog?.enabled`).
3935
+ *
3936
+ * The host is the single source of truth: when it sends no `posthog` block
3937
+ * (older nom-ui, or recording disabled for this env/app) or `enabled: false`,
3938
+ * this does nothing — `enabled` is the kill-switch even though `posthog-js` is
3939
+ * bundled into the bridge. Cross-origin replay needs `recordCrossOriginIframes`
3940
+ * on both sides so the parent receives the iframe's rrweb frames into one
3941
+ * stitched recording, and `identify()` ties the iframe's recording + events to
3942
+ * the same person as the host. Wrapped in try/catch so a PostHog failure is
3943
+ * best-effort and never blocks `connect()`.
3944
+ */
3945
+ private initPostHog;
3821
3946
  /**
3822
3947
  * Monkey-patches `history.pushState` and `history.replaceState` to
3823
3948
  * auto-detect SPA navigation and report it to the host. Called after
@@ -3842,9 +3967,15 @@ declare class VibeAppBridge extends BridgeDataMethods {
3842
3967
  * ```
3843
3968
  */
3844
3969
  request<K extends keyof RequestRegistry>(type: K, payload: RequestRegistry[K]['payload'], onProgress?: (payload: unknown) => void): Promise<RequestRegistry[K]['data']>;
3970
+ /**
3971
+ * Posts to the embedding host. No-op when no concrete target origin could be
3972
+ * resolved (a config/embedding error) — `connect()` rejects fast in that case
3973
+ * so calls never silently hang waiting on a timeout.
3974
+ */
3975
+ private postToParent;
3845
3976
  private handleMessage;
3846
3977
  private handleResponse;
3847
3978
  private handleProgress;
3848
3979
  }
3849
3980
 
3850
- export { version as BRIDGE_VERSION, BridgeError, type BridgeSubsidiary, type ContextPayload, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type RequestRegistry, type SetSideNavPayload, type UploadOptions, type UploadProgress, type UploadResponse, VibeAppBridge, type VibeAppBridgeOptions };
3981
+ export { version as BRIDGE_VERSION, BridgeError, type BridgeSubsidiary, type ContextPayload, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type PostHogContext, type RequestRegistry, type SetSideNavPayload, type UploadOptions, type UploadProgress, type UploadResponse, VibeAppBridge, type VibeAppBridgeOptions };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- var version = "0.2.0";
1
+ var version = "0.4.0";
2
2
 
3
3
  type AccountingAccountDimensionValues = {
4
4
  account_id: string;
@@ -3182,6 +3182,54 @@ interface BridgeSubsidiary {
3182
3182
  id: number;
3183
3183
  displayName: string;
3184
3184
  }
3185
+ /**
3186
+ * PostHog configuration the host supplies so the iframe can record a session
3187
+ * replay (stitched into the host's recording via `recordCrossOriginIframes`)
3188
+ * and emit custom events, all attributed to the **same PostHog person** as
3189
+ * nom-ui.
3190
+ *
3191
+ * The host (nom-ui) is the single source of truth for these values — it knows
3192
+ * the project key, the env-aware ingestion host, and the resolved person — and
3193
+ * delivers them over the bridge so the iframe never hardcodes any of it. When
3194
+ * the host omits the {@link ContextPayload.posthog} block entirely (older host,
3195
+ * or recording disabled for this env/app), the bridge initializes nothing.
3196
+ */
3197
+ interface PostHogContext {
3198
+ /** Project API key — the same PostHog project the host (nom-ui) uses. */
3199
+ token: string;
3200
+ /**
3201
+ * **Absolute** ingestion host, e.g. `"https://us.i.posthog.com"`.
3202
+ *
3203
+ * Must NOT be the host's relative `/ingest` reverse-proxy path: a relative
3204
+ * path resolves against the iframe's *own* (cross-origin) host and 404s.
3205
+ */
3206
+ apiHost: string;
3207
+ /**
3208
+ * Host-resolved PostHog distinct id. The bridge calls `identify()` with it so
3209
+ * the iframe's recording and events attribute to the same person as the host.
3210
+ */
3211
+ distinctId: string;
3212
+ /**
3213
+ * **Optional.** The host's current PostHog session id (from
3214
+ * `posthog.get_session_id()`). When provided, the bridge seeds it as the
3215
+ * iframe instance's session via `bootstrap.sessionID`, so custom events from
3216
+ * `bridge.track()` share the **same `$session_id`** as the host — letting them
3217
+ * line up with the stitched session replay on one timeline.
3218
+ *
3219
+ * Not needed for *replay* stitching itself (that works via
3220
+ * `recordCrossOriginIframes` on both sides regardless). Must be a valid UUID
3221
+ * v7 — PostHog session ids already are. It is a one-time seed at connect, so
3222
+ * if the host's session later rotates the two can diverge; alignment is
3223
+ * best-effort for the session in progress.
3224
+ */
3225
+ sessionId?: string;
3226
+ /**
3227
+ * Master switch, env- and/or app-gated by the host. The bridge only calls
3228
+ * `posthog.init()` when this is `true`; it stays the kill-switch even though
3229
+ * `posthog-js` is bundled into the bridge.
3230
+ */
3231
+ enabled: boolean;
3232
+ }
3185
3233
  interface ContextPayload {
3186
3234
  tenant: string;
3187
3235
  subsidiaryId: number;
@@ -3194,6 +3242,13 @@ interface ContextPayload {
3194
3242
  subroute?: string;
3195
3243
  /** Slug of the last closed accounting period, e.g. "mar-2026". */
3196
3244
  lastClosedPeriodSlug?: string;
3245
+ /**
3246
+ * PostHog config for in-iframe session recording + custom events. **Absent
3247
+ * when recording is off** (older host, or disabled for this env/app), in which
3248
+ * case the bridge initializes no analytics. Optional so old hosts/bridges stay
3249
+ * compatible.
3250
+ */
3251
+ posthog?: PostHogContext;
3197
3252
  /** Version of the host SDK (@nominalso/vibe-host) running in nom-ui. */
3198
3253
  hostVersion: string;
3199
3254
  }
@@ -3659,13 +3714,34 @@ declare abstract class BridgeDataMethods {
3659
3714
 
3660
3715
  interface VibeAppBridgeOptions {
3661
3716
  /**
3662
- * Origin of the Nominal app embedding this iframe (e.g.
3663
- * `https://app.nominal.so` or `http://localhost:3000`). Must match the host
3664
- * origin **exactly** messages from any other origin are ignored, and the
3665
- * bridge only `postMessage`s to this origin. Drive it from an env var so the
3666
- * same code works locally and in production.
3717
+ * Origin(s) of the Nominal app embedding this iframe. **Optional.**
3718
+ *
3719
+ * **Omit it** (the recommended default) and the bridge auto-detects the
3720
+ * Nominal page that actually embeds it and talks only to that origin. This is
3721
+ * safe because Nominal serves Vibe Apps behind a `frame-ancestors` CSP, so
3722
+ * only nom-ui can frame the app — a hostile site can't embed it to intercept
3723
+ * what the iframe sends out (`postTaskOutput` bodies, file uploads). Most apps
3724
+ * need nothing here.
3725
+ *
3726
+ * Provide a value to **pin explicitly** — defense in depth, or when deploying
3727
+ * outside Nominal's edge:
3728
+ *
3729
+ * ```ts
3730
+ * parentOrigin: 'https://app.nominal.so' // one exact origin
3731
+ * parentOrigin: ['https://app.nominal.so', 'https://*.vercel.app'] // exact + preview pattern
3732
+ * ```
3733
+ *
3734
+ * A pattern's `*` matches exactly one DNS label or port — anchored, scheme
3735
+ * literal — so `https://*.vercel.app` matches `https://pr-7.vercel.app` but
3736
+ * not `https://a.b.vercel.app` or `https://pr-7.vercel.app.evil.com`. Patterns
3737
+ * are honoured only when this app's own page is served from a recognised
3738
+ * preview host (`*.vercel.app`, `*.lovable.app`, `localhost`, …); on a
3739
+ * production custom domain only exact origins match.
3740
+ *
3741
+ * If neither an explicit value nor an auto-detected parent is available,
3742
+ * `connect()` rejects with `BridgeError` code `PARENT_ORIGIN_UNRESOLVED`.
3667
3743
  */
3668
- parentOrigin: string;
3744
+ parentOrigin?: string | string[];
3669
3745
  /**
3670
3746
  * Global per-request timeout in milliseconds before a call rejects with a
3671
3747
  * `BridgeError` whose `code` is `'TIMEOUT'`.
@@ -3702,13 +3778,23 @@ interface VibeAppBridgeOptions {
3702
3778
  * ```
3703
3779
  */
3704
3780
  declare class VibeAppBridge extends BridgeDataMethods {
3705
- private readonly parentOrigin;
3781
+ /** Concrete origin to `postMessage` to, or `null` if none could be resolved. */
3782
+ private readonly targetOrigin;
3783
+ /** Validates an inbound `event.origin` against the configured allowlist. */
3784
+ private readonly isTrustedOrigin;
3706
3785
  private readonly requestTimeout;
3707
3786
  private readonly pendingRequests;
3708
3787
  private readonly boundHandleMessage;
3709
3788
  private context;
3789
+ /**
3790
+ * Set once {@link initPostHog} has successfully called `posthog.init()`.
3791
+ * Guards against a double-init and makes {@link track} a no-op until
3792
+ * recording is actually live.
3793
+ */
3794
+ private posthogInitialized;
3710
3795
  private connectPromise;
3711
3796
  private connectPollInterval;
3797
+ private connectTimeout;
3712
3798
  private onContextReceived;
3713
3799
  private onContextError;
3714
3800
  /**
@@ -3726,7 +3812,7 @@ declare class VibeAppBridge extends BridgeDataMethods {
3726
3812
  private readonly kindHandlers;
3727
3813
  private dispatchPush;
3728
3814
  private readonly pushHandlers;
3729
- constructor({ parentOrigin, requestTimeout }: VibeAppBridgeOptions);
3815
+ constructor({ parentOrigin, requestTimeout }?: VibeAppBridgeOptions);
3730
3816
  /**
3731
3817
  * Connects to the host and returns the tenant/user {@link ContextPayload}.
3732
3818
  * **Call this once on app init and await it before any other method** — data
@@ -3816,8 +3902,47 @@ declare class VibeAppBridge extends BridgeDataMethods {
3816
3902
  * ```
3817
3903
  */
3818
3904
  invalidateCache(payload: InvalidateCachePayload): Promise<InvalidateResult>;
3905
+ /**
3906
+ * Captures a custom product event from inside the Vibe App, attributed to the
3907
+ * same PostHog person as the host (via the `distinctId` the host supplied at
3908
+ * connect).
3909
+ *
3910
+ * **No-op until {@link connect} has resolved with PostHog enabled by the
3911
+ * host** — if the host didn't send a `posthog` block (older host, or recording
3912
+ * disabled for this env/app), nothing is sent. App authors don't import
3913
+ * `posthog-js` themselves; this is the supported event surface.
3914
+ *
3915
+ * The event is sent **directly** from the iframe's own PostHog instance (only
3916
+ * the session *recording* is forwarded to the parent), so it carries the
3917
+ * iframe instance's `$session_id` rather than the host's — events tie to the
3918
+ * same person, but cross-boundary event↔replay timeline correlation is a known
3919
+ * limitation.
3920
+ *
3921
+ * @param event Event name. Namespacing app events (e.g. `'vibe:<slug>:<action>'`)
3922
+ * keeps them filterable from the host's own events.
3923
+ * @param properties Optional event properties.
3924
+ * @example
3925
+ * ```ts
3926
+ * bridge.track('vibe:fixed-assets:report_exported', { format: 'csv', rows: 128 })
3927
+ * ```
3928
+ */
3929
+ track(event: string, properties?: Record<string, unknown>): void;
3819
3930
  destroy(): void;
3820
3931
  private stopConnectPolling;
3932
+ /**
3933
+ * Initializes PostHog session recording + analytics from the host-supplied
3934
+ * config, **only when the host opted in** (`ctx.posthog?.enabled`).
3935
+ *
3936
+ * The host is the single source of truth: when it sends no `posthog` block
3937
+ * (older nom-ui, or recording disabled for this env/app) or `enabled: false`,
3938
+ * this does nothing — `enabled` is the kill-switch even though `posthog-js` is
3939
+ * bundled into the bridge. Cross-origin replay needs `recordCrossOriginIframes`
3940
+ * on both sides so the parent receives the iframe's rrweb frames into one
3941
+ * stitched recording, and `identify()` ties the iframe's recording + events to
3942
+ * the same person as the host. Wrapped in try/catch so a PostHog failure is
3943
+ * best-effort and never blocks `connect()`.
3944
+ */
3945
+ private initPostHog;
3821
3946
  /**
3822
3947
  * Monkey-patches `history.pushState` and `history.replaceState` to
3823
3948
  * auto-detect SPA navigation and report it to the host. Called after
@@ -3842,9 +3967,15 @@ declare class VibeAppBridge extends BridgeDataMethods {
3842
3967
  * ```
3843
3968
  */
3844
3969
  request<K extends keyof RequestRegistry>(type: K, payload: RequestRegistry[K]['payload'], onProgress?: (payload: unknown) => void): Promise<RequestRegistry[K]['data']>;
3970
+ /**
3971
+ * Posts to the embedding host. No-op when no concrete target origin could be
3972
+ * resolved (a config/embedding error) — `connect()` rejects fast in that case
3973
+ * so calls never silently hang waiting on a timeout.
3974
+ */
3975
+ private postToParent;
3845
3976
  private handleMessage;
3846
3977
  private handleResponse;
3847
3978
  private handleProgress;
3848
3979
  }
3849
3980
 
3850
- export { version as BRIDGE_VERSION, BridgeError, type BridgeSubsidiary, type ContextPayload, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type RequestRegistry, type SetSideNavPayload, type UploadOptions, type UploadProgress, type UploadResponse, VibeAppBridge, type VibeAppBridgeOptions };
3981
+ export { version as BRIDGE_VERSION, BridgeError, type BridgeSubsidiary, type ContextPayload, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type PostHogContext, type RequestRegistry, type SetSideNavPayload, type UploadOptions, type UploadProgress, type UploadResponse, VibeAppBridge, type VibeAppBridgeOptions };