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.
- package/kernel/api/browser/index.js +3 -1
- package/kernel/api/cloudflare/index.js +3 -3
- package/kernel/api/index.js +187 -51
- package/kernel/api/loading/index.js +15 -0
- package/kernel/api/process/index.js +7 -0
- package/kernel/api/shell/index.js +0 -2
- package/kernel/bin/browserless.js +22 -0
- package/kernel/bin/caddy.js +36 -4
- package/kernel/bin/index.js +4 -1
- package/kernel/bin/setup.js +38 -5
- package/kernel/connect/backend.js +110 -0
- package/kernel/connect/config.js +171 -0
- package/kernel/connect/index.js +18 -7
- package/kernel/connect/providers/huggingface/index.js +98 -0
- package/kernel/connect/providers/x/index.js +0 -1
- package/kernel/environment.js +91 -19
- package/kernel/git.js +46 -3
- package/kernel/index.js +119 -39
- package/kernel/peer.js +40 -5
- package/kernel/plugin.js +3 -2
- package/kernel/procs.js +27 -20
- package/kernel/prototype.js +30 -16
- package/kernel/router/common.js +1 -1
- package/kernel/router/connector.js +1 -3
- package/kernel/router/index.js +38 -4
- package/kernel/router/localhost_home_router.js +5 -1
- package/kernel/router/localhost_port_router.js +27 -1
- package/kernel/router/localhost_static_router.js +93 -0
- package/kernel/router/localhost_variable_router.js +14 -9
- package/kernel/router/peer_peer_router.js +3 -0
- package/kernel/router/peer_static_router.js +43 -0
- package/kernel/router/peer_variable_router.js +15 -14
- package/kernel/router/processor.js +26 -1
- package/kernel/router/rewriter.js +59 -0
- package/kernel/scripts/git/commit +11 -1
- package/kernel/shell.js +8 -3
- package/kernel/util.js +65 -6
- package/package.json +2 -1
- package/server/index.js +1037 -964
- package/server/public/common.js +382 -1
- package/server/public/fscreator.js +0 -1
- package/server/public/loading.js +17 -0
- package/server/public/notifyinput.js +0 -1
- package/server/public/opener.js +4 -2
- package/server/public/style.css +311 -11
- package/server/socket.js +7 -1
- package/server/views/app.ejs +1747 -351
- package/server/views/columns.ejs +338 -0
- package/server/views/connect/huggingface.ejs +353 -0
- package/server/views/connect/index.ejs +410 -0
- package/server/views/connect/x.ejs +43 -9
- package/server/views/connect.ejs +709 -49
- package/server/views/container.ejs +357 -0
- package/server/views/d.ejs +251 -62
- package/server/views/download.ejs +54 -10
- package/server/views/editor.ejs +11 -0
- package/server/views/explore.ejs +40 -15
- package/server/views/file_explorer.ejs +25 -246
- package/server/views/form.ejs +44 -1
- package/server/views/frame.ejs +39 -1
- package/server/views/github.ejs +48 -11
- package/server/views/help.ejs +48 -7
- package/server/views/index.ejs +119 -58
- package/server/views/index2.ejs +3 -4
- package/server/views/init/index.ejs +651 -197
- package/server/views/install.ejs +1 -1
- package/server/views/mini.ejs +47 -18
- package/server/views/net.ejs +199 -67
- package/server/views/network.ejs +220 -94
- package/server/views/network2.ejs +3 -4
- package/server/views/old_network.ejs +3 -3
- package/server/views/prototype/index.ejs +48 -11
- package/server/views/review.ejs +1005 -0
- package/server/views/rows.ejs +341 -0
- package/server/views/screenshots.ejs +1020 -0
- package/server/views/settings.ejs +160 -23
- package/server/views/setup.ejs +49 -7
- package/server/views/setup_home.ejs +43 -10
- package/server/views/shell.ejs +7 -1
- package/server/views/start.ejs +14 -9
- package/server/views/terminal.ejs +13 -2
- 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>
|