reddy-api-srm 1.0.4 → 1.0.6

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.
@@ -0,0 +1,22 @@
1
+ export interface TerminateSessionsInput {
2
+ /** The concurrent session flow/ticket ID from validatePassword (isConcurrentLimit response) */
3
+ flowId: string | null;
4
+ /** Zoho identifier from verifyUser */
5
+ identifier: string;
6
+ /** Zoho digest from verifyUser */
7
+ digest: string;
8
+ /** Optional CSRF token (value of iamcsr cookie, without the prefix) */
9
+ csrfToken?: string;
10
+ }
11
+
12
+ export interface TerminateSessionsResult {
13
+ success: boolean;
14
+ status?: number;
15
+ /** Which strategy succeeded: "block-sessions" | "webclient-delete" | "none" */
16
+ strategy?: string;
17
+ data?: unknown;
18
+ error?: string;
19
+ errorReason?: unknown;
20
+ }
21
+
22
+ export declare function terminateSessions(params: TerminateSessionsInput): Promise<TerminateSessionsResult>;
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.terminateSessions = terminateSessions;
4
+
5
+ /**
6
+ * Terminates all concurrent/existing sessions for a user on SRM Academia.
7
+ *
8
+ * SRM uses Zoho Accounts for IAM. When the 2-device limit is hit during login,
9
+ * the server redirects to a "block-sessions" preannouncement page where the user
10
+ * can terminate old sessions to continue with the new login.
11
+ *
12
+ * Two strategies are tried in sequence:
13
+ *
14
+ * Strategy 1 – Block-Sessions page POST (login-time termination):
15
+ * POST https://academia.srmist.edu.in/accounts/p/40-10002227248/preannouncement/block-sessions
16
+ * with mode=terminate and the current login's digest/identifier.
17
+ * This is the endpoint hit when the user clicks "Terminate All Sessions" on the
18
+ * preannouncement page during a login attempt.
19
+ *
20
+ * Strategy 2 – webclient DELETE (session-ticket termination, used in "My Account → Sessions"):
21
+ * DELETE https://academia.srmist.edu.in/accounts/p/40-10002227248/webclient/v1/account/self/user/self/session/{SESSION_TICKET}
22
+ * with x-zcsrf-token header. Used to delete individual known sessions.
23
+ * flowId is used as the session ticket here if provided.
24
+ *
25
+ * @param {string} flowId - The flow/session ID from validatePassword (concurrent limit response)
26
+ * @param {string} identifier - Zoho identifier from verifyUser
27
+ * @param {string} digest - Zoho digest from verifyUser
28
+ * @param {string} [csrfToken] - Optional x-zcsrf-token value (extracted from iamcsr cookie)
29
+ */
30
+ async function terminateSessions({ flowId, identifier, digest, csrfToken }) {
31
+ // ── Strategy 1: Block-Sessions preannouncement POST ────────────────────────
32
+ // This is what fires when the user clicks "Terminate All Sessions" on the
33
+ // SRM concurrent-sessions warning page during a fresh login attempt.
34
+ try {
35
+ const blockSessionsUrl =
36
+ "https://academia.srmist.edu.in/accounts/p/40-10002227248/preannouncement/block-sessions";
37
+
38
+ const body = new URLSearchParams({
39
+ mode: "terminate",
40
+ identifier: identifier ?? "",
41
+ digest: digest ?? "",
42
+ cli_time: String(Date.now()),
43
+ servicename: "ZohoCreator",
44
+ service_language: "en",
45
+ serviceurl:
46
+ "https://academia.srmist.edu.in/portal/academia-academic-services/redirectFromLogin",
47
+ });
48
+
49
+ // Append flowId if we have it (some Zoho versions need it in the body)
50
+ if (flowId) body.append("flowId", flowId);
51
+
52
+ const res1 = await fetch(blockSessionsUrl, {
53
+ method: "POST",
54
+ headers: {
55
+ accept: "*/*",
56
+ "accept-language": "en-US,en;q=0.9",
57
+ "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
58
+ "sec-fetch-dest": "empty",
59
+ "sec-fetch-mode": "cors",
60
+ "sec-fetch-site": "same-origin",
61
+ ...(csrfToken
62
+ ? { "x-zcsrf-token": `iamcsrcoo=${csrfToken}` }
63
+ : {}),
64
+ Referer:
65
+ "https://academia.srmist.edu.in/accounts/p/10002227248/signin?hide_fp=true&servicename=ZohoCreator&service_language=en&dcc=true&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin",
66
+ "Referrer-Policy": "strict-origin-when-cross-origin",
67
+ },
68
+ body: body.toString(),
69
+ });
70
+
71
+ if (res1.ok || res1.status === 200 || res1.status === 201 || res1.redirected) {
72
+ let data = null;
73
+ try { data = await res1.json(); } catch { /* redirect or empty body is fine */ }
74
+ return { success: true, status: res1.status, strategy: "block-sessions", data };
75
+ }
76
+
77
+ console.warn("[terminateSessions] Strategy 1 (block-sessions POST) returned:", res1.status);
78
+ } catch (e1) {
79
+ console.warn("[terminateSessions] Strategy 1 error:", e1);
80
+ }
81
+
82
+ // ── Strategy 2: webclient DELETE by session ticket ─────────────────────────
83
+ // Used when we have a specific session ticket (flowId acts as session ticket).
84
+ // This is what the "My Account → Sessions" page uses to delete individual sessions.
85
+ if (flowId) {
86
+ try {
87
+ const deleteUrl = `https://academia.srmist.edu.in/accounts/p/40-10002227248/webclient/v1/account/self/user/self/session/${encodeURIComponent(flowId)}`;
88
+
89
+ const res2 = await fetch(deleteUrl, {
90
+ method: "DELETE",
91
+ headers: {
92
+ accept: "application/json, text/javascript, */*; q=0.01",
93
+ "accept-language": "en-US,en;q=0.9",
94
+ "x-requested-with": "XMLHttpRequest",
95
+ ...(csrfToken
96
+ ? { "x-zcsrf-token": `iamcsrcoo=${csrfToken}` }
97
+ : {}),
98
+ Referer: "https://accounts.zoho.in/",
99
+ "Referrer-Policy": "strict-origin-when-cross-origin",
100
+ },
101
+ });
102
+
103
+ if (res2.ok || res2.status === 200 || res2.status === 204) {
104
+ let data = null;
105
+ try { data = await res2.json(); } catch { /* 204 No Content is fine */ }
106
+ return { success: true, status: res2.status, strategy: "webclient-delete", data };
107
+ }
108
+
109
+ console.warn("[terminateSessions] Strategy 2 (webclient DELETE) returned:", res2.status);
110
+ } catch (e2) {
111
+ console.warn("[terminateSessions] Strategy 2 error:", e2);
112
+ }
113
+ }
114
+
115
+ // Both strategies failed — return a non-fatal "best-effort" result.
116
+ // The caller (handleTerminateAndRetry) will still attempt to re-login because
117
+ // sometimes the termination succeeds server-side even if we get an odd HTTP response.
118
+ return {
119
+ success: false,
120
+ error: "Both termination strategies failed — re-login will still be attempted",
121
+ strategy: "none",
122
+ };
123
+ }
@@ -11,7 +11,9 @@ async function validatePassword({ identifier, digest, password, }) {
11
11
  "sec-fetch-dest": "empty",
12
12
  "sec-fetch-mode": "cors",
13
13
  "sec-fetch-site": "same-origin",
14
- Referer: "https://academia.srmist.edu.in/accounts/p/10002227248/signin?hide_fp=true&servicename=ZohoCreator&service_language=en&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin",
14
+ "x-zcsrf-token": "iamcsrcoo=fae2d8fa-e5a1-4cb0-a5ee-cc40af87e89f",
15
+ cookie: "zalb_74c3a1eecc=4cad43ac9848cc7edd20d2313fcde774; zccpn=a6fa7bc8-11c7-44ad-8be8-0aa6b04fad8a; JSESSIONID=3BD0053672AF3D628D983A15AA469D07; cli_rgn=IN; _ga=GA1.3.2061081340.1748689001; _gid=GA1.3.1677956689.1748689001; _ga_HQWPLLNMKY=GS2.3.s1748689001$o1$g0$t1748689001$j60$l0$h0; zalb_f0e8db9d3d=7ad3232c36fdd9cc324fb86c2c0a58ad; iamcsr=fae2d8fa-e5a1-4cb0-a5ee-cc40af87e89f; _zcsr_tmp=fae2d8fa-e5a1-4cb0-a5ee-cc40af87e89f; stk=d6559e9a58e77dbea9e24adf3bb57941",
16
+ Referer: "https://academia.srmist.edu.in/accounts/p/10002227248/signin?hide_fp=true&servicename=ZohoCreator&service_language=en&css_url=/49910842/academia-academic-services/downloadPortalCustomCss/login&dcc=true&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin",
15
17
  "Referrer-Policy": "strict-origin-when-cross-origin",
16
18
  },
