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