reploid 0.0.0-stage → 0.1.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/apps/poolday/config/chat-files.json +27 -0
- package/apps/poolday/config/partition-generation.js +21 -0
- package/apps/poolday/config/partition-policy.json +74 -0
- package/apps/poolday/config/partition-releases.json +3 -0
- package/apps/poolday/config/work-profile.json +122 -0
- package/apps/poolday/host/chat-view.js +47 -0
- package/apps/poolday/host/earning/client.js +199 -0
- package/apps/poolday/host/earning/contribution.js +168 -0
- package/apps/poolday/host/partition-release.js +33 -0
- package/apps/poolday/host/work-partition-artifact-source.js +37 -0
- package/apps/poolday/host/work-partition-capsule.js +39 -0
- package/apps/poolday/host/work-partition-plan.js +167 -0
- package/apps/poolday/host/work-partition-tokenizer-guard.js +21 -0
- package/apps/poolday/infrastructure/model-storage-lock.js +22 -0
- package/apps/poolday/infrastructure/pack-release-storage.js +52 -0
- package/apps/poolday/pool/pack-operation.js +161 -0
- package/apps/poolday/pool/pack-release-policy.js +44 -0
- package/apps/poolday/pool/peer-pack-data-channel.js +217 -0
- package/apps/poolday/providers/work-adapter.js +67 -0
- package/apps/poolday/providers/work-device.js +33 -0
- package/apps/poolday/providers/work-generation-completion.js +55 -0
- package/apps/poolday/providers/work-provider.js +209 -0
- package/apps/poolday/providers/work-resident-provider.js +113 -0
- package/deploy/artifacts/doppler-gpu-0.7.1.tgz +0 -0
- package/docs/fleet-service.md +214 -0
- package/docs/native-participant.md +534 -0
- package/native-delivery.json +1174 -0
- package/package.json +29 -4
- package/packages/reploid/src/artifacts/binary.js +25 -0
- package/packages/reploid/src/artifacts/canonical-json.js +23 -0
- package/packages/reploid/src/artifacts/custody/exchange.js +402 -0
- package/packages/reploid/src/artifacts/custody/piece-acquisition.js +70 -0
- package/packages/reploid/src/artifacts/custody/runtime.js +404 -0
- package/packages/reploid/src/artifacts/custody/supply-budget.js +19 -0
- package/packages/reploid/src/artifacts/custody-certificate.js +123 -0
- package/packages/reploid/src/artifacts/delivery-attestation.js +64 -0
- package/packages/reploid/src/artifacts/generation-terminal.js +36 -0
- package/packages/reploid/src/artifacts/identity.js +91 -0
- package/packages/reploid/src/artifacts/participant-proofs.js +159 -0
- package/packages/reploid/src/artifacts/receipt.js +131 -0
- package/packages/reploid/src/artifacts/signatures.js +94 -0
- package/packages/reploid/src/artifacts/whole-model-receipt.js +42 -0
- package/packages/reploid/src/cancellation.js +44 -0
- package/packages/reploid/src/config/defaults.json +110 -0
- package/packages/reploid/src/config/index.js +209 -0
- package/packages/reploid/src/config/request-overrides.json +9 -0
- package/packages/reploid/src/config/transport-defaults.json +7 -0
- package/packages/reploid/src/hashing.js +30 -0
- package/packages/reploid/src/measurement/capacity-utilization.js +321 -0
- package/packages/reploid/src/measurement/index.js +5 -0
- package/packages/reploid/src/measurement/readiness-records.js +310 -0
- package/packages/reploid/src/mesh/computer-identity.js +50 -0
- package/packages/reploid/src/mesh/computer-identity.policy.json +8 -0
- package/packages/reploid/src/mesh/contribution.js +141 -0
- package/packages/reploid/src/mesh/generation-recovery.js +58 -0
- package/packages/reploid/src/mesh/gpu-signature-shader.js +27 -0
- package/packages/reploid/src/mesh/gpu-signature.js +136 -0
- package/packages/reploid/src/mesh/gpu-signature.policy.json +11 -0
- package/packages/reploid/src/mesh/index.js +66 -0
- package/packages/reploid/src/mesh/partitions/automatic-ordered-partitions.js +540 -0
- package/packages/reploid/src/mesh/partitions/automatic-partitions.js +298 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue-policy.json +6 -0
- package/packages/reploid/src/mesh/partitions/partition-attempt-queue.js +112 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-contract.js +167 -0
- package/packages/reploid/src/mesh/partitions/partition-canonical-execution.js +168 -0
- package/packages/reploid/src/mesh/partitions/partition-chat-canonical.js +49 -0
- package/packages/reploid/src/mesh/partitions/partition-chat.js +125 -0
- package/packages/reploid/src/mesh/partitions/partition-contract.js +192 -0
- package/packages/reploid/src/mesh/partitions/partition-device-admission.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-discovery.js +96 -0
- package/packages/reploid/src/mesh/partitions/partition-entry-publication.js +183 -0
- package/packages/reploid/src/mesh/partitions/partition-entry.js +434 -0
- package/packages/reploid/src/mesh/partitions/partition-generation-request.js +33 -0
- package/packages/reploid/src/mesh/partitions/partition-grants.js +277 -0
- package/packages/reploid/src/mesh/partitions/partition-link.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-network.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-chat.js +290 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-peer.js +415 -0
- package/packages/reploid/src/mesh/partitions/partition-ordered-publication.js +100 -0
- package/packages/reploid/src/mesh/partitions/partition-peer.js +314 -0
- package/packages/reploid/src/mesh/partitions/partition-placement.js +143 -0
- package/packages/reploid/src/mesh/partitions/partition-preparation-progress.js +25 -0
- package/packages/reploid/src/mesh/partitions/partition-receiver-policy.json +7 -0
- package/packages/reploid/src/mesh/partitions/partition-reservations.js +75 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope-policy.json +16 -0
- package/packages/reploid/src/mesh/partitions/partition-route-envelope.js +74 -0
- package/packages/reploid/src/mesh/partitions/partition-route-execution.js +409 -0
- package/packages/reploid/src/mesh/partitions/partition-route-network.js +179 -0
- package/packages/reploid/src/mesh/partitions/partition-route-ranking.js +268 -0
- package/packages/reploid/src/mesh/partitions/partition-runner.js +319 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-assignment.js +109 -0
- package/packages/reploid/src/mesh/partitions/partition-stage-commit.js +103 -0
- package/packages/reploid/src/mesh/partitions/partition-step-receiver.js +291 -0
- package/packages/reploid/src/mesh/partitions/partition-summary.js +122 -0
- package/packages/reploid/src/mesh/partitions/preparation-owner.js +40 -0
- package/packages/reploid/src/mesh/partitions/resident-error-codes.json +10 -0
- package/packages/reploid/src/mesh/partitions/resident-partition.js +333 -0
- package/packages/reploid/src/mesh/partitions/resident-route.js +46 -0
- package/packages/reploid/src/mesh/partitions/signed-resident-factory.js +59 -0
- package/packages/reploid/src/mesh/peer-generation.js +896 -0
- package/packages/reploid/src/mesh/peer-identity.js +87 -0
- package/packages/reploid/src/mesh/placement-beliefs.js +107 -0
- package/packages/reploid/src/mesh/preparation-progress.js +46 -0
- package/packages/reploid/src/mesh/provider-hardware.js +11 -0
- package/packages/reploid/src/mesh/remote-generation-requests.js +53 -0
- package/packages/reploid/src/mesh/swarm-coordination.js +140 -0
- package/packages/reploid/src/mesh/web-access-network.js +112 -0
- package/packages/reploid/src/mesh/web-access-policy.json +17 -0
- package/packages/reploid/src/retrieval/citations.js +156 -0
- package/packages/reploid/src/retrieval/claim-support.js +113 -0
- package/packages/reploid/src/retrieval/costs.js +101 -0
- package/packages/reploid/src/retrieval/fixed-provider.js +34 -0
- package/packages/reploid/src/retrieval/index.js +20 -0
- package/packages/reploid/src/retrieval/lexical-support.js +27 -0
- package/packages/reploid/src/retrieval/model-reviewer.js +67 -0
- package/packages/reploid/src/retrieval/page-fetch.json +10 -0
- package/packages/reploid/src/retrieval/page-text.js +182 -0
- package/packages/reploid/src/retrieval/passage.js +37 -0
- package/packages/reploid/src/retrieval/policy.json +14 -0
- package/packages/reploid/src/retrieval/retrieval.js +23 -0
- package/packages/reploid/src/retrieval/review-queue.js +76 -0
- package/packages/reploid/src/retrieval/sources.js +263 -0
- package/packages/reploid/src/retrieval/support-review.js +155 -0
- package/packages/reploid/src/retrieval/text.js +21 -0
- package/packages/reploid/src/rules/contribution.rules.json +8 -0
- package/packages/reploid/src/rules/routing.rules.json +24 -0
- package/packages/reploid/src/transport/assignment.js +571 -0
- package/packages/reploid/src/transport/bounded-channel-writer.js +46 -0
- package/packages/reploid/src/transport/index.js +11 -0
- package/packages/reploid/src/transport/introduction-admission.js +24 -0
- package/packages/reploid/src/transport/introduction-bootstrap.js +112 -0
- package/packages/reploid/src/transport/introduction-policy.json +12 -0
- package/packages/reploid/src/transport/introduction-records.js +90 -0
- package/packages/reploid/src/transport/introductions.js +209 -0
- package/packages/reploid/src/transport/partition-data-channel.js +421 -0
- package/packages/reploid/src/transport/partition-errors.json +16 -0
- package/packages/reploid/src/transport/retry-policy.js +36 -0
- package/packages/reploid/src/transport/room.js +502 -0
- package/packages/reploid/src/transport/sdp-fingerprint.js +5 -0
- package/packages/reploid/src/transport/signaling.js +571 -0
- package/packages/reploid/src/transport/swarm.js +1247 -0
- package/packages/reploid/src/transport/tool-offer-channel.js +137 -0
- package/server/browser-roots.js +42 -0
- package/server/browser-web-worker.js +94 -0
- package/server/chat-completions.js +45 -0
- package/server/fleet/browser-bridge.js +145 -0
- package/server/fleet/contract.js +42 -0
- package/server/fleet/credit-evidence.js +103 -0
- package/server/fleet/credit-recovery.js +175 -0
- package/server/fleet/credits.js +178 -0
- package/server/fleet/execution-admission.js +32 -0
- package/server/fleet/native-host.js +817 -0
- package/server/fleet/placement.js +132 -0
- package/server/fleet/requester.js +346 -0
- package/server/fleet/service.js +311 -0
- package/server/fleet/terminal-policy.js +83 -0
- package/server/fleet/text-api.js +94 -0
- package/server/hashing.js +35 -0
- package/server/http-request-owner.js +49 -0
- package/server/messages.js +57 -0
- package/server/native-canonical-epochs.js +90 -0
- package/server/native-change-log.js +40 -0
- package/server/native-earning.js +203 -0
- package/server/native-generation-journal.js +46 -0
- package/server/native-model-config.js +57 -0
- package/server/native-model-files.js +382 -0
- package/server/native-model-origin.js +54 -0
- package/server/native-model-origins.json +12 -0
- package/server/native-node.js +83 -0
- package/server/native-owner/controls.js +162 -0
- package/server/native-owner/monitor.js +251 -0
- package/server/native-owner/policy.js +273 -0
- package/server/native-owner/rate.js +92 -0
- package/server/native-participant.js +866 -0
- package/server/native-readiness-log.js +30 -0
- package/server/native-release-storage.js +64 -0
- package/server/native-shader-cache.js +369 -0
- package/server/native-supply-budget.js +66 -0
- package/server/native-whole-earning.js +67 -0
- package/server/native-whole-model-fill.js +182 -0
- package/server/native-whole-model.js +130 -0
- package/server/public-web-worker.js +66 -0
- package/server/responses.js +92 -0
- package/server/search/errors.js +47 -0
- package/server/search/page-fetch.js +185 -0
- package/server/text-api.js +83 -0
- package/server/text-recovery.js +45 -0
- package/server/text-request-options.js +23 -0
- package/server/text-requester.js +231 -0
- package/server/token-auth.js +9 -0
- package/server/whole-model-requester.js +99 -0
- package/shared/config/chat-models.json +4685 -0
- package/shared/config/doppler-execution-contracts.js +32 -0
- package/shared/config/doppler-execution-contracts.json +43 -0
- package/shared/config/doppler-generation-contract.js +223 -0
- package/shared/config/doppler-local-models.js +79 -0
- package/shared/config/doppler-package.json +7 -0
- package/shared/config/model-manifests/a0dc69e917133a82780823d17fc771c0f6b882d7c86c571deeaef45be2ff108b.json +6434 -0
- package/shared/config/model-manifests/e985e3cdad60d0cb57d84b436161e66ab5b3f6589a168de6f00b4610e5f1fb59.json +16534 -0
- package/shared/config/model-manifests/fa6eb7bbc3ba63571f8cede780b877d9272bde948697af93dd3d33837cb46137.json +7175 -0
- package/shared/config/model-pieces/qwen-0-8b-pieces.json +1506 -0
- package/shared/config/model-pieces/qwen-2b-pieces.json +4800 -0
- package/shared/config/model-pieces/qwen35b-q4_0-pieces.json +28880 -0
- package/shared/config/reploid-library.json +49 -0
- package/shared/config/swarm-bootstrap.json +21 -0
- package/shared/infrastructure/doppler-runtime-service.js +261 -0
- package/shared/pool/executable-pack.js +87 -0
- package/shared/pool/pack-operation-adapters.js +124 -0
- package/shared/pool/pack-operation-policy.js +75 -0
- package/shared/pool/pool-config.json +2280 -0
- package/shared/vendor/reploid/adapters/doppler-session.js +372 -0
- package/shared/vendor/reploid/artifacts/binary.js +25 -0
- package/shared/vendor/reploid/artifacts/canonical-json.js +23 -0
- package/shared/vendor/reploid/artifacts/delivery-attestation.js +64 -0
- package/shared/vendor/reploid/artifacts/identity.js +91 -0
- package/shared/vendor/reploid/artifacts/manifest-identity.js +17 -0
- package/shared/vendor/reploid/artifacts/participant-proofs.js +159 -0
- package/shared/vendor/reploid/artifacts/signatures.js +94 -0
- package/shared/vendor/reploid/config/defaults.json +110 -0
- package/shared/vendor/reploid/config/index.js +209 -0
- package/shared/vendor/reploid/config/request-overrides.json +9 -0
- package/shared/vendor/reploid/hashing.js +30 -0
- package/shared/vendor/reploid/mesh/generation-context-cache.js +97 -0
- package/shared/vendor/reploid/mesh/partitions/partition-attempt-evidence.js +282 -0
- package/shared/vendor/reploid/mesh/partitions/partition-contract.js +192 -0
- package/shared/vendor/reploid/mesh/partitions/partition-receiver-policy.json +7 -0
- package/shared/vendor/reploid/mesh/partitions/partition-route-ranking.js +268 -0
- package/shared/vendor/reploid/mesh/preparation-progress.js +46 -0
- package/shared/vendor/reploid/mesh/provider-hardware.js +11 -0
- package/shared/vendor/reploid/rules/routing.rules.json +24 -0
- package/shared/vendor/reploid/transport/partition-errors.json +16 -0
- package/README.md +0 -3
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import { SIGNAL_TYPES } from './signaling.js';
|
|
2
|
+
import { requireResolvedConfig } from '../config/index.js';
|
|
3
|
+
export const P2P_TRANSPORT_STATES = Object.freeze({
|
|
4
|
+
IDLE: 'idle',
|
|
5
|
+
CONNECTING: 'connecting',
|
|
6
|
+
CONNECTED: 'connected',
|
|
7
|
+
CLOSING: 'closing',
|
|
8
|
+
CLOSED: 'closed',
|
|
9
|
+
FAILED: 'failed',
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export function createP2PTransport({
|
|
14
|
+
config,
|
|
15
|
+
signaling,
|
|
16
|
+
initiator,
|
|
17
|
+
rtcConfig = requireResolvedConfig(config).webrtc.rtcConfig,
|
|
18
|
+
dataChannelLabel = requireResolvedConfig(config).webrtc.dataChannelLabel,
|
|
19
|
+
dataChannelOptions = Object.fromEntries(Object.entries(requireResolvedConfig(config).webrtc.dataChannelOptions).filter(([, value]) => value !== null)),
|
|
20
|
+
serialize = defaultSerialize,
|
|
21
|
+
deserialize = defaultDeserialize,
|
|
22
|
+
onMessage = null,
|
|
23
|
+
onStateChange = null,
|
|
24
|
+
onPeerConnection = null,
|
|
25
|
+
onDataChannel = null,
|
|
26
|
+
onAuxiliaryChannel = null,
|
|
27
|
+
maxPendingRemoteIceCandidates = requireResolvedConfig(config).webrtc.maxPendingRemoteIceCandidates,
|
|
28
|
+
pendingRemoteIceTtlMs = requireResolvedConfig(config).webrtc.pendingRemoteIceTtlMs,
|
|
29
|
+
now = () => Date.now(),
|
|
30
|
+
RTCPeerConnectionImpl = globalThis.RTCPeerConnection,
|
|
31
|
+
RTCSessionDescriptionImpl = globalThis.RTCSessionDescription,
|
|
32
|
+
RTCIceCandidateImpl = globalThis.RTCIceCandidate,
|
|
33
|
+
} = {}) {
|
|
34
|
+
const policy = requireResolvedConfig(config).webrtc;
|
|
35
|
+
let connectTimer = null;
|
|
36
|
+
if (!signaling || typeof signaling.subscribe !== 'function') {
|
|
37
|
+
throw new TypeError('signaling must provide subscribe()');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof signaling.sendOffer !== 'function' || typeof signaling.sendAnswer !== 'function') {
|
|
41
|
+
throw new TypeError('signaling must provide sendOffer() and sendAnswer()');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof signaling.sendIceCandidate !== 'function') {
|
|
45
|
+
throw new TypeError('signaling must provide sendIceCandidate()');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof RTCPeerConnectionImpl !== 'function') {
|
|
49
|
+
throw new Error('RTCPeerConnection is not available in this browser context');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const transportInitiator = Boolean(initiator);
|
|
53
|
+
let state = P2P_TRANSPORT_STATES.IDLE;
|
|
54
|
+
let peerConnection = null;
|
|
55
|
+
let dataChannel = null;
|
|
56
|
+
let unsubscribeSignals = null;
|
|
57
|
+
let openPromise = null;
|
|
58
|
+
let resolveOpen = null;
|
|
59
|
+
let rejectOpen = null;
|
|
60
|
+
let pendingRemoteIceCandidates = [];
|
|
61
|
+
let lastConnectionState = null;
|
|
62
|
+
let lastIceConnectionState = null;
|
|
63
|
+
let lastIceGatheringState = null;
|
|
64
|
+
let localIceCandidateCount = 0;
|
|
65
|
+
let remoteIceCandidateCount = 0;
|
|
66
|
+
let expiredRemoteIceCandidateCount = 0;
|
|
67
|
+
let overflowRemoteIceCandidateCount = 0;
|
|
68
|
+
if (!Number.isSafeInteger(maxPendingRemoteIceCandidates) || maxPendingRemoteIceCandidates < 1
|
|
69
|
+
|| !Number.isSafeInteger(pendingRemoteIceTtlMs) || pendingRemoteIceTtlMs < 1) {
|
|
70
|
+
throw new TypeError('Explicit positive ICE candidate limits are required');
|
|
71
|
+
}
|
|
72
|
+
const localIceCandidateTypes = new Set();
|
|
73
|
+
const remoteIceCandidateTypes = new Set();
|
|
74
|
+
const maxPendingIceCandidates = Math.max(1, Number(maxPendingRemoteIceCandidates));
|
|
75
|
+
const pendingIceTtlMs = Math.max(1, Number(pendingRemoteIceTtlMs));
|
|
76
|
+
|
|
77
|
+
const prunePendingRemoteIceCandidates = () => {
|
|
78
|
+
const cutoff = now() - pendingIceTtlMs;
|
|
79
|
+
const retained = pendingRemoteIceCandidates.filter((entry) => entry.receivedAt >= cutoff);
|
|
80
|
+
expiredRemoteIceCandidateCount += pendingRemoteIceCandidates.length - retained.length;
|
|
81
|
+
pendingRemoteIceCandidates = retained;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const recordCandidateType = (target, candidate) => {
|
|
85
|
+
const explicitType = String(candidate?.type || '').trim();
|
|
86
|
+
if (explicitType) {
|
|
87
|
+
target.add(explicitType);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const match = String(candidate?.candidate || '').match(/\btyp\s+([a-z0-9_-]+)/i);
|
|
91
|
+
if (match?.[1]) target.add(match[1].toLowerCase());
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function setState(nextState) {
|
|
95
|
+
if (state === nextState) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
state = nextState;
|
|
100
|
+
|
|
101
|
+
if (typeof onStateChange === 'function') {
|
|
102
|
+
onStateChange(state);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isActivePeer(pc) {
|
|
107
|
+
return peerConnection === pc && (
|
|
108
|
+
state === P2P_TRANSPORT_STATES.CONNECTING || state === P2P_TRANSPORT_STATES.CONNECTED
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function connect() {
|
|
113
|
+
if (state !== P2P_TRANSPORT_STATES.IDLE) {
|
|
114
|
+
return ready();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
openPromise = new Promise((resolve, reject) => {
|
|
118
|
+
resolveOpen = resolve;
|
|
119
|
+
rejectOpen = reject;
|
|
120
|
+
});
|
|
121
|
+
setState(P2P_TRANSPORT_STATES.CONNECTING);
|
|
122
|
+
if (state !== P2P_TRANSPORT_STATES.CONNECTING) return openPromise;
|
|
123
|
+
|
|
124
|
+
connectTimer = setTimeout(() => fail(createConnectionError('Connection deadline exceeded')), policy.connectTimeoutMs);
|
|
125
|
+
try {
|
|
126
|
+
const pc = new RTCPeerConnectionImpl(rtcConfig);
|
|
127
|
+
peerConnection = pc;
|
|
128
|
+
wirePeerConnection(pc);
|
|
129
|
+
|
|
130
|
+
if (typeof onPeerConnection === 'function') {
|
|
131
|
+
onPeerConnection(pc);
|
|
132
|
+
}
|
|
133
|
+
if (!isActivePeer(pc)) return openPromise;
|
|
134
|
+
|
|
135
|
+
const unsubscribe = signaling.subscribe((message) => {
|
|
136
|
+
void handleSignal(message).catch((error) => {
|
|
137
|
+
if (isActivePeer(pc)) fail(error);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
if (!isActivePeer(pc)) {
|
|
141
|
+
unsubscribe();
|
|
142
|
+
return openPromise;
|
|
143
|
+
}
|
|
144
|
+
unsubscribeSignals = unsubscribe;
|
|
145
|
+
|
|
146
|
+
if (transportInitiator) {
|
|
147
|
+
attachDataChannel(pc.createDataChannel(dataChannelLabel, dataChannelOptions));
|
|
148
|
+
if (isActivePeer(pc)) {
|
|
149
|
+
void sendInitialOffer(pc).catch((error) => {
|
|
150
|
+
if (isActivePeer(pc)) fail(error);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch (error) {
|
|
155
|
+
fail(error);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Return the lifecycle promise before awaiting browser setup, so close and
|
|
159
|
+
// the deadline can settle every waiter even if an offer never resolves.
|
|
160
|
+
return openPromise;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function sendInitialOffer(pc) {
|
|
164
|
+
const offer = await pc.createOffer();
|
|
165
|
+
if (!isActivePeer(pc)) return;
|
|
166
|
+
await pc.setLocalDescription(offer);
|
|
167
|
+
if (!isActivePeer(pc)) return;
|
|
168
|
+
await signaling.sendOffer(descriptionToPayload(pc.localDescription));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function ready() {
|
|
172
|
+
if (state === P2P_TRANSPORT_STATES.CONNECTED) {
|
|
173
|
+
return Promise.resolve();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!openPromise) {
|
|
177
|
+
return Promise.reject(new Error('transport is not connecting'));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return openPromise;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function send(value) {
|
|
184
|
+
if (!dataChannel || dataChannel.readyState !== 'open') {
|
|
185
|
+
throw new Error('data channel is not open');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const encoded = serialize(value);
|
|
189
|
+
const bytes = typeof encoded === 'string' ? new TextEncoder().encode(encoded).byteLength : encoded.byteLength ?? encoded.size;
|
|
190
|
+
if (!Number.isFinite(bytes) || bytes > policy.maxMessageBytes) throw new Error('Data channel message limit exceeded');
|
|
191
|
+
if (dataChannel.bufferedAmount + bytes > policy.maxBufferedBytes) throw new Error('Data channel backpressure limit exceeded');
|
|
192
|
+
dataChannel.send(encoded);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function close(reason = null) {
|
|
196
|
+
if (state === P2P_TRANSPORT_STATES.CLOSED || state === P2P_TRANSPORT_STATES.CLOSING) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
setState(P2P_TRANSPORT_STATES.CLOSING);
|
|
201
|
+
closeLocal(createConnectionError(
|
|
202
|
+
reason ? `peer transport closed while connecting: ${String(reason)}` : 'peer transport closed while connecting'
|
|
203
|
+
));
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
if (typeof signaling.sendClose === 'function') {
|
|
207
|
+
void Promise.resolve(signaling.sendClose(reason)).catch(() => {});
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
// Closing is best-effort.
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function rejectPendingOpen(error) {
|
|
215
|
+
if (!rejectOpen) return;
|
|
216
|
+
rejectOpen(error);
|
|
217
|
+
rejectOpen = null;
|
|
218
|
+
resolveOpen = null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function closeLocal(reason = null) {
|
|
222
|
+
clearTimeout(connectTimer);
|
|
223
|
+
connectTimer = null;
|
|
224
|
+
// A close can occur before a DataChannel opens: explicit cancellation,
|
|
225
|
+
// remote close, and RTCPeerConnection.close() all take this path. The
|
|
226
|
+
// connect() caller must settle in every case so a failed session cannot
|
|
227
|
+
// strand a provider slot or hide a retryable transport failure.
|
|
228
|
+
if (rejectOpen) {
|
|
229
|
+
rejectPendingOpen(reason instanceof Error
|
|
230
|
+
? reason
|
|
231
|
+
: createConnectionError('peer transport closed before data channel opened'));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Drop ownership before invoking callbacks: browser close events and host
|
|
235
|
+
// unsubscribe hooks can reenter this path.
|
|
236
|
+
const unsubscribe = unsubscribeSignals;
|
|
237
|
+
const channel = dataChannel;
|
|
238
|
+
const pc = peerConnection;
|
|
239
|
+
unsubscribeSignals = null;
|
|
240
|
+
dataChannel = null;
|
|
241
|
+
peerConnection = null;
|
|
242
|
+
pendingRemoteIceCandidates = [];
|
|
243
|
+
|
|
244
|
+
if (state !== P2P_TRANSPORT_STATES.FAILED) {
|
|
245
|
+
setState(P2P_TRANSPORT_STATES.CLOSED);
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
unsubscribe?.();
|
|
249
|
+
} finally {
|
|
250
|
+
try {
|
|
251
|
+
if (channel && channel.readyState !== 'closed') channel.close();
|
|
252
|
+
} finally {
|
|
253
|
+
pc?.close();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function fail(error) {
|
|
259
|
+
if (state === P2P_TRANSPORT_STATES.CLOSED || state === P2P_TRANSPORT_STATES.CLOSING) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
setState(P2P_TRANSPORT_STATES.FAILED);
|
|
264
|
+
|
|
265
|
+
if (rejectOpen) {
|
|
266
|
+
rejectOpen(error);
|
|
267
|
+
rejectOpen = null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
closeLocal(error);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function wirePeerConnection(pc) {
|
|
274
|
+
pc.onicecandidate = (event) => {
|
|
275
|
+
if (!isActivePeer(pc) || !event.candidate) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
localIceCandidateCount += 1;
|
|
280
|
+
recordCandidateType(localIceCandidateTypes, event.candidate);
|
|
281
|
+
void Promise.resolve(signaling.sendIceCandidate(candidateToPayload(event.candidate))).catch((error) => {
|
|
282
|
+
fail(error);
|
|
283
|
+
});
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
pc.ondatachannel = (event) => {
|
|
287
|
+
if (!isActivePeer(pc)) {
|
|
288
|
+
event.channel.close();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (event.channel.label !== dataChannelLabel && onAuxiliaryChannel) {
|
|
292
|
+
if (onAuxiliaryChannel(event.channel) !== true) event.channel.close();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (transportInitiator) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
attachDataChannel(event.channel);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
pc.onconnectionstatechange = () => {
|
|
303
|
+
if (!isActivePeer(pc)) return;
|
|
304
|
+
lastConnectionState = pc.connectionState || null;
|
|
305
|
+
if (pc.connectionState === 'failed') {
|
|
306
|
+
fail(createConnectionError('peer connection failed'));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (pc.connectionState === 'closed') {
|
|
310
|
+
closeLocal();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
pc.oniceconnectionstatechange = () => {
|
|
315
|
+
if (!isActivePeer(pc)) return;
|
|
316
|
+
lastIceConnectionState = pc.iceConnectionState || null;
|
|
317
|
+
if (pc.iceConnectionState === 'failed') {
|
|
318
|
+
fail(createConnectionError('ICE connection failed'));
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
pc.onicegatheringstatechange = () => {
|
|
323
|
+
if (!isActivePeer(pc)) return;
|
|
324
|
+
lastIceGatheringState = pc.iceGatheringState || null;
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function attachDataChannel(channel) {
|
|
329
|
+
dataChannel = channel;
|
|
330
|
+
const pc = peerConnection;
|
|
331
|
+
const isActiveChannel = () => dataChannel === channel && isActivePeer(pc);
|
|
332
|
+
|
|
333
|
+
if (typeof onDataChannel === 'function') {
|
|
334
|
+
onDataChannel(channel);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
channel.onopen = () => {
|
|
338
|
+
if (!isActiveChannel()) return;
|
|
339
|
+
clearTimeout(connectTimer);
|
|
340
|
+
connectTimer = null;
|
|
341
|
+
setState(P2P_TRANSPORT_STATES.CONNECTED);
|
|
342
|
+
|
|
343
|
+
if (resolveOpen) {
|
|
344
|
+
resolveOpen();
|
|
345
|
+
resolveOpen = null;
|
|
346
|
+
rejectOpen = null;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
channel.onmessage = (event) => {
|
|
351
|
+
if (!isActiveChannel()) return;
|
|
352
|
+
if (typeof onMessage === 'function') {
|
|
353
|
+
try {
|
|
354
|
+
const bytes = typeof event.data === 'string' ? new TextEncoder().encode(event.data).byteLength : event.data?.byteLength ?? event.data?.size;
|
|
355
|
+
if (!Number.isFinite(bytes) || bytes > policy.maxMessageBytes) throw new Error('Incoming data channel message limit exceeded');
|
|
356
|
+
onMessage(deserialize(event.data), event);
|
|
357
|
+
} catch (error) { fail(error); }
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
channel.onerror = () => {
|
|
362
|
+
if (!isActiveChannel()) return;
|
|
363
|
+
fail(new Error('data channel failed'));
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
channel.onclose = () => {
|
|
367
|
+
if (isActiveChannel()) {
|
|
368
|
+
closeLocal(createConnectionError('data channel closed before completion'));
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async function handleSignal(message) {
|
|
374
|
+
const pc = peerConnection;
|
|
375
|
+
if (!isActivePeer(pc)) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (message.type === SIGNAL_TYPES.OFFER) {
|
|
380
|
+
if (transportInitiator) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
await pc.setRemoteDescription(makeSessionDescription(message.payload));
|
|
385
|
+
if (!isActivePeer(pc)) return;
|
|
386
|
+
await flushRemoteIceCandidates(pc);
|
|
387
|
+
if (!isActivePeer(pc)) return;
|
|
388
|
+
const answer = await pc.createAnswer();
|
|
389
|
+
if (!isActivePeer(pc)) return;
|
|
390
|
+
await pc.setLocalDescription(answer);
|
|
391
|
+
if (!isActivePeer(pc)) return;
|
|
392
|
+
await signaling.sendAnswer(descriptionToPayload(pc.localDescription));
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (message.type === SIGNAL_TYPES.ANSWER) {
|
|
397
|
+
if (!transportInitiator) {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
await pc.setRemoteDescription(makeSessionDescription(message.payload));
|
|
402
|
+
if (!isActivePeer(pc)) return;
|
|
403
|
+
await flushRemoteIceCandidates(pc);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (message.type === SIGNAL_TYPES.ICE_CANDIDATE) {
|
|
408
|
+
remoteIceCandidateCount += 1;
|
|
409
|
+
recordCandidateType(remoteIceCandidateTypes, message.payload);
|
|
410
|
+
if (!hasRemoteDescription(pc)) {
|
|
411
|
+
prunePendingRemoteIceCandidates();
|
|
412
|
+
pendingRemoteIceCandidates.push({ payload: message.payload, receivedAt: now() });
|
|
413
|
+
if (pendingRemoteIceCandidates.length > maxPendingIceCandidates) {
|
|
414
|
+
const droppedCount = pendingRemoteIceCandidates.length - maxPendingIceCandidates;
|
|
415
|
+
pendingRemoteIceCandidates.splice(0, droppedCount);
|
|
416
|
+
overflowRemoteIceCandidateCount += droppedCount;
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
await pc.addIceCandidate(makeIceCandidate(message.payload));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (message.type === SIGNAL_TYPES.CLOSE) {
|
|
425
|
+
closeLocal();
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function flushRemoteIceCandidates(pc) {
|
|
430
|
+
if (!isActivePeer(pc) || !hasRemoteDescription(pc) || pendingRemoteIceCandidates.length === 0) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
prunePendingRemoteIceCandidates();
|
|
435
|
+
const candidates = pendingRemoteIceCandidates;
|
|
436
|
+
pendingRemoteIceCandidates = [];
|
|
437
|
+
for (const candidate of candidates) {
|
|
438
|
+
if (!isActivePeer(pc)) return;
|
|
439
|
+
await pc.addIceCandidate(makeIceCandidate(candidate.payload));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function hasRemoteDescription(pc) {
|
|
444
|
+
return Boolean(pc?.remoteDescription || pc?.currentRemoteDescription);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function makeSessionDescription(payload) {
|
|
448
|
+
if (RTCSessionDescriptionImpl) {
|
|
449
|
+
return new RTCSessionDescriptionImpl(payload);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return payload;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function makeIceCandidate(payload) {
|
|
456
|
+
if (RTCIceCandidateImpl) {
|
|
457
|
+
return new RTCIceCandidateImpl(payload);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return payload;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function getDiagnostics() {
|
|
464
|
+
prunePendingRemoteIceCandidates();
|
|
465
|
+
const configuredServers = Array.isArray(rtcConfig?.iceServers) ? rtcConfig.iceServers : [];
|
|
466
|
+
return {
|
|
467
|
+
state,
|
|
468
|
+
connectionState: peerConnection?.connectionState || lastConnectionState,
|
|
469
|
+
iceConnectionState: peerConnection?.iceConnectionState || lastIceConnectionState,
|
|
470
|
+
iceGatheringState: peerConnection?.iceGatheringState || lastIceGatheringState,
|
|
471
|
+
signalingState: peerConnection?.signalingState || null,
|
|
472
|
+
localIceCandidateCount,
|
|
473
|
+
remoteIceCandidateCount,
|
|
474
|
+
localIceCandidateTypes: [...localIceCandidateTypes].sort(),
|
|
475
|
+
remoteIceCandidateTypes: [...remoteIceCandidateTypes].sort(),
|
|
476
|
+
pendingRemoteIceCandidateCount: pendingRemoteIceCandidates.length,
|
|
477
|
+
expiredRemoteIceCandidateCount,
|
|
478
|
+
overflowRemoteIceCandidateCount,
|
|
479
|
+
iceTransportPolicy: rtcConfig?.iceTransportPolicy || 'all',
|
|
480
|
+
stunConfigured: configuredServers.some((server) => (
|
|
481
|
+
(Array.isArray(server.urls) ? server.urls : [server.urls])
|
|
482
|
+
.some((url) => String(url || '').startsWith('stun:'))
|
|
483
|
+
)),
|
|
484
|
+
turnConfigured: configuredServers.some((server) => (
|
|
485
|
+
(Array.isArray(server.urls) ? server.urls : [server.urls])
|
|
486
|
+
.some((url) => /^turns?:/i.test(String(url || '')))
|
|
487
|
+
))
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function createConnectionError(message) {
|
|
492
|
+
const error = new Error(message);
|
|
493
|
+
error.code = 'webrtc_connection_failed';
|
|
494
|
+
error.diagnostics = getDiagnostics();
|
|
495
|
+
if (!error.diagnostics.turnConfigured) {
|
|
496
|
+
error.action = 'Direct ICE connectivity failed and no TURN relay is configured.';
|
|
497
|
+
}
|
|
498
|
+
return error;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return Object.freeze({
|
|
502
|
+
connect,
|
|
503
|
+
ready,
|
|
504
|
+
send,
|
|
505
|
+
close,
|
|
506
|
+
getState: () => state,
|
|
507
|
+
getPeerConnection: () => peerConnection,
|
|
508
|
+
getDataChannel: () => dataChannel,
|
|
509
|
+
getDiagnostics,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
export function descriptionToPayload(description) {
|
|
514
|
+
if (!description) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
type: description.type,
|
|
520
|
+
sdp: description.sdp,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export function candidateToPayload(candidate) {
|
|
525
|
+
if (!candidate) {
|
|
526
|
+
return null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (typeof candidate.toJSON === 'function') {
|
|
530
|
+
return candidate.toJSON();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return {
|
|
534
|
+
candidate: candidate.candidate,
|
|
535
|
+
sdpMid: candidate.sdpMid,
|
|
536
|
+
sdpMLineIndex: candidate.sdpMLineIndex,
|
|
537
|
+
usernameFragment: candidate.usernameFragment,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export function defaultSerialize(value) {
|
|
542
|
+
if (typeof value === 'string') {
|
|
543
|
+
return value;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (value instanceof ArrayBuffer) {
|
|
547
|
+
return value;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (ArrayBuffer.isView(value)) {
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (globalThis.Blob && value instanceof Blob) {
|
|
555
|
+
return value;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return JSON.stringify(value);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function defaultDeserialize(value) {
|
|
562
|
+
if (typeof value !== 'string') {
|
|
563
|
+
return value;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
return JSON.parse(value);
|
|
568
|
+
} catch {
|
|
569
|
+
return value;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Serializes frame writes so concurrent requests share one buffer/byte budget. */
|
|
2
|
+
export function createBoundedChannelWriter({ channel, limits, signal, account }) {
|
|
3
|
+
let tail = Promise.resolve();
|
|
4
|
+
let sentBytes = 0;
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
|
|
7
|
+
function waitForSpace(size, requestSignal) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const finish = error => {
|
|
10
|
+
clearTimeout(timer);
|
|
11
|
+
channel.removeEventListener('bufferedamountlow', low);
|
|
12
|
+
signal.removeEventListener('abort', abort);
|
|
13
|
+
requestSignal?.removeEventListener('abort', abort);
|
|
14
|
+
error ? reject(error) : resolve();
|
|
15
|
+
};
|
|
16
|
+
const abort = () => finish(new Error('Partition channel write cancelled'));
|
|
17
|
+
const low = () => {
|
|
18
|
+
if (channel.bufferedAmount + size <= limits.maxBufferedBytes) finish();
|
|
19
|
+
};
|
|
20
|
+
const timer = setTimeout(() => finish(new Error('Partition channel backpressure timeout')), limits.timeoutMs);
|
|
21
|
+
channel.addEventListener('bufferedamountlow', low);
|
|
22
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
23
|
+
requestSignal?.addEventListener('abort', abort, { once: true });
|
|
24
|
+
if (signal.aborted || requestSignal?.aborted) abort();
|
|
25
|
+
else low();
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return function send(data, requestSignal) {
|
|
30
|
+
const size = typeof data === 'string' ? encoder.encode(data).byteLength : data.byteLength;
|
|
31
|
+
const operation = tail.then(async () => {
|
|
32
|
+
signal.throwIfAborted();
|
|
33
|
+
requestSignal?.throwIfAborted();
|
|
34
|
+
if (channel.bufferedAmount + size > limits.maxBufferedBytes) await waitForSpace(size, requestSignal);
|
|
35
|
+
signal.throwIfAborted();
|
|
36
|
+
requestSignal?.throwIfAborted();
|
|
37
|
+
if (channel.readyState !== 'open') throw new Error('Partition channel unavailable');
|
|
38
|
+
if (sentBytes + size > limits.maxTransferBytes) throw new Error('Partition channel outgoing byte budget exhausted');
|
|
39
|
+
channel.send(data);
|
|
40
|
+
sentBytes += size;
|
|
41
|
+
account(size);
|
|
42
|
+
});
|
|
43
|
+
tail = operation.catch(() => {});
|
|
44
|
+
return operation;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { createP2PTransport, P2P_TRANSPORT_STATES, descriptionToPayload, candidateToPayload, defaultSerialize, defaultDeserialize } from './assignment.js';
|
|
2
|
+
export * from './signaling.js';
|
|
3
|
+
export { createWebRTCSwarm } from './swarm.js';
|
|
4
|
+
export { createSwarmTransport } from './room.js';
|
|
5
|
+
export { createToolOfferChannel, TOOL_OFFER_MESSAGE, TOOL_OFFER_ACK } from './tool-offer-channel.js';
|
|
6
|
+
export * from './retry-policy.js';
|
|
7
|
+
export { default as retryPolicy } from './retry-policy.js';
|
|
8
|
+
export { createPartitionDataChannel } from './partition-data-channel.js';
|
|
9
|
+
export * from './introductions.js';
|
|
10
|
+
export * from './introduction-bootstrap.js';
|
|
11
|
+
export * from './introduction-admission.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { canonicalize } from '../artifacts/canonical-json.js';
|
|
2
|
+
import { toBase64Url, fromBase64Url } from '../artifacts/identity.js';
|
|
3
|
+
|
|
4
|
+
/** Prove the existing private-room capability locally; never disclose the token. */
|
|
5
|
+
export function createRoomIntroductionAdmission({ roomId, token }) {
|
|
6
|
+
if (typeof roomId !== 'string' || !roomId || typeof token !== 'string' || token.length < 32) {
|
|
7
|
+
throw new Error('Private introduction room capability required');
|
|
8
|
+
}
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
const key = crypto.subtle.importKey('raw', encoder.encode(token), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']);
|
|
11
|
+
const bytes = body => encoder.encode(canonicalize({ domain: 'reploid.room-admission/v1', contact: body }));
|
|
12
|
+
return Object.freeze({
|
|
13
|
+
async create(body) {
|
|
14
|
+
if (body.roomId !== roomId) throw new Error('Private introduction namespace mismatch');
|
|
15
|
+
return { schema: 'reploid.room-admission/v1', proof: toBase64Url(await crypto.subtle.sign('HMAC', await key, bytes(body))) };
|
|
16
|
+
},
|
|
17
|
+
async verify(record) {
|
|
18
|
+
const { admission, ...body } = record?.body || {};
|
|
19
|
+
if (body.roomId !== roomId || admission?.schema !== 'reploid.room-admission/v1' || typeof admission.proof !== 'string') return false;
|
|
20
|
+
try { return await crypto.subtle.verify('HMAC', await key, fromBase64Url(admission.proof), bytes(body)); }
|
|
21
|
+
catch { return false; }
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|