@walkhi/code-relax 0.1.0-beta.23 → 0.1.0-beta.24
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/dist/bin/cleanup-accounts.mjs +63 -0
- package/dist/src/self-relay/account-cleanup.mjs +30 -0
- package/dist/src/self-relay/admin-page.mjs +6 -12
- package/dist/src/self-relay/admin-server.mjs +17 -9
- package/dist/src/self-relay/client.mjs +45 -45
- package/dist/src/self-relay/connector.mjs +293 -289
- package/dist/src/self-relay/diagnostics.mjs +54 -54
- package/dist/src/self-relay/email-accounts.mjs +158 -141
- package/dist/src/self-relay/privacy-page.mjs +26 -0
- package/dist/src/self-relay/server.mjs +153 -67
- package/dist/src/self-relay/usage-meter.mjs +141 -136
- package/dist/src/server.mjs +15 -13
- package/package.json +1 -1
|
@@ -1,136 +1,141 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { recordDailyUsage } from './usage-daily.mjs';
|
|
3
|
-
|
|
4
|
-
const epoch = value => new Date(`${value.replace(' ', 'T')}Z`).getTime();
|
|
5
|
-
|
|
6
|
-
// Count confirmed intervals only; a lost connection never creates estimated time.
|
|
7
|
-
export function usageMeter(pool) {
|
|
8
|
-
async function action(connection, computerId, clientId, kind, key) {
|
|
9
|
-
if (!connection.userId || !['message', 'turn'].includes(kind) || typeof key !== 'string' || !key || key.length > 128) return;
|
|
10
|
-
const column = kind === 'message' ? 'message_count' : 'turn_count';
|
|
11
|
-
const db = await pool.getConnection();
|
|
12
|
-
try {
|
|
13
|
-
await db.beginTransaction();
|
|
14
|
-
const [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
VALUES
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const [
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const [
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
await db.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
LEFT JOIN (SELECT computer_id,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
LEFT JOIN (SELECT user_id,
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { recordDailyUsage } from './usage-daily.mjs';
|
|
3
|
+
|
|
4
|
+
const epoch = value => new Date(`${value.replace(' ', 'T')}Z`).getTime();
|
|
5
|
+
|
|
6
|
+
// Count confirmed intervals only; a lost connection never creates estimated time.
|
|
7
|
+
export function usageMeter(pool) {
|
|
8
|
+
async function action(connection, computerId, clientId, kind, key) {
|
|
9
|
+
if (!connection.userId || !['message', 'turn'].includes(kind) || typeof key !== 'string' || !key || key.length > 128) return;
|
|
10
|
+
const column = kind === 'message' ? 'message_count' : 'turn_count';
|
|
11
|
+
const db = await pool.getConnection();
|
|
12
|
+
try {
|
|
13
|
+
await db.beginTransaction();
|
|
14
|
+
const [users] = await db.execute("SELECT id FROM users WHERE id=? AND status='active' FOR UPDATE", [connection.userId]);
|
|
15
|
+
if (!users.length) { await db.commit(); return false; }
|
|
16
|
+
const [result] = await db.execute(`INSERT IGNORE INTO relay_usage_actions
|
|
17
|
+
(computer_id,kind,action_key,user_id,client_id,occurred_at)
|
|
18
|
+
VALUES (?,?,?,?,?,UTC_TIMESTAMP(3))`,
|
|
19
|
+
[computerId, kind, key, connection.userId, clientId]);
|
|
20
|
+
if (result.affectedRows === 1) {
|
|
21
|
+
await db.execute(`INSERT INTO relay_usage_totals (user_id,${column},updated_at)
|
|
22
|
+
VALUES (?,1,UTC_TIMESTAMP(3)) ON DUPLICATE KEY UPDATE ${column}=${column}+1,
|
|
23
|
+
updated_at=VALUES(updated_at)`, [connection.userId]);
|
|
24
|
+
await db.execute(`INSERT INTO relay_usage_computer_totals (computer_id,${column},updated_at)
|
|
25
|
+
VALUES (?,1,UTC_TIMESTAMP(3)) ON DUPLICATE KEY UPDATE ${column}=${column}+1,
|
|
26
|
+
updated_at=VALUES(updated_at)`, [computerId]);
|
|
27
|
+
}
|
|
28
|
+
await db.commit();
|
|
29
|
+
return result.affectedRows === 1;
|
|
30
|
+
} catch (error) { await db.rollback(); throw error; }
|
|
31
|
+
finally { db.release(); }
|
|
32
|
+
}
|
|
33
|
+
async function turnOrigin(computerId, key) {
|
|
34
|
+
const [rows] = await pool.execute(`SELECT a.user_id,a.client_id FROM relay_usage_actions a JOIN users u ON u.id=a.user_id
|
|
35
|
+
WHERE a.computer_id=? AND a.kind='turn' AND a.action_key=? AND u.status='active'`, [computerId, key]);
|
|
36
|
+
return rows[0] ? { userId: rows[0].user_id, clientId: rows[0].client_id } : null;
|
|
37
|
+
}
|
|
38
|
+
async function pulse(connection, active, computerId, clientId, externalKey = null) {
|
|
39
|
+
if (!connection.userId) return;
|
|
40
|
+
if (!active && !connection.usageSessionId) return;
|
|
41
|
+
if (active && !connection.usageSessionId) {
|
|
42
|
+
const id = randomUUID();
|
|
43
|
+
const [inserted] = await pool.execute(`INSERT INTO relay_usage_sessions
|
|
44
|
+
(id,kind,external_key,user_id,computer_id,client_id,started_at,confirmed_until,charged_seconds)
|
|
45
|
+
SELECT ?,'task',?,?,?,?,UTC_TIMESTAMP(3),UTC_TIMESTAMP(3),0 FROM users WHERE id=? AND status='active'`, [id, externalKey, connection.userId, computerId, clientId, connection.userId]);
|
|
46
|
+
if (!inserted.affectedRows) return;
|
|
47
|
+
connection.usageSessionId = id;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const id = connection.usageSessionId;
|
|
51
|
+
const db = await pool.getConnection();
|
|
52
|
+
try {
|
|
53
|
+
await db.beginTransaction();
|
|
54
|
+
const [users] = await db.execute("SELECT id FROM users WHERE id=? AND status='active' FOR UPDATE", [connection.userId]);
|
|
55
|
+
if (!users.length) { await db.commit(); connection.usageSessionId = undefined; return; }
|
|
56
|
+
const [rows] = await db.execute('SELECT user_id,computer_id,confirmed_until,ended_at FROM relay_usage_sessions WHERE id=? FOR UPDATE', [id]);
|
|
57
|
+
const row = rows[0];
|
|
58
|
+
if (!row || row.ended_at) throw new Error('Invalid relay usage session');
|
|
59
|
+
const [clock] = await db.query('SELECT UTC_TIMESTAMP(3) AS now');
|
|
60
|
+
const now = clock[0].now;
|
|
61
|
+
const nowMs = epoch(now);
|
|
62
|
+
const chargeStart = Math.max(epoch(row.confirmed_until), nowMs - 20000);
|
|
63
|
+
const seconds = Math.max(0, nowMs - chargeStart) / 1000;
|
|
64
|
+
const [plans] = await db.execute('SELECT tier,valid_until FROM relay_usage_entitlements WHERE user_id=?', [row.user_id]);
|
|
65
|
+
const subscribed = plans[0]?.tier === 'subscriber' && plans[0].valid_until && epoch(plans[0].valid_until) > epoch(now);
|
|
66
|
+
const freeSeconds = subscribed ? 0 : seconds;
|
|
67
|
+
await db.execute(`UPDATE relay_usage_sessions SET confirmed_until=?,ended_at=?,charged_seconds=charged_seconds+?,
|
|
68
|
+
free_charged_seconds=free_charged_seconds+? WHERE id=?`, [now, active ? null : now, seconds, freeSeconds, id]);
|
|
69
|
+
if (seconds > 0) {
|
|
70
|
+
await recordDailyUsage(db, row.user_id, row.computer_id, !subscribed,
|
|
71
|
+
chargeStart, nowMs, now);
|
|
72
|
+
await db.execute(`INSERT INTO relay_usage_totals (user_id,task_seconds,updated_at)
|
|
73
|
+
VALUES (?,?,?) ON DUPLICATE KEY UPDATE task_seconds=task_seconds+VALUES(task_seconds),
|
|
74
|
+
updated_at=VALUES(updated_at)`, [row.user_id, seconds, now]);
|
|
75
|
+
await db.execute(`INSERT INTO relay_usage_computer_totals (computer_id,task_seconds,free_task_seconds,updated_at)
|
|
76
|
+
VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE task_seconds=task_seconds+VALUES(task_seconds),
|
|
77
|
+
free_task_seconds=free_task_seconds+VALUES(free_task_seconds),updated_at=VALUES(updated_at)`,
|
|
78
|
+
[row.computer_id, seconds, freeSeconds, now]);
|
|
79
|
+
}
|
|
80
|
+
await db.commit();
|
|
81
|
+
if (!active) connection.usageSessionId = undefined;
|
|
82
|
+
} catch (error) { await db.rollback(); throw error; }
|
|
83
|
+
finally { db.release(); }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function lost(connection) {
|
|
87
|
+
const id = connection.usageSessionId;
|
|
88
|
+
connection.usageSessionId = undefined;
|
|
89
|
+
if (!id) return;
|
|
90
|
+
await pool.execute(`UPDATE relay_usage_sessions SET ended_at=confirmed_until
|
|
91
|
+
WHERE id=? AND ended_at IS NULL`, [id]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { action, turnOrigin, pulse, lost };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function usageReport(pool, page, view = 'user') {
|
|
98
|
+
if (view === 'computer') {
|
|
99
|
+
const offset = (page - 1) * 30;
|
|
100
|
+
const [counts] = await pool.query(`SELECT COUNT(*) AS total FROM relay_usage_computer_days
|
|
101
|
+
WHERE usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND task_seconds>0`);
|
|
102
|
+
const [rows] = await pool.execute(`SELECT c.id,
|
|
103
|
+
d.task_seconds AS today_task_seconds,
|
|
104
|
+
d.free_task_seconds AS today_free_task_seconds,
|
|
105
|
+
COALESCE(a.message_count,0) AS message_count,
|
|
106
|
+
COALESCE(a.turn_count,0) AS turn_count,
|
|
107
|
+
COALESCE(GREATEST(s.last_used_at,a.last_used_at),s.last_used_at,a.last_used_at) AS last_used_at
|
|
108
|
+
FROM relay_usage_computer_days d JOIN computers c ON c.id=d.computer_id
|
|
109
|
+
LEFT JOIN (SELECT computer_id,SUM(kind='message') AS message_count,
|
|
110
|
+
SUM(kind='turn') AS turn_count,MAX(occurred_at) AS last_used_at FROM relay_usage_actions
|
|
111
|
+
WHERE occurred_at >= DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) - INTERVAL 8 HOUR
|
|
112
|
+
AND occurred_at < DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) + INTERVAL 16 HOUR
|
|
113
|
+
GROUP BY computer_id) a ON a.computer_id=c.id
|
|
114
|
+
LEFT JOIN (SELECT computer_id,MAX(confirmed_until) AS last_used_at
|
|
115
|
+
FROM relay_usage_sessions GROUP BY computer_id) s ON s.computer_id=c.id
|
|
116
|
+
WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0
|
|
117
|
+
ORDER BY d.task_seconds DESC,c.id LIMIT ? OFFSET ?`, [30, offset]);
|
|
118
|
+
return { page, pageSize: 30, total: counts[0].total, rows };
|
|
119
|
+
}
|
|
120
|
+
const offset = (page - 1) * 30;
|
|
121
|
+
const [counts] = await pool.query(`SELECT COUNT(*) AS total FROM relay_usage_user_days d
|
|
122
|
+
JOIN user_login_identities i ON i.user_id=d.user_id AND i.kind='email'
|
|
123
|
+
WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0`);
|
|
124
|
+
const [rows] = await pool.execute(`SELECT u.id,i.normalized_value AS email,
|
|
125
|
+
d.task_seconds AS today_task_seconds,
|
|
126
|
+
COALESCE(a.message_count,0) AS message_count,
|
|
127
|
+
COALESCE(a.turn_count,0) AS turn_count,
|
|
128
|
+
COALESCE(GREATEST(s.last_used_at,a.last_used_at),s.last_used_at,a.last_used_at) AS last_used_at
|
|
129
|
+
FROM relay_usage_user_days d JOIN users u ON u.id=d.user_id
|
|
130
|
+
JOIN user_login_identities i ON i.user_id=u.id AND i.kind='email'
|
|
131
|
+
LEFT JOIN (SELECT user_id,SUM(kind='message') AS message_count,
|
|
132
|
+
SUM(kind='turn') AS turn_count,MAX(occurred_at) AS last_used_at FROM relay_usage_actions
|
|
133
|
+
WHERE occurred_at >= DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) - INTERVAL 8 HOUR
|
|
134
|
+
AND occurred_at < DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) + INTERVAL 16 HOUR
|
|
135
|
+
GROUP BY user_id) a ON a.user_id=u.id
|
|
136
|
+
LEFT JOIN (SELECT user_id,MAX(confirmed_until) AS last_used_at
|
|
137
|
+
FROM relay_usage_sessions GROUP BY user_id) s ON s.user_id=u.id
|
|
138
|
+
WHERE d.usage_date=DATE(UTC_TIMESTAMP() + INTERVAL 8 HOUR) AND d.task_seconds>0
|
|
139
|
+
ORDER BY d.task_seconds DESC,u.id LIMIT ? OFFSET ?`, [30, offset]);
|
|
140
|
+
return { page, pageSize: 30, total: counts[0].total, rows };
|
|
141
|
+
}
|
package/dist/src/server.mjs
CHANGED
|
@@ -299,8 +299,9 @@ async function handleRequest(request, response) {
|
|
|
299
299
|
return;
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
if (request.method === 'GET' && url.pathname === '/api/turn-events') {
|
|
303
|
-
|
|
302
|
+
if (request.method === 'GET' && url.pathname === '/api/turn-events') {
|
|
303
|
+
const execution = sharedServer;
|
|
304
|
+
response.startEvents();
|
|
304
305
|
const listener = ({ method, params }) => {
|
|
305
306
|
if (!['turn/started', 'turn/completed'].includes(method)) return;
|
|
306
307
|
const threadId = params?.threadId, turnId = params?.turn?.id;
|
|
@@ -309,11 +310,11 @@ async function handleRequest(request, response) {
|
|
|
309
310
|
}
|
|
310
311
|
};
|
|
311
312
|
const disconnected = () => response.end();
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
response.once('close', () => {
|
|
315
|
-
|
|
316
|
-
|
|
313
|
+
execution.on('notification', listener);
|
|
314
|
+
execution.on('disconnected', disconnected);
|
|
315
|
+
response.once('close', () => {
|
|
316
|
+
execution.off('notification', listener);
|
|
317
|
+
execution.off('disconnected', disconnected);
|
|
317
318
|
});
|
|
318
319
|
return;
|
|
319
320
|
}
|
|
@@ -375,12 +376,13 @@ async function handleRequest(request, response) {
|
|
|
375
376
|
}
|
|
376
377
|
|
|
377
378
|
const timelineMatch = /^\/api\/threads\/([^/]+)\/timeline$/.exec(url.pathname);
|
|
378
|
-
if (request.method === 'GET' && timelineMatch) {
|
|
379
|
-
const threadId = decodeURIComponent(timelineMatch[1]);
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
response.
|
|
379
|
+
if (request.method === 'GET' && timelineMatch) {
|
|
380
|
+
const threadId = decodeURIComponent(timelineMatch[1]);
|
|
381
|
+
const execution = sharedServer;
|
|
382
|
+
const timeline = normalizeSharedTimeline(await execution.timeline(threadId, sharedHistoryPageOptions()));
|
|
383
|
+
if (request.socketRequest) {
|
|
384
|
+
response.on('progress', () => execution.keepSnapshotAlive(threadId, timeline.sync));
|
|
385
|
+
response.once('close', () => execution.keepSnapshotAlive(threadId, timeline.sync));
|
|
384
386
|
}
|
|
385
387
|
sendJson(response, 200, timeline);
|
|
386
388
|
return;
|