chainlesschain 0.47.8 → 0.47.9
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/package.json +10 -8
- package/src/commands/activitypub.js +533 -0
- package/src/commands/compliance.js +597 -6
- package/src/commands/matrix.js +283 -0
- package/src/commands/mcp.js +344 -0
- package/src/commands/nostr.js +196 -7
- package/src/commands/social.js +265 -0
- package/src/index.js +2 -0
- package/src/lib/activitypub-bridge.js +623 -0
- package/src/lib/compliance-framework-reporter.js +600 -0
- package/src/lib/matrix-bridge.js +252 -0
- package/src/lib/mcp-registry.js +347 -0
- package/src/lib/mcp-scaffold.js +385 -0
- package/src/lib/nostr-bridge.js +214 -38
- package/src/lib/social-graph.js +408 -0
- package/src/lib/stix-parser.js +167 -0
- package/src/lib/threat-intel.js +268 -0
- package/src/lib/topic-classifier.js +400 -0
- package/src/lib/ueba.js +403 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server scaffolder — pure template generator.
|
|
3
|
+
*
|
|
4
|
+
* Turns a `{name, description, transport, author}` spec into a list of
|
|
5
|
+
* `{path, content}` files that together form a runnable boilerplate
|
|
6
|
+
* project built on `@modelcontextprotocol/sdk`. Intentionally pure: the
|
|
7
|
+
* command layer decides where to write the files (and whether to
|
|
8
|
+
* overwrite), so tests can snapshot the output without hitting disk.
|
|
9
|
+
*
|
|
10
|
+
* Two transports are supported:
|
|
11
|
+
*
|
|
12
|
+
* - **stdio** — default, matches every MCP example under
|
|
13
|
+
* `@modelcontextprotocol/server-*`. Uses `StdioServerTransport`,
|
|
14
|
+
* run via `node index.js` and wired into a host's MCP config with
|
|
15
|
+
* `command: "node"`.
|
|
16
|
+
* - **http** — streamable HTTP + SSE. Uses `StreamableHTTPServerTransport`
|
|
17
|
+
* behind a tiny Express app so `cc mcp add <name> -u http://...`
|
|
18
|
+
* (or any MCP host) can connect.
|
|
19
|
+
*
|
|
20
|
+
* Every generated project includes:
|
|
21
|
+
*
|
|
22
|
+
* - package.json — `type: module`, `start` script, MCP SDK dep
|
|
23
|
+
* - index.js — 1 example tool (`echo`) + 1 example resource
|
|
24
|
+
* (`hello://world`) wired through the chosen transport
|
|
25
|
+
* - README.md — run + wire-up instructions tailored to transport
|
|
26
|
+
* - .gitignore — node_modules / logs / `.env`
|
|
27
|
+
*
|
|
28
|
+
* The scaffold stays intentionally tiny — "hello-world MCP server" —
|
|
29
|
+
* rather than trying to demo every SDK feature. Authors clone and extend.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/* ── constants ──────────────────────────────────────────────── */
|
|
33
|
+
|
|
34
|
+
export const SUPPORTED_TRANSPORTS = Object.freeze(["stdio", "http"]);
|
|
35
|
+
|
|
36
|
+
export const SDK_VERSION = "^1.0.0";
|
|
37
|
+
export const EXPRESS_VERSION = "^4.19.2";
|
|
38
|
+
|
|
39
|
+
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/;
|
|
40
|
+
|
|
41
|
+
/* ── public API ─────────────────────────────────────────────── */
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generate the full file set for a new MCP server project.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} spec
|
|
47
|
+
* @param {string} spec.name — npm-style kebab-case package name
|
|
48
|
+
* @param {string} [spec.description]
|
|
49
|
+
* @param {"stdio"|"http"} [spec.transport="stdio"]
|
|
50
|
+
* @param {string} [spec.author]
|
|
51
|
+
* @param {number} [spec.port=3333] — only used for http transport
|
|
52
|
+
* @returns {{ files: Array<{path:string, content:string}>, summary: object }}
|
|
53
|
+
*/
|
|
54
|
+
export function generateMcpServerScaffold(spec = {}) {
|
|
55
|
+
const name = normalizeName(spec.name);
|
|
56
|
+
const description =
|
|
57
|
+
(typeof spec.description === "string" && spec.description.trim()) ||
|
|
58
|
+
`An MCP server — ${name}`;
|
|
59
|
+
const transport = normalizeTransport(spec.transport);
|
|
60
|
+
const author = typeof spec.author === "string" ? spec.author.trim() : "";
|
|
61
|
+
const port = Number.isInteger(spec.port) && spec.port > 0 ? spec.port : 3333;
|
|
62
|
+
|
|
63
|
+
const files = [
|
|
64
|
+
{
|
|
65
|
+
path: "package.json",
|
|
66
|
+
content: renderPackageJson({ name, description, transport, author }),
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
path: "index.js",
|
|
70
|
+
content: renderIndexJs({ name, description, transport, port }),
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
path: "README.md",
|
|
74
|
+
content: renderReadme({ name, description, transport, port }),
|
|
75
|
+
},
|
|
76
|
+
{ path: ".gitignore", content: renderGitignore() },
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const summary = {
|
|
80
|
+
name,
|
|
81
|
+
description,
|
|
82
|
+
transport,
|
|
83
|
+
port: transport === "http" ? port : null,
|
|
84
|
+
fileCount: files.length,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return { files, summary };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Validate a proposed server name. Throws with a concrete reason —
|
|
92
|
+
* callers can surface the message verbatim.
|
|
93
|
+
*/
|
|
94
|
+
export function normalizeName(raw) {
|
|
95
|
+
if (typeof raw !== "string" || !raw.trim()) {
|
|
96
|
+
throw new Error("Server name is required.");
|
|
97
|
+
}
|
|
98
|
+
const name = raw.trim().toLowerCase();
|
|
99
|
+
if (!NAME_PATTERN.test(name)) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Invalid server name "${raw}". ` +
|
|
102
|
+
`Use lowercase letters, digits, or hyphens (e.g. "my-weather").`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (name.length > 60) {
|
|
106
|
+
throw new Error("Server name must be 60 characters or fewer.");
|
|
107
|
+
}
|
|
108
|
+
return name;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function normalizeTransport(raw) {
|
|
112
|
+
const t =
|
|
113
|
+
(typeof raw === "string" ? raw.trim().toLowerCase() : "stdio") || "stdio";
|
|
114
|
+
if (!SUPPORTED_TRANSPORTS.includes(t)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Unknown transport "${raw}". Supported: ${SUPPORTED_TRANSPORTS.join(", ")}.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return t;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* ── renderers ──────────────────────────────────────────────── */
|
|
123
|
+
|
|
124
|
+
function renderPackageJson({ name, description, transport, author }) {
|
|
125
|
+
const deps = {
|
|
126
|
+
"@modelcontextprotocol/sdk": SDK_VERSION,
|
|
127
|
+
};
|
|
128
|
+
if (transport === "http") deps.express = EXPRESS_VERSION;
|
|
129
|
+
|
|
130
|
+
const pkg = {
|
|
131
|
+
name,
|
|
132
|
+
version: "0.1.0",
|
|
133
|
+
description,
|
|
134
|
+
type: "module",
|
|
135
|
+
main: "index.js",
|
|
136
|
+
scripts: {
|
|
137
|
+
start: "node index.js",
|
|
138
|
+
},
|
|
139
|
+
dependencies: deps,
|
|
140
|
+
};
|
|
141
|
+
if (author) pkg.author = author;
|
|
142
|
+
|
|
143
|
+
return JSON.stringify(pkg, null, 2) + "\n";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function renderIndexJs({ name, description, transport, port }) {
|
|
147
|
+
if (transport === "stdio") return renderStdioIndex({ name, description });
|
|
148
|
+
return renderHttpIndex({ name, description, port });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function renderStdioIndex({ name, description }) {
|
|
152
|
+
return `#!/usr/bin/env node
|
|
153
|
+
/**
|
|
154
|
+
* ${name} — ${description}
|
|
155
|
+
*
|
|
156
|
+
* An MCP server over stdio. Registered by an MCP host via:
|
|
157
|
+
*
|
|
158
|
+
* { "command": "node", "args": ["./index.js"] }
|
|
159
|
+
*
|
|
160
|
+
* (Or via \`cc mcp add ${name} -c node -a "./index.js"\`.)
|
|
161
|
+
*/
|
|
162
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
163
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
164
|
+
import {
|
|
165
|
+
CallToolRequestSchema,
|
|
166
|
+
ListResourcesRequestSchema,
|
|
167
|
+
ListToolsRequestSchema,
|
|
168
|
+
ReadResourceRequestSchema,
|
|
169
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
170
|
+
|
|
171
|
+
const server = new Server(
|
|
172
|
+
{ name: "${name}", version: "0.1.0" },
|
|
173
|
+
{ capabilities: { tools: {}, resources: {} } },
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// ── Example tool ─────────────────────────────────────────────
|
|
177
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
178
|
+
tools: [
|
|
179
|
+
{
|
|
180
|
+
name: "echo",
|
|
181
|
+
description: "Echo back the supplied message.",
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: "object",
|
|
184
|
+
required: ["message"],
|
|
185
|
+
properties: {
|
|
186
|
+
message: { type: "string", description: "Text to echo." },
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
}));
|
|
192
|
+
|
|
193
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
194
|
+
const { name: tool, arguments: args } = req.params;
|
|
195
|
+
if (tool === "echo") {
|
|
196
|
+
return {
|
|
197
|
+
content: [{ type: "text", text: String(args?.message ?? "") }],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
throw new Error(\`Unknown tool: \${tool}\`);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// ── Example resource ─────────────────────────────────────────
|
|
204
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
205
|
+
resources: [
|
|
206
|
+
{
|
|
207
|
+
uri: "hello://world",
|
|
208
|
+
name: "Hello world",
|
|
209
|
+
description: "A trivial resource you can read to test connectivity.",
|
|
210
|
+
mimeType: "text/plain",
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
}));
|
|
214
|
+
|
|
215
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
216
|
+
if (req.params.uri === "hello://world") {
|
|
217
|
+
return {
|
|
218
|
+
contents: [
|
|
219
|
+
{ uri: req.params.uri, mimeType: "text/plain", text: "Hello, MCP!" },
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
throw new Error(\`Unknown resource: \${req.params.uri}\`);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// ── Start ────────────────────────────────────────────────────
|
|
227
|
+
const transport = new StdioServerTransport();
|
|
228
|
+
await server.connect(transport);
|
|
229
|
+
`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function renderHttpIndex({ name, description, port }) {
|
|
233
|
+
return `#!/usr/bin/env node
|
|
234
|
+
/**
|
|
235
|
+
* ${name} — ${description}
|
|
236
|
+
*
|
|
237
|
+
* An MCP server over Streamable HTTP + SSE. Started with:
|
|
238
|
+
*
|
|
239
|
+
* npm start # listens on 0.0.0.0:${port}
|
|
240
|
+
* PORT=9001 npm start # override port
|
|
241
|
+
*
|
|
242
|
+
* Hosts connect by URL — with this CLI:
|
|
243
|
+
*
|
|
244
|
+
* cc mcp add ${name} -u http://localhost:${port}/mcp
|
|
245
|
+
*/
|
|
246
|
+
import express from "express";
|
|
247
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
248
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
249
|
+
import {
|
|
250
|
+
CallToolRequestSchema,
|
|
251
|
+
ListResourcesRequestSchema,
|
|
252
|
+
ListToolsRequestSchema,
|
|
253
|
+
ReadResourceRequestSchema,
|
|
254
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
255
|
+
|
|
256
|
+
function makeServer() {
|
|
257
|
+
const server = new Server(
|
|
258
|
+
{ name: "${name}", version: "0.1.0" },
|
|
259
|
+
{ capabilities: { tools: {}, resources: {} } },
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
263
|
+
tools: [
|
|
264
|
+
{
|
|
265
|
+
name: "echo",
|
|
266
|
+
description: "Echo back the supplied message.",
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: "object",
|
|
269
|
+
required: ["message"],
|
|
270
|
+
properties: {
|
|
271
|
+
message: { type: "string", description: "Text to echo." },
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
}));
|
|
277
|
+
|
|
278
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
279
|
+
const { name: tool, arguments: args } = req.params;
|
|
280
|
+
if (tool === "echo") {
|
|
281
|
+
return {
|
|
282
|
+
content: [{ type: "text", text: String(args?.message ?? "") }],
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
throw new Error(\`Unknown tool: \${tool}\`);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
289
|
+
resources: [
|
|
290
|
+
{
|
|
291
|
+
uri: "hello://world",
|
|
292
|
+
name: "Hello world",
|
|
293
|
+
description: "A trivial resource you can read to test connectivity.",
|
|
294
|
+
mimeType: "text/plain",
|
|
295
|
+
},
|
|
296
|
+
],
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
300
|
+
if (req.params.uri === "hello://world") {
|
|
301
|
+
return {
|
|
302
|
+
contents: [
|
|
303
|
+
{ uri: req.params.uri, mimeType: "text/plain", text: "Hello, MCP!" },
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
throw new Error(\`Unknown resource: \${req.params.uri}\`);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
return server;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const app = express();
|
|
314
|
+
app.use(express.json());
|
|
315
|
+
|
|
316
|
+
app.all("/mcp", async (req, res) => {
|
|
317
|
+
const transport = new StreamableHTTPServerTransport({
|
|
318
|
+
sessionIdGenerator: () => crypto.randomUUID(),
|
|
319
|
+
});
|
|
320
|
+
const server = makeServer();
|
|
321
|
+
await server.connect(transport);
|
|
322
|
+
await transport.handleRequest(req, res, req.body);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const port = Number(process.env.PORT) || ${port};
|
|
326
|
+
app.listen(port, () => {
|
|
327
|
+
console.log(\`[${name}] MCP server listening on http://localhost:\${port}/mcp\`);
|
|
328
|
+
});
|
|
329
|
+
`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function renderReadme({ name, description, transport, port }) {
|
|
333
|
+
const wireUp =
|
|
334
|
+
transport === "stdio"
|
|
335
|
+
? `cc mcp add ${name} -c node -a "./index.js"`
|
|
336
|
+
: `cc mcp add ${name} -u http://localhost:${port}/mcp`;
|
|
337
|
+
|
|
338
|
+
const run =
|
|
339
|
+
transport === "stdio"
|
|
340
|
+
? "An MCP host invokes the server on demand — there's no long-running process. Use `node index.js` only to sanity-check that the file parses."
|
|
341
|
+
: `\`\`\`bash\nnpm install\nnpm start # listens on http://localhost:${port}/mcp\n\`\`\``;
|
|
342
|
+
|
|
343
|
+
return `# ${name}
|
|
344
|
+
|
|
345
|
+
${description}
|
|
346
|
+
|
|
347
|
+
Transport: **${transport}**${transport === "http" ? ` (port ${port})` : ""}
|
|
348
|
+
|
|
349
|
+
## Install
|
|
350
|
+
|
|
351
|
+
\`\`\`bash
|
|
352
|
+
npm install
|
|
353
|
+
\`\`\`
|
|
354
|
+
|
|
355
|
+
## Run
|
|
356
|
+
|
|
357
|
+
${run}
|
|
358
|
+
|
|
359
|
+
## Wire into a host
|
|
360
|
+
|
|
361
|
+
${transport === "stdio" ? "" : ""}\`\`\`bash
|
|
362
|
+
${wireUp}
|
|
363
|
+
cc mcp tools # list discovered tools
|
|
364
|
+
cc mcp call ${name} echo --args '{"message":"hi"}'
|
|
365
|
+
\`\`\`
|
|
366
|
+
|
|
367
|
+
## What's in the box
|
|
368
|
+
|
|
369
|
+
- \`echo\` tool — accepts \`{message}\`, returns it verbatim.
|
|
370
|
+
- \`hello://world\` resource — a trivial plain-text resource you can read
|
|
371
|
+
to confirm the server is reachable.
|
|
372
|
+
|
|
373
|
+
Extend \`index.js\` with your own \`Set*RequestHandler\` calls; the MCP SDK
|
|
374
|
+
docs at https://modelcontextprotocol.io/docs cover every request type.
|
|
375
|
+
`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function renderGitignore() {
|
|
379
|
+
return `node_modules/
|
|
380
|
+
*.log
|
|
381
|
+
.env
|
|
382
|
+
.env.*
|
|
383
|
+
!.env.example
|
|
384
|
+
`;
|
|
385
|
+
}
|
package/src/lib/nostr-bridge.js
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import crypto from "crypto";
|
|
7
|
+
import {
|
|
8
|
+
generatePrivateKey as _genPriv,
|
|
9
|
+
getPublicKey as _getPub,
|
|
10
|
+
signEvent as _signEvent,
|
|
11
|
+
verifyEvent as _verifyEvent,
|
|
12
|
+
npubEncode as _npubEncode,
|
|
13
|
+
nsecEncode as _nsecEncode,
|
|
14
|
+
} from "@chainlesschain/session-core/nostr-crypto";
|
|
7
15
|
|
|
8
16
|
/* ── In-memory stores ──────────────────────────────────────── */
|
|
9
17
|
const _relays = new Map();
|
|
@@ -88,36 +96,71 @@ export function addRelay(db, url) {
|
|
|
88
96
|
|
|
89
97
|
/* ── Event Publishing ─────────────────────────────────────── */
|
|
90
98
|
|
|
91
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Publish a NIP-01 event. When `privateKey` is provided the event is
|
|
101
|
+
* schnorr-signed with BIP-340 and `pubkey` is derived from it (any
|
|
102
|
+
* caller-supplied `pubkey` is ignored and overridden to match the key).
|
|
103
|
+
* Without a private key the caller gets an *unsigned* event — useful
|
|
104
|
+
* for in-memory workflows (e.g. `cc nostr publish` from REPL demos) but
|
|
105
|
+
* such events will be rejected by real relays.
|
|
106
|
+
*/
|
|
107
|
+
export function publishEvent(db, kind, content, pubkey, tags, privateKey) {
|
|
92
108
|
if (content === undefined || content === null)
|
|
93
109
|
throw new Error("Content is required");
|
|
94
110
|
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
const createdAt = Math.floor(Date.now() / 1000);
|
|
112
|
+
const resolvedKind = kind || EVENT_KINDS.TEXT_NOTE;
|
|
113
|
+
const resolvedTags = tags || [];
|
|
114
|
+
|
|
115
|
+
let event;
|
|
116
|
+
if (privateKey) {
|
|
117
|
+
const derivedPubkey = _getPub(privateKey);
|
|
118
|
+
event = _signEvent(
|
|
119
|
+
{
|
|
120
|
+
pubkey: derivedPubkey,
|
|
121
|
+
created_at: createdAt,
|
|
122
|
+
kind: resolvedKind,
|
|
123
|
+
tags: resolvedTags,
|
|
124
|
+
content,
|
|
125
|
+
},
|
|
126
|
+
privateKey,
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
// Unsigned path: compute a deterministic id from canonical serialization
|
|
130
|
+
// so dedup still works, but leave sig empty to signal "not signed".
|
|
131
|
+
const fallbackPubkey = pubkey || "anonymous";
|
|
132
|
+
const serialized = JSON.stringify([
|
|
133
|
+
0,
|
|
134
|
+
fallbackPubkey,
|
|
135
|
+
createdAt,
|
|
136
|
+
resolvedKind,
|
|
137
|
+
resolvedTags,
|
|
138
|
+
content,
|
|
139
|
+
]);
|
|
140
|
+
const id = crypto.createHash("sha256").update(serialized).digest("hex");
|
|
141
|
+
event = {
|
|
142
|
+
id,
|
|
143
|
+
pubkey: fallbackPubkey,
|
|
144
|
+
created_at: createdAt,
|
|
145
|
+
kind: resolvedKind,
|
|
146
|
+
tags: resolvedTags,
|
|
147
|
+
content,
|
|
148
|
+
sig: "",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
106
151
|
|
|
107
|
-
const
|
|
108
|
-
id,
|
|
109
|
-
pubkey: pubkey
|
|
110
|
-
kind: kind
|
|
111
|
-
content,
|
|
112
|
-
tags: tags
|
|
113
|
-
sig,
|
|
114
|
-
createdAt:
|
|
152
|
+
const storedEvent = {
|
|
153
|
+
id: event.id,
|
|
154
|
+
pubkey: event.pubkey,
|
|
155
|
+
kind: event.kind,
|
|
156
|
+
content: event.content,
|
|
157
|
+
tags: event.tags,
|
|
158
|
+
sig: event.sig,
|
|
159
|
+
createdAt: event.created_at,
|
|
115
160
|
relayUrl: null,
|
|
116
161
|
};
|
|
162
|
+
_events.set(event.id, storedEvent);
|
|
117
163
|
|
|
118
|
-
_events.set(id, event);
|
|
119
|
-
|
|
120
|
-
// Count relays that received the event
|
|
121
164
|
const writeRelays = [..._relays.values()].filter((r) => r.writeEnabled);
|
|
122
165
|
let sentCount = 0;
|
|
123
166
|
for (const relay of writeRelays) {
|
|
@@ -129,18 +172,35 @@ export function publishEvent(db, kind, content, pubkey, tags) {
|
|
|
129
172
|
`INSERT INTO nostr_events (id, pubkey, kind, content, tags, sig, created_at, relay_url, imported)
|
|
130
173
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
131
174
|
).run(
|
|
132
|
-
id,
|
|
175
|
+
event.id,
|
|
133
176
|
event.pubkey,
|
|
134
177
|
event.kind,
|
|
135
178
|
content,
|
|
136
|
-
JSON.stringify(
|
|
137
|
-
sig,
|
|
138
|
-
|
|
179
|
+
JSON.stringify(resolvedTags),
|
|
180
|
+
event.sig,
|
|
181
|
+
new Date(createdAt * 1000).toISOString(),
|
|
139
182
|
null,
|
|
140
183
|
0,
|
|
141
184
|
);
|
|
142
185
|
|
|
143
|
-
return { success: true, event, sentCount };
|
|
186
|
+
return { success: true, event: storedEvent, sentCount };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Verify an event's schnorr signature + id integrity (NIP-01).
|
|
191
|
+
* Returns false for unsigned events (empty `sig`).
|
|
192
|
+
*/
|
|
193
|
+
export function verifyEventSignature(event) {
|
|
194
|
+
if (!event || !event.sig) return false;
|
|
195
|
+
return _verifyEvent({
|
|
196
|
+
id: event.id,
|
|
197
|
+
pubkey: event.pubkey,
|
|
198
|
+
created_at: event.createdAt ?? event.created_at,
|
|
199
|
+
kind: event.kind,
|
|
200
|
+
tags: event.tags,
|
|
201
|
+
content: event.content,
|
|
202
|
+
sig: event.sig,
|
|
203
|
+
});
|
|
144
204
|
}
|
|
145
205
|
|
|
146
206
|
/* ── Event Retrieval ──────────────────────────────────────── */
|
|
@@ -157,21 +217,17 @@ export function getEvents(filter = {}) {
|
|
|
157
217
|
/* ── Keypair Generation ───────────────────────────────────── */
|
|
158
218
|
|
|
159
219
|
export function generateKeypair() {
|
|
160
|
-
const privateKey =
|
|
161
|
-
const publicKey =
|
|
162
|
-
.createHash("sha256")
|
|
163
|
-
.update(privateKey)
|
|
164
|
-
.digest("hex");
|
|
165
|
-
const id = crypto.randomUUID();
|
|
166
|
-
|
|
220
|
+
const privateKey = _genPriv();
|
|
221
|
+
const publicKey = _getPub(privateKey);
|
|
167
222
|
const keypair = {
|
|
168
|
-
id,
|
|
223
|
+
id: crypto.randomUUID(),
|
|
169
224
|
publicKey,
|
|
170
225
|
privateKey,
|
|
226
|
+
npub: _npubEncode(publicKey),
|
|
227
|
+
nsec: _nsecEncode(privateKey),
|
|
171
228
|
createdAt: new Date().toISOString(),
|
|
172
229
|
};
|
|
173
|
-
_keypairs.set(id, keypair);
|
|
174
|
-
|
|
230
|
+
_keypairs.set(keypair.id, keypair);
|
|
175
231
|
return keypair;
|
|
176
232
|
}
|
|
177
233
|
|
|
@@ -185,6 +241,126 @@ export function mapDid(did, nostrPubkey) {
|
|
|
185
241
|
return { did, nostrPubkey, mapped: true };
|
|
186
242
|
}
|
|
187
243
|
|
|
244
|
+
/* ── NIP-04: Encrypted Direct Messages ─────────────────────── */
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Compute NIP-04 ECDH shared secret.
|
|
248
|
+
* Nostr pubkeys are x-only (32 bytes); prepend 0x02 to reconstruct a
|
|
249
|
+
* compressed point. Point negation yields the same x-coordinate, so the
|
|
250
|
+
* y-sign does not affect the shared secret.
|
|
251
|
+
*/
|
|
252
|
+
function _computeSharedSecret(privKeyHex, pubKeyHex) {
|
|
253
|
+
const ecdh = crypto.createECDH("secp256k1");
|
|
254
|
+
ecdh.setPrivateKey(Buffer.from(privKeyHex, "hex"));
|
|
255
|
+
const compressedPub = Buffer.concat([
|
|
256
|
+
Buffer.from([0x02]),
|
|
257
|
+
Buffer.from(pubKeyHex, "hex"),
|
|
258
|
+
]);
|
|
259
|
+
return ecdh.computeSecret(compressedPub);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Publish a NIP-04 encrypted direct message (kind=4).
|
|
264
|
+
* @param {Object} db - SQLite database handle
|
|
265
|
+
* @param {Object} params - { senderPrivkey, senderPubkey, recipientPubkey, plaintext }
|
|
266
|
+
* @returns {Object} { success, event, sentCount }
|
|
267
|
+
*/
|
|
268
|
+
export function publishDirectMessage(
|
|
269
|
+
db,
|
|
270
|
+
{ senderPrivkey, senderPubkey, recipientPubkey, plaintext },
|
|
271
|
+
) {
|
|
272
|
+
if (!senderPrivkey || !senderPubkey || !recipientPubkey) {
|
|
273
|
+
throw new Error(
|
|
274
|
+
"senderPrivkey, senderPubkey, and recipientPubkey are required",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (plaintext === undefined || plaintext === null) {
|
|
278
|
+
throw new Error("plaintext is required");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const sharedSecret = _computeSharedSecret(senderPrivkey, recipientPubkey);
|
|
282
|
+
const iv = crypto.randomBytes(16);
|
|
283
|
+
const cipher = crypto.createCipheriv("aes-256-cbc", sharedSecret, iv);
|
|
284
|
+
const encrypted = Buffer.concat([
|
|
285
|
+
cipher.update(plaintext, "utf8"),
|
|
286
|
+
cipher.final(),
|
|
287
|
+
]);
|
|
288
|
+
const content = `${encrypted.toString("base64")}?iv=${iv.toString("base64")}`;
|
|
289
|
+
|
|
290
|
+
return publishEvent(db, EVENT_KINDS.ENCRYPTED_DM, content, senderPubkey, [
|
|
291
|
+
["p", recipientPubkey],
|
|
292
|
+
]);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Decrypt a NIP-04 direct message event.
|
|
297
|
+
* @param {Object} params - { event, recipientPrivkey }
|
|
298
|
+
* @returns {string} decrypted plaintext
|
|
299
|
+
*/
|
|
300
|
+
export function decryptDirectMessage({ event, recipientPrivkey }) {
|
|
301
|
+
if (!event || !event.content || !event.pubkey) {
|
|
302
|
+
throw new Error("event with content and pubkey is required");
|
|
303
|
+
}
|
|
304
|
+
if (!recipientPrivkey) {
|
|
305
|
+
throw new Error("recipientPrivkey is required");
|
|
306
|
+
}
|
|
307
|
+
if (event.kind !== EVENT_KINDS.ENCRYPTED_DM) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Expected kind=${EVENT_KINDS.ENCRYPTED_DM}, got kind=${event.kind}`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const parts = event.content.split("?iv=");
|
|
314
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
315
|
+
throw new Error("Invalid NIP-04 content format");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const ciphertext = Buffer.from(parts[0], "base64");
|
|
319
|
+
const iv = Buffer.from(parts[1], "base64");
|
|
320
|
+
const sharedSecret = _computeSharedSecret(recipientPrivkey, event.pubkey);
|
|
321
|
+
const decipher = crypto.createDecipheriv("aes-256-cbc", sharedSecret, iv);
|
|
322
|
+
const decrypted = Buffer.concat([
|
|
323
|
+
decipher.update(ciphertext),
|
|
324
|
+
decipher.final(),
|
|
325
|
+
]);
|
|
326
|
+
return decrypted.toString("utf8");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* ── NIP-09: Event Deletion Request ─────────────────────────── */
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Publish a NIP-09 deletion request (kind=5) referencing prior events.
|
|
333
|
+
* @param {Object} db - SQLite database handle
|
|
334
|
+
* @param {Object} params - { eventIds: string[], reason?: string, pubkey?: string }
|
|
335
|
+
*/
|
|
336
|
+
export function publishDeletion(db, { eventIds, reason = "", pubkey }) {
|
|
337
|
+
if (!Array.isArray(eventIds) || eventIds.length === 0) {
|
|
338
|
+
throw new Error("eventIds must be a non-empty array");
|
|
339
|
+
}
|
|
340
|
+
const tags = eventIds.map((id) => ["e", id]);
|
|
341
|
+
return publishEvent(db, EVENT_KINDS.DELETE, reason, pubkey, tags);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* ── NIP-25: Reactions ──────────────────────────────────────── */
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Publish a NIP-25 reaction (kind=7) to another event.
|
|
348
|
+
* @param {Object} db - SQLite database handle
|
|
349
|
+
* @param {Object} params - { targetEventId, targetPubkey, content?, pubkey? }
|
|
350
|
+
*/
|
|
351
|
+
export function publishReaction(
|
|
352
|
+
db,
|
|
353
|
+
{ targetEventId, targetPubkey, content = "+", pubkey },
|
|
354
|
+
) {
|
|
355
|
+
if (!targetEventId || !targetPubkey) {
|
|
356
|
+
throw new Error("targetEventId and targetPubkey are required");
|
|
357
|
+
}
|
|
358
|
+
return publishEvent(db, EVENT_KINDS.REACTION, content, pubkey, [
|
|
359
|
+
["e", targetEventId],
|
|
360
|
+
["p", targetPubkey],
|
|
361
|
+
]);
|
|
362
|
+
}
|
|
363
|
+
|
|
188
364
|
/* ── Reset (for testing) ───────────────────────────────────── */
|
|
189
365
|
|
|
190
366
|
export function _resetState() {
|