@stackmemoryai/stackmemory 0.5.19 → 0.5.20
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.
package/dist/hooks/schemas.js
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname as __pathDirname } from 'path';
|
|
|
3
3
|
const __filename = __fileURLToPath(import.meta.url);
|
|
4
4
|
const __dirname = __pathDirname(__filename);
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { logConfigInvalid } from "./security-logger.js";
|
|
6
7
|
const PromptOptionSchema = z.object({
|
|
7
8
|
key: z.string().max(10),
|
|
8
9
|
label: z.string().max(200),
|
|
@@ -71,10 +72,11 @@ function parseConfigSafe(schema, data, defaultValue, configName) {
|
|
|
71
72
|
if (result.success) {
|
|
72
73
|
return result.data;
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")
|
|
75
|
+
const errors = result.error.issues.map(
|
|
76
|
+
(i) => `${i.path.join(".")}: ${i.message}`
|
|
77
77
|
);
|
|
78
|
+
logConfigInvalid(configName, errors);
|
|
79
|
+
console.error(`[hooks] Invalid ${configName} config:`, errors.join(", "));
|
|
78
80
|
return defaultValue;
|
|
79
81
|
}
|
|
80
82
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/schemas.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * Zod schemas for hook configuration validation\n * Prevents malformed or malicious configs from being loaded\n */\n\nimport { z } from 'zod';\n\n// SMS/WhatsApp notification schemas\nexport const PromptOptionSchema = z.object({\n key: z.string().max(10),\n label: z.string().max(200),\n action: z.string().max(500).optional(),\n});\n\nexport const PendingPromptSchema = z.object({\n id: z.string().max(32),\n timestamp: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n message: z.string().max(1000),\n options: z.array(PromptOptionSchema).max(10),\n type: z.enum(['options', 'yesno', 'freeform']),\n callback: z.string().max(500).optional(),\n expiresAt: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n});\n\nexport const NotifyOnSchema = z.object({\n taskComplete: z.boolean(),\n reviewReady: z.boolean(),\n error: z.boolean(),\n custom: z.boolean(),\n});\n\nexport const QuietHoursSchema = z.object({\n enabled: z.boolean(),\n start: z.string().regex(/^\\d{2}:\\d{2}$/),\n end: z.string().regex(/^\\d{2}:\\d{2}$/),\n});\n\nexport const SMSConfigSchema = z.object({\n enabled: z.boolean(),\n channel: z.enum(['whatsapp', 'sms']),\n accountSid: z.string().max(100).optional(),\n authToken: z.string().max(100).optional(),\n smsFromNumber: z.string().max(20).optional(),\n smsToNumber: z.string().max(20).optional(),\n whatsappFromNumber: z.string().max(30).optional(),\n whatsappToNumber: z.string().max(30).optional(),\n fromNumber: z.string().max(20).optional(),\n toNumber: z.string().max(20).optional(),\n webhookUrl: z.string().url().max(500).optional(),\n notifyOn: NotifyOnSchema,\n quietHours: QuietHoursSchema.optional(),\n responseTimeout: z.number().int().min(30).max(3600),\n pendingPrompts: z.array(PendingPromptSchema).max(100),\n});\n\n// Action queue schemas\nexport const PendingActionSchema = z.object({\n id: z.string().max(32),\n promptId: z.string().max(32),\n response: z.string().max(1000),\n action: z.string().max(500),\n timestamp: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n status: z.enum(['pending', 'running', 'completed', 'failed']),\n result: z.string().max(10000).optional(),\n error: z.string().max(1000).optional(),\n});\n\nexport const ActionQueueSchema = z.object({\n actions: z.array(PendingActionSchema).max(1000),\n lastChecked: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n});\n\n// Auto-background config schema\nexport const AutoBackgroundConfigSchema = z.object({\n enabled: z.boolean(),\n timeoutMs: z.number().int().min(1000).max(600000),\n alwaysBackground: z.array(z.string().max(200)).max(100),\n neverBackground: z.array(z.string().max(200)).max(100),\n verbose: z.boolean().optional(),\n});\n\n// Type exports\nexport type SMSConfigValidated = z.infer<typeof SMSConfigSchema>;\nexport type ActionQueueValidated = z.infer<typeof ActionQueueSchema>;\nexport type AutoBackgroundConfigValidated = z.infer<\n typeof AutoBackgroundConfigSchema\n>;\n\n/**\n * Safely parse and validate config, returning default on failure\n */\nexport function parseConfigSafe<T>(\n schema: z.ZodSchema<T>,\n data: unknown,\n defaultValue: T,\n configName: string\n): T {\n const result = schema.safeParse(data);\n if (result.success) {\n return result.data;\n }\n
|
|
5
|
-
"mappings": ";;;;AAKA,SAAS,SAAS;
|
|
4
|
+
"sourcesContent": ["/**\n * Zod schemas for hook configuration validation\n * Prevents malformed or malicious configs from being loaded\n */\n\nimport { z } from 'zod';\nimport { logConfigInvalid } from './security-logger.js';\n\n// SMS/WhatsApp notification schemas\nexport const PromptOptionSchema = z.object({\n key: z.string().max(10),\n label: z.string().max(200),\n action: z.string().max(500).optional(),\n});\n\nexport const PendingPromptSchema = z.object({\n id: z.string().max(32),\n timestamp: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n message: z.string().max(1000),\n options: z.array(PromptOptionSchema).max(10),\n type: z.enum(['options', 'yesno', 'freeform']),\n callback: z.string().max(500).optional(),\n expiresAt: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n});\n\nexport const NotifyOnSchema = z.object({\n taskComplete: z.boolean(),\n reviewReady: z.boolean(),\n error: z.boolean(),\n custom: z.boolean(),\n});\n\nexport const QuietHoursSchema = z.object({\n enabled: z.boolean(),\n start: z.string().regex(/^\\d{2}:\\d{2}$/),\n end: z.string().regex(/^\\d{2}:\\d{2}$/),\n});\n\nexport const SMSConfigSchema = z.object({\n enabled: z.boolean(),\n channel: z.enum(['whatsapp', 'sms']),\n accountSid: z.string().max(100).optional(),\n authToken: z.string().max(100).optional(),\n smsFromNumber: z.string().max(20).optional(),\n smsToNumber: z.string().max(20).optional(),\n whatsappFromNumber: z.string().max(30).optional(),\n whatsappToNumber: z.string().max(30).optional(),\n fromNumber: z.string().max(20).optional(),\n toNumber: z.string().max(20).optional(),\n webhookUrl: z.string().url().max(500).optional(),\n notifyOn: NotifyOnSchema,\n quietHours: QuietHoursSchema.optional(),\n responseTimeout: z.number().int().min(30).max(3600),\n pendingPrompts: z.array(PendingPromptSchema).max(100),\n});\n\n// Action queue schemas\nexport const PendingActionSchema = z.object({\n id: z.string().max(32),\n promptId: z.string().max(32),\n response: z.string().max(1000),\n action: z.string().max(500),\n timestamp: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n status: z.enum(['pending', 'running', 'completed', 'failed']),\n result: z.string().max(10000).optional(),\n error: z.string().max(1000).optional(),\n});\n\nexport const ActionQueueSchema = z.object({\n actions: z.array(PendingActionSchema).max(1000),\n lastChecked: z\n .string()\n .datetime({ offset: true })\n .or(z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T/)),\n});\n\n// Auto-background config schema\nexport const AutoBackgroundConfigSchema = z.object({\n enabled: z.boolean(),\n timeoutMs: z.number().int().min(1000).max(600000),\n alwaysBackground: z.array(z.string().max(200)).max(100),\n neverBackground: z.array(z.string().max(200)).max(100),\n verbose: z.boolean().optional(),\n});\n\n// Type exports\nexport type SMSConfigValidated = z.infer<typeof SMSConfigSchema>;\nexport type ActionQueueValidated = z.infer<typeof ActionQueueSchema>;\nexport type AutoBackgroundConfigValidated = z.infer<\n typeof AutoBackgroundConfigSchema\n>;\n\n/**\n * Safely parse and validate config, returning default on failure\n */\nexport function parseConfigSafe<T>(\n schema: z.ZodSchema<T>,\n data: unknown,\n defaultValue: T,\n configName: string\n): T {\n const result = schema.safeParse(data);\n if (result.success) {\n return result.data;\n }\n const errors = result.error.issues.map(\n (i) => `${i.path.join('.')}: ${i.message}`\n );\n logConfigInvalid(configName, errors);\n console.error(`[hooks] Invalid ${configName} config:`, errors.join(', '));\n return defaultValue;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { fileURLToPath as __fileURLToPath } from 'url';
|
|
2
|
+
import { dirname as __pathDirname } from 'path';
|
|
3
|
+
const __filename = __fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = __pathDirname(__filename);
|
|
5
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { homedir } from "os";
|
|
8
|
+
import { ensureSecureDir } from "./secure-fs.js";
|
|
9
|
+
const LOG_DIR = join(homedir(), ".stackmemory", "logs");
|
|
10
|
+
const SECURITY_LOG = join(LOG_DIR, "security.log");
|
|
11
|
+
const MAX_LOG_ENTRIES = 1e4;
|
|
12
|
+
let logCount = 0;
|
|
13
|
+
function logSecurityEvent(type, source, message, details, ip) {
|
|
14
|
+
try {
|
|
15
|
+
ensureSecureDir(LOG_DIR);
|
|
16
|
+
const event = {
|
|
17
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18
|
+
type,
|
|
19
|
+
source,
|
|
20
|
+
message,
|
|
21
|
+
...details && { details },
|
|
22
|
+
...ip && { ip: maskIp(ip) }
|
|
23
|
+
};
|
|
24
|
+
const logLine = JSON.stringify(event) + "\n";
|
|
25
|
+
appendFileSync(SECURITY_LOG, logLine, { mode: 384 });
|
|
26
|
+
logCount++;
|
|
27
|
+
if (logCount > MAX_LOG_ENTRIES) {
|
|
28
|
+
rotateLog();
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function maskIp(ip) {
|
|
34
|
+
if (!ip) return "unknown";
|
|
35
|
+
if (ip === "::1" || ip === "::ffff:127.0.0.1") return "127.0.0.x";
|
|
36
|
+
const parts = ip.replace("::ffff:", "").split(".");
|
|
37
|
+
if (parts.length === 4) {
|
|
38
|
+
return `${parts[0]}.${parts[1]}.x.x`;
|
|
39
|
+
}
|
|
40
|
+
if (ip.includes(":")) {
|
|
41
|
+
const segments = ip.split(":");
|
|
42
|
+
if (segments.length >= 4) {
|
|
43
|
+
return segments.slice(0, 4).join(":") + ":x:x:x:x";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return "masked";
|
|
47
|
+
}
|
|
48
|
+
function rotateLog() {
|
|
49
|
+
try {
|
|
50
|
+
if (existsSync(SECURITY_LOG)) {
|
|
51
|
+
const content = readFileSync(SECURITY_LOG, "utf8");
|
|
52
|
+
const lines = content.trim().split("\n");
|
|
53
|
+
const keepLines = lines.slice(-MAX_LOG_ENTRIES / 2);
|
|
54
|
+
writeFileSync(SECURITY_LOG, keepLines.join("\n") + "\n", { mode: 384 });
|
|
55
|
+
logCount = keepLines.length;
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function logAuthSuccess(source, details) {
|
|
61
|
+
logSecurityEvent(
|
|
62
|
+
"auth_success",
|
|
63
|
+
source,
|
|
64
|
+
"Authentication successful",
|
|
65
|
+
details
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
function logAuthFailure(source, reason, ip, details) {
|
|
69
|
+
logSecurityEvent(
|
|
70
|
+
"auth_failure",
|
|
71
|
+
source,
|
|
72
|
+
`Authentication failed: ${reason}`,
|
|
73
|
+
details,
|
|
74
|
+
ip
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
function logRateLimit(source, ip) {
|
|
78
|
+
logSecurityEvent("rate_limit", source, "Rate limit exceeded", void 0, ip);
|
|
79
|
+
}
|
|
80
|
+
function logActionAllowed(source, action) {
|
|
81
|
+
logSecurityEvent(
|
|
82
|
+
"action_allowed",
|
|
83
|
+
source,
|
|
84
|
+
`Action executed: ${action.substring(0, 100)}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
function logActionBlocked(source, action, reason) {
|
|
88
|
+
logSecurityEvent("action_blocked", source, `Action blocked: ${reason}`, {
|
|
89
|
+
action: action.substring(0, 100)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function logConfigInvalid(source, errors) {
|
|
93
|
+
logSecurityEvent("config_invalid", source, "Invalid config rejected", {
|
|
94
|
+
errors: errors.slice(0, 5)
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function logWebhookRequest(source, method, path, ip) {
|
|
98
|
+
logSecurityEvent(
|
|
99
|
+
"webhook_request",
|
|
100
|
+
source,
|
|
101
|
+
`${method} ${path}`,
|
|
102
|
+
void 0,
|
|
103
|
+
ip
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
function logSignatureInvalid(source, ip) {
|
|
107
|
+
logSecurityEvent(
|
|
108
|
+
"signature_invalid",
|
|
109
|
+
source,
|
|
110
|
+
"Invalid request signature",
|
|
111
|
+
void 0,
|
|
112
|
+
ip
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
function logBodyTooLarge(source, size, ip) {
|
|
116
|
+
logSecurityEvent(
|
|
117
|
+
"body_too_large",
|
|
118
|
+
source,
|
|
119
|
+
`Request body too large: ${size} bytes`,
|
|
120
|
+
void 0,
|
|
121
|
+
ip
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
function logContentTypeInvalid(source, contentType, ip) {
|
|
125
|
+
logSecurityEvent(
|
|
126
|
+
"content_type_invalid",
|
|
127
|
+
source,
|
|
128
|
+
`Invalid content type: ${contentType}`,
|
|
129
|
+
void 0,
|
|
130
|
+
ip
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
function logCleanup(source, expiredPrompts, oldActions) {
|
|
134
|
+
if (expiredPrompts > 0 || oldActions > 0) {
|
|
135
|
+
logSecurityEvent("cleanup", source, "Cleanup completed", {
|
|
136
|
+
expiredPrompts,
|
|
137
|
+
oldActions
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export {
|
|
142
|
+
logActionAllowed,
|
|
143
|
+
logActionBlocked,
|
|
144
|
+
logAuthFailure,
|
|
145
|
+
logAuthSuccess,
|
|
146
|
+
logBodyTooLarge,
|
|
147
|
+
logCleanup,
|
|
148
|
+
logConfigInvalid,
|
|
149
|
+
logContentTypeInvalid,
|
|
150
|
+
logRateLimit,
|
|
151
|
+
logSecurityEvent,
|
|
152
|
+
logSignatureInvalid,
|
|
153
|
+
logWebhookRequest
|
|
154
|
+
};
|
|
155
|
+
//# sourceMappingURL=security-logger.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/hooks/security-logger.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Security Event Logger for hooks\n * Logs security-relevant events for audit trail\n */\n\nimport { appendFileSync, existsSync, readFileSync, writeFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { ensureSecureDir } from './secure-fs.js';\n\nconst LOG_DIR = join(homedir(), '.stackmemory', 'logs');\nconst SECURITY_LOG = join(LOG_DIR, 'security.log');\nconst MAX_LOG_ENTRIES = 10000;\n\nexport type SecurityEventType =\n | 'auth_success'\n | 'auth_failure'\n | 'rate_limit'\n | 'action_allowed'\n | 'action_blocked'\n | 'config_invalid'\n | 'config_loaded'\n | 'webhook_request'\n | 'signature_invalid'\n | 'body_too_large'\n | 'content_type_invalid'\n | 'cleanup';\n\nexport interface SecurityEvent {\n timestamp: string;\n type: SecurityEventType;\n source: string;\n message: string;\n details?: Record<string, unknown>;\n ip?: string;\n}\n\nlet logCount = 0;\n\n/**\n * Log a security event\n */\nexport function logSecurityEvent(\n type: SecurityEventType,\n source: string,\n message: string,\n details?: Record<string, unknown>,\n ip?: string\n): void {\n try {\n ensureSecureDir(LOG_DIR);\n\n const event: SecurityEvent = {\n timestamp: new Date().toISOString(),\n type,\n source,\n message,\n ...(details && { details }),\n ...(ip && { ip: maskIp(ip) }),\n };\n\n const logLine = JSON.stringify(event) + '\\n';\n appendFileSync(SECURITY_LOG, logLine, { mode: 0o600 });\n\n logCount++;\n\n // Rotate log if too large (simple rotation - truncate)\n if (logCount > MAX_LOG_ENTRIES) {\n rotateLog();\n }\n } catch {\n // Don't let logging failures break the application\n }\n}\n\n/**\n * Mask IP address for privacy (keep first two octets)\n */\nfunction maskIp(ip: string): string {\n if (!ip) return 'unknown';\n\n // Handle IPv6 localhost\n if (ip === '::1' || ip === '::ffff:127.0.0.1') return '127.0.0.x';\n\n // Handle IPv4\n const parts = ip.replace('::ffff:', '').split('.');\n if (parts.length === 4) {\n return `${parts[0]}.${parts[1]}.x.x`;\n }\n\n // Handle IPv6 - mask last 64 bits\n if (ip.includes(':')) {\n const segments = ip.split(':');\n if (segments.length >= 4) {\n return segments.slice(0, 4).join(':') + ':x:x:x:x';\n }\n }\n\n return 'masked';\n}\n\n/**\n * Simple log rotation - keep last half of entries\n */\nfunction rotateLog(): void {\n try {\n if (existsSync(SECURITY_LOG)) {\n const content = readFileSync(SECURITY_LOG, 'utf8');\n const lines = content.trim().split('\\n');\n const keepLines = lines.slice(-MAX_LOG_ENTRIES / 2);\n writeFileSync(SECURITY_LOG, keepLines.join('\\n') + '\\n', { mode: 0o600 });\n logCount = keepLines.length;\n }\n } catch {\n // Ignore rotation errors\n }\n}\n\n// Convenience functions for common events\n\nexport function logAuthSuccess(\n source: string,\n details?: Record<string, unknown>\n): void {\n logSecurityEvent(\n 'auth_success',\n source,\n 'Authentication successful',\n details\n );\n}\n\nexport function logAuthFailure(\n source: string,\n reason: string,\n ip?: string,\n details?: Record<string, unknown>\n): void {\n logSecurityEvent(\n 'auth_failure',\n source,\n `Authentication failed: ${reason}`,\n details,\n ip\n );\n}\n\nexport function logRateLimit(source: string, ip: string): void {\n logSecurityEvent('rate_limit', source, 'Rate limit exceeded', undefined, ip);\n}\n\nexport function logActionAllowed(source: string, action: string): void {\n logSecurityEvent(\n 'action_allowed',\n source,\n `Action executed: ${action.substring(0, 100)}`\n );\n}\n\nexport function logActionBlocked(\n source: string,\n action: string,\n reason: string\n): void {\n logSecurityEvent('action_blocked', source, `Action blocked: ${reason}`, {\n action: action.substring(0, 100),\n });\n}\n\nexport function logConfigInvalid(source: string, errors: string[]): void {\n logSecurityEvent('config_invalid', source, 'Invalid config rejected', {\n errors: errors.slice(0, 5),\n });\n}\n\nexport function logWebhookRequest(\n source: string,\n method: string,\n path: string,\n ip?: string\n): void {\n logSecurityEvent(\n 'webhook_request',\n source,\n `${method} ${path}`,\n undefined,\n ip\n );\n}\n\nexport function logSignatureInvalid(source: string, ip?: string): void {\n logSecurityEvent(\n 'signature_invalid',\n source,\n 'Invalid request signature',\n undefined,\n ip\n );\n}\n\nexport function logBodyTooLarge(\n source: string,\n size: number,\n ip?: string\n): void {\n logSecurityEvent(\n 'body_too_large',\n source,\n `Request body too large: ${size} bytes`,\n undefined,\n ip\n );\n}\n\nexport function logContentTypeInvalid(\n source: string,\n contentType: string,\n ip?: string\n): void {\n logSecurityEvent(\n 'content_type_invalid',\n source,\n `Invalid content type: ${contentType}`,\n undefined,\n ip\n );\n}\n\nexport function logCleanup(\n source: string,\n expiredPrompts: number,\n oldActions: number\n): void {\n if (expiredPrompts > 0 || oldActions > 0) {\n logSecurityEvent('cleanup', source, 'Cleanup completed', {\n expiredPrompts,\n oldActions,\n });\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -20,7 +20,19 @@ import {
|
|
|
20
20
|
cleanupOldActions
|
|
21
21
|
} from "./sms-action-runner.js";
|
|
22
22
|
import { writeFileSecure, ensureSecureDir } from "./secure-fs.js";
|
|
23
|
+
import {
|
|
24
|
+
logWebhookRequest,
|
|
25
|
+
logRateLimit,
|
|
26
|
+
logSignatureInvalid,
|
|
27
|
+
logBodyTooLarge,
|
|
28
|
+
logContentTypeInvalid,
|
|
29
|
+
logActionAllowed,
|
|
30
|
+
logActionBlocked,
|
|
31
|
+
logCleanup
|
|
32
|
+
} from "./security-logger.js";
|
|
23
33
|
const CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
34
|
+
const MAX_SMS_BODY_LENGTH = 1e3;
|
|
35
|
+
const MAX_PHONE_LENGTH = 20;
|
|
24
36
|
const MAX_BODY_SIZE = 50 * 1024;
|
|
25
37
|
const RATE_LIMIT_WINDOW_MS = 60 * 1e3;
|
|
26
38
|
const RATE_LIMIT_MAX_REQUESTS = 30;
|
|
@@ -83,6 +95,14 @@ function storeLatestResponse(promptId, response, action) {
|
|
|
83
95
|
}
|
|
84
96
|
function handleSMSWebhook(payload) {
|
|
85
97
|
const { From, Body } = payload;
|
|
98
|
+
if (Body && Body.length > MAX_SMS_BODY_LENGTH) {
|
|
99
|
+
console.log(`[sms-webhook] Body too long: ${Body.length} chars`);
|
|
100
|
+
return { response: "Message too long. Max 1000 characters." };
|
|
101
|
+
}
|
|
102
|
+
if (From && From.length > MAX_PHONE_LENGTH) {
|
|
103
|
+
console.log(`[sms-webhook] Invalid phone number length`);
|
|
104
|
+
return { response: "Invalid phone number." };
|
|
105
|
+
}
|
|
86
106
|
console.log(`[sms-webhook] Received from ${From}: ${Body}`);
|
|
87
107
|
const result = processIncomingResponse(From, Body);
|
|
88
108
|
if (!result.matched) {
|
|
@@ -106,6 +126,7 @@ function handleSMSWebhook(payload) {
|
|
|
106
126
|
result.response || Body
|
|
107
127
|
);
|
|
108
128
|
if (actionResult.success) {
|
|
129
|
+
logActionAllowed("sms-webhook", result.action);
|
|
109
130
|
console.log(
|
|
110
131
|
`[sms-webhook] Action completed: ${(actionResult.output || "").substring(0, 200)}`
|
|
111
132
|
);
|
|
@@ -115,6 +136,11 @@ function handleSMSWebhook(payload) {
|
|
|
115
136
|
queued: false
|
|
116
137
|
};
|
|
117
138
|
} else {
|
|
139
|
+
logActionBlocked(
|
|
140
|
+
"sms-webhook",
|
|
141
|
+
result.action,
|
|
142
|
+
actionResult.error || "unknown"
|
|
143
|
+
);
|
|
118
144
|
console.log(`[sms-webhook] Action failed: ${actionResult.error}`);
|
|
119
145
|
queueAction(
|
|
120
146
|
result.prompt?.id || "unknown",
|
|
@@ -185,7 +211,14 @@ function startWebhookServer(port = 3456) {
|
|
|
185
211
|
}
|
|
186
212
|
if ((url.pathname === "/sms" || url.pathname === "/sms/incoming" || url.pathname === "/webhook") && req.method === "POST") {
|
|
187
213
|
const clientIp = req.socket.remoteAddress || "unknown";
|
|
214
|
+
logWebhookRequest(
|
|
215
|
+
"sms-webhook",
|
|
216
|
+
req.method || "POST",
|
|
217
|
+
url.pathname || "/sms",
|
|
218
|
+
clientIp
|
|
219
|
+
);
|
|
188
220
|
if (!checkRateLimit(clientIp)) {
|
|
221
|
+
logRateLimit("sms-webhook", clientIp);
|
|
189
222
|
res.writeHead(429, {
|
|
190
223
|
"Content-Type": "text/xml",
|
|
191
224
|
"Retry-After": "60"
|
|
@@ -195,6 +228,7 @@ function startWebhookServer(port = 3456) {
|
|
|
195
228
|
}
|
|
196
229
|
const contentType = req.headers["content-type"] || "";
|
|
197
230
|
if (!contentType.includes("application/x-www-form-urlencoded")) {
|
|
231
|
+
logContentTypeInvalid("sms-webhook", contentType, clientIp);
|
|
198
232
|
res.writeHead(400, { "Content-Type": "text/xml" });
|
|
199
233
|
res.end(twimlResponse("Invalid content type"));
|
|
200
234
|
return;
|
|
@@ -205,6 +239,7 @@ function startWebhookServer(port = 3456) {
|
|
|
205
239
|
body += chunk;
|
|
206
240
|
if (body.length > MAX_BODY_SIZE) {
|
|
207
241
|
bodyTooLarge = true;
|
|
242
|
+
logBodyTooLarge("sms-webhook", body.length, clientIp);
|
|
208
243
|
req.destroy();
|
|
209
244
|
}
|
|
210
245
|
});
|
|
@@ -225,6 +260,7 @@ function startWebhookServer(port = 3456) {
|
|
|
225
260
|
payload,
|
|
226
261
|
twilioSignature
|
|
227
262
|
)) {
|
|
263
|
+
logSignatureInvalid("sms-webhook", clientIp);
|
|
228
264
|
console.error("[sms-webhook] Invalid Twilio signature");
|
|
229
265
|
res.writeHead(401, { "Content-Type": "text/xml" });
|
|
230
266
|
res.end(twimlResponse("Unauthorized"));
|
|
@@ -299,6 +335,7 @@ function startWebhookServer(port = 3456) {
|
|
|
299
335
|
const expiredPrompts = cleanupExpiredPrompts();
|
|
300
336
|
const oldActions = cleanupOldActions();
|
|
301
337
|
if (expiredPrompts > 0 || oldActions > 0) {
|
|
338
|
+
logCleanup("sms-webhook", expiredPrompts, oldActions);
|
|
302
339
|
console.log(
|
|
303
340
|
`[sms-webhook] Cleanup: ${expiredPrompts} expired prompts, ${oldActions} old actions`
|
|
304
341
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/hooks/sms-webhook.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * SMS Webhook Handler for receiving Twilio responses\n * Can run as standalone server or integrate with existing Express app\n *\n * Security features:\n * - Twilio signature verification\n * - Rate limiting per IP\n * - Body size limits\n * - Content-type validation\n * - Safe action execution (no shell injection)\n */\n\nimport { createServer, IncomingMessage, ServerResponse } from 'http';\nimport { parse as parseUrl } from 'url';\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { createHmac } from 'crypto';\nimport { execFileSync } from 'child_process';\nimport {\n processIncomingResponse,\n loadSMSConfig,\n cleanupExpiredPrompts,\n} from './sms-notify.js';\nimport {\n queueAction,\n executeActionSafe,\n cleanupOldActions,\n} from './sms-action-runner.js';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\n\n// Cleanup interval (5 minutes)\nconst CLEANUP_INTERVAL_MS = 5 * 60 * 1000;\n\n// Security constants\nconst MAX_BODY_SIZE = 50 * 1024; // 50KB max body\nconst RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute\nconst RATE_LIMIT_MAX_REQUESTS = 30; // 30 requests per minute per IP\n\n// Rate limiting store (in production, use Redis)\nconst rateLimitStore = new Map<string, { count: number; resetTime: number }>();\n\nfunction checkRateLimit(ip: string): boolean {\n const now = Date.now();\n const record = rateLimitStore.get(ip);\n\n if (!record || now > record.resetTime) {\n rateLimitStore.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });\n return true;\n }\n\n if (record.count >= RATE_LIMIT_MAX_REQUESTS) {\n return false;\n }\n\n record.count++;\n return true;\n}\n\n// Twilio signature verification\nfunction verifyTwilioSignature(\n url: string,\n params: Record<string, string>,\n signature: string\n): boolean {\n const authToken = process.env['TWILIO_AUTH_TOKEN'];\n if (!authToken) {\n console.warn(\n '[sms-webhook] TWILIO_AUTH_TOKEN not set, skipping signature verification'\n );\n return true; // Allow in development, but log warning\n }\n\n // Build the data string (URL + sorted params)\n const sortedKeys = Object.keys(params).sort();\n let data = url;\n for (const key of sortedKeys) {\n data += key + params[key];\n }\n\n // Calculate expected signature\n const hmac = createHmac('sha1', authToken);\n hmac.update(data);\n const expectedSignature = hmac.digest('base64');\n\n return signature === expectedSignature;\n}\n\ninterface TwilioWebhookPayload {\n From: string;\n To: string;\n Body: string;\n MessageSid: string;\n}\n\nfunction parseFormData(body: string): Record<string, string> {\n const params = new URLSearchParams(body);\n const result: Record<string, string> = {};\n params.forEach((value, key) => {\n result[key] = value;\n });\n return result;\n}\n\n// Store response for Claude hook to pick up\nfunction storeLatestResponse(\n promptId: string,\n response: string,\n action?: string\n): void {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n const responsePath = join(\n homedir(),\n '.stackmemory',\n 'sms-latest-response.json'\n );\n writeFileSecure(\n responsePath,\n JSON.stringify({\n promptId,\n response,\n action,\n timestamp: new Date().toISOString(),\n })\n );\n}\n\nexport function handleSMSWebhook(payload: TwilioWebhookPayload): {\n response: string;\n action?: string;\n queued?: boolean;\n} {\n const { From, Body } = payload;\n\n console.log(`[sms-webhook] Received from ${From}: ${Body}`);\n\n const result = processIncomingResponse(From, Body);\n\n if (!result.matched) {\n if (result.prompt) {\n return {\n response: `Invalid response. Expected: ${result.prompt.options.map((o) => o.key).join(', ')}`,\n };\n }\n return { response: 'No pending prompt found.' };\n }\n\n // Store response for Claude hook\n storeLatestResponse(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n // Trigger notification to alert user/Claude\n triggerResponseNotification(result.response || Body);\n\n // Execute action safely if present (no shell injection)\n if (result.action) {\n console.log(`[sms-webhook] Executing action: ${result.action}`);\n\n const actionResult = executeActionSafe(\n result.action,\n result.response || Body\n );\n\n if (actionResult.success) {\n console.log(\n `[sms-webhook] Action completed: ${(actionResult.output || '').substring(0, 200)}`\n );\n\n return {\n response: `Done! Action executed successfully.`,\n action: result.action,\n queued: false,\n };\n } else {\n console.log(`[sms-webhook] Action failed: ${actionResult.error}`);\n\n // Queue for retry\n queueAction(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n return {\n response: `Action failed, queued for retry: ${(actionResult.error || '').substring(0, 50)}`,\n action: result.action,\n queued: true,\n };\n }\n }\n\n return {\n response: `Received: ${result.response}. Next action will be triggered.`,\n };\n}\n\n// Escape string for AppleScript (prevent injection)\nfunction escapeAppleScript(str: string): string {\n return str\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .substring(0, 200); // Limit length\n}\n\n// Trigger notification when response received\nfunction triggerResponseNotification(response: string): void {\n const safeMessage = escapeAppleScript(`SMS Response: ${response}`);\n\n // macOS notification using execFile (safer than execSync with shell)\n try {\n execFileSync(\n 'osascript',\n [\n '-e',\n `display notification \"${safeMessage}\" with title \"StackMemory\" sound name \"Glass\"`,\n ],\n { stdio: 'ignore', timeout: 5000 }\n );\n } catch {\n // Ignore if not on macOS\n }\n\n // Write signal file for other processes\n try {\n const signalPath = join(homedir(), '.stackmemory', 'sms-signal.txt');\n writeFileSecure(\n signalPath,\n JSON.stringify({\n type: 'sms_response',\n response,\n timestamp: new Date().toISOString(),\n })\n );\n } catch {\n // Ignore\n }\n\n console.log(`\\n*** SMS RESPONSE RECEIVED: \"${response}\" ***`);\n console.log(`*** Run: stackmemory notify run-actions ***\\n`);\n}\n\n// TwiML response helper\nfunction twimlResponse(message: string): string {\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Message>${escapeXml(message)}</Message>\n</Response>`;\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n// Standalone webhook server\nexport function startWebhookServer(port: number = 3456): void {\n const server = createServer(\n async (req: IncomingMessage, res: ServerResponse) => {\n const url = parseUrl(req.url || '/', true);\n\n // Health check\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok' }));\n return;\n }\n\n // SMS webhook endpoint (incoming messages)\n if (\n (url.pathname === '/sms' ||\n url.pathname === '/sms/incoming' ||\n url.pathname === '/webhook') &&\n req.method === 'POST'\n ) {\n // Rate limiting\n const clientIp = req.socket.remoteAddress || 'unknown';\n if (!checkRateLimit(clientIp)) {\n res.writeHead(429, {\n 'Content-Type': 'text/xml',\n 'Retry-After': '60',\n });\n res.end(twimlResponse('Too many requests. Please try again later.'));\n return;\n }\n\n // Content-type validation\n const contentType = req.headers['content-type'] || '';\n if (!contentType.includes('application/x-www-form-urlencoded')) {\n res.writeHead(400, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Invalid content type'));\n return;\n }\n\n let body = '';\n let bodyTooLarge = false;\n\n req.on('data', (chunk) => {\n body += chunk;\n // Body size limit\n if (body.length > MAX_BODY_SIZE) {\n bodyTooLarge = true;\n req.destroy();\n }\n });\n\n req.on('end', () => {\n if (bodyTooLarge) {\n res.writeHead(413, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Request too large'));\n return;\n }\n\n try {\n const payload = parseFormData(\n body\n ) as unknown as TwilioWebhookPayload;\n\n // Verify Twilio signature\n const twilioSignature = req.headers['x-twilio-signature'] as string;\n const webhookUrl = `${req.headers['x-forwarded-proto'] || 'http'}://${req.headers.host}${req.url}`;\n\n if (\n twilioSignature &&\n !verifyTwilioSignature(\n webhookUrl,\n payload as unknown as Record<string, string>,\n twilioSignature\n )\n ) {\n console.error('[sms-webhook] Invalid Twilio signature');\n res.writeHead(401, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Unauthorized'));\n return;\n }\n\n const result = handleSMSWebhook(payload);\n\n res.writeHead(200, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse(result.response));\n } catch (err) {\n console.error('[sms-webhook] Error:', err);\n res.writeHead(500, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Error processing message'));\n }\n });\n return;\n }\n\n // Status callback endpoint (delivery status updates)\n if (url.pathname === '/sms/status' && req.method === 'POST') {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk;\n });\n\n req.on('end', () => {\n try {\n const payload = parseFormData(body);\n console.log(\n `[sms-webhook] Status update: ${payload['MessageSid']} -> ${payload['MessageStatus']}`\n );\n\n // Store status for tracking\n const statusPath = join(\n homedir(),\n '.stackmemory',\n 'sms-status.json'\n );\n const statuses: Record<string, string> = existsSync(statusPath)\n ? JSON.parse(readFileSync(statusPath, 'utf8'))\n : {};\n statuses[payload['MessageSid']] = payload['MessageStatus'];\n writeFileSecure(statusPath, JSON.stringify(statuses, null, 2));\n\n res.writeHead(200, { 'Content-Type': 'text/plain' });\n res.end('OK');\n } catch (err) {\n console.error('[sms-webhook] Status error:', err);\n res.writeHead(500);\n res.end('Error');\n }\n });\n return;\n }\n\n // Server status endpoint\n if (url.pathname === '/status') {\n const config = loadSMSConfig();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n enabled: config.enabled,\n pendingPrompts: config.pendingPrompts.length,\n })\n );\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n }\n );\n\n server.listen(port, () => {\n console.log(`[sms-webhook] Server listening on port ${port}`);\n console.log(\n `[sms-webhook] Incoming messages: http://localhost:${port}/sms/incoming`\n );\n console.log(\n `[sms-webhook] Status callback: http://localhost:${port}/sms/status`\n );\n console.log(`[sms-webhook] Configure these URLs in Twilio console`);\n\n // Start timed cleanup of expired prompts and old actions\n setInterval(() => {\n try {\n const expiredPrompts = cleanupExpiredPrompts();\n const oldActions = cleanupOldActions();\n if (expiredPrompts > 0 || oldActions > 0) {\n console.log(\n `[sms-webhook] Cleanup: ${expiredPrompts} expired prompts, ${oldActions} old actions`\n );\n }\n } catch {\n // Ignore cleanup errors\n }\n }, CLEANUP_INTERVAL_MS);\n console.log(\n `[sms-webhook] Cleanup interval: every ${CLEANUP_INTERVAL_MS / 1000}s`\n );\n });\n}\n\n// Express middleware for integration\nexport function smsWebhookMiddleware(\n req: { body: TwilioWebhookPayload },\n res: { type: (t: string) => void; send: (s: string) => void }\n): void {\n const result = handleSMSWebhook(req.body);\n res.type('text/xml');\n res.send(twimlResponse(result.response));\n}\n\n// CLI entry\nif (process.argv[1]?.endsWith('sms-webhook.js')) {\n const port = parseInt(process.env['SMS_WEBHOOK_PORT'] || '3456', 10);\n startWebhookServer(port);\n}\n"],
|
|
5
|
-
"mappings": ";;;;AAYA,SAAS,oBAAqD;AAC9D,SAAS,SAAS,gBAAgB;AAClC,SAAS,YAAY,oBAAoB;AACzC,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAC3B,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,iBAAiB,uBAAuB;
|
|
4
|
+
"sourcesContent": ["/**\n * SMS Webhook Handler for receiving Twilio responses\n * Can run as standalone server or integrate with existing Express app\n *\n * Security features:\n * - Twilio signature verification\n * - Rate limiting per IP\n * - Body size limits\n * - Content-type validation\n * - Safe action execution (no shell injection)\n */\n\nimport { createServer, IncomingMessage, ServerResponse } from 'http';\nimport { parse as parseUrl } from 'url';\nimport { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { createHmac } from 'crypto';\nimport { execFileSync } from 'child_process';\nimport {\n processIncomingResponse,\n loadSMSConfig,\n cleanupExpiredPrompts,\n} from './sms-notify.js';\nimport {\n queueAction,\n executeActionSafe,\n cleanupOldActions,\n} from './sms-action-runner.js';\nimport { writeFileSecure, ensureSecureDir } from './secure-fs.js';\nimport {\n logWebhookRequest,\n logRateLimit,\n logSignatureInvalid,\n logBodyTooLarge,\n logContentTypeInvalid,\n logActionAllowed,\n logActionBlocked,\n logCleanup,\n} from './security-logger.js';\n\n// Cleanup interval (5 minutes)\nconst CLEANUP_INTERVAL_MS = 5 * 60 * 1000;\n\n// Input validation constants\nconst MAX_SMS_BODY_LENGTH = 1000;\nconst MAX_PHONE_LENGTH = 20;\n\n// Security constants\nconst MAX_BODY_SIZE = 50 * 1024; // 50KB max body\nconst RATE_LIMIT_WINDOW_MS = 60 * 1000; // 1 minute\nconst RATE_LIMIT_MAX_REQUESTS = 30; // 30 requests per minute per IP\n\n// Rate limiting store (in production, use Redis)\nconst rateLimitStore = new Map<string, { count: number; resetTime: number }>();\n\nfunction checkRateLimit(ip: string): boolean {\n const now = Date.now();\n const record = rateLimitStore.get(ip);\n\n if (!record || now > record.resetTime) {\n rateLimitStore.set(ip, { count: 1, resetTime: now + RATE_LIMIT_WINDOW_MS });\n return true;\n }\n\n if (record.count >= RATE_LIMIT_MAX_REQUESTS) {\n return false;\n }\n\n record.count++;\n return true;\n}\n\n// Twilio signature verification\nfunction verifyTwilioSignature(\n url: string,\n params: Record<string, string>,\n signature: string\n): boolean {\n const authToken = process.env['TWILIO_AUTH_TOKEN'];\n if (!authToken) {\n console.warn(\n '[sms-webhook] TWILIO_AUTH_TOKEN not set, skipping signature verification'\n );\n return true; // Allow in development, but log warning\n }\n\n // Build the data string (URL + sorted params)\n const sortedKeys = Object.keys(params).sort();\n let data = url;\n for (const key of sortedKeys) {\n data += key + params[key];\n }\n\n // Calculate expected signature\n const hmac = createHmac('sha1', authToken);\n hmac.update(data);\n const expectedSignature = hmac.digest('base64');\n\n return signature === expectedSignature;\n}\n\ninterface TwilioWebhookPayload {\n From: string;\n To: string;\n Body: string;\n MessageSid: string;\n}\n\nfunction parseFormData(body: string): Record<string, string> {\n const params = new URLSearchParams(body);\n const result: Record<string, string> = {};\n params.forEach((value, key) => {\n result[key] = value;\n });\n return result;\n}\n\n// Store response for Claude hook to pick up\nfunction storeLatestResponse(\n promptId: string,\n response: string,\n action?: string\n): void {\n ensureSecureDir(join(homedir(), '.stackmemory'));\n const responsePath = join(\n homedir(),\n '.stackmemory',\n 'sms-latest-response.json'\n );\n writeFileSecure(\n responsePath,\n JSON.stringify({\n promptId,\n response,\n action,\n timestamp: new Date().toISOString(),\n })\n );\n}\n\nexport function handleSMSWebhook(payload: TwilioWebhookPayload): {\n response: string;\n action?: string;\n queued?: boolean;\n} {\n const { From, Body } = payload;\n\n // Input length validation\n if (Body && Body.length > MAX_SMS_BODY_LENGTH) {\n console.log(`[sms-webhook] Body too long: ${Body.length} chars`);\n return { response: 'Message too long. Max 1000 characters.' };\n }\n\n if (From && From.length > MAX_PHONE_LENGTH) {\n console.log(`[sms-webhook] Invalid phone number length`);\n return { response: 'Invalid phone number.' };\n }\n\n console.log(`[sms-webhook] Received from ${From}: ${Body}`);\n\n const result = processIncomingResponse(From, Body);\n\n if (!result.matched) {\n if (result.prompt) {\n return {\n response: `Invalid response. Expected: ${result.prompt.options.map((o) => o.key).join(', ')}`,\n };\n }\n return { response: 'No pending prompt found.' };\n }\n\n // Store response for Claude hook\n storeLatestResponse(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n // Trigger notification to alert user/Claude\n triggerResponseNotification(result.response || Body);\n\n // Execute action safely if present (no shell injection)\n if (result.action) {\n console.log(`[sms-webhook] Executing action: ${result.action}`);\n\n const actionResult = executeActionSafe(\n result.action,\n result.response || Body\n );\n\n if (actionResult.success) {\n logActionAllowed('sms-webhook', result.action);\n console.log(\n `[sms-webhook] Action completed: ${(actionResult.output || '').substring(0, 200)}`\n );\n\n return {\n response: `Done! Action executed successfully.`,\n action: result.action,\n queued: false,\n };\n } else {\n logActionBlocked(\n 'sms-webhook',\n result.action,\n actionResult.error || 'unknown'\n );\n console.log(`[sms-webhook] Action failed: ${actionResult.error}`);\n\n // Queue for retry\n queueAction(\n result.prompt?.id || 'unknown',\n result.response || Body,\n result.action\n );\n\n return {\n response: `Action failed, queued for retry: ${(actionResult.error || '').substring(0, 50)}`,\n action: result.action,\n queued: true,\n };\n }\n }\n\n return {\n response: `Received: ${result.response}. Next action will be triggered.`,\n };\n}\n\n// Escape string for AppleScript (prevent injection)\nfunction escapeAppleScript(str: string): string {\n return str\n .replace(/\\\\/g, '\\\\\\\\')\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, '\\\\n')\n .replace(/\\r/g, '\\\\r')\n .substring(0, 200); // Limit length\n}\n\n// Trigger notification when response received\nfunction triggerResponseNotification(response: string): void {\n const safeMessage = escapeAppleScript(`SMS Response: ${response}`);\n\n // macOS notification using execFile (safer than execSync with shell)\n try {\n execFileSync(\n 'osascript',\n [\n '-e',\n `display notification \"${safeMessage}\" with title \"StackMemory\" sound name \"Glass\"`,\n ],\n { stdio: 'ignore', timeout: 5000 }\n );\n } catch {\n // Ignore if not on macOS\n }\n\n // Write signal file for other processes\n try {\n const signalPath = join(homedir(), '.stackmemory', 'sms-signal.txt');\n writeFileSecure(\n signalPath,\n JSON.stringify({\n type: 'sms_response',\n response,\n timestamp: new Date().toISOString(),\n })\n );\n } catch {\n // Ignore\n }\n\n console.log(`\\n*** SMS RESPONSE RECEIVED: \"${response}\" ***`);\n console.log(`*** Run: stackmemory notify run-actions ***\\n`);\n}\n\n// TwiML response helper\nfunction twimlResponse(message: string): string {\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>\n <Message>${escapeXml(message)}</Message>\n</Response>`;\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n}\n\n// Standalone webhook server\nexport function startWebhookServer(port: number = 3456): void {\n const server = createServer(\n async (req: IncomingMessage, res: ServerResponse) => {\n const url = parseUrl(req.url || '/', true);\n\n // Health check\n if (url.pathname === '/health') {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ status: 'ok' }));\n return;\n }\n\n // SMS webhook endpoint (incoming messages)\n if (\n (url.pathname === '/sms' ||\n url.pathname === '/sms/incoming' ||\n url.pathname === '/webhook') &&\n req.method === 'POST'\n ) {\n const clientIp = req.socket.remoteAddress || 'unknown';\n\n // Log webhook request\n logWebhookRequest(\n 'sms-webhook',\n req.method || 'POST',\n url.pathname || '/sms',\n clientIp\n );\n\n // Rate limiting\n if (!checkRateLimit(clientIp)) {\n logRateLimit('sms-webhook', clientIp);\n res.writeHead(429, {\n 'Content-Type': 'text/xml',\n 'Retry-After': '60',\n });\n res.end(twimlResponse('Too many requests. Please try again later.'));\n return;\n }\n\n // Content-type validation\n const contentType = req.headers['content-type'] || '';\n if (!contentType.includes('application/x-www-form-urlencoded')) {\n logContentTypeInvalid('sms-webhook', contentType, clientIp);\n res.writeHead(400, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Invalid content type'));\n return;\n }\n\n let body = '';\n let bodyTooLarge = false;\n\n req.on('data', (chunk) => {\n body += chunk;\n // Body size limit\n if (body.length > MAX_BODY_SIZE) {\n bodyTooLarge = true;\n logBodyTooLarge('sms-webhook', body.length, clientIp);\n req.destroy();\n }\n });\n\n req.on('end', () => {\n if (bodyTooLarge) {\n res.writeHead(413, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Request too large'));\n return;\n }\n\n try {\n const payload = parseFormData(\n body\n ) as unknown as TwilioWebhookPayload;\n\n // Verify Twilio signature\n const twilioSignature = req.headers['x-twilio-signature'] as string;\n const webhookUrl = `${req.headers['x-forwarded-proto'] || 'http'}://${req.headers.host}${req.url}`;\n\n if (\n twilioSignature &&\n !verifyTwilioSignature(\n webhookUrl,\n payload as unknown as Record<string, string>,\n twilioSignature\n )\n ) {\n logSignatureInvalid('sms-webhook', clientIp);\n console.error('[sms-webhook] Invalid Twilio signature');\n res.writeHead(401, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Unauthorized'));\n return;\n }\n\n const result = handleSMSWebhook(payload);\n\n res.writeHead(200, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse(result.response));\n } catch (err) {\n console.error('[sms-webhook] Error:', err);\n res.writeHead(500, { 'Content-Type': 'text/xml' });\n res.end(twimlResponse('Error processing message'));\n }\n });\n return;\n }\n\n // Status callback endpoint (delivery status updates)\n if (url.pathname === '/sms/status' && req.method === 'POST') {\n let body = '';\n req.on('data', (chunk) => {\n body += chunk;\n });\n\n req.on('end', () => {\n try {\n const payload = parseFormData(body);\n console.log(\n `[sms-webhook] Status update: ${payload['MessageSid']} -> ${payload['MessageStatus']}`\n );\n\n // Store status for tracking\n const statusPath = join(\n homedir(),\n '.stackmemory',\n 'sms-status.json'\n );\n const statuses: Record<string, string> = existsSync(statusPath)\n ? JSON.parse(readFileSync(statusPath, 'utf8'))\n : {};\n statuses[payload['MessageSid']] = payload['MessageStatus'];\n writeFileSecure(statusPath, JSON.stringify(statuses, null, 2));\n\n res.writeHead(200, { 'Content-Type': 'text/plain' });\n res.end('OK');\n } catch (err) {\n console.error('[sms-webhook] Status error:', err);\n res.writeHead(500);\n res.end('Error');\n }\n });\n return;\n }\n\n // Server status endpoint\n if (url.pathname === '/status') {\n const config = loadSMSConfig();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n enabled: config.enabled,\n pendingPrompts: config.pendingPrompts.length,\n })\n );\n return;\n }\n\n res.writeHead(404);\n res.end('Not found');\n }\n );\n\n server.listen(port, () => {\n console.log(`[sms-webhook] Server listening on port ${port}`);\n console.log(\n `[sms-webhook] Incoming messages: http://localhost:${port}/sms/incoming`\n );\n console.log(\n `[sms-webhook] Status callback: http://localhost:${port}/sms/status`\n );\n console.log(`[sms-webhook] Configure these URLs in Twilio console`);\n\n // Start timed cleanup of expired prompts and old actions\n setInterval(() => {\n try {\n const expiredPrompts = cleanupExpiredPrompts();\n const oldActions = cleanupOldActions();\n if (expiredPrompts > 0 || oldActions > 0) {\n logCleanup('sms-webhook', expiredPrompts, oldActions);\n console.log(\n `[sms-webhook] Cleanup: ${expiredPrompts} expired prompts, ${oldActions} old actions`\n );\n }\n } catch {\n // Ignore cleanup errors\n }\n }, CLEANUP_INTERVAL_MS);\n console.log(\n `[sms-webhook] Cleanup interval: every ${CLEANUP_INTERVAL_MS / 1000}s`\n );\n });\n}\n\n// Express middleware for integration\nexport function smsWebhookMiddleware(\n req: { body: TwilioWebhookPayload },\n res: { type: (t: string) => void; send: (s: string) => void }\n): void {\n const result = handleSMSWebhook(req.body);\n res.type('text/xml');\n res.send(twimlResponse(result.response));\n}\n\n// CLI entry\nif (process.argv[1]?.endsWith('sms-webhook.js')) {\n const port = parseInt(process.env['SMS_WEBHOOK_PORT'] || '3456', 10);\n startWebhookServer(port);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackmemoryai/stackmemory",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.20",
|
|
4
4
|
"description": "Lossless memory runtime for AI coding tools - organizes context as a call stack instead of linear chat logs, with team collaboration and infinite retention",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=20.0.0",
|