1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239 | /**
* The public API contract, checked against the committed dist/.
*
* rooms-kit's only consumer is the bot's room worker (`dist/worker.js`), which
* value-imports `ChatClient` from "./chat", `BoardClient` from "./board" and
* `Identity` from the root — and nothing else. That import list is asserted here
* literally, because a rename anywhere in it breaks a shipped process.
*
* The class shapes matter as much as the names: `shadow.cljs.modern/defclass`
* has no syntax for statics or getters (dev/docs/CLJS.md), so `ChatClient.join`,
* `Identity.fromSeed` and every `get` accessor are attached by hand after the
* class form — exactly the constructs that silently go missing.
*/
import { test } from "node:test";
import assert from "node:assert/strict";
import { EventEmitter } from "node:events";
import { load, TS_MODE } from "./helpers/load.mjs";
const names = (m) => Object.keys(m).filter((k) => k !== "default" && k !== "__esModule").sort();
test('the "." export surface', async () => {
const root = await load("index.js");
assert.deepEqual(names(root), [
"BOARD_SALT",
"BoardClient",
"CHAT_SALT",
"ChatClient",
"Identity",
"installBrowserGlobals",
"installOrigin",
"newRoomSecret",
]);
assert.equal(root.CHAT_SALT, "chat.ardegazu.ro/v2");
assert.equal(root.BOARD_SALT, "board.ardegazu.ro/v1");
assert.equal(typeof root.newRoomSecret, "function");
assert.equal(typeof root.installOrigin, "function");
assert.equal(typeof root.installBrowserGlobals, "function");
});
test('the "./chat" and "./board" export surfaces', async () => {
const chat = await load("chat/index.js");
const board = await load("board/index.js");
assert.deepEqual(names(chat), ["CHAT_SALT", "ChatClient", "newRoomSecret"]);
assert.deepEqual(names(board), ["BOARD_SALT", "BoardClient", "newRoomSecret"]);
const root = await load("index.js");
// the same objects, not copies
assert.equal(chat.ChatClient, root.ChatClient);
assert.equal(board.BoardClient, root.BoardClient);
assert.equal(chat.newRoomSecret, board.newRoomSecret);
});
test("ChatClient and BoardClient are EventEmitters with static async join()", async () => {
const { ChatClient } = await load("chat/index.js");
const { BoardClient } = await load("board/index.js");
for (const C of [ChatClient, BoardClient]) {
assert.equal(typeof C, "function");
assert.equal(typeof C.join, "function", `${C.name}.join`);
assert.equal(Object.create(C.prototype) instanceof EventEmitter, true, `${C.name} extends EventEmitter`);
for (const m of ["on", "once", "off", "emit", "removeListener"]) {
assert.equal(typeof C.prototype[m], "function", `${C.name}#${m}`);
}
assert.equal(typeof C.prototype.close, "function");
}
for (const m of ["send", "react", "setName", "history", "peers"]) {
assert.equal(typeof ChatClient.prototype[m], "function", `ChatClient#${m}`);
}
for (const m of ["elements", "drawStroke", "addText", "erase"]) {
assert.equal(typeof BoardClient.prototype[m], "function", `BoardClient#${m}`);
}
// myAuthorId is a getter, not an own data property
const d = Object.getOwnPropertyDescriptor(ChatClient.prototype, "myAuthorId");
assert.equal(typeof d?.get, "function", "ChatClient#myAuthorId must be a getter");
});
test("Identity is id-kit's own class, compiled here, wire-compatible with id-kit's dist", async () => {
const root = await load("index.js");
assert.equal(typeof root.Identity, "function");
assert.equal(typeof root.Identity.fromSeed, "function");
assert.equal(typeof root.Identity.newSeed, "function");
const seed = root.Identity.newSeed();
assert.match(seed, /^[A-Za-z0-9_-]{43}$/);
const identity = await root.Identity.fromSeed(seed);
assert.match(identity.publicKeyB64, /^[A-Za-z0-9_-]{43}$/);
assert.equal(typeof identity.fingerprint.emoji, "string");
assert.match(identity.fingerprint.hex, /^[0-9a-f]{64}$/);
assert.equal(typeof identity.assert, "function");
assert.equal(typeof identity.signRaw, "function");
assert.equal((await identity.signRaw(new Uint8Array([1]))).length, 64);
await assert.rejects(() => root.Identity.fromSeed("too-short"));
// THE vendor-retirement invariant, restated for the pure-CLJS world.
//
// v1.0.0 shipped a hand-maintained TypeScript COPY of id-kit under
// src/vendor/id/ — a second IMPLEMENTATION, free to drift. The port replaced
// it with a dist re-export and asserted `rooms.Identity === idKit.Identity`,
// object identity standing in for "not a fork". That proxy expired: id-kit is
// now compiled from its sha-pinned SOURCES into this build (deps.edn), so the
// class object is ours and the equality is false by construction — while the
// implementation is, byte for byte, id-kit's own.
//
// So assert the property the object identity was ever a proxy FOR, and assert
// it the way the network does. Nothing in the suite runs `instanceof` against
// an Identity; what crosses between implementations is the seed and the
// signature bytes. Same seed must give the same public key, and a signature
// from OUR Identity must verify under the INSTALLED id-kit dist's verifier —
// that is what makes a bot's room entries readable by everyone else.
if (!TS_MODE) {
const idKit = await import("ardegazu-id-kit");
const theirs = await idKit.Identity.fromSeed(seed);
assert.equal(
identity.publicKeyB64,
theirs.publicKeyB64,
"same seed must derive the same identity in both compiles",
);
assert.equal(identity.fingerprint.hex, theirs.fingerprint.hex);
assert.equal(identity.fingerprint.emoji, theirs.fingerprint.emoji);
const data = new TextEncoder().encode("cross-compile signature check");
const sig = await identity.signRaw(data);
const b64url = Buffer.from(sig).toString("base64url");
assert.equal(
await idKit.verifyRaw(identity.publicKeyB64, b64url, data),
true,
"id-kit's dist must verify a signature made by our compiled Identity",
);
// and the session binding assertion, the one signature peers actually check
const asserted = await identity.assert("chat.ardegazu.ro/v2", "room-id", "peer-id");
assert.deepEqual(
await idKit.verifyAssertion(
"chat.ardegazu.ro/v2",
"room-id",
"peer-id",
identity.publicKeyB64,
asserted,
),
{ emoji: identity.fingerprint.emoji, hex: identity.fingerprint.hex },
"id-kit's dist must accept our binding assertion and return the fingerprint",
);
}
});
test("the bot worker's exact import list resolves through the exports map", async () => {
// Self-reference by package name — this is what `dist/worker.js` does, so it
// exercises package.json "exports" and not just the file layout.
const { ChatClient } = await import("ardegazu-rooms-kit/chat");
const { BoardClient } = await import("ardegazu-rooms-kit/board");
const { Identity } = await import("ardegazu-rooms-kit");
assert.equal(typeof ChatClient.join, "function");
assert.equal(typeof BoardClient.join, "function");
assert.equal(typeof Identity.fromSeed, "function");
// and the "./src/*" escape hatch is still published (its targets are source
// files, not importable modules — the map entry is the contract)
const { readFileSync } = await import("node:fs");
const { join, dirname } = await import("node:path");
const { fileURLToPath } = await import("node:url");
const pkg = JSON.parse(
readFileSync(join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), "utf8"),
);
assert.deepEqual(Object.keys(pkg.exports).sort(), [".", "./board", "./chat", "./internals", "./src/*"]);
assert.equal(pkg.exports["./src/*"], "./src/*");
assert.deepEqual(pkg.files.slice().sort(), ["dist", "src", "types"]);
});
test('the "./internals" subpath publishes the whole node room stack', async () => {
// A Node participant in a rooms app that is neither chat nor board (the
// banker bot's banca arm is the first) boots the stack itself — session key,
// room crypto, net, fs helia, room log — but must NOT compile lib/net &
// friends off the classpath: their bare npm imports would resolve against
// the CONSUMER's root node_modules, where a peer-kit install wins the libp2p
// hoist. Importing this subpath keeps every bare import inside this
// package's own nested tree. Raw surface, deliberately: it is dist/
// internals.js as the build emits it.
const m = await import("ardegazu-rooms-kit/internals");
for (const name of ["Net", "newSessionKey", "RoomCrypto", "RoomLog", "openHelia",
"resolveRelayConfig", "IceConfig", "HardenedIPFSAccessController",
"makeLogEncryption", "registerSuetaProvider"]) {
assert.notEqual(m[name], undefined, `internals is missing ${name}`);
}
assert.equal(typeof m.RoomCrypto.create, "function");
assert.equal(typeof m.RoomLog.open, "function");
});
test("the internal classes keep their static factories and getters", async () => {
const { RoomCrypto, Sealer, AtRestCipher } = await load("lib/crypto.js");
assert.equal(typeof RoomCrypto.create, "function");
for (const m of ["dbName", "mailboxRoomId", "sealSignal", "openSignal", "sealMsg", "openMsg", "sealMsgBinary",
"openMsgBinary", "logEntryCipher", "logPayloadCipher", "imgCipher", "mbxCipher"]) {
assert.equal(typeof RoomCrypto.prototype[m], "function", `RoomCrypto#${m}`);
}
assert.equal(typeof Sealer.prototype.seal, "function");
assert.equal(typeof Sealer.prototype.sealBinary, "function");
assert.equal(typeof AtRestCipher.prototype.seal, "function");
assert.equal(typeof AtRestCipher.prototype.open, "function");
// roomId / peerId / roomTopic are readonly instance fields, not getters
const rc = await RoomCrypto.create("A".repeat(43), "x", "p");
for (const f of ["roomId", "peerId", "roomTopic"]) {
assert.equal(Object.prototype.hasOwnProperty.call(rc, f), true, `RoomCrypto.${f} own property`);
}
const { RoomLog, openHelia } = await load("lib/log.js");
assert.equal(typeof RoomLog.open, "function");
assert.equal(typeof openHelia, "function");
for (const g of ["address", "myAuthorId"]) {
assert.equal(typeof Object.getOwnPropertyDescriptor(RoomLog.prototype, g)?.get, "function", `RoomLog#${g} getter`);
}
for (const m of ["append", "loadTail", "putImage", "getImage", "pruneImages", "sealedEntryBytes",
"myIdentityBlock", "rawBlock", "imageDagBlocks", "decodeSealedEntry", "putEntryBlock", "putRawBlock",
"putIdentityBlock", "hasEntry", "ingestEntry", "entryMeta", "pinImageIfLocal", "emitUnseen", "close", "drop"]) {
assert.equal(typeof RoomLog.prototype[m], "function", `RoomLog#${m}`);
}
const { Net, newSessionKey } = await load("lib/net.js");
assert.equal(typeof Net.create, "function");
assert.equal(typeof newSessionKey, "function");
for (const g of ["myId", "relayUp"]) {
assert.equal(typeof Object.getOwnPropertyDescriptor(Net.prototype, g)?.get, "function", `Net#${g} getter`);
}
for (const m of ["handleFrames", "sendFrame", "sendTo", "sendBinaryTo", "broadcast", "broadcastBinary",
"openPeers", "debugState", "close"]) {
assert.equal(typeof Net.prototype[m], "function", `Net#${m}`);
}
const { IceConfig } = await load("lib/turn.js");
assert.equal(typeof IceConfig.prototype.get, "function");
const { SuetaIdentityProvider } = await load("lib/orbit-identity.js");
assert.equal(SuetaIdentityProvider.type, "sueta");
assert.equal(typeof SuetaIdentityProvider.verifyIdentity, "function");
});
test("newSessionKey mints a fresh libp2p Ed25519 identity", async () => {
const { newSessionKey } = await load("lib/net.js");
const a = await newSessionKey();
const b = await newSessionKey();
assert.match(a.peerId, /^12D3Koo/);
assert.notEqual(a.peerId, b.peerId);
assert.equal(a.privateKey.type, "Ed25519");
});
|