openclaw-telegram-manager 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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +110 -0
  3. package/dist/commands/archive.d.ts +4 -0
  4. package/dist/commands/archive.d.ts.map +1 -0
  5. package/dist/commands/archive.js +71 -0
  6. package/dist/commands/archive.js.map +1 -0
  7. package/dist/commands/doctor-all.d.ts +3 -0
  8. package/dist/commands/doctor-all.d.ts.map +1 -0
  9. package/dist/commands/doctor-all.js +193 -0
  10. package/dist/commands/doctor-all.js.map +1 -0
  11. package/dist/commands/doctor.d.ts +3 -0
  12. package/dist/commands/doctor.d.ts.map +1 -0
  13. package/dist/commands/doctor.js +74 -0
  14. package/dist/commands/doctor.js.map +1 -0
  15. package/dist/commands/help.d.ts +4 -0
  16. package/dist/commands/help.d.ts.map +1 -0
  17. package/dist/commands/help.js +8 -0
  18. package/dist/commands/help.js.map +1 -0
  19. package/dist/commands/init.d.ts +17 -0
  20. package/dist/commands/init.d.ts.map +1 -0
  21. package/dist/commands/init.js +304 -0
  22. package/dist/commands/init.js.map +1 -0
  23. package/dist/commands/list.d.ts +3 -0
  24. package/dist/commands/list.d.ts.map +1 -0
  25. package/dist/commands/list.js +22 -0
  26. package/dist/commands/list.js.map +1 -0
  27. package/dist/commands/rename.d.ts +3 -0
  28. package/dist/commands/rename.d.ts.map +1 -0
  29. package/dist/commands/rename.js +115 -0
  30. package/dist/commands/rename.js.map +1 -0
  31. package/dist/commands/snooze.d.ts +3 -0
  32. package/dist/commands/snooze.d.ts.map +1 -0
  33. package/dist/commands/snooze.js +52 -0
  34. package/dist/commands/snooze.js.map +1 -0
  35. package/dist/commands/status.d.ts +3 -0
  36. package/dist/commands/status.d.ts.map +1 -0
  37. package/dist/commands/status.js +48 -0
  38. package/dist/commands/status.js.map +1 -0
  39. package/dist/commands/sync.d.ts +3 -0
  40. package/dist/commands/sync.d.ts.map +1 -0
  41. package/dist/commands/sync.js +38 -0
  42. package/dist/commands/sync.js.map +1 -0
  43. package/dist/commands/upgrade.d.ts +3 -0
  44. package/dist/commands/upgrade.d.ts.map +1 -0
  45. package/dist/commands/upgrade.js +52 -0
  46. package/dist/commands/upgrade.js.map +1 -0
  47. package/dist/index.d.ts +25 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +30 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/lib/audit.d.ts +12 -0
  52. package/dist/lib/audit.d.ts.map +1 -0
  53. package/dist/lib/audit.js +35 -0
  54. package/dist/lib/audit.js.map +1 -0
  55. package/dist/lib/auth.d.ts +26 -0
  56. package/dist/lib/auth.d.ts.map +1 -0
  57. package/dist/lib/auth.js +73 -0
  58. package/dist/lib/auth.js.map +1 -0
  59. package/dist/lib/capsule.d.ts +27 -0
  60. package/dist/lib/capsule.d.ts.map +1 -0
  61. package/dist/lib/capsule.js +130 -0
  62. package/dist/lib/capsule.js.map +1 -0
  63. package/dist/lib/config-restart.d.ts +23 -0
  64. package/dist/lib/config-restart.d.ts.map +1 -0
  65. package/dist/lib/config-restart.js +129 -0
  66. package/dist/lib/config-restart.js.map +1 -0
  67. package/dist/lib/doctor-checks.d.ts +50 -0
  68. package/dist/lib/doctor-checks.d.ts.map +1 -0
  69. package/dist/lib/doctor-checks.js +421 -0
  70. package/dist/lib/doctor-checks.js.map +1 -0
  71. package/dist/lib/include-generator.d.ts +35 -0
  72. package/dist/lib/include-generator.d.ts.map +1 -0
  73. package/dist/lib/include-generator.js +140 -0
  74. package/dist/lib/include-generator.js.map +1 -0
  75. package/dist/lib/registry.d.ts +27 -0
  76. package/dist/lib/registry.d.ts.map +1 -0
  77. package/dist/lib/registry.js +154 -0
  78. package/dist/lib/registry.js.map +1 -0
  79. package/dist/lib/security.d.ts +57 -0
  80. package/dist/lib/security.d.ts.map +1 -0
  81. package/dist/lib/security.js +133 -0
  82. package/dist/lib/security.js.map +1 -0
  83. package/dist/lib/telegram.d.ts +55 -0
  84. package/dist/lib/telegram.d.ts.map +1 -0
  85. package/dist/lib/telegram.js +254 -0
  86. package/dist/lib/telegram.js.map +1 -0
  87. package/dist/lib/types.d.ts +120 -0
  88. package/dist/lib/types.d.ts.map +1 -0
  89. package/dist/lib/types.js +85 -0
  90. package/dist/lib/types.js.map +1 -0
  91. package/dist/setup.d.ts +3 -0
  92. package/dist/setup.d.ts.map +1 -0
  93. package/dist/setup.js +333 -0
  94. package/dist/setup.js.map +1 -0
  95. package/dist/tool.d.ts +15 -0
  96. package/dist/tool.d.ts.map +1 -0
  97. package/dist/tool.js +201 -0
  98. package/dist/tool.js.map +1 -0
  99. package/openclaw.plugin.json +9 -0
  100. package/package.json +48 -0
  101. package/skills/topic/SKILL.md +35 -0
  102. package/src/commands/archive.ts +89 -0
  103. package/src/commands/doctor-all.ts +243 -0
  104. package/src/commands/doctor.ts +100 -0
  105. package/src/commands/help.ts +11 -0
  106. package/src/commands/init.ts +376 -0
  107. package/src/commands/list.ts +28 -0
  108. package/src/commands/rename.ts +140 -0
  109. package/src/commands/snooze.ts +69 -0
  110. package/src/commands/status.ts +59 -0
  111. package/src/commands/sync.ts +46 -0
  112. package/src/commands/upgrade.ts +64 -0
  113. package/src/index.ts +54 -0
  114. package/src/lib/audit.ts +44 -0
  115. package/src/lib/auth.ts +96 -0
  116. package/src/lib/capsule.ts +206 -0
  117. package/src/lib/config-restart.ts +167 -0
  118. package/src/lib/doctor-checks.ts +639 -0
  119. package/src/lib/include-generator.ts +174 -0
  120. package/src/lib/registry.ts +197 -0
  121. package/src/lib/security.ts +174 -0
  122. package/src/lib/telegram.ts +311 -0
  123. package/src/lib/types.ts +172 -0
  124. package/src/setup.ts +402 -0
  125. package/src/templates/base/COMMANDS.md +3 -0
  126. package/src/templates/base/CRON.md +3 -0
  127. package/src/templates/base/LINKS.md +3 -0
  128. package/src/templates/base/NOTES.md +3 -0
  129. package/src/templates/base/README.md +3 -0
  130. package/src/templates/base/STATUS.md +13 -0
  131. package/src/templates/base/TODO.md +11 -0
  132. package/src/templates/overlays/coding/ARCHITECTURE.md +3 -0
  133. package/src/templates/overlays/coding/DEPLOY.md +3 -0
  134. package/src/templates/overlays/marketing/CAMPAIGNS.md +3 -0
  135. package/src/templates/overlays/marketing/METRICS.md +3 -0
  136. package/src/templates/overlays/research/FINDINGS.md +3 -0
  137. package/src/templates/overlays/research/SOURCES.md +3 -0
  138. package/src/tool.ts +282 -0
