@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
package/dist/db.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAA;AACrC,OAAO,EAAE,MAAM,IAAI,CAAA;AACnB,OAAO,IAAI,MAAM,MAAM,CAAA;AAEvB,IAAI,EAAE,GAA6B,IAAI,CAAA;AAEvC,MAAM,UAAU,KAAK;IACnB,IAAI,CAAC,EAAE;QAAE,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAA;IACpE,OAAO,EAAE,CAAA;AACX,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,EAAE,EAAE,KAAK,EAAE,CAAA;IACX,EAAE,GAAG,IAAI,CAAA;AACX,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,MAAc;IACnC,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACvD,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAA;IACzB,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAA;IAC/B,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAA;IAC9B,aAAa,EAAE,CAAA;AACjB,CAAC;AAED,SAAS,aAAa;IACpB,MAAM,CAAC,GAAG,KAAK,EAAE,CAAA;IACjB,CAAC,CAAC,IAAI,CAAC;;;;;;GAMN,CAAC,CAAA;IAEF,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAA;IAClE,MAAM,KAAK,GAAG,EAAE,CAAC,WAAW,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAA;IAEpF,MAAM,GAAG,GAAG,IAAI,GAAG,CAChB,CAAC,CAAC,OAAO,CAAC,8BAA8B,CAAC,CAAC,GAAG,EAAyB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAC3F,CAAA;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAQ;QAC3B,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAA;QACpE,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE;YACjB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YACX,CAAC,CAAC,OAAO,CAAC,2CAA2C,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QAClE,CAAC,CAAC,EAAE,CAAA;IACN,CAAC;AACH,CAAC"}
@@ -0,0 +1,6 @@
1
+ export { RelayServer } from './RelayServer.js';
2
+ export { SessionManager } from './SessionManager.js';
3
+ export { initDb, getDb, closeDb } from './db.js';
4
+ export type { Session } from './SessionManager.js';
5
+ export type { RelayMessage, MessageType } from './types.js';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AAChD,YAAY,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { RelayServer } from './RelayServer.js';
2
+ export { SessionManager } from './SessionManager.js';
3
+ export { initDb, getDb, closeDb } from './db.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA"}
@@ -0,0 +1,34 @@
1
+ import { z } from 'zod';
2
+ declare const configSchema: z.ZodObject<{
3
+ server: z.ZodObject<{
4
+ port: z.ZodNumber;
5
+ dataDir: z.ZodString;
6
+ jwtSecret: z.ZodString;
7
+ }, z.core.$strip>;
8
+ smtp: z.ZodObject<{
9
+ host: z.ZodString;
10
+ port: z.ZodNumber;
11
+ secure: z.ZodBoolean;
12
+ user: z.ZodString;
13
+ pass: z.ZodString;
14
+ from: z.ZodString;
15
+ }, z.core.$strip>;
16
+ }, z.core.$strip>;
17
+ export type TapflowConfig = z.infer<typeof configSchema>;
18
+ export declare const config: {
19
+ server: {
20
+ port: number;
21
+ dataDir: string;
22
+ jwtSecret: string;
23
+ };
24
+ smtp: {
25
+ host: string;
26
+ port: number;
27
+ secure: boolean;
28
+ user: string;
29
+ pass: string;
30
+ from: string;
31
+ };
32
+ };
33
+ export {};
34
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAKvB,QAAA,MAAM,YAAY;;;;;;;;;;;;;;iBAchB,CAAA;AAEF,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AA8ExD,eAAO,MAAM,MAAM;;;;;;;;;;;;;;CAAS,CAAA"}
@@ -0,0 +1,97 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { z } from 'zod';
4
+ import { createLogger } from '@tapflowio/agent-core';
5
+ const logger = createLogger('relay:config');
6
+ const configSchema = z.object({
7
+ server: z.object({
8
+ port: z.number().int().min(1).max(65535),
9
+ dataDir: z.string().min(1),
10
+ jwtSecret: z.string().min(32, 'jwtSecret must be at least 32 characters'),
11
+ }),
12
+ smtp: z.object({
13
+ host: z.string(),
14
+ port: z.number().int().min(1).max(65535),
15
+ secure: z.boolean(),
16
+ user: z.string(),
17
+ pass: z.string(),
18
+ from: z.string(),
19
+ }),
20
+ });
21
+ const DEFAULTS = {
22
+ server: {
23
+ port: 4000,
24
+ dataDir: '.tapflow',
25
+ jwtSecret: 'tapflow-dev-secret-change-in-production',
26
+ },
27
+ smtp: {
28
+ host: '',
29
+ port: 587,
30
+ secure: false,
31
+ user: '',
32
+ pass: '',
33
+ from: 'tapflow <noreply@tapflow.local>',
34
+ },
35
+ };
36
+ function resolveDataDir(raw) {
37
+ return path.isAbsolute(raw) ? raw : path.join(process.cwd(), raw);
38
+ }
39
+ function load() {
40
+ let file = {};
41
+ const configPath = path.join(process.cwd(), 'tapflow.config.json');
42
+ if (fs.existsSync(configPath)) {
43
+ try {
44
+ file = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
45
+ }
46
+ catch {
47
+ logger.warn('Failed to parse tapflow.config.json — using defaults');
48
+ }
49
+ }
50
+ const cfg = {
51
+ server: {
52
+ port: file.server?.port ?? DEFAULTS.server.port,
53
+ dataDir: resolveDataDir(file.server?.dataDir ?? DEFAULTS.server.dataDir),
54
+ jwtSecret: file.server?.jwtSecret ?? DEFAULTS.server.jwtSecret,
55
+ },
56
+ smtp: {
57
+ host: file.smtp?.host ?? DEFAULTS.smtp.host,
58
+ port: file.smtp?.port ?? DEFAULTS.smtp.port,
59
+ secure: file.smtp?.secure ?? DEFAULTS.smtp.secure,
60
+ user: file.smtp?.user ?? DEFAULTS.smtp.user,
61
+ pass: file.smtp?.pass ?? DEFAULTS.smtp.pass,
62
+ from: file.smtp?.from ?? DEFAULTS.smtp.from,
63
+ },
64
+ };
65
+ // env vars override config file (useful for Docker / CI)
66
+ if (process.env.TAPFLOW_PORT)
67
+ cfg.server.port = Number(process.env.TAPFLOW_PORT);
68
+ if (process.env.TAPFLOW_DATA_DIR)
69
+ cfg.server.dataDir = resolveDataDir(process.env.TAPFLOW_DATA_DIR);
70
+ if (process.env.JWT_SECRET)
71
+ cfg.server.jwtSecret = process.env.JWT_SECRET;
72
+ if (process.env.SMTP_HOST)
73
+ cfg.smtp.host = process.env.SMTP_HOST;
74
+ if (process.env.SMTP_PORT)
75
+ cfg.smtp.port = Number(process.env.SMTP_PORT);
76
+ if (process.env.SMTP_SECURE)
77
+ cfg.smtp.secure = process.env.SMTP_SECURE === 'true';
78
+ if (process.env.SMTP_USER)
79
+ cfg.smtp.user = process.env.SMTP_USER;
80
+ if (process.env.SMTP_PASS)
81
+ cfg.smtp.pass = process.env.SMTP_PASS;
82
+ if (process.env.SMTP_FROM)
83
+ cfg.smtp.from = process.env.SMTP_FROM;
84
+ const result = configSchema.safeParse(cfg);
85
+ if (!result.success) {
86
+ for (const issue of result.error.issues) {
87
+ logger.error(`config error: ${issue.path.join('.')} — ${issue.message}`);
88
+ }
89
+ process.exit(1);
90
+ }
91
+ if (cfg.server.jwtSecret === DEFAULTS.server.jwtSecret) {
92
+ logger.warn('JWT_SECRET is using the dev default — set a strong secret in production');
93
+ }
94
+ return result.data;
95
+ }
96
+ export const config = load();
97
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function sendMail(to: string, subject: string, html: string): Promise<boolean>;
2
+ //# sourceMappingURL=mailer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mailer.d.ts","sourceRoot":"","sources":["../../src/lib/mailer.ts"],"names":[],"mappings":"AAkBA,wBAAsB,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAU1F"}
@@ -0,0 +1,30 @@
1
+ import nodemailer from 'nodemailer';
2
+ import { config } from './config.js';
3
+ import { createLogger } from '@tapflowio/agent-core';
4
+ const logger = createLogger('relay:mailer');
5
+ function createTransport() {
6
+ if (!config.smtp.host)
7
+ return null;
8
+ return nodemailer.createTransport({
9
+ host: config.smtp.host,
10
+ port: config.smtp.port,
11
+ secure: config.smtp.secure,
12
+ auth: config.smtp.user
13
+ ? { user: config.smtp.user, pass: config.smtp.pass }
14
+ : undefined,
15
+ });
16
+ }
17
+ export async function sendMail(to, subject, html) {
18
+ const transport = createTransport();
19
+ if (!transport)
20
+ return false;
21
+ try {
22
+ await transport.sendMail({ from: config.smtp.from, to, subject, html });
23
+ return true;
24
+ }
25
+ catch (err) {
26
+ logger.error('send failed', err);
27
+ return false;
28
+ }
29
+ }
30
+ //# sourceMappingURL=mailer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mailer.js","sourceRoot":"","sources":["../../src/lib/mailer.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,YAAY,CAAA;AACnC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAA;AAEpD,MAAM,MAAM,GAAG,YAAY,CAAC,cAAc,CAAC,CAAA;AAE3C,SAAS,eAAe;IACtB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IAClC,OAAO,UAAU,CAAC,eAAe,CAAC;QAChC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI;QACtB,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI;QACtB,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM;QAC1B,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI;YACpB,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE;YACpD,CAAC,CAAC,SAAS;KACd,CAAC,CAAA;AACJ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,EAAU,EAAE,OAAe,EAAE,IAAY;IACtE,MAAM,SAAS,GAAG,eAAe,EAAE,CAAA;IACnC,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAA;IAC5B,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;QACvE,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,GAAG,CAAC,CAAA;QAChC,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ import http from 'http';
2
+ export interface AuthContext {
3
+ userId: number;
4
+ email: string;
5
+ role: string;
6
+ }
7
+ export declare function signJwt(payload: AuthContext): string;
8
+ export declare function verifyJwt(token: string): AuthContext | null;
9
+ export declare function getAuth(req: http.IncomingMessage): AuthContext | null;
10
+ export declare function requireAuth(req: http.IncomingMessage, res: http.ServerResponse): AuthContext | null;
11
+ export declare function requireRole(req: http.IncomingMessage, res: http.ServerResponse, roles: string[]): AuthContext | null;
12
+ export declare function verifyPat(req: http.IncomingMessage): {
13
+ userId: number;
14
+ scope: string;
15
+ } | null;
16
+ export declare function hashPat(token: string): string;
17
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,MAAM,CAAA;AAMvB,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,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"}
@@ -0,0 +1,65 @@
1
+ import crypto from 'crypto';
2
+ import jwt from 'jsonwebtoken';
3
+ import { getDb } from '../db.js';
4
+ import { config } from '../lib/config.js';
5
+ const JWT_EXPIRES = '7d';
6
+ export function signJwt(payload) {
7
+ return jwt.sign(payload, config.server.jwtSecret, { expiresIn: JWT_EXPIRES });
8
+ }
9
+ export function verifyJwt(token) {
10
+ try {
11
+ return jwt.verify(token, config.server.jwtSecret);
12
+ }
13
+ catch {
14
+ return null;
15
+ }
16
+ }
17
+ export function getAuth(req) {
18
+ const cookie = req.headers.cookie ?? '';
19
+ const match = /(?:^|;\s*)tapflow_token=([^;]+)/.exec(cookie);
20
+ if (!match)
21
+ return null;
22
+ return verifyJwt(match[1]);
23
+ }
24
+ export function requireAuth(req, res) {
25
+ const auth = getAuth(req);
26
+ if (!auth) {
27
+ res.writeHead(401, { 'Content-Type': 'application/json' });
28
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
29
+ return null;
30
+ }
31
+ return auth;
32
+ }
33
+ export function requireRole(req, res, roles) {
34
+ const auth = requireAuth(req, res);
35
+ if (!auth)
36
+ return null;
37
+ if (!roles.includes(auth.role)) {
38
+ res.writeHead(403, { 'Content-Type': 'application/json' });
39
+ res.end(JSON.stringify({ error: 'Forbidden' }));
40
+ return null;
41
+ }
42
+ return auth;
43
+ }
44
+ export function verifyPat(req) {
45
+ const header = req.headers.authorization ?? '';
46
+ if (!header.startsWith('Bearer tflw_pat_'))
47
+ return null;
48
+ const token = header.slice(7);
49
+ const hash = hashPat(token);
50
+ const db = getDb();
51
+ const row = db.prepare(`
52
+ SELECT pat.user_id, pat.scope
53
+ FROM personal_access_tokens pat
54
+ WHERE pat.token_hash = ?
55
+ AND (pat.expires_at IS NULL OR pat.expires_at > datetime('now'))
56
+ `).get(hash);
57
+ if (!row)
58
+ return null;
59
+ db.prepare(`UPDATE personal_access_tokens SET last_used_at = datetime('now') WHERE token_hash = ?`).run(hash);
60
+ return { userId: row.user_id, scope: row.scope };
61
+ }
62
+ export function hashPat(token) {
63
+ return crypto.createHash('sha256').update(token).digest('hex');
64
+ }
65
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/middleware/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ -- 001_initial.sql
2
+ -- 기본 스키마: apps, users, invitations, sessions, bug_reports, test_cases
3
+
4
+ CREATE TABLE IF NOT EXISTS apps (
5
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ name TEXT NOT NULL,
7
+ version TEXT,
8
+ bundle_id TEXT,
9
+ platform TEXT NOT NULL CHECK (platform IN ('ios', 'android')),
10
+ file_path TEXT NOT NULL,
11
+ label TEXT,
12
+ uploaded_at TEXT NOT NULL DEFAULT (datetime('now'))
13
+ );
14
+
15
+ CREATE TABLE IF NOT EXISTS users (
16
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
17
+ email TEXT NOT NULL UNIQUE,
18
+ role TEXT NOT NULL DEFAULT 'QA' CHECK (role IN ('Admin', 'Developer', 'QA', 'Viewer')),
19
+ joined_at TEXT NOT NULL DEFAULT (datetime('now'))
20
+ );
21
+
22
+ CREATE TABLE IF NOT EXISTS invitations (
23
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
24
+ token TEXT NOT NULL UNIQUE,
25
+ email TEXT,
26
+ role TEXT NOT NULL DEFAULT 'QA',
27
+ expires_at TEXT NOT NULL,
28
+ used_at TEXT
29
+ );
30
+
31
+ CREATE TABLE IF NOT EXISTS sessions (
32
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ device_id TEXT NOT NULL,
34
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
35
+ duration INTEGER,
36
+ recording_path TEXT
37
+ );
38
+
39
+ CREATE TABLE IF NOT EXISTS bug_reports (
40
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
41
+ session_id INTEGER REFERENCES sessions(id),
42
+ screenshot_path TEXT,
43
+ memo TEXT,
44
+ steps TEXT,
45
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS test_cases (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ title TEXT NOT NULL,
51
+ steps TEXT,
52
+ result TEXT,
53
+ run_at TEXT,
54
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
55
+ );
@@ -0,0 +1,54 @@
1
+ -- 002_dashboard_foundation.sql
2
+ -- Phase 2 dashboard 기반: App Center 태그, 코멘트, PAT, 팀 설정
3
+
4
+ -- apps 테이블 확장
5
+ ALTER TABLE apps ADD COLUMN status_label TEXT CHECK (
6
+ status_label IN ('Backlog', 'In Progress', 'Done', 'Rejected')
7
+ ) DEFAULT NULL;
8
+ ALTER TABLE apps ADD COLUMN version_label TEXT DEFAULT NULL;
9
+ ALTER TABLE apps ADD COLUMN uploader_id INTEGER REFERENCES users(id);
10
+
11
+ -- users 테이블 확장
12
+ ALTER TABLE users ADD COLUMN password_hash TEXT;
13
+ ALTER TABLE users ADD COLUMN display_name TEXT;
14
+
15
+ -- 빌드별 QA 코멘트
16
+ CREATE TABLE IF NOT EXISTS comments (
17
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
18
+ build_id INTEGER NOT NULL REFERENCES apps(id) ON DELETE CASCADE,
19
+ author_id INTEGER NOT NULL REFERENCES users(id),
20
+ body TEXT NOT NULL,
21
+ parent_id INTEGER REFERENCES comments(id),
22
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
23
+ );
24
+
25
+ -- 코멘트 첨부 이미지
26
+ CREATE TABLE IF NOT EXISTS comment_attachments (
27
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
28
+ comment_id INTEGER NOT NULL REFERENCES comments(id) ON DELETE CASCADE,
29
+ file_path TEXT NOT NULL,
30
+ mime TEXT NOT NULL,
31
+ size INTEGER NOT NULL
32
+ );
33
+
34
+ -- API 배포용 Personal Access Token
35
+ CREATE TABLE IF NOT EXISTS personal_access_tokens (
36
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
37
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
38
+ name TEXT NOT NULL,
39
+ token_hash TEXT NOT NULL UNIQUE,
40
+ scope TEXT NOT NULL DEFAULT 'builds:write',
41
+ last_used_at TEXT,
42
+ expires_at TEXT,
43
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
44
+ );
45
+
46
+ -- 팀 설정 (singleton row, id=1 고정)
47
+ CREATE TABLE IF NOT EXISTS team_settings (
48
+ id INTEGER PRIMARY KEY CHECK (id = 1),
49
+ team_name TEXT NOT NULL DEFAULT 'tapflow',
50
+ logo_path TEXT,
51
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
52
+ );
53
+
54
+ INSERT OR IGNORE INTO team_settings (id, team_name) VALUES (1, 'tapflow');
@@ -0,0 +1,14 @@
1
+ -- 003_recordings.sql
2
+ -- 클라이언트 녹화 히스토리: 팀원 누구나 TTL 72h 내 다운로드 가능
3
+
4
+ CREATE TABLE IF NOT EXISTS recordings (
5
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ filename TEXT NOT NULL UNIQUE,
7
+ session_id TEXT,
8
+ uploader_id INTEGER REFERENCES users(id),
9
+ file_size INTEGER NOT NULL,
10
+ mime TEXT NOT NULL,
11
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
12
+ expires_at TEXT NOT NULL,
13
+ events_path TEXT
14
+ );
@@ -0,0 +1,88 @@
1
+ -- 004_app_build_split.sql
2
+ -- Phase 3 구조 정비: apps(앱 엔티티) + builds(빌드 산출물) 분리
3
+ -- 기존 apps 데이터를 builds로 이관, bundle_id 기준 apps row 자동 생성
4
+
5
+ PRAGMA foreign_keys = OFF;
6
+
7
+ -- Step 1. 기존 apps를 임시 테이블로 이동
8
+ ALTER TABLE apps RENAME TO _apps_v3_legacy;
9
+
10
+ -- Step 2. 새 apps 테이블 — 앱 엔티티
11
+ CREATE TABLE apps (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ name TEXT NOT NULL,
14
+ bundle_id_key TEXT NOT NULL,
15
+ platform TEXT NOT NULL CHECK (platform IN ('ios', 'android')),
16
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
17
+ UNIQUE (bundle_id_key, platform)
18
+ );
19
+
20
+ -- Step 3. 앱 엔티티 생성 (bundle_id + platform 기준 dedup)
21
+ -- NULL bundle_id → '__unknown__' 그룹, NULL platform → 'ios' fallback
22
+ INSERT INTO apps (name, bundle_id_key, platform, created_at)
23
+ SELECT
24
+ COALESCE(name, bundle_id, 'Unknown App') AS name,
25
+ COALESCE(bundle_id, '__unknown__') AS bundle_id_key,
26
+ COALESCE(platform, 'ios') AS platform,
27
+ MIN(uploaded_at) AS created_at
28
+ FROM _apps_v3_legacy
29
+ GROUP BY COALESCE(bundle_id, '__unknown__'), COALESCE(platform, 'ios');
30
+
31
+ -- Step 4. builds 테이블
32
+ -- id는 기존 apps.id 보존 → comments.build_id FK 값 그대로 유지
33
+ CREATE TABLE builds (
34
+ id INTEGER PRIMARY KEY,
35
+ app_id INTEGER NOT NULL REFERENCES apps(id),
36
+ version_name TEXT,
37
+ build_number TEXT,
38
+ bundle_id TEXT,
39
+ status_label TEXT CHECK (status_label IN ('Backlog', 'In Progress', 'Done', 'Rejected')),
40
+ file_path TEXT NOT NULL,
41
+ label TEXT,
42
+ version_label TEXT,
43
+ uploader_id INTEGER REFERENCES users(id),
44
+ uploaded_at TEXT NOT NULL DEFAULT (datetime('now'))
45
+ );
46
+
47
+ -- Step 5. 기존 데이터 이관
48
+ -- version_name / build_number는 이 이전 데이터에서 추출 불가 → NULL
49
+ INSERT INTO builds (
50
+ id, app_id, version_name, build_number,
51
+ bundle_id, status_label, file_path, label, version_label,
52
+ uploader_id, uploaded_at
53
+ )
54
+ SELECT
55
+ l.id,
56
+ a.id,
57
+ NULL,
58
+ NULL,
59
+ l.bundle_id,
60
+ l.status_label,
61
+ l.file_path,
62
+ l.label,
63
+ l.version_label,
64
+ l.uploader_id,
65
+ l.uploaded_at
66
+ FROM _apps_v3_legacy l
67
+ JOIN apps a
68
+ ON a.bundle_id_key = COALESCE(l.bundle_id, '__unknown__')
69
+ AND a.platform = COALESCE(l.platform, 'ios');
70
+
71
+ -- Step 6. comments 테이블을 builds(id) FK로 재생성
72
+ -- build_id 값은 builds.id = 구 apps.id 이므로 그대로 유효
73
+ CREATE TABLE _comments_rebuild (
74
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
75
+ build_id INTEGER NOT NULL REFERENCES builds(id) ON DELETE CASCADE,
76
+ author_id INTEGER NOT NULL REFERENCES users(id),
77
+ body TEXT NOT NULL,
78
+ parent_id INTEGER REFERENCES _comments_rebuild(id),
79
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
80
+ );
81
+ INSERT INTO _comments_rebuild SELECT * FROM comments;
82
+ DROP TABLE comments;
83
+ ALTER TABLE _comments_rebuild RENAME TO comments;
84
+
85
+ -- Step 7. legacy 삭제
86
+ DROP TABLE _apps_v3_legacy;
87
+
88
+ PRAGMA foreign_keys = ON;
@@ -0,0 +1 @@
1
+ ALTER TABLE users ADD COLUMN avatar_url TEXT;
@@ -0,0 +1,7 @@
1
+ CREATE TABLE password_reset_tokens (
2
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
3
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
4
+ token TEXT NOT NULL UNIQUE,
5
+ expires_at TEXT NOT NULL,
6
+ used_at TEXT
7
+ );
@@ -0,0 +1,34 @@
1
+ -- 007_app_platform_model.sql
2
+ -- App을 제품 단위 엔티티로 확장: platform이 'ios' | 'android' | 'both' 를 지원
3
+ -- 개별 build는 항상 'ios' | 'android' 중 하나를 가지므로 builds에 platform 컬럼 추가
4
+
5
+ PRAGMA foreign_keys = OFF;
6
+
7
+ -- Step 1. apps 테이블 재구성
8
+ -- - bundle_id_key: NOT NULL → nullable (앱 먼저 생성, 빌드 나중 추가 지원)
9
+ -- - platform CHECK: 'both' 추가
10
+ -- - UNIQUE(bundle_id_key, platform): 제거 (application 레벨에서 처리)
11
+ CREATE TABLE apps_new (
12
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13
+ name TEXT NOT NULL,
14
+ bundle_id_key TEXT,
15
+ platform TEXT NOT NULL CHECK (platform IN ('ios', 'android', 'both')),
16
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
17
+ );
18
+
19
+ INSERT INTO apps_new (id, name, bundle_id_key, platform, created_at)
20
+ SELECT id, name, bundle_id_key, platform, created_at FROM apps;
21
+
22
+ DROP TABLE apps;
23
+ ALTER TABLE apps_new RENAME TO apps;
24
+
25
+ -- Step 2. builds에 platform 컬럼 추가
26
+ -- - app.platform이 'both'인 경우 개별 빌드 수준에서 플랫폼 식별이 필요
27
+ ALTER TABLE builds ADD COLUMN platform TEXT CHECK (platform IN ('ios', 'android'));
28
+
29
+ -- Step 3. 기존 builds.platform 채우기 (기존 앱은 모두 'ios' | 'android')
30
+ UPDATE builds SET platform = (
31
+ SELECT a.platform FROM apps a WHERE a.id = builds.app_id
32
+ );
33
+
34
+ PRAGMA foreign_keys = ON;
@@ -0,0 +1,4 @@
1
+ -- 008_recordings_build_id.sql
2
+ -- recordings에 build_id 추가 — 빌드 기준 필터링을 위해
3
+
4
+ ALTER TABLE recordings ADD COLUMN build_id INTEGER REFERENCES builds(id);
@@ -0,0 +1,12 @@
1
+ -- 009_agent_resources.sql
2
+ -- Mac 에이전트 CPU·RAM 시계열 데이터 — 1분 집계, 30일 보존
3
+
4
+ CREATE TABLE agent_resources (
5
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
6
+ agent_name TEXT NOT NULL,
7
+ cpu_percent REAL NOT NULL,
8
+ mem_percent REAL NOT NULL,
9
+ recorded_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
10
+ );
11
+
12
+ CREATE INDEX idx_agent_resources_agent_at ON agent_resources (agent_name, recorded_at);
@@ -0,0 +1,16 @@
1
+ import type http from 'http';
2
+ type Handler = (req: http.IncomingMessage, res: http.ServerResponse, params: Record<string, string>) => void | Promise<void>;
3
+ export declare class Router {
4
+ private routes;
5
+ on(method: string, path: string, handler: Handler): void;
6
+ get(path: string, handler: Handler): void;
7
+ post(path: string, handler: Handler): void;
8
+ patch(path: string, handler: Handler): void;
9
+ delete(path: string, handler: Handler): void;
10
+ handle(req: http.IncomingMessage, res: http.ServerResponse): Promise<boolean>;
11
+ }
12
+ export declare function json(res: http.ServerResponse, status: number, body: unknown): void;
13
+ export declare function readBody(req: http.IncomingMessage): Promise<Buffer>;
14
+ export declare function readJson<T = unknown>(req: http.IncomingMessage): Promise<T>;
15
+ export {};
16
+ //# sourceMappingURL=router.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,MAAM,CAAA;AAE5B,KAAK,OAAO,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAS5H,qBAAa,MAAM;IACjB,OAAO,CAAC,MAAM,CAAc;IAE5B,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI;IAQxD,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAClC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IACnC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IACpC,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IAE/B,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC;CAmBpF;AAED,wBAAgB,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,IAAI,CAIlF;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAOnE;AAED,wBAAsB,QAAQ,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,CAAC,CAAC,CAAC,CAGjF"}