@xtr-dev/rondevu-server 0.5.13 → 0.5.14
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/index.js +932 -1110
- package/dist/index.js.map +4 -4
- package/package.json +1 -1
- package/wrangler.toml +1 -1
package/dist/index.js
CHANGED
|
@@ -29,320 +29,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
29
29
|
mod
|
|
30
30
|
));
|
|
31
31
|
|
|
32
|
-
// src/crypto.ts
|
|
33
|
-
var crypto_exports = {};
|
|
34
|
-
__export(crypto_exports, {
|
|
35
|
-
buildSignatureMessage: () => buildSignatureMessage,
|
|
36
|
-
decryptSecret: () => decryptSecret,
|
|
37
|
-
encryptSecret: () => encryptSecret,
|
|
38
|
-
generateCredentialName: () => generateCredentialName,
|
|
39
|
-
generateSecret: () => generateSecret,
|
|
40
|
-
generateSignature: () => generateSignature,
|
|
41
|
-
validateTag: () => validateTag,
|
|
42
|
-
validateTags: () => validateTags,
|
|
43
|
-
validateUsername: () => validateUsername,
|
|
44
|
-
verifySignature: () => verifySignature
|
|
45
|
-
});
|
|
46
|
-
function generateCredentialName() {
|
|
47
|
-
const adjectives = [
|
|
48
|
-
"brave",
|
|
49
|
-
"calm",
|
|
50
|
-
"eager",
|
|
51
|
-
"fancy",
|
|
52
|
-
"gentle",
|
|
53
|
-
"happy",
|
|
54
|
-
"jolly",
|
|
55
|
-
"kind",
|
|
56
|
-
"lively",
|
|
57
|
-
"merry",
|
|
58
|
-
"nice",
|
|
59
|
-
"proud",
|
|
60
|
-
"quiet",
|
|
61
|
-
"swift",
|
|
62
|
-
"witty",
|
|
63
|
-
"young",
|
|
64
|
-
"bright",
|
|
65
|
-
"clever",
|
|
66
|
-
"daring",
|
|
67
|
-
"fair",
|
|
68
|
-
"grand",
|
|
69
|
-
"humble",
|
|
70
|
-
"noble",
|
|
71
|
-
"quick"
|
|
72
|
-
];
|
|
73
|
-
const nouns = [
|
|
74
|
-
"tiger",
|
|
75
|
-
"eagle",
|
|
76
|
-
"river",
|
|
77
|
-
"mountain",
|
|
78
|
-
"ocean",
|
|
79
|
-
"forest",
|
|
80
|
-
"desert",
|
|
81
|
-
"valley",
|
|
82
|
-
"thunder",
|
|
83
|
-
"wind",
|
|
84
|
-
"fire",
|
|
85
|
-
"stone",
|
|
86
|
-
"cloud",
|
|
87
|
-
"star",
|
|
88
|
-
"moon",
|
|
89
|
-
"sun",
|
|
90
|
-
"wolf",
|
|
91
|
-
"bear",
|
|
92
|
-
"hawk",
|
|
93
|
-
"lion",
|
|
94
|
-
"fox",
|
|
95
|
-
"deer",
|
|
96
|
-
"owl",
|
|
97
|
-
"swan"
|
|
98
|
-
];
|
|
99
|
-
const adjective = adjectives[Math.floor(Math.random() * adjectives.length)];
|
|
100
|
-
const noun = nouns[Math.floor(Math.random() * nouns.length)];
|
|
101
|
-
const random = crypto.getRandomValues(new Uint8Array(8));
|
|
102
|
-
const hex = Array.from(random).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
103
|
-
return `${adjective}-${noun}-${hex}`;
|
|
104
|
-
}
|
|
105
|
-
function generateSecret() {
|
|
106
|
-
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
|
107
|
-
const secret = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
108
|
-
if (secret.length !== 64) {
|
|
109
|
-
throw new Error("Secret generation failed: invalid length");
|
|
110
|
-
}
|
|
111
|
-
for (let i = 0; i < secret.length; i++) {
|
|
112
|
-
const c = secret[i];
|
|
113
|
-
if ((c < "0" || c > "9") && (c < "a" || c > "f")) {
|
|
114
|
-
throw new Error(`Secret generation failed: invalid hex character at position ${i}: '${c}'`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return secret;
|
|
118
|
-
}
|
|
119
|
-
function hexToBytes(hex) {
|
|
120
|
-
if (hex.length % 2 !== 0) {
|
|
121
|
-
throw new Error("Hex string must have even length");
|
|
122
|
-
}
|
|
123
|
-
for (let i = 0; i < hex.length; i++) {
|
|
124
|
-
const c = hex[i].toLowerCase();
|
|
125
|
-
if ((c < "0" || c > "9") && (c < "a" || c > "f")) {
|
|
126
|
-
throw new Error(`Invalid hex character at position ${i}: '${hex[i]}'`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
const match = hex.match(/.{1,2}/g);
|
|
130
|
-
if (!match) {
|
|
131
|
-
throw new Error("Invalid hex string format");
|
|
132
|
-
}
|
|
133
|
-
return new Uint8Array(match.map((byte) => {
|
|
134
|
-
const parsed = parseInt(byte, 16);
|
|
135
|
-
if (isNaN(parsed)) {
|
|
136
|
-
throw new Error(`Invalid hex byte: ${byte}`);
|
|
137
|
-
}
|
|
138
|
-
return parsed;
|
|
139
|
-
}));
|
|
140
|
-
}
|
|
141
|
-
async function encryptSecret(secret, masterKeyHex) {
|
|
142
|
-
if (!masterKeyHex || masterKeyHex.length !== 64) {
|
|
143
|
-
throw new Error("Master key must be 64-character hex string (32 bytes)");
|
|
144
|
-
}
|
|
145
|
-
const keyBytes = hexToBytes(masterKeyHex);
|
|
146
|
-
const key = await crypto.subtle.importKey(
|
|
147
|
-
"raw",
|
|
148
|
-
keyBytes,
|
|
149
|
-
{ name: "AES-GCM", length: 256 },
|
|
150
|
-
false,
|
|
151
|
-
["encrypt"]
|
|
152
|
-
);
|
|
153
|
-
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
154
|
-
const encoder = new TextEncoder();
|
|
155
|
-
const secretBytes = encoder.encode(secret);
|
|
156
|
-
const ciphertext = await crypto.subtle.encrypt(
|
|
157
|
-
{ name: "AES-GCM", iv, tagLength: 128 },
|
|
158
|
-
key,
|
|
159
|
-
secretBytes
|
|
160
|
-
);
|
|
161
|
-
const ivHex = Array.from(iv).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
162
|
-
const ciphertextHex = Array.from(new Uint8Array(ciphertext)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
163
|
-
return `${ivHex}:${ciphertextHex}`;
|
|
164
|
-
}
|
|
165
|
-
async function decryptSecret(encryptedSecret, masterKeyHex) {
|
|
166
|
-
if (!masterKeyHex || masterKeyHex.length !== 64) {
|
|
167
|
-
throw new Error("Master key must be 64-character hex string (32 bytes)");
|
|
168
|
-
}
|
|
169
|
-
const parts = encryptedSecret.split(":");
|
|
170
|
-
if (parts.length !== 2) {
|
|
171
|
-
throw new Error("Invalid encrypted secret format (expected iv:ciphertext)");
|
|
172
|
-
}
|
|
173
|
-
const [ivHex, ciphertextHex] = parts;
|
|
174
|
-
if (ivHex.length !== 24) {
|
|
175
|
-
throw new Error("Invalid IV length (expected 12 bytes = 24 hex characters)");
|
|
176
|
-
}
|
|
177
|
-
if (ciphertextHex.length < 32) {
|
|
178
|
-
throw new Error("Invalid ciphertext length (must include 16-byte auth tag)");
|
|
179
|
-
}
|
|
180
|
-
const iv = hexToBytes(ivHex);
|
|
181
|
-
const ciphertext = hexToBytes(ciphertextHex);
|
|
182
|
-
const keyBytes = hexToBytes(masterKeyHex);
|
|
183
|
-
const key = await crypto.subtle.importKey(
|
|
184
|
-
"raw",
|
|
185
|
-
keyBytes,
|
|
186
|
-
{ name: "AES-GCM", length: 256 },
|
|
187
|
-
false,
|
|
188
|
-
["decrypt"]
|
|
189
|
-
);
|
|
190
|
-
const decryptedBytes = await crypto.subtle.decrypt(
|
|
191
|
-
{ name: "AES-GCM", iv, tagLength: 128 },
|
|
192
|
-
key,
|
|
193
|
-
ciphertext
|
|
194
|
-
);
|
|
195
|
-
const decoder = new TextDecoder();
|
|
196
|
-
return decoder.decode(decryptedBytes);
|
|
197
|
-
}
|
|
198
|
-
async function generateSignature(secret, message) {
|
|
199
|
-
const secretBytes = hexToBytes(secret);
|
|
200
|
-
const key = await crypto.subtle.importKey(
|
|
201
|
-
"raw",
|
|
202
|
-
secretBytes,
|
|
203
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
204
|
-
false,
|
|
205
|
-
["sign"]
|
|
206
|
-
);
|
|
207
|
-
const encoder = new TextEncoder();
|
|
208
|
-
const messageBytes = encoder.encode(message);
|
|
209
|
-
const signatureBytes = await crypto.subtle.sign("HMAC", key, messageBytes);
|
|
210
|
-
return import_node_buffer.Buffer.from(signatureBytes).toString("base64");
|
|
211
|
-
}
|
|
212
|
-
async function verifySignature(secret, message, signature) {
|
|
213
|
-
try {
|
|
214
|
-
const secretBytes = hexToBytes(secret);
|
|
215
|
-
const key = await crypto.subtle.importKey(
|
|
216
|
-
"raw",
|
|
217
|
-
secretBytes,
|
|
218
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
219
|
-
false,
|
|
220
|
-
["verify"]
|
|
221
|
-
);
|
|
222
|
-
const encoder = new TextEncoder();
|
|
223
|
-
const messageBytes = encoder.encode(message);
|
|
224
|
-
const signatureBytes = import_node_buffer.Buffer.from(signature, "base64");
|
|
225
|
-
return await crypto.subtle.verify("HMAC", key, signatureBytes, messageBytes);
|
|
226
|
-
} catch (error) {
|
|
227
|
-
console.error("Signature verification error:", error);
|
|
228
|
-
return false;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
function canonicalJSON(obj, depth = 0) {
|
|
232
|
-
const MAX_DEPTH = 100;
|
|
233
|
-
if (depth > MAX_DEPTH) {
|
|
234
|
-
throw new Error("Object nesting too deep for canonicalization");
|
|
235
|
-
}
|
|
236
|
-
if (obj === null) return "null";
|
|
237
|
-
if (obj === void 0) return JSON.stringify(void 0);
|
|
238
|
-
const type = typeof obj;
|
|
239
|
-
if (type === "function") throw new Error("Functions are not supported in RPC parameters");
|
|
240
|
-
if (type === "symbol" || type === "bigint") throw new Error(`${type} is not supported in RPC parameters`);
|
|
241
|
-
if (type === "number" && !Number.isFinite(obj)) throw new Error("NaN and Infinity are not supported in RPC parameters");
|
|
242
|
-
if (type !== "object") return JSON.stringify(obj);
|
|
243
|
-
if (Array.isArray(obj)) {
|
|
244
|
-
return "[" + obj.map((item) => canonicalJSON(item, depth + 1)).join(",") + "]";
|
|
245
|
-
}
|
|
246
|
-
const sortedKeys = Object.keys(obj).sort();
|
|
247
|
-
const pairs = sortedKeys.map((key) => JSON.stringify(key) + ":" + canonicalJSON(obj[key], depth + 1));
|
|
248
|
-
return "{" + pairs.join(",") + "}";
|
|
249
|
-
}
|
|
250
|
-
function buildSignatureMessage(timestamp, nonce, method, params) {
|
|
251
|
-
if (nonce.length !== 36) {
|
|
252
|
-
throw new Error("Nonce must be a valid UUID v4 (use crypto.randomUUID())");
|
|
253
|
-
}
|
|
254
|
-
if (nonce[8] !== "-" || nonce[13] !== "-" || nonce[18] !== "-" || nonce[23] !== "-") {
|
|
255
|
-
throw new Error("Nonce must be a valid UUID v4 (use crypto.randomUUID())");
|
|
256
|
-
}
|
|
257
|
-
if (nonce[14] !== "4") {
|
|
258
|
-
throw new Error("Nonce must be a valid UUID v4 (use crypto.randomUUID())");
|
|
259
|
-
}
|
|
260
|
-
const variant = nonce[19].toLowerCase();
|
|
261
|
-
if (variant !== "8" && variant !== "9" && variant !== "a" && variant !== "b") {
|
|
262
|
-
throw new Error("Nonce must be a valid UUID v4 (use crypto.randomUUID())");
|
|
263
|
-
}
|
|
264
|
-
const hexChars = nonce.replace(/-/g, "");
|
|
265
|
-
for (let i = 0; i < hexChars.length; i++) {
|
|
266
|
-
const c = hexChars[i].toLowerCase();
|
|
267
|
-
if ((c < "0" || c > "9") && (c < "a" || c > "f")) {
|
|
268
|
-
throw new Error("Nonce must be a valid UUID v4 (use crypto.randomUUID())");
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
const paramsStr = canonicalJSON(params || {});
|
|
272
|
-
return `${timestamp}:${nonce}:${method}:${paramsStr}`;
|
|
273
|
-
}
|
|
274
|
-
function validateUsername(username) {
|
|
275
|
-
if (typeof username !== "string") {
|
|
276
|
-
return { valid: false, error: "Username must be a string" };
|
|
277
|
-
}
|
|
278
|
-
if (username.length < USERNAME_MIN_LENGTH) {
|
|
279
|
-
return { valid: false, error: `Username must be at least ${USERNAME_MIN_LENGTH} characters` };
|
|
280
|
-
}
|
|
281
|
-
if (username.length > USERNAME_MAX_LENGTH) {
|
|
282
|
-
return { valid: false, error: `Username must be at most ${USERNAME_MAX_LENGTH} characters` };
|
|
283
|
-
}
|
|
284
|
-
if (!USERNAME_REGEX.test(username)) {
|
|
285
|
-
return { valid: false, error: "Username must be lowercase alphanumeric with optional dashes/periods, and start/end with alphanumeric" };
|
|
286
|
-
}
|
|
287
|
-
return { valid: true };
|
|
288
|
-
}
|
|
289
|
-
function validateTag(tag) {
|
|
290
|
-
if (typeof tag !== "string") {
|
|
291
|
-
return { valid: false, error: "Tag must be a string" };
|
|
292
|
-
}
|
|
293
|
-
if (tag.length < TAG_MIN_LENGTH) {
|
|
294
|
-
return { valid: false, error: `Tag must be at least ${TAG_MIN_LENGTH} character` };
|
|
295
|
-
}
|
|
296
|
-
if (tag.length > TAG_MAX_LENGTH) {
|
|
297
|
-
return { valid: false, error: `Tag must be at most ${TAG_MAX_LENGTH} characters` };
|
|
298
|
-
}
|
|
299
|
-
if (tag.length === 1) {
|
|
300
|
-
if (!/^[a-z0-9]$/.test(tag)) {
|
|
301
|
-
return { valid: false, error: "Tag must be lowercase alphanumeric" };
|
|
302
|
-
}
|
|
303
|
-
return { valid: true };
|
|
304
|
-
}
|
|
305
|
-
if (!TAG_REGEX.test(tag)) {
|
|
306
|
-
return { valid: false, error: "Tag must be lowercase alphanumeric with optional dots/dashes, and start/end with alphanumeric" };
|
|
307
|
-
}
|
|
308
|
-
return { valid: true };
|
|
309
|
-
}
|
|
310
|
-
function validateTags(tags, maxTags = 20) {
|
|
311
|
-
if (!Array.isArray(tags)) {
|
|
312
|
-
return { valid: false, error: "Tags must be an array" };
|
|
313
|
-
}
|
|
314
|
-
if (tags.length === 0) {
|
|
315
|
-
return { valid: false, error: "At least one tag is required" };
|
|
316
|
-
}
|
|
317
|
-
if (tags.length > maxTags) {
|
|
318
|
-
return { valid: false, error: `Maximum ${maxTags} tags allowed` };
|
|
319
|
-
}
|
|
320
|
-
for (let i = 0; i < tags.length; i++) {
|
|
321
|
-
const result = validateTag(tags[i]);
|
|
322
|
-
if (!result.valid) {
|
|
323
|
-
return { valid: false, error: `Tag ${i + 1}: ${result.error}` };
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
const uniqueTags = new Set(tags);
|
|
327
|
-
if (uniqueTags.size !== tags.length) {
|
|
328
|
-
return { valid: false, error: "Duplicate tags are not allowed" };
|
|
329
|
-
}
|
|
330
|
-
return { valid: true };
|
|
331
|
-
}
|
|
332
|
-
var import_node_buffer, USERNAME_REGEX, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH, TAG_MIN_LENGTH, TAG_MAX_LENGTH, TAG_REGEX;
|
|
333
|
-
var init_crypto = __esm({
|
|
334
|
-
"src/crypto.ts"() {
|
|
335
|
-
"use strict";
|
|
336
|
-
import_node_buffer = require("node:buffer");
|
|
337
|
-
USERNAME_REGEX = /^[a-z0-9][a-z0-9.-]*[a-z0-9]$/;
|
|
338
|
-
USERNAME_MIN_LENGTH = 4;
|
|
339
|
-
USERNAME_MAX_LENGTH = 32;
|
|
340
|
-
TAG_MIN_LENGTH = 1;
|
|
341
|
-
TAG_MAX_LENGTH = 64;
|
|
342
|
-
TAG_REGEX = /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/;
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
|
|
346
32
|
// src/storage/hash-id.ts
|
|
347
33
|
async function generateOfferHash(sdp) {
|
|
348
34
|
const randomBytes = crypto.getRandomValues(new Uint8Array(8));
|
|
@@ -371,31 +57,28 @@ var memory_exports = {};
|
|
|
371
57
|
__export(memory_exports, {
|
|
372
58
|
MemoryStorage: () => MemoryStorage
|
|
373
59
|
});
|
|
374
|
-
var
|
|
60
|
+
var MemoryStorage;
|
|
375
61
|
var init_memory = __esm({
|
|
376
62
|
"src/storage/memory.ts"() {
|
|
377
63
|
"use strict";
|
|
378
64
|
init_hash_id();
|
|
379
|
-
YEAR_IN_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
380
65
|
MemoryStorage = class {
|
|
381
|
-
constructor(
|
|
66
|
+
constructor() {
|
|
382
67
|
// Primary storage
|
|
383
|
-
this.credentials = /* @__PURE__ */ new Map();
|
|
384
68
|
this.offers = /* @__PURE__ */ new Map();
|
|
385
69
|
this.iceCandidates = /* @__PURE__ */ new Map();
|
|
386
70
|
// offerId → candidates
|
|
387
71
|
this.rateLimits = /* @__PURE__ */ new Map();
|
|
388
72
|
this.nonces = /* @__PURE__ */ new Map();
|
|
389
73
|
// Secondary indexes for efficient lookups
|
|
390
|
-
this.
|
|
391
|
-
//
|
|
74
|
+
this.offersByPublicKey = /* @__PURE__ */ new Map();
|
|
75
|
+
// publicKey → offer IDs
|
|
392
76
|
this.offersByTag = /* @__PURE__ */ new Map();
|
|
393
77
|
// tag → offer IDs
|
|
394
78
|
this.offersByAnswerer = /* @__PURE__ */ new Map();
|
|
395
|
-
// answerer
|
|
79
|
+
// answerer publicKey → offer IDs
|
|
396
80
|
// Auto-increment counter for ICE candidates
|
|
397
81
|
this.iceCandidateIdCounter = 0;
|
|
398
|
-
this.masterEncryptionKey = masterEncryptionKey;
|
|
399
82
|
}
|
|
400
83
|
// ===== Offer Management =====
|
|
401
84
|
async createOffers(offers) {
|
|
@@ -405,7 +88,7 @@ var init_memory = __esm({
|
|
|
405
88
|
const id = request.id || await generateOfferHash(request.sdp);
|
|
406
89
|
const offer = {
|
|
407
90
|
id,
|
|
408
|
-
|
|
91
|
+
publicKey: request.publicKey,
|
|
409
92
|
tags: request.tags,
|
|
410
93
|
sdp: request.sdp,
|
|
411
94
|
createdAt: now,
|
|
@@ -413,10 +96,10 @@ var init_memory = __esm({
|
|
|
413
96
|
lastSeen: now
|
|
414
97
|
};
|
|
415
98
|
this.offers.set(id, offer);
|
|
416
|
-
if (!this.
|
|
417
|
-
this.
|
|
99
|
+
if (!this.offersByPublicKey.has(request.publicKey)) {
|
|
100
|
+
this.offersByPublicKey.set(request.publicKey, /* @__PURE__ */ new Set());
|
|
418
101
|
}
|
|
419
|
-
this.
|
|
102
|
+
this.offersByPublicKey.get(request.publicKey).add(id);
|
|
420
103
|
for (const tag of request.tags) {
|
|
421
104
|
if (!this.offersByTag.has(tag)) {
|
|
422
105
|
this.offersByTag.set(tag, /* @__PURE__ */ new Set());
|
|
@@ -427,9 +110,9 @@ var init_memory = __esm({
|
|
|
427
110
|
}
|
|
428
111
|
return created;
|
|
429
112
|
}
|
|
430
|
-
async
|
|
113
|
+
async getOffersByPublicKey(publicKey) {
|
|
431
114
|
const now = Date.now();
|
|
432
|
-
const offerIds = this.
|
|
115
|
+
const offerIds = this.offersByPublicKey.get(publicKey);
|
|
433
116
|
if (!offerIds) return [];
|
|
434
117
|
const offers = [];
|
|
435
118
|
for (const id of offerIds) {
|
|
@@ -447,9 +130,9 @@ var init_memory = __esm({
|
|
|
447
130
|
}
|
|
448
131
|
return offer;
|
|
449
132
|
}
|
|
450
|
-
async deleteOffer(offerId,
|
|
133
|
+
async deleteOffer(offerId, ownerPublicKey) {
|
|
451
134
|
const offer = this.offers.get(offerId);
|
|
452
|
-
if (!offer || offer.
|
|
135
|
+
if (!offer || offer.publicKey !== ownerPublicKey) {
|
|
453
136
|
return false;
|
|
454
137
|
}
|
|
455
138
|
this.removeOfferFromIndexes(offer);
|
|
@@ -469,41 +152,41 @@ var init_memory = __esm({
|
|
|
469
152
|
}
|
|
470
153
|
return count;
|
|
471
154
|
}
|
|
472
|
-
async answerOffer(offerId,
|
|
155
|
+
async answerOffer(offerId, answererPublicKey, answerSdp, matchedTags) {
|
|
473
156
|
const offer = await this.getOfferById(offerId);
|
|
474
157
|
if (!offer) {
|
|
475
158
|
return { success: false, error: "Offer not found or expired" };
|
|
476
159
|
}
|
|
477
|
-
if (offer.
|
|
160
|
+
if (offer.answererPublicKey) {
|
|
478
161
|
return { success: false, error: "Offer already answered" };
|
|
479
162
|
}
|
|
480
163
|
const now = Date.now();
|
|
481
|
-
offer.
|
|
164
|
+
offer.answererPublicKey = answererPublicKey;
|
|
482
165
|
offer.answerSdp = answerSdp;
|
|
483
166
|
offer.answeredAt = now;
|
|
484
167
|
offer.matchedTags = matchedTags;
|
|
485
|
-
if (!this.offersByAnswerer.has(
|
|
486
|
-
this.offersByAnswerer.set(
|
|
168
|
+
if (!this.offersByAnswerer.has(answererPublicKey)) {
|
|
169
|
+
this.offersByAnswerer.set(answererPublicKey, /* @__PURE__ */ new Set());
|
|
487
170
|
}
|
|
488
|
-
this.offersByAnswerer.get(
|
|
171
|
+
this.offersByAnswerer.get(answererPublicKey).add(offerId);
|
|
489
172
|
return { success: true };
|
|
490
173
|
}
|
|
491
|
-
async getAnsweredOffers(
|
|
174
|
+
async getAnsweredOffers(offererPublicKey) {
|
|
492
175
|
const now = Date.now();
|
|
493
|
-
const offerIds = this.
|
|
176
|
+
const offerIds = this.offersByPublicKey.get(offererPublicKey);
|
|
494
177
|
if (!offerIds) return [];
|
|
495
178
|
const offers = [];
|
|
496
179
|
for (const id of offerIds) {
|
|
497
180
|
const offer = this.offers.get(id);
|
|
498
|
-
if (offer && offer.
|
|
181
|
+
if (offer && offer.answererPublicKey && offer.expiresAt > now) {
|
|
499
182
|
offers.push(offer);
|
|
500
183
|
}
|
|
501
184
|
}
|
|
502
185
|
return offers.sort((a, b) => (b.answeredAt || 0) - (a.answeredAt || 0));
|
|
503
186
|
}
|
|
504
|
-
async getOffersAnsweredBy(
|
|
187
|
+
async getOffersAnsweredBy(answererPublicKey) {
|
|
505
188
|
const now = Date.now();
|
|
506
|
-
const offerIds = this.offersByAnswerer.get(
|
|
189
|
+
const offerIds = this.offersByAnswerer.get(answererPublicKey);
|
|
507
190
|
if (!offerIds) return [];
|
|
508
191
|
const offers = [];
|
|
509
192
|
for (const id of offerIds) {
|
|
@@ -515,7 +198,7 @@ var init_memory = __esm({
|
|
|
515
198
|
return offers.sort((a, b) => (b.answeredAt || 0) - (a.answeredAt || 0));
|
|
516
199
|
}
|
|
517
200
|
// ===== Discovery =====
|
|
518
|
-
async discoverOffers(tags,
|
|
201
|
+
async discoverOffers(tags, excludePublicKey, limit, offset) {
|
|
519
202
|
if (tags.length === 0) return [];
|
|
520
203
|
const now = Date.now();
|
|
521
204
|
const matchingOfferIds = /* @__PURE__ */ new Set();
|
|
@@ -530,14 +213,14 @@ var init_memory = __esm({
|
|
|
530
213
|
const offers = [];
|
|
531
214
|
for (const id of matchingOfferIds) {
|
|
532
215
|
const offer = this.offers.get(id);
|
|
533
|
-
if (offer && offer.expiresAt > now && !offer.
|
|
216
|
+
if (offer && offer.expiresAt > now && !offer.answererPublicKey && (!excludePublicKey || offer.publicKey !== excludePublicKey)) {
|
|
534
217
|
offers.push(offer);
|
|
535
218
|
}
|
|
536
219
|
}
|
|
537
220
|
offers.sort((a, b) => b.createdAt - a.createdAt);
|
|
538
221
|
return offers.slice(offset, offset + limit);
|
|
539
222
|
}
|
|
540
|
-
async getRandomOffer(tags,
|
|
223
|
+
async getRandomOffer(tags, excludePublicKey) {
|
|
541
224
|
if (tags.length === 0) return null;
|
|
542
225
|
const now = Date.now();
|
|
543
226
|
const matchingOffers = [];
|
|
@@ -552,7 +235,7 @@ var init_memory = __esm({
|
|
|
552
235
|
}
|
|
553
236
|
for (const id of matchingOfferIds) {
|
|
554
237
|
const offer = this.offers.get(id);
|
|
555
|
-
if (offer && offer.expiresAt > now && !offer.
|
|
238
|
+
if (offer && offer.expiresAt > now && !offer.answererPublicKey && (!excludePublicKey || offer.publicKey !== excludePublicKey)) {
|
|
556
239
|
matchingOffers.push(offer);
|
|
557
240
|
}
|
|
558
241
|
}
|
|
@@ -561,7 +244,7 @@ var init_memory = __esm({
|
|
|
561
244
|
return matchingOffers[randomIndex];
|
|
562
245
|
}
|
|
563
246
|
// ===== ICE Candidate Management =====
|
|
564
|
-
async addIceCandidates(offerId,
|
|
247
|
+
async addIceCandidates(offerId, publicKey, role, candidates) {
|
|
565
248
|
const baseTimestamp = Date.now();
|
|
566
249
|
if (!this.iceCandidates.has(offerId)) {
|
|
567
250
|
this.iceCandidates.set(offerId, []);
|
|
@@ -571,7 +254,7 @@ var init_memory = __esm({
|
|
|
571
254
|
const candidate = {
|
|
572
255
|
id: ++this.iceCandidateIdCounter,
|
|
573
256
|
offerId,
|
|
574
|
-
|
|
257
|
+
publicKey,
|
|
575
258
|
role,
|
|
576
259
|
candidate: candidates[i],
|
|
577
260
|
createdAt: baseTimestamp + i
|
|
@@ -584,7 +267,7 @@ var init_memory = __esm({
|
|
|
584
267
|
const candidates = this.iceCandidates.get(offerId) || [];
|
|
585
268
|
return candidates.filter((c) => c.role === targetRole && (since === void 0 || c.createdAt > since)).sort((a, b) => a.createdAt - b.createdAt);
|
|
586
269
|
}
|
|
587
|
-
async getIceCandidatesForMultipleOffers(offerIds,
|
|
270
|
+
async getIceCandidatesForMultipleOffers(offerIds, publicKey, since) {
|
|
588
271
|
const result = /* @__PURE__ */ new Map();
|
|
589
272
|
if (offerIds.length === 0) return result;
|
|
590
273
|
if (offerIds.length > 1e3) {
|
|
@@ -594,8 +277,8 @@ var init_memory = __esm({
|
|
|
594
277
|
const offer = this.offers.get(offerId);
|
|
595
278
|
if (!offer) continue;
|
|
596
279
|
const candidates = this.iceCandidates.get(offerId) || [];
|
|
597
|
-
const isOfferer = offer.
|
|
598
|
-
const isAnswerer = offer.
|
|
280
|
+
const isOfferer = offer.publicKey === publicKey;
|
|
281
|
+
const isAnswerer = offer.answererPublicKey === publicKey;
|
|
599
282
|
if (!isOfferer && !isAnswerer) continue;
|
|
600
283
|
const targetRole = isOfferer ? "answerer" : "offerer";
|
|
601
284
|
const filteredCandidates = candidates.filter((c) => c.role === targetRole && (since === void 0 || c.createdAt > since)).sort((a, b) => a.createdAt - b.createdAt);
|
|
@@ -605,79 +288,6 @@ var init_memory = __esm({
|
|
|
605
288
|
}
|
|
606
289
|
return result;
|
|
607
290
|
}
|
|
608
|
-
// ===== Credential Management =====
|
|
609
|
-
async generateCredentials(request) {
|
|
610
|
-
const now = Date.now();
|
|
611
|
-
const expiresAt = request.expiresAt || now + YEAR_IN_MS;
|
|
612
|
-
const { generateCredentialName: generateCredentialName2, generateSecret: generateSecret2, encryptSecret: encryptSecret2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
613
|
-
let name;
|
|
614
|
-
if (request.name) {
|
|
615
|
-
if (this.credentials.has(request.name)) {
|
|
616
|
-
throw new Error("Username already taken");
|
|
617
|
-
}
|
|
618
|
-
name = request.name;
|
|
619
|
-
} else {
|
|
620
|
-
let attempts = 0;
|
|
621
|
-
const maxAttempts = 100;
|
|
622
|
-
while (attempts < maxAttempts) {
|
|
623
|
-
name = generateCredentialName2();
|
|
624
|
-
if (!this.credentials.has(name)) break;
|
|
625
|
-
attempts++;
|
|
626
|
-
}
|
|
627
|
-
if (attempts >= maxAttempts) {
|
|
628
|
-
throw new Error(`Failed to generate unique credential name after ${maxAttempts} attempts`);
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
const secret = generateSecret2();
|
|
632
|
-
const encryptedSecret = await encryptSecret2(secret, this.masterEncryptionKey);
|
|
633
|
-
const credential = {
|
|
634
|
-
name,
|
|
635
|
-
secret: encryptedSecret,
|
|
636
|
-
createdAt: now,
|
|
637
|
-
expiresAt,
|
|
638
|
-
lastUsed: now
|
|
639
|
-
};
|
|
640
|
-
this.credentials.set(name, credential);
|
|
641
|
-
return {
|
|
642
|
-
...credential,
|
|
643
|
-
secret
|
|
644
|
-
// Return plaintext, not encrypted
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
async getCredential(name) {
|
|
648
|
-
const credential = this.credentials.get(name);
|
|
649
|
-
if (!credential || credential.expiresAt <= Date.now()) {
|
|
650
|
-
return null;
|
|
651
|
-
}
|
|
652
|
-
try {
|
|
653
|
-
const { decryptSecret: decryptSecret2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
654
|
-
const decryptedSecret = await decryptSecret2(credential.secret, this.masterEncryptionKey);
|
|
655
|
-
return {
|
|
656
|
-
...credential,
|
|
657
|
-
secret: decryptedSecret
|
|
658
|
-
};
|
|
659
|
-
} catch (error) {
|
|
660
|
-
console.error(`Failed to decrypt secret for credential '${name}':`, error);
|
|
661
|
-
return null;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
async updateCredentialUsage(name, lastUsed, expiresAt) {
|
|
665
|
-
const credential = this.credentials.get(name);
|
|
666
|
-
if (credential) {
|
|
667
|
-
credential.lastUsed = lastUsed;
|
|
668
|
-
credential.expiresAt = expiresAt;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
async deleteExpiredCredentials(now) {
|
|
672
|
-
let count = 0;
|
|
673
|
-
for (const [name, credential] of this.credentials) {
|
|
674
|
-
if (credential.expiresAt < now) {
|
|
675
|
-
this.credentials.delete(name);
|
|
676
|
-
count++;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
return count;
|
|
680
|
-
}
|
|
681
291
|
// ===== Rate Limiting =====
|
|
682
292
|
async checkRateLimit(identifier, limit, windowMs) {
|
|
683
293
|
const now = Date.now();
|
|
@@ -721,12 +331,11 @@ var init_memory = __esm({
|
|
|
721
331
|
return count;
|
|
722
332
|
}
|
|
723
333
|
async close() {
|
|
724
|
-
this.credentials.clear();
|
|
725
334
|
this.offers.clear();
|
|
726
335
|
this.iceCandidates.clear();
|
|
727
336
|
this.rateLimits.clear();
|
|
728
337
|
this.nonces.clear();
|
|
729
|
-
this.
|
|
338
|
+
this.offersByPublicKey.clear();
|
|
730
339
|
this.offersByTag.clear();
|
|
731
340
|
this.offersByAnswerer.clear();
|
|
732
341
|
}
|
|
@@ -734,24 +343,21 @@ var init_memory = __esm({
|
|
|
734
343
|
async getOfferCount() {
|
|
735
344
|
return this.offers.size;
|
|
736
345
|
}
|
|
737
|
-
async
|
|
738
|
-
const offerIds = this.
|
|
346
|
+
async getOfferCountByPublicKey(publicKey) {
|
|
347
|
+
const offerIds = this.offersByPublicKey.get(publicKey);
|
|
739
348
|
return offerIds ? offerIds.size : 0;
|
|
740
349
|
}
|
|
741
|
-
async getCredentialCount() {
|
|
742
|
-
return this.credentials.size;
|
|
743
|
-
}
|
|
744
350
|
async getIceCandidateCount(offerId) {
|
|
745
351
|
const candidates = this.iceCandidates.get(offerId);
|
|
746
352
|
return candidates ? candidates.length : 0;
|
|
747
353
|
}
|
|
748
354
|
// ===== Helper Methods =====
|
|
749
355
|
removeOfferFromIndexes(offer) {
|
|
750
|
-
const
|
|
751
|
-
if (
|
|
752
|
-
|
|
753
|
-
if (
|
|
754
|
-
this.
|
|
356
|
+
const publicKeyOffers = this.offersByPublicKey.get(offer.publicKey);
|
|
357
|
+
if (publicKeyOffers) {
|
|
358
|
+
publicKeyOffers.delete(offer.id);
|
|
359
|
+
if (publicKeyOffers.size === 0) {
|
|
360
|
+
this.offersByPublicKey.delete(offer.publicKey);
|
|
755
361
|
}
|
|
756
362
|
}
|
|
757
363
|
for (const tag of offer.tags) {
|
|
@@ -763,12 +369,12 @@ var init_memory = __esm({
|
|
|
763
369
|
}
|
|
764
370
|
}
|
|
765
371
|
}
|
|
766
|
-
if (offer.
|
|
767
|
-
const answererOffers = this.offersByAnswerer.get(offer.
|
|
372
|
+
if (offer.answererPublicKey) {
|
|
373
|
+
const answererOffers = this.offersByAnswerer.get(offer.answererPublicKey);
|
|
768
374
|
if (answererOffers) {
|
|
769
375
|
answererOffers.delete(offer.id);
|
|
770
376
|
if (answererOffers.size === 0) {
|
|
771
|
-
this.offersByAnswerer.delete(offer.
|
|
377
|
+
this.offersByAnswerer.delete(offer.answererPublicKey);
|
|
772
378
|
}
|
|
773
379
|
}
|
|
774
380
|
}
|
|
@@ -782,54 +388,63 @@ var sqlite_exports = {};
|
|
|
782
388
|
__export(sqlite_exports, {
|
|
783
389
|
SQLiteStorage: () => SQLiteStorage
|
|
784
390
|
});
|
|
785
|
-
var import_better_sqlite3,
|
|
391
|
+
var import_better_sqlite3, SQLiteStorage;
|
|
786
392
|
var init_sqlite = __esm({
|
|
787
393
|
"src/storage/sqlite.ts"() {
|
|
788
394
|
"use strict";
|
|
789
395
|
import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
790
396
|
init_hash_id();
|
|
791
|
-
YEAR_IN_MS2 = 365 * 24 * 60 * 60 * 1e3;
|
|
792
397
|
SQLiteStorage = class {
|
|
793
398
|
/**
|
|
794
399
|
* Creates a new SQLite storage instance
|
|
795
400
|
* @param path Path to SQLite database file, or ':memory:' for in-memory database
|
|
796
|
-
* @param masterEncryptionKey 64-char hex string for encrypting secrets (32 bytes)
|
|
797
401
|
*/
|
|
798
|
-
constructor(path = ":memory:"
|
|
402
|
+
constructor(path = ":memory:") {
|
|
799
403
|
this.db = new import_better_sqlite3.default(path);
|
|
800
|
-
this.masterEncryptionKey = masterEncryptionKey;
|
|
801
404
|
this.initializeDatabase();
|
|
802
405
|
}
|
|
803
406
|
/**
|
|
804
|
-
* Initializes database schema with
|
|
407
|
+
* Initializes database schema with Ed25519 public key identity
|
|
805
408
|
*/
|
|
806
409
|
initializeDatabase() {
|
|
807
410
|
this.db.exec(`
|
|
411
|
+
-- Identities table (Ed25519 public key as identity)
|
|
412
|
+
CREATE TABLE IF NOT EXISTS identities (
|
|
413
|
+
public_key TEXT PRIMARY KEY,
|
|
414
|
+
created_at INTEGER NOT NULL,
|
|
415
|
+
expires_at INTEGER NOT NULL,
|
|
416
|
+
last_used INTEGER NOT NULL,
|
|
417
|
+
CHECK(length(public_key) = 64)
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
CREATE INDEX IF NOT EXISTS idx_identities_expires ON identities(expires_at);
|
|
421
|
+
|
|
808
422
|
-- WebRTC signaling offers with tags
|
|
809
423
|
CREATE TABLE IF NOT EXISTS offers (
|
|
810
424
|
id TEXT PRIMARY KEY,
|
|
811
|
-
|
|
425
|
+
public_key TEXT NOT NULL,
|
|
812
426
|
tags TEXT NOT NULL,
|
|
813
427
|
sdp TEXT NOT NULL,
|
|
814
428
|
created_at INTEGER NOT NULL,
|
|
815
429
|
expires_at INTEGER NOT NULL,
|
|
816
430
|
last_seen INTEGER NOT NULL,
|
|
817
|
-
|
|
431
|
+
answerer_public_key TEXT,
|
|
818
432
|
answer_sdp TEXT,
|
|
819
433
|
answered_at INTEGER,
|
|
820
|
-
matched_tags TEXT
|
|
434
|
+
matched_tags TEXT,
|
|
435
|
+
FOREIGN KEY (public_key) REFERENCES identities(public_key) ON DELETE CASCADE
|
|
821
436
|
);
|
|
822
437
|
|
|
823
|
-
CREATE INDEX IF NOT EXISTS
|
|
438
|
+
CREATE INDEX IF NOT EXISTS idx_offers_public_key ON offers(public_key);
|
|
824
439
|
CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at);
|
|
825
440
|
CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
|
|
826
|
-
CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(
|
|
441
|
+
CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_public_key);
|
|
827
442
|
|
|
828
443
|
-- ICE candidates table
|
|
829
444
|
CREATE TABLE IF NOT EXISTS ice_candidates (
|
|
830
445
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
831
446
|
offer_id TEXT NOT NULL,
|
|
832
|
-
|
|
447
|
+
public_key TEXT NOT NULL,
|
|
833
448
|
role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
|
|
834
449
|
candidate TEXT NOT NULL,
|
|
835
450
|
created_at INTEGER NOT NULL,
|
|
@@ -837,22 +452,9 @@ var init_sqlite = __esm({
|
|
|
837
452
|
);
|
|
838
453
|
|
|
839
454
|
CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);
|
|
840
|
-
CREATE INDEX IF NOT EXISTS
|
|
455
|
+
CREATE INDEX IF NOT EXISTS idx_ice_public_key ON ice_candidates(public_key);
|
|
841
456
|
CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);
|
|
842
457
|
|
|
843
|
-
-- Credentials table (replaces usernames with simpler name + secret auth)
|
|
844
|
-
CREATE TABLE IF NOT EXISTS credentials (
|
|
845
|
-
name TEXT PRIMARY KEY,
|
|
846
|
-
secret TEXT NOT NULL UNIQUE,
|
|
847
|
-
created_at INTEGER NOT NULL,
|
|
848
|
-
expires_at INTEGER NOT NULL,
|
|
849
|
-
last_used INTEGER NOT NULL,
|
|
850
|
-
CHECK(length(name) >= 3 AND length(name) <= 32)
|
|
851
|
-
);
|
|
852
|
-
|
|
853
|
-
CREATE INDEX IF NOT EXISTS idx_credentials_expires ON credentials(expires_at);
|
|
854
|
-
CREATE INDEX IF NOT EXISTS idx_credentials_secret ON credentials(secret);
|
|
855
|
-
|
|
856
458
|
-- Rate limits table (for distributed rate limiting)
|
|
857
459
|
CREATE TABLE IF NOT EXISTS rate_limits (
|
|
858
460
|
identifier TEXT PRIMARY KEY,
|
|
@@ -883,14 +485,14 @@ var init_sqlite = __esm({
|
|
|
883
485
|
);
|
|
884
486
|
const transaction = this.db.transaction((offersWithIds2) => {
|
|
885
487
|
const offerStmt = this.db.prepare(`
|
|
886
|
-
INSERT INTO offers (id,
|
|
488
|
+
INSERT INTO offers (id, public_key, tags, sdp, created_at, expires_at, last_seen)
|
|
887
489
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
888
490
|
`);
|
|
889
491
|
for (const offer of offersWithIds2) {
|
|
890
492
|
const now = Date.now();
|
|
891
493
|
offerStmt.run(
|
|
892
494
|
offer.id,
|
|
893
|
-
offer.
|
|
495
|
+
offer.publicKey,
|
|
894
496
|
JSON.stringify(offer.tags),
|
|
895
497
|
offer.sdp,
|
|
896
498
|
now,
|
|
@@ -899,7 +501,7 @@ var init_sqlite = __esm({
|
|
|
899
501
|
);
|
|
900
502
|
created.push({
|
|
901
503
|
id: offer.id,
|
|
902
|
-
|
|
504
|
+
publicKey: offer.publicKey,
|
|
903
505
|
tags: offer.tags,
|
|
904
506
|
sdp: offer.sdp,
|
|
905
507
|
createdAt: now,
|
|
@@ -911,13 +513,13 @@ var init_sqlite = __esm({
|
|
|
911
513
|
transaction(offersWithIds);
|
|
912
514
|
return created;
|
|
913
515
|
}
|
|
914
|
-
async
|
|
516
|
+
async getOffersByPublicKey(publicKey) {
|
|
915
517
|
const stmt = this.db.prepare(`
|
|
916
518
|
SELECT * FROM offers
|
|
917
|
-
WHERE
|
|
519
|
+
WHERE public_key = ? AND expires_at > ?
|
|
918
520
|
ORDER BY last_seen DESC
|
|
919
521
|
`);
|
|
920
|
-
const rows = stmt.all(
|
|
522
|
+
const rows = stmt.all(publicKey, Date.now());
|
|
921
523
|
return rows.map((row) => this.rowToOffer(row));
|
|
922
524
|
}
|
|
923
525
|
async getOfferById(offerId) {
|
|
@@ -931,12 +533,12 @@ var init_sqlite = __esm({
|
|
|
931
533
|
}
|
|
932
534
|
return this.rowToOffer(row);
|
|
933
535
|
}
|
|
934
|
-
async deleteOffer(offerId,
|
|
536
|
+
async deleteOffer(offerId, ownerPublicKey) {
|
|
935
537
|
const stmt = this.db.prepare(`
|
|
936
538
|
DELETE FROM offers
|
|
937
|
-
WHERE id = ? AND
|
|
539
|
+
WHERE id = ? AND public_key = ?
|
|
938
540
|
`);
|
|
939
|
-
const result = stmt.run(offerId,
|
|
541
|
+
const result = stmt.run(offerId, ownerPublicKey);
|
|
940
542
|
return result.changes > 0;
|
|
941
543
|
}
|
|
942
544
|
async deleteExpiredOffers(now) {
|
|
@@ -944,7 +546,7 @@ var init_sqlite = __esm({
|
|
|
944
546
|
const result = stmt.run(now);
|
|
945
547
|
return result.changes;
|
|
946
548
|
}
|
|
947
|
-
async answerOffer(offerId,
|
|
549
|
+
async answerOffer(offerId, answererPublicKey, answerSdp, matchedTags) {
|
|
948
550
|
const offer = await this.getOfferById(offerId);
|
|
949
551
|
if (!offer) {
|
|
950
552
|
return {
|
|
@@ -952,7 +554,7 @@ var init_sqlite = __esm({
|
|
|
952
554
|
error: "Offer not found or expired"
|
|
953
555
|
};
|
|
954
556
|
}
|
|
955
|
-
if (offer.
|
|
557
|
+
if (offer.answererPublicKey) {
|
|
956
558
|
return {
|
|
957
559
|
success: false,
|
|
958
560
|
error: "Offer already answered"
|
|
@@ -960,11 +562,11 @@ var init_sqlite = __esm({
|
|
|
960
562
|
}
|
|
961
563
|
const stmt = this.db.prepare(`
|
|
962
564
|
UPDATE offers
|
|
963
|
-
SET
|
|
964
|
-
WHERE id = ? AND
|
|
565
|
+
SET answerer_public_key = ?, answer_sdp = ?, answered_at = ?, matched_tags = ?
|
|
566
|
+
WHERE id = ? AND answerer_public_key IS NULL
|
|
965
567
|
`);
|
|
966
568
|
const matchedTagsJson = matchedTags ? JSON.stringify(matchedTags) : null;
|
|
967
|
-
const result = stmt.run(
|
|
569
|
+
const result = stmt.run(answererPublicKey, answerSdp, Date.now(), matchedTagsJson, offerId);
|
|
968
570
|
if (result.changes === 0) {
|
|
969
571
|
return {
|
|
970
572
|
success: false,
|
|
@@ -973,26 +575,26 @@ var init_sqlite = __esm({
|
|
|
973
575
|
}
|
|
974
576
|
return { success: true };
|
|
975
577
|
}
|
|
976
|
-
async getAnsweredOffers(
|
|
578
|
+
async getAnsweredOffers(offererPublicKey) {
|
|
977
579
|
const stmt = this.db.prepare(`
|
|
978
580
|
SELECT * FROM offers
|
|
979
|
-
WHERE
|
|
581
|
+
WHERE public_key = ? AND answerer_public_key IS NOT NULL AND expires_at > ?
|
|
980
582
|
ORDER BY answered_at DESC
|
|
981
583
|
`);
|
|
982
|
-
const rows = stmt.all(
|
|
584
|
+
const rows = stmt.all(offererPublicKey, Date.now());
|
|
983
585
|
return rows.map((row) => this.rowToOffer(row));
|
|
984
586
|
}
|
|
985
|
-
async getOffersAnsweredBy(
|
|
587
|
+
async getOffersAnsweredBy(answererPublicKey) {
|
|
986
588
|
const stmt = this.db.prepare(`
|
|
987
589
|
SELECT * FROM offers
|
|
988
|
-
WHERE
|
|
590
|
+
WHERE answerer_public_key = ? AND expires_at > ?
|
|
989
591
|
ORDER BY answered_at DESC
|
|
990
592
|
`);
|
|
991
|
-
const rows = stmt.all(
|
|
593
|
+
const rows = stmt.all(answererPublicKey, Date.now());
|
|
992
594
|
return rows.map((row) => this.rowToOffer(row));
|
|
993
595
|
}
|
|
994
596
|
// ===== Discovery =====
|
|
995
|
-
async discoverOffers(tags,
|
|
597
|
+
async discoverOffers(tags, excludePublicKey, limit, offset) {
|
|
996
598
|
if (tags.length === 0) {
|
|
997
599
|
return [];
|
|
998
600
|
}
|
|
@@ -1001,12 +603,12 @@ var init_sqlite = __esm({
|
|
|
1001
603
|
SELECT DISTINCT o.* FROM offers o, json_each(o.tags) as t
|
|
1002
604
|
WHERE t.value IN (${placeholders})
|
|
1003
605
|
AND o.expires_at > ?
|
|
1004
|
-
AND o.
|
|
606
|
+
AND o.answerer_public_key IS NULL
|
|
1005
607
|
`;
|
|
1006
608
|
const params = [...tags, Date.now()];
|
|
1007
|
-
if (
|
|
1008
|
-
query += " AND o.
|
|
1009
|
-
params.push(
|
|
609
|
+
if (excludePublicKey) {
|
|
610
|
+
query += " AND o.public_key != ?";
|
|
611
|
+
params.push(excludePublicKey);
|
|
1010
612
|
}
|
|
1011
613
|
query += " ORDER BY o.created_at DESC LIMIT ? OFFSET ?";
|
|
1012
614
|
params.push(limit, offset);
|
|
@@ -1014,7 +616,7 @@ var init_sqlite = __esm({
|
|
|
1014
616
|
const rows = stmt.all(...params);
|
|
1015
617
|
return rows.map((row) => this.rowToOffer(row));
|
|
1016
618
|
}
|
|
1017
|
-
async getRandomOffer(tags,
|
|
619
|
+
async getRandomOffer(tags, excludePublicKey) {
|
|
1018
620
|
if (tags.length === 0) {
|
|
1019
621
|
return null;
|
|
1020
622
|
}
|
|
@@ -1023,12 +625,12 @@ var init_sqlite = __esm({
|
|
|
1023
625
|
SELECT DISTINCT o.* FROM offers o, json_each(o.tags) as t
|
|
1024
626
|
WHERE t.value IN (${placeholders})
|
|
1025
627
|
AND o.expires_at > ?
|
|
1026
|
-
AND o.
|
|
628
|
+
AND o.answerer_public_key IS NULL
|
|
1027
629
|
`;
|
|
1028
630
|
const params = [...tags, Date.now()];
|
|
1029
|
-
if (
|
|
1030
|
-
query += " AND o.
|
|
1031
|
-
params.push(
|
|
631
|
+
if (excludePublicKey) {
|
|
632
|
+
query += " AND o.public_key != ?";
|
|
633
|
+
params.push(excludePublicKey);
|
|
1032
634
|
}
|
|
1033
635
|
query += " ORDER BY RANDOM() LIMIT 1";
|
|
1034
636
|
const stmt = this.db.prepare(query);
|
|
@@ -1036,9 +638,9 @@ var init_sqlite = __esm({
|
|
|
1036
638
|
return row ? this.rowToOffer(row) : null;
|
|
1037
639
|
}
|
|
1038
640
|
// ===== ICE Candidate Management =====
|
|
1039
|
-
async addIceCandidates(offerId,
|
|
641
|
+
async addIceCandidates(offerId, publicKey, role, candidates) {
|
|
1040
642
|
const stmt = this.db.prepare(`
|
|
1041
|
-
INSERT INTO ice_candidates (offer_id,
|
|
643
|
+
INSERT INTO ice_candidates (offer_id, public_key, role, candidate, created_at)
|
|
1042
644
|
VALUES (?, ?, ?, ?, ?)
|
|
1043
645
|
`);
|
|
1044
646
|
const baseTimestamp = Date.now();
|
|
@@ -1046,7 +648,7 @@ var init_sqlite = __esm({
|
|
|
1046
648
|
for (let i = 0; i < candidates2.length; i++) {
|
|
1047
649
|
stmt.run(
|
|
1048
650
|
offerId,
|
|
1049
|
-
|
|
651
|
+
publicKey,
|
|
1050
652
|
role,
|
|
1051
653
|
JSON.stringify(candidates2[i]),
|
|
1052
654
|
baseTimestamp + i
|
|
@@ -1072,13 +674,13 @@ var init_sqlite = __esm({
|
|
|
1072
674
|
return rows.map((row) => ({
|
|
1073
675
|
id: row.id,
|
|
1074
676
|
offerId: row.offer_id,
|
|
1075
|
-
|
|
677
|
+
publicKey: row.public_key,
|
|
1076
678
|
role: row.role,
|
|
1077
679
|
candidate: JSON.parse(row.candidate),
|
|
1078
680
|
createdAt: row.created_at
|
|
1079
681
|
}));
|
|
1080
682
|
}
|
|
1081
|
-
async getIceCandidatesForMultipleOffers(offerIds,
|
|
683
|
+
async getIceCandidatesForMultipleOffers(offerIds, publicKey, since) {
|
|
1082
684
|
const result = /* @__PURE__ */ new Map();
|
|
1083
685
|
if (offerIds.length === 0) {
|
|
1084
686
|
return result;
|
|
@@ -1091,16 +693,16 @@ var init_sqlite = __esm({
|
|
|
1091
693
|
}
|
|
1092
694
|
const placeholders = offerIds.map(() => "?").join(",");
|
|
1093
695
|
let query = `
|
|
1094
|
-
SELECT ic.*, o.
|
|
696
|
+
SELECT ic.*, o.public_key as offer_public_key
|
|
1095
697
|
FROM ice_candidates ic
|
|
1096
698
|
INNER JOIN offers o ON o.id = ic.offer_id
|
|
1097
699
|
WHERE ic.offer_id IN (${placeholders})
|
|
1098
700
|
AND (
|
|
1099
|
-
(o.
|
|
1100
|
-
OR (o.
|
|
701
|
+
(o.public_key = ? AND ic.role = 'answerer')
|
|
702
|
+
OR (o.answerer_public_key = ? AND ic.role = 'offerer')
|
|
1101
703
|
)
|
|
1102
704
|
`;
|
|
1103
|
-
const params = [...offerIds,
|
|
705
|
+
const params = [...offerIds, publicKey, publicKey];
|
|
1104
706
|
if (since !== void 0) {
|
|
1105
707
|
query += " AND ic.created_at > ?";
|
|
1106
708
|
params.push(since);
|
|
@@ -1112,7 +714,7 @@ var init_sqlite = __esm({
|
|
|
1112
714
|
const candidate = {
|
|
1113
715
|
id: row.id,
|
|
1114
716
|
offerId: row.offer_id,
|
|
1115
|
-
|
|
717
|
+
publicKey: row.public_key,
|
|
1116
718
|
role: row.role,
|
|
1117
719
|
candidate: JSON.parse(row.candidate),
|
|
1118
720
|
createdAt: row.created_at
|
|
@@ -1124,92 +726,6 @@ var init_sqlite = __esm({
|
|
|
1124
726
|
}
|
|
1125
727
|
return result;
|
|
1126
728
|
}
|
|
1127
|
-
// ===== Credential Management =====
|
|
1128
|
-
async generateCredentials(request) {
|
|
1129
|
-
const now = Date.now();
|
|
1130
|
-
const expiresAt = request.expiresAt || now + YEAR_IN_MS2;
|
|
1131
|
-
const { generateCredentialName: generateCredentialName2, generateSecret: generateSecret2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
1132
|
-
let name;
|
|
1133
|
-
if (request.name) {
|
|
1134
|
-
const existing = this.db.prepare(`
|
|
1135
|
-
SELECT name FROM credentials WHERE name = ?
|
|
1136
|
-
`).get(request.name);
|
|
1137
|
-
if (existing) {
|
|
1138
|
-
throw new Error("Username already taken");
|
|
1139
|
-
}
|
|
1140
|
-
name = request.name;
|
|
1141
|
-
} else {
|
|
1142
|
-
let attempts = 0;
|
|
1143
|
-
const maxAttempts = 100;
|
|
1144
|
-
while (attempts < maxAttempts) {
|
|
1145
|
-
name = generateCredentialName2();
|
|
1146
|
-
const existing = this.db.prepare(`
|
|
1147
|
-
SELECT name FROM credentials WHERE name = ?
|
|
1148
|
-
`).get(name);
|
|
1149
|
-
if (!existing) {
|
|
1150
|
-
break;
|
|
1151
|
-
}
|
|
1152
|
-
attempts++;
|
|
1153
|
-
}
|
|
1154
|
-
if (attempts >= maxAttempts) {
|
|
1155
|
-
throw new Error(`Failed to generate unique credential name after ${maxAttempts} attempts`);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
const secret = generateSecret2();
|
|
1159
|
-
const { encryptSecret: encryptSecret2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
1160
|
-
const encryptedSecret = await encryptSecret2(secret, this.masterEncryptionKey);
|
|
1161
|
-
const stmt = this.db.prepare(`
|
|
1162
|
-
INSERT INTO credentials (name, secret, created_at, expires_at, last_used)
|
|
1163
|
-
VALUES (?, ?, ?, ?, ?)
|
|
1164
|
-
`);
|
|
1165
|
-
stmt.run(name, encryptedSecret, now, expiresAt, now);
|
|
1166
|
-
return {
|
|
1167
|
-
name,
|
|
1168
|
-
secret,
|
|
1169
|
-
// Return plaintext secret, not encrypted
|
|
1170
|
-
createdAt: now,
|
|
1171
|
-
expiresAt,
|
|
1172
|
-
lastUsed: now
|
|
1173
|
-
};
|
|
1174
|
-
}
|
|
1175
|
-
async getCredential(name) {
|
|
1176
|
-
const stmt = this.db.prepare(`
|
|
1177
|
-
SELECT * FROM credentials
|
|
1178
|
-
WHERE name = ? AND expires_at > ?
|
|
1179
|
-
`);
|
|
1180
|
-
const row = stmt.get(name, Date.now());
|
|
1181
|
-
if (!row) {
|
|
1182
|
-
return null;
|
|
1183
|
-
}
|
|
1184
|
-
try {
|
|
1185
|
-
const { decryptSecret: decryptSecret2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
1186
|
-
const decryptedSecret = await decryptSecret2(row.secret, this.masterEncryptionKey);
|
|
1187
|
-
return {
|
|
1188
|
-
name: row.name,
|
|
1189
|
-
secret: decryptedSecret,
|
|
1190
|
-
// Return decrypted secret
|
|
1191
|
-
createdAt: row.created_at,
|
|
1192
|
-
expiresAt: row.expires_at,
|
|
1193
|
-
lastUsed: row.last_used
|
|
1194
|
-
};
|
|
1195
|
-
} catch (error) {
|
|
1196
|
-
console.error(`Failed to decrypt secret for credential '${name}':`, error);
|
|
1197
|
-
return null;
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
async updateCredentialUsage(name, lastUsed, expiresAt) {
|
|
1201
|
-
const stmt = this.db.prepare(`
|
|
1202
|
-
UPDATE credentials
|
|
1203
|
-
SET last_used = ?, expires_at = ?
|
|
1204
|
-
WHERE name = ?
|
|
1205
|
-
`);
|
|
1206
|
-
stmt.run(lastUsed, expiresAt, name);
|
|
1207
|
-
}
|
|
1208
|
-
async deleteExpiredCredentials(now) {
|
|
1209
|
-
const stmt = this.db.prepare("DELETE FROM credentials WHERE expires_at < ?");
|
|
1210
|
-
const result = stmt.run(now);
|
|
1211
|
-
return result.changes;
|
|
1212
|
-
}
|
|
1213
729
|
// ===== Rate Limiting =====
|
|
1214
730
|
async checkRateLimit(identifier, limit, windowMs) {
|
|
1215
731
|
const now = Date.now();
|
|
@@ -1264,12 +780,8 @@ var init_sqlite = __esm({
|
|
|
1264
780
|
const result = this.db.prepare("SELECT COUNT(*) as count FROM offers").get();
|
|
1265
781
|
return result.count;
|
|
1266
782
|
}
|
|
1267
|
-
async
|
|
1268
|
-
const result = this.db.prepare("SELECT COUNT(*) as count FROM offers WHERE
|
|
1269
|
-
return result.count;
|
|
1270
|
-
}
|
|
1271
|
-
async getCredentialCount() {
|
|
1272
|
-
const result = this.db.prepare("SELECT COUNT(*) as count FROM credentials").get();
|
|
783
|
+
async getOfferCountByPublicKey(publicKey) {
|
|
784
|
+
const result = this.db.prepare("SELECT COUNT(*) as count FROM offers WHERE public_key = ?").get(publicKey);
|
|
1273
785
|
return result.count;
|
|
1274
786
|
}
|
|
1275
787
|
async getIceCandidateCount(offerId) {
|
|
@@ -1283,13 +795,13 @@ var init_sqlite = __esm({
|
|
|
1283
795
|
rowToOffer(row) {
|
|
1284
796
|
return {
|
|
1285
797
|
id: row.id,
|
|
1286
|
-
|
|
798
|
+
publicKey: row.public_key,
|
|
1287
799
|
tags: JSON.parse(row.tags),
|
|
1288
800
|
sdp: row.sdp,
|
|
1289
801
|
createdAt: row.created_at,
|
|
1290
802
|
expiresAt: row.expires_at,
|
|
1291
803
|
lastSeen: row.last_seen,
|
|
1292
|
-
|
|
804
|
+
answererPublicKey: row.answerer_public_key || void 0,
|
|
1293
805
|
answerSdp: row.answer_sdp || void 0,
|
|
1294
806
|
answeredAt: row.answered_at || void 0,
|
|
1295
807
|
matchedTags: row.matched_tags ? JSON.parse(row.matched_tags) : void 0
|
|
@@ -1304,25 +816,22 @@ var mysql_exports = {};
|
|
|
1304
816
|
__export(mysql_exports, {
|
|
1305
817
|
MySQLStorage: () => MySQLStorage
|
|
1306
818
|
});
|
|
1307
|
-
var import_promise,
|
|
819
|
+
var import_promise, MySQLStorage;
|
|
1308
820
|
var init_mysql = __esm({
|
|
1309
821
|
"src/storage/mysql.ts"() {
|
|
1310
822
|
"use strict";
|
|
1311
823
|
import_promise = __toESM(require("mysql2/promise"));
|
|
1312
824
|
init_hash_id();
|
|
1313
|
-
YEAR_IN_MS3 = 365 * 24 * 60 * 60 * 1e3;
|
|
1314
825
|
MySQLStorage = class _MySQLStorage {
|
|
1315
|
-
constructor(pool
|
|
826
|
+
constructor(pool) {
|
|
1316
827
|
this.pool = pool;
|
|
1317
|
-
this.masterEncryptionKey = masterEncryptionKey;
|
|
1318
828
|
}
|
|
1319
829
|
/**
|
|
1320
830
|
* Creates a new MySQL storage instance with connection pooling
|
|
1321
831
|
* @param connectionString MySQL connection URL
|
|
1322
|
-
* @param masterEncryptionKey 64-char hex string for encrypting secrets
|
|
1323
832
|
* @param poolSize Maximum number of connections in the pool
|
|
1324
833
|
*/
|
|
1325
|
-
static async create(connectionString,
|
|
834
|
+
static async create(connectionString, poolSize = 10) {
|
|
1326
835
|
const pool = import_promise.default.createPool({
|
|
1327
836
|
uri: connectionString,
|
|
1328
837
|
waitForConnections: true,
|
|
@@ -1331,7 +840,7 @@ var init_mysql = __esm({
|
|
|
1331
840
|
enableKeepAlive: true,
|
|
1332
841
|
keepAliveInitialDelay: 1e4
|
|
1333
842
|
});
|
|
1334
|
-
const storage = new _MySQLStorage(pool
|
|
843
|
+
const storage = new _MySQLStorage(pool);
|
|
1335
844
|
await storage.initializeDatabase();
|
|
1336
845
|
return storage;
|
|
1337
846
|
}
|
|
@@ -1339,47 +848,47 @@ var init_mysql = __esm({
|
|
|
1339
848
|
const conn = await this.pool.getConnection();
|
|
1340
849
|
try {
|
|
1341
850
|
await conn.query(`
|
|
851
|
+
CREATE TABLE IF NOT EXISTS identities (
|
|
852
|
+
public_key CHAR(64) PRIMARY KEY,
|
|
853
|
+
created_at BIGINT NOT NULL,
|
|
854
|
+
expires_at BIGINT NOT NULL,
|
|
855
|
+
last_used BIGINT NOT NULL,
|
|
856
|
+
INDEX idx_identities_expires (expires_at)
|
|
857
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
858
|
+
`);
|
|
859
|
+
await conn.query(`
|
|
1342
860
|
CREATE TABLE IF NOT EXISTS offers (
|
|
1343
861
|
id VARCHAR(64) PRIMARY KEY,
|
|
1344
|
-
|
|
862
|
+
public_key CHAR(64) NOT NULL,
|
|
1345
863
|
tags JSON NOT NULL,
|
|
1346
864
|
sdp MEDIUMTEXT NOT NULL,
|
|
1347
865
|
created_at BIGINT NOT NULL,
|
|
1348
866
|
expires_at BIGINT NOT NULL,
|
|
1349
867
|
last_seen BIGINT NOT NULL,
|
|
1350
|
-
|
|
868
|
+
answerer_public_key CHAR(64),
|
|
1351
869
|
answer_sdp MEDIUMTEXT,
|
|
1352
870
|
answered_at BIGINT,
|
|
1353
871
|
matched_tags JSON,
|
|
1354
|
-
INDEX
|
|
872
|
+
INDEX idx_offers_public_key (public_key),
|
|
1355
873
|
INDEX idx_offers_expires (expires_at),
|
|
1356
874
|
INDEX idx_offers_last_seen (last_seen),
|
|
1357
|
-
INDEX idx_offers_answerer (
|
|
875
|
+
INDEX idx_offers_answerer (answerer_public_key),
|
|
876
|
+
FOREIGN KEY (public_key) REFERENCES identities(public_key) ON DELETE CASCADE
|
|
1358
877
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
1359
878
|
`);
|
|
1360
879
|
await conn.query(`
|
|
1361
880
|
CREATE TABLE IF NOT EXISTS ice_candidates (
|
|
1362
881
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
1363
882
|
offer_id VARCHAR(64) NOT NULL,
|
|
1364
|
-
|
|
883
|
+
public_key CHAR(64) NOT NULL,
|
|
1365
884
|
role ENUM('offerer', 'answerer') NOT NULL,
|
|
1366
885
|
candidate JSON NOT NULL,
|
|
1367
886
|
created_at BIGINT NOT NULL,
|
|
1368
887
|
INDEX idx_ice_offer (offer_id),
|
|
1369
|
-
INDEX
|
|
888
|
+
INDEX idx_ice_public_key (public_key),
|
|
1370
889
|
INDEX idx_ice_created (created_at),
|
|
1371
890
|
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
|
|
1372
891
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
1373
|
-
`);
|
|
1374
|
-
await conn.query(`
|
|
1375
|
-
CREATE TABLE IF NOT EXISTS credentials (
|
|
1376
|
-
name VARCHAR(32) PRIMARY KEY,
|
|
1377
|
-
secret VARCHAR(512) NOT NULL UNIQUE,
|
|
1378
|
-
created_at BIGINT NOT NULL,
|
|
1379
|
-
expires_at BIGINT NOT NULL,
|
|
1380
|
-
last_used BIGINT NOT NULL,
|
|
1381
|
-
INDEX idx_credentials_expires (expires_at)
|
|
1382
|
-
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
|
|
1383
892
|
`);
|
|
1384
893
|
await conn.query(`
|
|
1385
894
|
CREATE TABLE IF NOT EXISTS rate_limits (
|
|
@@ -1411,13 +920,13 @@ var init_mysql = __esm({
|
|
|
1411
920
|
for (const request of offers) {
|
|
1412
921
|
const id = request.id || await generateOfferHash(request.sdp);
|
|
1413
922
|
await conn.query(
|
|
1414
|
-
`INSERT INTO offers (id,
|
|
923
|
+
`INSERT INTO offers (id, public_key, tags, sdp, created_at, expires_at, last_seen)
|
|
1415
924
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
1416
|
-
[id, request.
|
|
925
|
+
[id, request.publicKey, JSON.stringify(request.tags), request.sdp, now, request.expiresAt, now]
|
|
1417
926
|
);
|
|
1418
927
|
created.push({
|
|
1419
928
|
id,
|
|
1420
|
-
|
|
929
|
+
publicKey: request.publicKey,
|
|
1421
930
|
tags: request.tags,
|
|
1422
931
|
sdp: request.sdp,
|
|
1423
932
|
createdAt: now,
|
|
@@ -1434,10 +943,10 @@ var init_mysql = __esm({
|
|
|
1434
943
|
}
|
|
1435
944
|
return created;
|
|
1436
945
|
}
|
|
1437
|
-
async
|
|
946
|
+
async getOffersByPublicKey(publicKey) {
|
|
1438
947
|
const [rows] = await this.pool.query(
|
|
1439
|
-
`SELECT * FROM offers WHERE
|
|
1440
|
-
[
|
|
948
|
+
`SELECT * FROM offers WHERE public_key = ? AND expires_at > ? ORDER BY last_seen DESC`,
|
|
949
|
+
[publicKey, Date.now()]
|
|
1441
950
|
);
|
|
1442
951
|
return rows.map((row) => this.rowToOffer(row));
|
|
1443
952
|
}
|
|
@@ -1448,10 +957,10 @@ var init_mysql = __esm({
|
|
|
1448
957
|
);
|
|
1449
958
|
return rows.length > 0 ? this.rowToOffer(rows[0]) : null;
|
|
1450
959
|
}
|
|
1451
|
-
async deleteOffer(offerId,
|
|
960
|
+
async deleteOffer(offerId, ownerPublicKey) {
|
|
1452
961
|
const [result] = await this.pool.query(
|
|
1453
|
-
`DELETE FROM offers WHERE id = ? AND
|
|
1454
|
-
[offerId,
|
|
962
|
+
`DELETE FROM offers WHERE id = ? AND public_key = ?`,
|
|
963
|
+
[offerId, ownerPublicKey]
|
|
1455
964
|
);
|
|
1456
965
|
return result.affectedRows > 0;
|
|
1457
966
|
}
|
|
@@ -1462,94 +971,94 @@ var init_mysql = __esm({
|
|
|
1462
971
|
);
|
|
1463
972
|
return result.affectedRows;
|
|
1464
973
|
}
|
|
1465
|
-
async answerOffer(offerId,
|
|
974
|
+
async answerOffer(offerId, answererPublicKey, answerSdp, matchedTags) {
|
|
1466
975
|
const offer = await this.getOfferById(offerId);
|
|
1467
976
|
if (!offer) {
|
|
1468
977
|
return { success: false, error: "Offer not found or expired" };
|
|
1469
978
|
}
|
|
1470
|
-
if (offer.
|
|
979
|
+
if (offer.answererPublicKey) {
|
|
1471
980
|
return { success: false, error: "Offer already answered" };
|
|
1472
981
|
}
|
|
1473
982
|
const matchedTagsJson = matchedTags ? JSON.stringify(matchedTags) : null;
|
|
1474
983
|
const [result] = await this.pool.query(
|
|
1475
|
-
`UPDATE offers SET
|
|
1476
|
-
WHERE id = ? AND
|
|
1477
|
-
[
|
|
984
|
+
`UPDATE offers SET answerer_public_key = ?, answer_sdp = ?, answered_at = ?, matched_tags = ?
|
|
985
|
+
WHERE id = ? AND answerer_public_key IS NULL`,
|
|
986
|
+
[answererPublicKey, answerSdp, Date.now(), matchedTagsJson, offerId]
|
|
1478
987
|
);
|
|
1479
988
|
if (result.affectedRows === 0) {
|
|
1480
989
|
return { success: false, error: "Offer already answered (race condition)" };
|
|
1481
990
|
}
|
|
1482
991
|
return { success: true };
|
|
1483
992
|
}
|
|
1484
|
-
async getAnsweredOffers(
|
|
993
|
+
async getAnsweredOffers(offererPublicKey) {
|
|
1485
994
|
const [rows] = await this.pool.query(
|
|
1486
995
|
`SELECT * FROM offers
|
|
1487
|
-
WHERE
|
|
996
|
+
WHERE public_key = ? AND answerer_public_key IS NOT NULL AND expires_at > ?
|
|
1488
997
|
ORDER BY answered_at DESC`,
|
|
1489
|
-
[
|
|
998
|
+
[offererPublicKey, Date.now()]
|
|
1490
999
|
);
|
|
1491
1000
|
return rows.map((row) => this.rowToOffer(row));
|
|
1492
1001
|
}
|
|
1493
|
-
async getOffersAnsweredBy(
|
|
1002
|
+
async getOffersAnsweredBy(answererPublicKey) {
|
|
1494
1003
|
const [rows] = await this.pool.query(
|
|
1495
1004
|
`SELECT * FROM offers
|
|
1496
|
-
WHERE
|
|
1005
|
+
WHERE answerer_public_key = ? AND expires_at > ?
|
|
1497
1006
|
ORDER BY answered_at DESC`,
|
|
1498
|
-
[
|
|
1007
|
+
[answererPublicKey, Date.now()]
|
|
1499
1008
|
);
|
|
1500
1009
|
return rows.map((row) => this.rowToOffer(row));
|
|
1501
1010
|
}
|
|
1502
1011
|
// ===== Discovery =====
|
|
1503
|
-
async discoverOffers(tags,
|
|
1012
|
+
async discoverOffers(tags, excludePublicKey, limit, offset) {
|
|
1504
1013
|
if (tags.length === 0) return [];
|
|
1505
1014
|
const tagArray = JSON.stringify(tags);
|
|
1506
1015
|
let query = `
|
|
1507
1016
|
SELECT DISTINCT o.* FROM offers o
|
|
1508
1017
|
WHERE JSON_OVERLAPS(o.tags, ?)
|
|
1509
1018
|
AND o.expires_at > ?
|
|
1510
|
-
AND o.
|
|
1019
|
+
AND o.answerer_public_key IS NULL
|
|
1511
1020
|
`;
|
|
1512
1021
|
const params = [tagArray, Date.now()];
|
|
1513
|
-
if (
|
|
1514
|
-
query += " AND o.
|
|
1515
|
-
params.push(
|
|
1022
|
+
if (excludePublicKey) {
|
|
1023
|
+
query += " AND o.public_key != ?";
|
|
1024
|
+
params.push(excludePublicKey);
|
|
1516
1025
|
}
|
|
1517
1026
|
query += " ORDER BY o.created_at DESC LIMIT ? OFFSET ?";
|
|
1518
1027
|
params.push(limit, offset);
|
|
1519
1028
|
const [rows] = await this.pool.query(query, params);
|
|
1520
1029
|
return rows.map((row) => this.rowToOffer(row));
|
|
1521
1030
|
}
|
|
1522
|
-
async getRandomOffer(tags,
|
|
1031
|
+
async getRandomOffer(tags, excludePublicKey) {
|
|
1523
1032
|
if (tags.length === 0) return null;
|
|
1524
1033
|
const tagArray = JSON.stringify(tags);
|
|
1525
1034
|
let query = `
|
|
1526
1035
|
SELECT DISTINCT o.* FROM offers o
|
|
1527
1036
|
WHERE JSON_OVERLAPS(o.tags, ?)
|
|
1528
1037
|
AND o.expires_at > ?
|
|
1529
|
-
AND o.
|
|
1038
|
+
AND o.answerer_public_key IS NULL
|
|
1530
1039
|
`;
|
|
1531
1040
|
const params = [tagArray, Date.now()];
|
|
1532
|
-
if (
|
|
1533
|
-
query += " AND o.
|
|
1534
|
-
params.push(
|
|
1041
|
+
if (excludePublicKey) {
|
|
1042
|
+
query += " AND o.public_key != ?";
|
|
1043
|
+
params.push(excludePublicKey);
|
|
1535
1044
|
}
|
|
1536
1045
|
query += " ORDER BY RAND() LIMIT 1";
|
|
1537
1046
|
const [rows] = await this.pool.query(query, params);
|
|
1538
1047
|
return rows.length > 0 ? this.rowToOffer(rows[0]) : null;
|
|
1539
1048
|
}
|
|
1540
1049
|
// ===== ICE Candidate Management =====
|
|
1541
|
-
async addIceCandidates(offerId,
|
|
1050
|
+
async addIceCandidates(offerId, publicKey, role, candidates) {
|
|
1542
1051
|
if (candidates.length === 0) return 0;
|
|
1543
1052
|
const baseTimestamp = Date.now();
|
|
1544
1053
|
const values = candidates.map((c, i) => [
|
|
1545
1054
|
offerId,
|
|
1546
|
-
|
|
1055
|
+
publicKey,
|
|
1547
1056
|
role,
|
|
1548
1057
|
JSON.stringify(c),
|
|
1549
1058
|
baseTimestamp + i
|
|
1550
1059
|
]);
|
|
1551
1060
|
await this.pool.query(
|
|
1552
|
-
`INSERT INTO ice_candidates (offer_id,
|
|
1061
|
+
`INSERT INTO ice_candidates (offer_id, public_key, role, candidate, created_at)
|
|
1553
1062
|
VALUES ?`,
|
|
1554
1063
|
[values]
|
|
1555
1064
|
);
|
|
@@ -1566,7 +1075,7 @@ var init_mysql = __esm({
|
|
|
1566
1075
|
const [rows] = await this.pool.query(query, params);
|
|
1567
1076
|
return rows.map((row) => this.rowToIceCandidate(row));
|
|
1568
1077
|
}
|
|
1569
|
-
async getIceCandidatesForMultipleOffers(offerIds,
|
|
1078
|
+
async getIceCandidatesForMultipleOffers(offerIds, publicKey, since) {
|
|
1570
1079
|
const result = /* @__PURE__ */ new Map();
|
|
1571
1080
|
if (offerIds.length === 0) return result;
|
|
1572
1081
|
if (offerIds.length > 1e3) {
|
|
@@ -1574,16 +1083,16 @@ var init_mysql = __esm({
|
|
|
1574
1083
|
}
|
|
1575
1084
|
const placeholders = offerIds.map(() => "?").join(",");
|
|
1576
1085
|
let query = `
|
|
1577
|
-
SELECT ic.*, o.
|
|
1086
|
+
SELECT ic.*, o.public_key as offer_public_key
|
|
1578
1087
|
FROM ice_candidates ic
|
|
1579
1088
|
INNER JOIN offers o ON o.id = ic.offer_id
|
|
1580
1089
|
WHERE ic.offer_id IN (${placeholders})
|
|
1581
1090
|
AND (
|
|
1582
|
-
(o.
|
|
1583
|
-
OR (o.
|
|
1091
|
+
(o.public_key = ? AND ic.role = 'answerer')
|
|
1092
|
+
OR (o.answerer_public_key = ? AND ic.role = 'offerer')
|
|
1584
1093
|
)
|
|
1585
1094
|
`;
|
|
1586
|
-
const params = [...offerIds,
|
|
1095
|
+
const params = [...offerIds, publicKey, publicKey];
|
|
1587
1096
|
if (since !== void 0) {
|
|
1588
1097
|
query += " AND ic.created_at > ?";
|
|
1589
1098
|
params.push(since);
|
|
@@ -1599,86 +1108,6 @@ var init_mysql = __esm({
|
|
|
1599
1108
|
}
|
|
1600
1109
|
return result;
|
|
1601
1110
|
}
|
|
1602
|
-
// ===== Credential Management =====
|
|
1603
|
-
async generateCredentials(request) {
|
|
1604
|
-
const now = Date.now();
|
|
1605
|
-
const expiresAt = request.expiresAt || now + YEAR_IN_MS3;
|
|
1606
|
-
const { generateCredentialName: generateCredentialName2, generateSecret: generateSecret2, encryptSecret: encryptSecret2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
1607
|
-
let name;
|
|
1608
|
-
if (request.name) {
|
|
1609
|
-
const [existing] = await this.pool.query(
|
|
1610
|
-
`SELECT name FROM credentials WHERE name = ?`,
|
|
1611
|
-
[request.name]
|
|
1612
|
-
);
|
|
1613
|
-
if (existing.length > 0) {
|
|
1614
|
-
throw new Error("Username already taken");
|
|
1615
|
-
}
|
|
1616
|
-
name = request.name;
|
|
1617
|
-
} else {
|
|
1618
|
-
let attempts = 0;
|
|
1619
|
-
const maxAttempts = 100;
|
|
1620
|
-
while (attempts < maxAttempts) {
|
|
1621
|
-
name = generateCredentialName2();
|
|
1622
|
-
const [existing] = await this.pool.query(
|
|
1623
|
-
`SELECT name FROM credentials WHERE name = ?`,
|
|
1624
|
-
[name]
|
|
1625
|
-
);
|
|
1626
|
-
if (existing.length === 0) break;
|
|
1627
|
-
attempts++;
|
|
1628
|
-
}
|
|
1629
|
-
if (attempts >= maxAttempts) {
|
|
1630
|
-
throw new Error(`Failed to generate unique credential name after ${maxAttempts} attempts`);
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
const secret = generateSecret2();
|
|
1634
|
-
const encryptedSecret = await encryptSecret2(secret, this.masterEncryptionKey);
|
|
1635
|
-
await this.pool.query(
|
|
1636
|
-
`INSERT INTO credentials (name, secret, created_at, expires_at, last_used)
|
|
1637
|
-
VALUES (?, ?, ?, ?, ?)`,
|
|
1638
|
-
[name, encryptedSecret, now, expiresAt, now]
|
|
1639
|
-
);
|
|
1640
|
-
return {
|
|
1641
|
-
name,
|
|
1642
|
-
secret,
|
|
1643
|
-
createdAt: now,
|
|
1644
|
-
expiresAt,
|
|
1645
|
-
lastUsed: now
|
|
1646
|
-
};
|
|
1647
|
-
}
|
|
1648
|
-
async getCredential(name) {
|
|
1649
|
-
const [rows] = await this.pool.query(
|
|
1650
|
-
`SELECT * FROM credentials WHERE name = ? AND expires_at > ?`,
|
|
1651
|
-
[name, Date.now()]
|
|
1652
|
-
);
|
|
1653
|
-
if (rows.length === 0) return null;
|
|
1654
|
-
try {
|
|
1655
|
-
const { decryptSecret: decryptSecret2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
1656
|
-
const decryptedSecret = await decryptSecret2(rows[0].secret, this.masterEncryptionKey);
|
|
1657
|
-
return {
|
|
1658
|
-
name: rows[0].name,
|
|
1659
|
-
secret: decryptedSecret,
|
|
1660
|
-
createdAt: Number(rows[0].created_at),
|
|
1661
|
-
expiresAt: Number(rows[0].expires_at),
|
|
1662
|
-
lastUsed: Number(rows[0].last_used)
|
|
1663
|
-
};
|
|
1664
|
-
} catch (error) {
|
|
1665
|
-
console.error(`Failed to decrypt secret for credential '${name}':`, error);
|
|
1666
|
-
return null;
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
async updateCredentialUsage(name, lastUsed, expiresAt) {
|
|
1670
|
-
await this.pool.query(
|
|
1671
|
-
`UPDATE credentials SET last_used = ?, expires_at = ? WHERE name = ?`,
|
|
1672
|
-
[lastUsed, expiresAt, name]
|
|
1673
|
-
);
|
|
1674
|
-
}
|
|
1675
|
-
async deleteExpiredCredentials(now) {
|
|
1676
|
-
const [result] = await this.pool.query(
|
|
1677
|
-
`DELETE FROM credentials WHERE expires_at < ?`,
|
|
1678
|
-
[now]
|
|
1679
|
-
);
|
|
1680
|
-
return result.affectedRows;
|
|
1681
|
-
}
|
|
1682
1111
|
// ===== Rate Limiting =====
|
|
1683
1112
|
async checkRateLimit(identifier, limit, windowMs) {
|
|
1684
1113
|
const now = Date.now();
|
|
@@ -1734,17 +1163,13 @@ var init_mysql = __esm({
|
|
|
1734
1163
|
const [rows] = await this.pool.query("SELECT COUNT(*) as count FROM offers");
|
|
1735
1164
|
return Number(rows[0].count);
|
|
1736
1165
|
}
|
|
1737
|
-
async
|
|
1166
|
+
async getOfferCountByPublicKey(publicKey) {
|
|
1738
1167
|
const [rows] = await this.pool.query(
|
|
1739
|
-
"SELECT COUNT(*) as count FROM offers WHERE
|
|
1740
|
-
[
|
|
1168
|
+
"SELECT COUNT(*) as count FROM offers WHERE public_key = ?",
|
|
1169
|
+
[publicKey]
|
|
1741
1170
|
);
|
|
1742
1171
|
return Number(rows[0].count);
|
|
1743
1172
|
}
|
|
1744
|
-
async getCredentialCount() {
|
|
1745
|
-
const [rows] = await this.pool.query("SELECT COUNT(*) as count FROM credentials");
|
|
1746
|
-
return Number(rows[0].count);
|
|
1747
|
-
}
|
|
1748
1173
|
async getIceCandidateCount(offerId) {
|
|
1749
1174
|
const [rows] = await this.pool.query(
|
|
1750
1175
|
"SELECT COUNT(*) as count FROM ice_candidates WHERE offer_id = ?",
|
|
@@ -1756,13 +1181,13 @@ var init_mysql = __esm({
|
|
|
1756
1181
|
rowToOffer(row) {
|
|
1757
1182
|
return {
|
|
1758
1183
|
id: row.id,
|
|
1759
|
-
|
|
1184
|
+
publicKey: row.public_key,
|
|
1760
1185
|
tags: typeof row.tags === "string" ? JSON.parse(row.tags) : row.tags,
|
|
1761
1186
|
sdp: row.sdp,
|
|
1762
1187
|
createdAt: Number(row.created_at),
|
|
1763
1188
|
expiresAt: Number(row.expires_at),
|
|
1764
1189
|
lastSeen: Number(row.last_seen),
|
|
1765
|
-
|
|
1190
|
+
answererPublicKey: row.answerer_public_key || void 0,
|
|
1766
1191
|
answerSdp: row.answer_sdp || void 0,
|
|
1767
1192
|
answeredAt: row.answered_at ? Number(row.answered_at) : void 0,
|
|
1768
1193
|
matchedTags: row.matched_tags ? typeof row.matched_tags === "string" ? JSON.parse(row.matched_tags) : row.matched_tags : void 0
|
|
@@ -1772,7 +1197,7 @@ var init_mysql = __esm({
|
|
|
1772
1197
|
return {
|
|
1773
1198
|
id: Number(row.id),
|
|
1774
1199
|
offerId: row.offer_id,
|
|
1775
|
-
|
|
1200
|
+
publicKey: row.public_key,
|
|
1776
1201
|
role: row.role,
|
|
1777
1202
|
candidate: typeof row.candidate === "string" ? JSON.parse(row.candidate) : row.candidate,
|
|
1778
1203
|
createdAt: Number(row.created_at)
|
|
@@ -1787,32 +1212,29 @@ var postgres_exports = {};
|
|
|
1787
1212
|
__export(postgres_exports, {
|
|
1788
1213
|
PostgreSQLStorage: () => PostgreSQLStorage
|
|
1789
1214
|
});
|
|
1790
|
-
var import_pg,
|
|
1215
|
+
var import_pg, PostgreSQLStorage;
|
|
1791
1216
|
var init_postgres = __esm({
|
|
1792
1217
|
"src/storage/postgres.ts"() {
|
|
1793
1218
|
"use strict";
|
|
1794
1219
|
import_pg = require("pg");
|
|
1795
1220
|
init_hash_id();
|
|
1796
|
-
YEAR_IN_MS4 = 365 * 24 * 60 * 60 * 1e3;
|
|
1797
1221
|
PostgreSQLStorage = class _PostgreSQLStorage {
|
|
1798
|
-
constructor(pool
|
|
1222
|
+
constructor(pool) {
|
|
1799
1223
|
this.pool = pool;
|
|
1800
|
-
this.masterEncryptionKey = masterEncryptionKey;
|
|
1801
1224
|
}
|
|
1802
1225
|
/**
|
|
1803
1226
|
* Creates a new PostgreSQL storage instance with connection pooling
|
|
1804
1227
|
* @param connectionString PostgreSQL connection URL
|
|
1805
|
-
* @param masterEncryptionKey 64-char hex string for encrypting secrets
|
|
1806
1228
|
* @param poolSize Maximum number of connections in the pool
|
|
1807
1229
|
*/
|
|
1808
|
-
static async create(connectionString,
|
|
1230
|
+
static async create(connectionString, poolSize = 10) {
|
|
1809
1231
|
const pool = new import_pg.Pool({
|
|
1810
1232
|
connectionString,
|
|
1811
1233
|
max: poolSize,
|
|
1812
1234
|
idleTimeoutMillis: 3e4,
|
|
1813
1235
|
connectionTimeoutMillis: 5e3
|
|
1814
1236
|
});
|
|
1815
|
-
const storage = new _PostgreSQLStorage(pool
|
|
1237
|
+
const storage = new _PostgreSQLStorage(pool);
|
|
1816
1238
|
await storage.initializeDatabase();
|
|
1817
1239
|
return storage;
|
|
1818
1240
|
}
|
|
@@ -1820,49 +1242,48 @@ var init_postgres = __esm({
|
|
|
1820
1242
|
const client = await this.pool.connect();
|
|
1821
1243
|
try {
|
|
1822
1244
|
await client.query(`
|
|
1823
|
-
CREATE TABLE IF NOT EXISTS
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1245
|
+
CREATE TABLE IF NOT EXISTS identities (
|
|
1246
|
+
public_key CHAR(64) PRIMARY KEY,
|
|
1247
|
+
created_at BIGINT NOT NULL,
|
|
1248
|
+
expires_at BIGINT NOT NULL,
|
|
1249
|
+
last_used BIGINT NOT NULL
|
|
1250
|
+
)
|
|
1251
|
+
`);
|
|
1252
|
+
await client.query(`CREATE INDEX IF NOT EXISTS idx_identities_expires ON identities(expires_at)`);
|
|
1253
|
+
await client.query(`
|
|
1254
|
+
CREATE TABLE IF NOT EXISTS offers (
|
|
1255
|
+
id VARCHAR(64) PRIMARY KEY,
|
|
1256
|
+
public_key CHAR(64) NOT NULL REFERENCES identities(public_key) ON DELETE CASCADE,
|
|
1257
|
+
tags JSONB NOT NULL,
|
|
1258
|
+
sdp TEXT NOT NULL,
|
|
1828
1259
|
created_at BIGINT NOT NULL,
|
|
1829
1260
|
expires_at BIGINT NOT NULL,
|
|
1830
1261
|
last_seen BIGINT NOT NULL,
|
|
1831
|
-
|
|
1262
|
+
answerer_public_key CHAR(64),
|
|
1832
1263
|
answer_sdp TEXT,
|
|
1833
1264
|
answered_at BIGINT,
|
|
1834
1265
|
matched_tags JSONB
|
|
1835
1266
|
)
|
|
1836
1267
|
`);
|
|
1837
|
-
await client.query(`CREATE INDEX IF NOT EXISTS
|
|
1268
|
+
await client.query(`CREATE INDEX IF NOT EXISTS idx_offers_public_key ON offers(public_key)`);
|
|
1838
1269
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_offers_expires ON offers(expires_at)`);
|
|
1839
1270
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen)`);
|
|
1840
|
-
await client.query(`CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(
|
|
1271
|
+
await client.query(`CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_public_key)`);
|
|
1841
1272
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_offers_tags ON offers USING GIN(tags)`);
|
|
1842
1273
|
await client.query(`
|
|
1843
1274
|
CREATE TABLE IF NOT EXISTS ice_candidates (
|
|
1844
1275
|
id BIGSERIAL PRIMARY KEY,
|
|
1845
1276
|
offer_id VARCHAR(64) NOT NULL REFERENCES offers(id) ON DELETE CASCADE,
|
|
1846
|
-
|
|
1277
|
+
public_key CHAR(64) NOT NULL,
|
|
1847
1278
|
role VARCHAR(8) NOT NULL CHECK (role IN ('offerer', 'answerer')),
|
|
1848
1279
|
candidate JSONB NOT NULL,
|
|
1849
1280
|
created_at BIGINT NOT NULL
|
|
1850
1281
|
)
|
|
1851
1282
|
`);
|
|
1852
1283
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id)`);
|
|
1853
|
-
await client.query(`CREATE INDEX IF NOT EXISTS
|
|
1284
|
+
await client.query(`CREATE INDEX IF NOT EXISTS idx_ice_public_key ON ice_candidates(public_key)`);
|
|
1854
1285
|
await client.query(`CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at)`);
|
|
1855
1286
|
await client.query(`
|
|
1856
|
-
CREATE TABLE IF NOT EXISTS credentials (
|
|
1857
|
-
name VARCHAR(32) PRIMARY KEY,
|
|
1858
|
-
secret VARCHAR(512) NOT NULL UNIQUE,
|
|
1859
|
-
created_at BIGINT NOT NULL,
|
|
1860
|
-
expires_at BIGINT NOT NULL,
|
|
1861
|
-
last_used BIGINT NOT NULL
|
|
1862
|
-
)
|
|
1863
|
-
`);
|
|
1864
|
-
await client.query(`CREATE INDEX IF NOT EXISTS idx_credentials_expires ON credentials(expires_at)`);
|
|
1865
|
-
await client.query(`
|
|
1866
1287
|
CREATE TABLE IF NOT EXISTS rate_limits (
|
|
1867
1288
|
identifier VARCHAR(255) PRIMARY KEY,
|
|
1868
1289
|
count INT NOT NULL,
|
|
@@ -1892,13 +1313,13 @@ var init_postgres = __esm({
|
|
|
1892
1313
|
for (const request of offers) {
|
|
1893
1314
|
const id = request.id || await generateOfferHash(request.sdp);
|
|
1894
1315
|
await client.query(
|
|
1895
|
-
`INSERT INTO offers (id,
|
|
1316
|
+
`INSERT INTO offers (id, public_key, tags, sdp, created_at, expires_at, last_seen)
|
|
1896
1317
|
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
1897
|
-
[id, request.
|
|
1318
|
+
[id, request.publicKey, JSON.stringify(request.tags), request.sdp, now, request.expiresAt, now]
|
|
1898
1319
|
);
|
|
1899
1320
|
created.push({
|
|
1900
1321
|
id,
|
|
1901
|
-
|
|
1322
|
+
publicKey: request.publicKey,
|
|
1902
1323
|
tags: request.tags,
|
|
1903
1324
|
sdp: request.sdp,
|
|
1904
1325
|
createdAt: now,
|
|
@@ -1915,10 +1336,10 @@ var init_postgres = __esm({
|
|
|
1915
1336
|
}
|
|
1916
1337
|
return created;
|
|
1917
1338
|
}
|
|
1918
|
-
async
|
|
1339
|
+
async getOffersByPublicKey(publicKey) {
|
|
1919
1340
|
const result = await this.pool.query(
|
|
1920
|
-
`SELECT * FROM offers WHERE
|
|
1921
|
-
[
|
|
1341
|
+
`SELECT * FROM offers WHERE public_key = $1 AND expires_at > $2 ORDER BY last_seen DESC`,
|
|
1342
|
+
[publicKey, Date.now()]
|
|
1922
1343
|
);
|
|
1923
1344
|
return result.rows.map((row) => this.rowToOffer(row));
|
|
1924
1345
|
}
|
|
@@ -1929,10 +1350,10 @@ var init_postgres = __esm({
|
|
|
1929
1350
|
);
|
|
1930
1351
|
return result.rows.length > 0 ? this.rowToOffer(result.rows[0]) : null;
|
|
1931
1352
|
}
|
|
1932
|
-
async deleteOffer(offerId,
|
|
1353
|
+
async deleteOffer(offerId, ownerPublicKey) {
|
|
1933
1354
|
const result = await this.pool.query(
|
|
1934
|
-
`DELETE FROM offers WHERE id = $1 AND
|
|
1935
|
-
[offerId,
|
|
1355
|
+
`DELETE FROM offers WHERE id = $1 AND public_key = $2`,
|
|
1356
|
+
[offerId, ownerPublicKey]
|
|
1936
1357
|
);
|
|
1937
1358
|
return (result.rowCount ?? 0) > 0;
|
|
1938
1359
|
}
|
|
@@ -1943,57 +1364,57 @@ var init_postgres = __esm({
|
|
|
1943
1364
|
);
|
|
1944
1365
|
return result.rowCount ?? 0;
|
|
1945
1366
|
}
|
|
1946
|
-
async answerOffer(offerId,
|
|
1367
|
+
async answerOffer(offerId, answererPublicKey, answerSdp, matchedTags) {
|
|
1947
1368
|
const offer = await this.getOfferById(offerId);
|
|
1948
1369
|
if (!offer) {
|
|
1949
1370
|
return { success: false, error: "Offer not found or expired" };
|
|
1950
1371
|
}
|
|
1951
|
-
if (offer.
|
|
1372
|
+
if (offer.answererPublicKey) {
|
|
1952
1373
|
return { success: false, error: "Offer already answered" };
|
|
1953
1374
|
}
|
|
1954
1375
|
const matchedTagsJson = matchedTags ? JSON.stringify(matchedTags) : null;
|
|
1955
1376
|
const result = await this.pool.query(
|
|
1956
|
-
`UPDATE offers SET
|
|
1957
|
-
WHERE id = $5 AND
|
|
1958
|
-
[
|
|
1377
|
+
`UPDATE offers SET answerer_public_key = $1, answer_sdp = $2, answered_at = $3, matched_tags = $4
|
|
1378
|
+
WHERE id = $5 AND answerer_public_key IS NULL`,
|
|
1379
|
+
[answererPublicKey, answerSdp, Date.now(), matchedTagsJson, offerId]
|
|
1959
1380
|
);
|
|
1960
1381
|
if ((result.rowCount ?? 0) === 0) {
|
|
1961
1382
|
return { success: false, error: "Offer already answered (race condition)" };
|
|
1962
1383
|
}
|
|
1963
1384
|
return { success: true };
|
|
1964
1385
|
}
|
|
1965
|
-
async getAnsweredOffers(
|
|
1386
|
+
async getAnsweredOffers(offererPublicKey) {
|
|
1966
1387
|
const result = await this.pool.query(
|
|
1967
1388
|
`SELECT * FROM offers
|
|
1968
|
-
WHERE
|
|
1389
|
+
WHERE public_key = $1 AND answerer_public_key IS NOT NULL AND expires_at > $2
|
|
1969
1390
|
ORDER BY answered_at DESC`,
|
|
1970
|
-
[
|
|
1391
|
+
[offererPublicKey, Date.now()]
|
|
1971
1392
|
);
|
|
1972
1393
|
return result.rows.map((row) => this.rowToOffer(row));
|
|
1973
1394
|
}
|
|
1974
|
-
async getOffersAnsweredBy(
|
|
1395
|
+
async getOffersAnsweredBy(answererPublicKey) {
|
|
1975
1396
|
const result = await this.pool.query(
|
|
1976
1397
|
`SELECT * FROM offers
|
|
1977
|
-
WHERE
|
|
1398
|
+
WHERE answerer_public_key = $1 AND expires_at > $2
|
|
1978
1399
|
ORDER BY answered_at DESC`,
|
|
1979
|
-
[
|
|
1400
|
+
[answererPublicKey, Date.now()]
|
|
1980
1401
|
);
|
|
1981
1402
|
return result.rows.map((row) => this.rowToOffer(row));
|
|
1982
1403
|
}
|
|
1983
1404
|
// ===== Discovery =====
|
|
1984
|
-
async discoverOffers(tags,
|
|
1405
|
+
async discoverOffers(tags, excludePublicKey, limit, offset) {
|
|
1985
1406
|
if (tags.length === 0) return [];
|
|
1986
1407
|
let query = `
|
|
1987
1408
|
SELECT DISTINCT o.* FROM offers o
|
|
1988
1409
|
WHERE o.tags ?| $1
|
|
1989
1410
|
AND o.expires_at > $2
|
|
1990
|
-
AND o.
|
|
1411
|
+
AND o.answerer_public_key IS NULL
|
|
1991
1412
|
`;
|
|
1992
1413
|
const params = [tags, Date.now()];
|
|
1993
1414
|
let paramIndex = 3;
|
|
1994
|
-
if (
|
|
1995
|
-
query += ` AND o.
|
|
1996
|
-
params.push(
|
|
1415
|
+
if (excludePublicKey) {
|
|
1416
|
+
query += ` AND o.public_key != $${paramIndex}`;
|
|
1417
|
+
params.push(excludePublicKey);
|
|
1997
1418
|
paramIndex++;
|
|
1998
1419
|
}
|
|
1999
1420
|
query += ` ORDER BY o.created_at DESC LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`;
|
|
@@ -2001,26 +1422,26 @@ var init_postgres = __esm({
|
|
|
2001
1422
|
const result = await this.pool.query(query, params);
|
|
2002
1423
|
return result.rows.map((row) => this.rowToOffer(row));
|
|
2003
1424
|
}
|
|
2004
|
-
async getRandomOffer(tags,
|
|
1425
|
+
async getRandomOffer(tags, excludePublicKey) {
|
|
2005
1426
|
if (tags.length === 0) return null;
|
|
2006
1427
|
let query = `
|
|
2007
1428
|
SELECT DISTINCT o.* FROM offers o
|
|
2008
1429
|
WHERE o.tags ?| $1
|
|
2009
1430
|
AND o.expires_at > $2
|
|
2010
|
-
AND o.
|
|
1431
|
+
AND o.answerer_public_key IS NULL
|
|
2011
1432
|
`;
|
|
2012
1433
|
const params = [tags, Date.now()];
|
|
2013
1434
|
let paramIndex = 3;
|
|
2014
|
-
if (
|
|
2015
|
-
query += ` AND o.
|
|
2016
|
-
params.push(
|
|
1435
|
+
if (excludePublicKey) {
|
|
1436
|
+
query += ` AND o.public_key != $${paramIndex}`;
|
|
1437
|
+
params.push(excludePublicKey);
|
|
2017
1438
|
}
|
|
2018
1439
|
query += " ORDER BY RANDOM() LIMIT 1";
|
|
2019
1440
|
const result = await this.pool.query(query, params);
|
|
2020
1441
|
return result.rows.length > 0 ? this.rowToOffer(result.rows[0]) : null;
|
|
2021
1442
|
}
|
|
2022
1443
|
// ===== ICE Candidate Management =====
|
|
2023
|
-
async addIceCandidates(offerId,
|
|
1444
|
+
async addIceCandidates(offerId, publicKey, role, candidates) {
|
|
2024
1445
|
if (candidates.length === 0) return 0;
|
|
2025
1446
|
const baseTimestamp = Date.now();
|
|
2026
1447
|
const client = await this.pool.connect();
|
|
@@ -2028,9 +1449,9 @@ var init_postgres = __esm({
|
|
|
2028
1449
|
await client.query("BEGIN");
|
|
2029
1450
|
for (let i = 0; i < candidates.length; i++) {
|
|
2030
1451
|
await client.query(
|
|
2031
|
-
`INSERT INTO ice_candidates (offer_id,
|
|
1452
|
+
`INSERT INTO ice_candidates (offer_id, public_key, role, candidate, created_at)
|
|
2032
1453
|
VALUES ($1, $2, $3, $4, $5)`,
|
|
2033
|
-
[offerId,
|
|
1454
|
+
[offerId, publicKey, role, JSON.stringify(candidates[i]), baseTimestamp + i]
|
|
2034
1455
|
);
|
|
2035
1456
|
}
|
|
2036
1457
|
await client.query("COMMIT");
|
|
@@ -2053,23 +1474,23 @@ var init_postgres = __esm({
|
|
|
2053
1474
|
const result = await this.pool.query(query, params);
|
|
2054
1475
|
return result.rows.map((row) => this.rowToIceCandidate(row));
|
|
2055
1476
|
}
|
|
2056
|
-
async getIceCandidatesForMultipleOffers(offerIds,
|
|
1477
|
+
async getIceCandidatesForMultipleOffers(offerIds, publicKey, since) {
|
|
2057
1478
|
const resultMap = /* @__PURE__ */ new Map();
|
|
2058
1479
|
if (offerIds.length === 0) return resultMap;
|
|
2059
1480
|
if (offerIds.length > 1e3) {
|
|
2060
1481
|
throw new Error("Too many offer IDs (max 1000)");
|
|
2061
1482
|
}
|
|
2062
1483
|
let query = `
|
|
2063
|
-
SELECT ic.*, o.
|
|
1484
|
+
SELECT ic.*, o.public_key as offer_public_key
|
|
2064
1485
|
FROM ice_candidates ic
|
|
2065
1486
|
INNER JOIN offers o ON o.id = ic.offer_id
|
|
2066
1487
|
WHERE ic.offer_id = ANY($1)
|
|
2067
1488
|
AND (
|
|
2068
|
-
(o.
|
|
2069
|
-
OR (o.
|
|
1489
|
+
(o.public_key = $2 AND ic.role = 'answerer')
|
|
1490
|
+
OR (o.answerer_public_key = $2 AND ic.role = 'offerer')
|
|
2070
1491
|
)
|
|
2071
1492
|
`;
|
|
2072
|
-
const params = [offerIds,
|
|
1493
|
+
const params = [offerIds, publicKey];
|
|
2073
1494
|
if (since !== void 0) {
|
|
2074
1495
|
query += " AND ic.created_at > $3";
|
|
2075
1496
|
params.push(since);
|
|
@@ -2085,86 +1506,6 @@ var init_postgres = __esm({
|
|
|
2085
1506
|
}
|
|
2086
1507
|
return resultMap;
|
|
2087
1508
|
}
|
|
2088
|
-
// ===== Credential Management =====
|
|
2089
|
-
async generateCredentials(request) {
|
|
2090
|
-
const now = Date.now();
|
|
2091
|
-
const expiresAt = request.expiresAt || now + YEAR_IN_MS4;
|
|
2092
|
-
const { generateCredentialName: generateCredentialName2, generateSecret: generateSecret2, encryptSecret: encryptSecret2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
2093
|
-
let name;
|
|
2094
|
-
if (request.name) {
|
|
2095
|
-
const existing = await this.pool.query(
|
|
2096
|
-
`SELECT name FROM credentials WHERE name = $1`,
|
|
2097
|
-
[request.name]
|
|
2098
|
-
);
|
|
2099
|
-
if (existing.rows.length > 0) {
|
|
2100
|
-
throw new Error("Username already taken");
|
|
2101
|
-
}
|
|
2102
|
-
name = request.name;
|
|
2103
|
-
} else {
|
|
2104
|
-
let attempts = 0;
|
|
2105
|
-
const maxAttempts = 100;
|
|
2106
|
-
while (attempts < maxAttempts) {
|
|
2107
|
-
name = generateCredentialName2();
|
|
2108
|
-
const existing = await this.pool.query(
|
|
2109
|
-
`SELECT name FROM credentials WHERE name = $1`,
|
|
2110
|
-
[name]
|
|
2111
|
-
);
|
|
2112
|
-
if (existing.rows.length === 0) break;
|
|
2113
|
-
attempts++;
|
|
2114
|
-
}
|
|
2115
|
-
if (attempts >= maxAttempts) {
|
|
2116
|
-
throw new Error(`Failed to generate unique credential name after ${maxAttempts} attempts`);
|
|
2117
|
-
}
|
|
2118
|
-
}
|
|
2119
|
-
const secret = generateSecret2();
|
|
2120
|
-
const encryptedSecret = await encryptSecret2(secret, this.masterEncryptionKey);
|
|
2121
|
-
await this.pool.query(
|
|
2122
|
-
`INSERT INTO credentials (name, secret, created_at, expires_at, last_used)
|
|
2123
|
-
VALUES ($1, $2, $3, $4, $5)`,
|
|
2124
|
-
[name, encryptedSecret, now, expiresAt, now]
|
|
2125
|
-
);
|
|
2126
|
-
return {
|
|
2127
|
-
name,
|
|
2128
|
-
secret,
|
|
2129
|
-
createdAt: now,
|
|
2130
|
-
expiresAt,
|
|
2131
|
-
lastUsed: now
|
|
2132
|
-
};
|
|
2133
|
-
}
|
|
2134
|
-
async getCredential(name) {
|
|
2135
|
-
const result = await this.pool.query(
|
|
2136
|
-
`SELECT * FROM credentials WHERE name = $1 AND expires_at > $2`,
|
|
2137
|
-
[name, Date.now()]
|
|
2138
|
-
);
|
|
2139
|
-
if (result.rows.length === 0) return null;
|
|
2140
|
-
try {
|
|
2141
|
-
const { decryptSecret: decryptSecret2 } = await Promise.resolve().then(() => (init_crypto(), crypto_exports));
|
|
2142
|
-
const decryptedSecret = await decryptSecret2(result.rows[0].secret, this.masterEncryptionKey);
|
|
2143
|
-
return {
|
|
2144
|
-
name: result.rows[0].name,
|
|
2145
|
-
secret: decryptedSecret,
|
|
2146
|
-
createdAt: Number(result.rows[0].created_at),
|
|
2147
|
-
expiresAt: Number(result.rows[0].expires_at),
|
|
2148
|
-
lastUsed: Number(result.rows[0].last_used)
|
|
2149
|
-
};
|
|
2150
|
-
} catch (error) {
|
|
2151
|
-
console.error(`Failed to decrypt secret for credential '${name}':`, error);
|
|
2152
|
-
return null;
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
async updateCredentialUsage(name, lastUsed, expiresAt) {
|
|
2156
|
-
await this.pool.query(
|
|
2157
|
-
`UPDATE credentials SET last_used = $1, expires_at = $2 WHERE name = $3`,
|
|
2158
|
-
[lastUsed, expiresAt, name]
|
|
2159
|
-
);
|
|
2160
|
-
}
|
|
2161
|
-
async deleteExpiredCredentials(now) {
|
|
2162
|
-
const result = await this.pool.query(
|
|
2163
|
-
`DELETE FROM credentials WHERE expires_at < $1`,
|
|
2164
|
-
[now]
|
|
2165
|
-
);
|
|
2166
|
-
return result.rowCount ?? 0;
|
|
2167
|
-
}
|
|
2168
1509
|
// ===== Rate Limiting =====
|
|
2169
1510
|
async checkRateLimit(identifier, limit, windowMs) {
|
|
2170
1511
|
const now = Date.now();
|
|
@@ -2223,17 +1564,13 @@ var init_postgres = __esm({
|
|
|
2223
1564
|
const result = await this.pool.query("SELECT COUNT(*) as count FROM offers");
|
|
2224
1565
|
return Number(result.rows[0].count);
|
|
2225
1566
|
}
|
|
2226
|
-
async
|
|
1567
|
+
async getOfferCountByPublicKey(publicKey) {
|
|
2227
1568
|
const result = await this.pool.query(
|
|
2228
|
-
"SELECT COUNT(*) as count FROM offers WHERE
|
|
2229
|
-
[
|
|
1569
|
+
"SELECT COUNT(*) as count FROM offers WHERE public_key = $1",
|
|
1570
|
+
[publicKey]
|
|
2230
1571
|
);
|
|
2231
1572
|
return Number(result.rows[0].count);
|
|
2232
1573
|
}
|
|
2233
|
-
async getCredentialCount() {
|
|
2234
|
-
const result = await this.pool.query("SELECT COUNT(*) as count FROM credentials");
|
|
2235
|
-
return Number(result.rows[0].count);
|
|
2236
|
-
}
|
|
2237
1574
|
async getIceCandidateCount(offerId) {
|
|
2238
1575
|
const result = await this.pool.query(
|
|
2239
1576
|
"SELECT COUNT(*) as count FROM ice_candidates WHERE offer_id = $1",
|
|
@@ -2245,13 +1582,13 @@ var init_postgres = __esm({
|
|
|
2245
1582
|
rowToOffer(row) {
|
|
2246
1583
|
return {
|
|
2247
1584
|
id: row.id,
|
|
2248
|
-
|
|
1585
|
+
publicKey: row.public_key.trim(),
|
|
2249
1586
|
tags: typeof row.tags === "string" ? JSON.parse(row.tags) : row.tags,
|
|
2250
1587
|
sdp: row.sdp,
|
|
2251
1588
|
createdAt: Number(row.created_at),
|
|
2252
1589
|
expiresAt: Number(row.expires_at),
|
|
2253
1590
|
lastSeen: Number(row.last_seen),
|
|
2254
|
-
|
|
1591
|
+
answererPublicKey: row.answerer_public_key?.trim() || void 0,
|
|
2255
1592
|
answerSdp: row.answer_sdp || void 0,
|
|
2256
1593
|
answeredAt: row.answered_at ? Number(row.answered_at) : void 0,
|
|
2257
1594
|
matchedTags: row.matched_tags || void 0
|
|
@@ -2261,7 +1598,7 @@ var init_postgres = __esm({
|
|
|
2261
1598
|
return {
|
|
2262
1599
|
id: Number(row.id),
|
|
2263
1600
|
offerId: row.offer_id,
|
|
2264
|
-
|
|
1601
|
+
publicKey: row.public_key.trim(),
|
|
2265
1602
|
role: row.role,
|
|
2266
1603
|
candidate: typeof row.candidate === "string" ? JSON.parse(row.candidate) : row.candidate,
|
|
2267
1604
|
createdAt: Number(row.created_at)
|
|
@@ -2278,10 +1615,610 @@ var import_node_server = require("@hono/node-server");
|
|
|
2278
1615
|
var import_hono = require("hono");
|
|
2279
1616
|
var import_cors = require("hono/cors");
|
|
2280
1617
|
|
|
1618
|
+
// src/crypto.ts
|
|
1619
|
+
var import_node_buffer = require("node:buffer");
|
|
1620
|
+
|
|
1621
|
+
// node_modules/@noble/ed25519/index.js
|
|
1622
|
+
var ed25519_CURVE = {
|
|
1623
|
+
p: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedn,
|
|
1624
|
+
n: 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3edn,
|
|
1625
|
+
h: 8n,
|
|
1626
|
+
a: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffecn,
|
|
1627
|
+
d: 0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3n,
|
|
1628
|
+
Gx: 0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an,
|
|
1629
|
+
Gy: 0x6666666666666666666666666666666666666666666666666666666666666658n
|
|
1630
|
+
};
|
|
1631
|
+
var { p: P, n: N, Gx, Gy, a: _a, d: _d, h } = ed25519_CURVE;
|
|
1632
|
+
var L = 32;
|
|
1633
|
+
var L2 = 64;
|
|
1634
|
+
var captureTrace = (...args) => {
|
|
1635
|
+
if ("captureStackTrace" in Error && typeof Error.captureStackTrace === "function") {
|
|
1636
|
+
Error.captureStackTrace(...args);
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
var err = (message = "") => {
|
|
1640
|
+
const e = new Error(message);
|
|
1641
|
+
captureTrace(e, err);
|
|
1642
|
+
throw e;
|
|
1643
|
+
};
|
|
1644
|
+
var isBig = (n) => typeof n === "bigint";
|
|
1645
|
+
var isStr = (s) => typeof s === "string";
|
|
1646
|
+
var isBytes = (a) => a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
|
|
1647
|
+
var abytes = (value, length, title = "") => {
|
|
1648
|
+
const bytes = isBytes(value);
|
|
1649
|
+
const len = value?.length;
|
|
1650
|
+
const needsLen = length !== void 0;
|
|
1651
|
+
if (!bytes || needsLen && len !== length) {
|
|
1652
|
+
const prefix = title && `"${title}" `;
|
|
1653
|
+
const ofLen = needsLen ? ` of length ${length}` : "";
|
|
1654
|
+
const got = bytes ? `length=${len}` : `type=${typeof value}`;
|
|
1655
|
+
err(prefix + "expected Uint8Array" + ofLen + ", got " + got);
|
|
1656
|
+
}
|
|
1657
|
+
return value;
|
|
1658
|
+
};
|
|
1659
|
+
var u8n = (len) => new Uint8Array(len);
|
|
1660
|
+
var u8fr = (buf) => Uint8Array.from(buf);
|
|
1661
|
+
var padh = (n, pad) => n.toString(16).padStart(pad, "0");
|
|
1662
|
+
var bytesToHex = (b) => Array.from(abytes(b)).map((e) => padh(e, 2)).join("");
|
|
1663
|
+
var C = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 };
|
|
1664
|
+
var _ch = (ch) => {
|
|
1665
|
+
if (ch >= C._0 && ch <= C._9)
|
|
1666
|
+
return ch - C._0;
|
|
1667
|
+
if (ch >= C.A && ch <= C.F)
|
|
1668
|
+
return ch - (C.A - 10);
|
|
1669
|
+
if (ch >= C.a && ch <= C.f)
|
|
1670
|
+
return ch - (C.a - 10);
|
|
1671
|
+
return;
|
|
1672
|
+
};
|
|
1673
|
+
var hexToBytes = (hex) => {
|
|
1674
|
+
const e = "hex invalid";
|
|
1675
|
+
if (!isStr(hex))
|
|
1676
|
+
return err(e);
|
|
1677
|
+
const hl = hex.length;
|
|
1678
|
+
const al = hl / 2;
|
|
1679
|
+
if (hl % 2)
|
|
1680
|
+
return err(e);
|
|
1681
|
+
const array = u8n(al);
|
|
1682
|
+
for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {
|
|
1683
|
+
const n1 = _ch(hex.charCodeAt(hi));
|
|
1684
|
+
const n2 = _ch(hex.charCodeAt(hi + 1));
|
|
1685
|
+
if (n1 === void 0 || n2 === void 0)
|
|
1686
|
+
return err(e);
|
|
1687
|
+
array[ai] = n1 * 16 + n2;
|
|
1688
|
+
}
|
|
1689
|
+
return array;
|
|
1690
|
+
};
|
|
1691
|
+
var cr = () => globalThis?.crypto;
|
|
1692
|
+
var subtle = () => cr()?.subtle ?? err("crypto.subtle must be defined, consider polyfill");
|
|
1693
|
+
var concatBytes = (...arrs) => {
|
|
1694
|
+
const r = u8n(arrs.reduce((sum, a) => sum + abytes(a).length, 0));
|
|
1695
|
+
let pad = 0;
|
|
1696
|
+
arrs.forEach((a) => {
|
|
1697
|
+
r.set(a, pad);
|
|
1698
|
+
pad += a.length;
|
|
1699
|
+
});
|
|
1700
|
+
return r;
|
|
1701
|
+
};
|
|
1702
|
+
var big = BigInt;
|
|
1703
|
+
var assertRange = (n, min, max, msg = "bad number: out of range") => isBig(n) && min <= n && n < max ? n : err(msg);
|
|
1704
|
+
var M = (a, b = P) => {
|
|
1705
|
+
const r = a % b;
|
|
1706
|
+
return r >= 0n ? r : b + r;
|
|
1707
|
+
};
|
|
1708
|
+
var modN = (a) => M(a, N);
|
|
1709
|
+
var invert = (num, md) => {
|
|
1710
|
+
if (num === 0n || md <= 0n)
|
|
1711
|
+
err("no inverse n=" + num + " mod=" + md);
|
|
1712
|
+
let a = M(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n;
|
|
1713
|
+
while (a !== 0n) {
|
|
1714
|
+
const q = b / a, r = b % a;
|
|
1715
|
+
const m = x - u * q, n = y - v * q;
|
|
1716
|
+
b = a, a = r, x = u, y = v, u = m, v = n;
|
|
1717
|
+
}
|
|
1718
|
+
return b === 1n ? M(x, md) : err("no inverse");
|
|
1719
|
+
};
|
|
1720
|
+
var apoint = (p) => p instanceof Point ? p : err("Point expected");
|
|
1721
|
+
var B256 = 2n ** 256n;
|
|
1722
|
+
var Point = class _Point {
|
|
1723
|
+
static BASE;
|
|
1724
|
+
static ZERO;
|
|
1725
|
+
X;
|
|
1726
|
+
Y;
|
|
1727
|
+
Z;
|
|
1728
|
+
T;
|
|
1729
|
+
constructor(X, Y, Z, T) {
|
|
1730
|
+
const max = B256;
|
|
1731
|
+
this.X = assertRange(X, 0n, max);
|
|
1732
|
+
this.Y = assertRange(Y, 0n, max);
|
|
1733
|
+
this.Z = assertRange(Z, 1n, max);
|
|
1734
|
+
this.T = assertRange(T, 0n, max);
|
|
1735
|
+
Object.freeze(this);
|
|
1736
|
+
}
|
|
1737
|
+
static CURVE() {
|
|
1738
|
+
return ed25519_CURVE;
|
|
1739
|
+
}
|
|
1740
|
+
static fromAffine(p) {
|
|
1741
|
+
return new _Point(p.x, p.y, 1n, M(p.x * p.y));
|
|
1742
|
+
}
|
|
1743
|
+
/** RFC8032 5.1.3: Uint8Array to Point. */
|
|
1744
|
+
static fromBytes(hex, zip215 = false) {
|
|
1745
|
+
const d = _d;
|
|
1746
|
+
const normed = u8fr(abytes(hex, L));
|
|
1747
|
+
const lastByte = hex[31];
|
|
1748
|
+
normed[31] = lastByte & ~128;
|
|
1749
|
+
const y = bytesToNumLE(normed);
|
|
1750
|
+
const max = zip215 ? B256 : P;
|
|
1751
|
+
assertRange(y, 0n, max);
|
|
1752
|
+
const y2 = M(y * y);
|
|
1753
|
+
const u = M(y2 - 1n);
|
|
1754
|
+
const v = M(d * y2 + 1n);
|
|
1755
|
+
let { isValid, value: x } = uvRatio(u, v);
|
|
1756
|
+
if (!isValid)
|
|
1757
|
+
err("bad point: y not sqrt");
|
|
1758
|
+
const isXOdd = (x & 1n) === 1n;
|
|
1759
|
+
const isLastByteOdd = (lastByte & 128) !== 0;
|
|
1760
|
+
if (!zip215 && x === 0n && isLastByteOdd)
|
|
1761
|
+
err("bad point: x==0, isLastByteOdd");
|
|
1762
|
+
if (isLastByteOdd !== isXOdd)
|
|
1763
|
+
x = M(-x);
|
|
1764
|
+
return new _Point(x, y, 1n, M(x * y));
|
|
1765
|
+
}
|
|
1766
|
+
static fromHex(hex, zip215) {
|
|
1767
|
+
return _Point.fromBytes(hexToBytes(hex), zip215);
|
|
1768
|
+
}
|
|
1769
|
+
get x() {
|
|
1770
|
+
return this.toAffine().x;
|
|
1771
|
+
}
|
|
1772
|
+
get y() {
|
|
1773
|
+
return this.toAffine().y;
|
|
1774
|
+
}
|
|
1775
|
+
/** Checks if the point is valid and on-curve. */
|
|
1776
|
+
assertValidity() {
|
|
1777
|
+
const a = _a;
|
|
1778
|
+
const d = _d;
|
|
1779
|
+
const p = this;
|
|
1780
|
+
if (p.is0())
|
|
1781
|
+
return err("bad point: ZERO");
|
|
1782
|
+
const { X, Y, Z, T } = p;
|
|
1783
|
+
const X2 = M(X * X);
|
|
1784
|
+
const Y2 = M(Y * Y);
|
|
1785
|
+
const Z2 = M(Z * Z);
|
|
1786
|
+
const Z4 = M(Z2 * Z2);
|
|
1787
|
+
const aX2 = M(X2 * a);
|
|
1788
|
+
const left = M(Z2 * M(aX2 + Y2));
|
|
1789
|
+
const right = M(Z4 + M(d * M(X2 * Y2)));
|
|
1790
|
+
if (left !== right)
|
|
1791
|
+
return err("bad point: equation left != right (1)");
|
|
1792
|
+
const XY = M(X * Y);
|
|
1793
|
+
const ZT = M(Z * T);
|
|
1794
|
+
if (XY !== ZT)
|
|
1795
|
+
return err("bad point: equation left != right (2)");
|
|
1796
|
+
return this;
|
|
1797
|
+
}
|
|
1798
|
+
/** Equality check: compare points P&Q. */
|
|
1799
|
+
equals(other) {
|
|
1800
|
+
const { X: X1, Y: Y1, Z: Z1 } = this;
|
|
1801
|
+
const { X: X2, Y: Y2, Z: Z2 } = apoint(other);
|
|
1802
|
+
const X1Z2 = M(X1 * Z2);
|
|
1803
|
+
const X2Z1 = M(X2 * Z1);
|
|
1804
|
+
const Y1Z2 = M(Y1 * Z2);
|
|
1805
|
+
const Y2Z1 = M(Y2 * Z1);
|
|
1806
|
+
return X1Z2 === X2Z1 && Y1Z2 === Y2Z1;
|
|
1807
|
+
}
|
|
1808
|
+
is0() {
|
|
1809
|
+
return this.equals(I);
|
|
1810
|
+
}
|
|
1811
|
+
/** Flip point over y coordinate. */
|
|
1812
|
+
negate() {
|
|
1813
|
+
return new _Point(M(-this.X), this.Y, this.Z, M(-this.T));
|
|
1814
|
+
}
|
|
1815
|
+
/** Point doubling. Complete formula. Cost: `4M + 4S + 1*a + 6add + 1*2`. */
|
|
1816
|
+
double() {
|
|
1817
|
+
const { X: X1, Y: Y1, Z: Z1 } = this;
|
|
1818
|
+
const a = _a;
|
|
1819
|
+
const A = M(X1 * X1);
|
|
1820
|
+
const B = M(Y1 * Y1);
|
|
1821
|
+
const C2 = M(2n * M(Z1 * Z1));
|
|
1822
|
+
const D = M(a * A);
|
|
1823
|
+
const x1y1 = X1 + Y1;
|
|
1824
|
+
const E = M(M(x1y1 * x1y1) - A - B);
|
|
1825
|
+
const G2 = D + B;
|
|
1826
|
+
const F = G2 - C2;
|
|
1827
|
+
const H = D - B;
|
|
1828
|
+
const X3 = M(E * F);
|
|
1829
|
+
const Y3 = M(G2 * H);
|
|
1830
|
+
const T3 = M(E * H);
|
|
1831
|
+
const Z3 = M(F * G2);
|
|
1832
|
+
return new _Point(X3, Y3, Z3, T3);
|
|
1833
|
+
}
|
|
1834
|
+
/** Point addition. Complete formula. Cost: `8M + 1*k + 8add + 1*2`. */
|
|
1835
|
+
add(other) {
|
|
1836
|
+
const { X: X1, Y: Y1, Z: Z1, T: T1 } = this;
|
|
1837
|
+
const { X: X2, Y: Y2, Z: Z2, T: T2 } = apoint(other);
|
|
1838
|
+
const a = _a;
|
|
1839
|
+
const d = _d;
|
|
1840
|
+
const A = M(X1 * X2);
|
|
1841
|
+
const B = M(Y1 * Y2);
|
|
1842
|
+
const C2 = M(T1 * d * T2);
|
|
1843
|
+
const D = M(Z1 * Z2);
|
|
1844
|
+
const E = M((X1 + Y1) * (X2 + Y2) - A - B);
|
|
1845
|
+
const F = M(D - C2);
|
|
1846
|
+
const G2 = M(D + C2);
|
|
1847
|
+
const H = M(B - a * A);
|
|
1848
|
+
const X3 = M(E * F);
|
|
1849
|
+
const Y3 = M(G2 * H);
|
|
1850
|
+
const T3 = M(E * H);
|
|
1851
|
+
const Z3 = M(F * G2);
|
|
1852
|
+
return new _Point(X3, Y3, Z3, T3);
|
|
1853
|
+
}
|
|
1854
|
+
subtract(other) {
|
|
1855
|
+
return this.add(apoint(other).negate());
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Point-by-scalar multiplication. Scalar must be in range 1 <= n < CURVE.n.
|
|
1859
|
+
* Uses {@link wNAF} for base point.
|
|
1860
|
+
* Uses fake point to mitigate side-channel leakage.
|
|
1861
|
+
* @param n scalar by which point is multiplied
|
|
1862
|
+
* @param safe safe mode guards against timing attacks; unsafe mode is faster
|
|
1863
|
+
*/
|
|
1864
|
+
multiply(n, safe = true) {
|
|
1865
|
+
if (!safe && (n === 0n || this.is0()))
|
|
1866
|
+
return I;
|
|
1867
|
+
assertRange(n, 1n, N);
|
|
1868
|
+
if (n === 1n)
|
|
1869
|
+
return this;
|
|
1870
|
+
if (this.equals(G))
|
|
1871
|
+
return wNAF(n).p;
|
|
1872
|
+
let p = I;
|
|
1873
|
+
let f = G;
|
|
1874
|
+
for (let d = this; n > 0n; d = d.double(), n >>= 1n) {
|
|
1875
|
+
if (n & 1n)
|
|
1876
|
+
p = p.add(d);
|
|
1877
|
+
else if (safe)
|
|
1878
|
+
f = f.add(d);
|
|
1879
|
+
}
|
|
1880
|
+
return p;
|
|
1881
|
+
}
|
|
1882
|
+
multiplyUnsafe(scalar) {
|
|
1883
|
+
return this.multiply(scalar, false);
|
|
1884
|
+
}
|
|
1885
|
+
/** Convert point to 2d xy affine point. (X, Y, Z) ∋ (x=X/Z, y=Y/Z) */
|
|
1886
|
+
toAffine() {
|
|
1887
|
+
const { X, Y, Z } = this;
|
|
1888
|
+
if (this.equals(I))
|
|
1889
|
+
return { x: 0n, y: 1n };
|
|
1890
|
+
const iz = invert(Z, P);
|
|
1891
|
+
if (M(Z * iz) !== 1n)
|
|
1892
|
+
err("invalid inverse");
|
|
1893
|
+
const x = M(X * iz);
|
|
1894
|
+
const y = M(Y * iz);
|
|
1895
|
+
return { x, y };
|
|
1896
|
+
}
|
|
1897
|
+
toBytes() {
|
|
1898
|
+
const { x, y } = this.assertValidity().toAffine();
|
|
1899
|
+
const b = numTo32bLE(y);
|
|
1900
|
+
b[31] |= x & 1n ? 128 : 0;
|
|
1901
|
+
return b;
|
|
1902
|
+
}
|
|
1903
|
+
toHex() {
|
|
1904
|
+
return bytesToHex(this.toBytes());
|
|
1905
|
+
}
|
|
1906
|
+
clearCofactor() {
|
|
1907
|
+
return this.multiply(big(h), false);
|
|
1908
|
+
}
|
|
1909
|
+
isSmallOrder() {
|
|
1910
|
+
return this.clearCofactor().is0();
|
|
1911
|
+
}
|
|
1912
|
+
isTorsionFree() {
|
|
1913
|
+
let p = this.multiply(N / 2n, false).double();
|
|
1914
|
+
if (N % 2n)
|
|
1915
|
+
p = p.add(this);
|
|
1916
|
+
return p.is0();
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
var G = new Point(Gx, Gy, 1n, M(Gx * Gy));
|
|
1920
|
+
var I = new Point(0n, 1n, 1n, 0n);
|
|
1921
|
+
Point.BASE = G;
|
|
1922
|
+
Point.ZERO = I;
|
|
1923
|
+
var numTo32bLE = (num) => hexToBytes(padh(assertRange(num, 0n, B256), L2)).reverse();
|
|
1924
|
+
var bytesToNumLE = (b) => big("0x" + bytesToHex(u8fr(abytes(b)).reverse()));
|
|
1925
|
+
var pow2 = (x, power) => {
|
|
1926
|
+
let r = x;
|
|
1927
|
+
while (power-- > 0n) {
|
|
1928
|
+
r *= r;
|
|
1929
|
+
r %= P;
|
|
1930
|
+
}
|
|
1931
|
+
return r;
|
|
1932
|
+
};
|
|
1933
|
+
var pow_2_252_3 = (x) => {
|
|
1934
|
+
const x2 = x * x % P;
|
|
1935
|
+
const b2 = x2 * x % P;
|
|
1936
|
+
const b4 = pow2(b2, 2n) * b2 % P;
|
|
1937
|
+
const b5 = pow2(b4, 1n) * x % P;
|
|
1938
|
+
const b10 = pow2(b5, 5n) * b5 % P;
|
|
1939
|
+
const b20 = pow2(b10, 10n) * b10 % P;
|
|
1940
|
+
const b40 = pow2(b20, 20n) * b20 % P;
|
|
1941
|
+
const b80 = pow2(b40, 40n) * b40 % P;
|
|
1942
|
+
const b160 = pow2(b80, 80n) * b80 % P;
|
|
1943
|
+
const b240 = pow2(b160, 80n) * b80 % P;
|
|
1944
|
+
const b250 = pow2(b240, 10n) * b10 % P;
|
|
1945
|
+
const pow_p_5_8 = pow2(b250, 2n) * x % P;
|
|
1946
|
+
return { pow_p_5_8, b2 };
|
|
1947
|
+
};
|
|
1948
|
+
var RM1 = 0x2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0n;
|
|
1949
|
+
var uvRatio = (u, v) => {
|
|
1950
|
+
const v3 = M(v * v * v);
|
|
1951
|
+
const v7 = M(v3 * v3 * v);
|
|
1952
|
+
const pow = pow_2_252_3(u * v7).pow_p_5_8;
|
|
1953
|
+
let x = M(u * v3 * pow);
|
|
1954
|
+
const vx2 = M(v * x * x);
|
|
1955
|
+
const root1 = x;
|
|
1956
|
+
const root2 = M(x * RM1);
|
|
1957
|
+
const useRoot1 = vx2 === u;
|
|
1958
|
+
const useRoot2 = vx2 === M(-u);
|
|
1959
|
+
const noRoot = vx2 === M(-u * RM1);
|
|
1960
|
+
if (useRoot1)
|
|
1961
|
+
x = root1;
|
|
1962
|
+
if (useRoot2 || noRoot)
|
|
1963
|
+
x = root2;
|
|
1964
|
+
if ((M(x) & 1n) === 1n)
|
|
1965
|
+
x = M(-x);
|
|
1966
|
+
return { isValid: useRoot1 || useRoot2, value: x };
|
|
1967
|
+
};
|
|
1968
|
+
var modL_LE = (hash) => modN(bytesToNumLE(hash));
|
|
1969
|
+
var sha512a = (...m) => hashes.sha512Async(concatBytes(...m));
|
|
1970
|
+
var hashFinishA = (res) => sha512a(res.hashable).then(res.finish);
|
|
1971
|
+
var defaultVerifyOpts = { zip215: true };
|
|
1972
|
+
var _verify = (sig, msg, pub, opts = defaultVerifyOpts) => {
|
|
1973
|
+
sig = abytes(sig, L2);
|
|
1974
|
+
msg = abytes(msg);
|
|
1975
|
+
pub = abytes(pub, L);
|
|
1976
|
+
const { zip215 } = opts;
|
|
1977
|
+
let A;
|
|
1978
|
+
let R;
|
|
1979
|
+
let s;
|
|
1980
|
+
let SB;
|
|
1981
|
+
let hashable = Uint8Array.of();
|
|
1982
|
+
try {
|
|
1983
|
+
A = Point.fromBytes(pub, zip215);
|
|
1984
|
+
R = Point.fromBytes(sig.slice(0, L), zip215);
|
|
1985
|
+
s = bytesToNumLE(sig.slice(L, L2));
|
|
1986
|
+
SB = G.multiply(s, false);
|
|
1987
|
+
hashable = concatBytes(R.toBytes(), A.toBytes(), msg);
|
|
1988
|
+
} catch (error) {
|
|
1989
|
+
}
|
|
1990
|
+
const finish = (hashed) => {
|
|
1991
|
+
if (SB == null)
|
|
1992
|
+
return false;
|
|
1993
|
+
if (!zip215 && A.isSmallOrder())
|
|
1994
|
+
return false;
|
|
1995
|
+
const k = modL_LE(hashed);
|
|
1996
|
+
const RkA = R.add(A.multiply(k, false));
|
|
1997
|
+
return RkA.add(SB.negate()).clearCofactor().is0();
|
|
1998
|
+
};
|
|
1999
|
+
return { hashable, finish };
|
|
2000
|
+
};
|
|
2001
|
+
var verifyAsync = async (signature, message, publicKey, opts = defaultVerifyOpts) => hashFinishA(_verify(signature, message, publicKey, opts));
|
|
2002
|
+
var hashes = {
|
|
2003
|
+
sha512Async: async (message) => {
|
|
2004
|
+
const s = subtle();
|
|
2005
|
+
const m = concatBytes(message);
|
|
2006
|
+
return u8n(await s.digest("SHA-512", m.buffer));
|
|
2007
|
+
},
|
|
2008
|
+
sha512: void 0
|
|
2009
|
+
};
|
|
2010
|
+
var W = 8;
|
|
2011
|
+
var scalarBits = 256;
|
|
2012
|
+
var pwindows = Math.ceil(scalarBits / W) + 1;
|
|
2013
|
+
var pwindowSize = 2 ** (W - 1);
|
|
2014
|
+
var precompute = () => {
|
|
2015
|
+
const points = [];
|
|
2016
|
+
let p = G;
|
|
2017
|
+
let b = p;
|
|
2018
|
+
for (let w = 0; w < pwindows; w++) {
|
|
2019
|
+
b = p;
|
|
2020
|
+
points.push(b);
|
|
2021
|
+
for (let i = 1; i < pwindowSize; i++) {
|
|
2022
|
+
b = b.add(p);
|
|
2023
|
+
points.push(b);
|
|
2024
|
+
}
|
|
2025
|
+
p = b.double();
|
|
2026
|
+
}
|
|
2027
|
+
return points;
|
|
2028
|
+
};
|
|
2029
|
+
var Gpows = void 0;
|
|
2030
|
+
var ctneg = (cnd, p) => {
|
|
2031
|
+
const n = p.negate();
|
|
2032
|
+
return cnd ? n : p;
|
|
2033
|
+
};
|
|
2034
|
+
var wNAF = (n) => {
|
|
2035
|
+
const comp = Gpows || (Gpows = precompute());
|
|
2036
|
+
let p = I;
|
|
2037
|
+
let f = G;
|
|
2038
|
+
const pow_2_w = 2 ** W;
|
|
2039
|
+
const maxNum = pow_2_w;
|
|
2040
|
+
const mask = big(pow_2_w - 1);
|
|
2041
|
+
const shiftBy = big(W);
|
|
2042
|
+
for (let w = 0; w < pwindows; w++) {
|
|
2043
|
+
let wbits = Number(n & mask);
|
|
2044
|
+
n >>= shiftBy;
|
|
2045
|
+
if (wbits > pwindowSize) {
|
|
2046
|
+
wbits -= maxNum;
|
|
2047
|
+
n += 1n;
|
|
2048
|
+
}
|
|
2049
|
+
const off = w * pwindowSize;
|
|
2050
|
+
const offF = off;
|
|
2051
|
+
const offP = off + Math.abs(wbits) - 1;
|
|
2052
|
+
const isEven = w % 2 !== 0;
|
|
2053
|
+
const isNeg = wbits < 0;
|
|
2054
|
+
if (wbits === 0) {
|
|
2055
|
+
f = f.add(ctneg(isEven, comp[offF]));
|
|
2056
|
+
} else {
|
|
2057
|
+
p = p.add(ctneg(isNeg, comp[offP]));
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
if (n !== 0n)
|
|
2061
|
+
err("invalid wnaf");
|
|
2062
|
+
return { p, f };
|
|
2063
|
+
};
|
|
2064
|
+
|
|
2065
|
+
// src/crypto.ts
|
|
2066
|
+
hashes.sha512Async = async (message) => {
|
|
2067
|
+
const hashBuffer = await crypto.subtle.digest("SHA-512", message);
|
|
2068
|
+
return new Uint8Array(hashBuffer);
|
|
2069
|
+
};
|
|
2070
|
+
function hexToBytes2(hex) {
|
|
2071
|
+
if (hex.length % 2 !== 0) {
|
|
2072
|
+
throw new Error("Hex string must have even length");
|
|
2073
|
+
}
|
|
2074
|
+
for (let i = 0; i < hex.length; i++) {
|
|
2075
|
+
const c = hex[i].toLowerCase();
|
|
2076
|
+
if ((c < "0" || c > "9") && (c < "a" || c > "f")) {
|
|
2077
|
+
throw new Error(`Invalid hex character at position ${i}: '${hex[i]}'`);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
const match = hex.match(/.{1,2}/g);
|
|
2081
|
+
if (!match) {
|
|
2082
|
+
throw new Error("Invalid hex string format");
|
|
2083
|
+
}
|
|
2084
|
+
return new Uint8Array(match.map((byte) => {
|
|
2085
|
+
const parsed = parseInt(byte, 16);
|
|
2086
|
+
if (isNaN(parsed)) {
|
|
2087
|
+
throw new Error(`Invalid hex byte: ${byte}`);
|
|
2088
|
+
}
|
|
2089
|
+
return parsed;
|
|
2090
|
+
}));
|
|
2091
|
+
}
|
|
2092
|
+
function canonicalJSON(obj, depth = 0) {
|
|
2093
|
+
const MAX_DEPTH = 100;
|
|
2094
|
+
if (depth > MAX_DEPTH) {
|
|
2095
|
+
throw new Error("Object nesting too deep for canonicalization");
|
|
2096
|
+
}
|
|
2097
|
+
if (obj === null) return "null";
|
|
2098
|
+
if (obj === void 0) return JSON.stringify(void 0);
|
|
2099
|
+
const type = typeof obj;
|
|
2100
|
+
if (type === "function") throw new Error("Functions are not supported in RPC parameters");
|
|
2101
|
+
if (type === "symbol" || type === "bigint") throw new Error(`${type} is not supported in RPC parameters`);
|
|
2102
|
+
if (type === "number" && !Number.isFinite(obj)) throw new Error("NaN and Infinity are not supported in RPC parameters");
|
|
2103
|
+
if (type !== "object") return JSON.stringify(obj);
|
|
2104
|
+
if (Array.isArray(obj)) {
|
|
2105
|
+
return "[" + obj.map((item) => canonicalJSON(item, depth + 1)).join(",") + "]";
|
|
2106
|
+
}
|
|
2107
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
2108
|
+
const pairs = sortedKeys.map((key) => JSON.stringify(key) + ":" + canonicalJSON(obj[key], depth + 1));
|
|
2109
|
+
return "{" + pairs.join(",") + "}";
|
|
2110
|
+
}
|
|
2111
|
+
function buildSignatureMessage(timestamp, nonce, method, params) {
|
|
2112
|
+
if (nonce.length !== 36) {
|
|
2113
|
+
throw new Error("Nonce must be a valid UUID v4 (use crypto.randomUUID())");
|
|
2114
|
+
}
|
|
2115
|
+
if (nonce[8] !== "-" || nonce[13] !== "-" || nonce[18] !== "-" || nonce[23] !== "-") {
|
|
2116
|
+
throw new Error("Nonce must be a valid UUID v4 (use crypto.randomUUID())");
|
|
2117
|
+
}
|
|
2118
|
+
if (nonce[14] !== "4") {
|
|
2119
|
+
throw new Error("Nonce must be a valid UUID v4 (use crypto.randomUUID())");
|
|
2120
|
+
}
|
|
2121
|
+
const variant = nonce[19].toLowerCase();
|
|
2122
|
+
if (variant !== "8" && variant !== "9" && variant !== "a" && variant !== "b") {
|
|
2123
|
+
throw new Error("Nonce must be a valid UUID v4 (use crypto.randomUUID())");
|
|
2124
|
+
}
|
|
2125
|
+
const hexChars = nonce.replace(/-/g, "");
|
|
2126
|
+
for (let i = 0; i < hexChars.length; i++) {
|
|
2127
|
+
const c = hexChars[i].toLowerCase();
|
|
2128
|
+
if ((c < "0" || c > "9") && (c < "a" || c > "f")) {
|
|
2129
|
+
throw new Error("Nonce must be a valid UUID v4 (use crypto.randomUUID())");
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
const paramsStr = canonicalJSON(params || {});
|
|
2133
|
+
return `${timestamp}:${nonce}:${method}:${paramsStr}`;
|
|
2134
|
+
}
|
|
2135
|
+
var ED25519_PUBLIC_KEY_LENGTH = 32;
|
|
2136
|
+
var ED25519_SIGNATURE_LENGTH = 64;
|
|
2137
|
+
function validatePublicKey(publicKey) {
|
|
2138
|
+
if (typeof publicKey !== "string") {
|
|
2139
|
+
return { valid: false, error: "Public key must be a string" };
|
|
2140
|
+
}
|
|
2141
|
+
if (publicKey.length !== ED25519_PUBLIC_KEY_LENGTH * 2) {
|
|
2142
|
+
return { valid: false, error: `Public key must be ${ED25519_PUBLIC_KEY_LENGTH * 2} hex characters (${ED25519_PUBLIC_KEY_LENGTH} bytes)` };
|
|
2143
|
+
}
|
|
2144
|
+
for (let i = 0; i < publicKey.length; i++) {
|
|
2145
|
+
const c = publicKey[i];
|
|
2146
|
+
if ((c < "0" || c > "9") && (c < "a" || c > "f")) {
|
|
2147
|
+
return { valid: false, error: `Invalid hex character at position ${i}: '${c}' (use lowercase hex)` };
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
return { valid: true };
|
|
2151
|
+
}
|
|
2152
|
+
async function verifyEd25519Signature(publicKey, message, signature) {
|
|
2153
|
+
try {
|
|
2154
|
+
const pkValidation = validatePublicKey(publicKey);
|
|
2155
|
+
if (!pkValidation.valid) {
|
|
2156
|
+
console.error("Ed25519 verification error: invalid public key:", pkValidation.error);
|
|
2157
|
+
return false;
|
|
2158
|
+
}
|
|
2159
|
+
const publicKeyBytes = hexToBytes2(publicKey);
|
|
2160
|
+
const encoder = new TextEncoder();
|
|
2161
|
+
const messageBytes = encoder.encode(message);
|
|
2162
|
+
const signatureBytes = import_node_buffer.Buffer.from(signature, "base64");
|
|
2163
|
+
if (signatureBytes.length !== ED25519_SIGNATURE_LENGTH) {
|
|
2164
|
+
console.error(`Ed25519 verification error: signature length ${signatureBytes.length}, expected ${ED25519_SIGNATURE_LENGTH}`);
|
|
2165
|
+
return false;
|
|
2166
|
+
}
|
|
2167
|
+
return await verifyAsync(signatureBytes, messageBytes, publicKeyBytes);
|
|
2168
|
+
} catch (error) {
|
|
2169
|
+
console.error("Ed25519 verification error:", error);
|
|
2170
|
+
return false;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
var TAG_MIN_LENGTH = 1;
|
|
2174
|
+
var TAG_MAX_LENGTH = 64;
|
|
2175
|
+
var TAG_REGEX = /^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/;
|
|
2176
|
+
function validateTag(tag) {
|
|
2177
|
+
if (typeof tag !== "string") {
|
|
2178
|
+
return { valid: false, error: "Tag must be a string" };
|
|
2179
|
+
}
|
|
2180
|
+
if (tag.length < TAG_MIN_LENGTH) {
|
|
2181
|
+
return { valid: false, error: `Tag must be at least ${TAG_MIN_LENGTH} character` };
|
|
2182
|
+
}
|
|
2183
|
+
if (tag.length > TAG_MAX_LENGTH) {
|
|
2184
|
+
return { valid: false, error: `Tag must be at most ${TAG_MAX_LENGTH} characters` };
|
|
2185
|
+
}
|
|
2186
|
+
if (tag.length === 1) {
|
|
2187
|
+
if (!/^[a-z0-9]$/.test(tag)) {
|
|
2188
|
+
return { valid: false, error: "Tag must be lowercase alphanumeric" };
|
|
2189
|
+
}
|
|
2190
|
+
return { valid: true };
|
|
2191
|
+
}
|
|
2192
|
+
if (!TAG_REGEX.test(tag)) {
|
|
2193
|
+
return { valid: false, error: "Tag must be lowercase alphanumeric with optional dots/dashes, and start/end with alphanumeric" };
|
|
2194
|
+
}
|
|
2195
|
+
return { valid: true };
|
|
2196
|
+
}
|
|
2197
|
+
function validateTags(tags, maxTags = 20) {
|
|
2198
|
+
if (!Array.isArray(tags)) {
|
|
2199
|
+
return { valid: false, error: "Tags must be an array" };
|
|
2200
|
+
}
|
|
2201
|
+
if (tags.length === 0) {
|
|
2202
|
+
return { valid: false, error: "At least one tag is required" };
|
|
2203
|
+
}
|
|
2204
|
+
if (tags.length > maxTags) {
|
|
2205
|
+
return { valid: false, error: `Maximum ${maxTags} tags allowed` };
|
|
2206
|
+
}
|
|
2207
|
+
for (let i = 0; i < tags.length; i++) {
|
|
2208
|
+
const result = validateTag(tags[i]);
|
|
2209
|
+
if (!result.valid) {
|
|
2210
|
+
return { valid: false, error: `Tag ${i + 1}: ${result.error}` };
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
const uniqueTags = new Set(tags);
|
|
2214
|
+
if (uniqueTags.size !== tags.length) {
|
|
2215
|
+
return { valid: false, error: "Duplicate tags are not allowed" };
|
|
2216
|
+
}
|
|
2217
|
+
return { valid: true };
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2281
2220
|
// src/rpc.ts
|
|
2282
|
-
init_crypto();
|
|
2283
2221
|
var MAX_PAGE_SIZE = 100;
|
|
2284
|
-
var CREDENTIAL_RATE_WINDOW = 1e3;
|
|
2285
2222
|
var REQUEST_RATE_WINDOW = 1e3;
|
|
2286
2223
|
function getJsonDepth(obj, maxDepth, currentDepth = 0) {
|
|
2287
2224
|
if (obj === null || typeof obj !== "object") {
|
|
@@ -2312,7 +2249,7 @@ var ErrorCodes = {
|
|
|
2312
2249
|
AUTH_REQUIRED: "AUTH_REQUIRED",
|
|
2313
2250
|
INVALID_CREDENTIALS: "INVALID_CREDENTIALS",
|
|
2314
2251
|
// Validation errors
|
|
2315
|
-
|
|
2252
|
+
INVALID_PUBLIC_KEY: "INVALID_PUBLIC_KEY",
|
|
2316
2253
|
INVALID_TAG: "INVALID_TAG",
|
|
2317
2254
|
INVALID_SDP: "INVALID_SDP",
|
|
2318
2255
|
INVALID_PARAMS: "INVALID_PARAMS",
|
|
@@ -2353,108 +2290,29 @@ function validateTimestamp(timestamp, config) {
|
|
|
2353
2290
|
throw new RpcError(ErrorCodes.INVALID_PARAMS, "Timestamp too far in future");
|
|
2354
2291
|
}
|
|
2355
2292
|
}
|
|
2356
|
-
async function verifyRequestSignature(
|
|
2293
|
+
async function verifyRequestSignature(publicKey, timestamp, nonce, signature, method, params, storage, config) {
|
|
2357
2294
|
validateTimestamp(timestamp, config);
|
|
2358
|
-
const
|
|
2359
|
-
if (!
|
|
2360
|
-
throw new RpcError(ErrorCodes.
|
|
2295
|
+
const pkValidation = validatePublicKey(publicKey);
|
|
2296
|
+
if (!pkValidation.valid) {
|
|
2297
|
+
throw new RpcError(ErrorCodes.INVALID_PUBLIC_KEY, pkValidation.error || "Invalid public key");
|
|
2361
2298
|
}
|
|
2362
2299
|
const message = buildSignatureMessage(timestamp, nonce, method, params);
|
|
2363
|
-
const isValid = await
|
|
2300
|
+
const isValid = await verifyEd25519Signature(publicKey, message, signature);
|
|
2364
2301
|
if (!isValid) {
|
|
2365
2302
|
throw new RpcError(ErrorCodes.INVALID_CREDENTIALS, "Invalid signature");
|
|
2366
2303
|
}
|
|
2367
|
-
const nonceKey = `nonce:${
|
|
2304
|
+
const nonceKey = `nonce:${publicKey}:${nonce}`;
|
|
2368
2305
|
const nonceExpiresAt = timestamp + config.timestampMaxAge;
|
|
2369
2306
|
const nonceIsNew = await storage.checkAndMarkNonce(nonceKey, nonceExpiresAt);
|
|
2370
2307
|
if (!nonceIsNew) {
|
|
2371
2308
|
throw new RpcError(ErrorCodes.INVALID_CREDENTIALS, "Nonce already used (replay attack detected)");
|
|
2372
2309
|
}
|
|
2373
|
-
const now = Date.now();
|
|
2374
|
-
const credentialExpiresAt = now + 365 * 24 * 60 * 60 * 1e3;
|
|
2375
|
-
await storage.updateCredentialUsage(name, now, credentialExpiresAt);
|
|
2376
2310
|
}
|
|
2377
2311
|
var handlers = {
|
|
2378
2312
|
/**
|
|
2379
|
-
*
|
|
2380
|
-
* No authentication required - this is how users get started
|
|
2381
|
-
* SECURITY: Rate limited per IP to prevent abuse (database-backed for multi-instance support)
|
|
2382
|
-
*/
|
|
2383
|
-
async generateCredentials(params, name, timestamp, signature, storage, config, request) {
|
|
2384
|
-
const credentialCount = await storage.getCredentialCount();
|
|
2385
|
-
if (credentialCount >= config.maxTotalCredentials) {
|
|
2386
|
-
throw new RpcError(
|
|
2387
|
-
ErrorCodes.STORAGE_FULL,
|
|
2388
|
-
`Server credential limit reached (${config.maxTotalCredentials}). Try again later.`
|
|
2389
|
-
);
|
|
2390
|
-
}
|
|
2391
|
-
let rateLimitKey;
|
|
2392
|
-
let rateLimit;
|
|
2393
|
-
if (!request.clientIp) {
|
|
2394
|
-
console.warn("\u26A0\uFE0F WARNING: Unable to determine client IP for credential generation. Using global rate limit.");
|
|
2395
|
-
rateLimitKey = "cred_gen:global_unknown";
|
|
2396
|
-
rateLimit = 2;
|
|
2397
|
-
} else {
|
|
2398
|
-
rateLimitKey = `cred_gen:${request.clientIp}`;
|
|
2399
|
-
rateLimit = config.credentialsPerIpPerSecond;
|
|
2400
|
-
}
|
|
2401
|
-
const allowed = await storage.checkRateLimit(
|
|
2402
|
-
rateLimitKey,
|
|
2403
|
-
rateLimit,
|
|
2404
|
-
CREDENTIAL_RATE_WINDOW
|
|
2405
|
-
);
|
|
2406
|
-
if (!allowed) {
|
|
2407
|
-
throw new RpcError(
|
|
2408
|
-
ErrorCodes.RATE_LIMIT_EXCEEDED,
|
|
2409
|
-
`Rate limit exceeded. Maximum ${rateLimit} credentials per second${request.clientIp ? " per IP" : " (global limit for unidentified IPs)"}.`
|
|
2410
|
-
);
|
|
2411
|
-
}
|
|
2412
|
-
if (params.name !== void 0) {
|
|
2413
|
-
if (typeof params.name !== "string") {
|
|
2414
|
-
throw new RpcError(ErrorCodes.INVALID_PARAMS, "name must be a string");
|
|
2415
|
-
}
|
|
2416
|
-
const usernameValidation = validateUsername(params.name);
|
|
2417
|
-
if (!usernameValidation.valid) {
|
|
2418
|
-
throw new RpcError(ErrorCodes.INVALID_PARAMS, usernameValidation.error || "Invalid username");
|
|
2419
|
-
}
|
|
2420
|
-
}
|
|
2421
|
-
if (params.expiresAt !== void 0) {
|
|
2422
|
-
if (typeof params.expiresAt !== "number" || isNaN(params.expiresAt) || !Number.isFinite(params.expiresAt)) {
|
|
2423
|
-
throw new RpcError(ErrorCodes.INVALID_PARAMS, "expiresAt must be a valid timestamp");
|
|
2424
|
-
}
|
|
2425
|
-
const now = Date.now();
|
|
2426
|
-
if (params.expiresAt < now - 6e4) {
|
|
2427
|
-
throw new RpcError(ErrorCodes.INVALID_PARAMS, "expiresAt cannot be in the past");
|
|
2428
|
-
}
|
|
2429
|
-
const maxFuture = now + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
2430
|
-
if (params.expiresAt > maxFuture) {
|
|
2431
|
-
throw new RpcError(ErrorCodes.INVALID_PARAMS, "expiresAt cannot be more than 10 years in the future");
|
|
2432
|
-
}
|
|
2433
|
-
}
|
|
2434
|
-
try {
|
|
2435
|
-
const credential = await storage.generateCredentials({
|
|
2436
|
-
name: params.name,
|
|
2437
|
-
expiresAt: params.expiresAt
|
|
2438
|
-
});
|
|
2439
|
-
return {
|
|
2440
|
-
name: credential.name,
|
|
2441
|
-
secret: credential.secret,
|
|
2442
|
-
createdAt: credential.createdAt,
|
|
2443
|
-
expiresAt: credential.expiresAt
|
|
2444
|
-
};
|
|
2445
|
-
} catch (error) {
|
|
2446
|
-
if (error.message === "Username already taken") {
|
|
2447
|
-
throw new RpcError(ErrorCodes.INVALID_PARAMS, "Username already taken");
|
|
2448
|
-
}
|
|
2449
|
-
throw error;
|
|
2450
|
-
}
|
|
2451
|
-
},
|
|
2452
|
-
/**
|
|
2453
|
-
* Discover offers by tags - Supports 2 modes:
|
|
2454
|
-
* 1. Paginated discovery: tags array with limit/offset
|
|
2455
|
-
* 2. Random discovery: tags array without limit (returns single random offer)
|
|
2313
|
+
* Discover offers by tags
|
|
2456
2314
|
*/
|
|
2457
|
-
async discover(params,
|
|
2315
|
+
async discover(params, publicKey, timestamp, signature, storage, config, request) {
|
|
2458
2316
|
const { tags, limit, offset } = params;
|
|
2459
2317
|
const tagsValidation = validateTags(tags);
|
|
2460
2318
|
if (!tagsValidation.valid) {
|
|
@@ -2469,17 +2327,17 @@ var handlers = {
|
|
|
2469
2327
|
}
|
|
2470
2328
|
const pageLimit = Math.min(Math.max(1, limit), MAX_PAGE_SIZE);
|
|
2471
2329
|
const pageOffset = Math.max(0, offset || 0);
|
|
2472
|
-
const
|
|
2330
|
+
const excludePublicKey2 = publicKey || null;
|
|
2473
2331
|
const offers = await storage.discoverOffers(
|
|
2474
2332
|
tags,
|
|
2475
|
-
|
|
2333
|
+
excludePublicKey2,
|
|
2476
2334
|
pageLimit,
|
|
2477
2335
|
pageOffset
|
|
2478
2336
|
);
|
|
2479
2337
|
return {
|
|
2480
2338
|
offers: offers.map((offer2) => ({
|
|
2481
2339
|
offerId: offer2.id,
|
|
2482
|
-
|
|
2340
|
+
publicKey: offer2.publicKey,
|
|
2483
2341
|
tags: offer2.tags,
|
|
2484
2342
|
sdp: offer2.sdp,
|
|
2485
2343
|
createdAt: offer2.createdAt,
|
|
@@ -2490,14 +2348,14 @@ var handlers = {
|
|
|
2490
2348
|
offset: pageOffset
|
|
2491
2349
|
};
|
|
2492
2350
|
}
|
|
2493
|
-
const
|
|
2494
|
-
const offer = await storage.getRandomOffer(tags,
|
|
2351
|
+
const excludePublicKey = publicKey || null;
|
|
2352
|
+
const offer = await storage.getRandomOffer(tags, excludePublicKey);
|
|
2495
2353
|
if (!offer) {
|
|
2496
2354
|
throw new RpcError(ErrorCodes.OFFER_NOT_FOUND, "No offers found matching tags");
|
|
2497
2355
|
}
|
|
2498
2356
|
return {
|
|
2499
2357
|
offerId: offer.id,
|
|
2500
|
-
|
|
2358
|
+
publicKey: offer.publicKey,
|
|
2501
2359
|
tags: offer.tags,
|
|
2502
2360
|
sdp: offer.sdp,
|
|
2503
2361
|
createdAt: offer.createdAt,
|
|
@@ -2507,10 +2365,10 @@ var handlers = {
|
|
|
2507
2365
|
/**
|
|
2508
2366
|
* Publish offers with tags
|
|
2509
2367
|
*/
|
|
2510
|
-
async publishOffer(params,
|
|
2368
|
+
async publishOffer(params, publicKey, timestamp, signature, storage, config, request) {
|
|
2511
2369
|
const { tags, offers, ttl } = params;
|
|
2512
|
-
if (!
|
|
2513
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "
|
|
2370
|
+
if (!publicKey) {
|
|
2371
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "Authentication required for offer publishing");
|
|
2514
2372
|
}
|
|
2515
2373
|
const tagsValidation = validateTags(tags);
|
|
2516
2374
|
if (!tagsValidation.valid) {
|
|
@@ -2525,7 +2383,7 @@ var handlers = {
|
|
|
2525
2383
|
`Too many offers (max ${config.maxOffersPerRequest})`
|
|
2526
2384
|
);
|
|
2527
2385
|
}
|
|
2528
|
-
const userOfferCount = await storage.
|
|
2386
|
+
const userOfferCount = await storage.getOfferCountByPublicKey(publicKey);
|
|
2529
2387
|
if (userOfferCount + offers.length > config.maxOffersPerUser) {
|
|
2530
2388
|
throw new RpcError(
|
|
2531
2389
|
ErrorCodes.TOO_MANY_OFFERS_PER_USER,
|
|
@@ -2559,20 +2417,17 @@ var handlers = {
|
|
|
2559
2417
|
}
|
|
2560
2418
|
}
|
|
2561
2419
|
const now = Date.now();
|
|
2562
|
-
const offerTtl = ttl !== void 0 ? Math.min(
|
|
2563
|
-
Math.max(ttl, config.offerMinTtl),
|
|
2564
|
-
config.offerMaxTtl
|
|
2565
|
-
) : config.offerDefaultTtl;
|
|
2420
|
+
const offerTtl = ttl !== void 0 ? Math.min(Math.max(ttl, config.offerMinTtl), config.offerMaxTtl) : config.offerDefaultTtl;
|
|
2566
2421
|
const expiresAt = now + offerTtl;
|
|
2567
2422
|
const offerRequests = offers.map((offer) => ({
|
|
2568
|
-
|
|
2423
|
+
publicKey,
|
|
2569
2424
|
tags,
|
|
2570
2425
|
sdp: offer.sdp,
|
|
2571
2426
|
expiresAt
|
|
2572
2427
|
}));
|
|
2573
2428
|
const createdOffers = await storage.createOffers(offerRequests);
|
|
2574
2429
|
return {
|
|
2575
|
-
|
|
2430
|
+
publicKey,
|
|
2576
2431
|
tags,
|
|
2577
2432
|
offers: createdOffers.map((offer) => ({
|
|
2578
2433
|
offerId: offer.id,
|
|
@@ -2587,26 +2442,26 @@ var handlers = {
|
|
|
2587
2442
|
/**
|
|
2588
2443
|
* Delete an offer by ID
|
|
2589
2444
|
*/
|
|
2590
|
-
async deleteOffer(params,
|
|
2445
|
+
async deleteOffer(params, publicKey, timestamp, signature, storage, config, request) {
|
|
2591
2446
|
const { offerId } = params;
|
|
2592
|
-
if (!
|
|
2593
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "
|
|
2447
|
+
if (!publicKey) {
|
|
2448
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "Authentication required");
|
|
2594
2449
|
}
|
|
2595
2450
|
validateStringParam(offerId, "offerId");
|
|
2596
|
-
const deleted = await storage.deleteOffer(offerId,
|
|
2451
|
+
const deleted = await storage.deleteOffer(offerId, publicKey);
|
|
2597
2452
|
if (!deleted) {
|
|
2598
|
-
throw new RpcError(ErrorCodes.NOT_AUTHORIZED, "Offer not found or not owned by this
|
|
2453
|
+
throw new RpcError(ErrorCodes.NOT_AUTHORIZED, "Offer not found or not owned by this identity");
|
|
2599
2454
|
}
|
|
2600
2455
|
return { success: true };
|
|
2601
2456
|
},
|
|
2602
2457
|
/**
|
|
2603
2458
|
* Answer an offer
|
|
2604
2459
|
*/
|
|
2605
|
-
async answerOffer(params,
|
|
2460
|
+
async answerOffer(params, publicKey, timestamp, signature, storage, config, request) {
|
|
2606
2461
|
const { offerId, sdp, matchedTags } = params;
|
|
2607
2462
|
validateStringParam(offerId, "offerId");
|
|
2608
|
-
if (!
|
|
2609
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "
|
|
2463
|
+
if (!publicKey) {
|
|
2464
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "Authentication required");
|
|
2610
2465
|
}
|
|
2611
2466
|
if (!sdp || typeof sdp !== "string" || sdp.length === 0) {
|
|
2612
2467
|
throw new RpcError(ErrorCodes.INVALID_SDP, "Invalid SDP");
|
|
@@ -2621,7 +2476,7 @@ var handlers = {
|
|
|
2621
2476
|
if (!offer) {
|
|
2622
2477
|
throw new RpcError(ErrorCodes.OFFER_NOT_FOUND, "Offer not found");
|
|
2623
2478
|
}
|
|
2624
|
-
if (offer.
|
|
2479
|
+
if (offer.answererPublicKey) {
|
|
2625
2480
|
throw new RpcError(ErrorCodes.OFFER_ALREADY_ANSWERED, "Offer already answered");
|
|
2626
2481
|
}
|
|
2627
2482
|
if (matchedTags && matchedTags.length > 0) {
|
|
@@ -2631,53 +2486,53 @@ var handlers = {
|
|
|
2631
2486
|
throw new RpcError(ErrorCodes.INVALID_PARAMS, `matchedTags contains tags not on offer: ${invalidTags.join(", ")}`);
|
|
2632
2487
|
}
|
|
2633
2488
|
}
|
|
2634
|
-
await storage.answerOffer(offerId,
|
|
2489
|
+
await storage.answerOffer(offerId, publicKey, sdp, matchedTags);
|
|
2635
2490
|
return { success: true, offerId };
|
|
2636
2491
|
},
|
|
2637
2492
|
/**
|
|
2638
2493
|
* Get answer for an offer
|
|
2639
2494
|
*/
|
|
2640
|
-
async getOfferAnswer(params,
|
|
2495
|
+
async getOfferAnswer(params, publicKey, timestamp, signature, storage, config, request) {
|
|
2641
2496
|
const { offerId } = params;
|
|
2642
2497
|
validateStringParam(offerId, "offerId");
|
|
2643
|
-
if (!
|
|
2644
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "
|
|
2498
|
+
if (!publicKey) {
|
|
2499
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "Authentication required");
|
|
2645
2500
|
}
|
|
2646
2501
|
const offer = await storage.getOfferById(offerId);
|
|
2647
2502
|
if (!offer) {
|
|
2648
2503
|
throw new RpcError(ErrorCodes.OFFER_NOT_FOUND, "Offer not found");
|
|
2649
2504
|
}
|
|
2650
|
-
if (offer.
|
|
2505
|
+
if (offer.publicKey !== publicKey) {
|
|
2651
2506
|
throw new RpcError(ErrorCodes.NOT_AUTHORIZED, "Not authorized to access this offer");
|
|
2652
2507
|
}
|
|
2653
|
-
if (!offer.
|
|
2508
|
+
if (!offer.answererPublicKey || !offer.answerSdp) {
|
|
2654
2509
|
throw new RpcError(ErrorCodes.OFFER_NOT_ANSWERED, "Offer not yet answered");
|
|
2655
2510
|
}
|
|
2656
2511
|
return {
|
|
2657
2512
|
sdp: offer.answerSdp,
|
|
2658
2513
|
offerId: offer.id,
|
|
2659
|
-
|
|
2514
|
+
answererPublicKey: offer.answererPublicKey,
|
|
2660
2515
|
answeredAt: offer.answeredAt
|
|
2661
2516
|
};
|
|
2662
2517
|
},
|
|
2663
2518
|
/**
|
|
2664
2519
|
* Combined polling for answers and ICE candidates
|
|
2665
2520
|
*/
|
|
2666
|
-
async poll(params,
|
|
2521
|
+
async poll(params, publicKey, timestamp, signature, storage, config, request) {
|
|
2667
2522
|
const { since } = params;
|
|
2668
|
-
if (!
|
|
2669
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "
|
|
2523
|
+
if (!publicKey) {
|
|
2524
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "Authentication required");
|
|
2670
2525
|
}
|
|
2671
2526
|
if (since !== void 0 && (typeof since !== "number" || since < 0 || !Number.isFinite(since))) {
|
|
2672
2527
|
throw new RpcError(ErrorCodes.INVALID_PARAMS, "Invalid since parameter: must be a non-negative number");
|
|
2673
2528
|
}
|
|
2674
2529
|
const sinceTimestamp = since !== void 0 ? since : 0;
|
|
2675
|
-
const answeredOffers = await storage.getAnsweredOffers(
|
|
2530
|
+
const answeredOffers = await storage.getAnsweredOffers(publicKey);
|
|
2676
2531
|
const filteredAnswers = answeredOffers.filter(
|
|
2677
2532
|
(offer) => offer.answeredAt && offer.answeredAt > sinceTimestamp
|
|
2678
2533
|
);
|
|
2679
|
-
const ownedOffers = await storage.
|
|
2680
|
-
const answeredByUser = await storage.getOffersAnsweredBy(
|
|
2534
|
+
const ownedOffers = await storage.getOffersByPublicKey(publicKey);
|
|
2535
|
+
const answeredByUser = await storage.getOffersAnsweredBy(publicKey);
|
|
2681
2536
|
const allOfferIds = [
|
|
2682
2537
|
...ownedOffers.map((offer) => offer.id),
|
|
2683
2538
|
...answeredByUser.map((offer) => offer.id)
|
|
@@ -2685,7 +2540,7 @@ var handlers = {
|
|
|
2685
2540
|
const offerIds = [...new Set(allOfferIds)];
|
|
2686
2541
|
const iceCandidatesMap = await storage.getIceCandidatesForMultipleOffers(
|
|
2687
2542
|
offerIds,
|
|
2688
|
-
|
|
2543
|
+
publicKey,
|
|
2689
2544
|
sinceTimestamp
|
|
2690
2545
|
);
|
|
2691
2546
|
const iceCandidatesByOffer = {};
|
|
@@ -2695,7 +2550,7 @@ var handlers = {
|
|
|
2695
2550
|
return {
|
|
2696
2551
|
answers: filteredAnswers.map((offer) => ({
|
|
2697
2552
|
offerId: offer.id,
|
|
2698
|
-
|
|
2553
|
+
answererPublicKey: offer.answererPublicKey,
|
|
2699
2554
|
sdp: offer.answerSdp,
|
|
2700
2555
|
answeredAt: offer.answeredAt
|
|
2701
2556
|
})),
|
|
@@ -2705,11 +2560,11 @@ var handlers = {
|
|
|
2705
2560
|
/**
|
|
2706
2561
|
* Add ICE candidates
|
|
2707
2562
|
*/
|
|
2708
|
-
async addIceCandidates(params,
|
|
2563
|
+
async addIceCandidates(params, publicKey, timestamp, signature, storage, config, request) {
|
|
2709
2564
|
const { offerId, candidates } = params;
|
|
2710
2565
|
validateStringParam(offerId, "offerId");
|
|
2711
|
-
if (!
|
|
2712
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "
|
|
2566
|
+
if (!publicKey) {
|
|
2567
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "Authentication required");
|
|
2713
2568
|
}
|
|
2714
2569
|
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
2715
2570
|
throw new RpcError(ErrorCodes.MISSING_PARAMS, "Missing or invalid required parameter: candidates");
|
|
@@ -2755,10 +2610,10 @@ var handlers = {
|
|
|
2755
2610
|
`ICE candidate limit exceeded for offer. Current: ${currentCandidateCount}, limit: ${config.maxIceCandidatesPerOffer}.`
|
|
2756
2611
|
);
|
|
2757
2612
|
}
|
|
2758
|
-
const role = offer.
|
|
2613
|
+
const role = offer.publicKey === publicKey ? "offerer" : "answerer";
|
|
2759
2614
|
const count = await storage.addIceCandidates(
|
|
2760
2615
|
offerId,
|
|
2761
|
-
|
|
2616
|
+
publicKey,
|
|
2762
2617
|
role,
|
|
2763
2618
|
candidates
|
|
2764
2619
|
);
|
|
@@ -2767,11 +2622,11 @@ var handlers = {
|
|
|
2767
2622
|
/**
|
|
2768
2623
|
* Get ICE candidates
|
|
2769
2624
|
*/
|
|
2770
|
-
async getIceCandidates(params,
|
|
2625
|
+
async getIceCandidates(params, publicKey, timestamp, signature, storage, config, request) {
|
|
2771
2626
|
const { offerId, since } = params;
|
|
2772
2627
|
validateStringParam(offerId, "offerId");
|
|
2773
|
-
if (!
|
|
2774
|
-
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "
|
|
2628
|
+
if (!publicKey) {
|
|
2629
|
+
throw new RpcError(ErrorCodes.AUTH_REQUIRED, "Authentication required");
|
|
2775
2630
|
}
|
|
2776
2631
|
if (since !== void 0 && (typeof since !== "number" || since < 0 || !Number.isFinite(since))) {
|
|
2777
2632
|
throw new RpcError(ErrorCodes.INVALID_PARAMS, "Invalid since parameter: must be a non-negative number");
|
|
@@ -2781,8 +2636,8 @@ var handlers = {
|
|
|
2781
2636
|
if (!offer) {
|
|
2782
2637
|
throw new RpcError(ErrorCodes.OFFER_NOT_FOUND, "Offer not found");
|
|
2783
2638
|
}
|
|
2784
|
-
const isOfferer = offer.
|
|
2785
|
-
const isAnswerer = offer.
|
|
2639
|
+
const isOfferer = offer.publicKey === publicKey;
|
|
2640
|
+
const isAnswerer = offer.answererPublicKey === publicKey;
|
|
2786
2641
|
if (!isOfferer && !isAnswerer) {
|
|
2787
2642
|
throw new RpcError(ErrorCodes.NOT_AUTHORIZED, "Not authorized to access ICE candidates for this offer");
|
|
2788
2643
|
}
|
|
@@ -2801,12 +2656,10 @@ var handlers = {
|
|
|
2801
2656
|
};
|
|
2802
2657
|
}
|
|
2803
2658
|
};
|
|
2804
|
-
var UNAUTHENTICATED_METHODS = /* @__PURE__ */ new Set(["
|
|
2659
|
+
var UNAUTHENTICATED_METHODS = /* @__PURE__ */ new Set(["discover"]);
|
|
2805
2660
|
async function handleRpc(requests, ctx, storage, config) {
|
|
2806
2661
|
const responses = [];
|
|
2807
|
-
const clientIp = ctx.req.header("cf-connecting-ip") ||
|
|
2808
|
-
ctx.req.header("x-real-ip") || // Nginx
|
|
2809
|
-
ctx.req.header("x-forwarded-for")?.split(",")[0].trim() || void 0;
|
|
2662
|
+
const clientIp = ctx.req.header("cf-connecting-ip") || ctx.req.header("x-real-ip") || ctx.req.header("x-forwarded-for")?.split(",")[0].trim() || void 0;
|
|
2810
2663
|
if (clientIp) {
|
|
2811
2664
|
const rateLimitKey = `req:${clientIp}`;
|
|
2812
2665
|
const allowed = await storage.checkRateLimit(
|
|
@@ -2822,7 +2675,7 @@ async function handleRpc(requests, ctx, storage, config) {
|
|
|
2822
2675
|
}));
|
|
2823
2676
|
}
|
|
2824
2677
|
}
|
|
2825
|
-
const
|
|
2678
|
+
const publicKey = ctx.req.header("X-PublicKey");
|
|
2826
2679
|
const timestampHeader = ctx.req.header("X-Timestamp");
|
|
2827
2680
|
const nonce = ctx.req.header("X-Nonce");
|
|
2828
2681
|
const signature = ctx.req.header("X-Signature");
|
|
@@ -2867,10 +2720,10 @@ async function handleRpc(requests, ctx, storage, config) {
|
|
|
2867
2720
|
}
|
|
2868
2721
|
const requiresAuth = !UNAUTHENTICATED_METHODS.has(method);
|
|
2869
2722
|
if (requiresAuth) {
|
|
2870
|
-
if (!
|
|
2723
|
+
if (!publicKey || typeof publicKey !== "string") {
|
|
2871
2724
|
responses.push({
|
|
2872
2725
|
success: false,
|
|
2873
|
-
error: "Missing or invalid X-
|
|
2726
|
+
error: "Missing or invalid X-PublicKey header",
|
|
2874
2727
|
errorCode: ErrorCodes.AUTH_REQUIRED
|
|
2875
2728
|
});
|
|
2876
2729
|
continue;
|
|
@@ -2900,7 +2753,7 @@ async function handleRpc(requests, ctx, storage, config) {
|
|
|
2900
2753
|
continue;
|
|
2901
2754
|
}
|
|
2902
2755
|
await verifyRequestSignature(
|
|
2903
|
-
|
|
2756
|
+
publicKey,
|
|
2904
2757
|
timestamp,
|
|
2905
2758
|
nonce,
|
|
2906
2759
|
signature,
|
|
@@ -2911,7 +2764,7 @@ async function handleRpc(requests, ctx, storage, config) {
|
|
|
2911
2764
|
);
|
|
2912
2765
|
const result = await handler(
|
|
2913
2766
|
params || {},
|
|
2914
|
-
|
|
2767
|
+
publicKey,
|
|
2915
2768
|
timestamp,
|
|
2916
2769
|
signature,
|
|
2917
2770
|
storage,
|
|
@@ -2925,11 +2778,9 @@ async function handleRpc(requests, ctx, storage, config) {
|
|
|
2925
2778
|
} else {
|
|
2926
2779
|
const result = await handler(
|
|
2927
2780
|
params || {},
|
|
2928
|
-
|
|
2781
|
+
publicKey || "",
|
|
2929
2782
|
0,
|
|
2930
|
-
// timestamp
|
|
2931
2783
|
"",
|
|
2932
|
-
// signature
|
|
2933
2784
|
storage,
|
|
2934
2785
|
config,
|
|
2935
2786
|
{ ...request, clientIp }
|
|
@@ -2939,15 +2790,15 @@ async function handleRpc(requests, ctx, storage, config) {
|
|
|
2939
2790
|
result
|
|
2940
2791
|
});
|
|
2941
2792
|
}
|
|
2942
|
-
} catch (
|
|
2943
|
-
if (
|
|
2793
|
+
} catch (err2) {
|
|
2794
|
+
if (err2 instanceof RpcError) {
|
|
2944
2795
|
responses.push({
|
|
2945
2796
|
success: false,
|
|
2946
|
-
error:
|
|
2947
|
-
errorCode:
|
|
2797
|
+
error: err2.message,
|
|
2798
|
+
errorCode: err2.errorCode
|
|
2948
2799
|
});
|
|
2949
2800
|
} else {
|
|
2950
|
-
console.error("Unexpected RPC error:",
|
|
2801
|
+
console.error("Unexpected RPC error:", err2);
|
|
2951
2802
|
responses.push({
|
|
2952
2803
|
success: false,
|
|
2953
2804
|
error: "Internal server error",
|
|
@@ -3019,9 +2870,9 @@ function createApp(storage, config) {
|
|
|
3019
2870
|
}
|
|
3020
2871
|
const responses = await handleRpc(requests, c, storage, config);
|
|
3021
2872
|
return c.json(responses, 200);
|
|
3022
|
-
} catch (
|
|
3023
|
-
console.error("RPC error:",
|
|
3024
|
-
const errorMsg =
|
|
2873
|
+
} catch (err2) {
|
|
2874
|
+
console.error("RPC error:", err2);
|
|
2875
|
+
const errorMsg = err2 instanceof SyntaxError ? "Invalid JSON in request body" : "Request must be valid JSON array";
|
|
3025
2876
|
return c.json([{
|
|
3026
2877
|
success: false,
|
|
3027
2878
|
error: errorMsg,
|
|
@@ -3038,24 +2889,8 @@ function createApp(storage, config) {
|
|
|
3038
2889
|
}
|
|
3039
2890
|
|
|
3040
2891
|
// src/config.ts
|
|
3041
|
-
var BUILD_VERSION = true ? "0.5.
|
|
2892
|
+
var BUILD_VERSION = true ? "0.5.13" : "unknown";
|
|
3042
2893
|
function loadConfig() {
|
|
3043
|
-
let masterEncryptionKey = process.env.MASTER_ENCRYPTION_KEY;
|
|
3044
|
-
if (!masterEncryptionKey) {
|
|
3045
|
-
const isDevelopment = process.env.NODE_ENV === "development";
|
|
3046
|
-
if (!isDevelopment) {
|
|
3047
|
-
throw new Error(
|
|
3048
|
-
"MASTER_ENCRYPTION_KEY environment variable must be set. Generate with: openssl rand -hex 32\nFor development only, set NODE_ENV=development to use insecure dev key."
|
|
3049
|
-
);
|
|
3050
|
-
}
|
|
3051
|
-
console.error("\u26A0\uFE0F WARNING: Using insecure deterministic development key");
|
|
3052
|
-
console.error("\u26A0\uFE0F ONLY use NODE_ENV=development for local development");
|
|
3053
|
-
console.error("\u26A0\uFE0F Generate production key with: openssl rand -hex 32");
|
|
3054
|
-
masterEncryptionKey = "a3f8b9c2d1e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0";
|
|
3055
|
-
}
|
|
3056
|
-
if (masterEncryptionKey.length !== 64 || !/^[0-9a-fA-F]{64}$/.test(masterEncryptionKey)) {
|
|
3057
|
-
throw new Error("MASTER_ENCRYPTION_KEY must be 64-character hex string (32 bytes). Generate with: openssl rand -hex 32");
|
|
3058
|
-
}
|
|
3059
2894
|
function parsePositiveInt(value, defaultValue, name, min = 1) {
|
|
3060
2895
|
const parsed = parseInt(value || defaultValue, 10);
|
|
3061
2896
|
if (isNaN(parsed)) {
|
|
@@ -3091,13 +2926,10 @@ function loadConfig() {
|
|
|
3091
2926
|
// Min 1 second
|
|
3092
2927
|
timestampMaxFuture: parsePositiveInt(process.env.TIMESTAMP_MAX_FUTURE, "60000", "TIMESTAMP_MAX_FUTURE", 1e3),
|
|
3093
2928
|
// Min 1 second
|
|
3094
|
-
masterEncryptionKey,
|
|
3095
2929
|
// Resource limits
|
|
3096
2930
|
maxOffersPerUser: parsePositiveInt(process.env.MAX_OFFERS_PER_USER, "1000", "MAX_OFFERS_PER_USER", 1),
|
|
3097
2931
|
maxTotalOffers: parsePositiveInt(process.env.MAX_TOTAL_OFFERS, "100000", "MAX_TOTAL_OFFERS", 1),
|
|
3098
|
-
maxTotalCredentials: parsePositiveInt(process.env.MAX_TOTAL_CREDENTIALS, "50000", "MAX_TOTAL_CREDENTIALS", 1),
|
|
3099
2932
|
maxIceCandidatesPerOffer: parsePositiveInt(process.env.MAX_ICE_CANDIDATES_PER_OFFER, "50", "MAX_ICE_CANDIDATES_PER_OFFER", 1),
|
|
3100
|
-
credentialsPerIpPerSecond: parsePositiveInt(process.env.CREDENTIALS_PER_IP_PER_SECOND, "5", "CREDENTIALS_PER_IP_PER_SECOND", 1),
|
|
3101
2933
|
requestsPerIpPerSecond: parsePositiveInt(process.env.REQUESTS_PER_IP_PER_SECOND, "50", "REQUESTS_PER_IP_PER_SECOND", 1)
|
|
3102
2934
|
};
|
|
3103
2935
|
return config;
|
|
@@ -3119,17 +2951,14 @@ var CONFIG_DEFAULTS = {
|
|
|
3119
2951
|
// Resource limits
|
|
3120
2952
|
maxOffersPerUser: 1e3,
|
|
3121
2953
|
maxTotalOffers: 1e5,
|
|
3122
|
-
maxTotalCredentials: 5e4,
|
|
3123
2954
|
maxIceCandidatesPerOffer: 50,
|
|
3124
|
-
credentialsPerIpPerSecond: 5,
|
|
3125
2955
|
requestsPerIpPerSecond: 50
|
|
3126
2956
|
};
|
|
3127
2957
|
async function runCleanup(storage, now) {
|
|
3128
2958
|
const offers = await storage.deleteExpiredOffers(now);
|
|
3129
|
-
const credentials = await storage.deleteExpiredCredentials(now);
|
|
3130
2959
|
const rateLimits = await storage.deleteExpiredRateLimits(now);
|
|
3131
2960
|
const nonces = await storage.deleteExpiredNonces(now);
|
|
3132
|
-
return { offers,
|
|
2961
|
+
return { offers, rateLimits, nonces };
|
|
3133
2962
|
}
|
|
3134
2963
|
|
|
3135
2964
|
// src/storage/factory.ts
|
|
@@ -3137,36 +2966,25 @@ async function createStorage(config) {
|
|
|
3137
2966
|
switch (config.type) {
|
|
3138
2967
|
case "memory": {
|
|
3139
2968
|
const { MemoryStorage: MemoryStorage2 } = await Promise.resolve().then(() => (init_memory(), memory_exports));
|
|
3140
|
-
return new MemoryStorage2(
|
|
2969
|
+
return new MemoryStorage2();
|
|
3141
2970
|
}
|
|
3142
2971
|
case "sqlite": {
|
|
3143
2972
|
const { SQLiteStorage: SQLiteStorage2 } = await Promise.resolve().then(() => (init_sqlite(), sqlite_exports));
|
|
3144
|
-
return new SQLiteStorage2(
|
|
3145
|
-
config.sqlitePath || ":memory:",
|
|
3146
|
-
config.masterEncryptionKey
|
|
3147
|
-
);
|
|
2973
|
+
return new SQLiteStorage2(config.sqlitePath || ":memory:");
|
|
3148
2974
|
}
|
|
3149
2975
|
case "mysql": {
|
|
3150
2976
|
if (!config.connectionString) {
|
|
3151
2977
|
throw new Error("MySQL storage requires DATABASE_URL connection string");
|
|
3152
2978
|
}
|
|
3153
2979
|
const { MySQLStorage: MySQLStorage2 } = await Promise.resolve().then(() => (init_mysql(), mysql_exports));
|
|
3154
|
-
return MySQLStorage2.create(
|
|
3155
|
-
config.connectionString,
|
|
3156
|
-
config.masterEncryptionKey,
|
|
3157
|
-
config.poolSize || 10
|
|
3158
|
-
);
|
|
2980
|
+
return MySQLStorage2.create(config.connectionString, config.poolSize || 10);
|
|
3159
2981
|
}
|
|
3160
2982
|
case "postgres": {
|
|
3161
2983
|
if (!config.connectionString) {
|
|
3162
2984
|
throw new Error("PostgreSQL storage requires DATABASE_URL connection string");
|
|
3163
2985
|
}
|
|
3164
2986
|
const { PostgreSQLStorage: PostgreSQLStorage2 } = await Promise.resolve().then(() => (init_postgres(), postgres_exports));
|
|
3165
|
-
return PostgreSQLStorage2.create(
|
|
3166
|
-
config.connectionString,
|
|
3167
|
-
config.masterEncryptionKey,
|
|
3168
|
-
config.poolSize || 10
|
|
3169
|
-
);
|
|
2987
|
+
return PostgreSQLStorage2.create(config.connectionString, config.poolSize || 10);
|
|
3170
2988
|
}
|
|
3171
2989
|
default:
|
|
3172
2990
|
throw new Error(`Unsupported storage type: ${config.type}`);
|
|
@@ -3189,7 +3007,6 @@ async function main() {
|
|
|
3189
3007
|
});
|
|
3190
3008
|
const storage = await createStorage({
|
|
3191
3009
|
type: config.storageType,
|
|
3192
|
-
masterEncryptionKey: config.masterEncryptionKey,
|
|
3193
3010
|
sqlitePath: config.storagePath,
|
|
3194
3011
|
connectionString: config.databaseUrl,
|
|
3195
3012
|
poolSize: config.dbPoolSize
|
|
@@ -3198,12 +3015,12 @@ async function main() {
|
|
|
3198
3015
|
const cleanupTimer = setInterval(async () => {
|
|
3199
3016
|
try {
|
|
3200
3017
|
const result = await runCleanup(storage, Date.now());
|
|
3201
|
-
const total = result.offers + result.
|
|
3018
|
+
const total = result.offers + result.rateLimits + result.nonces;
|
|
3202
3019
|
if (total > 0) {
|
|
3203
|
-
console.log(`Cleanup: ${result.offers} offers, ${result.
|
|
3020
|
+
console.log(`Cleanup: ${result.offers} offers, ${result.rateLimits} rate limits, ${result.nonces} nonces`);
|
|
3204
3021
|
}
|
|
3205
|
-
} catch (
|
|
3206
|
-
console.error("Cleanup error:",
|
|
3022
|
+
} catch (err2) {
|
|
3023
|
+
console.error("Cleanup error:", err2);
|
|
3207
3024
|
}
|
|
3208
3025
|
}, config.cleanupInterval);
|
|
3209
3026
|
const app = createApp(storage, config);
|
|
@@ -3222,8 +3039,13 @@ async function main() {
|
|
|
3222
3039
|
process.on("SIGINT", shutdown);
|
|
3223
3040
|
process.on("SIGTERM", shutdown);
|
|
3224
3041
|
}
|
|
3225
|
-
main().catch((
|
|
3226
|
-
console.error("Fatal error:",
|
|
3042
|
+
main().catch((err2) => {
|
|
3043
|
+
console.error("Fatal error:", err2);
|
|
3227
3044
|
process.exit(1);
|
|
3228
3045
|
});
|
|
3046
|
+
/*! Bundled license information:
|
|
3047
|
+
|
|
3048
|
+
@noble/ed25519/index.js:
|
|
3049
|
+
(*! noble-ed25519 - MIT License (c) 2019 Paul Miller (paulmillr.com) *)
|
|
3050
|
+
*/
|
|
3229
3051
|
//# sourceMappingURL=index.js.map
|