icoa-cli 2.19.14 → 2.19.16
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/exam.js +52 -5
- package/dist/lib/access.d.ts +1 -0
- package/dist/lib/access.js +1 -1
- package/dist/lib/demo-exam.js +1 -1
- package/dist/lib/log-sync.js +1 -1
- package/dist/repl.js +4 -4
- package/package.json +1 -1
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;
|
|
@@ -694,8 +695,27 @@ export function registerExamCommand(program) {
|
|
|
694
695
|
console.log();
|
|
695
696
|
const questionsSnapshot = [...state.questions];
|
|
696
697
|
const answersSnapshot = { ...state.answers };
|
|
698
|
+
const helpState = getHelpState(state);
|
|
697
699
|
clearExamState();
|
|
698
700
|
const percentage = Math.round(score / total * 100);
|
|
701
|
+
// Report demo stats to server
|
|
702
|
+
const config = getConfig();
|
|
703
|
+
fetch('https://practice.icoa2026.au/api/icoa/demo-stats', {
|
|
704
|
+
method: 'POST',
|
|
705
|
+
headers: { 'Content-Type': 'application/json' },
|
|
706
|
+
body: JSON.stringify({
|
|
707
|
+
type: 'demo-exam',
|
|
708
|
+
score,
|
|
709
|
+
total,
|
|
710
|
+
lang: config.language || 'en',
|
|
711
|
+
help_used: helpState.used,
|
|
712
|
+
help_max: helpState.max,
|
|
713
|
+
tokens_used: 0,
|
|
714
|
+
solved: percentage >= 60 ? 1 : 0,
|
|
715
|
+
timestamp: new Date().toISOString(),
|
|
716
|
+
}),
|
|
717
|
+
signal: AbortSignal.timeout(5000),
|
|
718
|
+
}).catch(() => { });
|
|
699
719
|
console.log();
|
|
700
720
|
console.log(chalk.cyan(' ═══════════════════════════════════════'));
|
|
701
721
|
console.log();
|
|
@@ -779,12 +799,35 @@ export function registerExamCommand(program) {
|
|
|
779
799
|
return;
|
|
780
800
|
}
|
|
781
801
|
// Real exam: submit to server
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
802
|
+
// Token-based exam: use direct fetch with exam token
|
|
803
|
+
// CTFd-based exam: use ExamClient
|
|
804
|
+
const examToken = state.session.token;
|
|
785
805
|
try {
|
|
786
806
|
drawProgress(0, 'Uploading answers...');
|
|
787
|
-
|
|
807
|
+
let result;
|
|
808
|
+
if (examToken) {
|
|
809
|
+
// Submit via exam token
|
|
810
|
+
const config = getConfig();
|
|
811
|
+
const serverUrl = config.ctfdUrl || 'https://practice.icoa2026.au';
|
|
812
|
+
const res = await fetch(`${serverUrl}/api/icoa/exams/${state.session.examId}/submit`, {
|
|
813
|
+
method: 'POST',
|
|
814
|
+
headers: { 'Content-Type': 'application/json' },
|
|
815
|
+
body: JSON.stringify({ token: examToken, answers: state.answers }),
|
|
816
|
+
signal: AbortSignal.timeout(10000),
|
|
817
|
+
});
|
|
818
|
+
if (!res.ok) {
|
|
819
|
+
const err = await res.json().catch(() => ({ message: 'Submit failed' }));
|
|
820
|
+
throw new Error(err.message || 'Submit failed');
|
|
821
|
+
}
|
|
822
|
+
const json = await res.json();
|
|
823
|
+
result = json.data;
|
|
824
|
+
}
|
|
825
|
+
else {
|
|
826
|
+
const client = requireExamConnection();
|
|
827
|
+
if (!client)
|
|
828
|
+
return;
|
|
829
|
+
result = await client.submitExam(state.session.examId, state.answers);
|
|
830
|
+
}
|
|
788
831
|
drawProgress(50, 'Grading...');
|
|
789
832
|
await sleep(300);
|
|
790
833
|
drawProgress(100, 'Complete!');
|
|
@@ -859,7 +902,11 @@ export function registerExamCommand(program) {
|
|
|
859
902
|
const res = await fetch(`${serverUrl}/api/icoa/exam-token`, {
|
|
860
903
|
method: 'POST',
|
|
861
904
|
headers: { 'Content-Type': 'application/json' },
|
|
862
|
-
body: JSON.stringify({
|
|
905
|
+
body: JSON.stringify({
|
|
906
|
+
token: code.trim(),
|
|
907
|
+
deviceHash: getDeviceFingerprint(),
|
|
908
|
+
lang: config.language || 'en',
|
|
909
|
+
}),
|
|
863
910
|
signal: AbortSignal.timeout(10000),
|
|
864
911
|
});
|
|
865
912
|
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 {
|
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/dist/lib/log-sync.js
CHANGED
|
@@ -49,7 +49,7 @@ async function syncLogs() {
|
|
|
49
49
|
};
|
|
50
50
|
try {
|
|
51
51
|
// POST to CTFd server audit endpoint
|
|
52
|
-
const url = new URL('/api/
|
|
52
|
+
const url = new URL('/api/icoa/audit', config.ctfdUrl).href;
|
|
53
53
|
const res = await fetch(url, {
|
|
54
54
|
method: 'POST',
|
|
55
55
|
headers: {
|
package/dist/repl.js
CHANGED
|
@@ -10,6 +10,7 @@ import { getExamState } from './lib/exam-state.js';
|
|
|
10
10
|
import { resetTerminalTheme } from './lib/theme.js';
|
|
11
11
|
import { ensureSandbox, runInSandbox, isDockerAvailable } from './lib/sandbox.js';
|
|
12
12
|
import { logCommand } from './lib/logger.js';
|
|
13
|
+
import { startLogSync, stopLogSync } from './lib/log-sync.js';
|
|
13
14
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
14
15
|
import { join } from 'node:path';
|
|
15
16
|
import { homedir } from 'node:os';
|
|
@@ -301,8 +302,7 @@ export async function startRepl(program, resumeMode) {
|
|
|
301
302
|
});
|
|
302
303
|
let processing = false;
|
|
303
304
|
setReplMode(true);
|
|
304
|
-
|
|
305
|
-
// startLogSync();
|
|
305
|
+
startLogSync();
|
|
306
306
|
rl.prompt();
|
|
307
307
|
rl.on('line', async (line) => {
|
|
308
308
|
if (processing)
|
|
@@ -349,7 +349,7 @@ export async function startRepl(program, resumeMode) {
|
|
|
349
349
|
rl.prompt();
|
|
350
350
|
return;
|
|
351
351
|
}
|
|
352
|
-
|
|
352
|
+
stopLogSync();
|
|
353
353
|
recordExit();
|
|
354
354
|
console.log(chalk.gray(' Session saved. Use ') + chalk.white('icoa --resume') + chalk.gray(' to continue.'));
|
|
355
355
|
resetTerminalTheme();
|
|
@@ -657,7 +657,7 @@ export async function startRepl(program, resumeMode) {
|
|
|
657
657
|
rl.prompt();
|
|
658
658
|
});
|
|
659
659
|
rl.on('close', () => {
|
|
660
|
-
|
|
660
|
+
stopLogSync();
|
|
661
661
|
recordExit();
|
|
662
662
|
resetTerminalTheme();
|
|
663
663
|
realExit(0);
|