@peers-app/peers-sdk 0.27.4 → 0.27.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.
@@ -63,7 +63,8 @@ export declare const CONTRACT_REQUEST_CHANNEL = "contractCall";
63
63
  export declare const CONTRACT_NOTIFY_CHANNEL = "contractNotify";
64
64
  /**
65
65
  * Wrap a duplex RPC emitter (e.g. a device {@link Connection}) as an
66
- * {@link IContractTransport}. `emit` delegates directly. `on` uses one adapter-owned
66
+ * {@link IContractTransport}. `emit` delegates directly and rejects with an `Error`
67
+ * carrying the remote message, whatever the emitter rejected with. `on` uses one adapter-owned
67
68
  * dispatcher per emitter/channel and returns an unsubscribe that removes only the caller's
68
69
  * handler from that dispatcher; it never uses a coarse `removeAllListeners`.
69
70
  *
@@ -33,9 +33,24 @@ exports.CONTRACT_REQUEST_CHANNEL = "contractCall";
33
33
  */
34
34
  exports.CONTRACT_NOTIFY_CHANNEL = "contractNotify";
35
35
  const connectionDispatcherStates = new WeakMap();
36
+ /**
37
+ * Turn a failed emit into an `Error`. A device connection rejects with the remote handler's
38
+ * `{ error, errorType }` envelope, or with a string when the call times out.
39
+ */
40
+ function toError(error) {
41
+ if (error instanceof Error)
42
+ return error;
43
+ if (error &&
44
+ typeof error === "object" &&
45
+ typeof error.error === "string") {
46
+ return new Error(error.error);
47
+ }
48
+ return new Error(typeof error === "string" ? error : JSON.stringify(error));
49
+ }
36
50
  /**
37
51
  * Wrap a duplex RPC emitter (e.g. a device {@link Connection}) as an
38
- * {@link IContractTransport}. `emit` delegates directly. `on` uses one adapter-owned
52
+ * {@link IContractTransport}. `emit` delegates directly and rejects with an `Error`
53
+ * carrying the remote message, whatever the emitter rejected with. `on` uses one adapter-owned
39
54
  * dispatcher per emitter/channel and returns an unsubscribe that removes only the caller's
40
55
  * handler from that dispatcher; it never uses a coarse `removeAllListeners`.
41
56
  *
@@ -53,7 +68,14 @@ const connectionDispatcherStates = new WeakMap();
53
68
  */
