@peers-app/peers-sdk 0.20.2 → 0.20.3

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.
@@ -3,6 +3,7 @@ import { type IUser } from "../data";
3
3
  import type { DataSourceFactory, IDataChangedEvent, Table } from "../data/orm";
4
4
  import { type IPublicPrivateKeys } from "../keys";
5
5
  import { type Observable } from "../observable";
6
+ /** Coordinates personal and group data contexts for one signed-in user. */
6
7
  export declare class UserContext {
7
8
  readonly userId: string;
8
9
  readonly dataSourceFactory: DataSourceFactory;
@@ -16,6 +17,8 @@ export declare class UserContext {
16
17
  readonly defaultDataContext: Observable<DataContext>;
17
18
  readonly loadingPromise: Promise<UserContext>;
18
19
  private personalUserSubscription?;
20
+ private ownUserSubscription?;
21
+ private groupUserSyncs;
19
22
  private _currentlyActiveGroupIdPVar?;
20
23
  constructor(userId: string, dataSourceFactory: DataSourceFactory, ephemeral?: boolean | undefined);
21
24
  private init;
@@ -42,11 +45,14 @@ export declare class UserContext {
42
45
  signature?: string | undefined;
43
46
  } | undefined>;
44
47
  syncUserAndGroupObjects(userId: string, keys: IPublicPrivateKeys, me?: IUser): Promise<void>;
48
+ private syncMyUserToAllGroups;
49
+ private syncMyUserToGroup;
45
50
  subscribeToDataChangedAcrossAllGroups<T extends {
46
51
  [key: string]: any;
47
52
  }>(table: string | Table<T>, handler: (evt: ICrossGroupSubscriptionHandlerArgs<T>) => any): import("../events").ISubscriptionResult;
48
53
  dispose(): void;
49
54
  }
55
+ /** Event payload supplied to subscriptions spanning every group data context. */
50
56
  export interface ICrossGroupSubscriptionHandlerArgs<T> {
51
57
  name: string;
52
58
  userContext: UserContext;
@@ -8,6 +8,7 @@ const events_1 = require("../events");
8
8
  const keys_1 = require("../keys");
9
9
  const observable_1 = require("../observable");
10
10
  const utils_1 = require("../utils");
11
+ /** Coordinates personal and group data contexts for one signed-in user. */
11
12
  class UserContext {
12
13
  userId;
13
14
  dataSourceFactory;
@@ -21,6 +22,8 @@ class UserContext {
21
22
  defaultDataContext;
22
23
  loadingPromise;
23
24
  personalUserSubscription;
25
+ ownUserSubscription;
26
+ groupUserSyncs = new Map();
24
27
  _currentlyActiveGroupIdPVar;
25
28
  constructor(userId, dataSourceFactory, ephemeral) {
26
29
  this.userId = userId;
@@ -69,7 +72,10 @@ class UserContext {
69
72
  this.groupDataContexts.delete(groupId);
70
73
  }
71
74
  else {
72
- this.getDataContext(groupId);
75
+ const dataContext = this.getDataContext(groupId);
76
+ void this.syncMyUserToGroup(dataContext).catch((error) => {
77
+ console.error(`Error syncing current user to new group ${groupId}:`, error);
78
+ });
73
79
  }
74
80
  });
75
81
  // load up group data contexts and set currently active group
@@ -182,6 +188,7 @@ class UserContext {
182
188
  publicBoxKey: "",
183
189
  };
184
190
  }
191
+ me.name = me.name.slice(0, data_1.DISPLAY_NAME_MAX_LENGTH);
185
192
  me.publicKey = keys.publicKey;
186
193
  me.publicBoxKey = keys.publicBoxKey;
187
194
  const meSigned = (0, keys_1.addSignatureToObject)(me, keys.secretKey);
@@ -190,13 +197,7 @@ class UserContext {
190
197
  (0, keys_1.verifyObjectSignature)(meSigned);
191
198
  await (0, data_1.Users)(this.userDataContext).save(meSigned, { weakInsert: true });
192
199
  }
193
- // sync my user to all my groups
194
- for (const [, dataContext] of this.groupDataContexts) {
195
- const groupMe = await (0, data_1.Users)(dataContext).get(me.userId);
196
- if (!(0, lodash_1.isEqual)(groupMe, meSigned)) {
197
- await (0, data_1.Users)(dataContext).save(meSigned, { weakInsert: true });
198
- }
199
- }
200
+ await this.syncMyUserToAllGroups();
200
201
  // sync group objects to my personal db
201
202
  for (const [, dataContext] of this.groupDataContexts) {
202
203
  if (!dataContext.groupId) {
@@ -234,6 +235,44 @@ class UserContext {
234
235
  }
235
236
  });
236
237
  }
238
+ if (!this.ownUserSubscription) {
239
+ this.ownUserSubscription = (0, data_1.Users)(this.userDataContext).dataChanged.subscribe((evt) => {
240
+ if (evt.op === "delete" || evt.dataObject.userId !== this.userId) {
241
+ return;
242
+ }
243
+ void this.syncMyUserToAllGroups().catch((error) => {
244
+ console.error(`Error syncing current user ${this.userId} to groups:`, error);
245
+ });
246
+ });
247
+ }
248
+ }
249
+ async syncMyUserToAllGroups() {
250
+ await Promise.all(Array.from(this.groupDataContexts.values(), (dataContext) => this.syncMyUserToGroup(dataContext)));
251
+ }
252
+ async syncMyUserToGroup(dataContext) {
253
+ const contextId = dataContext.dataContextId;
254
+ const previousSync = this.groupUserSyncs.get(contextId);
255
+ const currentSync = (previousSync ?? Promise.resolve())
256
+ .catch(() => undefined)
257
+ .then(async () => {
258
+ const currentUser = await (0, data_1.Users)(this.userDataContext).get(this.userId);
259
+ if (!currentUser) {
260
+ return;
261
+ }
262
+ const groupMe = await (0, data_1.Users)(dataContext).get(currentUser.userId);
263
+ if (!(0, lodash_1.isEqual)(groupMe, currentUser)) {
264
+ await (0, data_1.Users)(dataContext).save(currentUser, { weakInsert: true });
265
+ }
266
+ });
267
+ this.groupUserSyncs.set(contextId, currentSync);
268
+ try {
269
+ await currentSync;
270
+ }
271
+ finally {
272
+ if (this.groupUserSyncs.get(contextId) === currentSync) {
273
+ this.groupUserSyncs.delete(contextId);
274
+ }
275
+ }
237
276
  }
238
277
  subscribeToDataChangedAcrossAllGroups(table, handler) {
239
278
  const tableName = typeof table === "string" ? table : table.tableName;
@@ -258,6 +297,7 @@ class UserContext {
258
297
  }
259
298
  dispose() {
260
299
  this.personalUserSubscription?.unsubscribe();
300
+ this.ownUserSubscription?.unsubscribe();
261
301
  }
262
302
  }
263
303
  exports.UserContext = UserContext;
@@ -1,12 +1,18 @@
1
1
  import { z } from "zod";
2
2
  import type { DataContext } from "../context/data-context";
3
3
  import { TrustLevel } from "../device/socket.type";
