@xtr-dev/rondevu-server 0.0.1 → 0.1.1
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/API.md +39 -9
- package/CLAUDE.md +47 -0
- package/README.md +144 -187
- package/build.js +12 -0
- package/dist/index.js +799 -266
- package/dist/index.js.map +4 -4
- package/migrations/0001_add_peer_id.sql +21 -0
- package/migrations/0002_remove_topics.sql +22 -0
- package/migrations/0003_remove_origin.sql +29 -0
- package/migrations/0004_add_secret.sql +4 -0
- package/migrations/schema.sql +18 -0
- package/package.json +4 -3
- package/src/app.ts +421 -127
- package/src/bloom.ts +66 -0
- package/src/config.ts +27 -2
- package/src/crypto.ts +149 -0
- package/src/index.ts +28 -12
- package/src/middleware/auth.ts +51 -0
- package/src/storage/d1.ts +394 -0
- package/src/storage/hash-id.ts +37 -0
- package/src/storage/sqlite.ts +323 -178
- package/src/storage/types.ts +128 -54
- package/src/worker.ts +51 -16
- package/wrangler.toml +45 -0
- package/DEPLOYMENT.md +0 -346
- package/src/storage/kv.ts +0 -241
package/src/storage/kv.ts
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { Storage, Session } from './types.ts';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Cloudflare KV storage adapter for session management
|
|
5
|
-
*/
|
|
6
|
-
export class KVStorage implements Storage {
|
|
7
|
-
private kv: KVNamespace;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Creates a new KV storage instance
|
|
11
|
-
* @param kv Cloudflare KV namespace binding
|
|
12
|
-
*/
|
|
13
|
-
constructor(kv: KVNamespace) {
|
|
14
|
-
this.kv = kv;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Generates a unique code using Web Crypto API
|
|
19
|
-
*/
|
|
20
|
-
private generateCode(): string {
|
|
21
|
-
return crypto.randomUUID();
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Gets the key for storing a session
|
|
26
|
-
*/
|
|
27
|
-
private sessionKey(code: string): string {
|
|
28
|
-
return `session:${code}`;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Gets the key for the topic index
|
|
33
|
-
*/
|
|
34
|
-
private topicIndexKey(origin: string, topic: string): string {
|
|
35
|
-
return `index:${origin}:${topic}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async createSession(origin: string, topic: string, info: string, offer: string, expiresAt: number): Promise<string> {
|
|
39
|
-
// Validate info length
|
|
40
|
-
if (info.length > 1024) {
|
|
41
|
-
throw new Error('Info string must be 1024 characters or less');
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const code = this.generateCode();
|
|
45
|
-
const createdAt = Date.now();
|
|
46
|
-
|
|
47
|
-
const session: Session = {
|
|
48
|
-
code,
|
|
49
|
-
origin,
|
|
50
|
-
topic,
|
|
51
|
-
info,
|
|
52
|
-
offer,
|
|
53
|
-
answer: undefined,
|
|
54
|
-
offerCandidates: [],
|
|
55
|
-
answerCandidates: [],
|
|
56
|
-
createdAt,
|
|
57
|
-
expiresAt,
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
// Calculate TTL in seconds for KV
|
|
61
|
-
const ttl = Math.max(60, Math.floor((expiresAt - createdAt) / 1000));
|
|
62
|
-
|
|
63
|
-
// Store the session
|
|
64
|
-
await this.kv.put(
|
|
65
|
-
this.sessionKey(code),
|
|
66
|
-
JSON.stringify(session),
|
|
67
|
-
{ expirationTtl: ttl }
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
// Update the topic index
|
|
71
|
-
const indexKey = this.topicIndexKey(origin, topic);
|
|
72
|
-
const existingIndex = await this.kv.get(indexKey, 'json') as string[] | null;
|
|
73
|
-
const updatedIndex = existingIndex ? [...existingIndex, code] : [code];
|
|
74
|
-
|
|
75
|
-
// Set index TTL to slightly longer than session TTL to avoid race conditions
|
|
76
|
-
await this.kv.put(
|
|
77
|
-
indexKey,
|
|
78
|
-
JSON.stringify(updatedIndex),
|
|
79
|
-
{ expirationTtl: ttl + 300 }
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
return code;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async listSessionsByTopic(origin: string, topic: string): Promise<Session[]> {
|
|
86
|
-
const indexKey = this.topicIndexKey(origin, topic);
|
|
87
|
-
const codes = await this.kv.get(indexKey, 'json') as string[] | null;
|
|
88
|
-
|
|
89
|
-
if (!codes || codes.length === 0) {
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Fetch all sessions in parallel
|
|
94
|
-
const sessionPromises = codes.map(async (code) => {
|
|
95
|
-
const sessionData = await this.kv.get(this.sessionKey(code), 'json') as Session | null;
|
|
96
|
-
return sessionData;
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const sessions = await Promise.all(sessionPromises);
|
|
100
|
-
|
|
101
|
-
// Filter out expired or answered sessions, and null values
|
|
102
|
-
const now = Date.now();
|
|
103
|
-
const validSessions = sessions.filter(
|
|
104
|
-
(session): session is Session =>
|
|
105
|
-
session !== null &&
|
|
106
|
-
session.expiresAt > now &&
|
|
107
|
-
session.answer === undefined
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
// Sort by creation time (newest first)
|
|
111
|
-
return validSessions.sort((a, b) => b.createdAt - a.createdAt);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async listTopics(origin: string, page: number, limit: number): Promise<{
|
|
115
|
-
topics: Array<{ topic: string; count: number }>;
|
|
116
|
-
pagination: {
|
|
117
|
-
page: number;
|
|
118
|
-
limit: number;
|
|
119
|
-
total: number;
|
|
120
|
-
hasMore: boolean;
|
|
121
|
-
};
|
|
122
|
-
}> {
|
|
123
|
-
// Ensure limit doesn't exceed 1000
|
|
124
|
-
const safeLimit = Math.min(Math.max(1, limit), 1000);
|
|
125
|
-
const safePage = Math.max(1, page);
|
|
126
|
-
|
|
127
|
-
const prefix = `index:${origin}:`;
|
|
128
|
-
const topicCounts = new Map<string, number>();
|
|
129
|
-
|
|
130
|
-
// List all index keys for this origin
|
|
131
|
-
const list = await this.kv.list({ prefix });
|
|
132
|
-
|
|
133
|
-
// Process each topic index
|
|
134
|
-
for (const key of list.keys) {
|
|
135
|
-
// Extract topic from key: "index:{origin}:{topic}"
|
|
136
|
-
const topic = key.name.substring(prefix.length);
|
|
137
|
-
|
|
138
|
-
// Get the session codes for this topic
|
|
139
|
-
const codes = await this.kv.get(key.name, 'json') as string[] | null;
|
|
140
|
-
|
|
141
|
-
if (!codes || codes.length === 0) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Fetch sessions to count only valid ones (unexpired and unanswered)
|
|
146
|
-
const sessionPromises = codes.map(async (code) => {
|
|
147
|
-
const sessionData = await this.kv.get(this.sessionKey(code), 'json') as Session | null;
|
|
148
|
-
return sessionData;
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const sessions = await Promise.all(sessionPromises);
|
|
152
|
-
|
|
153
|
-
// Count valid sessions
|
|
154
|
-
const now = Date.now();
|
|
155
|
-
const validCount = sessions.filter(
|
|
156
|
-
(session) =>
|
|
157
|
-
session !== null &&
|
|
158
|
-
session.expiresAt > now &&
|
|
159
|
-
session.answer === undefined
|
|
160
|
-
).length;
|
|
161
|
-
|
|
162
|
-
if (validCount > 0) {
|
|
163
|
-
topicCounts.set(topic, validCount);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Convert to array and sort by topic name
|
|
168
|
-
const allTopics = Array.from(topicCounts.entries())
|
|
169
|
-
.map(([topic, count]) => ({ topic, count }))
|
|
170
|
-
.sort((a, b) => a.topic.localeCompare(b.topic));
|
|
171
|
-
|
|
172
|
-
// Apply pagination
|
|
173
|
-
const total = allTopics.length;
|
|
174
|
-
const offset = (safePage - 1) * safeLimit;
|
|
175
|
-
const topics = allTopics.slice(offset, offset + safeLimit);
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
topics,
|
|
179
|
-
pagination: {
|
|
180
|
-
page: safePage,
|
|
181
|
-
limit: safeLimit,
|
|
182
|
-
total,
|
|
183
|
-
hasMore: offset + topics.length < total,
|
|
184
|
-
},
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
async getSession(code: string, origin: string): Promise<Session | null> {
|
|
189
|
-
const sessionData = await this.kv.get(this.sessionKey(code), 'json') as Session | null;
|
|
190
|
-
|
|
191
|
-
if (!sessionData) {
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Validate origin and expiration
|
|
196
|
-
if (sessionData.origin !== origin || sessionData.expiresAt <= Date.now()) {
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return sessionData;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async updateSession(code: string, origin: string, update: Partial<Session>): Promise<void> {
|
|
204
|
-
const current = await this.getSession(code, origin);
|
|
205
|
-
|
|
206
|
-
if (!current) {
|
|
207
|
-
throw new Error('Session not found or origin mismatch');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// Merge updates
|
|
211
|
-
const updated: Session = {
|
|
212
|
-
...current,
|
|
213
|
-
...(update.answer !== undefined && { answer: update.answer }),
|
|
214
|
-
...(update.offerCandidates !== undefined && { offerCandidates: update.offerCandidates }),
|
|
215
|
-
...(update.answerCandidates !== undefined && { answerCandidates: update.answerCandidates }),
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
// Calculate remaining TTL
|
|
219
|
-
const ttl = Math.max(60, Math.floor((updated.expiresAt - Date.now()) / 1000));
|
|
220
|
-
|
|
221
|
-
// Update the session
|
|
222
|
-
await this.kv.put(
|
|
223
|
-
this.sessionKey(code),
|
|
224
|
-
JSON.stringify(updated),
|
|
225
|
-
{ expirationTtl: ttl }
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async deleteSession(code: string): Promise<void> {
|
|
230
|
-
await this.kv.delete(this.sessionKey(code));
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
async cleanup(): Promise<void> {
|
|
234
|
-
// KV automatically expires keys based on TTL
|
|
235
|
-
// No manual cleanup needed
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
async close(): Promise<void> {
|
|
239
|
-
// No connection to close for KV
|
|
240
|
-
}
|
|
241
|
-
}
|