54
69
  function connectionContractTransport(emitter) {
55
70
  return {
56
- emit: (channel, ...args) => emitter.emit(channel, ...args),
71
+ emit: async (channel, ...args) => {
72
+ try {
73
+ return await emitter.emit(channel, ...args);
74
+ }
75
+ catch (error) {
76
+ throw toError(error);
77
+ }
78
+ },
57
79
  on: (channel, handler) => {
58
80
  let channelStates = connectionDispatcherStates.get(emitter);
59
81
  if (!channelStates) {
@@ -179,13 +179,17 @@ function validateToolCatalogWrite(existing, candidate, recordId = candidate?.too
179
179
  !isMissingTargetQuarantine(existing) &&
180
180
  isMissingTargetQuarantine(candidate))
181
181
  throw new Error(`Quarantined tool identity: ${candidate.toolId}`);
182
- // Quarantine preserves the original evidence byte-for-byte; it cannot retarget it.
182
+ // Quarantine preserves the original evidence byte-for-byte; it cannot retarget it. That
183
+ // includes an existing quarantine's reason: a peer's resolve patch whose `catalogReason`
184
+ // half arrives first must park, not erase the missing-target reason its other half needs.
183
185
  if (existing &&
184
186
  candidate.catalogState === "quarantined" &&
185
187
  existing.contractId === candidate.contractId &&
186
188
  existing.contractVersion === candidate.contractVersion &&
187
189
  existing.toolName === candidate.toolName &&
188
- existing.catalogIdentity === candidate.catalogIdentity)
190
+ existing.catalogIdentity === candidate.catalogIdentity &&
191
+ (existing.catalogState !== "quarantined" ||
192
+ (existing.catalogReason ?? undefined) === (candidate.catalogReason ?? undefined)))
189
193
  return;
190
194
  // Changing quarantined identity evidence also requires an approved repair, even
191
195
  // when the candidate stays quarantined and would be resolved by reconciliation.
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * binary-peer-connection-v2.ts
3
3
  *
4
- * Unified binary transport layer that replaces both binary-peer-connection.ts
5
- * and streamed-socket.ts. Provides two cleanly separated modes:
4
+ * Binary transport layer under every device connection. Provides two cleanly
5
+ * separated modes:
6
6
  *
7
7
  * 1. **RPC mode** (emit/on): request/response interactions with automatic
8
8
  * encoding (txEncode for objects, passthrough for Uint8Array) and transparent
9
9
  * chunking for large payloads.
10
10
  *
11
11
  * 2. **Stream mode** (sendRawBytes/onRawBytes): continuous byte transfer for
12
- * file syncing, database downloads, log streaming, etc. Each sendRawBytes
12
+ * file syncing, log streaming, etc. Each sendRawBytes
13
13
  * call = exactly one onRawBytes callback (atomic delivery after reassembly).
14
14
  *
15
15
  * Both modes share a single sendFrame() function that handles backpressure
@@ -61,19 +61,12 @@ export declare const DEFAULT_BINARY_PEER_RECEIVE_LIMITS: Readonly<BinaryPeerRece
61
61
  export interface WrapBinaryPeerOptions {
62
62
  /** Protocol identifier used for server address (e.g., 'wrtc' or 'ws') */
63
63
  protocol: "wrtc" | "ws";
64
- /**
65
- * Mark the transport as already secure (encrypted at transport layer).
66
- * When true, sets secureLocal and secureRemote to skip the legacy per-message
67
- * envelope against a legacy peer. Irrelevant once the frame cipher is armed.
68
- * @deprecated Removed with the legacy path.
69
- */
70
- markTransportSecure?: boolean;
71
64
  /** Optional hard limits for untrusted inbound messages. */
72
65
  receiveLimits?: Partial<BinaryPeerReceiveLimits>;
73
66
  }
74
67
  /**
75
68
  * Creates an ISocket from a binary peer. This is the core v2 factory function.
76
- * Replaces both createBinaryPeerSocket (v1) and StreamedSocket.
69
+ * Used for every device connection.
77
70
  */
78
71
  export declare function createBinaryPeerSocket(connectionId: string, peer: IBinaryPeer, protocol?: string, receiveLimitOverrides?: Partial<BinaryPeerReceiveLimits>): ISocket;
79
72
  /**
@@ -82,10 +75,9 @@ export declare function createBinaryPeerSocket(connectionId: string, peer: IBina
82
75
  */
83
76
  export declare function wrapBinaryPeer(connectionId: string, peer: IBinaryPeer, localDevice: Device, initiator: boolean, options: WrapBinaryPeerOptions, getTrustLevel?: ITrustLevelFn): Connection;
84
77
  /**
85
- * Wraps a WebRTC peer into a Connection.
86
- * WebRTC is encrypted at the transport layer (DTLS-SRTP); the frame cipher is
87
- * armed on top of it by the handshake so no code path depends on transport
88
- * classification.
78
+ * Wraps a WebRTC peer into a Connection. WebRTC is encrypted at the transport
79
+ * layer (DTLS-SRTP); the handshake arms the frame cipher on top of it, as on
80
+ * every transport.
89
81
  */
90
82
  export declare function wrapWrtc(connectionId: string, peer: IWebRTCPeer, localDevice: Device, initiator: boolean, getTrustLevel?: ITrustLevelFn, receiveLimits?: Partial<BinaryPeerReceiveLimits>): Connection;
91
83
  export {};
@@ -2,15 +2,15 @@
2
2
  /**
3
3
  * binary-peer-connection-v2.ts
4
4
  *
5
- * Unified binary transport layer that replaces both binary-peer-connection.ts
6
- * and streamed-socket.ts. Provides two cleanly separated modes:
5
+ * Binary transport layer under every device connection. Provides two cleanly
6
+ * separated modes:
7
7
  *
8
8
  * 1. **RPC mode** (emit/on): request/response interactions with automatic
9
9
  * encoding (txEncode for objects, passthrough for Uint8Array) and transparent
10
10
  * chunking for large payloads.
11
11
  *
12
12
  * 2. **Stream mode** (sendRawBytes/onRawBytes): continuous byte transfer for
13
- * file syncing, database downloads, log streaming, etc. Each sendRawBytes
13
+ * file syncing, log streaming, etc. Each sendRawBytes
14
14
  * call = exactly one onRawBytes callback (atomic delivery after reassembly).
15
15
  *
16
16
  * Both modes share a single sendFrame() function that handles backpressure
@@ -240,7 +240,7 @@ function buildStreamCont(sidBytes, chunkData) {
240
240
  }
241
241
  /**
242
242
  * Creates an ISocket from a binary peer. This is the core v2 factory function.
243
- * Replaces both createBinaryPeerSocket (v1) and StreamedSocket.
243
+ * Used for every device connection.
244
244
  */
245
245
  function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receiveLimitOverrides = {}) {
246
246
  const handlers = {};
@@ -265,7 +265,7 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receive
265
265
  let sendSealer = null;
266
266
  let receiveOpener = null;
267
267
  // Bare sockets (no Connection) deliver plaintext streams; Connection turns
268
- // this off and re-enables it only for a verified legacy peer.
268
+ // this off, so it receives only sealed stream frames.
269
269
  let plaintextStreamsAllowed = true;
270
270
  // Every inner frame must leave room for the cipher's 17 bytes so a sealed
271
271
  // frame never exceeds the transport limit (64 KB SCTP on WebRTC). Reserved
@@ -503,7 +503,7 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receive
503
503
  else
504
504
  responseMessage.result = result;
505
505
  const encoded = encodeMessage(responseMessage);
506
- sendRPC(encoded, sendOptions);
506
+ return sendRPC(encoded, sendOptions);
507
507
  };
508
508
  handler(message.args, callback);
509
509
  }
@@ -730,17 +730,8 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receive
730
730
  break;
731
731
  }
