chore: Lint extensions folder.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { OpenClawConfig, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
||||
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
||||
|
||||
import { nostrPlugin } from "./src/channel.js";
|
||||
@@ -20,13 +20,13 @@ const plugin = {
|
||||
const httpHandler = createNostrProfileHttpHandler({
|
||||
getConfigProfile: (accountId: string) => {
|
||||
const runtime = getNostrRuntime();
|
||||
const cfg = runtime.config.loadConfig() as OpenClawConfig;
|
||||
const cfg = runtime.config.loadConfig();
|
||||
const account = resolveNostrAccount({ cfg, accountId });
|
||||
return account.profile;
|
||||
},
|
||||
updateConfigProfile: async (accountId: string, profile: NostrProfile) => {
|
||||
const runtime = getNostrRuntime();
|
||||
const cfg = runtime.config.loadConfig() as OpenClawConfig;
|
||||
const cfg = runtime.config.loadConfig();
|
||||
|
||||
// Build the config patch for channels.nostr.profile
|
||||
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
||||
@@ -49,7 +49,7 @@ const plugin = {
|
||||
},
|
||||
getAccountInfo: (accountId: string) => {
|
||||
const runtime = getNostrRuntime();
|
||||
const cfg = runtime.config.loadConfig() as OpenClawConfig;
|
||||
const cfg = runtime.config.loadConfig();
|
||||
const account = resolveNostrAccount({ cfg, accountId });
|
||||
if (!account.configured || !account.publicKey) {
|
||||
return null;
|
||||
|
||||
@@ -61,14 +61,18 @@ describe("nostrPlugin", () => {
|
||||
|
||||
it("recognizes npub as valid target", () => {
|
||||
const looksLikeId = nostrPlugin.messaging?.targetResolver?.looksLikeId;
|
||||
if (!looksLikeId) return;
|
||||
if (!looksLikeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(looksLikeId("npub1xyz123")).toBe(true);
|
||||
});
|
||||
|
||||
it("recognizes hex pubkey as valid target", () => {
|
||||
const looksLikeId = nostrPlugin.messaging?.targetResolver?.looksLikeId;
|
||||
if (!looksLikeId) return;
|
||||
if (!looksLikeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hexPubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
expect(looksLikeId(hexPubkey)).toBe(true);
|
||||
@@ -76,7 +80,9 @@ describe("nostrPlugin", () => {
|
||||
|
||||
it("rejects invalid input", () => {
|
||||
const looksLikeId = nostrPlugin.messaging?.targetResolver?.looksLikeId;
|
||||
if (!looksLikeId) return;
|
||||
if (!looksLikeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(looksLikeId("not-a-pubkey")).toBe(false);
|
||||
expect(looksLikeId("")).toBe(false);
|
||||
@@ -84,7 +90,9 @@ describe("nostrPlugin", () => {
|
||||
|
||||
it("normalizeTarget strips nostr: prefix", () => {
|
||||
const normalize = nostrPlugin.messaging?.normalizeTarget;
|
||||
if (!normalize) return;
|
||||
if (!normalize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hexPubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
expect(normalize(`nostr:${hexPubkey}`)).toBe(hexPubkey);
|
||||
@@ -108,7 +116,9 @@ describe("nostrPlugin", () => {
|
||||
|
||||
it("normalizes nostr: prefix in allow entries", () => {
|
||||
const normalize = nostrPlugin.pairing?.normalizeAllowEntry;
|
||||
if (!normalize) return;
|
||||
if (!normalize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hexPubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
expect(normalize(`nostr:${hexPubkey}`)).toBe(hexPubkey);
|
||||
|
||||
@@ -63,7 +63,9 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
||||
.map((entry) => String(entry).trim())
|
||||
.filter(Boolean)
|
||||
.map((entry) => {
|
||||
if (entry === "*") return "*";
|
||||
if (entry === "*") {
|
||||
return "*";
|
||||
}
|
||||
try {
|
||||
return normalizePubkey(entry);
|
||||
} catch {
|
||||
@@ -162,7 +164,9 @@ export const nostrPlugin: ChannelPlugin<ResolvedNostrAccount> = {
|
||||
collectStatusIssues: (accounts) =>
|
||||
accounts.flatMap((account) => {
|
||||
const lastError = typeof account.lastError === "string" ? account.lastError.trim() : "";
|
||||
if (!lastError) return [];
|
||||
if (!lastError) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
channel: "nostr",
|
||||
|
||||
@@ -300,34 +300,54 @@ export function createMetrics(onMetric?: OnMetricCallback): NostrMetrics {
|
||||
|
||||
// Relay metrics
|
||||
case "relay.connect":
|
||||
if (relayUrl) getOrCreateRelay(relayUrl).connects += value;
|
||||
if (relayUrl) {
|
||||
getOrCreateRelay(relayUrl).connects += value;
|
||||
}
|
||||
break;
|
||||
case "relay.disconnect":
|
||||
if (relayUrl) getOrCreateRelay(relayUrl).disconnects += value;
|
||||
if (relayUrl) {
|
||||
getOrCreateRelay(relayUrl).disconnects += value;
|
||||
}
|
||||
break;
|
||||
case "relay.reconnect":
|
||||
if (relayUrl) getOrCreateRelay(relayUrl).reconnects += value;
|
||||
if (relayUrl) {
|
||||
getOrCreateRelay(relayUrl).reconnects += value;
|
||||
}
|
||||
break;
|
||||
case "relay.error":
|
||||
if (relayUrl) getOrCreateRelay(relayUrl).errors += value;
|
||||
if (relayUrl) {
|
||||
getOrCreateRelay(relayUrl).errors += value;
|
||||
}
|
||||
break;
|
||||
case "relay.message.event":
|
||||
if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.event += value;
|
||||
if (relayUrl) {
|
||||
getOrCreateRelay(relayUrl).messagesReceived.event += value;
|
||||
}
|
||||
break;
|
||||
case "relay.message.eose":
|
||||
if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.eose += value;
|
||||
if (relayUrl) {
|
||||
getOrCreateRelay(relayUrl).messagesReceived.eose += value;
|
||||
}
|
||||
break;
|
||||
case "relay.message.closed":
|
||||
if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.closed += value;
|
||||
if (relayUrl) {
|
||||
getOrCreateRelay(relayUrl).messagesReceived.closed += value;
|
||||
}
|
||||
break;
|
||||
case "relay.message.notice":
|
||||
if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.notice += value;
|
||||
if (relayUrl) {
|
||||
getOrCreateRelay(relayUrl).messagesReceived.notice += value;
|
||||
}
|
||||
break;
|
||||
case "relay.message.ok":
|
||||
if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.ok += value;
|
||||
if (relayUrl) {
|
||||
getOrCreateRelay(relayUrl).messagesReceived.ok += value;
|
||||
}
|
||||
break;
|
||||
case "relay.message.auth":
|
||||
if (relayUrl) getOrCreateRelay(relayUrl).messagesReceived.auth += value;
|
||||
if (relayUrl) {
|
||||
getOrCreateRelay(relayUrl).messagesReceived.auth += value;
|
||||
}
|
||||
break;
|
||||
case "relay.circuit_breaker.open":
|
||||
if (relayUrl) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createSeenTracker } from "./seen-tracker.js";
|
||||
import { createMetrics, createNoopMetrics, type MetricEvent } from "./metrics.js";
|
||||
|
||||
|
||||
@@ -36,11 +36,6 @@ const STARTUP_LOOKBACK_SEC = 120; // tolerate relay lag / clock skew
|
||||
const MAX_PERSISTED_EVENT_IDS = 5000;
|
||||
const STATE_PERSIST_DEBOUNCE_MS = 5000; // Debounce state writes
|
||||
|
||||
// Reconnect configuration (exponential backoff with jitter)
|
||||
const RECONNECT_BASE_MS = 1000; // 1 second base
|
||||
const RECONNECT_MAX_MS = 60000; // 60 seconds max
|
||||
const RECONNECT_JITTER = 0.3; // ±30% jitter
|
||||
|
||||
// Circuit breaker configuration
|
||||
const CIRCUIT_BREAKER_THRESHOLD = 5; // failures before opening
|
||||
const CIRCUIT_BREAKER_RESET_MS = 30000; // 30 seconds before half-open
|
||||
@@ -137,7 +132,9 @@ function createCircuitBreaker(
|
||||
|
||||
return {
|
||||
canAttempt(): boolean {
|
||||
if (state.state === "closed") return true;
|
||||
if (state.state === "closed") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (state.state === "open") {
|
||||
// Check if enough time has passed to try half-open
|
||||
@@ -243,10 +240,14 @@ function createRelayHealthTracker(): RelayHealthTracker {
|
||||
|
||||
getScore(relay: string): number {
|
||||
const s = stats.get(relay);
|
||||
if (!s) return 0.5; // Unknown relay gets neutral score
|
||||
if (!s) {
|
||||
return 0.5;
|
||||
} // Unknown relay gets neutral score
|
||||
|
||||
const total = s.successCount + s.failureCount;
|
||||
if (total === 0) return 0.5;
|
||||
if (total === 0) {
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
// Success rate (0-1)
|
||||
const successRate = s.successCount / total;
|
||||
@@ -266,25 +267,11 @@ function createRelayHealthTracker(): RelayHealthTracker {
|
||||
},
|
||||
|
||||
getSortedRelays(relays: string[]): string[] {
|
||||
return [...relays].sort((a, b) => this.getScore(b) - this.getScore(a));
|
||||
return [...relays].toSorted((a, b) => this.getScore(b) - this.getScore(a));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Reconnect with Exponential Backoff + Jitter
|
||||
// ============================================================================
|
||||
|
||||
function computeReconnectDelay(attempt: number): number {
|
||||
// Exponential backoff: base * 2^attempt
|
||||
const exponential = RECONNECT_BASE_MS * Math.pow(2, attempt);
|
||||
const capped = Math.min(exponential, RECONNECT_MAX_MS);
|
||||
|
||||
// Add jitter: ±JITTER%
|
||||
const jitter = capped * RECONNECT_JITTER * (Math.random() * 2 - 1);
|
||||
return Math.max(RECONNECT_BASE_MS, capped + jitter);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Key Validation
|
||||
// ============================================================================
|
||||
@@ -397,7 +384,9 @@ export async function startNostrBus(options: NostrBusOptions): Promise<NostrBusH
|
||||
recentEventIds = recentEventIds.slice(-MAX_PERSISTED_EVENT_IDS);
|
||||
}
|
||||
|
||||
if (pendingWrite) clearTimeout(pendingWrite);
|
||||
if (pendingWrite) {
|
||||
clearTimeout(pendingWrite);
|
||||
}
|
||||
pendingWrite = setTimeout(() => {
|
||||
writeNostrBusState({
|
||||
accountId,
|
||||
@@ -461,7 +450,7 @@ export async function startNostrBus(options: NostrBusOptions): Promise<NostrBusH
|
||||
// Decrypt the message
|
||||
let plaintext: string;
|
||||
try {
|
||||
plaintext = await decrypt(sk, event.pubkey, event.content);
|
||||
plaintext = decrypt(sk, event.pubkey, event.content);
|
||||
metrics.emit("decrypt.success");
|
||||
} catch (err) {
|
||||
metrics.emit("decrypt.failure");
|
||||
@@ -515,7 +504,7 @@ export async function startNostrBus(options: NostrBusOptions): Promise<NostrBusH
|
||||
metrics.emit("relay.message.closed", 1, { relay });
|
||||
options.onDisconnect?.(relay);
|
||||
}
|
||||
onError?.(new Error(`Subscription closed: ${reason}`), "subscription");
|
||||
onError?.(new Error(`Subscription closed: ${reason.join(", ")}`), "subscription");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -614,7 +603,7 @@ async function sendEncryptedDm(
|
||||
healthTracker: RelayHealthTracker,
|
||||
onError?: (error: Error, context: string) => void,
|
||||
): Promise<void> {
|
||||
const ciphertext = await encrypt(sk, toPubkey, text);
|
||||
const ciphertext = encrypt(sk, toPubkey, text);
|
||||
const reply = finalizeEvent(
|
||||
{
|
||||
kind: 4,
|
||||
@@ -640,6 +629,7 @@ async function sendEncryptedDm(
|
||||
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
// oxlint-disable-next-line typescript/await-thenable typesciript/no-floating-promises
|
||||
await pool.publish([relay], reply);
|
||||
const latency = Date.now() - startTime;
|
||||
|
||||
@@ -672,7 +662,9 @@ async function sendEncryptedDm(
|
||||
* Check if a string looks like a valid Nostr pubkey (hex or npub)
|
||||
*/
|
||||
export function isValidPubkey(input: string): boolean {
|
||||
if (typeof input !== "string") return false;
|
||||
if (typeof input !== "string") {
|
||||
return false;
|
||||
}
|
||||
const trimmed = input.trim();
|
||||
|
||||
// npub format
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Tests for Nostr Profile HTTP Handler
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { Socket } from "node:net";
|
||||
|
||||
@@ -56,7 +56,6 @@ function createMockResponse(): ServerResponse & {
|
||||
_getData: () => string;
|
||||
_getStatusCode: () => number;
|
||||
} {
|
||||
const socket = new Socket();
|
||||
const res = new ServerResponse({} as IncomingMessage);
|
||||
|
||||
let data = "";
|
||||
@@ -68,7 +67,10 @@ function createMockResponse(): ServerResponse & {
|
||||
};
|
||||
|
||||
res.end = function (chunk?: unknown) {
|
||||
if (chunk) data += String(chunk);
|
||||
if (chunk) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
data += String(chunk);
|
||||
}
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
@@ -113,33 +113,53 @@ function isPrivateIp(ip: string): boolean {
|
||||
// Handle IPv4
|
||||
const ipv4Match = ip.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
|
||||
if (ipv4Match) {
|
||||
const [, a, b, c] = ipv4Match.map(Number);
|
||||
const [, a, b] = ipv4Match.map(Number);
|
||||
// 127.0.0.0/8 (loopback)
|
||||
if (a === 127) return true;
|
||||
if (a === 127) {
|
||||
return true;
|
||||
}
|
||||
// 10.0.0.0/8 (private)
|
||||
if (a === 10) return true;
|
||||
if (a === 10) {
|
||||
return true;
|
||||
}
|
||||
// 172.16.0.0/12 (private)
|
||||
if (a === 172 && b >= 16 && b <= 31) return true;
|
||||
if (a === 172 && b >= 16 && b <= 31) {
|
||||
return true;
|
||||
}
|
||||
// 192.168.0.0/16 (private)
|
||||
if (a === 192 && b === 168) return true;
|
||||
if (a === 192 && b === 168) {
|
||||
return true;
|
||||
}
|
||||
// 169.254.0.0/16 (link-local)
|
||||
if (a === 169 && b === 254) return true;
|
||||
if (a === 169 && b === 254) {
|
||||
return true;
|
||||
}
|
||||
// 0.0.0.0/8
|
||||
if (a === 0) return true;
|
||||
if (a === 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle IPv6
|
||||
const ipLower = ip.toLowerCase().replace(/^\[|\]$/g, "");
|
||||
// ::1 (loopback)
|
||||
if (ipLower === "::1") return true;
|
||||
if (ipLower === "::1") {
|
||||
return true;
|
||||
}
|
||||
// fe80::/10 (link-local)
|
||||
if (ipLower.startsWith("fe80:")) return true;
|
||||
if (ipLower.startsWith("fe80:")) {
|
||||
return true;
|
||||
}
|
||||
// fc00::/7 (unique local)
|
||||
if (ipLower.startsWith("fc") || ipLower.startsWith("fd")) return true;
|
||||
if (ipLower.startsWith("fc") || ipLower.startsWith("fd")) {
|
||||
return true;
|
||||
}
|
||||
// ::ffff:x.x.x.x (IPv4-mapped IPv6) - extract and check IPv4
|
||||
const v4Mapped = ipLower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
||||
if (v4Mapped) return isPrivateIp(v4Mapped[1]);
|
||||
if (v4Mapped) {
|
||||
return isPrivateIp(v4Mapped[1]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* Tests for Nostr Profile Import
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { mergeProfiles, type ProfileImportOptions } from "./nostr-profile-import.js";
|
||||
import { mergeProfiles } from "./nostr-profile-import.js";
|
||||
import type { NostrProfile } from "./config-schema.js";
|
||||
|
||||
// Note: importProfileFromRelays requires real network calls or complex mocking
|
||||
|
||||
@@ -243,8 +243,12 @@ export function mergeProfiles(
|
||||
local: NostrProfile | undefined,
|
||||
imported: NostrProfile | undefined,
|
||||
): NostrProfile {
|
||||
if (!imported) return local ?? {};
|
||||
if (!local) return imported;
|
||||
if (!imported) {
|
||||
return local ?? {};
|
||||
}
|
||||
if (!local) {
|
||||
return imported;
|
||||
}
|
||||
|
||||
return {
|
||||
name: local.name ?? imported.name,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getPublicKey } from "nostr-tools";
|
||||
import {
|
||||
createProfileEvent,
|
||||
profileToContent,
|
||||
@@ -422,7 +421,7 @@ describe("profile type confusion", () => {
|
||||
|
||||
it("handles prototype pollution attempt", () => {
|
||||
const malicious = JSON.parse('{"__proto__": {"polluted": true}}') as unknown;
|
||||
const result = validateProfile(malicious);
|
||||
validateProfile(malicious);
|
||||
// Should not pollute Object.prototype
|
||||
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -49,14 +49,30 @@ export function profileToContent(profile: NostrProfile): ProfileContent {
|
||||
|
||||
const content: ProfileContent = {};
|
||||
|
||||
if (validated.name !== undefined) content.name = validated.name;
|
||||
if (validated.displayName !== undefined) content.display_name = validated.displayName;
|
||||
if (validated.about !== undefined) content.about = validated.about;
|
||||
if (validated.picture !== undefined) content.picture = validated.picture;
|
||||
if (validated.banner !== undefined) content.banner = validated.banner;
|
||||
if (validated.website !== undefined) content.website = validated.website;
|
||||
if (validated.nip05 !== undefined) content.nip05 = validated.nip05;
|
||||
if (validated.lud16 !== undefined) content.lud16 = validated.lud16;
|
||||
if (validated.name !== undefined) {
|
||||
content.name = validated.name;
|
||||
}
|
||||
if (validated.displayName !== undefined) {
|
||||
content.display_name = validated.displayName;
|
||||
}
|
||||
if (validated.about !== undefined) {
|
||||
content.about = validated.about;
|
||||
}
|
||||
if (validated.picture !== undefined) {
|
||||
content.picture = validated.picture;
|
||||
}
|
||||
if (validated.banner !== undefined) {
|
||||
content.banner = validated.banner;
|
||||
}
|
||||
if (validated.website !== undefined) {
|
||||
content.website = validated.website;
|
||||
}
|
||||
if (validated.nip05 !== undefined) {
|
||||
content.nip05 = validated.nip05;
|
||||
}
|
||||
if (validated.lud16 !== undefined) {
|
||||
content.lud16 = validated.lud16;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
@@ -68,14 +84,30 @@ export function profileToContent(profile: NostrProfile): ProfileContent {
|
||||
export function contentToProfile(content: ProfileContent): NostrProfile {
|
||||
const profile: NostrProfile = {};
|
||||
|
||||
if (content.name !== undefined) profile.name = content.name;
|
||||
if (content.display_name !== undefined) profile.displayName = content.display_name;
|
||||
if (content.about !== undefined) profile.about = content.about;
|
||||
if (content.picture !== undefined) profile.picture = content.picture;
|
||||
if (content.banner !== undefined) profile.banner = content.banner;
|
||||
if (content.website !== undefined) profile.website = content.website;
|
||||
if (content.nip05 !== undefined) profile.nip05 = content.nip05;
|
||||
if (content.lud16 !== undefined) profile.lud16 = content.lud16;
|
||||
if (content.name !== undefined) {
|
||||
profile.name = content.name;
|
||||
}
|
||||
if (content.display_name !== undefined) {
|
||||
profile.displayName = content.display_name;
|
||||
}
|
||||
if (content.about !== undefined) {
|
||||
profile.about = content.about;
|
||||
}
|
||||
if (content.picture !== undefined) {
|
||||
profile.picture = content.picture;
|
||||
}
|
||||
if (content.banner !== undefined) {
|
||||
profile.banner = content.banner;
|
||||
}
|
||||
if (content.website !== undefined) {
|
||||
profile.website = content.website;
|
||||
}
|
||||
if (content.nip05 !== undefined) {
|
||||
profile.nip05 = content.nip05;
|
||||
}
|
||||
if (content.lud16 !== undefined) {
|
||||
profile.lud16 = content.lud16;
|
||||
}
|
||||
|
||||
return profile;
|
||||
}
|
||||
@@ -150,6 +182,7 @@ export async function publishProfileEvent(
|
||||
setTimeout(() => reject(new Error("timeout")), RELAY_PUBLISH_TIMEOUT_MS);
|
||||
});
|
||||
|
||||
// oxlint-disable-next-line typescript/no-floating-promises
|
||||
await Promise.race([pool.publish([relay], event), timeoutPromise]);
|
||||
|
||||
successes.push(relay);
|
||||
@@ -220,7 +253,9 @@ export function validateProfile(profile: unknown): {
|
||||
*/
|
||||
export function sanitizeProfileForDisplay(profile: NostrProfile): NostrProfile {
|
||||
const escapeHtml = (str: string | undefined): string | undefined => {
|
||||
if (str === undefined) return undefined;
|
||||
if (str === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
|
||||
@@ -20,7 +20,9 @@ async function withTempStateDir<T>(fn: (dir: string) => Promise<T>) {
|
||||
state: {
|
||||
resolveStateDir: (env, homedir) => {
|
||||
const override = env.OPENCLAW_STATE_DIR?.trim() || env.OPENCLAW_STATE_DIR?.trim();
|
||||
if (override) return override;
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
return path.join(homedir(), ".openclaw");
|
||||
},
|
||||
},
|
||||
@@ -28,8 +30,11 @@ async function withTempStateDir<T>(fn: (dir: string) => Promise<T>) {
|
||||
try {
|
||||
return await fn(dir);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.OPENCLAW_STATE_DIR;
|
||||
else process.env.OPENCLAW_STATE_DIR = previous;
|
||||
if (previous === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = previous;
|
||||
}
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,9 @@ export type NostrProfileState = {
|
||||
|
||||
function normalizeAccountId(accountId?: string): string {
|
||||
const trimmed = accountId?.trim();
|
||||
if (!trimmed) return "default";
|
||||
if (!trimmed) {
|
||||
return "default";
|
||||
}
|
||||
return trimmed.replace(/[^a-z0-9._-]+/gi, "_");
|
||||
}
|
||||
|
||||
@@ -101,7 +103,9 @@ export async function readNostrBusState(params: {
|
||||
return safeParseState(raw);
|
||||
} catch (err) {
|
||||
const code = (err as { code?: string }).code;
|
||||
if (code === "ENOENT") return null;
|
||||
if (code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -139,14 +143,18 @@ export function computeSinceTimestamp(
|
||||
state: NostrBusState | null,
|
||||
nowSec: number = Math.floor(Date.now() / 1000),
|
||||
): number {
|
||||
if (!state) return nowSec;
|
||||
if (!state) {
|
||||
return nowSec;
|
||||
}
|
||||
|
||||
// Use the most recent timestamp we have
|
||||
const candidates = [state.lastProcessedAt, state.gatewayStartedAt].filter(
|
||||
(t): t is number => t !== null && t > 0,
|
||||
);
|
||||
|
||||
if (candidates.length === 0) return nowSec;
|
||||
if (candidates.length === 0) {
|
||||
return nowSec;
|
||||
}
|
||||
return Math.max(...candidates);
|
||||
}
|
||||
|
||||
@@ -166,7 +174,7 @@ function safeParseProfileState(raw: string): NostrProfileState | null {
|
||||
typeof parsed.lastPublishedEventId === "string" ? parsed.lastPublishedEventId : null,
|
||||
lastPublishResults:
|
||||
parsed.lastPublishResults && typeof parsed.lastPublishResults === "object"
|
||||
? (parsed.lastPublishResults as Record<string, "ok" | "failed" | "timeout">)
|
||||
? parsed.lastPublishResults
|
||||
: null,
|
||||
};
|
||||
}
|
||||
@@ -187,7 +195,9 @@ export async function readNostrProfileState(params: {
|
||||
return safeParseProfileState(raw);
|
||||
} catch (err) {
|
||||
const code = (err as { code?: string }).code;
|
||||
if (code === "ENOENT") return null;
|
||||
if (code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,19 +56,27 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker {
|
||||
// Move an entry to the front (most recently used)
|
||||
function moveToFront(id: string): void {
|
||||
const entry = entries.get(id);
|
||||
if (!entry) return;
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Already at front
|
||||
if (head === id) return;
|
||||
if (head === id) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove from current position
|
||||
if (entry.prev) {
|
||||
const prevEntry = entries.get(entry.prev);
|
||||
if (prevEntry) prevEntry.next = entry.next;
|
||||
if (prevEntry) {
|
||||
prevEntry.next = entry.next;
|
||||
}
|
||||
}
|
||||
if (entry.next) {
|
||||
const nextEntry = entries.get(entry.next);
|
||||
if (nextEntry) nextEntry.prev = entry.prev;
|
||||
if (nextEntry) {
|
||||
nextEntry.prev = entry.prev;
|
||||
}
|
||||
}
|
||||
|
||||
// Update tail if this was the tail
|
||||
@@ -81,29 +89,39 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker {
|
||||
entry.next = head;
|
||||
if (head) {
|
||||
const headEntry = entries.get(head);
|
||||
if (headEntry) headEntry.prev = id;
|
||||
if (headEntry) {
|
||||
headEntry.prev = id;
|
||||
}
|
||||
}
|
||||
head = id;
|
||||
|
||||
// If no tail, this is also the tail
|
||||
if (!tail) tail = id;
|
||||
if (!tail) {
|
||||
tail = id;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove an entry from the linked list
|
||||
function removeFromList(id: string): void {
|
||||
const entry = entries.get(id);
|
||||
if (!entry) return;
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.prev) {
|
||||
const prevEntry = entries.get(entry.prev);
|
||||
if (prevEntry) prevEntry.next = entry.next;
|
||||
if (prevEntry) {
|
||||
prevEntry.next = entry.next;
|
||||
}
|
||||
} else {
|
||||
head = entry.next;
|
||||
}
|
||||
|
||||
if (entry.next) {
|
||||
const nextEntry = entries.get(entry.next);
|
||||
if (nextEntry) nextEntry.prev = entry.prev;
|
||||
if (nextEntry) {
|
||||
nextEntry.prev = entry.prev;
|
||||
}
|
||||
} else {
|
||||
tail = entry.prev;
|
||||
}
|
||||
@@ -111,7 +129,9 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker {
|
||||
|
||||
// Evict the least recently used entry
|
||||
function evictLRU(): void {
|
||||
if (!tail) return;
|
||||
if (!tail) {
|
||||
return;
|
||||
}
|
||||
const idToEvict = tail;
|
||||
removeFromList(idToEvict);
|
||||
entries.delete(idToEvict);
|
||||
@@ -139,7 +159,9 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker {
|
||||
if (pruneIntervalMs > 0) {
|
||||
pruneTimer = setInterval(pruneExpired, pruneIntervalMs);
|
||||
// Don't keep process alive just for pruning
|
||||
if (pruneTimer.unref) pruneTimer.unref();
|
||||
if (pruneTimer.unref) {
|
||||
pruneTimer.unref();
|
||||
}
|
||||
}
|
||||
|
||||
function add(id: string): void {
|
||||
@@ -167,12 +189,16 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker {
|
||||
|
||||
if (head) {
|
||||
const headEntry = entries.get(head);
|
||||
if (headEntry) headEntry.prev = id;
|
||||
if (headEntry) {
|
||||
headEntry.prev = id;
|
||||
}
|
||||
}
|
||||
|
||||
entries.set(id, newEntry);
|
||||
head = id;
|
||||
if (!tail) tail = id;
|
||||
if (!tail) {
|
||||
tail = id;
|
||||
}
|
||||
}
|
||||
|
||||
function has(id: string): boolean {
|
||||
@@ -198,7 +224,9 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker {
|
||||
|
||||
function peek(id: string): boolean {
|
||||
const entry = entries.get(id);
|
||||
if (!entry) return false;
|
||||
if (!entry) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
if (Date.now() - entry.seenAt > ttlMs) {
|
||||
@@ -248,12 +276,16 @@ export function createSeenTracker(options?: SeenTrackerOptions): SeenTracker {
|
||||
|
||||
if (head) {
|
||||
const headEntry = entries.get(head);
|
||||
if (headEntry) headEntry.prev = id;
|
||||
if (headEntry) {
|
||||
headEntry.prev = id;
|
||||
}
|
||||
}
|
||||
|
||||
entries.set(id, newEntry);
|
||||
head = id;
|
||||
if (!tail) tail = id;
|
||||
if (!tail) {
|
||||
tail = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ export function listNostrAccountIds(cfg: OpenClawConfig): string[] {
|
||||
*/
|
||||
export function resolveDefaultNostrAccountId(cfg: OpenClawConfig): string {
|
||||
const ids = listNostrAccountIds(cfg);
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
|
||||
if (ids.includes(DEFAULT_ACCOUNT_ID)) {
|
||||
return DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
return ids[0] ?? DEFAULT_ACCOUNT_ID;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user