@qontinui/ui-bridge 0.21.0 → 0.21.1

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.
@@ -25865,7 +25865,12 @@ function handleRelayRoute(method, path, request, relay, config) {
25865
25865
  400
25866
25866
  );
25867
25867
  }
25868
- relay.recordRegistration(heartbeatTabId, { userId, sessionId });
25868
+ const callerUserId = request.headers.get("x-caller-user-id") ?? request.headers.get("X-Caller-User-Id") ?? void 0;
25869
+ const registration = relay.recordRegistration(
25870
+ heartbeatTabId,
25871
+ { userId, sessionId },
25872
+ { callerUserId }
25873
+ );
25869
25874
  relay.receiveHeartbeat(heartbeatTabId, {
25870
25875
  url: typeof body?.url === "string" ? body.url : void 0,
25871
25876
  title: typeof body?.title === "string" ? body.title : void 0,
@@ -25875,7 +25880,15 @@ function handleRelayRoute(method, path, request, relay, config) {
25875
25880
  const tabRegistered = diag.connectedTabs.includes(heartbeatTabId);
25876
25881
  return jsonResponse({
25877
25882
  success: true,
25878
- data: { received: true, tabRegistered },
25883
+ data: {
25884
+ received: true,
25885
+ tabRegistered,
25886
+ // Observability: signals an ownership claim/transfer was refused
25887
+ // because the claimed userId was not auth-proven. Not a fatal
25888
+ // heartbeat error — the loop continues; the tab simply keeps its
25889
+ // prior (or no) owner. Absent when ownership was assigned.
25890
+ ...registration.ownershipAssigned ? {} : { ownershipAssigned: false, ownerChangeRejected: registration.rejectedReason }
25891
+ },
25879
25892
  timestamp: Date.now()
25880
25893
  });
25881
25894
  })();
@@ -29125,20 +29138,52 @@ var CommandRelay = class {
29125
29138
  * has been called for it. `listOwnedTabs`, `assertOwnership`, and the
29126
29139
  * bus-envelope receiver check all key off this map.
29127
29140
  *
29128
- * Re-registration under a DIFFERENT `userId` is permitted (e.g. a tab
29129
- * survives a re-login) — the entry is overwritten in place. This is
29130
- * the safe path: the alternative (reject + force a reconnect) would
29131
- * leave the tab unaddressable until the SSE stream re-establishes,
29132
- * which is strictly worse for the operator UX. The new ownership
29133
- * takes effect on the next dispatch.
29134
- */
29135
- recordRegistration(tabId, metadata) {
29141
+ * Re-registration under a DIFFERENT `userId` (e.g. a tab survives a
29142
+ * re-login) is permitted ONLY when the new user is AUTH-PROVEN, not
29143
+ * merely asserted. `metadata.userId` rides in the (browser-supplied)
29144
+ * heartbeat body and is therefore untrusted on its own: without a
29145
+ * proof gate, anyone who learns a victim's `tabId` could POST a
29146
+ * heartbeat claiming any `userId` and silently seize ownership of the
29147
+ * tab (and with it the ability to drive it / settle its commands).
29148
+ *
29149
+ * `options.callerUserId` is the identity the consumer's auth gate
29150
+ * proved and injected as `X-Caller-User-Id` (never a browser value).
29151
+ * The rule:
29152
+ *
29153
+ * - Same-owner heartbeat (existing owner === claimed userId): always
29154
+ * a refresh — `sessionId`/`lastSeen` updated in place. No proof
29155
+ * needed; you already hold the tab and the claim matches.
29156
+ * - TRANSFER (a DIFFERENT user already owns the tab): the claimed
29157
+ * `userId` MUST be auth-proven and equal to `callerUserId`. An
29158
+ * unauthenticated (`callerUserId` absent) or mismatched claim is
29159
+ * REFUSED and the prior owner is left untouched — this is the
29160
+ * takeover primitive being closed. A genuine re-login carries the
29161
+ * new user's proven identity and therefore still transfers.
29162
+ * - First CLAIM (tab has no owner yet — bootstrap): honored, so a
29163
+ * header-less / single-trust-domain deployment can still register
29164
+ * tabs. A *present* proven identity must still match the claimed
29165
+ * `userId` (you cannot bootstrap a tab under an identity you
29166
+ * cannot back).
29167
+ *
29168
+ * Returns whether ownership was (re)assigned so the HTTP layer can
29169
+ * surface the refusal without bricking the heartbeat loop.
29170
+ */
29171
+ recordRegistration(tabId, metadata, options) {
29136
29172
  const now = Date.now();
29173
+ const proven = options?.callerUserId;
29174
+ if (proven != null && proven !== metadata.userId) {
29175
+ return { ownershipAssigned: false, rejectedReason: "OWNER_CHANGE_UNVERIFIED" };
29176
+ }
29137
29177
  const existing = this.tabOwnership.get(tabId);
29138
29178
  if (existing && existing.userId === metadata.userId) {
29139
29179
  existing.sessionId = metadata.sessionId;
29140
29180
  existing.lastSeen = now;
29141
- return;
29181
+ return { ownershipAssigned: true };
29182
+ }
29183
+ if (existing) {
29184
+ if (proven == null) {
29185
+ return { ownershipAssigned: false, rejectedReason: "OWNER_CHANGE_UNVERIFIED" };
29186
+ }
29142
29187
  }
29143
29188
  this.tabOwnership.set(tabId, {
29144
29189
  userId: metadata.userId,
@@ -29146,6 +29191,7 @@ var CommandRelay = class {
29146
29191
  firstSeen: existing?.firstSeen ?? now,
29147
29192
  lastSeen: now
29148
29193
  });
29194
+ return { ownershipAssigned: true };
29149
29195
  }
29150
29196
  /**
29151
29197
  * Return the stored ownership record for a tab, or `null` if the tab