@claudecollab/cli 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/LICENSE +21 -0
- package/README.md +109 -0
- package/package.json +36 -0
- package/packages/cli/bin/claude-share.js +1319 -0
- package/packages/cli/package.json +15 -0
- package/packages/cli/src/brain/card.js +157 -0
- package/packages/cli/src/brain/commands.js +122 -0
- package/packages/cli/src/brain/drafts.js +557 -0
- package/packages/cli/src/brain/gate.js +134 -0
- package/packages/cli/src/brain/knocks.js +27 -0
- package/packages/cli/src/brain/log.js +123 -0
- package/packages/cli/src/brain/queue.js +81 -0
- package/packages/cli/src/brain/state.js +245 -0
- package/packages/cli/src/hooks.js +243 -0
- package/packages/cli/src/invite.js +43 -0
- package/packages/cli/src/known-relays.js +66 -0
- package/packages/cli/src/pty.js +175 -0
- package/packages/cli/src/relay-client.js +271 -0
- package/packages/cli/src/renderer.js +230 -0
- package/packages/cli/src/screen-snapshot.js +139 -0
- package/packages/cli/src/term-chatter.js +89 -0
- package/packages/relay/auth.js +18 -0
- package/packages/relay/bin/serve.js +73 -0
- package/packages/relay/names.js +27 -0
- package/packages/relay/package.json +13 -0
- package/packages/relay/public/client.js +1553 -0
- package/packages/relay/public/index.html +110 -0
- package/packages/relay/public/style.css +952 -0
- package/packages/relay/rooms.js +149 -0
- package/packages/relay/server.js +652 -0
- package/packages/relay/web.js +334 -0
- package/packages/relay/wordlists.js +38 -0
- package/packages/shared/package.json +8 -0
- package/packages/shared/protocol.js +189 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Relay room registry — pure state, no ssh. Hands out unique room codes, tracks
|
|
2
|
+
// guests/bans/cap per room, runs the 10-minute host-drop TTL, and rate-limits
|
|
3
|
+
// knock attempts per ip. The ssh server (Task 2) wires this to real connections.
|
|
4
|
+
|
|
5
|
+
import { adjectives as defaultAdjectives, animals as defaultAnimals } from './wordlists.js';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_TTL_MS = 10 * 60 * 1000; // spec: code survives a 10-min host drop
|
|
8
|
+
const DEFAULT_CAP = 8; // spec: room cap, composer stays legible
|
|
9
|
+
const DEFAULT_KNOCK_LIMIT = 5; // spec: per-room lockout — 5 knock attempts/min per ip
|
|
10
|
+
const DEFAULT_KNOCK_WINDOW_MS = 60 * 1000;
|
|
11
|
+
|
|
12
|
+
const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create an isolated room registry. All timing is injectable so tests stay
|
|
16
|
+
* deterministic: `now` for knock windows, node:test fake timers for the TTL.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} [opts]
|
|
19
|
+
* @param {number} [opts.ttlMs] host-drop grace period before close
|
|
20
|
+
* @param {number} [opts.cap] max guests per room
|
|
21
|
+
* @param {number} [opts.knockLimit] knock attempts allowed per window per ip
|
|
22
|
+
* @param {number} [opts.knockWindowMs] sliding window for the knock limit
|
|
23
|
+
* @param {() => number} [opts.now] millisecond clock (defaults to Date.now)
|
|
24
|
+
* @param {{adjectives:string[], animals:string[]}} [opts.wordlists]
|
|
25
|
+
*/
|
|
26
|
+
export function createRegistry(opts = {}) {
|
|
27
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
28
|
+
const cap = opts.cap ?? DEFAULT_CAP;
|
|
29
|
+
const knockLimit = opts.knockLimit ?? DEFAULT_KNOCK_LIMIT;
|
|
30
|
+
const knockWindowMs = opts.knockWindowMs ?? DEFAULT_KNOCK_WINDOW_MS;
|
|
31
|
+
const now = opts.now ?? (() => Date.now());
|
|
32
|
+
const adjectives = opts.wordlists?.adjectives ?? defaultAdjectives;
|
|
33
|
+
const animals = opts.wordlists?.animals ?? defaultAnimals;
|
|
34
|
+
|
|
35
|
+
/** @type {Map<string, object>} code -> room */
|
|
36
|
+
const rooms = new Map();
|
|
37
|
+
|
|
38
|
+
function makeCode() {
|
|
39
|
+
for (let i = 0; i < 5; i++) {
|
|
40
|
+
const code = `${pick(adjectives)}-${pick(animals)}`;
|
|
41
|
+
if (!rooms.has(code)) return code;
|
|
42
|
+
}
|
|
43
|
+
// 5 collisions: widen to a third word (spec fallback).
|
|
44
|
+
for (let i = 0; i < 50; i++) {
|
|
45
|
+
const code = `${pick(adjectives)}-${pick(animals)}-${pick(adjectives)}`;
|
|
46
|
+
if (!rooms.has(code)) return code;
|
|
47
|
+
}
|
|
48
|
+
// Pathologically tiny wordlists: guarantee uniqueness with a numeric suffix.
|
|
49
|
+
const base = `${pick(adjectives)}-${pick(animals)}-${pick(adjectives)}`;
|
|
50
|
+
let n = 2;
|
|
51
|
+
while (rooms.has(`${base}-${n}`)) n++;
|
|
52
|
+
return `${base}-${n}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function create() {
|
|
56
|
+
const code = makeCode();
|
|
57
|
+
const room = {
|
|
58
|
+
code,
|
|
59
|
+
guests: new Map(), // id -> {name, fp, role}
|
|
60
|
+
banned: new Set(), // banned fingerprints
|
|
61
|
+
cap,
|
|
62
|
+
hostPresent: true,
|
|
63
|
+
createdAt: now(),
|
|
64
|
+
_ttlTimer: null, // pending host-drop close timer
|
|
65
|
+
_knocks: new Map(), // ip -> number[] (recent knock timestamps)
|
|
66
|
+
};
|
|
67
|
+
rooms.set(code, room);
|
|
68
|
+
return room;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function get(code) {
|
|
72
|
+
return rooms.get(code);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function addGuest(code, id, { name, fp, role } = {}) {
|
|
76
|
+
const room = rooms.get(code);
|
|
77
|
+
if (!room) return false;
|
|
78
|
+
if (fp && room.banned.has(fp)) return false;
|
|
79
|
+
if (room.guests.size >= room.cap) return false;
|
|
80
|
+
room.guests.set(id, { name, fp, role: role ?? 'prompter' });
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function removeGuest(code, id) {
|
|
85
|
+
const room = rooms.get(code);
|
|
86
|
+
if (!room) return false;
|
|
87
|
+
return room.guests.delete(id);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Blocklist a fingerprint for the room's lifetime and evict any live guests
|
|
91
|
+
// holding it (spec §host controls: /kick drops + blocklists the pubkey).
|
|
92
|
+
function ban(code, fp) {
|
|
93
|
+
const room = rooms.get(code);
|
|
94
|
+
if (!room) return;
|
|
95
|
+
room.banned.add(fp);
|
|
96
|
+
for (const [id, g] of room.guests) {
|
|
97
|
+
if (g.fp === fp) room.guests.delete(id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Host connection dropped: start the grace timer that closes the room.
|
|
102
|
+
function hostDropped(code) {
|
|
103
|
+
const room = rooms.get(code);
|
|
104
|
+
if (!room) return;
|
|
105
|
+
room.hostPresent = false;
|
|
106
|
+
if (room._ttlTimer) clearTimeout(room._ttlTimer);
|
|
107
|
+
room._ttlTimer = setTimeout(() => close(code), ttlMs);
|
|
108
|
+
if (typeof room._ttlTimer?.unref === 'function') room._ttlTimer.unref();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Host reconnected in time: cancel the pending close.
|
|
112
|
+
function hostReturned(code) {
|
|
113
|
+
const room = rooms.get(code);
|
|
114
|
+
if (!room) return;
|
|
115
|
+
room.hostPresent = true;
|
|
116
|
+
if (room._ttlTimer) {
|
|
117
|
+
clearTimeout(room._ttlTimer);
|
|
118
|
+
room._ttlTimer = null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function close(code) {
|
|
123
|
+
const room = rooms.get(code);
|
|
124
|
+
if (!room) return;
|
|
125
|
+
if (room._ttlTimer) {
|
|
126
|
+
clearTimeout(room._ttlTimer);
|
|
127
|
+
room._ttlTimer = null;
|
|
128
|
+
}
|
|
129
|
+
rooms.delete(code);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Record + rate-check a knock attempt. Returns false if the room is gone or
|
|
133
|
+
// this ip has used up its allowance inside the sliding window.
|
|
134
|
+
function tryKnock(code, ip) {
|
|
135
|
+
const room = rooms.get(code);
|
|
136
|
+
if (!room) return false;
|
|
137
|
+
const t = now();
|
|
138
|
+
const recent = (room._knocks.get(ip) ?? []).filter((ts) => t - ts < knockWindowMs);
|
|
139
|
+
if (recent.length >= knockLimit) {
|
|
140
|
+
room._knocks.set(ip, recent);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
recent.push(t);
|
|
144
|
+
room._knocks.set(ip, recent);
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { create, get, addGuest, removeGuest, ban, hostDropped, hostReturned, close, tryKnock };
|
|
149
|
+
}
|