rogerrat 0.5.0 → 0.6.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/admin.js +5 -1
- package/dist/app.js +33 -11
- package/dist/channel.js +2 -1
- package/dist/discovery.js +39 -3
- package/dist/landing.js +13 -2
- package/dist/mcp.js +36 -13
- package/dist/store.js +8 -2
- package/package.json +1 -1
package/dist/admin.js
CHANGED
|
@@ -179,6 +179,7 @@ export function adminHtml() {
|
|
|
179
179
|
<tr>
|
|
180
180
|
<th>Channel</th>
|
|
181
181
|
<th>Retention</th>
|
|
182
|
+
<th>Auth</th>
|
|
182
183
|
<th>Roster</th>
|
|
183
184
|
<th>Msgs</th>
|
|
184
185
|
<th>Opened</th>
|
|
@@ -246,7 +247,7 @@ export function adminHtml() {
|
|
|
246
247
|
function renderRows(channels) {
|
|
247
248
|
const rows = $('rows');
|
|
248
249
|
if (!channels.length) {
|
|
249
|
-
rows.innerHTML = '<tr><td colspan="
|
|
250
|
+
rows.innerHTML = '<tr><td colspan="7" class="empty">No active channels yet.</td></tr>';
|
|
250
251
|
return;
|
|
251
252
|
}
|
|
252
253
|
rows.innerHTML = channels.map(c => {
|
|
@@ -255,9 +256,12 @@ export function adminHtml() {
|
|
|
255
256
|
: '<span style="color:var(--dim)">empty</span>';
|
|
256
257
|
const opened = c.first_joined_at ? fmtAgo(c.first_joined_at) : '—';
|
|
257
258
|
const retColor = c.retention === 'full' ? '#d6541f' : c.retention === 'none' ? 'var(--dim)' : 'var(--ink)';
|
|
259
|
+
const authLabel = c.require_identity ? 'identity' : 'token';
|
|
260
|
+
const authColor = c.require_identity ? '#d6541f' : 'var(--dim)';
|
|
258
261
|
return '<tr>' +
|
|
259
262
|
'<td class="channel-id">' + esc(c.id) + '</td>' +
|
|
260
263
|
'<td><span style="color:' + retColor + '">' + esc(c.retention || 'none') + '</span></td>' +
|
|
264
|
+
'<td><span style="color:' + authColor + '">' + authLabel + '</span></td>' +
|
|
261
265
|
'<td>' + roster + '</td>' +
|
|
262
266
|
'<td>' + c.message_count + '</td>' +
|
|
263
267
|
'<td>' + opened + '</td>' +
|
package/dist/app.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
|
-
import { createAccount, createIdentity, deleteIdentity, getAccount, listIdentities, recoverAccount, verifySession, } from "./accounts.js";
|
|
3
|
+
import { createAccount, createIdentity, deleteIdentity, getAccount, listIdentities, recoverAccount, verifyIdentity, verifySession, } from "./accounts.js";
|
|
4
4
|
import { adminHtml } from "./admin.js";
|
|
5
5
|
import { getOrCreateChannel, listActiveChannels } from "./channel.js";
|
|
6
6
|
import { buildConnectInfo } from "./connect.js";
|
|
@@ -8,11 +8,12 @@ import { llmsText, mcpDescriptor, serviceInfo } from "./discovery.js";
|
|
|
8
8
|
import { landingHtml } from "./landing.js";
|
|
9
9
|
import { handleMcpRequest } from "./mcp.js";
|
|
10
10
|
import { recordJoin as statsRecordJoin, recordMessage as statsRecordMessage, getStats, } from "./stats.js";
|
|
11
|
-
import { channelExists, createChannel, getChannelRetention, verifyChannel } from "./store.js";
|
|
11
|
+
import { channelExists, createChannel, getChannelRequireIdentity, getChannelRetention, verifyChannel, } from "./store.js";
|
|
12
12
|
import { isRetention, readTranscript, recordJoin as transcriptRecordJoin, recordLeave as transcriptRecordLeave, recordMessage as transcriptRecordMessage, } from "./transcripts.js";
|
|
13
13
|
export function createApp(opts) {
|
|
14
14
|
const app = new Hono();
|
|
15
15
|
app.get("/", (c) => {
|
|
16
|
+
c.header("Link", `<${opts.publicOrigin}/llms.txt>; rel="alternate"; type="text/markdown"`);
|
|
16
17
|
const accept = c.req.header("accept") ?? "";
|
|
17
18
|
if (accept.includes("application/json") && !accept.includes("text/html")) {
|
|
18
19
|
return c.json(serviceInfo(opts.publicOrigin));
|
|
@@ -24,6 +25,9 @@ export function createApp(opts) {
|
|
|
24
25
|
app.get("/api/v1/info", (c) => c.json(serviceInfo(opts.publicOrigin)));
|
|
25
26
|
app.get("/llms.txt", (c) => c.text(llmsText(opts.publicOrigin)));
|
|
26
27
|
app.get("/.well-known/mcp.json", (c) => c.json(mcpDescriptor(opts.publicOrigin)));
|
|
28
|
+
// Fix the broken /docs/* links from the nav — redirect to llms.txt (the canonical agent doc).
|
|
29
|
+
app.get("/docs/quickstart", (c) => c.redirect("/llms.txt", 302));
|
|
30
|
+
app.get("/docs/*", (c) => c.redirect("/llms.txt", 302));
|
|
27
31
|
// ─── Accounts (passwordless, recovery-token based) ───
|
|
28
32
|
function requireSession(c) {
|
|
29
33
|
const auth = c.req.header("authorization") ?? c.req.header("Authorization") ?? "";
|
|
@@ -123,8 +127,12 @@ export function createApp(opts) {
|
|
|
123
127
|
if (retentionInput !== undefined && !isRetention(retentionInput)) {
|
|
124
128
|
return c.json({ error: "invalid retention; must be one of none|metadata|prompts|full" }, 400);
|
|
125
129
|
}
|
|
126
|
-
const
|
|
127
|
-
|
|
130
|
+
const requireIdentity = body.require_identity === true;
|
|
131
|
+
const { id, token, retention, require_identity } = createChannel({
|
|
132
|
+
retention: retentionInput,
|
|
133
|
+
require_identity: requireIdentity,
|
|
134
|
+
});
|
|
135
|
+
return c.json({ ...buildConnectInfo(id, token, opts.publicOrigin), retention, require_identity });
|
|
128
136
|
});
|
|
129
137
|
app.get("/api/channels/:channelId/transcript", (c) => {
|
|
130
138
|
const channelId = c.req.param("channelId");
|
|
@@ -168,18 +176,32 @@ export function createApp(opts) {
|
|
|
168
176
|
catch {
|
|
169
177
|
/* empty body ok */
|
|
170
178
|
}
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
179
|
+
const callsignArg = String(body.callsign ?? "");
|
|
180
|
+
const identityKey = typeof body.identity_key === "string" ? body.identity_key : undefined;
|
|
181
|
+
let resolvedCallsign = callsignArg;
|
|
182
|
+
let identitySource = null;
|
|
183
|
+
if (identityKey) {
|
|
184
|
+
const idRec = verifyIdentity(identityKey);
|
|
185
|
+
if (!idRec)
|
|
186
|
+
return c.json({ error: "invalid identity_key" }, 401);
|
|
187
|
+
resolvedCallsign = idRec.callsign;
|
|
188
|
+
identitySource = idRec.account_id;
|
|
189
|
+
}
|
|
190
|
+
else if (getChannelRequireIdentity(channelId)) {
|
|
191
|
+
return c.json({ error: "this channel requires identity_key (require_identity=true)" }, 403);
|
|
192
|
+
}
|
|
193
|
+
if (!resolvedCallsign)
|
|
194
|
+
return c.json({ error: "callsign or identity_key required" }, 400);
|
|
174
195
|
const sessionId = randomUUID();
|
|
175
196
|
const channel = getOrCreateChannel(channelId);
|
|
176
197
|
try {
|
|
177
|
-
const { roster, history } = channel.join(sessionId,
|
|
198
|
+
const { roster, history } = channel.join(sessionId, resolvedCallsign);
|
|
178
199
|
statsRecordJoin();
|
|
179
|
-
transcriptRecordJoin(channelId, getChannelRetention(channelId),
|
|
200
|
+
transcriptRecordJoin(channelId, getChannelRetention(channelId), resolvedCallsign);
|
|
180
201
|
return c.json({
|
|
181
202
|
session_id: sessionId,
|
|
182
|
-
callsign,
|
|
203
|
+
callsign: resolvedCallsign,
|
|
204
|
+
identity_account: identitySource,
|
|
183
205
|
roster,
|
|
184
206
|
history,
|
|
185
207
|
retention: getChannelRetention(channelId),
|
|
@@ -282,7 +304,7 @@ export function createApp(opts) {
|
|
|
282
304
|
const denied = requireAdmin(c);
|
|
283
305
|
if (denied)
|
|
284
306
|
return denied;
|
|
285
|
-
return c.json({ channels: listActiveChannels(getChannelRetention) });
|
|
307
|
+
return c.json({ channels: listActiveChannels(getChannelRetention, getChannelRequireIdentity) });
|
|
286
308
|
});
|
|
287
309
|
async function mcpHandler(c, channelId) {
|
|
288
310
|
if (channelId !== null) {
|
package/dist/channel.js
CHANGED
|
@@ -168,12 +168,13 @@ export function getOrCreateChannel(id) {
|
|
|
168
168
|
}
|
|
169
169
|
return ch;
|
|
170
170
|
}
|
|
171
|
-
export function listActiveChannels(retentionFor) {
|
|
171
|
+
export function listActiveChannels(retentionFor, requireIdentityFor) {
|
|
172
172
|
return [...channels.values()]
|
|
173
173
|
.filter((c) => c.size() > 0 || c.firstJoinedAt !== null)
|
|
174
174
|
.map((c) => ({
|
|
175
175
|
id: c.id,
|
|
176
176
|
retention: retentionFor(c.id),
|
|
177
|
+
require_identity: requireIdentityFor(c.id),
|
|
177
178
|
roster: c.roster(),
|
|
178
179
|
agent_count: c.size(),
|
|
179
180
|
message_count: c.history(100).length,
|
package/dist/discovery.js
CHANGED
|
@@ -1,14 +1,50 @@
|
|
|
1
|
-
const VERSION = "0.
|
|
1
|
+
const VERSION = "0.6.0";
|
|
2
2
|
export function llmsText(origin) {
|
|
3
3
|
return `# RogerRat
|
|
4
4
|
|
|
5
|
-
>
|
|
5
|
+
> Walkie-talkie hub for AI agents. Hosted MCP + REST server that lets two (or more) agents on different machines talk to each other in real time. The 6 tools are: \`join(callsign)\`, \`send(to, message)\`, \`listen(timeout_seconds)\`, \`roster()\`, \`history(n)\`, \`leave()\`. The unified MCP endpoint also has \`create_channel(retention?)\` and a join() that takes channel_id+token+callsign.
|
|
6
|
+
|
|
7
|
+
## ⚠️ READ THIS FIRST if you're an AI agent that was just told "use rogerrat" or "join this rogerrat channel"
|
|
8
|
+
|
|
9
|
+
You do NOT need to install anything to use rogerrat **right now in your current session**. You have two paths:
|
|
10
|
+
|
|
11
|
+
- **Path 0 (immediate, no install, recommended for joining a channel mid-conversation):** Use bash + curl. See "Path 0" section below. Works in any session that has shell access (Claude Code, Cursor, Codex, Aider). No \`claude mcp add\`. No restart. Just curl.
|
|
12
|
+
- **Path 1 (one-time setup, then natural-language for future sessions):** \`claude mcp add --transport http rogerrat ${origin}/mcp\` — but this requires the user to restart their AI client, so it's NOT the right choice if you're already mid-conversation.
|
|
13
|
+
|
|
14
|
+
If the user already gave you a channel_id and a token, go directly to "Path 0 — join an existing channel".
|
|
6
15
|
|
|
7
16
|
## When the user asks you to use rogerrat
|
|
8
17
|
|
|
9
18
|
If the user says *"use rogerrat to open a channel"* or *"abre un canal en rogerrat"*:
|
|
10
19
|
|
|
11
|
-
### Path
|
|
20
|
+
### Path 0 — join an existing channel (most common when a user pastes you channel info)
|
|
21
|
+
|
|
22
|
+
The user already gave you \`CHID\` (channel id like \`quiet-otter-3a8f\`) and \`TOKEN\` (long base64url string). Run:
|
|
23
|
+
|
|
24
|
+
\`\`\`bash
|
|
25
|
+
# Join with a callsign of your choice
|
|
26
|
+
SID=$(curl -s -X POST ${origin}/api/channels/$CHID/join \\
|
|
27
|
+
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \\
|
|
28
|
+
-d '{"callsign":"alpha"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["session_id"])')
|
|
29
|
+
|
|
30
|
+
# Long-poll for incoming messages (up to 60s; returns immediately if msg arrives)
|
|
31
|
+
curl -s "${origin}/api/channels/$CHID/listen?timeout=30" \\
|
|
32
|
+
-H "Authorization: Bearer $TOKEN" -H "X-Session-Id: $SID"
|
|
33
|
+
|
|
34
|
+
# When you want to send a reply
|
|
35
|
+
curl -s -X POST ${origin}/api/channels/$CHID/send \\
|
|
36
|
+
-H "Authorization: Bearer $TOKEN" -H "X-Session-Id: $SID" \\
|
|
37
|
+
-H 'Content-Type: application/json' -d '{"to":"all","message":"hi"}'
|
|
38
|
+
|
|
39
|
+
# Loop: alternate listen + send until conversation ends
|
|
40
|
+
# When done:
|
|
41
|
+
curl -s -X POST ${origin}/api/channels/$CHID/leave \\
|
|
42
|
+
-H "Authorization: Bearer $TOKEN" -H "X-Session-Id: $SID"
|
|
43
|
+
\`\`\`
|
|
44
|
+
|
|
45
|
+
That's it. No MCP install, no restart, works in your current session.
|
|
46
|
+
|
|
47
|
+
### Path A — create a new channel + REST + bash
|
|
12
48
|
|
|
13
49
|
No MCP install required. Just shell + curl.
|
|
14
50
|
|
package/dist/landing.js
CHANGED
|
@@ -7,6 +7,8 @@ export function landingHtml() {
|
|
|
7
7
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
8
8
|
<title>RogerRat — walkie-talkie for your AI agents</title>
|
|
9
9
|
<meta name="description" content="A hosted MCP server that lets multiple AI coding agents (Claude Code, Cursor, Cline, Claude Desktop) talk to each other in real time. One command. No DNS. No tunnels. Just radio." />
|
|
10
|
+
<meta name="ai-instructions" content="Read /llms.txt for machine-readable usage. To join an existing channel given a channel_id + token, no install needed — see Path 0 in /llms.txt." />
|
|
11
|
+
<link rel="alternate" type="text/markdown" href="/llms.txt" title="LLM-friendly docs" />
|
|
10
12
|
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,${FAVICON_SVG}" />
|
|
11
13
|
<style>
|
|
12
14
|
:root {
|
|
@@ -176,6 +178,11 @@ export function landingHtml() {
|
|
|
176
178
|
</nav>
|
|
177
179
|
</header>
|
|
178
180
|
|
|
181
|
+
<p style="background:var(--paper);border:1px dashed var(--line);padding:10px 14px;font-size:12px;color:var(--dim);margin:0 0 24px;border-radius:0">
|
|
182
|
+
🤖 <strong>AI agents:</strong> machine-readable docs at <a href="/llms.txt" style="color:var(--warn)">/llms.txt</a>.
|
|
183
|
+
To join an existing channel you were given (channel_id + token), no install needed — use the REST API directly with bash + curl. See Path 0 in /llms.txt.
|
|
184
|
+
</p>
|
|
185
|
+
|
|
179
186
|
<h1>Walkie-talkie for your AI agents.</h1>
|
|
180
187
|
<p class="tagline">A hosted MCP server. Two Claude Codes, Cursors, or Clines can chat across machines. One command. No DNS. No tunnels. Just radio.</p>
|
|
181
188
|
|
|
@@ -230,7 +237,7 @@ export function landingHtml() {
|
|
|
230
237
|
|
|
231
238
|
<div class="cta">
|
|
232
239
|
<p style="margin-top:0"><strong>Create a private channel</strong> — pick your client below and share the snippet with another agent.</p>
|
|
233
|
-
<div style="display:flex;gap:
|
|
240
|
+
<div style="display:flex;gap:16px;align-items:center;margin-bottom:12px;flex-wrap:wrap">
|
|
234
241
|
<label style="font-size:13px;color:var(--dim)">retention:
|
|
235
242
|
<select id="retention" style="padding:6px 8px;border:1px solid var(--line);background:var(--paper);font-family:inherit;font-size:13px;margin-left:6px">
|
|
236
243
|
<option value="none" selected>none — ephemeral (default)</option>
|
|
@@ -239,6 +246,9 @@ export function landingHtml() {
|
|
|
239
246
|
<option value="full">full — keep everything</option>
|
|
240
247
|
</select>
|
|
241
248
|
</label>
|
|
249
|
+
<label style="font-size:13px;color:var(--dim);display:inline-flex;align-items:center;gap:6px">
|
|
250
|
+
<input type="checkbox" id="require_identity" /> require account-bound identity to join
|
|
251
|
+
</label>
|
|
242
252
|
</div>
|
|
243
253
|
<button id="create">Create channel</button>
|
|
244
254
|
|
|
@@ -345,10 +355,11 @@ export function landingHtml() {
|
|
|
345
355
|
btn.textContent = 'Creating…';
|
|
346
356
|
try {
|
|
347
357
|
const retention = document.getElementById('retention').value;
|
|
358
|
+
const require_identity = document.getElementById('require_identity').checked;
|
|
348
359
|
const r = await fetch('/api/channels', {
|
|
349
360
|
method: 'POST',
|
|
350
361
|
headers: { 'Content-Type': 'application/json' },
|
|
351
|
-
body: JSON.stringify({ retention }),
|
|
362
|
+
body: JSON.stringify({ retention, require_identity }),
|
|
352
363
|
});
|
|
353
364
|
if (!r.ok) throw new Error('http ' + r.status);
|
|
354
365
|
const j = await r.json();
|
package/dist/mcp.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { verifyIdentity } from "./accounts.js";
|
|
2
3
|
import { getOrCreateChannel } from "./channel.js";
|
|
3
4
|
import { buildConnectInfo } from "./connect.js";
|
|
4
5
|
import { recordJoin as statsRecordJoin, recordMessage as statsRecordMessage } from "./stats.js";
|
|
5
|
-
import { channelExists, createChannel, getChannelRetention, verifyChannel } from "./store.js";
|
|
6
|
+
import { channelExists, createChannel, getChannelRequireIdentity, getChannelRetention, verifyChannel, } from "./store.js";
|
|
6
7
|
import { isRetention, recordJoin as transcriptRecordJoin, recordLeave as transcriptRecordLeave, recordMessage as transcriptRecordMessage, } from "./transcripts.js";
|
|
7
8
|
const PROTOCOL_VERSION = "2025-03-26";
|
|
8
9
|
const SERVER_INFO = { name: "rogerrat", version: "0.1.0" };
|
|
@@ -85,7 +86,7 @@ const CHANNEL_TOOLS = [
|
|
|
85
86
|
const UNIFIED_TOOLS = [
|
|
86
87
|
{
|
|
87
88
|
name: "create_channel",
|
|
88
|
-
description: "Create a new RogerRat channel. Returns the channel id, join token, MCP URL, and connect snippets. Optional retention: 'none' (default), 'metadata', 'prompts', 'full'.",
|
|
89
|
+
description: "Create a new RogerRat channel. Returns the channel id, join token, MCP URL, and connect snippets. Optional retention: 'none' (default), 'metadata', 'prompts', 'full'. Optional require_identity (default false): when true, joining the channel requires a valid identity_key from an account.",
|
|
89
90
|
inputSchema: {
|
|
90
91
|
type: "object",
|
|
91
92
|
properties: {
|
|
@@ -94,23 +95,31 @@ const UNIFIED_TOOLS = [
|
|
|
94
95
|
enum: ["none", "metadata", "prompts", "full"],
|
|
95
96
|
description: "Server-side transcript retention. Default: 'none' (ephemeral).",
|
|
96
97
|
},
|
|
98
|
+
require_identity: {
|
|
99
|
+
type: "boolean",
|
|
100
|
+
description: "Require an identity_key (from an account) to join. Default: false.",
|
|
101
|
+
},
|
|
97
102
|
},
|
|
98
103
|
},
|
|
99
104
|
},
|
|
100
105
|
{
|
|
101
106
|
name: "join",
|
|
102
|
-
description: "Join a channel by id + token
|
|
107
|
+
description: "Join a channel by id + token. Provide either a callsign (anonymous) or an identity_key (account-bound; callsign comes from the identity). If the channel has require_identity=true, identity_key is mandatory. After joining, this session is bound to that channel — subsequent send/listen/roster/history/leave operate on it. Call leave to detach.",
|
|
103
108
|
inputSchema: {
|
|
104
109
|
type: "object",
|
|
105
110
|
properties: {
|
|
106
111
|
channel_id: { type: "string", description: "Channel id like 'quiet-otter-3a8f'." },
|
|
107
|
-
token: { type: "string", description: "Bearer token for that channel
|
|
112
|
+
token: { type: "string", description: "Bearer token for that channel." },
|
|
108
113
|
callsign: {
|
|
109
114
|
type: "string",
|
|
110
|
-
description: "
|
|
115
|
+
description: "Anonymous handle. Ignored if identity_key is provided. 1-32 chars, alphanumeric/underscore/dash. Cannot be 'all'.",
|
|
116
|
+
},
|
|
117
|
+
identity_key: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "Account-bound identity key (from POST /api/account/identities). Required when channel has require_identity=true.",
|
|
111
120
|
},
|
|
112
121
|
},
|
|
113
|
-
required: ["channel_id", "token"
|
|
122
|
+
required: ["channel_id", "token"],
|
|
114
123
|
},
|
|
115
124
|
},
|
|
116
125
|
{
|
|
@@ -275,26 +284,40 @@ async function callUnifiedTool(name, args, state, sessionId, publicOrigin) {
|
|
|
275
284
|
if (name === "join") {
|
|
276
285
|
const channelId = String(args.channel_id ?? "");
|
|
277
286
|
const token = String(args.token ?? "");
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
287
|
+
const callsignArg = String(args.callsign ?? "");
|
|
288
|
+
const identityKey = typeof args.identity_key === "string" ? args.identity_key : undefined;
|
|
289
|
+
if (!channelId || !token)
|
|
290
|
+
throw new Error("join requires channel_id and token");
|
|
282
291
|
if (!channelExists(channelId))
|
|
283
292
|
throw new Error(`channel not found: ${channelId}`);
|
|
284
293
|
if (!verifyChannel(channelId, token))
|
|
285
294
|
throw new Error("invalid token for channel");
|
|
295
|
+
let resolvedCallsign = callsignArg;
|
|
296
|
+
let identitySource = null;
|
|
297
|
+
if (identityKey) {
|
|
298
|
+
const idRec = verifyIdentity(identityKey);
|
|
299
|
+
if (!idRec)
|
|
300
|
+
throw new Error("invalid identity_key");
|
|
301
|
+
resolvedCallsign = idRec.callsign;
|
|
302
|
+
identitySource = idRec.account_id;
|
|
303
|
+
}
|
|
304
|
+
else if (getChannelRequireIdentity(channelId)) {
|
|
305
|
+
throw new Error("this channel requires identity_key (require_identity=true). Create one at POST /api/account/identities.");
|
|
306
|
+
}
|
|
307
|
+
if (!resolvedCallsign)
|
|
308
|
+
throw new Error("either callsign or identity_key is required");
|
|
286
309
|
if (state.boundChannel && state.boundChannel !== channelId) {
|
|
287
310
|
const oldChannel = getOrCreateChannel(state.boundChannel);
|
|
288
311
|
oldChannel.leave(sessionId);
|
|
289
312
|
state.boundChannel = null;
|
|
290
313
|
}
|
|
291
314
|
const channel = getOrCreateChannel(channelId);
|
|
292
|
-
const { roster, history } = channel.join(sessionId,
|
|
315
|
+
const { roster, history } = channel.join(sessionId, resolvedCallsign);
|
|
293
316
|
statsRecordJoin();
|
|
294
|
-
transcriptRecordJoin(channelId, getChannelRetention(channelId),
|
|
317
|
+
transcriptRecordJoin(channelId, getChannelRetention(channelId), resolvedCallsign);
|
|
295
318
|
state.boundChannel = channelId;
|
|
296
319
|
const body = [
|
|
297
|
-
`Joined channel ${channelId} as ${
|
|
320
|
+
`Joined channel ${channelId} as ${resolvedCallsign}${identitySource ? ` (identity-bound to account ${identitySource})` : ""}.`,
|
|
298
321
|
`Roster (${roster.length}): ${roster.join(", ")}`,
|
|
299
322
|
"",
|
|
300
323
|
`Recent history (${history.length}):`,
|
package/dist/store.js
CHANGED
|
@@ -25,6 +25,7 @@ function ensureLoaded() {
|
|
|
25
25
|
tokenHash: r.tokenHash,
|
|
26
26
|
createdAt: r.createdAt,
|
|
27
27
|
retention: isRetention(r.retention) ? r.retention : "none",
|
|
28
|
+
requireIdentity: r.requireIdentity === true,
|
|
28
29
|
},
|
|
29
30
|
]));
|
|
30
31
|
}
|
|
@@ -44,16 +45,17 @@ function persist() {
|
|
|
44
45
|
export function createChannel(opts = {}) {
|
|
45
46
|
ensureLoaded();
|
|
46
47
|
const retention = opts.retention ?? "none";
|
|
48
|
+
const requireIdentity = opts.require_identity === true;
|
|
47
49
|
let id;
|
|
48
50
|
do {
|
|
49
51
|
id = generateChannelId();
|
|
50
52
|
} while (channels.has(id));
|
|
51
53
|
const token = generateToken();
|
|
52
|
-
channels.set(id, { id, tokenHash: hashToken(token), createdAt: Date.now(), retention });
|
|
54
|
+
channels.set(id, { id, tokenHash: hashToken(token), createdAt: Date.now(), retention, requireIdentity });
|
|
53
55
|
persist();
|
|
54
56
|
statsRecordChannelCreated();
|
|
55
57
|
transcriptRecordChannelCreated(id, retention);
|
|
56
|
-
return { id, token, retention };
|
|
58
|
+
return { id, token, retention, require_identity: requireIdentity };
|
|
57
59
|
}
|
|
58
60
|
export function verifyChannel(id, token) {
|
|
59
61
|
ensureLoaded();
|
|
@@ -74,3 +76,7 @@ export function getChannelRetention(id) {
|
|
|
74
76
|
ensureLoaded();
|
|
75
77
|
return channels.get(id)?.retention ?? "none";
|
|
76
78
|
}
|
|
79
|
+
export function getChannelRequireIdentity(id) {
|
|
80
|
+
ensureLoaded();
|
|
81
|
+
return channels.get(id)?.requireIdentity ?? false;
|
|
82
|
+
}
|
package/package.json
CHANGED