@principal-ai/control-tower-core 0.1.11 → 0.1.13

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.mjs CHANGED
@@ -3213,11 +3213,28 @@ class DefaultPresenceManager extends PresenceManager {
3213
3213
  userPresences = new Map;
3214
3214
  deviceToUser = new Map;
3215
3215
  gracePeriodEntries = new Map;
3216
+ extensions = [];
3217
+ server;
3216
3218
  constructor(config) {
3217
3219
  super(config);
3220
+ this.extensions = config?.extensions || [];
3221
+ }
3222
+ setServer(server) {
3223
+ this.server = server;
3224
+ }
3225
+ async initializeExtensions() {
3226
+ for (const ext of this.extensions) {
3227
+ await ext.initialize?.();
3228
+ }
3229
+ }
3230
+ async destroyExtensions() {
3231
+ for (const ext of this.extensions) {
3232
+ await ext.destroy?.();
3233
+ }
3218
3234
  }
3219
3235
  async connectDevice(userId, deviceId, deviceInfo) {
3220
3236
  const now = Date.now();
3237
+ const isFirstDevice = !this.userPresences.has(userId) && !this.gracePeriodEntries.has(userId);
3221
3238
  const gracePeriodEntry = this.gracePeriodEntries.get(userId);
3222
3239
  if (gracePeriodEntry) {
3223
3240
  this.gracePeriodEntries.delete(userId);
@@ -3234,9 +3251,31 @@ class DefaultPresenceManager extends PresenceManager {
3234
3251
  presence2.lastActivity = now;
3235
3252
  this.userPresences.set(userId, presence2);
3236
3253
  this.deviceToUser.set(deviceId, userId);
3254
+ if (isFirstDevice) {
3255
+ for (const ext of this.extensions) {
3256
+ await ext.onUserOnline?.(userId, deviceId, deviceInfo?.metadata || {});
3257
+ }
3258
+ } else {
3259
+ for (const ext of this.extensions) {
3260
+ await ext.onDeviceConnected?.(userId, deviceId, deviceInfo?.metadata || {});
3261
+ }
3262
+ }
3263
+ if (this.config.broadcastPresenceUpdates && this.server) {
3264
+ const experimental = this.server.experimental;
3265
+ await experimental?.broadcastAuthenticated({
3266
+ type: "presence:user_online",
3267
+ payload: {
3268
+ userId,
3269
+ deviceId,
3270
+ status: "online",
3271
+ timestamp: now
3272
+ }
3273
+ });
3274
+ }
3237
3275
  return presence2;
3238
3276
  }
3239
3277
  let presence = this.userPresences.get(userId);
3278
+ const wasOnline = presence !== undefined;
3240
3279
  if (!presence) {
3241
3280
  presence = {
3242
3281
  userId,
@@ -3259,6 +3298,27 @@ class DefaultPresenceManager extends PresenceManager {
3259
3298
  presence.status = this.calculatePresenceStatus(presence.devices, now);
3260
3299
  presence.lastActivity = now;
3261
3300
  this.deviceToUser.set(deviceId, userId);
3301
+ if (!wasOnline) {
3302
+ for (const ext of this.extensions) {
3303
+ await ext.onUserOnline?.(userId, deviceId, deviceInfo?.metadata || {});
3304
+ }
3305
+ } else {
3306
+ for (const ext of this.extensions) {
3307
+ await ext.onDeviceConnected?.(userId, deviceId, deviceInfo?.metadata || {});
3308
+ }
3309
+ }
3310
+ if (!wasOnline && this.config.broadcastPresenceUpdates && this.server) {
3311
+ const experimental = this.server.experimental;
3312
+ await experimental?.broadcastAuthenticated({
3313
+ type: "presence:user_online",
3314
+ payload: {
3315
+ userId,
3316
+ deviceId,
3317
+ status: "online",
3318
+ timestamp: now
3319
+ }
3320
+ });
3321
+ }
3262
3322
  return presence;
3263
3323
  }
3264
3324
  async disconnectDevice(userId, deviceId) {
@@ -3269,7 +3329,24 @@ class DefaultPresenceManager extends PresenceManager {
3269
3329
  presence.devices.delete(deviceId);
3270
3330
  this.deviceToUser.delete(deviceId);
3271
3331
  const now = Date.now();
3272
- if (presence.devices.size === 0) {
3332
+ const isLastDevice = presence.devices.size === 0;
3333
+ if (isLastDevice) {
3334
+ for (const ext of this.extensions) {
3335
+ await ext.onUserOffline?.(userId, deviceId);
3336
+ }
3337
+ if (this.config.broadcastPresenceUpdates && this.server) {
3338
+ const experimental = this.server.experimental;
3339
+ await experimental?.broadcastAuthenticated({
3340
+ type: "presence:user_offline",
3341
+ payload: {
3342
+ userId,
3343
+ deviceId,
3344
+ status: "offline",
3345
+ timestamp: now,
3346
+ gracePeriod: this.config.gracePeriod
3347
+ }
3348
+ });
3349
+ }
3273
3350
  if (this.config.gracePeriod > 0) {
3274
3351
  this.gracePeriodEntries.set(userId, {
3275
3352
  userId,
@@ -3283,6 +3360,9 @@ class DefaultPresenceManager extends PresenceManager {
3283
3360
  return null;
3284
3361
  }
3285
3362
  }
3363
+ for (const ext of this.extensions) {
3364
+ await ext.onDeviceDisconnected?.(userId, deviceId);
3365
+ }
3286
3366
  presence.status = this.calculatePresenceStatus(presence.devices, now);
3287
3367
  let mostRecentActivity = 0;
3288
3368
  for (const device of presence.devices.values()) {
@@ -3294,7 +3374,7 @@ class DefaultPresenceManager extends PresenceManager {
3294
3374
  return presence;
3295
3375
  }
3296
3376
  async updateActivity(update) {
3297
- const { userId, deviceId, timestamp } = update;
3377
+ const { userId, deviceId, timestamp, activityType } = update;
3298
3378
  const presence = this.userPresences.get(userId);
3299
3379
  if (!presence) {
3300
3380
  throw new Error(`User ${userId} not found in presence system`);
@@ -3309,10 +3389,30 @@ class DefaultPresenceManager extends PresenceManager {
3309
3389
  }
3310
3390
  const previousStatus = presence.status;
3311
3391
  presence.status = this.calculatePresenceStatus(presence.devices, timestamp);
3392
+ for (const ext of this.extensions) {
3393
+ await ext.onActivity?.(userId, deviceId, activityType || "heartbeat");
3394
+ }
3312
3395
  return presence;
3313
3396
  }
3314
3397
  async getUserPresence(userId) {
3315
- return this.userPresences.get(userId) || null;
3398
+ const basePresence = this.userPresences.get(userId);
3399
+ if (!basePresence) {
3400
+ return null;
3401
+ }
3402
+ if (this.extensions.length > 0) {
3403
+ const extendedData = {};
3404
+ for (const ext of this.extensions) {
3405
+ const data = await ext.getExtendedData?.(userId);
3406
+ if (data) {
3407
+ Object.assign(extendedData, data);
3408
+ }
3409
+ }
3410
+ return {
3411
+ ...basePresence,
3412
+ extended: extendedData
3413
+ };
3414
+ }
3415
+ return basePresence;
3316
3416
  }
3317
3417
  async getUsersPresence(userIds) {
3318
3418
  const result = new Map;
@@ -3328,7 +3428,20 @@ class DefaultPresenceManager extends PresenceManager {
3328
3428
  const online = [];
3329
3429
  for (const presence of this.userPresences.values()) {
3330
3430
  if (presence.status === "online") {
3331
- online.push(presence);
3431
+ let visible = true;
3432
+ for (const ext of this.extensions) {
3433
+ const shouldShow = await ext.shouldBeVisible?.(presence.userId);
3434
+ if (shouldShow === false) {
3435
+ visible = false;
3436
+ break;
3437
+ }
3438
+ }
3439
+ if (visible) {
3440
+ const fullPresence = await this.getUserPresence(presence.userId);
3441
+ if (fullPresence) {
3442
+ online.push(fullPresence);
3443
+ }
3444
+ }
3332
3445
  }
3333
3446
  }
3334
3447
  return online;
@@ -3405,6 +3518,7 @@ class DefaultPresenceManager extends PresenceManager {
3405
3518
  return changes;
3406
3519
  }
3407
3520
  async clear() {
3521
+ await this.destroyExtensions();
3408
3522
  this.userPresences.clear();
3409
3523
  this.deviceToUser.clear();
3410
3524
  this.gracePeriodEntries.clear();
@@ -4488,16 +4602,28 @@ class BaseClient extends TypedEventEmitter {
4488
4602
  this.lastConnectionUrl = url;
4489
4603
  this.lastCredentials = credentials || null;
4490
4604
  try {
4605
+ let tokenToApply = null;
4491
4606
  if (credentials && this.auth) {
4492
4607
  const authResult = await this.auth.authenticate(credentials);
4493
4608
  if (authResult.success && authResult.token) {
4494
4609
  this.authToken = authResult.token;
4495
4610
  this.userId = authResult.user?.userId || authResult.userId || "";
4496
- if (typeof this.transport.setAuthToken === "function") {
4497
- this.transport.setAuthToken(authResult.token);
4498
- }
4611
+ tokenToApply = authResult.token;
4612
+ }
4613
+ } else if (this.auth && typeof this.auth.getCurrentToken === "function") {
4614
+ const currentToken = this.auth.getCurrentToken();
4615
+ if (currentToken) {
4616
+ this.authToken = currentToken;
4617
+ tokenToApply = currentToken;
4618
+ try {
4619
+ const payload = await this.auth.validateToken(currentToken);
4620
+ this.userId = payload.userId;
4621
+ } catch {}
4499
4622
  }
4500
4623
  }
4624
+ if (tokenToApply && typeof this.transport.setAuthToken === "function") {
4625
+ this.transport.setAuthToken(tokenToApply);
4626
+ }
4501
4627
  const options = {
4502
4628
  url,
4503
4629
  reconnect: false,
@@ -4766,6 +4892,107 @@ class ClientBuilder {
4766
4892
  return new BaseClient(config);
4767
4893
  }
4768
4894
  }
4895
+ // src/client/PresenceClient.ts
4896
+ class PresenceClient extends TypedEventEmitter {
4897
+ client;
4898
+ wsUrl;
4899
+ autoReconnectEnabled;
4900
+ globalRoomId;
4901
+ reconnectTimeout;
4902
+ constructor(config) {
4903
+ super();
4904
+ this.wsUrl = config.wsUrl;
4905
+ this.autoReconnectEnabled = config.autoReconnect ?? true;
4906
+ this.globalRoomId = config.globalRoomId ?? "__global_presence__";
4907
+ this.client = new BaseClient(config.clientConfig);
4908
+ this.setupEventForwarding();
4909
+ if (config.autoConnect) {
4910
+ this.connect();
4911
+ }
4912
+ }
4913
+ async connect() {
4914
+ try {
4915
+ await this.client.connect(this.wsUrl);
4916
+ await this.client.joinRoom(this.globalRoomId);
4917
+ await this.emit("connected", {});
4918
+ } catch (error) {
4919
+ await this.emit("error", { error });
4920
+ throw error;
4921
+ }
4922
+ }
4923
+ async disconnect() {
4924
+ this.autoReconnectEnabled = false;
4925
+ if (this.reconnectTimeout) {
4926
+ clearTimeout(this.reconnectTimeout);
4927
+ this.reconnectTimeout = undefined;
4928
+ }
4929
+ try {
4930
+ await this.client.leaveRoom();
4931
+ await this.client.disconnect();
4932
+ await this.emit("disconnected", {});
4933
+ } catch (error) {
4934
+ await this.emit("error", { error });
4935
+ }
4936
+ }
4937
+ async getOnlineUsers() {
4938
+ throw new Error("getOnlineUsers() requires a REST API endpoint. " + "Implement this method based on your server API.");
4939
+ }
4940
+ async setStatus(status, message) {
4941
+ throw new Error("setStatus() requires a REST API endpoint. " + "Implement this method based on your server API.");
4942
+ }
4943
+ async setVisibility(visible) {
4944
+ throw new Error("setVisibility() requires a REST API endpoint. " + "Implement this method based on your server API.");
4945
+ }
4946
+ getClient() {
4947
+ return this.client;
4948
+ }
4949
+ isConnected() {
4950
+ return this.client.connectionState === "connected";
4951
+ }
4952
+ setupEventForwarding() {
4953
+ this.client.on("presence:user_online", (data) => {
4954
+ if (data && typeof data === "object") {
4955
+ const payload = data;
4956
+ this.emit("userOnline", {
4957
+ userId: payload.userId,
4958
+ devices: payload.devices || []
4959
+ });
4960
+ }
4961
+ });
4962
+ this.client.on("presence:user_offline", (data) => {
4963
+ if (data && typeof data === "object") {
4964
+ const payload = data;
4965
+ this.emit("userOffline", { userId: payload.userId });
4966
+ }
4967
+ });
4968
+ this.client.on("presence:status_changed", (data) => {
4969
+ if (data && typeof data === "object") {
4970
+ const payload = data;
4971
+ this.emit("statusChanged", {
4972
+ userId: payload.userId,
4973
+ status: payload.status,
4974
+ statusMessage: payload.statusMessage
4975
+ });
4976
+ }
4977
+ });
4978
+ this.client.on("disconnected", () => {
4979
+ this.emit("disconnected", {});
4980
+ if (this.autoReconnectEnabled) {
4981
+ this.reconnectTimeout = setTimeout(() => {
4982
+ this.connect().catch((error) => {
4983
+ this.emit("error", { error });
4984
+ });
4985
+ }, 5000);
4986
+ }
4987
+ });
4988
+ this.client.on("error", (data) => {
4989
+ if (data && typeof data === "object" && "error" in data) {
4990
+ const payload = data;
4991
+ this.emit("error", { error: payload.error });
4992
+ }
4993
+ });
4994
+ }
4995
+ }
4769
4996
  // src/server/ExperimentalAPI.ts
4770
4997
  class ExperimentalAPI {
4771
4998
  config;
@@ -4904,6 +5131,8 @@ class BaseServer extends TypedEventEmitter {
4904
5131
  config;
4905
5132
  clients = new Map;
4906
5133
  clientMessageHandlers = new Map;
5134
+ clientUserMap = new Map;
5135
+ userClientMap = new Map;
4907
5136
  running = false;
4908
5137
  initialized = false;
4909
5138
  mode;
@@ -5054,6 +5283,8 @@ class BaseServer extends TypedEventEmitter {
5054
5283
  }
5055
5284
  this.clients.clear();
5056
5285
  this.clientMessageHandlers.clear();
5286
+ this.clientUserMap.clear();
5287
+ this.userClientMap.clear();
5057
5288
  if (this.presenceManager) {
5058
5289
  await this.presenceManager.clear();
5059
5290
  }
@@ -5157,6 +5388,11 @@ class BaseServer extends TypedEventEmitter {
5157
5388
  if (client) {
5158
5389
  client.authenticated = true;
5159
5390
  client.userId = payload.userId;
5391
+ this.clientUserMap.set(payload.clientId, payload.userId);
5392
+ if (!this.userClientMap.has(payload.userId)) {
5393
+ this.userClientMap.set(payload.userId, new Set);
5394
+ }
5395
+ this.userClientMap.get(payload.userId).add(payload.clientId);
5160
5396
  await this.emit("client_authenticated", {
5161
5397
  clientId: payload.clientId,
5162
5398
  userId: payload.userId
@@ -5192,6 +5428,16 @@ class BaseServer extends TypedEventEmitter {
5192
5428
  if (!client) {
5193
5429
  return;
5194
5430
  }
5431
+ if (client.userId) {
5432
+ this.clientUserMap.delete(clientId);
5433
+ const clientSet = this.userClientMap.get(client.userId);
5434
+ if (clientSet) {
5435
+ clientSet.delete(clientId);
5436
+ if (clientSet.size === 0) {
5437
+ this.userClientMap.delete(client.userId);
5438
+ }
5439
+ }
5440
+ }
5195
5441
  if (this.presenceManager?.isEnabled() && client.userId) {
5196
5442
  try {
5197
5443
  const presence = await this.presenceManager.disconnectDevice(client.userId, clientId);
@@ -5235,6 +5481,9 @@ class BaseServer extends TypedEventEmitter {
5235
5481
  if (!client) {
5236
5482
  return;
5237
5483
  }
5484
+ if (this.presenceManager?.isEnabled() && client.userId && client.authenticated) {
5485
+ await this.updatePresenceActivity(client.userId, clientId, "message");
5486
+ }
5238
5487
  switch (message.type) {
5239
5488
  case "authenticate":
5240
5489
  await this.handleAuthenticate(clientId, message.payload);
@@ -5256,9 +5505,6 @@ class BaseServer extends TypedEventEmitter {
5256
5505
  break;
5257
5506
  case "ping":
5258
5507
  await this.sendToClient(clientId, { type: "pong", timestamp: Date.now() });
5259
- if (this.presenceManager?.isEnabled() && client.userId) {
5260
- await this.updatePresenceActivity(client.userId, clientId, "heartbeat");
5261
- }
5262
5508
  break;
5263
5509
  default:
5264
5510
  await this.sendToClient(clientId, {
@@ -5498,6 +5744,15 @@ class BaseServer extends TypedEventEmitter {
5498
5744
  getPresenceManager() {
5499
5745
  return this.presenceManager;
5500
5746
  }
5747
+ getUserIdFromClientId(clientId) {
5748
+ return this.clientUserMap.get(clientId);
5749
+ }
5750
+ getClientIdsForUser(userId) {
5751
+ return Array.from(this.userClientMap.get(userId) || []);
5752
+ }
5753
+ getDeviceIdFromClientId(clientId) {
5754
+ return clientId;
5755
+ }
5501
5756
  }
5502
5757
  // src/server/ServerBuilder.ts
5503
5758
  class ServerBuilder {
@@ -5587,7 +5842,44 @@ class ServerBuilder {
5587
5842
  trackUsageMetrics: false
5588
5843
  }
5589
5844
  };
5590
- return new BaseServer(config);
5845
+ const server = new BaseServer(config);
5846
+ if (this.presenceManager instanceof DefaultPresenceManager) {
5847
+ this.presenceManager.setServer(server);
5848
+ }
5849
+ if (this.presenceManager instanceof DefaultPresenceManager) {
5850
+ this.wirePresenceExtensionHooks(server, this.presenceManager);
5851
+ }
5852
+ return server;
5853
+ }
5854
+ wirePresenceExtensionHooks(server, presenceManager) {
5855
+ server.on("started", async () => {
5856
+ await presenceManager.initializeExtensions();
5857
+ });
5858
+ server.on("client_joined_room", async ({ clientId, roomId }) => {
5859
+ const userId = server.getUserIdFromClientId(clientId);
5860
+ if (userId && presenceManager.isEnabled()) {
5861
+ const extensions = presenceManager.extensions;
5862
+ if (extensions) {
5863
+ for (const ext of extensions) {
5864
+ await ext.onRoomJoined?.(userId, roomId, clientId);
5865
+ }
5866
+ }
5867
+ }
5868
+ });
5869
+ server.on("client_left_room", async ({ clientId, roomId }) => {
5870
+ const userId = server.getUserIdFromClientId(clientId);
5871
+ if (userId && presenceManager.isEnabled()) {
5872
+ const extensions = presenceManager.extensions;
5873
+ if (extensions) {
5874
+ for (const ext of extensions) {
5875
+ await ext.onRoomLeft?.(userId, roomId, clientId);
5876
+ }
5877
+ }
5878
+ }
5879
+ });
5880
+ server.on("stopped", async () => {
5881
+ await presenceManager.destroyExtensions();
5882
+ });
5591
5883
  }
5592
5884
  }
5593
5885
  export {
@@ -5597,6 +5889,7 @@ export {
5597
5889
  ServerBuilder,
5598
5890
  RoomManager,
5599
5891
  PresenceManager,
5892
+ PresenceClient,
5600
5893
  MockTransportAdapter,
5601
5894
  MockStorageAdapter,
5602
5895
  MockAuthAdapter,
@@ -5611,4 +5904,4 @@ export {
5611
5904
  BaseClient
5612
5905
  };
5613
5906
 
5614
- //# debugId=FF090C02C3545A4B64756E2164756E21
5907
+ //# debugId=205D80A7D7E855D364756E2164756E21