@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/AGENTS.md +9 -8
- package/README.md +60 -20
- package/dist/index.cjs +409 -125
- package/dist/index.d.cts +265 -85
- package/dist/index.d.ts +265 -85
- package/dist/index.js +407 -125
- package/docs/connect-lifecycle.md +3 -3
- package/docs/data-fetching.md +7 -2
- package/docs/file-upload.md +8 -1
- package/docs/getting-started.md +12 -10
- package/llms.txt +2 -2
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -15,9 +15,7 @@ The host counterpart is `@nominalso/vibe-host` (used by Nominal's own app — yo
|
|
|
15
15
|
```ts
|
|
16
16
|
import { VibeAppBridge, type ContextPayload } from '@nominalso/vibe-bridge'
|
|
17
17
|
|
|
18
|
-
const bridge = new VibeAppBridge(
|
|
19
|
-
parentOrigin: import.meta.env.VITE_PARENT_ORIGIN, // exact origin of the embedding Nominal app
|
|
20
|
-
})
|
|
18
|
+
const bridge = new VibeAppBridge() // no config — auto-detects the embedding Nominal app
|
|
21
19
|
|
|
22
20
|
const ctx: ContextPayload = await bridge.connect() // call ONCE, await before anything else
|
|
23
21
|
|
|
@@ -32,15 +30,18 @@ bridge.destroy() // on unmount
|
|
|
32
30
|
|
|
33
31
|
## Core surface
|
|
34
32
|
|
|
35
|
-
- `new VibeAppBridge({ parentOrigin
|
|
36
|
-
- `connect(): Promise<ContextPayload>` — call once; rejects after 10 s on `
|
|
33
|
+
- `new VibeAppBridge({ parentOrigin?, requestTimeout? })` — both optional; `new VibeAppBridge()` auto-detects the host. `requestTimeout` omitted → tuned per-op defaults (API `30000`, `UPLOAD_FILE` `120000`, `INVALIDATE_CACHE` `10000`, `GET_CONTEXT` `5000` ms).
|
|
34
|
+
- `connect(): Promise<ContextPayload>` — call once; rejects after 10 s on a host that never mounts, or immediately with code `PARENT_ORIGIN_UNRESOLVED` if no host could be detected/pinned.
|
|
37
35
|
- ~46 typed data methods, e.g. `getChartOfAccounts`, `getSubsidiaries`, `getPeriods`, `getJournalEntries`, `getAccounts`, `postTaskOutput`. Each takes the operation's `payload` and returns its `data`.
|
|
38
36
|
- `request('<OPERATION>', payload, onProgress?)` — typed escape hatch for any operation; `'<OPERATION>'` autocompletes and narrows the payload/return types.
|
|
39
37
|
- `upload(file: File, { entityType, entityId?, onProgress? }): Promise<{ attachmentId, name }>`.
|
|
38
|
+
- `navigate`/`navigateTo`/`refresh` and `ui.setSideNav`/`ui.switchSubsidiary` — drive the host's chrome (router, side nav, subsidiary switcher); `invalidateCache(...)` busts host caches.
|
|
40
39
|
- `reportSubroute(subroute, { replace? })`, `onSubrouteRequest(cb): () => void` — usually unnecessary; standard SPA navigation is auto-tracked.
|
|
41
40
|
- `destroy()`.
|
|
42
41
|
|
|
43
|
-
|
|
42
|
+
A failed operation throws a `BridgeError` (`extends Error`) with a machine-readable `code` (`'RATE_LIMITED'`, `'FILE_TOO_LARGE'`, `'TIMEOUT'`, …); branch on `err.code` rather than the message. A failed Nominal API call throws the `HttpBridgeError` subtype (`code: 'REQUEST_FAILED'`) carrying the HTTP `status` — branch on `err.status` (e.g. `404`).
|
|
43
|
+
|
|
44
|
+
Exported values/types: `VibeAppBridge`, `BridgeError`, `BRIDGE_VERSION`, and types `VibeAppBridgeOptions`, `UploadOptions`, `ContextPayload`, `BridgeSubsidiary`, `UploadResponse`, `UploadProgress`, `RequestRegistry`.
|
|
44
45
|
|
|
45
46
|
`ContextPayload` shape: `{ tenant, subsidiaryId, subsidiaries: { id, displayName }[], user: { id, displayName }, subroute?, lastClosedPeriodSlug?, hostVersion }`.
|
|
46
47
|
|
|
@@ -49,7 +50,7 @@ The full method/operation list is in `README.md` ("Operation catalog") and in `d
|
|
|
49
50
|
## Gotchas (wrong → right)
|
|
50
51
|
|
|
51
52
|
- **Don't call data methods before `connect()` resolves.** Always `await bridge.connect()` first.
|
|
52
|
-
- **`parentOrigin` is an origin
|
|
53
|
+
- **`parentOrigin` is optional** — omit it and the bridge auto-detects the embedding host. If you do pass it, it's an origin or list of origins (not a URL): `https://app.nominal.so` ✅ — not `https://app.nominal.so/app`, no trailing slash. For preview testing pass a list with a glob: `['https://app.nominal.so', 'https://*.vercel.app']` — patterns match only when the app itself runs on a preview host.
|
|
53
54
|
- **`upload()` takes a `File`**, not a path or `FormData`.
|
|
54
55
|
- **Writes go through `postTaskOutput` only.** Vibe Apps never write Nominal data directly.
|
|
55
56
|
- **Call `bridge.destroy()` on unmount** to remove listeners and reject pending requests.
|
|
@@ -57,5 +58,5 @@ The full method/operation list is in `README.md` ("Operation catalog") and in `d
|
|
|
57
58
|
## Don't
|
|
58
59
|
|
|
59
60
|
- Don't poll or busy-wait for context — `await connect()` resolves when it's ready.
|
|
60
|
-
- Don't
|
|
61
|
+
- Don't pass `parentOrigin` unless you mean to pin the host; the default auto-detects it. If you pin, read it from an env var (and add a preview pattern to the list when testing deploy previews).
|
|
61
62
|
- Don't import from `@nominalso/vibe-protocol-types` / `vibe-api-types` — they are not published; all public types are re-exported from `@nominalso/vibe-bridge`.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Iframe-side SDK for building **Nominal Vibe Apps** — standalone web apps (typically built with [Lovable](https://lovable.dev)) embedded in the Nominal platform via a cross-origin `<iframe>`. The bridge connects your app to its Nominal host over a typed `postMessage` protocol: read Nominal data, submit Close-Management task outputs, upload files, and keep deep-link routing in sync.
|
|
4
4
|
|
|
5
|
-
> **For AI agents / Lovable:** this is a browser-only SDK. The wiring is always the same —
|
|
5
|
+
> **For AI agents / Lovable:** this is a browser-only SDK. The wiring is always the same — `new VibeAppBridge()` (no config — it auto-detects the Nominal host), `await bridge.connect()` **once**, then call data methods. Copy the quickstart below verbatim; it is the complete happy path.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -17,11 +17,10 @@ Zero runtime dependencies. Ships ESM + CJS and self-contained TypeScript types.
|
|
|
17
17
|
```ts
|
|
18
18
|
import { VibeAppBridge, type ContextPayload } from '@nominalso/vibe-bridge'
|
|
19
19
|
|
|
20
|
-
// 1. Construct
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
})
|
|
20
|
+
// 1. Construct. No config needed — the bridge auto-detects the Nominal app
|
|
21
|
+
// that embeds it. (Pin the host origin explicitly only if you want to;
|
|
22
|
+
// see "Pinning the host origin" below.)
|
|
23
|
+
const bridge = new VibeAppBridge()
|
|
25
24
|
|
|
26
25
|
// 2. Connect ONCE on init and await it before any other call.
|
|
27
26
|
// Resolves with the tenant/user context (or rejects after 10s).
|
|
@@ -47,28 +46,47 @@ await bridge.postTaskOutput({ path: { task_instance_id: 'task-1' }, body: {} })
|
|
|
47
46
|
bridge.destroy()
|
|
48
47
|
```
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
### Pinning the host origin (optional)
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
By default you pass **nothing** — the bridge talks only to the Nominal page that
|
|
52
|
+
actually embeds it. That's safe because Nominal serves Vibe Apps behind a
|
|
53
|
+
`frame-ancestors` CSP, so only nom-ui can frame your app. Most apps need nothing
|
|
54
|
+
more.
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
Pass `parentOrigin` to pin explicitly — defense in depth, or when running
|
|
57
|
+
outside Nominal's edge. Drive it from an env var, or use a list / **glob
|
|
58
|
+
patterns** to accept dynamic preview deployments:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
new VibeAppBridge({ parentOrigin: import.meta.env.VITE_PARENT_ORIGIN })
|
|
62
|
+
new VibeAppBridge({ parentOrigin: ['https://app.nominal.so', 'https://*.vercel.app'] })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
# .env.local
|
|
57
67
|
VITE_PARENT_ORIGIN=http://localhost:3000
|
|
58
68
|
```
|
|
59
69
|
|
|
70
|
+
A pattern's `*` matches exactly one DNS label or port (anchored, scheme
|
|
71
|
+
literal): `https://*.vercel.app` matches `https://pr-7.vercel.app` but not
|
|
72
|
+
`https://a.b.vercel.app` or `https://pr-7.vercel.app.evil.com`. **Patterns are
|
|
73
|
+
honoured only when this app's own page is served from a recognised preview host**
|
|
74
|
+
(`*.vercel.app`, `*.lovable.app`, `localhost`, …) — on a production custom
|
|
75
|
+
domain pattern entries are ignored and only exact origins match. Always include
|
|
76
|
+
at least one exact origin alongside any patterns.
|
|
77
|
+
|
|
60
78
|
## API
|
|
61
79
|
|
|
62
80
|
### `new VibeAppBridge(options)`
|
|
63
81
|
|
|
64
|
-
| Option | Type
|
|
65
|
-
| ---------------- |
|
|
66
|
-
| `parentOrigin` | `string` |
|
|
67
|
-
| `requestTimeout` | `number`
|
|
82
|
+
| Option | Type | Default | Description |
|
|
83
|
+
| ---------------- | -------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| `parentOrigin` | `string \| string[]` | _auto_ | **Optional.** Omit to auto-detect the embedding Nominal host. Pass to pin: exact origins always match; glob patterns (`https://*.vercel.app`) match only when this app runs on a preview host. See above. |
|
|
85
|
+
| `requestTimeout` | `number` | _per-op_ | Global timeout (ms) before a call rejects with `BridgeError` code `'TIMEOUT'`. Omit to use per-operation defaults: API `30000`, `UPLOAD_FILE` `120000`, `INVALIDATE_CACHE` `10000`, `GET_CONTEXT` `5000`. |
|
|
68
86
|
|
|
69
87
|
### `connect(): Promise<ContextPayload>`
|
|
70
88
|
|
|
71
|
-
Call **once** on init and `await` it before anything else. Polls the host every 500 ms until context arrives; rejects with `Bridge connect timed out` after 10 s (usually a `parentOrigin` mismatch or the host hasn't mounted). Concurrent calls return the same promise.
|
|
89
|
+
Call **once** on init and `await` it before anything else. Polls the host every 500 ms until context arrives; rejects with `Bridge connect timed out` after 10 s (usually a `parentOrigin` mismatch or the host hasn't mounted). If no concrete parent origin can be resolved (e.g. a pattern-only `parentOrigin` on a non-preview host), it rejects immediately with `BridgeError` code `PARENT_ORIGIN_UNRESOLVED`. Concurrent calls return the same promise.
|
|
72
90
|
|
|
73
91
|
### Data operations
|
|
74
92
|
|
|
@@ -102,7 +120,7 @@ Removes listeners, rejects pending requests, and restores patched history method
|
|
|
102
120
|
|
|
103
121
|
### Exports
|
|
104
122
|
|
|
105
|
-
`VibeAppBridge`, `BRIDGE_VERSION`, and the types `VibeAppBridgeOptions`, `UploadOptions`, `ContextPayload`, `BridgeSubsidiary`, `UploadResponse`, `UploadProgress`, `RequestRegistry`.
|
|
123
|
+
`VibeAppBridge`, `BridgeError`, `BRIDGE_VERSION`, and the types `VibeAppBridgeOptions`, `UploadOptions`, `ContextPayload`, `BridgeSubsidiary`, `UploadResponse`, `UploadProgress`, `RequestRegistry`.
|
|
106
124
|
|
|
107
125
|
## Common recipes
|
|
108
126
|
|
|
@@ -115,7 +133,7 @@ import { VibeAppBridge, type ContextPayload } from '@nominalso/vibe-bridge'
|
|
|
115
133
|
function useVibeBridge() {
|
|
116
134
|
const [ctx, setCtx] = useState<ContextPayload | null>(null)
|
|
117
135
|
useEffect(() => {
|
|
118
|
-
const bridge = new VibeAppBridge(
|
|
136
|
+
const bridge = new VibeAppBridge()
|
|
119
137
|
bridge.connect().then(setCtx).catch(console.error)
|
|
120
138
|
return () => bridge.destroy()
|
|
121
139
|
}, [])
|
|
@@ -138,6 +156,26 @@ await bridge.upload(file, {
|
|
|
138
156
|
const events = await bridge.request('GET_AUDIT_EVENTS', {})
|
|
139
157
|
```
|
|
140
158
|
|
|
159
|
+
**Handle a failure by its code:**
|
|
160
|
+
|
|
161
|
+
A rejected operation throws a `BridgeError` carrying the host's `code` (`'RATE_LIMITED'`, `'FILE_TOO_LARGE'`, `'TIMEOUT'`, …); branch on it instead of string-matching the message. A failed Nominal API call throws the `HttpBridgeError` subtype (`code: 'REQUEST_FAILED'`), which adds the HTTP `status` — branch on `err.status` (e.g. `404`).
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { BridgeError, HttpBridgeError } from '@nominalso/vibe-bridge'
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
await bridge.getAccounts({})
|
|
168
|
+
} catch (err) {
|
|
169
|
+
if (err instanceof HttpBridgeError && err.status === 404) {
|
|
170
|
+
// handle not-found
|
|
171
|
+
}
|
|
172
|
+
if (err instanceof BridgeError && err.code === 'RATE_LIMITED') {
|
|
173
|
+
// back off and retry
|
|
174
|
+
}
|
|
175
|
+
throw err
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
141
179
|
## Common mistakes
|
|
142
180
|
|
|
143
181
|
```ts
|
|
@@ -152,13 +190,15 @@ const accounts = await bridge.getChartOfAccounts({ path: { subsidiary_id: ctx.su
|
|
|
152
190
|
```
|
|
153
191
|
|
|
154
192
|
```ts
|
|
155
|
-
// ❌ WRONG —
|
|
193
|
+
// ❌ WRONG — each entry must be an ORIGIN (scheme + host + port), not a URL with a path.
|
|
156
194
|
new VibeAppBridge({ parentOrigin: 'https://app.nominal.so/some/path' })
|
|
157
195
|
// ❌ a trailing slash or wrong port also fails → connect() times out after 10s.
|
|
158
196
|
new VibeAppBridge({ parentOrigin: 'http://localhost:3000/' })
|
|
159
197
|
|
|
160
|
-
// ✅ CORRECT — scheme + host + port only,
|
|
198
|
+
// ✅ CORRECT — scheme + host + port only, matching the embedding app.
|
|
161
199
|
new VibeAppBridge({ parentOrigin: 'https://app.nominal.so' })
|
|
200
|
+
// ✅ CORRECT — a list with a preview pattern (honoured only on a preview host).
|
|
201
|
+
new VibeAppBridge({ parentOrigin: ['https://app.nominal.so', 'https://*.vercel.app'] })
|
|
162
202
|
```
|
|
163
203
|
|
|
164
204
|
```ts
|