@nominalso/vibe-host 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,8 @@
1
1
  # @nominalso/vibe-host
2
2
 
3
- Host-side SDK for embedding **Nominal Vibe Apps**. Used by the Nominal app (nom-ui) to receive requests from an
4
- embedded Vibe App over a typed `postMessage` protocol and dispatch them to Nominal APIs. The host ships a
5
- **complete, type-checked handler map** covering every protocol operation you only supply a `clientConfig`
6
- pointing at the Nominal API, and the SDK wires up and dispatches all operations for you.
3
+ Host-side SDK for embedding **Nominal Vibe Apps**. Used by the Nominal app (nom-ui) to receive requests from an embedded Vibe App over a typed `postMessage` protocol and dispatch them to Nominal APIs. The host ships a **complete, type-checked handler map** covering every protocol operation — you supply a `clientConfig` pointing at the Nominal API, and the SDK wires up and dispatches all operations for you.
4
+
5
+ > **For AI agents:** you do **not** pass a `handlers` map. Provide `clientConfig` (where the Nominal API lives) and the SDK builds every handler. The iframe-side counterpart is `@nominalso/vibe-bridge`.
7
6
 
8
7
  ## Install
9
8
 
@@ -11,28 +10,93 @@ pointing at the Nominal API, and the SDK wires up and dispatches all operations
11
10
  npm install @nominalso/vibe-host
12
11
  ```
13
12
 
14
- ## Usage
13
+ Externalizes only `@hey-api/client-fetch`; TypeScript types are self-contained.
14
+
15
+ ## Quickstart
15
16
 
16
17
  ```tsx
17
18
  import { VibeAppHost } from '@nominalso/vibe-host'
18
19
 
19
20
  const host = new VibeAppHost({
21
+ // Iframe origin(s) allowed to talk to this host — must match exactly.
20
22
  trustedOrigins: ['https://my-vibe-app.lovable.app'],
23
+ // This Vibe App's base path in nom-ui (used to sync the browser URL).
21
24
  appBasePath: `/${tenant}/${subsidiary}/apps/${slug}`,
25
+ // Context pushed to the iframe on connect.
22
26
  getContext: () => ({ tenant, subsidiaryId, subsidiaries, user, lastClosedPeriodSlug }),
23
27
  // Points the built-in handler map at the Nominal API (e.g. nom-ui's proxy route).
24
- // The SDK provides handlers for every protocol operation out of the box.
25
28
  clientConfig: { baseUrl: '/api/proxy', stripApiPrefix: true },
26
29
  })
27
30
 
28
- // Start listening before the iframe loads, then push context on its `load` event.
31
+ // Start listening BEFORE the iframe loads, then push context on its `load` event.
29
32
  const unmount = host.mount()
