@tapflowio/relay 0.24.0 → 0.26.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/dist/RelayServer.d.ts +51 -0
- package/dist/RelayServer.d.ts.map +1 -1
- package/dist/RelayServer.js +262 -31
- package/dist/RelayServer.js.map +1 -1
- package/dist/api/apps.d.ts.map +1 -1
- package/dist/api/apps.js +6 -16
- package/dist/api/apps.js.map +1 -1
- package/dist/api/auth.d.ts +1 -1
- package/dist/api/auth.d.ts.map +1 -1
- package/dist/api/auth.js +30 -19
- package/dist/api/auth.js.map +1 -1
- package/dist/api/builds.d.ts.map +1 -1
- package/dist/api/builds.js +7 -5
- package/dist/api/builds.js.map +1 -1
- package/dist/api/comments.d.ts.map +1 -1
- package/dist/api/comments.js +41 -15
- package/dist/api/comments.js.map +1 -1
- package/dist/api/invitations.d.ts +2 -0
- package/dist/api/invitations.d.ts.map +1 -1
- package/dist/api/invitations.js +17 -17
- package/dist/api/invitations.js.map +1 -1
- package/dist/api/passwordReset.d.ts +13 -2
- package/dist/api/passwordReset.d.ts.map +1 -1
- package/dist/api/passwordReset.js +47 -12
- package/dist/api/passwordReset.js.map +1 -1
- package/dist/api/team.d.ts +2 -2
- package/dist/api/team.d.ts.map +1 -1
- package/dist/api/team.js +15 -5
- package/dist/api/team.js.map +1 -1
- package/dist/api/tokens.d.ts +1 -1
- package/dist/api/tokens.d.ts.map +1 -1
- package/dist/api/tokens.js +33 -8
- package/dist/api/tokens.js.map +1 -1
- package/dist/api/webhooks.d.ts.map +1 -1
- package/dist/api/webhooks.js +11 -5
- package/dist/api/webhooks.js.map +1 -1
- package/dist/lib/adminAccount.d.ts +0 -5
- package/dist/lib/adminAccount.d.ts.map +1 -1
- package/dist/lib/adminAccount.js +6 -7
- package/dist/lib/adminAccount.js.map +1 -1
- package/dist/lib/clientAddress.d.ts +7 -0
- package/dist/lib/clientAddress.d.ts.map +1 -1
- package/dist/lib/clientAddress.js +15 -0
- package/dist/lib/clientAddress.js.map +1 -1
- package/dist/lib/connectionAuth.d.ts +58 -2
- package/dist/lib/connectionAuth.d.ts.map +1 -1
- package/dist/lib/connectionAuth.js +66 -6
- package/dist/lib/connectionAuth.js.map +1 -1
- package/dist/lib/email.d.ts +16 -0
- package/dist/lib/email.d.ts.map +1 -0
- package/dist/lib/email.js +18 -0
- package/dist/lib/email.js.map +1 -0
- package/dist/middleware/auth.d.ts +63 -4
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +127 -28
- package/dist/middleware/auth.js.map +1 -1
- package/dist/migrations/014_normalize_emails.sql +16 -0
- package/package.json +3 -3
- package/public/assets/AppCenter-DZCsC2wa.js +26 -0
- package/public/assets/AppCenter-DZCsC2wa.js.br +0 -0
- package/public/assets/AppCenter-DZCsC2wa.js.gz +0 -0
- package/public/assets/Default-xD9Fmd5X.js +1 -0
- package/public/assets/Default-xD9Fmd5X.js.br +0 -0
- package/public/assets/Default-xD9Fmd5X.js.gz +0 -0
- package/public/assets/Invite-29Erwv8j.js +1 -0
- package/public/assets/Invite-29Erwv8j.js.br +0 -0
- package/public/assets/Invite-29Erwv8j.js.gz +0 -0
- package/public/assets/{MacResources-fCn40Tf5.js → MacResources-B0DQ9ISE.js} +1 -1
- package/public/assets/MacResources-B0DQ9ISE.js.br +0 -0
- package/public/assets/MacResources-B0DQ9ISE.js.gz +0 -0
- package/public/assets/QASession-D-n6PXuD.js +150 -0
- package/public/assets/QASession-D-n6PXuD.js.br +0 -0
- package/public/assets/QASession-D-n6PXuD.js.gz +0 -0
- package/public/assets/{ResetPassword-D-SFg4ap.js → ResetPassword-BnPIwTYH.js} +1 -1
- package/public/assets/ResetPassword-BnPIwTYH.js.br +0 -0
- package/public/assets/ResetPassword-BnPIwTYH.js.gz +0 -0
- package/public/assets/{Setup-ULjvIfAi.js → Setup-DmUGFblg.js} +1 -1
- package/public/assets/Setup-DmUGFblg.js.br +0 -0
- package/public/assets/Setup-DmUGFblg.js.gz +0 -0
- package/public/assets/Team-BpvRgFfZ.js +6 -0
- package/public/assets/Team-BpvRgFfZ.js.br +0 -0
- package/public/assets/Team-BpvRgFfZ.js.gz +0 -0
- package/public/assets/Tokens-x6BtCcL7.js +1 -0
- package/public/assets/Tokens-x6BtCcL7.js.br +0 -0
- package/public/assets/Tokens-x6BtCcL7.js.gz +0 -0
- package/public/assets/alert-dialog-C8wDimW-.js +1 -0
- package/public/assets/alert-dialog-C8wDimW-.js.br +0 -0
- package/public/assets/alert-dialog-C8wDimW-.js.gz +0 -0
- package/public/assets/{badge-B4QAWrzA.js → badge-CWVbeuH3.js} +1 -1
- package/public/assets/badge-CWVbeuH3.js.br +0 -0
- package/public/assets/badge-CWVbeuH3.js.gz +0 -0
- package/public/assets/{build-format-CtmRXihN.js → build-format-fzt-JrUX.js} +1 -1
- package/public/assets/build-format-fzt-JrUX.js.br +0 -0
- package/public/assets/build-format-fzt-JrUX.js.gz +0 -0
- package/public/assets/{dialog-BAFN_ONP.js → dialog-DQkbaw8h.js} +3 -3
- package/public/assets/dialog-DQkbaw8h.js.br +0 -0
- package/public/assets/dialog-DQkbaw8h.js.gz +0 -0
- package/public/assets/{index-COOWQXpW.js → index-C1Fo8g_c.js} +33 -33
- package/public/assets/index-C1Fo8g_c.js.br +0 -0
- package/public/assets/index-C1Fo8g_c.js.gz +0 -0
- package/public/assets/index-C2F1AgwB.css +1 -0
- package/public/assets/index-C2F1AgwB.css.br +0 -0
- package/public/assets/{index-ozCdBYxk.css.gz → index-C2F1AgwB.css.gz} +0 -0
- package/public/assets/{list-view-Rrqp8TkG.js → list-view-DxI397-H.js} +1 -1
- package/public/assets/list-view-DxI397-H.js.br +0 -0
- package/public/assets/list-view-DxI397-H.js.gz +0 -0
- package/public/assets/{pencil-CAHyK7Qf.js → pencil-BJcOp3oN.js} +1 -1
- package/public/assets/pencil-BJcOp3oN.js.br +0 -0
- package/public/assets/pencil-BJcOp3oN.js.gz +0 -0
- package/public/assets/resource-health-Cq3gqqVd.js +1 -0
- package/public/assets/resource-health-Cq3gqqVd.js.br +0 -0
- package/public/assets/resource-health-Cq3gqqVd.js.gz +0 -0
- package/public/assets/{trash-CkO-5TRA.js → trash-DhBxh6H7.js} +1 -1
- package/public/assets/trash-DhBxh6H7.js.br +0 -0
- package/public/assets/trash-DhBxh6H7.js.gz +0 -0
- package/public/index.html +2 -2
- package/public/index.html.br +0 -0
- package/public/index.html.gz +0 -0
- package/public/assets/AppCenter-BHz6vh5p.js +0 -26
- package/public/assets/AppCenter-BHz6vh5p.js.br +0 -0
- package/public/assets/AppCenter-BHz6vh5p.js.gz +0 -0
- package/public/assets/Default-bCqU0Qz9.js +0 -1
- package/public/assets/Default-bCqU0Qz9.js.br +0 -0
- package/public/assets/Default-bCqU0Qz9.js.gz +0 -0
- package/public/assets/Invite-EVMTsuNz.js +0 -1
- package/public/assets/Invite-EVMTsuNz.js.br +0 -0
- package/public/assets/Invite-EVMTsuNz.js.gz +0 -0
- package/public/assets/MacResources-fCn40Tf5.js.br +0 -0
- package/public/assets/MacResources-fCn40Tf5.js.gz +0 -0
- package/public/assets/QASession-CL-eyFxG.js +0 -150
- package/public/assets/QASession-CL-eyFxG.js.br +0 -0
- package/public/assets/QASession-CL-eyFxG.js.gz +0 -0
- package/public/assets/ResetPassword-D-SFg4ap.js.br +0 -0
- package/public/assets/ResetPassword-D-SFg4ap.js.gz +0 -0
- package/public/assets/Setup-ULjvIfAi.js.br +0 -0
- package/public/assets/Setup-ULjvIfAi.js.gz +0 -0
- package/public/assets/Team-DH91GD4w.js +0 -6
- package/public/assets/Team-DH91GD4w.js.br +0 -0
- package/public/assets/Team-DH91GD4w.js.gz +0 -0
- package/public/assets/Tokens-DImLpOVZ.js +0 -1
- package/public/assets/Tokens-DImLpOVZ.js.br +0 -0
- package/public/assets/Tokens-DImLpOVZ.js.gz +0 -0
- package/public/assets/alert-dialog-CwcUyDZQ.js +0 -1
- package/public/assets/alert-dialog-CwcUyDZQ.js.br +0 -0
- package/public/assets/alert-dialog-CwcUyDZQ.js.gz +0 -0
- package/public/assets/badge-B4QAWrzA.js.br +0 -0
- package/public/assets/badge-B4QAWrzA.js.gz +0 -0
- package/public/assets/build-format-CtmRXihN.js.br +0 -0
- package/public/assets/build-format-CtmRXihN.js.gz +0 -0
- package/public/assets/dialog-BAFN_ONP.js.br +0 -0
- package/public/assets/dialog-BAFN_ONP.js.gz +0 -0
- package/public/assets/index-COOWQXpW.js.br +0 -0
- package/public/assets/index-COOWQXpW.js.gz +0 -0
- package/public/assets/index-ozCdBYxk.css +0 -1
- package/public/assets/index-ozCdBYxk.css.br +0 -0
- package/public/assets/list-view-Rrqp8TkG.js.br +0 -0
- package/public/assets/list-view-Rrqp8TkG.js.gz +0 -0
- package/public/assets/pencil-CAHyK7Qf.js.br +0 -0
- package/public/assets/pencil-CAHyK7Qf.js.gz +0 -0
- package/public/assets/resource-health-BRpSwarg.js +0 -1
- package/public/assets/resource-health-BRpSwarg.js.br +0 -0
- package/public/assets/resource-health-BRpSwarg.js.gz +0 -0
- package/public/assets/trash-CkO-5TRA.js.br +0 -0
- package/public/assets/trash-CkO-5TRA.js.gz +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The form an email address is stored and compared in: surrounding spaces removed, ASCII letters
|
|
3
|
+
* lowercased.
|
|
4
|
+
*
|
|
5
|
+
* **Deliberately the same function as SQLite's `lower(trim(x))`**, not JavaScript's `trim()` and
|
|
6
|
+
* `toLowerCase()`. SQLite's `lower` folds ASCII only and its `trim` removes spaces only, and migration
|
|
7
|
+
* 014 normalizes the rows already stored with those. Were this function wider, an address with a
|
|
8
|
+
* non-ASCII capital or a tab would be rewritten one way by the migration and looked up another way
|
|
9
|
+
* here, and could never sign in again. `EMAIL_KEY_SQL` is the column side of the same comparison.
|
|
10
|
+
*/
|
|
11
|
+
export function normalizeEmail(email) {
|
|
12
|
+
return email.replace(/^ +| +$/g, '').replace(/[A-Z]/g, (c) => c.toLowerCase());
|
|
13
|
+
}
|
|
14
|
+
/** `users.email` / `invitations.email` in normalized form, for comparing with `normalizeEmail(input)`. */
|
|
15
|
+
export const EMAIL_KEY_SQL = 'lower(trim(email))';
|
|
16
|
+
/** The domain of the placeholder address a member invited without an email is given. Mail to it reaches nobody. */
|
|
17
|
+
export const SYNTHETIC_EMAIL_DOMAIN = '@tapflow.local';
|
|
18
|
+
//# sourceMappingURL=email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.js","sourceRoot":"","sources":["../../src/lib/email.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa;IAC1C,OAAO,KAAK,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAA;AAChF,CAAC;AAED,0GAA0G;AAC1G,MAAM,CAAC,MAAM,aAAa,GAAG,oBAAoB,CAAA;AAEjD,mHAAmH;AACnH,MAAM,CAAC,MAAM,sBAAsB,GAAG,gBAAgB,CAAA"}
|
|
@@ -4,16 +4,75 @@ export interface AuthContext {
|
|
|
4
4
|
email: string;
|
|
5
5
|
role: string;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Which password a session was issued under: a keyed digest of the stored hash, carried in the JWT as
|
|
9
|
+
* `pwv` and compared with the row on every check. Changing the password in any way — reset link,
|
|
10
|
+
* self-service change, anything added later — changes the hash and so ends every session issued before
|
|
11
|
+
* it, without each writer having to remember to. A per-user "changed at" column was the alternative,
|
|
12
|
+
* and it needed exactly that memory: the self-service path was missed on paper before any code existed.
|
|
13
|
+
*
|
|
14
|
+
* Keyed with the JWT secret so the claim reveals nothing about the hash. A cookie issued before this
|
|
15
|
+
* claim existed carries none and is refused, which signs everyone in once after the upgrade: accepting
|
|
16
|
+
* it would leave a reset unable to end those sessions for their remaining seven days.
|
|
17
|
+
*/
|
|
18
|
+
export declare function passwordVersion(passwordHash: string | null): string;
|
|
19
|
+
/** Signs a session for `payload.userId`, bound to that user's current password (see `passwordVersion`). */
|
|
7
20
|
export declare function signJwt(payload: AuthContext): string;
|
|
21
|
+
/** The `Set-Cookie` value for a new session. One place, so every issuing path sets the same attributes. */
|
|
22
|
+
export declare function sessionCookie(payload: AuthContext): string;
|
|
8
23
|
export declare function verifyJwtOrThrow(token: string): AuthContext;
|
|
9
24
|
export declare function verifyJwt(token: string): AuthContext | null;
|
|
10
|
-
|
|
25
|
+
/** A signed-in session: who the cookie names, as the users table describes them now. */
|
|
26
|
+
export interface SessionAuth extends AuthContext {
|
|
27
|
+
/** The JWT's `exp` (seconds since the epoch), so an open socket can be closed when it passes. */
|
|
28
|
+
exp?: number;
|
|
29
|
+
/** The password version the session was issued under, so an open socket can be closed when it changes. */
|
|
30
|
+
pwv: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The cookie's user, **checked against the users table on every call**.
|
|
34
|
+
*
|
|
35
|
+
* The JWT lives seven days and removing a member deletes their row, so a signature check alone kept a
|
|
36
|
+
* removed member signed in for up to a week on every cookie path: this function, `requireViewAuth`,
|
|
37
|
+
* `requireBuildAuth`, recordings, and the WebSocket handshake. This is the one place all of them pass,
|
|
38
|
+
* so the row check lives here, and so does the password-version check that ends sessions on a password
|
|
39
|
+
* change. User ids are `AUTOINCREMENT`, so a removed id never comes back and "the
|
|
40
|
+
* row exists" is a sound test. `email` and `role` come from the row, never from the JWT.
|
|
41
|
+
*
|
|
42
|
+
* Only the JWT verification failure becomes `null`. A database error propagates: turning a transient
|
|
43
|
+
* `SQLITE_BUSY` into "not signed in" would sign everyone out and hide the fault.
|
|
44
|
+
*/
|
|
45
|
+
export declare function getAuth(req: http.IncomingMessage): SessionAuth | null;
|
|
46
|
+
export declare function findUser(userId: number): {
|
|
47
|
+
email: string;
|
|
48
|
+
role: string;
|
|
49
|
+
pwv: string;
|
|
50
|
+
} | null;
|
|
11
51
|
export declare function requireAuth(req: http.IncomingMessage, res: http.ServerResponse): AuthContext | null;
|
|
52
|
+
export declare function currentRole(res: http.ServerResponse, userId: number): string | null;
|
|
12
53
|
export declare function requireRole(req: http.IncomingMessage, res: http.ServerResponse, roles: string[]): AuthContext | null;
|
|
13
|
-
export declare function
|
|
54
|
+
export declare function assertCanWrite(res: http.ServerResponse, userId: number): boolean;
|
|
55
|
+
/** A live PAT and its owner, as the database describes them now. */
|
|
56
|
+
export interface PatAuth {
|
|
57
|
+
patId: number;
|
|
14
58
|
userId: number;
|
|
15
|
-
|
|
16
|
-
|
|
59
|
+
scopes: string[];
|
|
60
|
+
ownerEmail: string;
|
|
61
|
+
ownerRole: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The PAT on this request, or null when there is none, it is unknown, or it has expired.
|
|
65
|
+
*
|
|
66
|
+
* **Reads only.** `last_used_at` is written by `touchPat` once the credential check has accepted the
|
|
67
|
+
* token: a token refused there (a scope it lacks, a WebSocket refused for its scope or its owner's
|
|
68
|
+
* role) is not used, and a "last used" that moved on those told an Admin a leaked token was in use
|
|
69
|
+
* when nothing got through. A route that accepts the token and then refuses the request on the
|
|
70
|
+
* owner's role (`assertCanWrite` for a Viewer) still counts as a use — the token did reach the route.
|
|
71
|
+
*/
|
|
72
|
+
export declare function verifyPat(req: http.IncomingMessage): PatAuth | null;
|
|
73
|
+
/** The same lookup by id, for re-checking a token an open socket was accepted with. */
|
|
74
|
+
export declare function findPat(patId: number): PatAuth | null;
|
|
75
|
+
export declare function touchPat(patId: number): void;
|
|
17
76
|
export declare function hashPat(token: string): string;
|
|
18
77
|
export declare function requireViewAuth(req: http.IncomingMessage, res: http.ServerResponse): AuthContext | null;
|
|
19
78
|
export declare function requireBuildAuth(req: http.IncomingMessage, res: http.ServerResponse): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAOvB,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb;AAID,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAOvB,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;CACb;AAID;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAEnE;AAED,2GAA2G;AAC3G,wBAAgB,OAAO,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAWpD;AAED,2GAA2G;AAC3G,wBAAgB,aAAa,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAE1D;AAOD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAM3D;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAM3D;AAED,wFAAwF;AACxF,MAAM,WAAW,WAAY,SAAQ,WAAW;IAC9C,iGAAiG;IACjG,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,0GAA0G;IAC1G,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,WAAW,GAAG,IAAI,CASrE;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAK5F;AAED,wBAAgB,WAAW,CACzB,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,GACvB,WAAW,GAAG,IAAI,CAQpB;AAKD,wBAAgB,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAUnF;AAED,wBAAgB,WAAW,CACzB,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,KAAK,EAAE,MAAM,EAAE,GACd,WAAW,GAAG,IAAI,CAWpB;AAMD,wBAAgB,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAShF;AAED,oEAAoE;AACpE,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,EAAE,MAAM,CAAA;CAClB;AAyBD;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,GAAG,IAAI,CAMnE;AAED,uFAAuF;AACvF,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAGrD;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAE5C;AAED,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,wBAAgB,eAAe,CAC7B,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,GACvB,WAAW,GAAG,IAAI,CAgBpB;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,GACvB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAc3B"}
|
package/dist/middleware/auth.js
CHANGED
|
@@ -4,8 +4,34 @@ import { AuthError } from '@tapflowio/agent-core';
|
|
|
4
4
|
import { getDb } from '../db.js';
|
|
5
5
|
import { getJwtSecret } from '../lib/config.js';
|
|
6
6
|
const JWT_EXPIRES = '7d';
|
|
7
|
+
/**
|
|
8
|
+
* Which password a session was issued under: a keyed digest of the stored hash, carried in the JWT as
|
|
9
|
+
* `pwv` and compared with the row on every check. Changing the password in any way — reset link,
|
|
10
|
+
* self-service change, anything added later — changes the hash and so ends every session issued before
|
|
11
|
+
* it, without each writer having to remember to. A per-user "changed at" column was the alternative,
|
|
12
|
+
* and it needed exactly that memory: the self-service path was missed on paper before any code existed.
|
|
13
|
+
*
|
|
14
|
+
* Keyed with the JWT secret so the claim reveals nothing about the hash. A cookie issued before this
|
|
15
|
+
* claim existed carries none and is refused, which signs everyone in once after the upgrade: accepting
|
|
16
|
+
* it would leave a reset unable to end those sessions for their remaining seven days.
|
|
17
|
+
*/
|
|
18
|
+
export function passwordVersion(passwordHash) {
|
|
19
|
+
return crypto.createHmac('sha256', getJwtSecret()).update(passwordHash ?? '').digest('hex').slice(0, 16);
|
|
20
|
+
}
|
|
21
|
+
/** Signs a session for `payload.userId`, bound to that user's current password (see `passwordVersion`). */
|
|
7
22
|
export function signJwt(payload) {
|
|
8
|
-
|
|
23
|
+
const row = getDb().prepare('SELECT password_hash FROM users WHERE id = ?').get(payload.userId);
|
|
24
|
+
// Picked, not spread: a caller holding a `SessionAuth` would otherwise pass its `exp` along, and
|
|
25
|
+
// `jwt.sign` refuses an `exp` beside `expiresIn`.
|
|
26
|
+
const claims = {
|
|
27
|
+
userId: payload.userId, email: payload.email, role: payload.role,
|
|
28
|
+
pwv: passwordVersion(row?.password_hash ?? null),
|
|
29
|
+
};
|
|
30
|
+
return jwt.sign(claims, getJwtSecret(), { expiresIn: JWT_EXPIRES });
|
|
31
|
+
}
|
|
32
|
+
/** The `Set-Cookie` value for a new session. One place, so every issuing path sets the same attributes. */
|
|
33
|
+
export function sessionCookie(payload) {
|
|
34
|
+
return `tapflow_token=${signJwt(payload)}; HttpOnly; Path=/; SameSite=Lax; Max-Age=${7 * 24 * 3600}`;
|
|
9
35
|
}
|
|
10
36
|
export function verifyJwtOrThrow(token) {
|
|
11
37
|
try {
|
|
@@ -23,12 +49,35 @@ export function verifyJwt(token) {
|
|
|
23
49
|
return null;
|
|
24
50
|
}
|
|
25
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The cookie's user, **checked against the users table on every call**.
|
|
54
|
+
*
|
|
55
|
+
* The JWT lives seven days and removing a member deletes their row, so a signature check alone kept a
|
|
56
|
+
* removed member signed in for up to a week on every cookie path: this function, `requireViewAuth`,
|
|
57
|
+
* `requireBuildAuth`, recordings, and the WebSocket handshake. This is the one place all of them pass,
|
|
58
|
+
* so the row check lives here, and so does the password-version check that ends sessions on a password
|
|
59
|
+
* change. User ids are `AUTOINCREMENT`, so a removed id never comes back and "the
|
|
60
|
+
* row exists" is a sound test. `email` and `role` come from the row, never from the JWT.
|
|
61
|
+
*
|
|
62
|
+
* Only the JWT verification failure becomes `null`. A database error propagates: turning a transient
|
|
63
|
+
* `SQLITE_BUSY` into "not signed in" would sign everyone out and hide the fault.
|
|
64
|
+
*/
|
|
26
65
|
export function getAuth(req) {
|
|
27
66
|
const cookie = req.headers.cookie ?? '';
|
|
28
67
|
const match = /(?:^|;\s*)tapflow_token=([^;]+)/.exec(cookie);
|
|
29
68
|
if (!match)
|
|
30
69
|
return null;
|
|
31
|
-
|
|
70
|
+
const claims = verifyJwt(match[1]);
|
|
71
|
+
if (!claims)
|
|
72
|
+
return null;
|
|
73
|
+
const user = findUser(claims.userId);
|
|
74
|
+
if (!user || claims.pwv !== user.pwv)
|
|
75
|
+
return null;
|
|
76
|
+
return { userId: claims.userId, email: user.email, role: user.role, exp: claims.exp, pwv: user.pwv };
|
|
77
|
+
}
|
|
78
|
+
export function findUser(userId) {
|
|
79
|
+
const row = getDb().prepare('SELECT email, role, password_hash FROM users WHERE id = ?').get(userId);
|
|
80
|
+
return row ? { email: row.email, role: row.role, pwv: passwordVersion(row.password_hash) } : null;
|
|
32
81
|
}
|
|
33
82
|
export function requireAuth(req, res) {
|
|
34
83
|
const auth = getAuth(req);
|
|
@@ -39,34 +88,90 @@ export function requireAuth(req, res) {
|
|
|
39
88
|
}
|
|
40
89
|
return auth;
|
|
41
90
|
}
|
|
91
|
+
// The cookie JWT carries a role for its whole 7-day life, so a demoted member would keep their old
|
|
92
|
+
// rights for a week and a promoted one would be refused for a week. Every role decision reads the
|
|
93
|
+
// users table instead; the JWT only says who is asking. A missing row means the member was removed.
|
|
94
|
+
export function currentRole(res, userId) {
|
|
95
|
+
const row = getDb().prepare('SELECT role FROM users WHERE id = ?').get(userId);
|
|
96
|
+
if (!row) {
|
|
97
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
98
|
+
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return row.role;
|
|
102
|
+
}
|
|
42
103
|
export function requireRole(req, res, roles) {
|
|
43
104
|
const auth = requireAuth(req, res);
|
|
44
105
|
if (!auth)
|
|
45
106
|
return null;
|
|
46
|
-
|
|
107
|
+
const role = currentRole(res, auth.userId);
|
|
108
|
+
if (role === null)
|
|
109
|
+
return null;
|
|
110
|
+
if (!roles.includes(role)) {
|
|
47
111
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
48
112
|
res.end(JSON.stringify({ error: 'Forbidden' }));
|
|
49
113
|
return null;
|
|
50
114
|
}
|
|
51
|
-
return auth;
|
|
115
|
+
return { ...auth, role };
|
|
52
116
|
}
|
|
117
|
+
// Viewer is read-only: it can look at builds, test them in a QA Session and comment, but not change
|
|
118
|
+
// builds, apps or webhooks. Called after a route's own auth, so it never changes which credentials
|
|
119
|
+
// the route accepts. The PAT path matters most here: any role can mint a builds:write token through
|
|
120
|
+
// the API, so the owner's role is what decides, at the moment the token is used.
|
|
121
|
+
export function assertCanWrite(res, userId) {
|
|
122
|
+
const role = currentRole(res, userId);
|
|
123
|
+
if (role === null)
|
|
124
|
+
return false;
|
|
125
|
+
if (role === 'Viewer') {
|
|
126
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
127
|
+
res.end(JSON.stringify({ error: 'Viewers have read-only access' }));
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
function toPatAuth(row) {
|
|
133
|
+
return {
|
|
134
|
+
patId: row.id,
|
|
135
|
+
userId: row.user_id,
|
|
136
|
+
scopes: row.scope.split(',').map((s) => s.trim()),
|
|
137
|
+
ownerEmail: row.email,
|
|
138
|
+
ownerRole: row.role,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
// Joined with users so the owner's current role comes with the token: an `agent` token is only as good
|
|
142
|
+
// as its owner's Admin role, and a Viewer's `builds:write` token is refused on write routes.
|
|
143
|
+
// `datetime(expires_at)`, not the bare column: it is written by `toISOString()` ('2026-09-27T10:00:00.000Z')
|
|
144
|
+
// and `datetime('now')` is '2026-09-27 10:00:00'. Compared as text, 'T' sorts after ' ', so a token that
|
|
145
|
+
// expired this morning kept passing until the end of its UTC day.
|
|
146
|
+
const PAT_SELECT = `
|
|
147
|
+
SELECT pat.id, pat.user_id, pat.scope, u.email, u.role
|
|
148
|
+
FROM personal_access_tokens pat
|
|
149
|
+
JOIN users u ON u.id = pat.user_id
|
|
150
|
+
WHERE (pat.expires_at IS NULL OR datetime(pat.expires_at) > datetime('now'))`;
|
|
151
|
+
/**
|
|
152
|
+
* The PAT on this request, or null when there is none, it is unknown, or it has expired.
|
|
153
|
+
*
|
|
154
|
+
* **Reads only.** `last_used_at` is written by `touchPat` once the credential check has accepted the
|
|
155
|
+
* token: a token refused there (a scope it lacks, a WebSocket refused for its scope or its owner's
|
|
156
|
+
* role) is not used, and a "last used" that moved on those told an Admin a leaked token was in use
|
|
157
|
+
* when nothing got through. A route that accepts the token and then refuses the request on the
|
|
158
|
+
* owner's role (`assertCanWrite` for a Viewer) still counts as a use — the token did reach the route.
|
|
159
|
+
*/
|
|
53
160
|
export function verifyPat(req) {
|
|
54
161
|
const header = req.headers.authorization ?? '';
|
|
55
162
|
if (!header.startsWith('Bearer tflw_pat_'))
|
|
56
163
|
return null;
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
db.prepare(`UPDATE personal_access_tokens SET last_used_at = datetime('now') WHERE token_hash = ?`).run(hash);
|
|
69
|
-
return { userId: row.user_id, scope: row.scope };
|
|
164
|
+
const hash = hashPat(header.slice(7));
|
|
165
|
+
const row = getDb().prepare(`${PAT_SELECT} AND pat.token_hash = ?`).get(hash);
|
|
166
|
+
return row ? toPatAuth(row) : null;
|
|
167
|
+
}
|
|
168
|
+
/** The same lookup by id, for re-checking a token an open socket was accepted with. */
|
|
169
|
+
export function findPat(patId) {
|
|
170
|
+
const row = getDb().prepare(`${PAT_SELECT} AND pat.id = ?`).get(patId);
|
|
171
|
+
return row ? toPatAuth(row) : null;
|
|
172
|
+
}
|
|
173
|
+
export function touchPat(patId) {
|
|
174
|
+
getDb().prepare(`UPDATE personal_access_tokens SET last_used_at = datetime('now') WHERE id = ?`).run(patId);
|
|
70
175
|
}
|
|
71
176
|
export function hashPat(token) {
|
|
72
177
|
return crypto.createHash('sha256').update(token).digest('hex');
|
|
@@ -81,29 +186,23 @@ export function requireViewAuth(req, res) {
|
|
|
81
186
|
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
82
187
|
return null;
|
|
83
188
|
}
|
|
84
|
-
if (!pat.
|
|
189
|
+
if (!pat.scopes.includes('view')) {
|
|
85
190
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
86
191
|
res.end(JSON.stringify({ error: 'Insufficient scope' }));
|
|
87
192
|
return null;
|
|
88
193
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
.get(pat.userId);
|
|
92
|
-
if (!user) {
|
|
93
|
-
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
94
|
-
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
return { userId: pat.userId, email: user.email, role: user.role };
|
|
194
|
+
touchPat(pat.patId);
|
|
195
|
+
return { userId: pat.userId, email: pat.ownerEmail, role: pat.ownerRole };
|
|
98
196
|
}
|
|
99
197
|
export function requireBuildAuth(req, res) {
|
|
100
198
|
const pat = verifyPat(req);
|
|
101
199
|
if (pat !== null) {
|
|
102
|
-
if (!pat.
|
|
200
|
+
if (!pat.scopes.includes('builds:write')) {
|
|
103
201
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
104
202
|
res.end(JSON.stringify({ error: 'Insufficient scope' }));
|
|
105
203
|
return null;
|
|
106
204
|
}
|
|
205
|
+
touchPat(pat.patId);
|
|
107
206
|
return { userId: pat.userId };
|
|
108
207
|
}
|
|
109
208
|
const auth = requireAuth(req, res);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,OAAO,GAAG,MAAM,cAAc,CAAA;AAC9B,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AAQ/C,MAAM,WAAW,GAAG,IAAI,CAAA;AAExB,MAAM,UAAU,OAAO,CAAC,OAAoB;IAC1C,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC,CAAA;
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Store email addresses in the form `normalizeEmail` (lib/email.ts) produces: spaces trimmed, ASCII
|
|
2
|
+
-- lowercased. Before this, `Alice@x.com` and `alice@x.com` could be two accounts, and login matched
|
|
3
|
+
-- case exactly.
|
|
4
|
+
--
|
|
5
|
+
-- A row whose normalized address another row already has, or would have, is left untouched: rewriting
|
|
6
|
+
-- it would violate `UNIQUE(email)`, abort this migration, and with it the relay's boot. The relay logs
|
|
7
|
+
-- those groups at startup so an Admin can remove the duplicate.
|
|
8
|
+
UPDATE users SET email = lower(trim(email))
|
|
9
|
+
WHERE email <> lower(trim(email))
|
|
10
|
+
AND NOT EXISTS (
|
|
11
|
+
SELECT 1 FROM users other
|
|
12
|
+
WHERE other.id <> users.id AND lower(trim(other.email)) = lower(trim(users.email))
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
UPDATE invitations SET email = lower(trim(email))
|
|
16
|
+
WHERE email IS NOT NULL AND email <> lower(trim(email));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tapflowio/relay",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"description": "WebSocket relay server for tapflow — NAT traversal, session routing, JWT auth, bundled dashboard",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"tapflow",
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
"nodemailer": "^9.1.1",
|
|
53
53
|
"ws": "^8.21.3",
|
|
54
54
|
"zod": "^4.6.5",
|
|
55
|
-
"@tapflowio/agent-core": "0.
|
|
56
|
-
"@tapflowio/protocol": "0.
|
|
55
|
+
"@tapflowio/agent-core": "0.26.0",
|
|
56
|
+
"@tapflowio/protocol": "0.26.0"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/better-sqlite3": "^7.6.13",
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
var Ue=l=>{throw TypeError(l)};var Ee=(l,e,s)=>e.has(l)||Ue("Cannot "+s);var k=(l,e,s)=>(Ee(l,e,"read from private field"),s?s.call(l):e.get(l)),fe=(l,e,s)=>e.has(l)?Ue("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(l):e.set(l,s),me=(l,e,s,a)=>(Ee(l,e,"write to private field"),a?a.call(l,s):e.set(l,s),s),he=(l,e,s)=>(Ee(l,e,"access private method"),s);import{k as c,f as pe,j as t}from"./vendor-react-BlPSzrEe.js";import{z as xt,as as gt,ak as Ve,ac as bt,ao as et,aJ as tt,an as jt,at as yt,a2 as ve,av as J,B as re,L as ye,W as vt,I as He,F as St,Z as Nt,S as wt,G as Dt,T as kt,H as Ct,E as _t,f as At,ay as It,aI as We,aj as Et,Q as Ot,ar as Rt,ax as Ft,r as Tt,aq as $t,a7 as Mt,am as Pt,aa as Bt}from"./index-C1Fo8g_c.js";import{f as zt,d as Fe,S as Lt,P as qt,b as Qt,e as Kt}from"./build-format-fzt-JrUX.js";import{f as st,d as at,e as lt,i as ke,j as Ce,g as _e,h as F,S as Ae,a as nt,D as rt,C as Ut}from"./dialog-DQkbaw8h.js";import{P as Vt,T as Ht}from"./trash-DhBxh6H7.js";import{B as Oe}from"./badge-CWVbeuH3.js";import{f as Wt,g as Jt,d as Gt,b as Xt,c as Yt,e as Zt,a as es,A as ts}from"./alert-dialog-C8wDimW-.js";import{u as ss}from"./useFocusAfterSwap-BY3N9f9v.js";import{b as as,c as ls}from"./vendor-router-B_lVHrKl.js";var le,de,M,ne,se,je,Te,Ze,ns=(Ze=class extends xt{constructor(e,s){super();fe(this,se);fe(this,le);fe(this,de);fe(this,M);fe(this,ne);me(this,le,e),this.setOptions(s),this.bindMethods(),he(this,se,je).call(this)}bindMethods(){this.mutate=this.mutate.bind(this),this.reset=this.reset.bind(this)}setOptions(e){var a;const s=this.options;this.options=k(this,le).defaultMutationOptions(e),gt(this.options,s)||k(this,le).getMutationCache().notify({type:"observerOptionsUpdated",mutation:k(this,M),observer:this}),s!=null&&s.mutationKey&&this.options.mutationKey&&Ve(s.mutationKey)!==Ve(this.options.mutationKey)?this.reset():((a=k(this,M))==null?void 0:a.state.status)==="pending"&&k(this,M).setOptions(this.options)}onSubscribe(){this.listeners.size===1&&k(this,M)&&(k(this,M).addObserver(this),he(this,se,je).call(this))}onUnsubscribe(){var e;this.hasListeners()||(e=k(this,M))==null||e.removeObserver(this)}onMutationUpdate(e){he(this,se,je).call(this),he(this,se,Te).call(this,e)}getCurrentResult(){return k(this,de)}reset(){var e;(e=k(this,M))==null||e.removeObserver(this),me(this,M,void 0),he(this,se,je).call(this),he(this,se,Te).call(this)}mutate(e,s){var a;return me(this,ne,s),(a=k(this,M))==null||a.removeObserver(this),me(this,M,k(this,le).getMutationCache().build(k(this,le),this.options)),k(this,M).addObserver(this),k(this,M).execute(e)}},le=new WeakMap,de=new WeakMap,M=new WeakMap,ne=new WeakMap,se=new WeakSet,je=function(){var s;const e=((s=k(this,M))==null?void 0:s.state)??bt();me(this,de,{...e,isPending:e.status==="pending",isSuccess:e.status==="success",isError:e.status==="error",isIdle:e.status==="idle",mutate:this.mutate,reset:this.reset})},Te=function(e){et.batch(()=>{var s,a,i,u,f,j,b,m;if(k(this,ne)&&this.hasListeners()){const d=k(this,de).variables,y=k(this,de).context,p={client:k(this,le),meta:this.options.meta,mutationKey:this.options.mutationKey};if((e==null?void 0:e.type)==="success"){try{(a=(s=k(this,ne)).onSuccess)==null||a.call(s,e.data,d,y,p)}catch(r){Promise.reject(r)}try{(u=(i=k(this,ne)).onSettled)==null||u.call(i,e.data,null,d,y,p)}catch(r){Promise.reject(r)}}else if((e==null?void 0:e.type)==="error"){try{(j=(f=k(this,ne)).onError)==null||j.call(f,e.error,d,y,p)}catch(r){Promise.reject(r)}try{(m=(b=k(this,ne)).onSettled)==null||m.call(b,void 0,e.error,d,y,p)}catch(r){Promise.reject(r)}}}this.listeners.forEach(d=>{d(k(this,de))})})},Ze);function Re(l,e){const s=tt(),[a]=c.useState(()=>new ns(s,l));c.useEffect(()=>{a.setOptions(l)},[a,l]);const i=c.useSyncExternalStore(c.useCallback(f=>a.subscribe(et.batchCalls(f)),[a]),()=>a.getCurrentResult(),()=>a.getCurrentResult()),u=c.useCallback((...f)=>{a.mutate(f[0],f[1]).catch(jt)},[a]);if(i.error&&yt(a.options.throwOnError,[i.error]))throw i.error;return{...i,mutate:u,mutateAsync:i.mutate}}/**
|
|
2
|
+
* @license lucide-react v1.47.0 - ISC
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the ISC license.
|
|
5
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/const it={name:"ban",size:24,node:[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M4.929 4.929 19.07 19.071",key:"196cmz"}]]};it.node;const rs=ve(it);/**
|
|
7
|
+
* @license lucide-react v1.47.0 - ISC
|
|
8
|
+
*
|
|
9
|
+
* This source code is licensed under the ISC license.
|
|
10
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
11
|
+
*/const ot={name:"layers",size:24,node:[["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"}]],aliases:["layers-3"]};ot.node;const is=ve(ot);/**
|
|
12
|
+
* @license lucide-react v1.47.0 - ISC
|
|
13
|
+
*
|
|
14
|
+
* This source code is licensed under the ISC license.
|
|
15
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
16
|
+
*/const ct={name:"package",size:24,node:[["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"}]]};ct.node;const os=ve(ct);/**
|
|
17
|
+
* @license lucide-react v1.47.0 - ISC
|
|
18
|
+
*
|
|
19
|
+
* This source code is licensed under the ISC license.
|
|
20
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
21
|
+
*/const dt={name:"timer-off",size:24,node:[["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"}]]};dt.node;const cs=ve(dt);/**
|
|
22
|
+
* @license lucide-react v1.47.0 - ISC
|
|
23
|
+
*
|
|
24
|
+
* This source code is licensed under the ISC license.
|
|
25
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
26
|
+
*/const ut={name:"upload",size:24,node:[["path",{d:"M12 3v12",key:"1x0j5s"}],["path",{d:"m17 8-5-5-5 5",key:"7q97r8"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}]]};ut.node;const Je=ve(ut);function ds(l){const e=pe.c(37),{onSuccess:s,appId:a,canWrite:i}=l,[u,f]=c.useState(!1),[j,b]=c.useState(null),[m,d]=c.useState("none"),y=c.useRef(null);let p;e[0]!==a||e[1]!==j||e[2]!==s||e[3]!==m?(p=function(D){if(D.preventDefault(),!j)return;const O=new FormData;O.append("file",j),m!=="none"&&O.append("status",m),a&&O.append("app_id",String(a));const Z=fetch("/api/v1/builds",{method:"POST",credentials:"include",body:O}).then(async V=>{if(!V.ok){const ue=await V.json().catch(ms);throw new Error(ue.error??"Upload failed. Check the file format.")}s()});J.promise(Z,{loading:"Uploading build…",success:"Build uploaded",error:fs}),f(!1),b(null),d("none")},e[0]=a,e[1]=j,e[2]=s,e[3]=m,e[4]=p):p=e[4];const r=p;let N;e[5]===Symbol.for("react.memo_cache_sentinel")?(N=function(D){f(D),D||(b(null),d("none"))},e[5]=N):N=e[5];const v=N;if(!i){let w;return e[6]===Symbol.for("react.memo_cache_sentinel")?(w=t.jsxs(re,{size:"sm",onClick:hs,children:[t.jsx(Je,{className:"mr-2 h-4 w-4"}),"Upload build"]}),e[6]=w):w=e[6],w}let g;e[7]===Symbol.for("react.memo_cache_sentinel")?(g=t.jsx(st,{asChild:!0,children:t.jsxs(re,{size:"sm",children:[t.jsx(Je,{className:"mr-2 h-4 w-4"}),"Upload build"]})}),e[7]=g):g=e[7];let C;e[8]===Symbol.for("react.memo_cache_sentinel")?(C=t.jsx(at,{children:t.jsx(lt,{children:"Upload build"})}),e[8]=C):C=e[8];let E;e[9]===Symbol.for("react.memo_cache_sentinel")?(E=t.jsx(ye,{children:"File"}),e[9]=E):E=e[9];let x;e[10]===Symbol.for("react.memo_cache_sentinel")?(x=()=>{var w;return(w=y.current)==null?void 0:w.click()},e[10]=x):x=e[10];let _;e[11]===Symbol.for("react.memo_cache_sentinel")?(_=w=>{w.preventDefault();const D=w.dataTransfer.files[0];D&&b(D)},e[11]=_):_=e[11];const P=j?j.name:"Click or drag to upload";let T;e[12]!==P?(T=t.jsx("div",{className:"flex h-24 cursor-pointer items-center justify-center rounded-md border-2 border-dashed text-sm text-muted-foreground hover:border-primary",onClick:x,onDragOver:us,onDrop:_,children:P}),e[12]=P,e[13]=T):T=e[13];let B;e[14]===Symbol.for("react.memo_cache_sentinel")?(B=t.jsx("input",{ref:y,type:"file",accept:".zip,.apk,.tgz,.tar.gz,application/gzip,application/x-gzip",className:"hidden",onChange:w=>{var D;return b(((D=w.target.files)==null?void 0:D[0])??null)}}),e[14]=B):B=e[14];let L;e[15]===Symbol.for("react.memo_cache_sentinel")?(L=t.jsx("code",{children:".app.zip"}),e[15]=L):L=e[15];let G;e[16]===Symbol.for("react.memo_cache_sentinel")?(G=t.jsx("code",{children:".tar.gz"}),e[16]=G):G=e[16];let X;e[17]===Symbol.for("react.memo_cache_sentinel")?(X=t.jsx("code",{children:".tgz"}),e[17]=X):X=e[17];let H;e[18]===Symbol.for("react.memo_cache_sentinel")?(H=t.jsxs("p",{className:"text-xs text-muted-foreground",children:["iOS: ",L," or ",G," / ",X," (e.g. an EAS simulator build) · Android: ",t.jsx("code",{children:".apk"})]}),e[18]=H):H=e[18];let q;e[19]!==T?(q=t.jsxs("div",{className:"grid gap-2",children:[E,T,B,H]}),e[19]=T,e[20]=q):q=e[20];let Q;e[21]===Symbol.for("react.memo_cache_sentinel")?(Q=t.jsxs(ye,{children:["Status ",t.jsx("span",{className:"text-muted-foreground",children:"(optional)"})]}),e[21]=Q):Q=e[21];let Y;e[22]===Symbol.for("react.memo_cache_sentinel")?(Y=t.jsx(ke,{children:t.jsx(Ce,{})}),e[22]=Y):Y=e[22];let K;e[23]===Symbol.for("react.memo_cache_sentinel")?(K=t.jsxs(_e,{children:[t.jsx(F,{value:"none",children:"None"}),t.jsx(F,{value:"Backlog",children:"Backlog"}),t.jsx(F,{value:"In Progress",children:"In Progress"}),t.jsx(F,{value:"Done",children:"Done"}),t.jsx(F,{value:"Rejected",children:"Rejected"})]}),e[23]=K):K=e[23];let z;e[24]!==m?(z=t.jsxs("div",{className:"grid gap-2",children:[Q,t.jsxs(Ae,{value:m,onValueChange:d,children:[Y,K]})]}),e[24]=m,e[25]=z):z=e[25];let U;e[26]===Symbol.for("react.memo_cache_sentinel")?(U=t.jsx(re,{type:"button",variant:"outline",className:"w-24",onClick:()=>f(!1),children:"Cancel"}),e[26]=U):U=e[26];const W=!j;let A;e[27]!==W?(A=t.jsxs("div",{className:"flex justify-end gap-2",children:[U,t.jsx(re,{type:"submit",className:"w-24",disabled:W,children:"Upload"})]}),e[27]=W,e[28]=A):A=e[28];let $;e[29]!==r||e[30]!==q||e[31]!==z||e[32]!==A?($=t.jsxs(nt,{className:"sm:max-w-md",children:[C,t.jsxs("form",{onSubmit:r,className:"flex flex-col gap-4 pt-2",children:[q,z,A]})]}),e[29]=r,e[30]=q,e[31]=z,e[32]=A,e[33]=$):$=e[33];let R;return e[34]!==u||e[35]!==$?(R=t.jsxs(rt,{open:u,onOpenChange:v,children:[g,$]}),e[34]=u,e[35]=$,e[36]=R):R=e[36],R}function us(l){return l.preventDefault()}function hs(){return J.error("Viewers can't upload builds. Ask an Admin for QA or Developer access.")}function fs(l){return l instanceof Error?l.message:"Failed to upload build"}function ms(){return{}}function De(l){const e=pe.c(8);let s,a;e[0]!==l?({className:s,...a}=l,e[0]=l,e[1]=s,e[2]=a):(s=e[1],a=e[2]);let i;e[3]!==s?(i=vt("font-mono text-xs tabular-nums tracking-tight",s),e[3]=s,e[4]=i):i=e[4];let u;return e[5]!==a||e[6]!==i?(u=t.jsx("span",{className:i,...a}),e[5]=a,e[6]=i,e[7]=u):u=e[7],u}function ps({onSuccess:l,canWrite:e}){const[s,a]=c.useState(!1),[i,u]=c.useState(""),[f,j]=c.useState(""),[b,m]=c.useState("ios"),[d,y]=c.useState(""),[p,r]=c.useState(!1);async function N(g){g.preventDefault(),y(""),r(!0);try{const C=await Nt({name:i.trim(),bundle_id_key:f.trim(),platform:b});if(!C){y("Failed to create app");return}if("error"in C){y(C.error);return}a(!1),u(""),j(""),m("ios"),J.success("App created"),l()}catch{y("Failed to create app — check your network"),J.error("Failed to create app — check your network")}finally{r(!1)}}const v=g=>t.jsxs("button",{type:"button",onClick:g,className:"flex w-full items-center gap-1.5 rounded-md px-2 py-1.5 text-left text-sm text-muted-foreground hover:bg-accent hover:text-foreground transition-colors",children:[t.jsx(Vt,{className:"size-3.5 shrink-0"}),t.jsx("span",{children:"Add App"})]});return e?t.jsxs(rt,{open:s,onOpenChange:a,children:[t.jsx(st,{asChild:!0,children:v()}),t.jsxs(nt,{className:"sm:max-w-sm",children:[t.jsx(at,{children:t.jsx(lt,{children:"Add App"})}),t.jsxs("form",{onSubmit:N,className:"flex flex-col gap-4 pt-2",children:[t.jsxs("div",{className:"flex flex-col gap-1.5",children:[t.jsx(ye,{htmlFor:"app-name",children:"Name"}),t.jsx(He,{id:"app-name",value:i,onChange:g=>u(g.target.value),placeholder:"My App",required:!0})]}),t.jsxs("div",{className:"flex flex-col gap-1.5",children:[t.jsx(ye,{htmlFor:"bundle-id",children:"Bundle ID"}),t.jsx(He,{id:"bundle-id",value:f,onChange:g=>j(g.target.value),placeholder:"com.example.myapp",required:!0})]}),t.jsxs("div",{className:"flex flex-col gap-1.5",children:[t.jsx(ye,{htmlFor:"platform",children:"Platform"}),t.jsxs(Ae,{value:b,onValueChange:g=>m(g),children:[t.jsx(ke,{id:"platform",children:t.jsx(Ce,{})}),t.jsxs(_e,{children:[t.jsx(F,{value:"ios",children:"iOS"}),t.jsx(F,{value:"android",children:"Android"}),t.jsx(F,{value:"both",children:"Both"})]})]})]}),t.jsx(St,{assertive:!0,id:"add-app-error",message:d||void 0}),t.jsx(re,{type:"submit",disabled:p,className:"w-full",children:p?"Creating…":"Create App"})]})]})]}):v(()=>J.error("Viewers can't add apps. Ask an Admin for QA or Developer access."))}function xs(l){const e=pe.c(17),{apps:s,selectedAppId:a,onSelect:i,onAdd:u,canWrite:f}=l;let j;e[0]===Symbol.for("react.memo_cache_sentinel")?(j=t.jsx("span",{className:"px-2 pb-1 font-mono text-xs font-medium text-muted-foreground uppercase tracking-wider",children:"Apps"}),e[0]=j):j=e[0];let b;e[1]!==s.length?(b=s.length===0&&t.jsx("span",{className:"px-2 text-sm text-muted-foreground",children:"No apps yet"}),e[1]=s.length,e[2]=b):b=e[2];let m;if(e[3]!==s||e[4]!==i||e[5]!==a){let p;e[7]!==i||e[8]!==a?(p=r=>t.jsxs("button",{onClick:()=>i(r.id),className:["flex flex-col gap-0.5 rounded-md px-2 py-2 text-left text-sm hover:bg-accent overflow-hidden",r.id===a?"bg-accent font-medium":""].join(" "),children:[t.jsx("span",{className:"truncate",children:r.name}),t.jsx(De,{className:"text-muted-foreground truncate",children:r.bundle_id_key})]},r.id),e[7]=i,e[8]=a,e[9]=p):p=e[9],m=s.map(p),e[3]=s,e[4]=i,e[5]=a,e[6]=m}else m=e[6];let d;e[10]!==f||e[11]!==u?(d=t.jsx("div",{className:"mt-1 border-t pt-1",children:t.jsx(ps,{onSuccess:u,canWrite:f})}),e[10]=f,e[11]=u,e[12]=d):d=e[12];let y;return e[13]!==b||e[14]!==m||e[15]!==d?(y=t.jsxs("aside",{className:"w-64 shrink-0 border-r flex flex-col gap-1 p-3 overflow-y-auto",children:[j,b,m,d]}),e[13]=b,e[14]=m,e[15]=d,e[16]=y):y=e[16],y}function gs(l){const e=pe.c(70),{build:s,isLast:a,onNavigate:i,onStatusChange:u,onScheduleDeletion:f,onCancelDeletion:j,canWrite:b,statusDescribedBy:m}=l,[d,y]=c.useState(!1),p=c.useRef(null),r=s.status_label==="Done";let N;e[0]!==s.delete_after?(N=s.delete_after?zt(s.delete_after):null,e[0]=s.delete_after,e[1]=N):N=e[1];const v=N;let g;e[2]!==s?(g=Fe(s),e[2]=s,e[3]=g):g=e[3];const C=g;let E;e[4]!==s.id||e[5]!==u?(E=function(ce){u(s.id,ce==="none"?null:ce)},e[4]=s.id,e[5]=u,e[6]=E):E=e[6];const x=E;let _;e[7]===Symbol.for("react.memo_cache_sentinel")?(_=xe=>{var ce;xe.preventDefault(),(ce=p.current)==null||ce.focus()},e[7]=_):_=e[7];let P;e[8]===Symbol.for("react.memo_cache_sentinel")?(P=t.jsxs(Wt,{children:[t.jsx(Jt,{children:"Schedule deletion?"}),t.jsx(Gt,{children:"Build files will be deleted after 7 days. You can cancel the scheduled deletion anytime before then."})]}),e[8]=P):P=e[8];let T;e[9]===Symbol.for("react.memo_cache_sentinel")?(T=t.jsx(Xt,{children:"Cancel"}),e[9]=T):T=e[9];let B;e[10]!==s.id||e[11]!==f?(B=t.jsxs(Yt,{onCloseAutoFocus:_,children:[P,t.jsxs(Zt,{children:[T,t.jsx(es,{onClick:()=>{f(s.id),y(!1)},children:"Schedule deletion"})]})]}),e[10]=s.id,e[11]=f,e[12]=B):B=e[12];let L;e[13]!==d||e[14]!==B?(L=t.jsx(ts,{open:d,onOpenChange:y,children:B}),e[13]=d,e[14]=B,e[15]=L):L=e[15];const G=a?"":"border-b";let X;e[16]!==G?(X=["flex items-center gap-3 px-4 py-3",G],e[16]=G,e[17]=X):X=e[17];const H=X.join(" "),q=s.build_number??"—";let Q;e[18]!==q?(Q=t.jsxs("span",{className:"text-sm font-medium",children:["build ",t.jsx(De,{children:q})]}),e[18]=q,e[19]=Q):Q=e[19];const Y=s.platform==="ios"?"ios":"android";let K;e[20]!==s.platform||e[21]!==Y?(K=t.jsx(Oe,{tone:Y,className:"text-xs capitalize",children:s.platform}),e[20]=s.platform,e[21]=Y,e[22]=K):K=e[22];let z;e[23]!==s.status_label?(z=s.status_label&&t.jsx(Oe,{tone:Lt[s.status_label],className:"text-xs",children:s.status_label}),e[23]=s.status_label,e[24]=z):z=e[24];let U;e[25]!==v?(U=v&&t.jsx(Oe,{tone:v.urgent?"rejected":"backlog",className:"text-xs",children:v.label}),e[25]=v,e[26]=U):U=e[26];let W;e[27]!==Q||e[28]!==K||e[29]!==z||e[30]!==U?(W=t.jsxs("div",{className:"flex items-center gap-2 flex-wrap",children:[Q,K,z,U]}),e[27]=Q,e[28]=K,e[29]=z,e[30]=U,e[31]=W):W=e[31];let A;e[32]===Symbol.for("react.memo_cache_sentinel")?(A={display:"flex",flexDirection:"row",gap:"14px",marginTop:"0.7rem",alignItems:"center"},e[32]=A):A=e[32];let $;e[33]!==s.uploader?($=s.uploader&&t.jsxs(t.Fragment,{children:[t.jsx("span",{children:s.uploader}),t.jsx(wt,{orientation:"vertical",className:"h-3"})]}),e[33]=s.uploader,e[34]=$):$=e[34];let R;e[35]!==s.uploaded_at?(R=new Date(s.uploaded_at).toLocaleDateString(),e[35]=s.uploaded_at,e[36]=R):R=e[36];let w;e[37]!==R?(w=t.jsx(De,{children:R}),e[37]=R,e[38]=w):w=e[38];let D;e[39]!==$||e[40]!==w?(D=t.jsxs("div",{style:A,className:"text-xs text-muted-foreground",children:[$,w]}),e[39]=$,e[40]=w,e[41]=D):D=e[41];let O;e[42]!==W||e[43]!==D?(O=t.jsxs("div",{className:"flex-1 min-w-0",children:[W,D]}),e[42]=W,e[43]=D,e[44]=O):O=e[44];let Z;e[45]!==s.id||e[46]!==s.status_label||e[47]!==b||e[48]!==v||e[49]!==x||e[50]!==j||e[51]!==C||e[52]!==m?(Z=b&&t.jsxs(t.Fragment,{children:[t.jsxs(Ae,{value:s.status_label??"none",onValueChange:x,children:[t.jsx(ke,{className:"h-9 w-32 text-xs","aria-label":`Status for ${C}`,"aria-describedby":m,"data-status-trigger":s.id,children:t.jsx(Ce,{})}),t.jsxs(_e,{children:[t.jsx(F,{value:"none",children:t.jsx("span",{className:"text-muted-foreground",children:"—"})}),t.jsx(F,{value:"Backlog",children:"Backlog"}),t.jsx(F,{value:"In Progress",children:"In Progress"}),t.jsx(F,{value:"Done",children:"Done"}),t.jsx(F,{value:"Rejected",children:"Rejected"})]})]}),t.jsx(Dt,{delayDuration:400,children:t.jsxs(kt,{children:[t.jsx(Ct,{asChild:!0,children:v?t.jsx(re,{ref:p,size:"icon-sm",variant:"outline",onClick:()=>j(s.id),"aria-label":`Cancel scheduled deletion of ${C}`,children:t.jsx(cs,{className:"h-3.5 w-3.5","aria-hidden":!0})}):t.jsx(re,{ref:p,size:"icon-sm",variant:"destructive",onClick:()=>y(!0),"aria-label":`Schedule deletion of ${C}`,children:t.jsx(Ht,{className:"h-3.5 w-3.5","aria-hidden":!0})})}),t.jsx(_t,{children:v?"Cancel scheduled deletion":"Schedule deletion"})]})})]}),e[45]=s.id,e[46]=s.status_label,e[47]=b,e[48]=v,e[49]=x,e[50]=j,e[51]=C,e[52]=m,e[53]=Z):Z=e[53];let V;e[54]!==s.id||e[55]!==i?(V=()=>i(s.id),e[54]=s.id,e[55]=i,e[56]=V):V=e[56];const ue=`Start QA on ${C}`;let ie;e[57]===Symbol.for("react.memo_cache_sentinel")?(ie=t.jsx(qt,{className:"mr-1.5 h-3.5 w-3.5"}),e[57]=ie):ie=e[57];let oe;e[58]!==r||e[59]!==V||e[60]!==ue?(oe=t.jsxs(re,{size:"sm",onClick:V,disabled:r,"aria-label":ue,children:[ie,"Start QA"]}),e[58]=r,e[59]=V,e[60]=ue,e[61]=oe):oe=e[61];let ae;e[62]!==H||e[63]!==O||e[64]!==Z||e[65]!==oe?(ae=t.jsxs("div",{className:H,children:[O,Z,oe]}),e[62]=H,e[63]=O,e[64]=Z,e[65]=oe,e[66]=ae):ae=e[66];let te;return e[67]!==ae||e[68]!==L?(te=t.jsxs(t.Fragment,{children:[L,ae]}),e[67]=ae,e[68]=L,e[69]=te):te=e[69],te}function bs(l){const e=pe.c(29),{versionName:s,builds:a,isOpen:i,onToggle:u,onNavigate:f,onStatusChange:j,onScheduleDeletion:b,onCancelDeletion:m,canWrite:d,describedBy:y,rowNote:p}=l,r=c.useId(),N=i?r:void 0;let v;e[0]!==i?(v=i?t.jsx(Ut,{className:"h-4 w-4 shrink-0 text-muted-foreground"}):t.jsx(At,{className:"h-4 w-4 shrink-0 text-muted-foreground"}),e[0]=i,e[1]=v):v=e[1];let g;e[2]!==s?(g=t.jsx(De,{className:"font-medium text-sm text-foreground",children:s}),e[2]=s,e[3]=g):g=e[3];const C=a.length>1?"s":"";let E;e[4]!==a.length||e[5]!==C?(E=t.jsxs("span",{className:"text-xs text-muted-foreground",children:["(",a.length," build",C,")"]}),e[4]=a.length,e[5]=C,e[6]=E):E=e[6];let x;e[7]!==y||e[8]!==i||e[9]!==u||e[10]!==N||e[11]!==v||e[12]!==g||e[13]!==E||e[14]!==s?(x=t.jsx("h2",{children:t.jsxs("button",{className:"flex w-full items-center gap-2 px-4 py-3 text-left hover:bg-accent/50 bg-card focus-visible:outline-none focus-visible:outline-offset-[-2px] focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",onClick:u,"aria-expanded":i,"aria-controls":N,"aria-describedby":y,"data-release-header":s,children:[v,g,E]})}),e[7]=y,e[8]=i,e[9]=u,e[10]=N,e[11]=v,e[12]=g,e[13]=E,e[14]=s,e[15]=x):x=e[15];let _;e[16]!==a||e[17]!==d||e[18]!==i||e[19]!==m||e[20]!==f||e[21]!==b||e[22]!==j||e[23]!==r||e[24]!==p?(_=i&&t.jsx("div",{id:r,className:"border-t bg-card",children:a.map((T,B)=>t.jsx(gs,{build:T,isLast:B===a.length-1,onNavigate:f,onStatusChange:j,onScheduleDeletion:b,onCancelDeletion:m,canWrite:d,statusDescribedBy:(p==null?void 0:p.buildId)===T.id?p.id:void 0},T.id))}),e[16]=a,e[17]=d,e[18]=i,e[19]=m,e[20]=f,e[21]=b,e[22]=j,e[23]=r,e[24]=p,e[25]=_):_=e[25];let P;return e[26]!==x||e[27]!==_?(P=t.jsxs("div",{className:"rounded-lg shadow-card-2 overflow-hidden",children:[x,_]}),e[26]=x,e[27]=_,e[28]=P):P=e[28],P}const ht="tapflow-app-center-releases:";function js(l){try{const e=JSON.parse(localStorage.getItem(ht+l)??"[]");return Array.isArray(e)?e.filter(s=>Array.isArray(s)&&s.length===2&&typeof s[0]=="string"&&typeof s[1]=="boolean"):[]}catch{return[]}}function ys(l,e){try{localStorage.setItem(ht+l,JSON.stringify(e))}catch{}}function vs(l,e){const s=pe.c(13),[a,i]=c.useState(Ss);let u;s[0]!==l||s[1]!==a?(u=l===null?[]:a.get(l)??js(l),s[0]=l,s[1]=a,s[2]=u):u=s[2];const f=u,j=e[0];let b;s[3]!==j||s[4]!==f?(b=r=>{var N;return((N=f.find(v=>{const[g]=v;return g===r}))==null?void 0:N[1])??r===j},s[3]=j,s[4]=f,s[5]=b):b=s[5];const m=b;let d;s[6]!==l||s[7]!==m||s[8]!==f?(d=r=>{if(l===null)return;const N=[...f.filter(v=>{const[g]=v;return g!==r}),[r,!m(r)]].slice(-200);ys(l,N),i(v=>new Map(v).set(l,N))},s[6]=l,s[7]=m,s[8]=f,s[9]=d):d=s[9];const y=d;let p;return s[10]!==m||s[11]!==y?(p={isOpen:m,toggle:y},s[10]=m,s[11]=y,s[12]=p):p=s[12],p}function Ss(){return new Map}const Ne=l=>`row:${l}`,we=l=>`release:${l}`;function Ge(l){return l instanceof HTMLElement?l.dataset.statusTrigger!==void 0?Ne(Number(l.dataset.statusTrigger)):l.dataset.releaseHeader!==void 0?we(l.dataset.releaseHeader):null:null}function Xe(l,e){const s=new Set(JSON.parse(e));return l.find(a=>s.has(a))??null}function Ye(l,e){return l.map((s,a)=>({item:s,d:a>e?2*(a-e)-1:2*(e-a)})).filter(({d:s})=>s>0).sort((s,a)=>s.d-a.d).map(({item:s})=>s)}function Rs(){var Qe;const l=as(),[e,s]=ls(),a=tt(),{user:i}=It(),u=(i==null?void 0:i.role)!=="Viewer",[f,j]=c.useState(""),[b,m]=c.useState(""),[d,y]=c.useState("all");c.useEffect(()=>{const n=setTimeout(()=>m(f),250);return()=>clearTimeout(n)},[f]);const p=e.get("appId"),r=p!==null&&/^[1-9][0-9]*$/.test(p)?Number(p):null,N=We({queryKey:["apps"],queryFn:Mt}),v=N.data??[],g=v.find(n=>n.id===r)??null,C=["builds",r,b,d],E=JSON.stringify(C),x=We({queryKey:C,queryFn:()=>Bt({appId:r,search:b,statusFilter:d}),enabled:r!==null,placeholderData:Pt}),_=x.data??[];c.useEffect(()=>{!N.data||e.get("appId")||N.data.length===0||s({appId:String(N.data[0].id)},{replace:!0})},[N.data,e,s]);const[P,T]=c.useState(null),[B,L]=c.useState(!1);c.useEffect(()=>{x.isRefetchError&&J.error("Couldn't refresh builds — showing the last list",{id:"builds:refresh"})},[x.isRefetchError]);const G=c.useRef(null);c.useEffect(()=>{r===null||x.isPlaceholderData||!x.data||G.current!==r&&(G.current=r,T(r))},[r,x.isPlaceholderData,x.data]);function X(n){s({appId:String(n)})}function H(n,h){return{onMutate:async o=>{const S=C;await a.cancelQueries({queryKey:S});const I=a.getQueryData(S);return a.setQueryData(S,ee=>ee&&n(ee,o)),{previous:I,key:S}},onError:(o,S,I)=>{if(I!=null&&I.previous&&a.setQueryData(I.key,I.previous),o instanceof Tt){a.invalidateQueries({queryKey:$t.me}),J.error(o.message);return}J.error(h)},onSettled:()=>{a.invalidateQueries({queryKey:["builds"]})}}}const q=H((n,h)=>n.map(o=>o.id===h.buildId?{...o,status_label:h.status}:o),"Failed to update status"),Q=Re({mutationFn:({buildId:n,status:h})=>Ft(n,h),...q,onError:(n,h,o)=>{var S;q.onError(n,h,o),((S=R.current)==null?void 0:S.leaving)===h.buildId&&(R.current=null,O(null))}}),Y=(n,h)=>{var S;const o=(S=a.getQueryData(n))==null?void 0:S.find(I=>I.id===h);return o?Fe(o):"the build"},K=Re({mutationFn:n=>Rt(n),...H((n,h)=>n.map(o=>o.id===h?{...o,delete_after:new Date().toISOString()}:o),"Failed to schedule deletion"),onSuccess:(n,h,o)=>{o&&(a.setQueryData(o.key,S=>S==null?void 0:S.map(I=>I.id===h?{...I,delete_after:n}:I)),J.success(`Deletion scheduled for ${Y(o.key,h)} — it will be deleted ${Kt(n)}`))}}),z=Re({mutationFn:n=>Ot(n),...H((n,h)=>n.map(o=>o.id===h?{...o,delete_after:null}:o),"Failed to cancel scheduled deletion"),onSuccess:(n,h,o)=>{J.success(`Scheduled deletion cancelled for ${o?Y(o.key,h):"the build"}`)}}),U=n=>K.mutate(n),W=n=>z.mutate(n),A=Et(_),$=vs(((Qe=_[0])==null?void 0:Qe.app_id)??null,A.map(n=>n.versionName)),R=c.useRef(null),w=c.useRef(null),[D,O]=c.useState(null),Z=c.useRef(null),V=c.useId();function ue(n){const h=A.findIndex(S=>S.builds.some(I=>I.id===n));if(h<0)return[];const o=A[h].builds;return[...Ye(o,o.findIndex(S=>S.id===n)).map(S=>Ne(S.id)),...Ye(A,h).map(S=>we(S.versionName))]}const ie=JSON.stringify(A.flatMap(n=>[we(n.versionName),...$.isOpen(n.versionName)?n.builds.map(h=>Ne(h.id)):[]])),oe=(n,h)=>{const o=_.find(ee=>ee.id===n),I=d!=="all"&&h!==d?ue(n):[];if(o&&I.length>0){const ee=`${Fe(o)} was set to ${h??"no status"}, so the ${d} filter no longer shows it.`,Ke={leaving:n,candidates:I,text:ee,key:E};R.current=Ke,w.current=null,O(Ke)}Q.mutate({buildId:n,status:h})},ae=D!==null&&!_.some(n=>n.id===D.leaving),te=ae?Xe(D.candidates,ie):null,xe=x.data,ce=x.isRefetchError;c.useLayoutEffect(()=>{var I;const n=R.current;if(!n)return;if(n.key!==E||ce){R.current=null,O(null);return}if((xe??[]).some(ee=>ee.id===n.leaving))return;R.current=null;const h=Xe(n.candidates,ie);if(!h||document.activeElement!==null&&document.activeElement!==document.body){J.success(n.text),O(null);return}const o=((I=Z.current)==null?void 0:I.querySelectorAll("[data-status-trigger], [data-release-header]"))??[],S=Array.from(o).find(ee=>Ge(ee)===h);S instanceof HTMLElement?(w.current=h,S.focus()):(J.success(n.text),O(null))},[E,ce,xe,ie]),c.useEffect(()=>{if(!D)return;const n=h=>{const o=Ge(h.target);if(w.current!==null){o!==w.current&&O(null);return}o!==Ne(D.leaving)&&(o===null||!D.candidates.includes(o))&&O(null)};return document.addEventListener("focusin",n),()=>document.removeEventListener("focusin",n)},[D]);const ge=(g==null?void 0:g.name)??"this app",Ie=b!==""||d!=="all",$e=x.isPlaceholderData&&P!==r,ft=x.isFetchedAfterMount&&(x.isPlaceholderData||x.isLoading),be=B||ft,Se=x.isLoadingError||be?"error":x.isLoading||A.length===0&&$e?"loading":A.length===0?"empty":"list",Me=c.useRef(null),mt=ss(Se,Me),Pe=c.useId(),Be=c.useId(),ze=c.useId(),Le=c.useId(),qe=c.useId(),pt=r===null?"":x.isLoadingError?`Couldn't load builds for ${ge}`:x.isLoading||$e||be?`Loading builds for ${ge}`:A.length>0?`Showing ${_.length} build${_.length===1?"":"s"} for ${ge}`:Ie?`No builds match the current filters for ${ge}`:`No builds for ${ge}`;return t.jsxs("div",{className:"flex h-full gap-0",children:[t.jsx(xs,{apps:v,selectedAppId:r,onSelect:X,onAdd:()=>a.invalidateQueries({queryKey:["apps"]}),canWrite:u}),t.jsxs("div",{className:"flex-1 flex flex-col gap-4 p-4 overflow-y-auto min-w-0",children:[t.jsxs("div",{className:"flex items-center justify-between flex-wrap gap-2",children:[t.jsx("h1",{className:"text-xl font-semibold tracking-display-sm",children:g?g.name:"App Center"}),t.jsx(ds,{onSuccess:()=>{a.invalidateQueries({queryKey:["apps"]}),a.invalidateQueries({queryKey:["builds"]})},appId:r,canWrite:u})]}),t.jsx("p",{id:qe,role:"status",className:"sr-only",children:pt}),r===null?t.jsxs("div",{className:"flex-1 flex flex-col items-center justify-center gap-2 text-center",children:[t.jsx(is,{className:"w-8 h-8 text-muted-foreground/40"}),t.jsx("p",{className:"text-sm font-medium",children:"No app selected"}),t.jsx("p",{className:"text-sm text-muted-foreground",children:"Choose an app from the sidebar to view its builds."})]}):t.jsxs(t.Fragment,{children:[t.jsxs("div",{className:"flex flex-wrap gap-2",children:[t.jsx(Qt,{ref:Me,"aria-describedby":`${Le} ${ze}`,"aria-label":"Search versions",placeholder:"Search version…",value:f,onChange:j}),t.jsxs(Ae,{value:d,onValueChange:y,children:[t.jsx(ke,{className:"h-8 w-36","aria-label":"Filter by status",children:t.jsx(Ce,{placeholder:"Status"})}),t.jsxs(_e,{children:[t.jsx(F,{value:"all",children:"All statuses"}),t.jsx(F,{value:"Backlog",children:"Backlog"}),t.jsx(F,{value:"In Progress",children:"In Progress"}),t.jsx(F,{value:"Done",children:"Done"}),t.jsx(F,{value:"Rejected",children:"Rejected"})]})]}),x.isRefetchError&&t.jsx("p",{className:"text-sm text-muted-foreground self-center",children:"Showing the last list — refresh failed."})]}),t.jsx("div",{...mt,className:"contents",children:Se==="error"?t.jsxs("div",{className:"flex-1 flex flex-col items-center justify-center gap-2 text-center",children:[t.jsx(rs,{className:"w-8 h-8 text-muted-foreground/40"}),t.jsx("p",{id:Pe,className:"text-sm font-medium",children:"Couldn't load builds"}),t.jsx("p",{id:Be,className:"text-sm text-muted-foreground",children:"Check that the relay is reachable."}),t.jsx(re,{variant:"outline",size:"sm",className:"mt-1 aria-disabled:opacity-50","aria-describedby":`${Pe} ${Be}`,"aria-disabled":be,onClick:()=>{be||(L(!0),x.refetch().finally(()=>L(!1)))},children:be?"Trying…":"Try again"})]}):Se==="loading"?t.jsx("p",{id:Le,className:"text-sm text-muted-foreground",children:"Loading…"}):Se==="empty"?t.jsxs("div",{className:"flex-1 flex flex-col items-center justify-center gap-2 text-center",children:[t.jsx(os,{className:"w-8 h-8 text-muted-foreground/40"}),t.jsx("p",{id:ze,className:"text-sm font-medium",children:Ie?"No matching builds":"No builds yet"}),t.jsx("p",{className:"text-sm text-muted-foreground",children:Ie?"No build matches the search and status filters.":u?"Upload the first build to get started.":"Builds appear here once a teammate uploads one."})]}):t.jsxs("div",{ref:Z,className:"flex flex-col gap-2","aria-busy":x.isPlaceholderData,children:[ae&&t.jsx("p",{id:V,hidden:!0,children:D.text}),A.map(({versionName:n,builds:h},o)=>t.jsx(bs,{describedBy:[o===0&&!x.isPlaceholderData?qe:null,te===we(n)?V:null].filter(Boolean).join(" ")||void 0,rowNote:te!=null&&te.startsWith("row:")?{buildId:Number(te.slice(4)),id:V}:void 0,versionName:n,builds:h,isOpen:$.isOpen(n),onToggle:()=>$.toggle(n),onNavigate:S=>l(`/app-center/build?id=${S}`),onStatusChange:oe,onScheduleDeletion:U,onCancelDeletion:W,canWrite:u},n))]})})]})]})]})}export{Rs as AppCenter};
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{k as m,j as e}from"./vendor-react-BlPSzrEe.js";import{aO as ce,ay as me,aJ as ue,aI as $,aF as A,aw as k,aL as pe,C as y,c as b,d as N,a as S,L as c,I as u,F as p,S as Y,h as G,av as i,B as f,K as ge,W as fe,O as he,aq as g,ap as F,J as Z,au as h,ag as xe,a7 as je}from"./index-C1Fo8g_c.js";import{B as ve}from"./badge-CWVbeuH3.js";import{A as we,h as ye,c as be,f as Ne,g as Se,d as Pe,e as Ce,b as Ae,a as ke}from"./alert-dialog-C8wDimW-.js";import{P as X}from"./pencil-BJcOp3oN.js";import"./vendor-router-B_lVHrKl.js";const Fe=F({teamName:h().min(1,"Team name is required"),logo:Z(File).nullable().optional()}),De=F({displayName:h().min(1,"Display name is required"),avatar:Z(File).nullable().optional()}),qe=F({currentPassword:h().min(1,"Enter your current password"),newPassword:h().min(8,"Password must be at least 8 characters"),confirmPassword:h()}).refine(x=>x.newPassword===x.confirmPassword,{message:"Passwords do not match",path:["confirmPassword"]});function _e(){var K,U,W,H,M,V,J;const{resolvedTheme:x}=ce(),I=x==="dark"?"/logo-dark.svg":"/logo.svg",{user:t}=me(),D=(t==null?void 0:t.role)==="Admin",q=(t==null?void 0:t.role)==="Admin"||(t==null?void 0:t.role)==="Developer"||(t==null?void 0:t.role)==="QA",j=ue(),P=$({queryKey:g.settings,queryFn:xe,enabled:D}),[ee,se]=m.useState(null),re=ee??((K=P.data)==null?void 0:K.logo_url)??null,T=m.useRef(null),d=A({resolver:k(Fe),defaultValues:{teamName:"",logo:null},values:P.data?{teamName:P.data.team_name,logo:null}:void 0,resetOptions:{keepDirtyValues:!0}});async function ae(s){const r=new FormData;r.append("team_name",s.teamName),s.logo&&r.append("logo",s.logo);try{if(!(await fetch("/api/v1/settings",{method:"PATCH",credentials:"include",body:r})).ok){i.error("Failed to update workspace");return}i.success("Workspace updated"),j.invalidateQueries({queryKey:g.settings})}catch{i.error("Failed to update workspace")}}const[te,oe]=m.useState(null),L=te??(t==null?void 0:t.avatarUrl)??null,Q=m.useRef(null),l=A({resolver:k(De),defaultValues:{displayName:"",avatar:null},values:t?{displayName:t.displayName??"",avatar:null}:void 0,resetOptions:{keepDirtyValues:!0}});async function ie(s){const r=new FormData;r.append("display_name",s.displayName),s.avatar&&r.append("avatar",s.avatar);try{if(!(await fetch("/api/v1/profile",{method:"PATCH",credentials:"include",body:r})).ok){i.error("Failed to update profile");return}i.success("Profile updated"),j.invalidateQueries({queryKey:g.me})}catch{i.error("Failed to update profile")}}const o=A({resolver:k(qe)});async function ne(s){try{const r=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(!r.ok){const a=await r.json();o.setError("root",{message:a.error??"Failed to change password"});return}o.reset(),i.success("Password changed")}catch{o.setError("root",{message:"Network error"})}}const C=$({queryKey:g.apps,queryFn:je,enabled:q}).data??[],[E,z]=m.useState({}),[O,R]=m.useState({}),[_,B]=m.useState({});async function le(s){B(r=>({...r,[s]:!0}));try{if(!(await fetch(`/api/v1/apps/${s}`,{method:"DELETE",credentials:"include"})).ok){i.error("Failed to delete app");return}j.invalidateQueries({queryKey:g.apps}),i.success("App deleted")}catch{i.error("Failed to delete app")}finally{B(r=>({...r,[s]:!1}))}}async function de(s){var r;R(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:E[s]??((r=C.find(n=>n.id===s))==null?void 0:r.name)})})).ok){i.error("Failed to update app");return}i.success("App updated"),z(({[s]:n,...w})=>w),j.invalidateQueries({queryKey:g.apps})}catch{i.error("Failed to update app")}finally{R(a=>({...a,[s]:!1}))}}const v=pe({control:l.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"}),D&&e.jsxs(y,{children:[e.jsx(b,{children:e.jsx(N,{children:"Workspace"})}),e.jsx(S,{children:e.jsxs("form",{onSubmit:d.handleSubmit(ae),className:"flex flex-col gap-4",children:[e.jsxs("div",{className:"grid gap-2",children:[e.jsx(c,{htmlFor:"team-name",children:"Team name"}),e.jsx(u,{id:"team-name",placeholder:"My QA Team","aria-required":"true","aria-invalid":!!d.formState.errors.teamName,"aria-describedby":d.formState.errors.teamName?"teamName-error":void 0,...d.register("teamName")}),e.jsx(p,{id:"teamName-error",message:(U=d.formState.errors.teamName)==null?void 0:U.message})]}),e.jsx(Y,{}),e.jsxs("div",{className:"grid gap-2",children:[e.jsxs(c,{children:["Logo ",e.jsx("span",{className:"text-muted-foreground text-xs",children:"(png · jpg, max 2MB)"})]}),e.jsx(G,{name:"logo",control:d.control,render:({field:s})=>e.jsxs("div",{className:"relative w-16 h-16",children:[e.jsx("img",{src:re??I,alt:"Workspace logo",className:"w-16 h-16 rounded-lg object-contain"}),e.jsx("button",{type:"button","aria-label":"Change logo",onClick:()=>{var r;return(r=T.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(X,{className:"w-3 h-3","aria-hidden":"true"})}),e.jsx("input",{ref:T,type:"file",accept:".png,.jpg,.jpeg",className:"hidden",onChange:r=>{var n;const a=(n=r.target.files)==null?void 0:n[0];if(a&&a.size>2*1024*1024){i.error("Image must be 2MB or less");return}a&&(s.onChange(a),se(URL.createObjectURL(a)))}})]})})]}),e.jsx("div",{className:"flex justify-end",children:e.jsx(f,{type:"submit",size:"sm",disabled:d.formState.isSubmitting,children:d.formState.isSubmitting?"Saving…":"Save changes"})})]})})]}),e.jsxs(y,{children:[e.jsx(b,{children:e.jsx(N,{children:"Profile"})}),e.jsx(S,{children:e.jsxs("form",{onSubmit:l.handleSubmit(ie),className:"flex flex-col gap-4",children:[e.jsxs("div",{className:"grid gap-2",children:[e.jsx(c,{htmlFor:"display-name",children:"Nickname"}),e.jsx(u,{id:"display-name",placeholder:"Your name","aria-required":"true","aria-invalid":!!l.formState.errors.displayName,"aria-describedby":l.formState.errors.displayName?"displayName-error":void 0,...l.register("displayName")}),e.jsx(p,{id:"displayName-error",message:(W=l.formState.errors.displayName)==null?void 0:W.message})]}),e.jsx(Y,{}),e.jsxs("div",{className:"grid gap-2",children:[e.jsxs(c,{children:["Avatar ",e.jsx("span",{className:"text-muted-foreground text-xs",children:"(png · jpg, max 2MB)"})]}),e.jsx(G,{name:"avatar",control:l.control,render:({field:s})=>{var r;return e.jsxs("div",{className:"relative w-14 h-14",children:[L?e.jsx("img",{src:L,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 a=ge(v||(t==null?void 0:t.email)||"");return{backgroundColor:a.bg,color:a.fg}})(),children:((r=v==null?void 0:v[0])==null?void 0:r.toUpperCase())??"?"}),e.jsx("button",{type:"button","aria-label":"Change avatar",onClick:()=>{var a;return(a=Q.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(X,{className:"w-3 h-3","aria-hidden":"true"})}),e.jsx("input",{ref:Q,type:"file",accept:".png,.jpg,.jpeg",className:"hidden",onChange:a=>{var w;const n=(w=a.target.files)==null?void 0:w[0];if(n&&n.size>2*1024*1024){i.error("Image must be 2MB or less");return}n&&(s.onChange(n),oe(URL.createObjectURL(n)))}})]})}})]}),e.jsx("div",{className:"flex justify-end",children:e.jsx(f,{type:"submit",size:"sm",disabled:l.formState.isSubmitting,children:l.formState.isSubmitting?"Saving…":"Save changes"})})]})})]}),e.jsxs(y,{children:[e.jsx(b,{children:e.jsx(N,{children:"Password"})}),e.jsx(S,{children:e.jsxs("form",{onSubmit:o.handleSubmit(ne),className:"flex flex-col gap-4",children:[e.jsxs("div",{className:"grid gap-2",children:[e.jsx(c,{htmlFor:"current-password",children:"Current password"}),e.jsx(u,{id:"current-password",type:"password","aria-required":"true",autoComplete:"current-password","aria-invalid":!!o.formState.errors.currentPassword,"aria-describedby":o.formState.errors.currentPassword?"currentPassword-error":void 0,...o.register("currentPassword")}),e.jsx(p,{id:"currentPassword-error",message:(H=o.formState.errors.currentPassword)==null?void 0:H.message})]}),e.jsxs("div",{className:"grid gap-2",children:[e.jsx(c,{htmlFor:"new-password",children:"New password"}),e.jsx(u,{id:"new-password",type:"password","aria-required":"true",autoComplete:"new-password","aria-invalid":!!o.formState.errors.newPassword,"aria-describedby":o.formState.errors.newPassword?"newPassword-error":void 0,...o.register("newPassword")}),e.jsx(p,{id:"newPassword-error",message:(M=o.formState.errors.newPassword)==null?void 0:M.message})]}),e.jsxs("div",{className:"grid gap-2",children:[e.jsx(c,{htmlFor:"confirm-password",children:"Confirm new password"}),e.jsx(u,{id:"confirm-password",type:"password","aria-required":"true",autoComplete:"new-password","aria-invalid":!!o.formState.errors.confirmPassword,"aria-describedby":o.formState.errors.confirmPassword?"confirmPassword-error":void 0,...o.register("confirmPassword")}),e.jsx(p,{id:"confirmPassword-error",message:(V=o.formState.errors.confirmPassword)==null?void 0:V.message})]}),e.jsx(p,{assertive:!0,id:"default-error",message:(J=o.formState.errors.root)==null?void 0:J.message}),e.jsx("div",{className:"flex justify-end",children:e.jsx(f,{type:"submit",size:"sm",disabled:o.formState.isSubmitting,children:o.formState.isSubmitting?"Saving…":"Change password"})})]})})]}),q&&C.length>0&&e.jsxs(y,{children:[e.jsx(b,{children:e.jsx(N,{children:"Apps"})}),e.jsx(S,{children:e.jsx("div",{className:"flex flex-col divide-y divide-border",children:C.map(s=>e.jsxs("div",{className:"flex flex-col gap-2 py-4 first:pt-0 last:pb-0",children:[e.jsx(c,{htmlFor:`app-${s.id}`,children:"App Name"}),e.jsx(u,{id:`app-${s.id}`,value:E[s.id]??s.name,onChange:r=>z(a=>({...a,[s.id]:r.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(ve,{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(f,{size:"sm",variant:"outline",disabled:O[s.id],onClick:()=>de(s.id),children:O[s.id]?"Saving…":"Save"}),e.jsxs(we,{children:[e.jsx(ye,{asChild:!0,children:e.jsx(f,{size:"sm",variant:"destructive",disabled:_[s.id],children:_[s.id]?"Deleting…":"Delete"})}),e.jsxs(be,{children:[e.jsxs(Ne,{children:[e.jsx(Se,{children:"Delete app?"}),e.jsxs(Pe,{children:["This will permanently delete ",e.jsx("strong",{children:s.name})," and all its builds. This action cannot be undone."]})]}),e.jsxs(Ce,{children:[e.jsx(Ae,{className:"w-24",children:"Cancel"}),e.jsx(ke,{className:fe(he({variant:"destructive"}),"w-24"),onClick:()=>le(s.id),children:"Delete"})]})]})]})]})]})]},s.id))})})]})]})}export{_e as DefaultSettings};
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{k as F,j as e}from"./vendor-react-BlPSzrEe.js";import{aF as U,aw as D,aL as J,aJ as O,aI as Y,C as q,c as I,d as L,b as T,a as _,L as m,I as f,h as z,K as G,F as u,S as H,B as V,aq as R,ap as W,au as v,J as X,aM as Z}from"./index-C1Fo8g_c.js";import{b as $,c as ee,L as ae}from"./vendor-router-B_lVHrKl.js";import{P as se}from"./pencil-BJcOp3oN.js";const re=W({displayName:v().optional(),avatar:X(File).nullable().optional(),password:v().min(8,"Password must be at least 8 characters"),confirm:v()}).refine(o=>o.password===o.confirm,{message:"Passwords do not match",path:["confirm"]});function le(){var b,N,C,k,P,S;const o=$(),[E]=ee(),i=E.get("token")??"",[x,A]=F.useState(null),g=F.useRef(null),{register:p,handleSubmit:B,control:j,setError:l,formState:{errors:s,isSubmitting:y}}=U({resolver:D(re),defaultValues:{displayName:"",avatar:null}}),d=J({control:j,name:"displayName"})??"",K=O(),h=Y({queryKey:R.inviteToken(i),queryFn:()=>Z(i),enabled:i!=="",staleTime:1/0}),w=!i||h.isError?"invalid":h.isPending?"loading":"valid",M=((b=h.data)==null?void 0:b.role)??"";async function Q(t){var n;try{const a=new FormData;a.append("token",i),a.append("password",t.password),(n=t.displayName)!=null&&n.trim()&&a.append("display_name",t.displayName.trim()),t.avatar&&a.append("avatar",t.avatar);const r=await fetch("/api/v1/invitations/accept",{method:"POST",body:a});if(r.status===409){l("root",{type:"already-member",message:"This email already has an account. Sign in instead, or ask your admin for a password reset link."});return}if(!r.ok){const c=await r.json().catch(()=>({}));l("root",{message:c.error??"Failed to accept invitation"});return}K.removeQueries({queryKey:R.inviteToken(i)}),o("/app-center",{replace:!0})}catch{l("root",{message:"Network error. Please try again."})}}return w==="loading"?e.jsx("div",{className:"flex min-h-svh items-center justify-center"}):w==="invalid"?e.jsx("div",{className:"bg-mesh-gradient flex min-h-svh items-center justify-center overflow-hidden p-4",children:e.jsx(q,{level:4,className:"w-full max-w-sm text-center",children:e.jsxs(I,{children:[e.jsx(L,{children:"Invitation expired"}),e.jsx(T,{children:"This invite link is invalid or has expired. Ask your admin for a new one."})]})})}):e.jsx("div",{className:"bg-mesh-gradient flex min-h-svh items-center justify-center overflow-hidden p-4",children:e.jsxs(q,{level:4,className:"w-full max-w-sm",children:[e.jsxs(I,{className:"text-center",children:[e.jsx(L,{children:"Set up your account"}),e.jsxs(T,{children:["You're joining as ",e.jsx("strong",{children:M})]})]}),e.jsx(_,{children:e.jsxs("form",{onSubmit:B(Q),className:"flex flex-col gap-4",children:[e.jsxs("div",{className:"grid gap-2",children:[e.jsxs(m,{htmlFor:"display-name",children:["Nickname ",e.jsx("span",{className:"text-muted-foreground text-xs",children:"(optional)"})]}),e.jsx(f,{id:"display-name",placeholder:"Your name",...p("displayName")})]}),e.jsxs("div",{className:"grid gap-2",children:[e.jsxs(m,{children:["Avatar ",e.jsx("span",{className:"text-muted-foreground text-xs",children:"(optional, png · jpg, max 2MB)"})]}),e.jsx(z,{name:"avatar",control:j,render:({field:t})=>{var n;return e.jsxs("div",{className:"relative w-14 h-14",children:[x?e.jsx("img",{src:x,alt:"Avatar preview",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 a=G(d);return{backgroundColor:a.bg,color:a.fg}})(),children:((n=d==null?void 0:d[0])==null?void 0:n.toUpperCase())??"?"}),e.jsx("button",{type:"button","aria-label":s.avatar?"Change avatar — the file was rejected":"Change avatar","aria-describedby":s.avatar?"avatar-error":void 0,onClick:()=>{var a;return(a=g.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(se,{className:"w-3 h-3","aria-hidden":"true"})}),e.jsx("input",{ref:g,type:"file",accept:"image/png,image/jpeg",className:"hidden",onChange:a=>{var c;const r=(c=a.target.files)==null?void 0:c[0];if(r){if(r.size>2*1024*1024){l("avatar",{message:"Max 2MB for avatar"});return}t.onChange(r),A(URL.createObjectURL(r))}}})]})}}),e.jsx(u,{assertive:!0,id:"avatar-error",message:(N=s.avatar)==null?void 0:N.message})]}),e.jsx(H,{}),e.jsxs("div",{className:"grid gap-2",children:[e.jsx(m,{htmlFor:"password",children:"Password"}),e.jsx(f,{id:"password",type:"password","aria-required":"true",autoComplete:"new-password","aria-invalid":!!s.password,"aria-describedby":s.password?"password-error":void 0,...p("password")}),e.jsx(u,{id:"password-error",message:(C=s.password)==null?void 0:C.message})]}),e.jsxs("div",{className:"grid gap-2",children:[e.jsx(m,{htmlFor:"confirm",children:"Confirm password"}),e.jsx(f,{id:"confirm",type:"password","aria-required":"true",autoComplete:"new-password","aria-invalid":!!s.confirm,"aria-describedby":s.confirm?"confirm-error":void 0,...p("confirm")}),e.jsx(u,{id:"confirm-error",message:(k=s.confirm)==null?void 0:k.message})]}),e.jsx(u,{assertive:!0,id:"invite-error",message:(P=s.root)==null?void 0:P.message}),((S=s.root)==null?void 0:S.type)==="already-member"&&e.jsx(ae,{to:"/login",className:"text-sm text-center underline underline-offset-4",children:"Go to sign in"}),e.jsx(V,{type:"submit",disabled:y,className:"w-full",children:y?"Creating account…":"Create account"})]})})]})})}export{le as Invite};
|
|
Binary file
|
|
Binary file
|