kalvium-worklog 1.4.2 → 1.4.4
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 -2
- package/dist/capture-token.js +186 -117
- package/package.json +1 -1
package/dist/capture-token.d.ts
CHANGED
|
@@ -1,7 +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
|
|
5
|
-
*
|
|
4
|
+
* On platforms without Playwright (e.g. Android/Termux), uses a
|
|
5
|
+
* reverse proxy to capture the token automatically.
|
|
6
6
|
*/
|
|
7
7
|
export declare function captureToken(): Promise<boolean>;
|
package/dist/capture-token.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { PROFILE_DIR, TOKEN_FILE, ensureInstallDir, saveToken, } from "./config.js";
|
|
2
2
|
import * as readline from "readline";
|
|
3
3
|
import { createServer } from "http";
|
|
4
|
+
import { request as httpRequest } from "https";
|
|
4
5
|
import { execSync } from "child_process";
|
|
5
6
|
/**
|
|
6
7
|
* Check if Playwright is available on this platform.
|
|
@@ -20,24 +21,21 @@ function isPlaywrightAvailable() {
|
|
|
20
21
|
/**
|
|
21
22
|
* Try headless mode first (fast — works if session is still valid).
|
|
22
23
|
* If no session, opens a headed browser and waits for manual login.
|
|
23
|
-
* On platforms without Playwright (e.g. Android/Termux), uses
|
|
24
|
-
*
|
|
24
|
+
* On platforms without Playwright (e.g. Android/Termux), uses a
|
|
25
|
+
* reverse proxy to capture the token automatically.
|
|
25
26
|
*/
|
|
26
27
|
export async function captureToken() {
|
|
27
28
|
ensureInstallDir();
|
|
28
29
|
if (!isPlaywrightAvailable()) {
|
|
29
|
-
return
|
|
30
|
+
return captureTokenViaProxy();
|
|
30
31
|
}
|
|
31
|
-
// Lazy import — only load Playwright when available
|
|
32
32
|
const { chromium } = await import("playwright");
|
|
33
|
-
// Step 1: Try headless (quick — works if session is still valid)
|
|
34
33
|
console.log("Trying headless mode (existing session)...");
|
|
35
34
|
let data = await tryCapture(chromium, true);
|
|
36
35
|
if (data) {
|
|
37
36
|
console.log(" Session still valid — token captured!");
|
|
38
37
|
}
|
|
39
38
|
else {
|
|
40
|
-
// Step 2: Need manual login
|
|
41
39
|
console.log(" No valid session. Opening browser for manual login...");
|
|
42
40
|
data = await tryCapture(chromium, false);
|
|
43
41
|
}
|
|
@@ -52,150 +50,221 @@ export async function captureToken() {
|
|
|
52
50
|
return false;
|
|
53
51
|
}
|
|
54
52
|
/**
|
|
55
|
-
* Capture token
|
|
56
|
-
*
|
|
57
|
-
*
|
|
53
|
+
* Capture token via reverse proxy (for Android/Termux without Playwright).
|
|
54
|
+
*
|
|
55
|
+
* Starts a local server on port 4567 that proxies kalvium.community.
|
|
56
|
+
* The user navigates to http://localhost:4567, logs in normally,
|
|
57
|
+
* and the proxy intercepts the Keycloak token exchange automatically.
|
|
58
|
+
*
|
|
59
|
+
* No javascript: injection, no bookmarklets — just log in normally.
|
|
58
60
|
*/
|
|
59
|
-
async function
|
|
61
|
+
async function captureTokenViaProxy() {
|
|
60
62
|
const PORT = 4567;
|
|
63
|
+
const TARGET = "kalvium.community";
|
|
64
|
+
const AUTH_TARGET = "auth.kalvium.community";
|
|
61
65
|
console.log("\n ========================================");
|
|
62
|
-
console.log("
|
|
66
|
+
console.log(" Reverse proxy token capture (no Playwright)");
|
|
63
67
|
console.log(" ========================================\n");
|
|
64
|
-
|
|
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
|
|
68
|
+
let capturedToken = null;
|
|
67
69
|
const tokenPromise = new Promise((resolve) => {
|
|
68
70
|
const server = createServer((req, res) => {
|
|
71
|
+
// Handle CORS
|
|
69
72
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
70
|
-
res.setHeader("Access-Control-Allow-Headers", "
|
|
73
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
71
74
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
72
75
|
if (req.method === "OPTIONS") {
|
|
73
76
|
res.writeHead(204);
|
|
74
77
|
res.end();
|
|
75
78
|
return;
|
|
76
79
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
80
|
+
const url = new URL(req.url ?? "/", `http://localhost:${PORT}`);
|
|
81
|
+
// Check if we already captured the token — serve success page
|
|
82
|
+
if (url.pathname === "/__done__") {
|
|
83
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
84
|
+
res.end(`<html><body style="font-family:sans-serif;text-align:center;padding:40px">
|
|
85
|
+
<h2>✓ Token captured!</h2>
|
|
86
|
+
<p>You can close this tab and go back to Termux.</p>
|
|
87
|
+
</body></html>`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Determine which backend to proxy to
|
|
91
|
+
let targetHost = TARGET;
|
|
92
|
+
let targetPath = req.url ?? "/";
|
|
93
|
+
// Route auth requests to auth.kalvium.community
|
|
94
|
+
if (url.pathname.startsWith("/auth/") || url.pathname.startsWith("/realms/")) {
|
|
95
|
+
targetHost = AUTH_TARGET;
|
|
96
|
+
targetPath = req.url ?? "/";
|
|
97
|
+
}
|
|
98
|
+
// Intercept token exchange requests
|
|
99
|
+
if (url.pathname.includes("openid-connect/token") &&
|
|
100
|
+
req.method === "POST") {
|
|
101
|
+
// Collect body
|
|
102
|
+
let body = "";
|
|
103
|
+
req.on("data", (chunk) => {
|
|
104
|
+
body += chunk;
|
|
105
|
+
});
|
|
106
|
+
req.on("end", () => {
|
|
107
|
+
// Forward to actual Keycloak
|
|
108
|
+
const proxyReq = httpRequest({
|
|
109
|
+
hostname: AUTH_TARGET,
|
|
110
|
+
port: 443,
|
|
111
|
+
path: url.pathname,
|
|
112
|
+
method: "POST",
|
|
113
|
+
headers: {
|
|
114
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
115
|
+
"content-length": Buffer.byteLength(body),
|
|
116
|
+
},
|
|
117
|
+
}, (proxyRes) => {
|
|
118
|
+
let responseBody = "";
|
|
119
|
+
proxyRes.on("data", (chunk) => {
|
|
120
|
+
responseBody += chunk;
|
|
121
|
+
});
|
|
122
|
+
proxyRes.on("end", () => {
|
|
123
|
+
// Try to capture token from response
|
|
124
|
+
try {
|
|
125
|
+
const data = JSON.parse(responseBody);
|
|
126
|
+
if (data.refresh_token) {
|
|
127
|
+
console.log(" Refresh token captured from proxy!");
|
|
128
|
+
capturedToken = data;
|
|
129
|
+
resolve(data);
|
|
130
|
+
}
|
|
96
131
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
res.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
res.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
catch {
|
|
133
|
+
// not JSON, ignore
|
|
134
|
+
}
|
|
135
|
+
// Send response back to browser
|
|
136
|
+
res.writeHead(proxyRes.statusCode ?? 200, {
|
|
137
|
+
"content-type": proxyRes.headers["content-type"] ?? "application/json",
|
|
138
|
+
"access-control-allow-origin": "*",
|
|
139
|
+
});
|
|
140
|
+
res.end(responseBody);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
proxyReq.on("error", () => {
|
|
144
|
+
res.writeHead(502);
|
|
145
|
+
res.end("Proxy error");
|
|
146
|
+
});
|
|
147
|
+
proxyReq.write(body);
|
|
148
|
+
proxyReq.end();
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Proxy all other requests to kalvium.community
|
|
153
|
+
const proxyHeaders = {};
|
|
154
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
155
|
+
if (key === "host")
|
|
156
|
+
continue;
|
|
157
|
+
if (Array.isArray(value)) {
|
|
158
|
+
proxyHeaders[key] = value.join(", ");
|
|
159
|
+
}
|
|
160
|
+
else if (value) {
|
|
161
|
+
proxyHeaders[key] = value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
proxyHeaders["host"] = targetHost;
|
|
165
|
+
const proxyReq = httpRequest({
|
|
166
|
+
hostname: targetHost,
|
|
167
|
+
port: 443,
|
|
168
|
+
path: targetPath,
|
|
169
|
+
method: req.method ?? "GET",
|
|
170
|
+
headers: proxyHeaders,
|
|
171
|
+
}, (proxyRes) => {
|
|
172
|
+
// Rewrite location headers to point back to proxy
|
|
173
|
+
const headers = {};
|
|
174
|
+
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
175
|
+
if (key === "location" && typeof value === "string") {
|
|
176
|
+
// Rewrite kalvium.community URLs to localhost
|
|
177
|
+
let rewritten = value
|
|
178
|
+
.replace(/https:\/\/kalvium\.community/g, `http://localhost:${PORT}`)
|
|
179
|
+
.replace(/https:\/\/auth\.kalvium\.community/g, `http://localhost:${PORT}`);
|
|
180
|
+
headers[key] = rewritten;
|
|
109
181
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
182
|
+
else if (key === "content-security-policy") {
|
|
183
|
+
// Relax CSP to allow the proxy to work
|
|
184
|
+
headers[key] = value;
|
|
185
|
+
}
|
|
186
|
+
else if (key === "strict-transport-security") {
|
|
187
|
+
// Skip HSTS
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
else if (Array.isArray(value)) {
|
|
191
|
+
headers[key] = value.join(", ");
|
|
192
|
+
}
|
|
193
|
+
else if (value) {
|
|
194
|
+
headers[key] = value;
|
|
114
195
|
}
|
|
115
|
-
return;
|
|
116
196
|
}
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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>`);
|
|
197
|
+
// Rewrite set-cookie domains
|
|
198
|
+
if (headers["set-cookie"]) {
|
|
199
|
+
headers["set-cookie"] = headers["set-cookie"].replace(/domain=\.kalvium\.community/gi, `domain=localhost`);
|
|
200
|
+
}
|
|
201
|
+
res.writeHead(proxyRes.statusCode ?? 200, headers);
|
|
202
|
+
// For HTML responses, rewrite URLs
|
|
203
|
+
const contentType = proxyRes.headers["content-type"] ?? "";
|
|
204
|
+
if (contentType.includes("text/html") || contentType.includes("javascript") || contentType.includes("css")) {
|
|
205
|
+
let body = "";
|
|
206
|
+
proxyRes.on("data", (chunk) => {
|
|
207
|
+
body += chunk.toString();
|
|
208
|
+
});
|
|
209
|
+
proxyRes.on("end", () => {
|
|
210
|
+
// Rewrite absolute URLs to go through proxy
|
|
211
|
+
const rewritten = body
|
|
212
|
+
.replace(/https:\/\/kalvium\.community/g, `http://localhost:${PORT}`)
|
|
213
|
+
.replace(/https:\/\/auth\.kalvium\.community/g, `http://localhost:${PORT}`)
|
|
214
|
+
.replace(/https:\/\/student-api\.kalvium\.community/g, `http://localhost:${PORT}/api`);
|
|
215
|
+
res.end(rewritten);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
proxyRes.pipe(res);
|
|
220
|
+
}
|
|
166
221
|
});
|
|
222
|
+
proxyReq.on("error", () => {
|
|
223
|
+
res.writeHead(502);
|
|
224
|
+
res.end("Proxy error — check your internet connection");
|
|
225
|
+
});
|
|
226
|
+
// Forward request body for POST/PUT
|
|
227
|
+
if (req.method === "POST" || req.method === "PUT") {
|
|
228
|
+
req.pipe(proxyReq);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
proxyReq.end();
|
|
232
|
+
}
|
|
167
233
|
});
|
|
168
234
|
server.listen(PORT, () => {
|
|
169
|
-
console.log(`
|
|
235
|
+
console.log(` Proxy server running on http://localhost:${PORT}`);
|
|
170
236
|
});
|
|
237
|
+
// Timeout after 5 minutes
|
|
171
238
|
setTimeout(() => {
|
|
172
|
-
|
|
173
|
-
|
|
239
|
+
if (!capturedToken) {
|
|
240
|
+
server.close();
|
|
241
|
+
resolve(null);
|
|
242
|
+
}
|
|
174
243
|
}, 300000);
|
|
175
244
|
});
|
|
176
|
-
// Open the
|
|
177
|
-
const
|
|
245
|
+
// Open the proxy URL in the phone's default browser
|
|
246
|
+
const proxyUrl = `http://localhost:${PORT}/internships`;
|
|
178
247
|
try {
|
|
179
248
|
if (process.platform === "android") {
|
|
180
|
-
execSync(`termux-open-url ${
|
|
249
|
+
execSync(`termux-open-url ${proxyUrl}`, { stdio: "ignore" });
|
|
181
250
|
}
|
|
182
251
|
else {
|
|
183
|
-
execSync(`xdg-open ${
|
|
252
|
+
execSync(`xdg-open ${proxyUrl} || open ${proxyUrl}`, {
|
|
184
253
|
stdio: "ignore",
|
|
185
254
|
});
|
|
186
255
|
}
|
|
187
|
-
console.log(` Opened ${
|
|
256
|
+
console.log(` Opened ${proxyUrl} in your browser.`);
|
|
188
257
|
}
|
|
189
258
|
catch {
|
|
190
|
-
console.log(` Could not auto-open browser. Please open: ${
|
|
259
|
+
console.log(` Could not auto-open browser. Please open: ${proxyUrl}`);
|
|
191
260
|
}
|
|
192
261
|
console.log("\n ────────────────────────────────────────────");
|
|
193
|
-
console.log(" A page opened in your browser
|
|
194
|
-
console.log("
|
|
195
|
-
console.log("
|
|
196
|
-
console.log("
|
|
262
|
+
console.log(" A page opened in your browser.");
|
|
263
|
+
console.log(" Just log in with Google normally.");
|
|
264
|
+
console.log(" The token is captured automatically — no");
|
|
265
|
+
console.log(" javascript: or bookmarks needed.");
|
|
197
266
|
console.log(" ────────────────────────────────────────────");
|
|
198
|
-
console.log("\n Waiting for
|
|
267
|
+
console.log("\n Waiting for login (up to 5 minutes)...\n");
|
|
199
268
|
const data = await tokenPromise;
|
|
200
269
|
if (data) {
|
|
201
270
|
saveToken(data);
|
|
@@ -205,12 +274,12 @@ async function captureTokenViaBrowser() {
|
|
|
205
274
|
return true;
|
|
206
275
|
}
|
|
207
276
|
// Fallback: manual paste
|
|
208
|
-
console.log("\n
|
|
277
|
+
console.log("\n Proxy capture failed. Let's try manual paste.");
|
|
209
278
|
return captureTokenManual();
|
|
210
279
|
}
|
|
211
280
|
/**
|
|
212
|
-
* Manual token capture — fallback
|
|
213
|
-
* Prompts the user to paste a refresh token JSON
|
|
281
|
+
* Manual token capture — fallback.
|
|
282
|
+
* Prompts the user to paste a refresh token JSON.
|
|
214
283
|
*/
|
|
215
284
|
async function captureTokenManual() {
|
|
216
285
|
console.log("\n ========================================");
|