@xtr-dev/rondevu-server 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -29,10 +29,467 @@ var import_node_server = require("@hono/node-server");
29
29
  var import_hono = require("hono");
30
30
  var import_cors = require("hono/cors");
31
31
 
32
+ // node_modules/@noble/ed25519/index.js
33
+ var ed25519_CURVE = {
34
+ p: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffedn,
35
+ n: 0x1000000000000000000000000000000014def9dea2f79cd65812631a5cf5d3edn,
36
+ h: 8n,
37
+ a: 0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffecn,
38
+ d: 0x52036cee2b6ffe738cc740797779e89800700a4d4141d8ab75eb4dca135978a3n,
39
+ Gx: 0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an,
40
+ Gy: 0x6666666666666666666666666666666666666666666666666666666666666658n
41
+ };
42
+ var { p: P, n: N, Gx, Gy, a: _a, d: _d, h } = ed25519_CURVE;
43
+ var L = 32;
44
+ var L2 = 64;
45
+ var captureTrace = (...args) => {
46
+ if ("captureStackTrace" in Error && typeof Error.captureStackTrace === "function") {
47
+ Error.captureStackTrace(...args);
48
+ }
49
+ };
50
+ var err = (message = "") => {
51
+ const e = new Error(message);
52
+ captureTrace(e, err);
53
+ throw e;
54
+ };
55
+ var isBig = (n) => typeof n === "bigint";
56
+ var isStr = (s) => typeof s === "string";
57
+ var isBytes = (a) => a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array";
58
+ var abytes = (value, length, title = "") => {
59
+ const bytes = isBytes(value);
60
+ const len = value?.length;
61
+ const needsLen = length !== void 0;
62
+ if (!bytes || needsLen && len !== length) {
63
+ const prefix = title && `"${title}" `;
64
+ const ofLen = needsLen ? ` of length ${length}` : "";
65
+ const got = bytes ? `length=${len}` : `type=${typeof value}`;
66
+ err(prefix + "expected Uint8Array" + ofLen + ", got " + got);
67
+ }
68
+ return value;
69
+ };
70
+ var u8n = (len) => new Uint8Array(len);
71
+ var u8fr = (buf) => Uint8Array.from(buf);
72
+ var padh = (n, pad) => n.toString(16).padStart(pad, "0");
73
+ var bytesToHex = (b) => Array.from(abytes(b)).map((e) => padh(e, 2)).join("");
74
+ var C = { _0: 48, _9: 57, A: 65, F: 70, a: 97, f: 102 };
75
+ var _ch = (ch) => {
76
+ if (ch >= C._0 && ch <= C._9)
77
+ return ch - C._0;
78
+ if (ch >= C.A && ch <= C.F)
79
+ return ch - (C.A - 10);
80
+ if (ch >= C.a && ch <= C.f)
81
+ return ch - (C.a - 10);
82
+ return;
83
+ };
84
+ var hexToBytes = (hex) => {
85
+ const e = "hex invalid";
86
+ if (!isStr(hex))
87
+ return err(e);
88
+ const hl = hex.length;
89
+ const al = hl / 2;
90
+ if (hl % 2)
91
+ return err(e);
92
+ const array = u8n(al);
93
+ for (let ai = 0, hi = 0; ai < al; ai++, hi += 2) {
94
+ const n1 = _ch(hex.charCodeAt(hi));
95
+ const n2 = _ch(hex.charCodeAt(hi + 1));
96
+ if (n1 === void 0 || n2 === void 0)
97
+ return err(e);
98
+ array[ai] = n1 * 16 + n2;
99
+ }
100
+ return array;
101
+ };
102
+ var cr = () => globalThis?.crypto;
103
+ var subtle = () => cr()?.subtle ?? err("crypto.subtle must be defined, consider polyfill");
104
+ var concatBytes = (...arrs) => {
105
+ const r = u8n(arrs.reduce((sum, a) => sum + abytes(a).length, 0));
106
+ let pad = 0;
107
+ arrs.forEach((a) => {
108
+ r.set(a, pad);
109
+ pad += a.length;
110
+ });
111
+ return r;
112
+ };
113
+ var big = BigInt;
114
+ var assertRange = (n, min, max, msg = "bad number: out of range") => isBig(n) && min <= n && n < max ? n : err(msg);
115
+ var M = (a, b = P) => {
116
+ const r = a % b;
117
+ return r >= 0n ? r : b + r;
118
+ };
119
+ var modN = (a) => M(a, N);
120
+ var invert = (num, md) => {
121
+ if (num === 0n || md <= 0n)
122
+ err("no inverse n=" + num + " mod=" + md);
123
+ let a = M(num, md), b = md, x = 0n, y = 1n, u = 1n, v = 0n;
124
+ while (a !== 0n) {
125
+ const q = b / a, r = b % a;
126
+ const m = x - u * q, n = y - v * q;
127
+ b = a, a = r, x = u, y = v, u = m, v = n;
128
+ }
129
+ return b === 1n ? M(x, md) : err("no inverse");
130
+ };
131
+ var callHash = (name) => {
132
+ const fn = hashes[name];
133
+ if (typeof fn !== "function")
134
+ err("hashes." + name + " not set");
135
+ return fn;
136
+ };
137
+ var apoint = (p) => p instanceof Point ? p : err("Point expected");
138
+ var B256 = 2n ** 256n;
139
+ var Point = class _Point {
140
+ static BASE;
141
+ static ZERO;
142
+ X;
143
+ Y;
144
+ Z;
145
+ T;
146
+ constructor(X, Y, Z, T) {
147
+ const max = B256;
148
+ this.X = assertRange(X, 0n, max);
149
+ this.Y = assertRange(Y, 0n, max);
150
+ this.Z = assertRange(Z, 1n, max);
151
+ this.T = assertRange(T, 0n, max);
152
+ Object.freeze(this);
153
+ }
154
+ static CURVE() {
155
+ return ed25519_CURVE;
156
+ }
157
+ static fromAffine(p) {
158
+ return new _Point(p.x, p.y, 1n, M(p.x * p.y));
159
+ }
160
+ /** RFC8032 5.1.3: Uint8Array to Point. */
161
+ static fromBytes(hex, zip215 = false) {
162
+ const d = _d;
163
+ const normed = u8fr(abytes(hex, L));
164
+ const lastByte = hex[31];
165
+ normed[31] = lastByte & ~128;
166
+ const y = bytesToNumLE(normed);
167
+ const max = zip215 ? B256 : P;
168
+ assertRange(y, 0n, max);
169
+ const y2 = M(y * y);
170
+ const u = M(y2 - 1n);
171
+ const v = M(d * y2 + 1n);
172
+ let { isValid, value: x } = uvRatio(u, v);
173
+ if (!isValid)
174
+ err("bad point: y not sqrt");
175
+ const isXOdd = (x & 1n) === 1n;
176
+ const isLastByteOdd = (lastByte & 128) !== 0;
177
+ if (!zip215 && x === 0n && isLastByteOdd)
178
+ err("bad point: x==0, isLastByteOdd");
179
+ if (isLastByteOdd !== isXOdd)
180
+ x = M(-x);
181
+ return new _Point(x, y, 1n, M(x * y));
182
+ }
183
+ static fromHex(hex, zip215) {
184
+ return _Point.fromBytes(hexToBytes(hex), zip215);
185
+ }
186
+ get x() {
187
+ return this.toAffine().x;
188
+ }
189
+ get y() {
190
+ return this.toAffine().y;
191
+ }
192
+ /** Checks if the point is valid and on-curve. */
193
+ assertValidity() {
194
+ const a = _a;
195
+ const d = _d;
196
+ const p = this;
197
+ if (p.is0())
198
+ return err("bad point: ZERO");
199
+ const { X, Y, Z, T } = p;
200
+ const X2 = M(X * X);
201
+ const Y2 = M(Y * Y);
202
+ const Z2 = M(Z * Z);
203
+ const Z4 = M(Z2 * Z2);
204
+ const aX2 = M(X2 * a);
205
+ const left = M(Z2 * M(aX2 + Y2));
206
+ const right = M(Z4 + M(d * M(X2 * Y2)));
207
+ if (left !== right)
208
+ return err("bad point: equation left != right (1)");
209
+ const XY = M(X * Y);
210
+ const ZT = M(Z * T);
211
+ if (XY !== ZT)
212
+ return err("bad point: equation left != right (2)");
213
+ return this;
214
+ }
215
+ /** Equality check: compare points P&Q. */
216
+ equals(other) {
217
+ const { X: X1, Y: Y1, Z: Z1 } = this;
218
+ const { X: X2, Y: Y2, Z: Z2 } = apoint(other);
219
+ const X1Z2 = M(X1 * Z2);
220
+ const X2Z1 = M(X2 * Z1);
221
+ const Y1Z2 = M(Y1 * Z2);
222
+ const Y2Z1 = M(Y2 * Z1);
223
+ return X1Z2 === X2Z1 && Y1Z2 === Y2Z1;
224
+ }
225
+ is0() {
226
+ return this.equals(I);
227
+ }
228
+ /** Flip point over y coordinate. */
229
+ negate() {
230
+ return new _Point(M(-this.X), this.Y, this.Z, M(-this.T));
231
+ }
232
+ /** Point doubling. Complete formula. Cost: `4M + 4S + 1*a + 6add + 1*2`. */
233
+ double() {
234
+ const { X: X1, Y: Y1, Z: Z1 } = this;
235
+ const a = _a;
236
+ const A = M(X1 * X1);
237
+ const B = M(Y1 * Y1);
238
+ const C2 = M(2n * M(Z1 * Z1));
239
+ const D = M(a * A);
240
+ const x1y1 = X1 + Y1;
241
+ const E = M(M(x1y1 * x1y1) - A - B);
242
+ const G2 = D + B;
243
+ const F = G2 - C2;
244
+ const H = D - B;
245
+ const X3 = M(E * F);
246
+ const Y3 = M(G2 * H);
247
+ const T3 = M(E * H);
248
+ const Z3 = M(F * G2);
249
+ return new _Point(X3, Y3, Z3, T3);
250
+ }
251
+ /** Point addition. Complete formula. Cost: `8M + 1*k + 8add + 1*2`. */
252
+ add(other) {
253
+ const { X: X1, Y: Y1, Z: Z1, T: T1 } = this;
254
+ const { X: X2, Y: Y2, Z: Z2, T: T2 } = apoint(other);
255
+ const a = _a;
256
+ const d = _d;
257
+ const A = M(X1 * X2);
258
+ const B = M(Y1 * Y2);
259
+ const C2 = M(T1 * d * T2);
260
+ const D = M(Z1 * Z2);
261
+ const E = M((X1 + Y1) * (X2 + Y2) - A - B);
262
+ const F = M(D - C2);
263
+ const G2 = M(D + C2);
264
+ const H = M(B - a * A);
265
+ const X3 = M(E * F);
266
+ const Y3 = M(G2 * H);
267
+ const T3 = M(E * H);
268
+ const Z3 = M(F * G2);
269
+ return new _Point(X3, Y3, Z3, T3);
270
+ }
271
+ subtract(other) {
272
+ return this.add(apoint(other).negate());
273
+ }
274
+ /**
275
+ * Point-by-scalar multiplication. Scalar must be in range 1 <= n < CURVE.n.
276
+ * Uses {@link wNAF} for base point.
277
+ * Uses fake point to mitigate side-channel leakage.
278
+ * @param n scalar by which point is multiplied
279
+ * @param safe safe mode guards against timing attacks; unsafe mode is faster
280
+ */
281
+ multiply(n, safe = true) {
282
+ if (!safe && (n === 0n || this.is0()))
283
+ return I;
284
+ assertRange(n, 1n, N);
285
+ if (n === 1n)
286
+ return this;
287
+ if (this.equals(G))
288
+ return wNAF(n).p;
289
+ let p = I;
290
+ let f = G;
291
+ for (let d = this; n > 0n; d = d.double(), n >>= 1n) {
292
+ if (n & 1n)
293
+ p = p.add(d);
294
+ else if (safe)
295
+ f = f.add(d);
296
+ }
297
+ return p;
298
+ }
299
+ multiplyUnsafe(scalar) {
300
+ return this.multiply(scalar, false);
301
+ }
302
+ /** Convert point to 2d xy affine point. (X, Y, Z) ∋ (x=X/Z, y=Y/Z) */
303
+ toAffine() {
304
+ const { X, Y, Z } = this;
305
+ if (this.equals(I))
306
+ return { x: 0n, y: 1n };
307
+ const iz = invert(Z, P);
308
+ if (M(Z * iz) !== 1n)
309
+ err("invalid inverse");
310
+ const x = M(X * iz);
311
+ const y = M(Y * iz);
312
+ return { x, y };
313
+ }
314
+ toBytes() {
315
+ const { x, y } = this.assertValidity().toAffine();
316
+ const b = numTo32bLE(y);
317
+ b[31] |= x & 1n ? 128 : 0;
318
+ return b;
319
+ }
320
+ toHex() {
321
+ return bytesToHex(this.toBytes());
322
+ }
323
+ clearCofactor() {
324
+ return this.multiply(big(h), false);
325
+ }
326
+ isSmallOrder() {
327
+ return this.clearCofactor().is0();
328
+ }
329
+ isTorsionFree() {
330
+ let p = this.multiply(N / 2n, false).double();
331
+ if (N % 2n)
332
+ p = p.add(this);
333
+ return p.is0();
334
+ }
335
+ };
336
+ var G = new Point(Gx, Gy, 1n, M(Gx * Gy));
337
+ var I = new Point(0n, 1n, 1n, 0n);
338
+ Point.BASE = G;
339
+ Point.ZERO = I;
340
+ var numTo32bLE = (num) => hexToBytes(padh(assertRange(num, 0n, B256), L2)).reverse();
341
+ var bytesToNumLE = (b) => big("0x" + bytesToHex(u8fr(abytes(b)).reverse()));
342
+ var pow2 = (x, power) => {
343
+ let r = x;
344
+ while (power-- > 0n) {
345
+ r *= r;
346
+ r %= P;
347
+ }
348
+ return r;
349
+ };
350
+ var pow_2_252_3 = (x) => {
351
+ const x2 = x * x % P;
352
+ const b2 = x2 * x % P;
353
+ const b4 = pow2(b2, 2n) * b2 % P;
354
+ const b5 = pow2(b4, 1n) * x % P;
355
+ const b10 = pow2(b5, 5n) * b5 % P;
356
+ const b20 = pow2(b10, 10n) * b10 % P;
357
+ const b40 = pow2(b20, 20n) * b20 % P;
358
+ const b80 = pow2(b40, 40n) * b40 % P;
359
+ const b160 = pow2(b80, 80n) * b80 % P;
360
+ const b240 = pow2(b160, 80n) * b80 % P;
361
+ const b250 = pow2(b240, 10n) * b10 % P;
362
+ const pow_p_5_8 = pow2(b250, 2n) * x % P;
363
+ return { pow_p_5_8, b2 };
364
+ };
365
+ var RM1 = 0x2b8324804fc1df0b2b4d00993dfbd7a72f431806ad2fe478c4ee1b274a0ea0b0n;
366
+ var uvRatio = (u, v) => {
367
+ const v3 = M(v * v * v);
368
+ const v7 = M(v3 * v3 * v);
369
+ const pow = pow_2_252_3(u * v7).pow_p_5_8;
370
+ let x = M(u * v3 * pow);
371
+ const vx2 = M(v * x * x);
372
+ const root1 = x;
373
+ const root2 = M(x * RM1);
374
+ const useRoot1 = vx2 === u;
375
+ const useRoot2 = vx2 === M(-u);
376
+ const noRoot = vx2 === M(-u * RM1);
377
+ if (useRoot1)
378
+ x = root1;
379
+ if (useRoot2 || noRoot)
380
+ x = root2;
381
+ if ((M(x) & 1n) === 1n)
382
+ x = M(-x);
383
+ return { isValid: useRoot1 || useRoot2, value: x };
384
+ };
385
+ var modL_LE = (hash) => modN(bytesToNumLE(hash));
386
+ var sha512s = (...m) => callHash("sha512")(concatBytes(...m));
387
+ var hashFinishS = (res) => res.finish(sha512s(res.hashable));
388
+ var defaultVerifyOpts = { zip215: true };
389
+ var _verify = (sig, msg, pub, opts = defaultVerifyOpts) => {
390
+ sig = abytes(sig, L2);
391
+ msg = abytes(msg);
392
+ pub = abytes(pub, L);
393
+ const { zip215 } = opts;
394
+ let A;
395
+ let R;
396
+ let s;
397
+ let SB;
398
+ let hashable = Uint8Array.of();
399
+ try {
400
+ A = Point.fromBytes(pub, zip215);
401
+ R = Point.fromBytes(sig.slice(0, L), zip215);
402
+ s = bytesToNumLE(sig.slice(L, L2));
403
+ SB = G.multiply(s, false);
404
+ hashable = concatBytes(R.toBytes(), A.toBytes(), msg);
405
+ } catch (error) {
406
+ }
407
+ const finish = (hashed) => {
408
+ if (SB == null)
409
+ return false;
410
+ if (!zip215 && A.isSmallOrder())
411
+ return false;
412
+ const k = modL_LE(hashed);
413
+ const RkA = R.add(A.multiply(k, false));
414
+ return RkA.add(SB.negate()).clearCofactor().is0();
415
+ };
416
+ return { hashable, finish };
417
+ };
418
+ var verify = (signature, message, publicKey, opts = defaultVerifyOpts) => hashFinishS(_verify(signature, message, publicKey, opts));
419
+ var hashes = {
420
+ sha512Async: async (message) => {
421
+ const s = subtle();
422
+ const m = concatBytes(message);
423
+ return u8n(await s.digest("SHA-512", m.buffer));
424
+ },
425
+ sha512: void 0
426
+ };
427
+ var W = 8;
428
+ var scalarBits = 256;
429
+ var pwindows = Math.ceil(scalarBits / W) + 1;
430
+ var pwindowSize = 2 ** (W - 1);
431
+ var precompute = () => {
432
+ const points = [];
433
+ let p = G;
434
+ let b = p;
435
+ for (let w = 0; w < pwindows; w++) {
436
+ b = p;
437
+ points.push(b);
438
+ for (let i = 1; i < pwindowSize; i++) {
439
+ b = b.add(p);
440
+ points.push(b);
441
+ }
442
+ p = b.double();
443
+ }
444
+ return points;
445
+ };
446
+ var Gpows = void 0;
447
+ var ctneg = (cnd, p) => {
448
+ const n = p.negate();
449
+ return cnd ? n : p;
450
+ };
451
+ var wNAF = (n) => {
452
+ const comp = Gpows || (Gpows = precompute());
453
+ let p = I;
454
+ let f = G;
455
+ const pow_2_w = 2 ** W;
456
+ const maxNum = pow_2_w;
457
+ const mask = big(pow_2_w - 1);
458
+ const shiftBy = big(W);
459
+ for (let w = 0; w < pwindows; w++) {
460
+ let wbits = Number(n & mask);
461
+ n >>= shiftBy;
462
+ if (wbits > pwindowSize) {
463
+ wbits -= maxNum;
464
+ n += 1n;
465
+ }
466
+ const off = w * pwindowSize;
467
+ const offF = off;
468
+ const offP = off + Math.abs(wbits) - 1;
469
+ const isEven = w % 2 !== 0;
470
+ const isNeg = wbits < 0;
471
+ if (wbits === 0) {
472
+ f = f.add(ctneg(isEven, comp[offF]));
473
+ } else {
474
+ p = p.add(ctneg(isNeg, comp[offP]));
475
+ }
476
+ }
477
+ if (n !== 0n)
478
+ err("invalid wnaf");
479
+ return { p, f };
480
+ };
481
+
32
482
  // src/crypto.ts
