@peers-app/peers-sdk 0.30.4 → 0.30.5
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.
|
@@ -5,7 +5,9 @@ export type MeshDialReason = "own-device" | "group-network-info" | "broadcast-an
|
|
|
5
5
|
/** Bounded path categories used for lifetime counters; attempts also retain the address. */
|
|
6
6
|
export type MeshPath = "websocket" | "webrtc" | "cloud" | "discovery";
|
|
7
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"
|
|
8
|
+
export type MeshFailureReason = "ttl0" | "no-route" | "signaling-timeout" | "socket-error" | "socket-timeout" | "handshake" | "rejected" | "closed" | "superseded" | "unsupported" | "sender-not-known"
|
|
9
|
+
/** This device can't encrypt to the target yet: no owner-signed row or profile for it here. */
|
|
10
|
+
| "recipient-unknown" | "trust-refused" | "identity-mismatch" | "cancelled" | "capacity";
|
|
9
11
|
/** Observed stage; timestamps are offsets in milliseconds from the attempt's queued time. */
|
|
10
12
|
export type MeshStage = "queued" | "started" | "authenticated" | "admitted" | "ready" | "firstSync";
|
|
11
13
|
/** Local dial metadata and cancellation; never serialized into device messages. */
|
|
@@ -19,6 +21,12 @@ export interface IMeshDial {
|
|
|
19
21
|
targetDeviceId?: string;
|
|
20
22
|
/** One-based retry number; absent for the initial dial. */
|
|
21
23
|
retry?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Metrics only: the logical dial this transport races alongside (a published address tried
|
|
26
|
+
* next to the device-id path). It's recorded as an attempt of its own but counted as part of
|
|
27
|
+
* that dial, and its failures don't end the dial.
|
|
28
|
+
*/
|
|
29
|
+
siblingOf?: string;
|
|
22
30
|
}
|
|
23
31
|
/** One dial or inbound connection. Missing stages have not been observed. */
|
|
24
32
|
export interface IMeshAttempt {
|
|
@@ -135,6 +143,17 @@ export interface IMeshStats {
|
|
|
135
143
|
probes: number;
|
|
136
144
|
connected: number;
|
|
137
145
|
};
|
|
146
|
+
/**
|
|
147
|
+
* Dials helped by what devices publish: dials that tried a device's published addresses,
|
|
148
|
+
* those an address won, and the relayed protocol queries made and skipped because the
|
|
149
|
+
* device's protocols were known. Absent from devices older than these counters.
|
|
150
|
+
*/
|
|
151
|
+
dialHints?: {
|
|
152
|
+
addressDials: number;
|
|
153
|
+
addressWins: number;
|
|
154
|
+
protocolQueries: number;
|
|
155
|
+
protocolQueriesSkipped: number;
|
|
156
|
+
};
|
|
138
157
|
/** Lifetime episodes excluded from latency percentiles, by reason. */
|
|
139
158
|
unmeasuredEpisodes?: {
|
|
140
159
|
pageHidden: number;
|
|
@@ -312,6 +312,14 @@ async function checkDevice(existing, candidate, context) {
|
|
|
312
312
|
!signed(candidate)) {
|
|
313
313
|
throw (0, write_checks_1.writeNotAuthorized)("Devices", "the ownership claim doesn't verify");
|
|
314
314
|
}
|
|
315
|
+
// Only the owner publishes how to dial its device. Sync keeps the stored block when a writer
|
|
316
|
+
// drops it or holds an older one (retainDeviceReachability), so this refuses only a forgery.
|
|
317
|
+
if (candidate?.reachability &&
|
|
318
|
+
(candidate.userId !== existing?.userId ||
|
|
319
|
+
!(0, devices_1.sameDeviceReachability)(candidate.reachability, existing?.reachability)) &&
|
|
320
|
+
!(0, devices_1.isDeviceReachabilitySigned)(candidate, users.get(ownerId ?? candidate.userId)?.publicKey)) {
|
|
321
|
+
throw (0, write_checks_1.writeNotAuthorized)("Devices", "reachability isn't signed by the device's owner");
|
|
322
|
+
}
|
|
315
323
|
}
|
|
316
324
|
(0, write_checks_1.registerTableWriteCheck)("Users", checkUser);
|
|
317
325
|
(0, write_checks_1.registerTableWriteCheck)("Devices", checkDevice);
|
package/dist/data/devices.d.ts
CHANGED
|
@@ -5,6 +5,34 @@ import { TrustLevel } from "../device/socket.type";
|
|
|
5
5
|
export declare const DISPLAY_NAME_MAX_LENGTH = 128;
|
|
6
6
|
/** Shared schema for bounded user and device display names. */
|
|
7
7
|
export declare const displayNameSchema: z.ZodString;
|
|
8
|
+
/** Most origins one device publishes: every LAN interface plus a configured public origin. */
|
|
9
|
+
export declare const DEVICE_REACHABILITY_MAX_ADDRESSES = 16;
|
|
10
|
+
/** Most protocol names one device publishes. */
|
|
11
|
+
export declare const DEVICE_REACHABILITY_MAX_PROTOCOLS = 8;
|
|
12
|
+
/**
|
|
13
|
+
* How a device can be dialed, published by its owner on its own Devices row: the origins its peer
|
|
14
|
+
* listener accepts connections on, and the protocols it supports. Only the owner writes it, signed
|
|
15
|
+
* with the owner's key, and the newest signed `updatedAt` wins. Addresses are a fast path; every
|
|
16
|
+
* dial still checks that the expected device answers.
|
|
17
|
+
*/
|
|
18
|
+
export declare const deviceReachabilitySchema: z.ZodObject<{
|
|
19
|
+
addresses: z.ZodArray<z.ZodString, "many">;
|
|
20
|
+
protocols: z.ZodArray<z.ZodString, "many">;
|
|
21
|
+
updatedAt: z.ZodNumber;
|
|
22
|
+
signature: z.ZodString;
|
|
23
|
+
}, "strip", z.ZodTypeAny, {
|
|
24
|
+
signature: string;
|
|
25
|
+
updatedAt: number;
|
|
26
|
+
addresses: string[];
|
|
27
|
+
protocols: string[];
|
|
28
|
+
}, {
|
|
29
|
+
signature: string;
|
|
30
|
+
updatedAt: number;
|
|
31
|
+
addresses: string[];
|
|
32
|
+
protocols: string[];
|
|
33
|
+
}>;
|
|
34
|
+
/** How a device can be dialed, as its owner published it (see {@link deviceReachabilitySchema}). */
|
|
35
|
+
export type IDeviceReachability = z.infer<typeof deviceReachabilitySchema>;
|
|
8
36
|
/** Schema for a persisted, context-scoped device record. */
|
|
9
37
|
export declare const deviceSchema: z.ZodObject<{
|
|
10
38
|
deviceId: z.ZodEffects<z.ZodString, string, string>;
|
|
@@ -16,6 +44,22 @@ export declare const deviceSchema: z.ZodObject<{
|
|
|
16
44
|
serverUrl: z.ZodOptional<z.ZodString>;
|
|
17
45
|
trustLevel: z.ZodNativeEnum<typeof TrustLevel>;
|
|
18
46
|
ownerSignature: z.ZodOptional<z.ZodString>;
|
|
47
|
+
reachability: z.ZodOptional<z.ZodObject<{
|
|
48
|
+
addresses: z.ZodArray<z.ZodString, "many">;
|
|
49
|
+
protocols: z.ZodArray<z.ZodString, "many">;
|
|
50
|
+
updatedAt: z.ZodNumber;
|
|
51
|
+
signature: z.ZodString;
|
|
52
|
+
}, "strip", z.ZodTypeAny, {
|
|
53
|
+
signature: string;
|
|
54
|
+
updatedAt: number;
|
|
55
|
+
addresses: string[];
|
|
56
|
+
protocols: string[];
|
|
57
|
+
}, {
|
|
58
|
+
signature: string;
|
|
59
|
+
updatedAt: number;
|
|
60
|
+
addresses: string[];
|
|
61
|
+
protocols: string[];
|
|
62
|
+
}>>;
|
|
19
63
|
}, "strip", z.ZodTypeAny, {
|
|
20
64
|
userId: string;
|
|
21
65
|
deviceId: string;
|
|
@@ -26,6 +70,12 @@ export declare const deviceSchema: z.ZodObject<{
|
|
|
26
70
|
reportedName?: string | undefined;
|
|
27
71
|
serverUrl?: string | undefined;
|
|
28
72
|
ownerSignature?: string | undefined;
|
|
73
|
+
reachability?: {
|
|
74
|
+
signature: string;
|
|
75
|
+
updatedAt: number;
|
|
76
|
+
addresses: string[];
|
|
77
|
+
protocols: string[];
|
|
78
|
+
} | undefined;
|
|
29
79
|
}, {
|
|
30
80
|
userId: string;
|
|
31
81
|
deviceId: string;
|
|
@@ -36,6 +86,12 @@ export declare const deviceSchema: z.ZodObject<{
|
|
|
36
86
|
reportedName?: string | undefined;
|
|
37
87
|
serverUrl?: string | undefined;
|
|
38
88
|
ownerSignature?: string | undefined;
|
|
89
|
+
reachability?: {
|
|
90
|
+
signature: string;
|
|
91
|
+
updatedAt: number;
|
|
92
|
+
addresses: string[];
|
|
93
|
+
protocols: string[];
|
|
94
|
+
} | undefined;
|
|
39
95
|
}>;
|
|
40
96
|
/** A context-scoped device record containing local state and owner-reported metadata. */
|
|
41
97
|
export type IDevice = z.infer<typeof deviceSchema>;
|
|
@@ -58,6 +114,41 @@ export declare function deviceOwnerStatement(device: Pick<IDevice, "deviceId" |
|
|
|
58
114
|
* @param ownerPublicKey - The signing key of `device.userId`, as this device knows it.
|
|
59
115
|
*/
|
|
60
116
|
export declare function isDeviceOwnerSigned(device: Pick<IDevice, "deviceId" | "userId" | "ownerSignature">, ownerPublicKey: string | undefined): boolean;
|
|
117
|
+
/**
|
|
118
|
+
* What a device's owner signs to publish how to dial it: the contents of
|
|
119
|
+
* `reachability.signature`. Arrays are signed in the order given, so publishers sort them.
|
|
120
|
+
*
|
|
121
|
+
* @param deviceId - The device the reachability is for.
|
|
122
|
+
* @param reachability - Its addresses, protocols and publishing time.
|
|
123
|
+
*/
|
|
124
|
+
export declare function deviceReachabilityStatement(deviceId: string, reachability: Pick<IDeviceReachability, "addresses" | "protocols" | "updatedAt">): {
|
|
125
|
+
purpose: string;
|
|
126
|
+
deviceId: string;
|
|
127
|
+
addresses: string[];
|
|
128
|
+
protocols: string[];
|
|
129
|
+
updatedAt: number;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Whether `device.reachability` carries `ownerPublicKey`'s signature over
|
|
133
|
+
* {@link deviceReachabilityStatement}: the device's owner published it, not whoever wrote the row.
|
|
134
|
+
*
|
|
135
|
+
* @param device - A Devices row.
|
|
136
|
+
* @param ownerPublicKey - The signing key of the device's owner, as this device knows it.
|
|
137
|
+
*/
|
|
138
|
+
export declare function isDeviceReachabilitySigned(device: Pick<IDevice, "deviceId" | "reachability">, ownerPublicKey: string | undefined): boolean;
|
|
139
|
+
/** Whether two reachability blocks are the same publication. */
|
|
140
|
+
export declare function sameDeviceReachability(a: IDeviceReachability | undefined, b: IDeviceReachability | undefined): boolean;
|
|
141
|
+
/**
|
|
142
|
+
* The row to store when `candidate` replaces `existing`: the candidate, keeping the stored
|
|
143
|
+
* `reachability` when the candidate has none or an older one (a tie keeps the stored one too).
|
|
144
|
+
* Writers that don't know the field (older builds) or hold a stale copy never roll it back;
|
|
145
|
+
* only a newer publication replaces it. Signatures are checked separately, by the group write
|
|
146
|
+
* check and by whoever dials.
|
|
147
|
+
*
|
|
148
|
+
* @param existing - The row as stored now, if any.
|
|
149
|
+
* @param candidate - The row about to be stored.
|
|
150
|
+
*/
|
|
151
|
+
export declare function retainDeviceReachability<T extends Partial<IDevice>>(existing: Partial<IDevice> | undefined, candidate: T): T;
|
|
61
152
|
/** Schema for the signed identity and bounded presentation hints exchanged by devices. */
|
|
62
153
|
export declare const deviceInfoSchema: z.ZodObject<{
|
|
63
154
|
userId: z.ZodEffects<z.ZodString, string, string>;
|
|
@@ -237,6 +328,12 @@ export declare function Devices(dataContext?: DataContext): import("..").Table<{
|
|
|
237
328
|
reportedName?: string | undefined;
|
|
238
329
|
serverUrl?: string | undefined;
|
|
239
330
|
ownerSignature?: string | undefined;
|
|
331
|
+
reachability?: {
|
|
332
|
+
signature: string;
|
|
333
|
+
updatedAt: number;
|
|
334
|
+
addresses: string[];
|
|
335
|
+
protocols: string[];
|
|
336
|
+
} | undefined;
|
|
240
337
|
}>;
|
|
241
338
|
/** Persistent identifier for the current physical/runtime device. */
|
|
242
339
|
export declare const thisDeviceId: import("./persistent-vars").PersistentVar<string | undefined>;
|
package/dist/data/devices.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.thisDeviceId = exports.deviceHandshakeSchema = exports.handshakeFrameCryptoSchema = exports.deviceInfoSchema = exports.deviceSchema = exports.displayNameSchema = exports.DISPLAY_NAME_MAX_LENGTH = void 0;
|
|
3
|
+
exports.thisDeviceId = exports.deviceHandshakeSchema = exports.handshakeFrameCryptoSchema = exports.deviceInfoSchema = exports.deviceSchema = exports.deviceReachabilitySchema = exports.DEVICE_REACHABILITY_MAX_PROTOCOLS = exports.DEVICE_REACHABILITY_MAX_ADDRESSES = exports.displayNameSchema = exports.DISPLAY_NAME_MAX_LENGTH = void 0;
|
|
4
4
|
exports.deviceOwnerStatement = deviceOwnerStatement;
|
|
5
5
|
exports.isDeviceOwnerSigned = isDeviceOwnerSigned;
|
|
6
|
+
exports.deviceReachabilityStatement = deviceReachabilityStatement;
|
|
7
|
+
exports.isDeviceReachabilitySigned = isDeviceReachabilitySigned;
|
|
8
|
+
exports.sameDeviceReachability = sameDeviceReachability;
|
|
9
|
+
exports.retainDeviceReachability = retainDeviceReachability;
|
|
6
10
|
exports.Devices = Devices;
|
|
7
11
|
const zod_1 = require("zod");
|
|
8
12
|
const user_context_singleton_1 = require("../context/user-context-singleton");
|
|
@@ -17,6 +21,30 @@ const persistent_vars_1 = require("./persistent-vars");
|
|
|
17
21
|
exports.DISPLAY_NAME_MAX_LENGTH = 128;
|
|
18
22
|
/** Shared schema for bounded user and device display names. */
|
|
19
23
|
exports.displayNameSchema = zod_1.z.string().max(exports.DISPLAY_NAME_MAX_LENGTH);
|
|
24
|
+
/** Most origins one device publishes: every LAN interface plus a configured public origin. */
|
|
25
|
+
exports.DEVICE_REACHABILITY_MAX_ADDRESSES = 16;
|
|
26
|
+
/** Most protocol names one device publishes. */
|
|
27
|
+
exports.DEVICE_REACHABILITY_MAX_PROTOCOLS = 8;
|
|
28
|
+
/**
|
|
29
|
+
* How a device can be dialed, published by its owner on its own Devices row: the origins its peer
|
|
30
|
+
* listener accepts connections on, and the protocols it supports. Only the owner writes it, signed
|
|
31
|
+
* with the owner's key, and the newest signed `updatedAt` wins. Addresses are a fast path; every
|
|
32
|
+
* dial still checks that the expected device answers.
|
|
33
|
+
*/
|
|
34
|
+
exports.deviceReachabilitySchema = zod_1.z.object({
|
|
35
|
+
addresses: zod_1.z
|
|
36
|
+
.array(zod_1.z.string().max(256))
|
|
37
|
+
.max(exports.DEVICE_REACHABILITY_MAX_ADDRESSES)
|
|
38
|
+
.describe("Origins the device's peer listener accepts connections on, in a stable order"),
|
|
39
|
+
protocols: zod_1.z
|
|
40
|
+
.array(zod_1.z.string().max(32))
|
|
41
|
+
.max(exports.DEVICE_REACHABILITY_MAX_PROTOCOLS)
|
|
42
|
+
.describe("Protocols the device can be dialed with, such as ws and wrtc"),
|
|
43
|
+
updatedAt: zod_1.z.number().describe("When the owner published this, in ms; the newest one wins"),
|
|
44
|
+
signature: zod_1.z
|
|
45
|
+
.string()
|
|
46
|
+
.describe("The owner's signature over deviceReachabilityStatement, without the key prefix"),
|
|
47
|
+
});
|
|
20
48
|
/** Schema for a persisted, context-scoped device record. */
|
|
21
49
|
exports.deviceSchema = zod_1.z.object({
|
|
22
50
|
deviceId: zod_types_1.zodPeerId,
|
|
@@ -31,6 +59,9 @@ exports.deviceSchema = zod_1.z.object({
|
|
|
31
59
|
.string()
|
|
32
60
|
.optional()
|
|
33
61
|
.describe("The owner's signature claiming this device for userId (see deviceOwnerStatement). In a group, only a row with a valid one says who a device belongs to, and it stays theirs"),
|
|
62
|
+
reachability: exports.deviceReachabilitySchema
|
|
63
|
+
.optional()
|
|
64
|
+
.describe("How to dial this device, published and signed by its owner. Sync keeps the newest signed copy, so a writer without it never clears it"),
|
|
34
65
|
});
|
|
35
66
|
/**
|
|
36
67
|
* What a device's owner signs to claim it: the contents of `ownerSignature`. Each device signs
|
|
@@ -56,6 +87,74 @@ function isDeviceOwnerSigned(device, ownerPublicKey) {
|
|
|
56
87
|
signature: `${ownerPublicKey}:${device.ownerSignature}`,
|
|
57
88
|
}) === ownerPublicKey);
|
|
58
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* What a device's owner signs to publish how to dial it: the contents of
|
|
92
|
+
* `reachability.signature`. Arrays are signed in the order given, so publishers sort them.
|
|
93
|
+
*
|
|
94
|
+
* @param deviceId - The device the reachability is for.
|
|
95
|
+
* @param reachability - Its addresses, protocols and publishing time.
|
|
96
|
+
*/
|
|
97
|
+
function deviceReachabilityStatement(deviceId, reachability) {
|
|
98
|
+
return {
|
|
99
|
+
purpose: "peers-device-reachability",
|
|
100
|
+
deviceId,
|
|
101
|
+
addresses: reachability.addresses,
|
|
102
|
+
protocols: reachability.protocols,
|
|
103
|
+
updatedAt: reachability.updatedAt,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Whether `device.reachability` carries `ownerPublicKey`'s signature over
|
|
108
|
+
* {@link deviceReachabilityStatement}: the device's owner published it, not whoever wrote the row.
|
|
109
|
+
*
|
|
110
|
+
* @param device - A Devices row.
|
|
111
|
+
* @param ownerPublicKey - The signing key of the device's owner, as this device knows it.
|
|
112
|
+
*/
|
|
113
|
+
function isDeviceReachabilitySigned(device, ownerPublicKey) {
|
|
114
|
+
const reachability = device.reachability;
|
|
115
|
+
if (!reachability?.signature || !ownerPublicKey)
|
|
116
|
+
return false;
|
|
117
|
+
return ((0, keys_1.signedByKey)({
|
|
118
|
+
...deviceReachabilityStatement(device.deviceId, reachability),
|
|
119
|
+
signature: `${ownerPublicKey}:${reachability.signature}`,
|
|
120
|
+
}) === ownerPublicKey);
|
|
121
|
+
}
|
|
122
|
+
/** Whether two reachability blocks are the same publication. */
|
|
123
|
+
function sameDeviceReachability(a, b) {
|
|
124
|
+
if (!a || !b)
|
|
125
|
+
return a === b;
|
|
126
|
+
return (a.updatedAt === b.updatedAt &&
|
|
127
|
+
a.signature === b.signature &&
|
|
128
|
+
a.addresses.length === b.addresses.length &&
|
|
129
|
+
a.addresses.every((address, i) => address === b.addresses[i]) &&
|
|
130
|
+
a.protocols.length === b.protocols.length &&
|
|
131
|
+
a.protocols.every((protocol, i) => protocol === b.protocols[i]));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The row to store when `candidate` replaces `existing`: the candidate, keeping the stored
|
|
135
|
+
* `reachability` when the candidate has none or an older one (a tie keeps the stored one too).
|
|
136
|
+
* Writers that don't know the field (older builds) or hold a stale copy never roll it back;
|
|
137
|
+
* only a newer publication replaces it. Signatures are checked separately, by the group write
|
|
138
|
+
* check and by whoever dials.
|
|
139
|
+
*
|
|
140
|
+
* @param existing - The row as stored now, if any.
|
|
141
|
+
* @param candidate - The row about to be stored.
|
|
142
|
+
*/
|
|
143
|
+
function retainDeviceReachability(existing, candidate) {
|
|
144
|
+
const stored = existing?.reachability;
|
|
145
|
+
if (!stored)
|
|
146
|
+
return candidate;
|
|
147
|
+
// An unclaimed discovery row can acquire a different owner. Its former owner's
|
|
148
|
+
// publication must neither follow that claim nor prevent the new owner's publication.
|
|
149
|
+
if (existing.userId && candidate.userId && existing.userId !== candidate.userId)
|
|
150
|
+
return candidate;
|
|
151
|
+
const incoming = candidate.reachability;
|
|
152
|
+
if (incoming && incoming.updatedAt > stored.updatedAt)
|
|
153
|
+
return candidate;
|
|
154
|
+
if (incoming && sameDeviceReachability(incoming, stored))
|
|
155
|
+
return candidate;
|
|
156
|
+
return { ...candidate, reachability: stored };
|
|
157
|
+
}
|
|
59
158
|
/** Schema for the signed identity and bounded presentation hints exchanged by devices. */
|
|
60
159
|
exports.deviceInfoSchema = zod_1.z.object({
|
|
61
160
|
userId: zod_types_1.zodPeerId,
|