shadok-ai 0.13.13 → 0.13.14

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.
@@ -558,19 +558,45 @@ async function cmdScreen(id, flags) {
558
558
  const HELP =
559
559
  "usage: pilotctl <spawn|prompt|dialog|choose|toggle|confirm|freetext|list|diff|stop|screen|profile-prompt|peer> …\n" +
560
560
  " add --peer <alias> to any command to run it against another instance's agents\n" +
561
- " pilotctl peer <add <alias> <url> <token>|list|rm <alias>>";
561
+ " pilotctl peer <add <alias> <url> <ticket> [--token]|list|rm <alias>>";
562
562
 
563
- // Manage the outbound-peers file: aliases → { url, token }. The token is minted by
564
- // the REMOTE instance's admin (POST /peers there) and pasted in here once. Local
565
- // only, no server involved. `list` never prints tokens.
566
- export function cmdPeer(pos) {
567
- const [sub, alias, url, token] = pos;
563
+ /**
564
+ * Manage the outbound-peers file: aliases → { url, token }.
565
+ *
566
+ * `add` takes the single-use TICKET from the invitation and ENROLS with it: it
567
+ * posts the ticket to the remote `POST /peers/enrol`, which consumes it and
568
+ * returns the durable token stored here. So the credential we keep is one this
569
+ * side obtained itself — the invitation, which gets pasted into a session and
570
+ * lives on in a transcript, only ever held something that dies on first use.
571
+ *
572
+ * A ticket that has already been redeemed, expired or never existed is refused
573
+ * by the remote with one indistinguishable message, and we store NOTHING: a
574
+ * saved credential that does not authenticate is worse than a clear error.
575
+ *
576
+ * `--token` skips the exchange and stores the value as a durable token, for a
577
+ * credential issued some other way. `list` never prints either.
578
+ */
579
+ export async function cmdPeer(pos, flags = {}) {
580
+ const [sub, alias, url, secret] = pos;
568
581
  const map = loadPeersOut();
569
582
  if (sub === "add") {
570
- if (!alias || !url || !token) throw new Error("usage: pilotctl peer add <alias> <url> <token>");
571
- map[alias] = { url, token };
583
+ if (!alias || !url || !secret)
584
+ throw new Error("usage: pilotctl peer add <alias> <url> <ticket> [--token]");
585
+ const base = String(url).replace(/\/+$/, "");
586
+ let token = secret;
587
+ if (!flags.token) {
588
+ const r = await fetch(`${base}/peers/enrol`, {
589
+ method: "POST",
590
+ headers: { "content-type": "application/json" },
591
+ body: JSON.stringify({ ticket: secret }),
592
+ });
593
+ const j = await r.json().catch(() => ({}));
594
+ if (!r.ok || !j?.token) throw new Error(j?.error || `enrolment refused (HTTP ${r.status})`);
595
+ token = j.token;
596
+ }
597
+ map[alias] = { url: base, token };
572
598
  savePeersOut(map);
573
- return { ok: true, alias, url };
599
+ return { ok: true, alias, url: base, enrolled: !flags.token };
574
600
  }
575
601
  if (sub === "rm") {
576
602
  if (!alias) throw new Error("usage: pilotctl peer rm <alias>");
@@ -586,7 +612,7 @@ export function cmdPeer(pos) {
586
612
  export async function run(argv) {
587
613
  const { cmd, pos, flags } = parseArgs(argv);
588
614
  // Manage the outbound-peers file (local, no server).
589
- if (cmd === "peer") return cmdPeer(pos);
615
+ if (cmd === "peer") return cmdPeer(pos, flags);
590
616
  // Target a REMOTE peer for this command — every call then goes to its URL with
591
617
  // the peer token instead of the local session key. `--peer <alias>` resolves
592
618
  // the outbound file; `--peer-url/--peer-token` is a one-off; SHADOK_PEER_URL/
package/dist/peers.d.ts CHANGED
@@ -1,24 +1,58 @@
1
1
  /**
2
2
  * PEERS — other instances (or harnesses) allowed to reach THIS one's agents.
3
3
  *
4
- * A peer is the cross-instance twin of a web account (`accounts.ts`): named,
5
- * revocable, per launch dir. Its credential is a token shaped exactly like the
6
- * agent session key (invariant 33): an HMAC of the name, **non-expiring** (a
7
- * cross-instance link is persistent and must not hit the 7-day cookie cliff) and
8
- * **revocable by the registry** — the token embeds the name, and verifying
9
- * requires that name to still be listed here. Delete the row ⇒ every token for
10
- * it is dead, with no server state to keep. No secret is stored (it is derived),
11
- * same as the session key. See docs/superpowers/specs/2026-09-26-cross-instance-
4
+ * A peer is the cross-instance twin of a web account (`accounts.ts`), and it
5
+ * enrols the same way: named, revocable, per launch dir, and reached through a
6
+ * **single-use ticket** that is CONSUMED on first use, exactly as a user's
7
+ * invitation link is. What the invitee keeps afterwards is a durable token it
8
+ * obtained itself — so the long-lived credential never travels through an
9
+ * invitation, a chat, or an invitee's transcript.
10
+ *
11
+ * Two properties are load-bearing, and the first one was missing at first.
12
+ *
13
+ * The token is salted PER PEER. It used to be `hmac(secret, "peer:"+name)`,
14
+ * which is deterministic: deleting a row revoked the token, but re-adding a peer
15
+ * under the SAME name minted the identical token again — handing a leaked
16
+ * credential straight back. Revocation was not rotation. The row now carries a
17
+ * random `salt` mixed into the MAC, so re-inviting the same name yields a
18
+ * different token, and the only way to rotate used to be renaming the peer or
19
+ * rotating the instance secret (which is shared with every user session).
20
+ *
21
+ * It stays **non-expiring** once enrolled — a cross-instance link is persistent
22
+ * and must not hit the 7-day cookie cliff — and **revocable by the registry**:
23
+ * verifying requires the name to still be listed here, so deleting the row
24
+ * kills every token for it. See docs/superpowers/specs/2026-09-26-cross-instance-
12
25
  * agent-collaboration-design.md.
13
26
  *
14
27
  * A peer that connects is a DISTINCT principal, scoped by the caller to the
15
28
  * agents it created (`Channel.createdByPeer`) — never B's whole fleet.
16
29
  */
30
+ /** A pending enrolment: the single-use ticket, and when it stops being usable. */
31
+ export interface PeerInvite {
32
+ ticket: string;
33
+ expiresAt: number;
34
+ }
17
35
  export interface Peer {
18
36
  name: string;
19
37
  createdAt: number;
20
38
  note?: string;
39
+ /**
40
+ * Random, per peer, mixed into the token's MAC. Without it the token is a
41
+ * pure function of the name, so revoke-then-re-invite returns the SAME
42
+ * credential — see the note above. Absent on a row written before this
43
+ * existed; `withSalts` backfills one, which invalidates that row's old token
44
+ * (deliberately: a deterministic token is the thing being removed).
45
+ */
46
+ salt?: string;
47
+ /** Present until the ticket is redeemed. While it is here the peer has NOT
48
+ * enrolled, and its durable token does not authenticate anything. */
49
+ invite?: PeerInvite;
50
+ /** When the ticket was redeemed and the durable token handed over. */
51
+ enrolledAt?: number;
21
52
  }
53
+ /** How long a ticket stays redeemable. Long enough to reach an agent that is
54
+ * mid-task, short enough that a forgotten invitation is not a standing door. */
55
+ export declare const PEER_INVITE_TTL_MS: number;
22
56
  /** INBOUND registry, per launch dir, 600 — peers allowed to reach THIS instance. */
23
57
  export declare function peerFileFor(cwd?: string): string;
24
58
  /** OUTBOUND registry, per launch dir, 600 — peers THIS instance can reach
@@ -32,18 +66,71 @@ export declare function savePeers(file: string, peers: Peer[]): void;
32
66
  /** A peer name is a slug: lowercased, safe chars, non-empty. null if nothing
33
67
  * usable survives — an empty/invalid name must never become a live credential. */
34
68
  export declare function normPeerName(name: unknown): string | null;
35
- /** Pure: add (or supersede) a peer by name. */
36
- export declare function addPeer(peers: Peer[], name: string, now: number, note?: string): Peer[];
69
+ /**
70
+ * Pure: add (or supersede) a peer by name, with a FRESH salt and a fresh
71
+ * single-use ticket.
72
+ *
73
+ * Superseding re-rolls the salt on purpose: re-inviting a name is exactly when
74
+ * the old credential must stop working. `salt`/`ticket` are injected so the
75
+ * result is testable; `newPeer` below supplies real randomness.
76
+ */
77
+ export declare function addPeer(peers: Peer[], name: string, now: number, note: string | undefined, salt: string, ticket: string, ttlMs?: number): Peer[];
78
+ /** Impure twin of `addPeer`: draws the salt and the ticket. */
79
+ export declare function newPeer(peers: Peer[], name: string, now: number, note?: string): {
80
+ peers: Peer[];
81
+ ticket: string;
82
+ };
83
+ /**
84
+ * Backfill a salt on any row written before salts existed.
85
+ *
86
+ * Doing this INVALIDATES that row's existing token, and that is the point: the
87
+ * saltless token is the deterministic one being removed. A peer whose token
88
+ * stops working is re-invited; a peer whose leaked token kept working would not
89
+ * know to.
90
+ */
91
+ export declare function withSalts(peers: Peer[], mint?: () => string): Peer[];
92
+ export type RedeemVerdict = {
93
+ ok: true;
94
+ name: string;
95
+ } | {
96
+ ok: false;
97
+ error: string;
98
+ };
99
+ /**
100
+ * Pure: may this ticket be redeemed right now?
101
+ *
102
+ * Refuses an unknown, an expired and an ALREADY-REDEEMED ticket with the same
103
+ * wording on purpose. Redemption deletes the ticket, so a used one and an
104
+ * invented one are indistinguishable from here — and saying which would tell a
105
+ * guesser that a ticket once existed. Same reasoning as the user invitation's
106
+ * "this link is no longer valid".
107
+ */
108
+ export declare function redeemVerdict(peers: readonly Peer[], ticket: unknown, now: number): RedeemVerdict;
109
+ /** Pure: consume the ticket — the row stays (that is the peer), the invitation
110
+ * goes. This is what makes the ticket single-use. */
111
+ export declare function redeemPeer(peers: readonly Peer[], name: string, now: number): Peer[];
37
112
  /** Pure: drop a peer by name (revocation). */
38
113
  export declare function removePeer(peers: Peer[], name: string): Peer[];
39
- /** The peer token: `<b64url(name)>.<hmac("peer:"+b64)>`, twin of `signSessionKey`. */
40
- export declare function signPeerToken(name: string, secret: Buffer): string;
41
- /** Pure: the name a token attests to by its MAC alone, or null. Does NOT check
42
- * the registry — that is `peerFromToken`. */
43
- export declare function readPeerToken(token: string, secret: Buffer): string | null;
44
- /** The peer a token authenticates: a valid MAC AND a name still in the registry.
45
- * A token whose row was deleted is refused — that is the revocation. */
46
- export declare function peerFromToken(token: string, secret: Buffer, peers: Peer[]): string | null;
114
+ /**
115
+ * The peer token: `<b64url(name)>.<hmac("peer:"+b64+":"+salt)>`, twin of
116
+ * `signSessionKey` but SALTED per peer — see the note at the top of this file.
117
+ */
118
+ export declare function signPeerToken(name: string, secret: Buffer, salt: string): string;
119
+ /** Pure: the name a token CLAIMS, from its first half. Proves nothing on its
120
+ * own — the MAC is checked in `peerFromToken`, which needs the row's salt. */
121
+ export declare function peerTokenName(token: unknown): string | null;
122
+ /**
123
+ * The peer a token authenticates, or null.
124
+ *
125
+ * Three conditions, and each one is a different refusal:
126
+ * - the name is still in the registry — deleting the row is the revocation;
127
+ * - that row has ENROLLED (no pending invite left). A durable token for a peer
128
+ * that never redeemed its ticket authenticates nothing, which is what keeps
129
+ * the ticket the only way in;
130
+ * - the MAC verifies against THAT ROW's salt, so a token minted before a
131
+ * re-invitation is dead.
132
+ */
133
+ export declare function peerFromToken(token: string, secret: Buffer, peers: readonly Peer[]): string | null;
47
134
  /**
48
135
  * The ownership rule — a peer may only touch an agent IT created. The single
49
136
  * named check behind every peer scope (resume, stop, /diff): a channel with a
@@ -54,13 +141,18 @@ export declare function ownedByPeer(channel: {
54
141
  } | undefined, peer: string): boolean;
55
142
  /**
56
143
  * The copy-paste invitation for an AGENT — the peer equivalent of a user's
57
- * single-use link. The invitee is an agent, not a human, so instead of a page
58
- * where it picks a password it gets a PROMPT to paste into its own session: the
59
- * cockpit URL, its key, the brief, and exactly how to join and report back.
144
+ * single-use link, and single-use in the same way. The invitee is an agent, not a
145
+ * human, so instead of a page where it picks a password it gets a PROMPT to paste
146
+ * into its own session: the cockpit URL, a TICKET, the brief, and exactly how to
147
+ * join and report back.
148
+ *
149
+ * It carries the ticket and never the durable token, which is the point: an
150
+ * invitation is copied, pasted and kept in a transcript, so what it holds must
151
+ * be worthless once used.
60
152
  * `brief` is the human's task (empty allowed). Pure so the wording is tested.
61
153
  */
62
154
  export declare function agentInvitePrompt(opts: {
63
155
  url: string;
64
- token: string;
156
+ ticket: string;
65
157
  brief?: string;
66
158
  }): string;
package/dist/peers.js CHANGED
@@ -1,8 +1,11 @@
1
- import { createHmac, timingSafeEqual } from "node:crypto";
1
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { instanceKey } from "./paths.js";
6
+ /** How long a ticket stays redeemable. Long enough to reach an agent that is
7
+ * mid-task, short enough that a forgotten invitation is not a standing door. */
8
+ export const PEER_INVITE_TTL_MS = 7 * 24 * 60 * 60_000;
6
9
  /** INBOUND registry, per launch dir, 600 — peers allowed to reach THIS instance. */
7
10
  export function peerFileFor(cwd = process.cwd()) {
8
11
  return path.join(os.homedir(), ".shadok-ai", "peers", instanceKey(cwd) + ".json");
@@ -40,36 +43,92 @@ export function normPeerName(name) {
40
43
  .replace(/^-+|-+$/g, "");
41
44
  return s || null;
42
45
  }
43
- /** Pure: add (or supersede) a peer by name. */
44
- export function addPeer(peers, name, now, note) {
45
- const row = { name, createdAt: now, ...(note ? { note } : {}) };
46
+ /**
47
+ * Pure: add (or supersede) a peer by name, with a FRESH salt and a fresh
48
+ * single-use ticket.
49
+ *
50
+ * Superseding re-rolls the salt on purpose: re-inviting a name is exactly when
51
+ * the old credential must stop working. `salt`/`ticket` are injected so the
52
+ * result is testable; `newPeer` below supplies real randomness.
53
+ */
54
+ export function addPeer(peers, name, now, note, salt, ticket, ttlMs = PEER_INVITE_TTL_MS) {
55
+ const row = {
56
+ name,
57
+ createdAt: now,
58
+ ...(note ? { note } : {}),
59
+ salt,
60
+ invite: { ticket, expiresAt: now + ttlMs },
61
+ };
46
62
  return [...peers.filter((p) => p.name !== name), row];
47
63
  }
64
+ /** Impure twin of `addPeer`: draws the salt and the ticket. */
65
+ export function newPeer(peers, name, now, note) {
66
+ const ticket = randomBytes(32).toString("base64url");
67
+ return { peers: addPeer(peers, name, now, note, randomBytes(16).toString("hex"), ticket), ticket };
68
+ }
69
+ /**
70
+ * Backfill a salt on any row written before salts existed.
71
+ *
72
+ * Doing this INVALIDATES that row's existing token, and that is the point: the
73
+ * saltless token is the deterministic one being removed. A peer whose token
74
+ * stops working is re-invited; a peer whose leaked token kept working would not
75
+ * know to.
76
+ */
77
+ export function withSalts(peers, mint = () => randomBytes(16).toString("hex")) {
78
+ return peers.map((p) => (p.salt ? p : { ...p, salt: mint() }));
79
+ }
80
+ /**
81
+ * Pure: may this ticket be redeemed right now?
82
+ *
83
+ * Refuses an unknown, an expired and an ALREADY-REDEEMED ticket with the same
84
+ * wording on purpose. Redemption deletes the ticket, so a used one and an
85
+ * invented one are indistinguishable from here — and saying which would tell a
86
+ * guesser that a ticket once existed. Same reasoning as the user invitation's
87
+ * "this link is no longer valid".
88
+ */
89
+ export function redeemVerdict(peers, ticket, now) {
90
+ const t = typeof ticket === "string" ? ticket.trim() : "";
91
+ const gone = { ok: false, error: "this invitation is no longer valid — it may already have been used" };
92
+ if (!t)
93
+ return { ok: false, error: "no invitation ticket" };
94
+ const row = peers.find((p) => p.invite && p.invite.ticket === t);
95
+ if (!row || !row.invite)
96
+ return gone;
97
+ if (row.invite.expiresAt <= now)
98
+ return gone;
99
+ return { ok: true, name: row.name };
100
+ }
101
+ /** Pure: consume the ticket — the row stays (that is the peer), the invitation
102
+ * goes. This is what makes the ticket single-use. */
103
+ export function redeemPeer(peers, name, now) {
104
+ return peers.map((p) => {
105
+ if (p.name !== name)
106
+ return p;
107
+ const { invite: _used, ...rest } = p;
108
+ return { ...rest, enrolledAt: now };
109
+ });
110
+ }
48
111
  /** Pure: drop a peer by name (revocation). */
49
112
  export function removePeer(peers, name) {
50
113
  return peers.filter((p) => p.name !== name);
51
114
  }
52
- /** The peer token: `<b64url(name)>.<hmac("peer:"+b64)>`, twin of `signSessionKey`. */
53
- export function signPeerToken(name, secret) {
115
+ /**
116
+ * The peer token: `<b64url(name)>.<hmac("peer:"+b64+":"+salt)>`, twin of
117
+ * `signSessionKey` but SALTED per peer — see the note at the top of this file.
118
+ */
119
+ export function signPeerToken(name, secret, salt) {
54
120
  const n = Buffer.from(name, "utf8").toString("base64url");
55
- return `${n}.${createHmac("sha256", secret).update(`peer:${n}`).digest("hex")}`;
121
+ return `${n}.${createHmac("sha256", secret).update(`peer:${n}:${salt}`).digest("hex")}`;
56
122
  }
57
- /** Pure: the name a token attests to by its MAC alone, or null. Does NOT check
58
- * the registry — that is `peerFromToken`. */
59
- export function readPeerToken(token, secret) {
123
+ /** Pure: the name a token CLAIMS, from its first half. Proves nothing on its
124
+ * own — the MAC is checked in `peerFromToken`, which needs the row's salt. */
125
+ export function peerTokenName(token) {
60
126
  const parts = String(token ?? "").split(".");
61
127
  if (parts.length !== 2)
62
128
  return null;
63
129
  const [n, mac] = parts;
64
130
  if (!n || !mac)
65
131
  return null;
66
- const want = createHmac("sha256", secret).update(`peer:${n}`).digest("hex");
67
- // Length first: timingSafeEqual THROWS on a length mismatch (invariant in
68
- // readSessionKey), which would be a 500 instead of a clean refusal.
69
- if (mac.length !== want.length)
70
- return null;
71
- if (!timingSafeEqual(Buffer.from(mac), Buffer.from(want)))
72
- return null;
73
132
  try {
74
133
  return Buffer.from(n, "base64url").toString("utf8") || null;
75
134
  }
@@ -77,13 +136,33 @@ export function readPeerToken(token, secret) {
77
136
  return null;
78
137
  }
79
138
  }
80
- /** The peer a token authenticates: a valid MAC AND a name still in the registry.
81
- * A token whose row was deleted is refused — that is the revocation. */
139
+ /**
140
+ * The peer a token authenticates, or null.
141
+ *
142
+ * Three conditions, and each one is a different refusal:
143
+ * - the name is still in the registry — deleting the row is the revocation;
144
+ * - that row has ENROLLED (no pending invite left). A durable token for a peer
145
+ * that never redeemed its ticket authenticates nothing, which is what keeps
146
+ * the ticket the only way in;
147
+ * - the MAC verifies against THAT ROW's salt, so a token minted before a
148
+ * re-invitation is dead.
149
+ */
82
150
  export function peerFromToken(token, secret, peers) {
83
- const name = readPeerToken(token, secret);
84
- if (!name)
151
+ const claimed = peerTokenName(token);
152
+ if (!claimed)
153
+ return null;
154
+ const row = peers.find((p) => p.name === claimed);
155
+ if (!row || !row.salt || row.invite)
156
+ return null;
157
+ const mac = String(token).split(".")[1] ?? "";
158
+ const want = createHmac("sha256", secret).update(`peer:${Buffer.from(claimed, "utf8").toString("base64url")}:${row.salt}`).digest("hex");
159
+ // Length first: timingSafeEqual THROWS on a length mismatch (invariant in
160
+ // readSessionKey), which would be a 500 instead of a clean refusal.
161
+ if (mac.length !== want.length)
162
+ return null;
163
+ if (!timingSafeEqual(Buffer.from(mac), Buffer.from(want)))
85
164
  return null;
86
- return peers.some((p) => p.name === name) ? name : null;
165
+ return row.name;
87
166
  }
88
167
  /**
89
168
  * The ownership rule — a peer may only touch an agent IT created. The single
@@ -95,9 +174,14 @@ export function ownedByPeer(channel, peer) {
95
174
  }
96
175
  /**
97
176
  * The copy-paste invitation for an AGENT — the peer equivalent of a user's
98
- * single-use link. The invitee is an agent, not a human, so instead of a page
99
- * where it picks a password it gets a PROMPT to paste into its own session: the
100
- * cockpit URL, its key, the brief, and exactly how to join and report back.
177
+ * single-use link, and single-use in the same way. The invitee is an agent, not a
178
+ * human, so instead of a page where it picks a password it gets a PROMPT to paste
179
+ * into its own session: the cockpit URL, a TICKET, the brief, and exactly how to
180
+ * join and report back.
181
+ *
182
+ * It carries the ticket and never the durable token, which is the point: an
183
+ * invitation is copied, pasted and kept in a transcript, so what it holds must
184
+ * be worthless once used.
101
185
  * `brief` is the human's task (empty allowed). Pure so the wording is tested.
102
186
  */
103
187
  export function agentInvitePrompt(opts) {
@@ -107,11 +191,13 @@ export function agentInvitePrompt(opts) {
107
191
  "You've been invited to collaborate with a shadok-ai cockpit as a peer agent.",
108
192
  "",
109
193
  `Cockpit: ${url}`,
110
- `Your peer key: ${opts.token}`,
111
- "(Keep the key secret — it authenticates you as this peer, and it can be revoked at any time.)",
194
+ `Your invitation ticket: ${opts.ticket}`,
195
+ "(Single use: `peer add` exchanges it for your own durable key, and the ticket",
196
+ " dies at that moment. Keep the key you get back secret — it authenticates you",
197
+ " as this peer, and it can be revoked at any time.)",
112
198
  "",
113
199
  "To join, use the shadok-ai-agents skill (pilotctl):",
114
- ` pilotctl peer add host ${url} ${opts.token}`,
200
+ ` pilotctl peer add host ${url} ${opts.ticket}`,
115
201
  " pilotctl spawn --peer host --cwd /workspace # start an agent on the cockpit",
116
202
  ' pilotctl prompt <id> "<your work>" --peer host # drive it',
117
203
  " pilotctl diff <id> --peer host # read what it changed",
package/dist/peers.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"peers.js","sourceRoot":"","sources":["../src/peers.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"peers.js","sourceRoot":"","sources":["../src/peers.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -37,7 +37,7 @@ import { deliverWithRetry } from "./reload.js";
37
37
  import { startTelegram, renameTelegramTopic, closeTelegramTopic, probeToken, isStalePreface, announceLoggedOut, resetLoggedOutNotice, attachmentPrompt, pasteFileName, MEDIA_DIR, } from "./telegram.js";
38
38
  import { migrateTgBindings } from "./channels.js";
39
39
  import { instanceKey } from "./paths.js";
40
- import { peerFileFor, loadPeers, savePeers, peerFromToken, signPeerToken, normPeerName, addPeer, removePeer, ownedByPeer, peersOutFileFor, agentInvitePrompt, } from "./peers.js";
40
+ import { peerFileFor, loadPeers, savePeers, peerFromToken, signPeerToken, newPeer, withSalts, redeemVerdict, redeemPeer, normPeerName, removePeer, ownedByPeer, peersOutFileFor, agentInvitePrompt, } from "./peers.js";
41
41
  import { loadCrons, saveCrons, upsertCron, removeCron, resolveCronId, nextRunFor, nextRunAfterFailure, isTransient, CRON_MAX_RETRIES, markCronPrompt, normalizeSchedule, onceAt, stateAfterFire, scheduleLabel, cronTimeZone, defaultTimeZone, systemTimeZone, isValidTimeZone, } from "./crons.js";
42
42
  import { markPromptMeta, promptMetaHeader } from "./promptmeta.js";
43
43
  import { runRefusal, isShellMode } from "./bang.js";
@@ -664,7 +664,9 @@ function peerName(req) {
664
664
  const tok = req.headers["x-shadok-peer"];
665
665
  if (typeof tok !== "string" || !tok)
666
666
  return null;
667
- return peerFromToken(tok, signingSecret(), loadPeers(peerFileFor()));
667
+ // `withSalts` so a row written before salts existed is verified against a
668
+ // backfilled one — i.e. its old deterministic token is refused, not honoured.
669
+ return peerFromToken(tok, signingSecret(), withSalts(loadPeers(peerFileFor())));
668
670
  }
669
671
  function passwordMatches(input) {
670
672
  const a = Buffer.from(input);
@@ -857,6 +859,30 @@ app.post("/logout", (_req, res) => {
857
859
  res.setHeader("Set-Cookie", "sk_auth=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0");
858
860
  res.json({ ok: true });
859
861
  });
862
+ /**
863
+ * Redeem a peer invitation: a single-use ticket in, a durable token out.
864
+ *
865
+ * BEFORE the password gate on purpose, and it is the only peer route that is: an
866
+ * invitee has no credential yet, which is the whole point of enrolling. The
867
+ * surface is deliberately one field — the ticket authenticates the call and
868
+ * nothing else is read — and it is narrow in three more ways: the ticket is
869
+ * consumed on success (so a replay fails), it expires, and an unknown, used or
870
+ * expired ticket all get the SAME refusal, so a guesser learns nothing about
871
+ * which tickets ever existed.
872
+ *
873
+ * Twin of `POST /invite/:token` for humans.
874
+ */
875
+ app.post("/peers/enrol", (req, res) => {
876
+ const file = peerFileFor();
877
+ const peers = withSalts(loadPeers(file));
878
+ const v = redeemVerdict(peers, req.body?.ticket, Date.now());
879
+ if (!v.ok)
880
+ return res.status(400).json({ error: v.error });
881
+ const row = peers.find((p) => p.name === v.name);
882
+ savePeers(file, redeemPeer(peers, v.name, Date.now()));
883
+ console.log(`peers: ${v.name} enrolled (ticket consumed)`);
884
+ res.json({ ok: true, name: v.name, token: signPeerToken(v.name, signingSecret(), row.salt) });
885
+ });
860
886
  // Who am I? The client labels itself with this, and a tab whose session expired
861
887
  // finds out here rather than by a silent failure.
862
888
  app.get("/me", (req, res) => {
@@ -1435,17 +1461,19 @@ app.post("/peers", (req, res) => {
1435
1461
  const note = typeof req.body?.note === "string" ? req.body.note.trim() || undefined : undefined;
1436
1462
  const brief = typeof req.body?.brief === "string" ? req.body.brief : "";
1437
1463
  const file = peerFileFor();
1438
- savePeers(file, addPeer(loadPeers(file), name, Date.now(), note));
1439
- console.log(`peers: ${me.name} added peer ${name}`);
1440
- // The token is shown ONCE. It is derived (HMAC), so re-adding the peer re-mints
1441
- // the SAME token — it is never stored to hand back later, like an invitation.
1442
- // `invitePrompt` is the "invite an agent like a user" gesture: a ready-to-paste
1443
- // brief the invitee drops into its own session (endpoint + key + task + how to
1444
- // join). Its URL is the one the ADMIN is reaching us at (their browser origin),
1445
- // which is the public URL an external agent would use too.
1446
- const token = signPeerToken(name, signingSecret());
1464
+ const { peers, ticket } = newPeer(loadPeers(file), name, Date.now(), note);
1465
+ savePeers(file, peers);
1466
+ console.log(`peers: ${me.name} invited peer ${name}`);
1467
+ // What is handed out is a SINGLE-USE TICKET, not the credential — the peer
1468
+ // exchanges it at `POST /peers/enrol` for a durable token it then keeps. So the
1469
+ // long-lived credential never travels through this response, the invite prompt,
1470
+ // a chat, or the invitee's transcript; only a ticket that dies on first use
1471
+ // does. Exactly how a user's invitation link works (`/invite/:token`).
1472
+ // Re-inviting the same name re-rolls the salt, so any earlier token is dead.
1473
+ // The URL is the one the ADMIN is reaching us at (their browser origin), which
1474
+ // is the public URL an external agent would use too.
1447
1475
  const url = (req.headers.origin?.trim() || `http://${req.headers.host ?? "localhost"}`).replace(/\/+$/, "");
1448
- res.json({ ok: true, name, token, invitePrompt: agentInvitePrompt({ url, token, brief }) });
1476
+ res.json({ ok: true, name, ticket, invitePrompt: agentInvitePrompt({ url, ticket, brief }) });
1449
1477
  });
1450
1478
  app.delete("/peers", (req, res) => {
1451
1479
  const me = accountAdmin(req, res);