@nordbyte/nordrelay 0.2.1 → 0.3.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.
@@ -0,0 +1,58 @@
1
+ const TELEGRAM_CAPABILITIES = [
2
+ "text",
3
+ "streaming-edits",
4
+ "typing",
5
+ "inline-buttons",
6
+ "files",
7
+ "photos",
8
+ "voice",
9
+ "topics",
10
+ "webhooks",
11
+ ];
12
+ const PLANNED_CHANNELS = [
13
+ {
14
+ id: "discord",
15
+ label: "Discord",
16
+ capabilities: ["text", "streaming-edits", "typing", "inline-buttons", "files", "photos", "voice"],
17
+ status: "planned",
18
+ notes: "Adapter boundary is ready; runtime integration still needs bot credentials and event mapping.",
19
+ },
20
+ {
21
+ id: "whatsapp",
22
+ label: "WhatsApp",
23
+ capabilities: ["text", "typing", "files", "photos", "voice", "webhooks"],
24
+ status: "planned",
25
+ notes: "Requires a WhatsApp Business provider integration.",
26
+ },
27
+ {
28
+ id: "slack",
29
+ label: "Slack",
30
+ capabilities: ["text", "streaming-edits", "typing", "inline-buttons", "files"],
31
+ status: "planned",
32
+ },
33
+ {
34
+ id: "matrix",
35
+ label: "Matrix",
36
+ capabilities: ["text", "files", "photos", "voice"],
37
+ status: "planned",
38
+ },
39
+ ];
40
+ export class TelegramChannelAdapter {
41
+ id = "telegram";
42
+ label = "Telegram";
43
+ capabilities = new Set(TELEGRAM_CAPABILITIES);
44
+ describe() {
45
+ return {
46
+ id: this.id,
47
+ label: this.label,
48
+ capabilities: [...this.capabilities],
49
+ status: "available",
50
+ };
51
+ }
52
+ }
53
+ export function listChannelDescriptors() {
54
+ return [
55
+ new TelegramChannelAdapter().describe(),
56
+ ...PLANNED_CHANNELS,
57
+ ];
58
+ }
@@ -47,6 +47,8 @@ export class CodexSessionService {
47
47
  const effectiveLaunchProfile = this.activeThreadLaunchProfile ?? this.currentLaunchProfile;
48
48
  const codexFastMode = readCodexFastMode();
49
49
  this.lastObservedFastMode = codexFastMode;
50
+ const attachedThreadFastMode = effectiveLaunchProfile.id === "attached-thread" &&
51
+ effectiveLaunchProfile.approvalPolicy === "never";
50
52
  const info = {
51
53
  agentId: "codex",
52
54
  agentLabel: "Codex",
@@ -58,7 +60,7 @@ export class CodexSessionService {
58
60
  launchProfileBehavior: formatLaunchProfileBehavior(effectiveLaunchProfile),
59
61
  sandboxMode: effectiveLaunchProfile.sandboxMode,
60
62
  approvalPolicy: effectiveLaunchProfile.approvalPolicy,
61
- fastMode: codexFastMode ?? (effectiveLaunchProfile.approvalPolicy === "never"),
63
+ fastMode: attachedThreadFastMode || (codexFastMode ?? (effectiveLaunchProfile.approvalPolicy === "never")),
62
64
  unsafeLaunch: effectiveLaunchProfile.unsafe,
63
65
  capabilities: CODEX_AGENT_CAPABILITIES,
64
66
  };
package/dist/config.js CHANGED
@@ -24,9 +24,16 @@ export function loadConfig() {
24
24
  const telegramNotifyMode = parseNotifyMode(optionalString(process.env.TELEGRAM_NOTIFY_MODE), "minimal");
25
25
  const telegramQuietHours = parseQuietHours(optionalString(process.env.TELEGRAM_QUIET_HOURS));
26
26
  const telegramRedactPatterns = parseOptionalStringList(optionalString(process.env.TELEGRAM_REDACT_PATTERNS));
27
+ const telegramTransport = parseTelegramTransport(optionalString(process.env.TELEGRAM_TRANSPORT));
28
+ const telegramWebhookUrl = optionalString(process.env.TELEGRAM_WEBHOOK_URL);
29
+ const telegramWebhookHost = optionalString(process.env.TELEGRAM_WEBHOOK_HOST) ?? "127.0.0.1";
30
+ const telegramWebhookPort = parsePositiveIntegerEnv(optionalString(process.env.TELEGRAM_WEBHOOK_PORT), 8080, "TELEGRAM_WEBHOOK_PORT");
31
+ const telegramWebhookPath = parseWebhookPath(optionalString(process.env.TELEGRAM_WEBHOOK_PATH));
32
+ const telegramWebhookSecret = optionalString(process.env.TELEGRAM_WEBHOOK_SECRET);
27
33
  const workspace = resolveWorkspace();
28
34
  const workspaceAllowedRoots = parsePathList(optionalString(process.env.WORKSPACE_ALLOWED_ROOTS));
29
35
  const workspaceWarnRoots = parsePathList(optionalString(process.env.WORKSPACE_WARN_ROOTS));
36
+ const stateBackend = parseStateBackend(optionalString(process.env.NORDRELAY_STATE_BACKEND));
30
37
  const maxFileSize = parseMaxFileSize(optionalString(process.env.MAX_FILE_SIZE));
31
38
  const artifactRetentionDays = parsePositiveNumberEnv(optionalString(process.env.ARTIFACT_RETENTION_DAYS), 7, "ARTIFACT_RETENTION_DAYS");
32
39
  const artifactMaxTurnDirs = parsePositiveIntegerEnv(optionalString(process.env.ARTIFACT_MAX_TURNS), 30, "ARTIFACT_MAX_TURNS");
@@ -60,6 +67,11 @@ export function loadConfig() {
60
67
  const voicePreferredBackend = parseVoiceBackendPreference(optionalString(process.env.VOICE_PREFERRED_BACKEND));
61
68
  const voiceDefaultLanguage = optionalString(process.env.VOICE_DEFAULT_LANGUAGE);
62
69
  const voiceTranscribeOnly = parseBooleanEnv(optionalString(process.env.VOICE_TRANSCRIBE_ONLY), false);
70
+ const auditMaxEvents = parsePositiveIntegerEnv(optionalString(process.env.NORDRELAY_AUDIT_MAX_EVENTS), 1000, "NORDRELAY_AUDIT_MAX_EVENTS");
71
+ const sessionLockTtlMs = parseNonNegativeIntegerEnv(optionalString(process.env.NORDRELAY_SESSION_LOCK_TTL_MS), 30 * 60 * 1000, "NORDRELAY_SESSION_LOCK_TTL_MS");
72
+ if (telegramTransport === "webhook" && !telegramWebhookUrl) {
73
+ throw new Error("TELEGRAM_TRANSPORT=webhook requires TELEGRAM_WEBHOOK_URL");
74
+ }
63
75
  return {
64
76
  telegramBotToken,
65
77
  telegramAllowedUserIds,
@@ -79,9 +91,16 @@ export function loadConfig() {
79
91
  telegramNotifyMode,
80
92
  telegramQuietHours,
81
93
  telegramRedactPatterns,
94
+ telegramTransport,
95
+ telegramWebhookUrl,
96
+ telegramWebhookHost,
97
+ telegramWebhookPort,
98
+ telegramWebhookPath,
99
+ telegramWebhookSecret,
82
100
  workspace,
83
101
  workspaceAllowedRoots,
84
102
  workspaceWarnRoots,
103
+ stateBackend,
85
104
  maxFileSize,
86
105
  artifactRetentionDays,
87
106
  artifactMaxTurnDirs,
@@ -114,6 +133,8 @@ export function loadConfig() {
114
133
  voicePreferredBackend,
115
134
  voiceDefaultLanguage,
116
135
  voiceTranscribeOnly,
136
+ auditMaxEvents,
137
+ sessionLockTtlMs,
117
138
  };
118
139
  }
119
140
  /**
@@ -313,6 +334,32 @@ function parseLogFormat(raw) {
313
334
  console.warn(`Invalid CONNECTOR_LOG_FORMAT value: "${raw}". Expected text or json. Falling back to "text".`);
314
335
  return "text";
315
336
  }
337
+ function parseTelegramTransport(raw) {
338
+ if (!raw) {
339
+ return "polling";
340
+ }
341
+ if (raw === "polling" || raw === "webhook") {
342
+ return raw;
343
+ }
344
+ console.warn(`Invalid TELEGRAM_TRANSPORT value: "${raw}". Expected polling or webhook. Falling back to polling.`);
345
+ return "polling";
346
+ }
347
+ function parseWebhookPath(raw) {
348
+ if (!raw) {
349
+ return "/telegram/webhook";
350
+ }
351
+ return raw.startsWith("/") ? raw : `/${raw}`;
352
+ }
353
+ function parseStateBackend(raw) {
354
+ if (!raw) {
355
+ return "json";
356
+ }
357
+ if (raw === "json" || raw === "sqlite") {
358
+ return raw;
359
+ }
360
+ console.warn(`Invalid NORDRELAY_STATE_BACKEND value: "${raw}". Expected json or sqlite. Falling back to json.`);
361
+ return "json";
362
+ }
316
363
  function parseLaunchProfiles(raw, codexSandboxMode, codexApprovalPolicy, enableUnsafeLaunchProfiles) {
317
364
  const defaultProfile = createDefaultLaunchProfile(codexSandboxMode, codexApprovalPolicy);
318
365
  const profiles = createBuiltinLaunchProfiles(defaultProfile, {
@@ -21,3 +21,26 @@ export function parseContextKey(key) {
21
21
  export function isTopicContextKey(key) {
22
22
  return key.includes(":");
23
23
  }
24
+ export function isTelegramContextKey(key) {
25
+ const parts = key.split(":");
26
+ if (parts.length < 1 || parts.length > 2) {
27
+ return false;
28
+ }
29
+ const chatIdText = parts[0];
30
+ if (!chatIdText || !/^-?\d+$/.test(chatIdText)) {
31
+ return false;
32
+ }
33
+ const chatId = Number(chatIdText);
34
+ if (!Number.isSafeInteger(chatId) || chatId === 0) {
35
+ return false;
36
+ }
37
+ const threadIdText = parts[1];
38
+ if (threadIdText === undefined) {
39
+ return true;
40
+ }
41
+ if (!/^\d+$/.test(threadIdText)) {
42
+ return false;
43
+ }
44
+ const threadId = Number(threadIdText);
45
+ return Number.isSafeInteger(threadId) && threadId > 0;
46
+ }
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
+ import { createServer } from "node:http";
1
2
  import { mkdir, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
4
+ import { webhookCallback } from "grammy";
3
5
  import { createBot, registerCommands } from "./bot.js";
4
6
  import { checkAuthStatus } from "./codex-auth.js";
5
7
  import { describeCodexCli, resolveCodexCli } from "./codex-cli.js";
@@ -12,8 +14,11 @@ import { configureRedaction } from "./redaction.js";
12
14
  import { SessionRegistry } from "./session-registry.js";
13
15
  let registry;
14
16
  let bot;
17
+ let webhookServer;
18
+ let runtimeConfig;
15
19
  try {
16
20
  const config = loadConfig();
21
+ runtimeConfig = config;
17
22
  configureRedaction(config.telegramRedactPatterns);
18
23
  installConsoleLogger(config.logFormat);
19
24
  registry = new SessionRegistry(config);
@@ -46,6 +51,7 @@ try {
46
51
  }
47
52
  }
48
53
  console.log("Session mode: per Telegram context");
54
+ console.log(`Telegram transport: ${config.telegramTransport}`);
49
55
  await writeConnectorState({
50
56
  status: "ready",
51
57
  pid: Number(process.env.NORDRELAY_WRAPPER_PID) || process.pid,
@@ -56,6 +62,7 @@ try {
56
62
  authMethod: authStatus.method,
57
63
  codexCli: describeCodexCli(codexCli),
58
64
  piCli: describePiCli(piCli),
65
+ telegramTransport: config.telegramTransport,
59
66
  });
60
67
  }
61
68
  catch (error) {
@@ -77,8 +84,9 @@ const shutdown = (signal) => {
77
84
  }
78
85
  shuttingDown = true;
79
86
  console.log(`Received ${signal}, shutting down NordRelay...`);
80
- if (bot)
87
+ if (bot && runtimeConfig?.telegramTransport !== "webhook")
81
88
  bot.stop();
89
+ webhookServer?.close();
82
90
  setTimeout(() => {
83
91
  registry?.disposeAll();
84
92
  void writeConnectorState({
@@ -124,7 +132,44 @@ async function startPolling() {
124
132
  process.exit(1);
125
133
  }
126
134
  }
127
- await startPolling();
135
+ if (registry && bot) {
136
+ if (runtimeConfig?.telegramTransport === "webhook") {
137
+ webhookServer = await startWebhook(bot, runtimeConfig);
138
+ }
139
+ else {
140
+ await startPolling();
141
+ }
142
+ }
143
+ async function startWebhook(activeBot, config) {
144
+ const callback = webhookCallback(activeBot, "http", {
145
+ secretToken: config.telegramWebhookSecret,
146
+ });
147
+ const server = createServer((req, res) => {
148
+ if (req.method === "GET" && req.url === "/healthz") {
149
+ res.writeHead(200, { "content-type": "text/plain" });
150
+ res.end("ok\n");
151
+ return;
152
+ }
153
+ if (req.url?.split("?")[0] !== config.telegramWebhookPath) {
154
+ res.writeHead(404, { "content-type": "text/plain" });
155
+ res.end("not found\n");
156
+ return;
157
+ }
158
+ void callback(req, res);
159
+ });
160
+ await activeBot.api.setWebhook(joinWebhookUrl(config.telegramWebhookUrl, config.telegramWebhookPath), {
161
+ secret_token: config.telegramWebhookSecret,
162
+ drop_pending_updates: process.env.NORDRELAY_DROP_PENDING_UPDATES !== "0",
163
+ });
164
+ await new Promise((resolve) => {
165
+ server.listen(config.telegramWebhookPort, config.telegramWebhookHost, resolve);
166
+ });
167
+ console.log(`Webhook listening on ${config.telegramWebhookHost}:${config.telegramWebhookPort}${config.telegramWebhookPath}`);
168
+ return server;
169
+ }
170
+ function joinWebhookUrl(baseUrl, webhookPath) {
171
+ return `${baseUrl.replace(/\/+$/, "")}${webhookPath.startsWith("/") ? webhookPath : `/${webhookPath}`}`;
172
+ }
128
173
  async function writeConnectorState(payload) {
129
174
  const stateFile = process.env.NORDRELAY_STATE_FILE;
130
175
  if (!stateFile) {
package/dist/logger.js CHANGED
@@ -1,9 +1,20 @@
1
1
  import { redactUnknown } from "./redaction.js";
2
2
  export function installConsoleLogger(format) {
3
+ const targetLog = console.log.bind(console);
4
+ const targetWarn = console.warn.bind(console);
5
+ const targetError = console.error.bind(console);
3
6
  if (format !== "json") {
7
+ console.log = (...args) => {
8
+ targetLog(toTextRecord("info", args));
9
+ };
10
+ console.warn = (...args) => {
11
+ targetWarn(toTextRecord("warn", args));
12
+ };
13
+ console.error = (...args) => {
14
+ targetError(toTextRecord("error", args));
15
+ };
4
16
  return;
5
17
  }
6
- const targetLog = console.log.bind(console);
7
18
  console.log = (...args) => {
8
19
  targetLog(JSON.stringify(toLogRecord("info", args)));
9
20
  };
@@ -14,6 +25,9 @@ export function installConsoleLogger(format) {
14
25
  targetLog(JSON.stringify(toLogRecord("error", args)));
15
26
  };
16
27
  }
28
+ function toTextRecord(level, args) {
29
+ return `[${formatLocalTimestamp(new Date())}] ${level.toUpperCase()} ${args.map(formatArg).join(" ")}`;
30
+ }
17
31
  function toLogRecord(level, args) {
18
32
  return {
19
33
  ts: new Date().toISOString(),
@@ -25,3 +39,12 @@ function toLogRecord(level, args) {
25
39
  function formatArg(value) {
26
40
  return redactUnknown(value);
27
41
  }
42
+ function formatLocalTimestamp(date) {
43
+ return [
44
+ `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`,
45
+ `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`,
46
+ ].join(" ");
47
+ }
48
+ function pad(value) {
49
+ return String(value).padStart(2, "0");
50
+ }