pinokiod 3.41.0 → 3.43.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.
Files changed (82) hide show
  1. package/kernel/api/browser/index.js +3 -1
  2. package/kernel/api/cloudflare/index.js +3 -3
  3. package/kernel/api/index.js +187 -51
  4. package/kernel/api/loading/index.js +15 -0
  5. package/kernel/api/process/index.js +7 -0
  6. package/kernel/api/shell/index.js +0 -2
  7. package/kernel/bin/browserless.js +22 -0
  8. package/kernel/bin/caddy.js +36 -4
  9. package/kernel/bin/index.js +4 -1
  10. package/kernel/bin/setup.js +38 -5
  11. package/kernel/connect/backend.js +110 -0
  12. package/kernel/connect/config.js +171 -0
  13. package/kernel/connect/index.js +18 -7
  14. package/kernel/connect/providers/huggingface/index.js +98 -0
  15. package/kernel/connect/providers/x/index.js +0 -1
  16. package/kernel/environment.js +91 -19
  17. package/kernel/git.js +46 -3
  18. package/kernel/index.js +119 -39
  19. package/kernel/peer.js +40 -5
  20. package/kernel/plugin.js +3 -2
  21. package/kernel/procs.js +27 -20
  22. package/kernel/prototype.js +30 -16
  23. package/kernel/router/common.js +1 -1
  24. package/kernel/router/connector.js +1 -3
  25. package/kernel/router/index.js +38 -4
  26. package/kernel/router/localhost_home_router.js +5 -1
  27. package/kernel/router/localhost_port_router.js +27 -1
  28. package/kernel/router/localhost_static_router.js +93 -0
  29. package/kernel/router/localhost_variable_router.js +14 -9
  30. package/kernel/router/peer_peer_router.js +3 -0
  31. package/kernel/router/peer_static_router.js +43 -0
  32. package/kernel/router/peer_variable_router.js +15 -14
  33. package/kernel/router/processor.js +26 -1
  34. package/kernel/router/rewriter.js +59 -0
  35. package/kernel/scripts/git/commit +11 -1
  36. package/kernel/shell.js +8 -3
  37. package/kernel/util.js +65 -6
  38. package/package.json +2 -1
  39. package/server/index.js +1037 -964
  40. package/server/public/common.js +382 -1
  41. package/server/public/fscreator.js +0 -1
  42. package/server/public/loading.js +17 -0
  43. package/server/public/notifyinput.js +0 -1
  44. package/server/public/opener.js +4 -2
  45. package/server/public/style.css +311 -11
  46. package/server/socket.js +7 -1
  47. package/server/views/app.ejs +1747 -351
  48. package/server/views/columns.ejs +338 -0
  49. package/server/views/connect/huggingface.ejs +353 -0
  50. package/server/views/connect/index.ejs +410 -0
  51. package/server/views/connect/x.ejs +43 -9
  52. package/server/views/connect.ejs +709 -49
  53. package/server/views/container.ejs +357 -0
  54. package/server/views/d.ejs +251 -62
  55. package/server/views/download.ejs +54 -10
  56. package/server/views/editor.ejs +11 -0
  57. package/server/views/explore.ejs +40 -15
  58. package/server/views/file_explorer.ejs +25 -246
  59. package/server/views/form.ejs +44 -1
  60. package/server/views/frame.ejs +39 -1
  61. package/server/views/github.ejs +48 -11
  62. package/server/views/help.ejs +48 -7
  63. package/server/views/index.ejs +119 -58
  64. package/server/views/index2.ejs +3 -4
  65. package/server/views/init/index.ejs +651 -197
  66. package/server/views/install.ejs +1 -1
  67. package/server/views/mini.ejs +47 -18
  68. package/server/views/net.ejs +199 -67
  69. package/server/views/network.ejs +220 -94
  70. package/server/views/network2.ejs +3 -4
  71. package/server/views/old_network.ejs +3 -3
  72. package/server/views/prototype/index.ejs +48 -11
  73. package/server/views/review.ejs +1005 -0
  74. package/server/views/rows.ejs +341 -0
  75. package/server/views/screenshots.ejs +1020 -0
  76. package/server/views/settings.ejs +160 -23
  77. package/server/views/setup.ejs +49 -7
  78. package/server/views/setup_home.ejs +43 -10
  79. package/server/views/shell.ejs +7 -1
  80. package/server/views/start.ejs +14 -9
  81. package/server/views/terminal.ejs +13 -2
  82. package/server/views/tools.ejs +1015 -0
