opencode-remote-control 0.2.7 → 0.2.8

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.
@@ -0,0 +1,95 @@
1
+ // Authorization management for OpenCode Remote Control
2
+ // First user to send /start becomes the owner automatically
3
+ const authState = {
4
+ telegramOwner: null,
5
+ feishuOwner: null,
6
+ };
7
+ // Auth file path for persistence
8
+ import { homedir } from 'os';
9
+ import { join } from 'path';
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
11
+ const AUTH_DIR = join(homedir(), '.opencode-remote');
12
+ const AUTH_FILE = join(AUTH_DIR, 'auth.json');
13
+ function ensureAuthDir() {
14
+ if (!existsSync(AUTH_DIR)) {
15
+ mkdirSync(AUTH_DIR, { recursive: true });
16
+ }
17
+ }
18
+ function loadAuth() {
19
+ try {
20
+ if (existsSync(AUTH_FILE)) {
21
+ const data = JSON.parse(readFileSync(AUTH_FILE, 'utf-8'));
22
+ authState.telegramOwner = data.telegramOwner || null;
23
+ authState.feishuOwner = data.feishuOwner || null;
24
+ }
25
+ }
26
+ catch (error) {
27
+ console.warn('Failed to load auth state, starting fresh:', error);
28
+ }
29
+ }
30
+ function saveAuth() {
31
+ try {
32
+ ensureAuthDir();
33
+ writeFileSync(AUTH_FILE, JSON.stringify(authState, null, 2));
34
+ }
35
+ catch (error) {
36
+ console.error('Failed to save auth state:', error);
37
+ }
38
+ }
39
+ // Initialize on module load
40
+ loadAuth();
41
+ export function isAuthorized(platform, userId) {
42
+ if (platform === 'telegram') {
43
+ return authState.telegramOwner === userId;
44
+ }
45
+ else {
46
+ return authState.feishuOwner === userId;
47
+ }
48
+ }
49
+ export function hasOwner(platform) {
50
+ if (platform === 'telegram') {
51
+ return authState.telegramOwner !== null;
52
+ }
53
+ else {
54
+ return authState.feishuOwner !== null;
55
+ }
56
+ }
57
+ export function claimOwnership(platform, userId) {
58
+ if (platform === 'telegram') {
59
+ if (authState.telegramOwner) {
60
+ if (authState.telegramOwner === userId) {
61
+ return { success: true, message: 'already_owner' };
62
+ }
63
+ return { success: false, message: 'already_claimed' };
64
+ }
65
+ authState.telegramOwner = userId;
66
+ saveAuth();
67
+ return { success: true, message: 'claimed' };
68
+ }
69
+ else {
70
+ if (authState.feishuOwner) {
71
+ if (authState.feishuOwner === userId) {
72
+ return { success: true, message: 'already_owner' };
73
+ }
74
+ return { success: false, message: 'already_claimed' };
75
+ }
76
+ authState.feishuOwner = userId;
77
+ saveAuth();
78
+ return { success: true, message: 'claimed' };
79
+ }
80
+ }
81
+ export function getOwner(platform) {
82
+ if (platform === 'telegram') {
83
+ return authState.telegramOwner;
84
+ }
85
+ else {
86
+ return authState.feishuOwner;
87
+ }
88
+ }
89
+ // For debugging/display
90
+ export function getAuthStatus() {
91
+ return {
92
+ telegram: authState.telegramOwner !== null,
93
+ feishu: authState.feishuOwner !== null,
94
+ };
95
+ }
@@ -5,6 +5,7 @@ import { initSessionManager, getOrCreateSession } from '../core/session.js';
5
5
  import { splitMessage } from '../core/notifications.js';
6
6
  import { EMOJI } from '../core/types.js';
7
7
  import { initOpenCode, createSession, sendMessage, checkConnection } from '../opencode/client.js';
8
+ import { isAuthorized, hasOwner, claimOwnership, getAuthStatus } from '../core/auth.js';
8
9
  let feishuClient = null;
9
10
  let wsClient = null;
10
11
  let config = null;
