keyline-cli 1.0.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 +60 -0
- package/keyline.js +577 -0
- package/package.json +30 -0
- package/update_schema.sql +1 -0
- package/website/README.md +36 -0
- package/website/app/favicon.ico +0 -0
- package/website/app/globals.css +137 -0
- package/website/app/layout.tsx +29 -0
- package/website/app/page.tsx +345 -0
- package/website/eslint.config.mjs +18 -0
- package/website/next.config.ts +7 -0
- package/website/package-lock.json +6619 -0
- package/website/package.json +29 -0
- package/website/postcss.config.mjs +7 -0
- package/website/public/file.svg +1 -0
- package/website/public/globe.svg +1 -0
- package/website/public/next.svg +1 -0
- package/website/public/vercel.svg +1 -0
- package/website/public/window.svg +1 -0
- package/website/tsconfig.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# KeyLine
|
|
2
|
+
|
|
3
|
+
> **Secure Terminal Communication Protocol**
|
|
4
|
+
|
|
5
|
+
KeyLine is a persistent, encrypted chat protocol for the terminal. No browsers. No trackers. Just raw, distributed messaging for the modern node.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🔒 **Secure**: End-to-end encrypted messaging (via Supabase RLS).
|
|
12
|
+
- 🆔 **Identity**: Unique alphanumeric IDs (e.g., `ABC1234`).
|
|
13
|
+
- ⚡ **Real-time**: Instant message delivery using Supabase Realtime.
|
|
14
|
+
- 🎨 **Hacker UX**: Premium retro-terminal interface.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
You can run KeyLine directly using `npx` or install it globally.
|
|
19
|
+
|
|
20
|
+
### Option 1: Quick Run (Recommended)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx keyline
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Option 2: Global Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install -g keyline
|
|
30
|
+
keyline
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration (Optional)
|
|
34
|
+
|
|
35
|
+
By default, KeyLine connects to the official secure public network. No configuration is required.
|
|
36
|
+
|
|
37
|
+
**Advanced: Self-Hosting**
|
|
38
|
+
If you want to host your own backend:
|
|
39
|
+
1. Create a `.env` file in the directory where you run the tool.
|
|
40
|
+
2. Add your Supabase credentials:
|
|
41
|
+
|
|
42
|
+
```env
|
|
43
|
+
SUPABASE_URL=your_supabase_project_url
|
|
44
|
+
SUPABASE_ANON_KEY=your_supabase_anon_key
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> If you are the host, ensure your Supabase instance has the correct schema applied (see `update_schema.sql`).
|
|
48
|
+
|
|
49
|
+
## Commands
|
|
50
|
+
|
|
51
|
+
- `/help` - Show protocol list
|
|
52
|
+
- `/clear` - Flush display buffer
|
|
53
|
+
- `/info` - Peer identity status
|
|
54
|
+
- `/shrug` - ¯\\_(ツ)_/¯
|
|
55
|
+
- `/flip` - (╯°□°)╯︵ ┻━┻
|
|
56
|
+
- `/nuke` - Emergency disconnect
|
|
57
|
+
|
|
58
|
+
## License
|
|
59
|
+
|
|
60
|
+
ISC
|
package/keyline.js
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { createClient } = require('@supabase/supabase-js');
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const figlet = require('figlet');
|
|
6
|
+
const ora = require('ora');
|
|
7
|
+
const { customAlphabet } = require('nanoid');
|
|
8
|
+
require('dotenv').config();
|
|
9
|
+
|
|
10
|
+
// Default Configuration (KeyLine Public Network)
|
|
11
|
+
const DEFAULT_CONFIG = {
|
|
12
|
+
SUPABASE_URL: 'https://xpzaefdnzddsibxqgwnx.supabase.co',
|
|
13
|
+
SUPABASE_ANON_KEY: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InhwemFlZmRuemRkc2lieHFnd254Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjY5MDk3MDksImV4cCI6MjA4MjQ4NTcwOX0.tyjkDMoC34msnoBOky7JUVuuhEw4m7cLsl86JUTSPQY'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SUPABASE_URL = process.env.SUPABASE_URL || DEFAULT_CONFIG.SUPABASE_URL;
|
|
17
|
+
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || DEFAULT_CONFIG.SUPABASE_ANON_KEY;
|
|
18
|
+
|
|
19
|
+
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
|
|
20
|
+
const nanoid = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 7);
|
|
21
|
+
|
|
22
|
+
const hackerTheme = {
|
|
23
|
+
primary: chalk.greenBright,
|
|
24
|
+
secondary: chalk.green,
|
|
25
|
+
error: chalk.redBright,
|
|
26
|
+
info: chalk.cyan,
|
|
27
|
+
system: chalk.white.dim,
|
|
28
|
+
warning: chalk.yellowBright,
|
|
29
|
+
accent: chalk.magentaBright,
|
|
30
|
+
bg: chalk.bgBlack
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const SLEEP = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
34
|
+
|
|
35
|
+
async function typewriter(text, color = hackerTheme.primary, speed = 10) {
|
|
36
|
+
for (const char of text) {
|
|
37
|
+
process.stdout.write(color(char));
|
|
38
|
+
await SLEEP(speed);
|
|
39
|
+
}
|
|
40
|
+
process.stdout.write('\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function clearConsole() {
|
|
44
|
+
process.stdout.write('\x1Bc');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderBox(content, title = 'SYSTEM') {
|
|
48
|
+
const lines = content.split('\n');
|
|
49
|
+
const width = Math.max(...lines.map(l => l.length), title.length) + 4;
|
|
50
|
+
const border = '═'.repeat(width);
|
|
51
|
+
|
|
52
|
+
console.log(hackerTheme.secondary(`╔═ ${title} ${'═'.repeat(width - title.length - 3)}╗`));
|
|
53
|
+
lines.forEach(line => {
|
|
54
|
+
console.log(hackerTheme.secondary('║ ') + hackerTheme.primary(line.padEnd(width - 2)) + hackerTheme.secondary('║'));
|
|
55
|
+
});
|
|
56
|
+
console.log(hackerTheme.secondary(`╚${border}╝`));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function showBanner() {
|
|
60
|
+
console.log(hackerTheme.primary(figlet.textSync('KeyLine', { horizontalLayout: 'full' })));
|
|
61
|
+
console.log(hackerTheme.system('--- SECURE TERMINAL CONNECTION ESTABLISHED ---\n'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function generateUniqueID() {
|
|
65
|
+
// ID format: 3 letters + 4 numbers (e.g., ABC1234)
|
|
66
|
+
const letters = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 3)();
|
|
67
|
+
const numbers = customAlphabet('0123456789', 4)();
|
|
68
|
+
return `${letters}${numbers}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function main() {
|
|
72
|
+
clearConsole();
|
|
73
|
+
showBanner();
|
|
74
|
+
|
|
75
|
+
// Credentials now guaranteed by defaults
|
|
76
|
+
|
|
77
|
+
const { action } = await inquirer.prompt([
|
|
78
|
+
{
|
|
79
|
+
type: 'list',
|
|
80
|
+
name: 'action',
|
|
81
|
+
message: hackerTheme.primary('Select initial protocol:'),
|
|
82
|
+
choices: [
|
|
83
|
+
{ name: hackerTheme.secondary('[LOGIN] Access existing terminal'), value: 'login' },
|
|
84
|
+
{ name: hackerTheme.secondary('[SIGNUP] Register new node'), value: 'signup' },
|
|
85
|
+
{ name: hackerTheme.error('[EXIT] Terminate session'), value: 'exit' }
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
if (action === 'exit') process.exit(0);
|
|
91
|
+
|
|
92
|
+
if (action === 'signup') {
|
|
93
|
+
await handleSignup();
|
|
94
|
+
} else {
|
|
95
|
+
await handleLogin();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function handleSignup() {
|
|
100
|
+
const { displayName } = await inquirer.prompt([
|
|
101
|
+
{ type: 'input', name: 'displayName', message: hackerTheme.primary('Enter Display Name (or type "back"):') }
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
if (displayName.toLowerCase() === 'back') return main();
|
|
105
|
+
|
|
106
|
+
const { email, password, chatPassword } = await inquirer.prompt([
|
|
107
|
+
{ type: 'input', name: 'email', message: hackerTheme.primary('Enter Email (for identity):') },
|
|
108
|
+
{ type: 'password', name: 'password', message: hackerTheme.primary('Enter Secure Account Password:') },
|
|
109
|
+
{ type: 'password', name: 'chatPassword', message: hackerTheme.warning('Set Chat Access Password (others need this to add you):') }
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const spinner = ora(hackerTheme.info('Encrypting identity and generating ID...')).start();
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const { data: authData, error: authError } = await supabase.auth.signUp({
|
|
116
|
+
email,
|
|
117
|
+
password
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (authError) throw authError;
|
|
121
|
+
|
|
122
|
+
if (authData.session) {
|
|
123
|
+
const uniqueID = await generateUniqueID();
|
|
124
|
+
|
|
125
|
+
const { error: profileError } = await supabase
|
|
126
|
+
.from('profiles')
|
|
127
|
+
.insert([
|
|
128
|
+
{ id: authData.user.id, display_name: displayName, unique_id: uniqueID, chat_password: chatPassword }
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
if (profileError) throw profileError;
|
|
132
|
+
|
|
133
|
+
spinner.succeed(hackerTheme.primary(`Identity registered! Your Unique ID is: ${chalk.white.bold(uniqueID)}`));
|
|
134
|
+
await showMainMenu(authData.user);
|
|
135
|
+
} else {
|
|
136
|
+
spinner.succeed(hackerTheme.primary('Identity registered!'));
|
|
137
|
+
console.log(hackerTheme.warning('\n[SYSTEM] ATTENTION: Verification required.'));
|
|
138
|
+
console.log(hackerTheme.info('Please check your mailbox and verify your email ID.'));
|
|
139
|
+
console.log(hackerTheme.info('Return to this terminal and [LOGIN] after verification.\n'));
|
|
140
|
+
|
|
141
|
+
await inquirer.prompt([{ type: 'input', name: 'continue', message: hackerTheme.system('Press Enter to return to main menu...') }]);
|
|
142
|
+
await main();
|
|
143
|
+
}
|
|
144
|
+
} catch (err) {
|
|
145
|
+
spinner.fail(hackerTheme.error(`Registration failed: ${err.message}`));
|
|
146
|
+
await inquirer.prompt([{ type: 'input', name: 'continue', message: hackerTheme.system('Press Enter to retry...') }]);
|
|
147
|
+
await main();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function handleLogin() {
|
|
152
|
+
const { email } = await inquirer.prompt([
|
|
153
|
+
{ type: 'input', name: 'email', message: hackerTheme.primary('Enter Email (or type "back"):') }
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
if (email.toLowerCase() === 'back') return main();
|
|
157
|
+
|
|
158
|
+
const { password } = await inquirer.prompt([
|
|
159
|
+
{ type: 'password', name: 'password', message: hackerTheme.primary('Enter Password:') }
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
const spinner = ora(hackerTheme.info('Authenticating credentials...')).start();
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const { data, error } = await supabase.auth.signInWithPassword({
|
|
166
|
+
email,
|
|
167
|
+
password
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (error) throw error;
|
|
171
|
+
|
|
172
|
+
spinner.succeed(hackerTheme.primary('Access granted. Welcome back.'));
|
|
173
|
+
await showMainMenu(data.user);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
spinner.fail(hackerTheme.error(`Authentication failed: ${err.message}`));
|
|
176
|
+
await inquirer.prompt([{ type: 'input', name: 'continue', message: hackerTheme.system('Press Enter to retry...') }]);
|
|
177
|
+
await main();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function handleMissingProfile(user) {
|
|
182
|
+
const { displayName, chatPassword } = await inquirer.prompt([
|
|
183
|
+
{ type: 'input', name: 'displayName', message: hackerTheme.primary('Complete your profile. Enter Display Name:') },
|
|
184
|
+
{ type: 'password', name: 'chatPassword', message: hackerTheme.warning('Set Chat Access Password (others need this to add you):') }
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
const spinner = ora(hackerTheme.info('Completing registration...')).start();
|
|
188
|
+
const uniqueID = await generateUniqueID();
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const { error: profileError } = await supabase
|
|
192
|
+
.from('profiles')
|
|
193
|
+
.insert([
|
|
194
|
+
{ id: user.id, display_name: displayName, unique_id: uniqueID, chat_password: chatPassword }
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
if (profileError) throw profileError;
|
|
198
|
+
|
|
199
|
+
spinner.succeed(hackerTheme.primary(`Profile created! Your ID is: ${chalk.white.bold(uniqueID)}`));
|
|
200
|
+
await showMainMenu(user);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
spinner.fail(hackerTheme.error(`Failed to create profile: ${err.message}`));
|
|
203
|
+
await inquirer.prompt([{ type: 'input', name: 'continue', message: hackerTheme.system('Press Enter to retry...') }]);
|
|
204
|
+
await main();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function showMainMenu(user) {
|
|
209
|
+
const { error: profileError, data: profile } = await supabase
|
|
210
|
+
.from('profiles')
|
|
211
|
+
.select('*')
|
|
212
|
+
.eq('id', user.id)
|
|
213
|
+
.single();
|
|
214
|
+
|
|
215
|
+
if (profileError) {
|
|
216
|
+
if (profileError.code === 'PGRST116') {
|
|
217
|
+
console.log(hackerTheme.warning('\n[SYSTEM] Warning: Profile not found for this account.'));
|
|
218
|
+
return handleMissingProfile(user);
|
|
219
|
+
}
|
|
220
|
+
console.log(hackerTheme.error(`[ERROR] Profile retrieval failed: ${profileError.message}`));
|
|
221
|
+
await inquirer.prompt([{ type: 'input', name: 'continue', message: hackerTheme.system('Press Enter to return to main menu...') }]);
|
|
222
|
+
return main();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
clearConsole();
|
|
226
|
+
showBanner();
|
|
227
|
+
console.log(hackerTheme.info(`User: ${profile.display_name} | ID: ${profile.unique_id}\n`));
|
|
228
|
+
|
|
229
|
+
const { action } = await inquirer.prompt([
|
|
230
|
+
{
|
|
231
|
+
type: 'list',
|
|
232
|
+
name: 'action',
|
|
233
|
+
message: hackerTheme.primary('COMMAND MENU:'),
|
|
234
|
+
choices: [
|
|
235
|
+
{ name: hackerTheme.secondary('[SEARCH] Find user by ID'), value: 'search' },
|
|
236
|
+
{ name: hackerTheme.secondary('[REQUESTS] View chat requests'), value: 'requests' },
|
|
237
|
+
{ name: hackerTheme.secondary('[CHATS] My active conversations'), value: 'chats' },
|
|
238
|
+
{ name: hackerTheme.error('[LOGOUT] Disconnect'), value: 'logout' }
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
switch (action) {
|
|
244
|
+
case 'search':
|
|
245
|
+
await handleSearch(profile);
|
|
246
|
+
break;
|
|
247
|
+
case 'requests':
|
|
248
|
+
await handleRequests(profile);
|
|
249
|
+
break;
|
|
250
|
+
case 'chats':
|
|
251
|
+
await handleChats(profile);
|
|
252
|
+
break;
|
|
253
|
+
case 'logout':
|
|
254
|
+
await supabase.auth.signOut();
|
|
255
|
+
await main();
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function handleSearch(profile) {
|
|
261
|
+
const { targetID } = await inquirer.prompt([
|
|
262
|
+
{ type: 'input', name: 'targetID', message: hackerTheme.primary('Enter Unique ID to search (or type "back"):') }
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
if (targetID.toLowerCase() === 'back') return showMainMenu({ id: profile.id });
|
|
266
|
+
|
|
267
|
+
if (targetID === profile.unique_id) {
|
|
268
|
+
console.log(hackerTheme.error('[ERROR] Cannot search for yourself.'));
|
|
269
|
+
return showMainMenu({ id: profile.id });
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const spinner = ora(hackerTheme.info('Pinging target ID...')).start();
|
|
273
|
+
|
|
274
|
+
try {
|
|
275
|
+
const { data: targetProfile, error } = await supabase
|
|
276
|
+
.from('profiles')
|
|
277
|
+
.select('*')
|
|
278
|
+
.eq('unique_id', targetID)
|
|
279
|
+
.single();
|
|
280
|
+
|
|
281
|
+
if (error || !targetProfile) {
|
|
282
|
+
spinner.fail(hackerTheme.error('ID not found in network.'));
|
|
283
|
+
} else {
|
|
284
|
+
spinner.stop();
|
|
285
|
+
// Check if relationship already exists
|
|
286
|
+
const { data: existing, error: checkError } = await supabase
|
|
287
|
+
.from('chat_requests')
|
|
288
|
+
.select('*')
|
|
289
|
+
.or(`and(sender_id.eq.${profile.id},receiver_id.eq.${targetProfile.id}),and(sender_id.eq.${targetProfile.id},receiver_id.eq.${profile.id})`)
|
|
290
|
+
.single();
|
|
291
|
+
|
|
292
|
+
if (existing) {
|
|
293
|
+
if (existing.status === 'accepted') {
|
|
294
|
+
console.log(hackerTheme.info(`[SYSTEM] Connection with ${targetProfile.display_name} already exists.`));
|
|
295
|
+
} else if (existing.sender_id === profile.id) {
|
|
296
|
+
console.log(hackerTheme.info(`[SYSTEM] Signal already transmitted. Waiting for ${targetProfile.display_name} to accept.`));
|
|
297
|
+
} else {
|
|
298
|
+
console.log(hackerTheme.warning(`[SYSTEM] ${targetProfile.display_name} has already sent you a request! Check your [REQUESTS].`));
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
console.log(hackerTheme.primary(`Target found: ${targetProfile.display_name}`));
|
|
302
|
+
|
|
303
|
+
// Chat Password Verification
|
|
304
|
+
if (targetProfile.chat_password) {
|
|
305
|
+
const { inputPassword } = await inquirer.prompt([
|
|
306
|
+
{ type: 'password', name: 'inputPassword', message: hackerTheme.warning(`Enter Chat Access Password for ${targetProfile.unique_id}:`) }
|
|
307
|
+
]);
|
|
308
|
+
|
|
309
|
+
if (inputPassword !== targetProfile.chat_password) {
|
|
310
|
+
console.log(hackerTheme.error('[ACCESS DENIED] Incorrect Chat Password.'));
|
|
311
|
+
return showMainMenu({ id: profile.id });
|
|
312
|
+
}
|
|
313
|
+
console.log(hackerTheme.secondary('[ACCESS GRANTED] Credentials verified.'));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const { confirm } = await inquirer.prompt([
|
|
317
|
+
{
|
|
318
|
+
type: 'confirm',
|
|
319
|
+
name: 'confirm',
|
|
320
|
+
message: hackerTheme.primary(`Send chat request to ${targetProfile.display_name}?`),
|
|
321
|
+
default: true
|
|
322
|
+
}
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
if (confirm) {
|
|
326
|
+
const { error: reqError } = await supabase
|
|
327
|
+
.from('chat_requests')
|
|
328
|
+
.insert([
|
|
329
|
+
{ sender_id: profile.id, receiver_id: targetProfile.id, status: 'pending' }
|
|
330
|
+
]);
|
|
331
|
+
|
|
332
|
+
if (reqError) throw reqError;
|
|
333
|
+
console.log(hackerTheme.primary('Request transmitted successfully.'));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
} catch (err) {
|
|
338
|
+
spinner.fail(hackerTheme.error(`Operation failed: ${err.message}`));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
await showMainMenu({ id: profile.id });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function handleRequests(profile) {
|
|
345
|
+
const spinner = ora(hackerTheme.info('Fetching pending requests...')).start();
|
|
346
|
+
|
|
347
|
+
try {
|
|
348
|
+
const { data: requests, error } = await supabase
|
|
349
|
+
.from('chat_requests')
|
|
350
|
+
.select(`
|
|
351
|
+
id,
|
|
352
|
+
sender_id,
|
|
353
|
+
profiles:sender_id (display_name, unique_id)
|
|
354
|
+
`)
|
|
355
|
+
.eq('receiver_id', profile.id)
|
|
356
|
+
.eq('status', 'pending');
|
|
357
|
+
|
|
358
|
+
if (error) throw error;
|
|
359
|
+
|
|
360
|
+
spinner.stop();
|
|
361
|
+
|
|
362
|
+
if (requests.length === 0) {
|
|
363
|
+
console.log(hackerTheme.info('No pending requests.'));
|
|
364
|
+
} else {
|
|
365
|
+
const { requestAction } = await inquirer.prompt([
|
|
366
|
+
{
|
|
367
|
+
type: 'list',
|
|
368
|
+
name: 'requestAction',
|
|
369
|
+
message: hackerTheme.primary('PENDING REQUESTS:'),
|
|
370
|
+
choices: [
|
|
371
|
+
...requests.map(r => ({
|
|
372
|
+
name: `${r.profiles.display_name} (${r.profiles.unique_id})`,
|
|
373
|
+
value: r
|
|
374
|
+
})),
|
|
375
|
+
{ name: hackerTheme.error('[BACK] Main menu'), value: 'back' }
|
|
376
|
+
]
|
|
377
|
+
}
|
|
378
|
+
]);
|
|
379
|
+
|
|
380
|
+
if (requestAction !== 'back') {
|
|
381
|
+
const { decision } = await inquirer.prompt([
|
|
382
|
+
{
|
|
383
|
+
type: 'list',
|
|
384
|
+
name: 'decision',
|
|
385
|
+
message: `Action for ${requestAction.profiles.display_name}:`,
|
|
386
|
+
choices: [
|
|
387
|
+
{ name: hackerTheme.primary('[ACCEPT] Establish link'), value: 'accepted' },
|
|
388
|
+
{ name: hackerTheme.error('[REJECT] Deny access'), value: 'rejected' },
|
|
389
|
+
{ name: hackerTheme.system('[CANCEL] do nothing'), value: 'cancel' }
|
|
390
|
+
]
|
|
391
|
+
}
|
|
392
|
+
]);
|
|
393
|
+
|
|
394
|
+
if (decision !== 'cancel') {
|
|
395
|
+
const { error: updateError } = await supabase
|
|
396
|
+
.from('chat_requests')
|
|
397
|
+
.update({ status: decision })
|
|
398
|
+
.eq('id', requestAction.id);
|
|
399
|
+
|
|
400
|
+
if (updateError) throw updateError;
|
|
401
|
+
console.log(hackerTheme.primary(`Link status updated to: ${decision}`));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
} catch (err) {
|
|
406
|
+
spinner.fail(hackerTheme.error(`Failed to manage requests: ${err.message}`));
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
await showMainMenu({ id: profile.id });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function handleChats(profile) {
|
|
413
|
+
const spinner = ora(hackerTheme.info('Opening active channels...')).start();
|
|
414
|
+
|
|
415
|
+
try {
|
|
416
|
+
// Get accepted requests where user is sender or receiver
|
|
417
|
+
const { data: links, error } = await supabase
|
|
418
|
+
.from('chat_requests')
|
|
419
|
+
.select(`
|
|
420
|
+
id,
|
|
421
|
+
sender_id,
|
|
422
|
+
receiver_id,
|
|
423
|
+
sender:sender_id (id, display_name, unique_id),
|
|
424
|
+
receiver:receiver_id (id, display_name, unique_id)
|
|
425
|
+
`)
|
|
426
|
+
.or(`sender_id.eq.${profile.id},receiver_id.eq.${profile.id}`)
|
|
427
|
+
.eq('status', 'accepted');
|
|
428
|
+
|
|
429
|
+
if (error) throw error;
|
|
430
|
+
|
|
431
|
+
spinner.stop();
|
|
432
|
+
|
|
433
|
+
if (links.length === 0) {
|
|
434
|
+
console.log(hackerTheme.info('No active chats found. Find someone by ID first!'));
|
|
435
|
+
} else {
|
|
436
|
+
const { chat } = await inquirer.prompt([
|
|
437
|
+
{
|
|
438
|
+
type: 'list',
|
|
439
|
+
name: 'chat',
|
|
440
|
+
message: hackerTheme.primary('ACTIVE CHANNELS:'),
|
|
441
|
+
choices: [
|
|
442
|
+
...links.map(l => {
|
|
443
|
+
const other = l.sender_id === profile.id ? l.receiver : l.sender;
|
|
444
|
+
return { name: `${other.display_name} (${other.unique_id})`, value: other };
|
|
445
|
+
}),
|
|
446
|
+
{ name: hackerTheme.error('[BACK] Main menu'), value: 'back' }
|
|
447
|
+
]
|
|
448
|
+
}
|
|
449
|
+
]);
|
|
450
|
+
|
|
451
|
+
if (chat !== 'back') {
|
|
452
|
+
await startChat(profile, chat);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} catch (err) {
|
|
456
|
+
spinner.fail(hackerTheme.error(`Failed to load chats: ${err.message}`));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
await showMainMenu({ id: profile.id });
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function startChat(me, other) {
|
|
463
|
+
clearConsole();
|
|
464
|
+
showBanner();
|
|
465
|
+
renderBox(`CONNECTION ESTABLISHED\nPEER: ${other.display_name}\nID: ${other.unique_id}\nSTATUS: ENCRYPTED`, 'SECURE_CHANNEL');
|
|
466
|
+
console.log(hackerTheme.system('Type "/help" for protocol list or "/exit" to disconnect.\n'));
|
|
467
|
+
|
|
468
|
+
// Fetch history
|
|
469
|
+
const { data: messages, error: fetchError } = await supabase
|
|
470
|
+
.from('messages')
|
|
471
|
+
.select('*')
|
|
472
|
+
.or(`and(sender_id.eq.${me.id},receiver_id.eq.${other.id}),and(sender_id.eq.${other.id},receiver_id.eq.${me.id})`)
|
|
473
|
+
.order('created_at', { ascending: true });
|
|
474
|
+
|
|
475
|
+
if (!fetchError) {
|
|
476
|
+
messages.forEach(msg => {
|
|
477
|
+
const label = msg.sender_id === me.id ? hackerTheme.primary('[YOU]') : hackerTheme.secondary(`[${other.display_name}]`);
|
|
478
|
+
console.log(`${label}: ${msg.content}`);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Subscribe to new messages
|
|
483
|
+
const channel = supabase
|
|
484
|
+
.channel(`chat_${me.id}_${other.id}`)
|
|
485
|
+
.on('postgres_changes', {
|
|
486
|
+
event: 'INSERT',
|
|
487
|
+
schema: 'public',
|
|
488
|
+
table: 'messages',
|
|
489
|
+
filter: `receiver_id=eq.${me.id}`
|
|
490
|
+
}, payload => {
|
|
491
|
+
if (payload.new.sender_id === other.id) {
|
|
492
|
+
console.log(`${hackerTheme.secondary(`[${other.display_name}]`)}: ${payload.new.content}`);
|
|
493
|
+
}
|
|
494
|
+
})
|
|
495
|
+
.subscribe();
|
|
496
|
+
|
|
497
|
+
let active = true;
|
|
498
|
+
while (active) {
|
|
499
|
+
const { content } = await inquirer.prompt([
|
|
500
|
+
{ type: 'input', name: 'content', message: hackerTheme.primary('>_ ') }
|
|
501
|
+
]);
|
|
502
|
+
|
|
503
|
+
if (content.startsWith('/')) {
|
|
504
|
+
const cmd = content.split(' ')[0].toLowerCase();
|
|
505
|
+
switch (cmd) {
|
|
506
|
+
case '/exit':
|
|
507
|
+
active = false;
|
|
508
|
+
break;
|
|
509
|
+
case '/help':
|
|
510
|
+
renderBox(
|
|
511
|
+
'/help - Show this manual\n' +
|
|
512
|
+
'/clear - Flush display buffer\n' +
|
|
513
|
+
'/info - Peer identity status\n' +
|
|
514
|
+
'/shrug - Inject ASCII shrug\n' +
|
|
515
|
+
'/flip - Table maneuver\n' +
|
|
516
|
+
'/nuke - Emergency disconnect',
|
|
517
|
+
'COMMAND_PROTOCOLS'
|
|
518
|
+
);
|
|
519
|
+
break;
|
|
520
|
+
case '/clear':
|
|
521
|
+
clearConsole();
|
|
522
|
+
showBanner();
|
|
523
|
+
renderBox(`PEER: ${other.display_name}\nID: ${other.unique_id}`, 'SESSION_RECOVERED');
|
|
524
|
+
break;
|
|
525
|
+
case '/info':
|
|
526
|
+
renderBox(`NAME: ${other.display_name}\nUID: ${other.unique_id}\nLINK_STABILITY: 100%`, 'PEER_INTEL');
|
|
527
|
+
break;
|
|
528
|
+
case '/shrug':
|
|
529
|
+
await sendMessage(me.id, other.id, '¯\\_(ツ)_/¯');
|
|
530
|
+
break;
|
|
531
|
+
case '/flip':
|
|
532
|
+
await sendMessage(me.id, other.id, '(╯°□°)╯︵ ┻━┻');
|
|
533
|
+
break;
|
|
534
|
+
case '/nuke':
|
|
535
|
+
await handleNuke();
|
|
536
|
+
process.exit(0);
|
|
537
|
+
break;
|
|
538
|
+
default:
|
|
539
|
+
console.log(hackerTheme.error(`[SYSTEM] Invalid protocol: ${cmd}`));
|
|
540
|
+
}
|
|
541
|
+
} else if (content.trim()) {
|
|
542
|
+
await sendMessage(me.id, other.id, content);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
supabase.removeChannel(channel);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function sendMessage(senderId, receiverId, content) {
|
|
550
|
+
const { error: sendError } = await supabase
|
|
551
|
+
.from('messages')
|
|
552
|
+
.insert([{ sender_id: senderId, receiver_id: receiverId, content }]);
|
|
553
|
+
|
|
554
|
+
if (sendError) {
|
|
555
|
+
console.log(hackerTheme.error(`[ERROR] Transmission failed: ${sendError.message}`));
|
|
556
|
+
} else {
|
|
557
|
+
// Local echo for self messages if not handled by subscription (subscription only filters for receiver)
|
|
558
|
+
console.log(`${hackerTheme.primary('[YOU]')}: ${content}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function handleNuke() {
|
|
563
|
+
const chars = '█▓▒░/\\<>#!?@$%^&*()';
|
|
564
|
+
const lines = 10;
|
|
565
|
+
for (let i = 0; i < lines; i++) {
|
|
566
|
+
let noise = '';
|
|
567
|
+
for (let j = 0; j < 50; j++) {
|
|
568
|
+
noise += chars[Math.floor(Math.random() * chars.length)];
|
|
569
|
+
}
|
|
570
|
+
console.log(hackerTheme.error(noise));
|
|
571
|
+
await SLEEP(50);
|
|
572
|
+
}
|
|
573
|
+
console.log(hackerTheme.error('\n!!! CRITICAL SYSTEM FAILURE: EMERGENCY SHUTDOWN INITIATED !!!\n'));
|
|
574
|
+
await SLEEP(500);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "keyline-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Secure Terminal Communication Protocol",
|
|
5
|
+
"main": "keyline.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"keyline": "./keyline.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"cli",
|
|
14
|
+
"chat",
|
|
15
|
+
"secure",
|
|
16
|
+
"terminal"
|
|
17
|
+
],
|
|
18
|
+
"author": "KeyLine Inc.",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"type": "commonjs",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@supabase/supabase-js": "^2.39.1",
|
|
23
|
+
"chalk": "^4.1.2",
|
|
24
|
+
"dotenv": "^16.3.1",
|
|
25
|
+
"figlet": "^1.7.0",
|
|
26
|
+
"inquirer": "^8.2.6",
|
|
27
|
+
"nanoid": "^3.3.7",
|
|
28
|
+
"ora": "^5.4.1"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE profiles ADD COLUMN IF NOT EXISTS chat_password TEXT;
|