opencode-router 0.11.77 → 0.11.79
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge.js +1456 -0
- package/dist/cli.js +553 -0
- package/dist/config.js +195 -0
- package/dist/db.js +196 -0
- package/dist/events.js +11 -0
- package/dist/health.js +499 -0
- package/dist/logger.js +14 -0
- package/dist/opencode.js +34 -0
- package/dist/slack.js +169 -0
- package/dist/telegram.js +78 -0
- package/dist/text.js +41 -0
- package/package.json +1 -1
package/dist/config.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import dotenv from "dotenv";
|
|
6
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const packageDir = path.resolve(moduleDir, "..");
|
|
8
|
+
dotenv.config({ path: path.join(packageDir, ".env") });
|
|
9
|
+
dotenv.config();
|
|
10
|
+
function parseBoolean(value, fallback) {
|
|
11
|
+
if (value === undefined)
|
|
12
|
+
return fallback;
|
|
13
|
+
return ["1", "true", "yes", "on"].includes(value.toLowerCase());
|
|
14
|
+
}
|
|
15
|
+
function parseInteger(value) {
|
|
16
|
+
if (!value)
|
|
17
|
+
return undefined;
|
|
18
|
+
const parsed = Number.parseInt(value, 10);
|
|
19
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
20
|
+
}
|
|
21
|
+
function parseList(value) {
|
|
22
|
+
if (!value)
|
|
23
|
+
return [];
|
|
24
|
+
return value
|
|
25
|
+
.split(",")
|
|
26
|
+
.map((item) => item.trim())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
function parseModel(value) {
|
|
30
|
+
if (!value?.trim())
|
|
31
|
+
return undefined;
|
|
32
|
+
const parts = value.trim().split("/");
|
|
33
|
+
if (parts.length < 2)
|
|
34
|
+
return undefined;
|
|
35
|
+
const providerID = parts[0];
|
|
36
|
+
const modelID = parts.slice(1).join("/");
|
|
37
|
+
if (!providerID || !modelID)
|
|
38
|
+
return undefined;
|
|
39
|
+
return { providerID, modelID };
|
|
40
|
+
}
|
|
41
|
+
function expandHome(value) {
|
|
42
|
+
if (!value.startsWith("~/"))
|
|
43
|
+
return value;
|
|
44
|
+
return path.join(os.homedir(), value.slice(2));
|
|
45
|
+
}
|
|
46
|
+
function resolveConfigPath(dataDir, env) {
|
|
47
|
+
const override = env.OPENCODE_ROUTER_CONFIG_PATH?.trim();
|
|
48
|
+
if (override)
|
|
49
|
+
return expandHome(override);
|
|
50
|
+
return path.join(dataDir, "opencode-router.json");
|
|
51
|
+
}
|
|
52
|
+
export function readConfigFile(configPath) {
|
|
53
|
+
try {
|
|
54
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
55
|
+
const parsed = JSON.parse(raw);
|
|
56
|
+
return { exists: true, config: parsed };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
if (error.code === "ENOENT") {
|
|
60
|
+
return { exists: false, config: { version: 1 } };
|
|
61
|
+
}
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function writeConfigFile(configPath, config) {
|
|
66
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
67
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
68
|
+
}
|
|
69
|
+
function normalizeId(value) {
|
|
70
|
+
const trimmed = value.trim();
|
|
71
|
+
if (!trimmed)
|
|
72
|
+
return "";
|
|
73
|
+
const safe = trimmed.replace(/[^a-zA-Z0-9_.-]+/g, "-");
|
|
74
|
+
return safe.replace(/^-+|-+$/g, "").slice(0, 48) || "default";
|
|
75
|
+
}
|
|
76
|
+
function coerceTelegramBots(file) {
|
|
77
|
+
const telegram = file.channels?.telegram;
|
|
78
|
+
const bots = Array.isArray(telegram?.bots) ? telegram.bots : [];
|
|
79
|
+
const normalized = [];
|
|
80
|
+
for (const entry of bots) {
|
|
81
|
+
if (!entry || typeof entry !== "object")
|
|
82
|
+
continue;
|
|
83
|
+
const record = entry;
|
|
84
|
+
const token = typeof record.token === "string" ? record.token.trim() : "";
|
|
85
|
+
if (!token)
|
|
86
|
+
continue;
|
|
87
|
+
const id = normalizeId(typeof record.id === "string" ? record.id : "default");
|
|
88
|
+
const directory = typeof record.directory === "string" ? record.directory.trim() : "";
|
|
89
|
+
normalized.push({
|
|
90
|
+
id,
|
|
91
|
+
token,
|
|
92
|
+
enabled: record.enabled === undefined ? true : record.enabled === true,
|
|
93
|
+
...(directory ? { directory } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (normalized.length)
|
|
97
|
+
return normalized;
|
|
98
|
+
// Legacy single-bot migration (in-memory).
|
|
99
|
+
const legacyToken = typeof telegram?.token === "string" ? String(telegram.token).trim() : "";
|
|
100
|
+
if (legacyToken) {
|
|
101
|
+
return [{ id: "default", token: legacyToken, enabled: true }];
|
|
102
|
+
}
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
function coerceSlackApps(file) {
|
|
106
|
+
const slack = file.channels?.slack;
|
|
107
|
+
const apps = Array.isArray(slack?.apps) ? slack.apps : [];
|
|
108
|
+
const normalized = [];
|
|
109
|
+
for (const entry of apps) {
|
|
110
|
+
if (!entry || typeof entry !== "object")
|
|
111
|
+
continue;
|
|
112
|
+
const record = entry;
|
|
113
|
+
const botToken = typeof record.botToken === "string" ? record.botToken.trim() : "";
|
|
114
|
+
const appToken = typeof record.appToken === "string" ? record.appToken.trim() : "";
|
|
115
|
+
if (!botToken || !appToken)
|
|
116
|
+
continue;
|
|
117
|
+
const id = normalizeId(typeof record.id === "string" ? record.id : "default");
|
|
118
|
+
const directory = typeof record.directory === "string" ? record.directory.trim() : "";
|
|
119
|
+
normalized.push({
|
|
120
|
+
id,
|
|
121
|
+
botToken,
|
|
122
|
+
appToken,
|
|
123
|
+
enabled: record.enabled === undefined ? true : record.enabled === true,
|
|
124
|
+
...(directory ? { directory } : {}),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (normalized.length)
|
|
128
|
+
return normalized;
|
|
129
|
+
// Legacy single-app migration (in-memory).
|
|
130
|
+
const legacyBot = typeof slack?.botToken === "string" ? String(slack.botToken).trim() : "";
|
|
131
|
+
const legacyApp = typeof slack?.appToken === "string" ? String(slack.appToken).trim() : "";
|
|
132
|
+
if (legacyBot && legacyApp) {
|
|
133
|
+
return [{ id: "default", botToken: legacyBot, appToken: legacyApp, enabled: true }];
|
|
134
|
+
}
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
export function loadConfig(env = process.env, options = {}) {
|
|
138
|
+
const requireOpencode = options.requireOpencode ?? false;
|
|
139
|
+
const defaultDataDir = path.join(os.homedir(), ".openwork", "opencode-router");
|
|
140
|
+
const dataDir = expandHome(env.OPENCODE_ROUTER_DATA_DIR ?? defaultDataDir);
|
|
141
|
+
const dbPath = expandHome(env.OPENCODE_ROUTER_DB_PATH ?? path.join(dataDir, "opencode-router.db"));
|
|
142
|
+
const logFile = expandHome(env.OPENCODE_ROUTER_LOG_FILE ?? path.join(dataDir, "logs", "opencode-router.log"));
|
|
143
|
+
const configPath = resolveConfigPath(dataDir, env);
|
|
144
|
+
let { config: configFile } = readConfigFile(configPath);
|
|
145
|
+
const opencodeDirectory = env.OPENCODE_DIRECTORY?.trim() || configFile.opencodeDirectory || "";
|
|
146
|
+
if (!opencodeDirectory && requireOpencode) {
|
|
147
|
+
throw new Error("OPENCODE_DIRECTORY is required");
|
|
148
|
+
}
|
|
149
|
+
const resolvedDirectory = opencodeDirectory || process.cwd();
|
|
150
|
+
const toolOutputLimit = parseInteger(env.TOOL_OUTPUT_LIMIT) ?? 1200;
|
|
151
|
+
const permissionMode = env.PERMISSION_MODE?.toLowerCase() === "deny" ? "deny" : "allow";
|
|
152
|
+
// Identities are loaded from config. Env vars are still supported as a convenience
|
|
153
|
+
// for single-identity setups.
|
|
154
|
+
const telegramBots = coerceTelegramBots(configFile);
|
|
155
|
+
const slackApps = coerceSlackApps(configFile);
|
|
156
|
+
const envTelegram = env.TELEGRAM_BOT_TOKEN?.trim() ?? "";
|
|
157
|
+
if (envTelegram && !telegramBots.some((bot) => bot.token === envTelegram)) {
|
|
158
|
+
telegramBots.unshift({ id: "env", token: envTelegram, enabled: true });
|
|
159
|
+
}
|
|
160
|
+
const envSlackBot = env.SLACK_BOT_TOKEN?.trim() ?? "";
|
|
161
|
+
const envSlackApp = env.SLACK_APP_TOKEN?.trim() ?? "";
|
|
162
|
+
if (envSlackBot && envSlackApp && !slackApps.some((app) => app.botToken === envSlackBot && app.appToken === envSlackApp)) {
|
|
163
|
+
slackApps.unshift({ id: "env", botToken: envSlackBot, appToken: envSlackApp, enabled: true });
|
|
164
|
+
}
|
|
165
|
+
const healthPort = parseInteger(env.OPENCODE_ROUTER_HEALTH_PORT) ??
|
|
166
|
+
// Convenience alias (common on PaaS / local experiments)
|
|
167
|
+
parseInteger(env.PORT) ??
|
|
168
|
+
3005;
|
|
169
|
+
const model = parseModel(env.OPENCODE_ROUTER_MODEL);
|
|
170
|
+
const telegramEnabledDefault = configFile.channels?.telegram?.enabled ?? true;
|
|
171
|
+
const slackEnabledDefault = configFile.channels?.slack?.enabled ?? true;
|
|
172
|
+
return {
|
|
173
|
+
configPath,
|
|
174
|
+
configFile,
|
|
175
|
+
opencodeUrl: env.OPENCODE_URL?.trim() || configFile.opencodeUrl || "http://127.0.0.1:4096",
|
|
176
|
+
opencodeDirectory: resolvedDirectory,
|
|
177
|
+
opencodeUsername: env.OPENCODE_SERVER_USERNAME?.trim() || undefined,
|
|
178
|
+
opencodePassword: env.OPENCODE_SERVER_PASSWORD?.trim() || undefined,
|
|
179
|
+
model,
|
|
180
|
+
telegramBots: telegramBots.map((bot) => ({ ...bot, enabled: bot.enabled !== false && parseBoolean(env.TELEGRAM_ENABLED, telegramEnabledDefault) })),
|
|
181
|
+
slackApps: slackApps.map((app) => ({
|
|
182
|
+
...app,
|
|
183
|
+
enabled: app.enabled !== false && parseBoolean(env.SLACK_ENABLED, slackEnabledDefault),
|
|
184
|
+
})),
|
|
185
|
+
dataDir,
|
|
186
|
+
dbPath,
|
|
187
|
+
logFile,
|
|
188
|
+
toolUpdatesEnabled: parseBoolean(env.TOOL_UPDATES_ENABLED, false),
|
|
189
|
+
groupsEnabled: parseBoolean(env.GROUPS_ENABLED, configFile.groupsEnabled ?? false),
|
|
190
|
+
permissionMode,
|
|
191
|
+
toolOutputLimit,
|
|
192
|
+
healthPort,
|
|
193
|
+
logLevel: env.LOG_LEVEL?.trim() || "info",
|
|
194
|
+
};
|
|
195
|
+
}
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Database } from "bun:sqlite";
|
|
4
|
+
export class BridgeStore {
|
|
5
|
+
dbPath;
|
|
6
|
+
db;
|
|
7
|
+
constructor(dbPath) {
|
|
8
|
+
this.dbPath = dbPath;
|
|
9
|
+
this.ensureDir();
|
|
10
|
+
this.db = new Database(dbPath, { create: true });
|
|
11
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
12
|
+
this.db.exec(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
14
|
+
channel TEXT NOT NULL,
|
|
15
|
+
identity_id TEXT NOT NULL,
|
|
16
|
+
peer_id TEXT NOT NULL,
|
|
17
|
+
session_id TEXT NOT NULL,
|
|
18
|
+
directory TEXT,
|
|
19
|
+
created_at INTEGER NOT NULL,
|
|
20
|
+
updated_at INTEGER NOT NULL,
|
|
21
|
+
PRIMARY KEY (channel, identity_id, peer_id)
|
|
22
|
+
);
|
|
23
|
+
CREATE TABLE IF NOT EXISTS allowlist (
|
|
24
|
+
channel TEXT NOT NULL,
|
|
25
|
+
peer_id TEXT NOT NULL,
|
|
26
|
+
created_at INTEGER NOT NULL,
|
|
27
|
+
PRIMARY KEY (channel, peer_id)
|
|
28
|
+
);
|
|
29
|
+
CREATE TABLE IF NOT EXISTS bindings (
|
|
30
|
+
channel TEXT NOT NULL,
|
|
31
|
+
identity_id TEXT NOT NULL,
|
|
32
|
+
peer_id TEXT NOT NULL,
|
|
33
|
+
directory TEXT NOT NULL,
|
|
34
|
+
created_at INTEGER NOT NULL,
|
|
35
|
+
updated_at INTEGER NOT NULL,
|
|
36
|
+
PRIMARY KEY (channel, identity_id, peer_id)
|
|
37
|
+
);
|
|
38
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
39
|
+
key TEXT PRIMARY KEY,
|
|
40
|
+
value TEXT NOT NULL
|
|
41
|
+
);
|
|
42
|
+
`);
|
|
43
|
+
this.migrate();
|
|
44
|
+
}
|
|
45
|
+
migrate() {
|
|
46
|
+
// Sessions: migrate from legacy (channel, peer_id) PK to identity-scoped PK.
|
|
47
|
+
const sessionColumns = this.db
|
|
48
|
+
.prepare("PRAGMA table_info(sessions)")
|
|
49
|
+
.all();
|
|
50
|
+
const hasSessionIdentity = sessionColumns.some((column) => column.name === "identity_id");
|
|
51
|
+
if (!hasSessionIdentity) {
|
|
52
|
+
this.db.exec(`
|
|
53
|
+
CREATE TABLE IF NOT EXISTS sessions_v2 (
|
|
54
|
+
channel TEXT NOT NULL,
|
|
55
|
+
identity_id TEXT NOT NULL,
|
|
56
|
+
peer_id TEXT NOT NULL,
|
|
57
|
+
session_id TEXT NOT NULL,
|
|
58
|
+
directory TEXT,
|
|
59
|
+
created_at INTEGER NOT NULL,
|
|
60
|
+
updated_at INTEGER NOT NULL,
|
|
61
|
+
PRIMARY KEY (channel, identity_id, peer_id)
|
|
62
|
+
);
|
|
63
|
+
`);
|
|
64
|
+
// Copy existing rows, defaulting identity_id to "default".
|
|
65
|
+
this.db.exec(`
|
|
66
|
+
INSERT OR IGNORE INTO sessions_v2 (channel, identity_id, peer_id, session_id, directory, created_at, updated_at)
|
|
67
|
+
SELECT channel, 'default', peer_id, session_id, NULL, created_at, updated_at FROM sessions;
|
|
68
|
+
`);
|
|
69
|
+
this.db.exec("DROP TABLE sessions");
|
|
70
|
+
this.db.exec("ALTER TABLE sessions_v2 RENAME TO sessions");
|
|
71
|
+
}
|
|
72
|
+
// Bindings: migrate from legacy (channel, peer_id) PK to identity-scoped PK.
|
|
73
|
+
const bindingColumns = this.db
|
|
74
|
+
.prepare("PRAGMA table_info(bindings)")
|
|
75
|
+
.all();
|
|
76
|
+
const hasBindingIdentity = bindingColumns.some((column) => column.name === "identity_id");
|
|
77
|
+
if (!hasBindingIdentity) {
|
|
78
|
+
this.db.exec(`
|
|
79
|
+
CREATE TABLE IF NOT EXISTS bindings_v2 (
|
|
80
|
+
channel TEXT NOT NULL,
|
|
81
|
+
identity_id TEXT NOT NULL,
|
|
82
|
+
peer_id TEXT NOT NULL,
|
|
83
|
+
directory TEXT NOT NULL,
|
|
84
|
+
created_at INTEGER NOT NULL,
|
|
85
|
+
updated_at INTEGER NOT NULL,
|
|
86
|
+
PRIMARY KEY (channel, identity_id, peer_id)
|
|
87
|
+
);
|
|
88
|
+
`);
|
|
89
|
+
this.db.exec(`
|
|
90
|
+
INSERT OR IGNORE INTO bindings_v2 (channel, identity_id, peer_id, directory, created_at, updated_at)
|
|
91
|
+
SELECT channel, 'default', peer_id, directory, created_at, updated_at FROM bindings;
|
|
92
|
+
`);
|
|
93
|
+
this.db.exec("DROP TABLE bindings");
|
|
94
|
+
this.db.exec("ALTER TABLE bindings_v2 RENAME TO bindings");
|
|
95
|
+
}
|
|
96
|
+
// Cleanup: WhatsApp pairing table is no longer used.
|
|
97
|
+
try {
|
|
98
|
+
this.db.exec("DROP TABLE IF EXISTS pairing_requests");
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// ignore
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
ensureDir() {
|
|
105
|
+
const dir = path.dirname(this.dbPath);
|
|
106
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
107
|
+
}
|
|
108
|
+
getSession(channel, identityId, peerId) {
|
|
109
|
+
const stmt = this.db.prepare("SELECT channel, identity_id, peer_id, session_id, directory, created_at, updated_at FROM sessions WHERE channel = ? AND identity_id = ? AND peer_id = ?");
|
|
110
|
+
const row = stmt.get(channel, identityId, peerId);
|
|
111
|
+
return row ?? null;
|
|
112
|
+
}
|
|
113
|
+
upsertSession(channel, identityId, peerId, sessionId, directory) {
|
|
114
|
+
const now = Date.now();
|
|
115
|
+
const stmt = this.db.prepare(`INSERT INTO sessions (channel, identity_id, peer_id, session_id, directory, created_at, updated_at)
|
|
116
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
117
|
+
ON CONFLICT(channel, identity_id, peer_id) DO UPDATE SET session_id = excluded.session_id, directory = excluded.directory, updated_at = excluded.updated_at`);
|
|
118
|
+
stmt.run(channel, identityId, peerId, sessionId, directory ?? null, now, now);
|
|
119
|
+
}
|
|
120
|
+
deleteSession(channel, identityId, peerId) {
|
|
121
|
+
const stmt = this.db.prepare("DELETE FROM sessions WHERE channel = ? AND identity_id = ? AND peer_id = ?");
|
|
122
|
+
const result = stmt.run(channel, identityId, peerId);
|
|
123
|
+
return result.changes > 0;
|
|
124
|
+
}
|
|
125
|
+
getBinding(channel, identityId, peerId) {
|
|
126
|
+
const stmt = this.db.prepare("SELECT channel, identity_id, peer_id, directory, created_at, updated_at FROM bindings WHERE channel = ? AND identity_id = ? AND peer_id = ?");
|
|
127
|
+
const row = stmt.get(channel, identityId, peerId);
|
|
128
|
+
return row ?? null;
|
|
129
|
+
}
|
|
130
|
+
upsertBinding(channel, identityId, peerId, directory) {
|
|
131
|
+
const now = Date.now();
|
|
132
|
+
const stmt = this.db.prepare(`INSERT INTO bindings (channel, identity_id, peer_id, directory, created_at, updated_at)
|
|
133
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
134
|
+
ON CONFLICT(channel, identity_id, peer_id) DO UPDATE SET directory = excluded.directory, updated_at = excluded.updated_at`);
|
|
135
|
+
stmt.run(channel, identityId, peerId, directory, now, now);
|
|
136
|
+
}
|
|
137
|
+
deleteBinding(channel, identityId, peerId) {
|
|
138
|
+
const stmt = this.db.prepare("DELETE FROM bindings WHERE channel = ? AND identity_id = ? AND peer_id = ?");
|
|
139
|
+
const result = stmt.run(channel, identityId, peerId);
|
|
140
|
+
return result.changes > 0;
|
|
141
|
+
}
|
|
142
|
+
listBindings(filters = {}) {
|
|
143
|
+
const where = [];
|
|
144
|
+
const args = [];
|
|
145
|
+
if (filters.channel) {
|
|
146
|
+
where.push("channel = ?");
|
|
147
|
+
args.push(filters.channel);
|
|
148
|
+
}
|
|
149
|
+
if (filters.identityId) {
|
|
150
|
+
where.push("identity_id = ?");
|
|
151
|
+
args.push(filters.identityId);
|
|
152
|
+
}
|
|
153
|
+
if (filters.directory) {
|
|
154
|
+
where.push("directory = ?");
|
|
155
|
+
args.push(filters.directory);
|
|
156
|
+
}
|
|
157
|
+
const clause = where.length ? ` WHERE ${where.join(" AND ")}` : "";
|
|
158
|
+
const stmt = this.db.prepare(`SELECT channel, identity_id, peer_id, directory, created_at, updated_at FROM bindings${clause} ORDER BY updated_at DESC`);
|
|
159
|
+
return stmt.all(...args);
|
|
160
|
+
}
|
|
161
|
+
isAllowed(channel, peerId) {
|
|
162
|
+
const stmt = this.db.prepare("SELECT channel, peer_id, created_at FROM allowlist WHERE channel = ? AND peer_id = ?");
|
|
163
|
+
return Boolean(stmt.get(channel, peerId));
|
|
164
|
+
}
|
|
165
|
+
allowPeer(channel, peerId) {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
const stmt = this.db.prepare(`INSERT INTO allowlist (channel, peer_id, created_at)
|
|
168
|
+
VALUES (?, ?, ?)
|
|
169
|
+
ON CONFLICT(channel, peer_id) DO UPDATE SET created_at = excluded.created_at`);
|
|
170
|
+
stmt.run(channel, peerId, now);
|
|
171
|
+
}
|
|
172
|
+
seedAllowlist(channel, peers) {
|
|
173
|
+
const insert = this.db.prepare(`INSERT INTO allowlist (channel, peer_id, created_at)
|
|
174
|
+
VALUES (?, ?, ?)
|
|
175
|
+
ON CONFLICT(channel, peer_id) DO NOTHING`);
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
const transaction = this.db.transaction(() => {
|
|
178
|
+
for (const peer of peers) {
|
|
179
|
+
insert.run(channel, peer, now);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
transaction();
|
|
183
|
+
}
|
|
184
|
+
getSetting(key) {
|
|
185
|
+
const stmt = this.db.prepare("SELECT value FROM settings WHERE key = ?");
|
|
186
|
+
const row = stmt.get(key);
|
|
187
|
+
return row?.value ?? null;
|
|
188
|
+
}
|
|
189
|
+
setSetting(key, value) {
|
|
190
|
+
const stmt = this.db.prepare("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value");
|
|
191
|
+
stmt.run(key, value);
|
|
192
|
+
}
|
|
193
|
+
close() {
|
|
194
|
+
this.db.close();
|
|
195
|
+
}
|
|
196
|
+
}
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function normalizeEvent(raw) {
|
|
2
|
+
if (!raw)
|
|
3
|
+
return null;
|
|
4
|
+
if (typeof raw.type === "string") {
|
|
5
|
+
return { type: raw.type, properties: raw.properties };
|
|
6
|
+
}
|
|
7
|
+
if (raw.payload && typeof raw.payload.type === "string") {
|
|
8
|
+
return { type: raw.payload.type, properties: raw.payload.properties };
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|