@@ -82,9 +83,33 @@ async function handleCommand(adapter, ctx, text) {
82
83
  const parts = text.split(/\s+/);
83
84
  const command = parts[0].toLowerCase();
84
85
  switch (command) {
85
- case '/start':
86
- case '/help':
87
- await adapter.reply(ctx.threadId, `šŸš€ OpenCode Remote Control ready
86
+ case '/start': {
87
+ const result = claimOwnership('feishu', ctx.userId);
88
+ if (result.success) {
89
+ if (result.message === 'claimed') {
90
+ await adapter.reply(ctx.threadId, `šŸ” **Security Setup Complete!**
91
+
92
+ āœ… You are now the authorized owner of this bot.
93
+
94
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
95
+ āš ļø **IMPORTANT SECURITY NOTICE**
96
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
97
+
98
+ Only YOU can control OpenCode through this bot.
99
+ Other users will be blocked automatically.
100
+
101
+ Your Feishu ID: \`${ctx.userId}\`
102
+
103
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
104
+
105
+ šŸš€ **Ready to use!**
106
+ šŸ’¬ Send me a prompt to start coding
107
+ /help — see all commands
108
+ /status — check OpenCode connection`);
109
+ }
110
+ else {
111
+ // Already owner
112
+ await adapter.reply(ctx.threadId, `šŸš€ OpenCode Remote Control ready
88
113
 
89
114
  šŸ’¬ Send me a prompt to start coding
90
115
  /help — see all commands
@@ -100,6 +125,31 @@ Commands:
100
125
  /files — List changed files
101
126
  /retry — Retry connection
102
127
 
128
+ šŸ’¬ Anything else is treated as a prompt for OpenCode!`);
129
+ }
130
+ }
131
+ else {
132
+ // Already claimed by someone else
133
+ await adapter.reply(ctx.threadId, `🚫 **Access Denied**
134
+
135
+ This bot is already secured by another user.
136
+
137
+ If you are the owner, check your configuration.`);
138
+ }
139
+ break;
140
+ }
141
+ case '/help':
142
+ await adapter.reply(ctx.threadId, `šŸ“– Commands
143
+
144
+ /start — Claim ownership & Start bot
145
+ /status — Check connection
146
+ /reset — Reset session
147
+ /approve — Approve pending changes
148
+ /reject — Reject pending changes
149
+ /diff — See full diff
150
+ /files — List changed files
151
+ /retry — Retry connection
152
+
103
153
  šŸ’¬ Anything else is treated as a prompt for OpenCode!`);
104
154
  break;
