appback-remoteagent 0.20.1 → 0.21.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/README.md
CHANGED
|
@@ -325,7 +325,7 @@ remoteagent bot add
|
|
|
325
325
|
remoteagent-start
|
|
326
326
|
```
|
|
327
327
|
|
|
328
|
-
The command validates the BotFather token
|
|
328
|
+
The command validates the BotFather token and shows a one-time Telegram `/start` link. Open that link from the account that will own the installation; RemoteAgent detects the sender's numeric user ID and stores it as the owner automatically. The token is entered through a hidden prompt, and the owner ID does not need to be looked up or typed manually.
|
|
329
329
|
|
|
330
330
|
To move installation-wide `/secret` values to another PC, export and import a password-encrypted bundle:
|
|
331
331
|
|
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import process from "node:process";
|
|
5
|
-
import
|
|
6
|
-
import { registerTelegramBot, readConfiguredOwnerId } from "./services/cli-config-service.js";
|
|
6
|
+
import { fetchTelegramBotIdentity, registerTelegramBot, readConfiguredOwnerId, waitForTelegramOwner, } from "./services/cli-config-service.js";
|
|
7
7
|
import { exportSecrets, importSecrets } from "./services/secret-transfer-service.js";
|
|
8
8
|
async function main() {
|
|
9
9
|
const args = process.argv.slice(2);
|
|
@@ -46,9 +46,27 @@ async function addBot(dataDir, args) {
|
|
|
46
46
|
if (args.length > 0) {
|
|
47
47
|
throw new Error(`Unexpected bot argument: ${args[0]}`);
|
|
48
48
|
}
|
|
49
|
+
const identity = await fetchTelegramBotIdentity(token);
|
|
49
50
|
const configuredOwner = await readConfiguredOwnerId(dataDir);
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
let ownerId = ownerOption || configuredOwner;
|
|
52
|
+
if (!ownerId) {
|
|
53
|
+
const startPayload = `ra_${randomBytes(8).toString("hex")}`;
|
|
54
|
+
console.log([
|
|
55
|
+
`Validated @${identity.username} (${identity.id}).`,
|
|
56
|
+
"",
|
|
57
|
+
"Open this Telegram link within 3 minutes to confirm the owner:",
|
|
58
|
+
` https://t.me/${identity.username}?start=${startPayload}`,
|
|
59
|
+
"",
|
|
60
|
+
"Or send this exact command to the bot:",
|
|
61
|
+
` /start ${startPayload}`,
|
|
62
|
+
"",
|
|
63
|
+
"Waiting for owner confirmation...",
|
|
64
|
+
].join("\n"));
|
|
65
|
+
const owner = await waitForTelegramOwner(token, startPayload);
|
|
66
|
+
ownerId = owner.id;
|
|
67
|
+
console.log(`Detected Telegram owner: ${owner.displayName}${owner.username ? ` (@${owner.username})` : ""} (${owner.id})`);
|
|
68
|
+
}
|
|
69
|
+
const result = await registerTelegramBot({ dataDir, token, ownerId, identity });
|
|
52
70
|
console.log([
|
|
53
71
|
`${result.added ? "Registered" : "Updated"} @${result.identity.username} (${result.identity.id}).`,
|
|
54
72
|
`Configured bots: ${result.botCount}`,
|
|
@@ -119,18 +137,6 @@ function takeFlag(args, name) {
|
|
|
119
137
|
args.splice(index, 1);
|
|
120
138
|
return true;
|
|
121
139
|
}
|
|
122
|
-
async function promptVisible(question) {
|
|
123
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
124
|
-
throw new Error(`${question.trim()} is required as a command option in non-interactive mode.`);
|
|
125
|
-
}
|
|
126
|
-
const terminal = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
127
|
-
try {
|
|
128
|
-
return (await terminal.question(question)).trim();
|
|
129
|
-
}
|
|
130
|
-
finally {
|
|
131
|
-
terminal.close();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
140
|
async function promptHidden(question) {
|
|
135
141
|
if (!process.stdin.isTTY || !process.stdout.isTTY || !process.stdin.setRawMode) {
|
|
136
142
|
throw new Error("Interactive hidden input requires a TTY. Use the corresponding --*-file option instead.");
|
|
@@ -101,6 +101,86 @@ export async function fetchTelegramBotIdentity(token) {
|
|
|
101
101
|
username: payload.result.username,
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
|
+
export async function waitForTelegramOwner(token, startPayload, timeoutMs = 180_000) {
|
|
105
|
+
assertBotToken(token);
|
|
106
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(startPayload)) {
|
|
107
|
+
throw new Error("Telegram start payload must use 1-64 URL-safe characters.");
|
|
108
|
+
}
|
|
109
|
+
const startedAt = Date.now();
|
|
110
|
+
let offset = await nextTelegramUpdateOffset(token);
|
|
111
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
112
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
113
|
+
const pollSeconds = Math.max(1, Math.min(15, Math.floor(remainingMs / 1000)));
|
|
114
|
+
const updates = await getTelegramUpdates(token, offset, pollSeconds);
|
|
115
|
+
for (const update of updates) {
|
|
116
|
+
if (typeof update.update_id === "number") {
|
|
117
|
+
offset = Math.max(offset, update.update_id + 1);
|
|
118
|
+
}
|
|
119
|
+
const message = update.message;
|
|
120
|
+
const sender = message?.from;
|
|
121
|
+
if (message?.chat?.type !== "private"
|
|
122
|
+
|| sender?.is_bot
|
|
123
|
+
|| typeof sender?.id !== "number"
|
|
124
|
+
|| message.text?.trim() !== `/start ${startPayload}`) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
id: String(sender.id),
|
|
129
|
+
username: sender.username,
|
|
130
|
+
displayName: [sender.first_name, sender.last_name].filter(Boolean).join(" ") || sender.username || String(sender.id),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
throw new Error("Timed out waiting for the Telegram owner confirmation. Run the command again and use the new /start link.");
|
|
135
|
+
}
|
|
136
|
+
async function nextTelegramUpdateOffset(token) {
|
|
137
|
+
const updates = await getTelegramUpdates(token, undefined, 0);
|
|
138
|
+
return updates.reduce((next, update) => typeof update.update_id === "number" ? Math.max(next, update.update_id + 1) : next, 0);
|
|
139
|
+
}
|
|
140
|
+
async function getTelegramUpdates(token, offset, timeoutSeconds) {
|
|
141
|
+
const args = [
|
|
142
|
+
"-4",
|
|
143
|
+
"-sS",
|
|
144
|
+
"--get",
|
|
145
|
+
"--connect-timeout",
|
|
146
|
+
"10",
|
|
147
|
+
"--max-time",
|
|
148
|
+
String(Math.max(20, timeoutSeconds + 10)),
|
|
149
|
+
"--data-urlencode",
|
|
150
|
+
`timeout=${timeoutSeconds}`,
|
|
151
|
+
"--data-urlencode",
|
|
152
|
+
"limit=100",
|
|
153
|
+
"--data-urlencode",
|
|
154
|
+
'allowed_updates=["message"]',
|
|
155
|
+
];
|
|
156
|
+
if (offset !== undefined) {
|
|
157
|
+
args.push("--data-urlencode", `offset=${offset}`);
|
|
158
|
+
}
|
|
159
|
+
args.push(`https://api.telegram.org/bot${token}/getUpdates`);
|
|
160
|
+
let stdout;
|
|
161
|
+
try {
|
|
162
|
+
const result = await execFileAsync("curl", args);
|
|
163
|
+
stdout = result.stdout;
|
|
164
|
+
if (result.stderr?.trim()) {
|
|
165
|
+
console.error(`curl stderr for Telegram getUpdates: ${result.stderr.trim()}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const detail = error instanceof Error ? error.message.replace(token, "[redacted]") : String(error);
|
|
170
|
+
throw new Error(`Telegram getUpdates request failed over IPv4: ${detail}`);
|
|
171
|
+
}
|
|
172
|
+
let payload;
|
|
173
|
+
try {
|
|
174
|
+
payload = JSON.parse(stdout);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
throw new Error("Telegram getUpdates returned an invalid response.");
|
|
178
|
+
}
|
|
179
|
+
if (!payload.ok || !Array.isArray(payload.result)) {
|
|
180
|
+
throw new Error(payload.description || "Telegram rejected the getUpdates request.");
|
|
181
|
+
}
|
|
182
|
+
return payload.result;
|
|
183
|
+
}
|
|
104
184
|
function assertBotToken(token) {
|
|
105
185
|
if (!isBotToken(token)) {
|
|
106
186
|
throw new Error("Invalid Telegram bot token format.");
|
|
@@ -11,9 +11,11 @@ remoteagent bot add
|
|
|
11
11
|
remoteagent-start
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
`remoteagent bot add` asks for the BotFather token without echoing it and
|
|
14
|
+
`remoteagent bot add` asks for the BotFather token without echoing it and validates the token with Telegram `getMe`. On the first registration it then prints a one-time Telegram `/start` link and waits up to three minutes. Open that link from the Telegram account that will own the installation; RemoteAgent detects and stores that account's numeric user ID automatically. No manual owner ID lookup or entry is required.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
The confirmation update is consumed by the setup command. After `remoteagent-start`, send `/start` to the bot normally to begin using RemoteAgent.
|
|
17
|
+
|
|
18
|
+
For unattended automation, keep sensitive values out of shell history by using a permission-restricted token file and pass the already verified owner ID explicitly:
|
|
17
19
|
|
|
18
20
|
```bash
|
|
19
21
|
chmod 600 /secure/path/telegram-token
|
package/package.json
CHANGED
package/scripts/selftest-cli.mjs
CHANGED
|
@@ -4,7 +4,11 @@ import assert from "node:assert/strict";
|
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
fetchTelegramBotIdentity,
|
|
9
|
+
registerTelegramBot,
|
|
10
|
+
waitForTelegramOwner,
|
|
11
|
+
} from "../dist/services/cli-config-service.js";
|
|
8
12
|
import { exportSecrets, importSecrets } from "../dist/services/secret-transfer-service.js";
|
|
9
13
|
|
|
10
14
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), "remoteagent-cli-selftest-"));
|
|
@@ -17,10 +21,25 @@ const passphrase = "correct-horse-battery-staple";
|
|
|
17
21
|
try {
|
|
18
22
|
const binDir = path.join(root, "bin");
|
|
19
23
|
const curlArgsPath = path.join(root, "curl-args.txt");
|
|
24
|
+
const curlUpdateCallsPath = path.join(root, "curl-update-calls.txt");
|
|
20
25
|
await fs.mkdir(binDir, { recursive: true });
|
|
21
26
|
await fs.writeFile(path.join(binDir, "curl"), `#!/usr/bin/env bash
|
|
22
27
|
printf '%s\\n' "$@" > ${JSON.stringify(curlArgsPath)}
|
|
23
|
-
printf '
|
|
28
|
+
if printf '%s\\n' "$@" | grep -q '/getUpdates'; then
|
|
29
|
+
count=0
|
|
30
|
+
if [ -f ${JSON.stringify(curlUpdateCallsPath)} ]; then
|
|
31
|
+
count="$(cat ${JSON.stringify(curlUpdateCallsPath)})"
|
|
32
|
+
fi
|
|
33
|
+
count=$((count + 1))
|
|
34
|
+
printf '%s' "$count" > ${JSON.stringify(curlUpdateCallsPath)}
|
|
35
|
+
if [ "$count" -eq 1 ]; then
|
|
36
|
+
printf '{"ok":true,"result":[{"update_id":41,"message":{"text":"/start stale_payload","chat":{"id":777,"type":"private"},"from":{"id":777,"is_bot":false,"username":"stale"}}}]}'
|
|
37
|
+
else
|
|
38
|
+
printf '{"ok":true,"result":[{"update_id":42,"message":{"text":"/start ra_selftest","chat":{"id":8202993989,"type":"private"},"from":{"id":8202993989,"is_bot":false,"username":"roy","first_name":"Roy"}}}]}'
|
|
39
|
+
fi
|
|
40
|
+
else
|
|
41
|
+
printf '{"ok":true,"result":{"id":100000,"username":"bootstrap_test_bot"}}'
|
|
42
|
+
fi
|
|
24
43
|
`, { mode: 0o755 });
|
|
25
44
|
const originalPath = process.env.PATH;
|
|
26
45
|
process.env.PATH = `${binDir}:${originalPath ?? ""}`;
|
|
@@ -29,6 +48,19 @@ printf '{"ok":true,"result":{"id":100000,"username":"bootstrap_test_bot"}}'
|
|
|
29
48
|
const curlArgs = await fs.readFile(curlArgsPath, "utf8");
|
|
30
49
|
assert.match(curlArgs, /^-4$/m);
|
|
31
50
|
assert.match(curlArgs, /\/getMe$/m);
|
|
51
|
+
const detectedOwner = await waitForTelegramOwner(
|
|
52
|
+
"100000:abcdefghijklmnopqrstuvwxyz_123456",
|
|
53
|
+
"ra_selftest",
|
|
54
|
+
2_000,
|
|
55
|
+
);
|
|
56
|
+
assert.deepEqual(detectedOwner, {
|
|
57
|
+
id: "8202993989",
|
|
58
|
+
username: "roy",
|
|
59
|
+
displayName: "Roy",
|
|
60
|
+
});
|
|
61
|
+
const ownerCurlArgs = await fs.readFile(curlArgsPath, "utf8");
|
|
62
|
+
assert.match(ownerCurlArgs, /^-4$/m);
|
|
63
|
+
assert.match(ownerCurlArgs, /^offset=42$/m);
|
|
32
64
|
process.env.PATH = originalPath;
|
|
33
65
|
|
|
34
66
|
await fs.mkdir(sourceDataDir, { recursive: true });
|