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.
Files changed (83) hide show
  1. package/dist/ai-tool-to-genai.js +208 -0
  2. package/dist/ai-tool-to-genai.test.js +267 -0
  3. package/dist/channel-management.js +96 -0
  4. package/dist/cli.js +1674 -0
  5. package/dist/commands/abort.js +89 -0
  6. package/dist/commands/add-project.js +117 -0
  7. package/dist/commands/agent.js +250 -0
  8. package/dist/commands/ask-question.js +219 -0
  9. package/dist/commands/compact.js +126 -0
  10. package/dist/commands/context-menu.js +171 -0
  11. package/dist/commands/context.js +89 -0
  12. package/dist/commands/cost.js +93 -0
  13. package/dist/commands/create-new-project.js +111 -0
  14. package/dist/commands/diff.js +77 -0
  15. package/dist/commands/export.js +100 -0
  16. package/dist/commands/files.js +73 -0
  17. package/dist/commands/fork.js +199 -0
  18. package/dist/commands/help.js +54 -0
  19. package/dist/commands/login.js +488 -0
  20. package/dist/commands/merge-worktree.js +165 -0
  21. package/dist/commands/model.js +325 -0
  22. package/dist/commands/permissions.js +140 -0
  23. package/dist/commands/ping.js +13 -0
  24. package/dist/commands/queue.js +133 -0
  25. package/dist/commands/remove-project.js +119 -0
  26. package/dist/commands/rename.js +70 -0
  27. package/dist/commands/restart-opencode-server.js +77 -0
  28. package/dist/commands/resume.js +276 -0
  29. package/dist/commands/run-config.js +79 -0
  30. package/dist/commands/run.js +240 -0
  31. package/dist/commands/schedule.js +170 -0
  32. package/dist/commands/session-info.js +58 -0
  33. package/dist/commands/session.js +191 -0
  34. package/dist/commands/settings.js +84 -0
  35. package/dist/commands/share.js +89 -0
  36. package/dist/commands/status.js +79 -0
  37. package/dist/commands/sync.js +119 -0
  38. package/dist/commands/theme.js +53 -0
  39. package/dist/commands/types.js +2 -0
  40. package/dist/commands/undo-redo.js +170 -0
  41. package/dist/commands/user-command.js +135 -0
  42. package/dist/commands/verbosity.js +59 -0
  43. package/dist/commands/worktree-settings.js +50 -0
  44. package/dist/commands/worktree.js +288 -0
  45. package/dist/config.js +139 -0
  46. package/dist/database.js +585 -0
  47. package/dist/discord-bot.js +700 -0
  48. package/dist/discord-utils.js +336 -0
  49. package/dist/discord-utils.test.js +20 -0
  50. package/dist/errors.js +193 -0
  51. package/dist/escape-backticks.test.js +429 -0
  52. package/dist/format-tables.js +96 -0
  53. package/dist/format-tables.test.js +418 -0
  54. package/dist/genai-worker-wrapper.js +109 -0
  55. package/dist/genai-worker.js +299 -0
  56. package/dist/genai.js +230 -0
  57. package/dist/image-utils.js +107 -0
  58. package/dist/interaction-handler.js +289 -0
  59. package/dist/limit-heading-depth.js +25 -0
  60. package/dist/limit-heading-depth.test.js +105 -0
  61. package/dist/logger.js +111 -0
  62. package/dist/markdown.js +323 -0
  63. package/dist/markdown.test.js +269 -0
  64. package/dist/message-formatting.js +447 -0
  65. package/dist/message-formatting.test.js +73 -0
  66. package/dist/openai-realtime.js +226 -0
  67. package/dist/opencode.js +224 -0
  68. package/dist/reaction-handler.js +128 -0
  69. package/dist/scheduler.js +93 -0
  70. package/dist/security.js +200 -0
  71. package/dist/session-handler.js +1436 -0
  72. package/dist/system-message.js +138 -0
  73. package/dist/tools.js +354 -0
  74. package/dist/unnest-code-blocks.js +117 -0
  75. package/dist/unnest-code-blocks.test.js +432 -0
  76. package/dist/utils.js +95 -0
  77. package/dist/voice-handler.js +569 -0
  78. package/dist/voice.js +344 -0
  79. package/dist/worker-types.js +4 -0
  80. package/dist/worktree-utils.js +134 -0
  81. package/dist/xml.js +90 -0
  82. package/dist/xml.test.js +32 -0
  83. package/package.json +84 -0
@@ -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, '&amp;')
189
+ .replace(/</g, '&lt;')
190
+ .replace(/>/g, '&gt;')
191
+ .replace(/"/g, '&quot;')
192
+ .replace(/'/g, '&#39;');
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
+ }