@tapflowio/relay 0.23.0 → 0.25.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.
Files changed (204) hide show
  1. package/dist/RelayServer.d.ts +45 -0
  2. package/dist/RelayServer.d.ts.map +1 -1
  3. package/dist/RelayServer.js +263 -38
  4. package/dist/RelayServer.js.map +1 -1
  5. package/dist/api/apps.d.ts.map +1 -1
  6. package/dist/api/apps.js +6 -16
  7. package/dist/api/apps.js.map +1 -1
  8. package/dist/api/auth.d.ts +1 -1
  9. package/dist/api/auth.d.ts.map +1 -1
  10. package/dist/api/auth.js +13 -14
  11. package/dist/api/auth.js.map +1 -1
  12. package/dist/api/builds.d.ts +1 -1
  13. package/dist/api/builds.d.ts.map +1 -1
  14. package/dist/api/builds.js +15 -7
  15. package/dist/api/builds.js.map +1 -1
  16. package/dist/api/comments.d.ts.map +1 -1
  17. package/dist/api/comments.js +41 -15
  18. package/dist/api/comments.js.map +1 -1
  19. package/dist/api/invitations.d.ts +1 -1
  20. package/dist/api/invitations.d.ts.map +1 -1
  21. package/dist/api/invitations.js +7 -3
  22. package/dist/api/invitations.js.map +1 -1
  23. package/dist/api/passwordReset.js +2 -2
  24. package/dist/api/team.d.ts +2 -2
  25. package/dist/api/team.d.ts.map +1 -1
  26. package/dist/api/team.js +6 -2
  27. package/dist/api/team.js.map +1 -1
  28. package/dist/api/tokens.d.ts +1 -1
  29. package/dist/api/tokens.d.ts.map +1 -1
  30. package/dist/api/tokens.js +26 -7
  31. package/dist/api/tokens.js.map +1 -1
  32. package/dist/api/webhooks.d.ts.map +1 -1
  33. package/dist/api/webhooks.js +11 -5
  34. package/dist/api/webhooks.js.map +1 -1
  35. package/dist/index.d.ts +3 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +2 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/lib/buildFiles.d.ts +23 -0
  40. package/dist/lib/buildFiles.d.ts.map +1 -0
  41. package/dist/lib/buildFiles.js +39 -0
  42. package/dist/lib/buildFiles.js.map +1 -0
  43. package/dist/lib/clientAddress.d.ts +7 -0
  44. package/dist/lib/clientAddress.d.ts.map +1 -1
  45. package/dist/lib/clientAddress.js +15 -0
  46. package/dist/lib/clientAddress.js.map +1 -1
  47. package/dist/lib/config.d.ts +26 -1
  48. package/dist/lib/config.d.ts.map +1 -1
  49. package/dist/lib/config.js +81 -17
  50. package/dist/lib/config.js.map +1 -1
  51. package/dist/lib/connectionAuth.d.ts +57 -2
  52. package/dist/lib/connectionAuth.d.ts.map +1 -1
  53. package/dist/lib/connectionAuth.js +65 -6
  54. package/dist/lib/connectionAuth.js.map +1 -1
  55. package/dist/lib/dataDir.d.ts +48 -1
  56. package/dist/lib/dataDir.d.ts.map +1 -1
  57. package/dist/lib/dataDir.js +108 -12
  58. package/dist/lib/dataDir.js.map +1 -1
  59. package/dist/middleware/auth.d.ts +44 -4
  60. package/dist/middleware/auth.d.ts.map +1 -1
  61. package/dist/middleware/auth.js +102 -30
  62. package/dist/middleware/auth.js.map +1 -1
  63. package/dist/server.js +2 -1
  64. package/dist/server.js.map +1 -1
  65. package/package.json +4 -4
  66. package/public/assets/AppCenter-D_1rFI40.js +26 -0
  67. package/public/assets/AppCenter-D_1rFI40.js.br +0 -0
  68. package/public/assets/AppCenter-D_1rFI40.js.gz +0 -0
  69. package/public/assets/Default-DF_X2_3I.js +1 -0
  70. package/public/assets/Default-DF_X2_3I.js.br +0 -0
  71. package/public/assets/Default-DF_X2_3I.js.gz +0 -0
  72. package/public/assets/Invite-DETTWdYp.js +1 -0
  73. package/public/assets/Invite-DETTWdYp.js.br +0 -0
  74. package/public/assets/Invite-DETTWdYp.js.gz +0 -0
  75. package/public/assets/MacResources--1Enisxs.js +1 -0
  76. package/public/assets/MacResources--1Enisxs.js.br +0 -0
  77. package/public/assets/MacResources--1Enisxs.js.gz +0 -0
  78. package/public/assets/NotFound-wHOnT2Fl.js +1 -0
  79. package/public/assets/NotFound-wHOnT2Fl.js.br +0 -0
  80. package/public/assets/NotFound-wHOnT2Fl.js.gz +0 -0
  81. package/public/assets/QASession-BjoD9Toj.js +150 -0
  82. package/public/assets/QASession-BjoD9Toj.js.br +0 -0
  83. package/public/assets/QASession-BjoD9Toj.js.gz +0 -0
  84. package/public/assets/ResetPassword-B9miyXTP.js +1 -0
  85. package/public/assets/ResetPassword-B9miyXTP.js.br +0 -0
  86. package/public/assets/ResetPassword-B9miyXTP.js.gz +0 -0
  87. package/public/assets/Setup-CywZCqHU.js +1 -0
  88. package/public/assets/Setup-CywZCqHU.js.br +0 -0
  89. package/public/assets/Setup-CywZCqHU.js.gz +0 -0
  90. package/public/assets/Team-CxdX_9mY.js +6 -0
  91. package/public/assets/Team-CxdX_9mY.js.br +0 -0
  92. package/public/assets/Team-CxdX_9mY.js.gz +0 -0
  93. package/public/assets/Tokens-BQnnawvG.js +1 -0
  94. package/public/assets/Tokens-BQnnawvG.js.br +0 -0
  95. package/public/assets/Tokens-BQnnawvG.js.gz +0 -0
  96. package/public/assets/alert-dialog-CuPdBkxF.js +1 -0
  97. package/public/assets/alert-dialog-CuPdBkxF.js.br +0 -0
  98. package/public/assets/alert-dialog-CuPdBkxF.js.gz +0 -0
  99. package/public/assets/badge-CSVsKOK_.js +1 -0
  100. package/public/assets/badge-CSVsKOK_.js.br +0 -0
  101. package/public/assets/badge-CSVsKOK_.js.gz +0 -0
  102. package/public/assets/build-format-l9BrlbZl.js +11 -0
  103. package/public/assets/build-format-l9BrlbZl.js.br +0 -0
  104. package/public/assets/build-format-l9BrlbZl.js.gz +0 -0
  105. package/public/assets/dialog-B6jt7-2f.js +11 -0
  106. package/public/assets/dialog-B6jt7-2f.js.br +0 -0
  107. package/public/assets/dialog-B6jt7-2f.js.gz +0 -0
  108. package/public/assets/index-Cm17I31a.js +218 -0
  109. package/public/assets/index-Cm17I31a.js.br +0 -0
  110. package/public/assets/index-Cm17I31a.js.gz +0 -0
  111. package/public/assets/index-D_P99bPX.css +1 -0
  112. package/public/assets/index-D_P99bPX.css.br +0 -0
  113. package/public/assets/index-D_P99bPX.css.gz +0 -0
  114. package/public/assets/list-view-Bp6pvsde.js +1 -0
  115. package/public/assets/list-view-Bp6pvsde.js.br +0 -0
  116. package/public/assets/list-view-Bp6pvsde.js.gz +0 -0
  117. package/public/assets/{pencil-CPaoEKFm.js → pencil-g-WiK1F2.js} +2 -2
  118. package/public/assets/pencil-g-WiK1F2.js.br +0 -0
  119. package/public/assets/pencil-g-WiK1F2.js.gz +0 -0
  120. package/public/assets/resource-health-BbeyA8an.js +1 -0
  121. package/public/assets/resource-health-BbeyA8an.js.br +0 -0
  122. package/public/assets/resource-health-BbeyA8an.js.gz +0 -0
  123. package/public/assets/{trash-CD9Y9G-i.js → trash-DHPg15P4.js} +3 -3
  124. package/public/assets/trash-DHPg15P4.js.br +0 -0
  125. package/public/assets/trash-DHPg15P4.js.gz +0 -0
  126. package/public/assets/useFocusAfterSwap-BY3N9f9v.js +1 -0
  127. package/public/assets/useFocusAfterSwap-BY3N9f9v.js.br +0 -0
  128. package/public/assets/useFocusAfterSwap-BY3N9f9v.js.gz +0 -0
  129. package/public/assets/{vendor-charts-YPhPkjLw.js → vendor-charts-BykzUV2D.js} +1 -1
  130. package/public/assets/vendor-charts-BykzUV2D.js.br +0 -0
  131. package/public/assets/vendor-charts-BykzUV2D.js.gz +0 -0
  132. package/public/assets/{vendor-react-C5H_1fOn.js → vendor-react-BlPSzrEe.js} +18 -10
  133. package/public/assets/vendor-react-BlPSzrEe.js.br +0 -0
  134. package/public/assets/vendor-react-BlPSzrEe.js.gz +0 -0
  135. package/public/assets/{vendor-router-CnmrRwJF.js → vendor-router-B_lVHrKl.js} +3 -3
  136. package/public/assets/vendor-router-B_lVHrKl.js.br +0 -0
  137. package/public/assets/vendor-router-B_lVHrKl.js.gz +0 -0
  138. package/public/index.html +4 -4
  139. package/public/index.html.br +0 -0
  140. package/public/index.html.gz +0 -0
  141. package/public/assets/AppCenter-OTDuqwSf.js +0 -21
  142. package/public/assets/AppCenter-OTDuqwSf.js.br +0 -0
  143. package/public/assets/AppCenter-OTDuqwSf.js.gz +0 -0
  144. package/public/assets/Default-D9GLpWlN.js +0 -1
  145. package/public/assets/Default-D9GLpWlN.js.br +0 -0
  146. package/public/assets/Default-D9GLpWlN.js.gz +0 -0
  147. package/public/assets/Invite-Bhiam8En.js +0 -1
  148. package/public/assets/Invite-Bhiam8En.js.br +0 -0
  149. package/public/assets/Invite-Bhiam8En.js.gz +0 -0
  150. package/public/assets/MacResources-DObzVJxQ.js +0 -3
  151. package/public/assets/MacResources-DObzVJxQ.js.br +0 -0
  152. package/public/assets/MacResources-DObzVJxQ.js.gz +0 -0
  153. package/public/assets/NotFound-Dz_k_Mzf.js +0 -1
  154. package/public/assets/NotFound-Dz_k_Mzf.js.br +0 -0
  155. package/public/assets/NotFound-Dz_k_Mzf.js.gz +0 -0
  156. package/public/assets/QASession-2crubOLY.js +0 -150
  157. package/public/assets/QASession-2crubOLY.js.br +0 -0
  158. package/public/assets/QASession-2crubOLY.js.gz +0 -0
  159. package/public/assets/ResetPassword-EUm12ojo.js +0 -1
  160. package/public/assets/ResetPassword-EUm12ojo.js.br +0 -0
  161. package/public/assets/ResetPassword-EUm12ojo.js.gz +0 -0
  162. package/public/assets/Setup-Fc_0RW0a.js +0 -1
  163. package/public/assets/Setup-Fc_0RW0a.js.br +0 -0
  164. package/public/assets/Setup-Fc_0RW0a.js.gz +0 -0
  165. package/public/assets/Team-Ac5aOGgx.js +0 -6
  166. package/public/assets/Team-Ac5aOGgx.js.br +0 -0
  167. package/public/assets/Team-Ac5aOGgx.js.gz +0 -0
  168. package/public/assets/Tokens-pEnCy-7l.js +0 -1
  169. package/public/assets/Tokens-pEnCy-7l.js.br +0 -0
  170. package/public/assets/Tokens-pEnCy-7l.js.gz +0 -0
  171. package/public/assets/alert-dialog-DhOTocMh.js +0 -1
  172. package/public/assets/alert-dialog-DhOTocMh.js.br +0 -0
  173. package/public/assets/alert-dialog-DhOTocMh.js.gz +0 -0
  174. package/public/assets/badge-B4gbViMs.js +0 -1
  175. package/public/assets/badge-B4gbViMs.js.br +0 -0
  176. package/public/assets/badge-B4gbViMs.js.gz +0 -0
  177. package/public/assets/build-format-DKGajT96.js +0 -11
  178. package/public/assets/build-format-DKGajT96.js.br +0 -0
  179. package/public/assets/build-format-DKGajT96.js.gz +0 -0
  180. package/public/assets/dialog-DEL1xxKJ.js +0 -11
  181. package/public/assets/dialog-DEL1xxKJ.js.br +0 -0
  182. package/public/assets/dialog-DEL1xxKJ.js.gz +0 -0
  183. package/public/assets/index-Bxs7CdEi.css +0 -1
  184. package/public/assets/index-Bxs7CdEi.css.br +0 -0
  185. package/public/assets/index-Bxs7CdEi.css.gz +0 -0
  186. package/public/assets/index-COIlCLCW.js +0 -218
  187. package/public/assets/index-COIlCLCW.js.br +0 -0
  188. package/public/assets/index-COIlCLCW.js.gz +0 -0
  189. package/public/assets/pencil-CPaoEKFm.js.br +0 -0
  190. package/public/assets/pencil-CPaoEKFm.js.gz +0 -0
  191. package/public/assets/resource-health-Ca4-5Rde.js +0 -1
  192. package/public/assets/resource-health-Ca4-5Rde.js.br +0 -0
  193. package/public/assets/resource-health-Ca4-5Rde.js.gz +0 -0
  194. package/public/assets/table-CSeGVnii.js +0 -1
  195. package/public/assets/table-CSeGVnii.js.br +0 -0
  196. package/public/assets/table-CSeGVnii.js.gz +0 -0
  197. package/public/assets/trash-CD9Y9G-i.js.br +0 -0
  198. package/public/assets/trash-CD9Y9G-i.js.gz +0 -0
  199. package/public/assets/vendor-charts-YPhPkjLw.js.br +0 -0
  200. package/public/assets/vendor-charts-YPhPkjLw.js.gz +0 -0
  201. package/public/assets/vendor-react-C5H_1fOn.js.br +0 -0
  202. package/public/assets/vendor-react-C5H_1fOn.js.gz +0 -0
  203. package/public/assets/vendor-router-CnmrRwJF.js.br +0 -0
  204. package/public/assets/vendor-router-CnmrRwJF.js.gz +0 -0
