icoa-cli 2.19.13 → 2.19.14

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.
@@ -176,12 +176,27 @@ export function registerCtfCommands(program) {
176
176
  }
177
177
  }
178
178
  catch (err) {
179
- spinner2.fail('Connection test failed');
180
- printError(err.message);
181
- // Still save URL so user can retry
182
- saveConfig({ ctfdUrl: url, sessionCookie: sessionCookie });
183
- console.log();
184
- printInfo('Connection saved. Try: join ' + url);
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 ───
@@ -29,14 +29,17 @@ function checkTimeRemaining() {
29
29
  return true;
30
30
  if (new Date() >= deadline) {
31
31
  const state = getExamState();
32
- if (state && state.session.examId === 'demo-free') {
33
- // Demo expired — auto-clear
34
- printWarning('Demo time expired. Starting fresh!');
35
- printInfo('Type: demo');
36
- clearExamState();
37
- }
38
- else {
39
- printError('Time expired! Use "exam submit" to submit your answers.');
32
+ if (state) {
33
+ const answered = Object.keys(state.answers).length;
34
+ if (state.session.examId === 'demo-free' || answered === 0) {
35
+ // Demo or unanswered expired exam — auto-clear to avoid deadlock
36
+ clearExamState();
37
+ printWarning('Exam expired and cleared.');
38
+ printInfo('Type: demo to start again.');
39
+ }
40
+ else {
41
+ printError('Time expired! Use "exam submit" to submit your answers.');
42
+ }
40
43
  }
41
44
  return false;
42
45
  }
@@ -639,12 +642,15 @@ export function registerExamCommand(program) {
639
642
  const total = state.session.questionCount;
640
643
  const unanswered = total - answered;
641
644
  if (answered === 0) {
642
- printWarning('No answers to submit.');
643
- if (state.session.examId === 'demo-free') {
645
+ const deadline = getExamDeadline();
646
+ const expired = deadline && new Date() >= deadline;
647
+ if (state.session.examId === 'demo-free' || expired) {
644
648
  clearExamState();
645
- printInfo('Demo cleared. Type: demo to start again.');
649
+ printWarning('No answers to submit. Exam cleared.');
650
+ printInfo('Type: demo to start again.');
646
651
  }
647
652
  else {
653
+ printWarning('No answers to submit.');
648
654
  printInfo('Answer some questions first: exam q 1');
649
655
  }
650
656
  return;
@@ -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']>;
@@ -69,7 +69,37 @@ export class CTFdClient {
69
69
  return json.data;
70
70
  }
71
71
  async testConnection() {
72
- return this.request('GET', '/users/me');
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.13",
3
+ "version": "2.19.14",
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 2>/dev/null || true"
18
+ "postinstall": "node -e \"try{require('./dist/postinstall.js')}catch(e){}\""
19
19
  },
20
20
  "keywords": [
21
21
  "ctf",