17
19
  body: `{"passwordauth":{"password":"${password}"}}`,
@@ -33,17 +35,38 @@ async function validatePassword({ identifier, digest, password, }) {
33
35
  };
34
36
  return { data, isAuthenticated: true };
35
37
  }
36
- const captchaRequired = response.localized_message
37
- ?.toLowerCase()
38
- ?.includes("captcha")
39
- ? true
40
- : false;
38
+ // ── Concurrent / device-limit detection ──────────────────────────────────
39
+ // Zoho returns status_code 435 when the concurrent session limit is hit.
40
+ // The response also embeds a flowId used to call the terminate endpoint.
41
+ // Field names seen in Zoho responses: flowId, flow_id, concurrent_flow_id
42
+ const msg = (response.localized_message ?? response.message ?? "").toLowerCase();
43
+ const isConcurrentLimit =
44
+ response.status_code === 435 ||
45
+ msg.includes("concurrent") ||
46
+ msg.includes("maximum number") ||
47
+ msg.includes("device limit") ||
48
+ msg.includes("already logged") ||
49
+ !!response.flowId ||
50
+ !!response.flow_id ||
51
+ !!response.concurrent_flow_id;
52
+ // Extract the flow identifier — try all known field names
53
+ const flowId =
54
+ response.flowId ??
55
+ response.flow_id ??
56
+ response.concurrent_flow_id ??
57
+ response.login_flow_id ??
58
+ null;
59
+ // ─────────────────────────────────────────────────────────────────────────
60
+ const captchaRequired = msg.includes("captcha");
41
61
  const data = {
42
62
  statusCode: response.status_code,
43
- message: response.localized_message,
63
+ message: response.localized_message ?? response.message,
44
64
  captcha: captchaRequired
45
65
  ? { required: true, digest: response.cdigest }
46
66
  : { required: false, digest: null },
67
+ // NEW: concurrent session fields
68
+ isConcurrentLimit,
69
+ flowId,
47
70
  };
