@kraki/head 0.20.1 → 0.20.2
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 +16 -0
- package/dist/cli.js +13 -0
- package/dist/cli.js.map +1 -1
- package/dist/metrics.d.ts +50 -0
- package/dist/metrics.js +117 -0
- package/dist/metrics.js.map +1 -0
- package/dist/observability.d.ts +40 -0
- package/dist/observability.js +58 -0
- package/dist/observability.js.map +1 -0
- package/dist/pulse-hub.d.ts +1 -0
- package/dist/pulse-hub.js +16 -2
- package/dist/pulse-hub.js.map +1 -1
- package/dist/push/apns.js +2 -1
- package/dist/push/apns.js.map +1 -1
- package/dist/push/index.d.ts +2 -0
- package/dist/push/index.js +16 -0
- package/dist/push/index.js.map +1 -1
- package/dist/server/account-control.d.ts +28 -0
- package/dist/server/account-control.js +288 -0
- package/dist/server/account-control.js.map +1 -0
- package/dist/server/auth-flow.d.ts +82 -0
- package/dist/server/auth-flow.js +315 -0
- package/dist/server/auth-flow.js.map +1 -0
- package/dist/server/presence.d.ts +8 -0
- package/dist/server/presence.js +103 -0
- package/dist/server/presence.js.map +1 -0
- package/dist/server/shared.d.ts +106 -0
- package/dist/server/shared.js +49 -0
- package/dist/server/shared.js.map +1 -0
- package/dist/server/voice-lease.d.ts +5 -0
- package/dist/server/voice-lease.js +111 -0
- package/dist/server/voice-lease.js.map +1 -0
- package/dist/server.d.ts +19 -131
- package/dist/server.js +65 -862
- package/dist/server.js.map +1 -1
- package/dist/storage.d.ts +4 -1
- package/dist/storage.js +5 -2
- package/dist/storage.js.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/** The WebSocket auth handshake: token / challenge auth, pairing tokens, replacing an older connection. */
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
|
+
import { metrics } from '../metrics.js';
|
|
4
|
+
import { DEVICE_REPLACED_CLOSE_CODE } from '@kraki/protocol';
|
|
5
|
+
import { PULSE_ACK_EVERY_BYTES } from '../pulse-hub.js';
|
|
6
|
+
import { getLogger } from '../logger.js';
|
|
7
|
+
import { trace } from '../trace.js';
|
|
8
|
+
import { broadcastDeviceJoined, getDeviceSummaries } from './presence.js';
|
|
9
|
+
import { publicPreferences } from './shared.js';
|
|
10
|
+
export function sendAuthError(server, ws, code, message, redirect, deviceId) {
|
|
11
|
+
metrics().authFailed(code);
|
|
12
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
13
|
+
ws.send(JSON.stringify({ type: 'auth_error', code, message, ...(redirect && { redirect }), ...(deviceId && { deviceId }) }));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function handleAuth(server, ws, state, msg) {
|
|
17
|
+
state.pulseProgressAck = msg.device?.pulseProgressAck === true;
|
|
18
|
+
const logger = getLogger();
|
|
19
|
+
const { auth } = msg;
|
|
20
|
+
// One auth attempt per socket at a time: a second `auth` while the first
|
|
21
|
+
// is awaiting its backend would register the device twice.
|
|
22
|
+
if (state.authInFlight || state.pendingNonce) {
|
|
23
|
+
// Ignored rather than answered with auth_error: clients treat auth
|
|
24
|
+
// errors as credential failures, and this one is just a duplicate.
|
|
25
|
+
logger.warn('Ignoring duplicate auth while one is in progress', { ip: state.ip });
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
state.authInFlight = true;
|
|
29
|
+
try {
|
|
30
|
+
if (auth.method === 'challenge') {
|
|
31
|
+
// Challenge is multi-step — start here, verify in handleChallengeResponse
|
|
32
|
+
const result = await server.authBackend.startChallenge(auth.deviceId, msg.device.encryptionKey, server.options.region);
|
|
33
|
+
if (!socketStillAuthenticating(server, ws, state))
|
|
34
|
+
return;
|
|
35
|
+
if (!result.ok) {
|
|
36
|
+
sendAuthError(server, ws, result.code, result.message, result.code === 'wrong_region' ? result.redirect : undefined);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
state.pendingNonce = result.nonce;
|
|
40
|
+
state.pendingDeviceId = result.deviceId;
|
|
41
|
+
state.pendingDeviceInfo = { encryptionKey: msg.device.encryptionKey };
|
|
42
|
+
state.pendingAuthMethod = 'challenge';
|
|
43
|
+
logger.debug('Issuing auth challenge', { deviceId: auth.deviceId });
|
|
44
|
+
ws.send(JSON.stringify({ type: 'auth_challenge', nonce: result.nonce }));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const result = await server.authBackend.authenticate(auth, msg.device, server.options.region, state.ip);
|
|
48
|
+
if (!socketStillAuthenticating(server, ws, state))
|
|
49
|
+
return;
|
|
50
|
+
handleBackendAuthResult(server, ws, state, result);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
state.authInFlight = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** After an awaited auth step: is this socket still open, still ours and
|
|
57
|
+
* still unauthenticated? The auth deadline may have closed it meanwhile;
|
|
58
|
+
* registering a dead socket would evict the device's live connection. */
|
|
59
|
+
export function socketStillAuthenticating(server, ws, state) {
|
|
60
|
+
return ws.readyState === WebSocket.OPEN && server.clients.get(ws) === state && !state.authenticated;
|
|
61
|
+
}
|
|
62
|
+
export function handleChallengeResponse(server, ws, state, msg) {
|
|
63
|
+
const logger = getLogger();
|
|
64
|
+
const deviceId = state.pendingDeviceId;
|
|
65
|
+
const nonce = state.pendingNonce;
|
|
66
|
+
const pendingInfo = state.pendingDeviceInfo;
|
|
67
|
+
state.pendingNonce = undefined;
|
|
68
|
+
state.pendingDeviceId = undefined;
|
|
69
|
+
state.pendingDeviceInfo = undefined;
|
|
70
|
+
if (!deviceId || !nonce) {
|
|
71
|
+
sendAuthError(server, ws, 'no_pending_challenge', 'No pending challenge');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
state.authInFlight = true;
|
|
75
|
+
server.authBackend.verifyChallenge(deviceId, nonce, msg.signature, pendingInfo?.encryptionKey, server.options.region).then(result => {
|
|
76
|
+
if (!socketStillAuthenticating(server, ws, state))
|
|
77
|
+
return;
|
|
78
|
+
handleBackendAuthResult(server, ws, state, result);
|
|
79
|
+
}).catch(err => {
|
|
80
|
+
// A thrown error means the auth backend itself was unreachable / timed
|
|
81
|
+
// out (network, 5xx, abort) — NOT that the signature was invalid. A
|
|
82
|
+
// deterministic auth failure comes back as a resolved AuthError outcome
|
|
83
|
+
// above. Reporting this as auth_rejected made the arm treat a transient
|
|
84
|
+
// backend outage as a permanent credential failure and wipe the paired
|
|
85
|
+
// device, forcing a full re-pair. Surface it as auth_unavailable so the
|
|
86
|
+
// client retries instead of destroying a valid session.
|
|
87
|
+
logger.error('Auth backend verifyChallenge failed', { error: err.message });
|
|
88
|
+
if (socketStillAuthenticating(server, ws, state)) {
|
|
89
|
+
sendAuthError(server, ws, 'service_unavailable', 'Authentication service unavailable');
|
|
90
|
+
}
|
|
91
|
+
}).finally(() => {
|
|
92
|
+
state.authInFlight = false;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Handle a result from AuthBackend — registers connection state and sends response.
|
|
97
|
+
* Used for both initial auth and challenge-response verification.
|
|
98
|
+
*/
|
|
99
|
+
export function handleBackendAuthResult(server, ws, state, result) {
|
|
100
|
+
const logger = getLogger();
|
|
101
|
+
if (!result.ok) {
|
|
102
|
+
const redirect = result.code === 'wrong_region' ? result.redirect : undefined;
|
|
103
|
+
const deviceId = result.code === 'wrong_region' ? result.deviceId : undefined;
|
|
104
|
+
sendAuthError(server, ws, result.code, result.message, redirect, deviceId);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Register connection
|
|
108
|
+
state.authenticated = true;
|
|
109
|
+
state.authenticatedAt = Date.now();
|
|
110
|
+
if (state.authTimer) {
|
|
111
|
+
clearTimeout(state.authTimer);
|
|
112
|
+
state.authTimer = undefined;
|
|
113
|
+
}
|
|
114
|
+
state.deviceId = result.deviceId;
|
|
115
|
+
state.userId = result.userId;
|
|
116
|
+
evictPreviousConnection(server, result.deviceId, ws);
|
|
117
|
+
server.connections.set(result.deviceId, ws);
|
|
118
|
+
server.userByDevice.set(result.deviceId, result.userId);
|
|
119
|
+
trace('DEV-CONNECT', { device: result.deviceId, user: result.userId, auth: 'backend' });
|
|
120
|
+
metrics().inc('auth.ok');
|
|
121
|
+
logger.info('Device authenticated (via backend)', {
|
|
122
|
+
deviceId: result.deviceId,
|
|
123
|
+
user: result.user.login,
|
|
124
|
+
method: result.authMethod,
|
|
125
|
+
ip: state.ip,
|
|
126
|
+
});
|
|
127
|
+
// Mirror user + devices into local storage so edge-local operations
|
|
128
|
+
// (broadcastDeviceJoined, getDeviceSummaries, etc.) work even when auth
|
|
129
|
+
// is delegated to a remote backend.
|
|
130
|
+
try {
|
|
131
|
+
server.storage.upsertUser(result.user.id, result.user.login, result.user.provider, result.user.email);
|
|
132
|
+
for (const d of result.devices) {
|
|
133
|
+
try {
|
|
134
|
+
server.storage.upsertDevice(d.id, result.userId, d.name, d.role, d.kind ?? undefined, d.publicKey ?? undefined, d.encryptionKey ?? undefined);
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
logger.warn('Failed to mirror device locally', { deviceId: d.id, error: err.message });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
logger.warn('Failed to mirror user locally', { userId: result.userId, error: err.message });
|
|
143
|
+
}
|
|
144
|
+
completeAuthHandshake(server, ws, {
|
|
145
|
+
deviceId: result.deviceId,
|
|
146
|
+
userId: result.userId,
|
|
147
|
+
user: result.user,
|
|
148
|
+
authMethod: result.authMethod,
|
|
149
|
+
devices: result.devices,
|
|
150
|
+
githubClientId: result.githubClientId,
|
|
151
|
+
vapidPublicKey: result.vapidPublicKey,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Common post-auth completion logic, used by every auth path.
|
|
156
|
+
*
|
|
157
|
+
* Responsibilities:
|
|
158
|
+
* - Read user preferences from local storage and merge into the user object.
|
|
159
|
+
* - Flush any locally-queued pending messages for this device (offline-queue).
|
|
160
|
+
* - Build and send the `auth_ok` response.
|
|
161
|
+
* - Broadcast `device_joined` to the user's other connected devices.
|
|
162
|
+
*
|
|
163
|
+
* Each caller is responsible for path-specific work BEFORE calling this:
|
|
164
|
+
* validating credentials, persisting user/device records, setting up
|
|
165
|
+
* ClientState, populating `connections` / `userByDevice` maps, and logging
|
|
166
|
+
* the path-specific "authenticated via X" line.
|
|
167
|
+
*
|
|
168
|
+
* Centralising the tail eliminates a class of bugs where new auth paths
|
|
169
|
+
* (notably the multi-region delegated-backend path) silently diverge from
|
|
170
|
+
* the established post-auth flow.
|
|
171
|
+
*/
|
|
172
|
+
export function completeAuthHandshake(server, ws, params) {
|
|
173
|
+
// Merge preferences. Local storage takes precedence (it captures per-edge
|
|
174
|
+
// overrides the backend might not know about), but fall back to the
|
|
175
|
+
// backend-supplied preferences if local has none — e.g. on a user's very
|
|
176
|
+
// first auth to this edge, before upsertUser has been called or when the
|
|
177
|
+
// backend is the canonical source of truth.
|
|
178
|
+
//
|
|
179
|
+
// Why this matters: storage.upsertUser only writes username/provider/email,
|
|
180
|
+
// NOT preferences. So a brand-new user record reads back with
|
|
181
|
+
// preferences=undefined. Without this fallback, backend preferences would
|
|
182
|
+
// be silently dropped at every first auth.
|
|
183
|
+
const fullUser = server.storage.getUser(params.userId);
|
|
184
|
+
const userResponse = {
|
|
185
|
+
...params.user,
|
|
186
|
+
preferences: publicPreferences(fullUser?.preferences ?? params.user.preferences),
|
|
187
|
+
};
|
|
188
|
+
// Devices list. If the caller provided one (edge mode), recompute online
|
|
189
|
+
// status against our current connections map. Otherwise derive locally.
|
|
190
|
+
const devices = params.devices
|
|
191
|
+
? params.devices.map(d => ({ ...d, online: server.connections.has(d.id) }))
|
|
192
|
+
: getDeviceSummaries(server, params.userId);
|
|
193
|
+
const deviceRole = devices.find((device) => device.id === params.deviceId)?.role
|
|
194
|
+
?? server.storage.getDevice(params.deviceId)?.role;
|
|
195
|
+
ws.send(JSON.stringify({
|
|
196
|
+
type: 'auth_ok',
|
|
197
|
+
deviceId: params.deviceId,
|
|
198
|
+
authMethod: params.authMethod,
|
|
199
|
+
user: userResponse,
|
|
200
|
+
devices,
|
|
201
|
+
githubClientId: params.githubClientId ?? server.authBackend.getAuthInfo().githubClientId,
|
|
202
|
+
vapidPublicKey: params.vapidPublicKey ?? server.getVapidPublicKey(),
|
|
203
|
+
relayVersion: server.options.version,
|
|
204
|
+
pulseAckBytes: PULSE_ACK_EVERY_BYTES,
|
|
205
|
+
// Apps only: a tentacle has no use for the user's word list.
|
|
206
|
+
...(deviceRole === 'app' && { voiceVocabulary: server.storage.getVoiceVocabulary(params.userId) }),
|
|
207
|
+
...(server.getVoiceCapability() && { voice: server.getVoiceCapability() }),
|
|
208
|
+
}));
|
|
209
|
+
// Clients only accept Pulse envelopes after processing auth_ok. Starting
|
|
210
|
+
// the endpoint before auth_ok loses the initial hello, leaving peerEpoch
|
|
211
|
+
// empty and making a later relay restart indistinguishable from the first
|
|
212
|
+
// connection. That stale recvCursor then drops the restarted seq=1..N as
|
|
213
|
+
// duplicates. Keep this ordering centralized for every auth path.
|
|
214
|
+
//
|
|
215
|
+
// Apps have explicit reconnect authorities (fresh session_list,
|
|
216
|
+
// subscription snapshot, and history ranges). If their HELLO carries a new
|
|
217
|
+
// process epoch, fence the preceding process's non-durable downlink so the
|
|
218
|
+
// new UI cannot animate through old live events. Tentacle downlinks carry
|
|
219
|
+
// commands and retain ordinary Pulse resume semantics across process epochs.
|
|
220
|
+
server.pulseHub.onDeviceConnected(params.deviceId, {
|
|
221
|
+
discardPreviousProcessNonDurable: deviceRole === 'app',
|
|
222
|
+
});
|
|
223
|
+
broadcastDeviceJoined(server, params.userId, params.deviceId);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* A device is about to register `ws` as its connection. If the SAME device id
|
|
227
|
+
* already had a socket, that older socket is now orphaned: once we overwrite
|
|
228
|
+
* the map entry its close handler hits the reconnect-guard and skips cleanup,
|
|
229
|
+
* and the ping timer (which only walks `connections`) never visits it again.
|
|
230
|
+
* It would sit in `clients` holding its ClientState until TCP finally gives
|
|
231
|
+
* up. Retire both the WebSocket and its Pulse link explicitly before the new
|
|
232
|
+
* connection is installed.
|
|
233
|
+
*
|
|
234
|
+
* Must be called BEFORE `connections.set(deviceId, ws)`.
|
|
235
|
+
*/
|
|
236
|
+
export function evictPreviousConnection(server, deviceId, ws) {
|
|
237
|
+
const previous = server.connections.get(deviceId);
|
|
238
|
+
if (!previous || previous === ws)
|
|
239
|
+
return;
|
|
240
|
+
getLogger().info('Replacing existing connection for device', { deviceId });
|
|
241
|
+
// The old close callback will see the replacement in `connections` and skip
|
|
242
|
+
// its guarded cleanup. Publish the Pulse disconnect here so capability and
|
|
243
|
+
// connection-scoped state cannot leak across the replacement boundary.
|
|
244
|
+
server.pulseHub.onDeviceDisconnected(deviceId);
|
|
245
|
+
const previousState = server.clients.get(previous);
|
|
246
|
+
if (previousState)
|
|
247
|
+
previousState.evicted = true;
|
|
248
|
+
server.clients.delete(previous);
|
|
249
|
+
// Say why (a bare terminate() reads as a network drop, and a client that
|
|
250
|
+
// reconnects on it evicts this new connection in turn). Force the TCP
|
|
251
|
+
// teardown shortly after in case the old peer never completes the close.
|
|
252
|
+
try {
|
|
253
|
+
previous.close(DEVICE_REPLACED_CLOSE_CODE, 'replaced');
|
|
254
|
+
}
|
|
255
|
+
catch { /* best effort */ }
|
|
256
|
+
const force = setTimeout(() => { try {
|
|
257
|
+
previous.terminate();
|
|
258
|
+
}
|
|
259
|
+
catch { /* already gone */ } }, 2_000);
|
|
260
|
+
force.unref?.();
|
|
261
|
+
previous.once('close', () => clearTimeout(force));
|
|
262
|
+
}
|
|
263
|
+
export async function handleCreatePairingToken(server, ws, state) {
|
|
264
|
+
const logger = getLogger();
|
|
265
|
+
if (!(server.options.pairingEnabled ?? true)) {
|
|
266
|
+
server.sendError(ws, 'Pairing is disabled on this relay');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (!state.userId) {
|
|
270
|
+
server.sendError(ws, 'Not authenticated');
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
try {
|
|
274
|
+
// A remote (edge) backend creates tokens on the account service.
|
|
275
|
+
const backend = server.authBackend;
|
|
276
|
+
const result = backend.createPairingTokenAsync
|
|
277
|
+
? await backend.createPairingTokenAsync(state.userId)
|
|
278
|
+
: backend.createPairingToken(state.userId);
|
|
279
|
+
if (!result?.token)
|
|
280
|
+
throw new Error('no token returned');
|
|
281
|
+
logger.info('Pairing token created', { userId: state.userId });
|
|
282
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
283
|
+
ws.send(JSON.stringify({ type: 'pairing_token_created', token: result.token, expiresIn: result.expiresIn }));
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
logger.warn('Pairing token creation failed', { userId: state.userId, error: err.message });
|
|
288
|
+
server.sendError(ws, 'Pairing is temporarily unavailable');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
export async function handleRequestPairingToken(server, ws, state, msg) {
|
|
292
|
+
const logger = getLogger();
|
|
293
|
+
if (!(server.options.pairingEnabled ?? true)) {
|
|
294
|
+
server.sendError(ws, 'Pairing is disabled on this relay');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (!msg.token) {
|
|
298
|
+
server.sendError(ws, 'Token required for pairing request');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const result = await server.authBackend.requestPairingToken(msg.token, state.ip);
|
|
302
|
+
if (ws.readyState !== WebSocket.OPEN)
|
|
303
|
+
return;
|
|
304
|
+
if (!result.ok) {
|
|
305
|
+
sendAuthError(server, ws, result.code, result.message);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
logger.info('Pairing token created (one-shot)', { userId: result.userId });
|
|
309
|
+
ws.send(JSON.stringify({
|
|
310
|
+
type: 'pairing_token_created',
|
|
311
|
+
token: result.pairingToken,
|
|
312
|
+
expiresIn: result.expiresIn,
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
//# sourceMappingURL=auth-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-flow.js","sourceRoot":"","sources":["../../src/server/auth-flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Device presence: who joined, left, is pending or was removed, and the device list. */
|
|
2
|
+
import type { DeviceSummary } from '@kraki/protocol';
|
|
3
|
+
import type { HeadServer } from '../server.js';
|
|
4
|
+
export declare function broadcastDeviceRemoved(server: HeadServer, userId: string, removedDeviceId: string): void;
|
|
5
|
+
export declare function broadcastDeviceJoined(server: HeadServer, userId: string, newDeviceId: string): void;
|
|
6
|
+
export declare function broadcastDeviceLeft(server: HeadServer, userId: string, leftDeviceId: string): void;
|
|
7
|
+
export declare function broadcastDevicePending(server: HeadServer, userId: string, pendingDeviceId: string): void;
|
|
8
|
+
export declare function getDeviceSummaries(server: HeadServer, userId: string): DeviceSummary[];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Device presence: who joined, left, is pending or was removed, and the device list. */
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
|
+
export function broadcastDeviceRemoved(server, userId, removedDeviceId) {
|
|
4
|
+
let userDevices;
|
|
5
|
+
try {
|
|
6
|
+
userDevices = server.storage.getDevicesByUser(userId);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
for (const d of userDevices) {
|
|
12
|
+
const ws = server.connections.get(d.id);
|
|
13
|
+
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
14
|
+
continue;
|
|
15
|
+
server.sendPresenceToDevice(d.id, { type: 'device_removed', deviceId: removedDeviceId });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function broadcastDeviceJoined(server, userId, newDeviceId) {
|
|
19
|
+
let device;
|
|
20
|
+
try {
|
|
21
|
+
device = server.storage.getDevice(newDeviceId);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return;
|
|
25
|
+
} // Storage may be closed during shutdown
|
|
26
|
+
if (!device)
|
|
27
|
+
return;
|
|
28
|
+
const summary = {
|
|
29
|
+
id: device.id,
|
|
30
|
+
name: device.name,
|
|
31
|
+
role: device.role,
|
|
32
|
+
kind: device.kind ?? undefined,
|
|
33
|
+
publicKey: device.publicKey ?? undefined,
|
|
34
|
+
encryptionKey: device.encryptionKey ?? undefined,
|
|
35
|
+
online: true,
|
|
36
|
+
lastSeen: device.lastSeen,
|
|
37
|
+
createdAt: device.createdAt,
|
|
38
|
+
};
|
|
39
|
+
let userDevices;
|
|
40
|
+
try {
|
|
41
|
+
userDevices = server.storage.getDevicesByUser(userId);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
for (const d of userDevices) {
|
|
47
|
+
if (d.id === newDeviceId)
|
|
48
|
+
continue;
|
|
49
|
+
const ws = server.connections.get(d.id);
|
|
50
|
+
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
51
|
+
continue;
|
|
52
|
+
server.sendPresenceToDevice(d.id, { type: 'device_joined', device: summary });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function broadcastDeviceLeft(server, userId, leftDeviceId) {
|
|
56
|
+
let userDevices;
|
|
57
|
+
try {
|
|
58
|
+
userDevices = server.storage.getDevicesByUser(userId);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return;
|
|
62
|
+
} // Storage may be closed during shutdown
|
|
63
|
+
for (const d of userDevices) {
|
|
64
|
+
if (d.id === leftDeviceId)
|
|
65
|
+
continue;
|
|
66
|
+
const ws = server.connections.get(d.id);
|
|
67
|
+
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
68
|
+
continue;
|
|
69
|
+
server.sendPresenceToDevice(d.id, { type: 'device_left', deviceId: leftDeviceId });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
export function broadcastDevicePending(server, userId, pendingDeviceId) {
|
|
73
|
+
let userDevices;
|
|
74
|
+
try {
|
|
75
|
+
userDevices = server.storage.getDevicesByUser(userId);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
for (const d of userDevices) {
|
|
81
|
+
if (d.id === pendingDeviceId)
|
|
82
|
+
continue;
|
|
83
|
+
const ws = server.connections.get(d.id);
|
|
84
|
+
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
85
|
+
continue;
|
|
86
|
+
server.sendPresenceToDevice(d.id, { type: 'device_pending', deviceId: pendingDeviceId });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export function getDeviceSummaries(server, userId) {
|
|
90
|
+
const stored = server.storage.getDevicesByUser(userId);
|
|
91
|
+
return stored.map(d => ({
|
|
92
|
+
id: d.id,
|
|
93
|
+
name: d.name,
|
|
94
|
+
role: d.role,
|
|
95
|
+
kind: d.kind ?? undefined,
|
|
96
|
+
publicKey: d.publicKey ?? undefined,
|
|
97
|
+
encryptionKey: d.encryptionKey ?? undefined,
|
|
98
|
+
online: server.connections.has(d.id),
|
|
99
|
+
lastSeen: d.lastSeen,
|
|
100
|
+
createdAt: d.createdAt,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=presence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"presence.js","sourceRoot":"","sources":["../../src/server/presence.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Shared by HeadServer and its handler modules: per-socket state, options, limits. */
|
|
2
|
+
import { LeaseIssuer } from '../lease-issuer.js';
|
|
3
|
+
import type { AuthProvider } from '../auth.js';
|
|
4
|
+
import type { PushManager } from '../push/index.js';
|
|
5
|
+
import type { AuthBackend } from '../auth-backend.js';
|
|
6
|
+
/** Custom Words has its own channel (`auth_ok.voiceVocabulary` and
|
|
7
|
+
* `voice_vocabulary_updated`). Never ship it inside generic preferences: that
|
|
8
|
+
* would duplicate it on every auth and broadcast it on every theme change. */
|
|
9
|
+
export declare function publicPreferences(prefs: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
|
|
10
|
+
export interface ClientState {
|
|
11
|
+
deviceId?: string;
|
|
12
|
+
userId?: string;
|
|
13
|
+
authenticated: boolean;
|
|
14
|
+
ip?: string;
|
|
15
|
+
isAlive: boolean;
|
|
16
|
+
authenticatedAt?: number;
|
|
17
|
+
pendingNonce?: string;
|
|
18
|
+
/** An `auth` / `auth_response` is awaiting its backend. */
|
|
19
|
+
authInFlight?: boolean;
|
|
20
|
+
pendingDeviceId?: string;
|
|
21
|
+
pendingDeviceInfo?: {
|
|
22
|
+
encryptionKey?: string;
|
|
23
|
+
};
|
|
24
|
+
pendingAuthMethod?: string;
|
|
25
|
+
/** Replaced by a newer socket of the same device: closing, inert. */
|
|
26
|
+
evicted?: boolean;
|
|
27
|
+
/** Closes the socket if it never authenticates (G1). */
|
|
28
|
+
authTimer?: ReturnType<typeof setTimeout>;
|
|
29
|
+
/** Timestamp when pong grace period expires (null = no pending ping). */
|
|
30
|
+
pongOverdueAt: number | null;
|
|
31
|
+
/** Whether we already broadcast device_pending for this ping cycle. */
|
|
32
|
+
pendingLivenessBroadcast: boolean;
|
|
33
|
+
/** Diagnostic: last time we sent a ping to this client (ms epoch). */
|
|
34
|
+
lastPingSentAt?: number;
|
|
35
|
+
/** Diagnostic: last time we received a pong from this client (ms epoch). */
|
|
36
|
+
lastPongRecvAt?: number;
|
|
37
|
+
/** Last time any frame arrived from this client (ms epoch). */
|
|
38
|
+
lastInboundAt?: number;
|
|
39
|
+
/** ws.bufferedAmount when the last ping was queued. */
|
|
40
|
+
bufferedAtPing?: number;
|
|
41
|
+
/** Declared at auth: this client's Pulse accepts progress heartbeats. */
|
|
42
|
+
pulseProgressAck?: boolean;
|
|
43
|
+
}
|
|
44
|
+
export interface HeadServerOptions {
|
|
45
|
+
authProviders?: Map<string, AuthProvider>;
|
|
46
|
+
authProvider?: AuthProvider;
|
|
47
|
+
maxPayload?: number;
|
|
48
|
+
pairingEnabled?: boolean;
|
|
49
|
+
pairingTtl?: number;
|
|
50
|
+
version?: string;
|
|
51
|
+
pushManager?: PushManager;
|
|
52
|
+
/** Optional auth backend. If provided, auth is delegated to it instead of using local providers. */
|
|
53
|
+
authBackend?: AuthBackend;
|
|
54
|
+
/** This head's region identifier (e.g., 'us', 'china'). Sent to auth backend for region checks. */
|
|
55
|
+
region?: string;
|
|
56
|
+
/** Voice-broker lease signer. If absent, request_voice_lease is rejected with 'not_entitled'. */
|
|
57
|
+
leaseIssuer?: LeaseIssuer;
|
|
58
|
+
/** Lease (device voice credential) lifetime in seconds. Default 86400 (24h). */
|
|
59
|
+
voiceLeaseTtlSec?: number;
|
|
60
|
+
/** The only voice limit: max seconds of audio per user per UTC day. Default 7200 (2h). */
|
|
61
|
+
voiceDailyQuotaSec?: number;
|
|
62
|
+
/**
|
|
63
|
+
* Public WSS URL of the voice broker for this region (e.g.
|
|
64
|
+
* `wss://cn.stt.kraki.chat/voice`). When set, head advertises a
|
|
65
|
+
* `VoiceCapability` inside `auth_ok.voice` so arm can render the mic UI.
|
|
66
|
+
* When unset, the field is omitted entirely — arm hides the mic UI.
|
|
67
|
+
*
|
|
68
|
+
* Must be set together with `leaseIssuer`: cli-side interlock refuses to
|
|
69
|
+
* start in either-without-the-other configurations because:
|
|
70
|
+
* - Advertising a broker URL but having no issuer would mean clients
|
|
71
|
+
* show the mic UI then get `not_entitled` on every press.
|
|
72
|
+
* - Having an issuer but no advertised URL means clients have no way
|
|
73
|
+
* to actually use the leases they're being issued.
|
|
74
|
+
*/
|
|
75
|
+
voiceBrokerUrl?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Close sockets that have not authenticated within this time (ms).
|
|
78
|
+
* Default 15s. A socket that asked for `auth_info` (an app on its login
|
|
79
|
+
* screen, waiting for the user to finish signing in) gets
|
|
80
|
+
* `loginScreenAuthTimeoutMs` instead (default 10 min).
|
|
81
|
+
*/
|
|
82
|
+
authTimeoutMs?: number;
|
|
83
|
+
loginScreenAuthTimeoutMs?: number;
|
|
84
|
+
}
|
|
85
|
+
export declare const DEFAULT_VOICE_LEASE_TTL_SEC = 86400;
|
|
86
|
+
export declare const DEFAULT_VOICE_DAILY_QUOTA_SEC = 7200;
|
|
87
|
+
export declare const MIN_VOICE_LEASE_GRANT_SEC = 1;
|
|
88
|
+
export declare const VALID_VOICE_RESOURCES: Set<"voice/doubao">;
|
|
89
|
+
export declare const DEFAULT_MAX_PAYLOAD: number;
|
|
90
|
+
export declare const DEFAULT_AUTH_TIMEOUT_MS = 15000;
|
|
91
|
+
export declare const DEFAULT_LOGIN_SCREEN_AUTH_TIMEOUT_MS: number;
|
|
92
|
+
export declare const MAX_MULTICAST_TARGETS = 64;
|
|
93
|
+
/** Send-buffer ceiling per socket before a stalled client is disconnected. */
|
|
94
|
+
export declare const MAX_WS_BUFFERED_BYTES: number;
|
|
95
|
+
/** How often the liveness sweep runs (ms). Faster than the 30s ping timer so
|
|
96
|
+
* device_pending detection latency tracks the recipient's ping cadence
|
|
97
|
+
* (~10s), not the slower liveness check cadence. */
|
|
98
|
+
export declare const LIVENESS_SWEEP_INTERVAL_MS = 2500;
|
|
99
|
+
/** Grace period after ping before broadcasting device_pending (ms).
|
|
100
|
+
* Overridable via PONG_GRACE_MS environment variable. */
|
|
101
|
+
export declare const PONG_GRACE_MS: number;
|
|
102
|
+
export declare function isValidMessage(msg: unknown): msg is {
|
|
103
|
+
type: string;
|
|
104
|
+
[key: string]: unknown;
|
|
105
|
+
};
|
|
106
|
+
export declare function isValidAuth(msg: Record<string, unknown>): boolean;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Shared by HeadServer and its handler modules: per-socket state, options, limits. */
|
|
2
|
+
/** Custom Words has its own channel (`auth_ok.voiceVocabulary` and
|
|
3
|
+
* `voice_vocabulary_updated`). Never ship it inside generic preferences: that
|
|
4
|
+
* would duplicate it on every auth and broadcast it on every theme change. */
|
|
5
|
+
export function publicPreferences(prefs) {
|
|
6
|
+
if (!prefs || !('voiceVocabulary' in prefs))
|
|
7
|
+
return prefs;
|
|
8
|
+
const { voiceVocabulary: _omitted, ...rest } = prefs;
|
|
9
|
+
return rest;
|
|
10
|
+
}
|
|
11
|
+
export const DEFAULT_VOICE_LEASE_TTL_SEC = 86_400;
|
|
12
|
+
export const DEFAULT_VOICE_DAILY_QUOTA_SEC = 7_200;
|
|
13
|
+
export const MIN_VOICE_LEASE_GRANT_SEC = 1;
|
|
14
|
+
export const VALID_VOICE_RESOURCES = new Set(['voice/doubao']);
|
|
15
|
+
export const DEFAULT_MAX_PAYLOAD = 10 * 1024 * 1024;
|
|
16
|
+
export const DEFAULT_AUTH_TIMEOUT_MS = 15_000;
|
|
17
|
+
export const DEFAULT_LOGIN_SCREEN_AUTH_TIMEOUT_MS = 10 * 60_000;
|
|
18
|
+
export const MAX_MULTICAST_TARGETS = 64;
|
|
19
|
+
/** Send-buffer ceiling per socket before a stalled client is disconnected. */
|
|
20
|
+
export const MAX_WS_BUFFERED_BYTES = 32 * 1024 * 1024;
|
|
21
|
+
/** How often the liveness sweep runs (ms). Faster than the 30s ping timer so
|
|
22
|
+
* device_pending detection latency tracks the recipient's ping cadence
|
|
23
|
+
* (~10s), not the slower liveness check cadence. */
|
|
24
|
+
export const LIVENESS_SWEEP_INTERVAL_MS = 2_500;
|
|
25
|
+
/** Grace period after ping before broadcasting device_pending (ms).
|
|
26
|
+
* Overridable via PONG_GRACE_MS environment variable. */
|
|
27
|
+
export const PONG_GRACE_MS = Math.max(1_000, parseInt(process.env.PONG_GRACE_MS ?? '8000', 10) || 8_000);
|
|
28
|
+
export function isValidMessage(msg) {
|
|
29
|
+
if (typeof msg !== 'object' || msg === null)
|
|
30
|
+
return false;
|
|
31
|
+
const obj = msg;
|
|
32
|
+
return typeof obj.type === 'string' && obj.type.length > 0;
|
|
33
|
+
}
|
|
34
|
+
export function isValidAuth(msg) {
|
|
35
|
+
if (!msg.device || typeof msg.device !== 'object')
|
|
36
|
+
return false;
|
|
37
|
+
const dev = msg.device;
|
|
38
|
+
if (typeof dev.name !== 'string' || typeof dev.role !== 'string')
|
|
39
|
+
return false;
|
|
40
|
+
if (dev.role !== 'tentacle' && dev.role !== 'app')
|
|
41
|
+
return false;
|
|
42
|
+
if (!msg.auth || typeof msg.auth !== 'object')
|
|
43
|
+
return false;
|
|
44
|
+
const auth = msg.auth;
|
|
45
|
+
if (typeof auth.method !== 'string')
|
|
46
|
+
return false;
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=shared.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/server/shared.ts"],"names":[],"mappings":"AAAA,uFAAuF;AAQvF;;+EAE+E;AAC/E,MAAM,UAAU,iBAAiB,CAAC,KAA0C;IAC1E,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,iBAAiB,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,EAAE,eAAe,EAAE,QAAQ,EAAE,GAAG,IAAI,EAAE,GAAG,KAAK,CAAC;IACrD,OAAO,IAAI,CAAC;AACd,CAAC;AA+ED,MAAM,CAAC,MAAM,2BAA2B,GAAG,MAAM,CAAC;AAClD,MAAM,CAAC,MAAM,6BAA6B,GAAG,KAAK,CAAC;AACnD,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC;AAC3C,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,GAAG,CAAgB,CAAC,cAAc,CAAC,CAAC,CAAC;AAE9E,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AACpD,MAAM,CAAC,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAC9C,MAAM,CAAC,MAAM,oCAAoC,GAAG,EAAE,GAAG,MAAM,CAAC;AAChE,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,CAAC;AACxC,8EAA8E;AAC9E,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEtD;;qDAEqD;AACrD,MAAM,CAAC,MAAM,0BAA0B,GAAG,KAAK,CAAC;AAChD;0DAC0D;AAC1D,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,MAAM,EAAE,EAAE,CAAC,IAAI,KAAK,CAAC,CAAC;AAEzG,MAAM,UAAU,cAAc,CAAC,GAAY;IACzC,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC1D,MAAM,GAAG,GAAG,GAA8B,CAAC;IAC3C,OAAO,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAA4B;IACtD,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAChE,MAAM,GAAG,GAAG,GAAG,CAAC,MAAiC,CAAC;IAClD,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC/E,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,IAAI,GAAG,CAAC,IAAI,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IAChE,IAAI,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5D,MAAM,IAAI,GAAG,GAAG,CAAC,IAA+B,CAAC;IACjD,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAClD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Voice leases: short-lived credentials for the voice service, within the daily quota. */
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
|
+
import type { HeadServer } from '../server.js';
|
|
4
|
+
import { type ClientState } from './shared.js';
|
|
5
|
+
export declare function handleRequestVoiceLease(server: HeadServer, ws: WebSocket, state: ClientState, msg: Record<string, unknown>): void;
|