@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.
Files changed (164) hide show
  1. package/dist/RelayServer.d.ts +51 -0
  2. package/dist/RelayServer.d.ts.map +1 -1
  3. package/dist/RelayServer.js +262 -31
  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 +30 -19
  11. package/dist/api/auth.js.map +1 -1
  12. package/dist/api/builds.d.ts.map +1 -1
  13. package/dist/api/builds.js +7 -5
  14. package/dist/api/builds.js.map +1 -1
  15. package/dist/api/comments.d.ts.map +1 -1
  16. package/dist/api/comments.js +41 -15
  17. package/dist/api/comments.js.map +1 -1
  18. package/dist/api/invitations.d.ts +2 -0
  19. package/dist/api/invitations.d.ts.map +1 -1
  20. package/dist/api/invitations.js +17 -17
  21. package/dist/api/invitations.js.map +1 -1
  22. package/dist/api/passwordReset.d.ts +13 -2
  23. package/dist/api/passwordReset.d.ts.map +1 -1
  24. package/dist/api/passwordReset.js +47 -12
  25. package/dist/api/passwordReset.js.map +1 -1
  26. package/dist/api/team.d.ts +2 -2
  27. package/dist/api/team.d.ts.map +1 -1
  28. package/dist/api/team.js +15 -5
  29. package/dist/api/team.js.map +1 -1
  30. package/dist/api/tokens.d.ts +1 -1
  31. package/dist/api/tokens.d.ts.map +1 -1
  32. package/dist/api/tokens.js +33 -8
  33. package/dist/api/tokens.js.map +1 -1
  34. package/dist/api/webhooks.d.ts.map +1 -1
  35. package/dist/api/webhooks.js +11 -5
  36. package/dist/api/webhooks.js.map +1 -1
  37. package/dist/lib/adminAccount.d.ts +0 -5
  38. package/dist/lib/adminAccount.d.ts.map +1 -1
  39. package/dist/lib/adminAccount.js +6 -7
  40. package/dist/lib/adminAccount.js.map +1 -1
  41. package/dist/lib/clientAddress.d.ts +7 -0
  42. package/dist/lib/clientAddress.d.ts.map +1 -1
  43. package/dist/lib/clientAddress.js +15 -0
  44. package/dist/lib/clientAddress.js.map +1 -1
  45. package/dist/lib/connectionAuth.d.ts +58 -2
  46. package/dist/lib/connectionAuth.d.ts.map +1 -1
  47. package/dist/lib/connectionAuth.js +66 -6
  48. package/dist/lib/connectionAuth.js.map +1 -1
  49. package/dist/lib/email.d.ts +16 -0
  50. package/dist/lib/email.d.ts.map +1 -0
  51. package/dist/lib/email.js +18 -0
  52. package/dist/lib/email.js.map +1 -0
  53. package/dist/middleware/auth.d.ts +63 -4
  54. package/dist/middleware/auth.d.ts.map +1 -1
  55. package/dist/middleware/auth.js +127 -28
  56. package/dist/middleware/auth.js.map +1 -1
  57. package/dist/migrations/014_normalize_emails.sql +16 -0
  58. package/package.json +3 -3
  59. package/public/assets/AppCenter-DZCsC2wa.js +26 -0
  60. package/public/assets/AppCenter-DZCsC2wa.js.br +0 -0
  61. package/public/assets/AppCenter-DZCsC2wa.js.gz +0 -0
  62. package/public/assets/Default-xD9Fmd5X.js +1 -0
  63. package/public/assets/Default-xD9Fmd5X.js.br +0 -0
  64. package/public/assets/Default-xD9Fmd5X.js.gz +0 -0
  65. package/public/assets/Invite-29Erwv8j.js +1 -0
  66. package/public/assets/Invite-29Erwv8j.js.br +0 -0
  67. package/public/assets/Invite-29Erwv8j.js.gz +0 -0
  68. package/public/assets/{MacResources-fCn40Tf5.js → MacResources-B0DQ9ISE.js} +1 -1
  69. package/public/assets/MacResources-B0DQ9ISE.js.br +0 -0
  70. package/public/assets/MacResources-B0DQ9ISE.js.gz +0 -0
  71. package/public/assets/QASession-D-n6PXuD.js +150 -0
  72. package/public/assets/QASession-D-n6PXuD.js.br +0 -0
  73. package/public/assets/QASession-D-n6PXuD.js.gz +0 -0
  74. package/public/assets/{ResetPassword-D-SFg4ap.js → ResetPassword-BnPIwTYH.js} +1 -1
  75. package/public/assets/ResetPassword-BnPIwTYH.js.br +0 -0
  76. package/public/assets/ResetPassword-BnPIwTYH.js.gz +0 -0
  77. package/public/assets/{Setup-ULjvIfAi.js → Setup-DmUGFblg.js} +1 -1
  78. package/public/assets/Setup-DmUGFblg.js.br +0 -0
  79. package/public/assets/Setup-DmUGFblg.js.gz +0 -0
  80. package/public/assets/Team-BpvRgFfZ.js +6 -0
  81. package/public/assets/Team-BpvRgFfZ.js.br +0 -0
  82. package/public/assets/Team-BpvRgFfZ.js.gz +0 -0
  83. package/public/assets/Tokens-x6BtCcL7.js +1 -0
  84. package/public/assets/Tokens-x6BtCcL7.js.br +0 -0
  85. package/public/assets/Tokens-x6BtCcL7.js.gz +0 -0
  86. package/public/assets/alert-dialog-C8wDimW-.js +1 -0
  87. package/public/assets/alert-dialog-C8wDimW-.js.br +0 -0
  88. package/public/assets/alert-dialog-C8wDimW-.js.gz +0 -0
  89. package/public/assets/{badge-B4QAWrzA.js → badge-CWVbeuH3.js} +1 -1
  90. package/public/assets/badge-CWVbeuH3.js.br +0 -0
  91. package/public/assets/badge-CWVbeuH3.js.gz +0 -0
  92. package/public/assets/{build-format-CtmRXihN.js → build-format-fzt-JrUX.js} +1 -1
  93. package/public/assets/build-format-fzt-JrUX.js.br +0 -0
  94. package/public/assets/build-format-fzt-JrUX.js.gz +0 -0
  95. package/public/assets/{dialog-BAFN_ONP.js → dialog-DQkbaw8h.js} +3 -3
  96. package/public/assets/dialog-DQkbaw8h.js.br +0 -0
  97. package/public/assets/dialog-DQkbaw8h.js.gz +0 -0
  98. package/public/assets/{index-COOWQXpW.js → index-C1Fo8g_c.js} +33 -33
  99. package/public/assets/index-C1Fo8g_c.js.br +0 -0
  100. package/public/assets/index-C1Fo8g_c.js.gz +0 -0
  101. package/public/assets/index-C2F1AgwB.css +1 -0
  102. package/public/assets/index-C2F1AgwB.css.br +0 -0
  103. package/public/assets/{index-ozCdBYxk.css.gz → index-C2F1AgwB.css.gz} +0 -0
  104. package/public/assets/{list-view-Rrqp8TkG.js → list-view-DxI397-H.js} +1 -1
  105. package/public/assets/list-view-DxI397-H.js.br +0 -0
  106. package/public/assets/list-view-DxI397-H.js.gz +0 -0
  107. package/public/assets/{pencil-CAHyK7Qf.js → pencil-BJcOp3oN.js} +1 -1
  108. package/public/assets/pencil-BJcOp3oN.js.br +0 -0
  109. package/public/assets/pencil-BJcOp3oN.js.gz +0 -0
  110. package/public/assets/resource-health-Cq3gqqVd.js +1 -0
  111. package/public/assets/resource-health-Cq3gqqVd.js.br +0 -0
  112. package/public/assets/resource-health-Cq3gqqVd.js.gz +0 -0
  113. package/public/assets/{trash-CkO-5TRA.js → trash-DhBxh6H7.js} +1 -1
  114. package/public/assets/trash-DhBxh6H7.js.br +0 -0
  115. package/public/assets/trash-DhBxh6H7.js.gz +0 -0
  116. package/public/index.html +2 -2
  117. package/public/index.html.br +0 -0
  118. package/public/index.html.gz +0 -0
  119. package/public/assets/AppCenter-BHz6vh5p.js +0 -26
  120. package/public/assets/AppCenter-BHz6vh5p.js.br +0 -0
  121. package/public/assets/AppCenter-BHz6vh5p.js.gz +0 -0
  122. package/public/assets/Default-bCqU0Qz9.js +0 -1
  123. package/public/assets/Default-bCqU0Qz9.js.br +0 -0
  124. package/public/assets/Default-bCqU0Qz9.js.gz +0 -0
  125. package/public/assets/Invite-EVMTsuNz.js +0 -1
  126. package/public/assets/Invite-EVMTsuNz.js.br +0 -0
  127. package/public/assets/Invite-EVMTsuNz.js.gz +0 -0
  128. package/public/assets/MacResources-fCn40Tf5.js.br +0 -0
  129. package/public/assets/MacResources-fCn40Tf5.js.gz +0 -0
  130. package/public/assets/QASession-CL-eyFxG.js +0 -150
  131. package/public/assets/QASession-CL-eyFxG.js.br +0 -0
  132. package/public/assets/QASession-CL-eyFxG.js.gz +0 -0
  133. package/public/assets/ResetPassword-D-SFg4ap.js.br +0 -0
  134. package/public/assets/ResetPassword-D-SFg4ap.js.gz +0 -0
  135. package/public/assets/Setup-ULjvIfAi.js.br +0 -0
  136. package/public/assets/Setup-ULjvIfAi.js.gz +0 -0
  137. package/public/assets/Team-DH91GD4w.js +0 -6
  138. package/public/assets/Team-DH91GD4w.js.br +0 -0
  139. package/public/assets/Team-DH91GD4w.js.gz +0 -0
  140. package/public/assets/Tokens-DImLpOVZ.js +0 -1
  141. package/public/assets/Tokens-DImLpOVZ.js.br +0 -0
  142. package/public/assets/Tokens-DImLpOVZ.js.gz +0 -0
  143. package/public/assets/alert-dialog-CwcUyDZQ.js +0 -1
  144. package/public/assets/alert-dialog-CwcUyDZQ.js.br +0 -0
  145. package/public/assets/alert-dialog-CwcUyDZQ.js.gz +0 -0
  146. package/public/assets/badge-B4QAWrzA.js.br +0 -0
  147. package/public/assets/badge-B4QAWrzA.js.gz +0 -0
  148. package/public/assets/build-format-CtmRXihN.js.br +0 -0
  149. package/public/assets/build-format-CtmRXihN.js.gz +0 -0
  150. package/public/assets/dialog-BAFN_ONP.js.br +0 -0
  151. package/public/assets/dialog-BAFN_ONP.js.gz +0 -0
  152. package/public/assets/index-COOWQXpW.js.br +0 -0
  153. package/public/assets/index-COOWQXpW.js.gz +0 -0
  154. package/public/assets/index-ozCdBYxk.css +0 -1
  155. package/public/assets/index-ozCdBYxk.css.br +0 -0
  156. package/public/assets/list-view-Rrqp8TkG.js.br +0 -0
  157. package/public/assets/list-view-Rrqp8TkG.js.gz +0 -0
  158. package/public/assets/pencil-CAHyK7Qf.js.br +0 -0
  159. package/public/assets/pencil-CAHyK7Qf.js.gz +0 -0
  160. package/public/assets/resource-health-BRpSwarg.js +0 -1
  161. package/public/assets/resource-health-BRpSwarg.js.br +0 -0
  162. package/public/assets/resource-health-BRpSwarg.js.gz +0 -0
  163. package/public/assets/trash-CkO-5TRA.js.br +0 -0
  164. package/public/assets/trash-CkO-5TRA.js.gz +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"team.d.ts","sourceRoot":"","sources":["../../src/api/team.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAOvB,OAAO,EAA0D,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEhH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAS3F;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA8B7H;AAED,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,OAAO,CAAC,IAAI,CAAC,CAaf;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,IAAI,CAYN"}
