@xtr-dev/rondevu-server 0.1.4 → 0.2.0

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,464 @@ 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
33
483
  var ALGORITHM = "AES-GCM";
34
484
  var IV_LENGTH = 12;
35
485
  var KEY_LENGTH = 32;
486
+ var USERNAME_REGEX = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
487
+ var USERNAME_MIN_LENGTH = 3;
488
+ var USERNAME_MAX_LENGTH = 32;
489
+ var TIMESTAMP_TOLERANCE_MS = 5 * 60 * 1e3;
36
490
  function generatePeerId() {
37
491
  const bytes = crypto.getRandomValues(new Uint8Array(16));
38
492
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
@@ -41,7 +495,7 @@ function generateSecretKey() {
41
495
  const bytes = crypto.getRandomValues(new Uint8Array(KEY_LENGTH));
42
496
  return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
43
497
  }
44
- function hexToBytes(hex) {
498
+ function hexToBytes2(hex) {
45
499
  const bytes = new Uint8Array(hex.length / 2);
46
500
  for (let i = 0; i < hex.length; i += 2) {
47
501
  bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16);
@@ -60,7 +514,7 @@ function base64ToBytes(base64) {
60
514
  return Uint8Array.from(binString, (char) => char.codePointAt(0));
61
515
  }
62
516
  async function encryptPeerId(peerId, secretKeyHex) {
63
- const keyBytes = hexToBytes(secretKeyHex);
517
+ const keyBytes = hexToBytes2(secretKeyHex);
64
518
  if (keyBytes.length !== KEY_LENGTH) {
65
519
  throw new Error(`Secret key must be ${KEY_LENGTH * 2} hex characters (${KEY_LENGTH} bytes)`);
66
520
  }
@@ -86,7 +540,7 @@ async function encryptPeerId(peerId, secretKeyHex) {
86
540
  }
87
541
  async function decryptPeerId(encryptedSecret, secretKeyHex) {
88
542
  try {
89
- const keyBytes = hexToBytes(secretKeyHex);
543
+ const keyBytes = hexToBytes2(secretKeyHex);
90
544
  if (keyBytes.length !== KEY_LENGTH) {
91
545
  throw new Error(`Secret key must be ${KEY_LENGTH * 2} hex characters (${KEY_LENGTH} bytes)`);
92
546
  }
@@ -107,7 +561,7 @@ async function decryptPeerId(encryptedSecret, secretKeyHex) {
107
561
  );
108
562
  const decoder = new TextDecoder();
109
563
  return decoder.decode(decrypted);
110
- } catch (err) {
564
+ } catch (err2) {
111
565
  throw new Error("Failed to decrypt peer ID: invalid secret or secret key");
112
566
  }
113
567
  }
@@ -119,6 +573,90 @@ async function validateCredentials(peerId, encryptedSecret, secretKey) {
119
573
  return false;
120
574
  }
121
575
  }
576
+ function validateUsername(username) {
577
+ if (typeof username !== "string") {
578
+ return { valid: false, error: "Username must be a string" };
579
+ }
580
+ if (username.length < USERNAME_MIN_LENGTH) {
581
+ return { valid: false, error: `Username must be at least ${USERNAME_MIN_LENGTH} characters` };
582
+ }
583
+ if (username.length > USERNAME_MAX_LENGTH) {
584
+ return { valid: false, error: `Username must be at most ${USERNAME_MAX_LENGTH} characters` };
585
+ }
586
+ if (!USERNAME_REGEX.test(username)) {
587
+ return { valid: false, error: "Username must be lowercase alphanumeric with optional dashes, and start/end with alphanumeric" };
588
+ }
589
+ return { valid: true };
590
+ }
591
+ function validateServiceFqn(fqn) {
592
+ if (typeof fqn !== "string") {
593
+ return { valid: false, error: "Service FQN must be a string" };
594
+ }
595
+ const parts = fqn.split("@");
596
+ if (parts.length !== 2) {
597
+ return { valid: false, error: "Service FQN must be in format: service-name@version" };
598
+ }
599
+ const [serviceName, version] = parts;
600
+ const serviceNameRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/;
601
+ if (!serviceNameRegex.test(serviceName)) {
602
+ return { valid: false, error: "Service name must be reverse domain notation (e.g., com.example.service)" };
603
+ }
604
+ if (serviceName.length < 3 || serviceName.length > 128) {
605
+ return { valid: false, error: "Service name must be 3-128 characters" };
606
+ }
607
+ const versionRegex = /^[0-9]+\.[0-9]+\.[0-9]+(-[a-z0-9.-]+)?$/;
608
+ if (!versionRegex.test(version)) {
609
+ return { valid: false, error: "Version must be semantic versioning (e.g., 1.0.0, 2.1.3-beta)" };
610
+ }
611
+ return { valid: true };
612
+ }
613
+ function validateTimestamp(timestamp) {
614
+ if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
615
+ return { valid: false, error: "Timestamp must be a finite number" };
616
+ }
617
+ const now = Date.now();
618
+ const diff = Math.abs(now - timestamp);
619
+ if (diff > TIMESTAMP_TOLERANCE_MS) {
620
+ return { valid: false, error: `Timestamp too old or too far in future (tolerance: ${TIMESTAMP_TOLERANCE_MS / 1e3}s)` };
621
+ }
622
+ return { valid: true };
623
+ }
624
+ async function verifyEd25519Signature(publicKey, signature, message) {
625
+ try {
626
+ const publicKeyBytes = base64ToBytes(publicKey);
627
+ const signatureBytes = base64ToBytes(signature);
628
+ const encoder = new TextEncoder();
629
+ const messageBytes = encoder.encode(message);
630
+ const isValid = await verify(signatureBytes, messageBytes, publicKeyBytes);
631
+ return isValid;
632
+ } catch (err2) {
633
+ console.error("Ed25519 signature verification failed:", err2);
634
+ return false;
635
+ }
636
+ }
637
+ async function validateUsernameClaim(username, publicKey, signature, message) {
638
+ const usernameCheck = validateUsername(username);
639
+ if (!usernameCheck.valid) {
640
+ return usernameCheck;
641
+ }
642
+ const parts = message.split(":");
643
+ if (parts.length !== 3 || parts[0] !== "claim" || parts[1] !== username) {
644
+ return { valid: false, error: "Invalid message format (expected: claim:{username}:{timestamp})" };
645
+ }
646
+ const timestamp = parseInt(parts[2], 10);
647
+ if (isNaN(timestamp)) {
648
+ return { valid: false, error: "Invalid timestamp in message" };
649
+ }
650
+ const timestampCheck = validateTimestamp(timestamp);
651
+ if (!timestampCheck.valid) {
652
+ return timestampCheck;
653
+ }
654
+ const signatureValid = await verifyEd25519Signature(publicKey, signature, message);
655
+ if (!signatureValid) {
656
+ return { valid: false, error: "Invalid signature" };
657
+ }
658
+ return { valid: true };
659
+ }
122
660
 
123
661
  // src/middleware/auth.ts
124
662
  function createAuthMiddleware(authSecret) {
@@ -152,57 +690,6 @@ function getAuthenticatedPeerId(c) {
152
690
  return peerId;
153
691
  }
154
692
 
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
693
  // src/app.ts
207
694
  function createApp(storage, config) {
208
695
  const app = new import_hono.Hono();
@@ -227,7 +714,7 @@ function createApp(storage, config) {
227
714
  return c.json({
228
715
  version: config.version,
229
716
  name: "Rondevu",
230
- description: "Topic-based peer discovery and signaling server"
717
+ description: "DNS-like WebRTC signaling with username claiming and service discovery"
231
718
  });
232
719
  });
233
720
  app.get("/health", (c) => {
@@ -239,227 +726,277 @@ function createApp(storage, config) {
239
726
  });
240
727
  app.post("/register", async (c) => {
241
728
  try {
242
- let peerId;
243
- const body = await c.req.json().catch(() => ({}));
244
- const customPeerId = body.peerId;
245
- if (customPeerId !== void 0) {
246
- if (typeof customPeerId !== "string" || customPeerId.length === 0) {
247
- return c.json({ error: "Peer ID must be a non-empty string" }, 400);
248
- }
249
- if (customPeerId.length > 128) {
250
- return c.json({ error: "Peer ID must be 128 characters or less" }, 400);
251
- }
252
- const existingOffers = await storage.getOffersByPeerId(customPeerId);
253
- if (existingOffers.length > 0) {
254
- return c.json({ error: "Peer ID is already in use" }, 409);
255
- }
256
- peerId = customPeerId;
257
- } else {
258
- peerId = generatePeerId();
259
- }
729
+ const peerId = generatePeerId();
260
730
  const secret = await encryptPeerId(peerId, config.authSecret);
261
731
  return c.json({
262
732
  peerId,
263
733
  secret
264
734
  }, 200);
265
- } catch (err) {
266
- console.error("Error registering peer:", err);
735
+ } catch (err2) {
736
+ console.error("Error registering peer:", err2);
267
737
  return c.json({ error: "Internal server error" }, 500);
268
738
  }
269
739
  });
270
- app.post("/offers", authMiddleware, async (c) => {
740
+ app.post("/usernames/claim", async (c) => {
271
741
  try {
272
742
  const body = await c.req.json();
273
- const { offers } = body;
274
- if (!Array.isArray(offers) || offers.length === 0) {
275
- return c.json({ error: "Missing or invalid required parameter: offers (must be non-empty array)" }, 400);
743
+ const { username, publicKey, signature, message } = body;
744
+ if (!username || !publicKey || !signature || !message) {
745
+ return c.json({ error: "Missing required parameters: username, publicKey, signature, message" }, 400);
276
746
  }
277
- if (offers.length > config.maxOffersPerRequest) {
278
- return c.json({ error: `Too many offers. Maximum ${config.maxOffersPerRequest} per request` }, 400);
747
+ const validation = await validateUsernameClaim(username, publicKey, signature, message);
748
+ if (!validation.valid) {
749
+ return c.json({ error: validation.error }, 400);
279
750
  }
280
- const peerId = getAuthenticatedPeerId(c);
281
- const offerRequests = [];
282
- for (const offer of offers) {
283
- if (!offer.sdp || typeof offer.sdp !== "string") {
284
- return c.json({ error: "Each offer must have an sdp field" }, 400);
285
- }
286
- if (offer.sdp.length > 65536) {
287
- return c.json({ error: "SDP must be 64KB or less" }, 400);
288
- }
289
- if (offer.secret !== void 0) {
290
- if (typeof offer.secret !== "string") {
291
- return c.json({ error: "Secret must be a string" }, 400);
292
- }
293
- if (offer.secret.length > 128) {
294
- return c.json({ error: "Secret must be 128 characters or less" }, 400);
295
- }
296
- }
297
- if (offer.info !== void 0) {
298
- if (typeof offer.info !== "string") {
299
- return c.json({ error: "Info must be a string" }, 400);
300
- }
301
- if (offer.info.length > 128) {
302
- return c.json({ error: "Info must be 128 characters or less" }, 400);
303
- }
304
- }
305
- if (!Array.isArray(offer.topics) || offer.topics.length === 0) {
306
- return c.json({ error: "Each offer must have a non-empty topics array" }, 400);
307
- }
308
- if (offer.topics.length > config.maxTopicsPerOffer) {
309
- return c.json({ error: `Too many topics. Maximum ${config.maxTopicsPerOffer} per offer` }, 400);
310
- }
311
- for (const topic of offer.topics) {
312
- if (typeof topic !== "string" || topic.length === 0 || topic.length > 256) {
313
- return c.json({ error: "Each topic must be a string between 1 and 256 characters" }, 400);
314
- }
315
- }
316
- let ttl = offer.ttl || config.offerDefaultTtl;
317
- if (ttl < config.offerMinTtl) {
318
- ttl = config.offerMinTtl;
319
- }
320
- if (ttl > config.offerMaxTtl) {
321
- ttl = config.offerMaxTtl;
322
- }
323
- offerRequests.push({
324
- id: offer.id,
325
- peerId,
326
- sdp: offer.sdp,
327
- topics: offer.topics,
328
- expiresAt: Date.now() + ttl,
329
- secret: offer.secret,
330
- info: offer.info
751
+ try {
752
+ const claimed = await storage.claimUsername({
753
+ username,
754
+ publicKey,
755
+ signature,
756
+ message
331
757
  });
758
+ return c.json({
759
+ username: claimed.username,
760
+ claimedAt: claimed.claimedAt,
761
+ expiresAt: claimed.expiresAt
762
+ }, 200);
763
+ } catch (err2) {
764
+ if (err2.message?.includes("already claimed")) {
765
+ return c.json({ error: "Username already claimed by different public key" }, 409);
766
+ }
767
+ throw err2;
768
+ }
769
+ } catch (err2) {
770
+ console.error("Error claiming username:", err2);
771
+ return c.json({ error: "Internal server error" }, 500);
772
+ }
773
+ });
774
+ app.get("/usernames/:username", async (c) => {
775
+ try {
776
+ const username = c.req.param("username");
777
+ const claimed = await storage.getUsername(username);
778
+ if (!claimed) {
779
+ return c.json({
780
+ username,
781
+ available: true
782
+ }, 200);
332
783
  }
333
- const createdOffers = await storage.createOffers(offerRequests);
334
784
  return c.json({
335
- offers: createdOffers.map((o) => ({
336
- id: o.id,
337
- peerId: o.peerId,
338
- topics: o.topics,
339
- expiresAt: o.expiresAt
340
- }))
785
+ username: claimed.username,
786
+ available: false,
787
+ claimedAt: claimed.claimedAt,
788
+ expiresAt: claimed.expiresAt,
789
+ publicKey: claimed.publicKey
341
790
  }, 200);
342
- } catch (err) {
343
- console.error("Error creating offers:", err);
791
+ } catch (err2) {
792
+ console.error("Error checking username:", err2);
344
793
  return c.json({ error: "Internal server error" }, 500);
345
794
  }
346
795
  });
347
- app.get("/offers/by-topic/:topic", async (c) => {
796
+ app.get("/usernames/:username/services", async (c) => {
348
797
  try {
349
- const topic = c.req.param("topic");
350
- const bloomParam = c.req.query("bloom");
351
- const limitParam = c.req.query("limit");
352
- const limit = limitParam ? Math.min(parseInt(limitParam, 10), 200) : 50;
353
- let excludePeerIds = [];
354
- if (bloomParam) {
355
- const bloom = parseBloomFilter(bloomParam);
356
- if (!bloom) {
357
- return c.json({ error: "Invalid bloom filter format" }, 400);
358
- }
359
- const allOffers = await storage.getOffersByTopic(topic);
360
- const excludeSet = /* @__PURE__ */ new Set();
361
- for (const offer of allOffers) {
362
- if (bloom.test(offer.peerId)) {
363
- excludeSet.add(offer.peerId);
364
- }
365
- }
366
- excludePeerIds = Array.from(excludeSet);
367
- }
368
- let offers = await storage.getOffersByTopic(topic, excludePeerIds.length > 0 ? excludePeerIds : void 0);
369
- const total = offers.length;
370
- offers = offers.slice(0, limit);
798
+ const username = c.req.param("username");
799
+ const services = await storage.listServicesForUsername(username);
371
800
  return c.json({
372
- topic,
373
- offers: offers.map((o) => ({
374
- id: o.id,
375
- peerId: o.peerId,
376
- sdp: o.sdp,
377
- topics: o.topics,
378
- expiresAt: o.expiresAt,
379
- lastSeen: o.lastSeen,
380
- hasSecret: !!o.secret,
381
- // Indicate if secret is required without exposing it
382
- info: o.info
383
- // Public info field
384
- })),
385
- total: bloomParam ? total + excludePeerIds.length : total,
386
- returned: offers.length
801
+ username,
802
+ services
387
803
  }, 200);
388
- } catch (err) {
389
- console.error("Error fetching offers by topic:", err);
804
+ } catch (err2) {
805
+ console.error("Error listing services:", err2);
390
806
  return c.json({ error: "Internal server error" }, 500);
391
807
  }
392
808
  });
393
- app.get("/topics", async (c) => {
809
+ app.post("/services", authMiddleware, async (c) => {
394
810
  try {
395
- const limitParam = c.req.query("limit");
396
- const offsetParam = c.req.query("offset");
397
- const startsWithParam = c.req.query("startsWith");
398
- const limit = limitParam ? Math.min(parseInt(limitParam, 10), 200) : 50;
399
- const offset = offsetParam ? parseInt(offsetParam, 10) : 0;
400
- const startsWith = startsWithParam || void 0;
401
- const result = await storage.getTopics(limit, offset, startsWith);
811
+ const body = await c.req.json();
812
+ const { username, serviceFqn, sdp, ttl, isPublic, metadata, signature, message } = body;
813
+ if (!username || !serviceFqn || !sdp) {
814
+ return c.json({ error: "Missing required parameters: username, serviceFqn, sdp" }, 400);
815
+ }
816
+ const fqnValidation = validateServiceFqn(serviceFqn);
817
+ if (!fqnValidation.valid) {
818
+ return c.json({ error: fqnValidation.error }, 400);
819
+ }
820
+ if (!signature || !message) {
821
+ return c.json({ error: "Missing signature or message for username verification" }, 400);
822
+ }
823
+ const usernameRecord = await storage.getUsername(username);
824
+ if (!usernameRecord) {
825
+ return c.json({ error: "Username not claimed" }, 404);
826
+ }
827
+ const signatureValidation = await validateUsernameClaim(username, usernameRecord.publicKey, signature, message);
828
+ if (!signatureValidation.valid) {
829
+ return c.json({ error: "Invalid signature for username" }, 403);
830
+ }
831
+ if (typeof sdp !== "string" || sdp.length === 0) {
832
+ return c.json({ error: "Invalid SDP" }, 400);
833
+ }
834
+ if (sdp.length > 64 * 1024) {
835
+ return c.json({ error: "SDP too large (max 64KB)" }, 400);
836
+ }
837
+ const peerId = getAuthenticatedPeerId(c);
838
+ const offerTtl = Math.min(
839
+ Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
840
+ config.offerMaxTtl
841
+ );
842
+ const expiresAt = Date.now() + offerTtl;
843
+ const offers = await storage.createOffers([{
844
+ peerId,
845
+ sdp,
846
+ expiresAt
847
+ }]);
848
+ if (offers.length === 0) {
849
+ return c.json({ error: "Failed to create offer" }, 500);
850
+ }
851
+ const offer = offers[0];
852
+ const result = await storage.createService({
853
+ username,
854
+ serviceFqn,
855
+ offerId: offer.id,
856
+ expiresAt,
857
+ isPublic: isPublic || false,
858
+ metadata: metadata ? JSON.stringify(metadata) : void 0
859
+ });
402
860
  return c.json({
403
- topics: result.topics,
404
- total: result.total,
405
- limit,
406
- offset,
407
- ...startsWith && { startsWith }
861
+ serviceId: result.service.id,
862
+ uuid: result.indexUuid,
863
+ offerId: offer.id,
864
+ expiresAt: result.service.expiresAt
865
+ }, 201);
866
+ } catch (err2) {
867
+ console.error("Error creating service:", err2);
868
+ return c.json({ error: "Internal server error" }, 500);
869
+ }
870
+ });
871
+ app.get("/services/:uuid", async (c) => {
872
+ try {
873
+ const uuid = c.req.param("uuid");
874
+ const service = await storage.getServiceByUuid(uuid);
875
+ if (!service) {
876
+ return c.json({ error: "Service not found" }, 404);
877
+ }
878
+ const offer = await storage.getOfferById(service.offerId);
879
+ if (!offer) {
880
+ return c.json({ error: "Associated offer not found" }, 404);
881
+ }
882
+ return c.json({
883
+ serviceId: service.id,
884
+ username: service.username,
885
+ serviceFqn: service.serviceFqn,
886
+ offerId: service.offerId,
887
+ sdp: offer.sdp,
888
+ isPublic: service.isPublic,
889
+ metadata: service.metadata ? JSON.parse(service.metadata) : void 0,
890
+ createdAt: service.createdAt,
891
+ expiresAt: service.expiresAt
408
892
  }, 200);
409
- } catch (err) {
410
- console.error("Error fetching topics:", err);
893
+ } catch (err2) {
894
+ console.error("Error getting service:", err2);
411
895
  return c.json({ error: "Internal server error" }, 500);
412
896
  }
413
897
  });
414
- app.get("/peers/:peerId/offers", async (c) => {
898
+ app.delete("/services/:serviceId", authMiddleware, async (c) => {
415
899
  try {
416
- const peerId = c.req.param("peerId");
417
- const offers = await storage.getOffersByPeerId(peerId);
418
- const topicsSet = /* @__PURE__ */ new Set();
419
- offers.forEach((o) => o.topics.forEach((t) => topicsSet.add(t)));
900
+ const serviceId = c.req.param("serviceId");
901
+ const body = await c.req.json();
902
+ const { username } = body;
903
+ if (!username) {
904
+ return c.json({ error: "Missing required parameter: username" }, 400);
905
+ }
906
+ const deleted = await storage.deleteService(serviceId, username);
907
+ if (!deleted) {
908
+ return c.json({ error: "Service not found or not owned by this username" }, 404);
909
+ }
910
+ return c.json({ success: true }, 200);
911
+ } catch (err2) {
912
+ console.error("Error deleting service:", err2);
913
+ return c.json({ error: "Internal server error" }, 500);
914
+ }
915
+ });
916
+ app.post("/index/:username/query", async (c) => {
917
+ try {
918
+ const username = c.req.param("username");
919
+ const body = await c.req.json();
920
+ const { serviceFqn } = body;
921
+ if (!serviceFqn) {
922
+ return c.json({ error: "Missing required parameter: serviceFqn" }, 400);
923
+ }
924
+ const uuid = await storage.queryService(username, serviceFqn);
925
+ if (!uuid) {
926
+ return c.json({ error: "Service not found" }, 404);
927
+ }
420
928
  return c.json({
421
- peerId,
422
- offers: offers.map((o) => ({
423
- id: o.id,
424
- sdp: o.sdp,
425
- topics: o.topics,
426
- expiresAt: o.expiresAt,
427
- lastSeen: o.lastSeen,
428
- hasSecret: !!o.secret,
429
- // Indicate if secret is required without exposing it
430
- info: o.info
431
- // Public info field
432
- })),
433
- topics: Array.from(topicsSet)
929
+ uuid,
930
+ allowed: true
434
931
  }, 200);
435
- } catch (err) {
436
- console.error("Error fetching peer offers:", err);
932
+ } catch (err2) {
933
+ console.error("Error querying service:", err2);
437
934
  return c.json({ error: "Internal server error" }, 500);
438
935
  }
439
936
  });
937
+ app.post("/offers", authMiddleware, async (c) => {
938
+ try {
939
+ const body = await c.req.json();
940
+ const { offers } = body;
941
+ if (!Array.isArray(offers) || offers.length === 0) {
942
+ return c.json({ error: "Missing or invalid required parameter: offers (must be non-empty array)" }, 400);
943
+ }
944
+ if (offers.length > config.maxOffersPerRequest) {
945
+ return c.json({ error: `Too many offers (max ${config.maxOffersPerRequest})` }, 400);
946
+ }
947
+ const peerId = getAuthenticatedPeerId(c);
948
+ const validated = offers.map((offer) => {
949
+ const { sdp, ttl, secret } = offer;
950
+ if (typeof sdp !== "string" || sdp.length === 0) {
951
+ throw new Error("Invalid SDP in offer");
952
+ }
953
+ if (sdp.length > 64 * 1024) {
954
+ throw new Error("SDP too large (max 64KB)");
955
+ }
956
+ const offerTtl = Math.min(
957
+ Math.max(ttl || config.offerDefaultTtl, config.offerMinTtl),
958
+ config.offerMaxTtl
959
+ );
960
+ return {
961
+ peerId,
962
+ sdp,
963
+ expiresAt: Date.now() + offerTtl,
964
+ secret: secret ? String(secret).substring(0, 128) : void 0
965
+ };
966
+ });
967
+ const created = await storage.createOffers(validated);
968
+ return c.json({
969
+ offers: created.map((offer) => ({
970
+ id: offer.id,
971
+ peerId: offer.peerId,
972
+ expiresAt: offer.expiresAt,
973
+ createdAt: offer.createdAt,
974
+ hasSecret: !!offer.secret
975
+ }))
976
+ }, 201);
977
+ } catch (err2) {
978
+ console.error("Error creating offers:", err2);
979
+ return c.json({ error: err2.message || "Internal server error" }, 500);
980
+ }
981
+ });
440
982
  app.get("/offers/mine", authMiddleware, async (c) => {
441
983
  try {
442
984
  const peerId = getAuthenticatedPeerId(c);
443
985
  const offers = await storage.getOffersByPeerId(peerId);
444
986
  return c.json({
445
- peerId,
446
- offers: offers.map((o) => ({
447
- id: o.id,
448
- sdp: o.sdp,
449
- topics: o.topics,
450
- createdAt: o.createdAt,
451
- expiresAt: o.expiresAt,
452
- lastSeen: o.lastSeen,
453
- secret: o.secret,
454
- // Owner can see the secret
455
- info: o.info,
456
- // Owner can see the info
457
- answererPeerId: o.answererPeerId,
458
- answeredAt: o.answeredAt
987
+ offers: offers.map((offer) => ({
988
+ id: offer.id,
989
+ sdp: offer.sdp,
990
+ createdAt: offer.createdAt,
991
+ expiresAt: offer.expiresAt,
992
+ lastSeen: offer.lastSeen,
993
+ hasSecret: !!offer.secret,
994
+ answererPeerId: offer.answererPeerId,
995
+ answered: !!offer.answererPeerId
459
996
  }))
460
997
  }, 200);
461
- } catch (err) {
462
- console.error("Error fetching own offers:", err);
998
+ } catch (err2) {
999
+ console.error("Error getting offers:", err2);
463
1000
  return c.json({ error: "Internal server error" }, 500);
464
1001
  }
465
1002
  });
@@ -469,40 +1006,36 @@ function createApp(storage, config) {
469
1006
  const peerId = getAuthenticatedPeerId(c);
470
1007
  const deleted = await storage.deleteOffer(offerId, peerId);
471
1008
  if (!deleted) {
472
- return c.json({ error: "Offer not found or not authorized" }, 404);
1009
+ return c.json({ error: "Offer not found or not owned by this peer" }, 404);
473
1010
  }
474
- return c.json({ deleted: true }, 200);
475
- } catch (err) {
476
- console.error("Error deleting offer:", err);
1011
+ return c.json({ success: true }, 200);
1012
+ } catch (err2) {
1013
+ console.error("Error deleting offer:", err2);
477
1014
  return c.json({ error: "Internal server error" }, 500);
478
1015
  }
479
1016
  });
480
1017
  app.post("/offers/:offerId/answer", authMiddleware, async (c) => {
481
1018
  try {
482
1019
  const offerId = c.req.param("offerId");
483
- const peerId = getAuthenticatedPeerId(c);
484
1020
  const body = await c.req.json();
485
1021
  const { sdp, secret } = body;
486
- if (!sdp || typeof sdp !== "string") {
487
- return c.json({ error: "Missing or invalid required parameter: sdp" }, 400);
1022
+ if (!sdp) {
1023
+ return c.json({ error: "Missing required parameter: sdp" }, 400);
488
1024
  }
489
- if (sdp.length > 65536) {
490
- return c.json({ error: "SDP must be 64KB or less" }, 400);
1025
+ if (typeof sdp !== "string" || sdp.length === 0) {
1026
+ return c.json({ error: "Invalid SDP" }, 400);
491
1027
  }
492
- if (secret !== void 0 && typeof secret !== "string") {
493
- return c.json({ error: "Secret must be a string" }, 400);
1028
+ if (sdp.length > 64 * 1024) {
1029
+ return c.json({ error: "SDP too large (max 64KB)" }, 400);
494
1030
  }
495
- const result = await storage.answerOffer(offerId, peerId, sdp, secret);
1031
+ const answererPeerId = getAuthenticatedPeerId(c);
1032
+ const result = await storage.answerOffer(offerId, answererPeerId, sdp, secret);
496
1033
  if (!result.success) {
497
1034
  return c.json({ error: result.error }, 400);
498
1035
  }
499
- return c.json({
500
- offerId,
501
- answererId: peerId,
502
- answeredAt: Date.now()
503
- }, 200);
504
- } catch (err) {
505
- console.error("Error answering offer:", err);
1036
+ return c.json({ success: true }, 200);
1037
+ } catch (err2) {
1038
+ console.error("Error answering offer:", err2);
506
1039
  return c.json({ error: "Internal server error" }, 500);
507
1040
  }
508
1041
  });
@@ -511,83 +1044,59 @@ function createApp(storage, config) {
511
1044
  const peerId = getAuthenticatedPeerId(c);
512
1045
  const offers = await storage.getAnsweredOffers(peerId);
513
1046
  return c.json({
514
- answers: offers.map((o) => ({
515
- offerId: o.id,
516
- answererId: o.answererPeerId,
517
- sdp: o.answerSdp,
518
- answeredAt: o.answeredAt,
519
- topics: o.topics
1047
+ answers: offers.map((offer) => ({
1048
+ offerId: offer.id,
1049
+ answererPeerId: offer.answererPeerId,
1050
+ answerSdp: offer.answerSdp,
1051
+ answeredAt: offer.answeredAt
520
1052
  }))
521
1053
  }, 200);
522
- } catch (err) {
523
- console.error("Error fetching answers:", err);
1054
+ } catch (err2) {
1055
+ console.error("Error getting answers:", err2);
524
1056
  return c.json({ error: "Internal server error" }, 500);
525
1057
  }
526
1058
  });
527
1059
  app.post("/offers/:offerId/ice-candidates", authMiddleware, async (c) => {
528
1060
  try {
529
1061
  const offerId = c.req.param("offerId");
530
- const peerId = getAuthenticatedPeerId(c);
531
1062
  const body = await c.req.json();
532
1063
  const { candidates } = body;
533
1064
  if (!Array.isArray(candidates) || candidates.length === 0) {
534
- return c.json({ error: "Missing or invalid required parameter: candidates (must be non-empty array)" }, 400);
1065
+ return c.json({ error: "Missing or invalid required parameter: candidates" }, 400);
535
1066
  }
1067
+ const peerId = getAuthenticatedPeerId(c);
536
1068
  const offer = await storage.getOfferById(offerId);
537
1069
  if (!offer) {
538
- return c.json({ error: "Offer not found or expired" }, 404);
539
- }
540
- let role;
541
- if (offer.peerId === peerId) {
542
- role = "offerer";
543
- } else if (offer.answererPeerId === peerId) {
544
- role = "answerer";
545
- } else {
546
- return c.json({ error: "Not authorized to post ICE candidates for this offer" }, 403);
1070
+ return c.json({ error: "Offer not found" }, 404);
547
1071
  }
548
- const added = await storage.addIceCandidates(offerId, peerId, role, candidates);
549
- return c.json({
550
- offerId,
551
- candidatesAdded: added
552
- }, 200);
553
- } catch (err) {
554
- console.error("Error adding ICE candidates:", err);
1072
+ const role = offer.peerId === peerId ? "offerer" : "answerer";
1073
+ const count = await storage.addIceCandidates(offerId, peerId, role, candidates);
1074
+ return c.json({ count }, 200);
1075
+ } catch (err2) {
1076
+ console.error("Error adding ICE candidates:", err2);
555
1077
  return c.json({ error: "Internal server error" }, 500);
556
1078
  }
557
1079
  });
558
1080
  app.get("/offers/:offerId/ice-candidates", authMiddleware, async (c) => {
559
1081
  try {
560
1082
  const offerId = c.req.param("offerId");
1083
+ const since = c.req.query("since");
561
1084
  const peerId = getAuthenticatedPeerId(c);
562
- const sinceParam = c.req.query("since");
563
- const since = sinceParam ? parseInt(sinceParam, 10) : void 0;
564
1085
  const offer = await storage.getOfferById(offerId);
565
1086
  if (!offer) {
566
- return c.json({ error: "Offer not found or expired" }, 404);
567
- }
568
- let targetRole;
569
- if (offer.peerId === peerId) {
570
- targetRole = "answerer";
571
- console.log(`[ICE GET] Offerer ${peerId} requesting answerer ICE candidates for offer ${offerId}, since=${since}, answererPeerId=${offer.answererPeerId}`);
572
- } else if (offer.answererPeerId === peerId) {
573
- targetRole = "offerer";
574
- console.log(`[ICE GET] Answerer ${peerId} requesting offerer ICE candidates for offer ${offerId}, since=${since}, offererPeerId=${offer.peerId}`);
575
- } else {
576
- return c.json({ error: "Not authorized to view ICE candidates for this offer" }, 403);
577
- }
578
- const candidates = await storage.getIceCandidates(offerId, targetRole, since);
579
- console.log(`[ICE GET] Found ${candidates.length} candidates for offer ${offerId}, targetRole=${targetRole}, since=${since}`);
1087
+ return c.json({ error: "Offer not found" }, 404);
1088
+ }
1089
+ const targetRole = offer.peerId === peerId ? "answerer" : "offerer";
1090
+ const sinceTimestamp = since ? parseInt(since, 10) : void 0;
1091
+ const candidates = await storage.getIceCandidates(offerId, targetRole, sinceTimestamp);
580
1092
  return c.json({
581
- offerId,
582
1093
  candidates: candidates.map((c2) => ({
583
1094
  candidate: c2.candidate,
584
- peerId: c2.peerId,
585
- role: c2.role,
586
1095
  createdAt: c2.createdAt
587
1096
  }))
588
1097
  }, 200);
589
- } catch (err) {
590
- console.error("Error fetching ICE candidates:", err);
1098
+ } catch (err2) {
1099
+ console.error("Error getting ICE candidates:", err2);
591
1100
  return c.json({ error: "Internal server error" }, 500);
592
1101
  }
593
1102
  });
@@ -621,6 +1130,7 @@ function loadConfig() {
621
1130
 
622
1131
  // src/storage/sqlite.ts
623
1132
  var import_better_sqlite3 = __toESM(require("better-sqlite3"));
1133
+ var import_crypto4 = require("crypto");
624
1134
 
625
1135
  // src/storage/hash-id.ts
626
1136
  async function generateOfferHash(sdp, topics) {
@@ -639,6 +1149,7 @@ async function generateOfferHash(sdp, topics) {
639
1149
  }
640
1150
 
641
1151
  // src/storage/sqlite.ts
1152
+ var YEAR_IN_MS = 365 * 24 * 60 * 60 * 1e3;
642
1153
  var SQLiteStorage = class {
643
1154
  /**
644
1155
  * Creates a new SQLite storage instance
@@ -649,10 +1160,11 @@ var SQLiteStorage = class {
649
1160
  this.initializeDatabase();
650
1161
  }
651
1162
  /**
652
- * Initializes database schema with new topic-based structure
1163
+ * Initializes database schema with username and service-based structure
653
1164
  */
654
1165
  initializeDatabase() {
655
1166
  this.db.exec(`
1167
+ -- Offers table (no topics)
656
1168
  CREATE TABLE IF NOT EXISTS offers (
657
1169
  id TEXT PRIMARY KEY,
658
1170
  peer_id TEXT NOT NULL,
@@ -671,22 +1183,13 @@ var SQLiteStorage = class {
671
1183
  CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
672
1184
  CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_peer_id);
673
1185
 
674
- CREATE TABLE IF NOT EXISTS offer_topics (
675
- offer_id TEXT NOT NULL,
676
- topic TEXT NOT NULL,
677
- PRIMARY KEY (offer_id, topic),
678
- FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
679
- );
680
-
681
- CREATE INDEX IF NOT EXISTS idx_topics_topic ON offer_topics(topic);
682
- CREATE INDEX IF NOT EXISTS idx_topics_offer ON offer_topics(offer_id);
683
-
1186
+ -- ICE candidates table
684
1187
  CREATE TABLE IF NOT EXISTS ice_candidates (
685
1188
  id INTEGER PRIMARY KEY AUTOINCREMENT,
686
1189
  offer_id TEXT NOT NULL,
687
1190
  peer_id TEXT NOT NULL,
688
1191
  role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
689
- candidate TEXT NOT NULL, -- JSON: RTCIceCandidateInit object
1192
+ candidate TEXT NOT NULL,
690
1193
  created_at INTEGER NOT NULL,
691
1194
  FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
692
1195
  );
@@ -694,15 +1197,64 @@ var SQLiteStorage = class {
694
1197
  CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);
695
1198
  CREATE INDEX IF NOT EXISTS idx_ice_peer ON ice_candidates(peer_id);
696
1199
  CREATE INDEX IF NOT EXISTS idx_ice_created ON ice_candidates(created_at);
1200
+
1201
+ -- Usernames table
1202
+ CREATE TABLE IF NOT EXISTS usernames (
1203
+ username TEXT PRIMARY KEY,
1204
+ public_key TEXT NOT NULL UNIQUE,
1205
+ claimed_at INTEGER NOT NULL,
1206
+ expires_at INTEGER NOT NULL,
1207
+ last_used INTEGER NOT NULL,
1208
+ metadata TEXT,
1209
+ CHECK(length(username) >= 3 AND length(username) <= 32)
1210
+ );
1211
+
1212
+ CREATE INDEX IF NOT EXISTS idx_usernames_expires ON usernames(expires_at);
1213
+ CREATE INDEX IF NOT EXISTS idx_usernames_public_key ON usernames(public_key);
1214
+
1215
+ -- Services table
1216
+ CREATE TABLE IF NOT EXISTS services (
1217
+ id TEXT PRIMARY KEY,
1218
+ username TEXT NOT NULL,
1219
+ service_fqn TEXT NOT NULL,
1220
+ offer_id TEXT NOT NULL,
1221
+ created_at INTEGER NOT NULL,
1222
+ expires_at INTEGER NOT NULL,
1223
+ is_public INTEGER NOT NULL DEFAULT 0,
1224
+ metadata TEXT,
1225
+ FOREIGN KEY (username) REFERENCES usernames(username) ON DELETE CASCADE,
1226
+ FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE,
1227
+ UNIQUE(username, service_fqn)
1228
+ );
1229
+
1230
+ CREATE INDEX IF NOT EXISTS idx_services_username ON services(username);
1231
+ CREATE INDEX IF NOT EXISTS idx_services_fqn ON services(service_fqn);
1232
+ CREATE INDEX IF NOT EXISTS idx_services_expires ON services(expires_at);
1233
+ CREATE INDEX IF NOT EXISTS idx_services_offer ON services(offer_id);
1234
+
1235
+ -- Service index table (privacy layer)
1236
+ CREATE TABLE IF NOT EXISTS service_index (
1237
+ uuid TEXT PRIMARY KEY,
1238
+ service_id TEXT NOT NULL,
1239
+ username TEXT NOT NULL,
1240
+ service_fqn TEXT NOT NULL,
1241
+ created_at INTEGER NOT NULL,
1242
+ expires_at INTEGER NOT NULL,
1243
+ FOREIGN KEY (service_id) REFERENCES services(id) ON DELETE CASCADE
1244
+ );
1245
+
1246
+ CREATE INDEX IF NOT EXISTS idx_service_index_username ON service_index(username);
1247
+ CREATE INDEX IF NOT EXISTS idx_service_index_expires ON service_index(expires_at);
697
1248
  `);
698
1249
  this.db.pragma("foreign_keys = ON");
699
1250
  }
1251
+ // ===== Offer Management =====
700
1252
  async createOffers(offers) {
701
1253
  const created = [];
702
1254
  const offersWithIds = await Promise.all(
703
1255
  offers.map(async (offer) => ({
704
1256
  ...offer,
705
- id: offer.id || await generateOfferHash(offer.sdp, offer.topics)
1257
+ id: offer.id || await generateOfferHash(offer.sdp, [])
706
1258
  }))
707
1259
  );
708
1260
  const transaction = this.db.transaction((offersWithIds2) => {
@@ -710,10 +1262,6 @@ var SQLiteStorage = class {
710
1262
  INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen, secret)
711
1263
  VALUES (?, ?, ?, ?, ?, ?, ?)
712
1264
  `);
713
- const topicStmt = this.db.prepare(`
714
- INSERT INTO offer_topics (offer_id, topic)
715
- VALUES (?, ?)
716
- `);
717
1265
  for (const offer of offersWithIds2) {
718
1266
  const now = Date.now();
719
1267
  offerStmt.run(
@@ -725,14 +1273,10 @@ var SQLiteStorage = class {
725
1273
  now,
726
1274
  offer.secret || null
727
1275
  );
728
- for (const topic of offer.topics) {
729
- topicStmt.run(offer.id, topic);
730
- }
731
1276
  created.push({
732
1277
  id: offer.id,
733
1278
  peerId: offer.peerId,
734
1279
  sdp: offer.sdp,
735
- topics: offer.topics,
736
1280
  createdAt: now,
737
1281
  expiresAt: offer.expiresAt,
738
1282
  lastSeen: now,
@@ -743,24 +1287,6 @@ var SQLiteStorage = class {
743
1287
  transaction(offersWithIds);
744
1288
  return created;
745
1289
  }
746
- async getOffersByTopic(topic, excludePeerIds) {
747
- let query = `
748
- SELECT DISTINCT o.*
749
- FROM offers o
750
- INNER JOIN offer_topics ot ON o.id = ot.offer_id
751
- WHERE ot.topic = ? AND o.expires_at > ?
752
- `;
753
- const params = [topic, Date.now()];
754
- if (excludePeerIds && excludePeerIds.length > 0) {
755
- const placeholders = excludePeerIds.map(() => "?").join(",");
756
- query += ` AND o.peer_id NOT IN (${placeholders})`;
757
- params.push(...excludePeerIds);
758
- }
759
- query += " ORDER BY o.last_seen DESC";
760
- const stmt = this.db.prepare(query);
761
- const rows = stmt.all(...params);
762
- return Promise.all(rows.map((row) => this.rowToOffer(row)));
763
- }
764
1290
  async getOffersByPeerId(peerId) {
765
1291
  const stmt = this.db.prepare(`
766
1292
  SELECT * FROM offers
@@ -768,7 +1294,7 @@ var SQLiteStorage = class {
768
1294
  ORDER BY last_seen DESC
769
1295
  `);
770
1296
  const rows = stmt.all(peerId, Date.now());
771
- return Promise.all(rows.map((row) => this.rowToOffer(row)));
1297
+ return rows.map((row) => this.rowToOffer(row));
772
1298
  }
773
1299
  async getOfferById(offerId) {
774
1300
  const stmt = this.db.prepare(`
@@ -835,8 +1361,9 @@ var SQLiteStorage = class {
835
1361
  ORDER BY answered_at DESC
836
1362
  `);
837
1363
  const rows = stmt.all(offererPeerId, Date.now());
838
- return Promise.all(rows.map((row) => this.rowToOffer(row)));
1364
+ return rows.map((row) => this.rowToOffer(row));
839
1365
  }
1366
+ // ===== ICE Candidate Management =====
840
1367
  async addIceCandidates(offerId, peerId, role, candidates) {
841
1368
  const stmt = this.db.prepare(`
842
1369
  INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, created_at)
@@ -850,9 +1377,7 @@ var SQLiteStorage = class {
850
1377
  peerId,
851
1378
  role,
852
1379
  JSON.stringify(candidates2[i]),
853
- // Store full object as JSON
854
1380
  baseTimestamp + i
855
- // Ensure unique timestamps to avoid "since" filtering issues
856
1381
  );
857
1382
  }
858
1383
  });
@@ -878,61 +1403,198 @@ var SQLiteStorage = class {
878
1403
  peerId: row.peer_id,
879
1404
  role: row.role,
880
1405
  candidate: JSON.parse(row.candidate),
881
- // Parse JSON back to object
882
1406
  createdAt: row.created_at
883
1407
  }));
884
1408
  }
885
- async getTopics(limit, offset, startsWith) {
1409
+ // ===== Username Management =====
1410
+ async claimUsername(request) {
886
1411
  const now = Date.now();
887
- const whereClause = startsWith ? "o.expires_at > ? AND ot.topic LIKE ?" : "o.expires_at > ?";
888
- const startsWithPattern = startsWith ? `${startsWith}%` : null;
889
- const countQuery = `
890
- SELECT COUNT(DISTINCT ot.topic) as count
891
- FROM offer_topics ot
892
- INNER JOIN offers o ON ot.offer_id = o.id
893
- WHERE ${whereClause}
894
- `;
895
- const countStmt = this.db.prepare(countQuery);
896
- const countParams = startsWith ? [now, startsWithPattern] : [now];
897
- const countRow = countStmt.get(...countParams);
898
- const total = countRow.count;
899
- const topicsQuery = `
900
- SELECT
901
- ot.topic,
902
- COUNT(DISTINCT o.peer_id) as active_peers
903
- FROM offer_topics ot
904
- INNER JOIN offers o ON ot.offer_id = o.id
905
- WHERE ${whereClause}
906
- GROUP BY ot.topic
907
- ORDER BY active_peers DESC, ot.topic ASC
908
- LIMIT ? OFFSET ?
909
- `;
910
- const topicsStmt = this.db.prepare(topicsQuery);
911
- const topicsParams = startsWith ? [now, startsWithPattern, limit, offset] : [now, limit, offset];
912
- const rows = topicsStmt.all(...topicsParams);
913
- const topics = rows.map((row) => ({
914
- topic: row.topic,
915
- activePeers: row.active_peers
1412
+ const expiresAt = now + YEAR_IN_MS;
1413
+ const stmt = this.db.prepare(`
1414
+ INSERT INTO usernames (username, public_key, claimed_at, expires_at, last_used, metadata)
1415
+ VALUES (?, ?, ?, ?, ?, NULL)
1416
+ ON CONFLICT(username) DO UPDATE SET
1417
+ expires_at = ?,
1418
+ last_used = ?
1419
+ WHERE public_key = ?
1420
+ `);
1421
+ const result = stmt.run(
1422
+ request.username,
1423
+ request.publicKey,
1424
+ now,
1425
+ expiresAt,
1426
+ now,
1427
+ expiresAt,
1428
+ now,
1429
+ request.publicKey
1430
+ );
1431
+ if (result.changes === 0) {
1432
+ throw new Error("Username already claimed by different public key");
1433
+ }
1434
+ return {
1435
+ username: request.username,
1436
+ publicKey: request.publicKey,
1437
+ claimedAt: now,
1438
+ expiresAt,
1439
+ lastUsed: now
1440
+ };
1441
+ }
1442
+ async getUsername(username) {
1443
+ const stmt = this.db.prepare(`
1444
+ SELECT * FROM usernames
1445
+ WHERE username = ? AND expires_at > ?
1446
+ `);
1447
+ const row = stmt.get(username, Date.now());
1448
+ if (!row) {
1449
+ return null;
1450
+ }
1451
+ return {
1452
+ username: row.username,
1453
+ publicKey: row.public_key,
1454
+ claimedAt: row.claimed_at,
1455
+ expiresAt: row.expires_at,
1456
+ lastUsed: row.last_used,
1457
+ metadata: row.metadata || void 0
1458
+ };
1459
+ }
1460
+ async touchUsername(username) {
1461
+ const now = Date.now();
1462
+ const expiresAt = now + YEAR_IN_MS;
1463
+ const stmt = this.db.prepare(`
1464
+ UPDATE usernames
1465
+ SET last_used = ?, expires_at = ?
1466
+ WHERE username = ? AND expires_at > ?
1467
+ `);
1468
+ const result = stmt.run(now, expiresAt, username, now);
1469
+ return result.changes > 0;
1470
+ }
1471
+ async deleteExpiredUsernames(now) {
1472
+ const stmt = this.db.prepare("DELETE FROM usernames WHERE expires_at < ?");
1473
+ const result = stmt.run(now);
1474
+ return result.changes;
1475
+ }
1476
+ // ===== Service Management =====
1477
+ async createService(request) {
1478
+ const serviceId = (0, import_crypto4.randomUUID)();
1479
+ const indexUuid = (0, import_crypto4.randomUUID)();
1480
+ const now = Date.now();
1481
+ const transaction = this.db.transaction(() => {
1482
+ const serviceStmt = this.db.prepare(`
1483
+ INSERT INTO services (id, username, service_fqn, offer_id, created_at, expires_at, is_public, metadata)
1484
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
1485
+ `);
1486
+ serviceStmt.run(
1487
+ serviceId,
1488
+ request.username,
1489
+ request.serviceFqn,
1490
+ request.offerId,
1491
+ now,
1492
+ request.expiresAt,
1493
+ request.isPublic ? 1 : 0,
1494
+ request.metadata || null
1495
+ );
1496
+ const indexStmt = this.db.prepare(`
1497
+ INSERT INTO service_index (uuid, service_id, username, service_fqn, created_at, expires_at)
1498
+ VALUES (?, ?, ?, ?, ?, ?)
1499
+ `);
1500
+ indexStmt.run(
1501
+ indexUuid,
1502
+ serviceId,
1503
+ request.username,
1504
+ request.serviceFqn,
1505
+ now,
1506
+ request.expiresAt
1507
+ );
1508
+ this.touchUsername(request.username);
1509
+ });
1510
+ transaction();
1511
+ return {
1512
+ service: {
1513
+ id: serviceId,
1514
+ username: request.username,
1515
+ serviceFqn: request.serviceFqn,
1516
+ offerId: request.offerId,
1517
+ createdAt: now,
1518
+ expiresAt: request.expiresAt,
1519
+ isPublic: request.isPublic || false,
1520
+ metadata: request.metadata
1521
+ },
1522
+ indexUuid
1523
+ };
1524
+ }
1525
+ async getServiceById(serviceId) {
1526
+ const stmt = this.db.prepare(`
1527
+ SELECT * FROM services
1528
+ WHERE id = ? AND expires_at > ?
1529
+ `);
1530
+ const row = stmt.get(serviceId, Date.now());
1531
+ if (!row) {
1532
+ return null;
1533
+ }
1534
+ return this.rowToService(row);
1535
+ }
1536
+ async getServiceByUuid(uuid) {
1537
+ const stmt = this.db.prepare(`
1538
+ SELECT s.* FROM services s
1539
+ INNER JOIN service_index si ON s.id = si.service_id
1540
+ WHERE si.uuid = ? AND s.expires_at > ?
1541
+ `);
1542
+ const row = stmt.get(uuid, Date.now());
1543
+ if (!row) {
1544
+ return null;
1545
+ }
1546
+ return this.rowToService(row);
1547
+ }
1548
+ async listServicesForUsername(username) {
1549
+ const stmt = this.db.prepare(`
1550
+ SELECT si.uuid, s.is_public, s.service_fqn, s.metadata
1551
+ FROM service_index si
1552
+ INNER JOIN services s ON si.service_id = s.id
1553
+ WHERE si.username = ? AND si.expires_at > ?
1554
+ ORDER BY s.created_at DESC
1555
+ `);
1556
+ const rows = stmt.all(username, Date.now());
1557
+ return rows.map((row) => ({
1558
+ uuid: row.uuid,
1559
+ isPublic: row.is_public === 1,
1560
+ serviceFqn: row.is_public === 1 ? row.service_fqn : void 0,
1561
+ metadata: row.is_public === 1 ? row.metadata || void 0 : void 0
916
1562
  }));
917
- return { topics, total };
1563
+ }
1564
+ async queryService(username, serviceFqn) {
1565
+ const stmt = this.db.prepare(`
1566
+ SELECT si.uuid FROM service_index si
1567
+ INNER JOIN services s ON si.service_id = s.id
1568
+ WHERE si.username = ? AND si.service_fqn = ? AND si.expires_at > ?
1569
+ `);
1570
+ const row = stmt.get(username, serviceFqn, Date.now());
1571
+ return row ? row.uuid : null;
1572
+ }
1573
+ async deleteService(serviceId, username) {
1574
+ const stmt = this.db.prepare(`
1575
+ DELETE FROM services
1576
+ WHERE id = ? AND username = ?
1577
+ `);
1578
+ const result = stmt.run(serviceId, username);
1579
+ return result.changes > 0;
1580
+ }
1581
+ async deleteExpiredServices(now) {
1582
+ const stmt = this.db.prepare("DELETE FROM services WHERE expires_at < ?");
1583
+ const result = stmt.run(now);
1584
+ return result.changes;
918
1585
  }
919
1586
  async close() {
920
1587
  this.db.close();
921
1588
  }
1589
+ // ===== Helper Methods =====
922
1590
  /**
923
- * Helper method to convert database row to Offer object with topics
1591
+ * Helper method to convert database row to Offer object
924
1592
  */
925
- async rowToOffer(row) {
926
- const topicStmt = this.db.prepare(`
927
- SELECT topic FROM offer_topics WHERE offer_id = ?
928
- `);
929
- const topicRows = topicStmt.all(row.id);
930
- const topics = topicRows.map((t) => t.topic);
1593
+ rowToOffer(row) {
931
1594
  return {
932
1595
  id: row.id,
933
1596
  peerId: row.peer_id,
934
1597
  sdp: row.sdp,
935
- topics,
936
1598
  createdAt: row.created_at,
937
1599
  expiresAt: row.expires_at,
938
1600
  lastSeen: row.last_seen,
@@ -942,6 +1604,21 @@ var SQLiteStorage = class {
942
1604
  answeredAt: row.answered_at || void 0
943
1605
  };
944
1606
  }
1607
+ /**
1608
+ * Helper method to convert database row to Service object
1609
+ */
1610
+ rowToService(row) {
1611
+ return {
1612
+ id: row.id,
1613
+ username: row.username,
1614
+ serviceFqn: row.service_fqn,
1615
+ offerId: row.offer_id,
1616
+ createdAt: row.created_at,
1617
+ expiresAt: row.expires_at,
1618
+ isPublic: row.is_public === 1,
1619
+ metadata: row.metadata || void 0
1620
+ };
1621
+ }
945
1622
  };
946
1623
 
947
1624
  // src/index.ts
@@ -975,8 +1652,8 @@ async function main() {
975
1652
  if (deleted > 0) {
976
1653
  console.log(`Cleanup: Deleted ${deleted} expired offer(s)`);
977
1654
  }
978
- } catch (err) {
979
- console.error("Cleanup error:", err);
1655
+ } catch (err2) {
1656
+ console.error("Cleanup error:", err2);
980
1657
  }
981
1658
  }, config.cleanupInterval);
982
1659
  const app = createApp(storage, config);
@@ -995,8 +1672,13 @@ async function main() {
995
1672
  process.on("SIGINT", shutdown);
996
1673
  process.on("SIGTERM", shutdown);
997
1674
  }
998
- main().catch((err) => {
999
- console.error("Fatal error:", err);
1675
+ main().catch((err2) => {
1676
+ console.error("Fatal error:", err2);
1000
1677
  process.exit(1);
1001
1678
  });
1679
+ /*! Bundled license information:
1680
+
1681
+ @noble/ed25519/index.js:
1682
+ (*! noble-ed25519 - MIT License (c) 2019 Paul Miller (paulmillr.com) *)
1683
+ */
1002
1684
  //# sourceMappingURL=index.js.map