732
732
  default: {
733
- // v1 backwards compatibility: if frame type isn't recognized but looks
734
- // like a v1 PROTOCOL_RPC (0x00) it's already handled above. For true
735
- // legacy format (no discriminator), try to decode as RPC.
736
- // This handles the transition period where v1 peers send raw RPC without
737
- // a frame type byte.
738
- if (bytes.length > receiveLimits.maxRpcMessageBytes) {
739
- closeForProtocolViolation("legacy RPC exceeds the message-size limit");
740
- return;
741
- }
742
- handleIncomingRPC(bytes);
743
- break;
733
+ closeForProtocolViolation(`unknown frame type ${frameType}`);
734
+ return;
744
735
  }
745
736
  }
746
737
  }
@@ -762,7 +753,6 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receive
762
753
  const socket = {
763
754
  id: connectionId,
764
755
  connected: true,
765
- handlesOwnEncoding: true,
766
756
  transportType,
767
757
  stats,
768
758
  armFrameCipher,
@@ -811,26 +801,20 @@ function createBinaryPeerSocket(connectionId, peer, protocol = "binary", receive
811
801
  * This is the main implementation that handles all binary protocols.
812
802
  */
813
803
  function wrapBinaryPeer(connectionId, peer, localDevice, initiator, options, getTrustLevel) {
814
- const { protocol, markTransportSecure, receiveLimits } = options;
804
+ const { protocol, receiveLimits } = options;
815
805
  const socket = createBinaryPeerSocket(connectionId, peer, protocol, receiveLimits);
816
806
  const serverAddress = `${protocol}://${connectionId}`;
817
807
  const localServerAddresses = initiator ? undefined : [serverAddress];
818
808
  if (process.env.NODE_ENV !== "test") {
819
809
  getTrustLevel ??= (0, get_trust_level_fn_1.getTrustLevelFn)(localDevice, serverAddress);
820
810
  }
821
- const connection = new connection_1.Connection(socket, localDevice, localServerAddresses, getTrustLevel);
822
- if (markTransportSecure) {
823
- connection.secureLocal = true;
824
- connection.secureRemote = true;
825
- }
826
- return connection;
811
+ return new connection_1.Connection(socket, localDevice, localServerAddresses, getTrustLevel);
827
812
  }
828
813
  /**
829
- * Wraps a WebRTC peer into a Connection.
830
- * WebRTC is encrypted at the transport layer (DTLS-SRTP); the frame cipher is
831
- * armed on top of it by the handshake so no code path depends on transport
832
- * classification.
814
+ * Wraps a WebRTC peer into a Connection. WebRTC is encrypted at the transport
815
+ * layer (DTLS-SRTP); the handshake arms the frame cipher on top of it, as on
816
+ * every transport.
833
817
  */
834
818
  function wrapWrtc(connectionId, peer, localDevice, initiator, getTrustLevel, receiveLimits) {
835
- return wrapBinaryPeer(connectionId, peer, localDevice, initiator, { protocol: "wrtc", markTransportSecure: true, receiveLimits }, getTrustLevel);
819
+ return wrapBinaryPeer(connectionId, peer, localDevice, initiator, { protocol: "wrtc", receiveLimits }, getTrustLevel);
836
820
  }
@@ -8,12 +8,10 @@ export type GetTrustLevel = (deviceInfo: IDeviceInfo, registerNew?: boolean) =>
8
8
  * Authenticated RPC connection between two Peers devices.
9
9
  *
10
10
  * The handshake (`doHandshake` on the client, `completeHandshake` on the
11
- * server) authenticates both identities with signed, boxed messages. When both
12
- * sockets support it, the handshake also agrees on per-connection frame-cipher
13
- * keys (`frame-cipher.ts`) and arms the socket, after which every frame on the
14
- * wire is sealed and the legacy per-message sign+box envelope is skipped.
15
- * Against a peer that does not advertise `frameCrypto` the connection falls
16
- * back to the legacy behaviour and logs a warning.
11
+ * server) authenticates both identities with signed, boxed messages and agrees
12
+ * on per-connection frame-cipher keys (`frame-cipher.ts`), then arms the
13
+ * socket: every frame after the handshake is sealed. A peer that does not
14
+ * offer frame encryption (a build before 0.25.7) is refused.
17
15
  */
18
16
  export declare class Connection {
19
17
  readonly localDevice: Device;
@@ -21,23 +19,9 @@ export declare class Connection {
21
19
  protected readonly getTrustLevel: GetTrustLevel;
22
20
  private _verified;
23
21
  private _remoteDeviceInfo;
24
- /**
25
- * @deprecated Legacy transport classification, only consulted when the frame
26
- * cipher is not armed. Removed once legacy peers are no longer supported.
27
- */
28
- secureLocal: boolean;
29
- /** @deprecated See {@link Connection.secureLocal}. */
30
- secureRemote: boolean;
31
- /**
32
- * Legacy per-message encryption override. Ignored once the frame cipher is
33
- * armed. `"dont-encrypt-traffic-after-handshake"` is honoured only under
34
- * `NODE_ENV=test`.
35
- * @deprecated Removed with the legacy path.
36
- */
37
- overrideEncryption?: "encrypt-traffic-regardless-of-protocol" | "dont-encrypt-traffic-after-handshake";
38
22
  /** Trust assigned to the remote identity once the handshake completes. */
39
23
  trustLevel: TrustLevel;
40
- /** Server-side hook fired shortly after a successful handshake. */
24
+ /** Server-side hook fired shortly after the successful handshake reply has been sent. */
41
25
  onHandshakeComplete: ((deviceInfo: IDeviceInfo) => void) | undefined;
42
26
  /** RPC timeout for {@link Connection.emit}. */
43
27
  timeoutMs: number;
@@ -53,21 +37,13 @@ export declare class Connection {
53
37
  private pendingClientHandshake;
54
38
  /** Server side: keys to arm for sending right after the handshake response frame. */
55
39
  private pendingSendArm;
40
+ /** Bumped by every {@link Connection.reset}, so a handshake can tell it was cut short. */
41
+ private handshakeGeneration;
56
42
  constructor(socket: ISocket, localDevice: Device, localDeviceServerAddresses?: string[] | undefined, getTrustLevel?: GetTrustLevel);
57
43
  /** True once the remote identity has been authenticated by the handshake. */
58
44
  get verified(): boolean;
59
45
  /** True when every frame on this connection is sealed by the frame cipher. */
60
46
  get frameCipherArmed(): boolean;
61
- /**
62
- * Whether to apply the legacy per-message envelope (sign + box) to RPC payloads.
63
- *
64
- * Always false once the frame cipher is armed: the frame layer already gives
65
- * confidentiality, integrity, and sender binding for every frame. Otherwise
66
- * (legacy peer) the transport classification below applies:
67
- * - skipped when both sides agreed the transport is secure (`secureLocal && secureRemote`)
68
- * - applied on plain `ws` / `http`
69
- */
70
- get encryptTraffic(): boolean;
71
47
  /** Copy of the authenticated remote device identity (empty before the handshake). */
72
48
  get remoteDeviceInfo(): {
73
49
  publicKey: string;
@@ -79,8 +55,8 @@ export declare class Connection {
79
55
  };
80
56
  /** Socket ID shared by both ends; also the frame-cipher AAD and HKDF salt. */
81
57
  get connectionId(): string;
82
- private get supportsFrameCrypto();
83
- private warnLegacyPeer;
58
+ /** The socket's frame-cipher arming; throws when the transport can't encrypt frames. */
59
+ private requireFrameCipher;
84
60
  private readonly handleCompleteHandshakeRPC;
85
61
  /**
86
62
  * Get connection throughput stats with calculated rates.
@@ -105,10 +81,15 @@ export declare class Connection {
105
81
  exposeRPC<T extends Function>(fn: T): void;
106
82
  /** Calls a remote RPC handler and resolves with its result (rejects on error or timeout). */
107
83
  emit<T = any>(eventName: string, ...args: any): Promise<T>;
84
+ /**
85
+ * Sends one RPC. `onReply`, when given, runs synchronously while the reply frame is
86
+ * processed, before any later frame from the peer; its return value (or throw) settles
87
+ * the call.
88
+ */
89
+ private request;
108
90
  /**
109
91
  * Registers an RPC handler. Events other than the handshake set are rejected
110
- * until the connection is verified; payloads use the legacy envelope only
111
- * when {@link Connection.encryptTraffic} is true.
92
+ * until the connection is verified.
112
93
  */
113
94
  on(eventName: string, handler: Function): void;
114
95
  /** Removes RPC handlers for `eventName`, or all handlers when omitted. */
@@ -120,26 +101,36 @@ export declare class Connection {
120
101
  */
121
102
  reset(): void;
122
103
  /**
123
- * Client side: builds the signed handshake (advertising frame-cipher support
124
- * with a fresh ephemeral key when the socket can arm one) and boxes it to the
125
- * remote device.
104
+ * Client side: builds the signed handshake (offering frame encryption with a
105
+ * fresh ephemeral key) and boxes it to the remote device.
126
106
  */
127
107
  initiateHandshake(serverAddress: string, remoteDeviceInfo?: IDeviceInfo): IDataBox;
128
108
  /**
129
109
  * Server side: verifies the client's handshake, resolves trust, and returns
130
- * the (unsigned) response for the caller to sign and box. When the client
131
- * offered `frameCrypto` and this socket can arm a cipher, derives the frame
110
+ * the (unsigned) response for the caller to sign and box. Derives the frame
132
111
  * keys, arms the receive direction immediately, and schedules the send
133
112
  * direction to arm right after the response frame (see
134
- * {@link Connection.handleCompleteHandshakeRPC}).
113
+ * {@link Connection.handleCompleteHandshakeRPC}). A client that does not
114
+ * offer frame encryption is refused.
135
115
  */
136
116
  completeHandshake(boxedHandshake: IDataBox): Promise<IDeviceHandshake>;
137
117
  /**
138
118
  * Client side: runs the full handshake against `remoteAddress`. On success
139
- * the remote identity is verified and, if both sockets support it, the
140
- * frame cipher is armed in both directions before this resolves.
119
+ * the remote identity is verified and the frame cipher is armed in both
120
+ * directions before this resolves.
141
121
  */
142
122
  doHandshake(remoteAddress: string): Promise<IDeviceHandshake>;
123
+ /**
124
+ * Client side: checks the server's handshake reply, arms the frame cipher, and records the
125
+ * verified identity. Runs synchronously while the reply frame is processed, so a sealed
126
+ * frame read right after it finds the cipher armed and the connection verified.
127
+ */
128
+ private acceptHandshakeReply;
129
+ /**
130
+ * Throws when the connection was reset or closed after the handshake started: a handshake
131
+ * cut short must fail, not finish on a dead or reused connection.
132
+ */
133
+ private assertHandshakeCurrent;
143
134
  /**
144
135
  * Resets this side and asks the remote to do the same. The remote call is a courtesy: a
145
136
  * peer that is not a server has no `reset` handler, and a peer we just refused may already