483
+ hashes.sha512Async = async (message) => {
484
+ return new Uint8Array(await crypto.subtle.digest("SHA-512", message));
485
+ };
33
486
  var ALGORITHM = "AES-GCM";
34
487
  var IV_LENGTH = 12;
35
488
  var KEY_LENGTH = 32;
489
+ var USERNAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
490
+ var USERNAME_MIN_LENGTH = 3;
491
+ var USERNAME_MAX_LENGTH = 32;
492
+ var TIMESTAMP_TOLERANCE_MS = 5 * 60 * 1e3;
36
493
  function generatePeerId() {
37
494
  const bytes = crypto.getRandomValues(new Uint8Array(16));
38
495
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
@@ -41,7 +498,7 @@ function generateSecretKey() {
41
498
  const bytes = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
42
499
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
43
500
  }
44
- function hexToBytes(hex) {
501
+ function hexToBytes2(hex) {
45
502
  const bytes = new Uint8Array(hex.length / 2);
46
503
  for (let i = 0; i < hex.length; i += 2) {
47
504
  bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
@@ -60,7 +517,7 @@ function base64ToBytes(base64) {
60
517
  return Uint8Array.from(binString, (char) => char.codePointAt(0));
61
518
  }
62
519
  async function encryptPeerId(peerId, secretKeyHex) {
63
- const keyBytes = hexToBytes(secretKeyHex);
520
+ const keyBytes = hexToBytes2(secretKeyHex);
64
521
  if (keyBytes.length !== KEY_LENGTH) {
65
522
  throw new Error(`Secret key must be ${KEY_LENGTH * 2} hex characters (${KEY_LENGTH} bytes)`);
66
523
  }
@@ -86,7 +543,7 @@ async function encryptPeerId(peerId, secretKeyHex) {
86
543
  }
87
544
  async function decryptPeerId(encryptedSecret, secretKeyHex) {
88
545
  try {
89
- const keyBytes = hexToBytes(secretKeyHex);
546
+ const keyBytes = hexToBytes2(secretKeyHex);
90
547
  if (keyBytes.length !== KEY_LENGTH) {
91
548
  throw new Error(`Secret key must be ${KEY_LENGTH * 2} hex characters (${KEY_LENGTH} bytes)`);
92
549
  }
@@ -107,7 +564,7 @@ async function decryptPeerId(encryptedSecret, secretKeyHex) {
107
564
  );
108
565
  const decoder = new TextDecoder();
109
566
  return decoder.decode(decrypted);
110
- } catch (err) {
567
+ } catch (err2) {
111
568
  throw new Error("Failed to decrypt peer ID: invalid secret or secret key");
112
569
  }
113
570
  }
@@ -119,6 +576,90 @@ async function validateCredentials(peerId, encryptedSecret, secretKey) {
119
576
  return false;
120
577
  }
121
578
  }
579
+ function validateUsername(username) {
580
+ if (typeof username !== "string") {
581
+ return { valid: false, error: "Username must be a string" };
582
+ }
583
+ if (username.length < USERNAME_MIN_LENGTH) {
584
+ return { valid: false, error: `Username must be at least ${USERNAME_MIN_LENGTH} characters` };
585
+ }
586
+ if (username.length > USERNAME_MAX_LENGTH) {
587
+ return { valid: false, error: `Username must be at most ${USERNAME_MAX_LENGTH} characters` };
588
+ }
589
+ if (!USERNAME_REGEX.test(username)) {
590
+ return { valid: false, error: "Username must be lowercase alphanumeric with optional dashes, and start/end with alphanumeric" };
591
+ }
592
+ return { valid: true };
593
+ }
594
+ function validateServiceFqn(fqn) {
595
+ if (typeof fqn !== "string") {
596
+ return { valid: false, error: "Service FQN must be a string" };
597
+ }
598
+ const parts = fqn.split("@");
599
+ if (parts.length !== 2) {
600
+ return { valid: false, error: "Service FQN must be in format: service-name@version" };
601
+ }
602
+ const [serviceName, version] = parts;
603
+ const serviceNameRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
604
+ if (!serviceNameRegex.test(serviceName)) {
605
+ return { valid: false, error: "Service name must be reverse domain notation (e.g., com.example.service)" };
606
+ }
607
+ if (serviceName.length < 3 || serviceName.length > 128) {
608
+ return { valid: false, error: "Service name must be 3-128 characters" };
609
+ }
610
+ const versionRegex = /^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.-]+)?$/;
611
+ if (!versionRegex.test(version)) {
612
+ return { valid: false, error: "Version must be semantic versioning (e.g., 1.0.0, 2.1.3-beta)" };
613
+ }
614
+ return { valid: true };
615
+ }
616
+ function validateTimestamp(timestamp) {
617
+ if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
618
+ return { valid: false, error: "Timestamp must be a finite number" };
619
+ }
620
+ const now = Date.now();
621
+ const diff = Math.abs(now - timestamp);
622
+ if (diff > TIMESTAMP_TOLERANCE_MS) {
623
+ return { valid: false, error: `Timestamp too old or too far in future (tolerance: ${TIMESTAMP_TOLERANCE_MS / 1e3}s)` };
624
+ }
625
+ return { valid: true };
626
+ }
627
+ async function verifyEd25519Signature(publicKey, signature, message) {
628
+ try {
629
+ const publicKeyBytes = base64ToBytes(publicKey);
630
+ const signatureBytes = base64ToBytes(signature);
631
+ const encoder = new TextEncoder();
632
+ const messageBytes = encoder.encode(message);
633
+ const isValid = await verify(signatureBytes, messageBytes, publicKeyBytes);
634
+ return isValid;
635
+ } catch (err2) {
636
+ console.error("Ed25519 signature verification failed:", err2);
637
+ return false;
638
+ }
639
+ }
640
+ async function validateUsernameClaim(username, publicKey, signature, message) {
641
+ const usernameCheck = validateUsername(username);
642
+ if (!usernameCheck.valid) {
643
+ return usernameCheck;
644
+ }
645
+ const parts = message.split(":");
646
+ if (parts.length !== 3 || parts[0] !== "claim" || parts[1] !== username) {
647
+ return { valid: false, error: "Invalid message format (expected: claim:{username}:{timestamp})" };
648
+ }
649
+ const timestamp = parseInt(parts[2], 10);
650
+ if (isNaN(timestamp)) {
651
+ return { valid: false, error: "Invalid timestamp in message" };
652
+ }
653
+ const timestampCheck = validateTimestamp(timestamp);
654
+ if (!timestampCheck.valid) {
655
+ return timestampCheck;
656
+ }
657
+ const signatureValid = await verifyEd25519Signature(publicKey, signature, message);
658
+ if (!signatureValid) {
659
+ return { valid: false, error: "Invalid signature" };
660
+ }
661
+ return { valid: true };
662
+ }
122
663
 
123
664
  // src/middleware/auth.ts
124
665
  function createAuthMiddleware(authSecret) {
@@ -152,57 +693,6 @@ function getAuthenticatedPeerId(c) {
152
693
  return peerId;
153
694
  }
154
695
 
155
- // src/bloom.ts
156
- var BloomFilter = class {
157
- /**
158
- * Creates a bloom filter from a base64 encoded bit array
159
- */
160
- constructor(base64Data, numHashes = 3) {
161
- const binaryString = atob(base64Data);
162
- const bytes = new Uint8Array(binaryString.length);
163
- for (let i = 0; i < binaryString.length; i++) {
164
- bytes[i] = binaryString.charCodeAt(i);
165
- }
166
- this.bits = bytes;
167
- this.size = this.bits.length * 8;
168
- this.numHashes = numHashes;
169
- }
170
- /**
171
- * Test if a peer ID might be in the filter
172
- * Returns true if possibly in set, false if definitely not in set
173
- */
174
- test(peerId) {
175
- for (let i = 0; i < this.numHashes; i++) {
176
- const hash = this.hash(peerId, i);
177
- const index = hash % this.size;
178
- const byteIndex = Math.floor(index / 8);
179
- const bitIndex = index % 8;
180
- if (!(this.bits[byteIndex] & 1 << bitIndex)) {
181
- return false;
182
- }
183
- }
184
- return true;
185
- }
186
- /**
187
- * Simple hash function (FNV-1a variant)
188
- */
189
- hash(str, seed) {
190
- let hash = 2166136261 ^ seed;
191
- for (let i = 0; i < str.length; i++) {
192
- hash ^= str.charCodeAt(i);
193
- hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
194
- }
195
- return hash >>> 0;
196
- }
197
- };
198
- function parseBloomFilter(base64) {
199
- try {
200
- return new BloomFilter(base64);
201
- } catch {
202
- return null;
203
- }
204
- }
205
-
206
696
  // src/app.ts
207
697
  function createApp(storage, config) {
208
698
  const app = new import_hono.Hono();
@@ -227,7 +717,7 @@ function createApp(storage, config) {
227
717
  return c.json({
228
718
  version: config.version,
229
719
  name: "Rondevu",
230
- description: "Topic-based peer discovery and signaling server"
720
+ description: "DNS-like WebRTC signaling with username claiming and service discovery"
231
721
  });
232
722
  });
233
723
  app.get("/health", (c) => {
@@ -245,204 +735,271 @@ function createApp(storage, config) {
245
735
  peerId,
246
736
  secret
247
737
  }, 200);
248
- } catch (err) {
249
- console.error("Error registering peer:", err);
738
+ } catch (err2) {
739
+ console.error("Error registering peer:", err2);
250
740
  return c.json({ error: "Internal server error" }, 500);
251
741
  }
252
742
  });
