@gorlitzer-labs/apiary 1.13.9 → 1.14.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.
@@ -5,10 +5,10 @@ import {
5
5
  invitePrompt,
6
6
  peekInvite,
7
7
  sameApiaryServer
8
- } from "./chunk-MEWZT5KA.js";
9
- import "./chunk-BCQQJR5E.js";
8
+ } from "./chunk-XP5F5NCQ.js";
9
+ import "./chunk-LOONM4G3.js";
10
10
  import "./chunk-PEB6HEFT.js";
11
- import "./chunk-4A7MREYX.js";
11
+ import "./chunk-N2LPKMPO.js";
12
12
  import "./chunk-D7DXZQWT.js";
13
13
  import "./chunk-TMXQDGMN.js";
14
14
  import "./chunk-BZ4VDWC2.js";
@@ -20,4 +20,4 @@ export {
20
20
  peekInvite,
21
21
  sameApiaryServer
22
22
  };
23
- //# sourceMappingURL=invites-GRROUSCE.js.map
23
+ //# sourceMappingURL=invites-ID5SRM3D.js.map
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  HEADLESS_READY,
3
3
  join
4
- } from "./chunk-7JXPJO3R.js";
5
- import "./chunk-BCQQJR5E.js";
4
+ } from "./chunk-LNSDH2V5.js";
5
+ import "./chunk-LOONM4G3.js";
6
6
  import "./chunk-PEB6HEFT.js";
7
- import "./chunk-4A7MREYX.js";
7
+ import "./chunk-N2LPKMPO.js";
8
8
  import "./chunk-D7DXZQWT.js";
9
9
  import "./chunk-TMXQDGMN.js";
10
10
  import "./chunk-BZ4VDWC2.js";
@@ -12,4 +12,4 @@ export {
12
12
  HEADLESS_READY,
13
13
  join
14
14
  };
15
- //# sourceMappingURL=join-H62VB4HU.js.map
15
+ //# sourceMappingURL=join-W53OPEHR.js.map
@@ -9,16 +9,16 @@ import {
9
9
  roomEmoji,
10
10
  roomRulesPath,
11
11
  saveRoomSession,
12
- tailnetAddress,
13
- tmuxAvailable,
14
- tmuxKillSession,
15
- tmuxSessionExists
16
- } from "./chunk-BCQQJR5E.js";
12
+ tailnetAddress
13
+ } from "./chunk-LOONM4G3.js";
17
14
  import "./chunk-PEB6HEFT.js";
18
15
  import {
19
16
  buildShareUrl,
20
- extractToken
21
- } from "./chunk-4A7MREYX.js";
17
+ extractToken,
18
+ tmuxAvailable,
19
+ tmuxKillSession,
20
+ tmuxSessionExists
21
+ } from "./chunk-N2LPKMPO.js";
22
22
  import "./chunk-D7DXZQWT.js";
23
23
  import "./chunk-TMXQDGMN.js";
24
24
  import "./chunk-BZ4VDWC2.js";
@@ -847,7 +847,7 @@ ${daemonRes.stderr.split("\n").map((l) => " " + l).join("\n")}`);
847
847
  console.log(` ${D2}Rules:${R2} ${C2}${shortenPath(roomRulesPath(daemon.roomName))}${R2} ${D2}(edit to customize \u2014 hot-reloads)${R2}`);
848
848
  console.log(` Press Ctrl+C to leave \u2014 server keeps running.
849
849
  `);
850
- const { join: join2 } = await import("./join-H62VB4HU.js");
850
+ const { join: join2 } = await import("./join-W53OPEHR.js");
851
851
  const adminJoinUrl = buildShareUrl(daemon.serverUrl, daemon.adminToken);
852
852
  const expectedAgents = participants.filter((p) => p.role === "agent").map((p) => p.alias);
853
853
  await join2({ server: adminJoinUrl, name: hostName, expectedAgents });
@@ -973,7 +973,7 @@ ${daemonRes.stderr.split("\n").map((l) => " " + l).join("\n")}`);
973
973
  }