@@ -1,18 +1,77 @@
1
1
  export const AGENT_SCOPE = 'agent';
2
+ export const VIEW_SCOPE = 'view';
3
+ const ADMIN_ROLE = 'Admin';
2
4
  // ws close reason은 123바이트 한도 (RFC 6455 §5.5.1).
3
5
  export const WS_REJECT_REASON = "Unauthorized: agents need a PAT with 'agent' scope (--token / TAPFLOW_AGENT_TOKEN); browsers must sign in";
6
+ /** A PAT with neither `view` nor `agent`: it may upload builds, not open device sessions. */
7
+ export const WS_SCOPE_REASON = "Forbidden: this token lacks the 'view' scope needed for device sessions; create an API-type token";
8
+ /** An `agent` token is issued by an Admin and is only as good as that Admin role, checked at every use. */
9
+ export const WS_AGENT_OWNER_REASON = "Unauthorized: this agent token's owner is no longer an Admin; an Admin must issue a new agent token";
10
+ /** An open socket whose token was revoked, expired, or went with its owner. */
11
+ export const WS_TOKEN_GONE_REASON = 'Unauthorized: this token was revoked or has expired, or its owner was removed';
4
12
  // 새 WebSocket 연결을 첫 메시지 도착 전에 분류한다. (주소 × 자격) 매트릭스를
