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.
- package/dist/capture-token.js +72 -38
- package/dist/discover-position.d.ts +4 -2
- package/dist/discover-position.js +65 -17
- package/package.json +1 -1
package/dist/capture-token.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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, """)}">📋 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
|
|
138
|
-
const
|
|
176
|
+
// Open the localhost page in the browser
|
|
177
|
+
const localUrl = `http://localhost:${PORT}`;
|
|
139
178
|
try {
|
|
140
179
|
if (process.platform === "android") {
|
|
141
|
-
|
|
142
|
-
execSync(`termux-open-url ${url}`, { stdio: "ignore" });
|
|
180
|
+
execSync(`termux-open-url ${localUrl}`, { stdio: "ignore" });
|
|
143
181
|
}
|
|
144
182
|
else {
|
|
145
|
-
|
|
146
|
-
|
|
183
|
+
execSync(`xdg-open ${localUrl} || open ${localUrl}`, {
|
|
184
|
+
stdio: "ignore",
|
|
185
|
+
});
|
|
147
186
|
}
|
|
148
|
-
console.log(` Opened ${
|
|
187
|
+
console.log(` Opened ${localUrl} in your browser.`);
|
|
149
188
|
}
|
|
150
189
|
catch {
|
|
151
|
-
console.log(` Could not auto-open browser. Please open: ${
|
|
190
|
+
console.log(` Could not auto-open browser. Please open: ${localUrl}`);
|
|
152
191
|
}
|
|
153
192
|
console.log("\n ────────────────────────────────────────────");
|
|
154
|
-
console.log("
|
|
155
|
-
console.log("
|
|
156
|
-
console.log("
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
3
|
-
* Tries
|
|
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 {
|
|
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
|
-
*
|
|
5
|
-
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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
|
|
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.
|
|
79
|
-
console.log("
|
|
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
|
}
|