pinomad 0.11.0 → 0.13.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 +1 -1
- package/dist/{chunk-wb10efyq.js → chunk-445cevpm.js} +1 -1
- package/dist/{chunk-jt34g8st.js → chunk-4fferbxw.js} +247 -27
- package/dist/{chunk-67gq282t.js → chunk-53xv3fny.js} +725 -75
- package/dist/{chunk-44pnts98.js → chunk-563fnad9.js} +1 -1
- package/dist/{chunk-b457q0tm.js → chunk-6th9xv39.js} +11 -8
- package/dist/{chunk-ncbqa8jk.js → chunk-7mn38n4f.js} +1 -1
- package/dist/{chunk-as6pyf00.js → chunk-90g1cg9n.js} +1 -1
- package/dist/{chunk-9h5vawnm.js → chunk-aapsc9g0.js} +270 -66
- package/dist/{chunk-n1m1tjrk.js → chunk-d4tq62yr.js} +57 -16
- package/dist/{chunk-gtdpjqa3.js → chunk-d9mq5d46.js} +205 -49
- package/dist/{chunk-y127fe1a.js → chunk-g3ahp7b9.js} +11 -0
- package/dist/{chunk-xqycs43g.js → chunk-kmzzjv53.js} +83 -8
- package/dist/{chunk-4dcs8sbj.js → chunk-m51mq8vh.js} +43 -21
- package/dist/{chunk-tj57aqav.js → chunk-maqv34cs.js} +2 -2
- package/dist/chunk-nq57fdnq.js +106 -0
- package/dist/{chunk-h5q2y57m.js → chunk-ra86wjp5.js} +10 -4
- package/dist/{chunk-sd5fkxjj.js → chunk-s0h7c7da.js} +1 -1
- package/dist/chunk-sd640dt7.js +22 -0
- package/dist/{chunk-6d16c2fv.js → chunk-t6fw0wa9.js} +13 -6
- package/dist/{chunk-vvj9b1gc.js → chunk-wd7aracm.js} +188 -54
- package/dist/pinomad.js +15 -14
- package/package.json +13 -11
- package/web/assets/astro-DOly5024.js +1 -0
- package/web/assets/c-BIGW1oBm.js +1 -0
- package/web/assets/core-BHm2gdHL.js +4 -0
- package/web/assets/cpp-DddhVWhK.js +1 -0
- package/web/assets/csharp-B_TYUkTI.js +1 -0
- package/web/assets/css-Z72CF80r.js +1 -0
- package/web/assets/dart-CtKP3W-j.js +1 -0
- package/web/assets/diff-D97Zzqfu.js +1 -0
- package/web/assets/dockerfile-BcOcwvcX.js +1 -0
- package/web/assets/elixir-DkJLSxtU.js +1 -0
- package/web/assets/engine-javascript-BHPwbUfN.js +141 -0
- package/web/assets/engine-oniguruma-CQjQPVHy.js +1 -0
- package/web/assets/fish-BvzEVeQv.js +1 -0
- package/web/assets/geist-mono-cyrillic-ext-wght-normal-X_5orZeX.woff2 +0 -0
- package/web/assets/geist-mono-cyrillic-wght-normal-DiZS0aHC.woff2 +0 -0
- package/web/assets/geist-mono-latin-ext-wght-normal-Bwz-egvJ.woff2 +0 -0
- package/web/assets/geist-mono-latin-wght-normal-XN7g48iV.woff2 +0 -0
- package/web/assets/geist-mono-symbols2-wght-normal-CO5SzqOn.woff2 +0 -0
- package/web/assets/geist-mono-vietnamese-wght-normal-DadHysG0.woff2 +0 -0
- package/web/assets/go-C27-OAKa.js +1 -0
- package/web/assets/graphql-ChdNCCLP.js +1 -0
- package/web/assets/haskell-Df6bDoY_.js +1 -0
- package/web/assets/hcl-BWvSN4gD.js +1 -0
- package/web/assets/highlighted-diff-DrHvSLK9.js +175 -0
- package/web/assets/highlights-1aLwssz-.js +4 -0
- package/web/assets/highlights-themes-C1p4pFYS.js +1 -0
- package/web/assets/html-BlS82ilI.js +1 -0
- package/web/assets/index-A_AUJoO6.css +1 -0
- package/web/assets/index-BafYMKiN.js +396 -0
- package/web/assets/ini-BEwlwnbL.js +1 -0
- package/web/assets/java-CylS5w8V.js +1 -0
- package/web/assets/javascript-wDzz0qaB.js +1 -0
- package/web/assets/json-Cp-IABpG.js +1 -0
- package/web/assets/jsonc-Des-eS-w.js +1 -0
- package/web/assets/jsx-g9-lgVsj.js +1 -0
- package/web/assets/kotlin-BdnUsdx6.js +1 -0
- package/web/assets/less-B1dDrJ26.js +1 -0
- package/web/assets/lua-BaeVxFsk.js +1 -0
- package/web/assets/make-CHLpvVh8.js +1 -0
- package/web/assets/markdown-Cvjx9yec.js +1 -0
- package/web/assets/nix-CwoSXNpI.js +1 -0
- package/web/assets/objective-c-DXmwc3jG.js +1 -0
- package/web/assets/php-DoruYMF0.js +1 -0
- package/web/assets/pierre-dark-CyvmCCZW.js +1 -0
- package/web/assets/pierre-dark-VKo95ZgA.js +1 -0
- package/web/assets/pierre-light-480U9XYS.js +1 -0
- package/web/assets/pierre-light-BVSW-Xev.js +1 -0
- package/web/assets/postcss-DnzrXTVv.js +1 -0
- package/web/assets/powershell-Dd0kTxKP.js +1 -0
- package/web/assets/prisma-Vru482bI.js +1 -0
- package/web/assets/proto-C7zT0LnQ.js +1 -0
- package/web/assets/python-B6aJPvgy.js +1 -0
- package/web/assets/ruby-B43yHlf9.js +1 -0
- package/web/assets/rust-DPvrY3IQ.js +1 -0
- package/web/assets/scala-CqE71os6.js +1 -0
- package/web/assets/scss-Cw6XMfVs.js +1 -0
- package/web/assets/shellscript-Yzrsuije.js +1 -0
- package/web/assets/shiki-BwyCm3kZ.js +2 -0
- package/web/assets/shiki-langs-CMlALLSg.js +2 -0
- package/web/assets/shiki-wasm-CgFRq91U.js +1 -0
- package/web/assets/sql-DkvoVjww.js +1 -0
- package/web/assets/stream-tokenizer-BlgIhw0y.js +4 -0
- package/web/assets/svelte-DoIXUgJc.js +1 -0
- package/web/assets/swift-C2oV4EkX.js +1 -0
- package/web/assets/terraform-BETggiCN.js +1 -0
- package/web/assets/theming-themes-DDvoqEqT.js +1 -0
- package/web/assets/toml-vGWfd6FD.js +1 -0
- package/web/assets/tsx-COt5Ahok.js +1 -0
- package/web/assets/typescript-BPQ3VLAy.js +1 -0
- package/web/assets/vue-CwSeJimo.js +1 -0
- package/web/assets/xml-sdJ4AIDG.js +1 -0
- package/web/assets/xterm-view-6GBZ9nXN.css +32 -0
- package/web/assets/xterm-view-BnKOnF3C.js +38 -0
- package/web/assets/yaml-Buea-lGh.js +1 -0
- package/web/assets/zig-VOosw3JB.js +1 -0
- package/web/index.html +2 -2
- package/dist/chunk-etf7ef97.js +0 -55
- package/dist/chunk-greb3dp6.js +0 -22
- package/web/assets/index-BhHrKpoo.js +0 -395
- package/web/assets/index-DoyHMPwf.css +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
serveWebClient
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-445cevpm.js";
|
|
5
5
|
import {
|
|
6
6
|
RELAY_CLOSE,
|
|
7
7
|
decodeHostToRelay,
|
|
@@ -9,14 +9,14 @@ import {
|
|
|
9
9
|
relayWsBase,
|
|
10
10
|
verifyRelayChallenge,
|
|
11
11
|
verifyRelayChallengeV2
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-g3ahp7b9.js";
|
|
13
13
|
import {
|
|
14
14
|
normalizeHostName
|
|
15
15
|
} from "./chunk-pqe7q1s7.js";
|
|
16
16
|
import {
|
|
17
17
|
commandHint,
|
|
18
18
|
webRoot
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-s0h7c7da.js";
|
|
20
20
|
import"./chunk-df32avw6.js";
|
|
21
21
|
|
|
22
22
|
// apps/server/src/main.ts
|
|
@@ -58,13 +58,14 @@ async function lockDataDir(dir, options) {
|
|
|
58
58
|
|
|
59
59
|
// apps/server/src/accounts.ts
|
|
60
60
|
var DB_FILE = "server.sqlite";
|
|
61
|
-
var SCHEMA_VERSION =
|
|
61
|
+
var SCHEMA_VERSION = 8;
|
|
62
62
|
var SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
63
63
|
var JOIN_TOKEN_TTL_MS = 10 * 60 * 1000;
|
|
64
64
|
var USER_CODE_ALPHABET = "BCDFGHJKLMNPQRSTVWXZ";
|
|
65
65
|
var USER_CODE_LENGTH = 8;
|
|
66
66
|
var HOST_LINK_TTL_MS = JOIN_TOKEN_TTL_MS;
|
|
67
67
|
var HOST_LINK_INTERVAL_S = 5;
|
|
68
|
+
var HOST_LINK_CODE_TTL_MS = 2 * 60000;
|
|
68
69
|
var SLOW_DOWN_STEP_S = 5;
|
|
69
70
|
var POLL_SLACK_MS = 1000;
|
|
70
71
|
var HOST_OS_MAX_CODEPOINTS = 64;
|
|
@@ -114,6 +115,11 @@ var MAX_USERNAME_CODEPOINTS = 254;
|
|
|
114
115
|
var scryptAsync = promisify(scrypt);
|
|
115
116
|
var DUMMY_HASH = `scrypt$${SCRYPT.N}$${SCRYPT.r}$${SCRYPT.p}$${"A".repeat(22)}$${"A".repeat(43)}`;
|
|
116
117
|
var sha256Hex = (input) => createHash("sha256").update(input, "utf8").digest("hex");
|
|
118
|
+
var safeEqual = (a, b) => {
|
|
119
|
+
const left = Buffer.from(a);
|
|
120
|
+
const right = Buffer.from(b);
|
|
121
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
122
|
+
};
|
|
117
123
|
async function hashPassword(password) {
|
|
118
124
|
const salt = randomBytes(SALT_LEN);
|
|
119
125
|
const hash = await scryptAsync(password, salt, KEY_LEN, SCRYPT);
|
|
@@ -239,6 +245,32 @@ function migrate(db) {
|
|
|
239
245
|
COMMIT;
|
|
240
246
|
`);
|
|
241
247
|
}
|
|
248
|
+
if (version < 7) {
|
|
249
|
+
db.exec(`
|
|
250
|
+
BEGIN;
|
|
251
|
+
CREATE TABLE removed_hosts (
|
|
252
|
+
host_id TEXT PRIMARY KEY,
|
|
253
|
+
removed_at INTEGER NOT NULL
|
|
254
|
+
);
|
|
255
|
+
PRAGMA user_version = 7;
|
|
256
|
+
COMMIT;
|
|
257
|
+
`);
|
|
258
|
+
}
|
|
259
|
+
if (version < 8) {
|
|
260
|
+
db.exec(`
|
|
261
|
+
BEGIN;
|
|
262
|
+
ALTER TABLE host_link_requests ADD COLUMN request_id_hash TEXT;
|
|
263
|
+
ALTER TABLE host_link_requests ADD COLUMN redirect_uri TEXT;
|
|
264
|
+
ALTER TABLE host_link_requests ADD COLUMN code_challenge TEXT;
|
|
265
|
+
ALTER TABLE host_link_requests ADD COLUMN client_state TEXT;
|
|
266
|
+
ALTER TABLE host_link_requests ADD COLUMN auth_code_hash TEXT;
|
|
267
|
+
ALTER TABLE host_link_requests ADD COLUMN auth_code_expires_at INTEGER;
|
|
268
|
+
CREATE UNIQUE INDEX host_link_requests_request_id ON host_link_requests (request_id_hash);
|
|
269
|
+
CREATE UNIQUE INDEX host_link_requests_auth_code ON host_link_requests (auth_code_hash);
|
|
270
|
+
PRAGMA user_version = 8;
|
|
271
|
+
COMMIT;
|
|
272
|
+
`);
|
|
273
|
+
}
|
|
242
274
|
}
|
|
243
275
|
function migrateTo4(db) {
|
|
244
276
|
db.exec("BEGIN");
|
|
@@ -329,6 +361,12 @@ function migrateTo1(db) {
|
|
|
329
361
|
COMMIT;
|
|
330
362
|
`);
|
|
331
363
|
}
|
|
364
|
+
var loopbackRedirect = (redirectUri, params) => {
|
|
365
|
+
const url = new URL(redirectUri);
|
|
366
|
+
for (const [key, value] of Object.entries(params))
|
|
367
|
+
url.searchParams.set(key, value);
|
|
368
|
+
return url.toString();
|
|
369
|
+
};
|
|
332
370
|
|
|
333
371
|
class WindowedCounter {
|
|
334
372
|
map = new Map;
|
|
@@ -447,6 +485,8 @@ async function openAccounts(options) {
|
|
|
447
485
|
const linkStarts = new WindowedCounter(linkLimits.counterMaxEntries, now);
|
|
448
486
|
const linkUserFailures = new WindowedCounter(linkLimits.counterMaxEntries, now);
|
|
449
487
|
const linkIpFailures = new WindowedCounter(linkLimits.counterMaxEntries, now);
|
|
488
|
+
const PENDING_SELECT = `SELECT host_id AS hostId, host_name AS name, host_os AS os, source_ip AS sourceIp, expires_at AS expiresAt,
|
|
489
|
+
redirect_uri AS redirectUri, client_state AS clientState FROM host_link_requests`;
|
|
450
490
|
const sweepExpired = () => {
|
|
451
491
|
const at = now();
|
|
452
492
|
db.prepare("DELETE FROM join_tokens WHERE expires_at <= ?").run(at);
|
|
@@ -458,29 +498,47 @@ async function openAccounts(options) {
|
|
|
458
498
|
db.prepare("INSERT INTO join_tokens (token_hash, user_id, created_at, expires_at, host_id) VALUES (?, ?, ?, ?, ?)").run(sha256Hex(token), userId, now(), expiresAt, hostId);
|
|
459
499
|
return { token, expiresAt };
|
|
460
500
|
};
|
|
461
|
-
const
|
|
501
|
+
const pendingByRef = (userId, ref, ip) => {
|
|
502
|
+
if ("requestId" in ref) {
|
|
503
|
+
const hash = sha256Hex(ref.requestId);
|
|
504
|
+
const row2 = db.prepare(`${PENDING_SELECT} WHERE request_id_hash = ? AND state = 'pending' AND expires_at > ?`).get(hash, now());
|
|
505
|
+
return row2 === undefined ? "not_found" : { column: "request_id_hash", hash, row: row2 };
|
|
506
|
+
}
|
|
507
|
+
const { userCode } = ref;
|
|
462
508
|
const userKey = String(userId);
|
|
463
509
|
if (linkUserFailures.isOver(userKey, linkLimits.userMaxFailures) || linkIpFailures.isOver(ip, linkLimits.ipMaxFailures)) {
|
|
464
510
|
return "limited";
|
|
465
511
|
}
|
|
466
512
|
const code = normalizeUserCode(userCode);
|
|
467
513
|
const codeHash = code === undefined ? undefined : sha256Hex(code);
|
|
468
|
-
const row = codeHash === undefined ? undefined : db.prepare(
|
|
469
|
-
FROM host_link_requests WHERE user_code_hash = ? AND state = 'pending' AND expires_at > ?`).get(codeHash, now());
|
|
514
|
+
const row = codeHash === undefined ? undefined : db.prepare(`${PENDING_SELECT} WHERE user_code_hash = ? AND state = 'pending' AND expires_at > ?`).get(codeHash, now());
|
|
470
515
|
if (codeHash === undefined || row === undefined) {
|
|
471
516
|
linkUserFailures.bump(userKey, linkLimits.failureWindowMs);
|
|
472
517
|
linkIpFailures.bump(ip, linkLimits.failureWindowMs);
|
|
473
518
|
return "not_found";
|
|
474
519
|
}
|
|
475
|
-
return { codeHash, row };
|
|
520
|
+
return { column: "user_code_hash", hash: codeHash, row };
|
|
476
521
|
};
|
|
477
522
|
const settleHostLink = (args, state) => {
|
|
478
|
-
const found =
|
|
523
|
+
const found = pendingByRef(args.userId, args, args.ip);
|
|
479
524
|
if (found === "limited" || found === "not_found")
|
|
480
525
|
return { status: found };
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
526
|
+
const { column, hash, row } = found;
|
|
527
|
+
const at = now();
|
|
528
|
+
const loopback = row.redirectUri === null ? undefined : { redirectUri: row.redirectUri, state: row.clientState ?? "" };
|
|
529
|
+
if (loopback !== undefined && state === "denied") {
|
|
530
|
+
const dropped = db.prepare(`DELETE FROM host_link_requests WHERE ${column} = ? AND state = 'pending' AND expires_at > ?`).run(hash, at);
|
|
531
|
+
if (Number(dropped.changes) !== 1)
|
|
532
|
+
return { status: "not_found" };
|
|
533
|
+
return { status: "ok", redirect: loopbackRedirect(loopback.redirectUri, { error: "access_denied", state: loopback.state }) };
|
|
534
|
+
}
|
|
535
|
+
const code = loopback === undefined ? undefined : randomBytes(32).toString("base64url");
|
|
536
|
+
const settled = db.prepare(`UPDATE host_link_requests SET state = ?, user_id = ?, host_name = NULL, host_os = NULL, source_ip = NULL,
|
|
537
|
+
auth_code_hash = ?, auth_code_expires_at = ?
|
|
538
|
+
WHERE ${column} = ? AND state = 'pending' AND expires_at > ?`).run(state, state === "confirmed" ? args.userId : null, code === undefined ? null : sha256Hex(code), code === undefined ? null : at + HOST_LINK_CODE_TTL_MS, hash, at);
|
|
539
|
+
if (Number(settled.changes) !== 1)
|
|
540
|
+
return { status: "not_found" };
|
|
541
|
+
return loopback === undefined || code === undefined ? { status: "ok" } : { status: "ok", redirect: loopbackRedirect(loopback.redirectUri, { code, state: loopback.state }) };
|
|
484
542
|
};
|
|
485
543
|
const deviceForSignIn = (userId, claim) => {
|
|
486
544
|
const at = now();
|
|
@@ -777,7 +835,7 @@ async function openAccounts(options) {
|
|
|
777
835
|
sweepExpired();
|
|
778
836
|
return mintJoinToken(userId, null);
|
|
779
837
|
},
|
|
780
|
-
startHostLink({ hostId, name, os, ip }) {
|
|
838
|
+
startHostLink({ hostId, name, os, ip, loopback }) {
|
|
781
839
|
if (linkStarts.hit(ip, linkLimits.startMaxPerIp, linkLimits.startWindowMs))
|
|
782
840
|
return { status: "limited" };
|
|
783
841
|
sweepExpired();
|
|
@@ -787,14 +845,24 @@ async function openAccounts(options) {
|
|
|
787
845
|
const deviceCode = randomBytes(32).toString("base64url");
|
|
788
846
|
const at = now();
|
|
789
847
|
const expiresAt = at + HOST_LINK_TTL_MS;
|
|
848
|
+
const hostName = normalizeHostName(name);
|
|
849
|
+
const hostOs = [...os.trim()].slice(0, HOST_OS_MAX_CODEPOINTS).join("");
|
|
850
|
+
if (loopback !== undefined) {
|
|
851
|
+
const requestId = randomBytes(32).toString("base64url");
|
|
852
|
+
db.prepare(`INSERT INTO host_link_requests
|
|
853
|
+
(device_code_hash, user_code_hash, request_id_hash, redirect_uri, code_challenge, client_state,
|
|
854
|
+
host_id, host_name, host_os, source_ip, interval_s, created_at, expires_at)
|
|
855
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(sha256Hex(randomBytes(32).toString("base64url")), sha256Hex(randomBytes(32).toString("base64url")), sha256Hex(requestId), loopback.redirectUri, loopback.codeChallenge, loopback.state, hostId, hostName, hostOs, ip, HOST_LINK_INTERVAL_S, at, expiresAt);
|
|
856
|
+
return { status: "ok", mode: "loopback", requestId, expiresAt };
|
|
857
|
+
}
|
|
790
858
|
const insert = db.prepare(`INSERT INTO host_link_requests
|
|
791
859
|
(device_code_hash, user_code_hash, host_id, host_name, host_os, source_ip, interval_s, created_at, expires_at)
|
|
792
860
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
793
861
|
for (let attempt = 0;; attempt++) {
|
|
794
862
|
const code = Array.from({ length: USER_CODE_LENGTH }, () => USER_CODE_ALPHABET[randomInt(USER_CODE_ALPHABET.length)]).join("");
|
|
795
863
|
try {
|
|
796
|
-
insert.run(sha256Hex(deviceCode), sha256Hex(code), hostId,
|
|
797
|
-
return { status: "ok", deviceCode, userCode: formatUserCode(code), expiresAt, interval: HOST_LINK_INTERVAL_S };
|
|
864
|
+
insert.run(sha256Hex(deviceCode), sha256Hex(code), hostId, hostName, hostOs, ip, HOST_LINK_INTERVAL_S, at, expiresAt);
|
|
865
|
+
return { status: "ok", mode: "code", deviceCode, userCode: formatUserCode(code), expiresAt, interval: HOST_LINK_INTERVAL_S };
|
|
798
866
|
} catch (error) {
|
|
799
867
|
if (attempt >= 4 || !String(error).includes("UNIQUE"))
|
|
800
868
|
throw error;
|
|
@@ -830,8 +898,9 @@ async function openAccounts(options) {
|
|
|
830
898
|
throw error;
|
|
831
899
|
}
|
|
832
900
|
},
|
|
833
|
-
lookupHostLink(
|
|
834
|
-
const
|
|
901
|
+
lookupHostLink(args) {
|
|
902
|
+
const { userId, ip } = args;
|
|
903
|
+
const found = pendingByRef(userId, args, ip);
|
|
835
904
|
if (found === "limited" || found === "not_found")
|
|
836
905
|
return { status: found };
|
|
837
906
|
const { row } = found;
|
|
@@ -853,6 +922,24 @@ async function openAccounts(options) {
|
|
|
853
922
|
denyHostLink(args) {
|
|
854
923
|
return settleHostLink(args, "denied");
|
|
855
924
|
},
|
|
925
|
+
exchangeHostLinkCode(code, codeVerifier) {
|
|
926
|
+
const codeHash = sha256Hex(code);
|
|
927
|
+
const at = now();
|
|
928
|
+
db.exec("BEGIN IMMEDIATE");
|
|
929
|
+
try {
|
|
930
|
+
const row = db.prepare(`SELECT host_id AS hostId, user_id AS userId, code_challenge AS challenge, auth_code_expires_at AS codeExpiresAt
|
|
931
|
+
FROM host_link_requests WHERE auth_code_hash = ? AND state = 'confirmed'`).get(codeHash);
|
|
932
|
+
if (row !== undefined)
|
|
933
|
+
db.prepare("DELETE FROM host_link_requests WHERE auth_code_hash = ?").run(codeHash);
|
|
934
|
+
const valid = row !== undefined && row.userId !== null && row.challenge !== null && row.codeExpiresAt !== null && row.codeExpiresAt > at && safeEqual(createHash("sha256").update(codeVerifier).digest("base64url"), row.challenge);
|
|
935
|
+
const result = valid ? { status: "ok", ...mintJoinToken(row.userId, row.hostId) } : { status: "invalid" };
|
|
936
|
+
db.exec("COMMIT");
|
|
937
|
+
return result;
|
|
938
|
+
} catch (error) {
|
|
939
|
+
db.exec("ROLLBACK");
|
|
940
|
+
throw error;
|
|
941
|
+
}
|
|
942
|
+
},
|
|
856
943
|
sweepExpired,
|
|
857
944
|
joinHost({ token, hostId, noisePublicKey, name }) {
|
|
858
945
|
const tokenHash = sha256Hex(token);
|
|
@@ -888,6 +975,7 @@ async function openAccounts(options) {
|
|
|
888
975
|
name = excluded.name,
|
|
889
976
|
joined_at = excluded.joined_at,
|
|
890
977
|
last_seen_at = excluded.last_seen_at`).run(hostId, row.userId, noisePublicKey, hostName, at, at);
|
|
978
|
+
db.prepare("DELETE FROM removed_hosts WHERE host_id = ?").run(hostId);
|
|
891
979
|
const user = db.prepare("SELECT username FROM users WHERE id = ?").get(row.userId);
|
|
892
980
|
db.exec("COMMIT");
|
|
893
981
|
return { status: "joined", username: user?.username ?? "unknown" };
|
|
@@ -899,6 +987,37 @@ async function openAccounts(options) {
|
|
|
899
987
|
leaveHost(hostId) {
|
|
900
988
|
return Number(db.prepare("DELETE FROM hosts WHERE host_id = ?").run(hostId).changes) > 0;
|
|
901
989
|
},
|
|
990
|
+
async removeHost({ userId, username, password, hostId }) {
|
|
991
|
+
const owned = () => db.prepare("SELECT 1 AS x FROM hosts WHERE host_id = ? AND user_id = ?").get(hostId, userId) !== undefined;
|
|
992
|
+
if (!owned())
|
|
993
|
+
return { status: "refused", reason: "not_found" };
|
|
994
|
+
const checked = await checkPassword(userId, username, password);
|
|
995
|
+
if (checked === "limited")
|
|
996
|
+
return { status: "limited" };
|
|
997
|
+
if (checked === "wrong_password")
|
|
998
|
+
return { status: "refused", reason: "wrong_password" };
|
|
999
|
+
db.exec("BEGIN IMMEDIATE");
|
|
1000
|
+
try {
|
|
1001
|
+
if (currentHash(userId) !== checked.verifiedHash) {
|
|
1002
|
+
db.exec("ROLLBACK");
|
|
1003
|
+
return { status: "refused", reason: "wrong_password" };
|
|
1004
|
+
}
|
|
1005
|
+
if (!owned()) {
|
|
1006
|
+
db.exec("ROLLBACK");
|
|
1007
|
+
return { status: "refused", reason: "not_found" };
|
|
1008
|
+
}
|
|
1009
|
+
db.prepare("DELETE FROM hosts WHERE host_id = ?").run(hostId);
|
|
1010
|
+
db.prepare("INSERT INTO removed_hosts (host_id, removed_at) VALUES (?, ?) ON CONFLICT(host_id) DO UPDATE SET removed_at = excluded.removed_at").run(hostId, now());
|
|
1011
|
+
db.exec("COMMIT");
|
|
1012
|
+
return { status: "ok" };
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
db.exec("ROLLBACK");
|
|
1015
|
+
throw error;
|
|
1016
|
+
}
|
|
1017
|
+
},
|
|
1018
|
+
wasRemoved(hostId) {
|
|
1019
|
+
return db.prepare("SELECT 1 AS x FROM removed_hosts WHERE host_id = ?").get(hostId) !== undefined;
|
|
1020
|
+
},
|
|
902
1021
|
listHosts(userId) {
|
|
903
1022
|
return db.prepare(`SELECT host_id AS hostId, user_id AS userId, noise_public_key AS noisePublicKey,
|
|
904
1023
|
name, joined_at AS joinedAt, last_seen_at AS lastSeenAt, device_reports AS deviceReports
|
|
@@ -1422,6 +1541,27 @@ function createRelayLimiter(limits, now = Date.now) {
|
|
|
1422
1541
|
// apps/server/src/api.ts
|
|
1423
1542
|
var SESSION_COOKIE = "__Host-pinomad-session";
|
|
1424
1543
|
var HOST_ID_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
1544
|
+
var PKCE_CHALLENGE = /^[A-Za-z0-9_-]{43}$/;
|
|
1545
|
+
var PKCE_VERIFIER = /^[A-Za-z0-9._~-]{43,128}$/;
|
|
1546
|
+
var LOOPBACK_STATE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
1547
|
+
var MAX_REDIRECT_URI = 256;
|
|
1548
|
+
function parseLoopback(value) {
|
|
1549
|
+
const { redirectUri, codeChallenge, state } = value;
|
|
1550
|
+
if (typeof redirectUri !== "string" || redirectUri.length > MAX_REDIRECT_URI)
|
|
1551
|
+
return null;
|
|
1552
|
+
if (typeof codeChallenge !== "string" || !PKCE_CHALLENGE.test(codeChallenge))
|
|
1553
|
+
return null;
|
|
1554
|
+
if (typeof state !== "string" || !LOOPBACK_STATE.test(state))
|
|
1555
|
+
return null;
|
|
1556
|
+
let url;
|
|
1557
|
+
try {
|
|
1558
|
+
url = new URL(redirectUri);
|
|
1559
|
+
} catch {
|
|
1560
|
+
return null;
|
|
1561
|
+
}
|
|
1562
|
+
const loopbackOnly = url.protocol === "http:" && url.hostname === "127.0.0.1" && url.port !== "" && url.username === "" && url.password === "" && url.search === "" && url.hash === "" && url.href === redirectUri;
|
|
1563
|
+
return loopbackOnly ? { redirectUri, codeChallenge, state } : null;
|
|
1564
|
+
}
|
|
1425
1565
|
var MAX_BODY = 8 * 1024;
|
|
1426
1566
|
var MUTATING = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
1427
1567
|
var DESKTOP_ORIGIN = "app://pinomad";
|
|
@@ -1469,6 +1609,15 @@ async function readJson(request, optional = false) {
|
|
|
1469
1609
|
return { ok: false, tooLarge: false };
|
|
1470
1610
|
}
|
|
1471
1611
|
}
|
|
1612
|
+
async function authedJson(accounts, request, token, optional = false) {
|
|
1613
|
+
const body = await readJson(request, optional);
|
|
1614
|
+
if (!body.ok)
|
|
1615
|
+
return { ok: false, status: body.tooLarge ? 413 : 400, error: body.tooLarge ? "payload_too_large" : "invalid_json" };
|
|
1616
|
+
const session = accounts.authenticate(token);
|
|
1617
|
+
if (session === undefined)
|
|
1618
|
+
return { ok: false, status: 401, error: "unauthenticated" };
|
|
1619
|
+
return { ok: true, value: body.value, session };
|
|
1620
|
+
}
|
|
1472
1621
|
function sessionToken(request) {
|
|
1473
1622
|
const authorization = request.headers.authorization;
|
|
1474
1623
|
if (authorization !== undefined && authorization.startsWith("Bearer "))
|
|
@@ -1512,7 +1661,7 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1512
1661
|
}
|
|
1513
1662
|
const bearer = (request.headers.authorization ?? "").startsWith("Bearer ");
|
|
1514
1663
|
const loginTokenMode = (pathname === "/api/login" || pathname === "/api/register") && (origin === undefined || origin === DESKTOP_ORIGIN);
|
|
1515
|
-
const hostLinkHostMode = (pathname === "/api/host-link/start" || pathname === "/api/host-link/poll") && origin === undefined;
|
|
1664
|
+
const hostLinkHostMode = (pathname === "/api/host-link/start" || pathname === "/api/host-link/poll" || pathname === "/api/host-link/token") && origin === undefined;
|
|
1516
1665
|
if (MUTATING.has(method) && !bearer && !loginTokenMode && !hostLinkHostMode && origin !== ctx.publicOrigin) {
|
|
1517
1666
|
deny(403, "origin_mismatch");
|
|
1518
1667
|
return;
|
|
@@ -1593,7 +1742,7 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1593
1742
|
send(200, { user: { username }, expiresAt: result.expiresAt, ...cookieMode ? {} : { token: result.token } }, cookieMode ? { "set-cookie": setSessionCookie(result.token) } : undefined);
|
|
1594
1743
|
return;
|
|
1595
1744
|
}
|
|
1596
|
-
if (pathname === "/api/host-link/start" || pathname === "/api/host-link/poll") {
|
|
1745
|
+
if (pathname === "/api/host-link/start" || pathname === "/api/host-link/poll" || pathname === "/api/host-link/token") {
|
|
1597
1746
|
if (method !== "POST") {
|
|
1598
1747
|
deny(405, "method_not_allowed", { allow: "POST" });
|
|
1599
1748
|
return;
|
|
@@ -1605,7 +1754,8 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1605
1754
|
}
|
|
1606
1755
|
if (pathname === "/api/host-link/start") {
|
|
1607
1756
|
const value = body.value;
|
|
1608
|
-
|
|
1757
|
+
const loopback = value?.redirectUri === undefined ? undefined : parseLoopback(value);
|
|
1758
|
+
if (typeof value?.hostId !== "string" || !HOST_ID_PATTERN.test(value.hostId) || typeof value.name !== "string" || typeof value.os !== "string" || loopback === null) {
|
|
1609
1759
|
deny(400, "invalid_request");
|
|
1610
1760
|
return;
|
|
1611
1761
|
}
|
|
@@ -1613,7 +1763,8 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1613
1763
|
hostId: value.hostId,
|
|
1614
1764
|
name: value.name,
|
|
1615
1765
|
os: value.os,
|
|
1616
|
-
ip: clientIp(request, ctx.trustProxy)
|
|
1766
|
+
ip: clientIp(request, ctx.trustProxy),
|
|
1767
|
+
...loopback === undefined ? {} : { loopback }
|
|
1617
1768
|
});
|
|
1618
1769
|
if (result2.status === "limited") {
|
|
1619
1770
|
deny(429, "rate_limited");
|
|
@@ -1623,6 +1774,14 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1623
1774
|
deny(503, "busy");
|
|
1624
1775
|
return;
|
|
1625
1776
|
}
|
|
1777
|
+
if (result2.mode === "loopback") {
|
|
1778
|
+
send(200, {
|
|
1779
|
+
requestId: result2.requestId,
|
|
1780
|
+
verifyUrl: `${ctx.publicOrigin}/host-link?request=${result2.requestId}`,
|
|
1781
|
+
expiresAt: result2.expiresAt
|
|
1782
|
+
});
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1626
1785
|
send(200, {
|
|
1627
1786
|
deviceCode: result2.deviceCode,
|
|
1628
1787
|
userCode: result2.userCode,
|
|
@@ -1632,6 +1791,19 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1632
1791
|
});
|
|
1633
1792
|
return;
|
|
1634
1793
|
}
|
|
1794
|
+
if (pathname === "/api/host-link/token") {
|
|
1795
|
+
const value = body.value;
|
|
1796
|
+
if (typeof value?.code !== "string" || value.code.length > 256 || typeof value.codeVerifier !== "string" || !PKCE_VERIFIER.test(value.codeVerifier)) {
|
|
1797
|
+
deny(400, "invalid_request");
|
|
1798
|
+
return;
|
|
1799
|
+
}
|
|
1800
|
+
const result2 = accounts.exchangeHostLinkCode(value.code, value.codeVerifier);
|
|
1801
|
+
if (result2.status === "invalid")
|
|
1802
|
+
deny(400, "invalid_grant");
|
|
1803
|
+
else
|
|
1804
|
+
send(200, { token: result2.token, expiresAt: result2.expiresAt });
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1635
1807
|
const deviceCode = body.value?.deviceCode;
|
|
1636
1808
|
if (typeof deviceCode !== "string" || deviceCode.length > 256) {
|
|
1637
1809
|
deny(400, "invalid_request");
|
|
@@ -1666,22 +1838,18 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1666
1838
|
deny(401, "unauthenticated");
|
|
1667
1839
|
return;
|
|
1668
1840
|
}
|
|
1669
|
-
const body = await
|
|
1841
|
+
const body = await authedJson(accounts, request, token);
|
|
1670
1842
|
if (!body.ok) {
|
|
1671
|
-
deny(body.
|
|
1843
|
+
deny(body.status, body.error);
|
|
1672
1844
|
return;
|
|
1673
1845
|
}
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1846
|
+
const named = body.value;
|
|
1847
|
+
const ref = typeof named?.userCode === "string" && named.userCode.length <= 64 && named.requestId === undefined ? { userCode: named.userCode } : typeof named?.requestId === "string" && named.requestId.length <= 64 && named.userCode === undefined ? { requestId: named.requestId } : undefined;
|
|
1848
|
+
if (ref === undefined) {
|
|
1676
1849
|
deny(400, "invalid_request");
|
|
1677
1850
|
return;
|
|
1678
1851
|
}
|
|
1679
|
-
const
|
|
1680
|
-
if (session === undefined) {
|
|
1681
|
-
deny(401, "unauthenticated");
|
|
1682
|
-
return;
|
|
1683
|
-
}
|
|
1684
|
-
const args = { userId: session.userId, userCode, ip: clientIp(request, ctx.trustProxy) };
|
|
1852
|
+
const args = { userId: body.session.userId, ip: clientIp(request, ctx.trustProxy), ...ref };
|
|
1685
1853
|
const result = pathname === "/api/host-link/lookup" ? accounts.lookupHostLink(args) : pathname === "/api/host-link/confirm" ? accounts.confirmHostLink(args) : accounts.denyHostLink(args);
|
|
1686
1854
|
if (result.status === "limited") {
|
|
1687
1855
|
deny(429, "rate_limited");
|
|
@@ -1693,6 +1861,8 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1693
1861
|
}
|
|
1694
1862
|
if ("request" in result)
|
|
1695
1863
|
send(200, result.request);
|
|
1864
|
+
else if (result.redirect !== undefined)
|
|
1865
|
+
send(200, { redirect: result.redirect });
|
|
1696
1866
|
else
|
|
1697
1867
|
send(204);
|
|
1698
1868
|
return;
|
|
@@ -1720,9 +1890,13 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1720
1890
|
send(200, accounts.createInvite(session.userId));
|
|
1721
1891
|
return;
|
|
1722
1892
|
}
|
|
1723
|
-
const body = await
|
|
1893
|
+
const body = await authedJson(accounts, request, token);
|
|
1724
1894
|
if (!body.ok) {
|
|
1725
|
-
deny(body.
|
|
1895
|
+
deny(body.status, body.error);
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
if (!body.session.admin) {
|
|
1899
|
+
deny(401, "unauthenticated");
|
|
1726
1900
|
return;
|
|
1727
1901
|
}
|
|
1728
1902
|
const id = body.value?.id;
|
|
@@ -1730,16 +1904,52 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1730
1904
|
deny(400, "invalid_request");
|
|
1731
1905
|
return;
|
|
1732
1906
|
}
|
|
1733
|
-
if (accounts.authenticate(token)?.admin !== true) {
|
|
1734
|
-
deny(401, "unauthenticated");
|
|
1735
|
-
return;
|
|
1736
|
-
}
|
|
1737
1907
|
if (accounts.revokeInvite(id))
|
|
1738
1908
|
send(204);
|
|
1739
1909
|
else
|
|
1740
1910
|
deny(404, "not_found");
|
|
1741
1911
|
return;
|
|
1742
1912
|
}
|
|
1913
|
+
const hostRemove = /^\/api\/hosts\/([A-Za-z0-9_-]{43})\/remove$/.exec(pathname);
|
|
1914
|
+
if (hostRemove !== null) {
|
|
1915
|
+
if (method !== "POST") {
|
|
1916
|
+
deny(405, "method_not_allowed", { allow: "POST" });
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
const token = sessionToken(request);
|
|
1920
|
+
if (token === undefined || accounts.authenticate(token) === undefined) {
|
|
1921
|
+
deny(401, "unauthenticated");
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
const body = await authedJson(accounts, request, token);
|
|
1925
|
+
if (!body.ok) {
|
|
1926
|
+
deny(body.status, body.error);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
const value = body.value;
|
|
1930
|
+
if (typeof value?.password !== "string" || [...value.password].length > MAX_PASSWORD_CODEPOINTS) {
|
|
1931
|
+
deny(400, "invalid_request");
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
const current = body.session;
|
|
1935
|
+
const result = await accounts.removeHost({
|
|
1936
|
+
userId: current.userId,
|
|
1937
|
+
username: current.username,
|
|
1938
|
+
password: value.password,
|
|
1939
|
+
hostId: hostRemove[1]
|
|
1940
|
+
});
|
|
1941
|
+
if (result.status === "limited") {
|
|
1942
|
+
deny(429, "rate_limited");
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
if (result.status === "refused") {
|
|
1946
|
+
deny(result.reason === "not_found" ? 404 : 403, result.reason);
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
ctx.disconnectHost(hostRemove[1], true);
|
|
1950
|
+
send(204);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1743
1953
|
const hostPairing = /^\/api\/hosts\/([A-Za-z0-9_-]{43})\/pairing$/.exec(pathname);
|
|
1744
1954
|
if (pathname === "/api/hosts" || hostPairing !== null) {
|
|
1745
1955
|
const expected = pathname === "/api/hosts" ? "GET" : "POST";
|
|
@@ -1834,9 +2044,9 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1834
2044
|
send(200, accounts.listDevices(session.userId).map((device) => ({ ...device, current: device.id === session.deviceId })));
|
|
1835
2045
|
return;
|
|
1836
2046
|
}
|
|
1837
|
-
const body = await
|
|
2047
|
+
const body = await authedJson(accounts, request, token);
|
|
1838
2048
|
if (!body.ok) {
|
|
1839
|
-
deny(body.
|
|
2049
|
+
deny(body.status, body.error);
|
|
1840
2050
|
return;
|
|
1841
2051
|
}
|
|
1842
2052
|
const value = body.value;
|
|
@@ -1844,11 +2054,7 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1844
2054
|
deny(400, "invalid_request");
|
|
1845
2055
|
return;
|
|
1846
2056
|
}
|
|
1847
|
-
const current =
|
|
1848
|
-
if (current === undefined) {
|
|
1849
|
-
deny(401, "unauthenticated");
|
|
1850
|
-
return;
|
|
1851
|
-
}
|
|
2057
|
+
const current = body.session;
|
|
1852
2058
|
const result = await accounts.kickDevice({
|
|
1853
2059
|
userId: current.userId,
|
|
1854
2060
|
username: current.username,
|
|
@@ -1886,9 +2092,9 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1886
2092
|
return;
|
|
1887
2093
|
}
|
|
1888
2094
|
if (pathname === "/api/push-tokens" || pathname === "/api/push-tokens/remove") {
|
|
1889
|
-
const body = await
|
|
2095
|
+
const body = await authedJson(accounts, request, token);
|
|
1890
2096
|
if (!body.ok) {
|
|
1891
|
-
deny(body.
|
|
2097
|
+
deny(body.status, body.error);
|
|
1892
2098
|
return;
|
|
1893
2099
|
}
|
|
1894
2100
|
const value = body.value;
|
|
@@ -1898,30 +2104,27 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1898
2104
|
deny(400, "invalid_request");
|
|
1899
2105
|
return;
|
|
1900
2106
|
}
|
|
1901
|
-
const current =
|
|
1902
|
-
if (current === undefined) {
|
|
1903
|
-
deny(401, "unauthenticated");
|
|
1904
|
-
return;
|
|
1905
|
-
}
|
|
2107
|
+
const current = body.session;
|
|
1906
2108
|
if (pathname === "/api/push-tokens")
|
|
1907
|
-
accounts.registerPushToken(
|
|
2109
|
+
accounts.registerPushToken(current.userId, apns, environment, current.deviceId);
|
|
1908
2110
|
else
|
|
1909
|
-
accounts.removePushToken(
|
|
2111
|
+
accounts.removePushToken(current.userId, apns, environment);
|
|
1910
2112
|
send(204);
|
|
1911
2113
|
return;
|
|
1912
2114
|
}
|
|
1913
2115
|
if (pathname === "/api/me/delete") {
|
|
1914
|
-
const body = await
|
|
2116
|
+
const body = await authedJson(accounts, request, token);
|
|
1915
2117
|
if (!body.ok) {
|
|
1916
|
-
deny(body.
|
|
2118
|
+
deny(body.status, body.error);
|
|
1917
2119
|
return;
|
|
1918
2120
|
}
|
|
2121
|
+
const current = body.session;
|
|
1919
2122
|
const value = body.value;
|
|
1920
2123
|
if (typeof value?.password !== "string" || [...value.password].length > MAX_PASSWORD_CODEPOINTS) {
|
|
1921
2124
|
deny(400, "invalid_request");
|
|
1922
2125
|
return;
|
|
1923
2126
|
}
|
|
1924
|
-
const result = await accounts.deleteAccount({ userId:
|
|
2127
|
+
const result = await accounts.deleteAccount({ userId: current.userId, username: current.username, password: value.password });
|
|
1925
2128
|
if (result.status === "limited") {
|
|
1926
2129
|
deny(429, "rate_limited");
|
|
1927
2130
|
return;
|
|
@@ -1932,24 +2135,25 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1932
2135
|
}
|
|
1933
2136
|
for (const hostId of result.hostIds)
|
|
1934
2137
|
ctx.disconnectHost(hostId);
|
|
1935
|
-
ctx.forgetAccount(
|
|
2138
|
+
ctx.forgetAccount(current.userId);
|
|
1936
2139
|
send(204, undefined, { "set-cookie": clearSessionCookie });
|
|
1937
2140
|
return;
|
|
1938
2141
|
}
|
|
1939
2142
|
if (pathname === "/api/password") {
|
|
1940
|
-
const body = await
|
|
2143
|
+
const body = await authedJson(accounts, request, token);
|
|
1941
2144
|
if (!body.ok) {
|
|
1942
|
-
deny(body.
|
|
2145
|
+
deny(body.status, body.error);
|
|
1943
2146
|
return;
|
|
1944
2147
|
}
|
|
2148
|
+
const current = body.session;
|
|
1945
2149
|
const value = body.value;
|
|
1946
2150
|
if (typeof value?.currentPassword !== "string" || typeof value.newPassword !== "string" || [...value.currentPassword].length > MAX_PASSWORD_CODEPOINTS) {
|
|
1947
2151
|
deny(400, "invalid_request");
|
|
1948
2152
|
return;
|
|
1949
2153
|
}
|
|
1950
2154
|
const result = await accounts.changePassword({
|
|
1951
|
-
userId:
|
|
1952
|
-
username:
|
|
2155
|
+
userId: current.userId,
|
|
2156
|
+
username: current.username,
|
|
1953
2157
|
keepToken: token,
|
|
1954
2158
|
currentPassword: value.currentPassword,
|
|
1955
2159
|
newPassword: value.newPassword
|
|
@@ -1975,9 +2179,9 @@ async function handleApiRequest(request, response, ctx) {
|
|
|
1975
2179
|
return;
|
|
1976
2180
|
}
|
|
1977
2181
|
if (pathname === "/api/logout") {
|
|
1978
|
-
const body = await
|
|
2182
|
+
const body = await authedJson(accounts, request, token, true);
|
|
1979
2183
|
if (!body.ok) {
|
|
1980
|
-
deny(body.
|
|
2184
|
+
deny(body.status, body.error);
|
|
1981
2185
|
return;
|
|
1982
2186
|
}
|
|
1983
2187
|
const value = body.value;
|
|
@@ -2118,8 +2322,8 @@ async function startRelay(options) {
|
|
|
2118
2322
|
});
|
|
2119
2323
|
}
|
|
2120
2324
|
const pairing = { status: pairingStatus, request: requestPairing };
|
|
2121
|
-
function disconnectHost(hostId) {
|
|
2122
|
-
controls.get(hostId)?.socket.close(RELAY_CLOSE.hostNotAllowed);
|
|
2325
|
+
function disconnectHost(hostId, removed = false) {
|
|
2326
|
+
controls.get(hostId)?.socket.close(removed ? RELAY_CLOSE.hostRemoved : RELAY_CLOSE.hostNotAllowed);
|
|
2123
2327
|
closeHostDevices(hostId, RELAY_CLOSE.hostNotAllowed, "host left its account");
|
|
2124
2328
|
}
|
|
2125
2329
|
function closeHostDevices(hostId, code, reason) {
|
|
@@ -2306,7 +2510,7 @@ async function startRelay(options) {
|
|
|
2306
2510
|
return;
|
|
2307
2511
|
}
|
|
2308
2512
|
if (accounts?.hasHost(frame.hostId) !== true) {
|
|
2309
|
-
socket.close(RELAY_CLOSE.hostNotAllowed);
|
|
2513
|
+
socket.close(accounts?.wasRemoved(frame.hostId) === true ? RELAY_CLOSE.hostRemoved : RELAY_CLOSE.hostNotAllowed);
|
|
2310
2514
|
return;
|
|
2311
2515
|
}
|
|
2312
2516
|
hostId = frame.hostId;
|