@juspay/shooter 1.19.0 → 1.20.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/build/client/_app/immutable/assets/11.F10lvwyh.css +1 -0
- package/build/client/_app/immutable/assets/11.F10lvwyh.css.br +0 -0
- package/build/client/_app/immutable/assets/11.F10lvwyh.css.gz +0 -0
- package/build/client/_app/immutable/chunks/C_YNQL8b.js +3 -0
- package/build/client/_app/immutable/chunks/C_YNQL8b.js.br +0 -0
- package/build/client/_app/immutable/chunks/C_YNQL8b.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DA4Zt9Me.js → DIZ3Qst5.js} +1 -1
- package/build/client/_app/immutable/chunks/DIZ3Qst5.js.br +0 -0
- package/build/client/_app/immutable/chunks/{DA4Zt9Me.js.gz → DIZ3Qst5.js.gz} +0 -0
- package/build/client/_app/immutable/chunks/{DCDL_9ys.js → DT4H19pV.js} +1 -1
- package/build/client/_app/immutable/chunks/DT4H19pV.js.br +0 -0
- package/build/client/_app/immutable/chunks/DT4H19pV.js.gz +0 -0
- package/build/client/_app/immutable/chunks/J5-Cr5oR.js +6 -0
- package/build/client/_app/immutable/chunks/J5-Cr5oR.js.br +0 -0
- package/build/client/_app/immutable/chunks/J5-Cr5oR.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.D4TXlu7A.js → app.Bd-DfeJi.js} +2 -2
- package/build/client/_app/immutable/entry/app.Bd-DfeJi.js.br +0 -0
- package/build/client/_app/immutable/entry/app.Bd-DfeJi.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.evvp4tX7.js +1 -0
- package/build/client/_app/immutable/entry/start.evvp4tX7.js.br +2 -0
- package/build/client/_app/immutable/entry/start.evvp4tX7.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.1zylwAPT.js → 0.Bl-1LQWM.js} +1 -1
- package/build/client/_app/immutable/nodes/0.Bl-1LQWM.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.Bl-1LQWM.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.BVnLUSs-.js → 1.DT4dq6Ay.js} +1 -1
- package/build/client/_app/immutable/nodes/1.DT4dq6Ay.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.DT4dq6Ay.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{10.D1wl2wPX.js → 10.CF7RGXpe.js} +1 -1
- package/build/client/_app/immutable/nodes/10.CF7RGXpe.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.CF7RGXpe.js.gz +0 -0
- package/build/client/_app/immutable/nodes/11.BV_G7yLI.js +2 -0
- package/build/client/_app/immutable/nodes/11.BV_G7yLI.js.br +0 -0
- package/build/client/_app/immutable/nodes/11.BV_G7yLI.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.D1Mm0DUX.js → 2.DcRhsjYp.js} +1 -1
- package/build/client/_app/immutable/nodes/2.DcRhsjYp.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.DcRhsjYp.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.Wfz3TcJd.js → 3.0MMe3oxR.js} +1 -1
- package/build/client/_app/immutable/nodes/3.0MMe3oxR.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.0MMe3oxR.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{6.DtZAEPXb.js → 6.ComiWlV6.js} +1 -1
- package/build/client/_app/immutable/nodes/6.ComiWlV6.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.ComiWlV6.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{7.MfBRh32I.js → 7.vkPx1kVP.js} +1 -1
- package/build/client/_app/immutable/nodes/7.vkPx1kVP.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.vkPx1kVP.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{8.DVE6LnOC.js → 8.Bmr3sWbS.js} +1 -1
- package/build/client/_app/immutable/nodes/8.Bmr3sWbS.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.Bmr3sWbS.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{9.BCel5OqI.js → 9.CAJucyeI.js} +1 -1
- package/build/client/_app/immutable/nodes/9.CAJucyeI.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.CAJucyeI.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-DJqyZZTr.js → 0-DDGB6CRT.js} +2 -2
- package/build/server/chunks/{0-DJqyZZTr.js.map → 0-DDGB6CRT.js.map} +1 -1
- package/build/server/chunks/{1-2YUVen1F.js → 1-DEjonQXD.js} +2 -2
- package/build/server/chunks/{1-2YUVen1F.js.map → 1-DEjonQXD.js.map} +1 -1
- package/build/server/chunks/{10-D1X7LB3v.js → 10-BK1kiiiw.js} +2 -2
- package/build/server/chunks/{10-D1X7LB3v.js.map → 10-BK1kiiiw.js.map} +1 -1
- package/build/server/chunks/{11-qXSPdF5j.js → 11-CJPjkEF3.js} +4 -4
- package/build/server/chunks/11-CJPjkEF3.js.map +1 -0
- package/build/server/chunks/{2-BD7kj1mt.js → 2-RLnhlWh5.js} +2 -2
- package/build/server/chunks/{2-BD7kj1mt.js.map → 2-RLnhlWh5.js.map} +1 -1
- package/build/server/chunks/{3-oNjv-BhZ.js → 3-Dd4pJBqZ.js} +2 -2
- package/build/server/chunks/{3-oNjv-BhZ.js.map → 3-Dd4pJBqZ.js.map} +1 -1
- package/build/server/chunks/{6-DRJGUqHG.js → 6-DdRMnKNa.js} +2 -2
- package/build/server/chunks/{6-DRJGUqHG.js.map → 6-DdRMnKNa.js.map} +1 -1
- package/build/server/chunks/{7-_giJiu0L.js → 7-vLOMMetm.js} +2 -2
- package/build/server/chunks/{7-_giJiu0L.js.map → 7-vLOMMetm.js.map} +1 -1
- package/build/server/chunks/{8-zvWAVNT5.js → 8-rJyiQLFs.js} +2 -2
- package/build/server/chunks/{8-zvWAVNT5.js.map → 8-rJyiQLFs.js.map} +1 -1
- package/build/server/chunks/{9-DVyDL445.js → 9-CVSNNYED.js} +2 -2
- package/build/server/chunks/{9-DVyDL445.js.map → 9-CVSNNYED.js.map} +1 -1
- package/build/server/chunks/Banner-BgaAs1rs.js.map +1 -1
- package/build/server/chunks/Button-D0hZ7JYt.js.map +1 -1
- package/build/server/chunks/Icon-D0GBnDcs.js.map +1 -1
- package/build/server/chunks/Input-OmIiydSx.js.map +1 -1
- package/build/server/chunks/Pill-4xJ-VhAA.js.map +1 -1
- package/build/server/chunks/Shimmer-Dw2uvTC1.js.map +1 -1
- package/build/server/chunks/_error.svelte-CZnkxeLr.js.map +1 -1
- package/build/server/chunks/_page.svelte-BLo2v_8E.js.map +1 -1
- package/build/server/chunks/_page.svelte-BTlfUsBp.js.map +1 -1
- package/build/server/chunks/_page.svelte-BX2FMgSg.js.map +1 -1
- package/build/server/chunks/_page.svelte-C7B0qdrC.js.map +1 -1
- package/build/server/chunks/_page.svelte-CE7COWnF.js.map +1 -1
- package/build/server/chunks/_page.svelte-CWsjjd4l.js.map +1 -1
- package/build/server/chunks/_page.svelte-D5S2hkBk.js.map +1 -1
- package/build/server/chunks/_page.svelte-D_Ey8QRG.js.map +1 -1
- package/build/server/chunks/{_page.svelte-BUBLUSGo.js → _page.svelte-dabsQl9c.js} +206 -5
- package/build/server/chunks/_page.svelte-dabsQl9c.js.map +1 -0
- package/build/server/chunks/_page.svelte-tBuIq8Pg.js.map +1 -1
- package/build/server/chunks/{_server.ts-C_OOUqsd.js → _server.ts-AnBXfZXh.js} +2 -2
- package/build/server/chunks/{_server.ts-C_OOUqsd.js.map → _server.ts-AnBXfZXh.js.map} +1 -1
- package/build/server/chunks/_server.ts-B-evHL2q.js +13 -0
- package/build/server/chunks/_server.ts-B-evHL2q.js.map +1 -0
- package/build/server/chunks/_server.ts-B2wIgsW4.js +95 -0
- package/build/server/chunks/_server.ts-B2wIgsW4.js.map +1 -0
- package/build/server/chunks/{_server.ts-Bi0Oe4PF.js → _server.ts-CJGyN8mw.js} +14 -9
- package/build/server/chunks/_server.ts-CJGyN8mw.js.map +1 -0
- package/build/server/chunks/{_server.ts-DhJx0DLr.js → _server.ts-DEx9-epI.js} +16 -7
- package/build/server/chunks/_server.ts-DEx9-epI.js.map +1 -0
- package/build/server/chunks/{_server.ts-DxT9IlZF.js → _server.ts-DKNIsQeH.js} +3 -3
- package/build/server/chunks/{_server.ts-DxT9IlZF.js.map → _server.ts-DKNIsQeH.js.map} +1 -1
- package/build/server/chunks/_server.ts-DpRr0Tfh.js +68 -0
- package/build/server/chunks/_server.ts-DpRr0Tfh.js.map +1 -0
- package/build/server/chunks/{_server.ts-CRVNEOd2.js → _server.ts-Dz9Jd9Jh.js} +3 -3
- package/build/server/chunks/{_server.ts-CRVNEOd2.js.map → _server.ts-Dz9Jd9Jh.js.map} +1 -1
- package/build/server/chunks/{_server.ts-Bjbr7glm.js → _server.ts-QN-Bo5ql.js} +12 -5
- package/build/server/chunks/_server.ts-QN-Bo5ql.js.map +1 -0
- package/build/server/chunks/{_server.ts-BrqaMMAa.js → _server.ts-W6i3EnGX.js} +29 -6
- package/build/server/chunks/_server.ts-W6i3EnGX.js.map +1 -0
- package/build/server/chunks/{_server.ts-DMm0hBP4.js → _server.ts-bk_EeAdY.js} +2 -2
- package/build/server/chunks/{_server.ts-DMm0hBP4.js.map → _server.ts-bk_EeAdY.js.map} +1 -1
- package/build/server/chunks/cache-BlMaDsHi.js.map +1 -1
- package/build/server/chunks/guest-registry-t0-7Zv5q.js +39 -0
- package/build/server/chunks/guest-registry-t0-7Zv5q.js.map +1 -0
- package/build/server/chunks/index-CoYB03g7.js.map +1 -1
- package/build/server/chunks/index2-dSGQ9Eaa.js.map +1 -1
- package/build/server/chunks/{pty-manager-41h3IK8K.js → pty-manager-CkZNoW1t.js} +6 -2
- package/build/server/chunks/pty-manager-CkZNoW1t.js.map +1 -0
- package/build/server/chunks/root-D4IoFC8F.js.map +1 -1
- package/build/server/chunks/share-auth-BS7JuiHf.js +27 -0
- package/build/server/chunks/share-auth-BS7JuiHf.js.map +1 -0
- package/build/server/chunks/share-store-B9jMpVg0.js +127 -0
- package/build/server/chunks/share-store-B9jMpVg0.js.map +1 -0
- package/build/server/chunks/state.svelte-CmHqngc_.js.map +1 -1
- package/build/server/chunks/stores-CRYxfF0o.js.map +1 -1
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +40 -19
- package/build/server/manifest.js.map +1 -1
- package/package.json +2 -2
- package/server.ts +8 -3
- package/src/lib/modules/client/terminal/ShareGate.svelte +96 -0
- package/src/lib/modules/client/terminal/ShareSheet.svelte +395 -0
- package/src/lib/modules/client/terminal/xterm-wrapper.ts +19 -2
- package/src/lib/modules/server/terminal/pty-manager.ts +6 -0
- package/src/lib/modules/server/terminal/share-auth.ts +37 -0
- package/src/lib/modules/server/terminal/share-store.ts +172 -0
- package/src/lib/modules/server/ws/guest-registry.ts +49 -0
- package/src/lib/modules/server/ws/server.ts +22 -3
- package/src/lib/modules/server/ws/session-handler.ts +18 -4
- package/src/lib/modules/server/ws/terminal-handler.ts +21 -2
- package/src/lib/modules/server/ws/ticket-store.ts +18 -10
- package/src/lib/types/generated/Client.ts +25 -1
- package/src/lib/types/generated/Share.ts +404 -0
- package/src/lib/types/generated/WsProtocol.ts +73 -2
- package/src/lib/types/generated/index.ts +1 -0
- package/src/lib/types/terminal-client.ts +19 -2
- package/src/lib/types/ws.ts +1 -0
- package/src/routes/api/terminals/[id]/+server.ts +14 -3
- package/src/routes/api/terminals/[id]/paste-image/+server.ts +8 -4
- package/src/routes/api/terminals/[id]/resize/+server.ts +8 -4
- package/src/routes/api/terminals/[id]/share/+server.ts +98 -0
- package/src/routes/api/terminals/[id]/share/auth/+server.ts +81 -0
- package/src/routes/api/terminals/[id]/share/status/+server.ts +11 -0
- package/src/routes/api/ws-ticket/+server.ts +26 -5
- package/src/routes/terminals/[id]/+page.svelte +184 -43
- package/build/client/_app/immutable/assets/11.v5KA95xm.css +0 -1
- package/build/client/_app/immutable/assets/11.v5KA95xm.css.br +0 -0
- package/build/client/_app/immutable/assets/11.v5KA95xm.css.gz +0 -0
- package/build/client/_app/immutable/chunks/BcqA7eKM.js +0 -3
- package/build/client/_app/immutable/chunks/BcqA7eKM.js.br +0 -0
- package/build/client/_app/immutable/chunks/BcqA7eKM.js.gz +0 -0
- package/build/client/_app/immutable/chunks/CR6bkGJW.js +0 -6
- package/build/client/_app/immutable/chunks/CR6bkGJW.js.br +0 -0
- package/build/client/_app/immutable/chunks/CR6bkGJW.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DA4Zt9Me.js.br +0 -0
- package/build/client/_app/immutable/chunks/DCDL_9ys.js.br +0 -0
- package/build/client/_app/immutable/chunks/DCDL_9ys.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.D4TXlu7A.js.br +0 -0
- package/build/client/_app/immutable/entry/app.D4TXlu7A.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BBQhtURO.js +0 -1
- package/build/client/_app/immutable/entry/start.BBQhtURO.js.br +0 -0
- package/build/client/_app/immutable/entry/start.BBQhtURO.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.1zylwAPT.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.1zylwAPT.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.BVnLUSs-.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.BVnLUSs-.js.gz +0 -0
- package/build/client/_app/immutable/nodes/10.D1wl2wPX.js.br +0 -0
- package/build/client/_app/immutable/nodes/10.D1wl2wPX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/11.C18nMGmp.js +0 -2
- package/build/client/_app/immutable/nodes/11.C18nMGmp.js.br +0 -0
- package/build/client/_app/immutable/nodes/11.C18nMGmp.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.D1Mm0DUX.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.D1Mm0DUX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.Wfz3TcJd.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.Wfz3TcJd.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.DtZAEPXb.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.DtZAEPXb.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.MfBRh32I.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.MfBRh32I.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.DVE6LnOC.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.DVE6LnOC.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.BCel5OqI.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.BCel5OqI.js.gz +0 -0
- package/build/server/chunks/11-qXSPdF5j.js.map +0 -1
- package/build/server/chunks/_page.svelte-BUBLUSGo.js.map +0 -1
- package/build/server/chunks/_server.ts-Bi0Oe4PF.js.map +0 -1
- package/build/server/chunks/_server.ts-Bjbr7glm.js.map +0 -1
- package/build/server/chunks/_server.ts-BrqaMMAa.js.map +0 -1
- package/build/server/chunks/_server.ts-DhJx0DLr.js.map +0 -1
- package/build/server/chunks/events-handler-Dm1mNPQP.js +0 -20
- package/build/server/chunks/events-handler-Dm1mNPQP.js.map +0 -1
- package/build/server/chunks/pty-manager-41h3IK8K.js.map +0 -1
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ShareInfoResponse, ShareMode, ShareSheetProps } from '$lib/types';
|
|
3
|
+
|
|
4
|
+
import { getApiKey } from '$lib/modules/client/common';
|
|
5
|
+
import { Button, Input } from '@juspay/svelte-ui-components';
|
|
6
|
+
|
|
7
|
+
const { onClose, open = false, shareUrl, terminalId }: ShareSheetProps = $props();
|
|
8
|
+
|
|
9
|
+
let active = $state(false);
|
|
10
|
+
let currentMode = $state<null | ShareMode>(null);
|
|
11
|
+
let mode = $state<ShareMode>('view');
|
|
12
|
+
let password = $state('');
|
|
13
|
+
let busy = $state(false);
|
|
14
|
+
let errorMsg = $state<null | string>(null);
|
|
15
|
+
let copied = $state(false);
|
|
16
|
+
|
|
17
|
+
// Password characters avoid ambiguous glyphs (0/O, 1/l/I).
|
|
18
|
+
const PASSWORD_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
|
|
19
|
+
|
|
20
|
+
const canSubmit = $derived(
|
|
21
|
+
active ? password.length === 0 || password.length >= 6 : password.length >= 6
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
$effect(() => {
|
|
25
|
+
if (open) {
|
|
26
|
+
void loadInfo();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
async function loadInfo(): Promise<void> {
|
|
31
|
+
errorMsg = null;
|
|
32
|
+
copied = false;
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(`/api/terminals/${terminalId}/share`, {
|
|
35
|
+
headers: { Authorization: `Bearer ${getApiKey()}` },
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
errorMsg = 'Failed to load share state.';
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const info = (await res.json()) as ShareInfoResponse;
|
|
42
|
+
active = info.active;
|
|
43
|
+
currentMode = info.mode ?? null;
|
|
44
|
+
mode = info.mode ?? 'view';
|
|
45
|
+
password = '';
|
|
46
|
+
} catch {
|
|
47
|
+
errorMsg = 'Failed to reach the server.';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function generatePassword(): void {
|
|
52
|
+
const bytes = crypto.getRandomValues(new Uint8Array(16));
|
|
53
|
+
password = Array.from(bytes, (b) => PASSWORD_ALPHABET[b % PASSWORD_ALPHABET.length]).join('');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function saveShare(): Promise<void> {
|
|
57
|
+
if (busy || !canSubmit) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
busy = true;
|
|
61
|
+
errorMsg = null;
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(`/api/terminals/${terminalId}/share`, {
|
|
64
|
+
body: JSON.stringify({ mode, ...(password ? { password } : {}) }),
|
|
65
|
+
headers: {
|
|
66
|
+
Authorization: `Bearer ${getApiKey()}`,
|
|
67
|
+
'Content-Type': 'application/json',
|
|
68
|
+
},
|
|
69
|
+
method: 'PUT',
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
const data = (await res.json().catch(() => ({}))) as { error?: string };
|
|
73
|
+
errorMsg = data.error ?? 'Failed to save share.';
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const info = (await res.json()) as ShareInfoResponse;
|
|
77
|
+
active = info.active;
|
|
78
|
+
currentMode = info.mode ?? null;
|
|
79
|
+
password = '';
|
|
80
|
+
} catch {
|
|
81
|
+
errorMsg = 'Failed to reach the server.';
|
|
82
|
+
} finally {
|
|
83
|
+
busy = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function revokeShare(): Promise<void> {
|
|
88
|
+
if (busy) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
busy = true;
|
|
92
|
+
errorMsg = null;
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch(`/api/terminals/${terminalId}/share`, {
|
|
95
|
+
headers: { Authorization: `Bearer ${getApiKey()}` },
|
|
96
|
+
method: 'DELETE',
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
errorMsg = 'Failed to revoke share.';
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
active = false;
|
|
103
|
+
currentMode = null;
|
|
104
|
+
password = '';
|
|
105
|
+
} catch {
|
|
106
|
+
errorMsg = 'Failed to reach the server.';
|
|
107
|
+
} finally {
|
|
108
|
+
busy = false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function copyUrl(): Promise<void> {
|
|
113
|
+
try {
|
|
114
|
+
await navigator.clipboard.writeText(shareUrl);
|
|
115
|
+
copied = true;
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
copied = false;
|
|
118
|
+
}, 2000);
|
|
119
|
+
} catch {
|
|
120
|
+
errorMsg = 'Failed to copy.';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
</script>
|
|
124
|
+
|
|
125
|
+
{#if open}
|
|
126
|
+
<div
|
|
127
|
+
class="share-backdrop"
|
|
128
|
+
onclick={onClose}
|
|
129
|
+
onkeydown={(e: KeyboardEvent): void => {
|
|
130
|
+
if (e.key === 'Escape') {
|
|
131
|
+
onClose();
|
|
132
|
+
}
|
|
133
|
+
}}
|
|
134
|
+
role="presentation"
|
|
135
|
+
>
|
|
136
|
+
<div
|
|
137
|
+
class="share-sheet"
|
|
138
|
+
onclick={(e: MouseEvent): void => {
|
|
139
|
+
e.stopPropagation();
|
|
140
|
+
}}
|
|
141
|
+
role="dialog"
|
|
142
|
+
aria-label="Share terminal"
|
|
143
|
+
tabindex="-1"
|
|
144
|
+
onkeydown={(e: KeyboardEvent): void => {
|
|
145
|
+
if (e.key === 'Escape') {
|
|
146
|
+
onClose();
|
|
147
|
+
}
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
<div class="share-sheet-header">
|
|
151
|
+
<h2 class="share-sheet-title">Share terminal</h2>
|
|
152
|
+
<button class="share-sheet-close" onclick={onClose} aria-label="Close">×</button>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{#if active}
|
|
156
|
+
<div class="share-active-row">
|
|
157
|
+
<span class="share-active-dot"></span>
|
|
158
|
+
<span class="share-active-label">
|
|
159
|
+
Sharing is active ({currentMode === 'control' ? 'full control' : 'view only'})
|
|
160
|
+
</span>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="share-url-row">
|
|
163
|
+
<span class="share-url">{shareUrl}</span>
|
|
164
|
+
<Button
|
|
165
|
+
classes="btn-secondary btn-sm"
|
|
166
|
+
onclick={(): void => {
|
|
167
|
+
void copyUrl();
|
|
168
|
+
}}
|
|
169
|
+
text={copied ? 'Copied' : 'Copy'}
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
{:else}
|
|
173
|
+
<p class="share-sheet-sub">
|
|
174
|
+
Anyone with this page's link and the password below can access this terminal.
|
|
175
|
+
</p>
|
|
176
|
+
{/if}
|
|
177
|
+
|
|
178
|
+
<div class="share-field">
|
|
179
|
+
<span class="share-field-label">Access</span>
|
|
180
|
+
<div class="share-mode-toggle">
|
|
181
|
+
<button
|
|
182
|
+
class="share-mode-btn {mode === 'view' ? 'share-mode-active' : ''}"
|
|
183
|
+
onclick={(): void => {
|
|
184
|
+
mode = 'view';
|
|
185
|
+
}}
|
|
186
|
+
>
|
|
187
|
+
View only
|
|
188
|
+
</button>
|
|
189
|
+
<button
|
|
190
|
+
class="share-mode-btn {mode === 'control' ? 'share-mode-active' : ''}"
|
|
191
|
+
onclick={(): void => {
|
|
192
|
+
mode = 'control';
|
|
193
|
+
}}
|
|
194
|
+
>
|
|
195
|
+
Full control
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div class="share-field">
|
|
201
|
+
<span class="share-field-label">
|
|
202
|
+
{active ? 'New password (leave empty to keep current)' : 'Password (min 6 chars)'}
|
|
203
|
+
</span>
|
|
204
|
+
<div class="share-password-row">
|
|
205
|
+
<Input
|
|
206
|
+
bind:value={password}
|
|
207
|
+
dataType="text"
|
|
208
|
+
placeholder="Password"
|
|
209
|
+
classes="share-password-input"
|
|
210
|
+
/>
|
|
211
|
+
<Button classes="btn-secondary btn-sm" onclick={generatePassword} text="Generate" />
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{#if errorMsg}
|
|
216
|
+
<p class="share-error">{errorMsg}</p>
|
|
217
|
+
{/if}
|
|
218
|
+
|
|
219
|
+
<div class="share-actions">
|
|
220
|
+
{#if active}
|
|
221
|
+
<Button
|
|
222
|
+
classes="btn-danger btn-sm"
|
|
223
|
+
onclick={(): void => {
|
|
224
|
+
void revokeShare();
|
|
225
|
+
}}
|
|
226
|
+
disabled={busy}
|
|
227
|
+
text="Stop sharing"
|
|
228
|
+
/>
|
|
229
|
+
{/if}
|
|
230
|
+
<Button
|
|
231
|
+
classes="btn-primary btn-sm"
|
|
232
|
+
onclick={(): void => {
|
|
233
|
+
void saveShare();
|
|
234
|
+
}}
|
|
235
|
+
disabled={busy || !canSubmit}
|
|
236
|
+
showLoader={busy}
|
|
237
|
+
text={active ? 'Update' : 'Start sharing'}
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
{/if}
|
|
243
|
+
|
|
244
|
+
<style>
|
|
245
|
+
.share-backdrop {
|
|
246
|
+
position: fixed;
|
|
247
|
+
inset: 0;
|
|
248
|
+
z-index: 100;
|
|
249
|
+
display: flex;
|
|
250
|
+
align-items: center;
|
|
251
|
+
justify-content: center;
|
|
252
|
+
background: rgba(0, 0, 0, 0.5);
|
|
253
|
+
padding: var(--space-4);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.share-sheet {
|
|
257
|
+
display: flex;
|
|
258
|
+
flex-direction: column;
|
|
259
|
+
gap: var(--space-3);
|
|
260
|
+
width: 100%;
|
|
261
|
+
max-width: 420px;
|
|
262
|
+
padding: var(--space-5);
|
|
263
|
+
background: var(--ds-background-100);
|
|
264
|
+
border: 1px solid var(--border);
|
|
265
|
+
border-radius: var(--radius-lg);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.share-sheet-header {
|
|
269
|
+
display: flex;
|
|
270
|
+
align-items: center;
|
|
271
|
+
justify-content: space-between;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.share-sheet-title {
|
|
275
|
+
margin: 0;
|
|
276
|
+
font-size: var(--text-lg);
|
|
277
|
+
color: var(--text-primary);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.share-sheet-close {
|
|
281
|
+
background: none;
|
|
282
|
+
border: none;
|
|
283
|
+
color: var(--text-tertiary);
|
|
284
|
+
font-size: 22px;
|
|
285
|
+
cursor: pointer;
|
|
286
|
+
line-height: 1;
|
|
287
|
+
padding: 4px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.share-sheet-close:hover {
|
|
291
|
+
color: var(--text-primary);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.share-sheet-sub {
|
|
295
|
+
margin: 0;
|
|
296
|
+
font-size: var(--text-sm);
|
|
297
|
+
color: var(--text-tertiary);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.share-active-row {
|
|
301
|
+
display: flex;
|
|
302
|
+
align-items: center;
|
|
303
|
+
gap: var(--space-2);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.share-active-dot {
|
|
307
|
+
width: 8px;
|
|
308
|
+
height: 8px;
|
|
309
|
+
border-radius: 50%;
|
|
310
|
+
background: var(--ds-green-500, #22c55e);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.share-active-label {
|
|
314
|
+
font-size: var(--text-sm);
|
|
315
|
+
color: var(--text-secondary);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.share-url-row {
|
|
319
|
+
display: flex;
|
|
320
|
+
align-items: center;
|
|
321
|
+
gap: var(--space-2);
|
|
322
|
+
padding: var(--space-2);
|
|
323
|
+
background: var(--ds-gray-200);
|
|
324
|
+
border-radius: var(--radius-md);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.share-url {
|
|
328
|
+
flex: 1;
|
|
329
|
+
font-family: var(--font-mono);
|
|
330
|
+
font-size: 11px;
|
|
331
|
+
color: var(--text-secondary);
|
|
332
|
+
overflow: hidden;
|
|
333
|
+
text-overflow: ellipsis;
|
|
334
|
+
white-space: nowrap;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.share-field {
|
|
338
|
+
display: flex;
|
|
339
|
+
flex-direction: column;
|
|
340
|
+
gap: var(--space-1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.share-field-label {
|
|
344
|
+
font-size: var(--text-xs);
|
|
345
|
+
color: var(--text-tertiary);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.share-mode-toggle {
|
|
349
|
+
display: flex;
|
|
350
|
+
gap: 2px;
|
|
351
|
+
padding: 2px;
|
|
352
|
+
background: var(--ds-gray-200);
|
|
353
|
+
border: 1px solid var(--ds-gray-400);
|
|
354
|
+
border-radius: var(--radius-md);
|
|
355
|
+
width: fit-content;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.share-mode-btn {
|
|
359
|
+
padding: 6px 12px;
|
|
360
|
+
font-size: var(--text-xs);
|
|
361
|
+
color: var(--text-tertiary);
|
|
362
|
+
background: none;
|
|
363
|
+
border: none;
|
|
364
|
+
border-radius: var(--radius-sm);
|
|
365
|
+
cursor: pointer;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
.share-mode-active {
|
|
369
|
+
background: var(--ds-background-100);
|
|
370
|
+
color: var(--text-primary);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.share-password-row {
|
|
374
|
+
display: flex;
|
|
375
|
+
align-items: center;
|
|
376
|
+
gap: var(--space-2);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
.share-password-row :global(.share-password-input) {
|
|
380
|
+
--input-container-margin: 0;
|
|
381
|
+
flex: 1;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.share-error {
|
|
385
|
+
margin: 0;
|
|
386
|
+
font-size: var(--text-sm);
|
|
387
|
+
color: var(--ds-red-700, #ef4444);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.share-actions {
|
|
391
|
+
display: flex;
|
|
392
|
+
justify-content: flex-end;
|
|
393
|
+
gap: var(--space-2);
|
|
394
|
+
}
|
|
395
|
+
</style>
|
|
@@ -48,7 +48,12 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
|
|
|
48
48
|
term.loadAddon(fitAddon);
|
|
49
49
|
term.loadAddon(new WebLinksAddon());
|
|
50
50
|
term.open(options.container);
|
|
51
|
-
|
|
51
|
+
if (options.readOnly && options.initialCols && options.initialRows) {
|
|
52
|
+
// View-only: render at the PTY's size (we may not resize the shared PTY).
|
|
53
|
+
term.resize(options.initialCols, options.initialRows);
|
|
54
|
+
} else {
|
|
55
|
+
fitAddon.fit();
|
|
56
|
+
}
|
|
52
57
|
|
|
53
58
|
// Block browser-level Cmd/Ctrl shortcuts from reaching the PTY.
|
|
54
59
|
// Allow Ctrl+<letter> terminal signals (Ctrl+C/D/L/R/Z etc.) through.
|
|
@@ -71,7 +76,7 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
|
|
|
71
76
|
|
|
72
77
|
// Clipboard image paste interception
|
|
73
78
|
let pasteListener: ((e: ClipboardEvent) => void) | null = null;
|
|
74
|
-
if (options.terminalId && options.apiKey) {
|
|
79
|
+
if (options.terminalId && options.apiKey && !options.readOnly) {
|
|
75
80
|
const pasteTermId = options.terminalId;
|
|
76
81
|
const pasteApiKey = options.apiKey;
|
|
77
82
|
|
|
@@ -185,6 +190,12 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
|
|
|
185
190
|
options.onActivity?.(msg.active ?? false);
|
|
186
191
|
} else if (msg.type === 'cwd') {
|
|
187
192
|
options.onCwd?.(msg.path ?? '');
|
|
193
|
+
} else if (msg.type === 'resize') {
|
|
194
|
+
// PTY was resized by another client (e.g. the owner). View-only
|
|
195
|
+
// terminals follow it; interactive ones are governed by their fit.
|
|
196
|
+
if (options.readOnly && msg.cols && msg.rows) {
|
|
197
|
+
term.resize(msg.cols, msg.rows);
|
|
198
|
+
}
|
|
188
199
|
}
|
|
189
200
|
};
|
|
190
201
|
|
|
@@ -205,6 +216,9 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
|
|
|
205
216
|
|
|
206
217
|
// Terminal input -> WebSocket
|
|
207
218
|
term.onData((data) => {
|
|
219
|
+
if (options.readOnly) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
208
222
|
if (ws?.readyState === WebSocket.OPEN) {
|
|
209
223
|
ws.send(JSON.stringify({ data, type: 'input' }));
|
|
210
224
|
}
|
|
@@ -212,6 +226,9 @@ export async function createTerminal(options: TerminalOptions): Promise<Terminal
|
|
|
212
226
|
|
|
213
227
|
// Handle resize — skip when container is hidden (display:none → size 0)
|
|
214
228
|
const resizeObserver = new ResizeObserver((): void => {
|
|
229
|
+
if (options.readOnly) {
|
|
230
|
+
return; // View-only terminals keep the PTY's dimensions.
|
|
231
|
+
}
|
|
215
232
|
if (!options.container.offsetWidth || !options.container.offsetHeight) {
|
|
216
233
|
return;
|
|
217
234
|
}
|
|
@@ -483,6 +483,12 @@ class PtyManager {
|
|
|
483
483
|
terminal.pty.resize(cols, rows);
|
|
484
484
|
terminal.cols = cols;
|
|
485
485
|
terminal.rows = rows;
|
|
486
|
+
// Broadcast the new PTY size so attached clients (e.g. view-only
|
|
487
|
+
// guests) can follow the terminal dimensions.
|
|
488
|
+
const msg = JSON.stringify({ cols, rows, type: 'resize' });
|
|
489
|
+
for (const ws of terminal.clients) {
|
|
490
|
+
this.safeSend(ws, msg);
|
|
491
|
+
}
|
|
486
492
|
return true;
|
|
487
493
|
} catch {
|
|
488
494
|
return false;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Resolves a terminal-scoped request to owner (API key) or guest (share token).
|
|
2
|
+
// Kept separate from auth.ts so routes without share semantics don't pull in SQLite.
|
|
3
|
+
|
|
4
|
+
import type { AccessContext } from '$lib/types';
|
|
5
|
+
|
|
6
|
+
import { validateAuth } from '../auth';
|
|
7
|
+
import { shareStore } from './share-store';
|
|
8
|
+
|
|
9
|
+
/** Extract the Bearer token from a request, or null. */
|
|
10
|
+
export function bearerToken(request: Request): null | string {
|
|
11
|
+
const auth = request.headers.get('Authorization') || request.headers.get('authorization');
|
|
12
|
+
if (!auth?.startsWith('Bearer ')) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return auth.slice(7).trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve access for a request targeting one terminal.
|
|
20
|
+
* Owner (valid API key) → { level: 'owner' }.
|
|
21
|
+
* Guest (valid share session for THIS terminal) → { level: 'guest', mode }.
|
|
22
|
+
* Anything else → null.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveAccess(request: Request, terminalId: string): AccessContext | null {
|
|
25
|
+
if (validateAuth(request) === null) {
|
|
26
|
+
return { level: 'owner', mode: null };
|
|
27
|
+
}
|
|
28
|
+
const token = bearerToken(request);
|
|
29
|
+
if (!token) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const session = shareStore.resolveToken(token);
|
|
33
|
+
if (session?.terminalId !== terminalId) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return { level: 'guest', mode: session.mode };
|
|
37
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Share Store — SQLite persistence for terminal sharing.
|
|
3
|
+
*
|
|
4
|
+
* terminal_shares: one share per terminal (scrypt password hash + mode).
|
|
5
|
+
* share_sessions: guest sessions keyed by sha256(token), 7-day TTL.
|
|
6
|
+
*
|
|
7
|
+
* Database location: ~/.shooter/shooter.db (same file as terminal-store).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ShareMode, TerminalShareRecord } from '$lib/types';
|
|
11
|
+
|
|
12
|
+
import Database from 'better-sqlite3';
|
|
13
|
+
import { createHash, randomBytes, scryptSync, timingSafeEqual } from 'crypto';
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
|
|
17
|
+
const DB_DIR = path.join(process.env.HOME || '', '.shooter');
|
|
18
|
+
const DB_PATH = path.join(DB_DIR, 'shooter.db');
|
|
19
|
+
|
|
20
|
+
const SESSION_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
21
|
+
|
|
22
|
+
// ── Password hashing (scrypt, per-share random salt) ─────────────────
|
|
23
|
+
|
|
24
|
+
export class ShareStore {
|
|
25
|
+
private db: Database.Database;
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
fs.mkdirSync(DB_DIR, { recursive: true });
|
|
29
|
+
this.db = new Database(DB_PATH);
|
|
30
|
+
this.db.pragma('journal_mode = WAL');
|
|
31
|
+
|
|
32
|
+
this.db.exec(`
|
|
33
|
+
CREATE TABLE IF NOT EXISTS terminal_shares (
|
|
34
|
+
terminal_id TEXT PRIMARY KEY,
|
|
35
|
+
password_hash TEXT NOT NULL,
|
|
36
|
+
mode TEXT NOT NULL,
|
|
37
|
+
created_at INTEGER NOT NULL,
|
|
38
|
+
updated_at INTEGER NOT NULL
|
|
39
|
+
);
|
|
40
|
+
CREATE TABLE IF NOT EXISTS share_sessions (
|
|
41
|
+
token_hash TEXT PRIMARY KEY,
|
|
42
|
+
terminal_id TEXT NOT NULL,
|
|
43
|
+
created_at INTEGER NOT NULL,
|
|
44
|
+
expires_at INTEGER NOT NULL
|
|
45
|
+
)
|
|
46
|
+
`);
|
|
47
|
+
|
|
48
|
+
this.cleanup();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Purge expired sessions and shares whose terminal no longer exists. */
|
|
52
|
+
cleanup(): void {
|
|
53
|
+
this.db.prepare('DELETE FROM share_sessions WHERE expires_at < ?').run(Date.now());
|
|
54
|
+
try {
|
|
55
|
+
this.db
|
|
56
|
+
.prepare('DELETE FROM terminal_shares WHERE terminal_id NOT IN (SELECT id FROM terminals)')
|
|
57
|
+
.run();
|
|
58
|
+
this.db
|
|
59
|
+
.prepare('DELETE FROM share_sessions WHERE terminal_id NOT IN (SELECT id FROM terminals)')
|
|
60
|
+
.run();
|
|
61
|
+
} catch {
|
|
62
|
+
// terminals table may not exist yet on a fresh database — skip orphan cleanup.
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Issue a new guest session for a shared terminal. Returns the raw token (stored hashed). */
|
|
67
|
+
createSession(terminalId: string): { expiresAt: number; token: string } {
|
|
68
|
+
const token = randomBytes(32).toString('hex');
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const expiresAt = now + SESSION_TTL_MS;
|
|
71
|
+
this.db
|
|
72
|
+
.prepare(
|
|
73
|
+
'INSERT INTO share_sessions (token_hash, terminal_id, created_at, expires_at) VALUES (?, ?, ?, ?)'
|
|
74
|
+
)
|
|
75
|
+
.run(hashToken(token), terminalId, now, expiresAt);
|
|
76
|
+
return { expiresAt, token };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Delete all guest sessions for a terminal (password change / revoke). */
|
|
80
|
+
deleteSessions(terminalId: string): void {
|
|
81
|
+
this.db.prepare('DELETE FROM share_sessions WHERE terminal_id = ?').run(terminalId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Revoke a share: delete the share row and every guest session for it. */
|
|
85
|
+
deleteShare(terminalId: string): void {
|
|
86
|
+
this.db.prepare('DELETE FROM terminal_shares WHERE terminal_id = ?').run(terminalId);
|
|
87
|
+
this.deleteSessions(terminalId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getShare(terminalId: string): null | TerminalShareRecord {
|
|
91
|
+
const row = this.db
|
|
92
|
+
.prepare('SELECT * FROM terminal_shares WHERE terminal_id = ?')
|
|
93
|
+
.get(terminalId) as Record<string, unknown> | undefined;
|
|
94
|
+
return row ? rowToShare(row) : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve a guest bearer token to its terminal + mode.
|
|
99
|
+
* Returns null if unknown, expired, or the share was revoked.
|
|
100
|
+
*/
|
|
101
|
+
resolveToken(token: string): null | { mode: ShareMode; terminalId: string } {
|
|
102
|
+
if (!token) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
const row = this.db
|
|
106
|
+
.prepare(
|
|
107
|
+
`SELECT s.terminal_id, s.expires_at, sh.mode
|
|
108
|
+
FROM share_sessions s
|
|
109
|
+
JOIN terminal_shares sh ON sh.terminal_id = s.terminal_id
|
|
110
|
+
WHERE s.token_hash = ?`
|
|
111
|
+
)
|
|
112
|
+
.get(hashToken(token)) as Record<string, unknown> | undefined;
|
|
113
|
+
if (!row) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
if ((row.expires_at as number) < Date.now()) {
|
|
117
|
+
this.db.prepare('DELETE FROM share_sessions WHERE token_hash = ?').run(hashToken(token));
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return { mode: row.mode as ShareMode, terminalId: row.terminal_id as string };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Create or replace the share for a terminal. */
|
|
124
|
+
setShare(record: TerminalShareRecord): void {
|
|
125
|
+
this.db
|
|
126
|
+
.prepare(
|
|
127
|
+
`INSERT OR REPLACE INTO terminal_shares
|
|
128
|
+
(terminal_id, password_hash, mode, created_at, updated_at)
|
|
129
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
130
|
+
)
|
|
131
|
+
.run(record.terminalId, record.passwordHash, record.mode, record.createdAt, record.updatedAt);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function hashPassword(password: string): string {
|
|
136
|
+
const salt = randomBytes(16).toString('hex');
|
|
137
|
+
const hash = scryptSync(password, salt, 64).toString('hex');
|
|
138
|
+
return `scrypt:${salt}:${hash}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function verifyPassword(password: string, stored: string): boolean {
|
|
142
|
+
const parts = stored.split(':');
|
|
143
|
+
if (parts.length !== 3 || parts[0] !== 'scrypt') {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const expected = Buffer.from(parts[2], 'hex');
|
|
147
|
+
const actual = scryptSync(password, parts[1], 64);
|
|
148
|
+
return expected.length === actual.length && timingSafeEqual(actual, expected);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ── Row mapping ──────────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
function hashToken(token: string): string {
|
|
154
|
+
return createHash('sha256').update(token).digest('hex');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function rowToShare(row: Record<string, unknown>): TerminalShareRecord {
|
|
158
|
+
return {
|
|
159
|
+
createdAt: row.created_at as number,
|
|
160
|
+
mode: row.mode as ShareMode,
|
|
161
|
+
passwordHash: row.password_hash as string,
|
|
162
|
+
terminalId: row.terminal_id as string,
|
|
163
|
+
updatedAt: row.updated_at as number,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Singleton (globalThis bridges tsx server.ts + SvelteKit handler) ─
|
|
168
|
+
|
|
169
|
+
const SS_GLOBAL_KEY = '__shooter_share_store';
|
|
170
|
+
export const shareStore: ShareStore =
|
|
171
|
+
((globalThis as Record<string, unknown>)[SS_GLOBAL_KEY] as ShareStore) || new ShareStore();
|
|
172
|
+
(globalThis as Record<string, unknown>)[SS_GLOBAL_KEY] = shareStore;
|