@nominalso/vibe-bridge 0.5.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.
@@ -0,0 +1,1211 @@
1
+ 'use client';
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.browser.ts
32
+ var index_browser_exports = {};
33
+ __export(index_browser_exports, {
34
+ BRIDGE_VERSION: () => BRIDGE_VERSION,
35
+ BridgeError: () => BridgeError,
36
+ HttpBridgeError: () => HttpBridgeError,
37
+ VibeAppBridge: () => VibeAppBridge
38
+ });
39
+ module.exports = __toCommonJS(index_browser_exports);
40
+
41
+ // ../protocol-types/dist/index.js
42
+ function normalizeSubroute(subroute) {
43
+ const normalized = subroute.startsWith("/") ? subroute : `/${subroute}`;
44
+ const path = normalized.split(/[?#]/)[0];
45
+ let decoded = path;
46
+ let prev;
47
+ do {
48
+ prev = decoded;
49
+ decoded = decoded.replace(/%25/gi, "%").replace(/%2e/gi, ".").replace(/%2f/gi, "/");
50
+ } while (decoded !== prev);
51
+ if (decoded.split("/").some((segment) => segment === "..")) return null;
52
+ return normalized;
53
+ }
54
+ var PROTOCOL_ID = "nominal-vibe-bridge";
55
+ var PROTOCOL_VERSION = 2;
56
+ var BRIDGE_KINDS = ["request", "response", "push", "command", "progress"];
57
+ var CORRELATED_KINDS = ["request", "response", "progress"];
58
+ var BRIDGE_KINDS_SET = new Set(BRIDGE_KINDS);
59
+ var CORRELATED_KINDS_SET = new Set(CORRELATED_KINDS);
60
+ function isBridgeMessage(data) {
61
+ if (typeof data !== "object" || data === null) return false;
62
+ const d = data;
63
+ if (d.__protocol !== PROTOCOL_ID) return false;
64
+ if (d.__version !== PROTOCOL_VERSION) return false;
65
+ if (typeof d.kind !== "string" || !BRIDGE_KINDS_SET.has(d.kind)) return false;
66
+ if (typeof d.type !== "string") return false;
67
+ if (CORRELATED_KINDS_SET.has(d.kind) && typeof d.requestId !== "string") return false;
68
+ return true;
69
+ }
70
+ var BRIDGE_ERROR_BRAND = /* @__PURE__ */ Symbol.for("@nominalso/vibe-bridge:BridgeError");
71
+ var HTTP_BRIDGE_ERROR_BRAND = /* @__PURE__ */ Symbol.for("@nominalso/vibe-bridge:HttpBridgeError");
72
+ var BridgeError = class extends Error {
73
+ constructor(code, message) {
74
+ super(message);
75
+ this.code = code;
76
+ this.name = "BridgeError";
77
+ }
78
+ /**
79
+ * Cross-realm-safe type guard. Prefer this over `instanceof BridgeError` when
80
+ * an error may have been thrown by a differently-built copy of the SDK (e.g.
81
+ * server bundle vs client bundle) — it matches on a process-global brand
82
+ * rather than class identity.
83
+ */
84
+ static isBridgeError(err) {
85
+ return typeof err === "object" && err !== null && err[BRIDGE_ERROR_BRAND] === true;
86
+ }
87
+ };
88
+ Object.defineProperty(BridgeError.prototype, BRIDGE_ERROR_BRAND, {
89
+ value: true,
90
+ enumerable: false
91
+ });
92
+ var HttpBridgeError = class extends BridgeError {
93
+ constructor(status, message) {
94
+ super("REQUEST_FAILED", message);
95
+ this.status = status;
96
+ this.name = "HttpBridgeError";
97
+ }
98
+ /**
99
+ * Cross-realm-safe type guard (see {@link BridgeError.isBridgeError}). Returns
100
+ * `true` only for `HttpBridgeError` instances, not plain `BridgeError`s.
101
+ */
102
+ static isHttpBridgeError(err) {
103
+ return typeof err === "object" && err !== null && err[HTTP_BRIDGE_ERROR_BRAND] === true;
104
+ }
105
+ };
106
+ Object.defineProperty(HttpBridgeError.prototype, HTTP_BRIDGE_ERROR_BRAND, {
107
+ value: true,
108
+ enumerable: false
109
+ });
110
+ function isOriginPattern(entry) {
111
+ return entry.includes("*");
112
+ }
113
+ var patternCache = /* @__PURE__ */ new Map();
114
+ function patternToRegExp(pattern) {
115
+ const cached = patternCache.get(pattern);
116
+ if (cached) return cached;
117
+ const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
118
+ const body = escaped.replace(/\\\*/g, "[^./:@?#\\s]+");
119
+ const compiled = new RegExp(`^${body}$`);
120
+ patternCache.set(pattern, compiled);
121
+ return compiled;
122
+ }
123
+ function matchesOrigin(origin, entry) {
124
+ if (!isOriginPattern(entry)) return origin === entry;
125
+ return patternToRegExp(entry).test(origin);
126
+ }
127
+ function isOriginAllowed(origin, allowlist, { allowPatterns }) {
128
+ for (const entry of allowlist) {
129
+ if (isOriginPattern(entry)) {
130
+ if (allowPatterns && matchesOrigin(origin, entry)) return true;
131
+ } else if (origin === entry) {
132
+ return true;
133
+ }
134
+ }
135
+ return false;
136
+ }
137
+
138
+ // package.json
139
+ var version = "0.6.0";
140
+
141
+ // src/version.ts
142
+ var BRIDGE_VERSION = version;
143
+
144
+ // src/data-methods.ts
145
+ var BridgeDataMethods = class {
146
+ // ---------------------------------------------------------------------------
147
+ // Operations — Accounting: chart of accounts
148
+ //
149
+ // Each method is a typed wrapper over `request('<OP>', payload)`. The payload
150
+ // and return types come straight from the Nominal API (`RequestRegistry`).
151
+ // For any operation without a named method, call `request('<OP>', payload)`.
152
+ // ---------------------------------------------------------------------------
153
+ /**
154
+ * Fetch the flat chart of accounts for a subsidiary.
155
+ *
156
+ * @example
157
+ * ```ts
158
+ * const ctx = await bridge.connect()
159
+ * const accounts = await bridge.getChartOfAccounts({
160
+ * path: { subsidiary_id: ctx.subsidiaryId },
161
+ * })
162
+ * ```
163
+ */
164
+ getChartOfAccounts(payload) {
165
+ return this.request("GET_CHART_OF_ACCOUNTS", payload);
166
+ }
167
+ /** Fetch the chart of accounts as a hierarchical tree for a subsidiary. */
168
+ getCoaTree(payload) {
169
+ return this.request("GET_COA_TREE", payload);
170
+ }
171
+ /** Fetch a simplified flat chart of accounts for a subsidiary. */
172
+ getCoaFlatSimple(payload) {
173
+ return this.request("GET_COA_FLAT_SIMPLE", payload);
174
+ }
175
+ /** Fetch the flat chart of accounts grouped by account group. */
176
+ getCoaGrouped(payload) {
177
+ return this.request("GET_COA_GROUPED", payload);
178
+ }
179
+ /** Fetch a single chart-of-accounts account by id within a subsidiary. */
180
+ getCoaAccount(payload) {
181
+ return this.request("GET_COA_ACCOUNT", payload);
182
+ }
183
+ /** List the currencies available to a subsidiary's chart of accounts. */
184
+ getSubsidiaryAvailableCurrencies(payload) {
185
+ return this.request("GET_SUBSIDIARY_AVAILABLE_CURRENCIES", payload);
186
+ }
187
+ /** Fetch accounts at the tenant level. */
188
+ getAccounts(payload) {
189
+ return this.request("GET_ACCOUNTS", payload);
190
+ }
191
+ /** Fetch a single account by id. */
192
+ getAccount(payload) {
193
+ return this.request("GET_ACCOUNT", payload);
194
+ }
195
+ // ---------------------------------------------------------------------------
196
+ // Operations — Accounting: exchange rates
197
+ // ---------------------------------------------------------------------------
198
+ /** Fetch currency conversion rates for a currency. */
199
+ getConversionRates(payload) {
200
+ return this.request("GET_CONVERSION_RATES", payload);
201
+ }
202
+ /** Fetch the effective consolidation exchange rate. */
203
+ getEffectiveExchangeRate(payload) {
204
+ return this.request("GET_EFFECTIVE_EXCHANGE_RATE", payload);
205
+ }
206
+ /** Fetch the exchange-rate period covering a given input date. */
207
+ getExchangeRateByDate(payload) {
208
+ return this.request("GET_EXCHANGE_RATE_BY_DATE", payload);
209
+ }
210
+ // ---------------------------------------------------------------------------
211
+ // Operations — Accounting: dimensions
212
+ // ---------------------------------------------------------------------------
213
+ /** List accounting dimensions. */
214
+ getDimensions(payload) {
215
+ return this.request("GET_DIMENSIONS", payload);
216
+ }
217
+ /** List values for accounting dimensions. */
218
+ getDimensionValues(payload) {
219
+ return this.request("GET_DIMENSION_VALUES", payload);
220
+ }
221
+ /** List dimension values in hierarchical form. */
222
+ getDimensionValuesHierarchical(payload) {
223
+ return this.request("GET_DIMENSION_VALUES_HIERARCHICAL", payload);
224
+ }
225
+ /** List account assignments for a dimension/account pair. */
226
+ getDimensionAccountAssignments(payload) {
227
+ return this.request("GET_DIMENSION_ACCOUNT_ASSIGNMENTS", payload);
228
+ }
229
+ // ---------------------------------------------------------------------------
230
+ // Operations — Accounting: journal entries
231
+ // ---------------------------------------------------------------------------
232
+ /**
233
+ * Fetch journal entries for a subsidiary, optionally filtered by date range.
234
+ *
235
+ * @example
236
+ * ```ts
237
+ * const entries = await bridge.getJournalEntries({
238
+ * path: { subsidiary_id: ctx.subsidiaryId },
239
+ * query: { from_date: '2026-01-01', to_date: '2026-03-31' },
240
+ * })
241
+ * ```
242
+ */
243
+ getJournalEntries(payload) {
244
+ return this.request("GET_JOURNAL_ENTRIES", payload);
245
+ }
246
+ /** Fetch journal entry lines for a subsidiary. */
247
+ getJournalLines(payload) {
248
+ return this.request("GET_JOURNAL_LINES", payload);
249
+ }
250
+ /** Fetch a single journal entry by id within a subsidiary. */
251
+ getJournalEntry(payload) {
252
+ return this.request("GET_JOURNAL_ENTRY", payload);
253
+ }
254
+ // ---------------------------------------------------------------------------
255
+ // Operations — Activity: period instances
256
+ // ---------------------------------------------------------------------------
257
+ /** List accounting period instances. */
258
+ getPeriods(payload) {
259
+ return this.request("GET_PERIODS", payload);
260
+ }
261
+ /** Fetch a single period instance by id. */
262
+ getPeriodInstance(payload) {
263
+ return this.request("GET_PERIOD_INSTANCE", payload);
264
+ }
265
+ /** Fetch a period instance by its display slug (e.g. `"mar-2026"`). */
266
+ getPeriodInstanceBySlug(payload) {
267
+ return this.request("GET_PERIOD_INSTANCE_BY_SLUG", payload);
268
+ }
269
+ /** Fetch the close-progress breakdown for a period (by slug). */
270
+ getPeriodProgressBreakdown(payload) {
271
+ return this.request("GET_PERIOD_PROGRESS_BREAKDOWN", payload);
272
+ }
273
+ // ---------------------------------------------------------------------------
274
+ // Operations — Activity: activity definitions
275
+ // ---------------------------------------------------------------------------
276
+ /** List activity definitions. */
277
+ getActivityDefinitions(payload) {
278
+ return this.request("GET_ACTIVITY_DEFINITIONS", payload);
279
+ }
280
+ /** Fetch a single activity definition by id. */
281
+ getActivityDefinition(payload) {
282
+ return this.request("GET_ACTIVITY_DEFINITION", payload);
283
+ }
284
+ // ---------------------------------------------------------------------------
285
+ // Operations — Activity: activity instances
286
+ // ---------------------------------------------------------------------------
287
+ /** List activity instances. */
288
+ getActivityInstances(payload) {
289
+ return this.request("GET_ACTIVITY_INSTANCES", payload);
290
+ }
291
+ /** Fetch a single activity instance by id. */
292
+ getActivityInstance(payload) {
293
+ return this.request("GET_ACTIVITY_INSTANCE", payload);
294
+ }
295
+ /** Fetch an activity instance for a given period + activity definition. */
296
+ getActivityInstanceByPeriod(payload) {
297
+ return this.request("GET_ACTIVITY_INSTANCE_BY_PERIOD", payload);
298
+ }
299
+ /** List task instances for an activity instance. */
300
+ getActivityInstanceTasks(payload) {
301
+ return this.request("GET_ACTIVITY_INSTANCE_TASKS", payload);
302
+ }
303
+ /** List task instances for an activity in a given period. */
304
+ getActivityPeriodTasks(payload) {
305
+ return this.request("GET_ACTIVITY_PERIOD_TASKS", payload);
306
+ }
307
+ // ---------------------------------------------------------------------------
308
+ // Operations — Activity: task definitions
309
+ // ---------------------------------------------------------------------------
310
+ /** List task definitions. */
311
+ getTaskDefinitions(payload) {
312
+ return this.request("GET_TASK_DEFINITIONS", payload);
313
+ }
314
+ /** Fetch a single task definition by id. */
315
+ getTaskDefinition(payload) {
316
+ return this.request("GET_TASK_DEFINITION", payload);
317
+ }
318
+ /** Create a task definition. */
319
+ createTaskDefinition(payload) {
320
+ return this.request("CREATE_TASK_DEFINITION", payload);
321
+ }
322
+ /** Update a task definition by id. */
323
+ updateTaskDefinition(payload) {
324
+ return this.request("UPDATE_TASK_DEFINITION", payload);
325
+ }
326
+ /** Query task definitions by filter. */
327
+ getTaskDefinitionsByFilter(payload) {
328
+ return this.request("GET_TASK_DEFINITIONS_BY_FILTER", payload);
329
+ }
330
+ // ---------------------------------------------------------------------------
331
+ // Operations — Activity: task instances
332
+ // ---------------------------------------------------------------------------
333
+ /** List task instances. */
334
+ getTaskInstances(payload) {
335
+ return this.request("GET_TASK_INSTANCES", payload);
336
+ }
337
+ /** Fetch a single task instance by id. */
338
+ getTaskInstance(payload) {
339
+ return this.request("GET_TASK_INSTANCE", payload);
340
+ }
341
+ /**
342
+ * Submit a Close-Management task output. This is the **only write path** back
343
+ * into Nominal — Vibe Apps never write Nominal data directly.
344
+ *
345
+ * @example
346
+ * ```ts
347
+ * // `body` carries the task-specific output shape.
348
+ * await bridge.postTaskOutput({
349
+ * path: { task_instance_id: 'task-1' },
350
+ * body: {},
351
+ * })
352
+ * ```
353
+ */
354
+ postTaskOutput(payload) {
355
+ return this.request("POST_TASK_OUTPUT", payload);
356
+ }
357
+ /** Query task instances by filter. */
358
+ getTaskInstancesByFilter(payload) {
359
+ return this.request("GET_TASK_INSTANCES_BY_FILTER", payload);
360
+ }
361
+ // ---------------------------------------------------------------------------
362
+ // Operations — Audit trail
363
+ // ---------------------------------------------------------------------------
364
+ /** List audit-trail events. */
365
+ getAuditEvents(payload) {
366
+ return this.request("GET_AUDIT_EVENTS", payload);
367
+ }
368
+ /** List audit-trail events for a specific entity. */
369
+ getEntityAuditEvents(payload) {
370
+ return this.request("GET_ENTITY_AUDIT_EVENTS", payload);
371
+ }
372
+ // ---------------------------------------------------------------------------
373
+ // Operations — Period manager: fiscal calendars
374
+ // ---------------------------------------------------------------------------
375
+ /** List fiscal calendars for a subsidiary. */
376
+ getFiscalCalendars(payload) {
377
+ return this.request("GET_FISCAL_CALENDARS", payload);
378
+ }
379
+ /** Fetch a single fiscal calendar by id. */
380
+ getFiscalCalendar(payload) {
381
+ return this.request("GET_FISCAL_CALENDAR", payload);
382
+ }
383
+ // ---------------------------------------------------------------------------
384
+ // Operations — Tenancy
385
+ // ---------------------------------------------------------------------------
386
+ /** List subsidiaries for the tenant. */
387
+ getSubsidiaries(payload) {
388
+ return this.request("GET_SUBSIDIARIES", payload);
389
+ }
390
+ /** Fetch a single subsidiary by id. */
391
+ getSubsidiary(payload) {
392
+ return this.request("GET_SUBSIDIARY", payload);
393
+ }
394
+ /** List parent currencies for a subsidiary. */
395
+ getSubsidiaryParentCurrencies(payload) {
396
+ return this.request("GET_SUBSIDIARY_PARENT_CURRENCIES", payload);
397
+ }
398
+ /** List users for the tenant. */
399
+ getTenantUsers(payload) {
400
+ return this.request("GET_TENANT_USERS", payload);
401
+ }
402
+ /**
403
+ * Uploads a file through the host. Converts the `File` to an `ArrayBuffer`
404
+ * before sending — sandboxed iframes cannot pass `File` handles across
405
+ * windows. Resolves once Nominal has stored the file.
406
+ *
407
+ * @example
408
+ * ```ts
409
+ * const input = document.querySelector<HTMLInputElement>('#file')!
410
+ * const file = input.files![0]
411
+ * const result = await bridge.upload(file, {
412
+ * entityType: 'JOURNAL_ENTRY',
413
+ * entityId: '123',
414
+ * onProgress: (p) => console.log(`${p.progress}%`),
415
+ * })
416
+ * // result.attachmentId, result.name
417
+ * ```
418
+ */
419
+ async upload(file, options) {
420
+ const buffer = await file.arrayBuffer();
421
+ const payload = {
422
+ buffer,
423
+ fileName: file.name,
424
+ fileType: file.type,
425
+ entityType: options.entityType,
426
+ entityId: options.entityId
427
+ };
428
+ const onProgress = options.onProgress ? (p) => options.onProgress(p) : void 0;
429
+ return this.request("UPLOAD_FILE", payload, onProgress);
430
+ }
431
+ };
432
+
433
+ // src/timeouts.ts
434
+ var CONNECT_TIMEOUT_MS = 1e4;
435
+ var CONNECT_POLL_MS = 500;
436
+ var DEFAULT_TIMEOUTS = {
437
+ /** Typed Nominal API reads/writes (`GET_*` / `POST_*` / `CREATE_*` / ...). */
438
+ api: 3e4,
439
+ /** `INVALIDATE_CACHE`. */
440
+ invalidate: 1e4,
441
+ /** `UPLOAD_FILE` — large payloads + presigned S3 PUT take a while. */
442
+ upload: 12e4
443
+ };
444
+ function resolveRequestTimeout(type, requestTimeout) {
445
+ if (requestTimeout !== void 0) return requestTimeout;
446
+ switch (type) {
447
+ case "UPLOAD_FILE":
448
+ return DEFAULT_TIMEOUTS.upload;
449
+ case "INVALIDATE_CACHE":
450
+ return DEFAULT_TIMEOUTS.invalidate;
451
+ default:
452
+ return DEFAULT_TIMEOUTS.api;
453
+ }
454
+ }
455
+
456
+ // src/VibeAppBridge.ts
457
+ function resolvePosthog(mod) {
458
+ const hasInit = (o) => typeof o?.init === "function";
459
+ const d1 = mod?.default;
460
+ if (hasInit(d1)) return d1;
461
+ const d2 = d1?.default;
462
+ if (hasInit(d2)) return d2;
463
+ if (hasInit(mod)) return mod;
464
+ return null;
465
+ }
466
+ function serverEnvError(method) {
467
+ return new BridgeError(
468
+ "SERVER_ENVIRONMENT",
469
+ `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.`
470
+ );
471
+ }
472
+ function assertBrowser(method) {
473
+ if (typeof window === "undefined") throw serverEnvError(method);
474
+ }
475
+ var PARENT_ORIGIN_OVERRIDE_KEY = "__NOM_VIBE_PARENT_ORIGIN__";
476
+ var PREVIEW_HOST_RE = /(?:^|\.)(?:vercel\.app|lovable\.app|lovable\.dev|lovableproject\.com)$|^(?:localhost|127\.0\.0\.1)$/;
477
+ function isPreviewSelfHost(hostname) {
478
+ return PREVIEW_HOST_RE.test(hostname);
479
+ }
480
+ function resolveActualParentOrigin() {
481
+ if (typeof window === "undefined") return null;
482
+ try {
483
+ const ancestors = window.location.ancestorOrigins;
484
+ if (ancestors && ancestors.length > 0 && ancestors[0]) return ancestors[0];
485
+ } catch {
486
+ }
487
+ try {
488
+ if (document.referrer) return new URL(document.referrer).origin;
489
+ } catch {
490
+ }
491
+ return null;
492
+ }
493
+ function resolveOriginPolicy(configured) {
494
+ if (typeof window !== "undefined") {
495
+ const override = window[PARENT_ORIGIN_OVERRIDE_KEY];
496
+ if (typeof override === "string" && override.length > 0) {
497
+ console.log(`[vibe-bridge] Using dev-bridge parentOrigin override: ${override}`);
498
+ return { targetOrigin: override, isTrusted: (o) => o === override };
499
+ }
500
+ }
501
+ const rawList = configured === void 0 ? [] : Array.isArray(configured) ? configured : [configured];
502
+ const allowlist = rawList.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
503
+ if (allowlist.length === 0) {
504
+ const actual2 = resolveActualParentOrigin();
505
+ if (actual2) return { targetOrigin: actual2, isTrusted: (o) => o === actual2 };
506
+ console.warn(
507
+ "[vibe-bridge] No `parentOrigin` set and the embedding host could not be auto-detected (no ancestorOrigins/referrer). Pass `parentOrigin` explicitly."
508
+ );
509
+ return { targetOrigin: null, isTrusted: () => false };
510
+ }
511
+ const exactEntries = allowlist.filter((entry) => !isOriginPattern(entry));
512
+ const hasPatterns = exactEntries.length !== allowlist.length;
513
+ if (allowlist.length === 1 && !hasPatterns) {
514
+ const only = allowlist[0];
515
+ return { targetOrigin: only, isTrusted: (o) => o === only };
516
+ }
517
+ const allowPatterns = hasPatterns && typeof window !== "undefined" && isPreviewSelfHost(window.location.hostname);
518
+ const isTrusted = (o) => isOriginAllowed(o, allowlist, { allowPatterns });
519
+ const actual = resolveActualParentOrigin();
520
+ let targetOrigin = null;
521
+ if (actual && isTrusted(actual)) {
522
+ targetOrigin = actual;
523
+ } else if (actual === null && !allowPatterns && exactEntries.length === 1) {
524
+ targetOrigin = exactEntries[0];
525
+ }
526
+ if (targetOrigin === null) {
527
+ console.warn(
528
+ "[vibe-bridge] Could not resolve a parent origin to post to. Check `parentOrigin` against the embedding host" + (hasPatterns && !allowPatterns ? " \u2014 pattern entries are ignored because this page is not served from a recognised preview host." : ".")
529
+ );
530
+ }
531
+ return { targetOrigin, isTrusted };
532
+ }
533
+ var VibeAppBridge = class extends BridgeDataMethods {
534
+ /**
535
+ * Stores options only — **pure and side-effect-free**. No `window`,
536
+ * `document`, `history`, `postMessage`, `crypto`, or `posthog-js` access
537
+ * happens here, so constructing a bridge is safe during SSR / in a Server
538
+ * Component / in a Worker. Browser wiring is deferred to {@link connect} /
539
+ * {@link attach}.
540
+ */
541
+ constructor(options = {}) {
542
+ super();
543
+ /** Concrete origin to `postMessage` to, or `null` until resolved at {@link attach}. */
544
+ this.targetOrigin = null;
545
+ /** Validates an inbound `event.origin`; rejects everything until {@link attach}. */
546
+ this.isTrustedOrigin = () => false;
547
+ this.pendingRequests = /* @__PURE__ */ new Map();
548
+ /** Whether {@link attach} has installed the message listener + resolved origin. */
549
+ this.attached = false;
550
+ this.context = null;
551
+ /**
552
+ * Set once {@link initPostHog} has successfully called `posthog.init()`.
553
+ * Guards against a double-init and makes {@link track} a no-op until
554
+ * recording is actually live.
555
+ */
556
+ this.posthogInitialized = false;
557
+ /** The lazily-imported `posthog-js` singleton, once recording is initialized. */
558
+ this.posthogInstance = null;
559
+ /**
560
+ * Set for the duration of an in-flight {@link initPostHog} (which is async — it
561
+ * awaits `import('posthog-js')`). Serializes initialization so two overlapping
562
+ * `POSTHOG_PUSH`es can't both call `posthog.init()`.
563
+ */
564
+ this.posthogIniting = false;
565
+ /** So the "posthog-js not installed" warning is logged at most once. */
566
+ this.posthogUnavailableWarned = false;
567
+ this.connectPromise = null;
568
+ this.connectPollInterval = null;
569
+ this.connectTimeout = null;
570
+ this.onContextReceived = null;
571
+ /** Rejects an in-flight connect() (used by destroy()); `null` once settled. */
572
+ this.rejectConnect = null;
573
+ /**
574
+ * Persistent subscriber for post-connect context changes (tenant/subsidiary
575
+ * switch, period close, …). Distinct from {@link onContextReceived}, which only
576
+ * resolves the connect handshake and is cleared afterward. `null` until
577
+ * {@link onContextChange} is called.
578
+ */
579
+ this.contextChangeCallback = null;
580
+ /**
581
+ * Persistent subscriber for host auth changes (Nominal logout or
582
+ * identity/tenant switch). `null` until {@link onAuthChange} is called.
583
+ */
584
+ this.authChangeCallback = null;
585
+ this.lastReportedSubroute = null;
586
+ this.suppressSubrouteReport = false;
587
+ this.subrouteCallback = null;
588
+ this.origPushState = null;
589
+ this.origReplaceState = null;
590
+ this.popstateHandler = null;
591
+ this.kindHandlers = {
592
+ response: (msg) => this.handleResponse(msg),
593
+ push: (msg) => this.dispatchPush(msg),
594
+ progress: (msg) => this.handleProgress(msg)
595
+ };
596
+ this.pushHandlers = {
597
+ CONTEXT_PUSH: (payload) => {
598
+ if (this.onContextReceived) {
599
+ this.onContextReceived(payload);
600
+ } else if (this.context) {
601
+ this.context = payload;
602
+ this.contextChangeCallback?.(payload);
603
+ }
604
+ },
605
+ // Persistent (not nulled after connect): the host delivers PostHog once at
606
+ // connect via its own push, and initPostHog is idempotent, so this can run
607
+ // whenever the message arrives, independent of how connect() resolved. Fire
608
+ // and forget — initPostHog is async (lazy-imports posthog-js) and swallows
609
+ // its own errors.
610
+ POSTHOG_PUSH: (payload) => {
611
+ void this.initPostHog(payload);
612
+ },
613
+ // Persistent: the host pushes auth changes (logout, identity/tenant
614
+ // switch) at any time after connect.
615
+ AUTH_CHANGED: (payload) => this.authChangeCallback?.(payload),
616
+ SUBROUTE_PUSH: (payload) => {
617
+ const safe = normalizeSubroute(payload.subroute);
618
+ if (!safe) return;
619
+ this.withSubrouteSuppressed(() => {
620
+ if (this.subrouteCallback) {
621
+ this.subrouteCallback(safe);
622
+ } else {
623
+ history.pushState(null, "", safe);
624
+ window.dispatchEvent(new PopStateEvent("popstate"));
625
+ }
626
+ this.lastReportedSubroute = safe;
627
+ });
628
+ }
629
+ };
630
+ /**
631
+ * Commands targeting the host's UI chrome. All fire-and-forget.
632
+ *
633
+ * @example
634
+ * ```ts
635
+ * bridge.ui.setSideNav({ collapsed: true })
636
+ * bridge.ui.switchSubsidiary(42)
637
+ * ```
638
+ */
639
+ this.ui = {
640
+ /** Collapse or expand the host's side navigation. */
641
+ setSideNav: (payload) => {
642
+ assertBrowser("ui.setSideNav()");
643
+ this.sendCommand("SET_SIDE_NAV", payload);
644
+ },
645
+ /** Switch the host to a different subsidiary. */
646
+ switchSubsidiary: (subsidiaryId) => {
647
+ assertBrowser("ui.switchSubsidiary()");
648
+ this.sendCommand("SWITCH_SUBSIDIARY", { subsidiaryId });
649
+ }
650
+ };
651
+ this.parentOriginOption = options.parentOrigin;
652
+ this.requestTimeout = options.requestTimeout;
653
+ this.boundHandleMessage = this.handleMessage.bind(this);
654
+ }
655
+ dispatchPush(msg) {
656
+ const handler = this.pushHandlers[msg.type];
657
+ handler(msg.payload);
658
+ }
659
+ /**
660
+ * Installs the browser wiring: resolves the parent-origin policy and starts
661
+ * listening for host `postMessage`s. **Idempotent** and safe to call before
662
+ * {@link connect} if you want the bridge receiving pushes early; {@link
663
+ * connect} calls it for you, so most apps never call this directly.
664
+ *
665
+ * @throws `BridgeError` code `'SERVER_ENVIRONMENT'` if called where there is no
666
+ * `window`.
667
+ */
668
+ attach() {
669
+ assertBrowser("attach()");
670
+ if (this.attached) return;
671
+ const policy = resolveOriginPolicy(this.parentOriginOption);
672
+ this.targetOrigin = policy.targetOrigin;
673
+ this.isTrustedOrigin = policy.isTrusted;
674
+ window.addEventListener("message", this.boundHandleMessage);
675
+ this.attached = true;
676
+ }
677
+ /**
678
+ * Lazily installs the browser wiring on first transport use. Keeps the
679
+ * constructor pure while ensuring that any method reaching the wire (a data
680
+ * `request`, a fire-and-forget command) works without an explicit prior
681
+ * `attach()`/`connect()` — attach stays idempotent, so `connect()` is
682
+ * unaffected.
683
+ */
684
+ ensureAttached() {
685
+ if (!this.attached) this.attach();
686
+ }
687
+ /**
688
+ * Connects to the host and returns the tenant/user {@link ContextPayload}.
689
+ * **Call this once on app init and await it before any other method** — data
690
+ * methods, `upload()`, and subroute reporting all require an established
691
+ * connection. Concurrent calls return the same promise.
692
+ *
693
+ * First {@link attach | attaches} the browser wiring (message listener +
694
+ * origin resolution), then sends a `CONNECT` handshake, re-sending every 500ms
695
+ * until the host replies by pushing the context (so it works even if the host
696
+ * mounts after the iframe loads). Context is only ever delivered by push.
697
+ * Rejects with `Bridge connect timed out` after 10 seconds if no context
698
+ * arrives (usually a `parentOrigin` mismatch or the host hasn't mounted).
699
+ *
700
+ * After connecting, if the context includes an initial subroute (e.g. from
701
+ * a deep link), the bridge navigates to it and starts auto-tracking
702
+ * internal navigation via the history API.
703
+ *
704
+ * @returns The tenant/user/subsidiary context for this app session.
705
+ * @throws Rejects with `BridgeError` code `'SERVER_ENVIRONMENT'` if called
706
+ * where there is no `window`, `'PARENT_ORIGIN_UNRESOLVED'` if no embedding
707
+ * origin could be resolved, or `'TIMEOUT'` if no context arrives in 10s.
708
+ * @example
709
+ * ```ts
710
+ * const ctx = await bridge.connect()
711
+ * // ctx.tenant, ctx.subsidiaryId, ctx.subsidiaries, ctx.user, ctx.lastClosedPeriodSlug
712
+ * ```
713
+ */
714
+ connect() {
715
+ if (typeof window === "undefined") return Promise.reject(serverEnvError("connect()"));
716
+ if (this.context) return Promise.resolve(this.context);
717
+ if (this.connectPromise) return this.connectPromise;
718
+ this.attach();
719
+ if (this.targetOrigin === null) {
720
+ return Promise.reject(
721
+ new BridgeError(
722
+ "PARENT_ORIGIN_UNRESOLVED",
723
+ "Could not resolve a parent origin to connect to. Check `parentOrigin` against the embedding host."
724
+ )
725
+ );
726
+ }
727
+ this.connectPromise = new Promise((resolve, reject) => {
728
+ const timer = setTimeout(() => {
729
+ this.stopConnectPolling();
730
+ this.connectPromise = null;
731
+ this.onContextReceived = null;
732
+ this.rejectConnect = null;
733
+ reject(new BridgeError("TIMEOUT", "Bridge connect timed out"));
734
+ }, CONNECT_TIMEOUT_MS);
735
+ this.connectTimeout = timer;
736
+ this.rejectConnect = (error) => {
737
+ clearTimeout(timer);
738
+ this.stopConnectPolling();
739
+ this.connectPromise = null;
740
+ this.onContextReceived = null;
741
+ this.rejectConnect = null;
742
+ reject(error);
743
+ };
744
+ this.onContextReceived = (ctx) => {
745
+ clearTimeout(timer);
746
+ this.stopConnectPolling();
747
+ this.context = ctx;
748
+ this.connectPromise = null;
749
+ this.onContextReceived = null;
750
+ this.rejectConnect = null;
751
+ console.log(
752
+ `[vibe-bridge] Connected \u2014 bridge: ${BRIDGE_VERSION}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
753
+ );
754
+ const initialSubroute = ctx.subroute ? normalizeSubroute(ctx.subroute) : null;
755
+ if (initialSubroute && initialSubroute !== "/") {
756
+ this.withSubrouteSuppressed(() => {
757
+ history.replaceState(null, "", initialSubroute);
758
+ window.dispatchEvent(new PopStateEvent("popstate"));
759
+ this.lastReportedSubroute = initialSubroute;
760
+ });
761
+ } else {
762
+ this.lastReportedSubroute = window.location.pathname + window.location.search;
763
+ }
764
+ this.setupNavigationTracking();
765
+ this.warnMissingListeners();
766
+ resolve(ctx);
767
+ };
768
+ const sendConnect = () => {
769
+ this.sendCommand("CONNECT", { bridgeVersion: BRIDGE_VERSION });
770
+ };
771
+ sendConnect();
772
+ this.connectPollInterval = setInterval(sendConnect, CONNECT_POLL_MS);
773
+ });
774
+ return this.connectPromise;
775
+ }
776
+ /**
777
+ * Manually reports the current subroute to the host.
778
+ * Usually not needed — navigation is auto-detected via the history API.
779
+ * Use this for hash-based routers or non-standard navigation patterns.
780
+ */
781
+ reportSubroute(subroute, options) {
782
+ assertBrowser("reportSubroute()");
783
+ const normalized = normalizeSubroute(subroute);
784
+ if (normalized === null) return;
785
+ this.lastReportedSubroute = normalized;
786
+ this.sendCommand("REPORT_SUBROUTE", { subroute: normalized, replace: options?.replace });
787
+ }
788
+ /**
789
+ * Registers a callback for host-initiated navigation (browser back/forward).
790
+ * If no callback is registered, the SDK uses `history.pushState` + a
791
+ * `popstate` event, which works for most SPA routers automatically.
792
+ * Returns an unsubscribe function.
793
+ *
794
+ * Pure — only stores the callback, so it is safe to call before {@link connect}.
795
+ */
796
+ onSubrouteRequest(callback) {
797
+ this.subrouteCallback = callback;
798
+ return () => {
799
+ this.subrouteCallback = null;
800
+ };
801
+ }
802
+ /**
803
+ * Subscribes to post-connect context changes (tenant/subsidiary switch, period
804
+ * close, …). The callback fires for each {@link ContextPayload} the host pushes
805
+ * *after* connect — not for the initial context, which {@link connect} already
806
+ * returns. Returns an unsubscribe function. Single subscriber: a second call
807
+ * replaces the first. Wire it **before {@link connect}** (the bridge warns at
808
+ * connect if it isn't).
809
+ *
810
+ * Pure — only stores the callback, so it is safe to call before {@link connect}.
811
+ */
812
+ onContextChange(callback) {
813
+ this.contextChangeCallback = callback;
814
+ return () => {
815
+ this.contextChangeCallback = null;
816
+ };
817
+ }
818
+ /**
819
+ * Subscribes to host auth changes: a Nominal logout (`authenticated: false` —
820
+ * sign your backend out) or an identity/tenant switch (`authenticated: true`
821
+ * with a new `userId`/`tenant` — re-authenticate). Fires for each change the
822
+ * host pushes after connect. Returns an unsubscribe function. Single
823
+ * subscriber: a second call replaces the first.
824
+ *
825
+ * **Wire this before {@link connect}** — without it the embedded app won't
826
+ * sign out when the user logs out of Nominal (the bridge warns at connect if
827
+ * it isn't wired). Pure — only stores the callback.
828
+ */
829
+ onAuthChange(callback) {
830
+ this.authChangeCallback = callback;
831
+ return () => {
832
+ this.authChangeCallback = null;
833
+ };
834
+ }
835
+ /**
836
+ * At connect, nudge the app if it hasn't wired the change listeners: every
837
+ * embedded app should react to a Nominal logout ({@link onAuthChange}) and a
838
+ * tenant/subsidiary switch ({@link onContextChange}). Fires once during the
839
+ * connect flow (not speculatively later), so wire both **before** `connect()`.
840
+ * The skill/codegen does this by default; this catches hand-wired apps.
841
+ */
842
+ warnMissingListeners() {
843
+ if (!this.authChangeCallback) {
844
+ console.warn(
845
+ "[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()."
846
+ );
847
+ }
848
+ if (!this.contextChangeCallback) {
849
+ console.warn(
850
+ "[vibe-bridge] connected without onContextChange \u2014 the app will not react to a tenant/subsidiary/period switch. Call bridge.onContextChange() before connect()."
851
+ );
852
+ }
853
+ }
854
+ // ---------------------------------------------------------------------------
855
+ // Host navigation & UI commands
856
+ //
857
+ // These drive the *host* (nom-ui) chrome — its router, side nav, and
858
+ // subsidiary switcher — rather than the iframe's own internal routing
859
+ // (`reportSubroute`). Navigation/UI commands are fire-and-forget;
860
+ // `invalidateCache` is awaited.
861
+ // ---------------------------------------------------------------------------
862
+ /**
863
+ * Navigates the host to a raw path relative to this app's tenant/subsidiary
864
+ * base. Fire-and-forget.
865
+ *
866
+ * @example
867
+ * ```ts
868
+ * bridge.navigate('/journal-entries/123')
869
+ * bridge.navigate('/journal-entries/123', 'replace')
870
+ * ```
871
+ */
872
+ navigate(path, action = "push") {
873
+ assertBrowser("navigate()");
874
+ this.sendCommand("NAVIGATE", { path, action });
875
+ }
876
+ /**
877
+ * Navigates the host to a named Nominal route, with optional params for
878
+ * placeholder segments. Fire-and-forget.
879
+ *
880
+ * @example
881
+ * ```ts
882
+ * bridge.navigateTo('CHART_OF_ACCOUNTS')
883
+ * bridge.navigateTo('JOURNAL_ENTRY', { id: '123' }, 'replace')
884
+ * ```
885
+ */
886
+ navigateTo(route, routeParams, action = "push") {
887
+ assertBrowser("navigateTo()");
888
+ this.sendCommand("NAVIGATE", { route, routeParams, action });
889
+ }
890
+ /** Refreshes the host's current route (re-fetches its server data). Fire-and-forget. */
891
+ refresh() {
892
+ assertBrowser("refresh()");
893
+ this.sendCommand("NAVIGATE", { action: "refresh" });
894
+ }
895
+ /**
896
+ * Asks the host to invalidate cached data by path prefix and/or cache tag so
897
+ * subsequent reads see fresh values. Awaited.
898
+ *
899
+ * @example
900
+ * ```ts
901
+ * await bridge.invalidateCache({ tags: ['SUBSIDIARIES'], paths: ['/accounting/accounts'] })
902
+ * ```
903
+ */
904
+ invalidateCache(payload) {
905
+ return this.request("INVALIDATE_CACHE", payload);
906
+ }
907
+ /**
908
+ * Captures a custom product event from inside the Vibe App, attributed to the
909
+ * same PostHog person as the host (via the `distinctId` the host supplied at
910
+ * connect).
911
+ *
912
+ * **No-op until {@link connect} has resolved with PostHog enabled by the
913
+ * host** — if the host didn't send a `posthog` block (older host, or recording
914
+ * disabled for this env/app), or `posthog-js` isn't installed (it is an
915
+ * optional dependency), nothing is sent. App authors don't import `posthog-js`
916
+ * themselves; this is the supported event surface.
917
+ *
918
+ * The event is sent **directly** from the iframe's own PostHog instance (only
919
+ * the session *recording* is forwarded to the parent), so it carries the
920
+ * iframe instance's `$session_id` rather than the host's — events tie to the
921
+ * same person, but cross-boundary event↔replay timeline correlation is a known
922
+ * limitation.
923
+ *
924
+ * @param event Event name. Namespacing app events (e.g. `'vibe:<slug>:<action>'`)
925
+ * keeps them filterable from the host's own events.
926
+ * @param properties Optional event properties.
927
+ * @example
928
+ * ```ts
929
+ * bridge.track('vibe:fixed-assets:report_exported', { format: 'csv', rows: 128 })
930
+ * ```
931
+ */
932
+ track(event, properties) {
933
+ assertBrowser("track()");
934
+ if (!this.posthogInitialized || !this.posthogInstance) return;
935
+ this.posthogInstance.capture(event, properties);
936
+ }
937
+ /**
938
+ * Tears down the bridge: stops the connect handshake, removes the message
939
+ * listener, restores patched history methods, and rejects any in-flight calls
940
+ * with a `'DESTROYED'` error. **Idempotent and safe on the server** — calling
941
+ * it when {@link connect}/{@link attach} never ran (or where there is no
942
+ * `window`) is a no-op, so it is safe as a React effect cleanup.
943
+ */
944
+ destroy() {
945
+ this.teardownNavigationTracking();
946
+ this.rejectConnect?.(new BridgeError("DESTROYED", "Bridge destroyed"));
947
+ this.stopConnectPolling();
948
+ if (this.attached && typeof window !== "undefined") {
949
+ window.removeEventListener("message", this.boundHandleMessage);
950
+ }
951
+ this.attached = false;
952
+ for (const pending of this.pendingRequests.values()) {
953
+ pending.reject(new BridgeError("DESTROYED", "Bridge destroyed"));
954
+ }
955
+ this.pendingRequests.clear();
956
+ this.context = null;
957
+ this.posthogInitialized = false;
958
+ this.posthogInstance = null;
959
+ this.posthogIniting = false;
960
+ this.connectPromise = null;
961
+ this.onContextReceived = null;
962
+ this.rejectConnect = null;
963
+ this.contextChangeCallback = null;
964
+ this.authChangeCallback = null;
965
+ this.subrouteCallback = null;
966
+ this.lastReportedSubroute = null;
967
+ }
968
+ stopConnectPolling() {
969
+ if (this.connectPollInterval !== null) {
970
+ clearInterval(this.connectPollInterval);
971
+ this.connectPollInterval = null;
972
+ }
973
+ if (this.connectTimeout !== null) {
974
+ clearTimeout(this.connectTimeout);
975
+ this.connectTimeout = null;
976
+ }
977
+ }
978
+ warnPosthogUnavailable() {
979
+ if (this.posthogUnavailableWarned) return;
980
+ this.posthogUnavailableWarned = true;
981
+ console.warn(
982
+ "[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."
983
+ );
984
+ }
985
+ /**
986
+ * Initializes PostHog session recording + analytics from the host-supplied
987
+ * config carried by a one-time `POSTHOG_PUSH`, **only when the host opted in**
988
+ * (`config.enabled`).
989
+ *
990
+ * `posthog-js` is an **optional dependency**, loaded lazily via `await
991
+ * import('posthog-js')` the first time recording is enabled — so it stays out
992
+ * of the import graph (and out of any server bundle) for apps that never
993
+ * record. If it isn't installed, this warns once and no-ops (never throws).
994
+ *
995
+ * The host is the single source of truth: when it sends no `POSTHOG_PUSH`
996
+ * (recording disabled for this env/app) or `enabled: false`, this does
997
+ * nothing. Cross-origin replay needs `recordCrossOriginIframes` on both sides
998
+ * so the parent receives the iframe's rrweb frames into one stitched recording,
999
+ * and `identify()` ties the iframe's recording + events to the same person as
1000
+ * the host. Idempotent (the `posthogInitialized` guard) and wrapped in
1001
+ * try/catch so a duplicate push or a PostHog failure is harmless.
1002
+ */
1003
+ async initPostHog(config) {
1004
+ if (!config.enabled) return;
1005
+ if (this.posthogInitialized || this.posthogIniting) return;
1006
+ if (typeof window === "undefined") return;
1007
+ if (!config.token || !config.apiHost || !config.distinctId) {
1008
+ console.warn(
1009
+ "[vibe-bridge] PostHog enabled but token/apiHost/distinctId missing \u2014 skipping init."
1010
+ );
1011
+ return;
1012
+ }
1013
+ this.posthogIniting = true;
1014
+ try {
1015
+ let posthog;
1016
+ try {
1017
+ posthog = resolvePosthog(await import("posthog-js"));
1018
+ } catch {
1019
+ this.warnPosthogUnavailable();
1020
+ return;
1021
+ }
1022
+ if (!posthog) {
1023
+ this.warnPosthogUnavailable();
1024
+ return;
1025
+ }
1026
+ if (!this.attached) return;
1027
+ posthog.init(config.token, {
1028
+ api_host: config.apiHost,
1029
+ // absolute ingestion host, NOT a relative /ingest proxy
1030
+ person_profiles: "identified_only",
1031
+ capture_pageview: false,
1032
+ // the host owns pageviews; don't double-count
1033
+ autocapture: false,
1034
+ // opt-in events only, via track()
1035
+ // Seed the host's session id so track() events share the host's
1036
+ // $session_id and line up with the stitched replay. Replay stitching
1037
+ // itself does not depend on this (recordCrossOriginIframes handles it).
1038
+ ...config.sessionId ? { bootstrap: { sessionID: config.sessionId } } : {},
1039
+ session_recording: {
1040
+ recordCrossOriginIframes: true,
1041
+ // REQUIRED so the parent receives our rrweb frames
1042
+ // Mirror nom-ui's posture (PostHogProvider.tsx): unmask inputs for
1043
+ // richer insight, but keep password fields masked.
1044
+ maskAllInputs: false,
1045
+ maskInputOptions: { password: true }
1046
+ }
1047
+ });
1048
+ posthog.identify(config.distinctId);
1049
+ this.posthogInstance = posthog;
1050
+ this.posthogInitialized = true;
1051
+ console.log("[vibe-bridge] PostHog session recording initialized");
1052
+ } catch (err) {
1053
+ console.warn("[vibe-bridge] PostHog init failed \u2014 continuing without recording.", err);
1054
+ } finally {
1055
+ this.posthogIniting = false;
1056
+ }
1057
+ }
1058
+ /**
1059
+ * Monkey-patches `history.pushState` and `history.replaceState` to
1060
+ * auto-detect SPA navigation and report it to the host. Called after
1061
+ * connect() resolves (so `window`/`history` are guaranteed present).
1062
+ */
1063
+ setupNavigationTracking() {
1064
+ if (this.origPushState) return;
1065
+ this.origPushState = history.pushState.bind(history);
1066
+ this.origReplaceState = history.replaceState.bind(history);
1067
+ const origPush = this.origPushState;
1068
+ const origReplace = this.origReplaceState;
1069
+ history.pushState = (...args) => {
1070
+ origPush(...args);
1071
+ this.reportCurrentSubroute(false);
1072
+ };
1073
+ history.replaceState = (...args) => {
1074
+ origReplace(...args);
1075
+ this.reportCurrentSubroute(true);
1076
+ };
1077
+ this.popstateHandler = () => this.reportCurrentSubroute(true);
1078
+ window.addEventListener("popstate", this.popstateHandler);
1079
+ }
1080
+ teardownNavigationTracking() {
1081
+ if (this.origPushState) {
1082
+ history.pushState = this.origPushState;
1083
+ this.origPushState = null;
1084
+ }
1085
+ if (this.origReplaceState) {
1086
+ history.replaceState = this.origReplaceState;
1087
+ this.origReplaceState = null;
1088
+ }
1089
+ if (this.popstateHandler) {
1090
+ window.removeEventListener("popstate", this.popstateHandler);
1091
+ this.popstateHandler = null;
1092
+ }
1093
+ }
1094
+ /** Runs `fn` while suppressing subroute auto-reporting to prevent echo loops. */
1095
+ withSubrouteSuppressed(fn) {
1096
+ this.suppressSubrouteReport = true;
1097
+ try {
1098
+ fn();
1099
+ } finally {
1100
+ this.suppressSubrouteReport = false;
1101
+ }
1102
+ }
1103
+ reportCurrentSubroute(replace) {
1104
+ if (this.suppressSubrouteReport) return;
1105
+ const subroute = normalizeSubroute(window.location.pathname + window.location.search);
1106
+ if (subroute === null) return;
1107
+ if (subroute === this.lastReportedSubroute) return;
1108
+ this.lastReportedSubroute = subroute;
1109
+ this.sendCommand("REPORT_SUBROUTE", { subroute, replace });
1110
+ }
1111
+ sendCommand(type, payload) {
1112
+ this.ensureAttached();
1113
+ this.postToParent({
1114
+ __protocol: PROTOCOL_ID,
1115
+ __version: PROTOCOL_VERSION,
1116
+ kind: "command",
1117
+ type,
1118
+ payload
1119
+ });
1120
+ }
1121
+ /**
1122
+ * Low-level typed request for **any** operation in {@link RequestRegistry}.
1123
+ * The named methods (e.g. {@link VibeAppBridge.getChartOfAccounts}) wrap this;
1124
+ * use it directly for operations without a named method, or to pass an
1125
+ * `onProgress` callback. `type` autocompletes to every operation name and
1126
+ * narrows `payload`/return to that operation's types.
1127
+ *
1128
+ * @throws Rejects with `BridgeError` code `'SERVER_ENVIRONMENT'` if called
1129
+ * where there is no `window`.
1130
+ * @example
1131
+ * ```ts
1132
+ * const accounts = await bridge.request('GET_ACCOUNTS', { query: { account_ids: ['acc-1'] } })
1133
+ * ```
1134
+ */
1135
+ request(type, payload, onProgress) {
1136
+ if (typeof window === "undefined") {
1137
+ return Promise.reject(serverEnvError(`request(${String(type)})`));
1138
+ }
1139
+ this.ensureAttached();
1140
+ return new Promise((resolve, reject) => {
1141
+ const requestId = crypto.randomUUID();
1142
+ const timer = setTimeout(
1143
+ () => {
1144
+ this.pendingRequests.delete(requestId);
1145
+ reject(new BridgeError("TIMEOUT", `Vibe bridge request timed out: ${type}`));
1146
+ },
1147
+ resolveRequestTimeout(type, this.requestTimeout)
1148
+ );
1149
+ this.pendingRequests.set(requestId, {
1150
+ resolve: (data) => {
1151
+ clearTimeout(timer);
1152
+ resolve(data);
1153
+ },
1154
+ reject: (error) => {
1155
+ clearTimeout(timer);
1156
+ reject(error);
1157
+ },
1158
+ onProgress
1159
+ });
1160
+ const message = {
1161
+ __protocol: PROTOCOL_ID,
1162
+ __version: PROTOCOL_VERSION,
1163
+ kind: "request",
1164
+ type,
1165
+ requestId,
1166
+ payload
1167
+ };
1168
+ this.postToParent(message);
1169
+ });
1170
+ }
1171
+ /**
1172
+ * Posts to the embedding host. No-op when no concrete target origin could be
1173
+ * resolved (a config/embedding error) — `connect()` rejects fast in that case
1174
+ * so calls never silently hang waiting on a timeout.
1175
+ */
1176
+ postToParent(message) {
1177
+ if (this.targetOrigin === null) return;
1178
+ window.parent.postMessage(message, this.targetOrigin);
1179
+ }
1180
+ handleMessage(event) {
1181
+ if (!this.isTrustedOrigin(event.origin)) return;
1182
+ if (!isBridgeMessage(event.data)) return;
1183
+ this.kindHandlers[event.data.kind]?.(event.data);
1184
+ }
1185
+ handleResponse(msg) {
1186
+ const pending = this.pendingRequests.get(msg.requestId);
1187
+ if (!pending) return;
1188
+ this.pendingRequests.delete(msg.requestId);
1189
+ if (msg.ok) {
1190
+ pending.resolve(msg.data);
1191
+ } else if (msg.status !== void 0) {
1192
+ pending.reject(new HttpBridgeError(msg.status, msg.error));
1193
+ } else if (msg.code) {
1194
+ pending.reject(new BridgeError(msg.code, msg.error));
1195
+ } else {
1196
+ pending.reject(new Error(msg.error));
1197
+ }
1198
+ }
1199
+ handleProgress(msg) {
1200
+ const pending = this.pendingRequests.get(msg.requestId);
1201
+ pending?.onProgress?.(msg.payload);
1202
+ }
1203
+ };
1204
+ // Annotate the CommonJS export names for ESM import in node:
1205
+ 0 && (module.exports = {
1206
+ BRIDGE_VERSION,
1207
+ BridgeError,
1208
+ HttpBridgeError,
1209
+ VibeAppBridge
1210
+ });
1211
+ //# sourceMappingURL=index.browser.cjs.map