@b01lers/ctfd-api 1.2.2 → 2.0.1

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/index.js CHANGED
@@ -1,5 +1,147 @@
1
1
  'use strict';
2
2
 
3
+ function createChallengesMethods(client) {
4
+ return {
5
+ /**
6
+ * Attempts to submit a flag for the given challenge.
7
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/challenges/operation/post_challenge_attempt}
8
+ *
9
+ * @param id The ID of the challenge to submit a flag for.
10
+ * @param flag The flag to submit.
11
+ * @returns The status of the flag submission.
12
+ */
13
+ async submitFlag(id, flag) {
14
+ const { session, nonce } = await client.getAuthedSessionNonce();
15
+ const res = await (await fetch(`${client.url}/api/v1/challenges/attempt`, {
16
+ method: "POST",
17
+ headers: {
18
+ "Content-Type": "application/json",
19
+ "Csrf-Token": nonce,
20
+ cookie: session
21
+ },
22
+ body: JSON.stringify({ challenge_id: id, submission: flag })
23
+ })).json();
24
+ return res.data;
25
+ },
26
+ /**
27
+ * Fetches the list of all challenges.
28
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/challenges/operation/get_challenge_list}
29
+ *
30
+ * @returns The list of challenges, as a `Challenge[]`.
31
+ */
32
+ async get() {
33
+ return client.callApi("/challenges");
34
+ },
35
+ /**
36
+ * Fetches details for the given challenge.
37
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/challenges/operation/get_challenge}.
38
+ *
39
+ * @param id The ID of the challenge to fetch.
40
+ * @returns The challenge details.
41
+ */
42
+ async getDetails(id) {
43
+ return client.callApi(`/challenges/${id}`);
44
+ },
45
+ /**
46
+ * Fetches solves for the given challenge.
47
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/challenges/operation/get_challenge_solves}.
48
+ *
49
+ * @param id The ID of the challenge to fetch solves for.
50
+ * @returns The challenge's solves.
51
+ */
52
+ async getSolves(id) {
53
+ return client.callApi(`/challenges/${id}/solves`);
54
+ }
55
+ };
56
+ }
57
+
58
+ function createScoreboardMethods(client) {
59
+ return {
60
+ /**
61
+ * Fetches the list of scoreboard entries.
62
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/scoreboard/operation/get_scoreboard_list}
63
+ *
64
+ * @returns The scoreboard, as a `ScoreboardEntry[]`.
65
+ */
66
+ async get() {
67
+ return client.callApi("/scoreboard");
68
+ },
69
+ /**
70
+ * Fetches details for the top `n` teams on the scoreboard.
71
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/scoreboard/operation/get_scoreboard_detail}
72
+ *
73
+ * @param count The number of teams to fetch.
74
+ * @returns The scoreboard details.
75
+ */
76
+ async getTop(count) {
77
+ return client.callApi(`/scoreboard/top/${count}`);
78
+ }
79
+ };
80
+ }
81
+
82
+ function createUsersMethods(client) {
83
+ return {
84
+ me: {
85
+ /**
86
+ * Retrieves the currently logged-in user.
87
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_private}
88
+ * @returns The logged-in user.
89
+ */
90
+ async get() {
91
+ return client.callApi("/users/me");
92
+ },
93
+ /**
94
+ * Retrieves solves for the currently logged-in user.
95
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_private_solves}
96
+ *
97
+ * @returns The logged-in user's solves.
98
+ */
99
+ async getSolves() {
100
+ return client.callApi("/users/me/solves");
101
+ },
102
+ /**
103
+ * Retrieves awards for the currently logged-in user.
104
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_private_awards}
105
+ *
106
+ * @returns The logged-in user's awards.
107
+ */
108
+ async getAwards() {
109
+ return client.callApi("/users/me/awards");
110
+ }
111
+ },
112
+ /**
113
+ * Retrieves a user by ID.
114
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_public}
115
+ *
116
+ * @param id The ID of the user to retrieve.
117
+ * @returns The fetched user.
118
+ */
119
+ async getById(id) {
120
+ return client.callApi(`/users/${id}`);
121
+ },
122
+ /**
123
+ * Retrieves solves for a given user.
124
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_public_solves}
125
+ *
126
+ * @param id The ID of the user to retrieve solves for.
127
+ * @returns The user's solves.
128
+ */
129
+ async getSolves(id) {
130
+ return client.callApi(`/users/${id}/solves`);
131
+ },
132
+ /**
133
+ * Retrieves awards for a given user.
134
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_public_awards}
135
+ *
136
+ * @param id The ID of the user to retrieve awards for.
137
+ * @returns The user's awards.
138
+ */
139
+ async getAwards(id) {
140
+ return client.callApi(`/users/${id}/awards`);
141
+ }
142
+ };
143
+ }
144
+
3
145
  function extractNonce(raw) {
4
146
  return raw.match(/'csrfNonce': "(.+?)"/)[1];
5
147
  }
