kalvium-worklog 1.4.3 → 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 -137
- 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,170 +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
|
-
|
|
68
|
+
let capturedToken = null;
|
|
65
69
|
const tokenPromise = new Promise((resolve) => {
|
|
66
70
|
const server = createServer((req, res) => {
|
|
71
|
+
// Handle CORS
|
|
67
72
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
68
|
-
res.setHeader("Access-Control-Allow-Headers", "
|
|
73
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
69
74
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
70
75
|
if (req.method === "OPTIONS") {
|
|
71
76
|
res.writeHead(204);
|
|
72
77
|
res.end();
|
|
73
78
|
return;
|
|
74
79
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
+
}
|
|
94
131
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
res.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
res.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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;
|
|
107
181
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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;
|
|
112
195
|
}
|
|
113
|
-
return;
|
|
114
196
|
}
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
</head>
|
|
140
|
-
<body>
|
|
141
|
-
<h2>Kalvium Worklog — Login</h2>
|
|
142
|
-
|
|
143
|
-
<div class="step">
|
|
144
|
-
<h3>Step 1: Log in to Kalvium</h3>
|
|
145
|
-
<p>Tap below, log in with Google (2FA if asked).</p>
|
|
146
|
-
<a class="btn btn-green" href="https://kalvium.community/internships" target="_blank">Open Kalvium →</a>
|
|
147
|
-
</div>
|
|
148
|
-
|
|
149
|
-
<div class="step">
|
|
150
|
-
<h3>Step 2: Extract token</h3>
|
|
151
|
-
<p>After logging in, go to the Kalvium tab.<br>
|
|
152
|
-
<b>Type</b> this in the address bar (don't paste — type <code>javascript:</code> yourself, then paste the rest):</p>
|
|
153
|
-
<div class="code-box" id="jscode">javascript:void(function(){var t;for(var i=0;i<localStorage.length;i++){var k=localStorage.key(i),v=localStorage.getItem(i);if(v&&v.includes('refresh_token')){t=v}}if(!t){for(var i=0;i<sessionStorage.length;i++){var k=sessionStorage.key(i),v=sessionStorage.getItem(i);if(v&&v.includes('refresh_token')){t=v}}}if(t){fetch('http://localhost:${PORT}/capture',{method:'POST',body:t}).then(function(r){alert(r.ok?'Token sent! Go back to Termux':'Failed: '+r.status)}).catch(function(e){alert('Error: '+e)})}else{alert('No token found. Are you logged in?')}})()</div>
|
|
154
|
-
<p><small>👆 Long-press the code above → "Copy", then go to Kalvium tab, type <code>javascript:</code> in address bar, paste the rest after it, press Enter.</small></p>
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
<div class="warn">
|
|
158
|
-
<b>Brave users:</b> Brave strips <code>javascript:</code> when pasting. You must <b>type</b> <code>javascript:</code> manually, then paste only the code after <code>javascript:</code>.
|
|
159
|
-
</div>
|
|
160
|
-
|
|
161
|
-
<div class="step">
|
|
162
|
-
<h3>Or: Paste token manually</h3>
|
|
163
|
-
<p>If the above doesn't work, extract the token another way and paste it here:</p>
|
|
164
|
-
<textarea id="manualToken" placeholder='Paste token JSON here (starts with {"access_token"...'></textarea>
|
|
165
|
-
<button class="btn" onclick="sendManual()">Send Token</button>
|
|
166
|
-
<div id="result"></div>
|
|
167
|
-
</div>
|
|
168
|
-
|
|
169
|
-
<script>
|
|
170
|
-
function sendManual() {
|
|
171
|
-
var val = document.getElementById('manualToken').value.trim();
|
|
172
|
-
var result = document.getElementById('result');
|
|
173
|
-
result.style.display = 'block';
|
|
174
|
-
if (!val) { result.className = 'error'; result.textContent = 'Paste token JSON first'; return; }
|
|
175
|
-
fetch('http://localhost:${PORT}/capture', { method: 'POST', body: val })
|
|
176
|
-
.then(function(r) {
|
|
177
|
-
if (r.ok) { result.className = 'success'; result.textContent = '✓ Token sent! Go back to Termux.'; }
|
|
178
|
-
else { result.className = 'error'; result.textContent = 'Failed: ' + r.status; }
|
|
179
|
-
})
|
|
180
|
-
.catch(function(e) { result.className = 'error'; result.textContent = 'Error: ' + e; });
|
|
181
|
-
}
|
|
182
|
-
</script>
|
|
183
|
-
|
|
184
|
-
</body>
|
|
185
|
-
</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
|
+
}
|
|
186
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
|
+
}
|
|
187
233
|
});
|
|
188
234
|
server.listen(PORT, () => {
|
|
189
|
-
console.log(`
|
|
235
|
+
console.log(` Proxy server running on http://localhost:${PORT}`);
|
|
190
236
|
});
|
|
237
|
+
// Timeout after 5 minutes
|
|
191
238
|
setTimeout(() => {
|
|
192
|
-
|
|
193
|
-
|
|
239
|
+
if (!capturedToken) {
|
|
240
|
+
server.close();
|
|
241
|
+
resolve(null);
|
|
242
|
+
}
|
|
194
243
|
}, 300000);
|
|
195
244
|
});
|
|
196
|
-
// Open the
|
|
197
|
-
const
|
|
245
|
+
// Open the proxy URL in the phone's default browser
|
|
246
|
+
const proxyUrl = `http://localhost:${PORT}/internships`;
|
|
198
247
|
try {
|
|
199
248
|
if (process.platform === "android") {
|
|
200
|
-
execSync(`termux-open-url ${
|
|
249
|
+
execSync(`termux-open-url ${proxyUrl}`, { stdio: "ignore" });
|
|
201
250
|
}
|
|
202
251
|
else {
|
|
203
|
-
execSync(`xdg-open ${
|
|
252
|
+
execSync(`xdg-open ${proxyUrl} || open ${proxyUrl}`, {
|
|
204
253
|
stdio: "ignore",
|
|
205
254
|
});
|
|
206
255
|
}
|
|
207
|
-
console.log(` Opened ${
|
|
256
|
+
console.log(` Opened ${proxyUrl} in your browser.`);
|
|
208
257
|
}
|
|
209
258
|
catch {
|
|
210
|
-
console.log(` Could not auto-open browser. Please open: ${
|
|
259
|
+
console.log(` Could not auto-open browser. Please open: ${proxyUrl}`);
|
|
211
260
|
}
|
|
212
261
|
console.log("\n ────────────────────────────────────────────");
|
|
213
|
-
console.log(" A page opened in your browser
|
|
214
|
-
console.log("
|
|
215
|
-
console.log("
|
|
216
|
-
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.");
|
|
217
266
|
console.log(" ────────────────────────────────────────────");
|
|
218
|
-
console.log("\n Waiting for
|
|
267
|
+
console.log("\n Waiting for login (up to 5 minutes)...\n");
|
|
219
268
|
const data = await tokenPromise;
|
|
220
269
|
if (data) {
|
|
221
270
|
saveToken(data);
|
|
@@ -225,12 +274,12 @@ function sendManual() {
|
|
|
225
274
|
return true;
|
|
226
275
|
}
|
|
227
276
|
// Fallback: manual paste
|
|
228
|
-
console.log("\n
|
|
277
|
+
console.log("\n Proxy capture failed. Let's try manual paste.");
|
|
229
278
|
return captureTokenManual();
|
|
230
279
|
}
|
|
231
280
|
/**
|
|
232
|
-
* Manual token capture — fallback
|
|
233
|
-
* Prompts the user to paste a refresh token JSON
|
|
281
|
+
* Manual token capture — fallback.
|
|
282
|
+
* Prompts the user to paste a refresh token JSON.
|
|
234
283
|
*/
|
|
235
284
|
async function captureTokenManual() {
|
|
236
285
|
console.log("\n ========================================");
|