@thynker-labs/server 0.0.1
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/dist/index.d.ts +32 -0
- package/dist/index.js +115 -0
- package/package.json +40 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Router, Express, Request, Response, NextFunction } from 'express';
|
|
2
|
+
export { Router } from 'express';
|
|
3
|
+
import { Kernel, JobQueue, Stores } from '@thynker-labs/core';
|
|
4
|
+
import { WebTransport } from '@thynker-labs/transport-web';
|
|
5
|
+
|
|
6
|
+
interface ThynkerServerConfig {
|
|
7
|
+
port?: number;
|
|
8
|
+
/** Env var for shared webhook secret (default WEBHOOK_SECRET). */
|
|
9
|
+
webhookSecretEnv?: string;
|
|
10
|
+
/** Allowed CORS origins (default * for dev). */
|
|
11
|
+
corsOrigins?: string | string[];
|
|
12
|
+
}
|
|
13
|
+
interface CreateThynkerServerOptions {
|
|
14
|
+
kernel: Kernel;
|
|
15
|
+
webTransport: WebTransport;
|
|
16
|
+
config?: ThynkerServerConfig;
|
|
17
|
+
queue?: JobQueue;
|
|
18
|
+
stores?: Stores;
|
|
19
|
+
/** Extra REST routes mounted at /api (after built-in routes). */
|
|
20
|
+
apiRouter?: Router;
|
|
21
|
+
/** Webhook routes mounted at /webhooks (after secret middleware). */
|
|
22
|
+
webhookRouter?: Router;
|
|
23
|
+
}
|
|
24
|
+
declare function verifyWebhookSecret(secretEnv?: string): (req: Request, res: Response, next: NextFunction) => void;
|
|
25
|
+
declare function createThynkerServer(options: CreateThynkerServerOptions): Express;
|
|
26
|
+
declare function startThynkerServer(options: CreateThynkerServerOptions): Promise<{
|
|
27
|
+
app: Express;
|
|
28
|
+
port: number;
|
|
29
|
+
close: () => Promise<void>;
|
|
30
|
+
}>;
|
|
31
|
+
|
|
32
|
+
export { type CreateThynkerServerOptions, type ThynkerServerConfig, createThynkerServer, startThynkerServer, verifyWebhookSecret };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import express from "express";
|
|
4
|
+
import cors from "cors";
|
|
5
|
+
function verifyWebhookSecret(secretEnv = "WEBHOOK_SECRET") {
|
|
6
|
+
return (req, res, next) => {
|
|
7
|
+
const expected = process.env[secretEnv];
|
|
8
|
+
if (!expected) {
|
|
9
|
+
res.status(503).json({ error: `${secretEnv} not configured` });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const provided = req.header("x-webhook-secret") ?? req.header("x-thynker-webhook-secret") ?? (typeof req.query.secret === "string" ? req.query.secret : void 0);
|
|
13
|
+
if (provided !== expected) {
|
|
14
|
+
res.status(401).json({ error: "Invalid webhook secret" });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
next();
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function createThynkerServer(options) {
|
|
21
|
+
const app = express();
|
|
22
|
+
const secretEnv = options.config?.webhookSecretEnv ?? "WEBHOOK_SECRET";
|
|
23
|
+
const corsOrigins = options.config?.corsOrigins ?? "*";
|
|
24
|
+
app.use(cors({ origin: corsOrigins, credentials: true }));
|
|
25
|
+
app.use(express.json({ limit: "2mb" }));
|
|
26
|
+
app.get("/api/health", (_req, res) => {
|
|
27
|
+
res.json({ ok: true, service: "thynker-api" });
|
|
28
|
+
});
|
|
29
|
+
app.post("/api/chat", async (req, res) => {
|
|
30
|
+
try {
|
|
31
|
+
const sessionId = String(req.body.sessionId ?? req.body.session ?? randomUUID());
|
|
32
|
+
const message = String(req.body.message ?? "");
|
|
33
|
+
const asyncMode = Boolean(req.body.async);
|
|
34
|
+
if (!message.trim()) {
|
|
35
|
+
res.status(400).json({ error: "message is required" });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (asyncMode) {
|
|
39
|
+
if (!options.queue) {
|
|
40
|
+
res.status(503).json({ error: "Async chat requires a configured job queue (BullMQ)" });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const jobId = randomUUID();
|
|
44
|
+
await options.queue.enqueue({
|
|
45
|
+
envelope: {
|
|
46
|
+
identity: {
|
|
47
|
+
id: sessionId,
|
|
48
|
+
plane: "user",
|
|
49
|
+
roles: ["user"],
|
|
50
|
+
scopes: ["*"]
|
|
51
|
+
},
|
|
52
|
+
channel: "web",
|
|
53
|
+
replyTo: sessionId,
|
|
54
|
+
parts: [{ kind: "text", text: message }]
|
|
55
|
+
},
|
|
56
|
+
meta: { jobId, asyncChat: true, sessionId }
|
|
57
|
+
});
|
|
58
|
+
res.status(202).json({ jobId, status: "queued", sessionId });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const reply = await options.webTransport.chat({ sessionId, message });
|
|
62
|
+
res.json({ reply, sessionId });
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
65
|
+
res.status(500).json({ error: message });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
app.get("/api/jobs/:jobId", async (req, res) => {
|
|
69
|
+
if (!options.stores) {
|
|
70
|
+
res.status(503).json({ error: "Job status requires stores" });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const key = `job:${req.params.jobId}`;
|
|
74
|
+
const record = await options.stores.cache.get(key);
|
|
75
|
+
if (!record) {
|
|
76
|
+
res.status(404).json({ status: "pending" });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
res.json(record);
|
|
80
|
+
});
|
|
81
|
+
if (options.apiRouter) {
|
|
82
|
+
app.use("/api", options.apiRouter);
|
|
83
|
+
}
|
|
84
|
+
app.use("/webhooks", verifyWebhookSecret(secretEnv));
|
|
85
|
+
if (options.webhookRouter) {
|
|
86
|
+
app.use("/webhooks", options.webhookRouter);
|
|
87
|
+
} else {
|
|
88
|
+
app.post("/webhooks/ping", (_req, res) => {
|
|
89
|
+
res.json({ ok: true, message: "webhook secret verified" });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return app;
|
|
93
|
+
}
|
|
94
|
+
async function startThynkerServer(options) {
|
|
95
|
+
const app = createThynkerServer(options);
|
|
96
|
+
const port = options.config?.port ?? Number(process.env.API_PORT ?? 3001);
|
|
97
|
+
await new Promise((resolve) => {
|
|
98
|
+
app.listen(port, () => resolve());
|
|
99
|
+
});
|
|
100
|
+
console.log(`Thynker API listening on http://localhost:${port}`);
|
|
101
|
+
console.log(` POST /api/chat \u2014 web chat (inline or async)`);
|
|
102
|
+
console.log(` GET /api/jobs/:jobId \u2014 poll async results`);
|
|
103
|
+
console.log(` POST /webhooks/* \u2014 webhook routes (X-Webhook-Secret)`);
|
|
104
|
+
return {
|
|
105
|
+
app,
|
|
106
|
+
port,
|
|
107
|
+
close: async () => {
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export {
|
|
112
|
+
createThynkerServer,
|
|
113
|
+
startThynkerServer,
|
|
114
|
+
verifyWebhookSecret
|
|
115
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@thynker-labs/server",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Express API server with REST routes, webhooks, and web chat for Thynker runtime",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"cors": "2.8.5",
|
|
19
|
+
"express": "5.1.0",
|
|
20
|
+
"@thynker-labs/core": "0.0.1",
|
|
21
|
+
"@thynker-labs/transport-web": "0.0.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/cors": "2.8.19",
|
|
25
|
+
"@types/express": "5.0.3",
|
|
26
|
+
"supertest": "7.1.1",
|
|
27
|
+
"@types/supertest": "6.0.3"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "vitest run"
|
|
39
|
+
}
|
|
40
|
+
}
|