@peers-app/peers-sdk 0.30.2 → 0.30.4
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/contracts/system/mesh-metrics-types.d.ts +169 -0
- package/dist/contracts/system/mesh-metrics-types.js +2 -0
- package/dist/contracts/system/network-contracts.d.ts +8 -0
- package/dist/contracts/system/network-contracts.js +16 -0
- package/dist/data/files/files.js +0 -16
- package/dist/device/connection.d.ts +9 -1
- package/dist/device/connection.js +25 -4
- package/dist/device/socket-io-binary-peer.d.ts +1 -0
- package/dist/device/socket-io-binary-peer.js +21 -5
- package/dist/rpc-types.d.ts +0 -22
- package/dist/rpc-types.js +0 -10
- package/dist/types/peer-device.d.ts +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/** Trigger for an overlapping, at most five-minute connectivity measurement. */
|
|
2
|
+
export type MeshEpisodeReason = "startup" | "resume" | "network-change" | "connection-loss";
|
|
3
|
+
/** Provenance of a dial; explicit requests and group joins are also distinguished. */
|
|
4
|
+
export type MeshDialReason = "own-device" | "group-network-info" | "broadcast-answer" | "configured-server" | "lan-scan" | "websocket-upgrade" | "incoming" | "direct-request" | "group-join";
|
|
5
|
+
/** Bounded path categories used for lifetime counters; attempts also retain the address. */
|
|
6
|
+
export type MeshPath = "websocket" | "webrtc" | "cloud" | "discovery";
|
|
7
|
+
/** Failure categories, independent of free-form transport error messages. */
|
|
8
|
+
export type MeshFailureReason = "ttl0" | "no-route" | "signaling-timeout" | "socket-error" | "socket-timeout" | "handshake" | "rejected" | "closed" | "superseded" | "unsupported" | "sender-not-known" | "trust-refused" | "identity-mismatch" | "cancelled" | "capacity";
|
|
9
|
+
/** Observed stage; timestamps are offsets in milliseconds from the attempt's queued time. */
|
|
10
|
+
export type MeshStage = "queued" | "started" | "authenticated" | "admitted" | "ready" | "firstSync";
|
|
11
|
+
/** Local dial metadata and cancellation; never serialized into device messages. */
|
|
12
|
+
export interface IMeshDial {
|
|
13
|
+
/** Cancels pending transport work; an admitted winner is retained. */
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
/** Scheduler priority: bootstrap, coverage, repair, then redundancy. */
|
|
16
|
+
priority?: 1 | 2 | 3 | 4;
|
|
17
|
+
attemptId?: string;
|
|
18
|
+
reason?: MeshDialReason;
|
|
19
|
+
targetDeviceId?: string;
|
|
20
|
+
/** One-based retry number; absent for the initial dial. */
|
|
21
|
+
retry?: number;
|
|
22
|
+
}
|
|
23
|
+
/** One dial or inbound connection. Missing stages have not been observed. */
|
|
24
|
+
export interface IMeshAttempt {
|
|
25
|
+
attemptId: string;
|
|
26
|
+
targetDeviceId?: string;
|
|
27
|
+
reason: MeshDialReason;
|
|
28
|
+
/** One-based retry number; absent for initial and incoming attempts. */
|
|
29
|
+
retry?: number;
|
|
30
|
+
/** Scheduler priority when this was planned locally. */
|
|
31
|
+
priority?: 1 | 2 | 3 | 4;
|
|
32
|
+
direction: "outgoing" | "incoming";
|
|
33
|
+
path: MeshPath;
|
|
34
|
+
address?: string;
|
|
35
|
+
queuedAt: number;
|
|
36
|
+
stages: Partial<Record<MeshStage, number>>;
|
|
37
|
+
answeredDeviceId?: string;
|
|
38
|
+
expectedIdentity?: boolean;
|
|
39
|
+
answered: boolean;
|
|
40
|
+
lastSignalFailure?: {
|
|
41
|
+
reason: "ttl0" | "no-route";
|
|
42
|
+
detail?: string;
|
|
43
|
+
};
|
|
44
|
+
ready: Record<string, number>;
|
|
45
|
+
firstSync: Record<string, number>;
|
|
46
|
+
outcome: "pending" | "admitted" | "failed" | "cancelled" | "unfinished";
|
|
47
|
+
endReason?: "buffer-evicted" | "disposed";
|
|
48
|
+
endedAt?: number;
|
|
49
|
+
durationMs?: number;
|
|
50
|
+
failure?: {
|
|
51
|
+
stage: MeshStage;
|
|
52
|
+
reason: MeshFailureReason;
|
|
53
|
+
detail?: string;
|
|
54
|
+
};
|
|
55
|
+
doubleDial: boolean;
|
|
56
|
+
offline: boolean;
|
|
57
|
+
outOfOrder: boolean;
|
|
58
|
+
}
|
|
59
|
+
/** Millisecond durations derived from one episode; absent means not observed. */
|
|
60
|
+
export interface IMeshDurations {
|
|
61
|
+
T_first?: number;
|
|
62
|
+
T_own?: number;
|
|
63
|
+
T_cover?: number;
|
|
64
|
+
T_resume?: number;
|
|
65
|
+
repairStart?: number;
|
|
66
|
+
}
|
|
67
|
+
/** Coverage at the latest observation (at completion for a finished episode). */
|
|
68
|
+
export interface IMeshContextCoverage {
|
|
69
|
+
dataContextId: string;
|
|
70
|
+
possibleSourceDeviceIds: string[];
|
|
71
|
+
usableDeviceIds: string[];
|
|
72
|
+
excluded: boolean;
|
|
73
|
+
/** True when an unmet context cannot currently fit at the connection cap. */
|
|
74
|
+
limitedByCapacity?: boolean;
|
|
75
|
+
/** Planner target; zero means no possible source. */
|
|
76
|
+
target?: number;
|
|
77
|
+
}
|
|
78
|
+
/** One connectivity episode, independent of any other episode in flight. */
|
|
79
|
+
export interface IMeshEpisode {
|
|
80
|
+
episodeId: string;
|
|
81
|
+
reason: MeshEpisodeReason;
|
|
82
|
+
trigger?: string;
|
|
83
|
+
startedAt: number;
|
|
84
|
+
endedAt?: number;
|
|
85
|
+
durationMs?: number;
|
|
86
|
+
outcome: "active" | "covered" | "unfinished" | "not-measured";
|
|
87
|
+
endReason?: "covered" | "deadline" | "buffer-evicted" | "disposed" | "page-hidden" | "still-covered";
|
|
88
|
+
metrics: IMeshDurations;
|
|
89
|
+
contexts: IMeshContextCoverage[];
|
|
90
|
+
}
|
|
91
|
+
/** Nearest-rank percentiles of observed values only; count exposes the denominator. */
|
|
92
|
+
export interface IMeshPercentiles {
|
|
93
|
+
count: number;
|
|
94
|
+
p50?: number;
|
|
95
|
+
p90?: number;
|
|
96
|
+
p95?: number;
|
|
97
|
+
p99?: number;
|
|
98
|
+
}
|
|
99
|
+
/** Lifetime dial totals. Incoming connections are separate from the outgoing-dial ratio. */
|
|
100
|
+
export interface IMeshPathCounters {
|
|
101
|
+
dials: number;
|
|
102
|
+
connections: number;
|
|
103
|
+
incomingConnections: number;
|
|
104
|
+
dialsPerConnection: number | null;
|
|
105
|
+
failures: Partial<Record<MeshFailureReason, number>>;
|
|
106
|
+
}
|
|
107
|
+
/** Shared relay totals and current gauges, also exposed on non-Services hosts. */
|
|
108
|
+
export interface IMeshServicesCounters {
|
|
109
|
+
directRelays: number;
|
|
110
|
+
floodsForwarded: number;
|
|
111
|
+
floodsPruned: number;
|
|
112
|
+
pendingEmits: number;
|
|
113
|
+
concurrentConnections: number;
|
|
114
|
+
connects: number;
|
|
115
|
+
disconnects: number;
|
|
116
|
+
}
|
|
117
|
+
/** In-memory diagnostics snapshot. No history survives a runtime restart. */
|
|
118
|
+
export interface IMeshStats {
|
|
119
|
+
startedAt: number;
|
|
120
|
+
sampledAt: number;
|
|
121
|
+
uptimeMs: number;
|
|
122
|
+
readyToDialMs?: number;
|
|
123
|
+
limits: {
|
|
124
|
+
episodes: number;
|
|
125
|
+
attempts: number;
|
|
126
|
+
episodeMs: number;
|
|
127
|
+
};
|
|
128
|
+
currentEpisodes: IMeshEpisode[];
|
|
129
|
+
recentEpisodes: IMeshEpisode[];
|
|
130
|
+
percentiles: Record<keyof IMeshDurations, IMeshPercentiles>;
|
|
131
|
+
counters: {
|
|
132
|
+
paths: Record<MeshPath, IMeshPathCounters>;
|
|
133
|
+
lanScan: {
|
|
134
|
+
scans: number;
|
|
135
|
+
probes: number;
|
|
136
|
+
connected: number;
|
|
137
|
+
};
|
|
138
|
+
/** Lifetime episodes excluded from latency percentiles, by reason. */
|
|
139
|
+
unmeasuredEpisodes?: {
|
|
140
|
+
pageHidden: number;
|
|
141
|
+
stillCovered: number;
|
|
142
|
+
};
|
|
143
|
+
unfinishedAttempts: number;
|
|
144
|
+
doubleDials: number;
|
|
145
|
+
offlineDials: number;
|
|
146
|
+
offlineDialsPerHour: number;
|
|
147
|
+
broadcasts: {
|
|
148
|
+
sent: number;
|
|
149
|
+
answered: number;
|
|
150
|
+
ttl0: number;
|
|
151
|
+
failed: number;
|
|
152
|
+
pending: number;
|
|
153
|
+
perHour: number;
|
|
154
|
+
};
|
|
155
|
+
services: IMeshServicesCounters;
|
|
156
|
+
};
|
|
157
|
+
recentAttempts: IMeshAttempt[];
|
|
158
|
+
recentBroadcasts: IMeshBroadcast[];
|
|
159
|
+
/** Latest planner coverage, including zero targets and capacity limitations. */
|
|
160
|
+
coverage?: IMeshContextCoverage[];
|
|
161
|
+
}
|
|
162
|
+
/** A discovery broadcast and its observed reply, bounded with the attempt history. */
|
|
163
|
+
export interface IMeshBroadcast {
|
|
164
|
+
broadcastId: string;
|
|
165
|
+
dataContextId?: string;
|
|
166
|
+
sentAt: number;
|
|
167
|
+
durationMs?: number;
|
|
168
|
+
outcome: "pending" | "answered" | "ttl0" | "failed";
|
|
169
|
+
}
|
|
@@ -4,12 +4,15 @@ import type { Observable } from "../../observable";
|
|
|
4
4
|
import type { INetworkInfo } from "../../types/peer-device";
|
|
5
5
|
import type { IContractConsumerBase } from "../contract-proxy";
|
|
6
6
|
import type { IContractDefinition } from "../types";
|
|
7
|
+
import type { IMeshStats } from "./mesh-metrics-types";
|
|
8
|
+
export type * from "./mesh-metrics-types";
|
|
7
9
|
/** Version of the built-in System Network Diagnostics contract. */
|
|
8
10
|
export declare const NETWORK_DIAGNOSTICS_CONTRACT_VERSION = 1;
|
|
9
11
|
/** Version of the built-in System Network Control contract. */
|
|
10
12
|
export declare const NETWORK_CONTROL_CONTRACT_VERSION = 1;
|
|
11
13
|
/** Stable IDs for tools in System Network Diagnostics v1. */
|
|
12
14
|
export declare const networkDiagnosticsToolIds: {
|
|
15
|
+
readonly getMeshStats: string;
|
|
13
16
|
readonly getOverview: "00ms1yb50ym353ads0gel5jpv";
|
|
14
17
|
readonly getConnections: "00ms1yb50ykny9kdgiclpa1t4";
|
|
15
18
|
readonly getGroupSyncStatus: "00ms1yb50yp2q7jr0jglzsztq";
|
|
@@ -241,6 +244,11 @@ export interface INetworkDiagnosticsConsumer extends IContractConsumerBase {
|
|
|
241
244
|
getOverview(): Promise<{
|
|
242
245
|
overview: INetworkOverview;
|
|
243
246
|
}>;
|
|
247
|
+
getMeshStats(input?: {
|
|
248
|
+
limit?: number;
|
|
249
|
+
}): Promise<{
|
|
250
|
+
stats: IMeshStats;
|
|
251
|
+
}>;
|
|
244
252
|
getConnections(): Promise<{
|
|
245
253
|
connections: INetworkConnectionInfo[];
|
|
246
254
|
}>;
|
|
@@ -6,12 +6,18 @@ exports.getNetworkControlContractDefinition = getNetworkControlContractDefinitio
|
|
|
6
6
|
const system_ids_1 = require("../../system-ids");
|
|
7
7
|
const field_type_1 = require("../../types/field-type");
|
|
8
8
|
const system_contracts_1 = require("../system-contracts");
|
|
9
|
+
const tool_catalog_identity_1 = require("../tool-catalog-identity");
|
|
9
10
|
/** Version of the built-in System Network Diagnostics contract. */
|
|
10
11
|
exports.NETWORK_DIAGNOSTICS_CONTRACT_VERSION = 1;
|
|
11
12
|
/** Version of the built-in System Network Control contract. */
|
|
12
13
|
exports.NETWORK_CONTROL_CONTRACT_VERSION = 1;
|
|
13
14
|
/** Stable IDs for tools in System Network Diagnostics v1. */
|
|
14
15
|
exports.networkDiagnosticsToolIds = {
|
|
16
|
+
getMeshStats: (0, tool_catalog_identity_1.deriveToolCatalogId)({
|
|
17
|
+
contractId: system_ids_1.networkDiagnosticsContractId,
|
|
18
|
+
version: exports.NETWORK_DIAGNOSTICS_CONTRACT_VERSION,
|
|
19
|
+
toolName: "getMeshStats",
|
|
20
|
+
}),
|
|
15
21
|
getOverview: "00ms1yb50ym353ads0gel5jpv",
|
|
16
22
|
getConnections: "00ms1yb50ykny9kdgiclpa1t4",
|
|
17
23
|
getGroupSyncStatus: "00ms1yb50yp2q7jr0jglzsztq",
|
|
@@ -46,6 +52,16 @@ function getNetworkDiagnosticsContractDefinition() {
|
|
|
46
52
|
description: "Portable local network, connection, and synchronization diagnostics.",
|
|
47
53
|
tables: [],
|
|
48
54
|
tools: [
|
|
55
|
+
{
|
|
56
|
+
name: "getMeshStats",
|
|
57
|
+
usageDescription: "Read bounded in-memory connectivity episodes, attempts, percentiles and relay counters.",
|
|
58
|
+
inputFields: [
|
|
59
|
+
field("limit", field_type_1.FieldType.number, "Maximum recent attempts and broadcasts (default 50, range 0–500). Episodes and counters are always complete.", { optional: true }),
|
|
60
|
+
],
|
|
61
|
+
outputFields: [
|
|
62
|
+
field("stats", field_type_1.FieldType.object, "Connectivity measurements since runtime startup."),
|
|
63
|
+
],
|
|
64
|
+
},
|
|
49
65
|
{
|
|
50
66
|
name: "getOverview",
|
|
51
67
|
usageDescription: "Return aggregate network and group information.",
|
package/dist/data/files/files.js
CHANGED
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.FilesTable = void 0;
|
|
4
4
|
exports.Files = Files;
|
|
5
5
|
const user_context_singleton_1 = require("../../context/user-context-singleton");
|
|
6
|
-
const rpc_types_1 = require("../../rpc-types");
|
|
7
6
|
const orm_1 = require("../orm");
|
|
8
7
|
const table_definitions_system_1 = require("../orm/table-definitions.system");
|
|
9
8
|
const chunk_store_1 = require("./chunk-store");
|
|
@@ -166,18 +165,3 @@ exports.FilesTable = FilesTable;
|
|
|
166
165
|
function Files(dataContext) {
|
|
167
166
|
return (0, user_context_singleton_1.getTableContainer)(dataContext).getTable(file_types_1.filesMetaData, file_types_1.fileSchema);
|
|
168
167
|
}
|
|
169
|
-
// Deprecated RPCs, kept only for Family Hub 0.3.x until its port is Home's version. New code
|
|
170
|
-
// uses System Files, which is bound to the caller's own data context.
|
|
171
|
-
rpc_types_1.rpcServerCalls.getFileContentsBase64 = async (fileId) => {
|
|
172
|
-
const data = await Files().getFileContents(fileId);
|
|
173
|
-
if (data === null) {
|
|
174
|
-
throw new Error(`File not found: ${fileId}`);
|
|
175
|
-
}
|
|
176
|
-
return Buffer.from(data).toString("base64");
|
|
177
|
-
};
|
|
178
|
-
rpc_types_1.rpcServerCalls.saveFile = async (input) => {
|
|
179
|
-
const encoding = input.encoding || "utf8";
|
|
180
|
-
const dataBuffer = encoding === "base64" ? Buffer.from(input.data, "base64") : Buffer.from(input.data, "utf8");
|
|
181
|
-
// Kept: Family Hub 0.3.x doesn't declare its file fields, so nothing would reference these.
|
|
182
|
-
return await Files().saveFile({ name: input.name, mimeType: input.mimeType, keep: true }, new Uint8Array(dataBuffer));
|
|
183
|
-
};
|
|
@@ -21,6 +21,9 @@ export declare class Connection {
|
|
|
21
21
|
private _remoteDeviceInfo;
|
|
22
22
|
/** Trust assigned to the remote identity once the handshake completes. */
|
|
23
23
|
trustLevel: TrustLevel;
|
|
24
|
+
/** Synchronous diagnostics only; observer errors cannot affect handshake results. */
|
|
25
|
+
onHandshakeObserved?: (stage: "authenticated" | "failed", error?: unknown) => void;
|
|
26
|
+
private observeHandshake;
|
|
24
27
|
/** Server-side hook fired shortly after the successful handshake reply has been sent. */
|
|
25
28
|
onHandshakeComplete: ((deviceInfo: IDeviceInfo) => void) | undefined;
|
|
26
29
|
/** RPC timeout for {@link Connection.emit}. */
|
|
@@ -39,6 +42,8 @@ export declare class Connection {
|
|
|
39
42
|
private pendingSendArm;
|
|
40
43
|
/** Bumped by every {@link Connection.reset}, so a handshake can tell it was cut short. */
|
|
41
44
|
private handshakeGeneration;
|
|
45
|
+
/** Local close intent, retained through reset so retry owners do not penalize orderly shedding. */
|
|
46
|
+
closeIntent?: "remote" | "policy";
|
|
42
47
|
constructor(socket: ISocket, localDevice: Device, localDeviceServerAddresses?: string[] | undefined, getTrustLevel?: GetTrustLevel);
|
|
43
48
|
/** True once the remote identity has been authenticated by the handshake. */
|
|
44
49
|
get verified(): boolean;
|
|
@@ -148,5 +153,8 @@ export declare class Connection {
|
|
|
148
153
|
*/
|
|
149
154
|
close(): Promise<void>;
|
|
150
155
|
}
|
|
151
|
-
/**
|
|
156
|
+
/** Lowercases a peer address and removes a trailing slash; preserves its scheme. */
|
|
152
157
|
export declare function normalizeAddress(address: string): string;
|
|
158
|
+
/** Local handshake refusal. Its existing Error text remains compatible with older peers. */
|
|
159
|
+
export declare class ConnectionTrustError extends Error {
|
|
160
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Connection = void 0;
|
|
3
|
+
exports.ConnectionTrustError = exports.Connection = void 0;
|
|
4
4
|
exports.normalizeAddress = normalizeAddress;
|
|
5
5
|
const user_context_singleton_1 = require("../context/user-context-singleton");
|
|
6
6
|
const data_1 = require("../data");
|
|
@@ -32,6 +32,16 @@ class Connection {
|
|
|
32
32
|
};
|
|
33
33
|
/** Trust assigned to the remote identity once the handshake completes. */
|
|
34
34
|
trustLevel = socket_type_1.TrustLevel.Unknown;
|
|
35
|
+
/** Synchronous diagnostics only; observer errors cannot affect handshake results. */
|
|
36
|
+
onHandshakeObserved;
|
|
37
|
+
observeHandshake(stage, error) {
|
|
38
|
+
try {
|
|
39
|
+
this.onHandshakeObserved?.(stage, error);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* diagnostics never affect the handshake */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
35
45
|
/** Server-side hook fired shortly after the successful handshake reply has been sent. */
|
|
36
46
|
onHandshakeComplete;
|
|
37
47
|
/** RPC timeout for {@link Connection.emit}. */
|
|
@@ -52,6 +62,8 @@ class Connection {
|
|
|
52
62
|
pendingSendArm;
|
|
53
63
|
/** Bumped by every {@link Connection.reset}, so a handshake can tell it was cut short. */
|
|
54
64
|
handshakeGeneration = 0;
|
|
65
|
+
/** Local close intent, retained through reset so retry owners do not penalize orderly shedding. */
|
|
66
|
+
closeIntent;
|
|
55
67
|
constructor(socket, localDevice, localDeviceServerAddresses, getTrustLevel = () => Promise.resolve(socket_type_1.TrustLevel.Unknown)) {
|
|
56
68
|
this.localDevice = localDevice;
|
|
57
69
|
this.localDeviceServerAddresses = localDeviceServerAddresses;
|
|
@@ -66,6 +78,7 @@ class Connection {
|
|
|
66
78
|
socket.setPlaintextStreamsAllowed?.(false);
|
|
67
79
|
// Only the server side of a connection should expose these methods
|
|
68
80
|
this.exposeRPC("close", () => {
|
|
81
|
+
this.closeIntent ??= "remote";
|
|
69
82
|
(0, utils_1.sleep)(100).then(() => {
|
|
70
83
|
this.closeLocal();
|
|
71
84
|
});
|
|
@@ -133,6 +146,7 @@ class Connection {
|
|
|
133
146
|
}
|
|
134
147
|
}
|
|
135
148
|
catch (e) {
|
|
149
|
+
this.observeHandshake("failed", e);
|
|
136
150
|
console.error("Error handling completeHandshake", e);
|
|
137
151
|
callback({ error: e?.message ?? String(e), errorType: "RPC_ERROR" });
|
|
138
152
|
}
|
|
@@ -393,7 +407,7 @@ class Connection {
|
|
|
393
407
|
this.assertHandshakeCurrent(generation);
|
|
394
408
|
if (trustLevel < socket_type_1.TrustLevel.Unknown) {
|
|
395
409
|
this.resetRemote();
|
|
396
|
-
throw new
|
|
410
|
+
throw new ConnectionTrustError("Untrusted connection");
|
|
397
411
|
}
|
|
398
412
|
// The client seals everything after it reads our response, so receive
|
|
399
413
|
// can arm now; send arms after the response frame goes out.
|
|
@@ -412,6 +426,7 @@ class Connection {
|
|
|
412
426
|
};
|
|
413
427
|
this.trustLevel = trustLevel;
|
|
414
428
|
this._verified = true;
|
|
429
|
+
this.observeHandshake("authenticated");
|
|
415
430
|
return handshakeResponse;
|
|
416
431
|
}
|
|
417
432
|
/**
|
|
@@ -454,11 +469,12 @@ class Connection {
|
|
|
454
469
|
this.trustLevel = trustLevel;
|
|
455
470
|
if (this.trustLevel < socket_type_1.TrustLevel.Unknown) {
|
|
456
471
|
this.resetRemote();
|
|
457
|
-
throw new
|
|
472
|
+
throw new ConnectionTrustError("Untrusted connection");
|
|
458
473
|
}
|
|
459
474
|
return handshakeResponse;
|
|
460
475
|
}
|
|
461
476
|
catch (e) {
|
|
477
|
+
this.observeHandshake("failed", e);
|
|
462
478
|
// A failed or timed-out attempt must neither retain its ephemeral secret nor let a
|
|
463
479
|
// late reply authenticate it. Do not clear state belonging to a newer handshake.
|
|
464
480
|
if (generation === this.handshakeGeneration)
|
|
@@ -522,6 +538,7 @@ class Connection {
|
|
|
522
538
|
: remoteDeviceInfo.deviceName,
|
|
523
539
|
};
|
|
524
540
|
this._verified = true;
|
|
541
|
+
this.observeHandshake("authenticated");
|
|
525
542
|
this._connectionAddress = remoteAddress;
|
|
526
543
|
return handshakeResponse;
|
|
527
544
|
}
|
|
@@ -599,7 +616,11 @@ async function noteHandshakeContest(contest) {
|
|
|
599
616
|
console.warn("Could not apply a key contest from the handshake", err);
|
|
600
617
|
}
|
|
601
618
|
}
|
|
602
|
-
/**
|
|
619
|
+
/** Lowercases a peer address and removes a trailing slash; preserves its scheme. */
|
|
603
620
|
function normalizeAddress(address) {
|
|
604
621
|
return address.toLowerCase().replace(/\/$/, "");
|
|
605
622
|
}
|
|
623
|
+
/** Local handshake refusal. Its existing Error text remains compatible with older peers. */
|
|
624
|
+
class ConnectionTrustError extends Error {
|
|
625
|
+
}
|
|
626
|
+
exports.ConnectionTrustError = ConnectionTrustError;
|
|
@@ -14,6 +14,7 @@ import type { IBinaryPeer } from "./binary-peer-connection-v2";
|
|
|
14
14
|
interface ISocketIO {
|
|
15
15
|
id?: string;
|
|
16
16
|
on(event: string, handler: (...args: any[]) => void): void;
|
|
17
|
+
off(event: string, handler: (...args: any[]) => void): void;
|
|
17
18
|
emit(event: string, ...args: any[]): void;
|
|
18
19
|
removeAllListeners(event?: string): void;
|
|
19
20
|
disconnect(): void;
|
|
@@ -22,8 +22,11 @@ function wrapSocketIOAsBinaryPeer(socket) {
|
|
|
22
22
|
const closeHandlers = [];
|
|
23
23
|
const drainHandlers = [];
|
|
24
24
|
let pendingBytes = 0;
|
|
25
|
+
let closed = false;
|
|
25
26
|
// Listen for binary data
|
|
26
|
-
|
|
27
|
+
const onData = (data) => {
|
|
28
|
+
if (closed)
|
|
29
|
+
return;
|
|
27
30
|
// Ensure we have a proper Uint8Array
|
|
28
31
|
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
29
32
|
dataHandlers.forEach((handler) => {
|
|
@@ -34,9 +37,16 @@ function wrapSocketIOAsBinaryPeer(socket) {
|
|
|
34
37
|
console.error("[socket-io-binary] Error in data handler:", err);
|
|
35
38
|
}
|
|
36
39
|
});
|
|
37
|
-
}
|
|
40
|
+
};
|
|
38
41
|
// Listen for disconnect
|
|
39
|
-
|
|
42
|
+
const onDisconnect = () => {
|
|
43
|
+
if (closed)
|
|
44
|
+
return;
|
|
45
|
+
closed = true;
|
|
46
|
+
// This adapter belongs to one connection incarnation. A delayed close from it
|
|
47
|
+
// must never disconnect a reconnected Socket.IO socket or remove its listeners.
|
|
48
|
+
socket.off(BINARY_EVENT, onData);
|
|
49
|
+
socket.off("disconnect", onDisconnect);
|
|
40
50
|
closeHandlers.forEach((handler) => {
|
|
41
51
|
try {
|
|
42
52
|
handler();
|
|
@@ -45,7 +55,9 @@ function wrapSocketIOAsBinaryPeer(socket) {
|
|
|
45
55
|
console.error("[socket-io-binary] Error in close handler:", err);
|
|
46
56
|
}
|
|
47
57
|
});
|
|
48
|
-
}
|
|
58
|
+
};
|
|
59
|
+
socket.on(BINARY_EVENT, onData);
|
|
60
|
+
socket.on("disconnect", onDisconnect);
|
|
49
61
|
return {
|
|
50
62
|
on(event, handler) {
|
|
51
63
|
if (event === "data") {
|
|
@@ -59,6 +71,8 @@ function wrapSocketIOAsBinaryPeer(socket) {
|
|
|
59
71
|
}
|
|
60
72
|
},
|
|
61
73
|
send(data) {
|
|
74
|
+
if (closed)
|
|
75
|
+
return;
|
|
62
76
|
const bytes = typeof data === "string"
|
|
63
77
|
? Buffer.from(data)
|
|
64
78
|
: data instanceof Uint8Array
|
|
@@ -79,7 +93,9 @@ function wrapSocketIOAsBinaryPeer(socket) {
|
|
|
79
93
|
});
|
|
80
94
|
},
|
|
81
95
|
destroy() {
|
|
82
|
-
|
|
96
|
+
if (closed)
|
|
97
|
+
return;
|
|
98
|
+
onDisconnect();
|
|
83
99
|
socket.disconnect();
|
|
84
100
|
},
|
|
85
101
|
getBufferedAmount() {
|
package/dist/rpc-types.d.ts
CHANGED
|
@@ -29,28 +29,6 @@ export interface IRemoteLogsQueryInput {
|
|
|
29
29
|
export declare const rpcServerCalls: {
|
|
30
30
|
encryptData: (value: string, groupId?: string) => Promise<string>;
|
|
31
31
|
tableMethodCall: (dataContextId: string, tableName: string, methodName: string, ...args: any[]) => Promise<any>;
|
|
32
|
-
/**
|
|
33
|
-
* @deprecated Use System Files (`SystemFiles`, `readWholeFile`). Kept only for Family Hub
|
|
34
|
-
* 0.3.x; deleted once its port is Home's version.
|
|
35
|
-
*/
|
|
36
|
-
getFileContentsBase64: (fileId: string) => Promise<string>;
|
|
37
|
-
/**
|
|
38
|
-
* @deprecated Use System Files (`SystemFiles`). Kept only for Family Hub 0.3.x; deleted once
|
|
39
|
-
* its port is Home's version.
|
|
40
|
-
*/
|
|
41
|
-
saveFile: (input: {
|
|
42
|
-
name: string;
|
|
43
|
-
mimeType?: string;
|
|
44
|
-
data: string;
|
|
45
|
-
encoding?: "utf8" | "base64";
|
|
46
|
-
fileId?: string;
|
|
47
|
-
}) => Promise<{
|
|
48
|
-
fileId: string;
|
|
49
|
-
name: string;
|
|
50
|
-
fileSize: number;
|
|
51
|
-
fileHash: string;
|
|
52
|
-
mimeType?: string;
|
|
53
|
-
}>;
|
|
54
32
|
deleteLocalDatabase: () => Promise<void>;
|
|
55
33
|
/** Returns info about the pre-migration data directory, or `null` if already cleaned up. */
|
|
56
34
|
getOldDataDirInfo: () => Promise<{
|
package/dist/rpc-types.js
CHANGED
|
@@ -14,16 +14,6 @@ function rpcStub(rpcName) {
|
|
|
14
14
|
exports.rpcServerCalls = {
|
|
15
15
|
encryptData: rpcStub("encryptData"),
|
|
16
16
|
tableMethodCall: rpcStub("tableMethodCall"),
|
|
17
|
-
/**
|
|
18
|
-
* @deprecated Use System Files (`SystemFiles`, `readWholeFile`). Kept only for Family Hub
|
|
19
|
-
* 0.3.x; deleted once its port is Home's version.
|
|
20
|
-
*/
|
|
21
|
-
getFileContentsBase64: rpcStub("getFileContentsBase64"),
|
|
22
|
-
/**
|
|
23
|
-
* @deprecated Use System Files (`SystemFiles`). Kept only for Family Hub 0.3.x; deleted once
|
|
24
|
-
* its port is Home's version.
|
|
25
|
-
*/
|
|
26
|
-
saveFile: rpcStub("saveFile"),
|
|
27
17
|
deleteLocalDatabase: rpcStub("deleteLocalDatabase"),
|
|
28
18
|
/** Returns info about the pre-migration data directory, or `null` if already cleaned up. */
|
|
29
19
|
getOldDataDirInfo: rpcStub("getOldDataDirInfo"),
|
|
@@ -26,7 +26,11 @@ export interface IDeviceMessage<T = any> {
|
|
|
26
26
|
deviceMessageId: string;
|
|
27
27
|
/** Device that originated the message. */
|
|
28
28
|
fromDeviceId: string;
|
|
29
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Signed source identity. Lets a receiver accept a device it hasn't met yet when the device
|
|
31
|
+
* belongs to the receiver's own user (personal context) or to a counted member of the
|
|
32
|
+
* message's group.
|
|
33
|
+
*/
|
|
30
34
|
fromDeviceInfo?: ISignedObject<IDeviceInfo>;
|
|
31
35
|
/** Intended destination device. */
|
|
32
36
|
toDeviceId: string;
|