@decocms/runtime 1.0.0-alpha.5 → 1.0.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/config-schema.json +19 -8
- package/package.json +11 -17
- package/src/asset-server/index.test.ts +306 -0
- package/src/asset-server/index.ts +217 -34
- package/src/bindings/binder.ts +2 -5
- package/src/bindings/index.ts +0 -33
- package/src/bindings/language-model/utils.ts +0 -91
- package/src/bindings.ts +146 -139
- package/src/client.ts +1 -145
- package/src/cors.ts +140 -0
- package/src/events.ts +472 -0
- package/src/index.ts +206 -202
- package/src/mcp.ts +7 -166
- package/src/oauth.ts +495 -0
- package/src/proxy.ts +1 -208
- package/src/state.ts +3 -31
- package/src/tools.ts +645 -0
- package/src/wrangler.ts +6 -5
- package/tsconfig.json +1 -1
- package/src/admin.ts +0 -16
- package/src/auth.ts +0 -233
- package/src/bindings/deconfig/helpers.ts +0 -107
- package/src/bindings/deconfig/index.ts +0 -1
- package/src/bindings/deconfig/resources.ts +0 -689
- package/src/bindings/deconfig/types.ts +0 -106
- package/src/bindings/language-model/ai-sdk.ts +0 -90
- package/src/bindings/language-model/index.ts +0 -4
- package/src/bindings/resources/bindings.ts +0 -99
- package/src/bindings/resources/helpers.ts +0 -95
- package/src/bindings/resources/schemas.ts +0 -265
- package/src/bindings/views.ts +0 -14
- package/src/drizzle.ts +0 -201
- package/src/http-client-transport.ts +0 -1
- package/src/mastra.ts +0 -670
- package/src/mcp-client.ts +0 -139
- package/src/resources.ts +0 -168
- package/src/views.ts +0 -26
- package/src/well-known.ts +0 -20
|
@@ -1,44 +1,227 @@
|
|
|
1
|
-
import { serveStatic } from "hono/bun";
|
|
2
|
-
import { Hono } from "hono";
|
|
3
1
|
import { devServerProxy } from "./dev-server-proxy";
|
|
4
|
-
import {
|
|
2
|
+
import { resolve, dirname } from "path";
|
|
5
3
|
|
|
6
|
-
interface AssetServerConfig {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
export interface AssetServerConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Environment mode. Determines whether to proxy to dev server or serve static files.
|
|
7
|
+
* @default process.env.NODE_ENV || "development"
|
|
8
|
+
*/
|
|
9
|
+
env?: "development" | "production" | "test";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* URL of the Vite dev server for development mode.
|
|
13
|
+
* @default "http://localhost:4000"
|
|
14
|
+
*/
|
|
15
|
+
devServerUrl?: string | URL;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Directory containing the built client assets.
|
|
19
|
+
* For production bundles, use `resolveClientDir()` to get the correct path.
|
|
20
|
+
* @default "./dist/client"
|
|
21
|
+
*/
|
|
22
|
+
clientDir?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Function to check if a path should be handled by the API server.
|
|
26
|
+
* Return true for API routes, false for static files.
|
|
27
|
+
* If not provided, defaults to serving everything as static.
|
|
28
|
+
*/
|
|
29
|
+
isServerPath?: (path: string) => boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const DEFAULT_DEV_SERVER_URL = "http://localhost:4000";
|
|
33
|
+
const DEFAULT_CLIENT_DIR = "./dist/client";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if a resolved file path is safely within the allowed base directory.
|
|
37
|
+
* Prevents path traversal attacks (e.g., /../../../etc/passwd).
|
|
38
|
+
*
|
|
39
|
+
* @param filePath - The resolved absolute file path
|
|
40
|
+
* @param baseDir - The base directory that files must be within
|
|
41
|
+
* @returns true if the path is safe, false if it's a traversal attempt
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* isPathWithinDirectory("/app/client/style.css", "/app/client") // true
|
|
46
|
+
* isPathWithinDirectory("/etc/passwd", "/app/client") // false
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export function isPathWithinDirectory(
|
|
50
|
+
filePath: string,
|
|
51
|
+
baseDir: string,
|
|
52
|
+
): boolean {
|
|
53
|
+
const resolvedBase = resolve(baseDir);
|
|
54
|
+
const resolvedPath = resolve(filePath);
|
|
55
|
+
return (
|
|
56
|
+
resolvedPath === resolvedBase || resolvedPath.startsWith(resolvedBase + "/")
|
|
57
|
+
);
|
|
10
58
|
}
|
|
11
59
|
|
|
12
|
-
|
|
13
|
-
|
|
60
|
+
export interface ResolveAssetPathOptions {
|
|
61
|
+
/** The decoded URL pathname (e.g., "/assets/style.css") */
|
|
62
|
+
requestPath: string;
|
|
63
|
+
/** The base directory containing static files */
|
|
64
|
+
clientDir: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a URL pathname to a file path, with path traversal protection.
|
|
69
|
+
* Returns null if the path is a traversal attempt.
|
|
70
|
+
*
|
|
71
|
+
* @returns File path, or null if unsafe
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* resolveAssetPathWithTraversalCheck({ requestPath: "/style.css", clientDir: "/app/client" })
|
|
76
|
+
* // "/app/client/style.css"
|
|
77
|
+
*
|
|
78
|
+
* resolveAssetPathWithTraversalCheck({ requestPath: "/../../../etc/passwd", clientDir: "/app/client" })
|
|
79
|
+
* // null (blocked)
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function resolveAssetPathWithTraversalCheck({
|
|
83
|
+
requestPath,
|
|
84
|
+
clientDir,
|
|
85
|
+
}: ResolveAssetPathOptions): string | null {
|
|
86
|
+
const relativePath = requestPath.startsWith("/")
|
|
87
|
+
? requestPath.slice(1)
|
|
88
|
+
: requestPath;
|
|
89
|
+
const filePath = resolve(clientDir, relativePath);
|
|
90
|
+
|
|
91
|
+
// Security: block path traversal attempts
|
|
92
|
+
if (!isPathWithinDirectory(filePath, clientDir)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return filePath;
|
|
97
|
+
}
|
|
14
98
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the client directory path relative to the running script.
|
|
101
|
+
* Use this for production bundles where the script location differs from CWD.
|
|
102
|
+
*
|
|
103
|
+
* @param importMetaUrl - Pass `import.meta.url` from the calling module
|
|
104
|
+
* @param relativePath - Path relative to the script (default: "../client")
|
|
105
|
+
* @returns Absolute path to the client directory
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* const clientDir = resolveClientDir(import.meta.url, "../client");
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export function resolveClientDir(
|
|
113
|
+
importMetaUrl: string,
|
|
114
|
+
relativePath = "../client",
|
|
115
|
+
): string {
|
|
116
|
+
const scriptUrl = new URL(importMetaUrl);
|
|
117
|
+
const scriptDir = dirname(scriptUrl.pathname);
|
|
118
|
+
return resolve(scriptDir, relativePath);
|
|
18
119
|
}
|
|
19
120
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
121
|
+
/**
|
|
122
|
+
* TODO(@camudo): make "modes" so we can for example try to serve the asset then fallback to api on miss
|
|
123
|
+
* or try to serve the api call then fallback to asset or index.html on 404
|
|
124
|
+
*/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create an asset handler that works with Bun.serve.
|
|
128
|
+
*
|
|
129
|
+
* In development: Proxies requests to Vite dev server
|
|
130
|
+
* In production: Serves static files with SPA fallback
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```ts
|
|
134
|
+
* const handleAssets = createAssetHandler({
|
|
135
|
+
* env: process.env.NODE_ENV as "development" | "production",
|
|
136
|
+
* clientDir: resolveClientDir(import.meta.url),
|
|
137
|
+
* isServerPath: (path) => path.startsWith("/api/") || path.startsWith("/mcp/"),
|
|
138
|
+
* });
|
|
139
|
+
*
|
|
140
|
+
* Bun.serve({
|
|
141
|
+
* fetch: async (request) => {
|
|
142
|
+
* return await handleAssets(request) ?? app.fetch(request);
|
|
143
|
+
* },
|
|
144
|
+
* });
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
export function createAssetHandler(config: AssetServerConfig = {}) {
|
|
148
|
+
const {
|
|
149
|
+
env = (process.env.NODE_ENV as "development" | "production" | "test") ||
|
|
150
|
+
"development",
|
|
151
|
+
devServerUrl = DEFAULT_DEV_SERVER_URL,
|
|
152
|
+
clientDir = DEFAULT_CLIENT_DIR,
|
|
153
|
+
isServerPath = () => false,
|
|
154
|
+
} = config;
|
|
155
|
+
|
|
156
|
+
// Development: Create a proxy handler
|
|
157
|
+
if (env === "development") {
|
|
158
|
+
const proxyHandler = devServerProxy(devServerUrl);
|
|
159
|
+
|
|
160
|
+
return async function handleAssets(
|
|
161
|
+
request: Request,
|
|
162
|
+
): Promise<Response | null> {
|
|
163
|
+
// In dev, proxy everything except server paths
|
|
164
|
+
const url = new URL(request.url);
|
|
165
|
+
if (isServerPath(url.pathname)) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Create a minimal Hono context for the proxy
|
|
170
|
+
const fakeContext = {
|
|
171
|
+
req: { raw: request, url: request.url },
|
|
172
|
+
};
|
|
173
|
+
return proxyHandler(fakeContext as any);
|
|
174
|
+
};
|
|
34
175
|
}
|
|
35
|
-
};
|
|
36
176
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
177
|
+
// Production: Serve static files
|
|
178
|
+
return async function handleAssets(
|
|
179
|
+
request: Request,
|
|
180
|
+
): Promise<Response | null> {
|
|
181
|
+
// Only handle GET requests
|
|
182
|
+
if (request.method !== "GET") {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const requestUrl = new URL(request.url);
|
|
42
187
|
|
|
43
|
-
|
|
44
|
-
|
|
188
|
+
// Decode the pathname to handle URL-encoded characters (e.g., %20 -> space)
|
|
189
|
+
// decodeURIComponent can throw URIError for malformed sequences (e.g., %E0%A4%A)
|
|
190
|
+
let path: string;
|
|
191
|
+
try {
|
|
192
|
+
path = decodeURIComponent(requestUrl.pathname);
|
|
193
|
+
} catch {
|
|
194
|
+
// Malformed URL encoding - return null to let API server handle or return 400
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Let API server handle its routes
|
|
199
|
+
if (isServerPath(path)) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Resolve path with traversal check
|
|
204
|
+
const filePath = resolveAssetPathWithTraversalCheck({
|
|
205
|
+
requestPath: path,
|
|
206
|
+
clientDir,
|
|
207
|
+
});
|
|
208
|
+
if (!filePath) {
|
|
209
|
+
return null; // Path traversal attempt blocked
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Try to serve the requested file, fall back to index.html for SPA routing
|
|
213
|
+
const indexPath = resolve(clientDir, "index.html");
|
|
214
|
+
for (const pathToTry of [filePath, indexPath]) {
|
|
215
|
+
try {
|
|
216
|
+
const file = Bun.file(pathToTry);
|
|
217
|
+
if (await file.exists()) {
|
|
218
|
+
return new Response(file);
|
|
219
|
+
}
|
|
220
|
+
} catch {
|
|
221
|
+
// Continue to next path
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return null;
|
|
226
|
+
};
|
|
227
|
+
}
|
package/src/bindings/binder.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/* oxlint-disable no-explicit-any */
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import type { MCPConnection } from "../connection.ts";
|
|
4
|
-
import { createPrivateTool, createStreamableTool } from "../mastra.ts";
|
|
5
4
|
import {
|
|
6
5
|
createMCPFetchStub,
|
|
7
6
|
type MCPClientFetchStub,
|
|
8
7
|
type ToolBinder,
|
|
9
8
|
} from "../mcp.ts";
|
|
9
|
+
import { createPrivateTool, createStreamableTool } from "../tools.ts";
|
|
10
10
|
import { CHANNEL_BINDING } from "./channels.ts";
|
|
11
|
-
import { VIEW_BINDING } from "./views.ts";
|
|
12
11
|
|
|
13
12
|
// ToolLike is a simplified version of the Tool interface that matches what we need for bindings
|
|
14
13
|
export interface ToolLike<
|
|
@@ -89,12 +88,10 @@ export const bindingClient = <TDefinition extends readonly ToolBinder[]>(
|
|
|
89
88
|
};
|
|
90
89
|
};
|
|
91
90
|
|
|
92
|
-
export type MCPBindingClient<T extends ReturnType<typeof bindingClient
|
|
91
|
+
export type MCPBindingClient<T extends ReturnType<typeof bindingClient<any>>> =
|
|
93
92
|
ReturnType<T["forConnection"]>;
|
|
94
93
|
|
|
95
94
|
export const ChannelBinding = bindingClient(CHANNEL_BINDING);
|
|
96
|
-
export const ViewBinding = bindingClient(VIEW_BINDING);
|
|
97
|
-
|
|
98
95
|
export type { Callbacks } from "./channels.ts";
|
|
99
96
|
|
|
100
97
|
export const impl = <TBinder extends Binder>(
|
package/src/bindings/index.ts
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
import { CHANNEL_BINDING } from "./channels.ts";
|
|
2
|
-
import { VIEW_BINDING as VIEWS_BINDING } from "./views.ts";
|
|
3
2
|
|
|
4
3
|
// Import new Resources 2.0 bindings function
|
|
5
4
|
import { LANGUAGE_MODEL_BINDING } from "@decocms/bindings/llm";
|
|
6
|
-
import { createResourceBindings } from "./resources/bindings.ts";
|
|
7
5
|
|
|
8
6
|
// Export types and utilities from binder
|
|
9
7
|
export {
|
|
10
8
|
bindingClient,
|
|
11
9
|
ChannelBinding,
|
|
12
10
|
impl,
|
|
13
|
-
ViewBinding,
|
|
14
11
|
type Binder,
|
|
15
12
|
type BinderImplementation,
|
|
16
13
|
type MCPBindingClient,
|
|
@@ -23,40 +20,10 @@ export * from "./channels.ts";
|
|
|
23
20
|
// Export binding utilities
|
|
24
21
|
export * from "./utils.ts";
|
|
25
22
|
|
|
26
|
-
// Export views schemas
|
|
27
|
-
export * from "./views.ts";
|
|
28
|
-
|
|
29
|
-
// Re-export Resources bindings function for convenience
|
|
30
|
-
export { createResourceBindings };
|
|
31
|
-
|
|
32
|
-
// Export resources types and schemas
|
|
33
|
-
export * from "./resources/bindings.ts";
|
|
34
|
-
export * from "./resources/helpers.ts";
|
|
35
|
-
export * from "./resources/schemas.ts";
|
|
36
|
-
|
|
37
|
-
// Export deconfig helpers and types
|
|
38
|
-
export {
|
|
39
|
-
getMetadataString as deconfigGetMetadataString,
|
|
40
|
-
getMetadataValue as deconfigGetMetadataValue,
|
|
41
|
-
normalizeDirectory as deconfigNormalizeDirectory,
|
|
42
|
-
ResourcePath,
|
|
43
|
-
ResourceUri,
|
|
44
|
-
} from "./deconfig/helpers.ts";
|
|
45
|
-
export { createDeconfigResource } from "./deconfig/index.ts";
|
|
46
|
-
export type {
|
|
47
|
-
DeconfigClient,
|
|
48
|
-
DeconfigResourceOptions,
|
|
49
|
-
EnhancedResourcesTools,
|
|
50
|
-
ResourcesBinding,
|
|
51
|
-
ResourcesTools,
|
|
52
|
-
} from "./deconfig/index.ts";
|
|
53
|
-
export { deconfigTools } from "./deconfig/types.ts";
|
|
54
|
-
|
|
55
23
|
export { streamToResponse } from "./language-model/utils.ts";
|
|
56
24
|
|
|
57
25
|
export const WellKnownBindings = {
|
|
58
26
|
Channel: CHANNEL_BINDING,
|
|
59
|
-
View: VIEWS_BINDING,
|
|
60
27
|
LanguageModel: LANGUAGE_MODEL_BINDING,
|
|
61
28
|
// Note: Resources is not included here since it's a generic function
|
|
62
29
|
// Use createResourceBindings(dataSchema) directly for Resources 2.0
|
|
@@ -25,94 +25,3 @@ export function streamToResponse(
|
|
|
25
25
|
},
|
|
26
26
|
});
|
|
27
27
|
}
|
|
28
|
-
|
|
29
|
-
export function responseToStream(
|
|
30
|
-
response: Response,
|
|
31
|
-
): ReadableStream<LanguageModelV2StreamPart> {
|
|
32
|
-
if (!response.body) {
|
|
33
|
-
throw new Error("Response body is null");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return response.body.pipeThrough(new TextDecoderStream()).pipeThrough(
|
|
37
|
-
new TransformStream<string, LanguageModelV2StreamPart>({
|
|
38
|
-
transform(chunk, controller) {
|
|
39
|
-
// Split by newlines and parse each line
|
|
40
|
-
const lines = chunk.split("\n");
|
|
41
|
-
|
|
42
|
-
for (const line of lines) {
|
|
43
|
-
if (line.trim()) {
|
|
44
|
-
try {
|
|
45
|
-
const parsed = JSON.parse(line) as LanguageModelV2StreamPart;
|
|
46
|
-
controller.enqueue(parsed);
|
|
47
|
-
} catch (error) {
|
|
48
|
-
console.error("Failed to parse stream chunk:", error);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
}),
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Lazy promise wrapper that defers execution until the promise is awaited.
|
|
59
|
-
* The factory function is only called when .then() is invoked for the first time.
|
|
60
|
-
*/
|
|
61
|
-
class Lazy<T> implements PromiseLike<T> {
|
|
62
|
-
private promise: Promise<T> | null = null;
|
|
63
|
-
|
|
64
|
-
constructor(private factory: () => Promise<T>) {}
|
|
65
|
-
|
|
66
|
-
private getOrCreatePromise(): Promise<T> {
|
|
67
|
-
if (!this.promise) {
|
|
68
|
-
this.promise = this.factory();
|
|
69
|
-
}
|
|
70
|
-
return this.promise;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// eslint-disable-next-line no-thenable
|
|
74
|
-
then<TResult1 = T, TResult2 = never>(
|
|
75
|
-
onfulfilled?:
|
|
76
|
-
| ((value: T) => TResult1 | PromiseLike<TResult1>)
|
|
77
|
-
| null
|
|
78
|
-
| undefined,
|
|
79
|
-
onrejected?:
|
|
80
|
-
| ((reason: unknown) => TResult2 | PromiseLike<TResult2>)
|
|
81
|
-
| null
|
|
82
|
-
| undefined,
|
|
83
|
-
): PromiseLike<TResult1 | TResult2> {
|
|
84
|
-
return this.getOrCreatePromise().then(onfulfilled, onrejected);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
catch<TResult = never>(
|
|
88
|
-
onrejected?:
|
|
89
|
-
| ((reason: unknown) => TResult | PromiseLike<TResult>)
|
|
90
|
-
| null
|
|
91
|
-
| undefined,
|
|
92
|
-
): Promise<T | TResult> {
|
|
93
|
-
return this.getOrCreatePromise().catch(onrejected);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
finally(onfinally?: (() => void) | null | undefined): Promise<T> {
|
|
97
|
-
return this.getOrCreatePromise().finally(onfinally);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Creates a lazy promise that only executes when awaited.
|
|
103
|
-
*
|
|
104
|
-
* @param factory - A function that returns a Promise<T>
|
|
105
|
-
* @returns A Promise-like object that defers execution until .then() is called
|
|
106
|
-
*
|
|
107
|
-
* @example
|
|
108
|
-
* ```ts
|
|
109
|
-
* const lazyData = lazy(() => fetchExpensiveData());
|
|
110
|
-
* // fetchExpensiveData() is NOT called yet
|
|
111
|
-
*
|
|
112
|
-
* const result = await lazyData;
|
|
113
|
-
* // fetchExpensiveData() is called NOW
|
|
114
|
-
* ```
|
|
115
|
-
*/
|
|
116
|
-
export function lazy<T>(factory: () => Promise<T>): Promise<T> {
|
|
117
|
-
return new Lazy(factory) as unknown as Promise<T>;
|
|
118
|
-
}
|