@brianli/kimaki 0.4.72-brianli.1
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/bin.js +2 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/bin.js +87 -0
- package/dist/bot-token.js +121 -0
- package/dist/bot-token.test.js +134 -0
- package/dist/channel-management.js +101 -0
- package/dist/cli-parsing.test.js +89 -0
- package/dist/cli.js +2529 -0
- package/dist/commands/abort.js +82 -0
- package/dist/commands/action-buttons.js +257 -0
- package/dist/commands/add-project.js +114 -0
- package/dist/commands/agent.js +291 -0
- package/dist/commands/ask-question.js +223 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +118 -0
- package/dist/commands/diff.js +128 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +217 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +490 -0
- package/dist/commands/mention-mode.js +51 -0
- package/dist/commands/merge-worktree.js +124 -0
- package/dist/commands/model.js +694 -0
- package/dist/commands/permissions.js +163 -0
- package/dist/commands/queue.js +217 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +116 -0
- package/dist/commands/resume.js +159 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +192 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +159 -0
- package/dist/commands/unset-model.js +152 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +148 -0
- package/dist/commands/verbosity.js +60 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +299 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +110 -0
- package/dist/database.js +1050 -0
- package/dist/db.js +159 -0
- package/dist/db.test.js +49 -0
- package/dist/discord-api.js +28 -0
- package/dist/discord-auth.js +231 -0
- package/dist/discord-auth.test.js +80 -0
- package/dist/discord-bot.js +997 -0
- package/dist/discord-utils.js +560 -0
- package/dist/discord-utils.test.js +115 -0
- package/dist/errors.js +167 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +122 -0
- package/dist/format-tables.test.js +199 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +117 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-consumer.js +232 -0
- package/dist/gateway-consumer.test.js +18 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +35 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +30 -0
- package/dist/generated/internal/class.js +41 -0
- package/dist/generated/internal/prismaNamespace.js +239 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +95 -0
- package/dist/hrana-server.js +416 -0
- package/dist/hrana-server.test.js +368 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +327 -0
- package/dist/ipc-polling.js +251 -0
- package/dist/kimaki-digital-twin.e2e.test.js +165 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +160 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +253 -0
- package/dist/message-formatting.js +433 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode-plugin-loading.e2e.test.js +91 -0
- package/dist/opencode-plugin.js +536 -0
- package/dist/opencode-plugin.test.js +98 -0
- package/dist/opencode.js +409 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/runtime-mode.js +51 -0
- package/dist/runtime-mode.test.js +115 -0
- package/dist/sentry.js +127 -0
- package/dist/session-handler/state.js +151 -0
- package/dist/session-handler.js +1874 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/startup-service.js +153 -0
- package/dist/system-message.js +499 -0
- package/dist/task-runner.js +282 -0
- package/dist/task-schedule.js +191 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +781 -0
- package/dist/tools.js +359 -0
- package/dist/unnest-code-blocks.js +136 -0
- package/dist/unnest-code-blocks.test.js +641 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +109 -0
- package/dist/voice-handler.js +606 -0
- package/dist/voice.js +304 -0
- package/dist/voice.test.js +187 -0
- package/dist/wait-session.js +94 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +727 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +82 -0
- package/schema.prisma +246 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +129 -0
- package/skills/errore/SKILL.md +589 -0
- package/skills/goke/.prettierrc +5 -0
- package/skills/goke/CHANGELOG.md +40 -0
- package/skills/goke/LICENSE +21 -0
- package/skills/goke/README.md +666 -0
- package/skills/goke/SKILL.md +458 -0
- package/skills/goke/package.json +43 -0
- package/skills/goke/src/__test__/coerce.test.ts +411 -0
- package/skills/goke/src/__test__/index.test.ts +1798 -0
- package/skills/goke/src/__test__/types.test-d.ts +111 -0
- package/skills/goke/src/coerce.ts +547 -0
- package/skills/goke/src/goke.ts +1362 -0
- package/skills/goke/src/index.ts +16 -0
- package/skills/goke/src/mri.ts +164 -0
- package/skills/goke/tsconfig.json +15 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/playwriter/SKILL.md +31 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/zustand-centralized-state/SKILL.md +582 -0
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +41 -0
- package/src/__snapshots__/first-session-no-info.md +17 -0
- package/src/__snapshots__/first-session-with-info.md +23 -0
- package/src/__snapshots__/session-1.md +17 -0
- package/src/__snapshots__/session-2.md +5871 -0
- package/src/__snapshots__/session-3.md +17 -0
- package/src/__snapshots__/session-with-tools.md +5871 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +282 -0
- package/src/ai-tool.ts +39 -0
- package/src/bin.ts +108 -0
- package/src/bot-token.test.ts +171 -0
- package/src/bot-token.ts +159 -0
- package/src/channel-management.ts +172 -0
- package/src/cli-parsing.test.ts +132 -0
- package/src/cli.ts +3605 -0
- package/src/commands/abort.ts +112 -0
- package/src/commands/action-buttons.ts +376 -0
- package/src/commands/add-project.ts +152 -0
- package/src/commands/agent.ts +404 -0
- package/src/commands/ask-question.ts +330 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +179 -0
- package/src/commands/diff.ts +165 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +320 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +634 -0
- package/src/commands/mention-mode.ts +77 -0
- package/src/commands/merge-worktree.ts +177 -0
- package/src/commands/model.ts +961 -0
- package/src/commands/permissions.ts +261 -0
- package/src/commands/queue.ts +296 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +242 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +250 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +221 -0
- package/src/commands/unset-model.ts +189 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +193 -0
- package/src/commands/verbosity.ts +88 -0
- package/src/commands/worktree-settings.ts +79 -0
- package/src/commands/worktree.ts +431 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +148 -0
- package/src/database.ts +1530 -0
- package/src/db.test.ts +60 -0
- package/src/db.ts +190 -0
- package/src/discord-api.ts +35 -0
- package/src/discord-bot.ts +1316 -0
- package/src/discord-utils.test.ts +132 -0
- package/src/discord-utils.ts +767 -0
- package/src/errors.ts +213 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/format-tables.test.ts +223 -0
- package/src/format-tables.ts +145 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +176 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +109 -0
- package/src/generated/client.ts +131 -0
- package/src/generated/commonInputTypes.ts +512 -0
- package/src/generated/enums.ts +46 -0
- package/src/generated/internal/class.ts +362 -0
- package/src/generated/internal/prismaNamespace.ts +2251 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1577 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +2104 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1224 -0
- package/src/generated/models/channel_worktrees.ts +1308 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1599 -0
- package/src/generated/models/thread_worktrees.ts +1352 -0
- package/src/generated/models.ts +29 -0
- package/src/heap-monitor.ts +121 -0
- package/src/hrana-server.test.ts +428 -0
- package/src/hrana-server.ts +547 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +461 -0
- package/src/ipc-polling.ts +325 -0
- package/src/kimaki-digital-twin.e2e.test.ts +201 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +203 -0
- package/src/markdown.test.ts +360 -0
- package/src/markdown.ts +410 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +549 -0
- package/src/openai-realtime.ts +362 -0
- package/src/opencode-plugin-loading.e2e.test.ts +112 -0
- package/src/opencode-plugin.test.ts +108 -0
- package/src/opencode-plugin.ts +652 -0
- package/src/opencode.ts +554 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/schema.sql +158 -0
- package/src/sentry.ts +137 -0
- package/src/session-handler/state.ts +232 -0
- package/src/session-handler.ts +2668 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/startup-service.ts +200 -0
- package/src/system-message.ts +568 -0
- package/src/task-runner.ts +425 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +287 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +997 -0
- package/src/tools.ts +432 -0
- package/src/unnest-code-blocks.test.ts +679 -0
- package/src/unnest-code-blocks.ts +168 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +145 -0
- package/src/voice-handler.ts +852 -0
- package/src/voice.test.ts +219 -0
- package/src/voice.ts +444 -0
- package/src/wait-session.ts +147 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-utils.ts +988 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
package/dist/db.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Prisma client initialization with libsql adapter.
|
|
2
|
+
// Uses KIMAKI_DB_URL env var when set (plugin process → Hrana HTTP),
|
|
3
|
+
// otherwise falls back to direct file: access (bot process, CLI subcommands).
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { PrismaLibSql } from '@prisma/adapter-libsql';
|
|
7
|
+
import { PrismaClient, Prisma } from './generated/client.js';
|
|
8
|
+
import { getDataDir } from './config.js';
|
|
9
|
+
import { createLogger, formatErrorWithStack, LogPrefix } from './logger.js';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = path.dirname(__filename);
|
|
13
|
+
export { PrismaClient };
|
|
14
|
+
// Under vitest, clear any inherited KIMAKI_DB_URL from the parent bot process
|
|
15
|
+
// so tests default to file-based access using the auto-isolated temp data dir.
|
|
16
|
+
// Tests that need Hrana (like the e2e test) can set KIMAKI_DB_URL explicitly
|
|
17
|
+
// after import — getDbUrl() reads process.env dynamically on each call.
|
|
18
|
+
if (process.env.KIMAKI_VITEST) {
|
|
19
|
+
delete process.env['KIMAKI_DB_URL'];
|
|
20
|
+
}
|
|
21
|
+
const dbLogger = createLogger(LogPrefix.DB);
|
|
22
|
+
let prismaInstance = null;
|
|
23
|
+
let initPromise = null;
|
|
24
|
+
/**
|
|
25
|
+
* Get the singleton Prisma client instance.
|
|
26
|
+
* Initializes the database on first call, running schema setup if needed.
|
|
27
|
+
*/
|
|
28
|
+
export function getPrisma() {
|
|
29
|
+
if (prismaInstance) {
|
|
30
|
+
return Promise.resolve(prismaInstance);
|
|
31
|
+
}
|
|
32
|
+
if (initPromise) {
|
|
33
|
+
return initPromise;
|
|
34
|
+
}
|
|
35
|
+
initPromise = initializePrisma();
|
|
36
|
+
return initPromise;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the libsql connection URL.
|
|
40
|
+
* KIMAKI_DB_URL is set by the bot when spawning opencode plugin processes,
|
|
41
|
+
* pointing them at the in-process Hrana HTTP server. Future-proof for remote
|
|
42
|
+
* opencode processes on different machines.
|
|
43
|
+
* Without the env var (bot process, CLI subcommands), uses direct file: access.
|
|
44
|
+
*/
|
|
45
|
+
function getDbUrl() {
|
|
46
|
+
if (process.env.KIMAKI_DB_URL) {
|
|
47
|
+
return process.env.KIMAKI_DB_URL;
|
|
48
|
+
}
|
|
49
|
+
const dataDir = getDataDir();
|
|
50
|
+
const dbPath = path.join(dataDir, 'discord-sessions.db');
|
|
51
|
+
return `file:${dbPath}`;
|
|
52
|
+
}
|
|
53
|
+
async function initializePrisma() {
|
|
54
|
+
const dbUrl = getDbUrl();
|
|
55
|
+
const isFileMode = dbUrl.startsWith('file:');
|
|
56
|
+
if (isFileMode) {
|
|
57
|
+
const dataDir = getDataDir();
|
|
58
|
+
try {
|
|
59
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
dbLogger.error(`Failed to create data directory ${dataDir}:`, e.message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
dbLogger.log(`Opening database via: ${dbUrl}`);
|
|
66
|
+
const adapter = new PrismaLibSql({ url: dbUrl });
|
|
67
|
+
const prisma = new PrismaClient({ adapter });
|
|
68
|
+
try {
|
|
69
|
+
if (isFileMode) {
|
|
70
|
+
// WAL mode allows concurrent reads while writing instead of blocking.
|
|
71
|
+
// busy_timeout makes SQLite retry for 5s instead of immediately failing with SQLITE_BUSY.
|
|
72
|
+
// The Hrana server (serving the plugin process) sets the same pragmas on its own connection.
|
|
73
|
+
// PRAGMAs are skipped for HTTP connections — they're connection-scoped and the Hrana
|
|
74
|
+
// server already configures them on its own libsql Database handle.
|
|
75
|
+
await prisma.$executeRawUnsafe('PRAGMA journal_mode = WAL');
|
|
76
|
+
await prisma.$executeRawUnsafe('PRAGMA busy_timeout = 5000');
|
|
77
|
+
}
|
|
78
|
+
// Always run migrations - schema.sql uses IF NOT EXISTS so it's idempotent
|
|
79
|
+
dbLogger.log('Running schema migrations...');
|
|
80
|
+
await migrateSchema(prisma);
|
|
81
|
+
dbLogger.log('Schema migration complete');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
dbLogger.error('Prisma init failed:', formatErrorWithStack(error));
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
prismaInstance = prisma;
|
|
88
|
+
return prisma;
|
|
89
|
+
}
|
|
90
|
+
async function migrateSchema(prisma) {
|
|
91
|
+
const schemaPath = path.join(__dirname, '../src/schema.sql');
|
|
92
|
+
const sql = fs.readFileSync(schemaPath, 'utf-8');
|
|
93
|
+
const statements = sql
|
|
94
|
+
.split(';')
|
|
95
|
+
.map((s) => s
|
|
96
|
+
.split('\n')
|
|
97
|
+
.filter((line) => !line.trimStart().startsWith('--'))
|
|
98
|
+
.join('\n')
|
|
99
|
+
.trim())
|
|
100
|
+
.filter((s) => s.length > 0 &&
|
|
101
|
+
!/^CREATE\s+TABLE\s+["']?sqlite_sequence["']?\s*\(/i.test(s))
|
|
102
|
+
// Make CREATE INDEX idempotent
|
|
103
|
+
.map((s) => s
|
|
104
|
+
.replace(/^CREATE\s+UNIQUE\s+INDEX\b(?!\s+IF)/i, 'CREATE UNIQUE INDEX IF NOT EXISTS')
|
|
105
|
+
.replace(/^CREATE\s+INDEX\b(?!\s+IF)/i, 'CREATE INDEX IF NOT EXISTS'));
|
|
106
|
+
for (const statement of statements) {
|
|
107
|
+
await prisma.$executeRawUnsafe(statement);
|
|
108
|
+
}
|
|
109
|
+
// Migration: add variant column to model tables (for thinking/reasoning level).
|
|
110
|
+
// ALTERs throw if column already exists, so each is wrapped in try/catch.
|
|
111
|
+
const alterStatements = [
|
|
112
|
+
'ALTER TABLE channel_models ADD COLUMN variant TEXT',
|
|
113
|
+
'ALTER TABLE session_models ADD COLUMN variant TEXT',
|
|
114
|
+
'ALTER TABLE global_models ADD COLUMN variant TEXT',
|
|
115
|
+
];
|
|
116
|
+
for (const stmt of alterStatements) {
|
|
117
|
+
try {
|
|
118
|
+
await prisma.$executeRawUnsafe(stmt);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Column already exists – expected on subsequent runs
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Migration: add openai_api_key column to bot_api_keys.
|
|
125
|
+
try {
|
|
126
|
+
await prisma.$executeRawUnsafe('ALTER TABLE bot_api_keys ADD COLUMN openai_api_key TEXT');
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Column already exists
|
|
130
|
+
}
|
|
131
|
+
// Migration: move session_thinking data into session_models.variant.
|
|
132
|
+
// session_thinking table is left in place (not dropped) so older kimaki versions
|
|
133
|
+
// that still reference it won't crash on the same database.
|
|
134
|
+
try {
|
|
135
|
+
// For sessions that already have a model row, copy the thinking value
|
|
136
|
+
await prisma.$executeRawUnsafe(`
|
|
137
|
+
UPDATE session_models SET variant = (
|
|
138
|
+
SELECT thinking_value FROM session_thinking
|
|
139
|
+
WHERE session_thinking.session_id = session_models.session_id
|
|
140
|
+
) WHERE variant IS NULL AND EXISTS (
|
|
141
|
+
SELECT 1 FROM session_thinking WHERE session_thinking.session_id = session_models.session_id
|
|
142
|
+
)
|
|
143
|
+
`);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// session_thinking table may not exist in fresh installs
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Close the Prisma connection.
|
|
151
|
+
*/
|
|
152
|
+
export async function closePrisma() {
|
|
153
|
+
if (prismaInstance) {
|
|
154
|
+
await prismaInstance.$disconnect();
|
|
155
|
+
prismaInstance = null;
|
|
156
|
+
initPromise = null;
|
|
157
|
+
dbLogger.log('Prisma connection closed');
|
|
158
|
+
}
|
|
159
|
+
}
|
package/dist/db.test.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Tests for Prisma client initialization and schema migration.
|
|
2
|
+
// Auto-isolated via VITEST guards in config.ts (temp data dir) and db.ts (clears KIMAKI_DB_URL).
|
|
3
|
+
import { afterAll, describe, expect, test } from 'vitest';
|
|
4
|
+
import { getPrisma, closePrisma } from './db.js';
|
|
5
|
+
import { createPendingWorktree } from './database.js';
|
|
6
|
+
afterAll(async () => {
|
|
7
|
+
await closePrisma();
|
|
8
|
+
});
|
|
9
|
+
describe('getPrisma', () => {
|
|
10
|
+
test('creates sqlite file and migrates schema automatically', async () => {
|
|
11
|
+
const prisma = await getPrisma();
|
|
12
|
+
const session = await prisma.thread_sessions.create({
|
|
13
|
+
data: { thread_id: 'test-thread-123', session_id: 'test-session-456' },
|
|
14
|
+
});
|
|
15
|
+
expect(session.thread_id).toBe('test-thread-123');
|
|
16
|
+
expect(session.created_at).toBeInstanceOf(Date);
|
|
17
|
+
const found = await prisma.thread_sessions.findUnique({
|
|
18
|
+
where: { thread_id: session.thread_id },
|
|
19
|
+
});
|
|
20
|
+
expect(found?.session_id).toBe('test-session-456');
|
|
21
|
+
// Cleanup test data
|
|
22
|
+
await prisma.thread_sessions.delete({
|
|
23
|
+
where: { thread_id: 'test-thread-123' },
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
test('createPendingWorktree creates parent and child rows', async () => {
|
|
27
|
+
const prisma = await getPrisma();
|
|
28
|
+
const threadId = `test-worktree-${Date.now()}`;
|
|
29
|
+
await createPendingWorktree({
|
|
30
|
+
threadId,
|
|
31
|
+
worktreeName: 'regression-worktree',
|
|
32
|
+
projectDirectory: '/tmp/regression-project',
|
|
33
|
+
});
|
|
34
|
+
const session = await prisma.thread_sessions.findUnique({
|
|
35
|
+
where: { thread_id: threadId },
|
|
36
|
+
});
|
|
37
|
+
expect(session).toBeTruthy();
|
|
38
|
+
expect(session?.session_id).toBe('');
|
|
39
|
+
const worktree = await prisma.thread_worktrees.findUnique({
|
|
40
|
+
where: { thread_id: threadId },
|
|
41
|
+
});
|
|
42
|
+
expect(worktree).toBeTruthy();
|
|
43
|
+
expect(worktree?.worktree_name).toBe('regression-worktree');
|
|
44
|
+
expect(worktree?.project_directory).toBe('/tmp/regression-project');
|
|
45
|
+
expect(worktree?.status).toBe('pending');
|
|
46
|
+
await prisma.thread_worktrees.delete({ where: { thread_id: threadId } });
|
|
47
|
+
await prisma.thread_sessions.delete({ where: { thread_id: threadId } });
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { REST } from 'discord.js';
|
|
2
|
+
const DISCORD_API_BASE_URL = 'https://discord.com/api';
|
|
3
|
+
function normalizeApiBaseUrl(rawValue) {
|
|
4
|
+
const trimmed = rawValue?.trim();
|
|
5
|
+
if (!trimmed) {
|
|
6
|
+
return DISCORD_API_BASE_URL;
|
|
7
|
+
}
|
|
8
|
+
const withoutTrailingSlash = trimmed.replace(/\/+$/, '');
|
|
9
|
+
if (withoutTrailingSlash.endsWith('/api')) {
|
|
10
|
+
return withoutTrailingSlash;
|
|
11
|
+
}
|
|
12
|
+
return `${withoutTrailingSlash}/api`;
|
|
13
|
+
}
|
|
14
|
+
export function getDiscordApiBaseUrl() {
|
|
15
|
+
return normalizeApiBaseUrl(process.env.KIMAKI_DISCORD_HTTP_URL);
|
|
16
|
+
}
|
|
17
|
+
export function getDiscordApiV10BaseUrl() {
|
|
18
|
+
return `${getDiscordApiBaseUrl()}/v10`;
|
|
19
|
+
}
|
|
20
|
+
export function createDiscordRest(token) {
|
|
21
|
+
const rest = new REST({
|
|
22
|
+
api: getDiscordApiBaseUrl(),
|
|
23
|
+
});
|
|
24
|
+
if (token) {
|
|
25
|
+
rest.setToken(token);
|
|
26
|
+
}
|
|
27
|
+
return rest;
|
|
28
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// Shared Discord auth + endpoint helpers used by bot + CLI + plugin.
|
|
2
|
+
// Supports:
|
|
3
|
+
// - Proxy auth via KIMAKI_PRIVATE_KEY + KIMAKI_GUILD_ID
|
|
4
|
+
// - Shared authorization header format: Ed25519 <guild_id>.<timestamp>.<signature>
|
|
5
|
+
// where the signature input is `${guild_id}\n${timestamp}`.
|
|
6
|
+
// - Distinct configurable endpoints:
|
|
7
|
+
// - KIMAKI_DISCORD_WS_URL (default wss://gateway.discord.gg)
|
|
8
|
+
// - KIMAKI_DISCORD_HTTP_URL (default https://discord.com/api)
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
import { REST } from 'discord.js';
|
|
11
|
+
const DEFAULT_DISCORD_WS_URL = 'wss://gateway.discord.gg';
|
|
12
|
+
const DEFAULT_DISCORD_HTTP_URL = 'https://discord.com/api';
|
|
13
|
+
export function getDiscordAuthConfig() {
|
|
14
|
+
const httpBaseUrl = normalizeDiscordHttpBaseUrl(process.env.KIMAKI_DISCORD_HTTP_URL);
|
|
15
|
+
const wsUrl = normalizeDiscordWsUrl(process.env.KIMAKI_DISCORD_WS_URL);
|
|
16
|
+
const privateKey = trimEnv(process.env.KIMAKI_PRIVATE_KEY);
|
|
17
|
+
const guildId = trimEnv(process.env.KIMAKI_GUILD_ID) || trimEnv(process.env.KIMAKI_GUILD);
|
|
18
|
+
return {
|
|
19
|
+
httpBaseUrl,
|
|
20
|
+
wsUrl,
|
|
21
|
+
privateKey,
|
|
22
|
+
guildId,
|
|
23
|
+
proxyAuthEnabled: Boolean(privateKey && guildId),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function getDiscordWsUrl() {
|
|
27
|
+
return getDiscordAuthConfig().wsUrl;
|
|
28
|
+
}
|
|
29
|
+
export function getDiscordHttpBaseUrl() {
|
|
30
|
+
return getDiscordAuthConfig().httpBaseUrl;
|
|
31
|
+
}
|
|
32
|
+
export function isProxyDiscordAuthEnabled() {
|
|
33
|
+
return getDiscordAuthConfig().proxyAuthEnabled;
|
|
34
|
+
}
|
|
35
|
+
export function buildDiscordApiRoute(pathname) {
|
|
36
|
+
const normalizedPath = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
|
37
|
+
return `${getDiscordHttpBaseUrl()}/v10${normalizedPath}`;
|
|
38
|
+
}
|
|
39
|
+
export function createDiscordRest(botToken) {
|
|
40
|
+
const { proxyAuthEnabled, privateKey, guildId } = getDiscordAuthConfig();
|
|
41
|
+
const effectiveBotToken = proxyAuthEnabled ? undefined : botToken;
|
|
42
|
+
if (!effectiveBotToken && proxyAuthEnabled) {
|
|
43
|
+
ensureProxyAuthReady({ privateKey, guildId });
|
|
44
|
+
}
|
|
45
|
+
const options = createDiscordRestOptions({
|
|
46
|
+
botToken: effectiveBotToken,
|
|
47
|
+
privateKey,
|
|
48
|
+
guildId,
|
|
49
|
+
proxyAuthEnabled,
|
|
50
|
+
});
|
|
51
|
+
const rest = new REST(options);
|
|
52
|
+
if (effectiveBotToken) {
|
|
53
|
+
rest.setToken(effectiveBotToken);
|
|
54
|
+
}
|
|
55
|
+
return rest;
|
|
56
|
+
}
|
|
57
|
+
export function createDiscordRestOptions({ botToken, privateKey, guildId, proxyAuthEnabled, }) {
|
|
58
|
+
const options = {
|
|
59
|
+
api: getDiscordHttpBaseUrl(),
|
|
60
|
+
makeRequest: createDiscordMakeRequest({
|
|
61
|
+
botToken,
|
|
62
|
+
privateKey,
|
|
63
|
+
guildId,
|
|
64
|
+
proxyAuthEnabled,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
if (botToken) {
|
|
68
|
+
options.authPrefix = 'Bot';
|
|
69
|
+
}
|
|
70
|
+
return options;
|
|
71
|
+
}
|
|
72
|
+
export function resolveDiscordAuthHeader(botToken) {
|
|
73
|
+
const { privateKey, guildId, proxyAuthEnabled } = getDiscordAuthConfig();
|
|
74
|
+
const effectiveBotToken = proxyAuthEnabled ? undefined : botToken;
|
|
75
|
+
if (effectiveBotToken) {
|
|
76
|
+
return { value: `Bot ${effectiveBotToken}` };
|
|
77
|
+
}
|
|
78
|
+
if (!proxyAuthEnabled) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
const authHeader = buildProxyAuthHeader({ privateKey, guildId });
|
|
82
|
+
if (!authHeader) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return { value: authHeader };
|
|
86
|
+
}
|
|
87
|
+
export function createDiscordHeaders(botToken) {
|
|
88
|
+
const headers = new Headers();
|
|
89
|
+
const authorization = resolveDiscordAuthHeader(botToken);
|
|
90
|
+
if (authorization) {
|
|
91
|
+
headers.set('Authorization', authorization.value);
|
|
92
|
+
}
|
|
93
|
+
return headers;
|
|
94
|
+
}
|
|
95
|
+
export function createDiscordFetchHeaders({ botToken, existingHeaders, }) {
|
|
96
|
+
const headers = new Headers(existingHeaders);
|
|
97
|
+
const auth = resolveDiscordAuthHeader(botToken);
|
|
98
|
+
if (!auth) {
|
|
99
|
+
return headers;
|
|
100
|
+
}
|
|
101
|
+
headers.set('Authorization', auth.value);
|
|
102
|
+
return headers;
|
|
103
|
+
}
|
|
104
|
+
function createDiscordMakeRequest({ botToken, privateKey, guildId, proxyAuthEnabled, }) {
|
|
105
|
+
const wsUrl = getDiscordWsUrl();
|
|
106
|
+
const makeRequest = async (request, init) => {
|
|
107
|
+
const headers = new Headers(init.headers);
|
|
108
|
+
const authorization = botToken
|
|
109
|
+
? `Bot ${botToken}`
|
|
110
|
+
: buildProxyAuthHeader({ privateKey, guildId });
|
|
111
|
+
if (authorization) {
|
|
112
|
+
headers.set('Authorization', authorization);
|
|
113
|
+
}
|
|
114
|
+
const response = await fetch(request, {
|
|
115
|
+
...init,
|
|
116
|
+
headers,
|
|
117
|
+
});
|
|
118
|
+
if (!isGatewayBotEndpoint(request)) {
|
|
119
|
+
return response;
|
|
120
|
+
}
|
|
121
|
+
if (!proxyAuthEnabled) {
|
|
122
|
+
return response;
|
|
123
|
+
}
|
|
124
|
+
const body = await response.text();
|
|
125
|
+
const updatedBody = overrideGatewayUrl({ body, wsUrl });
|
|
126
|
+
if (!updatedBody) {
|
|
127
|
+
return new Response(body, {
|
|
128
|
+
status: response.status,
|
|
129
|
+
statusText: response.statusText,
|
|
130
|
+
headers: response.headers,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return new Response(updatedBody, {
|
|
134
|
+
status: response.status,
|
|
135
|
+
statusText: response.statusText,
|
|
136
|
+
headers: response.headers,
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
return makeRequest;
|
|
140
|
+
}
|
|
141
|
+
function overrideGatewayUrl({ body, wsUrl, }) {
|
|
142
|
+
try {
|
|
143
|
+
const payload = JSON.parse(body);
|
|
144
|
+
if (!payload || typeof payload !== 'object' || !('url' in payload)) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
if (typeof payload.url !== 'string') {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
return JSON.stringify({ ...payload, url: wsUrl });
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function isGatewayBotEndpoint(url) {
|
|
157
|
+
try {
|
|
158
|
+
return new URL(url).pathname.endsWith('/gateway/bot');
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function buildProxyAuthHeader({ privateKey, guildId, }) {
|
|
165
|
+
if (!privateKey || !guildId) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
const privateSigningKey = parsePrivateKey(privateKey);
|
|
169
|
+
if (!privateSigningKey) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const timestamp = Date.now().toString();
|
|
173
|
+
const message = `${guildId}\n${timestamp}`;
|
|
174
|
+
const signature = crypto
|
|
175
|
+
.sign(null, Buffer.from(message, 'utf8'), privateSigningKey)
|
|
176
|
+
.toString('base64url');
|
|
177
|
+
return `Ed25519 ${guildId}.${timestamp}.${signature}`;
|
|
178
|
+
}
|
|
179
|
+
function ensureProxyAuthReady({ privateKey, guildId, }) {
|
|
180
|
+
if (!guildId) {
|
|
181
|
+
throw new Error('Discord proxy auth requires KIMAKI_GUILD_ID');
|
|
182
|
+
}
|
|
183
|
+
if (!privateKey) {
|
|
184
|
+
throw new Error('Discord proxy auth requires KIMAKI_PRIVATE_KEY');
|
|
185
|
+
}
|
|
186
|
+
const parsedKey = parsePrivateKey(privateKey);
|
|
187
|
+
if (!parsedKey) {
|
|
188
|
+
throw new Error('Discord proxy auth key is not a valid Ed25519 private key');
|
|
189
|
+
}
|
|
190
|
+
void parsedKey;
|
|
191
|
+
}
|
|
192
|
+
function parsePrivateKey(privateKey) {
|
|
193
|
+
const normalized = privateKey.trim();
|
|
194
|
+
if (!normalized) {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
if (normalized.includes('BEGIN PRIVATE KEY')) {
|
|
198
|
+
return normalized;
|
|
199
|
+
}
|
|
200
|
+
const candidates = [
|
|
201
|
+
{ key: Buffer.from(normalized, 'base64'), format: 'der', type: 'pkcs8' },
|
|
202
|
+
{ key: Buffer.from(normalized, 'hex'), format: 'der', type: 'pkcs8' },
|
|
203
|
+
];
|
|
204
|
+
for (const candidate of candidates) {
|
|
205
|
+
try {
|
|
206
|
+
return crypto.createPrivateKey(candidate);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
function normalizeDiscordWsUrl(rawWsUrl) {
|
|
215
|
+
const raw = trimEnv(rawWsUrl) || DEFAULT_DISCORD_WS_URL;
|
|
216
|
+
return new URL(raw).toString().replace(/\/$/, '');
|
|
217
|
+
}
|
|
218
|
+
function normalizeDiscordHttpBaseUrl(rawHttpUrl) {
|
|
219
|
+
const base = new URL(trimEnv(rawHttpUrl) || DEFAULT_DISCORD_HTTP_URL);
|
|
220
|
+
const sanitizedPath = base.pathname === '/'
|
|
221
|
+
? '/api'
|
|
222
|
+
: base.pathname.replace(/\/$/, '').replace(/\/v10\/?$/, '');
|
|
223
|
+
base.pathname = sanitizedPath.endsWith('/api')
|
|
224
|
+
? sanitizedPath
|
|
225
|
+
: `${sanitizedPath}/api`;
|
|
226
|
+
return base.toString().replace(/\/$/, '');
|
|
227
|
+
}
|
|
228
|
+
function trimEnv(value) {
|
|
229
|
+
const trimmed = value?.trim();
|
|
230
|
+
return trimmed || undefined;
|
|
231
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { createDiscordFetchHeaders, resolveDiscordAuthHeader, } from './discord-auth.js';
|
|
4
|
+
describe('Discord proxy auth header', () => {
|
|
5
|
+
const originalPrivateKey = process.env.KIMAKI_PRIVATE_KEY;
|
|
6
|
+
const originalGuildId = process.env.KIMAKI_GUILD_ID;
|
|
7
|
+
test('writes valid Ed25519 Authorization header from proxy env vars', () => {
|
|
8
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
|
|
9
|
+
const pemPrivateKey = privateKey.export({
|
|
10
|
+
type: 'pkcs8',
|
|
11
|
+
format: 'pem',
|
|
12
|
+
});
|
|
13
|
+
const pemPublicKey = publicKey.export({
|
|
14
|
+
type: 'spki',
|
|
15
|
+
format: 'pem',
|
|
16
|
+
});
|
|
17
|
+
const guildId = '987654321098765432';
|
|
18
|
+
process.env.KIMAKI_PRIVATE_KEY = pemPrivateKey;
|
|
19
|
+
process.env.KIMAKI_GUILD_ID = guildId;
|
|
20
|
+
const headers = createDiscordFetchHeaders({ botToken: undefined });
|
|
21
|
+
const authHeader = headers.get('Authorization');
|
|
22
|
+
process.env.KIMAKI_PRIVATE_KEY = originalPrivateKey;
|
|
23
|
+
process.env.KIMAKI_GUILD_ID = originalGuildId;
|
|
24
|
+
expect(authHeader).toBeTruthy();
|
|
25
|
+
if (!authHeader) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const [prefix, value] = authHeader.split(' ', 2);
|
|
29
|
+
expect(prefix).toBe('Ed25519');
|
|
30
|
+
expect(value).toBeDefined();
|
|
31
|
+
const parts = value ? value.split('.') : null;
|
|
32
|
+
expect(parts).toHaveLength(3);
|
|
33
|
+
if (!parts) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const [tokenGuildId, timestamp, signatureBase64url] = parts;
|
|
37
|
+
expect(tokenGuildId).toBe(guildId);
|
|
38
|
+
expect(Number(timestamp)).not.toBeNaN();
|
|
39
|
+
const signature = Buffer.from(signatureBase64url, 'base64url');
|
|
40
|
+
const message = `${guildId}\n${timestamp}`;
|
|
41
|
+
const isValidSignature = crypto.verify(null, Buffer.from(message, 'utf8'), pemPublicKey, signature);
|
|
42
|
+
expect(isValidSignature).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
test('uses bot token header format when token is present', () => {
|
|
45
|
+
process.env.KIMAKI_PRIVATE_KEY = '';
|
|
46
|
+
process.env.KIMAKI_GUILD_ID = '';
|
|
47
|
+
const headers = resolveDiscordAuthHeader('bot-token-123');
|
|
48
|
+
process.env.KIMAKI_PRIVATE_KEY = originalPrivateKey;
|
|
49
|
+
process.env.KIMAKI_GUILD_ID = originalGuildId;
|
|
50
|
+
expect(headers).toEqual({ value: 'Bot bot-token-123' });
|
|
51
|
+
});
|
|
52
|
+
test('prefers proxy auth over bot token when private key env vars are set', () => {
|
|
53
|
+
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
|
|
54
|
+
const pemPrivateKey = privateKey.export({
|
|
55
|
+
type: 'pkcs8',
|
|
56
|
+
format: 'pem',
|
|
57
|
+
});
|
|
58
|
+
const pemPublicKey = publicKey.export({
|
|
59
|
+
type: 'spki',
|
|
60
|
+
format: 'pem',
|
|
61
|
+
});
|
|
62
|
+
const guildId = '111111111111111111';
|
|
63
|
+
process.env.KIMAKI_PRIVATE_KEY = pemPrivateKey;
|
|
64
|
+
process.env.KIMAKI_GUILD_ID = guildId;
|
|
65
|
+
const headers = createDiscordFetchHeaders({ botToken: 'db-bot-token' });
|
|
66
|
+
const authHeader = headers.get('Authorization');
|
|
67
|
+
process.env.KIMAKI_PRIVATE_KEY = originalPrivateKey;
|
|
68
|
+
process.env.KIMAKI_GUILD_ID = originalGuildId;
|
|
69
|
+
expect(authHeader).toBeTruthy();
|
|
70
|
+
if (!authHeader) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const [prefix, value] = authHeader.split(' ', 2);
|
|
74
|
+
expect(prefix).toBe('Ed25519');
|
|
75
|
+
const [tokenGuildId, timestamp, signatureBase64url] = value?.split('.');
|
|
76
|
+
expect(tokenGuildId).toBe(guildId);
|
|
77
|
+
const isValidSignature = crypto.verify(null, Buffer.from(`${guildId}\n${timestamp}`, 'utf8'), pemPublicKey, Buffer.from(signatureBase64url, 'base64url'));
|
|
78
|
+
expect(isValidSignature).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
});
|