@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.
Files changed (2) hide show
  1. package/dist/cli.js +189 -85
  2. 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.consume(req.token)) {
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
- const response = await handler.handleRequest(req);
19921
- conn.end(JSON.stringify(response));
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 init_socket = () => {};
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 pollForResponse(config2, sent.message_id, nonce);
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 pollForResponse(config2, sent.message_id, nonce);
20091
+ return awaitApproval(config2, sent.message_id, nonce);
20013
20092
  }
20014
- async function pollForResponse(config2, messageId, nonce) {
20015
- const deadline = Date.now() + config2.timeoutMs;
20016
- let offset = 0;
20017
- while (Date.now() < deadline) {
20018
- const remainingMs = deadline - Date.now();
20019
- const pollTimeout = Math.min(Math.floor(remainingMs / 1000), 30);
20020
- if (pollTimeout <= 0) {
20021
- break;
20022
- }
20023
- const updates = await apiCall(config2.botToken, "getUpdates", {
20024
- offset,
20025
- timeout: pollTimeout
20026
- });
20027
- for (const update of updates) {
20028
- offset = update.update_id + 1;
20029
- if (update.callback_query?.data?.startsWith(`${nonce}:`)) {
20030
- const action = update.callback_query.data.slice(nonce.length + 1);
20031
- await apiCall(config2.botToken, "answerCallbackQuery", {
20032
- callback_query_id: update.callback_query.id
20033
- });
20034
- if (action === "approve" || action === "auto_approve") {
20035
- const label2 = action === "auto_approve" ? "Auto-approved" : "Approved";
20036
- await apiCall(config2.botToken, "editMessageText", {
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: `✅ ${label2} at ${new Date().toLocaleTimeString()}`
20119
+ text: `❌ ${label2}: ${reason}`
20040
20120
  });
20041
- return { action };
20121
+ resolve({ action: rejectionAction, reason });
20122
+ return true;
20042
20123
  }
20043
- const label = action === "stop" ? "Stopped" : "Rejected";
20044
- await apiCall(config2.botToken, "editMessageText", {
20045
- chat_id: config2.chatId,
20046
- message_id: messageId,
20047
- text: `❌ ${label}. Reply with a reason:`,
20048
- reply_markup: {
20049
- force_reply: true,
20050
- selective: true
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
- const reason = await pollForTextReply(config2, messageId, offset, deadline);
20054
- await apiCall(config2.botToken, "editMessageText", {
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: `❌ ${label}: ${reason}`
20146
+ text: `✅ ${label2} at ${new Date().toLocaleTimeString()}`
20058
20147
  });
20059
- return {
20060
- action,
20061
- reason
20062
- };
20148
+ resolve({ action });
20149
+ return true;
20063
20150
  }
20064
- }
20065
- }
20066
- await apiCall(config2.botToken, "editMessageText", {
20067
- chat_id: config2.chatId,
20068
- message_id: messageId,
20069
- text: "⏰ Timed out"
20070
- }).catch(() => {});
20071
- return { action: "reject", reason: "permission request timed out" };
20072
- }
20073
- async function pollForTextReply(config2, originalMessageId, startOffset, deadline) {
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
- for (const update of updates) {
20086
- offset = update.update_id + 1;
20087
- if (update.message?.reply_to_message?.message_id === originalMessageId && update.message.text) {
20088
- return update.message.text;
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 expiry = this.tokens.get(token);
20112
- if (expiry === undefined) {
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
- return { telegramBotToken: botToken, telegramChatId: chatId, timeoutMs };
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
- if (secrets.length === 0) {
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(secrets.map(escapeRegExp).join("|"), "g");
20466
+ const pattern = new RegExp(filtered.map(escapeRegExp).join("|"), "g");
20363
20467
  return (input) => input.replace(pattern, "<redacted>");
20364
20468
  }
20365
20469
 
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "@wyattjoh/op-remote",
3
- "version": "0.1.0",
3
+ "version": "0.2.2",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/wyattjoh/op-remote.git"
7
+ },
4
8
  "bin": {
5
9
  "op-remote": "dist/cli.js"
6
10
  },