@nominalso/vibe-bridge 0.4.0 → 0.6.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 +13 -4
- package/README.md +45 -7
- package/dist/chunk-AH2BCQVB.js +405 -0
- package/dist/chunk-AH2BCQVB.js.map +1 -0
- package/dist/index.browser.cjs +1211 -0
- package/dist/index.browser.cjs.map +1 -0
- package/dist/index.browser.js +1172 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.cjs +290 -89
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +138 -3784
- package/dist/index.d.ts +138 -3784
- package/dist/index.js +267 -445
- package/dist/index.js.map +1 -0
- package/dist/index.server.cjs +448 -0
- package/dist/index.server.cjs.map +1 -0
- package/dist/index.server.d.cts +51 -0
- package/dist/index.server.d.ts +51 -0
- package/dist/index.server.js +84 -0
- package/dist/index.server.js.map +1 -0
- package/dist/version-MpAmSu0t.d.cts +3819 -0
- package/dist/version-MpAmSu0t.d.ts +3819 -0
- package/docs/getting-started.md +3 -1
- package/llms.txt +3 -1
- package/package.json +27 -30
package/dist/index.js
CHANGED
|
@@ -1,368 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} while (decoded !== prev);
|
|
14
|
-
if (decoded.split("/").some((segment) => segment === "..")) return null;
|
|
15
|
-
return normalized;
|
|
16
|
-
}
|
|
17
|
-
var PROTOCOL_ID = "nominal-vibe-bridge";
|
|
18
|
-
var PROTOCOL_VERSION = 1;
|
|
19
|
-
var BRIDGE_KINDS = ["request", "response", "push", "command", "progress"];
|
|
20
|
-
var CORRELATED_KINDS = ["request", "response", "progress"];
|
|
21
|
-
var BRIDGE_KINDS_SET = new Set(BRIDGE_KINDS);
|
|
22
|
-
var CORRELATED_KINDS_SET = new Set(CORRELATED_KINDS);
|
|
23
|
-
function isBridgeMessage(data) {
|
|
24
|
-
if (typeof data !== "object" || data === null) return false;
|
|
25
|
-
const d = data;
|
|
26
|
-
if (d.__protocol !== PROTOCOL_ID) return false;
|
|
27
|
-
if (d.__version !== PROTOCOL_VERSION) return false;
|
|
28
|
-
if (typeof d.kind !== "string" || !BRIDGE_KINDS_SET.has(d.kind)) return false;
|
|
29
|
-
if (typeof d.type !== "string") return false;
|
|
30
|
-
if (CORRELATED_KINDS_SET.has(d.kind) && typeof d.requestId !== "string") return false;
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
var BridgeError = class extends Error {
|
|
34
|
-
constructor(code, message) {
|
|
35
|
-
super(message);
|
|
36
|
-
this.code = code;
|
|
37
|
-
this.name = "BridgeError";
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
var HttpBridgeError = class extends BridgeError {
|
|
41
|
-
constructor(status, message) {
|
|
42
|
-
super("REQUEST_FAILED", message);
|
|
43
|
-
this.status = status;
|
|
44
|
-
this.name = "HttpBridgeError";
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
function isOriginPattern(entry) {
|
|
48
|
-
return entry.includes("*");
|
|
49
|
-
}
|
|
50
|
-
var patternCache = /* @__PURE__ */ new Map();
|
|
51
|
-
function patternToRegExp(pattern) {
|
|
52
|
-
const cached = patternCache.get(pattern);
|
|
53
|
-
if (cached) return cached;
|
|
54
|
-
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
55
|
-
const body = escaped.replace(/\\\*/g, "[^./:@?#\\s]+");
|
|
56
|
-
const compiled = new RegExp(`^${body}$`);
|
|
57
|
-
patternCache.set(pattern, compiled);
|
|
58
|
-
return compiled;
|
|
59
|
-
}
|
|
60
|
-
function matchesOrigin(origin, entry) {
|
|
61
|
-
if (!isOriginPattern(entry)) return origin === entry;
|
|
62
|
-
return patternToRegExp(entry).test(origin);
|
|
63
|
-
}
|
|
64
|
-
function isOriginAllowed(origin, allowlist, { allowPatterns }) {
|
|
65
|
-
for (const entry of allowlist) {
|
|
66
|
-
if (isOriginPattern(entry)) {
|
|
67
|
-
if (allowPatterns && matchesOrigin(origin, entry)) return true;
|
|
68
|
-
} else if (origin === entry) {
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// src/VibeAppBridge.ts
|
|
76
|
-
import posthogDefault from "posthog-js";
|
|
77
|
-
|
|
78
|
-
// src/data-methods.ts
|
|
79
|
-
var BridgeDataMethods = class {
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
// Operations — Accounting: chart of accounts
|
|
82
|
-
//
|
|
83
|
-
// Each method is a typed wrapper over `request('<OP>', payload)`. The payload
|
|
84
|
-
// and return types come straight from the Nominal API (`RequestRegistry`).
|
|
85
|
-
// For any operation without a named method, call `request('<OP>', payload)`.
|
|
86
|
-
// ---------------------------------------------------------------------------
|
|
87
|
-
/**
|
|
88
|
-
* Fetch the flat chart of accounts for a subsidiary.
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* ```ts
|
|
92
|
-
* const ctx = await bridge.connect()
|
|
93
|
-
* const accounts = await bridge.getChartOfAccounts({
|
|
94
|
-
* path: { subsidiary_id: ctx.subsidiaryId },
|
|
95
|
-
* })
|
|
96
|
-
* ```
|
|
97
|
-
*/
|
|
98
|
-
getChartOfAccounts(payload) {
|
|
99
|
-
return this.request("GET_CHART_OF_ACCOUNTS", payload);
|
|
100
|
-
}
|
|
101
|
-
/** Fetch the chart of accounts as a hierarchical tree for a subsidiary. */
|
|
102
|
-
getCoaTree(payload) {
|
|
103
|
-
return this.request("GET_COA_TREE", payload);
|
|
104
|
-
}
|
|
105
|
-
/** Fetch a simplified flat chart of accounts for a subsidiary. */
|
|
106
|
-
getCoaFlatSimple(payload) {
|
|
107
|
-
return this.request("GET_COA_FLAT_SIMPLE", payload);
|
|
108
|
-
}
|
|
109
|
-
/** Fetch the flat chart of accounts grouped by account group. */
|
|
110
|
-
getCoaGrouped(payload) {
|
|
111
|
-
return this.request("GET_COA_GROUPED", payload);
|
|
112
|
-
}
|
|
113
|
-
/** Fetch a single chart-of-accounts account by id within a subsidiary. */
|
|
114
|
-
getCoaAccount(payload) {
|
|
115
|
-
return this.request("GET_COA_ACCOUNT", payload);
|
|
116
|
-
}
|
|
117
|
-
/** List the currencies available to a subsidiary's chart of accounts. */
|
|
118
|
-
getSubsidiaryAvailableCurrencies(payload) {
|
|
119
|
-
return this.request("GET_SUBSIDIARY_AVAILABLE_CURRENCIES", payload);
|
|
120
|
-
}
|
|
121
|
-
/** Fetch accounts at the tenant level. */
|
|
122
|
-
getAccounts(payload) {
|
|
123
|
-
return this.request("GET_ACCOUNTS", payload);
|
|
124
|
-
}
|
|
125
|
-
/** Fetch a single account by id. */
|
|
126
|
-
getAccount(payload) {
|
|
127
|
-
return this.request("GET_ACCOUNT", payload);
|
|
128
|
-
}
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
// Operations — Accounting: exchange rates
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
/** Fetch currency conversion rates for a currency. */
|
|
133
|
-
getConversionRates(payload) {
|
|
134
|
-
return this.request("GET_CONVERSION_RATES", payload);
|
|
135
|
-
}
|
|
136
|
-
/** Fetch the effective consolidation exchange rate. */
|
|
137
|
-
getEffectiveExchangeRate(payload) {
|
|
138
|
-
return this.request("GET_EFFECTIVE_EXCHANGE_RATE", payload);
|
|
139
|
-
}
|
|
140
|
-
/** Fetch the exchange-rate period covering a given input date. */
|
|
141
|
-
getExchangeRateByDate(payload) {
|
|
142
|
-
return this.request("GET_EXCHANGE_RATE_BY_DATE", payload);
|
|
143
|
-
}
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
// Operations — Accounting: dimensions
|
|
146
|
-
// ---------------------------------------------------------------------------
|
|
147
|
-
/** List accounting dimensions. */
|
|
148
|
-
getDimensions(payload) {
|
|
149
|
-
return this.request("GET_DIMENSIONS", payload);
|
|
150
|
-
}
|
|
151
|
-
/** List values for accounting dimensions. */
|
|
152
|
-
getDimensionValues(payload) {
|
|
153
|
-
return this.request("GET_DIMENSION_VALUES", payload);
|
|
154
|
-
}
|
|
155
|
-
/** List dimension values in hierarchical form. */
|
|
156
|
-
getDimensionValuesHierarchical(payload) {
|
|
157
|
-
return this.request("GET_DIMENSION_VALUES_HIERARCHICAL", payload);
|
|
158
|
-
}
|
|
159
|
-
/** List account assignments for a dimension/account pair. */
|
|
160
|
-
getDimensionAccountAssignments(payload) {
|
|
161
|
-
return this.request("GET_DIMENSION_ACCOUNT_ASSIGNMENTS", payload);
|
|
162
|
-
}
|
|
163
|
-
// ---------------------------------------------------------------------------
|
|
164
|
-
// Operations — Accounting: journal entries
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
/**
|
|
167
|
-
* Fetch journal entries for a subsidiary, optionally filtered by date range.
|
|
168
|
-
*
|
|
169
|
-
* @example
|
|
170
|
-
* ```ts
|
|
171
|
-
* const entries = await bridge.getJournalEntries({
|
|
172
|
-
* path: { subsidiary_id: ctx.subsidiaryId },
|
|
173
|
-
* query: { from_date: '2026-01-01', to_date: '2026-03-31' },
|
|
174
|
-
* })
|
|
175
|
-
* ```
|
|
176
|
-
*/
|
|
177
|
-
getJournalEntries(payload) {
|
|
178
|
-
return this.request("GET_JOURNAL_ENTRIES", payload);
|
|
179
|
-
}
|
|
180
|
-
/** Fetch journal entry lines for a subsidiary. */
|
|
181
|
-
getJournalLines(payload) {
|
|
182
|
-
return this.request("GET_JOURNAL_LINES", payload);
|
|
183
|
-
}
|
|
184
|
-
/** Fetch a single journal entry by id within a subsidiary. */
|
|
185
|
-
getJournalEntry(payload) {
|
|
186
|
-
return this.request("GET_JOURNAL_ENTRY", payload);
|
|
187
|
-
}
|
|
188
|
-
// ---------------------------------------------------------------------------
|
|
189
|
-
// Operations — Activity: period instances
|
|
190
|
-
// ---------------------------------------------------------------------------
|
|
191
|
-
/** List accounting period instances. */
|
|
192
|
-
getPeriods(payload) {
|
|
193
|
-
return this.request("GET_PERIODS", payload);
|
|
194
|
-
}
|
|
195
|
-
/** Fetch a single period instance by id. */
|
|
196
|
-
getPeriodInstance(payload) {
|
|
197
|
-
return this.request("GET_PERIOD_INSTANCE", payload);
|
|
198
|
-
}
|
|
199
|
-
/** Fetch a period instance by its display slug (e.g. `"mar-2026"`). */
|
|
200
|
-
getPeriodInstanceBySlug(payload) {
|
|
201
|
-
return this.request("GET_PERIOD_INSTANCE_BY_SLUG", payload);
|
|
202
|
-
}
|
|
203
|
-
/** Fetch the close-progress breakdown for a period (by slug). */
|
|
204
|
-
getPeriodProgressBreakdown(payload) {
|
|
205
|
-
return this.request("GET_PERIOD_PROGRESS_BREAKDOWN", payload);
|
|
206
|
-
}
|
|
207
|
-
// ---------------------------------------------------------------------------
|
|
208
|
-
// Operations — Activity: activity definitions
|
|
209
|
-
// ---------------------------------------------------------------------------
|
|
210
|
-
/** List activity definitions. */
|
|
211
|
-
getActivityDefinitions(payload) {
|
|
212
|
-
return this.request("GET_ACTIVITY_DEFINITIONS", payload);
|
|
213
|
-
}
|
|
214
|
-
/** Fetch a single activity definition by id. */
|
|
215
|
-
getActivityDefinition(payload) {
|
|
216
|
-
return this.request("GET_ACTIVITY_DEFINITION", payload);
|
|
217
|
-
}
|
|
218
|
-
// ---------------------------------------------------------------------------
|
|
219
|
-
// Operations — Activity: activity instances
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
/** List activity instances. */
|
|
222
|
-
getActivityInstances(payload) {
|
|
223
|
-
return this.request("GET_ACTIVITY_INSTANCES", payload);
|
|
224
|
-
}
|
|
225
|
-
/** Fetch a single activity instance by id. */
|
|
226
|
-
getActivityInstance(payload) {
|
|
227
|
-
return this.request("GET_ACTIVITY_INSTANCE", payload);
|
|
228
|
-
}
|
|
229
|
-
/** Fetch an activity instance for a given period + activity definition. */
|
|
230
|
-
getActivityInstanceByPeriod(payload) {
|
|
231
|
-
return this.request("GET_ACTIVITY_INSTANCE_BY_PERIOD", payload);
|
|
232
|
-
}
|
|
233
|
-
/** List task instances for an activity instance. */
|
|
234
|
-
getActivityInstanceTasks(payload) {
|
|
235
|
-
return this.request("GET_ACTIVITY_INSTANCE_TASKS", payload);
|
|
236
|
-
}
|
|
237
|
-
/** List task instances for an activity in a given period. */
|
|
238
|
-
getActivityPeriodTasks(payload) {
|
|
239
|
-
return this.request("GET_ACTIVITY_PERIOD_TASKS", payload);
|
|
240
|
-
}
|
|
241
|
-
// ---------------------------------------------------------------------------
|
|
242
|
-
// Operations — Activity: task definitions
|
|
243
|
-
// ---------------------------------------------------------------------------
|
|
244
|
-
/** List task definitions. */
|
|
245
|
-
getTaskDefinitions(payload) {
|
|
246
|
-
return this.request("GET_TASK_DEFINITIONS", payload);
|
|
247
|
-
}
|
|
248
|
-
/** Fetch a single task definition by id. */
|
|
249
|
-
getTaskDefinition(payload) {
|
|
250
|
-
return this.request("GET_TASK_DEFINITION", payload);
|
|
251
|
-
}
|
|
252
|
-
/** Create a task definition. */
|
|
253
|
-
createTaskDefinition(payload) {
|
|
254
|
-
return this.request("CREATE_TASK_DEFINITION", payload);
|
|
255
|
-
}
|
|
256
|
-
/** Update a task definition by id. */
|
|
257
|
-
updateTaskDefinition(payload) {
|
|
258
|
-
return this.request("UPDATE_TASK_DEFINITION", payload);
|
|
259
|
-
}
|
|
260
|
-
/** Query task definitions by filter. */
|
|
261
|
-
getTaskDefinitionsByFilter(payload) {
|
|
262
|
-
return this.request("GET_TASK_DEFINITIONS_BY_FILTER", payload);
|
|
263
|
-
}
|
|
264
|
-
// ---------------------------------------------------------------------------
|
|
265
|
-
// Operations — Activity: task instances
|
|
266
|
-
// ---------------------------------------------------------------------------
|
|
267
|
-
/** List task instances. */
|
|
268
|
-
getTaskInstances(payload) {
|
|
269
|
-
return this.request("GET_TASK_INSTANCES", payload);
|
|
270
|
-
}
|
|
271
|
-
/** Fetch a single task instance by id. */
|
|
272
|
-
getTaskInstance(payload) {
|
|
273
|
-
return this.request("GET_TASK_INSTANCE", payload);
|
|
274
|
-
}
|
|
275
|
-
/**
|
|
276
|
-
* Submit a Close-Management task output. This is the **only write path** back
|
|
277
|
-
* into Nominal — Vibe Apps never write Nominal data directly.
|
|
278
|
-
*
|
|
279
|
-
* @example
|
|
280
|
-
* ```ts
|
|
281
|
-
* // `body` carries the task-specific output shape.
|
|
282
|
-
* await bridge.postTaskOutput({
|
|
283
|
-
* path: { task_instance_id: 'task-1' },
|
|
284
|
-
* body: {},
|
|
285
|
-
* })
|
|
286
|
-
* ```
|
|
287
|
-
*/
|
|
288
|
-
postTaskOutput(payload) {
|
|
289
|
-
return this.request("POST_TASK_OUTPUT", payload);
|
|
290
|
-
}
|
|
291
|
-
/** Query task instances by filter. */
|
|
292
|
-
getTaskInstancesByFilter(payload) {
|
|
293
|
-
return this.request("GET_TASK_INSTANCES_BY_FILTER", payload);
|
|
294
|
-
}
|
|
295
|
-
// ---------------------------------------------------------------------------
|
|
296
|
-
// Operations — Audit trail
|
|
297
|
-
// ---------------------------------------------------------------------------
|
|
298
|
-
/** List audit-trail events. */
|
|
299
|
-
getAuditEvents(payload) {
|
|
300
|
-
return this.request("GET_AUDIT_EVENTS", payload);
|
|
301
|
-
}
|
|
302
|
-
/** List audit-trail events for a specific entity. */
|
|
303
|
-
getEntityAuditEvents(payload) {
|
|
304
|
-
return this.request("GET_ENTITY_AUDIT_EVENTS", payload);
|
|
305
|
-
}
|
|
306
|
-
// ---------------------------------------------------------------------------
|
|
307
|
-
// Operations — Period manager: fiscal calendars
|
|
308
|
-
// ---------------------------------------------------------------------------
|
|
309
|
-
/** List fiscal calendars for a subsidiary. */
|
|
310
|
-
getFiscalCalendars(payload) {
|
|
311
|
-
return this.request("GET_FISCAL_CALENDARS", payload);
|
|
312
|
-
}
|
|
313
|
-
/** Fetch a single fiscal calendar by id. */
|
|
314
|
-
getFiscalCalendar(payload) {
|
|
315
|
-
return this.request("GET_FISCAL_CALENDAR", payload);
|
|
316
|
-
}
|
|
317
|
-
// ---------------------------------------------------------------------------
|
|
318
|
-
// Operations — Tenancy
|
|
319
|
-
// ---------------------------------------------------------------------------
|
|
320
|
-
/** List subsidiaries for the tenant. */
|
|
321
|
-
getSubsidiaries(payload) {
|
|
322
|
-
return this.request("GET_SUBSIDIARIES", payload);
|
|
323
|
-
}
|
|
324
|
-
/** Fetch a single subsidiary by id. */
|
|
325
|
-
getSubsidiary(payload) {
|
|
326
|
-
return this.request("GET_SUBSIDIARY", payload);
|
|
327
|
-
}
|
|
328
|
-
/** List parent currencies for a subsidiary. */
|
|
329
|
-
getSubsidiaryParentCurrencies(payload) {
|
|
330
|
-
return this.request("GET_SUBSIDIARY_PARENT_CURRENCIES", payload);
|
|
331
|
-
}
|
|
332
|
-
/** List users for the tenant. */
|
|
333
|
-
getTenantUsers(payload) {
|
|
334
|
-
return this.request("GET_TENANT_USERS", payload);
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* Uploads a file through the host. Converts the `File` to an `ArrayBuffer`
|
|
338
|
-
* before sending — sandboxed iframes cannot pass `File` handles across
|
|
339
|
-
* windows. Resolves once Nominal has stored the file.
|
|
340
|
-
*
|
|
341
|
-
* @example
|
|
342
|
-
* ```ts
|
|
343
|
-
* const input = document.querySelector<HTMLInputElement>('#file')!
|
|
344
|
-
* const file = input.files![0]
|
|
345
|
-
* const result = await bridge.upload(file, {
|
|
346
|
-
* entityType: 'JOURNAL_ENTRY',
|
|
347
|
-
* entityId: '123',
|
|
348
|
-
* onProgress: (p) => console.log(`${p.progress}%`),
|
|
349
|
-
* })
|
|
350
|
-
* // result.attachmentId, result.name
|
|
351
|
-
* ```
|
|
352
|
-
*/
|
|
353
|
-
async upload(file, options) {
|
|
354
|
-
const buffer = await file.arrayBuffer();
|
|
355
|
-
const payload = {
|
|
356
|
-
buffer,
|
|
357
|
-
fileName: file.name,
|
|
358
|
-
fileType: file.type,
|
|
359
|
-
entityType: options.entityType,
|
|
360
|
-
entityId: options.entityId
|
|
361
|
-
};
|
|
362
|
-
const onProgress = options.onProgress ? (p) => options.onProgress(p) : void 0;
|
|
363
|
-
return this.request("UPLOAD_FILE", payload, onProgress);
|
|
364
|
-
}
|
|
365
|
-
};
|
|
1
|
+
import {
|
|
2
|
+
BRIDGE_VERSION,
|
|
3
|
+
BridgeDataMethods,
|
|
4
|
+
BridgeError,
|
|
5
|
+
HttpBridgeError,
|
|
6
|
+
PROTOCOL_ID,
|
|
7
|
+
PROTOCOL_VERSION,
|
|
8
|
+
isBridgeMessage,
|
|
9
|
+
isOriginAllowed,
|
|
10
|
+
isOriginPattern,
|
|
11
|
+
normalizeSubroute
|
|
12
|
+
} from "./chunk-AH2BCQVB.js";
|
|
366
13
|
|
|
367
14
|
// src/timeouts.ts
|
|
368
15
|
var CONNECT_TIMEOUT_MS = 1e4;
|
|
@@ -370,8 +17,6 @@ var CONNECT_POLL_MS = 500;
|
|
|
370
17
|
var DEFAULT_TIMEOUTS = {
|
|
371
18
|
/** Typed Nominal API reads/writes (`GET_*` / `POST_*` / `CREATE_*` / ...). */
|
|
372
19
|
api: 3e4,
|
|
373
|
-
/** `GET_CONTEXT` re-fetch (the initial fetch is governed by `connect()`). */
|
|
374
|
-
context: 5e3,
|
|
375
20
|
/** `INVALIDATE_CACHE`. */
|
|
376
21
|
invalidate: 1e4,
|
|
377
22
|
/** `UPLOAD_FILE` — large payloads + presigned S3 PUT take a while. */
|
|
@@ -384,8 +29,6 @@ function resolveRequestTimeout(type, requestTimeout) {
|
|
|
384
29
|
return DEFAULT_TIMEOUTS.upload;
|
|
385
30
|
case "INVALIDATE_CACHE":
|
|
386
31
|
return DEFAULT_TIMEOUTS.invalidate;
|
|
387
|
-
case "GET_CONTEXT":
|
|
388
|
-
return DEFAULT_TIMEOUTS.context;
|
|
389
32
|
default:
|
|
390
33
|
return DEFAULT_TIMEOUTS.api;
|
|
391
34
|
}
|
|
@@ -393,13 +36,23 @@ function resolveRequestTimeout(type, requestTimeout) {
|
|
|
393
36
|
|
|
394
37
|
// src/VibeAppBridge.ts
|
|
395
38
|
function resolvePosthog(mod) {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
return
|
|
39
|
+
const hasInit = (o) => typeof o?.init === "function";
|
|
40
|
+
const d1 = mod?.default;
|
|
41
|
+
if (hasInit(d1)) return d1;
|
|
42
|
+
const d2 = d1?.default;
|
|
43
|
+
if (hasInit(d2)) return d2;
|
|
44
|
+
if (hasInit(mod)) return mod;
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function serverEnvError(method) {
|
|
48
|
+
return new BridgeError(
|
|
49
|
+
"SERVER_ENVIRONMENT",
|
|
50
|
+
`VibeAppBridge.${method} was called outside a browser (no \`window\`). The bridge only runs in the browser \u2014 call it from a client component or effect, not during SSR, in a React Server Component, or in a Worker.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
function assertBrowser(method) {
|
|
54
|
+
if (typeof window === "undefined") throw serverEnvError(method);
|
|
401
55
|
}
|
|
402
|
-
var posthog = resolvePosthog(posthogDefault);
|
|
403
56
|
var PARENT_ORIGIN_OVERRIDE_KEY = "__NOM_VIBE_PARENT_ORIGIN__";
|
|
404
57
|
var PREVIEW_HOST_RE = /(?:^|\.)(?:vercel\.app|lovable\.app|lovable\.dev|lovableproject\.com)$|^(?:localhost|127\.0\.0\.1)$/;
|
|
405
58
|
function isPreviewSelfHost(hostname) {
|
|
@@ -459,9 +112,22 @@ function resolveOriginPolicy(configured) {
|
|
|
459
112
|
return { targetOrigin, isTrusted };
|
|
460
113
|
}
|
|
461
114
|
var VibeAppBridge = class extends BridgeDataMethods {
|
|
462
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Stores options only — **pure and side-effect-free**. No `window`,
|
|
117
|
+
* `document`, `history`, `postMessage`, `crypto`, or `posthog-js` access
|
|
118
|
+
* happens here, so constructing a bridge is safe during SSR / in a Server
|
|
119
|
+
* Component / in a Worker. Browser wiring is deferred to {@link connect} /
|
|
120
|
+
* {@link attach}.
|
|
121
|
+
*/
|
|
122
|
+
constructor(options = {}) {
|
|
463
123
|
super();
|
|
124
|
+
/** Concrete origin to `postMessage` to, or `null` until resolved at {@link attach}. */
|
|
125
|
+
this.targetOrigin = null;
|
|
126
|
+
/** Validates an inbound `event.origin`; rejects everything until {@link attach}. */
|
|
127
|
+
this.isTrustedOrigin = () => false;
|
|
464
128
|
this.pendingRequests = /* @__PURE__ */ new Map();
|
|
129
|
+
/** Whether {@link attach} has installed the message listener + resolved origin. */
|
|
130
|
+
this.attached = false;
|
|
465
131
|
this.context = null;
|
|
466
132
|
/**
|
|
467
133
|
* Set once {@link initPostHog} has successfully called `posthog.init()`.
|
|
@@ -469,17 +135,34 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
469
135
|
* recording is actually live.
|
|
470
136
|
*/
|
|
471
137
|
this.posthogInitialized = false;
|
|
138
|
+
/** The lazily-imported `posthog-js` singleton, once recording is initialized. */
|
|
139
|
+
this.posthogInstance = null;
|
|
140
|
+
/**
|
|
141
|
+
* Set for the duration of an in-flight {@link initPostHog} (which is async — it
|
|
142
|
+
* awaits `import('posthog-js')`). Serializes initialization so two overlapping
|
|
143
|
+
* `POSTHOG_PUSH`es can't both call `posthog.init()`.
|
|
144
|
+
*/
|
|
145
|
+
this.posthogIniting = false;
|
|
146
|
+
/** So the "posthog-js not installed" warning is logged at most once. */
|
|
147
|
+
this.posthogUnavailableWarned = false;
|
|
472
148
|
this.connectPromise = null;
|
|
473
149
|
this.connectPollInterval = null;
|
|
474
150
|
this.connectTimeout = null;
|
|
475
151
|
this.onContextReceived = null;
|
|
476
|
-
|
|
152
|
+
/** Rejects an in-flight connect() (used by destroy()); `null` once settled. */
|
|
153
|
+
this.rejectConnect = null;
|
|
154
|
+
/**
|
|
155
|
+
* Persistent subscriber for post-connect context changes (tenant/subsidiary
|
|
156
|
+
* switch, period close, …). Distinct from {@link onContextReceived}, which only
|
|
157
|
+
* resolves the connect handshake and is cleared afterward. `null` until
|
|
158
|
+
* {@link onContextChange} is called.
|
|
159
|
+
*/
|
|
160
|
+
this.contextChangeCallback = null;
|
|
477
161
|
/**
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
* resolve/reject the new one (a stale error could reject a healthy reconnect).
|
|
162
|
+
* Persistent subscriber for host auth changes (Nominal logout or
|
|
163
|
+
* identity/tenant switch). `null` until {@link onAuthChange} is called.
|
|
481
164
|
*/
|
|
482
|
-
this.
|
|
165
|
+
this.authChangeCallback = null;
|
|
483
166
|
this.lastReportedSubroute = null;
|
|
484
167
|
this.suppressSubrouteReport = false;
|
|
485
168
|
this.subrouteCallback = null;
|
|
@@ -492,7 +175,25 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
492
175
|
progress: (msg) => this.handleProgress(msg)
|
|
493
176
|
};
|
|
494
177
|
this.pushHandlers = {
|
|
495
|
-
CONTEXT_PUSH: (payload) =>
|
|
178
|
+
CONTEXT_PUSH: (payload) => {
|
|
179
|
+
if (this.onContextReceived) {
|
|
180
|
+
this.onContextReceived(payload);
|
|
181
|
+
} else if (this.context) {
|
|
182
|
+
this.context = payload;
|
|
183
|
+
this.contextChangeCallback?.(payload);
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
// Persistent (not nulled after connect): the host delivers PostHog once at
|
|
187
|
+
// connect via its own push, and initPostHog is idempotent, so this can run
|
|
188
|
+
// whenever the message arrives, independent of how connect() resolved. Fire
|
|
189
|
+
// and forget — initPostHog is async (lazy-imports posthog-js) and swallows
|
|
190
|
+
// its own errors.
|
|
191
|
+
POSTHOG_PUSH: (payload) => {
|
|
192
|
+
void this.initPostHog(payload);
|
|
193
|
+
},
|
|
194
|
+
// Persistent: the host pushes auth changes (logout, identity/tenant
|
|
195
|
+
// switch) at any time after connect.
|
|
196
|
+
AUTH_CHANGED: (payload) => this.authChangeCallback?.(payload),
|
|
496
197
|
SUBROUTE_PUSH: (payload) => {
|
|
497
198
|
const safe = normalizeSubroute(payload.subroute);
|
|
498
199
|
if (!safe) return;
|
|
@@ -519,42 +220,72 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
519
220
|
this.ui = {
|
|
520
221
|
/** Collapse or expand the host's side navigation. */
|
|
521
222
|
setSideNav: (payload) => {
|
|
223
|
+
assertBrowser("ui.setSideNav()");
|
|
522
224
|
this.sendCommand("SET_SIDE_NAV", payload);
|
|
523
225
|
},
|
|
524
226
|
/** Switch the host to a different subsidiary. */
|
|
525
227
|
switchSubsidiary: (subsidiaryId) => {
|
|
228
|
+
assertBrowser("ui.switchSubsidiary()");
|
|
526
229
|
this.sendCommand("SWITCH_SUBSIDIARY", { subsidiaryId });
|
|
527
230
|
}
|
|
528
231
|
};
|
|
529
|
-
|
|
530
|
-
this.
|
|
531
|
-
this.isTrustedOrigin = policy.isTrusted;
|
|
532
|
-
this.requestTimeout = requestTimeout;
|
|
232
|
+
this.parentOriginOption = options.parentOrigin;
|
|
233
|
+
this.requestTimeout = options.requestTimeout;
|
|
533
234
|
this.boundHandleMessage = this.handleMessage.bind(this);
|
|
534
|
-
window.addEventListener("message", this.boundHandleMessage);
|
|
535
235
|
}
|
|
536
236
|
dispatchPush(msg) {
|
|
537
237
|
const handler = this.pushHandlers[msg.type];
|
|
538
238
|
handler(msg.payload);
|
|
539
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Installs the browser wiring: resolves the parent-origin policy and starts
|
|
242
|
+
* listening for host `postMessage`s. **Idempotent** and safe to call before
|
|
243
|
+
* {@link connect} if you want the bridge receiving pushes early; {@link
|
|
244
|
+
* connect} calls it for you, so most apps never call this directly.
|
|
245
|
+
*
|
|
246
|
+
* @throws `BridgeError` code `'SERVER_ENVIRONMENT'` if called where there is no
|
|
247
|
+
* `window`.
|
|
248
|
+
*/
|
|
249
|
+
attach() {
|
|
250
|
+
assertBrowser("attach()");
|
|
251
|
+
if (this.attached) return;
|
|
252
|
+
const policy = resolveOriginPolicy(this.parentOriginOption);
|
|
253
|
+
this.targetOrigin = policy.targetOrigin;
|
|
254
|
+
this.isTrustedOrigin = policy.isTrusted;
|
|
255
|
+
window.addEventListener("message", this.boundHandleMessage);
|
|
256
|
+
this.attached = true;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Lazily installs the browser wiring on first transport use. Keeps the
|
|
260
|
+
* constructor pure while ensuring that any method reaching the wire (a data
|
|
261
|
+
* `request`, a fire-and-forget command) works without an explicit prior
|
|
262
|
+
* `attach()`/`connect()` — attach stays idempotent, so `connect()` is
|
|
263
|
+
* unaffected.
|
|
264
|
+
*/
|
|
265
|
+
ensureAttached() {
|
|
266
|
+
if (!this.attached) this.attach();
|
|
267
|
+
}
|
|
540
268
|
/**
|
|
541
269
|
* Connects to the host and returns the tenant/user {@link ContextPayload}.
|
|
542
270
|
* **Call this once on app init and await it before any other method** — data
|
|
543
271
|
* methods, `upload()`, and subroute reporting all require an established
|
|
544
272
|
* connection. Concurrent calls return the same promise.
|
|
545
273
|
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
* after
|
|
550
|
-
*
|
|
274
|
+
* First {@link attach | attaches} the browser wiring (message listener +
|
|
275
|
+
* origin resolution), then sends a `CONNECT` handshake, re-sending every 500ms
|
|
276
|
+
* until the host replies by pushing the context (so it works even if the host
|
|
277
|
+
* mounts after the iframe loads). Context is only ever delivered by push.
|
|
278
|
+
* Rejects with `Bridge connect timed out` after 10 seconds if no context
|
|
279
|
+
* arrives (usually a `parentOrigin` mismatch or the host hasn't mounted).
|
|
551
280
|
*
|
|
552
281
|
* After connecting, if the context includes an initial subroute (e.g. from
|
|
553
282
|
* a deep link), the bridge navigates to it and starts auto-tracking
|
|
554
283
|
* internal navigation via the history API.
|
|
555
284
|
*
|
|
556
285
|
* @returns The tenant/user/subsidiary context for this app session.
|
|
557
|
-
* @throws
|
|
286
|
+
* @throws Rejects with `BridgeError` code `'SERVER_ENVIRONMENT'` if called
|
|
287
|
+
* where there is no `window`, `'PARENT_ORIGIN_UNRESOLVED'` if no embedding
|
|
288
|
+
* origin could be resolved, or `'TIMEOUT'` if no context arrives in 10s.
|
|
558
289
|
* @example
|
|
559
290
|
* ```ts
|
|
560
291
|
* const ctx = await bridge.connect()
|
|
@@ -562,8 +293,10 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
562
293
|
* ```
|
|
563
294
|
*/
|
|
564
295
|
connect() {
|
|
296
|
+
if (typeof window === "undefined") return Promise.reject(serverEnvError("connect()"));
|
|
565
297
|
if (this.context) return Promise.resolve(this.context);
|
|
566
298
|
if (this.connectPromise) return this.connectPromise;
|
|
299
|
+
this.attach();
|
|
567
300
|
if (this.targetOrigin === null) {
|
|
568
301
|
return Promise.reject(
|
|
569
302
|
new BridgeError(
|
|
@@ -577,16 +310,16 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
577
310
|
this.stopConnectPolling();
|
|
578
311
|
this.connectPromise = null;
|
|
579
312
|
this.onContextReceived = null;
|
|
580
|
-
this.
|
|
313
|
+
this.rejectConnect = null;
|
|
581
314
|
reject(new BridgeError("TIMEOUT", "Bridge connect timed out"));
|
|
582
315
|
}, CONNECT_TIMEOUT_MS);
|
|
583
316
|
this.connectTimeout = timer;
|
|
584
|
-
this.
|
|
317
|
+
this.rejectConnect = (error) => {
|
|
585
318
|
clearTimeout(timer);
|
|
586
319
|
this.stopConnectPolling();
|
|
587
320
|
this.connectPromise = null;
|
|
588
321
|
this.onContextReceived = null;
|
|
589
|
-
this.
|
|
322
|
+
this.rejectConnect = null;
|
|
590
323
|
reject(error);
|
|
591
324
|
};
|
|
592
325
|
this.onContextReceived = (ctx) => {
|
|
@@ -595,11 +328,10 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
595
328
|
this.context = ctx;
|
|
596
329
|
this.connectPromise = null;
|
|
597
330
|
this.onContextReceived = null;
|
|
598
|
-
this.
|
|
331
|
+
this.rejectConnect = null;
|
|
599
332
|
console.log(
|
|
600
|
-
`[vibe-bridge] Connected \u2014 bridge: ${
|
|
333
|
+
`[vibe-bridge] Connected \u2014 bridge: ${BRIDGE_VERSION}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
601
334
|
);
|
|
602
|
-
this.initPostHog(ctx);
|
|
603
335
|
const initialSubroute = ctx.subroute ? normalizeSubroute(ctx.subroute) : null;
|
|
604
336
|
if (initialSubroute && initialSubroute !== "/") {
|
|
605
337
|
this.withSubrouteSuppressed(() => {
|
|
@@ -611,23 +343,14 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
611
343
|
this.lastReportedSubroute = window.location.pathname + window.location.search;
|
|
612
344
|
}
|
|
613
345
|
this.setupNavigationTracking();
|
|
346
|
+
this.warnMissingListeners();
|
|
614
347
|
resolve(ctx);
|
|
615
348
|
};
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
this.connectPollIds.add(requestId);
|
|
619
|
-
const message = {
|
|
620
|
-
__protocol: PROTOCOL_ID,
|
|
621
|
-
__version: PROTOCOL_VERSION,
|
|
622
|
-
kind: "request",
|
|
623
|
-
type: "GET_CONTEXT",
|
|
624
|
-
requestId,
|
|
625
|
-
payload: { bridgeVersion: version }
|
|
626
|
-
};
|
|
627
|
-
this.postToParent(message);
|
|
349
|
+
const sendConnect = () => {
|
|
350
|
+
this.sendCommand("CONNECT", { bridgeVersion: BRIDGE_VERSION });
|
|
628
351
|
};
|
|
629
|
-
|
|
630
|
-
this.connectPollInterval = setInterval(
|
|
352
|
+
sendConnect();
|
|
353
|
+
this.connectPollInterval = setInterval(sendConnect, CONNECT_POLL_MS);
|
|
631
354
|
});
|
|
632
355
|
return this.connectPromise;
|
|
633
356
|
}
|
|
@@ -637,6 +360,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
637
360
|
* Use this for hash-based routers or non-standard navigation patterns.
|
|
638
361
|
*/
|
|
639
362
|
reportSubroute(subroute, options) {
|
|
363
|
+
assertBrowser("reportSubroute()");
|
|
640
364
|
const normalized = normalizeSubroute(subroute);
|
|
641
365
|
if (normalized === null) return;
|
|
642
366
|
this.lastReportedSubroute = normalized;
|
|
@@ -647,6 +371,8 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
647
371
|
* If no callback is registered, the SDK uses `history.pushState` + a
|
|
648
372
|
* `popstate` event, which works for most SPA routers automatically.
|
|
649
373
|
* Returns an unsubscribe function.
|
|
374
|
+
*
|
|
375
|
+
* Pure — only stores the callback, so it is safe to call before {@link connect}.
|
|
650
376
|
*/
|
|
651
377
|
onSubrouteRequest(callback) {
|
|
652
378
|
this.subrouteCallback = callback;
|
|
@@ -654,6 +380,58 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
654
380
|
this.subrouteCallback = null;
|
|
655
381
|
};
|
|
656
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* Subscribes to post-connect context changes (tenant/subsidiary switch, period
|
|
385
|
+
* close, …). The callback fires for each {@link ContextPayload} the host pushes
|
|
386
|
+
* *after* connect — not for the initial context, which {@link connect} already
|
|
387
|
+
* returns. Returns an unsubscribe function. Single subscriber: a second call
|
|
388
|
+
* replaces the first. Wire it **before {@link connect}** (the bridge warns at
|
|
389
|
+
* connect if it isn't).
|
|
390
|
+
*
|
|
391
|
+
* Pure — only stores the callback, so it is safe to call before {@link connect}.
|
|
392
|
+
*/
|
|
393
|
+
onContextChange(callback) {
|
|
394
|
+
this.contextChangeCallback = callback;
|
|
395
|
+
return () => {
|
|
396
|
+
this.contextChangeCallback = null;
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Subscribes to host auth changes: a Nominal logout (`authenticated: false` —
|
|
401
|
+
* sign your backend out) or an identity/tenant switch (`authenticated: true`
|
|
402
|
+
* with a new `userId`/`tenant` — re-authenticate). Fires for each change the
|
|
403
|
+
* host pushes after connect. Returns an unsubscribe function. Single
|
|
404
|
+
* subscriber: a second call replaces the first.
|
|
405
|
+
*
|
|
406
|
+
* **Wire this before {@link connect}** — without it the embedded app won't
|
|
407
|
+
* sign out when the user logs out of Nominal (the bridge warns at connect if
|
|
408
|
+
* it isn't wired). Pure — only stores the callback.
|
|
409
|
+
*/
|
|
410
|
+
onAuthChange(callback) {
|
|
411
|
+
this.authChangeCallback = callback;
|
|
412
|
+
return () => {
|
|
413
|
+
this.authChangeCallback = null;
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* At connect, nudge the app if it hasn't wired the change listeners: every
|
|
418
|
+
* embedded app should react to a Nominal logout ({@link onAuthChange}) and a
|
|
419
|
+
* tenant/subsidiary switch ({@link onContextChange}). Fires once during the
|
|
420
|
+
* connect flow (not speculatively later), so wire both **before** `connect()`.
|
|
421
|
+
* The skill/codegen does this by default; this catches hand-wired apps.
|
|
422
|
+
*/
|
|
423
|
+
warnMissingListeners() {
|
|
424
|
+
if (!this.authChangeCallback) {
|
|
425
|
+
console.warn(
|
|
426
|
+
"[vibe-bridge] connected without onAuthChange \u2014 the app will not sign out on Nominal logout or react to an identity/tenant switch. Call bridge.onAuthChange() before connect()."
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
if (!this.contextChangeCallback) {
|
|
430
|
+
console.warn(
|
|
431
|
+
"[vibe-bridge] connected without onContextChange \u2014 the app will not react to a tenant/subsidiary/period switch. Call bridge.onContextChange() before connect()."
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
657
435
|
// ---------------------------------------------------------------------------
|
|
658
436
|
// Host navigation & UI commands
|
|
659
437
|
//
|
|
@@ -673,6 +451,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
673
451
|
* ```
|
|
674
452
|
*/
|
|
675
453
|
navigate(path, action = "push") {
|
|
454
|
+
assertBrowser("navigate()");
|
|
676
455
|
this.sendCommand("NAVIGATE", { path, action });
|
|
677
456
|
}
|
|
678
457
|
/**
|
|
@@ -686,10 +465,12 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
686
465
|
* ```
|
|
687
466
|
*/
|
|
688
467
|
navigateTo(route, routeParams, action = "push") {
|
|
468
|
+
assertBrowser("navigateTo()");
|
|
689
469
|
this.sendCommand("NAVIGATE", { route, routeParams, action });
|
|
690
470
|
}
|
|
691
471
|
/** Refreshes the host's current route (re-fetches its server data). Fire-and-forget. */
|
|
692
472
|
refresh() {
|
|
473
|
+
assertBrowser("refresh()");
|
|
693
474
|
this.sendCommand("NAVIGATE", { action: "refresh" });
|
|
694
475
|
}
|
|
695
476
|
/**
|
|
@@ -711,8 +492,9 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
711
492
|
*
|
|
712
493
|
* **No-op until {@link connect} has resolved with PostHog enabled by the
|
|
713
494
|
* host** — if the host didn't send a `posthog` block (older host, or recording
|
|
714
|
-
* disabled for this env/app),
|
|
715
|
-
*
|
|
495
|
+
* disabled for this env/app), or `posthog-js` isn't installed (it is an
|
|
496
|
+
* optional dependency), nothing is sent. App authors don't import `posthog-js`
|
|
497
|
+
* themselves; this is the supported event surface.
|
|
716
498
|
*
|
|
717
499
|
* The event is sent **directly** from the iframe's own PostHog instance (only
|
|
718
500
|
* the session *recording* is forwarded to the parent), so it carries the
|
|
@@ -729,23 +511,38 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
729
511
|
* ```
|
|
730
512
|
*/
|
|
731
513
|
track(event, properties) {
|
|
732
|
-
|
|
733
|
-
|
|
514
|
+
assertBrowser("track()");
|
|
515
|
+
if (!this.posthogInitialized || !this.posthogInstance) return;
|
|
516
|
+
this.posthogInstance.capture(event, properties);
|
|
734
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* Tears down the bridge: stops the connect handshake, removes the message
|
|
520
|
+
* listener, restores patched history methods, and rejects any in-flight calls
|
|
521
|
+
* with a `'DESTROYED'` error. **Idempotent and safe on the server** — calling
|
|
522
|
+
* it when {@link connect}/{@link attach} never ran (or where there is no
|
|
523
|
+
* `window`) is a no-op, so it is safe as a React effect cleanup.
|
|
524
|
+
*/
|
|
735
525
|
destroy() {
|
|
736
526
|
this.teardownNavigationTracking();
|
|
737
|
-
this.
|
|
527
|
+
this.rejectConnect?.(new BridgeError("DESTROYED", "Bridge destroyed"));
|
|
738
528
|
this.stopConnectPolling();
|
|
739
|
-
window
|
|
529
|
+
if (this.attached && typeof window !== "undefined") {
|
|
530
|
+
window.removeEventListener("message", this.boundHandleMessage);
|
|
531
|
+
}
|
|
532
|
+
this.attached = false;
|
|
740
533
|
for (const pending of this.pendingRequests.values()) {
|
|
741
534
|
pending.reject(new BridgeError("DESTROYED", "Bridge destroyed"));
|
|
742
535
|
}
|
|
743
536
|
this.pendingRequests.clear();
|
|
744
537
|
this.context = null;
|
|
745
538
|
this.posthogInitialized = false;
|
|
539
|
+
this.posthogInstance = null;
|
|
540
|
+
this.posthogIniting = false;
|
|
746
541
|
this.connectPromise = null;
|
|
747
542
|
this.onContextReceived = null;
|
|
748
|
-
this.
|
|
543
|
+
this.rejectConnect = null;
|
|
544
|
+
this.contextChangeCallback = null;
|
|
545
|
+
this.authChangeCallback = null;
|
|
749
546
|
this.subrouteCallback = null;
|
|
750
547
|
this.lastReportedSubroute = null;
|
|
751
548
|
}
|
|
@@ -758,25 +555,35 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
758
555
|
clearTimeout(this.connectTimeout);
|
|
759
556
|
this.connectTimeout = null;
|
|
760
557
|
}
|
|
761
|
-
|
|
558
|
+
}
|
|
559
|
+
warnPosthogUnavailable() {
|
|
560
|
+
if (this.posthogUnavailableWarned) return;
|
|
561
|
+
this.posthogUnavailableWarned = true;
|
|
562
|
+
console.warn(
|
|
563
|
+
"[vibe-bridge] The host enabled session recording but `posthog-js` is not installed (it is an optional dependency). Recording and track() are disabled. Run `npm i posthog-js` to enable them."
|
|
564
|
+
);
|
|
762
565
|
}
|
|
763
566
|
/**
|
|
764
567
|
* Initializes PostHog session recording + analytics from the host-supplied
|
|
765
|
-
* config
|
|
568
|
+
* config carried by a one-time `POSTHOG_PUSH`, **only when the host opted in**
|
|
569
|
+
* (`config.enabled`).
|
|
766
570
|
*
|
|
767
|
-
*
|
|
768
|
-
* (
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
571
|
+
* `posthog-js` is an **optional dependency**, loaded lazily via `await
|
|
572
|
+
* import('posthog-js')` the first time recording is enabled — so it stays out
|
|
573
|
+
* of the import graph (and out of any server bundle) for apps that never
|
|
574
|
+
* record. If it isn't installed, this warns once and no-ops (never throws).
|
|
575
|
+
*
|
|
576
|
+
* The host is the single source of truth: when it sends no `POSTHOG_PUSH`
|
|
577
|
+
* (recording disabled for this env/app) or `enabled: false`, this does
|
|
578
|
+
* nothing. Cross-origin replay needs `recordCrossOriginIframes` on both sides
|
|
579
|
+
* so the parent receives the iframe's rrweb frames into one stitched recording,
|
|
580
|
+
* and `identify()` ties the iframe's recording + events to the same person as
|
|
581
|
+
* the host. Idempotent (the `posthogInitialized` guard) and wrapped in
|
|
582
|
+
* try/catch so a duplicate push or a PostHog failure is harmless.
|
|
775
583
|
*/
|
|
776
|
-
initPostHog(
|
|
777
|
-
|
|
778
|
-
if (
|
|
779
|
-
if (this.posthogInitialized) return;
|
|
584
|
+
async initPostHog(config) {
|
|
585
|
+
if (!config.enabled) return;
|
|
586
|
+
if (this.posthogInitialized || this.posthogIniting) return;
|
|
780
587
|
if (typeof window === "undefined") return;
|
|
781
588
|
if (!config.token || !config.apiHost || !config.distinctId) {
|
|
782
589
|
console.warn(
|
|
@@ -784,7 +591,20 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
784
591
|
);
|
|
785
592
|
return;
|
|
786
593
|
}
|
|
594
|
+
this.posthogIniting = true;
|
|
787
595
|
try {
|
|
596
|
+
let posthog;
|
|
597
|
+
try {
|
|
598
|
+
posthog = resolvePosthog(await import("posthog-js"));
|
|
599
|
+
} catch {
|
|
600
|
+
this.warnPosthogUnavailable();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (!posthog) {
|
|
604
|
+
this.warnPosthogUnavailable();
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
if (!this.attached) return;
|
|
788
608
|
posthog.init(config.token, {
|
|
789
609
|
api_host: config.apiHost,
|
|
790
610
|
// absolute ingestion host, NOT a relative /ingest proxy
|
|
@@ -807,16 +627,19 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
807
627
|
}
|
|
808
628
|
});
|
|
809
629
|
posthog.identify(config.distinctId);
|
|
630
|
+
this.posthogInstance = posthog;
|
|
810
631
|
this.posthogInitialized = true;
|
|
811
632
|
console.log("[vibe-bridge] PostHog session recording initialized");
|
|
812
633
|
} catch (err) {
|
|
813
634
|
console.warn("[vibe-bridge] PostHog init failed \u2014 continuing without recording.", err);
|
|
635
|
+
} finally {
|
|
636
|
+
this.posthogIniting = false;
|
|
814
637
|
}
|
|
815
638
|
}
|
|
816
639
|
/**
|
|
817
640
|
* Monkey-patches `history.pushState` and `history.replaceState` to
|
|
818
641
|
* auto-detect SPA navigation and report it to the host. Called after
|
|
819
|
-
* connect() resolves.
|
|
642
|
+
* connect() resolves (so `window`/`history` are guaranteed present).
|
|
820
643
|
*/
|
|
821
644
|
setupNavigationTracking() {
|
|
822
645
|
if (this.origPushState) return;
|
|
@@ -867,6 +690,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
867
690
|
this.sendCommand("REPORT_SUBROUTE", { subroute, replace });
|
|
868
691
|
}
|
|
869
692
|
sendCommand(type, payload) {
|
|
693
|
+
this.ensureAttached();
|
|
870
694
|
this.postToParent({
|
|
871
695
|
__protocol: PROTOCOL_ID,
|
|
872
696
|
__version: PROTOCOL_VERSION,
|
|
@@ -882,12 +706,18 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
882
706
|
* `onProgress` callback. `type` autocompletes to every operation name and
|
|
883
707
|
* narrows `payload`/return to that operation's types.
|
|
884
708
|
*
|
|
709
|
+
* @throws Rejects with `BridgeError` code `'SERVER_ENVIRONMENT'` if called
|
|
710
|
+
* where there is no `window`.
|
|
885
711
|
* @example
|
|
886
712
|
* ```ts
|
|
887
713
|
* const accounts = await bridge.request('GET_ACCOUNTS', { query: { account_ids: ['acc-1'] } })
|
|
888
714
|
* ```
|
|
889
715
|
*/
|
|
890
716
|
request(type, payload, onProgress) {
|
|
717
|
+
if (typeof window === "undefined") {
|
|
718
|
+
return Promise.reject(serverEnvError(`request(${String(type)})`));
|
|
719
|
+
}
|
|
720
|
+
this.ensureAttached();
|
|
891
721
|
return new Promise((resolve, reject) => {
|
|
892
722
|
const requestId = crypto.randomUUID();
|
|
893
723
|
const timer = setTimeout(
|
|
@@ -934,15 +764,6 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
934
764
|
this.kindHandlers[event.data.kind]?.(event.data);
|
|
935
765
|
}
|
|
936
766
|
handleResponse(msg) {
|
|
937
|
-
if (msg.type === "GET_CONTEXT") {
|
|
938
|
-
if (!this.connectPollIds.has(msg.requestId)) return;
|
|
939
|
-
if (msg.ok) {
|
|
940
|
-
this.onContextReceived?.(msg.data);
|
|
941
|
-
} else {
|
|
942
|
-
this.onContextError?.(new BridgeError(msg.code ?? "CONTEXT_ERROR", msg.error));
|
|
943
|
-
}
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
767
|
const pending = this.pendingRequests.get(msg.requestId);
|
|
947
768
|
if (!pending) return;
|
|
948
769
|
this.pendingRequests.delete(msg.requestId);
|
|
@@ -962,8 +783,9 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
962
783
|
}
|
|
963
784
|
};
|
|
964
785
|
export {
|
|
965
|
-
|
|
786
|
+
BRIDGE_VERSION,
|
|
966
787
|
BridgeError,
|
|
967
788
|
HttpBridgeError,
|
|
968
789
|
VibeAppBridge
|
|
969
790
|
};
|
|
791
|
+
//# sourceMappingURL=index.js.map
|