@nominalso/vibe-bridge 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
@@ -1,4 +1,4 @@
1
- var version = "0.1.0";
1
+ var version = "0.3.0";
2
2
 
3
3
  type AccountingAccountDimensionValues = {
4
4
  account_id: string;
@@ -3203,15 +3203,6 @@ interface GetContextPayload {
3203
3203
  bridgeVersion: string;
3204
3204
  }
3205
3205
 
3206
- /**
3207
- * All operations the iframe can invoke on the host, each with a typed request
3208
- * payload and a typed response. Adding a new operation here is all that's
3209
- * required — message union types and handler types below are derived
3210
- * automatically.
3211
- *
3212
- * Operations that also emit intermediate progress events should include a
3213
- * `progress` field with the progress payload type.
3214
- */
3215
3206
  interface UploadPayload {
3216
3207
  buffer: ArrayBuffer;
3217
3208
  fileName: string;
@@ -3222,11 +3213,39 @@ interface UploadPayload {
3222
3213
  interface UploadResponse {
3223
3214
  attachmentId: string;
3224
3215
  name: string;
3216
+ /** Public/presigned URL of the stored file, when the host returns one. */
3217
+ url?: string;
3225
3218
  }
3226
3219
  interface UploadProgress {
3227
3220
  attachmentId: string;
3228
3221
  progress: number;
3229
3222
  }
3223
+ /** How the host should apply a navigation. Defaults to `'push'`. */
3224
+ type NavigateAction = 'push' | 'replace' | 'refresh';
3225
+ /** Collapse/expand the host's side navigation. */
3226
+ interface SetSideNavPayload {
3227
+ collapsed: boolean;
3228
+ }
3229
+ /** Targets for a host cache invalidation. */
3230
+ interface InvalidateCachePayload {
3231
+ /** API path prefixes to invalidate, e.g. `['/accounting/accounts']`. */
3232
+ paths?: string[];
3233
+ /** Cache tag keys to invalidate, e.g. `['REPORTS', 'SUBSIDIARIES']`. */
3234
+ tags?: string[];
3235
+ }
3236
+ /** Result of a host cache invalidation. */
3237
+ interface InvalidateResult {
3238
+ invalidated: boolean;
3239
+ }
3240
+
3241
+ /**
3242
+ * All operations the iframe can invoke on the host, each with a typed request
3243
+ * payload and a typed response. Adding a new operation here is all that's
3244
+ * required — message union types and handler types are derived automatically.
3245
+ *
3246
+ * Operations that also emit intermediate progress events should include a
3247
+ * `progress` field with the progress payload type.
3248
+ */
3230
3249
  interface RequestRegistry {
3231
3250
  GET_CHART_OF_ACCOUNTS: {
3232
3251
  payload: Omit<GetCoaFlatApiAccountingSubsidiarySubsidiaryIdCoaAccountGetData, 'url'>;
@@ -3412,6 +3431,10 @@ interface RequestRegistry {
3412
3431
  payload: Omit<GetTenantUsersApiTenancyTenantTenantIdOrNameUsersGetData, 'url'>;
3413
3432
  data: GetTenantUsersApiTenancyTenantTenantIdOrNameUsersGetResponse;
3414
3433
  };
3434
+ INVALIDATE_CACHE: {
3435
+ payload: InvalidateCachePayload;
3436
+ data: InvalidateResult;
3437
+ };
3415
3438
  GET_CONTEXT: {
3416
3439
  payload: GetContextPayload;
3417
3440
  data: ContextPayload;
@@ -3423,18 +3446,53 @@ interface RequestRegistry {
3423
3446
  };
3424
3447
  }
3425
3448
 
3426
- interface VibeAppBridgeOptions {
3427
- /**
3428
- * Origin of the Nominal app embedding this iframe (e.g.
3429
- * `https://app.nominal.so` or `http://localhost:3000`). Must match the host
3430
- * origin **exactly** messages from any other origin are ignored, and the
3431
- * bridge only `postMessage`s to this origin. Drive it from an env var so the
3432
- * same code works locally and in production.
3433
- */
3434
- parentOrigin: string;
3435
- /** Per-request timeout in milliseconds before a call rejects. Default `10000`. */
3436
- requestTimeout?: number;
3449
+ /**
3450
+ * An error that crossed the bridge, carrying a machine-readable {@link code}
3451
+ * alongside the human-readable message.
3452
+ *
3453
+ * The host attaches a `code` to failed responses (e.g. `'RATE_LIMITED'`,
3454
+ * `'FILE_TOO_LARGE'`, `'REQUEST_FAILED'`); the bridge rehydrates it into a
3455
+ * `BridgeError` so app `catch` blocks can branch on `error.code` instead of
3456
+ * string-matching `error.message`. A failed Nominal API call rehydrates as the
3457
+ * {@link HttpBridgeError} subtype, which adds the HTTP `status`.
3458
+ *
3459
+ * @example
3460
+ * ```ts
3461
+ * try {
3462
+ * await bridge.getAccounts({})
3463
+ * } catch (err) {
3464
+ * if (err instanceof BridgeError && err.code === 'RATE_LIMITED') {
3465
+ * // back off and retry
3466
+ * }
3467
+ * }
3468
+ * ```
3469
+ */
3470
+ declare class BridgeError extends Error {
3471
+ readonly code: string;
3472
+ constructor(code: string, message: string);
3437
3473
  }
3474
+ /**
3475
+ * A {@link BridgeError} for a failed Nominal API call. Its `code` is always
3476
+ * `'REQUEST_FAILED'`; the HTTP {@link status} is carried as a structured field
3477
+ * so callers can branch on it (e.g. `err.status === 404`) instead of parsing
3478
+ * the code string.
3479
+ *
3480
+ * @example
3481
+ * ```ts
3482
+ * try {
3483
+ * await bridge.getAccount({ path: { account_id: 'missing' } })
3484
+ * } catch (err) {
3485
+ * if (err instanceof HttpBridgeError && err.status === 404) {
3486
+ * // handle not-found
3487
+ * }
3488
+ * }
3489
+ * ```
3490
+ */
3491
+ declare class HttpBridgeError extends BridgeError {
3492
+ readonly status: number;
3493
+ constructor(status: number, message: string);
3494
+ }
3495
+
3438
3496
  interface UploadOptions {
3439
3497
  /** Domain entity the file attaches to, e.g. `'JOURNAL_ENTRY'`. */
3440
3498
  entityType: string;
@@ -3444,72 +3502,17 @@ interface UploadOptions {
3444
3502
  onProgress?: (p: RequestRegistry['UPLOAD_FILE']['progress']) => void;
3445
3503
  }
3446
3504
  /**
3447
- * Iframe-side bridge to the Nominal host. One instance per app: construct it,
3448
- * `await connect()` once, then call data methods, `upload()`, and subroute
3449
- * helpers. Tear down with `destroy()` on unmount.
3450
- *
3451
- * Every data method delegates to {@link VibeAppBridge.request}; the ~46 named
3452
- * methods (e.g. {@link VibeAppBridge.getChartOfAccounts}) are the typed,
3453
- * discoverable surface, and `request('<OP>', payload)` is the escape hatch for
3454
- * any operation in {@link RequestRegistry}.
3455
- *
3456
- * @example
3457
- * ```ts
3458
- * import { VibeAppBridge, type ContextPayload } from '@nominalso/vibe-bridge'
3459
- *
3460
- * const bridge = new VibeAppBridge({ parentOrigin: import.meta.env.VITE_PARENT_ORIGIN })
3461
- *
3462
- * const ctx: ContextPayload = await bridge.connect()
3463
- * const accounts = await bridge.getChartOfAccounts({ path: { subsidiary_id: ctx.subsidiaryId } })
3464
- *
3465
- * // cleanup
3466
- * bridge.destroy()
3467
- * ```
3505
+ * The typed Nominal data methods (~46) plus {@link BridgeDataMethods.upload},
3506
+ * factored onto a base class so {@link VibeAppBridge} stays focused on
3507
+ * connection/transport. Each method is a thin typed wrapper over `request()`;
3508
+ * `VibeAppBridge` provides the concrete `request` implementation.
3468
3509
  */
3469
- declare class VibeAppBridge {
3470
- private readonly parentOrigin;
3471
- private readonly requestTimeout;
3472
- private readonly pendingRequests;
3473
- private readonly boundHandleMessage;
3474
- private context;
3475
- private connectPromise;
3476
- private connectPollInterval;
3477
- private onContextReceived;
3478
- private lastReportedSubroute;
3479
- private suppressSubrouteReport;
3480
- private subrouteCallback;
3481
- private origPushState;
3482
- private origReplaceState;
3483
- private popstateHandler;
3484
- private readonly kindHandlers;
3485
- private dispatchPush;
3486
- private readonly pushHandlers;
3487
- constructor({ parentOrigin, requestTimeout }: VibeAppBridgeOptions);
3510
+ declare abstract class BridgeDataMethods {
3488
3511
  /**
3489
- * Connects to the host and returns the tenant/user {@link ContextPayload}.
3490
- * **Call this once on app init and await it before any other method** — data
3491
- * methods, `upload()`, and subroute reporting all require an established
3492
- * connection. Concurrent calls return the same promise.
3493
- *
3494
- * Actively polls the host every 500ms until a response arrives, which
3495
- * handles the race condition where the host's initial context push arrives
3496
- * before this listener is ready. Rejects with `Bridge connect timed out`
3497
- * after 10 seconds if no context arrives (usually a `parentOrigin` mismatch
3498
- * or the host hasn't mounted).
3499
- *
3500
- * After connecting, if the context includes an initial subroute (e.g. from
3501
- * a deep link), the bridge navigates to it and starts auto-tracking
3502
- * internal navigation via the history API.
3503
- *
3504
- * @returns The tenant/user/subsidiary context for this app session.
3505
- * @throws If no context is received within 10 seconds.
3506
- * @example
3507
- * ```ts
3508
- * const ctx = await bridge.connect()
3509
- * // ctx.tenant, ctx.subsidiaryId, ctx.subsidiaries, ctx.user, ctx.lastClosedPeriodSlug
3510
- * ```
3512
+ * Low-level typed request for **any** operation in {@link RequestRegistry}
3513
+ * implemented by {@link VibeAppBridge}. The named methods below wrap this.
3511
3514
  */
3512
- connect(): Promise<ContextPayload>;
3515
+ abstract request<K extends keyof RequestRegistry>(type: K, payload: RequestRegistry[K]['payload'], onProgress?: (payload: unknown) => void): Promise<RequestRegistry[K]['data']>;
3513
3516
  /**
3514
3517
  * Fetch the flat chart of accounts for a subsidiary.
3515
3518
  *
@@ -3652,6 +3655,128 @@ declare class VibeAppBridge {
3652
3655
  * ```
3653
3656
  */
3654
3657
  upload(file: File, options: UploadOptions): Promise<RequestRegistry['UPLOAD_FILE']['data']>;
3658
+ }
3659
+
3660
+ interface VibeAppBridgeOptions {
3661
+ /**
3662
+ * Origin(s) of the Nominal app embedding this iframe. **Optional.**
3663
+ *
3664
+ * **Omit it** (the recommended default) and the bridge auto-detects the
3665
+ * Nominal page that actually embeds it and talks only to that origin. This is
3666
+ * safe because Nominal serves Vibe Apps behind a `frame-ancestors` CSP, so
3667
+ * only nom-ui can frame the app — a hostile site can't embed it to intercept
3668
+ * what the iframe sends out (`postTaskOutput` bodies, file uploads). Most apps
3669
+ * need nothing here.
3670
+ *
3671
+ * Provide a value to **pin explicitly** — defense in depth, or when deploying
3672
+ * outside Nominal's edge:
3673
+ *
3674
+ * ```ts
3675
+ * parentOrigin: 'https://app.nominal.so' // one exact origin
3676
+ * parentOrigin: ['https://app.nominal.so', 'https://*.vercel.app'] // exact + preview pattern
3677
+ * ```
3678
+ *
3679
+ * A pattern's `*` matches exactly one DNS label or port — anchored, scheme
3680
+ * literal — so `https://*.vercel.app` matches `https://pr-7.vercel.app` but
3681
+ * not `https://a.b.vercel.app` or `https://pr-7.vercel.app.evil.com`. Patterns
3682
+ * are honoured only when this app's own page is served from a recognised
3683
+ * preview host (`*.vercel.app`, `*.lovable.app`, `localhost`, …); on a
3684
+ * production custom domain only exact origins match.
3685
+ *
3686
+ * If neither an explicit value nor an auto-detected parent is available,
3687
+ * `connect()` rejects with `BridgeError` code `PARENT_ORIGIN_UNRESOLVED`.
3688
+ */
3689
+ parentOrigin?: string | string[];
3690
+ /**
3691
+ * Global per-request timeout in milliseconds before a call rejects with a
3692
+ * `BridgeError` whose `code` is `'TIMEOUT'`.
3693
+ *
3694
+ * When omitted, each operation uses an operation-specific default tuned to
3695
+ * 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.
3699
+ */
3700
+ requestTimeout?: number;
3701
+ }
3702
+ /**
3703
+ * Iframe-side bridge to the Nominal host. One instance per app: construct it,
3704
+ * `await connect()` once, then call data methods, `upload()`, and subroute
3705
+ * helpers. Tear down with `destroy()` on unmount.
3706
+ *
3707
+ * Every data method delegates to {@link VibeAppBridge.request}; the ~46 named
3708
+ * methods (e.g. {@link VibeAppBridge.getChartOfAccounts}) are the typed,
3709
+ * discoverable surface, and `request('<OP>', payload)` is the escape hatch for
3710
+ * any operation in {@link RequestRegistry}.
3711
+ *
3712
+ * @example
3713
+ * ```ts
3714
+ * import { VibeAppBridge, type ContextPayload } from '@nominalso/vibe-bridge'
3715
+ *
3716
+ * const bridge = new VibeAppBridge({ parentOrigin: import.meta.env.VITE_PARENT_ORIGIN })
3717
+ *
3718
+ * const ctx: ContextPayload = await bridge.connect()
3719
+ * const accounts = await bridge.getChartOfAccounts({ path: { subsidiary_id: ctx.subsidiaryId } })
3720
+ *
3721
+ * // cleanup
3722
+ * bridge.destroy()
3723
+ * ```
3724
+ */
3725
+ declare class VibeAppBridge extends BridgeDataMethods {
3726
+ /** Concrete origin to `postMessage` to, or `null` if none could be resolved. */
3727
+ private readonly targetOrigin;
3728
+ /** Validates an inbound `event.origin` against the configured allowlist. */
3729
+ private readonly isTrustedOrigin;
3730
+ private readonly requestTimeout;
3731
+ private readonly pendingRequests;
3732
+ private readonly boundHandleMessage;
3733
+ private context;
3734
+ private connectPromise;
3735
+ private connectPollInterval;
3736
+ private connectTimeout;
3737
+ private onContextReceived;
3738
+ private onContextError;
3739
+ /**
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).
3743
+ */
3744
+ private readonly connectPollIds;
3745
+ private lastReportedSubroute;
3746
+ private suppressSubrouteReport;
3747
+ private subrouteCallback;
3748
+ private origPushState;
3749
+ private origReplaceState;
3750
+ private popstateHandler;
3751
+ private readonly kindHandlers;
3752
+ private dispatchPush;
3753
+ private readonly pushHandlers;
3754
+ constructor({ parentOrigin, requestTimeout }?: VibeAppBridgeOptions);
3755
+ /**
3756
+ * Connects to the host and returns the tenant/user {@link ContextPayload}.
3757
+ * **Call this once on app init and await it before any other method** — data
3758
+ * methods, `upload()`, and subroute reporting all require an established
3759
+ * connection. Concurrent calls return the same promise.
3760
+ *
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).
3766
+ *
3767
+ * After connecting, if the context includes an initial subroute (e.g. from
3768
+ * a deep link), the bridge navigates to it and starts auto-tracking
3769
+ * internal navigation via the history API.
3770
+ *
3771
+ * @returns The tenant/user/subsidiary context for this app session.
3772
+ * @throws If no context is received within 10 seconds.
3773
+ * @example
3774
+ * ```ts
3775
+ * const ctx = await bridge.connect()
3776
+ * // ctx.tenant, ctx.subsidiaryId, ctx.subsidiaries, ctx.user, ctx.lastClosedPeriodSlug
3777
+ * ```
3778
+ */
3779
+ connect(): Promise<ContextPayload>;
3655
3780
  /**
3656
3781
  * Manually reports the current subroute to the host.
3657
3782
  * Usually not needed — navigation is auto-detected via the history API.
@@ -3667,6 +3792,55 @@ declare class VibeAppBridge {
3667
3792
  * Returns an unsubscribe function.
3668
3793
  */
3669
3794
  onSubrouteRequest(callback: (subroute: string) => void): () => void;
3795
+ /**
3796
+ * Navigates the host to a raw path relative to this app's tenant/subsidiary
3797
+ * base. Fire-and-forget.
3798
+ *
3799
+ * @example
3800
+ * ```ts
3801
+ * bridge.navigate('/journal-entries/123')
3802
+ * bridge.navigate('/journal-entries/123', 'replace')
3803
+ * ```
3804
+ */
3805
+ navigate(path: string, action?: NavigateAction): void;
3806
+ /**
3807
+ * Navigates the host to a named Nominal route, with optional params for
3808
+ * placeholder segments. Fire-and-forget.
3809
+ *
3810
+ * @example
3811
+ * ```ts
3812
+ * bridge.navigateTo('CHART_OF_ACCOUNTS')
3813
+ * bridge.navigateTo('JOURNAL_ENTRY', { id: '123' }, 'replace')
3814
+ * ```
3815
+ */
3816
+ navigateTo(route: string, routeParams?: Record<string, string>, action?: NavigateAction): void;
3817
+ /** Refreshes the host's current route (re-fetches its server data). Fire-and-forget. */
3818
+ refresh(): void;
3819
+ /**
3820
+ * Commands targeting the host's UI chrome. All fire-and-forget.
3821
+ *
3822
+ * @example
3823
+ * ```ts
3824
+ * bridge.ui.setSideNav({ collapsed: true })
3825
+ * bridge.ui.switchSubsidiary(42)
3826
+ * ```
3827
+ */
3828
+ readonly ui: {
3829
+ /** Collapse or expand the host's side navigation. */
3830
+ setSideNav: (payload: SetSideNavPayload) => void;
3831
+ /** Switch the host to a different subsidiary. */
3832
+ switchSubsidiary: (subsidiaryId: number) => void;
3833
+ };
3834
+ /**
3835
+ * Asks the host to invalidate cached data by path prefix and/or cache tag so
3836
+ * subsequent reads see fresh values. Awaited.
3837
+ *
3838
+ * @example
3839
+ * ```ts
3840
+ * await bridge.invalidateCache({ tags: ['SUBSIDIARIES'], paths: ['/accounting/accounts'] })
3841
+ * ```
3842
+ */
3843
+ invalidateCache(payload: InvalidateCachePayload): Promise<InvalidateResult>;
3670
3844
  destroy(): void;
3671
3845
  private stopConnectPolling;
3672
3846
  /**
@@ -3693,9 +3867,15 @@ declare class VibeAppBridge {
3693
3867
  * ```
3694
3868
  */
3695
3869
  request<K extends keyof RequestRegistry>(type: K, payload: RequestRegistry[K]['payload'], onProgress?: (payload: unknown) => void): Promise<RequestRegistry[K]['data']>;
3870
+ /**
3871
+ * Posts to the embedding host. No-op when no concrete target origin could be
3872
+ * resolved (a config/embedding error) — `connect()` rejects fast in that case
3873
+ * so calls never silently hang waiting on a timeout.
3874
+ */
3875
+ private postToParent;
3696
3876
  private handleMessage;
3697
3877
  private handleResponse;
3698
3878
  private handleProgress;
3699
3879
  }
3700
3880
 
3701
- export { version as BRIDGE_VERSION, type BridgeSubsidiary, type ContextPayload, type RequestRegistry, type UploadOptions, type UploadProgress, type UploadResponse, VibeAppBridge, type VibeAppBridgeOptions };
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 };