disunday 1.0.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.
- package/dist/ai-tool-to-genai.js +208 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/channel-management.js +96 -0
- package/dist/cli.js +1674 -0
- package/dist/commands/abort.js +89 -0
- package/dist/commands/add-project.js +117 -0
- package/dist/commands/agent.js +250 -0
- package/dist/commands/ask-question.js +219 -0
- package/dist/commands/compact.js +126 -0
- package/dist/commands/context-menu.js +171 -0
- package/dist/commands/context.js +89 -0
- package/dist/commands/cost.js +93 -0
- package/dist/commands/create-new-project.js +111 -0
- package/dist/commands/diff.js +77 -0
- package/dist/commands/export.js +100 -0
- package/dist/commands/files.js +73 -0
- package/dist/commands/fork.js +199 -0
- package/dist/commands/help.js +54 -0
- package/dist/commands/login.js +488 -0
- package/dist/commands/merge-worktree.js +165 -0
- package/dist/commands/model.js +325 -0
- package/dist/commands/permissions.js +140 -0
- package/dist/commands/ping.js +13 -0
- package/dist/commands/queue.js +133 -0
- package/dist/commands/remove-project.js +119 -0
- package/dist/commands/rename.js +70 -0
- package/dist/commands/restart-opencode-server.js +77 -0
- package/dist/commands/resume.js +276 -0
- package/dist/commands/run-config.js +79 -0
- package/dist/commands/run.js +240 -0
- package/dist/commands/schedule.js +170 -0
- package/dist/commands/session-info.js +58 -0
- package/dist/commands/session.js +191 -0
- package/dist/commands/settings.js +84 -0
- package/dist/commands/share.js +89 -0
- package/dist/commands/status.js +79 -0
- package/dist/commands/sync.js +119 -0
- package/dist/commands/theme.js +53 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +170 -0
- package/dist/commands/user-command.js +135 -0
- package/dist/commands/verbosity.js +59 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +288 -0
- package/dist/config.js +139 -0
- package/dist/database.js +585 -0
- package/dist/discord-bot.js +700 -0
- package/dist/discord-utils.js +336 -0
- package/dist/discord-utils.test.js +20 -0
- package/dist/errors.js +193 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +96 -0
- package/dist/format-tables.test.js +418 -0
- package/dist/genai-worker-wrapper.js +109 -0
- package/dist/genai-worker.js +299 -0
- package/dist/genai.js +230 -0
- package/dist/image-utils.js +107 -0
- package/dist/interaction-handler.js +289 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +111 -0
- package/dist/markdown.js +323 -0
- package/dist/markdown.test.js +269 -0
- package/dist/message-formatting.js +447 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +226 -0
- package/dist/opencode.js +224 -0
- package/dist/reaction-handler.js +128 -0
- package/dist/scheduler.js +93 -0
- package/dist/security.js +200 -0
- package/dist/session-handler.js +1436 -0
- package/dist/system-message.js +138 -0
- package/dist/tools.js +354 -0
- package/dist/unnest-code-blocks.js +117 -0
- package/dist/unnest-code-blocks.test.js +432 -0
- package/dist/utils.js +95 -0
- package/dist/voice-handler.js +569 -0
- package/dist/voice.js +344 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +134 -0
- package/dist/xml.js +90 -0
- package/dist/xml.test.js +32 -0
- package/package.json +84 -0
package/dist/security.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Security utilities for Disunday Discord bot.
|
|
2
|
+
// Provides encryption for sensitive data at rest and sanitization for user input.
|
|
3
|
+
// Uses AES-256-GCM with machine-derived keys for token/API key encryption.
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { createTaggedError } from 'errore';
|
|
9
|
+
import { getDataDir } from './config.js';
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
// ERROR DEFINITIONS
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
export class EncryptionError extends createTaggedError({
|
|
14
|
+
name: 'EncryptionError',
|
|
15
|
+
message: 'Encryption operation failed: $reason',
|
|
16
|
+
}) {
|
|
17
|
+
}
|
|
18
|
+
export class DecryptionError extends createTaggedError({
|
|
19
|
+
name: 'DecryptionError',
|
|
20
|
+
message: 'Decryption operation failed: $reason',
|
|
21
|
+
}) {
|
|
22
|
+
}
|
|
23
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
+
// CONSTANTS
|
|
25
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
26
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
27
|
+
const IV_LENGTH = 12; // GCM recommended IV length
|
|
28
|
+
const SALT_LENGTH = 32;
|
|
29
|
+
const KEY_LENGTH = 32; // 256 bits
|
|
30
|
+
const SALT_FILENAME = 'encryption.salt';
|
|
31
|
+
const APP_IDENTIFIER = 'com.disunday.discord';
|
|
32
|
+
// Scrypt parameters (memory-hard, resistant to GPU attacks)
|
|
33
|
+
const SCRYPT_OPTIONS = {
|
|
34
|
+
N: 16384, // CPU/memory cost
|
|
35
|
+
r: 8, // Block size
|
|
36
|
+
p: 1, // Parallelization
|
|
37
|
+
};
|
|
38
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
39
|
+
// KEY MANAGEMENT
|
|
40
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
41
|
+
// Cache derived key in memory to avoid re-deriving on every operation
|
|
42
|
+
let cachedKey = null;
|
|
43
|
+
/**
|
|
44
|
+
* Get or create the encryption salt.
|
|
45
|
+
* Salt is stored in <dataDir>/encryption.salt and generated once per installation.
|
|
46
|
+
*/
|
|
47
|
+
function getOrCreateSalt() {
|
|
48
|
+
const dataDir = getDataDir();
|
|
49
|
+
const saltPath = path.join(dataDir, SALT_FILENAME);
|
|
50
|
+
if (fs.existsSync(saltPath)) {
|
|
51
|
+
return fs.readFileSync(saltPath);
|
|
52
|
+
}
|
|
53
|
+
// Generate new salt on first use
|
|
54
|
+
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
55
|
+
// Ensure data directory exists
|
|
56
|
+
if (!fs.existsSync(dataDir)) {
|
|
57
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
fs.writeFileSync(saltPath, salt, { mode: 0o600 }); // Read/write only for owner
|
|
60
|
+
return salt;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Derive machine fingerprint for key generation.
|
|
64
|
+
* Combines stable machine-specific values with app identifier.
|
|
65
|
+
*/
|
|
66
|
+
function getMachineFingerprint() {
|
|
67
|
+
return [
|
|
68
|
+
os.platform(),
|
|
69
|
+
os.homedir(),
|
|
70
|
+
os.userInfo().username,
|
|
71
|
+
APP_IDENTIFIER,
|
|
72
|
+
].join(':');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Get the encryption key, deriving it from machine data if not cached.
|
|
76
|
+
* Uses scrypt for key derivation (memory-hard, resistant to brute force).
|
|
77
|
+
*/
|
|
78
|
+
export function getEncryptionKey() {
|
|
79
|
+
if (cachedKey) {
|
|
80
|
+
return cachedKey;
|
|
81
|
+
}
|
|
82
|
+
const machineData = getMachineFingerprint();
|
|
83
|
+
const salt = getOrCreateSalt();
|
|
84
|
+
cachedKey = crypto.scryptSync(machineData, salt, KEY_LENGTH, SCRYPT_OPTIONS);
|
|
85
|
+
return cachedKey;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Clear the cached encryption key.
|
|
89
|
+
* Useful for testing or when the salt file changes.
|
|
90
|
+
*/
|
|
91
|
+
export function clearKeyCache() {
|
|
92
|
+
cachedKey = null;
|
|
93
|
+
}
|
|
94
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
95
|
+
// ENCRYPTION / DECRYPTION
|
|
96
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
97
|
+
/**
|
|
98
|
+
* Encrypt a plaintext string using AES-256-GCM.
|
|
99
|
+
* Returns an object with IV, auth tag, and ciphertext (all base64 encoded).
|
|
100
|
+
*/
|
|
101
|
+
export function encrypt({ plaintext, key, }) {
|
|
102
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
103
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
104
|
+
const encrypted = Buffer.concat([
|
|
105
|
+
cipher.update(plaintext, 'utf8'),
|
|
106
|
+
cipher.final(),
|
|
107
|
+
]);
|
|
108
|
+
const authTag = cipher.getAuthTag();
|
|
109
|
+
return {
|
|
110
|
+
iv: iv.toString('base64'),
|
|
111
|
+
authTag: authTag.toString('base64'),
|
|
112
|
+
encrypted: encrypted.toString('base64'),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Decrypt an encrypted data object using AES-256-GCM.
|
|
117
|
+
* Throws DecryptionError if decryption fails (wrong key, corrupted data, etc).
|
|
118
|
+
*/
|
|
119
|
+
export function decrypt({ data, key, }) {
|
|
120
|
+
try {
|
|
121
|
+
const iv = Buffer.from(data.iv, 'base64');
|
|
122
|
+
const authTag = Buffer.from(data.authTag, 'base64');
|
|
123
|
+
const encrypted = Buffer.from(data.encrypted, 'base64');
|
|
124
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
125
|
+
decipher.setAuthTag(authTag);
|
|
126
|
+
const decrypted = Buffer.concat([
|
|
127
|
+
decipher.update(encrypted),
|
|
128
|
+
decipher.final(),
|
|
129
|
+
]);
|
|
130
|
+
return decrypted.toString('utf8');
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
throw new DecryptionError({
|
|
134
|
+
reason: error instanceof Error ? error.message : 'Unknown error',
|
|
135
|
+
cause: error,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Serialize encrypted data to a string for database storage.
|
|
141
|
+
* Format: iv:authTag:encrypted (all base64)
|
|
142
|
+
*/
|
|
143
|
+
export function serializeEncrypted(data) {
|
|
144
|
+
return `${data.iv}:${data.authTag}:${data.encrypted}`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Deserialize encrypted data from database storage format.
|
|
148
|
+
* Returns null if the format is invalid.
|
|
149
|
+
*/
|
|
150
|
+
export function deserializeEncrypted(serialized) {
|
|
151
|
+
const parts = serialized.split(':');
|
|
152
|
+
if (parts.length !== 3) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
const [iv, authTag, encrypted] = parts;
|
|
156
|
+
if (!iv || !authTag || !encrypted) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return { iv, authTag, encrypted };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Check if a string appears to be encrypted data.
|
|
163
|
+
* Encrypted data has format: base64:base64:base64
|
|
164
|
+
*/
|
|
165
|
+
export function isEncrypted(value) {
|
|
166
|
+
if (!value || !value.includes(':')) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const parts = value.split(':');
|
|
170
|
+
if (parts.length !== 3) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
// Check if all parts look like base64
|
|
174
|
+
const base64Regex = /^[A-Za-z0-9+/]+=*$/;
|
|
175
|
+
return parts.every((part) => {
|
|
176
|
+
return part.length > 0 && base64Regex.test(part);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
180
|
+
// SANITIZATION
|
|
181
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
182
|
+
/**
|
|
183
|
+
* Sanitize text for safe use in XML/HTML contexts.
|
|
184
|
+
* Escapes: < > & " '
|
|
185
|
+
*/
|
|
186
|
+
export function sanitizeForXml(text) {
|
|
187
|
+
return text
|
|
188
|
+
.replace(/&/g, '&')
|
|
189
|
+
.replace(/</g, '<')
|
|
190
|
+
.replace(/>/g, '>')
|
|
191
|
+
.replace(/"/g, '"')
|
|
192
|
+
.replace(/'/g, ''');
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Sanitize text for safe use in Discord messages.
|
|
196
|
+
* Escapes markdown special characters to prevent formatting injection.
|
|
197
|
+
*/
|
|
198
|
+
export function sanitizeForDiscord(text) {
|
|
199
|
+
return text.replace(/([*_~`|\\])/g, '\\$1');
|
|
200
|
+
}
|