@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/dist/index.js
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// package.json
|
|
2
|
+
var version = "0.0.1";
|
|
3
|
+
|
|
4
|
+
// ../protocol-types/dist/index.js
|
|
5
|
+
var PROTOCOL_ID = "nominal-vibe-bridge";
|
|
6
|
+
var PROTOCOL_VERSION = 1;
|
|
7
|
+
var BRIDGE_KINDS = ["request", "response", "push", "command", "progress"];
|
|
8
|
+
var CORRELATED_KINDS = ["request", "response", "progress"];
|
|
9
|
+
var BRIDGE_KINDS_SET = new Set(BRIDGE_KINDS);
|
|
10
|
+
var CORRELATED_KINDS_SET = new Set(CORRELATED_KINDS);
|
|
11
|
+
function isBridgeMessage(data) {
|
|
12
|
+
if (typeof data !== "object" || data === null) return false;
|
|
13
|
+
const d = data;
|
|
14
|
+
if (d.__protocol !== PROTOCOL_ID) return false;
|
|
15
|
+
if (d.__version !== PROTOCOL_VERSION) return false;
|
|
16
|
+
if (typeof d.kind !== "string" || !BRIDGE_KINDS_SET.has(d.kind)) return false;
|
|
17
|
+
if (typeof d.type !== "string") return false;
|
|
18
|
+
if (CORRELATED_KINDS_SET.has(d.kind) && typeof d.requestId !== "string") return false;
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/VibeAppBridge.ts
|
|
23
|
+
var CONNECT_TIMEOUT_MS = 1e4;
|
|
24
|
+
var CONNECT_POLL_MS = 500;
|
|
25
|
+
var VibeAppBridge = class {
|
|
26
|
+
constructor({ parentOrigin, requestTimeout = 1e4 }) {
|
|
27
|
+
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
28
|
+
this.context = null;
|
|
29
|
+
this.connectPromise = null;
|
|
30
|
+
this.connectPollInterval = null;
|
|
31
|
+
this.onContextReceived = null;
|
|
32
|
+
this.lastReportedSubroute = null;
|
|
33
|
+
this.suppressSubrouteReport = false;
|
|
34
|
+
this.subrouteCallback = null;
|
|
35
|
+
this.origPushState = null;
|
|
36
|
+
this.origReplaceState = null;
|
|
37
|
+
this.popstateHandler = null;
|
|
38
|
+
this.kindHandlers = {
|
|
39
|
+
response: (msg) => this.handleResponse(msg),
|
|
40
|
+
push: (msg) => this.dispatchPush(msg),
|
|
41
|
+
progress: (msg) => this.handleProgress(msg)
|
|
42
|
+
};
|
|
43
|
+
this.pushHandlers = {
|
|
44
|
+
CONTEXT_PUSH: (payload) => this.onContextReceived?.(payload),
|
|
45
|
+
SUBROUTE_PUSH: (payload) => this.withSubrouteSuppressed(() => {
|
|
46
|
+
if (this.subrouteCallback) {
|
|
47
|
+
this.subrouteCallback(payload.subroute);
|
|
48
|
+
} else {
|
|
49
|
+
history.pushState(null, "", payload.subroute);
|
|
50
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
51
|
+
}
|
|
52
|
+
this.lastReportedSubroute = payload.subroute;
|
|
53
|
+
})
|
|
54
|
+
};
|
|
55
|
+
this.parentOrigin = parentOrigin;
|
|
56
|
+
this.requestTimeout = requestTimeout;
|
|
57
|
+
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
58
|
+
window.addEventListener("message", this.boundHandleMessage);
|
|
59
|
+
}
|
|
60
|
+
dispatchPush(msg) {
|
|
61
|
+
const handler = this.pushHandlers[msg.type];
|
|
62
|
+
handler(msg.payload);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Connects to the host and returns the tenant/user context.
|
|
66
|
+
* Call once on app init — concurrent calls return the same promise.
|
|
67
|
+
*
|
|
68
|
+
* Actively polls the host every 500ms until a response arrives, which
|
|
69
|
+
* handles the race condition where the host's initial context push arrives
|
|
70
|
+
* before this listener is ready.
|
|
71
|
+
*
|
|
72
|
+
* After connecting, if the context includes an initial subroute (e.g. from
|
|
73
|
+
* a deep link), the bridge navigates to it and starts auto-tracking
|
|
74
|
+
* internal navigation via the history API.
|
|
75
|
+
*/
|
|
76
|
+
connect() {
|
|
77
|
+
if (this.context) return Promise.resolve(this.context);
|
|
78
|
+
if (this.connectPromise) return this.connectPromise;
|
|
79
|
+
this.connectPromise = new Promise((resolve, reject) => {
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
this.stopConnectPolling();
|
|
82
|
+
this.connectPromise = null;
|
|
83
|
+
reject(new Error("Bridge connect timed out"));
|
|
84
|
+
}, CONNECT_TIMEOUT_MS);
|
|
85
|
+
this.onContextReceived = (ctx) => {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
this.stopConnectPolling();
|
|
88
|
+
this.context = ctx;
|
|
89
|
+
this.connectPromise = null;
|
|
90
|
+
this.onContextReceived = null;
|
|
91
|
+
console.log(
|
|
92
|
+
`[vibe-bridge] Connected \u2014 bridge: ${version}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
93
|
+
);
|
|
94
|
+
if (ctx.subroute && ctx.subroute !== "/") {
|
|
95
|
+
this.withSubrouteSuppressed(() => {
|
|
96
|
+
history.replaceState(null, "", ctx.subroute);
|
|
97
|
+
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
98
|
+
this.lastReportedSubroute = ctx.subroute;
|
|
99
|
+
});
|
|
100
|
+
} else {
|
|
101
|
+
this.lastReportedSubroute = window.location.pathname + window.location.search;
|
|
102
|
+
}
|
|
103
|
+
this.setupNavigationTracking();
|
|
104
|
+
resolve(ctx);
|
|
105
|
+
};
|
|
106
|
+
const poll = () => {
|
|
107
|
+
const message = {
|
|
108
|
+
__protocol: PROTOCOL_ID,
|
|
109
|
+
__version: PROTOCOL_VERSION,
|
|
110
|
+
kind: "request",
|
|
111
|
+
type: "GET_CONTEXT",
|
|
112
|
+
requestId: crypto.randomUUID(),
|
|
113
|
+
payload: { bridgeVersion: version }
|
|
114
|
+
};
|
|
115
|
+
window.parent.postMessage(message, this.parentOrigin);
|
|
116
|
+
};
|
|
117
|
+
poll();
|
|
118
|
+
this.connectPollInterval = setInterval(poll, CONNECT_POLL_MS);
|
|
119
|
+
});
|
|
120
|
+
return this.connectPromise;
|
|
121
|
+
}
|
|
122
|
+
getChartOfAccounts(payload) {
|
|
123
|
+
return this.request("GET_CHART_OF_ACCOUNTS", payload);
|
|
124
|
+
}
|
|
125
|
+
getSubsidiaries(payload) {
|
|
126
|
+
return this.request("GET_SUBSIDIARIES", payload);
|
|
127
|
+
}
|
|
128
|
+
getPeriods(payload) {
|
|
129
|
+
return this.request("GET_PERIODS", payload);
|
|
130
|
+
}
|
|
131
|
+
postTaskOutput(payload) {
|
|
132
|
+
return this.request("POST_TASK_OUTPUT", payload);
|
|
133
|
+
}
|
|
134
|
+
getJournalEntries(payload) {
|
|
135
|
+
return this.request("GET_JOURNAL_ENTRIES", payload);
|
|
136
|
+
}
|
|
137
|
+
getJournalLines(payload) {
|
|
138
|
+
return this.request("GET_JOURNAL_LINES", payload);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Uploads a file through the host. Converts the File to an ArrayBuffer
|
|
142
|
+
* before sending — sandboxed iframes cannot pass File handles across windows.
|
|
143
|
+
*/
|
|
144
|
+
async upload(file, options) {
|
|
145
|
+
const buffer = await file.arrayBuffer();
|
|
146
|
+
const payload = {
|
|
147
|
+
buffer,
|
|
148
|
+
fileName: file.name,
|
|
149
|
+
fileType: file.type,
|
|
150
|
+
entityType: options.entityType,
|
|
151
|
+
entityId: options.entityId
|
|
152
|
+
};
|
|
153
|
+
const onProgress = options.onProgress ? (p) => options.onProgress(p) : void 0;
|
|
154
|
+
return this.request("UPLOAD_FILE", payload, onProgress);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Manually reports the current subroute to the host.
|
|
158
|
+
* Usually not needed — navigation is auto-detected via the history API.
|
|
159
|
+
* Use this for hash-based routers or non-standard navigation patterns.
|
|
160
|
+
*/
|
|
161
|
+
reportSubroute(subroute, options) {
|
|
162
|
+
const normalized = subroute.startsWith("/") ? subroute : `/${subroute}`;
|
|
163
|
+
this.lastReportedSubroute = normalized;
|
|
164
|
+
this.sendCommand("REPORT_SUBROUTE", { subroute: normalized, replace: options?.replace });
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Registers a callback for host-initiated navigation (browser back/forward).
|
|
168
|
+
* If no callback is registered, the SDK uses `history.pushState` + a
|
|
169
|
+
* `popstate` event, which works for most SPA routers automatically.
|
|
170
|
+
* Returns an unsubscribe function.
|
|
171
|
+
*/
|
|
172
|
+
onSubrouteRequest(callback) {
|
|
173
|
+
this.subrouteCallback = callback;
|
|
174
|
+
return () => {
|
|
175
|
+
this.subrouteCallback = null;
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
destroy() {
|
|
179
|
+
this.teardownNavigationTracking();
|
|
180
|
+
this.stopConnectPolling();
|
|
181
|
+
window.removeEventListener("message", this.boundHandleMessage);
|
|
182
|
+
for (const pending of this.pendingRequests.values()) {
|
|
183
|
+
pending.reject(new Error("Bridge destroyed"));
|
|
184
|
+
}
|
|
185
|
+
this.pendingRequests.clear();
|
|
186
|
+
this.context = null;
|
|
187
|
+
this.connectPromise = null;
|
|
188
|
+
this.onContextReceived = null;
|
|
189
|
+
this.subrouteCallback = null;
|
|
190
|
+
this.lastReportedSubroute = null;
|
|
191
|
+
}
|
|
192
|
+
stopConnectPolling() {
|
|
193
|
+
if (this.connectPollInterval !== null) {
|
|
194
|
+
clearInterval(this.connectPollInterval);
|
|
195
|
+
this.connectPollInterval = null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Monkey-patches `history.pushState` and `history.replaceState` to
|
|
200
|
+
* auto-detect SPA navigation and report it to the host. Called after
|
|
201
|
+
* connect() resolves.
|
|
202
|
+
*/
|
|
203
|
+
setupNavigationTracking() {
|
|
204
|
+
if (this.origPushState) return;
|
|
205
|
+
this.origPushState = history.pushState.bind(history);
|
|
206
|
+
this.origReplaceState = history.replaceState.bind(history);
|
|
207
|
+
const origPush = this.origPushState;
|
|
208
|
+
const origReplace = this.origReplaceState;
|
|
209
|
+
history.pushState = (...args) => {
|
|
210
|
+
origPush(...args);
|
|
211
|
+
this.reportCurrentSubroute(false);
|
|
212
|
+
};
|
|
213
|
+
history.replaceState = (...args) => {
|
|
214
|
+
origReplace(...args);
|
|
215
|
+
this.reportCurrentSubroute(true);
|
|
216
|
+
};
|
|
217
|
+
this.popstateHandler = () => this.reportCurrentSubroute(true);
|
|
218
|
+
window.addEventListener("popstate", this.popstateHandler);
|
|
219
|
+
}
|
|
220
|
+
teardownNavigationTracking() {
|
|
221
|
+
if (this.origPushState) {
|
|
222
|
+
history.pushState = this.origPushState;
|
|
223
|
+
this.origPushState = null;
|
|
224
|
+
}
|
|
225
|
+
if (this.origReplaceState) {
|
|
226
|
+
history.replaceState = this.origReplaceState;
|
|
227
|
+
this.origReplaceState = null;
|
|
228
|
+
}
|
|
229
|
+
if (this.popstateHandler) {
|
|
230
|
+
window.removeEventListener("popstate", this.popstateHandler);
|
|
231
|
+
this.popstateHandler = null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/** Runs `fn` while suppressing subroute auto-reporting to prevent echo loops. */
|
|
235
|
+
withSubrouteSuppressed(fn) {
|
|
236
|
+
this.suppressSubrouteReport = true;
|
|
237
|
+
try {
|
|
238
|
+
fn();
|
|
239
|
+
} finally {
|
|
240
|
+
this.suppressSubrouteReport = false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
reportCurrentSubroute(replace) {
|
|
244
|
+
if (this.suppressSubrouteReport) return;
|
|
245
|
+
const subroute = window.location.pathname + window.location.search;
|
|
246
|
+
if (subroute === this.lastReportedSubroute) return;
|
|
247
|
+
this.lastReportedSubroute = subroute;
|
|
248
|
+
this.sendCommand("REPORT_SUBROUTE", { subroute, replace });
|
|
249
|
+
}
|
|
250
|
+
sendCommand(type, payload) {
|
|
251
|
+
window.parent.postMessage(
|
|
252
|
+
{ __protocol: PROTOCOL_ID, __version: PROTOCOL_VERSION, kind: "command", type, payload },
|
|
253
|
+
this.parentOrigin
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
request(type, payload, onProgress) {
|
|
257
|
+
return new Promise((resolve, reject) => {
|
|
258
|
+
const requestId = crypto.randomUUID();
|
|
259
|
+
const timer = setTimeout(() => {
|
|
260
|
+
this.pendingRequests.delete(requestId);
|
|
261
|
+
reject(new Error(`Vibe bridge request timed out: ${type}`));
|
|
262
|
+
}, this.requestTimeout);
|
|
263
|
+
this.pendingRequests.set(requestId, {
|
|
264
|
+
resolve: (data) => {
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
resolve(data);
|
|
267
|
+
},
|
|
268
|
+
reject: (error) => {
|
|
269
|
+
clearTimeout(timer);
|
|
270
|
+
reject(error);
|
|
271
|
+
},
|
|
272
|
+
onProgress
|
|
273
|
+
});
|
|
274
|
+
const message = {
|
|
275
|
+
__protocol: PROTOCOL_ID,
|
|
276
|
+
__version: PROTOCOL_VERSION,
|
|
277
|
+
kind: "request",
|
|
278
|
+
type,
|
|
279
|
+
requestId,
|
|
280
|
+
payload
|
|
281
|
+
};
|
|
282
|
+
window.parent.postMessage(message, this.parentOrigin);
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
handleMessage(event) {
|
|
286
|
+
if (event.origin !== this.parentOrigin) return;
|
|
287
|
+
if (!isBridgeMessage(event.data)) return;
|
|
288
|
+
this.kindHandlers[event.data.kind]?.(event.data);
|
|
289
|
+
}
|
|
290
|
+
handleResponse(msg) {
|
|
291
|
+
if (msg.type === "GET_CONTEXT") {
|
|
292
|
+
if (msg.ok) this.onContextReceived?.(msg.data);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const pending = this.pendingRequests.get(msg.requestId);
|
|
296
|
+
if (!pending) return;
|
|
297
|
+
this.pendingRequests.delete(msg.requestId);
|
|
298
|
+
if (msg.ok) {
|
|
299
|
+
pending.resolve(msg.data);
|
|
300
|
+
} else {
|
|
301
|
+
pending.reject(new Error(msg.error));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
handleProgress(msg) {
|
|
305
|
+
const pending = this.pendingRequests.get(msg.requestId);
|
|
306
|
+
pending?.onProgress?.(msg.payload);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
export {
|
|
310
|
+
version as BRIDGE_VERSION,
|
|
311
|
+
VibeAppBridge
|
|
312
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nominalso/vibe-bridge",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Iframe-side SDK for building Nominal Vibe Apps — connects an embedded app to its Nominal host over a typed postMessage bridge (context, data fetch, file upload, subroute deep-linking).",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"nominal",
|
|
8
|
+
"vibe-apps",
|
|
9
|
+
"vibe-app",
|
|
10
|
+
"sdk",
|
|
11
|
+
"iframe",
|
|
12
|
+
"postmessage",
|
|
13
|
+
"bridge",
|
|
14
|
+
"embed"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/nominalso/vibe-apps-sdk#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/nominalso/vibe-apps-sdk/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "github:nominalso/vibe-apps-sdk",
|
|
23
|
+
"directory": "packages/bridge"
|
|
24
|
+
},
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"require": "./dist/index.cjs"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"main": "./dist/index.cjs",
|
|
34
|
+
"module": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"registry": "https://registry.npmjs.org/",
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@nominalso/vibe-api-types": "0.0.1",
|
|
45
|
+
"@nominalso/vibe-protocol-types": "0.0.1"
|
|
46
|
+
},
|
|
47
|
+
"tsup": {
|
|
48
|
+
"entry": [
|
|
49
|
+
"src/index.ts"
|
|
50
|
+
],
|
|
51
|
+
"format": [
|
|
52
|
+
"cjs",
|
|
53
|
+
"esm"
|
|
54
|
+
],
|
|
55
|
+
"noExternal": [
|
|
56
|
+
"@nominalso/vibe-protocol-types",
|
|
57
|
+
"@nominalso/vibe-api-types"
|
|
58
|
+
],
|
|
59
|
+
"dts": {
|
|
60
|
+
"resolve": [
|
|
61
|
+
"@nominalso/vibe-protocol-types",
|
|
62
|
+
"@nominalso/vibe-api-types"
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
"clean": true
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"clean": "rm -rf dist node_modules",
|
|
69
|
+
"build": "tsup",
|
|
70
|
+
"lint": "eslint src",
|
|
71
|
+
"typecheck": "tsc --noEmit",
|
|
72
|
+
"test": "vitest run"
|
|
73
|
+
}
|
|
74
|
+
}
|