@decocms/runtime 0.24.0 → 0.25.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/bindings/deconfig/index.d.ts +2 -2
- package/dist/bindings/deconfig/index.js +5 -5
- package/dist/bindings/index.d.ts +2 -2
- package/dist/bindings/index.js +6 -6
- package/dist/{chunk-4UQ5U73Y.js → chunk-F6XZPFWM.js} +3 -5
- package/dist/chunk-F6XZPFWM.js.map +1 -0
- package/dist/{chunk-ZRJ5SGAO.js → chunk-I2KGAHFY.js} +26 -6
- package/dist/chunk-I2KGAHFY.js.map +1 -0
- package/dist/{chunk-73FIKR3X.js → chunk-NKUMVYKI.js} +3 -3
- package/dist/chunk-NKUMVYKI.js.map +1 -0
- package/dist/{chunk-377XXI4J.js → chunk-O6IURJAY.js} +4 -4
- package/dist/{chunk-377XXI4J.js.map → chunk-O6IURJAY.js.map} +1 -1
- package/dist/{chunk-G3NWZG2F.js → chunk-QELHWEZH.js} +3 -3
- package/dist/chunk-QELHWEZH.js.map +1 -0
- package/dist/drizzle.d.ts +1 -1
- package/dist/{index-D_J_044C.d.ts → index-D8GtUDPS.d.ts} +11 -2
- package/dist/{index-BBAR4TQu.d.ts → index-SnnmAI05.d.ts} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +9 -6
- package/dist/index.js.map +1 -1
- package/dist/mastra.d.ts +1 -1
- package/dist/mastra.js +2 -2
- package/dist/mcp-client.js +1 -1
- package/dist/proxy.js +2 -2
- package/dist/resources.d.ts +2 -33
- package/dist/resources.js +1 -1
- package/package.json +14 -1
- package/src/admin.ts +16 -0
- package/src/auth.ts +233 -0
- package/src/bindings/README.md +132 -0
- package/src/bindings/binder.ts +143 -0
- package/src/bindings/channels.ts +54 -0
- package/src/bindings/deconfig/helpers.ts +107 -0
- package/src/bindings/deconfig/index.ts +1 -0
- package/src/bindings/deconfig/resources.ts +659 -0
- package/src/bindings/deconfig/types.ts +106 -0
- package/src/bindings/index.ts +61 -0
- package/src/bindings/resources/bindings.ts +99 -0
- package/src/bindings/resources/helpers.ts +95 -0
- package/src/bindings/resources/schemas.ts +265 -0
- package/src/bindings/utils.ts +22 -0
- package/src/bindings/views.ts +14 -0
- package/src/bindings.ts +178 -0
- package/src/cf-imports.ts +1 -0
- package/src/client.ts +201 -0
- package/src/connection.ts +53 -0
- package/src/d1-store.ts +34 -0
- package/src/deprecated.ts +59 -0
- package/src/drizzle.ts +201 -0
- package/src/http-client-transport.ts +66 -0
- package/src/index.ts +409 -0
- package/src/mastra.ts +894 -0
- package/src/mcp-client.ts +119 -0
- package/src/mcp.ts +170 -0
- package/src/proxy.ts +212 -0
- package/src/resources.ts +168 -0
- package/src/state.ts +44 -0
- package/src/views.ts +26 -0
- package/src/well-known.ts +20 -0
- package/src/workflow.ts +193 -0
- package/src/wrangler.ts +146 -0
- package/dist/chunk-4UQ5U73Y.js.map +0 -1
- package/dist/chunk-73FIKR3X.js.map +0 -1
- package/dist/chunk-G3NWZG2F.js.map +0 -1
- package/dist/chunk-ZRJ5SGAO.js.map +0 -1
package/src/drizzle.ts
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { DrizzleConfig } from "drizzle-orm";
|
|
2
|
+
import {
|
|
3
|
+
drizzle as drizzleProxy,
|
|
4
|
+
type SqliteRemoteDatabase,
|
|
5
|
+
} from "drizzle-orm/sqlite-proxy";
|
|
6
|
+
import { QueryResult } from "./mcp.ts";
|
|
7
|
+
export * from "drizzle-orm/sqlite-core";
|
|
8
|
+
export * as orm from "drizzle-orm";
|
|
9
|
+
import { sql } from "drizzle-orm";
|
|
10
|
+
import { DefaultEnv } from "./index.ts";
|
|
11
|
+
|
|
12
|
+
const mapGetResult = ({ result: [page] }: { result: QueryResult[] }) => {
|
|
13
|
+
return page.results ?? [];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const mapPostResult = ({ result }: { result: QueryResult[] }) => {
|
|
17
|
+
return (
|
|
18
|
+
result
|
|
19
|
+
.map((page) => page.results ?? [])
|
|
20
|
+
.flat()
|
|
21
|
+
// @ts-expect-error - this is ok, result comes as unknown
|
|
22
|
+
.map(Object.values)
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function drizzle<
|
|
27
|
+
TSchema extends Record<string, unknown> = Record<string, never>,
|
|
28
|
+
>(
|
|
29
|
+
{ DECO_WORKSPACE_DB }: Pick<DefaultEnv, "DECO_WORKSPACE_DB">,
|
|
30
|
+
config?: DrizzleConfig<TSchema>,
|
|
31
|
+
) {
|
|
32
|
+
return drizzleProxy((sql, params, method) => {
|
|
33
|
+
// https://orm.drizzle.team/docs/connect-drizzle-proxy says
|
|
34
|
+
// Drizzle always waits for {rows: string[][]} or {rows: string[]} for the return value.
|
|
35
|
+
// When the method is get, you should return a value as {rows: string[]}.
|
|
36
|
+
// Otherwise, you should return {rows: string[][]}.
|
|
37
|
+
const asRows = method === "get" ? mapGetResult : mapPostResult;
|
|
38
|
+
return DECO_WORKSPACE_DB.query({
|
|
39
|
+
sql,
|
|
40
|
+
params,
|
|
41
|
+
}).then((result) => ({ rows: asRows(result) }));
|
|
42
|
+
}, config);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The following code is a custom migration system tweaked
|
|
47
|
+
* from the durable-sqlite original migrator.
|
|
48
|
+
*
|
|
49
|
+
* @see https://github.com/drizzle-team/drizzle-orm/blob/main/drizzle-orm/src/durable-sqlite/migrator.ts
|
|
50
|
+
*
|
|
51
|
+
* It applies the migrations without transactions, as a workaround
|
|
52
|
+
* while we don't have remote transactions support on the
|
|
53
|
+
* workspace database durable object. Not ideal and we should
|
|
54
|
+
* look into implementing some way of doing transactions soon.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
export interface MigrationMeta {
|
|
58
|
+
sql: string[];
|
|
59
|
+
folderMillis: number;
|
|
60
|
+
hash: string;
|
|
61
|
+
bps: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface MigrationConfig {
|
|
65
|
+
journal: {
|
|
66
|
+
entries: { idx: number; when: number; tag: string; breakpoints: boolean }[];
|
|
67
|
+
};
|
|
68
|
+
migrations: Record<string, string>;
|
|
69
|
+
debug?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function readMigrationFiles({
|
|
73
|
+
journal,
|
|
74
|
+
migrations,
|
|
75
|
+
}: MigrationConfig): MigrationMeta[] {
|
|
76
|
+
const migrationQueries: MigrationMeta[] = [];
|
|
77
|
+
|
|
78
|
+
for (const journalEntry of journal.entries) {
|
|
79
|
+
const query =
|
|
80
|
+
migrations[`m${journalEntry.idx.toString().padStart(4, "0")}`];
|
|
81
|
+
|
|
82
|
+
if (!query) {
|
|
83
|
+
throw new Error(`Missing migration: ${journalEntry.tag}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const result = query.split("--> statement-breakpoint").map((it) => {
|
|
88
|
+
return it;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
migrationQueries.push({
|
|
92
|
+
sql: result,
|
|
93
|
+
bps: journalEntry.breakpoints,
|
|
94
|
+
folderMillis: journalEntry.when,
|
|
95
|
+
hash: "",
|
|
96
|
+
});
|
|
97
|
+
} catch {
|
|
98
|
+
throw new Error(`Failed to parse migration: ${journalEntry.tag}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return migrationQueries;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function migrateWithoutTransaction(
|
|
106
|
+
db: SqliteRemoteDatabase,
|
|
107
|
+
config: MigrationConfig,
|
|
108
|
+
): Promise<void> {
|
|
109
|
+
const debug = config.debug ?? false;
|
|
110
|
+
|
|
111
|
+
if (debug) console.log("Migrating database");
|
|
112
|
+
const migrations = readMigrationFiles(config);
|
|
113
|
+
if (debug) console.log("Migrations", migrations);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
if (debug) console.log("Setting up migrations table");
|
|
117
|
+
const migrationsTable = "__drizzle_migrations";
|
|
118
|
+
|
|
119
|
+
// Create migrations table if it doesn't exist
|
|
120
|
+
// Note: Changed from SERIAL to INTEGER PRIMARY KEY AUTOINCREMENT for SQLite compatibility
|
|
121
|
+
const migrationTableCreate = sql`
|
|
122
|
+
CREATE TABLE IF NOT EXISTS ${sql.identifier(migrationsTable)} (
|
|
123
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
124
|
+
hash text NOT NULL,
|
|
125
|
+
created_at numeric
|
|
126
|
+
)
|
|
127
|
+
`;
|
|
128
|
+
await db.run(migrationTableCreate);
|
|
129
|
+
|
|
130
|
+
// Get the last applied migration
|
|
131
|
+
const dbMigrations = await db.values<[number, string, string]>(
|
|
132
|
+
sql`SELECT id, hash, created_at FROM ${sql.identifier(
|
|
133
|
+
migrationsTable,
|
|
134
|
+
)} ORDER BY created_at DESC LIMIT 1`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const lastDbMigration = dbMigrations[0] ?? undefined;
|
|
138
|
+
if (debug) console.log("Last applied migration:", lastDbMigration);
|
|
139
|
+
|
|
140
|
+
// Apply pending migrations sequentially (without transaction wrapper)
|
|
141
|
+
for (const migration of migrations) {
|
|
142
|
+
const hasNoMigrations =
|
|
143
|
+
lastDbMigration === undefined || !lastDbMigration.length;
|
|
144
|
+
if (
|
|
145
|
+
hasNoMigrations ||
|
|
146
|
+
Number(lastDbMigration[2])! < migration.folderMillis
|
|
147
|
+
) {
|
|
148
|
+
if (debug) console.log(`Applying migration: ${migration.folderMillis}`);
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// Execute all statements in the migration
|
|
152
|
+
for (const stmt of migration.sql) {
|
|
153
|
+
if (stmt.trim()) {
|
|
154
|
+
// Skip empty statements
|
|
155
|
+
if (debug) {
|
|
156
|
+
console.log("Executing:", stmt.substring(0, 100) + "...");
|
|
157
|
+
}
|
|
158
|
+
await db.run(sql.raw(stmt));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Record successful migration
|
|
163
|
+
await db.run(
|
|
164
|
+
sql`INSERT INTO ${sql.identifier(
|
|
165
|
+
migrationsTable,
|
|
166
|
+
)} ("hash", "created_at") VALUES(${migration.hash}, ${migration.folderMillis})`,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (debug) {
|
|
170
|
+
console.log(
|
|
171
|
+
`✅ Migration ${migration.folderMillis} applied successfully`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
} catch (migrationError: unknown) {
|
|
175
|
+
console.error(
|
|
176
|
+
`❌ Migration ${migration.folderMillis} failed:`,
|
|
177
|
+
migrationError,
|
|
178
|
+
);
|
|
179
|
+
throw new Error(
|
|
180
|
+
`Migration failed at ${migration.folderMillis}: ${
|
|
181
|
+
migrationError instanceof Error
|
|
182
|
+
? migrationError.message
|
|
183
|
+
: String(migrationError)
|
|
184
|
+
}`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
if (debug) {
|
|
189
|
+
console.log(
|
|
190
|
+
`⏭️ Skipping already applied migration: ${migration.folderMillis}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (debug) console.log("✅ All migrations completed successfully");
|
|
197
|
+
} catch (error: unknown) {
|
|
198
|
+
console.error("❌ Migration process failed:", error);
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import {
|
|
3
|
+
StreamableHTTPClientTransport,
|
|
4
|
+
type StreamableHTTPClientTransportOptions,
|
|
5
|
+
} from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6
|
+
|
|
7
|
+
export class HTTPClientTransport extends StreamableHTTPClientTransport {
|
|
8
|
+
constructor(url: URL, opts?: StreamableHTTPClientTransportOptions) {
|
|
9
|
+
super(url, opts);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
override send(
|
|
13
|
+
message: JSONRPCMessage,
|
|
14
|
+
options?: {
|
|
15
|
+
resumptionToken?: string;
|
|
16
|
+
onresumptiontoken?: (token: string) => void;
|
|
17
|
+
},
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const mockAction = getMockActionFor(message);
|
|
20
|
+
if (mockAction?.type === "emit") {
|
|
21
|
+
this.onmessage?.(mockAction.message);
|
|
22
|
+
return Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
if (mockAction?.type === "suppress") {
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
}
|
|
27
|
+
return super.send(message, options);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type MockAction =
|
|
32
|
+
| { type: "emit"; message: JSONRPCMessage }
|
|
33
|
+
| { type: "suppress" };
|
|
34
|
+
|
|
35
|
+
function getMockActionFor(message: JSONRPCMessage): MockAction | null {
|
|
36
|
+
const m = message;
|
|
37
|
+
if (!m || typeof m !== "object" || !("method" in m)) return null;
|
|
38
|
+
|
|
39
|
+
switch (m.method) {
|
|
40
|
+
case "initialize": {
|
|
41
|
+
const protocolVersion = m?.params?.protocolVersion;
|
|
42
|
+
if (!protocolVersion) return null;
|
|
43
|
+
return {
|
|
44
|
+
type: "emit",
|
|
45
|
+
message: {
|
|
46
|
+
result: {
|
|
47
|
+
protocolVersion,
|
|
48
|
+
capabilities: { tools: {} },
|
|
49
|
+
serverInfo: { name: "deco-chat-server", version: "1.0.0" },
|
|
50
|
+
},
|
|
51
|
+
jsonrpc: m.jsonrpc ?? "2.0",
|
|
52
|
+
// @ts-expect-error - id is not typed
|
|
53
|
+
id: m.id,
|
|
54
|
+
} as JSONRPCMessage,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
case "notifications/roots/list_changed":
|
|
58
|
+
case "notifications/initialized":
|
|
59
|
+
case "notifications/cancelled":
|
|
60
|
+
case "notifications/progress": {
|
|
61
|
+
return { type: "suppress" };
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/* oxlint-disable no-explicit-any */
|
|
2
|
+
import type { ExecutionContext } from "@cloudflare/workers-types";
|
|
3
|
+
import { decodeJwt } from "jose";
|
|
4
|
+
import type { z } from "zod";
|
|
5
|
+
import {
|
|
6
|
+
getReqToken,
|
|
7
|
+
handleAuthCallback,
|
|
8
|
+
handleLogout,
|
|
9
|
+
StateParser,
|
|
10
|
+
} from "./auth.ts";
|
|
11
|
+
import {
|
|
12
|
+
createContractBinding,
|
|
13
|
+
createIntegrationBinding,
|
|
14
|
+
workspaceClient,
|
|
15
|
+
} from "./bindings.ts";
|
|
16
|
+
import { DeconfigResource } from "./bindings/deconfig/index.ts";
|
|
17
|
+
import { DECO_MCP_CLIENT_HEADER } from "./client.ts";
|
|
18
|
+
import { DeprecatedEnv } from "./deprecated.ts";
|
|
19
|
+
import {
|
|
20
|
+
createMCPServer,
|
|
21
|
+
type CreateMCPServerOptions,
|
|
22
|
+
MCPServer,
|
|
23
|
+
} from "./mastra.ts";
|
|
24
|
+
import { MCPClient, type QueryResult } from "./mcp.ts";
|
|
25
|
+
import { State } from "./state.ts";
|
|
26
|
+
import type { WorkflowDO } from "./workflow.ts";
|
|
27
|
+
import { Workflow } from "./workflow.ts";
|
|
28
|
+
import type { Binding, ContractBinding, MCPBinding } from "./wrangler.ts";
|
|
29
|
+
export { proxyConnectionForId } from "./bindings.ts";
|
|
30
|
+
export {
|
|
31
|
+
createMCPFetchStub,
|
|
32
|
+
type CreateStubAPIOptions,
|
|
33
|
+
type ToolBinder,
|
|
34
|
+
} from "./mcp.ts";
|
|
35
|
+
export interface WorkspaceDB {
|
|
36
|
+
query: (params: {
|
|
37
|
+
sql: string;
|
|
38
|
+
params: string[];
|
|
39
|
+
}) => Promise<{ result: QueryResult[] }>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DefaultEnv<TSchema extends z.ZodTypeAny = any>
|
|
43
|
+
extends DeprecatedEnv<TSchema> {
|
|
44
|
+
DECO_REQUEST_CONTEXT: RequestContext<TSchema>;
|
|
45
|
+
DECO_APP_NAME: string;
|
|
46
|
+
DECO_APP_SLUG: string;
|
|
47
|
+
DECO_APP_ENTRYPOINT: string;
|
|
48
|
+
DECO_API_URL?: string;
|
|
49
|
+
DECO_WORKSPACE: string;
|
|
50
|
+
DECO_API_JWT_PUBLIC_KEY: string;
|
|
51
|
+
DECO_APP_DEPLOYMENT_ID: string;
|
|
52
|
+
DECO_BINDINGS: string;
|
|
53
|
+
DECO_API_TOKEN: string;
|
|
54
|
+
DECO_WORKFLOW_DO: DurableObjectNamespace<WorkflowDO>;
|
|
55
|
+
DECO_WORKSPACE_DB: WorkspaceDB & {
|
|
56
|
+
forContext: (ctx: RequestContext) => WorkspaceDB;
|
|
57
|
+
};
|
|
58
|
+
IS_LOCAL: boolean;
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface BindingsObject {
|
|
63
|
+
bindings?: Binding[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const WorkersMCPBindings = {
|
|
67
|
+
parse: (bindings?: string): Binding[] => {
|
|
68
|
+
if (!bindings) return [];
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(atob(bindings)) as Binding[];
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
stringify: (bindings: Binding[]): string => {
|
|
76
|
+
return btoa(JSON.stringify(bindings));
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export interface UserDefaultExport<
|
|
81
|
+
TUserEnv = Record<string, unknown>,
|
|
82
|
+
TSchema extends z.ZodTypeAny = never,
|
|
83
|
+
TEnv = TUserEnv & DefaultEnv<TSchema>,
|
|
84
|
+
> extends CreateMCPServerOptions<TEnv, TSchema> {
|
|
85
|
+
fetch?: (
|
|
86
|
+
req: Request,
|
|
87
|
+
env: TEnv,
|
|
88
|
+
ctx: ExecutionContext,
|
|
89
|
+
) => Promise<Response> | Response;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 1. Map binding type to its interface
|
|
93
|
+
interface BindingTypeMap {
|
|
94
|
+
mcp: MCPBinding;
|
|
95
|
+
contract: ContractBinding;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface User {
|
|
99
|
+
id: string;
|
|
100
|
+
email: string;
|
|
101
|
+
workspace: string;
|
|
102
|
+
user_metadata: {
|
|
103
|
+
avatar_url: string;
|
|
104
|
+
full_name: string;
|
|
105
|
+
picture: string;
|
|
106
|
+
[key: string]: unknown;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface RequestContext<TSchema extends z.ZodTypeAny = any> {
|
|
111
|
+
state: z.infer<TSchema>;
|
|
112
|
+
branch?: string;
|
|
113
|
+
token: string;
|
|
114
|
+
workspace: string;
|
|
115
|
+
ensureAuthenticated: (options?: {
|
|
116
|
+
workspaceHint?: string;
|
|
117
|
+
}) => User | undefined;
|
|
118
|
+
callerApp?: string;
|
|
119
|
+
integrationId?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 2. Map binding type to its creator function
|
|
123
|
+
type CreatorByType = {
|
|
124
|
+
[K in keyof BindingTypeMap]: (
|
|
125
|
+
value: BindingTypeMap[K],
|
|
126
|
+
env: DefaultEnv,
|
|
127
|
+
) => unknown;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// 3. Strongly type creatorByType
|
|
131
|
+
const creatorByType: CreatorByType = {
|
|
132
|
+
mcp: createIntegrationBinding,
|
|
133
|
+
contract: createContractBinding,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const withDefaultBindings = ({
|
|
137
|
+
env,
|
|
138
|
+
server,
|
|
139
|
+
ctx,
|
|
140
|
+
url,
|
|
141
|
+
}: {
|
|
142
|
+
env: DefaultEnv;
|
|
143
|
+
server: MCPServer<any, any>;
|
|
144
|
+
ctx: RequestContext;
|
|
145
|
+
url?: string;
|
|
146
|
+
}) => {
|
|
147
|
+
const client = workspaceClient(ctx);
|
|
148
|
+
const createWorkspaceDB = (ctx: RequestContext): WorkspaceDB => {
|
|
149
|
+
const client = workspaceClient(ctx);
|
|
150
|
+
return {
|
|
151
|
+
query: ({ sql, params }) => {
|
|
152
|
+
return client.DATABASES_RUN_SQL({
|
|
153
|
+
sql,
|
|
154
|
+
params,
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
env["SELF"] = new Proxy(
|
|
160
|
+
{},
|
|
161
|
+
{
|
|
162
|
+
get: (_, prop) => {
|
|
163
|
+
if (prop === "toJSON") {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return async (args: unknown) => {
|
|
168
|
+
return await server.callTool({
|
|
169
|
+
toolCallId: prop as string,
|
|
170
|
+
toolCallInput: args,
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const workspaceDbBinding = {
|
|
178
|
+
...createWorkspaceDB(ctx),
|
|
179
|
+
forContext: createWorkspaceDB,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
env["DECO_API"] = MCPClient;
|
|
183
|
+
env["DECO_WORKSPACE_API"] = client;
|
|
184
|
+
env["DECO_WORKSPACE_DB"] = workspaceDbBinding;
|
|
185
|
+
|
|
186
|
+
// Backwards compatibility
|
|
187
|
+
env["DECO_CHAT_API"] = MCPClient;
|
|
188
|
+
env["DECO_CHAT_WORKSPACE_API"] = client;
|
|
189
|
+
env["DECO_CHAT_WORKSPACE_DB"] = workspaceDbBinding;
|
|
190
|
+
|
|
191
|
+
env["IS_LOCAL"] =
|
|
192
|
+
(url?.startsWith("http://localhost") ||
|
|
193
|
+
url?.startsWith("http://127.0.0.1")) ??
|
|
194
|
+
false;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export class UnauthorizedError extends Error {
|
|
198
|
+
constructor(
|
|
199
|
+
message: string,
|
|
200
|
+
public redirectTo: URL,
|
|
201
|
+
) {
|
|
202
|
+
super(message);
|
|
203
|
+
this.name = "UnauthorizedError";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const AUTH_CALLBACK_ENDPOINT = "/oauth/callback";
|
|
208
|
+
const AUTH_START_ENDPOINT = "/oauth/start";
|
|
209
|
+
const AUTH_LOGOUT_ENDPOINT = "/oauth/logout";
|
|
210
|
+
const AUTHENTICATED = (user?: unknown, workspace?: string) => () => {
|
|
211
|
+
return {
|
|
212
|
+
...((user as User) ?? {}),
|
|
213
|
+
workspace,
|
|
214
|
+
} as User;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export const withBindings = <TEnv>({
|
|
218
|
+
env: _env,
|
|
219
|
+
server,
|
|
220
|
+
tokenOrContext,
|
|
221
|
+
origin,
|
|
222
|
+
url,
|
|
223
|
+
branch,
|
|
224
|
+
}: {
|
|
225
|
+
env: TEnv;
|
|
226
|
+
server: MCPServer<TEnv, any>;
|
|
227
|
+
tokenOrContext?: string | RequestContext;
|
|
228
|
+
origin?: string | null;
|
|
229
|
+
url?: string;
|
|
230
|
+
branch?: string | null;
|
|
231
|
+
}): TEnv => {
|
|
232
|
+
branch ??= undefined;
|
|
233
|
+
const env = _env as DefaultEnv<any>;
|
|
234
|
+
|
|
235
|
+
const apiUrl = env.DECO_API_URL ?? "https://api.decocms.com";
|
|
236
|
+
let context;
|
|
237
|
+
if (typeof tokenOrContext === "string") {
|
|
238
|
+
const decoded = decodeJwt(tokenOrContext);
|
|
239
|
+
const workspace = decoded.aud as string;
|
|
240
|
+
|
|
241
|
+
context = {
|
|
242
|
+
state: decoded.state as Record<string, unknown>,
|
|
243
|
+
token: tokenOrContext,
|
|
244
|
+
integrationId: decoded.integrationId as string,
|
|
245
|
+
workspace,
|
|
246
|
+
ensureAuthenticated: AUTHENTICATED(decoded.user, workspace),
|
|
247
|
+
branch,
|
|
248
|
+
} as RequestContext<any>;
|
|
249
|
+
} else if (typeof tokenOrContext === "object") {
|
|
250
|
+
context = tokenOrContext;
|
|
251
|
+
const decoded = decodeJwt(tokenOrContext.token);
|
|
252
|
+
const workspace = decoded.aud as string;
|
|
253
|
+
const appName = decoded.appName as string | undefined;
|
|
254
|
+
context.callerApp = appName;
|
|
255
|
+
context.integrationId ??= decoded.integrationId as string;
|
|
256
|
+
context.ensureAuthenticated = AUTHENTICATED(decoded.user, workspace);
|
|
257
|
+
} else {
|
|
258
|
+
context = {
|
|
259
|
+
state: undefined,
|
|
260
|
+
token: env.DECO_API_TOKEN,
|
|
261
|
+
workspace: env.DECO_WORKSPACE,
|
|
262
|
+
branch,
|
|
263
|
+
ensureAuthenticated: (options?: { workspaceHint?: string }) => {
|
|
264
|
+
const workspaceHint = options?.workspaceHint ?? env.DECO_WORKSPACE;
|
|
265
|
+
const authUri = new URL("/apps/oauth", apiUrl);
|
|
266
|
+
authUri.searchParams.set("client_id", env.DECO_APP_NAME);
|
|
267
|
+
authUri.searchParams.set(
|
|
268
|
+
"redirect_uri",
|
|
269
|
+
new URL(AUTH_CALLBACK_ENDPOINT, origin ?? env.DECO_APP_ENTRYPOINT)
|
|
270
|
+
.href,
|
|
271
|
+
);
|
|
272
|
+
workspaceHint &&
|
|
273
|
+
authUri.searchParams.set("workspace_hint", workspaceHint);
|
|
274
|
+
throw new UnauthorizedError("Unauthorized", authUri);
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
env.DECO_REQUEST_CONTEXT = context;
|
|
280
|
+
// Backwards compatibility
|
|
281
|
+
env.DECO_CHAT_REQUEST_CONTEXT = context;
|
|
282
|
+
const bindings = WorkersMCPBindings.parse(env.DECO_BINDINGS);
|
|
283
|
+
|
|
284
|
+
for (const binding of bindings) {
|
|
285
|
+
env[binding.name] = creatorByType[binding.type](binding as any, env);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
withDefaultBindings({
|
|
289
|
+
env,
|
|
290
|
+
server,
|
|
291
|
+
ctx: env.DECO_REQUEST_CONTEXT,
|
|
292
|
+
url,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
return env as TEnv;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export const withRuntime = <TEnv, TSchema extends z.ZodTypeAny = never>(
|
|
299
|
+
userFns: UserDefaultExport<TEnv, TSchema>,
|
|
300
|
+
): ExportedHandler<TEnv & DefaultEnv<TSchema>> & {
|
|
301
|
+
Workflow: ReturnType<typeof Workflow>;
|
|
302
|
+
} => {
|
|
303
|
+
const server = createMCPServer<TEnv, TSchema>(userFns);
|
|
304
|
+
const fetcher = async (
|
|
305
|
+
req: Request,
|
|
306
|
+
env: TEnv & DefaultEnv<TSchema>,
|
|
307
|
+
ctx: ExecutionContext,
|
|
308
|
+
) => {
|
|
309
|
+
const url = new URL(req.url);
|
|
310
|
+
if (url.pathname === AUTH_CALLBACK_ENDPOINT) {
|
|
311
|
+
return handleAuthCallback(req, {
|
|
312
|
+
apiUrl: env.DECO_API_URL,
|
|
313
|
+
appName: env.DECO_APP_NAME,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
if (url.pathname === AUTH_START_ENDPOINT) {
|
|
317
|
+
env.DECO_REQUEST_CONTEXT.ensureAuthenticated();
|
|
318
|
+
const redirectTo = new URL("/", url);
|
|
319
|
+
const next = url.searchParams.get("next");
|
|
320
|
+
return Response.redirect(next ?? redirectTo, 302);
|
|
321
|
+
}
|
|
322
|
+
if (url.pathname === AUTH_LOGOUT_ENDPOINT) {
|
|
323
|
+
return handleLogout(req);
|
|
324
|
+
}
|
|
325
|
+
if (url.pathname === "/mcp") {
|
|
326
|
+
return server.fetch(req, env, ctx);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (url.pathname.startsWith("/mcp/call-tool")) {
|
|
330
|
+
const toolCallId = url.pathname.split("/").pop();
|
|
331
|
+
if (!toolCallId) {
|
|
332
|
+
return new Response("Not found", { status: 404 });
|
|
333
|
+
}
|
|
334
|
+
const toolCallInput = await req.json();
|
|
335
|
+
const result = await server.callTool({
|
|
336
|
+
toolCallId,
|
|
337
|
+
toolCallInput,
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
if (result instanceof Response) {
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return new Response(JSON.stringify(result), {
|
|
345
|
+
headers: {
|
|
346
|
+
"Content-Type": "application/json",
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (url.pathname.startsWith(DeconfigResource.WatchPathNameBase)) {
|
|
352
|
+
return DeconfigResource.watchAPI(req, env);
|
|
353
|
+
}
|
|
354
|
+
return (
|
|
355
|
+
userFns.fetch?.(req, env, ctx) ||
|
|
356
|
+
new Response("Not found", { status: 404 })
|
|
357
|
+
);
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
Workflow: Workflow(server, userFns.workflows),
|
|
361
|
+
fetch: async (
|
|
362
|
+
req: Request,
|
|
363
|
+
env: TEnv & DefaultEnv<TSchema>,
|
|
364
|
+
ctx: ExecutionContext,
|
|
365
|
+
) => {
|
|
366
|
+
const referer = req.headers.get("referer");
|
|
367
|
+
const isFetchRequest = req.headers.has(DECO_MCP_CLIENT_HEADER);
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
const bindings = withBindings({
|
|
371
|
+
env,
|
|
372
|
+
server,
|
|
373
|
+
branch:
|
|
374
|
+
req.headers.get("x-deco-branch") ??
|
|
375
|
+
new URL(req.url).searchParams.get("__b"),
|
|
376
|
+
tokenOrContext: await getReqToken(req, env),
|
|
377
|
+
origin:
|
|
378
|
+
referer ?? req.headers.get("origin") ?? new URL(req.url).origin,
|
|
379
|
+
url: req.url,
|
|
380
|
+
});
|
|
381
|
+
return await State.run(
|
|
382
|
+
{ req, env: bindings, ctx },
|
|
383
|
+
async () => await fetcher(req, bindings, ctx),
|
|
384
|
+
);
|
|
385
|
+
} catch (error) {
|
|
386
|
+
if (error instanceof UnauthorizedError) {
|
|
387
|
+
if (!isFetchRequest) {
|
|
388
|
+
const url = new URL(req.url);
|
|
389
|
+
error.redirectTo.searchParams.set(
|
|
390
|
+
"state",
|
|
391
|
+
StateParser.stringify({
|
|
392
|
+
next: url.searchParams.get("next") ?? referer ?? req.url,
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
return Response.redirect(error.redirectTo, 302);
|
|
396
|
+
}
|
|
397
|
+
return new Response(null, { status: 401 });
|
|
398
|
+
}
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
export {
|
|
406
|
+
type Contract,
|
|
407
|
+
type Migration,
|
|
408
|
+
type WranglerConfig,
|
|
409
|
+
} from "./wrangler.ts";
|