@kraki/head 0.20.0 → 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.
Files changed (57) hide show
  1. package/README.md +16 -0
  2. package/dist/account-api.d.ts +10 -0
  3. package/dist/account-api.js +67 -15
  4. package/dist/account-api.js.map +1 -1
  5. package/dist/auth-backend.d.ts +16 -5
  6. package/dist/auth.d.ts +0 -1
  7. package/dist/auth.js +32 -20
  8. package/dist/auth.js.map +1 -1
  9. package/dist/cli.js +25 -1
  10. package/dist/cli.js.map +1 -1
  11. package/dist/lease-issuer.js +1 -1
  12. package/dist/lease-issuer.js.map +1 -1
  13. package/dist/local-auth-backend.d.ts +5 -1
  14. package/dist/local-auth-backend.js +30 -17
  15. package/dist/local-auth-backend.js.map +1 -1
  16. package/dist/metrics.d.ts +50 -0
  17. package/dist/metrics.js +117 -0
  18. package/dist/metrics.js.map +1 -0
  19. package/dist/observability.d.ts +40 -0
  20. package/dist/observability.js +58 -0
  21. package/dist/observability.js.map +1 -0
  22. package/dist/pulse-hub.d.ts +9 -0
  23. package/dist/pulse-hub.js +67 -5
  24. package/dist/pulse-hub.js.map +1 -1
  25. package/dist/push/apns.js +21 -2
  26. package/dist/push/apns.js.map +1 -1
  27. package/dist/push/index.d.ts +2 -0
  28. package/dist/push/index.js +16 -0
  29. package/dist/push/index.js.map +1 -1
  30. package/dist/remote-auth-backend.d.ts +2 -1
  31. package/dist/remote-auth-backend.js +10 -2
  32. package/dist/remote-auth-backend.js.map +1 -1
  33. package/dist/server/account-control.d.ts +28 -0
  34. package/dist/server/account-control.js +288 -0
  35. package/dist/server/account-control.js.map +1 -0
  36. package/dist/server/auth-flow.d.ts +82 -0
  37. package/dist/server/auth-flow.js +315 -0
  38. package/dist/server/auth-flow.js.map +1 -0
  39. package/dist/server/presence.d.ts +8 -0
  40. package/dist/server/presence.js +103 -0
  41. package/dist/server/presence.js.map +1 -0
  42. package/dist/server/shared.d.ts +106 -0
  43. package/dist/server/shared.js +49 -0
  44. package/dist/server/shared.js.map +1 -0
  45. package/dist/server/voice-lease.d.ts +5 -0
  46. package/dist/server/voice-lease.js +111 -0
  47. package/dist/server/voice-lease.js.map +1 -0
  48. package/dist/server.d.ts +20 -125
  49. package/dist/server.js +114 -1072
  50. package/dist/server.js.map +1 -1
  51. package/dist/storage.d.ts +21 -1
  52. package/dist/storage.js +88 -12
  53. package/dist/storage.js.map +1 -1
  54. package/dist/trace.d.ts +0 -1
  55. package/dist/trace.js +0 -3
  56. package/dist/trace.js.map +1 -1
  57. package/package.json +3 -4
