@nominalso/vibe-bridge 0.0.1
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/LICENSE +10 -0
- package/README.md +38 -0
- package/dist/index.cjs +340 -0
- package/dist/index.d.cts +3512 -0
- package/dist/index.d.ts +3512 -0
- package/dist/index.js +312 -0
- package/package.json +74 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Copyright © 2026 Nominal. All rights reserved.
|
|
2
|
+
|
|
3
|
+
This software and its source code are proprietary and confidential.
|
|
4
|
+
The packages published from this repository are marked "UNLICENSED": no
|
|
5
|
+
license or right to use, copy, modify, distribute, or create derivative
|
|
6
|
+
works is granted except under a separate written agreement with Nominal.
|
|
7
|
+
|
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
9
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
10
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @nominalso/vibe-bridge
|
|
2
|
+
|
|
3
|
+
Iframe-side SDK for building **Nominal Vibe Apps** — standalone web apps (typically built with Lovable) embedded in the Nominal platform via a cross-origin `<iframe>`. The bridge connects your app to its Nominal host over a typed `postMessage` protocol: fetch Nominal data, submit Close-Management task outputs, upload files, and keep deep-link routing in sync.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install @nominalso/vibe-bridge
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { VibeAppBridge } from '@nominalso/vibe-bridge'
|
|
15
|
+
|
|
16
|
+
const bridge = new VibeAppBridge({
|
|
17
|
+
// Must match the Nominal app origin exactly.
|
|
18
|
+
parentOrigin: import.meta.env.VITE_PARENT_ORIGIN,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Call once on init — resolves when the host context arrives.
|
|
22
|
+
const ctx = await bridge.connect()
|
|
23
|
+
|
|
24
|
+
const accounts = await bridge.getChartOfAccounts()
|
|
25
|
+
await bridge.postTaskOutput(payload)
|
|
26
|
+
await bridge.upload(file, { entityType: 'JOURNAL_ENTRY', entityId: '123' })
|
|
27
|
+
|
|
28
|
+
// On unmount
|
|
29
|
+
bridge.destroy()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The host side is [`@nominalso/vibe-host`](https://www.npmjs.com/package/@nominalso/vibe-host). See the
|
|
33
|
+
[repository](https://github.com/nominalso/vibe-apps-sdk#readme) for the full protocol, architecture, and
|
|
34
|
+
`connect()` semantics.
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
UNLICENSED — proprietary. © Nominal. All rights reserved.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
BRIDGE_VERSION: () => version,
|
|
24
|
+
VibeAppBridge: () => VibeAppBridge
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// package.json
|
|
29
|
+
var version = "0.0.1";
|
|
30
|
+
|
|
31
|
+
// ../protocol-types/dist/index.js
|
|
32
|
+
var PROTOCOL_ID = "nominal-vibe-bridge";
|
|
33
|
+
var PROTOCOL_VERSION = 1;
|
|
34
|
+
var BRIDGE_KINDS = ["request", "response", "push", "command", "progress"];
|
|
35
|
+
var CORRELATED_KINDS = ["request", "response", "progress"];
|
|
36
|
+
var BRIDGE_KINDS_SET = new Set(BRIDGE_KINDS);
|
|
37
|
+
var CORRELATED_KINDS_SET = new Set(CORRELATED_KINDS);
|
|
38
|
+
function isBridgeMessage(data) {
|
|
39
|
+
if (typeof data !== "object" || data === null) return false;
|
|
40
|
+
const d = data;
|
|
41
|
+
if (d.__protocol !== PROTOCOL_ID) return false;
|
|
42
|
+
if (d.__version !== PROTOCOL_VERSION) return false;
|
|
43
|
+
if (typeof d.kind !== "string" || !BRIDGE_KINDS_SET.has(d.kind)) return false;
|
|
44
|
+
if (typeof d.type !== "string") return false;
|
|
45
|
+
if (CORRELATED_KINDS_SET.has(d.kind) && typeof d.requestId !== "string") return false;
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/VibeAppBridge.ts
|
|
50
|
+
var CONNECT_TIMEOUT_MS = 1e4;
|
|
51
|
+
var CONNECT_POLL_MS = 500;
|
|
52
|
+
var VibeAppBridge = class {
|
|
53
|
+
constructor({ parentOrigin, requestTimeout = 1e4 }) {
|
|
54
|
+
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
55
|
+
this.context = null;
|
|
56
|
+
this.connectPromise = null;
|
|
57
|
+
this.connectPollInterval = null;
|
|
58
|
+
this.onContextReceived = null;
|
|
59
|
+
this.lastReportedSubroute = null;
|
|
60
|
+
this.suppressSubrouteReport = false;
|
|
61
|
+
this.subrouteCallback = null;
|
|
62
|
+
this.origPushState = null;
|
|
63
|
+
this.origReplaceState = null;
|
|
64
|
+
this.popstateHandler = null;
|
|
65
|
+
this.kindHandlers = {
|
|
66
|
+
response: (msg) => this.handleResponse(msg),
|
|
67
|
+
push: (msg) => this.dispatchPush(msg),
|
|
68
|
+
progress: (msg) => this.handleProgress(msg)
|
|
69
|
+
};
|
|
70
|
+
this.pushHandlers = {
|
|
71
|
+
CONTEXT_PUSH: (payload) => this.onContextReceived?.(payload),
|
|
72
|
+
SUBROUTE_PUSH: (payload) => this.withSubrouteSuppressed(() => {
|
|
73
|
+
if (this.subrouteCallback) {
|
|
74
|
+
this.subrouteCallback(payload.subroute);
|
|
75
|
+
} else {
|
|
76
|
+
history.pushState(null, "", payload.subroute);
|
|
77
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
78
|
+
}
|
|
79
|
+
this.lastReportedSubroute = payload.subroute;
|
|
80
|
+
})
|
|
81
|
+
};
|
|
82
|
+
this.parentOrigin = parentOrigin;
|
|
83
|
+
this.requestTimeout = requestTimeout;
|
|
84
|
+
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
85
|
+
window.addEventListener("message", this.boundHandleMessage);
|
|
86
|
+
}
|
|
87
|
+
dispatchPush(msg) {
|
|
88
|
+
const handler = this.pushHandlers[msg.type];
|
|
89
|
+
handler(msg.payload);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Connects to the host and returns the tenant/user context.
|
|
93
|
+
* Call once on app init — concurrent calls return the same promise.
|
|
94
|
+
*
|
|
95
|
+
* Actively polls the host every 500ms until a response arrives, which
|
|
96
|
+
* handles the race condition where the host's initial context push arrives
|
|
97
|
+
* before this listener is ready.
|
|
98
|
+
*
|
|
99
|
+
* After connecting, if the context includes an initial subroute (e.g. from
|
|
100
|
+
* a deep link), the bridge navigates to it and starts auto-tracking
|
|
101
|
+
* internal navigation via the history API.
|
|
102
|
+
*/
|
|
103
|
+
connect() {
|
|
104
|
+
if (this.context) return Promise.resolve(this.context);
|
|
105
|
+
if (this.connectPromise) return this.connectPromise;
|
|
106
|
+
this.connectPromise = new Promise((resolve, reject) => {
|
|
107
|
+
const timer = setTimeout(() => {
|
|
108
|
+
this.stopConnectPolling();
|
|
109
|
+
this.connectPromise = null;
|
|
110
|
+
reject(new Error("Bridge connect timed out"));
|
|
111
|
+
}, CONNECT_TIMEOUT_MS);
|
|
112
|
+
this.onContextReceived = (ctx) => {
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
this.stopConnectPolling();
|
|
115
|
+
this.context = ctx;
|
|
116
|
+
this.connectPromise = null;
|
|
117
|
+
this.onContextReceived = null;
|
|
118
|
+
console.log(
|
|
119
|
+
`[vibe-bridge] Connected \u2014 bridge: ${version}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
120
|
+
);
|
|
121
|
+
if (ctx.subroute && ctx.subroute !== "/") {
|
|
122
|
+
this.withSubrouteSuppressed(() => {
|
|
123
|
+
history.replaceState(null, "", ctx.subroute);
|
|
124
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
125
|
+
this.lastReportedSubroute = ctx.subroute;
|
|
126
|
+
});
|
|
127
|
+
} else {
|
|
128
|
+
this.lastReportedSubroute = window.location.pathname + window.location.search;
|
|
129
|
+
}
|
|
130
|
+
this.setupNavigationTracking();
|
|
131
|
+
resolve(ctx);
|
|
132
|
+
};
|
|
133
|
+
const poll = () => {
|
|
134
|
+
const message = {
|
|
135
|
+
__protocol: PROTOCOL_ID,
|
|
136
|
+
__version: PROTOCOL_VERSION,
|
|
137
|
+
kind: "request",
|
|
138
|
+
type: "GET_CONTEXT",
|
|
139
|
+
requestId: crypto.randomUUID(),
|
|
140
|
+
payload: { bridgeVersion: version }
|
|
141
|
+
};
|
|
142
|
+
window.parent.postMessage(message, this.parentOrigin);
|
|
143
|
+
};
|
|
144
|
+
poll();
|
|
145
|
+
this.connectPollInterval = setInterval(poll, CONNECT_POLL_MS);
|
|
146
|
+
});
|
|
147
|
+
return this.connectPromise;
|
|
148
|
+
}
|
|
149
|
+
getChartOfAccounts(payload) {
|
|
150
|
+
return this.request("GET_CHART_OF_ACCOUNTS", payload);
|
|
151
|
+
}
|
|
152
|
+
getSubsidiaries(payload) {
|
|
153
|
+
return this.request("GET_SUBSIDIARIES", payload);
|
|
154
|
+
}
|
|
155
|
+
getPeriods(payload) {
|
|
156
|
+
return this.request("GET_PERIODS", payload);
|
|
157
|
+
}
|
|
158
|
+
postTaskOutput(payload) {
|
|
159
|
+
return this.request("POST_TASK_OUTPUT", payload);
|
|
160
|
+
}
|
|
161
|
+
getJournalEntries(payload) {
|
|
162
|
+
return this.request("GET_JOURNAL_ENTRIES", payload);
|
|
163
|
+
}
|
|
164
|
+
getJournalLines(payload) {
|
|
165
|
+
return this.request("GET_JOURNAL_LINES", payload);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Uploads a file through the host. Converts the File to an ArrayBuffer
|
|
169
|
+
* before sending — sandboxed iframes cannot pass File handles across windows.
|
|
170
|
+
*/
|
|
171
|
+
async upload(file, options) {
|
|
172
|
+
const buffer = await file.arrayBuffer();
|
|
173
|
+
const payload = {
|
|
174
|
+
buffer,
|
|
175
|
+
fileName: file.name,
|
|
176
|
+
fileType: file.type,
|
|
177
|
+
entityType: options.entityType,
|
|
178
|
+
entityId: options.entityId
|
|
179
|
+
};
|
|
180
|
+
const onProgress = options.onProgress ? (p) => options.onProgress(p) : void 0;
|
|
181
|
+
return this.request("UPLOAD_FILE", payload, onProgress);
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Manually reports the current subroute to the host.
|
|
185
|
+
* Usually not needed — navigation is auto-detected via the history API.
|
|
186
|
+
* Use this for hash-based routers or non-standard navigation patterns.
|
|
187
|
+
*/
|
|
188
|
+
reportSubroute(subroute, options) {
|
|
189
|
+
const normalized = subroute.startsWith("/") ? subroute : `/${subroute}`;
|
|
190
|
+
this.lastReportedSubroute = normalized;
|
|
191
|
+
this.sendCommand("REPORT_SUBROUTE", { subroute: normalized, replace: options?.replace });
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Registers a callback for host-initiated navigation (browser back/forward).
|
|
195
|
+
* If no callback is registered, the SDK uses `history.pushState` + a
|
|
196
|
+
* `popstate` event, which works for most SPA routers automatically.
|
|
197
|
+
* Returns an unsubscribe function.
|
|
198
|
+
*/
|
|
199
|
+
onSubrouteRequest(callback) {
|
|
200
|
+
this.subrouteCallback = callback;
|
|
201
|
+
return () => {
|
|
202
|
+
this.subrouteCallback = null;
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
destroy() {
|
|
206
|
+
this.teardownNavigationTracking();
|
|
207
|
+
this.stopConnectPolling();
|
|
208
|
+
window.removeEventListener("message", this.boundHandleMessage);
|
|
209
|
+
for (const pending of this.pendingRequests.values()) {
|
|
210
|
+
pending.reject(new Error("Bridge destroyed"));
|
|
211
|
+
}
|
|
212
|
+
this.pendingRequests.clear();
|
|
213
|
+
this.context = null;
|
|
214
|
+
this.connectPromise = null;
|
|
215
|
+
this.onContextReceived = null;
|
|
216
|
+
this.subrouteCallback = null;
|
|
217
|
+
this.lastReportedSubroute = null;
|
|
218
|
+
}
|
|
219
|
+
stopConnectPolling() {
|
|
220
|
+
if (this.connectPollInterval !== null) {
|
|
221
|
+
clearInterval(this.connectPollInterval);
|
|
222
|
+
this.connectPollInterval = null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Monkey-patches `history.pushState` and `history.replaceState` to
|
|
227
|
+
* auto-detect SPA navigation and report it to the host. Called after
|
|
228
|
+
* connect() resolves.
|
|
229
|
+
*/
|
|
230
|
+
setupNavigationTracking() {
|
|
231
|
+
if (this.origPushState) return;
|
|
232
|
+
this.origPushState = history.pushState.bind(history);
|
|
233
|
+
this.origReplaceState = history.replaceState.bind(history);
|
|
234
|
+
const origPush = this.origPushState;
|
|
235
|
+
const origReplace = this.origReplaceState;
|
|
236
|
+
history.pushState = (...args) => {
|
|
237
|
+
origPush(...args);
|
|
238
|
+
this.reportCurrentSubroute(false);
|
|
239
|
+
};
|
|
240
|
+
history.replaceState = (...args) => {
|
|
241
|
+
origReplace(...args);
|
|
242
|
+
this.reportCurrentSubroute(true);
|
|
243
|
+
};
|
|
244
|
+
this.popstateHandler = () => this.reportCurrentSubroute(true);
|
|
245
|
+
window.addEventListener("popstate", this.popstateHandler);
|
|
246
|
+
}
|
|
247
|
+
teardownNavigationTracking() {
|
|
248
|
+
if (this.origPushState) {
|
|
249
|
+
history.pushState = this.origPushState;
|
|
250
|
+
this.origPushState = null;
|
|
251
|
+
}
|
|
252
|
+
if (this.origReplaceState) {
|
|
253
|
+
history.replaceState = this.origReplaceState;
|
|
254
|
+
this.origReplaceState = null;
|
|
255
|
+
}
|
|
256
|
+
if (this.popstateHandler) {
|
|
257
|
+
window.removeEventListener("popstate", this.popstateHandler);
|
|
258
|
+
this.popstateHandler = null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Runs `fn` while suppressing subroute auto-reporting to prevent echo loops. */
|
|
262
|
+
withSubrouteSuppressed(fn) {
|
|
263
|
+
this.suppressSubrouteReport = true;
|
|
264
|
+
try {
|
|
265
|
+
fn();
|
|
266
|
+
} finally {
|
|
267
|
+
this.suppressSubrouteReport = false;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
reportCurrentSubroute(replace) {
|
|
271
|
+
if (this.suppressSubrouteReport) return;
|
|
272
|
+
const subroute = window.location.pathname + window.location.search;
|
|
273
|
+
if (subroute === this.lastReportedSubroute) return;
|
|
274
|
+
this.lastReportedSubroute = subroute;
|
|
275
|
+
this.sendCommand("REPORT_SUBROUTE", { subroute, replace });
|
|
276
|
+
}
|
|
277
|
+
sendCommand(type, payload) {
|
|
278
|
+
window.parent.postMessage(
|
|
279
|
+
{ __protocol: PROTOCOL_ID, __version: PROTOCOL_VERSION, kind: "command", type, payload },
|
|
280
|
+
this.parentOrigin
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
request(type, payload, onProgress) {
|
|
284
|
+
return new Promise((resolve, reject) => {
|
|
285
|
+
const requestId = crypto.randomUUID();
|
|
286
|
+
const timer = setTimeout(() => {
|
|
287
|
+
this.pendingRequests.delete(requestId);
|
|
288
|
+
reject(new Error(`Vibe bridge request timed out: ${type}`));
|
|
289
|
+
}, this.requestTimeout);
|
|
290
|
+
this.pendingRequests.set(requestId, {
|
|
291
|
+
resolve: (data) => {
|
|
292
|
+
clearTimeout(timer);
|
|
293
|
+
resolve(data);
|
|
294
|
+
},
|
|
295
|
+
reject: (error) => {
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
reject(error);
|
|
298
|
+
},
|
|
299
|
+
onProgress
|
|
300
|
+
});
|
|
301
|
+
const message = {
|
|
302
|
+
__protocol: PROTOCOL_ID,
|
|
303
|
+
__version: PROTOCOL_VERSION,
|
|
304
|
+
kind: "request",
|
|
305
|
+
type,
|
|
306
|
+
requestId,
|
|
307
|
+
payload
|
|
308
|
+
};
|
|
309
|
+
window.parent.postMessage(message, this.parentOrigin);
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
handleMessage(event) {
|
|
313
|
+
if (event.origin !== this.parentOrigin) return;
|
|
314
|
+
if (!isBridgeMessage(event.data)) return;
|
|
315
|
+
this.kindHandlers[event.data.kind]?.(event.data);
|
|
316
|
+
}
|
|
317
|
+
handleResponse(msg) {
|
|
318
|
+
if (msg.type === "GET_CONTEXT") {
|
|
319
|
+
if (msg.ok) this.onContextReceived?.(msg.data);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const pending = this.pendingRequests.get(msg.requestId);
|
|
323
|
+
if (!pending) return;
|
|
324
|
+
this.pendingRequests.delete(msg.requestId);
|
|
325
|
+
if (msg.ok) {
|
|
326
|
+
pending.resolve(msg.data);
|
|
327
|
+
} else {
|
|
328
|
+
pending.reject(new Error(msg.error));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
handleProgress(msg) {
|
|
332
|
+
const pending = this.pendingRequests.get(msg.requestId);
|
|
333
|
+
pending?.onProgress?.(msg.payload);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
337
|
+
0 && (module.exports = {
|
|
338
|
+
BRIDGE_VERSION,
|
|
339
|
+
VibeAppBridge
|
|
340
|
+
});
|