icoa-cli 2.19.303 → 2.19.304

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.
@@ -1 +1 @@
1
- import{createWriteStream as t,mkdirSync as e}from"node:fs";import{join as s}from"node:path";import{pipeline as a}from"node:stream/promises";import{Readable as o}from"node:stream";export class CTFdClient{baseUrl;token;sessionCookie;csrfNonce;constructor(t,e,s,a){this.baseUrl=t.replace(/\/+$/,""),this.token=e,this.sessionCookie=s||"",this.csrfNonce=a||""}getAuthHeaders(){if(this.token)return{Authorization:`Token ${this.token}`};if(this.sessionCookie){const t={Cookie:this.sessionCookie};return this.csrfNonce&&(t["CSRF-Token"]=this.csrfNonce),t}return{}}async fetchCsrfNonce(){if(this.csrfNonce)return this.csrfNonce;try{const t=await fetch(this.baseUrl,{headers:this.sessionCookie?{Cookie:this.sessionCookie}:{}}),e=(await t.text()).match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);if(e)return this.csrfNonce=e[1],this.csrfNonce}catch{}return""}async request(t,e,s){this.sessionCookie&&!this.csrfNonce&&await this.fetchCsrfNonce();const a=`${this.baseUrl}/api/v1${e}`,o={...this.getAuthHeaders()};void 0!==s&&(o["Content-Type"]="application/json");const n=await fetch(a,{method:t,headers:o,body:void 0!==s?JSON.stringify(s):void 0,redirect:"manual"});if(n.status>=300&&n.status<400)throw new Error("Authentication failed: token invalid or expired. Re-run `join <url>` to sign in again.");if(!n.ok){const t=await n.text().catch(()=>"Unknown error");throw new Error(`CTFd API error (${n.status}): ${t}`)}if(!(n.headers.get("content-type")||"").includes("application/json")&&(await n.text().catch(()=>"")).trimStart().startsWith("<"))throw new Error("Authentication failed: server returned a login page instead of data. Re-run `join <url>`.");const i=await n.json();if(!1===i.success)throw new Error(`CTFd error: ${i.errors?.join(", ")||"Unknown error"}`);return i.data}async testConnection(){try{return await this.request("GET","/users/me")}catch(t){if(this.sessionCookie&&(t.message?.includes("403")||t.message?.includes("Authentication failed")))return this.testConnectionViaProfile();throw t}}async testConnectionViaProfile(){const t=await fetch(`${this.baseUrl}/settings`,{headers:{Cookie:this.sessionCookie}});if(!t.ok)throw new Error("Session expired or invalid.");const e=await t.text(),s=e.match(/name="name"[^>]*value="([^"]+)"/)||e.match(/<input[^>]*id="name"[^>]*value="([^"]+)"/),a=s?.[1]||"User",o=e.match(/user_id['":\s]+(\d+)/)||e.match(/userId['":\s]+(\d+)/),n=o?parseInt(o[1],10):0,i=e.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);return i&&(this.csrfNonce=i[1]),{id:n,name:a,score:0,team_id:0,country:""}}async getChallenges(){return this.request("GET","/challenges")}async getChallenge(t){return this.request("GET",`/challenges/${t}`)}async submitFlag(t,e){this.sessionCookie&&!this.csrfNonce&&await this.fetchCsrfNonce();const s=await fetch(`${this.baseUrl}/api/v1/challenges/attempt`,{method:"POST",headers:{...this.getAuthHeaders(),"Content-Type":"application/json"},body:JSON.stringify({challenge_id:t,submission:e})});if(!s.ok){const t=await s.text().catch(()=>"Unknown error");throw new Error(`CTFd API error (${s.status}): ${t}`)}return(await s.json()).data}async getScoreboard(){return this.request("GET","/scoreboard")}async getTeam(){return this.request("GET","/teams/me")}async getCompetitionMeta(){const t=await fetch(this.baseUrl),e=await t.text(),s=e.match(/'start'\s*:\s*(\d+)/),a=e.match(/'end'\s*:\s*(\d+)/),o=e.match(/'userMode'\s*:\s*"([^"]+)"/),n=e.match(/'csrfNonce'\s*:\s*"([^"]+)"/);return{start:s?parseInt(s[1],10):null,end:a?parseInt(a[1],10):null,userMode:o?.[1]||"users",csrfNonce:n?.[1]||""}}async getChallengeFiles(t){return(await this.getChallenge(t)).files||[]}async downloadFile(n,i){e(i,{recursive:!0});const r=n.startsWith("http")?n:`${this.baseUrl}/${n.replace(/^\//,"")}`,c=await fetch(r,{headers:this.getAuthHeaders(),redirect:"follow"});if(!c.ok||!c.body)throw new Error(`Failed to download: ${r}`);const h=(n.split("/").pop()||"file").split("?")[0],l=s(i,h),d=t(l);return await a(o.fromWeb(c.body),d),l}async getTokenViaIcoaApi(t,e){const s=JSON.stringify({name:t,password:e}),a={"Content-Type":"application/json"};try{const t=await fetch(`${this.baseUrl}/api/icoa/token`,{method:"POST",headers:a,body:s,signal:AbortSignal.timeout(5e3)});if(t.ok){const e=await t.json();if(e.success&&e.data?.token)return e.data.token}}catch{}try{const t=await fetch(`${this.baseUrl}:9090/api/icoa/token`,{method:"POST",headers:a,body:s,signal:AbortSignal.timeout(5e3)});if(t.ok){const e=await t.json();if(e.success&&e.data?.token)return e.data.token}}catch{}return null}async tokenAuthenticates(t){try{const e=await fetch(`${this.baseUrl}/api/v1/users/me`,{headers:{Authorization:`Token ${t}`},redirect:"manual",signal:AbortSignal.timeout(5e3)});if(200!==e.status)return!1;if(!(e.headers.get("content-type")||"").includes("application/json"))return!1;const s=await e.json();return!0===s?.success}catch{return!1}}async loginWithCredentials(t,e){const s=await this.getTokenViaIcoaApi(t,e);if(s&&await this.tokenAuthenticates(s))return{token:s,session:"",csrf:""};const a=await fetch(`${this.baseUrl}/login`),o=await a.text(),n=o.match(/name="nonce"[^>]*value="([^"]+)"/)||o.match(/value="([^"]+)"[^>]*name="nonce"/)||o.match(/id="nonce"[^>]*value="([^"]+)"/)||o.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/),i=n?.[1]||"";if(!i)throw new Error("Could not extract CSRF nonce from login page.");const r=a.headers.getSetCookie?.()||[],c=r.map(t=>t.split(";")[0]).join("; "),h=await fetch(`${this.baseUrl}/login`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Cookie:c},body:new URLSearchParams({name:t,password:e,nonce:i,_submit:"Submit"}),redirect:"manual"}),l=h.headers.getSetCookie?.()||[],d=new Map;for(const t of[...r,...l]){const e=t.split(";")[0],s=e.indexOf("=");s>0&&d.set(e.slice(0,s),e.slice(s+1))}const u=[...d.entries()].map(([t,e])=>`${t}=${e}`).join("; "),f=h.headers.get("location")||"";if(!(h.status>=300&&h.status<400)||f.includes("/login"))throw new Error("Invalid username or password.");try{const t=await fetch(`${this.baseUrl}/settings`,{headers:{Cookie:u}}),e=(await t.text()).match(/csrfNonce['":\s]+['"]([^'"]+)['"]/),s=e?.[1]||i,a=await fetch(`${this.baseUrl}/api/v1/tokens`,{method:"POST",headers:{"Content-Type":"application/json",Cookie:u,"CSRF-Token":s},body:JSON.stringify({expiration:"2026-12-31T23:59:59+00:00"})});if(a.ok){const t=await a.json();if(t.success&&t.data?.value)return{token:t.data.value,session:"",csrf:""}}}catch{}let m="";try{const t=await fetch(`${this.baseUrl}/challenges`,{headers:{Cookie:u}}),e=(await t.text()).match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);e&&(m=e[1])}catch{}return{token:"",session:u,csrf:m}}}
1
+ import{createWriteStream as t,mkdirSync as e}from"node:fs";import{join as s}from"node:path";import{pipeline as n}from"node:stream/promises";import{Readable as o}from"node:stream";export class CTFdClient{baseUrl;token;sessionCookie;csrfNonce;constructor(t,e,s,n){this.baseUrl=t.replace(/\/+$/,""),this.token=e,this.sessionCookie=s||"",this.csrfNonce=n||""}getAuthHeaders(){if(this.token)return{Authorization:`Token ${this.token}`};if(this.sessionCookie){const t={Cookie:this.sessionCookie};return this.csrfNonce&&(t["CSRF-Token"]=this.csrfNonce),t}return{}}async fetchCsrfNonce(){if(this.csrfNonce)return this.csrfNonce;try{const t=await fetch(this.baseUrl,{headers:this.sessionCookie?{Cookie:this.sessionCookie}:{}}),e=(await t.text()).match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);if(e)return this.csrfNonce=e[1],this.csrfNonce}catch{}return""}async request(t,e,s){this.sessionCookie&&!this.csrfNonce&&await this.fetchCsrfNonce();const n=`${this.baseUrl}/api/v1${e}`,o={...this.getAuthHeaders()};void 0!==s&&(o["Content-Type"]="application/json");const a=await fetch(n,{method:t,headers:o,body:void 0!==s?JSON.stringify(s):void 0,redirect:"manual"});if(a.status>=300&&a.status<400)throw new Error("Authentication failed: token invalid or expired. Re-run `join <url>` to sign in again.");if(!a.ok){const t=await a.text().catch(()=>"Unknown error");if(t.trimStart().startsWith("<"))throw new Error("Access denied (403). Either your session expired — re-run `join <url>` — or this account is banned / not enabled for the competition (try a different account, or contact the organizer).");throw new Error(`CTFd API error (${a.status}): ${t}`)}if(!(a.headers.get("content-type")||"").includes("application/json")&&(await a.text().catch(()=>"")).trimStart().startsWith("<"))throw new Error("Authentication failed: server returned a login page instead of data. Re-run `join <url>`.");const r=await a.json();if(!1===r.success)throw new Error(`CTFd error: ${r.errors?.join(", ")||"Unknown error"}`);return r.data}async testConnection(){try{return await this.request("GET","/users/me")}catch(t){if(this.sessionCookie&&(t.message?.includes("403")||t.message?.includes("Authentication failed")))return this.testConnectionViaProfile();throw t}}async testConnectionViaProfile(){const t=await fetch(`${this.baseUrl}/settings`,{headers:{Cookie:this.sessionCookie}});if(!t.ok){if(403===t.status)throw new Error("ACCOUNT_BLOCKED: signed in, but this account is banned / disabled / not enabled for the competition. Use a different account, or contact the organizer.");throw new Error("Session expired or invalid.")}const e=await t.text(),s=e.match(/name="name"[^>]*value="([^"]+)"/)||e.match(/<input[^>]*id="name"[^>]*value="([^"]+)"/),n=e.match(/user_id['":\s]+(\d+)/)||e.match(/userId['":\s]+(\d+)/);if(!s&&!n)throw new Error("Authentication failed: session is not signed in. Re-run `join <url>` and check your username/password.");const o=s?.[1]||"User",a=n?parseInt(n[1],10):0,r=e.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);return r&&(this.csrfNonce=r[1]),{id:a,name:o,score:0,team_id:0,country:""}}async getChallenges(){return this.request("GET","/challenges")}async getChallenge(t){return this.request("GET",`/challenges/${t}`)}async submitFlag(t,e){this.sessionCookie&&!this.csrfNonce&&await this.fetchCsrfNonce();const s=await fetch(`${this.baseUrl}/api/v1/challenges/attempt`,{method:"POST",headers:{...this.getAuthHeaders(),"Content-Type":"application/json"},body:JSON.stringify({challenge_id:t,submission:e})});if(!s.ok){const t=await s.text().catch(()=>"Unknown error");if(t.trimStart().startsWith("<"))throw new Error("Authentication failed: not signed in (server returned the login page). Re-run `join <url>` and check your username/password.");throw new Error(`CTFd API error (${s.status}): ${t}`)}return(await s.json()).data}async getScoreboard(){return this.request("GET","/scoreboard")}async getTeam(){return this.request("GET","/teams/me")}async getCompetitionMeta(){const t=await fetch(this.baseUrl),e=await t.text(),s=e.match(/'start'\s*:\s*(\d+)/),n=e.match(/'end'\s*:\s*(\d+)/),o=e.match(/'userMode'\s*:\s*"([^"]+)"/),a=e.match(/'csrfNonce'\s*:\s*"([^"]+)"/);return{start:s?parseInt(s[1],10):null,end:n?parseInt(n[1],10):null,userMode:o?.[1]||"users",csrfNonce:a?.[1]||""}}async getChallengeFiles(t){return(await this.getChallenge(t)).files||[]}async downloadFile(a,r){e(r,{recursive:!0});const i=a.startsWith("http")?a:`${this.baseUrl}/${a.replace(/^\//,"")}`,c=await fetch(i,{headers:this.getAuthHeaders(),redirect:"follow"});if(!c.ok||!c.body)throw new Error(`Failed to download: ${i}`);const h=(a.split("/").pop()||"file").split("?")[0],d=s(r,h),u=t(d);return await n(o.fromWeb(c.body),u),d}async getTokenViaIcoaApi(t,e){const s=JSON.stringify({name:t,password:e}),n={"Content-Type":"application/json"};try{const t=await fetch(`${this.baseUrl}/api/icoa/token`,{method:"POST",headers:n,body:s,signal:AbortSignal.timeout(5e3)});if(t.ok){const e=await t.json();if(e.success&&e.data?.token)return e.data.token}}catch{}try{const t=await fetch(`${this.baseUrl}:9090/api/icoa/token`,{method:"POST",headers:n,body:s,signal:AbortSignal.timeout(5e3)});if(t.ok){const e=await t.json();if(e.success&&e.data?.token)return e.data.token}}catch{}return null}async tokenAuthenticates(t){try{const e=await fetch(`${this.baseUrl}/api/v1/users/me`,{headers:{Authorization:`Token ${t}`},redirect:"manual",signal:AbortSignal.timeout(5e3)});if(200!==e.status)return!1;if(!(e.headers.get("content-type")||"").includes("application/json"))return!1;const s=await e.json();return!0===s?.success}catch{return!1}}async loginWithCredentials(t,e){const s=await this.getTokenViaIcoaApi(t,e);if(s&&await this.tokenAuthenticates(s))return{token:s,session:"",csrf:""};const n=await fetch(`${this.baseUrl}/login`),o=await n.text(),a=o.match(/name="nonce"[^>]*value="([^"]+)"/)||o.match(/value="([^"]+)"[^>]*name="nonce"/)||o.match(/id="nonce"[^>]*value="([^"]+)"/)||o.match(/csrfNonce['":\s]+['"]([^'"]+)['"]/),r=a?.[1]||"";if(!r)throw new Error("Could not extract CSRF nonce from login page.");const i=n.headers.getSetCookie?.()||[],c=i.map(t=>t.split(";")[0]).join("; "),h=await fetch(`${this.baseUrl}/login`,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded",Cookie:c},body:new URLSearchParams({name:t,password:e,nonce:r,_submit:"Submit"}),redirect:"manual"}),d=h.headers.getSetCookie?.()||[],u=new Map;for(const t of[...i,...d]){const e=t.split(";")[0],s=e.indexOf("=");s>0&&u.set(e.slice(0,s),e.slice(s+1))}const l=[...u.entries()].map(([t,e])=>`${t}=${e}`).join("; "),f=h.headers.get("location")||"";if(!(h.status>=300&&h.status<400)||f.includes("/login"))throw new Error("Invalid username or password.");try{const t=await fetch(`${this.baseUrl}/settings`,{headers:{Cookie:l}}),e=(await t.text()).match(/csrfNonce['":\s]+['"]([^'"]+)['"]/),s=e?.[1]||r,n=await fetch(`${this.baseUrl}/api/v1/tokens`,{method:"POST",headers:{"Content-Type":"application/json",Cookie:l,"CSRF-Token":s},body:JSON.stringify({expiration:"2026-12-31T23:59:59+00:00"})});if(n.ok){const t=await n.json();if(t.success&&t.data?.value)return{token:t.data.value,session:"",csrf:""}}}catch{}let w="";try{const t=await fetch(`${this.baseUrl}/challenges`,{headers:{Cookie:l}}),e=(await t.text()).match(/csrfNonce['":\s]+['"]([^'"]+)['"]/);e&&(w=e[1])}catch{}return{token:"",session:l,csrf:w}}}
@@ -1 +1 @@
1
- import chalk from"chalk";import{spawn as t}from"node:child_process";import{existsSync as n,mkdirSync as o,writeFileSync as a}from"node:fs";import{join as r}from"node:path";import{getIcoaDir as e}from"./config.js";export function getLangCacheDir(t){return r(e(),"translations",t)}export function isLangCached(t){if("en"===t)return!0;const o=getLangCacheDir(t);return n(r(o,".cached"))&&n(r(o,"ui.json"))}export async function ensureLangCache(n,e={}){if("en"===n)return!0;if(isLangCached(n))return!0;const i=getLangCacheDir(n);o(i,{recursive:!0}),e.silent||console.log(chalk.gray(` Fetching ${n} translation pack from icoa2026.au ...`));try{const o=`https://icoa2026.au/assets/translations/${n}.tar.gz`,s=await fetch(o);if(!s.ok)return e.silent||console.log(chalk.yellow(` Translation pack not available (HTTP ${s.status}). The interface stays in English; per-question translations fall back to the on-the-fly translator.`)),!1;const c=r(i,`${n}.tar.gz`);return a(c,Buffer.from(await s.arrayBuffer())),await new Promise((n,o)=>{const a=t("tar",["-xzf",c,"-C",i],{stdio:"ignore"});a.on("close",t=>0===t?n():o(new Error(`tar exit ${t}`))),a.on("error",o)}),a(r(i,".cached"),(new Date).toISOString()),e.silent||console.log(chalk.green(` ✓ ${n} translations cached at ~/.icoa/translations/${n}/`)),!0}catch(t){return e.silent||(console.log(chalk.yellow(` Translation fetch failed: ${t instanceof Error?t.message:String(t)}`)),console.log(chalk.gray(" Falling back to on-the-fly translation for individual questions."))),!1}}
1
+ import chalk from"chalk";import{spawn as t}from"node:child_process";import{existsSync as n,mkdirSync as o,writeFileSync as a}from"node:fs";import{join as r}from"node:path";import{getIcoaDir as e}from"./config.js";export function getLangCacheDir(t){return r(e(),"translations",t)}export function isLangCached(t){if("en"===t)return!0;const o=getLangCacheDir(t);return n(r(o,".cached"))}export async function ensureLangCache(n,e={}){if("en"===n)return!0;if(isLangCached(n))return!0;const i=getLangCacheDir(n);o(i,{recursive:!0}),e.silent||console.log(chalk.gray(` Fetching ${n} translation pack from icoa2026.au ...`));try{const o=`https://icoa2026.au/assets/translations/${n}.tar.gz`,s=await fetch(o);if(!s.ok)return e.silent||console.log(chalk.yellow(` Translation pack not available (HTTP ${s.status}). The interface stays in English; per-question translations fall back to the on-the-fly translator.`)),!1;const c=r(i,`${n}.tar.gz`);return a(c,Buffer.from(await s.arrayBuffer())),await new Promise((n,o)=>{const a=t("tar",["-xzf",c,"-C",i],{stdio:"ignore"});a.on("close",t=>0===t?n():o(new Error(`tar exit ${t}`))),a.on("error",o)}),a(r(i,".cached"),(new Date).toISOString()),e.silent||console.log(chalk.green(` ✓ ${n} translations cached at ~/.icoa/translations/${n}/`)),!0}catch(t){return e.silent||(console.log(chalk.yellow(` Translation fetch failed: ${t instanceof Error?t.message:String(t)}`)),console.log(chalk.gray(" Falling back to on-the-fly translation for individual questions."))),!1}}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "icoa-cli",
3
- "version": "2.19.303",
3
+ "version": "2.19.304",
4
4
  "description": "ICOA CLI — The world's first CLI-native cyber & AI security olympiad terminal: AI4CTF (Day 1), CTF4AI (Day 2), VLA4CTF (Pioneer Round — embodied AI)",
5
5
  "type": "module",
6
6
  "bin": {