105
155
  case '/approve': {
@@ -180,9 +230,26 @@ async function handleMessage(adapter, ctx, text) {
180
230
  const session = getOrCreateSession(ctx.threadId, 'feishu');
181
231
  // Check if it's a command
182
232
  if (text.startsWith('/')) {
233
+ // Commands are handled by handleCommand which has its own auth logic for /start
183
234
  await handleCommand(adapter, ctx, text);
184
235
  return;
185
236
  }
237
+ // Authorization check for non-command messages
238
+ if (!isAuthorized('feishu', ctx.userId)) {
239
+ if (!hasOwner('feishu')) {
240
+ await adapter.reply(ctx.threadId, `šŸ” **Authorization Required**
241
+
242
+ This bot is not yet secured.
243
+
244
+ Please send /start to claim ownership first.`);
245
+ }
246
+ else {
247
+ await adapter.reply(ctx.threadId, `🚫 **Access Denied**
248
+
249
+ You are not authorized to use this bot.`);
250
+ }
251
+ return;
252
+ }
186
253
  // Check OpenCode connection
187
254
  const connected = await checkConnection();
188
255
  if (!connected) {
@@ -347,6 +414,25 @@ export async function startFeishuBot(botConfig) {
347
414
  console.log('✨ Long connection mode - NO tunnel/ngrok required!');
348
415
  console.log(' Just make sure your computer can access the internet.');
349
416
  console.log('');
417
+ // Show security status
418
+ const authStatus = getAuthStatus();
419
+ if (!authStatus.feishu) {
420
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
421
+ console.log(' šŸ” SECURITY NOTICE');
422
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
423
+ console.log('');
424
+ console.log(' Bot is NOT yet secured!');
425
+ console.log(' The FIRST user to send /start will become the owner.');
426
+ console.log('');
427
+ console.log(' šŸ‘‰ Open Feishu and send /start to YOUR bot NOW!');
428
+ console.log('');
429
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
430
+ console.log('');
431
+ }
432
+ else {
433
+ console.log('šŸ”’ Bot is secured (owner authorized)');
434
+ console.log('');
435
+ }
350
436
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
351
437
  console.log(' šŸ“‹ Configuration Checklist');
352
438
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
@@ -4,6 +4,7 @@ import { loadConfig } from '../core/types.js';
4
4
  import { initSessionManager, getOrCreateSession } from '../core/session.js';
5
5
  import { splitMessage } from '../core/notifications.js';
6
6
  import { initOpenCode, createSession, sendMessage, checkConnection } from '../opencode/client.js';
7
+ import { isAuthorized, hasOwner, claimOwnership, getAuthStatus } from '../core/auth.js';
7
8
  // Lazy initialization - bot is only created when startBot() is called
8
9
  let config = null;
9
10
  let bot = null;
@@ -18,11 +19,47 @@ function getThreadId(ctx) {
18
19
  function setupBotCommands(bot, openCodeSessions) {
19
20
  // Start command
20
21
  bot.command('start', async (ctx) => {
21
- await ctx.reply(`šŸš€ OpenCode Remote Control ready
22
+ const userId = String(ctx.from?.id);
23
+ const result = claimOwnership('telegram', userId);
24
+ if (result.success) {
25
+ if (result.message === 'claimed') {
26
+ await ctx.reply(`šŸ” **Security Setup Complete!**
27
+
28
+ āœ… You are now the authorized owner of this bot.
29
+
30
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
31
+ āš ļø **IMPORTANT SECURITY NOTICE**
32
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
33
+
34
+ Only YOU can control OpenCode through this bot.
35
+ Other users will be blocked automatically.
36
+
37
+ Your Telegram ID: \`${userId}\`
38
+
39
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
40
+
41
+ šŸš€ **Ready to use!**
42
+ šŸ’¬ Send me a prompt to start coding
43
+ /help — see all commands
44
+ /status — check OpenCode connection`, { parse_mode: 'Markdown' });
45
+ }
46
+ else {
47
+ // Already owner
48
+ await ctx.reply(`šŸš€ OpenCode Remote Control ready
22
49
 
23
50
  šŸ’¬ Send me a prompt to start coding
24
51
  /help — see all commands
25
52
  /status — check OpenCode connection`);
53
+ }
54
+ }
55
+ else {
56
+ // Already claimed by someone else
57
+ await ctx.reply(`🚫 **Access Denied**
58
+
59
+ This bot is already secured by another user.
60
+
61
+ If you are the owner, check your configuration.`, { parse_mode: 'Markdown' });
62
+ }
26
63
  });
27
64
  // Help command
28
65
  bot.command('help', async (ctx) => {
@@ -132,9 +169,26 @@ Cannot connect to OpenCode server.
132
169
  // Handle all other messages as prompts
133
170
  bot.on('message:text', async (ctx) => {
134
171
  const text = ctx.message.text;
172
+ const userId = String(ctx.from?.id);
135
173
  // Skip if it's a command (already handled)
136
174
  if (text.startsWith('/'))
137
175
  return;
176
+ // Authorization check
177
+ if (!isAuthorized('telegram', userId)) {
178
+ if (!hasOwner('telegram')) {
179
+ await ctx.reply(`šŸ” **Authorization Required**
180
+
181
+ This bot is not yet secured.
182
+
183
+ Please send /start to claim ownership first.`, { parse_mode: 'Markdown' });
184
+ }
185
+ else {
186
+ await ctx.reply(`🚫 **Access Denied**
187
+
188
+ You are not authorized to use this bot.`, { parse_mode: 'Markdown' });
189
+ }
190
+ return;
191
+ }
138
192
  const threadId = getThreadId(ctx);
139
193
  // Send typing indicator
140
194
  await ctx.api.sendChatAction(ctx.chat.id, 'typing');
@@ -236,6 +290,24 @@ export async function startBot() {
236
290
  console.log('Make sure OpenCode is running');
237
291
  }
238
292
  console.log('šŸš€ Starting Telegram bot...');
293
+ // Show security status
294
+ const authStatus = getAuthStatus();
295
+ if (!authStatus.telegram) {
296
+ console.log('');
297
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
298
+ console.log(' šŸ” SECURITY NOTICE');
299
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
300
+ console.log('');
301
+ console.log(' Bot is NOT yet secured!');
302
+ console.log(' The FIRST user to send /start will become the owner.');
303
+ console.log('');
304
+ console.log(' šŸ‘‰ Open Telegram and send /start to YOUR bot NOW!');
305
+ console.log('');
306
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
307
+ }
308
+ else {
309
+ console.log('šŸ”’ Bot is secured (owner authorized)');
310
+ }
239
311
  // Handle graceful shutdown
240
312
  const shutdown = async () => {
241
313
  console.log('\nšŸ›‘ Shutting down Telegram bot...');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-remote-control",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Control OpenCode from anywhere via Telegram or Feishu",
5
5
  "type": "module",
6
6
  "bin": {