kalvium-worklog 1.3.2 → 1.4.0
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.d.ts +2 -0
- package/dist/capture-token.js +188 -23
- package/package.json +4 -2
package/dist/capture-token.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Try headless mode first (fast — works if session is still valid).
|
|
3
3
|
* If no session, opens a headed browser and waits for manual login.
|
|
4
|
+
* On platforms without Playwright (e.g. Android/Termux), uses the
|
|
5
|
+
* phone's built-in browser + a local HTTP server to capture the token.
|
|
4
6
|
*/
|
|
5
7
|
export declare function captureToken(): Promise<boolean>;
|
package/dist/capture-token.js
CHANGED
|
@@ -1,21 +1,45 @@
|
|
|
1
|
-
import { chromium } from "playwright";
|
|
2
1
|
import { PROFILE_DIR, TOKEN_FILE, ensureInstallDir, saveToken, } from "./config.js";
|
|
2
|
+
import * as readline from "readline";
|
|
3
|
+
import { createServer } from "http";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
/**
|
|
6
|
+
* Check if Playwright is available on this platform.
|
|
7
|
+
* Playwright doesn't support Android/Termux.
|
|
8
|
+
*/
|
|
9
|
+
function isPlaywrightAvailable() {
|
|
10
|
+
try {
|
|
11
|
+
if (process.platform === "android")
|
|
12
|
+
return false;
|
|
13
|
+
require.resolve("playwright");
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
3
20
|
/**
|
|
4
21
|
* Try headless mode first (fast — works if session is still valid).
|
|
5
22
|
* If no session, opens a headed browser and waits for manual login.
|
|
23
|
+
* On platforms without Playwright (e.g. Android/Termux), uses the
|
|
24
|
+
* phone's built-in browser + a local HTTP server to capture the token.
|
|
6
25
|
*/
|
|
7
26
|
export async function captureToken() {
|
|
8
27
|
ensureInstallDir();
|
|
28
|
+
if (!isPlaywrightAvailable()) {
|
|
29
|
+
return captureTokenViaBrowser();
|
|
30
|
+
}
|
|
31
|
+
// Lazy import — only load Playwright when available
|
|
32
|
+
const { chromium } = await import("playwright");
|
|
9
33
|
// Step 1: Try headless (quick — works if session is still valid)
|
|
10
34
|
console.log("Trying headless mode (existing session)...");
|
|
11
|
-
let data = await tryCapture(true);
|
|
35
|
+
let data = await tryCapture(chromium, true);
|
|
12
36
|
if (data) {
|
|
13
37
|
console.log(" Session still valid — token captured!");
|
|
14
38
|
}
|
|
15
39
|
else {
|
|
16
40
|
// Step 2: Need manual login
|
|
17
41
|
console.log(" No valid session. Opening browser for manual login...");
|
|
18
|
-
data = await tryCapture(false);
|
|
42
|
+
data = await tryCapture(chromium, false);
|
|
19
43
|
}
|
|
20
44
|
if (data) {
|
|
21
45
|
saveToken(data);
|
|
@@ -27,17 +51,173 @@ export async function captureToken() {
|
|
|
27
51
|
console.log("\nERROR: Could not capture token. Please log in and try again.");
|
|
28
52
|
return false;
|
|
29
53
|
}
|
|
30
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Capture token on platforms without Playwright (Android/Termux).
|
|
56
|
+
* Starts a local HTTP server, opens the phone's browser to kalvium.community,
|
|
57
|
+
* and after login the user pastes a JS snippet that sends the token back.
|
|
58
|
+
*/
|
|
59
|
+
async function captureTokenViaBrowser() {
|
|
60
|
+
const PORT = 4567;
|
|
61
|
+
console.log("\n ========================================");
|
|
62
|
+
console.log(" Browser-based token capture (no Playwright)");
|
|
63
|
+
console.log(" ========================================\n");
|
|
64
|
+
// Start local HTTP server to receive the token
|
|
65
|
+
const tokenPromise = new Promise((resolve) => {
|
|
66
|
+
const server = createServer((req, res) => {
|
|
67
|
+
// Handle CORS + token capture
|
|
68
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
69
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
70
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
71
|
+
if (req.method === "OPTIONS") {
|
|
72
|
+
res.writeHead(204);
|
|
73
|
+
res.end();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let body = "";
|
|
77
|
+
req.on("data", (chunk) => {
|
|
78
|
+
body += chunk;
|
|
79
|
+
});
|
|
80
|
+
req.on("end", () => {
|
|
81
|
+
if (req.url?.startsWith("/capture")) {
|
|
82
|
+
try {
|
|
83
|
+
// Try parsing as JSON first (POST body)
|
|
84
|
+
let data;
|
|
85
|
+
if (body) {
|
|
86
|
+
data = JSON.parse(body);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Try query param
|
|
90
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
91
|
+
const tokenParam = url.searchParams.get("token");
|
|
92
|
+
if (!tokenParam) {
|
|
93
|
+
res.writeHead(400);
|
|
94
|
+
res.end("Missing token");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
data = JSON.parse(tokenParam);
|
|
98
|
+
}
|
|
99
|
+
if (data.refresh_token) {
|
|
100
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
101
|
+
res.end("<html><body><h2>Token captured! You can close this tab.</h2></body></html>");
|
|
102
|
+
resolve(data);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
res.writeHead(400);
|
|
106
|
+
res.end("Invalid token — missing refresh_token");
|
|
107
|
+
resolve(null);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
res.writeHead(400);
|
|
112
|
+
res.end("Invalid JSON");
|
|
113
|
+
resolve(null);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
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
|
+
}
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
server.listen(PORT, () => {
|
|
129
|
+
console.log(` Local server running on http://localhost:${PORT}`);
|
|
130
|
+
});
|
|
131
|
+
// Timeout after 5 minutes
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
server.close();
|
|
134
|
+
resolve(null);
|
|
135
|
+
}, 300000);
|
|
136
|
+
});
|
|
137
|
+
// Open kalvium.community in the phone's default browser
|
|
138
|
+
const url = "https://kalvium.community/internships";
|
|
139
|
+
try {
|
|
140
|
+
if (process.platform === "android") {
|
|
141
|
+
// Termux: use termux-open-url
|
|
142
|
+
execSync(`termux-open-url ${url}`, { stdio: "ignore" });
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Other platforms: try xdg-open or open
|
|
146
|
+
execSync(`xdg-open ${url} || open ${url}`, { stdio: "ignore" });
|
|
147
|
+
}
|
|
148
|
+
console.log(` Opened ${url} in your browser.`);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
console.log(` Could not auto-open browser. Please open: ${url}`);
|
|
152
|
+
}
|
|
153
|
+
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.)");
|
|
164
|
+
console.log("\n Waiting for token (up to 5 minutes)...\n");
|
|
165
|
+
const data = await tokenPromise;
|
|
166
|
+
if (data) {
|
|
167
|
+
saveToken(data);
|
|
168
|
+
console.log(`\n✓ Token captured and saved to ${TOKEN_FILE}`);
|
|
169
|
+
console.log(` Access token expires in: ${data.expires_in ?? "?"} seconds`);
|
|
170
|
+
console.log(` Refresh token expires in: ${data.refresh_expires_in ?? "?"} seconds`);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
// Fallback: manual paste
|
|
174
|
+
console.log("\n Automatic capture failed. Let's try manual paste.");
|
|
175
|
+
return captureTokenManual();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Manual token capture — fallback for platforms without Playwright.
|
|
179
|
+
* Prompts the user to paste a refresh token JSON obtained from a desktop.
|
|
180
|
+
*/
|
|
181
|
+
async function captureTokenManual() {
|
|
182
|
+
console.log("\n ========================================");
|
|
183
|
+
console.log(" Manual token setup");
|
|
184
|
+
console.log(" ========================================\n");
|
|
185
|
+
console.log(" To get your token:");
|
|
186
|
+
console.log(" 1. Run `kalvium-worklog login` on a desktop/laptop");
|
|
187
|
+
console.log(" 2. Copy the contents of ~/.kalvium/refresh_token.json");
|
|
188
|
+
console.log(" 3. Paste it below\n");
|
|
189
|
+
const rl = readline.createInterface({
|
|
190
|
+
input: process.stdin,
|
|
191
|
+
output: process.stdout,
|
|
192
|
+
});
|
|
193
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
194
|
+
const input = await ask("Paste token JSON here: ");
|
|
195
|
+
rl.close();
|
|
196
|
+
try {
|
|
197
|
+
const data = JSON.parse(input.trim());
|
|
198
|
+
if (!data.refresh_token) {
|
|
199
|
+
console.log("ERROR: Invalid token JSON — missing refresh_token field.");
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
saveToken(data);
|
|
203
|
+
console.log(`\nToken saved to ${TOKEN_FILE}`);
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
console.log("ERROR: Invalid JSON. Please paste the full token JSON.");
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function tryCapture(chromium, headless) {
|
|
31
212
|
const tokenData = {};
|
|
32
213
|
const context = await chromium.launchPersistentContext(PROFILE_DIR, {
|
|
33
|
-
headless: false,
|
|
214
|
+
headless: false,
|
|
34
215
|
args: [
|
|
35
216
|
"--disable-blink-features=AutomationControlled",
|
|
36
217
|
...(headless ? ["--headless=new"] : []),
|
|
37
218
|
],
|
|
38
219
|
});
|
|
39
220
|
const page = context.pages()[0] ?? (await context.newPage());
|
|
40
|
-
// Intercept token exchange requests
|
|
41
221
|
context.route("**/openid-connect/token", (route) => {
|
|
42
222
|
if (route.request().method() === "POST") {
|
|
43
223
|
route.fetch().then((response) => {
|
|
@@ -74,23 +254,17 @@ async function tryCapture(headless) {
|
|
|
74
254
|
await page
|
|
75
255
|
.waitForLoadState("networkidle", { timeout: 10000 })
|
|
76
256
|
.catch(() => { });
|
|
77
|
-
// Auto-click "Continue with Google" in headless mode too.
|
|
78
|
-
// If the Google session is still valid in the persistent profile,
|
|
79
|
-
// this completes the entire login invisibly — no browser window,
|
|
80
|
-
// no user interaction needed.
|
|
81
257
|
const googleBtn = page.locator("text=Continue with Google").first();
|
|
82
258
|
const isVisible = await googleBtn.isVisible().catch(() => false);
|
|
83
259
|
if (isVisible) {
|
|
84
260
|
await googleBtn.click();
|
|
85
261
|
console.log(" Clicked 'Continue with Google' (headless)...");
|
|
86
|
-
// Wait for token exchange after auto-click
|
|
87
262
|
for (let i = 0; i < 15; i++) {
|
|
88
263
|
if (tokenData.data)
|
|
89
264
|
break;
|
|
90
265
|
await page.waitForTimeout(1000);
|
|
91
266
|
}
|
|
92
267
|
}
|
|
93
|
-
// If still no token, wait a bit more for any redirects
|
|
94
268
|
if (!tokenData.data) {
|
|
95
269
|
await page.waitForTimeout(5000);
|
|
96
270
|
}
|
|
@@ -108,21 +282,14 @@ async function tryCapture(headless) {
|
|
|
108
282
|
waitUntil: "domcontentloaded",
|
|
109
283
|
timeout: 120000,
|
|
110
284
|
});
|
|
111
|
-
// Auto-click "Continue with Google" button
|
|
112
285
|
try {
|
|
113
|
-
|
|
114
|
-
const
|
|
115
|
-
.locator("text=Continue with Google")
|
|
116
|
-
.first();
|
|
117
|
-
const isVisible = await googleBtn
|
|
118
|
-
.isVisible()
|
|
119
|
-
.catch(() => false);
|
|
286
|
+
const googleBtn = page.locator("text=Continue with Google").first();
|
|
287
|
+
const isVisible = await googleBtn.isVisible().catch(() => false);
|
|
120
288
|
if (isVisible) {
|
|
121
289
|
await googleBtn.click();
|
|
122
290
|
console.log(" Clicked 'Continue with Google'.");
|
|
123
291
|
}
|
|
124
292
|
else {
|
|
125
|
-
// Fallback: try other patterns
|
|
126
293
|
await page
|
|
127
294
|
.locator("button:has-text('Google'), a:has-text('Google')")
|
|
128
295
|
.first()
|
|
@@ -135,7 +302,6 @@ async function tryCapture(headless) {
|
|
|
135
302
|
console.log(" Could not auto-click. Please click 'Continue with Google' manually.");
|
|
136
303
|
}
|
|
137
304
|
console.log(" Waiting for login to complete (up to 5 minutes)...");
|
|
138
|
-
// Poll up to 5 minutes
|
|
139
305
|
for (let i = 0; i < 300; i++) {
|
|
140
306
|
if (tokenData.data)
|
|
141
307
|
break;
|
|
@@ -150,7 +316,6 @@ async function tryCapture(headless) {
|
|
|
150
316
|
}
|
|
151
317
|
}
|
|
152
318
|
}
|
|
153
|
-
// Last resort: reload to trigger token exchange
|
|
154
319
|
if (!tokenData.data) {
|
|
155
320
|
console.log(" Trying page reload to trigger token exchange...");
|
|
156
321
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kalvium-worklog",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Auto-submit your daily Kalvium worklog without opening the website",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kalvium",
|
|
@@ -37,7 +37,9 @@
|
|
|
37
37
|
"postinstall": "node dist/postinstall.js"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"commander": "^12.1.0"
|
|
40
|
+
"commander": "^12.1.0"
|
|
41
|
+
},
|
|
42
|
+
"optionalDependencies": {
|
|
41
43
|
"playwright": "^1.48.0"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|