@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
package/src/bindings.ts
CHANGED
|
@@ -1,92 +1,104 @@
|
|
|
1
|
+
import { CollectionBinding } from "packages/bindings/src/well-known/collections.ts";
|
|
1
2
|
import type { MCPConnection } from "./connection.ts";
|
|
2
|
-
import type {
|
|
3
|
-
import { MCPClient } from "./mcp.ts";
|
|
4
|
-
import
|
|
5
|
-
BindingBase,
|
|
6
|
-
ContractBinding,
|
|
7
|
-
MCPBinding,
|
|
8
|
-
MCPIntegrationNameBinding,
|
|
9
|
-
} from "./wrangler.ts";
|
|
10
|
-
|
|
11
|
-
interface IntegrationContext {
|
|
12
|
-
integrationId: string;
|
|
13
|
-
workspace: string;
|
|
14
|
-
branch?: string;
|
|
15
|
-
decoCmsApiUrl?: string;
|
|
16
|
-
}
|
|
3
|
+
import type { RequestContext } from "./index.ts";
|
|
4
|
+
import { type MCPClientFetchStub, MCPClient, type ToolBinder } from "./mcp.ts";
|
|
5
|
+
import { z } from "zod";
|
|
17
6
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
return `/shared/${workspace}`;
|
|
29
|
-
};
|
|
7
|
+
type ClientContext = Omit<
|
|
8
|
+
RequestContext,
|
|
9
|
+
"ensureAuthenticated" | "state" | "fetchIntegrationMetadata"
|
|
10
|
+
>;
|
|
11
|
+
|
|
12
|
+
export interface Binding<TType extends string = string> {
|
|
13
|
+
__type: TType;
|
|
14
|
+
value: string;
|
|
15
|
+
}
|
|
30
16
|
|
|
31
17
|
/**
|
|
32
|
-
*
|
|
18
|
+
* A registry mapping binding type strings (e.g. "@deco/database") to their ToolBinder definitions.
|
|
19
|
+
* Used by ResolvedBindings to resolve binding types to their corresponding MCP client types.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* type MyBindings = {
|
|
24
|
+
* "@deco/database": typeof DATABASE_BINDING;
|
|
25
|
+
* "@deco/storage": typeof STORAGE_BINDING;
|
|
26
|
+
* };
|
|
27
|
+
* ```
|
|
33
28
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
decoChatApiUrl,
|
|
37
|
-
}: {
|
|
38
|
-
appName: string;
|
|
39
|
-
decoChatApiUrl?: string;
|
|
40
|
-
}) =>
|
|
41
|
-
new URL(
|
|
42
|
-
`/apps/mcp?appName=${appName}`,
|
|
43
|
-
decoChatApiUrl ?? "https://api.decocms.com",
|
|
44
|
-
).href;
|
|
29
|
+
export type BindingRegistry = Record<string, readonly ToolBinder[]>;
|
|
30
|
+
|
|
45
31
|
/**
|
|
46
|
-
*
|
|
32
|
+
* Function that returns Zod Schema
|
|
47
33
|
*/
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
type WorkspaceClientContext = Omit<
|
|
61
|
-
RequestContext,
|
|
62
|
-
"ensureAuthenticated" | "state" | "fetchIntegrationMetadata"
|
|
63
|
-
>;
|
|
64
|
-
export const workspaceClient = (
|
|
65
|
-
ctx: WorkspaceClientContext,
|
|
66
|
-
decocmsApiUrl?: string,
|
|
67
|
-
): ReturnType<(typeof MCPClient)["forWorkspace"]> => {
|
|
68
|
-
return MCPClient.forWorkspace(ctx.workspace, ctx.token, decocmsApiUrl);
|
|
34
|
+
export const BindingOf = <
|
|
35
|
+
TRegistry extends BindingRegistry,
|
|
36
|
+
TName extends keyof TRegistry | "*",
|
|
37
|
+
>(
|
|
38
|
+
name: TName,
|
|
39
|
+
) => {
|
|
40
|
+
return z.object({
|
|
41
|
+
__type: z.literal<TName>(name).default(name),
|
|
42
|
+
value: z.string(),
|
|
43
|
+
});
|
|
69
44
|
};
|
|
70
45
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Recursively transforms a type T by replacing all Binding instances with their
|
|
48
|
+
* corresponding MCPClientFetchStub based on the __type field.
|
|
49
|
+
*
|
|
50
|
+
* @template T - The source type to transform
|
|
51
|
+
* @template TBindings - A registry mapping binding __type strings to ToolBinder definitions
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* interface State {
|
|
56
|
+
* db: Binding<"@deco/database">;
|
|
57
|
+
* items: Array<Binding<"@deco/storage">>;
|
|
58
|
+
* config: { nested: Binding<"@deco/config"> };
|
|
59
|
+
* }
|
|
60
|
+
*
|
|
61
|
+
* type Resolved = ResolvedBindings<State, {
|
|
62
|
+
* "@deco/database": typeof DATABASE_BINDING;
|
|
63
|
+
* "@deco/storage": typeof STORAGE_BINDING;
|
|
64
|
+
* }>;
|
|
65
|
+
* // Result:
|
|
66
|
+
* // {
|
|
67
|
+
* // db: MCPClientFetchStub<typeof DATABASE_BINDING>;
|
|
68
|
+
* // items: Array<MCPClientFetchStub<typeof STORAGE_BINDING>>;
|
|
69
|
+
* // config: { nested: unknown }; // "@deco/config" not in registry
|
|
70
|
+
* // }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export type ResolvedBindings<
|
|
74
|
+
T,
|
|
75
|
+
TBindings extends BindingRegistry,
|
|
76
|
+
> = T extends Binding<infer TType>
|
|
77
|
+
? TType extends keyof TBindings
|
|
78
|
+
? MCPClientFetchStub<TBindings[TType]> & { __type: TType; value: string }
|
|
79
|
+
: MCPClientFetchStub<[]> & { __type: string; value: string }
|
|
80
|
+
: T extends Array<infer U>
|
|
81
|
+
? Array<ResolvedBindings<U, TBindings>>
|
|
82
|
+
: T extends object
|
|
83
|
+
? { [K in keyof T]: ResolvedBindings<T[K], TBindings> }
|
|
84
|
+
: T;
|
|
79
85
|
|
|
80
|
-
|
|
86
|
+
export const isBinding = (v: unknown): v is Binding => {
|
|
87
|
+
return (
|
|
88
|
+
typeof v === "object" &&
|
|
89
|
+
v !== null &&
|
|
90
|
+
typeof (v as { __type: string }).__type === "string" &&
|
|
91
|
+
typeof (v as { value: string }).value === "string"
|
|
92
|
+
);
|
|
81
93
|
};
|
|
82
94
|
|
|
83
95
|
export const proxyConnectionForId = (
|
|
84
|
-
|
|
85
|
-
ctx: Omit<
|
|
96
|
+
connectionId: string,
|
|
97
|
+
ctx: Omit<ClientContext, "token"> & {
|
|
86
98
|
token?: string;
|
|
87
99
|
cookie?: string;
|
|
100
|
+
meshUrl: string;
|
|
88
101
|
},
|
|
89
|
-
decocmsApiUrl?: string,
|
|
90
102
|
appName?: string,
|
|
91
103
|
): MCPConnection => {
|
|
92
104
|
let headers: Record<string, string> | undefined = appName
|
|
@@ -98,82 +110,77 @@ export const proxyConnectionForId = (
|
|
|
98
110
|
}
|
|
99
111
|
return {
|
|
100
112
|
type: "HTTP",
|
|
101
|
-
url:
|
|
102
|
-
integrationId,
|
|
103
|
-
workspace: ctx.workspace,
|
|
104
|
-
decoCmsApiUrl: decocmsApiUrl,
|
|
105
|
-
branch: ctx.branch,
|
|
106
|
-
}),
|
|
113
|
+
url: new URL(`/mcp/${connectionId}`, ctx.meshUrl).href,
|
|
107
114
|
token: ctx.token,
|
|
108
115
|
headers,
|
|
109
116
|
};
|
|
110
117
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
|
|
119
|
+
const mcpClientForConnectionId = (
|
|
120
|
+
connectionId: string,
|
|
121
|
+
ctx: ClientContext,
|
|
115
122
|
appName?: string,
|
|
116
123
|
) => {
|
|
117
|
-
const mcpConnection = proxyConnectionForId(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
const mcpConnection = proxyConnectionForId(connectionId, ctx, appName);
|
|
125
|
+
return new Proxy(MCPClient.forConnection(mcpConnection), {
|
|
126
|
+
get(target, name) {
|
|
127
|
+
if (name === "value") {
|
|
128
|
+
return connectionId;
|
|
129
|
+
}
|
|
130
|
+
if (name === "__type") {
|
|
131
|
+
return appName;
|
|
132
|
+
}
|
|
133
|
+
return target[name as keyof typeof target];
|
|
134
|
+
},
|
|
135
|
+
});
|
|
126
136
|
};
|
|
127
137
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const ctx = env.DECO_REQUEST_CONTEXT;
|
|
133
|
-
const bindingFromState = ctx?.state?.[binding.name];
|
|
134
|
-
const integrationId =
|
|
135
|
-
bindingFromState &&
|
|
136
|
-
typeof bindingFromState === "object" &&
|
|
137
|
-
"value" in bindingFromState
|
|
138
|
-
? bindingFromState.value
|
|
139
|
-
: undefined;
|
|
140
|
-
if (typeof integrationId !== "string" && "integration_name" in binding) {
|
|
141
|
-
// in case of a binding to an app name, we need to use the new apps/mcp endpoint which will proxy the request to the app but without any token
|
|
142
|
-
return mcpClientForAppName(binding.integration_name, env.DECO_API_URL);
|
|
138
|
+
const traverseAndReplace = (obj: unknown, ctx: ClientContext): unknown => {
|
|
139
|
+
// Handle null/undefined
|
|
140
|
+
if (obj === null || obj === undefined) {
|
|
141
|
+
return obj;
|
|
143
142
|
}
|
|
144
|
-
return mcpClientForIntegrationId(
|
|
145
|
-
integrationId,
|
|
146
|
-
ctx,
|
|
147
|
-
env.DECO_API_URL,
|
|
148
|
-
env.DECO_APP_NAME,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
143
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return mcpClientFromState(binding, env);
|
|
157
|
-
};
|
|
144
|
+
// Handle arrays
|
|
145
|
+
if (Array.isArray(obj)) {
|
|
146
|
+
return obj.map((item) => traverseAndReplace(item, ctx));
|
|
147
|
+
}
|
|
158
148
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
149
|
+
// Handle objects
|
|
150
|
+
if (typeof obj === "object") {
|
|
151
|
+
// Check if this is a binding
|
|
152
|
+
if (isBinding(obj)) {
|
|
153
|
+
return mcpClientForConnectionId(obj.value, ctx, obj.__type);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Traverse object properties
|
|
157
|
+
const result: Record<string, unknown> = {};
|
|
158
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
159
|
+
result[key] = traverseAndReplace(value, ctx);
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
167
162
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
163
|
+
|
|
164
|
+
// Return primitives as-is
|
|
165
|
+
return obj;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const initializeBindings = <
|
|
169
|
+
T,
|
|
170
|
+
TBindings extends BindingRegistry = BindingRegistry,
|
|
171
|
+
>(
|
|
172
|
+
ctx: RequestContext,
|
|
173
|
+
): ResolvedBindings<T, TBindings> => {
|
|
174
|
+
// resolves the state in-place
|
|
175
|
+
return traverseAndReplace(ctx.state, ctx) as ResolvedBindings<T, TBindings>;
|
|
179
176
|
};
|
|
177
|
+
|
|
178
|
+
interface DefaultRegistry extends BindingRegistry {
|
|
179
|
+
"@deco/mesh": CollectionBinding<{ hello: string }, "MESH">;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface XPTO {
|
|
183
|
+
MESH: Binding<"@deco/meh">;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export type XPTOResolved = ResolvedBindings<XPTO, DefaultRegistry>;
|
package/src/client.ts
CHANGED
|
@@ -1,35 +1,3 @@
|
|
|
1
|
-
import { toAsyncIterator } from "./bindings/deconfig/helpers.ts";
|
|
2
|
-
// Extract resource name from DECO_RESOURCE_${NAME}_READ pattern
|
|
3
|
-
type ExtractResourceName<K> = K extends `DECO_RESOURCE_${infer Name}_READ`
|
|
4
|
-
? Name
|
|
5
|
-
: never;
|
|
6
|
-
|
|
7
|
-
// Generate SUBSCRIBE method name from resource name
|
|
8
|
-
type SubscribeMethodName<Name extends string> =
|
|
9
|
-
`DECO_RESOURCE_${Name}_SUBSCRIBE`;
|
|
10
|
-
|
|
11
|
-
// Extract data type from READ method return type
|
|
12
|
-
type ExtractReadData<T> = T extends Promise<{ data: infer D }>
|
|
13
|
-
? D
|
|
14
|
-
: T extends { data: infer D }
|
|
15
|
-
? D
|
|
16
|
-
: never;
|
|
17
|
-
|
|
18
|
-
// Generate all SUBSCRIBE method names for a given type
|
|
19
|
-
type SubscribeMethods<T> = {
|
|
20
|
-
[K in keyof T as K extends `DECO_RESOURCE_${string}_READ`
|
|
21
|
-
? SubscribeMethodName<ExtractResourceName<K>>
|
|
22
|
-
: never]: K extends `DECO_RESOURCE_${string}_READ`
|
|
23
|
-
? // oxlint-disable-next-line no-explicit-any
|
|
24
|
-
T[K] extends (...args: any) => any
|
|
25
|
-
? (args: { id: string } | { uri: string }) => AsyncIterableIterator<{
|
|
26
|
-
uri: string;
|
|
27
|
-
data: ExtractReadData<Awaited<ReturnType<T[K]>>>;
|
|
28
|
-
}>
|
|
29
|
-
: never
|
|
30
|
-
: never;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
1
|
export type MCPClient<T> = {
|
|
34
2
|
// oxlint-disable-next-line no-explicit-any
|
|
35
3
|
[K in keyof T]: T[K] extends (...args: any) => any
|
|
@@ -38,7 +6,7 @@ export type MCPClient<T> = {
|
|
|
38
6
|
init?: CustomInit,
|
|
39
7
|
) => Promise<Awaited<ReturnType<T[K]>>>
|
|
40
8
|
: never;
|
|
41
|
-
}
|
|
9
|
+
};
|
|
42
10
|
|
|
43
11
|
export type CustomInit = RequestInit & {
|
|
44
12
|
handleResponse?: (response: Response) => Promise<unknown>;
|
|
@@ -53,123 +21,11 @@ export const DEFAULT_INIT: CustomInit = {
|
|
|
53
21
|
},
|
|
54
22
|
};
|
|
55
23
|
|
|
56
|
-
/**
|
|
57
|
-
* Helper function to call an MCP tool via fetch
|
|
58
|
-
*/
|
|
59
|
-
async function callMCPTool<T = unknown>(
|
|
60
|
-
methodName: string,
|
|
61
|
-
args: unknown,
|
|
62
|
-
init?: CustomInit,
|
|
63
|
-
): Promise<T> {
|
|
64
|
-
const mergedInit: CustomInit = {
|
|
65
|
-
...init,
|
|
66
|
-
headers: {
|
|
67
|
-
...DEFAULT_INIT.headers,
|
|
68
|
-
...init?.headers,
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const response = await fetch(`/mcp/call-tool/${methodName}`, {
|
|
73
|
-
method: "POST",
|
|
74
|
-
body: JSON.stringify(args),
|
|
75
|
-
credentials: "include",
|
|
76
|
-
...mergedInit,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
if (!response.ok) {
|
|
80
|
-
throw new Error(`Failed to call ${methodName}: ${response.statusText}`);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return response.json() as Promise<T>;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Creates a subscribe method for a resource that returns an async iterator
|
|
88
|
-
* yielding {uri, data} objects as resources are updated.
|
|
89
|
-
*/
|
|
90
|
-
function createSubscribeMethod(
|
|
91
|
-
resourceName: string,
|
|
92
|
-
init?: CustomInit,
|
|
93
|
-
): (args: { id: string }) => AsyncIterableIterator<{
|
|
94
|
-
uri: string;
|
|
95
|
-
data: unknown;
|
|
96
|
-
}> {
|
|
97
|
-
return async function* (args: { id: string } | { uri: string }) {
|
|
98
|
-
// Step 1: Call DESCRIBE to get watch endpoint configuration and URI template
|
|
99
|
-
const describeMethodName = `DECO_RESOURCE_${resourceName}_DESCRIBE`;
|
|
100
|
-
const readMethodName = `DECO_RESOURCE_${resourceName}_READ`;
|
|
101
|
-
|
|
102
|
-
// Get describe information
|
|
103
|
-
const describeData = await callMCPTool<{
|
|
104
|
-
uriTemplate?: string;
|
|
105
|
-
features?: {
|
|
106
|
-
watch?: {
|
|
107
|
-
pathname?: string;
|
|
108
|
-
};
|
|
109
|
-
};
|
|
110
|
-
}>(describeMethodName, {}, init);
|
|
111
|
-
|
|
112
|
-
const watchPathname = describeData?.features?.watch?.pathname;
|
|
113
|
-
const uriTemplate = describeData?.uriTemplate;
|
|
114
|
-
|
|
115
|
-
if (!watchPathname) {
|
|
116
|
-
throw new Error(
|
|
117
|
-
`Resource ${resourceName} does not support watch functionality`,
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (!uriTemplate) {
|
|
122
|
-
throw new Error(`Resource ${resourceName} does not provide uriTemplate`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Step 2: Construct URI from template by replacing * with id
|
|
126
|
-
const resourceUri =
|
|
127
|
-
"uri" in args ? args.uri : uriTemplate.replace("*", args.id);
|
|
128
|
-
|
|
129
|
-
// Step 3: Construct watch URL and create EventSource
|
|
130
|
-
const watchUrl = new URL(watchPathname, globalThis.location.origin);
|
|
131
|
-
watchUrl.searchParams.set("uri", resourceUri);
|
|
132
|
-
|
|
133
|
-
const eventSource = new EventSource(watchUrl.href);
|
|
134
|
-
|
|
135
|
-
// Step 4: Use toAsyncIterator to consume SSE events and enrich with READ data
|
|
136
|
-
const eventStream = toAsyncIterator<{ uri: string }>(
|
|
137
|
-
eventSource,
|
|
138
|
-
"message",
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
// Iterate over SSE events and enrich with full data
|
|
142
|
-
for await (const event of eventStream) {
|
|
143
|
-
const uri = event.uri;
|
|
144
|
-
|
|
145
|
-
if (uri) {
|
|
146
|
-
// Call READ to get full resource data
|
|
147
|
-
const readData = await callMCPTool<{ data: unknown }>(
|
|
148
|
-
readMethodName,
|
|
149
|
-
{ uri },
|
|
150
|
-
init,
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
yield { uri, data: readData.data };
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
24
|
export const createClient = <T>(init?: CustomInit): MCPClient<T> => {
|
|
160
25
|
return new Proxy(
|
|
161
26
|
{},
|
|
162
27
|
{
|
|
163
28
|
get: (_, prop) => {
|
|
164
|
-
const propStr = String(prop);
|
|
165
|
-
|
|
166
|
-
// Check if this is a SUBSCRIBE method call
|
|
167
|
-
const subscribeMatch = propStr.match(/^DECO_RESOURCE_(.+)_SUBSCRIBE$/);
|
|
168
|
-
if (subscribeMatch) {
|
|
169
|
-
const resourceName = subscribeMatch[1];
|
|
170
|
-
return createSubscribeMethod(resourceName, init);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
29
|
// Regular method call
|
|
174
30
|
return async (args: unknown, innerInit?: CustomInit) => {
|
|
175
31
|
const mergedInit: CustomInit = {
|
package/src/cors.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
export type CORSOrigin =
|
|
2
|
+
| string
|
|
3
|
+
| string[]
|
|
4
|
+
| ((origin: string, req: Request) => string | null | undefined);
|
|
5
|
+
|
|
6
|
+
export interface CORSOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The value of "Access-Control-Allow-Origin" CORS header.
|
|
9
|
+
* Can be a string, array of strings, or a function that returns the allowed origin.
|
|
10
|
+
* @default '*'
|
|
11
|
+
*/
|
|
12
|
+
origin?: CORSOrigin;
|
|
13
|
+
/**
|
|
14
|
+
* The value of "Access-Control-Allow-Methods" CORS header.
|
|
15
|
+
* @default ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']
|
|
16
|
+
*/
|
|
17
|
+
allowMethods?: string[];
|
|
18
|
+
/**
|
|
19
|
+
* The value of "Access-Control-Allow-Headers" CORS header.
|
|
20
|
+
* @default []
|
|
21
|
+
*/
|
|
22
|
+
allowHeaders?: string[];
|
|
23
|
+
/**
|
|
24
|
+
* The value of "Access-Control-Max-Age" CORS header (in seconds).
|
|
25
|
+
*/
|
|
26
|
+
maxAge?: number;
|
|
27
|
+
/**
|
|
28
|
+
* The value of "Access-Control-Allow-Credentials" CORS header.
|
|
29
|
+
*/
|
|
30
|
+
credentials?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* The value of "Access-Control-Expose-Headers" CORS header.
|
|
33
|
+
* @default []
|
|
34
|
+
*/
|
|
35
|
+
exposeHeaders?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const DEFAULT_ALLOW_METHODS = ["GET", "HEAD", "PUT", "POST", "DELETE", "PATCH"];
|
|
39
|
+
|
|
40
|
+
const resolveOrigin = (
|
|
41
|
+
origin: CORSOrigin | undefined,
|
|
42
|
+
requestOrigin: string | null,
|
|
43
|
+
req: Request,
|
|
44
|
+
): string | null => {
|
|
45
|
+
if (!requestOrigin) return null;
|
|
46
|
+
|
|
47
|
+
if (origin === undefined || origin === "*") {
|
|
48
|
+
return "*";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (typeof origin === "string") {
|
|
52
|
+
return origin === requestOrigin ? origin : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(origin)) {
|
|
56
|
+
return origin.includes(requestOrigin) ? requestOrigin : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (typeof origin === "function") {
|
|
60
|
+
return origin(requestOrigin, req) ?? null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const setCORSHeaders = (
|
|
67
|
+
headers: Headers,
|
|
68
|
+
req: Request,
|
|
69
|
+
options: CORSOptions,
|
|
70
|
+
): void => {
|
|
71
|
+
const requestOrigin = req.headers.get("Origin");
|
|
72
|
+
const allowedOrigin = resolveOrigin(options.origin, requestOrigin, req);
|
|
73
|
+
|
|
74
|
+
if (allowedOrigin) {
|
|
75
|
+
headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (options.credentials) {
|
|
79
|
+
headers.set("Access-Control-Allow-Credentials", "true");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (options.exposeHeaders?.length) {
|
|
83
|
+
headers.set(
|
|
84
|
+
"Access-Control-Expose-Headers",
|
|
85
|
+
options.exposeHeaders.join(", "),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const handlePreflight = (
|
|
91
|
+
req: Request,
|
|
92
|
+
options: CORSOptions,
|
|
93
|
+
): Response => {
|
|
94
|
+
const headers = new Headers();
|
|
95
|
+
const requestOrigin = req.headers.get("Origin");
|
|
96
|
+
const allowedOrigin = resolveOrigin(options.origin, requestOrigin, req);
|
|
97
|
+
|
|
98
|
+
if (allowedOrigin) {
|
|
99
|
+
headers.set("Access-Control-Allow-Origin", allowedOrigin);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (options.credentials) {
|
|
103
|
+
headers.set("Access-Control-Allow-Credentials", "true");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const allowMethods = options.allowMethods ?? DEFAULT_ALLOW_METHODS;
|
|
107
|
+
headers.set("Access-Control-Allow-Methods", allowMethods.join(", "));
|
|
108
|
+
|
|
109
|
+
const requestHeaders = req.headers.get("Access-Control-Request-Headers");
|
|
110
|
+
if (options.allowHeaders?.length) {
|
|
111
|
+
headers.set(
|
|
112
|
+
"Access-Control-Allow-Headers",
|
|
113
|
+
options.allowHeaders.join(", "),
|
|
114
|
+
);
|
|
115
|
+
} else if (requestHeaders) {
|
|
116
|
+
// Mirror the requested headers if no explicit allowHeaders configured
|
|
117
|
+
headers.set("Access-Control-Allow-Headers", requestHeaders);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (options.maxAge !== undefined) {
|
|
121
|
+
headers.set("Access-Control-Max-Age", options.maxAge.toString());
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return new Response(null, { status: 204, headers });
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export const withCORS = (
|
|
128
|
+
response: Response,
|
|
129
|
+
req: Request,
|
|
130
|
+
options: CORSOptions,
|
|
131
|
+
): Response => {
|
|
132
|
+
const newHeaders = new Headers(response.headers);
|
|
133
|
+
setCORSHeaders(newHeaders, req, options);
|
|
134
|
+
|
|
135
|
+
return new Response(response.body, {
|
|
136
|
+
status: response.status,
|
|
137
|
+
statusText: response.statusText,
|
|
138
|
+
headers: newHeaders,
|
|
139
|
+
});
|
|
140
|
+
};
|