4
+ /** Maximum number of JavaScript string code units accepted for display names. */
5
+ export declare const DISPLAY_NAME_MAX_LENGTH = 128;
6
+ /** Shared schema for bounded user and device display names. */
7
+ export declare const displayNameSchema: z.ZodString;
8
+ /** Schema for a persisted, context-scoped device record. */
4
9
  export declare const deviceSchema: z.ZodObject<{
5
10
  deviceId: z.ZodEffects<z.ZodString, string, string>;
6
11
  userId: z.ZodEffects<z.ZodString, string, string>;
7
12
  firstSeen: z.ZodDate;
8
13
  lastSeen: z.ZodDate;
9
14
  name: z.ZodOptional<z.ZodString>;
15
+ reportedName: z.ZodOptional<z.ZodString>;
10
16
  serverUrl: z.ZodOptional<z.ZodString>;
11
17
  trustLevel: z.ZodNativeEnum<typeof TrustLevel>;
12
18
  }, "strip", z.ZodTypeAny, {
@@ -16,6 +22,7 @@ export declare const deviceSchema: z.ZodObject<{
16
22
  lastSeen: Date;
17
23
  trustLevel: TrustLevel;
18
24
  name?: string | undefined;
25
+ reportedName?: string | undefined;
19
26
  serverUrl?: string | undefined;
20
27
  }, {
21
28
  userId: string;
@@ -24,20 +31,72 @@ export declare const deviceSchema: z.ZodObject<{
24
31
  lastSeen: Date;
25
32
  trustLevel: TrustLevel;
26
33
  name?: string | undefined;
34
+ reportedName?: string | undefined;
27
35
  serverUrl?: string | undefined;
28
36
  }>;
37
+ /** A context-scoped device record containing local state and owner-reported metadata. */
29
38
  export type IDevice = z.infer<typeof deviceSchema>;
30
- export interface IDeviceInfo {
39
+ /** Schema for the signed identity and bounded presentation hints exchanged by devices. */
40
+ export declare const deviceInfoSchema: z.ZodObject<{
41
+ userId: z.ZodEffects<z.ZodString, string, string>;
42
+ deviceId: z.ZodEffects<z.ZodString, string, string>;
43
+ publicKey: z.ZodString;
44
+ publicBoxKey: z.ZodString;
45
+ userName: z.ZodOptional<z.ZodString>;
46
+ deviceName: z.ZodOptional<z.ZodString>;
47
+ }, "strip", z.ZodTypeAny, {
48
+ publicKey: string;
49
+ publicBoxKey: string;
31
50
  userId: string;
32
51
  deviceId: string;
52
+ userName?: string | undefined;
53
+ deviceName?: string | undefined;
54
+ }, {
33
55
  publicKey: string;
34
56
  publicBoxKey: string;
35
- }
36
- export interface IDeviceHandshake extends IDeviceInfo {
57
+ userId: string;
58
+ deviceId: string;
59
+ userName?: string | undefined;
60
+ deviceName?: string | undefined;
61
+ }>;
62
+ /** Signed identity and optional presentation hints advertised by a device. */
63
+ export type IDeviceInfo = z.infer<typeof deviceInfoSchema>;
64
+ /** Schema for connection-bound device handshake contents. */
65
+ export declare const deviceHandshakeSchema: z.ZodObject<{
66
+ userId: z.ZodEffects<z.ZodString, string, string>;
67
+ deviceId: z.ZodEffects<z.ZodString, string, string>;
68
+ publicKey: z.ZodString;
69
+ publicBoxKey: z.ZodString;
70
+ userName: z.ZodOptional<z.ZodString>;
71
+ deviceName: z.ZodOptional<z.ZodString>;
72
+ } & {
73
+ connectionId: z.ZodString;
74
+ serverAddress: z.ZodString;
75
+ timestamp: z.ZodNumber;
76
+ }, "strip", z.ZodTypeAny, {
77
+ timestamp: number;
78
+ publicKey: string;
79
+ publicBoxKey: string;
80
+ userId: string;
81
+ deviceId: string;
37
82
  connectionId: string;
38
83
  serverAddress: string;
84
+ userName?: string | undefined;
85
+ deviceName?: string | undefined;
86
+ }, {
39
87
  timestamp: number;
40
- }
88
+ publicKey: string;
89
+ publicBoxKey: string;
90
+ userId: string;
91
+ deviceId: string;
92
+ connectionId: string;
93
+ serverAddress: string;
94
+ userName?: string | undefined;
95
+ deviceName?: string | undefined;
96
+ }>;
97
+ /** Signed device information bound to one connection handshake. */
98
+ export type IDeviceHandshake = z.infer<typeof deviceHandshakeSchema>;
99
+ /** Returns the Devices table for a data context. */
41
100
  export declare function Devices(dataContext?: DataContext): import("..").Table<{
42
101
  userId: string;
43
102
  deviceId: string;
@@ -45,7 +104,10 @@ export declare function Devices(dataContext?: DataContext): import("..").Table<{
45
104
  lastSeen: Date;
46
105
  trustLevel: TrustLevel;
47
106
  name?: string | undefined;
107
+ reportedName?: string | undefined;
48
108
  serverUrl?: string | undefined;
49
109
  }>;
110
+ /** Relay server URLs trusted by the active group. */
50
111
  export declare const trustedServers: import("./persistent-vars").PersistentVar<string[]>;
112
+ /** Persistent identifier for the current physical/runtime device. */
51
113
  export declare const thisDeviceId: import("./persistent-vars").PersistentVar<string | undefined>;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.thisDeviceId = exports.trustedServers = exports.deviceSchema = void 0;
3
+ exports.thisDeviceId = exports.trustedServers = exports.deviceHandshakeSchema = exports.deviceInfoSchema = exports.deviceSchema = exports.displayNameSchema = exports.DISPLAY_NAME_MAX_LENGTH = void 0;
4
4
  exports.Devices = Devices;
5
5
  const zod_1 = require("zod");
6
6
  const user_context_singleton_1 = require("../context/user-context-singleton");
@@ -9,15 +9,36 @@ const zod_types_1 = require("../types/zod-types");
9
9
  const table_definitions_system_1 = require("./orm/table-definitions.system");
10
10
  const types_1 = require("./orm/types");
11
11
  const persistent_vars_1 = require("./persistent-vars");
12
+ /** Maximum number of JavaScript string code units accepted for display names. */
13
+ exports.DISPLAY_NAME_MAX_LENGTH = 128;
14
+ /** Shared schema for bounded user and device display names. */
15
+ exports.displayNameSchema = zod_1.z.string().max(exports.DISPLAY_NAME_MAX_LENGTH);
16
+ /** Schema for a persisted, context-scoped device record. */
12
17
  exports.deviceSchema = zod_1.z.object({
13
18
  deviceId: zod_types_1.zodPeerId,
14
19
  userId: zod_types_1.zodPeerId,
15
20
  firstSeen: zod_1.z.date(),
16
21
  lastSeen: zod_1.z.date(),
17
- name: zod_1.z.string().optional(),
22
+ name: exports.displayNameSchema.optional(),
23
+ reportedName: exports.displayNameSchema.optional(),
18
24
  serverUrl: zod_1.z.string().optional(),
19
25
  trustLevel: zod_1.z.nativeEnum(socket_type_1.TrustLevel),
20
26
  });
27
+ /** Schema for the signed identity and bounded presentation hints exchanged by devices. */
28
+ exports.deviceInfoSchema = zod_1.z.object({
29
+ userId: zod_types_1.zodPeerId,
30
+ deviceId: zod_types_1.zodPeerId,
31
+ publicKey: zod_1.z.string(),
32
+ publicBoxKey: zod_1.z.string(),
33
+ userName: exports.displayNameSchema.optional(),
34
+ deviceName: exports.displayNameSchema.optional(),
35
+ });
36
+ /** Schema for connection-bound device handshake contents. */
37
+ exports.deviceHandshakeSchema = exports.deviceInfoSchema.extend({
38
+ connectionId: zod_1.z.string(),
39
+ serverAddress: zod_1.z.string(),
40
+ timestamp: zod_1.z.number(),
41
+ });
21
42
  const metaData = {
22
43
  name: "Devices",
23
44
  description: "The Peer devices that this computer is aware of",
@@ -25,10 +46,13 @@ const metaData = {
25
46
  fields: (0, types_1.schemaToFields)(exports.deviceSchema),
26
47
  };
27
48
  (0, table_definitions_system_1.registerSystemTableDefinition)(metaData, exports.deviceSchema);
49
+ /** Returns the Devices table for a data context. */
28
50
  function Devices(dataContext) {
29
51
  return (0, user_context_singleton_1.getTableContainer)(dataContext).getTable(metaData, exports.deviceSchema);
30
52
  }
53
+ /** Relay server URLs trusted by the active group. */
31
54
  exports.trustedServers = (0, persistent_vars_1.groupVar)("trustedServers", {
32
55
  defaultValue: ["https://peers.app"],
33
56
  });
57
+ /** Persistent identifier for the current physical/runtime device. */
34
58
  exports.thisDeviceId = (0, persistent_vars_1.deviceVar)("thisDeviceId");
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import type { DataContext } from "../context/data-context";
3
3
  import type { ISaveOptions } from "./orm/data-query";
4
4
  import { Table } from "./orm/table";
5
+ /** Schema for a signed user profile. */
5
6
  export declare const userSchema: z.ZodObject<{
6
7
  userId: z.ZodEffects<z.ZodString, string, string>;
7
8
  name: z.ZodString;
@@ -21,17 +22,22 @@ export declare const userSchema: z.ZodObject<{
21
22
  userId: string;
22
23
  signature?: string | undefined;
23
24
  }>;
25
+ /** A signed user profile shared across personal and group contexts. */
24
26
  export type IUser = z.infer<typeof userSchema>;
27
+ /** User table with owner-signature enforcement outside the personal context. */
25
28
  export declare class UsersTable extends Table<IUser> {
26
29
  static isPassthrough: boolean;
27
30
  save(user: IUser, opts?: ISaveOptions): Promise<IUser>;
28
31
  signAndSave(user: IUser, opts?: ISaveOptions): Promise<IUser>;
29
32
  private static addSignatureToUser;
33
+ /** Enables signing for proxied `signAndSave` calls. */
30
34
  static enableUserSigning(fn: (user: IUser) => IUser): void;
31
35
  /** @deprecated Forbidden on UsersTable; use save() */
32
36
  insert(..._args: Parameters<Table<IUser>["insert"]>): never;
33
37
  /** @deprecated Forbidden on UsersTable; use save() */
34
38
  update(..._args: Parameters<Table<IUser>["update"]>): never;
35
39
  }
40
+ /** Returns the Users table for a data context. */
36
41
  export declare function Users(dataContext?: DataContext): UsersTable;
42
+ /** Returns the current user's personal profile. */
37
43
  export declare function getMe(): Promise<IUser>;
@@ -40,14 +40,16 @@ exports.getMe = getMe;
40
40
  const zod_1 = require("zod");
41
41
  const user_context_singleton_1 = require("../context/user-context-singleton");
42
42
  const zod_types_1 = require("../types/zod-types");
43
+ const devices_1 = require("./devices");
43
44
  const decorators_1 = require("./orm/decorators");
44
45
  const table_1 = require("./orm/table");
45
46
  const table_definitions_system_1 = require("./orm/table-definitions.system");
46
47
  const types_1 = require("./orm/types");
47
48
  const user_permissions_1 = require("./user-permissions");
49
+ /** Schema for a signed user profile. */
48
50
  exports.userSchema = zod_1.z.object({
49
51
  userId: zod_types_1.zodPeerId,
50
- name: zod_1.z.string(),
52
+ name: devices_1.displayNameSchema,
51
53
  publicKey: zod_1.z.string().describe("The public key the user uses to sign messages"),
52
54
  publicBoxKey: zod_1.z
53
55
  .string()
@@ -64,6 +66,7 @@ const metaData = {
64
66
  fields: (0, types_1.schemaToFields)(exports.userSchema),
65
67
  indexes: [{ fields: ["name"] }, { fields: ["publicKey"] }, { fields: ["publicBoxKey"] }],
66
68
  };
69
+ /** User table with owner-signature enforcement outside the personal context. */
67
70
  let UsersTable = (() => {
68
71
  let _classSuper = table_1.Table;
69
72
  let _instanceExtraInitializers = [];
@@ -97,10 +100,15 @@ let UsersTable = (() => {
97
100
  if (!UsersTable.addSignatureToUser) {
98
101
  throw new Error("User signing must be enabled to sign and save users. Call UsersTable.enableUserSigning(fn) to enable it.");
99
102
  }
103
+ user = {
104
+ ...user,
105
+ name: user.name.slice(0, devices_1.DISPLAY_NAME_MAX_LENGTH),
106
+ };
100
107
  user = UsersTable.addSignatureToUser(user);
101
108
  return this.save(user, opts);
102
109
  }
103
110
  static addSignatureToUser = undefined;
111
+ /** Enables signing for proxied `signAndSave` calls. */
104
112
  static enableUserSigning(fn) {
105
113
  UsersTable.addSignatureToUser = fn;
106
114
  }
@@ -120,10 +128,12 @@ let UsersTable = (() => {
120
128
  })();
121
129
  exports.UsersTable = UsersTable;
122
130
  (0, table_definitions_system_1.registerSystemTableDefinition)(metaData, exports.userSchema, UsersTable);
131
+ /** Returns the Users table for a data context. */
123
132
  function Users(dataContext) {
124
133
  return (0, user_context_singleton_1.getTableContainer)(dataContext).getTable(metaData, exports.userSchema, UsersTable);
125
134
  }
126
135
  // let me: IUser | undefined = undefined;
136
+ /** Returns the current user's personal profile. */
127
137
  async function getMe() {
128
138
  const userContext = await (0, user_context_singleton_1.getUserContext)();
129
139
  const me = await userContext.getMe();
@@ -377,15 +377,15 @@ describe("binary-peer-connection-v2", () => {
377
377
  describe("wrapBinaryPeer", () => {
378
378
  it("should successfully handshake over wrtc protocol", async () => {
379
379
  const { iPeer, rPeer } = await getMockPeerPair();
380
- const userId = `00000000000000000user001`;
380
+ const userId = (0, utils_1.newid)();
381
381
  const keys = (0, keys_1.newKeys)();
382
382
  const connectionId = `00000000000000000conn001`;
383
- const iPeerDeviceId = `000000000000000device001`;
383
+ const iPeerDeviceId = (0, utils_1.newid)();
384
384
  const iPeerDevice = new device_1.Device(userId, iPeerDeviceId, keys);
385
385
  const iPeerConnection = (0, binary_peer_connection_v2_1.wrapBinaryPeer)(connectionId, iPeer, iPeerDevice, true, {
386
386
  protocol: "wrtc",
387
387
  });
388
- const rPeerDeviceId = `000000000000000device002`;
388
+ const rPeerDeviceId = (0, utils_1.newid)();
389
389
  const rPeerDevice = new device_1.Device(userId, rPeerDeviceId, keys);
390
390
  const rPeerConnection = (0, binary_peer_connection_v2_1.wrapBinaryPeer)(connectionId, rPeer, rPeerDevice, false, {
391
391
  protocol: "wrtc",
@@ -400,16 +400,16 @@ describe("binary-peer-connection-v2", () => {
400
400
  });
401
401
  it("should set secureLocal/secureRemote when markTransportSecure is true", async () => {
402
402
  const { iPeer, rPeer } = await getMockPeerPair();
403
- const userId = `00000000000000000user001`;
403
+ const userId = (0, utils_1.newid)();
404
404
  const keys = (0, keys_1.newKeys)();
405
405
  const connectionId = `00000000000000000conn001`;
406
- const iPeerDeviceId = `000000000000000device001`;
406
+ const iPeerDeviceId = (0, utils_1.newid)();
407
407
  const iPeerDevice = new device_1.Device(userId, iPeerDeviceId, keys);
408
408
  const iPeerConnection = (0, binary_peer_connection_v2_1.wrapBinaryPeer)(connectionId, iPeer, iPeerDevice, true, {
409
409
  protocol: "wrtc",
410
410
  markTransportSecure: true,
411
411
  });
412
- const rPeerDeviceId = `000000000000000device002`;
412
+ const rPeerDeviceId = (0, utils_1.newid)();
413
413
  const rPeerDevice = new device_1.Device(userId, rPeerDeviceId, keys);
414
414
  const rPeerConnection = (0, binary_peer_connection_v2_1.wrapBinaryPeer)(connectionId, rPeer, rPeerDevice, false, {
415
415
  protocol: "wrtc",
@@ -426,13 +426,13 @@ describe("binary-peer-connection-v2", () => {
426
426
  describe("backwards compatibility", () => {
427
427
  it("wrapWrtc should work as alias and set secure flags", async () => {
428
428
  const { iPeer, rPeer } = await getMockPeerPair();
429
- const userId = `00000000000000000user001`;
429
+ const userId = (0, utils_1.newid)();
430
430
  const keys = (0, keys_1.newKeys)();
431
431
  const connectionId = `00000000000000000conn001`;
432
- const iPeerDeviceId = `000000000000000device001`;
432
+ const iPeerDeviceId = (0, utils_1.newid)();
433
433
  const iPeerDevice = new device_1.Device(userId, iPeerDeviceId, keys);
434
434
  const iPeerConnection = (0, binary_peer_connection_v2_1.wrapWrtc)(connectionId, iPeer, iPeerDevice, true);
435
- const rPeerDeviceId = `000000000000000device002`;
435
+ const rPeerDeviceId = (0, utils_1.newid)();
436
436
  const rPeerDevice = new device_1.Device(userId, rPeerDeviceId, keys);
437
437
  const rPeerConnection = (0, binary_peer_connection_v2_1.wrapWrtc)(connectionId, rPeer, rPeerDevice, false);
438
438
  expect(iPeerConnection.secureLocal).toBe(true);
@@ -445,10 +445,10 @@ describe("binary-peer-connection-v2", () => {
445
445
  });
446
446
  it("binary peers should bypass StreamedSocket (handlesOwnEncoding)", async () => {
447
447
  const { iPeer } = await getMockPeerPair();
448
- const userId = `00000000000000000user001`;
448
+ const userId = (0, utils_1.newid)();
449
449
  const keys = (0, keys_1.newKeys)();
450
450
  const connectionId = `00000000000000000conn001`;
451
- const deviceId = `000000000000000device001`;
451
+ const deviceId = (0, utils_1.newid)();
452
452
  const device = new device_1.Device(userId, deviceId, keys);
453
453
  const connection = (0, binary_peer_connection_v2_1.wrapWrtc)(connectionId, iPeer, device, true);
454
454
  expect(connection.socket.handlesOwnEncoding).toBe(true);
@@ -78,15 +78,15 @@ describe("binary-peer-connection", () => {
78
78
  describe("wrapBinaryPeer", () => {
79
79
  it("should successfully handshake over wrtc protocol", async () => {
80
80
  const { iPeer, rPeer } = await getMockPeerPair();
81
- const userId = `00000000000000000user001`;
81
+ const userId = (0, utils_1.newid)();
82
82
  const keys = (0, keys_1.newKeys)();
83
83
  const connectionId = `00000000000000000conn001`;
84
- const iPeerDeviceId = `000000000000000device001`;
84
+ const iPeerDeviceId = (0, utils_1.newid)();
85
85
  const iPeerDevice = new device_1.Device(userId, iPeerDeviceId, keys);
86
86
  const iPeerConnection = (0, binary_peer_connection_1.wrapBinaryPeer)(connectionId, iPeer, iPeerDevice, true, {
87
87
  protocol: "wrtc",
88
88
  });
89
- const rPeerDeviceId = `000000000000000device002`;
89
+ const rPeerDeviceId = (0, utils_1.newid)();
90
90
  const rPeerDevice = new device_1.Device(userId, rPeerDeviceId, keys);
91
91
  const rPeerConnection = (0, binary_peer_connection_1.wrapBinaryPeer)(connectionId, rPeer, rPeerDevice, false, {
92
92
  protocol: "wrtc",
@@ -102,16 +102,16 @@ describe("binary-peer-connection", () => {
102
102
  });
103
103
  it("should set secureLocal/secureRemote when markTransportSecure is true", async () => {
104
104
  const { iPeer, rPeer } = await getMockPeerPair();
105
- const userId = `00000000000000000user001`;
105
+ const userId = (0, utils_1.newid)();
106
106
  const keys = (0, keys_1.newKeys)();
107
107
  const connectionId = `00000000000000000conn001`;
108
- const iPeerDeviceId = `000000000000000device001`;
108
+ const iPeerDeviceId = (0, utils_1.newid)();
109
109
  const iPeerDevice = new device_1.Device(userId, iPeerDeviceId, keys);
110
110
  const iPeerConnection = (0, binary_peer_connection_1.wrapBinaryPeer)(connectionId, iPeer, iPeerDevice, true, {
111
111
  protocol: "wrtc",
112
112
  markTransportSecure: true,
113
113
  });
114
- const rPeerDeviceId = `000000000000000device002`;
114
+ const rPeerDeviceId = (0, utils_1.newid)();
115
115
  const rPeerDevice = new device_1.Device(userId, rPeerDeviceId, keys);
116
116
  const rPeerConnection = (0, binary_peer_connection_1.wrapBinaryPeer)(connectionId, rPeer, rPeerDevice, false, {
117
117
  protocol: "wrtc",
@@ -129,13 +129,13 @@ describe("binary-peer-connection", () => {
129
129
  describe("backwards compatibility", () => {
130
130
  it("wrapWrtc should work as alias and set secure flags", async () => {
131
131
  const { iPeer, rPeer } = await getMockPeerPair();
132
- const userId = `00000000000000000user001`;
132
+ const userId = (0, utils_1.newid)();
133
133
  const keys = (0, keys_1.newKeys)();
134
134
  const connectionId = `00000000000000000conn001`;
135
- const iPeerDeviceId = `000000000000000device001`;
135
+ const iPeerDeviceId = (0, utils_1.newid)();
136
136
  const iPeerDevice = new device_1.Device(userId, iPeerDeviceId, keys);
137
137
  const iPeerConnection = (0, binary_peer_connection_1.wrapWrtc)(connectionId, iPeer, iPeerDevice, true);
138
- const rPeerDeviceId = `000000000000000device002`;
138
+ const rPeerDeviceId = (0, utils_1.newid)();
139
139
  const rPeerDevice = new device_1.Device(userId, rPeerDeviceId, keys);
140
140
  const rPeerConnection = (0, binary_peer_connection_1.wrapWrtc)(connectionId, rPeer, rPeerDevice, false);
141
141
  // wrapWrtc should set secure flags by default (transport-level encryption)
@@ -150,10 +150,10 @@ describe("binary-peer-connection", () => {
150
150
  });
151
151
  it("binary peers should bypass StreamedSocket (handlesOwnEncoding)", async () => {
152
152
  const { iPeer } = await getMockPeerPair();
153
- const userId = `00000000000000000user001`;
153
+ const userId = (0, utils_1.newid)();
154
154
  const keys = (0, keys_1.newKeys)();
155
155
  const connectionId = `00000000000000000conn001`;
156
- const deviceId = `000000000000000device001`;
156
+ const deviceId = (0, utils_1.newid)();
157
157
  const device = new device_1.Device(userId, deviceId, keys);
158
158
  const connection = (0, binary_peer_connection_1.wrapWrtc)(connectionId, iPeer, device, true);
159
159
  // The socket should NOT be wrapped in StreamedSocket
@@ -1,8 +1,10 @@
1
- import type { IDeviceHandshake, IDeviceInfo } from "../data";
1
+ import { type IDeviceHandshake, type IDeviceInfo } from "../data";
2
2
  import { type IDataBox } from "../keys";
3
3
  import type { Device } from "./device";
4
4
  import { type ISocket, TrustLevel } from "./socket.type";
5
+ /** Resolves the trust assigned to an authenticated remote device identity. */
5
6
  export type GetTrustLevel = (deviceInfo: IDeviceInfo, registerNew?: boolean) => Promise<TrustLevel>;
7
+ /** Authenticated RPC connection between two Peers devices. */
6
8
  export declare class Connection {
7
9
  readonly localDevice: Device;
8
10
  readonly localDeviceServerAddresses?: string[] | undefined;
@@ -35,10 +37,12 @@ export declare class Connection {
35
37
  */
36
38
  get encryptTraffic(): boolean;
37
39
  get remoteDeviceInfo(): {
38
- userId: string;
39
- deviceId: string;
40
40
  publicKey: string;
41
41
  publicBoxKey: string;
42
+ userId: string;
43
+ deviceId: string;
44
+ userName?: string | undefined;
45
+ deviceName?: string | undefined;
42
46
  };
43
47
  get connectionId(): string;
44
48
  /**
@@ -73,4 +77,5 @@ export declare class Connection {
73
77
  private closed;
74
78
  close(): Promise<void>;
75
79
  }
80
+ /** Normalizes a peer address for transport and security comparisons. */
76
81
  export declare function normalizeAddress(address: string): string;
@@ -2,10 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Connection = void 0;
4
4
  exports.normalizeAddress = normalizeAddress;
5
+ const data_1 = require("../data");
5
6
  const keys_1 = require("../keys");
6
7
  const utils_1 = require("../utils");
7
8
  const socket_type_1 = require("./socket.type");
8
9
  const HANDSHAKE_TIMESTAMP_TOLERANCE_MS = 300_000; // 5 minutes
10
+ /** Authenticated RPC connection between two Peers devices. */
9
11
  class Connection {
10
12
  localDevice;
11
13
  localDeviceServerAddresses;
@@ -280,7 +282,7 @@ class Connection {
280
282
  }
281
283
  async completeHandshake(boxedHandshake) {
282
284
  const signedHandshake = this.localDevice.openBoxWithSecretKey(boxedHandshake);
283
- const _handshake = signedHandshake.contents;
285
+ const _handshake = data_1.deviceHandshakeSchema.parse((0, keys_1.openSignedObject)(signedHandshake));
284
286
  const timeDiff = Math.abs(Date.now() - _handshake.timestamp);
285
287
  if (timeDiff > this.handshakeTimestampToleranceMs) {
286
288
  throw new Error(`Remote device's system clock is too far out of sync`);
@@ -299,8 +301,10 @@ class Connection {
299
301
  deviceId: _handshake.deviceId,
300
302
  publicKey: _handshake.publicKey,
301
303
  publicBoxKey: _handshake.publicBoxKey,
304
+ userName: _handshake.userName,
305
+ deviceName: _handshake.deviceName,
302
306
  };
303
- this.trustLevel = await this.getTrustLevel(signedHandshake.contents, true);
307
+ this.trustLevel = await this.getTrustLevel(_handshake, true);
304
308
  if (this.trustLevel < socket_type_1.TrustLevel.Unknown) {
305
309
  this.reset();
306
310
  this.emit("reset");
@@ -320,13 +324,13 @@ class Connection {
320
324
  this.reset();
321
325
  await this.emit("reset");
322
326
  const remoteDeviceInfoSigned = await this.emit("requestDeviceInfo");
323
- const remoteDeviceInfo = (0, keys_1.openSignedObject)(remoteDeviceInfoSigned);
327
+ const remoteDeviceInfo = data_1.deviceInfoSchema.parse((0, keys_1.openSignedObject)(remoteDeviceInfoSigned));
324
328
  if (remoteDeviceInfoSigned.publicKey !== remoteDeviceInfo.publicKey) {
325
329
  throw new Error("Device info signing key does not match claimed identity key");
326
330
  }
327
331
  const handshake = await this.initiateHandshake(remoteAddress, remoteDeviceInfo);
328
332
  const handshakeResponseBox = await this.emit("completeHandshake", handshake);
329
- const handshakeResponse = await this.localDevice.openBoxedAndSignedData(handshakeResponseBox);
333
+ const handshakeResponse = data_1.deviceHandshakeSchema.parse(await this.localDevice.openBoxedAndSignedData(handshakeResponseBox));
330
334
  if (handshakeResponse.connectionId !== this.connectionId) {
331
335
  throw new Error(`Invalid connectionId ${handshakeResponse.connectionId}, expected ${this.connectionId}`);
332
336
  }
@@ -338,6 +342,16 @@ class Connection {
338
342
  remoteDeviceInfo.deviceId !== handshakeResponse.deviceId) {
339
343
  throw new Error("Inconsistent device info");
340
344
  }
345
+ const currentRemoteDeviceInfo = {
346
+ ...remoteDeviceInfo,
347
+ userName: handshakeResponse.userName !== undefined
348
+ ? handshakeResponse.userName
349
+ : remoteDeviceInfo.userName,
350
+ deviceName: handshakeResponse.deviceName !== undefined
351
+ ? handshakeResponse.deviceName
352
+ : remoteDeviceInfo.deviceName,
353
+ };
354
+ this._remoteDeviceInfo = currentRemoteDeviceInfo;
341
355
  this._verified = true;
342
356
  this._connectionAddress = remoteAddress;
343
357
  const secureProtocols = ["https", "wss", "wrtc"];
@@ -348,7 +362,7 @@ class Connection {
348
362
  if (this.secureLocal) {
349
363
  await this.emit("requestSecure");
350
364
  }
351
- this.trustLevel = await this.getTrustLevel(remoteDeviceInfo, true);
365
+ this.trustLevel = await this.getTrustLevel(currentRemoteDeviceInfo, true);
352
366
  if (this.trustLevel < socket_type_1.TrustLevel.Unknown) {
353
367
  this.reset();
354
368
  this.emit("reset");
@@ -386,6 +400,7 @@ class Connection {
386
400
  }
387
401
  }
388
402
  exports.Connection = Connection;
403
+ /** Normalizes a peer address for transport and security comparisons. */
389
404
  function normalizeAddress(address) {
390
405
  return address.toLowerCase().replace(/\/$/, "");
391
406
  }
@@ -48,8 +48,14 @@ function createTestSocketPair() {
48
48
  describe(connection_1.Connection, () => {
49
49
  it("should allow simulating a client and server connection handshake", async () => {
50
50
  const { clientSocket, serverSocket } = createTestSocketPair();
51
- const clientDevice = new device_1.Device();
52
- const serverDevice = new device_1.Device();
51
+ const clientDevice = new device_1.Device(undefined, undefined, undefined, {
52
+ userName: "Client user",
53
+ deviceName: "Client laptop",
54
+ });
55
+ const serverDevice = new device_1.Device(undefined, undefined, undefined, {
56
+ userName: "Server user",
57
+ deviceName: "Server desktop",
58
+ });
53
59
  const clientConnection = new connection_1.Connection(clientSocket, clientDevice);
54
60
  const serverConnection = new connection_1.Connection(serverSocket, serverDevice, ["localhost"]);
55
61
  expect(clientConnection.connectionId).toBe(serverConnection.connectionId);
@@ -60,6 +66,16 @@ describe(connection_1.Connection, () => {
60
66
  expect(response.publicBoxKey).toBe(serverDevice.publicBoxKey);
61
67
  expect(response.serverAddress).toBe("localhost");
62
68
  expect(response.connectionId).toBe(serverConnection.connectionId);
69
+ expect(response.userName).toBe("Server user");
70
+ expect(response.deviceName).toBe("Server desktop");
71
+ expect(clientConnection.remoteDeviceInfo).toMatchObject({
72
+ userName: "Server user",
73
+ deviceName: "Server desktop",
74
+ });
75
+ expect(serverConnection.remoteDeviceInfo).toMatchObject({
76
+ userName: "Client user",
77
+ deviceName: "Client laptop",
78
+ });
63
79
  serverConnection.removeAllListeners("ping");
64
80
  serverConnection.exposeRPC("ping", async (n, s) => `pong - ${n}, ${s}`);
65
81
  const pong = await clientConnection.emit("ping", 1, "hello");
@@ -176,7 +192,7 @@ describe(connection_1.Connection, () => {
176
192
  serverConnection.socket.removeAllListeners("completeHandshake");
177
193
  serverConnection.exposeRPC("completeHandshake", async (handshakeBox) => {
178
194
  const handshake = await serverConnection.completeHandshake(handshakeBox);
179
- handshake.deviceId = "fake";
195
+ handshake.deviceId = (0, utils_1.newid)();
180
196
  return handshake;
181
197
  });
182
198
  const result = await clientConnection.doHandshake(serverAddress).catch((err) => String(err));
@@ -193,7 +209,7 @@ describe(connection_1.Connection, () => {
193
209
  serverConnection.socket.removeAllListeners("completeHandshake");
194
210
  serverConnection.exposeRPC("completeHandshake", async (handshakeBox) => {
195
211
  const handshake = await serverConnection.completeHandshake(handshakeBox);
196
- handshake.userId = "fake";
212
+ handshake.userId = (0, utils_1.newid)();
197
213
  return handshake;
198
214
  });
199
215
  const result = await clientConnection.doHandshake(serverAddress).catch((err) => String(err));
@@ -272,9 +288,11 @@ describe(connection_1.Connection, () => {
272
288
  const originalGetHandshake = clientDevice.getHandshake.bind(clientDevice);
273
289
  clientDevice.getHandshake = (connectionId, serverAddress) => {
274
290
  const handshake = originalGetHandshake(connectionId, serverAddress);
275
- // Make timestamp 2 seconds in the past (beyond tolerance)
276
- handshake.contents.timestamp = Date.now() - 2000;
277
- return handshake;
291
+ return clientDevice.signObjectWithSecretKey({
292
+ ...handshake.contents,
293
+ // Make timestamp 2 seconds in the past (beyond tolerance)
294
+ timestamp: Date.now() - 2000,
295
+ });
278
296
  };
279
297
  const result = await clientConnection
280
298
  .doHandshake("localhost")
@@ -1,10 +1,20 @@
1
- import type { IDeviceHandshake, IDeviceInfo } from "../data";
1
+ import { type IDeviceHandshake, type IDeviceInfo } from "../data";
2
2
  import { type IDataBox, type ISignedObject } from "../keys";
3
+ /** Optional presentation metadata advertised with signed device identity. */
4
+ export interface IDeviceDisplayInfo {
5
+ userName?: string;
6
+ deviceName?: string;
7
+ }
8
+ /** Runtime cryptographic identity for a Peers device. */
3
9
  export declare class Device implements IDeviceInfo {
4
10
  readonly userId: string;
5
11
  readonly deviceId: string;
6
12
  private readonly keys;
7
- constructor(userId?: string, deviceId?: string, keys?: import("../keys").IPublicPrivateKeys);
13
+ private _userName?;
14
+ private _deviceName?;
15
+ constructor(userId?: string, deviceId?: string, keys?: import("../keys").IPublicPrivateKeys, displayInfo?: IDeviceDisplayInfo);
16
+ get userName(): string | undefined;
17
+ get deviceName(): string | undefined;
8
18
  get publicKey(): string;
9
19
  get publicBoxKey(): string;
10
20
  signMessageWithSecretKey(msg: string): string;
@@ -16,8 +26,21 @@ export declare class Device implements IDeviceInfo {
16
26
  signAndBoxDataForKey<T>(data: T, toPublicBoxKey: string): IDataBox;
17
27
  unwrapResponse<T>(response: T | ISignedObject<T> | IDataBox): T;
18
28
  openBoxedAndSignedData<T>(data: IDataBox): T;
29
+ /** Updates bounded presentation metadata included in future signed identity messages. */
30
+ setDisplayInfo({ userName, deviceName }: IDeviceDisplayInfo): void;
19
31
  getHandshake(connectionId: string, serverAddress: string): ISignedObject<IDeviceHandshake>;
20
- handshakeResponse(remoteHandshake: ISignedObject<IDeviceHandshake>, connectionId: string, thisServerAddress: string): IDeviceHandshake;
32
+ handshakeResponse(remoteHandshake: ISignedObject<IDeviceHandshake>, connectionId: string, thisServerAddress: string): {
33
+ timestamp: number;
34
+ publicKey: string;
35
+ publicBoxKey: string;
36
+ userId: string;
37
+ deviceId: string;
38
+ connectionId: string;
39
+ serverAddress: string;
40
+ userName?: string | undefined;
41
+ deviceName?: string | undefined;
42
+ };
21
43
  private deviceInfoSigned;
44
+ /** Returns cached signed identity information, refreshing after display metadata changes. */
22
45
  getDeviceInfo(): ISignedObject<IDeviceInfo>;
23
46
  }
@@ -1,16 +1,27 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Device = void 0;
4
+ const data_1 = require("../data");
4
5
  const keys_1 = require("../keys");
5
6
  const utils_1 = require("../utils");
7
+ /** Runtime cryptographic identity for a Peers device. */
6
8
  class Device {
7
9
  userId;
8
10
  deviceId;
9
11
  keys;
10
- constructor(userId = (0, utils_1.newid)(), deviceId = (0, utils_1.newid)(), keys = (0, keys_1.newKeys)()) {
12
+ _userName;
13
+ _deviceName;
14
+ constructor(userId = (0, utils_1.newid)(), deviceId = (0, utils_1.newid)(), keys = (0, keys_1.newKeys)(), displayInfo = {}) {
11
15
  this.userId = userId;
12
16
  this.deviceId = deviceId;
13
17
  this.keys = keys;
18
+ this.setDisplayInfo(displayInfo);
19
+ }
20
+ get userName() {
21
+ return this._userName;
22
+ }
23
+ get deviceName() {
24
+ return this._deviceName;
14
25
  }
15
26
  get publicKey() {
16
27
  return this.keys.publicKey;
@@ -65,17 +76,32 @@ class Device {
65
76
  const signedData = this.openBoxWithSecretKey(data);
66
77
  return (0, keys_1.openSignedObject)(signedData);
67
78
  }
79
+ /** Updates bounded presentation metadata included in future signed identity messages. */
80
+ setDisplayInfo({ userName, deviceName }) {
81
+ userName = userName ? userName.slice(0, data_1.DISPLAY_NAME_MAX_LENGTH) : undefined;
82
+ deviceName = deviceName ? deviceName.slice(0, data_1.DISPLAY_NAME_MAX_LENGTH) : undefined;
83
+ data_1.displayNameSchema.optional().parse(userName);
84
+ data_1.displayNameSchema.optional().parse(deviceName);
85
+ if (this._userName === userName && this._deviceName === deviceName) {
86
+ return;
87
+ }
88
+ this._userName = userName;
89
+ this._deviceName = deviceName;
90
+ this.deviceInfoSigned = undefined;
91
+ }
68
92
  getHandshake(connectionId, serverAddress) {
69
93
  try {
70
- const localDeviceInfo = {
94
+ const localDeviceInfo = data_1.deviceHandshakeSchema.parse({
71
95
  connectionId,
72
96
  userId: this.userId,
73
97
  deviceId: this.deviceId,
74
98
  publicKey: this.publicKey,
75
99
  publicBoxKey: this.publicBoxKey,
100
+ userName: this.userName,
101
+ deviceName: this.deviceName,
76
102
  serverAddress,
77
103
  timestamp: Date.now(),
78
- };
104
+ });
79
105
  const localDeviceInfoSigned = this.signObjectWithSecretKey(localDeviceInfo);
80
106
  return localDeviceInfoSigned;
81
107
  }
@@ -85,22 +111,24 @@ class Device {
85
111
  }
86
112
  handshakeResponse(remoteHandshake, connectionId, thisServerAddress) {
87
113
  try {
88
- const deviceInfo = (0, keys_1.openSignedObject)(remoteHandshake);
114
+ const deviceInfo = data_1.deviceHandshakeSchema.parse((0, keys_1.openSignedObject)(remoteHandshake));
89
115
  if (remoteHandshake.publicKey !== deviceInfo.publicKey) {
90
116
  throw new Error("Handshake signing key does not match claimed identity key");
91
117
  }
92
118
  if (deviceInfo.connectionId !== connectionId) {
93
119
  throw new Error(`Invalid connectionId ${deviceInfo.connectionId}, expected ${connectionId}`);
94
120
  }
95
- const handshakeResponse = {
121
+ const handshakeResponse = data_1.deviceHandshakeSchema.parse({
96
122
  connectionId,
97
123
  userId: this.userId,
98
124
  deviceId: this.deviceId,
99
125
  publicKey: this.publicKey,
100
126
  publicBoxKey: this.publicBoxKey,
127
+ userName: this.userName,
128
+ deviceName: this.deviceName,
101
129
  serverAddress: thisServerAddress,
102
130
  timestamp: Date.now(),
103
- };
131
+ });
104
132
  return handshakeResponse;
105
133
  }
106
134
  catch (e) {
@@ -108,14 +136,18 @@ class Device {
108
136
  }
109
137
  }
110
138
  deviceInfoSigned;
139
+ /** Returns cached signed identity information, refreshing after display metadata changes. */
111
140
  getDeviceInfo() {
112
141
  if (!this.deviceInfoSigned) {
113
- this.deviceInfoSigned = this.signObjectWithSecretKey({
142
+ const deviceInfo = data_1.deviceInfoSchema.parse({
114
143
  userId: this.userId,
115
144
  deviceId: this.deviceId,
116
145
  publicKey: this.publicKey,
117
146
  publicBoxKey: this.publicBoxKey,
147
+ userName: this.userName,
148
+ deviceName: this.deviceName,
118
149
  });
150
+ this.deviceInfoSigned = this.signObjectWithSecretKey(deviceInfo);
119
151
  }
120
152
  return this.deviceInfoSigned;
121
153
  }
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const data_1 = require("../data");
3
4
  const keys_1 = require("../keys");
4
5
  const utils_1 = require("../utils");
5
6
  const device_1 = require("./device");
@@ -51,6 +52,35 @@ describe(device_1.Device, () => {
51
52
  expect(handshake.contents.publicBoxKey).toBe(client.publicBoxKey);
52
53
  expect(handshake.contents.connectionId).toBe(connectionId);
53
54
  });
55
+ it("includes bounded display metadata and refreshes cached device info", () => {
56
+ const device = new device_1.Device((0, utils_1.newid)(), (0, utils_1.newid)(), (0, keys_1.newKeys)(), {
57
+ userName: "Mark",
58
+ deviceName: "Office laptop",
59
+ });
60
+ const originalInfo = device.getDeviceInfo();
61
+ expect(originalInfo.contents).toMatchObject({
62
+ userName: "Mark",
63
+ deviceName: "Office laptop",
64
+ });
65
+ device.setDisplayInfo({ userName: "Mark Archer", deviceName: "Kitchen display" });
66
+ const refreshedInfo = device.getDeviceInfo();
67
+ const handshake = device.getHandshake((0, utils_1.newid)(), "localhost");
68
+ expect(refreshedInfo).not.toBe(originalInfo);
69
+ expect(refreshedInfo.contents).toMatchObject({
70
+ userName: "Mark Archer",
71
+ deviceName: "Kitchen display",
72
+ });
73
+ expect(handshake.contents).toMatchObject({
74
+ userName: "Mark Archer",
75
+ deviceName: "Kitchen display",
76
+ });
77
+ });
78
+ it("bounds invalid local display metadata before signing it", () => {
79
+ const device = new device_1.Device((0, utils_1.newid)(), (0, utils_1.newid)(), (0, keys_1.newKeys)(), {
80
+ deviceName: "x".repeat(data_1.DISPLAY_NAME_MAX_LENGTH + 1),
81
+ });
82
+ expect(device.getDeviceInfo().contents.deviceName).toHaveLength(data_1.DISPLAY_NAME_MAX_LENGTH);
83
+ });
54
84
  });
55
85
  describe("handshakeResponse", () => {
56
86
  it("should return a valid and correct handshake response object", async () => {
@@ -73,6 +103,20 @@ describe(device_1.Device, () => {
73
103
  const handshake = client.getHandshake(c1.connectionId, "localhost");
74
104
  expect(() => server.handshakeResponse(handshake, c2.connectionId, "localhost")).toThrow(/Invalid connectionId/);
75
105
  });
106
+ it("rejects signed handshake metadata that exceeds the display-name limit", () => {
107
+ const connectionId = (0, utils_1.newid)();
108
+ const handshake = client.signObjectWithSecretKey({
109
+ connectionId,
110
+ userId: client.userId,
111
+ deviceId: client.deviceId,
112
+ publicKey: client.publicKey,
113
+ publicBoxKey: client.publicBoxKey,
114
+ userName: "x".repeat(data_1.DISPLAY_NAME_MAX_LENGTH + 1),
115
+ serverAddress: "localhost",
116
+ timestamp: Date.now(),
117
+ });
118
+ expect(() => server.handshakeResponse(handshake, connectionId, "localhost")).toThrow();
119
+ });
76
120
  it("should reject a handshake signed by a different key than the one claimed in the payload (key confusion)", async () => {
77
121
  const connectionId = (0, utils_1.newid)();
78
122
  // Imposter holds a random secret key but claims the real client's publicKey/publicBoxKey.
@@ -4,6 +4,15 @@ exports.getTrustLevelFn = getTrustLevelFn;
4
4
  const context_1 = require("../context");
5
5
  const data_1 = require("../data");
6
6
  const socket_type_1 = require("./socket.type");
7
+ function applyReportedDeviceName(device, deviceInfo) {
8
+ if (deviceInfo.deviceName) {
9
+ device.reportedName = deviceInfo.deviceName;
10
+ }
11
+ }
12
+ function boundStoredDeviceNames(device) {
13
+ device.name = device.name?.slice(0, data_1.DISPLAY_NAME_MAX_LENGTH);
14
+ device.reportedName = device.reportedName?.slice(0, data_1.DISPLAY_NAME_MAX_LENGTH);
15
+ }
7
16
  /**
8
17
  * Build the connection-handshake trust evaluator for a local user identity.
9
18
  *
@@ -31,6 +40,8 @@ function getTrustLevelFn(me, serverUrl) {
31
40
  serverUrl,
32
41
  };
33
42
  device.lastSeen = new Date();
43
+ applyReportedDeviceName(device, deviceInfo);
44
+ boundStoredDeviceNames(device);
34
45
  const seenMoreThan2DaysAgo = Date.now() - device.firstSeen.getTime() > 1000 * 60 * 60 * 24 * 2;
35
46
  device.trustLevel = seenMoreThan2DaysAgo ? socket_type_1.TrustLevel.Trusted : socket_type_1.TrustLevel.NewDevice;
36
47
  console.log(`Updating my own device: ${deviceInfo.deviceId}`);
@@ -86,6 +97,8 @@ function getTrustLevelFn(me, serverUrl) {
86
97
  device?.trustLevel &&
87
98
  device.trustLevel >= socket_type_1.TrustLevel.Trusted) {
88
99
  device.lastSeen = new Date();
100
+ applyReportedDeviceName(device, deviceInfo);
101
+ boundStoredDeviceNames(device);
89
102
  await (0, data_1.Devices)(userDataContext).save(device, { restoreIfDeleted: true });
90
103
  return socket_type_1.TrustLevel.Trusted;
91
104
  }
@@ -113,6 +126,8 @@ function getTrustLevelFn(me, serverUrl) {
113
126
  device.trustLevel = socket_type_1.TrustLevel.Known;
114
127
  }
115
128
  }
129
+ applyReportedDeviceName(device, deviceInfo);
130
+ boundStoredDeviceNames(device);
116
131
  _trustLevel = device.trustLevel;
117
132
  let newUser = false;
118
133
  if (!user && !groupUser) {
@@ -123,7 +138,9 @@ function getTrustLevelFn(me, serverUrl) {
123
138
  userId: deviceInfo.userId,
124
139
  publicKey: deviceInfo.publicKey,
125
140
  publicBoxKey: deviceInfo.publicBoxKey,
126
- name: serverUrl || `Unnamed_${deviceInfo.userId.substring(20)}`,
141
+ name: (deviceInfo.userName ||
142
+ serverUrl ||
143
+ `Unnamed_${deviceInfo.userId.substring(20)}`).slice(0, data_1.DISPLAY_NAME_MAX_LENGTH),
127
144
  };
128
145
  _trustLevel = socket_type_1.TrustLevel.NewUser;
129
146
  newUser = true;
@@ -137,6 +137,24 @@ describe("getTrustLevelFn", () => {
137
137
  expect(savedDevice.lastSeen.getTime()).toBeGreaterThanOrEqual(before);
138
138
  expect(savedDevice.lastSeen.getTime()).toBeLessThanOrEqual(after);
139
139
  });
140
+ it("stores the owner-reported name without replacing a local device label", async () => {
141
+ const me = makeTestIdentity();
142
+ const deviceInfo = { ...me, deviceName: "Owner's laptop" };
143
+ const existingDevice = {
144
+ deviceId: me.deviceId,
145
+ userId: me.userId,
146
+ firstSeen: new Date(),
147
+ lastSeen: new Date(),
148
+ name: "My local label",
149
+ trustLevel: socket_type_1.TrustLevel.NewDevice,
150
+ };
151
+ const { devTable } = setupMocks(existingDevice);
152
+ await (0, get_trust_level_fn_1.getTrustLevelFn)(me)(deviceInfo);
153
+ expect(devTable.save).toHaveBeenCalledWith(expect.objectContaining({
154
+ name: "My local label",
155
+ reportedName: "Owner's laptop",
156
+ }), expect.anything());
157
+ });
140
158
  });
141
159
  // ─── Blocked device early exit ──────────────────────────────────────────────
142
160
  describe("explicitly blocked device (trustLevel < Unknown)", () => {
@@ -369,7 +387,11 @@ describe("getTrustLevelFn", () => {
369
387
  describe("new user and new device", () => {
370
388
  it("returns NewDevice and saves a user stub when both user and device are unseen", async () => {
371
389
  const me = makeTestIdentity();
372
- const other = makeTestIdentity();
390
+ const other = {
391
+ ...makeTestIdentity(),
392
+ userName: "Alice",
393
+ deviceName: "Alice's phone",
394
+ };
373
395
  const { devTable, usersTable } = setupMocks(null, null, null);
374
396
  const getTrustLevel = (0, get_trust_level_fn_1.getTrustLevelFn)(me, "https://server.example.com");
375
397
  const result = await getTrustLevel(other);
@@ -381,11 +403,12 @@ describe("getTrustLevelFn", () => {
381
403
  userId: other.userId,
382
404
  publicKey: other.publicKey,
383
405
  publicBoxKey: other.publicBoxKey,
384
- name: "https://server.example.com",
406
+ name: "Alice",
385
407
  }), expect.objectContaining({ restoreIfDeleted: true, weakInsert: true }));
386
408
  expect(devTable.save).toHaveBeenCalledWith(expect.objectContaining({
387
409
  deviceId: other.deviceId,
388
410
  userId: other.userId,
411
+ reportedName: "Alice's phone",
389
412
  serverUrl: "https://server.example.com",
390
413
  trustLevel: socket_type_1.TrustLevel.NewDevice,
391
414
  }), expect.objectContaining({ restoreIfDeleted: true, weakInsert: true }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-sdk",
3
- "version": "0.20.2",
3
+ "version": "0.20.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-sdk.git"