icoa-cli 2.19.84 → 2.19.86
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 +32 -0
- package/dist/lib/demo-flags.d.ts +8 -0
- package/dist/lib/demo-flags.js +27 -0
- package/dist/lib/exam-state.js +10 -1
- package/dist/lib/i18n.js +3 -2
- package/dist/repl.js +30 -0
- package/dist/types/index.d.ts +2 -0
- package/package.json +3 -2
package/dist/commands/exam.js
CHANGED
|
@@ -1045,6 +1045,9 @@ export function registerExamCommand(program) {
|
|
|
1045
1045
|
state.answers[num] = c;
|
|
1046
1046
|
state._lastQ = num;
|
|
1047
1047
|
saveExamState(state);
|
|
1048
|
+
// UX: visible "saved" indicator reassures beginners their answer is safe
|
|
1049
|
+
// even if they Ctrl+C or lose network. Neutral — doesn't reveal correctness.
|
|
1050
|
+
console.log(chalk.gray(' ✓ saved'));
|
|
1048
1051
|
// Per-question sync to server (real exam only). Best-effort, fire-and-forget.
|
|
1049
1052
|
// If timer expires or network drops before final submit, the server still
|
|
1050
1053
|
// has the answer. Final submit is authoritative and overwrites this.
|
|
@@ -1731,6 +1734,35 @@ export function registerExamCommand(program) {
|
|
|
1731
1734
|
// means the token alone is useless on another machine, so keeping
|
|
1732
1735
|
// it in exam-state.json is safe.
|
|
1733
1736
|
session.token = code.trim();
|
|
1737
|
+
// Best-effort server-time sync. Corrects client clock drift (NTP-less
|
|
1738
|
+
// Kali live-boots, timezone misconfigs) so the displayed countdown
|
|
1739
|
+
// matches the server's authoritative deadline. Silent on failure —
|
|
1740
|
+
// old servers (no endpoint) or offline clients fall back to the
|
|
1741
|
+
// local-clock-only deadline (see getExamDeadline in exam-state.ts).
|
|
1742
|
+
try {
|
|
1743
|
+
const t0 = Date.now();
|
|
1744
|
+
const tRes = await fetch(`${serverUrl}/api/icoa/server-time`, {
|
|
1745
|
+
method: 'GET',
|
|
1746
|
+
signal: AbortSignal.timeout(3000),
|
|
1747
|
+
});
|
|
1748
|
+
if (tRes.ok) {
|
|
1749
|
+
const tJson = await tRes.json();
|
|
1750
|
+
const serverMs = tJson?.data?.server_time_ms;
|
|
1751
|
+
if (typeof serverMs === 'number' && serverMs > 0) {
|
|
1752
|
+
// Offset is measured at the midpoint of the request window to
|
|
1753
|
+
// halve the latency bias (serverMs reflects when the server
|
|
1754
|
+
// read its clock; midpoint of our send/receive window is the
|
|
1755
|
+
// best single-sample approximation of when that reading aligns
|
|
1756
|
+
// with our clock).
|
|
1757
|
+
const clientMs = (t0 + Date.now()) / 2;
|
|
1758
|
+
session.clockOffsetMs = Math.round(serverMs - clientMs);
|
|
1759
|
+
session.deadlineServerMs = serverMs + session.durationMinutes * 60 * 1000;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
catch {
|
|
1764
|
+
// Offline or old server — keep fallback (confirmedAt + duration local).
|
|
1765
|
+
}
|
|
1734
1766
|
saveExamState({ session, questions, answers: {}, interactions: [], aiUsage: { ai4ctf: 0, ctf4ai: 0 } });
|
|
1735
1767
|
console.log();
|
|
1736
1768
|
printSuccess('Exam started! Timer is running.');
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const DEMO_AI4CTF_FLAG_B64 = "aWNvYXt3M2xjMG1lXzJfYWk0Y3RmfQ==";
|
|
2
|
+
export declare const DEMO_AI4CTF_FLAG_DECODED = "icoa{w3lc0me_2_ai4ctf}";
|
|
3
|
+
/**
|
|
4
|
+
* Runtime verification that the b64 constant decodes to the expected plaintext.
|
|
5
|
+
* Catches hand-edit drift between the two constants above.
|
|
6
|
+
* Throws if they disagree. Safe to call at module load or from tests.
|
|
7
|
+
*/
|
|
8
|
+
export declare function verifyDemoAi4ctfFlag(): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Single source of truth for the demo AI4CTF flag.
|
|
2
|
+
// i18n.ts (EN) and the i18n translations must reference this constant
|
|
3
|
+
// rather than hand-copying the base64 string — hand-copying created the
|
|
4
|
+
// BUG-002 class drift where Q33 base64 was written as "ISOA{...}" in one
|
|
5
|
+
// place and "ICOA{...}" in another. This file exists so every translation
|
|
6
|
+
// picks up the correct value from one place.
|
|
7
|
+
//
|
|
8
|
+
// The matching unit test (test/demo-flags-consistency.test.js) scans
|
|
9
|
+
// compiled i18n.js for any base64-looking string that starts with
|
|
10
|
+
// "aWNvYX" (= "icoa{") and asserts they ALL equal DEMO_AI4CTF_FLAG_B64.
|
|
11
|
+
// That's the drift protection: if someone hand-edits one translation's
|
|
12
|
+
// b64, the scan finds two distinct values and the test fails.
|
|
13
|
+
export const DEMO_AI4CTF_FLAG_B64 = 'aWNvYXt3M2xjMG1lXzJfYWk0Y3RmfQ==';
|
|
14
|
+
export const DEMO_AI4CTF_FLAG_DECODED = 'icoa{w3lc0me_2_ai4ctf}';
|
|
15
|
+
/**
|
|
16
|
+
* Runtime verification that the b64 constant decodes to the expected plaintext.
|
|
17
|
+
* Catches hand-edit drift between the two constants above.
|
|
18
|
+
* Throws if they disagree. Safe to call at module load or from tests.
|
|
19
|
+
*/
|
|
20
|
+
export function verifyDemoAi4ctfFlag() {
|
|
21
|
+
const decoded = Buffer.from(DEMO_AI4CTF_FLAG_B64, 'base64').toString('utf-8');
|
|
22
|
+
if (decoded !== DEMO_AI4CTF_FLAG_DECODED) {
|
|
23
|
+
throw new Error(`Demo flag drift: base64 '${DEMO_AI4CTF_FLAG_B64}' decodes to '${decoded}' ` +
|
|
24
|
+
`but DEMO_AI4CTF_FLAG_DECODED constant is '${DEMO_AI4CTF_FLAG_DECODED}'. ` +
|
|
25
|
+
`Fix demo-flags.ts so both halves agree.`);
|
|
26
|
+
}
|
|
27
|
+
}
|
package/dist/lib/exam-state.js
CHANGED
|
@@ -87,7 +87,16 @@ export function getExamDeadline() {
|
|
|
87
87
|
return null;
|
|
88
88
|
if (!state.session.durationMinutes)
|
|
89
89
|
return null; // 0 = no time limit
|
|
90
|
-
//
|
|
90
|
+
// Preferred path: server-time sync succeeded at exam start. deadlineServerMs
|
|
91
|
+
// is the authoritative server-time moment of expiry. Convert back to local
|
|
92
|
+
// clock domain so existing callers (which compare against Date.now()) stay
|
|
93
|
+
// correct even if the local clock drifts from server.
|
|
94
|
+
const { deadlineServerMs, clockOffsetMs } = state.session;
|
|
95
|
+
if (typeof deadlineServerMs === 'number' && typeof clockOffsetMs === 'number') {
|
|
96
|
+
return new Date(deadlineServerMs - clockOffsetMs);
|
|
97
|
+
}
|
|
98
|
+
// Fallback: local-clock-only (pre-v2.19.85 behavior). Accurate as long as
|
|
99
|
+
// client and server clocks agree; off by the clock skew otherwise.
|
|
91
100
|
const startTime = state.session.confirmedAt || state.session.startedAt;
|
|
92
101
|
const start = new Date(startTime).getTime();
|
|
93
102
|
return new Date(start + state.session.durationMinutes * 60 * 1000);
|
package/dist/lib/i18n.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getConfig } from './config.js';
|
|
2
|
+
import { DEMO_AI4CTF_FLAG_B64 } from './demo-flags.js';
|
|
2
3
|
export const EN = {
|
|
3
4
|
// How to play
|
|
4
5
|
howToPlay: 'How to play:',
|
|
@@ -72,7 +73,7 @@ export const EN = {
|
|
|
72
73
|
ai4ctfHintC: 'Critical assist — Nearly gives you the answer',
|
|
73
74
|
ai4ctfHintCUses: '2 uses only. Last resort!',
|
|
74
75
|
ai4ctfTryNow: 'Try now: ask the AI anything about the challenge above.',
|
|
75
|
-
ai4ctfExample:
|
|
76
|
+
ai4ctfExample: `Example: "What encoding is ${DEMO_AI4CTF_FLAG_B64}?"`,
|
|
76
77
|
ai4ctfFreeChat: 'Or just chat freely! You can also try hint a, hint b, hint c.',
|
|
77
78
|
ai4ctfExit: 'Type "exit" when done.',
|
|
78
79
|
ai4ctfReport: 'AI4CTF Session Report',
|
|
@@ -201,7 +202,7 @@ export const EN = {
|
|
|
201
202
|
' 2. Run a shell command directly. Shell commands inside ai4ctf\n' +
|
|
202
203
|
' must start with "!", otherwise your text goes to the AI. Example:\n' +
|
|
203
204
|
'\n' +
|
|
204
|
-
|
|
205
|
+
` !echo ${DEMO_AI4CTF_FLAG_B64} | base64 -d`,
|
|
205
206
|
ai4ctfHintNextA: 'Stuck? Try:',
|
|
206
207
|
ai4ctfHintNextB: 'Really stuck? Try:',
|
|
207
208
|
// Post-solve wrap-up (v2.19.32)
|
package/dist/repl.js
CHANGED
|
@@ -192,6 +192,14 @@ function printSelectionMenu() {
|
|
|
192
192
|
console.log(chalk.white(' lang es') + chalk.gray(' Switch language (17 supported)'));
|
|
193
193
|
console.log(chalk.gray(' ─────────────────────────────────────────────'));
|
|
194
194
|
}
|
|
195
|
+
// Beginner-friendly safety net: make it obvious how to pause / exit / get help.
|
|
196
|
+
// This is the first place a new K-12 user lands, so the three escape hatches
|
|
197
|
+
// need to be visible without cluttering the main command list above.
|
|
198
|
+
console.log(chalk.gray(' ') +
|
|
199
|
+
chalk.gray('Tip: ') + chalk.cyan('help') + chalk.gray(' for commands · ') +
|
|
200
|
+
chalk.cyan('Ctrl+C') + chalk.gray(' pauses · ') +
|
|
201
|
+
chalk.cyan('exit') + chalk.gray(' → menu · ') +
|
|
202
|
+
chalk.cyan('quit') + chalk.gray(' closes CLI'));
|
|
195
203
|
console.log();
|
|
196
204
|
}
|
|
197
205
|
export async function startRepl(program, resumeMode) {
|
|
@@ -975,6 +983,28 @@ export async function startRepl(program, resumeMode) {
|
|
|
975
983
|
console.log();
|
|
976
984
|
rl.prompt();
|
|
977
985
|
});
|
|
986
|
+
// SIGINT (Ctrl+C) — intercept gracefully so beginners don't lose confidence.
|
|
987
|
+
// Without this listener, readline's default is to raise SIGINT which our
|
|
988
|
+
// theme.ts handler converts to process.exit(130). Installing this listener
|
|
989
|
+
// swallows that path and lets the user get oriented. If they want to exit,
|
|
990
|
+
// they type `quit` or hit Ctrl+D (sends EOF → 'close' event below).
|
|
991
|
+
rl.on('SIGINT', () => {
|
|
992
|
+
console.log();
|
|
993
|
+
if (isChatActive() || isCtf4aiActive()) {
|
|
994
|
+
console.log(chalk.yellow(' Type ') + chalk.bold.cyan('exit') + chalk.yellow(' to leave chat, or Ctrl+D to close ICOA CLI.'));
|
|
995
|
+
}
|
|
996
|
+
else if (getExamState()) {
|
|
997
|
+
console.log(chalk.yellow(' Exam paused. Your answers are auto-saved.'));
|
|
998
|
+
console.log(chalk.white(' Resume: ') + chalk.cyan('exam q 1') +
|
|
999
|
+
chalk.gray(' · Back to menu: ') + chalk.cyan('back') +
|
|
1000
|
+
chalk.gray(' · Close CLI: ') + chalk.cyan('quit'));
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
console.log(chalk.yellow(' Press Ctrl+D or type ') + chalk.bold.cyan('quit') + chalk.yellow(' to close. ') + chalk.bold.cyan('help') + chalk.yellow(' for commands.'));
|
|
1004
|
+
}
|
|
1005
|
+
console.log();
|
|
1006
|
+
rl.prompt();
|
|
1007
|
+
});
|
|
978
1008
|
rl.on('close', () => {
|
|
979
1009
|
stopLogSync();
|
|
980
1010
|
recordExit();
|
package/dist/types/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "icoa-cli",
|
|
3
|
-
"version": "2.19.
|
|
3
|
+
"version": "2.19.86",
|
|
4
4
|
"description": "ICOA CLI — The world's first CLI-native CTF competition terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"build": "tsc",
|
|
16
16
|
"dev": "tsc --watch",
|
|
17
17
|
"start": "node dist/index.js",
|
|
18
|
+
"test": "tsc && node --test test/*.test.js",
|
|
18
19
|
"postinstall": "node -e \"try{require('./dist/postinstall.js')}catch(e){}\""
|
|
19
20
|
},
|
|
20
21
|
"keywords": [
|
|
@@ -24,7 +25,7 @@
|
|
|
24
25
|
"icoa",
|
|
25
26
|
"competition"
|
|
26
27
|
],
|
|
27
|
-
"license": "
|
|
28
|
+
"license": "BUSL-1.1",
|
|
28
29
|
"dependencies": {
|
|
29
30
|
"@google/genai": "^1.48.0",
|
|
30
31
|
"@inquirer/prompts": "^7.5.0",
|