@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 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
+ });