@@ -15,40 +157,19 @@ class CTFdClient {
15
157
  __publicField(this, "cachedSession", null);
16
158
  __publicField(this, "cachedNonce", null);
17
159
  __publicField(this, "sessionExpiry", /* @__PURE__ */ new Date());
160
+ __publicField(this, "challenges");
161
+ __publicField(this, "scoreboard");
162
+ __publicField(this, "users");
18
163
  this.url = options.url.endsWith("/") ? options.url.slice(0, -1) : options.url;
19
164
  this.username = options.username;
20
165
  this.password = options.password;
166
+ this.challenges = createChallengesMethods(this);
167
+ this.scoreboard = createScoreboardMethods(this);
168
+ this.users = createUsersMethods(this);
21
169
  }
22
- async submitFlag(id, flag) {
23
- const { session, nonce } = await this.getAuthedSessionNonce();
24
- const res = await (await fetch(`${this.url}/api/v1/challenges/attempt`, {
25
- method: "POST",
26
- headers: {
27
- "Content-Type": "application/json",
28
- "Csrf-Token": nonce,
29
- cookie: session
30
- },
31
- body: JSON.stringify({ challenge_id: id, submission: flag })
32
- })).json();
33
- return res.data;
34
- }
35
- async getChallenges() {
36
- const { session } = await this.getAuthedSessionNonce();
37
- const res = await (await fetch(`${this.url}/api/v1/challenges`, {
38
- headers: { cookie: session }
39
- })).json();
40
- return res.data;
41
- }
42
- async getChallengeDetails(id) {
43
- const { session } = await this.getAuthedSessionNonce();
44
- const res = await (await fetch(`${this.url}/api/v1/challenges/${id}`, {
45
- headers: { cookie: session }
46
- })).json();
47
- return res.data;
48
- }
49
- async getScoreboard() {
170
+ async callApi(endpoint) {
50
171
  const { session, nonce } = await this.getAuthedSessionNonce();
51
- const res = await (await fetch(`${this.url}/api/v1/scoreboard`, {
172
+ const res = await (await fetch(`${this.url}/api/v1${endpoint}`, {
52
173
  headers: {
53
174
  "Csrf-Token": nonce,
54
175
  cookie: session
@@ -56,22 +177,23 @@ class CTFdClient {
56
177
  })).json();
57
178
  return res.data;
58
179
  }
180
+ // TODO
59
181
  async getAuthedSessionNonce() {
60
182
  if (/* @__PURE__ */ new Date() < this.sessionExpiry && this.cachedSession && this.cachedNonce)
61
183
  return { session: this.cachedSession, nonce: this.cachedNonce };
62
184
  const res = await fetch(`${this.url}/login`);
63
185
  const [session] = res.headers.getSetCookie()[0].split("; ");
64
186
  const nonce = extractNonce(await res.text());
65
- const formData = new URLSearchParams();
66
- formData.append("name", this.username);
67
- formData.append("password", this.password);
68
- formData.append("_submit", "Submit");
69
- formData.append("nonce", nonce);
187
+ const params = new URLSearchParams();
188
+ params.append("name", this.username);
189
+ params.append("password", this.password);
190
+ params.append("_submit", "Submit");
191
+ params.append("nonce", nonce);
70
192
  const loginRes = await fetch(`${this.url}/login`, {
71
193
  method: "POST",
72
194
  headers: { cookie: session },
73
195
  redirect: "manual",
74
- body: formData
196
+ body: params
75
197
  });
76
198
  const [authedSession, expiresGmt] = loginRes.headers.getSetCookie()[0].split("; ");
77
199
  const authedRaw = await (await fetch(`${this.url}/challenges`, {
package/dist/types.d.ts CHANGED
@@ -1,30 +1,11 @@
1
- type BaseScoreboardEntry = {
2
- pos: number;
1
+ interface Solve {
3
2
  account_id: number;
4
3
  account_url: string;
5
- oauth_id: null;
4
+ date: string;
6
5
  name: string;
7
- score: number;
8
- bracket_id: number | null;
9
- bracket_name: string | null;
10
- };
11
- type ScoreboardUserEntry = BaseScoreboardEntry & {
12
- account_type: "user";
13
- };
14
- type ScoreboardTeamEntry = BaseScoreboardEntry & {
15
- account_type: "team";
16
- members: {
17
- id: number;
18
- oauth_id: null;
19
- name: string;
20
- score: number;
21
- bracket_id: null;
22
- bracket_name: null;
23
- }[];
24
- };
25
- type ScoreboardEntry = ScoreboardUserEntry | ScoreboardTeamEntry;
6
+ }
26
7
  type ChallengeType = 'standard' | 'multiple_choice' | 'code';
27
- type ChallengeData = {
8
+ interface Challenge {
28
9
  id: number;
29
10
  type: ChallengeType;
30
11
  name: string;
@@ -35,7 +16,7 @@ type ChallengeData = {
35
16
  tags: string[];
36
17
  template: string;
37
18
  value: number;
38
- };
19
+ }
39
20
  type BaseChallengeDetails = {
40
21
  id: number;
41
22
  name: string;
@@ -80,19 +61,15 @@ type ProgrammingChallengeDetails = BaseChallengeDetails & {
80
61
  };
81
62
  type ChallengeDetails = StandardChallengeDetails | ProgrammingChallengeDetails;
82
63
 
83
- type ClientOptions = {
84
- url: string;
85
- username: string;
86
- password: string;
87
- };
88
- declare class CTFdClient {
89
- private readonly url;
90
- private readonly username;
91
- private readonly password;
92
- private cachedSession;
93
- private cachedNonce;
94
- private sessionExpiry;
95
- constructor(options: ClientOptions);
64
+ declare function createChallengesMethods(client: CTFdClient): {
65
+ /**
66
+ * Attempts to submit a flag for the given challenge.
67
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/challenges/operation/post_challenge_attempt}
68
+ *
69
+ * @param id The ID of the challenge to submit a flag for.
70
+ * @param flag The flag to submit.
71
+ * @returns The status of the flag submission.
72
+ */
96
73
  submitFlag(id: number, flag: string): Promise<{
97
74
  status: "incorrect";
98
75
  message: "Incorrect";
@@ -109,10 +86,206 @@ declare class CTFdClient {
109
86
  status: "ratelimited";
110
87
  message: "You're submitting flags too fast. Slow down.";
111
88
  }>;
112
- getChallenges(): Promise<ChallengeData[]>;
113
- getChallengeDetails(id: number): Promise<ChallengeDetails>;
114
- getScoreboard(): Promise<ScoreboardEntry[]>;
115
- private getAuthedSessionNonce;
89
+ /**
90
+ * Fetches the list of all challenges.
91
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/challenges/operation/get_challenge_list}
92
+ *
93
+ * @returns The list of challenges, as a `Challenge[]`.
94
+ */
95
+ get(): Promise<Challenge[]>;
96
+ /**
97
+ * Fetches details for the given challenge.
98
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/challenges/operation/get_challenge}.
99
+ *
100
+ * @param id The ID of the challenge to fetch.
101
+ * @returns The challenge details.
102
+ */
103
+ getDetails(id: number): Promise<ChallengeDetails>;
104
+ /**
105
+ * Fetches solves for the given challenge.
106
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/challenges/operation/get_challenge_solves}.
107
+ *
108
+ * @param id The ID of the challenge to fetch solves for.
109
+ * @returns The challenge's solves.
110
+ */
111
+ getSolves(id: number): Promise<Solve[]>;
112
+ };
113
+
114
+ type BaseScoreboardEntry = {
115
+ pos: number;
116
+ account_id: number;
117
+ account_url: string;
118
+ oauth_id: null;
119
+ name: string;
120
+ score: number;
121
+ bracket_id: number | null;
122
+ bracket_name: string | null;
123
+ };
124
+ type ScoreboardUserEntry = BaseScoreboardEntry & {
125
+ account_type: "user";
126
+ };
127
+ type ScoreboardTeamEntry = BaseScoreboardEntry & {
128
+ account_type: "team";
129
+ members: {
130
+ id: number;
131
+ oauth_id: null;
132
+ name: string;
133
+ score: number;
134
+ bracket_id: null;
135
+ bracket_name: null;
136
+ }[];
137
+ };
138
+ type ScoreboardEntry = ScoreboardUserEntry | ScoreboardTeamEntry;
139
+ interface ScoreboardDetails {
140
+ id: number;
141
+ account_url: string;
142
+ name: string;
143
+ score: number;
144
+ bracket_id: null;
145
+ bracket_name: null;
146
+ solves: {
147
+ challenge_id: number;
148
+ account_id: number;
149
+ team_id: number | null;
150
+ user_id: number;
151
+ value: number;
152
+ date: string;
153
+ }[];
154
+ }
155
+
156
+ declare function createScoreboardMethods(client: CTFdClient): {
157
+ /**
158
+ * Fetches the list of scoreboard entries.
159
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/scoreboard/operation/get_scoreboard_list}
160
+ *
161
+ * @returns The scoreboard, as a `ScoreboardEntry[]`.
162
+ */
163
+ get(): Promise<ScoreboardEntry[]>;
164
+ /**
165
+ * Fetches details for the top `n` teams on the scoreboard.
166
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/scoreboard/operation/get_scoreboard_detail}
167
+ *
168
+ * @param count The number of teams to fetch.
169
+ * @returns The scoreboard details.
170
+ */
171
+ getTop(count: number): Promise<ScoreboardDetails[]>;
172
+ };
173
+
174
+ interface User {
175
+ id: number;
176
+ oauth_id: number | null;
177
+ name: string;
178
+ email?: string;
179
+ website: string | null;
180
+ affiliation: string | null;
181
+ country: string | null;
182
+ bracket_id: number | null;
183
+ language: string | null;
184
+ team_id: number | null;
185
+ created: string;
186
+ fields: [];
187
+ }
188
+ interface Award {
189
+ description: string | null;
190
+ date: string;
191
+ id: number;
192
+ category: string;
193
+ user_id: number;
194
+ team_id: null;
195
+ name: string;
196
+ user: number;
197
+ team: null;
198
+ value: number;
199
+ icon: string;
200
+ }
201
+
202
+ interface UserSolve {
203
+ user: {
204
+ name: string;
205
+ id: number;
206
+ };
207
+ date: string;
208
+ challenge_id: number;
209
+ challenge: {
210
+ value: number;
211
+ name: string;
212
+ id: number;
213
+ category: string;
214
+ };
215
+ team: number | null;
216
+ id: number;
217
+ type: "correct";
218
+ }
219
+ declare function createUsersMethods(client: CTFdClient): {
220
+ me: {
221
+ /**
222
+ * Retrieves the currently logged-in user.
223
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_private}
224
+ * @returns The logged-in user.
225
+ */
226
+ get(): Promise<User>;
227
+ /**
228
+ * Retrieves solves for the currently logged-in user.
229
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_private_solves}
230
+ *
231
+ * @returns The logged-in user's solves.
232
+ */
233
+ getSolves(): Promise<UserSolve[]>;
234
+ /**
235
+ * Retrieves awards for the currently logged-in user.
236
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_private_awards}
237
+ *
238
+ * @returns The logged-in user's awards.
239
+ */
240
+ getAwards(): Promise<Award[]>;
241
+ };
242
+ /**
243
+ * Retrieves a user by ID.
244
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_public}
245
+ *
246
+ * @param id The ID of the user to retrieve.
247
+ * @returns The fetched user.
248
+ */
249
+ getById(id: number): Promise<User>;
250
+ /**
251
+ * Retrieves solves for a given user.
252
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_public_solves}
253
+ *
254
+ * @param id The ID of the user to retrieve solves for.
255
+ * @returns The user's solves.
256
+ */
257
+ getSolves(id: number): Promise<UserSolve[]>;
258
+ /**
259
+ * Retrieves awards for a given user.
260
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_public_awards}
261
+ *
262
+ * @param id The ID of the user to retrieve awards for.
263
+ * @returns The user's awards.
264
+ */
265
+ getAwards(id: number): Promise<Award[]>;
266
+ };
267
+
268
+ type ClientOptions = {
269
+ url: string;
270
+ username: string;
271
+ password: string;
272
+ };
273
+ declare class CTFdClient {
274
+ readonly url: string;
275
+ private readonly username;
276
+ private readonly password;
277
+ private cachedSession;
278
+ private cachedNonce;
279
+ private sessionExpiry;
280
+ readonly challenges: ReturnType<typeof createChallengesMethods>;
281
+ readonly scoreboard: ReturnType<typeof createScoreboardMethods>;
282
+ readonly users: ReturnType<typeof createUsersMethods>;
283
+ constructor(options: ClientOptions);
284
+ callApi<T>(endpoint: string): Promise<T>;
285
+ getAuthedSessionNonce(): Promise<{
286
+ session: string;
287
+ nonce: string;
288
+ }>;
116
289
  }
117
290
 
118
- export { CTFdClient, type ChallengeData, type ChallengeDetails, type ChallengeType, type ScoreboardEntry };
291
+ export { type Award, CTFdClient, type Challenge, type ChallengeDetails, type ChallengeType, type ScoreboardEntry, type Solve, type User, type UserSolve };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b01lers/ctfd-api",
3
- "version": "1.2.2",
3
+ "version": "2.0.1",
4
4
  "description": "An API client for CTFd.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/types.d.ts",
package/src/client.ts CHANGED
@@ -1,10 +1,8 @@
1
+ import { createChallengesMethods } from './endpoints/challenges';
2
+ import { createScoreboardMethods } from './endpoints/scoreboard';
3
+ import { createUsersMethods } from './endpoints/users';
1
4
  import { extractNonce } from './util';
2
- import type {
3
- ChallengeDetailsResponse,
4
- ChallengesResponse,
5
- FlagSubmissionResponse,
6
- ScoreboardResponse
7
- } from './types';
5
+ import type { APISuccess } from './types/api';
8
6
 
9
7
 
10
8
  type ClientOptions = {
@@ -14,7 +12,7 @@ type ClientOptions = {
14
12
  }
15
13
 
16
14
  export class CTFdClient {
17
- private readonly url: string;
15
+ public readonly url: string;
18
16
  private readonly username: string;
19
17
  private readonly password: string;
20
18
 
@@ -22,62 +20,36 @@ export class CTFdClient {
22
20
  private cachedNonce: string | null = null;
23
21
  private sessionExpiry = new Date();
24
22
 
23
+ public readonly challenges: ReturnType<typeof createChallengesMethods>;
24
+ public readonly scoreboard: ReturnType<typeof createScoreboardMethods>;
25
+ public readonly users: ReturnType<typeof createUsersMethods>;
26
+
25
27
  constructor(options: ClientOptions) {
26
28
  this.url = options.url.endsWith('/') ? options.url.slice(0, -1) : options.url;
27
29
  this.username = options.username;
28
30
  this.password = options.password;
29
- }
30
-
31
- public async submitFlag(id: number, flag: string) {
32
- const { session, nonce } = await this.getAuthedSessionNonce();
33
31
 
34
- const res = await (await fetch(`${this.url}/api/v1/challenges/attempt`, {
35
- method: 'POST',
36
- headers: {
37
- 'Content-Type': 'application/json',
38
- 'Csrf-Token': nonce,
39
- cookie: session,
40
- },
41
- body: JSON.stringify({ challenge_id: id, submission: flag }),
42
- })).json() as FlagSubmissionResponse;
43
-
44
- return res.data;
45
- }
46
-
47
- public async getChallenges() {
48
- const { session } = await this.getAuthedSessionNonce();
49
-
50
- const res = await (await fetch(`${this.url}/api/v1/challenges`, {
51
- headers: { cookie: session }
52
- })).json() as ChallengesResponse;
53
-
54
- return res.data;
55
- }
56
-
57
- public async getChallengeDetails(id: number) {
58
- const { session } = await this.getAuthedSessionNonce();
59
-
60
- const res = await (await fetch(`${this.url}/api/v1/challenges/${id}`, {
61
- headers: { cookie: session }
62
- })).json() as ChallengeDetailsResponse;
63
-
64
- return res.data;
32
+ this.challenges = createChallengesMethods(this);
33
+ this.scoreboard = createScoreboardMethods(this);
34
+ this.users = createUsersMethods(this);
65
35
  }
66
36
 
67
- public async getScoreboard() {
37
+ public async callApi<T>(endpoint: string) {
68
38
  const { session, nonce } = await this.getAuthedSessionNonce();
69
39
 
70
- const res = await (await fetch(`${this.url}/api/v1/scoreboard`, {
40
+ // TODO: error handling?
41
+ const res = await (await fetch(`${this.url}/api/v1${endpoint}`, {
71
42
  headers: {
72
43
  'Csrf-Token': nonce,
73
44
  cookie: session,
74
45
  },
75
- })).json() as ScoreboardResponse;
46
+ })).json() as APISuccess<T>;
76
47
 
77
48
  return res.data;
78
49
  }
79
50
 
80
- private async getAuthedSessionNonce() {
51
+ // TODO
52
+ public async getAuthedSessionNonce() {
81
53
  // If we have a cached, non-expired session, use it
82
54
  if (new Date() < this.sessionExpiry && this.cachedSession && this.cachedNonce)
83
55
  return { session: this.cachedSession, nonce: this.cachedNonce };
@@ -87,17 +59,17 @@ export class CTFdClient {
87
59
  const [session] = res.headers.getSetCookie()[0].split('; ');
88
60
  const nonce = extractNonce(await res.text());
89
61
 
90
- const formData = new URLSearchParams();
91
- formData.append('name', this.username);
92
- formData.append('password', this.password);
93
- formData.append('_submit', 'Submit');
94
- formData.append('nonce', nonce);
62
+ const params = new URLSearchParams();
63
+ params.append('name', this.username);
64
+ params.append('password', this.password);
65
+ params.append('_submit', 'Submit');
66
+ params.append('nonce', nonce);
95
67
 
96
68
  const loginRes = await fetch(`${this.url}/login`, {
97
69
  method: 'POST',
98
70
  headers: { cookie: session },
99
71
  redirect: 'manual',
100
- body: formData,
72
+ body: params,
101
73
  });
102
74
 
103
75
  const [authedSession, expiresGmt] = loginRes.headers.getSetCookie()[0].split('; ');
@@ -0,0 +1,81 @@
1
+ import type { CTFdClient } from '../client';
2
+ import type { Challenge, ChallengeDetails, Solve } from '../types/challenges';
3
+ import type { APISuccess } from '../types/api';
4
+
5
+
6
+ export type FlagSubmissionResponse = APISuccess<{
7
+ status: 'incorrect',
8
+ message: 'Incorrect'
9
+ } | {
10
+ status: 'correct',
11
+ message: 'Correct'
12
+ } | {
13
+ status: 'already_solved',
14
+ message: 'You already solved this'
15
+ } | {
16
+ status: 'paused',
17
+ message: `${string} is paused`
18
+ } | {
19
+ status: 'ratelimited',
20
+ message: 'You\'re submitting flags too fast. Slow down.'
21
+ }>
22
+
23
+ export function createChallengesMethods(client: CTFdClient) {
24
+ return {
25
+ /**
26
+ * Attempts to submit a flag for the given challenge.
27
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/challenges/operation/post_challenge_attempt}
28
+ *
29
+ * @param id The ID of the challenge to submit a flag for.
30
+ * @param flag The flag to submit.
31
+ * @returns The status of the flag submission.
32
+ */
33
+ async submitFlag(id: number, flag: string) {
34
+ const { session, nonce } = await client.getAuthedSessionNonce();
35
+
36
+ const res = await (await fetch(`${client.url}/api/v1/challenges/attempt`, {
37
+ method: 'POST',
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ 'Csrf-Token': nonce,
41
+ cookie: session,
42
+ },
43
+ body: JSON.stringify({ challenge_id: id, submission: flag }),
44
+ })).json() as FlagSubmissionResponse;
45
+
46
+ return res.data;
47
+ },
48
+
49
+ /**
50
+ * Fetches the list of all challenges.
51
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/challenges/operation/get_challenge_list}
52
+ *
53
+ * @returns The list of challenges, as a `Challenge[]`.
54
+ */
55
+ async get() {
56
+ return client.callApi<Challenge[]>('/challenges');
57
+ },
58
+
59
+ /**
60
+ * Fetches details for the given challenge.
61
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/challenges/operation/get_challenge}.
62
+ *
63
+ * @param id The ID of the challenge to fetch.
64
+ * @returns The challenge details.
65
+ */
66
+ async getDetails(id: number) {
67
+ return client.callApi<ChallengeDetails>(`/challenges/${id}`);
68
+ },
69
+
70
+ /**
71
+ * Fetches solves for the given challenge.
72
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/challenges/operation/get_challenge_solves}.
73
+ *
74
+ * @param id The ID of the challenge to fetch solves for.
75
+ * @returns The challenge's solves.
76
+ */
77
+ async getSolves(id: number) {
78
+ return client.callApi<Solve[]>(`/challenges/${id}/solves`);
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,28 @@
1
+ import type { CTFdClient } from '../client';
2
+ import type { ScoreboardDetails, ScoreboardEntry } from '../types/scoreboard';
3
+
4
+
5
+ export function createScoreboardMethods(client: CTFdClient) {
6
+ return {
7
+ /**
8
+ * Fetches the list of scoreboard entries.
9
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/scoreboard/operation/get_scoreboard_list}
10
+ *
11
+ * @returns The scoreboard, as a `ScoreboardEntry[]`.
12
+ */
13
+ async get() {
14
+ return client.callApi<ScoreboardEntry[]>('/scoreboard');
15
+ },
16
+
17
+ /**
18
+ * Fetches details for the top `n` teams on the scoreboard.
19
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/scoreboard/operation/get_scoreboard_detail}
20
+ *
21
+ * @param count The number of teams to fetch.
22
+ * @returns The scoreboard details.
23
+ */
24
+ async getTop(count: number) {
25
+ return client.callApi<ScoreboardDetails[]>(`/scoreboard/top/${count}`);
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,89 @@
1
+ import type { CTFdClient } from '../client';
2
+ import type { Award, User } from '../types/users';
3
+
4
+
5
+ export interface UserSolve {
6
+ user: {
7
+ name: string,
8
+ id: number,
9
+ },
10
+ date: string, // ISO
11
+ challenge_id: number,
12
+ challenge: {
13
+ value: number,
14
+ name: string,
15
+ id: number,
16
+ category: string,
17
+ },
18
+ team: number | null,
19
+ id: number,
20
+ type: "correct" // TODO?
21
+ }
22
+
23
+ export function createUsersMethods(client: CTFdClient) {
24
+ return {
25
+ me: {
26
+ /**
27
+ * Retrieves the currently logged-in user.
28
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_private}
29
+ * @returns The logged-in user.
30
+ */
31
+ async get() {
32
+ return client.callApi<User>('/users/me');
33
+ },
34
+
35
+ /**
36
+ * Retrieves solves for the currently logged-in user.
37
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_private_solves}
38
+ *
39
+ * @returns The logged-in user's solves.
40
+ */
41
+ async getSolves() {
42
+ return client.callApi<UserSolve[]>('/users/me/solves');
43
+ },
44
+
45
+ /**
46
+ * Retrieves awards for the currently logged-in user.
47
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_private_awards}
48
+ *
49
+ * @returns The logged-in user's awards.
50
+ */
51
+ async getAwards() {
52
+ return client.callApi<Award[]>('/users/me/awards');
53
+ }
54
+ },
55
+
56
+ /**
57
+ * Retrieves a user by ID.
58
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_public}
59
+ *
60
+ * @param id The ID of the user to retrieve.
61
+ * @returns The fetched user.
62
+ */
63
+ async getById(id: number) {
64
+ return client.callApi<User>(`/users/${id}`);
65
+ },
66
+
67
+ /**
68
+ * Retrieves solves for a given user.
69
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_public_solves}
70
+ *
71
+ * @param id The ID of the user to retrieve solves for.
72
+ * @returns The user's solves.
73
+ */
74
+ async getSolves(id: number) {
75
+ return client.callApi<UserSolve[]>(`/users/${id}/solves`);
76
+ },
77
+
78
+ /**
79
+ * Retrieves awards for a given user.
80
+ * Ref: {@Link https://docs.ctfd.io/docs/api/redoc#tag/users/operation/get_user_public_awards}
81
+ *
82
+ * @param id The ID of the user to retrieve awards for.
83
+ * @returns The user's awards.
84
+ */
85
+ async getAwards(id: number) {
86
+ return client.callApi<Award[]>(`/users/${id}/awards`);
87
+ }
88
+ }
89
+ }
package/src/index.ts CHANGED
@@ -1,2 +1,8 @@
1
1
  export { CTFdClient } from './client';
2
- export type { ScoreboardEntry, ChallengeType, ChallengeData, ChallengeDetails } from './types';
2
+
3
+ export type { ChallengeType, Challenge, ChallengeDetails, Solve } from './types/challenges'
4
+ export type { ScoreboardEntry } from './types/scoreboard';
5
+ export type { User, Award } from './types/users';
6
+
7
+ // TODO
8
+ export type { UserSolve } from './endpoints/users'
@@ -0,0 +1,4 @@
1
+ export type APISuccess<T> = {
2
+ success: true,
3
+ data: T
4
+ }
@@ -1,43 +1,12 @@
1
- type APISuccess<T> = {
2
- success: true,
3
- data: T
4
- }
5
-
6
- export type ScoreboardResponse = APISuccess<ScoreboardEntry[]>
7
-
8
- type BaseScoreboardEntry = {
9
- pos: number,
1
+ export interface Solve {
10
2
  account_id: number,
11
3
  account_url: string,
12
- oauth_id: null,
4
+ date: string,
13
5
  name: string,
14
- score: number,
15
- bracket_id: number | null,
16
- bracket_name: string | null // e.g. "Open Bracket"
17
6
  }
18
7
 
19
- type ScoreboardUserEntry = BaseScoreboardEntry & {
20
- account_type: "user",
21
- }
22
-
23
- type ScoreboardTeamEntry = BaseScoreboardEntry & {
24
- account_type: "team",
25
- members: {
26
- id: number,
27
- oauth_id: null,
28
- name: string,
29
- score: number,
30
- bracket_id: null,
31
- bracket_name: null
32
- }[]
33
- }
34
-
35
- export type ScoreboardEntry = ScoreboardUserEntry | ScoreboardTeamEntry;
36
-
37
- export type ChallengesResponse = APISuccess<ChallengeData[]>
38
-
39
8
  export type ChallengeType = 'standard' | 'multiple_choice' | 'code';
40
- export type ChallengeData = {
9
+ export interface Challenge {
41
10
  id: number,
42
11
  type: ChallengeType,
43
12
  name: string,
@@ -50,8 +19,6 @@ export type ChallengeData = {
50
19
  value: number,
51
20
  }
52
21
 
53
- export type ChallengeDetailsResponse = APISuccess<ChallengeDetails>;
54
-
55
22
  type BaseChallengeDetails = {
56
23
  id: number,
57
24
  name: string,
@@ -90,20 +57,3 @@ type ProgrammingChallengeDetails = BaseChallengeDetails & {
90
57
  }
91
58
 
92
59
  export type ChallengeDetails = StandardChallengeDetails | ProgrammingChallengeDetails;
93
-
94
- export type FlagSubmissionResponse = APISuccess<{
95
- status: 'incorrect',
96
- message: 'Incorrect'
97
- } | {
98
- status: 'correct',
99
- message: 'Correct'
100
- } | {
101
- status: 'already_solved',
102
- message: 'You already solved this'
103
- } | {
104
- status: 'paused',
105
- message: `${string} is paused`
106
- } | {
107
- status: 'ratelimited',
108
- message: 'You\'re submitting flags too fast. Slow down.'
109
- }>
@@ -0,0 +1,45 @@
1
+ type BaseScoreboardEntry = {
2
+ pos: number,
3
+ account_id: number,
4
+ account_url: string,
5
+ oauth_id: null, // TODO
6
+ name: string,
7
+ score: number,
8
+ bracket_id: number | null,
9
+ bracket_name: string | null // e.g. "Open Bracket"
10
+ }
11
+
12
+ type ScoreboardUserEntry = BaseScoreboardEntry & {
13
+ account_type: "user",
14
+ }
15
+
16
+ type ScoreboardTeamEntry = BaseScoreboardEntry & {
17
+ account_type: "team",
18
+ members: {
19
+ id: number,
20
+ oauth_id: null,
21
+ name: string,
22
+ score: number,
23
+ bracket_id: null,
24
+ bracket_name: null
25
+ }[]
26
+ }
27
+
28
+ export type ScoreboardEntry = ScoreboardUserEntry | ScoreboardTeamEntry;
29
+
30
+ export interface ScoreboardDetails {
31
+ id: number,
32
+ account_url: string,
33
+ name: string,
34
+ score: number,
35
+ bracket_id: null, // TODO
36
+ bracket_name: null,
37
+ solves: {
38
+ challenge_id: number,
39
+ account_id: number,
40
+ team_id: number | null,
41
+ user_id: number,
42
+ value: number,
43
+ date: string // ISO
44
+ }[]
45
+ }
@@ -0,0 +1,36 @@
1
+ export interface User {
2
+ id: number,
3
+ oauth_id: number | null,
4
+ name: string,
5
+ // password: string,
6
+ email?: string,
7
+ // type: string,
8
+ // secret: string,
9
+ website: string | null,
10
+ affiliation: string | null,
11
+ country: string | null,
12
+ bracket_id: number | null,
13
+ // hidden: boolean,
14
+ // banned: boolean,
15
+ // verified: boolean,
16
+ language: string | null,
17
+ // change_password: boolean,
18
+ team_id: number | null,
19
+ created: string, // ISO
20
+
21
+ fields: [], // TODO
22
+ }
23
+
24
+ export interface Award {
25
+ description: string | null,
26
+ date: string,
27
+ id: number,
28
+ category: string,
29
+ user_id: number,
30
+ team_id: null, // TODO
31
+ name: string,
32
+ user: number,
33
+ team: null, // TODO
34
+ value: number,
35
+ icon: string,
36
+ }
package/tests/demo.ts CHANGED
@@ -8,16 +8,25 @@ import { CTFdClient } from '../src/client';
8
8
  password: 'password',
9
9
  });
10
10
 
11
- const challs = await client.getChallenges();
11
+ const challs = await client.challenges.get();
12
12
  console.log(challs);
13
13
 
14
- const scoreboard = await client.getScoreboard();
14
+ const scoreboard = await client.scoreboard.get();
15
15
  console.log(scoreboard.slice(0, 5));
16
16
 
17
17
  const chall = challs.find((c) => c.name === 'The Lost Park')!;
18
- const details = await client.getChallengeDetails(chall.id);
18
+ const details = await client.challenges.getDetails(chall.id);
19
19
  console.log(details);
20
20
 
21
- const res = await client.submitFlag(chall.id, 'Major Mark Park');
21
+ const res = await client.challenges.submitFlag(chall.id, 'Major Mark Park');
22
22
  console.log(res);
23
+
24
+ const solves = await client.challenges.getSolves(chall.id);
25
+ console.log(solves);
26
+
27
+ const user = await client.users.me.get();
28
+ console.log(user);
29
+
30
+ // This should be equivalent to the above
31
+ console.log(await client.users.getById(user.id));
23
32
  })()
@@ -8,12 +8,12 @@ import { CTFdClient } from '../src/client';
8
8
  password: 'password',
9
9
  });
10
10
 
11
- const chall = (await client.getChallenges()).find((c) => c.name === 'Too Many Puppers')!;
11
+ const chall = (await client.challenges.get()).find((c) => c.name === 'The Lost Park')!;
12
12
 
13
- const res = await client.submitFlag(chall.id, 'test');
13
+ const res = await client.challenges.submitFlag(chall.id, 'test');
14
14
  console.log(res);
15
15
 
16
16
  for (let i = 0; i < 20; i++) {
17
- console.log(await client.submitFlag(chall.id, 'test'));
17
+ console.log(await client.challenges.submitFlag(chall.id, 'test'));
18
18
  }
19
19
  })()