@net-mesh/browser 0.41.1 → 0.43.0
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/CHANGELOG.md +24 -0
- package/README.md +12 -0
- package/dist/index.bundle.js +2 -2
- package/dist/leader/session.d.ts +4 -2
- package/dist/leader/session.js +30 -6
- package/dist/lobby.js +8 -4
- package/dist/net_leaf.d.ts +10 -10
- package/dist/net_leaf.js +32 -32
- package/dist/net_leaf_bg.wasm +0 -0
- package/dist/net_leaf_bg.wasm.d.ts +10 -10
- package/dist/node.d.ts +15 -0
- package/dist/node.js +75 -1
- package/dist/store/host.js +10 -4
- package/dist/store/join.js +38 -2
- package/dist/store/owner.d.ts +11 -0
- package/dist/store/owner.js +27 -7
- package/dist/store/types.d.ts +6 -0
- package/dist/stream.d.ts +29 -1
- package/dist/stream.js +66 -4
- package/dist/udp-probe.d.ts +21 -3
- package/dist/udp-probe.js +78 -7
- package/package.json +1 -1
package/dist/leader/session.d.ts
CHANGED
|
@@ -57,7 +57,10 @@ export interface SessionOptions extends ConnectOptions {
|
|
|
57
57
|
dbName?: string;
|
|
58
58
|
/**
|
|
59
59
|
* Override the Web Lock and `BroadcastChannel` name. Defaults to
|
|
60
|
-
* `net-mesh/<origin>/<identity-fingerprint
|
|
60
|
+
* `net-mesh/<origin>/<identity-fingerprint>`, or — with a custodial
|
|
61
|
+
* `entitySecretHex` — to the identity lock `connect()` takes for that
|
|
62
|
+
* secret, so a `connect()` tab and an `openSession()` tab cannot both run
|
|
63
|
+
* the identity. Setting it opts out of that contention.
|
|
61
64
|
*
|
|
62
65
|
* For tests that need two independent elections inside one origin.
|
|
63
66
|
* A page should not set it: two scopes on one identity is two nodes
|
|
@@ -65,7 +68,6 @@ export interface SessionOptions extends ConnectOptions {
|
|
|
65
68
|
*/
|
|
66
69
|
lockScope?: string;
|
|
67
70
|
}
|
|
68
|
-
/** The origin's Net node, as this tab sees it. */
|
|
69
71
|
export declare class MeshSession {
|
|
70
72
|
private readonly inner;
|
|
71
73
|
/**
|
package/dist/leader/session.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { fromWasmError, OrgCancelledError, OrgLeaderLostError, } from '../errors.js';
|
|
30
30
|
import { clientStreamOrgTrampoline, duplexOrgTrampoline, OrgDuplex, OrgHandleRegistry, OrgServe, OrgStream, OrgUpload, streamingOrgTrampoline, toWasmOrgCallOptions, toWasmOrgServeOptions, unaryOrgTrampoline, } from '../org.js';
|
|
31
|
-
import { buildConnectRequest, parseAttemptStatus, parseCounters, parseDescriptors, } from '../node.js';
|
|
31
|
+
import { buildConnectRequest, identityLockName, parseAttemptStatus, parseCounters, parseDescriptors, } from '../node.js';
|
|
32
32
|
import { acceptPeer as driveAccept, answerWaitingOffer, connectPeer as driveConnect, noteOffer, PeerAttempts, } from '../peer-driver.js';
|
|
33
33
|
import { LeafStream } from '../stream.js';
|
|
34
34
|
import { idArg, loadLeafWasm } from '../wasm.js';
|
|
@@ -36,6 +36,13 @@ import { isLifecycleEvent, parseSessionEvent, } from './events.js';
|
|
|
36
36
|
import { asSessionModule } from './wasm.js';
|
|
37
37
|
import { EventHub } from '../events.js';
|
|
38
38
|
/** The origin's Net node, as this tab sees it. */
|
|
39
|
+
/**
|
|
40
|
+
* What a send on a stream ended by a leader change fails with. Worded
|
|
41
|
+
* the way the leaf words a stale handle, because `isStaleStream` (the
|
|
42
|
+
* store's reopen test) matches exactly that: a store over a session
|
|
43
|
+
* reopens on the new leader rather than counting `send-failed` forever.
|
|
44
|
+
*/
|
|
45
|
+
const STALE_AFTER_HANDOFF = 'stale stream handle: the leader changed; reopen the stream';
|
|
39
46
|
export class MeshSession {
|
|
40
47
|
inner;
|
|
41
48
|
/**
|
|
@@ -100,7 +107,7 @@ export class MeshSession {
|
|
|
100
107
|
// The generation that owned the calls is the one they were
|
|
101
108
|
// issued under — typed `LeaderLost`, never resumed.
|
|
102
109
|
this.orgHandles.retireAll(new OrgLeaderLostError(this.streamGeneration));
|
|
103
|
-
this.endStreams();
|
|
110
|
+
this.endStreams(STALE_AFTER_HANDOFF);
|
|
104
111
|
return;
|
|
105
112
|
}
|
|
106
113
|
// And the abrupt case: a generation that moved under us. Read
|
|
@@ -114,7 +121,7 @@ export class MeshSession {
|
|
|
114
121
|
const previous = this.streamGeneration;
|
|
115
122
|
this.streamGeneration = current;
|
|
116
123
|
this.orgHandles.retireAll(new OrgLeaderLostError(previous));
|
|
117
|
-
this.endStreams();
|
|
124
|
+
this.endStreams(STALE_AFTER_HANDOFF);
|
|
118
125
|
});
|
|
119
126
|
}
|
|
120
127
|
/** `'leader'` if this tab runs the node, `'follower'` if another does. */
|
|
@@ -725,11 +732,17 @@ export class MeshSession {
|
|
|
725
732
|
* on close; idempotent, because a page may have closed a stream
|
|
726
733
|
* itself.
|
|
727
734
|
*/
|
|
728
|
-
endStreams() {
|
|
735
|
+
endStreams(staleReason) {
|
|
729
736
|
const open = [...this.streams];
|
|
730
737
|
this.streams.clear();
|
|
731
|
-
for (const stream of open)
|
|
732
|
-
|
|
738
|
+
for (const stream of open) {
|
|
739
|
+
// A leader change retires with the reopenable wording; closing the
|
|
740
|
+
// session is permanent.
|
|
741
|
+
if (staleReason === undefined)
|
|
742
|
+
stream.close();
|
|
743
|
+
else
|
|
744
|
+
stream.retire(staleReason);
|
|
745
|
+
}
|
|
733
746
|
}
|
|
734
747
|
/**
|
|
735
748
|
* Re-type whatever the boundary threw.
|
|
@@ -777,6 +790,17 @@ export async function openSession(options) {
|
|
|
777
790
|
...(options.dbName === undefined ? {} : { dbName: options.dbName }),
|
|
778
791
|
...(options.lockScope === undefined ? {} : { lockScope: options.lockScope }),
|
|
779
792
|
};
|
|
793
|
+
// A custodial identity elects under the same lock name `connect()` takes
|
|
794
|
+
// for it, so mixed connect()/openSession() tabs with one secret cannot
|
|
795
|
+
// both run a node (cubic review of PR #1228). The scope also names the
|
|
796
|
+
// `BroadcastChannel`, and every session tab with that secret derives the
|
|
797
|
+
// same name, so the leader and its followers still meet.
|
|
798
|
+
// `lockIdentity: false` keeps the leaf's own default scope.
|
|
799
|
+
if (request.lockScope === undefined && request.entitySecretHex !== undefined && options.lockIdentity !== false) {
|
|
800
|
+
const shared = await identityLockName(request.entitySecretHex);
|
|
801
|
+
if (shared !== null)
|
|
802
|
+
request.lockScope = shared;
|
|
803
|
+
}
|
|
780
804
|
try {
|
|
781
805
|
return new MeshSession(await module.MeshSession.open(request));
|
|
782
806
|
}
|
package/dist/lobby.js
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
* tags the node should keep announcing as `tags`.
|
|
47
47
|
*/
|
|
48
48
|
import { StoreError } from './store/errors.js';
|
|
49
|
-
import { hostInternals, hostStore, } from './store/host.js';
|
|
49
|
+
import { hostInternals, hostStore, nodeHexOf, } from './store/host.js';
|
|
50
50
|
import { joinStore } from './store/join.js';
|
|
51
51
|
import { hostPlayer } from './store/player.js';
|
|
52
52
|
export class LobbyError extends Error {
|
|
@@ -215,10 +215,14 @@ function decodeRecord(game, tag) {
|
|
|
215
215
|
return null;
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
|
+
// Read the id the way every other caller-held id is read (`nodeHexOf`):
|
|
219
|
+
// sixteen hex digits are hex, and anything shorter may be the exact-decimal
|
|
220
|
+
// `peerNode` an event handed the page. Reading every digit run as hex kicked
|
|
221
|
+
// a DIFFERENT id and removed nobody (SDK audit 2026-10-11).
|
|
218
222
|
function hexPeer(peer) {
|
|
219
|
-
const
|
|
220
|
-
if (
|
|
221
|
-
return
|
|
223
|
+
const hex = nodeHexOf(peer);
|
|
224
|
+
if (hex !== null)
|
|
225
|
+
return hex;
|
|
222
226
|
throw new LobbyError('invalid', `not a peer id: ${JSON.stringify(peer)}`);
|
|
223
227
|
}
|
|
224
228
|
/**
|
package/dist/net_leaf.d.ts
CHANGED
|
@@ -1453,16 +1453,16 @@ export interface InitOutput {
|
|
|
1453
1453
|
readonly proxystream_send: (a: number, b: number) => number;
|
|
1454
1454
|
readonly proxystream_stream_id_hex: (a: number, b: number) => void;
|
|
1455
1455
|
readonly start: () => void;
|
|
1456
|
-
readonly
|
|
1457
|
-
readonly
|
|
1458
|
-
readonly
|
|
1459
|
-
readonly
|
|
1460
|
-
readonly
|
|
1461
|
-
readonly
|
|
1462
|
-
readonly
|
|
1463
|
-
readonly
|
|
1464
|
-
readonly
|
|
1465
|
-
readonly
|
|
1456
|
+
readonly __wasm_bindgen_func_elem_6508: (a: number, b: number, c: number, d: number) => void;
|
|
1457
|
+
readonly __wasm_bindgen_func_elem_6510: (a: number, b: number, c: number, d: number) => void;
|
|
1458
|
+
readonly __wasm_bindgen_func_elem_2366: (a: number, b: number, c: number) => number;
|
|
1459
|
+
readonly __wasm_bindgen_func_elem_2367: (a: number, b: number, c: number) => void;
|
|
1460
|
+
readonly __wasm_bindgen_func_elem_2367_112: (a: number, b: number, c: number) => void;
|
|
1461
|
+
readonly __wasm_bindgen_func_elem_2367_113: (a: number, b: number, c: number) => void;
|
|
1462
|
+
readonly __wasm_bindgen_func_elem_2367_114: (a: number, b: number, c: number) => void;
|
|
1463
|
+
readonly __wasm_bindgen_func_elem_2367_115: (a: number, b: number, c: number) => void;
|
|
1464
|
+
readonly __wasm_bindgen_func_elem_2367_116: (a: number, b: number, c: number) => void;
|
|
1465
|
+
readonly __wasm_bindgen_func_elem_2367_117: (a: number, b: number, c: number) => void;
|
|
1466
1466
|
readonly __wbindgen_export: (a: number, b: number) => number;
|
|
1467
1467
|
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
1468
1468
|
readonly __wbindgen_export3: (a: number) => void;
|
package/dist/net_leaf.js
CHANGED
|
@@ -2718,7 +2718,7 @@ function __wbg_get_imports() {
|
|
|
2718
2718
|
const ret = getObject(arg0).error;
|
|
2719
2719
|
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
|
2720
2720
|
},
|
|
2721
|
-
|
|
2721
|
+
__wbg_error_5b007554bf198f44: function(arg0, arg1) {
|
|
2722
2722
|
console.error(getStringFromWasm0(arg0, arg1));
|
|
2723
2723
|
},
|
|
2724
2724
|
__wbg_error_6a23b57453026a47: function() { return handleError(function (arg0) {
|
|
@@ -2914,7 +2914,7 @@ function __wbg_get_imports() {
|
|
|
2914
2914
|
const a = state0.a;
|
|
2915
2915
|
state0.a = 0;
|
|
2916
2916
|
try {
|
|
2917
|
-
return
|
|
2917
|
+
return __wasm_bindgen_func_elem_6510(a, state0.b, arg0, arg1);
|
|
2918
2918
|
} finally {
|
|
2919
2919
|
state0.a = a;
|
|
2920
2920
|
}
|
|
@@ -2956,7 +2956,7 @@ function __wbg_get_imports() {
|
|
|
2956
2956
|
const a = state0.a;
|
|
2957
2957
|
state0.a = 0;
|
|
2958
2958
|
try {
|
|
2959
|
-
return
|
|
2959
|
+
return __wasm_bindgen_func_elem_6510(a, state0.b, arg0, arg1);
|
|
2960
2960
|
} finally {
|
|
2961
2961
|
state0.a = a;
|
|
2962
2962
|
}
|
|
@@ -3266,47 +3266,47 @@ function __wbg_get_imports() {
|
|
|
3266
3266
|
},
|
|
3267
3267
|
__wbindgen_generic_0000000000000001: function(arg0, arg1) {
|
|
3268
3268
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 34, ret: Externref, inner_ret: Some(Externref) }, mutable: true }) -> Externref`.
|
|
3269
|
-
const ret = makeMutClosure(arg0, arg1,
|
|
3269
|
+
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_2366);
|
|
3270
3270
|
return addHeapObject(ret);
|
|
3271
3271
|
},
|
|
3272
3272
|
__wbindgen_generic_0000000000000002: function(arg0, arg1) {
|
|
3273
3273
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 35, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
|
3274
|
-
const ret = makeMutClosure(arg0, arg1,
|
|
3274
|
+
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_2367);
|
|
3275
3275
|
return addHeapObject(ret);
|
|
3276
3276
|
},
|
|
3277
3277
|
__wbindgen_generic_0000000000000003: function(arg0, arg1) {
|
|
3278
3278
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 867, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
|
3279
|
-
const ret = makeMutClosure(arg0, arg1,
|
|
3279
|
+
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_6508);
|
|
3280
3280
|
return addHeapObject(ret);
|
|
3281
3281
|
},
|
|
3282
3282
|
__wbindgen_generic_0000000000000004: function(arg0, arg1) {
|
|
3283
3283
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("CloseEvent")], shim_idx: 35, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
|
3284
|
-
const ret = makeMutClosure(arg0, arg1,
|
|
3284
|
+
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_2367_112);
|
|
3285
3285
|
return addHeapObject(ret);
|
|
3286
3286
|
},
|
|
3287
3287
|
__wbindgen_generic_0000000000000005: function(arg0, arg1) {
|
|
3288
3288
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 35, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
|
3289
|
-
const ret = makeMutClosure(arg0, arg1,
|
|
3289
|
+
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_2367_113);
|
|
3290
3290
|
return addHeapObject(ret);
|
|
3291
3291
|
},
|
|
3292
3292
|
__wbindgen_generic_0000000000000006: function(arg0, arg1) {
|
|
3293
3293
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("MessageEvent")], shim_idx: 35, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
|
3294
|
-
const ret = makeMutClosure(arg0, arg1,
|
|
3294
|
+
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_2367_114);
|
|
3295
3295
|
return addHeapObject(ret);
|
|
3296
3296
|
},
|
|
3297
3297
|
__wbindgen_generic_0000000000000007: function(arg0, arg1) {
|
|
3298
3298
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("RTCDataChannelEvent")], shim_idx: 35, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
|
3299
|
-
const ret = makeMutClosure(arg0, arg1,
|
|
3299
|
+
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_2367_115);
|
|
3300
3300
|
return addHeapObject(ret);
|
|
3301
3301
|
},
|
|
3302
3302
|
__wbindgen_generic_0000000000000008: function(arg0, arg1) {
|
|
3303
3303
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("RTCPeerConnectionIceErrorEvent")], shim_idx: 35, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
|
3304
|
-
const ret = makeMutClosure(arg0, arg1,
|
|
3304
|
+
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_2367_116);
|
|
3305
3305
|
return addHeapObject(ret);
|
|
3306
3306
|
},
|
|
3307
3307
|
__wbindgen_generic_0000000000000009: function(arg0, arg1) {
|
|
3308
3308
|
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("RTCPeerConnectionIceEvent")], shim_idx: 35, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
|
3309
|
-
const ret = makeMutClosure(arg0, arg1,
|
|
3309
|
+
const ret = makeMutClosure(arg0, arg1, __wasm_bindgen_func_elem_2367_117);
|
|
3310
3310
|
return addHeapObject(ret);
|
|
3311
3311
|
},
|
|
3312
3312
|
__wbindgen_generic_000000000000000a: function(arg0) {
|
|
@@ -3333,47 +3333,47 @@ function __wbg_get_imports() {
|
|
|
3333
3333
|
};
|
|
3334
3334
|
}
|
|
3335
3335
|
|
|
3336
|
-
function
|
|
3337
|
-
wasm.
|
|
3336
|
+
function __wasm_bindgen_func_elem_2367(arg0, arg1, arg2) {
|
|
3337
|
+
wasm.__wasm_bindgen_func_elem_2367(arg0, arg1, addHeapObject(arg2));
|
|
3338
3338
|
}
|
|
3339
3339
|
|
|
3340
|
-
function
|
|
3341
|
-
wasm.
|
|
3340
|
+
function __wasm_bindgen_func_elem_2367_112(arg0, arg1, arg2) {
|
|
3341
|
+
wasm.__wasm_bindgen_func_elem_2367_112(arg0, arg1, addHeapObject(arg2));
|
|
3342
3342
|
}
|
|
3343
3343
|
|
|
3344
|
-
function
|
|
3345
|
-
wasm.
|
|
3344
|
+
function __wasm_bindgen_func_elem_2367_113(arg0, arg1, arg2) {
|
|
3345
|
+
wasm.__wasm_bindgen_func_elem_2367_113(arg0, arg1, addHeapObject(arg2));
|
|
3346
3346
|
}
|
|
3347
3347
|
|
|
3348
|
-
function
|
|
3349
|
-
wasm.
|
|
3348
|
+
function __wasm_bindgen_func_elem_2367_114(arg0, arg1, arg2) {
|
|
3349
|
+
wasm.__wasm_bindgen_func_elem_2367_114(arg0, arg1, addHeapObject(arg2));
|
|
3350
3350
|
}
|
|
3351
3351
|
|
|
3352
|
-
function
|
|
3353
|
-
wasm.
|
|
3352
|
+
function __wasm_bindgen_func_elem_2367_115(arg0, arg1, arg2) {
|
|
3353
|
+
wasm.__wasm_bindgen_func_elem_2367_115(arg0, arg1, addHeapObject(arg2));
|
|
3354
3354
|
}
|
|
3355
3355
|
|
|
3356
|
-
function
|
|
3357
|
-
wasm.
|
|
3356
|
+
function __wasm_bindgen_func_elem_2367_116(arg0, arg1, arg2) {
|
|
3357
|
+
wasm.__wasm_bindgen_func_elem_2367_116(arg0, arg1, addHeapObject(arg2));
|
|
3358
3358
|
}
|
|
3359
3359
|
|
|
3360
|
-
function
|
|
3361
|
-
wasm.
|
|
3360
|
+
function __wasm_bindgen_func_elem_2367_117(arg0, arg1, arg2) {
|
|
3361
|
+
wasm.__wasm_bindgen_func_elem_2367_117(arg0, arg1, addHeapObject(arg2));
|
|
3362
3362
|
}
|
|
3363
3363
|
|
|
3364
|
-
function
|
|
3365
|
-
const ret = wasm.
|
|
3364
|
+
function __wasm_bindgen_func_elem_2366(arg0, arg1, arg2) {
|
|
3365
|
+
const ret = wasm.__wasm_bindgen_func_elem_2366(arg0, arg1, addHeapObject(arg2));
|
|
3366
3366
|
return takeObject(ret);
|
|
3367
3367
|
}
|
|
3368
3368
|
|
|
3369
|
-
function
|
|
3370
|
-
wasm.
|
|
3369
|
+
function __wasm_bindgen_func_elem_6510(arg0, arg1, arg2, arg3) {
|
|
3370
|
+
wasm.__wasm_bindgen_func_elem_6510(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
|
|
3371
3371
|
}
|
|
3372
3372
|
|
|
3373
|
-
function
|
|
3373
|
+
function __wasm_bindgen_func_elem_6508(arg0, arg1, arg2) {
|
|
3374
3374
|
try {
|
|
3375
3375
|
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
|
|
3376
|
-
wasm.
|
|
3376
|
+
wasm.__wasm_bindgen_func_elem_6508(retptr, arg0, arg1, addHeapObject(arg2));
|
|
3377
3377
|
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
|
|
3378
3378
|
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
|
|
3379
3379
|
if (r1) {
|
package/dist/net_leaf_bg.wasm
CHANGED
|
Binary file
|
|
@@ -119,16 +119,16 @@ export const proxystream_peer_node_hex: (a: number, b: number) => void;
|
|
|
119
119
|
export const proxystream_send: (a: number, b: number) => number;
|
|
120
120
|
export const proxystream_stream_id_hex: (a: number, b: number) => void;
|
|
121
121
|
export const start: () => void;
|
|
122
|
-
export const
|
|
123
|
-
export const
|
|
124
|
-
export const
|
|
125
|
-
export const
|
|
126
|
-
export const
|
|
127
|
-
export const
|
|
128
|
-
export const
|
|
129
|
-
export const
|
|
130
|
-
export const
|
|
131
|
-
export const
|
|
122
|
+
export const __wasm_bindgen_func_elem_6508: (a: number, b: number, c: number, d: number) => void;
|
|
123
|
+
export const __wasm_bindgen_func_elem_6510: (a: number, b: number, c: number, d: number) => void;
|
|
124
|
+
export const __wasm_bindgen_func_elem_2366: (a: number, b: number, c: number) => number;
|
|
125
|
+
export const __wasm_bindgen_func_elem_2367: (a: number, b: number, c: number) => void;
|
|
126
|
+
export const __wasm_bindgen_func_elem_2367_112: (a: number, b: number, c: number) => void;
|
|
127
|
+
export const __wasm_bindgen_func_elem_2367_113: (a: number, b: number, c: number) => void;
|
|
128
|
+
export const __wasm_bindgen_func_elem_2367_114: (a: number, b: number, c: number) => void;
|
|
129
|
+
export const __wasm_bindgen_func_elem_2367_115: (a: number, b: number, c: number) => void;
|
|
130
|
+
export const __wasm_bindgen_func_elem_2367_116: (a: number, b: number, c: number) => void;
|
|
131
|
+
export const __wasm_bindgen_func_elem_2367_117: (a: number, b: number, c: number) => void;
|
|
132
132
|
export const __wbindgen_export: (a: number, b: number) => number;
|
|
133
133
|
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
|
|
134
134
|
export const __wbindgen_export3: (a: number) => void;
|
package/dist/node.d.ts
CHANGED
|
@@ -257,6 +257,19 @@ export interface ConnectOptions extends WasmSource {
|
|
|
257
257
|
* an entity but not a static key.
|
|
258
258
|
*/
|
|
259
259
|
noiseSecretHex?: string;
|
|
260
|
+
/**
|
|
261
|
+
* Hold this identity exclusively while the node is open (default
|
|
262
|
+
* `true`; only read with {@link ConnectOptions.entitySecretHex}).
|
|
263
|
+
*
|
|
264
|
+
* Two tabs connected as the same node evict each other at the anchor,
|
|
265
|
+
* so stores and lobbies in both flap — the hazard `openSession()`
|
|
266
|
+
* avoids by electing one leader. `connect()` takes a Web Lock named
|
|
267
|
+
* after a hash of the identity (never the secret) and refuses a second
|
|
268
|
+
* holder with an {@link IdentityError} instead; `close()` releases it.
|
|
269
|
+
* Where Web Locks are unavailable the lock is skipped. Pass `false` to
|
|
270
|
+
* run without it (SDK audit 2026-10-11).
|
|
271
|
+
*/
|
|
272
|
+
lockIdentity?: boolean;
|
|
260
273
|
/**
|
|
261
274
|
* The `rtc_addr` the anchor publishes, supplied out of band.
|
|
262
275
|
*
|
|
@@ -312,6 +325,8 @@ export declare class BrowserNode {
|
|
|
312
325
|
private anchorRtcAddrs;
|
|
313
326
|
/** Set once the leaf reports a session — a bootstrap that demonstrably worked. */
|
|
314
327
|
private bootstrapObserved;
|
|
328
|
+
/** Releases the identity's Web Lock (`connect({ lockIdentity })`). */
|
|
329
|
+
private releaseIdentity;
|
|
315
330
|
/** This node's mesh id, hex. */
|
|
316
331
|
nodeIdHex(): string;
|
|
317
332
|
/** The anchor's mesh id, hex. */
|
package/dist/node.js
CHANGED
|
@@ -83,6 +83,12 @@ export class BrowserNode {
|
|
|
83
83
|
anchorRtcAddrs = [];
|
|
84
84
|
/** Set once the leaf reports a session — a bootstrap that demonstrably worked. */
|
|
85
85
|
bootstrapObserved = false;
|
|
86
|
+
/** Releases the identity's Web Lock (`connect({ lockIdentity })`). */
|
|
87
|
+
releaseIdentity = null;
|
|
88
|
+
/** @internal — `connect()` hands over the identity lock it took. */
|
|
89
|
+
releaseIdentityOnClose(release) {
|
|
90
|
+
this.releaseIdentity = release;
|
|
91
|
+
}
|
|
86
92
|
/** @internal — use {@link connect}. */
|
|
87
93
|
constructor(inner, bootstrapUrl, failureTyping, anchorRtcAddr) {
|
|
88
94
|
this.inner = inner;
|
|
@@ -787,6 +793,10 @@ export class BrowserNode {
|
|
|
787
793
|
if (this.closed)
|
|
788
794
|
return;
|
|
789
795
|
this.closed = true;
|
|
796
|
+
// Whatever else fails below, the identity is free for another tab.
|
|
797
|
+
const releaseIdentity = this.releaseIdentity;
|
|
798
|
+
this.releaseIdentity = null;
|
|
799
|
+
queueMicrotask(() => releaseIdentity?.());
|
|
790
800
|
const failures = [];
|
|
791
801
|
// Ownership retirement first, while the node can still put a
|
|
792
802
|
// CANCEL on the wire (plan §4.5): one per dropped caller handle,
|
|
@@ -851,11 +861,17 @@ export async function connect(options) {
|
|
|
851
861
|
const request = buildConnectRequest(options);
|
|
852
862
|
const failureTyping = options.failureTyping ?? {};
|
|
853
863
|
const wasm = await loadLeafWasm(options);
|
|
864
|
+
const release = request.entitySecretHex !== undefined && options.lockIdentity !== false
|
|
865
|
+
? await lockIdentity(request.entitySecretHex)
|
|
866
|
+
: null;
|
|
854
867
|
try {
|
|
855
868
|
const inner = await wasm.LeafNode.connect(request);
|
|
856
|
-
|
|
869
|
+
const node = new BrowserNode(inner, options.bootstrapUrl ?? null, failureTyping, options.anchorRtcAddr ?? null);
|
|
870
|
+
node.releaseIdentityOnClose(release);
|
|
871
|
+
return node;
|
|
857
872
|
}
|
|
858
873
|
catch (error) {
|
|
874
|
+
release?.();
|
|
859
875
|
throw await refineIceFailure(fromWasmError(error), {
|
|
860
876
|
bootstrapUrl: options.bootstrapUrl ?? null,
|
|
861
877
|
bootstrapObserved: false,
|
|
@@ -864,6 +880,64 @@ export async function connect(options) {
|
|
|
864
880
|
});
|
|
865
881
|
}
|
|
866
882
|
}
|
|
883
|
+
/**
|
|
884
|
+
* The Web Lock name for one custodial identity, or `null` where
|
|
885
|
+
* `crypto.subtle` is unavailable. A hash of the secret, never the secret.
|
|
886
|
+
*
|
|
887
|
+
* Shared by `connect()` and `openSession()`: a session with a custodial
|
|
888
|
+
* secret names its election lock with this too, so a `connect()` tab and
|
|
889
|
+
* an `openSession()` tab given one secret contend for ONE lock instead of
|
|
890
|
+
* each running a node with that identity (cubic review of PR #1228).
|
|
891
|
+
*
|
|
892
|
+
* @internal
|
|
893
|
+
*/
|
|
894
|
+
export async function identityLockName(entitySecretHex) {
|
|
895
|
+
const subtle = globalThis.crypto?.subtle;
|
|
896
|
+
if (subtle === undefined)
|
|
897
|
+
return null;
|
|
898
|
+
// Lowercased: the secret parses case-insensitively, so "AB…" and "ab…" are
|
|
899
|
+
// one identity and must contend for one lock (code review 2026-10-11,
|
|
900
|
+
// BROWSER-3).
|
|
901
|
+
const canonical = entitySecretHex.toLowerCase();
|
|
902
|
+
const digest = new Uint8Array(await subtle.digest('SHA-256', new TextEncoder().encode(`net-mesh:identity:${canonical}`)));
|
|
903
|
+
return `net-mesh:identity:${Array.from(digest.subarray(0, 16), b => b.toString(16).padStart(2, '0')).join('')}`;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Take the exclusive Web Lock for one custodial identity, or throw an
|
|
907
|
+
* {@link IdentityError} when another tab or window holds it. Returns the
|
|
908
|
+
* release, or `null` where Web Locks (or `crypto.subtle`, for the hashed
|
|
909
|
+
* name) are unavailable.
|
|
910
|
+
*/
|
|
911
|
+
async function lockIdentity(entitySecretHex) {
|
|
912
|
+
const locks = globalThis.navigator?.locks;
|
|
913
|
+
if (locks === undefined)
|
|
914
|
+
return null;
|
|
915
|
+
const name = await identityLockName(entitySecretHex);
|
|
916
|
+
if (name === null)
|
|
917
|
+
return null;
|
|
918
|
+
const acquired = await new Promise((resolve, reject) => {
|
|
919
|
+
let release;
|
|
920
|
+
const held = new Promise(done => {
|
|
921
|
+
release = done;
|
|
922
|
+
});
|
|
923
|
+
locks
|
|
924
|
+
.request(name, { ifAvailable: true }, async (lock) => {
|
|
925
|
+
if (lock === null) {
|
|
926
|
+
resolve('busy');
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
resolve(release);
|
|
930
|
+
await held;
|
|
931
|
+
})
|
|
932
|
+
.catch(reject);
|
|
933
|
+
});
|
|
934
|
+
if (acquired === 'busy') {
|
|
935
|
+
throw new IdentityError('this identity is already connected in another tab or window of this browser; ' +
|
|
936
|
+
'two nodes with one identity evict each other at the anchor (use openSession() to share one ' +
|
|
937
|
+
'node across tabs, or pass lockIdentity: false)');
|
|
938
|
+
}
|
|
939
|
+
return acquired;
|
|
940
|
+
}
|
|
867
941
|
/**
|
|
868
942
|
* Build exactly the options object `LeafNode.connect` reads, credential
|
|
869
943
|
* resolved, origin defaulted, custodial identity validated.
|
package/dist/store/host.js
CHANGED
|
@@ -64,16 +64,22 @@ export const FAREWELL_DEADLINE_MS = 500;
|
|
|
64
64
|
export function peerHexOf(peer) {
|
|
65
65
|
const decimal = /^\d+$/.test(peer);
|
|
66
66
|
const hex = /^(0x)?[0-9a-f]{1,16}$/i.test(peer);
|
|
67
|
+
let out;
|
|
67
68
|
try {
|
|
68
69
|
if (decimal)
|
|
69
|
-
|
|
70
|
-
if (hex)
|
|
71
|
-
|
|
70
|
+
out = BigInt(peer).toString(16).padStart(16, '0');
|
|
71
|
+
else if (hex)
|
|
72
|
+
out = BigInt(peer.startsWith('0x') ? peer : `0x${peer}`).toString(16).padStart(16, '0');
|
|
73
|
+
else
|
|
74
|
+
return null;
|
|
72
75
|
}
|
|
73
76
|
catch {
|
|
74
77
|
return null;
|
|
75
78
|
}
|
|
76
|
-
|
|
79
|
+
// A decimal above u64 renders as 17+ hex digits: no node has that id, so
|
|
80
|
+
// it is not one — `kick('18446744073709551616')` used to target nobody
|
|
81
|
+
// without an error (cubic review of PR #1228).
|
|
82
|
+
return out.length === 16 ? out : null;
|
|
77
83
|
}
|
|
78
84
|
/**
|
|
79
85
|
* A node id the CALLER holds, in the same spelling: 16 lowercase hex.
|
package/dist/store/join.js
CHANGED
|
@@ -119,6 +119,14 @@ export function joinStore(options) {
|
|
|
119
119
|
let closed = false;
|
|
120
120
|
let upstream = null;
|
|
121
121
|
let opening = null;
|
|
122
|
+
/**
|
|
123
|
+
* Bumped by `reconnect()`. An open started under an older epoch belongs
|
|
124
|
+
* to the lost session: it never publishes `upstream` and never clears a
|
|
125
|
+
* newer `opening`, so `reconnect()` can start fresh instead of inheriting
|
|
126
|
+
* that open's late rejection — or waiting on one that never settles
|
|
127
|
+
* (code review 2026-10-11, BROWSER-2).
|
|
128
|
+
*/
|
|
129
|
+
let openEpoch = 0;
|
|
122
130
|
const readyWaiters = [];
|
|
123
131
|
/** The transition in flight and its callers (§2). */
|
|
124
132
|
let transition = null;
|
|
@@ -141,6 +149,7 @@ export function joinStore(options) {
|
|
|
141
149
|
// the relay is installed before the direct half is even tried, so
|
|
142
150
|
// the open below is the thing that decides, and its refusal is
|
|
143
151
|
// typed.
|
|
152
|
+
const epoch = openEpoch;
|
|
144
153
|
opening = Promise.resolve(options.transport.connectPeer?.(hostHex))
|
|
145
154
|
.catch(() => undefined)
|
|
146
155
|
.then(() => options.transport.openStream({
|
|
@@ -149,6 +158,16 @@ export function joinStore(options) {
|
|
|
149
158
|
label: streamId,
|
|
150
159
|
}))
|
|
151
160
|
.then(open => {
|
|
161
|
+
if (epoch !== openEpoch) {
|
|
162
|
+
// Superseded by `reconnect()`: reclaim, never publish.
|
|
163
|
+
try {
|
|
164
|
+
open.close();
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Already gone.
|
|
168
|
+
}
|
|
169
|
+
throw new StoreError('aborted', 'the stream open was superseded by reconnect()');
|
|
170
|
+
}
|
|
152
171
|
opening = null;
|
|
153
172
|
// An open that resolves after `close()` is reclaimed, never
|
|
154
173
|
// published.
|
|
@@ -170,7 +189,8 @@ export function joinStore(options) {
|
|
|
170
189
|
// answered from it with the same stale error until the caller
|
|
171
190
|
// happens to call `reconnect()`. A transient failure is a retry,
|
|
172
191
|
// not a verdict on the peer.
|
|
173
|
-
|
|
192
|
+
if (epoch === openEpoch)
|
|
193
|
+
opening = null;
|
|
174
194
|
throw error;
|
|
175
195
|
});
|
|
176
196
|
return opening;
|
|
@@ -669,9 +689,25 @@ export function joinStore(options) {
|
|
|
669
689
|
reconnect: async () => {
|
|
670
690
|
if (closed)
|
|
671
691
|
throw new StoreError('closed', 'this store handle is closed');
|
|
672
|
-
// The old stream belongs to the lost session
|
|
692
|
+
// The old stream belongs to the lost session: CLOSE it, never just
|
|
693
|
+
// drop it — a dropped LeafStream stays registered with the node and
|
|
694
|
+
// its wasm listener, so every reconnect leaked one (SDK audit
|
|
695
|
+
// 2026-10-11). An open still in flight is detached by the epoch
|
|
696
|
+
// bump: it can no longer overwrite the new stream (it is closed on
|
|
697
|
+
// arrival instead), and this reconnect no longer inherits its
|
|
698
|
+
// rejection or its hang.
|
|
699
|
+
const old = upstream;
|
|
673
700
|
upstream = null;
|
|
701
|
+
openEpoch += 1;
|
|
674
702
|
opening = null;
|
|
703
|
+
if (old !== null) {
|
|
704
|
+
try {
|
|
705
|
+
old.close();
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
// Already gone with its session.
|
|
709
|
+
}
|
|
710
|
+
}
|
|
675
711
|
let requests;
|
|
676
712
|
try {
|
|
677
713
|
requests = replica.reconnect();
|
package/dist/store/owner.d.ts
CHANGED
|
@@ -39,6 +39,15 @@ import { type Hex } from './wire.js';
|
|
|
39
39
|
export declare const MAX_PENDING_ACTIONS = 32;
|
|
40
40
|
/** Handles one owner will hold (brief §2). */
|
|
41
41
|
export declare const MAX_HANDLES = 256;
|
|
42
|
+
/**
|
|
43
|
+
* Handles one authenticated peer may hold at once. Room for siblings and
|
|
44
|
+
* for a reconnect that overlaps its predecessor's lease, but never the
|
|
45
|
+
* whole table: one peer could otherwise take all {@link MAX_HANDLES} and
|
|
46
|
+
* lock every other player out — the lobby's capacity rule counts distinct
|
|
47
|
+
* peers, so a seated player's repeat joins bypassed it (SDK audit
|
|
48
|
+
* 2026-10-11).
|
|
49
|
+
*/
|
|
50
|
+
export declare const MAX_HANDLES_PER_PEER = 16;
|
|
42
51
|
/** How long a handle lives without an accepted authenticated message. */
|
|
43
52
|
export declare const HANDLE_LEASE_MS = 60000;
|
|
44
53
|
/** What the owner needs from its host. */
|
|
@@ -189,6 +198,8 @@ export interface OwnerHandle {
|
|
|
189
198
|
/** The authenticated peer this handle is bound to (§1.3). */
|
|
190
199
|
readonly peer: string;
|
|
191
200
|
readonly audience: readonly string[];
|
|
201
|
+
/** The opaque key the `join` carried; every read decision passes it on. */
|
|
202
|
+
readonly key?: string;
|
|
192
203
|
/** Highest generation allocated for this handle; monotone (§1.7a). */
|
|
193
204
|
readonly generation: number;
|
|
194
205
|
/**
|