@tapflowio/relay 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/dist/RelayServer.d.ts +37 -0
  4. package/dist/RelayServer.d.ts.map +1 -0
  5. package/dist/RelayServer.js +531 -0
  6. package/dist/RelayServer.js.map +1 -0
  7. package/dist/SessionManager.d.ts +62 -0
  8. package/dist/SessionManager.d.ts.map +1 -0
  9. package/dist/SessionManager.js +174 -0
  10. package/dist/SessionManager.js.map +1 -0
  11. package/dist/api/agents.d.ts +4 -0
  12. package/dist/api/agents.d.ts.map +1 -0
  13. package/dist/api/agents.js +36 -0
  14. package/dist/api/agents.js.map +1 -0
  15. package/dist/api/apps.d.ts +6 -0
  16. package/dist/api/apps.d.ts.map +1 -0
  17. package/dist/api/apps.js +76 -0
  18. package/dist/api/apps.js.map +1 -0
  19. package/dist/api/auth.d.ts +9 -0
  20. package/dist/api/auth.d.ts.map +1 -0
  21. package/dist/api/auth.js +81 -0
  22. package/dist/api/auth.js.map +1 -0
  23. package/dist/api/builds.d.ts +14 -0
  24. package/dist/api/builds.d.ts.map +1 -0
  25. package/dist/api/builds.js +368 -0
  26. package/dist/api/builds.js.map +1 -0
  27. package/dist/api/comments.d.ts +5 -0
  28. package/dist/api/comments.d.ts.map +1 -0
  29. package/dist/api/comments.js +114 -0
  30. package/dist/api/comments.js.map +1 -0
  31. package/dist/api/invitations.d.ts +4 -0
  32. package/dist/api/invitations.d.ts.map +1 -0
  33. package/dist/api/invitations.js +95 -0
  34. package/dist/api/invitations.js.map +1 -0
  35. package/dist/api/passwordReset.d.ts +6 -0
  36. package/dist/api/passwordReset.d.ts.map +1 -0
  37. package/dist/api/passwordReset.js +64 -0
  38. package/dist/api/passwordReset.js.map +1 -0
  39. package/dist/api/profile.d.ts +3 -0
  40. package/dist/api/profile.d.ts.map +1 -0
  41. package/dist/api/profile.js +56 -0
  42. package/dist/api/profile.js.map +1 -0
  43. package/dist/api/recordings.d.ts +6 -0
  44. package/dist/api/recordings.d.ts.map +1 -0
  45. package/dist/api/recordings.js +167 -0
  46. package/dist/api/recordings.js.map +1 -0
  47. package/dist/api/settings.d.ts +4 -0
  48. package/dist/api/settings.d.ts.map +1 -0
  49. package/dist/api/settings.js +65 -0
  50. package/dist/api/settings.js.map +1 -0
  51. package/dist/api/team.d.ts +6 -0
  52. package/dist/api/team.d.ts.map +1 -0
  53. package/dist/api/team.js +65 -0
  54. package/dist/api/team.js.map +1 -0
  55. package/dist/api/tokens.d.ts +5 -0
  56. package/dist/api/tokens.d.ts.map +1 -0
  57. package/dist/api/tokens.js +39 -0
  58. package/dist/api/tokens.js.map +1 -0
  59. package/dist/db.d.ts +5 -0
  60. package/dist/db.d.ts.map +1 -0
  61. package/dist/db.js +43 -0
  62. package/dist/db.js.map +1 -0
  63. package/dist/index.d.ts +6 -0
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +4 -0
  66. package/dist/index.js.map +1 -0
  67. package/dist/lib/config.d.ts +34 -0
  68. package/dist/lib/config.d.ts.map +1 -0
  69. package/dist/lib/config.js +97 -0
  70. package/dist/lib/config.js.map +1 -0
  71. package/dist/lib/mailer.d.ts +2 -0
  72. package/dist/lib/mailer.d.ts.map +1 -0
  73. package/dist/lib/mailer.js +30 -0
  74. package/dist/lib/mailer.js.map +1 -0
  75. package/dist/middleware/auth.d.ts +17 -0
  76. package/dist/middleware/auth.d.ts.map +1 -0
  77. package/dist/middleware/auth.js +65 -0
  78. package/dist/middleware/auth.js.map +1 -0
  79. package/dist/migrations/001_initial.sql +55 -0
  80. package/dist/migrations/002_dashboard_foundation.sql +54 -0
  81. package/dist/migrations/003_recordings.sql +14 -0
  82. package/dist/migrations/004_app_build_split.sql +88 -0
  83. package/dist/migrations/005_user_avatar.sql +1 -0
  84. package/dist/migrations/006_password_reset.sql +7 -0
  85. package/dist/migrations/007_app_platform_model.sql +34 -0
  86. package/dist/migrations/008_recordings_build_id.sql +4 -0
  87. package/dist/migrations/009_agent_resources.sql +12 -0
  88. package/dist/router.d.ts +16 -0
  89. package/dist/router.d.ts.map +1 -0
  90. package/dist/router.js +53 -0
  91. package/dist/router.js.map +1 -0
  92. package/dist/server.d.ts +2 -0
  93. package/dist/server.d.ts.map +1 -0
  94. package/dist/server.js +17 -0
  95. package/dist/server.js.map +1 -0
  96. package/dist/types.d.ts +41 -0
  97. package/dist/types.d.ts.map +1 -0
  98. package/dist/types.js +2 -0
  99. package/dist/types.js.map +1 -0
  100. package/package.json +72 -0
  101. package/public/assets/index-CIXHMEdP.css +1 -0
  102. package/public/assets/index-CzYNt1iH.js +499 -0
  103. package/public/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  104. package/public/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  105. package/public/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  106. package/public/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  107. package/public/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  108. package/public/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  109. package/public/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  110. package/public/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  111. package/public/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  112. package/public/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  113. package/public/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  114. package/public/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  115. package/public/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  116. package/public/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  117. package/public/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  118. package/public/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  119. package/public/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  120. package/public/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  121. package/public/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  122. package/public/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  123. package/public/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  124. package/public/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  125. package/public/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  126. package/public/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  127. package/public/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  128. package/public/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  129. package/public/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  130. package/public/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  131. package/public/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  132. package/public/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  133. package/public/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  134. package/public/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  135. package/public/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  136. package/public/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  137. package/public/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  138. package/public/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  139. package/public/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  140. package/public/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  141. package/public/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  142. package/public/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  143. package/public/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  144. package/public/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  145. package/public/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  146. package/public/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  147. package/public/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  148. package/public/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  149. package/public/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  150. package/public/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  151. package/public/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  152. package/public/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  153. package/public/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  154. package/public/favicon.svg +5 -0
  155. package/public/index.html +14 -0
  156. package/public/logo-dark.svg +12 -0
  157. package/public/logo.svg +12 -0
