@nominalso/vibe-bridge 0.4.0 → 0.5.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.cjs +109 -60
- package/dist/index.d.cts +86 -44
- package/dist/index.d.ts +86 -44
- package/dist/index.js +109 -60
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -38,7 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
module.exports = __toCommonJS(index_exports);
|
|
39
39
|
|
|
40
40
|
// package.json
|
|
41
|
-
var version = "0.
|
|
41
|
+
var version = "0.5.0";
|
|
42
42
|
|
|
43
43
|
// ../protocol-types/dist/index.js
|
|
44
44
|
function normalizeSubroute(subroute) {
|
|
@@ -54,7 +54,7 @@ function normalizeSubroute(subroute) {
|
|
|
54
54
|
return normalized;
|
|
55
55
|
}
|
|
56
56
|
var PROTOCOL_ID = "nominal-vibe-bridge";
|
|
57
|
-
var PROTOCOL_VERSION =
|
|
57
|
+
var PROTOCOL_VERSION = 2;
|
|
58
58
|
var BRIDGE_KINDS = ["request", "response", "push", "command", "progress"];
|
|
59
59
|
var CORRELATED_KINDS = ["request", "response", "progress"];
|
|
60
60
|
var BRIDGE_KINDS_SET = new Set(BRIDGE_KINDS);
|
|
@@ -409,8 +409,6 @@ var CONNECT_POLL_MS = 500;
|
|
|
409
409
|
var DEFAULT_TIMEOUTS = {
|
|
410
410
|
/** Typed Nominal API reads/writes (`GET_*` / `POST_*` / `CREATE_*` / ...). */
|
|
411
411
|
api: 3e4,
|
|
412
|
-
/** `GET_CONTEXT` re-fetch (the initial fetch is governed by `connect()`). */
|
|
413
|
-
context: 5e3,
|
|
414
412
|
/** `INVALIDATE_CACHE`. */
|
|
415
413
|
invalidate: 1e4,
|
|
416
414
|
/** `UPLOAD_FILE` — large payloads + presigned S3 PUT take a while. */
|
|
@@ -423,8 +421,6 @@ function resolveRequestTimeout(type, requestTimeout) {
|
|
|
423
421
|
return DEFAULT_TIMEOUTS.upload;
|
|
424
422
|
case "INVALIDATE_CACHE":
|
|
425
423
|
return DEFAULT_TIMEOUTS.invalidate;
|
|
426
|
-
case "GET_CONTEXT":
|
|
427
|
-
return DEFAULT_TIMEOUTS.context;
|
|
428
424
|
default:
|
|
429
425
|
return DEFAULT_TIMEOUTS.api;
|
|
430
426
|
}
|
|
@@ -512,13 +508,20 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
512
508
|
this.connectPollInterval = null;
|
|
513
509
|
this.connectTimeout = null;
|
|
514
510
|
this.onContextReceived = null;
|
|
515
|
-
|
|
511
|
+
/** Rejects an in-flight connect() (used by destroy()); `null` once settled. */
|
|
512
|
+
this.rejectConnect = null;
|
|
516
513
|
/**
|
|
517
|
-
*
|
|
518
|
-
*
|
|
519
|
-
*
|
|
514
|
+
* Persistent subscriber for post-connect context changes (tenant/subsidiary
|
|
515
|
+
* switch, period close, …). Distinct from {@link onContextReceived}, which only
|
|
516
|
+
* resolves the connect handshake and is cleared afterward. `null` until
|
|
517
|
+
* {@link onContextChange} is called.
|
|
520
518
|
*/
|
|
521
|
-
this.
|
|
519
|
+
this.contextChangeCallback = null;
|
|
520
|
+
/**
|
|
521
|
+
* Persistent subscriber for host auth changes (Nominal logout or
|
|
522
|
+
* identity/tenant switch). `null` until {@link onAuthChange} is called.
|
|
523
|
+
*/
|
|
524
|
+
this.authChangeCallback = null;
|
|
522
525
|
this.lastReportedSubroute = null;
|
|
523
526
|
this.suppressSubrouteReport = false;
|
|
524
527
|
this.subrouteCallback = null;
|
|
@@ -531,7 +534,21 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
531
534
|
progress: (msg) => this.handleProgress(msg)
|
|
532
535
|
};
|
|
533
536
|
this.pushHandlers = {
|
|
534
|
-
CONTEXT_PUSH: (payload) =>
|
|
537
|
+
CONTEXT_PUSH: (payload) => {
|
|
538
|
+
if (this.onContextReceived) {
|
|
539
|
+
this.onContextReceived(payload);
|
|
540
|
+
} else if (this.context) {
|
|
541
|
+
this.context = payload;
|
|
542
|
+
this.contextChangeCallback?.(payload);
|
|
543
|
+
}
|
|
544
|
+
},
|
|
545
|
+
// Persistent (not nulled after connect): the host delivers PostHog once at
|
|
546
|
+
// connect via its own push, and initPostHog is idempotent, so this can run
|
|
547
|
+
// whenever the message arrives, independent of how connect() resolved.
|
|
548
|
+
POSTHOG_PUSH: (payload) => this.initPostHog(payload),
|
|
549
|
+
// Persistent: the host pushes auth changes (logout, identity/tenant
|
|
550
|
+
// switch) at any time after connect.
|
|
551
|
+
AUTH_CHANGED: (payload) => this.authChangeCallback?.(payload),
|
|
535
552
|
SUBROUTE_PUSH: (payload) => {
|
|
536
553
|
const safe = normalizeSubroute(payload.subroute);
|
|
537
554
|
if (!safe) return;
|
|
@@ -582,11 +599,11 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
582
599
|
* methods, `upload()`, and subroute reporting all require an established
|
|
583
600
|
* connection. Concurrent calls return the same promise.
|
|
584
601
|
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
* after 10 seconds if no context arrives (usually a
|
|
589
|
-
* or the host hasn't mounted).
|
|
602
|
+
* Sends a `CONNECT` handshake, re-sending every 500ms until the host replies
|
|
603
|
+
* by pushing the context (so it works even if the host mounts after the
|
|
604
|
+
* iframe loads). Context is only ever delivered by push. Rejects with `Bridge
|
|
605
|
+
* connect timed out` after 10 seconds if no context arrives (usually a
|
|
606
|
+
* `parentOrigin` mismatch or the host hasn't mounted).
|
|
590
607
|
*
|
|
591
608
|
* After connecting, if the context includes an initial subroute (e.g. from
|
|
592
609
|
* a deep link), the bridge navigates to it and starts auto-tracking
|
|
@@ -616,16 +633,16 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
616
633
|
this.stopConnectPolling();
|
|
617
634
|
this.connectPromise = null;
|
|
618
635
|
this.onContextReceived = null;
|
|
619
|
-
this.
|
|
636
|
+
this.rejectConnect = null;
|
|
620
637
|
reject(new BridgeError("TIMEOUT", "Bridge connect timed out"));
|
|
621
638
|
}, CONNECT_TIMEOUT_MS);
|
|
622
639
|
this.connectTimeout = timer;
|
|
623
|
-
this.
|
|
640
|
+
this.rejectConnect = (error) => {
|
|
624
641
|
clearTimeout(timer);
|
|
625
642
|
this.stopConnectPolling();
|
|
626
643
|
this.connectPromise = null;
|
|
627
644
|
this.onContextReceived = null;
|
|
628
|
-
this.
|
|
645
|
+
this.rejectConnect = null;
|
|
629
646
|
reject(error);
|
|
630
647
|
};
|
|
631
648
|
this.onContextReceived = (ctx) => {
|
|
@@ -634,11 +651,10 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
634
651
|
this.context = ctx;
|
|
635
652
|
this.connectPromise = null;
|
|
636
653
|
this.onContextReceived = null;
|
|
637
|
-
this.
|
|
654
|
+
this.rejectConnect = null;
|
|
638
655
|
console.log(
|
|
639
656
|
`[vibe-bridge] Connected \u2014 bridge: ${version}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
640
657
|
);
|
|
641
|
-
this.initPostHog(ctx);
|
|
642
658
|
const initialSubroute = ctx.subroute ? normalizeSubroute(ctx.subroute) : null;
|
|
643
659
|
if (initialSubroute && initialSubroute !== "/") {
|
|
644
660
|
this.withSubrouteSuppressed(() => {
|
|
@@ -650,23 +666,14 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
650
666
|
this.lastReportedSubroute = window.location.pathname + window.location.search;
|
|
651
667
|
}
|
|
652
668
|
this.setupNavigationTracking();
|
|
669
|
+
this.warnMissingListeners();
|
|
653
670
|
resolve(ctx);
|
|
654
671
|
};
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
this.connectPollIds.add(requestId);
|
|
658
|
-
const message = {
|
|
659
|
-
__protocol: PROTOCOL_ID,
|
|
660
|
-
__version: PROTOCOL_VERSION,
|
|
661
|
-
kind: "request",
|
|
662
|
-
type: "GET_CONTEXT",
|
|
663
|
-
requestId,
|
|
664
|
-
payload: { bridgeVersion: version }
|
|
665
|
-
};
|
|
666
|
-
this.postToParent(message);
|
|
672
|
+
const sendConnect = () => {
|
|
673
|
+
this.sendCommand("CONNECT", { bridgeVersion: version });
|
|
667
674
|
};
|
|
668
|
-
|
|
669
|
-
this.connectPollInterval = setInterval(
|
|
675
|
+
sendConnect();
|
|
676
|
+
this.connectPollInterval = setInterval(sendConnect, CONNECT_POLL_MS);
|
|
670
677
|
});
|
|
671
678
|
return this.connectPromise;
|
|
672
679
|
}
|
|
@@ -693,6 +700,56 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
693
700
|
this.subrouteCallback = null;
|
|
694
701
|
};
|
|
695
702
|
}
|
|
703
|
+
/**
|
|
704
|
+
* Subscribes to post-connect context changes (tenant/subsidiary switch, period
|
|
705
|
+
* close, …). The callback fires for each {@link ContextPayload} the host pushes
|
|
706
|
+
* *after* connect — not for the initial context, which {@link connect} already
|
|
707
|
+
* returns. Returns an unsubscribe function. Single subscriber: a second call
|
|
708
|
+
* replaces the first. Wire it **before {@link connect}** (the bridge warns at
|
|
709
|
+
* connect if it isn't).
|
|
710
|
+
*/
|
|
711
|
+
onContextChange(callback) {
|
|
712
|
+
this.contextChangeCallback = callback;
|
|
713
|
+
return () => {
|
|
714
|
+
this.contextChangeCallback = null;
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Subscribes to host auth changes: a Nominal logout (`authenticated: false` —
|
|
719
|
+
* sign your backend out) or an identity/tenant switch (`authenticated: true`
|
|
720
|
+
* with a new `userId`/`tenant` — re-authenticate). Fires for each change the
|
|
721
|
+
* host pushes after connect. Returns an unsubscribe function. Single
|
|
722
|
+
* subscriber: a second call replaces the first.
|
|
723
|
+
*
|
|
724
|
+
* **Wire this before {@link connect}** — without it the embedded app won't
|
|
725
|
+
* sign out when the user logs out of Nominal (the bridge warns at connect if
|
|
726
|
+
* it isn't wired).
|
|
727
|
+
*/
|
|
728
|
+
onAuthChange(callback) {
|
|
729
|
+
this.authChangeCallback = callback;
|
|
730
|
+
return () => {
|
|
731
|
+
this.authChangeCallback = null;
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* At connect, nudge the app if it hasn't wired the change listeners: every
|
|
736
|
+
* embedded app should react to a Nominal logout ({@link onAuthChange}) and a
|
|
737
|
+
* tenant/subsidiary switch ({@link onContextChange}). Fires once during the
|
|
738
|
+
* connect flow (not speculatively later), so wire both **before** `connect()`.
|
|
739
|
+
* The skill/codegen does this by default; this catches hand-wired apps.
|
|
740
|
+
*/
|
|
741
|
+
warnMissingListeners() {
|
|
742
|
+
if (!this.authChangeCallback) {
|
|
743
|
+
console.warn(
|
|
744
|
+
"[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()."
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
if (!this.contextChangeCallback) {
|
|
748
|
+
console.warn(
|
|
749
|
+
"[vibe-bridge] connected without onContextChange \u2014 the app will not react to a tenant/subsidiary/period switch. Call bridge.onContextChange() before connect()."
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
696
753
|
// ---------------------------------------------------------------------------
|
|
697
754
|
// Host navigation & UI commands
|
|
698
755
|
//
|
|
@@ -773,7 +830,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
773
830
|
}
|
|
774
831
|
destroy() {
|
|
775
832
|
this.teardownNavigationTracking();
|
|
776
|
-
this.
|
|
833
|
+
this.rejectConnect?.(new BridgeError("DESTROYED", "Bridge destroyed"));
|
|
777
834
|
this.stopConnectPolling();
|
|
778
835
|
window.removeEventListener("message", this.boundHandleMessage);
|
|
779
836
|
for (const pending of this.pendingRequests.values()) {
|
|
@@ -784,7 +841,9 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
784
841
|
this.posthogInitialized = false;
|
|
785
842
|
this.connectPromise = null;
|
|
786
843
|
this.onContextReceived = null;
|
|
787
|
-
this.
|
|
844
|
+
this.rejectConnect = null;
|
|
845
|
+
this.contextChangeCallback = null;
|
|
846
|
+
this.authChangeCallback = null;
|
|
788
847
|
this.subrouteCallback = null;
|
|
789
848
|
this.lastReportedSubroute = null;
|
|
790
849
|
}
|
|
@@ -797,24 +856,23 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
797
856
|
clearTimeout(this.connectTimeout);
|
|
798
857
|
this.connectTimeout = null;
|
|
799
858
|
}
|
|
800
|
-
this.connectPollIds.clear();
|
|
801
859
|
}
|
|
802
860
|
/**
|
|
803
861
|
* Initializes PostHog session recording + analytics from the host-supplied
|
|
804
|
-
* config
|
|
862
|
+
* config carried by a one-time `POSTHOG_PUSH`, **only when the host opted in**
|
|
863
|
+
* (`config.enabled`).
|
|
805
864
|
*
|
|
806
|
-
* The host is the single source of truth: when it sends no `
|
|
807
|
-
* (
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
*
|
|
812
|
-
*
|
|
813
|
-
*
|
|
865
|
+
* The host is the single source of truth: when it sends no `POSTHOG_PUSH`
|
|
866
|
+
* (recording disabled for this env/app) or `enabled: false`, this does
|
|
867
|
+
* nothing — `enabled` is the kill-switch even though `posthog-js` is bundled
|
|
868
|
+
* into the bridge. Cross-origin replay needs `recordCrossOriginIframes` on both
|
|
869
|
+
* sides so the parent receives the iframe's rrweb frames into one stitched
|
|
870
|
+
* recording, and `identify()` ties the iframe's recording + events to the same
|
|
871
|
+
* person as the host. Idempotent (the `posthogInitialized` guard) and wrapped
|
|
872
|
+
* in try/catch so a duplicate push or a PostHog failure is harmless.
|
|
814
873
|
*/
|
|
815
|
-
initPostHog(
|
|
816
|
-
|
|
817
|
-
if (!config?.enabled) return;
|
|
874
|
+
initPostHog(config) {
|
|
875
|
+
if (!config.enabled) return;
|
|
818
876
|
if (this.posthogInitialized) return;
|
|
819
877
|
if (typeof window === "undefined") return;
|
|
820
878
|
if (!config.token || !config.apiHost || !config.distinctId) {
|
|
@@ -973,15 +1031,6 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
973
1031
|
this.kindHandlers[event.data.kind]?.(event.data);
|
|
974
1032
|
}
|
|
975
1033
|
handleResponse(msg) {
|
|
976
|
-
if (msg.type === "GET_CONTEXT") {
|
|
977
|
-
if (!this.connectPollIds.has(msg.requestId)) return;
|
|
978
|
-
if (msg.ok) {
|
|
979
|
-
this.onContextReceived?.(msg.data);
|
|
980
|
-
} else {
|
|
981
|
-
this.onContextError?.(new BridgeError(msg.code ?? "CONTEXT_ERROR", msg.error));
|
|
982
|
-
}
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
1034
|
const pending = this.pendingRequests.get(msg.requestId);
|
|
986
1035
|
if (!pending) return;
|
|
987
1036
|
this.pendingRequests.delete(msg.requestId);
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var version = "0.
|
|
1
|
+
var version = "0.5.0";
|
|
2
2
|
|
|
3
3
|
type AccountingAccountDimensionValues = {
|
|
4
4
|
account_id: string;
|
|
@@ -1392,6 +1392,7 @@ type ActivityProviderOperationErrorType = 'Connection Error' | 'Authentication E
|
|
|
1392
1392
|
type ActivityReconciliationDefinitionData = {
|
|
1393
1393
|
type: ActivityReconciliationType;
|
|
1394
1394
|
task_definition_id: string;
|
|
1395
|
+
currency?: ActivityCurrency | null;
|
|
1395
1396
|
side_a_entity_type: ActivityReconciliationEntityType;
|
|
1396
1397
|
side_a_entity_id: string;
|
|
1397
1398
|
side_b_entity_type: ActivityReconciliationEntityType;
|
|
@@ -1405,7 +1406,6 @@ type ActivityReconciliationInstanceDataInput = {
|
|
|
1405
1406
|
task_instance_id: string;
|
|
1406
1407
|
start_date: string;
|
|
1407
1408
|
end_date: string;
|
|
1408
|
-
currency?: ActivityCurrency | null;
|
|
1409
1409
|
side_a_value?: number | string | null;
|
|
1410
1410
|
side_b_value?: number | string | null;
|
|
1411
1411
|
reconciling_items_balance?: number | string | null;
|
|
@@ -1419,7 +1419,6 @@ type ActivityReconciliationInstanceDataOutput = {
|
|
|
1419
1419
|
task_instance_id: string;
|
|
1420
1420
|
start_date: string;
|
|
1421
1421
|
end_date: string;
|
|
1422
|
-
currency?: ActivityCurrency | null;
|
|
1423
1422
|
side_a_value?: string | null;
|
|
1424
1423
|
side_b_value?: string | null;
|
|
1425
1424
|
reconciling_items_balance?: string | null;
|
|
@@ -3190,9 +3189,10 @@ interface BridgeSubsidiary {
|
|
|
3190
3189
|
*
|
|
3191
3190
|
* The host (nom-ui) is the single source of truth for these values — it knows
|
|
3192
3191
|
* the project key, the env-aware ingestion host, and the resolved person — and
|
|
3193
|
-
* delivers
|
|
3194
|
-
*
|
|
3195
|
-
*
|
|
3192
|
+
* delivers it over the bridge (as a one-time `POSTHOG_PUSH`, separate from
|
|
3193
|
+
* {@link ContextPayload}) so the iframe never hardcodes any of it. When the host
|
|
3194
|
+
* has no PostHog config (recording disabled for this env/app), it sends no
|
|
3195
|
+
* `POSTHOG_PUSH` and the bridge initializes nothing.
|
|
3196
3196
|
*/
|
|
3197
3197
|
interface PostHogContext {
|
|
3198
3198
|
/** Project API key — the same PostHog project the host (nom-ui) uses. */
|
|
@@ -3230,6 +3230,27 @@ interface PostHogContext {
|
|
|
3230
3230
|
*/
|
|
3231
3231
|
enabled: boolean;
|
|
3232
3232
|
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Authentication-state change the host pushes to the iframe via `AUTH_CHANGED`.
|
|
3235
|
+
* It carries the security principal the embedded app authenticates and scopes
|
|
3236
|
+
* its own data by. Sent when the Nominal session changes: a logout
|
|
3237
|
+
* (`authenticated: false`) or an identity/tenant switch (`authenticated: true`
|
|
3238
|
+
* with a new `userId`/`tenant`). The initial state is implicit at connect — the
|
|
3239
|
+
* iframe only loads when authenticated, and the current identity/tenant are
|
|
3240
|
+
* {@link ContextPayload.user} / {@link ContextPayload.tenant}, so apps baseline
|
|
3241
|
+
* off those and react to this push.
|
|
3242
|
+
*
|
|
3243
|
+
* Subsidiary/period and other view-level state are NOT here — they ride the
|
|
3244
|
+
* follow-up `CONTEXT_PUSH` (`onContextChange`), per "auth change, then context".
|
|
3245
|
+
*/
|
|
3246
|
+
interface AuthPayload {
|
|
3247
|
+
/** Whether the Nominal user is still authenticated. */
|
|
3248
|
+
authenticated: boolean;
|
|
3249
|
+
/** The effective Nominal user id when authenticated; omitted on logout. */
|
|
3250
|
+
userId?: string;
|
|
3251
|
+
/** The tenant the user is acting in — the app's data-scoping key (RLS). */
|
|
3252
|
+
tenant?: string;
|
|
3253
|
+
}
|
|
3233
3254
|
interface ContextPayload {
|
|
3234
3255
|
tenant: string;
|
|
3235
3256
|
subsidiaryId: number;
|
|
@@ -3242,21 +3263,9 @@ interface ContextPayload {
|
|
|
3242
3263
|
subroute?: string;
|
|
3243
3264
|
/** Slug of the last closed accounting period, e.g. "mar-2026". */
|
|
3244
3265
|
lastClosedPeriodSlug?: string;
|
|
3245
|
-
/**
|
|
3246
|
-
* PostHog config for in-iframe session recording + custom events. **Absent
|
|
3247
|
-
* when recording is off** (older host, or disabled for this env/app), in which
|
|
3248
|
-
* case the bridge initializes no analytics. Optional so old hosts/bridges stay
|
|
3249
|
-
* compatible.
|
|
3250
|
-
*/
|
|
3251
|
-
posthog?: PostHogContext;
|
|
3252
3266
|
/** Version of the host SDK (@nominalso/vibe-host) running in nom-ui. */
|
|
3253
3267
|
hostVersion: string;
|
|
3254
3268
|
}
|
|
3255
|
-
/** Payload sent by the bridge when requesting context from the host. */
|
|
3256
|
-
interface GetContextPayload {
|
|
3257
|
-
/** Version of the bridge SDK (@nominalso/vibe-bridge) running in the iframe. */
|
|
3258
|
-
bridgeVersion: string;
|
|
3259
|
-
}
|
|
3260
3269
|
|
|
3261
3270
|
interface UploadPayload {
|
|
3262
3271
|
buffer: ArrayBuffer;
|
|
@@ -3490,10 +3499,6 @@ interface RequestRegistry {
|
|
|
3490
3499
|
payload: InvalidateCachePayload;
|
|
3491
3500
|
data: InvalidateResult;
|
|
3492
3501
|
};
|
|
3493
|
-
GET_CONTEXT: {
|
|
3494
|
-
payload: GetContextPayload;
|
|
3495
|
-
data: ContextPayload;
|
|
3496
|
-
};
|
|
3497
3502
|
UPLOAD_FILE: {
|
|
3498
3503
|
payload: UploadPayload;
|
|
3499
3504
|
data: UploadResponse;
|
|
@@ -3748,9 +3753,9 @@ interface VibeAppBridgeOptions {
|
|
|
3748
3753
|
*
|
|
3749
3754
|
* When omitted, each operation uses an operation-specific default tuned to
|
|
3750
3755
|
* how long it realistically takes (API reads `30000`, `UPLOAD_FILE`
|
|
3751
|
-
* `120000`, `INVALIDATE_CACHE` `10000
|
|
3752
|
-
*
|
|
3753
|
-
*
|
|
3756
|
+
* `120000`, `INVALIDATE_CACHE` `10000`). Setting this overrides all of them
|
|
3757
|
+
* with a single value — only do that if you have a specific reason; the
|
|
3758
|
+
* defaults already match the host's expectations.
|
|
3754
3759
|
*/
|
|
3755
3760
|
requestTimeout?: number;
|
|
3756
3761
|
}
|
|
@@ -3796,13 +3801,20 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3796
3801
|
private connectPollInterval;
|
|
3797
3802
|
private connectTimeout;
|
|
3798
3803
|
private onContextReceived;
|
|
3799
|
-
|
|
3804
|
+
/** Rejects an in-flight connect() (used by destroy()); `null` once settled. */
|
|
3805
|
+
private rejectConnect;
|
|
3806
|
+
/**
|
|
3807
|
+
* Persistent subscriber for post-connect context changes (tenant/subsidiary
|
|
3808
|
+
* switch, period close, …). Distinct from {@link onContextReceived}, which only
|
|
3809
|
+
* resolves the connect handshake and is cleared afterward. `null` until
|
|
3810
|
+
* {@link onContextChange} is called.
|
|
3811
|
+
*/
|
|
3812
|
+
private contextChangeCallback;
|
|
3800
3813
|
/**
|
|
3801
|
-
*
|
|
3802
|
-
*
|
|
3803
|
-
* resolve/reject the new one (a stale error could reject a healthy reconnect).
|
|
3814
|
+
* Persistent subscriber for host auth changes (Nominal logout or
|
|
3815
|
+
* identity/tenant switch). `null` until {@link onAuthChange} is called.
|
|
3804
3816
|
*/
|
|
3805
|
-
private
|
|
3817
|
+
private authChangeCallback;
|
|
3806
3818
|
private lastReportedSubroute;
|
|
3807
3819
|
private suppressSubrouteReport;
|
|
3808
3820
|
private subrouteCallback;
|
|
@@ -3819,11 +3831,11 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3819
3831
|
* methods, `upload()`, and subroute reporting all require an established
|
|
3820
3832
|
* connection. Concurrent calls return the same promise.
|
|
3821
3833
|
*
|
|
3822
|
-
*
|
|
3823
|
-
*
|
|
3824
|
-
*
|
|
3825
|
-
* after 10 seconds if no context arrives (usually a
|
|
3826
|
-
* or the host hasn't mounted).
|
|
3834
|
+
* Sends a `CONNECT` handshake, re-sending every 500ms until the host replies
|
|
3835
|
+
* by pushing the context (so it works even if the host mounts after the
|
|
3836
|
+
* iframe loads). Context is only ever delivered by push. Rejects with `Bridge
|
|
3837
|
+
* connect timed out` after 10 seconds if no context arrives (usually a
|
|
3838
|
+
* `parentOrigin` mismatch or the host hasn't mounted).
|
|
3827
3839
|
*
|
|
3828
3840
|
* After connecting, if the context includes an initial subroute (e.g. from
|
|
3829
3841
|
* a deep link), the bridge navigates to it and starts auto-tracking
|
|
@@ -3853,6 +3865,35 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3853
3865
|
* Returns an unsubscribe function.
|
|
3854
3866
|
*/
|
|
3855
3867
|
onSubrouteRequest(callback: (subroute: string) => void): () => void;
|
|
3868
|
+
/**
|
|
3869
|
+
* Subscribes to post-connect context changes (tenant/subsidiary switch, period
|
|
3870
|
+
* close, …). The callback fires for each {@link ContextPayload} the host pushes
|
|
3871
|
+
* *after* connect — not for the initial context, which {@link connect} already
|
|
3872
|
+
* returns. Returns an unsubscribe function. Single subscriber: a second call
|
|
3873
|
+
* replaces the first. Wire it **before {@link connect}** (the bridge warns at
|
|
3874
|
+
* connect if it isn't).
|
|
3875
|
+
*/
|
|
3876
|
+
onContextChange(callback: (ctx: ContextPayload) => void): () => void;
|
|
3877
|
+
/**
|
|
3878
|
+
* Subscribes to host auth changes: a Nominal logout (`authenticated: false` —
|
|
3879
|
+
* sign your backend out) or an identity/tenant switch (`authenticated: true`
|
|
3880
|
+
* with a new `userId`/`tenant` — re-authenticate). Fires for each change the
|
|
3881
|
+
* host pushes after connect. Returns an unsubscribe function. Single
|
|
3882
|
+
* subscriber: a second call replaces the first.
|
|
3883
|
+
*
|
|
3884
|
+
* **Wire this before {@link connect}** — without it the embedded app won't
|
|
3885
|
+
* sign out when the user logs out of Nominal (the bridge warns at connect if
|
|
3886
|
+
* it isn't wired).
|
|
3887
|
+
*/
|
|
3888
|
+
onAuthChange(callback: (auth: AuthPayload) => void): () => void;
|
|
3889
|
+
/**
|
|
3890
|
+
* At connect, nudge the app if it hasn't wired the change listeners: every
|
|
3891
|
+
* embedded app should react to a Nominal logout ({@link onAuthChange}) and a
|
|
3892
|
+
* tenant/subsidiary switch ({@link onContextChange}). Fires once during the
|
|
3893
|
+
* connect flow (not speculatively later), so wire both **before** `connect()`.
|
|
3894
|
+
* The skill/codegen does this by default; this catches hand-wired apps.
|
|
3895
|
+
*/
|
|
3896
|
+
private warnMissingListeners;
|
|
3856
3897
|
/**
|
|
3857
3898
|
* Navigates the host to a raw path relative to this app's tenant/subsidiary
|
|
3858
3899
|
* base. Fire-and-forget.
|
|
@@ -3931,16 +3972,17 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3931
3972
|
private stopConnectPolling;
|
|
3932
3973
|
/**
|
|
3933
3974
|
* Initializes PostHog session recording + analytics from the host-supplied
|
|
3934
|
-
* config
|
|
3975
|
+
* config carried by a one-time `POSTHOG_PUSH`, **only when the host opted in**
|
|
3976
|
+
* (`config.enabled`).
|
|
3935
3977
|
*
|
|
3936
|
-
* The host is the single source of truth: when it sends no `
|
|
3937
|
-
* (
|
|
3938
|
-
*
|
|
3939
|
-
*
|
|
3940
|
-
*
|
|
3941
|
-
*
|
|
3942
|
-
*
|
|
3943
|
-
*
|
|
3978
|
+
* The host is the single source of truth: when it sends no `POSTHOG_PUSH`
|
|
3979
|
+
* (recording disabled for this env/app) or `enabled: false`, this does
|
|
3980
|
+
* nothing — `enabled` is the kill-switch even though `posthog-js` is bundled
|
|
3981
|
+
* into the bridge. Cross-origin replay needs `recordCrossOriginIframes` on both
|
|
3982
|
+
* sides so the parent receives the iframe's rrweb frames into one stitched
|
|
3983
|
+
* recording, and `identify()` ties the iframe's recording + events to the same
|
|
3984
|
+
* person as the host. Idempotent (the `posthogInitialized` guard) and wrapped
|
|
3985
|
+
* in try/catch so a duplicate push or a PostHog failure is harmless.
|
|
3944
3986
|
*/
|
|
3945
3987
|
private initPostHog;
|
|
3946
3988
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var version = "0.
|
|
1
|
+
var version = "0.5.0";
|
|
2
2
|
|
|
3
3
|
type AccountingAccountDimensionValues = {
|
|
4
4
|
account_id: string;
|
|
@@ -1392,6 +1392,7 @@ type ActivityProviderOperationErrorType = 'Connection Error' | 'Authentication E
|
|
|
1392
1392
|
type ActivityReconciliationDefinitionData = {
|
|
1393
1393
|
type: ActivityReconciliationType;
|
|
1394
1394
|
task_definition_id: string;
|
|
1395
|
+
currency?: ActivityCurrency | null;
|
|
1395
1396
|
side_a_entity_type: ActivityReconciliationEntityType;
|
|
1396
1397
|
side_a_entity_id: string;
|
|
1397
1398
|
side_b_entity_type: ActivityReconciliationEntityType;
|
|
@@ -1405,7 +1406,6 @@ type ActivityReconciliationInstanceDataInput = {
|
|
|
1405
1406
|
task_instance_id: string;
|
|
1406
1407
|
start_date: string;
|
|
1407
1408
|
end_date: string;
|
|
1408
|
-
currency?: ActivityCurrency | null;
|
|
1409
1409
|
side_a_value?: number | string | null;
|
|
1410
1410
|
side_b_value?: number | string | null;
|
|
1411
1411
|
reconciling_items_balance?: number | string | null;
|
|
@@ -1419,7 +1419,6 @@ type ActivityReconciliationInstanceDataOutput = {
|
|
|
1419
1419
|
task_instance_id: string;
|
|
1420
1420
|
start_date: string;
|
|
1421
1421
|
end_date: string;
|
|
1422
|
-
currency?: ActivityCurrency | null;
|
|
1423
1422
|
side_a_value?: string | null;
|
|
1424
1423
|
side_b_value?: string | null;
|
|
1425
1424
|
reconciling_items_balance?: string | null;
|
|
@@ -3190,9 +3189,10 @@ interface BridgeSubsidiary {
|
|
|
3190
3189
|
*
|
|
3191
3190
|
* The host (nom-ui) is the single source of truth for these values — it knows
|
|
3192
3191
|
* the project key, the env-aware ingestion host, and the resolved person — and
|
|
3193
|
-
* delivers
|
|
3194
|
-
*
|
|
3195
|
-
*
|
|
3192
|
+
* delivers it over the bridge (as a one-time `POSTHOG_PUSH`, separate from
|
|
3193
|
+
* {@link ContextPayload}) so the iframe never hardcodes any of it. When the host
|
|
3194
|
+
* has no PostHog config (recording disabled for this env/app), it sends no
|
|
3195
|
+
* `POSTHOG_PUSH` and the bridge initializes nothing.
|
|
3196
3196
|
*/
|
|
3197
3197
|
interface PostHogContext {
|
|
3198
3198
|
/** Project API key — the same PostHog project the host (nom-ui) uses. */
|
|
@@ -3230,6 +3230,27 @@ interface PostHogContext {
|
|
|
3230
3230
|
*/
|
|
3231
3231
|
enabled: boolean;
|
|
3232
3232
|
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Authentication-state change the host pushes to the iframe via `AUTH_CHANGED`.
|
|
3235
|
+
* It carries the security principal the embedded app authenticates and scopes
|
|
3236
|
+
* its own data by. Sent when the Nominal session changes: a logout
|
|
3237
|
+
* (`authenticated: false`) or an identity/tenant switch (`authenticated: true`
|
|
3238
|
+
* with a new `userId`/`tenant`). The initial state is implicit at connect — the
|
|
3239
|
+
* iframe only loads when authenticated, and the current identity/tenant are
|
|
3240
|
+
* {@link ContextPayload.user} / {@link ContextPayload.tenant}, so apps baseline
|
|
3241
|
+
* off those and react to this push.
|
|
3242
|
+
*
|
|
3243
|
+
* Subsidiary/period and other view-level state are NOT here — they ride the
|
|
3244
|
+
* follow-up `CONTEXT_PUSH` (`onContextChange`), per "auth change, then context".
|
|
3245
|
+
*/
|
|
3246
|
+
interface AuthPayload {
|
|
3247
|
+
/** Whether the Nominal user is still authenticated. */
|
|
3248
|
+
authenticated: boolean;
|
|
3249
|
+
/** The effective Nominal user id when authenticated; omitted on logout. */
|
|
3250
|
+
userId?: string;
|
|
3251
|
+
/** The tenant the user is acting in — the app's data-scoping key (RLS). */
|
|
3252
|
+
tenant?: string;
|
|
3253
|
+
}
|
|
3233
3254
|
interface ContextPayload {
|
|
3234
3255
|
tenant: string;
|
|
3235
3256
|
subsidiaryId: number;
|
|
@@ -3242,21 +3263,9 @@ interface ContextPayload {
|
|
|
3242
3263
|
subroute?: string;
|
|
3243
3264
|
/** Slug of the last closed accounting period, e.g. "mar-2026". */
|
|
3244
3265
|
lastClosedPeriodSlug?: string;
|
|
3245
|
-
/**
|
|
3246
|
-
* PostHog config for in-iframe session recording + custom events. **Absent
|
|
3247
|
-
* when recording is off** (older host, or disabled for this env/app), in which
|
|
3248
|
-
* case the bridge initializes no analytics. Optional so old hosts/bridges stay
|
|
3249
|
-
* compatible.
|
|
3250
|
-
*/
|
|
3251
|
-
posthog?: PostHogContext;
|
|
3252
3266
|
/** Version of the host SDK (@nominalso/vibe-host) running in nom-ui. */
|
|
3253
3267
|
hostVersion: string;
|
|
3254
3268
|
}
|
|
3255
|
-
/** Payload sent by the bridge when requesting context from the host. */
|
|
3256
|
-
interface GetContextPayload {
|
|
3257
|
-
/** Version of the bridge SDK (@nominalso/vibe-bridge) running in the iframe. */
|
|
3258
|
-
bridgeVersion: string;
|
|
3259
|
-
}
|
|
3260
3269
|
|
|
3261
3270
|
interface UploadPayload {
|
|
3262
3271
|
buffer: ArrayBuffer;
|
|
@@ -3490,10 +3499,6 @@ interface RequestRegistry {
|
|
|
3490
3499
|
payload: InvalidateCachePayload;
|
|
3491
3500
|
data: InvalidateResult;
|
|
3492
3501
|
};
|
|
3493
|
-
GET_CONTEXT: {
|
|
3494
|
-
payload: GetContextPayload;
|
|
3495
|
-
data: ContextPayload;
|
|
3496
|
-
};
|
|
3497
3502
|
UPLOAD_FILE: {
|
|
3498
3503
|
payload: UploadPayload;
|
|
3499
3504
|
data: UploadResponse;
|
|
@@ -3748,9 +3753,9 @@ interface VibeAppBridgeOptions {
|
|
|
3748
3753
|
*
|
|
3749
3754
|
* When omitted, each operation uses an operation-specific default tuned to
|
|
3750
3755
|
* how long it realistically takes (API reads `30000`, `UPLOAD_FILE`
|
|
3751
|
-
* `120000`, `INVALIDATE_CACHE` `10000
|
|
3752
|
-
*
|
|
3753
|
-
*
|
|
3756
|
+
* `120000`, `INVALIDATE_CACHE` `10000`). Setting this overrides all of them
|
|
3757
|
+
* with a single value — only do that if you have a specific reason; the
|
|
3758
|
+
* defaults already match the host's expectations.
|
|
3754
3759
|
*/
|
|
3755
3760
|
requestTimeout?: number;
|
|
3756
3761
|
}
|
|
@@ -3796,13 +3801,20 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3796
3801
|
private connectPollInterval;
|
|
3797
3802
|
private connectTimeout;
|
|
3798
3803
|
private onContextReceived;
|
|
3799
|
-
|
|
3804
|
+
/** Rejects an in-flight connect() (used by destroy()); `null` once settled. */
|
|
3805
|
+
private rejectConnect;
|
|
3806
|
+
/**
|
|
3807
|
+
* Persistent subscriber for post-connect context changes (tenant/subsidiary
|
|
3808
|
+
* switch, period close, …). Distinct from {@link onContextReceived}, which only
|
|
3809
|
+
* resolves the connect handshake and is cleared afterward. `null` until
|
|
3810
|
+
* {@link onContextChange} is called.
|
|
3811
|
+
*/
|
|
3812
|
+
private contextChangeCallback;
|
|
3800
3813
|
/**
|
|
3801
|
-
*
|
|
3802
|
-
*
|
|
3803
|
-
* resolve/reject the new one (a stale error could reject a healthy reconnect).
|
|
3814
|
+
* Persistent subscriber for host auth changes (Nominal logout or
|
|
3815
|
+
* identity/tenant switch). `null` until {@link onAuthChange} is called.
|
|
3804
3816
|
*/
|
|
3805
|
-
private
|
|
3817
|
+
private authChangeCallback;
|
|
3806
3818
|
private lastReportedSubroute;
|
|
3807
3819
|
private suppressSubrouteReport;
|
|
3808
3820
|
private subrouteCallback;
|
|
@@ -3819,11 +3831,11 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3819
3831
|
* methods, `upload()`, and subroute reporting all require an established
|
|
3820
3832
|
* connection. Concurrent calls return the same promise.
|
|
3821
3833
|
*
|
|
3822
|
-
*
|
|
3823
|
-
*
|
|
3824
|
-
*
|
|
3825
|
-
* after 10 seconds if no context arrives (usually a
|
|
3826
|
-
* or the host hasn't mounted).
|
|
3834
|
+
* Sends a `CONNECT` handshake, re-sending every 500ms until the host replies
|
|
3835
|
+
* by pushing the context (so it works even if the host mounts after the
|
|
3836
|
+
* iframe loads). Context is only ever delivered by push. Rejects with `Bridge
|
|
3837
|
+
* connect timed out` after 10 seconds if no context arrives (usually a
|
|
3838
|
+
* `parentOrigin` mismatch or the host hasn't mounted).
|
|
3827
3839
|
*
|
|
3828
3840
|
* After connecting, if the context includes an initial subroute (e.g. from
|
|
3829
3841
|
* a deep link), the bridge navigates to it and starts auto-tracking
|
|
@@ -3853,6 +3865,35 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3853
3865
|
* Returns an unsubscribe function.
|
|
3854
3866
|
*/
|
|
3855
3867
|
onSubrouteRequest(callback: (subroute: string) => void): () => void;
|
|
3868
|
+
/**
|
|
3869
|
+
* Subscribes to post-connect context changes (tenant/subsidiary switch, period
|
|
3870
|
+
* close, …). The callback fires for each {@link ContextPayload} the host pushes
|
|
3871
|
+
* *after* connect — not for the initial context, which {@link connect} already
|
|
3872
|
+
* returns. Returns an unsubscribe function. Single subscriber: a second call
|
|
3873
|
+
* replaces the first. Wire it **before {@link connect}** (the bridge warns at
|
|
3874
|
+
* connect if it isn't).
|
|
3875
|
+
*/
|
|
3876
|
+
onContextChange(callback: (ctx: ContextPayload) => void): () => void;
|
|
3877
|
+
/**
|
|
3878
|
+
* Subscribes to host auth changes: a Nominal logout (`authenticated: false` —
|
|
3879
|
+
* sign your backend out) or an identity/tenant switch (`authenticated: true`
|
|
3880
|
+
* with a new `userId`/`tenant` — re-authenticate). Fires for each change the
|
|
3881
|
+
* host pushes after connect. Returns an unsubscribe function. Single
|
|
3882
|
+
* subscriber: a second call replaces the first.
|
|
3883
|
+
*
|
|
3884
|
+
* **Wire this before {@link connect}** — without it the embedded app won't
|
|
3885
|
+
* sign out when the user logs out of Nominal (the bridge warns at connect if
|
|
3886
|
+
* it isn't wired).
|
|
3887
|
+
*/
|
|
3888
|
+
onAuthChange(callback: (auth: AuthPayload) => void): () => void;
|
|
3889
|
+
/**
|
|
3890
|
+
* At connect, nudge the app if it hasn't wired the change listeners: every
|
|
3891
|
+
* embedded app should react to a Nominal logout ({@link onAuthChange}) and a
|
|
3892
|
+
* tenant/subsidiary switch ({@link onContextChange}). Fires once during the
|
|
3893
|
+
* connect flow (not speculatively later), so wire both **before** `connect()`.
|
|
3894
|
+
* The skill/codegen does this by default; this catches hand-wired apps.
|
|
3895
|
+
*/
|
|
3896
|
+
private warnMissingListeners;
|
|
3856
3897
|
/**
|
|
3857
3898
|
* Navigates the host to a raw path relative to this app's tenant/subsidiary
|
|
3858
3899
|
* base. Fire-and-forget.
|
|
@@ -3931,16 +3972,17 @@ declare class VibeAppBridge extends BridgeDataMethods {
|
|
|
3931
3972
|
private stopConnectPolling;
|
|
3932
3973
|
/**
|
|
3933
3974
|
* Initializes PostHog session recording + analytics from the host-supplied
|
|
3934
|
-
* config
|
|
3975
|
+
* config carried by a one-time `POSTHOG_PUSH`, **only when the host opted in**
|
|
3976
|
+
* (`config.enabled`).
|
|
3935
3977
|
*
|
|
3936
|
-
* The host is the single source of truth: when it sends no `
|
|
3937
|
-
* (
|
|
3938
|
-
*
|
|
3939
|
-
*
|
|
3940
|
-
*
|
|
3941
|
-
*
|
|
3942
|
-
*
|
|
3943
|
-
*
|
|
3978
|
+
* The host is the single source of truth: when it sends no `POSTHOG_PUSH`
|
|
3979
|
+
* (recording disabled for this env/app) or `enabled: false`, this does
|
|
3980
|
+
* nothing — `enabled` is the kill-switch even though `posthog-js` is bundled
|
|
3981
|
+
* into the bridge. Cross-origin replay needs `recordCrossOriginIframes` on both
|
|
3982
|
+
* sides so the parent receives the iframe's rrweb frames into one stitched
|
|
3983
|
+
* recording, and `identify()` ties the iframe's recording + events to the same
|
|
3984
|
+
* person as the host. Idempotent (the `posthogInitialized` guard) and wrapped
|
|
3985
|
+
* in try/catch so a duplicate push or a PostHog failure is harmless.
|
|
3944
3986
|
*/
|
|
3945
3987
|
private initPostHog;
|
|
3946
3988
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// package.json
|
|
2
|
-
var version = "0.
|
|
2
|
+
var version = "0.5.0";
|
|
3
3
|
|
|
4
4
|
// ../protocol-types/dist/index.js
|
|
5
5
|
function normalizeSubroute(subroute) {
|
|
@@ -15,7 +15,7 @@ function normalizeSubroute(subroute) {
|
|
|
15
15
|
return normalized;
|
|
16
16
|
}
|
|
17
17
|
var PROTOCOL_ID = "nominal-vibe-bridge";
|
|
18
|
-
var PROTOCOL_VERSION =
|
|
18
|
+
var PROTOCOL_VERSION = 2;
|
|
19
19
|
var BRIDGE_KINDS = ["request", "response", "push", "command", "progress"];
|
|
20
20
|
var CORRELATED_KINDS = ["request", "response", "progress"];
|
|
21
21
|
var BRIDGE_KINDS_SET = new Set(BRIDGE_KINDS);
|
|
@@ -370,8 +370,6 @@ var CONNECT_POLL_MS = 500;
|
|
|
370
370
|
var DEFAULT_TIMEOUTS = {
|
|
371
371
|
/** Typed Nominal API reads/writes (`GET_*` / `POST_*` / `CREATE_*` / ...). */
|
|
372
372
|
api: 3e4,
|
|
373
|
-
/** `GET_CONTEXT` re-fetch (the initial fetch is governed by `connect()`). */
|
|
374
|
-
context: 5e3,
|
|
375
373
|
/** `INVALIDATE_CACHE`. */
|
|
376
374
|
invalidate: 1e4,
|
|
377
375
|
/** `UPLOAD_FILE` — large payloads + presigned S3 PUT take a while. */
|
|
@@ -384,8 +382,6 @@ function resolveRequestTimeout(type, requestTimeout) {
|
|
|
384
382
|
return DEFAULT_TIMEOUTS.upload;
|
|
385
383
|
case "INVALIDATE_CACHE":
|
|
386
384
|
return DEFAULT_TIMEOUTS.invalidate;
|
|
387
|
-
case "GET_CONTEXT":
|
|
388
|
-
return DEFAULT_TIMEOUTS.context;
|
|
389
385
|
default:
|
|
390
386
|
return DEFAULT_TIMEOUTS.api;
|
|
391
387
|
}
|
|
@@ -473,13 +469,20 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
473
469
|
this.connectPollInterval = null;
|
|
474
470
|
this.connectTimeout = null;
|
|
475
471
|
this.onContextReceived = null;
|
|
476
|
-
|
|
472
|
+
/** Rejects an in-flight connect() (used by destroy()); `null` once settled. */
|
|
473
|
+
this.rejectConnect = null;
|
|
477
474
|
/**
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
475
|
+
* Persistent subscriber for post-connect context changes (tenant/subsidiary
|
|
476
|
+
* switch, period close, …). Distinct from {@link onContextReceived}, which only
|
|
477
|
+
* resolves the connect handshake and is cleared afterward. `null` until
|
|
478
|
+
* {@link onContextChange} is called.
|
|
481
479
|
*/
|
|
482
|
-
this.
|
|
480
|
+
this.contextChangeCallback = null;
|
|
481
|
+
/**
|
|
482
|
+
* Persistent subscriber for host auth changes (Nominal logout or
|
|
483
|
+
* identity/tenant switch). `null` until {@link onAuthChange} is called.
|
|
484
|
+
*/
|
|
485
|
+
this.authChangeCallback = null;
|
|
483
486
|
this.lastReportedSubroute = null;
|
|
484
487
|
this.suppressSubrouteReport = false;
|
|
485
488
|
this.subrouteCallback = null;
|
|
@@ -492,7 +495,21 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
492
495
|
progress: (msg) => this.handleProgress(msg)
|
|
493
496
|
};
|
|
494
497
|
this.pushHandlers = {
|
|
495
|
-
CONTEXT_PUSH: (payload) =>
|
|
498
|
+
CONTEXT_PUSH: (payload) => {
|
|
499
|
+
if (this.onContextReceived) {
|
|
500
|
+
this.onContextReceived(payload);
|
|
501
|
+
} else if (this.context) {
|
|
502
|
+
this.context = payload;
|
|
503
|
+
this.contextChangeCallback?.(payload);
|
|
504
|
+
}
|
|
505
|
+
},
|
|
506
|
+
// Persistent (not nulled after connect): the host delivers PostHog once at
|
|
507
|
+
// 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),
|
|
510
|
+
// Persistent: the host pushes auth changes (logout, identity/tenant
|
|
511
|
+
// switch) at any time after connect.
|
|
512
|
+
AUTH_CHANGED: (payload) => this.authChangeCallback?.(payload),
|
|
496
513
|
SUBROUTE_PUSH: (payload) => {
|
|
497
514
|
const safe = normalizeSubroute(payload.subroute);
|
|
498
515
|
if (!safe) return;
|
|
@@ -543,11 +560,11 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
543
560
|
* methods, `upload()`, and subroute reporting all require an established
|
|
544
561
|
* connection. Concurrent calls return the same promise.
|
|
545
562
|
*
|
|
546
|
-
*
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
* after 10 seconds if no context arrives (usually a
|
|
550
|
-
* or the host hasn't mounted).
|
|
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).
|
|
551
568
|
*
|
|
552
569
|
* After connecting, if the context includes an initial subroute (e.g. from
|
|
553
570
|
* a deep link), the bridge navigates to it and starts auto-tracking
|
|
@@ -577,16 +594,16 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
577
594
|
this.stopConnectPolling();
|
|
578
595
|
this.connectPromise = null;
|
|
579
596
|
this.onContextReceived = null;
|
|
580
|
-
this.
|
|
597
|
+
this.rejectConnect = null;
|
|
581
598
|
reject(new BridgeError("TIMEOUT", "Bridge connect timed out"));
|
|
582
599
|
}, CONNECT_TIMEOUT_MS);
|
|
583
600
|
this.connectTimeout = timer;
|
|
584
|
-
this.
|
|
601
|
+
this.rejectConnect = (error) => {
|
|
585
602
|
clearTimeout(timer);
|
|
586
603
|
this.stopConnectPolling();
|
|
587
604
|
this.connectPromise = null;
|
|
588
605
|
this.onContextReceived = null;
|
|
589
|
-
this.
|
|
606
|
+
this.rejectConnect = null;
|
|
590
607
|
reject(error);
|
|
591
608
|
};
|
|
592
609
|
this.onContextReceived = (ctx) => {
|
|
@@ -595,11 +612,10 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
595
612
|
this.context = ctx;
|
|
596
613
|
this.connectPromise = null;
|
|
597
614
|
this.onContextReceived = null;
|
|
598
|
-
this.
|
|
615
|
+
this.rejectConnect = null;
|
|
599
616
|
console.log(
|
|
600
617
|
`[vibe-bridge] Connected \u2014 bridge: ${version}, host: ${ctx.hostVersion}, tenant: ${ctx.tenant}, user: ${ctx.user.displayName}`
|
|
601
618
|
);
|
|
602
|
-
this.initPostHog(ctx);
|
|
603
619
|
const initialSubroute = ctx.subroute ? normalizeSubroute(ctx.subroute) : null;
|
|
604
620
|
if (initialSubroute && initialSubroute !== "/") {
|
|
605
621
|
this.withSubrouteSuppressed(() => {
|
|
@@ -611,23 +627,14 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
611
627
|
this.lastReportedSubroute = window.location.pathname + window.location.search;
|
|
612
628
|
}
|
|
613
629
|
this.setupNavigationTracking();
|
|
630
|
+
this.warnMissingListeners();
|
|
614
631
|
resolve(ctx);
|
|
615
632
|
};
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
this.connectPollIds.add(requestId);
|
|
619
|
-
const message = {
|
|
620
|
-
__protocol: PROTOCOL_ID,
|
|
621
|
-
__version: PROTOCOL_VERSION,
|
|
622
|
-
kind: "request",
|
|
623
|
-
type: "GET_CONTEXT",
|
|
624
|
-
requestId,
|
|
625
|
-
payload: { bridgeVersion: version }
|
|
626
|
-
};
|
|
627
|
-
this.postToParent(message);
|
|
633
|
+
const sendConnect = () => {
|
|
634
|
+
this.sendCommand("CONNECT", { bridgeVersion: version });
|
|
628
635
|
};
|
|
629
|
-
|
|
630
|
-
this.connectPollInterval = setInterval(
|
|
636
|
+
sendConnect();
|
|
637
|
+
this.connectPollInterval = setInterval(sendConnect, CONNECT_POLL_MS);
|
|
631
638
|
});
|
|
632
639
|
return this.connectPromise;
|
|
633
640
|
}
|
|
@@ -654,6 +661,56 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
654
661
|
this.subrouteCallback = null;
|
|
655
662
|
};
|
|
656
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* Subscribes to post-connect context changes (tenant/subsidiary switch, period
|
|
666
|
+
* close, …). The callback fires for each {@link ContextPayload} the host pushes
|
|
667
|
+
* *after* connect — not for the initial context, which {@link connect} already
|
|
668
|
+
* returns. Returns an unsubscribe function. Single subscriber: a second call
|
|
669
|
+
* replaces the first. Wire it **before {@link connect}** (the bridge warns at
|
|
670
|
+
* connect if it isn't).
|
|
671
|
+
*/
|
|
672
|
+
onContextChange(callback) {
|
|
673
|
+
this.contextChangeCallback = callback;
|
|
674
|
+
return () => {
|
|
675
|
+
this.contextChangeCallback = null;
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Subscribes to host auth changes: a Nominal logout (`authenticated: false` —
|
|
680
|
+
* sign your backend out) or an identity/tenant switch (`authenticated: true`
|
|
681
|
+
* with a new `userId`/`tenant` — re-authenticate). Fires for each change the
|
|
682
|
+
* host pushes after connect. Returns an unsubscribe function. Single
|
|
683
|
+
* subscriber: a second call replaces the first.
|
|
684
|
+
*
|
|
685
|
+
* **Wire this before {@link connect}** — without it the embedded app won't
|
|
686
|
+
* sign out when the user logs out of Nominal (the bridge warns at connect if
|
|
687
|
+
* it isn't wired).
|
|
688
|
+
*/
|
|
689
|
+
onAuthChange(callback) {
|
|
690
|
+
this.authChangeCallback = callback;
|
|
691
|
+
return () => {
|
|
692
|
+
this.authChangeCallback = null;
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* At connect, nudge the app if it hasn't wired the change listeners: every
|
|
697
|
+
* embedded app should react to a Nominal logout ({@link onAuthChange}) and a
|
|
698
|
+
* tenant/subsidiary switch ({@link onContextChange}). Fires once during the
|
|
699
|
+
* connect flow (not speculatively later), so wire both **before** `connect()`.
|
|
700
|
+
* The skill/codegen does this by default; this catches hand-wired apps.
|
|
701
|
+
*/
|
|
702
|
+
warnMissingListeners() {
|
|
703
|
+
if (!this.authChangeCallback) {
|
|
704
|
+
console.warn(
|
|
705
|
+
"[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()."
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
if (!this.contextChangeCallback) {
|
|
709
|
+
console.warn(
|
|
710
|
+
"[vibe-bridge] connected without onContextChange \u2014 the app will not react to a tenant/subsidiary/period switch. Call bridge.onContextChange() before connect()."
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
657
714
|
// ---------------------------------------------------------------------------
|
|
658
715
|
// Host navigation & UI commands
|
|
659
716
|
//
|
|
@@ -734,7 +791,7 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
734
791
|
}
|
|
735
792
|
destroy() {
|
|
736
793
|
this.teardownNavigationTracking();
|
|
737
|
-
this.
|
|
794
|
+
this.rejectConnect?.(new BridgeError("DESTROYED", "Bridge destroyed"));
|
|
738
795
|
this.stopConnectPolling();
|
|
739
796
|
window.removeEventListener("message", this.boundHandleMessage);
|
|
740
797
|
for (const pending of this.pendingRequests.values()) {
|
|
@@ -745,7 +802,9 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
745
802
|
this.posthogInitialized = false;
|
|
746
803
|
this.connectPromise = null;
|
|
747
804
|
this.onContextReceived = null;
|
|
748
|
-
this.
|
|
805
|
+
this.rejectConnect = null;
|
|
806
|
+
this.contextChangeCallback = null;
|
|
807
|
+
this.authChangeCallback = null;
|
|
749
808
|
this.subrouteCallback = null;
|
|
750
809
|
this.lastReportedSubroute = null;
|
|
751
810
|
}
|
|
@@ -758,24 +817,23 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
758
817
|
clearTimeout(this.connectTimeout);
|
|
759
818
|
this.connectTimeout = null;
|
|
760
819
|
}
|
|
761
|
-
this.connectPollIds.clear();
|
|
762
820
|
}
|
|
763
821
|
/**
|
|
764
822
|
* Initializes PostHog session recording + analytics from the host-supplied
|
|
765
|
-
* config
|
|
823
|
+
* config carried by a one-time `POSTHOG_PUSH`, **only when the host opted in**
|
|
824
|
+
* (`config.enabled`).
|
|
766
825
|
*
|
|
767
|
-
* The host is the single source of truth: when it sends no `
|
|
768
|
-
* (
|
|
769
|
-
*
|
|
770
|
-
*
|
|
771
|
-
*
|
|
772
|
-
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
826
|
+
* The host is the single source of truth: when it sends no `POSTHOG_PUSH`
|
|
827
|
+
* (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.
|
|
775
834
|
*/
|
|
776
|
-
initPostHog(
|
|
777
|
-
|
|
778
|
-
if (!config?.enabled) return;
|
|
835
|
+
initPostHog(config) {
|
|
836
|
+
if (!config.enabled) return;
|
|
779
837
|
if (this.posthogInitialized) return;
|
|
780
838
|
if (typeof window === "undefined") return;
|
|
781
839
|
if (!config.token || !config.apiHost || !config.distinctId) {
|
|
@@ -934,15 +992,6 @@ var VibeAppBridge = class extends BridgeDataMethods {
|
|
|
934
992
|
this.kindHandlers[event.data.kind]?.(event.data);
|
|
935
993
|
}
|
|
936
994
|
handleResponse(msg) {
|
|
937
|
-
if (msg.type === "GET_CONTEXT") {
|
|
938
|
-
if (!this.connectPollIds.has(msg.requestId)) return;
|
|
939
|
-
if (msg.ok) {
|
|
940
|
-
this.onContextReceived?.(msg.data);
|
|
941
|
-
} else {
|
|
942
|
-
this.onContextError?.(new BridgeError(msg.code ?? "CONTEXT_ERROR", msg.error));
|
|
943
|
-
}
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
995
|
const pending = this.pendingRequests.get(msg.requestId);
|
|
947
996
|
if (!pending) return;
|
|
948
997
|
this.pendingRequests.delete(msg.requestId);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nominalso/vibe-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|