@@ -0,0 +1,338 @@
1
+ <html>
2
+ <head>
3
+ <style>
4
+ html, body {
5
+ width: 100%;
6
+ height: 100%;
7
+ margin: 0;
8
+ }
9
+ body.single {
10
+ display: block;
11
+ }
12
+ body {
13
+ display: grid;
14
+ grid-template-columns: var(--col0, 1fr) 6px var(--col1, 1fr);
15
+ gap: 0px;
16
+ }
17
+ body iframe {
18
+ border: none;
19
+ width: 100%;
20
+ height: 100%;
21
+ }
22
+ /* Splitter (gutter) styles */
23
+ .gutter {
24
+ background: whitesmoke;
25
+ cursor: col-resize;
26
+ position: relative;
27
+ }
28
+ body.dark {
29
+ background: #1B1C1D;
30
+ }
31
+ body.dark .gutter {
32
+ background: #111111;
33
+ }
34
+ body.resizing, .gutter:hover {
35
+ cursor: col-resize;
36
+ }
37
+ body.resizing {
38
+ user-select: none;
39
+ }
40
+ /* Visible handle */
41
+ .gutter::before {
42
+ content: '';
43
+ position: absolute;
44
+ top: 50%;
45
+ left: 50%;
46
+ transform: translate(-50%, -50%);
47
+ width: 4px;
48
+ height: 32px;
49
+ border-radius: 2px;
50
+ /*
51
+ background: #bdbdbd;
52
+ */
53
+ }
54
+ .gutter:hover::before, body.resizing .gutter::before { background: #9e9e9e; }
55
+ .gutter:focus { outline: none; box-shadow: inset 0 0 0 2px #90caf9; }
56
+ </style>
57
+ </head>
58
+ <body class='<%=theme%>'>
59
+ <iframe id='col0' data-src="<%=src%>"></iframe>
60
+ <div id="gutter" class="gutter" tabindex="0" role="separator" aria-orientation="vertical" aria-label="Resize panels" aria-valuemin="120" aria-valuemax="0" aria-valuenow="0"></div>
61
+ <iframe id='col1' data-src="<%=src%>"></iframe>
62
+
63
+ <script>
64
+ (function() {
65
+ const gutter = document.getElementById('gutter');
66
+ const left = document.getElementById('col0');
67
+ const right = document.getElementById('col1');
68
+ const body = document.body;
69
+ const GUTTER = gutter ? gutter.getBoundingClientRect().width || 6 : 6;
70
+ const MIN = 120; // minimum width for each pane in px
71
+ // Stable instance path for recursive panes
72
+ // Prefer parent-assigned iframe name; fallback to window.name; else 'root' for top-level
73
+ const PATH = (window.frameElement && window.frameElement.name) || window.name || 'root';
74
+ if (!window.name) { try { window.name = PATH; } catch(_) {} }
75
+ // Assign deterministic names to child panes before loading
76
+ left.name = `${PATH}.0`;
77
+ right.name = `${PATH}.1`;
78
+ const splitKey = `splitRatio:${PATH}`;
79
+ const KEY_STEP = 20;
80
+ const KEY_STEP_BIG = 100;
81
+ const urlKeyFor = (paneName) => `paneUrl:${paneName}`;
82
+
83
+ function setColumns(leftPx) {
84
+ console.log("setColumns", leftPx)
85
+ document.body.style.gridTemplateColumns = `${leftPx}px ${GUTTER}px 1fr`;
86
+ }
87
+
88
+ function computeTotal() {
89
+ const bodyRect = body.getBoundingClientRect();
90
+ return bodyRect.width - GUTTER;
91
+ }
92
+
93
+ function clamp(val, min, max) { return Math.max(min, Math.min(max, val)); }
94
+
95
+ function updateAria(leftPx, total) {
96
+ if (!gutter) return;
97
+ gutter.setAttribute('aria-valuemin', String(MIN));
98
+ gutter.setAttribute('aria-valuemax', String(Math.max(MIN, total - MIN)));
99
+ gutter.setAttribute('aria-valuenow', String(Math.max(MIN, Math.min(total - MIN, Math.round(leftPx)))));
100
+ }
101
+
102
+ function saveRatioFromLeftPx(leftPx) {
103
+ const total = computeTotal();
104
+ if (total > 0) {
105
+ const ratio = clamp(leftPx / total, 0, 1);
106
+ try { sessionStorage.setItem(splitKey, String(ratio)); } catch (_) {}
107
+ }
108
+ }
109
+
110
+ function applyFromRatio(ratio) {
111
+ console.log("applyFromRatio", ratio)
112
+ const total = computeTotal();
113
+ let leftPx = clamp(Math.round(total * ratio), MIN, total - MIN);
114
+ setColumns(leftPx);
115
+ updateAria(leftPx, total);
116
+ }
117
+
118
+ // --- Per-window URL persistence for each pane ---
119
+ function restorePaneURL(pane, key) {
120
+ try {
121
+ const saved = sessionStorage.getItem(key);
122
+ const fallback = pane.getAttribute('data-src') || pane.getAttribute('src') || '';
123
+ const target = (saved && typeof saved === 'string') ? saved : fallback;
124
+ if (target && pane.src !== target) pane.src = target;
125
+ } catch (_) { /* ignore */ }
126
+ }
127
+
128
+ function attachSameOriginRouteHooks(pane, key) {
129
+ try {
130
+ const cw = pane.contentWindow;
131
+ if (!cw) return;
132
+ const notify = () => {
133
+ try { sessionStorage.setItem(key, cw.location.href); } catch (_) {}
134
+ };
135
+ // Hook SPA navigations
136
+ const _ps = cw.history.pushState;
137
+ cw.history.pushState = function() { const r = _ps.apply(this, arguments); notify(); return r; };
138
+ const _rs = cw.history.replaceState;
139
+ cw.history.replaceState = function() { const r = _rs.apply(this, arguments); notify(); return r; };
140
+ cw.addEventListener('popstate', notify);
141
+ cw.addEventListener('hashchange', notify);
142
+ if (cw.document?.readyState === 'loading') cw.document.addEventListener('DOMContentLoaded', notify, { once: true });
143
+ else notify();
144
+ } catch (err) {
145
+ // Cross-origin: fall back to saving src only
146
+ try { sessionStorage.setItem(key, pane.src); } catch (_) {}
147
+ }
148
+ }
149
+ function onPaneLoadFactory(pane, key) {
150
+ return function onPaneLoad() {
151
+ // Try to attach same-origin hooks and save current URL; if cross-origin, save src
152
+ attachSameOriginRouteHooks(pane, key);
153
+ };
154
+ }
155
+
156
+ // Overlay to avoid iframe stealing events in Chrome
157
+ function createOverlay() {
158
+ const el = document.createElement('div');
159
+ el.style.position = 'fixed';
160
+ el.style.inset = '0';
161
+ el.style.cursor = 'col-resize';
162
+ el.style.zIndex = '2147483647';
163
+ el.style.background = 'transparent';
164
+ return el;
165
+ }
166
+
167
+ let startX = 0;
168
+ let startLeft = 0;
169
+ let total = 0;
170
+ let overlay = null;
171
+
172
+ function refreshLayout (splitKey) {
173
+ let val = sessionStorage.getItem(splitKey)
174
+ let id = splitKey.replace("splitRatio:", "")
175
+ if (val === "1" || val === "0") {
176
+ if (val === "1") {
177
+ id_to_hide = id + ".1"
178
+ } else if (val === "0") {
179
+ id_to_hide = id + ".0"
180
+ }
181
+ const el = document.querySelector(`iframe[name='${id_to_hide}']`)
182
+ el.remove()
183
+ document.body.className = "single"
184
+ document.querySelector("#gutter").remove()
185
+ }
186
+ }
187
+
188
+ function onPointerMove(e) {
189
+ const delta = e.clientX - startX;
190
+ let newLeft = startLeft + delta;
191
+ newLeft = Math.max(MIN, Math.min(total - MIN, newLeft));
192
+ setColumns(newLeft);
193
+ updateAria(newLeft, total);
194
+ }
195
+
196
+ function endDrag(e) {
197
+ if (gutter && e && e.pointerId != null && gutter.hasPointerCapture?.(e.pointerId)) {
198
+ try { gutter.releasePointerCapture(e.pointerId); } catch (_) {}
199
+ }
200
+ window.removeEventListener('pointermove', onPointerMove, true);
201
+ window.removeEventListener('pointerup', endDrag, true);
202
+ window.removeEventListener('pointercancel', endDrag, true);
203
+ if (overlay) {
204
+ overlay.remove();
205
+ overlay = null;
206
+ }
207
+ body.classList.remove('resizing');
208
+ // Persist position as ratio
209
+ try {
210
+ const leftRect = left.getBoundingClientRect();
211
+ saveRatioFromLeftPx(leftRect.width);
212
+ } catch (_) {}
213
+ }
214
+
215
+ // Restore child pane URLs before any interaction (override hardcoded src)
216
+ restorePaneURL(left, urlKeyFor(left.name));
217
+ restorePaneURL(right, urlKeyFor(right.name));
218
+
219
+ // Track navigations for each pane
220
+ left.addEventListener('load', onPaneLoadFactory(left, urlKeyFor(left.name)));
221
+ right.addEventListener('load', onPaneLoadFactory(right, urlKeyFor(right.name)));
222
+
223
+ if (gutter) {
224
+ gutter.addEventListener('pointerdown', (e) => {
225
+ e.preventDefault();
226
+ const bodyRect = body.getBoundingClientRect();
227
+ const leftRect = left.getBoundingClientRect();
228
+ total = bodyRect.width - GUTTER; // total available for both iframes
229
+ startLeft = leftRect.width;
230
+ startX = e.clientX;
231
+
232
+ // Ensure we keep receiving events even over iframes
233
+ try { gutter.setPointerCapture(e.pointerId); } catch (_) {}
234
+ overlay = createOverlay();
235
+ document.body.appendChild(overlay);
236
+
237
+ window.addEventListener('pointermove', onPointerMove, true);
238
+ window.addEventListener('pointerup', endDrag, true);
239
+ window.addEventListener('pointercancel', endDrag, true);
240
+ body.classList.add('resizing');
241
+ });
242
+
243
+ // Keyboard resizing for accessibility
244
+ gutter.addEventListener('keydown', (e) => {
245
+ const { key } = e;
246
+ if (key !== 'ArrowLeft' && key !== 'ArrowRight' && key !== 'Home' && key !== 'End') return;
247
+ e.preventDefault();
248
+ const totalNow = computeTotal();
249
+ const rect = left.getBoundingClientRect();
250
+ const step = e.shiftKey ? KEY_STEP_BIG : KEY_STEP;
251
+ let leftPx = rect.width;
252
+ if (key === 'ArrowLeft') leftPx -= step;
253
+ else if (key === 'ArrowRight') leftPx += step;
254
+ else if (key === 'Home') leftPx = MIN;
255
+ else if (key === 'End') leftPx = totalNow - MIN;
256
+ leftPx = clamp(leftPx, MIN, totalNow - MIN);
257
+ setColumns(leftPx);
258
+ updateAria(leftPx, totalNow);
259
+ saveRatioFromLeftPx(leftPx);
260
+ });
261
+ }
262
+
263
+ // Initialize from saved ratio if available and set ARIA
264
+ try {
265
+ const saved = parseFloat(sessionStorage.getItem(splitKey) || '');
266
+ console.log({ saved })
267
+ if (!Number.isNaN(saved) && saved > 0 && saved < 1) {
268
+ console.log("> 1")
269
+ applyFromRatio(saved);
270
+ } else {
271
+ console.log("> 2")
272
+ updateAria(left.getBoundingClientRect().width, computeTotal());
273
+ refreshLayout(splitKey)
274
+ }
275
+ } catch (_) {
276
+ console.log("> 3")
277
+ updateAria(left.getBoundingClientRect().width, computeTotal());
278
+ }
279
+
280
+ // Re-apply on window resize to keep ratio
281
+ window.addEventListener('resize', () => {
282
+ try {
283
+ const saved = parseFloat(sessionStorage.getItem(splitKey) || '');
284
+ if (!Number.isNaN(saved) && saved > 0 && saved < 1) {
285
+ applyFromRatio(saved);
286
+ } else {
287
+ updateAria(left.getBoundingClientRect().width, computeTotal());
288
+ }
289
+ } catch (_) {
290
+ updateAria(left.getBoundingClientRect().width, computeTotal());
291
+ }
292
+ });
293
+ window.addEventListener('message', (event) => {
294
+ if (event.data && event.data.e === "close") {
295
+ // find the frame
296
+ // Find which iframe sent the message
297
+ const col0 = document.getElementById('col0');
298
+ const col1 = document.getElementById('col1');
299
+
300
+ let sourceFrameId = null;
301
+
302
+ if (event.source === col0.contentWindow) {
303
+ sourceFrameId = 'col0';
304
+ } else if (event.source === col1.contentWindow) {
305
+ sourceFrameId = 'col1';
306
+ }
307
+
308
+ console.log('Message received from iframe:', sourceFrameId);
309
+
310
+ // Or use this approach to loop through all iframes
311
+ const iframes = document.querySelectorAll('iframe');
312
+ console.log({ splitKey })
313
+ for (let iframe of iframes) {
314
+ if (event.source === iframe.contentWindow) {
315
+ // const splitKey = `splitRatio:${iframe.name}`
316
+ // console.log({ splitKey })
317
+ if (iframe.id === "col0") {
318
+ // hide col0 => ratio: 0
319
+ // col0.src = "about:blank"
320
+ // col0.style.display = "none"
321
+ try { sessionStorage.setItem(splitKey, "0"); } catch (_) {}
322
+ refreshLayout(splitKey)
323
+ } else if (iframe.id === "col1") {
324
+ // hide col1 => ratio: 1
325
+ // col1.src = "about:blank"
326
+ // col1.style.display = "none"
327
+ try { sessionStorage.setItem(splitKey, "1"); } catch (_) { console.log("<<< ", _ )}
328
+ refreshLayout(splitKey)
329
+ }
330
+ break;
331
+ }
332
+ }
333
+ }
334
+ })
335
+ })();
336
+ </script>
337
+ </body>
338
+ </html>
@@ -0,0 +1,353 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Minimal Hugging Face OAuth</title>
7
+ <style>
8
+ body {
9
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
10
+ max-width: 600px;
11
+ margin: 50px auto;
12
+ padding: 20px;
13
+ background: #f8fafc;
14
+ }
15
+ .container {
16
+ background: white;
17
+ padding: 30px;
18
+ border-radius: 10px;
19
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
20
+ }
21
+ h1 {
22
+ color: #1f2937;
23
+ margin-bottom: 20px;
24
+ }
25
+ .btn {
26
+ background: #ff6b35;
27
+ color: white;
28
+ border: none;
29
+ padding: 12px 24px;
30
+ border-radius: 6px;
31
+ cursor: pointer;
32
+ font-size: 16px;
33
+ margin: 10px 5px 10px 0;
34
+ }
35
+ .btn:hover {
36
+ background: #e55a2b;
37
+ }
38
+ .btn.secondary {
39
+ background: #6b7280;
40
+ }
41
+ .btn.secondary:hover {
42
+ background: #4b5563;
43
+ }
44
+ .user-info {
45
+ background: #f0f9ff;
46
+ padding: 15px;
47
+ border-radius: 6px;
48
+ border-left: 4px solid #3b82f6;
49
+ margin-top: 20px;
50
+ }
51
+ .status {
52
+ padding: 12px;
53
+ margin: 10px 0;
54
+ border-radius: 6px;
55
+ font-weight: 500;
56
+ }
57
+ .status.success { background: #d1fae5; color: #065f46; }
58
+ .status.error { background: #fee2e2; color: #991b1b; }
59
+ .status.warning { background: #fef3c7; color: #92400e; }
60
+ .hidden { display: none; }
61
+ </style>
62
+ </head>
63
+ <body>
64
+ <div class="container">
65
+ <h1>🤗 Hugging Face OAuth</h1>
66
+
67
+ <div id="status" class="status warning">
68
+ Checking authentication status...
69
+ </div>
70
+
71
+ <!-- Login Section -->
72
+ <div id="login-section">
73
+ <p>Click below to authenticate with Hugging Face:</p>
74
+ <button class="btn" onclick="login()">Login with Hugging Face</button>
75
+ </div>
76
+
77
+ <!-- User Section -->
78
+ <div id="user-section" class="hidden">
79
+ <div class="user-info">
80
+ <h3>Welcome!</h3>
81
+ <div id="user-details"></div>
82
+ <button class="btn secondary" onclick="logout()">Logout</button>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <script>
88
+ // Configuration
89
+ const CLIENT_ID = 'e90d4a4d-68a6-4c12-ae71-64756b5918de';
90
+ const REDIRECT_URI = 'https://pinokio.localhost/connect/huggingface';
91
+ const HF_OAUTH_URL = 'https://huggingface.co/oauth/authorize';
92
+ const HF_TOKEN_URL = 'https://huggingface.co/oauth/token';
93
+ const HF_API_URL = 'https://huggingface.co/api/whoami-v2';
94
+
95
+ // Utility functions
96
+ function setStatus(message, type) {
97
+ const status = document.getElementById('status');
98
+ status.textContent = message;
99
+ status.className = `status ${type}`;
100
+ }
101
+
102
+ function generateRandomString(length) {
103
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
104
+ let result = '';
105
+ for (let i = 0; i < length; i++) {
106
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
107
+ }
108
+ return result;
109
+ }
110
+
111
+ // PKCE functions
112
+ function generateCodeVerifier() {
113
+ const array = new Uint8Array(32);
114
+ crypto.getRandomValues(array);
115
+ return base64URLEncode(array);
116
+ }
117
+
118
+ async function generateCodeChallenge(verifier) {
119
+ const encoder = new TextEncoder();
120
+ const data = encoder.encode(verifier);
121
+ const digest = await crypto.subtle.digest('SHA-256', data);
122
+ return base64URLEncode(new Uint8Array(digest));
123
+ }
124
+
125
+ function base64URLEncode(array) {
126
+ return btoa(String.fromCharCode.apply(null, array))
127
+ .replace(/\+/g, '-')
128
+ .replace(/\//g, '_')
129
+ .replace(/=/g, '');
130
+ }
131
+
132
+ // Token management with automatic refresh
133
+ async function ensureValidToken() {
134
+ const res = await fetch('/connect/huggingface/keys', {
135
+ method: 'POST',
136
+ headers: { 'Content-Type': 'application/json' },
137
+ body: JSON.stringify({})
138
+ });
139
+ const json = await res.json();
140
+ console.log({ json })
141
+ if (json.error) {
142
+ return null
143
+ } else if (json.access_token) {
144
+ return json.access_token
145
+ } else {
146
+ return null
147
+ }
148
+ }
149
+
150
+ // OAuth functions
151
+ async function login() {
152
+ try {
153
+ // Clear existing data
154
+ localStorage.removeItem('oauth_state');
155
+ localStorage.removeItem('code_verifier');
156
+
157
+ // Generate PKCE parameters
158
+ const state = generateRandomString(32);
159
+ const codeVerifier = generateCodeVerifier();
160
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
161
+
162
+ // Store for later
163
+ localStorage.setItem('oauth_state', state);
164
+ localStorage.setItem('code_verifier', codeVerifier);
165
+
166
+ // Build auth URL
167
+ const params = new URLSearchParams({
168
+ client_id: CLIENT_ID,
169
+ redirect_uri: REDIRECT_URI,
170
+ response_type: 'code',
171
+ scope: 'openid profile email read-repos write-repos manage-repos',
172
+ state: state,
173
+ code_challenge: codeChallenge,
174
+ code_challenge_method: 'S256'
175
+ });
176
+
177
+ const authUrl = HF_OAUTH_URL + '?' + params.toString();
178
+
179
+ // Redirect
180
+ window.location.href = authUrl;
181
+
182
+ } catch (error) {
183
+ console.error('Login error:', error);
184
+ setStatus('Failed to start login', 'error');
185
+ }
186
+ }
187
+
188
+ async function handleOAuthCallback(code) {
189
+ try {
190
+ // Verify state
191
+ const urlParams = new URLSearchParams(window.location.search);
192
+ const returnedState = urlParams.get('state');
193
+ const storedState = localStorage.getItem('oauth_state');
194
+
195
+ if (returnedState !== storedState) {
196
+ throw new Error('Invalid state parameter');
197
+ }
198
+
199
+ // Get code verifier
200
+ const codeVerifier = localStorage.getItem('code_verifier');
201
+ if (!codeVerifier) {
202
+ throw new Error('Code verifier not found');
203
+ }
204
+
205
+ // Exchange code for tokens
206
+ const response = await fetch(HF_TOKEN_URL, {
207
+ method: 'POST',
208
+ headers: {
209
+ 'Content-Type': 'application/x-www-form-urlencoded',
210
+ },
211
+ body: new URLSearchParams({
212
+ client_id: CLIENT_ID,
213
+ code: code,
214
+ redirect_uri: REDIRECT_URI,
215
+ grant_type: 'authorization_code',
216
+ code_verifier: codeVerifier
217
+ })
218
+ });
219
+
220
+ if (!response.ok) {
221
+ const errorText = await response.text();
222
+ throw new Error(`Token exchange failed: ${response.status} - ${errorText}`);
223
+ }
224
+
225
+ const tokenData = await response.json();
226
+ console.log('Token response:', tokenData);
227
+
228
+ // Store tokens
229
+ if (tokenData.access_token) {
230
+
231
+ const res = await fetch('/connect/huggingface/login', {
232
+ method: 'POST',
233
+ headers: { 'Content-Type': 'application/json' },
234
+ body: JSON.stringify(tokenData)
235
+ });
236
+ const response = await res.json();
237
+ // Cleanup
238
+ localStorage.removeItem('oauth_state');
239
+ localStorage.removeItem('code_verifier');
240
+ window.history.replaceState({}, document.title, window.location.pathname);
241
+
242
+ // Get user info
243
+ await fetchUserInfo();
244
+ } else {
245
+ throw new Error('No access token received');
246
+ }
247
+
248
+ } catch (error) {
249
+ console.error('OAuth callback error:', error);
250
+ setStatus('Authentication failed: ' + error.message, 'error');
251
+ }
252
+ }
253
+
254
+ async function fetchUserInfo() {
255
+ try {
256
+ // Use ensureValidToken to automatically refresh if needed
257
+ const token = await ensureValidToken();
258
+ if (!token) {
259
+ throw new Error('No valid token available');
260
+ }
261
+
262
+ const response = await fetch(HF_API_URL, {
263
+ headers: { 'Authorization': 'Bearer ' + token }
264
+ });
265
+
266
+ if (!response.ok) {
267
+ throw new Error(`API call failed: ${response.status}`);
268
+ }
269
+
270
+ const userInfo = await response.json();
271
+ displayUserInfo(userInfo);
272
+
273
+ } catch (error) {
274
+ console.error('Error fetching user info:', error);
275
+ setStatus('Failed to fetch user info: ' + error.message, 'error');
276
+ logout();
277
+ }
278
+ }
279
+
280
+ function displayUserInfo(userInfo) {
281
+ const userDetails = document.getElementById('user-details');
282
+ userDetails.innerHTML = `
283
+ <p><strong>Username:</strong> ${userInfo.name || 'N/A'}</p>
284
+ <p><strong>Full Name:</strong> ${userInfo.fullname || 'N/A'}</p>
285
+ <p><strong>Email:</strong> ${userInfo.email || 'N/A'}</p>
286
+ <p><strong>Avatar:</strong> <img src="${userInfo.avatarUrl || ''}" alt="Avatar" style="width: 40px; height: 40px; border-radius: 50%; vertical-align: middle;"></p>
287
+ `;
288
+
289
+ document.getElementById('login-section').className = 'hidden';
290
+ document.getElementById('user-section').className = '';
291
+ setStatus(`Logged in as ${userInfo.name}`, 'success');
292
+ }
293
+
294
+ async function logout() {
295
+ document.getElementById('login-section').className = '';
296
+ document.getElementById('user-section').className = 'hidden';
297
+ setStatus('Logged out', 'warning');
298
+ const res = await fetch('/connect/huggingface/logout', {
299
+ method: 'POST',
300
+ headers: { 'Content-Type': 'application/json' },
301
+ body: JSON.stringify({})
302
+ });
303
+ const json = await res.json();
304
+ location.href = location.href
305
+ }
306
+
307
+ // Utility function for making authenticated API calls with automatic refresh
308
+ async function makeAuthenticatedRequest(url, options = {}) {
309
+ const token = await ensureValidToken();
310
+ if (!token) {
311
+ throw new Error('No valid token available');
312
+ }
313
+
314
+ return fetch(url, {
315
+ ...options,
316
+ headers: {
317
+ ...options.headers,
318
+ 'Authorization': 'Bearer ' + token
319
+ }
320
+ });
321
+ }
322
+
323
+ // Initialize on page load
324
+ window.addEventListener('load', async () => {
325
+ const urlParams = new URLSearchParams(window.location.search);
326
+ const code = urlParams.get('code');
327
+ const error = urlParams.get('error');
328
+
329
+ if (error) {
330
+ setStatus('OAuth error: ' + error, 'error');
331
+ return;
332
+ }
333
+
334
+ if (code) {
335
+ await handleOAuthCallback(code);
336
+ return;
337
+ }
338
+
339
+ // Check existing session with automatic refresh
340
+ const token = await ensureValidToken();
341
+ if (token) {
342
+ await fetchUserInfo();
343
+ } else {
344
+ setStatus('Not authenticated', 'warning');
345
+ }
346
+ });
347
+
348
+ // Export makeAuthenticatedRequest for external use
349
+ window.makeAuthenticatedRequest = makeAuthenticatedRequest;
350
+ window.ensureValidToken = ensureValidToken;
351
+ </script>
352
+ </body>
353
+ </html>