hammoc 1.0.3 → 1.1.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 (176) hide show
  1. package/README.md +46 -16
  2. package/bin/hammoc.js +22 -5
  3. package/package.json +2 -1
  4. package/packages/client/dist/assets/{index-BfemoSfM.js → index-CFWfpySn.js} +1 -1
  5. package/packages/client/dist/assets/index-D8ezrT4P.js +1446 -0
  6. package/packages/client/dist/assets/index-JvaBmdnx.css +32 -0
  7. package/packages/client/dist/favicon-192.png +0 -0
  8. package/packages/client/dist/favicon-512.png +0 -0
  9. package/packages/client/dist/favicon.png +0 -0
  10. package/packages/client/dist/index.html +5 -3
  11. package/packages/client/dist/manifest.webmanifest +1 -1
  12. package/packages/client/dist/sw.js +2 -1
  13. package/packages/server/dist/app.d.ts.map +1 -1
  14. package/packages/server/dist/app.js +58 -6
  15. package/packages/server/dist/app.js.map +1 -1
  16. package/packages/server/dist/config/index.d.ts +17 -9
  17. package/packages/server/dist/config/index.d.ts.map +1 -1
  18. package/packages/server/dist/config/index.js +17 -10
  19. package/packages/server/dist/config/index.js.map +1 -1
  20. package/packages/server/dist/controllers/boardController.d.ts +0 -1
  21. package/packages/server/dist/controllers/boardController.d.ts.map +1 -1
  22. package/packages/server/dist/controllers/boardController.js +62 -29
  23. package/packages/server/dist/controllers/boardController.js.map +1 -1
  24. package/packages/server/dist/controllers/cliController.d.ts.map +1 -1
  25. package/packages/server/dist/controllers/cliController.js +8 -0
  26. package/packages/server/dist/controllers/cliController.js.map +1 -1
  27. package/packages/server/dist/controllers/fileSystemController.d.ts +10 -0
  28. package/packages/server/dist/controllers/fileSystemController.d.ts.map +1 -1
  29. package/packages/server/dist/controllers/fileSystemController.js +156 -0
  30. package/packages/server/dist/controllers/fileSystemController.js.map +1 -1
  31. package/packages/server/dist/controllers/queueController.d.ts +0 -4
  32. package/packages/server/dist/controllers/queueController.d.ts.map +1 -1
  33. package/packages/server/dist/controllers/queueController.js +0 -88
  34. package/packages/server/dist/controllers/queueController.js.map +1 -1
  35. package/packages/server/dist/controllers/queueTemplateController.d.ts +4 -0
  36. package/packages/server/dist/controllers/queueTemplateController.d.ts.map +1 -1
  37. package/packages/server/dist/controllers/queueTemplateController.js +61 -4
  38. package/packages/server/dist/controllers/queueTemplateController.js.map +1 -1
  39. package/packages/server/dist/controllers/serverController.d.ts +3 -3
  40. package/packages/server/dist/controllers/serverController.d.ts.map +1 -1
  41. package/packages/server/dist/controllers/serverController.js +91 -39
  42. package/packages/server/dist/controllers/serverController.js.map +1 -1
  43. package/packages/server/dist/controllers/sessionController.d.ts.map +1 -1
  44. package/packages/server/dist/controllers/sessionController.js +2 -1
  45. package/packages/server/dist/controllers/sessionController.js.map +1 -1
  46. package/packages/server/dist/handlers/websocket.d.ts +12 -2
  47. package/packages/server/dist/handlers/websocket.d.ts.map +1 -1
  48. package/packages/server/dist/handlers/websocket.js +785 -106
  49. package/packages/server/dist/handlers/websocket.js.map +1 -1
  50. package/packages/server/dist/index.js +5 -1
  51. package/packages/server/dist/index.js.map +1 -1
  52. package/packages/server/dist/locales/en/server.json +12 -3
  53. package/packages/server/dist/locales/es/server.json +3 -1
  54. package/packages/server/dist/locales/ja/server.json +3 -1
  55. package/packages/server/dist/locales/ko/server.json +12 -3
  56. package/packages/server/dist/locales/pt/server.json +3 -1
  57. package/packages/server/dist/locales/zh-CN/server.json +3 -1
  58. package/packages/server/dist/middleware/pathGuard.d.ts +1 -7
  59. package/packages/server/dist/middleware/pathGuard.d.ts.map +1 -1
  60. package/packages/server/dist/middleware/pathGuard.js +57 -4
  61. package/packages/server/dist/middleware/pathGuard.js.map +1 -1
  62. package/packages/server/dist/middleware/session.d.ts.map +1 -1
  63. package/packages/server/dist/middleware/session.js +3 -1
  64. package/packages/server/dist/middleware/session.js.map +1 -1
  65. package/packages/server/dist/routes/board.d.ts.map +1 -1
  66. package/packages/server/dist/routes/board.js +0 -1
  67. package/packages/server/dist/routes/board.js.map +1 -1
  68. package/packages/server/dist/routes/fileSystem.d.ts.map +1 -1
  69. package/packages/server/dist/routes/fileSystem.js +39 -0
  70. package/packages/server/dist/routes/fileSystem.js.map +1 -1
  71. package/packages/server/dist/routes/preferences.d.ts.map +1 -1
  72. package/packages/server/dist/routes/preferences.js +80 -2
  73. package/packages/server/dist/routes/preferences.js.map +1 -1
  74. package/packages/server/dist/routes/queue.d.ts.map +1 -1
  75. package/packages/server/dist/routes/queue.js +9 -8
  76. package/packages/server/dist/routes/queue.js.map +1 -1
  77. package/packages/server/dist/services/bmadStatusService.d.ts.map +1 -1
  78. package/packages/server/dist/services/bmadStatusService.js +60 -0
  79. package/packages/server/dist/services/bmadStatusService.js.map +1 -1
  80. package/packages/server/dist/services/chatService.d.ts +4 -0
  81. package/packages/server/dist/services/chatService.d.ts.map +1 -1
  82. package/packages/server/dist/services/chatService.js +7 -1
  83. package/packages/server/dist/services/chatService.js.map +1 -1
  84. package/packages/server/dist/services/cliService.d.ts.map +1 -1
  85. package/packages/server/dist/services/cliService.js +66 -15
  86. package/packages/server/dist/services/cliService.js.map +1 -1
  87. package/packages/server/dist/services/fileSystemService.d.ts +29 -1
  88. package/packages/server/dist/services/fileSystemService.d.ts.map +1 -1
  89. package/packages/server/dist/services/fileSystemService.js +240 -5
  90. package/packages/server/dist/services/fileSystemService.js.map +1 -1
  91. package/packages/server/dist/services/gitService.js +1 -1
  92. package/packages/server/dist/services/gitService.js.map +1 -1
  93. package/packages/server/dist/services/historyParser.d.ts +13 -0
  94. package/packages/server/dist/services/historyParser.d.ts.map +1 -1
  95. package/packages/server/dist/services/historyParser.js +159 -14
  96. package/packages/server/dist/services/historyParser.js.map +1 -1
  97. package/packages/server/dist/services/issueService.d.ts +3 -10
  98. package/packages/server/dist/services/issueService.d.ts.map +1 -1
  99. package/packages/server/dist/services/issueService.js +145 -156
  100. package/packages/server/dist/services/issueService.js.map +1 -1
  101. package/packages/server/dist/services/notificationService.d.ts +11 -6
  102. package/packages/server/dist/services/notificationService.d.ts.map +1 -1
  103. package/packages/server/dist/services/notificationService.js +75 -20
  104. package/packages/server/dist/services/notificationService.js.map +1 -1
  105. package/packages/server/dist/services/preferencesService.d.ts +2 -3
  106. package/packages/server/dist/services/preferencesService.d.ts.map +1 -1
  107. package/packages/server/dist/services/preferencesService.js +3 -10
  108. package/packages/server/dist/services/preferencesService.js.map +1 -1
  109. package/packages/server/dist/services/projectService.d.ts +26 -1
  110. package/packages/server/dist/services/projectService.d.ts.map +1 -1
  111. package/packages/server/dist/services/projectService.js +113 -0
  112. package/packages/server/dist/services/projectService.js.map +1 -1
  113. package/packages/server/dist/services/queueService.d.ts +1 -1
  114. package/packages/server/dist/services/queueService.d.ts.map +1 -1
  115. package/packages/server/dist/services/queueService.js +6 -2
  116. package/packages/server/dist/services/queueService.js.map +1 -1
  117. package/packages/server/dist/services/queueTemplateService.d.ts +5 -0
  118. package/packages/server/dist/services/queueTemplateService.d.ts.map +1 -1
  119. package/packages/server/dist/services/queueTemplateService.js +71 -21
  120. package/packages/server/dist/services/queueTemplateService.js.map +1 -1
  121. package/packages/server/dist/services/sessionService.d.ts +12 -0
  122. package/packages/server/dist/services/sessionService.d.ts.map +1 -1
  123. package/packages/server/dist/services/sessionService.js +121 -107
  124. package/packages/server/dist/services/sessionService.js.map +1 -1
  125. package/packages/server/dist/services/streamHandler.d.ts +4 -2
  126. package/packages/server/dist/services/streamHandler.d.ts.map +1 -1
  127. package/packages/server/dist/services/streamHandler.js +20 -4
  128. package/packages/server/dist/services/streamHandler.js.map +1 -1
  129. package/packages/server/dist/services/webPushService.d.ts +61 -0
  130. package/packages/server/dist/services/webPushService.d.ts.map +1 -0
  131. package/packages/server/dist/services/webPushService.js +258 -0
  132. package/packages/server/dist/services/webPushService.js.map +1 -0
  133. package/packages/server/dist/utils/networkUtils.d.ts +17 -2
  134. package/packages/server/dist/utils/networkUtils.d.ts.map +1 -1
  135. package/packages/server/dist/utils/networkUtils.js +121 -8
  136. package/packages/server/dist/utils/networkUtils.js.map +1 -1
  137. package/packages/server/package.json +4 -0
  138. package/packages/shared/dist/constants/errorCodes.d.ts +1 -0
  139. package/packages/shared/dist/constants/errorCodes.d.ts.map +1 -1
  140. package/packages/shared/dist/constants/errorCodes.js +2 -0
  141. package/packages/shared/dist/constants/errorCodes.js.map +1 -1
  142. package/packages/shared/dist/index.d.ts +1 -1
  143. package/packages/shared/dist/index.d.ts.map +1 -1
  144. package/packages/shared/dist/index.js.map +1 -1
  145. package/packages/shared/dist/types/bmadStatus.d.ts +2 -0
  146. package/packages/shared/dist/types/bmadStatus.d.ts.map +1 -1
  147. package/packages/shared/dist/types/bmadStatus.js.map +1 -1
  148. package/packages/shared/dist/types/board.d.ts +8 -8
  149. package/packages/shared/dist/types/board.d.ts.map +1 -1
  150. package/packages/shared/dist/types/board.js +24 -39
  151. package/packages/shared/dist/types/board.js.map +1 -1
  152. package/packages/shared/dist/types/fileSystem.d.ts +36 -0
  153. package/packages/shared/dist/types/fileSystem.d.ts.map +1 -1
  154. package/packages/shared/dist/types/fileSystem.js +15 -0
  155. package/packages/shared/dist/types/fileSystem.js.map +1 -1
  156. package/packages/shared/dist/types/history.d.ts +10 -2
  157. package/packages/shared/dist/types/history.d.ts.map +1 -1
  158. package/packages/shared/dist/types/preferences.d.ts +26 -1
  159. package/packages/shared/dist/types/preferences.d.ts.map +1 -1
  160. package/packages/shared/dist/types/preferences.js.map +1 -1
  161. package/packages/shared/dist/types/sdk.d.ts +23 -0
  162. package/packages/shared/dist/types/sdk.d.ts.map +1 -1
  163. package/packages/shared/dist/types/sdk.js +48 -0
  164. package/packages/shared/dist/types/sdk.js.map +1 -1
  165. package/packages/shared/dist/types/streaming.d.ts +1 -0
  166. package/packages/shared/dist/types/streaming.d.ts.map +1 -1
  167. package/packages/shared/dist/types/streaming.js.map +1 -1
  168. package/packages/shared/dist/types/websocket.d.ts +41 -1
  169. package/packages/shared/dist/types/websocket.d.ts.map +1 -1
  170. package/packages/shared/dist/utils/queueTemplateUtils.d.ts +2 -1
  171. package/packages/shared/dist/utils/queueTemplateUtils.d.ts.map +1 -1
  172. package/packages/shared/dist/utils/queueTemplateUtils.js +10 -3
  173. package/packages/shared/dist/utils/queueTemplateUtils.js.map +1 -1
  174. package/packages/client/dist/assets/index-B335pbdL.js +0 -1297
  175. package/packages/client/dist/assets/index-CmTNL_3X.css +0 -32
  176. package/packages/client/dist/workbox-7a79b53c.js +0 -1
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Web Push Service — VAPID key management, subscription storage, push delivery
3
+ * Stores VAPID keys and push subscriptions in ~/.hammoc/ as JSON files.
4
+ * Uses the web-push library for VAPID signing and push message delivery.
5
+ */
6
+ export interface WebPushPayload {
7
+ title: string;
8
+ body: string;
9
+ icon?: string;
10
+ badge?: string;
11
+ tag?: string;
12
+ url?: string;
13
+ }
14
+ declare class WebPushService {
15
+ private vapidKeys;
16
+ private subscriptions;
17
+ private loaded;
18
+ /** Single-flight guard for initial load */
19
+ private loadPromise;
20
+ /** Async mutex: queued operations wait for the previous one to finish */
21
+ private mutationQueue;
22
+ private getDataDir;
23
+ private getVapidPath;
24
+ private getSubscriptionsPath;
25
+ /** Serialize subscription mutations to prevent concurrent read-modify-write races */
26
+ private enqueue;
27
+ /** Single-flight subscription load — safe to call from anywhere without mutex */
28
+ private ensureLoaded;
29
+ /** Ensure VAPID keys exist; generate only if file does not exist */
30
+ private ensureVapidKeys;
31
+ /** Load subscriptions from disk (only ENOENT is treated as empty) */
32
+ private loadSubscriptions;
33
+ /** Persist subscriptions to disk */
34
+ private saveSubscriptions;
35
+ /** Initialize: load VAPID keys and subscriptions */
36
+ init(): Promise<void>;
37
+ /** Get the VAPID public key (needed by client for PushManager.subscribe) */
38
+ getVapidPublicKey(): Promise<string>;
39
+ /** Get number of stored subscriptions */
40
+ getSubscriptionCount(): number;
41
+ /** Validate and add a push subscription (deduplicate by endpoint) */
42
+ subscribe(sub: {
43
+ endpoint: string;
44
+ keys: {
45
+ p256dh: string;
46
+ auth: string;
47
+ };
48
+ }, userAgent?: string): Promise<void>;
49
+ /** Remove a push subscription by endpoint */
50
+ unsubscribe(endpoint: string): Promise<boolean>;
51
+ /** Send a push notification to all subscriptions */
52
+ sendPush(payload: WebPushPayload): Promise<void>;
53
+ /** Send a test push notification */
54
+ sendTest(): Promise<{
55
+ success: boolean;
56
+ error?: string;
57
+ }>;
58
+ }
59
+ export declare const webPushService: WebPushService;
60
+ export {};
61
+ //# sourceMappingURL=webPushService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webPushService.d.ts","sourceRoot":"","sources":["../../src/services/webPushService.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAuBH,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAuBD,cAAM,cAAc;IAClB,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,aAAa,CAA4B;IACjD,OAAO,CAAC,MAAM,CAAS;IACvB,2CAA2C;IAC3C,OAAO,CAAC,WAAW,CAA8B;IACjD,yEAAyE;IACzE,OAAO,CAAC,aAAa,CAAoC;IAEzD,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,YAAY;IAIpB,OAAO,CAAC,oBAAoB;IAI5B,qFAAqF;IACrF,OAAO,CAAC,OAAO;IAKf,iFAAiF;IACjF,OAAO,CAAC,YAAY;IAUpB,oEAAoE;YACtD,eAAe;IA8B7B,qEAAqE;YACvD,iBAAiB;IAgB/B,oCAAoC;YACtB,iBAAiB;IAM/B,oDAAoD;IAC9C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,4EAA4E;IACtE,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC;IAK1C,yCAAyC;IACzC,oBAAoB,IAAI,MAAM;IAI9B,qEAAqE;IAC/D,SAAS,CAAC,GAAG,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA4BrH,6CAA6C;IACvC,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAqBrD,oDAAoD;IAC9C,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAsDtD,oCAAoC;IAC9B,QAAQ,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAyBhE;AAED,eAAO,MAAM,cAAc,gBAAuB,CAAC"}
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Web Push Service — VAPID key management, subscription storage, push delivery
3
+ * Stores VAPID keys and push subscriptions in ~/.hammoc/ as JSON files.
4
+ * Uses the web-push library for VAPID signing and push message delivery.
5
+ */
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ import os from 'node:os';
9
+ import webpush from 'web-push';
10
+ import { preferencesService } from './preferencesService.js';
11
+ import { createLogger } from '../utils/logger.js';
12
+ const logger = createLogger('webPushService');
13
+ /** Known push service hostnames (strict matching for SSRF prevention) */
14
+ const PUSH_SERVICE_PATTERNS = [
15
+ /^fcm\.googleapis\.com$/,
16
+ /^updates\.push\.services\.mozilla\.com$/,
17
+ /^push\.services\.mozilla\.com$/,
18
+ /^web\.push\.apple\.com$/,
19
+ /\.notify\.windows\.com$/,
20
+ /\.push\.samsung\.com$/,
21
+ ];
22
+ /** Validate that an endpoint URL is a legitimate push service */
23
+ function isValidPushEndpoint(endpoint) {
24
+ try {
25
+ const url = new URL(endpoint);
26
+ if (url.protocol !== 'https:')
27
+ return false;
28
+ return PUSH_SERVICE_PATTERNS.some(pattern => pattern.test(url.hostname));
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ class WebPushService {
35
+ vapidKeys = null;
36
+ subscriptions = [];
37
+ loaded = false;
38
+ /** Single-flight guard for initial load */
39
+ loadPromise = null;
40
+ /** Async mutex: queued operations wait for the previous one to finish */
41
+ mutationQueue = Promise.resolve();
42
+ getDataDir() {
43
+ return path.join(os.homedir(), '.hammoc');
44
+ }
45
+ getVapidPath() {
46
+ return path.join(this.getDataDir(), 'vapid-keys.json');
47
+ }
48
+ getSubscriptionsPath() {
49
+ return path.join(this.getDataDir(), 'push-subscriptions.json');
50
+ }
51
+ /** Serialize subscription mutations to prevent concurrent read-modify-write races */
52
+ enqueue(fn) {
53
+ this.mutationQueue = this.mutationQueue.then(fn, fn);
54
+ return this.mutationQueue;
55
+ }
56
+ /** Single-flight subscription load — safe to call from anywhere without mutex */
57
+ ensureLoaded() {
58
+ if (this.loaded)
59
+ return Promise.resolve();
60
+ if (!this.loadPromise) {
61
+ this.loadPromise = this.loadSubscriptions().finally(() => {
62
+ this.loadPromise = null;
63
+ });
64
+ }
65
+ return this.loadPromise;
66
+ }
67
+ /** Ensure VAPID keys exist; generate only if file does not exist */
68
+ async ensureVapidKeys() {
69
+ if (this.vapidKeys)
70
+ return this.vapidKeys;
71
+ const vapidPath = this.getVapidPath();
72
+ try {
73
+ const content = await fs.readFile(vapidPath, 'utf-8');
74
+ this.vapidKeys = JSON.parse(content);
75
+ if (!this.vapidKeys.publicKey || !this.vapidKeys.privateKey) {
76
+ throw new Error('Invalid VAPID keys file: missing publicKey or privateKey');
77
+ }
78
+ }
79
+ catch (err) {
80
+ const code = err.code;
81
+ if (code === 'ENOENT') {
82
+ // First run — generate new keys
83
+ const keys = webpush.generateVAPIDKeys();
84
+ this.vapidKeys = { publicKey: keys.publicKey, privateKey: keys.privateKey };
85
+ const dataDir = this.getDataDir();
86
+ await fs.mkdir(dataDir, { recursive: true });
87
+ await fs.writeFile(vapidPath, JSON.stringify(this.vapidKeys, null, 2), 'utf-8');
88
+ logger.info('[WebPush] Generated new VAPID keys');
89
+ }
90
+ else {
91
+ // Parse/permission error — don't silently regenerate, fail fast
92
+ logger.error(`[WebPush] Failed to read VAPID keys: ${err.message}`);
93
+ throw err;
94
+ }
95
+ }
96
+ return this.vapidKeys;
97
+ }
98
+ /** Load subscriptions from disk (only ENOENT is treated as empty) */
99
+ async loadSubscriptions() {
100
+ try {
101
+ const content = await fs.readFile(this.getSubscriptionsPath(), 'utf-8');
102
+ this.subscriptions = JSON.parse(content);
103
+ }
104
+ catch (err) {
105
+ const code = err.code;
106
+ if (code === 'ENOENT') {
107
+ this.subscriptions = [];
108
+ }
109
+ else {
110
+ logger.error(`[WebPush] Failed to load subscriptions: ${err.message}`);
111
+ throw err;
112
+ }
113
+ }
114
+ this.loaded = true;
115
+ }
116
+ /** Persist subscriptions to disk */
117
+ async saveSubscriptions() {
118
+ const dataDir = this.getDataDir();
119
+ await fs.mkdir(dataDir, { recursive: true });
120
+ await fs.writeFile(this.getSubscriptionsPath(), JSON.stringify(this.subscriptions, null, 2), 'utf-8');
121
+ }
122
+ /** Initialize: load VAPID keys and subscriptions */
123
+ async init() {
124
+ await this.ensureVapidKeys();
125
+ await this.loadSubscriptions();
126
+ }
127
+ /** Get the VAPID public key (needed by client for PushManager.subscribe) */
128
+ async getVapidPublicKey() {
129
+ const keys = await this.ensureVapidKeys();
130
+ return keys.publicKey;
131
+ }
132
+ /** Get number of stored subscriptions */
133
+ getSubscriptionCount() {
134
+ return this.subscriptions.length;
135
+ }
136
+ /** Validate and add a push subscription (deduplicate by endpoint) */
137
+ async subscribe(sub, userAgent) {
138
+ if (!isValidPushEndpoint(sub.endpoint)) {
139
+ throw new Error('Invalid push endpoint: must be HTTPS from a known push service');
140
+ }
141
+ return this.enqueue(async () => {
142
+ await this.ensureLoaded();
143
+ // Copy-on-write: build new list, persist, then assign
144
+ const next = this.subscriptions.filter(s => s.endpoint !== sub.endpoint);
145
+ next.push({
146
+ endpoint: sub.endpoint,
147
+ keys: sub.keys,
148
+ userAgent,
149
+ createdAt: new Date().toISOString(),
150
+ });
151
+ const prev = this.subscriptions;
152
+ this.subscriptions = next;
153
+ try {
154
+ await this.saveSubscriptions();
155
+ }
156
+ catch (err) {
157
+ this.subscriptions = prev; // rollback on write failure
158
+ throw err;
159
+ }
160
+ logger.info(`[WebPush] Subscription added (total: ${this.subscriptions.length})`);
161
+ });
162
+ }
163
+ /** Remove a push subscription by endpoint */
164
+ async unsubscribe(endpoint) {
165
+ let removed = false;
166
+ await this.enqueue(async () => {
167
+ await this.ensureLoaded();
168
+ const next = this.subscriptions.filter(s => s.endpoint !== endpoint);
169
+ if (next.length < this.subscriptions.length) {
170
+ const prev = this.subscriptions;
171
+ this.subscriptions = next;
172
+ try {
173
+ await this.saveSubscriptions();
174
+ }
175
+ catch (err) {
176
+ this.subscriptions = prev; // rollback on write failure
177
+ throw err;
178
+ }
179
+ logger.info(`[WebPush] Subscription removed (total: ${this.subscriptions.length})`);
180
+ removed = true;
181
+ }
182
+ });
183
+ return removed;
184
+ }
185
+ /** Send a push notification to all subscriptions */
186
+ async sendPush(payload) {
187
+ await this.ensureLoaded();
188
+ if (this.subscriptions.length === 0)
189
+ return;
190
+ const keys = await this.ensureVapidKeys();
191
+ const vapidSubject = process.env.VAPID_SUBJECT || 'mailto:hammoc@localhost';
192
+ webpush.setVapidDetails(vapidSubject, keys.publicKey, keys.privateKey);
193
+ const body = JSON.stringify({
194
+ ...payload,
195
+ icon: payload.icon || '/favicon-192.png',
196
+ badge: payload.badge || '/favicon-192.png',
197
+ });
198
+ const expiredEndpoints = [];
199
+ await Promise.allSettled(this.subscriptions.map(async (sub) => {
200
+ try {
201
+ await webpush.sendNotification({ endpoint: sub.endpoint, keys: sub.keys }, body, { TTL: 60 * 60 });
202
+ }
203
+ catch (err) {
204
+ const statusCode = err.statusCode;
205
+ if (statusCode === 410 || statusCode === 404) {
206
+ // Subscription expired or invalid — mark for removal
207
+ expiredEndpoints.push(sub.endpoint);
208
+ logger.debug(`[WebPush] Subscription expired (${statusCode}): ${sub.endpoint.slice(0, 60)}...`);
209
+ }
210
+ else {
211
+ logger.warn(`[WebPush] Send failed: ${err.message || err}`);
212
+ }
213
+ }
214
+ }));
215
+ // Clean up expired subscriptions (serialized, with rollback)
216
+ if (expiredEndpoints.length > 0) {
217
+ await this.enqueue(async () => {
218
+ const next = this.subscriptions.filter(s => !expiredEndpoints.includes(s.endpoint));
219
+ const prev = this.subscriptions;
220
+ this.subscriptions = next;
221
+ try {
222
+ await this.saveSubscriptions();
223
+ }
224
+ catch (err) {
225
+ this.subscriptions = prev;
226
+ throw err;
227
+ }
228
+ logger.info(`[WebPush] Removed ${expiredEndpoints.length} expired subscription(s)`);
229
+ });
230
+ }
231
+ }
232
+ /** Send a test push notification */
233
+ async sendTest() {
234
+ try {
235
+ await this.ensureLoaded();
236
+ const prefs = await preferencesService.readPreferences();
237
+ if (!prefs.webPush?.enabled) {
238
+ return { success: false, error: 'Web Push is not enabled' };
239
+ }
240
+ if (this.subscriptions.length === 0) {
241
+ return { success: false, error: 'No subscriptions registered' };
242
+ }
243
+ await this.sendPush({
244
+ title: 'Hammoc',
245
+ body: '🔔 Test push notification from Hammoc',
246
+ tag: 'test',
247
+ url: '/',
248
+ });
249
+ return { success: true };
250
+ }
251
+ catch (err) {
252
+ const message = err instanceof Error ? err.message : 'Unknown error';
253
+ return { success: false, error: message };
254
+ }
255
+ }
256
+ }
257
+ export const webPushService = new WebPushService();
258
+ //# sourceMappingURL=webPushService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webPushService.js","sourceRoot":"","sources":["../../src/services/webPushService.ts"],"names":[],"mappings":"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"}
@@ -3,6 +3,7 @@
3
3
  * Story 17.5: Terminal Security - IP-based access control
4
4
  */
5
5
  import type { Socket } from 'socket.io';
6
+ import type { Request } from 'express';
6
7
  /**
7
8
  * Check if an IP address belongs to a local/private network range.
8
9
  * Supports IPv4, IPv6 loopback, and IPv4-mapped IPv6 addresses.
@@ -12,12 +13,26 @@ import type { Socket } from 'socket.io';
12
13
  * ::ffff:<ipv4> variants of the above
13
14
  */
14
15
  export declare function isLocalIP(ip: string): boolean;
16
+ /**
17
+ * Check if an IP is strictly a loopback address (127.0.0.0/8 or ::1).
18
+ * Use this for privileged operations (server restart/update) where
19
+ * even private network IPs should NOT be trusted.
20
+ */
21
+ export declare function isLoopbackIP(ip: string): boolean;
15
22
  /**
16
23
  * Extract client IP address from a Socket.io socket.
17
- * Uses socket.handshake.address directly.
18
- * X-Forwarded-For and other proxy headers are intentionally ignored for security.
24
+ * When TRUST_PROXY is enabled AND the direct peer is loopback,
25
+ * reads proxy headers to get the real client IP.
26
+ * Otherwise uses socket.handshake.address directly (safe for direct connections).
19
27
  */
20
28
  export declare function extractClientIP(socket: Socket): string;
29
+ /**
30
+ * Extract client IP address from an Express request.
31
+ * When TRUST_PROXY is enabled AND the direct peer is loopback,
32
+ * reads proxy headers to get the real client IP.
33
+ * Otherwise uses req.socket.remoteAddress directly.
34
+ */
35
+ export declare function extractRequestIP(req: Request): string;
21
36
  /**
22
37
  * Check if the server binding address is an external interface.
23
38
  * Returns true for 0.0.0.0, ::, or non-loopback/non-localhost addresses.
@@ -1 +1 @@
1
- {"version":3,"file":"networkUtils.d.ts","sourceRoot":"","sources":["../../src/utils/networkUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAkC7C;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAWvD"}
1
+ {"version":3,"file":"networkUtils.d.ts","sourceRoot":"","sources":["../../src/utils/networkUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AACxC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AASvC;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAgC7C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAUhD;AAkED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAOtD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAOrD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAWvD"}
@@ -2,6 +2,13 @@
2
2
  * Network Utilities for Terminal Security
3
3
  * Story 17.5: Terminal Security - IP-based access control
4
4
  */
5
+ import { isIP } from 'net';
6
+ import { config } from '../config/index.js';
7
+ /**
8
+ * Strict IPv4 octet pattern — rejects leading zeros, ports, suffixes.
9
+ * Matches "0"-"255" only (no "01", "127.0.0.1:443", "127.0.0.1abc").
10
+ */
11
+ const STRICT_IPV4_RE = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)$/;
5
12
  /**
6
13
  * Check if an IP address belongs to a local/private network range.
7
14
  * Supports IPv4, IPv6 loopback, and IPv4-mapped IPv6 addresses.
@@ -21,13 +28,11 @@ export function isLocalIP(ip) {
21
28
  if (normalizedIP.startsWith('::ffff:')) {
22
29
  normalizedIP = normalizedIP.slice(7);
23
30
  }
24
- // Parse IPv4
25
- const parts = normalizedIP.split('.');
26
- if (parts.length !== 4)
31
+ // Strict IPv4 format check — rejects "127.0.0.1:443", "127.0.0.1abc", etc.
32
+ if (!STRICT_IPV4_RE.test(normalizedIP))
27
33
  return false;
34
+ const parts = normalizedIP.split('.');
28
35
  const octets = parts.map((p) => parseInt(p, 10));
29
- if (octets.some((o) => isNaN(o) || o < 0 || o > 255))
30
- return false;
31
36
  const [a, b] = octets;
32
37
  // 127.0.0.0/8 (loopback)
33
38
  if (a === 127)
@@ -43,13 +48,121 @@ export function isLocalIP(ip) {
43
48
  return true;
44
49
  return false;
45
50
  }
51
+ /**
52
+ * Check if an IP is strictly a loopback address (127.0.0.0/8 or ::1).
53
+ * Use this for privileged operations (server restart/update) where
54
+ * even private network IPs should NOT be trusted.
55
+ */
56
+ export function isLoopbackIP(ip) {
57
+ if (!ip || typeof ip !== 'string')
58
+ return false;
59
+ if (ip === '::1')
60
+ return true;
61
+ let normalizedIP = ip;
62
+ if (normalizedIP.startsWith('::ffff:')) {
63
+ normalizedIP = normalizedIP.slice(7);
64
+ }
65
+ if (!STRICT_IPV4_RE.test(normalizedIP))
66
+ return false;
67
+ const first = parseInt(normalizedIP.split('.')[0], 10);
68
+ return first === 127;
69
+ }
70
+ /**
71
+ * Check if the peer (direct TCP connection) is a loopback address.
72
+ * Used to gate whether proxy headers should be trusted — only trust
73
+ * forwarding headers when the immediate connection is from localhost
74
+ * (i.e. cloudflared or nginx running on the same machine).
75
+ */
76
+ function isPeerLoopback(peerAddress) {
77
+ if (!peerAddress)
78
+ return false;
79
+ const addr = peerAddress.startsWith('::ffff:') ? peerAddress.slice(7) : peerAddress;
80
+ return addr === '127.0.0.1' || addr === '::1';
81
+ }
82
+ /**
83
+ * Validate and sanitize an IP string from a proxy header.
84
+ * Returns the IP if valid, empty string otherwise.
85
+ */
86
+ function validateIP(value) {
87
+ if (!value)
88
+ return '';
89
+ const trimmed = value.trim();
90
+ // Use Node.js built-in net.isIP for strict validation
91
+ if (isIP(trimmed) === 0)
92
+ return '';
93
+ return trimmed;
94
+ }
95
+ /**
96
+ * Extract the rightmost valid IP from a comma-separated X-Forwarded-For header.
97
+ * The rightmost entry is the one added by the closest trusted proxy, which is
98
+ * the most reliable — leftmost entries can be attacker-injected.
99
+ *
100
+ * Format: "client, proxy1, proxy2" — rightmost is added by our proxy.
101
+ * Returns empty string if no valid IP found.
102
+ */
103
+ function extractRightmostIP(headerValue) {
104
+ if (!headerValue)
105
+ return '';
106
+ const parts = headerValue.split(',');
107
+ // Walk from right to find the first valid non-private IP,
108
+ // or fall back to the rightmost valid IP
109
+ for (let i = parts.length - 1; i >= 0; i--) {
110
+ const candidate = parts[i].trim();
111
+ if (candidate && isIP(candidate) !== 0) {
112
+ return candidate;
113
+ }
114
+ }
115
+ return '';
116
+ }
117
+ /**
118
+ * Read proxy headers to determine real client IP.
119
+ * Priority: CF-Connecting-IP > X-Forwarded-For > X-Real-IP.
120
+ * Returns empty string if no valid proxy header found.
121
+ */
122
+ function extractFromProxyHeaders(headers) {
123
+ // CF-Connecting-IP is set by Cloudflare and is a single IP (most reliable)
124
+ const cfIP = validateIP(headers['cf-connecting-ip']);
125
+ if (cfIP)
126
+ return cfIP;
127
+ // X-Forwarded-For is standard for reverse proxies
128
+ const forwarded = extractRightmostIP(headers['x-forwarded-for']);
129
+ if (forwarded)
130
+ return forwarded;
131
+ // X-Real-IP is used by nginx
132
+ const realIP = validateIP(headers['x-real-ip']);
133
+ if (realIP)
134
+ return realIP;
135
+ return '';
136
+ }
46
137
  /**
47
138
  * Extract client IP address from a Socket.io socket.
48
- * Uses socket.handshake.address directly.
49
- * X-Forwarded-For and other proxy headers are intentionally ignored for security.
139
+ * When TRUST_PROXY is enabled AND the direct peer is loopback,
140
+ * reads proxy headers to get the real client IP.
141
+ * Otherwise uses socket.handshake.address directly (safe for direct connections).
50
142
  */
51
143
  export function extractClientIP(socket) {
52
- return socket.handshake.address || '';
144
+ const peerAddress = socket.handshake.address || '';
145
+ if (config.server.trustProxy && isPeerLoopback(peerAddress)) {
146
+ const proxyIP = extractFromProxyHeaders(socket.handshake.headers);
147
+ if (proxyIP)
148
+ return proxyIP;
149
+ }
150
+ return peerAddress;
151
+ }
152
+ /**
153
+ * Extract client IP address from an Express request.
154
+ * When TRUST_PROXY is enabled AND the direct peer is loopback,
155
+ * reads proxy headers to get the real client IP.
156
+ * Otherwise uses req.socket.remoteAddress directly.
157
+ */
158
+ export function extractRequestIP(req) {
159
+ const peerAddress = req.socket.remoteAddress || '';
160
+ if (config.server.trustProxy && isPeerLoopback(peerAddress)) {
161
+ const proxyIP = extractFromProxyHeaders(req.headers);
162
+ if (proxyIP)
163
+ return proxyIP;
164
+ }
165
+ return peerAddress;
53
166
  }
54
167
  /**
55
168
  * Check if the server binding address is an external interface.
@@ -1 +1 @@
1
- {"version":3,"file":"networkUtils.js","sourceRoot":"","sources":["../../src/utils/networkUtils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,EAAU;IAClC,IAAI,CAAC,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAEhD,gBAAgB;IAChB,IAAI,EAAE,KAAK,KAAK;QAAE,OAAO,IAAI,CAAC;IAE9B,iDAAiD;IACjD,IAAI,YAAY,GAAG,EAAE,CAAC;IACtB,IAAI,YAAY,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACvC,YAAY,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACvC,CAAC;IAED,aAAa;IACb,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACtC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAErC,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACjD,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAEnE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC;IAEtB,yBAAyB;IACzB,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAE3B,uBAAuB;IACvB,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAE1B,2BAA2B;IAC3B,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAExC,0CAA0C;IAC1C,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;QAAE,OAAO,IAAI,CAAC;IAEjD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc;IAC5C,OAAO,MAAM,CAAC,SAAS,CAAC,OAAO,IAAI,EAAE,CAAC;AACxC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IAExB,+CAA+C;IAC/C,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAErD,oDAAoD;IACpD,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,KAAK;QAAE,OAAO,KAAK,CAAC;IAEjF,8DAA8D;IAC9D,OAAO,IAAI,CAAC;AACd,CAAC"}
1
+ {"version":3,"file":"networkUtils.js","sourceRoot":"","sources":["../../src/utils/networkUtils.ts"],"names":[],"mappings":"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"}
@@ -21,12 +21,15 @@
21
21
  "cookie-session": "^2.1.1",
22
22
  "cors": "^2.8.5",
23
23
  "express": "^4.21.0",
24
+ "express-rate-limit": "^8.3.1",
25
+ "helmet": "^8.1.0",
24
26
  "i18next": "^24.2.3",
25
27
  "js-yaml": "^4.1.1",
26
28
  "multer": "^2.1.1",
27
29
  "node-pty": "^1.0.0",
28
30
  "simple-git": "^3.27.0",
29
31
  "socket.io": "^4.8.0",
32
+ "web-push": "^3.6.7",
30
33
  "zod": "^4.3.6"
31
34
  },
32
35
  "devDependencies": {
@@ -39,6 +42,7 @@
39
42
  "@types/multer": "^2.1.0",
40
43
  "@types/node": "^22.0.0",
41
44
  "@types/supertest": "^6.0.3",
45
+ "@types/web-push": "^3.6.4",
42
46
  "@vitest/coverage-v8": "^2.0.0",
43
47
  "supertest": "^7.2.2",
44
48
  "tsx": "^4.19.0",
@@ -26,6 +26,7 @@ export declare const ERROR_CODES: {
26
26
  readonly TIMEOUT_ERROR: "TIMEOUT_ERROR";
27
27
  /** Input validation error (e.g., invalid image) */
28
28
  readonly VALIDATION_ERROR: "VALIDATION_ERROR";
29
+ readonly CHAIN_MAX_EXCEEDED: "CHAIN_MAX_EXCEEDED";
29
30
  };
