@xtr-dev/rondevu-server 0.1.5 → 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/README.md +217 -69
- package/dist/index.js +1067 -368
- package/dist/index.js.map +4 -4
- package/package.json +3 -2
- package/src/app.ts +339 -271
- package/src/crypto.ts +164 -0
- package/src/storage/d1.ts +295 -119
- package/src/storage/sqlite.ts +309 -107
- package/src/storage/types.ts +159 -29
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
|
|
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 =
|
|
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 =
|
|
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 (
|
|
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: "
|
|
717
|
+
description: "DNS-like WebRTC signaling with username claiming and service discovery"
|
|
231
718
|
});
|
|
232
719
|
});
|
|
233
720
|
app.get("/health", (c) => {
|
|
@@ -245,204 +732,271 @@ function createApp(storage, config) {
|
|
|
245
732
|
peerId,
|
|
246
733
|
secret
|
|
247
734
|
}, 200);
|
|
248
|
-
} catch (
|
|
249
|
-
console.error("Error registering peer:",
|
|
735
|
+
} catch (err2) {
|
|
736
|
+
console.error("Error registering peer:", err2);
|
|
250
737
|
return c.json({ error: "Internal server error" }, 500);
|
|
251
738
|
}
|
|
252
739
|
});
|
|
253
|
-
app.post("/
|
|
740
|
+
app.post("/usernames/claim", async (c) => {
|
|
254
741
|
try {
|
|
255
742
|
const body = await c.req.json();
|
|
256
|
-
const {
|
|
257
|
-
if (!
|
|
258
|
-
return c.json({ error: "Missing
|
|
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);
|
|
259
746
|
}
|
|
260
|
-
|
|
261
|
-
|
|
747
|
+
const validation = await validateUsernameClaim(username, publicKey, signature, message);
|
|
748
|
+
if (!validation.valid) {
|
|
749
|
+
return c.json({ error: validation.error }, 400);
|
|
262
750
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
751
|
+
try {
|
|
752
|
+
const claimed = await storage.claimUsername({
|
|
753
|
+
username,
|
|
754
|
+
publicKey,
|
|
755
|
+
signature,
|
|
756
|
+
message
|
|
314
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);
|
|
315
783
|
}
|
|
316
|
-
const createdOffers = await storage.createOffers(offerRequests);
|
|
317
784
|
return c.json({
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}))
|
|
785
|
+
username: claimed.username,
|
|
786
|
+
available: false,
|
|
787
|
+
claimedAt: claimed.claimedAt,
|
|
788
|
+
expiresAt: claimed.expiresAt,
|
|
789
|
+
publicKey: claimed.publicKey
|
|
324
790
|
}, 200);
|
|
325
|
-
} catch (
|
|
326
|
-
console.error("Error
|
|
791
|
+
} catch (err2) {
|
|
792
|
+
console.error("Error checking username:", err2);
|
|
327
793
|
return c.json({ error: "Internal server error" }, 500);
|
|
328
794
|
}
|
|
329
795
|
});
|
|
330
|
-
app.get("/
|
|
796
|
+
app.get("/usernames/:username/services", async (c) => {
|
|
331
797
|
try {
|
|
332
|
-
const
|
|
333
|
-
const
|
|
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);
|
|
798
|
+
const username = c.req.param("username");
|
|
799
|
+
const services = await storage.listServicesForUsername(username);
|
|
354
800
|
return c.json({
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
801
|
+
username,
|
|
802
|
+
services
|
|
370
803
|
}, 200);
|
|
371
|
-
} catch (
|
|
372
|
-
console.error("Error
|
|
804
|
+
} catch (err2) {
|
|
805
|
+
console.error("Error listing services:", err2);
|
|
373
806
|
return c.json({ error: "Internal server error" }, 500);
|
|
374
807
|
}
|
|
375
808
|
});
|
|
376
|
-
app.
|
|
809
|
+
app.post("/services", authMiddleware, async (c) => {
|
|
377
810
|
try {
|
|
378
|
-
const
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
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
|
+
});
|
|
860
|
+
return c.json({
|
|
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
|
+
}
|
|
385
882
|
return c.json({
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
391
892
|
}, 200);
|
|
392
|
-
} catch (
|
|
393
|
-
console.error("Error
|
|
893
|
+
} catch (err2) {
|
|
894
|
+
console.error("Error getting service:", err2);
|
|
394
895
|
return c.json({ error: "Internal server error" }, 500);
|
|
395
896
|
}
|
|
396
897
|
});
|
|
397
|
-
app.
|
|
898
|
+
app.delete("/services/:serviceId", authMiddleware, async (c) => {
|
|
398
899
|
try {
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
|
|
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
|
+
}
|
|
403
928
|
return c.json({
|
|
404
|
-
|
|
405
|
-
|
|
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)
|
|
929
|
+
uuid,
|
|
930
|
+
allowed: true
|
|
417
931
|
}, 200);
|
|
418
|
-
} catch (
|
|
419
|
-
console.error("Error
|
|
932
|
+
} catch (err2) {
|
|
933
|
+
console.error("Error querying service:", err2);
|
|
420
934
|
return c.json({ error: "Internal server error" }, 500);
|
|
421
935
|
}
|
|
422
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
|
+
});
|
|
423
982
|
app.get("/offers/mine", authMiddleware, async (c) => {
|
|
424
983
|
try {
|
|
425
984
|
const peerId = getAuthenticatedPeerId(c);
|
|
426
985
|
const offers = await storage.getOffersByPeerId(peerId);
|
|
427
986
|
return c.json({
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
// Owner can see the secret
|
|
438
|
-
info: o.info,
|
|
439
|
-
// Owner can see the info
|
|
440
|
-
answererPeerId: o.answererPeerId,
|
|
441
|
-
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
|
|
442
996
|
}))
|
|
443
997
|
}, 200);
|
|
444
|
-
} catch (
|
|
445
|
-
console.error("Error
|
|
998
|
+
} catch (err2) {
|
|
999
|
+
console.error("Error getting offers:", err2);
|
|
446
1000
|
return c.json({ error: "Internal server error" }, 500);
|
|
447
1001
|
}
|
|
448
1002
|
});
|
|
@@ -452,40 +1006,36 @@ function createApp(storage, config) {
|
|
|
452
1006
|
const peerId = getAuthenticatedPeerId(c);
|
|
453
1007
|
const deleted = await storage.deleteOffer(offerId, peerId);
|
|
454
1008
|
if (!deleted) {
|
|
455
|
-
return c.json({ error: "Offer not found or not
|
|
1009
|
+
return c.json({ error: "Offer not found or not owned by this peer" }, 404);
|
|
456
1010
|
}
|
|
457
|
-
return c.json({
|
|
458
|
-
} catch (
|
|
459
|
-
console.error("Error deleting offer:",
|
|
1011
|
+
return c.json({ success: true }, 200);
|
|
1012
|
+
} catch (err2) {
|
|
1013
|
+
console.error("Error deleting offer:", err2);
|
|
460
1014
|
return c.json({ error: "Internal server error" }, 500);
|
|
461
1015
|
}
|
|
462
1016
|
});
|
|
463
1017
|
app.post("/offers/:offerId/answer", authMiddleware, async (c) => {
|
|
464
1018
|
try {
|
|
465
1019
|
const offerId = c.req.param("offerId");
|
|
466
|
-
const peerId = getAuthenticatedPeerId(c);
|
|
467
1020
|
const body = await c.req.json();
|
|
468
1021
|
const { sdp, secret } = body;
|
|
469
|
-
if (!sdp
|
|
470
|
-
return c.json({ error: "Missing
|
|
1022
|
+
if (!sdp) {
|
|
1023
|
+
return c.json({ error: "Missing required parameter: sdp" }, 400);
|
|
471
1024
|
}
|
|
472
|
-
if (sdp.length
|
|
473
|
-
return c.json({ error: "SDP
|
|
1025
|
+
if (typeof sdp !== "string" || sdp.length === 0) {
|
|
1026
|
+
return c.json({ error: "Invalid SDP" }, 400);
|
|
474
1027
|
}
|
|
475
|
-
if (
|
|
476
|
-
return c.json({ error: "
|
|
1028
|
+
if (sdp.length > 64 * 1024) {
|
|
1029
|
+
return c.json({ error: "SDP too large (max 64KB)" }, 400);
|
|
477
1030
|
}
|
|
478
|
-
const
|
|
1031
|
+
const answererPeerId = getAuthenticatedPeerId(c);
|
|
1032
|
+
const result = await storage.answerOffer(offerId, answererPeerId, sdp, secret);
|
|
479
1033
|
if (!result.success) {
|
|
480
1034
|
return c.json({ error: result.error }, 400);
|
|
481
1035
|
}
|
|
482
|
-
return c.json({
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
answeredAt: Date.now()
|
|
486
|
-
}, 200);
|
|
487
|
-
} catch (err) {
|
|
488
|
-
console.error("Error answering offer:", err);
|
|
1036
|
+
return c.json({ success: true }, 200);
|
|
1037
|
+
} catch (err2) {
|
|
1038
|
+
console.error("Error answering offer:", err2);
|
|
489
1039
|
return c.json({ error: "Internal server error" }, 500);
|
|
490
1040
|
}
|
|
491
1041
|
});
|
|
@@ -494,83 +1044,59 @@ function createApp(storage, config) {
|
|
|
494
1044
|
const peerId = getAuthenticatedPeerId(c);
|
|
495
1045
|
const offers = await storage.getAnsweredOffers(peerId);
|
|
496
1046
|
return c.json({
|
|
497
|
-
answers: offers.map((
|
|
498
|
-
offerId:
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
answeredAt:
|
|
502
|
-
topics: o.topics
|
|
1047
|
+
answers: offers.map((offer) => ({
|
|
1048
|
+
offerId: offer.id,
|
|
1049
|
+
answererPeerId: offer.answererPeerId,
|
|
1050
|
+
answerSdp: offer.answerSdp,
|
|
1051
|
+
answeredAt: offer.answeredAt
|
|
503
1052
|
}))
|
|
504
1053
|
}, 200);
|
|
505
|
-
} catch (
|
|
506
|
-
console.error("Error
|
|
1054
|
+
} catch (err2) {
|
|
1055
|
+
console.error("Error getting answers:", err2);
|
|
507
1056
|
return c.json({ error: "Internal server error" }, 500);
|
|
508
1057
|
}
|
|
509
1058
|
});
|
|
510
1059
|
app.post("/offers/:offerId/ice-candidates", authMiddleware, async (c) => {
|
|
511
1060
|
try {
|
|
512
1061
|
const offerId = c.req.param("offerId");
|
|
513
|
-
const peerId = getAuthenticatedPeerId(c);
|
|
514
1062
|
const body = await c.req.json();
|
|
515
1063
|
const { candidates } = body;
|
|
516
1064
|
if (!Array.isArray(candidates) || candidates.length === 0) {
|
|
517
|
-
return c.json({ error: "Missing or invalid required parameter: candidates
|
|
1065
|
+
return c.json({ error: "Missing or invalid required parameter: candidates" }, 400);
|
|
518
1066
|
}
|
|
1067
|
+
const peerId = getAuthenticatedPeerId(c);
|
|
519
1068
|
const offer = await storage.getOfferById(offerId);
|
|
520
1069
|
if (!offer) {
|
|
521
|
-
return c.json({ error: "Offer not found
|
|
1070
|
+
return c.json({ error: "Offer not found" }, 404);
|
|
522
1071
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
} else {
|
|
529
|
-
return c.json({ error: "Not authorized to post ICE candidates for this offer" }, 403);
|
|
530
|
-
}
|
|
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);
|
|
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);
|
|
538
1077
|
return c.json({ error: "Internal server error" }, 500);
|
|
539
1078
|
}
|
|
540
1079
|
});
|
|
541
1080
|
app.get("/offers/:offerId/ice-candidates", authMiddleware, async (c) => {
|
|
542
1081
|
try {
|
|
543
1082
|
const offerId = c.req.param("offerId");
|
|
1083
|
+
const since = c.req.query("since");
|
|
544
1084
|
const peerId = getAuthenticatedPeerId(c);
|
|
545
|
-
const sinceParam = c.req.query("since");
|
|
546
|
-
const since = sinceParam ? parseInt(sinceParam, 10) : void 0;
|
|
547
1085
|
const offer = await storage.getOfferById(offerId);
|
|
548
1086
|
if (!offer) {
|
|
549
|
-
return c.json({ error: "Offer not found
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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}`);
|
|
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);
|
|
563
1092
|
return c.json({
|
|
564
|
-
offerId,
|
|
565
1093
|
candidates: candidates.map((c2) => ({
|
|
566
1094
|
candidate: c2.candidate,
|
|
567
|
-
peerId: c2.peerId,
|
|
568
|
-
role: c2.role,
|
|
569
1095
|
createdAt: c2.createdAt
|
|
570
1096
|
}))
|
|
571
1097
|
}, 200);
|
|
572
|
-
} catch (
|
|
573
|
-
console.error("Error
|
|
1098
|
+
} catch (err2) {
|
|
1099
|
+
console.error("Error getting ICE candidates:", err2);
|
|
574
1100
|
return c.json({ error: "Internal server error" }, 500);
|
|
575
1101
|
}
|
|
576
1102
|
});
|
|
@@ -604,6 +1130,7 @@ function loadConfig() {
|
|
|
604
1130
|
|
|
605
1131
|
// src/storage/sqlite.ts
|
|
606
1132
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
1133
|
+
var import_crypto4 = require("crypto");
|
|
607
1134
|
|
|
608
1135
|
// src/storage/hash-id.ts
|
|
609
1136
|
async function generateOfferHash(sdp, topics) {
|
|
@@ -622,6 +1149,7 @@ async function generateOfferHash(sdp, topics) {
|
|
|
622
1149
|
}
|
|
623
1150
|
|
|
624
1151
|
// src/storage/sqlite.ts
|
|
1152
|
+
var YEAR_IN_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
625
1153
|
var SQLiteStorage = class {
|
|
626
1154
|
/**
|
|
627
1155
|
* Creates a new SQLite storage instance
|
|
@@ -632,10 +1160,11 @@ var SQLiteStorage = class {
|
|
|
632
1160
|
this.initializeDatabase();
|
|
633
1161
|
}
|
|
634
1162
|
/**
|
|
635
|
-
* Initializes database schema with
|
|
1163
|
+
* Initializes database schema with username and service-based structure
|
|
636
1164
|
*/
|
|
637
1165
|
initializeDatabase() {
|
|
638
1166
|
this.db.exec(`
|
|
1167
|
+
-- Offers table (no topics)
|
|
639
1168
|
CREATE TABLE IF NOT EXISTS offers (
|
|
640
1169
|
id TEXT PRIMARY KEY,
|
|
641
1170
|
peer_id TEXT NOT NULL,
|
|
@@ -654,22 +1183,13 @@ var SQLiteStorage = class {
|
|
|
654
1183
|
CREATE INDEX IF NOT EXISTS idx_offers_last_seen ON offers(last_seen);
|
|
655
1184
|
CREATE INDEX IF NOT EXISTS idx_offers_answerer ON offers(answerer_peer_id);
|
|
656
1185
|
|
|
657
|
-
|
|
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
|
-
|
|
1186
|
+
-- ICE candidates table
|
|
667
1187
|
CREATE TABLE IF NOT EXISTS ice_candidates (
|
|
668
1188
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
669
1189
|
offer_id TEXT NOT NULL,
|
|
670
1190
|
peer_id TEXT NOT NULL,
|
|
671
1191
|
role TEXT NOT NULL CHECK(role IN ('offerer', 'answerer')),
|
|
672
|
-
candidate TEXT NOT NULL,
|
|
1192
|
+
candidate TEXT NOT NULL,
|
|
673
1193
|
created_at INTEGER NOT NULL,
|
|
674
1194
|
FOREIGN KEY (offer_id) REFERENCES offers(id) ON DELETE CASCADE
|
|
675
1195
|
);
|
|
@@ -677,15 +1197,64 @@ var SQLiteStorage = class {
|
|
|
677
1197
|
CREATE INDEX IF NOT EXISTS idx_ice_offer ON ice_candidates(offer_id);
|
|
678
1198
|
CREATE INDEX IF NOT EXISTS idx_ice_peer ON ice_candidates(peer_id);
|
|
679
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);
|
|
680
1248
|
`);
|
|
681
1249
|
this.db.pragma("foreign_keys = ON");
|
|
682
1250
|
}
|
|
1251
|
+
// ===== Offer Management =====
|
|
683
1252
|
async createOffers(offers) {
|
|
684
1253
|
const created = [];
|
|
685
1254
|
const offersWithIds = await Promise.all(
|
|
686
1255
|
offers.map(async (offer) => ({
|
|
687
1256
|
...offer,
|
|
688
|
-
id: offer.id || await generateOfferHash(offer.sdp,
|
|
1257
|
+
id: offer.id || await generateOfferHash(offer.sdp, [])
|
|
689
1258
|
}))
|
|
690
1259
|
);
|
|
691
1260
|
const transaction = this.db.transaction((offersWithIds2) => {
|
|
@@ -693,10 +1262,6 @@ var SQLiteStorage = class {
|
|
|
693
1262
|
INSERT INTO offers (id, peer_id, sdp, created_at, expires_at, last_seen, secret)
|
|
694
1263
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
695
1264
|
`);
|
|
696
|
-
const topicStmt = this.db.prepare(`
|
|
697
|
-
INSERT INTO offer_topics (offer_id, topic)
|
|
698
|
-
VALUES (?, ?)
|
|
699
|
-
`);
|
|
700
1265
|
for (const offer of offersWithIds2) {
|
|
701
1266
|
const now = Date.now();
|
|
702
1267
|
offerStmt.run(
|
|
@@ -708,14 +1273,10 @@ var SQLiteStorage = class {
|
|
|
708
1273
|
now,
|
|
709
1274
|
offer.secret || null
|
|
710
1275
|
);
|
|
711
|
-
for (const topic of offer.topics) {
|
|
712
|
-
topicStmt.run(offer.id, topic);
|
|
713
|
-
}
|
|
714
1276
|
created.push({
|
|
715
1277
|
id: offer.id,
|
|
716
1278
|
peerId: offer.peerId,
|
|
717
1279
|
sdp: offer.sdp,
|
|
718
|
-
topics: offer.topics,
|
|
719
1280
|
createdAt: now,
|
|
720
1281
|
expiresAt: offer.expiresAt,
|
|
721
1282
|
lastSeen: now,
|
|
@@ -726,24 +1287,6 @@ var SQLiteStorage = class {
|
|
|
726
1287
|
transaction(offersWithIds);
|
|
727
1288
|
return created;
|
|
728
1289
|
}
|
|
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
1290
|
async getOffersByPeerId(peerId) {
|
|
748
1291
|
const stmt = this.db.prepare(`
|
|
749
1292
|
SELECT * FROM offers
|
|
@@ -751,7 +1294,7 @@ var SQLiteStorage = class {
|
|
|
751
1294
|
ORDER BY last_seen DESC
|
|
752
1295
|
`);
|
|
753
1296
|
const rows = stmt.all(peerId, Date.now());
|
|
754
|
-
return
|
|
1297
|
+
return rows.map((row) => this.rowToOffer(row));
|
|
755
1298
|
}
|
|
756
1299
|
async getOfferById(offerId) {
|
|
757
1300
|
const stmt = this.db.prepare(`
|
|
@@ -818,8 +1361,9 @@ var SQLiteStorage = class {
|
|
|
818
1361
|
ORDER BY answered_at DESC
|
|
819
1362
|
`);
|
|
820
1363
|
const rows = stmt.all(offererPeerId, Date.now());
|
|
821
|
-
return
|
|
1364
|
+
return rows.map((row) => this.rowToOffer(row));
|
|
822
1365
|
}
|
|
1366
|
+
// ===== ICE Candidate Management =====
|
|
823
1367
|
async addIceCandidates(offerId, peerId, role, candidates) {
|
|
824
1368
|
const stmt = this.db.prepare(`
|
|
825
1369
|
INSERT INTO ice_candidates (offer_id, peer_id, role, candidate, created_at)
|
|
@@ -833,9 +1377,7 @@ var SQLiteStorage = class {
|
|
|
833
1377
|
peerId,
|
|
834
1378
|
role,
|
|
835
1379
|
JSON.stringify(candidates2[i]),
|
|
836
|
-
// Store full object as JSON
|
|
837
1380
|
baseTimestamp + i
|
|
838
|
-
// Ensure unique timestamps to avoid "since" filtering issues
|
|
839
1381
|
);
|
|
840
1382
|
}
|
|
841
1383
|
});
|
|
@@ -861,61 +1403,198 @@ var SQLiteStorage = class {
|
|
|
861
1403
|
peerId: row.peer_id,
|
|
862
1404
|
role: row.role,
|
|
863
1405
|
candidate: JSON.parse(row.candidate),
|
|
864
|
-
// Parse JSON back to object
|
|
865
1406
|
createdAt: row.created_at
|
|
866
1407
|
}));
|
|
867
1408
|
}
|
|
868
|
-
|
|
1409
|
+
// ===== Username Management =====
|
|
1410
|
+
async claimUsername(request) {
|
|
869
1411
|
const now = Date.now();
|
|
870
|
-
const
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
|
899
1562
|
}));
|
|
900
|
-
|
|
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;
|
|
901
1585
|
}
|
|
902
1586
|
async close() {
|
|
903
1587
|
this.db.close();
|
|
904
1588
|
}
|
|
1589
|
+
// ===== Helper Methods =====
|
|
905
1590
|
/**
|
|
906
|
-
* Helper method to convert database row to Offer object
|
|
1591
|
+
* Helper method to convert database row to Offer object
|
|
907
1592
|
*/
|
|
908
|
-
|
|
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);
|
|
1593
|
+
rowToOffer(row) {
|
|
914
1594
|
return {
|
|
915
1595
|
id: row.id,
|
|
916
1596
|
peerId: row.peer_id,
|
|
917
1597
|
sdp: row.sdp,
|
|
918
|
-
topics,
|
|
919
1598
|
createdAt: row.created_at,
|
|
920
1599
|
expiresAt: row.expires_at,
|
|
921
1600
|
lastSeen: row.last_seen,
|
|
@@ -925,6 +1604,21 @@ var SQLiteStorage = class {
|
|
|
925
1604
|
answeredAt: row.answered_at || void 0
|
|
926
1605
|
};
|
|
927
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
|
+
}
|
|
928
1622
|
};
|
|
929
1623
|
|
|
930
1624
|
// src/index.ts
|
|
@@ -958,8 +1652,8 @@ async function main() {
|
|
|
958
1652
|
if (deleted > 0) {
|
|
959
1653
|
console.log(`Cleanup: Deleted ${deleted} expired offer(s)`);
|
|
960
1654
|
}
|
|
961
|
-
} catch (
|
|
962
|
-
console.error("Cleanup error:",
|
|
1655
|
+
} catch (err2) {
|
|
1656
|
+
console.error("Cleanup error:", err2);
|
|
963
1657
|
}
|
|
964
1658
|
}, config.cleanupInterval);
|
|
965
1659
|
const app = createApp(storage, config);
|
|
@@ -978,8 +1672,13 @@ async function main() {
|
|
|
978
1672
|
process.on("SIGINT", shutdown);
|
|
979
1673
|
process.on("SIGTERM", shutdown);
|
|
980
1674
|
}
|
|
981
|
-
main().catch((
|
|
982
|
-
console.error("Fatal error:",
|
|
1675
|
+
main().catch((err2) => {
|
|
1676
|
+
console.error("Fatal error:", err2);
|
|
983
1677
|
process.exit(1);
|
|
984
1678
|
});
|
|
1679
|
+
/*! Bundled license information:
|
|
1680
|
+
|
|
1681
|
+
@noble/ed25519/index.js:
|
|
1682
|
+
(*! noble-ed25519 - MIT License (c) 2019 Paul Miller (paulmillr.com) *)
|
|
1683
|
+
*/
|
|
985
1684
|
//# sourceMappingURL=index.js.map
|