@nominalso/vibe-bridge 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/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- var version = "0.3.0";
1
+ var version = "0.5.0";
2
2
 
3
3
  type AccountingAccountDimensionValues = {
4
4
  account_id: string;
@@ -1392,6 +1392,7 @@ type ActivityProviderOperationErrorType = 'Connection Error' | 'Authentication E
1392
1392
  type ActivityReconciliationDefinitionData = {
1393
1393
  type: ActivityReconciliationType;
1394
1394
  task_definition_id: string;
1395
+ currency?: ActivityCurrency | null;
1395
1396
  side_a_entity_type: ActivityReconciliationEntityType;
1396
1397
  side_a_entity_id: string;
1397
1398
  side_b_entity_type: ActivityReconciliationEntityType;
@@ -1405,7 +1406,6 @@ type ActivityReconciliationInstanceDataInput = {
1405
1406
  task_instance_id: string;
1406
1407
  start_date: string;
1407
1408
  end_date: string;
1408
- currency?: ActivityCurrency | null;
1409
1409
  side_a_value?: number | string | null;
1410
1410
  side_b_value?: number | string | null;
1411
1411
  reconciling_items_balance?: number | string | null;
@@ -1419,7 +1419,6 @@ type ActivityReconciliationInstanceDataOutput = {
1419
1419
  task_instance_id: string;
1420
1420
  start_date: string;
1421
1421
  end_date: string;
1422
- currency?: ActivityCurrency | null;
1423
1422
  side_a_value?: string | null;
1424
1423
  side_b_value?: string | null;
1425
1424
  reconciling_items_balance?: string | null;
@@ -3182,6 +3181,76 @@ interface BridgeSubsidiary {
3182
3181
  id: number;
3183
3182
  displayName: string;
3184
3183
  }
3184
+ /**
3185
+ * PostHog configuration the host supplies so the iframe can record a session
3186
+ * replay (stitched into the host's recording via `recordCrossOriginIframes`)
3187
+ * and emit custom events, all attributed to the **same PostHog person** as
3188
+ * nom-ui.
3189
+ *
3190
+ * The host (nom-ui) is the single source of truth for these values — it knows
3191
+ * the project key, the env-aware ingestion host, and the resolved person — and
3192
+ * delivers it over the bridge (as a one-time `POSTHOG_PUSH`, separate from
3193
+ * {@link ContextPayload}) so the iframe never hardcodes any of it. When the host
3194
+ * has no PostHog config (recording disabled for this env/app), it sends no
3195
+ * `POSTHOG_PUSH` and 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
+ }
3233
+ /**
3234
+ * Authentication-state change the host pushes to the iframe via `AUTH_CHANGED`.
3235
+ * It carries the security principal the embedded app authenticates and scopes
3236
+ * its own data by. Sent when the Nominal session changes: a logout
3237
+ * (`authenticated: false`) or an identity/tenant switch (`authenticated: true`
3238
+ * with a new `userId`/`tenant`). The initial state is implicit at connect — the
3239
+ * iframe only loads when authenticated, and the current identity/tenant are
3240
+ * {@link ContextPayload.user} / {@link ContextPayload.tenant}, so apps baseline
3241
+ * off those and react to this push.
3242
+ *
3243
+ * Subsidiary/period and other view-level state are NOT here — they ride the
3244
+ * follow-up `CONTEXT_PUSH` (`onContextChange`), per "auth change, then context".
3245
+ */
3246
+ interface AuthPayload {
3247
+ /** Whether the Nominal user is still authenticated. */
3248
+ authenticated: boolean;
3249
+ /** The effective Nominal user id when authenticated; omitted on logout. */
3250
+ userId?: string;
3251
+ /** The tenant the user is acting in — the app's data-scoping key (RLS). */
3252
+ tenant?: string;
3253
+ }
3185
3254
  interface ContextPayload {
3186
3255
  tenant: string;
3187
3256
  subsidiaryId: number;
@@ -3197,11 +3266,6 @@ interface ContextPayload {
3197
3266
  /** Version of the host SDK (@nominalso/vibe-host) running in nom-ui. */
3198
3267
  hostVersion: string;
3199
3268
  }
3200
- /** Payload sent by the bridge when requesting context from the host. */
3201
- interface GetContextPayload {
3202
- /** Version of the bridge SDK (@nominalso/vibe-bridge) running in the iframe. */
3203
- bridgeVersion: string;
3204
- }
3205
3269
 
3206
3270
  interface UploadPayload {
3207
3271
  buffer: ArrayBuffer;
@@ -3435,10 +3499,6 @@ interface RequestRegistry {
3435
3499
  payload: InvalidateCachePayload;
3436
3500
  data: InvalidateResult;
3437
3501
  };
3438
- GET_CONTEXT: {
3439
- payload: GetContextPayload;
3440
- data: ContextPayload;
3441
- };
3442
3502
  UPLOAD_FILE: {
3443
3503
  payload: UploadPayload;
3444
3504
  data: UploadResponse;
@@ -3693,9 +3753,9 @@ interface VibeAppBridgeOptions {
3693
3753
  *
3694
3754
  * When omitted, each operation uses an operation-specific default tuned to
3695
3755
  * how long it realistically takes (API reads `30000`, `UPLOAD_FILE`
3696
- * `120000`, `INVALIDATE_CACHE` `10000`, `GET_CONTEXT` `5000`). Setting this
3697
- * overrides all of them with a single value — only do that if you have a
3698
- * specific reason; the defaults already match the host's expectations.
3756
+ * `120000`, `INVALIDATE_CACHE` `10000`). Setting this overrides all of them
3757
+ * with a single value — only do that if you have a specific reason; the
3758
+ * defaults already match the host's expectations.
3699
3759
  */
3700
3760
  requestTimeout?: number;
3701
3761
  }
@@ -3731,17 +3791,30 @@ declare class VibeAppBridge extends BridgeDataMethods {
3731
3791
  private readonly pendingRequests;
3732
3792
  private readonly boundHandleMessage;
3733
3793
  private context;
3794
+ /**
3795
+ * Set once {@link initPostHog} has successfully called `posthog.init()`.
3796
+ * Guards against a double-init and makes {@link track} a no-op until
3797
+ * recording is actually live.
3798
+ */
3799
+ private posthogInitialized;
3734
3800
  private connectPromise;
3735
3801
  private connectPollInterval;
3736
3802
  private connectTimeout;
3737
3803
  private onContextReceived;
3738
- private onContextError;
3804
+ /** Rejects an in-flight connect() (used by destroy()); `null` once settled. */
3805
+ private rejectConnect;
3806
+ /**
3807
+ * Persistent subscriber for post-connect context changes (tenant/subsidiary
3808
+ * switch, period close, …). Distinct from {@link onContextReceived}, which only
3809
+ * resolves the connect handshake and is cleared afterward. `null` until
3810
+ * {@link onContextChange} is called.
3811
+ */
3812
+ private contextChangeCallback;
3739
3813
  /**
3740
- * Request ids of the current connect()'s GET_CONTEXT polls. Used to ignore a
3741
- * late response from a *previous* connect attempt, which would otherwise
3742
- * resolve/reject the new one (a stale error could reject a healthy reconnect).
3814
+ * Persistent subscriber for host auth changes (Nominal logout or
3815
+ * identity/tenant switch). `null` until {@link onAuthChange} is called.
3743
3816
  */
3744
- private readonly connectPollIds;
3817
+ private authChangeCallback;
3745
3818
  private lastReportedSubroute;
3746
3819
  private suppressSubrouteReport;
3747
3820
  private subrouteCallback;
@@ -3758,11 +3831,11 @@ declare class VibeAppBridge extends BridgeDataMethods {
3758
3831
  * methods, `upload()`, and subroute reporting all require an established
3759
3832
  * connection. Concurrent calls return the same promise.
3760
3833
  *
3761
- * Actively polls the host every 500ms until a response arrives, which
3762
- * handles the race condition where the host's initial context push arrives
3763
- * before this listener is ready. Rejects with `Bridge connect timed out`
3764
- * after 10 seconds if no context arrives (usually a `parentOrigin` mismatch
3765
- * or the host hasn't mounted).
3834
+ * Sends a `CONNECT` handshake, re-sending every 500ms until the host replies
3835
+ * by pushing the context (so it works even if the host mounts after the
3836
+ * iframe loads). Context is only ever delivered by push. Rejects with `Bridge
3837
+ * connect timed out` after 10 seconds if no context arrives (usually a
3838
+ * `parentOrigin` mismatch or the host hasn't mounted).
3766
3839
  *
3767
3840
  * After connecting, if the context includes an initial subroute (e.g. from
3768
3841
  * a deep link), the bridge navigates to it and starts auto-tracking
@@ -3792,6 +3865,35 @@ declare class VibeAppBridge extends BridgeDataMethods {
3792
3865
  * Returns an unsubscribe function.
3793
3866
  */
3794
3867
  onSubrouteRequest(callback: (subroute: string) => void): () => void;
3868
+ /**
3869
+ * Subscribes to post-connect context changes (tenant/subsidiary switch, period
3870
+ * close, …). The callback fires for each {@link ContextPayload} the host pushes
3871
+ * *after* connect — not for the initial context, which {@link connect} already
3872
+ * returns. Returns an unsubscribe function. Single subscriber: a second call
3873
+ * replaces the first. Wire it **before {@link connect}** (the bridge warns at
3874
+ * connect if it isn't).
3875
+ */
3876
+ onContextChange(callback: (ctx: ContextPayload) => void): () => void;
3877
+ /**
3878
+ * Subscribes to host auth changes: a Nominal logout (`authenticated: false` —
3879
+ * sign your backend out) or an identity/tenant switch (`authenticated: true`
3880
+ * with a new `userId`/`tenant` — re-authenticate). Fires for each change the
3881
+ * host pushes after connect. Returns an unsubscribe function. Single
3882
+ * subscriber: a second call replaces the first.
3883
+ *
3884
+ * **Wire this before {@link connect}** — without it the embedded app won't
3885
+ * sign out when the user logs out of Nominal (the bridge warns at connect if
3886
+ * it isn't wired).
3887
+ */
3888
+ onAuthChange(callback: (auth: AuthPayload) => void): () => void;
3889
+ /**
3890
+ * At connect, nudge the app if it hasn't wired the change listeners: every
3891
+ * embedded app should react to a Nominal logout ({@link onAuthChange}) and a
3892
+ * tenant/subsidiary switch ({@link onContextChange}). Fires once during the
3893
+ * connect flow (not speculatively later), so wire both **before** `connect()`.
3894
+ * The skill/codegen does this by default; this catches hand-wired apps.
3895
+ */
3896
+ private warnMissingListeners;
3795
3897
  /**
3796
3898
  * Navigates the host to a raw path relative to this app's tenant/subsidiary
3797
3899
  * base. Fire-and-forget.
@@ -3841,8 +3943,48 @@ declare class VibeAppBridge extends BridgeDataMethods {
3841
3943
  * ```
3842
3944
  */
3843
3945
  invalidateCache(payload: InvalidateCachePayload): Promise<InvalidateResult>;
3946
+ /**
3947
+ * Captures a custom product event from inside the Vibe App, attributed to the
3948
+ * same PostHog person as the host (via the `distinctId` the host supplied at
3949
+ * connect).
3950
+ *
3951
+ * **No-op until {@link connect} has resolved with PostHog enabled by the
3952
+ * host** — if the host didn't send a `posthog` block (older host, or recording
3953
+ * disabled for this env/app), nothing is sent. App authors don't import
3954
+ * `posthog-js` themselves; this is the supported event surface.
3955
+ *
3956
+ * The event is sent **directly** from the iframe's own PostHog instance (only
3957
+ * the session *recording* is forwarded to the parent), so it carries the
3958
+ * iframe instance's `$session_id` rather than the host's — events tie to the
3959
+ * same person, but cross-boundary event↔replay timeline correlation is a known
3960
+ * limitation.
3961
+ *
3962
+ * @param event Event name. Namespacing app events (e.g. `'vibe:<slug>:<action>'`)
3963
+ * keeps them filterable from the host's own events.
3964
+ * @param properties Optional event properties.
3965
+ * @example
3966
+ * ```ts
3967
+ * bridge.track('vibe:fixed-assets:report_exported', { format: 'csv', rows: 128 })
3968
+ * ```
3969
+ */
3970
+ track(event: string, properties?: Record<string, unknown>): void;
3844
3971
  destroy(): void;
3845
3972
  private stopConnectPolling;
3973
+ /**
3974
+ * Initializes PostHog session recording + analytics from the host-supplied
3975
+ * config carried by a one-time `POSTHOG_PUSH`, **only when the host opted in**
3976
+ * (`config.enabled`).
3977
+ *
3978
+ * The host is the single source of truth: when it sends no `POSTHOG_PUSH`
3979
+ * (recording disabled for this env/app) or `enabled: false`, this does
3980
+ * nothing — `enabled` is the kill-switch even though `posthog-js` is bundled
3981
+ * into the bridge. Cross-origin replay needs `recordCrossOriginIframes` on both
3982
+ * sides so the parent receives the iframe's rrweb frames into one stitched
3983
+ * recording, and `identify()` ties the iframe's recording + events to the same
3984
+ * person as the host. Idempotent (the `posthogInitialized` guard) and wrapped
3985
+ * in try/catch so a duplicate push or a PostHog failure is harmless.
3986
+ */
3987
+ private initPostHog;
3846
3988
  /**
3847
3989
  * Monkey-patches `history.pushState` and `history.replaceState` to
3848
3990
  * auto-detect SPA navigation and report it to the host. Called after
@@ -3878,4 +4020,4 @@ declare class VibeAppBridge extends BridgeDataMethods {
3878
4020
  private handleProgress;
3879
4021
  }
3880
4022
 
3881
- 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 };
4023
+ 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.3.0";
1
+ var version = "0.5.0";
2
2
 
3
3
  type AccountingAccountDimensionValues = {
4
4
  account_id: string;
@@ -1392,6 +1392,7 @@ type ActivityProviderOperationErrorType = 'Connection Error' | 'Authentication E
1392
1392
  type ActivityReconciliationDefinitionData = {
1393
1393
  type: ActivityReconciliationType;
1394
1394
  task_definition_id: string;
1395
+ currency?: ActivityCurrency | null;
1395
1396
  side_a_entity_type: ActivityReconciliationEntityType;
1396
1397
  side_a_entity_id: string;
1397
1398
  side_b_entity_type: ActivityReconciliationEntityType;
@@ -1405,7 +1406,6 @@ type ActivityReconciliationInstanceDataInput = {
1405
1406
  task_instance_id: string;
1406
1407
  start_date: string;
1407
1408
  end_date: string;
1408
- currency?: ActivityCurrency | null;
1409
1409
  side_a_value?: number | string | null;
1410
1410
  side_b_value?: number | string | null;
1411
1411
  reconciling_items_balance?: number | string | null;
@@ -1419,7 +1419,6 @@ type ActivityReconciliationInstanceDataOutput = {
1419
1419
  task_instance_id: string;
1420
1420
  start_date: string;
1421
1421
  end_date: string;
1422
- currency?: ActivityCurrency | null;
1423
1422
  side_a_value?: string | null;
1424
1423
  side_b_value?: string | null;
1425
1424
  reconciling_items_balance?: string | null;
@@ -3182,6 +3181,76 @@ interface BridgeSubsidiary {
3182
3181
  id: number;
3183
3182
  displayName: string;
3184
3183
  }
3184
+ /**
3185
+ * PostHog configuration the host supplies so the iframe can record a session
3186
+ * replay (stitched into the host's recording via `recordCrossOriginIframes`)
3187
+ * and emit custom events, all attributed to the **same PostHog person** as
3188
+ * nom-ui.
3189
+ *
3190
+ * The host (nom-ui) is the single source of truth for these values — it knows
3191
+ * the project key, the env-aware ingestion host, and the resolved person — and
3192
+ * delivers it over the bridge (as a one-time `POSTHOG_PUSH`, separate from
3193
+ * {@link ContextPayload}) so the iframe never hardcodes any of it. When the host
3194
+ * has no PostHog config (recording disabled for this env/app), it sends no
3195
+ * `POSTHOG_PUSH` and 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
+ }
3233
+ /**
3234
+ * Authentication-state change the host pushes to the iframe via `AUTH_CHANGED`.
3235
+ * It carries the security principal the embedded app authenticates and scopes
3236
+ * its own data by. Sent when the Nominal session changes: a logout
3237
+ * (`authenticated: false`) or an identity/tenant switch (`authenticated: true`
3238
+ * with a new `userId`/`tenant`). The initial state is implicit at connect — the
3239
+ * iframe only loads when authenticated, and the current identity/tenant are
3240
+ * {@link ContextPayload.user} / {@link ContextPayload.tenant}, so apps baseline
3241
+ * off those and react to this push.
3242
+ *
3243
+ * Subsidiary/period and other view-level state are NOT here — they ride the
3244
+ * follow-up `CONTEXT_PUSH` (`onContextChange`), per "auth change, then context".
3245
+ */
3246
+ interface AuthPayload {
3247
+ /** Whether the Nominal user is still authenticated. */
3248
+ authenticated: boolean;
3249
+ /** The effective Nominal user id when authenticated; omitted on logout. */
3250
+ userId?: string;
3251
+ /** The tenant the user is acting in — the app's data-scoping key (RLS). */
3252
+ tenant?: string;
3253
+ }
3185
3254
  interface ContextPayload {
3186
3255
  tenant: string;
3187
3256
  subsidiaryId: number;
@@ -3197,11 +3266,6 @@ interface ContextPayload {
3197
3266
  /** Version of the host SDK (@nominalso/vibe-host) running in nom-ui. */
3198
3267
  hostVersion: string;
3199
3268
  }
3200
- /** Payload sent by the bridge when requesting context from the host. */
3201
- interface GetContextPayload {
3202
- /** Version of the bridge SDK (@nominalso/vibe-bridge) running in the iframe. */
3203
- bridgeVersion: string;
3204
- }
3205
3269
 
3206
3270
  interface UploadPayload {
3207
3271
  buffer: ArrayBuffer;
@@ -3435,10 +3499,6 @@ interface RequestRegistry {
3435
3499
  payload: InvalidateCachePayload;
3436
3500
  data: InvalidateResult;
3437
3501
  };
3438
- GET_CONTEXT: {
3439
- payload: GetContextPayload;
3440
- data: ContextPayload;
3441
- };
3442
3502
  UPLOAD_FILE: {
3443
3503
  payload: UploadPayload;
3444
3504
  data: UploadResponse;
@@ -3693,9 +3753,9 @@ interface VibeAppBridgeOptions {
3693
3753
  *
3694
3754
  * When omitted, each operation uses an operation-specific default tuned to
3695
3755
  * how long it realistically takes (API reads `30000`, `UPLOAD_FILE`
3696
- * `120000`, `INVALIDATE_CACHE` `10000`, `GET_CONTEXT` `5000`). Setting this
3697
- * overrides all of them with a single value — only do that if you have a
3698
- * specific reason; the defaults already match the host's expectations.
3756
+ * `120000`, `INVALIDATE_CACHE` `10000`). Setting this overrides all of them
3757
+ * with a single value — only do that if you have a specific reason; the
3758
+ * defaults already match the host's expectations.
3699
3759
  */
3700
3760
  requestTimeout?: number;
3701
3761
  }
@@ -3731,17 +3791,30 @@ declare class VibeAppBridge extends BridgeDataMethods {
3731
3791
  private readonly pendingRequests;
3732
3792
  private readonly boundHandleMessage;
3733
3793
  private context;
3794
+ /**
3795
+ * Set once {@link initPostHog} has successfully called `posthog.init()`.
3796
+ * Guards against a double-init and makes {@link track} a no-op until
3797
+ * recording is actually live.
3798
+ */
3799
+ private posthogInitialized;
3734
3800
  private connectPromise;
3735
3801
  private connectPollInterval;
3736
3802
  private connectTimeout;
3737
3803
  private onContextReceived;
3738
- private onContextError;
3804
+ /** Rejects an in-flight connect() (used by destroy()); `null` once settled. */
3805
+ private rejectConnect;
3806
+ /**
3807
+ * Persistent subscriber for post-connect context changes (tenant/subsidiary
3808
+ * switch, period close, …). Distinct from {@link onContextReceived}, which only
3809
+ * resolves the connect handshake and is cleared afterward. `null` until
3810
+ * {@link onContextChange} is called.
3811
+ */
3812
+ private contextChangeCallback;
3739
3813
  /**
3740
- * Request ids of the current connect()'s GET_CONTEXT polls. Used to ignore a
3741
- * late response from a *previous* connect attempt, which would otherwise
3742
- * resolve/reject the new one (a stale error could reject a healthy reconnect).
3814
+ * Persistent subscriber for host auth changes (Nominal logout or
3815
+ * identity/tenant switch). `null` until {@link onAuthChange} is called.
3743
3816
  */
3744
- private readonly connectPollIds;
3817
+ private authChangeCallback;
3745
3818
  private lastReportedSubroute;
3746
3819
  private suppressSubrouteReport;
3747
3820
  private subrouteCallback;
@@ -3758,11 +3831,11 @@ declare class VibeAppBridge extends BridgeDataMethods {
3758
3831
  * methods, `upload()`, and subroute reporting all require an established
3759
3832
  * connection. Concurrent calls return the same promise.
3760
3833
  *
3761
- * Actively polls the host every 500ms until a response arrives, which
3762
- * handles the race condition where the host's initial context push arrives
3763
- * before this listener is ready. Rejects with `Bridge connect timed out`
3764
- * after 10 seconds if no context arrives (usually a `parentOrigin` mismatch
3765
- * or the host hasn't mounted).
3834
+ * Sends a `CONNECT` handshake, re-sending every 500ms until the host replies
3835
+ * by pushing the context (so it works even if the host mounts after the
3836
+ * iframe loads). Context is only ever delivered by push. Rejects with `Bridge
3837
+ * connect timed out` after 10 seconds if no context arrives (usually a
3838
+ * `parentOrigin` mismatch or the host hasn't mounted).
3766
3839
  *
3767
3840
  * After connecting, if the context includes an initial subroute (e.g. from
3768
3841
  * a deep link), the bridge navigates to it and starts auto-tracking
@@ -3792,6 +3865,35 @@ declare class VibeAppBridge extends BridgeDataMethods {
3792
3865
  * Returns an unsubscribe function.
3793
3866
  */
3794
3867
  onSubrouteRequest(callback: (subroute: string) => void): () => void;
3868
+ /**
3869
+ * Subscribes to post-connect context changes (tenant/subsidiary switch, period
3870
+ * close, …). The callback fires for each {@link ContextPayload} the host pushes
3871
+ * *after* connect — not for the initial context, which {@link connect} already
3872
+ * returns. Returns an unsubscribe function. Single subscriber: a second call
3873
+ * replaces the first. Wire it **before {@link connect}** (the bridge warns at
3874
+ * connect if it isn't).
3875
+ */
3876
+ onContextChange(callback: (ctx: ContextPayload) => void): () => void;
3877
+ /**
3878
+ * Subscribes to host auth changes: a Nominal logout (`authenticated: false` —
3879
+ * sign your backend out) or an identity/tenant switch (`authenticated: true`
3880
+ * with a new `userId`/`tenant` — re-authenticate). Fires for each change the
3881
+ * host pushes after connect. Returns an unsubscribe function. Single
3882
+ * subscriber: a second call replaces the first.
3883
+ *
3884
+ * **Wire this before {@link connect}** — without it the embedded app won't
3885
+ * sign out when the user logs out of Nominal (the bridge warns at connect if
3886
+ * it isn't wired).
3887
+ */
3888
+ onAuthChange(callback: (auth: AuthPayload) => void): () => void;
3889
+ /**
3890
+ * At connect, nudge the app if it hasn't wired the change listeners: every
3891
+ * embedded app should react to a Nominal logout ({@link onAuthChange}) and a
3892
+ * tenant/subsidiary switch ({@link onContextChange}). Fires once during the
3893
+ * connect flow (not speculatively later), so wire both **before** `connect()`.
3894
+ * The skill/codegen does this by default; this catches hand-wired apps.
3895
+ */
3896
+ private warnMissingListeners;
3795
3897
  /**
3796
3898
  * Navigates the host to a raw path relative to this app's tenant/subsidiary
3797
3899
  * base. Fire-and-forget.
@@ -3841,8 +3943,48 @@ declare class VibeAppBridge extends BridgeDataMethods {
3841
3943
  * ```
3842
3944
  */
3843
3945
  invalidateCache(payload: InvalidateCachePayload): Promise<InvalidateResult>;
3946
+ /**
3947
+ * Captures a custom product event from inside the Vibe App, attributed to the
3948
+ * same PostHog person as the host (via the `distinctId` the host supplied at
3949
+ * connect).
3950
+ *
3951
+ * **No-op until {@link connect} has resolved with PostHog enabled by the
3952
+ * host** — if the host didn't send a `posthog` block (older host, or recording
3953
+ * disabled for this env/app), nothing is sent. App authors don't import
3954
+ * `posthog-js` themselves; this is the supported event surface.
3955
+ *
3956
+ * The event is sent **directly** from the iframe's own PostHog instance (only
3957
+ * the session *recording* is forwarded to the parent), so it carries the
3958
+ * iframe instance's `$session_id` rather than the host's — events tie to the
3959
+ * same person, but cross-boundary event↔replay timeline correlation is a known
3960
+ * limitation.
3961
+ *
3962
+ * @param event Event name. Namespacing app events (e.g. `'vibe:<slug>:<action>'`)
3963
+ * keeps them filterable from the host's own events.
3964
+ * @param properties Optional event properties.
3965
+ * @example
3966
+ * ```ts
3967
+ * bridge.track('vibe:fixed-assets:report_exported', { format: 'csv', rows: 128 })
3968
+ * ```
3969
+ */
3970
+ track(event: string, properties?: Record<string, unknown>): void;
3844
3971
  destroy(): void;
3845
3972
  private stopConnectPolling;
3973
+ /**
3974
+ * Initializes PostHog session recording + analytics from the host-supplied
3975
+ * config carried by a one-time `POSTHOG_PUSH`, **only when the host opted in**
3976
+ * (`config.enabled`).
3977
+ *
3978
+ * The host is the single source of truth: when it sends no `POSTHOG_PUSH`
3979
+ * (recording disabled for this env/app) or `enabled: false`, this does
3980
+ * nothing — `enabled` is the kill-switch even though `posthog-js` is bundled
3981
+ * into the bridge. Cross-origin replay needs `recordCrossOriginIframes` on both
3982
+ * sides so the parent receives the iframe's rrweb frames into one stitched
3983
+ * recording, and `identify()` ties the iframe's recording + events to the same
3984
+ * person as the host. Idempotent (the `posthogInitialized` guard) and wrapped
3985
+ * in try/catch so a duplicate push or a PostHog failure is harmless.
3986
+ */
3987
+ private initPostHog;
3846
3988
  /**
3847
3989
  * Monkey-patches `history.pushState` and `history.replaceState` to
3848
3990
  * auto-detect SPA navigation and report it to the host. Called after
@@ -3878,4 +4020,4 @@ declare class VibeAppBridge extends BridgeDataMethods {
3878
4020
  private handleProgress;
3879
4021
  }
3880
4022
 
3881
- 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 };
4023
+ 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 };