@kodelyth/nostr 2026.5.42 → 2026.6.2

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.
Files changed (47) hide show
  1. package/klaw.plugin.json +185 -2
  2. package/package.json +19 -6
  3. package/api.ts +0 -10
  4. package/channel-plugin-api.ts +0 -1
  5. package/index.ts +0 -95
  6. package/runtime-api.ts +0 -6
  7. package/setup-api.ts +0 -1
  8. package/setup-entry.ts +0 -9
  9. package/setup-plugin-api.ts +0 -3
  10. package/src/channel-api.ts +0 -11
  11. package/src/channel.inbound.test.ts +0 -187
  12. package/src/channel.outbound.test.ts +0 -163
  13. package/src/channel.setup.ts +0 -234
  14. package/src/channel.test.ts +0 -526
  15. package/src/channel.ts +0 -215
  16. package/src/config-schema.ts +0 -98
  17. package/src/default-relays.ts +0 -1
  18. package/src/gateway.ts +0 -321
  19. package/src/inbound-direct-dm-runtime.ts +0 -1
  20. package/src/metrics.ts +0 -458
  21. package/src/nostr-bus.fuzz.test.ts +0 -382
  22. package/src/nostr-bus.inbound.test.ts +0 -526
  23. package/src/nostr-bus.integration.test.ts +0 -477
  24. package/src/nostr-bus.test.ts +0 -231
  25. package/src/nostr-bus.ts +0 -789
  26. package/src/nostr-key-utils.ts +0 -94
  27. package/src/nostr-profile-core.ts +0 -134
  28. package/src/nostr-profile-http-runtime.ts +0 -6
  29. package/src/nostr-profile-http.test.ts +0 -632
  30. package/src/nostr-profile-http.ts +0 -583
  31. package/src/nostr-profile-import.test.ts +0 -119
  32. package/src/nostr-profile-import.ts +0 -262
  33. package/src/nostr-profile-url-safety.ts +0 -21
  34. package/src/nostr-profile.fuzz.test.ts +0 -430
  35. package/src/nostr-profile.test.ts +0 -415
  36. package/src/nostr-profile.ts +0 -144
  37. package/src/nostr-state-store.test.ts +0 -237
  38. package/src/nostr-state-store.ts +0 -206
  39. package/src/runtime.ts +0 -9
  40. package/src/seen-tracker.ts +0 -289
  41. package/src/session-route.ts +0 -25
  42. package/src/setup-surface.ts +0 -264
  43. package/src/test-fixtures.ts +0 -45
  44. package/src/types.ts +0 -117
  45. package/test/setup.ts +0 -5
  46. package/test-api.ts +0 -1
  47. package/tsconfig.json +0 -16
