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.
@@ -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
- const client = requireExamConnection();
783
- if (!client)
784
- return;
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
- const result = await client.submitExam(state.session.examId, state.answers);
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({ token: code.trim() }),
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) {
@@ -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;
@@ -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 {
@@ -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 === 30)
101
+ if (Array.isArray(data) && data.length >= DEMO_QUESTIONS.length)
102
102
  return data;
103
103
  }
104
104
  catch { }
@@ -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/v1/audit/logs', config.ctfdUrl).href;
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
- // Log sync disabled until server audit endpoint is configured
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
- // stopLogSync();
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
- // stopLogSync();
660
+ stopLogSync();
661
661
  recordExit();
662
662
  resetTerminalTheme();
663
663
  realExit(0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.14",
3
+ "version": "2.19.16",
4
4
  "description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
5
5
  "type": "module",
6
6
  "bin": {