48
71
  return { data, isAuthenticated: false };
49
72
  }
@@ -14,7 +14,9 @@ async function validateUser(username) {
14
14
  "sec-fetch-dest": "empty",
15
15
  "sec-fetch-mode": "cors",
16
16
  "sec-fetch-site": "same-origin",
17
- Referer: "https://academia.srmist.edu.in/accounts/p/10002227248/signin?hide_fp=true&servicename=ZohoCreator&service_language=en&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin",
17
+ "x-zcsrf-token": "iamcsrcoo=3dea6395-0540-44ea-8de7-544256dd7549",
18
+ cookie: "zalb_74c3a1eecc=50830239914cba1225506e915a665a91; zccpn=68703e7d-ccf0-42ba-92b2-9c87c7a0c8ae; JSESSIONID=739937C3826C1F58C37186170B4F4B36; cli_rgn=IN; _ga=GA1.3.1846200817.1748679237; _gid=GA1.3.734795940.1748679237; _gat=1; _ga_HQWPLLNMKY=GS2.3.s1748679237$o1$g0$t1748679237$j60$l0$h0; zalb_f0e8db9d3d=983d6a65b2f29022f18db52385bfc639; iamcsr=3dea6395-0540-44ea-8de7-544256dd7549; _zcsr_tmp=3dea6395-0540-44ea-8de7-544256dd7549; stk=4ec13d42454007681bd4337cf126baec",
19
+ Referer: "https://academia.srmist.edu.in/accounts/p/10002227248/signin?hide_fp=true&servicename=ZohoCreator&service_language=en&css_url=/49910842/academia-academic-services/downloadPortalCustomCss/login&dcc=true&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin",
18
20
  "Referrer-Policy": "strict-origin-when-cross-origin",
19
21
  },