30
- // host.pushContextTo(iframe.contentWindow)
33
+ // once the iframe has loaded: host.pushContextTo(iframe.contentWindow)
31
34
  ```
32
35
 
33
- The iframe side is [`@nominalso/vibe-bridge`](https://www.npmjs.com/package/@nominalso/vibe-bridge). See the
34
- [repository](https://github.com/nominalso/vibe-apps-sdk#readme) for the full protocol, architecture, and the
35
- SSR / iframe-mounting notes.
36
+ ## API
37
+
38
+ ### `new VibeAppHost(options)`
39
+
40
+ | Option | Type | Description |
41
+ | ---------------- | ----------------------- | --------------------------------------------------------------------------------------------------------- |
42
+ | `trustedOrigins` | `string[]` | Iframe origins allowed to talk to this host. Messages from other origins are ignored. |
43
+ | `getContext` | `() => HostContext` | Returns the current context to send to the iframe (`hostVersion` is injected automatically). |
44
+ | `appBasePath` | `string` | Base URL path of this Vibe App in nom-ui, e.g. `/acme/1/apps/fixed-assets`. |
45
+ | `clientConfig` | `VibeApiClientOptions?` | Points the built-in handler map at the Nominal API. Defaults to `{ baseUrl: '', stripApiPrefix: false }`. |
46
+
47
+ `VibeApiClientOptions` is `{ baseUrl: string; stripApiPrefix?: boolean }`. Set `stripApiPrefix: true` when routing through a proxy whose convention expects paths without the `/api` prefix.
48
+
49
+ ### `mount(iframeWindow?): () => void`
50
+
51
+ Starts listening for bridge messages and sets up browser back/forward sync. Returns a cleanup function. Pass `iframeWindow` to immediately push context; if omitted, the host answers `GET_CONTEXT` polls but won't proactively push until you call `pushContextTo`.
52
+
53
+ ### `pushContextTo(iframeWindow): void`
54
+
55
+ Pushes the current context to the iframe and stores the window reference for future pushes (e.g. subroute sync). Call once the iframe has loaded if you called `mount()` with no argument.
56
+
57
+ ### Exports
58
+
59
+ `VibeAppHost`, and the types `VibeAppHostOptions`, `HostContext`, `HostHandlers`, `RequestHandlers`, `ContextPayload`, `VibeApiClientOptions`.
60
+
61
+ ## Mounting inside Next.js (nom-ui)
62
+
63
+ The iframe is server-rendered, so it may start loading before React hydrates, and cross-origin `contentDocument` is always `null`. The robust pattern: `mount()` (no arg) immediately to start listening, then `pushContextTo(iframe.contentWindow)` from the iframe's `load` event.
64
+
65
+ ```tsx
66
+ 'use client'
67
+ import { useEffect, useRef } from 'react'
68
+ import { VibeAppHost } from '@nominalso/vibe-host'
69
+
70
+ function VibeAppFrame({ host, src }: { host: VibeAppHost; src: string }) {
71
+ const iframeRef = useRef<HTMLIFrameElement>(null)
72
+
73
+ useEffect(() => {
74
+ const iframe = iframeRef.current
75
+ if (!iframe) return
76
+ const unmount = host.mount() // listen before the iframe loads
77
+ const onLoad = () => iframe.contentWindow && host.pushContextTo(iframe.contentWindow)
78
+ iframe.addEventListener('load', onLoad)
79
+ return () => {
80
+ iframe.removeEventListener('load', onLoad)
81
+ unmount()
82
+ }
83
+ }, [host])
84
+
85
+ return (
86
+ <iframe
87
+ ref={iframeRef}
88
+ src={src}
89
+ sandbox="allow-scripts allow-forms allow-same-origin allow-popups"
90
+ />
91
+ )
92
+ }
93
+ ```
94
+
95
+ Calling `contentWindow.postMessage` while the iframe is still at `about:blank` throws a `DOMException` — that's why context is pushed from the `load` event, not on effect setup.
96
+
97
+ ## How it fits together
98
+
99
+ The iframe side is [`@nominalso/vibe-bridge`](https://www.npmjs.com/package/@nominalso/vibe-bridge). See the [repository](https://github.com/nominalso/vibe-apps-sdk#readme) for the full protocol and architecture.
36
100
 
37
101
  ## License
38
102
 
package/dist/index.cjs CHANGED
@@ -25,7 +25,7 @@ __export(index_exports, {
25
25
  module.exports = __toCommonJS(index_exports);
26
26
 
27
27
  // package.json
28
- var version = "0.0.1";
28
+ var version = "0.1.0";
29
29
 
30
30
  // ../protocol-types/dist/index.js
31
31
  var PROTOCOL_ID = "nominal-vibe-bridge";
package/dist/index.d.cts CHANGED
@@ -3447,6 +3447,11 @@ interface VibeApiClientOptions {
3447
3447
  type HostHandlers = Omit<RequestHandlers, 'GET_CONTEXT'>;
3448
3448
  type HostContext = Omit<ContextPayload, 'hostVersion'>;
3449
3449
  interface VibeAppHostOptions {
3450
+ /**
3451
+ * 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'`).
3454
+ */
3450
3455
  trustedOrigins: string[];
3451
3456
  /** Returns the current context to send to the iframe on connect. */
3452
3457
  getContext: () => HostContext;
@@ -3455,8 +3460,44 @@ interface VibeAppHostOptions {
3455
3460
  * Used to sync the browser URL with the iframe's internal subroute.
3456
3461
  */
3457
3462
  appBasePath: string;
3463
+ /**
3464
+ * Points the built-in handler map at the Nominal API (e.g. nom-ui's proxy
3465
+ * route). The host provides handlers for every protocol operation out of the
3466
+ * box; this just tells them where to send requests. Defaults to
3467
+ * `{ baseUrl: '', stripApiPrefix: false }`.
3468
+ */
3458
3469
  clientConfig?: VibeApiClientOptions;
3459
3470
  }
