tabminal 3.0.26 → 3.0.28
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/AGENTS.md +13 -7
- package/package.json +1 -1
- package/public/app.js +682 -123
- package/public/index.html +28 -0
- package/public/modules/url-auth.js +57 -35
- package/public/styles.css +113 -0
- package/src/auth.mjs +471 -37
- package/src/persistence.mjs +75 -0
- package/src/server.mjs +99 -1
package/public/index.html
CHANGED
|
@@ -489,6 +489,34 @@
|
|
|
489
489
|
<p id="add-server-error" class="error-message"></p>
|
|
490
490
|
</div>
|
|
491
491
|
</div>
|
|
492
|
+
<div id="auth-sessions-modal" class="modal" style="display: none;">
|
|
493
|
+
<div class="modal-content auth-sessions-content">
|
|
494
|
+
<h2 id="auth-sessions-title">Login sessions</h2>
|
|
495
|
+
<p id="auth-sessions-description"></p>
|
|
496
|
+
<div
|
|
497
|
+
id="auth-sessions-list"
|
|
498
|
+
class="auth-sessions-list"
|
|
499
|
+
aria-live="polite"
|
|
500
|
+
></div>
|
|
501
|
+
<p id="auth-sessions-error" class="error-message"></p>
|
|
502
|
+
<div class="auth-sessions-actions">
|
|
503
|
+
<button
|
|
504
|
+
id="auth-sessions-revoke-others"
|
|
505
|
+
class="danger-button"
|
|
506
|
+
type="button"
|
|
507
|
+
>
|
|
508
|
+
Log out others
|
|
509
|
+
</button>
|
|
510
|
+
<button
|
|
511
|
+
id="auth-sessions-close"
|
|
512
|
+
class="secondary-button"
|
|
513
|
+
type="button"
|
|
514
|
+
>
|
|
515
|
+
Close
|
|
516
|
+
</button>
|
|
517
|
+
</div>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
492
520
|
<div id="agent-setup-modal" class="modal" style="display: none;">
|
|
493
521
|
<div class="modal-content agent-setup-content">
|
|
494
522
|
<h2 id="agent-setup-title">Agent setup</h2>
|
|
@@ -49,8 +49,8 @@ export function isLikelyAccessLoginResponse(response) {
|
|
|
49
49
|
return false;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
export function
|
|
53
|
-
return `
|
|
52
|
+
export function buildAuthStateStorageKey(serverId) {
|
|
53
|
+
return `tabminal_auth_state:${serverId}`;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export function makeSessionKey(serverId, sessionId) {
|
|
@@ -69,17 +69,20 @@ export function splitSessionKey(sessionKey) {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export async function hashPassword(password) {
|
|
72
|
+
const normalized = String(password || '');
|
|
72
73
|
if (window.crypto && window.crypto.subtle) {
|
|
73
74
|
try {
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
return
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
const message = new TextEncoder().encode(normalized);
|
|
76
|
+
const buffer = await window.crypto.subtle.digest('SHA-256', message);
|
|
77
|
+
const bytes = Array.from(new Uint8Array(buffer));
|
|
78
|
+
return bytes.map((byte) => {
|
|
79
|
+
return byte.toString(16).padStart(2, '0');
|
|
80
|
+
}).join('');
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.warn('Web Crypto API failed, falling back to JS SHA-256', error);
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
|
-
return sha256Fallback(
|
|
85
|
+
return sha256Fallback(normalized);
|
|
83
86
|
}
|
|
84
87
|
|
|
85
88
|
function sha256Fallback(ascii) {
|
|
@@ -90,43 +93,60 @@ function sha256Fallback(ascii) {
|
|
|
90
93
|
const mathPow = Math.pow;
|
|
91
94
|
const maxWord = mathPow(2, 32);
|
|
92
95
|
const lengthProperty = 'length';
|
|
93
|
-
let i
|
|
96
|
+
let i;
|
|
97
|
+
let j;
|
|
94
98
|
let result = '';
|
|
95
99
|
const words = [];
|
|
96
100
|
const asciiBitLength = ascii[lengthProperty] * 8;
|
|
97
101
|
let hash = [
|
|
98
|
-
0x6a09e667,
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
0x6a09e667,
|
|
103
|
+
0xbb67ae85,
|
|
104
|
+
0x3c6ef372,
|
|
105
|
+
0xa54ff53a,
|
|
106
|
+
0x510e527f,
|
|
107
|
+
0x9b05688c,
|
|
108
|
+
0x1f83d9ab,
|
|
109
|
+
0x5be0cd19
|
|
110
|
+
];
|
|
101
111
|
const k = [
|
|
102
|
-
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
|
|
113
|
+
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
114
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
115
|
+
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
116
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
|
|
117
|
+
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
118
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
|
119
|
+
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
120
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
121
|
+
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
122
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
|
123
|
+
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
124
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
|
|
125
|
+
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
126
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
127
|
+
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
|
110
128
|
];
|
|
111
129
|
|
|
112
130
|
ascii += '\x80';
|
|
113
131
|
while (ascii[lengthProperty] % 64 - 56) ascii += '\x00';
|
|
114
132
|
|
|
115
|
-
for (i = 0; i < ascii[lengthProperty]; i
|
|
133
|
+
for (i = 0; i < ascii[lengthProperty]; i += 1) {
|
|
116
134
|
j = ascii.charCodeAt(i);
|
|
117
135
|
words[i >> 2] |= j << ((3 - i) % 4) * 8;
|
|
118
136
|
}
|
|
119
|
-
words[words[lengthProperty]] = (
|
|
120
|
-
words[words[lengthProperty]] =
|
|
137
|
+
words[words[lengthProperty]] = (asciiBitLength / maxWord) | 0;
|
|
138
|
+
words[words[lengthProperty]] = asciiBitLength;
|
|
121
139
|
|
|
122
140
|
for (j = 0; j < words[lengthProperty];) {
|
|
123
141
|
const w = words.slice(j, j += 16);
|
|
124
142
|
const oldHash = hash;
|
|
125
143
|
hash = hash.slice(0, 8);
|
|
126
144
|
|
|
127
|
-
for (i = 0; i < 64; i
|
|
128
|
-
const w15 = w[i - 15]
|
|
129
|
-
const
|
|
145
|
+
for (i = 0; i < 64; i += 1) {
|
|
146
|
+
const w15 = w[i - 15];
|
|
147
|
+
const w2 = w[i - 2];
|
|
148
|
+
const a = hash[0];
|
|
149
|
+
const e = hash[4];
|
|
130
150
|
const temp1 = hash[7]
|
|
131
151
|
+ (rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25))
|
|
132
152
|
+ ((e & hash[5]) ^ ((~e) & hash[6]))
|
|
@@ -136,23 +156,25 @@ function sha256Fallback(ascii) {
|
|
|
136
156
|
+ (rightRotate(w15, 7) ^ rightRotate(w15, 18) ^ (w15 >>> 3))
|
|
137
157
|
+ w[i - 7]
|
|
138
158
|
+ (rightRotate(w2, 17) ^ rightRotate(w2, 19) ^ (w2 >>> 10))
|
|
139
|
-
) | 0
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
159
|
+
) | 0);
|
|
160
|
+
const temp2 = (
|
|
161
|
+
rightRotate(a, 2)
|
|
162
|
+
^ rightRotate(a, 13)
|
|
163
|
+
^ rightRotate(a, 22)
|
|
164
|
+
) + ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2]));
|
|
143
165
|
hash = [(temp1 + temp2) | 0].concat(hash);
|
|
144
166
|
hash[4] = (hash[4] + temp1) | 0;
|
|
145
167
|
}
|
|
146
168
|
|
|
147
|
-
for (i = 0; i < 8; i
|
|
169
|
+
for (i = 0; i < 8; i += 1) {
|
|
148
170
|
hash[i] = (hash[i] + oldHash[i]) | 0;
|
|
149
171
|
}
|
|
150
172
|
}
|
|
151
173
|
|
|
152
|
-
for (i = 0; i < 8; i
|
|
153
|
-
for (j = 3; j + 1; j
|
|
174
|
+
for (i = 0; i < 8; i += 1) {
|
|
175
|
+
for (j = 3; j + 1; j -= 1) {
|
|
154
176
|
const b = (hash[i] >> (j * 8)) & 255;
|
|
155
|
-
result += ((b < 16) ? 0 : '') + b.toString(16);
|
|
177
|
+
result += ((b < 16) ? '0' : '') + b.toString(16);
|
|
156
178
|
}
|
|
157
179
|
}
|
|
158
180
|
return result;
|
package/public/styles.css
CHANGED
|
@@ -481,6 +481,10 @@ body {
|
|
|
481
481
|
transition: background-color 0.2s ease, border-color 0.2s ease;
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
+
.server-row.has-auth-sessions .server-main-button {
|
|
485
|
+
padding-right: 42px;
|
|
486
|
+
}
|
|
487
|
+
|
|
484
488
|
.server-main-button:hover {
|
|
485
489
|
background-color: var(--bg-surface);
|
|
486
490
|
}
|
|
@@ -538,6 +542,50 @@ body {
|
|
|
538
542
|
transform: translateY(1px);
|
|
539
543
|
}
|
|
540
544
|
|
|
545
|
+
.server-auth-button {
|
|
546
|
+
position: absolute;
|
|
547
|
+
top: 4px;
|
|
548
|
+
right: 4px;
|
|
549
|
+
width: 28px;
|
|
550
|
+
height: 28px;
|
|
551
|
+
border-radius: 4px;
|
|
552
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
553
|
+
background-color: rgba(0, 0, 0, 0.6);
|
|
554
|
+
backdrop-filter: blur(4px);
|
|
555
|
+
-webkit-backdrop-filter: blur(4px);
|
|
556
|
+
color: var(--text-highlight);
|
|
557
|
+
display: flex;
|
|
558
|
+
align-items: center;
|
|
559
|
+
justify-content: center;
|
|
560
|
+
padding: 0;
|
|
561
|
+
opacity: 0.72;
|
|
562
|
+
z-index: 10;
|
|
563
|
+
cursor: pointer;
|
|
564
|
+
transition: all 0.2s ease;
|
|
565
|
+
font-size: 14px;
|
|
566
|
+
line-height: 1;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.server-auth-button svg {
|
|
570
|
+
width: 15px;
|
|
571
|
+
height: 15px;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.server-row:hover .server-auth-button,
|
|
575
|
+
.server-row:focus-within .server-auth-button {
|
|
576
|
+
opacity: 1;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.server-auth-button:hover {
|
|
580
|
+
background-color: rgba(0, 0, 0, 0.8);
|
|
581
|
+
border-color: rgba(255, 255, 255, 0.4);
|
|
582
|
+
color: var(--text-highlight);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.server-auth-button:active {
|
|
586
|
+
transform: translateY(1px);
|
|
587
|
+
}
|
|
588
|
+
|
|
541
589
|
.server-action-text {
|
|
542
590
|
display: block;
|
|
543
591
|
color: var(--text-muted);
|
|
@@ -1203,6 +1251,71 @@ body {
|
|
|
1203
1251
|
min-width: 120px;
|
|
1204
1252
|
}
|
|
1205
1253
|
|
|
1254
|
+
.auth-sessions-content {
|
|
1255
|
+
max-width: 620px;
|
|
1256
|
+
text-align: left;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
.auth-sessions-list {
|
|
1260
|
+
display: flex;
|
|
1261
|
+
flex-direction: column;
|
|
1262
|
+
gap: 0.75rem;
|
|
1263
|
+
max-height: min(50vh, 420px);
|
|
1264
|
+
overflow-y: auto;
|
|
1265
|
+
margin: 1rem 0;
|
|
1266
|
+
padding-right: 0.25rem;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
.auth-session-row {
|
|
1270
|
+
display: flex;
|
|
1271
|
+
align-items: center;
|
|
1272
|
+
justify-content: space-between;
|
|
1273
|
+
gap: 1rem;
|
|
1274
|
+
padding: 0.8rem;
|
|
1275
|
+
border: 1px solid var(--border-color);
|
|
1276
|
+
border-radius: 8px;
|
|
1277
|
+
background-color: rgba(255, 255, 255, 0.03);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
.auth-session-row.current {
|
|
1281
|
+
border-color: var(--accent-color);
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
.auth-session-info {
|
|
1285
|
+
min-width: 0;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
.auth-session-title {
|
|
1289
|
+
color: var(--text-highlight);
|
|
1290
|
+
font-weight: 700;
|
|
1291
|
+
margin-bottom: 0.35rem;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
.auth-session-meta,
|
|
1295
|
+
.auth-session-empty {
|
|
1296
|
+
color: var(--text-dim);
|
|
1297
|
+
font-size: 0.82rem;
|
|
1298
|
+
line-height: 1.45;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
.auth-session-revoke {
|
|
1302
|
+
width: auto !important;
|
|
1303
|
+
min-width: 92px;
|
|
1304
|
+
flex: 0 0 auto;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
.auth-sessions-actions {
|
|
1308
|
+
display: flex;
|
|
1309
|
+
justify-content: flex-end;
|
|
1310
|
+
gap: 0.75rem;
|
|
1311
|
+
margin-top: 1rem;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
.auth-sessions-actions button {
|
|
1315
|
+
width: auto;
|
|
1316
|
+
min-width: 130px;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1206
1319
|
.agent-setup-content {
|
|
1207
1320
|
max-width: 520px;
|
|
1208
1321
|
text-align: left;
|