@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.
package/dist/index.js CHANGED
@@ -1,368 +1,15 @@
1
- // package.json
2
- var version = "0.5.0";
3
-
4
- // ../protocol-types/dist/index.js
5
- function normalizeSubroute(subroute) {
6
- const normalized = subroute.startsWith("/") ? subroute : `/${subroute}`;
7
- const path = normalized.split(/[?#]/)[0];
8
- let decoded = path;
9
- let prev;
10
- do {
11
- prev = decoded;
12
- decoded = decoded.replace(/%25/gi, "%").replace(/%2e/gi, ".").replace(/%2f/gi, "/");
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 = 2;
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;
@@ -389,13 +36,23 @@ function resolveRequestTimeout(type, requestTimeout) {
389
36
 
390
37
  // src/VibeAppBridge.ts
391
38
  function resolvePosthog(mod) {
392
- let candidate = mod;
393
- while (candidate && typeof candidate.init !== "function" && candidate.default) {
394
- candidate = candidate.default;
395
- }
396
- return candidate;
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);
397
55
  }
398
- var posthog = resolvePosthog(posthogDefault);
399
56
  var PARENT_ORIGIN_OVERRIDE_KEY = "__NOM_VIBE_PARENT_ORIGIN__";
400
57
  var PREVIEW_HOST_RE = /(?:^|\.)(?:vercel\.app|lovable\.app|lovable\.dev|lovableproject\.com)$|^(?:localhost|127\.0\.0\.1)$/;
401
58
  function isPreviewSelfHost(hostname) {
@@ -455,9 +112,22 @@ function resolveOriginPolicy(configured) {
455
112
  return { targetOrigin, isTrusted };
456
113
  }
457
114
  var VibeAppBridge = class extends BridgeDataMethods {
458
- constructor({ parentOrigin, requestTimeout } = {}) {
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 = {}) {
459
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;
460
128
  this.pendingRequests = /* @__PURE__ */ new Map();
129
+ /** Whether {@link attach} has installed the message listener + resolved origin. */
130
+ this.attached = false;
461
131
  this.context = null;
462
132
  /**
463
133
  * Set once {@link initPostHog} has successfully called `posthog.init()`.
@@ -465,6 +135,16 @@ var VibeAppBridge = class extends BridgeDataMethods {
465
135
  * recording is actually live.
466
136
  */
467
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;
468
148
  this.connectPromise = null;
469
149
  this.connectPollInterval = null;
470
150
  this.connectTimeout = null;
@@ -505,8 +185,12 @@ var VibeAppBridge = class extends BridgeDataMethods {
505
185
  },
506
186
  // Persistent (not nulled after connect): the host delivers PostHog once at
507
187
  // connect via its own push, and initPostHog is idempotent, so this can run
508
- // whenever the message arrives, independent of how connect() resolved.
509
- POSTHOG_PUSH: (payload) => this.initPostHog(payload),
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
+ },
510
194
  // Persistent: the host pushes auth changes (logout, identity/tenant
511
195
  // switch) at any time after connect.
512
196
  AUTH_CHANGED: (payload) => this.authChangeCallback?.(payload),
@@ -536,42 +220,72 @@ var VibeAppBridge = class extends BridgeDataMethods {
536
220
  this.ui = {
537
221
  /** Collapse or expand the host's side navigation. */
538
222
  setSideNav: (payload) => {
223
+ assertBrowser("ui.setSideNav()");
539
224
  this.sendCommand("SET_SIDE_NAV", payload);
540
225
  },
541
226
  /** Switch the host to a different subsidiary. */
542
227
  switchSubsidiary: (subsidiaryId) => {
228
+ assertBrowser("ui.switchSubsidiary()");
543
229
  this.sendCommand("SWITCH_SUBSIDIARY", { subsidiaryId });
544
230
  }
545
231
  };
546
- const policy = resolveOriginPolicy(parentOrigin);
547
- this.targetOrigin = policy.targetOrigin;
548
- this.isTrustedOrigin = policy.isTrusted;
549
- this.requestTimeout = requestTimeout;
232
+ this.parentOriginOption = options.parentOrigin;
233
+ this.requestTimeout = options.requestTimeout;
550
234
  this.boundHandleMessage = this.handleMessage.bind(this);
551
- window.addEventListener("message", this.boundHandleMessage);
552
235
  }
553
236
  dispatchPush(msg) {
554
237
  const handler = this.pushHandlers[msg.type];
555
238
  handler(msg.payload);
556
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
+ }
557
268
  /**
558
269
  * Connects to the host and returns the tenant/user {@link ContextPayload}.
559
270
  * **Call this once on app init and await it before any other method** — data
560
271
  * methods, `upload()`, and subroute reporting all require an established
561
272
  * connection. Concurrent calls return the same promise.
562
273
  *
563
- * Sends a `CONNECT` handshake, re-sending every 500ms until the host replies
564
- * by pushing the context (so it works even if the host mounts after the
565
- * iframe loads). Context is only ever delivered by push. Rejects with `Bridge
566
- * connect timed out` after 10 seconds if no context arrives (usually a
567
- * `parentOrigin` mismatch or the host hasn't mounted).
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).
568
280
  *
569
281
  * After connecting, if the context includes an initial subroute (e.g. from
570
282
  * a deep link), the bridge navigates to it and starts auto-tracking
571
283
  * internal navigation via the history API.
572
284
  *
573
285
  * @returns The tenant/user/subsidiary context for this app session.
574
- * @throws If no context is received within 10 seconds.
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.
575
289
  * @example
576
290
  * ```ts
577
291
  * const ctx = await bridge.connect()
@@ -579,8 +293,10 @@ var VibeAppBridge = class extends BridgeDataMethods {
579
293
  * ```
580
294
  */
581
295
  connect() {
296
+ if (typeof window === "undefined") return Promise.reject(serverEnvError("connect()"));
582
297
  if (this.context) return Promise.resolve(this.context);
583
298
  if (this.connectPromise) return this.connectPromise;
299
+ this.attach();
584
300
  if (this.targetOrigin === null) {
585
301
  return Promise.reject(
586
302
  new BridgeError(
@@ -614,7 +330,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
614
330
  this.onContextReceived = null;
615
331
  this.rejectConnect = null;
616
332
  console.log(
617
- `[vibe-bridge] Connected \u2014 bridge: ${version}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
333
+ `[vibe-bridge] Connected \u2014 bridge: ${BRIDGE_VERSION}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
618
334
  );
619
335
  const initialSubroute = ctx.subroute ? normalizeSubroute(ctx.subroute) : null;
620
336
  if (initialSubroute && initialSubroute !== "/") {
@@ -631,7 +347,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
631
347
  resolve(ctx);
632
348
  };
633
349
  const sendConnect = () => {
634
- this.sendCommand("CONNECT", { bridgeVersion: version });
350
+ this.sendCommand("CONNECT", { bridgeVersion: BRIDGE_VERSION });
635
351
  };
636
352
  sendConnect();
637
353
  this.connectPollInterval = setInterval(sendConnect, CONNECT_POLL_MS);
@@ -644,6 +360,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
644
360
  * Use this for hash-based routers or non-standard navigation patterns.
645
361
  */
646
362
  reportSubroute(subroute, options) {
363
+ assertBrowser("reportSubroute()");
647
364
  const normalized = normalizeSubroute(subroute);
648
365
  if (normalized === null) return;
649
366
  this.lastReportedSubroute = normalized;
@@ -654,6 +371,8 @@ var VibeAppBridge = class extends BridgeDataMethods {
654
371
  * If no callback is registered, the SDK uses `history.pushState` + a
655
372
  * `popstate` event, which works for most SPA routers automatically.
656
373
  * Returns an unsubscribe function.
374
+ *
375
+ * Pure — only stores the callback, so it is safe to call before {@link connect}.
657
376
  */
658
377
  onSubrouteRequest(callback) {
659
378
  this.subrouteCallback = callback;
@@ -668,6 +387,8 @@ var VibeAppBridge = class extends BridgeDataMethods {
668
387
  * returns. Returns an unsubscribe function. Single subscriber: a second call
669
388
  * replaces the first. Wire it **before {@link connect}** (the bridge warns at
670
389
  * connect if it isn't).
390
+ *
391
+ * Pure — only stores the callback, so it is safe to call before {@link connect}.
671
392
  */
672
393
  onContextChange(callback) {
673
394
  this.contextChangeCallback = callback;
@@ -684,7 +405,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
684
405
  *
685
406
  * **Wire this before {@link connect}** — without it the embedded app won't
686
407
  * sign out when the user logs out of Nominal (the bridge warns at connect if
687
- * it isn't wired).
408
+ * it isn't wired). Pure — only stores the callback.
688
409
  */
689
410
  onAuthChange(callback) {
690
411
  this.authChangeCallback = callback;
@@ -730,6 +451,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
730
451
  * ```
731
452
  */
732
453
  navigate(path, action = "push") {
454
+ assertBrowser("navigate()");
733
455
  this.sendCommand("NAVIGATE", { path, action });
734
456
  }
735
457
  /**
@@ -743,10 +465,12 @@ var VibeAppBridge = class extends BridgeDataMethods {
743
465
  * ```
744
466
  */
745
467
  navigateTo(route, routeParams, action = "push") {
468
+ assertBrowser("navigateTo()");
746
469
  this.sendCommand("NAVIGATE", { route, routeParams, action });
747
470
  }
748
471
  /** Refreshes the host's current route (re-fetches its server data). Fire-and-forget. */
749
472
  refresh() {
473
+ assertBrowser("refresh()");
750
474
  this.sendCommand("NAVIGATE", { action: "refresh" });
751
475
  }
752
476
  /**
@@ -768,8 +492,9 @@ var VibeAppBridge = class extends BridgeDataMethods {
768
492
  *
769
493
  * **No-op until {@link connect} has resolved with PostHog enabled by the
770
494
  * host** — if the host didn't send a `posthog` block (older host, or recording
771
- * disabled for this env/app), nothing is sent. App authors don't import
772
- * `posthog-js` themselves; this is the supported event surface.
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.
773
498
  *
774
499
  * The event is sent **directly** from the iframe's own PostHog instance (only
775
500
  * the session *recording* is forwarded to the parent), so it carries the
@@ -786,20 +511,33 @@ var VibeAppBridge = class extends BridgeDataMethods {
786
511
  * ```
787
512
  */
788
513
  track(event, properties) {
789
- if (!this.posthogInitialized) return;
790
- posthog.capture(event, properties);
514
+ assertBrowser("track()");
515
+ if (!this.posthogInitialized || !this.posthogInstance) return;
516
+ this.posthogInstance.capture(event, properties);
791
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
+ */
792
525
  destroy() {
793
526
  this.teardownNavigationTracking();
794
527
  this.rejectConnect?.(new BridgeError("DESTROYED", "Bridge destroyed"));
795
528
  this.stopConnectPolling();
796
- window.removeEventListener("message", this.boundHandleMessage);
529
+ if (this.attached && typeof window !== "undefined") {
530
+ window.removeEventListener("message", this.boundHandleMessage);
531
+ }
532
+ this.attached = false;
797
533
  for (const pending of this.pendingRequests.values()) {
798
534
  pending.reject(new BridgeError("DESTROYED", "Bridge destroyed"));
799
535
  }
800
536
  this.pendingRequests.clear();
801
537
  this.context = null;
802
538
  this.posthogInitialized = false;
539
+ this.posthogInstance = null;
540
+ this.posthogIniting = false;
803
541
  this.connectPromise = null;
804
542
  this.onContextReceived = null;
805
543
  this.rejectConnect = null;
@@ -818,23 +556,34 @@ var VibeAppBridge = class extends BridgeDataMethods {
818
556
  this.connectTimeout = null;
819
557
  }
820
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
+ );
565
+ }
821
566
  /**
822
567
  * Initializes PostHog session recording + analytics from the host-supplied
823
568
  * config carried by a one-time `POSTHOG_PUSH`, **only when the host opted in**
824
569
  * (`config.enabled`).
825
570
  *
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
+ *
826
576
  * The host is the single source of truth: when it sends no `POSTHOG_PUSH`
827
577
  * (recording disabled for this env/app) or `enabled: false`, this does
828
- * nothing — `enabled` is the kill-switch even though `posthog-js` is bundled
829
- * into the bridge. Cross-origin replay needs `recordCrossOriginIframes` on both
830
- * sides so the parent receives the iframe's rrweb frames into one stitched
831
- * recording, and `identify()` ties the iframe's recording + events to the same
832
- * person as the host. Idempotent (the `posthogInitialized` guard) and wrapped
833
- * in try/catch so a duplicate push or a PostHog failure is harmless.
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.
834
583
  */
835
- initPostHog(config) {
584
+ async initPostHog(config) {
836
585
  if (!config.enabled) return;
837
- if (this.posthogInitialized) return;
586
+ if (this.posthogInitialized || this.posthogIniting) return;
838
587
  if (typeof window === "undefined") return;
839
588
  if (!config.token || !config.apiHost || !config.distinctId) {
840
589
  console.warn(
@@ -842,7 +591,20 @@ var VibeAppBridge = class extends BridgeDataMethods {
842
591
  );
843
592
  return;
844
593
  }
594
+ this.posthogIniting = true;
845
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;
846
608
  posthog.init(config.token, {
847
609
  api_host: config.apiHost,
848
610
  // absolute ingestion host, NOT a relative /ingest proxy
@@ -865,16 +627,19 @@ var VibeAppBridge = class extends BridgeDataMethods {
865
627
  }
866
628
  });
867
629
  posthog.identify(config.distinctId);
630
+ this.posthogInstance = posthog;
868
631
  this.posthogInitialized = true;
869
632
  console.log("[vibe-bridge] PostHog session recording initialized");
870
633
  } catch (err) {
871
634
  console.warn("[vibe-bridge] PostHog init failed \u2014 continuing without recording.", err);
635
+ } finally {
636
+ this.posthogIniting = false;
872
637
  }
873
638
  }
874
639
  /**
875
640
  * Monkey-patches `history.pushState` and `history.replaceState` to
876
641
  * auto-detect SPA navigation and report it to the host. Called after
877
- * connect() resolves.
642
+ * connect() resolves (so `window`/`history` are guaranteed present).
878
643
  */
879
644
  setupNavigationTracking() {
880
645
  if (this.origPushState) return;
@@ -925,6 +690,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
925
690
  this.sendCommand("REPORT_SUBROUTE", { subroute, replace });
926
691
  }
927
692
  sendCommand(type, payload) {
693
+ this.ensureAttached();
928
694
  this.postToParent({
929
695
  __protocol: PROTOCOL_ID,
930
696
  __version: PROTOCOL_VERSION,
@@ -940,12 +706,18 @@ var VibeAppBridge = class extends BridgeDataMethods {
940
706
  * `onProgress` callback. `type` autocompletes to every operation name and
941
707
  * narrows `payload`/return to that operation's types.
942
708
  *
709
+ * @throws Rejects with `BridgeError` code `'SERVER_ENVIRONMENT'` if called
710
+ * where there is no `window`.
943
711
  * @example
944
712
  * ```ts
945
713
  * const accounts = await bridge.request('GET_ACCOUNTS', { query: { account_ids: ['acc-1'] } })
946
714
  * ```
947
715
  */
948
716
  request(type, payload, onProgress) {
717
+ if (typeof window === "undefined") {
718
+ return Promise.reject(serverEnvError(`request(${String(type)})`));
719
+ }
720
+ this.ensureAttached();
949
721
  return new Promise((resolve, reject) => {
950
722
  const requestId = crypto.randomUUID();
951
723
  const timer = setTimeout(
@@ -1011,8 +783,9 @@ var VibeAppBridge = class extends BridgeDataMethods {
1011
783
  }
1012
784
  };
1013
785
  export {
1014
- version as BRIDGE_VERSION,
786
+ BRIDGE_VERSION,
1015
787
  BridgeError,
1016
788
  HttpBridgeError,
1017
789
  VibeAppBridge
1018
790
  };
791
+ //# sourceMappingURL=index.js.map