253
- app.post("/offers", authMiddleware, async (c) => {
743
+ app.post("/usernames/claim", async (c) => {
254
744
  try {
255
745
  const body = await c.req.json();
256
- const { offers } = body;
257
- if (!Array.isArray(offers) || offers.length === 0) {
258
- return c.json({ error: "Missing or invalid required parameter: offers (must be non-empty array)" }, 400);
746
+ const { username, publicKey, signature, message } = body;
747
+ if (!username || !publicKey || !signature || !message) {
748
+ return c.json({ error: "Missing required parameters: username, publicKey, signature, message" }, 400);
259
749
  }
260
- if (offers.length > config.maxOffersPerRequest) {
261
- return c.json({ error: `Too many offers. Maximum ${config.maxOffersPerRequest} per request` }, 400);
750
+ const validation = await validateUsernameClaim(username, publicKey, signature, message);
751
+ if (!validation.valid) {
752
+ return c.json({ error: validation.error }, 400);
262
753
  }
263
- const peerId = getAuthenticatedPeerId(c);
264
- const offerRequests = [];
265
- for (const offer of offers) {
266
- if (!offer.sdp || typeof offer.sdp !== "string") {
267
- return c.json({ error: "Each offer must have an sdp field" }, 400);
268
- }
269
- if (offer.sdp.length > 65536) {
270
- return c.json({ error: "SDP must be 64KB or less" }, 400);
271
- }
272
- if (offer.secret !== void 0) {
273
- if (typeof offer.secret !== "string") {
274
- return c.json({ error: "Secret must be a string" }, 400);
275
- }
276
- if (offer.secret.length > 128) {
277
- return c.json({ error: "Secret must be 128 characters or less" }, 400);
278
- }
279
- }
280
- if (offer.info !== void 0) {
281
- if (typeof offer.info !== "string") {
282
- return c.json({ error: "Info must be a string" }, 400);
283
- }
284
- if (offer.info.length > 128) {
285
- return c.json({ error: "Info must be 128 characters or less" }, 400);
286
- }
287
- }
288
- if (!Array.isArray(offer.topics) || offer.topics.length === 0) {
289
- return c.json({ error: "Each offer must have a non-empty topics array" }, 400);
290
- }
291
- if (offer.topics.length > config.maxTopicsPerOffer) {
292
- return c.json({ error: `Too many topics. Maximum ${config.maxTopicsPerOffer} per offer` }, 400);
293
- }
294
- for (const topic of offer.topics) {
295
- if (typeof topic !== "string" || topic.length === 0 || topic.length > 256) {
296
- return c.json({ error: "Each topic must be a string between 1 and 256 characters" }, 400);
297
- }
298
- }
299
- let ttl = offer.ttl || config.offerDefaultTtl;
300
- if (ttl < config.offerMinTtl) {
301
- ttl = config.offerMinTtl;
302
- }
303
- if (ttl > config.offerMaxTtl) {
304
- ttl = config.offerMaxTtl;
305
- }
306
- offerRequests.push({
307
- id: offer.id,
308
- peerId,
309
- sdp: offer.sdp,
310
- topics: offer.topics,
311
- expiresAt: Date.now() + ttl,
312
- secret: offer.secret,
313
- info: offer.info
754
+ try {
755
+ const claimed = await storage.claimUsername({
756
+ username,
757
+ publicKey,
758
+ signature,
759
+ message
314
760
  });
761
+ return c.json({
762
+ username: claimed.username,
763
+ claimedAt: claimed.claimedAt,
764
+ expiresAt: claimed.expiresAt
765
+ }, 200);
766
+ } catch (err2) {
767
+ if (err2.message?.includes("already claimed")) {
768
+ return c.json({ error: "Username already claimed by different public key" }, 409);
769
+ }
770
+ throw err2;
771
+ }
772
+ } catch (err2) {
773
+ console.error("Error claiming username:", err2);
774
+ return c.json({ error: "Internal server error" }, 500);
775
+ }
776
+ });
777
+ app.get("/usernames/:username", async (c) => {
778
+ try {
779
+ const username = c.req.param("username");
780
+ const claimed = await storage.getUsername(username);
781
+ if (!claimed) {
782
+ return c.json({
783
+ username,
784
+ available: true
785
+ }, 200);
315
786
  }
316
- const createdOffers = await storage.createOffers(offerRequests);
317
787
  return c.json({
318
- offers: createdOffers.map((o) => ({
319
- id: o.id,
320
- peerId: o.peerId,
321
- topics: o.topics,
322
- expiresAt: o.expiresAt
323
- }))
788
+ username: claimed.username,
789
+ available: false,
790
+ claimedAt: claimed.claimedAt,
791
+ expiresAt: claimed.expiresAt,
792
+ publicKey: claimed.publicKey
324
793
  }, 200);
325
- } catch (err) {
326
- console.error("Error creating offers:", err);
794
+ } catch (err2) {
795
+ console.error("Error checking username:", err2);
327
796
  return c.json({ error: "Internal server error" }, 500);
328
797
  }
329
798
  });
330
- app.get("/offers/by-topic/:topic", async (c) => {
799
+ app.get("/usernames/:username/services", async (c) => {
331
800
  try {
332
- const topic = c.req.param("topic");
333
- const bloomParam = c.req.query("bloom");
334
- const limitParam = c.req.query("limit");
335
- const limit = limitParam ? Math.min(parseInt(limitParam, 10), 200) : 50;
336
- let excludePeerIds = [];
337
- if (bloomParam) {
338
- const bloom = parseBloomFilter(bloomParam);
339
- if (!bloom) {
340
- return c.json({ error: "Invalid bloom filter format" }, 400);
341
- }
342
- const allOffers = await storage.getOffersByTopic(topic);
343
- const excludeSet = /* @__PURE__ */ new Set();
344
- for (const offer of allOffers) {
345
- if (bloom.test(offer.peerId)) {
346
- excludeSet.add(offer.peerId);
347
- }
348
- }
349
- excludePeerIds = Array.from(excludeSet);
350
- }
351
- let offers = await storage.getOffersByTopic(topic, excludePeerIds.length > 0 ? excludePeerIds : void 0);
352
- const total = offers.length;
353
- offers = offers.slice(0, limit);
801
+ const username = c.req.param("username");
802
+ const services = await storage.listServicesForUsername(username);
354
803
  return c.json({
355
- topic,
356
- offers: offers.map((o) => ({
357
- id: o.id,
358
- peerId: o.peerId,
359
- sdp: o.sdp,
360
- topics: o.topics,
361
- expiresAt: o.expiresAt,
362
- lastSeen: o.lastSeen,
363
- hasSecret: !!o.secret,
364
- // Indicate if secret is required without exposing it
365
- info: o.info
366
- // Public info field
367
- })),
368
- total: bloomParam ? total + excludePeerIds.length : total,
369
- returned: offers.length
804
+ username,
805
+ services
370
806
  }, 200);
371
- } catch (err) {
372
- console.error("Error fetching offers by topic:", err);
807
+ } catch (err2) {
808
+ console.error("Error listing services:", err2);
373
809
  return c.json({ error: "Internal server error" }, 500);
374
810
  }
375
811
  });
376
- app.get("/topics", async (c) => {
812
+ app.post("/services", authMiddleware, async (c) => {
377
813
  try {
378
- const limitParam = c.req.query("limit");
379
- const offsetParam = c.req.query("offset");
380
- const startsWithParam = c.req.query("startsWith");
381
- const limit = limitParam ? Math.min(parseInt(limitParam, 10), 200) : 50;
382
- const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
383
- const startsWith = startsWithParam || void 0;
384
- const result = await storage.getTopics(limit, offset, startsWith);
814
+ const body = await c.req.json();
815
+ const { username, serviceFqn, sdp, ttl, isPublic, metadata, signature, message } = body;
816
+ if (!username || !serviceFqn || !sdp) {
817
+ return c.json({ error: "Missing required parameters: username, serviceFqn, sdp" }, 400);
818
+ }
819
+ const fqnValidation = validateServiceFqn(serviceFqn);
820
+ if (!fqnValidation.valid) {
821
+ return c.json({ error: fqnValidation.error }, 400);
822
+ }
823
+ if (!signature || !message) {
824
+ return c.json({ error: "Missing signature or message for username verification" }, 400);
825
+ }
826
+ const usernameRecord = await storage.getUsername(username);
827
+ if (!usernameRecord) {
828
+ return c.json({ error: "Username not claimed" }, 404);
829
+ }
830
+ const signatureValidation = await validateUsernameClaim(username, usernameRecord.publicKey, signature, message);
831
+ if (!signatureValidation.valid) {
832
+ return c.json({ error: "Invalid signature for username" }, 403);
833
+ }
834
+ if (typeof sdp !== "string" || sdp.length === 0) {
835
+ return c.json({ error: "Invalid SDP" }, 400);
836
+ }
837
+ if (sdp.length > 64 * 1024) {
838
+ return c.json({ error: "SDP too large (max 64KB)" }, 400);
839
+ }
840
+ const peerId = getAuthenticatedPeerId(c);
841
+ const offerTtl = Math.min(
842
+ Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
843
+ config.offerMaxTtl
844
+ );
845
+ const expiresAt = Date.now() + offerTtl;
846
+ const offers = await storage.createOffers([{
847
+ peerId,
848
+ sdp,
849
+ expiresAt
850
+ }]);
851
+ if (offers.length === 0) {
852
+ return c.json({ error: "Failed to create offer" }, 500);
853
+ }
854
+ const offer = offers[0];
855
+ const result = await storage.createService({
856
+ username,
857
+ serviceFqn,
858
+ offerId: offer.id,
859
+ expiresAt,
860
+ isPublic: isPublic || false,
861
+ metadata: metadata ? JSON.stringify(metadata) : void 0
862
+ });
863
+ return c.json({
864
+ serviceId: result.service.id,
865
+ uuid: result.indexUuid,
866
+ offerId: offer.id,
867
+ expiresAt: result.service.expiresAt
868
+ }, 201);
869
+ } catch (err2) {
870
+ console.error("Error creating service:", err2);
871
+ return c.json({ error: "Internal server error" }, 500);
872
+ }
873
+ });
874
+ app.get("/services/:uuid", async (c) => {
875
+ try {
876
+ const uuid = c.req.param("uuid");
877
+ const service = await storage.getServiceByUuid(uuid);
878
+ if (!service) {
879
+ return c.json({ error: "Service not found" }, 404);
880
+ }
881
+ const offer = await storage.getOfferById(service.offerId);
882
+ if (!offer) {
883
+ return c.json({ error: "Associated offer not found" }, 404);
884
+ }
385
885
  return c.json({
386
- topics: result.topics,
387
- total: result.total,
388
- limit,
389
- offset,
390
- ...startsWith && { startsWith }
886
+ serviceId: service.id,
887
+ username: service.username,
888
+ serviceFqn: service.serviceFqn,
889
+ offerId: service.offerId,
890
+ sdp: offer.sdp,
891
+ isPublic: service.isPublic,
892
+ metadata: service.metadata ? JSON.parse(service.metadata) : void 0,
893
+ createdAt: service.createdAt,
894
+ expiresAt: service.expiresAt
391
895
  }, 200);
392
- } catch (err) {
393
- console.error("Error fetching topics:", err);
896
+ } catch (err2) {
897
+ console.error("Error getting service:", err2);
394
898
  return c.json({ error: "Internal server error" }, 500);
395
899
  }
396
900
  });
397
- app.get("/peers/:peerId/offers", async (c) => {
901
+ app.delete("/services/:serviceId", authMiddleware, async (c) => {
398
902
  try {
399
- const peerId = c.req.param("peerId");
400
- const offers = await storage.getOffersByPeerId(peerId);
401
- const topicsSet = /* @__PURE__ */ new Set();
402
- offers.forEach((o) => o.topics.forEach((t) => topicsSet.add(t)));
903
+ const serviceId = c.req.param("serviceId");
904
+ const body = await c.req.json();
905
+ const { username } = body;
906
+ if (!username) {
907
+ return c.json({ error: "Missing required parameter: username" }, 400);
908
+ }
909
+ const deleted = await storage.deleteService(serviceId, username);
910
+ if (!deleted) {
911
+ return c.json({ error: "Service not found or not owned by this username" }, 404);
912
+ }
913
+ return c.json({ success: true }, 200);
914
+ } catch (err2) {
915
+ console.error("Error deleting service:", err2);
916
+ return c.json({ error: "Internal server error" }, 500);
917
+ }
918
+ });
919
+ app.post("/index/:username/query", async (c) => {
920
+ try {
921
+ const username = c.req.param("username");
922
+ const body = await c.req.json();
923
+ const { serviceFqn } = body;
924
+ if (!serviceFqn) {
925
+ return c.json({ error: "Missing required parameter: serviceFqn" }, 400);
926
+ }
927
+ const uuid = await storage.queryService(username, serviceFqn);
928
+ if (!uuid) {
929
+ return c.json({ error: "Service not found" }, 404);
930
+ }
403
931
  return c.json({
404
- peerId,
405
- offers: offers.map((o) => ({
406
- id: o.id,
407
- sdp: o.sdp,
408
- topics: o.topics,
409
- expiresAt: o.expiresAt,
410
- lastSeen: o.lastSeen,
411
- hasSecret: !!o.secret,
412
- // Indicate if secret is required without exposing it
413
- info: o.info
414
- // Public info field
415
- })),
416
- topics: Array.from(topicsSet)
932
+ uuid,
933
+ allowed: true
417
934
  }, 200);
418
- } catch (err) {
419
- console.error("Error fetching peer offers:", err);
935
+ } catch (err2) {
936
+ console.error("Error querying service:", err2);
420
937
  return c.json({ error: "Internal server error" }, 500);
421
938
  }
422
939
  });
940
+ app.post("/offers", authMiddleware, async (c) => {
941
+ try {
942
+ const body = await c.req.json();
943
+ const { offers } = body;
944
+ if (!Array.isArray(offers) || offers.length === 0) {
945
+ return c.json({ error: "Missing or invalid required parameter: offers (must be non-empty array)" }, 400);
946
+ }
947
+ if (offers.length > config.maxOffersPerRequest) {
948
+ return c.json({ error: `Too many offers (max ${config.maxOffersPerRequest})` }, 400);
949
+ }
950
+ const peerId = getAuthenticatedPeerId(c);
951
+ const validated = offers.map((offer) => {
952
+ const { sdp, ttl, secret } = offer;
953
+ if (typeof sdp !== "string" || sdp.length === 0) {
954
+ throw new Error("Invalid SDP in offer");
955
+ }
956
+ if (sdp.length > 64 * 1024) {
957
+ throw new Error("SDP too large (max 64KB)");
958
+ }
959
+ const offerTtl = Math.min(
960
+ Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
961
+ config.offerMaxTtl
962
+ );
963
+ return {
964
+ peerId,
965
+ sdp,
966
+ expiresAt: Date.now() + offerTtl,
967
+ secret: secret ? String(secret).substring(0, 128) : void 0
968
+ };
969
+ });
970
+ const created = await storage.createOffers(validated);
971
+ return c.json({
972
+ offers: created.map((offer) => ({
973
+ id: offer.id,
974
+ peerId: offer.peerId,
975
+ expiresAt: offer.expiresAt,
976
+ createdAt: offer.createdAt,
977
+ hasSecret: !!offer.secret
978
+ }))
979
+ }, 201);
980
+ } catch (err2) {
981
+ console.error("Error creating offers:", err2);
982
+ return c.json({ error: err2.message || "Internal server error" }, 500);
983
+ }
984
+ });
423
985
  app.get("/offers/mine", authMiddleware, async (c) => {
424
986
  try {
425
987
  const peerId = getAuthenticatedPeerId(c);
426
988
  const offers = await storage.getOffersByPeerId(peerId);
427
989
  return c.json({
428
- peerId,
429
- offers: offers.map((o) => ({
430
- id: o.id,
431
- sdp: o.sdp,
432
- topics: o.topics,
433
- createdAt: o.createdAt,
434
- expiresAt: o.expiresAt,
435
- lastSeen: o.lastSeen,
436
- secret: o.secret,
437
- // Owner can see the secret
438
- info: o.info,
439
- // Owner can see the info
440
- answererPeerId: o.answererPeerId,
441
- answeredAt: o.answeredAt
990
+ offers: offers.map((offer) => ({
991
+ id: offer.id,
992
+ sdp: offer.sdp,
993
+ createdAt: offer.createdAt,
994
+ expiresAt: offer.expiresAt,
995
+ lastSeen: offer.lastSeen,
996
+ hasSecret: !!offer.secret,
997
+ answererPeerId: offer.answererPeerId,
998
+ answered: !!offer.answererPeerId
442
999
  }))
443
1000
  }, 200);
444
- } catch (err) {
445
- console.error("Error fetching own offers:", err);
1001
+ } catch (err2) {
1002
+ console.error("Error getting offers:", err2);
446
1003
  return c.json({ error: "Internal server error" }, 500);
447
1004
  }
448
1005
  });
@@ -452,40 +1009,36 @@ function createApp(storage, config) {
452
1009
  const peerId = getAuthenticatedPeerId(c);
453
1010
  const deleted = await storage.deleteOffer(offerId, peerId);
454
1011
  if (!deleted) {
455
- return c.json({ error: "Offer not found or not authorized" }, 404);
1012
+ return c.json({ error: "Offer not found or not owned by this peer" }, 404);
456
1013
  }
457
- return c.json({ deleted: true }, 200);
458
- } catch (err) {
459
- console.error("Error deleting offer:", err);
1014
+ return c.json({ success: true }, 200);
1015
+ } catch (err2) {
1016
+ console.error("Error deleting offer:", err2);
460
1017
  return c.json({ error: "Internal server error" }, 500);
461
1018
  }
462
1019
  });
463
1020
  app.post("/offers/:offerId/answer", authMiddleware, async (c) => {
464
1021
  try {
465
1022
  const offerId = c.req.param("offerId");
466
- const peerId = getAuthenticatedPeerId(c);
467
1023
  const body = await c.req.json();
468
1024
  const { sdp, secret } = body;
469
- if (!sdp || typeof sdp !== "string") {
470
- return c.json({ error: "Missing or invalid required parameter: sdp" }, 400);
1025
+ if (!sdp) {
1026
+ return c.json({ error: "Missing required parameter: sdp" }, 400);
471
1027
  }
472
- if (sdp.length > 65536) {
473
- return c.json({ error: "SDP must be 64KB or less" }, 400);
1028
+ if (typeof sdp !== "string" || sdp.length === 0) {
1029
+ return c.json({ error: "Invalid SDP" }, 400);
474
1030
  }
475
- if (secret !== void 0 && typeof secret !== "string") {
476
- return c.json({ error: "Secret must be a string" }, 400);
1031
+ if (sdp.length > 64 * 1024) {
1032
+ return c.json({ error: "SDP too large (max 64KB)" }, 400);
477
1033
  }
478
- const result = await storage.answerOffer(offerId, peerId, sdp, secret);
1034
+ const answererPeerId = getAuthenticatedPeerId(c);
1035
+ const result = await storage.answerOffer(offerId, answererPeerId, sdp, secret);
479
1036
  if (!result.success) {
480
1037
  return c.json({ error: result.error }, 400);
481
1038
  }
482
- return c.json({
483
- offerId,
484
- answererId: peerId,
485
- answeredAt: Date.now()
486
- }, 200);
487
- } catch (err) {
488
- console.error("Error answering offer:", err);
1039
+ return c.json({ success: true }, 200);
1040
+ } catch (err2) {
1041
+ console.error("Error answering offer:", err2);
489
1042
  return c.json({ error: "Internal server error" }, 500);
490
1043
  }
491
1044
  });
@@ -494,83 +1047,59 @@ function createApp(storage, config) {
494
1047
  const peerId = getAuthenticatedPeerId(c);
495
1048
  const offers = await storage.getAnsweredOffers(peerId);
496
1049
  return c.json({
497
- answers: offers.map((o) => ({
498
- offerId: o.id,
499
- answererId: o.answererPeerId,
500
- sdp: o.answerSdp,
501
- answeredAt: o.answeredAt,
502
- topics: o.topics
1050
+ answers: offers.map((offer) => ({
1051
+ offerId: offer.id,
1052
+ answererPeerId: offer.answererPeerId,
1053
+ answerSdp: offer.answerSdp,
1054
+ answeredAt: offer.answeredAt
503
1055
  }))
504
1056
  }, 200);
505
- } catch (err) {
506
- console.error("Error fetching answers:", err);
1057
+ } catch (err2) {
1058
+ console.error("Error getting answers:", err2);
507
1059
  return c.json({ error: "Internal server error" }, 500);
508
1060
  }
509
1061
  });
510
1062
  app.post("/offers/:offerId/ice-candidates", authMiddleware, async (c) => {
511
1063
  try {
512
1064
  const offerId = c.req.param("offerId");
513
- const peerId = getAuthenticatedPeerId(c);
514
1065
  const body = await c.req.json();
515
1066
  const { candidates } = body;
516
1067
  if (!Array.isArray(candidates) || candidates.length === 0) {
517
- return c.json({ error: "Missing or invalid required parameter: candidates (must be non-empty array)" }, 400);
1068
+ return c.json({ error: "Missing or invalid required parameter: candidates" }, 400);
518
1069
  }
1070
+ const peerId = getAuthenticatedPeerId(c);
519
1071
  const offer = await storage.getOfferById(offerId);
520
1072
  if (!offer) {
521
- return c.json({ error: "Offer not found or expired" }, 404);
522
- }
523
- let role;
524
- if (offer.peerId === peerId) {
525
- role = "offerer";
526
- } else if (offer.answererPeerId === peerId) {
527
- role = "answerer";
528
- } else {
529
- return c.json({ error: "Not authorized to post ICE candidates for this offer" }, 403);
1073
+ return c.json({ error: "Offer not found" }, 404);
530
1074
  }
531
- const added = await storage.addIceCandidates(offerId, peerId, role, candidates);
532
- return c.json({
533
- offerId,
534
- candidatesAdded: added
535
- }, 200);
536
- } catch (err) {
537
- console.error("Error adding ICE candidates:", err);
1075
+ const role = offer.peerId === peerId ? "offerer" : "answerer";
1076
+ const count = await storage.addIceCandidates(offerId, peerId, role, candidates);
1077
+ return c.json({ count }, 200);
1078
+ } catch (err2) {
1079
+ console.error("Error adding ICE candidates:", err2);
538
1080
  return c.json({ error: "Internal server error" }, 500);
539
1081
  }
540
1082
  });
541
1083
  app.get("/offers/:offerId/ice-candidates", authMiddleware, async (c) => {
542
1084
  try {
543
1085
  const offerId = c.req.param("offerId");
1086
+ const since = c.req.query("since");
544
1087
  const peerId = getAuthenticatedPeerId(c);
545
- const sinceParam = c.req.query("since");
546
- const since = sinceParam ? parseInt(sinceParam, 10) : void 0;
547
1088
  const offer = await storage.getOfferById(offerId);
548
1089
  if (!offer) {
549
- return c.json({ error: "Offer not found or expired" }, 404);
550
- }
551
- let targetRole;
552
- if (offer.peerId === peerId) {
553
- targetRole = "answerer";
554
- console.log(`[ICE GET] Offerer ${peerId} requesting answerer ICE candidates for offer ${offerId}, since=${since}, answererPeerId=${offer.answererPeerId}`);
555
- } else if (offer.answererPeerId === peerId) {
556
- targetRole = "offerer";
557
- console.log(`[ICE GET] Answerer ${peerId} requesting offerer ICE candidates for offer ${offerId}, since=${since}, offererPeerId=${offer.peerId}`);
558
- } else {
559
- return c.json({ error: "Not authorized to view ICE candidates for this offer" }, 403);
560
- }
561
- const candidates = await storage.getIceCandidates(offerId, targetRole, since);
562
- console.log(`[ICE GET] Found ${candidates.length} candidates for offer ${offerId}, targetRole=${targetRole}, since=${since}`);
1090
+ return c.json({ error: "Offer not found" }, 404);
1091
+ }
1092
+ const targetRole = offer.peerId === peerId ? "answerer" : "offerer";
1093
+ const sinceTimestamp = since ? parseInt(since, 10) : void 0;
1094
+ const candidates = await storage.getIceCandidates(offerId, targetRole, sinceTimestamp);
563
1095
  return c.json({
564
- offerId,
565
1096
  candidates: candidates.map((c2) => ({
566
1097
  candidate: c2.candidate,
567
- peerId: c2.peerId,
568
- role: c2.role,
569
1098
  createdAt: c2.createdAt
570
1099
  }))
571
1100
  }, 200);
572
- } catch (err) {
573
- console.error("Error fetching ICE candidates:", err);
1101
+ } catch (err2) {
1102
+ console.error("Error getting ICE candidates:", err2);
574
1103
  return c.json({ error: "Internal server error" }, 500);
575
1104
  }
576
1105
  });
@@ -604,6 +1133,7 @@ function loadConfig() {
604
1133
 
605
1134
  // src/storage/sqlite.ts
606
1135
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
1136
+ var import_crypto4 = require("crypto");
607
1137
 
608
1138
  // src/storage/hash-id.ts
609
1139
  async function generateOfferHash(sdp, topics) {
@@ -622,6 +1152,7 @@ async function generateOfferHash(sdp, topics) {
622
1152
  }
623
1153
 
624
1154
  // src/storage/sqlite.ts
1155
+ var YEAR_IN_MS = 365 * 24 * 60 * 60 * 1e3;
625
1156
  var SQLiteStorage = class {
626
1157
  /**
627
1158
  * Creates a new SQLite storage instance
@@ -632,10 +1163,11 @@ var SQLiteStorage = class {
632
1163
  this.initializeDatabase();
633
1164
  }
634
1165
  /**
635
- * Initializes database schema with new topic-based structure
1166
+ * Initializes database schema with username and service-based structure
636
1167
  */
637
1168
  initializeDatabase() {
638
1169
  this.db.exec(`
1170
+ -- Offers table (no topics)
639
1171
  CREATE TABLE IF NOT EXISTS offers (
640
1172
  id TEXT PRIMARY KEY,
641
1173
  peer_id TEXT NOT NULL,
@@ -654,22 +1186,13 @@ var SQLiteStorage = class {
654
1186
  CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
655
1187
  CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_peer_id);
656
1188
 
657
- CREATE TABLE IF NOT EXISTS offer_topics (
658
- offer_id TEXT NOT NULL,
659
- topic TEXT NOT NULL,
660
- PRIMARY KEY (offer_id, topic),
661
- FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
662
- );
663
-
664
- CREATE INDEX IF NOT EXISTS idx_topics_topic ON offer_topics(topic);
665
- CREATE INDEX IF NOT EXISTS idx_topics_offer ON offer_topics(offer_id);
666
-
1189
+ -- ICE candidates table
667
1190
  CREATE TABLE IF NOT EXISTS ice_candidates (
668
1191
  id INTEGER PRIMARY KEY AUTOINCREMENT,
669
1192
  offer_id TEXT NOT NULL,
670
1193
  peer_id TEXT NOT NULL,
671
1194
  role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
672
- candidate TEXT NOT NULL, -- JSON: RTCIceCandidateInit object
1195
+ candidate TEXT NOT NULL,
673
1196
  created_at INTEGER NOT NULL,
674
1197
  FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
675
1198
  );
@@ -677,15 +1200,64 @@ var SQLiteStorage = class {
677
1200
  CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);
678
1201
  CREATE INDEX IF NOT EXISTS idx_ice_peer ON ice_candidates(peer_id);
679
1202
  CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);
1203
+
1204
+ -- Usernames table
1205
+ CREATE TABLE IF NOT EXISTS usernames (
1206
+ username TEXT PRIMARY KEY,
1207
+ public_key TEXT NOT NULL UNIQUE,
1208
+ claimed_at INTEGER NOT NULL,
1209
+ expires_at INTEGER NOT NULL,
1210
+ last_used INTEGER NOT NULL,
1211
+ metadata TEXT,
1212
+ CHECK(length(username) >= 3 AND length(username) <= 32)
1213
+ );
1214
+
1215
+ CREATE INDEX IF NOT EXISTS idx_usernames_expires ON usernames(expires_at);
1216
+ CREATE INDEX IF NOT EXISTS idx_usernames_public_key ON usernames(public_key);
1217
+
1218
+ -- Services table
1219
+ CREATE TABLE IF NOT EXISTS services (
1220
+ id TEXT PRIMARY KEY,
1221
+ username TEXT NOT NULL,
1222
+ service_fqn TEXT NOT NULL,
1223
+ offer_id TEXT NOT NULL,
1224
+ created_at INTEGER NOT NULL,
1225
+ expires_at INTEGER NOT NULL,
1226
+ is_public INTEGER NOT NULL DEFAULT 0,
1227
+ metadata TEXT,
1228
+ FOREIGN KEY (username) REFERENCES usernames(username) ON DELETE CASCADE,
1229
+ FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE,
1230
+ UNIQUE(username, service_fqn)
1231
+ );
1232
+
1233
+ CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
1234
+ CREATE INDEX IF NOT EXISTS idx_services_fqn ON services(service_fqn);
1235
+ CREATE INDEX IF NOT EXISTS idx_services_expires ON services(expires_at);
1236
+ CREATE INDEX IF NOT EXISTS idx_services_offer ON services(offer_id);
1237
+
1238
+ -- Service index table (privacy layer)
1239
+ CREATE TABLE IF NOT EXISTS service_index (
1240
+ uuid TEXT PRIMARY KEY,
1241
+ service_id TEXT NOT NULL,
1242
+ username TEXT NOT NULL,
1243
+ service_fqn TEXT NOT NULL,
1244
+ created_at INTEGER NOT NULL,
1245
+ expires_at INTEGER NOT NULL,
1246
+ FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
1247
+ );
1248
+
1249
+ CREATE INDEX IF NOT EXISTS idx_service_index_username ON service_index(username);
1250
+ CREATE INDEX IF NOT EXISTS idx_service_index_expires ON service_index(expires_at);
680
1251
  `);
681
1252
  this.db.pragma("foreign_keys = ON");
682
1253
  }
1254
+ // ===== Offer Management =====
683
1255
  async createOffers(offers) {
684
1256
  const created = [];
685
1257
  const offersWithIds = await Promise.all(
686
1258
  offers.map(async (offer) => ({
687
1259
  ...offer,
688
- id: offer.id || await generateOfferHash(offer.sdp, offer.topics)
1260
+ id: offer.id || await generateOfferHash(offer.sdp, [])
689
1261
  }))
690
1262
  );
691
1263
  const transaction = this.db.transaction((offersWithIds2) => {
@@ -693,10 +1265,6 @@ var SQLiteStorage = class {
693
1265
  INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen, secret)
694
1266
  VALUES (?, ?, ?, ?, ?, ?, ?)
695
1267
  `);
696
- const topicStmt = this.db.prepare(`
697
- INSERT INTO offer_topics (offer_id, topic)
698
- VALUES (?, ?)
699
- `);
700
1268
  for (const offer of offersWithIds2) {
701
1269
  const now = Date.now();
702
1270
  offerStmt.run(
@@ -708,14 +1276,10 @@ var SQLiteStorage = class {
708
1276
  now,
709
1277
  offer.secret || null
710
1278
  );
711
- for (const topic of offer.topics) {
712
- topicStmt.run(offer.id, topic);
713
- }
714
1279
  created.push({
715
1280
  id: offer.id,
716
1281
  peerId: offer.peerId,
717
1282
  sdp: offer.sdp,
718
- topics: offer.topics,
719
1283
  createdAt: now,
720
1284
  expiresAt: offer.expiresAt,
721
1285
  lastSeen: now,
@@ -726,24 +1290,6 @@ var SQLiteStorage = class {
726
1290
  transaction(offersWithIds);
727
1291
  return created;
728
1292
  }
729
- async getOffersByTopic(topic, excludePeerIds) {
730
- let query = `
731
- SELECT DISTINCT o.*
732
- FROM offers o
733
- INNER JOIN offer_topics ot ON o.id = ot.offer_id
734
- WHERE ot.topic = ? AND o.expires_at > ?
735
- `;
736
- const params = [topic, Date.now()];
737
- if (excludePeerIds && excludePeerIds.length > 0) {
738
- const placeholders = excludePeerIds.map(() => "?").join(",");
739
- query += ` AND o.peer_id NOT IN (${placeholders})`;
740
- params.push(...excludePeerIds);
741
- }
742
- query += " ORDER BY o.last_seen DESC";
743
- const stmt = this.db.prepare(query);
744
- const rows = stmt.all(...params);
745
- return Promise.all(rows.map((row) => this.rowToOffer(row)));
746
- }
747
1293
  async getOffersByPeerId(peerId) {
748
1294
  const stmt = this.db.prepare(`
749
1295
  SELECT * FROM offers
@@ -751,7 +1297,7 @@ var SQLiteStorage = class {
751
1297
  ORDER BY last_seen DESC
752
1298
  `);
753
1299
  const rows = stmt.all(peerId, Date.now());
754
- return Promise.all(rows.map((row) => this.rowToOffer(row)));
1300
+ return rows.map((row) => this.rowToOffer(row));
755
1301
  }
756
1302
  async getOfferById(offerId) {
757
1303
  const stmt = this.db.prepare(`
@@ -818,8 +1364,9 @@ var SQLiteStorage = class {
818
1364
  ORDER BY answered_at DESC
819
1365
  `);
820
1366
  const rows = stmt.all(offererPeerId, Date.now());
821
- return Promise.all(rows.map((row) => this.rowToOffer(row)));
1367
+ return rows.map((row) => this.rowToOffer(row));
822
1368
  }
1369
+ // ===== ICE Candidate Management =====
823
1370
  async addIceCandidates(offerId, peerId, role, candidates) {
824
1371
  const stmt = this.db.prepare(`
825
1372
  INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, created_at)
@@ -833,9 +1380,7 @@ var SQLiteStorage = class {
833
1380
  peerId,
834
1381
  role,
835
1382
  JSON.stringify(candidates2[i]),
836
- // Store full object as JSON
837
1383
  baseTimestamp + i
838
- // Ensure unique timestamps to avoid "since" filtering issues
839
1384
  );
840
1385
  }
841
1386
  });
@@ -861,61 +1406,198 @@ var SQLiteStorage = class {
861
1406
  peerId: row.peer_id,
862
1407
  role: row.role,
863
1408
  candidate: JSON.parse(row.candidate),
864
- // Parse JSON back to object
865
1409
  createdAt: row.created_at
866
1410
  }));
867
1411
  }
868
- async getTopics(limit, offset, startsWith) {
1412
+ // ===== Username Management =====
1413
+ async claimUsername(request) {
869
1414
  const now = Date.now();
870
- const whereClause = startsWith ? "o.expires_at > ? AND ot.topic LIKE ?" : "o.expires_at > ?";
871
- const startsWithPattern = startsWith ? `${startsWith}%` : null;
872
- const countQuery = `
873
- SELECT COUNT(DISTINCT ot.topic) as count
874
- FROM offer_topics ot
875
- INNER JOIN offers o ON ot.offer_id = o.id
876
- WHERE ${whereClause}
877
- `;
878
- const countStmt = this.db.prepare(countQuery);
879
- const countParams = startsWith ? [now, startsWithPattern] : [now];
880
- const countRow = countStmt.get(...countParams);
881
- const total = countRow.count;
882
- const topicsQuery = `
883
- SELECT
884
- ot.topic,
885
- COUNT(DISTINCT o.peer_id) as active_peers
886
- FROM offer_topics ot
887
- INNER JOIN offers o ON ot.offer_id = o.id
888
- WHERE ${whereClause}
889
- GROUP BY ot.topic
890
- ORDER BY active_peers DESC, ot.topic ASC
891
- LIMIT ? OFFSET ?
892
- `;
893
- const topicsStmt = this.db.prepare(topicsQuery);
894
- const topicsParams = startsWith ? [now, startsWithPattern, limit, offset] : [now, limit, offset];
895
- const rows = topicsStmt.all(...topicsParams);
896
- const topics = rows.map((row) => ({
897
- topic: row.topic,
898
- activePeers: row.active_peers
1415
+ const expiresAt = now + YEAR_IN_MS;
1416
+ const stmt = this.db.prepare(`
1417
+ INSERT INTO usernames (username, public_key, claimed_at, expires_at, last_used, metadata)
1418
+ VALUES (?, ?, ?, ?, ?, NULL)
1419
+ ON CONFLICT(username) DO UPDATE SET
1420
+ expires_at = ?,
1421
+ last_used = ?
1422
+ WHERE public_key = ?
1423
+ `);
1424
+ const result = stmt.run(
1425
+ request.username,
1426
+ request.publicKey,
1427
+ now,
1428
+ expiresAt,
1429
+ now,
1430
+ expiresAt,
1431
+ now,
1432
+ request.publicKey
1433
+ );
1434
+ if (result.changes === 0) {
1435
+ throw new Error("Username already claimed by different public key");
1436
+ }
1437
+ return {
1438
+ username: request.username,
1439
+ publicKey: request.publicKey,
1440
+ claimedAt: now,
1441
+ expiresAt,
1442
+ lastUsed: now
1443
+ };
1444
+ }
1445
+ async getUsername(username) {
1446
+ const stmt = this.db.prepare(`
1447
+ SELECT * FROM usernames
1448
+ WHERE username = ? AND expires_at > ?
1449
+ `);
1450
+ const row = stmt.get(username, Date.now());
1451
+ if (!row) {
1452
+ return null;
1453
+ }
1454
+ return {
1455
+ username: row.username,
1456
+ publicKey: row.public_key,
1457
+ claimedAt: row.claimed_at,
1458
+ expiresAt: row.expires_at,
1459
+ lastUsed: row.last_used,
1460
+ metadata: row.metadata || void 0
1461
+ };
1462
+ }
1463
+ async touchUsername(username) {
1464
+ const now = Date.now();
1465
+ const expiresAt = now + YEAR_IN_MS;
1466
+ const stmt = this.db.prepare(`
1467
+ UPDATE usernames
1468
+ SET last_used = ?, expires_at = ?
1469
+ WHERE username = ? AND expires_at > ?
1470
+ `);
1471
+ const result = stmt.run(now, expiresAt, username, now);
1472
+ return result.changes > 0;
1473
+ }
1474
+ async deleteExpiredUsernames(now) {
1475
+ const stmt = this.db.prepare("DELETE FROM usernames WHERE expires_at < ?");
1476
+ const result = stmt.run(now);
1477
+ return result.changes;
1478
+ }
1479
+ // ===== Service Management =====
1480
+ async createService(request) {
1481
+ const serviceId = (0, import_crypto4.randomUUID)();
1482
+ const indexUuid = (0, import_crypto4.randomUUID)();
1483
+ const now = Date.now();
1484
+ const transaction = this.db.transaction(() => {
1485
+ const serviceStmt = this.db.prepare(`
1486
+ INSERT INTO services (id, username, service_fqn, offer_id, created_at, expires_at, is_public, metadata)
1487
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1488
+ `);
1489
+ serviceStmt.run(
1490
+ serviceId,
1491
+ request.username,
1492
+ request.serviceFqn,
1493
+ request.offerId,
1494
+ now,
1495
+ request.expiresAt,
1496
+ request.isPublic ? 1 : 0,
1497
+ request.metadata || null
1498
+ );
1499
+ const indexStmt = this.db.prepare(`
1500
+ INSERT INTO service_index (uuid, service_id, username, service_fqn, created_at, expires_at)
1501
+ VALUES (?, ?, ?, ?, ?, ?)
1502
+ `);
1503
+ indexStmt.run(
1504
+ indexUuid,
1505
+ serviceId,
1506
+ request.username,
1507
+ request.serviceFqn,
1508
+ now,
1509
+ request.expiresAt
1510
+ );
1511
+ this.touchUsername(request.username);
1512
+ });
1513
+ transaction();
1514
+ return {
1515
+ service: {
1516
+ id: serviceId,
1517
+ username: request.username,
1518
+ serviceFqn: request.serviceFqn,
1519
+ offerId: request.offerId,
1520
+ createdAt: now,
1521
+ expiresAt: request.expiresAt,
1522
+ isPublic: request.isPublic || false,
1523
+ metadata: request.metadata
1524
+ },
1525
+ indexUuid
1526
+ };
1527
+ }
1528
+ async getServiceById(serviceId) {
1529
+ const stmt = this.db.prepare(`
1530
+ SELECT * FROM services
1531
+ WHERE id = ? AND expires_at > ?
1532
+ `);
1533
+ const row = stmt.get(serviceId, Date.now());
1534
+ if (!row) {
1535
+ return null;
1536
+ }
1537
+ return this.rowToService(row);
1538
+ }
1539
+ async getServiceByUuid(uuid) {
1540
+ const stmt = this.db.prepare(`
1541
+ SELECT s.* FROM services s
1542
+ INNER JOIN service_index si ON s.id = si.service_id
1543
+ WHERE si.uuid = ? AND s.expires_at > ?
1544
+ `);
1545
+ const row = stmt.get(uuid, Date.now());
1546
+ if (!row) {
1547
+ return null;
1548
+ }
1549
+ return this.rowToService(row);
1550
+ }
1551
+ async listServicesForUsername(username) {
1552
+ const stmt = this.db.prepare(`
1553
+ SELECT si.uuid, s.is_public, s.service_fqn, s.metadata
1554
+ FROM service_index si
1555
+ INNER JOIN services s ON si.service_id = s.id
1556
+ WHERE si.username = ? AND si.expires_at > ?
1557
+ ORDER BY s.created_at DESC
1558
+ `);
1559
+ const rows = stmt.all(username, Date.now());
1560
+ return rows.map((row) => ({
1561
+ uuid: row.uuid,
1562
+ isPublic: row.is_public === 1,
1563
+ serviceFqn: row.is_public === 1 ? row.service_fqn : void 0,
1564
+ metadata: row.is_public === 1 ? row.metadata || void 0 : void 0
899
1565
  }));
900
- return { topics, total };
1566
+ }
1567
+ async queryService(username, serviceFqn) {
1568
+ const stmt = this.db.prepare(`
1569
+ SELECT si.uuid FROM service_index si
1570
+ INNER JOIN services s ON si.service_id = s.id
1571
+ WHERE si.username = ? AND si.service_fqn = ? AND si.expires_at > ?
1572
+ `);
1573
+ const row = stmt.get(username, serviceFqn, Date.now());
1574
+ return row ? row.uuid : null;
1575
+ }
1576
+ async deleteService(serviceId, username) {
1577
+ const stmt = this.db.prepare(`
1578
+ DELETE FROM services
1579
+ WHERE id = ? AND username = ?
1580
+ `);
1581
+ const result = stmt.run(serviceId, username);
1582
+ return result.changes > 0;
1583
+ }
1584
+ async deleteExpiredServices(now) {
1585
+ const stmt = this.db.prepare("DELETE FROM services WHERE expires_at < ?");
1586
+ const result = stmt.run(now);
1587
+ return result.changes;
901
1588
  }
902
1589
  async close() {
903
1590
  this.db.close();
904
1591
  }
1592
+ // ===== Helper Methods =====
905
1593
  /**
906
- * Helper method to convert database row to Offer object with topics
1594
+ * Helper method to convert database row to Offer object
907
1595
  */
908
- async rowToOffer(row) {
909
- const topicStmt = this.db.prepare(`
910
- SELECT topic FROM offer_topics WHERE offer_id = ?
911
- `);
912
- const topicRows = topicStmt.all(row.id);
913
- const topics = topicRows.map((t) => t.topic);
1596
+ rowToOffer(row) {
914
1597
  return {
915
1598
  id: row.id,
916
1599
  peerId: row.peer_id,
917
1600
  sdp: row.sdp,
918
- topics,
919
1601
  createdAt: row.created_at,
920
1602
  expiresAt: row.expires_at,
921
1603
  lastSeen: row.last_seen,
@@ -925,6 +1607,21 @@ var SQLiteStorage = class {
925
1607
  answeredAt: row.answered_at || void 0
926
1608
  };
927
1609
  }
1610
+ /**
1611
+ * Helper method to convert database row to Service object
1612
+ */
1613
+ rowToService(row) {
1614
+ return {
1615
+ id: row.id,
1616
+ username: row.username,
1617
+ serviceFqn: row.service_fqn,
1618
+ offerId: row.offer_id,
1619
+ createdAt: row.created_at,
1620
+ expiresAt: row.expires_at,
1621
+ isPublic: row.is_public === 1,
1622
+ metadata: row.metadata || void 0
1623
+ };
1624
+ }
928
1625
  };
929
1626
 
930
1627
  // src/index.ts
@@ -958,8 +1655,8 @@ async function main() {
958
1655
  if (deleted > 0) {
959
1656
  console.log(`Cleanup: Deleted ${deleted} expired offer(s)`);
960
1657
  }
961
- } catch (err) {
962
- console.error("Cleanup error:", err);
1658
+ } catch (err2) {
1659
+ console.error("Cleanup error:", err2);
963
1660
  }
964
1661
  }, config.cleanupInterval);
965
1662
  const app = createApp(storage, config);
@@ -978,8 +1675,13 @@ async function main() {
978
1675
  process.on("SIGINT", shutdown);
979
1676
  process.on("SIGTERM", shutdown);
980
1677
  }
981
- main().catch((err) => {
982
- console.error("Fatal error:", err);
1678
+ main().catch((err2) => {
1679
+ console.error("Fatal error:", err2);
983
1680
  process.exit(1);
984
1681
  });
1682
+ /*! Bundled license information:
1683
+
1684
+ @noble/ed25519/index.js:
1685
+ (*! noble-ed25519 - MIT License (c) 2019 Paul Miller (paulmillr.com) *)
1686
+ */
985
1687
  //# sourceMappingURL=index.js.map