@@ -0,0 +1,6 @@
1
+ import http from 'http';
2
+ export declare function sendPasswordResetEmail(userId: number, origin: string): Promise<boolean>;
3
+ export declare function handleVerifyReset(req: http.IncomingMessage, res: http.ServerResponse): void;
4
+ export declare function handleDoReset(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
5
+ export declare function handleSendMemberReset(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>): Promise<void>;
6
+ //# sourceMappingURL=passwordReset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passwordReset.d.ts","sourceRoot":"","sources":["../../src/api/passwordReset.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAQvB,wBAAsB,sBAAsB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAgB7F;AAED,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAY3F;AAED,wBAAsB,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBtG;AAED,wBAAsB,qBAAqB,CACzC,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"}
@@ -0,0 +1,64 @@
1
+ import crypto from 'crypto';
2
+ import { getDb } from '../db.js';
3
+ import { requireRole } from '../middleware/auth.js';
4
+ import { makePasswordHash } from './auth.js';
5
+ import { sendMail } from '../lib/mailer.js';
6
+ import { json, readJson } from '../router.js';
7
+ export async function sendPasswordResetEmail(userId, origin) {
8
+ const db = getDb();
9
+ const user = db.prepare('SELECT id, email FROM users WHERE id = ?').get(userId);
10
+ if (!user)
11
+ return false;
12
+ const token = crypto.randomBytes(32).toString('hex');
13
+ const expiresAt = new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString();
14
+ db.prepare('INSERT INTO password_reset_tokens (user_id, token, expires_at) VALUES (?, ?, ?)').run(userId, token, expiresAt);
15
+ const link = `${origin}/reset-password?token=${token}`;
16
+ const html = `<p>A password reset was requested for your tapflow account.</p>
17
+ <p><a href="${link}">Reset your password</a></p>
18
+ <p>This link expires in 2 hours. If you did not request this, ignore this email.</p>`;
19
+ return sendMail(user.email, 'Reset your tapflow password', html);
20
+ }
21
+ export function handleVerifyReset(req, res) {
22
+ const token = new URL(req.url ?? '/', 'http://x').searchParams.get('token');
23
+ if (!token)
24
+ return json(res, 400, { error: 'token required' });
25
+ const db = getDb();
26
+ const row = db.prepare(`
27
+ SELECT id FROM password_reset_tokens
28
+ WHERE token = ? AND used_at IS NULL AND expires_at > datetime('now')
29
+ `).get(token);
30
+ if (!row)
31
+ return json(res, 410, { error: 'Token expired or not found' });
32
+ json(res, 200, { ok: true });
33
+ }
34
+ export async function handleDoReset(req, res) {
35
+ const body = await readJson(req);
36
+ if (!body.token || !body.password)
37
+ return json(res, 400, { error: 'token and password required' });
38
+ if (body.password.length < 8)
39
+ return json(res, 400, { error: 'Password must be at least 8 characters' });
40
+ const db = getDb();
41
+ const row = db.prepare(`
42
+ SELECT id, user_id FROM password_reset_tokens
43
+ WHERE token = ? AND used_at IS NULL AND expires_at > datetime('now')
44
+ `).get(body.token);
45
+ if (!row)
46
+ return json(res, 410, { error: 'Token expired or not found' });
47
+ db.prepare('UPDATE users SET password_hash = ? WHERE id = ?').run(makePasswordHash(body.password), row.user_id);
48
+ db.prepare("UPDATE password_reset_tokens SET used_at = datetime('now') WHERE id = ?").run(row.id);
49
+ json(res, 200, { ok: true });
50
+ }
51
+ export async function handleSendMemberReset(req, res, params) {
52
+ const auth = requireRole(req, res, ['Admin']);
53
+ if (!auth)
54
+ return;
55
+ const db = getDb();
56
+ const member = db.prepare('SELECT id FROM users WHERE id = ?').get(params.id);
57
+ if (!member)
58
+ return json(res, 404, { error: 'Member not found' });
59
+ const proto = req.headers['x-forwarded-proto'] ?? 'http';
60
+ const origin = `${proto}://${req.headers.host}`;
61
+ const emailSent = await sendPasswordResetEmail(member.id, origin);
62
+ json(res, 200, { ok: true, emailSent });
63
+ }
64
+ //# sourceMappingURL=passwordReset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passwordReset.js","sourceRoot":"","sources":["../../src/api/passwordReset.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import http from 'http';
2
+ export declare function handleUpdateProfile(req: http.IncomingMessage, res: http.ServerResponse, uploadsDir: string): void;
3
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/api/profile.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAQvB,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,UAAU,EAAE,MAAM,GACjB,IAAI,CA8CN"}
@@ -0,0 +1,56 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import busboy from 'busboy';
4
+ import { getDb } from '../db.js';
5
+ import { requireAuth } from '../middleware/auth.js';
6
+ import { json } from '../router.js';
7
+ export function handleUpdateProfile(req, res, uploadsDir) {
8
+ const auth = requireAuth(req, res);
9
+ if (!auth)
10
+ return;
11
+ const bb = busboy({ headers: req.headers, limits: { fileSize: 2 * 1024 * 1024 } });
12
+ const fields = {};
13
+ let avatarPath = '';
14
+ let sizeError = false;
15
+ bb.on('field', (name, val) => { fields[name] = val; });
16
+ bb.on('file', (_field, stream, info) => {
17
+ const allowed = ['image/png', 'image/jpeg'];
18
+ if (!allowed.includes(info.mimeType)) {
19
+ stream.resume();
20
+ return;
21
+ }
22
+ const ext = info.mimeType === 'image/png' ? '.png' : '.jpg';
23
+ avatarPath = path.join(uploadsDir, 'avatars', `user-${auth.userId}${ext}`);
24
+ fs.mkdirSync(path.dirname(avatarPath), { recursive: true });
25
+ let size = 0;
26
+ stream.on('data', (chunk) => {
27
+ size += chunk.length;
28
+ if (size > 2 * 1024 * 1024)
29
+ sizeError = true;
30
+ });
31
+ stream.pipe(fs.createWriteStream(avatarPath));
32
+ });
33
+ bb.on('finish', () => {
34
+ if (sizeError)
35
+ return json(res, 400, { error: 'Max 2MB for avatar' });
36
+ const db = getDb();
37
+ const updates = [];
38
+ const params = [];
39
+ if (fields.display_name !== undefined) {
40
+ updates.push('display_name = ?');
41
+ params.push(fields.display_name || null);
42
+ }
43
+ if (avatarPath) {
44
+ updates.push('avatar_url = ?');
45
+ params.push(`/uploads/avatars/${path.basename(avatarPath)}`);
46
+ }
47
+ if (updates.length === 0)
48
+ return json(res, 400, { error: 'Nothing to update' });
49
+ params.push(auth.userId);
50
+ db.prepare(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`).run(...params);
51
+ json(res, 200, { ok: true });
52
+ });
53
+ bb.on('error', () => json(res, 500, { error: 'Update failed' }));
54
+ req.pipe(bb);
55
+ }
56
+ //# sourceMappingURL=profile.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.js","sourceRoot":"","sources":["../../src/api/profile.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import http from 'http';
2
+ export declare function handleUploadRecording(req: http.IncomingMessage, res: http.ServerResponse, recordingsDir: string): void;
3
+ export declare function handleListRecordings(req: http.IncomingMessage, res: http.ServerResponse): void;
4
+ export declare function handleDownloadRecording(req: http.IncomingMessage, res: http.ServerResponse, recordingsDir: string): void;
5
+ export declare function purgeExpiredRecordings(recordingsDir: string): void;
6
+ //# sourceMappingURL=recordings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recordings.d.ts","sourceRoot":"","sources":["../../src/api/recordings.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAcvB,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,aAAa,EAAE,MAAM,GACpB,IAAI,CA6CN;AAED,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,GACvB,IAAI,CA0CN;AAED,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,aAAa,EAAE,MAAM,GACpB,IAAI,CAmCN;AAED,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,MAAM,GAAG,IAAI,CAgClE"}
@@ -0,0 +1,167 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { randomUUID } from 'crypto';
4
+ import busboy from 'busboy';
5
+ import { requireAuth, getAuth } from '../middleware/auth.js';
6
+ import { getDb } from '../db.js';
7
+ const TTL_MS = 72 * 60 * 60 * 1000;
8
+ function expiresAt() {
9
+ return new Date(Date.now() + TTL_MS).toISOString().replace('T', ' ').slice(0, 19);
10
+ }
11
+ export function handleUploadRecording(req, res, recordingsDir) {
12
+ const auth = requireAuth(req, res);
13
+ if (!auth)
14
+ return;
15
+ const u = new URL(req.url ?? '/', 'http://x');
16
+ const sessionId = u.searchParams.get('sessionId') ?? null;
17
+ const buildId = u.searchParams.get('buildId') ? Number(u.searchParams.get('buildId')) : null;
18
+ fs.mkdirSync(recordingsDir, { recursive: true });
19
+ let filePath = '';
20
+ let filename = '';
21
+ let mime = 'video/webm';
22
+ let writeDone = null;
23
+ const bb = busboy({ headers: req.headers });
24
+ bb.on('file', (_field, fileStream, info) => {
25
+ const ext = path.extname(info.filename || '.webm') || '.webm';
26
+ mime = info.mimeType || mime;
27
+ filename = `${randomUUID()}-${Date.now()}${ext}`;
28
+ filePath = path.join(recordingsDir, filename);
29
+ const ws = fs.createWriteStream(filePath);
30
+ fileStream.pipe(ws);
31
+ writeDone = new Promise((resolve, reject) => {
32
+ ws.on('finish', resolve);
33
+ ws.on('error', reject);
34
+ });
35
+ });
36
+ bb.on('finish', () => {
37
+ if (!writeDone) {
38
+ res.writeHead(400);
39
+ res.end('No file');
40
+ return;
41
+ }
42
+ writeDone
43
+ .then(() => {
44
+ const stat = fs.statSync(filePath);
45
+ const db = getDb();
46
+ db.prepare(`
47
+ INSERT INTO recordings (filename, session_id, build_id, uploader_id, file_size, mime, expires_at)
48
+ VALUES (?, ?, ?, ?, ?, ?, ?)
49
+ `).run(filename, sessionId, buildId, auth.userId, stat.size, mime, expiresAt());
50
+ res.writeHead(200, { 'Content-Type': 'application/json' });
51
+ res.end(JSON.stringify({ url: `/api/v1/recordings/${filename}` }));
52
+ })
53
+ .catch(() => { res.writeHead(500); res.end('Write failed'); });
54
+ });
55
+ bb.on('error', () => { res.writeHead(500); res.end('Upload failed'); });
56
+ req.pipe(bb);
57
+ }
58
+ export function handleListRecordings(req, res) {
59
+ const auth = requireAuth(req, res);
60
+ if (!auth)
61
+ return;
62
+ const u = new URL(req.url ?? '/', 'http://x');
63
+ const buildId = u.searchParams.get('buildId');
64
+ const db = getDb();
65
+ const rows = buildId
66
+ ? db.prepare(`
67
+ SELECT id, filename, session_id, file_size, mime, created_at, expires_at
68
+ FROM recordings
69
+ WHERE expires_at > datetime('now') AND build_id = ?
70
+ ORDER BY created_at DESC
71
+ `).all(Number(buildId))
72
+ : db.prepare(`
73
+ SELECT id, filename, session_id, file_size, mime, created_at, expires_at
74
+ FROM recordings
75
+ WHERE expires_at > datetime('now')
76
+ ORDER BY created_at DESC
77
+ `).all();
78
+ const result = rows.map((r) => ({
79
+ id: r.id,
80
+ url: `/api/v1/recordings/${r.filename}`,
81
+ sessionId: r.session_id,
82
+ fileSize: r.file_size,
83
+ mime: r.mime,
84
+ createdAt: r.created_at,
85
+ expiresAt: r.expires_at,
86
+ }));
87
+ res.writeHead(200, { 'Content-Type': 'application/json' });
88
+ res.end(JSON.stringify(result));
89
+ }
90
+ export function handleDownloadRecording(req, res, recordingsDir) {
91
+ const auth = getAuth(req);
92
+ if (!auth) {
93
+ res.writeHead(401);
94
+ res.end('Unauthorized');
95
+ return;
96
+ }
97
+ const filename = (req.url ?? '/').split('/').pop() ?? '';
98
+ if (!filename || filename.includes('..') || filename.includes('/')) {
99
+ res.writeHead(400);
100
+ res.end('Invalid filename');
101
+ return;
102
+ }
103
+ const db = getDb();
104
+ const row = db.prepare(`
105
+ SELECT expires_at, mime FROM recordings WHERE filename = ?
106
+ `).get(filename);
107
+ if (!row) {
108
+ res.writeHead(404);
109
+ res.end('Not found');
110
+ return;
111
+ }
112
+ if (new Date(row.expires_at).getTime() < Date.now()) {
113
+ db.prepare(`DELETE FROM recordings WHERE filename = ?`).run(filename);
114
+ fs.unlink(path.join(recordingsDir, filename), () => { });
115
+ res.writeHead(404);
116
+ res.end('Expired');
117
+ return;
118
+ }
119
+ const filePath = path.join(recordingsDir, filename);
120
+ if (!fs.existsSync(filePath)) {
121
+ db.prepare(`DELETE FROM recordings WHERE filename = ?`).run(filename);
122
+ res.writeHead(404);
123
+ res.end('Not found');
124
+ return;
125
+ }
126
+ const stat = fs.statSync(filePath);
127
+ res.writeHead(200, {
128
+ 'Content-Type': row.mime,
129
+ 'Content-Length': stat.size,
130
+ 'Content-Disposition': `attachment; filename="${filename}"`,
131
+ });
132
+ fs.createReadStream(filePath).pipe(res);
133
+ }
134
+ export function purgeExpiredRecordings(recordingsDir) {
135
+ if (!fs.existsSync(recordingsDir))
136
+ return;
137
+ try {
138
+ const db = getDb();
139
+ const expired = db.prepare(`
140
+ SELECT filename FROM recordings
141
+ WHERE expires_at < datetime('now')
142
+ `).all();
143
+ for (const { filename } of expired) {
144
+ fs.unlink(path.join(recordingsDir, filename), () => { });
145
+ }
146
+ if (expired.length > 0) {
147
+ const placeholders = expired.map(() => '?').join(',');
148
+ db.prepare(`DELETE FROM recordings WHERE filename IN (${placeholders})`).run(...expired.map((r) => r.filename));
149
+ }
150
+ // orphan 파일 정리 (DB row 없는 파일)
151
+ for (const file of fs.readdirSync(recordingsDir)) {
152
+ const row = db.prepare(`SELECT id FROM recordings WHERE filename = ?`).get(file);
153
+ if (!row)
154
+ fs.unlink(path.join(recordingsDir, file), () => { });
155
+ }
156
+ }
157
+ catch {
158
+ // DB 미초기화 시 (테스트 등) — 파일명 기반 TTL fallback
159
+ for (const file of fs.readdirSync(recordingsDir)) {
160
+ const match = file.match(/-(\d+)\.[^.]+$/);
161
+ if (match && Date.now() - Number(match[1]) > TTL_MS) {
162
+ fs.unlink(path.join(recordingsDir, file), () => { });
163
+ }
164
+ }
165
+ }
166
+ }
167
+ //# sourceMappingURL=recordings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recordings.js","sourceRoot":"","sources":["../../src/api/recordings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import http from 'http';
2
+ export declare function handleGetSettings(req: http.IncomingMessage, res: http.ServerResponse): void;
3
+ export declare function handleUpdateSettings(req: http.IncomingMessage, res: http.ServerResponse, uploadsDir: string): void;
4
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/api/settings.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAQvB,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,IAAI,CAY3F;AAED,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,IAAI,CAAC,eAAe,EACzB,GAAG,EAAE,IAAI,CAAC,cAAc,EACxB,UAAU,EAAE,MAAM,GACjB,IAAI,CA4CN"}
@@ -0,0 +1,65 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import busboy from 'busboy';
4
+ import { getDb } from '../db.js';
5
+ import { requireRole, requireAuth } from '../middleware/auth.js';
6
+ import { json } from '../router.js';
7
+ export function handleGetSettings(req, res) {
8
+ const auth = requireAuth(req, res);
9
+ if (!auth)
10
+ return;
11
+ const db = getDb();
12
+ const settings = db.prepare('SELECT team_name, logo_path FROM team_settings WHERE id = 1').get();
13
+ json(res, 200, {
14
+ team_name: settings?.team_name ?? 'tapflow',
15
+ logo_url: settings?.logo_path ? `/uploads/team/${path.basename(settings.logo_path)}` : null,
16
+ });
17
+ }
18
+ export function handleUpdateSettings(req, res, uploadsDir) {
19
+ const auth = requireRole(req, res, ['Admin']);
20
+ if (!auth)
21
+ return;
22
+ const bb = busboy({ headers: req.headers, limits: { fileSize: 2 * 1024 * 1024 } });
23
+ const fields = {};
24
+ let logoPath = '';
25
+ let sizeError = false;
26
+ bb.on('field', (name, val) => { fields[name] = val; });
27
+ bb.on('file', (_field, stream, info) => {
28
+ const allowed = ['image/png', 'image/jpeg'];
29
+ if (!allowed.includes(info.mimeType)) {
30
+ stream.resume();
31
+ return;
32
+ }
33
+ const ext = info.mimeType === 'image/png' ? '.png' : '.jpg';
34
+ logoPath = path.join(uploadsDir, 'team', `logo${ext}`);
35
+ fs.mkdirSync(path.dirname(logoPath), { recursive: true });
36
+ let size = 0;
37
+ stream.on('data', (chunk) => {
38
+ size += chunk.length;
39
+ if (size > 2 * 1024 * 1024)
40
+ sizeError = true;
41
+ });
42
+ stream.pipe(fs.createWriteStream(logoPath));
43
+ });
44
+ bb.on('finish', () => {
45
+ if (sizeError)
46
+ return json(res, 400, { error: 'Max 2MB for logo' });
47
+ const db = getDb();
48
+ const updates = ['updated_at = datetime(\'now\')'];
49
+ const params = [];
50
+ if (fields.team_name) {
51
+ updates.push('team_name = ?');
52
+ params.push(fields.team_name);
53
+ }
54
+ if (logoPath) {
55
+ updates.push('logo_path = ?');
56
+ params.push(logoPath);
57
+ }
58
+ params.push(1);
59
+ db.prepare(`UPDATE team_settings SET ${updates.join(', ')} WHERE id = ?`).run(...params);
60
+ json(res, 200, { ok: true });
61
+ });
62
+ bb.on('error', () => json(res, 500, { error: 'Update failed' }));
63
+ req.pipe(bb);
64
+ }
65
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/api/settings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import http from 'http';
2
+ export declare function handleListMembers(req: http.IncomingMessage, res: http.ServerResponse): void;
3
+ export declare function handleInvite(req: http.IncomingMessage, res: http.ServerResponse): Promise<void>;
4
+ export declare function handleUpdateMember(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>): Promise<void>;
5
+ export declare function handleDeleteMember(req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>): void;
6
+ //# sourceMappingURL=team.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team.d.ts","sourceRoot":"","sources":["../../src/api/team.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAOvB,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,GAAG,OAAO,CAAC,IAAI,CAAC,CA2BrG;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"}
@@ -0,0 +1,65 @@
1
+ import crypto from 'crypto';
2
+ import { getDb } from '../db.js';
3
+ import { requireRole } from '../middleware/auth.js';
4
+ import { json, readJson } from '../router.js';
5
+ import { sendMail } from '../lib/mailer.js';
6
+ export function handleListMembers(req, res) {
7
+ const auth = requireRole(req, res, ['Admin']);
8
+ if (!auth)
9
+ return;
10
+ const db = getDb();
11
+ const members = db.prepare('SELECT id, email, display_name, role, joined_at FROM users ORDER BY joined_at ASC').all();
12
+ json(res, 200, members);
13
+ }
14
+ export async function handleInvite(req, res) {
15
+ const auth = requireRole(req, res, ['Admin']);
16
+ if (!auth)
17
+ return;
18
+ const body = await readJson(req);
19
+ const role = ['Admin', 'Developer', 'QA', 'Viewer'].includes(body.role ?? '')
20
+ ? body.role
21
+ : 'QA';
22
+ const token = crypto.randomBytes(32).toString('hex');
23
+ const expiresAt = new Date(Date.now() + 7 * 24 * 3600 * 1000).toISOString();
24
+ const db = getDb();
25
+ db.prepare('INSERT INTO invitations (token, email, role, expires_at) VALUES (?, ?, ?, ?)')
26
+ .run(token, body.email ?? null, role, expiresAt);
27
+ let emailSent = false;
28
+ if (body.email) {
29
+ const proto = req.headers['x-forwarded-proto'] ?? 'http';
30
+ const inviteUrl = `${proto}://${req.headers.host}/invite?token=${token}`;
31
+ const html = `<p>You've been invited to join tapflow as <strong>${role}</strong>.</p>
32
+ <p><a href="${inviteUrl}">Accept invitation</a></p>
33
+ <p>This link expires in 7 days.</p>`;
34
+ emailSent = await sendMail(body.email, 'You have been invited to tapflow', html);
35
+ }
36
+ json(res, 201, { token, emailSent });
37
+ }
38
+ export async function handleUpdateMember(req, res, params) {
39
+ const auth = requireRole(req, res, ['Admin']);
40
+ if (!auth)
41
+ return;
42
+ const body = await readJson(req);
43
+ if (!body.role || !['Admin', 'Developer', 'QA', 'Viewer'].includes(body.role)) {
44
+ return json(res, 400, { error: 'Valid role required' });
45
+ }
46
+ const db = getDb();
47
+ const result = db.prepare('UPDATE users SET role = ? WHERE id = ?').run(body.role, params.id);
48
+ if (result.changes === 0)
49
+ return json(res, 404, { error: 'Member not found' });
50
+ json(res, 200, { ok: true });
51
+ }
52
+ export function handleDeleteMember(req, res, params) {
53
+ const auth = requireRole(req, res, ['Admin']);
54
+ if (!auth)
55
+ return;
56
+ if (String(auth.userId) === params.id) {
57
+ return json(res, 400, { error: 'Cannot remove yourself' });
58
+ }
59
+ const db = getDb();
60
+ const result = db.prepare('DELETE FROM users WHERE id = ?').run(params.id);
61
+ if (result.changes === 0)
62
+ return json(res, 404, { error: 'Member not found' });
63
+ json(res, 204, null);
64
+ }
65
+ //# sourceMappingURL=team.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"team.js","sourceRoot":"","sources":["../../src/api/team.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import http from 'http';
2
+ export declare function handleListTokens(req: http.IncomingMessage, res: http.ServerResponse): void;
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;
5
+ //# sourceMappingURL=tokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../src/api/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAMvB,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,CAmB1G;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"}
@@ -0,0 +1,39 @@
1
+ import crypto from 'crypto';
2
+ import { getDb } from '../db.js';
3
+ import { requireAuth, hashPat } from '../middleware/auth.js';
4
+ import { json, readJson } from '../router.js';
5
+ export function handleListTokens(req, res) {
6
+ const auth = requireAuth(req, res);
7
+ if (!auth)
8
+ return;
9
+ const db = getDb();
10
+ const tokens = db.prepare('SELECT id, name, scope, last_used_at, expires_at, created_at FROM personal_access_tokens WHERE user_id = ? ORDER BY created_at DESC').all(auth.userId);
11
+ json(res, 200, tokens);
12
+ }
13
+ export async function handleCreateToken(req, res) {
14
+ const auth = requireAuth(req, res);
15
+ if (!auth)
16
+ return;
17
+ const body = await readJson(req);
18
+ if (!body.name?.trim())
19
+ return json(res, 400, { error: 'name required' });
20
+ const rawToken = `tflw_pat_${crypto.randomBytes(32).toString('hex')}`;
21
+ const tokenHash = hashPat(rawToken);
22
+ const expiresAt = body.expires_in_days
23
+ ? new Date(Date.now() + body.expires_in_days * 24 * 3600 * 1000).toISOString()
24
+ : null;
25
+ const db = getDb();
26
+ db.prepare('INSERT INTO personal_access_tokens (user_id, name, token_hash, scope, expires_at) VALUES (?, ?, ?, ?, ?)').run(auth.userId, body.name.trim(), tokenHash, 'builds:write', expiresAt);
27
+ json(res, 201, { token: rawToken });
28
+ }
29
+ export function handleRevokeToken(req, res, params) {
30
+ const auth = requireAuth(req, res);
31
+ if (!auth)
32
+ return;
33
+ const db = getDb();
34
+ const result = db.prepare('DELETE FROM personal_access_tokens WHERE id = ? AND user_id = ?').run(params.id, auth.userId);
35
+ if (result.changes === 0)
36
+ return json(res, 404, { error: 'Token not found' });
37
+ json(res, 204, null);
38
+ }
39
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/api/tokens.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,QAAQ,CAAA;AAC3B,OAAO,EAAE,KAAK,EAAE,MAAM,UAAU,CAAA;AAChC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAC5D,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAE7C,MAAM,UAAU,gBAAgB,CAAC,GAAyB,EAAE,GAAwB;IAClF,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAClC,IAAI,CAAC,IAAI;QAAE,OAAM;IAEjB,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAClB,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CACvB,qIAAqI,CACtI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAClB,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAA;AACxB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,GAAyB,EAAE,GAAwB;IACzF,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAClC,IAAI,CAAC,IAAI;QAAE,OAAM;IAEjB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAA8C,GAAG,CAAC,CAAA;IAC7E,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAA;IAEzE,MAAM,QAAQ,GAAG,YAAY,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAA;IACrE,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IACnC,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe;QACpC,CAAC,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,eAAe,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,WAAW,EAAE;QAC9E,CAAC,CAAC,IAAI,CAAA;IAER,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAClB,EAAE,CAAC,OAAO,CACR,0GAA0G,CAC3G,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,cAAc,EAAE,SAAS,CAAC,CAAA;IAE1E,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAA;AACrC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,GAAyB,EACzB,GAAwB,EACxB,MAA8B;IAE9B,MAAM,IAAI,GAAG,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IAClC,IAAI,CAAC,IAAI;QAAE,OAAM;IAEjB,MAAM,EAAE,GAAG,KAAK,EAAE,CAAA;IAClB,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CACvB,iEAAiE,CAClE,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAA;IAE7B,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE,KAAK,EAAE,iBAAiB,EAAE,CAAC,CAAA;IAC7E,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAA;AACtB,CAAC"}
package/dist/db.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import Database from 'better-sqlite3';
2
+ export declare function getDb(): Database.Database;
3
+ export declare function closeDb(): void;
4
+ export declare function initDb(dbPath: string): void;
5
+ //# sourceMappingURL=db.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AAMrC,wBAAgB,KAAK,IAAI,QAAQ,CAAC,QAAQ,CAGzC;AAED,wBAAgB,OAAO,IAAI,IAAI,CAG9B;AAED,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAM3C"}
package/dist/db.js ADDED
@@ -0,0 +1,43 @@
1
+ import Database from 'better-sqlite3';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ let db = null;
5
+ export function getDb() {
6
+ if (!db)
7
+ throw new Error('DB not initialized — call initDb() first');
8
+ return db;
9
+ }
10
+ export function closeDb() {
11
+ db?.close();
12
+ db = null;
13
+ }
14
+ export function initDb(dbPath) {
15
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
16
+ db = new Database(dbPath);
17
+ db.pragma('journal_mode = WAL');
18
+ db.pragma('foreign_keys = ON');
19
+ runMigrations();
20
+ }
21
+ function runMigrations() {
22
+ const d = getDb();
23
+ d.exec(`
24
+ CREATE TABLE IF NOT EXISTS _migrations (
25
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
26
+ name TEXT NOT NULL UNIQUE,
27
+ run_at TEXT NOT NULL DEFAULT (datetime('now'))
28
+ )
29
+ `);
30
+ const migrationsDir = path.join(import.meta.dirname, 'migrations');
31
+ const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith('.sql')).sort();
32
+ const ran = new Set(d.prepare('SELECT name FROM _migrations').all().map((r) => r.name));
33
+ for (const file of files) {
34
+ if (ran.has(file))
35
+ continue;
36
+ const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf-8');
37
+ d.transaction(() => {
38
+ d.exec(sql);
39
+ d.prepare('INSERT INTO _migrations (name) VALUES (?)').run(file);
40
+ })();
41
+ }
42
+ }
43
+ //# sourceMappingURL=db.js.map