20
22
  body: `mode=primary&cli_time=${Date.now()}&servicename=ZohoCreator&service_language=en&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin`,
@@ -3,6 +3,9 @@ export type { PasswordInput, UserResponse, AuthResult, UserValidationResult, Log
3
3
  export declare function verifyUser(username: string): Promise<UserValidationResult>;
4
4
  export declare function verifyPassword({ identifier, digest, password, }: PasswordInput): Promise<AuthResult>;
5
5
  export declare function logoutUser(cookie: string): Promise<LogoutResponse>;
6
+ export interface TerminateSessionsInput { flowId: string | null; identifier: string; digest: string; csrfToken?: string; }
7
+ export interface TerminateSessionsResult { success: boolean; status?: number; strategy?: string; data?: unknown; error?: string; errorReason?: unknown; }
8
+ export declare function terminateSessions(params: TerminateSessionsInput): Promise<TerminateSessionsResult>;
6
9
  export declare function getTimetable(cookie: string): Promise<TimetableResponse>;
7
10
  export declare function getAttendance(cookie: string): Promise<AttendanceResponse>;
8
11
  export declare function getMarks(cookie: string): Promise<MarksResponse>;
package/dist/src/index.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.verifyUser = verifyUser;
4
4
  exports.verifyPassword = verifyPassword;
5
5
  exports.logoutUser = logoutUser;
6
+ exports.terminateSessions = terminateSessions;
6
7
  exports.getTimetable = getTimetable;
7
8
  exports.getAttendance = getAttendance;
8
9
  exports.getMarks = getMarks;
@@ -12,6 +13,7 @@ exports.getDayOrder = getDayOrder;
12
13
  exports.getCourse = getCourse;
13
14
  const validatePassword_1 = require("./auth/validatePassword");
14
15
  const validateUser_1 = require("./auth/validateUser");
16
+ const terminateSessions_1 = require("./auth/terminateSessions");
15
17
  const fetchAttendance_1 = require("./fetch/fetchAttendance");
16
18
  const fetchCourseDetails_1 = require("./fetch/fetchCourseDetails");
17
19
  const fetchMarks_1 = require("./fetch/fetchMarks");
@@ -39,6 +41,10 @@ async function verifyPassword({ identifier, digest, password, }) {
39
41
  async function logoutUser(cookie) {
40
42
  return await (0, fetchLogout_1.fetchLogout)(cookie);
41
43
  }
44
+ // Terminate concurrent sessions (2-device limit bypass)
45
+ async function terminateSessions({ flowId, identifier, digest }) {
46
+ return await (0, terminateSessions_1.terminateSessions)({ flowId, identifier, digest });
47
+ }
42
48
  // Get TimeTable
43
49
  async function getTimetable(cookie) {
44
50
  const fetch = await (0, fetchTimetable_1.fetchTimetable)(cookie);
@@ -9,14 +9,12 @@ async function calendarDynamicUrl() {
9
9
  let academicYearString;
10
10
  let semesterType;
11
11
  if (currentMonth >= 1 && currentMonth <= 6) {
12
- // Jan-June: EVEN semester of previous academic year
13
12
  semesterType = "EVEN";
14
13
  academicYearString = `${currentYear - 1}_${currentYear
15
14
  .toString()
16
15
  .slice(-2)}`;
17
16
  }
18
17
  else {
19
- // July-Dec: ODD semester of current academic year
20
18
  semesterType = "ODD";
21
19
  academicYearString = `${currentYear}_${(currentYear + 1)
22
20
  .toString()
@@ -27,8 +25,6 @@ async function calendarDynamicUrl() {
27
25
  return dynamicUrl;
28
26
  }
29
27
  async function courseDynamicUrl() {
30
- // My_Time_Table_2023_24 serves the current semester timetable data
31
- // Note: Despite the name, this endpoint returns the current academic year data
32
28
  const baseUrl = "https://academia.srmist.edu.in/srm_university/academia-academic-services/page/My_Time_Table_2023_24";
33
29
  return baseUrl;
34
30
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reddy-api-srm",
3
3
  "description": "SRMIST KTR Academia portal",
4
- "version": "1.0.4",
4
+ "version": "1.0.6",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
7
7
  "exports": {