icoa-cli 2.19.14 → 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.
@@ -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;
@@ -779,12 +780,35 @@ export function registerExamCommand(program) {
779
780
  return;
780
781
  }
781
782
  // Real exam: submit to server
782
- const client = requireExamConnection();
783
- if (!client)
784
- return;
783
+ // Token-based exam: use direct fetch with exam token
784
+ // CTFd-based exam: use ExamClient
785
+ const examToken = state.session.token;
785
786
  try {
786
787
  drawProgress(0, 'Uploading answers...');
787
- const result = await client.submitExam(state.session.examId, state.answers);
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
+ }
788
812
  drawProgress(50, 'Grading...');
789
813
  await sleep(300);
790
814
  drawProgress(100, 'Complete!');
@@ -859,7 +883,11 @@ export function registerExamCommand(program) {
859
883
  const res = await fetch(`${serverUrl}/api/icoa/exam-token`, {
860
884
  method: 'POST',
861
885
  headers: { 'Content-Type': 'application/json' },
862
- body: JSON.stringify({ token: code.trim() }),
886
+ body: JSON.stringify({
887
+ token: code.trim(),
888
+ deviceHash: getDeviceFingerprint(),
889
+ lang: config.language || 'en',
890
+ }),
863
891
  signal: AbortSignal.timeout(10000),
864
892
  });
865
893
  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 { }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.14",
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": {