mothpost 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +326 -2
- package/config.example.json +19 -0
- package/package.json +49 -4
- package/src/admin.js +208 -0
- package/src/base-command.js +87 -0
- package/src/cli.js +24 -0
- package/src/client.js +394 -0
- package/src/common.js +80 -0
- package/src/config.js +53 -0
- package/src/server.js +441 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Option } from "commander";
|
|
2
|
+
import { mkdirSync } from "node:fs"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import { SeptClient } from "@sept-protocol/client"
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
betterSqliteDataStore,
|
|
9
|
+
loadOrCreateSecret,
|
|
10
|
+
} from "./common.js"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export class BaseCommand {
|
|
14
|
+
constructor(command, descr, program) {
|
|
15
|
+
this.role = command
|
|
16
|
+
this.command = program
|
|
17
|
+
.command(command)
|
|
18
|
+
.description(descr)
|
|
19
|
+
.option("-c, --config <file>", "JSON configuration file")
|
|
20
|
+
.option("--rest-endpoint <url>", "SEPT REST endpoint")
|
|
21
|
+
.option("--data-dir <directory>", "SEPT state and secret directory")
|
|
22
|
+
.addOption(
|
|
23
|
+
new Option("--log-level <level>", "logging verbosity")
|
|
24
|
+
.choices(["debug", "info", "warn", "error"]),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async run(method, p) {
|
|
29
|
+
const [command, options, ...args] = p.reverse()
|
|
30
|
+
this.config = loadConfig(command.optsWithGlobals())
|
|
31
|
+
const id = this.config[this.role]?.id ?? ""
|
|
32
|
+
mkdirSync(this.config.sept.dataDir, { recursive: true })
|
|
33
|
+
const fname = `${this.role}${id ? '-' + id : ''}`
|
|
34
|
+
this.sept = await SeptClient.create({
|
|
35
|
+
restEndpoint: this.config.sept.restEndpoint,
|
|
36
|
+
dataStore: betterSqliteDataStore(
|
|
37
|
+
join(this.config.sept.dataDir, `${fname}.db`)
|
|
38
|
+
),
|
|
39
|
+
secretKeyProvider: () =>
|
|
40
|
+
loadOrCreateSecret(
|
|
41
|
+
join(this.config.sept.dataDir, `${fname}.secret`)
|
|
42
|
+
),
|
|
43
|
+
})
|
|
44
|
+
await this.init()
|
|
45
|
+
await this[method](...args.reverse(), options)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
action(method) {
|
|
49
|
+
return async (...p) => {
|
|
50
|
+
try {
|
|
51
|
+
await this.run(method, p)
|
|
52
|
+
} finally {
|
|
53
|
+
await this.stop()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
daemonAction(method) {
|
|
59
|
+
return async (...p) => {
|
|
60
|
+
try {
|
|
61
|
+
await this.run(method, p)
|
|
62
|
+
} catch (error) {
|
|
63
|
+
await this.stop()
|
|
64
|
+
throw error
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const shutdown = async signal => {
|
|
68
|
+
console.log(`Received ${signal}, stopping...`)
|
|
69
|
+
try {
|
|
70
|
+
await this.stop()
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("Error while stopping:", error)
|
|
73
|
+
process.exitCode = 1
|
|
74
|
+
}
|
|
75
|
+
process.exit()
|
|
76
|
+
}
|
|
77
|
+
process.once("SIGINT", shutdown)
|
|
78
|
+
process.once("SIGTERM", shutdown)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async init() {}
|
|
83
|
+
|
|
84
|
+
async stop() {
|
|
85
|
+
await this.sept?.disconnect()
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
|
|
5
|
+
import { MothpostAdmin } from "./admin.js";
|
|
6
|
+
import { SeptMcpClient } from "./client.js";
|
|
7
|
+
import { SeptMcpServer } from "./server.js";
|
|
8
|
+
|
|
9
|
+
const program = new Command();
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name("mothpost")
|
|
13
|
+
.description("Bridge AI agents and MCP servers across networks through SEPT")
|
|
14
|
+
.version("0.1.0")
|
|
15
|
+
.showHelpAfterError();
|
|
16
|
+
|
|
17
|
+
new SeptMcpClient(program)
|
|
18
|
+
new SeptMcpServer(program)
|
|
19
|
+
new MothpostAdmin(program)
|
|
20
|
+
|
|
21
|
+
program.parseAsync().catch(error => {
|
|
22
|
+
console.error(error);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
});
|
package/src/client.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import crypto from "node:crypto"
|
|
2
|
+
import {
|
|
3
|
+
createMcpHandler,
|
|
4
|
+
fromJsonSchema,
|
|
5
|
+
McpServer,
|
|
6
|
+
} from "@modelcontextprotocol/server"
|
|
7
|
+
import { toNodeHandler } from "@modelcontextprotocol/node"
|
|
8
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/express"
|
|
9
|
+
import { BaseCommand } from "./base-command.js"
|
|
10
|
+
import { pairIfNeeded } from "./common.js"
|
|
11
|
+
|
|
12
|
+
export class SeptMcpClient extends BaseCommand {
|
|
13
|
+
constructor(program) {
|
|
14
|
+
super("client", "Run the client bridge: a local MCP endpoint for an AI agent", program)
|
|
15
|
+
this.command
|
|
16
|
+
.action(this.daemonAction("start"))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async init() {
|
|
21
|
+
this.service = this.config.client
|
|
22
|
+
|
|
23
|
+
this.gatewayDeviceId = null
|
|
24
|
+
this.allowedEventTypes = []
|
|
25
|
+
this.pending = new Map()
|
|
26
|
+
|
|
27
|
+
this.mcpHandler = null
|
|
28
|
+
this.httpServer = null
|
|
29
|
+
this.stopping = false
|
|
30
|
+
this.tools = null
|
|
31
|
+
this.toolsRequest = null
|
|
32
|
+
this.toolsGeneration = 0
|
|
33
|
+
let networkId = await this.sept.getNetworkId()
|
|
34
|
+
if (!networkId) {
|
|
35
|
+
await pairIfNeeded(
|
|
36
|
+
this.sept,
|
|
37
|
+
this.service.id,
|
|
38
|
+
"client"
|
|
39
|
+
);
|
|
40
|
+
networkId = await this.sept.getNetworkId()
|
|
41
|
+
console.log("Pairing done")
|
|
42
|
+
}
|
|
43
|
+
console.log(`NetworkID: ${networkId}`)
|
|
44
|
+
this.registerResponseHandler()
|
|
45
|
+
|
|
46
|
+
this.sept.on("policy.update", ({ policies }) =>
|
|
47
|
+
this.onPolicyUpdate(policies)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
await this.sept.connect()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async start() {
|
|
54
|
+
|
|
55
|
+
const deviceId = await this.sept.getDeviceId()
|
|
56
|
+
|
|
57
|
+
console.log(`[${this.service.id}] Connected as ${deviceId}`)
|
|
58
|
+
|
|
59
|
+
let gateway = null
|
|
60
|
+
while (gateway === null) {
|
|
61
|
+
try {
|
|
62
|
+
gateway = await this.discoverGateway()
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.log(`Waiting gateway grant`)
|
|
65
|
+
await new Promise(r => setTimeout(r, 1000))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.gatewayDeviceId = gateway.gatewayDeviceId
|
|
70
|
+
// this.allowedEventTypes = gateway.allowedEventTypes
|
|
71
|
+
|
|
72
|
+
console.log(
|
|
73
|
+
`[${this.service.id}] Discovered MCP gateway: ${this.gatewayDeviceId}`
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
console.log(
|
|
77
|
+
`[${this.service.id}] Granted MCP capabilities:`,
|
|
78
|
+
gateway.allowedEventTypes.filter(type => type.startsWith("mcp."))
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
this.mcpHandler = createMcpHandler(
|
|
82
|
+
async () => await this.buildMcpServer(),
|
|
83
|
+
{ responseMode: "json" }
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const nodeHandler = toNodeHandler(this.mcpHandler, {
|
|
87
|
+
onerror: error => {
|
|
88
|
+
console.error(`[${this.service.id}] MCP HTTP handler error:`, error)
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const app = createMcpExpressApp()
|
|
93
|
+
|
|
94
|
+
app.all("/mcp", (req, res) => {
|
|
95
|
+
void nodeHandler(req, res, req.body)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const { port, listenHost } = this.service
|
|
99
|
+
|
|
100
|
+
// Express 5 invokes the listen callback with the error too (e.g.
|
|
101
|
+
// EADDRINUSE), so check it instead of always reporting success.
|
|
102
|
+
try {
|
|
103
|
+
this.httpServer = await new Promise((resolve, reject) => {
|
|
104
|
+
const server = app.listen(port, listenHost, error =>
|
|
105
|
+
error ? reject(error) : resolve(server)
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(
|
|
110
|
+
`[${this.service.id}] Cannot listen on ${listenHost}:${port}: ${error.message}`
|
|
111
|
+
)
|
|
112
|
+
process.exit(1)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(
|
|
116
|
+
`[${this.service.id}] MCP proxy listening on http://${listenHost}:${port}/mcp`
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async discoverGateway() {
|
|
121
|
+
const ownDeviceId = await this.sept.getDeviceId()
|
|
122
|
+
const graph = await this.sept.getACL()
|
|
123
|
+
|
|
124
|
+
const relations = graph.filter(edge => {
|
|
125
|
+
if (edge.srcDeviceId !== ownDeviceId) {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const allowedEventTypes = edge.policy?.allowedEventTypes ?? []
|
|
130
|
+
|
|
131
|
+
return allowedEventTypes.some(type =>
|
|
132
|
+
type === "mcp.tools.list" ||
|
|
133
|
+
type.startsWith("mcp.tools.call.")
|
|
134
|
+
)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
if (relations.length === 0) {
|
|
138
|
+
throw new Error(`No MCP gateway authorized for ${ownDeviceId}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (relations.length > 1) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
"Multiple MCP gateways found; explicit selection is required"
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const relation = relations[0]
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
gatewayDeviceId: relation.dstDeviceId,
|
|
151
|
+
allowedEventTypes: relation.policy?.allowedEventTypes ?? [],
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
registerResponseHandler() {
|
|
156
|
+
this.sept.register("mcp.response", async event => {
|
|
157
|
+
if (event.senderDeviceId !== this.gatewayDeviceId) {
|
|
158
|
+
console.warn(
|
|
159
|
+
`[${this.service.id}] Ignoring mcp.response from`,
|
|
160
|
+
event.senderDeviceId
|
|
161
|
+
)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const {
|
|
166
|
+
requestId,
|
|
167
|
+
result,
|
|
168
|
+
error,
|
|
169
|
+
rpc,
|
|
170
|
+
} = event.payload
|
|
171
|
+
|
|
172
|
+
const request = this.pending.get(requestId)
|
|
173
|
+
|
|
174
|
+
if (!request) {
|
|
175
|
+
console.warn(`[${this.service.id}] Unknown requestId: ${requestId}`)
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
clearTimeout(request.timeout)
|
|
180
|
+
this.pending.delete(requestId)
|
|
181
|
+
|
|
182
|
+
if (error) {
|
|
183
|
+
request.reject(
|
|
184
|
+
new Error(error.message ?? "Unknown MCP gateway error")
|
|
185
|
+
)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (rpc?.error) {
|
|
190
|
+
request.reject(
|
|
191
|
+
new Error(rpc.error.message ?? "MCP JSON-RPC error")
|
|
192
|
+
)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
request.resolve(result ?? rpc?.result)
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async request(eventType, payload = {}) {
|
|
201
|
+
const requestId = crypto.randomUUID()
|
|
202
|
+
|
|
203
|
+
const responsePromise = new Promise((resolve, reject) => {
|
|
204
|
+
const timeout = setTimeout(() => {
|
|
205
|
+
this.pending.delete(requestId)
|
|
206
|
+
reject(new Error(`Timeout waiting for ${eventType}`))
|
|
207
|
+
}, this.service.requestTimeoutMs)
|
|
208
|
+
|
|
209
|
+
this.pending.set(requestId, {
|
|
210
|
+
resolve,
|
|
211
|
+
reject,
|
|
212
|
+
timeout,
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
await this.sept.send(
|
|
218
|
+
eventType,
|
|
219
|
+
{
|
|
220
|
+
requestId,
|
|
221
|
+
// Lets the server drop the request once we've stopped waiting
|
|
222
|
+
timeoutMs: this.service.requestTimeoutMs,
|
|
223
|
+
...payload,
|
|
224
|
+
},
|
|
225
|
+
[this.gatewayDeviceId]
|
|
226
|
+
)
|
|
227
|
+
} catch (error) {
|
|
228
|
+
const request = this.pending.get(requestId)
|
|
229
|
+
|
|
230
|
+
if (request) {
|
|
231
|
+
clearTimeout(request.timeout)
|
|
232
|
+
this.pending.delete(requestId)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
throw error
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return responsePromise
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async onPolicyUpdate(policies = []) {
|
|
242
|
+
const ownDeviceId = await this.sept.getDeviceId()
|
|
243
|
+
|
|
244
|
+
const affectsTools = policies.some(p =>
|
|
245
|
+
p.srcDeviceId === ownDeviceId &&
|
|
246
|
+
(this.gatewayDeviceId === null || p.dstDeviceId === this.gatewayDeviceId)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
if (!affectsTools) {
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
this.toolsGeneration++
|
|
254
|
+
this.tools = null
|
|
255
|
+
this.toolsRequest = null
|
|
256
|
+
|
|
257
|
+
this.mcpHandler?.notify.toolsChanged()
|
|
258
|
+
console.log(`[${this.service.id}] Tool permissions changed, notified agents`)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async getTools() {
|
|
262
|
+
if (this.tools !== null) {
|
|
263
|
+
return this.tools
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (this.toolsRequest === null) {
|
|
267
|
+
const generation = this.toolsGeneration
|
|
268
|
+
|
|
269
|
+
const request = this.request("mcp.tools.list")
|
|
270
|
+
.then(discovery => {
|
|
271
|
+
const tools = discovery?.tools ?? []
|
|
272
|
+
if (generation === this.toolsGeneration) {
|
|
273
|
+
this.tools = tools
|
|
274
|
+
}
|
|
275
|
+
return tools
|
|
276
|
+
})
|
|
277
|
+
.finally(() => {
|
|
278
|
+
if (this.toolsRequest === request) {
|
|
279
|
+
this.toolsRequest = null
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
|
|
283
|
+
this.toolsRequest = request
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return this.toolsRequest
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async buildMcpServer() {
|
|
290
|
+
const tools = await this.getTools()
|
|
291
|
+
|
|
292
|
+
console.log(
|
|
293
|
+
`[${this.service.id}] Allowed MCP tools:`,
|
|
294
|
+
tools.map(tool => tool.name)
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
const server = new McpServer(
|
|
298
|
+
{
|
|
299
|
+
name: `mothpost (${this.service.id})`,
|
|
300
|
+
version: "0.1.0",
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
capabilities: { tools: { listChanged: true } },
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
for (const tool of tools) {
|
|
308
|
+
const toolConfig = {
|
|
309
|
+
description: tool.description ?? "",
|
|
310
|
+
inputSchema: fromJsonSchema(
|
|
311
|
+
tool.inputSchema ?? {
|
|
312
|
+
type: "object",
|
|
313
|
+
properties: {},
|
|
314
|
+
}
|
|
315
|
+
),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (tool.title) {
|
|
319
|
+
toolConfig.title = tool.title
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (tool.annotations) {
|
|
323
|
+
toolConfig.annotations = tool.annotations
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (tool.outputSchema) {
|
|
327
|
+
toolConfig.outputSchema = fromJsonSchema(tool.outputSchema)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
server.registerTool(
|
|
331
|
+
tool.name,
|
|
332
|
+
toolConfig,
|
|
333
|
+
async arguments_ => {
|
|
334
|
+
console.log(
|
|
335
|
+
`[${this.service.id}] Calling ${tool.name} over SEPT`,
|
|
336
|
+
arguments_
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return this.request(
|
|
340
|
+
`mcp.tools.call.${tool.name}`,
|
|
341
|
+
{
|
|
342
|
+
arguments: arguments_,
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
console.log(`[${this.service.id}] Exposed MCP tool: ${tool.name}`)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return server
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async stop() {
|
|
355
|
+
if (this.stopping) {
|
|
356
|
+
return
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
this.stopping = true
|
|
360
|
+
|
|
361
|
+
console.log(`[${this.service?.id}] Stopping...`)
|
|
362
|
+
|
|
363
|
+
for (const [requestId, request] of this.pending ?? []) {
|
|
364
|
+
clearTimeout(request.timeout)
|
|
365
|
+
|
|
366
|
+
request.reject(
|
|
367
|
+
new Error(`Proxy stopped while waiting for ${requestId}`)
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
this.pending?.clear()
|
|
372
|
+
|
|
373
|
+
if (this.httpServer) {
|
|
374
|
+
this.httpServer.closeAllConnections()
|
|
375
|
+
await new Promise((resolve, reject) => {
|
|
376
|
+
this.httpServer.close(error => {
|
|
377
|
+
if (error) {
|
|
378
|
+
reject(error)
|
|
379
|
+
} else {
|
|
380
|
+
resolve()
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (this.mcpHandler) {
|
|
387
|
+
await this.mcpHandler.close?.()
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (this.sept) {
|
|
391
|
+
await this.sept.disconnect()
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
package/src/common.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { randomBytes } from "node:crypto"
|
|
3
|
+
import Database from "better-sqlite3"
|
|
4
|
+
import readline from "node:readline/promises"
|
|
5
|
+
import { stdin, stdout } from "node:process"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export function now() {
|
|
9
|
+
return Math.floor(Date.now() / 1000);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
export function isExpired(ts, w) {
|
|
14
|
+
return now() >= ts + w;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
export function sleep(ms) {
|
|
19
|
+
return new Promise(resolve => setTimeout(resolve, ms))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
export function betterSqliteDataStore(filename) {
|
|
24
|
+
return {
|
|
25
|
+
type: "better-sqlite",
|
|
26
|
+
|
|
27
|
+
open: () => {
|
|
28
|
+
const db = new Database(filename)
|
|
29
|
+
db.pragma("journal_mode = WAL")
|
|
30
|
+
return db
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
close: db => db.close(),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
export function loadOrCreateSecret(filename) {
|
|
39
|
+
if (!existsSync(filename)) {
|
|
40
|
+
writeFileSync(filename, randomBytes(32), { mode: 0o600 })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const secret = readFileSync(filename)
|
|
44
|
+
|
|
45
|
+
if (secret.length !== 32) {
|
|
46
|
+
throw new Error(`${filename} must contain exactly 32 bytes`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return new Uint8Array(secret)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
export async function pairIfNeeded(sept, name, role) {
|
|
54
|
+
if (await sept.getNetworkId()) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
const rl = readline.createInterface({
|
|
58
|
+
input: stdin,
|
|
59
|
+
output: stdout,
|
|
60
|
+
})
|
|
61
|
+
try {
|
|
62
|
+
const deviceData = await sept.initDevice()
|
|
63
|
+
|
|
64
|
+
deviceData.metadata = {
|
|
65
|
+
name,
|
|
66
|
+
role
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pairingData = Buffer.from(JSON.stringify(deviceData)).toString("base64")
|
|
70
|
+
|
|
71
|
+
console.log("Your pairing data:\n")
|
|
72
|
+
console.log(pairingData)
|
|
73
|
+
const pin = await rl.question("Pairing PIN: ")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
await sept.pairDevice(pin.trim())
|
|
77
|
+
} finally {
|
|
78
|
+
rl.close()
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_REST_ENDPOINT = "https://sept.filippo-572.workers.dev"
|
|
4
|
+
|
|
5
|
+
export function loadConfig(options = {}) {
|
|
6
|
+
const configFile = options.config ?? "./config.json";
|
|
7
|
+
if (options.config !== undefined && !fs.existsSync(configFile)) {
|
|
8
|
+
throw new Error(`Configuration file not found: ${configFile}`)
|
|
9
|
+
}
|
|
10
|
+
const fileConfig = fs.existsSync(configFile)
|
|
11
|
+
? JSON.parse(fs.readFileSync(configFile, "utf8"))
|
|
12
|
+
: {};
|
|
13
|
+
|
|
14
|
+
const config = {
|
|
15
|
+
sept: {
|
|
16
|
+
restEndpoint: fileConfig.sept?.restEndpoint || DEFAULT_REST_ENDPOINT,
|
|
17
|
+
logLevel: fileConfig.sept?.logLevel ?? "info",
|
|
18
|
+
dataDir: fileConfig.sept?.dataDir ?? ".",
|
|
19
|
+
},
|
|
20
|
+
client: {
|
|
21
|
+
id: fileConfig.client?.id ?? "client1",
|
|
22
|
+
listenHost: fileConfig.client?.listenHost ?? "127.0.0.1",
|
|
23
|
+
port: fileConfig.client?.port ?? 8001,
|
|
24
|
+
requestTimeoutMs: fileConfig.client?.requestTimeoutMs || 30000
|
|
25
|
+
},
|
|
26
|
+
server: {
|
|
27
|
+
id: fileConfig.server?.id ?? "server1",
|
|
28
|
+
transport: "streamable-http",
|
|
29
|
+
maxRequestAgeMs: 60000,
|
|
30
|
+
clockSkewMs: 5000,
|
|
31
|
+
...fileConfig.server,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const override = (section, key) => {
|
|
36
|
+
if (options[key] !== undefined) config[section][key] = options[key];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
override("sept", "restEndpoint")
|
|
40
|
+
override("sept", "logLevel")
|
|
41
|
+
override("sept", "dataDir")
|
|
42
|
+
override("client", "id")
|
|
43
|
+
override("client", "listenHost")
|
|
44
|
+
override("client", "port")
|
|
45
|
+
override("client", "requestTimeoutMs")
|
|
46
|
+
override("server", "id")
|
|
47
|
+
override("server", "transport")
|
|
48
|
+
override("server", "url")
|
|
49
|
+
override("server", "command")
|
|
50
|
+
override("server", "args")
|
|
51
|
+
|
|
52
|
+
return config
|
|
53
|
+
}
|