diff --git a/extensions/nostr/doctor-contract-api.test.ts b/extensions/nostr/doctor-contract-api.test.ts index ba9e298408d0..88cbebb7caa8 100644 --- a/extensions/nostr/doctor-contract-api.test.ts +++ b/extensions/nostr/doctor-contract-api.test.ts @@ -102,4 +102,33 @@ describe("nostr doctor state migration", () => { lastPublishResults: { "wss://relay.example": "ok" }, }); }); + + it("preserves legacy account key bytes when importing state files", async () => { + const nostrDir = path.join(stateDir, "nostr"); + const busPath = path.join(nostrDir, "bus-state-Team.A.json"); + await fs.mkdir(nostrDir, { recursive: true }); + await fs.writeFile( + busPath, + JSON.stringify({ + version: 1, + lastProcessedAt: 1700, + gatewayStartedAt: 1600, + }), + ); + + const context = createDoctorContext(env); + await stateMigrations[0].migrateLegacyState({ + config: {}, + env, + stateDir, + oauthDir: path.join(stateDir, "oauth"), + context, + }); + + const store = context.openPluginStateKeyedStore({ namespace: "bus-state", maxEntries: 256 }); + await expect(store.lookup("Team.A")).resolves.toMatchObject({ + lastProcessedAt: 1700, + }); + await expect(store.lookup("team-a")).resolves.toBeUndefined(); + }); }); diff --git a/extensions/nostr/doctor-contract-api.ts b/extensions/nostr/doctor-contract-api.ts index 6189dd363436..dbb6e1aec55f 100644 --- a/extensions/nostr/doctor-contract-api.ts +++ b/extensions/nostr/doctor-contract-api.ts @@ -3,6 +3,7 @@ import type { Dirent } from "node:fs"; import fs from "node:fs/promises"; import path from "node:path"; import type { PluginDoctorStateMigration } from "openclaw/plugin-sdk/runtime-doctor"; +import { normalizeNostrStateAccountId } from "./src/state-account-id.js"; type NostrBusState = { version: 2; @@ -22,14 +23,6 @@ const BUS_STATE_NAMESPACE = "bus-state"; const PROFILE_STATE_NAMESPACE = "profile-state"; const MAX_NOSTR_STATE_ENTRIES = 256; -function normalizeAccountId(accountId?: string): string { - const trimmed = accountId?.trim(); - if (!trimmed) { - return "default"; - } - return trimmed.replace(/[^a-z0-9._-]+/gi, "_"); -} - function finiteNumberOrNull(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } @@ -114,7 +107,7 @@ async function listLegacyFiles(params: { continue; } const rawAccountId = entry.name.slice(params.prefix.length, -suffix.length); - const accountId = normalizeAccountId(rawAccountId); + const accountId = normalizeNostrStateAccountId(rawAccountId); const filePath = path.join(dir, entry.name); try { const value = params.parse(await readJsonFile(filePath)); diff --git a/extensions/nostr/src/nostr-state-store.test.ts b/extensions/nostr/src/nostr-state-store.test.ts index 19b0922bfe4d..0c2492760a61 100644 --- a/extensions/nostr/src/nostr-state-store.test.ts +++ b/extensions/nostr/src/nostr-state-store.test.ts @@ -97,6 +97,21 @@ describe("nostr bus state store", () => { expect(stateB?.lastProcessedAt).toBe(2000); }); }); + + it("preserves legacy account key bytes for state lookup", async () => { + await withTempStateDir(async () => { + await writeNostrBusState({ + accountId: " Team.A ", + lastProcessedAt: 1234, + gatewayStartedAt: 1200, + }); + + await expect(readNostrBusState({ accountId: "Team.A" })).resolves.toMatchObject({ + lastProcessedAt: 1234, + }); + await expect(readNostrBusState({ accountId: "team-a" })).resolves.toBeNull(); + }); + }); }); describe("nostr profile state store", () => { diff --git a/extensions/nostr/src/nostr-state-store.ts b/extensions/nostr/src/nostr-state-store.ts index 49447cc7afc9..e090a7780937 100644 --- a/extensions/nostr/src/nostr-state-store.ts +++ b/extensions/nostr/src/nostr-state-store.ts @@ -1,5 +1,6 @@ // Nostr plugin module implements nostr state store behavior. import { getNostrRuntime } from "./runtime.js"; +import { normalizeNostrStateAccountId } from "./state-account-id.js"; const STORE_VERSION = 2; const PROFILE_STATE_VERSION = 1; @@ -25,14 +26,6 @@ type NostrProfileState = { lastPublishResults: Record | null; }; -function normalizeAccountId(accountId?: string): string { - const trimmed = accountId?.trim(); - if (!trimmed) { - return "default"; - } - return trimmed.replace(/[^a-z0-9._-]+/gi, "_"); -} - function openNostrBusStateStore(env?: NodeJS.ProcessEnv) { return getNostrRuntime().state.openKeyedStore({ namespace: "bus-state", @@ -54,7 +47,9 @@ export async function readNostrBusState(params: { env?: NodeJS.ProcessEnv; }): Promise { return ( - (await openNostrBusStateStore(params.env).lookup(normalizeAccountId(params.accountId))) ?? null + (await openNostrBusStateStore(params.env).lookup( + normalizeNostrStateAccountId(params.accountId), + )) ?? null ); } @@ -71,7 +66,10 @@ export async function writeNostrBusState(params: { gatewayStartedAt: params.gatewayStartedAt, recentEventIds: (params.recentEventIds ?? []).filter((x): x is string => typeof x === "string"), }; - await openNostrBusStateStore(params.env).register(normalizeAccountId(params.accountId), payload); + await openNostrBusStateStore(params.env).register( + normalizeNostrStateAccountId(params.accountId), + payload, + ); } /** @@ -107,8 +105,9 @@ export async function readNostrProfileState(params: { env?: NodeJS.ProcessEnv; }): Promise { return ( - (await openNostrProfileStateStore(params.env).lookup(normalizeAccountId(params.accountId))) ?? - null + (await openNostrProfileStateStore(params.env).lookup( + normalizeNostrStateAccountId(params.accountId), + )) ?? null ); } @@ -126,7 +125,7 @@ export async function writeNostrProfileState(params: { lastPublishResults: params.lastPublishResults, }; await openNostrProfileStateStore(params.env).register( - normalizeAccountId(params.accountId), + normalizeNostrStateAccountId(params.accountId), payload, ); } diff --git a/extensions/nostr/src/state-account-id.ts b/extensions/nostr/src/state-account-id.ts new file mode 100644 index 000000000000..77fd9f06ce5a --- /dev/null +++ b/extensions/nostr/src/state-account-id.ts @@ -0,0 +1,8 @@ +// Nostr state stores keep legacy account key bytes; do not use the newer SDK normalizer here. +export function normalizeNostrStateAccountId(accountId?: string): string { + const trimmed = accountId?.trim(); + if (!trimmed) { + return "default"; + } + return trimmed.replace(/[^a-z0-9._-]+/gi, "_"); +}