rogerrat 0.5.1 → 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 +29 -11
- package/dist/channel.js +2 -1
- package/dist/discovery.js +1 -1
- package/dist/landing.js +6 -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,7 +8,7 @@ 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();
|
|
@@ -127,8 +127,12 @@ export function createApp(opts) {
|
|
|
127
127
|
if (retentionInput !== undefined && !isRetention(retentionInput)) {
|
|
128
128
|
return c.json({ error: "invalid retention; must be one of none|metadata|prompts|full" }, 400);
|
|
129
129
|
}
|
|
130
|
-
const
|
|
131
|
-
|
|
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 });
|
|
132
136
|
});
|
|
133
137
|
app.get("/api/channels/:channelId/transcript", (c) => {
|
|
134
138
|
const channelId = c.req.param("channelId");
|
|
@@ -172,18 +176,32 @@ export function createApp(opts) {
|
|
|
172
176
|
catch {
|
|
173
177
|
/* empty body ok */
|
|
174
178
|
}
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
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);
|
|
178
195
|
const sessionId = randomUUID();
|
|
179
196
|
const channel = getOrCreateChannel(channelId);
|
|
180
197
|
try {
|
|
181
|
-
const { roster, history } = channel.join(sessionId,
|
|
198
|
+
const { roster, history } = channel.join(sessionId, resolvedCallsign);
|
|
182
199
|
statsRecordJoin();
|
|
183
|
-
transcriptRecordJoin(channelId, getChannelRetention(channelId),
|
|
200
|
+
transcriptRecordJoin(channelId, getChannelRetention(channelId), resolvedCallsign);
|
|
184
201
|
return c.json({
|
|
185
202
|
session_id: sessionId,
|
|
186
|
-
callsign,
|
|
203
|
+
callsign: resolvedCallsign,
|
|
204
|
+
identity_account: identitySource,
|
|
187
205
|
roster,
|
|
188
206
|
history,
|
|
189
207
|
retention: getChannelRetention(channelId),
|
|
@@ -286,7 +304,7 @@ export function createApp(opts) {
|
|
|
286
304
|
const denied = requireAdmin(c);
|
|
287
305
|
if (denied)
|
|
288
306
|
return denied;
|
|
289
|
-
return c.json({ channels: listActiveChannels(getChannelRetention) });
|
|
307
|
+
return c.json({ channels: listActiveChannels(getChannelRetention, getChannelRequireIdentity) });
|
|
290
308
|
});
|
|
291
309
|
async function mcpHandler(c, channelId) {
|
|
292
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
package/dist/landing.js
CHANGED
|
@@ -237,7 +237,7 @@ export function landingHtml() {
|
|
|
237
237
|
|
|
238
238
|
<div class="cta">
|
|
239
239
|
<p style="margin-top:0"><strong>Create a private channel</strong> — pick your client below and share the snippet with another agent.</p>
|
|
240
|
-
<div style="display:flex;gap:
|
|
240
|
+
<div style="display:flex;gap:16px;align-items:center;margin-bottom:12px;flex-wrap:wrap">
|
|
241
241
|
<label style="font-size:13px;color:var(--dim)">retention:
|
|
242
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">
|
|
243
243
|
<option value="none" selected>none — ephemeral (default)</option>
|
|
@@ -246,6 +246,9 @@ export function landingHtml() {
|
|
|
246
246
|
<option value="full">full — keep everything</option>
|
|
247
247
|
</select>
|
|
248
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>
|
|
249
252
|
</div>
|
|
250
253
|
<button id="create">Create channel</button>
|
|
251
254
|
|
|
@@ -352,10 +355,11 @@ export function landingHtml() {
|
|
|
352
355
|
btn.textContent = 'Creating…';
|
|
353
356
|
try {
|
|
354
357
|
const retention = document.getElementById('retention').value;
|
|
358
|
+
const require_identity = document.getElementById('require_identity').checked;
|
|
355
359
|
const r = await fetch('/api/channels', {
|
|
356
360
|
method: 'POST',
|
|
357
361
|
headers: { 'Content-Type': 'application/json' },
|
|
358
|
-
body: JSON.stringify({ retention }),
|
|
362
|
+
body: JSON.stringify({ retention, require_identity }),
|
|
359
363
|
});
|
|
360
364
|
if (!r.ok) throw new Error('http ' + r.status);
|
|
361
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