5
13
  // 단위 테스트할 수 있도록 순수 함수로 분리 (#271).
6
- export function classifyConnection({ isLocal, hasCookieAuth, patScopes }) {
14
+ //
15
+ // **A PAT confers the browser role only with `view`.** Any valid PAT used to, so a `builds:write`
16
+ // token meant for CI uploads could drive devices, and an agent-only token whose first frame was not a
17
+ // handshake fell through to `browser` as well. The HTTP side already asked for `view` (screenshots,
18
+ // UI trees); the socket now asks the same question.
19
+ export function classifyConnection({ isLocal, hasCookieAuth, patScopes, patOwnerRole }) {
20
+ if (isLocal) {
21
+ return hasCookieAuth
22
+ ? { action: 'accept', role: 'browser' }
23
+ : { action: 'accept', role: 'first-message', mayBrowse: true, mayRegisterAgent: true };
24
+ }
25
+ const canView = patScopes?.includes(VIEW_SCOPE) ?? false;
7
26
  // 명시적 에이전트 자격은 떠돌이 쿠키보다 우선한다 — 원격 에이전트의 인증 경로.
8
- if (!isLocal && patScopes?.includes(AGENT_SCOPE))
9
- return { action: 'accept', role: 'first-message' };
27
+ if (patScopes?.includes(AGENT_SCOPE)) {
28
+ if (patOwnerRole === ADMIN_ROLE) {
29
+ return { action: 'accept', role: 'first-message', mayBrowse: canView, mayRegisterAgent: true };
30
+ }
31
+ // A demoted owner's `view,agent` token keeps what `view` alone would give: a Viewer's `view` token
32
+ // may run QA sessions, so taking browsing away here would be stricter than the role model.
33
+ if (canView)
34
+ return { action: 'accept', role: 'browser' };
35
+ return { action: 'reject', reason: WS_AGENT_OWNER_REASON };
36
+ }
10
37
  if (hasCookieAuth)
11
38
  return { action: 'accept', role: 'browser' };
12
- if (isLocal)
13
- return { action: 'accept', role: 'first-message' };
14
- if (patScopes)
39
+ if (canView)
15
40
  return { action: 'accept', role: 'browser' };
41
+ if (patScopes)
42
+ return { action: 'reject', reason: WS_SCOPE_REASON };
16
43
  return { action: 'reject', reason: WS_REJECT_REASON };
17
44
  }
45
+ /**
46
+ * Would this socket still be accepted, given what the database says now? Returns the close reason, or
47
+ * null when it would.
48
+ *
49
+ * **One predicate instead of one rule per event.** Removal, demotion, revocation, expiry and an
50
+ * invitation accept that changes a role all reach an open socket through the same question, asked on
51
+ * the heartbeat and again right after any of those writes. A list of "which sockets does this event
52
+ * affect" is the design this replaced, and it had already missed expiry and the invitation path.
53
+ *
54
+ * `role` is what the socket has become: `agent` and `stream` rest on the `agent` scope and its owner's
55
+ * Admin role; `browser` rests on `view`. A socket that has not sent its first frame yet (`undefined`)
56
+ * is re-classified as a fresh handshake would be.
57
+ */
58
+ export function revalidatePrincipal(principal, role, current, nowSeconds) {
59
+ if (principal.via === 'cookie') {
60
+ if (!current.userExists)
61
+ return WS_REJECT_REASON;
62
+ if (principal.jwtExp !== undefined && principal.jwtExp <= nowSeconds)
63
+ return WS_REJECT_REASON;
64
+ return null;
65
+ }
66
+ const pat = current.pat;
67
+ if (!pat)
68
+ return WS_TOKEN_GONE_REASON;
69
+ if (role === 'agent' || role === 'stream') {
70
+ return pat.scopes.includes(AGENT_SCOPE) && pat.ownerRole === ADMIN_ROLE ? null : WS_AGENT_OWNER_REASON;
71
+ }
72
+ if (role === 'browser')
73
+ return pat.scopes.includes(VIEW_SCOPE) ? null : WS_SCOPE_REASON;
74
+ const again = classifyConnection({ isLocal: false, hasCookieAuth: false, patScopes: pat.scopes, patOwnerRole: pat.ownerRole });
75
+ return again.action === 'reject' ? again.reason : null;
76
+ }
18
77
  //# sourceMappingURL=connectionAuth.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"connectionAuth.js","sourceRoot":"","sources":["../../src/lib/connectionAuth.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG,OAAO,CAAA;AAElC,gDAAgD;AAChD,MAAM,CAAC,MAAM,gBAAgB,GAC3B,2GAA2G,CAAA;AAe7G,oDAAoD;AACpD,kCAAkC;AAClC,MAAM,UAAU,kBAAkB,CAChC,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAuB;IAE1D,+CAA+C;IAC/C,IAAI,CAAC,OAAO,IAAI,SAAS,EAAE,QAAQ,CAAC,WAAW,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,EAAE,CAAA;IACpG,IAAI,aAAa;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IAC/D,IAAI,OAAO;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,eAAe,EAAE,CAAA;IAC/D,IAAI,SAAS;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IAC3D,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAA;AACvD,CAAC"}
1
+ {"version":3,"file":"connectionAuth.js","sourceRoot":"","sources":["../../src/lib/connectionAuth.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,55 @@
1
1
  export declare const LEGACY_DATA_DIR = ".tapflow-data";
2
2
  export declare const UNIFIED_DATA_DIR: string;
3
+ export declare const OWN_DATA_DIR = "data";
4
+ export declare const CONFIG_FILE = "tapflow.config.json";
5
+ export declare const DEFAULT_INSTALL_DIR_NAME = ".tapflow";
3
6
  export interface DefaultDataDir {
4
7
  dataDir: string;
5
8
  usingLegacy: boolean;
9
+ existing: boolean;
6
10
  }
7
- export declare function resolveDefaultDataDir(cwd: string): DefaultDataDir;
11
+ /**
12
+ * Whether `dir` holds an install's data. An empty dir counts — the Docker image and the systemd
13
+ * docs create one before the relay first starts. A dir whose only entry is `jwt-secret` does not:
14
+ * every CLI command up to this change wrote one wherever it ran, because the relay config created
15
+ * it at import, and the relay itself always creates `tapflow.db` on boot. So a dir holding the
16
+ * secret alone is one no relay ever ran in, and treating it as an install made an app repo that
17
+ * had once run `tapflow flow run` look like one.
18
+ */
19
+ export declare function holdsInstallData(dir: string): boolean;
20
+ export declare function resolveDefaultDataDir(installDir: string, fallback?: string): DefaultDataDir;
21
+ export type InstallReason = 'TAPFLOW_HOME' | 'config in the current directory' | 'data in the current directory' | 'default';
22
+ export interface InstallDir {
23
+ dir: string;
24
+ reason: InstallReason;
25
+ configPath: string;
26
+ defaultDataLayout: string;
27
+ missing: boolean;
28
+ shadowed: string[];
29
+ }
30
+ /**
31
+ * **Which directory is this machine's tapflow install.** Every command asks this and none takes a
32
+ * path, so `init` writes where `start` reads:
33
+ *
34
+ * 1. `TAPFLOW_HOME`, when set and non-empty (relative values resolve against the cwd).
35
+ * 2. The current directory, when it already is an install — it holds `tapflow.config.json`, or a
36
+ * `.tapflow/data` / `.tapflow-data` with install data in it. This is how installs from before the
37
+ * home existed keep running where they are, and how `pnpm dev` keeps its own.
38
+ * 3. `~/.tapflow`.
39
+ *
40
+ * A permanent rule, like npm's project `.npmrc` before the user one — not a migration path.
41
+ *
42
+ * Two cases around the home directory, both found in design review:
43
+ * - At `~`, `~/.tapflow/data` is the default install's own data, so it is not a signal there. And
44
+ * once `~/.tapflow/tapflow.config.json` exists, `~` is never an install: an older install at `~`
45
+ * would otherwise serve the new install's data under the old config.
46
+ * - An install at `~/.tapflow` with no config of its own reads `~/tapflow.config.json`, which is
47
+ * where `tapflow init` run from a new terminal used to write it. Without that, starting it from
48
+ * anywhere but `~` served the same database and secret with its TLS, tunnel and SMTP gone.
49
+ */
50
+ export declare function resolveInstallDir({ env, cwd, home }?: {
51
+ env?: NodeJS.ProcessEnv;
52
+ cwd?: string;
53
+ home?: string;
54
+ }): InstallDir;
8
55
  //# sourceMappingURL=dataDir.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dataDir.d.ts","sourceRoot":"","sources":["../../src/lib/dataDir.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,eAAe,kBAAkB,CAAA;AAC9C,eAAO,MAAM,gBAAgB,QAAgC,CAAA;AAE7D,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAA;IAEf,WAAW,EAAE,OAAO,CAAA;CACrB;AAMD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,cAAc,CAMjE"}
1
+ {"version":3,"file":"dataDir.d.ts","sourceRoot":"","sources":["../../src/lib/dataDir.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,eAAe,kBAAkB,CAAA;AAC9C,eAAO,MAAM,gBAAgB,QAAgC,CAAA;AAG7D,eAAO,MAAM,YAAY,SAAS,CAAA;AAClC,eAAO,MAAM,WAAW,wBAAwB,CAAA;AAChD,eAAO,MAAM,wBAAwB,aAAa,CAAA;AAElD,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAA;IAEf,WAAW,EAAE,OAAO,CAAA;IAEpB,QAAQ,EAAE,OAAO,CAAA;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASrD;AAOD,wBAAgB,qBAAqB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAyB,GAAG,cAAc,CAM7G;AAED,MAAM,MAAM,aAAa,GAAG,cAAc,GAAG,iCAAiC,GAAG,+BAA+B,GAAG,SAAS,CAAA;AAE5H,MAAM,WAAW,UAAU;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,aAAa,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;IAElB,iBAAiB,EAAE,MAAM,CAAA;IAGzB,OAAO,EAAE,OAAO,CAAA;IAEhB,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAUD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,GAAiB,EAAE,GAAmB,EAAE,IAAmB,EAAE,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7H,UAAU,CAoCZ"}
@@ -1,18 +1,114 @@
1
1
  import fs from 'fs';
2
+ import os from 'os';
2
3
  import path from 'path';
3
4
  export const LEGACY_DATA_DIR = '.tapflow-data';
4
5
  export const UNIFIED_DATA_DIR = path.join('.tapflow', 'data');
5
- // Read-only resolution of the default data dir. The relay never MOVES anything — that is the job of
6
- // `tapflow migrate data-dir`. It prefers the unified .tapflow/data/, but if that does not exist yet
7
- // and a legacy .tapflow-data/ does, it keeps reading the legacy dir so an un-migrated default install
8
- // is not silently reset to empty. The caller warns when usingLegacy is true.
9
- export function resolveDefaultDataDir(cwd) {
10
- const target = path.join(cwd, UNIFIED_DATA_DIR);
11
- if (fs.existsSync(target))
12
- return { dataDir: target, usingLegacy: false };
13
- const legacy = path.join(cwd, LEGACY_DATA_DIR);
14
- if (fs.existsSync(legacy))
15
- return { dataDir: legacy, usingLegacy: true };
16
- return { dataDir: target, usingLegacy: false };
6
+ // The layout of an install dir tapflow owns: config and data side by side, nothing wrapped.
7
+ // `.tapflow/data` wraps because it dates from when tapflow lived inside the user's own folder.
8
+ export const OWN_DATA_DIR = 'data';
9
+ export const CONFIG_FILE = 'tapflow.config.json';
10
+ export const DEFAULT_INSTALL_DIR_NAME = '.tapflow';
11
+ /**
12
+ * Whether `dir` holds an install's data. An empty dir counts — the Docker image and the systemd
13
+ * docs create one before the relay first starts. A dir whose only entry is `jwt-secret` does not:
14
+ * every CLI command up to this change wrote one wherever it ran, because the relay config created
15
+ * it at import, and the relay itself always creates `tapflow.db` on boot. So a dir holding the
16
+ * secret alone is one no relay ever ran in, and treating it as an install made an app repo that
17
+ * had once run `tapflow flow run` look like one.
18
+ */
19
+ export function holdsInstallData(dir) {
20
+ let entries;
21
+ try {
22
+ entries = fs.readdirSync(dir);
23
+ }
24
+ catch {
25
+ return false;
26
+ }
27
+ const real = entries.filter((e) => e !== '.DS_Store');
28
+ return !(real.length === 1 && real[0] === 'jwt-secret');
29
+ }
30
+ // Read-only resolution of an install's data dir when nothing names one. The relay never MOVES
31
+ // anything — that is the job of `tapflow migrate data-dir`. Existing layouts come first, so an
32
+ // install that has data keeps it: `.tapflow/data`, then the legacy `.tapflow-data`, and only then
33
+ // `fallback`. The existing ones win over a new-layout `data/` because an install living inside an
34
+ // app repo can have an unrelated `data/` of the app's own. The caller warns when usingLegacy is true.
35
+ export function resolveDefaultDataDir(installDir, fallback = UNIFIED_DATA_DIR) {
36
+ const unified = path.join(installDir, UNIFIED_DATA_DIR);
37
+ if (holdsInstallData(unified))
38
+ return { dataDir: unified, usingLegacy: false, existing: true };
39
+ const legacy = path.join(installDir, LEGACY_DATA_DIR);
40
+ if (holdsInstallData(legacy))
41
+ return { dataDir: legacy, usingLegacy: true, existing: true };
42
+ return { dataDir: path.join(installDir, fallback), usingLegacy: false, existing: false };
43
+ }
44
+ function realpathOr(p) {
45
+ try {
46
+ return fs.realpathSync(p);
47
+ }
48
+ catch {
49
+ return path.resolve(p);
50
+ }
51
+ }
52
+ /**
53
+ * **Which directory is this machine's tapflow install.** Every command asks this and none takes a
54
+ * path, so `init` writes where `start` reads:
55
+ *
56
+ * 1. `TAPFLOW_HOME`, when set and non-empty (relative values resolve against the cwd).
57
+ * 2. The current directory, when it already is an install — it holds `tapflow.config.json`, or a
58
+ * `.tapflow/data` / `.tapflow-data` with install data in it. This is how installs from before the
59
+ * home existed keep running where they are, and how `pnpm dev` keeps its own.
60
+ * 3. `~/.tapflow`.
61
+ *
62
+ * A permanent rule, like npm's project `.npmrc` before the user one — not a migration path.
63
+ *
64
+ * Two cases around the home directory, both found in design review:
65
+ * - At `~`, `~/.tapflow/data` is the default install's own data, so it is not a signal there. And
66
+ * once `~/.tapflow/tapflow.config.json` exists, `~` is never an install: an older install at `~`
67
+ * would otherwise serve the new install's data under the old config.
68
+ * - An install at `~/.tapflow` with no config of its own reads `~/tapflow.config.json`, which is
69
+ * where `tapflow init` run from a new terminal used to write it. Without that, starting it from
70
+ * anywhere but `~` served the same database and secret with its TLS, tunnel and SMTP gone.
71
+ */
72
+ export function resolveInstallDir({ env = process.env, cwd = process.cwd(), home = os.homedir() } = {}) {
73
+ const defaultDir = path.join(home, DEFAULT_INSTALL_DIR_NAME);
74
+ let reason = 'default';
75
+ let shadowed = [];
76
+ if (env.TAPFLOW_HOME) {
77
+ const dir = path.resolve(cwd, env.TAPFLOW_HOME);
78
+ if (realpathOr(dir) !== realpathOr(defaultDir)) {
79
+ return {
80
+ dir, reason: 'TAPFLOW_HOME', configPath: path.join(dir, CONFIG_FILE),
81
+ defaultDataLayout: OWN_DATA_DIR, missing: !fs.existsSync(dir), shadowed,
82
+ };
83
+ }
84
+ reason = 'TAPFLOW_HOME';
85
+ }
86
+ else {
87
+ const atHome = realpathOr(cwd) === realpathOr(home);
88
+ const homeInstallConfigured = atHome && fs.existsSync(path.join(defaultDir, CONFIG_FILE));
89
+ if (homeInstallConfigured) {
90
+ shadowed = [path.join(home, CONFIG_FILE)].filter((p) => fs.existsSync(p));
91
+ if (holdsInstallData(path.join(home, LEGACY_DATA_DIR)))
92
+ shadowed.push(path.join(home, LEGACY_DATA_DIR));
93
+ }
94
+ else {
95
+ const inCwd = (why) => ({
96
+ dir: cwd, reason: why, configPath: path.join(cwd, CONFIG_FILE),
97
+ // An install in its own folder from before the home existed: keep its layout.
98
+ defaultDataLayout: UNIFIED_DATA_DIR, missing: false, shadowed: [],
99
+ });
100
+ if (fs.existsSync(path.join(cwd, CONFIG_FILE)))
101
+ return inCwd('config in the current directory');
102
+ if (!atHome && holdsInstallData(path.join(cwd, UNIFIED_DATA_DIR)))
103
+ return inCwd('data in the current directory');
104
+ if (holdsInstallData(path.join(cwd, LEGACY_DATA_DIR)))
105
+ return inCwd('data in the current directory');
106
+ }
107
+ }
108
+ let configPath = path.join(defaultDir, CONFIG_FILE);
109
+ const homeConfig = path.join(home, CONFIG_FILE);
110
+ if (!fs.existsSync(configPath) && fs.existsSync(homeConfig))
111
+ configPath = homeConfig;
112
+ return { dir: defaultDir, reason, configPath, defaultDataLayout: OWN_DATA_DIR, missing: false, shadowed };
17
113
  }
18
114
  //# sourceMappingURL=dataDir.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dataDir.js","sourceRoot":"","sources":["../../src/lib/dataDir.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,IAAI,MAAM,MAAM,CAAA;AAEvB,MAAM,CAAC,MAAM,eAAe,GAAG,eAAe,CAAA;AAC9C,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;AAQ7D,oGAAoG;AACpG,oGAAoG;AACpG,sGAAsG;AACtG,6EAA6E;AAC7E,MAAM,UAAU,qBAAqB,CAAC,GAAW;IAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAA;IAC/C,IAAI,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;IACzE,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,eAAe,CAAC,CAAA;IAC9C,IAAI,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,CAAA;IACxE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAA;AAChD,CAAC"}
1
+ {"version":3,"file":"dataDir.js","sourceRoot":"","sources":["../../src/lib/dataDir.ts"],"names":[],"mappings":"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"}
@@ -7,13 +7,53 @@ export interface AuthContext {
7
7
  export declare function signJwt(payload: AuthContext): string;
8
8
  export declare function verifyJwtOrThrow(token: string): AuthContext;
9
9
  export declare function verifyJwt(token: string): AuthContext | null;
10
- export declare function getAuth(req: http.IncomingMessage): AuthContext | null;
10
+ /** A signed-in session: who the cookie names, as the users table describes them now. */
11
+ export interface SessionAuth extends AuthContext {
12
+ /** The JWT's `exp` (seconds since the epoch), so an open socket can be closed when it passes. */
13
+ exp?: number;
14
+ }
15
+ /**
16
+ * The cookie's user, **checked against the users table on every call**.
17
+ *
18
+ * The JWT lives seven days and removing a member deletes their row, so a signature check alone kept a
19
+ * removed member signed in for up to a week on every cookie path: this function, `requireViewAuth`,
20
+ * `requireBuildAuth`, recordings, and the WebSocket handshake. This is the one place all of them pass,
21
+ * so the row check lives here. User ids are `AUTOINCREMENT`, so a removed id never comes back and "the
22
+ * row exists" is a sound test. `email` and `role` come from the row, never from the JWT.
23
+ *
24
+ * Only the JWT verification failure becomes `null`. A database error propagates: turning a transient
25
+ * `SQLITE_BUSY` into "not signed in" would sign everyone out and hide the fault.
26
+ */
27
+ export declare function getAuth(req: http.IncomingMessage): SessionAuth | null;
28
+ export declare function findUser(userId: number): {
29
+ email: string;
30
+ role: string;
31
+ } | null;
11
32
  export declare function requireAuth(req: http.IncomingMessage, res: http.ServerResponse): AuthContext | null;
33
+ export declare function currentRole(res: http.ServerResponse, userId: number): string | null;
12
34
  export declare function requireRole(req: http.IncomingMessage, res: http.ServerResponse, roles: string[]): AuthContext | null;
13
- export declare function verifyPat(req: http.IncomingMessage): {
35
+ export declare function assertCanWrite(res: http.ServerResponse, userId: number): boolean;
36
+ /** A live PAT and its owner, as the database describes them now. */
37
+ export interface PatAuth {
38
+ patId: number;
14
39
  userId: number;
15
- scope: string;
16
- } | null;
40
+ scopes: string[];
41
+ ownerEmail: string;
42
+ ownerRole: string;
43
+ }
44
+ /**
45
+ * The PAT on this request, or null when there is none, it is unknown, or it has expired.
46
+ *
47
+ * **Reads only.** `last_used_at` is written by `touchPat` once the credential check has accepted the
48
+ * token: a token refused there (a scope it lacks, a WebSocket refused for its scope or its owner's
49
+ * role) is not used, and a "last used" that moved on those told an Admin a leaked token was in use
50
+ * when nothing got through. A route that accepts the token and then refuses the request on the
51
+ * owner's role (`assertCanWrite` for a Viewer) still counts as a use — the token did reach the route.
52
+ */
53
+ export declare function verifyPat(req: http.IncomingMessage): PatAuth | null;
54
+ /** The same lookup by id, for re-checking a token an open socket was accepted with. */
55
+ export declare function findPat(patId: number): PatAuth | null;
56
+ export declare function touchPat(patId: number): void;
17
57
  export declare function hashPat(token: string): string;
18
58
  export declare function requireViewAuth(req: http.IncomingMessage, res: http.ServerResponse): AuthContext | null;
19
59
  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,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAM3D;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAM3D;AAED,wBAAgB,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,WAAW,GAAG,IAAI,CAKrE;AAED,wBAAgB,WAAW,CACzB,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,GACvB,WAAW,GAAG,IAAI,CAQpB;AAED,wBAAgB,WAAW,CACzB,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,KAAK,EAAE,MAAM,EAAE,GACd,WAAW,GAAG,IAAI,CASpB;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAe7F;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,CAuBpB;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,GACvB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAa3B"}
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,CAEpD;AAED,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;CACb;AAED;;;;;;;;;;;GAWG;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,CAAA;CAAE,GAAG,IAAI,CAK/E;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"}
@@ -2,14 +2,14 @@ import crypto from 'crypto';
2
2
  import jwt from 'jsonwebtoken';
3
3
  import { AuthError } from '@tapflowio/agent-core';
4
4
  import { getDb } from '../db.js';
5
- import { jwtSecret } from '../lib/config.js';
5
+ import { getJwtSecret } from '../lib/config.js';
6
6
  const JWT_EXPIRES = '7d';
7
7
  export function signJwt(payload) {
8
- return jwt.sign(payload, jwtSecret, { expiresIn: JWT_EXPIRES });
8
+ return jwt.sign(payload, getJwtSecret(), { expiresIn: JWT_EXPIRES });
9
9
  }
10
10
  export function verifyJwtOrThrow(token) {
11
11
  try {
12
- return jwt.verify(token, jwtSecret);
12
+ return jwt.verify(token, getJwtSecret());
13
13
  }
14
14
  catch (cause) {
15
15
  throw new AuthError('Invalid or expired auth token', { cause });
@@ -23,12 +23,34 @@ export function verifyJwt(token) {
23
23
  return null;
24
24
  }
25
25
  }
26
+ /**
27
+ * The cookie's user, **checked against the users table on every call**.
28
+ *
29
+ * The JWT lives seven days and removing a member deletes their row, so a signature check alone kept a
30
+ * removed member signed in for up to a week on every cookie path: this function, `requireViewAuth`,
31
+ * `requireBuildAuth`, recordings, and the WebSocket handshake. This is the one place all of them pass,
32
+ * so the row check lives here. User ids are `AUTOINCREMENT`, so a removed id never comes back and "the
33
+ * row exists" is a sound test. `email` and `role` come from the row, never from the JWT.
34
+ *
35
+ * Only the JWT verification failure becomes `null`. A database error propagates: turning a transient
36
+ * `SQLITE_BUSY` into "not signed in" would sign everyone out and hide the fault.
37
+ */
26
38
  export function getAuth(req) {
27
39
  const cookie = req.headers.cookie ?? '';
28
40
  const match = /(?:^|;\s*)tapflow_token=([^;]+)/.exec(cookie);
29
41
  if (!match)
30
42
  return null;
31
- return verifyJwt(match[1]);
43
+ const claims = verifyJwt(match[1]);
44
+ if (!claims)
45
+ return null;
46
+ const user = findUser(claims.userId);
47
+ if (!user)
48
+ return null;
49
+ return { userId: claims.userId, email: user.email, role: user.role, exp: claims.exp };
50
+ }
51
+ export function findUser(userId) {
52
+ const row = getDb().prepare('SELECT email, role FROM users WHERE id = ?').get(userId);
53
+ return row ?? null;
32
54
  }
33
55
  export function requireAuth(req, res) {
34
56
  const auth = getAuth(req);
@@ -39,34 +61,90 @@ export function requireAuth(req, res) {
39
61
  }
40
62
  return auth;
41
63
  }
64
+ // The cookie JWT carries a role for its whole 7-day life, so a demoted member would keep their old
65
+ // rights for a week and a promoted one would be refused for a week. Every role decision reads the
66
+ // users table instead; the JWT only says who is asking. A missing row means the member was removed.
67
+ export function currentRole(res, userId) {
68
+ const row = getDb().prepare('SELECT role FROM users WHERE id = ?').get(userId);
69
+ if (!row) {
70
+ res.writeHead(401, { 'Content-Type': 'application/json' });
71
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
72
+ return null;
73
+ }
74
+ return row.role;
75
+ }
42
76
  export function requireRole(req, res, roles) {
43
77
  const auth = requireAuth(req, res);
44
78
  if (!auth)
45
79
  return null;
46
- if (!roles.includes(auth.role)) {
80
+ const role = currentRole(res, auth.userId);
81
+ if (role === null)
82
+ return null;
83
+ if (!roles.includes(role)) {
47
84
  res.writeHead(403, { 'Content-Type': 'application/json' });
48
85
  res.end(JSON.stringify({ error: 'Forbidden' }));
49
86
  return null;
50
87
  }
51
- return auth;
88
+ return { ...auth, role };
52
89
  }
90
+ // Viewer is read-only: it can look at builds, test them in a QA Session and comment, but not change
91
+ // builds, apps or webhooks. Called after a route's own auth, so it never changes which credentials
92
+ // the route accepts. The PAT path matters most here: any role can mint a builds:write token through
93
+ // the API, so the owner's role is what decides, at the moment the token is used.
94
+ export function assertCanWrite(res, userId) {
95
+ const role = currentRole(res, userId);
96
+ if (role === null)
97
+ return false;
98
+ if (role === 'Viewer') {
99
+ res.writeHead(403, { 'Content-Type': 'application/json' });
100
+ res.end(JSON.stringify({ error: 'Viewers have read-only access' }));
101
+ return false;
102
+ }
103
+ return true;
104
+ }
105
+ function toPatAuth(row) {
106
+ return {
107
+ patId: row.id,
108
+ userId: row.user_id,
109
+ scopes: row.scope.split(',').map((s) => s.trim()),
110
+ ownerEmail: row.email,
111
+ ownerRole: row.role,
112
+ };
113
+ }
114
+ // Joined with users so the owner's current role comes with the token: an `agent` token is only as good
115
+ // as its owner's Admin role, and a Viewer's `builds:write` token is refused on write routes.
116
+ // `datetime(expires_at)`, not the bare column: it is written by `toISOString()` ('2026-09-27T10:00:00.000Z')
117
+ // and `datetime('now')` is '2026-09-27 10:00:00'. Compared as text, 'T' sorts after ' ', so a token that
118
+ // expired this morning kept passing until the end of its UTC day.
119
+ const PAT_SELECT = `
120
+ SELECT pat.id, pat.user_id, pat.scope, u.email, u.role
121
+ FROM personal_access_tokens pat
122
+ JOIN users u ON u.id = pat.user_id
123
+ WHERE (pat.expires_at IS NULL OR datetime(pat.expires_at) > datetime('now'))`;
124
+ /**
125
+ * The PAT on this request, or null when there is none, it is unknown, or it has expired.
126
+ *
127
+ * **Reads only.** `last_used_at` is written by `touchPat` once the credential check has accepted the
128
+ * token: a token refused there (a scope it lacks, a WebSocket refused for its scope or its owner's
129
+ * role) is not used, and a "last used" that moved on those told an Admin a leaked token was in use
130
+ * when nothing got through. A route that accepts the token and then refuses the request on the
131
+ * owner's role (`assertCanWrite` for a Viewer) still counts as a use — the token did reach the route.
132
+ */
53
133
  export function verifyPat(req) {
54
134
  const header = req.headers.authorization ?? '';
55
135
  if (!header.startsWith('Bearer tflw_pat_'))
56
136
  return null;
57
- const token = header.slice(7);
58
- const hash = hashPat(token);
59
- const db = getDb();
60
- const row = db.prepare(`
61
- SELECT pat.user_id, pat.scope
62
- FROM personal_access_tokens pat
63
- WHERE pat.token_hash = ?
64
- AND (pat.expires_at IS NULL OR pat.expires_at > datetime('now'))
65
- `).get(hash);
66
- if (!row)
67
- return null;
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 };
137
+ const hash = hashPat(header.slice(7));
138
+ const row = getDb().prepare(`${PAT_SELECT} AND pat.token_hash = ?`).get(hash);
139
+ return row ? toPatAuth(row) : null;
140
+ }
141
+ /** The same lookup by id, for re-checking a token an open socket was accepted with. */
142
+ export function findPat(patId) {
143
+ const row = getDb().prepare(`${PAT_SELECT} AND pat.id = ?`).get(patId);
144
+ return row ? toPatAuth(row) : null;
145
+ }
146
+ export function touchPat(patId) {
147
+ getDb().prepare(`UPDATE personal_access_tokens SET last_used_at = datetime('now') WHERE id = ?`).run(patId);
70
148
  }
71
149
  export function hashPat(token) {
72
150
  return crypto.createHash('sha256').update(token).digest('hex');
@@ -81,29 +159,23 @@ export function requireViewAuth(req, res) {
81
159
  res.end(JSON.stringify({ error: 'Unauthorized' }));
82
160
  return null;
83
161
  }
84
- if (!pat.scope.split(',').map((s) => s.trim()).includes('view')) {
162
+ if (!pat.scopes.includes('view')) {
85
163
  res.writeHead(403, { 'Content-Type': 'application/json' });
86
164
  res.end(JSON.stringify({ error: 'Insufficient scope' }));
87
165
  return null;
88
166
  }
89
- const user = getDb()
90
- .prepare('SELECT email, role FROM users WHERE id = ?')
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 };
167
+ touchPat(pat.patId);
168
+ return { userId: pat.userId, email: pat.ownerEmail, role: pat.ownerRole };
98
169
  }
99
170
  export function requireBuildAuth(req, res) {
100
171
  const pat = verifyPat(req);
101
172
  if (pat !== null) {
102
- if (!pat.scope.split(',').map((s) => s.trim()).includes('builds:write')) {
173
+ if (!pat.scopes.includes('builds:write')) {
103
174
  res.writeHead(403, { 'Content-Type': 'application/json' });
104
175
  res.end(JSON.stringify({ error: 'Insufficient scope' }));
105
176
  return null;
106
177
  }
178
+ touchPat(pat.patId);
107
179
  return { userId: pat.userId };
108
180
  }
109
181
  const auth = requireAuth(req, res);
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from 'path';
2
2
  import { initDb } from './db.js';
3
3
  import { RelayServer } from './RelayServer.js';
4
- import { config, loadedEnvPath } from './lib/config.js';
4
+ import { config, loadedEnvPath, assertInstallDir } from './lib/config.js';
5
5
  import { buildCorsOrigins, containerWithoutPublicUrlWarning, proxyWithoutPublicUrlWarning } from './lib/proxyConfig.js';
6
6
  import { runningInContainer } from './lib/lanAddress.js';
7
7
  import { bootstrapAdminFromEnv, AdminBootstrapError } from './lib/adminBootstrap.js';
@@ -9,6 +9,7 @@ import { createCertProvider, resolveRelayDisplayHost } from './lib/cert/index.js
9
9
  import { startTlsBackgroundTasks } from './lib/tlsTasks.js';
10
10
  import { createLogger } from '@tapflowio/agent-core';
11
11
  const logger = createLogger('relay');
12
+ assertInstallDir();
12
13
  const { port, dataDir } = config.local;
13
14
  // config loaded <dataDir>/.env before reading any secret (JWT/SMTP/DNS tokens); just report it here.
14
15
  if (loadedEnvPath)