p2party 0.14.2 → 0.14.7
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/README.md +230 -30
- package/docs/getting-started.md +15 -0
- package/docs/protocol-v4-security.md +40 -0
- package/docs/references.md +28 -8
- package/docs/session-api.md +10 -2
- package/lib/api/signalingServerApi.d.ts +13 -2
- package/lib/api/webrtc/applyIceConfiguration.d.ts +9 -0
- package/lib/api/webrtc/dataChannelHandler.d.ts +10 -0
- package/lib/api/webrtc/deleteRoomData.d.ts +13 -0
- package/lib/api/webrtc/disconnectFromAllRoomsQuery.d.ts +2 -1
- package/lib/api/webrtc/disconnectFromRoomQuery.d.ts +2 -1
- package/lib/api/webrtc/disconnectQuery.d.ts +2 -1
- package/lib/api/webrtc/iceRepair.d.ts +3 -1
- package/lib/api/webrtc/index.d.ts +2 -1
- package/lib/api/webrtc/interfaces.d.ts +37 -0
- package/lib/api/webrtc/negotiationLock.d.ts +24 -0
- package/lib/api/webrtc/pendingOffer.d.ts +29 -0
- package/lib/api/webrtc/rebindDeadline.d.ts +61 -0
- package/lib/api/webrtc/remoteTransportChange.d.ts +142 -0
- package/lib/api/webrtc/retiredRemoteTransports.d.ts +70 -0
- package/lib/api/webrtc/roomEdgeBudget.d.ts +10 -0
- package/lib/api/webrtc/setDescriptionQuery.d.ts +8 -1
- package/lib/api/webrtc/settleOnClose.d.ts +36 -0
- package/lib/blindRendezvous/admissionInput.d.ts +11 -0
- package/lib/blindRendezvous/admissionPolicy.d.ts +217 -0
- package/lib/blindRendezvous/canonicalSchema.d.ts +139 -0
- package/lib/blindRendezvous/carrierEnvelope.d.ts +105 -0
- package/lib/blindRendezvous/coreReduce.d.ts +199 -0
- package/lib/blindRendezvous/derivations.d.ts +381 -0
- package/lib/blindRendezvous/identityHelloAdmission.d.ts +89 -0
- package/lib/blindRendezvous/identityHelloAdmissionPersistence.d.ts +133 -0
- package/lib/blindRendezvous/identityHelloHistoricalEvidence.d.ts +52 -0
- package/lib/blindRendezvous/identityHelloOpen.d.ts +167 -0
- package/lib/blindRendezvous/identityHelloTrial.d.ts +271 -0
- package/lib/blindRendezvous/localAdmissibleProject.d.ts +101 -0
- package/lib/blindRendezvous/localPresenceKeyBinding.d.ts +108 -0
- package/lib/blindRendezvous/localStableIdentityCustody.d.ts +46 -0
- package/lib/blindRendezvous/normativeKernel.d.ts +222 -0
- package/lib/blindRendezvous/operation.d.ts +137 -0
- package/lib/blindRendezvous/operationPayload.d.ts +134 -0
- package/lib/blindRendezvous/pairBootstrapSuite.d.ts +93 -0
- package/lib/blindRendezvous/pairCrypto.d.ts +122 -0
- package/lib/blindRendezvous/pairLedger.d.ts +154 -0
- package/lib/blindRendezvous/pairOpenBudget.d.ts +67 -0
- package/lib/blindRendezvous/pairOpenReservation.d.ts +157 -0
- package/lib/blindRendezvous/pairPlaintext.d.ts +240 -0
- package/lib/blindRendezvous/pairStepEnvelope.d.ts +60 -0
- package/lib/blindRendezvous/plannerState.d.ts +61 -0
- package/lib/blindRendezvous/projections.d.ts +71 -0
- package/lib/blindRendezvous/publicWindow.d.ts +330 -0
- package/lib/blindRendezvous/retainedOperationStore.d.ts +187 -0
- package/lib/blindRendezvous/roomInviteV2.d.ts +92 -0
- package/lib/blindRendezvous/roomPolicyV4.d.ts +90 -0
- package/lib/blindRendezvous/schemas.d.ts +221 -0
- package/lib/blindRendezvous/storeDescriptor.d.ts +160 -0
- package/lib/cryptography/aeadWasm.d.ts +18 -0
- package/lib/cryptography/byteInput.d.ts +12 -0
- package/lib/cryptography/carrierAead.d.ts +23 -0
- package/lib/cryptography/chacha20poly1305.d.ts +7 -0
- package/lib/cryptography/fips202.d.ts +10 -0
- package/lib/cryptography/hpke.d.ts +84 -0
- package/lib/cryptography/hpkeSuite.d.ts +39 -0
- package/lib/cryptography/hybridKem.d.ts +42 -0
- package/lib/cryptography/hybridKemSuite.d.ts +22 -0
- package/lib/cryptography/interfaces.d.ts +3 -0
- package/lib/cryptography/mlkem.d.ts +27 -1
- package/lib/cryptography/mnemonic.d.ts +65 -1
- package/lib/cryptography/ownedEd25519KeyMaterial.d.ts +27 -0
- package/lib/cryptography/webCryptoSha256.d.ts +20 -0
- package/lib/cryptography/x25519.d.ts +18 -0
- package/lib/cryptography/xchacha20poly1305.d.ts +7 -0
- package/lib/db/api.d.ts +6 -3
- package/lib/db/identityEd25519ClientBoundary.d.ts +13 -0
- package/lib/db/src/getDB.d.ts +2 -2
- package/lib/db/types.d.ts +52 -4
- package/lib/db.worker.js +1 -1
- package/lib/handlers/coverChannelRegistry.d.ts +22 -0
- package/lib/handlers/edgeTeardownFollowUp.d.ts +35 -0
- package/lib/handlers/handleChallenge.d.ts +2 -1
- package/lib/handlers/handleConnectToPeer.d.ts +1 -1
- package/lib/handlers/handleHandshake.d.ts +2 -6
- package/lib/handlers/handleSendMessage.d.ts +60 -7
- package/lib/handlers/handleWebSocketMessage.d.ts +1 -1
- package/lib/handlers/handshakeCore.d.ts +2 -7
- package/lib/handlers/handshakeFrame.d.ts +25 -0
- package/lib/handlers/ratchetGateWait.d.ts +13 -0
- package/lib/handlers/reconcile.d.ts +24 -0
- package/lib/handlers/requestRoom.d.ts +30 -0
- package/lib/handlers/roomResponse.d.ts +53 -0
- package/lib/index.d.ts +99 -12
- package/lib/index.js +1 -1
- package/lib/index.min.js +1 -1
- package/lib/index.mjs +1 -1
- package/lib/libcrypto.provenance.json +5 -5
- package/lib/libcrypto.wasm +0 -0
- package/lib/middleware/roomListenerMiddleware.d.ts +4 -1
- package/lib/reducers/commonSlice.d.ts +3 -2
- package/lib/reducers/keyPairSlice.d.ts +3 -6
- package/lib/reducers/roomSlice.d.ts +81 -5
- package/lib/reducers/signalingServerSlice.d.ts +3 -2
- package/lib/session.d.ts +6 -3
- package/lib/session.js +1 -1
- package/lib/session.mjs +1 -1
- package/lib/store.d.ts +4 -2
- package/lib/utils/constants.d.ts +12 -0
- package/lib/utils/correlationId.d.ts +4 -0
- package/lib/utils/identityRestore.d.ts +108 -0
- package/lib/utils/interfaces.d.ts +25 -6
- package/lib/utils/roomConnectAdmission.d.ts +15 -0
- package/lib/utils/roomLeaveCoordinator.d.ts +38 -0
- package/lib/utils/roomRequestCoordinator.d.ts +104 -0
- package/lib/utils/roomRosterReconciler.d.ts +83 -0
- package/lib/utils/sdpFingerprint.d.ts +6 -0
- package/lib/utils/signalingAttemptLifecycle.d.ts +33 -0
- package/lib/utils/signalingAuth.d.ts +21 -2
- package/lib/utils/signalingBounds.d.ts +6 -0
- package/lib/utils/signalingFrame.d.ts +21 -0
- package/lib/utils/signalingHeartbeatWatchdog.d.ts +45 -0
- package/lib/utils/signalingIngressQueue.d.ts +49 -0
- package/lib/utils/signalingPeerRequestDebouncer.d.ts +13 -0
- package/lib/utils/signalingRoomLifecycle.d.ts +7 -0
- package/lib/utils/signalingRoomOperationLifecycle.d.ts +14 -0
- package/lib/utils/signalingServerBoundary.d.ts +11 -0
- package/lib/utils/terminalSettlement.d.ts +53 -0
- package/lib/utils/transportIdentity.d.ts +5 -0
- package/package.json +1 -1
package/lib/utils/constants.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** One bound shared by room admission and all signaling lifecycle registries. */
|
|
2
|
+
export declare const MAX_ACTIVE_SIGNALING_ROOMS = 256;
|
|
1
3
|
export declare const MESSAGE_LEN: number;
|
|
2
4
|
export declare const MAX_MESSAGE_SIZE: number;
|
|
3
5
|
export declare const MAX_BUFFERED_AMOUNT: number;
|
|
@@ -49,9 +51,19 @@ export declare const MAX_RETRANSMITS = 5;
|
|
|
49
51
|
export declare const RETRANSMIT_TIMEOUT_MS = 2000;
|
|
50
52
|
export declare const CHANNEL_OPEN_TIMEOUT_MS = 3000;
|
|
51
53
|
export declare const CHANNEL_OPEN_POLL_MS = 25;
|
|
54
|
+
export declare const PEER_ADMISSION_WAIT_MS = 12000;
|
|
52
55
|
export declare const RECONNECT_RESUME_TIMEOUT_MS = 30000;
|
|
53
56
|
export declare const RECONNECT_RESUME_POLL_MS = 500;
|
|
54
57
|
export declare const MAX_RESUME_ATTEMPTS = 3;
|
|
58
|
+
export declare const RATCHET_GATE_WAIT_TIMEOUT_MS = 45000;
|
|
59
|
+
export declare const SIGNALING_HEARTBEAT_TIMEOUT_MS = 25000;
|
|
60
|
+
export declare const SIGNALING_HEARTBEAT_CHECK_MS = 5000;
|
|
61
|
+
export declare const REBIND_AUTHENTICATION_DEADLINE_MS = 10000;
|
|
62
|
+
export declare const PC_OPERATION_POLL_MS = 100;
|
|
63
|
+
export declare const PC_OPERATION_TIMEOUT_MS = 8000;
|
|
55
64
|
export declare const HASH_WINDOW_BYTES: number;
|
|
56
65
|
export declare const HASH_WASM_CHUNK_BYTES: number;
|
|
57
66
|
export declare const OPFS_REASSEMBLE_DIR = "p2party-reassembled";
|
|
67
|
+
export declare const ROOM_ROSTER_RECONCILE_DELAYS_MS: number[];
|
|
68
|
+
export declare const ROOM_ROSTER_RECONCILE_MAX_ATTEMPTS = 8;
|
|
69
|
+
export declare const ROOM_ROSTER_RECONCILE_DEBOUNCE_MS = 4000;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity restore, expressed as effects rather than as calls into the store,
|
|
3
|
+
* the database worker and the signaling API.
|
|
4
|
+
*
|
|
5
|
+
* Restoring a recovery phrase rotates the Ed25519 account key underneath a
|
|
6
|
+
* running mesh, so the order of what happens is the whole of the security
|
|
7
|
+
* argument: transports carrying the outgoing identity are terminal before the
|
|
8
|
+
* incoming one is adopted, and nothing is torn down until a key has actually
|
|
9
|
+
* been derived. Keeping that order here, behind an injectable surface, is what
|
|
10
|
+
* makes it assertable without a socket, a WebRTC stack or IndexedDB.
|
|
11
|
+
*/
|
|
12
|
+
import type { TerminalSettlementStep } from "./terminalSettlement";
|
|
13
|
+
/**
|
|
14
|
+
* A recovery phrase that cannot derive an identity: an unknown word, the wrong
|
|
15
|
+
* number of words, or a failed checksum. Typed so a caller can tell a mistyped
|
|
16
|
+
* phrase apart from a storage or transport failure and keep the user in the
|
|
17
|
+
* form instead of showing them a purge.
|
|
18
|
+
*/
|
|
19
|
+
export declare class InvalidRecoveryPhraseError extends Error {
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
|
22
|
+
/** Raw Ed25519 account key material, owned by the restore that derived it. */
|
|
23
|
+
export interface DerivedIdentityKeyPair {
|
|
24
|
+
publicKey: Uint8Array;
|
|
25
|
+
secretKey: Uint8Array;
|
|
26
|
+
}
|
|
27
|
+
export interface IdentityRestoreEffects {
|
|
28
|
+
/** Stretch the phrase into the Ed25519 identity it encodes (argon2id). */
|
|
29
|
+
readonly deriveIdentity: (mnemonic: string, password?: string) => Promise<DerivedIdentityKeyPair>;
|
|
30
|
+
/**
|
|
31
|
+
* Run the identity swap under the same exclusive boundary a purge uses, so
|
|
32
|
+
* no connection attempt, room operation or bootstrap can interleave with it.
|
|
33
|
+
*/
|
|
34
|
+
readonly runExclusive: <T>(operation: () => Promise<T>) => Promise<T>;
|
|
35
|
+
readonly disconnectSignaling: TerminalSettlementStep;
|
|
36
|
+
/**
|
|
37
|
+
* Close every authenticated WebRTC transport. Rooms, their messages and the
|
|
38
|
+
* address book are kept: this rotates the account key, it does not purge the
|
|
39
|
+
* account.
|
|
40
|
+
*/
|
|
41
|
+
readonly teardownRooms: TerminalSettlementStep;
|
|
42
|
+
/**
|
|
43
|
+
* D2=B: the X25519 identity is cross-signed by the outgoing Ed25519 key, so
|
|
44
|
+
* it must not outlive it. The next bootstrap regenerates and re-cross-signs.
|
|
45
|
+
*/
|
|
46
|
+
readonly deleteIdentityX25519: TerminalSettlementStep;
|
|
47
|
+
readonly writeIdentityEd25519: (identity: DerivedIdentityKeyPair) => void | Promise<void>;
|
|
48
|
+
/** Record that this key came from a phrase, so it can be backed up again. */
|
|
49
|
+
readonly recordDerivation: TerminalSettlementStep;
|
|
50
|
+
/** Drop the previous identity's peer id, challenge and signature. */
|
|
51
|
+
readonly resetIdentityState: TerminalSettlementStep;
|
|
52
|
+
readonly adoptIdentity: (publicKey: string, secretKey: string) => void;
|
|
53
|
+
}
|
|
54
|
+
/** What a caller needs to show the user, and nothing that could identify them further. */
|
|
55
|
+
export interface RestoredIdentity {
|
|
56
|
+
publicKey: string;
|
|
57
|
+
}
|
|
58
|
+
export interface CreateRecoverableIdentityOptions {
|
|
59
|
+
/** Entropy bits: 128 gives a 12-word phrase, 256 a 24-word one. */
|
|
60
|
+
strength?: 128 | 256;
|
|
61
|
+
/**
|
|
62
|
+
* Folded into the derivation as the argon2id salt. It is never stored and
|
|
63
|
+
* never checked, so restoring with a different password silently produces a
|
|
64
|
+
* different identity — the phrase alone is not enough to get back in.
|
|
65
|
+
*/
|
|
66
|
+
password?: string;
|
|
67
|
+
}
|
|
68
|
+
export interface CreatedRecoverableIdentity extends RestoredIdentity {
|
|
69
|
+
/**
|
|
70
|
+
* Shown to the user exactly once. Neither the SDK nor its storage keeps a
|
|
71
|
+
* copy, by design: a phrase at rest is the whole identity at rest.
|
|
72
|
+
*/
|
|
73
|
+
mnemonic: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Adopt the identity a recovery phrase encodes, replacing whatever identity is
|
|
77
|
+
* currently in use.
|
|
78
|
+
*
|
|
79
|
+
* The phrase is checked, then derived, before any effect runs, so a mistyped
|
|
80
|
+
* phrase costs the caller nothing. Only then does the exclusive boundary open:
|
|
81
|
+
* signaling is disconnected, every room transport is brought to a terminal
|
|
82
|
+
* state, and the cross-signed X25519 identity is dropped — all three settle
|
|
83
|
+
* even if one of them fails, and a failure there aborts the swap rather than
|
|
84
|
+
* moving the identity out from under a transport that may still be live.
|
|
85
|
+
*
|
|
86
|
+
* The remaining steps are fail-fast in dependency order: the Ed25519 key is
|
|
87
|
+
* persisted first, because the derivation flag binds to whatever key is stored
|
|
88
|
+
* when it is written; the in-memory identity is adopted last, because an
|
|
89
|
+
* identity that could not be persisted must not survive only until a reload.
|
|
90
|
+
*
|
|
91
|
+
* @param effects - The storage, transport and state seams to drive
|
|
92
|
+
* @param mnemonic - The recovery phrase, in any transcription
|
|
93
|
+
* @param password - The optional passphrase the phrase was created with
|
|
94
|
+
* @returns The restored Ed25519 public key, hex-encoded
|
|
95
|
+
*/
|
|
96
|
+
export declare const restoreIdentityFromMnemonicWith: (effects: IdentityRestoreEffects, mnemonic: string, password?: string) => Promise<RestoredIdentity>;
|
|
97
|
+
/**
|
|
98
|
+
* Generate a recovery phrase and adopt the identity it encodes, so the phrase
|
|
99
|
+
* the user writes down is provably the one that restores this account.
|
|
100
|
+
*
|
|
101
|
+
* The phrase is returned once and kept nowhere. A caller that loses it before
|
|
102
|
+
* the user has copied it has lost the only backup.
|
|
103
|
+
*
|
|
104
|
+
* @param effects - The storage, transport and state seams to drive
|
|
105
|
+
* @param options - Entropy bits and an optional passphrase
|
|
106
|
+
* @returns The phrase and the Ed25519 public key it now derives
|
|
107
|
+
*/
|
|
108
|
+
export declare const createRecoverableIdentityWith: (effects: IdentityRestoreEffects, options?: CreateRecoverableIdentityOptions) => Promise<CreatedRecoverableIdentity>;
|
|
@@ -5,6 +5,19 @@ export interface WebSocketMessagePongResponse {
|
|
|
5
5
|
type: "pong";
|
|
6
6
|
fromPeerId: string;
|
|
7
7
|
}
|
|
8
|
+
export interface WebSocketMessageLeaveRoomRequest {
|
|
9
|
+
type: "leaveRoom";
|
|
10
|
+
protocolVersion: 4;
|
|
11
|
+
fromPeerId: string;
|
|
12
|
+
roomUrl: string;
|
|
13
|
+
roomLeaveId: string;
|
|
14
|
+
}
|
|
15
|
+
export interface WebSocketMessageRoomLeftResponse {
|
|
16
|
+
type: "roomLeft";
|
|
17
|
+
protocolVersion: 4;
|
|
18
|
+
roomUrl: string;
|
|
19
|
+
roomLeaveId: string;
|
|
20
|
+
}
|
|
8
21
|
export interface WebSocketMessageChallengeRequest {
|
|
9
22
|
type: "peerId";
|
|
10
23
|
peerId: string;
|
|
@@ -23,28 +36,34 @@ export interface WebSocketMessageSuccessfulChallenge {
|
|
|
23
36
|
type: "challenge";
|
|
24
37
|
challengeId: string;
|
|
25
38
|
protocolVersion: number;
|
|
26
|
-
username?: string;
|
|
27
|
-
credential?: string;
|
|
28
39
|
}
|
|
29
40
|
export interface WebSocketMessageRoomIdRequest {
|
|
30
41
|
type: "room";
|
|
31
42
|
fromPeerId: string;
|
|
32
43
|
roomUrl: string;
|
|
33
|
-
|
|
44
|
+
/** Fresh request/response correlation nonce. Never reused across requests. */
|
|
45
|
+
roomRequestId: string;
|
|
46
|
+
protocolVersion: 4;
|
|
34
47
|
}
|
|
35
48
|
export interface TurnCredentials {
|
|
36
49
|
urls: string[];
|
|
37
50
|
username: string;
|
|
38
51
|
credential: string;
|
|
39
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Absolute TURN REST expiry. It must equal the timestamp prefix authenticated
|
|
54
|
+
* inside `username`; carrying it explicitly lets clients schedule refresh
|
|
55
|
+
* without interpreting an undocumented deployment convention.
|
|
56
|
+
*/
|
|
57
|
+
expiresAtUnixSeconds: number;
|
|
40
58
|
}
|
|
41
59
|
export interface WebSocketMessageRoomIdResponse {
|
|
42
60
|
type: "roomId";
|
|
43
61
|
roomId: string;
|
|
44
62
|
roomUrl: string;
|
|
63
|
+
/** Exact echo of the request's fresh `roomRequestId`. */
|
|
64
|
+
roomRequestId: string;
|
|
45
65
|
turnCredentials?: TurnCredentials;
|
|
46
|
-
|
|
47
|
-
protocolVersion?: number;
|
|
66
|
+
protocolVersion: 4;
|
|
48
67
|
}
|
|
49
68
|
export interface WebSocketMessageDescriptionSend {
|
|
50
69
|
type: "description";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RoomPolicyV1 } from "../roomPolicy";
|
|
2
|
+
export interface RoomConnectAdmissionInput {
|
|
3
|
+
readonly existingPolicy?: RoomPolicyV1;
|
|
4
|
+
readonly persistedPolicy?: RoomPolicyV1;
|
|
5
|
+
readonly requestedPolicy?: RoomPolicyV1;
|
|
6
|
+
readonly providedPin?: unknown;
|
|
7
|
+
readonly hasTransientPin: boolean;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* One pure admission decision for every public room-connect entry point.
|
|
11
|
+
*
|
|
12
|
+
* In particular, an empty Redux store does not imply a new no-PIN room when
|
|
13
|
+
* IndexedDB retains an immutable PIN policy for the same capability.
|
|
14
|
+
*/
|
|
15
|
+
export declare const resolveRoomConnectAdmission: ({ existingPolicy, persistedPolicy, requestedPolicy, providedPin, hasTransientPin, }: RoomConnectAdmissionInput) => RoomPolicyV1;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { WebSocketMessageLeaveRoomRequest, WebSocketMessageRoomLeftResponse } from "./interfaces";
|
|
2
|
+
export declare const ROOM_LEAVE_ACK_TIMEOUT_MS = 2000;
|
|
3
|
+
export declare const MAX_PENDING_ROOM_LEAVES = 256;
|
|
4
|
+
export type RemoteRoomRevocationFailure = "no-current-socket" | "send-failed" | "ack-timeout" | "socket-closed" | "socket-replaced" | "ingress-rejected";
|
|
5
|
+
/**
|
|
6
|
+
* Local room state and transports are already terminal when this is surfaced.
|
|
7
|
+
* It says only that authoritative unlinking at the rendezvous server was not
|
|
8
|
+
* confirmed; callers may safely forget the local room while reporting that
|
|
9
|
+
* narrower remote uncertainty.
|
|
10
|
+
*/
|
|
11
|
+
export declare class RemoteRoomRevocationUnconfirmedError extends Error {
|
|
12
|
+
readonly roomUrl: string;
|
|
13
|
+
readonly roomLeaveId: string;
|
|
14
|
+
readonly reason: RemoteRoomRevocationFailure;
|
|
15
|
+
constructor(roomUrl: string, roomLeaveId: string, reason: RemoteRoomRevocationFailure, detail?: string);
|
|
16
|
+
}
|
|
17
|
+
type SendRoomLeave = (request: WebSocketMessageLeaveRoomRequest) => void | Promise<unknown>;
|
|
18
|
+
export declare const buildRoomLeaveRequest: (fromPeerId: string, roomUrl: string, roomLeaveId?: string) => WebSocketMessageLeaveRoomRequest;
|
|
19
|
+
export interface RequestRoomLeaveOptions {
|
|
20
|
+
readonly timeoutMs?: number;
|
|
21
|
+
readonly roomLeaveId?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Install correlation before sending, so even a synchronous in-memory server
|
|
25
|
+
* acknowledgement cannot outrun the waiter.
|
|
26
|
+
*/
|
|
27
|
+
export declare const requestRoomLeaveAcknowledgement: (fromPeerId: string, roomUrl: string, socketToken: object, send: SendRoomLeave, options?: RequestRoomLeaveOptions) => Promise<void>;
|
|
28
|
+
export declare const hasExactRoomLeftShape: (message: WebSocketMessageRoomLeftResponse) => boolean;
|
|
29
|
+
/** Parse only the tiny exact ack shape used by the socket fast path. */
|
|
30
|
+
export declare const parseRoomLeftAcknowledgement: (data: unknown) => WebSocketMessageRoomLeftResponse | null;
|
|
31
|
+
/**
|
|
32
|
+
* Settle only the exact socket+room+nonce waiter. The acknowledgement has no
|
|
33
|
+
* reducer side effect, so a duplicate or late frame cannot resurrect a room.
|
|
34
|
+
*/
|
|
35
|
+
export declare const acknowledgeRoomLeft: (message: WebSocketMessageRoomLeftResponse, socketToken: object) => boolean;
|
|
36
|
+
export declare const cancelRoomLeavesForSocket: (socketToken: object, reason: Exclude<RemoteRoomRevocationFailure, "no-current-socket" | "send-failed" | "ack-timeout">) => number;
|
|
37
|
+
export declare const pendingRoomLeaveCount: () => number;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { MAX_ACTIVE_SIGNALING_ROOMS } from "./constants";
|
|
2
|
+
import type { WebSocketMessageRoomIdRequest } from "./interfaces";
|
|
3
|
+
export { MAX_ACTIVE_SIGNALING_ROOMS };
|
|
4
|
+
export declare const MAX_PENDING_ROOM_REQUESTS = 256;
|
|
5
|
+
export declare const MAX_ROOM_REQUEST_RETRY_WAITERS = 256;
|
|
6
|
+
export declare const ROOM_RESPONSE_TIMEOUT_MS = 15000;
|
|
7
|
+
export declare const SOCKET_PROOF_TIMEOUT_MS = 15000;
|
|
8
|
+
export declare const TURN_REFRESH_LEAD_MS: number;
|
|
9
|
+
export declare const MIN_TURN_REFRESH_DELAY_MS = 1000;
|
|
10
|
+
export declare const TURN_REFRESH_RETRY_BASE_MS = 1000;
|
|
11
|
+
export declare const MAX_TURN_REFRESH_RETRY_MS = 30000;
|
|
12
|
+
export declare const MAX_TURN_REFRESH_ATTEMPTS = 8;
|
|
13
|
+
export interface AppliedRoomResponse {
|
|
14
|
+
roomUrl: string;
|
|
15
|
+
roomId: string;
|
|
16
|
+
roomRequestId: string;
|
|
17
|
+
requestGeneration: number;
|
|
18
|
+
expiresAtUnixSeconds: number | null;
|
|
19
|
+
}
|
|
20
|
+
type SendRoomRequest = (request: WebSocketMessageRoomIdRequest) => void | Promise<unknown>;
|
|
21
|
+
export interface RoomCredentialRefreshReservation {
|
|
22
|
+
readonly roomUrl: string;
|
|
23
|
+
readonly token: symbol;
|
|
24
|
+
}
|
|
25
|
+
export declare const buildRoomRequest: (peerId: string, roomUrl: string) => WebSocketMessageRoomIdRequest;
|
|
26
|
+
/**
|
|
27
|
+
* Start one wire-bound room request, or join the exact in-flight request.
|
|
28
|
+
*
|
|
29
|
+
* A fresh UUID is carried on the wire and must be echoed by the server. Merely
|
|
30
|
+
* keeping a local generation would not distinguish an old same-room response
|
|
31
|
+
* that arrived during a later request.
|
|
32
|
+
*/
|
|
33
|
+
export declare const requestRoomResponse: (peerId: string, roomUrl: string, send: SendRoomRequest, options?: {
|
|
34
|
+
readonly timeoutMs?: number;
|
|
35
|
+
}) => Promise<AppliedRoomResponse>;
|
|
36
|
+
export interface PendingRoomResponseClaim {
|
|
37
|
+
readonly roomUrl: string;
|
|
38
|
+
readonly roomRequestId: string;
|
|
39
|
+
readonly requestGeneration: number;
|
|
40
|
+
}
|
|
41
|
+
/** Claim only the exact echoed UUID of the current single-flight request. */
|
|
42
|
+
export declare const claimRoomResponse: (roomUrl: string, roomRequestId: string) => PendingRoomResponseClaim | null;
|
|
43
|
+
export declare const completeRoomResponse: (response: AppliedRoomResponse) => boolean;
|
|
44
|
+
export declare const rejectRoomResponse: (roomUrl: string, roomRequestId: string, error: Error) => boolean;
|
|
45
|
+
export declare const pendingRoomRequestId: (roomUrl: string) => string | null;
|
|
46
|
+
/** True only while the exact claimed response still owns its pending slot. */
|
|
47
|
+
export declare const isRoomResponseClaimCurrent: (claim: PendingRoomResponseClaim) => boolean;
|
|
48
|
+
export declare const waitForFreshSocketProof: (alreadyVerified: boolean, timeoutMs?: number, lifecycleEpoch?: number) => Promise<void>;
|
|
49
|
+
export declare const markFreshSocketProof: (lifecycleEpoch?: number) => void;
|
|
50
|
+
export declare const currentRoomRequestLifecycleEpoch: () => number;
|
|
51
|
+
export declare const isRoomRequestLifecycleCurrent: (lifecycleEpoch: number) => boolean;
|
|
52
|
+
export declare const waitForRoomRequestRetry: (roomUrl: string, lifecycleEpoch: number, delayMs: number) => Promise<void>;
|
|
53
|
+
export declare const turnRefreshDelayMs: (expiresAtUnixSeconds: number, nowMs?: number, minimumDelayMs?: number) => number;
|
|
54
|
+
export interface RoomCredentialRefreshOptions {
|
|
55
|
+
/** Test seam; production uses Date.now(). */
|
|
56
|
+
nowMs?: number;
|
|
57
|
+
/** Dynamic test seam for time that can advance while refresh is awaited. */
|
|
58
|
+
now?: () => number;
|
|
59
|
+
/** Test seams; production uses the bounded constants above. */
|
|
60
|
+
retryBaseMs?: number;
|
|
61
|
+
maxRetryMs?: number;
|
|
62
|
+
maxAttempts?: number;
|
|
63
|
+
minimumInitialDelayMs?: number;
|
|
64
|
+
}
|
|
65
|
+
export declare const turnRefreshRetryDelayMs: (attempt: number, remainingMs: number, options?: RoomCredentialRefreshOptions) => number;
|
|
66
|
+
export declare const scheduleRoomCredentialRefresh: (roomUrl: string, expiresAtUnixSeconds: number | null, refresh: () => Promise<unknown>, expire?: () => void | Promise<void>, options?: RoomCredentialRefreshOptions, ownerToken?: symbol) => void;
|
|
67
|
+
/**
|
|
68
|
+
* Preflight used immediately before publishing a room response.
|
|
69
|
+
*
|
|
70
|
+
* A replacement owns its existing slot; a STUN-only response needs no slot.
|
|
71
|
+
* Callers recheck after asynchronous live-PC configuration, then publish and
|
|
72
|
+
* schedule synchronously so the bound cannot change between those operations.
|
|
73
|
+
*/
|
|
74
|
+
export declare function canScheduleRoomCredentialRefresh(roomUrl: string, expiresAtUnixSeconds: number | null): boolean;
|
|
75
|
+
export declare const reserveRoomCredentialRefresh: (roomUrl: string, expiresAtUnixSeconds: number | null) => RoomCredentialRefreshReservation;
|
|
76
|
+
export declare const isRoomCredentialRefreshReservationCurrent: (reservation: RoomCredentialRefreshReservation) => boolean;
|
|
77
|
+
export declare const releaseRoomCredentialRefreshReservation: (reservation: RoomCredentialRefreshReservation) => boolean;
|
|
78
|
+
export declare const finalizeRoomCredentialRefresh: (reservation: RoomCredentialRefreshReservation, expiresAtUnixSeconds: number | null, refresh: () => Promise<unknown>, expire?: () => void | Promise<void>, options?: RoomCredentialRefreshOptions) => void;
|
|
79
|
+
/**
|
|
80
|
+
* Release whichever refresh object this exact response still owns.
|
|
81
|
+
*
|
|
82
|
+
* A stale handler must never clear a replacement reservation/timer installed
|
|
83
|
+
* for the same room URL after delete/reactivate or socket reset.
|
|
84
|
+
*/
|
|
85
|
+
export declare const clearRoomCredentialRefreshIfOwned: (reservation: RoomCredentialRefreshReservation) => boolean;
|
|
86
|
+
export declare const clearRoomCredentialRefresh: (roomUrl: string) => void;
|
|
87
|
+
/** Cancel every coordinator handle owned by one retained room. */
|
|
88
|
+
export declare const cancelRoomRequestLifecycle: (roomUrl: string, reason?: string) => void;
|
|
89
|
+
/**
|
|
90
|
+
* Socket teardown invalidates responses and fails closed on managed TURN.
|
|
91
|
+
*
|
|
92
|
+
* With no authenticated path available to refresh, keeping a credential timer
|
|
93
|
+
* but losing its refresh context risks stale use after background suspension.
|
|
94
|
+
* Remove the managed bundle now; a later proof repopulates it before repair.
|
|
95
|
+
*/
|
|
96
|
+
export declare const resetRoomRequestLifecycle: (reason?: string) => void;
|
|
97
|
+
/** Test-only observability without exposing mutable coordinator maps. */
|
|
98
|
+
export declare const roomRequestCoordinatorCounts: () => {
|
|
99
|
+
pending: number;
|
|
100
|
+
proofWaiters: number;
|
|
101
|
+
retryWaiters: number;
|
|
102
|
+
refreshTimers: number;
|
|
103
|
+
refreshReservations: number;
|
|
104
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded "is anyone still there?" retries for a room that has nobody in it.
|
|
3
|
+
*
|
|
4
|
+
* Every roster request this SDK sends is caused by an event: a room join, a
|
|
5
|
+
* peer introduction from the server, an edge teardown that re-dials. That is
|
|
6
|
+
* enough while events keep arriving, and nothing at all once they stop. A
|
|
7
|
+
* socket can be connected and identity-verified, its room still active, and
|
|
8
|
+
* its roster empty for good — no peer to hear from, no teardown left to fire,
|
|
9
|
+
* so no event that would ask again. Both sides then sit in a healthy-looking
|
|
10
|
+
* room, alone, until the user reloads the page.
|
|
11
|
+
*
|
|
12
|
+
* The property this restores is deliberately weak and deliberately bounded: a
|
|
13
|
+
* verified socket in an active room with nobody authenticated on it asks the
|
|
14
|
+
* server for its peer list again on a backoff, a capped number of times, and
|
|
15
|
+
* stops the moment anything changes. It is not a keepalive and not a repair
|
|
16
|
+
* loop — a peers request is idempotent at the server (it re-introduces this
|
|
17
|
+
* client to the room's live members, which is exactly what a stranded room
|
|
18
|
+
* needs) and the caller still owns whether one may be sent at all.
|
|
19
|
+
*
|
|
20
|
+
* Store-free on purpose: the caller injects the timeout scheduler and decides,
|
|
21
|
+
* at each attempt, whether the room is still stranded. Nothing here reads a
|
|
22
|
+
* clock, a store or a browser global, so the whole backoff is exercisable in
|
|
23
|
+
* virtual time.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Runs one reconciliation attempt for a room. Returns whether the room is
|
|
27
|
+
* still stranded and the backoff should continue; `false` retires it.
|
|
28
|
+
*/
|
|
29
|
+
export type RoomRosterReconcileTick = (roomId: string) => boolean;
|
|
30
|
+
export interface RoomRosterReconcilerScheduler<Handle> {
|
|
31
|
+
readonly setTimeout: (callback: () => void, delayMs: number) => Handle;
|
|
32
|
+
readonly clearTimeout: (handle: Handle) => void;
|
|
33
|
+
}
|
|
34
|
+
export interface RoomRosterReconcilerOptions<Handle> {
|
|
35
|
+
readonly schedule: RoomRosterReconcilerScheduler<Handle>;
|
|
36
|
+
/** Delay before attempt n; the last entry is the cap for every attempt after. */
|
|
37
|
+
readonly delaysMs: readonly number[];
|
|
38
|
+
/** Total attempts allowed for one uninterrupted strand. */
|
|
39
|
+
readonly maxAttempts: number;
|
|
40
|
+
}
|
|
41
|
+
export interface RoomRosterReconciler {
|
|
42
|
+
/**
|
|
43
|
+
* Report that a room is stranded. Idempotent: a room already under a backoff
|
|
44
|
+
* keeps its schedule and its attempt count, and only its tick is refreshed,
|
|
45
|
+
* so repeated observations can never rewind the backoff or stack timers. A
|
|
46
|
+
* room that already spent its attempts stays retired until `cancel`.
|
|
47
|
+
*/
|
|
48
|
+
arm(roomId: string, tick: RoomRosterReconcileTick): void;
|
|
49
|
+
/** Forget a room: releases its timer and resets its attempt budget. */
|
|
50
|
+
cancel(roomId: string): void;
|
|
51
|
+
/** Forget every room, for a lost socket or a purged store. */
|
|
52
|
+
cancelAll(): void;
|
|
53
|
+
/** Rooms this reconciler still tracks, retired ones included. */
|
|
54
|
+
armedRooms(): readonly string[];
|
|
55
|
+
}
|
|
56
|
+
export declare const createRoomRosterReconciler: <Handle>(options: RoomRosterReconcilerOptions<Handle>) => RoomRosterReconciler;
|
|
57
|
+
/** The shape of a room this predicate needs; a superset of the reducer's. */
|
|
58
|
+
export interface ReconcilableRoom {
|
|
59
|
+
readonly id: string;
|
|
60
|
+
readonly signalingActive?: boolean;
|
|
61
|
+
readonly peers: readonly {
|
|
62
|
+
readonly peerId: string;
|
|
63
|
+
}[];
|
|
64
|
+
readonly handshakeStatusByPeer?: Readonly<Record<string, {
|
|
65
|
+
readonly status: string;
|
|
66
|
+
} | undefined>>;
|
|
67
|
+
}
|
|
68
|
+
export interface ReconcilableSocket {
|
|
69
|
+
readonly isConnected: boolean;
|
|
70
|
+
readonly isVerified: boolean;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether a room is worth asking about again.
|
|
74
|
+
*
|
|
75
|
+
* "Nobody to talk to" is not the same as "no peers": a roster full of edges
|
|
76
|
+
* that never finished authenticating is just as empty in practice, and the
|
|
77
|
+
* server re-introducing this client is what gives those edges a fresh start.
|
|
78
|
+
* A room the user left (`signalingActive === false`, which `deleteRoom` sets),
|
|
79
|
+
* a room with no signaling identity yet, and any socket that is not both
|
|
80
|
+
* connected and verified are all excluded — a request on those either cannot
|
|
81
|
+
* be sent or would be an unwanted rejoin.
|
|
82
|
+
*/
|
|
83
|
+
export declare const isRoomRosterStranded: (room: ReconcilableRoom, socket: ReconcilableSocket) => boolean;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw bytes of the `a=fingerprint:sha-256 XX:XX:...` line in an SDP blob.
|
|
3
|
+
* Throws if no sha-256 fingerprint attribute is present, or if the one found
|
|
4
|
+
* doesn't decode to exactly 32 bytes.
|
|
5
|
+
*/
|
|
6
|
+
export declare const parseFingerprintFromSdp: (sdp: string) => Uint8Array;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface SignalingAttempt {
|
|
2
|
+
readonly token: symbol;
|
|
3
|
+
readonly cancelled: Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* SSOT for socket-attempt ownership, prompt cancellation, and serialized
|
|
7
|
+
* identity bootstrap. Cancellation settles independently of a blocked
|
|
8
|
+
* IndexedDB/crypto operation; the bootstrap lock remains held until that
|
|
9
|
+
* operation unwinds, so its replacement cannot race identity mutation.
|
|
10
|
+
*/
|
|
11
|
+
export declare class SignalingAttemptLifecycle {
|
|
12
|
+
private activeToken;
|
|
13
|
+
private cancelCurrent;
|
|
14
|
+
private identityPurgeRequests;
|
|
15
|
+
private readonly bootstrapMutex;
|
|
16
|
+
begin(label: string): SignalingAttempt;
|
|
17
|
+
isCurrent(token: symbol): boolean;
|
|
18
|
+
hasActiveAttempt(): boolean;
|
|
19
|
+
assertCurrent(token: symbol): void;
|
|
20
|
+
addCancellationEffect(token: symbol, effect: () => void): boolean;
|
|
21
|
+
disarmCancellation(token: symbol): boolean;
|
|
22
|
+
clear(token: symbol): boolean;
|
|
23
|
+
cancelActive(): void;
|
|
24
|
+
isIdentityPurgeActive(): boolean;
|
|
25
|
+
acquireBootstrap(token: symbol): Promise<() => void>;
|
|
26
|
+
/**
|
|
27
|
+
* Cancel any socket/bootstrap attempt, then enter the same exclusive
|
|
28
|
+
* identity boundary used by bootstrap. Purge uses this so no cancelled
|
|
29
|
+
* bootstrap can persist or dispatch an identity after deletion.
|
|
30
|
+
*/
|
|
31
|
+
cancelAndAcquireBootstrap(): Promise<() => void>;
|
|
32
|
+
}
|
|
33
|
+
export declare const signalingAttemptLifecycle: SignalingAttemptLifecycle;
|
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
import type { WebSocketMessageChallengeRequest, WebSocketMessageSuccessfulChallenge } from "./interfaces";
|
|
1
|
+
import type { TurnCredentials, WebSocketMessageChallengeRequest, WebSocketMessageSuccessfulChallenge } from "./interfaces";
|
|
2
|
+
export declare const MIN_TURN_CREDENTIAL_REMAINING_SECONDS = 30;
|
|
3
|
+
export declare const MAX_TURN_CREDENTIAL_LIFETIME_SECONDS: number;
|
|
4
|
+
/** Stale bearer-shaped key fields never authenticate a replacement socket. */
|
|
5
|
+
export declare const hasCurrentSocketProof: (keyPair: {
|
|
6
|
+
peerId: string;
|
|
7
|
+
challenge: string;
|
|
8
|
+
signature: string;
|
|
9
|
+
}, signalingServer: {
|
|
10
|
+
isConnected: boolean;
|
|
11
|
+
isVerified: boolean;
|
|
12
|
+
}) => boolean;
|
|
2
13
|
/**
|
|
3
14
|
* Exact protocol-v3 server challenge. Legacy reconnect credentials are
|
|
4
15
|
* deliberately not accepted: every WebSocket must prove possession anew.
|
|
5
16
|
*/
|
|
6
17
|
export declare const isFreshV3Challenge: (value: unknown) => value is WebSocketMessageChallengeRequest;
|
|
7
|
-
/** Exact protocol-v3 proof-success response
|
|
18
|
+
/** Exact protocol-v3 proof-success response. TURN is issued only at room join. */
|
|
8
19
|
export declare const isV3ChallengeSuccess: (value: unknown) => value is WebSocketMessageSuccessfulChallenge;
|
|
20
|
+
/**
|
|
21
|
+
* Decode the one nested TURN credential object used by room responses.
|
|
22
|
+
*
|
|
23
|
+
* The server controls the relay, but only bounded `turn:`/`turns:` URIs may
|
|
24
|
+
* reach the browser ICE stack. Rejecting the complete object on one malformed
|
|
25
|
+
* field keeps this a closed wire contract rather than a best-effort filter.
|
|
26
|
+
*/
|
|
27
|
+
export declare const parseTurnCredentials: (value: unknown, nowMs?: number, expectedPublicKey?: string) => TurnCredentials | null;
|
|
@@ -4,6 +4,12 @@ export declare const MAX_ICE_CANDIDATE_CHARS = 4096;
|
|
|
4
4
|
export declare const MAX_ICE_FIELD_CHARS = 256;
|
|
5
5
|
export declare const MAX_SIGNALING_LABELS = 64;
|
|
6
6
|
export declare const MAX_DATA_CHANNEL_LABEL_CHARS = 193;
|
|
7
|
+
/** Per-client legacy mesh edge cap; also bounds cumulative server roster state. */
|
|
8
|
+
export declare const MAX_SIGNALING_ROOM_PEERS = 64;
|
|
9
|
+
/** At most 64 active logical channels per bounded room edge. */
|
|
10
|
+
export declare const MAX_SIGNALING_ROOM_CHANNELS: number;
|
|
7
11
|
export declare const isBoundedDescription: (description: RTCSessionDescription | RTCSessionDescriptionInit) => boolean;
|
|
8
12
|
export declare const isBoundedIceCandidate: (candidate: RTCIceCandidateInit | RTCIceCandidate) => boolean;
|
|
9
13
|
export declare const areBoundedDataChannelLabels: (labels: unknown) => labels is string[];
|
|
14
|
+
/** Legacy connection signaling negotiates only the persistent handshake lane. */
|
|
15
|
+
export declare const areCanonicalConnectionLabels: (labels: unknown) => labels is ["main"];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { BaseQueryApi } from "@reduxjs/toolkit/query";
|
|
2
|
+
import type { WebSocketPeerConnectionParams } from "./interfaces";
|
|
3
|
+
export type WebSocketBaseResult = {
|
|
4
|
+
data: undefined;
|
|
5
|
+
} | {
|
|
6
|
+
error: string;
|
|
7
|
+
};
|
|
8
|
+
type SignalingQueryApi = Pick<BaseQueryApi, "dispatch" | "getState">;
|
|
9
|
+
type SignalingSocket = Pick<WebSocket, "readyState" | "send">;
|
|
10
|
+
export interface AuthenticatedSignalingFrame<Content extends {
|
|
11
|
+
fromPeerId: string;
|
|
12
|
+
} = {
|
|
13
|
+
fromPeerId: string;
|
|
14
|
+
}> {
|
|
15
|
+
content: Content;
|
|
16
|
+
}
|
|
17
|
+
export declare const sendAuthenticatedSignalingFrame: <Content extends {
|
|
18
|
+
fromPeerId: string;
|
|
19
|
+
}>(socket: SignalingSocket, message: AuthenticatedSignalingFrame<Content>, api: SignalingQueryApi, isCurrentSocket: () => boolean) => WebSocketBaseResult;
|
|
20
|
+
export declare const sendPeerConnectionRequest: (socket: SignalingSocket, { peerId, peerPublicKey, roomId }: WebSocketPeerConnectionParams, api: SignalingQueryApi, isCurrentSocket: () => boolean) => WebSocketBaseResult;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wall-clock watchdog over the signaling server's heartbeats.
|
|
3
|
+
*
|
|
4
|
+
* A WebSocket that the platform silently half-opened (page frozen with the
|
|
5
|
+
* radio off, NAT binding dropped) fires no `close` event until the TCP stack
|
|
6
|
+
* notices, which can take minutes. Meanwhile the server, which pings on a
|
|
7
|
+
* fixed interval, has already closed its end. This watchdog owns no browser
|
|
8
|
+
* globals: the caller injects the clock, the interval scheduler and the wake
|
|
9
|
+
* event source, so it is exactly testable and inert outside a browser.
|
|
10
|
+
*
|
|
11
|
+
* `check()` runs on the interval and immediately on every wake event
|
|
12
|
+
* (`visibilitychange` while visible, `pageshow`, `online`, `focus`), because
|
|
13
|
+
* timers are throttled or suspended while a page is hidden and the first
|
|
14
|
+
* useful moment to notice a dead socket is the moment the page wakes up.
|
|
15
|
+
*/
|
|
16
|
+
export type SignalingWakeEventName = "visibilitychange" | "pageshow" | "online" | "focus";
|
|
17
|
+
export interface SignalingHeartbeatWatchdogScheduler<Handle> {
|
|
18
|
+
readonly setInterval: (callback: () => void, intervalMs: number) => Handle;
|
|
19
|
+
readonly clearInterval: (handle: Handle) => void;
|
|
20
|
+
}
|
|
21
|
+
export interface SignalingHeartbeatWatchdogEnv {
|
|
22
|
+
readonly addListener: (name: SignalingWakeEventName, listener: () => void) => void;
|
|
23
|
+
readonly removeListener: (name: SignalingWakeEventName, listener: () => void) => void;
|
|
24
|
+
/** Consulted on `visibilitychange`; a page going hidden is not a wake. */
|
|
25
|
+
readonly isVisible: () => boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface SignalingHeartbeatWatchdogOptions<Handle> {
|
|
28
|
+
/** Wall clock in milliseconds; must keep advancing while the page sleeps. */
|
|
29
|
+
readonly now: () => number;
|
|
30
|
+
readonly timeoutMs: number;
|
|
31
|
+
readonly checkEveryMs: number;
|
|
32
|
+
readonly schedule: SignalingHeartbeatWatchdogScheduler<Handle>;
|
|
33
|
+
readonly env?: SignalingHeartbeatWatchdogEnv;
|
|
34
|
+
/** Invoked at most once; the watchdog has already stopped itself. */
|
|
35
|
+
readonly onDead: () => void;
|
|
36
|
+
}
|
|
37
|
+
export interface SignalingHeartbeatWatchdog {
|
|
38
|
+
/** Record that a server heartbeat was just answered. */
|
|
39
|
+
beat(): void;
|
|
40
|
+
/** Declare the socket dead if the heartbeat window has elapsed. */
|
|
41
|
+
check(): void;
|
|
42
|
+
/** Release the interval and wake listeners; idempotent and terminal. */
|
|
43
|
+
stop(): void;
|
|
44
|
+
}
|
|
45
|
+
export declare const createSignalingHeartbeatWatchdog: <Handle>(options: SignalingHeartbeatWatchdogOptions<Handle>) => SignalingHeartbeatWatchdog;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export declare const MAX_SIGNALING_INGRESS_BACKLOG = 128;
|
|
2
|
+
export declare const MAX_SIGNALING_INGRESS_BYTES: number;
|
|
3
|
+
export type SignalingHeartbeatKind = "raw" | "json";
|
|
4
|
+
/**
|
|
5
|
+
* Heartbeats bypass protocol work so an ICE/IndexedDB operation cannot make a
|
|
6
|
+
* healthy client miss the server timeout. Parsing is restricted to a tiny
|
|
7
|
+
* input and the JSON form must be exactly `{type:"ping"}`.
|
|
8
|
+
*/
|
|
9
|
+
export declare const classifySignalingHeartbeat: (data: unknown) => SignalingHeartbeatKind | null;
|
|
10
|
+
export interface SignalingHeartbeatResponders {
|
|
11
|
+
readonly raw: () => void;
|
|
12
|
+
readonly json: () => void;
|
|
13
|
+
}
|
|
14
|
+
/** Answer one exact heartbeat synchronously; return false for protocol data. */
|
|
15
|
+
export declare const answerImmediateSignalingHeartbeat: (data: unknown, responders: SignalingHeartbeatResponders) => boolean;
|
|
16
|
+
export interface SignalingIngressQueueOptions {
|
|
17
|
+
readonly maxPending?: number;
|
|
18
|
+
readonly maxPendingBytes?: number;
|
|
19
|
+
readonly onOverflow: () => void;
|
|
20
|
+
readonly onTaskError?: (error: unknown) => void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Synchronously revocable ownership for handlers already running on a socket.
|
|
24
|
+
* Closing a WebSocket is asynchronous, so exact-socket identity alone remains
|
|
25
|
+
* true until `close`; this lease makes overflow/oversize rejection immediate.
|
|
26
|
+
*/
|
|
27
|
+
export declare class SignalingSocketIngressLease {
|
|
28
|
+
#private;
|
|
29
|
+
isCurrent(ownsSocket: () => boolean): boolean;
|
|
30
|
+
invalidate(): void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A socket-local, bounded FIFO for signaling frames.
|
|
34
|
+
*
|
|
35
|
+
* WebSocket EventTarget ignores promises returned by `onmessage`; assigning an
|
|
36
|
+
* async handler therefore starts unbounded concurrent protocol and IndexedDB
|
|
37
|
+
* work. This queue preserves wire order and closes admission before invoking
|
|
38
|
+
* the overflow callback, so a synchronous burst cannot schedule more work
|
|
39
|
+
* while the socket close handshake is in flight.
|
|
40
|
+
*/
|
|
41
|
+
export declare class SignalingIngressQueue {
|
|
42
|
+
#private;
|
|
43
|
+
constructor(options: SignalingIngressQueueOptions);
|
|
44
|
+
get pendingCount(): number;
|
|
45
|
+
get pendingBytes(): number;
|
|
46
|
+
get isClosed(): boolean;
|
|
47
|
+
enqueue(task: () => void | Promise<void>, byteLength?: number): boolean;
|
|
48
|
+
close(): void;
|
|
49
|
+
}
|