@@ -1,237 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { describe, expect, it } from "vitest";
5
- import type { PluginRuntime } from "../runtime-api.js";
6
- import {
7
- readNostrBusState,
8
- readNostrProfileState,
9
- writeNostrBusState,
10
- writeNostrProfileState,
11
- computeSinceTimestamp,
12
- } from "./nostr-state-store.js";
13
- import { setNostrRuntime } from "./runtime.js";
14
-
15
- async function withTempStateDir<T>(fn: (dir: string) => Promise<T>) {
16
- const previous = process.env.KLAW_STATE_DIR;
17
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), "klaw-nostr-"));
18
- process.env.KLAW_STATE_DIR = dir;
19
- setNostrRuntime({
20
- state: {
21
- resolveStateDir: (env, homedir) => {
22
- const stateEnv = env ?? process.env;
23
- const override = stateEnv.KLAW_STATE_DIR?.trim();
24
- if (override) {
25
- return override;
26
- }
27
- const resolveHome = homedir ?? os.homedir;
28
- return path.join(resolveHome(), ".klaw");
29
- },
30
- },
31
- } as PluginRuntime);
32
- try {
33
- return await fn(dir);
34
- } finally {
35
- if (previous === undefined) {
36
- delete process.env.KLAW_STATE_DIR;
37
- } else {
38
- process.env.KLAW_STATE_DIR = previous;
39
- }
40
- await fs.rm(dir, { recursive: true, force: true });
41
- }
42
- }
43
-
44
- describe("nostr bus state store", () => {
45
- it("persists and reloads state across restarts", async () => {
46
- await withTempStateDir(async () => {
47
- // Fresh start - no state
48
- expect(await readNostrBusState({ accountId: "test-bot" })).toBeNull();
49
-
50
- // Write state
51
- await writeNostrBusState({
52
- accountId: "test-bot",
53
- lastProcessedAt: 1700000000,
54
- gatewayStartedAt: 1700000100,
55
- });
56
-
57
- // Read it back
58
- const state = await readNostrBusState({ accountId: "test-bot" });
59
- expect(state).toEqual({
60
- version: 2,
61
- lastProcessedAt: 1700000000,
62
- gatewayStartedAt: 1700000100,
63
- recentEventIds: [],
64
- });
65
- });
66
- });
67
-
68
- it("isolates state by accountId", async () => {
69
- await withTempStateDir(async () => {
70
- await writeNostrBusState({
71
- accountId: "bot-a",
72
- lastProcessedAt: 1000,
73
- gatewayStartedAt: 1000,
74
- });
75
- await writeNostrBusState({
76
- accountId: "bot-b",
77
- lastProcessedAt: 2000,
78
- gatewayStartedAt: 2000,
79
- });
80
-
81
- const stateA = await readNostrBusState({ accountId: "bot-a" });
82
- const stateB = await readNostrBusState({ accountId: "bot-b" });
83
-
84
- expect(stateA?.lastProcessedAt).toBe(1000);
85
- expect(stateB?.lastProcessedAt).toBe(2000);
86
- });
87
- });
88
-
89
- it("upgrades v1 bus state files on read", async () => {
90
- await withTempStateDir(async (dir) => {
91
- const filePath = path.join(dir, "nostr", "bus-state-test-bot.json");
92
- await fs.mkdir(path.dirname(filePath), { recursive: true });
93
- await fs.writeFile(
94
- filePath,
95
- JSON.stringify({
96
- version: 1,
97
- lastProcessedAt: 1700000000,
98
- gatewayStartedAt: 1700000100,
99
- }),
100
- "utf-8",
101
- );
102
-
103
- const state = await readNostrBusState({ accountId: "test-bot" });
104
- expect(state).toEqual({
105
- version: 2,
106
- lastProcessedAt: 1700000000,
107
- gatewayStartedAt: 1700000100,
108
- recentEventIds: [],
109
- });
110
- });
111
- });
112
-
113
- it("drops malformed recent event ids while keeping the state", async () => {
114
- await withTempStateDir(async (dir) => {
115
- const filePath = path.join(dir, "nostr", "bus-state-test-bot.json");
116
- await fs.mkdir(path.dirname(filePath), { recursive: true });
117
- await fs.writeFile(
118
- filePath,
119
- JSON.stringify({
120
- version: 2,
121
- lastProcessedAt: 1700000000,
122
- gatewayStartedAt: 1700000100,
123
- recentEventIds: ["evt-1", 2, null],
124
- }),
125
- "utf-8",
126
- );
127
-
128
- const state = await readNostrBusState({ accountId: "test-bot" });
129
- expect(state).toEqual({
130
- version: 2,
131
- lastProcessedAt: 1700000000,
132
- gatewayStartedAt: 1700000100,
133
- recentEventIds: ["evt-1"],
134
- });
135
- });
136
- });
137
- });
138
-
139
- describe("nostr profile state store", () => {
140
- it("persists and reloads profile publish state", async () => {
141
- await withTempStateDir(async () => {
142
- await writeNostrProfileState({
143
- accountId: "test-bot",
144
- lastPublishedAt: 1700000000,
145
- lastPublishedEventId: "evt-1",
146
- lastPublishResults: {
147
- "wss://relay.example": "ok",
148
- },
149
- });
150
-
151
- const state = await readNostrProfileState({ accountId: "test-bot" });
152
- expect(state).toEqual({
153
- version: 1,
154
- lastPublishedAt: 1700000000,
155
- lastPublishedEventId: "evt-1",
156
- lastPublishResults: {
157
- "wss://relay.example": "ok",
158
- },
159
- });
160
- });
161
- });
162
-
163
- it("drops malformed relay results while keeping valid state fields", async () => {
164
- await withTempStateDir(async (dir) => {
165
- const filePath = path.join(dir, "nostr", "profile-state-test-bot.json");
166
- await fs.mkdir(path.dirname(filePath), { recursive: true });
167
- await fs.writeFile(
168
- filePath,
169
- JSON.stringify({
170
- version: 1,
171
- lastPublishedAt: 1700000000,
172
- lastPublishedEventId: "evt-1",
173
- lastPublishResults: {
174
- "wss://relay.example": "ok",
175
- "wss://relay.bad": "unknown",
176
- },
177
- }),
178
- "utf-8",
179
- );
180
-
181
- const state = await readNostrProfileState({ accountId: "test-bot" });
182
- expect(state).toEqual({
183
- version: 1,
184
- lastPublishedAt: 1700000000,
185
- lastPublishedEventId: "evt-1",
186
- lastPublishResults: null,
187
- });
188
- });
189
- });
190
- });
191
-
192
- describe("computeSinceTimestamp", () => {
193
- it("returns now for null state (fresh start)", () => {
194
- const now = 1700000000;
195
- expect(computeSinceTimestamp(null, now)).toBe(now);
196
- });
197
-
198
- it("uses lastProcessedAt when available", () => {
199
- const state: Parameters<typeof computeSinceTimestamp>[0] = {
200
- version: 2,
201
- lastProcessedAt: 1699999000,
202
- gatewayStartedAt: null,
203
- recentEventIds: [],
204
- };
205
- expect(computeSinceTimestamp(state, 1700000000)).toBe(1699999000);
206
- });
207
-
208
- it("uses gatewayStartedAt when lastProcessedAt is null", () => {
209
- const state: Parameters<typeof computeSinceTimestamp>[0] = {
210
- version: 2,
211
- lastProcessedAt: null,
212
- gatewayStartedAt: 1699998000,
213
- recentEventIds: [],
214
- };
215
- expect(computeSinceTimestamp(state, 1700000000)).toBe(1699998000);
216
- });
217
-
218
- it("uses the max of both timestamps", () => {
219
- const state: Parameters<typeof computeSinceTimestamp>[0] = {
220
- version: 2,
221
- lastProcessedAt: 1699999000,
222
- gatewayStartedAt: 1699998000,
223
- recentEventIds: [],
224
- };
225
- expect(computeSinceTimestamp(state, 1700000000)).toBe(1699999000);
226
- });
227
-
228
- it("falls back to now if both are null", () => {
229
- const state: Parameters<typeof computeSinceTimestamp>[0] = {
230
- version: 2,
231
- lastProcessedAt: null,
232
- gatewayStartedAt: null,
233
- recentEventIds: [],
234
- };
235
- expect(computeSinceTimestamp(state, 1700000000)).toBe(1700000000);
236
- });
237
- });
@@ -1,206 +0,0 @@
1
- import os from "node:os";
2
- import path from "node:path";
3
- import { safeParseJsonWithSchema } from "klaw/plugin-sdk/extension-shared";
4
- import { privateFileStore } from "klaw/plugin-sdk/security-runtime";
5
- import { z } from "zod";
6
- import { getNostrRuntime } from "./runtime.js";
7
-
8
- const STORE_VERSION = 2;
9
- const PROFILE_STATE_VERSION = 1;
10
-
11
- type NostrBusState = {
12
- version: 2;
13
- /** Unix timestamp (seconds) of the last processed event */
14
- lastProcessedAt: number | null;
15
- /** Gateway startup timestamp (seconds) - events before this are old */
16
- gatewayStartedAt: number | null;
17
- /** Recent processed event IDs for overlap dedupe across restarts */
18
- recentEventIds: string[];
19
- };
20
-
21
- /** Profile publish state (separate from bus state) */
22
- type NostrProfileState = {
23
- version: 1;
24
- /** Unix timestamp (seconds) of last successful profile publish */
25
- lastPublishedAt: number | null;
26
- /** Event ID of the last published profile */
27
- lastPublishedEventId: string | null;
28
- /** Per-relay publish results from last attempt */
29
- lastPublishResults: Record<string, "ok" | "failed" | "timeout"> | null;
30
- };
31
-
32
- const NullableFiniteNumberSchema = z.number().finite().nullable().catch(null);
33
- const NostrBusStateV1Schema = z.object({
34
- version: z.literal(1),
35
- lastProcessedAt: NullableFiniteNumberSchema,
36
- gatewayStartedAt: NullableFiniteNumberSchema,
37
- });
38
-
39
- const NostrBusStateSchema = z.object({
40
- version: z.literal(2),
41
- lastProcessedAt: NullableFiniteNumberSchema,
42
- gatewayStartedAt: NullableFiniteNumberSchema,
43
- recentEventIds: z
44
- .array(z.unknown())
45
- .catch([])
46
- .transform((ids) => ids.filter((id): id is string => typeof id === "string")),
47
- });
48
-
49
- const NostrProfileStateSchema = z.object({
50
- version: z.literal(1),
51
- lastPublishedAt: NullableFiniteNumberSchema,
52
- lastPublishedEventId: z.string().nullable().catch(null),
53
- lastPublishResults: z
54
- .record(z.string(), z.enum(["ok", "failed", "timeout"]))
55
- .nullable()
56
- .catch(null),
57
- });
58
-
59
- function normalizeAccountId(accountId?: string): string {
60
- const trimmed = accountId?.trim();
61
- if (!trimmed) {
62
- return "default";
63
- }
64
- return trimmed.replace(/[^a-z0-9._-]+/gi, "_");
65
- }
66
-
67
- function resolveNostrStatePath(accountId?: string, env: NodeJS.ProcessEnv = process.env): string {
68
- const stateDir = getNostrRuntime().state.resolveStateDir(env, os.homedir);
69
- const normalized = normalizeAccountId(accountId);
70
- return path.join(stateDir, "nostr", `bus-state-${normalized}.json`);
71
- }
72
-
73
- function resolveNostrProfileStatePath(
74
- accountId?: string,
75
- env: NodeJS.ProcessEnv = process.env,
76
- ): string {
77
- const stateDir = getNostrRuntime().state.resolveStateDir(env, os.homedir);
78
- const normalized = normalizeAccountId(accountId);
79
- return path.join(stateDir, "nostr", `profile-state-${normalized}.json`);
80
- }
81
-
82
- function safeParseState(raw: string): NostrBusState | null {
83
- const parsedV2 = safeParseJsonWithSchema(NostrBusStateSchema, raw);
84
- if (parsedV2) {
85
- return parsedV2;
86
- }
87
-
88
- const parsedV1 = safeParseJsonWithSchema(NostrBusStateV1Schema, raw);
89
- if (!parsedV1) {
90
- return null;
91
- }
92
-
93
- // Back-compat: v1 state files
94
- return {
95
- version: 2,
96
- lastProcessedAt: parsedV1.lastProcessedAt,
97
- gatewayStartedAt: parsedV1.gatewayStartedAt,
98
- recentEventIds: [],
99
- };
100
- }
101
-
102
- export async function readNostrBusState(params: {
103
- accountId?: string;
104
- env?: NodeJS.ProcessEnv;
105
- }): Promise<NostrBusState | null> {
106
- const filePath = resolveNostrStatePath(params.accountId, params.env);
107
- try {
108
- const raw = await privateFileStore(path.dirname(filePath)).readTextIfExists(
109
- path.basename(filePath),
110
- );
111
- if (raw === null) {
112
- return null;
113
- }
114
- return safeParseState(raw);
115
- } catch {
116
- return null;
117
- }
118
- }
119
-
120
- export async function writeNostrBusState(params: {
121
- accountId?: string;
122
- lastProcessedAt: number;
123
- gatewayStartedAt: number;
124
- recentEventIds?: string[];
125
- env?: NodeJS.ProcessEnv;
126
- }): Promise<void> {
127
- const filePath = resolveNostrStatePath(params.accountId, params.env);
128
- const payload: NostrBusState = {
129
- version: STORE_VERSION,
130
- lastProcessedAt: params.lastProcessedAt,
131
- gatewayStartedAt: params.gatewayStartedAt,
132
- recentEventIds: (params.recentEventIds ?? []).filter((x): x is string => typeof x === "string"),
133
- };
134
- await privateFileStore(path.dirname(filePath)).writeJson(path.basename(filePath), payload, {
135
- trailingNewline: true,
136
- });
137
- }
138
-
139
- /**
140
- * Determine the `since` timestamp for subscription.
141
- * Returns the later of: lastProcessedAt or gatewayStartedAt (both from disk),
142
- * falling back to `now` for fresh starts.
143
- */
144
- export function computeSinceTimestamp(
145
- state: NostrBusState | null,
146
- nowSec: number = Math.floor(Date.now() / 1000),
147
- ): number {
148
- if (!state) {
149
- return nowSec;
150
- }
151
-
152
- // Use the most recent timestamp we have
153
- const candidates = [state.lastProcessedAt, state.gatewayStartedAt].filter(
154
- (t): t is number => t !== null && t > 0,
155
- );
156
-
157
- if (candidates.length === 0) {
158
- return nowSec;
159
- }
160
- return Math.max(...candidates);
161
- }
162
-
163
- // ============================================================================
164
- // Profile State Management
165
- // ============================================================================
166
-
167
- function safeParseProfileState(raw: string): NostrProfileState | null {
168
- return safeParseJsonWithSchema(NostrProfileStateSchema, raw);
169
- }
170
-
171
- export async function readNostrProfileState(params: {
172
- accountId?: string;
173
- env?: NodeJS.ProcessEnv;
174
- }): Promise<NostrProfileState | null> {
175
- const filePath = resolveNostrProfileStatePath(params.accountId, params.env);
176
- try {
177
- const raw = await privateFileStore(path.dirname(filePath)).readTextIfExists(
178
- path.basename(filePath),
179
- );
180
- if (raw === null) {
181
- return null;
182
- }
183
- return safeParseProfileState(raw);
184
- } catch {
185
- return null;
186
- }
187
- }
188
-
189
- export async function writeNostrProfileState(params: {
190
- accountId?: string;
191
- lastPublishedAt: number;
192
- lastPublishedEventId: string;
193
- lastPublishResults: Record<string, "ok" | "failed" | "timeout">;
194
- env?: NodeJS.ProcessEnv;
195
- }): Promise<void> {
196
- const filePath = resolveNostrProfileStatePath(params.accountId, params.env);
197
- const payload: NostrProfileState = {
198
- version: PROFILE_STATE_VERSION,
199
- lastPublishedAt: params.lastPublishedAt,
200
- lastPublishedEventId: params.lastPublishedEventId,
201
- lastPublishResults: params.lastPublishResults,
202
- };
203
- await privateFileStore(path.dirname(filePath)).writeJson(path.basename(filePath), payload, {
204
- trailingNewline: true,
205
- });
206
- }
package/src/runtime.ts DELETED
@@ -1,9 +0,0 @@
1
- import type { PluginRuntime } from "klaw/plugin-sdk/core";
2
- import { createPluginRuntimeStore } from "klaw/plugin-sdk/runtime-store";
3
-
4
- const { setRuntime: setNostrRuntime, getRuntime: getNostrRuntime } =
5
- createPluginRuntimeStore<PluginRuntime>({
6
- pluginId: "nostr",
7
- errorMessage: "Nostr runtime not initialized",
8
- });
9
- export { getNostrRuntime, setNostrRuntime };