@peers-app/peers-sdk 0.20.1 → 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.
- package/dist/context/user-context.d.ts +6 -0
- package/dist/context/user-context.js +48 -8
- package/dist/data/devices.d.ts +66 -4
- package/dist/data/devices.js +26 -2
- package/dist/data/users.d.ts +6 -0
- package/dist/data/users.js +11 -1
- package/dist/device/binary-peer-connection-v2.test.js +11 -11
- package/dist/device/binary-peer-connection.test.js +11 -11
- package/dist/device/connection.d.ts +8 -3
- package/dist/device/connection.js +20 -5
- package/dist/device/connection.test.js +25 -7
- package/dist/device/device.d.ts +26 -3
- package/dist/device/device.js +39 -7
- package/dist/device/device.test.js +44 -0
- package/dist/device/get-trust-level-fn.js +18 -1
- package/dist/device/get-trust-level-fn.test.js +25 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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;
|
package/dist/data/devices.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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>;
|
package/dist/data/devices.js
CHANGED
|
@@ -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:
|
|
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");
|
package/dist/data/users.d.ts
CHANGED
|
@@ -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>;
|
package/dist/data/users.js
CHANGED
|
@@ -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:
|
|
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 =
|
|
380
|
+
const userId = (0, utils_1.newid)();
|
|
381
381
|
const keys = (0, keys_1.newKeys)();
|
|
382
382
|
const connectionId = `00000000000000000conn001`;
|
|
383
|
-
const iPeerDeviceId =
|
|
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 =
|
|
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 =
|
|
403
|
+
const userId = (0, utils_1.newid)();
|
|
404
404
|
const keys = (0, keys_1.newKeys)();
|
|
405
405
|
const connectionId = `00000000000000000conn001`;
|
|
406
|
-
const iPeerDeviceId =
|
|
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 =
|
|
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 =
|
|
429
|
+
const userId = (0, utils_1.newid)();
|
|
430
430
|
const keys = (0, keys_1.newKeys)();
|
|
431
431
|
const connectionId = `00000000000000000conn001`;
|
|
432
|
-
const iPeerDeviceId =
|
|
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 =
|
|
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 =
|
|
448
|
+
const userId = (0, utils_1.newid)();
|
|
449
449
|
const keys = (0, keys_1.newKeys)();
|
|
450
450
|
const connectionId = `00000000000000000conn001`;
|
|
451
|
-
const deviceId =
|
|
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 =
|
|
81
|
+
const userId = (0, utils_1.newid)();
|
|
82
82
|
const keys = (0, keys_1.newKeys)();
|
|
83
83
|
const connectionId = `00000000000000000conn001`;
|
|
84
|
-
const iPeerDeviceId =
|
|
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 =
|
|
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 =
|
|
105
|
+
const userId = (0, utils_1.newid)();
|
|
106
106
|
const keys = (0, keys_1.newKeys)();
|
|
107
107
|
const connectionId = `00000000000000000conn001`;
|
|
108
|
-
const iPeerDeviceId =
|
|
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 =
|
|
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 =
|
|
132
|
+
const userId = (0, utils_1.newid)();
|
|
133
133
|
const keys = (0, keys_1.newKeys)();
|
|
134
134
|
const connectionId = `00000000000000000conn001`;
|
|
135
|
-
const iPeerDeviceId =
|
|
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 =
|
|
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 =
|
|
153
|
+
const userId = (0, utils_1.newid)();
|
|
154
154
|
const keys = (0, keys_1.newKeys)();
|
|
155
155
|
const connectionId = `00000000000000000conn001`;
|
|
156
|
-
const deviceId =
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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")
|
package/dist/device/device.d.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import type
|
|
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
|
-
|
|
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):
|
|
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
|
}
|
package/dist/device/device.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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: "
|
|
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 }));
|