@@ -0,0 +1,35 @@
1
+ import type { Registry } from './types.js';
2
+ /**
3
+ * Build the per-topic systemPrompt using absolute paths resolved at generation time.
4
+ */
5
+ export declare function getSystemPromptTemplate(slug: string, absoluteWorkspacePath: string): string;
6
+ /**
7
+ * Compute a SHA256 hash of the registry topics for drift detection.
8
+ */
9
+ export declare function computeRegistryHash(topics: Registry['topics']): string;
10
+ /**
11
+ * Build the JavaScript object for the generated include file.
12
+ * Groups topics by groupId, with each topic's config under its threadId.
13
+ */
14
+ export declare function buildIncludeObject(registry: Registry, workspaceDir: string): Record<string, unknown>;
15
+ /**
16
+ * Generate the JSON5 include file from the registry.
17
+ *
18
+ * Steps:
19
+ * 1. Build JS object from registry entries
20
+ * 2. Serialize via JSON5.stringify (never string interpolation)
21
+ * 3. Parse back to verify round-trip integrity
22
+ * 4. Atomic write with .bak
23
+ * 5. Prepend registry-hash comment
24
+ */
25
+ export declare function generateInclude(workspaceDir: string, registry: Registry, configDir: string): void;
26
+ /**
27
+ * Extract the registry-hash from an existing include file's content.
28
+ * Returns the hash string or null if not found.
29
+ */
30
+ export declare function extractRegistryHash(includeContent: string): string | null;
31
+ /**
32
+ * Get the path to the generated include file.
33
+ */
34
+ export declare function includePath(configDir: string): string;
35
+ //# sourceMappingURL=include-generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"include-generator.d.ts","sourceRoot":"","sources":["../../src/lib/include-generator.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAyB,MAAM,YAAY,CAAC;AASlE;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,qBAAqB,EAAE,MAAM,GAAG,MAAM,CAqB3F;AAID;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,QAAQ,CAAC,QAAQ,CAAC,GAAG,MAAM,CAGtE;AAID;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,QAAQ,EAClB,YAAY,EAAE,MAAM,GACnB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuBzB;AAoBD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,MAAM,GAChB,IAAI,CAwCN;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGzE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAErD"}
@@ -0,0 +1,140 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import JSON5 from 'json5';
5
+ // ── Constants ──────────────────────────────────────────────────────────
6
+ const INCLUDE_FILENAME = 'telegram-manager.generated.groups.json5';
7
+ const FILE_MODE = 0o600;
8
+ // ── System prompt template ─────────────────────────────────────────────
9
+ /**
10
+ * Build the per-topic systemPrompt using absolute paths resolved at generation time.
11
+ */
12
+ export function getSystemPromptTemplate(slug, absoluteWorkspacePath) {
13
+ return `You are the assistant for the Telegram topic: ${slug}.
14
+
15
+ Determinism rules:
16
+ - Source of truth is the project capsule at: ${absoluteWorkspacePath}/projects/${slug}/
17
+ - After /reset, /new, or context compaction: ALWAYS re-read STATUS.md,
18
+ then TODO.md, then COMMANDS.md before continuing work. Do not rely on
19
+ summarized memory for paths, commands, or task state.
20
+ - Before context compaction or when the conversation is long: proactively
21
+ flush current progress to STATUS.md (update "Last done (UTC)" and
22
+ "Next 3 actions") so compaction cannot erase critical state.
23
+ Use the standard file write tool directly — do not route through /topic.
24
+ - Keep STATUS.md accurate: always maintain "Last done (UTC)" and "Next 3 actions".
25
+ - When new commands appear, add them to COMMANDS.md (don't leave them only in chat).
26
+ - When new links/paths/services appear, add them to LINKS.md.
27
+ - If automation/cron is involved, record job IDs + schedules in CRON.md.
28
+ - Task IDs (e.g., [T-1]) must stay consistent between STATUS.md and TODO.md.
29
+
30
+ Separation:
31
+ - Do not mix in other topics' work unless explicitly requested.
32
+ - Ask one clarifying question if the next action is ambiguous.`;
33
+ }
34
+ // ── Registry hash ──────────────────────────────────────────────────────
35
+ /**
36
+ * Compute a SHA256 hash of the registry topics for drift detection.
37
+ */
38
+ export function computeRegistryHash(topics) {
39
+ const content = JSON.stringify(topics);
40
+ return crypto.createHash('sha256').update(content).digest('hex');
41
+ }
42
+ // ── Build include object ───────────────────────────────────────────────
43
+ /**
44
+ * Build the JavaScript object for the generated include file.
45
+ * Groups topics by groupId, with each topic's config under its threadId.
46
+ */
47
+ export function buildIncludeObject(registry, workspaceDir) {
48
+ const absoluteWorkspacePath = path.resolve(workspaceDir);
49
+ const groups = {};
50
+ for (const entry of Object.values(registry.topics)) {
51
+ const { groupId, threadId, slug, type, status } = entry;
52
+ if (!groups[groupId]) {
53
+ groups[groupId] = { topics: {} };
54
+ }
55
+ const isEnabled = status !== 'archived';
56
+ const skills = getSkillsForType(type);
57
+ const systemPrompt = getSystemPromptTemplate(slug, absoluteWorkspacePath);
58
+ groups[groupId].topics[threadId] = {
59
+ enabled: isEnabled,
60
+ skills,
61
+ systemPrompt,
62
+ };
63
+ }
64
+ return groups;
65
+ }
66
+ /**
67
+ * Get the default skills list for a topic type.
68
+ */
69
+ function getSkillsForType(type) {
70
+ switch (type) {
71
+ case 'coding':
72
+ return ['coding-agent'];
73
+ case 'research':
74
+ return ['research-agent'];
75
+ case 'marketing':
76
+ return ['marketing-agent'];
77
+ default:
78
+ return [];
79
+ }
80
+ }
81
+ // ── Generate include file ──────────────────────────────────────────────
82
+ /**
83
+ * Generate the JSON5 include file from the registry.
84
+ *
85
+ * Steps:
86
+ * 1. Build JS object from registry entries
87
+ * 2. Serialize via JSON5.stringify (never string interpolation)
88
+ * 3. Parse back to verify round-trip integrity
89
+ * 4. Atomic write with .bak
90
+ * 5. Prepend registry-hash comment
91
+ */
92
+ export function generateInclude(workspaceDir, registry, configDir) {
93
+ const includeObj = buildIncludeObject(registry, workspaceDir);
94
+ const hash = computeRegistryHash(registry.topics);
95
+ // Serialize via JSON5.stringify
96
+ const json5Content = JSON5.stringify(includeObj, null, 2);
97
+ // Round-trip validation: parse back to verify integrity
98
+ try {
99
+ JSON5.parse(json5Content);
100
+ }
101
+ catch (err) {
102
+ throw new Error(`Include generation failed: round-trip validation error. ${err instanceof Error ? err.message : String(err)}`);
103
+ }
104
+ // Build final content with header comment
105
+ const header = [
106
+ '// This file is generated by telegram-manager. Do not hand-edit.',
107
+ `// Rebuild from: ${path.resolve(workspaceDir)}/projects/topics.json`,
108
+ `// registry-hash: sha256:${hash}`,
109
+ ].join('\n');
110
+ const finalContent = header + '\n' + json5Content + '\n';
111
+ // Atomic write
112
+ const includePath = path.join(configDir, INCLUDE_FILENAME);
113
+ const tmpPath = includePath + '.tmp';
114
+ const bakPath = includePath + '.bak';
115
+ // Backup existing file if it exists
116
+ if (fs.existsSync(includePath)) {
117
+ fs.copyFileSync(includePath, bakPath);
118
+ fs.chmodSync(bakPath, FILE_MODE);
119
+ }
120
+ // Write to tmp then rename (atomic on POSIX)
121
+ fs.writeFileSync(tmpPath, finalContent, { mode: FILE_MODE });
122
+ fs.renameSync(tmpPath, includePath);
123
+ fs.chmodSync(includePath, FILE_MODE);
124
+ }
125
+ // ── Extract registry hash from include file ────────────────────────────
126
+ /**
127
+ * Extract the registry-hash from an existing include file's content.
128
+ * Returns the hash string or null if not found.
129
+ */
130
+ export function extractRegistryHash(includeContent) {
131
+ const match = includeContent.match(/^\/\/ registry-hash: sha256:([a-f0-9]+)$/m);
132
+ return match?.[1] ?? null;
133
+ }
134
+ /**
135
+ * Get the path to the generated include file.
136
+ */
137
+ export function includePath(configDir) {
138
+ return path.join(configDir, INCLUDE_FILENAME);
139
+ }
140
+ //# sourceMappingURL=include-generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"include-generator.js","sourceRoot":"","sources":["../../src/lib/include-generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import type { Registry } from './types.js';
2
+ export declare function registryPath(workspaceDir: string): string;
3
+ /**
4
+ * Read and validate the registry from disk.
5
+ * - Migrates if version is behind current
6
+ * - Rejects if version is ahead of current
7
+ * - Quarantines invalid topic entries (logs + excludes)
8
+ */
9
+ export declare function readRegistry(workspaceDir: string): Registry;
10
+ /**
11
+ * Atomically write registry data to disk.
12
+ * Writes to a .tmp file then renames (atomic on POSIX).
13
+ */
14
+ export declare function writeRegistryAtomic(filePath: string, data: Registry): void;
15
+ /**
16
+ * Lock the registry, read it, apply a mutation function, and write it back.
17
+ * The lock prevents concurrent writes from corrupting the registry.
18
+ *
19
+ * The mutation function receives the registry data and can modify it.
20
+ * Return value of the mutation function is passed through as the return value.
21
+ */
22
+ export declare function withRegistry<T>(workspaceDir: string, fn: (data: Registry) => T | Promise<T>): Promise<T>;
23
+ /**
24
+ * Create a new empty registry with default values.
25
+ */
26
+ export declare function createEmptyRegistry(callbackSecret: string): Registry;
27
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/lib/registry.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,QAAQ,EAAc,MAAM,YAAY,CAAC;AAWvD,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEzD;AAoCD;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,YAAY,EAAE,MAAM,GAAG,QAAQ,CAoD3D;AAID;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAO1E;AAID;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAClC,YAAY,EAAE,MAAM,EACpB,EAAE,EAAE,CAAC,IAAI,EAAE,QAAQ,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GACrC,OAAO,CAAC,CAAC,CAAC,CAkCZ;AAID;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,GAAG,QAAQ,CASpE"}
@@ -0,0 +1,154 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import lockfile from 'proper-lockfile';
4
+ import { Value } from '@sinclair/typebox/value';
5
+ import { RegistrySchema, TopicEntrySchema, CURRENT_REGISTRY_VERSION, MAX_TOPICS_DEFAULT, } from './types.js';
6
+ // ── Constants ──────────────────────────────────────────────────────────
7
+ const REGISTRY_FILENAME = 'topics.json';
8
+ const FILE_MODE = 0o600;
9
+ const LOCK_TIMEOUT = 5000;
10
+ const LOCK_RETRY_INTERVAL = 100;
11
+ // ── Path helpers ───────────────────────────────────────────────────────
12
+ export function registryPath(workspaceDir) {
13
+ return path.join(workspaceDir, 'projects', REGISTRY_FILENAME);
14
+ }
15
+ const migrations = {
16
+ // Add migrations here as needed:
17
+ // '1_to_2': (data) => { ... return data; },
18
+ };
19
+ function migrateRegistry(data) {
20
+ const rawVersion = data['version'];
21
+ if (typeof rawVersion !== 'number') {
22
+ throw new Error('Registry missing or invalid version field in migration');
23
+ }
24
+ let version = rawVersion;
25
+ while (version < CURRENT_REGISTRY_VERSION) {
26
+ const key = `${version}_to_${version + 1}`;
27
+ const fn = migrations[key];
28
+ if (!fn) {
29
+ throw new Error(`No migration function found for ${key}. Cannot upgrade registry from v${version}.`);
30
+ }
31
+ data = fn(data);
32
+ version++;
33
+ data['version'] = version;
34
+ }
35
+ return data;
36
+ }
37
+ // ── Read ───────────────────────────────────────────────────────────────
38
+ /**
39
+ * Read and validate the registry from disk.
40
+ * - Migrates if version is behind current
41
+ * - Rejects if version is ahead of current
42
+ * - Quarantines invalid topic entries (logs + excludes)
43
+ */
44
+ export function readRegistry(workspaceDir) {
45
+ const regPath = registryPath(workspaceDir);
46
+ const raw = fs.readFileSync(regPath, 'utf-8');
47
+ let data;
48
+ try {
49
+ data = JSON.parse(raw);
50
+ }
51
+ catch {
52
+ throw new Error(`Failed to parse registry at ${regPath}: invalid JSON`);
53
+ }
54
+ // Version check
55
+ const version = data['version'];
56
+ if (typeof version !== 'number') {
57
+ throw new Error('Registry missing version field');
58
+ }
59
+ if (version > CURRENT_REGISTRY_VERSION) {
60
+ throw new Error(`Registry version ${version} is newer than this plugin supports (v${CURRENT_REGISTRY_VERSION}). Please upgrade openclaw-telegram-manager.`);
61
+ }
62
+ // Migrate if needed
63
+ if (version < CURRENT_REGISTRY_VERSION) {
64
+ data = migrateRegistry(data);
65
+ }
66
+ // Quarantine invalid topic entries
67
+ const topics = data['topics'];
68
+ if (topics && typeof topics === 'object' && !Array.isArray(topics)) {
69
+ const validTopics = {};
70
+ for (const [key, entry] of Object.entries(topics)) {
71
+ if (Value.Check(TopicEntrySchema, entry)) {
72
+ validTopics[key] = entry;
73
+ }
74
+ else {
75
+ const errors = [...Value.Errors(TopicEntrySchema, entry)];
76
+ const errorMsg = errors.map((e) => `${e.path}: ${e.message}`).join('; ');
77
+ console.error(`[registry] Quarantined invalid entry "${key}": ${errorMsg}`);
78
+ }
79
+ }
80
+ data['topics'] = validTopics;
81
+ }
82
+ // Validate the full registry schema
83
+ if (!Value.Check(RegistrySchema, data)) {
84
+ const errors = [...Value.Errors(RegistrySchema, data)];
85
+ const errorMsg = errors.map((e) => `${e.path}: ${e.message}`).join('; ');
86
+ throw new Error(`Registry validation failed: ${errorMsg}`);
87
+ }
88
+ return data;
89
+ }
90
+ // ── Atomic write ───────────────────────────────────────────────────────
91
+ /**
92
+ * Atomically write registry data to disk.
93
+ * Writes to a .tmp file then renames (atomic on POSIX).
94
+ */
95
+ export function writeRegistryAtomic(filePath, data) {
96
+ const tmpPath = filePath + '.tmp';
97
+ const content = JSON.stringify(data, null, 2) + '\n';
98
+ fs.writeFileSync(tmpPath, content, { mode: FILE_MODE });
99
+ fs.renameSync(tmpPath, filePath);
100
+ fs.chmodSync(filePath, FILE_MODE);
101
+ }
102
+ // ── withRegistry pattern ───────────────────────────────────────────────
103
+ /**
104
+ * Lock the registry, read it, apply a mutation function, and write it back.
105
+ * The lock prevents concurrent writes from corrupting the registry.
106
+ *
107
+ * The mutation function receives the registry data and can modify it.
108
+ * Return value of the mutation function is passed through as the return value.
109
+ */
110
+ export async function withRegistry(workspaceDir, fn) {
111
+ const regPath = registryPath(workspaceDir);
112
+ const lockDir = path.dirname(regPath);
113
+ // Ensure the registry file exists before locking
114
+ if (!fs.existsSync(regPath)) {
115
+ throw new Error(`Registry not found at ${regPath}. Run setup first.`);
116
+ }
117
+ let release;
118
+ try {
119
+ release = await lockfile.lock(regPath, {
120
+ stale: LOCK_TIMEOUT * 2,
121
+ retries: {
122
+ retries: Math.ceil(LOCK_TIMEOUT / LOCK_RETRY_INTERVAL),
123
+ minTimeout: LOCK_RETRY_INTERVAL,
124
+ maxTimeout: LOCK_RETRY_INTERVAL,
125
+ },
126
+ lockfilePath: path.join(lockDir, REGISTRY_FILENAME + '.lock'),
127
+ });
128
+ const data = readRegistry(workspaceDir);
129
+ const result = await fn(data);
130
+ // Write the (potentially mutated) registry back
131
+ writeRegistryAtomic(regPath, data);
132
+ return result;
133
+ }
134
+ finally {
135
+ if (release) {
136
+ await release();
137
+ }
138
+ }
139
+ }
140
+ // ── Empty registry factory ─────────────────────────────────────────────
141
+ /**
142
+ * Create a new empty registry with default values.
143
+ */
144
+ export function createEmptyRegistry(callbackSecret) {
145
+ return {
146
+ version: CURRENT_REGISTRY_VERSION,
147
+ topicManagerAdmins: [],
148
+ callbackSecret,
149
+ lastDoctorAllRunAt: null,
150
+ maxTopics: MAX_TOPICS_DEFAULT,
151
+ topics: {},
152
+ };
153
+ }
154
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/lib/registry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,57 @@
1
+ /** Validate a slug against the allowed pattern. */
2
+ export declare function validateSlug(slug: string): boolean;
3
+ /**
4
+ * Sanitize a title into a valid slug.
5
+ * Strip non-alphanumeric (except hyphens), strip dots, collapse
6
+ * consecutive hyphens, trim leading/trailing hyphens, lowercase.
7
+ */
8
+ export declare function sanitizeSlug(title: string): string;
9
+ /**
10
+ * Jail check: ensure `userPath` resolves within `base`.
11
+ * Returns true if safe, false if the path escapes the base.
12
+ */
13
+ export declare function jailCheck(base: string, userPath: string): boolean;
14
+ /**
15
+ * Reject symlinks. Returns true if the path is a symlink (should be rejected).
16
+ * Returns false if not a symlink or path does not exist.
17
+ */
18
+ export declare function rejectSymlink(filePath: string): boolean;
19
+ /**
20
+ * Sign a payload with HMAC-SHA256, returning the first 16 hex chars.
21
+ * Truncated to 8 bytes (16 hex chars) to fit within Telegram's 64-byte
22
+ * callback_data limit. Online brute-force is infeasible due to Telegram rate limits.
23
+ */
24
+ export declare function hmacSign(secret: string, payload: string): string;
25
+ /**
26
+ * Verify an HMAC signature using constant-time comparison.
27
+ * Returns true if the signature is valid.
28
+ */
29
+ export declare function hmacVerify(secret: string, payload: string, signature: string): boolean;
30
+ /** Escape HTML special characters for safe Telegram HTML output. */
31
+ export declare function htmlEscape(str: string): string;
32
+ /** Validate a Telegram group ID (may be negative). */
33
+ export declare function validateGroupId(id: string): boolean;
34
+ /** Validate a Telegram thread ID (positive integer). */
35
+ export declare function validateThreadId(id: string): boolean;
36
+ export interface CallbackData {
37
+ action: string;
38
+ slug: string;
39
+ groupId: string;
40
+ threadId: string;
41
+ }
42
+ /**
43
+ * Build callback data string with HMAC signature.
44
+ * Format: tm:<action>:<slug>:<groupId>:<threadId>:<hmac>
45
+ */
46
+ export declare function buildCallbackData(action: string, slug: string, groupId: string, threadId: string, secret: string): string;
47
+ /**
48
+ * Parse and verify callback data.
49
+ * Returns the parsed data or null if verification fails.
50
+ *
51
+ * Checks:
52
+ * 1. Format matches the expected regex
53
+ * 2. HMAC is valid
54
+ * 3. groupId and threadId match the context (prevents cross-topic tampering)
55
+ */
56
+ export declare function parseAndVerifyCallback(data: string, secret: string, contextGroupId: string, contextThreadId: string): CallbackData | null;
57
+ //# sourceMappingURL=security.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../src/lib/security.ts"],"names":[],"mappings":"AAQA,mDAAmD;AACnD,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAElD;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQlD;AAID;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAIjE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMvD;AAID;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAWtF;AAWD,oEAAoE;AACpE,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE9C;AAOD,sDAAsD;AACtD,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEpD;AAMD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,MAAM,CAIR;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,EACtB,eAAe,EAAE,MAAM,GACtB,YAAY,GAAG,IAAI,CAmBrB"}
@@ -0,0 +1,133 @@
1
+ import * as crypto from 'node:crypto';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ // ── Slug validation ────────────────────────────────────────────────────
5
+ const SLUG_RE = /^[a-z][a-z0-9-]{0,49}$/;
6
+ /** Validate a slug against the allowed pattern. */
7
+ export function validateSlug(slug) {
8
+ return SLUG_RE.test(slug);
9
+ }
10
+ /**
11
+ * Sanitize a title into a valid slug.
12
+ * Strip non-alphanumeric (except hyphens), strip dots, collapse
13
+ * consecutive hyphens, trim leading/trailing hyphens, lowercase.
14
+ */
15
+ export function sanitizeSlug(title) {
16
+ return title
17
+ .toLowerCase()
18
+ .replace(/\./g, '') // strip dots explicitly
19
+ .replace(/[^a-z0-9-]/g, '-') // non-alphanum to hyphen
20
+ .replace(/-{2,}/g, '-') // collapse consecutive hyphens
21
+ .replace(/^-+|-+$/g, '') // trim leading/trailing hyphens
22
+ .slice(0, 50); // enforce max length
23
+ }
24
+ // ── Path safety ────────────────────────────────────────────────────────
25
+ /**
26
+ * Jail check: ensure `userPath` resolves within `base`.
27
+ * Returns true if safe, false if the path escapes the base.
28
+ */
29
+ export function jailCheck(base, userPath) {
30
+ const resolved = path.resolve(base, userPath);
31
+ const normalizedBase = path.resolve(base) + path.sep;
32
+ return resolved.startsWith(normalizedBase) || resolved === path.resolve(base);
33
+ }
34
+ /**
35
+ * Reject symlinks. Returns true if the path is a symlink (should be rejected).
36
+ * Returns false if not a symlink or path does not exist.
37
+ */
38
+ export function rejectSymlink(filePath) {
39
+ try {
40
+ return fs.lstatSync(filePath).isSymbolicLink();
41
+ }
42
+ catch {
43
+ return false;
44
+ }
45
+ }
46
+ // ── HMAC signing / verification ────────────────────────────────────────
47
+ /**
48
+ * Sign a payload with HMAC-SHA256, returning the first 16 hex chars.
49
+ * Truncated to 8 bytes (16 hex chars) to fit within Telegram's 64-byte
50
+ * callback_data limit. Online brute-force is infeasible due to Telegram rate limits.
51
+ */
52
+ export function hmacSign(secret, payload) {
53
+ return crypto
54
+ .createHmac('sha256', secret)
55
+ .update(payload)
56
+ .digest('hex')
57
+ .slice(0, 16);
58
+ }
59
+ /**
60
+ * Verify an HMAC signature using constant-time comparison.
61
+ * Returns true if the signature is valid.
62
+ */
63
+ export function hmacVerify(secret, payload, signature) {
64
+ const expected = hmacSign(secret, payload);
65
+ if (expected.length !== signature.length)
66
+ return false;
67
+ try {
68
+ return crypto.timingSafeEqual(Buffer.from(expected, 'utf8'), Buffer.from(signature, 'utf8'));
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ // ── HTML escaping ──────────────────────────────────────────────────────
75
+ const HTML_ESCAPE_MAP = {
76
+ '<': '&lt;',
77
+ '>': '&gt;',
78
+ '&': '&amp;',
79
+ '"': '&quot;',
80
+ };
81
+ /** Escape HTML special characters for safe Telegram HTML output. */
82
+ export function htmlEscape(str) {
83
+ return str.replace(/[<>&"]/g, (ch) => HTML_ESCAPE_MAP[ch] ?? ch);
84
+ }
85
+ // ── ID validation ──────────────────────────────────────────────────────
86
+ const GROUP_ID_RE = /^-?\d+$/;
87
+ const THREAD_ID_RE = /^\d+$/;
88
+ /** Validate a Telegram group ID (may be negative). */
89
+ export function validateGroupId(id) {
90
+ return GROUP_ID_RE.test(id);
91
+ }
92
+ /** Validate a Telegram thread ID (positive integer). */
93
+ export function validateThreadId(id) {
94
+ return THREAD_ID_RE.test(id);
95
+ }
96
+ // ── Callback data handling ─────────────────────────────────────────────
97
+ const CALLBACK_RE = /^tm:[a-z0-9]+:[a-z0-9-]+:-?\d+:\d+:[a-f0-9]+$/;
98
+ /**
99
+ * Build callback data string with HMAC signature.
100
+ * Format: tm:<action>:<slug>:<groupId>:<threadId>:<hmac>
101
+ */
102
+ export function buildCallbackData(action, slug, groupId, threadId, secret) {
103
+ const payload = `tm:${action}:${slug}:${groupId}:${threadId}`;
104
+ const sig = hmacSign(secret, payload);
105
+ return `${payload}:${sig}`;
106
+ }
107
+ /**
108
+ * Parse and verify callback data.
109
+ * Returns the parsed data or null if verification fails.
110
+ *
111
+ * Checks:
112
+ * 1. Format matches the expected regex
113
+ * 2. HMAC is valid
114
+ * 3. groupId and threadId match the context (prevents cross-topic tampering)
115
+ */
116
+ export function parseAndVerifyCallback(data, secret, contextGroupId, contextThreadId) {
117
+ if (!CALLBACK_RE.test(data))
118
+ return null;
119
+ const parts = data.split(':');
120
+ // tm : action : slug : groupId : threadId : hmac
121
+ if (parts.length !== 6)
122
+ return null;
123
+ const [, action, slug, groupId, threadId, signature] = parts;
124
+ // Verify context match (prevent cross-topic tampering)
125
+ if (groupId !== contextGroupId || threadId !== contextThreadId)
126
+ return null;
127
+ // Verify HMAC
128
+ const payload = `tm:${action}:${slug}:${groupId}:${threadId}`;
129
+ if (!hmacVerify(secret, payload, signature))
130
+ return null;
131
+ return { action, slug, groupId, threadId };
132
+ }
133
+ //# sourceMappingURL=security.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.js","sourceRoot":"","sources":["../../src/lib/security.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import type { TopicEntry, DoctorCheckResult, InlineKeyboardButton, InlineKeyboardMarkup } from './types.js';
2
+ import type { TopicType } from './types.js';
3
+ export type { InlineKeyboardButton, InlineKeyboardMarkup } from './types.js';
4
+ export interface RateLimitConfig {
5
+ sameGroupDelayMs: number;
6
+ crossGroupDelayMs: number;
7
+ }
8
+ export declare const DEFAULT_RATE_LIMIT: RateLimitConfig;
9
+ /**
10
+ * Build an InlineKeyboardMarkup from rows of buttons.
11
+ */
12
+ export declare function buildInlineKeyboard(rows: InlineKeyboardButton[][]): InlineKeyboardMarkup;
13
+ /**
14
+ * Build inline keyboard buttons for a doctor report.
15
+ */
16
+ export declare function buildDoctorButtons(slug: string, groupId: string, threadId: string, secret: string): InlineKeyboardMarkup;
17
+ /**
18
+ * Build inline keyboard with a [Confirm] button for slug confirmation (init step 1).
19
+ */
20
+ export declare function buildInitSlugButtons(slug: string, groupId: string, threadId: string, secret: string): InlineKeyboardMarkup;
21
+ /**
22
+ * Build inline keyboard with type picker buttons for init step 2.
23
+ */
24
+ export declare function buildInitTypeButtons(slug: string, groupId: string, threadId: string, secret: string): InlineKeyboardMarkup;
25
+ /**
26
+ * Build HTML Topic Card displayed after init.
27
+ */
28
+ export declare function buildTopicCard(slug: string, type: TopicType, capsuleVersion: number): string;
29
+ /**
30
+ * Build HTML doctor report with severity icons.
31
+ */
32
+ export declare function buildDoctorReport(slug: string, results: DoctorCheckResult[]): string;
33
+ /**
34
+ * Build HTML help card with command reference.
35
+ */
36
+ export declare function buildHelpCard(): string;
37
+ /**
38
+ * Build compact topic list message in HTML.
39
+ * Groups by status: active first, snoozed, then archived.
40
+ */
41
+ export declare function buildListMessage(topics: TopicEntry[]): string;
42
+ /**
43
+ * Helper that wraps a post function with rate limiting delays.
44
+ * Returns a function that posts messages respecting Telegram rate limits.
45
+ *
46
+ * The postFn should handle the actual Telegram API call.
47
+ * If postFn throws with a 429 status, the helper respects retry_after.
48
+ */
49
+ export declare function createRateLimitedPoster(postFn: (groupId: string, threadId: string, text: string, keyboard?: InlineKeyboardMarkup) => Promise<void>, config?: RateLimitConfig): (groupId: string, threadId: string, text: string, keyboard?: InlineKeyboardMarkup) => Promise<void>;
50
+ /**
51
+ * Truncate a message to fit within Telegram's limit.
52
+ * Appends a truncation indicator if the message was cut.
53
+ */
54
+ export declare function truncateMessage(msg: string, limit?: number): string;
55
+ //# sourceMappingURL=telegram.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telegram.d.ts","sourceRoot":"","sources":["../../src/lib/telegram.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAE5G,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAO5C,YAAY,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAI7E,MAAM,WAAW,eAAe;IAC9B,gBAAgB,EAAE,MAAM,CAAC;IACzB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,kBAAkB,EAAE,eAGhC,CAAC;AAIF;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,oBAAoB,EAAE,EAAE,GAAG,oBAAoB,CAExF;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,oBAAoB,CAatB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,oBAAoB,CAKtB;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,oBAAoB,CAYtB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,GAAG,MAAM,CAiB5F;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,EAAE,GAAG,MAAM,CAoBpF;AAeD;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAiBtC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,MAAM,CAkC7D;AAkBD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,EAC3G,MAAM,GAAE,eAAoC,GAC3C,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,oBAAoB,KAAK,OAAO,CAAC,IAAI,CAAC,CA8BrG;AAyBD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,GAAE,MAA2B,GAAG,MAAM,CAUvF"}