@pikku/bun-server 0.12.2 → 0.12.4
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/CHANGELOG.md +34 -0
- package/dist/pikku-bun-server.d.ts +19 -0
- package/dist/pikku-bun-server.js +95 -2
- package/dist/pikku-bun-server.js.map +1 -1
- package/package.json +9 -2
- package/src/pikku-bun-server.ts +131 -2
- package/src/static-mount.test.ts +108 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,39 @@
|
|
|
1
1
|
# @pikku/bun-server
|
|
2
2
|
|
|
3
|
+
## 0.12.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d4a2503: Serve the console same-origin at /console (#861). Both dev servers gain
|
|
8
|
+
`staticMounts` (prefix → directory static serving with SPA fallback and path
|
|
9
|
+
traversal protection); `pikku serve` / `pikku dev` mount the bundled console
|
|
10
|
+
app at `/console` on the API port whenever it is bundled, so auth cookies are
|
|
11
|
+
first-party and no `?server=` param is needed. The console is built with
|
|
12
|
+
`base: '/console/'` (its router already derives the basename from BASE_URL).
|
|
13
|
+
The separate `--console <port>` static server is removed; `pikku console`
|
|
14
|
+
serves the bundle under /console and redirects the root there.
|
|
15
|
+
- Updated dependencies [61c9ce9]
|
|
16
|
+
- Updated dependencies [f1f39f8]
|
|
17
|
+
- Updated dependencies [c45e98d]
|
|
18
|
+
- Updated dependencies [472a349]
|
|
19
|
+
- @pikku/core@0.12.52
|
|
20
|
+
|
|
21
|
+
## 0.12.3
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- 6f6abfe: Mount MCP on the bun runtime. `@pikku/bun-server` now accepts `mcpJson`/`mcpPath`
|
|
26
|
+
options and serves the MCP endpoint (default `/mcp`) via a new fetch-native
|
|
27
|
+
handler on `PikkuMCPServer.createFetchHandler()`, which uses the MCP SDK's
|
|
28
|
+
Web-Standard (`Request`→`Response`) streamable-HTTP transport — no `node:http`
|
|
29
|
+
req/res. The standalone `--runtime bun` entry now wires the same `mcpImport` +
|
|
30
|
+
`mcpJson` option the node entry already used, so a bun standalone build serves
|
|
31
|
+
`/mcp` with the project's tools/resources/prompts instead of silently dropping
|
|
32
|
+
them. `@pikku/modelcontextprotocol` is an optional peer dep of `@pikku/bun-server`
|
|
33
|
+
(only imported when `mcpJson` is non-empty).
|
|
34
|
+
- Updated dependencies [6f6abfe]
|
|
35
|
+
- @pikku/modelcontextprotocol@0.12.6
|
|
36
|
+
|
|
3
37
|
## 0.12.2
|
|
4
38
|
|
|
5
39
|
### Patch Changes
|
|
@@ -2,13 +2,25 @@ import type { CoreConfig } from '@pikku/core';
|
|
|
2
2
|
import type { Logger } from '@pikku/core/services';
|
|
3
3
|
import { type RunHTTPWiringOptions } from '@pikku/core/http';
|
|
4
4
|
import { BunEventHubService } from './bun-event-hub-service.js';
|
|
5
|
+
export type StaticMount = {
|
|
6
|
+
urlPrefix: string;
|
|
7
|
+
directory: string;
|
|
8
|
+
spaFallback?: boolean;
|
|
9
|
+
};
|
|
5
10
|
export type BunServerConfig = CoreConfig & {
|
|
6
11
|
port: number;
|
|
7
12
|
hostname?: string;
|
|
13
|
+
staticMounts?: StaticMount[];
|
|
8
14
|
healthCheckPath?: string;
|
|
9
15
|
};
|
|
10
16
|
export type PikkuBunServerOptions = RunHTTPWiringOptions & {
|
|
11
17
|
eventHub?: BunEventHubService;
|
|
18
|
+
mcpJson?: {
|
|
19
|
+
tools?: unknown[];
|
|
20
|
+
resources?: unknown[];
|
|
21
|
+
prompts?: unknown[];
|
|
22
|
+
};
|
|
23
|
+
mcpPath?: string;
|
|
12
24
|
};
|
|
13
25
|
export declare class PikkuBunServer {
|
|
14
26
|
private readonly config;
|
|
@@ -16,9 +28,16 @@ export declare class PikkuBunServer {
|
|
|
16
28
|
private server;
|
|
17
29
|
private readonly eventHub;
|
|
18
30
|
private readonly options;
|
|
31
|
+
private readonly mcpJson?;
|
|
32
|
+
private readonly mcpPath;
|
|
33
|
+
private mcpHandler?;
|
|
19
34
|
constructor(config: BunServerConfig, logger: Logger, options?: PikkuBunServerOptions);
|
|
20
35
|
init(): Promise<void>;
|
|
36
|
+
private initMCP;
|
|
21
37
|
start(): Promise<void>;
|
|
38
|
+
get port(): number;
|
|
39
|
+
private serveStaticMounts;
|
|
40
|
+
private resolveStaticFile;
|
|
22
41
|
stop(): Promise<void>;
|
|
23
42
|
enableExitOnSignals(): void;
|
|
24
43
|
}
|
package/dist/pikku-bun-server.js
CHANGED
|
@@ -3,6 +3,7 @@ import { fetchData, PikkuFetchHTTPRequest, PikkuFetchHTTPResponse, logRoutes as
|
|
|
3
3
|
import { logChannels } from '@pikku/core/channel';
|
|
4
4
|
import { runLocalChannel } from '@pikku/core/channel/local';
|
|
5
5
|
import { compileAllSchemas } from '@pikku/core/schema';
|
|
6
|
+
import { resolve } from 'node:path';
|
|
6
7
|
import { BunEventHubService } from './bun-event-hub-service.js';
|
|
7
8
|
const isSerializable = (data) => !(typeof data === 'string' ||
|
|
8
9
|
data instanceof ArrayBuffer ||
|
|
@@ -20,20 +21,54 @@ export class PikkuBunServer {
|
|
|
20
21
|
server = null;
|
|
21
22
|
eventHub;
|
|
22
23
|
options;
|
|
24
|
+
mcpJson;
|
|
25
|
+
mcpPath;
|
|
26
|
+
mcpHandler;
|
|
23
27
|
constructor(config, logger, options = {}) {
|
|
24
28
|
this.config = config;
|
|
25
29
|
this.logger = logger;
|
|
26
|
-
const { eventHub, ...httpOptions } = options;
|
|
30
|
+
const { eventHub, mcpJson, mcpPath, ...httpOptions } = options;
|
|
27
31
|
this.eventHub = eventHub ?? new BunEventHubService();
|
|
32
|
+
this.mcpJson = mcpJson;
|
|
33
|
+
this.mcpPath = mcpPath ?? '/mcp';
|
|
28
34
|
this.options = httpOptions;
|
|
29
35
|
}
|
|
30
36
|
async init() {
|
|
31
37
|
compileAllSchemas(this.logger);
|
|
32
38
|
logRegisterRoutes(this.logger);
|
|
33
39
|
logChannels(this.logger);
|
|
40
|
+
await this.initMCP();
|
|
41
|
+
}
|
|
42
|
+
async initMCP() {
|
|
43
|
+
const mcpJson = this.mcpJson;
|
|
44
|
+
if (!mcpJson)
|
|
45
|
+
return;
|
|
46
|
+
const { tools = [], resources = [], prompts = [] } = mcpJson;
|
|
47
|
+
if (tools.length + resources.length + prompts.length === 0)
|
|
48
|
+
return;
|
|
49
|
+
try {
|
|
50
|
+
const { PikkuMCPServer } = await import('@pikku/modelcontextprotocol');
|
|
51
|
+
const mcpServer = new PikkuMCPServer({
|
|
52
|
+
name: 'pikku',
|
|
53
|
+
version: '1.0.0',
|
|
54
|
+
mcpJSON: mcpJson,
|
|
55
|
+
capabilities: {
|
|
56
|
+
...(tools.length > 0 && { tools: {} }),
|
|
57
|
+
...(resources.length > 0 && { resources: {} }),
|
|
58
|
+
...(prompts.length > 0 && { prompts: {} }),
|
|
59
|
+
},
|
|
60
|
+
}, this.logger);
|
|
61
|
+
await mcpServer.init();
|
|
62
|
+
const { handler } = mcpServer.createFetchHandler({ path: this.mcpPath });
|
|
63
|
+
this.mcpHandler = handler;
|
|
64
|
+
this.logger.info(`pikku-bun-server: MCP mounted at ${this.mcpPath}`);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
this.logger.warn(`pikku-bun-server: MCP could not be mounted — ${err instanceof Error ? err.message : String(err)}`);
|
|
68
|
+
}
|
|
34
69
|
}
|
|
35
70
|
async start() {
|
|
36
|
-
const { config, logger, options, eventHub } = this;
|
|
71
|
+
const { config, logger, options, eventHub, mcpHandler, mcpPath } = this;
|
|
37
72
|
this.server = Bun.serve({
|
|
38
73
|
port: config.port,
|
|
39
74
|
hostname: config.hostname,
|
|
@@ -61,6 +96,16 @@ export class PikkuBunServer {
|
|
|
61
96
|
headers: { 'content-type': 'application/json' },
|
|
62
97
|
});
|
|
63
98
|
}
|
|
99
|
+
if (mcpHandler) {
|
|
100
|
+
const pathname = new URL(req.url).pathname;
|
|
101
|
+
if (pathname === mcpPath || pathname.startsWith(`${mcpPath}/`)) {
|
|
102
|
+
return await mcpHandler(req);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const staticResponse = await this.serveStaticMounts(req);
|
|
106
|
+
if (staticResponse) {
|
|
107
|
+
return staticResponse;
|
|
108
|
+
}
|
|
64
109
|
const pikkuReq = new PikkuFetchHTTPRequest(req);
|
|
65
110
|
const pikkuRes = new PikkuFetchHTTPResponse();
|
|
66
111
|
await fetchData(pikkuReq, pikkuRes, {
|
|
@@ -110,6 +155,54 @@ export class PikkuBunServer {
|
|
|
110
155
|
eventHub.setServer(this.server);
|
|
111
156
|
logger.info(`pikku-bun-server: listening on http://${config.hostname ?? 'localhost'}:${config.port}`);
|
|
112
157
|
}
|
|
158
|
+
get port() {
|
|
159
|
+
return this.server?.port ?? this.config.port;
|
|
160
|
+
}
|
|
161
|
+
async serveStaticMounts(req) {
|
|
162
|
+
const mounts = this.config.staticMounts;
|
|
163
|
+
if (!mounts?.length || (req.method !== 'GET' && req.method !== 'HEAD')) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const pathname = decodeURIComponent(new URL(req.url).pathname);
|
|
167
|
+
for (const mount of mounts) {
|
|
168
|
+
if (pathname !== mount.urlPrefix &&
|
|
169
|
+
!pathname.startsWith(`${mount.urlPrefix}/`)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const key = pathname.slice(mount.urlPrefix.length).replace(/^\/+/, '');
|
|
173
|
+
const file = await this.resolveStaticFile(mount, key);
|
|
174
|
+
if (file) {
|
|
175
|
+
return new Response(req.method === 'HEAD' ? null : file);
|
|
176
|
+
}
|
|
177
|
+
if (mount.spaFallback) {
|
|
178
|
+
const index = await this.resolveStaticFile(mount, 'index.html');
|
|
179
|
+
if (index) {
|
|
180
|
+
return new Response(req.method === 'HEAD' ? null : index);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return new Response('Not Found', { status: 404 });
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
async resolveStaticFile(mount, key) {
|
|
188
|
+
const directory = resolve(mount.directory);
|
|
189
|
+
const targetPath = key === '' ? resolve(directory, 'index.html') : resolve(directory, key);
|
|
190
|
+
if (targetPath !== directory && !targetPath.startsWith(`${directory}/`)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
let file = Bun.file(targetPath);
|
|
194
|
+
if (!(await file.exists())) {
|
|
195
|
+
const indexPath = resolve(targetPath, 'index.html');
|
|
196
|
+
if (!indexPath.startsWith(`${directory}/`)) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
file = Bun.file(indexPath);
|
|
200
|
+
if (!(await file.exists())) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return file;
|
|
205
|
+
}
|
|
113
206
|
async stop() {
|
|
114
207
|
await this.server?.stop();
|
|
115
208
|
this.server = null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pikku-bun-server.js","sourceRoot":"","sources":["../src/pikku-bun-server.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAEnD,OAAO,EACL,SAAS,EACT,qBAAqB,EACrB,sBAAsB,EACtB,SAAS,IAAI,iBAAiB,GAE/B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;
|
|
1
|
+
{"version":3,"file":"pikku-bun-server.js","sourceRoot":"","sources":["../src/pikku-bun-server.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAEnD,OAAO,EACL,SAAS,EACT,qBAAqB,EACrB,sBAAsB,EACtB,SAAS,IAAI,iBAAiB,GAE/B,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAEjD,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAA;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAA;AAEtD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAA;AAwC/D,MAAM,cAAc,GAAG,CAAC,IAAa,EAAW,EAAE,CAChD,CAAC,CACC,OAAO,IAAI,KAAK,QAAQ;IACxB,IAAI,YAAY,WAAW;IAC3B,IAAI,YAAY,UAAU;IAC1B,IAAI,YAAY,SAAS;IACzB,IAAI,YAAY,WAAW;IAC3B,IAAI,YAAY,UAAU;IAC1B,IAAI,YAAY,WAAW;IAC3B,IAAI,YAAY,UAAU;IAC1B,IAAI,YAAY,YAAY;IAC5B,IAAI,YAAY,YAAY,CAC7B,CAAA;AAQH,MAAM,OAAO,cAAc;IASN;IACA;IATX,MAAM,GAA6B,IAAI,CAAA;IAC9B,QAAQ,CAAoB;IAC5B,OAAO,CAAsB;IAC7B,OAAO,CAAmC;IAC1C,OAAO,CAAQ;IACxB,UAAU,CAA0C;IAE5D,YACmB,MAAuB,EACvB,MAAc,EAC/B,UAAiC,EAAE;QAFlB,WAAM,GAAN,MAAM,CAAiB;QACvB,WAAM,GAAN,MAAM,CAAQ;QAG/B,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,WAAW,EAAE,GAAG,OAAO,CAAA;QAC9D,IAAI,CAAC,QAAQ,GAAG,QAAQ,IAAI,IAAI,kBAAkB,EAAE,CAAA;QACpD,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,OAAO,GAAG,OAAO,IAAI,MAAM,CAAA;QAChC,IAAI,CAAC,OAAO,GAAG,WAAW,CAAA;IAC5B,CAAC;IAEM,KAAK,CAAC,IAAI;QACf,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC9B,iBAAiB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAC9B,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACxB,MAAM,IAAI,CAAC,OAAO,EAAE,CAAA;IACtB,CAAC;IAEO,KAAK,CAAC,OAAO;QACnB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAA;QAC5B,IAAI,CAAC,OAAO;YAAE,OAAM;QACpB,MAAM,EAAE,KAAK,GAAG,EAAE,EAAE,SAAS,GAAG,EAAE,EAAE,OAAO,GAAG,EAAE,EAAE,GAAG,OAAO,CAAA;QAC5D,IAAI,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QAClE,IAAI,CAAC;YACH,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,6BAA6B,CAAC,CAAA;YACtE,MAAM,SAAS,GAAG,IAAI,cAAc,CAClC;gBACE,IAAI,EAAE,OAAO;gBACb,OAAO,EAAE,OAAO;gBAChB,OAAO,EAAE,OAAO;gBAChB,YAAY,EAAE;oBACZ,GAAG,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;oBACtC,GAAG,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;oBAC9C,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;iBAC3C;aACF,EACD,IAAI,CAAC,MAAM,CACZ,CAAA;YACD,MAAM,SAAS,CAAC,IAAI,EAAE,CAAA;YACtB,MAAM,EAAE,OAAO,EAAE,GAAG,SAAS,CAAC,kBAAkB,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;YACxE,IAAI,CAAC,UAAU,GAAG,OAAO,CAAA;YACzB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,oCAAoC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAA;QACtE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,gDAAgD,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CACnG,CAAA;QACH,CAAC;IACH,CAAC;IAEM,KAAK,CAAC,KAAK;QAChB,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,IAAI,CAAA;QAEvE,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC,KAAK,CAAS;YAC9B,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YAEzB,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE;gBAC3B,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,CAAC;oBAC9D,MAAM,QAAQ,GAAG,IAAI,qBAAqB,CAAC,GAAG,CAAC,CAAA;oBAC/C,MAAM,QAAQ,GAAG,IAAI,sBAAsB,EAAE,CAAA;oBAC7C,MAAM,cAAc,GAAG,MAAM,eAAe,CAAC;wBAC3C,SAAS,EAAE,MAAM,CAAC,UAAU,EAAE;wBAC9B,OAAO,EAAE,QAAQ;wBACjB,QAAQ,EAAE,QAAQ;wBAClB,KAAK,EAAE,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ;qBACjC,CAAC,CAAA;oBACF,IAAI,CAAC,cAAc,EAAE,CAAC;wBACpB,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;oBACnD,CAAC;oBACD,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,EAAE,cAAc,EAAE,EAAE,CAAC,CAAA;oBAClE,IAAI,QAAQ;wBAAE,OAAO,SAAgC,CAAA;oBACrD,OAAO,IAAI,QAAQ,CAAC,0BAA0B,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;gBAClE,CAAC;gBAED,IACE,MAAM,CAAC,eAAe;oBACtB,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,KAAK,MAAM,CAAC,eAAe,EACpD,CAAC;oBACD,OAAO,IAAI,QAAQ,CAAC,aAAa,EAAE;wBACjC,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;qBAChD,CAAC,CAAA;gBACJ,CAAC;gBAED,IAAI,UAAU,EAAE,CAAC;oBACf,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAA;oBAC1C,IAAI,QAAQ,KAAK,OAAO,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,OAAO,GAAG,CAAC,EAAE,CAAC;wBAC/D,OAAO,MAAM,UAAU,CAAC,GAAG,CAAC,CAAA;oBAC9B,CAAC;gBACH,CAAC;gBAED,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAA;gBACxD,IAAI,cAAc,EAAE,CAAC;oBACnB,OAAO,cAAc,CAAA;gBACvB,CAAC;gBAED,MAAM,QAAQ,GAAG,IAAI,qBAAqB,CAAC,GAAG,CAAC,CAAA;gBAC/C,MAAM,QAAQ,GAAG,IAAI,sBAAsB,EAAE,CAAA;gBAC7C,MAAM,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE;oBAClC,cAAc,EAAE,IAAI;oBACpB,GAAG,OAAO;iBACX,CAAC,CAAA;gBACF,OAAO,QAAQ,CAAC,UAAU,EAAE,CAAA;YAC9B,CAAC;YAED,SAAS,EAAE;gBACT,IAAI,EAAE,CAAC,EAA2B,EAAE,EAAE;oBACpC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAA;oBAClC,cAAc,CAAC,cAAc,CAAC,CAAC,IAAI,EAAE,EAAE;wBACrC,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,IAAY,CAAC,CAAA;oBACtE,CAAC,CAAC,CAAA;oBACF,cAAc,CAAC,oBAAoB,CAAC,CAAC,IAAI,EAAE,EAAE;wBAC3C,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;oBACrB,CAAC,CAAC,CAAA;oBACF,cAAc,CAAC,eAAe,CAAC,GAAG,EAAE;wBAClC,EAAE,CAAC,KAAK,EAAE,CAAA;oBACZ,CAAC,CAAC,CAAA;oBACF,QAAQ,CAAC,eAAe,CAAC,cAAc,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;oBACtD,cAAc,CAAC,IAAI,EAAE,CAAA;gBACvB,CAAC;gBAED,OAAO,EAAE,KAAK,EAAE,EAA2B,EAAE,OAAO,EAAE,EAAE;oBACtD,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAA;oBAClC,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE,CAAC;wBAChC,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;wBACpD,IAAI,MAAM;4BAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAA;oBAC7C,CAAC;yBAAM,CAAC;wBACN,MAAM,KAAK,GACT,OAAO,YAAY,WAAW;4BAC5B,CAAC,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC;4BACzB,CAAC,CAAC,IAAI,UAAU,CACZ,OAAO,CAAC,MAAM,EACd,OAAO,CAAC,UAAU,EAClB,OAAO,CAAC,UAAU,CACnB,CAAA;wBACP,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,aAAa,CAAC,KAAK,CAAC,CAAA;wBACxD,IAAI,MAAM;4BAAE,cAAc,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;oBAC/C,CAAC;gBACH,CAAC;gBAED,KAAK,EAAE,CAAC,EAA2B,EAAE,EAAE;oBACrC,MAAM,EAAE,cAAc,EAAE,GAAG,EAAE,CAAC,IAAI,CAAA;oBAClC,QAAQ,CAAC,eAAe,CAAC,cAAc,CAAC,SAAS,CAAC,CAAA;oBAClD,cAAc,CAAC,KAAK,EAAE,CAAA;gBACxB,CAAC;aACF;SACF,CAAC,CAAA;QAEF,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,MAA4B,CAAC,CAAA;QACrD,MAAM,CAAC,IAAI,CACT,yCAAyC,MAAM,CAAC,QAAQ,IAAI,WAAW,IAAI,MAAM,CAAC,IAAI,EAAE,CACzF,CAAA;IACH,CAAC;IAED,IAAW,IAAI;QACb,OAAO,IAAI,CAAC,MAAM,EAAE,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,CAAA;IAC9C,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAAC,GAAY;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,YAAY,CAAA;QACvC,IAAI,CAAC,MAAM,EAAE,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,EAAE,CAAC;YACvE,OAAO,IAAI,CAAA;QACb,CAAC;QACD,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,CAAA;QAE9D,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IACE,QAAQ,KAAK,KAAK,CAAC,SAAS;gBAC5B,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,KAAK,CAAC,SAAS,GAAG,CAAC,EAC3C,CAAC;gBACD,SAAQ;YACV,CAAC;YACD,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;YACtE,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,GAAG,CAAC,CAAA;YACrD,IAAI,IAAI,EAAE,CAAC;gBACT,OAAO,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;YAC1D,CAAC;YACD,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;gBACtB,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,YAAY,CAAC,CAAA;gBAC/D,IAAI,KAAK,EAAE,CAAC;oBACV,OAAO,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;gBAC3D,CAAC;YACH,CAAC;YACD,OAAO,IAAI,QAAQ,CAAC,WAAW,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAA;QACnD,CAAC;QAED,OAAO,IAAI,CAAA;IACb,CAAC;IAEO,KAAK,CAAC,iBAAiB,CAC7B,KAAkB,EAClB,GAAW;QAEX,MAAM,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;QAC1C,MAAM,UAAU,GACd,GAAG,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAA;QACzE,IAAI,UAAU,KAAK,SAAS,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,GAAG,SAAS,GAAG,CAAC,EAAE,CAAC;YACxE,OAAO,IAAI,CAAA;QACb,CAAC;QACD,IAAI,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QAC/B,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;YAC3B,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;YACnD,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,GAAG,SAAS,GAAG,CAAC,EAAE,CAAC;gBAC3C,OAAO,IAAI,CAAA;YACb,CAAC;YACD,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YAC1B,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;gBAC3B,OAAO,IAAI,CAAA;YACb,CAAC;QACH,CAAC;QACD,OAAO,IAAI,CAAA;IACb,CAAC;IAEM,KAAK,CAAC,IAAI;QACf,MAAM,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,CAAA;QACzB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAA;IACpB,CAAC;IAEM,mBAAmB;QACxB,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAc,EAAE,EAAE;YACxC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,qBAAqB,MAAM,qBAAqB,CAAC,CAAA;YAClE,IAAI,CAAC;gBACH,MAAM,qBAAqB,EAAE,CAAA;gBAC7B,MAAM,IAAI,CAAC,IAAI,EAAE,CAAA;YACnB,CAAC;oBAAS,CAAC;gBACT,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACjB,CAAC;QACH,CAAC,CAAA;QACD,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAA;QAChD,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAA;IACpD,CAAC;CACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/bun-server",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.4",
|
|
4
4
|
"description": "Pikku server adapter for Bun (Bun.serve)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "yasser.fadl@gmail.com",
|
|
@@ -16,9 +16,16 @@
|
|
|
16
16
|
"prepublishOnly": "bun run build"
|
|
17
17
|
},
|
|
18
18
|
"peerDependencies": {
|
|
19
|
-
"@pikku/core": "^0.12.
|
|
19
|
+
"@pikku/core": "^0.12.52",
|
|
20
|
+
"@pikku/modelcontextprotocol": "^0.12.6"
|
|
21
|
+
},
|
|
22
|
+
"peerDependenciesMeta": {
|
|
23
|
+
"@pikku/modelcontextprotocol": {
|
|
24
|
+
"optional": true
|
|
25
|
+
}
|
|
20
26
|
},
|
|
21
27
|
"devDependencies": {
|
|
28
|
+
"@pikku/modelcontextprotocol": "^0.12.6",
|
|
22
29
|
"@types/bun": "latest",
|
|
23
30
|
"typescript": "^6.0.3"
|
|
24
31
|
},
|
package/src/pikku-bun-server.ts
CHANGED
|
@@ -15,11 +15,20 @@ import type { PikkuLocalChannelHandler } from '@pikku/core/channel/local'
|
|
|
15
15
|
import { runLocalChannel } from '@pikku/core/channel/local'
|
|
16
16
|
import { compileAllSchemas } from '@pikku/core/schema'
|
|
17
17
|
|
|
18
|
+
import { resolve } from 'node:path'
|
|
19
|
+
|
|
18
20
|
import { BunEventHubService } from './bun-event-hub-service.js'
|
|
19
21
|
|
|
22
|
+
export type StaticMount = {
|
|
23
|
+
urlPrefix: string
|
|
24
|
+
directory: string
|
|
25
|
+
spaFallback?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
export type BunServerConfig = CoreConfig & {
|
|
21
29
|
port: number
|
|
22
30
|
hostname?: string
|
|
31
|
+
staticMounts?: StaticMount[]
|
|
23
32
|
healthCheckPath?: string
|
|
24
33
|
}
|
|
25
34
|
|
|
@@ -32,6 +41,18 @@ export type PikkuBunServerOptions = RunHTTPWiringOptions & {
|
|
|
32
41
|
* connected clients. Defaults to a fresh `BunEventHubService`.
|
|
33
42
|
*/
|
|
34
43
|
eventHub?: BunEventHubService
|
|
44
|
+
/**
|
|
45
|
+
* Parsed content of `.pikku/mcp/mcp.gen.json`. When provided and non-empty,
|
|
46
|
+
* `@pikku/modelcontextprotocol` is dynamically imported and the MCP server is
|
|
47
|
+
* mounted at `mcpPath` via the SDK's fetch-native (Web Standard) transport.
|
|
48
|
+
* Import the JSON statically so bundlers (esbuild) inline it — no file read.
|
|
49
|
+
* Mirrors `PikkuNodeHTTPServerOptions.mcpJson`.
|
|
50
|
+
*/
|
|
51
|
+
mcpJson?: { tools?: unknown[]; resources?: unknown[]; prompts?: unknown[] }
|
|
52
|
+
/**
|
|
53
|
+
* Path the MCP server is mounted at when `mcpJson` is provided. Default `/mcp`.
|
|
54
|
+
*/
|
|
55
|
+
mcpPath?: string
|
|
35
56
|
}
|
|
36
57
|
|
|
37
58
|
type WsData = { channelHandler: PikkuLocalChannelHandler }
|
|
@@ -60,14 +81,19 @@ export class PikkuBunServer {
|
|
|
60
81
|
private server: BunServer<WsData> | null = null
|
|
61
82
|
private readonly eventHub: BunEventHubService
|
|
62
83
|
private readonly options: RunHTTPWiringOptions
|
|
84
|
+
private readonly mcpJson?: PikkuBunServerOptions['mcpJson']
|
|
85
|
+
private readonly mcpPath: string
|
|
86
|
+
private mcpHandler?: (request: Request) => Promise<Response>
|
|
63
87
|
|
|
64
88
|
constructor(
|
|
65
89
|
private readonly config: BunServerConfig,
|
|
66
90
|
private readonly logger: Logger,
|
|
67
91
|
options: PikkuBunServerOptions = {}
|
|
68
92
|
) {
|
|
69
|
-
const { eventHub, ...httpOptions } = options
|
|
93
|
+
const { eventHub, mcpJson, mcpPath, ...httpOptions } = options
|
|
70
94
|
this.eventHub = eventHub ?? new BunEventHubService()
|
|
95
|
+
this.mcpJson = mcpJson
|
|
96
|
+
this.mcpPath = mcpPath ?? '/mcp'
|
|
71
97
|
this.options = httpOptions
|
|
72
98
|
}
|
|
73
99
|
|
|
@@ -75,10 +101,42 @@ export class PikkuBunServer {
|
|
|
75
101
|
compileAllSchemas(this.logger)
|
|
76
102
|
logRegisterRoutes(this.logger)
|
|
77
103
|
logChannels(this.logger)
|
|
104
|
+
await this.initMCP()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async initMCP(): Promise<void> {
|
|
108
|
+
const mcpJson = this.mcpJson
|
|
109
|
+
if (!mcpJson) return
|
|
110
|
+
const { tools = [], resources = [], prompts = [] } = mcpJson
|
|
111
|
+
if (tools.length + resources.length + prompts.length === 0) return
|
|
112
|
+
try {
|
|
113
|
+
const { PikkuMCPServer } = await import('@pikku/modelcontextprotocol')
|
|
114
|
+
const mcpServer = new PikkuMCPServer(
|
|
115
|
+
{
|
|
116
|
+
name: 'pikku',
|
|
117
|
+
version: '1.0.0',
|
|
118
|
+
mcpJSON: mcpJson,
|
|
119
|
+
capabilities: {
|
|
120
|
+
...(tools.length > 0 && { tools: {} }),
|
|
121
|
+
...(resources.length > 0 && { resources: {} }),
|
|
122
|
+
...(prompts.length > 0 && { prompts: {} }),
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
this.logger
|
|
126
|
+
)
|
|
127
|
+
await mcpServer.init()
|
|
128
|
+
const { handler } = mcpServer.createFetchHandler({ path: this.mcpPath })
|
|
129
|
+
this.mcpHandler = handler
|
|
130
|
+
this.logger.info(`pikku-bun-server: MCP mounted at ${this.mcpPath}`)
|
|
131
|
+
} catch (err) {
|
|
132
|
+
this.logger.warn(
|
|
133
|
+
`pikku-bun-server: MCP could not be mounted — ${err instanceof Error ? err.message : String(err)}`
|
|
134
|
+
)
|
|
135
|
+
}
|
|
78
136
|
}
|
|
79
137
|
|
|
80
138
|
public async start(): Promise<void> {
|
|
81
|
-
const { config, logger, options, eventHub } = this
|
|
139
|
+
const { config, logger, options, eventHub, mcpHandler, mcpPath } = this
|
|
82
140
|
|
|
83
141
|
this.server = Bun.serve<WsData>({
|
|
84
142
|
port: config.port,
|
|
@@ -111,6 +169,18 @@ export class PikkuBunServer {
|
|
|
111
169
|
})
|
|
112
170
|
}
|
|
113
171
|
|
|
172
|
+
if (mcpHandler) {
|
|
173
|
+
const pathname = new URL(req.url).pathname
|
|
174
|
+
if (pathname === mcpPath || pathname.startsWith(`${mcpPath}/`)) {
|
|
175
|
+
return await mcpHandler(req)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const staticResponse = await this.serveStaticMounts(req)
|
|
180
|
+
if (staticResponse) {
|
|
181
|
+
return staticResponse
|
|
182
|
+
}
|
|
183
|
+
|
|
114
184
|
const pikkuReq = new PikkuFetchHTTPRequest(req)
|
|
115
185
|
const pikkuRes = new PikkuFetchHTTPResponse()
|
|
116
186
|
await fetchData(pikkuReq, pikkuRes, {
|
|
@@ -169,6 +239,65 @@ export class PikkuBunServer {
|
|
|
169
239
|
)
|
|
170
240
|
}
|
|
171
241
|
|
|
242
|
+
public get port(): number {
|
|
243
|
+
return this.server?.port ?? this.config.port
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private async serveStaticMounts(req: Request): Promise<Response | null> {
|
|
247
|
+
const mounts = this.config.staticMounts
|
|
248
|
+
if (!mounts?.length || (req.method !== 'GET' && req.method !== 'HEAD')) {
|
|
249
|
+
return null
|
|
250
|
+
}
|
|
251
|
+
const pathname = decodeURIComponent(new URL(req.url).pathname)
|
|
252
|
+
|
|
253
|
+
for (const mount of mounts) {
|
|
254
|
+
if (
|
|
255
|
+
pathname !== mount.urlPrefix &&
|
|
256
|
+
!pathname.startsWith(`${mount.urlPrefix}/`)
|
|
257
|
+
) {
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
const key = pathname.slice(mount.urlPrefix.length).replace(/^\/+/, '')
|
|
261
|
+
const file = await this.resolveStaticFile(mount, key)
|
|
262
|
+
if (file) {
|
|
263
|
+
return new Response(req.method === 'HEAD' ? null : file)
|
|
264
|
+
}
|
|
265
|
+
if (mount.spaFallback) {
|
|
266
|
+
const index = await this.resolveStaticFile(mount, 'index.html')
|
|
267
|
+
if (index) {
|
|
268
|
+
return new Response(req.method === 'HEAD' ? null : index)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return new Response('Not Found', { status: 404 })
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return null
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private async resolveStaticFile(
|
|
278
|
+
mount: StaticMount,
|
|
279
|
+
key: string
|
|
280
|
+
): Promise<ReturnType<typeof Bun.file> | null> {
|
|
281
|
+
const directory = resolve(mount.directory)
|
|
282
|
+
const targetPath =
|
|
283
|
+
key === '' ? resolve(directory, 'index.html') : resolve(directory, key)
|
|
284
|
+
if (targetPath !== directory && !targetPath.startsWith(`${directory}/`)) {
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
287
|
+
let file = Bun.file(targetPath)
|
|
288
|
+
if (!(await file.exists())) {
|
|
289
|
+
const indexPath = resolve(targetPath, 'index.html')
|
|
290
|
+
if (!indexPath.startsWith(`${directory}/`)) {
|
|
291
|
+
return null
|
|
292
|
+
}
|
|
293
|
+
file = Bun.file(indexPath)
|
|
294
|
+
if (!(await file.exists())) {
|
|
295
|
+
return null
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return file
|
|
299
|
+
}
|
|
300
|
+
|
|
172
301
|
public async stop(): Promise<void> {
|
|
173
302
|
await this.server?.stop()
|
|
174
303
|
this.server = null
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, test, before, after } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { setSingletonServices } from '@pikku/core'
|
|
7
|
+
import { resetPikkuState } from '@pikku/core/internal'
|
|
8
|
+
import type { Logger } from '@pikku/core/services'
|
|
9
|
+
import { PikkuBunServer } from './pikku-bun-server.js'
|
|
10
|
+
|
|
11
|
+
const noopLogger = {
|
|
12
|
+
info: () => {},
|
|
13
|
+
error: () => {},
|
|
14
|
+
warn: () => {},
|
|
15
|
+
debug: () => {},
|
|
16
|
+
trace: () => {},
|
|
17
|
+
setLevel: () => {},
|
|
18
|
+
} as unknown as Logger
|
|
19
|
+
|
|
20
|
+
describe('PikkuBunServer static mounts', () => {
|
|
21
|
+
let server: PikkuBunServer
|
|
22
|
+
let tmpDir: string
|
|
23
|
+
let origin: string
|
|
24
|
+
|
|
25
|
+
before(async () => {
|
|
26
|
+
resetPikkuState()
|
|
27
|
+
setSingletonServices({
|
|
28
|
+
logger: noopLogger,
|
|
29
|
+
schema: {
|
|
30
|
+
compileSchema: () => {},
|
|
31
|
+
getSchemaNames: () => new Set<string>(),
|
|
32
|
+
},
|
|
33
|
+
} as any)
|
|
34
|
+
tmpDir = mkdtempSync(join(tmpdir(), 'pikku-bun-static-'))
|
|
35
|
+
writeFileSync(
|
|
36
|
+
join(tmpDir, 'index.html'),
|
|
37
|
+
'<!doctype html><title>console</title>'
|
|
38
|
+
)
|
|
39
|
+
mkdirSync(join(tmpDir, 'assets'))
|
|
40
|
+
writeFileSync(join(tmpDir, 'assets', 'app.js'), 'console.log("app")')
|
|
41
|
+
|
|
42
|
+
server = new PikkuBunServer(
|
|
43
|
+
{
|
|
44
|
+
port: 0,
|
|
45
|
+
hostname: '127.0.0.1',
|
|
46
|
+
staticMounts: [
|
|
47
|
+
{ urlPrefix: '/console', directory: tmpDir, spaFallback: true },
|
|
48
|
+
],
|
|
49
|
+
} as any,
|
|
50
|
+
noopLogger
|
|
51
|
+
)
|
|
52
|
+
await server.init()
|
|
53
|
+
await server.start()
|
|
54
|
+
origin = `http://127.0.0.1:${server.port}`
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
after(async () => {
|
|
58
|
+
await server.stop()
|
|
59
|
+
rmSync(tmpDir, { recursive: true, force: true })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('serves files under the prefix with a content type', async () => {
|
|
63
|
+
const response = await fetch(`${origin}/console/assets/app.js`)
|
|
64
|
+
assert.equal(response.status, 200)
|
|
65
|
+
assert.match(response.headers.get('content-type') ?? '', /javascript/)
|
|
66
|
+
assert.equal(await response.text(), 'console.log("app")')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('serves index.html at the mount root, with and without trailing slash', async () => {
|
|
70
|
+
for (const path of ['/console', '/console/']) {
|
|
71
|
+
const response = await fetch(`${origin}${path}`)
|
|
72
|
+
assert.equal(response.status, 200)
|
|
73
|
+
assert.match(await response.text(), /<title>console<\/title>/)
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('SPA fallback serves index.html for unknown paths under the prefix', async () => {
|
|
78
|
+
const response = await fetch(`${origin}/console/tests/userflows?id=x`)
|
|
79
|
+
assert.equal(response.status, 200)
|
|
80
|
+
assert.match(await response.text(), /<title>console<\/title>/)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('does not intercept paths outside the prefix', async () => {
|
|
84
|
+
const response = await fetch(`${origin}/consolerelated`)
|
|
85
|
+
assert.equal(response.status, 404)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('does not intercept non-GET methods', async () => {
|
|
89
|
+
const response = await fetch(`${origin}/console/assets/app.js`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
})
|
|
92
|
+
assert.equal(response.status, 404)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('blocks path traversal out of the mount directory', async () => {
|
|
96
|
+
writeFileSync(join(tmpDir, '..', 'pikku-bun-static-secret.txt'), 'secret')
|
|
97
|
+
try {
|
|
98
|
+
const response = await fetch(
|
|
99
|
+
`${origin}/console/%2e%2e/pikku-bun-static-secret.txt`
|
|
100
|
+
)
|
|
101
|
+
assert.notEqual(response.status, 200)
|
|
102
|
+
} finally {
|
|
103
|
+
rmSync(join(tmpDir, '..', 'pikku-bun-static-secret.txt'), {
|
|
104
|
+
force: true,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
})
|