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