icoa-cli 2.19.13 → 2.19.15
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/commands/ctf.js +21 -6
- package/dist/commands/exam.js +50 -16
- package/dist/lib/access.d.ts +1 -0
- package/dist/lib/access.js +1 -1
- package/dist/lib/ctfd-client.d.ts +1 -0
- package/dist/lib/ctfd-client.js +31 -1
- package/dist/lib/demo-exam.js +1 -1
- package/package.json +2 -2
package/dist/commands/ctf.js
CHANGED
|
@@ -176,12 +176,27 @@ export function registerCtfCommands(program) {
|
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
catch (err) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
179
|
+
if (sessionCookie) {
|
|
180
|
+
// Session login succeeded but API test failed — save anyway
|
|
181
|
+
spinner2.succeed('Connected (session mode — limited API)');
|
|
182
|
+
saveConfig({
|
|
183
|
+
ctfdUrl: url,
|
|
184
|
+
token: sessionCookie,
|
|
185
|
+
userName: username,
|
|
186
|
+
sessionCookie: sessionCookie,
|
|
187
|
+
});
|
|
188
|
+
console.log();
|
|
189
|
+
printKeyValue('User', username);
|
|
190
|
+
printWarning('API access limited. Some features may not work.');
|
|
191
|
+
printSuccess('Connection saved.');
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
spinner2.fail('Connection test failed');
|
|
195
|
+
printError(err.message);
|
|
196
|
+
saveConfig({ ctfdUrl: url });
|
|
197
|
+
console.log();
|
|
198
|
+
printInfo('Connection saved. Try: join ' + url);
|
|
199
|
+
}
|
|
185
200
|
}
|
|
186
201
|
});
|
|
187
202
|
// ─── icoa ctf logout ───
|
package/dist/commands/exam.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getExamState, saveExamState, clearExamState, getExamDeadline } from '..
|
|
|
6
6
|
import { logCommand } from '../lib/logger.js';
|
|
7
7
|
import { printSuccess, printError, printWarning, printInfo, printTable, printHeader, printKeyValue, createSpinner, formatCountdown, } from '../lib/ui.js';
|
|
8
8
|
import { t } from '../lib/i18n.js';
|
|
9
|
+
import { getDeviceFingerprint } from '../lib/access.js';
|
|
9
10
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
10
11
|
function drawProgress(percent, label) {
|
|
11
12
|
const width = 30;
|
|
@@ -29,14 +30,17 @@ function checkTimeRemaining() {
|
|
|
29
30
|
return true;
|
|
30
31
|
if (new Date() >= deadline) {
|
|
31
32
|
const state = getExamState();
|
|
32
|
-
if (state
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
if (state) {
|
|
34
|
+
const answered = Object.keys(state.answers).length;
|
|
35
|
+
if (state.session.examId === 'demo-free' || answered === 0) {
|
|
36
|
+
// Demo or unanswered expired exam — auto-clear to avoid deadlock
|
|
37
|
+
clearExamState();
|
|
38
|
+
printWarning('Exam expired and cleared.');
|
|
39
|
+
printInfo('Type: demo to start again.');
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
printError('Time expired! Use "exam submit" to submit your answers.');
|
|
43
|
+
}
|
|
40
44
|
}
|
|
41
45
|
return false;
|
|
42
46
|
}
|
|
@@ -639,12 +643,15 @@ export function registerExamCommand(program) {
|
|
|
639
643
|
const total = state.session.questionCount;
|
|
640
644
|
const unanswered = total - answered;
|
|
641
645
|
if (answered === 0) {
|
|
642
|
-
|
|
643
|
-
|
|
646
|
+
const deadline = getExamDeadline();
|
|
647
|
+
const expired = deadline && new Date() >= deadline;
|
|
648
|
+
if (state.session.examId === 'demo-free' || expired) {
|
|
644
649
|
clearExamState();
|
|
645
|
-
|
|
650
|
+
printWarning('No answers to submit. Exam cleared.');
|
|
651
|
+
printInfo('Type: demo to start again.');
|
|
646
652
|
}
|
|
647
653
|
else {
|
|
654
|
+
printWarning('No answers to submit.');
|
|
648
655
|
printInfo('Answer some questions first: exam q 1');
|
|
649
656
|
}
|
|
650
657
|
return;
|
|
@@ -773,12 +780,35 @@ export function registerExamCommand(program) {
|
|
|
773
780
|
return;
|
|
774
781
|
}
|
|
775
782
|
// Real exam: submit to server
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
783
|
+
// Token-based exam: use direct fetch with exam token
|
|
784
|
+
// CTFd-based exam: use ExamClient
|
|
785
|
+
const examToken = state.session.token;
|
|
779
786
|
try {
|
|
780
787
|
drawProgress(0, 'Uploading answers...');
|
|
781
|
-
|
|
788
|
+
let result;
|
|
789
|
+
if (examToken) {
|
|
790
|
+
// Submit via exam token
|
|
791
|
+
const config = getConfig();
|
|
792
|
+
const serverUrl = config.ctfdUrl || 'https://practice.icoa2026.au';
|
|
793
|
+
const res = await fetch(`${serverUrl}/api/icoa/exams/${state.session.examId}/submit`, {
|
|
794
|
+
method: 'POST',
|
|
795
|
+
headers: { 'Content-Type': 'application/json' },
|
|
796
|
+
body: JSON.stringify({ token: examToken, answers: state.answers }),
|
|
797
|
+
signal: AbortSignal.timeout(10000),
|
|
798
|
+
});
|
|
799
|
+
if (!res.ok) {
|
|
800
|
+
const err = await res.json().catch(() => ({ message: 'Submit failed' }));
|
|
801
|
+
throw new Error(err.message || 'Submit failed');
|
|
802
|
+
}
|
|
803
|
+
const json = await res.json();
|
|
804
|
+
result = json.data;
|
|
805
|
+
}
|
|
806
|
+
else {
|
|
807
|
+
const client = requireExamConnection();
|
|
808
|
+
if (!client)
|
|
809
|
+
return;
|
|
810
|
+
result = await client.submitExam(state.session.examId, state.answers);
|
|
811
|
+
}
|
|
782
812
|
drawProgress(50, 'Grading...');
|
|
783
813
|
await sleep(300);
|
|
784
814
|
drawProgress(100, 'Complete!');
|
|
@@ -853,7 +883,11 @@ export function registerExamCommand(program) {
|
|
|
853
883
|
const res = await fetch(`${serverUrl}/api/icoa/exam-token`, {
|
|
854
884
|
method: 'POST',
|
|
855
885
|
headers: { 'Content-Type': 'application/json' },
|
|
856
|
-
body: JSON.stringify({
|
|
886
|
+
body: JSON.stringify({
|
|
887
|
+
token: code.trim(),
|
|
888
|
+
deviceHash: getDeviceFingerprint(),
|
|
889
|
+
lang: config.language || 'en',
|
|
890
|
+
}),
|
|
857
891
|
signal: AbortSignal.timeout(10000),
|
|
858
892
|
});
|
|
859
893
|
if (!res.ok) {
|
package/dist/lib/access.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export declare function isFirstRunOrUpgrade(currentVersion: string): boolean;
|
|
|
2
2
|
export declare function markVersionSeen(version: string): void;
|
|
3
3
|
export declare function validateToken(token: string): boolean;
|
|
4
4
|
export declare function isActivated(): boolean;
|
|
5
|
+
export declare function getDeviceFingerprint(): string;
|
|
5
6
|
export type ActivateResult = 'ok' | 'invalid' | 'already_bound';
|
|
6
7
|
export declare function activateToken(token: string): ActivateResult;
|
|
7
8
|
export declare function isDeviceMatch(): boolean;
|
package/dist/lib/access.js
CHANGED
|
@@ -127,7 +127,7 @@ export function isActivated() {
|
|
|
127
127
|
const config = getConfig();
|
|
128
128
|
return !!(config.accessToken && TOKEN_HASHES.has(hashToken(config.accessToken)));
|
|
129
129
|
}
|
|
130
|
-
function getDeviceFingerprint() {
|
|
130
|
+
export function getDeviceFingerprint() {
|
|
131
131
|
const parts = [hostname(), platform(), arch()];
|
|
132
132
|
// Add hardware UUID where possible
|
|
133
133
|
try {
|
|
@@ -9,6 +9,7 @@ export declare class CTFdClient {
|
|
|
9
9
|
fetchCsrfNonce(): Promise<string>;
|
|
10
10
|
private request;
|
|
11
11
|
testConnection(): Promise<CTFdUser>;
|
|
12
|
+
private testConnectionViaProfile;
|
|
12
13
|
getChallenges(): Promise<CTFdChallengeListItem[]>;
|
|
13
14
|
getChallenge(id: number): Promise<CTFdChallenge>;
|
|
14
15
|
submitFlag(challengeId: number, submission: string): Promise<CTFdAttemptResponse['data']>;
|
package/dist/lib/ctfd-client.js
CHANGED
|
@@ -69,7 +69,37 @@ export class CTFdClient {
|
|
|
69
69
|
return json.data;
|
|
70
70
|
}
|
|
71
71
|
async testConnection() {
|
|
72
|
-
|
|
72
|
+
try {
|
|
73
|
+
return await this.request('GET', '/users/me');
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
// Session mode fallback: API may return 403, try scraping profile page
|
|
77
|
+
if (this.sessionCookie && err.message?.includes('403')) {
|
|
78
|
+
return this.testConnectionViaProfile();
|
|
79
|
+
}
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async testConnectionViaProfile() {
|
|
84
|
+
const res = await fetch(`${this.baseUrl}/settings`, {
|
|
85
|
+
headers: { Cookie: this.sessionCookie },
|
|
86
|
+
});
|
|
87
|
+
if (!res.ok)
|
|
88
|
+
throw new Error('Session expired or invalid.');
|
|
89
|
+
const html = await res.text();
|
|
90
|
+
// Extract user name from settings page
|
|
91
|
+
const nameMatch = html.match(/name="name"[^>]*value="([^"]+)"/) ||
|
|
92
|
+
html.match(/<input[^>]*id="name"[^>]*value="([^"]+)"/);
|
|
93
|
+
const name = nameMatch?.[1] || 'User';
|
|
94
|
+
// Extract user ID from page
|
|
95
|
+
const idMatch = html.match(/user_id['":\s]+(\d+)/) ||
|
|
96
|
+
html.match(/userId['":\s]+(\d+)/);
|
|
97
|
+
const id = idMatch ? parseInt(idMatch[1], 10) : 0;
|
|
98
|
+
// Update CSRF nonce from settings page
|
|
99
|
+
const csrfMatch = html.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);
|
|
100
|
+
if (csrfMatch)
|
|
101
|
+
this.csrfNonce = csrfMatch[1];
|
|
102
|
+
return { id, name, score: 0, team_id: 0, country: '' };
|
|
73
103
|
}
|
|
74
104
|
async getChallenges() {
|
|
75
105
|
return this.request('GET', '/challenges');
|
package/dist/lib/demo-exam.js
CHANGED
|
@@ -98,7 +98,7 @@ export function getLocalizedDemoQuestions() {
|
|
|
98
98
|
return DEMO_QUESTIONS;
|
|
99
99
|
try {
|
|
100
100
|
const data = JSON.parse(readFileSync(path, 'utf-8'));
|
|
101
|
-
if (Array.isArray(data) && data.length
|
|
101
|
+
if (Array.isArray(data) && data.length >= DEMO_QUESTIONS.length)
|
|
102
102
|
return data;
|
|
103
103
|
}
|
|
104
104
|
catch { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "icoa-cli",
|
|
3
|
-
"version": "2.19.
|
|
3
|
+
"version": "2.19.15",
|
|
4
4
|
"description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"build": "tsc",
|
|
16
16
|
"dev": "tsc --watch",
|
|
17
17
|
"start": "node dist/index.js",
|
|
18
|
-
"postinstall": "node dist/postinstall.js
|
|
18
|
+
"postinstall": "node -e \"try{require('./dist/postinstall.js')}catch(e){}\""
|
|
19
19
|
},
|
|
20
20
|
"keywords": [
|
|
21
21
|
"ctf",
|