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.
@@ -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
+ }
@@ -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
- // Use confirmedAt (when user pressed Enter) as the real start, fallback to startedAt
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: 'Example: "What encoding is aWNvYXt3M2xjMG1lXzJfYWk0Y3RmfQ==?"',
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
- ' !echo aWNvYXt3M2xjMG1lXzJfYWk0Y3RmfQ== | base64 -d',
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)
@@ -167,6 +167,8 @@ export interface ExamSession {
167
167
  passingScore?: number;
168
168
  country: string;
169
169
  token?: string;
170
+ clockOffsetMs?: number;
171
+ deadlineServerMs?: number;
170
172
  }
171
173
  export interface ExamInteraction {
172
174
  ts: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.84",
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": [