thepopebot 1.2.57 → 1.2.59
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/README.md +16 -0
- package/api/index.js +3 -6
- package/bin/cli.js +89 -33
- package/config/index.js +2 -0
- package/lib/auth/config.js +2 -20
- package/lib/auth/edge-config.js +26 -0
- package/lib/auth/middleware.js +4 -1
- package/lib/channels/telegram.js +8 -6
- package/lib/db/api-keys.js +5 -2
- package/package.json +1 -1
- /package/templates/{gitignore → .gitignore.template} +0 -0
- /package/templates/{CLAUDE.md → CLAUDE.md.template} +0 -0
- /package/templates/api/{CLAUDE.md → CLAUDE.md.template} +0 -0
package/README.md
CHANGED
|
@@ -200,6 +200,22 @@ Pushing to `main` triggers the `rebuild-event-handler.yml` workflow on your serv
|
|
|
200
200
|
|
|
201
201
|
---
|
|
202
202
|
|
|
203
|
+
## Template File Conventions
|
|
204
|
+
|
|
205
|
+
The `templates/` directory contains files scaffolded into user projects by `thepopebot init`. Two naming conventions handle files that npm or AI tools would otherwise misinterpret:
|
|
206
|
+
|
|
207
|
+
**`.template` suffix** — Files ending in `.template` are scaffolded with the suffix stripped. This is used for files that npm mangles (`.gitignore`) or that AI tools would pick up as real project docs (`CLAUDE.md`).
|
|
208
|
+
|
|
209
|
+
| In `templates/` | Scaffolded as |
|
|
210
|
+
|-----------------|---------------|
|
|
211
|
+
| `.gitignore.template` | `.gitignore` |
|
|
212
|
+
| `CLAUDE.md.template` | `CLAUDE.md` |
|
|
213
|
+
| `api/CLAUDE.md.template` | `api/CLAUDE.md` |
|
|
214
|
+
|
|
215
|
+
**`CLAUDE.md` exclusion** — The scaffolding walker skips any file named `CLAUDE.md` (without the `.template` suffix). This is a safety net so a bare `CLAUDE.md` accidentally added to `templates/` never gets copied into user projects where AI tools would confuse it with real project instructions.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
203
219
|
## Docs
|
|
204
220
|
|
|
205
221
|
| Document | Description |
|
package/api/index.js
CHANGED
|
@@ -161,12 +161,9 @@ async function processChannelMessage(adapter, normalized) {
|
|
|
161
161
|
async function handleGithubWebhook(request) {
|
|
162
162
|
const { GH_WEBHOOK_SECRET } = process.env;
|
|
163
163
|
|
|
164
|
-
// Validate webhook secret (timing-safe)
|
|
165
|
-
if (GH_WEBHOOK_SECRET) {
|
|
166
|
-
|
|
167
|
-
if (!safeCompare(headerSecret, GH_WEBHOOK_SECRET)) {
|
|
168
|
-
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
169
|
-
}
|
|
164
|
+
// Validate webhook secret (timing-safe, required)
|
|
165
|
+
if (!GH_WEBHOOK_SECRET || !safeCompare(request.headers.get('x-github-webhook-secret-token'), GH_WEBHOOK_SECRET)) {
|
|
166
|
+
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
|
170
167
|
}
|
|
171
168
|
|
|
172
169
|
const payload = await request.json();
|
package/bin/cli.js
CHANGED
|
@@ -25,6 +25,26 @@ function isManaged(relPath) {
|
|
|
25
25
|
return MANAGED_PATHS.some(p => relPath === p || relPath.startsWith(p));
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// Files that must never be scaffolded directly (use .template suffix instead).
|
|
29
|
+
const EXCLUDED_FILENAMES = ['CLAUDE.md'];
|
|
30
|
+
|
|
31
|
+
// Files ending in .template are scaffolded with the suffix stripped.
|
|
32
|
+
// e.g. .gitignore.template → .gitignore, CLAUDE.md.template → CLAUDE.md
|
|
33
|
+
function destPath(templateRelPath) {
|
|
34
|
+
if (templateRelPath.endsWith('.template')) {
|
|
35
|
+
return templateRelPath.slice(0, -'.template'.length);
|
|
36
|
+
}
|
|
37
|
+
return templateRelPath;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function templatePath(userPath, templatesDir) {
|
|
41
|
+
const withSuffix = userPath + '.template';
|
|
42
|
+
if (fs.existsSync(path.join(templatesDir, withSuffix))) {
|
|
43
|
+
return withSuffix;
|
|
44
|
+
}
|
|
45
|
+
return userPath;
|
|
46
|
+
}
|
|
47
|
+
|
|
28
48
|
function printUsage() {
|
|
29
49
|
console.log(`
|
|
30
50
|
Usage: thepopebot <command>
|
|
@@ -49,7 +69,7 @@ function getTemplateFiles(templatesDir) {
|
|
|
49
69
|
const fullPath = path.join(dir, entry.name);
|
|
50
70
|
if (entry.isDirectory()) {
|
|
51
71
|
walk(fullPath);
|
|
52
|
-
} else {
|
|
72
|
+
} else if (!EXCLUDED_FILENAMES.includes(entry.name)) {
|
|
53
73
|
files.push(path.relative(templatesDir, fullPath));
|
|
54
74
|
}
|
|
55
75
|
}
|
|
@@ -58,12 +78,47 @@ function getTemplateFiles(templatesDir) {
|
|
|
58
78
|
return files;
|
|
59
79
|
}
|
|
60
80
|
|
|
61
|
-
function init() {
|
|
62
|
-
|
|
81
|
+
async function init() {
|
|
82
|
+
let cwd = process.cwd();
|
|
63
83
|
const packageDir = path.join(__dirname, '..');
|
|
64
84
|
const templatesDir = path.join(packageDir, 'templates');
|
|
65
85
|
const noManaged = args.includes('--no-managed');
|
|
66
86
|
|
|
87
|
+
// Guard: warn if the directory is not empty (unless it's an existing thepopebot project)
|
|
88
|
+
const entries = fs.readdirSync(cwd);
|
|
89
|
+
if (entries.length > 0) {
|
|
90
|
+
const pkgPath = path.join(cwd, 'package.json');
|
|
91
|
+
let isExistingProject = false;
|
|
92
|
+
if (fs.existsSync(pkgPath)) {
|
|
93
|
+
try {
|
|
94
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
95
|
+
const deps = pkg.dependencies || {};
|
|
96
|
+
const devDeps = pkg.devDependencies || {};
|
|
97
|
+
if (deps.thepopebot || devDeps.thepopebot) {
|
|
98
|
+
isExistingProject = true;
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!isExistingProject) {
|
|
104
|
+
console.log('\nThis directory is not empty.');
|
|
105
|
+
const { default: inquirer } = await import('inquirer');
|
|
106
|
+
const { dirName } = await inquirer.prompt([
|
|
107
|
+
{
|
|
108
|
+
type: 'input',
|
|
109
|
+
name: 'dirName',
|
|
110
|
+
message: 'Project directory name:',
|
|
111
|
+
default: 'my-popebot',
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
const newDir = path.resolve(cwd, dirName);
|
|
115
|
+
fs.mkdirSync(newDir, { recursive: true });
|
|
116
|
+
process.chdir(newDir);
|
|
117
|
+
cwd = newDir;
|
|
118
|
+
console.log(`\nCreated ${dirName}/`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
67
122
|
console.log('\nScaffolding thepopebot project...\n');
|
|
68
123
|
|
|
69
124
|
const templateFiles = getTemplateFiles(templatesDir);
|
|
@@ -74,29 +129,30 @@ function init() {
|
|
|
74
129
|
|
|
75
130
|
for (const relPath of templateFiles) {
|
|
76
131
|
const src = path.join(templatesDir, relPath);
|
|
77
|
-
const
|
|
132
|
+
const outPath = destPath(relPath);
|
|
133
|
+
const dest = path.join(cwd, outPath);
|
|
78
134
|
|
|
79
135
|
if (!fs.existsSync(dest)) {
|
|
80
136
|
// File doesn't exist — create it
|
|
81
137
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
82
138
|
fs.copyFileSync(src, dest);
|
|
83
|
-
created.push(
|
|
84
|
-
console.log(` Created ${
|
|
139
|
+
created.push(outPath);
|
|
140
|
+
console.log(` Created ${outPath}`);
|
|
85
141
|
} else {
|
|
86
142
|
// File exists — check if template has changed
|
|
87
143
|
const srcContent = fs.readFileSync(src);
|
|
88
144
|
const destContent = fs.readFileSync(dest);
|
|
89
145
|
if (srcContent.equals(destContent)) {
|
|
90
|
-
skipped.push(
|
|
91
|
-
} else if (!noManaged && isManaged(
|
|
146
|
+
skipped.push(outPath);
|
|
147
|
+
} else if (!noManaged && isManaged(outPath)) {
|
|
92
148
|
// Managed file differs — auto-update to match package
|
|
93
149
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
94
150
|
fs.copyFileSync(src, dest);
|
|
95
|
-
updated.push(
|
|
96
|
-
console.log(` Updated ${
|
|
151
|
+
updated.push(outPath);
|
|
152
|
+
console.log(` Updated ${outPath}`);
|
|
97
153
|
} else {
|
|
98
|
-
changed.push(
|
|
99
|
-
console.log(` Skipped ${
|
|
154
|
+
changed.push(outPath);
|
|
155
|
+
console.log(` Skipped ${outPath} (already exists)`);
|
|
100
156
|
}
|
|
101
157
|
}
|
|
102
158
|
}
|
|
@@ -163,14 +219,6 @@ function init() {
|
|
|
163
219
|
console.log(' To reset to default: npx thepopebot reset <file>');
|
|
164
220
|
}
|
|
165
221
|
|
|
166
|
-
// Handle gitignore rename (npm strips .gitignore from packages)
|
|
167
|
-
const gitignoreSrc = path.join(templatesDir, 'gitignore');
|
|
168
|
-
const gitignoreDest = path.join(cwd, '.gitignore');
|
|
169
|
-
if (fs.existsSync(gitignoreSrc) && !fs.existsSync(gitignoreDest)) {
|
|
170
|
-
fs.copyFileSync(gitignoreSrc, gitignoreDest);
|
|
171
|
-
console.log(' Created .gitignore');
|
|
172
|
-
}
|
|
173
|
-
|
|
174
222
|
// Run npm install
|
|
175
223
|
console.log('\nInstalling dependencies...\n');
|
|
176
224
|
execSync('npm install', { stdio: 'inherit', cwd });
|
|
@@ -207,14 +255,15 @@ function reset(filePath) {
|
|
|
207
255
|
console.log('\nAvailable template files:\n');
|
|
208
256
|
const files = getTemplateFiles(templatesDir);
|
|
209
257
|
for (const file of files) {
|
|
210
|
-
console.log(` ${file}`);
|
|
258
|
+
console.log(` ${destPath(file)}`);
|
|
211
259
|
}
|
|
212
260
|
console.log('\nUsage: thepopebot reset <file>');
|
|
213
261
|
console.log('Example: thepopebot reset config/SOUL.md\n');
|
|
214
262
|
return;
|
|
215
263
|
}
|
|
216
264
|
|
|
217
|
-
const
|
|
265
|
+
const tmplPath = templatePath(filePath, templatesDir);
|
|
266
|
+
const src = path.join(templatesDir, tmplPath);
|
|
218
267
|
const dest = path.join(cwd, filePath);
|
|
219
268
|
|
|
220
269
|
if (!fs.existsSync(src)) {
|
|
@@ -225,7 +274,7 @@ function reset(filePath) {
|
|
|
225
274
|
|
|
226
275
|
if (fs.statSync(src).isDirectory()) {
|
|
227
276
|
console.log(`\nRestoring ${filePath}/...\n`);
|
|
228
|
-
copyDirSyncForce(src, dest);
|
|
277
|
+
copyDirSyncForce(src, dest, tmplPath);
|
|
229
278
|
} else {
|
|
230
279
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
231
280
|
fs.copyFileSync(src, dest);
|
|
@@ -248,16 +297,17 @@ function diff(filePath) {
|
|
|
248
297
|
let anyDiff = false;
|
|
249
298
|
for (const file of files) {
|
|
250
299
|
const src = path.join(templatesDir, file);
|
|
251
|
-
const
|
|
300
|
+
const outPath = destPath(file);
|
|
301
|
+
const dest = path.join(cwd, outPath);
|
|
252
302
|
if (fs.existsSync(dest)) {
|
|
253
303
|
const srcContent = fs.readFileSync(src);
|
|
254
304
|
const destContent = fs.readFileSync(dest);
|
|
255
305
|
if (!srcContent.equals(destContent)) {
|
|
256
|
-
console.log(` ${
|
|
306
|
+
console.log(` ${outPath}`);
|
|
257
307
|
anyDiff = true;
|
|
258
308
|
}
|
|
259
309
|
} else {
|
|
260
|
-
console.log(` ${
|
|
310
|
+
console.log(` ${outPath} (missing)`);
|
|
261
311
|
anyDiff = true;
|
|
262
312
|
}
|
|
263
313
|
}
|
|
@@ -269,7 +319,8 @@ function diff(filePath) {
|
|
|
269
319
|
return;
|
|
270
320
|
}
|
|
271
321
|
|
|
272
|
-
const
|
|
322
|
+
const tmplPath = templatePath(filePath, templatesDir);
|
|
323
|
+
const src = path.join(templatesDir, tmplPath);
|
|
273
324
|
const dest = path.join(cwd, filePath);
|
|
274
325
|
|
|
275
326
|
if (!fs.existsSync(src)) {
|
|
@@ -293,17 +344,22 @@ function diff(filePath) {
|
|
|
293
344
|
}
|
|
294
345
|
}
|
|
295
346
|
|
|
296
|
-
function copyDirSyncForce(src, dest) {
|
|
347
|
+
function copyDirSyncForce(src, dest, templateRelBase = '') {
|
|
297
348
|
fs.mkdirSync(dest, { recursive: true });
|
|
298
349
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
299
350
|
for (const entry of entries) {
|
|
351
|
+
if (EXCLUDED_FILENAMES.includes(entry.name)) continue;
|
|
300
352
|
const srcPath = path.join(src, entry.name);
|
|
301
|
-
const
|
|
353
|
+
const templateRel = templateRelBase
|
|
354
|
+
? path.join(templateRelBase, entry.name)
|
|
355
|
+
: entry.name;
|
|
356
|
+
const outName = path.basename(destPath(templateRel));
|
|
357
|
+
const destFile = path.join(dest, outName);
|
|
302
358
|
if (entry.isDirectory()) {
|
|
303
|
-
copyDirSyncForce(srcPath,
|
|
359
|
+
copyDirSyncForce(srcPath, destFile, templateRel);
|
|
304
360
|
} else {
|
|
305
|
-
fs.copyFileSync(srcPath,
|
|
306
|
-
console.log(` Restored ${path.relative(process.cwd(),
|
|
361
|
+
fs.copyFileSync(srcPath, destFile);
|
|
362
|
+
console.log(` Restored ${path.relative(process.cwd(), destFile)}`);
|
|
307
363
|
}
|
|
308
364
|
}
|
|
309
365
|
}
|
|
@@ -345,7 +401,7 @@ async function resetAuth() {
|
|
|
345
401
|
|
|
346
402
|
switch (command) {
|
|
347
403
|
case 'init':
|
|
348
|
-
init();
|
|
404
|
+
await init();
|
|
349
405
|
break;
|
|
350
406
|
case 'setup':
|
|
351
407
|
setup();
|
package/config/index.js
CHANGED
package/lib/auth/config.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import NextAuth from 'next-auth';
|
|
2
2
|
import Credentials from 'next-auth/providers/credentials';
|
|
3
|
-
|
|
4
|
-
// DB imports are dynamic inside authorize() so they don't get pulled in
|
|
5
|
-
// at module level — middleware runs on Edge which has no fs/better-sqlite3.
|
|
3
|
+
import { authConfig } from './edge-config.js';
|
|
6
4
|
|
|
7
5
|
export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
6
|
+
...authConfig,
|
|
8
7
|
providers: [
|
|
9
8
|
Credentials({
|
|
10
9
|
credentials: {
|
|
@@ -25,21 +24,4 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
|
25
24
|
},
|
|
26
25
|
}),
|
|
27
26
|
],
|
|
28
|
-
session: { strategy: 'jwt' },
|
|
29
|
-
pages: { signIn: '/login' },
|
|
30
|
-
callbacks: {
|
|
31
|
-
jwt({ token, user }) {
|
|
32
|
-
if (user) {
|
|
33
|
-
token.role = user.role;
|
|
34
|
-
}
|
|
35
|
-
return token;
|
|
36
|
-
},
|
|
37
|
-
session({ session, token }) {
|
|
38
|
-
if (session.user) {
|
|
39
|
-
session.user.id = token.sub;
|
|
40
|
-
session.user.role = token.role;
|
|
41
|
-
}
|
|
42
|
-
return session;
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
27
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge-safe auth configuration — shared between middleware and server.
|
|
3
|
+
* Contains only JWT/session/callbacks/pages config. No providers, no DB imports.
|
|
4
|
+
* Both instances use the same AUTH_SECRET for JWT signing/verification.
|
|
5
|
+
*
|
|
6
|
+
* Official pattern: https://authjs.dev/guides/edge-compatibility
|
|
7
|
+
*/
|
|
8
|
+
export const authConfig = {
|
|
9
|
+
session: { strategy: 'jwt' },
|
|
10
|
+
pages: { signIn: '/login' },
|
|
11
|
+
callbacks: {
|
|
12
|
+
jwt({ token, user }) {
|
|
13
|
+
if (user) {
|
|
14
|
+
token.role = user.role;
|
|
15
|
+
}
|
|
16
|
+
return token;
|
|
17
|
+
},
|
|
18
|
+
session({ session, token }) {
|
|
19
|
+
if (session.user) {
|
|
20
|
+
session.user.id = token.sub;
|
|
21
|
+
session.user.role = token.role;
|
|
22
|
+
}
|
|
23
|
+
return session;
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
package/lib/auth/middleware.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import NextAuth from 'next-auth';
|
|
2
|
+
import { authConfig } from './edge-config.js';
|
|
2
3
|
import { NextResponse } from 'next/server';
|
|
3
4
|
|
|
5
|
+
const { auth } = NextAuth(authConfig);
|
|
6
|
+
|
|
4
7
|
export const middleware = auth((req) => {
|
|
5
8
|
const { pathname } = req.nextUrl;
|
|
6
9
|
|
package/lib/channels/telegram.js
CHANGED
|
@@ -21,12 +21,14 @@ class TelegramAdapter extends ChannelAdapter {
|
|
|
21
21
|
async receive(request) {
|
|
22
22
|
const { TELEGRAM_WEBHOOK_SECRET, TELEGRAM_CHAT_ID, TELEGRAM_VERIFICATION } = process.env;
|
|
23
23
|
|
|
24
|
-
// Validate secret token
|
|
25
|
-
if (TELEGRAM_WEBHOOK_SECRET) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
// Validate secret token (required)
|
|
25
|
+
if (!TELEGRAM_WEBHOOK_SECRET) {
|
|
26
|
+
console.error('[telegram] TELEGRAM_WEBHOOK_SECRET not configured — rejecting webhook');
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const headerSecret = request.headers.get('x-telegram-bot-api-secret-token');
|
|
30
|
+
if (headerSecret !== TELEGRAM_WEBHOOK_SECRET) {
|
|
31
|
+
return null;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
const update = await request.json();
|
package/lib/db/api-keys.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID, randomBytes, createHash } from 'crypto';
|
|
1
|
+
import { randomUUID, randomBytes, createHash, timingSafeEqual } from 'crypto';
|
|
2
2
|
import { eq } from 'drizzle-orm';
|
|
3
3
|
import { getDb } from './index.js';
|
|
4
4
|
import { settings } from './schema.js';
|
|
@@ -137,7 +137,10 @@ export function verifyApiKey(rawKey) {
|
|
|
137
137
|
const keyHash = hashApiKey(rawKey);
|
|
138
138
|
const cached = _ensureCache();
|
|
139
139
|
|
|
140
|
-
if (!cached
|
|
140
|
+
if (!cached) return null;
|
|
141
|
+
const a = Buffer.from(cached.keyHash, 'hex');
|
|
142
|
+
const b = Buffer.from(keyHash, 'hex');
|
|
143
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
|
|
141
144
|
|
|
142
145
|
// Update last_used_at in background (non-blocking)
|
|
143
146
|
try {
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|