handmux 0.21.0 → 0.22.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/README.md +6 -6
- package/README.zh-CN.md +6 -6
- package/package.json +4 -3
- package/public/assets/index-CRAlQZDz.css +32 -0
- package/public/assets/index-CzlitgRX.js +335 -0
- package/public/index.html +2 -2
- package/src/push.js +16 -7
- package/src/routes/push.js +5 -4
- package/src/routes/terminal.js +12 -0
- package/public/assets/index-DgkdA9oQ.js +0 -335
- package/public/assets/index-WH6Olfjg.css +0 -32
package/public/index.html
CHANGED
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
@keyframes bootDot { 0%,100% { opacity: .22; transform: translateY(0); } 40% { opacity: 1; transform: translateY(-3px); } }
|
|
49
49
|
@media (prefers-reduced-motion: reduce) { .boot-glow, .boot-dots i { animation: none; opacity: .8; } }
|
|
50
50
|
</style>
|
|
51
|
-
<script type="module" crossorigin src="/assets/index-
|
|
52
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
51
|
+
<script type="module" crossorigin src="/assets/index-CzlitgRX.js"></script>
|
|
52
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CRAlQZDz.css" media="print" onload="this.media='all'"><noscript><link rel="stylesheet" crossorigin href="/assets/index-CRAlQZDz.css"></noscript>
|
|
53
53
|
</head>
|
|
54
54
|
<body>
|
|
55
55
|
<div id="boot-splash" aria-hidden="true">
|
package/src/push.js
CHANGED
|
@@ -39,6 +39,7 @@ function ensureInit() {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const genKey = () => crypto.randomBytes(18).toString('base64url');
|
|
42
|
+
const VALID_PUSH_KEY = /^[A-Za-z0-9_-]{16,128}$/;
|
|
42
43
|
|
|
43
44
|
let subs = load();
|
|
44
45
|
|
|
@@ -56,13 +57,20 @@ export function isConfigured() { ensureInit(); return configured; }
|
|
|
56
57
|
export function publicKey() { ensureInit(); return process.env.VAPID_PUBLIC || null; }
|
|
57
58
|
export function count() { return subs.length; }
|
|
58
59
|
|
|
59
|
-
export function addSubscription(sub, boundSessions = []) {
|
|
60
|
+
export function addSubscription(sub, boundSessions = [], preferredPushKey = null) {
|
|
60
61
|
if (!sub || typeof sub.endpoint !== 'string') return false;
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
const endpointRecord = subs.find((s) => s.subscription.endpoint === sub.endpoint);
|
|
63
|
+
const requestedKey = VALID_PUSH_KEY.test(preferredPushKey || '') ? preferredPushKey : null;
|
|
64
|
+
const keyRecord = requestedKey ? subs.find((s) => s.pushKey === requestedKey) : null;
|
|
65
|
+
const pushKey = keyRecord?.pushKey || endpointRecord?.pushKey || requestedKey || genKey();
|
|
66
|
+
|
|
67
|
+
// A browser PushSubscription is transport state, not device identity. Re-enabling push can produce a
|
|
68
|
+
// different endpoint; replace the old endpoint that owns this stable key instead of creating a new
|
|
69
|
+
// script-push target. Also collapse any legacy duplicate for the new endpoint.
|
|
70
|
+
subs = subs.filter((s) => s.subscription.endpoint !== sub.endpoint && s.pushKey !== pushKey);
|
|
71
|
+
subs.push({ subscription: sub, boundSessions, pushKey });
|
|
64
72
|
persist();
|
|
65
|
-
return
|
|
73
|
+
return pushKey;
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
export function updateBound(endpoint, boundSessions = []) {
|
|
@@ -71,9 +79,10 @@ export function updateBound(endpoint, boundSessions = []) {
|
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
export function removeSubscription(endpoint) {
|
|
74
|
-
const
|
|
82
|
+
const removed = subs.find((s) => s.subscription.endpoint === endpoint);
|
|
75
83
|
subs = subs.filter((s) => s.subscription.endpoint !== endpoint);
|
|
76
|
-
if (
|
|
84
|
+
if (removed) persist();
|
|
85
|
+
return removed?.pushKey || null;
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
// The device-addressing id (NOT an auth credential — see /api/push/send-local). Lazy-generate for
|
package/src/routes/push.js
CHANGED
|
@@ -16,10 +16,11 @@ export function pushRoutes({ push, notifications }) {
|
|
|
16
16
|
// the toggle proves the whole pipe (subscribe → push service → SW → notification) end to end.
|
|
17
17
|
r.post('/push/subscribe', async (req, res, next) => {
|
|
18
18
|
const sub = req.body?.subscription;
|
|
19
|
+
const preferredPushKey = req.body?.pushKey;
|
|
19
20
|
const boundSessions = Array.isArray(req.body?.boundSessions) ? req.body.boundSessions : [];
|
|
20
21
|
if (!sub || typeof sub.endpoint !== 'string') return res.status(400).json({ error: 'bad subscription' });
|
|
21
22
|
try {
|
|
22
|
-
push.addSubscription(sub, boundSessions);
|
|
23
|
+
const pushKey = push.addSubscription(sub, boundSessions, preferredPushKey);
|
|
23
24
|
const delivery = await push.sendToOne(sub, { title: '通知已开启 ✅', body: '会话「需要你」或「已完成」时提醒你', tag: 'handmux-welcome' }, { topic: 'handmux', urgency: 'high' });
|
|
24
25
|
// `deliver` deliberately contains failures so an automatic pane push can never break the polling
|
|
25
26
|
// loop. Enabling notifications is different: its welcome push is the end-to-end health check, so a
|
|
@@ -30,14 +31,14 @@ export function pushRoutes({ push, notifications }) {
|
|
|
30
31
|
error: expired ? 'push subscription expired' : 'push delivery rejected',
|
|
31
32
|
});
|
|
32
33
|
}
|
|
33
|
-
res.json({ ok: true, count: push.count(), pushKey
|
|
34
|
+
res.json({ ok: true, count: push.count(), pushKey });
|
|
34
35
|
} catch (e) { next(e); }
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
r.post('/push/unsubscribe', (req, res) => {
|
|
38
39
|
const endpoint = req.body?.endpoint;
|
|
39
|
-
|
|
40
|
-
res.json({ ok: true });
|
|
40
|
+
const pushKey = typeof endpoint === 'string' ? push.removeSubscription(endpoint) : null;
|
|
41
|
+
res.json({ ok: true, pushKey });
|
|
41
42
|
});
|
|
42
43
|
|
|
43
44
|
// Manual "send me a test" — pushes to every stored subscription.
|
package/src/routes/terminal.js
CHANGED
|
@@ -137,6 +137,18 @@ export function terminalRoutes({ commands }) {
|
|
|
137
137
|
catch (e) { next(e); }
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
// Restore only the pane arrangement. Window sizing remains untouched; handing the whole window
|
|
141
|
+
// back to an attached client is the separate `auto` resize operation below.
|
|
142
|
+
r.post('/layout', async (req, res, next) => {
|
|
143
|
+
const { window, layout } = req.body || {};
|
|
144
|
+
if (!isWindowId(window)) return res.status(400).json({ error: 'bad window id' });
|
|
145
|
+
if (typeof layout !== 'string' || !layout) return res.status(400).json({ error: 'bad window layout' });
|
|
146
|
+
try {
|
|
147
|
+
await commands.applyWindowLayout(window, layout);
|
|
148
|
+
res.json({ ok: true });
|
|
149
|
+
} catch (e) { next(e); }
|
|
150
|
+
});
|
|
151
|
+
|
|
140
152
|
r.post('/resize', async (req, res, next) => {
|
|
141
153
|
const { window, pane, cols, rows, auto, layout } = req.body || {};
|
|
142
154
|
const c = Math.min(Math.max(Number(cols) || 0, 20), 500);
|