3471
+ /**
3472
+ * Host-side counterpart to `VibeAppBridge`. Construct it with the trusted
3473
+ * iframe origin(s), a `getContext` callback, the app's base path, and a
3474
+ * `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.
3477
+ *
3478
+ * @example
3479
+ * ```tsx
3480
+ * // In a React (nom-ui) component:
3481
+ * const host = new VibeAppHost({
3482
+ * trustedOrigins: ['https://my-vibe-app.lovable.app'],
3483
+ * appBasePath: `/${tenant}/${subsidiary}/apps/${slug}`,
3484
+ * getContext: () => ({ tenant, subsidiaryId, subsidiaries, user, lastClosedPeriodSlug }),
3485
+ * clientConfig: { baseUrl: '/api/proxy', stripApiPrefix: true },
3486
+ * })
3487
+ *
3488
+ * useEffect(() => {
3489
+ * const iframe = iframeRef.current
3490
+ * if (!iframe) return
3491
+ * const unmount = host.mount() // listen before the iframe loads
3492
+ * const onLoad = () => iframe.contentWindow && host.pushContextTo(iframe.contentWindow)
3493
+ * iframe.addEventListener('load', onLoad)
3494
+ * return () => {
3495
+ * iframe.removeEventListener('load', onLoad)
3496
+ * unmount()
3497
+ * }
3498
+ * }, [host])
3499
+ * ```
3500
+ */
3460
3501
  declare class VibeAppHost {
3461
3502
  private readonly trustedOrigins;
3462
3503
  private readonly handlers;
@@ -3494,4 +3535,4 @@ declare class VibeAppHost {
3494
3535
  private respond;
3495
3536
  }
3496
3537
 
3497
- export { type HostContext, type HostHandlers, type RequestHandlers, VibeAppHost, type VibeAppHostOptions };
3538
+ export { type ContextPayload, type HostContext, type HostHandlers, type RequestHandlers, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };
package/dist/index.d.ts CHANGED
@@ -3447,6 +3447,11 @@ interface VibeApiClientOptions {
3447
3447
  type HostHandlers = Omit<RequestHandlers, 'GET_CONTEXT'>;
3448
3448
  type HostContext = Omit<ContextPayload, 'hostVersion'>;
3449
3449
  interface VibeAppHostOptions {
3450
+ /**
3451
+ * 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'`).
3454
+ */
3450
3455
  trustedOrigins: string[];
3451
3456
  /** Returns the current context to send to the iframe on connect. */
3452
3457
  getContext: () => HostContext;
@@ -3455,8 +3460,44 @@ interface VibeAppHostOptions {
3455
3460
  * Used to sync the browser URL with the iframe's internal subroute.
3456
3461
  */
3457
3462
  appBasePath: string;
3463
+ /**
3464
+ * Points the built-in handler map at the Nominal API (e.g. nom-ui's proxy
3465
+ * route). The host provides handlers for every protocol operation out of the
3466
+ * box; this just tells them where to send requests. Defaults to
3467
+ * `{ baseUrl: '', stripApiPrefix: false }`.
3468
+ */
3458
3469
  clientConfig?: VibeApiClientOptions;
3459
3470
  }
3471
+ /**
3472
+ * Host-side counterpart to `VibeAppBridge`. Construct it with the trusted
3473
+ * iframe origin(s), a `getContext` callback, the app's base path, and a
3474
+ * `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.
3477
+ *
3478
+ * @example
3479
+ * ```tsx
3480
+ * // In a React (nom-ui) component:
3481
+ * const host = new VibeAppHost({
3482
+ * trustedOrigins: ['https://my-vibe-app.lovable.app'],
3483
+ * appBasePath: `/${tenant}/${subsidiary}/apps/${slug}`,
3484
+ * getContext: () => ({ tenant, subsidiaryId, subsidiaries, user, lastClosedPeriodSlug }),
3485
+ * clientConfig: { baseUrl: '/api/proxy', stripApiPrefix: true },
3486
+ * })
3487
+ *
3488
+ * useEffect(() => {
3489
+ * const iframe = iframeRef.current
3490
+ * if (!iframe) return
3491
+ * const unmount = host.mount() // listen before the iframe loads
3492
+ * const onLoad = () => iframe.contentWindow && host.pushContextTo(iframe.contentWindow)
3493
+ * iframe.addEventListener('load', onLoad)
3494
+ * return () => {
3495
+ * iframe.removeEventListener('load', onLoad)
3496
+ * unmount()
3497
+ * }
3498
+ * }, [host])
3499
+ * ```
3500
+ */
3460
3501
  declare class VibeAppHost {
3461
3502
  private readonly trustedOrigins;
3462
3503
  private readonly handlers;
@@ -3494,4 +3535,4 @@ declare class VibeAppHost {
3494
3535
  private respond;
3495
3536
  }
3496
3537
 
3497
- export { type HostContext, type HostHandlers, type RequestHandlers, VibeAppHost, type VibeAppHostOptions };
3538
+ export { type ContextPayload, type HostContext, type HostHandlers, type RequestHandlers, type VibeApiClientOptions, VibeAppHost, type VibeAppHostOptions };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "0.0.1";
2
+ var version = "0.1.0";
3
3
 
4
4
  // ../protocol-types/dist/index.js
5
5
  var PROTOCOL_ID = "nominal-vibe-bridge";
package/package.json CHANGED
@@ -1,18 +1,8 @@
1
1
  {
2
2
  "name": "@nominalso/vibe-host",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "description": "Host-side SDK for embedding Nominal Vibe Apps — receives bridge requests over a typed postMessage protocol and dispatches them to Nominal APIs (used by nom-ui).",
5
5
  "license": "UNLICENSED",
6
- "keywords": [
7
- "nominal",
8
- "vibe-apps",
9
- "vibe-app",
10
- "sdk",
11
- "iframe",
12
- "postmessage",
13
- "host",
14
- "embed"
15
- ],
16
6
  "homepage": "https://github.com/nominalso/vibe-apps-sdk#readme",
17
7
  "bugs": {
18
8
  "url": "https://github.com/nominalso/vibe-apps-sdk/issues"