@decocms/runtime 1.0.0-alpha.2 → 1.0.0-alpha.20
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 +80 -154
- package/src/mcp.ts +7 -166
- package/src/proxy.ts +3 -54
- package/src/state.ts +3 -31
- package/src/tools.ts +376 -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
package/src/tools.ts
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/* oxlint-disable no-explicit-any */
|
|
2
|
+
/* oxlint-disable ban-types */
|
|
3
|
+
import { HttpServerTransport } from "@deco/mcp/http";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
7
|
+
import type { DefaultEnv } from "./index.ts";
|
|
8
|
+
import { State } from "./state.ts";
|
|
9
|
+
import { Binding } from "./wrangler.ts";
|
|
10
|
+
|
|
11
|
+
export const createRuntimeContext = (prev?: AppContext) => {
|
|
12
|
+
const store = State.getStore();
|
|
13
|
+
if (!store) {
|
|
14
|
+
if (prev) {
|
|
15
|
+
return prev;
|
|
16
|
+
}
|
|
17
|
+
throw new Error("Missing context, did you forget to call State.bind?");
|
|
18
|
+
}
|
|
19
|
+
return store;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface ToolExecutionContext<
|
|
23
|
+
TSchemaIn extends z.ZodTypeAny = z.ZodTypeAny,
|
|
24
|
+
> {
|
|
25
|
+
context: z.infer<TSchemaIn>;
|
|
26
|
+
runtimeContext: AppContext;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Tool interface with generic schema types for type-safe tool creation.
|
|
31
|
+
*/
|
|
32
|
+
export interface Tool<
|
|
33
|
+
TSchemaIn extends z.ZodTypeAny = z.ZodTypeAny,
|
|
34
|
+
TSchemaOut extends z.ZodTypeAny | undefined = undefined,
|
|
35
|
+
> {
|
|
36
|
+
id: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
inputSchema: TSchemaIn;
|
|
39
|
+
outputSchema?: TSchemaOut;
|
|
40
|
+
execute(
|
|
41
|
+
context: ToolExecutionContext<TSchemaIn>,
|
|
42
|
+
): TSchemaOut extends z.ZodSchema
|
|
43
|
+
? Promise<z.infer<TSchemaOut>>
|
|
44
|
+
: Promise<unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Streamable tool interface for tools that return Response streams.
|
|
49
|
+
*/
|
|
50
|
+
export interface StreamableTool<TSchemaIn extends z.ZodSchema = z.ZodSchema> {
|
|
51
|
+
id: string;
|
|
52
|
+
inputSchema: TSchemaIn;
|
|
53
|
+
streamable?: true;
|
|
54
|
+
description?: string;
|
|
55
|
+
execute(input: ToolExecutionContext<TSchemaIn>): Promise<Response>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* CreatedTool is a permissive type that any Tool or StreamableTool can be assigned to.
|
|
60
|
+
* Uses a structural type with relaxed execute signature to allow tools with any schema.
|
|
61
|
+
*/
|
|
62
|
+
export type CreatedTool = {
|
|
63
|
+
id: string;
|
|
64
|
+
description?: string;
|
|
65
|
+
inputSchema: z.ZodTypeAny;
|
|
66
|
+
outputSchema?: z.ZodTypeAny;
|
|
67
|
+
streamable?: true;
|
|
68
|
+
// Use a permissive execute signature - accepts any context shape
|
|
69
|
+
execute(context: {
|
|
70
|
+
context: unknown;
|
|
71
|
+
runtimeContext: AppContext;
|
|
72
|
+
}): Promise<unknown>;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* creates a private tool that always ensure for athentication before being executed
|
|
77
|
+
*/
|
|
78
|
+
export function createPrivateTool<
|
|
79
|
+
TSchemaIn extends z.ZodSchema = z.ZodSchema,
|
|
80
|
+
TSchemaOut extends z.ZodSchema | undefined = undefined,
|
|
81
|
+
>(opts: Tool<TSchemaIn, TSchemaOut>): Tool<TSchemaIn, TSchemaOut> {
|
|
82
|
+
const execute = opts.execute;
|
|
83
|
+
if (typeof execute === "function") {
|
|
84
|
+
opts.execute = (input: ToolExecutionContext<TSchemaIn>) => {
|
|
85
|
+
const env = input.runtimeContext.env;
|
|
86
|
+
if (env) {
|
|
87
|
+
env.MESH_REQUEST_CONTEXT?.ensureAuthenticated();
|
|
88
|
+
}
|
|
89
|
+
return execute(input);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return createTool(opts);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createStreamableTool<
|
|
96
|
+
TSchemaIn extends z.ZodSchema = z.ZodSchema,
|
|
97
|
+
>(streamableTool: StreamableTool<TSchemaIn>): StreamableTool<TSchemaIn> {
|
|
98
|
+
return {
|
|
99
|
+
...streamableTool,
|
|
100
|
+
execute: (input: ToolExecutionContext<TSchemaIn>) => {
|
|
101
|
+
const env = input.runtimeContext.env;
|
|
102
|
+
if (env) {
|
|
103
|
+
env.MESH_REQUEST_CONTEXT?.ensureAuthenticated();
|
|
104
|
+
}
|
|
105
|
+
return streamableTool.execute({
|
|
106
|
+
...input,
|
|
107
|
+
runtimeContext: createRuntimeContext(input.runtimeContext),
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function createTool<
|
|
114
|
+
TSchemaIn extends z.ZodSchema = z.ZodSchema,
|
|
115
|
+
TSchemaOut extends z.ZodSchema | undefined = undefined,
|
|
116
|
+
>(opts: Tool<TSchemaIn, TSchemaOut>): Tool<TSchemaIn, TSchemaOut> {
|
|
117
|
+
return {
|
|
118
|
+
...opts,
|
|
119
|
+
execute: (input: ToolExecutionContext<TSchemaIn>) => {
|
|
120
|
+
return opts.execute({
|
|
121
|
+
...input,
|
|
122
|
+
runtimeContext: createRuntimeContext(input.runtimeContext),
|
|
123
|
+
});
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ViewExport {
|
|
129
|
+
title: string;
|
|
130
|
+
icon: string;
|
|
131
|
+
url: string;
|
|
132
|
+
tools?: string[];
|
|
133
|
+
rules?: string[];
|
|
134
|
+
installBehavior?: "none" | "open" | "autoPin";
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface Integration {
|
|
138
|
+
id: string;
|
|
139
|
+
appId: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function isStreamableTool(
|
|
143
|
+
tool: CreatedTool,
|
|
144
|
+
): tool is StreamableTool & CreatedTool {
|
|
145
|
+
return tool && "streamable" in tool && tool.streamable === true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface OnChangeCallback<TSchema extends z.ZodTypeAny = never> {
|
|
149
|
+
state: z.infer<TSchema>;
|
|
150
|
+
scopes: string[];
|
|
151
|
+
}
|
|
152
|
+
export interface CreateMCPServerOptions<
|
|
153
|
+
Env = unknown,
|
|
154
|
+
TSchema extends z.ZodTypeAny = never,
|
|
155
|
+
> {
|
|
156
|
+
before?: (env: Env & DefaultEnv<TSchema>) => Promise<void> | void;
|
|
157
|
+
configuration?: {
|
|
158
|
+
onChange?: (
|
|
159
|
+
env: Env & DefaultEnv<TSchema>,
|
|
160
|
+
cb: OnChangeCallback<TSchema>,
|
|
161
|
+
) => Promise<void>;
|
|
162
|
+
state?: TSchema;
|
|
163
|
+
scopes?: string[];
|
|
164
|
+
};
|
|
165
|
+
bindings?: Binding[];
|
|
166
|
+
tools?:
|
|
167
|
+
| Array<
|
|
168
|
+
(
|
|
169
|
+
env: Env & DefaultEnv<TSchema>,
|
|
170
|
+
) =>
|
|
171
|
+
| Promise<CreatedTool>
|
|
172
|
+
| CreatedTool
|
|
173
|
+
| CreatedTool[]
|
|
174
|
+
| Promise<CreatedTool[]>
|
|
175
|
+
>
|
|
176
|
+
| ((
|
|
177
|
+
env: Env & DefaultEnv<TSchema>,
|
|
178
|
+
) => CreatedTool[] | Promise<CreatedTool[]>);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export type Fetch<TEnv = unknown> = (
|
|
182
|
+
req: Request,
|
|
183
|
+
env: TEnv,
|
|
184
|
+
ctx: ExecutionContext,
|
|
185
|
+
) => Promise<Response> | Response;
|
|
186
|
+
|
|
187
|
+
export interface AppContext<TEnv extends DefaultEnv = DefaultEnv> {
|
|
188
|
+
env: TEnv;
|
|
189
|
+
ctx: { waitUntil: (promise: Promise<unknown>) => void };
|
|
190
|
+
req?: Request;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const configurationToolsFor = <TSchema extends z.ZodTypeAny = never>({
|
|
194
|
+
state: schema,
|
|
195
|
+
scopes,
|
|
196
|
+
onChange,
|
|
197
|
+
}: CreateMCPServerOptions<
|
|
198
|
+
any,
|
|
199
|
+
TSchema
|
|
200
|
+
>["configuration"] = {}): CreatedTool[] => {
|
|
201
|
+
const jsonSchema = schema
|
|
202
|
+
? zodToJsonSchema(schema)
|
|
203
|
+
: { type: "object", properties: {} };
|
|
204
|
+
return [
|
|
205
|
+
...(onChange
|
|
206
|
+
? [
|
|
207
|
+
createTool({
|
|
208
|
+
id: "ON_MCP_CONFIGURATION",
|
|
209
|
+
description: "MCP Configuration On Change",
|
|
210
|
+
inputSchema: z.object({
|
|
211
|
+
state: schema ?? z.unknown(),
|
|
212
|
+
scopes: z
|
|
213
|
+
.array(z.string())
|
|
214
|
+
.describe(
|
|
215
|
+
"Array of scopes in format 'KEY::SCOPE' (e.g., 'GMAIL::GetCurrentUser')",
|
|
216
|
+
),
|
|
217
|
+
}),
|
|
218
|
+
outputSchema: z.object({}),
|
|
219
|
+
execute: async (input) => {
|
|
220
|
+
await onChange(input.runtimeContext.env, {
|
|
221
|
+
state: input.context.state,
|
|
222
|
+
scopes: input.context.scopes,
|
|
223
|
+
});
|
|
224
|
+
return Promise.resolve({});
|
|
225
|
+
},
|
|
226
|
+
}),
|
|
227
|
+
]
|
|
228
|
+
: []),
|
|
229
|
+
createTool({
|
|
230
|
+
id: "MCP_CONFIGURATION",
|
|
231
|
+
description: "MCP Configuration",
|
|
232
|
+
inputSchema: z.object({}),
|
|
233
|
+
outputSchema: z.object({
|
|
234
|
+
stateSchema: z.unknown(),
|
|
235
|
+
scopes: z.array(z.string()).optional(),
|
|
236
|
+
}),
|
|
237
|
+
execute: () => {
|
|
238
|
+
return Promise.resolve({
|
|
239
|
+
stateSchema: jsonSchema,
|
|
240
|
+
scopes,
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
];
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
type CallTool = (opts: {
|
|
248
|
+
toolCallId: string;
|
|
249
|
+
toolCallInput: unknown;
|
|
250
|
+
}) => Promise<unknown>;
|
|
251
|
+
|
|
252
|
+
export type MCPServer<TEnv = unknown, TSchema extends z.ZodTypeAny = never> = {
|
|
253
|
+
fetch: Fetch<TEnv & DefaultEnv<TSchema>>;
|
|
254
|
+
callTool: CallTool;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
export const createMCPServer = <
|
|
258
|
+
TEnv = unknown,
|
|
259
|
+
TSchema extends z.ZodTypeAny = never,
|
|
260
|
+
>(
|
|
261
|
+
options: CreateMCPServerOptions<TEnv, TSchema>,
|
|
262
|
+
): MCPServer<TEnv, TSchema> => {
|
|
263
|
+
const createServer = async (bindings: TEnv & DefaultEnv<TSchema>) => {
|
|
264
|
+
await options.before?.(bindings);
|
|
265
|
+
|
|
266
|
+
const server = new McpServer(
|
|
267
|
+
{ name: "@deco/mcp-api", version: "1.0.0" },
|
|
268
|
+
{ capabilities: { tools: {} } },
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const toolsFn =
|
|
272
|
+
typeof options.tools === "function"
|
|
273
|
+
? options.tools
|
|
274
|
+
: async (bindings: TEnv & DefaultEnv<TSchema>) => {
|
|
275
|
+
if (typeof options.tools === "function") {
|
|
276
|
+
return await options.tools(bindings);
|
|
277
|
+
}
|
|
278
|
+
return await Promise.all(
|
|
279
|
+
options.tools?.flatMap(async (tool) => {
|
|
280
|
+
const toolResult = tool(bindings);
|
|
281
|
+
const awaited = await toolResult;
|
|
282
|
+
if (Array.isArray(awaited)) {
|
|
283
|
+
return awaited;
|
|
284
|
+
}
|
|
285
|
+
return [awaited];
|
|
286
|
+
}) ?? [],
|
|
287
|
+
).then((t) => t.flat());
|
|
288
|
+
};
|
|
289
|
+
const tools = await toolsFn(bindings);
|
|
290
|
+
|
|
291
|
+
tools.push(...configurationToolsFor<TSchema>(options.configuration));
|
|
292
|
+
|
|
293
|
+
for (const tool of tools) {
|
|
294
|
+
server.registerTool(
|
|
295
|
+
tool.id,
|
|
296
|
+
{
|
|
297
|
+
_meta: {
|
|
298
|
+
streamable: isStreamableTool(tool),
|
|
299
|
+
},
|
|
300
|
+
description: tool.description,
|
|
301
|
+
inputSchema:
|
|
302
|
+
tool.inputSchema && "shape" in tool.inputSchema
|
|
303
|
+
? (tool.inputSchema.shape as z.ZodRawShape)
|
|
304
|
+
: z.object({}).shape,
|
|
305
|
+
outputSchema: isStreamableTool(tool)
|
|
306
|
+
? z.object({ bytes: z.record(z.string(), z.number()) }).shape
|
|
307
|
+
: tool.outputSchema &&
|
|
308
|
+
typeof tool.outputSchema === "object" &&
|
|
309
|
+
"shape" in tool.outputSchema
|
|
310
|
+
? (tool.outputSchema.shape as z.ZodRawShape)
|
|
311
|
+
: z.object({}).shape,
|
|
312
|
+
},
|
|
313
|
+
async (args) => {
|
|
314
|
+
let result = await tool.execute({
|
|
315
|
+
context: args,
|
|
316
|
+
runtimeContext: createRuntimeContext(),
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
if (isStreamableTool(tool) && result instanceof Response) {
|
|
320
|
+
result = { bytes: await result.bytes() };
|
|
321
|
+
}
|
|
322
|
+
return {
|
|
323
|
+
structuredContent: result as Record<string, unknown>,
|
|
324
|
+
content: [
|
|
325
|
+
{
|
|
326
|
+
type: "text",
|
|
327
|
+
text: JSON.stringify(result),
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
};
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return { server, tools };
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const fetch = async (
|
|
339
|
+
req: Request,
|
|
340
|
+
env: TEnv & DefaultEnv<TSchema>,
|
|
341
|
+
_ctx: ExecutionContext,
|
|
342
|
+
) => {
|
|
343
|
+
const { server } = await createServer(env);
|
|
344
|
+
const transport = new HttpServerTransport();
|
|
345
|
+
|
|
346
|
+
await server.connect(transport);
|
|
347
|
+
|
|
348
|
+
return await transport.handleMessage(req);
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
const callTool: CallTool = async ({ toolCallId, toolCallInput }) => {
|
|
352
|
+
const currentState = State.getStore();
|
|
353
|
+
if (!currentState) {
|
|
354
|
+
throw new Error("Missing state, did you forget to call State.bind?");
|
|
355
|
+
}
|
|
356
|
+
const env = currentState?.env;
|
|
357
|
+
const { tools } = await createServer(env as TEnv & DefaultEnv<TSchema>);
|
|
358
|
+
const tool = tools.find((t) => t.id === toolCallId);
|
|
359
|
+
const execute = tool?.execute;
|
|
360
|
+
if (!execute) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`Tool ${toolCallId} not found or does not have an execute function`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return execute({
|
|
367
|
+
context: toolCallInput,
|
|
368
|
+
runtimeContext: createRuntimeContext(),
|
|
369
|
+
});
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
fetch,
|
|
374
|
+
callTool,
|
|
375
|
+
};
|
|
376
|
+
};
|
package/src/wrangler.ts
CHANGED
|
@@ -2,20 +2,20 @@ export interface BindingBase {
|
|
|
2
2
|
name: string;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
export interface
|
|
5
|
+
export interface MCPConnectionBinding extends BindingBase {
|
|
6
6
|
type: "mcp";
|
|
7
7
|
/**
|
|
8
8
|
* If not provided, will return a function that takes the integration id and return the binding implementation..
|
|
9
9
|
*/
|
|
10
|
-
|
|
10
|
+
connection_id: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export interface
|
|
13
|
+
export interface MCPAppBinding extends BindingBase {
|
|
14
14
|
type: "mcp";
|
|
15
15
|
/**
|
|
16
16
|
* The name of the integration to bind.
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
app_name: string;
|
|
19
19
|
}
|
|
20
20
|
export interface ContractClause {
|
|
21
21
|
id: string;
|
|
@@ -36,7 +36,7 @@ export interface ContractBinding extends BindingBase {
|
|
|
36
36
|
contract: Contract;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export type MCPBinding =
|
|
39
|
+
export type MCPBinding = MCPConnectionBinding | MCPAppBinding;
|
|
40
40
|
|
|
41
41
|
export type Binding = MCPBinding | ContractBinding;
|
|
42
42
|
|
package/tsconfig.json
CHANGED
package/src/admin.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { createChannel } from "bidc";
|
|
2
|
-
|
|
3
|
-
export const requestMissingScopes = ({ scopes }: { scopes: string[] }) => {
|
|
4
|
-
try {
|
|
5
|
-
const channel = createChannel();
|
|
6
|
-
channel.send({
|
|
7
|
-
type: "request_missing_scopes",
|
|
8
|
-
payload: {
|
|
9
|
-
scopes,
|
|
10
|
-
},
|
|
11
|
-
});
|
|
12
|
-
channel.cleanup();
|
|
13
|
-
} catch (error) {
|
|
14
|
-
console.error("Failed to request missing scopes", error);
|
|
15
|
-
}
|
|
16
|
-
};
|
package/src/auth.ts
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
import { JWK, jwtVerify } from "jose";
|
|
2
|
-
import type { DefaultEnv } from "./index.ts";
|
|
3
|
-
|
|
4
|
-
const DECO_APP_AUTH_COOKIE_NAME = "deco_page_auth";
|
|
5
|
-
const MAX_COOKIE_SIZE = 4000; // Leave some buffer below the 4096 limit
|
|
6
|
-
|
|
7
|
-
export interface State {
|
|
8
|
-
next?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const StateParser = {
|
|
12
|
-
parse: (state: string) => {
|
|
13
|
-
return JSON.parse(decodeURIComponent(atob(state))) as State;
|
|
14
|
-
},
|
|
15
|
-
stringify: (state: State) => {
|
|
16
|
-
return btoa(encodeURIComponent(JSON.stringify(state)));
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
// Helper function to chunk a value into multiple cookies
|
|
21
|
-
const chunkValue = (value: string): string[] => {
|
|
22
|
-
if (value.length <= MAX_COOKIE_SIZE) {
|
|
23
|
-
return [value];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const chunks: string[] = [];
|
|
27
|
-
for (let i = 0; i < value.length; i += MAX_COOKIE_SIZE) {
|
|
28
|
-
chunks.push(value.slice(i, i + MAX_COOKIE_SIZE));
|
|
29
|
-
}
|
|
30
|
-
return chunks;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// Helper function to reassemble chunked cookies
|
|
34
|
-
const reassembleChunkedCookies = (
|
|
35
|
-
cookies: Record<string, string>,
|
|
36
|
-
baseName: string,
|
|
37
|
-
): string | undefined => {
|
|
38
|
-
// First try the base cookie (non-chunked)
|
|
39
|
-
if (cookies[baseName]) {
|
|
40
|
-
return cookies[baseName];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Try to reassemble from chunks
|
|
44
|
-
const chunks: string[] = [];
|
|
45
|
-
let index = 0;
|
|
46
|
-
|
|
47
|
-
while (true) {
|
|
48
|
-
const chunkName = `${baseName}_${index}`;
|
|
49
|
-
if (!cookies[chunkName]) {
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
chunks.push(cookies[chunkName]);
|
|
53
|
-
index++;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return chunks.length > 0 ? chunks.join("") : undefined;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
// Helper function to parse cookies from request
|
|
60
|
-
const parseCookies = (cookieHeader: string): Record<string, string> => {
|
|
61
|
-
const cookies: Record<string, string> = {};
|
|
62
|
-
if (!cookieHeader) return cookies;
|
|
63
|
-
|
|
64
|
-
cookieHeader.split(";").forEach((cookie) => {
|
|
65
|
-
const [name, ...rest] = cookie.trim().split("=");
|
|
66
|
-
if (name && rest.length > 0) {
|
|
67
|
-
cookies[name] = decodeURIComponent(rest.join("="));
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
return cookies;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
const parseJWK = (jwk: string): JWK => JSON.parse(atob(jwk)) as JWK;
|
|
75
|
-
|
|
76
|
-
export const getReqToken = async (req: Request, env: DefaultEnv) => {
|
|
77
|
-
const token = () => {
|
|
78
|
-
// First try to get token from Authorization header
|
|
79
|
-
const authHeader = req.headers.get("Authorization");
|
|
80
|
-
if (authHeader) {
|
|
81
|
-
return authHeader.split(" ")[1];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// If not found, try to get from cookie
|
|
85
|
-
const cookieHeader = req.headers.get("Cookie");
|
|
86
|
-
if (cookieHeader) {
|
|
87
|
-
const cookies = parseCookies(cookieHeader);
|
|
88
|
-
return reassembleChunkedCookies(cookies, DECO_APP_AUTH_COOKIE_NAME);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return undefined;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
const authToken = token();
|
|
95
|
-
if (!authToken) {
|
|
96
|
-
return undefined;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
env.DECO_API_JWT_PUBLIC_KEY &&
|
|
100
|
-
(await jwtVerify(authToken, parseJWK(env.DECO_API_JWT_PUBLIC_KEY), {
|
|
101
|
-
issuer: "https://api.decocms.com",
|
|
102
|
-
algorithms: ["RS256"],
|
|
103
|
-
typ: "JWT",
|
|
104
|
-
}).catch((err) => {
|
|
105
|
-
console.error(
|
|
106
|
-
`[auth-token]: error validating: ${err} ${env.DECO_API_JWT_PUBLIC_KEY}`,
|
|
107
|
-
);
|
|
108
|
-
}));
|
|
109
|
-
|
|
110
|
-
return authToken;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
export interface AuthCallbackOptions {
|
|
114
|
-
apiUrl?: string;
|
|
115
|
-
appName: string;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export const handleAuthCallback = async (
|
|
119
|
-
req: Request,
|
|
120
|
-
options: AuthCallbackOptions,
|
|
121
|
-
): Promise<Response> => {
|
|
122
|
-
const url = new URL(req.url);
|
|
123
|
-
const code = url.searchParams.get("code");
|
|
124
|
-
const state = url.searchParams.get("state");
|
|
125
|
-
|
|
126
|
-
if (!code) {
|
|
127
|
-
return new Response("Missing authorization code", { status: 400 });
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Parse state to get the next URL
|
|
131
|
-
let next = "/";
|
|
132
|
-
if (state) {
|
|
133
|
-
try {
|
|
134
|
-
const parsedState = StateParser.parse(state);
|
|
135
|
-
next = parsedState.next || "/";
|
|
136
|
-
} catch {
|
|
137
|
-
// ignore parse errors
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
// Exchange code for token
|
|
143
|
-
const apiUrl = options.apiUrl ?? "https://api.decocms.com";
|
|
144
|
-
const exchangeResponse = await fetch(`${apiUrl}/apps/code-exchange`, {
|
|
145
|
-
method: "POST",
|
|
146
|
-
headers: {
|
|
147
|
-
"Content-Type": "application/json",
|
|
148
|
-
},
|
|
149
|
-
body: JSON.stringify({
|
|
150
|
-
code,
|
|
151
|
-
client_id: options.appName,
|
|
152
|
-
}),
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
if (!exchangeResponse.ok) {
|
|
156
|
-
console.error(
|
|
157
|
-
"authentication failed",
|
|
158
|
-
code,
|
|
159
|
-
options.appName,
|
|
160
|
-
await exchangeResponse.text().catch((_) => ""),
|
|
161
|
-
);
|
|
162
|
-
return new Response("Authentication failed", { status: 401 });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const { access_token } = (await exchangeResponse.json()) as {
|
|
166
|
-
access_token: string;
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
if (!access_token) {
|
|
170
|
-
return new Response("No access token received", { status: 401 });
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// Chunk the token if it's too large
|
|
174
|
-
const chunks = chunkValue(access_token);
|
|
175
|
-
const headers = new Headers();
|
|
176
|
-
headers.set("Location", next);
|
|
177
|
-
|
|
178
|
-
// Set cookies for each chunk
|
|
179
|
-
if (chunks.length === 1) {
|
|
180
|
-
// Single cookie for small tokens
|
|
181
|
-
headers.set(
|
|
182
|
-
"Set-Cookie",
|
|
183
|
-
`${DECO_APP_AUTH_COOKIE_NAME}=${access_token}; HttpOnly; SameSite=None; Secure; Path=/`,
|
|
184
|
-
);
|
|
185
|
-
} else {
|
|
186
|
-
// Multiple cookies for large tokens
|
|
187
|
-
chunks.forEach((chunk, index) => {
|
|
188
|
-
headers.append(
|
|
189
|
-
"Set-Cookie",
|
|
190
|
-
`${DECO_APP_AUTH_COOKIE_NAME}_${index}=${chunk}; HttpOnly; SameSite=None; Secure; Path=/`,
|
|
191
|
-
);
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return new Response(null, {
|
|
196
|
-
status: 302,
|
|
197
|
-
headers,
|
|
198
|
-
});
|
|
199
|
-
} catch (err) {
|
|
200
|
-
return new Response(`Authentication failed ${err}`, { status: 500 });
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
const removeAuthCookie = (headers: Headers) => {
|
|
205
|
-
// Clear the base cookie
|
|
206
|
-
headers.append(
|
|
207
|
-
"Set-Cookie",
|
|
208
|
-
`${DECO_APP_AUTH_COOKIE_NAME}=; HttpOnly; SameSite=None; Secure; Path=/; Max-Age=0`,
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
// Clear all potential chunked cookies
|
|
212
|
-
// We'll try to clear up to 10 chunks (which would support tokens up to 40KB)
|
|
213
|
-
// This is a reasonable upper limit
|
|
214
|
-
for (let i = 0; i < 10; i++) {
|
|
215
|
-
headers.append(
|
|
216
|
-
"Set-Cookie",
|
|
217
|
-
`${DECO_APP_AUTH_COOKIE_NAME}_${i}=; HttpOnly; SameSite=None; Secure; Path=/; Max-Age=0`,
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
export const handleLogout = (req: Request) => {
|
|
223
|
-
const url = new URL(req.url);
|
|
224
|
-
const next = url.searchParams.get("next");
|
|
225
|
-
const redirectTo = new URL("/", url);
|
|
226
|
-
const headers = new Headers();
|
|
227
|
-
removeAuthCookie(headers);
|
|
228
|
-
headers.set("Location", next ?? redirectTo.href);
|
|
229
|
-
return new Response(null, {
|
|
230
|
-
status: 302,
|
|
231
|
-
headers,
|
|
232
|
-
});
|
|
233
|
-
};
|