@tapflowio/relay 0.20.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/auth.d.ts +2 -2
- package/dist/api/auth.d.ts.map +1 -1
- package/dist/api/auth.js +22 -33
- package/dist/api/auth.js.map +1 -1
- package/dist/lib/adminAccount.d.ts +24 -0
- package/dist/lib/adminAccount.d.ts.map +1 -0
- package/dist/lib/adminAccount.js +61 -0
- package/dist/lib/adminAccount.js.map +1 -0
- package/dist/lib/adminBootstrap.d.ts +48 -0
- package/dist/lib/adminBootstrap.d.ts.map +1 -0
- package/dist/lib/adminBootstrap.js +68 -0
- package/dist/lib/adminBootstrap.js.map +1 -0
- package/dist/lib/config.d.ts +4 -4
- package/dist/lib/loadEnvFile.d.ts +1 -1
- package/dist/lib/loadEnvFile.d.ts.map +1 -1
- package/dist/lib/loadEnvFile.js +11 -1
- package/dist/lib/loadEnvFile.js.map +1 -1
- package/dist/server.js +22 -0
- package/dist/server.js.map +1 -1
- package/package.json +6 -6
- package/public/assets/{AppCenter-Dg4dscW-.js → AppCenter-DtF_g4F7.js} +5 -5
- package/public/assets/AppCenter-DtF_g4F7.js.br +0 -0
- package/public/assets/{Default-Dxi8aZ1t.js → Default-C_p-ZWS6.js} +1 -1
- package/public/assets/Default-C_p-ZWS6.js.br +0 -0
- package/public/assets/{Invite-0EwoClX6.js → Invite-DKT4qT6M.js} +1 -1
- package/public/assets/Invite-DKT4qT6M.js.br +0 -0
- package/public/assets/MacResources-D8T_KpPV.js +1 -0
- package/public/assets/MacResources-D8T_KpPV.js.br +0 -0
- package/public/assets/QASession-Daup7RGP.js +140 -0
- package/public/assets/QASession-Daup7RGP.js.br +0 -0
- package/public/assets/{ResetPassword-CXU2bRry.js → ResetPassword-BnmE9MfY.js} +1 -1
- package/public/assets/ResetPassword-BnmE9MfY.js.br +0 -0
- package/public/assets/{Setup-XXChRxeL.js → Setup-I6nFcwNZ.js} +1 -1
- package/public/assets/Setup-I6nFcwNZ.js.br +0 -0
- package/public/assets/{Team-D0bXpmEY.js → Team-JhXhGt1H.js} +2 -2
- package/public/assets/Team-JhXhGt1H.js.br +0 -0
- package/public/assets/{Tokens-D0YNHS5e.js → Tokens-KAKIDZ0V.js} +1 -1
- package/public/assets/Tokens-KAKIDZ0V.js.br +0 -0
- package/public/assets/{alert-dialog-CgatMXjB.js → alert-dialog-CLhPOSdB.js} +1 -1
- package/public/assets/alert-dialog-CLhPOSdB.js.br +0 -0
- package/public/assets/{badge-B6Nf3A0Y.js → badge-CP-xbjo3.js} +1 -1
- package/public/assets/badge-CP-xbjo3.js.br +0 -0
- package/public/assets/{build-format-BFC8gLoh.js → build-format-DO1zN4vW.js} +3 -3
- package/public/assets/build-format-DO1zN4vW.js.br +0 -0
- package/public/assets/{dialog-DK9a3b4z.js → dialog-BGH9MPcH.js} +3 -3
- package/public/assets/dialog-BGH9MPcH.js.br +0 -0
- package/public/assets/index-O-Temo_p.js +198 -0
- package/public/assets/index-O-Temo_p.js.br +0 -0
- package/public/assets/{pencil-BeLNLEKe.js → pencil-0UYCdq7e.js} +2 -2
- package/public/assets/pencil-0UYCdq7e.js.br +0 -0
- package/public/assets/{table-DW57-o1p.js → table-4DnBixRm.js} +1 -1
- package/public/assets/table-4DnBixRm.js.br +0 -0
- package/public/assets/{tabs-Xlttuqxh.js → tabs-DwiCx3dV.js} +1 -1
- package/public/assets/tabs-DwiCx3dV.js.br +0 -0
- package/public/assets/{trash-2-CGxISX8x.js → trash-2-CAhMuWgg.js} +3 -3
- package/public/assets/trash-2-CAhMuWgg.js.br +0 -0
- package/public/assets/vendor-router-BFibuab1.js +12 -0
- package/public/assets/vendor-router-BFibuab1.js.br +0 -0
- package/public/index.html +2 -2
- package/public/index.html.br +0 -0
- package/public/assets/AppCenter-Dg4dscW-.js.br +0 -0
- package/public/assets/Default-Dxi8aZ1t.js.br +0 -0
- package/public/assets/Invite-0EwoClX6.js.br +0 -0
- package/public/assets/MacResources-Dy1M3EqK.js +0 -1
- package/public/assets/MacResources-Dy1M3EqK.js.br +0 -0
- package/public/assets/QASession-DqMj8JL1.js +0 -140
- package/public/assets/QASession-DqMj8JL1.js.br +0 -0
- package/public/assets/ResetPassword-CXU2bRry.js.br +0 -0
- package/public/assets/Setup-XXChRxeL.js.br +0 -0
- package/public/assets/Team-D0bXpmEY.js.br +0 -0
- package/public/assets/Tokens-D0YNHS5e.js.br +0 -0
- package/public/assets/alert-dialog-CgatMXjB.js.br +0 -0
- package/public/assets/badge-B6Nf3A0Y.js.br +0 -0
- package/public/assets/build-format-BFC8gLoh.js.br +0 -0
- package/public/assets/dialog-DK9a3b4z.js.br +0 -0
- package/public/assets/index-9vdYIm7s.js +0 -210
- package/public/assets/index-9vdYIm7s.js.br +0 -0
- package/public/assets/pencil-BeLNLEKe.js.br +0 -0
- package/public/assets/table-DW57-o1p.js.br +0 -0
- package/public/assets/tabs-Xlttuqxh.js.br +0 -0
- package/public/assets/trash-2-CGxISX8x.js.br +0 -0
- package/public/assets/vendor-router-ChFy8CZ1.js +0 -12
- package/public/assets/vendor-router-ChFy8CZ1.js.br +0 -0
package/dist/api/auth.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import { type RateLimiter } from '../middleware/rateLimit.js';
|
|
3
|
-
export
|
|
4
|
-
export
|
|
3
|
+
export { makePasswordHash, verifyPassword, isInitialized, createAdminAccount } from '../lib/adminAccount.js';
|
|
4
|
+
export type { CreateAdminResult } from '../lib/adminAccount.js';
|
|
5
5
|
export declare function handleLogin(req: http.IncomingMessage, res: http.ServerResponse, trustedProxies?: string[], limiter?: RateLimiter): Promise<void>;
|
|
6
6
|
export declare function handleMe(req: http.IncomingMessage, res: http.ServerResponse): void;
|
|
7
7
|
export declare function handleChangePassword(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
|
package/dist/api/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAOvB,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,4BAA4B,CAAA;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAOvB,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,4BAA4B,CAAA;AAiBhF,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAA;AAC5G,YAAY,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAE/D,wBAAsB,WAAW,CAC/B,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,cAAc,GAAE,MAAM,EAAgC,EACtD,OAAO,GAAE,WAA0B,GAClC,OAAO,CAAC,IAAI,CAAC,CA2Bf;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAWlF;AAED,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAe7G;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAMvF;AAED,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAE3F;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,cAAc,GAAE,MAAM,EAAgC,GAAG,OAAO,CAAC,IAAI,CAAC,CAyB3J"}
|
package/dist/api/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import crypto from 'crypto';
|
|
2
1
|
import { getDb } from '../db.js';
|
|
2
|
+
import { makePasswordHash, verifyPassword, isInitialized, createAdminAccount } from '../lib/adminAccount.js';
|
|
3
3
|
import { signJwt, requireAuth } from '../middleware/auth.js';
|
|
4
4
|
import { json, readJson } from '../router.js';
|
|
5
5
|
import { config } from '../lib/config.js';
|
|
@@ -15,25 +15,10 @@ function resolveClient(req, trustedProxies) {
|
|
|
15
15
|
}
|
|
16
16
|
// 로그인 무차별 대입 방어: IP+계정 단위로 실패를 세고 지수 백오프로 잠근다.
|
|
17
17
|
const loginLimiter = createRateLimiter();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export
|
|
22
|
-
const salt = crypto.randomBytes(16).toString('hex');
|
|
23
|
-
const hash = hashPassword(password, salt);
|
|
24
|
-
return `${salt}:${hash}`;
|
|
25
|
-
}
|
|
26
|
-
export function verifyPassword(password, stored) {
|
|
27
|
-
const [salt, hash] = stored.split(':');
|
|
28
|
-
if (!salt || !hash)
|
|
29
|
-
return false;
|
|
30
|
-
const computed = Buffer.from(hashPassword(password, salt), 'hex');
|
|
31
|
-
const expected = Buffer.from(hash, 'hex');
|
|
32
|
-
// 저장 해시 포맷 손상 시 길이 불일치로 timingSafeEqual이 RangeError → 안전 실패로 처리.
|
|
33
|
-
if (computed.length !== expected.length)
|
|
34
|
-
return false;
|
|
35
|
-
return crypto.timingSafeEqual(computed, expected);
|
|
36
|
-
}
|
|
18
|
+
// Hashing and the first-owner rules live in `lib/adminAccount.ts`, because the boot-time bootstrap
|
|
19
|
+
// needs them too and a `lib/` module must not import this layer. Re-exported here so the callers
|
|
20
|
+
// that already reach for them through this module keep their import path.
|
|
21
|
+
export { makePasswordHash, verifyPassword, isInitialized, createAdminAccount } from '../lib/adminAccount.js';
|
|
37
22
|
export async function handleLogin(req, res, trustedProxies = config.local.trustedProxies, limiter = loginLimiter) {
|
|
38
23
|
const body = await readJson(req);
|
|
39
24
|
if (!body.email || !body.password)
|
|
@@ -94,9 +79,7 @@ export function handleLogout(_req, res) {
|
|
|
94
79
|
res.end(JSON.stringify({ ok: true }));
|
|
95
80
|
}
|
|
96
81
|
export function handleAuthStatus(_req, res) {
|
|
97
|
-
|
|
98
|
-
const { n } = db.prepare('SELECT COUNT(*) as n FROM users').get();
|
|
99
|
-
json(res, 200, { initialized: n > 0 });
|
|
82
|
+
json(res, 200, { initialized: isInitialized() });
|
|
100
83
|
}
|
|
101
84
|
export async function handleInit(req, res, trustedProxies = config.local.trustedProxies) {
|
|
102
85
|
// 무인증 부트스트랩(`auth/init`)은 노출 인스턴스에서 최초 부팅~소유자 설정 사이 선점당할 수 있다.
|
|
@@ -104,17 +87,23 @@ export async function handleInit(req, res, trustedProxies = config.local.trusted
|
|
|
104
87
|
if (!resolveClient(req, trustedProxies).isLocal) {
|
|
105
88
|
return json(res, 403, { error: 'Initialization is only allowed from localhost. Run `tapflow admin init` on the relay host.' });
|
|
106
89
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (
|
|
90
|
+
// The owner check stays ahead of reading the body, as it was: an already-initialized install
|
|
91
|
+
// answers 403 without parsing anything.
|
|
92
|
+
if (isInitialized())
|
|
110
93
|
return json(res, 403, { error: 'Already initialized' });
|
|
111
94
|
const body = await readJson(req);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return json(res, 400, { error: 'Password must be at least 8 characters' });
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
95
|
+
const result = createAdminAccount(body.email, body.password);
|
|
96
|
+
switch (result) {
|
|
97
|
+
case 'missing-fields': return json(res, 400, { error: 'email and password required' });
|
|
98
|
+
case 'password-too-short': return json(res, 400, { error: 'Password must be at least 8 characters' });
|
|
99
|
+
case 'ok': return json(res, 201, { ok: true });
|
|
100
|
+
// `noImplicitReturns` is off here, so a new member of `CreateAdminResult` would compile and fall
|
|
101
|
+
// through — leaving the request with no response at all, hanging until the caller's own timeout.
|
|
102
|
+
// `never` makes that a type error instead. The boot path guards the same way.
|
|
103
|
+
default: {
|
|
104
|
+
const unreachable = result;
|
|
105
|
+
return json(res, 500, { error: `Unhandled result: ${String(unreachable)}` });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
119
108
|
}
|
|
120
109
|
//# sourceMappingURL=auth.js.map
|
package/dist/api/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/api/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare function makePasswordHash(password: string): string;
|
|
2
|
+
export declare function verifyPassword(password: string, stored: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Does this install have an owner yet?
|
|
5
|
+
*
|
|
6
|
+
* Shared so the two bootstrap paths cannot disagree about what "already initialized" means.
|
|
7
|
+
*
|
|
8
|
+
* They are not identical in every respect: the boot path trims `TAPFLOW_ADMIN_EMAIL` and the HTTP
|
|
9
|
+
* one stores `body.email` as sent, so the same address with surrounding whitespace produces two
|
|
10
|
+
* different rows and the HTTP one never matches `handleLogin`'s `WHERE email = ?`. Normalising
|
|
11
|
+
* inside this function would change what a shipped endpoint stores, so it is a decision rather than
|
|
12
|
+
* a fix here — tracked separately.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isInitialized(): boolean;
|
|
15
|
+
/** Why `createAdminAccount` refused, or `ok`. The caller turns this into an HTTP status or a log line. */
|
|
16
|
+
export type CreateAdminResult = 'ok' | 'missing-fields' | 'password-too-short';
|
|
17
|
+
/**
|
|
18
|
+
* Create the first Admin.
|
|
19
|
+
*
|
|
20
|
+
* Does **not** check whether an owner already exists — callers do, because they answer that case
|
|
21
|
+
* differently: 403 over HTTP, a silent no-op on every restart at boot.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createAdminAccount(email: string, password: string): CreateAdminResult;
|
|
24
|
+
//# sourceMappingURL=adminAccount.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adminAccount.d.ts","sourceRoot":"","sources":["../../src/lib/adminAccount.ts"],"names":[],"mappings":"AAgBA,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAQxE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAGvC;AAED,0GAA0G;AAC1G,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,gBAAgB,GAAG,oBAAoB,CAAA;AAE9E;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAOrF"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { getDb } from '../db.js';
|
|
3
|
+
// The first-owner rules, and the password hashing they need.
|
|
4
|
+
//
|
|
5
|
+
// **Why this is in `lib/` and not beside the HTTP handlers.** Two callers need it: `api/auth.ts`
|
|
6
|
+
// serves `POST /api/v1/auth/init`, and `lib/adminBootstrap.ts` creates the first account at boot for
|
|
7
|
+
// a container, which cannot reach that endpoint at all — it requires a local client and a container
|
|
8
|
+
// is always behind its bridge gateway. A `lib/` module importing `api/` is backwards here, and doing
|
|
9
|
+
// it made `isInitialized` read as `undefined` depending on which module the test runner loaded
|
|
10
|
+
// first: green on its own file, nine failures in the full suite.
|
|
11
|
+
function hashPassword(password, salt) {
|
|
12
|
+
return crypto.pbkdf2Sync(password, salt, 100_000, 64, 'sha512').toString('hex');
|
|
13
|
+
}
|
|
14
|
+
export function makePasswordHash(password) {
|
|
15
|
+
const salt = crypto.randomBytes(16).toString('hex');
|
|
16
|
+
const hash = hashPassword(password, salt);
|
|
17
|
+
return `${salt}:${hash}`;
|
|
18
|
+
}
|
|
19
|
+
export function verifyPassword(password, stored) {
|
|
20
|
+
const [salt, hash] = stored.split(':');
|
|
21
|
+
if (!salt || !hash)
|
|
22
|
+
return false;
|
|
23
|
+
const computed = Buffer.from(hashPassword(password, salt), 'hex');
|
|
24
|
+
const expected = Buffer.from(hash, 'hex');
|
|
25
|
+
// 저장 해시 포맷 손상 시 길이 불일치로 timingSafeEqual이 RangeError → 안전 실패로 처리.
|
|
26
|
+
if (computed.length !== expected.length)
|
|
27
|
+
return false;
|
|
28
|
+
return crypto.timingSafeEqual(computed, expected);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Does this install have an owner yet?
|
|
32
|
+
*
|
|
33
|
+
* Shared so the two bootstrap paths cannot disagree about what "already initialized" means.
|
|
34
|
+
*
|
|
35
|
+
* They are not identical in every respect: the boot path trims `TAPFLOW_ADMIN_EMAIL` and the HTTP
|
|
36
|
+
* one stores `body.email` as sent, so the same address with surrounding whitespace produces two
|
|
37
|
+
* different rows and the HTTP one never matches `handleLogin`'s `WHERE email = ?`. Normalising
|
|
38
|
+
* inside this function would change what a shipped endpoint stores, so it is a decision rather than
|
|
39
|
+
* a fix here — tracked separately.
|
|
40
|
+
*/
|
|
41
|
+
export function isInitialized() {
|
|
42
|
+
const { n } = getDb().prepare('SELECT COUNT(*) as n FROM users').get();
|
|
43
|
+
return n > 0;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create the first Admin.
|
|
47
|
+
*
|
|
48
|
+
* Does **not** check whether an owner already exists — callers do, because they answer that case
|
|
49
|
+
* differently: 403 over HTTP, a silent no-op on every restart at boot.
|
|
50
|
+
*/
|
|
51
|
+
export function createAdminAccount(email, password) {
|
|
52
|
+
if (!email || !password)
|
|
53
|
+
return 'missing-fields';
|
|
54
|
+
if (password.length < 8)
|
|
55
|
+
return 'password-too-short';
|
|
56
|
+
getDb()
|
|
57
|
+
.prepare('INSERT INTO users (email, display_name, role, password_hash) VALUES (?, ?, ?, ?)')
|
|
58
|
+
.run(email, 'Admin', 'Admin', makePasswordHash(password));
|
|
59
|
+
return 'ok';
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=adminAccount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adminAccount.js","sourceRoot":"","sources":["../../src/lib/adminAccount.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAEhC,6DAA6D;AAC7D,EAAE;AACF,iGAAiG;AACjG,qGAAqG;AACrG,oGAAoG;AACpG,qGAAqG;AACrG,+FAA+F;AAC/F,iEAAiE;AAEjE,SAAS,YAAY,CAAC,QAAgB,EAAE,IAAY;IAClD,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AACjF,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;IACnD,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IACzC,OAAO,GAAG,IAAI,IAAI,IAAI,EAAE,CAAA;AAC1B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,MAAc;IAC7D,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACtC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAA;IAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,KAAK,CAAC,CAAA;IACjE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACzC,iEAAiE;IACjE,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACrD,OAAO,MAAM,CAAC,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;AACnD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,EAAE,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC,GAAG,EAAmB,CAAA;IACvF,OAAO,CAAC,GAAG,CAAC,CAAA;AACd,CAAC;AAKD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa,EAAE,QAAgB;IAChE,IAAI,CAAC,KAAK,IAAI,CAAC,QAAQ;QAAE,OAAO,gBAAgB,CAAA;IAChD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,oBAAoB,CAAA;IACpD,KAAK,EAAE;SACJ,OAAO,CAAC,kFAAkF,CAAC;SAC3F,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC3D,OAAO,IAAI,CAAA;AACb,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create the first Admin from the environment, once, at boot.
|
|
3
|
+
*
|
|
4
|
+
* **Why this exists at all.** `POST /api/v1/auth/init` requires `resolveClient(req).isLocal`, which
|
|
5
|
+
* stops a public instance being claimed by a stranger between first boot and the owner setting a
|
|
6
|
+
* password. A container is always behind its bridge gateway, so that check can never pass there —
|
|
7
|
+
* measured against the published image: the call answers 403 from the host's own browser and from
|
|
8
|
+
* the LAN alike. The error text points at `tapflow admin init`, but the image is relay-only by
|
|
9
|
+
* design (`pnpm deploy --filter @tapflowio/relay`) and carries no CLI. A Docker install therefore
|
|
10
|
+
* had no way to make its first account at all.
|
|
11
|
+
*
|
|
12
|
+
* This path does not go through HTTP, so the `isLocal` gate is not weakened — it is simply not on
|
|
13
|
+
* this road. Whoever can set the container's environment already controls the install.
|
|
14
|
+
*
|
|
15
|
+
* **Idempotent and silent when there is nothing to do.** It runs on every boot, and an install that
|
|
16
|
+
* already has an owner must not accumulate a log line per restart, nor have its account touched.
|
|
17
|
+
*/
|
|
18
|
+
export interface BootstrapLogger {
|
|
19
|
+
info(message: string): void;
|
|
20
|
+
warn(message: string): void;
|
|
21
|
+
error(message: string): void;
|
|
22
|
+
}
|
|
23
|
+
export interface BootstrapEnv {
|
|
24
|
+
TAPFLOW_ADMIN_EMAIL?: string;
|
|
25
|
+
TAPFLOW_ADMIN_PASSWORD?: string;
|
|
26
|
+
}
|
|
27
|
+
/** Thrown when the environment asks for a first account and cannot get one. Fatal at boot. */
|
|
28
|
+
export declare class AdminBootstrapError extends Error {
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* **Misconfiguration refuses to boot, and the first draft had this backwards.**
|
|
32
|
+
*
|
|
33
|
+
* That draft warned and carried on, reasoning that these variables are first-run-only and a typo in
|
|
34
|
+
* them must not keep a running relay down. Review pointed out that the reasoning describes a state
|
|
35
|
+
* this function cannot reach: `isInitialized()` returns above every one of these branches, so an
|
|
36
|
+
* install that already has an owner never gets here whatever its environment says. The only state
|
|
37
|
+
* that does reach them is *the operator asked for a first account, there is none, and the settings
|
|
38
|
+
* are wrong* — and serving anyway leaves the install ownerless and claimable by anything that can
|
|
39
|
+
* reach loopback, indefinitely, behind one line of boot spam.
|
|
40
|
+
*
|
|
41
|
+
* Refusing costs nothing, which is what makes the trade one-sided: an ownerless relay cannot be
|
|
42
|
+
* logged into by anyone legitimate either, so there is no working service to lose, and a container
|
|
43
|
+
* that will not stay up is far louder than a warning.
|
|
44
|
+
*
|
|
45
|
+
* Neither the password nor its length is ever written to the log.
|
|
46
|
+
*/
|
|
47
|
+
export declare function bootstrapAdminFromEnv(env: BootstrapEnv, logger: BootstrapLogger): void;
|
|
48
|
+
//# sourceMappingURL=adminBootstrap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adminBootstrap.d.ts","sourceRoot":"","sources":["../../src/lib/adminBootstrap.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B;AAED,MAAM,WAAW,YAAY;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,sBAAsB,CAAC,EAAE,MAAM,CAAA;CAChC;AAED,8FAA8F;AAC9F,qBAAa,mBAAoB,SAAQ,KAAK;CAAG;AAEjD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAoDtF"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { isInitialized, createAdminAccount } from './adminAccount.js';
|
|
2
|
+
/** Thrown when the environment asks for a first account and cannot get one. Fatal at boot. */
|
|
3
|
+
export class AdminBootstrapError extends Error {
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* **Misconfiguration refuses to boot, and the first draft had this backwards.**
|
|
7
|
+
*
|
|
8
|
+
* That draft warned and carried on, reasoning that these variables are first-run-only and a typo in
|
|
9
|
+
* them must not keep a running relay down. Review pointed out that the reasoning describes a state
|
|
10
|
+
* this function cannot reach: `isInitialized()` returns above every one of these branches, so an
|
|
11
|
+
* install that already has an owner never gets here whatever its environment says. The only state
|
|
12
|
+
* that does reach them is *the operator asked for a first account, there is none, and the settings
|
|
13
|
+
* are wrong* — and serving anyway leaves the install ownerless and claimable by anything that can
|
|
14
|
+
* reach loopback, indefinitely, behind one line of boot spam.
|
|
15
|
+
*
|
|
16
|
+
* Refusing costs nothing, which is what makes the trade one-sided: an ownerless relay cannot be
|
|
17
|
+
* logged into by anyone legitimate either, so there is no working service to lose, and a container
|
|
18
|
+
* that will not stay up is far louder than a warning.
|
|
19
|
+
*
|
|
20
|
+
* Neither the password nor its length is ever written to the log.
|
|
21
|
+
*/
|
|
22
|
+
export function bootstrapAdminFromEnv(env, logger) {
|
|
23
|
+
const email = env.TAPFLOW_ADMIN_EMAIL?.trim();
|
|
24
|
+
const password = env.TAPFLOW_ADMIN_PASSWORD;
|
|
25
|
+
// Neither set is the ordinary case — every install that is not a fresh container.
|
|
26
|
+
if (!email && !password)
|
|
27
|
+
return;
|
|
28
|
+
if (isInitialized())
|
|
29
|
+
return;
|
|
30
|
+
const fail = (message) => {
|
|
31
|
+
logger.error(message);
|
|
32
|
+
throw new AdminBootstrapError(message);
|
|
33
|
+
};
|
|
34
|
+
if (!email || !password) {
|
|
35
|
+
const missing = email ? 'TAPFLOW_ADMIN_PASSWORD' : 'TAPFLOW_ADMIN_EMAIL';
|
|
36
|
+
// `return` so the compiler narrows both to `string` below — `fail` returning `never` is not
|
|
37
|
+
// enough on its own when it is called as a statement.
|
|
38
|
+
return fail(`${missing} is not set, so the first admin account could not be created. ` +
|
|
39
|
+
'Set both TAPFLOW_ADMIN_EMAIL and TAPFLOW_ADMIN_PASSWORD, or neither.');
|
|
40
|
+
}
|
|
41
|
+
let result;
|
|
42
|
+
try {
|
|
43
|
+
result = createAdminAccount(email, password);
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
// **A second container on the same volume is a race, not a misconfiguration.** `users.email` is
|
|
47
|
+
// `UNIQUE`, so two replicas booting together have one of them lose the INSERT. The install ends
|
|
48
|
+
// up with an owner either way, which is the outcome that was asked for — carry on rather than
|
|
49
|
+
// crash-looping whichever replica arrived second.
|
|
50
|
+
if (isInitialized()) {
|
|
51
|
+
logger.warn('Another process created the first admin account while this one was starting.');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
switch (result) {
|
|
57
|
+
case 'password-too-short':
|
|
58
|
+
return fail('TAPFLOW_ADMIN_PASSWORD is shorter than 8 characters, so the first admin account could not ' +
|
|
59
|
+
'be created. Set a longer one.');
|
|
60
|
+
case 'missing-fields':
|
|
61
|
+
// Unreachable: both are non-empty by the checks above. Handled so a later change to
|
|
62
|
+
// `createAdminAccount` cannot silently fall through to "created".
|
|
63
|
+
return fail('The first admin account could not be created: the configured email or password was empty.');
|
|
64
|
+
case 'ok':
|
|
65
|
+
logger.info(`Created the first admin account for ${email} from the environment.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=adminBootstrap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adminBootstrap.js","sourceRoot":"","sources":["../../src/lib/adminBootstrap.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AA8BrE,8FAA8F;AAC9F,MAAM,OAAO,mBAAoB,SAAQ,KAAK;CAAG;AAEjD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,qBAAqB,CAAC,GAAiB,EAAE,MAAuB;IAC9E,MAAM,KAAK,GAAG,GAAG,CAAC,mBAAmB,EAAE,IAAI,EAAE,CAAA;IAC7C,MAAM,QAAQ,GAAG,GAAG,CAAC,sBAAsB,CAAA;IAE3C,kFAAkF;IAClF,IAAI,CAAC,KAAK,IAAI,CAAC,QAAQ;QAAE,OAAM;IAE/B,IAAI,aAAa,EAAE;QAAE,OAAM;IAE3B,MAAM,IAAI,GAAG,CAAC,OAAe,EAAS,EAAE;QACtC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;QACrB,MAAM,IAAI,mBAAmB,CAAC,OAAO,CAAC,CAAA;IACxC,CAAC,CAAA;IAED,IAAI,CAAC,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;QACxB,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,qBAAqB,CAAA;QACxE,4FAA4F;QAC5F,sDAAsD;QACtD,OAAO,IAAI,CACT,GAAG,OAAO,gEAAgE;YAC1E,sEAAsE,CACvE,CAAA;IACH,CAAC;IAED,IAAI,MAAM,CAAA;IACV,IAAI,CAAC;QACH,MAAM,GAAG,kBAAkB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAA;IAC9C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,gGAAgG;QAChG,gGAAgG;QAChG,8FAA8F;QAC9F,kDAAkD;QAClD,IAAI,aAAa,EAAE,EAAE,CAAC;YACpB,MAAM,CAAC,IAAI,CAAC,8EAA8E,CAAC,CAAA;YAC3F,OAAM;QACR,CAAC;QACD,MAAM,GAAG,CAAA;IACX,CAAC;IAED,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,oBAAoB;YACvB,OAAO,IAAI,CACT,4FAA4F;gBAC5F,+BAA+B,CAChC,CAAA;QACH,KAAK,gBAAgB;YACnB,oFAAoF;YACpF,kEAAkE;YAClE,OAAO,IAAI,CAAC,2FAA2F,CAAC,CAAA;QAC1G,KAAK,IAAI;YACP,MAAM,CAAC,IAAI,CAAC,uCAAuC,KAAK,wBAAwB,CAAC,CAAA;IACrF,CAAC;AACH,CAAC"}
|
package/dist/lib/config.d.ts
CHANGED
|
@@ -75,15 +75,15 @@ export declare const config: {
|
|
|
75
75
|
publicUrl?: string | undefined;
|
|
76
76
|
} | null;
|
|
77
77
|
tls: {
|
|
78
|
+
mode: "import-cert";
|
|
79
|
+
certPath: string;
|
|
80
|
+
keyPath: string;
|
|
81
|
+
} | {
|
|
78
82
|
mode: "byo-api-token";
|
|
79
83
|
domain: string;
|
|
80
84
|
dnsProvider: string;
|
|
81
85
|
publishAddress?: boolean | undefined;
|
|
82
86
|
address?: string | undefined;
|
|
83
|
-
} | {
|
|
84
|
-
mode: "import-cert";
|
|
85
|
-
certPath: string;
|
|
86
|
-
keyPath: string;
|
|
87
87
|
} | null;
|
|
88
88
|
smtp: {
|
|
89
89
|
host: string;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function loadDataDirEnv(dataDir: string): string | null;
|
|
1
|
+
export declare function loadDataDirEnv(dataDir: string, warn?: (m: string) => void): string | null;
|
|
2
2
|
//# sourceMappingURL=loadEnvFile.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loadEnvFile.d.ts","sourceRoot":"","sources":["../../src/lib/loadEnvFile.ts"],"names":[],"mappings":"AAMA,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"loadEnvFile.d.ts","sourceRoot":"","sources":["../../src/lib/loadEnvFile.ts"],"names":[],"mappings":"AAMA,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAA6B,GAAG,MAAM,GAAG,IAAI,CAoBjH"}
|
package/dist/lib/loadEnvFile.js
CHANGED
|
@@ -3,11 +3,21 @@ import path from 'path';
|
|
|
3
3
|
// gitignore된 <dataDir>/.env 에서 자격 증명(JWT_SECRET·SMTP·DNS/ACME 토큰 등)을 로드한다.
|
|
4
4
|
// config.load()가 secret을 읽기 전에 호출되어 .env가 모든 비밀의 기본 경로가 된다(#287에서 시작).
|
|
5
5
|
// process.loadEnvFile 은 기존 process.env 값을 안 덮으므로 ambient(셸) 가 항상 우선.
|
|
6
|
-
export function loadDataDirEnv(dataDir) {
|
|
6
|
+
export function loadDataDirEnv(dataDir, warn = (m) => console.warn(m)) {
|
|
7
7
|
const envPath = path.join(dataDir, '.env');
|
|
8
8
|
if (!fs.existsSync(envPath))
|
|
9
9
|
return null;
|
|
10
10
|
try {
|
|
11
|
+
// **Everything else that writes a secret into dataDir sets 0600 and this file trusted its
|
|
12
|
+
// caller.** `config.ts` writes `jwt-secret` with `{ mode: 0o600 }` and chmods it; `tapflow init`
|
|
13
|
+
// does the same for this file. The relay-only image has no CLI, so a container operator writes
|
|
14
|
+
// it by hand under their own umask — 0644 — and the admin bootstrap now tells them to put a
|
|
15
|
+
// plaintext password in it. Warn rather than refuse: an unreadable-by-others file is the goal,
|
|
16
|
+
// but a relay that will not start because of a permission bit helps nobody.
|
|
17
|
+
const mode = fs.statSync(envPath).mode & 0o777;
|
|
18
|
+
if (mode & 0o077) {
|
|
19
|
+
warn(`${envPath} is readable by other users (mode ${mode.toString(8)}). Run: chmod 600 ${envPath}`);
|
|
20
|
+
}
|
|
11
21
|
process.loadEnvFile(envPath);
|
|
12
22
|
return envPath;
|
|
13
23
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loadEnvFile.js","sourceRoot":"","sources":["../../src/lib/loadEnvFile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,IAAI,MAAM,MAAM,CAAA;AAEvB,2EAA2E;AAC3E,uEAAuE;AACvE,qEAAqE;AACrE,MAAM,UAAU,cAAc,CAAC,OAAe;
|
|
1
|
+
{"version":3,"file":"loadEnvFile.js","sourceRoot":"","sources":["../../src/lib/loadEnvFile.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,IAAI,MAAM,MAAM,CAAA;AAEvB,2EAA2E;AAC3E,uEAAuE;AACvE,qEAAqE;AACrE,MAAM,UAAU,cAAc,CAAC,OAAe,EAAE,OAA4B,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;IAChG,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAA;IAC1C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,IAAI,CAAA;IACxC,IAAI,CAAC;QACH,0FAA0F;QAC1F,iGAAiG;QACjG,+FAA+F;QAC/F,4FAA4F;QAC5F,+FAA+F;QAC/F,4EAA4E;QAC5E,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,IAAI,GAAG,KAAK,CAAA;QAC9C,IAAI,IAAI,GAAG,KAAK,EAAE,CAAC;YACjB,IAAI,CAAC,GAAG,OAAO,qCAAqC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,qBAAqB,OAAO,EAAE,CAAC,CAAA;QACrG,CAAC;QACD,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAA;QAC5B,OAAO,OAAO,CAAA;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,gDAAgD;QAChD,OAAO,IAAI,CAAA;IACb,CAAC;AACH,CAAC"}
|
package/dist/server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { initDb } from './db.js';
|
|
|
3
3
|
import { RelayServer } from './RelayServer.js';
|
|
4
4
|
import { config, loadedEnvPath } from './lib/config.js';
|
|
5
5
|
import { buildCorsOrigins, proxyWithoutPublicUrlWarning } from './lib/proxyConfig.js';
|
|
6
|
+
import { bootstrapAdminFromEnv, AdminBootstrapError } from './lib/adminBootstrap.js';
|
|
6
7
|
import { createCertProvider, resolveRelayDisplayHost } from './lib/cert/index.js';
|
|
7
8
|
import { startTlsBackgroundTasks } from './lib/tlsTasks.js';
|
|
8
9
|
import { createLogger } from '@tapflowio/agent-core';
|
|
@@ -14,6 +15,27 @@ if (loadedEnvPath)
|
|
|
14
15
|
const dbPath = path.join(dataDir, 'tapflow.db');
|
|
15
16
|
const uploadsDir = path.join(dataDir, 'uploads');
|
|
16
17
|
initDb(dbPath);
|
|
18
|
+
// After the schema exists and before anything is served: a container cannot reach the HTTP
|
|
19
|
+
// bootstrap, because that one requires a local client and a container is always behind its bridge.
|
|
20
|
+
//
|
|
21
|
+
// **Fatal on failure.** The only way to get here with a problem is "an admin was asked for, there is
|
|
22
|
+
// none, and the settings are wrong" — serving that would leave the install ownerless and claimable
|
|
23
|
+
// from loopback. See `lib/adminBootstrap.ts`.
|
|
24
|
+
try {
|
|
25
|
+
bootstrapAdminFromEnv(process.env, logger);
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
// The reason is already logged where it was decided; re-logging it here printed every refusal
|
|
29
|
+
// twice. Anything that is not a refusal has no message of its own yet, so that one is logged.
|
|
30
|
+
if (!(err instanceof AdminBootstrapError))
|
|
31
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
// **The password does not outlive its use.** It is read once here, and every `spawnSync` in
|
|
35
|
+
// `api/builds.ts` — twelve of them, over user-uploaded archives — inherits `process.env` without
|
|
36
|
+
// passing one explicitly. Nothing reads these names again; `<dataDir>/.env` is re-read each boot.
|
|
37
|
+
delete process.env.TAPFLOW_ADMIN_PASSWORD;
|
|
38
|
+
delete process.env.TAPFLOW_ADMIN_EMAIL;
|
|
17
39
|
const corsOrigins = buildCorsOrigins(config, port);
|
|
18
40
|
const proxyWarning = proxyWithoutPublicUrlWarning(config);
|
|
19
41
|
if (proxyWarning)
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tapflowio/relay",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "WebSocket relay server for tapflow — NAT traversal, session routing, JWT auth, bundled dashboard",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tapflow",
|
|
@@ -49,11 +49,11 @@
|
|
|
49
49
|
"better-sqlite3": "^12.11.1",
|
|
50
50
|
"busboy": "^1.6.0",
|
|
51
51
|
"jsonwebtoken": "^9.0.3",
|
|
52
|
-
"nodemailer": "^9.0.
|
|
52
|
+
"nodemailer": "^9.0.6",
|
|
53
53
|
"ws": "^8.21.3",
|
|
54
|
-
"zod": "^4.4
|
|
55
|
-
"@tapflowio/agent-core": "0.20.
|
|
56
|
-
"@tapflowio/protocol": "0.20.
|
|
54
|
+
"zod": "^4.5.4",
|
|
55
|
+
"@tapflowio/agent-core": "0.20.1",
|
|
56
|
+
"@tapflowio/protocol": "0.20.1"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"@types/nodemailer": "^8.0.1",
|
|
64
64
|
"@types/ws": "^8.0.0",
|
|
65
65
|
"typescript": "^5.0.0",
|
|
66
|
-
"vitest": "^4.1.
|
|
66
|
+
"vitest": "^4.1.11",
|
|
67
67
|
"@tapflowio/test-utils": "0.0.0"
|
|
68
68
|
},
|
|
69
69
|
"scripts": {
|
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
import{i as r,j as e}from"./vendor-react-AJpf4YZ0.js";import{W as C,B as y,L as b,a4 as k,J,I as M,S as Q,f as W}from"./index-
|
|
2
|
-
* @license lucide-react v1.
|
|
1
|
+
import{i as r,j as e}from"./vendor-react-AJpf4YZ0.js";import{W as C,B as y,L as b,a4 as k,J,I as M,S as Q,f as W}from"./index-O-Temo_p.js";import{e as Y,S as K,P as X,g as ee,h as se,i as ae,b as te,d as le,s as ne,u as re}from"./build-format-DO1zN4vW.js";import{D as O,f as L,a as U,d as E,e as V,S as A,i as _,j as B,g as z,h as i,C as ce}from"./dialog-BGH9MPcH.js";import{P as ie,T as oe}from"./trash-2-CAhMuWgg.js";import{B as T}from"./badge-CP-xbjo3.js";import{A as de,c as ue,f as he,g as xe,d as pe,e as me,b as fe,a as ge}from"./alert-dialog-CLhPOSdB.js";import{b as je,c as ve}from"./vendor-router-BFibuab1.js";/**
|
|
2
|
+
* @license lucide-react v1.37.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
5
5
|
* See the LICENSE file in the root directory of this source tree.
|
|
6
6
|
*/const Ne=[["path",{d:"M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z",key:"zw3jo"}],["path",{d:"M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12",key:"1wduqc"}],["path",{d:"M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17",key:"kqbvx6"}]],Se=C("layers",Ne);/**
|
|
7
|
-
* @license lucide-react v1.
|
|
7
|
+
* @license lucide-react v1.37.0 - ISC
|
|
8
8
|
*
|
|
9
9
|
* This source code is licensed under the ISC license.
|
|
10
10
|
* See the LICENSE file in the root directory of this source tree.
|
|
11
11
|
*/const ye=[["path",{d:"M11 21.73a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73z",key:"1a0edw"}],["path",{d:"M12 22V12",key:"d0xqtd"}],["polyline",{points:"3.29 7 12 12 20.71 7",key:"ousv84"}],["path",{d:"m7.5 4.27 9 5.15",key:"1c824w"}]],we=C("package",ye);/**
|
|
12
|
-
* @license lucide-react v1.
|
|
12
|
+
* @license lucide-react v1.37.0 - ISC
|
|
13
13
|
*
|
|
14
14
|
* This source code is licensed under the ISC license.
|
|
15
15
|
* See the LICENSE file in the root directory of this source tree.
|
|
16
16
|
*/const be=[["path",{d:"M10 2h4",key:"n1abiw"}],["path",{d:"M4.6 11a8 8 0 0 0 1.7 8.7 8 8 0 0 0 8.7 1.7",key:"10he05"}],["path",{d:"M7.4 7.4a8 8 0 0 1 10.3 1 8 8 0 0 1 .9 10.2",key:"15f7sh"}],["path",{d:"m2 2 20 20",key:"1ooewy"}],["path",{d:"M12 12v-2",key:"fwoke6"}]],ke=C("timer-off",be);/**
|
|
17
|
-
* @license lucide-react v1.
|
|
17
|
+
* @license lucide-react v1.37.0 - ISC
|
|
18
18
|
*
|
|
19
19
|
* This source code is licensed under the ISC license.
|
|
20
20
|
* See the LICENSE file in the root directory of this source tree.
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{i,j as e}from"./vendor-react-AJpf4YZ0.js";import{ai as X,a6 as Z,ad as S,a5 as y,ah as I,C as g,c as j,d as v,a as w,L as m,I as h,S as M,h as O,a4 as n,B as u,E as ee,J as se,H as ae,a2 as C,_ as $,a3 as p}from"./index-9vdYIm7s.js";import{B as re}from"./badge-B6Nf3A0Y.js";import{A as te,h as oe,c as ne,f as ie,g as le,d as ce,e as de,b as me,a as he}from"./alert-dialog-CgatMXjB.js";import{P as W}from"./pencil-BeLNLEKe.js";import"./vendor-router-ChFy8CZ1.js";const ue=C({teamName:p().min(1),logo:$(File).nullable().optional()}),pe=C({displayName:p().min(1),avatar:$(File).nullable().optional()}),fe=C({currentPassword:p().min(1),newPassword:p().min(8),confirmPassword:p()}).refine(f=>f.newPassword===f.confirmPassword,{message:"Passwords do not match",path:["confirmPassword"]});function Ne(){const{resolvedTheme:f}=X(),J=f==="dark"?"/logo-dark.svg":"/logo.svg",{user:o}=Z(),b=(o==null?void 0:o.role)==="Admin",N=(o==null?void 0:o.role)==="Admin"||(o==null?void 0:o.role)==="Developer",[V,P]=i.useState(null),A=i.useRef(null),d=S({resolver:y(ue),mode:"onBlur",defaultValues:{teamName:"",logo:null}});i.useEffect(()=>{b&&fetch("/api/v1/settings",{credentials:"include"}).then(s=>s.ok?s.json():null).then(s=>{s&&(d.reset({teamName:s.team_name,logo:null}),P(s.logo_url))})},[b]);async function Q(s){const a=new FormData;a.append("team_name",s.teamName),s.logo&&a.append("logo",s.logo);try{if(!(await fetch("/api/v1/settings",{method:"PATCH",credentials:"include",body:a})).ok){n.error("Failed to update workspace");return}n.success("Workspace updated")}catch{n.error("Failed to update workspace")}}const[k,F]=i.useState(null),D=i.useRef(null),c=S({resolver:y(pe),mode:"onBlur",defaultValues:{displayName:"",avatar:null}});i.useEffect(()=>{o&&(c.reset({displayName:o.displayName??"",avatar:null}),F(o.avatarUrl??null))},[o]);async function Y(s){const a=new FormData;a.append("display_name",s.displayName),s.avatar&&a.append("avatar",s.avatar);try{if(!(await fetch("/api/v1/profile",{method:"PATCH",credentials:"include",body:a})).ok){n.error("Failed to update profile");return}n.success("Profile updated")}catch{n.error("Failed to update profile")}}const t=S({resolver:y(fe),mode:"onBlur"});async function q(s){try{const a=await fetch("/api/v1/auth/change-password",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({currentPassword:s.currentPassword,newPassword:s.newPassword})});if(!a.ok){const r=await a.json();t.setError("root",{message:r.error??"Failed to change password"});return}t.reset(),n.success("Password changed")}catch{t.setError("root",{message:"Network error"})}}const[T,E]=i.useState([]),[B,L]=i.useState({}),[U,z]=i.useState({}),[R,_]=i.useState({});i.useEffect(()=>{N&&fetch("/api/v1/apps",{credentials:"include"}).then(s=>s.ok?s.json():null).then(s=>{if(!s)return;E(s.items);const a={};s.items.forEach(r=>{a[r.id]=r.name}),L(a)})},[N]);async function G(s){_(a=>({...a,[s]:!0}));try{if(!(await fetch(`/api/v1/apps/${s}`,{method:"DELETE",credentials:"include"})).ok){n.error("Failed to delete app");return}E(r=>r.filter(l=>l.id!==s)),n.success("App deleted")}catch{n.error("Failed to delete app")}finally{_(a=>({...a,[s]:!1}))}}async function K(s){z(a=>({...a,[s]:!0}));try{if(!(await fetch(`/api/v1/apps/${s}`,{method:"PATCH",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:B[s]})})).ok){n.error("Failed to update app");return}n.success("App updated")}catch{n.error("Failed to update app")}finally{z(a=>({...a,[s]:!1}))}}const x=I({control:c.control,name:"displayName"});return e.jsxs("div",{className:"flex flex-col gap-6 max-w-[900px] mx-auto w-full p-6",children:[e.jsx("h1",{className:"text-xl font-semibold tracking-display-sm",children:"Settings"}),b&&e.jsxs(g,{children:[e.jsx(j,{children:e.jsx(v,{children:"Workspace"})}),e.jsx(w,{children:e.jsxs("form",{onSubmit:d.handleSubmit(Q),className:"flex flex-col gap-4",children:[e.jsxs("div",{className:"grid gap-2",children:[e.jsx(m,{htmlFor:"team-name",children:"Team name"}),e.jsx(h,{id:"team-name",placeholder:"My QA Team",...d.register("teamName")}),d.formState.errors.teamName&&e.jsx("p",{className:"text-sm text-destructive",children:d.formState.errors.teamName.message})]}),e.jsx(M,{}),e.jsxs("div",{className:"grid gap-2",children:[e.jsxs(m,{children:["Logo ",e.jsx("span",{className:"text-muted-foreground text-xs",children:"(png · jpg, max 2MB)"})]}),e.jsx(O,{name:"logo",control:d.control,render:({field:s})=>e.jsxs("div",{className:"relative w-16 h-16",children:[e.jsx("img",{src:V??J,alt:"Workspace logo",className:"w-16 h-16 rounded-lg object-contain"}),e.jsx("button",{type:"button","aria-label":"Change logo",onClick:()=>{var a;return(a=A.current)==null?void 0:a.click()},className:"absolute bottom-0 right-0 w-6 h-6 rounded-full bg-background border border-border shadow-sm flex items-center justify-center hover:bg-accent transition-colors",children:e.jsx(W,{className:"w-3 h-3","aria-hidden":"true"})}),e.jsx("input",{ref:A,type:"file",accept:".png,.jpg,.jpeg",className:"hidden",onChange:a=>{var l;const r=(l=a.target.files)==null?void 0:l[0];if(r&&r.size>2*1024*1024){n.error("Image must be 2MB or less");return}r&&(s.onChange(r),P(URL.createObjectURL(r)))}})]})})]}),e.jsx("div",{className:"flex justify-end",children:e.jsx(u,{type:"submit",size:"sm",disabled:d.formState.isSubmitting,children:d.formState.isSubmitting?"Saving…":"Save changes"})})]})})]}),e.jsxs(g,{children:[e.jsx(j,{children:e.jsx(v,{children:"Profile"})}),e.jsx(w,{children:e.jsxs("form",{onSubmit:c.handleSubmit(Y),className:"flex flex-col gap-4",children:[e.jsxs("div",{className:"grid gap-2",children:[e.jsx(m,{htmlFor:"display-name",children:"Nickname"}),e.jsx(h,{id:"display-name",placeholder:"Your name",...c.register("displayName")}),c.formState.errors.displayName&&e.jsx("p",{className:"text-sm text-destructive",children:c.formState.errors.displayName.message})]}),e.jsx(M,{}),e.jsxs("div",{className:"grid gap-2",children:[e.jsxs(m,{children:["Avatar ",e.jsx("span",{className:"text-muted-foreground text-xs",children:"(png · jpg, max 2MB)"})]}),e.jsx(O,{name:"avatar",control:c.control,render:({field:s})=>{var a;return e.jsxs("div",{className:"relative w-14 h-14",children:[k?e.jsx("img",{src:k,alt:"Profile avatar",className:"w-14 h-14 rounded-full object-cover border"}):e.jsx("div",{className:"w-14 h-14 rounded-full flex items-center justify-center text-lg font-medium",style:(()=>{const r=ee(x||(o==null?void 0:o.email)||"");return{backgroundColor:r.bg,color:r.fg}})(),children:((a=x==null?void 0:x[0])==null?void 0:a.toUpperCase())??"?"}),e.jsx("button",{type:"button","aria-label":"Change avatar",onClick:()=>{var r;return(r=D.current)==null?void 0:r.click()},className:"absolute bottom-0 right-0 w-6 h-6 rounded-full bg-background border border-border shadow-sm flex items-center justify-center hover:bg-accent transition-colors",children:e.jsx(W,{className:"w-3 h-3","aria-hidden":"true"})}),e.jsx("input",{ref:D,type:"file",accept:".png,.jpg,.jpeg",className:"hidden",onChange:r=>{var H;const l=(H=r.target.files)==null?void 0:H[0];if(l&&l.size>2*1024*1024){n.error("Image must be 2MB or less");return}l&&(s.onChange(l),F(URL.createObjectURL(l)))}})]})}})]}),e.jsx("div",{className:"flex justify-end",children:e.jsx(u,{type:"submit",size:"sm",disabled:c.formState.isSubmitting,children:c.formState.isSubmitting?"Saving…":"Save changes"})})]})})]}),e.jsxs(g,{children:[e.jsx(j,{children:e.jsx(v,{children:"Password"})}),e.jsx(w,{children:e.jsxs("form",{onSubmit:t.handleSubmit(q),className:"flex flex-col gap-4",children:[e.jsxs("div",{className:"grid gap-2",children:[e.jsx(m,{htmlFor:"current-password",children:"Current password"}),e.jsx(h,{id:"current-password",type:"password",...t.register("currentPassword")}),t.formState.errors.currentPassword&&e.jsx("p",{className:"text-sm text-destructive",children:t.formState.errors.currentPassword.message})]}),e.jsxs("div",{className:"grid gap-2",children:[e.jsx(m,{htmlFor:"new-password",children:"New password"}),e.jsx(h,{id:"new-password",type:"password",...t.register("newPassword")}),t.formState.errors.newPassword&&e.jsx("p",{className:"text-sm text-destructive",children:t.formState.errors.newPassword.message})]}),e.jsxs("div",{className:"grid gap-2",children:[e.jsx(m,{htmlFor:"confirm-password",children:"Confirm new password"}),e.jsx(h,{id:"confirm-password",type:"password",...t.register("confirmPassword")}),t.formState.errors.confirmPassword&&e.jsx("p",{className:"text-sm text-destructive",children:t.formState.errors.confirmPassword.message})]}),t.formState.errors.root&&e.jsx("p",{className:"text-sm text-destructive",children:t.formState.errors.root.message}),e.jsx("div",{className:"flex justify-end",children:e.jsx(u,{type:"submit",size:"sm",disabled:t.formState.isSubmitting,children:t.formState.isSubmitting?"Saving…":"Change password"})})]})})]}),N&&T.length>0&&e.jsxs(g,{children:[e.jsx(j,{children:e.jsx(v,{children:"Apps"})}),e.jsx(w,{children:e.jsx("div",{className:"flex flex-col divide-y divide-border",children:T.map(s=>e.jsxs("div",{className:"flex flex-col gap-2 py-4 first:pt-0 last:pb-0",children:[e.jsx(m,{htmlFor:`app-${s.id}`,children:"App Name"}),e.jsx(h,{id:`app-${s.id}`,value:B[s.id]??"",onChange:a=>L(r=>({...r,[s.id]:a.target.value}))}),e.jsxs("div",{className:"flex flex-col gap-1.5 mt-1",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("span",{className:"text-xs text-muted-foreground font-mono",children:s.bundle_id_key}),e.jsx(re,{tone:s.platform==="ios"?"ios":s.platform==="android"?"android":void 0,variant:s.platform==="both"?"secondary":void 0,className:"capitalize",children:s.platform})]}),e.jsxs("div",{className:"flex items-center justify-end gap-2",children:[e.jsx(u,{size:"sm",variant:"outline",disabled:U[s.id],onClick:()=>K(s.id),children:U[s.id]?"Saving…":"Save"}),e.jsxs(te,{children:[e.jsx(oe,{asChild:!0,children:e.jsx(u,{size:"sm",variant:"destructive",disabled:R[s.id],children:R[s.id]?"Deleting…":"Delete"})}),e.jsxs(ne,{children:[e.jsxs(ie,{children:[e.jsx(le,{children:"Delete app?"}),e.jsxs(ce,{children:["This will permanently delete ",e.jsx("strong",{children:s.name})," and all its builds. This action cannot be undone."]})]}),e.jsxs(de,{children:[e.jsx(me,{className:"w-24",children:"Cancel"}),e.jsx(he,{className:se(ae({variant:"destructive"}),"w-24"),onClick:()=>G(s.id),children:"Delete"})]})]})]})]})]})]},s.id))})})]})]})}export{Ne as DefaultSettings};
|
|
1
|
+
import{i,j as e}from"./vendor-react-AJpf4YZ0.js";import{ai as X,a6 as Z,ad as S,a5 as y,ah as I,C as g,c as j,d as v,a as w,L as m,I as h,S as M,h as O,a4 as n,B as u,E as ee,J as se,H as ae,a2 as C,_ as $,a3 as p}from"./index-O-Temo_p.js";import{B as re}from"./badge-CP-xbjo3.js";import{A as te,h as oe,c as ne,f as ie,g as le,d as ce,e as de,b as me,a as he}from"./alert-dialog-CLhPOSdB.js";import{P as W}from"./pencil-0UYCdq7e.js";import"./vendor-router-BFibuab1.js";const ue=C({teamName:p().min(1),logo:$(File).nullable().optional()}),pe=C({displayName:p().min(1),avatar:$(File).nullable().optional()}),fe=C({currentPassword:p().min(1),newPassword:p().min(8),confirmPassword:p()}).refine(f=>f.newPassword===f.confirmPassword,{message:"Passwords do not match",path:["confirmPassword"]});function Ne(){const{resolvedTheme:f}=X(),J=f==="dark"?"/logo-dark.svg":"/logo.svg",{user:o}=Z(),b=(o==null?void 0:o.role)==="Admin",N=(o==null?void 0:o.role)==="Admin"||(o==null?void 0:o.role)==="Developer",[V,P]=i.useState(null),A=i.useRef(null),d=S({resolver:y(ue),mode:"onBlur",defaultValues:{teamName:"",logo:null}});i.useEffect(()=>{b&&fetch("/api/v1/settings",{credentials:"include"}).then(s=>s.ok?s.json():null).then(s=>{s&&(d.reset({teamName:s.team_name,logo:null}),P(s.logo_url))})},[b]);async function Q(s){const a=new FormData;a.append("team_name",s.teamName),s.logo&&a.append("logo",s.logo);try{if(!(await fetch("/api/v1/settings",{method:"PATCH",credentials:"include",body:a})).ok){n.error("Failed to update workspace");return}n.success("Workspace updated")}catch{n.error("Failed to update workspace")}}const[k,F]=i.useState(null),D=i.useRef(null),c=S({resolver:y(pe),mode:"onBlur",defaultValues:{displayName:"",avatar:null}});i.useEffect(()=>{o&&(c.reset({displayName:o.displayName??"",avatar:null}),F(o.avatarUrl??null))},[o]);async function Y(s){const a=new FormData;a.append("display_name",s.displayName),s.avatar&&a.append("avatar",s.avatar);try{if(!(await fetch("/api/v1/profile",{method:"PATCH",credentials:"include",body:a})).ok){n.error("Failed to update profile");return}n.success("Profile updated")}catch{n.error("Failed to update profile")}}const t=S({resolver:y(fe),mode:"onBlur"});async function q(s){try{const a=await fetch("/api/v1/auth/change-password",{method:"POST",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({currentPassword:s.currentPassword,newPassword:s.newPassword})});if(!a.ok){const r=await a.json();t.setError("root",{message:r.error??"Failed to change password"});return}t.reset(),n.success("Password changed")}catch{t.setError("root",{message:"Network error"})}}const[T,E]=i.useState([]),[B,L]=i.useState({}),[U,z]=i.useState({}),[R,_]=i.useState({});i.useEffect(()=>{N&&fetch("/api/v1/apps",{credentials:"include"}).then(s=>s.ok?s.json():null).then(s=>{if(!s)return;E(s.items);const a={};s.items.forEach(r=>{a[r.id]=r.name}),L(a)})},[N]);async function G(s){_(a=>({...a,[s]:!0}));try{if(!(await fetch(`/api/v1/apps/${s}`,{method:"DELETE",credentials:"include"})).ok){n.error("Failed to delete app");return}E(r=>r.filter(l=>l.id!==s)),n.success("App deleted")}catch{n.error("Failed to delete app")}finally{_(a=>({...a,[s]:!1}))}}async function K(s){z(a=>({...a,[s]:!0}));try{if(!(await fetch(`/api/v1/apps/${s}`,{method:"PATCH",credentials:"include",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:B[s]})})).ok){n.error("Failed to update app");return}n.success("App updated")}catch{n.error("Failed to update app")}finally{z(a=>({...a,[s]:!1}))}}const x=I({control:c.control,name:"displayName"});return e.jsxs("div",{className:"flex flex-col gap-6 max-w-[900px] mx-auto w-full p-6",children:[e.jsx("h1",{className:"text-xl font-semibold tracking-display-sm",children:"Settings"}),b&&e.jsxs(g,{children:[e.jsx(j,{children:e.jsx(v,{children:"Workspace"})}),e.jsx(w,{children:e.jsxs("form",{onSubmit:d.handleSubmit(Q),className:"flex flex-col gap-4",children:[e.jsxs("div",{className:"grid gap-2",children:[e.jsx(m,{htmlFor:"team-name",children:"Team name"}),e.jsx(h,{id:"team-name",placeholder:"My QA Team",...d.register("teamName")}),d.formState.errors.teamName&&e.jsx("p",{className:"text-sm text-destructive",children:d.formState.errors.teamName.message})]}),e.jsx(M,{}),e.jsxs("div",{className:"grid gap-2",children:[e.jsxs(m,{children:["Logo ",e.jsx("span",{className:"text-muted-foreground text-xs",children:"(png · jpg, max 2MB)"})]}),e.jsx(O,{name:"logo",control:d.control,render:({field:s})=>e.jsxs("div",{className:"relative w-16 h-16",children:[e.jsx("img",{src:V??J,alt:"Workspace logo",className:"w-16 h-16 rounded-lg object-contain"}),e.jsx("button",{type:"button","aria-label":"Change logo",onClick:()=>{var a;return(a=A.current)==null?void 0:a.click()},className:"absolute bottom-0 right-0 w-6 h-6 rounded-full bg-background border border-border shadow-sm flex items-center justify-center hover:bg-accent transition-colors",children:e.jsx(W,{className:"w-3 h-3","aria-hidden":"true"})}),e.jsx("input",{ref:A,type:"file",accept:".png,.jpg,.jpeg",className:"hidden",onChange:a=>{var l;const r=(l=a.target.files)==null?void 0:l[0];if(r&&r.size>2*1024*1024){n.error("Image must be 2MB or less");return}r&&(s.onChange(r),P(URL.createObjectURL(r)))}})]})})]}),e.jsx("div",{className:"flex justify-end",children:e.jsx(u,{type:"submit",size:"sm",disabled:d.formState.isSubmitting,children:d.formState.isSubmitting?"Saving…":"Save changes"})})]})})]}),e.jsxs(g,{children:[e.jsx(j,{children:e.jsx(v,{children:"Profile"})}),e.jsx(w,{children:e.jsxs("form",{onSubmit:c.handleSubmit(Y),className:"flex flex-col gap-4",children:[e.jsxs("div",{className:"grid gap-2",children:[e.jsx(m,{htmlFor:"display-name",children:"Nickname"}),e.jsx(h,{id:"display-name",placeholder:"Your name",...c.register("displayName")}),c.formState.errors.displayName&&e.jsx("p",{className:"text-sm text-destructive",children:c.formState.errors.displayName.message})]}),e.jsx(M,{}),e.jsxs("div",{className:"grid gap-2",children:[e.jsxs(m,{children:["Avatar ",e.jsx("span",{className:"text-muted-foreground text-xs",children:"(png · jpg, max 2MB)"})]}),e.jsx(O,{name:"avatar",control:c.control,render:({field:s})=>{var a;return e.jsxs("div",{className:"relative w-14 h-14",children:[k?e.jsx("img",{src:k,alt:"Profile avatar",className:"w-14 h-14 rounded-full object-cover border"}):e.jsx("div",{className:"w-14 h-14 rounded-full flex items-center justify-center text-lg font-medium",style:(()=>{const r=ee(x||(o==null?void 0:o.email)||"");return{backgroundColor:r.bg,color:r.fg}})(),children:((a=x==null?void 0:x[0])==null?void 0:a.toUpperCase())??"?"}),e.jsx("button",{type:"button","aria-label":"Change avatar",onClick:()=>{var r;return(r=D.current)==null?void 0:r.click()},className:"absolute bottom-0 right-0 w-6 h-6 rounded-full bg-background border border-border shadow-sm flex items-center justify-center hover:bg-accent transition-colors",children:e.jsx(W,{className:"w-3 h-3","aria-hidden":"true"})}),e.jsx("input",{ref:D,type:"file",accept:".png,.jpg,.jpeg",className:"hidden",onChange:r=>{var H;const l=(H=r.target.files)==null?void 0:H[0];if(l&&l.size>2*1024*1024){n.error("Image must be 2MB or less");return}l&&(s.onChange(l),F(URL.createObjectURL(l)))}})]})}})]}),e.jsx("div",{className:"flex justify-end",children:e.jsx(u,{type:"submit",size:"sm",disabled:c.formState.isSubmitting,children:c.formState.isSubmitting?"Saving…":"Save changes"})})]})})]}),e.jsxs(g,{children:[e.jsx(j,{children:e.jsx(v,{children:"Password"})}),e.jsx(w,{children:e.jsxs("form",{onSubmit:t.handleSubmit(q),className:"flex flex-col gap-4",children:[e.jsxs("div",{className:"grid gap-2",children:[e.jsx(m,{htmlFor:"current-password",children:"Current password"}),e.jsx(h,{id:"current-password",type:"password",...t.register("currentPassword")}),t.formState.errors.currentPassword&&e.jsx("p",{className:"text-sm text-destructive",children:t.formState.errors.currentPassword.message})]}),e.jsxs("div",{className:"grid gap-2",children:[e.jsx(m,{htmlFor:"new-password",children:"New password"}),e.jsx(h,{id:"new-password",type:"password",...t.register("newPassword")}),t.formState.errors.newPassword&&e.jsx("p",{className:"text-sm text-destructive",children:t.formState.errors.newPassword.message})]}),e.jsxs("div",{className:"grid gap-2",children:[e.jsx(m,{htmlFor:"confirm-password",children:"Confirm new password"}),e.jsx(h,{id:"confirm-password",type:"password",...t.register("confirmPassword")}),t.formState.errors.confirmPassword&&e.jsx("p",{className:"text-sm text-destructive",children:t.formState.errors.confirmPassword.message})]}),t.formState.errors.root&&e.jsx("p",{className:"text-sm text-destructive",children:t.formState.errors.root.message}),e.jsx("div",{className:"flex justify-end",children:e.jsx(u,{type:"submit",size:"sm",disabled:t.formState.isSubmitting,children:t.formState.isSubmitting?"Saving…":"Change password"})})]})})]}),N&&T.length>0&&e.jsxs(g,{children:[e.jsx(j,{children:e.jsx(v,{children:"Apps"})}),e.jsx(w,{children:e.jsx("div",{className:"flex flex-col divide-y divide-border",children:T.map(s=>e.jsxs("div",{className:"flex flex-col gap-2 py-4 first:pt-0 last:pb-0",children:[e.jsx(m,{htmlFor:`app-${s.id}`,children:"App Name"}),e.jsx(h,{id:`app-${s.id}`,value:B[s.id]??"",onChange:a=>L(r=>({...r,[s.id]:a.target.value}))}),e.jsxs("div",{className:"flex flex-col gap-1.5 mt-1",children:[e.jsxs("div",{className:"flex items-center gap-2",children:[e.jsx("span",{className:"text-xs text-muted-foreground font-mono",children:s.bundle_id_key}),e.jsx(re,{tone:s.platform==="ios"?"ios":s.platform==="android"?"android":void 0,variant:s.platform==="both"?"secondary":void 0,className:"capitalize",children:s.platform})]}),e.jsxs("div",{className:"flex items-center justify-end gap-2",children:[e.jsx(u,{size:"sm",variant:"outline",disabled:U[s.id],onClick:()=>K(s.id),children:U[s.id]?"Saving…":"Save"}),e.jsxs(te,{children:[e.jsx(oe,{asChild:!0,children:e.jsx(u,{size:"sm",variant:"destructive",disabled:R[s.id],children:R[s.id]?"Deleting…":"Delete"})}),e.jsxs(ne,{children:[e.jsxs(ie,{children:[e.jsx(le,{children:"Delete app?"}),e.jsxs(ce,{children:["This will permanently delete ",e.jsx("strong",{children:s.name})," and all its builds. This action cannot be undone."]})]}),e.jsxs(de,{children:[e.jsx(me,{className:"w-24",children:"Cancel"}),e.jsx(he,{className:se(ae({variant:"destructive"}),"w-24"),onClick:()=>G(s.id),children:"Delete"})]})]})]})]})]})]},s.id))})})]})]})}export{Ne as DefaultSettings};
|
|
Binary file
|