30
31
  export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
31
32
  //# sourceMappingURL=errorCodes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"errorCodes.d.ts","sourceRoot":"","sources":["../../src/constants/errorCodes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,eAAO,MAAM,WAAW;IACtB,oCAAoC;;IAEpC,yCAAyC;;IAEzC,2BAA2B;;IAE3B,gCAAgC;;IAEhC,wBAAwB;;IAExB,+BAA+B;;IAG/B,4BAA4B;;IAE5B,sDAAsD;;IAGtD,4BAA4B;;IAG5B,mDAAmD;;CAE3C,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC"}
1
+ {"version":3,"file":"errorCodes.d.ts","sourceRoot":"","sources":["../../src/constants/errorCodes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,eAAO,MAAM,WAAW;IACtB,oCAAoC;;IAEpC,yCAAyC;;IAEzC,2BAA2B;;IAE3B,gCAAgC;;IAEhC,wBAAwB;;IAExB,+BAA+B;;IAG/B,4BAA4B;;IAE5B,sDAAsD;;IAGtD,4BAA4B;;IAG5B,mDAAmD;;;CAI3C,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC"}
@@ -29,5 +29,7 @@ export const ERROR_CODES = {
29
29
  // Story 5.5: Image Attachment validation
30
30
  /** Input validation error (e.g., invalid image) */
31
31
  VALIDATION_ERROR: 'VALIDATION_ERROR',
32
+ // Story 24.1: Prompt chain max items exceeded
33
+ CHAIN_MAX_EXCEEDED: 'CHAIN_MAX_EXCEEDED',
32
34
  };
33
35
  //# sourceMappingURL=errorCodes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"errorCodes.js","sourceRoot":"","sources":["../../src/constants/errorCodes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,oCAAoC;IACpC,UAAU,EAAE,YAAY;IACxB,yCAAyC;IACzC,oBAAoB,EAAE,sBAAsB;IAC5C,2BAA2B;IAC3B,WAAW,EAAE,aAAa;IAC1B,gCAAgC;IAChC,mBAAmB,EAAE,qBAAqB;IAC1C,wBAAwB;IACxB,iBAAiB,EAAE,mBAAmB;IACtC,+BAA+B;IAC/B,aAAa,EAAE,eAAe;IAC9B,iDAAiD;IACjD,4BAA4B;IAC5B,mBAAmB,EAAE,qBAAqB;IAC1C,sDAAsD;IACtD,YAAY,EAAE,cAAc;IAC5B,gCAAgC;IAChC,4BAA4B;IAC5B,aAAa,EAAE,eAAe;IAC9B,yCAAyC;IACzC,mDAAmD;IACnD,gBAAgB,EAAE,kBAAkB;CAC5B,CAAC"}
1
+ {"version":3,"file":"errorCodes.js","sourceRoot":"","sources":["../../src/constants/errorCodes.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,oCAAoC;IACpC,UAAU,EAAE,YAAY;IACxB,yCAAyC;IACzC,oBAAoB,EAAE,sBAAsB;IAC5C,2BAA2B;IAC3B,WAAW,EAAE,aAAa;IAC1B,gCAAgC;IAChC,mBAAmB,EAAE,qBAAqB;IAC1C,wBAAwB;IACxB,iBAAiB,EAAE,mBAAmB;IACtC,+BAA+B;IAC/B,aAAa,EAAE,eAAe;IAC9B,iDAAiD;IACjD,4BAA4B;IAC5B,mBAAmB,EAAE,qBAAqB;IAC1C,sDAAsD;IACtD,YAAY,EAAE,cAAc;IAC5B,gCAAgC;IAChC,4BAA4B;IAC5B,aAAa,EAAE,eAAe;IAC9B,yCAAyC;IACzC,mDAAmD;IACnD,gBAAgB,EAAE,kBAAkB;IACpC,8CAA8C;IAC9C,kBAAkB,EAAE,oBAAoB;CAChC,CAAC"}
@@ -11,7 +11,7 @@ export { LogoutResponse, AuthStatus, AUTH_ERROR_CODES, SetupPasswordRequest, Set
11
11
  export { AUTH_ERROR_MESSAGES } from './types/auth.js';
12
12
  export { ProjectInfo, ProjectListResponse, PROJECT_ERRORS, CreateProjectRequest, CreateProjectResponse, DeleteProjectResponse, ValidatePathRequest, ValidatePathResponse, BmadVersionsResponse, ProjectSettings, UpdateProjectSettingsRequest, ProjectSettingsApiResponse, SetupBmadRequest, SetupBmadResponse, } from './types/project.js';
13
13
  export { SlashCommand, StarCommand, CommandsResponse } from './types/command.js';
14
- export { UserPreferences, PromptHistoryData, DEFAULT_PREFERENCES, PreferencesApiResponse, TelegramSettings, TelegramSettingsApiResponse, UpdateTelegramSettingsRequest, SupportedLanguage, SUPPORTED_LANGUAGES, } from './types/preferences.js';
14
+ export { UserPreferences, PromptHistoryData, DEFAULT_PREFERENCES, PreferencesApiResponse, TelegramSettings, TelegramSettingsApiResponse, UpdateTelegramSettingsRequest, WebPushSettings, WebPushSettingsApiResponse, WebPushSubscribeRequest, SupportedLanguage, SUPPORTED_LANGUAGES, PermissionSyncPolicy, } from './types/preferences.js';
15
15
  export { RawJSONLMessage, HistoryMessage, PaginationInfo, HistoryMessagesResponse, PaginationOptions, ContentBlock, TextContentBlock, ThinkingContentBlock, ToolUseContentBlock, ToolResultContentBlock, ImageContentBlock, } from './types/history.js';
16
16
  export * from './types/fileSystem.js';
17
17
  export * from './types/bmadStatus.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,gBAAgB,CAAC;AAG/B,cAAc,sBAAsB,CAAC;AAGrC,cAAc,sBAAsB,CAAC;AAGrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAI1C,cAAc,oBAAoB,CAAC;AAGnC,cAAc,gBAAgB,CAAC;AAG/B,OAAO,EACL,UAAU,EACV,wBAAwB,EACxB,mBAAmB,EACnB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,aAAa,EACb,sBAAsB,EACtB,YAAY,GACb,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAItD,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,4BAA4B,EAC5B,0BAA0B,EAC1B,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGjF,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,gBAAgB,EAChB,2BAA2B,EAC3B,6BAA6B,EAC7B,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,eAAe,EACf,cAAc,EACd,cAAc,EACd,uBAAuB,EACvB,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAG5B,cAAc,uBAAuB,CAAC;AAGtC,cAAc,uBAAuB,CAAC;AAGtC,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG1D,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAG/F,cAAc,gBAAgB,CAAC;AAG/B,cAAc,qBAAqB,CAAC;AAGpC,cAAc,sBAAsB,CAAC;AAGrC,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,gBAAgB,CAAC;AAG/B,cAAc,sBAAsB,CAAC;AAGrC,cAAc,sBAAsB,CAAC;AAGrC,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAI1C,cAAc,oBAAoB,CAAC;AAGnC,cAAc,gBAAgB,CAAC;AAG/B,OAAO,EACL,UAAU,EACV,wBAAwB,EACxB,mBAAmB,EACnB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,YAAY,EACZ,aAAa,EACb,aAAa,EACb,sBAAsB,EACtB,YAAY,GACb,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,cAAc,EACd,UAAU,EACV,gBAAgB,EAChB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAItD,OAAO,EACL,WAAW,EACX,mBAAmB,EACnB,cAAc,EACd,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,EACf,4BAA4B,EAC5B,0BAA0B,EAC1B,gBAAgB,EAChB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAI5B,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAGjF,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,sBAAsB,EACtB,gBAAgB,EAChB,2BAA2B,EAC3B,6BAA6B,EAC7B,eAAe,EACf,0BAA0B,EAC1B,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAGhC,OAAO,EACL,eAAe,EACf,cAAc,EACd,cAAc,EACd,uBAAuB,EACvB,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,sBAAsB,EACtB,iBAAiB,GAClB,MAAM,oBAAoB,CAAC;AAG5B,cAAc,uBAAuB,CAAC;AAGtC,cAAc,uBAAuB,CAAC;AAGtC,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG1D,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAG/F,cAAc,gBAAgB,CAAC;AAG/B,cAAc,qBAAqB,CAAC;AAGpC,cAAc,sBAAsB,CAAC;AAGrC,cAAc,kBAAkB,CAAC;AAGjC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,cAAc,gBAAgB,CAAC;AAE/B,+CAA+C;AAC/C,cAAc,sBAAsB,CAAC;AAErC,2CAA2C;AAC3C,cAAc,sBAAsB,CAAC;AAErC,4DAA4D;AAC5D,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAE1C,kDAAkD;AAClD,2FAA2F;AAC3F,cAAc,oBAAoB,CAAC;AAEnC,iDAAiD;AACjD,cAAc,gBAAgB,CAAC;AAE/B,mDAAmD;AACnD,OAAO,EAGL,mBAAmB,EACnB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAEzB,yBAAyB;AACzB,OAAO,EAKL,YAAY,GACb,MAAM,iBAAiB,CAAC;AAEzB,0CAA0C;AAC1C,OAAO,EAGL,gBAAgB,GAGjB,MAAM,iBAAiB,CAAC;AAEzB,iCAAiC;AACjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEtD,gDAAgD;AAChD,kDAAkD;AAClD,OAAO,EAGL,cAAc,GAYf,MAAM,oBAAoB,CAAC;AAM5B,gDAAgD;AAChD,OAAO,EAGL,mBAAmB,EAMnB,mBAAmB,GACpB,MAAM,wBAAwB,CAAC;AAiBhC,qEAAqE;AACrE,cAAc,uBAAuB,CAAC;AAEtC,uDAAuD;AACvD,cAAc,uBAAuB,CAAC;AAEtC,iCAAiC;AACjC,cAAc,kBAAkB,CAAC;AAEjC,kCAAkC;AAClC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,uCAAuC;AACvC,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAE/F,kCAAkC;AAClC,cAAc,gBAAgB,CAAC;AAE/B,2CAA2C;AAC3C,cAAc,qBAAqB,CAAC;AAEpC,8BAA8B;AAC9B,cAAc,sBAAsB,CAAC;AAErC,oCAAoC;AACpC,cAAc,kBAAkB,CAAC;AAEjC,kDAAkD;AAClD,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,cAAc,gBAAgB,CAAC;AAE/B,+CAA+C;AAC/C,cAAc,sBAAsB,CAAC;AAErC,2CAA2C;AAC3C,cAAc,sBAAsB,CAAC;AAErC,4DAA4D;AAC5D,cAAc,oBAAoB,CAAC;AACnC,cAAc,2BAA2B,CAAC;AAE1C,kDAAkD;AAClD,2FAA2F;AAC3F,cAAc,oBAAoB,CAAC;AAEnC,iDAAiD;AACjD,cAAc,gBAAgB,CAAC;AAE/B,mDAAmD;AACnD,OAAO,EAGL,mBAAmB,EACnB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AAEzB,yBAAyB;AACzB,OAAO,EAKL,YAAY,GACb,MAAM,iBAAiB,CAAC;AAEzB,0CAA0C;AAC1C,OAAO,EAGL,gBAAgB,GAGjB,MAAM,iBAAiB,CAAC;AAEzB,iCAAiC;AACjC,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEtD,gDAAgD;AAChD,kDAAkD;AAClD,OAAO,EAGL,cAAc,GAYf,MAAM,oBAAoB,CAAC;AAM5B,gDAAgD;AAChD,OAAO,EAGL,mBAAmB,EASnB,mBAAmB,GAEpB,MAAM,wBAAwB,CAAC;AAiBhC,qEAAqE;AACrE,cAAc,uBAAuB,CAAC;AAEtC,uDAAuD;AACvD,cAAc,uBAAuB,CAAC;AAEtC,iCAAiC;AACjC,cAAc,kBAAkB,CAAC;AAEjC,kCAAkC;AAClC,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE1D,uCAAuC;AACvC,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,+BAA+B,CAAC;AAE/F,kCAAkC;AAClC,cAAc,gBAAgB,CAAC;AAE/B,2CAA2C;AAC3C,cAAc,qBAAqB,CAAC;AAEpC,8BAA8B;AAC9B,cAAc,sBAAsB,CAAC;AAErC,oCAAoC;AACpC,cAAc,kBAAkB,CAAC;AAEjC,kDAAkD;AAClD,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC"}
@@ -50,6 +50,8 @@ export interface BmadStoryStatus {
50
50
  file: string;
51
51
  status: string;
52
52
  title?: string;
53
+ /** Latest QA gate decision: 'PASS' | 'CONCERNS' | 'FAIL' | 'WAIVED' */
54
+ gateResult?: string;
53
55
  }
54
56
  /** Epic with its stories */
55
57
  export interface BmadEpicStatus {