@wyattjoh/op-remote 0.1.0 → 0.2.2
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/cli.js +189 -85
- package/package.json +5 -1
package/dist/cli.js
CHANGED
|
@@ -19901,7 +19901,15 @@ function createSocketServer(tokens, handler) {
|
|
|
19901
19901
|
}
|
|
19902
19902
|
function handleConnection(conn, tokens, handler) {
|
|
19903
19903
|
const chunks = [];
|
|
19904
|
+
let totalBytes = 0;
|
|
19904
19905
|
let responded = false;
|
|
19906
|
+
const reject = (reason) => {
|
|
19907
|
+
if (responded)
|
|
19908
|
+
return;
|
|
19909
|
+
responded = true;
|
|
19910
|
+
const res = { status: "rejected", reason };
|
|
19911
|
+
conn.end(JSON.stringify(res));
|
|
19912
|
+
};
|
|
19905
19913
|
const respond = async () => {
|
|
19906
19914
|
if (responded)
|
|
19907
19915
|
return;
|
|
@@ -19909,7 +19917,7 @@ function handleConnection(conn, tokens, handler) {
|
|
|
19909
19917
|
try {
|
|
19910
19918
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
19911
19919
|
const req = JSON.parse(raw);
|
|
19912
|
-
if (!tokens.
|
|
19920
|
+
if (!tokens.reserve(req.token)) {
|
|
19913
19921
|
const res = {
|
|
19914
19922
|
status: "rejected",
|
|
19915
19923
|
reason: "invalid or expired token"
|
|
@@ -19917,8 +19925,14 @@ function handleConnection(conn, tokens, handler) {
|
|
|
19917
19925
|
conn.end(JSON.stringify(res));
|
|
19918
19926
|
return;
|
|
19919
19927
|
}
|
|
19920
|
-
|
|
19921
|
-
|
|
19928
|
+
try {
|
|
19929
|
+
const response = await handler.handleRequest(req);
|
|
19930
|
+
tokens.consume(req.token);
|
|
19931
|
+
conn.end(JSON.stringify(response));
|
|
19932
|
+
} catch (err) {
|
|
19933
|
+
tokens.release(req.token);
|
|
19934
|
+
throw err;
|
|
19935
|
+
}
|
|
19922
19936
|
} catch (err) {
|
|
19923
19937
|
const res = {
|
|
19924
19938
|
status: "rejected",
|
|
@@ -19928,6 +19942,12 @@ function handleConnection(conn, tokens, handler) {
|
|
|
19928
19942
|
}
|
|
19929
19943
|
};
|
|
19930
19944
|
conn.on("data", (chunk) => {
|
|
19945
|
+
totalBytes += chunk.length;
|
|
19946
|
+
if (totalBytes > MAX_REQUEST_BYTES) {
|
|
19947
|
+
reject("request too large");
|
|
19948
|
+
conn.destroy();
|
|
19949
|
+
return;
|
|
19950
|
+
}
|
|
19931
19951
|
chunks.push(chunk);
|
|
19932
19952
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
19933
19953
|
try {
|
|
@@ -19939,7 +19959,10 @@ function handleConnection(conn, tokens, handler) {
|
|
|
19939
19959
|
respond();
|
|
19940
19960
|
});
|
|
19941
19961
|
}
|
|
19942
|
-
var
|
|
19962
|
+
var MAX_REQUEST_BYTES;
|
|
19963
|
+
var init_socket = __esm(() => {
|
|
19964
|
+
MAX_REQUEST_BYTES = 1024 * 1024;
|
|
19965
|
+
});
|
|
19943
19966
|
|
|
19944
19967
|
// src/serve/telegram.ts
|
|
19945
19968
|
async function apiCall(token, method, body) {
|
|
@@ -19954,6 +19977,62 @@ async function apiCall(token, method, body) {
|
|
|
19954
19977
|
}
|
|
19955
19978
|
return data.result;
|
|
19956
19979
|
}
|
|
19980
|
+
function isAuthorizedUser(config2, userId) {
|
|
19981
|
+
if (!config2.approverIds || config2.approverIds.length === 0) {
|
|
19982
|
+
return true;
|
|
19983
|
+
}
|
|
19984
|
+
if (userId === undefined) {
|
|
19985
|
+
return false;
|
|
19986
|
+
}
|
|
19987
|
+
return config2.approverIds.includes(userId);
|
|
19988
|
+
}
|
|
19989
|
+
function getPoller(botToken) {
|
|
19990
|
+
let state = pollers.get(botToken);
|
|
19991
|
+
if (state) {
|
|
19992
|
+
return state;
|
|
19993
|
+
}
|
|
19994
|
+
state = { offset: 0, listeners: new Set, running: false };
|
|
19995
|
+
pollers.set(botToken, state);
|
|
19996
|
+
return state;
|
|
19997
|
+
}
|
|
19998
|
+
function registerListener(botToken, listener) {
|
|
19999
|
+
const state = getPoller(botToken);
|
|
20000
|
+
state.listeners.add(listener);
|
|
20001
|
+
if (!state.running) {
|
|
20002
|
+
state.running = true;
|
|
20003
|
+
runPollLoop(botToken, state);
|
|
20004
|
+
}
|
|
20005
|
+
return () => {
|
|
20006
|
+
state.listeners.delete(listener);
|
|
20007
|
+
};
|
|
20008
|
+
}
|
|
20009
|
+
async function runPollLoop(botToken, state) {
|
|
20010
|
+
while (true) {
|
|
20011
|
+
if (state.listeners.size === 0) {
|
|
20012
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
20013
|
+
if (state.listeners.size === 0) {
|
|
20014
|
+
break;
|
|
20015
|
+
}
|
|
20016
|
+
}
|
|
20017
|
+
try {
|
|
20018
|
+
const updates = await apiCall(botToken, "getUpdates", {
|
|
20019
|
+
offset: state.offset,
|
|
20020
|
+
timeout: 30
|
|
20021
|
+
});
|
|
20022
|
+
for (const update of updates) {
|
|
20023
|
+
state.offset = update.update_id + 1;
|
|
20024
|
+
for (const listener of state.listeners) {
|
|
20025
|
+
if (listener(update)) {
|
|
20026
|
+
break;
|
|
20027
|
+
}
|
|
20028
|
+
}
|
|
20029
|
+
}
|
|
20030
|
+
} catch {
|
|
20031
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
20032
|
+
}
|
|
20033
|
+
}
|
|
20034
|
+
state.running = false;
|
|
20035
|
+
}
|
|
19957
20036
|
function buildKeyboard(nonce, buttons) {
|
|
19958
20037
|
return {
|
|
19959
20038
|
inline_keyboard: buttons.map((row) => row.map((btn) => ({
|
|
@@ -19992,7 +20071,7 @@ async function requestRunApproval(config2, opts) {
|
|
|
19992
20071
|
text,
|
|
19993
20072
|
reply_markup: keyboard
|
|
19994
20073
|
});
|
|
19995
|
-
return
|
|
20074
|
+
return awaitApproval(config2, sent.message_id, nonce);
|
|
19996
20075
|
}
|
|
19997
20076
|
async function requestResumeApproval(config2) {
|
|
19998
20077
|
const nonce = crypto.randomUUID().slice(0, 8);
|
|
@@ -20009,89 +20088,87 @@ async function requestResumeApproval(config2) {
|
|
|
20009
20088
|
text,
|
|
20010
20089
|
reply_markup: keyboard
|
|
20011
20090
|
});
|
|
20012
|
-
return
|
|
20091
|
+
return awaitApproval(config2, sent.message_id, nonce);
|
|
20013
20092
|
}
|
|
20014
|
-
|
|
20015
|
-
|
|
20016
|
-
|
|
20017
|
-
|
|
20018
|
-
|
|
20019
|
-
const
|
|
20020
|
-
|
|
20021
|
-
|
|
20022
|
-
|
|
20023
|
-
|
|
20024
|
-
|
|
20025
|
-
|
|
20026
|
-
|
|
20027
|
-
|
|
20028
|
-
|
|
20029
|
-
if (
|
|
20030
|
-
|
|
20031
|
-
|
|
20032
|
-
|
|
20033
|
-
|
|
20034
|
-
|
|
20035
|
-
|
|
20036
|
-
|
|
20093
|
+
function awaitApproval(config2, messageId, nonce) {
|
|
20094
|
+
return new Promise((resolve) => {
|
|
20095
|
+
const deadline = Date.now() + config2.timeoutMs;
|
|
20096
|
+
let waitingForReason = false;
|
|
20097
|
+
let rejectionAction = "reject";
|
|
20098
|
+
const timer = setTimeout(() => {
|
|
20099
|
+
cleanup();
|
|
20100
|
+
apiCall(config2.botToken, "editMessageText", {
|
|
20101
|
+
chat_id: config2.chatId,
|
|
20102
|
+
message_id: messageId,
|
|
20103
|
+
text: "⏰ Timed out"
|
|
20104
|
+
}).catch(() => {});
|
|
20105
|
+
resolve({ action: "reject", reason: "permission request timed out" });
|
|
20106
|
+
}, config2.timeoutMs);
|
|
20107
|
+
const unregister = registerListener(config2.botToken, (update) => {
|
|
20108
|
+
if (Date.now() > deadline) {
|
|
20109
|
+
return false;
|
|
20110
|
+
}
|
|
20111
|
+
if (waitingForReason) {
|
|
20112
|
+
if (update.message?.reply_to_message?.message_id === messageId && update.message.text && isAuthorizedUser(config2, update.message.from?.id)) {
|
|
20113
|
+
const reason = update.message.text;
|
|
20114
|
+
cleanup();
|
|
20115
|
+
const label2 = rejectionAction === "stop" ? "Stopped" : "Rejected";
|
|
20116
|
+
apiCall(config2.botToken, "editMessageText", {
|
|
20037
20117
|
chat_id: config2.chatId,
|
|
20038
20118
|
message_id: messageId,
|
|
20039
|
-
text:
|
|
20119
|
+
text: `❌ ${label2}: ${reason}`
|
|
20040
20120
|
});
|
|
20041
|
-
|
|
20121
|
+
resolve({ action: rejectionAction, reason });
|
|
20122
|
+
return true;
|
|
20042
20123
|
}
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
|
|
20046
|
-
|
|
20047
|
-
|
|
20048
|
-
|
|
20049
|
-
|
|
20050
|
-
|
|
20051
|
-
|
|
20124
|
+
return false;
|
|
20125
|
+
}
|
|
20126
|
+
if (!update.callback_query?.data?.startsWith(`${nonce}:`)) {
|
|
20127
|
+
return false;
|
|
20128
|
+
}
|
|
20129
|
+
if (!isAuthorizedUser(config2, update.callback_query.from?.id)) {
|
|
20130
|
+
apiCall(config2.botToken, "answerCallbackQuery", {
|
|
20131
|
+
callback_query_id: update.callback_query.id,
|
|
20132
|
+
text: "You are not authorized to respond to this request."
|
|
20052
20133
|
});
|
|
20053
|
-
|
|
20054
|
-
|
|
20134
|
+
return true;
|
|
20135
|
+
}
|
|
20136
|
+
const action = update.callback_query.data.slice(nonce.length + 1);
|
|
20137
|
+
apiCall(config2.botToken, "answerCallbackQuery", {
|
|
20138
|
+
callback_query_id: update.callback_query.id
|
|
20139
|
+
});
|
|
20140
|
+
if (action === "approve" || action === "auto_approve") {
|
|
20141
|
+
cleanup();
|
|
20142
|
+
const label2 = action === "auto_approve" ? "Auto-approved" : "Approved";
|
|
20143
|
+
apiCall(config2.botToken, "editMessageText", {
|
|
20055
20144
|
chat_id: config2.chatId,
|
|
20056
20145
|
message_id: messageId,
|
|
20057
|
-
text:
|
|
20146
|
+
text: `✅ ${label2} at ${new Date().toLocaleTimeString()}`
|
|
20058
20147
|
});
|
|
20059
|
-
|
|
20060
|
-
|
|
20061
|
-
reason
|
|
20062
|
-
};
|
|
20148
|
+
resolve({ action });
|
|
20149
|
+
return true;
|
|
20063
20150
|
}
|
|
20064
|
-
|
|
20065
|
-
|
|
20066
|
-
|
|
20067
|
-
|
|
20068
|
-
|
|
20069
|
-
|
|
20070
|
-
|
|
20071
|
-
|
|
20072
|
-
}
|
|
20073
|
-
|
|
20074
|
-
let offset = startOffset;
|
|
20075
|
-
while (Date.now() < deadline) {
|
|
20076
|
-
const remainingMs = deadline - Date.now();
|
|
20077
|
-
const pollTimeout = Math.min(Math.floor(remainingMs / 1000), 30);
|
|
20078
|
-
if (pollTimeout <= 0) {
|
|
20079
|
-
break;
|
|
20080
|
-
}
|
|
20081
|
-
const updates = await apiCall(config2.botToken, "getUpdates", {
|
|
20082
|
-
offset,
|
|
20083
|
-
timeout: pollTimeout
|
|
20151
|
+
rejectionAction = action === "stop" ? "stop" : "reject";
|
|
20152
|
+
waitingForReason = true;
|
|
20153
|
+
const label = action === "stop" ? "Stopped" : "Rejected";
|
|
20154
|
+
apiCall(config2.botToken, "editMessageText", {
|
|
20155
|
+
chat_id: config2.chatId,
|
|
20156
|
+
message_id: messageId,
|
|
20157
|
+
text: `❌ ${label}. Reply with a reason:`,
|
|
20158
|
+
reply_markup: { force_reply: true, selective: true }
|
|
20159
|
+
});
|
|
20160
|
+
return true;
|
|
20084
20161
|
});
|
|
20085
|
-
|
|
20086
|
-
|
|
20087
|
-
|
|
20088
|
-
|
|
20089
|
-
|
|
20090
|
-
}
|
|
20091
|
-
}
|
|
20092
|
-
return "(no reason provided)";
|
|
20162
|
+
const cleanup = () => {
|
|
20163
|
+
clearTimeout(timer);
|
|
20164
|
+
unregister();
|
|
20165
|
+
};
|
|
20166
|
+
});
|
|
20093
20167
|
}
|
|
20094
|
-
var API_BASE = "https://api.telegram.org/bot";
|
|
20168
|
+
var API_BASE = "https://api.telegram.org/bot", pollers;
|
|
20169
|
+
var init_telegram = __esm(() => {
|
|
20170
|
+
pollers = new Map;
|
|
20171
|
+
});
|
|
20095
20172
|
|
|
20096
20173
|
// src/serve/tokens.ts
|
|
20097
20174
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
@@ -20104,20 +20181,41 @@ class TokenStore {
|
|
|
20104
20181
|
}
|
|
20105
20182
|
create() {
|
|
20106
20183
|
const token = randomUUID2();
|
|
20107
|
-
this.tokens.set(token, Date.now() + this.ttlMs);
|
|
20184
|
+
this.tokens.set(token, { expiry: Date.now() + this.ttlMs, reserved: false });
|
|
20108
20185
|
return token;
|
|
20109
20186
|
}
|
|
20110
20187
|
validate(token) {
|
|
20111
|
-
const
|
|
20112
|
-
if (
|
|
20188
|
+
const entry = this.tokens.get(token);
|
|
20189
|
+
if (!entry) {
|
|
20113
20190
|
return false;
|
|
20114
20191
|
}
|
|
20115
|
-
if (Date.now() > expiry) {
|
|
20192
|
+
if (Date.now() > entry.expiry) {
|
|
20116
20193
|
this.tokens.delete(token);
|
|
20117
20194
|
return false;
|
|
20118
20195
|
}
|
|
20119
20196
|
return true;
|
|
20120
20197
|
}
|
|
20198
|
+
reserve(token) {
|
|
20199
|
+
const entry = this.tokens.get(token);
|
|
20200
|
+
if (!entry) {
|
|
20201
|
+
return false;
|
|
20202
|
+
}
|
|
20203
|
+
if (Date.now() > entry.expiry) {
|
|
20204
|
+
this.tokens.delete(token);
|
|
20205
|
+
return false;
|
|
20206
|
+
}
|
|
20207
|
+
if (entry.reserved) {
|
|
20208
|
+
return false;
|
|
20209
|
+
}
|
|
20210
|
+
entry.reserved = true;
|
|
20211
|
+
return true;
|
|
20212
|
+
}
|
|
20213
|
+
release(token) {
|
|
20214
|
+
const entry = this.tokens.get(token);
|
|
20215
|
+
if (entry) {
|
|
20216
|
+
entry.reserved = false;
|
|
20217
|
+
}
|
|
20218
|
+
}
|
|
20121
20219
|
consume(token) {
|
|
20122
20220
|
if (!this.validate(token)) {
|
|
20123
20221
|
return false;
|
|
@@ -20143,7 +20241,9 @@ function readConfig() {
|
|
|
20143
20241
|
throw new Error("REMOTE_OP_TELEGRAM_CHAT_ID is required");
|
|
20144
20242
|
}
|
|
20145
20243
|
const timeoutMs = Number.parseInt(process.env.REMOTE_OP_TIMEOUT ?? "120", 10) * 1000;
|
|
20146
|
-
|
|
20244
|
+
const approverIdsRaw = process.env.REMOTE_OP_TELEGRAM_APPROVER_IDS ?? "";
|
|
20245
|
+
const telegramApproverIds = approverIdsRaw.split(",").map((s) => s.trim()).filter((s) => s.length > 0).map(Number).filter((n) => !Number.isNaN(n));
|
|
20246
|
+
return { telegramBotToken: botToken, telegramChatId: chatId, timeoutMs, telegramApproverIds };
|
|
20147
20247
|
}
|
|
20148
20248
|
async function startServer() {
|
|
20149
20249
|
const config2 = readConfig();
|
|
@@ -20163,7 +20263,8 @@ async function startServer() {
|
|
|
20163
20263
|
const telegramConfig = {
|
|
20164
20264
|
botToken: config2.telegramBotToken,
|
|
20165
20265
|
chatId: config2.telegramChatId,
|
|
20166
|
-
timeoutMs: config2.timeoutMs
|
|
20266
|
+
timeoutMs: config2.timeoutMs,
|
|
20267
|
+
approverIds: config2.telegramApproverIds
|
|
20167
20268
|
};
|
|
20168
20269
|
const result = await requestRunApproval(telegramConfig, {
|
|
20169
20270
|
command: req.command,
|
|
@@ -20228,7 +20329,8 @@ async function startServer() {
|
|
|
20228
20329
|
const telegramConfig = {
|
|
20229
20330
|
botToken: config2.telegramBotToken,
|
|
20230
20331
|
chatId: config2.telegramChatId,
|
|
20231
|
-
timeoutMs: config2.timeoutMs
|
|
20332
|
+
timeoutMs: config2.timeoutMs,
|
|
20333
|
+
approverIds: config2.telegramApproverIds
|
|
20232
20334
|
};
|
|
20233
20335
|
const result = await requestResumeApproval(telegramConfig);
|
|
20234
20336
|
if (result.action === "approve") {
|
|
@@ -20271,6 +20373,7 @@ var init_server3 = __esm(() => {
|
|
|
20271
20373
|
init_stdio2();
|
|
20272
20374
|
init_zod();
|
|
20273
20375
|
init_socket();
|
|
20376
|
+
init_telegram();
|
|
20274
20377
|
init_tokens();
|
|
20275
20378
|
});
|
|
20276
20379
|
|
|
@@ -20356,10 +20459,11 @@ function escapeRegExp(s) {
|
|
|
20356
20459
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20357
20460
|
}
|
|
20358
20461
|
function createMasker(secrets) {
|
|
20359
|
-
|
|
20462
|
+
const filtered = secrets.filter((s) => s.length > 0);
|
|
20463
|
+
if (filtered.length === 0) {
|
|
20360
20464
|
return (input) => input;
|
|
20361
20465
|
}
|
|
20362
|
-
const pattern = new RegExp(
|
|
20466
|
+
const pattern = new RegExp(filtered.map(escapeRegExp).join("|"), "g");
|
|
20363
20467
|
return (input) => input.replace(pattern, "<redacted>");
|
|
20364
20468
|
}
|
|
20365
20469
|
|