reddy-api-srm 1.0.10 → 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,6 +1,51 @@
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
+ }
4
49
  async function getSigninSessionHeaders() {
5
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";
6
51
  const seedRes = await fetch(signinUrl, {
@@ -80,17 +125,54 @@ async function validatePassword({ identifier, digest, password, }) {
80
125
  }
81
126
  return { error: "Internal Server Error", errorReason: new Error("Non-JSON response from login endpoint") };
82
127
  }
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
+ }
83
154
  if (response.status_code === 201 || response.status_code === 200) {
84
- const setCookieHeaders = (typeof res.headers.getSetCookie === "function"
85
- ? res.headers.getSetCookie()
86
- : [res.headers.get("set-cookie")].filter(Boolean));
87
- if (!setCookieHeaders.length)
155
+ if (!responseSetCookieHeaders.length)
88
156
  throw new Error("Couldn't able to get cookie from response header ");
89
- const combinedCookieHeader = setCookieHeaders.join("; ");
90
- const matches = [
91
- ...combinedCookieHeader.matchAll(/(_(?:iamadt|iambdt)_client_\d+|_z_identity)=[^;]+/g),
92
- ];
93
- 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 (_) { }
94
176
  const data = {
95
177
  cookies: extractedCookies,
96
178
  statusCode: response.status_code,
@@ -102,14 +184,30 @@ async function validatePassword({ identifier, digest, password, }) {
102
184
  const setCookieHeaders = (typeof res.headers.getSetCookie === "function"
103
185
  ? res.headers.getSetCookie()
104
186
  : [res.headers.get("set-cookie")].filter(Boolean));
105
- const combinedCookieHeader = setCookieHeaders.join("; ");
106
- const matches = [
107
- ...combinedCookieHeader.matchAll(/(_(?:iamadt|iambdt)_client_\d+|_z_identity)=[^;]+/g),
108
- ];
109
- if (matches.length > 0) {
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) {
110
208
  return {
111
209
  data: {
112
- cookies: matches.map((m) => m[0]).join("; ") + ";",
210
+ cookies: extractedCookies,
113
211
  statusCode: 201,
114
212
  },
115
213
  isAuthenticated: true,
@@ -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.10",
4
+ "version": "1.0.11",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
7
7
  "exports": {