icoa-cli 1.3.3 → 1.3.4
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/index.js +1 -1
- package/dist/lib/access.d.ts +3 -1
- package/dist/lib/access.js +70 -4
- package/dist/repl.js +30 -6
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -36,7 +36,7 @@ ${B} ${B}
|
|
|
36
36
|
${B} ${chalk.white('Sydney, Australia')} ${chalk.gray('Jun 27 - Jul 2, 2026')} ${B}
|
|
37
37
|
${B} ${chalk.cyan.underline('https://icoa2026.au')} ${B}
|
|
38
38
|
${B} ${B}
|
|
39
|
-
${B} ${chalk.gray('CLI-Native Competition Terminal v1.3.
|
|
39
|
+
${B} ${chalk.gray('CLI-Native Competition Terminal v1.3.4')} ${B}
|
|
40
40
|
${B} ${B}
|
|
41
41
|
${chalk.cyan('╚══════════════════════════════════════════════════════════╝')}
|
|
42
42
|
`;
|
package/dist/lib/access.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export declare function validateToken(token: string): boolean;
|
|
2
2
|
export declare function isActivated(): boolean;
|
|
3
|
-
export
|
|
3
|
+
export type ActivateResult = 'ok' | 'invalid' | 'already_bound';
|
|
4
|
+
export declare function activateToken(token: string): ActivateResult;
|
|
5
|
+
export declare function isDeviceMatch(): boolean;
|
|
4
6
|
export declare function isFreeCommand(cmd: string): boolean;
|
|
5
7
|
interface SessionState {
|
|
6
8
|
startedAt: string;
|
package/dist/lib/access.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { hostname, platform, arch, networkInterfaces } from 'node:os';
|
|
2
4
|
import { getConfig, saveConfig, getIcoaDir } from './config.js';
|
|
3
5
|
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
4
6
|
import { join } from 'node:path';
|
|
@@ -117,12 +119,76 @@ export function isActivated() {
|
|
|
117
119
|
const config = getConfig();
|
|
118
120
|
return !!(config.accessToken && TOKEN_HASHES.has(hashToken(config.accessToken)));
|
|
119
121
|
}
|
|
122
|
+
function getDeviceFingerprint() {
|
|
123
|
+
const parts = [hostname(), platform(), arch()];
|
|
124
|
+
// Add hardware UUID where possible
|
|
125
|
+
try {
|
|
126
|
+
if (platform() === 'darwin') {
|
|
127
|
+
const out = execSync('ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID', { encoding: 'utf-8' });
|
|
128
|
+
const match = out.match(/"([A-F0-9-]+)"/);
|
|
129
|
+
if (match)
|
|
130
|
+
parts.push(match[1]);
|
|
131
|
+
}
|
|
132
|
+
else if (platform() === 'linux') {
|
|
133
|
+
if (existsSync('/etc/machine-id')) {
|
|
134
|
+
parts.push(readFileSync('/etc/machine-id', 'utf-8').trim());
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if (platform() === 'win32') {
|
|
138
|
+
const out = execSync('wmic csproduct get uuid', { encoding: 'utf-8' });
|
|
139
|
+
const lines = out.trim().split('\n');
|
|
140
|
+
if (lines.length > 1)
|
|
141
|
+
parts.push(lines[1].trim());
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Fallback: use first non-internal MAC address
|
|
146
|
+
const nets = networkInterfaces();
|
|
147
|
+
for (const ifaces of Object.values(nets)) {
|
|
148
|
+
if (!ifaces)
|
|
149
|
+
continue;
|
|
150
|
+
for (const iface of ifaces) {
|
|
151
|
+
if (!iface.internal && iface.mac !== '00:00:00:00:00:00') {
|
|
152
|
+
parts.push(iface.mac);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (parts.length > 3)
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return createHash('sha256').update(parts.join('|')).digest('hex');
|
|
161
|
+
}
|
|
120
162
|
export function activateToken(token) {
|
|
121
|
-
if (validateToken(token))
|
|
122
|
-
|
|
123
|
-
|
|
163
|
+
if (!validateToken(token))
|
|
164
|
+
return 'invalid';
|
|
165
|
+
const fingerprint = getDeviceFingerprint();
|
|
166
|
+
const bindingFile = join(getIcoaDir(), 'token-bindings.json');
|
|
167
|
+
// Load existing bindings
|
|
168
|
+
let bindings = {};
|
|
169
|
+
if (existsSync(bindingFile)) {
|
|
170
|
+
try {
|
|
171
|
+
bindings = JSON.parse(readFileSync(bindingFile, 'utf-8'));
|
|
172
|
+
}
|
|
173
|
+
catch { /* ignore */ }
|
|
174
|
+
}
|
|
175
|
+
const tokenHash = hashToken(token);
|
|
176
|
+
const existingDevice = bindings[tokenHash];
|
|
177
|
+
if (existingDevice && existingDevice !== fingerprint) {
|
|
178
|
+
// Token already bound to a different device
|
|
179
|
+
return 'already_bound';
|
|
124
180
|
}
|
|
125
|
-
|
|
181
|
+
// Bind token to this device
|
|
182
|
+
bindings[tokenHash] = fingerprint;
|
|
183
|
+
writeFileSync(bindingFile, JSON.stringify(bindings, null, 2));
|
|
184
|
+
saveConfig({ accessToken: token.trim().toUpperCase(), deviceFingerprint: fingerprint });
|
|
185
|
+
return 'ok';
|
|
186
|
+
}
|
|
187
|
+
export function isDeviceMatch() {
|
|
188
|
+
const config = getConfig();
|
|
189
|
+
if (!config.deviceFingerprint)
|
|
190
|
+
return true; // Legacy: no fingerprint yet
|
|
191
|
+
return config.deviceFingerprint === getDeviceFingerprint();
|
|
126
192
|
}
|
|
127
193
|
export function isFreeCommand(cmd) {
|
|
128
194
|
return FREE_COMMANDS.has(cmd.toLowerCase());
|
package/dist/repl.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { isConnected, getConfig } from './lib/config.js';
|
|
4
|
-
import { isActivated, activateToken, isFreeCommand, recordExit, recordResume } from './lib/access.js';
|
|
4
|
+
import { isActivated, activateToken, isFreeCommand, isDeviceMatch, recordExit, recordResume } from './lib/access.js';
|
|
5
5
|
const INTERCEPT = '__REPL_NO_EXIT__';
|
|
6
6
|
export function startRepl(program, resumeMode) {
|
|
7
7
|
const config = getConfig();
|
|
@@ -21,7 +21,14 @@ export function startRepl(program, resumeMode) {
|
|
|
21
21
|
}
|
|
22
22
|
console.log();
|
|
23
23
|
}
|
|
24
|
-
|
|
24
|
+
// Device mismatch check
|
|
25
|
+
if (activated && !isDeviceMatch()) {
|
|
26
|
+
console.log(chalk.red(' Token was activated on a different device. Access denied.'));
|
|
27
|
+
console.log(chalk.gray(' Contact organizer for assistance.'));
|
|
28
|
+
console.log();
|
|
29
|
+
// Fall through to restricted mode
|
|
30
|
+
}
|
|
31
|
+
else if (connected) {
|
|
25
32
|
console.log(chalk.gray(' Connected to: ') + chalk.white(config.ctfdUrl));
|
|
26
33
|
console.log(chalk.gray(' User: ') + chalk.white(config.userName));
|
|
27
34
|
}
|
|
@@ -78,8 +85,12 @@ export function startRepl(program, resumeMode) {
|
|
|
78
85
|
// Activate token
|
|
79
86
|
if (input.startsWith('activate ')) {
|
|
80
87
|
const token = input.slice(9).trim();
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
const result = activateToken(token);
|
|
89
|
+
if (result === 'ok') {
|
|
90
|
+
console.log(chalk.green(' Access granted! Token bound to this device.'));
|
|
91
|
+
}
|
|
92
|
+
else if (result === 'already_bound') {
|
|
93
|
+
console.log(chalk.red(' Token already activated on another device.'));
|
|
83
94
|
}
|
|
84
95
|
else {
|
|
85
96
|
console.log(chalk.red(' Invalid token.'));
|
|
@@ -94,9 +105,9 @@ export function startRepl(program, resumeMode) {
|
|
|
94
105
|
rl.prompt();
|
|
95
106
|
return;
|
|
96
107
|
}
|
|
97
|
-
// Token check — only ref allowed without activation
|
|
108
|
+
// Token check — only ref allowed without activation or device mismatch
|
|
98
109
|
const cmd = input.split(/\s+/)[0].toLowerCase();
|
|
99
|
-
if (!isActivated() && !isFreeCommand(cmd)) {
|
|
110
|
+
if ((!isActivated() || !isDeviceMatch()) && !isFreeCommand(cmd)) {
|
|
100
111
|
console.log(chalk.yellow(' Restricted mode. ') + chalk.gray('Enter your access token:'));
|
|
101
112
|
console.log(chalk.white(' activate <token>'));
|
|
102
113
|
console.log();
|
|
@@ -105,6 +116,19 @@ export function startRepl(program, resumeMode) {
|
|
|
105
116
|
rl.prompt();
|
|
106
117
|
return;
|
|
107
118
|
}
|
|
119
|
+
// Check if it's a known command before passing to Commander
|
|
120
|
+
const knownCommands = [
|
|
121
|
+
'join', 'activate', 'challenges', 'ch', 'open', 'submit', 'flag',
|
|
122
|
+
'scoreboard', 'sb', 'status', 'time', 'hint', 'hint-b', 'hint-c',
|
|
123
|
+
'hint-budget', 'ref', 'shell', 'files', 'connect', 'note', 'log',
|
|
124
|
+
'lang', 'setup', 'model', 'ctf',
|
|
125
|
+
];
|
|
126
|
+
if (!knownCommands.includes(cmd)) {
|
|
127
|
+
console.log(chalk.yellow(` Unknown command: ${cmd}. Type 'help' for commands.`));
|
|
128
|
+
console.log();
|
|
129
|
+
rl.prompt();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
108
132
|
processing = true;
|
|
109
133
|
const args = mapCommand(input);
|
|
110
134
|
process.exit = (() => {
|
package/dist/types/index.d.ts
CHANGED
|
@@ -99,6 +99,7 @@ export interface IcoaConfig {
|
|
|
99
99
|
geminiApiKey: string;
|
|
100
100
|
geminiModel: string;
|
|
101
101
|
accessToken: string;
|
|
102
|
+
deviceFingerprint: string;
|
|
102
103
|
}
|
|
103
104
|
export type CompetitionState = 'pre_competition' | 'demo' | 'live' | 'finished' | 'unknown';
|
|
104
105
|
export type HintLevel = 'A' | 'B' | 'C';
|
package/dist/types/index.js
CHANGED