shadok-ai 0.16.19 → 0.16.21
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/README.md +10 -1
- package/dist/server.js +83 -6
- package/dist/server.js.map +1 -1
- package/dist/telegram.d.ts +13 -6
- package/dist/telegram.js +68 -35
- package/dist/telegram.js.map +1 -1
- package/dist/tg-pairing.d.ts +38 -0
- package/dist/tg-pairing.js +97 -0
- package/dist/tg-pairing.js.map +1 -0
- package/package.json +1 -1
- package/public/index.html +66 -4
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Telegram pairing code: the ONLY way a Telegram account gets to bind this
|
|
3
|
+
* instance's board group, or to become the owner of its DMs.
|
|
4
|
+
*
|
|
5
|
+
* It replaces two first-come rules. `/setup` bound whichever group sent it
|
|
6
|
+
* first, with no check on the sender, so anyone who found the bot's username
|
|
7
|
+
* could add it to their own group, send `/setup`, and drive agents — i.e. run
|
|
8
|
+
* commands on the host. And DMs belonged to whoever wrote to the bot first.
|
|
9
|
+
* Both now require a code shown in the web cockpit's Telegram panel, which is
|
|
10
|
+
* admin-only: whoever can read the code is someone the cockpit already trusts,
|
|
11
|
+
* and the Telegram account that types it is the one that account vouches for.
|
|
12
|
+
*
|
|
13
|
+
* The code is 8 digits, single-use, lives 10 minutes, and dies after
|
|
14
|
+
* `MAX_ATTEMPTS` wrong guesses — 10^8 codes against 5 tries leaves a guesser
|
|
15
|
+
* nothing. Issuing a new one revokes the previous one. It lives in memory only:
|
|
16
|
+
* a server restart invalidating a 10-minute code costs one click.
|
|
17
|
+
*
|
|
18
|
+
* The state transitions are pure (`redeemPairing`), so the rules are tested
|
|
19
|
+
* without a clock or a bot.
|
|
20
|
+
*/
|
|
21
|
+
import { randomInt, timingSafeEqual } from "node:crypto";
|
|
22
|
+
export const PAIRING_DIGITS = 8;
|
|
23
|
+
export const PAIRING_TTL_MS = 10 * 60_000;
|
|
24
|
+
export const PAIRING_MAX_ATTEMPTS = 5;
|
|
25
|
+
/** A fresh code. `rand` is injectable for the tests. */
|
|
26
|
+
export function newPairing(now, rand = randomInt) {
|
|
27
|
+
let code = "";
|
|
28
|
+
for (let i = 0; i < PAIRING_DIGITS; i++)
|
|
29
|
+
code += String(rand(10));
|
|
30
|
+
return { code, expiresAt: now + PAIRING_TTL_MS, failures: 0 };
|
|
31
|
+
}
|
|
32
|
+
/** Pull the code out of what was typed: tolerant of spaces and dashes
|
|
33
|
+
* ("1234 5678", "1234-5678"), since people copy codes the way they read them. */
|
|
34
|
+
export function normalizeCode(input) {
|
|
35
|
+
return input.replace(/[\s-]/g, "");
|
|
36
|
+
}
|
|
37
|
+
function sameCode(a, b) {
|
|
38
|
+
const x = Buffer.from(a);
|
|
39
|
+
const y = Buffer.from(b);
|
|
40
|
+
return x.length === y.length && timingSafeEqual(x, y);
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Try a code against the current state. Returns the verdict and the state to
|
|
44
|
+
* keep: `null` once the code is spent (used, expired or locked out), so nothing
|
|
45
|
+
* can be retried against a dead code. Pure — unit tested.
|
|
46
|
+
*/
|
|
47
|
+
export function redeemPairing(state, input, now) {
|
|
48
|
+
if (!state)
|
|
49
|
+
return { verdict: "none", next: null };
|
|
50
|
+
if (now >= state.expiresAt)
|
|
51
|
+
return { verdict: "expired", next: null };
|
|
52
|
+
if (sameCode(normalizeCode(input), state.code))
|
|
53
|
+
return { verdict: "ok", next: null };
|
|
54
|
+
const failures = state.failures + 1;
|
|
55
|
+
if (failures >= PAIRING_MAX_ATTEMPTS)
|
|
56
|
+
return { verdict: "locked", next: null };
|
|
57
|
+
return { verdict: "invalid", next: { ...state, failures } };
|
|
58
|
+
}
|
|
59
|
+
/** What a refused redemption tells the Telegram sender. Never says how many
|
|
60
|
+
* tries are left: that is information for the guesser, not for the owner. */
|
|
61
|
+
export function pairingRefusal(verdict) {
|
|
62
|
+
const where = "Open the cockpit's Telegram panel (admin) and click “Pair Telegram” to get a code.";
|
|
63
|
+
switch (verdict) {
|
|
64
|
+
case "none":
|
|
65
|
+
return `🔒 This bot is not paired yet. ${where}`;
|
|
66
|
+
case "expired":
|
|
67
|
+
return `⌛ That code has expired. ${where}`;
|
|
68
|
+
case "locked":
|
|
69
|
+
return `⛔ Too many wrong codes — that code is cancelled. ${where}`;
|
|
70
|
+
case "invalid":
|
|
71
|
+
return "❌ Wrong code.";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// The live code. One per process: the server and the bridge share it.
|
|
75
|
+
let current = null;
|
|
76
|
+
/** Issue a new code, revoking any previous one. */
|
|
77
|
+
export function issuePairingCode(now = Date.now()) {
|
|
78
|
+
current = newPairing(now);
|
|
79
|
+
return current;
|
|
80
|
+
}
|
|
81
|
+
/** Redeem against the live code, advancing its state. */
|
|
82
|
+
export function redeemPairingCode(input, now = Date.now()) {
|
|
83
|
+
const { verdict, next } = redeemPairing(current, input, now);
|
|
84
|
+
current = next;
|
|
85
|
+
return verdict;
|
|
86
|
+
}
|
|
87
|
+
/** Revoke the live code, if any (the panel's "cancel"). */
|
|
88
|
+
export function cancelPairingCode() {
|
|
89
|
+
current = null;
|
|
90
|
+
}
|
|
91
|
+
/** Is a code live right now, and until when? For the panel, never the code. */
|
|
92
|
+
export function pairingStatus(now = Date.now()) {
|
|
93
|
+
if (current && now < current.expiresAt)
|
|
94
|
+
return { active: true, expiresAt: current.expiresAt };
|
|
95
|
+
return { active: false, expiresAt: null };
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=tg-pairing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tg-pairing.js","sourceRoot":"","sources":["../src/tg-pairing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -1066,6 +1066,9 @@
|
|
|
1066
1066
|
.tg-guide ol { margin: 8px 0 0; padding-left: 20px; font-size: 13px; line-height: 1.5; }
|
|
1067
1067
|
.tg-guide li { margin-bottom: 4px; }
|
|
1068
1068
|
.tg-guide code { font-size: 12px; }
|
|
1069
|
+
.tg-pair { margin-bottom: 12px; }
|
|
1070
|
+
.tg-pair-code { font: 600 26px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 3px; margin: 6px 0; user-select: all; }
|
|
1071
|
+
.tg-pair-links { display: flex; gap: 8px; flex-wrap: wrap; margin: 6px 0; }
|
|
1069
1072
|
/* A field with no effect in the current mode (resume/continue) — visible but inert. */
|
|
1070
1073
|
.na { opacity: .45; pointer-events: none; }
|
|
1071
1074
|
|
|
@@ -2958,11 +2961,23 @@
|
|
|
2958
2961
|
<li>Paste that token below and click <b>Save</b>. The status above turns 🟢 with the bot's name.</li>
|
|
2959
2962
|
<li>Create a Telegram <b>group</b>, open its settings and turn on <b>Topics</b>.</li>
|
|
2960
2963
|
<li>Add your bot to the group and make it an <b>admin</b> with <i>Manage topics</i> (and <i>Delete messages</i>, so <code>/secret</code> can scrub values).</li>
|
|
2961
|
-
<li>
|
|
2964
|
+
<li>Click <b>Pair Telegram</b> below, then send <code>/setup <code></code> in the group (or use the one-click link). The status above now shows the bound group, and you own the bot's DMs.</li>
|
|
2962
2965
|
<li>Send <code>/spawn <name></code> (or create a topic by hand): each topic is one agent, also visible here as a tab.</li>
|
|
2963
2966
|
</ol>
|
|
2964
2967
|
<p class="check-hint">Optional: list chat ids under <i>Allowed chats</i> so only they can reach the bot.</p>
|
|
2965
2968
|
</details>
|
|
2969
|
+
<div class="tg-pair">
|
|
2970
|
+
<button id="tgPair" title="Show a one-time code: only the Telegram account that enters it can bind the board group or own the bot's DMs">Pair Telegram</button>
|
|
2971
|
+
<div id="tgPairBox" hidden>
|
|
2972
|
+
<div class="tg-pair-code" id="tgPairCode"></div>
|
|
2973
|
+
<p class="check-hint">Send <code>/setup <span id="tgPairCodeInline"></span></code> in the group to make it the board, or in a private chat with the bot to own its DMs. Single use · <span id="tgPairLeft"></span></p>
|
|
2974
|
+
<div class="tg-pair-links">
|
|
2975
|
+
<a id="tgPairGroup" target="_blank" rel="noopener noreferrer" hidden>Add to a group ↗</a>
|
|
2976
|
+
<a id="tgPairDm" target="_blank" rel="noopener noreferrer" hidden>Open a private chat ↗</a>
|
|
2977
|
+
<button id="tgPairCancel">Cancel code</button>
|
|
2978
|
+
</div>
|
|
2979
|
+
</div>
|
|
2980
|
+
</div>
|
|
2966
2981
|
<button id="tgUnbind" hidden title="Unbind the board group so you can /setup a fresh one (e.g. after it became a supergroup)">Unbind group</button>
|
|
2967
2982
|
<label class="check"><input type="checkbox" id="tgEnabled"><span>Bot enabled</span></label>
|
|
2968
2983
|
<div class="field">
|
|
@@ -4319,7 +4334,9 @@
|
|
|
4319
4334
|
// The Telegram mirror. The label states the action AND its cost: turning it
|
|
4320
4335
|
// off deletes the topic, hence the conversation on the phone — so we confirm.
|
|
4321
4336
|
// The main channel (the General) is the board's home base: not un-mirrorable.
|
|
4322
|
-
|
|
4337
|
+
// Admin-only, like the rest of the Telegram surface (the server refuses a
|
|
4338
|
+
// member's mirror write regardless; this just doesn't offer it).
|
|
4339
|
+
if (!tab.isMain && me?.role === "admin") {
|
|
4323
4340
|
items.push([
|
|
4324
4341
|
tab.mirror ? "✈️ Stop mirroring (deletes the topic)" : "✈️ Mirror to Telegram",
|
|
4325
4342
|
() => {
|
|
@@ -10240,6 +10257,14 @@
|
|
|
10240
10257
|
// Every signed-in PERSON opens it — a member manages their own passkeys
|
|
10241
10258
|
// there; only an admin sees the accounts below.
|
|
10242
10259
|
if (b) b.hidden = !(me?.role === "admin" || me?.name);
|
|
10260
|
+
// The Telegram bot config (header ✈️ button) and the "Mirror to Telegram"
|
|
10261
|
+
// creation checkbox are ADMIN-ONLY — the server enforces it (GET/PUT
|
|
10262
|
+
// /telegram, and the `start` / PUT /channels mirror gate); hiding the
|
|
10263
|
+
// controls just avoids offering a member a door that 403s. The agent-menu
|
|
10264
|
+
// mirror item is gated where that menu is built.
|
|
10265
|
+
const amAdmin = me?.role === "admin";
|
|
10266
|
+
const tgb = $("telegramBtn"); if (tgb) tgb.hidden = !amAdmin;
|
|
10267
|
+
const mf = $("mirrorField"); if (mf) mf.hidden = !amAdmin;
|
|
10243
10268
|
// No password on this instance means no session at all: nothing to name and
|
|
10244
10269
|
// nothing to leave. Showing "Sign out" there would promise a door that does
|
|
10245
10270
|
// not exist.
|
|
@@ -10740,7 +10765,7 @@
|
|
|
10740
10765
|
tgAllowed = s.allowedChats.slice();
|
|
10741
10766
|
const dot = s.running && s.username ? "🟢" : s.tokenError ? "🟠" : "⚪";
|
|
10742
10767
|
const who = s.username ? s.username : s.hasToken ? "(token set)" : "no token";
|
|
10743
|
-
const grp = s.boundGroup ? ` · group ${s.boundGroup.chatId}` : " · no group bound (
|
|
10768
|
+
const grp = s.boundGroup ? ` · group ${s.boundGroup.chatId}` : " · no group bound (pair it below)";
|
|
10744
10769
|
const err = s.tokenError ? ` — ${s.tokenError}` : "";
|
|
10745
10770
|
$("tgStatus").textContent = `${dot} ${who}${grp}${err}`;
|
|
10746
10771
|
$("tgUnbind").hidden = !s.boundGroup; // only offer unbind when a group is bound
|
|
@@ -10767,6 +10792,41 @@
|
|
|
10767
10792
|
ul.appendChild(li);
|
|
10768
10793
|
}
|
|
10769
10794
|
}
|
|
10795
|
+
// Pairing code: shown once, as issued — the server never returns it again.
|
|
10796
|
+
let tgPairTimer = null;
|
|
10797
|
+
function hidePairing() {
|
|
10798
|
+
clearInterval(tgPairTimer);
|
|
10799
|
+
tgPairTimer = null;
|
|
10800
|
+
$("tgPairBox").hidden = true;
|
|
10801
|
+
}
|
|
10802
|
+
function showPairing(p) {
|
|
10803
|
+
$("tgPairCode").textContent = p.code.slice(0, 4) + " " + p.code.slice(4);
|
|
10804
|
+
$("tgPairCodeInline").textContent = p.code;
|
|
10805
|
+
for (const [id, href] of [["tgPairGroup", p.groupLink], ["tgPairDm", p.dmLink]]) {
|
|
10806
|
+
const a = $(id);
|
|
10807
|
+
a.hidden = !href;
|
|
10808
|
+
if (href) a.href = href;
|
|
10809
|
+
}
|
|
10810
|
+
$("tgPairBox").hidden = false;
|
|
10811
|
+
const tick = () => {
|
|
10812
|
+
const left = Math.max(0, p.expiresAt - Date.now());
|
|
10813
|
+
if (left === 0) { hidePairing(); return; }
|
|
10814
|
+
const m = Math.floor(left / 60000), sec = Math.floor((left % 60000) / 1000);
|
|
10815
|
+
$("tgPairLeft").textContent = `expires in ${m}:${String(sec).padStart(2, "0")}`;
|
|
10816
|
+
};
|
|
10817
|
+
clearInterval(tgPairTimer);
|
|
10818
|
+
tick();
|
|
10819
|
+
tgPairTimer = setInterval(tick, 1000);
|
|
10820
|
+
}
|
|
10821
|
+
async function issuePairing() {
|
|
10822
|
+
const r = await fetch("/telegram/pairing", { method: "POST" });
|
|
10823
|
+
if (r.ok) showPairing(await r.json());
|
|
10824
|
+
else alert((await r.json().catch(() => ({}))).error || "could not issue a pairing code");
|
|
10825
|
+
}
|
|
10826
|
+
async function cancelPairing() {
|
|
10827
|
+
await fetch("/telegram/pairing", { method: "DELETE" });
|
|
10828
|
+
hidePairing();
|
|
10829
|
+
}
|
|
10770
10830
|
async function loadTelegram() {
|
|
10771
10831
|
delete $("tgGuide").dataset.auto; // each opening of the panel re-decides the fold
|
|
10772
10832
|
renderTelegram(await (await fetch("/telegram")).json());
|
|
@@ -11174,7 +11234,9 @@
|
|
|
11174
11234
|
// Backdrop click closes a dismissable (voluntary) card; the panel itself does not.
|
|
11175
11235
|
$("authOverlay").addEventListener("click", (e) => { if (e.target === $("authOverlay")) closeAuthCard(); });
|
|
11176
11236
|
|
|
11177
|
-
$("
|
|
11237
|
+
$("tgPair").addEventListener("click", issuePairing);
|
|
11238
|
+
$("tgPairCancel").addEventListener("click", cancelPairing);
|
|
11239
|
+
$("tgUnbind").addEventListener("click", () => { if (confirm("Unbind the current board group? You'll need to pair a group again (Pair Telegram, then /setup <code>).")) putTelegram({ unbindGroup: true }); });
|
|
11178
11240
|
$("telegramBtn").addEventListener("click", () => { $("telegramOverlay").hidden = false; loadTelegram(); });
|
|
11179
11241
|
$("telegramClose").addEventListener("click", () => { $("telegramOverlay").hidden = true; });
|
|
11180
11242
|
$("telegramOverlay").addEventListener("click", (e) => { if (e.target === $("telegramOverlay")) $("telegramOverlay").hidden = true; });
|