@fairfox/polly 0.69.0 → 0.70.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/dist/src/client/index.js +17 -20
- package/dist/src/client/index.js.map +4 -4
- package/dist/src/mesh.js +62 -5
- package/dist/src/mesh.js.map +5 -4
- package/dist/src/peer.js +62 -5
- package/dist/src/peer.js.map +5 -4
- package/dist/src/polly-ui/markdown.js +3 -3
- package/dist/src/polly-ui/markdown.js.map +2 -2
- package/dist/src/shared/lib/mesh-diagnostics.d.ts +98 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +0 -4
- package/dist/tools/test/src/e2e-mesh/console-allowlist.d.ts +31 -0
- package/dist/tools/test/src/e2e-mesh/index.d.ts +27 -0
- package/dist/tools/test/src/e2e-mesh/index.js +1089 -0
- package/dist/tools/test/src/e2e-mesh/index.js.map +22 -0
- package/dist/tools/test/src/e2e-mesh/keys.d.ts +55 -0
- package/dist/tools/test/src/e2e-mesh/launch-peer.d.ts +70 -0
- package/dist/tools/test/src/e2e-mesh/mesh-assertions.d.ts +53 -0
- package/dist/tools/test/src/e2e-mesh/serve-consumer.d.ts +32 -0
- package/dist/tools/test/src/e2e-mesh/wait-for-convergence.d.ts +38 -0
- package/dist/tools/test/src/e2e-mesh/with-relay.d.ts +53 -0
- package/dist/tools/test/src/visual/index.js +24 -24
- package/dist/tools/test/src/visual/index.js.map +2 -2
- package/dist/tools/verify/src/cli.js +361 -22
- package/dist/tools/verify/src/cli.js.map +6 -6
- package/dist/tools/verify/src/config.d.ts +26 -1
- package/dist/tools/verify/src/config.js +9 -1
- package/dist/tools/verify/src/config.js.map +4 -4
- package/dist/tools/verify/src/primitives/index.d.ts +30 -0
- package/dist/tools/visualize/src/cli.js +43 -1
- package/dist/tools/visualize/src/cli.js.map +3 -3
- package/package.json +11 -8
- package/LICENSE +0 -21
- package/README.md +0 -362
|
@@ -0,0 +1,1089 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
function __accessProp(key) {
|
|
8
|
+
return this[key];
|
|
9
|
+
}
|
|
10
|
+
var __toESMCache_node;
|
|
11
|
+
var __toESMCache_esm;
|
|
12
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
13
|
+
var canCache = mod != null && typeof mod === "object";
|
|
14
|
+
if (canCache) {
|
|
15
|
+
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
16
|
+
var cached = cache.get(mod);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
}
|
|
20
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
21
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
22
|
+
for (let key of __getOwnPropNames(mod))
|
|
23
|
+
if (!__hasOwnProp.call(to, key))
|
|
24
|
+
__defProp(to, key, {
|
|
25
|
+
get: __accessProp.bind(mod, key),
|
|
26
|
+
enumerable: true
|
|
27
|
+
});
|
|
28
|
+
if (canCache)
|
|
29
|
+
cache.set(mod, to);
|
|
30
|
+
return to;
|
|
31
|
+
};
|
|
32
|
+
var __toCommonJS = (from) => {
|
|
33
|
+
var entry = (__moduleCache ??= new WeakMap).get(from), desc;
|
|
34
|
+
if (entry)
|
|
35
|
+
return entry;
|
|
36
|
+
entry = __defProp({}, "__esModule", { value: true });
|
|
37
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
38
|
+
for (var key of __getOwnPropNames(from))
|
|
39
|
+
if (!__hasOwnProp.call(entry, key))
|
|
40
|
+
__defProp(entry, key, {
|
|
41
|
+
get: __accessProp.bind(from, key),
|
|
42
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
__moduleCache.set(from, entry);
|
|
46
|
+
return entry;
|
|
47
|
+
};
|
|
48
|
+
var __moduleCache;
|
|
49
|
+
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
50
|
+
var __returnValue = (v) => v;
|
|
51
|
+
function __exportSetter(name, newValue) {
|
|
52
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
53
|
+
}
|
|
54
|
+
var __export = (target, all) => {
|
|
55
|
+
for (var name in all)
|
|
56
|
+
__defProp(target, name, {
|
|
57
|
+
get: all[name],
|
|
58
|
+
enumerable: true,
|
|
59
|
+
configurable: true,
|
|
60
|
+
set: __exportSetter.bind(all, name)
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
64
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
65
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
66
|
+
}) : x)(function(x) {
|
|
67
|
+
if (typeof require !== "undefined")
|
|
68
|
+
return require.apply(this, arguments);
|
|
69
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// src/shared/lib/encryption.ts
|
|
73
|
+
var exports_encryption = {};
|
|
74
|
+
__export(exports_encryption, {
|
|
75
|
+
sealEnvelope: () => sealEnvelope,
|
|
76
|
+
openEnvelope: () => openEnvelope,
|
|
77
|
+
generateDocumentKey: () => generateDocumentKey,
|
|
78
|
+
encrypt: () => encrypt,
|
|
79
|
+
encodeEncryptedEnvelope: () => encodeEncryptedEnvelope,
|
|
80
|
+
decryptOrThrow: () => decryptOrThrow,
|
|
81
|
+
decrypt: () => decrypt,
|
|
82
|
+
decodeEncryptedEnvelope: () => decodeEncryptedEnvelope,
|
|
83
|
+
TAG_BYTES: () => TAG_BYTES,
|
|
84
|
+
NONCE_BYTES: () => NONCE_BYTES,
|
|
85
|
+
KEY_BYTES: () => KEY_BYTES,
|
|
86
|
+
EncryptionError: () => EncryptionError
|
|
87
|
+
});
|
|
88
|
+
import nacl from "tweetnacl";
|
|
89
|
+
function generateDocumentKey() {
|
|
90
|
+
return nacl.randomBytes(KEY_BYTES);
|
|
91
|
+
}
|
|
92
|
+
function encrypt(payload, key) {
|
|
93
|
+
if (key.length !== KEY_BYTES) {
|
|
94
|
+
throw new EncryptionError(`secretbox key must be ${KEY_BYTES} bytes, got ${key.length}.`, "invalid-key-length");
|
|
95
|
+
}
|
|
96
|
+
const nonce = nacl.randomBytes(NONCE_BYTES);
|
|
97
|
+
const ciphertext = nacl.secretbox(payload, nonce, key);
|
|
98
|
+
const out = new Uint8Array(NONCE_BYTES + ciphertext.length);
|
|
99
|
+
out.set(nonce, 0);
|
|
100
|
+
out.set(ciphertext, NONCE_BYTES);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
function decrypt(sealed, key) {
|
|
104
|
+
if (key.length !== KEY_BYTES) {
|
|
105
|
+
throw new EncryptionError(`secretbox key must be ${KEY_BYTES} bytes, got ${key.length}.`, "invalid-key-length");
|
|
106
|
+
}
|
|
107
|
+
if (sealed.length < NONCE_BYTES + TAG_BYTES) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const nonce = sealed.subarray(0, NONCE_BYTES);
|
|
111
|
+
const ciphertext = sealed.subarray(NONCE_BYTES);
|
|
112
|
+
const opened = nacl.secretbox.open(ciphertext, nonce, key);
|
|
113
|
+
return opened ?? undefined;
|
|
114
|
+
}
|
|
115
|
+
function decryptOrThrow(sealed, key) {
|
|
116
|
+
const opened = decrypt(sealed, key);
|
|
117
|
+
if (!opened) {
|
|
118
|
+
throw new EncryptionError(`Failed to decrypt sealed blob: wrong key, malformed input, or tampered ciphertext.`, "decrypt-failed");
|
|
119
|
+
}
|
|
120
|
+
return opened;
|
|
121
|
+
}
|
|
122
|
+
function sealEnvelope(payload, documentId, key) {
|
|
123
|
+
return {
|
|
124
|
+
documentId,
|
|
125
|
+
sealed: encrypt(payload, key)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function openEnvelope(envelope, key) {
|
|
129
|
+
return decryptOrThrow(envelope.sealed, key);
|
|
130
|
+
}
|
|
131
|
+
function encodeEncryptedEnvelope(envelope) {
|
|
132
|
+
const idBytes = new TextEncoder().encode(envelope.documentId);
|
|
133
|
+
const out = new Uint8Array(4 + idBytes.length + envelope.sealed.length);
|
|
134
|
+
const view = new DataView(out.buffer);
|
|
135
|
+
view.setUint32(0, idBytes.length, false);
|
|
136
|
+
out.set(idBytes, 4);
|
|
137
|
+
out.set(envelope.sealed, 4 + idBytes.length);
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
function decodeEncryptedEnvelope(bytes) {
|
|
141
|
+
if (bytes.length < 4) {
|
|
142
|
+
throw new EncryptionError(`Encrypted envelope too short: ${bytes.length} bytes.`, "envelope-malformed");
|
|
143
|
+
}
|
|
144
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
145
|
+
const idLen = view.getUint32(0, false);
|
|
146
|
+
if (bytes.length < 4 + idLen) {
|
|
147
|
+
throw new EncryptionError(`Encrypted envelope truncated: declared id length ${idLen}, total ${bytes.length}.`, "envelope-malformed");
|
|
148
|
+
}
|
|
149
|
+
const documentId = new TextDecoder().decode(bytes.subarray(4, 4 + idLen));
|
|
150
|
+
const sealed = bytes.slice(4 + idLen);
|
|
151
|
+
return { documentId, sealed };
|
|
152
|
+
}
|
|
153
|
+
var KEY_BYTES = 32, NONCE_BYTES = 24, TAG_BYTES = 16, EncryptionError;
|
|
154
|
+
var init_encryption = __esm(() => {
|
|
155
|
+
EncryptionError = class EncryptionError extends Error {
|
|
156
|
+
code;
|
|
157
|
+
constructor(message, code) {
|
|
158
|
+
super(message);
|
|
159
|
+
this.name = "EncryptionError";
|
|
160
|
+
this.code = code;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// tools/test/src/e2e-mesh/console-allowlist.ts
|
|
166
|
+
var MESH_CONSOLE_ALLOWLIST = [
|
|
167
|
+
{
|
|
168
|
+
match: "[polly#107 H5]",
|
|
169
|
+
reason: "polly#107 H5 warning fires whenever $meshState resolves against an unconfigured module instance during normal warmup; the issue tracks the longer fix."
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
match: "automerge",
|
|
173
|
+
level: "log",
|
|
174
|
+
reason: "Automerge logs its own version banner at log level on first import; benign."
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
match: "using deprecated parameters for `initSync()`",
|
|
178
|
+
reason: "Automerge WASM bundler emits a deprecation warning on first init; upstream noise polly cannot suppress without a forked bundle."
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
match: "Failed to load resource",
|
|
182
|
+
level: "error",
|
|
183
|
+
reason: "Puppeteer logs a 404 for /favicon.ico against the in-tree consumer because the consumer does not ship one; the e2e harness only cares about app-level errors."
|
|
184
|
+
}
|
|
185
|
+
];
|
|
186
|
+
function isAllowedConsoleLine(line, allowlist = MESH_CONSOLE_ALLOWLIST) {
|
|
187
|
+
for (const entry of allowlist) {
|
|
188
|
+
if (entry.level && entry.level !== line.level)
|
|
189
|
+
continue;
|
|
190
|
+
if (typeof entry.match === "string") {
|
|
191
|
+
if (line.text.includes(entry.match))
|
|
192
|
+
return true;
|
|
193
|
+
} else if (entry.match.test(line.text)) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
// tools/test/src/e2e-mesh/keys.ts
|
|
200
|
+
init_encryption();
|
|
201
|
+
|
|
202
|
+
// src/shared/lib/signing.ts
|
|
203
|
+
import nacl2 from "tweetnacl";
|
|
204
|
+
var PUBLIC_KEY_BYTES = 32;
|
|
205
|
+
var SECRET_KEY_BYTES = 64;
|
|
206
|
+
var SIGNATURE_BYTES = 64;
|
|
207
|
+
|
|
208
|
+
class SigningError extends Error {
|
|
209
|
+
code;
|
|
210
|
+
constructor(message, code) {
|
|
211
|
+
super(message);
|
|
212
|
+
this.name = "SigningError";
|
|
213
|
+
this.code = code;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function generateSigningKeyPair() {
|
|
217
|
+
const pair = nacl2.sign.keyPair();
|
|
218
|
+
return {
|
|
219
|
+
publicKey: pair.publicKey,
|
|
220
|
+
secretKey: pair.secretKey
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function signingKeyPairFromSecret(secretKey) {
|
|
224
|
+
if (secretKey.length !== SECRET_KEY_BYTES) {
|
|
225
|
+
throw new SigningError(`Ed25519 secret key must be ${SECRET_KEY_BYTES} bytes, got ${secretKey.length}.`, "invalid-secret-key");
|
|
226
|
+
}
|
|
227
|
+
const pair = nacl2.sign.keyPair.fromSecretKey(secretKey);
|
|
228
|
+
return {
|
|
229
|
+
publicKey: pair.publicKey,
|
|
230
|
+
secretKey: pair.secretKey
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function sign(payload, secretKey) {
|
|
234
|
+
if (secretKey.length !== SECRET_KEY_BYTES) {
|
|
235
|
+
throw new SigningError(`Ed25519 secret key must be ${SECRET_KEY_BYTES} bytes, got ${secretKey.length}.`, "invalid-secret-key");
|
|
236
|
+
}
|
|
237
|
+
return nacl2.sign.detached(payload, secretKey);
|
|
238
|
+
}
|
|
239
|
+
function verify(payload, signature, publicKey) {
|
|
240
|
+
if (publicKey.length !== PUBLIC_KEY_BYTES) {
|
|
241
|
+
throw new SigningError(`Ed25519 public key must be ${PUBLIC_KEY_BYTES} bytes, got ${publicKey.length}.`, "invalid-public-key");
|
|
242
|
+
}
|
|
243
|
+
if (signature.length !== SIGNATURE_BYTES) {
|
|
244
|
+
throw new SigningError(`Ed25519 signature must be ${SIGNATURE_BYTES} bytes, got ${signature.length}.`, "invalid-signature-length");
|
|
245
|
+
}
|
|
246
|
+
return nacl2.sign.detached.verify(payload, signature, publicKey);
|
|
247
|
+
}
|
|
248
|
+
function signEnvelope(payload, senderId, secretKey) {
|
|
249
|
+
const signature = sign(payload, secretKey);
|
|
250
|
+
return { senderId, payload, signature };
|
|
251
|
+
}
|
|
252
|
+
function openEnvelope2(envelope, publicKey) {
|
|
253
|
+
const ok = verify(envelope.payload, envelope.signature, publicKey);
|
|
254
|
+
if (!ok) {
|
|
255
|
+
throw new SigningError(`Signature verification failed for envelope from ${envelope.senderId}.`, "envelope-malformed");
|
|
256
|
+
}
|
|
257
|
+
return envelope.payload;
|
|
258
|
+
}
|
|
259
|
+
function encodeSignedEnvelope(envelope) {
|
|
260
|
+
const senderBytes = new TextEncoder().encode(envelope.senderId);
|
|
261
|
+
const total = 4 + senderBytes.length + SIGNATURE_BYTES + envelope.payload.length;
|
|
262
|
+
const out = new Uint8Array(total);
|
|
263
|
+
const view = new DataView(out.buffer);
|
|
264
|
+
view.setUint32(0, senderBytes.length, false);
|
|
265
|
+
out.set(senderBytes, 4);
|
|
266
|
+
out.set(envelope.signature, 4 + senderBytes.length);
|
|
267
|
+
out.set(envelope.payload, 4 + senderBytes.length + SIGNATURE_BYTES);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
function decodeSignedEnvelope(bytes) {
|
|
271
|
+
if (bytes.length < 4 + SIGNATURE_BYTES) {
|
|
272
|
+
throw new SigningError(`Envelope too short: ${bytes.length} bytes, need at least ${4 + SIGNATURE_BYTES}.`, "envelope-malformed");
|
|
273
|
+
}
|
|
274
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
275
|
+
const senderLen = view.getUint32(0, false);
|
|
276
|
+
if (bytes.length < 4 + senderLen + SIGNATURE_BYTES) {
|
|
277
|
+
throw new SigningError(`Envelope truncated: declared sender length ${senderLen}, total ${bytes.length}.`, "envelope-malformed");
|
|
278
|
+
}
|
|
279
|
+
const senderId = new TextDecoder().decode(bytes.subarray(4, 4 + senderLen));
|
|
280
|
+
const signature = bytes.slice(4 + senderLen, 4 + senderLen + SIGNATURE_BYTES);
|
|
281
|
+
const payload = bytes.slice(4 + senderLen + SIGNATURE_BYTES);
|
|
282
|
+
return { senderId, payload, signature };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// tools/test/src/e2e-mesh/keys.ts
|
|
286
|
+
function toBase64(bytes) {
|
|
287
|
+
let binary = "";
|
|
288
|
+
for (let i = 0;i < bytes.byteLength; i++) {
|
|
289
|
+
binary += String.fromCharCode(bytes[i]);
|
|
290
|
+
}
|
|
291
|
+
return btoa(binary);
|
|
292
|
+
}
|
|
293
|
+
function prebakeKeyringPair(peerIdA = "peer-a", peerIdB = "peer-b") {
|
|
294
|
+
const set = prebakeKeyringSet([peerIdA, peerIdB]);
|
|
295
|
+
return {
|
|
296
|
+
peers: [set.peers[0], set.peers[1]],
|
|
297
|
+
docKeyB64: set.docKeyB64
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function prebakeKeyringSet(peerIds) {
|
|
301
|
+
if (peerIds.length < 2) {
|
|
302
|
+
throw new Error("prebakeKeyringSet: at least two peer ids required");
|
|
303
|
+
}
|
|
304
|
+
const docKey = generateDocumentKey();
|
|
305
|
+
const peers = peerIds.map((peerId) => {
|
|
306
|
+
const pair = generateSigningKeyPair();
|
|
307
|
+
return {
|
|
308
|
+
peerId,
|
|
309
|
+
identitySecretKeyB64: toBase64(pair.secretKey),
|
|
310
|
+
identityPublicKeyB64: toBase64(pair.publicKey)
|
|
311
|
+
};
|
|
312
|
+
});
|
|
313
|
+
return { peers, docKeyB64: toBase64(docKey) };
|
|
314
|
+
}
|
|
315
|
+
function knownPeersFor(set, thisPeerId) {
|
|
316
|
+
const out = {};
|
|
317
|
+
for (const peer of set.peers) {
|
|
318
|
+
if (peer.peerId === thisPeerId)
|
|
319
|
+
continue;
|
|
320
|
+
out[peer.peerId] = peer.identityPublicKeyB64;
|
|
321
|
+
}
|
|
322
|
+
return out;
|
|
323
|
+
}
|
|
324
|
+
// tools/test/src/e2e-mesh/launch-peer.ts
|
|
325
|
+
var {existsSync, mkdirSync, rmSync} = (() => ({}));
|
|
326
|
+
|
|
327
|
+
// node:os
|
|
328
|
+
var tmpdir = function() {
|
|
329
|
+
return "/tmp";
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// node:path
|
|
333
|
+
function assertPath(path) {
|
|
334
|
+
if (typeof path !== "string")
|
|
335
|
+
throw TypeError("Path must be a string. Received " + JSON.stringify(path));
|
|
336
|
+
}
|
|
337
|
+
function normalizeStringPosix(path, allowAboveRoot) {
|
|
338
|
+
var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
|
|
339
|
+
for (var i = 0;i <= path.length; ++i) {
|
|
340
|
+
if (i < path.length)
|
|
341
|
+
code = path.charCodeAt(i);
|
|
342
|
+
else if (code === 47)
|
|
343
|
+
break;
|
|
344
|
+
else
|
|
345
|
+
code = 47;
|
|
346
|
+
if (code === 47) {
|
|
347
|
+
if (lastSlash === i - 1 || dots === 1)
|
|
348
|
+
;
|
|
349
|
+
else if (lastSlash !== i - 1 && dots === 2) {
|
|
350
|
+
if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
|
|
351
|
+
if (res.length > 2) {
|
|
352
|
+
var lastSlashIndex = res.lastIndexOf("/");
|
|
353
|
+
if (lastSlashIndex !== res.length - 1) {
|
|
354
|
+
if (lastSlashIndex === -1)
|
|
355
|
+
res = "", lastSegmentLength = 0;
|
|
356
|
+
else
|
|
357
|
+
res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
|
|
358
|
+
lastSlash = i, dots = 0;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
} else if (res.length === 2 || res.length === 1) {
|
|
362
|
+
res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (allowAboveRoot) {
|
|
367
|
+
if (res.length > 0)
|
|
368
|
+
res += "/..";
|
|
369
|
+
else
|
|
370
|
+
res = "..";
|
|
371
|
+
lastSegmentLength = 2;
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
if (res.length > 0)
|
|
375
|
+
res += "/" + path.slice(lastSlash + 1, i);
|
|
376
|
+
else
|
|
377
|
+
res = path.slice(lastSlash + 1, i);
|
|
378
|
+
lastSegmentLength = i - lastSlash - 1;
|
|
379
|
+
}
|
|
380
|
+
lastSlash = i, dots = 0;
|
|
381
|
+
} else if (code === 46 && dots !== -1)
|
|
382
|
+
++dots;
|
|
383
|
+
else
|
|
384
|
+
dots = -1;
|
|
385
|
+
}
|
|
386
|
+
return res;
|
|
387
|
+
}
|
|
388
|
+
function _format(sep, pathObject) {
|
|
389
|
+
var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
|
|
390
|
+
if (!dir)
|
|
391
|
+
return base;
|
|
392
|
+
if (dir === pathObject.root)
|
|
393
|
+
return dir + base;
|
|
394
|
+
return dir + sep + base;
|
|
395
|
+
}
|
|
396
|
+
function resolve() {
|
|
397
|
+
var resolvedPath = "", resolvedAbsolute = false, cwd;
|
|
398
|
+
for (var i = arguments.length - 1;i >= -1 && !resolvedAbsolute; i--) {
|
|
399
|
+
var path;
|
|
400
|
+
if (i >= 0)
|
|
401
|
+
path = arguments[i];
|
|
402
|
+
else {
|
|
403
|
+
if (cwd === undefined)
|
|
404
|
+
cwd = process.cwd();
|
|
405
|
+
path = cwd;
|
|
406
|
+
}
|
|
407
|
+
if (assertPath(path), path.length === 0)
|
|
408
|
+
continue;
|
|
409
|
+
resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
|
|
410
|
+
}
|
|
411
|
+
if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
|
|
412
|
+
if (resolvedPath.length > 0)
|
|
413
|
+
return "/" + resolvedPath;
|
|
414
|
+
else
|
|
415
|
+
return "/";
|
|
416
|
+
else if (resolvedPath.length > 0)
|
|
417
|
+
return resolvedPath;
|
|
418
|
+
else
|
|
419
|
+
return ".";
|
|
420
|
+
}
|
|
421
|
+
function normalize(path) {
|
|
422
|
+
if (assertPath(path), path.length === 0)
|
|
423
|
+
return ".";
|
|
424
|
+
var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
|
|
425
|
+
if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute)
|
|
426
|
+
path = ".";
|
|
427
|
+
if (path.length > 0 && trailingSeparator)
|
|
428
|
+
path += "/";
|
|
429
|
+
if (isAbsolute)
|
|
430
|
+
return "/" + path;
|
|
431
|
+
return path;
|
|
432
|
+
}
|
|
433
|
+
function isAbsolute(path) {
|
|
434
|
+
return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
|
|
435
|
+
}
|
|
436
|
+
function join() {
|
|
437
|
+
if (arguments.length === 0)
|
|
438
|
+
return ".";
|
|
439
|
+
var joined;
|
|
440
|
+
for (var i = 0;i < arguments.length; ++i) {
|
|
441
|
+
var arg = arguments[i];
|
|
442
|
+
if (assertPath(arg), arg.length > 0)
|
|
443
|
+
if (joined === undefined)
|
|
444
|
+
joined = arg;
|
|
445
|
+
else
|
|
446
|
+
joined += "/" + arg;
|
|
447
|
+
}
|
|
448
|
+
if (joined === undefined)
|
|
449
|
+
return ".";
|
|
450
|
+
return normalize(joined);
|
|
451
|
+
}
|
|
452
|
+
function relative(from, to) {
|
|
453
|
+
if (assertPath(from), assertPath(to), from === to)
|
|
454
|
+
return "";
|
|
455
|
+
if (from = resolve(from), to = resolve(to), from === to)
|
|
456
|
+
return "";
|
|
457
|
+
var fromStart = 1;
|
|
458
|
+
for (;fromStart < from.length; ++fromStart)
|
|
459
|
+
if (from.charCodeAt(fromStart) !== 47)
|
|
460
|
+
break;
|
|
461
|
+
var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
|
|
462
|
+
for (;toStart < to.length; ++toStart)
|
|
463
|
+
if (to.charCodeAt(toStart) !== 47)
|
|
464
|
+
break;
|
|
465
|
+
var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
|
|
466
|
+
for (;i <= length; ++i) {
|
|
467
|
+
if (i === length) {
|
|
468
|
+
if (toLen > length) {
|
|
469
|
+
if (to.charCodeAt(toStart + i) === 47)
|
|
470
|
+
return to.slice(toStart + i + 1);
|
|
471
|
+
else if (i === 0)
|
|
472
|
+
return to.slice(toStart + i);
|
|
473
|
+
} else if (fromLen > length) {
|
|
474
|
+
if (from.charCodeAt(fromStart + i) === 47)
|
|
475
|
+
lastCommonSep = i;
|
|
476
|
+
else if (i === 0)
|
|
477
|
+
lastCommonSep = 0;
|
|
478
|
+
}
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
|
|
482
|
+
if (fromCode !== toCode)
|
|
483
|
+
break;
|
|
484
|
+
else if (fromCode === 47)
|
|
485
|
+
lastCommonSep = i;
|
|
486
|
+
}
|
|
487
|
+
var out = "";
|
|
488
|
+
for (i = fromStart + lastCommonSep + 1;i <= fromEnd; ++i)
|
|
489
|
+
if (i === fromEnd || from.charCodeAt(i) === 47)
|
|
490
|
+
if (out.length === 0)
|
|
491
|
+
out += "..";
|
|
492
|
+
else
|
|
493
|
+
out += "/..";
|
|
494
|
+
if (out.length > 0)
|
|
495
|
+
return out + to.slice(toStart + lastCommonSep);
|
|
496
|
+
else {
|
|
497
|
+
if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47)
|
|
498
|
+
++toStart;
|
|
499
|
+
return to.slice(toStart);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
function _makeLong(path) {
|
|
503
|
+
return path;
|
|
504
|
+
}
|
|
505
|
+
function dirname(path) {
|
|
506
|
+
if (assertPath(path), path.length === 0)
|
|
507
|
+
return ".";
|
|
508
|
+
var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
|
|
509
|
+
for (var i = path.length - 1;i >= 1; --i)
|
|
510
|
+
if (code = path.charCodeAt(i), code === 47) {
|
|
511
|
+
if (!matchedSlash) {
|
|
512
|
+
end = i;
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
} else
|
|
516
|
+
matchedSlash = false;
|
|
517
|
+
if (end === -1)
|
|
518
|
+
return hasRoot ? "/" : ".";
|
|
519
|
+
if (hasRoot && end === 1)
|
|
520
|
+
return "//";
|
|
521
|
+
return path.slice(0, end);
|
|
522
|
+
}
|
|
523
|
+
function basename(path, ext) {
|
|
524
|
+
if (ext !== undefined && typeof ext !== "string")
|
|
525
|
+
throw TypeError('"ext" argument must be a string');
|
|
526
|
+
assertPath(path);
|
|
527
|
+
var start = 0, end = -1, matchedSlash = true, i;
|
|
528
|
+
if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
|
|
529
|
+
if (ext.length === path.length && ext === path)
|
|
530
|
+
return "";
|
|
531
|
+
var extIdx = ext.length - 1, firstNonSlashEnd = -1;
|
|
532
|
+
for (i = path.length - 1;i >= 0; --i) {
|
|
533
|
+
var code = path.charCodeAt(i);
|
|
534
|
+
if (code === 47) {
|
|
535
|
+
if (!matchedSlash) {
|
|
536
|
+
start = i + 1;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
if (firstNonSlashEnd === -1)
|
|
541
|
+
matchedSlash = false, firstNonSlashEnd = i + 1;
|
|
542
|
+
if (extIdx >= 0)
|
|
543
|
+
if (code === ext.charCodeAt(extIdx)) {
|
|
544
|
+
if (--extIdx === -1)
|
|
545
|
+
end = i;
|
|
546
|
+
} else
|
|
547
|
+
extIdx = -1, end = firstNonSlashEnd;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (start === end)
|
|
551
|
+
end = firstNonSlashEnd;
|
|
552
|
+
else if (end === -1)
|
|
553
|
+
end = path.length;
|
|
554
|
+
return path.slice(start, end);
|
|
555
|
+
} else {
|
|
556
|
+
for (i = path.length - 1;i >= 0; --i)
|
|
557
|
+
if (path.charCodeAt(i) === 47) {
|
|
558
|
+
if (!matchedSlash) {
|
|
559
|
+
start = i + 1;
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
} else if (end === -1)
|
|
563
|
+
matchedSlash = false, end = i + 1;
|
|
564
|
+
if (end === -1)
|
|
565
|
+
return "";
|
|
566
|
+
return path.slice(start, end);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
function extname(path) {
|
|
570
|
+
assertPath(path);
|
|
571
|
+
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
|
|
572
|
+
for (var i = path.length - 1;i >= 0; --i) {
|
|
573
|
+
var code = path.charCodeAt(i);
|
|
574
|
+
if (code === 47) {
|
|
575
|
+
if (!matchedSlash) {
|
|
576
|
+
startPart = i + 1;
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
if (end === -1)
|
|
582
|
+
matchedSlash = false, end = i + 1;
|
|
583
|
+
if (code === 46) {
|
|
584
|
+
if (startDot === -1)
|
|
585
|
+
startDot = i;
|
|
586
|
+
else if (preDotState !== 1)
|
|
587
|
+
preDotState = 1;
|
|
588
|
+
} else if (startDot !== -1)
|
|
589
|
+
preDotState = -1;
|
|
590
|
+
}
|
|
591
|
+
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
|
|
592
|
+
return "";
|
|
593
|
+
return path.slice(startDot, end);
|
|
594
|
+
}
|
|
595
|
+
function format(pathObject) {
|
|
596
|
+
if (pathObject === null || typeof pathObject !== "object")
|
|
597
|
+
throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
|
|
598
|
+
return _format("/", pathObject);
|
|
599
|
+
}
|
|
600
|
+
function parse(path) {
|
|
601
|
+
assertPath(path);
|
|
602
|
+
var ret = { root: "", dir: "", base: "", ext: "", name: "" };
|
|
603
|
+
if (path.length === 0)
|
|
604
|
+
return ret;
|
|
605
|
+
var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
|
|
606
|
+
if (isAbsolute2)
|
|
607
|
+
ret.root = "/", start = 1;
|
|
608
|
+
else
|
|
609
|
+
start = 0;
|
|
610
|
+
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
|
|
611
|
+
for (;i >= start; --i) {
|
|
612
|
+
if (code = path.charCodeAt(i), code === 47) {
|
|
613
|
+
if (!matchedSlash) {
|
|
614
|
+
startPart = i + 1;
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (end === -1)
|
|
620
|
+
matchedSlash = false, end = i + 1;
|
|
621
|
+
if (code === 46) {
|
|
622
|
+
if (startDot === -1)
|
|
623
|
+
startDot = i;
|
|
624
|
+
else if (preDotState !== 1)
|
|
625
|
+
preDotState = 1;
|
|
626
|
+
} else if (startDot !== -1)
|
|
627
|
+
preDotState = -1;
|
|
628
|
+
}
|
|
629
|
+
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
|
|
630
|
+
if (end !== -1)
|
|
631
|
+
if (startPart === 0 && isAbsolute2)
|
|
632
|
+
ret.base = ret.name = path.slice(1, end);
|
|
633
|
+
else
|
|
634
|
+
ret.base = ret.name = path.slice(startPart, end);
|
|
635
|
+
} else {
|
|
636
|
+
if (startPart === 0 && isAbsolute2)
|
|
637
|
+
ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
|
|
638
|
+
else
|
|
639
|
+
ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
|
|
640
|
+
ret.ext = path.slice(startDot, end);
|
|
641
|
+
}
|
|
642
|
+
if (startPart > 0)
|
|
643
|
+
ret.dir = path.slice(0, startPart - 1);
|
|
644
|
+
else if (isAbsolute2)
|
|
645
|
+
ret.dir = "/";
|
|
646
|
+
return ret;
|
|
647
|
+
}
|
|
648
|
+
var sep = "/";
|
|
649
|
+
var delimiter = ":";
|
|
650
|
+
var posix = ((p) => (p.posix = p, p))({ resolve, normalize, isAbsolute, join, relative, _makeLong, dirname, basename, extname, format, parse, sep, delimiter, win32: null, posix: null });
|
|
651
|
+
|
|
652
|
+
// tools/test/src/e2e-mesh/launch-peer.ts
|
|
653
|
+
import puppeteer from "puppeteer";
|
|
654
|
+
|
|
655
|
+
// src/shared/lib/mesh-diagnostics.ts
|
|
656
|
+
var listeners = new Set;
|
|
657
|
+
function emitMeshDiagnostic(diagnostic) {
|
|
658
|
+
const event = { ...diagnostic, timestamp: Date.now() };
|
|
659
|
+
for (const listener of listeners) {
|
|
660
|
+
try {
|
|
661
|
+
listener(event);
|
|
662
|
+
} catch {}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function subscribeToMeshDiagnostics(listener) {
|
|
666
|
+
listeners.add(listener);
|
|
667
|
+
return () => {
|
|
668
|
+
listeners.delete(listener);
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
function recordMeshDiagnostics() {
|
|
672
|
+
const captured = [];
|
|
673
|
+
const stop = subscribeToMeshDiagnostics((event) => {
|
|
674
|
+
captured.push(event);
|
|
675
|
+
});
|
|
676
|
+
return { events: captured, stop };
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// tools/test/src/e2e-mesh/mesh-assertions.ts
|
|
680
|
+
class MeshAssertionError extends Error {
|
|
681
|
+
kind = "mesh-assertion-failure";
|
|
682
|
+
unexpected;
|
|
683
|
+
constructor(message, unexpected) {
|
|
684
|
+
super(message);
|
|
685
|
+
this.name = "MeshAssertionError";
|
|
686
|
+
this.unexpected = unexpected;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
function startDiagnosticRecorder() {
|
|
690
|
+
const { events, stop } = recordMeshDiagnostics();
|
|
691
|
+
function assertNoSilentDrops(allow = []) {
|
|
692
|
+
const allowed = new Set(allow);
|
|
693
|
+
const unexpected = events.filter((event) => event.kind.startsWith("drop:") && !allowed.has(event.kind));
|
|
694
|
+
if (unexpected.length === 0)
|
|
695
|
+
return;
|
|
696
|
+
const summary = unexpected.map((event) => ` ${event.kind} ${JSON.stringify(eventDetails(event))}`).join(`
|
|
697
|
+
`);
|
|
698
|
+
throw new MeshAssertionError(`Unexpected silent-drop diagnostics fired during the e2e run.
|
|
699
|
+
${summary}
|
|
700
|
+
` + `If a drop kind is legitimately expected for this scenario, pass it ` + `to assertNoSilentDrops({ allow: [...] }).`, unexpected);
|
|
701
|
+
}
|
|
702
|
+
return { events, stop, assertNoSilentDrops };
|
|
703
|
+
}
|
|
704
|
+
function eventDetails(event) {
|
|
705
|
+
const { kind: _kind, timestamp: _ts, ...rest } = event;
|
|
706
|
+
return rest;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// tools/test/src/e2e-mesh/launch-peer.ts
|
|
710
|
+
var READY_POLL_MS = 100;
|
|
711
|
+
function profileDir(parent, peerId) {
|
|
712
|
+
const safePeerId = peerId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
713
|
+
return resolve(parent, `polly-e2e-${safePeerId}-${Date.now()}`);
|
|
714
|
+
}
|
|
715
|
+
async function launchPeer(options) {
|
|
716
|
+
const {
|
|
717
|
+
peerId,
|
|
718
|
+
consumerUrl,
|
|
719
|
+
headless = process.env["HEADLESS"] !== "false",
|
|
720
|
+
consoleAllowlist = MESH_CONSOLE_ALLOWLIST,
|
|
721
|
+
readyTimeoutMs = 15000,
|
|
722
|
+
profileParent = resolve(tmpdir(), "polly-e2e")
|
|
723
|
+
} = options;
|
|
724
|
+
if (!existsSync(profileParent))
|
|
725
|
+
mkdirSync(profileParent, { recursive: true });
|
|
726
|
+
const userDataDir = profileDir(profileParent, peerId);
|
|
727
|
+
if (existsSync(userDataDir)) {
|
|
728
|
+
rmSync(userDataDir, { recursive: true, force: true });
|
|
729
|
+
}
|
|
730
|
+
const browser = await puppeteer.launch({
|
|
731
|
+
headless,
|
|
732
|
+
userDataDir,
|
|
733
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
734
|
+
});
|
|
735
|
+
const page = await browser.newPage();
|
|
736
|
+
const consoleLines = [];
|
|
737
|
+
const pageErrors = [];
|
|
738
|
+
page.on("console", (msg) => {
|
|
739
|
+
const level = msg.type();
|
|
740
|
+
const text = msg.text();
|
|
741
|
+
const allowed = isAllowedConsoleLine({ level, text }, consoleAllowlist);
|
|
742
|
+
consoleLines.push({ level, text, allowed });
|
|
743
|
+
});
|
|
744
|
+
page.on("pageerror", (err) => {
|
|
745
|
+
pageErrors.push(err instanceof Error ? err.message : String(err));
|
|
746
|
+
});
|
|
747
|
+
await page.goto(consumerUrl, { waitUntil: "domcontentloaded" });
|
|
748
|
+
const deadline = Date.now() + readyTimeoutMs;
|
|
749
|
+
let ready = false;
|
|
750
|
+
let lastStatus = "";
|
|
751
|
+
while (Date.now() < deadline) {
|
|
752
|
+
lastStatus = await page.evaluate(() => document.querySelector("[data-e2e='status']")?.textContent ?? "");
|
|
753
|
+
if (lastStatus === "ready") {
|
|
754
|
+
ready = true;
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
if (lastStatus.startsWith("error") || lastStatus.startsWith("bootstrap-failed")) {
|
|
758
|
+
await browser.close();
|
|
759
|
+
rmSync(userDataDir, { recursive: true, force: true });
|
|
760
|
+
throw new Error(`launchPeer(${peerId}): consumer reported "${lastStatus}"`);
|
|
761
|
+
}
|
|
762
|
+
await new Promise((r) => setTimeout(r, READY_POLL_MS));
|
|
763
|
+
}
|
|
764
|
+
if (!ready) {
|
|
765
|
+
await browser.close();
|
|
766
|
+
rmSync(userDataDir, { recursive: true, force: true });
|
|
767
|
+
throw new Error(`launchPeer(${peerId}): consumer did not reach "ready" within ${readyTimeoutMs}ms (last status: "${lastStatus}")`);
|
|
768
|
+
}
|
|
769
|
+
function assertNoUnexpectedConsole() {
|
|
770
|
+
const bad = consoleLines.filter((line) => !line.allowed && (line.level === "error" || line.level === "warn" || line.level === "warning"));
|
|
771
|
+
if (bad.length > 0) {
|
|
772
|
+
const summary = bad.map((l) => ` [${l.level}] ${l.text}`).join(`
|
|
773
|
+
`);
|
|
774
|
+
throw new Error(`launchPeer(${peerId}): unexpected console output:
|
|
775
|
+
${summary}`);
|
|
776
|
+
}
|
|
777
|
+
if (pageErrors.length > 0) {
|
|
778
|
+
throw new Error(`launchPeer(${peerId}): page errors:
|
|
779
|
+
${pageErrors.map((e) => ` ${e}`).join(`
|
|
780
|
+
`)}`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
async function collectDiagnostics() {
|
|
784
|
+
const events = await page.evaluate(() => {
|
|
785
|
+
const w = window;
|
|
786
|
+
return w.__pollyE2eDiagnostics ? [...w.__pollyE2eDiagnostics] : [];
|
|
787
|
+
});
|
|
788
|
+
return events;
|
|
789
|
+
}
|
|
790
|
+
async function assertNoSilentDrops(allow = []) {
|
|
791
|
+
const allowed = new Set(allow);
|
|
792
|
+
const events = await collectDiagnostics();
|
|
793
|
+
const unexpected = events.filter((event) => event.kind.startsWith("drop:") && !allowed.has(event.kind));
|
|
794
|
+
if (unexpected.length === 0)
|
|
795
|
+
return;
|
|
796
|
+
const summary = unexpected.map((event) => {
|
|
797
|
+
const { kind, timestamp: _ts, ...rest } = event;
|
|
798
|
+
return ` ${kind} ${JSON.stringify(rest)}`;
|
|
799
|
+
}).join(`
|
|
800
|
+
`);
|
|
801
|
+
throw new MeshAssertionError(`launchPeer(${peerId}): unexpected silent-drop diagnostics fired during the e2e run.
|
|
802
|
+
${summary}
|
|
803
|
+
` + `If a drop kind is legitimately expected, pass it to peer.assertNoSilentDrops([...]).`, unexpected);
|
|
804
|
+
}
|
|
805
|
+
let closed = false;
|
|
806
|
+
return {
|
|
807
|
+
peerId,
|
|
808
|
+
page,
|
|
809
|
+
console: consoleLines,
|
|
810
|
+
pageErrors,
|
|
811
|
+
assertNoUnexpectedConsole,
|
|
812
|
+
collectDiagnostics,
|
|
813
|
+
assertNoSilentDrops,
|
|
814
|
+
close: async () => {
|
|
815
|
+
if (closed)
|
|
816
|
+
return;
|
|
817
|
+
closed = true;
|
|
818
|
+
try {
|
|
819
|
+
await page.close();
|
|
820
|
+
} catch {}
|
|
821
|
+
try {
|
|
822
|
+
await browser.close();
|
|
823
|
+
} catch {}
|
|
824
|
+
try {
|
|
825
|
+
rmSync(userDataDir, { recursive: true, force: true });
|
|
826
|
+
} catch {}
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
// tools/test/src/e2e-mesh/serve-consumer.ts
|
|
831
|
+
var {readFileSync} = (() => ({}));
|
|
832
|
+
var __dirname = "/Users/AJT/projects/polly/packages/polly/tools/test/src/e2e-mesh";
|
|
833
|
+
var pollyRoot = resolve(__dirname, "../../../..");
|
|
834
|
+
var consumerEntry = resolve(pollyRoot, "examples/e2e-consumer/main.ts");
|
|
835
|
+
var consumerHtml = resolve(pollyRoot, "examples/e2e-consumer/index.html");
|
|
836
|
+
var automergeBase64Path = resolve(pollyRoot, "node_modules/@automerge/automerge/dist/mjs/entrypoints/fullfat_base64.js");
|
|
837
|
+
var automergeBase64Plugin = {
|
|
838
|
+
name: "automerge-base64",
|
|
839
|
+
setup(build) {
|
|
840
|
+
build.onResolve({ filter: /^@automerge\/automerge(\/slim)?$/ }, () => ({
|
|
841
|
+
path: automergeBase64Path
|
|
842
|
+
}));
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
async function serveConsumer(options) {
|
|
846
|
+
const buildResult = await Bun.build({
|
|
847
|
+
entrypoints: [consumerEntry],
|
|
848
|
+
target: "browser",
|
|
849
|
+
format: "esm",
|
|
850
|
+
minify: false,
|
|
851
|
+
sourcemap: "inline",
|
|
852
|
+
plugins: [automergeBase64Plugin]
|
|
853
|
+
});
|
|
854
|
+
if (!buildResult.success) {
|
|
855
|
+
const logs = buildResult.logs.map((log) => String(log)).join(`
|
|
856
|
+
`);
|
|
857
|
+
throw new Error(`serveConsumer: build failed:
|
|
858
|
+
${logs}`);
|
|
859
|
+
}
|
|
860
|
+
const jsText = await buildResult.outputs[0]?.text();
|
|
861
|
+
if (!jsText) {
|
|
862
|
+
throw new Error("serveConsumer: build produced no output");
|
|
863
|
+
}
|
|
864
|
+
const rawHtml = readFileSync(consumerHtml, "utf-8");
|
|
865
|
+
const bootstrapJson = JSON.stringify(options.bootstrap);
|
|
866
|
+
const bootstrapShim = `<script>window.__pollyE2eBootstrap = ${bootstrapJson};</script>`;
|
|
867
|
+
const html = rawHtml.replace(/<script type="module" src="\.\/main\.js"><\/script>/, `${bootstrapShim}
|
|
868
|
+
<script type="module" src="./main.js"></script>`);
|
|
869
|
+
const server = Bun.serve({
|
|
870
|
+
port: 0,
|
|
871
|
+
fetch(req) {
|
|
872
|
+
const url = new URL(req.url);
|
|
873
|
+
if (url.pathname === "/" || url.pathname === "/index.html") {
|
|
874
|
+
return new Response(html, { headers: { "Content-Type": "text/html" } });
|
|
875
|
+
}
|
|
876
|
+
if (url.pathname === "/main.js") {
|
|
877
|
+
return new Response(jsText, {
|
|
878
|
+
headers: { "Content-Type": "application/javascript" }
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
return new Response("not found", { status: 404 });
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
return {
|
|
885
|
+
url: `http://127.0.0.1:${server.port}/`,
|
|
886
|
+
close: async () => {
|
|
887
|
+
server.stop();
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
// tools/test/src/e2e-mesh/wait-for-convergence.ts
|
|
892
|
+
async function readPeerSnapshot(peer) {
|
|
893
|
+
return peer.page.evaluate(() => {
|
|
894
|
+
const itemEls = Array.from(document.querySelectorAll("[data-e2e-item]"));
|
|
895
|
+
const items = itemEls.map((el) => el.textContent ?? "");
|
|
896
|
+
const peerCountText = document.querySelector("[data-e2e='peer-count']")?.textContent ?? "0";
|
|
897
|
+
const status = document.querySelector("[data-e2e='status']")?.textContent ?? "";
|
|
898
|
+
return { items, peerCount: Number(peerCountText) || 0, status };
|
|
899
|
+
}).then((data) => ({ peerId: peer.peerId, ...data }));
|
|
900
|
+
}
|
|
901
|
+
async function waitForConvergence(peers, predicate, options = {}) {
|
|
902
|
+
const { timeoutMs = 20000, pollMs = 200 } = options;
|
|
903
|
+
const deadline = Date.now() + timeoutMs;
|
|
904
|
+
let lastSnapshots = [];
|
|
905
|
+
while (Date.now() < deadline) {
|
|
906
|
+
const snapshots = await Promise.all(peers.map(readPeerSnapshot));
|
|
907
|
+
lastSnapshots = snapshots;
|
|
908
|
+
if (snapshots.every(predicate))
|
|
909
|
+
return;
|
|
910
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
911
|
+
}
|
|
912
|
+
const summary = lastSnapshots.map((s) => ` ${s.peerId}: status="${s.status}" peerCount=${s.peerCount} items=${JSON.stringify(s.items)}`).join(`
|
|
913
|
+
`);
|
|
914
|
+
throw new Error(`waitForConvergence: predicate did not hold for every peer within ${timeoutMs}ms.
|
|
915
|
+
${summary}`);
|
|
916
|
+
}
|
|
917
|
+
async function waitForMeshConnected(peers, options = {}) {
|
|
918
|
+
const minPeers = peers.length - 1;
|
|
919
|
+
await waitForConvergence(peers, (snapshot) => snapshot.peerCount >= minPeers, options);
|
|
920
|
+
}
|
|
921
|
+
// tools/test/src/e2e-mesh/with-relay.ts
|
|
922
|
+
import { Elysia as Elysia2 } from "elysia";
|
|
923
|
+
|
|
924
|
+
// src/elysia/signaling-server-plugin.ts
|
|
925
|
+
import { Elysia } from "elysia";
|
|
926
|
+
function signalingServer(options = {}) {
|
|
927
|
+
const path = options.path ?? "/polly/signaling";
|
|
928
|
+
const onCustomFrame = options.onCustomFrame;
|
|
929
|
+
const peerSockets = new Map;
|
|
930
|
+
const parseMessage = (raw) => {
|
|
931
|
+
try {
|
|
932
|
+
return typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
933
|
+
} catch {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
};
|
|
937
|
+
const handleJoin = (ws, peerId) => {
|
|
938
|
+
const newcomer = ws;
|
|
939
|
+
const incumbents = [];
|
|
940
|
+
for (const [existingPeerId, existingSocket] of peerSockets) {
|
|
941
|
+
if (existingPeerId === peerId)
|
|
942
|
+
continue;
|
|
943
|
+
incumbents.push({ peerId: existingPeerId, socket: existingSocket });
|
|
944
|
+
}
|
|
945
|
+
peerSockets.set(peerId, newcomer);
|
|
946
|
+
const wsWithData = ws;
|
|
947
|
+
wsWithData.data.peerId = peerId;
|
|
948
|
+
newcomer.send({
|
|
949
|
+
type: "peers-present",
|
|
950
|
+
peerIds: incumbents.map((i) => i.peerId)
|
|
951
|
+
});
|
|
952
|
+
for (const incumbent of incumbents) {
|
|
953
|
+
try {
|
|
954
|
+
incumbent.socket.send({ type: "peer-joined", peerId });
|
|
955
|
+
} catch {}
|
|
956
|
+
}
|
|
957
|
+
};
|
|
958
|
+
const sendUnknownTarget = (ws, targetPeerId) => {
|
|
959
|
+
ws.send({
|
|
960
|
+
type: "error",
|
|
961
|
+
reason: "unknown-target",
|
|
962
|
+
targetPeerId
|
|
963
|
+
});
|
|
964
|
+
};
|
|
965
|
+
const findOpenTarget = (targetPeerId) => {
|
|
966
|
+
const target = peerSockets.get(targetPeerId);
|
|
967
|
+
if (!target)
|
|
968
|
+
return;
|
|
969
|
+
const readyState = target.readyState;
|
|
970
|
+
const OPEN = 1;
|
|
971
|
+
if (readyState !== undefined && readyState !== OPEN) {
|
|
972
|
+
peerSockets.delete(targetPeerId);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
return target;
|
|
976
|
+
};
|
|
977
|
+
const handleSignal = (ws, msg) => {
|
|
978
|
+
const wsWithData = ws;
|
|
979
|
+
const senderId = wsWithData.data.peerId;
|
|
980
|
+
if (!senderId) {
|
|
981
|
+
wsWithData.send({ type: "error", reason: "not-joined" });
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const target = findOpenTarget(msg.targetPeerId);
|
|
985
|
+
if (!target) {
|
|
986
|
+
sendUnknownTarget(ws, msg.targetPeerId);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const relayed = {
|
|
990
|
+
type: "signal",
|
|
991
|
+
peerId: senderId,
|
|
992
|
+
targetPeerId: msg.targetPeerId,
|
|
993
|
+
payload: msg.payload
|
|
994
|
+
};
|
|
995
|
+
try {
|
|
996
|
+
target.send(relayed);
|
|
997
|
+
} catch {
|
|
998
|
+
peerSockets.delete(msg.targetPeerId);
|
|
999
|
+
sendUnknownTarget(ws, msg.targetPeerId);
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
return new Elysia().ws(path, {
|
|
1003
|
+
message(ws, raw) {
|
|
1004
|
+
const msg = parseMessage(raw);
|
|
1005
|
+
if (!msg) {
|
|
1006
|
+
ws.send({ type: "error", reason: "malformed" });
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (msg.type === "join") {
|
|
1010
|
+
handleJoin(ws, msg.peerId);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (msg.type === "signal") {
|
|
1014
|
+
handleSignal(ws, msg);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (onCustomFrame !== undefined) {
|
|
1018
|
+
const wsWithData = ws;
|
|
1019
|
+
const senderId = wsWithData.data["peerId"];
|
|
1020
|
+
const peerId = typeof senderId === "string" ? senderId : undefined;
|
|
1021
|
+
onCustomFrame(wsWithData, msg, peerId);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
ws.send({ type: "error", reason: "malformed" });
|
|
1025
|
+
},
|
|
1026
|
+
close(ws) {
|
|
1027
|
+
const peerId = ws.data.peerId;
|
|
1028
|
+
if (!peerId) {
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
const mapped = peerSockets.get(peerId);
|
|
1032
|
+
const wsData = ws.data;
|
|
1033
|
+
const mappedData = mapped?.data;
|
|
1034
|
+
if (mapped === undefined || mappedData !== wsData) {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
peerSockets.delete(peerId);
|
|
1038
|
+
for (const [_incumbentId, incumbentSocket] of peerSockets) {
|
|
1039
|
+
try {
|
|
1040
|
+
incumbentSocket.send({ type: "peer-left", peerId });
|
|
1041
|
+
} catch {}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// tools/test/src/e2e-mesh/with-relay.ts
|
|
1048
|
+
function pickPort() {
|
|
1049
|
+
return 30000 + Math.floor(Math.random() * 1e4);
|
|
1050
|
+
}
|
|
1051
|
+
async function withRelay(options = {}) {
|
|
1052
|
+
const mode = options.mode ?? "embedded";
|
|
1053
|
+
const path = options.path ?? "/polly/signaling";
|
|
1054
|
+
if (mode === "env") {
|
|
1055
|
+
const url2 = process.env["SIGNALING_URL"];
|
|
1056
|
+
if (!url2) {
|
|
1057
|
+
throw new Error("withRelay({ mode: 'env' }) requires SIGNALING_URL to be set in the environment.");
|
|
1058
|
+
}
|
|
1059
|
+
return {
|
|
1060
|
+
url: url2,
|
|
1061
|
+
close: async () => {}
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
const port = pickPort();
|
|
1065
|
+
const app = new Elysia2().use(signalingServer({ path })).listen(port);
|
|
1066
|
+
const url = `ws://127.0.0.1:${port}${path}`;
|
|
1067
|
+
return {
|
|
1068
|
+
url,
|
|
1069
|
+
close: async () => {
|
|
1070
|
+
app.server?.stop?.(true);
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
export {
|
|
1075
|
+
withRelay,
|
|
1076
|
+
waitForMeshConnected,
|
|
1077
|
+
waitForConvergence,
|
|
1078
|
+
startDiagnosticRecorder,
|
|
1079
|
+
serveConsumer,
|
|
1080
|
+
prebakeKeyringSet,
|
|
1081
|
+
prebakeKeyringPair,
|
|
1082
|
+
launchPeer,
|
|
1083
|
+
knownPeersFor,
|
|
1084
|
+
isAllowedConsoleLine,
|
|
1085
|
+
MeshAssertionError,
|
|
1086
|
+
MESH_CONSOLE_ALLOWLIST
|
|
1087
|
+
};
|
|
1088
|
+
|
|
1089
|
+
//# debugId=7F448F738B995B3064756E2164756E21
|