kalvium-worklog 1.4.0 → 1.4.2

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.
@@ -61,10 +61,11 @@ async function captureTokenViaBrowser() {
61
61
  console.log("\n ========================================");
62
62
  console.log(" Browser-based token capture (no Playwright)");
63
63
  console.log(" ========================================\n");
64
- // Start local HTTP server to receive the token
64
+ // The JS that runs on kalvium.community to extract and send the token
65
+ const bookmarkletJS = `javascript:void(function(){var t=localStorage.getItem('kc-token')||localStorage.getItem('keycloak-token');if(!t){for(var i=0;i<localStorage.length;i++){var k=localStorage.key(i),v=localStorage.getItem(k);if(v&&v.includes('refresh_token')){t=v;break}}}if(!t){for(var i=0;i<sessionStorage.length;i++){var k=sessionStorage.key(i),v=sessionStorage.getItem(k);if(v&&v.includes('refresh_token')){t=v;break}}}if(t){fetch('http://localhost:${PORT}/capture',{method:'POST',headers:{'Content-Type':'text/plain'},body:t}).then(function(r){if(r.ok)alert('Token sent! You can close this tab.');else alert('Failed: '+r.status)}).catch(function(e){alert('Error: '+e)})}else{alert('No token found in storage. Make sure you are logged in.')}})()`;
66
+ // Start local HTTP server
65
67
  const tokenPromise = new Promise((resolve) => {
66
68
  const server = createServer((req, res) => {
67
- // Handle CORS + token capture
68
69
  res.setHeader("Access-Control-Allow-Origin", "*");
69
70
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
70
71
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
@@ -78,15 +79,14 @@ async function captureTokenViaBrowser() {
78
79
  body += chunk;
79
80
  });
80
81
  req.on("end", () => {
82
+ // Token capture endpoint
81
83
  if (req.url?.startsWith("/capture")) {
82
84
  try {
83
- // Try parsing as JSON first (POST body)
84
85
  let data;
85
86
  if (body) {
86
87
  data = JSON.parse(body);
87
88
  }
88
89
  else {
89
- // Try query param
90
90
  const url = new URL(req.url, `http://localhost:${PORT}`);
91
91
  const tokenParam = url.searchParams.get("token");
92
92
  if (!tokenParam) {
@@ -98,69 +98,103 @@ async function captureTokenViaBrowser() {
98
98
  }
99
99
  if (data.refresh_token) {
100
100
  res.writeHead(200, { "Content-Type": "text/html" });
101
- res.end("<html><body><h2>Token captured! You can close this tab.</h2></body></html>");
101
+ res.end("<html><body style='font-family:sans-serif;text-align:center;padding:40px'><h2>✓ Token captured!</h2><p>You can close this tab and go back to Termux.</p></body></html>");
102
102
  resolve(data);
103
103
  }
104
104
  else {
105
- res.writeHead(400);
106
- res.end("Invalid token — missing refresh_token");
105
+ res.writeHead(400, { "Content-Type": "text/html" });
106
+ res.end("<html><body><h2>Invalid token — missing refresh_token</h2></body></html>");
107
107
  resolve(null);
108
108
  }
109
109
  }
110
110
  catch {
111
- res.writeHead(400);
112
- res.end("Invalid JSON");
111
+ res.writeHead(400, { "Content-Type": "text/html" });
112
+ res.end("<html><body><h2>Invalid JSON</h2></body></html>");
113
113
  resolve(null);
114
114
  }
115
+ return;
115
116
  }
116
- else {
117
- res.writeHead(200, { "Content-Type": "text/html" });
118
- res.end(`
119
- <html><body>
120
- <h2>Kalvium Worklog — Token Capture</h2>
121
- <p>If you're seeing this, the local server is running.</p>
122
- <p>Go back to the Kalvium tab and paste the JavaScript snippet.</p>
123
- </body></html>
124
- `);
125
- }
117
+ // Main page — serves the bookmarklet + instructions
118
+ res.writeHead(200, { "Content-Type": "text/html" });
119
+ res.end(`<!DOCTYPE html>
120
+ <html>
121
+ <head>
122
+ <meta name="viewport" content="width=device-width, initial-scale=1">
123
+ <title>Kalvium Worklog Token Capture</title>
124
+ <style>
125
+ body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px; line-height: 1.6; }
126
+ .step { background: #f0f4ff; border-radius: 12px; padding: 16px; margin: 12px 0; }
127
+ .step h3 { margin-top: 0; }
128
+ .bookmarklet { display: inline-block; background: #4285f4; color: white; padding: 14px 24px; border-radius: 8px; text-decoration: none; font-size: 18px; font-weight: bold; margin: 12px 0; }
129
+ .bookmarklet:active { background: #3367d6; }
130
+ .note { background: #fff8e1; border-radius: 8px; padding: 12px; margin: 12px 0; font-size: 14px; }
131
+ code { background: #f5f5f5; padding: 2px 6px; border-radius: 4px; font-size: 13px; word-break: break-all; }
132
+ .kalvium-link { display: inline-block; background: #34a853; color: white; padding: 12px 20px; border-radius: 8px; text-decoration: none; font-size: 16px; margin: 8px 0; }
133
+ </style>
134
+ </head>
135
+ <body>
136
+ <h2>Kalvium Worklog — Token Capture</h2>
137
+
138
+ <div class="step">
139
+ <h3>Step 1: Open Kalvium & log in</h3>
140
+ <p>Tap the button below to open Kalvium. Log in with Google (complete 2FA if asked).</p>
141
+ <a class="kalvium-link" href="https://kalvium.community/internships" target="_blank">Open Kalvium →</a>
142
+ </div>
143
+
144
+ <div class="step">
145
+ <h3>Step 2: Save this bookmarklet</h3>
146
+ <p><b>Long-press</b> the blue button below, then select <b>"Bookmark link"</b> or <b>"Add bookmark"</b>.</p>
147
+ <a class="bookmarklet" href="${bookmarkletJS.replace(/"/g, "&quot;")}">📋 Capture Token</a>
148
+ <p><small>If long-press doesn't work: bookmark this page, then edit the bookmark and change the URL to the code below.</small></p>
149
+ </div>
150
+
151
+ <div class="step">
152
+ <h3>Step 3: Run the bookmarklet</h3>
153
+ <p>Go back to the <b>Kalvium tab</b> (where you logged in).<br>
154
+ Open your <b>bookmarks menu</b> and tap <b>"📋 Capture Token"</b>.</p>
155
+ <p>You should see <b>"Token sent!"</b> — then come back to Termux.</p>
156
+ </div>
157
+
158
+ <div class="note">
159
+ <b>Trouble with bookmarklet?</b> Copy this code, open Kalvium, then paste it in the address bar (type <code>javascript:</code> first, then paste the rest after the colon):
160
+ <br><br>
161
+ <code>${bookmarkletJS.replace(/^javascript:/, "")}</code>
162
+ </div>
163
+
164
+ </body>
165
+ </html>`);
126
166
  });
127
167
  });
128
168
  server.listen(PORT, () => {
129
169
  console.log(` Local server running on http://localhost:${PORT}`);
130
170
  });
131
- // Timeout after 5 minutes
132
171
  setTimeout(() => {
133
172
  server.close();
134
173
  resolve(null);
135
174
  }, 300000);
136
175
  });
137
- // Open kalvium.community in the phone's default browser
138
- const url = "https://kalvium.community/internships";
176
+ // Open the localhost page in the browser
177
+ const localUrl = `http://localhost:${PORT}`;
139
178
  try {
140
179
  if (process.platform === "android") {
141
- // Termux: use termux-open-url
142
- execSync(`termux-open-url ${url}`, { stdio: "ignore" });
180
+ execSync(`termux-open-url ${localUrl}`, { stdio: "ignore" });
143
181
  }
144
182
  else {
145
- // Other platforms: try xdg-open or open
146
- execSync(`xdg-open ${url} || open ${url}`, { stdio: "ignore" });
183
+ execSync(`xdg-open ${localUrl} || open ${localUrl}`, {
184
+ stdio: "ignore",
185
+ });
147
186
  }
148
- console.log(` Opened ${url} in your browser.`);
187
+ console.log(` Opened ${localUrl} in your browser.`);
149
188
  }
150
189
  catch {
151
- console.log(` Could not auto-open browser. Please open: ${url}`);
190
+ console.log(` Could not auto-open browser. Please open: ${localUrl}`);
152
191
  }
153
192
  console.log("\n ────────────────────────────────────────────");
154
- console.log(" 1. Log in with Google (complete 2FA if needed)");
155
- console.log(" 2. After you're logged in, copy this line:");
156
- console.log(" ────────────────────────────────────────────\n");
157
- // The JS snippet extracts tokens from the page's network requests
158
- // and sends them to our local server
159
- const jsSnippet = `javascript:void(function(){var t=localStorage.getItem('kc-token')||localStorage.getItem('keycloak-token');if(!t){for(var i=0;i<localStorage.length;i++){var k=localStorage.key(i),v=localStorage.getItem(k);if(v&&v.includes('refresh_token')){t=v;break}}}if(!t){for(var i=0;i<sessionStorage.length;i++){var k=sessionStorage.key(i),v=sessionStorage.getItem(k);if(v&&v.includes('refresh_token')){t=v;break}}}if(t){fetch('http://localhost:${PORT}/capture',{method:'POST',headers:{'Content-Type':'text/plain'},body:t}).then(function(r){if(r.ok)alert('Token sent! You can close this tab.');else alert('Failed: '+r.status)}).catch(function(e){alert('Error: '+e)})}else{alert('No token found in storage. Make sure you are logged in.')}})()`;
160
- console.log(` ${jsSnippet}\n`);
161
- console.log(" 3. Paste it in the browser ADDRESS BAR (where you type URLs)");
162
- console.log(" and press Enter. (Remove any leading 'javascript:' that");
163
- console.log(" the browser strips, then type it manually if needed.)");
193
+ console.log(" A page opened in your browser with instructions:");
194
+ console.log(" 1. Click 'Open Kalvium' and log in");
195
+ console.log(" 2. Long-press 'Capture Token' → Bookmark it");
196
+ console.log(" 3. Go to Kalvium tab open the bookmark");
197
+ console.log(" ────────────────────────────────────────────");
164
198
  console.log("\n Waiting for token (up to 5 minutes)...\n");
165
199
  const data = await tokenPromise;
166
200
  if (data) {
@@ -1,5 +1,7 @@
1
1
  /**
2
- * Discover the user's internship position ID by capturing API calls.
3
- * Tries headless first, falls back to headed browser for manual login.
2
+ * Discover the user's internship position ID.
3
+ * Tries JWT extraction first (no browser needed).
4
+ * Falls back to browser-based discovery via Playwright if available.
5
+ * On Android, uses JWT extraction only.
4
6
  */
5
7
  export declare function discoverPosition(): Promise<boolean>;
@@ -1,31 +1,71 @@
1
- import { chromium } from "playwright";
2
- import { PROFILE_DIR, CONFIG_FILE, ensureInstallDir, saveConfig, } from "./config.js";
1
+ import { PROFILE_DIR, CONFIG_FILE, ensureInstallDir, saveConfig, loadToken, getPositionIdFromToken, } from "./config.js";
3
2
  /**
4
- * Discover the user's internship position ID by capturing API calls.
5
- * Tries headless first, falls back to headed browser for manual login.
3
+ * Check if Playwright is available on this platform.
4
+ */
5
+ function isPlaywrightAvailable() {
6
+ try {
7
+ if (process.platform === "android")
8
+ return false;
9
+ require.resolve("playwright");
10
+ return true;
11
+ }
12
+ catch {
13
+ return false;
14
+ }
15
+ }
16
+ /**
17
+ * Discover the user's internship position ID.
18
+ * Tries JWT extraction first (no browser needed).
19
+ * Falls back to browser-based discovery via Playwright if available.
20
+ * On Android, uses JWT extraction only.
6
21
  */
7
22
  export async function discoverPosition() {
8
23
  ensureInstallDir();
9
24
  console.log("=== Discovering internship position ID ===\n");
10
- // Step 1: Try headless first
11
- console.log("1. Trying headless mode (existing session)...");
12
- let captured = await discoverViaBrowser(true);
25
+ // Step 1: Try extracting from saved token (no browser needed)
26
+ const token = loadToken();
27
+ if (token) {
28
+ const positionId = getPositionIdFromToken(token);
29
+ if (positionId) {
30
+ const worklogUrl = `https://student-api.kalvium.community/api/internships/worklogs/${positionId}`;
31
+ saveConfig({ position_id: positionId, worklog_api_url: worklogUrl });
32
+ console.log(`\n=== SUCCESS ===`);
33
+ console.log(`Position ID: ${positionId}`);
34
+ console.log(`Worklog API: ${worklogUrl}`);
35
+ console.log(`Saved to: ${CONFIG_FILE}`);
36
+ return true;
37
+ }
38
+ console.log(" Could not extract position ID from token.");
39
+ }
40
+ else {
41
+ console.log(" No token found. Run `kalvium-worklog login` first.");
42
+ }
43
+ // Step 2: Fall back to browser-based discovery (if Playwright available)
44
+ if (!isPlaywrightAvailable()) {
45
+ console.log("\nERROR: Could not discover position ID.");
46
+ console.log("Playwright is not available on this platform.");
47
+ console.log("Make sure you have a valid token (run `kalvium-worklog login`).");
48
+ return false;
49
+ }
50
+ const { chromium } = await import("playwright");
51
+ console.log("\n1. Trying headless mode (existing session)...");
52
+ let captured = await discoverViaBrowser(chromium, true);
13
53
  if (!captured.token) {
14
54
  console.log(" No valid session. Opening browser for manual login...");
15
- captured = await discoverViaBrowser(false);
55
+ captured = await discoverViaBrowser(chromium, false);
16
56
  }
17
57
  if (!captured.token) {
18
58
  console.log("\nERROR: Could not capture token. Please log in and try again.");
19
59
  return false;
20
60
  }
21
61
  console.log(` Token captured: ${captured.token.slice(0, 40)}...`);
22
- // Step 2: Extract position ID from captured API URLs
62
+ // Step 3: Extract position ID from captured API URLs
23
63
  const positionId = extractPositionId(captured.api_url ?? "");
24
64
  if (!positionId) {
25
65
  console.log("\nERROR: Could not determine position ID from API calls");
26
66
  return false;
27
67
  }
28
- // Step 3: Save config
68
+ // Step 4: Save config
29
69
  const worklogUrl = `https://student-api.kalvium.community/api/internships/worklogs/${positionId}`;
30
70
  saveConfig({ position_id: positionId, worklog_api_url: worklogUrl });
31
71
  console.log(`\n=== SUCCESS ===`);
@@ -34,7 +74,7 @@ export async function discoverPosition() {
34
74
  console.log(`Saved to: ${CONFIG_FILE}`);
35
75
  return true;
36
76
  }
37
- async function discoverViaBrowser(headless) {
77
+ async function discoverViaBrowser(chromium, headless) {
38
78
  const captured = {};
39
79
  const context = await chromium.launchPersistentContext(PROFILE_DIR, {
40
80
  headless: false,
@@ -75,16 +115,25 @@ async function discoverViaBrowser(headless) {
75
115
  }
76
116
  else {
77
117
  console.log("\n ========================================");
78
- console.log(" Browser opened. Please log in now:");
79
- console.log(" 1. Click 'Continue with Google'");
80
- console.log(" 2. Sign in with your Kalvium account");
81
- console.log(" 3. Complete 2FA if prompted");
82
- console.log(" 4. Wait — the script continues automatically");
118
+ console.log(" Browser opened. Auto-clicking 'Continue with Google'...");
119
+ console.log(" Please complete 2FA if prompted.");
83
120
  console.log(" ========================================\n");
84
121
  await page.goto("https://kalvium.community/internships", {
85
122
  waitUntil: "domcontentloaded",
86
123
  timeout: 120000,
87
124
  });
125
+ // Auto-click "Continue with Google"
126
+ try {
127
+ const googleBtn = page.locator("text=Continue with Google").first();
128
+ const isVisible = await googleBtn.isVisible().catch(() => false);
129
+ if (isVisible) {
130
+ await googleBtn.click();
131
+ console.log(" Clicked 'Continue with Google'.");
132
+ }
133
+ }
134
+ catch {
135
+ // ignore
136
+ }
88
137
  console.log(" Waiting for login to complete (up to 5 minutes)...");
89
138
  for (let i = 0; i < 300; i++) {
90
139
  if (captured.token && captured.api_url)
@@ -112,7 +161,6 @@ function extractPositionId(apiUrl) {
112
161
  if (internshipsIdx === -1)
113
162
  return null;
114
163
  for (const part of parts.slice(internshipsIdx + 1)) {
115
- // Position IDs are UUIDs (36 chars, 4 dashes)
116
164
  if (part.length === 36 && part.split("-").length === 5) {
117
165
  return part;
118
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kalvium-worklog",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "Auto-submit your daily Kalvium worklog without opening the website",
5
5
  "keywords": [
6
6
  "kalvium",