@@ -0,0 +1,288 @@
1
+ /** Account control messages: preferences, Custom Words, push tokens and previews, removing devices, deleting the account. */
2
+ import { RemoteAuthBackend } from '../remote-auth-backend.js';
3
+ import { WebSocket } from 'ws';
4
+ import { ACCOUNT_DELETED_CLOSE_CODE } from '@kraki/protocol';
5
+ import { parseVoiceWordOps } from '../voice-vocabulary.js';
6
+ import { getLogger } from '../logger.js';
7
+ import { trace } from '../trace.js';
8
+ import { broadcastDeviceRemoved } from './presence.js';
9
+ import { publicPreferences } from './shared.js';
10
+ /** The relay-terminated control ops, callable from BOTH the raw-WS dispatch
11
+ * (onMessage) and the pulse deliver-to-self path. Returns true if handled. */
12
+ export function handleControlMessage(server, ws, state, msg) {
13
+ switch (msg.type) {
14
+ case 'update_preferences':
15
+ handleUpdatePreferences(server, ws, state, msg);
16
+ return true;
17
+ case 'update_voice_vocabulary':
18
+ handleUpdateVoiceVocabulary(server, state, msg);
19
+ return true;
20
+ case 'remove_device':
21
+ handleRemoveDevice(server, ws, state, msg.deviceId);
22
+ return true;
23
+ case 'delete_account':
24
+ void handleDeleteAccount(server, ws, state);
25
+ return true;
26
+ case 'register_push_token':
27
+ handleRegisterPushToken(server, ws, state, msg);
28
+ return true;
29
+ case 'unregister_push_token':
30
+ handleUnregisterPushToken(server, ws, state, msg);
31
+ return true;
32
+ case 'dispatch_push': {
33
+ const sender = state.deviceId ? server.storage.getDevice(state.deviceId) : undefined;
34
+ const payload = msg.payload;
35
+ if (sender?.role !== 'tentacle' || !payload?.preview)
36
+ return true;
37
+ firePushPreview(server, state, payload.preview);
38
+ return true;
39
+ }
40
+ default: return false;
41
+ }
42
+ }
43
+ /** Persist a preferences update and echo the merged result to the sender + the
44
+ * user's other online devices. */
45
+ export function handleUpdatePreferences(server, ws, state, msg) {
46
+ if (!state.userId)
47
+ return;
48
+ const prefs = msg.preferences;
49
+ if (!prefs || typeof prefs !== 'object')
50
+ return;
51
+ server.storage.updatePreferences(state.userId, prefs);
52
+ // Read back the full merged preferences
53
+ const fullUser = server.storage.getUser(state.userId);
54
+ const merged = publicPreferences(fullUser?.preferences ?? prefs);
55
+ const confirmation = { type: 'preferences_updated', preferences: merged };
56
+ // Confirm to the sender + fan out to the user's other devices — all over pulse.
57
+ if (state.deviceId)
58
+ server.sendControlToDevice(state.deviceId, confirmation);
59
+ const userDevices = (() => {
60
+ try {
61
+ return server.storage.getDevicesByUser(state.userId);
62
+ }
63
+ catch {
64
+ return [];
65
+ }
66
+ })();
67
+ for (const d of userDevices) {
68
+ if (d.id === state.deviceId)
69
+ continue;
70
+ const other = server.connections.get(d.id);
71
+ if (!other || other.readyState !== WebSocket.OPEN)
72
+ continue;
73
+ server.sendControlToDevice(d.id, confirmation);
74
+ }
75
+ }
76
+ /** Account data, not session data: no tentacle is involved. The sender
77
+ * always gets the resulting list back (its acknowledgement); other online
78
+ * apps of the account get it only when something was applied. */
79
+ export function handleUpdateVoiceVocabulary(server, state, msg) {
80
+ if (!state.userId || !state.deviceId)
81
+ return;
82
+ const requestId = typeof msg.requestId === 'string' ? msg.requestId.slice(0, 64) : undefined;
83
+ const ops = parseVoiceWordOps(msg.ops);
84
+ let words;
85
+ try {
86
+ words = ops?.length
87
+ ? server.storage.updateVoiceVocabulary(state.userId, ops)
88
+ : server.storage.getVoiceVocabulary(state.userId);
89
+ }
90
+ catch (err) {
91
+ // Pulse self-delivery has no outer try/catch. No reply: the client keeps
92
+ // the ops in its outbox and retries.
93
+ getLogger().warn('Voice vocabulary update failed', { error: err.message });
94
+ return;
95
+ }
96
+ server.sendControlToDevice(state.deviceId, { type: 'voice_vocabulary_updated', requestId, words });
97
+ if (!ops?.length)
98
+ return;
99
+ for (const d of server.storage.getDevicesByUser(state.userId)) {
100
+ if (d.id !== state.deviceId && d.role === 'app' && server.connections.get(d.id)?.readyState === WebSocket.OPEN) {
101
+ server.sendControlToDevice(d.id, { type: 'voice_vocabulary_updated', words });
102
+ }
103
+ }
104
+ }
105
+ /** Notify offline devices from a pushPreview side-channel. The preview rides the
106
+ * same pulse broadcast envelope as the reliable message. */
107
+ export function firePushPreview(server, state, pushPreview) {
108
+ const senderUserId = state.userId;
109
+ const senderDeviceId = state.deviceId;
110
+ if (!senderUserId || !senderDeviceId || !pushPreview) {
111
+ trace('PUSH-SKIP', { reason: !senderUserId ? 'no-user' : !senderDeviceId ? 'no-device' : 'no-preview' });
112
+ return;
113
+ }
114
+ // Online native clients that post their own notifications (macOS) get the
115
+ // same encrypted preview over their live connection.
116
+ for (const token of server.storage.getPushTokensForOfflineDevices(senderUserId, [])) {
117
+ if (token.provider !== 'local' || token.deviceId === senderDeviceId)
118
+ continue;
119
+ if (!server.connections.has(token.deviceId))
120
+ continue;
121
+ const key = pushPreview.keys[token.deviceId];
122
+ if (!key)
123
+ continue;
124
+ server.sendControlToDevice(token.deviceId, { type: 'notification_preview', payload: { blob: pushPreview.blob, key } });
125
+ trace('PUSH-LOCAL', { deviceId: token.deviceId });
126
+ }
127
+ if (!server.options.pushManager) {
128
+ trace('PUSH-SKIP', { reason: 'no-pushmgr' });
129
+ return;
130
+ }
131
+ const onlineDeviceIds = [senderDeviceId];
132
+ let offlineCount = 0;
133
+ for (const device of server.storage.getDevicesByUser(senderUserId)) {
134
+ if (device.id === senderDeviceId)
135
+ continue;
136
+ if (server.connections.get(device.id))
137
+ onlineDeviceIds.push(device.id);
138
+ else
139
+ offlineCount++;
140
+ }
141
+ trace('PUSH-FIRE', { sender: senderDeviceId, onlineCount: onlineDeviceIds.length, offlineCount, previewKeys: Object.keys(pushPreview.keys) });
142
+ server.options.pushManager.sendToOfflineDevices(senderUserId, onlineDeviceIds, pushPreview)
143
+ .catch(err => getLogger().warn('Push notification send failed', { error: err.message }));
144
+ }
145
+ export function handleRemoveDevice(server, ws, state, targetDeviceId) {
146
+ const logger = getLogger();
147
+ if (!state.userId || !state.deviceId) {
148
+ server.sendError(ws, 'Not authenticated');
149
+ return;
150
+ }
151
+ if (!targetDeviceId || typeof targetDeviceId !== 'string') {
152
+ server.sendError(ws, 'Missing deviceId');
153
+ return;
154
+ }
155
+ const targetDevice = server.storage.getDevice(targetDeviceId);
156
+ if (!targetDevice || targetDevice.userId !== state.userId) {
157
+ server.sendError(ws, 'Device not found');
158
+ return;
159
+ }
160
+ // Only allow removing offline devices (zombie cleanup)
161
+ if (server.connections.has(targetDeviceId)) {
162
+ server.sendError(ws, 'Cannot remove an online device');
163
+ return;
164
+ }
165
+ server.storage.deleteDevice(targetDeviceId);
166
+ server.storage.deletePushTokensForDevice(targetDeviceId);
167
+ // Drop the device's pulse endpoint + persisted outbox/snapshot/capability
168
+ // rows too. Otherwise they outlive the device record forever, and
169
+ // recoverOnBoot faithfully rebuilds an endpoint for a device that can
170
+ // never authenticate again.
171
+ server.pulseHub.forgetDevice(targetDeviceId);
172
+ logger.info('Device removed', { deviceId: targetDeviceId, byDevice: state.deviceId });
173
+ // Edge mode: the account service owns the device list and every auth
174
+ // mirrors it back here, so the removal must reach it too (otherwise the
175
+ // device reappears after the next reconnect).
176
+ const remote = server.authBackend;
177
+ if (remote instanceof RemoteAuthBackend) {
178
+ const userId = state.userId;
179
+ remote.removeDevice(userId, targetDeviceId).then((ok) => {
180
+ if (!ok)
181
+ logger.warn('Account service did not remove device', { deviceId: targetDeviceId });
182
+ }).catch((err) => {
183
+ logger.warn('Failed to remove device at account service', { deviceId: targetDeviceId, error: err.message });
184
+ });
185
+ }
186
+ // Broadcast removal to all remaining user devices
187
+ broadcastDeviceRemoved(server, state.userId, targetDeviceId);
188
+ }
189
+ /**
190
+ * Delete the requesting user's account: at the account store (the account
191
+ * service in edge mode) and in this relay's own storage and pulse state.
192
+ * Then tell every connected device and close it. Devices that were offline
193
+ * get `auth_error` code `account_deleted` when they next connect.
194
+ */
195
+ export async function handleDeleteAccount(server, ws, state) {
196
+ const logger = getLogger();
197
+ const userId = state.userId;
198
+ const deviceId = state.deviceId;
199
+ if (!userId || !deviceId) {
200
+ server.sendError(ws, 'Not authenticated');
201
+ return;
202
+ }
203
+ if (server.storage.getDevice(deviceId)?.role !== 'app') {
204
+ server.sendError(ws, 'Only the Kraki app can delete the account');
205
+ return;
206
+ }
207
+ if (server.deletingAccounts.has(userId))
208
+ return;
209
+ server.deletingAccounts.add(userId);
210
+ try {
211
+ const deviceIds = new Set(server.storage.getDevicesByUser(userId).map((d) => d.id));
212
+ if (server.authBackend.deleteAccount) {
213
+ for (const id of await server.authBackend.deleteAccount(userId))
214
+ deviceIds.add(id);
215
+ }
216
+ // This relay's own rows (the edge mirror; already gone when the
217
+ // account store is this relay's storage).
218
+ for (const id of server.storage.deleteUser(userId))
219
+ deviceIds.add(id);
220
+ logger.info('Account deleted', { userId, devices: deviceIds.size, byDevice: deviceId });
221
+ for (const id of deviceIds) {
222
+ const socket = server.connections.get(id);
223
+ if (socket) {
224
+ // Detach before closing so the close handler doesn't announce
225
+ // device_left or touch the deleted rows.
226
+ server.connections.delete(id);
227
+ server.userByDevice.delete(id);
228
+ server.clients.delete(socket);
229
+ if (socket.readyState === WebSocket.OPEN) {
230
+ socket.send(JSON.stringify({ type: 'account_deleted' }));
231
+ socket.close(ACCOUNT_DELETED_CLOSE_CODE, 'account_deleted');
232
+ }
233
+ }
234
+ server.pulseHub.forgetDevice(id);
235
+ }
236
+ }
237
+ catch (err) {
238
+ logger.error('Account deletion failed', { userId, error: err.message });
239
+ server.sendError(ws, 'Could not delete the account. Try again.');
240
+ }
241
+ finally {
242
+ server.deletingAccounts.delete(userId);
243
+ }
244
+ }
245
+ export function handleRegisterPushToken(server, ws, state, msg) {
246
+ const logger = getLogger();
247
+ if (!state.userId || !state.deviceId) {
248
+ server.sendError(ws, 'Not authenticated');
249
+ return;
250
+ }
251
+ const payload = msg.payload;
252
+ if (!payload || typeof payload.token !== 'string' || typeof payload.provider !== 'string') {
253
+ server.sendError(ws, 'Invalid push token: payload.token and payload.provider required');
254
+ return;
255
+ }
256
+ server.storage.upsertPushToken(state.deviceId, payload.provider, payload.token, payload.environment, payload.bundleId);
257
+ // Clean up Web Push tokens from stale devices (offline >24h) to prevent
258
+ // duplicate notifications from abandoned PWA installs on the same phone.
259
+ // APNs tokens are kept: a phone not opened for a day still needs its pushes.
260
+ // Only this user's devices matter; passing every connection on the relay
261
+ // made each registration O(global connections) in SQL parameters.
262
+ const onlineDeviceIds = server.storage.getDevicesByUser(state.userId)
263
+ .map((d) => d.id)
264
+ .filter((id) => server.connections.has(id));
265
+ const pruned = server.storage.deleteStaleUserPushTokens(state.userId, state.deviceId, onlineDeviceIds);
266
+ if (pruned > 0) {
267
+ logger.info('Pruned stale push tokens', { userId: state.userId, count: pruned });
268
+ }
269
+ logger.info('Push token registered', { deviceId: state.deviceId, provider: payload.provider });
270
+ if (state.deviceId) {
271
+ server.sendControlToDevice(state.deviceId, { type: 'push_token_registered', payload: { provider: payload.provider } });
272
+ }
273
+ }
274
+ export function handleUnregisterPushToken(server, ws, state, msg) {
275
+ const logger = getLogger();
276
+ if (!state.userId || !state.deviceId) {
277
+ server.sendError(ws, 'Not authenticated');
278
+ return;
279
+ }
280
+ const payload = msg.payload;
281
+ if (!payload || typeof payload.provider !== 'string') {
282
+ server.sendError(ws, 'Invalid unregister: payload.provider required');
283
+ return;
284
+ }
285
+ server.storage.deletePushToken(state.deviceId, payload.provider);
286
+ logger.info('Push token unregistered', { deviceId: state.deviceId, provider: payload.provider });
287
+ }
288
+ //# sourceMappingURL=account-control.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account-control.js","sourceRoot":"","sources":["../../src/server/account-control.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ /** The WebSocket auth handshake: token / challenge auth, pairing tokens, replacing an older connection. */
2
+ import { WebSocket } from 'ws';
3
+ import type { AuthMessage, AuthErrorCode, DeviceSummary } from '@kraki/protocol';
4
+ import type { AuthOutcome } from '../auth-backend.js';
5
+ import type { HeadServer } from '../server.js';
6
+ import { type ClientState } from './shared.js';
7
+ export declare function sendAuthError(server: HeadServer, ws: WebSocket, code: AuthErrorCode, message: string, redirect?: string, deviceId?: string): void;
8
+ export declare function handleAuth(server: HeadServer, ws: WebSocket, state: ClientState, msg: AuthMessage): Promise<void>;
9
+ /** After an awaited auth step: is this socket still open, still ours and
10
+ * still unauthenticated? The auth deadline may have closed it meanwhile;
11
+ * registering a dead socket would evict the device's live connection. */
12
+ export declare function socketStillAuthenticating(server: HeadServer, ws: WebSocket, state: ClientState): boolean;
13
+ export declare function handleChallengeResponse(server: HeadServer, ws: WebSocket, state: ClientState, msg: {
14
+ deviceId: string;
15
+ signature: string;
16
+ }): void;
17
+ /**
18
+ * Handle a result from AuthBackend — registers connection state and sends response.
19
+ * Used for both initial auth and challenge-response verification.
20
+ */
21
+ export declare function handleBackendAuthResult(server: HeadServer, ws: WebSocket, state: ClientState, result: AuthOutcome): void;
22
+ /**
23
+ * Common post-auth completion logic, used by every auth path.
24
+ *
25
+ * Responsibilities:
26
+ * - Read user preferences from local storage and merge into the user object.
27
+ * - Flush any locally-queued pending messages for this device (offline-queue).
28
+ * - Build and send the `auth_ok` response.
29
+ * - Broadcast `device_joined` to the user's other connected devices.
30
+ *
31
+ * Each caller is responsible for path-specific work BEFORE calling this:
32
+ * validating credentials, persisting user/device records, setting up
33
+ * ClientState, populating `connections` / `userByDevice` maps, and logging
34
+ * the path-specific "authenticated via X" line.
35
+ *
36
+ * Centralising the tail eliminates a class of bugs where new auth paths
37
+ * (notably the multi-region delegated-backend path) silently diverge from
38
+ * the established post-auth flow.
39
+ */
40
+ export declare function completeAuthHandshake(server: HeadServer, ws: WebSocket, params: {
41
+ /** The authenticated device's ID. */
42
+ deviceId: string;
43
+ /** The authenticated user's ID. */
44
+ userId: string;
45
+ /** Identity fields from the auth source (local DB or remote backend).
46
+ * `preferences` and `region` are optional and only meaningful from the
47
+ * delegated-backend path — local paths leave them undefined and the
48
+ * helper reads preferences from local storage instead. */
49
+ user: {
50
+ id: string;
51
+ login: string;
52
+ provider: string;
53
+ email?: string;
54
+ preferences?: Record<string, unknown>;
55
+ region?: string;
56
+ };
57
+ /** Auth method, echoed back in auth_ok. */
58
+ authMethod: string;
59
+ /** Devices list to include in auth_ok. If omitted, derived from local storage.
60
+ * Edge mode supplies this from the remote backend's view. */
61
+ devices?: DeviceSummary[];
62
+ /** Override of the backend's GitHub client id (used when delegated). */
63
+ githubClientId?: string;
64
+ /** Override of `getVapidPublicKey()` (used when delegated). */
65
+ vapidPublicKey?: string;
66
+ }): void;
67
+ /**
68
+ * A device is about to register `ws` as its connection. If the SAME device id
69
+ * already had a socket, that older socket is now orphaned: once we overwrite
70
+ * the map entry its close handler hits the reconnect-guard and skips cleanup,
71
+ * and the ping timer (which only walks `connections`) never visits it again.
72
+ * It would sit in `clients` holding its ClientState until TCP finally gives
73
+ * up. Retire both the WebSocket and its Pulse link explicitly before the new
74
+ * connection is installed.
75
+ *
76
+ * Must be called BEFORE `connections.set(deviceId, ws)`.
77
+ */
78
+ export declare function evictPreviousConnection(server: HeadServer, deviceId: string, ws: WebSocket): void;
79
+ export declare function handleCreatePairingToken(server: HeadServer, ws: WebSocket, state: ClientState): Promise<void>;
80
+ export declare function handleRequestPairingToken(server: HeadServer, ws: WebSocket, state: ClientState, msg: {
81
+ token?: string;
82
+ }): Promise<void>;