974
974
  console.log(` Press Ctrl+C to leave \u2014 server keeps running.
975
975
  `);
976
- const { join: join2 } = await import("./join-H62VB4HU.js");
976
+ const { join: join2 } = await import("./join-W53OPEHR.js");
977
977
  const adminJoinUrl = buildShareUrl(serverUrl, adminToken);
978
978
  await join2({ server: adminJoinUrl, name: resumeHostName });
979
979
  console.log(`
@@ -1096,4 +1096,4 @@ export {
1096
1096
  spawnDaemonServer,
1097
1097
  writeInvite
1098
1098
  };
1099
- //# sourceMappingURL=room-ID4DQDPT.js.map
1099
+ //# sourceMappingURL=room-XC546NTX.js.map
@@ -0,0 +1,164 @@
1
+ import {
2
+ TmuxBridge,
3
+ buildRuntimeMcpOptions
4
+ } from "./chunk-36WX2UAF.js";
5
+ import {
6
+ EventProcessor,
7
+ SseMultiplexer,
8
+ classifyEvent,
9
+ createStdioRuntimeMcpServer
10
+ } from "./chunk-WJD5EKO6.js";
11
+ import "./chunk-N2LPKMPO.js";
12
+ import {
13
+ randomName
14
+ } from "./chunk-D7DXZQWT.js";
15
+ import "./chunk-TMXQDGMN.js";
16
+ import "./chunk-BZ4VDWC2.js";
17
+
18
+ // src/cli/mcp/wake.ts
19
+ function resolveWakeConfig(env) {
20
+ const setting = (env.APIARY_MCP_WAKE ?? "addressed").trim().toLowerCase();
21
+ if (setting === "off" || setting === "0" || setting === "false") {
22
+ return { policy: "off", reason: "APIARY_MCP_WAKE=off" };
23
+ }
24
+ const pane = env.TMUX_PANE?.trim();
25
+ if (!pane || !/^%\d+$/.test(pane)) {
26
+ return { policy: "off", reason: "not running inside tmux (no $TMUX_PANE)" };
27
+ }
28
+ return { policy: "addressed", pane };
29
+ }
30
+ function addressedKind(event, selfId, participants) {
31
+ const senderId = event.type === "Mentioned" ? event.message.sender_id : event.type === "Pinged" ? event.pinger_id : event.participant_id;
32
+ if (senderId === selfId) return null;
33
+ const senderType = participants.find((p) => p.id === senderId)?.type ?? "human";
34
+ if (classifyEvent(event, "standby-everyone", selfId, senderType, senderId) !== "trigger") return null;
35
+ if (event.type === "Pinged") return "ping";
36
+ if (event.type === "Mentioned") return "mention";
37
+ return "whisper";
38
+ }
39
+ function safeLabel(name) {
40
+ const cleaned = name.replace(/[^A-Za-z0-9 _.-]/g, "").trim().slice(0, 40);
41
+ return cleaned || "someone";
42
+ }
43
+ var VERB = {
44
+ mention: "mentioned you",
45
+ whisper: "whispered to you",
46
+ ping: "pinged you"
47
+ };
48
+ function wakeLine(room, sender, kind) {
49
+ const r = safeLabel(room);
50
+ return `apiary: ${safeLabel(sender)} ${VERB[kind]} in room "${r}". Read it with apiary__catch_up("${r}") and answer there with apiary__send_message.`;
51
+ }
52
+ var McpWaker = class {
53
+ constructor(nudger) {
54
+ this.nudger = nudger;
55
+ }
56
+ nudger;
57
+ seen = /* @__PURE__ */ new Set();
58
+ onEvent(roomName, event, selfId, participants) {
59
+ const kind = addressedKind(event, selfId, participants);
60
+ if (!kind) return false;
61
+ const key = event.type === "Pinged" ? event.id : event.message.id;
62
+ if (this.seen.has(key)) return false;
63
+ this.seen.add(key);
64
+ if (this.seen.size > 500) this.seen.delete(this.seen.values().next().value);
65
+ const senderName = event.type === "Pinged" ? participants.find((p) => p.id === event.pinger_id)?.name ?? "someone" : event.message.sender_name;
66
+ this.nudger.nudge(wakeLine(roomName, senderName, kind));
67
+ return true;
68
+ }
69
+ };
70
+
71
+ // src/cli/mcp/run.ts
72
+ async function runMcpServer(options) {
73
+ const agentName = options.name ?? randomName();
74
+ const authority = options.authority ?? (options.admin ? "admin" : void 0);
75
+ const joinResults = [];
76
+ const sseMux = new SseMultiplexer();
77
+ const processor = new EventProcessor("", agentName, {
78
+ defaultMode: "everyone",
79
+ pullOnly: true
80
+ });
81
+ const wake = resolveWakeConfig(process.env);
82
+ const bridge = wake.pane ? new TmuxBridge(wake.pane, { attended: true }) : null;
83
+ const waker = bridge ? new McpWaker({
84
+ nudge: (text) => {
85
+ bridge.deliver([{ type: "text", text }], { dedupe: true }).catch(() => {
86
+ });
87
+ }
88
+ }) : null;
89
+ const wrappedSource = {
90
+ [Symbol.asyncIterator]() {
91
+ const inner = sseMux[Symbol.asyncIterator]();
92
+ return {
93
+ async next() {
94
+ const result = await inner.next();
95
+ if (!result.done) {
96
+ const { roomId, event } = result.value;
97
+ const jr = joinResults.find((j) => j.roomId === roomId);
98
+ if (jr) {
99
+ if (event.type === "ParticipantJoined") {
100
+ jr.dataSource.addParticipant(event.participant);
101
+ } else if (event.type === "ParticipantLeft") {
102
+ jr.dataSource.removeParticipant(event.participant_id);
103
+ }
104
+ try {
105
+ waker?.onEvent(jr.roomName, event, jr.participantId, jr.dataSource.listParticipants());
106
+ } catch {
107
+ }
108
+ }
109
+ }
110
+ return result;
111
+ }
112
+ };
113
+ }
114
+ };
115
+ const mcpServer = await createStdioRuntimeMcpServer(
116
+ buildRuntimeMcpOptions({ processor, sseMux, joinResults, agentName, authority })
117
+ );
118
+ const deliver = async (_parts) => {
119
+ };
120
+ const eventLoopPromise = processor.run(deliver, wrappedSource).catch(() => {
121
+ });
122
+ process.stderr.write(`apiary mcp server running as "${agentName}"
123
+ `);
124
+ if (options.joinUrls && options.joinUrls.length > 0) {
125
+ process.stderr.write(`pending rooms: ${options.joinUrls.join(", ")}
126
+ `);
127
+ }
128
+ process.stderr.write(`use join_room(url) to connect to a room
129
+ `);
130
+ process.stderr.write(
131
+ waker ? `wake: on \u2014 @mentions, whispers and pings nudge tmux pane ${wake.pane}
132
+ ` : `wake: off \u2014 ${wake.reason}; call catch_up() to see room messages
133
+ `
134
+ );
135
+ async function cleanup() {
136
+ bridge?.stop();
137
+ await processor.stop();
138
+ sseMux.close();
139
+ await mcpServer.stop();
140
+ for (const jr of joinResults) {
141
+ try {
142
+ await fetch(`${jr.serverUrl}/disconnect`, {
143
+ method: "POST",
144
+ headers: { "Content-Type": "application/json" },
145
+ body: JSON.stringify({ token: jr.sessionToken })
146
+ });
147
+ } catch {
148
+ }
149
+ }
150
+ }
151
+ await Promise.race([
152
+ mcpServer.closed,
153
+ new Promise((resolve) => {
154
+ process.on("SIGINT", resolve);
155
+ process.on("SIGTERM", resolve);
156
+ })
157
+ ]);
158
+ await cleanup();
159
+ await eventLoopPromise;
160
+ }
161
+ export {
162
+ runMcpServer
163
+ };
164
+ //# sourceMappingURL=run-6JRFWMAQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/mcp/wake.ts","../src/cli/mcp/run.ts"],"sourcesContent":["/**\n * Wake an `apiary mcp` session when the room addresses it.\n *\n * `apiary mcp` is pull-based: the agent sees room traffic only when it calls\n * catch_up(), so an idle session at its prompt never notices an @mention until\n * a human types. When the MCP server runs inside tmux — Claude Code passes\n * $TMUX_PANE through to its stdio MCP servers — it can nudge that pane.\n *\n * The nudge is FIXED text built by apiary, never room content. Typed input is\n * read by the agent as its operator's instruction, and a room is shared with\n * other agents and people; letting their words arrive that way would let anyone\n * in the room drive this session. The message itself is read through\n * catch_up(), where it arrives as room data. Only the room name and the sender's\n * name are interpolated, and both are reduced to a safe character set.\n *\n * Only events addressed to this agent wake it — an @mention, a whisper naming\n * it, or a ping — whatever its engagement mode, so a busy room costs a pane\n * nothing. `APIARY_MCP_WAKE=off` turns it off.\n */\n\nimport { classifyEvent } from \"../../agent/engagement.js\";\nimport type { RoomEvent } from \"../../core/events.js\";\nimport type { Participant } from \"../../core/types.js\";\n\nexport type WakePolicy = \"addressed\" | \"off\";\n\nexport interface WakeConfig {\n policy: WakePolicy;\n /** tmux pane to nudge, from $TMUX_PANE. */\n pane?: string;\n /** Why waking is off, for the startup line on stderr. */\n reason?: string;\n}\n\n/** Decide whether and where to wake, from the environment. */\nexport function resolveWakeConfig(env: NodeJS.ProcessEnv): WakeConfig {\n const setting = (env.APIARY_MCP_WAKE ?? \"addressed\").trim().toLowerCase();\n if (setting === \"off\" || setting === \"0\" || setting === \"false\") {\n return { policy: \"off\", reason: \"APIARY_MCP_WAKE=off\" };\n }\n const pane = env.TMUX_PANE?.trim();\n if (!pane || !/^%\\d+$/.test(pane)) {\n return { policy: \"off\", reason: \"not running inside tmux (no $TMUX_PANE)\" };\n }\n return { policy: \"addressed\", pane };\n}\n\nexport type WakeKind = \"mention\" | \"whisper\" | \"ping\";\n\n/**\n * Is this event aimed at this agent? Same rule a standby agent wakes on, so\n * the two paths cannot disagree about what \"addressed\" means.\n */\nexport function addressedKind(\n event: RoomEvent,\n selfId: string,\n participants: Participant[],\n): WakeKind | null {\n const senderId =\n event.type === \"Mentioned\" ? event.message.sender_id\n : event.type === \"Pinged\" ? event.pinger_id\n : event.participant_id;\n if (senderId === selfId) return null;\n const senderType = participants.find((p) => p.id === senderId)?.type ?? \"human\";\n if (classifyEvent(event, \"standby-everyone\", selfId, senderType, senderId) !== \"trigger\") return null;\n if (event.type === \"Pinged\") return \"ping\";\n if (event.type === \"Mentioned\") return \"mention\";\n return \"whisper\";\n}\n\n/** Keep a room-controlled name from carrying anything but a plain label. */\nexport function safeLabel(name: string): string {\n const cleaned = name.replace(/[^A-Za-z0-9 _.-]/g, \"\").trim().slice(0, 40);\n return cleaned || \"someone\";\n}\n\nconst VERB: Record<WakeKind, string> = {\n mention: \"mentioned you\",\n whisper: \"whispered to you\",\n ping: \"pinged you\",\n};\n\n/** The one line typed into the pane. Fixed wording; two sanitised names. */\nexport function wakeLine(room: string, sender: string, kind: WakeKind): string {\n const r = safeLabel(room);\n return `apiary: ${safeLabel(sender)} ${VERB[kind]} in room \"${r}\". ` +\n `Read it with apiary__catch_up(\"${r}\") and answer there with apiary__send_message.`;\n}\n\nexport interface Nudger {\n nudge(text: string): void;\n}\n\n/**\n * Turns room events into nudges. One nudge per message: a whisper that also\n * @mentions the agent arrives as two events and must not wake it twice.\n */\nexport class McpWaker {\n private seen = new Set<string>();\n\n constructor(private nudger: Nudger) {}\n\n onEvent(roomName: string, event: RoomEvent, selfId: string, participants: Participant[]): boolean {\n const kind = addressedKind(event, selfId, participants);\n if (!kind) return false;\n\n const key = event.type === \"Pinged\" ? event.id : (event as { message: { id: string } }).message.id;\n if (this.seen.has(key)) return false;\n this.seen.add(key);\n if (this.seen.size > 500) this.seen.delete(this.seen.values().next().value as string);\n\n const senderName =\n event.type === \"Pinged\"\n ? participants.find((p) => p.id === event.pinger_id)?.name ?? \"someone\"\n : (event as { message: { sender_name: string } }).message.sender_name;\n this.nudger.nudge(wakeLine(roomName, senderName, kind));\n return true;\n }\n}\n","/**\n * apiary mcp — standalone MCP server for any MCP client.\n *\n * Unlike `apiary claude` (which wraps Claude Code in tmux), this exposes\n * apiary tools as a stdio MCP server. Any MCP client (Claude Code, Cursor,\n * Windsurf, etc.) can add it to their MCP config and call join_room()\n * to participate in apiary rooms on the fly.\n *\n * No tmux. No wrapper. Just tools.\n *\n * Event delivery is pull-based: the agent calls catch_up() to get new events.\n * Inside tmux, events addressed to the agent also nudge its pane (see wake.ts),\n * so an idle session notices an @mention without anyone typing.\n * The EventProcessor still runs in the background, classifying events via the\n * engagement model and buffering content — catch_up() returns the formatted,\n * classified result.\n *\n * The MCP tool handlers are the same ones the tmux/HTTP runtimes use; they are\n * built by the shared runtime-handlers factory so this path can't drift.\n */\n\nimport { randomName } from \"../../core/names.js\";\nimport { EventProcessor } from \"../../agent/event-processor.js\";\nimport { SseMultiplexer } from \"../../agent/sse-multiplexer.js\";\nimport { createStdioRuntimeMcpServer } from \"../../agent/mcp/runtime.js\";\nimport type { AuthorityLevel } from \"../../core/types.js\";\nimport type { LabeledEvent } from \"../../agent/multiplexer.js\";\nimport type { ContentPart } from \"../../agent/types.js\";\nimport { buildRuntimeMcpOptions, type JoinResult } from \"../runtime-handlers.js\";\nimport { TmuxBridge } from \"../claude/tmux-bridge.js\";\nimport { McpWaker, resolveWakeConfig } from \"./wake.js\";\n\nexport interface McpServerOptions {\n name?: string;\n /** Legacy CLI surface flag. Equivalent to `authority: \"admin\"`. */\n admin?: boolean;\n /** Pre-declared authority for tool exposure. See AgentRuntimeOptions. */\n authority?: AuthorityLevel;\n joinUrls?: string[];\n}\n\nexport async function runMcpServer(options: McpServerOptions): Promise<void> {\n const agentName = options.name ?? randomName();\n const authority: AuthorityLevel | undefined = options.authority\n ?? (options.admin ? \"admin\" : undefined);\n const joinResults: JoinResult[] = [];\n\n // ── SSE multiplexer (starts empty, grows as agent joins rooms) ────────\n const sseMux = new SseMultiplexer();\n\n // ── EventProcessor (selfId set on first join) ─────────────────────────\n const processor = new EventProcessor(\"\", agentName, {\n defaultMode: \"everyone\",\n pullOnly: true,\n });\n\n // ── Wake the session's own tmux pane when the room addresses it ───────\n const wake = resolveWakeConfig(process.env);\n const bridge = wake.pane ? new TmuxBridge(wake.pane, { attended: true }) : null;\n const waker = bridge\n ? new McpWaker({\n nudge: (text) => {\n bridge.deliver([{ type: \"text\", text }], { dedupe: true }).catch(() => {});\n },\n })\n : null;\n\n // ── Wrap SSE source for participant cache updates and wake-ups ────────\n const wrappedSource: AsyncIterable<LabeledEvent> = {\n [Symbol.asyncIterator]() {\n const inner = sseMux[Symbol.asyncIterator]();\n return {\n async next() {\n const result = await inner.next();\n if (!result.done) {\n const { roomId, event } = result.value;\n const jr = joinResults.find((j) => j.roomId === roomId);\n if (jr) {\n if (event.type === \"ParticipantJoined\") {\n jr.dataSource.addParticipant(event.participant);\n } else if (event.type === \"ParticipantLeft\") {\n jr.dataSource.removeParticipant(event.participant_id);\n }\n try {\n waker?.onEvent(jr.roomName, event, jr.participantId, jr.dataSource.listParticipants());\n } catch {\n // A failed nudge must never stop the event stream; catch_up still works.\n }\n }\n }\n return result;\n },\n };\n },\n };\n\n // ── Create stdio MCP server (shared handler set) ──────────────────────\n const mcpServer = await createStdioRuntimeMcpServer(\n buildRuntimeMcpOptions({ processor, sseMux, joinResults, agentName, authority }),\n );\n\n // ── Start event loop in background ────────────────────────────────────\n // Delivery is a no-op: events are buffered in the EventProcessor and\n // returned via catch_up(). We still run the loop so engagement\n // classification, buffering, and seen-event tracking happen in real-time.\n const deliver = async (_parts: ContentPart[]) => {\n // No-op — standalone MCP is pull-based.\n // Events are classified and buffered by EventProcessor.\n // Agent pulls them via catch_up().\n };\n\n const eventLoopPromise = processor\n .run(deliver, wrappedSource)\n .catch(() => {});\n\n process.stderr.write(`apiary mcp server running as \"${agentName}\"\\n`);\n if (options.joinUrls && options.joinUrls.length > 0) {\n process.stderr.write(`pending rooms: ${options.joinUrls.join(\", \")}\\n`);\n }\n process.stderr.write(`use join_room(url) to connect to a room\\n`);\n process.stderr.write(\n waker\n ? `wake: on — @mentions, whispers and pings nudge tmux pane ${wake.pane}\\n`\n : `wake: off — ${wake.reason}; call catch_up() to see room messages\\n`,\n );\n\n // ── Cleanup ───────────────────────────────────────────────────────────\n async function cleanup(): Promise<void> {\n bridge?.stop();\n await processor.stop();\n sseMux.close();\n await mcpServer.stop();\n\n for (const jr of joinResults) {\n try {\n await fetch(`${jr.serverUrl}/disconnect`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ token: jr.sessionToken }),\n });\n } catch {\n // Server may be down\n }\n }\n }\n\n // Wait for stdio transport to close (MCP client disconnected)\n // or process signal\n await Promise.race([\n mcpServer.closed,\n new Promise<void>((resolve) => {\n process.on(\"SIGINT\", resolve);\n process.on(\"SIGTERM\", resolve);\n }),\n ]);\n\n await cleanup();\n await eventLoopPromise;\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gorlitzer-labs/apiary",
3
- "version": "1.13.9",
3
+ "version": "1.14.0",
4
4
  "description": "Multiplayer rooms for AI agents",
5
5
  "keywords": [
6
6
  "ai",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/cli/auth.ts"],"sourcesContent":["/**\n * Auth token system for apiary share links and session management.\n *\n * Two token types:\n * - Share tokens — embedded in URLs, map to an authority tier.\n * Anyone with the link joins at that tier.\n * - Session tokens — issued on join, identify a participant + authority.\n * Used for all subsequent API calls.\n */\n\nimport { randomBytes } from \"node:crypto\";\nimport type { AuthorityLevel } from \"../core/types.js\";\nimport { TIER_ORDER } from \"../core/authority.js\";\n\ninterface SessionData {\n participantId: string;\n authority: AuthorityLevel;\n createdAt: number;\n expiresAt: number;\n}\n\ninterface ShareTokenData {\n authority: AuthorityLevel;\n createdAt: number;\n /** null = never expires. Only the room's own founding pair gets that. */\n expiresAt: number | null;\n}\n\nexport interface TokenManagerOptions {\n /** Session token TTL in ms. Default: 24 hours. */\n sessionTtlMs?: number;\n /** Share token TTL in ms. Default: 1 hour. */\n shareTtlMs?: number;\n}\n\nconst DEFAULT_SESSION_TTL = 24 * 60 * 60 * 1000; // 24h\nconst DEFAULT_SHARE_TTL = 24 * 60 * 60 * 1000; // 24h\n\nexport class TokenManager {\n /** share token → share data */\n private _shareTokens = new Map<string, ShareTokenData>();\n /** session token → participant data */\n private _sessionTokens = new Map<string, SessionData>();\n\n private _sessionTtlMs: number;\n private _shareTtlMs: number;\n\n constructor(options?: TokenManagerOptions) {\n this._sessionTtlMs = options?.sessionTtlMs ?? DEFAULT_SESSION_TTL;\n this._shareTtlMs = options?.shareTtlMs ?? DEFAULT_SHARE_TTL;\n }\n\n /**\n * Generate a share token at the given authority tier.\n * Callers can only generate tokens at their own tier or below.\n *\n * `neverExpires` is for the room's OWN founding pair, not for links handed\n * to anybody. Those two tokens are written into ~/.apiary/sessions and are\n * the only recorded way back into a running room, so expiring them locks the\n * owner out of their own live server: observed at 26 hours, with a room\n * still serving, agents still working inside it, and `apiary room resume`\n * answering \"Invalid share token\" with no hint that time was the cause.\n *\n * A TTL there protected nothing — the token sits in a file already readable\n * only by its owner — while the thing it broke was the operator's way in. A\n * link generated by /share is a genuine hand-out and still expires.\n */\n generateShareToken(\n callerAuthority: AuthorityLevel,\n targetAuthority: AuthorityLevel,\n opts?: { neverExpires?: boolean },\n ): string | null {\n if (!canGrant(callerAuthority, targetAuthority)) return null;\n const token = randomBytes(16).toString(\"hex\");\n const now = Date.now();\n this._shareTokens.set(token, {\n authority: targetAuthority,\n createdAt: now,\n expiresAt: opts?.neverExpires === true ? null : now + this._shareTtlMs,\n });\n return token;\n }\n\n /** Validate a share token and return its authority level. Deletes if expired. */\n validateShareToken(token: string): AuthorityLevel | null {\n const data = this._shareTokens.get(token);\n if (!data) return null;\n // A null expiry is the room's own founding token: it lives as long as the\n // room does, because it IS the room's credential rather than a hand-out.\n if (data.expiresAt !== null && Date.now() > data.expiresAt) {\n this._shareTokens.delete(token);\n return null;\n }\n return data.authority;\n }\n\n /** Create a session token for a participant. */\n createSessionToken(participantId: string, authority: AuthorityLevel): string {\n const token = randomBytes(16).toString(\"hex\");\n const now = Date.now();\n this._sessionTokens.set(token, {\n participantId,\n authority,\n createdAt: now,\n expiresAt: now + this._sessionTtlMs,\n });\n return token;\n }\n\n /** Validate a session token and return participant data. Deletes if expired. */\n validateSessionToken(token: string): SessionData | null {\n const data = this._sessionTokens.get(token);\n if (!data) return null;\n if (Date.now() > data.expiresAt) {\n this._sessionTokens.delete(token);\n return null;\n }\n return data;\n }\n\n /** Revoke a session token (on disconnect). */\n revokeSessionToken(token: string): void {\n this._sessionTokens.delete(token);\n }\n\n /** Update the authority level for an existing session. */\n updateSessionAuthority(token: string, newAuthority: AuthorityLevel): boolean {\n const data = this._sessionTokens.get(token);\n if (!data) return false;\n data.authority = newAuthority;\n return true;\n }\n\n /** Find a session token by participant ID (for cleanup). */\n findSessionByParticipant(participantId: string): string | null {\n for (const [token, data] of this._sessionTokens) {\n if (data.participantId === participantId) return token;\n }\n return null;\n }\n\n /** Rotate a session token: create new with same participant data, delete old. */\n rotateSessionToken(oldToken: string): { newToken: string; data: SessionData } | null {\n const data = this._sessionTokens.get(oldToken);\n if (!data) return null;\n if (Date.now() > data.expiresAt) {\n this._sessionTokens.delete(oldToken);\n return null;\n }\n this._sessionTokens.delete(oldToken);\n const newToken = randomBytes(16).toString(\"hex\");\n const now = Date.now();\n const newData: SessionData = {\n participantId: data.participantId,\n authority: data.authority,\n createdAt: now,\n expiresAt: now + this._sessionTtlMs,\n };\n this._sessionTokens.set(newToken, newData);\n return { newToken, data: newData };\n }\n\n /** Sweep expired tokens from both maps. */\n pruneExpired(): void {\n const now = Date.now();\n for (const [token, data] of this._shareTokens) {\n // `now > null` is true in JS — null coerces to 0 — so an unguarded sweep\n // would delete every never-expiring token on its first pass and undo the\n // whole point of them.\n if (data.expiresAt !== null && now > data.expiresAt) this._shareTokens.delete(token);\n }\n for (const [token, data] of this._sessionTokens) {\n if (now > data.expiresAt) this._sessionTokens.delete(token);\n }\n }\n}\n\n/**\n * Can a caller at `callerLevel` grant a share token at `targetLevel`?\n * Admin/product_owner links can only be granted by admins.\n * Everyone else can grant links at or below their own tier (excluding PO/admin).\n */\nfunction canGrant(callerLevel: AuthorityLevel, targetLevel: AuthorityLevel): boolean {\n if (targetLevel === \"admin\" || targetLevel === \"product_owner\") {\n return callerLevel === \"admin\";\n }\n return TIER_ORDER[callerLevel] >= TIER_ORDER[targetLevel];\n}\n\n/** Build a share URL from a base URL and token. */\nexport function buildShareUrl(baseUrl: string, token: string): string {\n const url = new URL(baseUrl);\n url.searchParams.set(\"token\", token);\n return url.toString();\n}\n\n/** Extract a token from a share URL. */\nexport function extractToken(url: string): string | null {\n try {\n const parsed = new URL(url);\n return parsed.searchParams.get(\"token\");\n } catch {\n return null;\n }\n}\n"],"mappings":"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","names":[]}