1
+ {"version":3,"file":"team.d.ts","sourceRoot":"","sources":["../../src/api/team.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AASvB,OAAO,EAA0D,KAAK,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAEhH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAS3F;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAiC7H;AAED,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,aAAa,GAAE,MAAM,IAAe,GACnC,OAAO,CAAC,IAAI,CAAC,CAef;AAED,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,aAAa,GAAE,MAAM,IAAe,GACnC,IAAI,CAcN"}
package/dist/api/team.js CHANGED
@@ -4,6 +4,8 @@ import { requireRole } from '../middleware/auth.js';
4
4
  import { json, readJson } from '../router.js';
5
5
  import { sendMail } from '../lib/mailer.js';
6
6
  import { config } from '../lib/config.js';
7
+ import { normalizeEmail } from '../lib/email.js';
8
+ import { isMemberEmail } from './invitations.js';
7
9
  import { buildInviteBaseUrl, forTeammates, resolvePublicBaseUrl } from '../lib/publicUrl.js';
8
10
  export function handleListMembers(req, res) {
9
11
  const auth = requireRole(req, res, ['Admin']);
@@ -18,6 +20,10 @@ export async function handleInvite(req, res, tunnel) {
18
20
  if (!auth)
19
21
  return;
20
22
  const body = await readJson(req);
23
+ const email = body.email ? normalizeEmail(body.email) : '';
24
+ // Inviting a member would only produce a link that is refused on accept.
25
+ if (email && isMemberEmail(email))
26
+ return json(res, 409, { error: 'Already a member' });
21
27
  const role = ['Admin', 'Developer', 'QA', 'Viewer'].includes(body.role ?? '')
22
28
  ? body.role
23
29
  : 'QA';
@@ -25,21 +31,21 @@ export async function handleInvite(req, res, tunnel) {
25
31
  const expiresAt = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
26
32
  const db = getDb();
27
33
  db.prepare('INSERT INTO invitations (token, email, role, expires_at) VALUES (?, ?, ?, ?)')
28
- .run(token, body.email ?? null, role, expiresAt);
34
+ .run(token, email || null, role, expiresAt);
29
35
  // The dashboard copies the same link the mail carries (#788). It gets null when the only address is
30
36
  // the mail's localhost fallback or a loopback relay.url, and builds from the browser's origin instead.
31
37
  const inviteUrl = `${buildInviteBaseUrl(config, tunnel)}/invite?token=${token}`;
32
38
  const offered = forTeammates(resolvePublicBaseUrl(config, tunnel)) === null ? null : inviteUrl;
33
39
  let emailSent = false;
34
- if (body.email) {
40
+ if (email) {
35
41
  const html = `<p>You've been invited to join tapflow as <strong>${role}</strong>.</p>
36
42
  <p><a href="${inviteUrl}">Accept invitation</a></p>
37
43
  <p>This link expires in 7 days.</p>`;
38
- emailSent = await sendMail(body.email, 'You have been invited to tapflow', html);
44
+ emailSent = await sendMail(email, 'You have been invited to tapflow', html);
39
45
  }
40
46
  json(res, 201, { token, emailSent, inviteUrl: offered });
41
47
  }
42
- export async function handleUpdateMember(req, res, params) {
48
+ export async function handleUpdateMember(req, res, params, onAuthChanged = () => { }) {
43
49
  const auth = requireRole(req, res, ['Admin']);
44
50
  if (!auth)
45
51
  return;
@@ -51,9 +57,11 @@ export async function handleUpdateMember(req, res, params) {
51
57
  const result = db.prepare('UPDATE users SET role = ? WHERE id = ?').run(body.role, params.id);
52
58
  if (result.changes === 0)
53
59
  return json(res, 404, { error: 'Member not found' });
60
+ // A demoted Admin's agent tokens stop working now, including on sockets already open.
61
+ onAuthChanged();
54
62
  json(res, 200, { ok: true });
55
63
  }
56
- export function handleDeleteMember(req, res, params) {
64
+ export function handleDeleteMember(req, res, params, onAuthChanged = () => { }) {
57
65
  const auth = requireRole(req, res, ['Admin']);
58
66
  if (!auth)
59
67
  return;
@@ -64,6 +72,8 @@ export function handleDeleteMember(req, res, params) {
64
72
  const result = db.prepare('DELETE FROM users WHERE id = ?').run(params.id);
65
73
  if (result.changes === 0)
66
74
  return json(res, 404, { error: 'Member not found' });
75
+ // Their cookie and PATs (cascaded) stop working on HTTP at once; this closes their open sockets too.
76
+ onAuthChanged();
67
77
  json(res, 204, null);
68
78
  }
69
79
  //# sourceMappingURL=team.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"team.js","sourceRoot":"","sources":["../../src/api/team.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"team.js","sourceRoot":"","sources":["../../src/api/team.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import http from 'http';
2
2
  export declare function handleListTokens(req: http.IncomingMessage, res: http.ServerResponse): void;
3
3
  export declare function handleCreateToken(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
4
- export declare function handleRevokeToken(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>): void;
4
+ export declare function handleRevokeToken(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>, onAuthChanged?: () => void): void;
5
5
  //# sourceMappingURL=tokens.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/api/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAUvB,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAS1F;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA4B1G;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,IAAI,CAWN"}
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/api/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAUvB,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAS1F;AAED,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAkD1G;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,aAAa,GAAE,MAAM,IAAe,GACnC,IAAI,CAaN"}
@@ -1,6 +1,6 @@
1
1
  import crypto from 'crypto';
2
2
  import { getDb } from '../db.js';
3
- import { requireAuth, hashPat } from '../middleware/auth.js';
3
+ import { currentRole, requireAuth, hashPat } from '../middleware/auth.js';
4
4
  import { json, readJson } from '../router.js';
5
5
  import { AGENT_SCOPE } from '../lib/connectionAuth.js';
6
6
  const ALLOWED_SCOPES = new Set(['view', 'builds:write', AGENT_SCOPE]);
@@ -14,10 +14,16 @@ export function handleListTokens(req, res) {
14
14
  json(res, 200, tokens);
15
15
  }
16
16
  export async function handleCreateToken(req, res) {
17
+ if (!requireAuth(req, res))
18
+ return;
19
+ const body = await readJson(req);
20
+ // Checked again once the body is in: a request can hold its body back for as long as it likes, and a
21
+ // password reset that commits meanwhile revokes every token of the account and ends its sessions. A
22
+ // token minted from the check made before the wait would outlive that reset. From here to the INSERT
23
+ // nothing awaits, so no reset can land between this check and the write.
17
24
  const auth = requireAuth(req, res);
18
25
  if (!auth)
19
26
  return;
20
- const body = await readJson(req);
21
27
  if (!body.name?.trim())
22
28
  return json(res, 400, { error: 'name required' });
23
29
  const scopes = (body.scope?.trim() || DEFAULT_SCOPE).split(',').map((s) => s.trim());
@@ -25,19 +31,36 @@ export async function handleCreateToken(req, res) {
25
31
  return json(res, 400, { error: `Invalid scope. Allowed: ${[...ALLOWED_SCOPES].join(', ')}` });
26
32
  }
27
33
  // agent 스코프 토큰은 화면을 공급하는 에이전트의 자격이므로 발급을 Admin으로 제한한다 (#271).
28
- if (scopes.includes(AGENT_SCOPE) && auth.role !== 'Admin') {
29
- return json(res, 403, { error: `'${AGENT_SCOPE}' scope requires the Admin role` });
34
+ // The DB role, not the JWT's: a demoted Admin keeps a valid cookie for up to 7 days.
35
+ if (scopes.includes(AGENT_SCOPE)) {
36
+ const role = currentRole(res, auth.userId);
37
+ if (role === null)
38
+ return;
39
+ if (role !== 'Admin') {
40
+ return json(res, 403, { error: `'${AGENT_SCOPE}' scope requires the Admin role` });
41
+ }
42
+ }
43
+ // Omitted or 0 means no expiry. A negative count used to mint a token that was already expired, and one too
44
+ // large for a Date threw on toISOString(). No upper cap: 365 is the dialog's limit, and API callers may rely on more.
45
+ // Only a number or a numeric string counts: `Number()` alone turns "" / " " / [] into 0, so a CI template whose
46
+ // variable came out empty would get a token that never expires — the most permissive result, silently.
47
+ const raw = body.expires_in_days;
48
+ const days = raw == null ? 0
49
+ : typeof raw === 'number' ? raw
50
+ : typeof raw === 'string' && raw.trim() !== '' ? Number(raw)
51
+ : NaN;
52
+ const expiresDate = days ? new Date(Date.now() + days * 24 * 3600 * 1000) : null;
53
+ if (!Number.isFinite(days) || days < 0 || (expiresDate && isNaN(expiresDate.getTime()))) {
54
+ return json(res, 400, { error: 'expires_in_days must be a number of days, 0 or more and small enough to be a date (omit it or send 0 for no expiry)' });
30
55
  }
31
56
  const rawToken = `tflw_pat_${crypto.randomBytes(32).toString('hex')}`;
32
57
  const tokenHash = hashPat(rawToken);
33
- const expiresAt = body.expires_in_days
34
- ? new Date(Date.now() + body.expires_in_days * 24 * 3600 * 1000).toISOString()
35
- : null;
58
+ const expiresAt = expiresDate ? expiresDate.toISOString() : null;
36
59
  const db = getDb();
37
60
  db.prepare('INSERT INTO personal_access_tokens (user_id, name, token_hash, scope, expires_at) VALUES (?, ?, ?, ?, ?)').run(auth.userId, body.name.trim(), tokenHash, scopes.join(','), expiresAt);
38
61
  json(res, 201, { token: rawToken });
39
62
  }
40
- export function handleRevokeToken(req, res, params) {
63
+ export function handleRevokeToken(req, res, params, onAuthChanged = () => { }) {
41
64
  const auth = requireAuth(req, res);
42
65
  if (!auth)
43
66
  return;
@@ -45,6 +68,8 @@ export function handleRevokeToken(req, res, params) {
45
68
  const result = db.prepare('DELETE FROM personal_access_tokens WHERE id = ? AND user_id = ?').run(params.id, auth.userId);
46
69
  if (result.changes === 0)
47
70
  return json(res, 404, { error: 'Token not found' });
71
+ // Closes the sockets that were opened with it.
72
+ onAuthChanged();
48
73
  json(res, 204, null);
49
74
  }
50
75
  //# sourceMappingURL=tokens.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/api/tokens.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/api/tokens.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../src/api/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAmB5B,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAI5F;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAY5G;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,OAAO,CAAC,IAAI,CAAC,CA6Bf;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,IAAI,CAKN"}
1
+ {"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../src/api/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAqB5B,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAK5F;AAED,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAa5G;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAED,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,IAAI,CAMN"}
@@ -1,19 +1,23 @@
1
1
  import { getDb } from '../db.js';
2
- import { requireBuildAuth } from '../middleware/auth.js';
2
+ import { assertCanWrite, requireBuildAuth } from '../middleware/auth.js';
3
3
  import { json, readJson } from '../router.js';
4
4
  import { validateWebhookUrl } from '../lib/webhooks.js';
5
5
  // secret is write-only: never returned, only whether one is set.
6
6
  function publicView(r) {
7
7
  return { id: r.id, url: r.url, enabled: !!r.enabled, has_secret: !!r.secret, created_at: r.created_at };
8
8
  }
9
+ // All four routes, GET included, are closed to Viewer: webhook URLs are often secrets in themselves
10
+ // (a Slack incoming webhook is its own credential), so reading the list is not a read-only act.
9
11
  export function handleListWebhooks(req, res) {
10
- if (!requireBuildAuth(req, res))
12
+ const auth = requireBuildAuth(req, res);
13
+ if (!auth || !assertCanWrite(res, auth.userId))
11
14
  return;
12
15
  const rows = getDb().prepare('SELECT * FROM webhook_endpoints ORDER BY id').all();
13
16
  json(res, 200, { webhooks: rows.map(publicView) });
14
17
  }
15
18
  export async function handleCreateWebhook(req, res) {
16
- if (!requireBuildAuth(req, res))
19
+ const auth = requireBuildAuth(req, res);
20
+ if (!auth || !assertCanWrite(res, auth.userId))
17
21
  return;
18
22
  const body = await readJson(req);
19
23
  if (!body.url || typeof body.url !== 'string')
@@ -29,7 +33,8 @@ export async function handleCreateWebhook(req, res) {
29
33
  json(res, 201, publicView(row));
30
34
  }
31
35
  export async function handleUpdateWebhook(req, res, params) {
32
- if (!requireBuildAuth(req, res))
36
+ const auth = requireBuildAuth(req, res);
37
+ if (!auth || !assertCanWrite(res, auth.userId))
33
38
  return;
34
39
  const db = getDb();
35
40
  const existing = db.prepare('SELECT id FROM webhook_endpoints WHERE id = ?').get(params.id);
@@ -62,7 +67,8 @@ export async function handleUpdateWebhook(req, res, params) {
62
67
  json(res, 200, publicView(row));
63
68
  }
64
69
  export function handleDeleteWebhook(req, res, params) {
65
- if (!requireBuildAuth(req, res))
70
+ const auth = requireBuildAuth(req, res);
71
+ if (!auth || !assertCanWrite(res, auth.userId))
66
72
  return;
67
73
  const r = getDb().prepare('DELETE FROM webhook_endpoints WHERE id = ?').run(params.id);
68
74
  if (r.changes === 0)
@@ -1 +1 @@
1
- {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../src/api/webhooks.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"webhooks.js","sourceRoot":"","sources":["../../src/api/webhooks.ts"],"names":[],"mappings":"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"}
@@ -5,11 +5,6 @@ export declare function verifyPassword(password: string, stored: string): boolea
5
5
  *
6
6
  * Shared so the two bootstrap paths cannot disagree about what "already initialized" means.
7
7
  *
8
- * They are not identical in every respect: the boot path trims `TAPFLOW_ADMIN_EMAIL` and the HTTP
9
- * one stores `body.email` as sent, so the same address with surrounding whitespace produces two
10
- * different rows and the HTTP one never matches `handleLogin`'s `WHERE email = ?`. Normalising
11
- * inside this function would change what a shipped endpoint stores, so it is a decision rather than
12
- * a fix here — tracked separately.
13
8
  */
14
9
  export declare function isInitialized(): boolean;
15
10
  /** Why `createAdminAccount` refused, or `ok`. The caller turns this into an HTTP status or a log line. */
@@ -1 +1 @@
1
- {"version":3,"file":"adminAccount.d.ts","sourceRoot":"","sources":["../../src/lib/adminAccount.ts"],"names":[],"mappings":"AAgBA,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAQxE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAGvC;AAED,0GAA0G;AAC1G,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,gBAAgB,GAAG,oBAAoB,CAAA;AAE9E;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAOrF"}
1
+ {"version":3,"file":"adminAccount.d.ts","sourceRoot":"","sources":["../../src/lib/adminAccount.ts"],"names":[],"mappings":"AAiBA,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAIzD;AAED,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAQxE;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAGvC;AAED,0GAA0G;AAC1G,MAAM,MAAM,iBAAiB,GAAG,IAAI,GAAG,gBAAgB,GAAG,oBAAoB,CAAA;AAE9E;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,iBAAiB,CAUrF"}
@@ -1,5 +1,6 @@
1
1
  import crypto from 'crypto';
2
2
  import { getDb } from '../db.js';
3
+ import { normalizeEmail } from './email.js';
3
4
  // The first-owner rules, and the password hashing they need.
4
5
  //
5
6
  // **Why this is in `lib/` and not beside the HTTP handlers.** Two callers need it: `api/auth.ts`
@@ -32,11 +33,6 @@ export function verifyPassword(password, stored) {
32
33
  *
33
34
  * Shared so the two bootstrap paths cannot disagree about what "already initialized" means.
34
35
  *
35
- * They are not identical in every respect: the boot path trims `TAPFLOW_ADMIN_EMAIL` and the HTTP
36
- * one stores `body.email` as sent, so the same address with surrounding whitespace produces two
37
- * different rows and the HTTP one never matches `handleLogin`'s `WHERE email = ?`. Normalising
38
- * inside this function would change what a shipped endpoint stores, so it is a decision rather than
39
- * a fix here — tracked separately.
40
36
  */
41
37
  export function isInitialized() {
42
38
  const { n } = getDb().prepare('SELECT COUNT(*) as n FROM users').get();
@@ -49,13 +45,16 @@ export function isInitialized() {
49
45
  * differently: 403 over HTTP, a silent no-op on every restart at boot.
50
46
  */
51
47
  export function createAdminAccount(email, password) {
52
- if (!email || !password)
48
+ // Normalized here rather than by each caller, so the boot path and `POST /auth/init` store the same
49
+ // value for the same address (#715).
50
+ const stored = email ? normalizeEmail(email) : '';
51
+ if (!stored || !password)
53
52
  return 'missing-fields';
54
53
  if (password.length < 8)
55
54
  return 'password-too-short';
56
55
  getDb()
57
56
  .prepare('INSERT INTO users (email, display_name, role, password_hash) VALUES (?, ?, ?, ?)')
58
- .run(email, 'Admin', 'Admin', makePasswordHash(password));
57
+ .run(stored, 'Admin', 'Admin', makePasswordHash(password));
59
58
  return 'ok';
60
59
  }
61
60
  //# sourceMappingURL=adminAccount.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"adminAccount.js","sourceRoot":"","sources":["../../src/lib/adminAccount.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAEhC,6DAA6D;AAC7D,EAAE;AACF,iGAAiG;AACjG,qGAAqG;AACrG,oGAAoG;AACpG,qGAAqG;AACrG,+FAA+F;AAC/F,iEAAiE;AAEjE,SAAS,YAAY,CAAC,QAAgB,EAAE,IAAY;IAClD,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AACjF,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;IACnD,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IACzC,OAAO,GAAG,IAAI,IAAI,IAAI,EAAE,CAAA;AAC1B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,MAAc;IAC7D,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACtC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAA;IAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,KAAK,CAAC,CAAA;IACjE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACzC,iEAAiE;IACjE,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACrD,OAAO,MAAM,CAAC,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;AACnD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,EAAE,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC,GAAG,EAAmB,CAAA;IACvF,OAAO,CAAC,GAAG,CAAC,CAAA;AACd,CAAC;AAKD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa,EAAE,QAAgB;IAChE,IAAI,CAAC,KAAK,IAAI,CAAC,QAAQ;QAAE,OAAO,gBAAgB,CAAA;IAChD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,oBAAoB,CAAA;IACpD,KAAK,EAAE;SACJ,OAAO,CAAC,kFAAkF,CAAC;SAC3F,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC3D,OAAO,IAAI,CAAA;AACb,CAAC"}
1
+ {"version":3,"file":"adminAccount.js","sourceRoot":"","sources":["../../src/lib/adminAccount.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE3C,6DAA6D;AAC7D,EAAE;AACF,iGAAiG;AACjG,qGAAqG;AACrG,oGAAoG;AACpG,qGAAqG;AACrG,+FAA+F;AAC/F,iEAAiE;AAEjE,SAAS,YAAY,CAAC,QAAgB,EAAE,IAAY;IAClD,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AACjF,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAgB;IAC/C,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;IACnD,MAAM,IAAI,GAAG,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;IACzC,OAAO,GAAG,IAAI,IAAI,IAAI,EAAE,CAAA;AAC1B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,MAAc;IAC7D,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACtC,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAA;IAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,KAAK,CAAC,CAAA;IACjE,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;IACzC,iEAAiE;IACjE,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACrD,OAAO,MAAM,CAAC,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;AACnD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,EAAE,CAAC,EAAE,GAAG,KAAK,EAAE,CAAC,OAAO,CAAC,iCAAiC,CAAC,CAAC,GAAG,EAAmB,CAAA;IACvF,OAAO,CAAC,GAAG,CAAC,CAAA;AACd,CAAC;AAKD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAa,EAAE,QAAgB;IAChE,oGAAoG;IACpG,qCAAqC;IACrC,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACjD,IAAI,CAAC,MAAM,IAAI,CAAC,QAAQ;QAAE,OAAO,gBAAgB,CAAA;IACjD,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,oBAAoB,CAAA;IACpD,KAAK,EAAE;SACJ,OAAO,CAAC,kFAAkF,CAAC;SAC3F,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAA;IAC5D,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -24,4 +24,11 @@ export interface ResolvedClientAddress {
24
24
  export declare function resolveClientAddress(input: ResolveClientAddressInput): ResolvedClientAddress;
25
25
  export declare function markTunnelIngress(req: http.IncomingMessage): void;
26
26
  export declare function isTunnelIngress(req: http.IncomingMessage): boolean;
27
+ /**
28
+ * Locality of an HTTP request, for every door that serves only the relay host (`auth/init`,
29
+ * `auth/status`, `/api/v1/logs`). The trusted-proxy list is an argument rather than read from config
30
+ * here, so each caller passes the same list the WebSocket path uses: one locality rule, not one per
31
+ * route. The tunnel listener's mark is read here, so no caller can forget it.
32
+ */
33
+ export declare function resolveRequestClient(req: http.IncomingMessage, trustedProxies: string[]): ResolvedClientAddress;
27
34
  //# sourceMappingURL=clientAddress.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"clientAddress.d.ts","sourceRoot":"","sources":["../../src/lib/clientAddress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAmB5B,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAMrE;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,yBAAyB,GAAG,qBAAqB,CAG5F;AA6BD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,IAAI,CAEjE;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,CAElE"}
1
+ {"version":3,"file":"clientAddress.d.ts","sourceRoot":"","sources":["../../src/lib/clientAddress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAmB5B,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAMrE;AAED,MAAM,WAAW,yBAAyB;IACxC,UAAU,EAAE,MAAM,CAAA;IAClB,YAAY,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,cAAc,EAAE,MAAM,EAAE,CAAA;IACxB;;;OAGG;IACH,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,OAAO,CAAA;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,yBAAyB,GAAG,qBAAqB,CAG5F;AA6BD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,IAAI,CAEjE;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,cAAc,EAAE,MAAM,EAAE,GAAG,qBAAqB,CAQ/G"}
@@ -64,4 +64,19 @@ export function markTunnelIngress(req) {
64
64
  export function isTunnelIngress(req) {
65
65
  return tunnelRequests.has(req);
66
66
  }
67
+ /**
68
+ * Locality of an HTTP request, for every door that serves only the relay host (`auth/init`,
69
+ * `auth/status`, `/api/v1/logs`). The trusted-proxy list is an argument rather than read from config
70
+ * here, so each caller passes the same list the WebSocket path uses: one locality rule, not one per
71
+ * route. The tunnel listener's mark is read here, so no caller can forget it.
72
+ */
73
+ export function resolveRequestClient(req, trustedProxies) {
74
+ const xff = req.headers['x-forwarded-for'];
75
+ return resolveClientAddress({
76
+ socketAddr: req.socket.remoteAddress ?? '',
77
+ forwardedFor: Array.isArray(xff) ? xff[0] : xff,
78
+ trustedProxies,
79
+ viaTunnel: isTunnelIngress(req),
80
+ });
81
+ }
67
82
  //# sourceMappingURL=clientAddress.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"clientAddress.js","sourceRoot":"","sources":["../../src/lib/clientAddress.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"clientAddress.js","sourceRoot":"","sources":["../../src/lib/clientAddress.ts"],"names":[],"mappings":"AAEA,qEAAqE;AACrE,sEAAsE;AAEtE,6EAA6E;AAC7E,SAAS,SAAS,CAAC,IAAY;IAC7B,IAAI,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAA;IACnB,MAAM,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAC3B,IAAI,IAAI,KAAK,CAAC,CAAC;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAA;IACrC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IAC5E,OAAO,CAAC,CAAA;AACV,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IACzB,OAAO,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;AAC5C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAuB;IACzD,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAA;IACnB,OAAO,GAAG;SACP,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;SACxB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;AAChC,CAAC;AAkBD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAgC;IACnE,MAAM,QAAQ,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAA;IACxC,OAAO,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAA;AAC7E,CAAC;AAED,SAAS,gBAAgB,CAAC,KAAgC;IACxD,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;IAC1C,MAAM,gBAAgB,GAAG,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAA;IAC9D,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,YAAY,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;IAE7C,IAAI,gBAAgB,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvC,kEAAkE;QAClE,8DAA8D;QAC9D,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QACxF,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;QACxB,OAAO,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,cAAc,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,CAAC,EAAE,CAAA;QAC7D,2CAA2C;QAC3C,IAAI,CAAC,GAAG,CAAC;YAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAA;QAClD,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC1D,CAAC;IAED,8DAA8D;IAC9D,uDAAuD;IACvD,uDAAuD;IACvD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,CAAA;AACtD,CAAC;AAED,0GAA0G;AAC1G,2GAA2G;AAC3G,kCAAkC;AAClC,MAAM,cAAc,GAAG,IAAI,OAAO,EAAwB,CAAA;AAE1D,MAAM,UAAU,iBAAiB,CAAC,GAAyB;IACzD,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;AACzB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,GAAyB;IACvD,OAAO,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;AAChC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAyB,EAAE,cAAwB;IACtF,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAA;IAC1C,OAAO,oBAAoB,CAAC;QAC1B,UAAU,EAAE,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI,EAAE;QAC1C,YAAY,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG;QAC/C,cAAc;QACd,SAAS,EAAE,eAAe,CAAC,GAAG,CAAC;KAChC,CAAC,CAAA;AACJ,CAAC"}
@@ -1,10 +1,19 @@
1
1
  export declare const AGENT_SCOPE = "agent";
2
+ export declare const VIEW_SCOPE = "view";
2
3
  export declare const WS_REJECT_REASON = "Unauthorized: agents need a PAT with 'agent' scope (--token / TAPFLOW_AGENT_TOKEN); browsers must sign in";
4
+ /** A PAT with neither `view` nor `agent`: it may upload builds, not open device sessions. */
5
+ export declare const WS_SCOPE_REASON = "Forbidden: this token lacks the 'view' scope needed for device sessions; create an API-type token";
6
+ /** An `agent` token is issued by an Admin and is only as good as that Admin role, checked at every use. */
7
+ export declare const WS_AGENT_OWNER_REASON = "Unauthorized: this agent token's owner is no longer an Admin; an Admin must issue a new agent token";
8
+ /** An open socket whose token was revoked, expired, or went with its owner. */
9
+ export declare const WS_TOKEN_GONE_REASON = "Unauthorized: this token was revoked or has expired, or its owner was removed";
3
10
  export interface ConnectionAuthInput {
4
11
  isLocal: boolean;
5
12
  hasCookieAuth: boolean;
6
13
  /** Scopes of a valid PAT on the upgrade request, or null when none was presented. */
7
14
  patScopes: string[] | null;
15
+ /** The PAT owner's current role (from the users table), or null when no PAT was presented. */
16
+ patOwnerRole: string | null;
8
17
  }
9
18
  export type ConnectionDecision = {
10
19
  action: 'reject';
@@ -13,10 +22,57 @@ export type ConnectionDecision = {
13
22
  action: 'accept';
14
23
  role: 'browser';
15
24
  }
16
- /** 첫 메시지(agent:register / stream:register)가 역할을 결정한다. */
25
+ /**
26
+ * 첫 메시지(agent:register / stream:register)가 역할을 결정한다. What the first message may make of
27
+ * the socket is decided here, with the credentials: `mayBrowse` gates the fallback to `browser`, and
28
+ * `mayRegisterAgent` gates the two handshakes. A local socket may do both.
29
+ */
17
30
  | {
18
31
  action: 'accept';
19
32
  role: 'first-message';
33
+ mayBrowse: boolean;
34
+ mayRegisterAgent: boolean;
20
35
  };
21
- export declare function classifyConnection({ isLocal, hasCookieAuth, patScopes }: ConnectionAuthInput): ConnectionDecision;
36
+ export declare function classifyConnection({ isLocal, hasCookieAuth, patScopes, patOwnerRole }: ConnectionAuthInput): ConnectionDecision;
37
+ /**
38
+ * The credential a remote socket was accepted with. Recorded once at the handshake and re-checked
39
+ * against the database for as long as the socket is open. Local sockets have none: they needed no
40
+ * credential, so there is nothing to revoke.
41
+ */
42
+ export type SocketPrincipal = {
43
+ via: 'cookie';
44
+ userId: number; /** JWT `exp`, seconds */
45
+ jwtExp?: number; /** JWT `pwv` */
46
+ pwv: string;
47
+ }
48
+ /** The PAT's id, never its hash: the hash is a bearer secret, the id is only a row. */
49
+ | {
50
+ via: 'pat';
51
+ userId: number;
52
+ patId: number;
53
+ };
54
+ /** What the database says now about a principal's credential. */
55
+ export interface CurrentCredential {
56
+ /** Cookie principals: the user's current password version, or null when the row is gone. */
57
+ userPasswordVersion: string | null;
58
+ /** PAT principals: the token row, unexpired, with its owner — or null when revoked, expired or cascaded. */
59
+ pat: {
60
+ scopes: string[];
61
+ ownerRole: string;
62
+ } | null;
63
+ }
64
+ /**
65
+ * Would this socket still be accepted, given what the database says now? Returns the close reason, or
66
+ * null when it would.
67
+ *
68
+ * **One predicate instead of one rule per event.** Removal, demotion, revocation, expiry and a
69
+ * password change all reach an open socket through the same question, asked on
70
+ * the heartbeat and again right after any of those writes. A list of "which sockets does this event
71
+ * affect" is the design this replaced, and it had already missed expiry and the invitation path.
72
+ *
73
+ * `role` is what the socket has become: `agent` and `stream` rest on the `agent` scope and its owner's
74
+ * Admin role; `browser` rests on `view`. A socket that has not sent its first frame yet (`undefined`)
75
+ * is re-classified as a fresh handshake would be.
76
+ */
77
+ export declare function revalidatePrincipal(principal: SocketPrincipal, role: 'agent' | 'browser' | 'stream' | undefined, current: CurrentCredential, nowSeconds: number): string | null;
22
78
  //# sourceMappingURL=connectionAuth.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"connectionAuth.d.ts","sourceRoot":"","sources":["../../src/lib/connectionAuth.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,UAAU,CAAA;AAGlC,eAAO,MAAM,gBAAgB,8GACgF,CAAA;AAE7G,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,OAAO,CAAA;IACtB,qFAAqF;IACrF,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;CAC3B;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE;AACvC,yDAAyD;GACvD;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,eAAe,CAAA;CAAE,CAAA;AAI/C,wBAAgB,kBAAkB,CAChC,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,EAAE,mBAAmB,GACzD,kBAAkB,CAOpB"}
1
+ {"version":3,"file":"connectionAuth.d.ts","sourceRoot":"","sources":["../../src/lib/connectionAuth.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,WAAW,UAAU,CAAA;AAClC,eAAO,MAAM,UAAU,SAAS,CAAA;AAIhC,eAAO,MAAM,gBAAgB,8GACgF,CAAA;AAC7G,6FAA6F;AAC7F,eAAO,MAAM,eAAe,sGACyE,CAAA;AACrG,2GAA2G;AAC3G,eAAO,MAAM,qBAAqB,wGACqE,CAAA;AACvG,+EAA+E;AAC/E,eAAO,MAAM,oBAAoB,kFACgD,CAAA;AAEjF,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,aAAa,EAAE,OAAO,CAAA;IACtB,qFAAqF;IACrF,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;IAC1B,8FAA8F;IAC9F,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,SAAS,CAAA;CAAE;AACvC;;;;GAIG;GACD;IAAE,MAAM,EAAE,QAAQ,CAAC;IAAC,IAAI,EAAE,eAAe,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,gBAAgB,EAAE,OAAO,CAAA;CAAE,CAAA;AAS9F,wBAAgB,kBAAkB,CAChC,EAAE,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,YAAY,EAAE,EAAE,mBAAmB,GACvE,kBAAkB,CAqBpB;AAED;;;;GAIG;AACH,MAAM,MAAM,eAAe,GACvB;IAAE,GAAG,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,CAAC,yBAAyB;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,gBAAgB;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE;AAC5G,uFAAuF;GACrF;IAAE,GAAG,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjD,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,4FAA4F;IAC5F,mBAAmB,EAAE,MAAM,GAAG,IAAI,CAAA;IAClC,4GAA4G;IAC5G,GAAG,EAAE;QAAE,MAAM,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CACpD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,eAAe,EAC1B,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,EAChD,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,MAAM,GACjB,MAAM,GAAG,IAAI,CAef"}
@@ -1,18 +1,78 @@
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 a
50
+ * password change 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
+ // A changed password ends the session the same way a removed member does (`passwordVersion`).
61
+ if (current.userPasswordVersion !== principal.pwv)
62
+ return WS_REJECT_REASON;
63
+ if (principal.jwtExp !== undefined && principal.jwtExp <= nowSeconds)
64
+ return WS_REJECT_REASON;
65
+ return null;
66
+ }
67
+ const pat = current.pat;
68
+ if (!pat)
69
+ return WS_TOKEN_GONE_REASON;
70
+ if (role === 'agent' || role === 'stream') {
71
+ return pat.scopes.includes(AGENT_SCOPE) && pat.ownerRole === ADMIN_ROLE ? null : WS_AGENT_OWNER_REASON;
72
+ }
73
+ if (role === 'browser')
74
+ return pat.scopes.includes(VIEW_SCOPE) ? null : WS_SCOPE_REASON;
75
+ const again = classifyConnection({ isLocal: false, hasCookieAuth: false, patScopes: pat.scopes, patOwnerRole: pat.ownerRole });
76
+ return again.action === 'reject' ? again.reason : null;
77
+ }
18
78
  //# 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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"}
@@ -0,0 +1,16 @@
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 declare function normalizeEmail(email: string): string;
12
+ /** `users.email` / `invitations.email` in normalized form, for comparing with `normalizeEmail(input)`. */
13
+ export declare const EMAIL_KEY_SQL = "lower(trim(email))";
14
+ /** The domain of the placeholder address a member invited without an email is given. Mail to it reaches nobody. */
15
+ export declare const SYNTHETIC_EMAIL_DOMAIN = "@tapflow.local";
16
+ //# sourceMappingURL=email.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../src/lib/email.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,0GAA0G;AAC1G,eAAO,MAAM,aAAa,uBAAuB,CAAA;AAEjD,mHAAmH;AACnH,eAAO,MAAM,sBAAsB,mBAAmB,CAAA"}