reddy-api-srm 1.0.9 → 1.0.11

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.
@@ -7,6 +7,8 @@ export interface TerminateSessionsInput {
7
7
  digest: string;
8
8
  /** Optional CSRF token (value of iamcsr cookie, without the prefix) */
9
9
  csrfToken?: string;
10
+ /** Optional cookie string from validatePassword pre-announcement flow */
11
+ sessionCookie?: string;
10
12
  }
11
13
 
12
14
  export interface TerminateSessionsResult {
@@ -27,7 +27,11 @@ exports.terminateSessions = terminateSessions;
27
27
  * @param {string} digest - Zoho digest from verifyUser
28
28
  * @param {string} [csrfToken] - Optional x-zcsrf-token value (extracted from iamcsr cookie)
29
29
  */
30
- async function terminateSessions({ flowId, identifier, digest, csrfToken }) {
30
+ async function terminateSessions({ flowId, identifier, digest, csrfToken, sessionCookie }) {
31
+ const normalizedCsrfToken = csrfToken ??
32
+ (((sessionCookie ?? "").match(/(?:^|;\s*)iamcsr=([^;]+)/) ??
33
+ (sessionCookie ?? "").match(/(?:^|;\s*)_zcsr_tmp=([^;]+)/) ??
34
+ [])[1] ?? null);
31
35
  // ── Strategy 1: Block-Sessions preannouncement POST ────────────────────────
32
36
  // This is what fires when the user clicks "Terminate All Sessions" on the
33
37
  // SRM concurrent-sessions warning page during a fresh login attempt.
@@ -58,9 +62,10 @@ async function terminateSessions({ flowId, identifier, digest, csrfToken }) {
58
62
  "sec-fetch-dest": "empty",
59
63
  "sec-fetch-mode": "cors",
60
64
  "sec-fetch-site": "same-origin",
61
- ...(csrfToken
62
- ? { "x-zcsrf-token": `iamcsrcoo=${csrfToken}` }
65
+ ...(normalizedCsrfToken
66
+ ? { "x-zcsrf-token": `iamcsrcoo=${normalizedCsrfToken}` }
63
67
  : {}),
68
+ ...(sessionCookie ? { cookie: sessionCookie } : {}),
64
69
  Referer:
65
70
  "https://academia.srmist.edu.in/accounts/p/40-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
71
  "Referrer-Policy": "strict-origin-when-cross-origin",
@@ -92,9 +97,10 @@ async function terminateSessions({ flowId, identifier, digest, csrfToken }) {
92
97
  accept: "application/json, text/javascript, */*; q=0.01",
93
98
  "accept-language": "en-US,en;q=0.9",
94
99
  "x-requested-with": "XMLHttpRequest",
95
- ...(csrfToken
96
- ? { "x-zcsrf-token": `iamcsrcoo=${csrfToken}` }
100
+ ...(normalizedCsrfToken
101
+ ? { "x-zcsrf-token": `iamcsrcoo=${normalizedCsrfToken}` }
97
102
  : {}),
103
+ ...(sessionCookie ? { cookie: sessionCookie } : {}),
98
104
  Referer: "https://accounts.zoho.in/",
99
105
  "Referrer-Policy": "strict-origin-when-cross-origin",
100
106
  },
@@ -111,6 +117,41 @@ async function terminateSessions({ flowId, identifier, digest, csrfToken }) {
111
117
  console.warn("[terminateSessions] Strategy 2 error:", e2);
112
118
  }
113
119
  }
120
+ // ── Strategy 3: Announcement pre-blocksessions DELETE ───────────────────────
121
+ // This is what current SRM preannouncement page JS calls.
122
+ if (sessionCookie) {
123
+ try {
124
+ const preBlockUrl = "https://academia.srmist.edu.in/accounts/p/40-10002227248/webclient/v1/announcement/pre/blocksessions";
125
+ const res3 = await fetch(preBlockUrl, {
126
+ method: "DELETE",
127
+ headers: {
128
+ accept: "application/json, text/javascript, */*; q=0.01",
129
+ "accept-language": "en-US,en;q=0.9",
130
+ "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
131
+ ...(normalizedCsrfToken
132
+ ? {
133
+ "x-zcsrf-token": `iamcsrcoo=${encodeURIComponent(normalizedCsrfToken)}`,
134
+ "X-ZCSRF-TOKEN": `iamcsrcoo=${encodeURIComponent(normalizedCsrfToken)}`,
135
+ }
136
+ : {}),
137
+ cookie: sessionCookie,
138
+ Referer: "https://academia.srmist.edu.in/accounts/p/40-10002227248/preannouncement/block-sessions",
139
+ "Referrer-Policy": "strict-origin-when-cross-origin",
140
+ },
141
+ });
142
+ let data = null;
143
+ try {
144
+ data = await res3.json();
145
+ } catch (_) { }
146
+ if (res3.ok || res3.status === 200 || res3.status === 202 || res3.status === 204 ||
147
+ (data && (String(data.status_code) === "204" || data.code === "SUCCESS"))) {
148
+ return { success: true, status: res3.status, strategy: "announcement-pre-blocksessions", data };
149
+ }
150
+ console.warn("[terminateSessions] Strategy 3 (announcement pre-blocksessions DELETE) returned:", res3.status, data);
151
+ } catch (e3) {
152
+ console.warn("[terminateSessions] Strategy 3 error:", e3);
153
+ }
154
+ }
114
155
 
115
156
  // Both strategies failed — return a non-fatal "best-effort" result.
116
157
  // The caller (handleTerminateAndRetry) will still attempt to re-login because
@@ -1,9 +1,85 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validatePassword = validatePassword;
4
+ function parseCookiePairsFromSetCookie(headers) {
5
+ const pairs = [];
6
+ for (const header of headers) {
7
+ const pair = header.split(";")[0]?.trim();
8
+ if (pair && pair.includes("="))
9
+ pairs.push(pair);
10
+ }
11
+ return pairs;
12
+ }
13
+ function parseCookiePairsFromCookieString(cookie) {
14
+ if (!cookie)
15
+ return [];
16
+ return cookie
17
+ .split(";")
18
+ .map((v) => v.trim())
19
+ .filter((v) => v.includes("="));
20
+ }
21
+ function mergeCookies(...cookieSources) {
22
+ const cookieMap = new Map();
23
+ for (const source of cookieSources) {
24
+ for (const pair of parseCookiePairsFromCookieString(source)) {
25
+ const idx = pair.indexOf("=");
26
+ if (idx <= 0)
27
+ continue;
28
+ const name = pair.slice(0, idx).trim();
29
+ cookieMap.set(name, pair.trim());
30
+ }
31
+ }
32
+ return `${Array.from(cookieMap.values()).join("; ")};`;
33
+ }
34
+ function getCookieValue(cookie, name) {
35
+ const match = (cookie || "").match(new RegExp(`(?:^|;\\s*)${name}=([^;]+)`));
36
+ return match ? match[1] : null;
37
+ }
38
+ function getFlowIdFromUrl(urlValue) {
39
+ if (!urlValue)
40
+ return null;
41
+ try {
42
+ const u = new URL(urlValue, "https://academia.srmist.edu.in");
43
+ return u.searchParams.get("flowId") ?? u.searchParams.get("flow_id") ?? null;
44
+ }
45
+ catch (_) {
46
+ return null;
47
+ }
48
+ }
49
+ async function getSigninSessionHeaders() {
50
+ const signinUrl = "https://academia.srmist.edu.in/accounts/p/40-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";
51
+ const seedRes = await fetch(signinUrl, {
52
+ method: "GET",
53
+ redirect: "manual",
54
+ headers: {
55
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
56
+ "accept-language": "en-US,en;q=0.9",
57
+ },
58
+ });
59
+ const setCookieHeaders = (typeof seedRes.headers.getSetCookie === "function"
60
+ ? seedRes.headers.getSetCookie()
61
+ : [seedRes.headers.get("set-cookie")].filter(Boolean));
62
+ const cookiePairs = [];
63
+ let csrfToken = null;
64
+ for (const header of setCookieHeaders) {
65
+ const pair = header.split(";")[0]?.trim();
66
+ if (!pair || !pair.includes("="))
67
+ continue;
68
+ cookiePairs.push(pair);
69
+ if (pair.startsWith("iamcsr="))
70
+ csrfToken = pair.slice("iamcsr=".length);
71
+ if (!csrfToken && pair.startsWith("_zcsr_tmp="))
72
+ csrfToken = pair.slice("_zcsr_tmp=".length);
73
+ }
74
+ return {
75
+ cookie: cookiePairs.join("; "),
76
+ csrfToken,
77
+ };
78
+ }
4
79
  async function validatePassword({ identifier, digest, password, }) {
5
80
  let res;
6
81
  try {
82
+ const seed = await getSigninSessionHeaders();
7
83
  res = await fetch(`https://academia.srmist.edu.in/accounts/p/40-10002227248/signin/v2/primary/${identifier}/password?digest=${digest}&cli_time=${Date.now()}&servicename=ZohoCreator&service_language=en&serviceurl=https%3A%2F%2Facademia.srmist.edu.in%2Fportal%2Facademia-academic-services%2FredirectFromLogin`, {
8
84
  headers: {
9
85
  accept: "*/*",
@@ -12,12 +88,25 @@ async function validatePassword({ identifier, digest, password, }) {
12
88
  "sec-fetch-dest": "empty",
13
89
  "sec-fetch-mode": "cors",
14
90
  "sec-fetch-site": "same-origin",
91
+ ...(seed.csrfToken ? { "x-zcsrf-token": `iamcsrcoo=${seed.csrfToken}` } : {}),
92
+ ...(seed.cookie ? { cookie: seed.cookie } : {}),
15
93
  Referer: "https://academia.srmist.edu.in/accounts/p/40-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",
16
94
  "Referrer-Policy": "strict-origin-when-cross-origin",
17
95
  },
18
96
  body: `{"passwordauth":{"password":"${password}"}}`,
19
97
  method: "POST",
98
+ redirect: "manual",
20
99
  });
100
+ const location = res.headers.get("location") ?? "";
101
+ if ([301, 302, 303, 307, 308].includes(res.status) && location.includes("sessions-reminder")) {
102
+ let flowId = null;
103
+ try {
104
+ const u = new URL(location, "https://academia.srmist.edu.in");
105
+ flowId = u.searchParams.get("flowId") ?? u.searchParams.get("flow_id") ?? null;
106
+ }
107
+ catch (_) { }
108
+ return { data: { statusCode: 435, message: "Maximum concurrent sessions reached. Please terminate existing sessions to continue.", captcha: { required: false, digest: null }, isConcurrentLimit: true, flowId }, isAuthenticated: false };
109
+ }
21
110
  // ── Detect redirect to Zoho sessions-reminder page ───────────────────────
22
111
  if (res.redirected && res.url && res.url.includes("sessions-reminder")) {
23
112
  let flowId = null;
@@ -36,23 +125,95 @@ async function validatePassword({ identifier, digest, password, }) {
36
125
  }
37
126
  return { error: "Internal Server Error", errorReason: new Error("Non-JSON response from login endpoint") };
38
127
  }
39
- if (response.status_code === 201) {
40
- const setCookieHeaders = (typeof res.headers.getSetCookie === "function"
41
- ? res.headers.getSetCookie()
42
- : [res.headers.get("set-cookie")].filter(Boolean));
43
- if (!setCookieHeaders.length)
128
+ const responseSetCookieHeaders = (typeof res.headers.getSetCookie === "function"
129
+ ? res.headers.getSetCookie()
130
+ : [res.headers.get("set-cookie")].filter(Boolean));
131
+ const responseCookies = parseCookiePairsFromSetCookie(responseSetCookieHeaders).join("; ");
132
+ const sessionCookie = mergeCookies(seed.cookie, responseCookies);
133
+ const sessionCsrfToken = getCookieValue(sessionCookie, "iamcsr") ?? getCookieValue(sessionCookie, "_zcsr_tmp");
134
+ const preAnnouncementRedirect = response?.passwordauth?.redirect_uri ?? null;
135
+ const isPreAnnouncementFlow = response?.code === "SI303" ||
136
+ (typeof response?.message === "string" &&
137
+ response.message.toLowerCase().includes("pre announcement")) ||
138
+ (typeof preAnnouncementRedirect === "string" &&
139
+ preAnnouncementRedirect.includes("/preannouncement/block-sessions"));
140
+ if (isPreAnnouncementFlow) {
141
+ return {
142
+ data: {
143
+ statusCode: 435,
144
+ message: "Maximum concurrent sessions reached. Please terminate existing sessions to continue.",
145
+ captcha: { required: false, digest: null },
146
+ isConcurrentLimit: true,
147
+ flowId: getFlowIdFromUrl(preAnnouncementRedirect),
148
+ sessionCookie,
149
+ csrfToken: sessionCsrfToken,
150
+ },
151
+ isAuthenticated: false,
152
+ };
153
+ }
154
+ if (response.status_code === 201 || response.status_code === 200) {
155
+ if (!responseSetCookieHeaders.length)
44
156
  throw new Error("Couldn't able to get cookie from response header ");
45
- const combinedCookieHeader = setCookieHeaders.join("; ");
46
- const matches = [
47
- ...combinedCookieHeader.matchAll(/(_(?:iamadt|iambdt)_client_\d+|_z_identity)=[^;]+/g),
48
- ];
49
- const extractedCookies = matches.map((m) => m[0]).join("; ") + ";";
157
+ let extractedCookies = sessionCookie;
158
+ // Complete the service redirect once to receive portal/session cookies needed by data APIs.
159
+ try {
160
+ const bridgeRes = await fetch("https://academia.srmist.edu.in/portal/academia-academic-services/redirectFromLogin", {
161
+ method: "GET",
162
+ headers: {
163
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
164
+ "accept-language": "en-US,en;q=0.9",
165
+ cookie: extractedCookies,
166
+ },
167
+ });
168
+ const bridgeSetCookies = (typeof bridgeRes.headers.getSetCookie === "function"
169
+ ? bridgeRes.headers.getSetCookie()
170
+ : [bridgeRes.headers.get("set-cookie")].filter(Boolean));
171
+ if (bridgeSetCookies.length) {
172
+ extractedCookies = mergeCookies(extractedCookies, parseCookiePairsFromSetCookie(bridgeSetCookies).join("; "));
173
+ }
174
+ }
175
+ catch (_) { }
50
176
  const data = {
51
177
  cookies: extractedCookies,
52
- statusCode: 201,
178
+ statusCode: response.status_code,
53
179
  };
54
180
  return { data, isAuthenticated: true };
55
181
  }
182
+ if ([301, 302, 303, 307, 308].includes(res.status) &&
183
+ (location.includes("redirectFromLogin") || location.includes("/portal/academia-academic-services"))) {
184
+ const setCookieHeaders = (typeof res.headers.getSetCookie === "function"
185
+ ? res.headers.getSetCookie()
186
+ : [res.headers.get("set-cookie")].filter(Boolean));
187
+ const loginCookies = parseCookiePairsFromSetCookie(setCookieHeaders).join("; ");
188
+ let extractedCookies = mergeCookies(seed.cookie, loginCookies);
189
+ try {
190
+ const redirectUrl = new URL(location, "https://academia.srmist.edu.in").toString();
191
+ const bridgeRes = await fetch(redirectUrl, {
192
+ method: "GET",
193
+ headers: {
194
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
195
+ "accept-language": "en-US,en;q=0.9",
196
+ cookie: extractedCookies,
197
+ },
198
+ });
199
+ const bridgeSetCookies = (typeof bridgeRes.headers.getSetCookie === "function"
200
+ ? bridgeRes.headers.getSetCookie()
201
+ : [bridgeRes.headers.get("set-cookie")].filter(Boolean));
202
+ if (bridgeSetCookies.length) {
203
+ extractedCookies = mergeCookies(extractedCookies, parseCookiePairsFromSetCookie(bridgeSetCookies).join("; "));
204
+ }
205
+ }
206
+ catch (_) { }
207
+ if (extractedCookies.length > 1) {
208
+ return {
209
+ data: {
210
+ cookies: extractedCookies,
211
+ statusCode: 201,
212
+ },
213
+ isAuthenticated: true,
214
+ };
215
+ }
216
+ }
56
217
  // ── Concurrent / device-limit detection ──────────────────────────────────
57
218
  const msg = (response.localized_message ?? response.message ?? "").toLowerCase();
58
219
  const isConcurrentLimit =
@@ -1,8 +1,39 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateUser = validateUser;
4
+ async function getSigninSessionHeaders() {
5
+ const signinUrl = "https://academia.srmist.edu.in/accounts/p/40-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";
6
+ const seedRes = await fetch(signinUrl, {
7
+ method: "GET",
8
+ redirect: "manual",
9
+ headers: {
10
+ accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
11
+ "accept-language": "en-US,en;q=0.9",
12
+ },
13
+ });
14
+ const setCookieHeaders = (typeof seedRes.headers.getSetCookie === "function"
15
+ ? seedRes.headers.getSetCookie()
16
+ : [seedRes.headers.get("set-cookie")].filter(Boolean));
17
+ const cookiePairs = [];
18
+ let csrfToken = null;
19
+ for (const header of setCookieHeaders) {
20
+ const pair = header.split(";")[0]?.trim();
21
+ if (!pair || !pair.includes("="))
22
+ continue;
23
+ cookiePairs.push(pair);
24
+ if (pair.startsWith("iamcsr="))
25
+ csrfToken = pair.slice("iamcsr=".length);
26
+ if (!csrfToken && pair.startsWith("_zcsr_tmp="))
27
+ csrfToken = pair.slice("_zcsr_tmp=".length);
28
+ }
29
+ return {
30
+ cookie: cookiePairs.join("; "),
31
+ csrfToken,
32
+ };
33
+ }
4
34
  async function validateUser(username) {
5
35
  try {
36
+ const seed = await getSigninSessionHeaders();
6
37
  const res = await fetch(`https://academia.srmist.edu.in/accounts/p/40-10002227248/signin/v2/lookup/${username}`, {
7
38
  headers: {
8
39
  accept: "*/*",
@@ -11,6 +42,8 @@ async function validateUser(username) {
11
42
  "sec-fetch-dest": "empty",
12
43
  "sec-fetch-mode": "cors",
13
44
  "sec-fetch-site": "same-origin",
45
+ ...(seed.csrfToken ? { "x-zcsrf-token": `iamcsrcoo=${seed.csrfToken}` } : {}),
46
+ ...(seed.cookie ? { cookie: seed.cookie } : {}),
14
47
  Referer: "https://academia.srmist.edu.in/accounts/p/40-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",
15
48
  "Referrer-Policy": "strict-origin-when-cross-origin",
16
49
  },
@@ -27,7 +60,6 @@ async function validateUser(username) {
27
60
  return { data };
28
61
  }
29
62
  catch (e) {
30
- console.error(e);
31
63
  return {
32
64
  error: "Internal Server Error",
33
65
  errorReason: e,
@@ -3,7 +3,7 @@ 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; }
6
+ export interface TerminateSessionsInput { flowId: string | null; identifier: string; digest: string; csrfToken?: string; sessionCookie?: string; }
7
7
  export interface TerminateSessionsResult { success: boolean; status?: number; strategy?: string; data?: unknown; error?: string; errorReason?: unknown; }
8
8
  export declare function terminateSessions(params: TerminateSessionsInput): Promise<TerminateSessionsResult>;
9
9
  export declare function getTimetable(cookie: string): Promise<TimetableResponse>;
@@ -13,4 +13,4 @@ export declare function getUserInfo(cookie: string): Promise<UserInfoResponse>;
13
13
  export declare function getCalendar(cookie: string): Promise<CalendarResponse>;
14
14
  export declare function getDayOrder(cookie: string): Promise<DayOrderResponse>;
15
15
  export declare function getCourse(cookie: string): Promise<CourseResponse>;
16
- //# sourceMappingURL=index.d.ts.map
16
+ //# sourceMappingURL=index.d.ts.map
package/dist/src/index.js CHANGED
@@ -42,8 +42,8 @@ async function logoutUser(cookie) {
42
42
  return await (0, fetchLogout_1.fetchLogout)(cookie);
43
43
  }
44
44
  // Terminate concurrent sessions (2-device limit bypass)
45
- async function terminateSessions({ flowId, identifier, digest, csrfToken }) {
46
- return await (0, terminateSessions_1.terminateSessions)({ flowId, identifier, digest, csrfToken });
45
+ async function terminateSessions({ flowId, identifier, digest, csrfToken, sessionCookie }) {
46
+ return await (0, terminateSessions_1.terminateSessions)({ flowId, identifier, digest, csrfToken, sessionCookie });
47
47
  }
48
48
  // Get TimeTable
49
49
  async function getTimetable(cookie) {
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.9",
4
+ "version": "1.0.11",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
7
7
  "exports": {