icoa-cli 2.19.84 → 2.19.85
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 +29 -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/types/index.d.ts +2 -0
- package/package.json +2 -1
package/dist/commands/exam.js
CHANGED
|
@@ -1731,6 +1731,35 @@ export function registerExamCommand(program) {
|
|
|
1731
1731
|
// means the token alone is useless on another machine, so keeping
|
|
1732
1732
|
// it in exam-state.json is safe.
|
|
1733
1733
|
session.token = code.trim();
|
|
1734
|
+
// Best-effort server-time sync. Corrects client clock drift (NTP-less
|
|
1735
|
+
// Kali live-boots, timezone misconfigs) so the displayed countdown
|
|
1736
|
+
// matches the server's authoritative deadline. Silent on failure —
|
|
1737
|
+
// old servers (no endpoint) or offline clients fall back to the
|
|
1738
|
+
// local-clock-only deadline (see getExamDeadline in exam-state.ts).
|
|
1739
|
+
try {
|
|
1740
|
+
const t0 = Date.now();
|
|
1741
|
+
const tRes = await fetch(`${serverUrl}/api/icoa/server-time`, {
|
|
1742
|
+
method: 'GET',
|
|
1743
|
+
signal: AbortSignal.timeout(3000),
|
|
1744
|
+
});
|
|
1745
|
+
if (tRes.ok) {
|
|
1746
|
+
const tJson = await tRes.json();
|
|
1747
|
+
const serverMs = tJson?.data?.server_time_ms;
|
|
1748
|
+
if (typeof serverMs === 'number' && serverMs > 0) {
|
|
1749
|
+
// Offset is measured at the midpoint of the request window to
|
|
1750
|
+
// halve the latency bias (serverMs reflects when the server
|
|
1751
|
+
// read its clock; midpoint of our send/receive window is the
|
|
1752
|
+
// best single-sample approximation of when that reading aligns
|
|
1753
|
+
// with our clock).
|
|
1754
|
+
const clientMs = (t0 + Date.now()) / 2;
|
|
1755
|
+
session.clockOffsetMs = Math.round(serverMs - clientMs);
|
|
1756
|
+
session.deadlineServerMs = serverMs + session.durationMinutes * 60 * 1000;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
catch {
|
|
1761
|
+
// Offline or old server — keep fallback (confirmedAt + duration local).
|
|
1762
|
+
}
|
|
1734
1763
|
saveExamState({ session, questions, answers: {}, interactions: [], aiUsage: { ai4ctf: 0, ctf4ai: 0 } });
|
|
1735
1764
|
console.log();
|
|
1736
1765
|
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/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.85",
|
|
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": [
|