@nominalso/vibe-bridge 0.2.0 → 0.4.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,5 +1,5 @@
1
1
  // package.json
2
- var version = "0.2.0";
2
+ var version = "0.4.0";
3
3
 
4
4
  // ../protocol-types/dist/index.js
5
5
  function normalizeSubroute(subroute) {
@@ -44,6 +44,36 @@ var HttpBridgeError = class extends BridgeError {
44
44
  this.name = "HttpBridgeError";
45
45
  }
46
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";
47
77
 
48
78
  // src/data-methods.ts
49
79
  var BridgeDataMethods = class {
@@ -362,13 +392,86 @@ function resolveRequestTimeout(type, requestTimeout) {
362
392
  }
363
393
 
364
394
  // src/VibeAppBridge.ts
395
+ function resolvePosthog(mod) {
396
+ let candidate = mod;
397
+ while (candidate && typeof candidate.init !== "function" && candidate.default) {
398
+ candidate = candidate.default;
399
+ }
400
+ return candidate;
401
+ }
402
+ var posthog = resolvePosthog(posthogDefault);
403
+ var PARENT_ORIGIN_OVERRIDE_KEY = "__NOM_VIBE_PARENT_ORIGIN__";
404
+ var PREVIEW_HOST_RE = /(?:^|\.)(?:vercel\.app|lovable\.app|lovable\.dev|lovableproject\.com)$|^(?:localhost|127\.0\.0\.1)$/;
405
+ function isPreviewSelfHost(hostname) {
406
+ return PREVIEW_HOST_RE.test(hostname);
407
+ }
408
+ function resolveActualParentOrigin() {
409
+ if (typeof window === "undefined") return null;
410
+ try {
411
+ const ancestors = window.location.ancestorOrigins;
412
+ if (ancestors && ancestors.length > 0 && ancestors[0]) return ancestors[0];
413
+ } catch {
414
+ }
415
+ try {
416
+ if (document.referrer) return new URL(document.referrer).origin;
417
+ } catch {
418
+ }
419
+ return null;
420
+ }
421
+ function resolveOriginPolicy(configured) {
422
+ if (typeof window !== "undefined") {
423
+ const override = window[PARENT_ORIGIN_OVERRIDE_KEY];
424
+ if (typeof override === "string" && override.length > 0) {
425
+ console.log(`[vibe-bridge] Using dev-bridge parentOrigin override: ${override}`);
426
+ return { targetOrigin: override, isTrusted: (o) => o === override };
427
+ }
428
+ }
429
+ const rawList = configured === void 0 ? [] : Array.isArray(configured) ? configured : [configured];
430
+ const allowlist = rawList.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
431
+ if (allowlist.length === 0) {
432
+ const actual2 = resolveActualParentOrigin();
433
+ if (actual2) return { targetOrigin: actual2, isTrusted: (o) => o === actual2 };
434
+ console.warn(
435
+ "[vibe-bridge] No `parentOrigin` set and the embedding host could not be auto-detected (no ancestorOrigins/referrer). Pass `parentOrigin` explicitly."
436
+ );
437
+ return { targetOrigin: null, isTrusted: () => false };
438
+ }
439
+ const exactEntries = allowlist.filter((entry) => !isOriginPattern(entry));
440
+ const hasPatterns = exactEntries.length !== allowlist.length;
441
+ if (allowlist.length === 1 && !hasPatterns) {
442
+ const only = allowlist[0];
443
+ return { targetOrigin: only, isTrusted: (o) => o === only };
444
+ }
445
+ const allowPatterns = hasPatterns && typeof window !== "undefined" && isPreviewSelfHost(window.location.hostname);
446
+ const isTrusted = (o) => isOriginAllowed(o, allowlist, { allowPatterns });
447
+ const actual = resolveActualParentOrigin();
448
+ let targetOrigin = null;
449
+ if (actual && isTrusted(actual)) {
450
+ targetOrigin = actual;
451
+ } else if (actual === null && !allowPatterns && exactEntries.length === 1) {
452
+ targetOrigin = exactEntries[0];
453
+ }
454
+ if (targetOrigin === null) {
455
+ console.warn(
456
+ "[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." : ".")
457
+ );
458
+ }
459
+ return { targetOrigin, isTrusted };
460
+ }
365
461
  var VibeAppBridge = class extends BridgeDataMethods {
366
- constructor({ parentOrigin, requestTimeout }) {
462
+ constructor({ parentOrigin, requestTimeout } = {}) {
367
463
  super();
368
464
  this.pendingRequests = /* @__PURE__ */ new Map();
369
465
  this.context = null;
466
+ /**
467
+ * Set once {@link initPostHog} has successfully called `posthog.init()`.
468
+ * Guards against a double-init and makes {@link track} a no-op until
469
+ * recording is actually live.
470
+ */
471
+ this.posthogInitialized = false;
370
472
  this.connectPromise = null;
371
473
  this.connectPollInterval = null;
474
+ this.connectTimeout = null;
372
475
  this.onContextReceived = null;
373
476
  this.onContextError = null;
374
477
  /**
@@ -423,7 +526,9 @@ var VibeAppBridge = class extends BridgeDataMethods {
423
526
  this.sendCommand("SWITCH_SUBSIDIARY", { subsidiaryId });
424
527
  }
425
528
  };
426
- this.parentOrigin = parentOrigin;
529
+ const policy = resolveOriginPolicy(parentOrigin);
530
+ this.targetOrigin = policy.targetOrigin;
531
+ this.isTrustedOrigin = policy.isTrusted;
427
532
  this.requestTimeout = requestTimeout;
428
533
  this.boundHandleMessage = this.handleMessage.bind(this);
429
534
  window.addEventListener("message", this.boundHandleMessage);
@@ -459,6 +564,14 @@ var VibeAppBridge = class extends BridgeDataMethods {
459
564
  connect() {
460
565
  if (this.context) return Promise.resolve(this.context);
461
566
  if (this.connectPromise) return this.connectPromise;
567
+ if (this.targetOrigin === null) {
568
+ return Promise.reject(
569
+ new BridgeError(
570
+ "PARENT_ORIGIN_UNRESOLVED",
571
+ "Could not resolve a parent origin to connect to. Check `parentOrigin` against the embedding host."
572
+ )
573
+ );
574
+ }
462
575
  this.connectPromise = new Promise((resolve, reject) => {
463
576
  const timer = setTimeout(() => {
464
577
  this.stopConnectPolling();
@@ -467,6 +580,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
467
580
  this.onContextError = null;
468
581
  reject(new BridgeError("TIMEOUT", "Bridge connect timed out"));
469
582
  }, CONNECT_TIMEOUT_MS);
583
+ this.connectTimeout = timer;
470
584
  this.onContextError = (error) => {
471
585
  clearTimeout(timer);
472
586
  this.stopConnectPolling();
@@ -485,6 +599,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
485
599
  console.log(
486
600
  `[vibe-bridge] Connected \u2014 bridge: ${version}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
487
601
  );
602
+ this.initPostHog(ctx);
488
603
  const initialSubroute = ctx.subroute ? normalizeSubroute(ctx.subroute) : null;
489
604
  if (initialSubroute && initialSubroute !== "/") {
490
605
  this.withSubrouteSuppressed(() => {
@@ -509,7 +624,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
509
624
  requestId,
510
625
  payload: { bridgeVersion: version }
511
626
  };
512
- window.parent.postMessage(message, this.parentOrigin);
627
+ this.postToParent(message);
513
628
  };
514
629
  poll();
515
630
  this.connectPollInterval = setInterval(poll, CONNECT_POLL_MS);
@@ -589,8 +704,37 @@ var VibeAppBridge = class extends BridgeDataMethods {
589
704
  invalidateCache(payload) {
590
705
  return this.request("INVALIDATE_CACHE", payload);
591
706
  }
707
+ /**
708
+ * Captures a custom product event from inside the Vibe App, attributed to the
709
+ * same PostHog person as the host (via the `distinctId` the host supplied at
710
+ * connect).
711
+ *
712
+ * **No-op until {@link connect} has resolved with PostHog enabled by the
713
+ * host** — if the host didn't send a `posthog` block (older host, or recording
714
+ * disabled for this env/app), nothing is sent. App authors don't import
715
+ * `posthog-js` themselves; this is the supported event surface.
716
+ *
717
+ * The event is sent **directly** from the iframe's own PostHog instance (only
718
+ * the session *recording* is forwarded to the parent), so it carries the
719
+ * iframe instance's `$session_id` rather than the host's — events tie to the
720
+ * same person, but cross-boundary event↔replay timeline correlation is a known
721
+ * limitation.
722
+ *
723
+ * @param event Event name. Namespacing app events (e.g. `'vibe:<slug>:<action>'`)
724
+ * keeps them filterable from the host's own events.
725
+ * @param properties Optional event properties.
726
+ * @example
727
+ * ```ts
728
+ * bridge.track('vibe:fixed-assets:report_exported', { format: 'csv', rows: 128 })
729
+ * ```
730
+ */
731
+ track(event, properties) {
732
+ if (!this.posthogInitialized) return;
733
+ posthog.capture(event, properties);
734
+ }
592
735
  destroy() {
593
736
  this.teardownNavigationTracking();
737
+ this.onContextError?.(new BridgeError("DESTROYED", "Bridge destroyed"));
594
738
  this.stopConnectPolling();
595
739
  window.removeEventListener("message", this.boundHandleMessage);
596
740
  for (const pending of this.pendingRequests.values()) {
@@ -598,6 +742,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
598
742
  }
599
743
  this.pendingRequests.clear();
600
744
  this.context = null;
745
+ this.posthogInitialized = false;
601
746
  this.connectPromise = null;
602
747
  this.onContextReceived = null;
603
748
  this.onContextError = null;
@@ -609,8 +754,65 @@ var VibeAppBridge = class extends BridgeDataMethods {
609
754
  clearInterval(this.connectPollInterval);
610
755
  this.connectPollInterval = null;
611
756
  }
757
+ if (this.connectTimeout !== null) {
758
+ clearTimeout(this.connectTimeout);
759
+ this.connectTimeout = null;
760
+ }
612
761
  this.connectPollIds.clear();
613
762
  }
763
+ /**
764
+ * Initializes PostHog session recording + analytics from the host-supplied
765
+ * config, **only when the host opted in** (`ctx.posthog?.enabled`).
766
+ *
767
+ * The host is the single source of truth: when it sends no `posthog` block
768
+ * (older nom-ui, or recording disabled for this env/app) or `enabled: false`,
769
+ * this does nothing — `enabled` is the kill-switch even though `posthog-js` is
770
+ * bundled into the bridge. Cross-origin replay needs `recordCrossOriginIframes`
771
+ * on both sides so the parent receives the iframe's rrweb frames into one
772
+ * stitched recording, and `identify()` ties the iframe's recording + events to
773
+ * the same person as the host. Wrapped in try/catch so a PostHog failure is
774
+ * best-effort and never blocks `connect()`.
775
+ */
776
+ initPostHog(ctx) {
777
+ const config = ctx.posthog;
778
+ if (!config?.enabled) return;
779
+ if (this.posthogInitialized) return;
780
+ if (typeof window === "undefined") return;
781
+ if (!config.token || !config.apiHost || !config.distinctId) {
782
+ console.warn(
783
+ "[vibe-bridge] PostHog enabled but token/apiHost/distinctId missing \u2014 skipping init."
784
+ );
785
+ return;
786
+ }
787
+ try {
788
+ posthog.init(config.token, {
789
+ api_host: config.apiHost,
790
+ // absolute ingestion host, NOT a relative /ingest proxy
791
+ person_profiles: "identified_only",
792
+ capture_pageview: false,
793
+ // the host owns pageviews; don't double-count
794
+ autocapture: false,
795
+ // opt-in events only, via track()
796
+ // Seed the host's session id so track() events share the host's
797
+ // $session_id and line up with the stitched replay. Replay stitching
798
+ // itself does not depend on this (recordCrossOriginIframes handles it).
799
+ ...config.sessionId ? { bootstrap: { sessionID: config.sessionId } } : {},
800
+ session_recording: {
801
+ recordCrossOriginIframes: true,
802
+ // REQUIRED so the parent receives our rrweb frames
803
+ // Mirror nom-ui's posture (PostHogProvider.tsx): unmask inputs for
804
+ // richer insight, but keep password fields masked.
805
+ maskAllInputs: false,
806
+ maskInputOptions: { password: true }
807
+ }
808
+ });
809
+ posthog.identify(config.distinctId);
810
+ this.posthogInitialized = true;
811
+ console.log("[vibe-bridge] PostHog session recording initialized");
812
+ } catch (err) {
813
+ console.warn("[vibe-bridge] PostHog init failed \u2014 continuing without recording.", err);
814
+ }
815
+ }
614
816
  /**
615
817
  * Monkey-patches `history.pushState` and `history.replaceState` to
616
818
  * auto-detect SPA navigation and report it to the host. Called after
@@ -665,10 +867,13 @@ var VibeAppBridge = class extends BridgeDataMethods {
665
867
  this.sendCommand("REPORT_SUBROUTE", { subroute, replace });
666
868
  }
667
869
  sendCommand(type, payload) {
668
- window.parent.postMessage(
669
- { __protocol: PROTOCOL_ID, __version: PROTOCOL_VERSION, kind: "command", type, payload },
670
- this.parentOrigin
671
- );
870
+ this.postToParent({
871
+ __protocol: PROTOCOL_ID,
872
+ __version: PROTOCOL_VERSION,
873
+ kind: "command",
874
+ type,
875
+ payload
876
+ });
672
877
  }
673
878
  /**
674
879
  * Low-level typed request for **any** operation in {@link RequestRegistry}.
@@ -711,11 +916,20 @@ var VibeAppBridge = class extends BridgeDataMethods {
711
916
  requestId,
712
917
  payload
713
918
  };
714
- window.parent.postMessage(message, this.parentOrigin);
919
+ this.postToParent(message);
715
920
  });
716
921
  }
922
+ /**
923
+ * Posts to the embedding host. No-op when no concrete target origin could be
924
+ * resolved (a config/embedding error) — `connect()` rejects fast in that case
925
+ * so calls never silently hang waiting on a timeout.
926
+ */
927
+ postToParent(message) {
928
+ if (this.targetOrigin === null) return;
929
+ window.parent.postMessage(message, this.targetOrigin);
930
+ }
717
931
  handleMessage(event) {
718
- if (event.origin !== this.parentOrigin) return;
932
+ if (!this.isTrustedOrigin(event.origin)) return;
719
933
  if (!isBridgeMessage(event.data)) return;
720
934
  this.kindHandlers[event.data.kind]?.(event.data);
721
935
  }
@@ -20,10 +20,20 @@ interface ContextPayload {
20
20
  user: { id: string; displayName: string }
21
21
  subroute?: string // initial deep-link path, if any
22
22
  lastClosedPeriodSlug?: string // e.g. "mar-2026"
23
+ posthog?: {
24
+ // present only when the host enables in-iframe recording
25
+ token: string
26
+ apiHost: string // absolute ingestion host, e.g. "https://us.i.posthog.com"
27
+ distinctId: string // host-resolved person id (so events tie to the same person)
28
+ sessionId?: string // host's posthog.get_session_id(); seeds bootstrap so track() events share the host session
29
+ enabled: boolean // master switch; false/absent → bridge initializes nothing
30
+ }
23
31
  hostVersion: string // version of @nominalso/vibe-host running in the host
24
32
  }
25
33
  ```
26
34
 
35
+ When the resolved context carries `posthog.enabled === true`, the bridge auto-initializes `posthog-js` for cross-origin session recording (stitched into the host's recording) and exposes `bridge.track(event, properties?)` for custom events — see [Session recording & custom events](../README.md#session-recording--custom-events). If the host omits the `posthog` block, recording stays off.
36
+
27
37
  ## How it works
28
38
 
29
39
  The host pushes context to the iframe on load, but the iframe's listener may not be ready when that push arrives. `connect()` solves the race:
@@ -34,7 +44,7 @@ The host pushes context to the iframe on load, but the iframe's listener may not
34
44
  4. The bridge begins auto-tracking SPA navigation (see below).
35
45
  5. If nothing arrives within **10 seconds**, `connect()` rejects with a `BridgeError` (code `'TIMEOUT'`, message `Bridge connect timed out`).
36
46
 
37
- A timeout almost always means a `parentOrigin` mismatch or the host never mounted.
47
+ A timeout almost always means a `parentOrigin` mismatch or the host never mounted. A separate, immediate rejection — `BridgeError` code `PARENT_ORIGIN_UNRESOLVED` — means no concrete parent origin could be resolved at all (e.g. a pattern-only `parentOrigin` on a non-preview host); fix `parentOrigin` rather than waiting on the timeout.
38
48
 
39
49
  ## Initial deep link
40
50
 
@@ -8,16 +8,14 @@
8
8
  npm install @nominalso/vibe-bridge
9
9
  ```
10
10
 
11
- No runtime dependencies. Ships ESM + CJS with self-contained TypeScript types.
11
+ Bundles `posthog-js` (its only runtime dependency) for host-enabled in-iframe session recording. Ships ESM + CJS with self-contained TypeScript types.
12
12
 
13
13
  ## Minimal app
14
14
 
15
15
  ```ts
16
16
  import { VibeAppBridge, type ContextPayload } from '@nominalso/vibe-bridge'
17
17
 
18
- const bridge = new VibeAppBridge({
19
- parentOrigin: import.meta.env.VITE_PARENT_ORIGIN,
20
- })
18
+ const bridge = new VibeAppBridge() // auto-detects the embedding Nominal host
21
19
 
22
20
  async function main() {
23
21
  const ctx: ContextPayload = await bridge.connect()
@@ -32,20 +30,24 @@ async function main() {
32
30
  main().catch(console.error)
33
31
  ```
34
32
 
35
- ## `parentOrigin`
33
+ ## `parentOrigin` (optional)
36
34
 
37
- `parentOrigin` is the **origin** (scheme + host + port, no path, no trailing slash) of the Nominal app that embeds your iframe. It must match exactly the bridge ignores messages from any other origin and only posts to this one.
35
+ By default you pass **nothing** the bridge auto-detects the Nominal app that embeds your iframe and talks only to it. This is safe because Nominal serves Vibe Apps behind a `frame-ancestors` CSP, so only nom-ui can frame the app. Most apps need nothing here.
38
36
 
39
- Drive it from an environment variable so the same code works locally and in production:
37
+ Pass `parentOrigin` to **pin the host origin** explicitly (defense in depth, or when running outside Nominal's edge). It's an **origin** — scheme + host + port, no path, no trailing slash. Drive it from an env var, or use a list / **glob patterns** for dynamic preview deployments:
40
38
 
41
- ```sh
42
- # .env (committed standalone/dev default)
43
- VITE_PARENT_ORIGIN=http://localhost:5173
39
+ ```ts
40
+ new VibeAppBridge({ parentOrigin: import.meta.env.VITE_PARENT_ORIGIN })
41
+ new VibeAppBridge({ parentOrigin: ['https://app.nominal.so', 'https://*.vercel.app'] })
42
+ ```
44
43
 
45
- # .env.local (git-ignored — when testing inside nom-ui locally)
44
+ ```sh
45
+ # .env.local — when pinning explicitly while testing inside nom-ui locally
46
46
  VITE_PARENT_ORIGIN=http://localhost:3000
47
47
  ```
48
48
 
49
+ A pattern's `*` matches exactly one DNS label or port (anchored, scheme literal). Patterns are honoured **only when the app's own page is served from a recognised preview host** (`*.vercel.app`, `*.lovable.app`, `localhost`, …); on a production custom domain they are ignored and only exact origins match. Keep at least one exact origin in the list.
50
+
49
51
  ## Next
50
52
 
51
53
  - [`connect-lifecycle.md`](./connect-lifecycle.md) — how `connect()` works and the connection lifecycle.
package/llms.txt CHANGED
@@ -1,13 +1,13 @@
1
1
  # @nominalso/vibe-bridge
2
2
 
3
- > Iframe-side TypeScript SDK for building Nominal Vibe Apps — standalone browser apps (often built with Lovable) embedded in the Nominal platform via a cross-origin iframe. Connects the app to its Nominal host over a typed postMessage protocol: read Nominal data, submit Close-Management task outputs, upload files, and keep deep-link routing in sync. Browser-only, zero runtime dependencies.
3
+ > Iframe-side TypeScript SDK for building Nominal Vibe Apps — standalone browser apps (often built with Lovable) embedded in the Nominal platform via a cross-origin iframe. Connects the app to its Nominal host over a typed postMessage protocol: read Nominal data, submit Close-Management task outputs, upload files, keep deep-link routing in sync, and (when the host enables it) record the in-iframe session to PostHog. Browser-only; bundles posthog-js as its only runtime dependency.
4
4
 
5
- The integration is always the same: construct `VibeAppBridge` with the host `parentOrigin`, `await bridge.connect()` once (it returns the tenant/user `ContextPayload`), then call any of the ~46 typed data methods, `upload(file, options)`, or `postTaskOutput(...)`. Tear down with `bridge.destroy()`. The only write path into Nominal is `postTaskOutput`.
5
+ The integration is always the same: `new VibeAppBridge()` (no config `parentOrigin` is optional and the bridge auto-detects the embedding Nominal host; pass it only to pin explicitly or to allow preview origins), `await bridge.connect()` once (it returns the tenant/user `ContextPayload`), then call any of the ~46 typed data methods, `upload(file, options)`, or `postTaskOutput(...)`. Tear down with `bridge.destroy()`. The only write path into Nominal is `postTaskOutput`. Session recording auto-initializes on `connect()` only when the host sends a `posthog` block with `enabled: true`; emit custom events with `bridge.track(event, properties?)` (a no-op until then).
6
6
 
7
7
  ## Docs
8
8
 
9
9
  - [README](./README.md): install, quickstart, full API, common mistakes, and the operation catalog.
10
- - [Getting started](./docs/getting-started.md): install + minimal app + `parentOrigin` setup.
10
+ - [Getting started](./docs/getting-started.md): install + minimal app + optional `parentOrigin` pinning.
11
11
  - [Connection lifecycle](./docs/connect-lifecycle.md): `connect()` semantics, `ContextPayload`, timeout, subroute sync, teardown.
12
12
  - [Fetching data](./docs/data-fetching.md): named methods vs the typed `request()` escape hatch, payload shapes, errors/timeouts, operation list.
13
13
  - [File upload](./docs/file-upload.md): `upload(file, options)`, progress events, constraints.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nominalso/vibe-bridge",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Iframe-side SDK for building Nominal Vibe Apps — connects an embedded app to its Nominal host over a typed postMessage bridge (context, data fetch, file upload, subroute deep-linking).",
5
5
  "license": "UNLICENSED",
6
6
  "homepage": "https://github.com/nominalso/vibe-apps-sdk#readme",
@@ -33,6 +33,9 @@
33
33
  "registry": "https://registry.npmjs.org/",
34
34
  "access": "public"
35
35
  },
36
+ "dependencies": {
37
+ "posthog-js": "^1.393.5"
38
+ },
36
39
  "devDependencies": {
37
40
  "@nominalso/vibe-api-types": "0.0.1",
38
41
  "@nominalso/vibe-protocol-types": "0.0.1"