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/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 buildTokenStorageKey(serverId) {
53
- return `tabminal_auth_token:${serverId}`;
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 msgBuffer = new TextEncoder().encode(password);
75
- const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
76
- const hashArray = Array.from(new Uint8Array(hashBuffer));
77
- return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
78
- } catch (e) {
79
- console.warn('Web Crypto API failed, falling back to JS implementation', e);
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(password);
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, j;
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, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
99
- 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
100
- ].slice(0);
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, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
103
- 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
104
- 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
105
- 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
106
- 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
107
- 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
108
- 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
109
- 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
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]] = ((asciiBitLength / maxWord) | 0);
120
- words[words[lengthProperty]] = (asciiBitLength);
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], w2 = w[i - 2];
129
- const a = hash[0], e = hash[4];
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
- const temp2 = (rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22))
142
- + ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2]));
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;