@nominalso/vibe-host 0.1.0 → 0.3.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
@@ -3201,15 +3201,6 @@ interface GetContextPayload {
3201
3201
  bridgeVersion: string;
3202
3202
  }
3203
3203
 
3204
- /**
3205
- * All operations the iframe can invoke on the host, each with a typed request
3206
- * payload and a typed response. Adding a new operation here is all that's
3207
- * required — message union types and handler types below are derived
3208
- * automatically.
3209
- *
3210
- * Operations that also emit intermediate progress events should include a
3211
- * `progress` field with the progress payload type.
3212
- */
3213
3204
  interface UploadPayload {
3214
3205
  buffer: ArrayBuffer;
3215
3206
  fileName: string;
@@ -3220,11 +3211,57 @@ interface UploadPayload {
3220
3211
  interface UploadResponse {
3221
3212
  attachmentId: string;
3222
3213
  name: string;
3214
+ /** Public/presigned URL of the stored file, when the host returns one. */
3215
+ url?: string;
3223
3216
  }
3224
3217
  interface UploadProgress {
3225
3218
  attachmentId: string;
3226
3219
  progress: number;
3227
3220
  }
3221
+ /** How the host should apply a navigation. Defaults to `'push'`. */
3222
+ type NavigateAction = 'push' | 'replace' | 'refresh';
3223
+ /**
3224
+ * Tells the host to navigate. Provide **either** a raw `path` (relative to the
3225
+ * app's tenant/subsidiary base) **or** a named `route` resolved host-side.
3226
+ */
3227
+ interface NavigatePayload {
3228
+ /** Raw path relative to the app's base, e.g. `'/journal-entries/123'`. */
3229
+ path?: string;
3230
+ /** Named route key the host resolves, e.g. `'CHART_OF_ACCOUNTS'`. */
3231
+ route?: string;
3232
+ /** Values for route placeholders, e.g. `{ id: '123' }`. */
3233
+ routeParams?: Record<string, string>;
3234
+ /** Navigation method. Defaults to `'push'`. */
3235
+ action?: NavigateAction;
3236
+ }
3237
+ /** Collapse/expand the host's side navigation. */
3238
+ interface SetSideNavPayload {
3239
+ collapsed: boolean;
3240
+ }
3241
+ /** Switch the host to a different subsidiary. */
3242
+ interface SwitchSubsidiaryPayload {
3243
+ subsidiaryId: number;
3244
+ }
3245
+ /** Targets for a host cache invalidation. */
3246
+ interface InvalidateCachePayload {
3247
+ /** API path prefixes to invalidate, e.g. `['/accounting/accounts']`. */
3248
+ paths?: string[];
3249
+ /** Cache tag keys to invalidate, e.g. `['REPORTS', 'SUBSIDIARIES']`. */
3250
+ tags?: string[];
3251
+ }
3252
+ /** Result of a host cache invalidation. */
3253
+ interface InvalidateResult {
3254
+ invalidated: boolean;
3255
+ }
3256
+
3257
+ /**
3258
+ * All operations the iframe can invoke on the host, each with a typed request
3259
+ * payload and a typed response. Adding a new operation here is all that's
3260
+ * required — message union types and handler types are derived automatically.
3261
+ *
3262
+ * Operations that also emit intermediate progress events should include a
3263
+ * `progress` field with the progress payload type.
3264
+ */
3228
3265
  interface RequestRegistry {
3229
3266
  GET_CHART_OF_ACCOUNTS: {
3230
3267
  payload: Omit<GetCoaFlatApiAccountingSubsidiarySubsidiaryIdCoaAccountGetData, 'url'>;
@@ -3410,6 +3447,10 @@ interface RequestRegistry {
3410
3447
  payload: Omit<GetTenantUsersApiTenancyTenantTenantIdOrNameUsersGetData, 'url'>;
3411
3448
  data: GetTenantUsersApiTenancyTenantTenantIdOrNameUsersGetResponse;
3412
3449
  };
3450
+ INVALIDATE_CACHE: {
3451
+ payload: InvalidateCachePayload;
3452
+ data: InvalidateResult;
3453
+ };
3413
3454
  GET_CONTEXT: {
3414
3455
  payload: GetContextPayload;
3415
3456
  data: ContextPayload;
@@ -3434,6 +3475,53 @@ type RequestHandlers = {
3434
3475
  } ? (payload: RequestRegistry[K]['payload'], sendProgress: (p: P) => void) => Promise<RequestRegistry[K]['data']> : (payload: RequestRegistry[K]['payload']) => Promise<RequestRegistry[K]['data']>;
3435
3476
  };
3436
3477
 
3478
+ /**
3479
+ * An error that crossed the bridge, carrying a machine-readable {@link code}
3480
+ * alongside the human-readable message.
3481
+ *
3482
+ * The host attaches a `code` to failed responses (e.g. `'RATE_LIMITED'`,
3483
+ * `'FILE_TOO_LARGE'`, `'REQUEST_FAILED'`); the bridge rehydrates it into a
3484
+ * `BridgeError` so app `catch` blocks can branch on `error.code` instead of
3485
+ * string-matching `error.message`. A failed Nominal API call rehydrates as the
3486
+ * {@link HttpBridgeError} subtype, which adds the HTTP `status`.
3487
+ *
3488
+ * @example
3489
+ * ```ts
3490
+ * try {
3491
+ * await bridge.getAccounts({})
3492
+ * } catch (err) {
3493
+ * if (err instanceof BridgeError && err.code === 'RATE_LIMITED') {
3494
+ * // back off and retry
3495
+ * }
3496
+ * }
3497
+ * ```
3498
+ */
3499
+ declare class BridgeError extends Error {
3500
+ readonly code: string;
3501
+ constructor(code: string, message: string);
3502
+ }
3503
+ /**
3504
+ * A {@link BridgeError} for a failed Nominal API call. Its `code` is always
3505
+ * `'REQUEST_FAILED'`; the HTTP {@link status} is carried as a structured field
3506
+ * so callers can branch on it (e.g. `err.status === 404`) instead of parsing
3507
+ * the code string.
3508
+ *
3509
+ * @example
3510
+ * ```ts
3511
+ * try {
3512
+ * await bridge.getAccount({ path: { account_id: 'missing' } })
3513
+ * } catch (err) {
3514
+ * if (err instanceof HttpBridgeError && err.status === 404) {
3515
+ * // handle not-found
3516
+ * }
3517
+ * }
3518
+ * ```
3519
+ */
3520
+ declare class HttpBridgeError extends BridgeError {
3521
+ readonly status: number;
3522
+ constructor(status: number, message: string);
3523
+ }
3524
+
3437
3525
  interface VibeApiClientOptions {
3438
3526
  baseUrl: string;
3439
3527
  /**
@@ -3446,11 +3534,36 @@ interface VibeApiClientOptions {
3446
3534
 
3447
3535
  type HostHandlers = Omit<RequestHandlers, 'GET_CONTEXT'>;
3448
3536
  type HostContext = Omit<ContextPayload, 'hostVersion'>;
3537
+ /**
3538
+ * Notified after each request resolves — except the `GET_CONTEXT` connect poll,
3539
+ * which is exempt. Used by nom-ui for SWR busting (SSOT §5j).
3540
+ */
3541
+ interface RequestCompleteInfo {
3542
+ /** The operation type, e.g. `'POST_TASK_OUTPUT'`. */
3543
+ type: string;
3544
+ /** Whether the operation succeeded. */
3545
+ ok: boolean;
3546
+ /** The request payload that was handled. */
3547
+ payload: unknown;
3548
+ }
3449
3549
  interface VibeAppHostOptions {
3450
3550
  /**
3451
3551
  * Iframe origins allowed to talk to this host. Messages from any other origin
3452
- * are ignored, and context/pushes are only sent to these origins. Must match
3453
- * the Vibe App's origin exactly (e.g. `'https://my-vibe-app.lovable.app'`).
3552
+ * are ignored, and context/pushes are only sent to allowed origins.
3553
+ *
3554
+ * Each entry is either an exact origin (`'https://my-vibe-app.lovable.app'`)
3555
+ * or a **glob pattern** containing `*` (`'https://*.vercel.app'`), where `*`
3556
+ * matches exactly one DNS label or port — anchored, scheme literal — so
3557
+ * `https://*.vercel.app` matches `https://pr-7.vercel.app` but not
3558
+ * `https://a.b.vercel.app` or `https://pr-7.vercel.app.evil.com`.
3559
+ *
3560
+ * Patterns are honoured unconditionally, so **only add them for non-production
3561
+ * environments** — gate this list on your own deploy env (e.g. pass exact
3562
+ * origins only in production, exact + `*.vercel.app`/`*.lovable.app` in
3563
+ * staging/dev) to test against dynamic preview deployments. Because you cannot
3564
+ * `postMessage` to a pattern, proactive context/subroute pushes target the
3565
+ * exact entries plus any origin seen on a validated inbound message; pattern-
3566
+ * matched iframes still receive context via their `connect()` poll response.
3454
3567
  */
3455
3568
  trustedOrigins: string[];
3456
3569
  /** Returns the current context to send to the iframe on connect. */
@@ -3467,13 +3580,33 @@ interface VibeAppHostOptions {
3467
3580
  * `{ baseUrl: '', stripApiPrefix: false }`.
3468
3581
  */
3469
3582
  clientConfig?: VibeApiClientOptions;
3583
+ /** Fire-and-forget. Resolve a path / named route and push to the host router. */
3584
+ onNavigate?: (payload: NavigatePayload) => void;
3585
+ /** Fire-and-forget. Collapse/expand the host side nav. */
3586
+ onSetSideNav?: (payload: SetSideNavPayload) => void;
3587
+ /** Fire-and-forget. Switch subsidiary (host owns validation + navigation). */
3588
+ onSwitchSubsidiary?: (payload: SwitchSubsidiaryPayload) => void;
3589
+ /** Awaited. Invalidate host cache by tags/paths. Defaults to a no-op. */
3590
+ onInvalidateCache?: (payload: InvalidateCachePayload) => Promise<InvalidateResult>;
3591
+ /**
3592
+ * Awaited. Real file upload — replaces the built-in stub (DP2). When omitted,
3593
+ * `UPLOAD_FILE` requests reject with a `'NOT_WIRED'` coded error.
3594
+ */
3595
+ onUpload?: (payload: UploadPayload, sendProgress: (p: UploadProgress) => void) => Promise<UploadResponse>;
3596
+ /** Optional. Notified after each request resolves (except the `GET_CONTEXT` connect poll) — used by nom-ui for SWR busting (SSOT §5j). */
3597
+ onRequestComplete?: (info: RequestCompleteInfo) => void;
3598
+ /** Requests/min before a request trips `RATE_LIMITED` (DP5). Default 60. */
3599
+ rateLimit?: number;
3470
3600
  }
3601
+
3471
3602
  /**
3472
3603
  * Host-side counterpart to `VibeAppBridge`. Construct it with the trusted
3473
3604
  * iframe origin(s), a `getContext` callback, the app's base path, and a
3474
3605
  * `clientConfig` pointing at the Nominal API; then call {@link VibeAppHost.mount}
3475
- * to start listening. The handler map for all protocol operations is built
3476
- * automatically — you do not pass handlers.
3606
+ * to start listening. The handler map for all protocol API operations is built
3607
+ * automatically — you do not pass those handlers. Non-API side effects
3608
+ * (navigation, side nav, subsidiary switch, cache invalidation, upload) are
3609
+ * supplied via the optional injected callbacks.
3477
3610
  *
3478
3611
  * @example
3479
3612
  * ```tsx
@@ -3483,6 +3616,8 @@ interface VibeAppHostOptions {
3483
3616
  * appBasePath: `/${tenant}/${subsidiary}/apps/${slug}`,
3484
3617
  * getContext: () => ({ tenant, subsidiaryId, subsidiaries, user, lastClosedPeriodSlug }),
3485
3618
  * clientConfig: { baseUrl: '/api/proxy', stripApiPrefix: true },
3619
+ * onNavigate: (p) => router.push(resolve(p)),
3620
+ * onUpload: (p, sendProgress) => uploadToS3(p, sendProgress),
3486
3621
  * })
3487
3622
  *
3488
3623
  * useEffect(() => {
@@ -3499,18 +3634,51 @@ interface VibeAppHostOptions {
3499
3634
  * ```
3500
3635
  */
3501
3636
  declare class VibeAppHost {
3637
+ /**
3638
+ * Configured allowlist. Entries may be exact origins or glob patterns
3639
+ * (`https://*.vercel.app`); inbound messages are validated against it with
3640
+ * {@link isOriginAllowed}. Pattern entries are honoured — nom-ui is expected
3641
+ * to supply patterns only in non-production environments.
3642
+ */
3502
3643
  private readonly trustedOrigins;
3644
+ /** The exact (non-pattern) subset of {@link trustedOrigins} — see {@link concreteTargets}. */
3645
+ private readonly exactOrigins;
3646
+ /**
3647
+ * Concrete iframe origins observed on validated inbound messages. Because you
3648
+ * cannot `postMessage` to a glob pattern, proactive pushes target these
3649
+ * learned origins (plus the exact allowlist entries) rather than the patterns.
3650
+ */
3651
+ private readonly connectedOrigins;
3503
3652
  private readonly handlers;
3653
+ private readonly commandHandlers;
3504
3654
  private readonly getContext;
3505
3655
  private readonly appBasePath;
3656
+ private readonly onRequestComplete?;
3657
+ private readonly rateLimiter;
3506
3658
  private readonly boundHandleMessage;
3507
3659
  private readonly boundHandlePopState;
3508
3660
  private iframeWindow;
3661
+ /**
3662
+ * Source windows we've already logged a successful handshake for. The bridge
3663
+ * polls `GET_CONTEXT` on an interval until it connects, so without this the
3664
+ * "Vibe App connected" line repeats on every poll (SSOT §13 F3).
3665
+ */
3666
+ private readonly handshakeLoggedWindows;
3509
3667
  private readonly kindHandlers;
3510
3668
  private dispatchCommand;
3511
- private readonly commandHandlers;
3669
+ /**
3670
+ * Commands: `REPORT_SUBROUTE` is handled internally (transport + traversal
3671
+ * guard, item `l`); navigation/UI side effects are injected by the consumer,
3672
+ * falling back to warn-and-ignore (DP4).
3673
+ */
3674
+ private buildCommandHandlers;
3675
+ /**
3676
+ * Requests: the built-in API map (hey-api), plus the injected/awaited side
3677
+ * effects with safe defaults — unwired `UPLOAD_FILE` rejects with a coded
3678
+ * error; unwired `INVALIDATE_CACHE` resolves to a no-op (DP4).
3679
+ */
3512
3680
  private initializeHandlers;
3513
- constructor({ trustedOrigins, getContext, appBasePath, clientConfig }: VibeAppHostOptions);
3681
+ constructor(opts: VibeAppHostOptions);
3514
3682
  /**
3515
3683
  * Starts listening for bridge messages and sets up browser back/forward sync.
3516
3684
  * Returns a cleanup function to call on unmount.
@@ -3531,8 +3699,15 @@ declare class VibeAppHost {
3531
3699
  private handlePopState;
3532
3700
  private pushToIframe;
3533
3701
  private handleMessage;
3702
+ /**
3703
+ * Concrete origins a proactive push may target: the exact allowlist entries
3704
+ * plus any origins seen on validated inbound messages. Never includes glob
3705
+ * patterns (you cannot `postMessage` to one). When only exact origins are
3706
+ * configured this equals the configured set, so behaviour is unchanged.
3707
+ */
3708
+ private concreteTargets;
3534
3709
  private handleRequest;
3535
3710
  private respond;
3536
3711
  }
3537
3712
 
3538
- export { type ContextPayload, type HostContext, type HostHandlers, type RequestHandlers, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };
3713
+ export { BridgeError, type ContextPayload, type HostContext, type HostHandlers, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type NavigatePayload, type RequestCompleteInfo, type RequestHandlers, type SetSideNavPayload, type SwitchSubsidiaryPayload, type UploadPayload, type UploadProgress, type UploadResponse, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };
package/dist/index.d.ts CHANGED
@@ -3201,15 +3201,6 @@ interface GetContextPayload {
3201
3201
  bridgeVersion: string;
3202
3202
  }
3203
3203
 
3204
- /**
3205
- * All operations the iframe can invoke on the host, each with a typed request
3206
- * payload and a typed response. Adding a new operation here is all that's
3207
- * required — message union types and handler types below are derived
3208
- * automatically.
3209
- *
3210
- * Operations that also emit intermediate progress events should include a
3211
- * `progress` field with the progress payload type.
3212
- */
3213
3204
  interface UploadPayload {
3214
3205
  buffer: ArrayBuffer;
3215
3206
  fileName: string;
@@ -3220,11 +3211,57 @@ interface UploadPayload {
3220
3211
  interface UploadResponse {
3221
3212
  attachmentId: string;
3222
3213
  name: string;
3214
+ /** Public/presigned URL of the stored file, when the host returns one. */
3215
+ url?: string;
3223
3216
  }
3224
3217
  interface UploadProgress {
3225
3218
  attachmentId: string;
3226
3219
  progress: number;
3227
3220
  }
3221
+ /** How the host should apply a navigation. Defaults to `'push'`. */
3222
+ type NavigateAction = 'push' | 'replace' | 'refresh';
3223
+ /**
3224
+ * Tells the host to navigate. Provide **either** a raw `path` (relative to the
3225
+ * app's tenant/subsidiary base) **or** a named `route` resolved host-side.
3226
+ */
3227
+ interface NavigatePayload {
3228
+ /** Raw path relative to the app's base, e.g. `'/journal-entries/123'`. */
3229
+ path?: string;
3230
+ /** Named route key the host resolves, e.g. `'CHART_OF_ACCOUNTS'`. */
3231
+ route?: string;
3232
+ /** Values for route placeholders, e.g. `{ id: '123' }`. */
3233
+ routeParams?: Record<string, string>;
3234
+ /** Navigation method. Defaults to `'push'`. */
3235
+ action?: NavigateAction;
3236
+ }
3237
+ /** Collapse/expand the host's side navigation. */
3238
+ interface SetSideNavPayload {
3239
+ collapsed: boolean;
3240
+ }
3241
+ /** Switch the host to a different subsidiary. */
3242
+ interface SwitchSubsidiaryPayload {
3243
+ subsidiaryId: number;
3244
+ }
3245
+ /** Targets for a host cache invalidation. */
3246
+ interface InvalidateCachePayload {
3247
+ /** API path prefixes to invalidate, e.g. `['/accounting/accounts']`. */
3248
+ paths?: string[];
3249
+ /** Cache tag keys to invalidate, e.g. `['REPORTS', 'SUBSIDIARIES']`. */
3250
+ tags?: string[];
3251
+ }
3252
+ /** Result of a host cache invalidation. */
3253
+ interface InvalidateResult {
3254
+ invalidated: boolean;
3255
+ }
3256
+
3257
+ /**
3258
+ * All operations the iframe can invoke on the host, each with a typed request
3259
+ * payload and a typed response. Adding a new operation here is all that's
3260
+ * required — message union types and handler types are derived automatically.
3261
+ *
3262
+ * Operations that also emit intermediate progress events should include a
3263
+ * `progress` field with the progress payload type.
3264
+ */
3228
3265
  interface RequestRegistry {
3229
3266
  GET_CHART_OF_ACCOUNTS: {
3230
3267
  payload: Omit<GetCoaFlatApiAccountingSubsidiarySubsidiaryIdCoaAccountGetData, 'url'>;
@@ -3410,6 +3447,10 @@ interface RequestRegistry {
3410
3447
  payload: Omit<GetTenantUsersApiTenancyTenantTenantIdOrNameUsersGetData, 'url'>;
3411
3448
  data: GetTenantUsersApiTenancyTenantTenantIdOrNameUsersGetResponse;
3412
3449
  };
3450
+ INVALIDATE_CACHE: {
3451
+ payload: InvalidateCachePayload;
3452
+ data: InvalidateResult;
3453
+ };
3413
3454
  GET_CONTEXT: {
3414
3455
  payload: GetContextPayload;
3415
3456
  data: ContextPayload;
@@ -3434,6 +3475,53 @@ type RequestHandlers = {
3434
3475
  } ? (payload: RequestRegistry[K]['payload'], sendProgress: (p: P) => void) => Promise<RequestRegistry[K]['data']> : (payload: RequestRegistry[K]['payload']) => Promise<RequestRegistry[K]['data']>;
3435
3476
  };
3436
3477
 
3478
+ /**
3479
+ * An error that crossed the bridge, carrying a machine-readable {@link code}
3480
+ * alongside the human-readable message.
3481
+ *
3482
+ * The host attaches a `code` to failed responses (e.g. `'RATE_LIMITED'`,
3483
+ * `'FILE_TOO_LARGE'`, `'REQUEST_FAILED'`); the bridge rehydrates it into a
3484
+ * `BridgeError` so app `catch` blocks can branch on `error.code` instead of
3485
+ * string-matching `error.message`. A failed Nominal API call rehydrates as the
3486
+ * {@link HttpBridgeError} subtype, which adds the HTTP `status`.
3487
+ *
3488
+ * @example
3489
+ * ```ts
3490
+ * try {
3491
+ * await bridge.getAccounts({})
3492
+ * } catch (err) {
3493
+ * if (err instanceof BridgeError && err.code === 'RATE_LIMITED') {
3494
+ * // back off and retry
3495
+ * }
3496
+ * }
3497
+ * ```
3498
+ */
3499
+ declare class BridgeError extends Error {
3500
+ readonly code: string;
3501
+ constructor(code: string, message: string);
3502
+ }
3503
+ /**
3504
+ * A {@link BridgeError} for a failed Nominal API call. Its `code` is always
3505
+ * `'REQUEST_FAILED'`; the HTTP {@link status} is carried as a structured field
3506
+ * so callers can branch on it (e.g. `err.status === 404`) instead of parsing
3507
+ * the code string.
3508
+ *
3509
+ * @example
3510
+ * ```ts
3511
+ * try {
3512
+ * await bridge.getAccount({ path: { account_id: 'missing' } })
3513
+ * } catch (err) {
3514
+ * if (err instanceof HttpBridgeError && err.status === 404) {
3515
+ * // handle not-found
3516
+ * }
3517
+ * }
3518
+ * ```
3519
+ */
3520
+ declare class HttpBridgeError extends BridgeError {
3521
+ readonly status: number;
3522
+ constructor(status: number, message: string);
3523
+ }
3524
+
3437
3525
  interface VibeApiClientOptions {
3438
3526
  baseUrl: string;
3439
3527
  /**
@@ -3446,11 +3534,36 @@ interface VibeApiClientOptions {
3446
3534
 
3447
3535
  type HostHandlers = Omit<RequestHandlers, 'GET_CONTEXT'>;
3448
3536
  type HostContext = Omit<ContextPayload, 'hostVersion'>;
3537
+ /**
3538
+ * Notified after each request resolves — except the `GET_CONTEXT` connect poll,
3539
+ * which is exempt. Used by nom-ui for SWR busting (SSOT §5j).
3540
+ */
3541
+ interface RequestCompleteInfo {
3542
+ /** The operation type, e.g. `'POST_TASK_OUTPUT'`. */
3543
+ type: string;
3544
+ /** Whether the operation succeeded. */
3545
+ ok: boolean;
3546
+ /** The request payload that was handled. */
3547
+ payload: unknown;
3548
+ }
3449
3549
  interface VibeAppHostOptions {
3450
3550
  /**
3451
3551
  * Iframe origins allowed to talk to this host. Messages from any other origin
3452
- * are ignored, and context/pushes are only sent to these origins. Must match
3453
- * the Vibe App's origin exactly (e.g. `'https://my-vibe-app.lovable.app'`).
3552
+ * are ignored, and context/pushes are only sent to allowed origins.
3553
+ *
3554
+ * Each entry is either an exact origin (`'https://my-vibe-app.lovable.app'`)
3555
+ * or a **glob pattern** containing `*` (`'https://*.vercel.app'`), where `*`
3556
+ * matches exactly one DNS label or port — anchored, scheme literal — so
3557
+ * `https://*.vercel.app` matches `https://pr-7.vercel.app` but not
3558
+ * `https://a.b.vercel.app` or `https://pr-7.vercel.app.evil.com`.
3559
+ *
3560
+ * Patterns are honoured unconditionally, so **only add them for non-production
3561
+ * environments** — gate this list on your own deploy env (e.g. pass exact
3562
+ * origins only in production, exact + `*.vercel.app`/`*.lovable.app` in
3563
+ * staging/dev) to test against dynamic preview deployments. Because you cannot
3564
+ * `postMessage` to a pattern, proactive context/subroute pushes target the
3565
+ * exact entries plus any origin seen on a validated inbound message; pattern-
3566
+ * matched iframes still receive context via their `connect()` poll response.
3454
3567
  */
3455
3568
  trustedOrigins: string[];
3456
3569
  /** Returns the current context to send to the iframe on connect. */
@@ -3467,13 +3580,33 @@ interface VibeAppHostOptions {
3467
3580
  * `{ baseUrl: '', stripApiPrefix: false }`.
3468
3581
  */
3469
3582
  clientConfig?: VibeApiClientOptions;
3583
+ /** Fire-and-forget. Resolve a path / named route and push to the host router. */
3584
+ onNavigate?: (payload: NavigatePayload) => void;
3585
+ /** Fire-and-forget. Collapse/expand the host side nav. */
3586
+ onSetSideNav?: (payload: SetSideNavPayload) => void;
3587
+ /** Fire-and-forget. Switch subsidiary (host owns validation + navigation). */
3588
+ onSwitchSubsidiary?: (payload: SwitchSubsidiaryPayload) => void;
3589
+ /** Awaited. Invalidate host cache by tags/paths. Defaults to a no-op. */
3590
+ onInvalidateCache?: (payload: InvalidateCachePayload) => Promise<InvalidateResult>;
3591
+ /**
3592
+ * Awaited. Real file upload — replaces the built-in stub (DP2). When omitted,
3593
+ * `UPLOAD_FILE` requests reject with a `'NOT_WIRED'` coded error.
3594
+ */
3595
+ onUpload?: (payload: UploadPayload, sendProgress: (p: UploadProgress) => void) => Promise<UploadResponse>;
3596
+ /** Optional. Notified after each request resolves (except the `GET_CONTEXT` connect poll) — used by nom-ui for SWR busting (SSOT §5j). */
3597
+ onRequestComplete?: (info: RequestCompleteInfo) => void;
3598
+ /** Requests/min before a request trips `RATE_LIMITED` (DP5). Default 60. */
3599
+ rateLimit?: number;
3470
3600
  }
3601
+
3471
3602
  /**
3472
3603
  * Host-side counterpart to `VibeAppBridge`. Construct it with the trusted
3473
3604
  * iframe origin(s), a `getContext` callback, the app's base path, and a
3474
3605
  * `clientConfig` pointing at the Nominal API; then call {@link VibeAppHost.mount}
3475
- * to start listening. The handler map for all protocol operations is built
3476
- * automatically — you do not pass handlers.
3606
+ * to start listening. The handler map for all protocol API operations is built
3607
+ * automatically — you do not pass those handlers. Non-API side effects
3608
+ * (navigation, side nav, subsidiary switch, cache invalidation, upload) are
3609
+ * supplied via the optional injected callbacks.
3477
3610
  *
3478
3611
  * @example
3479
3612
  * ```tsx
@@ -3483,6 +3616,8 @@ interface VibeAppHostOptions {
3483
3616
  * appBasePath: `/${tenant}/${subsidiary}/apps/${slug}`,
3484
3617
  * getContext: () => ({ tenant, subsidiaryId, subsidiaries, user, lastClosedPeriodSlug }),
3485
3618
  * clientConfig: { baseUrl: '/api/proxy', stripApiPrefix: true },
3619
+ * onNavigate: (p) => router.push(resolve(p)),
3620
+ * onUpload: (p, sendProgress) => uploadToS3(p, sendProgress),
3486
3621
  * })
3487
3622
  *
3488
3623
  * useEffect(() => {
@@ -3499,18 +3634,51 @@ interface VibeAppHostOptions {
3499
3634
  * ```
3500
3635
  */
3501
3636
  declare class VibeAppHost {
3637
+ /**
3638
+ * Configured allowlist. Entries may be exact origins or glob patterns
3639
+ * (`https://*.vercel.app`); inbound messages are validated against it with
3640
+ * {@link isOriginAllowed}. Pattern entries are honoured — nom-ui is expected
3641
+ * to supply patterns only in non-production environments.
3642
+ */
3502
3643
  private readonly trustedOrigins;
3644
+ /** The exact (non-pattern) subset of {@link trustedOrigins} — see {@link concreteTargets}. */
3645
+ private readonly exactOrigins;
3646
+ /**
3647
+ * Concrete iframe origins observed on validated inbound messages. Because you
3648
+ * cannot `postMessage` to a glob pattern, proactive pushes target these
3649
+ * learned origins (plus the exact allowlist entries) rather than the patterns.
3650
+ */
3651
+ private readonly connectedOrigins;
3503
3652
  private readonly handlers;
3653
+ private readonly commandHandlers;
3504
3654
  private readonly getContext;
3505
3655
  private readonly appBasePath;
3656
+ private readonly onRequestComplete?;
3657
+ private readonly rateLimiter;
3506
3658
  private readonly boundHandleMessage;
3507
3659
  private readonly boundHandlePopState;
3508
3660
  private iframeWindow;
3661
+ /**
3662
+ * Source windows we've already logged a successful handshake for. The bridge
3663
+ * polls `GET_CONTEXT` on an interval until it connects, so without this the
3664
+ * "Vibe App connected" line repeats on every poll (SSOT §13 F3).
3665
+ */
3666
+ private readonly handshakeLoggedWindows;
3509
3667
  private readonly kindHandlers;
3510
3668
  private dispatchCommand;
3511
- private readonly commandHandlers;
3669
+ /**
3670
+ * Commands: `REPORT_SUBROUTE` is handled internally (transport + traversal
3671
+ * guard, item `l`); navigation/UI side effects are injected by the consumer,
3672
+ * falling back to warn-and-ignore (DP4).
3673
+ */
3674
+ private buildCommandHandlers;
3675
+ /**
3676
+ * Requests: the built-in API map (hey-api), plus the injected/awaited side
3677
+ * effects with safe defaults — unwired `UPLOAD_FILE` rejects with a coded
3678
+ * error; unwired `INVALIDATE_CACHE` resolves to a no-op (DP4).
3679
+ */
3512
3680
  private initializeHandlers;
3513
- constructor({ trustedOrigins, getContext, appBasePath, clientConfig }: VibeAppHostOptions);
3681
+ constructor(opts: VibeAppHostOptions);
3514
3682
  /**
3515
3683
  * Starts listening for bridge messages and sets up browser back/forward sync.
3516
3684
  * Returns a cleanup function to call on unmount.
@@ -3531,8 +3699,15 @@ declare class VibeAppHost {
3531
3699
  private handlePopState;
3532
3700
  private pushToIframe;
3533
3701
  private handleMessage;
3702
+ /**
3703
+ * Concrete origins a proactive push may target: the exact allowlist entries
3704
+ * plus any origins seen on validated inbound messages. Never includes glob
3705
+ * patterns (you cannot `postMessage` to one). When only exact origins are
3706
+ * configured this equals the configured set, so behaviour is unchanged.
3707
+ */
3708
+ private concreteTargets;
3534
3709
  private handleRequest;
3535
3710
  private respond;
3536
3711
  }
3537
3712
 
3538
- export { type ContextPayload, type HostContext, type HostHandlers, type RequestHandlers, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };
3713
+ export { BridgeError, type ContextPayload, type HostContext, type HostHandlers, HttpBridgeError, type InvalidateCachePayload, type InvalidateResult, type NavigateAction, type NavigatePayload, type RequestCompleteInfo, type RequestHandlers, type SetSideNavPayload, type SwitchSubsidiaryPayload, type UploadPayload, type UploadProgress, type UploadResponse, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };