@decocms/runtime 1.0.0-alpha.2 → 1.0.0-alpha.21
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 +553 -0
- package/package.json +5 -14
- package/src/bindings/binder.ts +1 -4
- package/src/bindings/index.ts +0 -33
- package/src/bindings/language-model/utils.ts +0 -91
- package/src/bindings.ts +31 -110
- package/src/client.ts +1 -145
- package/src/cors.ts +140 -0
- package/src/index.ts +84 -167
- package/src/mcp.ts +7 -166
- package/src/proxy.ts +3 -54
- package/src/state.ts +3 -31
- package/src/tools.ts +372 -0
- package/src/wrangler.ts +5 -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/mastra.ts +0 -670
- package/src/resources.ts +0 -168
- package/src/views.ts +0 -26
- package/src/well-known.ts +0 -20
|
@@ -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
|
-
}
|
package/src/bindings.ts
CHANGED
|
@@ -4,89 +4,22 @@ import { MCPClient } from "./mcp.ts";
|
|
|
4
4
|
import type {
|
|
5
5
|
BindingBase,
|
|
6
6
|
ContractBinding,
|
|
7
|
+
MCPAppBinding,
|
|
7
8
|
MCPBinding,
|
|
8
|
-
MCPIntegrationNameBinding,
|
|
9
9
|
} from "./wrangler.ts";
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
integrationId: string;
|
|
13
|
-
workspace: string;
|
|
14
|
-
branch?: string;
|
|
15
|
-
decoCmsApiUrl?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const normalizeWorkspace = (workspace: string) => {
|
|
19
|
-
if (workspace.startsWith("/users")) {
|
|
20
|
-
return workspace;
|
|
21
|
-
}
|
|
22
|
-
if (workspace.startsWith("/shared")) {
|
|
23
|
-
return workspace;
|
|
24
|
-
}
|
|
25
|
-
if (workspace.includes("/")) {
|
|
26
|
-
return workspace;
|
|
27
|
-
}
|
|
28
|
-
return `/shared/${workspace}`;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Url: /apps/mcp?appName=$appName
|
|
33
|
-
*/
|
|
34
|
-
const createAppsUrl = ({
|
|
35
|
-
appName,
|
|
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;
|
|
45
|
-
/**
|
|
46
|
-
* Url: /:workspace.root/:workspace.slug/:integrationId/mcp
|
|
47
|
-
*/
|
|
48
|
-
const createIntegrationsUrl = ({
|
|
49
|
-
integrationId,
|
|
50
|
-
workspace,
|
|
51
|
-
decoCmsApiUrl,
|
|
52
|
-
branch,
|
|
53
|
-
}: IntegrationContext) => {
|
|
54
|
-
const base = `${normalizeWorkspace(workspace)}/${integrationId}/mcp`;
|
|
55
|
-
const url = new URL(base, decoCmsApiUrl ?? "https://api.decocms.com");
|
|
56
|
-
branch && url.searchParams.set("branch", branch);
|
|
57
|
-
return url.href;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
type WorkspaceClientContext = Omit<
|
|
11
|
+
type ClientContext = Omit<
|
|
61
12
|
RequestContext,
|
|
62
13
|
"ensureAuthenticated" | "state" | "fetchIntegrationMetadata"
|
|
63
14
|
>;
|
|
64
|
-
export const workspaceClient = (
|
|
65
|
-
ctx: WorkspaceClientContext,
|
|
66
|
-
decocmsApiUrl?: string,
|
|
67
|
-
): ReturnType<(typeof MCPClient)["forWorkspace"]> => {
|
|
68
|
-
return MCPClient.forWorkspace(ctx.workspace, ctx.token, decocmsApiUrl);
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const mcpClientForAppName = (appName: string, decoChatApiUrl?: string) => {
|
|
72
|
-
const mcpConnection: MCPConnection = {
|
|
73
|
-
type: "HTTP",
|
|
74
|
-
url: createAppsUrl({
|
|
75
|
-
appName,
|
|
76
|
-
decoChatApiUrl,
|
|
77
|
-
}),
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
return MCPClient.forConnection(mcpConnection, decoChatApiUrl);
|
|
81
|
-
};
|
|
82
15
|
|
|
83
16
|
export const proxyConnectionForId = (
|
|
84
|
-
|
|
85
|
-
ctx: Omit<
|
|
17
|
+
connectionId: string,
|
|
18
|
+
ctx: Omit<ClientContext, "token"> & {
|
|
86
19
|
token?: string;
|
|
87
20
|
cookie?: string;
|
|
21
|
+
meshUrl: string;
|
|
88
22
|
},
|
|
89
|
-
decocmsApiUrl?: string,
|
|
90
23
|
appName?: string,
|
|
91
24
|
): MCPConnection => {
|
|
92
25
|
let headers: Record<string, string> | undefined = appName
|
|
@@ -98,55 +31,39 @@ export const proxyConnectionForId = (
|
|
|
98
31
|
}
|
|
99
32
|
return {
|
|
100
33
|
type: "HTTP",
|
|
101
|
-
url:
|
|
102
|
-
integrationId,
|
|
103
|
-
workspace: ctx.workspace,
|
|
104
|
-
decoCmsApiUrl: decocmsApiUrl,
|
|
105
|
-
branch: ctx.branch,
|
|
106
|
-
}),
|
|
34
|
+
url: new URL(`/mcp/${connectionId}`, ctx.meshUrl).href,
|
|
107
35
|
token: ctx.token,
|
|
108
36
|
headers,
|
|
109
37
|
};
|
|
110
38
|
};
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
ctx:
|
|
114
|
-
decocmsApiUrl?: string,
|
|
39
|
+
const mcpClientForConnectionId = (
|
|
40
|
+
connectionId: string,
|
|
41
|
+
ctx: ClientContext,
|
|
115
42
|
appName?: string,
|
|
116
43
|
) => {
|
|
117
|
-
const mcpConnection = proxyConnectionForId(
|
|
118
|
-
integrationId,
|
|
119
|
-
ctx,
|
|
120
|
-
decocmsApiUrl,
|
|
121
|
-
appName,
|
|
122
|
-
);
|
|
44
|
+
const mcpConnection = proxyConnectionForId(connectionId, ctx, appName);
|
|
123
45
|
|
|
124
46
|
// TODO(@igorbrasileiro): Switch this proxy to be a proxy that call MCP Client.toolCall from @modelcontextprotocol
|
|
125
|
-
return MCPClient.forConnection(mcpConnection
|
|
47
|
+
return MCPClient.forConnection(mcpConnection);
|
|
126
48
|
};
|
|
127
49
|
|
|
128
50
|
function mcpClientFromState(
|
|
129
|
-
binding: BindingBase |
|
|
51
|
+
binding: BindingBase | MCPAppBinding,
|
|
130
52
|
env: DefaultEnv,
|
|
131
53
|
) {
|
|
132
|
-
const ctx = env.
|
|
54
|
+
const ctx = env.MESH_REQUEST_CONTEXT;
|
|
133
55
|
const bindingFromState = ctx?.state?.[binding.name];
|
|
134
|
-
const
|
|
56
|
+
const connectionId =
|
|
135
57
|
bindingFromState &&
|
|
136
58
|
typeof bindingFromState === "object" &&
|
|
137
59
|
"value" in bindingFromState
|
|
138
60
|
? bindingFromState.value
|
|
139
61
|
: undefined;
|
|
140
|
-
if (typeof
|
|
62
|
+
if (typeof connectionId !== "string" && "app_name" in binding) {
|
|
141
63
|
// 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
|
|
64
|
+
return undefined;
|
|
143
65
|
}
|
|
144
|
-
return
|
|
145
|
-
integrationId,
|
|
146
|
-
ctx,
|
|
147
|
-
env.DECO_API_URL,
|
|
148
|
-
env.DECO_APP_NAME,
|
|
149
|
-
);
|
|
66
|
+
return mcpClientForConnectionId(connectionId, ctx);
|
|
150
67
|
}
|
|
151
68
|
|
|
152
69
|
export const createContractBinding = (
|
|
@@ -160,20 +77,24 @@ export const createIntegrationBinding = (
|
|
|
160
77
|
binding: MCPBinding,
|
|
161
78
|
env: DefaultEnv,
|
|
162
79
|
) => {
|
|
163
|
-
const
|
|
164
|
-
"
|
|
165
|
-
if (!
|
|
80
|
+
const connectionId =
|
|
81
|
+
"connection_id" in binding ? binding.connection_id : undefined;
|
|
82
|
+
if (!connectionId) {
|
|
166
83
|
return mcpClientFromState(binding, env);
|
|
167
84
|
}
|
|
85
|
+
if (!env.MESH_RUNTIME_TOKEN) {
|
|
86
|
+
throw new Error("MESH_RUNTIME_TOKEN is required");
|
|
87
|
+
}
|
|
88
|
+
if (!env.MESH_URL) {
|
|
89
|
+
throw new Error("MESH_URL is required");
|
|
90
|
+
}
|
|
168
91
|
// bindings pointed to an specific integration id are binded using the app deployment workspace
|
|
169
|
-
return
|
|
170
|
-
|
|
92
|
+
return mcpClientForConnectionId(
|
|
93
|
+
connectionId,
|
|
171
94
|
{
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
branch: env.DECO_REQUEST_CONTEXT?.branch,
|
|
95
|
+
token: env.MESH_RUNTIME_TOKEN,
|
|
96
|
+
meshUrl: env.MESH_URL,
|
|
175
97
|
},
|
|
176
|
-
env.
|
|
177
|
-
env.DECO_APP_NAME,
|
|
98
|
+
env.MESH_APP_NAME,
|
|
178
99
|
);
|
|
179
100
|
};
|
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
|
+
};
|